From 9f00369fa1a55de07b144c84400a5b624607d0e8 Mon Sep 17 00:00:00 2001 From: codes-factory-of-bg Date: Thu, 6 Aug 2026 17:28:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?chore(install):=20=E7=AE=80=E5=8C=96=20pip?= =?UTF-8?q?=20=E9=80=BB=E8=BE=91=EF=BC=8C=E6=97=A0=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E5=8A=A0=20--break-system-packages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PEP 668 探测分支 + 兜底重试多出一步和报错输出,容易吓到小白。 现代系统(Homebrew/Debian/PEP 668)几乎都不允许 pip install --user, 直接无条件加 --break-system-packages 跨过;--user 限目标目录,不碰 系统 site-packages,安全。 Co-Authored-By: AtomCode (GLM-5.2) --- scripts/install-atomgit.sh | 43 +++++++------------------------------- scripts/install.sh | 43 +++++++------------------------------- 2 files changed, 16 insertions(+), 70 deletions(-) diff --git a/scripts/install-atomgit.sh b/scripts/install-atomgit.sh index c6487f8e..720b68db 100755 --- a/scripts/install-atomgit.sh +++ b/scripts/install-atomgit.sh @@ -473,44 +473,17 @@ install_python_deps() { ui_warn "$PY_BIN -m pip 不可用,跳过 python 依赖" return 0 fi - ui_info "Installing python skill deps via $PY_BIN (--user)" + ui_info "Installing python skill deps via $PY_BIN (--user --break-system-packages)" # --no-warn-script-location 抑制 'script installed in .../bin which is not on PATH'(小白噪音) # --disable-pip-version-check 抑制 pip 升级提示 - # PEP 668 兜底:Homebrew/Debian 等 externally-managed 环境下,pip install --user 仍被拒, - # 需加 --break-system-packages 才能写用户目录(--user 限目标,不碰系统 site-packages,安全)。 - local pep668_flag="" - local pyver; pyver="$("$PY_BIN" -c 'import sys;print("%d.%d"%sys.version_info[:2])' 2>/dev/null || echo "")" - if [ -n "$pyver" ]; then - local inc_dir - inc_dir="$("$PY_BIN" -c 'import sysconfig;print(sysconfig.get_path("include"))' 2>/dev/null || echo "")" - local sys_prefix - sys_prefix="$("$PY_BIN" -c 'import sys;print(sys.prefix)' 2>/dev/null || echo "")" - # EXTERNALLY-MANAGED 标记文件在 stdlib 目录(python3.x/)下 - if [ -n "$inc_dir" ] && [ -f "$(dirname "$inc_dir")/EXTERNALLY-MANAGED" ]; then - pep668_flag="--break-system-packages" - ui_info "检测到 PEP 668 externally-managed 环境,加 --break-system-packages" - fi - fi - # 兜底:上面探测失败时,pip 报 externally-managed-environment 退出码 1,重试加 flag - local pip_args=(--user --no-warn-script-location --disable-pip-version-check - -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com) - [ -n "$pep668_flag" ] && pip_args+=("$pep668_flag") - if ! printf '%s' "$merged" | sort -u | grep -vE '^\s*#|^\s*$' | \ - "$PY_BIN" -m pip install "${pip_args[@]}" -r /dev/stdin; then - # 首次没加 flag 且报 externally-managed,重试加 flag - if [ -z "$pep668_flag" ]; then - ui_warn "pip install 失败,尝试加 --break-system-packages 重试..." - if printf '%s' "$merged" | sort -u | grep -vE '^\s*#|^\s*$' | \ - "$PY_BIN" -m pip install --user --break-system-packages \ - --no-warn-script-location --disable-pip-version-check \ - -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \ - -r /dev/stdin; then - ui_success "Python deps done (with --break-system-packages)" - return 0 - fi - fi + # PEP 668:现代系统(Homebrew/Debian/PEP 668)下 pip install --user 仍被拒, + # 直接加 --break-system-packages 跨过;--user 限目标目录,不碰系统 site-packages,安全。 + printf '%s' "$merged" | sort -u | grep -vE '^\s*#|^\s*$' | \ + "$PY_BIN" -m pip install --user --break-system-packages \ + --no-warn-script-location --disable-pip-version-check \ + -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \ + -r /dev/stdin || \ ui_warn "pip install 部分失败,可稍后手动补" - fi ui_success "Python deps done" } diff --git a/scripts/install.sh b/scripts/install.sh index 04eb20d3..94d8639a 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -472,44 +472,17 @@ install_python_deps() { ui_warn "$PY_BIN -m pip 不可用,跳过 python 依赖" return 0 fi - ui_info "Installing python skill deps via $PY_BIN (--user)" + ui_info "Installing python skill deps via $PY_BIN (--user --break-system-packages)" # --no-warn-script-location 抑制 'script installed in .../bin which is not on PATH'(小白噪音) # --disable-pip-version-check 抑制 pip 升级提示 - # PEP 668 兜底:Homebrew/Debian 等 externally-managed 环境下,pip install --user 仍被拒, - # 需加 --break-system-packages 才能写用户目录(--user 限目标,不碰系统 site-packages,安全)。 - local pep668_flag="" - local pyver; pyver="$("$PY_BIN" -c 'import sys;print("%d.%d"%sys.version_info[:2])' 2>/dev/null || echo "")" - if [ -n "$pyver" ]; then - local inc_dir - inc_dir="$("$PY_BIN" -c 'import sysconfig;print(sysconfig.get_path("include"))' 2>/dev/null || echo "")" - local sys_prefix - sys_prefix="$("$PY_BIN" -c 'import sys;print(sys.prefix)' 2>/dev/null || echo "")" - # EXTERNALLY-MANAGED 标记文件在 stdlib 目录(python3.x/)下 - if [ -n "$inc_dir" ] && [ -f "$(dirname "$inc_dir")/EXTERNALLY-MANAGED" ]; then - pep668_flag="--break-system-packages" - ui_info "检测到 PEP 668 externally-managed 环境,加 --break-system-packages" - fi - fi - # 兜底:上面探测失败时,pip 报 externally-managed-environment 退出码 1,重试加 flag - local pip_args=(--user --no-warn-script-location --disable-pip-version-check - -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com) - [ -n "$pep668_flag" ] && pip_args+=("$pep668_flag") - if ! printf '%s' "$merged" | sort -u | grep -vE '^\s*#|^\s*$' | \ - "$PY_BIN" -m pip install "${pip_args[@]}" -r /dev/stdin; then - # 首次没加 flag 且报 externally-managed,重试加 flag - if [ -z "$pep668_flag" ]; then - ui_warn "pip install 失败,尝试加 --break-system-packages 重试..." - if printf '%s' "$merged" | sort -u | grep -vE '^\s*#|^\s*$' | \ - "$PY_BIN" -m pip install --user --break-system-packages \ - --no-warn-script-location --disable-pip-version-check \ - -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \ - -r /dev/stdin; then - ui_success "Python deps done (with --break-system-packages)" - return 0 - fi - fi + # PEP 668:现代系统(Homebrew/Debian/PEP 668)下 pip install --user 仍被拒, + # 直接加 --break-system-packages 跨过;--user 限目标目录,不碰系统 site-packages,安全。 + printf '%s' "$merged" | sort -u | grep -vE '^\s*#|^\s*$' | \ + "$PY_BIN" -m pip install --user --break-system-packages \ + --no-warn-script-location --disable-pip-version-check \ + -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \ + -r /dev/stdin || \ ui_warn "pip install 部分失败,可稍后手动补" - fi ui_success "Python deps done" } From 11ca1e1758686468c402dd15758fe96d0d3fb19a Mon Sep 17 00:00:00 2001 From: codes-factory-of-bg Date: Thu, 6 Aug 2026 22:57:57 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(install):=20=E4=BF=AE=E5=9B=9B=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=B5=8B=E8=AF=95=E5=8F=91=E7=8E=B0=E7=9A=84=204=20?= =?UTF-8?q?=E4=B8=AA=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug1: PS1 .env/daemon.env 不换行(install.ps1 / install-atomgit.ps1) - 根因:Where-Object 过滤掉所有行时返回 $null,$null += "line" 静默失败; Set-Content -Encoding UTF8 在 PS 5.1 写 BOM,gateway.cmd `call daemon.env` 解析 BOM 前缀的首行失败 - 修法:封装 Write-EnvFile 辅助函数,用 [System.IO.File]::WriteAllText + UTF8Encoding($false) 显式 CRLF 换行、无 BOM;@(...) 强制数组 Bug2: PS1 没创建 ~/.openclaw/bin 技能目录(两个 PS1) - 根因:PS1 路线没调 apply-addons.sh 的 expose_skill_wrappers,缺这步 - 修法:加 Expose-SkillWrappers 函数,用 .cmd shim 替代 symlink (Windows symlink 需管理员权限),在 Run-SetupCrew 之后调用; 同时把 ~/.openclaw/bin 注入用户 PATH Bug3: PS1 XIAOBEI_FORCE=1 不生效(两个 PS1) - 根因:$Force 等在顶层赋值时是 script-scope,但函数内 bare $Force 读的是 local $null,所以 XIAOBEI_FORCE=1 永远不生效 - 修法:改成 $script:Force 等前缀,所有引用点也改 $script: Bug4: macOS 第11步 OPENCLAW_HOME warning 噪音(两个 .sh) - 根因:用户 shell export 了 OPENCLAW_HOME=~/.openclaw,脚本命中 if 打 3 行 ui_warn;但脚本内部用的是未 export 的内部变量, 实际安装结构正确——warning 纯噪音 - 修法:3 行 ui_warn 降级为 1 行 ui_info,提示用户清理 shell 环境变量 Co-Authored-By: AtomCode (GLM-5.2) --- scripts/install-atomgit.ps1 | 82 ++++++++++++++++++++++---- scripts/install-atomgit.sh | 6 +- scripts/install.ps1 | 112 ++++++++++++++++++++++++++++++------ scripts/install.sh | 6 +- 4 files changed, 172 insertions(+), 34 deletions(-) diff --git a/scripts/install-atomgit.ps1 b/scripts/install-atomgit.ps1 index 1e19e71a..f48ec04c 100755 --- a/scripts/install-atomgit.ps1 +++ b/scripts/install-atomgit.ps1 @@ -58,11 +58,15 @@ $AtomgitApi = "https://api.atomgit.com/api/v5/repos/wiseflow/xiaobei" $Root = if ($env:XIAOBEI_HOME) { $env:XIAOBEI_HOME } else { Join-Path $env:USERPROFILE "xiaobei" } $OpenclawHome = if ($env:OPENCLAW_HOME) { $env:OPENCLAW_HOME } else { Join-Path $env:USERPROFILE ".openclaw" } -# Behavior switches (env vars, =1 to enable) -$Force = ($env:XIAOBEI_FORCE -eq "1" -or $env:XIAOBEI_FORCE -eq "true") -$SkipBind = ($env:XIAOBEI_SKIP_BIND -eq "1" -or $env:XIAOBEI_SKIP_BIND -eq "true") -$SkipBrowser = ($env:XIAOBEI_SKIP_BROWSER -eq "1" -or $env:XIAOBEI_SKIP_BROWSER -eq "true") -$NoPrompt = ($env:XIAOBEI_NO_PROMPT -eq "1" -or $env:XIAOBEI_NO_PROMPT -eq "true") +# Behavior switches (env vars, =1 to enable). +# IMPORTANT: use $script: scope so these are visible inside functions (Place-Config, +# Main, etc.). A bare $Force at the top level creates a *script-scope* variable that +# functions CANNOT see - they'd read a local $null instead, so XIAOBEI_FORCE=1 etc. +# would silently never take effect (a real bug observed in testing). +$script:Force = ($env:XIAOBEI_FORCE -eq "1" -or $env:XIAOBEI_FORCE -eq "true") +$script:SkipBind = ($env:XIAOBEI_SKIP_BIND -eq "1" -or $env:XIAOBEI_SKIP_BIND -eq "true") +$script:SkipBrowser = ($env:XIAOBEI_SKIP_BROWSER -eq "1" -or $env:XIAOBEI_SKIP_BROWSER -eq "true") +$script:NoPrompt = ($env:XIAOBEI_NO_PROMPT -eq "1" -or $env:XIAOBEI_NO_PROMPT -eq "true") $NodeExe = Join-Path $Root "tools\node\node.exe" $NpmCmd = Join-Path $Root "tools\node\npm.cmd" @@ -216,7 +220,7 @@ function Place-Config { $needPlace = $false; $reason = "" if (-not (Test-Path $cfg)) { $needPlace = $true; $reason = "not present" } - elseif ($Force) { $needPlace = $true; $reason = "XIAOBEI_FORCE=1" } + elseif ($script:Force) { $needPlace = $true; $reason = "XIAOBEI_FORCE=1" } else { try { # Must explicitly read as UTF-8: openclaw writes its config as UTF-8 without BOM, while @@ -318,9 +322,66 @@ function Run-SetupCrew { else { Write-Ok "crew templates set up" } } +# --- 7b. Expose skill wrappers as .cmd shims into ~\.openclaw\bin --- +# Mirrors the .sh route's expose_skill_wrappers + ensure_openclaw_bin_in_path +# (scripts/lib/skill-wrappers.sh). On Windows we cannot rely on symlinks +# (need admin/Developer Mode), so we generate a .cmd shim per skill +# that forwards to the bundled bash + the skill's .sh wrapper. +# The shim lives in ~\.openclaw\bin so the agent can call ` ` +# from anywhere on PATH. +function Expose-SkillWrappers { + $binDir = Join-Path $OpenclawHome "bin" + New-Item -ItemType Directory -Force -Path $binDir | Out-Null + + # The bundled bash shipped in the tarball (Git Bash fallback handled below). + $bashExe = $false + $bashCandidates = @( + (Join-Path $Root "tools\git\bin\bash.exe"), + "C:\Program Files\Git\bin\bash.exe", + "C:\Program Files\Git\usr\bin\bash.exe" + ) + foreach ($c in $bashCandidates) { if (Test-Path $c) { $bashExe = $c; break } } + if (-not $bashExe) { $bashExe = (Get-Command bash.exe -ErrorAction SilentlyContinue).Source } + if (-not $bashExe) { + Write-Warn "no bash found; cannot expose skill wrappers as .cmd shims" + return + } + + $exposed = 0 + foreach ($skillsRoot in @((Join-Path $Root "skills"), (Join-Path $Root "crews\main\skills"))) { + if (-not (Test-Path $skillsRoot)) { continue } + Get-ChildItem -Path $skillsRoot -Directory | ForEach-Object { + $skillName = $_.Name + if ($skillName.StartsWith("_")) { return } # skip _shared etc + $wrapper = Join-Path $_.FullName "$skillName.sh" + if (-not (Test-Path $wrapper)) { return } + $shim = Join-Path $binDir "$skillName.cmd" + $wrapperFwd = ($wrapper -replace '\\', '/') + $bashFwd = ($bashExe -replace '\\', '/') + $body = "@echo off`r`n`"$bashFwd`" `"$wrapperFwd`" %*`r`n" + $enc = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($shim, $body, $enc) + $exposed++ + } + } + + if ($exposed -gt 0) { + Write-Ok "exposed $exposed skill wrapper(s) -> $binDir" + # Inject ~\.openclaw\bin into user PATH (idempotent, safe non-truncating way) + $userPath = [Environment]::GetEnvironmentVariable("PATH", "User") + if ($userPath -notlike "*$binDir*") { + $newPath = "$binDir;$userPath" + [Environment]::SetEnvironmentVariable("PATH", $newPath, "User") + Write-Ok "added $binDir to user PATH (takes effect in a new terminal)" + } + } else { + Write-Host " [i] no skill wrappers found to expose" -ForegroundColor Yellow + } +} + # --- 8. camoufox-cli --- function Install-CamoufoxCli { - if ($SkipBrowser) { + if ($script:SkipBrowser) { Write-Host " [i] skipping camoufox-cli browser binary (XIAOBEI_SKIP_BROWSER=1); run manually later: camoufox-cli install" -ForegroundColor Yellow return } @@ -475,7 +536,7 @@ function Install-GatewayAndEnv { # --- Interactive AWK_API_KEY prompt --- $awkKey = $env:AWK_API_KEY - if (-not $NoPrompt -and -not $awkKey) { + if (-not $script:NoPrompt -and -not $awkKey) { $awkKey = Read-Host "Enter AWK_API_KEY (Volces ARK API key)" } @@ -549,7 +610,7 @@ function Test-WeixinBound { } function Bind-WeixinChannel { - if ($SkipBind -or $NoPrompt) { + if ($script:SkipBind -or $script:NoPrompt) { Write-Host " [i] skipping WeChat scan-to-bind (XIAOBEI_SKIP_BIND / XIAOBEI_NO_PROMPT=1); run manually later: openclaw channels login --channel openclaw-weixin" -ForegroundColor Yellow return } @@ -576,7 +637,7 @@ function Main { # Detect whether already installed (decides update vs fresh install) $cfgExisting = Join-Path $OpenclawHome "openclaw.json" - $isUpdate = (Test-Path $cfgExisting) -and -not $Force + $isUpdate = (Test-Path $cfgExisting) -and -not $script:Force if ($isUpdate) { Write-Warn "detected existing install ($cfgExisting) -> taking the update route, preserving runtime data (XIAOBEI_FORCE=1 to force overwrite)" # Stop the gateway first (mirrors install.sh stop_gateway_if_running): otherwise tar @@ -624,6 +685,7 @@ function Main { } else { Place-Config Run-SetupCrew + Expose-SkillWrappers Install-GatewayAndEnv Bind-WeixinChannel } diff --git a/scripts/install-atomgit.sh b/scripts/install-atomgit.sh index 720b68db..599b0484 100755 --- a/scripts/install-atomgit.sh +++ b/scripts/install-atomgit.sh @@ -1373,12 +1373,10 @@ install_gateway_and_env() { # 引擎 resolveStateDir 把 OPENCLAW_HOME 当 homedir 再 append /.openclaw(见 # openclaw/src/config/paths.ts),故若用户 shell export 了 OPENCLAW_HOME=~/.openclaw, # 会产出嵌套路径。本脚本顶部已把 OPENCLAW_HOME 设成内部变量且未 export,不会传给引擎子进程, - # 所以这里只打 warning 提示用户清理 shell 环境,不能 unset——unset 会误伤脚本内部变量, + # 所以这里只打 info 提示用户清理 shell 环境变量,不能 unset——unset 会误伤脚本内部变量, # 导致后续 ${OPENCLAW_HOME//...} 在 set -u 下报 unbound variable。 if [ -n "${OPENCLAW_HOME:-}" ] && [ "$(cd "${OPENCLAW_HOME}" && pwd)" = "$(cd "$HOME/.openclaw" && pwd)" ]; then - ui_warn "检测到 OPENCLAW_HOME=$OPENCLAW_HOME 已设成 state dir 路径" - ui_warn " 这会让引擎嵌套 append /.openclaw → ~/.openclaw/.openclaw/,config 全落错位置" - ui_warn " 正解:清掉 OPENCLAW_HOME,改用 OPENCLAW_STATE_DIR 显式指定 state dir" + ui_info "检测到 shell 里 export OPENCLAW_HOME=$OPENCLAW_HOME(不影响本次安装,但建议清掉避免引擎嵌套)" fi # redirect stdin from /dev/tty so interactive read 工作于 curl|bash diff --git a/scripts/install.ps1 b/scripts/install.ps1 index f6b78e0f..a2a1c347 100755 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -62,11 +62,15 @@ $Repo = if ($env:XIAOBEI_REPO) { $env:XIAOBEI_REPO } else { "TeamWiseFlow/xiaobe $Root = if ($env:XIAOBEI_HOME) { $env:XIAOBEI_HOME } else { Join-Path $env:USERPROFILE "xiaobei" } $OpenclawHome = if ($env:OPENCLAW_HOME) { $env:OPENCLAW_HOME } else { Join-Path $env:USERPROFILE ".openclaw" } -# Behavior switches (env vars, =1 to enable) -$Force = ($env:XIAOBEI_FORCE -eq "1" -or $env:XIAOBEI_FORCE -eq "true") -$SkipBind = ($env:XIAOBEI_SKIP_BIND -eq "1" -or $env:XIAOBEI_SKIP_BIND -eq "true") -$SkipBrowser = ($env:XIAOBEI_SKIP_BROWSER -eq "1" -or $env:XIAOBEI_SKIP_BROWSER -eq "true") -$NoPrompt = ($env:XIAOBEI_NO_PROMPT -eq "1" -or $env:XIAOBEI_NO_PROMPT -eq "true") +# Behavior switches (env vars, =1 to enable). +# IMPORTANT: use $script: scope so these are visible inside functions (Place-Config, +# Main, etc.). A bare $Force at the top level creates a *script-scope* variable that +# functions CANNOT see - they'd read a local $null instead, so XIAOBEI_FORCE=1 etc. +# would silently never take effect (a real bug observed in testing). +$script:Force = ($env:XIAOBEI_FORCE -eq "1" -or $env:XIAOBEI_FORCE -eq "true") +$script:SkipBind = ($env:XIAOBEI_SKIP_BIND -eq "1" -or $env:XIAOBEI_SKIP_BIND -eq "true") +$script:SkipBrowser = ($env:XIAOBEI_SKIP_BROWSER -eq "1" -or $env:XIAOBEI_SKIP_BROWSER -eq "true") +$script:NoPrompt = ($env:XIAOBEI_NO_PROMPT -eq "1" -or $env:XIAOBEI_NO_PROMPT -eq "true") $NodeExe = Join-Path $Root "tools\node\node.exe" $NpmCmd = Join-Path $Root "tools\node\npm.cmd" @@ -79,6 +83,21 @@ function Write-Ok([string]$msg) { Write-Host " [OK] $msg" -ForegroundColor G function Write-Warn([string]$msg) { Write-Host " [!] $msg" -ForegroundColor Yellow } function Write-Err([string]$msg) { Write-Host " [X] $msg" -ForegroundColor Red } +# Write env file (KEY=value or export KEY=value lines) with explicit CRLF and UTF-8 no BOM. +# Fixes two Set-Content pitfalls observed on Windows PowerShell 5.1: +# 1. Where-Object returning $null (all lines filtered) makes $null += "line" silently fail, +# so the final array is $null and Set-Content writes an empty file. +# 2. Set-Content -Encoding UTF8 writes a BOM (EF BB BF); gateway.cmd `call daemon.env` then +# chokes on the BOM-prefixed first line (OPENCLAW_BROWSER_TIMEOUT_MS=90000). +# Using [System.IO.File]::WriteAllText with UTF8Encoding($false) fixes both: no BOM, and the +# content is a single explicitly-joined string so array/null quirks don't matter. +function Write-EnvFile([string]$path, [string[]]$lines) { + $body = ($lines | Where-Object { $_ }) -join "`r`n" + if ($body) { $body += "`r`n" } + $enc = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($path, $body, $enc) +} + # Run a native command and echo stdout+stderr line by line. pip/npm/camoufox-cli/openclaw normally # write progress and warnings to stderr, and the $ErrorActionPreference="Stop" at the top of this # script would treat stderr lines merged via 2>&1 as terminating errors, throwing @@ -218,7 +237,7 @@ function Place-Config { $needPlace = $false; $reason = "" if (-not (Test-Path $cfg)) { $needPlace = $true; $reason = "not present" } - elseif ($Force) { $needPlace = $true; $reason = "XIAOBEI_FORCE=1" } + elseif ($script:Force) { $needPlace = $true; $reason = "XIAOBEI_FORCE=1" } else { try { # Must explicitly read as UTF-8: openclaw writes its config as UTF-8 without BOM, while @@ -320,9 +339,66 @@ function Run-SetupCrew { else { Write-Ok "crew templates set up" } } +# --- 7b. Expose skill wrappers as .cmd shims into ~\.openclaw\bin --- +# Mirrors the .sh route's expose_skill_wrappers + ensure_openclaw_bin_in_path +# (scripts/lib/skill-wrappers.sh). On Windows we cannot rely on symlinks +# (need admin/Developer Mode), so we generate a .cmd shim per skill +# that forwards to the bundled bash + the skill's .sh wrapper. +# The shim lives in ~\.openclaw\bin so the agent can call ` ` +# from anywhere on PATH. +function Expose-SkillWrappers { + $binDir = Join-Path $OpenclawHome "bin" + New-Item -ItemType Directory -Force -Path $binDir | Out-Null + + # The bundled bash shipped in the tarball (Git Bash fallback handled below). + $bashExe = $false + $bashCandidates = @( + (Join-Path $Root "tools\git\bin\bash.exe"), + "C:\Program Files\Git\bin\bash.exe", + "C:\Program Files\Git\usr\bin\bash.exe" + ) + foreach ($c in $bashCandidates) { if (Test-Path $c) { $bashExe = $c; break } } + if (-not $bashExe) { $bashExe = (Get-Command bash.exe -ErrorAction SilentlyContinue).Source } + if (-not $bashExe) { + Write-Warn "no bash found; cannot expose skill wrappers as .cmd shims" + return + } + + $exposed = 0 + foreach ($skillsRoot in @((Join-Path $Root "skills"), (Join-Path $Root "crews\main\skills"))) { + if (-not (Test-Path $skillsRoot)) { continue } + Get-ChildItem -Path $skillsRoot -Directory | ForEach-Object { + $skillName = $_.Name + if ($skillName.StartsWith("_")) { return } # skip _shared etc + $wrapper = Join-Path $_.FullName "$skillName.sh" + if (-not (Test-Path $wrapper)) { return } + $shim = Join-Path $binDir "$skillName.cmd" + $wrapperFwd = ($wrapper -replace '\\', '/') + $bashFwd = ($bashExe -replace '\\', '/') + $body = "@echo off`r`n`"$bashFwd`" `"$wrapperFwd`" %*`r`n" + $enc = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($shim, $body, $enc) + $exposed++ + } + } + + if ($exposed -gt 0) { + Write-Ok "exposed $exposed skill wrapper(s) -> $binDir" + # Inject ~\.openclaw\bin into user PATH (idempotent, safe non-truncating way) + $userPath = [Environment]::GetEnvironmentVariable("PATH", "User") + if ($userPath -notlike "*$binDir*") { + $newPath = "$binDir;$userPath" + [Environment]::SetEnvironmentVariable("PATH", $newPath, "User") + Write-Ok "added $binDir to user PATH (takes effect in a new terminal)" + } + } else { + Write-Host " [i] no skill wrappers found to expose" -ForegroundColor Yellow + } +} + # --- 8. camoufox-cli --- function Install-CamoufoxCli { - if ($SkipBrowser) { + if ($script:SkipBrowser) { Write-Host " [i] skipping camoufox-cli browser binary (XIAOBEI_SKIP_BROWSER=1); run manually later: camoufox-cli install" -ForegroundColor Yellow return } @@ -477,16 +553,19 @@ function Install-GatewayAndEnv { # --- Interactive AWK_API_KEY prompt --- $awkKey = $env:AWK_API_KEY - if (-not $NoPrompt -and -not $awkKey) { + if (-not $script:NoPrompt -and -not $awkKey) { $awkKey = Read-Host "Enter AWK_API_KEY (Volces ARK API key)" } # --- Write .env (business vars, export format for bash/sh source, read by the bare CLI) --- # Idempotent: strip old lines with the same key first, then append. AWK_API_KEY is a secret, so # it is wrapped in single quotes with any embedded single quote escaped as '\''. + # @(...) forces an array even when Where-Object filters everything (returns $null); + # Write-EnvFile joins with CRLF and writes UTF-8 no BOM (Set-Content UTF8 adds BOM, + # which makes gateway.cmd's `call daemon.env` choke on the BOM-prefixed first line). $exportLines = @() - if (Test-Path $dotEnv) { $exportLines = Get-Content $dotEnv } - $exportLines = $exportLines | Where-Object { $_ -notmatch "^export AWK_API_KEY=" -and $_ -notmatch "^export XIAOBEI_HOME=" -and $_ -notmatch "^export OPENCLAW_STATE_DIR=" } + if (Test-Path $dotEnv) { $exportLines = @(Get-Content $dotEnv) } + $exportLines = @($exportLines | Where-Object { $_ -notmatch "^export AWK_API_KEY=" -and $_ -notmatch "^export XIAOBEI_HOME=" -and $_ -notmatch "^export OPENCLAW_STATE_DIR=" }) if ($awkKey) { $awkEsc = $awkKey -replace "'", "'\''" $exportLines += "export AWK_API_KEY='$awkEsc'" @@ -495,21 +574,21 @@ function Install-GatewayAndEnv { $exportLines += "export XIAOBEI_HOME='$rootEsc'" $homeEsc = $OpenclawHome -replace "'", "'\''" $exportLines += "export OPENCLAW_STATE_DIR='$homeEsc'" - Set-Content -Path $dotEnv -Value $exportLines -Encoding UTF8 + Write-EnvFile $dotEnv $exportLines Write-Ok ".env written (business vars, used by the bare CLI)" # --- Write daemon.env (used by the gateway service, KEY=value format for gateway.cmd to call) --- # Holds only 3 fixed values + OPENCLAW_STATE_DIR (the gateway subprocess needs the state dir to # disambiguate the nested layer). $daemonLines = @() - if (Test-Path $daemonEnv) { $daemonLines = Get-Content $daemonEnv } - $daemonLines = $daemonLines | Where-Object { $_ -notmatch "^OPENCLAW_BROWSER_TIMEOUT_MS=" -and $_ -notmatch "^OPENCLAW_DISABLE_BONJOUR=" -and $_ -notmatch "^PATH=" -and $_ -notmatch "^OPENCLAW_STATE_DIR=" } + if (Test-Path $daemonEnv) { $daemonLines = @(Get-Content $daemonEnv) } + $daemonLines = @($daemonLines | Where-Object { $_ -notmatch "^OPENCLAW_BROWSER_TIMEOUT_MS=" -and $_ -notmatch "^OPENCLAW_DISABLE_BONJOUR=" -and $_ -notmatch "^PATH=" -and $_ -notmatch "^OPENCLAW_STATE_DIR=" }) $daemonLines += "OPENCLAW_BROWSER_TIMEOUT_MS=90000" $daemonLines += "OPENCLAW_DISABLE_BONJOUR=true" $daemonLines += "OPENCLAW_STATE_DIR=$OpenclawHome" $pathLine = "PATH=$(Join-Path $Root 'bin');$(Split-Path $NodeExe);$env:PATH" $daemonLines += $pathLine - Set-Content -Path $daemonEnv -Value $daemonLines -Encoding UTF8 + Write-EnvFile $daemonEnv $daemonLines Write-Ok "daemon.env written (used by the gateway service, 3 fixed values + PATH + OPENCLAW_STATE_DIR)" # Load the .env business vars into the current install shell so that subsequent daemon install / @@ -551,7 +630,7 @@ function Test-WeixinBound { } function Bind-WeixinChannel { - if ($SkipBind -or $NoPrompt) { + if ($script:SkipBind -or $script:NoPrompt) { Write-Host " [i] skipping WeChat scan-to-bind (XIAOBEI_SKIP_BIND / XIAOBEI_NO_PROMPT=1); run manually later: openclaw channels login --channel openclaw-weixin" -ForegroundColor Yellow return } @@ -578,7 +657,7 @@ function Main { # Detect whether already installed (decides update vs fresh install) $cfgExisting = Join-Path $OpenclawHome "openclaw.json" - $isUpdate = (Test-Path $cfgExisting) -and -not $Force + $isUpdate = (Test-Path $cfgExisting) -and -not $script:Force if ($isUpdate) { Write-Warn "detected existing install ($cfgExisting) -> taking the update route, preserving runtime data (XIAOBEI_FORCE=1 to force overwrite)" # Stop the gateway first (mirrors install.sh stop_gateway_if_running): otherwise tar @@ -626,6 +705,7 @@ function Main { } else { Place-Config Run-SetupCrew + Expose-SkillWrappers Install-GatewayAndEnv Bind-WeixinChannel } diff --git a/scripts/install.sh b/scripts/install.sh index 94d8639a..6bc6ee4a 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1697,12 +1697,10 @@ install_gateway_and_env() { # 引擎 resolveStateDir 把 OPENCLAW_HOME 当 homedir 再 append /.openclaw(见 # openclaw/src/config/paths.ts),故若用户 shell export 了 OPENCLAW_HOME=~/.openclaw, # 会产出嵌套路径。本脚本顶部已把 OPENCLAW_HOME 设成内部变量且未 export,不会传给引擎子进程, - # 所以这里只打 warning 提示用户清理 shell 环境,不能 unset——unset 会误伤脚本内部变量, + # 所以这里只打 info 提示用户清理 shell 环境变量,不能 unset——unset 会误伤脚本内部变量, # 导致后续 ${OPENCLAW_HOME//...} 在 set -u 下报 unbound variable。 if [ -n "${OPENCLAW_HOME:-}" ] && [ "$(cd "${OPENCLAW_HOME}" && pwd)" = "$(cd "$HOME/.openclaw" && pwd)" ]; then - ui_warn "检测到 OPENCLAW_HOME=$OPENCLAW_HOME 已设成 state dir 路径" - ui_warn " 这会让引擎嵌套 append /.openclaw → ~/.openclaw/.openclaw/,config 全落错位置" - ui_warn " 正解:清掉 OPENCLAW_HOME,改用 OPENCLAW_STATE_DIR 显式指定 state dir" + ui_info "检测到 shell 里 export OPENCLAW_HOME=$OPENCLAW_HOME(不影响本次安装,但建议清掉避免引擎嵌套)" fi # redirect stdin from /dev/tty so interactive read 工作于 curl|bash