跳到主要内容

dsh-im-gateway

已验证

@lijian-ui/dsh-im-gateway · v0.3.5 · MIT · Web 界面

Multi-channel IM gateway plugin for DeepSeek Harness (dsh): DingTalk / QQ / WeChat(iLink) / Feishu(Lark) / WeCom with QR-scan binding, streaming replies, and a unified ctx.imGateway service. 为 DeepSeek Harness 提供钉钉/QQ/个人微信/飞书/企业微信多 IM 通道接入。

安装

dsh plugin add @lijian-ui/dsh-im-gateway

用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。

源码

标签

作者

说明文档

@lijian-ui/dsh-im-gateway

English | 简体中文

为 DeepSeek Harness (dsh) 提供多 IM 通道接入的网关插件:钉钉 / QQ / 个人微信 / 飞书 / 企业微信,支持扫码绑定、流式回复、工具审批、交互提问、长文本分片、多段合并、双语界面。

npm version License: MIT


功能特性

通道与核心

  • 统一网关服务 — 一个插件、五个通道。所有通道都汇聚到单一的 ctx.imGateway 核心:会话管理、斜杠命令、流式回复、状态广播。
  • 钉钉 — 出站 WebSocket 长连接,群聊 + 单聊,@ 提及过滤,AI 卡片流式输出(实时增量回复),斜杠命令。
  • QQ — WebSocket 网关(官方 qqbot-nodejs SDK),私聊(c2c)+ 群聊,扫码绑定机器人(免去开放平台手动创建),流式消息(c2c)。
  • 个人微信(iLink) — 官方 iLink 长轮询协议,扫码登录 + 配对码,仅单聊,媒体(AES-128-ECB CDN)收发。
  • 飞书 / Lark — 官方 @larksuiteoapi/node-sdk 长连接(无需公网回调),扫码一键创建应用(权限与会话事件自动预填),私聊 + 群聊,CardKit 卡片流式输出,图片/文件收发。
  • 企业微信 — 官方智能机器人 WebSocket 长连接(无需公网回调、无需消息加解密),扫码一键创建机器人,私聊 + 群聊,原生流式气泡(replyStream 原位替换,官方原生能力),图片/文件/语音收发。
  • 多机器人实例 — 同一通道类型可配置多个实例(例如两个钉钉机器人),各自独立凭据。
  • 设置页 UI — 在官方 dsh web UI 内渲染完整的设置页(「IM 通道」),扫码绑定就在这里完成。
  • 渠道运维闭环 — 卡片上直接可见:状态点四态(连接中 / 已连接 / 未连接 / 异常)、掩码后的凭据标识、状态更新时刻、巡检结论;异常时把原因摊在卡片上。每个机器人都有 重连(停机原地重启,不改配置)和 测试(往它最近说过话的会话发一条测试消息,确认真能发出去)两个动作;二维码过期可一键 刷新二维码。有渠道处于「连接中」时状态轮询自动提速到 2 秒。
  • 连接监督器 — 每个渠道一个守护循环:连上之后每 15s 主动探一次活(不再"连上就再也不管"),断线按统一退避阶梯 250ms → 1s → 3s → 5s → 10s → 30s(带 ±20% 抖动)重连。退避等待中若渠道自己恢复了就取消本次重连;连续 5 次仍不健康则停手并提示手动介入,不做无限重启。有真实消息进出的渠道直接判健康、不打扰(IM 平台普遍只允许一条长连接,误判重启会自己踢自己)。
  • 流式回复 — 钉钉 AI 卡片、QQ stream_messages、飞书 CardKit 卡片、企业微信流式气泡;渠道不支持流式时自动回退纯文本。
  • 「正在输入」反馈 — 统一的 sendTyping 契约,由网关在 turn/start / turn/end 触发,渠道只实现平台原生那一层:微信 sendtyping(5s 心跳续期)、QQ sendInputNotify(仅 c2c,50s 续期)。钉钉 AI 卡片、企微流式气泡、飞书卡片本身就是占位反馈,不再额外实现。
  • 单例锁 — 通过 DSH_HOME 文件锁防止多个实例并发写坏 session log。

