diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..83831c5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# shell 脚本强制 LF —— core.autocrlf=true 下防止 checkout 成 CRLF, +# 否则在 Linux/Alpine/WSL 里会 `bad interpreter: /bin/sh^M` 跑不了。 +*.sh text eol=lf +*.bash text eol=lf + +# Windows 脚本用 CRLF +*.ps1 text eol=crlf +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/docs/bug_fix/README.md b/docs/bug_fix/README.md new file mode 100644 index 0000000..7fe89f2 --- /dev/null +++ b/docs/bug_fix/README.md @@ -0,0 +1,111 @@ +# 确定性修复记录(fork 专用) + +> 本目录是 **HyperGroups fork 自有记录**,只存在于 fork 仓库的镜像分支(`KongFangXun`,原名 `upstream`, +> 已改名以消除与 remote `upstream` 的歧义),**不向上游提 PR**。向上游贡献时从 `upstream/main` +> 另切干净的 `fix/xxx` 主题分支,只带代码改动,不带本目录。 +> +> 与 `issues/`(gitignored,本地审计草稿)的分工: +> - `issues/` —— 原始发现台账,本地保留,不进版本库。 +> - `docs/bug_fix/` —— 已确认的**确定性** bug + 复现 + 修复,提交到 fork,按拟提的主题分支分组。 + +「确定性」= 100% 可复现、与模型能力无关的脚本 bug(区别于 `docs/anti-cases/` 记录的模型行为问题)。 + +## 工作流 + +1. 在镜像分支(`KongFangXun`)按自己的方式修问题、记录到本目录、打 tag 标记。 +2. 真要回贡上游时,从最新 `upstream/main` 切 `fix/xxx` 主题分支,把对应那一组修复做成**原子提交**。 +3. 按作者规矩(CONTRIBUTING.md + PR 模板)走:`verify.sh` 全过 → 部署循环 → 非 OpenClaw 平台测试。 +4. 先推到自己 fork(`origin`)的主题分支,再从该分支开 PR 到上游 `KongFangXun:main`。 + +> **PR 创建方式(实测)**:本机终端到 GitHub 的 HTTPS 被墙、只有 SSH 通;`gh` 用 PAT +> 建 PR 一律被拒(`Resource not accessible by personal access token`,三种 token 都试过, +> 疑账号级限制/邮箱未验证)。**改用浏览器会话开 PR 成功**——`git push`(SSH)推分支, +> 再去上游 Pull requests 页点 "recently pushed branches" 黄条的 *Compare & pull request*。 + +## 提交状态 + +| 主题分支 | 覆盖的 bug | 状态 | +|---------|-----------|------| +| `fix/cross-platform-portability` | shasum 回退、stat GNU/BSD | ✅ **已提 [PR #1](https://github.com/KongFangXun/sofagent/pull/1)**(OPEN,待作者 review) | +| `fix/set-e-premature-exit` | set -e 提前退出(3 处) | ⏳ 待提(等 #1 反馈后) | +| `fix/arg-parsing-shift` | verify/uninstall 参数解析 | ⏳ 待提 | +| `fix/numeric-and-unbound-guards` | 除零 / grep -c 双 0 / set -u | ⏳ 待提 | + +> 策略:单人维护者,不一次性砸多个 PR。先用 #1(作者点名最缺的跨平台兼容)走通流程、摸清接受口味,再逐个发。 + +## 拟分组(= 拟提的主题分支) + +相对最新 `upstream/main`(含 v0.82)的净改动,按主题归为 4 个主题分支: + +### 1. `fix/set-e-premature-exit` — `set -e` 下裸命令提前退出(高) + +`task-orchestrate.sh` 在 `set -euo pipefail`(line 69)下,3 处裸 `ao run` 失败即退出, +后续失败处理/重试逻辑全成死代码。 + +| 位置 | 修复 | +|------|------| +| L3 模板分支 | `EXIT_CODE=0; ao run ... \|\| EXIT_CODE=$?` | +| L4 直接执行 | 同上 | +| 重试循环(上游 v0.73 新增) | 把 `\|\| EXIT_CODE=$?` 并入上游循环,否则重试永不触发 | + +**复现**:`ao` 返回非 0 → 脚本立即终止,看不到"重试 N/M"和失败汇总。 + +### 2. `fix/arg-parsing-shift` — `for arg in "$@"` + `shift` 失效(中) + +`for arg` 循环里 `shift` 无效、取 `$2` 拿到的是脚本位置参数而非"下一个 arg", +导致 `--quiet --platform X` 把 PLATFORM 误设为字面量 `--platform`。改用 `while [[ $# -gt 0 ]]` + `shift`。 + +| 文件 | 备注 | +|------|------| +| `verify.sh` | 同时保留上游新增的 `--quick` | +| `uninstall.sh` | 同类修复 | + +**复现**:`bash verify.sh --quiet --platform claude` → 平台探测错误。 + +### 3. `fix/cross-platform-portability` — BSD/GNU 工具差异(中)✅ 已提 PR #1 + +作者在 CONTRIBUTING「最需要的技能」里点名的 bash BSD/macOS 兼容性。 +> 已作为 [PR #1](https://github.com/KongFangXun/sofagent/pull/1) 提交(+4/-2,2 文件,OPEN)。 +> 验证:`bash -n` 通过 + 功能验证(sha256sum 回退出哈希、`stat -c %Y` 取到真实 mtime); +> 部署循环/非 OpenClaw 实测因无安装环境未跑,已在 PR 正文如实标注。 + +| 位置 | 修复 | +|------|------| +| `task-orchestrate.sh` TASK_SLUG | `shasum` 缺失时回退 `sha256sum`(Alpine/精简 Linux 无 shasum) | +| `verify.sh` think.md 时间 | `stat -c %Y`(GNU)优先 + `stat -f %m`(BSD)回退。原 BSD-only 写法在 GNU/Linux 上 `-f`=`--file-system`、`%m` 被当文件名,**取不到 mtime**,反思频率检查失真 | + +**复现**:精简 Linux 容器跑 `task-orchestrate.sh` → TASK_SLUG 恒为 unknown;Linux 跑 `verify.sh` → think.md 反思频率计算错误(实测旧写法在 GNU 上会输出文件系统信息、破坏算术)。 + +### 4. `fix/numeric-and-unbound-guards` — 数值/未绑定健壮性(低-中) + +| 文件 | 修复 | +|------|------| +| `task-record.sh` 预算 | `--limit 0`/非数字在 `$(( ))` 前拦截,防除零崩溃 | +| `task-record.sh` 闭环计数 | 修 `grep -c ... \|\| echo 0` 在 0 匹配时输出 `"0\n0"` | +| `task-orchestrate.sh` 清理 | guard 空 `$SOFAGENT_CONSTRAINT_FILE`,避免 set -u 下 `rm ""` | +| `install.sh` 数据目录 | `SOFAGENT_DATA="${SOFAGENT_DATA:-...}"` 保留外部环境变量覆盖 | + +**复现**:`task-record.sh --budget --steps 5 --limit 0` → 除零,set -e 崩脚本。 + +## 跨平台自检脚本(`tests/`) + +把验证固化成可移植脚本,各环境**独立执行**、方便复现与排查: + +- **`tests/check-portability.sh`** —— 纯 POSIX sh,可在 Alpine(busybox) / Ubuntu / macOS / MSYS2 直接 `sh` 跑, + 验证 PR #1 两处修复在当前平台成立(stat 取 mtime、shasum 缺失回退),退出码 0/1。 +- **`tests/run-envs.sh`** —— 本机驱动,把上面的自检丢进 **本机 MSYS2 + WSL Ubuntu + Docker Alpine** 各自独立跑、汇总; + 缺哪个环境就跳过哪个,互不影响。 + +已实测(2026-06):本机 MSYS2(GNU 8.32) 与 WSL Ubuntu 24.04(GNU 9.4) 均 **4/0 通过**,slug 跨平台一致; +旧 `stat -f %m` 在两处 GNU 平台均复现 bug。Docker Alpine(真·无 shasum)待引擎启动后补跑。 + +## 回归测试 + +`dev` 分支已有针对前 5 个确定性 bug 的回归用例(A–E,commit `44a0778`)。 +提主题分支时一并带上对应用例,满足作者 PR 模板的「verify.sh 全过」要求。 + +## 不在本目录的改动 + +- `install.ps1` + `install.sh` 环境检测 —— **功能新增**,不是确定性 bug 修复,不归此处。 + 按 fork 原则(见 `issues/FORK.md` §1.3)走独立 feature 分支评估。 +- `load-chain.sh` 哈希缓存修复 —— 上游 v0.64 已删该文件(hook 替代,无缓存层),修复已无对象。 diff --git a/docs/bug_fix/tests/ENVIRONMENTS.md b/docs/bug_fix/tests/ENVIRONMENTS.md new file mode 100644 index 0000000..63cc467 --- /dev/null +++ b/docs/bug_fix/tests/ENVIRONMENTS.md @@ -0,0 +1,103 @@ +# 测试环境与编码记录 + +> 本机实测过的环境、版本,以及踩过的编码/换行坑。供跨平台脚本(install.ps1 / uninstall.ps1 / +> verify.sh / 自检脚本)开发与排查参考。记录时间:2026-06(fork 维护)。 + +## 一、已测试环境矩阵 + +宿主:**Windows 11**(build 10.0-26200,中文版)。 + +| 环境 | 版本 | 解释器 | 关键工具 | 已实测 | +|------|------|--------|---------|--------| +| MSYS2 / Git Bash | MINGW64 | bash 5.3.9 | GNU coreutils **8.32**(stat 8.32 / sha256sum / shasum=perl) | ✅ 自检 4/0 | +| WSL Ubuntu | 24.04.1 LTS | bash | GNU coreutils **9.4**(shasum + sha256sum 均有) | ✅ 自检 4/0 | +| Windows PowerShell | **5.1**(`powershell.exe`) | — | — | ✅ install/uninstall 部署循环 PASS | +| Docker(rancher-desktop) | 29.1.4-rd | — | Alpine(busybox,真·无 shasum) | ⏳ 引擎未启动,待补 | + +辅助工具版本:git 2.54.0.windows.1、curl 8.19.0(**Schannel** TLS 后端)、python 3.12.10、 +node v24.15.0 / npm 11.12.1、gh 2.95.0。jq **未装**(脚本里用 python 替代解析 JSON)。 + +### 实测结论 +- `check-portability.sh`(PR#1 两修复):MSYS2(GNU 8.32) + WSL Ubuntu 24.04(GNU 9.4) 均 **4/0**,slug 跨平台一致。 +- `install.ps1` / `uninstall.ps1`:沙箱(临时 USERPROFILE)部署循环 install→uninstall→reinstall **PASS**,卸载正确保留 `.sofagent/` 数据。 +- 缺口:Alpine(真·无 shasum)需启动 docker 引擎后用 `run-envs.sh` 补跑。 + +## 二、编码与换行坑(重要,已踩过) + +### 1. PowerShell `.ps1` 必须 UTF-8 **带 BOM** +- **现象**:Windows PowerShell 5.1 读**无 BOM** 的 UTF-8 脚本时,按系统 ANSI 代码页(中文 Windows = **GBK**)解析 → 中文乱码、字符串提前截断、`解析报错`(Unexpected token / Missing closing quote)。 +- **规则**:含中文的 `.ps1` 一律存 **UTF-8 with BOM**(前 3 字节 `EF BB BF`)。 +- **正确加 BOM 写法**: + ```powershell + [System.IO.File]::WriteAllText($p, $c, (New-Object System.Text.UTF8Encoding $true)) + ``` + +### 2. 转 BOM 时,读取**必须显式 `-Encoding UTF8`** +- **本次踩坑**:用 `Get-Content -Raw`(不带 `-Encoding`)读一个**无 BOM** 的 UTF-8 文件,5.1 默认按 GBK 误读 → 中文变 `鍗歌浇`/`涓枃` 这类 mojibake,再写回直接把文件读坏。 +- **正确**:`Get-Content -Raw -Encoding UTF8 $p`。 +- **例外**:原文件是 **UTF-16 BOM** 时,`Get-Content -Raw`(不带 `-Encoding`)能靠 BOM 自动识别、正确读入(install.ps1 第一次从 UTF-16 转 UTF-8 即此情形,所以那次没坏)。 + +### 3. `.gitattributes` 锁换行(已落地) +``` +*.sh *.bash text eol=lf # Linux/Alpine/WSL 跑 shell 必须 LF +*.ps1 *.bat *.cmd text eol=crlf # Windows 脚本 +``` +- **背景**:仓库 `core.autocrlf=true` 且原本无 `.gitattributes` → checkout 会把 `*.sh` 变 CRLF → 在 Linux/Alpine/WSL 里 `bad interpreter: /bin/sh^M` 直接跑不了。 + +### 4. `wsl.exe` 输出是 UTF-16LE +- **现象**:`wsl.exe ... cmd` 的 stdout 是 UTF-16LE,Git Bash 里直接管道/`grep` 会乱码、或被当成 “Binary file matches”。 +- **解法**:让重定向**在 WSL 内部**完成,输出文件即 UTF-8: + ```bash + wsl.exe -d Ubuntu sh -c "sh /mnt/c/.../check.sh > /mnt/c/.../out.txt 2>&1" + cat /c/.../out.txt # UTF-8,干净 + ``` + +### 5. MSYS 路径转换(调 `wsl.exe` / native exe 时) +- **现象**:Git Bash 调 `wsl.exe` 时会把 `/tmp/x` 这类 `/unix/路径` 误转成 Windows 路径,导致 WSL 里 `No such file`。 +- **解法**:`export MSYS2_ARG_CONV_EXCL='*'`(或 `MSYS_NO_PATHCONV=1`)禁用转换。 + +## 三、Windows 安装器实测发现的 bug(`feat/windows-installer`) + +| # | bug | 影响 | 修复 | +|---|-----|------|------| +| 1 | install.ps1/install.sh 用 **`WSLENV`** 判 WSL | `WSLENV`(如 `WT_SESSION:`)在装了 WSL 的 Windows 主机上**本就有** → 脚本在 Windows 上误判为 WSL **直接拒跑** | 只认 `WSL_DISTRO_NAME` | +| 2 | install.ps1 rules.md 用旧路径 `constitution\rules.md` | v0.73 已扁平化到 `sofagent\rules.md` → **宪法部署失败** | 新路径优先 + 旧路径 fallback | + +两者均由**沙箱实测**逐轮跑出来,修复后部署循环全过。 + +## 四、Shell→PowerShell 全面移植(`feat/windows-installer`) + +把运行时 shell 脚本全量移植为原生 Windows PowerShell(纯 PowerShell + 非 WSL 可跑)。 + +### 已移植(**16 个 .ps1,100% 覆盖**,均实测对照 .sh) +`install` `uninstall` `task-record`(反思闭环)`audit` `lib/config` `task-orchestrate`(ao 包装) +`verify` `cleanup` `compress-memory` `verify-evidence` `benchmark`(A/B 题库 + audit-log 客观判定) +`daemon` `daemon-install` `daemon-status` `daemon-uninstall` `lib/daemon-lib`。 +**全部 .sh(14)+ 2 lib 均有对应 .ps1,无遗漏。** + +> daemon 系列特别说明:bash 版**明确拒绝在非 Unix 运行**;PS 版**反过来支持 Windows**—— +> Get-Process 替 pgrep、Start-Process 替 nohup、Register-ScheduledTask 替 launchd/systemd、 +> 原生 ConvertFrom/To-Json 替 grep/sed。实测主循环检测到真实 workbuddy/claude 进程。 +> 计划任务注册需管理员权限(否则 try/catch 降级 + 直接启动)。 + +### Skill dispatch +SKILL.md(第 1 层永远注入)加「跨平台脚本调用约定」:`bash X.sh --flag` 在纯 Windows PowerShell +改 `powershell -File X.ps1 -Flag`(kebab→Pascal)。install.ps1 部署 .ps1 到 `$TARGET\scripts\`。 +E2E 实测:install→部署7脚本→用部署后的 task-record.ps1 跑闭环→uninstall,纯 PowerShell 全过。 + +### PowerShell 移植踩坑全集(写 .ps1 必看) +1. **脚本编码**:含中文 .ps1 必须 **UTF-8 BOM**(PS 5.1 读无 BOM 按 GBK 解析、乱码/解析错)。 +2. **加 BOM 时读取**须 `Get-Content -Raw -Encoding UTF8`(不指定→GBK 误读、直接读坏文件)。 +3. **控制台输出**:脚本顶部加 `[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false`, + 否则输出按 OEM/GBK 编码,被 UTF-8 消费方(Agent/Git Bash)读到乱码。机器可读前缀(ASCII)不受影响。 +4. **`if` 表达式不能直接作函数参数**:`f (if(){}else{})` 运行时崩 → 先 `$x = if...` 再传。 +5. **`switch` 无 `break` 执行所有匹配 case**:兜底 `^-` 会误伤每个 flag → 用 if/elseif 链。 +6. **函数前向引用**:PS 顺序解释,函数定义须在调用之前(Write-Summary 被前置段调用就得提前定义)。 +7. **单元素嵌套数组 `@(@(...))` 被摊平**成一维 → `foreach` 遍历到字符、`$x[0]` 取首字符。多元素不摊平。 +8. **日志/数据文件写 UTF-8 无 BOM**:用 `[IO.File]::WriteAllText/AppendAllText($p,$s,(New-Object Text.UTF8Encoding $false))`,对齐 .sh,且 BOM 会污染追加。 +9. **`.gitattributes`** 锁 `*.ps1=CRLF` / `*.sh=LF`。 + +### 移植中顺带发现的 .sh bug(候选独立 PR) +- `task-record.sh` 的 `sanitize()` 用 BSD 专属词边界 `[[:<:]]`,**GNU sed 4.9 报 `Invalid character class`** → + AWS密钥/凭证/手机/IP 4 条脱敏在 Linux 上失效。ps1 用 `\b` 修对。 +- `stat -f %m`(BSD)在 GNU 上是 `--file-system`、取不到 mtime(已在 PR #1 修)。 diff --git a/docs/bug_fix/tests/check-portability.sh b/docs/bug_fix/tests/check-portability.sh new file mode 100644 index 0000000..bb158f0 --- /dev/null +++ b/docs/bug_fix/tests/check-portability.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ============================================================ +# sofagent 跨平台修复自检(对应上游 PR #1) +# ============================================================ +# 目标:在「任意」环境独立执行,验证两处确定性修复在当前平台成立。 +# 修复1 task-orchestrate.sh TASK_SLUG —— shasum 缺失时回退 sha256sum +# 修复2 verify.sh think.md mtime —— GNU `stat -c %Y` / BSD `stat -f %m` 回退 +# +# 纯 POSIX sh,无 bashism,可在 Alpine(busybox) / Ubuntu / macOS / MSYS2 直接跑: +# sh check-portability.sh +# 退出码:0=全过,1=有失败(方便 CI / 排查) +# ============================================================ +set -u +pass=0; fail=0 +ok() { echo " [PASS] $1"; pass=$((pass + 1)); } +ng() { echo " [FAIL] $1"; fail=$((fail + 1)); } +is_hex8() { case "$1" in [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]) return 0 ;; *) return 1 ;; esac; } + +echo "== 环境 ==" +echo " uname : $(uname -s) $(uname -m)" +echo " stat : $(stat --version 2>/dev/null | head -1 || echo '非GNU(BSD/busybox)')" +echo " shasum : $(command -v shasum 2>/dev/null || echo 无)" +echo " sha256sum: $(command -v sha256sum 2>/dev/null || echo 无)" +echo + +echo "== 修复1:TASK_SLUG(缺 shasum 回退 sha256sum)==" +slug=$(printf '%s' "sofagent-task" | { shasum -a 256 2>/dev/null || sha256sum 2>/dev/null; } | cut -c1-8) +if is_hex8 "$slug"; then ok "正常环境 slug=$slug(8 位十六进制)"; else ng "正常环境 slug 非法:'$slug'"; fi +# 模拟 shasum 缺失:临时目录放一个必失败的 shasum stub,前插 PATH +d=$(mktemp -d 2>/dev/null || { d=/tmp/nob.$$; mkdir -p "$d"; echo "$d"; }) +printf '#!/bin/sh\nexit 127\n' > "$d/shasum"; chmod +x "$d/shasum" +slug2=$(PATH="$d:$PATH" sh -c 'printf "%s" sofagent-task | { shasum -a 256 2>/dev/null || sha256sum 2>/dev/null; } | cut -c1-8') +rm -rf "$d" +if is_hex8 "$slug2"; then ok "缺 shasum 回退 slug=$slug2"; else ng "缺 shasum 回退失败:'$slug2'(这正是上游 bug)"; fi +if [ "$slug" = "$slug2" ]; then ok "回退透明(两次哈希一致)"; else ng "回退哈希不一致:$slug vs $slug2"; fi +echo + +echo "== 修复2:stat 取 mtime(GNU -c%Y / BSD -f%m 回退)==" +f=$(mktemp 2>/dev/null || { f=/tmp/t.$$; echo "$f"; }); : > "$f" +now=$(date +%s) +mt=$(stat -c %Y "$f" 2>/dev/null || stat -f %m "$f" 2>/dev/null || echo "") +# 对照:上游旧写法只有 BSD 语法(只取首行,避免 GNU 上 -f 吐出整块文件系统信息) +old=$(stat -f %m "$f" 2>/dev/null | head -1) +rm -f "$f" +case "$mt" in + '' | *[!0-9]*) ng "新写法未取到数字 mtime:'$mt'" ;; + *) age=$((now - mt)); if [ "$age" -ge 0 ] && [ "$age" -le 5 ]; then ok "新写法 mtime=$mt 年龄=${age}s(正确)"; else ng "mtime 异常 age=${age}s"; fi ;; +esac +case "$old" in + '') echo " (参考)旧写法 stat -f %m:失败/无输出(本平台不支持该 BSD 语法)" ;; + *[!0-9]*) echo " (参考)旧写法 stat -f %m:取不到 mtime(GNU 上 -f=--file-system)← bug 本体" ;; + *) echo " (参考)旧写法 stat -f %m = $old(本平台为 BSD/macOS 风格,恰好可用)" ;; +esac +echo + +echo "== 结果:$pass 通过 / $fail 失败 ==" +[ "$fail" -eq 0 ] diff --git a/docs/bug_fix/tests/run-envs.sh b/docs/bug_fix/tests/run-envs.sh new file mode 100644 index 0000000..a7d7851 --- /dev/null +++ b/docs/bug_fix/tests/run-envs.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# ============================================================ +# 在本机把 check-portability.sh 丢进多个真实环境独立执行并汇总 +# ============================================================ +# 用法(Git Bash / Windows): bash docs/bug_fix/tests/run-envs.sh +# 设计:各环境互不依赖,单独成段,方便排查;某环境缺失则跳过、不影响其他。 +# ============================================================ +set -u +HERE="$(cd "$(dirname "$0")" && pwd)" +CHECK="$HERE/check-portability.sh" +sep() { echo; echo "######################## $1 ########################"; } + +# 1) 本机 MSYS2 / Git Bash(Windows + GNU coreutils) +sep "本机 MSYS2 (Windows/GNU)" +sh "$CHECK"; echo "[exit $?]" + +# 2) WSL Ubuntu(真 Linux/GNU)—— 输出走文件,避开 wsl.exe 的 UTF-16 管道乱码 +if command -v wsl.exe >/dev/null 2>&1; then + sep "WSL Ubuntu (真 Linux/GNU)" + WIN_TMP="${HOME}/.cpcheck.sh"; WIN_OUT="${HOME}/.cpout.txt" + cp "$CHECK" "$WIN_TMP" + # 重定向在 WSL 内部完成 → 输出文件是 UTF-8(直接 cat wsl.exe 的 stdout 会是 UTF-16 乱码) + MSYS2_ARG_CONV_EXCL='*' wsl.exe -d Ubuntu sh -c "sh '/mnt${WIN_TMP}' > '/mnt${WIN_OUT}' 2>&1" + cat "$WIN_OUT" + rm -f "$WIN_TMP" "$WIN_OUT" +else + sep "WSL Ubuntu"; echo "(无 wsl,跳过)" +fi + +# 3) Docker Alpine(真·无 shasum 的 busybox 环境)—— 仅当 docker 引擎在跑 +if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then + sep "Docker Alpine (真·无 shasum / busybox)" + WINPATH="$(cygpath -w "$HERE" 2>/dev/null || echo "$HERE")" + docker run --rm -v "${WINPATH}:/t" alpine sh /t/check-portability.sh; echo "[exit $?]" +else + sep "Docker Alpine"; echo "(docker 引擎未启动,跳过;启动 rancher-desktop 后重跑即可补这一档)" +fi + +echo; echo "######################## 汇总完毕 ########################" diff --git a/sofagent/scripts/benchmark.sh b/sofagent/scripts/benchmark.sh index 2b2ac60..38fe3d1 100755 --- a/sofagent/scripts/benchmark.sh +++ b/sofagent/scripts/benchmark.sh @@ -62,7 +62,7 @@ while [[ $# -gt 0 ]]; do SUMMARY_ONLY=true; shift ;; --api) API_MODE=true; shift ;; - *) shift ;; + *) echo "未知参数: $1(--help 查看用法)"; exit 1 ;; esac done diff --git a/sofagent/scripts/cleanup.sh b/sofagent/scripts/cleanup.sh index 5140d50..b334af4 100755 --- a/sofagent/scripts/cleanup.sh +++ b/sofagent/scripts/cleanup.sh @@ -60,7 +60,14 @@ while [[ $# -gt 0 ]]; do fi shift 2 ;; - --before=*) BEFORE_DATE="${1#*=}"; FORCE=true; shift ;; + --before=*) + BEFORE_DATE="${1#*=}" + if ! echo "$BEFORE_DATE" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then + echo "[cleanup] 错误:--before 需要日期格式 YYYY-MM-DD(收到:${BEFORE_DATE})" + exit 1 + fi + shift + ;; --version) echo "sofagent-cleanup v${VERSION}"; exit 0 ;; --help) SHOW_HELP=true; shift ;; *) echo "未知参数: $1(--help 查看用法)"; exit 1 ;; diff --git a/sofagent/scripts/compress-memory.sh b/sofagent/scripts/compress-memory.sh index 95e377b..ae6edd7 100755 --- a/sofagent/scripts/compress-memory.sh +++ b/sofagent/scripts/compress-memory.sh @@ -89,7 +89,7 @@ if [ "$DRY_RUN" = true ]; then info "=== 预览:条目统计 ===" # 统计反思条目数(以 ## 开头的日期行) - ENTRY_COUNT=$(grep -c '^## 20' "$THINK_FILE" 2>/dev/null || echo "0") + ENTRY_COUNT=$(grep -c '^## 20' "$THINK_FILE" 2>/dev/null || true); ENTRY_COUNT=${ENTRY_COUNT:-0} echo " 反思条目: ${ENTRY_COUNT}" # 统计标签分布 @@ -102,7 +102,7 @@ if [ "$DRY_RUN" = true ]; then # 检查 60 天前条目 SIXTY_DAYS_AGO=$(date -v-60d +%Y-%m-%d 2>/dev/null || date -d '60 days ago' +%Y-%m-%d 2>/dev/null || echo "") if [ -n "$SIXTY_DAYS_AGO" ]; then - OLD_COUNT=$(grep -c "^## 20" "$THINK_FILE" 2>/dev/null | while IFS= read -r line; do + OLD_COUNT=$(grep "^## 20" "$THINK_FILE" 2>/dev/null | while IFS= read -r line; do date_str=$(echo "$line" | grep -o '[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' | head -1) [ -n "$date_str" ] && [ "$date_str" '<' "$SIXTY_DAYS_AGO" ] && echo "1" done | wc -l | tr -d ' ') diff --git a/sofagent/scripts/install.sh b/sofagent/scripts/install.sh index 88d3e9e..0adb3d0 100755 --- a/sofagent/scripts/install.sh +++ b/sofagent/scripts/install.sh @@ -164,7 +164,7 @@ else fi # ── 统一初始化数据目录路径(所有平台共用,避免 set -u 下未定义)── -SOFAGENT_DATA="${PROJECT_DIR}/.sofagent" +SOFAGENT_DATA="${SOFAGENT_DATA:-${PROJECT_DIR}/.sofagent}" # v0.90 P0-3 修复:写入数据目录标记文件,供 audit/verify/orchestrate 等脚本定位 # 标记文件放在平台 skill 目录下,config.sh 读取它来还原 --project-dir 指定的路径 @@ -617,12 +617,8 @@ fi # end OpenClaw-only Step 6 if [ "$PLATFORM" = "openclaw" ] && [ "${NO_CONFIG_INJECT:-0}" != "1" ]; then info "Step 7/7 · 注入断路器配置..." -# 确定配置文件路径(优先 OPENCLAW_CONFIG_PATH,其次 $TARGET/config.json) -if [ -n "${OPENCLAW_CONFIG_PATH:-}" ]; then - CONFIG_FILE="$OPENCLAW_CONFIG_PATH" -else - CONFIG_FILE="${TARGET}/config.json" -fi +# loopDetection 写入 config.json(与 openclaw.json 分离;OPENCLAW_CONFIG_PATH 仅指 hook 配置) +CONFIG_FILE="${TARGET}/config.json" LOOPDETECT_BLOCK='{ "tools": { diff --git a/sofagent/scripts/task-orchestrate.sh b/sofagent/scripts/task-orchestrate.sh index a95c5df..776268d 100755 --- a/sofagent/scripts/task-orchestrate.sh +++ b/sofagent/scripts/task-orchestrate.sh @@ -207,7 +207,7 @@ analyze_track_record() { local total=0 success=0 # 搜索 task/logs 中匹配的记录 shopt -s nullglob 2>/dev/null || true - for logfile in "${SOFAGENT_DATA}"/task/logs/*/*/*.md; do + for logfile in "${SOFAGENT_DATA}"/task/logs/*/*.md; do [ -f "$logfile" ] || continue grep -q "$slug" "$logfile" 2>/dev/null || continue while IFS= read -r line; do @@ -224,7 +224,7 @@ analyze_track_record() { sliding_window_rollback() { local slug="$1" current_level="$2" success_count=0 total=0 shopt -s nullglob 2>/dev/null || true - for logfile in "${SOFAGENT_DATA}"/task/logs/*/*/*.md; do + for logfile in "${SOFAGENT_DATA}"/task/logs/*/*.md; do [ -f "$logfile" ] || continue # 用 awk 提取匹配任务的执行状态行,支持 bash/sh 无引号转义 awk -v task="$TASK_DESC" ' @@ -313,7 +313,8 @@ case $LEVEL in done < <(jq -r '.inputs // {} | to_entries[] | "\(.key)=\(.value)"' "${ORCHESTRATOR_DIR}/${TASK_SLUG}.json" 2>/dev/null) info "Step 1-3/4 · L3 — 跳过编排,直接执行模板" START_TIME=$(date +%s) - ao run "$AO_TEMPLATE" $AO_INPUTS 2>&1; EXIT_CODE=$? + # set -e 下裸命令失败会立即退出 → 用 || 捕获,保证失败处理/日志可达 + EXIT_CODE=0; ao run "$AO_TEMPLATE" $AO_INPUTS 2>&1 || EXIT_CODE=$? END_TIME=$(date +%s); ELAPSED=$(( END_TIME - START_TIME )) echo "" [ $EXIT_CODE -eq 0 ] && ok "任务完成(耗时 ${ELAPSED}s)" || warn "任务结束(exit $EXIT_CODE)" @@ -355,7 +356,7 @@ if [ "$SKIP_ORCHESTRATE" = true ]; then info "Step 1-3/4 · L4 — 跳过编排/Harness/worktree" info "Step 4/4 · 直接执行任务..." START_TIME=$(date +%s) - ao run "$TASK_DESC" 2>&1; EXIT_CODE=$? + EXIT_CODE=0; ao run "$TASK_DESC" 2>&1 || EXIT_CODE=$? END_TIME=$(date +%s); ELAPSED=$(( END_TIME - START_TIME )) echo "" if [ $EXIT_CODE -eq 0 ]; then @@ -489,14 +490,15 @@ AO_RUN_ARGS="" [ -n "$AO_MODEL" ] && AO_RUN_ARGS="--model ${AO_MODEL}" # ── 重试循环(v0.73: --max-retries 默认 3)── +# set -e 下裸 ao run 失败会立即退出(重试/失败日志都成死代码)→ 用 || 捕获退出码(fork 修复) RETRY_COUNT=0 EXIT_CODE=1 while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do if [ "$RETRY_COUNT" -gt 0 ]; then warn "重试 ${RETRY_COUNT}/${MAX_RETRIES}..." fi - ao run $AO_RUN_ARGS "$WORKFLOW_FILE" 2>&1 - EXIT_CODE=$? + EXIT_CODE=0 + ao run $AO_RUN_ARGS "$WORKFLOW_FILE" 2>&1 || EXIT_CODE=$? [ "$EXIT_CODE" -eq 0 ] && break RETRY_COUNT=$((RETRY_COUNT + 1)) done @@ -539,7 +541,8 @@ fi # ── 滑窗回滚:分析最近 5 次,写降级建议 ── # 清理 -rm -f "$WORKFLOW_FILE" "${SOFAGENT_CONSTRAINT_FILE:-}" +rm -f "$WORKFLOW_FILE" +[ -n "${SOFAGENT_CONSTRAINT_FILE:-}" ] && rm -f "$SOFAGENT_CONSTRAINT_FILE" echo "" echo " ════════════════════════════════════" diff --git a/sofagent/scripts/task-record.sh b/sofagent/scripts/task-record.sh index 299655d..92f6792 100755 --- a/sofagent/scripts/task-record.sh +++ b/sofagent/scripts/task-record.sh @@ -131,6 +131,11 @@ if [ "$IS_BUDGET" = true ]; then echo "BUDGET_CHECK: 参数不完整(需 --steps 和 --limit)" exit 0 fi + # 防除零/非数字:--limit 0 或非整数会让 $(( )) 报错并在 set -e 下崩脚本 + if ! [[ "$TASK_STEPS" =~ ^[0-9]+$ ]] || ! [[ "$BUDGET_LIMIT" =~ ^[1-9][0-9]*$ ]]; then + echo "BUDGET_CHECK: 参数无效(--steps 需非负整数,--limit 需正整数)" + exit 0 + fi PCT=$(( TASK_STEPS * 100 / BUDGET_LIMIT )) if [ "$PCT" -ge 60 ]; then echo "BUDGET_CHECK: ${TASK_STEPS}/${BUDGET_LIMIT}=${PCT}% → ⚠️ 已达预算 60%,建议调 Loop Agent (checkpoint)" @@ -144,10 +149,10 @@ fi if [ "$IS_CLOSURE_CHECK" = true ]; then TODAY=$(date +"%Y-%m-%d") MONTH=$(date +"%Y-%m") - LOG_DIR="${PWD}/.sofagent/task/logs/${MONTH}" + LOG_DIR="${SOFAGENT_DATA}/task/logs/${MONTH}" LOG_FILE="${LOG_DIR}/${TODAY}.md" if [ -f "$LOG_FILE" ]; then - COUNT=$(grep -c "^## " "$LOG_FILE" 2>/dev/null || echo "0") + COUNT=$(grep -c "^## " "$LOG_FILE" 2>/dev/null || true); COUNT=${COUNT:-0} echo "CLOSURE_CHECK: ${LOG_FILE} 存在 ${COUNT} 条记录 → ✅ 已闭合" else echo "CLOSURE_CHECK: ${LOG_FILE} 不存在 → ❌ 今日无闭环记录,需警惕" @@ -177,10 +182,10 @@ sanitize() { # 3. JWT token(eyJ 开头的 base64url 三段式) input=$(echo "$input" | sed -E 's/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/***JWT-REDACTED***/g') # 4. AWS Access Key(AKIA 开头,20 字符) - input=$(echo "$input" | sed -E 's/[[:<:]]AKIA[0-9A-Z]{16}[[:>:]]/***AWS-KEY-REDACTED***/g') + # 4–8 使用 \b 词边界(GNU/BSD sed 均支持;[[:<:]] 在 GNU sed 4.x 失效) + input=$(echo "$input" | sed -E 's/\bAKIA[0-9A-Z]{16}\b/***AWS-KEY-REDACTED***/g') # 5. 凭证赋值(password= / token= / secret= / api_key= / key=) - # 加 [[:<:]] 词边界防误伤(如 "monkey=foo" 不会被打码) - input=$(echo "$input" | sed -E 's/[[:<:]](password|token|secret|api_key|key)[=:][[:space:]]*[^ ]+/\1=***REDACTED***/g') + input=$(echo "$input" | sed -E 's/\b(password|token|secret|api_key|key)[=:][[:space:]]*[^ ]+/\1=***REDACTED***/g') # 6. 私钥块(PEM 格式:-----BEGIN ... PRIVATE KEY----- ... -----END) input=$(echo "$input" | sed -E '/-----BEGIN .*PRIVATE KEY-----/,/-----END .*PRIVATE KEY-----/{ s/-----BEGIN .*PRIVATE KEY-----/***PRIVATE-KEY-BLOCK-REDACTED***/ @@ -189,10 +194,10 @@ sanitize() { }') # 7. 中国大陆手机号(1[3-9] 开头 + 9 位数字,共 11 位) # 加 [[:<:]] 词边界,避免误伤订单号、时间戳等长数字串 - input=$(echo "$input" | sed -E 's/[[:<:]]1[3-9][0-9]{9}[[:>:]]/[PHONE-REDACTED]/g') + input=$(echo "$input" | sed -E 's/\b1[3-9][0-9]{9}\b/[PHONE-REDACTED]/g') # 8. 内网 IP(可选,SOFA_SANITIZE_IPS=true 时启用) if [ "${SOFA_SANITIZE_IPS:-}" = "true" ]; then - input=$(echo "$input" | sed -E 's/[[:<:]](10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)[0-9]+\.[0-9]+[[:>:]]/[INTERNAL_IP]/g') + input=$(echo "$input" | sed -E 's/\b(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)[0-9]+\.[0-9]+\b/[INTERNAL_IP]/g') fi echo "$input" } diff --git a/sofagent/scripts/uninstall.sh b/sofagent/scripts/uninstall.sh index 47ae740..40df4a0 100755 --- a/sofagent/scripts/uninstall.sh +++ b/sofagent/scripts/uninstall.sh @@ -30,12 +30,14 @@ err() { echo -e "${RED}[✗]${NC} $1"; } FORCE=false LIST_ONLY=false PLATFORM="" -for arg in "$@"; do - case "$arg" in - --force) FORCE=true ;; - --list) LIST_ONLY=true ;; - --platform) PLATFORM="$2"; shift ;; - --platform=*) PLATFORM="${arg#*=}" ;; +# 用 while+shift 解析:for arg in "$@" 里取 $2 是脚本位置参数(非"下一个arg")且 shift 无效—— +# 会导致 `--force --platform X` 把 PLATFORM 误设为 "--platform" +while [[ $# -gt 0 ]]; do + case "$1" in + --force) FORCE=true; shift ;; + --list) LIST_ONLY=true; shift ;; + --platform) PLATFORM="$2"; shift 2 ;; + --platform=*) PLATFORM="${1#*=}"; shift ;; --help) echo "sofagent uninstall [--platform openclaw|workbuddy|claude|codex|hermes]" echo " 正常模式 交互确认后删除约束文件" @@ -44,6 +46,7 @@ for arg in "$@"; do echo " --platform 指定目标平台(未指定时自动探测)" echo " 保留: .sofagent/ 数据目录(task-record / orchestrator)" exit 0 ;; + *) shift ;; esac done diff --git a/sofagent/scripts/verify.sh b/sofagent/scripts/verify.sh index 250cd90..e022b58 100755 --- a/sofagent/scripts/verify.sh +++ b/sofagent/scripts/verify.sh @@ -26,13 +26,15 @@ JSON_MODE=false QUIET_MODE=false QUICK_MODE=false PLATFORM="" -for arg in "$@"; do - case "$arg" in - --json) JSON_MODE=true ;; - --quiet) QUIET_MODE=true ;; - --quick) QUICK_MODE=true ;; +# 用 while+shift 解析:for arg in "$@" 里取 $2 是脚本位置参数(非"下一个arg")且 shift 无效—— +# 会导致 `--quiet --platform X` 把 PLATFORM 误设为 "--platform"(fork 修复) +while [[ $# -gt 0 ]]; do + case "$1" in + --json) JSON_MODE=true; shift ;; + --quiet) QUIET_MODE=true; shift ;; + --quick) QUICK_MODE=true; shift ;; --platform) PLATFORM="$2"; shift 2 ;; - --platform=*) PLATFORM="${arg#*=}" ;; + --platform=*) PLATFORM="${1#*=}"; shift ;; --help) echo "sofagent verify v${VERSION}" echo " 正常模式 彩色终端,显示所有检查项" @@ -43,6 +45,7 @@ for arg in "$@"; do echo "退出码: 0=全部通过 1=存在失败项" exit 0 ;; + *) shift ;; esac done @@ -143,8 +146,8 @@ if [ "$QUICK_MODE" = true ]; then check_fail "SKILL.md 缺失或宪法关键词不全" fi - # 2. .sofagent/ 数据目录存在 - if [ -d "${PWD}/.sofagent" ]; then + # 2. .sofagent/ 数据目录存在(v0.90 P0-3:用 config.sh 解析的 SOFAGENT_DATA,非 PWD) + if [ -d "$SOFAGENT_DATA" ]; then check_pass ".sofagent/ 数据目录存在" else check_warn ".sofagent/ 数据目录不存在(首次使用会自动创建)" @@ -230,8 +233,8 @@ if [ "$PLATFORM" = "workbuddy" ]; then check_warn "Skills 目录不存在" fi - # 数据目录检查 - if [ -d "${PWD}/.sofagent" ]; then + # 数据目录检查(v0.90 P0-3:用 SOFAGENT_DATA,非 PWD) + if [ -d "$SOFAGENT_DATA" ]; then check_pass ".sofagent/ 数据目录存在" else check_warn ".sofagent/ 数据目录不存在(首次使用会自动创建)" @@ -375,8 +378,8 @@ else check_warn " 发现 v0.72 前安装残留(${LEGACY_CONST})——建议运行 install.sh 升级,旧路径将自动迁移" fi fi - # think.md 检查 - THINK_FILE="${PWD}/.sofagent/think.md" + # think.md 检查(v0.90 P0-3:用 SOFAGENT_DATA,非 PWD) + THINK_FILE="${SOFAGENT_DATA}/think.md" if [ -f "$THINK_FILE" ]; then check_pass "think.md 存在($(wc -m < "$THINK_FILE" | tr -d ' ') 字符)" else @@ -545,10 +548,8 @@ fi _hr _section "断路器配置" +# loopDetection 在 config.json(与 openclaw.json 分离;OPENCLAW_CONFIG_PATH 仅指 hook 配置) CONFIG_FILE="${OPENCLAW_DIR}/config.json" -if [ -n "${OPENCLAW_CONFIG_PATH:-}" ]; then - CONFIG_FILE="$OPENCLAW_CONFIG_PATH" -fi if command -v jq &>/dev/null; then check_pass "jq 可用"