DSH Plugins Marketplace

DSH Plugins

Plugins

/

Tools & Capabilities

/

dsh-onebot-channel

d

dsh-onebot-channel

Manifest valid

Connects QQ private chats and groups to DSH over OneBot 11, adding qq_send and other tools so the agent can reply, send stickers and wait for follow-ups.

UI (client)hasBundlePatch

dsh-onebot-channel

把 QQ 私聊与群聊接进 DeepSeek Harness 会话。 基于 OneBot 11 协议:入站走 WebSocket 事件推送,出站用 qq_send 工具回话。

npm license node

让「在 QQ 里发消息」变成 DSH 会话里的一个回合,agent 直接回话。

QQ 客户端
   ↓  OneBot 11 协议端(SnowLuma / LLOneBot 等)
   ↓  本插件:WS 事件推送为主,HTTP 轮询兜底
路由表:这个 QQ 号 / 群绑的是哪个会话
   ↓  agent.followup()  唤醒会话
会话跑一个回合(agent 看到「(2026-11-01 12:09)(QQ 私聊·用户本人 QQ …)…」)
   ↓  agent 调 qq_send 工具
OneBot send_private_msg / send_group_msg
   ↓  按换行切条(她自己写几行就几条),带随机延时
回到 QQ

安装

dsh plugin --profile desktop add dsh-onebot-channel

装完去 设置 → QQ 桥接 填协议端地址和 token,把 QQ 号 / 群绑到会话上。

配置存在 ~/.dsh/qq-bridge.config.json,不在插件目录里 —— 重装插件不会丢。

前置条件:需要先跑一个 OneBot 11 协议端,并把插件的 onebotHttp / token 指过去。 插件不读你的 QQ 密码,只跟协议端说话。

工具

装好之后 agent 有 10 个工具:

工具干什么参数
qq_send发消息、发表情;想自己决定分几条就传 segments;想明确引用某条就传 reply_to;想 @ 人就传 attext segments emoji custom_emoji user_id group_id reply_to at
qq_emoji_search搜 QQ 官方表情目录query all
qq_msg_history查群 / 好友历史消息(返回的图能看见,0.12.0)group_id user_id count before_message_id
qq_collect_face把收到的表情包存进 QQ 收藏夹index recent file desc dir list delete
qq_face_preview看收到的表情包(结果直接带图片)index from count
qq_qzone_post发表 QQ 空间说说(必须带 confirm: true)content visibility target_uins confirm
qq_wait等他把话说完,返回这期间新到的消息(默认 8s,上限 60s)ms user_id group_id
qq_pass划走:这一回合不开口(正文里已经写了真话的,仍然会发出去 —— 写了就算说出口)why user_id group_id
qq_close收摊:双方都收尾时结束会话,记下聊到哪儿、下次怎么接summary next user_id group_id
qq_remember记住关于他的一件事 / 一个称呼,下次还认得fact nick user_id group_id
// 发消息 + 经典小表情
{ "text": "搞定了", "emoji": "OK" }

// 发收藏夹里的商城大表情
{ "text": "看这个", "custom_emoji": "猫" }

// 查官方表情目录
{ "query": "比心" }        // → 比心(编号 319)
{ "all": true }            // → 列出全部目录(398 个,10 个包)

// 查历史消息(默认 20 条,上限 100)
{ "group_id": "1046436553", "count": 20 }

// 看表情包 —— 文件名是 MD5 哈希,只有看图才起得了名
{ "index": 1 }

// 收藏表情包
{ "index": 1, "desc": "猫脑过载" }

// 发说说(对外发布)
{ "content": "今天天气不错", "visibility": 1 }

说说可见性:1 所有人 · 4 仅好友 · 16 仅自己 · 64 指定好友 · 128 分组。

你不一定非要回话(沉默权)

QQ 桥接和真人聊天,最不像的地方是「真人会不回、会晾着、会说完了就走」。0.7.1 给模型补上了这三种不说话的出口,让对话有节奏、不粘人:

场景怎么做工具
对方明显还没说完(「我跟你说」「等下」「还有」)等一会儿,看下文再说qq_wait
没话说 / 不想接 / 话题翻篇了划走 —— 这一回合什么都不发qq_pass
双方都收尾了(嗯 / 好 / 哈哈 / 晚安),再补一句就多余收摊,记下聊到哪儿、下次怎么接qq_close

划走 ≠ 删掉刚写的话(0.9.3 修正):模型经常「先写一句正文 → 再觉得太催了 → 调 qq_pass」, 早先的实现会把正文一起吞掉,于是会话里她明明说了一句话、QQ 上却什么都没有(用户报的「她发消息了我没收到」)。 现在:正文里有真话就照发,只有纯动作(「(尾巴摆了摆)」)或纯标点时 qq_pass 才真的按下不发。

