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;坏路径能被握手判据发现(反向对照)。
This commit is contained in:
42
plugins/zcode-mail-bridge/lib/is-main.mjs
Normal file
42
plugins/zcode-mail-bridge/lib/is-main.mjs
Normal file
@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 「这个模块是不是被直接执行的入口?」——**必须比真实路径**。
|
||||
*
|
||||
* # 为什么不能直接比 `import.meta.url === 'file://' + process.argv[1]`
|
||||
*
|
||||
* ESM 的 `import.meta.url` 已经是**解析过软链的真实路径**,而 `process.argv[1]`
|
||||
* 是命令行里写的那个(可能是软链)。两者在软链下不相等,于是入口判断为假、
|
||||
* `main()` 从不执行 —— 没有输出、没有报错、**退出码 0**。
|
||||
*
|
||||
* 这不是假设:生产部署的布局就是软链(`/opt/agentmail/plugins/<name>/current`
|
||||
* → 某个时间戳目录),第一次从快照起 MCP 服务器时正是这样「握手 0 个工具、
|
||||
* stderr 一个字都没有」。而同一个文件从仓库路径跑是正常的 ——
|
||||
* 因为仓库路径没有软链。
|
||||
*
|
||||
* 这类缺陷的特点是**只在部署形态下出现**,本地怎么试都是对的;
|
||||
* 而它的表现(静默成功)与「功能没被调用」完全一样。
|
||||
*
|
||||
* # 为什么两边都要 realpath
|
||||
*
|
||||
* 只解析一侧仍会在「argv 是软链、真身是另一个软链」这类组合下失配。
|
||||
* 两侧都归一到真实路径,判断才是「同一个文件」这个语义本身。
|
||||
*/
|
||||
|
||||
import { realpathSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/**
|
||||
* @param {string} metaUrl 调用方传 `import.meta.url`
|
||||
* @param {string} [argv1] 默认 `process.argv[1]`
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isMainModule(metaUrl, argv1 = process.argv[1]) {
|
||||
if (!metaUrl || !argv1) return false;
|
||||
try {
|
||||
return realpathSync(fileURLToPath(metaUrl)) === realpathSync(argv1);
|
||||
} catch {
|
||||
// 文件不存在 / 权限不足:**当作不是**入口(保守)。
|
||||
// 判错的代价:要么该跑的没跑(静默),要么被 import 时误跑一遍。
|
||||
// 后者会带来副作用(起服务、改状态),更危险。
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user