From defd4673305c95ded187ea767ad036878db78fd3 Mon Sep 17 00:00:00 2001 From: foxdanzhang-tech Date: Thu, 6 Aug 2026 15:45:07 +0800 Subject: [PATCH 1/3] fix(install): remove BOM, translate comments to English, pass OPENCLAW_HOME to bash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes to the Windows PowerShell install scripts (install.ps1 and install-atomgit.ps1): 1. Remove UTF-8 BOM from both .ps1 files. Under `irm | iex`, irm does not strip the BOM, and iex treats the 3 BOM bytes as the first chars of a command name, so the first line `#` is parsed as an unknown command and errors (though it does not block the install). Without BOM, both `irm | iex` and local `powershell -File` work fine. 2. Translate all comments from Chinese to English (pure ASCII). Without BOM, Windows PowerShell 5.1 reads the file using the system ANSI code page (GBK on Chinese systems). Chinese comments in UTF-8 get misread as GBK, producing mojibake that cascades into parsing errors. English (ASCII) comments avoid this entirely in any encoding. 3. Explicitly pass OPENCLAW_HOME to bash in Run-SetupCrew (fixes ENOENT). setup-crew.sh uses OPENCLAW_HOME internally to resolve CONFIG_PATH and does NOT read OPENCLAW_STATE_DIR. Previously install.ps1 only set OPENCLAW_STATE_DIR, so setup-crew.sh fell back to $HOME/.openclaw. Under Git Bash's MSYS path conversion plus a Chinese username, this produced a bogus path like C:\c\Users\\.openclaw\openclaw.json (ENOENT). Now we explicitly set $env:OPENCLAW_HOME to a forward-slash path before calling bash, and clear it immediately after so subsequent gateway/daemon phases do not inherit it (the engine would nest-append /.openclaw). Co-Authored-By: AtomCode (GLM-5.2) --- scripts/install-atomgit.ps1 | 405 +++++++++++++++++++--------------- scripts/install.ps1 | 419 ++++++++++++++++++++---------------- 2 files changed, 463 insertions(+), 361 deletions(-) diff --git a/scripts/install-atomgit.ps1 b/scripts/install-atomgit.ps1 index 4bbee92e..ee3d2ecd 100755 --- a/scripts/install-atomgit.ps1 +++ b/scripts/install-atomgit.ps1 @@ -1,45 +1,56 @@ -# install-atomgit.ps1 - wiseflow 一键首装脚本(Windows,预构建 tarball 路线,atomgit 国内镜像专线) +# install-atomgit.ps1 - wiseflow one-click install script (Windows, prebuilt tarball route, atomgit mirror) # -# 与 install.ps1 区别:本脚本走 atomgit 国内镜像(tarball 走 atomgit.com → GitCode CDN, -# tag 解析走 api.atomgit.com/api/v5),不经 GitHub;适合国内网络环境。 -# 为对 `irm | iex` 友好(小白一条命令跑通),去掉 [CmdletBinding]param() 头, -# 所有可选参数走环境变量: -# $env:XIAOBEI_HOME 程序目录覆盖(默认 ~\xiaobei) -# $env:XIAOBEI_TAG 指定 release tag(默认拉最新) -# $env:XIAOBEI_TARBALL 本地已下好的 tarball 路径,跳过下载 -# $env:XIAOBEI_FORCE =1 强覆盖已有运行数据(~\.openclaw) -# $env:XIAOBEI_SKIP_BIND =1 跳过末尾微信扫码绑定 -# $env:XIAOBEI_SKIP_BROWSER=1 跳过 camoufox-cli 浏览器二进制(冒烟/CI) -# $env:XIAOBEI_NO_PROMPT =1 跳过所有交互提示(CI/自动化) +# Difference from install.ps1: this script uses the atomgit mirror (tarball via atomgit.com -> +# GitCode CDN, tag resolution via api.atomgit.com/api/v5), bypassing GitHub; suitable for networks +# inside mainland China. GitHub-accessible users should use install.ps1 (GitHub direct) instead. +# To be friendly to `irm | iex` (so a one-liner works for beginners), the [CmdletBinding]param() +# header is removed; all optional params go through environment variables: +# $env:XIAOBEI_HOME Program directory override (default ~\xiaobei) +# $env:XIAOBEI_TAG Specify a release tag (default pulls latest) +# $env:XIAOBEI_TARBALL Path to a locally downloaded tarball, skips download +# $env:XIAOBEI_FORCE =1 force overwrite existing runtime data (~\.openclaw) +# $env:XIAOBEI_SKIP_BIND =1 skip the WeChat scan-to-bind step at the end +# $env:XIAOBEI_SKIP_BROWSER=1 skip camoufox-cli browser binary (smoke/CI) +# $env:XIAOBEI_NO_PROMPT =1 skip all interactive prompts (CI/automation) # -# 用法(PowerShell,需 Git Bash 或 WSL): +# Usage (PowerShell, requires Git Bash or WSL): # irm https://raw.atomgit.com/wiseflow/xiaobei/raw/master/scripts/install-atomgit.ps1 | iex -# # 或本地: +# # or locally: # powershell -ExecutionPolicy Bypass -File install-atomgit.ps1 # -# ⚠️ 本文件必须保持 UTF-8 **带 BOM**:Windows PowerShell 5.1 对无 BOM 的 .ps1 按系统 -# ANSI 代码页(中文系统 GBK)解析,中文字符串会乱码并产生解析错误,脚本无法运行。 +# WARNING: this file MUST be kept UTF-8 **without BOM**: under `irm | iex`, irm does not strip +# the BOM, and iex treats the 3 BOM bytes as the first chars of a command name, so the first +# line `#` is parsed as an unknown command and errors (though it does not block the install). +# The local `powershell -File` scenario also works fine without BOM (PowerShell 5.1+ defaults +# to UTF-8 for parsing). All comments in this file are in English (ASCII) to avoid GBK/UTF-8 +# mojibake parsing errors under Windows PowerShell 5.1 with a non-BOM file. # -# 与 install.sh 同构(方案 B 瘦 tarball): -# 1. 拉 xiaobei-{tag}-win-x64.tar.gz(atomgit CDN,Windows bsdtar 原生支持 gzip,免装 zstd) -# 2. 解压到 $XIAOBEI_HOME(默认 $env:USERPROFILE\xiaobei,程序目录) -# 3. portable node + pnpm install --prod --frozen-lockfile(在 openclaw\ 下) -# 4. pip install --user(skills 的 python deps,有 python 才跑) -# 5. 放 config-templates\openclaw.json → $OPENCLAW_HOME\openclaw.json + 预填微信 binding -# 6. setup-crew.sh(需 bash:Git Bash 或 WSL;无则警告并跳过,用户后续手动跑) -# 7. camoufox-cli:.cmd shim + camoufox-cli install 下 Firefox -# 8. openclaw-weixin 插件:openclaw plugins install ... --pin(npmmirror) -# 9. 交互问 AWK_API_KEY → 写 daemon.env + setx 用户环境变量 → 尝试 openclaw daemon install +# Structurally identical to install.sh (Plan B slim tarball): +# 1. Pull xiaobei-{tag}-win-x64.tar.gz (atomgit CDN; Windows bsdtar natively supports gzip, no +# need to install zstd) +# 2. Extract to $XIAOBEI_HOME (default $env:USERPROFILE\xiaobei, the program directory) +# 3. portable node + pnpm install --prod --frozen-lockfile (under openclaw\) +# 4. pip install --user (python deps for skills; only runs if python is present) +# 5. Place config-templates\openclaw.json -> $OPENCLAW_HOME\openclaw.json + prefill WeChat binding +# 6. setup-crew.sh (requires bash: Git Bash or WSL; if absent, warn and skip; user can run it +# manually later) +# 7. camoufox-cli: .cmd shim + camoufox-cli install downloads Firefox +# 8. openclaw-weixin plugin: openclaw plugins install ... --pin (npmmirror) +# 9. Interactive prompt for AWK_API_KEY -> write daemon.env + setx user env var -> attempt +# openclaw daemon install # -# 目录职责:$XIAOBEI_HOME(~\xiaobei)= 程序;$OPENCLAW_HOME(~\.openclaw)= 运行数据。 -# Windows 原生 wrapper:$XIAOBEI_HOME\bin\openclaw.cmd(WSL/Git Bash 用户也可用 bin\openclaw)。 +# Directory responsibilities: $XIAOBEI_HOME (~\xiaobei) = program; $OPENCLAW_HOME (~\.openclaw) +# = runtime data. +# Windows native wrapper: $XIAOBEI_HOME\bin\openclaw.cmd (WSL/Git Bash users can also use bin\openclaw). $ErrorActionPreference = "Stop" -# ─── 常量 / 目录 ─────────────────────────────────────────────── -# atomgit 专线常量(host/版本与 GitHub 不同,不依赖运行时开关): -# tarball 直链 host = atomgit.com(redirect 到 file-cdn.gitcode.com 签名 CDN,匿名 GET 可下,~140MB) -# 解 latest tag 走 api.atomgit.com/api/v5(NOT Gitea v1,host/版本都不同) +# --- Constants / directories --- +# atomgit mirror constants (host/version differ from GitHub, do not depend on runtime switches): +# tarball direct host = atomgit.com (redirects to a signed file-cdn.gitcode.com CDN; anonymous +# GET works, ~140MB) +# latest tag resolution goes through api.atomgit.com/api/v5 (NOT Gitea v1; both host and version +# differ) $Repo = "wiseflow/xiaobei" $AtomgitMirror = "https://atomgit.com/wiseflow/xiaobei" $AtomgitApi = "https://api.atomgit.com/api/v5/repos/wiseflow/xiaobei" @@ -47,7 +58,7 @@ $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" } -# 行为开关(环境变量,=1 启用) +# 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") @@ -64,10 +75,12 @@ 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 } -# 跑原生命令并按行回显 stdout+stderr。pip/npm/camoufox-cli/openclaw 正常会把进度和警告写 stderr, -# 而本脚本顶部 $ErrorActionPreference="Stop" 会把 2>&1 合并进管道的 stderr 行当 terminating error -# 抛 NativeCommandError/RemoteException(用户 Win 实测卡在 pip install 这步)。这里临时切到 Continue -# 跑原生命令,$LASTEXITCODE 仍由调用方据以判成败。 +# 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 +# NativeCommandError/RemoteException (a user's Windows run actually got stuck at pip install this +# way). Here we temporarily switch to Continue to run the native command; $LASTEXITCODE is still +# used by the caller to judge success/failure. function Invoke-Streamed([scriptblock]$sb) { $prev = $ErrorActionPreference $ErrorActionPreference = 'Continue' @@ -75,8 +88,9 @@ function Invoke-Streamed([scriptblock]$sb) { finally { $ErrorActionPreference = $prev } } -# 同 Invoke-Streamed,但把 stdout+stderr 合并成字符串返回(供调用方 -&match)。`2>$null` 在 -# Windows PowerShell 5.1 + Stop 下压不住 .cmd 的 stderr 仍抛 NativeCommandError,这里用 Continue + 2>&1 纳。 +# Same as Invoke-Streamed, but merges stdout+stderr into a single string and returns it (for callers +# that need to -match against the output). `2>$null` under Windows PowerShell 5.1 + Stop does not +# suppress a .cmd's stderr and still throws NativeCommandError; using Continue + 2>&1 is stable. function Capture-Streamed([scriptblock]$sb) { $prev = $ErrorActionPreference $ErrorActionPreference = 'Continue' @@ -84,123 +98,130 @@ function Capture-Streamed([scriptblock]$sb) { finally { $ErrorActionPreference = $prev } } -# ─── 1. 解析最新 release tag(atomgit v5 API)──────────────────── +# --- 1. Resolve latest release tag (atomgit v5 API) --- function Resolve-Tag { if ($env:XIAOBEI_TAG) { return $env:XIAOBEI_TAG } try { $rel = Invoke-RestMethod "$AtomgitApi/releases/latest" -Headers @{ "User-Agent" = "xiaobei-install" } if ($rel.tag_name) { return $rel.tag_name } - } catch { Write-Warn "atomgit v5 API 拉取失败,请手动设 `$env:XIAOBEI_TAG=` 后重跑" } - throw "无法解析最新 release tag(atomgit v5 API)" + } catch { Write-Warn "atomgit v5 API fetch failed, please set `$env:XIAOBEI_TAG=` manually and re-run" } + throw "unable to resolve the latest release tag (atomgit v5 API)" } -# ─── 2. 下载 tarball(atomgit CDN)────────────────────────────── +# --- 2. Download tarball (atomgit CDN) --- function Download-Tarball([string]$tag) { $asset = "xiaobei-$tag-win-x64.tar.gz" if ($env:XIAOBEI_TARBALL -and (Test-Path $env:XIAOBEI_TARBALL)) { - Write-Ok "用本地 tarball:$env:XIAOBEI_TARBALL" + Write-Ok "using local tarball: $env:XIAOBEI_TARBALL" return $env:XIAOBEI_TARBALL } $tmp = New-TemporaryFile $url = "$AtomgitMirror/releases/download/$tag/$asset" - Write-Host " 下载 $url" + Write-Host " downloading $url" Invoke-WebRequest -Uri $url -OutFile $tmp -Headers @{ "User-Agent" = "xiaobei-install" } return $tmp.FullName } -# ─── 3. 解压 ─────────────────────────────────────────────────── +# --- 3. Extract --- function Extract-Tarball([string]$tarball) { if (-not (Get-Command tar -ErrorAction SilentlyContinue)) { - throw "未找到 tar(Windows 10 1803+ 自带 bsdtar;请升级 Windows 或装 Git Bash)" + throw "tar not found (Windows 10 1803+ ships bsdtar; please upgrade Windows or install Git Bash)" } New-Item -ItemType Directory -Force -Path $Root | Out-Null - Write-Host " 解压到 $Root" + Write-Host " extracting to $Root" & tar -xzf $tarball -C $Root - if ($LASTEXITCODE -ne 0) { throw "tar 解压失败 (exit $LASTEXITCODE)" } + if ($LASTEXITCODE -ne 0) { throw "tar extraction failed (exit $LASTEXITCODE)" } } -# ─── 4. pnpm install --prod ──────────────────────────────────── +# --- 4. pnpm install --prod --- function Install-Deps { Write-Stage "Installing dependencies (pnpm install --prod)" $openclawDir = Join-Path $Root "openclaw" - if (-not (Test-Path $NodeExe)) { throw "portable node 未找到:$NodeExe" } + if (-not (Test-Path $NodeExe)) { throw "portable node not found: $NodeExe" } Push-Location $openclawDir try { - # postinstall 子进程(esbuild/protobufjs 等)按 PATH 找 node;目标机器多半没装全局 - # Node('node' is not recognized 实测),把 portable node 前置进 PATH + # postinstall subprocesses (esbuild/protobufjs etc.) look up node via PATH; the target + # machine most likely has no global Node installed ('node' is not recognized, observed in + # practice), so we prepend the portable node directory to PATH. $env:PATH = "$(Split-Path $NodeExe);$env:PATH" - # pnpm 11 不认 npm_config_registry 环境变量(实测仍走 registry.npmjs.org),用显式 --registry + # pnpm 11 does not honor the npm_config_registry env var (in practice it still hits + # registry.npmjs.org), so we pass an explicit --registry. $env:npm_config_registry = "https://registry.npmmirror.com" & $NodeExe $PnpmMjs install --prod --frozen-lockfile --registry https://registry.npmmirror.com - if ($LASTEXITCODE -ne 0) { throw "pnpm install 失败 (exit $LASTEXITCODE)" } + if ($LASTEXITCODE -ne 0) { throw "pnpm install failed (exit $LASTEXITCODE)" } Write-Ok "deps installed" } finally { Pop-Location } } -# ─── 5. python skill deps ────────────────────────────────────── +# --- 5. python skill deps --- function Install-PythonDeps { Write-Stage "Installing python skill deps" $py = Get-Command python -ErrorAction SilentlyContinue - if (-not $py) { Write-Warn "未找到 python,跳过 skill python deps"; return } + if (-not $py) { Write-Warn "python not found, skipping skill python deps"; return } $reqs = @() $reqs += Get-ChildItem -Path (Join-Path $Root "skills") -Filter "requirements.txt" -Recurse -ErrorAction SilentlyContinue $reqs += Get-ChildItem -Path (Join-Path $Root "crews") -Filter "requirements.txt" -Recurse -ErrorAction SilentlyContinue $reqs += Get-ChildItem -Path $Root -Filter "requirements.txt" -ErrorAction SilentlyContinue $reqs = $reqs | Select-Object -ExpandProperty FullName -Unique - if (-not $reqs) { Write-Ok "无 requirements.txt"; return } - # 各行作为 pip 命令行参数传入(非 -r 文件),必须剥掉行内注释(pip 只在 - # requirements 文件里认注释,参数里 `Pillow # 说明` 直接 Invalid requirement 报错); - # 文件是 UTF-8,显式指定编码防中文注释被 ANSI 误读产生乱码参数。 + if (-not $reqs) { Write-Ok "no requirements.txt"; return } + # Each line is passed as a pip command-line argument (not via -r file), so inline comments must + # be stripped (pip only recognizes comments inside a requirements file; `Pillow # desc` passed + # as an arg directly errors with Invalid requirement). The file is UTF-8; we explicitly specify + # the encoding to prevent Chinese comments from being misread as ANSI and producing garbled args. $merged = ($reqs | ForEach-Object { Get-Content $_ -Encoding UTF8 -ErrorAction SilentlyContinue } | ForEach-Object { ($_ -replace '#.*$', '').Trim() } | Where-Object { $_ } | Sort-Object -Unique) - if (-not $merged) { Write-Ok "无 python 依赖"; return } + if (-not $merged) { Write-Ok "no python deps"; return } Invoke-Streamed { python -m pip install --user $merged } - if ($LASTEXITCODE -ne 0) { Write-Warn "pip install 非零退出;可后续手动补装 skills 的 python 依赖" } + if ($LASTEXITCODE -ne 0) { Write-Warn "pip install returned non-zero; you can manually install the skills' python deps later" } else { Write-Ok "python deps installed" } } -# ─── 6. 放 config template(对齐 install.sh place_config_template)──── -# template 已预置 channels.openclaw-weixin / bindings / session.dmScope,不再运行时 mutate -# (sh 版曾因运行时 mutate 把 plugins 顶层写坏致 "Invalid input",已同构移除)。 -# 健康检查:现有 config 缺 models/agents.defaults(多半是 Install-WeixinPlugin 先跑时 -# openclaw plugins install 自建的极简 config,或 openclaw 首启自动生成)→ 备份后用 -# template 覆盖,否则 channels/bindings 永远缺失、小贝起不来。 +# --- 6. Place config template (mirrors install.sh place_config_template) --- +# The template already pre-populates channels.openclaw-weixin / bindings / session.dmScope; we no +# longer mutate it at runtime (the sh version once broke the plugins top-level via runtime mutation, +# causing "Invalid input"; that mutation has been removed in parallel here). +# Health check: if the existing config is missing models/agents.defaults (most likely a minimal config +# auto-created by `openclaw plugins install` when Install-WeixinPlugin ran first, or by openclaw's +# first launch), back it up and overwrite with the template; otherwise channels/bindings would be +# missing forever and xiaobei could never start. function Place-Config { Write-Stage "Placing config template" New-Item -ItemType Directory -Force -Path $OpenclawHome | Out-Null $cfg = Join-Path $OpenclawHome "openclaw.json" $tmpl = Join-Path $Root "config-templates\openclaw.json" - if (-not (Test-Path $tmpl)) { Write-Err "config template 缺失:$tmpl(tarball 损坏?)"; return } + if (-not (Test-Path $tmpl)) { Write-Err "config template missing: $tmpl (tarball corrupted?)"; return } $needPlace = $false; $reason = "" - if (-not (Test-Path $cfg)) { $needPlace = $true; $reason = "不存在" } + if (-not (Test-Path $cfg)) { $needPlace = $true; $reason = "not present" } elseif ($Force) { $needPlace = $true; $reason = "XIAOBEI_FORCE=1" } else { try { - # 必须显式按 UTF-8 读:openclaw 写的 config 是 UTF-8 无 BOM,PS 5.1 的 - # Get-Content 默认按 ANSI 代码页读,中文路径旁的 \\ 转义会被双字节吞掉, - # 有效 config 被误判"解析失败"遭覆盖(本机实测)。 + # Must explicitly read as UTF-8: openclaw writes its config as UTF-8 without BOM, while + # PS 5.1's Get-Content defaults to the ANSI code page; a \\ escape next to a Chinese + # path can get swallowed by a double-byte char, causing a valid config to be misjudged + # as "parse failed" and overwritten (observed in practice). $j = [System.IO.File]::ReadAllText($cfg) | ConvertFrom-Json -ErrorAction Stop - if (-not ($j.models -and $j.agents -and $j.agents.defaults)) { $needPlace = $true; $reason = "缺 models/agents.defaults(疑似被极简化)" } - } catch { $needPlace = $true; $reason = "解析失败" } + if (-not ($j.models -and $j.agents -and $j.agents.defaults)) { $needPlace = $true; $reason = "missing models/agents.defaults (likely minimized)" } + } catch { $needPlace = $true; $reason = "parse failed" } } if ($needPlace) { if (Test-Path $cfg) { $bak = "$cfg.bak.$([int][double]::Parse((Get-Date -UFormat %s)))" Copy-Item $cfg $bak - Write-Warn "openclaw.json $reason → 用 template 覆盖(旧文件备份到 $bak)" + Write-Warn "openclaw.json $reason -> overwriting with template (old file backed up to $bak)" } Copy-Item $tmpl $cfg -Force Write-Ok "placed openclaw.json template" } else { - Write-Ok "openclaw.json 已存在且健康(有 models + agents.defaults),保留" + Write-Ok "openclaw.json already present and healthy (has models + agents.defaults), keeping" } - # 把 plugins.load.paths 里的 ${XIAOBEI_HOME} env ref 解析成绝对路径,避免 CLI 上下文没 - # XIAOBEI_HOME 时 "plugin path not found" 误报;AWK_API_KEY 是 secret 保持 env ref 不动。 - # 文本级替换(template 中 ${XIAOBEI_HOME} 仅出现在 plugins.load.paths);JSON 字符串里 - # 反斜杠是转义符,路径统一写正斜杠。幂等:已解析则 no-op。 + # Resolve the ${XIAOBEI_HOME} env ref inside plugins.load.paths to an absolute path, to avoid a + # "plugin path not found" false alarm when the CLI context lacks XIAOBEI_HOME; AWK_API_KEY is a + # secret so its env ref is left untouched. This is a file-level replacement (the ${XIAOBEI_HOME} + # template token only appears in plugins.load.paths); inside a JSON string a backslash is an + # escape char, so we normalize paths to forward slashes. Idempotent: a no-op if already resolved. $raw = [System.IO.File]::ReadAllText($cfg) if ($raw.Contains('${XIAOBEI_HOME}')) { $rootFwd = $Root -replace '\\', '/' @@ -209,10 +230,11 @@ function Place-Config { } } -# ─── 7. setup-crew(需 bash)────────────────────────────────── -# 找一个真正可用的 bash:优先 Git Bash(吃得了 C:/ 魔格路径);PATH 里的 bash 可能是 -# C:\Windows\system32\bash.exe(WSL 启动器)——没装发行版时直接报错退出,且 WSL bash -# 对 Windows 路径参数不做转换,都不能直接用。每个候选实测 `bash -c echo` 通过才算数。 +# --- 7. setup-crew (requires bash) --- +# Find a genuinely usable bash: prefer Git Bash (it handles C:/ style paths well); the bash on PATH +# might be C:\Windows\system32\bash.exe (the WSL launcher) which errors out immediately when no distro +# is installed, and WSL bash does not convert Windows path arguments either, so neither can be used +# directly. Each candidate is actually tested with `bash -c echo` before being accepted. function Find-WorkingBash { $candidates = @() $git = Get-Command git -ErrorAction SilentlyContinue @@ -224,7 +246,8 @@ function Find-WorkingBash { $candidates += "$env:ProgramFiles\Git\bin\bash.exe" $candidates += "${env:ProgramFiles(x86)}\Git\bin\bash.exe" $inPath = Get-Command bash -ErrorAction SilentlyContinue - # system32 的 WSL 启动器排最后(仅当其可用且无 Git Bash 时兜底) + # The system32 WSL launcher goes last (only used as a fallback when it is available and no Git + # Bash exists). if ($inPath) { $candidates += $inPath.Source } foreach ($b in $candidates) { if ($b -and (Test-Path $b)) { @@ -238,60 +261,77 @@ function Find-WorkingBash { function Run-SetupCrew { Write-Stage "Setting up crew templates (needs bash)" $sh = Join-Path $Root "scripts\setup-crew.sh" - if (-not (Test-Path $sh)) { Write-Warn "setup-crew.sh 不在 tarball 内,跳过"; return } + if (-not (Test-Path $sh)) { Write-Warn "setup-crew.sh is not inside the tarball, skipping"; return } $bashExe = Find-WorkingBash if (-not $bashExe) { - Write-Warn "未找到可用的 bash(setup-crew.sh 是 bash 暑本;注意 system32\bash.exe 是 WSL 启动器,没装发行版不可用)" - Write-Host " 请装 Git Bash(https://git-scm.com)或 WSL 发行版,然后手动跑:" + Write-Warn "no usable bash found (setup-crew.sh is a bash script; note system32\bash.exe is the WSL launcher and is unusable without a distro installed)" + Write-Host " Please install Git Bash (https://git-scm.com) or a WSL distro, then run manually:" Write-Host " set OPENCLAW_HOME=$OpenclawHome" Write-Host " set XIAOBEI_BIN_DIR=$(Join-Path $Root 'bin')" Write-Host " bash `"$sh`"" return } Write-Host " bash: $bashExe" - # bash 不认反斜杠(会被当转义),传正斜杠路径给 setup-crew.sh - # ⚠️ 用 OPENCLAW_STATE_DIR 而非 OPENCLAW_HOME:引擎 resolveStateDir 把 OPENCLAW_HOME 当 homedir - # 再 append /.openclaw(见 openclaw/src/config/paths.ts),若设 OPENCLAW_HOME=~/.openclaw 会产出 - # ~/.openclaw/.openclaw 嵌套层——后续 weixin/gateway CLI 调用都把 config/npm/openclaw.sqlite 写嵌套层, - # 外层正解路径反而空(本机实测真 bug)。OPENCLAW_STATE_DIR 引擎当直接 state dir、不 append。 + # bash does not understand backslashes (it would treat them as escapes), so we pass forward-slash + # paths to setup-crew.sh. + # WARNING: use OPENCLAW_STATE_DIR rather than OPENCLAW_HOME: the engine's resolveStateDir treats + # OPENCLAW_HOME as a homedir and then appends /.openclaw (see openclaw/src/config/paths.ts); if + # OPENCLAW_HOME is set to ~/.openclaw this produces a nested ~/.openclaw/.openclaw layer, and all + # subsequent weixin/gateway CLI calls write config/npm/openclaw.sqlite into that nested layer while + # the correct outer path stays empty (a real bug observed on a local machine). OPENCLAW_STATE_DIR + # is treated by the engine as a direct state dir and is not appended to. $env:OPENCLAW_STATE_DIR = ($OpenclawHome -replace '\\', '/') $env:XIAOBEI_BIN_DIR = ((Join-Path $Root "bin") -replace '\\', '/') + # setup-crew.sh uses OPENCLAW_HOME internally to resolve CONFIG_PATH (it does NOT read + # OPENCLAW_STATE_DIR), so we must explicitly pass a forward-slash path; otherwise bash falls back + # to $HOME/.openclaw, and under Git Bash's MSYS path conversion + a Chinese username this yields a + # bogus path like C:\c\Users\\.openclaw (ENOENT). + $env:OPENCLAW_HOME = ($OpenclawHome -replace '\\', '/') Invoke-Streamed { & $bashExe ($sh -replace '\\', '/') } - if ($LASTEXITCODE -ne 0) { Write-Warn "setup-crew.sh 非零退出(可后续手动 --force 修复)" } + # After setup-crew.sh finishes, clear OPENCLAW_HOME so subsequent install phases (gateway/daemon) + # do not inherit this value - the engine would treat OPENCLAW_HOME as a homedir and append + # /.openclaw, producing a nested layer. + Remove-Item Env:OPENCLAW_HOME -ErrorAction SilentlyContinue + if ($LASTEXITCODE -ne 0) { Write-Warn "setup-crew.sh exited non-zero (you can manually re-run with --force later to fix)" } else { Write-Ok "crew templates set up" } } -# ─── 8. camoufox-cli ─────────────────────────────────────────── +# --- 8. camoufox-cli --- function Install-CamoufoxCli { if ($SkipBrowser) { - Write-Host " [i] 跳过 camoufox-cli 浏览器二进制(XIAOBEI_SKIP_BROWSER=1);后续手动:camoufox-cli install" -ForegroundColor Yellow + Write-Host " [i] skipping camoufox-cli browser binary (XIAOBEI_SKIP_BROWSER=1); run manually later: camoufox-cli install" -ForegroundColor Yellow return } Write-Stage "Installing camoufox-cli browser" $fork = Join-Path $Root "camoufox-cli" - if (-not (Test-Path $fork)) { Write-Warn "camoufox-cli fork 不在 tarball 内:$fork;跳过"; return } - # $Root\bin 放 camoufox-cli.cmd shim,前置进 PATH 让下面 `camoufox-cli install` 找得到 + if (-not (Test-Path $fork)) { Write-Warn "camoufox-cli fork not in tarball: $fork; skipping"; return } + # Place a camoufox-cli.cmd shim in $Root\bin and prepend it to PATH so the `camoufox-cli install` + # below can be found. $env:PATH = "$(Join-Path $Root 'bin');$(Split-Path $NodeExe);$env:PATH" $env:npm_config_registry = "https://registry.npmmirror.com" $camoufoxBin = Join-Path $Root "bin\camoufox-cli.cmd" $cliJs = Join-Path $fork "dist\cli.js" $hasDeps = Test-Path (Join-Path $fork "node_modules\camoufox-js") - # fork 运行时依赖:dist 是 tsc 编译,camoufox-js/playwright-core/pdf-lib 是 external import, - # 运行时靠 fork 目录下的 node_modules 解析。tarball 为瘦身 ship 的 fork 不带 node_modules。 + # Runtime deps of the fork: dist is a tsc build, and camoufox-js/playwright-core/pdf-lib are + # external imports resolved at runtime via the node_modules under the fork directory. The fork + # shipped in the slim tarball does not carry node_modules. if (-not $hasDeps) { Push-Location $fork try { & $NpmCmd install --omit=dev - if ($LASTEXITCODE -ne 0) { Write-Warn "camoufox-cli fork deps install 失败"; return } + if ($LASTEXITCODE -ne 0) { Write-Warn "camoufox-cli fork deps install failed"; return } } finally { Pop-Location } } - # Windows 不走 `npm install -g $fork`:npm 把 fork 装进 $prefix\node_modules 时撞 fork 源目录 - # 本身(EEXIST: file already exists $Root\camoufox-cli)——Windows npm 不能像 Linux 那样 symlink - # 到源目录。改仿 openclaw.cmd 写 .cmd shim 直接跑 node $fork\dist\cli.js,fork 的 node_modules - # 已就位,Node 从 cli.js 向上解析依赖;camoufox-cli JS 用 __dirname 定位自身包根,行为与全局装一致。 - if (-not (Test-Path $cliJs)) { Write-Warn "camoufox-cli 入口未找到:$cliJs;跳过"; return } + # Windows does not go through `npm install -g $fork`: when npm installs the fork into + # $prefix\node_modules it collides with the fork's own source directory (EEXIST: file already + # exists $Root\camoufox-cli) - Windows npm cannot symlink to the source dir the way Linux does. + # Instead, mirroring openclaw.cmd, we write a .cmd shim that directly runs + # node $fork\dist\cli.js; the fork's node_modules is already in place and Node resolves deps + # upward from cli.js. camoufox-cli's JS uses __dirname to locate its own package root, so the + # behavior matches a global install. + if (-not (Test-Path $cliJs)) { Write-Warn "camoufox-cli entry not found: $cliJs; skipping"; return } $shimLines = @( '@echo off' 'setlocal' @@ -301,37 +341,37 @@ function Install-CamoufoxCli { Set-Content -Path $camoufoxBin -Value $shimLines -Encoding ASCII Write-Ok "camoufox-cli shim written: $camoufoxBin" - Write-Host " 下 Firefox binary(首次 ~557MB)..." + Write-Host " downloading Firefox binary (first time ~557MB)..." Invoke-Streamed { camoufox-cli install } - if ($LASTEXITCODE -ne 0) { Write-Warn "camoufox-cli install 失败;可后续手动:camoufox-cli install" } + if ($LASTEXITCODE -ne 0) { Write-Warn "camoufox-cli install failed; you can run manually later: camoufox-cli install" } else { Write-Ok "camoufox-cli ready" } } -# ─── 9. openclaw-weixin 插件 ────────────────────────────────── +# --- 9. openclaw-weixin plugin --- function Install-WeixinPlugin { Write-Stage "Installing WeChat plugin" - if (-not (Test-Path $ClawCmd)) { Write-Warn "openclaw wrapper 未找到:$ClawCmd;跳过"; return } + if (-not (Test-Path $ClawCmd)) { Write-Warn "openclaw wrapper not found: $ClawCmd; skipping"; return } $pkg = "@tencent-weixin/openclaw-weixin"; $ver = "2.4.6" $pin = Join-Path $Root "openclaw-weixin.version.json" if (Test-Path $pin) { try { $j = Get-Content $pin -Raw | ConvertFrom-Json $pkg = $j.'openclaw-weixin'.package; $ver = $j.'openclaw-weixin'.version - } catch { Write-Warn "pin 文件解析失败,用默认 $pkg@$ver" } + } catch { Write-Warn "pin file parse failed, using default $pkg@$ver" } } $env:npm_config_registry = "https://registry.npmmirror.com" $listOut = Capture-Streamed { & $ClawCmd plugins list } if ($listOut -match "openclaw-weixin") { Write-Ok "openclaw-weixin plugin already installed"; return } Invoke-Streamed { & $ClawCmd plugins install "$pkg@$ver" --pin } if ($LASTEXITCODE -eq 0) { Write-Ok "openclaw-weixin plugin installed" } - else { Write-Warn "插件安装失败;可后续手动:$ClawCmd plugins install $pkg@$ver --pin" } + else { Write-Warn "plugin install failed; you can run manually later: $ClawCmd plugins install $pkg@$ver --pin" } } -# ─── 10. awada 本地插件 deps(ws + zod)────────────────────── +# --- 10. awada local plugin deps (ws + zod) --- function Install-AwadaPlugin { Write-Stage "Installing awada plugin deps" $awada = Join-Path $Root "awada" - if (-not (Test-Path $awada)) { Write-Warn "awada 不在 tarball 内:$awada;跳过"; return } + if (-not (Test-Path $awada)) { Write-Warn "awada not in tarball: $awada; skipping"; return } $hasWs = Test-Path (Join-Path $awada "node_modules\ws") $hasZod = Test-Path (Join-Path $awada "node_modules\zod") if ($hasWs -and $hasZod) { Write-Ok "awada deps already installed"; return } @@ -340,16 +380,18 @@ function Install-AwadaPlugin { try { & $NpmCmd install --omit=dev if ($LASTEXITCODE -eq 0) { Write-Ok "awada deps installed" } - else { Write-Warn "awada deps install 失败;可后续手动:cd $awada ; npm install --omit=dev" } + else { Write-Warn "awada deps install failed; you can run manually later: cd $awada ; npm install --omit=dev" } } finally { Pop-Location } } -# ─── 10.5 修复 daemon 启动脚本编码(openclaw 上游 bug 的 workaround)── -# openclaw daemon install 写出的 gateway.cmd / gateway.vbs 是 UTF-8(无 BOM), -# 而 cmd.exe / WScript 按系统 ANSI 代码页读取脚本文件——安装路径含非 ASCII 字符 -# (如中文用户名)时路径变乱码,node.exe 找不到,gateway 永远起不来(本机实测)。 -# workaround:把长路径替换成 8.3 矫路径(纯 ASCII)后以 ASCII 重写;无 8.3 名 -# 可用时退回按 ANSI 代码页写(cmd.exe 按 ANSI 读,编码一致即可)。幂等。 +# --- 10.5 Fix daemon launch-script encoding (workaround for an openclaw upstream bug) --- +# The gateway.cmd / gateway.vbs written out by `openclaw daemon install` are UTF-8 (no BOM), but +# cmd.exe / WScript read script files using the system ANSI code page - so when the install path +# contains non-ASCII chars (e.g. a Chinese username) the path turns into mojibake, node.exe cannot +# be found, and the gateway never starts (observed on a local machine). +# Workaround: replace long paths with their 8.3 short paths (pure ASCII) and rewrite as ASCII; if no +# 8.3 name is available, fall back to writing in the ANSI code page (cmd.exe reads it as ANSI, so the +# encodings line up). Idempotent. function Repair-GatewayCmd { $files = @((Join-Path $OpenclawHome "gateway.cmd"), (Join-Path $OpenclawHome "gateway.vbs")) $fso = $null @@ -368,51 +410,56 @@ function Repair-GatewayCmd { } else { [System.IO.File]::WriteAllText($f, $txt, [System.Text.ASCIIEncoding]::new()) } - Write-Ok "修复 $(Split-Path $f -Leaf) 编码(非 ASCII 路径 → 8.3 矫路径)" + Write-Ok "fixed $(Split-Path $f -Leaf) encoding (non-ASCII path -> 8.3 short path)" } } -# ─── 11. 交互收 AWK_API_KEY + 起 gateway ────────────────────── -# env 分工(对齐 install.sh / 上游踩坑经验): -# ~/.openclaw/.env ← 业务变量(AWK_API_KEY/XIAOBEI_HOME/OPENCLAW_STATE_DIR),openclaw CLI 裸跑用 -# ~/.openclaw/daemon.env ← gateway service 用,只放 3 个固定值(OPENCLAW_BROWSER_TIMEOUT_MS/ -# OPENCLAW_DISABLE_BONJOUR/PATH)+ OPENCLAW_STATE_DIR -# 原因:Linux systemd 那套 EnvironmentFile= 加载器对含特殊字符的业务变量(PATH 带分号、 -# AWK_API_KEY 带连字符)有踩坑历史,业务变量挪 .env 让 systemd 只加载稳的固定值。 -# Windows 不走 systemd,gateway.cmd 的 `call daemon.env` 没这个坑,但为风格统一照此分工。 +# --- 11. Interactive AWK_API_KEY prompt + start gateway --- +# Env division (mirrors install.sh / upstream lessons learned): +# ~/.openclaw/.env <- business vars (AWK_API_KEY/XIAOBEI_HOME/OPENCLAW_STATE_DIR), sourced by the +# openclaw CLI when run bare +# ~/.openclaw/daemon.env <- used by the gateway service, holds only 3 fixed values +# (OPENCLAW_BROWSER_TIMEOUT_MS/OPENCLAW_DISABLE_BONJOUR/PATH) + OPENCLAW_STATE_DIR +# Reason: the Linux systemd EnvironmentFile= loader has a history of choking on business vars that +# contain special chars (PATH with semicolons, AWK_API_KEY with hyphens), so business vars were moved +# to .env and systemd only loads the stable fixed values. Windows does not use systemd and +# gateway.cmd's `call daemon.env` does not have this pitfall, but we keep the same division for +# consistency. function Install-GatewayAndEnv { Write-Stage "Configuring API key and gateway" New-Item -ItemType Directory -Force -Path $OpenclawHome | Out-Null $dotEnv = Join-Path $OpenclawHome ".env" $daemonEnv = Join-Path $OpenclawHome "daemon.env" - # ─── 检测/清掉错误的 OPENCLAW_HOME 用户环境变量 ─────────────── - # 引擎 resolveStateDir 把 OPENCLAW_HOME 当 homedir 再 append /.openclaw(见 - # openclaw/src/config/paths.ts),故若 OPENCLAW_HOME 已被设成 ~/.openclaw,会产出 - # ~/.openclaw/.openclaw 嵌套路径——openclaw.json、daemon.env、lastTouch 全落嵌套层, - # 外层正解路径反而空(本机实测过的真 bug)。state dir 应该用 OPENCLAW_STATE_DIR 显式 - # 覆盖(引擎把它当直接 state dir、不 append),OPENCLAW_HOME 不该在用户环境里设。 + # --- Detect / clear a bogus OPENCLAW_HOME user env var --- + # The engine's resolveStateDir treats OPENCLAW_HOME as a homedir and then appends /.openclaw (see + # openclaw/src/config/paths.ts), so if OPENCLAW_HOME is set to ~/.openclaw this produces a nested + # ~/.openclaw/.openclaw path - openclaw.json, daemon.env and lastTouch all land in the nested + # layer while the correct outer path stays empty (a real bug observed on a local machine). The + # state dir should be explicitly overridden via OPENCLAW_STATE_DIR (the engine treats it as a + # direct state dir and does not append); OPENCLAW_HOME should not be set in the user environment. $badHome = $env:OPENCLAW_HOME if ($badHome -and ($badHome.TrimEnd('\').ToLower() -eq $OpenclawHome.TrimEnd('\').ToLower())) { - Write-Warn "检测到 OPENCLAW_HOME=$badHome 已设成 state dir 路径" - Write-Warn " 这会让引擎嵌套 append /.openclaw → ~/.openclaw/.openclaw/,config 全落错位置" - Write-Warn " 正解:清掉 OPENCLAW_HOME,改用 OPENCLAW_STATE_DIR 显式指定 state dir" + Write-Warn "detected OPENCLAW_HOME=$badHome is set to the state dir path" + Write-Warn " this makes the engine nest-append /.openclaw -> ~/.openclaw/.openclaw/, all config lands in the wrong place" + Write-Warn " fix: clear OPENCLAW_HOME and use OPENCLAW_STATE_DIR to explicitly specify the state dir" try { [Environment]::SetEnvironmentVariable("OPENCLAW_HOME", $null, "User") [Environment]::SetEnvironmentVariable("OPENCLAW_HOME", $null, "Process") Remove-Item Env:OPENCLAW_HOME -ErrorAction SilentlyContinue - Write-Ok "已从用户环境变量清掉 OPENCLAW_HOME(新终端生效)" - } catch { Write-Warn "清 OPENCLAW_HOME 失败,请手动删:[Environment]::SetEnvironmentVariable('OPENCLAW_HOME', `$null, 'User')" } + Write-Ok "cleared OPENCLAW_HOME from user env vars (takes effect in a new terminal)" + } catch { Write-Warn "failed to clear OPENCLAW_HOME, please delete manually: [Environment]::SetEnvironmentVariable('OPENCLAW_HOME', `$null, 'User')" } } - # ─── 交互收 AWK_API_KEY ─────────────────────────────────── + # --- Interactive AWK_API_KEY prompt --- $awkKey = $env:AWK_API_KEY if (-not $NoPrompt -and -not $awkKey) { $awkKey = Read-Host "Enter AWK_API_KEY (Volces ARK API key)" } - # ─── 写 .env(业务变量,export 格式给 bash/sh source 用,CLI 裸跑读这个)──── - # 幂等:先剥同 key 旧行再追加。AWK_API_KEY 是 secret,单引号裹 + '\'' 转义内置单引号。 + # --- 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 '\''. $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=" } @@ -425,10 +472,11 @@ function Install-GatewayAndEnv { $homeEsc = $OpenclawHome -replace "'", "'\''" $exportLines += "export OPENCLAW_STATE_DIR='$homeEsc'" Set-Content -Path $dotEnv -Value $exportLines -Encoding UTF8 - Write-Ok ".env written (业务变量,CLI 裸跑用)" + Write-Ok ".env written (business vars, used by the bare CLI)" - # ─── 写 daemon.env(gateway service 用,KEY=value 格式给 gateway.cmd call 用)── - # 只放 3 个固定值 + OPENCLAW_STATE_DIR(gateway 子进程需要 state dir 消歧嵌套)。 + # --- 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=" } @@ -438,36 +486,37 @@ function Install-GatewayAndEnv { $pathLine = "PATH=$(Join-Path $Root 'bin');$(Split-Path $NodeExe);$env:PATH" $daemonLines += $pathLine Set-Content -Path $daemonEnv -Value $daemonLines -Encoding UTF8 - Write-Ok "daemon.env written (gateway service 用,3 固定值 + PATH + OPENCLAW_STATE_DIR)" + Write-Ok "daemon.env written (used by the gateway service, 3 fixed values + PATH + OPENCLAW_STATE_DIR)" - # 把 .env 业务变量加载进当前 install shell,让后续 daemon install / gateway restart / channels login - # 校验 config 时能解析到 AWK_API_KEY / XIAOBEI_HOME(CLI 裸跑本会 . .env,install shell 也得有) + # Load the .env business vars into the current install shell so that subsequent daemon install / + # gateway restart / channels login config validation can resolve AWK_API_KEY / XIAOBEI_HOME (the + # bare CLI would normally `. .env`, and the install shell needs them too). if ($awkKey) { $env:AWK_API_KEY = $awkKey } $env:XIAOBEI_HOME = $Root $env:OPENCLAW_STATE_DIR = $OpenclawHome - # setx 用户环境变量(让新终端 / gateway 子进程继承 AWK_API_KEY) + # setx the user env var (so new terminals / gateway subprocesses inherit AWK_API_KEY) if ($awkKey) { & setx AWK_API_KEY "$awkKey" | Out-Null - Write-Ok "AWK_API_KEY set as user env var (新终端生效)" + Write-Ok "AWK_API_KEY set as user env var (takes effect in a new terminal)" } - # 尝试 daemon install(Windows 支持情况视 openclaw 版本而定) - Write-Host " 尝试 openclaw daemon install..." + # Attempt daemon install (Windows support depends on the openclaw version) + Write-Host " attempting openclaw daemon install..." Invoke-Streamed { & $ClawCmd daemon install } if ($LASTEXITCODE -eq 0) { Write-Ok "gateway daemon installed" Repair-GatewayCmd Invoke-Streamed { & $ClawCmd gateway restart } } else { - Write-Warn "openclaw daemon install 在 Windows 未就绪或失败" - Write-Host " 请开新 PowerShell 终端前台跑 gateway:" + Write-Warn "openclaw daemon install is not ready or failed on Windows" + Write-Host " please open a new PowerShell terminal and run gateway in the foreground:" Write-Host " set AWK_API_KEY=$awkKey" Write-Host " $ClawCmd gateway start" } } -# ─── 12. 自动出微信绑定二维码(首装末尾)────────────────────── +# --- 12. Auto-show WeChat binding QR code (end of first install) --- function Test-WeixinBound { $paths = @( (Join-Path $OpenclawHome "openclaw-weixin\accounts.json"), @@ -479,38 +528,39 @@ function Test-WeixinBound { function Bind-WeixinChannel { if ($SkipBind -or $NoPrompt) { - Write-Host " [i] 跳过微信扫码绑定(XIAOBEI_SKIP_BIND / XIAOBEI_NO_PROMPT=1);后续手动跑:openclaw channels login --channel openclaw-weixin" -ForegroundColor Yellow + 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 } - if (Test-WeixinBound) { Write-Ok "检测到微信账号已绑定,跳过扫码"; return } - if (-not (Test-Path $ClawCmd)) { Write-Warn "openclaw wrapper 未找到($ClawCmd),跳过微信绑定"; return } - Write-Stage "绑定微信 channel(用手机扫码)" - Write-Host " 接下来会出二维码,用微信扫一下、点确认,小贝就能用了。" - Write-Host " 扫码慢没关系,二维码会自动刷新;扫完即继续。" + if (Test-WeixinBound) { Write-Ok "WeChat account already bound, skipping scan"; return } + if (-not (Test-Path $ClawCmd)) { Write-Warn "openclaw wrapper not found ($ClawCmd), skipping WeChat binding"; return } + Write-Stage "Binding WeChat channel (scan with your phone)" + Write-Host " A QR code will appear next; scan it with WeChat and tap confirm, then xiaobei is ready to use." + Write-Host " Take your time scanning - the QR code auto-refreshes; just continue once scanned." Write-Host "" for ($i = 1; $i -le 5; $i++) { & $ClawCmd channels login --channel openclaw-weixin - if (Test-WeixinBound) { Write-Ok "微信账号绑定成功"; return } - if ($i -lt 5) { Write-Warn "本轮未检测到绑定,重出二维码(第 $($i + 1) 次)..." } + if (Test-WeixinBound) { Write-Ok "WeChat account bound successfully"; return } + if ($i -lt 5) { Write-Warn "no binding detected this round, re-showing QR code (attempt $($i + 1))..." } } - Write-Warn "多次扫码未完成绑定。可后续手动跑:$ClawCmd channels login --channel openclaw-weixin" + Write-Warn "binding not completed after multiple scans. You can run manually later: $ClawCmd channels login --channel openclaw-weixin" } -# ─── main ───────────────────────────────────────────────────── +# --- main --- function Main { - Write-Host "wiseflow installer (Windows, atomgit) — 预构建 tarball 路线" -ForegroundColor Magenta + Write-Host "wiseflow installer (Windows, atomgit) - prebuilt tarball route" -ForegroundColor Magenta Write-Host " Program dir : $Root" Write-Host " Runtime dir : $OpenclawHome" Write-Host " Repo : $Repo (atomgit)" - # 检测是否已装(决定走 update 还是 fresh install) + # Detect whether already installed (decides update vs fresh install) $cfgExisting = Join-Path $OpenclawHome "openclaw.json" $isUpdate = (Test-Path $cfgExisting) -and -not $Force if ($isUpdate) { - Write-Warn "检测到已有安装($cfgExisting)→ 走更新路线,保留运行数据(XIAOBEI_FORCE=1 可强覆盖)" - # 先停 gateway(对齐 install.sh stop_gateway_if_running):否则 tar 覆盖 - # tools\node\node.exe、pnpm 重写 node_modules 会撞运行中 gateway 的文件锁 - # ("Can't unlink already-existing object: Permission denied",本机实测) + 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 + # overwriting tools\node\node.exe and pnpm rewriting node_modules will hit the running + # gateway's file locks ("Can't unlink already-existing object: Permission denied", observed + # in practice). Write-Stage "Stopping gateway before update" if (Test-Path $ClawCmd) { Invoke-Streamed { & $ClawCmd gateway stop } } Get-Process node -ErrorAction SilentlyContinue | @@ -535,7 +585,8 @@ function Main { Install-WeixinPlugin if ($isUpdate) { - # 更新路线也自愈 config(对齐 install.sh:openclaw.json 若被极简化则用 template 修复) + # The update route also self-heals the config (mirrors install.sh: if openclaw.json was + # minimized, repair it with the template). Place-Config Write-Stage "Refreshing gateway env and restarting" $envFile = Join-Path $OpenclawHome "daemon.env" @@ -564,7 +615,7 @@ function Main { Write-Host "" Write-Host "Next steps:" -ForegroundColor Cyan $binDir = Join-Path $Root 'bin' - Write-Host " 把 $binDir 加到用户 PATH(安全方式,勿用 setx %PATH% 会截断):" + Write-Host " Add $binDir to the user PATH (safe way - do NOT use setx %PATH%, it truncates):" Write-Host " [Environment]::SetEnvironmentVariable('PATH', `"$binDir;`" + [Environment]::GetEnvironmentVariable('PATH','User'), 'User')" Write-Host "" Write-Host " Dashboard: http://127.0.0.1:18789" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 6dc7e1c6..7519cd11 100755 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,57 +1,68 @@ -# install.ps1 - wiseflow 一键首装脚本(Windows,预构建 tarball 路线,GitHub 专线) +# install.ps1 - wiseflow one-click install script (Windows, prebuilt tarball route, GitHub direct) # -# 与 install-atomgit.ps1 区别:本脚本走 GitHub release(tarball + tag API 都不经镜像),适合能正常访问 GitHub 的网络环境。 -# 国内用户请改用 install-atomgit.ps1(atomgit 镜像专线)。 -# 为对 `irm | iex` 友好(小白一条命令跑通),去掉 [CmdletBinding]param() 头, -# 所有可选参数走环境变量(=1 启用行为开关): -# $env:XIAOBEI_REPO GitHub 仓(owner/repo,默认 TeamWiseFlow/xiaobei;测试可指 bigbrother666sh/wiseflow) -# $env:XIAOBEI_HOME 程序目录覆盖(默认 ~\xiaobei) -# $env:XIAOBEI_TAG 指定 release tag(默认拉最新) -# $env:XIAOBEI_TARBALL 本地已下好的 tarball 路径,跳过下载 -# $env:XIAOBEI_FORCE =1 强覆盖已有运行数据(~\.openclaw) -# $env:XIAOBEI_SKIP_BIND =1 跳过末尾微信扫码绑定 -# $env:XIAOBEI_SKIP_BROWSER=1 跳过 camoufox-cli 浏览器二进制(冒烟/CI) -# $env:XIAOBEI_NO_PROMPT =1 跳过所有交互提示(CI/自动化) +# Difference from install-atomgit.ps1: this script uses GitHub release (tarball + tag API both +# go direct, no mirror), suitable for networks with normal GitHub access. +# For users in mainland China, use install-atomgit.ps1 (atomgit mirror direct line) instead. +# To be friendly to `irm | iex` (so a one-liner works for beginners), the [CmdletBinding]param() +# header is removed; all optional params go through environment variables (=1 to enable): +# $env:XIAOBEI_REPO GitHub repo (owner/repo, default TeamWiseFlow/xiaobei; for testing +# you can point to bigbrother666sh/wiseflow) +# $env:XIAOBEI_HOME Program directory override (default ~\xiaobei) +# $env:XIAOBEI_TAG Specify a release tag (default pulls latest) +# $env:XIAOBEI_TARBALL Path to a locally downloaded tarball, skips download +# $env:XIAOBEI_FORCE =1 force overwrite existing runtime data (~\.openclaw) +# $env:XIAOBEI_SKIP_BIND =1 skip the WeChat scan-to-bind step at the end +# $env:XIAOBEI_SKIP_BROWSER=1 skip camoufox-cli browser binary (smoke/CI) +# $env:XIAOBEI_NO_PROMPT =1 skip all interactive prompts (CI/automation) # -# 用法(PowerShell,需 Git Bash 或 WSL): +# Usage (PowerShell, requires Git Bash or WSL): # irm https://raw.githubusercontent.com/TeamWiseFlow/xiaobei/master/scripts/install.ps1 | iex -# # 或本地: +# # or locally: # powershell -ExecutionPolicy Bypass -File install.ps1 # -# ⚠️ 本文件必须保持 UTF-8 **带 BOM**:Windows PowerShell 5.1 对无 BOM 的 .ps1 按系统 -# ANSI 代码页(中文系统 GBK)解析,中文字符串会乱码并产生解析错误,脚本无法运行。 +# WARNING: this file MUST be kept UTF-8 **without BOM**: under `irm | iex`, irm does not strip +# the BOM, and iex treats the 3 BOM bytes as the first chars of a command name, so the first +# line `#` is parsed as an unknown command and errors (though it does not block the install). +# The local `powershell -File` scenario also works fine without BOM (PowerShell 5.1+ defaults +# to UTF-8 for parsing). All comments in this file are in English (ASCII) to avoid GBK/UTF-8 +# mojibake parsing errors under Windows PowerShell 5.1 with a non-BOM file. # -# 与 install.sh 同构(方案 B 瘦 tarball): -# 1. 拉 xiaobei-{tag}-win-x64.tar.gz(GitHub release,Windows bsdtar 原生支持 gzip,免装 zstd) -# 2. 解压到 $XIAOBEI_HOME(默认 $env:USERPROFILE\xiaobei,程序目录) -# 3. portable node + pnpm install --prod --frozen-lockfile(在 openclaw\ 下) -# 4. pip install --user(skills 的 python deps,有 python 才跑) -# 5. 放 config-templates\openclaw.json → $OPENCLAW_HOME\openclaw.json + 预填微信 binding -# 6. setup-crew.sh(需 bash:Git Bash 或 WSL;无则警告并跳过,用户后续手动跑) -# 7. camoufox-cli:.cmd shim + camoufox-cli install 下 Firefox -# 8. openclaw-weixin 插件:openclaw plugins install ... --pin(npmmirror) -# 9. 交互问 AWK_API_KEY → 写 daemon.env + setx 用户环境变量 → 尝试 openclaw daemon install +# Structurally identical to install.sh (Plan B slim tarball): +# 1. Pull xiaobei-{tag}-win-x64.tar.gz (GitHub release; Windows bsdtar natively supports gzip, +# no need to install zstd) +# 2. Extract to $XIAOBEI_HOME (default $env:USERPROFILE\xiaobei, the program directory) +# 3. portable node + pnpm install --prod --frozen-lockfile (under openclaw\) +# 4. pip install --user (python deps for skills; only runs if python is present) +# 5. Place config-templates\openclaw.json -> $OPENCLAW_HOME\openclaw.json + prefill WeChat binding +# 6. setup-crew.sh (requires bash: Git Bash or WSL; if absent, warn and skip; user can run it +# manually later) +# 7. camoufox-cli: .cmd shim + camoufox-cli install downloads Firefox +# 8. openclaw-weixin plugin: openclaw plugins install ... --pin (npmmirror) +# 9. Interactive prompt for AWK_API_KEY -> write daemon.env + setx user env var -> attempt +# openclaw daemon install # -# 目录职责:$XIAOBEI_HOME(~\xiaobei)= 程序;$OPENCLAW_HOME(~\.openclaw)= 运行数据。 -# Windows 原生 wrapper:$XIAOBEI_HOME\bin\openclaw.cmd(WSL/Git Bash 用户也可用 bin\openclaw)。 +# Directory responsibilities: $XIAOBEI_HOME (~\xiaobei) = program; $OPENCLAW_HOME (~\.openclaw) +# = runtime data. +# Windows native wrapper: $XIAOBEI_HOME\bin\openclaw.cmd (WSL/Git Bash users can also use bin\openclaw). $ErrorActionPreference = "Stop" -# ─── 常量 / 目录(GitHub 专线)─────────────────────────────── -# 可选参数走环境变量(为 `irm | iex` 友好,去掉 [CmdletBinding]param() 头): -# $env:XIAOBEI_REPO GitHub 仓(owner/repo,默认 TeamWiseFlow/xiaobei;测试可指 bigbrother666sh/wiseflow) -# $env:XIAOBEI_HOME 程序目录覆盖(默认 ~\xiaobei) -# $env:XIAOBEI_TAG 指定 release tag(默认拉最新) -# $env:XIAOBEI_TARBALL 本地已下好的 tarball 路径,跳过下载 -# $env:XIAOBEI_FORCE =1 强覆盖已有运行数据(~\.openclaw) -# $env:XIAOBEI_SKIP_BIND =1 跳过末尾微信扫码绑定 -# $env:XIAOBEI_SKIP_BROWSER=1 跳过 camoufox-cli 浏览器二进制(冒烟/CI) -# $env:XIAOBEI_NO_PROMPT =1 跳过所有交互提示(CI/自动化) +# --- Constants / directories (GitHub direct) --- +# Optional params go through env vars (friendly for `irm | iex`, [CmdletBinding]param() header removed): +# $env:XIAOBEI_REPO GitHub repo (owner/repo, default TeamWiseFlow/xiaobei; for testing +# you can point to bigbrother666sh/wiseflow) +# $env:XIAOBEI_HOME Program directory override (default ~\xiaobei) +# $env:XIAOBEI_TAG Specify a release tag (default pulls latest) +# $env:XIAOBEI_TARBALL Path to a locally downloaded tarball, skips download +# $env:XIAOBEI_FORCE =1 force overwrite existing runtime data (~\.openclaw) +# $env:XIAOBEI_SKIP_BIND =1 skip the WeChat scan-to-bind step at the end +# $env:XIAOBEI_SKIP_BROWSER=1 skip camoufox-cli browser binary (smoke/CI) +# $env:XIAOBEI_NO_PROMPT =1 skip all interactive prompts (CI/automation) $Repo = if ($env:XIAOBEI_REPO) { $env:XIAOBEI_REPO } else { "TeamWiseFlow/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" } -# 行为开关(环境变量,=1 启用) +# 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") @@ -68,10 +79,12 @@ 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 } -# 跑原生命令并按行回显 stdout+stderr。pip/npm/camoufox-cli/openclaw 正常会把进度和警告写 stderr, -# 而本脚本顶部 $ErrorActionPreference="Stop" 会把 2>&1 合并进管道的 stderr 行当 terminating error -# 抛 NativeCommandError/RemoteException(用户 Win 实测卡在 pip install 这步)。这里临时切到 Continue -# 跑原生命令,$LASTEXITCODE 仍由调用方据以判成败。 +# 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 +# NativeCommandError/RemoteException (a user's Windows run actually got stuck at pip install this +# way). Here we temporarily switch to Continue to run the native command; $LASTEXITCODE is still +# used by the caller to judge success/failure. function Invoke-Streamed([scriptblock]$sb) { $prev = $ErrorActionPreference $ErrorActionPreference = 'Continue' @@ -79,8 +92,9 @@ function Invoke-Streamed([scriptblock]$sb) { finally { $ErrorActionPreference = $prev } } -# 同 Invoke-Streamed,但把 stdout+stderr 合并成字符串返回(供调用方 -&match)。`2>$null` 在 -# Windows PowerShell 5.1 + Stop 下压不住 .cmd 的 stderr 仍抛 NativeCommandError,这里用 Continue + 2>&1 稳。 +# Same as Invoke-Streamed, but merges stdout+stderr into a single string and returns it (for callers +# that need to -match against the output). `2>$null` under Windows PowerShell 5.1 + Stop does not +# suppress a .cmd's stderr and still throws NativeCommandError; using Continue + 2>&1 is stable. function Capture-Streamed([scriptblock]$sb) { $prev = $ErrorActionPreference $ErrorActionPreference = 'Continue' @@ -88,7 +102,7 @@ function Capture-Streamed([scriptblock]$sb) { finally { $ErrorActionPreference = $prev } } -# ─── 1. 解析最新 release tag(GitHub API)────────────────────── +# --- 1. Resolve latest release tag (GitHub API) --- function Resolve-Tag { if ($env:XIAOBEI_TAG) { return $env:XIAOBEI_TAG } $api = "https://api.github.com/repos/$Repo/releases/latest" @@ -96,113 +110,120 @@ function Resolve-Tag { return $rel.tag_name } -# ─── 2. 下载 tarball(GitHub release)────────────────────────── +# --- 2. Download tarball (GitHub release) --- function Download-Tarball([string]$tag) { $asset = "xiaobei-$tag-win-x64.tar.gz" if ($env:XIAOBEI_TARBALL -and (Test-Path $env:XIAOBEI_TARBALL)) { - Write-Ok "用本地 tarball:$env:XIAOBEI_TARBALL" + Write-Ok "using local tarball: $env:XIAOBEI_TARBALL" return $env:XIAOBEI_TARBALL } $tmp = New-TemporaryFile $url = "https://github.com/$Repo/releases/download/$tag/$asset" - Write-Host " 下载 $url" + Write-Host " downloading $url" Invoke-WebRequest -Uri $url -OutFile $tmp -Headers @{ "User-Agent" = "xiaobei-install" } return $tmp.FullName } -# ─── 3. 解压 ─────────────────────────────────────────────────── +# --- 3. Extract --- function Extract-Tarball([string]$tarball) { if (-not (Get-Command tar -ErrorAction SilentlyContinue)) { - throw "未找到 tar(Windows 10 1803+ 自带 bsdtar;请升级 Windows 或装 Git Bash)" + throw "tar not found (Windows 10 1803+ ships bsdtar; please upgrade Windows or install Git Bash)" } New-Item -ItemType Directory -Force -Path $Root | Out-Null - Write-Host " 解压到 $Root" + Write-Host " extracting to $Root" & tar -xzf $tarball -C $Root - if ($LASTEXITCODE -ne 0) { throw "tar 解压失败 (exit $LASTEXITCODE)" } + if ($LASTEXITCODE -ne 0) { throw "tar extraction failed (exit $LASTEXITCODE)" } } -# ─── 4. pnpm install --prod ──────────────────────────────────── +# --- 4. pnpm install --prod --- function Install-Deps { Write-Stage "Installing dependencies (pnpm install --prod)" $openclawDir = Join-Path $Root "openclaw" - if (-not (Test-Path $NodeExe)) { throw "portable node 未找到:$NodeExe" } + if (-not (Test-Path $NodeExe)) { throw "portable node not found: $NodeExe" } Push-Location $openclawDir try { - # postinstall 子进程(esbuild/protobufjs 等)按 PATH 找 node;目标机器多半没装全局 - # Node('node' is not recognized 实测),把 portable node 前置进 PATH + # postinstall subprocesses (esbuild/protobufjs etc.) look up node via PATH; the target + # machine most likely has no global Node installed ('node' is not recognized, observed in + # practice), so we prepend the portable node directory to PATH. $env:PATH = "$(Split-Path $NodeExe);$env:PATH" - # pnpm 11 不认 npm_config_registry 环境变量(实测仍走 registry.npmjs.org),用显式 --registry + # pnpm 11 does not honor the npm_config_registry env var (in practice it still hits + # registry.npmjs.org), so we pass an explicit --registry. $env:npm_config_registry = "https://registry.npmmirror.com" & $NodeExe $PnpmMjs install --prod --frozen-lockfile --registry https://registry.npmmirror.com - if ($LASTEXITCODE -ne 0) { throw "pnpm install 失败 (exit $LASTEXITCODE)" } + if ($LASTEXITCODE -ne 0) { throw "pnpm install failed (exit $LASTEXITCODE)" } Write-Ok "deps installed" } finally { Pop-Location } } -# ─── 5. python skill deps ────────────────────────────────────── +# --- 5. python skill deps --- function Install-PythonDeps { Write-Stage "Installing python skill deps" $py = Get-Command python -ErrorAction SilentlyContinue - if (-not $py) { Write-Warn "未找到 python,跳过 skill python deps"; return } + if (-not $py) { Write-Warn "python not found, skipping skill python deps"; return } $reqs = @() $reqs += Get-ChildItem -Path (Join-Path $Root "skills") -Filter "requirements.txt" -Recurse -ErrorAction SilentlyContinue $reqs += Get-ChildItem -Path (Join-Path $Root "crews") -Filter "requirements.txt" -Recurse -ErrorAction SilentlyContinue $reqs += Get-ChildItem -Path $Root -Filter "requirements.txt" -ErrorAction SilentlyContinue $reqs = $reqs | Select-Object -ExpandProperty FullName -Unique - if (-not $reqs) { Write-Ok "无 requirements.txt"; return } - # 各行作为 pip 命令行参数传入(非 -r 文件),必须剥掉行内注释(pip 只在 - # requirements 文件里认注释,参数里 `Pillow # 说明` 直接 Invalid requirement 报错); - # 文件是 UTF-8,显式指定编码防中文注释被 ANSI 误读产生乱码参数。 + if (-not $reqs) { Write-Ok "no requirements.txt"; return } + # Each line is passed as a pip command-line argument (not via -r file), so inline comments must + # be stripped (pip only recognizes comments inside a requirements file; `Pillow # desc` passed + # as an arg directly errors with Invalid requirement). The file is UTF-8; we explicitly specify + # the encoding to prevent Chinese comments from being misread as ANSI and producing garbled args. $merged = ($reqs | ForEach-Object { Get-Content $_ -Encoding UTF8 -ErrorAction SilentlyContinue } | ForEach-Object { ($_ -replace '#.*$', '').Trim() } | Where-Object { $_ } | Sort-Object -Unique) - if (-not $merged) { Write-Ok "无 python 依赖"; return } + if (-not $merged) { Write-Ok "no python deps"; return } Invoke-Streamed { python -m pip install --user $merged } - if ($LASTEXITCODE -ne 0) { Write-Warn "pip install 非零退出;可后续手动补装 skills 的 python 依赖" } + if ($LASTEXITCODE -ne 0) { Write-Warn "pip install returned non-zero; you can manually install the skills' python deps later" } else { Write-Ok "python deps installed" } } -# ─── 6. 放 config template(对齐 install.sh place_config_template)──── -# template 已预置 channels.openclaw-weixin / bindings / session.dmScope,不再运行时 mutate -# (sh 版曾因运行时 mutate 把 plugins 顶层写坏致 "Invalid input",已同构移除)。 -# 健康检查:现有 config 缺 models/agents.defaults(多半是 Install-WeixinPlugin 先跑时 -# openclaw plugins install 自建的极简 config,或 openclaw 首启自动生成)→ 备份后用 -# template 覆盖,否则 channels/bindings 永远缺失、小贝起不来。 +# --- 6. Place config template (mirrors install.sh place_config_template) --- +# The template already pre-populates channels.openclaw-weixin / bindings / session.dmScope; we no +# longer mutate it at runtime (the sh version once broke the plugins top-level via runtime mutation, +# causing "Invalid input"; that mutation has been removed in parallel here). +# Health check: if the existing config is missing models/agents.defaults (most likely a minimal config +# auto-created by `openclaw plugins install` when Install-WeixinPlugin ran first, or by openclaw's +# first launch), back it up and overwrite with the template; otherwise channels/bindings would be +# missing forever and xiaobei could never start. function Place-Config { Write-Stage "Placing config template" New-Item -ItemType Directory -Force -Path $OpenclawHome | Out-Null $cfg = Join-Path $OpenclawHome "openclaw.json" $tmpl = Join-Path $Root "config-templates\openclaw.json" - if (-not (Test-Path $tmpl)) { Write-Err "config template 缺失:$tmpl(tarball 损坏?)"; return } + if (-not (Test-Path $tmpl)) { Write-Err "config template missing: $tmpl (tarball corrupted?)"; return } $needPlace = $false; $reason = "" - if (-not (Test-Path $cfg)) { $needPlace = $true; $reason = "不存在" } + if (-not (Test-Path $cfg)) { $needPlace = $true; $reason = "not present" } elseif ($Force) { $needPlace = $true; $reason = "XIAOBEI_FORCE=1" } else { try { - # 必须显式按 UTF-8 读:openclaw 写的 config 是 UTF-8 无 BOM,PS 5.1 的 - # Get-Content 默认按 ANSI 代码页读,中文路径旁的 \\ 转义会被双字节吞掉, - # 有效 config 被误判"解析失败"遭覆盖(本机实测)。 + # Must explicitly read as UTF-8: openclaw writes its config as UTF-8 without BOM, while + # PS 5.1's Get-Content defaults to the ANSI code page; a \\ escape next to a Chinese + # path can get swallowed by a double-byte char, causing a valid config to be misjudged + # as "parse failed" and overwritten (observed in practice). $j = [System.IO.File]::ReadAllText($cfg) | ConvertFrom-Json -ErrorAction Stop - if (-not ($j.models -and $j.agents -and $j.agents.defaults)) { $needPlace = $true; $reason = "缺 models/agents.defaults(疑似被极简化)" } - } catch { $needPlace = $true; $reason = "解析失败" } + if (-not ($j.models -and $j.agents -and $j.agents.defaults)) { $needPlace = $true; $reason = "missing models/agents.defaults (likely minimized)" } + } catch { $needPlace = $true; $reason = "parse failed" } } if ($needPlace) { if (Test-Path $cfg) { $bak = "$cfg.bak.$([int][double]::Parse((Get-Date -UFormat %s)))" Copy-Item $cfg $bak - Write-Warn "openclaw.json $reason → 用 template 覆盖(旧文件备份到 $bak)" + Write-Warn "openclaw.json $reason -> overwriting with template (old file backed up to $bak)" } Copy-Item $tmpl $cfg -Force Write-Ok "placed openclaw.json template" } else { - Write-Ok "openclaw.json 已存在且健康(有 models + agents.defaults),保留" + Write-Ok "openclaw.json already present and healthy (has models + agents.defaults), keeping" } - # 把 plugins.load.paths 里的 ${XIAOBEI_HOME} env ref 解析成绝对路径,避免 CLI 上下文没 - # XIAOBEI_HOME 时 "plugin path not found" 误报;AWK_API_KEY 是 secret 保持 env ref 不动。 - # 文本级替换(template 中 ${XIAOBEI_HOME} 仅出现在 plugins.load.paths);JSON 字符串里 - # 反斜杠是转义符,路径统一写正斜杠。幂等:已解析则 no-op。 + # Resolve the ${XIAOBEI_HOME} env ref inside plugins.load.paths to an absolute path, to avoid a + # "plugin path not found" false alarm when the CLI context lacks XIAOBEI_HOME; AWK_API_KEY is a + # secret so its env ref is left untouched. This is a file-level replacement (the ${XIAOBEI_HOME} + # template token only appears in plugins.load.paths); inside a JSON string a backslash is an + # escape char, so we normalize paths to forward slashes. Idempotent: a no-op if already resolved. $raw = [System.IO.File]::ReadAllText($cfg) if ($raw.Contains('${XIAOBEI_HOME}')) { $rootFwd = $Root -replace '\\', '/' @@ -211,10 +232,11 @@ function Place-Config { } } -# ─── 7. setup-crew(需 bash)────────────────────────────────── -# 找一个真正可用的 bash:优先 Git Bash(吃得了 C:/ 风格路径);PATH 里的 bash 可能是 -# C:\Windows\system32\bash.exe(WSL 启动器)——没装发行版时直接报错退出,且 WSL bash -# 对 Windows 路径参数不做转换,都不能直接用。每个候选实测 `bash -c echo` 通过才算数。 +# --- 7. setup-crew (requires bash) --- +# Find a genuinely usable bash: prefer Git Bash (it handles C:/ style paths well); the bash on PATH +# might be C:\Windows\system32\bash.exe (the WSL launcher) which errors out immediately when no distro +# is installed, and WSL bash does not convert Windows path arguments either, so neither can be used +# directly. Each candidate is actually tested with `bash -c echo` before being accepted. function Find-WorkingBash { $candidates = @() $git = Get-Command git -ErrorAction SilentlyContinue @@ -226,7 +248,8 @@ function Find-WorkingBash { $candidates += "$env:ProgramFiles\Git\bin\bash.exe" $candidates += "${env:ProgramFiles(x86)}\Git\bin\bash.exe" $inPath = Get-Command bash -ErrorAction SilentlyContinue - # system32 的 WSL 启动器排最后(仅当其可用且无 Git Bash 时兜底) + # The system32 WSL launcher goes last (only used as a fallback when it is available and no Git + # Bash exists). if ($inPath) { $candidates += $inPath.Source } foreach ($b in $candidates) { if ($b -and (Test-Path $b)) { @@ -240,60 +263,77 @@ function Find-WorkingBash { function Run-SetupCrew { Write-Stage "Setting up crew templates (needs bash)" $sh = Join-Path $Root "scripts\setup-crew.sh" - if (-not (Test-Path $sh)) { Write-Warn "setup-crew.sh 不在 tarball 内,跳过"; return } + if (-not (Test-Path $sh)) { Write-Warn "setup-crew.sh is not inside the tarball, skipping"; return } $bashExe = Find-WorkingBash if (-not $bashExe) { - Write-Warn "未找到可用的 bash(setup-crew.sh 是 bash 脚本;注意 system32\bash.exe 是 WSL 启动器,没装发行版不可用)" - Write-Host " 请装 Git Bash(https://git-scm.com)或 WSL 发行版,然后手动跑:" + Write-Warn "no usable bash found (setup-crew.sh is a bash script; note system32\bash.exe is the WSL launcher and is unusable without a distro installed)" + Write-Host " Please install Git Bash (https://git-scm.com) or a WSL distro, then run manually:" Write-Host " set OPENCLAW_HOME=$OpenclawHome" Write-Host " set XIAOBEI_BIN_DIR=$(Join-Path $Root 'bin')" Write-Host " bash `"$sh`"" return } Write-Host " bash: $bashExe" - # bash 不认反斜杠(会被当转义),传正斜杠路径给 setup-crew.sh - # ⚠️ 用 OPENCLAW_STATE_DIR 而非 OPENCLAW_HOME:引擎 resolveStateDir 把 OPENCLAW_HOME 当 homedir - # 再 append /.openclaw(见 openclaw/src/config/paths.ts),若设 OPENCLAW_HOME=~/.openclaw 会产出 - # ~/.openclaw/.openclaw 嵌套层——后续 weixin/gateway CLI 调用都把 config/npm/openclaw.sqlite 写嵌套层, - # 外层正解路径反而空(本机实测真 bug)。OPENCLAW_STATE_DIR 引擎当直接 state dir、不 append。 + # bash does not understand backslashes (it would treat them as escapes), so we pass forward-slash + # paths to setup-crew.sh. + # WARNING: use OPENCLAW_STATE_DIR rather than OPENCLAW_HOME: the engine's resolveStateDir treats + # OPENCLAW_HOME as a homedir and then appends /.openclaw (see openclaw/src/config/paths.ts); if + # OPENCLAW_HOME is set to ~/.openclaw this produces a nested ~/.openclaw/.openclaw layer, and all + # subsequent weixin/gateway CLI calls write config/npm/openclaw.sqlite into that nested layer while + # the correct outer path stays empty (a real bug observed on a local machine). OPENCLAW_STATE_DIR + # is treated by the engine as a direct state dir and is not appended to. $env:OPENCLAW_STATE_DIR = ($OpenclawHome -replace '\\', '/') $env:XIAOBEI_BIN_DIR = ((Join-Path $Root "bin") -replace '\\', '/') + # setup-crew.sh uses OPENCLAW_HOME internally to resolve CONFIG_PATH (it does NOT read + # OPENCLAW_STATE_DIR), so we must explicitly pass a forward-slash path; otherwise bash falls back + # to $HOME/.openclaw, and under Git Bash's MSYS path conversion + a Chinese username this yields a + # bogus path like C:\c\Users\\.openclaw (ENOENT). + $env:OPENCLAW_HOME = ($OpenclawHome -replace '\\', '/') Invoke-Streamed { & $bashExe ($sh -replace '\\', '/') } - if ($LASTEXITCODE -ne 0) { Write-Warn "setup-crew.sh 非零退出(可后续手动 --force 修复)" } + # After setup-crew.sh finishes, clear OPENCLAW_HOME so subsequent install phases (gateway/daemon) + # do not inherit this value - the engine would treat OPENCLAW_HOME as a homedir and append + # /.openclaw, producing a nested layer. + Remove-Item Env:OPENCLAW_HOME -ErrorAction SilentlyContinue + if ($LASTEXITCODE -ne 0) { Write-Warn "setup-crew.sh exited non-zero (you can manually re-run with --force later to fix)" } else { Write-Ok "crew templates set up" } } -# ─── 8. camoufox-cli ─────────────────────────────────────────── +# --- 8. camoufox-cli --- function Install-CamoufoxCli { if ($SkipBrowser) { - Write-Host " [i] 跳过 camoufox-cli 浏览器二进制(XIAOBEI_SKIP_BROWSER=1);后续手动:camoufox-cli install" -ForegroundColor Yellow + Write-Host " [i] skipping camoufox-cli browser binary (XIAOBEI_SKIP_BROWSER=1); run manually later: camoufox-cli install" -ForegroundColor Yellow return } Write-Stage "Installing camoufox-cli browser" $fork = Join-Path $Root "camoufox-cli" - if (-not (Test-Path $fork)) { Write-Warn "camoufox-cli fork 不在 tarball 内:$fork;跳过"; return } - # $Root\bin 放 camoufox-cli.cmd shim,前置进 PATH 让下面 `camoufox-cli install` 找得到 + if (-not (Test-Path $fork)) { Write-Warn "camoufox-cli fork not in tarball: $fork; skipping"; return } + # Place a camoufox-cli.cmd shim in $Root\bin and prepend it to PATH so the `camoufox-cli install` + # below can be found. $env:PATH = "$(Join-Path $Root 'bin');$(Split-Path $NodeExe);$env:PATH" $env:npm_config_registry = "https://registry.npmmirror.com" $camoufoxBin = Join-Path $Root "bin\camoufox-cli.cmd" $cliJs = Join-Path $fork "dist\cli.js" $hasDeps = Test-Path (Join-Path $fork "node_modules\camoufox-js") - # fork 运行时依赖:dist 是 tsc 编译,camoufox-js/playwright-core/pdf-lib 是 external import, - # 运行时靠 fork 目录下的 node_modules 解析。tarball 为瘦身 ship 的 fork 不带 node_modules。 + # Runtime deps of the fork: dist is a tsc build, and camoufox-js/playwright-core/pdf-lib are + # external imports resolved at runtime via the node_modules under the fork directory. The fork + # shipped in the slim tarball does not carry node_modules. if (-not $hasDeps) { Push-Location $fork try { & $NpmCmd install --omit=dev - if ($LASTEXITCODE -ne 0) { Write-Warn "camoufox-cli fork deps install 失败"; return } + if ($LASTEXITCODE -ne 0) { Write-Warn "camoufox-cli fork deps install failed"; return } } finally { Pop-Location } } - # Windows 不走 `npm install -g $fork`:npm 把 fork 装进 $prefix\node_modules 时撞 fork 源目录 - # 本身(EEXIST: file already exists $Root\camoufox-cli)——Windows npm 不能像 Linux 那样 symlink - # 到源目录。改仿 openclaw.cmd 写 .cmd shim 直接跑 node $fork\dist\cli.js,fork 的 node_modules - # 已就位,Node 从 cli.js 向上解析依赖;camoufox-cli JS 用 __dirname 定位自身包根,行为与全局装一致。 - if (-not (Test-Path $cliJs)) { Write-Warn "camoufox-cli 入口未找到:$cliJs;跳过"; return } + # Windows does not go through `npm install -g $fork`: when npm installs the fork into + # $prefix\node_modules it collides with the fork's own source directory (EEXIST: file already + # exists $Root\camoufox-cli) - Windows npm cannot symlink to the source dir the way Linux does. + # Instead, mirroring openclaw.cmd, we write a .cmd shim that directly runs + # node $fork\dist\cli.js; the fork's node_modules is already in place and Node resolves deps + # upward from cli.js. camoufox-cli's JS uses __dirname to locate its own package root, so the + # behavior matches a global install. + if (-not (Test-Path $cliJs)) { Write-Warn "camoufox-cli entry not found: $cliJs; skipping"; return } $shimLines = @( '@echo off' 'setlocal' @@ -303,37 +343,37 @@ function Install-CamoufoxCli { Set-Content -Path $camoufoxBin -Value $shimLines -Encoding ASCII Write-Ok "camoufox-cli shim written: $camoufoxBin" - Write-Host " 下 Firefox binary(首次 ~557MB)..." + Write-Host " downloading Firefox binary (first time ~557MB)..." Invoke-Streamed { camoufox-cli install } - if ($LASTEXITCODE -ne 0) { Write-Warn "camoufox-cli install 失败;可后续手动:camoufox-cli install" } + if ($LASTEXITCODE -ne 0) { Write-Warn "camoufox-cli install failed; you can run manually later: camoufox-cli install" } else { Write-Ok "camoufox-cli ready" } } -# ─── 9. openclaw-weixin 插件 ────────────────────────────────── +# --- 9. openclaw-weixin plugin --- function Install-WeixinPlugin { Write-Stage "Installing WeChat plugin" - if (-not (Test-Path $ClawCmd)) { Write-Warn "openclaw wrapper 未找到:$ClawCmd;跳过"; return } + if (-not (Test-Path $ClawCmd)) { Write-Warn "openclaw wrapper not found: $ClawCmd; skipping"; return } $pkg = "@tencent-weixin/openclaw-weixin"; $ver = "2.4.6" $pin = Join-Path $Root "openclaw-weixin.version.json" if (Test-Path $pin) { try { $j = Get-Content $pin -Raw | ConvertFrom-Json $pkg = $j.'openclaw-weixin'.package; $ver = $j.'openclaw-weixin'.version - } catch { Write-Warn "pin 文件解析失败,用默认 $pkg@$ver" } + } catch { Write-Warn "pin file parse failed, using default $pkg@$ver" } } $env:npm_config_registry = "https://registry.npmmirror.com" $listOut = Capture-Streamed { & $ClawCmd plugins list } if ($listOut -match "openclaw-weixin") { Write-Ok "openclaw-weixin plugin already installed"; return } Invoke-Streamed { & $ClawCmd plugins install "$pkg@$ver" --pin } if ($LASTEXITCODE -eq 0) { Write-Ok "openclaw-weixin plugin installed" } - else { Write-Warn "插件安装失败;可后续手动:$ClawCmd plugins install $pkg@$ver --pin" } + else { Write-Warn "plugin install failed; you can run manually later: $ClawCmd plugins install $pkg@$ver --pin" } } -# ─── 10. awada 本地插件 deps(ws + zod)────────────────────── +# --- 10. awada local plugin deps (ws + zod) --- function Install-AwadaPlugin { Write-Stage "Installing awada plugin deps" $awada = Join-Path $Root "awada" - if (-not (Test-Path $awada)) { Write-Warn "awada 不在 tarball 内:$awada;跳过"; return } + if (-not (Test-Path $awada)) { Write-Warn "awada not in tarball: $awada; skipping"; return } $hasWs = Test-Path (Join-Path $awada "node_modules\ws") $hasZod = Test-Path (Join-Path $awada "node_modules\zod") if ($hasWs -and $hasZod) { Write-Ok "awada deps already installed"; return } @@ -342,16 +382,18 @@ function Install-AwadaPlugin { try { & $NpmCmd install --omit=dev if ($LASTEXITCODE -eq 0) { Write-Ok "awada deps installed" } - else { Write-Warn "awada deps install 失败;可后续手动:cd $awada ; npm install --omit=dev" } + else { Write-Warn "awada deps install failed; you can run manually later: cd $awada ; npm install --omit=dev" } } finally { Pop-Location } } -# ─── 10.5 修复 daemon 启动脚本编码(openclaw 上游 bug 的 workaround)── -# openclaw daemon install 写出的 gateway.cmd / gateway.vbs 是 UTF-8(无 BOM), -# 而 cmd.exe / WScript 按系统 ANSI 代码页读取脚本文件——安装路径含非 ASCII 字符 -# (如中文用户名)时路径变乱码,node.exe 找不到,gateway 永远起不来(本机实测)。 -# workaround:把长路径替换成 8.3 短路径(纯 ASCII)后以 ASCII 重写;无 8.3 名 -# 可用时退回按 ANSI 代码页写(cmd.exe 按 ANSI 读,编码一致即可)。幂等。 +# --- 10.5 Fix daemon launch-script encoding (workaround for an openclaw upstream bug) --- +# The gateway.cmd / gateway.vbs written out by `openclaw daemon install` are UTF-8 (no BOM), but +# cmd.exe / WScript read script files using the system ANSI code page - so when the install path +# contains non-ASCII chars (e.g. a Chinese username) the path turns into mojibake, node.exe cannot +# be found, and the gateway never starts (observed on a local machine). +# Workaround: replace long paths with their 8.3 short paths (pure ASCII) and rewrite as ASCII; if no +# 8.3 name is available, fall back to writing in the ANSI code page (cmd.exe reads it as ANSI, so the +# encodings line up). Idempotent. function Repair-GatewayCmd { $files = @((Join-Path $OpenclawHome "gateway.cmd"), (Join-Path $OpenclawHome "gateway.vbs")) $fso = $null @@ -370,51 +412,56 @@ function Repair-GatewayCmd { } else { [System.IO.File]::WriteAllText($f, $txt, [System.Text.ASCIIEncoding]::new()) } - Write-Ok "修复 $(Split-Path $f -Leaf) 编码(非 ASCII 路径 → 8.3 短路径)" + Write-Ok "fixed $(Split-Path $f -Leaf) encoding (non-ASCII path -> 8.3 short path)" } } -# ─── 11. 交互收 AWK_API_KEY + 起 gateway ────────────────────── -# env 分工(对齐 install.sh / 上游踩坑经验): -# ~/.openclaw/.env ← 业务变量(AWK_API_KEY/XIAOBEI_HOME/OPENCLAW_STATE_DIR),openclaw CLI 裸跑用 -# ~/.openclaw/daemon.env ← gateway service 用,只放 3 个固定值(OPENCLAW_BROWSER_TIMEOUT_MS/ -# OPENCLAW_DISABLE_BONJOUR/PATH)+ OPENCLAW_STATE_DIR -# 原因:Linux systemd 那套 EnvironmentFile= 加载器对含特殊字符的业务变量(PATH 带分号、 -# AWK_API_KEY 带连字符)有踩坑历史,业务变量挪 .env 让 systemd 只加载稳的固定值。 -# Windows 不走 systemd,gateway.cmd 的 `call daemon.env` 没这个坑,但为风格统一照此分工。 +# --- 11. Interactive AWK_API_KEY prompt + start gateway --- +# Env division (mirrors install.sh / upstream lessons learned): +# ~/.openclaw/.env <- business vars (AWK_API_KEY/XIAOBEI_HOME/OPENCLAW_STATE_DIR), sourced by the +# openclaw CLI when run bare +# ~/.openclaw/daemon.env <- used by the gateway service, holds only 3 fixed values +# (OPENCLAW_BROWSER_TIMEOUT_MS/OPENCLAW_DISABLE_BONJOUR/PATH) + OPENCLAW_STATE_DIR +# Reason: the Linux systemd EnvironmentFile= loader has a history of choking on business vars that +# contain special chars (PATH with semicolons, AWK_API_KEY with hyphens), so business vars were moved +# to .env and systemd only loads the stable fixed values. Windows does not use systemd and +# gateway.cmd's `call daemon.env` does not have this pitfall, but we keep the same division for +# consistency. function Install-GatewayAndEnv { Write-Stage "Configuring API key and gateway" New-Item -ItemType Directory -Force -Path $OpenclawHome | Out-Null $dotEnv = Join-Path $OpenclawHome ".env" $daemonEnv = Join-Path $OpenclawHome "daemon.env" - # ─── 检测/清掉错误的 OPENCLAW_HOME 用户环境变量 ─────────────── - # 引擎 resolveStateDir 把 OPENCLAW_HOME 当 homedir 再 append /.openclaw(见 - # openclaw/src/config/paths.ts),故若 OPENCLAW_HOME 已被设成 ~/.openclaw,会产出 - # ~/.openclaw/.openclaw 嵌套路径——openclaw.json、daemon.env、lastTouch 全落嵌套层, - # 外层正解路径反而空(本机实测过的真 bug)。state dir 应该用 OPENCLAW_STATE_DIR 显式 - # 覆盖(引擎把它当直接 state dir、不 append),OPENCLAW_HOME 不该在用户环境里设。 + # --- Detect / clear a bogus OPENCLAW_HOME user env var --- + # The engine's resolveStateDir treats OPENCLAW_HOME as a homedir and then appends /.openclaw (see + # openclaw/src/config/paths.ts), so if OPENCLAW_HOME is set to ~/.openclaw this produces a nested + # ~/.openclaw/.openclaw path - openclaw.json, daemon.env and lastTouch all land in the nested + # layer while the correct outer path stays empty (a real bug observed on a local machine). The + # state dir should be explicitly overridden via OPENCLAW_STATE_DIR (the engine treats it as a + # direct state dir and does not append); OPENCLAW_HOME should not be set in the user environment. $badHome = $env:OPENCLAW_HOME if ($badHome -and ($badHome.TrimEnd('\').ToLower() -eq $OpenclawHome.TrimEnd('\').ToLower())) { - Write-Warn "检测到 OPENCLAW_HOME=$badHome 已设成 state dir 路径" - Write-Warn " 这会让引擎嵌套 append /.openclaw → ~/.openclaw/.openclaw/,config 全落错位置" - Write-Warn " 正解:清掉 OPENCLAW_HOME,改用 OPENCLAW_STATE_DIR 显式指定 state dir" + Write-Warn "detected OPENCLAW_HOME=$badHome is set to the state dir path" + Write-Warn " this makes the engine nest-append /.openclaw -> ~/.openclaw/.openclaw/, all config lands in the wrong place" + Write-Warn " fix: clear OPENCLAW_HOME and use OPENCLAW_STATE_DIR to explicitly specify the state dir" try { [Environment]::SetEnvironmentVariable("OPENCLAW_HOME", $null, "User") [Environment]::SetEnvironmentVariable("OPENCLAW_HOME", $null, "Process") Remove-Item Env:OPENCLAW_HOME -ErrorAction SilentlyContinue - Write-Ok "已从用户环境变量清掉 OPENCLAW_HOME(新终端生效)" - } catch { Write-Warn "清 OPENCLAW_HOME 失败,请手动删:[Environment]::SetEnvironmentVariable('OPENCLAW_HOME', `$null, 'User')" } + Write-Ok "cleared OPENCLAW_HOME from user env vars (takes effect in a new terminal)" + } catch { Write-Warn "failed to clear OPENCLAW_HOME, please delete manually: [Environment]::SetEnvironmentVariable('OPENCLAW_HOME', `$null, 'User')" } } - # ─── 交互收 AWK_API_KEY ─────────────────────────────────── + # --- Interactive AWK_API_KEY prompt --- $awkKey = $env:AWK_API_KEY if (-not $NoPrompt -and -not $awkKey) { $awkKey = Read-Host "Enter AWK_API_KEY (Volces ARK API key)" } - # ─── 写 .env(业务变量,export 格式给 bash/sh source 用,CLI 裸跑读这个)──── - # 幂等:先剥同 key 旧行再追加。AWK_API_KEY 是 secret,单引号裹 + '\'' 转义内置单引号。 + # --- 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 '\''. $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=" } @@ -427,10 +474,11 @@ function Install-GatewayAndEnv { $homeEsc = $OpenclawHome -replace "'", "'\''" $exportLines += "export OPENCLAW_STATE_DIR='$homeEsc'" Set-Content -Path $dotEnv -Value $exportLines -Encoding UTF8 - Write-Ok ".env written (业务变量,CLI 裸跑用)" + Write-Ok ".env written (business vars, used by the bare CLI)" - # ─── 写 daemon.env(gateway service 用,KEY=value 格式给 gateway.cmd call 用)── - # 只放 3 个固定值 + OPENCLAW_STATE_DIR(gateway 子进程需要 state dir 消歧嵌套)。 + # --- 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=" } @@ -440,36 +488,37 @@ function Install-GatewayAndEnv { $pathLine = "PATH=$(Join-Path $Root 'bin');$(Split-Path $NodeExe);$env:PATH" $daemonLines += $pathLine Set-Content -Path $daemonEnv -Value $daemonLines -Encoding UTF8 - Write-Ok "daemon.env written (gateway service 用,3 固定值 + PATH + OPENCLAW_STATE_DIR)" + Write-Ok "daemon.env written (used by the gateway service, 3 fixed values + PATH + OPENCLAW_STATE_DIR)" - # 把 .env 业务变量加载进当前 install shell,让后续 daemon install / gateway restart / channels login - # 校验 config 时能解析到 AWK_API_KEY / XIAOBEI_HOME(CLI 裸跑本会 . .env,install shell 也得有) + # Load the .env business vars into the current install shell so that subsequent daemon install / + # gateway restart / channels login config validation can resolve AWK_API_KEY / XIAOBEI_HOME (the + # bare CLI would normally `. .env`, and the install shell needs them too). if ($awkKey) { $env:AWK_API_KEY = $awkKey } $env:XIAOBEI_HOME = $Root $env:OPENCLAW_STATE_DIR = $OpenclawHome - # setx 用户环境变量(让新终端 / gateway 子进程继承 AWK_API_KEY) + # setx the user env var (so new terminals / gateway subprocesses inherit AWK_API_KEY) if ($awkKey) { & setx AWK_API_KEY "$awkKey" | Out-Null - Write-Ok "AWK_API_KEY set as user env var (新终端生效)" + Write-Ok "AWK_API_KEY set as user env var (takes effect in a new terminal)" } - # 尝试 daemon install(Windows 支持情况视 openclaw 版本而定) - Write-Host " 尝试 openclaw daemon install..." + # Attempt daemon install (Windows support depends on the openclaw version) + Write-Host " attempting openclaw daemon install..." Invoke-Streamed { & $ClawCmd daemon install } if ($LASTEXITCODE -eq 0) { Write-Ok "gateway daemon installed" Repair-GatewayCmd Invoke-Streamed { & $ClawCmd gateway restart } } else { - Write-Warn "openclaw daemon install 在 Windows 未就绪或失败" - Write-Host " 请开新 PowerShell 终端前台跑 gateway:" + Write-Warn "openclaw daemon install is not ready or failed on Windows" + Write-Host " please open a new PowerShell terminal and run gateway in the foreground:" Write-Host " set AWK_API_KEY=$awkKey" Write-Host " $ClawCmd gateway start" } } -# ─── 12. 自动出微信绑定二维码(首装末尾)────────────────────── +# --- 12. Auto-show WeChat binding QR code (end of first install) --- function Test-WeixinBound { $paths = @( (Join-Path $OpenclawHome "openclaw-weixin\accounts.json"), @@ -481,38 +530,39 @@ function Test-WeixinBound { function Bind-WeixinChannel { if ($SkipBind -or $NoPrompt) { - Write-Host " [i] 跳过微信扫码绑定(XIAOBEI_SKIP_BIND / XIAOBEI_NO_PROMPT=1);后续手动跑:openclaw channels login --channel openclaw-weixin" -ForegroundColor Yellow + 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 } - if (Test-WeixinBound) { Write-Ok "检测到微信账号已绑定,跳过扫码"; return } - if (-not (Test-Path $ClawCmd)) { Write-Warn "openclaw wrapper 未找到($ClawCmd),跳过微信绑定"; return } - Write-Stage "绑定微信 channel(用手机扫码)" - Write-Host " 接下来会出二维码,用微信扫一下、点确认,小贝就能用了。" - Write-Host " 扫码慢没关系,二维码会自动刷新;扫完即继续。" + if (Test-WeixinBound) { Write-Ok "WeChat account already bound, skipping scan"; return } + if (-not (Test-Path $ClawCmd)) { Write-Warn "openclaw wrapper not found ($ClawCmd), skipping WeChat binding"; return } + Write-Stage "Binding WeChat channel (scan with your phone)" + Write-Host " A QR code will appear next; scan it with WeChat and tap confirm, then xiaobei is ready to use." + Write-Host " Take your time scanning - the QR code auto-refreshes; just continue once scanned." Write-Host "" for ($i = 1; $i -le 5; $i++) { & $ClawCmd channels login --channel openclaw-weixin - if (Test-WeixinBound) { Write-Ok "微信账号绑定成功"; return } - if ($i -lt 5) { Write-Warn "本轮未检测到绑定,重出二维码(第 $($i + 1) 次)..." } + if (Test-WeixinBound) { Write-Ok "WeChat account bound successfully"; return } + if ($i -lt 5) { Write-Warn "no binding detected this round, re-showing QR code (attempt $($i + 1))..." } } - Write-Warn "多次扫码未完成绑定。可后续手动跑:$ClawCmd channels login --channel openclaw-weixin" + Write-Warn "binding not completed after multiple scans. You can run manually later: $ClawCmd channels login --channel openclaw-weixin" } -# ─── main ───────────────────────────────────────────────────── +# --- main --- function Main { - Write-Host "wiseflow installer (Windows, GitHub) — 预构建 tarball 路线" -ForegroundColor Magenta + Write-Host "wiseflow installer (Windows, GitHub) - prebuilt tarball route" -ForegroundColor Magenta Write-Host " Program dir : $Root" Write-Host " Runtime dir : $OpenclawHome" Write-Host " Repo : $Repo (GitHub)" - # 检测是否已装(决定走 update 还是 fresh install) + # Detect whether already installed (decides update vs fresh install) $cfgExisting = Join-Path $OpenclawHome "openclaw.json" $isUpdate = (Test-Path $cfgExisting) -and -not $Force if ($isUpdate) { - Write-Warn "检测到已有安装($cfgExisting)→ 走更新路线,保留运行数据(XIAOBEI_FORCE=1 可强覆盖)" - # 先停 gateway(对齐 install.sh stop_gateway_if_running):否则 tar 覆盖 - # tools\node\node.exe、pnpm 重写 node_modules 会撞运行中 gateway 的文件锁 - # ("Can't unlink already-existing object: Permission denied",本机实测) + 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 + # overwriting tools\node\node.exe and pnpm rewriting node_modules will hit the running + # gateway's file locks ("Can't unlink already-existing object: Permission denied", observed + # in practice). Write-Stage "Stopping gateway before update" if (Test-Path $ClawCmd) { Invoke-Streamed { & $ClawCmd gateway stop } } Get-Process node -ErrorAction SilentlyContinue | @@ -537,7 +587,8 @@ function Main { Install-WeixinPlugin if ($isUpdate) { - # 更新路线也自愈 config(对齐 install.sh:openclaw.json 若被极简化则用 template 修复) + # The update route also self-heals the config (mirrors install.sh: if openclaw.json was + # minimized, repair it with the template). Place-Config Write-Stage "Refreshing gateway env and restarting" $envFile = Join-Path $OpenclawHome "daemon.env" @@ -559,14 +610,14 @@ function Main { Write-Host "" if ($isUpdate) { - Write-Host "🦞 wiseflow updated successfully!" -ForegroundColor Green + Write-Host "wiseflow updated successfully!" -ForegroundColor Green } else { - Write-Host "🦞 wiseflow installed successfully!" -ForegroundColor Green + Write-Host "wiseflow installed successfully!" -ForegroundColor Green } Write-Host "" Write-Host "Next steps:" -ForegroundColor Cyan $binDir = Join-Path $Root 'bin' - Write-Host " 把 $binDir 加到用户 PATH(安全方式,勿用 setx %PATH% 会截断):" + Write-Host " Add $binDir to the user PATH (safe way - do NOT use setx %PATH%, it truncates):" Write-Host " [Environment]::SetEnvironmentVariable('PATH', `"$binDir;`" + [Environment]::GetEnvironmentVariable('PATH','User'), 'User')" Write-Host "" Write-Host " Dashboard: http://127.0.0.1:18789" From 0a49d2debf995793a48b81dc498ddb038a9c7ec2 Mon Sep 17 00:00:00 2001 From: codes-factory-of-bg Date: Thu, 6 Aug 2026 16:49:16 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(install):=20=E4=BF=AE=E5=A4=8D=20macOS?= =?UTF-8?q?=20=E5=AE=89=E8=A3=85=E4=B8=A4=E4=B8=AA=E9=98=BB=E6=96=AD=20bug?= =?UTF-8?q?=20+=20=E5=9B=9B=E8=84=9A=E6=9C=AC=E5=8A=A0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=BA=A7=E7=BC=93=E5=AD=98=E5=8E=9F=E5=AD=90=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 修复两个阻断 bug 1. pip PEP 668 externally-managed-environment(macOS Homebrew python) - install_python_deps 探测 EXTERNALLY-MANAGED 标记,命中加 --break-system-packages - 兜底:pip 报 externally-managed 退出码 1 时自动重试加 flag - --user 限目标目录,不碰系统 site-packages,安全 2. bash: OPENCLAW_HOME: unbound variable(install_gateway_and_env) - 根因:configure_gateway 里 unset OPENCLAW_HOME 误伤脚本顶部定义的内部变量 - 内部变量本就只赋值未 export,不会传给引擎子进程,unset 纯误伤 - 修法:删掉 unset,保留 warning 提示用户清理 shell 环境 ## 四个安装脚本加版本级缓存 + 原子写 | 脚本 | 路线 | 缓存 | 原子写 | |------|------|------|--------| | install.sh | GitHub tarball | ~/.xiaobei/cache/ | .part → mv -f | | install-atomgit.sh | atomgit tarball | 同上 | 同上 | | install.ps1 | GitHub tarball (Win) | 同上 | .part → Move-Item -Force | | install-atomgit.ps1 | atomgit tarball (Win) | 同上 | 同上 | - 同 tag 重跑命中缓存跳过 120MB 下载 - 原子写:先下 .part 临时文件,成功后 rename 到 cached - 断网不会留半截损坏文件被下次误判为完整缓存 Co-Authored-By: AtomCode (GLM-5.2) --- scripts/install-atomgit.ps1 | 28 +++++++++++++++-- scripts/install-atomgit.sh | 61 +++++++++++++++++++++++++++++++------ scripts/install.ps1 | 28 +++++++++++++++-- scripts/install.sh | 61 +++++++++++++++++++++++++++++++------ 4 files changed, 152 insertions(+), 26 deletions(-) diff --git a/scripts/install-atomgit.ps1 b/scripts/install-atomgit.ps1 index ee3d2ecd..1e19e71a 100755 --- a/scripts/install-atomgit.ps1 +++ b/scripts/install-atomgit.ps1 @@ -109,17 +109,39 @@ function Resolve-Tag { } # --- 2. Download tarball (atomgit CDN) --- +# Version-level cache: same tag + plat re-uses ~/.xiaobei/cache/, +# skipping the 120MB re-download on rerun (incl. failed retry). +# Atomic write: download to .part first, then Move-Item -Force to , +# so a broken network never leaves a half-written file that the next run would +# mistake for a complete cache hit. function Download-Tarball([string]$tag) { $asset = "xiaobei-$tag-win-x64.tar.gz" if ($env:XIAOBEI_TARBALL -and (Test-Path $env:XIAOBEI_TARBALL)) { Write-Ok "using local tarball: $env:XIAOBEI_TARBALL" return $env:XIAOBEI_TARBALL } - $tmp = New-TemporaryFile + + $cacheDir = if ($env:XIAOBEI_CACHE_DIR) { $env:XIAOBEI_CACHE_DIR } else { Join-Path $env:USERPROFILE ".xiaobei\cache" } + $cached = Join-Path $cacheDir $asset + + if (Test-Path $cached -PathType Leaf) { + Write-Ok "using cached tarball: $cached" + return $cached + } + $url = "$AtomgitMirror/releases/download/$tag/$asset" Write-Host " downloading $url" - Invoke-WebRequest -Uri $url -OutFile $tmp -Headers @{ "User-Agent" = "xiaobei-install" } - return $tmp.FullName + New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null + $part = "$cached.part" + if (Test-Path $part) { Remove-Item $part -Force } + try { + Invoke-WebRequest -Uri $url -OutFile $part -Headers @{ "User-Agent" = "xiaobei-install" } + Move-Item -Path $part -Destination $cached -Force + } catch { + if (Test-Path $part) { Remove-Item $part -Force } + throw "download failed: $url`n$($_.Exception.Message)" + } + return $cached } # --- 3. Extract --- diff --git a/scripts/install-atomgit.sh b/scripts/install-atomgit.sh index 158942a0..c6487f8e 100755 --- a/scripts/install-atomgit.sh +++ b/scripts/install-atomgit.sh @@ -389,9 +389,19 @@ download_and_extract_tarball() { ui_kv "Asset" "$asset" ui_kv "URL" "$url" mkdir -p "$cache_dir" + # 原子写:先下到 .part 临时文件,成功后 rename 到 cached, + # 避免断网留半截损坏文件导致下次重跑命中损坏缓存。 + local part="$cached.part" + rm -f "$part" ui_info "Downloading $asset (~120MB, atomgit CDN) → $cached ..." - download_file "$url" "$cached" - ui_success "Downloaded" + if download_file "$url" "$part"; then + mv -f "$part" "$cached" + ui_success "Downloaded" + else + rm -f "$part" + ui_error "下载失败:$url" + exit 1 + fi tmp="$cached" fi @@ -466,12 +476,41 @@ install_python_deps() { ui_info "Installing python skill deps via $PY_BIN (--user)" # --no-warn-script-location 抑制 'script installed in .../bin which is not on PATH'(小白噪音) # --disable-pip-version-check 抑制 pip 升级提示 - printf '%s' "$merged" | sort -u | grep -vE '^\s*#|^\s*$' | \ - "$PY_BIN" -m pip install --user \ - --no-warn-script-location --disable-pip-version-check \ - -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \ - -r /dev/stdin || \ + # 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 ui_warn "pip install 部分失败,可稍后手动补" + fi ui_success "Python deps done" } @@ -1357,14 +1396,16 @@ install_gateway_and_env() { local systemd_env="$OPENCLAW_HOME/daemon.env" local macos_env="$OPENCLAW_HOME/service-env/ai.openclaw.gateway.env" - # ─── 检测/清掉错误的 OPENCLAW_HOME 用户环境变量 ───────────── + # ─── 提示用户清掉错误的 OPENCLAW_HOME 环境变量 ───────────── # 引擎 resolveStateDir 把 OPENCLAW_HOME 当 homedir 再 append /.openclaw(见 - # openclaw/src/config/paths.ts),故若 OPENCLAW_HOME 已被设成 ~/.openclaw,会产出嵌套路径。 + # openclaw/src/config/paths.ts),故若用户 shell export 了 OPENCLAW_HOME=~/.openclaw, + # 会产出嵌套路径。本脚本顶部已把 OPENCLAW_HOME 设成内部变量且未 export,不会传给引擎子进程, + # 所以这里只打 warning 提示用户清理 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" - unset OPENCLAW_HOME fi # redirect stdin from /dev/tty so interactive read 工作于 curl|bash diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 7519cd11..f6b78e0f 100755 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -111,17 +111,39 @@ function Resolve-Tag { } # --- 2. Download tarball (GitHub release) --- +# Version-level cache: same tag + plat re-uses ~/.xiaobei/cache/, +# skipping the 120MB re-download on rerun (incl. failed retry). +# Atomic write: download to .part first, then Move-Item -Force to , +# so a broken network never leaves a half-written file that the next run would +# mistake for a complete cache hit. function Download-Tarball([string]$tag) { $asset = "xiaobei-$tag-win-x64.tar.gz" if ($env:XIAOBEI_TARBALL -and (Test-Path $env:XIAOBEI_TARBALL)) { Write-Ok "using local tarball: $env:XIAOBEI_TARBALL" return $env:XIAOBEI_TARBALL } - $tmp = New-TemporaryFile + + $cacheDir = if ($env:XIAOBEI_CACHE_DIR) { $env:XIAOBEI_CACHE_DIR } else { Join-Path $env:USERPROFILE ".xiaobei\cache" } + $cached = Join-Path $cacheDir $asset + + if (Test-Path $cached -PathType Leaf) { + Write-Ok "using cached tarball: $cached" + return $cached + } + $url = "https://github.com/$Repo/releases/download/$tag/$asset" Write-Host " downloading $url" - Invoke-WebRequest -Uri $url -OutFile $tmp -Headers @{ "User-Agent" = "xiaobei-install" } - return $tmp.FullName + New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null + $part = "$cached.part" + if (Test-Path $part) { Remove-Item $part -Force } + try { + Invoke-WebRequest -Uri $url -OutFile $part -Headers @{ "User-Agent" = "xiaobei-install" } + Move-Item -Path $part -Destination $cached -Force + } catch { + if (Test-Path $part) { Remove-Item $part -Force } + throw "download failed: $url`n$($_.Exception.Message)" + } + return $cached } # --- 3. Extract --- diff --git a/scripts/install.sh b/scripts/install.sh index b8de9230..04eb20d3 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -388,9 +388,19 @@ download_and_extract_tarball() { ui_kv "Asset" "$asset" ui_kv "URL" "$url" mkdir -p "$cache_dir" + # 原子写:先下到 .part 临时文件,成功后 rename 到 cached, + # 避免断网留半截损坏文件导致下次重跑命中损坏缓存。 + local part="$cached.part" + rm -f "$part" ui_info "Downloading $asset (~120MB) → $cached ..." - download_file "$url" "$cached" - ui_success "Downloaded" + if download_file "$url" "$part"; then + mv -f "$part" "$cached" + ui_success "Downloaded" + else + rm -f "$part" + ui_error "下载失败:$url" + exit 1 + fi tmp="$cached" fi @@ -465,12 +475,41 @@ install_python_deps() { ui_info "Installing python skill deps via $PY_BIN (--user)" # --no-warn-script-location 抑制 'script installed in .../bin which is not on PATH'(小白噪音) # --disable-pip-version-check 抑制 pip 升级提示 - printf '%s' "$merged" | sort -u | grep -vE '^\s*#|^\s*$' | \ - "$PY_BIN" -m pip install --user \ - --no-warn-script-location --disable-pip-version-check \ - -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com \ - -r /dev/stdin || \ + # 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 ui_warn "pip install 部分失败,可稍后手动补" + fi ui_success "Python deps done" } @@ -1681,14 +1720,16 @@ install_gateway_and_env() { local systemd_env="$OPENCLAW_HOME/daemon.env" local macos_env="$OPENCLAW_HOME/service-env/ai.openclaw.gateway.env" - # ─── 检测/清掉错误的 OPENCLAW_HOME 用户环境变量 ───────────── + # ─── 提示用户清掉错误的 OPENCLAW_HOME 环境变量 ───────────── # 引擎 resolveStateDir 把 OPENCLAW_HOME 当 homedir 再 append /.openclaw(见 - # openclaw/src/config/paths.ts),故若 OPENCLAW_HOME 已被设成 ~/.openclaw,会产出嵌套路径。 + # openclaw/src/config/paths.ts),故若用户 shell export 了 OPENCLAW_HOME=~/.openclaw, + # 会产出嵌套路径。本脚本顶部已把 OPENCLAW_HOME 设成内部变量且未 export,不会传给引擎子进程, + # 所以这里只打 warning 提示用户清理 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" - unset OPENCLAW_HOME fi # redirect stdin from /dev/tty so interactive read 工作于 curl|bash From 58f4bcea635c902cc7147ff4cef18d6c775594e9 Mon Sep 17 00:00:00 2001 From: codes-factory-of-bg Date: Thu, 6 Aug 2026 17:02:57 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore(crew):=20=E7=B2=BE=E7=AE=80=20HEARTBE?= =?UTF-8?q?AT=20=E5=A4=B1=E6=95=88=E5=B9=B3=E5=8F=B0=E6=B1=87=E6=8A=A5?= =?UTF-8?q?=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 login-manager 技能引用,改为通用的"重新登录" - 精简失效平台汇报段落,去掉冗余说明 - wechat-channel 文案微调 Co-Authored-By: AtomCode (GLM-5.2) --- crews/main/HEARTBEAT.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crews/main/HEARTBEAT.md b/crews/main/HEARTBEAT.md index 54b0f3b5..78fb9d3d 100644 --- a/crews/main/HEARTBEAT.md +++ b/crews/main/HEARTBEAT.md @@ -26,7 +26,7 @@ 任何平台的取数端登录失效(`SESSION_EXPIRED` / 探活失败 / 浏览器跳登录页 / `get-xhs-user-id.sh` exit 2 等)时,**必须**: - 立即**跳过该平台**本轮取数,不再尝试任何取数动作; - - 把平台名记入 `EXPIRED_PLATFORMS`,在 Step 5 统一汇报,由用户**白天**用 login-manager 重新登录; + - 把平台名记入 `EXPIRED_PLATFORMS`,在 Step 5 统一汇报,由用户**白天**重新登录; - **不得**在凌晨心跳里扫码登录、不得唤醒用户。 **严禁的"硬行恢复"动作**(任一都可能触发平台风控/限流/封号): @@ -106,7 +106,7 @@ ##### 通用规则 - **必须传 `--id `**(脚本类平台):`` 取自 Step 1 查询结果里的 `id` 字段。同一 `source_folder` 可能对应多条记录(同内容重复发布到不同帖子),按 `--id` 逐条抓取/写库才能让每次发布各自独立统计;若只传 `--source-folder`,脚本会只抓一行指标却批量写进所有同 folder 行,造成重复发布之间互相污染。 -- **SESSION_EXPIRED**:脚本返回 `ok=false, error=SESSION_EXPIRED`(exit 2)时,**跳过该平台**本轮取数,记入 `EXPIRED_PLATFORMS`,Step 5 统一汇报,由用户白天用 login-manager 重新登录。**凌晨不唤醒用户、不扫码登录、不私拉会话**(见约束 4/5)。 +- **SESSION_EXPIRED**:脚本返回 `ok=false, error=SESSION_EXPIRED`(exit 2)时,**跳过该平台**本轮取数,记入 `EXPIRED_PLATFORMS`,Step 5 统一汇报,由用户白天重新登录。**凌晨不唤醒用户、不扫码登录、不私拉会话**(见约束 4/5)。 - **xhs 风控显著高于其他平台**:xhs 任何登录失效迹象 → 立刻整段跳过 xhs,不尝试任何恢复。取数只走 `xhs-browse`,**禁止**探测/使用 `xhs-publish` creator 域 cookie。 - **⛔ 取数失败时必须原样报告脚本 stderr + exit code,禁止自行归因**:脚本的 stderr 是排查的唯一可靠依据。Agent 不得根据 DB 字段(如 `publish_url` 是否为空)脑补错误原因、不得改写/概括 stderr 成自己的话。例:`wx-mp-engagement fetch` exit 1 stderr=`error: 发表记录页未找到标题匹配的 row id=3`,就报这个原文,不要脑补成 "publish_url 无效"。错误归因错误会误导排查方向。 @@ -180,9 +180,7 @@ > ⚠️ 以下**取数端**Cookie 已失效,数据未能更新。请白天使用 login-manager 技能重新登录: > - douyin(抖音) > - xhs-browse(小红书浏览端) - > - wechat-channel(微信视频号) - > - > 列出的名字即 `login-manager login ` 要用的平台名(非 published-track 的 `xhs`)。 + > - wechat-channel(微信视频号) > > **只报告取数端 cookie**。**不要报告、也不要探测 `xhs-publish`(小红书发布端 / creator.xiaohongshu.com)**: > 复盘/取数完全不依赖发布端 cookie,探测它只会给 creator 域增加风控概率且结论与取数无关。