
项目地址https://github.com/ranshaodexiao/dsh-wechat-ilinknpmdsh-wechat-ilinkdsh plugin --profile desktop add dsh-wechat-ilink一条命令安装⚠️ 先看这里本文对应的版本DSH 迭代非常快插件 API 随时可能变。本文所有结论都基于下面这组版本实测版本不同可能失效。组件本文实测版本说明DeepSeek Harness0.2.0-rc.2核心dsh/dsh-agent-loop/dsh-llm/dsh-session同版本Cordis4.0.4插件系统schemastery3.18.4配置校验可选依赖Node.js24.x插件要求 ≥ 22.13.0Electron44.0.0Desktop 外壳本插件dsh-wechat-ilink0.6.5怎么查你自己的版本# 插件声明的兼容范围dsh--version# 或者直接看已安装的 DSH 包版本node-econsole.log(require(deepseek-ai/dsh/package.json).version)本插件的package.json里声明了dsh:{engines:{dsh:0.2.0-rc.2}}⚠️注意这个字段不会被 DSH 强制检查—— 它只是声明不满足也不会拦你详见 1.2 节。所以升级 DSH 后请务必看启动自检的日志见 4.4 节那是唯一能告诉你插件是否还活着的信号。版本敏感度分级文中标注了每个结论的版本敏感度标记含义高直接依赖 DSH 内部实现源码行号、事件名、字段名升级后必须重新验证中依赖公开 API 或稳定约定一般不会变但值得留意低通用工程实践长轮询、日志、去重与版本无关前言为什么写这篇我想在微信里直接用DeepSeek Harness以下简称 DSH。不是把消息转发给某个 API那种而是真的在微信里跟 DSH 的会话对话—— 它能读写文件、跑命令、用工具回复发回微信。做完之后回头看真正难的不是微信协议而是DSH 插件的契约。我踩了 8 个 bug其中 5 个在离线测试全绿的情况下被真实 DSH 打出来。这篇文章把这些坑都写出来。如果你要开发 DeepSeek Harness 插件这些坑你大概率也会踩。一、先搞清楚 DSH 插件是什么DSH 用的是Cordis插件系统。一个插件就是一个 JS 模块导出四个东西exportconstnamewechat-clawbot// 插件名日志标签exportconstinject[agents,sessions]// 依赖的服务exportconstConfigSchema.object({...})// 配置校验exportfunctionapply(ctx,config){...}// 入口打包成bundlepackage.json里声明dsh.bundle.patch指向一个cordis.patch.yml。中—— 插件契约本身稳定Cordis 4.0.4 实测但inject的服务名会随 DSH 增加/改名。1.1 第一个坑inject是硬门槛Cordis 的ctx是Proxy。你没在inject里声明的服务属性访问会直接抛错Error: cannot get property sessionQuery without inject这不是返回 undefined是抛异常。我一开始只想读ctx.agents代码里顺手用了ctx.sessionQuery直接炸。正确做法// 必需的服务 → 写进 injectexportconstinject[agents,sessions,sessionQuery]// 真正可选的服务 → 用 ctx.get()它返回 undefined 而不抛错constattachmentsctx.get(attachments)if(attachments){/* ... */}我封装了一个readOptionalService(ctx, name)处理这个兼容 Cordis 的 Proxy 和普通对象测试里用。1.2 第二个坑peerDependencies会静默跳过整个 bundle高—— 静默失败最难排查的一类问题。bundle 的dsh.engines.dsh字段不会被强制检查。但如果你声明了deepseek-ai/dsh-*的peerDependencies不满足时会静默跳过整个 bundle—— 没有报错插件就是不加载。所以我的插件故意不声明任何dsh-*peer{peerDependencies:{deepseek-ai/cordis:~4.0.4,deepseek-ai/schemastery:~3.18.4},peerDependenciesMeta:{deepseek-ai/cordis:{optional:true},deepseek-ai/schemastery:{optional:true}}}两个都标 optional—— 因为我把它们做成了可降级依赖不装 schemastery 就用内置校验器。1.3 第三个坑Config校验必须同步高—— 依赖 Cordis / Standard Schema 的内部调用方式。resolveConfig内部调的是 Standard Schema 的~standard.validate()必须是同步的。我第一版写了个async validate直接挂。二、微信侧走官方 iLink Bot API不要去 hook 个人微信协议 —— 封号、不稳定、也不道德。微信 ClawBot 背后是腾讯的iLink Bot API有公开的协议行为可参考tencent-weixin/openclaw-weixinMIT。核心就几个端点端点作用get_bot_qrcode申请登录二维码get_qrcode_status轮询扫码状态getupdates长轮询收消息sendmessage发消息sendtyping“正在输入”getuploadurl媒体上传请求头iLink-App-Id: bot iLink-App-ClientVersion: 版本号 AuthorizationType: 1 Authorization: token X-WECHAT-UIN: base64(随机 uint32 的十进制字符串)几个实战要点errcode -14 登录态失效需要重新扫码。插件要能自动暂停轮询、清空游标媒体是 AES-128-ECB PKCS7 加密的aes_key有两种线上编码base64 的原始 16 字节 / base64 的 32 字符 hex 串两种都要处理回复必须带context_token—— 这是会话路由锚点只随入站消息下发。所以无法主动推送只能收到消息才回复三、真正的硬骨头把微信接到 DSH 会话上这是整个项目 80% 的 bug 来源。高全节—— 本节全部依赖 DSH agent 的内部契约。下面每个 API 名、事件名、字段名都在DSH0.2.0-rc.2上实测。升级 DSH 后这一节必须重新验证尤其 3.2 和 3.3。3.1 创建 / 恢复 agentDSH 的 agent API0.2.0-rc.2ctx.agents.get(id)// 拿活着的 agentctx.agents.create(options)// 新建ctx.agents.resume({resumeSessionId})// 恢复已持久化的会话坑固定会话在重启后已持久化不能再create会抛SessionAlreadyExistsError。⚠️ 这个异常来自deepseek-ai/dsh-session-persistence。我按名字判断而不是instanceof—— 因为插件是link:装的instanceof在模块实例不一致时会失效。正确逻辑constexistingctx.agents.get(sessionId)if(existing)returnexistingif(awaitsessionExists(sessionId)){returnawaitctx.agents.resume({resumeSessionId:sessionId})}returnawaitctx.agents.create(options)3.2 坑中之坑{{model}}和{{cwd}}这是最难的 bug也是所有空回复问题的根源。DSH 的系统提示词里引用了三个变量persona 模板含有{{model}}、{{cwd}}You are a coding agent powered by the {{model}} model. ... ... {{cwd}} ...而它们的值来自agent 对象dsh-agent-loop/lib/index.js0.2.0-rc.2 的 1564-1566 行ctx.systemPrompt.variable(provider,(context)context.agent?.options.provider);ctx.systemPrompt.variable(model,(context)context.agent?.options.model);ctx.systemPrompt.variable(cwd,(context)context.agent?.session.header.cwd);任何一个没值提示词组装直接抛错—— 而这个错发生在任何模型请求之前。症状回合立刻结束、零模型调用、回复为空。日志里只有prompt variable {{model}} has no value for this assembly修复constoptions{agentOptions:{provider,model},// ← 提供 {{model}} / {{provider}}meta:{cwd:resolveCwd()},// ← 提供 {{cwd}}}{{cwd}}特别阴险session.header.cwd在会话创建时就固定resume无法补。所以修复前创建的会话是永久损坏的 —— 只能删掉重建。而且 DSH 会把没有 cwd 的会话放在一个叫_no-cwd的目录下。这是我发现真相的关键线索~/.dsh/sessions/_no-cwd/wechat-clawbot ← 目录名本身就是铁证排查技巧turn/end事件的reason.kind error后面就是 DSH 的原始原因。而会话日志是多帧 zstd每次 flush 一个 frameNode 的zstdDecompressSync只读第一帧 ——我一开始就栽在这里以为事件没落盘。中——_no-cwd目录名和多帧 zstd 是观测手段即使 DSH 改了实现去看持久化事件而不是猜这个方法依然成立。3.3 回合边界不能用whenIdle()高—— 这一节的结论完全建立在对 agent 循环内部实现的观测上。我想等 agent 跑完一个回合然后取最终文本。第一版用了agent.whenIdle()——在空闲 agent 上立刻返回拿到空结果。第二版改成等任意turn/end—— 又错了。因为 agent 循环有个特性dsh-agent-loop0.2.0-rc.2// 回合在 driver 唤醒时打开claim 为空时立刻关闭if(phase.step0decision.messages.length0){turnEnds{kind:completed};returnfalse}也就是说会有一个不携带消息、不调模型的空回合它的turn/end会抢先满足我的等待条件。正确做法等持久的边界事件 ——先等agent/inbox/spliced事件里出现我们那条消息的 id证明它被接收了再等那之后的turn/end// 等消息被 admitconstmessageIdmessage.id// 然后等这个回合的 turn/end然后从事件流里提取助手文本取最后一个非空 content。这条规则即使 DSH 改了内部实现也大概率适用不要依赖agent 空闲了这种瞬时状态要依赖持久化事件证明我的消息被接收了和这个回合结束了。前者是观察后者是契约。3.4createUserMessage拿不到怎么办中—— 这是打包环境问题不是 API 问题。DSH 的deepseek-ai/dsh-llm打包在 Electron 的app.asar里。而插件是以link:装进 profile 的 ——裸 import 会解析到插件自己的node_modules找不到。我的做法优先用真实工厂能解析到就用否则用行为等价的本地实现。关键是本地实现要逐字段对齐真实工厂0.2.0-rc.2 的实现就是下面这三步functioncreateUserMessageLocal(input){returndeepFreeze(structuredClone({...input,id:randomUUID(),}))}⚠️ 如果你要抄这个 fallback务必对着当前版本的dsh-llm源码核对字段—— 字段一旦增加本地实现就会静默地少传东西。四、长期运行的稳定性4.1 长轮询不能饿死事件循环低—— 通用 Node.js 问题与 DSH 版本无关。getupdates是长轮询几十秒。如果在一个紧循环里跑会饿死事件循环。每轮迭代让出一次宏任务awaitnewPromise((resolve)setTimeout(resolve,0))我的实现里还挂了AbortSignal让停止时能立刻退出等待而不是干等一个定时器。4.2 去重和退避低按message_id去重网络重试会发重复消息失败退避我用的 2s → 30s连续失败 3 次后升到 30s游标持久化重启后接着收4.3 日志必须落文件低做法 中路径插件跑在 DSH 进程内部它的 stderr 你在外面看不到。所以我把所有运行记录写到一个持久化文件%DSH_HOME%\clawbot\channel.log带 token 脱敏、2 MiB 上限、超限裁剪到 512 KiB。没有这个日志线上问题只能靠猜—— 我前 3 个 bug 全靠它才定位到。4.4 启动自检 —— 版本升级的第一道防线中做法 高价值DSH 更新频繁插件很容易被上游改动打挂。我加了个启动自检启动后跑一个合成回合走真实的provider/model/cwd 提示词组装和真实模型调用把结果写进日志。SELFTEST PASS ms4202 events15 replyChars146 SELFTEST FAIL (turn error) ... reason{kind:error,...}用独立的sessionId-selftest会话不影响真实对话。成本是每次启动一次模型调用但它是 DSH 升级后最早能发现问题的信号。如果你在做 DSH 插件强烈建议也加一个。我所有 8 个 bug 里有 5 个是在提示词组装环节炸的 —— 而自检恰好完整覆盖了那个环节。DSH 升级后你不需要记得去验证任何东西看日志里那一行就够了。五、两个看起来能用其实没用的教训5.1 自检报假 PASS第一版自检的逻辑是没抛异常就算成功。结果它报SELFTEST PASS ... replyChars0回复长度是 0 却报 PASS。这是我验证最典型的失败模式判据本身是错的。现在空回复一律 FAIL并打印turn/end的原始 reason。5.2 离线测试全绿 ≠ 能用这是整个项目最重要的一课。我有 138 个离线测试假 iLink 服务器、假 DSH 上下文全绿。但它们证明不了插件在真实 DSH 上能跑。8 个 bug 里有 5 个是在测试全绿的情况下被真实 DSH 打出来的bug离线测试为什么没发现inject缺失假 ctx 是普通对象不抛错{{model}}无值假 agent 不做提示词组装{{cwd}}无值同上回合边界假 agent 没有真实循环的空回合特性blocked未识别假 agent 不实现归档门根本原因测试替身比真实实现宽松。你写的假对象只会实现你以为需要的东西。出路是用真实的 DSH 代码做验证—— 我从app.asar里解出 DSH 的全部包写了一批验证脚本直接对着真实实现跑// 用真实 Cordisconst{Context}awaitimport(.../cordis/lib/index.js)// 用真实 SessionconstsessionSession.create(id,undefined,{header:{cwd:...}})⚠️ 注意Session.create的签名是(id, seed, header, ...)——header是第三个参数不是第二个。我第一版写成(id, { header })直接报seed.entries is not a function。这类签名细节必须看源码不能猜。六、配得上生产可用的几件事6.1/list/use/back从微信远程接管另一个会话中—— 用的都是公开 APIagents.get/followup设计思路与版本无关。默认微信消息进插件自己的会话。但你可能正在 DSH 里干一个活想在外面用手机看进度、下指令。ctx.agents.get(sessionId)能拿到活着的 agent对它followup()就能把微信消息注入那个会话/list 列出正在运行的会话 /use 1 接管第 1 个 现在到哪了 ← 这条进那个会话 /back 退回微信自己的会话设计要点只拦截精确的命令词。/model、/new、/hello /use 1这些原样转发给 agent不误吞不打断正在跑的活——followup()进的是下一个回合队列/back不销毁别人的会话—— 只销毁插件自己创建的 agent默认只列正在运行的—— 一屏已停止的旧会话只是噪音6.2 归档的会话会被拒绝执行高—— 完整依赖archived-session-gate这个内部插件的行为。DSH 有个archived-session-gatedsh-api-session-controller0.2.0-rc.2ctx.on(agent/pre-step,(payload,next)underArchivedSession(ctx,payload.agent)?Promise.resolve({kind:reject}):next())归档的会话里任何步骤都被拒绝回合以blocked结束不调模型、无输出。我踩这个坑是因为/list第一版把所有会话都列出来一堆噪音我顺手把自己那个会话归档了 ——然后整个通道就死了而且症状是回复为空看不出原因。现在插件会提前检测归档状态并给出明确提示同时/list过滤掉归档/子代理/插件自己的会话。通用教训turn/end的reason.kind不止error和completed。DSH 里至少有completed/aborted/blocked/error/max-tokens/interrupted/forked。只判断有没有报错会漏掉blocked这类静默失败。我漏了一个版本才补上。七、发布GitHub 只放源码npm 放编译产物低做法 高一处依赖这是我认为最值得抄的一个实践渠道内容GitHub只有源码.gitignore排除lib/npm编译后的正式版tarball 含lib/{scripts:{prepare:tsc -p tsconfig.json,prepublishOnly:npm run verify}}preparenpm install和npm publish前自动编译prepublishOnly发布前跑完整测试 ——构建或测试挂了就发不出去好处两个渠道不可能不一致因为 npm 的产物就是从这份源码编译出来的。而且用户安装体验完全不同dsh plugin--profile desktop add dsh-wechat-ilink一条命令搞定。因为 tarball 里已经带着lib/不需要任何构建步骤。如果从 GitHub 直接装pnpm 默认禁止依赖运行构建脚本会卡在prepare上需要手工加allowBuilds配置。这就是为什么用户该走 npm。额外发现dsh plugin add会自动注册 bundle高—— 依赖dsh-plugin-manager的内部行为但这条最值得验证。我原以为用户还要手工编辑dsh.profile.bundles。读了官方 CLI 源码后发现不用dsh-plugin-manager/lib/types/operations.js0.2.0-rc.2 的第 503 行elseif(options.activateNewBundles!false){awaitreconcile(before,dir,context.installAnchor,options);}dsh plugin add会自动把包名写进dsh.profile.bundles。我实测确认了dsh plugin --profile test add dsh-wechat-ilink → bundles: [deepseek-ai/dsh-base, dsh-wechat-ilink] ← 自动写入教训别猜工具怎么工作去读它的源码。我差点把一个不存在的手工步骤写进文档。⚠️ 这个自动注册是当前版本的行为。如果将来变了症状是包装上了但插件不生效—— 那时候检查dsh.profile.bundles里有没有你的包名。八、清单开发 DSH 插件要注意的Cordis 侧inject里列全必需服务可选服务用ctx.get()不要声明deepseek-ai/dsh-*的 peer会静默跳过 bundleConfig校验必须同步用ctx.effect()绑定后台任务的生命周期DSH agent 侧 高风险区升级必查create/resume要处理SessionAlreadyExistsError按错误名判断别用instanceof必须提供agentOptionsprovider model和meta.cwdmeta.cwd一旦漏传会话永久损坏header.cwd创建后不可改回合边界等agent/inbox/splicedturn/end不要用whenIdle()识别turn/end的全部reason.kind至少completed/error/blocked注意archived-session-gate归档会话会被拒绝执行Session.create的签名是(id, seed, header, ...)别把 header 传成第二个参数运维侧日志落持久化文件进程内 stderr 看不到加 token 脱敏加启动自检跑真实回合←DSH 升级后最重要的一道防线长轮询每轮让出事件循环验证侧离线测试全绿不代表能用—— 用真实 DSH 代码做验证检查项别硬编码常量包名、路径从源头读取验证脚本自己也会错 —— 它报 PASS 不等于真的验证过升级 DSH 后看启动自检日志SELFTEST PASS还是FAIL若 FAILreason里有 DSH 的原始报错检查dsh.profile.bundles里插件名还在不在重跑一遍本文 3.2 / 3.3 / 6.2 三节对应的行为九、最后的复盘这个项目从能装上到真的能用一共8 个 bug其中最后 3 个本质是同一个错误我创建 agent 时只传了最少字段然后一个个撞上 DSH 期望的输入。如果一开始就把agentLoop.create的契约读清楚后面三个都不会发生。还有两次误判病根“空回合抢跑”、完成判据选错都是我拿着不确定的理解直接动手改结果写了个修复却没修到点子上。两次都是靠真实数据推翻的—— 先是channel.log然后是那个多帧 zstd 的会话文件我第一次解压只读了第一帧误以为事件没落盘。真正让项目往前走的一直是同一个动作看数据别猜。几条我认为最值钱的经验测试替身比真实实现宽松—— 你写的假对象只实现你以为需要的东西。5 个 bug 因此漏网。判据本身也可能是错的—— 自检报replyChars0却 PASS就是判据错了。工具的源码是最好的文档——dsh plugin add会自动注册 bundle这件事写在operations.js第 503 行一个grep就能看到。打算绕过去的那一步后面往往藏着真问题—— 我因为没装 git写了个模拟 git 行为的检查脚本装上真实 git 后立刻发现两个问题CRLF 规范化、CLI 缺 shebang后者会让 Linux 用户直接跑不起来。最后关于版本DSH 还在0.2.0-rc阶段API 会变。本文标 的地方请务必自己重新验证一遍 —— 但 那些工程实践长轮询让出、日志落盘、持久化事件边界、启动自检无论版本怎么变都成立那才是这篇文章能长期有效的部分。附项目信息GitHubhttps://github.com/ranshaodexiao/dsh-wechat-ilinknpmhttps://www.npmjs.com/package/dsh-wechat-ilink安装dsh plugin --profile desktop add dsh-wechat-ilink适配 DSH 版本0.2.0-rc.2Cordis4.0.4、Node ≥ 22.13.0规模源码 16 个文件 / 3904 行测试 7 个文件 / 2318 行 / 138 个用例协议腾讯官方 iLink Bot APIMIT 参考实现tencent-weixin/openclaw-weixin不做企业微信、公众号、小程序、个人号协议 hook、公网穿透功能微信 ↔ DSH 会话双向对话文字 图片、微信远程接管 DSH 里正在干活的会话、启动自检、持久化日志。