交互增强

  • 工具审批桥 — agent 调用需要审批的工具时,在 IM 里直接回复「批准」或「拒绝」即可放行/拦截,超时自动委托回 dsh 原生审批体系。
  • 交互提问桥 — agent 调用 ask_user_question 时,问题同步推送到 IM,用户回复选项编号/文字即可作答,与 Web 端竞速第一答生效。
  • 长回复分片 — 超过渠道单条上限的回复自动按句号/换行切分,带 (1/3) 分段前缀,逐条发送。
  • 多段输入合并 — 用户连续发多条消息时自动合并为一条(可配超时窗口);.. 续传合并、!! 立即提交。
  • 文件发送工具 — agent 可调用 im_send_file 工具把工作区文件(图片/视频/文档)直接发送到当前 IM 会话。
  • 双语界面 — 配置 language: 'zh' | 'en' 切换所有用户可见回复的语言。

会话管理

  • 内置斜杠命令 — /help、/model、/status、/new、/reset、/stop、/sessions、/session、/continue、/channels、/workspaces、/workspace、/compact(见下文)。
  • 会话列表与接管 — /sessions 列出最近会话(带序号 + 标题 + 完整 id),/session <序号> 或 /continue <id> 把本聊天接管到指定会话:正在运行的(如网页端开着)直接接管并与网页端共享同一份上下文,静息中的自动载入。
  • 上下文压缩 — /compact 压缩当前会话上下文(转发到官方命令注册表,与网页端手敲 /compact 等价)。
  • 会话标题带通道标识 — 来自 IM 的会话在 dsh 会话列表中自动显示为 [机器人名] 智能标题,一眼分辨会话来自哪个通道(机器人名取通道实例的 name,未填则回落到通道类型名)。标题沿用 dsh 官方生成的智能标题,只在前面加前缀;在 Web 端手动重命名后不再自动覆盖。
  • 工作区管理(两级粒度) — 「每个聊天」可在 IM 里用 /workspace <路径> 单独指定目录(落盘、跨 /new 保留),/workspace reset 恢复默认;「每个机器人」可在设置页直接指定默认工作区(内置目录浏览器,点选即可,不用手打 Windows 路径)。新建会话的目录按 聊天设置 → 机器人默认 → 全局 cwd 三级解析,所以新聊天、新群一进来就落在你指定的目录里。
  • 用户白名单 — 配置 allowAllUsers 或 allowedUserIds 精确控制谁可以使用机器人。

安装

需要 DeepSeek Harness (dsh)——本插件是标准 dsh bundle,通过官方插件通道安装。

版本兼容

dsh 0.2.0 起会在安装阶段直接拒绝 peer 版本不匹配的插件(判定在 @deepseek-ai/dsh-app-boot 的 evaluatePluginCompatibility)。本插件声明的 dsh peer 范围是 >=0.1.0 <0.3.0-0:

dsh 版本 状态
0.1.x(含 0.1.6-alpha.2、0.1.7-rc.*) ✅
0.2.x(含 0.2.0-rc.1) ✅
0.3.x 及以后 ❌ 需要插件跟一个版本

两个容易踩的点(写 peer 时务必注意):

  • 校验器只检查 @deepseek-ai/dsh 与 @deepseek-ai/dsh-* 开头的 peer —— @deepseek-ai/cordis(^4.0.0)、@deepseek-ai/schemastery(^3.18.0)不参与该校验,随便写。
  • 范围必须显式包含预发布段。判定用 semver.satisfies(runtime, range, { includePrerelease: true }),而 caret 展开 ^0.1.0 等于 >=0.1.0 <0.2.0-0 —— 上界带 -0 会把 0.2.0-rc.1 判成不满足。所以要么写 <0.3.0-0,要么直接写 ^0.2.0-rc.1(但后者会把插件锁死在单一版本线上)。

⚠️ 装得上 ≠ 跑得起来。 上面那道校验只看版本号字符串,不看代码。dsh 0.1.7 还改过一次插件设置接口,改法对新插件是静默的:

