|
|
630b5bfdd7
|
fix(bridges): 续谈失败静默 + duplicate_relay 静默挂死(两个都是「人那边什么都收不到」)
同一类问题在两个地方:出事的当下看不出来,表现是「信发出去了,然后再无音讯」。
## 1)pi 的续谈失败不回失败信(实测缺口)
模型侧 402(余额不足)时,**新会话**那条路会回一封「处理失败」,而**续谈**那条路
只写日志就 `throw` —— 发件人什么都收不到。邮件驱动的会话没有本地界面可以看,
没有这封信就等于静默挂死。复现条件很普通:往一条**已存在**的会话再发一封信。
修法与邻居一致:续谈失败也回失败信。但**不能复用**共用库的 `renderFailureReport`
——那段文案说「划定范围内的模型全部调用失败」并建议「调整可用模型范围」,
而续谈是**故意不降级**的(换模型=换会话=丢掉上下文,而上下文正是发件人指定
这条会话的原因)。照抄等于让人去调一个在这里无效的旋钮,他会去改配置,
然后发现依然失败。新增 `renderResumeFailure`:点明是续谈、附上游错误原文、
建议「确实要换模型就新建一条会话」。
**活体验证**(模型侧仍是 402,失败本身就是测试条件):发一封进 pi 的已有会话,
5 秒内收到失败信,内容含 402 原文且不再出现「调整模型范围」。
顺带把 pi 里 2 处没 clamp 的 relay_key 收敛(上一轮审计只看了权限键)。
## 2)duplicate_relay:只有 zcode 认,另三桥会等一个永远不会来的决策
网关对重复的 relay_key 回 **HTTP 200 `{status:"duplicate_relay"}` 并提前返回**:
不建请求、不发邮件、**永远不会有人来决策**。zcode 桥认它并当场失败,而
pi/opencode/dsh 把它当成功,接着等 `permission_decision` 事件 —— pi 那句
`await new Promise(...)` 连超时都没有。这是 zcode 上一轮那个缺陷的同类,
只是发生在另三个桥上。
- `lib/relay-key.js`(**共用**,四处逐字节同源)新增 `isDuplicateRelay` /
`DUPLICATE_RELAY_STATUS`:它长得像成功(200),所以必须单独认;对「发信」
那一侧重复就该当成功(幂等),但对「等一个决定」那一侧它与故障后果相同。
- pi / opencode / dsh 三桥在权限转发处接上判据并**当场拒绝**
(各自用自己的拒绝形状:`block: true` / `output.status = "deny"` / `'rejected'`)。
- zcode 里那份本地实现收敛到共用库(同一判据不该有两个定义)。
## 3)新增接线断言(带判据自检)
`test/permission-forward-wiring.test.mjs`(pi/opencode/dsh 三份同一内容):
纯函数测试对这类缺口天生无能为力(函数是对的,只是没人调用它),所以它读源码
验形态,钉住「判据在、落在权限转发这条路上、给出本桥形状的拒绝」。
三条自检都在写的过程中抓到了我自己的错:
- 第一次 `ROOT` 算错 → 过滤后 0 个桥、循环全不跑而「全绿」→ 加了
「找不到装着各桥的目录就判红」;
- 顺序判据写成「在文件里最早的 await 之前」,量到了别处的等待 → 三桥全红,
改成「必须在上报之后」;
- dsh 是**两段式**(`.then` 里抛、`catch` 的 `duplicateRelay` 分支里拒),
第一版抽取套错了分支 → 永远找不到 `return 'rejected'`。
扰动验证:把 pi 的判据禁用后该条变红,还原即绿(改动前后都核对了字节数)。
而 dsh 那条也暴露了:我把返回形状写成了 opencode 的 `{status:'deny'}`,
**`tsc` 没报错**(返回类型是宽联合),只有对着邻居读才发现 DSH 要的是
`'rejected'` 字符串 + `noteDenial`。
## 4)部署脚本:zcode 分支现在会重启驱动
`redeploy-plugin.sh` 的 zcode 分支只切软链(宿主是 ZCode 应用,不能重启它),
但**驱动是我们自己的 unit** —— 不重启它,进程里跑的还是切换前的代码。
这个由刚写的 `check-deploy-drift.mjs` 当场抓到(它比进程启动时刻与软链切换时刻),
而当时所有其它检查都是绿的。已补上重启并验证。
## 复查
四桥全量 413 / 321 / 370 / 380 全绿;共用库四方同源;部署漂移四项全通过;
四桥真发真收冒烟(dsh/opencode/zcode 正常回信;pi 因模型侧 402 回失败信 ——
这正是上面第 1 条要修的路径)。
另:写这段时踩到一个自伤 —— 用 `npx asar extract-file <asar> dist/index.html`
检查包内容时,它把文件**写进了 cwd**,正好覆盖掉 Vite 的源码模板
`client/electron/index.html`(下次构建会拿被污染的模板去构建)。已还原并重建,
产物哈希与之前一致。要看 asar 内容请用 `@electron/asar` 的 API(返回 Buffer),
别用这个 CLI 子命令。
|
2026-09-12 23:13:48 +08:00 |
|
|
|
b53afd4523
|
chore(deploy): 部署漂移检查器 —— 快照是不是还等于仓库、进程到底在跑哪份代码
「部署脚本跑过了」不等于「线上跑的是当前代码」。三种各自独立的漂移,谁都不会
在意,直到出问题时才发现线上是几天前的行为:
1. 仓库改了、快照没重新部署(快照是独立副本)
2. **软链切了、进程没重启** —— `current` 只是个符号链接,切换它不会重载
已在跑的进程,进程持有的是启动那一刻加载进内存的代码
3. 单元/配置改了、没 daemon-reload 或没重装
`node deploy/check-deploy-drift.mjs`(`--self-check` / `--json`)把三件事变成
可判定的:逐字节比内容(**不用 diff** —— 本机 PATH 上那个是鸿蒙工具链里的,
对内容不同的文件仍返回 0)、读进程真实 argv、比进程启动时刻与软链切换时刻。
## 判据必须按宿主真实的加载方式分开写(这是踩出来的)
第一版对四个宿主用同一套判据(「单元 ExecStart 指向 current」+「进程 argv 里有
快照路径」),结果 **opencode 与 dsh 双双假红**:它们的插件由**宿主进程**加载,
argv 里永远只有宿主自己的可执行文件。永假条件在检查器里表现为「稳定的红灯」,
人会学会忽略它。
| 宿主 | 谁加载 | 从哪里读 | 切换后要重启吗 |
|---|---|---|---|
| pi / zcode | 自己的进程 | unit 的 ExecStart | 要(启动时加载) |
| dsh | dsh 宿主 | profile `link:` → `node_modules` 软链 | 要(服务启动时) |
| opencode | opencode 宿主 | `opencode.jsonc` 的 `plugin:` | 不要(会话创建时惰加载) |
## 判据自检也修了一次
第一版自检「宿主表:运行时加载的宿主必须标记需要重启」写的是
`h.restartOnSwitch !== false`,而 `undefined` 也被放过 —— 于是 pi/zcode 的
`restartOnSwitch` 缺省成 undefined、落进「惰加载」分支、判据**永真**。
现在这条判据抽成纯函数 `judgeRestart`,自检用**行为用例**盖住:
旧进程 + 启动时加载的宿主必须判红、惰加载的宿主不判红、切换后启动的一律放行、
读不到启动时刻时不据此判红、容差内不判红。
## 扰动实验(在真实对象上证明判据会红)
- 改一个仓库运行文件 → pi 的「① 运行文件与仓库一致」变红,恢复即绿 ✓
- 只把 pi 的软链 mtime 拨到刚刚(模拟「切了没重启」)→ 「④」变红并指出原因 ✓
- 把 dsh 与 opencode 的软链都拨动 → **dsh 红、opencode 绿**(惰加载该保持绿)✓
全部拨动均已在实验后还原并复核为绿。
## 当前结论
四个宿主都在跑当前代码(pi/opencode/dsh 的快照自 11:45 起未变,其间无提交
动过它们的运行文件;zcode 是 18:57 的快照)。「三桥加载规范化」的切换本身
早已完成,缺的是这条可复跑的判据 —— 补齐了。
另外修了 `redeploy-plugin.sh` 的一个静默问题:`usage()` 按**行号范围**截取头部
注释(`sed -n '2,40p'`),我这次的注释把退出码那段挤到 40 行之后,`--help`
就少了半页。已同步范围并就地记下这个陷阱。
|
2026-09-12 20:39:22 +08:00 |
|
|
|
6a7356ebe7
|
fix(zcode): 软链部署下入口静默不执行 + 部署脚本支持 zcode 快照
## 缺陷:入口判断不解析软链 → 生产形态下 main() 从不执行
`mcp/server.mjs` 与 `src/index.mjs` 都这么判断是否被直接执行:
import.meta.url === `file://${process.argv[1]}`
而 ESM 的 `import.meta.url` 是**解析过软链的真实路径**,argv 是命令行里写的那个。
生产布局是软链(`/opt/agentmail/plugins/<name>/current` → 时间戳目录),
于是两者不等、`main()` 从不执行:**没有输出、没有报错、退出码 0**。
它是被部署脚本的后置验证抓到的:第一次从快照起 MCP 服务器时
「握手 0 个工具、stderr 一个字都没有」,而同一个文件从仓库路径跑完全正常
(仓库路径没有软链)。这类缺陷只在部署形态下出现,本地怎么试都对;
表现(静默成功)又与「功能没被调用」一模一样。
修法:新增 `lib/is-main.mjs`,**两侧都 realpath** 后比较(只归一 != 「同一文件」)。
单测含目录软链、文件软链、文件不存在(保守判否,避免被 import 时误跑一遍)。
## 部署脚本:引入 HOST 概念,四个插件一条部署路径
pi/opencode/dsh 的宿主是 systemd 单元,zcode 的宿主是 ZCode 应用本身 ——
没有我们的单元可重启。于是:
- `HOST=systemd`:重启单元 + 看网关库里有没有新心跳(原有判据)
- `HOST=zcode`:**从快照起一次 MCP 服务器并走完握手**,这与 ZCode 加载插件
走的是同一份入口代码;另查 `plugins list` 报告的路径是不是 current
后置验证带**判据自检**:握手函数对坏路径必须返回 0,否则判据本身失效就拒绝通过。
计数用 `grep -o | wc -l` 而不是 `grep -c` —— 每条响应只占一行,tools/list 的
11 个工具名全在同一行,用 -c 会得到 2,把好快照判失败(实测踩过)。
## 生产已切到快照
`/opt/agentmail/plugins/zcode-mail-bridge/current → 20260912-150547`,
`~/.zcode/cli/config.json` 的 `plugins.dirs` 已指向 current,
`zcode plugins list` 报的路径就是快照路径。仓库不再是生产代码。
验证:单测 325/325;快照握手 12 个 name 字段;官方 __zcode-plugin-host 从快照
启动正常;钩子从快照跑通 409 → block;坏路径能被握手判据发现(反向对照)。
|
2026-09-12 15:06:49 +08:00 |
|
|
|
cbe5ef5cec
|
fix(deploy): 快照改整包复制 + 存活判据改看网关心跳 —— 修两个会毁掉生产的缺陷
切换三个桥时这两个缺陷都真的触发了,记下来避免重犯。
# 缺陷一:白名单拷贝漏文件 → 服务直接起不来
第一版 staging 用白名单:`package.json lib src index.js dist`,漏掉了 dsh 的
`cordis.patch.yml`(dsh 读它做 overlay 配置)。后果不是「少个文件」而是**服务崩溃循环**:
Error: dsh: failed to read overlay .../dsh-mail-bridge/cordis.patch.yml: ENOENT
白名单的失败模式天生如此:**默认不带**,漏一个就等启动时炸,而那时旧版本已经被换掉。
改为整包复制 + 剔除明确不需要的(`test/`、`.git`、`node_modules/.cache`、`*.log`)。
# 缺陷二:存活判据写成了某一个平台特有的措辞
第一版后置验证 grep 日志里的「已接入」。那句话**只有 pi 与 opencode 会打印**,
dsh 启动时只输出 `dsh web: http://127.0.0.1:3080` —— 于是**一次成功的 dsh 部署
被判成失败**,脚本按设计回滚……回滚到了缺 cordis.patch.yml 的坏快照,
把 dsh 推进崩溃循环。
改为查网关库(平台无关,且是真的端到端):
SELECT count(*) FROM agents WHERE agent_name='$PLUGIN' AND last_seen > '$RESTART_AT'
**时刻必须用 UTC**:`agents.last_seen` 是 UTC(CURRENT_TIMESTAMP 语义),
而 `date` 默认给本地时间 —— 拿 11:44 去比 03:44 会永远为假,于是每次部署都被判成
「桥没连上」并回滚,**门禁主动破坏生产**。已用 `date -u`。
判据双向验证过:以「3 分钟前」为重启时刻 → 命中 1;以未来时刻 → 命中 0。
# 本次切换结果
pi /opt/agentmail/plugins/pi-mail-bridge/current/src/index.mjs
opencode /opt/agentmail/plugins/opencode-mail-bridge/current/index.js
dsh /opt/agentmail/plugins/dsh-mail-bridge/current/dist/index.js
三处配置已迁移(备份在 /root/config-backups/pre-snapshot-20260912-113826/):
pi 的 unit、opencode.jsonc 的 plugin 项、dsh profile 的 link:(含 pnpm install 重建软链)。
验证:三桥进程**打开仓库文件数均为 0**;快照与仓库文件 inode 不同(独立副本);
四个 Agent 心跳新鲜;dsh 读 cordis.patch.yml 走的就是该软链(坏快照时它起不来,
好快照时它 active —— 这条是最硬的证据)。
|
2026-09-12 11:47:32 +08:00 |
|
|
|
0549975555
|
feat(deploy): 插件规范化部署 —— 生产跑仓库外快照,可回滚
# 问题(实测的加载形态)
pi systemd: node /home/program/agentmail/plugins/pi-mail-bridge/src/index.mjs
opencode opencode.jsonc: "file:///home/program/agentmail/plugins/opencode-mail-bridge"
dsh profiles/web/package.json: "dsh-mail-bridge": "link:/home/program/.../dsh-mail-bridge"
三个桥跑的都是**仓库工作区**。于是:一次编辑 + 重启就是上线(没有构建、没有评审、
没有版本);**没有回滚目标**(网关有 `.bak-<时间戳>`,三个桥一个都没有);
`git checkout` / `git stash` / 半成品编辑会静默改变线上行为;仓库同时兼作构建目录
(`dist/`、`node_modules/` 都在里面)。
对照:homeagent 插件本来就是这个规范形态(跑 `/home/newqqagent/plugins/
homeagent-mail-bridge/plugin.bin` 部署副本)—— 所以这里不是发明新办法,
而是把已有的那个形态推广到三个 JS 桥。
# 本提交只交付工具与门禁,**未切换生产**
`deploy/redeploy-plugin.sh <pi|opencode|dsh> [--stage-only]`:
1 前置断言 → 2 staging(仓库外)→ 3 门禁 → 4 原子切换 `ln -sfn <ts> current`
→ 5 重启 → 6 后置验证 → 任一步失败即切回 `.prev` 并重启
后置验证不只看 `systemctl is-active`:桥可能进程活着却没连上 Gateway
(密钥失效、Gateway 未起、依赖在惰加载时才暴露),所以**必须以日志出现
「已接入」为准**,45 秒轮询。
需要编译的插件(dsh)**产出到 staging**,不写仓库的 `dist/`:先在仓库构建再拷贝
会有两个问题 —— 失败的构建也会 emit(tsc 默认 `noEmitOnError=false`)导致仓库产物
被半成品覆盖;以及生产产物与工作区之间多一条看不见的耦合。
# 依赖门禁:不能用 require.resolve
`deploy/check-plugin-snapshot.mjs` 从入口递归收集静态 import/export/动态
import/require 的说明符并逐个解析。
**判据用 ESM 而非 CJS 解析**——这是实测教训:`@earendil-works/pi-coding-agent`
的 `exports` 只声明 `"import"`,`require.resolve` 抛 ERR_PACKAGE_PATH_NOT_EXPORTED,
而插件是 `import` 它的、实际毫无问题。用错 API 会让门禁**报假缺陷**,
而假缺陷比不检查更糟(会让人去修一个没坏的东西)。
也不能只比对 `package.json` 的 dependencies:pi 的 dependencies 是 `{}`,
而它 import 了 `@earendil-works/pi-coding-agent` —— 声明是假的,只查声明等于空跑。
# 已验证(干跑,未切换、未重启)
pi 20/20 说明符可解析 opencode 23/23 dsh 26/26
dsh 经 tsc 构建到 staging 后通过
反向验证:拿掉一个依赖 → 门禁 rc=1 并点名该说明符(不是空转)。
|
2026-09-12 11:36:25 +08:00 |
|