dsh-onebot-channel
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.
Install
dsh plugin --profile web add dsh-onebot-channel
Sandbox test results
| Stage | Result |
|---|---|
| 1 install | ✓ |
| 2 list | ✓ |
| 3 mount | ✓ |
| 4 boot | — |
Four stages run in an isolated sandbox: install (network on) → list → mount → boot (last three with no network).
README
dsh-onebot-channel
把 QQ 私聊与群聊接进 DeepSeek Harness 会话。
基于 OneBot 11 协议:入站走 WebSocket 事件推送,出站用 qq_send 工具回话。


让「在 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 · text segments emoji custom_emoji user_id group_id
qq_emoji_search · 搜 QQ 官方表情目录 · query all
qq_msg_history · 查群 / 好友历史消息 · 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](docs/QQ对话说明.md)。
分几条,也由她定
不传 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
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::10,最后更新 2026-11-01