qq_close 有硬约束:如果对方还有提问没回答,桥接会拒绝收摊,逼模型先把话接完再走。qq_close 写下的「下次怎么接」会在下次开场时自动带回,避免冷启动尬聊。

更细的判断逻辑(收尾评分、节奏底座、自动静默)见 docs/QQ对话说明.md。

@ 人(0.14.0)

群里回话时点名叫人。跟引用一样,@ 只挂在第一条消息上 —— 分条发送时后面几条不带 @, 否则同一个人会被连点 N 次,群里看到的就是刷屏。

场景行为
群里有人 @ 她自动 @ 回那个人(引用 + @ 一起带)
群里没 @ 她(其他触发)不 @
模型想 @ 别人传 qq_send 的 at 参数(QQ 号)
私聊at 参数被忽略(私聊没有 @ 这个概念)

段顺序实测(SnowLuma 2.2.1,回读确认服务端原样保留):

reply → at → text

也就是先引用、再 @、最后正文,跟 QQ 客户端自己的排列一致。

不 @ 自己:at 填本机号会被忽略并记日志 —— 那是无意义动作,还容易写错成「@ 机器人自己」。

为什么不给 @ 单独做白名单:这条消息本来就只能发到已放行的群,群里所有人都看得见它; @ 只是多一次提醒,不构成新的越权面。真正的越权面在「发给谁」,那道闸由 allowlist.js 管。

引用回复(0.13.0)

