dsh-relay 测试与贡献指南
August 25, 2026 · View on GitHub
本插件把「测试用例」当作一等公民:任何改动/新通道,跑完整个测试套件并贴出通过输出,即可作为提交凭证。测试分三层:
| 层 | 文件 | 覆盖 | 依赖 |
|---|---|---|---|
| 单元测试 | src/index.test.js(69 项) | 路由(严格+语义)、提问应答解析、编号注册表、状态存储、编号每日重置、脱敏、邮件认证解析、iMessage attributedBody 提取与已读 SQL、微信 iLink 解析 | 纯 Node,无外部服务 |
| dry-run 集成 | src/dryrun.test.js(27 项) | 审批监听(批准/拒绝/超时/未启用)、提问监听(应答/超时/放行)、/relay 命令、重启恢复、注入防污染、会话范围语义 | 模拟 ctx,零真实发送 |
| apply 冒烟 | src/apply.smoke.js | 插件装载、监听器以 prepend+global 注册、通道未配置时不启动 | 模拟 ctx |
运行
npm test # 统一运行器(等价 node test/run-all.mjs),任一失败即退出码 1
node src/index.test.js # 单跑单元
node src/dryrun.test.js # 单跑 dry-run
贡献流程(提交到仓库时):
npm test→ 输出以全部测试套件通过 ✅结尾;- 把完整输出贴进 PR/提交说明;
- 若新增了通道/功能,附上对应的手动验收记录(见下),或至少给出 dry-run 测试;
- 通过后即可合并,完善整体设计。
手动验收清单(真实通道 E2E,需一台已授权的设备)
已在本机 iMessage 上完成;其他通道(email/wechat/telegram…)按同一协议验收。 建议只在测试会话上做,避免污染正在使用的对话。
| # | 场景 | 操作 | 预期 |
|---|---|---|---|
| M1 | 推送链路 | 跑任意 agent 任务至一轮结束(或插件手动推送) | 通道收到 [✅ 完成] … 或诉求推送 |
| M2 | 收信+回执 | 通道发 状态 | 收到状态总览(总开关/会话策略/通道/待办) |
| M3 | 语义待办 | 通道发 还有哪些没处理 | 收到待办列表(无则"(无)") |
| M4 | 提问全流程 | 网页 ask_user_question 触发 | 通道收到 ❓ #N 需要你的意见,回复 #N 1 后网页拿到答案 |
| M5 | 审批-批准 | 新会话触发审批 | 通道收到 🔐 #N,回复「把N号批了」→ 任务放行 |
| M6 | 审批-拒绝 | 再触发一次,回复 #N 拒绝 | 任务被拒 |
| M7 | 编号应答 | 并发两条诉求 | 编号不冲突,按编号各自应答 |
| M8 | 开关 | 通道发 关闭 / 开启 | 状态切换,关闭后不再推送 |
| M9 | 会话范围-严格 | 全部关闭 / 全部开启 / /enable <id> | 会话策略切换 |
| M10 | 会话范围-模糊 | 打开cb200 / 开启会话cb200 / 把cb200对话打开 / 关闭cb200对话 / 关掉最近会话 / 启用最近 / 把那个对话关掉 | 均作用于指定会话(短前缀/最近活跃),不误动总开关 |
| M11 | 绑定-模糊 | 绑定当前对话 / 绑到最近会话 / 解绑 / 取消绑定 | 绑定/解绑生效;解绑后裸文本不注入 |
| M12 | 会话列表-模糊 | 有哪些会话 / 会话列表 / 列出当前所有对话 / 所有对话 | 返回全量会话列表(id/标题/开关/活跃) |
| M13 | 外来编号静默 | 同通道其他 bot 发 #N …(非本插件编号且无 pending) | 本插件不回复 |
| M14 | 文本注入 | 绑定会话后发裸文本 | 注入到绑定会话(带安全护栏前缀),不跟随"最近活跃" |
| M15 | 电脑端处理完成通知 | 诉求超时后转网页,在电脑上批准/拒绝/回答 | 通道收到 #N ✅/❌ 已在电脑端处理(回答摘要),待办从队列移除 |
| M16 | 轮次推送开关 | 观察多轮任务 | 默认不推送轮次结束(turnEndPush: false);开启后片段 ≤120 字 |
Email 通道验收提示
Email 走与 iMessage 相同的 M1–M16 清单,额外注意:
- 配置
imap/smtp+passRef(~/.dsh/.credentials.yaml里加DSH_RELAY_EMAIL_PASS: <授权码>); - 第一封测试:发
状态→ 应收到状态总览;发件人必须是白名单地址; - 回信匹配:直接"回复"插件推送的邮件(
In-Reply-To命中自动补编号),或正文写#N 批准; - 认证失败(SPF/DKIM/DMARC 任一 fail)的邮件会被拒绝并告警。
通道成熟度与实现来源
「尽量沿用成熟实现」的落实:每个通道的实现来源都明确标注,新增通道优先移植下列成熟仓库。
状态说明:
✅ 端到端验收= 作者已在真实通道上跑通完整闭环(收发回执 + 诉求应答 + 回信匹配);预留接口= 未实现,欢迎社区按src/channels/types.js契约接入。
| 通道 | 实现 | 成熟来源 | 状态 |
|---|---|---|---|
| iMessage | AppleScript 发送 + chat.db 轮询(attributedBody 提取) | 自研(macOS 26 移除了消息类脚本,chat.db 是唯一可靠收信途径);模式沿用 im-bridge 的桥接/去重/游标设计 | ✅ 端到端验收(收发回执 + 诉求 #N 应答闭环 + 已读标注本地生效) |
| iLink 扫码登录 + 长轮询 | dsh-im-bridge(MIT)src/ilink.ts 逐行移植 | ✅ 端到端验收(扫码绑定 → 收信 → 路由 → 回执,全链路走通) | |
| imapflow(IMAP)+ nodemailer(SMTP)+ mailparser(MIME) | 业界标准 Node 库(多数 DSH 邮件插件的同一栈);SPF/DKIM/DMARC 校验、In-Reply-To 编号回填 | ✅ 端到端验收(IMAP 轮询收信 + SMTP 发送 + 认证校验 + 回信闭环:Gmail 回复 → In-Reply-To 补编号 → 插件收信应答,全链路走通) | |
| Telegram | Bot API 长轮询(待接入) | LoserFox/telegram(源自 Hermes 适配器:getUpdates/sendMessage/HTML 分段/用户白名单) | 🔧 预留接口,待社区实现 |
| 飞书/Lark | 卡片交互(待接入) | imetn/dsh-lark-bridge(WebSocket 长连接、卡片审批/提问) | 🔧 预留接口,待社区实现 |
| 钉钉 | 待接入 | 参照 im-bridge 通道层契约 | 🔧 预留接口,待社区实现 |
邀请社区贡献:Telegram / 飞书 / 钉钉 尚未实现,按 src/channels/types.js 契约实现 configured/start/stop/send/isTrusted/status + 接入 pushInbound 即可;也欢迎为各通道在更多平台上的 E2E 验收补充验证记录(见下方贡献流程)。
通道契约见 src/channels/types.js(configured/start/stop/send/isTrusted/status + pushInbound)。
历史修复(回归测试锚点)
这些 bug 都有对应单测,改动时不可回退:
- 双重复去重:去重只在
pushInbound一处(轮询不得预标记),否则入站永不派发(dryrun 覆盖); - 同账号消息:iMessage 手机/电脑同 Apple ID 时消息记为
is_from_me=1、正文在 attributedBody——收信必须两者都收、用自标记D5HR42排除自身推送; - ASCII 命令提取:attributedBody 分段提取需命令词兜底(
/sessions等); - 注入目标:只进显式绑定会话(
/bind或配置boundSessions),禁止跟随"最近活跃"(避免污染用户正在用的对话); - 外来编号静默:非本插件编号且无 pending 时静默,不插嘴同通道其他 bot;
- 电脑端处理完成通知:诉求超时移交电脑端(网页 UI)后,待办即从队列移除,并向通道推送
#N ✅/❌ 已在电脑端处理,避免手机悬挂未答复诉求(dryrun 覆盖); - 模糊语意-会话目标:
打开cb200/开启会话cb200/把X对话打开/关掉最近会话/绑定当前对话/解绑/有哪些会话必须解析到会话级操作,通道名(微信/imessage)不得被吞、不得误动总开关; - 轮次推送默认关闭:
turnEndPush !== true不推送,开启时片段 ≤120 字(避免把整段回复推到通道); - 编号每日重置、stale 诉求重启后不可应答、外发脱敏、邮件发件人认证。
覆盖矩阵
正向推演的测试设计表 + 质量检测基线(覆盖率快照、盲区标注、回归实证记录)见
test/DRYRUN.md。复测:npm run coverage。