dsh 版本 设置接口 本插件如何适配
≤ 0.1.6 settings.installSection() 注册命名空间 调它(能力探测,有才调)
≥ 0.1.7 删掉 installSection,改成「Config 里标 volatile 的字段才可编辑」,命名空间名 = profile 条目 id Config.channels 标 volatile;命名空间按条目 id 解析

0.3.3 起同时兼容两代(详见 src/gateway/volatile-ref.ts 的说明);0.3.4 起设置注册再包一层兜底 —— 官方若再改设置接口,插件最多丢掉设置页表单,不会整个启用失败(控制台留一条 设置命名空间注册失败 告警)。

排查「启用失败」前先确认装的是哪个版本。 报错栈里的 lib/index.js:<行号> 能直接对上版本(9037 = 0.3.1/0.3.2,9151 = 0.3.3+),也可以直接打开 profile 里那份 package.json 看 version: <DSH_HOME>/profiles/<profile>/node_modules/@lijian-ui/dsh-im-gateway/package.json 升级后若报错原文一字不变,基本就是没装上(或桌面端没完全重启,还挂着上一次的模块)。

⚠️ 升级后必须「彻底退出宿主进程」再启动 —— 只重开窗口 / 热重载没用。 JS 会把加载失败的模块记在进程里(实测:文件已改成正确版本后,同一进程里再次加载,报的还是老错误、老行号)。所以插件升级、或修好了一个启动期报错之后,只要宿主进程没换过,就会一直重放老错误 —— 连行号都对得上旧版本。桌面端请用菜单退出(Quit)而非关窗口,并在任务管理器确认 dsh 相关进程已全部结束(必要时重启电脑)。

装完务必重启 dsh 并新建会话。

从 npm 安装(推荐)

dsh plugin --profile web add @lijian-ui/dsh-im-gateway

npm 包自带预构建的 lib/ — 无需构建授权(不需要 allowBuilds)。

从 tarball 安装

npm pack @lijian-ui/dsh-im-gateway
dsh plugin --profile web add ./dsh-im-gateway-0.1.1.tgz

从 GitHub 安装

dsh plugin --profile web add github:lijian-ui/dsh-im-gateway

Git 安装拉取的是源码,首次安装需要批准包的 prepare 构建脚本(pnpm ≥ 10)。按提示把包键加进 profile 的 pnpm-workspace.yaml → allowBuilds 即可。优先用 npm / tarball 方式可跳过此步。

验证安装

dsh --profile web --dump-config     # 应看到 "# == @lijian-ui/dsh-im-gateway" 配置层
dsh --profile web                   # 启动后浏览器打开设置 → 「IM 通道」

快速上手

  1. 打开 dsh web UI → 设置 → IM 通道。
  2. 点击添加通道。
  3. 选择通道类型:
    • QQ:点击扫码登录 → 手机 QQ 扫码 → 凭据自动填入 → 保存。
    • 个人微信:点击扫码登录 → 手机微信扫码 →(如要求则输入配对码)→ 凭据自动填入 → 保存。
    • 钉钉:点击扫码登录由钉钉自动创建机器人,或手动填写 AppKey / AppSecret → 保存。
    • 飞书 / Lark:点击扫码登录 → 飞书 App 扫码一键创建应用(权限与会话事件已预填),或手动填写 App ID / App Secret → 保存。
    • 企业微信:点击扫码登录 → 企业微信扫码一键创建智能机器人,或手动填写 Bot ID / Secret → 保存。
  4. 在 IM 客户端给机器人发消息 — 回复实时流式返回。

配置存储在 ~/.dsh/settings.yaml(im-gateway.channels)。在 UI 保存配置会热重载通道(无需重启)。


斜杠命令

在任何 IM 通道里发给机器人:

