Tools & Capabilities
dsh-qq-bridge
banana770/dsh-qq-bridge · ★ 3
QQ ????? <-> DeepSeek Harness ??: ? QQ ?????? Harness ????? (???, Node.js ? 22)
安装
dsh plugin --profile web add github:banana770/dsh-qq-bridgeGitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试
For install and environment setup, plus plugin development tutorials, see DSH community docs ↗
README
Table of contents
- Compatibility at a glance (1.0.3)
- Architecture
- Quick start
- 1. Prerequisites
- 2. Configure
- 3. Run
- 4. Usage
- Optional integration A: install as a DSH settings-page plugin (plugin-pkg)
- Optional integration B: system autostart / keep-alive (Windows only)
- Platform rules (important)
- FAQ
- Repository layout
- Security notes
- Version compatibility
- License
A lightweight bridge that connects DeepSeek Harness (DSH Desktop or dsh web, via its
local API) to the QQ Bot Open Platform (q.qq.com), so a QQ bot can talk to users
through the agents running inside Harness.
- Zero dependencies — only Node.js >= 22 (built-in
fetch/WebSocket) - Does not patch DSH: it reuses the running DSH instance (Desktop or
dsh web) - Every chat peer (private user / group member) maps to its own DSH session, so histories never mix
- Supports DSH "ask user" (
ask_user) prompts: the question is forwarded to QQ and the answer is fed back automatically - Group chats trigger only when the bot is @-mentioned; private chats (C2C) work directly
- Optional: install as a "QQ Bot" management plugin inside the DSH settings page (status / logs / model / toggles)
Compatibility at a glance (1.0.3)
| Runtime | Verified version | Connection |
|---|---|---|
| DSH Desktop | 0.2.1-rc.2, 0.1.7-rc.2 | Load plugin-pkg in the desktop profile; the desktop Web port is random per launch (e.g. 60861), and the plugin forwards the live origin to the bridge via DSH_QQB_DSH_URL — no need to hand-edit config.json |
| DSH Web | 0.1.7-alpha.2, >= 0.2.x | Load plugin-pkg in the Web profile; default dsh.baseUrl is http://127.0.0.1:3080 |
| Legacy DSH | <= 0.1.1-rc.1 | No plugin: direct /api/* access (loopback-unauthenticated era) |
Since 1.0.3 the desktop no longer depends on a hard-coded port;
dsh.baseUrlinconfig.jsonis only a fallback for runningnode src/main.jsby hand. Also, the 0.2.x gateway requires a real Peer, so the adapter now usesconnection.browserPeer; otherwise every RPC fails withCannot read properties of undefined (reading 'signal').
Architecture
QQ user / group member
| 1. private message / group @-message
v
QQ Bot Open Platform gateway (wss://.../gateway/bot, official WebSocket)
|
v
dsh-qq-bridge (this service, Node.js)
| 2. POST /qqbapi/rpc (HTTP RPC, legacy client-request protocol)
| 3. GET /qqbapi/follow/stream (SSE downlink, receives replies)
| 4. POST /qqbapi/answer (feeds answers back into DSH questions)
v
dsh-qq-bridge plugin /qqbapi/* adapter (inside the DSH process, runs with DSH Desktop or dsh web)
| 5. in-process calls to typertGateway (dispatchRpc / openWireStream)
v
DeepSeek Harness (DSH Desktop or dsh web, already running; Web default 127.0.0.1:3080, Desktop uses its actual local port)
| 6. reply text via QQ Open API POST /v2/users|groups/.../messages
v
QQ user / group member
Why the adapter layer is needed. Starting with DSH
0.1.1-rc.2, every/api/*route requires browser Cookie authentication (requestRejectionindsh-client-connection) — loopback included — so the bridge's raw HTTP calls getHTTP 401. Theplugin-pkgplugin in this repo registers unauthenticated/qqbapi/*routes inside the DSH process and translates the legacy protocol into in-process gateway calls (typertGateway). After upgrading DSH, restart DSH Desktop ordsh webonce so the plugin code takes effect. Runningnode src/main.jswithout the plugin only works on old DSH versions.
Quick start
1. Prerequisites
The DSH local API address is configured by
dsh.baseUrl:dsh webdefaults tohttp://127.0.0.1:3080, while DSH Desktop uses its actual local port (tested here:http://127.0.0.1:19387). The bridge and DSH must run on the same computer because they talk over localhost.
- A running DSH Desktop or
dsh web(Web defaulthttp://127.0.0.1:3080; Desktop tested here athttp://127.0.0.1:19387) - Node.js >= 22: check with
node --version - A bot created at q.qq.com, together with its AppID and AppSecret
- New bots start in sandbox mode: only the developer's own QQ account and whitelisted test members can talk to them. Open access requires review and publishing.
2. Configure
# copy the template and fill in your own AppID / AppSecret
cp config.example.json config.json # Windows: copy config.example.json config.json
Key fields in config.json:
| Field | Meaning |
|---|---|
qq.appId / qq.appSecret |
copied from the bot settings page on q.qq.com |
qq.sandbox |
true = sandbox (default for new bots), false = production |
dsh.baseUrl |
DSH local API address: dsh web default http://127.0.0.1:3080; DSH Desktop uses its actual port (tested here: http://127.0.0.1:19387) |
dsh.workspaceCwd |
working directory for newly created DSH sessions (point it at your usual project folder) |
dsh.agentPreset |
optional; chat mode = DSH agent preset: standard / code (PTC) / minimal / cordis; leave empty to follow the Harness default |
dsh.model |
optional; pin provider / model plus reasoningEffort (for example off / high / max, model-dependent) |
dsh.sessionsFile |
peer <-> DSH session map, generated automatically |
bridge.autoStart |
when the bridge is managed by the plugin, start it automatically on plugin load (irrelevant for standalone runs) |
appSecretis shown only once, at creation time.config.jsonis covered by.gitignore— never commit it to a public repository. If you suspect a leak, reset the secret in the q.qq.com console and update the config.
3. Run
node src/main.js # or: npm start
You should see (log messages are in Chinese):
[INFO] 换取 access_token (appId=...) ...
[INFO] 连接 QQ 网关: wss://...
[INFO] 网关就绪 READY: session=... bot=...
[INFO] 已连接 DSH 事件流 (/qqbapi/follow/stream SSE)
[INFO] ========== 桥接已就绪: QQ <-> DSH ==========
4. Usage
- Private chat: message the bot from any QQ account (test members must be configured in the console first)
- Group chat: invite the bot into a group and @-mention it (only @-messages are received)
Built-in commands:
| Command | Effect |
|---|---|
/help |
command list |
/status |
bridge and session status |
/cancel |
abort the current turn |
/reset |
clear this chat's context and start a brand-new DSH session |
/compact |
manually compact the conversation history (DSH also compacts automatically when the context fills up, so this is rarely needed) |
Optional integration A: install as a DSH settings-page plugin (plugin-pkg)
plugin-pkg/ is a DeepSeek Harness static plugin. It adds a "QQ Bot" card to the
settings page where you can view bridge status and logs, edit
AppID/Secret/sandbox/model/chat mode/reasoning effort, configure autostart on boot
and keep-alive after closing the window, and start / stop / restart the bridge.
DSH Desktop and Web use the same plugin package; the desktop profile is
~/.dsh/profiles/desktop/package.json.
Edit the matching DSH profile
package.json(Web:~/.dsh/profiles/web/package.json; Desktop:~/.dsh/profiles/desktop/package.json) and add:{ "dependencies": { "dsh-qq-bridge": "link:C:/path/to/dsh-qq-bridge/plugin-pkg" }, "dsh": { "profile": { "bundles": [ "dsh-qq-bridge" ] } } }Restart DSH Desktop or
dsh web, then open Settings -> QQ Bot.
The plugin has to locate the bridge project directory. By default it derives it from its own location (with a
link:install that is the project root). If your layout differs, setDSH_QQB_BRIDGE_DIR=<absolute path to dsh-qq-bridge>in the environment of the DSH process (Desktop ordsh web).Desktop setup: since 1.0.3 no port is needed — the plugin injects
DSH_QQB_DSH_URL=<the desktop's live origin>when it spawns the bridge. After restarting the desktop app,/qqb/stateshould showrunning=true,status.dshBaseUrlset to the current port, and an established DSH event stream.
Optional integration B: system autostart / keep-alive (Windows only)
The "System" card on the settings page has two toggles (they can also be set in the
system section of config.json):
- Autostart on boot (
system.bootAutoStart): after Windows login, startdsh weband the bridge in the background (hidden windows, no UI). The generated launcher currently targetsdsh web; DSH Desktop users should prefer the desktop app's own startup setting. Implemented with a VBS launcher plus anHKCU\...\CurrentVersion\Runregistry entry. - Keep-alive after close (
system.keepAliveAfterClose): after closing the DSH desktop window, keep the backend and the bridge running. Implemented with akeep-backend.flagfile in the bridge project directory, which the desktop wrapper'smain.jschecks before killing child processes.
With both toggles on, the QQ bot is ready right after boot with no window open. On non-Windows systems the registry / VBS operations fail safely (they are only logged); everything else keeps working.
Platform rules (important)
- Sandbox mode: before review/publishing, a new bot is sandboxed — only the developer and whitelisted "test members" can talk to it. Add test QQ numbers under the "Sandbox configuration" section of the q.qq.com console.
- Private / group permissions: request the "private message" and "group message" capabilities under "Developer settings -> Feature configuration". Without them, messages of that type are never delivered to the bot.
- Passive-reply window: the platform only lets a bot message a user who interacted within the last 5 minutes. If a DSH turn exceeds that window the reply fails (4xx in the log); have the user send another message to reopen it.
- Rate limits: a single group text message is capped at roughly 2000 characters (this bridge splits at 1800 by default) and is subject to platform rate limiting; avoid spamming.
- @-mention: group messages must @-mention the bot; private chat has no such requirement.
FAQ
Q: Startup fails with "换取 access_token 失败" (failed to exchange access token)
A: Check appId / appSecret, and make sure the bot has been created and enabled.
Q: Connected to the gateway but no READY
A: Look for op9 (Identify rejected). Try flipping qq.sandbox, and make sure the host
can reach api.bot.qq.com / sandbox.api.sgroup.qq.com.
Q: No response to private or group messages
A: Check, in order: (1) is your test account in the sandbox whitelist; (2) is the
matching message capability enabled; (3) do bridge logs show the event (C2C 消息 ... /
群@消息 ...); (4) is DSH running.
Q: DSH asks a question (ask_user)
A: The bridge forwards the question and its options to QQ — reply with the option number
(for example 2) or with free text. Answers are fed back through the plugin adapter's
/qqbapi/answer route (in-process $events/result).
Q: The message was sent but there is no reply for a long time
A: Open DSH Desktop or the Web GUI to watch the session (tool calls, questions, ...). Model reasoning
can take a while; if it stalls, use /cancel.
Q: I answered a question on the DSH web page, and now my next QQ message disappears
A: Fixed in v1.0.1. The gateway emits a { type: "cancel", eventId } frame when a question
is consumed elsewhere (answered in the browser, turn ended, or aborted). The bridge used to
ignore that frame, so it still believed a question was pending and treated the next QQ
message as an "answer" — which the gateway then silently dropped. The bridge now clears the
pending question on cancel, and pushes back an explicit "this question has expired" reply
so the user's message is never swallowed.
Repository layout
dsh-qq-bridge/
config.example.json # template (copy to config.json, which is gitignored)
src/
main.js # bridge core (event wiring, commands, question relay)
qq.js # QQ Open API client (token / gateway / WS / send message)
dsh.js # DSH client (RPC + SSE event stream + session mapping)
selftest.js # self-test script
util.js # logging, retry, small helpers
plugin-pkg/ # optional: DSH settings-page static plugin + /qqbapi/* adapter
package.json
cordis.patch.yml
lib/{index.js, client.js}
package.json
Security notes
config.json(holds the AppSecret) andsessions.json(holds chat peer identifiers) are both gitignored — never force-add them.- The bridge and the plugin adapter only talk to
dsh webover loopback (127.0.0.1). The/qqbapi/*adapter is an unauthenticated local interface designed for the local bridge process (matching the loopback/apibehaviour of older DSH). Do not expose the DSH local port (dsh webdefault 3080; on Desktop it is a random port — seestatus.dshBaseUrlin/qqb/state) to the public internet or to untrusted LANs.
Version compatibility
| Repo version | DSH version | Notes |
|---|---|---|
| <= 0.1.0 | <= 0.1.1-rc.1 | direct /api/* access (loopback-unauthenticated era) |
| 1.0.0 | >= 0.1.1-rc.2 (incl. 0.1.2-rc.x, 0.1.5-alpha.x) | via the /qqbapi/* in-process adapter in plugin-pkg |
| 1.0.1 | >= 0.1.1-rc.2, incl. 0.1.7-alpha.2 | auto-detects both openWireStream signatures; forwards gateway cancel frames so an expired question no longer swallows the next QQ message |
| 1.0.2 | DSH Desktop 0.1.7-rc.2; DSH Web >= 0.1.1-rc.2 (incl. 0.1.7-alpha.2) | Desktop and Web share the same plugin-pkg adapter; point dsh.baseUrl at the actual local API (desktop tested here at 19387); clearer desktop wording and docs |
| 1.0.3 (current) | DSH Desktop 0.2.1-rc.2; DSH Web >= 0.1.1-rc.2 | Adapts to the 0.2.x gateway's mandatory real Peer (connection.browserPeer); follows the desktop's random port via DSH_QQB_DSH_URL; config.json now uses node:fs so the settings page is no longer blocked by the workspace-write sandbox; fixes a restart storm after saving the config |
Tested on DSH Desktop 0.2.1-rc.2. Load
plugin-pkgin the desktop profile — you do not need to hard-code a port any more: the plugin injects the live origin (DSH_QQB_DSH_URL) when spawning the bridge./qqb/stateshould reportrunning=truewithstatus.dshBaseUrlset to the current port, and the log should show the DSH event stream connected plusREADYon the QQ gateway.
Troubleshooting. Endless
events stream failed: fetch failedin the bridge log while QQ only receives "DSH seems not to be running" -> the bridge cannot reach DSH (stale port or DSH not running).Cannot read properties of undefined (reading 'signal')-> the adapter does not match the current DSH gateway; upgrade this plugin.
DSH 0.1.7-alpha.2 note.
dsh-api-gatewaychanged the wire-stream entry point:openWireStream(endpoint, payload, signal)becameopenWireStream(endpoint, payload, uplink, peer, signal, control). Passing the old third argument made the gateway reject the signal (signals[0] is not of type AbortSignal), so the$eventsstream could never be established and no replies ever reached QQ. The plugin now sniffs the function arity and adapts to both signatures.
License
MIT License — see LICENSE.