把 QQ 好友和群消息接入 agent 会话并回传回复
29.7/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/11/2 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add Kfytm/QQ-DeepSeek-Harness-bridge该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:仅索引
是什么
dsh 原生插件 · platform
装得上吗
本站尚未做安装检查
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 4 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
🔴 安全扫描发现高危能力自动 · 静态分析源码
v2.7.0🔴 扫描:高危尚无人工评估
静态扫描发现安装期即执行代码,或命令参数由运行时变量拼接(参数来源无法静态判定,从严处理)。
安装时即执行脚本可执行外部命令读写/删除本地文件读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 136 处,展示前 108 条)
- 发起网络请求plugins/qq-agent-host/client.js:39
const jget = async (p) => asJson(await fetch(API + p, { credentials: 'same-origin' })); - 发起网络请求plugins/qq-agent-host/client.js:40
const jpost = async (p, body) => asJson(await fetch(API + p, { - 发起网络请求scripts/check-onebot-status.mjs:19
const res = await fetch(`${baseHttp}/get_login_info`, { - 读取本地文件scripts/console-ui-fixture.mjs:345
await readFile(file); - 读取本地文件scripts/console-ui-fixture.mjs:361
res.end(await readFile(file)); - 执行外部命令固定命令scripts/harden-state-acl.mjs:64
const res = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps], { - 执行外部命令固定命令scripts/privacy-scan.mjs:90
const res = spawnSync('git', a, { cwd: ROOT, encoding: 'utf8', timeout: 60000 }); - 发起网络请求scripts/probe-020-protocol.mjs:24
const r = await fetch(`${BASE}/api/${endpoint}`, { - 发起网络请求scripts/probe-auth.mjs:59
const response = await fetch(`${base}/api/${endpoint}`, { - 执行外部命令固定命令scripts/resolve-dsh-port.mjs:33
out = execFileSync('netstat', ['-ano'], { encoding: 'latin1', timeout: 10000 }); - 发起网络请求scripts/resolve-dsh-port.mjs:50
const res = await fetch(`http://127.0.0.1:${port}/api/remote.mux`, { signal: controller.signal }); - 读取密钥环境变量scripts/send-voice.mjs:70
val('console-token', cfg.consoleToken || process.env.QQ_CONSOLE_TOKEN || readConsoleTokenFile() || '') - 发起网络请求scripts/send-voice.mjs:90
const res = await fetch(`${httpUrl}/${action}`, { - 发起网络请求scripts/send-voice.mjs:116
const res = await fetch(`http://127.0.0.1:${consolePort}${pathname}`, { - 执行外部命令来自配置/变量scripts/setup-dsh.mjs:335
const r = spawnSync(cmd, args, { - 发起网络请求scripts/test-audit-bridge.mjs:85
const request = http.request({ host: '127.0.0.1', port: server.address().port, path: 'http://[', method: 'GET' }, (response) => { - 发起网络请求scripts/test-audit-bridge.mjs:104
const request = http.request({ host: '127.0.0.1', port: server.address().port, path: '/api/session/reset?token=fixture-console-token', method: 'POST', - 发起网络请求scripts/test-audit-bridge.mjs:344
const request = http.request( - 发起网络请求scripts/test-audit-bridge.mjs:396
const request = http.request( - 发起网络请求scripts/test-audit-regressions.mjs:45
const request = http.request({ host: '127.0.0.1', port, path: pathname, method, headers: h }, (response) => { - 执行外部命令来自配置/变量scripts/test-audit-setup.mjs:39
const run = (profile = 'web', extraEnv = {}) => spawnSync(process.execPath, [path.join(repo, 'scripts/setup-dsh.mjs'), profile], { - 执行外部命令来自配置/变量scripts/test-audit.mjs:107
const result = spawnSync(process.execPath, [path.join(root, 'scripts', test)], { - 执行外部命令固定命令scripts/test-console-encoding.mjs:42
const r = spawnSync('chcp', ['65001'], { encoding: 'latin1', windowsHide: true }); - 执行外部命令固定命令scripts/test-console-encoding.mjs:49
const before = spawnSync('chcp', [], { encoding: 'latin1', windowsHide: true }); - 读取本地文件scripts/test-console-ui.mjs:18
const legacyIds = JSON.parse((await readFile(join(here, 'console-ui-legacy-ids.json'), 'utf8')).replace(/^ FEFF/, '')); - 读取本地文件scripts/test-console-ui.mjs:34
].find(x => existsSync(x)); - 发起网络请求scripts/test-console-ui.mjs:51
const requested = new URL(route.request().url()); - 发起网络请求scripts/test-console-ui.mjs:53
blockedRequests.push(route.request().url()); - 发起网络请求scripts/test-console-ui.mjs:88
page.waitForResponse(r => new URL(r.url()).pathname === path && r.request().method() === method), - 发起网络请求scripts/test-console-ui.mjs:125
await Promise.all([page.waitForResponse(r => r.url().endsWith('/api/mode') && r.request().method() === 'POST'), select.selectOption('qq-chat-v2')]); - 写入本地文件scripts/test-console-ui.mjs:249
await writeFile(baselinePath, JSON.stringify(fixture.requests, null, 2) + '\n'); - 读取本地文件scripts/test-console-ui.mjs:252
const expected = JSON.parse(await readFile(baselinePath, 'utf8')); - 写入本地文件scripts/test-console-ui.mjs:359
await mkdir(resolve(artifacts), { recursive: true }); - 发起网络请求scripts/test-console-ui.mjs:409
await systemDark.route('**/*', route => new URL(route.request().url()).origin === fixture.url ? route.continue() : route.abort('blockedbyclient')); - 发起网络请求scripts/test-console-ui.mjs:420
await calm.route('**/*', route => new URL(route.request().url()).origin === fixture.url ? route.continue() : route.abort('blockedbyclient')); - 写入本地文件scripts/test-console-ui.mjs:555
await mkdir(resolve(artifacts), { recursive: true }); - 写入本地文件scripts/test-console-ui.mjs:583
await mkdir(resolve(artifacts), { recursive: true }); - 写入本地文件scripts/test-console-ui.mjs:584
await writeFile(resolve(artifacts, 'test-results.json'), JSON.stringify({ original, results, requests: fixture.requests, errors, blockedRequests }, nu - 发起网络请求scripts/test-console.mjs:25
const res = await fetch(BASE + path, { method: method || 'GET', headers, body: body ? JSON.stringify(body) : undefined }); - 发起网络请求scripts/test-console.mjs:35
const page = await fetch(BASE + '/', { headers: authHeaders }); - 发起网络请求scripts/test-dsh-auth.mjs:55
const res = await fetch(`${BASE}/api/${method}`, { - 发起网络请求scripts/test-dsh-auth.mjs:103
const res = await fetch(`${BASE}/?token=${stale}`, { redirect: 'manual' }); - 发起网络请求scripts/test-forward.mjs:114
const resNoToken = await fetch(`${urlBase}?key=${encodeURIComponent(key)}&id=abc`, { headers: authHeaders, signal: AbortSignal.timeout(5000) }); - 发起网络请求scripts/test-forward.mjs:118
const resUnknown = await fetch(`${urlBase}?key=${encodeURIComponent(key)}&id=not_seen_in_session`, { - 读取密钥环境变量scripts/test-media-search.mjs:231
const oldKey = process.env.QQ_TEST_SEARCH_KEY; - 读取密钥环境变量scripts/test-media-search.mjs:241
delete process.env.QQ_TEST_SEARCH_KEY; - 读取密钥环境变量scripts/test-media-search.mjs:246
process.env.QQ_TEST_SEARCH_KEY = 'sk-from-env'; - 读取密钥环境变量scripts/test-media-search.mjs:254
delete process.env.QQ_TEST_SEARCH_KEY; - 读取密钥环境变量scripts/test-media-search.mjs:259
if (oldKey === undefined) delete process.env.QQ_TEST_SEARCH_KEY; else process.env.QQ_TEST_SEARCH_KEY = oldKey; - 读取密钥环境变量scripts/test-media-search.mjs:311
const oldKey = process.env.DEEPSEEK_API_KEY; - 发起网络请求scripts/test-reply-wait.mjs:20
const req = http.request({ hostname: '127.0.0.1', port: server.address().port, - 发起网络请求scripts/test-reply-wait.mjs:35
const req = http.request({ hostname: '127.0.0.1', port: server.address().port, - 执行外部命令来自配置/变量scripts/test-setup-dsh-idempotent.mjs:54
const r = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'setup-dsh.mjs'), 'web'], { - 发起网络请求scripts/test-stickers.mjs:133
const res = await fetch(`${base}/fetch_custom_face_detail`, { - 发起网络请求scripts/test-token-economy.mjs:31
const req = http.request({ hostname: '127.0.0.1', port: server.address().port, - 发起网络请求scripts/test-turn-budget.mjs:31
const res = await fetch(`http://127.0.0.1:${port}/api/socialV2/wait`, { - 发起网络请求scripts/test-v2-initial-prompt.mjs:30
const request = http.request( - 发起网络请求scripts/test-vision-media.mjs:64
const res = await fetch(url, { headers: { 'x-agent-token': agentToken, 'x-console-token': consoleToken }, signal: AbortSignal.timeout(10000) }); - 发起网络请求scripts/test-voice-edge.mjs:67
const boot = await fetch(`${base}/api/bootstrap`, { headers }).then((r) => r.json()); - 发起网络请求scripts/test-voice-edge.mjs:74
const results = await Promise.all(Array.from({ length: 10 }, () => fetch(`${base}/api/send`, { - 发起网络请求scripts/test-voice-edge.mjs:93
const res = await fetch(`${base}/api/send`, { method: 'POST', headers, body: JSON.stringify(body) }); - 发起网络请求scripts/test-voice-edge.mjs:100
const r1 = await fetch(`${base}/api/upload?name=${encodeURIComponent('测试 空格+特殊#字符.mp3')}`, { - 发起网络请求scripts/test-voice-edge.mjs:106
const readBack = await fetch(`${base}/api/audio?t=${encodeURIComponent(token)}&name=${encodeURIComponent(r1.saved)}`); - 发起网络请求scripts/test-voice-edge.mjs:108
await fetch(`${base}/api/delete`, { method: 'POST', headers, body: JSON.stringify({ name: r1.saved }) }).catch(() => {}); - 执行外部命令来自配置/变量scripts/test-voice-gui.mjs:187
child = spawn(process.execPath, [voiceToolPath('voice-gui.mjs'), '--no-open', '--port', String(port)], { - 发起网络请求scripts/test-voice-gui.mjs:217
const noAuth = await fetch(`http://127.0.0.1:${port}/api/library`).then((r) => r.status).catch(() => 0); - 发起网络请求scripts/test-voice-gui.mjs:220
const boot = await fetch(`http://127.0.0.1:${port}/api/bootstrap`, { headers: { 'x-voice-token': token } }) - 发起网络请求scripts/test-voice-gui.mjs:227
const bad = await fetch(`http://127.0.0.1:${port}/api/send`, { - 发起网络请求scripts/test-voice-gui.mjs:237
const dry = await fetch(`http://127.0.0.1:${port}/api/send`, { - 发起网络请求scripts/test-voice-preview.mjs:67
const boot = await fetch(`${base}/api/bootstrap`, { headers }).then((r) => r.json()); - 发起网络请求scripts/test-voice-preview.mjs:74
const res = await fetch(`${base}/api/preview?t=${encodeURIComponent(token)}&name=${encodeURIComponent(voice.name)}${qs}`); - 执行外部命令来自配置/变量scripts/test-voice-volume.mjs:28
try { child = spawn(bin, args, { windowsHide: true, stdio: 'ignore' }); } - 发起网络请求scripts/test-voice.mjs:344
const res = await fetch(`http://127.0.0.1:${port}/api/voice/send`, { - 发起网络请求scripts/test-voice.mjs:354
const denied = await fetch(`http://127.0.0.1:${port}/api/voice/send`, { - 发起网络请求scripts/verify-agent-presets-017.mjs:27
const res = await fetch(`${BASE}/api/${method}`, { - 执行外部命令来自配置/变量scripts/verify-dsh-020-adaptation.mjs:207
const probe = spawnSync(process.execPath, ['--check', path.join(ROOT, 'src/dsh-client.js')], { encoding: 'utf8' }); - 执行外部命令来自配置/变量scripts/verify-dsh-020-adaptation.mjs:212
const r = spawnSync(process.execPath, ['--check', path.join(ROOT, f)], { encoding: 'utf8' }); - 发起网络请求scripts/verify-dsh-020-adaptation.mjs:227
const r = await fetch(`${BASE}/api/${e}`, { method: 'POST', headers: { cookie, 'content-type': 'application/json' }, body: JSON.stringify({ type: 'cli - 发起网络请求scripts/verify-dsh-020-adaptation.mjs:365
const dotRes = await fetch(`${BASE}/api/session.list`, { - 执行外部命令固定命令src/bridge.js:1627
const out = execFileSync('netstat', ['-ano', '-p', 'TCP'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] }); - 执行外部命令固定命令src/bridge.js:1630
const out = execFileSync('sh', ['-c', 'ss -ltn 2>/dev/null || netstat -ltn 2>/dev/null || true'], { encoding: 'utf8', timeout: 5000, stdio: ['ignore', - 发起网络请求src/bridge.js:2063
const response = await bot.request('fetch_custom_face_detail', { count }); - 发起网络请求src/bridge.js:2344
const res = await fetch(`${httpUrl}/${action}`, { - 发起网络请求src/bridge.js:2419
const res = await fetch(`${httpUrl}/get_msg`, { - 发起网络请求src/bridge.js:2467
const res = await fetch(`${httpUrl}/fetch_ptt_text`, { - 发起网络请求src/bridge.js:2536
const res = await fetch(`${httpUrl}/${action}`, { - 发起网络请求src/bridge.js:2630
const response = await bot.request('modify_custom_face', { emoji_id: entry.id, desc: cleanRemark }); - 执行外部命令固定命令src/bridge.js:3613
const out = execFileSync('netstat', ['-ano', '-p', 'TCP'], { encoding: 'latin1', timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'] }); - 执行外部命令固定命令src/bridge.js:3619
const out = execFileSync('sh', ['-c', 'ss -ltn 2>/dev/null || true'], { encoding: 'utf8', timeout: 8000, stdio: ['ignore', 'pipe', 'ignore'] }); - 执行外部命令来自配置/变量src/bridge.js:8547
return execFileSync(phoneAdbPath(), [...phoneDeviceArgs(), ...args], { - 执行外部命令来自配置/变量src/bridge.js:8555
return execFileSync(phoneAdbPath(), [...phoneDeviceArgs(), ...args], { - 读取密钥环境变量src/dsh-client.js:37
const fromEnv = process.env.DSH_LAUNCH_TOKEN; - 发起网络请求src/dsh-client.js:418
const res = await fetch(url, { redirect: 'manual', signal: AbortSignal.timeout(this.timeoutMs) }); - 发起网络请求src/dsh-client.js:577
const response = await fetch(input, { ...init, headers }); - 发起网络请求src/mcp-host-server.js:56
const res = await fetch(`${httpUrl}/get_login_info`, { - 发起网络请求src/mcp-snowluma-safe.js:89
const res = await fetch(`${httpUrl}/${action}`, { - 发起网络请求src/mcp-snowluma-safe.js:157
const res = await fetch(`${agentApiBase()}${path}`, { ...rest, headers, signal: AbortSignal.timeout(timeoutMs) }); - 发起网络请求src/mcp-snowluma-safe.js:228
req = http.request({ - 发起网络请求src/mcp-web-browser.js:114
const res = await fetch(url, { - 执行外部命令来自配置/变量src/mcp-web-browser.js:130
execFileSync(ffmpeg, ['-y', '-v', 'error', '-i', raw, '-vf', 'scale=1280:-1', '-q:v', '5', jpg], { - 发起网络请求src/safe-fetch.js:218
req = mod.request({ - 发起网络请求src/snowluma-conn.js:86
const res = await fetch(`${baseUrl}/get_login_info`, { - 读取密钥环境变量src/snowluma-conn.js:120
const explicit = token || process.env.SNOWLUMA_TOKEN || sl.accessToken || ''; - 执行外部命令固定命令src/state-acl.mjs:36
return spawnSync('icacls', args, { encoding: 'utf8', timeout, windowsHide: true }); - 执行外部命令来自配置/变量src/voice-core.js:163
child = spawn(ffprobePath, [ - 执行外部命令来自配置/变量src/voice-core.js:402
child = spawn(ffmpeg, args, { windowsHide: true, stdio: 'ignore' }); - 执行外部命令来自配置/变量src/voice-core.js:423
child = spawn(ffmpeg, wavArgs, { windowsHide: true, stdio: 'ignore' }); - 发起网络请求src/web-search.js:210
req = https.request({
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-host-apiproxy@deepseek-ai/schemastery给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
把 QQ 接入 DeepSeek Harness:消息变成 DSH agent,人格/工具/上下文全用 DSH 原生机制
README
由 DeepSeek 最新模型翻译生成QQ ↔ DeepSeek Harness 桥接
关于本项目
本项目最初 fork 自 Derpyu520/qq-bridge,之后做了较大改造:桥接不再当 HTTP 客户端遥控 DSH,而是装成 DSH 插件在进程内建 agent。
本仓库地址:Kfytm/QQ-DeepSeek-Harness-bridge。
因此:
- 下面的「版本 / Release 对应表」指的是原项目的版本,不是本仓库的;
- 推荐用法见 插件化模式(推荐);
- 独立进程模式(start.bat)仍然可用,用于回退。
English: README.en.md | 中文: README.md
🗂️ 文档索引(全部文档一句话说明 + 是否仍然有效)见 docs/README.md;仓库目录/脚本命名约定见 docs/FOLDER_MAP.md。
🔒 QQ 会话的权限边界与安全承诺见 RULES.md。
🎤 独立的「发语音」工具已拆到仓库上一级的 ../voice-tool/(命令行 voice-cli.mjs / 图形界面 voice-gui.mjs),不再依赖 qq-bridge 运行。
把 QQ 消息接入 DSH agent:QQ 好友/群发来的消息会变成 DSH 会话里的用户消息,agent 的回复(含提问、工具审批)会发回 QQ。
展开完整文档(README 剩余 1 节)
QQ ↔ DeepSeek Harness 桥接
关于本项目
本项目最初 fork 自 Derpyu520/qq-bridge,之后做了较大改造:桥接不再当 HTTP 客户端遥控 DSH,而是装成 DSH 插件在进程内建 agent。
本仓库地址:Kfytm/QQ-DeepSeek-Harness-bridge。
因此:
- 下面的「版本 / Release 对应表」指的是原项目的版本,不是本仓库的;
- 推荐用法见 插件化模式(推荐);
- 独立进程模式(start.bat)仍然可用,用于回退。
English: README.en.md | 中文: README.md
🗂️ 文档索引(全部文档一句话说明 + 是否仍然有效)见 docs/README.md;仓库目录/脚本命名约定见 docs/FOLDER_MAP.md。
🔒 QQ 会话的权限边界与安全承诺见 RULES.md。
🎤 独立的「发语音」工具已拆到仓库上一级的 ../voice-tool/(命令行 voice-cli.mjs / 图形界面 voice-gui.mjs),不再依赖 qq-bridge 运行。
把 QQ 消息接入 DSH agent:QQ 好友/群发来的消息会变成 DSH 会话里的用户消息,agent 的回复(含提问、工具审批)会发回 QQ。
⚠️ 以下版本号属于原项目(v0.2.0-r4 / 适配 DSH 0.2.0-rc.2)(在该版本上逐项实测:npm run verify:adaptation)。
鉴权用 ~/.dsh 里持久化的浏览器会话签名密钥离线铸造 Cookie —— DSH 0.1.7 起进程启动 token 只存在于内存、不再落盘,旧版「从 guard 日志里读 token」的方式已失效。agent preset 是 DSH 0.1.7 起的 @deepseek-ai/dsh-agent-preset Cordis 行(~/.dsh/.agent-presets/ 目录机制已废)。协议代次(Cookie 鉴权 / 斜杠 RPC / /api/remote.mux 事件流)自 DSH 0.1.2-alpha.1 起引入,与更早的点号 endpoint 协议不兼容 —— DSH 0.1.1-rc.2 及更早请改用 tag v0.1.0;DSH 0.1.5-rc.1 用 v0.1.5;DSH 0.1.7-rc.2 用 v0.1.7。
v0.2.0 修的主要是会话待处理队列读不出来:桥接退役/重置 QQ 会话时要先清掉 DSH 侧没跑的队列,而那段解析认的是一个从来不存在的字段(session/control baseline 的 value.queues[<id>])——于是每一次退役都抛 invalid session/control baseline,队列清不掉、旧任务继续在 DSH 里跑,而日志里只有一行警告。注意:这不是 0.2.0 引入的——该代码随 v0.1.7 发布,报错日志早于 0.2.0-rc.2 的发布,0.2.0 只是把它翻了出来。现在读队列以一元 RPC session/projections 为主路径、以 projections[<id>].values.inbox 为回退形状,并补了两条回归网。
同一轮还按 0.2.0 的真实工具清单重新对账了 QQ 安全守卫(新增 15 个必须隐藏的工具名,含全局层默认启用的 read_mcp_resource、plugin_manager、subagent_codex 等)。
逐条改动说明见 docs/guides/DSH_020_ADAPTATION.md。
r4 主要改动:QQ 表情包/动图链路(表情 CDN 优先、emoji_id 兜底、DNS 多地址故障切换)、联网搜索改用与 DSH 同源的 DeepSeek 搜索(Bing 仅兜底)、qq_wait_for_messages 改 5 分钟长轮询、控制台切模式被 5 秒轮询改回去、回合永久「繁忙」、机器人身份未注入等 issue 修复,外加一轮整体体检。
逐条见 Release v0.2.0-r4。
默认分支 main 就是本版本,git clone 直接拿到,无需切换分支。
QQ 消息 ──► SnowLuma(OneBot v11 WS)──► 本桥接进程 ──► DSH Web API (127.0.0.1:3080/api) ▲ │ └── agent 回复/提问/审批 ┘
架构
- QQ 侧:@snowluma/sdk 的 SnowLumaWebSocketClient(OneBot v11 WebSocket 客户端,自动重连)
- DSH 侧:适配 DSH 0.1.2 起引入的协议,并在 0.2.0-rc.2 上逐项复核——用本机持久化签名密钥铸造会话 Cookie 鉴权、/api/<namespace>/<method> 斜杠 RPC、/api/remote.mux + session/follow 事件流(session/control 的队列读 value.projections[<id>].values.inbox,另有一元 session/projections 主路径);复用 AbstractApiClient 传输层,但不再依赖旧版 zod value schema。会话模型由桥接按 config.json 的 dsh.model 逐会话 session.selectModel 固定(默认 deepseek-flash = DeepSeek-V41-Flash,多模态)
- 智能体自主收发 QQ:DSH 的 MCP 客户端(~/.dsh/profiles/web/cordis.patch.yml 配置)接入三个 MCP server:
- snowluma(桥接自带 src/mcp-snowluma-safe.js):QQ 动作安全子集(查状态/查群/查消息/发消息,发送强制白名单;发送工具支持可选 replyToMessageId 引用回复)
- snowluma-host(桥接自带 src/mcp-host-server.js):只有 snowluma_status(只读探活 get_login_info)。
⚠️ v0.1.5 起移除了 start_snowluma / stop_snowluma(以及 snowluma.allowProcessControl 配置项)——原因是 SnowLuma EULA §5.4 规定「将其并入第三方安装包或 Docker 镜像、通过自动化脚本部署」须事先取得书面授权,而原生组件属专有组件。本程序只探测、不部署:SnowLuma 的安装、启动与扫码始终由你自己完成(详见 LICENSE 与 RULES.md)。
- web-search-safe(桥接自带 src/mcp-web-search-safe.js):只读 web_search / web_fetch(带 SSRF 防护),供智能体查网络用语/资料。
搜索优先复用 DSH 原本的联网搜索(同一个 DeepSeek Anthropic 兼容端点 + web_search 服务端工具,凭据按 config.webSearch.apiKey → 环境变量 DEEPSEEK_API_KEY → DSH 的 .credentials.yaml 顺序解析),因而结果质量与 DSH 内的搜索一致、且不再默认把群聊关键词发给 Bing(Bing RSS 只在 DeepSeek 不可用时兜底)。
抓取仍由桥接自己做 SSRF 校验并限制正文大小;黑话学习默认开启,会把群聊里提取的词条交给所选的搜索后端。
数据出机清单见 LICENSE 的「数据出机清单」,不需要就关掉黑话自动研究(slang.autoResearch)或把 webSearch.provider 设为 bing。
- 会话模型:每个 QQ 会话(私聊/群)对应一个独立的 DSH 会话,统一归组到「QQ 聊天」工作区(不再散落未分组);映射持久化在 state/sessions.json
- 性格定制:QQ 会话默认使用 qq-chat agent preset,reserved2 使用 qq-chat-v2。DSH 0.1.7 起 preset 不再是 ~/.dsh/.agent-presets/<名>/ 目录,而是 plugins/qq-agent-presets/ bundle 生成并插入 profile 的一条 @deepseek-ai/dsh-agent-preset 行(重启 DSH 后生效);人格与默认 DSH 一致(coding agent),仅附加 QQ 场景规则;角色扮演是可选机制——由控制台或管理端设置 state/current-role.json 注入(群友无法更改)
- 本地控制台:桥接自带 Web 控制台 http://127.0.0.1:3100——左侧按操作任务分为「运行总览 / 会话与审批 / 人格与角色 / 二代仿真 / 一代仿真 / 黑话词库 / 令牌与花费 / 访问与安全 / 调试与运维 / 工具参考」十个页面,右上角搜索框可跨页定位任意功能项;支持浅色 / 深色双主题(顶栏一键切换,未选择时跟随系统);切换运行模式(chat / closed-agent / reserved / reserved2)、设置角色、静默开关、查看活动日志、修改管理员/控制台令牌,全部即时生效;访问需要令牌(config.json 的 consoleToken,未配置时自动生成并打印在启动日志;控制台内可手动修改或重新生成)
- 令牌与花费看板:实时显示 AI 的 token 消耗与折算金额(元),可下钻到每个群 / 每个好友 / 每一轮对话(轮次、步数、缓存命中/未命中输入、输出、命中率、花费、峰谷时段),并有按时间的消耗走势图(24 小时 / 3 天 / 7 天 / 30 天,柱子按高峰/空闲着色,一眼看出什么时候烧得凶、哪几个小时是 2 倍价)。累计总量取自 DSH 的 tokenUsage 投影(精确,含桥接启动前的历史),逐轮明细由 assistant/message 的 usage 折叠而来;金额按 DeepSeek 官方价目表折算并区分高峰 / 空闲时段(高峰单价为空闲的 2 倍,已内置中国法定节假日)。详见 docs/guides/TOKEN_USAGE_CONSOLE.md
- 人格(角色扮演)管理:列表点选即可载入查看/编辑提示词,支持新建、保存修改、改名(自动重命名文件)、另存为副本、删除;超过注入上限或含一代专用指令会实时提示
- 两层提示词可分别查看与修改:「仿真提示词」(预设内置,管行为与协议,保存后同步到 DSH,需重启生效)与「人格提示词」(roles/.md,管人设与语气,保存即生效);仿真层保存前做安全不变量校验与自动备份,可一键还原
- 会话初始提示词(二代仿真):在「二代仿真 → 初始提示词」里,可以给某个群或私聊预写一段环境说明(圈子主题、群内氛围、常见话题、忌讳)。AI 只在该会话第一次唤醒时收到一次,之后它已在会话上下文里,不再重复注入。群还没说过话时也能提前写。AI 既读不到也改不了这份设置(仅控制台接口)。会话被重置(「会话映射」里的重置或「重置二代状态」)后,下一次首次唤醒会重新带上它。字数上限见 socialV2.initialPromptMaxChars
- DSH 思考强度:默认 max,可选 high / low。档位从 DSH 实际公布的能力读取,保存后下一条消息生效。控制台会明示:该设置同时写入 DSH 全局默认(~/.dsh/settings.yaml)
- 运行模式:
- chat:白名单群 + 白名单私聊 → qq-chat 安全聊天
- closed-agent:仅私聊 owner(config.json 的 ownerQQ,可在控制台设置)→ 完整工具(默认用 DSH 自己声明的默认 preset,即 standard;可在控制台「closed-agent preset」下拉改为任意 DSH preset),可在 QQ 上操控 DSH
- reserved(一代仿真):仿真群友,观望/活跃/试探/退场状态机,选择性参与、按空格分句发送、主动收尾
- reserved2(二代仿真,运行 setup-dsh.mjs 后 DSH 默认):文本不自动转发,AI 通过 qq_get_unread_messages / qq_send_message 等工具自主看消息、发言、等待、设置唤醒/潜水;DSH 端使用 qq-chat-v2 preset
- 交互增强:
- agent 通过 ask_user_question 提问时,问题会转发到 QQ,回复即自动应答
- agent 请求工具审批时,转发到 QQ,回复「通过」/「拒绝」即可决策
- 支持 DSH 斜杠命令(如 /model)与 /reset(重置会话上下文)
- 群聊引用/回复会解析成「被引用人 + 原文」注入 DSH(如 [引用 Derp:El Psy Kongroo是啥]机关的走狗),让 AI 判断这句话是对谁说的,不会把群友之间引用第三方的对话误当成指向自己;引用机器人自己时会被视为必回
- 图片/表情(含动图 GIF):表情包优先走 QQ 的 CDN 直链,get_image 失败时用 emoji_id 推出 CDN 地址兜底;媒体元数据保留 emoji_id/summary/sub_type 等字段。下载走同一套 SSRF 防护传输,并修掉了「DNS 先返回 IPv6、本机没有 IPv6 出口 → 每次白等 20 秒」的根因(改为 IPv4 优先 + 逐地址故障切换),动图表情不再时好时坏。单条消息上限默认 8MB / 64MP / 5 张(socialV2.media)
- MCP 发送工具支持可选 replyToMessageId,并新增专用 qq_reply 工具:AI 可以先用 qq_get_group_history 拿到真实消息 id,再引用/回复某条消息(是否允许 AI 主动使用由人格/策略决定;桥接会检测发送类工具调用并自动跳过该回合的重复自动转发)
- 一代仿真模式(reserved)下,AI 可以只输出 [SILENT] 表示“潜水/不接话”,桥接会静默不发送
- 一代仿真模式(reserved)按空格分句:AI 用空格表示拆成多条消息;中英文/数字之间的空格也会被当成分条信号,不想分条就不要加空格(reserved2 不适用,分条请用 qq_send_message 数组)
插件化模式(推荐)
本仓库支持两种运行方式,行为一致,但资源占用和可维护性差别很大:
| | 外部进程模式 | 插件化模式(推荐) |
| 运行形态 | 单独一个 Node 进程,通过 HTTP 调 DSH 会话 API | 直接跑在 DSH 进程里 |
| 会话 | 桥接自己管一份会话 id ↔ QQ 号的映射 | DSH 原生 agent,ctx.agents.create() |
| 人格 | 拼在唤醒提示里 | systemPrompt.section + agentPresets.mount() |
| 状态数据 | 每轮重发 | systemPrompt.context,每轮刷新不追加 |
| 启动 | start.bat | DSH 启动时自动加载 |
| 回退 | — | 插件 enabled: false + start.bat |
插件本体在 plugins/qq-agent-host,它把桥接对 DSH 的所有 HTTP 调用用进程内服务重新实现了一遍(lib/adapter.js),所以 src/bridge.js 几乎不用改。
安装:见 plugins/qq-agent-host/README.md。
要点是 profile 的 package.json 里两处都要写——dependencies 和 dsh.profile.bundles,只写一处不生效。
两种模式不要同时开:它们会抢同一个 QQ 号和同一个控制台端口(3100)。
为什么插件化更好
- 不再是遥控器:外部进程模式里,DSH 的会话生命周期、上下文组装、prefix cache 全都在桥接外面重实现了一遍,两边很容易漂移(本项目就漂移过:preset 从来没被挂载,她跑了几周"没有人格的 agent",而且不报错)。
- 缓存友好:人格/角色卡/状态从 user message 搬进 system prompt 和 context 之后,静态部分逐字不变 → 前缀缓存能命中;动态状态每轮刷新 → 不追加进对话历史。
- 少一个进程:不用管端口、文件锁、跨进程会话映射失效。
已知的坑(都踩过了)
插件化会碰到四个 DSH API 的隐含规则,它们全都不报错,写在这里省你几天:
1. 访问 DSH 服务必须先写进 inject —— cordis 里访问未声明的服务会直接抛 cannot get property "x" without inject,不是返回 undefined,?. 救不了。
2. meta.agentPreset 只是标签 —— 它不挂载 preset。真正挂载是
ctx.agentPresets.mount(agentCtx, id),参数必须是 setup 回调里的 agentCtx。
不调的话智能体的 system prompt 里没有任何 preset 内容(没有人格、没有安全规则), 而且不报错。
3. 会话要手动 Workspace.attachSession() —— cwd 相同只决定文件落哪个目录,不建立归属关系;不调的话 GUI 里会话显示成「未命名」。
4. ESM 里没有 require —— 即使在函数体内,调用时也会炸。
前置条件
1. 运行中的 DeepSeek Harness Web(默认 http://127.0.0.1:3080)
2. 运行中的 SnowLuma,且配置好 OneBot WebSocket 与 HTTP API(默认 ws://127.0.0.1:3001 / http://127.0.0.1:3000,accessToken 视配置填写)
3. Node.js ≥ 22.13
安装与配置
npm install # 安装依赖(postinstall 会自动修补 @snowluma/sdk 的 ESM 打包 bug)
复制 config.example.json 为 config.json 后编辑:
Windows CMD 用户请用:copy config.example.json config.json
⚠️ 真实 config.json 与 state/ 不会进入公开仓库,仓库只提供脱敏的 config.example.json 模板。
| 字段 | 说明 |
| dsh.baseUrl | DSH Web 地址,默认 http://127.0.0.1:3080 |
| dsh.provider / dsh.model / dsh.reasoningEffort | DSH 会话使用的模型/推理强度;若你的 DSH 没有示例中的模型,改成 DSH 设置页里可用的模型即可(选择失败只打日志,不阻塞启动) |
| dsh.authToken | DSH launch token(进程启动凭据)。通常应留空:留空时桥接用 ~/.dsh/.credentials.yaml 里持久化的签名密钥离线铸造会话 Cookie,不依赖任何进程期状态(DSH 0.1.7 起启动 token 只存在于内存、日志里那条是上一个进程的陈旧值,换 Cookie 只会 401)。只有显式填了才会优先走 token 交换 |
| dsh.authHeader / dsh.authPrefix | 保留字段,当前新版 DSH 链路使用 Cookie 交换,不再直接发送该鉴权头 |
| snowluma.wsUrl | SnowLuma OneBot WebSocket 地址(如 ws://127.0.0.1:3001) |
| snowluma.httpUrl | OneBot HTTP API 地址(如 http://127.0.0.1:3000);不要填 WebSocket 端口,否则会报 HTTP 426 |
| snowluma.accessToken | OneBot accessToken,未配置留空 |
| snowluma.followDiscoveredEndpoint | 默认 true。换 QQ 账号后 token 与端口都会变,桥接会自愈:重新发现并跟随 SnowLuma 的端点(同时写回 config.json)。想跑第二个实例 / 指向另一份 SnowLuma 安装(多账号、测试)时设为 false —— 那时只自愈 token,绝不动 URL,也不写回 URL。详见「已知行为」 |
| snowluma.silenceWarnMs | 上游静默告警阈值(毫秒),默认 600000(10 分钟)。已连接但这么久没收到任何事件包(含心跳)时打警告;这只是提示,不确定性的判断交给 good |
| snowluma.launcherPath / homeDir | SnowLuma 安装目录(用于 token/端点自发现)。⚠️ 桥接不会启动或停止 SnowLuma(见 LICENSE) |
| agentPreset | QQ 会话使用的 DSH agent preset,默认 qq-chat(改性格见下文) |
| socialV2.agentPreset | reserved2 模式使用的 DSH agent preset,默认 qq-chat-v2 |
| socialV2.initialPromptMaxChars | 会话「初始提示词」(控制台为某个群/私聊预写的环境说明)的字数上限,默认 2000,允许 100~8000。越界值会在启动时收敛并保持原文件不动 |
| socialV2.media | 图片/表情的上限:maxBytes(单条消息总字节,默认 8MB)、maxPixels(单张像素,默认 64MP)、maxCount(单条最多处理几张,默认 5)。越界值启动时收敛 |
| webSearch.provider | 联网搜索后端:auto(默认,优先 DeepSeek、失败回退 Bing)/ deepseek(仅 DeepSeek,无凭据时报错)/ bing |
| webSearch.apiKeyEnv / apiKey | 搜索凭据的环境变量名(默认 DEEPSEEK_API_KEY)/ 直接写死 key(不推荐)。都不设置时读 DSH 的 $DSH_HOME/.credentials.yaml 里同名 ref |
| webSearch.baseURL / model / maxUses / maxResults | 搜索端点(默认 https://api.deepseek.com/anthropic/v1,必须 https 且解析到公网)、模型(默认 deepseek-v4-flash)、单次搜索最多用几次工具、返回条数上限 |
| workspaceTitle | QQ 会话在 DSH 界面中的归组名称,默认「QQ 聊天」 |
| allow.private / allow.groups | 白名单(QQ 号/群号数组);留空且 allowAllWhenEmpty: true 时放行全部 |
| deny. | 黑名单,优先于白名单 |
| ackMessage | 消息投递后的立即回复,空字符串关闭 |
| sendDelayMs | QQ 连续发送间隔,防止触发频率限制 |
| consolePort | 本地控制台端口,默认 3100 |
| consoleToken | 控制台访问令牌;留空时启动自动生成并保存到 state/console-token |
⚠️ allowAllWhenEmpty: true 表示「白名单没填就全部放行」——把智能体接入 QQ 等于把账号控制权交给了模型,建议先填白名单。
DSH 端安装(必做:装 preset + 挂 MCP)
桥接和控制台能跑起来还不够,DSH 端还需要安装两个聊天 preset(qq-chat / qq-chat-v2)并挂载 MCP。单机新装同样必须执行这一步(不是只有「另一台设备」才需要),装完还要重启 DSH:
node scripts/setup-dsh.mjs
全新环境下脚本会把 DSH 默认模式设为 reserved2(二代仿真),并创建本地 state/mode.json 兜底;这样 AI 使用 qq_send_message 等工具收发消息时,DSH 会自动使用 qq-chat-v2 模式。如果本机已存在旧的 state/mode.json 或 DSH 设置值,脚本会保留不覆盖。之后可在桥接控制台(默认 http://127.0.0.1:3100)顶部按钮切换模式,控制台会同时写入 DSH 设置与本地兜底文件。
详细步骤见 docs/guides/DSH_SETUP.md。
完整启动流程(从零开始)
共 6 步。DSH 已安装并运行,缺的是 SnowLuma 本体 + 桥接侧的 DSH 端安装(第 2、3 步最容易漏,漏了 QQ 上会毫无反应):
1. DSH(已运行,无需操作)确认 http://127.0.0.1:3080 能打开即可。
2. 装桥接并复制配置
git clone https://github.com/Kfytm/QQ-DeepSeek-Harness-bridge.git
cd QQ-DeepSeek-Harness-bridge
npm install
Windows CMD 用 copy config.example.json config.json,其他平台用 cp config.example.json config.json。
示例模板里 allow.private / allow.groups 是空数组——空白名单 + allowAllWhenEmpty: false 时桥接不响应任何消息(这是刻意的 fail-closed 默认值)。白名单在第 5 步填。
3. 装 DSH 端(preset + MCP),然后重启 DSH
node scripts/setup-dsh.mjs
装完必须重启 DSH——preset 与 MCP 只在 DSH 启动时加载。
跳过这步桥接不会崩,但群聊会话拿不到 qq-chat preset,桥接会拒绝建会话(有意的安全设计:绝不回退到带 bash/文件工具的默认 preset),表现同样是 QQ 上没反应。
4. 下载并解压 SnowLuma
- 下载:<https://github.com/SnowLuma/SnowLuma/releases/latest> 选 SnowLuma-v<版本>-win-x64.zip(完整版,自带 Node 运行时;Lite 版需本机 Node 22.13+)
- 解压到任意目录(例如 C:\SnowLuma),双击 launcher.bat
5. 首次引导(WebUI)+ 填写桥接配置
- 打开启动日志里的 WebUI 地址(README 写的是 http://localhost:5099,以你启动日志里实际打印的为准)
- 用启动日志中的初始密码登录,按引导:同意条款 → 设置密码 → 接入 QQ 进程(扫码登录)
- 在 WebUI 里配置 OneBot 连接:开启 WebSocket 服务端 和 HTTP API,分别记下端口(默认 WS 3001、HTTP 3000)和 accessToken(若配置了)
- 回到 config.json 填好 snowluma 段,并把 allow.private / allow.groups 换成你自己的 QQ 号 / 群号:
"snowluma": {
"wsUrl": "ws://127.0.0.1:3001",
"httpUrl": "http://127.0.0.1:3000",
"accessToken": "你在 WebUI 里配置的 token(没配置就留空)" }
wsUrl 是 OneBot WebSocket 端口,httpUrl 是 OneBot HTTP API 端口(不要填成同一个 WS 端口,否则 MCP 工具会报 HTTP 426)。
6. 启动桥接
npm start # 前台运行(崩溃不自动重启)
看到 SnowLuma 已连接 即成功;然后 QQ 上给机器人账号发条消息测试。
Windows 想要「崩溃自动重启」请改用 start.bat(见下节)。
运行与运维
npm start # 或双击 start.bat(守护模式:崩溃自动重启,关闭窗口即停止)
⚠️ 重要:
- 桥接只能运行一个实例(有单实例锁,重复启动会被拒绝并提示"已有实例在运行")
- 用 start.bat 启动(守护模式),窗口别关——桥接崩溃会在 5 秒后自动拉起
- 桥接异常/消息无反应时:双击 restart.bat(自动杀旧实例 → 清理锁 → 重新启动守护)
- 重启 DSH 通常不需要动桥接:每 5 秒探活,DSH 不可用期间收到的 QQ 消息在桥接进程内排队(最多 50 条/会话,满后丢最旧项),恢复后尝试补投。桥接进程退出会丢失内存队列;断线期间已经结束的回复暂不保证补发。
修改 config.json / roles/ / state/current-role.json 后,重启桥接生效;修改 dsh/agent-presets/(改完要跑 node scripts/build-agent-preset-patches.mjs 重新生成 bundle patch)或 MCP 配置后,重启 DSH 生效
日志示例:
12:00:01 [bridge] SnowLuma 已连接:ws://127.0.0.1:3001
12:00:02 [bridge] 新会话 private:12345678 -> sess_xxxx
12:00:02 [bridge] 已投递 private:12345678: 你好
12:00:20 [bridge] agent 回复 (private:12345678) 42 字
自测(不需要 SnowLuma / QQ)
离线回归(使用临时目录和模拟服务,不读取真实配置、不发 QQ 消息):
npm run test:audit
reserved2 的 token 开销优化、消息水位协议、升级及回退说明(该专项分析为本地文档,未随仓库发布。)专项离线回归可运行 npm run test:token,已包含在上面的完整回归中。
控制台「令牌与花费」看板的账本与价目表回归(峰谷分时、幂等折叠、基线合并、压缩与容错):
npm run test:tokens # 单元测试
npm run test:console-ui # 离线浏览器回归(含看板渲染与逐轮下钻)
本轮审查与修复明细为本地审计文档,未随仓库发布。升级后会为没有权限元数据的历史映射重建一次 QQ 会话;模式或 preset 变化也会自动重建,避免保留旧权限。旧历史仍在 DSH 中。
验证 DSH 侧链路是否打通(会创建一个独立测试会话,不影响现有会话):
npm run self-test
预期输出:连接成功 → 测试会话创建 → prompt 被接受 → 打印 agent 回复。
目录结构
qq-bridge/
config.example.json # 配置模板(脱敏占位符;真实 config.json 不入库) README.md / README.en.md / RULES.md # 根目录只留这三个文档,其余全在 docs/ docs/ README.md # 文档索引(每篇一句话 + 是否仍然有效) FOLDER_MAP.md # 目录结构与 scripts/ 命名约定 guides/ # 面向使用者/运维:PROJECT_GUIDE / DSH_SETUP / VOICE / TOKEN_USAGE_CONSOLE / CONSOLE-UI-TESTING design/ # 设计与规划:GEN2_SIMULATION_PLAN / SOCIAL_MODE_PLAN / DSH_QQ_TOOLS_PLAN / 真人语感策略 research/ # 调研:SnowLuma功能调研 / QQ消息免打扰 / 表情包能力 / 本地模型选型 audits/ # 审查与优化报告(体检、控制台、token) legacy/ # 历史归档(已被取代或问题已全部修复的旧报告) audio/ # 语音库:把准备好的音频放这里,可作 QQ 语音发出(用法见 docs/README.md 文档索引) dsh/agent-presets/ # qq-chat / qq-chat-v2 的 DSH agent preset 模板 plugins/qq-mode-console # DSH 插件:注册 qq-mode 设置命名空间(仅 host 半,UI 卡片未实现) src/ bridge.js # 主程序 dsh-client.js # Node 版 DSH API 客户端(WS 下行) md-to-plain.js # Markdown → QQ 纯文本 self-test.js # DSH 侧自测 scripts/ # 测试/运维脚本(含 postinstall 的 patch-snowluma-sdk.mjs) patch-snowluma-sdk.mjs # 修补 SDK 的 ESM 打包 bug(postinstall 自动执行) state/ # 运行时数据(不入库)
🎤 独立的「发语音」工具不在这里:它是仓库上一级的 ../voice-tool/(voice-cli.mjs / voice-gui.mjs / public/voice.html / 发语音.cmd)。图形界面双击上一级的「发语音-图形界面.cmd」。
它只依赖 SnowLuma,共享内核仍由本仓库 src/voice-core.js / src/snowluma-conn.js 提供(单一实现)。
cd ../voice-tool
node voice-cli.mjs status # 账号/token 自动发现 + 连通性
node voice-cli.mjs list # 可发的群聊/私聊 + 语音库
node voice-cli.mjs # 交互菜单
node voice-gui.mjs # 图形界面(本地小服务 + 浏览器 UI)
SnowLuma 版本与上游健康(「连上了但收不到消息」)
「注入」在这个技术栈里只发生在一处:SnowLuma 用原生组件挂进 QQ 客户端进程(它自己日志里叫 [Hook],载体是安装目录的 native/snowluma-win32-x64.{dll,node})。本桥接没有任何原生/注入面(依赖全为纯 JS,源码里没有 .dll / .node / FFI / 进程注入),也不启动、不控制 SnowLuma 或 QQ ——所以「QQ 注入失败」不可能是本桥接造成的。但桥接过去无法把这个失败讲清楚,于是它经常被误报成「qq-bridge 的问题」。现在桥接会主动把上游健康报出来。
已知行为:连上了 ≠ 收得到
SnowLuma 的 [Hook] 会退化:它的进程和 OneBot WebSocket 都还活着,但收不到 QQ 侧的数据。
它自己的日志长这样:
WARN [Hook] receive path stale: PID=… UIN=… silentFor=136193ms; reporting good=false
WARN [Hook] process enumeration timed out after 4000ms (worker abandoned)
这时桥接这边一切正常:WebSocket 通、日志打过 SnowLuma 已连接、控制台没有异常,但群里一条消息都不来。
桥接现在对此做两件事:
1. 主信号:每 30 秒一次的 meta_event/heartbeat 载荷里的 status.good(SnowLuma 对「QQ → 原生 hook → 我」这条接收链路的自评,静默约 105 秒后翻 false)。心跳是 SnowLuma 自己的定时器无条件发的,所以这条信号免费且及时,且不需要较新的运行时。
兜底:每 60 秒问一次 get_status({online, good})。加分项:bot_status(账号会话上下线,SnowLuma 1.14.20 才真的有)+「距最近一个事件包的时长」(只能证明进程/WS 还活着,不能证明收得到 QQ 数据 —— 心跳会一直来)。
⚠️ 不要用 get_login_info 取 good:它只返回 {user_id, nickname},而 SDK 不校验 data 载荷 ⇒ 不会报错、good 永远是 null、这条判断变成永不触发的死代码。(本功能的第一版实现正是这么写错的,回归里已加反面断言钉住。)
2. 判定退化时:日志打明确指向 SnowLuma [Hook] 的警告、控制台「运行总览」出现 SnowLuma 上游 卡片、「访问与安全」页出现告警条,GET /api/status 的 snowluma 字段给出 degraded / good / hint。
排查顺序:控制台看 SnowLuma 上游 卡片 → 看 SnowLuma 的 logs/snowluma-*.log 里的 [Hook] 行 → 再考虑升级 SnowLuma(升级不换原生组件,见下)。
版本要求
| 组件 | 要求 | 说明 |
| SnowLuma 运行时(QQ 网关本体) | 1.14.20 已实测(1.14.9 亦兼容) | 实测记录(2026-10-03,SnowLuma 1.14.20 + QQ 在线):包 SHA256 与官方发布摘要一致;心跳 status.good=true/interval=30000;桥接走完「首次 1006 → 自愈 WS token → 已连接」;收到群消息(lastPacketKind: message/group);完整往返成功(唤醒 → DSH 建会话挂 qq-chat-v2 → 工具发送 1/1 条 → 实际发出两条回复)。 ⚠️ 升级运行时不会更换注入用的原生组件:实测 native/snowluma-win32-x64.{dll,node} 与 websocket-win32-x64.node 在 1.14.9 与 1.14.20 之间逐字节完全相同(三方对比:官方 1.14.9 包 == 本机安装 == 官方 1.14.20 包),变的只有 index.mjs(+414 KB)等 JS 层。所以升级能带来 OneBot 事件流水线、收发与新区块链路上的修复(bot_status、markdown 带 text、事件种类订阅更全),但不会修「原生 hook 挂不上 QQ」这类问题 —— 那种要走 SnowLuma 自己的排查。升级 SnowLuma 是使用者自己的事:本桥接只探测、不部署(见 LICENSE 与 RULES.md) |
| @snowluma/sdk / @snowluma/mcp(本仓库依赖) | 已钉 ^1.14.20 | 客户端库;OneBot v11 是稳定契约,新库可连旧运行时(本项目实测:库 1.14.20 + 运行时 1.14.9 可正常收发) |
| bot_status(账号上下线) | 1.14.20 有;1.14.9 没有 | 逐一核对的只有这两个版本:bot_status 在 1.14.9 的 index.mjs 里出现 0 次、1.14.20 里 3 次(带 sub_type/user_id)。具体从哪个版本引入未逐版核对。桥接对老运行时优雅降级:订阅不到就只靠心跳 good 与 get_status |
已知行为:HTTP token 与 WS token 是两个不同的值
SnowLuma 同一个账号的 HTTP token(默认 3000 端口)与 WebSocket token(默认 3001 端口)并不相同 (实测;snowluma.accessToken 里通常只能存一个 —— 存的是 HTTP 那个)。因此:
- 桥接每次启动的首次 WS 连接几乎必然失败一次(日志里 SnowLuma 连接断开(code=1006) → SnowLuma 首次连接未成功(将在后台自动重连并尝试自愈 WS token)),随后自愈重新发现 WS token、重连成功,才打出 SnowLuma 已连接。这条序列是正常的,不是故障。
- 真正会一直连不上的情况:自愈无从下手 —— snowluma.homeDir 没配 / 被移动 / 读不到 SnowLuma 的 config/onebot_<QQ号>.json,或权限不足。这时日志里会有 token 自愈失败,而 QQ 侧表现为完全收不到消息。先确认 homeDir 指向真正的 SnowLuma 安装目录。
已知行为:端点自愈会覆盖你写的 wsUrl
换 QQ 账号后 OneBot 端口与 token 都会变,所以桥接在连不上时会自愈:重新发现 SnowLuma 的端点并 写回 config.json。这在主场景下是对的,但它不区分「配置里是个过期值」和「你就是要指向别处」——想跑第二个实例或指向另一份 SnowLuma 安装时会连错账号。要关掉:
"snowluma": { "followDiscoveredEndpoint": false }
关掉后只自愈 token(换账号仍能恢复),绝不动 URL、也不写回 URL。
已知限制
- 智能体回复在回合结束时一次性发送(不做流式逐字转发);回复超过 socialV2.send.maxMessageChars(默认 500 字,可用 social.maxReplyChars 调整)自动按句读/URL 边界分段
- 图片及部分表情可以通过安全下载接入多模态模型;语音/视频以及无法取得图片字节的消息仍使用占位文本
- 发语音是支持的(独立工具的图形界面 / 命令行 / AI 工具,见 docs/README.md 的文档索引);但收到的语音目前只显示占位文本,不做语音转写入上下文
- AI 发语音默认关闭(socialV2.voice.enabled 与 tools.sendVoice 默认 false):先把音频放进 audio/,再到控制台打开开关;你自己发语音不受这个开关影响
- 智能体的 Markdown 回复会转成纯文本(链接保留 文字 (url) 形式)
- @snowluma/sdk 的 npm 发布版存在 ESM 扩展名 bug,本仓库通过 postinstall 补丁修复(见 scripts/patch-snowluma-sdk.mjs)
合规提醒
SnowLuma 是独立第三方项目,与腾讯/QQ 无隶属关系,仅供学习与技术研究;使用前请阅读其 EULA 与《QQ 用户协议》。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
Kfytm/QQ-DeepSeek-Harness-bridge
把 QQ 好友和群消息接入 agent 会话并回传回复
相关插件 · 平台集成
智能体工程课walkinglabs/learn-harness-engineering
一门项目制课程,教你搭建环境、管理状态、做验证、加控制机制,让 AI 编程智能体稳定干活。
★ 21,155已汉化DSH
71.7
视频翻译配音liuzhao1225/YouDub-webui
开源视频本地化工作台:导入视频,识别、翻译并逐句克隆原声,生成字幕和配音成片。
★ 5,792已汉化Cordis
71.4
失败经验库Ikalus1988/MisakaNet
📚 零依赖、由 git 支撑的微课程库,让 AI 智能体异步分享并检索经过验证的调试经验。| https://misakanet.org
★ 550已汉化DSH
71.2
IM 机器人接入xmanrui/dsh-im
让 DeepSeek Harness 触手可及 Connecting DeepSeek Harness
★ 3,170已汉化DeepSeek 客户端
71
桌面智能体工作区freestylefly/wesight
WeSight 是开源桌面控制台,面向本地 AI 智能体。它帮你安装或复用 Claude…
★ 933已汉化MCP
71
中国平台研究引擎Jesseovo/last30days-skill-cn
给 AI Agent 的中文互联网研究 Skill:检索 8 大平台最近 30…
★ 2,041已汉化DSH
70.7
这是你的仓库?认领条目