命令 说明
/help 列出可用命令
/model 用 emoji 编号列出模型;/model 1 或 /model <名称> 切换(无会话时 → 设为下次会话默认模型)
/status 通道 / cwd / 当前模型 / agent 状态
/new /reset /clear 开启全新会话
/stop 中止当前回复
/sessions 列出最近 10 个会话(带序号,标记当前会话,显示标题;每行下面给出完整可用的会话 id)
/session <序号> 按 /sessions 的序号接管会话(手机上不用敲 44 位 id)
/continue <会话id> 把本聊天接到指定会话:正在跑的(如网页端开着)直接接管并共享上下文,没在跑的自动载入(用 /sessions 查看 id)。等效于参考项目的 /bind,无需两个命令
/channels 列出所有 IM 通道及其实时连接状态(在线 / 连接中 / 离线 / 未启用 / 异常原因)
/compact 压缩当前会话上下文(历史过长时释放空间;需本环境下已启用官方 dsh-command-compact)
/workspaces 列出所有工作区(按最近活动排序,显示会话数;首行标出本聊天当前生效的目录)
/workspace <路径> 切换到指定工作区(重置当前会话,下次消息在新工作区创建新会话);相对路径以当前生效目录为基准
/workspace reset 清除本聊天的目录设置,恢复机器人默认(别名 /workspace default)
/workspace 不带参数时等同于 /workspaces

多段输入控制后缀

后缀 说明
(无) 进入合并窗口,等待后续消息(默认 3 秒超时后自动提交)
.. 续传合并:把本条加入缓冲,继续等待
!! 立即提交:把缓冲 + 本条合并后马上发给 agent

审批回复

当 agent 调用需要审批的工具时,直接回复:

回复 效果
批准 / 同意 / yes / y / allow 放行工具执行
拒绝 / no / n / reject / deny 拦截工具执行

超时后自动委托回 dsh 原生审批体系。


配置

所有配置都可在设置页编辑;底层 schema 在 ~/.dsh/settings.yaml:

im-gateway:
  language: zh                    # 界面语言:zh(中文)| en(英文)
  approvalTimeoutSecs: 120        # 工具审批超时(秒)
  questionTimeoutSecs: 600        # 交互提问超时(秒)
  mergeTimeoutSecs: 3             # 多段输入合并窗口(秒)
  allowAllUsers: false            # 全局放行所有用户(仅开发用)
  allowedUserIds:                 # 白名单:{ channelId: string[] } 或用 '*' 匹配任意渠道
    "*":
      - user-abc
  channels:
    - id: dingtalk-main
      type: dingtalk
      name: 主机器人
      enabled: true
      config:
        clientId: "..."
        clientSecret: "..."
        # 其余字段按通道类型取用:callbackBaseUrl / appId / botAppId / baseUrl /
        # botId / cdnBaseUrl / pollIntervalMs / domain(飞书)/ secret(企业微信)/
        # groupPolicy / groupAllowFrom / groups

网关级配置

字段 默认值 含义
language zh 界面语言(zh 中文 / en 英文),影响所有用户可见回复
streamThrottleMs 800 流式推送节流间隔(毫秒)
slashCommands true 是否启用斜杠命令
approvalTimeoutSecs 120 工具审批 IM 等待超时(秒),超时后委托回 dsh 原生审批
questionTimeoutSecs 600 交互提问 IM 等待超时(秒),超时后转回 Web 端
mergeTimeoutSecs 3 多段输入合并窗口(秒),用户连续发消息时合并为一条
allowAllUsers false 全局放行所有用户(仅开发用,生产环境勿开)
allowedUserIds {} 白名单;key 为 channelId(* 匹配任意),value 为用户 ID 数组

通道级配置

字段 适用渠道 含义
clientId / clientSecret dingtalk 钉钉应用 key / secret(Stream 模式)
appId / clientSecret qq QQ 开放平台凭据(扫码绑定所得)
token / botId / baseUrl / cdnBaseUrl weixin iLink 凭据(扫码绑定所得)
appId / clientSecret / domain feishu 飞书 / Lark 应用凭据(扫码创建所得);domain:feishu(中国版)或 lark(国际版)
botId / secret wecom 企业微信智能机器人 BotID + 长连接专用密钥(扫码创建所得)
groupPolicy / groupAllowFrom / groups qq · feishu · wecom 群消息策略:open 全响应 / allowlist 白名单 / disabled 不响应
enabled 全部 该实例是否连接

架构

