fix: MCP 信封也要暴露限额(formatMcpStructured 是白名单式挑字段) formatMcpStructured() 只挑固定字段返回,所以加在 IR 顶层的 content_truncated / content_budget_bytes / content_retained_bytes 在 MCP 响应里 被丢掉了——staging 实测 ?format=json 有标记、/mcp 没有。section 级标记因为 sections 整体透传所以没受影响。 - src/format_mcp.php: formatMcpStructured() 在截断时透出三个字段; formatMcpMarkdown() 的收尾说明在截断时改为告知「本节文本已封顶, 带 truncated 标记的条目附 content_bytes/content_lines,章节列表完整」, 不再承诺不再携带的 full documentation - test/integration/test_json_content_cap.php: 补 5 条 MCP 信封断言 验证:7 个页面的 MCP 信封与改动前逐字节一致(含 perlfunc 324KB subsection); 测试 384 → 389 通过。
feat: json/mcp 载荷限额,派生字段改为折行时单遍计算 info py 的 section 正文共 14.4MB(单 "Index" 一节就有 1.37MB),原本要整页 驻留内存才能序列化。现在折行阶段就按预算丢弃超出部分:全局 1MB (PHPMAN_JSON_MAX_CONTENT_BYTES) + 单节 512KB (PHPMAN_JSON_MAX_SECTION_BYTES), 两个常量都可在 phpman.config.php 覆盖。 因为超出预算的正文不再驻留,summary/synopsis/flags/examples/see_also 改为在 折行时从完整行流里累积,而不是事后从 sections 再扫一遍——否则被丢弃的 OPTIONS/EXAMPLES 正文会让 flags 丢失。被截断的条目带 truncated / content_bytes / content_lines,顶层带 content_truncated / content_budget_bytes / content_retained_bytes;section_outline 仍然完整,导航不受影响。 单节上限取 512KB 是实测结论:73 个常见 man 页里 perlfunc 有 324KB 的 subsection(bash 36KB、sudoers 59KB 的 section),32KB 会误伤这类总量远低于 预算的正常页面。放得下的页面必须逐字节不变。 - src/format_json.php: buildJsonData() 折行时按预算保留正文并累积派生字段 - src/config.php: 新增两个限额常量 - phpman.config.php.example: 记录这两个开关 - test/integration/test_json_content_cap.php: 17 条断言覆盖标记、预算上限、 数组/非 JSON 边界,以及最关键的「截断后派生字段仍完整」 验证:62 个 man 页 + perldoc 与改动前逐字节一致(脚本比对 HEAD 与工作区); 小预算下 flags/summary/examples/see_also 与不限额时完全相同;真实 info py 在 生产环境 PHP 8.5 下 JSON 16.1MB → 2.88MB(-82%)、峰值 67.4 → 46.5MB。
fix: 索引的 url 字段指向可用的地址(/man/json 实际是页面查询) getManIndex() / getInfoIndex() 输出的 url 自引用写的是 /man/json、/info/json, 但索引没有 command 段,PATH_INFO 表达不了格式——这两个地址会被路由成「名为 json 的页面」并返回 HTML。索引取格式只能走查询参数,phpMan.php 里页脚的格式 链接用的就是 ?mode=X&format=json。 - src/source_man.php / src/source_info.php: url 改为 ?mode=man|info&format=json 验证:四个地址均返回合法 JSON(man 索引 count=10、info 索引 count=195, mcp 形式为合法信封)。
fix: 大页面的 MCP/JSON 不再经由 IR 字符串(info py 内存超限)
info py 是 430k 行 / 19.5MB 的页面。MCP 路径原本把 IR 序列化成 ~15MB 字符
串、再由 formatForOutput() 解码回来,转换过程中同时持有三份整页文本,在
128MB memory_limit 下直接 fatal(生产日志累计 8 次,最近 02-Sep)。
- src/format_json.php: formatToJSON() 拆出 buildJsonData(): array;行缓冲折
进 $sections 后立即释放,编码前 unset($sections)。释放必须通过引用写空
($lines = array())——unset() 只去掉本地别名,调用方的数组仍然存活
- src/format_mcp.php: 抽出 formatMcpEnvelope(array),新增 formatPageOutput()
—— mcp 直传数组,json 走原路径;数组已在手的 search/索引页同样跳过
encode+decode
- source_{man,info,perldoc,pydoc,ri}.php: 9 处调用点改走 formatPageOutput
- test/integration/test_formatter_mcp.php: 锁定「数组路径 == 字符串路径」
验证:新旧路径输出字节级一致(mcp/json × 页面/索引,仅 generated 时间戳因
跨秒不同);info py chunk 预留峰值 127.4 → 104.0MB,peak_real 不变(67.4MB);
staging 真实请求 3×200,日志零新增。
fix: ?debug=1 不再为追加 _profiling 键重序列化整个响应 phpMan.php 原本用 json_decode() + json_encode() 给 json/mcp 响应加一个 _profiling 键。对 info py 这种 19.5MB 的响应,decoded 数组本身就要 44MB, 单独测峰值 62MB real / 127.6MB chunk,自己就贴着 128MB 上限——staging (PHPMAN_DEBUG=true)对该页面因此稳定 500,而生产因为 debug 关闭不受影响。 - src/cache.php: 新增 appendProfilingJson(),把 _profiling 注入到已序列化的 JSON 对象末尾(结果仍是合法 JSON);JSON 数组与非 JSON 响应原样返回, 避免旧实现把数组响应悄悄变成对象 - phpMan.php: 改调 appendProfilingJson() - test/unit/test_profiling_append.php: 15 条断言覆盖普通/紧凑/空对象、 尾随空白、JSON 数组、非 JSON、空响应、2000-section 大响应 验证:staging(debug 开启)连续 3 次未缓存请求全部 200,JSON 合法、 _profiling 为最后一个键、10757 个 section 完整,日志零新增。
fix: FTS5 查询把名字里的 and/not 误判为操作符(no such column) \b(AND|OR|NOT|NEAR)\b 在 "placeholders-and-bind-values"、 "SQL::Statement::Operation::And" 这类名字里同样成立,整串被原样返回、 未加引号;而未加引号的 -/: 让 FTS5 把后一个词读作列名,报 "no such column: and|not|SQL|forwarding"。生产日志累计 134 条该报错, 搜索静默降级到 apropos,丢失 FTS5 排序与前缀匹配。 - src/search_index.php: buildFtsQuery() 改为按空白切分后整词比对操作符 - AND 与隐式连接等价、裸 NEAR 非法(需 NEAR(a b, N)),不再参与拼接 - OR/NOT 仅在两个词之间保留;开头/结尾/连续出现时丢弃而非报错 - 引号短语判断前移,避免 "a AND b" 被拆开 - 其余词仍走 "term"* 引号路径(原有行为不变) - test/unit/test_search_fts.php: 12 条字符串级回归断言 - test/unit/test_search_fts_db.php: 9 条真实 FTS5 表执行回归断言 验证:21 个输入对照新旧实现,旧实现 10 个报 FTS5 错误、新实现 0 个; 测试 326 → 347 通过,0 失败。
openclaw update连续 4 次失败在global install swap步骤:retained package tree changed / Installation recovery is unverified,而安装树事后与 pristine(npm pack 解包)逐字节一致。根因是 swap 的完整性扫描带一个 30s 绝对截止(MAX_SCAN_MS=3e4),本机 454MB / 35050 文件的安装树在冷缓存 + 并发 I/O 下扫不完 → 超时抛错 →previousRoot未定义 → 二次校验误报「树变了」。失败记录只显示错误尾部,真正的超时被截断吞掉,极难定位。绕过用npm install -g openclaw@2026.9.4,根治把MAX_SCAN_MS补丁到 120s(注意:每次升级 npm 会覆盖补丁,需重打)。
openclaw update 稳定卡在 global install swap exit 1,失败记录 stderrTail 只有 ...: retained package tree changed Installation recovery is unverified; inspect the installation and backups in /usr/local/lib/node_modules before restarting.createPackageIntegrityReader 用 MAX_SCAN_MS = 30s 的绝对 wall-clock 截止包住每个 I/O。完整扫描安装树:热缓存 13–16s、冷缓存 ~23s、真实更新条件(刚暂存 454MB 候选包 + 系统在扫新文件)>30s → 抛 Package rollback verification timed out。此刻 prepareBaseline 的 previousRoot 还是 undefined,restoreSwap 的二次校验 !previousRoot 就误报 retained package tree changed。失败记录把 errors 数组从尾部截断,根因那一行被吃掉,只剩 restoreSwap 的次级消息。npm install -g openclaw@2026.9.4。npm 11 默认挡 install scripts,要带 --allow-scripts=openclaw,... 或配 npm config set allow-scripts=...,否则 koffi 原生库 / bundled-plugins postinstall 不会跑。targetVersion 为空时报 plugin-target-unavailable(feishu/opencode 无显式 target)。手动 openclaw plugins update --all 与 @latest 把 feishu、opencode 升到 2026.9.4 后,openclaw update --dry-run 的插件解析即通过。channels.feishu.groupPolicy 必须是 open / disabled / allowlist / allowall,旧的 disable 值会让 gateway 拒绝启动。dist/update-runner-*.mjs 里的 const MAX_SCAN_MS = 3e4; 补丁为 120000(120s),cron 的 openclaw update --yes 下次才能真正过 swap。openclaw update 在 validating 阶段 7 项候选校验全过(global update 57.8s、migration rehearsal、doctor lint、config validation、plugin resolution、migration continuation、gateway canary 23s),然后 swap 失败:
一次 SSH 密钥轮换(旧钥换新钥 + 加口令 + 全节点部署)踩坑后的沉淀,主机信息已隐去。
ssh-add 报 “Could not open a connection to your authentication agent”,九成是 SSH_AUTH_SOCK 没指向 launchd agent 的 socket——agent 本身在跑,只是 shell 找不到它:
# 找到 launchd agent 的 socket(路径每次重启会变)
launchctl print gui/$(id -u) | grep SSH_AUTH_SOCK
# 带上 socket 加载钥匙并存入钥匙串
SSH_AUTH_SOCK=<上一步的路径> ssh-add --apple-use-keychain ~/.ssh/id_ed25519_current
配套三件事缺一不可:
ssh-keygen -p -f ~/.ssh/id_ed25519_current~/.ssh/config 加 Host * 段的 AddKeysToAgent yes + UseKeychain yes~/.zshrc 加 socket 自动恢复段(见下文第 4 节),否则新终端里 pdsh 等批量工具全部认证失败~/.ssh/config 只显式指定当下该用的 IdentityFile。ssh-keygen -p -f ~/.ssh/id_ed25519_current
# 输入旧口令(无则直接回车) → 输两次新口令
验证口令已生效(能无口令派生公钥说明还是裸钥):
2026 年 9 月起,Homebrew 停止提供 Intel (x86_64) 的预编译 bottle。对 Intel Mac 用户意味着:任何没有预编译包的 formula,每次升级都要在本机源码编译。实测 rust 类程序在 8GiB 内存的机器上编译常报 SIGSEGV 栈溢出,llvm 等大依赖一次要编译很久。
替代方案 MacPorts 2.12.6 仍持续发布 x86_64 预编译二进制包。将重编译型软件迁到 MacPorts,brew 只留小工具,是一条可行的路。本文整理多台 Intel Mac 迁移的完整经验。
核心原则:MacPorts 有预编译 archive 才迁移;需要源码编译的保留 brew 版。另有一类根本不进系统包管理器——语言生态自带的官方工具链(Python 的 uv、Rust 的 rustup 等),见下文。
archive 探测(200 即迁,404 则保留):
curl -sI -o /dev/null -w "%{http_code}\n" \
https://packages.macports.org/eza/eza-0.23.5_0.darwin_24.x86_64.tbz2
_0 是 port revision,可能为 _1/_2。404 不一定是没有:
ripgrep-15.2.0_0+pcre.darwin_24.x86_64.tbz2、wget-1.25.0_1+gnutls.darwin_24.x86_64.tbz2。裸文件名探 404,加上 variant 就是 200。port install 会自动选择默认 variant,无需手动加。https://packages.macports.org/<port>/,能列出全部可用 archive 文件名。| brew | MacPorts 端口 |
|---|---|
| python@3.14 | python314 |
| go | go-1.27(无裸名端口) |
| httpd | apache2 |
| node@22 | nodejs22 |
| mysql-client@8.4 | mysql9(等官方二进制) |
| imagemagick | ImageMagick7 |
| ffmpeg | ffmpeg(+gpl2 variant 才有完整编解码) |
批量迁移要全自动,给 port 开 NOPASSWD(只限这一条命令):
openclaw update升 2026.9.3 时openclaw doctor卡在状态库迁移:SQLite canonical index idx_skill_workshop_collection_reviews_workspace_time failed ... no such column: workspace_dir。更新恢复机制按设计保持 Gateway 停止,于是 Telegram / Feishu 全部无响应。根因是 2026.9.x 迁移代码只补了claim_released_time列、漏补workspace_dir列;把两张空的 skill_workshop 表删掉让 canonical DDL 重建即可。
reason: "runtime-verification-failed",doctor 停在 no such column: workspace_dir。skill_workshop_collection_reviews 表从 owner_agent_id 列改成 workspace_dir 列(proposals 表新增 workspace_dir NOT NULL + claim_released_time),但迁移代码 ensureSkillWorkshopSchema 只执行了 ensureColumn(..., "claim_released_time INTEGER"),没有补 workspace_dir。随后 repairCanonicalSqliteIndexes 重建 canonical 索引 idx_..._workspace_time ON (workspace_dir, ...) 时列不存在 → 抛错 → doctor 拒绝继续 → 更新恢复保持 Gateway 停止。skill_workshop_* 表全部是空的,直接删表 + 删旧索引,让 canonical DDL(CREATE TABLE IF NOT EXISTS + 正确索引)按需重建。备份后零数据损失。openclaw doctor EXIT 0 → openclaw gateway start → Telegram / Feishu running, connected → 重跑 openclaw update 顺利升到 2026.9.3。openclaw update 输出里 doctor 阶段失败,诊断手稿(~/.openclaw/logs/support/openclaw-triage-prompt-*.md)记录: