Files
MailUI4Agents/plugins/zcode-mail-bridge/lib/attachment-ids.js
JianFeeeee e0e6f86d94 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 均返回
2026-09-12 13:47:51 +08:00

73 lines
3.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 把工具参数里的附件 id 列表归一成 `string[]`。
*
* # 为什么需要它(生产实测)
*
* opencode 上一轮把四个步骤全做完了 —— 下载两个附件、读出内容、上传回传文件 ——
* 却在最后一步卡住:`send_mail` 连续 **6 次**失败,模型自己总结为
* 「attachment_ids 参数有框架级序列化 bug」然后放弃了整个任务。
*
* 真实原因不是框架 bug而是模型把数组写成了 **JSON 字符串**
*
* attachment_ids = "[\"10e73e9f-c2a9-4226-bdb5-34ef1b340eb8\"]"
*
* 桥把这个字符串原样转发给服务端,服务端的严格解码器按契约拒收
* `字段 "attachment_ids" 类型不对:期望 string 数组,收到 string`)。
*
* # 修在哪一层
*
* **不在服务端放宽。** 服务端那个"严格"是刻意的,它挡的是字段名拼错、结构写错
* 这类真错误 —— 松开之后真 bug 会被静默接受同一封邮件少几个附件HTTP 仍是
* 200
*
* **在桥这一层收。** 桥是适配器:模型侧的形状天生不可靠(它按自然语言直觉填
* 参数),而适配器的职责就是把不可靠的输入归一成契约要求的形状。对模型宽容、
* 对服务端严格,这与 homeagent 那个 Go 插件里的 `stringList` 是同一个判断
* (那边的注释写着:「也接受单个字符串……拒绝它只会换来一次重试,而意图毫无
* 歧义」)。
*
* # 接受的形状
*
* - `["a", "b"]` 数组
* - `"[\"a\", \"b\"]"` JSON 数组字符串 ← **本次事故的形状**
* - `"a"` 单个 id
* - `"a, b"` / `"a b"` 逗号或空白分隔
* - `[null, "a", 3]` 混进杂质:丢掉坏的、留下好的
*
* 逐项过滤而不是整体放弃:三个附件里有一个写坏,不该变成"一个都不发"。
* 空串与 null 一并丢掉 —— 服务端的 parseAttachmentIDs 也跳过空串,
* 与它保持一致,免得插件放过去的东西换个形状在服务端再失败一次。
*
* @param {unknown} value 工具参数里的原始值
* @returns {string[]} 归一后的 id 列表(永不为 null
*/
export function normalizeAttachmentIDs(value) {
if (value === null || value === undefined) return [];
if (Array.isArray(value)) return value.flatMap(normalizeAttachmentIDs);
if (typeof value !== 'string') return [];
const raw = value.trim();
if (raw === '') return [];
// JSON 数组字符串:本次事故的形状。解析失败不算错 —— 它可能就是普通 id。
if (raw.startsWith('[')) {
try {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) return parsed.flatMap(normalizeAttachmentIDs);
} catch {
// 落到下面的分隔符分支
}
}
// 逗号/空白分隔:模型偶尔写成 "a, b"。
// 只在确实含分隔符时切分,避免把单个 id 切坏 —— uuid 里没有这些字符。
if (/[,\s]/.test(raw)) {
return raw
.split(/[,\s]+/)
.map(s => s.trim())
.filter(s => s !== '');
}
return [raw];
}