IM 客户端 ──► 通道适配器 (dingtalk / qq / weixin / feishu / wecom)
                   │  ImInboundMessage
                   ▼
             ctx.imGateway(核心)
                   │  多段合并 → 白名单检查 → 审批/提问拦截 → 斜杠命令
                   │  ensureSession → agent.followup
                   ▼
            dsh harness agent(LLM 循环)
                   │  会话事件 (turn/start, assistant/message, tool/call, turn/end)
                   │  实时流   (agent/assistant-stream: text-delta 增量)
                   ▼
        EventDispatcher → 流式回复 / 分片 / 工具提示
                   │  (AI 卡片 / stream_messages / 纯文本回退)
                   ▼
                IM 客户端

模块结构

模块 职责
im-gateway.ts 核心服务 ImGatewayService:会话管理、消息路由、工具注册
events.ts EventDispatcher:SessionEvent → IM 渠道操作(流式、分片、工具提示、sendTyping 打字指示)
session-title.ts prefixSessionTitle:给 IM 会话标题加 [机器人名] 前缀(走官方 sessionTitle.rename)
commands.ts CommandHandler:斜杠命令处理(/help /reset /model /status /stop /sessions /session /continue /channels /workspaces /workspace /compact)
stream.ts StreamThrottle:流式节流器,攒批 text-delta 后按间隔推送
supervisor.ts ChannelSupervisor:连接监督器(M9)——统一退避阶梯 + 连上后的健康巡检,每渠道一个
approval.ts ApprovalBroker:工具审批桥,挂起 approval/request 等待 IM 回复
questions.ts QuestionBroker:交互提问桥,挂起 ask_user_question 等待 IM 回复
split.ts splitText:长文本分片,按句号/换行切分,带分段前缀
merge.ts SessionMerger:多段输入合并,支持 .. / !! 控制后缀
i18n.ts Translator:中英文双语翻译表
instance-lock.ts acquireInstanceLock:DSH_HOME 文件锁,防止并发写坏 session log
types.ts 接口定义:ImChannelAdapter、ImGatewayConfig、ImGateway 等
  • Host 半(node):src/index.ts(apply)、src/gateway/(核心 + 上述模块)、src/channels/(dingtalk / qq / weixin / feishu / wecom + 协议助手)、src/remote.ts(设置页的 Typert RPC)、src/sync.ts(保存配置后热重载通道)。
  • Client 半(浏览器):src/client/ — 设置页「IM 通道」(添加/编辑弹窗 + 扫码登录(含刷新二维码)+ 状态点四态 + 重连/测试 动作 + 卡片元信息(掩码凭据、状态更新时刻、巡检结论)+ 目录选择器 DirectoryBrowser.ts,复用官方 directoryPicker 远程能力)。
  • 多机器人:channels 是数组,同一 type 可多次出现。

扩展点

第三方可以不 fork 直接注册自己的通道:

import { ImChannelAdapter } from '@lijian-ui/dsh-im-gateway'   // peerDependency 引用核心

class MyChannelAdapter implements ImChannelAdapter { /* ... */ }
ctx.imGateway.registerChannel(myAdapter)

ImChannelAdapter 接口可选方法:

方法 说明
sendText(convId, text) 必需。发送纯文本消息
sendMedia(convId, filePath, caption?) 可选。发送文件/图片/视频(im_send_file 工具使用)
beginStream(convId) 可选。开启流式回复(首个文本增量时调用)
streamText(convId, text) 可选。流式覆盖更新(节流推送)
endStream(convId, fullText) 可选。结束流式回复(turn/end 时调用)
sendTyping(convId, active) 可选。平台原生「正在输入」:turn/start 传 true、turn/end 传 false,由网关统一触发,渠道只需实现平台那一层
probeHealth() 可选。主动健康探活:连接监督器每 15s 调一次,返回 false(或抛错)即判不健康并安排重连。必须免副作用(只读自己的 socket 状态 / 打一个便宜的只读接口);没有可靠手段就不要实现 —— 伪探活会把健康的长连接判成不健康然后重启踢掉它
updateCard(convId, text) 可选。遗留单次卡片更新
authorizes(userId) 可选。渠道本地授权检查(返回 false 拦截)
maxMessageChars 可选。单条消息字符上限(默认 4000,用于分片)
label 可选。渠道显示名称(用于提问回执)

