🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

QQ 智能体桥 2710586329/dsh-qq

DeepSeek Harnessv1.2.8
27105863294 天前有提交已提供中文文档扫描:含敏感能力

把 QQ 接成 DeepSeek Harness 的一个前端:私聊、群聊都能和同一个 Agent…

36.4/100
综合分
5
GitHub 星标
8,313
npm 周下载
—/5
用户评分

数据截至 2026/10/31 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add dsh-qq-onebot
npm 源 · dsh-qq-onebot 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · platform
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 4 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-qq-onebot @ 1.2.8
✓Node 引擎要求 >=20.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/31 07:33:49

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 118 处,展示前 89 条)
  • 写入本地文件
    src/index.js:78
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    src/index.js:83
    appendFileSync(join(dir, `dsh-qq-${day}.log`), `${localStamp(now)} [${level}] ${args.map(formatArg).join(' ')}\n`, 'utf8')
  • 读取本地文件
    src/index.js:134
    lastMtimeMs = statSync(file).mtimeMs
  • 发起网络请求
    client.js:42
    const r = await fetch(base + '/status', { credentials: 'same-origin' })
  • 发起网络请求
    client.js:63
    const jget = async (p) => asJson(await fetch((await resolveBase()) + p, { credentials: 'same-origin' }))
  • 发起网络请求
    client.js:64
    const jpost = async (p, body) => asJson(await fetch((await resolveBase()) + p, {
  • 执行外部命令来自配置/变量
    src/inbound.js:45
    child = spawn(py, model ? [script, wavPath, model] : [script, wavPath], { windowsHide: true })
  • 写入本地文件
    src/inbound.js:432
    await writeFile(p, buf)
  • 发起网络请求
    src/inbound.js:438
    const res = await fetch(url, { signal: AbortSignal.timeout(20000) })
  • 写入本地文件
    src/inbound.js:445
    await writeFile(p, buf)
  • 读取本地文件
    src/inbound.js:455
    const names = (await readdir(cacheDir)).filter((n) => n.startsWith('qq-'))
  • 删除本地文件
    src/inbound.js:470
    await unlink(item.p).catch(() => {})
  • 写入本地文件
    src/inbound.js:869
    await mkdir(cacheDir, { recursive: true }).catch(() => {})
  • 写入本地文件
    src/inbound.js:878
    await mkdir(cacheDir, { recursive: true }).catch(() => {})
  • 写入本地文件
    src/inbound.js:906
    await mkdir(cacheDir, { recursive: true }).catch(() => {})
  • 写入本地文件
    src/inbound.js:908
    await writeFile(wavPath, Buffer.from(String(res.base64), 'base64'))
  • 写入本地文件
    src/chatlog.js:68
    await mkdir(join(dir, sub), { recursive: true })
  • 写入本地文件
    src/chatlog.js:69
    await appendFile(file, line, 'utf8')
  • 读取本地文件
    src/pin.js:33
    if (!file || !existsSync(file)) return { file, lines: [] }
  • 读取本地文件
    src/pin.js:35
    return { file, lines: readFileSync(file, 'utf8').replace(/ /g, '\n').split('\n') }
  • 写入本地文件
    src/pin.js:53
    writeFileSync(file, text, 'utf8')
  • 读取本地文件
    src/reading.js:52
    text = readFileSync(file, 'utf8')
  • 读取本地文件
    src/reading.js:75
    names = readdirSync(path)
  • 写入本地文件
    src/learning.js:102
    mkdirSync(dirname(file), { recursive: true })
  • 读取本地文件
    src/learning.js:103
    let text = existsSync(file) ? readFileSync(file, 'utf8') : ''
  • 读取本地文件
    src/learning.js:103
    let text = existsSync(file) ? readFileSync(file, 'utf8') : ''
  • 写入本地文件
    src/learning.js:125
    writeFileSync(file, text.replace('## 待确认\n', `## 待确认\n\n${block}`), 'utf8')
  • 写入本地文件
    src/learning.js:136
    writeFileSync(file, head + block + body + tail, 'utf8')
  • 读取本地文件
    src/learning.js:151
    if (!root || !existsSync(root)) return 0
  • 读取本地文件
    src/learning.js:161
    dirs = readdirSync(root, { withFileTypes: true })
  • 读取本地文件
    src/learning.js:172
    if (!existsSync(f)) continue
  • 读取本地文件
    src/learning.js:174
    hits.push(...findStuck(parseChatLog(readFileSync(f, 'utf8'), selfId)))
  • 写入本地文件
    src/context-guard.js:115
    await mkdir(dir, { recursive: true })
  • 写入本地文件
    src/context-guard.js:130
    await writeFile(path, body, 'utf8')
  • 读取本地文件
    src/tools.js:165
    return { ok: true, file, text: readFileSync(file, 'utf8') }
  • 读取本地文件
    src/tools.js:286
    const names = readdirSync(dir, { withFileTypes: true })
  • 读取本地文件
    src/tools.js:299
    const text = readFileSync(file, 'utf8')
  • 写入本地文件
    src/tools.js:310
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    src/tools.js:311
    writeFileSync(file, body, 'utf8')
  • 写入本地文件
    src/tools.js:315
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    src/tools.js:316
    appendFileSync(file, body.endsWith('\n') ? body : `${body}\n`, 'utf8')
  • 写入本地文件
    src/tools.js:601
    await mkdir(cacheDir, { recursive: true }).catch(() => {})
  • 读取本地文件
    src/tools.js:1139
    if (!existsSync(face.path)) {
  • 读取本地文件
    src/tools.js:1268
    dirs = readdirSync(root, { withFileTypes: true })
  • 读取本地文件
    src/tools.js:1283
    if (!existsSync(f)) continue
  • 写入本地文件
    src/settings.js:115
    mkdirSync(dir, { recursive: true })
  • 读取本地文件
    src/settings.js:138
    if (existsSync(file)) return { ok: true, created: false, file }
  • 写入本地文件
    src/settings.js:151
    writeFileSync(file, `${JSON.stringify(skeleton, null, 2)}\n`, 'utf8')
  • 读取本地文件
    src/settings.js:164
    if (!existsSync(file)) return {}
  • 读取本地文件
    src/settings.js:165
    const raw = JSON.parse(readFileSync(file, 'utf8'))
  • 写入本地文件
    src/settings.js:194
    writeFileSync(file, `${JSON.stringify(clean, null, 2)}\n`, 'utf8')
  • 读取本地文件
    src/settings.js:236
    if (!existsSync(file)) throw new Error('文件不存在')
  • 读取本地文件
    src/settings.js:237
    const text = readFileSync(file, 'utf8').trim()
  • 写入本地文件
    src/settings.js:242
    mkdirSync(dirname(cacheFile), { recursive: true })
  • 写入本地文件
    src/settings.js:243
    writeFileSync(cacheFile, text, 'utf8')
  • 读取本地文件
    src/settings.js:253
    if (!String(config?.persona ?? '').trim() && cacheFile && existsSync(cacheFile)) {
  • 写入本地文件
    src/settings.js:303
    writeFileSync(join(homeDirOf(ctx, logger), 'api-state.txt'), `${new Date().toISOString()} ok=${ok} ${detail || ''}\n`, 'utf8')
  • 读取本地文件
    src/client-selfcheck.js:125
    pkgText = readFileSync(join(root, 'package.json'), 'utf8')
  • 读取本地文件
    src/client-selfcheck.js:126
    clientText = readFileSync(join(root, 'client.js'), 'utf8')
  • 读取本地文件
    src/workspace.js:35
    await access(path)
  • 写入本地文件
    src/workspace.js:112
    await mkdir(join(dir, '记忆区', '日记'), { recursive: true })
  • 写入本地文件
    src/workspace.js:113
    await mkdir(join(dir, '记忆区', '项目'), { recursive: true })
  • 写入本地文件
    src/workspace.js:114
    await mkdir(join(dir, '记忆区', '大纲'), { recursive: true })
  • 写入本地文件
    src/workspace.js:115
    await mkdir(join(dir, '记忆区', '收藏'), { recursive: true })
  • 写入本地文件
    src/workspace.js:116
    await mkdir(join(dir, '记忆区', '压缩记忆'), { recursive: true })
  • 写入本地文件
    src/workspace.js:120
    await writeFile(agentsPath, agentsTemplate(info), 'utf8')
  • 读取本地文件
    src/workspace.js:142
    const text = await readFile(agentsPath, 'utf8')
  • 读取本地文件
    src/inject-state.js:23
    if (file && existsSync(file)) {
  • 读取本地文件
    src/inject-state.js:24
    const raw = JSON.parse(readFileSync(file, 'utf8'))
  • 写入本地文件
    src/inject-state.js:57
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    src/inject-state.js:58
    writeFileSync(file, JSON.stringify(map, null, 2), 'utf8')
  • 读取本地文件
    src/session-state.js:21
    if (file && existsSync(file)) {
  • 读取本地文件
    src/session-state.js:22
    const raw = JSON.parse(readFileSync(file, 'utf8'))
  • 写入本地文件
    src/session-state.js:37
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    src/session-state.js:38
    writeFileSync(file, JSON.stringify(data, null, 2), 'utf8')
  • 写入本地文件
    src/faces.js:52
    mkdirSync(facesDir(config), { recursive: true })
  • 写入本地文件
    src/faces.js:53
    copyFileSync(file, target)
  • 读取本地文件
    src/faces.js:59
    if (!existsSync(file)) return []
  • 读取本地文件
    src/faces.js:61
    const parsed = JSON.parse(readFileSync(file, 'utf8'))
  • 写入本地文件
    src/faces.js:71
    mkdirSync(dirname(file), { recursive: true })
  • 写入本地文件
    src/faces.js:72
    writeFileSync(file, JSON.stringify(list.slice(-MAX_KEEP), null, 2), 'utf8')
  • 读取本地文件
    src/faces.js:129
    if (!existsSync(file)) return { ok: false, added: false, total: facesCount(config), reason: `文件不存在:${file}` }
  • 读取本地文件
    src/door.js:51
    const raw = readFileSync(join(dir, LEDGER), 'utf8')
  • 写入本地文件
    src/door.js:61
    writeFileSync(join(dir, LEDGER), `${JSON.stringify(obj, null, 2)}\n`, 'utf8')
  • 读取本地文件
    src/door.js:82
    if (existsSync(dir)) {
  • 读取本地文件
    src/door.js:84
    for (const name of readdirSync(dir)) {
  • 读取本地文件
    src/door.js:92
    st = statSync(full)
  • 读取本地文件
    src/door.js:130
    st = statSync(full)
  • 读取本地文件
    src/meme.js:64
    if (!existsSync(dbFile)) {
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-llm@deepseek-ai/dsh-session@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
v1.2.82026-10-30
· 这次修的是什么 issue #3:白名单写成字符串会把入站处理整个炸掉。 privateAllow / groupAllow 少写一对括号("123456" 而不是 ["123456"])就抛 TypeError: (c.privateAllow ?? []).map is not a function,而抛出点在消息处理深处 —— 用户只看到「发了消息没反应」,看不出是自己写错了配置。 四处一起改掉,并加了 17 条写法用例 + 四处源码护栏: | 文件 | 原来 | 现在 | | --- | --- | --- | | src/inbound.js | (config.privateAllow ?? []).map(...) | listOf(config.privateAllow) | | src/inbound.js | (config.groupAllow ?? []).map(...) | listOf(config.groupAllow) | | src/index.js | 启动白名单检查同一写法(这条 warn 自己会先崩) | listOf(...) | | src/reading.js | privateAllow?.[0] —— 字符串会取到第一个字符,夜里读书问询发给不存在的号 | listOf(...)[0] | | src/wake.js | Array.isArray(...) ? ... : [] —— 字符串静默变空,冷会话复活失效 | listOf(...) | 新增 listOf()(src/config.js):数组照收;字符串按英文逗号 / 中文逗号 / 空白拆; 数字当单个号;null、undefined、对象、布尔一律当空。 issue #2 最后剩下的两条: · 骨架 settings.json 的模板没跟上 1.2.2 的新默认值 —— 模板只有 wsUrl / accessToken / selfId 三行,而 privateMode 已经默认 allowlist,白名单空着就谁都不理; 可这份骨架存在的场景恰好是「设置页没上场」。现在补了 "privateAllow": ["你的QQ号"]。 · 启动那句「私聊白名单是空的」warn 说了「去设置 → QQ 里填」,但设置页没上场的人没有这条路。 现在带上 settings.json 的完整路径。 · test/ 没进 files、也没有 scripts.test —— CHANGELOG 里说的护栏装包的人既看不到也跑不到。 现在 files 加上 test,npm test 可跑(test/run-all.mjs 逐个 node 跑, 绕开 node --test 在部分 Windows 环境上的 spawn EPERM)。 · 版本号为什么从 1.2.4 跳到 1.2.8 1.2.5–1.2.7 只在本地打过包、没发到 npm(当时 registry 上还是 1.2.2),这里一次补齐。 · 验证 全量 32 个测试文件逐个跑过,全部通过(含 patch-defaults / private-gate / wake / memo-write / door-read 等既有护栏)。
v1.2.22026-10-27
· 升级前先看这一条 私聊默认不再放行陌生人。 privateMode 默认从 open 改成 allowlist,而 privateAllow 默认是空的 —— 也就是说装好之后,谁私聊它都不理,得自己把 QQ 号填进 privateAllow: privateMode: 'allowlist' privateAllow: - '你的QQ号' 这是故意的:能私聊这个号的人,就能指挥跑在你这台机器上的 agent(读文件、跑命令都算)。配置里显式写过 privateMode 的不受影响。 · 修复 · 1.2.1 的 wsUrl 修复没到用户手上(#2 第三次实测):只改了 src/config.js 的 schema 默认值,漏了插件包自带的 cordis.patch.yml —— 那里还写着 wsUrl: 'ws://127.0.0.1:3001',而 patch 在 schema 之后应用,「没配就不去连」那段永远走不到,装完 1.2.1 重启照样看到 ECONNREFUSED 127.0.0.1:3001。现在 patch 里 wsUrl: ''。 · 新增护栏 test/patch-defaults.test.mjs:把 cordis.patch.yml 的每个键和 new ConfigSchema({}) 的默认值逐个比对,两处漂移就红。这个坑就是「代码对了、默认值在 patch 层被填回去」,光改代码藏不住下一次。 · 文档 · README 加「谁能跟它说话(⚠️ 默认只认白名单)」一节,含从 1.2.1 及更早升级的说明。 · docs/configuration.md 的 privateMode / privateAllow 默认值与说明改正。 升级:npm i dsh-qq-onebot@1.2.2(或 dsh plugin --profile add dsh-qq-onebot@1.2.2),然后重启宿主。
v1.2.12026-10-27
· 修了什么 · 没配 wsUrl 就不再偷连 3001(#2 第 1 条):wsUrl 的默认值原先是 ws://127.0.0.1:3001,于是「没配就先 warn 一句」成了死代码——空骨架照样连,连不上就无限重连,还让人以为是自己配错了。现在留空就完全不连接;填好保存约 0.8 秒自动接上,不用重启宿主。 · 断线重连不再刷屏(#2 第 2 条):退避 5s→10s→20s→40s→60s,每轮打两行 warn,一小时约 120 行,重要日志全被埋了。改成状态机:第一次断打一行、带原因;之后静默累计;持续断开时每小时最多一句汇总;连回来补一句「断了多久、重试几次」。 · README 里的日志文件名是错的(#2 第 3 条):实际落在 /logs/dsh-qq-.log(一天一个),不是 dsh-qq-debug.log。文档已改,并补上「找不到就在磁盘上搜 dsh-qq-2026-*.log」。 · inject 里写了废弃包名会先喊一声:dsh.client.inject 出现裸的 dsh-client-runtime / dsh-client(老文档抄来的那种)时,自检报 deprecated-inject 并给出正确写法。只按名字比对、不去 resolve,所以不会因为本机没装而误报。 · 新增 · 配置 memeDb(或环境变量 DSH_QQ_MEME_DB):查梗词库的路径不再写死本机路径。两个都空时 qq_meme 如实回「没配词库」,不编出处。 · 顺带带上上一批攒着没发的:qq_images(按消息 id 把图取回来看)、qq_meme(查梗)、合并转发真的展开、工具发出去的话也写进聊天流水、思考里的零碎不再被当成正文发出去。 · 升级 先退出 DSH,再: dsh plugin --profile desktop remove dsh-qq-onebot dsh plugin --profile desktop add dsh-qq-onebot (profile 名换成你实际在用的那个;真正连 NapCat 的通常是 qqbot。) 一台 NapCat 的 WS 只该被一个 Agent 连着。和 dsh-onebot-hub 同时装会抢同一条消息,先卸掉一个。 npm:
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
dsh-qq-onebot 把 QQ 接成 DeepSeek Harness 的一个前端:私聊、群聊都能和同一个智能体聊;语音听得懂、图片看得见,还能戳一戳、贴表情、点赞。 底层走 OneBot v11 协议,默认搭配 NapCat。不用公网 IP、不用备案、不用企业主体。 ⚠️ 先说清楚:NapCat 属于「QQ 协议端」,跑在你自己登录的 QQ 客户端上,不符合腾讯的用户协议。用小号、别和常用号同一台设备、别发垃圾消息,账号有被风控或封禁的可能。插件只负责把 OneBot 消息转给智能体,协议端本身怎么合规、怎么稳,由你自己负责。场景能用 QQ 官方机器人 的,优先用官方的。 📢 这是个公开仓库:MIT,随便取用、改、再发。作者是个人不是公司,没有客服、没有 SLA,能回 issue 但不保证秒回。 提 issue 或贴日志之前,请先把 token、真实 QQ 号、本机路径 抹掉——发出去就收不回来了。 🔌 一台 NapCat 的 WS 只该被一个智能体连着:再装一个 dsh-onebot-hub 之类的插件会抢同一条消息,两边都只收到一半(日志里每行出现两次就是证据)。细节见 和别的 OneBot 插件共存。
展开完整文档(README 剩余 1 节)
dsh-qq-onebot

把 QQ 接成 DeepSeek Harness 的一个前端:私聊、群聊都能和同一个智能体聊;语音听得懂、图片看得见,还能戳一戳、贴表情、点赞。

底层走 OneBot v11 协议,默认搭配 NapCat。不用公网 IP、不用备案、不用企业主体。

⚠️ 先说清楚:NapCat 属于「QQ 协议端」,跑在你自己登录的 QQ 客户端上,不符合腾讯的用户协议。用小号、别和常用号同一台设备、别发垃圾消息,账号有被风控或封禁的可能。插件只负责把 OneBot 消息转给智能体,协议端本身怎么合规、怎么稳,由你自己负责。场景能用 QQ 官方机器人 的,优先用官方的。

📢 这是个公开仓库:MIT,随便取用、改、再发。作者是个人不是公司,没有客服、没有 SLA,能回 issue 但不保证秒回。
提 issue 或贴日志之前,请先把 token、真实 QQ 号、本机路径 抹掉——发出去就收不回来了。

🔌 一台 NapCat 的 WS 只该被一个智能体连着:再装一个 dsh-onebot-hub 之类的插件会抢同一条消息,两边都只收到一半(日志里每行出现两次就是证据)。细节见 和别的 OneBot 插件共存。

你的 QQ 客户端 ←→ NapCat(协议端,注入 QQ) ↕ OneBot v11 WebSocket dsh-qq-onebot 插件(跑在 DSH 宿主进程里) ↕ 进程内 ctx.agents 你的智能体(你自己的预设、工具、记忆)

它能干什么

| 特性 | 说明 |
| 一个 QQ 会话 = 一个持久智能体会话 | 有历史、有记忆,不会聊两句就忘 |
| 每个会话一个独立工作目录 | 自带 AGENTS.md 和 记忆区/,彼此看不见,也碰不到宿主自己的私人记忆 |
| 语音转文字 | 本地 vosk 离线识别,不联网、不上传,结果直接进对话 |
| 群里默认 @ 才理 | 被某人 @ 过一次后 60 秒内不用再 @ 也能接(窗口按「群 + 人」算,别人蹭不到) |
| 分得清谁在跟自己说话 | 引用算叫我、[SILENT] 可以不接、同类机器人单独开关、跳过的群消息也进上下文 |
| 不会把自己聊丢 | 窗口快满时补回聊天规矩;再撑满就换新会话,旧的最近聊天先落盘再接上 |
| 十个互动工具 | qq_poke / qq_react / qq_like / qq_send_image / qq_send_file / qq_face / qq_mface / qq_reply / qq_send / qq_recall |
| 问答能转回 QQ | ask_user_question 抛的问题会变成一条 QQ 消息,回个数字就行 |

三步跑通

1. 装 NapCat,登录小号

下载 Windows 一键版(NapCat.Shell.Windows.OneKey.zip),解压运行 NapCatInstaller.exe——它装一份独立的 QQ,不会碰你现有的 QQ。登录小号,打开 WebUI(默认 http://127.0.0.1:6099),在「网络配置」里新建 WebSocket 服务器:

| 项 | 填什么 |
| 地址 | 127.0.0.1 |
| 端口 | 3001(被占用就换一个,记住它) |
| Token | 自己编一串,比如 my-token-2026 |
| 消息格式 | array(重要,别选 string) |

⚠️ NapCat 的 QQ 和你桌面的 QQ 是两套程序。 重装 / 重启 NapCat 时别用 Get-Process -Name QQ | Stop-Process 一把全杀——那会把自己正在用的 QQ 也踢下线。按进程路径挑,或者在 NapCat 自己的托盘 / WebUI 里退。

2. 装插件

先把 DSH 完全退出(托盘也要退掉,插件文件被占用会装到一半失败)。

从 npm 装(推荐)
dsh plugin --profile desktop add dsh-qq-onebot

或从 Releases 下载 dsh-qq-onebot-1.0.0.tgz
dsh plugin --profile desktop add "D:\下载\dsh-qq-onebot-1.0.0.tgz"

不想用打好的包,也可以直接装源码目录(git clone,或下 Source code (zip) 解压):

dsh plugin --profile desktop add "D:\dsh-qq-onebot-1.0.0"

三个容易踩的坑:

- 必须用 DSH 自带的 dsh,不能 npx @deepseek-ai/dsh。 desktop 这份配置档由桌面端独占管理,npx 会直接报 profile "desktop" is managed exclusively by the Electron application。PATH 里找不到 dsh 就用完整路径: & "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop add "D:\dsh-qq-onebot-1.0.0"

- --profile 不能省,省了直接报 error: --profile <name> is required。
- 写绝对路径,相对路径是按你敲命令那一刻的当前目录算的。

装完它会自动写进 $DSH_HOME/profiles/desktop/package.json 的 dsh.profile.bundles(默认就启用),启动后在 设置 → 插件 里能看到。想确认装没装上:

dsh plugin --profile desktop why dsh-qq-onebot

3. 重启 DSH,填连接信息

重新打开 DSH → 设置 → QQ,把 wsUrl 和 accessToken 填进去就行(34 项都在那个面板上,改完即时生效)。

设置里没看到 QQ 这一项? 别慌,不用等修——两条路都能救。

① 直接写设置文件(不用重启宿主):$DSH_HOME/dsh-qq/settings.json,Windows 一般是 C:\Users\<用户名>\.dsh\dsh-qq\settings.json。插件第一次启动时会在那儿放一份带说明的骨架,改成:

{
"wsUrl": "ws://127.0.0.1:3001",
"accessToken": "my-token-2026"
}

保存后约 800ms 自动生效。

② 看日志找原因:插件启动时会自检浏览器半端,日志里有 客户端半端自检通过:id=... 就是好的;如果是 客户端半端自检失败(client-id-mismatch / no-inject ...),那句话本身就说明了设置页为什么没出现。日志在 <workspaceRoot>/logs/dsh-qq-<日期>.log(workspaceRoot 默认是进程 cwd)。也可以看 $DSH_HOME/dsh-qq/ 下有没有东西——一个文件都没有,通常就是浏览器半端根本没被加载。

想直接写 profile 配置也可以,改 $DSH_HOME/profiles/desktop/cordis.patch.yml 里 dsh-qq 那一段(整段替换,不是往下叠):

- id: dsh-qq
name: dsh-qq
config:
wsUrl: ws://127.0.0.1:3001
accessToken: my-token-2026
cwd: D:/my-agent # Agent 工作目录,不填用进程 cwd workspaceRoot: D:/my-agent/QQ会话 # 每个会话的独立目录(不填用 <cwd>/会话) persona: | # 它是谁、什么脾气(不填就是个普通助手)你叫小满,是个住在电脑里的助手。说话简短、带点懒,被夸的时候会嘴硬。
provider: deepseek-official # 想指定模型时才填 model: deepseek-flash

日志里看到 已连接 ws://127.0.0.1:3001 就成了,给小号发条私聊试试。

最常改的几个配置

| 配置 | 默认 | 什么时候改 |
| persona | 空 | 想让它有名字、有性格、有称呼 |
| ownerIds / ownerLabel | [] / 主人 | 想在群里认出主人,优先响应 |
| requireMention | true | 群里不 @ 也想让它接话 |
| groupMode / privateMode | open / allowlist | 只想在指定群 / 指定人那里开;私聊默认只认白名单 |
| groupReplyGapMs | 20000 | 觉得回得太密(防刷屏、防风控) |
| rateLimitPerMin | 12 | 每个会话每分钟最多发几条 |
| contextGuard | true | 不想要自动换会话就关掉 |
| chatLog | true | 不想记原始流水就关掉 |

全部 50+ 项见 docs/configuration.md。

想深入看哪块

| 想看 | 去哪 |
| 它是谁、群里怎么认主人、上下文守卫、认时间、引用与沉默判断 | docs/persona.md |
| 所有配置项的默认值和含义 | docs/configuration.md |
| 长期跑、看门狗、定时写日记、开机自启 | docs/deployment.md |
| 改代码、跑测试、目录结构 | docs/development.md |
| 测试怎么做、踩坑记录、排障 | TESTING.md · tools/README.md |

在 DSH 设置里调它

不想手改 YAML 的话,设置 → QQ 那一节就是个面板,34 项都能点着改:

| 分组 | 能改什么 |
| 状态 | 连接点、登录账号、家目录、设置文件;「重连」「重新读一遍」两个按钮 |
| 连接 | wsUrl、accessToken、selfId |
| 它人设 | persona、personaFile、ownerLabel |
| 补充提示 | directPrompt、groupPrompt |
| 记忆区与工作区 | workspaceRoot、isolateByPeer |
| 群里怎么说话 | requireMention、groupWakeMs、groupReplyGapMs、groupMinGapMs、rateLimitPerMin、historyLimit |
| 谁能跟它说话 | privateMode、privateAllow、ownerIds、groupMode、groupAllow |
| 同类机器人 | botChatEnabled、peerBots |
| 其它 | debug、textChunkLimit、splitDelayMs |

改完立刻生效(wsUrl / accessToken 要点「重连」;reconnectMs / selfId / workspaceRoot / isolateByPeer / asrPython 要重启宿主)。设置写在 $DSH_HOME/dsh-qq/settings.json,只覆盖你显式设过的键,清空就回默认——不动 profile 的 cordis.patch.yml。

谁能跟它说话(⚠️ 默认只认白名单)

私聊默认 privateMode: allowlist,而 privateAllow 默认是空的——装好之后,谁私聊它都不理。这是故意的:能私聊这个号的人,就能指挥跑在你机器上的智能体(读文件、跑命令都算)。

要放人进来:

privateMode: 'allowlist'
privateAllow:
- '你的QQ号'

想谁都聊就写 privateMode: 'open'(风险自担),彻底关掉私聊写 disabled。

从 1.2.1 及更早升级上来:以前默认是 open。配置里显式写过 privateMode 的不受影响;没写过的,升级后私聊会静默不回应——启动日志里那句「私聊白名单(privateAllow)是空的」就是在说这件事。

同一个 QQ 号不能两处同时登录(会互相抢消息),所以桌面 / Web GUI 那份配 uiOnly: true:只提供设置页,不连 NapCat;真正干活的是 --profile qqbot 那个宿主。

和别的 OneBot 插件共存

一台 NapCat 的 WebSocket 一般只该被一个智能体连着。社区的 dsh-onebot-hub 之类也连同一台 NapCat 时,两边都会收到同一条消息、各自回一遍——表现是「回复错乱」「一条消息两份回复」,不是任何一边的 bug。

只留一个:

dsh plugin --profile desktop remove dsh-onebot-hub
再确认 profile 的 package.json 里 dsh.profile.bundles 已经没有它

搬家到 dsh-qq 时顺手清一下旧家的状态目录(不清也不影响,只是留着占地方):$DSH_HOME 下旧插件自己的目录。dsh-qq 的家是 $DSH_HOME/dsh-qq/,卸载插件时这个目录不会自动删,设置、日志、覆盖表都在里面,留着下次重装能直接用。

常见问题

设置里没有「QQ」这一项 浏览器半端没被加载。最常见两个原因:插件版本太老(1.1.1 之前的 dsh.client.inject 写了宿主要么没有的包),或者装的是改过包名却没跟着改 client.js 里 id 的版本。现在不用等更新:连上 NapCat 只看设置文件的话,改 $DSH_HOME/dsh-qq/settings.json 就够了(见上面第 3 步的兜底),QQ 本身不依赖设置页。插件启动日志里搜 客户端半端自检,它会把原因写在里面。

连不上 / 一直重连 先看 wsUrl 填没填——1.2.1 起留空就完全不连接,日志里会明说「wsUrl 还没填」,不会再对着 3001 空转。填了还连不上,看 NapCat 的 WebUI 里 WS 服务器是不是「启用」、端口对不对、Token 两边是否一模一样。日志里 与 NapCat 断开 那一行会带原因,之后的重试不再逐条刷屏,恢复时会补一句「断了多久、重试几次」。

配置看着没问题,但就是发不出去 NapCat 的 WS 和 HTTP 是两个不同的 token,填错不报错,只是安静地发不出去。另外别让 PowerShell 把配置写出 BOM——一个看不见的字节能让所有判断歪掉。

收得到消息但智能体不回 群里先确认 @ 了;私聊看 privateMode 是不是 allowlist 却没把自己加进 privateAllow;都没问题就看 DSH 那边是不是还在跑(一轮本身要时间)。

消息重复发两遍 一般是两个 qqbot 宿主同时连着 NapCat(日志里每行出现两次)。用 tools/start-bot.cmd 起,它会先掐掉旧的宿主。

语音没转出文字 先确认 asrPython -c "import vosk" 能过,再手动跑一次 <asrPython> <asrScript> <音频文件> [模型目录]。模型目录必须是纯 ASCII 路径(vosk 的 C++ 层不认中文路径)。

记忆存在哪 / 会不会和宿主串 默认 isolateByPeer: true:私聊在 <workspaceRoot>/会话/私聊_<QQ号>/,群在 群_<群号>/,第一次用自动铺好 AGENTS.md + 记忆区/。想共用宿主目录就设 false。

能不能群发 / 主动推消息 不做群发。冷场时会主动开口:会话安静满 proactiveSilentMs(默认 20 分钟)之后,它有机会自己说一句,说不说由它判断(不想说就 [SILENT])。有静默时段、每天次数上限、以及「连着几次没人接就闭嘴」三道闸,见 配置项。想定时定点让它说话,就在 DSH 里给它挂个定时任务。

装之前

| 需要 | 版本 |
| DeepSeek Harness | ≥ 0.2.0 |
| Node.js | ≥ 18(插件零编译依赖) |
| NapCat | v4.x(推荐 NapCat.Shell.Windows.OneKey 独立版) |
| 一个 QQ 小号 | 别用主号,真的 |
| (可选)vosk + numpy | 只有想让语音自动转文字才要;不装也行,语音会标成「没听清」 |

放出去之前,逐条确认

接入只是第一天,第二天才见真章。装之前先想清楚三件事:你准备长期维护它吗?不想养了会好好收尾吗?它在公共场合出现时,你负责吗?——群里其他人看到的是一个会说人话的账号,不是一个玩具。

1. 别把 token 提交进仓库。 NapCat 的 access token、模型 API key 都放配置里,别写进代码或 commit。
2. 别手改 profile 的 package.json / pnpm-lock.yaml / node_modules。 装插件用 dsh plugin --profile <名字> add,而且要在 DSH 完全退出时做。手改 lockfile 会让 pnpm 重排包、撞到被占用的文件,最坏是整个桌面端报「web 缺失或文件损坏」(这个坑真踩过,最后靠卸载重装才救回来)。
3. 杀进程按路径挑,别 Get-Process -Name QQ | Stop-Process 一把全杀。 NapCat 的 QQ 和你桌面的 QQ 是两套程序,一把杀会把自己踢下线。
4. 它会花你的 token。 群里每句话都是一次模型调用,先用 rateLimitPerMin、groupReplyGapMs、groupWakeMs 把节奏压住。
5. 它会读写你指定的工作区。 workspaceRoot 指到哪,它就能在哪读写文件。
6. 上下文会被压缩或换会话。 重要的事要落盘,别指望它一直记得。
7. 人多的地方它可能说错话。 先用 groupMode: 'allowlist' 缩到测试群,稳了再放开。
8. 账号风控不是插件的问题。 频繁换设备登录、短时间大量发消息都可能被判风险,遇到了去手机 QQ 做安全恢复。

出问题了去哪问

先在 Issues 提一个:带上插件版本、DSH 版本、日志里相关的几行、复现步骤。别贴 token 和真实 QQ 号。

日志在哪:<workspaceRoot>/logs/dsh-qq-<日期>.log(一天一个文件,workspaceRoot 默认是跑插件的那个进程的 cwd,桌面端通常在 $DSH_HOME/profiles/<profile>/logs/)。找不到就在磁盘上搜 dsh-qq-2026-*.log——老版本的文档里写过的 dsh-qq-debug.log 已经不存在了。

也可以直接把报错原文、api-state.txt、日志尾部丢给你的 AI 助手,通常能判断出是配置、连接还是插件本身的问题。

许可证

MIT,见 LICENSE。用出问题、账号被风控,作者不承担责任——协议端本来就是灰色地带,用之前想清楚。
预览分享卡片长什么样 ↓2710586329/dsh-qq
分享 · DeepSeek Harness Hub
2710586329/dsh-qq

把 QQ 接成 DeepSeek Harness 的一个前端:私聊、群聊都能和同一个 Agent…

这是你的仓库?认领条目

回到顶部