QQ 的引用是逐条消息的属性,所以插件按这个规则来:

  • 默认自动引用「触发这次回复的那条」——被 @ 的那句,或私聊里最新那句。
  • 模型想明确引用别的,就传 reply_to(填正文里 [#数字] 的那个数字)。
  • 分条发送时只引用第一条,后面几条是裸消息 —— 跟 QQ 客户端原生行为一致, 也避免刷屏。

正文里的锚点长这样(每条消息一个,紧跟在时间戳后面):

(2026-10-31 20:15)[#-2123597213](QQ群 123456·群友「张三」(QQ 789,不是用户本人))帮我看看这个

群上下文里如果某条消息引用了别人,会再标一个箭头:

[#114105673]群友「李四」: 把个人战改一改就是狼人杀 → #1519605856

→ #1519605856 表示李四这句话是引用 1519605856 那条说的。

为什么用 [#id] 而不是整句说明:这段文字每回合都进上下文, 写长了就是纯烧 token(当年「表情标记说明」被重复 92 次的教训)。 所以只放一个极短标记,含义在 qq_send 的工具描述和 docs/QQ对话说明.md 里各说一次。

装饰段失败不会吞掉回复:引用的那条可能已被撤回,@ 的号可能已退群, 表情 id 可能协议端不认 —— 任何一种都可能让整条消息发送失败。

出站按「装饰性从强到弱」逐级剥离,每一级只去掉一种段再试一次:

reply + at + text   ← 原样
      at + text     ← 去掉 @
           text     ← 再去掉引用

(表情也在这条链上,排在最后。)每一级都记日志。 宁可少一个 @ 或引用,也不能让回复凭空消失 —— 这是「表情幽灵 id」那次踩出来的原则。

⚠️ 踩过的坑:最初把三级降级写成串行的 if,判断条件读的是已经剥过的变量, 结果 @ 和引用在同一次重试里被一起去掉 —— 既试不出到底是哪种段把消息搞挂的, 也白白丢了本来能用的引用。现在按原始段逐级剥离,tests/at-sim.mjs 钉住了这个行为。

分几条,也由她定

不传 segments 时按句末标点自动切(老行为);传了就按她给的分段原样发 —— 真人会自己决定「这句分三条还是一条说完」,而不是被标点机械切开。

// 一句话拆成三条,逐条发出、中间带停顿
{ "segments": ["我跟你说", "那个事搞定了", "不过还差一点"] }

传 segments 时 text 被忽略;空字符串段自动丢掉;段内的 [表情:呲牙] 挂在该段末尾。

她记得你是谁(关系持久化)

称呼和「关于他的几条事实」落在 ~/.dsh/qq-bridge-memory.json,重启不丢:入站自动记昵称和活跃度,模型也能用 qq_remember 主动记(「他喜欢猫」「别叫他全名」)。下次被叫醒时,这些会以一小段 (关于他:称呼:…;记得:…) 带进正文,所以她不会每次都从零认识你。

一张卡最多 8 条事实、注入正文约 180 字上限 —— 它是每回合常驻的,不控长度就是烧 token。不想要这段就设 memoryEnabled: false。

先记账,再决定什么时候回(标记已读的中间态)

原来 WS 通路是「来一条投一条」:对方连发三句,就开三个回合、被连问三遍。 replyQuietMs 治不了这个 —— 它只是把每一个回合各推迟一遍,三条消息仍然是三个回合。

现在私聊多了一层合并窗口(mergeWindowMs,默认 2500,设 0 关闭):

来信 → 立刻记账(markSeen + 游标推进,等于「已读」)→ 入池攒着
     → 窗口到点(或攒够 mergeMaxMessages / 到达 mergeMaxWaitMs)→ 一批放行 → 一个回合
  • 「已读」指的是内部记账,不是推给 QQ 的回执(OneBot 没有已读回执接口)。 它的作用是不让轮询把同一条再拎一遍,同时保证「攒着没回」这件事在状态里是明确的。
  • 投递失败会把记账撤回去(mergeSnapshots),轮询或下一批还能重试 —— 不是静默丢消息。 唯一的代价:进程在窗口内被强杀(不是正常卸载)时,那一两条「已读」记账已经落盘,会丢掉。 窗口只有 2.5 秒,正常卸载会记日志并保留痕迹。
  • 只作用于私聊。群里是 @ 才投递,一条 @ 就是一件事,攒起来反而怪。
  • 跟 replyQuietMs 会叠加(2.5s 窗口 + 5s 静默 = 7.5s),两个都想开时建议只留一个。

没人叫她,她自己想起来找你(主动搭话)

默认关(proactiveEnabled)。打开后每 60 秒看一次该不该开口, 真正开口还要过四道闸:

闸默认配置
静默时段23:00–08:00 不打扰proactiveQuietFrom / proactiveQuietTo
空闲够久6 小时proactiveIdleHours
当日配额1 次proactiveDailyMax
没被回应的退避2ⁿ 倍,封顶 4 倍proactiveBackoffMax

配额按自然日算:跨过零点,昨天的次数不再计入,设置页的「今天几次」也随之归零(0.11.1 修;之前的版本只更新日期不清计数,数字会一直涨)。

「空闲够久」的起算点是上次来信和上次主动里较晚的那个 —— 她自己刚说完一句,也要重新等满这个时长。 之前只按「你上次说话」算,于是静默时段一结束就一分钟一条,几分钟把当日额度烧光(已修,并补了回归用例)。

关键点:这一层不自己写文案。它只是往同一个会话里投一条内部来信:

(主动搭话:距上次说话已经 7 小时。上次聊到「…」,可以从「…」接着聊。
想说什么就说……但这事你自己定:没什么想说的就 qq_pass,不用硬找话。)

于是「说」还是「不说」仍然由模型判断,转发 / 分条 / 抑制标记全部复用原链路。 硬拼一句「在吗」发出去,就退化成定时问候机器人了 —— 这是这条设计的红线。

状态落在 ~/.dsh/qq-bridge-proactive.json(重启不丢,否则每次重启都重新「攒够 6 小时」或把日配额清零)。 设置页有「现在试一次」,跳过所有闸立刻投一次(开关关着也能试)。

对谁开口:proactiveTargets 是白名单,元素形如 private:1000000001。

  • 空数组 = 所有已绑会话的私聊(默认,也是升级上来的老行为);
  • 非空 = 只对名单里的人算,名单外的连「该不该说」都不判;
  • 只认私聊,群里不主动(candidates() 只取 routes.private,且必须有 sessionId)。

设置页里是「所有已绑私聊 / 只对勾选的人」两选一 + 每人一行勾选框。从「所有人」取消一个人会自动展开成显式名单 —— 因为空数组的语义是「全员」,直接把「取消某个人」写成空数组会得到完全相反的结果。 每行还有「对TA试一次」(POST /proactive-now {only}),可以只对某个人手动开一次口,不用来回改名单。

手动触发会轮转:候选按「最久没被主动过」排序(从没主动过的排最前), 所以连点「现在试一次」会依次换人,不会三次都砸在同一个人身上(0.9.2 修的就是这个: 之前固定打候选列表第一个,另一个人永远轮不到)。按钮旁边会写明这次轮到谁。

只回动作 = 沉默:主动搭话回合里,如果她只回了「(尾巴摆了摆)」这类纯动作描写, 转发链会把它当沉默丢掉(日志记一笔),不会真的发到他手机上 —— 普通聊天里「(笑)好呀」这种带真话的照常发。

能力

能力说明
私聊 / 群聊投递每个号各绑一个会话,没绑定的号一律忽略(不会乱投到别的会话)
WebSocket 实时推送延迟接近 0;断线自动退回轮询补漏
区分本人与群友逐条打标「用户本人」/「群友(不是用户本人)」,agent 不会张冠李戴
群聊带上下文被 @ 时附带上次投递之后的未读群消息
发表情正文写 [表情:呲牙],或 qq_send 带 emoji
商城大表情qq_send 带 custom_emoji(需先收藏进账号收藏夹)
图片理解QQ 图片下载后进 DSH 附件库,作为 image 块投进会话 —— 模型真能看见图。三条路都通:入站投递、群 @ 的上下文、qq_msg_history 翻历史(后两条 0.12.0 补,该工具目前默认关闭)
正文带时间每条投进去的消息前面带「(2026-11-01 12:09)」,取的是消息自身的时间(不是轮询到的时刻),隔了几小时才看到也判断得出来
分条由她决定正文换行 = 另起一条(splitMode: paragraph,0.10.0 默认);想回「一句一条」切成 sentence。带随机延时,模型也能用 segments 直接指定
连发合并私聊连发几条先攒一会儿再一起回(mergeWindowMs,0 = 关),不再被连问三遍
主动搭话没人叫她时自己想起来找你(默认关,四道闸 + 退避;可选只对指定的人开口;设置页可「现在试一次」/「对TA试一次」;「今天几次」按自然日,跨天自动清零)
设置页 UI设置 → QQ 桥接:顶部一排功能分区(概览 / 绑定 / 对话节奏 / 主动搭话 / 关系记忆 / 推送 / 高级),点进去只看那一块

设置页能调什么

顶上一排是功能分区,一次只显示一块;「保存」和「重新载入」始终在右上角。

分区内容
概览连接状态 + 统计块(私聊/群绑定、今天主动几次、记住几个人、待合并条数、推送是否连通),以及「现在在跑什么」的自然语言摘要;数字都可点进对应分区
绑定私聊 / 群聊各自一行:QQ 号、名字、绑定会话、正文前缀
对话节奏replyQuietMs 回复前静默、mergeWindowMs 合并窗口(+ 上限 / 条数)、waitMaxMs 等待上限、closureEnabled 允许判断收尾、autoForwardReply 自动转发、backfillOnStart 上线补历史(+ 页数 / 投递条数)、splitMode 怎么分条:按换行(她自己决定说几条)/ 按句末标点(一句一条)
主动搭话上卡:proactiveEnabled 开关、proactiveIdleHours 空闲门槛、proactiveDailyMax 日上限、静默时段、退避上限。下卡「对谁开口」:所有人 / 只对勾选的人 + 每人上次主动时间与「为什么现在还不说」
关系记忆memoryEnabled 开关、记住了谁(称呼 / 事实条数 / 说过几句)、一键清空
配置两条连接链路同页:HTTP API(onebotHttp 地址 + token,留空 = 保持原样,自动发现读 httpServers)、WebSocket 推送(wsEnabled、WS 地址与 token,自动发现读 wsServers)。两套 token 相互独立,混用会 401
高级分条(字数 / 条数 / 碎片阈值 / 间隔)、轮询间隔、补漏间隔、正文前缀、图片副本目录、只在 @ 时响应、冷启动会话、说说必须带 confirm

概览里「现在在跑什么」的第一行是这版代码的版本号 + 进程 pid + 启动时间(0.10.1 补)。 改完代码发现行为没变,先看一眼这行:版本没涨、启动时间还是上午 —— 说明主进程仍在跑旧代码,重启即可,不用怀疑改错了。 第二行「连接」会同时列出 HTTP 地址与 WS 连接状态(0.15.0 补),排查「消息收不到」时先看它。

不给控件的:token / wsToken(凭据,前端不回显;留空即保持原样)、fallbackToLiveSession(会绕过路由表)、 allowSendToAny(安全逃生阀)、enabled(只在加载时读一次,放按钮会误导成即时生效 —— 页面只在它为 false 时显示告警)。 这批「故意不给控件」的字段在 tests/ui-wiring.mjs 里有白名单和理由,新加配置忘了接 UI 会直接测试失败。

不接:群信息检索、撤回消息、表情回应、发图片到空间。

发表情

两条路都通:

① 正文里写标记(推荐,一句话能带多个)

搞定了[表情:OK][表情:庆祝]

→ QQ 上收到的是 搞定了 + 两个真表情(标记本身不出现)。支持中文名和编号, 表情附在它前面那句话的后面。这条路不依赖工具调用,自动转发也生效。

② qq_send 的参数

{ "text": "搞定了", "emoji": "OK" }        // 经典小表情
{ "text": "看这个", "custom_emoji": "猫" } // 商城大表情

区别:经典小表情能跟文本拼在同一条消息里;商城大表情协议端只能单独发一条。

⚠️ 收藏表情走的是协议端扩展 action(SnowLuma 的 send_custom_face),不是 OneBot 11 标准 —— 换协议端(NapCat / LLOneBot)可能没有,届时经典表情照常能用。

表情编号的坑

内置表情表共 275 个(编号 0~348),但它是 OneBot 生态的社区通用编号,不是 QQ 官方编号, 两者对不上(2026-10-27 实测):

  • 275 条里有 109 条在 QQ 官方目录(398 个)里查无此 id —— 「幽灵 id」
  • 另有 11 条名字对得上但 id 错位(「晕」内置记 17、官方实际是 34)

拿幽灵 id 去发,协议端报 OIDB error 11000: id N sysemojis cfg not found, 整条消息发不出去 —— 不是丢个表情,是连文字一起没。

所以出站做了三层防护:查 QQ 官方目录解析 → 校验 id 真的存在 → 校验不过就跳过表情、文字照发 → 发送层再兜一层(带表情失败则去掉表情重发文字)。

宁可少发一个表情,也不能让整条回复凭空消失。

配置

~/.dsh/qq-bridge.config.json(UTF-8 无 BOM)。改完不用重启,下一轮轮询就生效。

{
  "enabled": true,

  // ---- 连接 ----
  "onebotHttp": "http://127.0.0.1:3000/",   // 协议端 HTTP 端口
  "token": "…",                              // config/onebot_<QQ号>.json 里的 httpServers.accessToken
  "selfUin": 1000000002,                     // 机器人自己的 QQ 号

  // ---- WebSocket(主通路)----
  "wsEnabled": true,                         // 关掉就退回纯 HTTP 轮询
  "wsUrl": "ws://127.0.0.1:3001",            // 留空 = 自动去协议端配置里发现
  "wsToken": "…",                            // ⚠️ 与上面的 http token 是两套,不能混用
  "wsFallbackPollMs": 30000,                 // WS 通着时,轮询降频到多少毫秒一次

  // ---- 私聊 ----
  "peerUin": 1000000001,                     // 监听的好友 QQ 号
  "ownerUin": 1000000001,                    // 用户本人(留 0 = 用 peerUin)。群聊靠它区分本人和群友
  "pollMs": 2000,
  "textPrefix": "(QQ 私聊)",

  // ---- 群聊 ----
  "groupUins": [],                           // 监听哪些群;设置页绑群时自动同步
  "groupOnlyWhenAt": true,                   // 只在 @ 本号时投递
  "groupContextCount": 5,                    // 被 @ 时带几条未读群消息
  "groupPollMs": 3000,

  // ---- 路由表(核心)----
  "routes": {
    "private": { "1000000001": { "sessionId": "…" } },
    "group":   { "2000000002": { "sessionId": "…" } }
  },

  // ---- 会话 ----
  "workspaceCwd": "D:\\DSH workspace\\QQ对话",
  "resumeColdSession": true,                   // 会话没开着时冷启动它
  "fallbackToLiveSession": false,               // 首选会话没 agent 时改投别的会话
                                                 // ⚠️ 这会绕过路由表,默认关着
  "agentOptions": { "provider": "deepseek-account", "model": "deepseek-flash" },

  // ---- 分条发送 ----
  "splitReply": true,
  "splitMode": "paragraph",            // paragraph = 换行分条(默认)/ sentence = 按句末标点
  "splitMaxChars": 120,
  "splitMinChars": 4,
  "splitMaxParts": 8,
  "splitDelayMinMs": 200,
  "splitDelayMaxMs": 700,

  "requestTimeoutMs": 8000                    // 协议端挂死时的超时
}

没有 routes 时按旧逻辑走(私聊用 sessionId、群聊用 groupSessionId)。 新配置建议只用 routes。

安全边界

13 个高风险 action 物理拦截 —— 拦截点在 callOneBot(),所有协议端调用的唯一出口。 不是「提示词里写别调用」,而是调用即抛错、连 HTTP 都不发。

类别action
交出账号凭证get_cookies get_credentials get_csrf_token get_clientkey get_rkey_server request_decrypt_key
踢下线 / 改状态bot_exit set_online_status set_diy_online_status set_input_status
越权社交动作send_like set_qzone_ban
删说说delete_qzone_msg(发说说允许,删不允许)

清单在 lib/blocked.js,要放开哪项改那个集合 —— 但凭据那六项别动。

add_custom_face 原先在封禁清单里,2026-10-27 放开了: 协议端没有「收到图片自动收藏」的逻辑,收藏夹不显式写就是空的, custom_emoji 那条路永远走不通。现在改成走受控工具 qq_collect_face —— 只允许收藏插件自己收到过的图。

出站两道闸门(2026-10-28) —— 都挂在 callOneBot() 这个唯一出口上,绕不过去:

闸门管什么实现拦住了什么
目标白名单发给谁lib/allowlist.jsqq_send 的 user_id/group_id 是 agent 随便填的 → 群里一句 prompt injection 就能让她往任意 QQ 号发消息
内容审计发什么lib/sensitive.js回复里顺嘴写出 C:\Users\...、token=xxx → 本机路径和凭据直接进群

白名单从已有的配置派生,不新增一份(避免两处漂移): 私聊 = peerUin ∪ ownerUin ∪ routes.private 的键;群 = groupUins ∪ routes.group 的键。 自动转发的目标全部来自路由表,所以永远不会被误伤。

  • 想临时放开:allowSendToAny: true(⚠️ 等于把上面那个风险还回去,只在排障时开)。
  • 白名单全空时(还没配 peerUin)不拦,只记一条警告 —— 闸门的语义是「限制已配置的服务范围」,不是「没配好就把插件锁死」。
  • 内容审计故意不匹配裸的 32 位十六进制串:表情包文件名就是 MD5, 加了这条会把正常表情和消息整条拦掉。详见 lib/sensitive.js 的注释。

发说说确认闸 —— qq_qzone_post 必须传 confirm: true 才真的发。 说说是对外发布,而 delete_qzone_msg 在封禁清单里(发得出去、删不掉), 所以这道闸不是防恶意,是逼 agent 在发之前把「这是对外发布」过一遍脑子。 关掉用 qzoneRequireConfirm: false(不推荐)。

HTTP 接口

插件自带六个同源接口(设置页用的也是它们):

方法路径作用
GET/api/qq-bridge/state配置(不含 token)+ 路由表 + 会话列表 + 好友/群列表 + 机器人信息
POST/api/qq-bridge/save保存 routes 与高级设置(含 onebotHttp / token,留空不覆盖)
POST/api/qq-bridge/detect-ws自动发现协议端的 WS 接入点
POST/api/qq-bridge/detect-http自动发现协议端的 HTTP 接入点(读 httpServers,0.15.0)
POST/api/qq-bridge/memory-clear清空整张关系卡表(不可撤销,设置页里有二次确认)
POST/api/qq-bridge/proactive-now跳过闸门立刻试一次主动搭话;{ "only": "private:1000000001" } 可指定对象,不传按轮转选人

另有一个独立的主动发消息接口(端口默认 39273),给 agent 之外的程序用:

curl -X POST http://127.0.0.1:39273/send \
     -H "Content-Type: application/json" \
     -d '{"text":"内容","user_id":"对方QQ号"}'

健康检查:curl http://127.0.0.1:39273/health

排障

~/.dsh/qq-bridge-load.log      插件日志(超 2 MB 自动轮转)
~/.dsh/qq-bridge-state.json    游标
~/.dsh/qq-bridge.config.json   配置
grep "\[config\]" ~/.dsh/qq-bridge-load.log | tail -1   # owner= 有没有 → 新版加载了吗
grep "\[poll\]"   ~/.dsh/qq-bridge-load.log | tail -3   # 群在监听吗
grep "\[group\]"  ~/.dsh/qq-bridge-load.log | tail -5   # skip (no at)? 未配置会话? seeded?
grep "error"      ~/.dsh/qq-bridge-load.log | tail -5   # fetch failed = 协议端挂了

收不到消息:① 协议端在跑吗 ② selfUin / peerUin 对吗 ③ 这个号在 routes.private 里吗 ④ 会话开着吗(没开靠 resumeColdSession 冷启动)

撞到白名单 —— 工具报「出站被白名单拦截:好友 xxx 不在允许范围内」:

grep "\[allow\]" ~/.dsh/qq-bridge-load.log | tail -3   # 白名单为空时会有警告

这是硬闸,不是 bug:qq_send 只能发给「插件正在服务的对象」 (routes 里的好友/群 + peerUin/ownerUin + groupUins)。 要么把目标加进设置页的会话绑定,要么临时 allowSendToAny: true(不推荐)。

日志里写着「收到消息事件」但会话里没动静 —— 看下一行是不是「重复消息(已处理),跳过」。 这是去重误杀:旧版本拿私聊的 message_seq 判新旧,但私聊的 seq 是两套编号混着的 (自己发的 71xx 连续、对方发的 50xxx/20xxx 跳号且不单调),会把新消息当成旧消息丢掉。 0.6.1 起私聊改用时间戳判新旧(群聊仍用 seq),这个问题不会再有。

grep "收到消息事件" ~/.dsh/qq-bridge-load.log | tail -3   # 消息进来了吗
grep "重复消息"     ~/.dsh/qq-bridge-load.log | tail -3   # 被去重误杀了吗

群里没反应:① 监听列表里有这个群吗(日志 [poll] group loop started, watching [...]) ② groupOnlyWhenAt 开着的话必须先 @ ③ 首次启动会 seed,启动前发的消息不会被回复,再 @ 一次

同一条回复连着发了好几遍:日志里搜「已转发过,跳过重复发送」。 这是「没等她发完就发下一条」触发的 —— agent 跑回合期间 session.seq 不推进, 两条消息拿到同一个游标 → 认到同一个回合 → 同一条回复发两遍。0.6.2 起按 「属于我自己的那条 user/message」定位回合,同一个回合只发一次。

重复收到同一条:删掉 qq-bridge-state.json,游标归零后会重新对齐(不会重放)。

设置页找不到「QQ 桥接」入口

服务端 API 和工具都正常、但设置页没入口 —— 通常是插件没进 boot graph, 浏览器侧拿不到 client bundle。

# DSH 的 web 端口(Electron,端口不固定)
Get-NetTCPConnection -State Listen | Where-Object { $pids -contains $_.OwningProcess }

# 拉 boot manifest —— 这条 SSE 直接吐出完整的插件清单
curl -s http://127.0.0.1:<port>/plugins/events | head -1
# → data: {"type":"graph","graph":{"entries":[{"id":"...",...}]}}

entries 里没有自己的插件 id = 没进 graph。常见原因是 package.json 的 name 跟插件的安装目录名不一致(DSH 按目录名找包,并校验 name 相等)。

开发

npm test                              # 跑全部离线自测
node tests/ui-wiring.mjs              # 设置页 ↔ 配置项一致性(死配置 / 漏接 UI / 过时文案)
node tests/merge-sim.mjs              # 合并窗口(窗口内合并 / 上限截断 / 失败回滚接线)
node tests/proactive-sim.mjs          # 主动搭话的四道闸(静默 / 空闲 / 配额 / 退避)+ 白名单选人
node tests/client-render.mjs          # 设置页真渲染(每个分区都过一遍,含选人名单)
node tests/pass-forward-sim.mjs       # qq_pass 但有真话 → 照发(沙箱 HOME,不碰真实 ~/.dsh)
node tests/sync-sim.mjs               # 检查运行副本的模块完整性 + 工具注册 + 包名一致性
node tests/assert.mjs                 # 断言套件(分条 / 出站队列竞态 / 增量扫描 / 群缓冲)

模拟脚本会临时借用真实的 ~/.dsh/qq-bridge.*,测试期间真 DSH 别在跑(两边会互相覆盖游标)。 想完全隔离:

mkdir -p /tmp/dsh-sandbox/.dsh && cp ~/.dsh/qq-bridge.config.json /tmp/dsh-sandbox/.dsh/
USERPROFILE=/tmp/dsh-sandbox node tests/identity-sim.mjs

⚠️ 模拟脚本必须显式设置 cfg.selfUin:群消息的 @ 段要跟 selfUin 比对, 继承真实配置的话 @ 认不出来,测试会「一条都没投递」。

模块划分

39 个模块,按职责分四层(数据流单向:QQ → 消息处理 → 投递 → 会话 → 出站 → QQ):

层模块
入口index.js(装配各模块)
基础设施env.js(数据目录/日志/宿主服务) config.js(配置) state.js(游标) onebot.js(协议调用) health.js(探活门控 + 错误分级) instance.js(单实例锁)
消息处理poll.js(轮询 + WS 事件入口) ws.js(WS 客户端) merge.js(私聊合并窗口) backfill.js(上线补历史) snowluma.js(WS 自动发现) images.js(图片入库) emoticons.js(表情段渲染) emoji-mark.js(正文表情标记)
投递与出站deliver.js(路由 + 投递) sessions.js(会话列表 + 冷启动) archive.js(识别已归档会话,不往收起来的会话里投) outbound.js(回发队列) split.js(分条) title.js(会话标题) history.js(消息历史检索)
能力与 UItool.js(10 个工具注册,含沉默权三件套 qq_wait/qq_pass/qq_close 与 qq_remember) api.js(设置页 API) client.js(设置页前端) sendapi.js(主动发消息接口) closure.js(收尾评分) rhythm.js(节奏底座 + 自动静默) memory.js(关系卡落盘) proactive.js(主动搭话的钟 + 四道闸)
表情与说说sys-faces.js(官方目录) face-resolve.js(id 解析) custom-faces.js(收藏表情) collect.js(收藏表情包) preview.js(看表情包) qzone.js(发说说)
安全blocked.js(13 个 action 物理封禁) allowlist.js(出站目标白名单) sensitive.js(出站内容审计)

各模块的详细职责和踩坑见 docs/开发笔记.md。

本地改了源码怎么推进 DSH

node sync.mjs

⚠️ 插件在 DSH 里有两份,sync.mjs 两个都要同步:

目录是什么
~/.dsh/plugins/dsh-onebot-channel/pnpm file: 依赖的源,重装时从这里取
~/.dsh/profiles/desktop/node_modules/dsh-onebot-channel/DSH 运行时真正 import 的目录

之前只同步前者 → 运行副本一直是旧的 → 现象是「日志写着工具注册成功,agent 那边只有 qq_send」。

sync.mjs 还会反向把工作区里的说明文档拉回仓库 docs/(工作区那份才是 agent 真正读到的那份)。

跑完 重启 DSH 才生效 —— 插件是进程启动时加载的。

⚠️ 改 package.json 的 name 时必须跟安装目录名一致,否则设置页入口会消失。 sync.mjs 会在同步前校验,对不上直接拒绝同步。

启动顺序:为什么入站要等一等

SnowLuma 的 OneBot 端口(HTTP 3000 / WS 3001)是等 QQ 登录成功之后才 listen 的。 而 DSH 常常比 QQ 先起来(开机自启的典型顺序),中间会差上 1~2 分钟。

这段窗口里如果直接起轮询和 WS,就是对着没开的门反复敲 —— 满屏 timeout 和 1006 断连, 真正出问题时反而在日志里找不到线索。

所以插件的启动分成两段:

阶段时机做什么
立即插件加载时注册 10 个工具、设置页 API、主动发消息接口、回复转发队列
延后OneBot 探活通过后启动私聊/群聊轮询、WS 事件推送、主动搭话的钟
  • 工具先注册:工具是出站能力,跟 QQ 登录无关。早给 agent,QQ 迟迟不登录时它也不至于「一个工具都看不到」。
  • 探活用 get_login_info:它要真的进了 QQ 才应答,是「能干活了」的准信儿。退避 1s→5s,总超时 150s。
  • 超时就照样启动:与其让桥接彻底不动,不如让轮询自己重试(错误分级保证不吵)。
  • 错误分级:ECONNREFUSED / fetch failed / 超时这类「门还没开」的错误, 不写 state.lastError(设置页那个字段是给「插件坏了」看的)、不逐条刷日志,只在状态翻转时记一条; retcode 不对、HTTP 5xx 这类真问题照旧上报。

文档

致谢与边界

MIT © 2026 单树见月

  • 协议端不在本仓库范围内 —— 本插件只跟 OneBot 11 协议端说话,不读 QQ 密码、不注入 QQ 进程
  • 插件跑在 DSH 进程内,拥有 DSH 能触及的一切权限 —— 安装前请自行审阅代码

最后更新:2026-10-31 · v0.15.0

Comments

Loading…

From the same category

reactive-resume

DeepSeek Harness plugin for Reactive Resume: bridges your resumes and job applications into a Harness session over MCP.

Tools & CapabilitiesManifest valid

★ 41.7k

↓ 215/wk

MIT

Aug 24, 2026

dsh plugin --profile web add dsh-plugin-reactive-resume

by dsh-market

The plugin market inside DeepSeek Harness — browse, search, one-click install · DSH 可视化插件市场

Tools & CapabilitiesManifest valid

★ 10.1k

↓ 361.7k/wk

MIT

TypeScript

Nov 1, 2026

dsh plugin --profile web add dshmarket

by Tencent

Let AI agents use your real, logged-in browser without interrupting your work. CLI + extension for browser automation across any shell-capable AI agent.

Tools & CapabilitiesManifest valid

★ 9.9k

↓ 12.3k/wk

MIT

TypeScript

Nov 1, 2026

dsh plugin --profile terminal add @wxg-prc-cpg/browser-skill-dsh-plugin

by yjh051108

dsh-routing-suite — injector + router-standard kit: install the runtime injector first, then the task-aware reasoning-mode router preset (measured P1-P23).

Tools & CapabilitiesManifest valid

★ 7k

MIT

JavaScript

Sep 18, 2026

dsh plugin --profile web add @dsh-external/dsh-super-injector

by superdesigndev

OpenRouter for agent tools. Join community here: https://discord.gg/6mQYYfFMAn

Tools & CapabilitiesManifest valid

★ 7k

NOASSERTION

Python

Nov 2, 2026

dsh plugin --profile web add treg-dsh

by Q00

Agent OS: the agent gets smarter on its own. We just hold the line: Interview-gated, staged evaluation, budgeted evolution loop. MCP server, 14 runtimes: Claude Code, Codex CLI, Gemini CLI, OpenCode,

Tools & CapabilitiesManifest valid

★ 6.3k

MIT

Python

Nov 1, 2026

Index only — not installable