feat(zcode): AgentMail 的 ZCode 插件 —— MCP 工具面 + 官方宿主启动验证
ZCode 用插件扩展能力(.zcode-plugin/plugin.json 声明 skills/commands/hooks/
mcpServers),所以适配它的正确形状是**插件**而不是又一个独立桥进程。
本提交是第一步:把 AgentMail 的工具面做成 MCP 服务器。
协议层(lib/mcp-rpc.mjs)手写,不引 @modelcontextprotocol/sdk:
协议面只有 initialize / notifications/initialized / tools/list / tools/call,
手写可省掉一条构建链与 1MB 打包产物(与 pi/opencode/dsh 三桥零运行时依赖的
取向一致),并让这一层成为可穷举的纯函数。分帧照官方插件产物实测确认是
换行分隔 JSON(Content-Length 出现 0 次,StdioServerTransport + split("\n"))。
工具面(lib/tools.mjs)与另三个桥**同名同参**,渲染走共用的
addressing/inbox-format/discovery(逐字节同源,已纳入 check-shared-libs.sh)。
测试里有一条断言直接拿 pi 桥的工具名做对照:少一个就让某平台行为与其它平台不同,
那种问题只在单平台复现,排查代价最高。
两处按真实缺陷定的行为:
- 工具失败回 result+isError 而非 JSON-RPC error —— 后者会让模型看不到失败原因,
只能重试(opencode 连试 6 次发不出附件正是这个后果)
- attachment_ids 声明放宽为 anyOf 数组/字符串并在桥侧归一 —— 模型常写成
JSON 字符串,服务端严格解码会拒(同样来自 opencode 那次失败)
入口 mcp/server.mjs 修掉一个真实缺陷:stdin 关闭即 process.exit 会杀掉在途请求,
表现为「协议全对但访问网关的调用完全没有响应」。现按在途计数 drain,
且把 stdout 写入也计入,避免最后一条响应卡在缓冲区。
顺带修 check-shared-libs.sh 的一个既有假绿:本机 PATH 上的 diff 是鸿蒙 SDK
工具链的 diff,不认 -q 且对不同的文件仍返回 0 —— 于是该检查器**一直是永真输出**。
改用 cmp -s,并加自检(判据本身必须先被证明能发现差异)。反向验证:
让 zcode 或 pi 的共用模块分叉,检查器都正确报错并返回 1。
验证:
- 单元 33 项 + 继承共用测试 87 项 = 120/120
- `zcode plugins list` → agentmail@inline [enabled],mcp: plugin:agentmail:agentmail
- 经官方 `node zcode.cjs __zcode-plugin-host <server.mjs>` 启动 → 握手与 tools/list 正常
- 真实网关调用:以 zcode 身份 read_inbox / suggest_address / list_contacts 均返回
This commit is contained in:
@ -4,39 +4,109 @@
|
||||
# 一侧改了另一侧没改,几个平台的行为就会悄悄分叉:同一封邮件在 opencode 那边
|
||||
# 标了已读、在 DSH 那边没标,而两处代码看起来都"对"。
|
||||
#
|
||||
# 三方比对以 opencode 为基准逐个对比,而不是两两对比:后者在三方都不同时
|
||||
# 比对一律以 opencode 为基准逐个对比,而不是两两对比:后者在三方都不同时
|
||||
# 会打出三条互相矛盾的差异,读的人无从判断谁是对的。
|
||||
#
|
||||
# # 为什么每个 peer 的清单不同
|
||||
#
|
||||
# 四个插件共用的是**协议无关**的那几个模块(地址解析、收件箱渲染、附件 id 归一…),
|
||||
# 但各自用到的子集不同:
|
||||
#
|
||||
# - pi / dsh / opencode 是邮件驱动的**桥**,需要会话快照、中继去重、SSE 客户端等;
|
||||
# - zcode-mail-bridge 目前是 **MCP 工具服务器**(模型侧的工具面),
|
||||
# 不订阅 SSE、不管会话生命周期,所以只需要那 5 个协议无关模块。
|
||||
#
|
||||
# 用「对等清单」会逼它拷一份用不到的 relay/session 模块进来 —— 那些文件
|
||||
# 永远不会被执行,却要跟着一起同步,反而让「哪些是真的共用」变得看不清。
|
||||
# 于是改成显式列举:**声明共用就必须逐字节相同**,声明之外的模块不检查
|
||||
# (因为根本没拷过来,结构上就不可能分叉)。
|
||||
set -euo pipefail
|
||||
BASE=plugins/opencode-mail-bridge
|
||||
PEERS=(plugins/dsh-mail-bridge plugins/pi-mail-bridge)
|
||||
PEERS=(plugins/dsh-mail-bridge plugins/pi-mail-bridge plugins/zcode-mail-bridge)
|
||||
|
||||
ALL_LIBS="relay-dedup relay-policy relay-key permission-mode bounded inbox-format session-snapshot workspace model-scope catchup addressing discovery rename-proposal permission-grants adopt sse-client user-question attachment-ids"
|
||||
ALL_TESTS="relay-policy relay-key permission-mode bounded inbox-format session-snapshot workspace model-scope catchup addressing discovery rename-proposal permission-grants adopt sse-client user-question attachment-ids"
|
||||
|
||||
# MCP 工具服务器用到的协议无关子集(实现与测试都同步)。
|
||||
MCP_LIBS="addressing inbox-format bounded discovery attachment-ids"
|
||||
MCP_TESTS="addressing inbox-format bounded discovery attachment-ids"
|
||||
|
||||
# ── 判据:用 cmp,不用 diff ─────────────────────────────────────────
|
||||
#
|
||||
# 本机 PATH 上的 `diff` **不是 GNU diff**,而是鸿蒙 SDK 工具链里的
|
||||
# /opt/huawei/harmonyos/ohos-sdk/linux/toolchains/diff。它不认 `-q`,
|
||||
# 却对**内容不同的文件照样返回 0** —— 于是 `if ! diff -q ... >/dev/null`
|
||||
# 恒为假,整支脚本变成永真输出(本检查器曾长期如此,实测才发现)。
|
||||
# cmp 的语义明确:相同 0、不同 1。
|
||||
same() { cmp -s "$1" "$2"; }
|
||||
|
||||
# 判据本身必须先被验证。否则「比较永远说相同」与「真的都相同」
|
||||
# 在结果上完全一样,而这正是上面那个坑的形状。
|
||||
selfcheck() {
|
||||
local a b
|
||||
a=$(mktemp) && b=$(mktemp)
|
||||
printf 'x\n' >"$a"
|
||||
printf 'y\n' >"$b"
|
||||
if same "$a" "$b"; then
|
||||
echo "检查器自检失败:比较函数无法发现差异(别用 PATH 上的 diff)" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'y\n' >"$a"
|
||||
if ! same "$a" "$b"; then
|
||||
echo "检查器自检失败:相同内容的两个文件被判为不同" >&2
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$a" "$b"
|
||||
}
|
||||
selfcheck
|
||||
|
||||
libs_for() {
|
||||
case "$1" in
|
||||
*zcode-mail-bridge) echo "$MCP_LIBS" ;;
|
||||
*) echo "$ALL_LIBS" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
tests_for() {
|
||||
case "$1" in
|
||||
*zcode-mail-bridge) echo "$MCP_TESTS" ;;
|
||||
*) echo "$ALL_TESTS" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 差异明细也走 cmp,不借 diff。
|
||||
show_diff() {
|
||||
cmp "$1" "$2" 2>&1 | head -3
|
||||
}
|
||||
|
||||
fail=0
|
||||
|
||||
for peer in "${PEERS[@]}"; do
|
||||
for f in relay-dedup relay-policy relay-key permission-mode bounded inbox-format session-snapshot workspace model-scope catchup addressing discovery rename-proposal permission-grants adopt sse-client user-question attachment-ids; do
|
||||
for f in $(libs_for "$peer"); do
|
||||
if [[ ! -f "$peer/lib/$f.js" ]]; then
|
||||
echo "共用模块缺失:$peer/lib/$f.js" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
if ! diff -q "$BASE/lib/$f.js" "$peer/lib/$f.js" >/dev/null 2>&1; then
|
||||
if ! same "$BASE/lib/$f.js" "$peer/lib/$f.js"; then
|
||||
echo "共用模块已分叉:lib/$f.js($BASE vs $peer)" >&2
|
||||
diff "$BASE/lib/$f.js" "$peer/lib/$f.js" | head -20 >&2
|
||||
show_diff "$BASE/lib/$f.js" "$peer/lib/$f.js" >&2
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
# 测试同样要同源:共用模块的行为约定写在测试里,
|
||||
# 只同步实现不同步测试,等于允许一侧偷偷放宽约定。
|
||||
for f in relay-policy relay-key permission-mode bounded inbox-format session-snapshot workspace model-scope catchup addressing discovery rename-proposal permission-grants adopt sse-client user-question attachment-ids; do
|
||||
for f in $(tests_for "$peer"); do
|
||||
if [[ ! -f "$peer/test/$f.test.mjs" ]]; then
|
||||
echo "共用测试缺失:$peer/test/$f.test.mjs" >&2
|
||||
fail=1
|
||||
continue
|
||||
fi
|
||||
if ! diff -q "$BASE/test/$f.test.mjs" "$peer/test/$f.test.mjs" >/dev/null 2>&1; then
|
||||
if ! same "$BASE/test/$f.test.mjs" "$peer/test/$f.test.mjs"; then
|
||||
echo "共用测试已分叉:test/$f.test.mjs($BASE vs $peer)" >&2
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
[[ $fail -eq 0 ]] && echo " 共用模块三方同源(opencode / dsh / pi)" || exit 1
|
||||
[[ $fail -eq 0 ]] && echo " 共用模块四方同源(opencode 基准 vs dsh / pi / zcode)(判据已自检)" || exit 1
|
||||
|
||||
Reference in New Issue
Block a user