dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
dsh 插件:QQ 通道(OneBot 11 标准客户端)——正向 WebSocket + HTTP,把你的 Agent 接进 QQ 群和私聊,零依赖 · OneBot 11 client channel for dsh: your agent in QQ groups and DMs, zero dependencies.
简体中文 | English
A QQ channel for DeepSeek Harness (dsh): a standard OneBot 11 client — it connects to the OneBot implementation you run yourself and brings your dsh agent into QQ groups and private chats.
Zero dependencies: only Node built-ins (fetch / WebSocket / node:crypto); there is no npm package to
install.
More mature options in the same space: for one codebase that connects several platforms at once (WeChat / Feishu / DingTalk / WeCom / Slack / Telegram / Discord / QQ ...) through each platform's official bot credentials, use xmanrui/dsh-im; for QQ only there are cheesehaqi/dsh-qq-onebot-bridge and Hoshino-Yumetsuki/dsh-onebot. Why we keep this one: it is a standard OneBot 11 channel with zero dependencies — the version our own production machine runs (the personal-account route); written against the protocol, switching OneBot implementations does not mean switching plugins. Install one of the above first; they cover more ground than this one does.
Because switching implementations does not mean switching plugins.
Almost every QQ bot implementation in the Chinese community (go-cqhttp, Lagrange, NapCat, LLOneBot, ...) implements OneBot 11. If this plugin were "an adapter for some framework", then switching implementations would mean switching a whole codebase; written against the protocol instead, it does not matter which one you are running — change one line of the address in the config file and you are done.
So what it does here is narrow: take the protocol in, hand the messages to the dsh agent. Persona, memory, content policy, daily digests and sticker collections all belong to "how you use it once you are connected" and should be decided by your own plugins or system prompt; they should not be stuffed into a channel plugin (whatever gets stuffed in, nobody else can turn it off).
QQ 用户 ──> 你的 OneBot 实现 ──ws──> [dsh-onebot] ──> dsh agent(一个会话一个 session)
│
QQ 用户 <── 你的 OneBot 实现 <──API───────┘
message events (group / private) over a forward WebSocket. Both shapes are accepted: CQ-code
strings and segment arrays.send_group_msg / send_private_msg; message segments support text / at / image /
record / reply / face.group:<group id> for groups and private:<QQ number> for private chats; one key is one
dsh session, and nothing crosses over. Messages in the same session are handled serially: one
message in, one reply out.get_msg fallback), the
group member roster (@123456789 → @Alice), exponential-backoff reconnect, log throttling.Someone in a group @-s the bot:
【群987654321 · 22:15】爱丽丝(QQ 111222333)
↩ 回复 bob(QQ 444555666):「(图)」
[在 @ 你] 这图啥意思
The header line is "where · what time"; the second line is the message being replied to (humanized); the
last line is the body. If the replied-to message was not kept locally, it is fetched with get_msg; if
that fails too, the envelope says that the original text was not available on our side, so the model knows
not to make it up — instead of receiving a lonely "what does this mean".
dsh plugin --profile web add github:JackZo400/dsh-onebot
Or clone it locally and insert it in your bundle patch (you can copy the example in cordis.patch.yml as
is).
Environment: Node 18+ (enough for the HTTP half). Receiving messages needs WebSocket: Node
22+ ships a global WebSocket; on Node 18/20 inject one in the config (WebSocketImpl) or start with
the HTTP half only (can send, cannot receive). The plugin itself depends on no npm package.
Pick any one (go-cqhttp / Lagrange / NapCat / LLOneBot will all do), log the QQ account in, then turn on "forward WebSocket service" in its config (some call it "WebSocket service" / "forward WS" / "ws server"), and write down:
3001)/, NapCat commonly uses /onebot/v11/ws)⚠️ Two easy traps: do not let the port collide with anything else; do not log one QQ account into two implementations at the same time (they will kick each other off).
Do not install the dsh plugin yet; first verify by hand with what Node gives you (Node 22+):
node -e "const ws=new WebSocket('ws://127.0.0.1:3001');
ws.onopen=()=>{console.log('连上了');ws.send(JSON.stringify({action:'get_login_info',params:{},echo:1}))};
ws.onerror=(e)=>console.log('连不上:',e.message||e);
ws.onmessage=(e)=>console.log('回包:',e.data)"
The plugin logs in Chinese; the two console strings above mean "connected" and "reply". If you see the
first one and a {"status":"ok","retcode":0,"data":{"user_id":...,"nickname":...}} line, then the
address, token and path are all correct; if this step does not work, do not bother with the rest — the
problem is on the implementation side. (If a token is set, use
ws://127.0.0.1:3001?access_token=your-token instead.)
Edit your profile patch (copy the one in cordis.patch.yml and change it):
- insert:
- id: onebot
name: dsh-onebot
config:
ws: ws://127.0.0.1:3001 # 第一步验通过的那个地址
accessToken: '' # 实现那边设了才填
selfId: '' # 留空就行(用事件里的 self_id)
groups: [] # 留空 = 所有群;想限定就写群号
private: false # 私聊默认关(要开就 true,并考虑填 users 白名单)
requireMention: true # 群里只有 @ 我才花模型的钱
Restart that profile, then look for [dsh-onebot] in the log (the plugin writes straight to stderr):
dsh ... 2>&1 | grep '\[dsh-onebot\]'
You should see these lines in order:
[dsh-onebot] 就绪:ws=ws://127.0.0.1:3001 群=不限 私聊=关 群内需@=是
[dsh-onebot] 已暴露 onebot 服务(别的插件可以调 send / call)
[dsh-onebot] 已连上 OneBot 服务
[dsh-onebot] 已登录:你的昵称(123456789)
The plugin prints these lines in Chinese; the third line means "connected to the OneBot service" and the fourth means "logged in as your nickname", and those two mean you are through. Next, @ the bot in a group and say something: the log will then show the received group message (session key, whether you were @-ed, who sent it), a line for the new session, and a line for the reply with its character count.
| Symptom | Likely cause | What to do |
|---|---|---|
no [dsh-onebot] line in the log at all | the plugin was not loaded, or the patch did not take effect | check the profile's patch path; the plugin name must be dsh-onebot |
| it keeps logging connect failures / disconnects | wrong address / port / path, or forward WS is not enabled | go back to step 1 and verify with that node -e snippet |
| connected, but no login info can be fetched | some implementations do not enable get_login_info | does not affect sending/receiving (self_id comes from the event), and the log will say so |
| no messages arrive at all | only http is configured, not ws | OneBot's HTTP API does not push events; configure ws |
| private chats are ignored | private: false (the default) | set it to true; to only answer yourself, fill in users |
| @-ing it in a group gets no answer | the group is not in groups / requireMention / the implementation sent no at segment | turn on debug: true and read the "not dispatching: " line in the log |
| it talks to itself (a loop) | that implementation's echo shape has not been seen before | open an issue and paste that event from the debug log |
the log shows a reply failure with retcode=xxx | rejected by the implementation or by risk control | read wording in the log; short messages going out while long ones do not is usually a length limit |
| the reply is sent but images are missing | images are not carried over in this version (see below) | turn on imageLinks: true to give the model the direct links |
The plugin calls ctx.provide('onebot', …), so other plugins (scheduled jobs, proactive pushes, for
example) can use it directly:
const onebot = ctx.get('onebot')
await onebot.send('group:987654321', '开会了', { replyTo: undefined })
await onebot.sendGroupMsg(987654321, [{ text: '嗨 ' }, { at: '111222333' }, { image: '/tmp/a.png' }])
const stats = onebot.stats() // { events, messages, dispatched, replied, skipped, errors, sessions, connected, pending, attempts }
Both of them are offline: a fake WebSocket, a fake fetch, fake event JSON and a fake ctx; no network
and no dsh needed.
node test/selftest.mjs # 协议层(纯函数):CQ 编解码 / 事件解析 / 发送构造 / API 错误 / 退避
node test/plugin-selftest.mjs # 插件层:拿假 ctx 把 apply() 真跑起来,灌事件看它有没有真的收发
The key points covered (every one of them was written after stepping on that bug):
& is restored)@ me decision ([CQ:at,qq=<self_id>] and at in segment form; @all does not count)user_id == self_id / message_sent) are not dispatched to the agentreply segment) can resolve the content of the quoted message (local cache + get_msg
fallback)text + at + image is built into the correct message segments (the reply
segment is always first)retcode≠0 error pathsThese two self-tests can fail: delete the comma inside escapeCq, remove the echo filter, or change
the backoff to a constant, and they report an error and exit non-zero (which is also how we verify that
"the tests are not just decoration").
Supported
post_type=message; both shapes of the message field (CQ-code
string / segment array)text / at / image / record / reply / facesend_group_msg, send_private_msg, get_msg, get_group_member_list,
get_group_member_info, get_group_info, get_login_infomessage_sent and user_id == self_id), automatic self_id detection@number → a real at segment, humanization ((image) / [voice] / [card]…)get_msg as a fallback (the same message is fetched
only once per 5 minutes)requireMention, quoteReply, format, imageLinksNot supported (deliberately not done, or not done in this version)
[CQ:image] only turns into the "(image)" placeholder, and voice
only turns into "[voice]". If you want it to really see pictures, give dsh a tool that can fetch images,
then turn on imageLinks: true to put the direct links into the body.forward segment): only a "[merged forward]" placeholder, never
expanded.onebot service (below), so other plugins can.selfId, but that has not been tested.It is a rewrite of a private QQ channel that was really running into a standard-protocol implementation: all the parts that had been paid for in bugs were carried over — protocol parsing (CQ codes, @ detection, quotes, echo, message segment construction), session scheduling (one agent per session, serial queue, splitting long messages), the roster and the caches, and reconnect backoff.
What was deliberately not carried over is the old deployment policy: the private platform's login/event stream, persona and memory tiers, content gating, message batching and daily digests, the sticker library, voice transcription, owner commands — none of that has anything to do with the protocol, and inside a general-purpose plugin it would only turn into "behaviour that other people cannot turn off".
MIT, see LICENSE.