开发

git clone https://github.com/lijian-ui/dsh-im-gateway.git
cd dsh-im-gateway
npm install
npm run build          # tsdown → lib/
npm run watch          # 保存自动重编译
npm run typecheck
npm test               # node --test tests/*.test.mjs

本地 link 进 dsh profile:

dsh plugin --profile web add ./   # 从本目录安装(link)

Windows 注意:dsh 子进程从 package.json 的 main 加载 lib/index.js — 修改 src/ 后必须 npm run build 再重启 dsh 进程(它的 require 缓存会保留旧模块)。

测试

测试使用 Node.js 内置测试运行器(node:test),位于 tests/ 目录:

测试文件 覆盖模块 测试数
approval.test.mjs ApprovalBroker 8
questions.test.mjs QuestionBroker + parseQuestionReply + formatQuestionPrompt 8
split.test.mjs splitText 8
merge.test.mjs SessionMerger + stripControlSuffix 9

常见问题

  • 插件没有任何日志 — cordis 默认把 ctx.logger.* 缓存进内存。本插件在 apply 时注册了 console exporter,日志会出现在 dsh 子进程 stderr(桌面壳会加 [dsh] 前缀)。
  • QQ 客户端一直显示「连接中」 — 流式开得太早或没收干净。本插件在第一个文本增量时才开流,并在 turn/end 无条件收流(0.1.x 已修复)。
  • 能对话但不流式 — 渠道回退到了纯文本(例如 QQ 群聊不支持 stream_messages;微信本身没有流式概念)。这是设计行为。
  • 没看到「正在输入」 — 分渠道:微信、QQ 单聊有原生输入态(QQ 仅 c2c,群聊平台不支持);钉钉/企微/飞书的反馈是「占位卡片/气泡」而非输入态提示(平台没有输入态 API)。整条链是 best-effort,平台拒绝时只记一行 warn、不影响回复。
  • 卡片显示「巡检异常」 — 连接监督器探活没通过(不是"没连上"):socket 还在,但对端已经不好使了(凭据被停用/重置、token 失效)。它会按退避阶梯自动重连,连续 5 次不成就停手并打一条 warn(已停止自动重连,请手动重连或检查凭据),此时用卡片上的 重连 按钮或检查凭据。日志里能看到具体原因。
  • 日志里出现「在退避等待期间自行恢复,取消本次重连」 — 正常现象:打算重连之前渠道自己好了,监督器复查后主动收手,不会去踢一个健康的连接。
  • 掉线后要等一会儿才自动重连 — 故意如此。监督器要求连续 3 轮(约 45s)确认不健康才动手,为的是不和渠道自身/SDK 的重连抢方向盘。想立刻恢复就用卡片上的 重连 按钮(手动重连会把退避阶梯复位,下次掉线从 250ms 快速重来)。
  • QQ / 企微 没有探活 — 这两家的 SDK 不暴露存活查询、也没有免副作用的只读接口,所以没有实现 probeHealth。它们的巡检退化为「状态停滞 + 真实流量」判定:socket 静默失效但仍报"已连接"时,只能靠下一次发消息失败暴露。这是有意的取舍 —— 伪探活会误踢健康的长连接。
  • 回复被截断成多条 — 超过渠道 maxMessageChars 上限时自动分片,带 (1/3) 前缀。这是设计行为,不是 bug。
  • 多段消息被合并了 — 默认 3 秒合并窗口内连续发的消息会合并为一条。发 !! 立即提交,或调大 mergeTimeoutSecs。
  • 审批/提问超时了 — 调大 approvalTimeoutSecs / questionTimeoutSecs。超时后会自动委托回 Web 端。
  • 切换英文后部分文本仍是中文 — formatAnswerSummary 中的分隔符(、 ;)和 (空) 目前固定中文,因为它们是格式符号而非自然语言。

许可

MIT © lijian-ui

为 DeepSeek Harness 构建 — 独立插件,与 DeepSeek 无隶属或背书关系。