@fengye_nobug/dsh-qq-bot 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/DESIGN.md +570 -1
- package/README.md +61 -605
- package/client/client.js +162 -10
- package/cordis.patch.yml +3 -1
- package/index.js +426 -3
- package/onebot.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## v0.2.9 — 2026-11-02
|
|
4
|
+
|
|
5
|
+
- **每个会话的轮数上限会真正压缩**:`maxRecentTurns`(默认 40,填 0 不限制)在 QQ 叫醒的一轮结束后,会请宿主把更早的内容压掉。以前这次调用少传了宿主要求的取消信号,压缩还没开始就失败,失败只留在日志里,会话仍会一直变长。现在会等这一轮结束并空闲下来,再按宿主的参数去压缩。宿主没有压缩能力时仍然跳过,不影响收发。
|
|
6
|
+
|
|
7
|
+
## 0.2.8
|
|
8
|
+
|
|
9
|
+
- 设置卡片:群白名单在连上 NapCat 时可显示群名和头像;查不到时仍只显示群号
|
|
10
|
+
- 设置卡片:新增「主人 QQ」(`ownerWhitelist`);留空则 QQ 侧白名单指令不生效
|
|
11
|
+
- 主人 QQ 指令:`白名单 加/删/列表`(需先配置主人 QQ)
|
|
12
|
+
|
|
13
|
+
|
|
3
14
|
## v0.2.7 — 2026-10-31
|
|
4
15
|
|
|
5
16
|
- **一键更新装完会自己重启 dsh-web**:安装成功后按面板原来的办法重启,不再把「请自己重启」当成正常结果。Linux 上如果是 systemd 拉起来的,会读本进程的 cgroup,大约 3 秒后只重启认出来的那个服务。服务在用户实例里时用 `systemctl --user`,不会重启整个登录。以前没把 cgroup 读进来,服务里一点更新就直接说面板没有重启。`systemd-run` 没权限或失败时,改为按当前的 node 命令行再拉起一次。两条都做不到,才用白话请你自己重启 dsh-web,并提醒刷新页面、登录口令可能会变。安装超过三分钟、但磁盘上已经是要装的版本时,同样会改包名并重启。进程还在,或没有确认装上时,仍然不会重启。
|
|
6
17
|
- **检查和安装的提示改成短句**:设置卡片不再把「优先从 npm…三分钟…」那段长说明当作主要提示。检查按钮旁边写「只检查,不安装。」有更新按钮时另写「装完会重启面板。请刷新;登录口令可能会变。不改 NapCat。」检查结果是「有新版本 x.y.z(现在是 a.b.c),在 npm 上。这里只检查,还没安装。」或「在 GitHub 上」。安装成功是「已经从 npm 装上 vX.Y.Z,面板即将重启。」或「已经从 GitHub 装上 vX.Y.Z,面板即将重启。」自动重启失败时才请自己重启,并写明这次是从 npm 还是从 GitHub 装上的。
|
|
18
|
+
- **发版改为推 main**:改 `package.json` 的 `version` 并推到 `main` 后,Actions 在 npm 上还没有 `@fengye_nobug/dsh-qq-bot` 的这一版时才 `npm publish`,并补 tag `vX.Y.Z` 和 GitHub Release。同一版本已在 registry 里则跳过。`NPM_TOKEN` 放在仓库 Actions Secrets,不写进仓库。
|
|
7
19
|
|
|
8
20
|
## v0.2.6 — 2026-10-30
|
|
9
21
|
|
package/DESIGN.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
> `<TARGET_SESSION_ID>`,<TARGET_WORKSPACE> 工作区)
|
|
7
7
|
> 接到 QQ,**保留原有上下文**(同一个 sessionId,不新建会话)。
|
|
8
8
|
|
|
9
|
-
>
|
|
9
|
+
> 用法以 README 为准。唤醒、出站、图片、NapCat 和设置全表见 §10。`sessionScope` 默认仍是 `single`(一个 `targetSessionId`)。设成 `perGroup` 时,每个群、每个私聊各有一条会话。出站在一轮里钉住唤醒时的目的地,`lastDestination` 只在没有这根钉时作回落。设置接口在本机可直接用;经反向代理时要已登录的 DSH 面板会话,且来源主机名和请求的 `Host` 一致。空的 `requestRejection` 不算已登录。
|
|
10
10
|
|
|
11
11
|
## 1. 与现有 QQ 插件的根本区别
|
|
12
12
|
|
|
@@ -77,6 +77,7 @@ ensureTargetAgent():
|
|
|
77
77
|
| `wakeProbability` | number | `0.05` | 非昵称消息的唤醒概率 |
|
|
78
78
|
| `whitelist` | string[] | `[]` | 私聊 openid 白名单(空=全拒,fail-closed) |
|
|
79
79
|
| `groupWhitelist` | string[] | `[]` | 群 openid 白名单 |
|
|
80
|
+
| `ownerWhitelist` | string[] | `[]` | 主人 QQ。空 = 不能在 QQ 里改群白名单 |
|
|
80
81
|
| `attachRecentChat` / `recentChatLimit` | boolean / number | `true` / `8` | 未唤醒聊天是否作为摘要附给下次唤醒、条数 |
|
|
81
82
|
| `replyMaxChars` | number | `1500` | 出站字数上限(下限 100);**超过就不发送** |
|
|
82
83
|
| `replyMode` | string | `'marker'` | 出站闸门:只发 `[QQ]…[/QQ]` 块内的内容;`'always'` = 整轮都发 |
|
|
@@ -87,6 +88,8 @@ ensureTargetAgent():
|
|
|
87
88
|
| `atOnlyInGroup` | boolean | `false` | 群里只有被 @ 才处理 |
|
|
88
89
|
| `stripMarkdown` | boolean | `true` | 出站去掉 Markdown 标记 |
|
|
89
90
|
|
|
91
|
+
主人用整句「白名单」命令改 `groupWhitelist` 时,发生在白名单闸门之前,所以新群还没被允许也能加。不是主人、主人名单为空、或句子里夹了别的字,都不改配置。调试工具模拟的入站不执行这条命令。保存走和设置卡片相同的用户层(0.2 为 `qq-bridge-settings.json`),不改 composition config 本身。
|
|
92
|
+
|
|
90
93
|
Config(`cordis.patch.yml`)另有:`agentPreset`、`statePath`、`debugTools`、`heartbeatTimeoutMs`。
|
|
91
94
|
|
|
92
95
|
## 6. 设置界面(client 半)
|
|
@@ -182,3 +185,569 @@ qq_bridge_napcat
|
|
|
182
185
|
并把 QQ 消息当**不可信输入**(群友可注入),需要动手就停下等人。理由见 `HANDOVER.md` §14。
|
|
183
186
|
- 形态:**host 平面 profile bundle**(像 dsh-cron 一样 `dsh plugin add` 安装)+ **一个浏览器半**
|
|
184
187
|
(`dsh.client`,只为了那张设置卡片),不是 agent preset。
|
|
188
|
+
|
|
189
|
+
## 10. 使用细节
|
|
190
|
+
|
|
191
|
+
产品说明(README)只保留安装、配置、主人命令和发版。下面是原先写在 README 里的操作细节。
|
|
192
|
+
|
|
193
|
+
### 升级
|
|
194
|
+
|
|
195
|
+
四条路。装完都要让 dsh-web 重新加载插件:
|
|
196
|
+
|
|
197
|
+
1. 设置卡片里先点「检查更新」。有新正式版时出现「更新到 vX.Y.Z」。点下去优先执行 `dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@X.Y.Z`,走这台机器上 pnpm 已经配置的 registry,然后重启面板。装完会把 profile、`$DSH_HOME/cordis.patch.yml`(写了 `qq-bridge` 才改)和插件补丁里 `qq-bridge` 的 `name` 改成磁盘上的包名,`config` 不动。设置还在,不改 NapCat。重启后刷新页面;登录口令可能会变。
|
|
198
|
+
2. 自己执行同一条命令。npm 上已经发出的是 `@fengye_nobug/dsh-qq-bot@0.2.5`。自己装不会自动改 profile 里的旧 `name`。
|
|
199
|
+
3. 指定 GitHub tag:`dsh plugin --profile web add github:yunfengnobug/dsh-qq-bot#vX.Y.Z`
|
|
200
|
+
4. 卸掉再装,然后重启。
|
|
201
|
+
|
|
202
|
+
无作用域名字发不出去。第 1 步如果发现 registry 里没有 `@fengye_nobug/dsh-qq-bot` 的这一版,会改从 GitHub 下载该标签的官方源码包:先 `github.com`,连不上再试同一标签的 `codeload.github.com`,然后交给 `dsh plugin add`。v0.2.4 及更早标签里的 `package.json` 仍是无作用域名,这种源码包仍然能装,Cordis 的 `name` 也保持 `dsh-qq-bot`。v0.2.5 的 `package.json` 已是作用域包,但补丁里的 `name` 还是旧的,所以会 404。插件不写死第三方镜像。registry 里已经有比当前新的正式版时,检查和安装不再访问 GitHub。
|
|
203
|
+
|
|
204
|
+
从 0.2.5 点「更新」时,执行的还是那一版里的安装程序,不会改 profile,也不会自己重启。若设置页变成 HTTP 404,把 profile 和插件目录里 `qq-bridge` 的 `name` 改成 `@fengye_nobug/dsh-qq-bot`,再重启 dsh-web。这一版(0.2.7)自己跑一键更新时会改包名,并在装完后自动重启。
|
|
205
|
+
|
|
206
|
+
「检查更新」和「启动时检查一次」只询问,不会安装。已经是最新正式版时不出现更新按钮。不会装比当前更旧的版本,也不装草稿和预发布。连不上、安装失败、面板没能重启,都用白话说明。安装已经成功但自动重启没有安排上时,新版本在磁盘上,需要自己重启 dsh-web。包名已经改好的话,重启后设置页会回来。
|
|
207
|
+
|
|
208
|
+
安装超过三分钟没有结束时,会停掉卡住的 dsh 和它带起来的 pnpm,并用白话说明。profile 里的 `.plugin-manager/run.json` 只在记录的进程已经不在、或记录读不懂时删掉;进程还在就不会再开一次安装。超时后如果要装的版本已经在磁盘上,包名仍会改成实际安装的包,然后自动重启 dsh-web。没有确认装上时,可以再点一次「更新」,这时不会重启。
|
|
209
|
+
|
|
210
|
+
在 Linux 上,进程由 systemd 拉起并且能认出具体服务时,大约 3 秒后只重启那个服务。服务挂在用户实例(`user@`)下面时,用 `systemctl --user`,不会重启整个登录。认不出单独服务,或 `systemd-run` 没有权限、执行失败时,改为按当前的 node 命令行再拉起一次。两条都做不到,才提示自己重启 dsh-web。没有被 systemd 拉起时,响应发出后按当前命令行再拉起一次。这次更新不改 NapCat 的配置,也不去停它。插件自己拉起的 NapCat 会随 dsh-web 退出而结束;已经在外面跑着的 NapCat 继续运行。
|
|
211
|
+
|
|
212
|
+
想让 npm 走镜像,把 npm / pnpm 的 registry 配成你自己的镜像。插件只在没读到配置时用 `https://registry.npmjs.org/`。
|
|
213
|
+
|
|
214
|
+
### 不要和官方插件一起挂
|
|
215
|
+
|
|
216
|
+
官方仓库是 [sj244/dsh-qq-bridge](https://github.com/sj244/dsh-qq-bridge)。两边的 Cordis id 都是 `qq-bridge`,连的是同一个 OneBot。一起装会抢这条连接,消息对不上。只用其中一个。
|
|
217
|
+
|
|
218
|
+
从官方换成这个 fork 时,包名一般是 `dsh-qq-bridge`。下面没有真实路径,profile 用你自己的:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
dsh plugin --profile web remove dsh-qq-bridge
|
|
222
|
+
dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@0.2.5
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
连不上 npm 时,把上面第二条换成 `dsh plugin --profile web add github:yunfengnobug/dsh-qq-bot#v0.2.5`。
|
|
226
|
+
|
|
227
|
+
然后重启 dsh-web。profile 里原来按 `id: qq-bridge` 写的覆盖可以留着。若其中有 `name`,写成 `@fengye_nobug/dsh-qq-bot`,不要留着 `dsh-qq-bot` 或 `dsh-qq-bridge`。`list` 里如果包名不是 `dsh-qq-bridge`,卸你看到的那一个官方包,不要卸别的插件。
|
|
228
|
+
|
|
229
|
+
### 开发期
|
|
230
|
+
|
|
231
|
+
改本仓库代码时用本地目录:
|
|
232
|
+
|
|
233
|
+
```powershell
|
|
234
|
+
dsh plugin --profile web add "<REPO>" # <REPO> = 本仓库在你机器上的绝对路径
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`dsh plugin add <本地目录>` 装的是符号链接。Node 按真实路径找依赖,DSH 的包在 `~/.dsh/profiles/node_modules/@deepseek-ai`,从工作区向上找不到,插件 `import` 会失败。在插件目录挂一个 junction:
|
|
238
|
+
|
|
239
|
+
```powershell
|
|
240
|
+
New-Item -ItemType Junction `
|
|
241
|
+
-Path "<REPO>\node_modules\@deepseek-ai" `
|
|
242
|
+
-Target "$env:USERPROFILE\.dsh\profiles\node_modules\@deepseek-ai"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
每次改完插件代码,重启 `dsh web` 再刷新页面。
|
|
246
|
+
|
|
247
|
+
## 用 npm 安装
|
|
248
|
+
|
|
249
|
+
`dsh plugin` 是 pnpm 的包装。安装和一键更新都用作用域包名。npm 上已经发出的是 0.2.5:
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@0.2.5
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
无作用域的 `dsh-qq-bot` 发不出去,npm 认为它和已有的 `dsh-qqbot` 太像。不要写成 `dsh plugin add dsh-qq-bot@版本`。
|
|
256
|
+
|
|
257
|
+
registry 里还没有要装的那一版时(源码 0.2.7 尚未发布),改用 GitHub:
|
|
258
|
+
|
|
259
|
+
```sh
|
|
260
|
+
dsh plugin --profile web add github:yunfengnobug/dsh-qq-bot#v0.2.5
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
一键更新的顺利路径是 npm,不再要求打开 github.com。某台机器上的 `dsh plugin --help` 如果只认 `github:`,就继续用 Release tag。想用镜像,配置这台机器的 npm / pnpm registry,不要等插件内置一个镜像地址。
|
|
264
|
+
|
|
265
|
+
发版见 [README 的更新](./README.md#更新)。
|
|
266
|
+
|
|
267
|
+
`files` 是白名单。Node 要求 `>=22`(`engines`)。`@deepseek-ai/*` 仍是 `peerDependencies`,由 DSH 的 profile 提供,不要改成普通 dependencies。
|
|
268
|
+
|
|
269
|
+
## 插件市场
|
|
270
|
+
|
|
271
|
+
[awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 里已经有上游 [sj244/dsh-qq-bridge](https://github.com/sj244/dsh-qq-bridge)。本 fork 还没进索引。安装不必等收录,用上面的 `@fengye_nobug/dsh-qq-bot` 即可。
|
|
272
|
+
|
|
273
|
+
要申请收录,按[贡献说明](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/blob/main/contributing.md)做:
|
|
274
|
+
|
|
275
|
+
1. 在 GitHub 仓库页面加上 topic `dsh-plugin`。这个 topic 不会跟着 git 走,要在网页上加。
|
|
276
|
+
2. 把本仓库的 [`docs/awesome-dsh-plugin-entry.yml`](./docs/awesome-dsh-plugin-entry.yml) 复制成对方仓库的 `data/plugins/yunfengnobug__dsh-qq-bot.yml`,提一个 PR。不要改上游那条。一个 PR 最多 3 条。对方的 README 由脚本生成,不要手改。
|
|
277
|
+
3. 描述只写插件实际做的事。`repository` 必须指回本仓库(`package.json` 里已经是了)。yml 里不要手写 `npm:`,市场自己从 registry 认。
|
|
278
|
+
|
|
279
|
+
npm 上已经有 `@fengye_nobug/dsh-qq-bot`(目前是 0.2.5)。无作用域名发不出去。发到 npm 之后,市场才能显示下载量;不发也能收录。
|
|
280
|
+
|
|
281
|
+
用着有问题,到 [Issues](https://github.com/yunfengnobug/dsh-qq-bot/issues/new/choose) 选模板:**连不上**、**不回**、**发不出**。不要在 Issue 里贴真实 QQ 号、群号、token 或服务器地址。
|
|
282
|
+
|
|
283
|
+
## 唤醒策略
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
QQ 消息 ─┬─ 不在白名单 ─────────────────→ 丢弃(白名单为空则全部拒绝)
|
|
287
|
+
├─ 在会话跟随窗口内 ───────────→ 唤醒(reason: listening)
|
|
288
|
+
├─ 群里 + atOnlyInGroup + 没 @ ─→ 丢弃
|
|
289
|
+
├─ 被 @ / 命中昵称 / 被戳我 ───→ 唤醒
|
|
290
|
+
├─ random() < wakeProbability ──→ 唤醒(默认 5%)
|
|
291
|
+
└─ 其余 ──────────────────────→ 只记录(零模型调用)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- **白名单默认全关**:`whitelist`(私聊)、`groupWhitelist`(群)为空数组时,任何来源都不能唤醒会话。
|
|
295
|
+
|
|
296
|
+
### 在 QQ 里改群白名单
|
|
297
|
+
|
|
298
|
+
先在设置卡片的「主人 QQ」里写好主人 QQ(`ownerWhitelist`,默认 `[]`)。**留空 = 谁都不能用这些命令。** 不是主人时,这句话按普通聊天处理:不改配置,也不回复“你不是管理员”。
|
|
299
|
+
|
|
300
|
+
只有**整句**是命令才算(可以在前面加一个 `/`)。多出来的字、或夹在别的话里,都不当命令。
|
|
301
|
+
|
|
302
|
+
| 在哪 | 整句 | 作用 |
|
|
303
|
+
|---|---|---|
|
|
304
|
+
| 群里 | `白名单 加`、`白名单 加本群` | 把当前群加进 `groupWhitelist`(已经在里面则不动) |
|
|
305
|
+
| 群里 | `白名单 删`、`白名单 删本群` | 把当前群移出 |
|
|
306
|
+
| 群或私聊 | `白名单 加 群号`、`白名单 删 群号` | 按写明的群号加或删 |
|
|
307
|
+
| 群或私聊 | `白名单`、`白名单 列表` | 回复现在有几个群 |
|
|
308
|
+
| 私聊 | `白名单 加`(没写群号) | 不改配置,提示要写群号 |
|
|
309
|
+
|
|
310
|
+
群还没在白名单里时,主人仍能在那个群里加。别人的消息照旧丢掉。改完和设置卡片同一条保存:DSH 0.2 写入 `$DSH_HOME/qq-bridge-settings.json`(比 `cordis.patch.yml` 优先,马上生效,重启还在)。更早的宿主写入 settings 里的用户层。调试工具模拟的消息不会改白名单。
|
|
311
|
+
|
|
312
|
+
- **必须唤醒**:被 @、正文命中 `nicknames`、被戳一戳(`[戳一戳](戳的是我)`)。
|
|
313
|
+
- **概率唤醒**:`wakeProbability`,默认 `0.05`。
|
|
314
|
+
- **只记录**:未唤醒的消息进入“最近聊天缓冲”,不调用模型。缓冲按目的地分开,每个群、每个私聊各自最多 200 条。
|
|
315
|
+
下一次真正唤醒时,只取**这个目的地**最近 `recentChatLimit` 条(默认 8)拼成 `[QQ 未唤醒期间聊天记录] … [记录结束]`,
|
|
316
|
+
附在该条消息前。别的群或私聊的闲聊不会跟进来。摘要表头写明最近几条,并注明没进摘要的条数(“更早的 N 条已省略”);
|
|
317
|
+
该条数按**真实丢失量**算,即这一目的地缓冲快照里的条目数减去摘要行数,窗口外的旧条目也计入。
|
|
318
|
+
该条数大于 99 时不写数字,改写成“更早的大量消息已省略”(不让巨大的数字进上下文)。
|
|
319
|
+
投递成功后,清掉的是**这个目的地**在唤醒那一刻缓冲里的条目,避免重复占用 token。
|
|
320
|
+
没进摘要的旧条目也一起删掉,不会留到下一次唤醒。别的目的地的缓冲不动。
|
|
321
|
+
只有投递期间新到的、同一个目的地的消息留在缓冲里,等下一次唤醒。
|
|
322
|
+
- **投递方式**(`delivery`):`auto`(默认)在目标会话正忙时插话(`steer`,下一个 step 边界塞入),空闲时排队;
|
|
323
|
+
`followup` 永远排成独立一轮;`steer` 总是插话。正忙时排进去的那一轮,到它真正开始时仍算 QQ 唤醒:
|
|
324
|
+
当前轮先结束、或中间夹进一条 DSH 里的消息,都不会把这次排队的归属清掉。
|
|
325
|
+
- **群内二次闸门**:`atOnlyInGroup: true` 时,群消息没有被 @ 就完全不处理。
|
|
326
|
+
|
|
327
|
+
### 会话跟随
|
|
328
|
+
|
|
329
|
+
真人聊天不会每句话都 @。第一次被 @ 之后,是否继续收听由模型自己决定:
|
|
330
|
+
|
|
331
|
+
| 调用 | 作用 |
|
|
332
|
+
|---|---|
|
|
333
|
+
| `qq_bridge_listen({ minutes: 5 })` | 开一个窗口(默认 5 分钟,上限 60) |
|
|
334
|
+
| 窗口打开期间 | **同一目的地**(同一个群 / 同一个私聊)的普通消息也唤醒目标会话,不必再 @ |
|
|
335
|
+
| `qq_bridge_listen({ off: true })` | 关掉当前目的地的窗口;到期也会自动失效 |
|
|
336
|
+
|
|
337
|
+
- 每个目的地各有一个窗口,同时开着互不影响。给甲群开窗不会关掉乙群已经开着的窗口。
|
|
338
|
+
- `qq_bridge_listen({ off: true })` 只关当前这一轮钉住的目的地,别的目的地的窗口还在。
|
|
339
|
+
- 窗口只影响那个目的地,别的群 / 私聊照旧要 @ 或点名才会叫醒。
|
|
340
|
+
- 警告:窗口打开时,每条消息都会唤醒,产生实际费用。默认 5 分钟、上限 60 分钟。
|
|
341
|
+
这条规则已经写进注入的系统提示(“别无脑常开”)。
|
|
342
|
+
- 窗口检查排在 `atOnlyInGroup` 之前。否则群里开启跟随后,消息会被“没 @ 就丢弃”掐断。
|
|
343
|
+
|
|
344
|
+
## 接入 QQ(OneBot / NapCat)
|
|
345
|
+
|
|
346
|
+
插件作为 **WebSocket 客户端**主动连接 NapCat 的“正向 WebSocket”服务,收发都走这一条连接。
|
|
347
|
+
|
|
348
|
+
```yaml
|
|
349
|
+
# 在 profile cordis.patch.yml 里按 id 覆盖,或直接在设置界面改
|
|
350
|
+
- id: qq-bridge
|
|
351
|
+
config:
|
|
352
|
+
onebotUrl: 'ws://127.0.0.1:3001'
|
|
353
|
+
accessTokenEnv: 'NAPCAT_ACCESS_TOKEN'
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
- `onebotUrl` **留空 = 完全不启用传输**(默认值,安全)。
|
|
357
|
+
- NapCat 侧要开一个 **WebSocket 服务器**(不是 HTTP 服务器),并允许本机连接。
|
|
358
|
+
- `selfId` 留空即可:连上后插件自动调用 `get_login_info` 取得自身的 QQ 号,用于过滤自身消息,
|
|
359
|
+
防止回复死循环,同时忽略 `message_sent` 事件。`get_login_info` 返回之前,事件里如果已经带了 `self_id`,
|
|
360
|
+
先用它过滤自身消息、识别 @ 和戳一戳。事件没带 `self_id` 的那一小段仍可能漏过;想完全避开就填上 `selfId`。
|
|
361
|
+
- 两次建连叠在一起时只保留后一次。先发起、后拿到 token 的那次不会拆掉新连接,也不会留下多余的 socket。
|
|
362
|
+
|
|
363
|
+
### 配置 access_token(不得明文写入配置)
|
|
364
|
+
|
|
365
|
+
`accessTokenEnv` 填的是**凭据引用名**(一个环境变量名)。token 本身由 DSH 的凭据服务解析,
|
|
366
|
+
不写入 `cordis.patch.yml`,也不进入日志或状态文件。token 按以下优先级逐层查找:
|
|
367
|
+
|
|
368
|
+
1. 进程环境变量:`$env:NAPCAT_ACCESS_TOKEN = '…'`
|
|
369
|
+
2. `~/.dsh/.credentials.yaml` 的 `refs:` 段
|
|
370
|
+
3. 工作目录下的 `.env`
|
|
371
|
+
|
|
372
|
+
> 解析在每次建连时重新执行,不缓存。轮换 token 不需要重启。
|
|
373
|
+
|
|
374
|
+
## NapCat 一键托管
|
|
375
|
+
|
|
376
|
+
插件可以按需从官方 GitHub Releases 下载、校验、解包、写配置、启停 NapCat,不预先打包 NapCat。
|
|
377
|
+
|
|
378
|
+
**NapCat 已经自己部署好**(包括 Docker、另一台机器上的实例)时,下面的下载、解包和安装器可以整段跳过。仍要让 `onebotUrl` 和 `onebotPort` 跟那边的 OneBot 正向 WebSocket 一致,并在「NapCat 安装目录」里填实际路径。默认的 `$DSH_HOME/napcat` 在这种装法里往往不存在;目录不存在时「打开文件夹」不会去猜路径,只提示你改设置。
|
|
379
|
+
|
|
380
|
+
在会话里调用 `qq_bridge_napcat` 工具,按以下顺序执行:
|
|
381
|
+
|
|
382
|
+
| 步骤 | 调用 | 说明 |
|
|
383
|
+
|---|---|---|
|
|
384
|
+
| 1 | `{ action: "status" }` | 查看 QQ 是否已安装、NapCat 是否存在、代理是否配置、端口是否正确 |
|
|
385
|
+
| 2 | `{ action: "download" }` | 取最新发行包 → **sha256 校验** → 解包到 `napcatInstallDir` |
|
|
386
|
+
| 3 | **手动运行一次安装器** | 见下文;这一步插件无法执行 |
|
|
387
|
+
| 4 | `{ action: "configure" }` | 写 `config/onebot11.json`,把正向 WS 端口和 token 配好(**不用打开 WebUI**) |
|
|
388
|
+
| 5 | `{ action: "launch" }` | 启动 NapCat(首次仍需扫码登录) |
|
|
389
|
+
|
|
390
|
+
安装目录可以是 OneKey 根,也可以是 OneKey 根下的 `NapCat.Shell` 子目录;两种布局都能识别。
|
|
391
|
+
|
|
392
|
+
**启动优先走官方 launcher**:目录里存在 `launcher-win10.bat`(或 `launcher.bat`)时,插件用
|
|
393
|
+
`cmd /c` 调用这个 bat 脚本,工作目录设为 `NapCat.Shell`。脚本自己读注册表找 QQ 路径,非管理员时用
|
|
394
|
+
UAC 自我提权,因此**可能弹出 UAC 窗口**,需要在系统弹窗里点“是”。
|
|
395
|
+
没有 launcher 脚本时,插件直接调用 `NapCatWinBootMain.exe`,并按官方参数顺序传入
|
|
396
|
+
QQ 路径与 `NapCatWinBootHook.dll`;此时 QQ 路径取不到会明确报错,先手动安装并登录一次 QQ NT 即可。
|
|
397
|
+
|
|
398
|
+
**第 3 步必须手动执行的原因**:
|
|
399
|
+
|
|
400
|
+
- `NapCatInstaller.exe` 是**交互式控制台程序**。DSH 的 `SubprocessStdio.stdin` 只支持
|
|
401
|
+
`'ignore' | 'pipe' | { data }`,没有 `inherit`,插件无法转发键盘输入。
|
|
402
|
+
- **QQ 首次登录必须手动完成**(扫码或密码)。NapCat 官方要求登录过一次之后,才能用 QQ 号快速登录。
|
|
403
|
+
|
|
404
|
+
**下载需要代理**:插件内的下载不读取 `HTTPS_PROXY`(Node 的 `fetch` 不识别代理),因此:
|
|
405
|
+
|
|
406
|
+
- 有代理时,把 `downloadProxy` 设成代理地址,例如 `http://127.0.0.1:7890`;
|
|
407
|
+
- `downloadProxy` 留空时,依次读取 `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY` 环境变量,都没有才直连;
|
|
408
|
+
- 下载优先使用系统自带的 **`curl.exe`**(可以带 `-x` 代理);没有 subprocess 服务时才退回内置 `fetch`。
|
|
409
|
+
|
|
410
|
+
**安全边界**:
|
|
411
|
+
|
|
412
|
+
- **插件从不自动下载或执行任何内容**。所有动作都由 `qq_bridge_napcat` 的显式调用触发。
|
|
413
|
+
- 每次下载都按 GitHub Release 自带的 `asset.digest` 做 **sha256 校验**,校验不符时删除文件并报错。
|
|
414
|
+
- ZIP 用**纯 Node 实现**解包,不依赖 7z / Expand-Archive,并拒绝 `..`、绝对路径等 zip-slip 条目。
|
|
415
|
+
- 默认安装位是 `$DSH_HOME/napcat`,不污染项目目录。
|
|
416
|
+
- 警告:**NapCat 是第三方 QQ 客户端**,存在账号风控风险,是否启用由用户决定。NapCat 由 DSH 的 subprocess
|
|
417
|
+
服务托管,**插件卸载或 DSH 退出时会一并结束**。
|
|
418
|
+
|
|
419
|
+
## 出站:三种模式 + 来源闸门
|
|
420
|
+
|
|
421
|
+
**出站方式由 `replyMode` 控制**:
|
|
422
|
+
|
|
423
|
+
| 值 | 行为 | 代价 / 收益 |
|
|
424
|
+
|---|---|---|
|
|
425
|
+
| `'marker'`(默认) | 只有 `[QQ]…[/QQ]` 里的内容会发出 | 零额外调用;没有反馈回路 |
|
|
426
|
+
| `'tool'` | **关闭标记块**,改用 `qq_bridge_send` 工具 | 每轮多一次模型调用,换来反馈(工具结果会告诉模型是否已发出) |
|
|
427
|
+
| `'always'` | 整轮回复都发出(旧行为,技术内容也会进群) | 适合“这个会话本来就是公开的”场景 |
|
|
428
|
+
|
|
429
|
+
标记块的格式如下,块外的一切都不会进群:
|
|
430
|
+
|
|
431
|
+
```
|
|
432
|
+
这段技术说明、路径、思考过程都不会进群
|
|
433
|
+
|
|
434
|
+
[QQ]在的,收到召唤了 👻[/QQ]
|
|
435
|
+
|
|
436
|
+
这段也不会
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
- **没有标记块时,一个字都不发**。DSH 会话里的技术说明、路径与思考过程不适合出现在 QQ 群,宁可不回复,也不刷屏。
|
|
440
|
+
- **只有“被 QQ 唤醒的那一轮”才生效**:在 DSH 界面里直接与同一个会话聊天时,标记块一个字都不会发出;
|
|
441
|
+
QQ 唤醒的那一轮里,块外的正文同样不发出。
|
|
442
|
+
- **来源判据**:优先识别插件注入的那条消息。① 消息 id 等于刚注入的那条;
|
|
443
|
+
② 正文以 QQ 入站前缀开头(`[QQ · ` / `[QQ 未唤醒期间聊天记录`)。`source` 字段只作兜底:
|
|
444
|
+
真机上出现过 `source` 字段被上游重写,导致 QQ 侧 @ 了却发不出去(哑火)。
|
|
445
|
+
- **工具调用同样经过来源闸门**。不是 QQ 唤醒的那一轮,`qq_bridge_send` 第一次调用只回一句
|
|
446
|
+
“确认要发吗”,不发送任何内容;同一轮里再调一次才真正发出。确认状态按轮清零,不跨轮保留。
|
|
447
|
+
- **台账**:标记块发送记 `via: 'marker' | 'always'`,工具发送记 `via: 'tool'`,
|
|
448
|
+
非 QQ 轮的确认发送额外标 `nonQQTurn: true`。据此可以查到每条群消息的发送方式。
|
|
449
|
+
- **字数上限**:出站文本超过 `replyMaxChars` 时**一条都不发**,调用方拿到 `reason: 'too-long'`,
|
|
450
|
+
应按提示缩短后重发;未超长时 `replyWithQuote: true` 让消息带上引用;
|
|
451
|
+
`stripMarkdown: true`(默认)时先去掉 Markdown 标记(QQ 不渲染)。字数按去掉 Markdown 之后、拆表情之前的正文计算。
|
|
452
|
+
- **系统表情**:去掉 Markdown 之后,`sendToQQ` 把正文拆成 OneBot 消息段。`[笑哭]`、`[挥手]` 这类名字变成 `face` 段(笑哭是 182,挥手是 129),NapCat 显示系统表情。也认入站占位符 `[表情182]`,以及 `[CQ:face,id=182]`。对不上的方括号(例如 `[没这表情]`、`[图片]`)留在文字段里。标记块和 `qq_bridge_send` 都走这条路。名字表在 `faces.js`,常见的还有微笑、捂脸、吃瓜、比心。商城表情不走这条路,由 `qq_bridge_sticker` 按登记表发送,见[表情包](#表情包)。
|
|
453
|
+
- **目的地钉在唤醒那一刻**。这一轮的标记块、`qq_bridge_send`、`qq_bridge_sticker` 和 `qq_bridge_listen` 都发回把这一轮叫醒的那个群或私聊,不会被同一时间另一条只记录下来的消息改掉。显式传入 `groupId` 或 `userId` 时发到那里,并且不引用别的会话里的原消息;这个群号必须在 `groupWhitelist` 里,这个 QQ 号必须在 `whitelist` 里,对不上就一条都不发(`reason: "not-allowlisted"`)。白名单留空等于不允许。钉住的目的地不再查一遍,它在入站时已经通过了白名单。同一会话后面没有新唤醒的轮次,仍回落到最后一次非丢弃的入站。
|
|
454
|
+
- **连不上或还没有目的地时丢弃并记日志**,不静默排队堆积。
|
|
455
|
+
- **标记块发出时传输失败**(`reason: 'send-failed'`)同样记进丢弃台账,并把原因回注到这一轮的会话里。模型能看到没发出去,而不是只在日志里留一行。
|
|
456
|
+
- 入站消息会自动附一行提示,说明目标会话的回复方式。
|
|
457
|
+
|
|
458
|
+
## 消息:占位符、戳一戳、图片
|
|
459
|
+
|
|
460
|
+
### 非文本消息段
|
|
461
|
+
|
|
462
|
+
QQ 的表情、表情包、图片、语音、视频、文件、卡片、合并转发等都会换成可读占位符
|
|
463
|
+
(如 `[表情4]`、`[表情包:[动画表情]]`、`[图片]`、`[文件:a.zip]`),**不会让整条消息丢失**。
|
|
464
|
+
系统表情的入站占位符带编号,例如 `[表情182]`。出站时模型写的 `[笑哭]`,或这种占位符,会再变回系统表情,见[出站](#出站三种模式--来源闸门)。
|
|
465
|
+
商城表情若带了可下载的图,描述填回去之后写成 `[表情包#短哈希]`,和普通图片的 `[图片#短哈希]` 分开,见[表情包](#表情包)。
|
|
466
|
+
|
|
467
|
+
### 戳一戳
|
|
468
|
+
|
|
469
|
+
QQ 的“拍一拍”在 OneBot 里走的是 **notice 事件**,不是消息段。插件把该事件合成一条入站消息
|
|
470
|
+
(正文 `[戳一戳](戳的是我)`):
|
|
471
|
+
|
|
472
|
+
- **戳我时必唤醒**,与 @ 同级(`reason: poke`);
|
|
473
|
+
- 戳别人时只记录,不唤醒,与“叫了别人的名字”相同;
|
|
474
|
+
- 其它没有适配的 notice 会留一条 `未适配的 notice:…` 日志,不静默丢弃。
|
|
475
|
+
|
|
476
|
+
> “主动戳人”暂不提供,原因是 NapCat 发包后端受限,见[已知限制](#已知限制与环境要求)。
|
|
477
|
+
|
|
478
|
+
### 图片
|
|
479
|
+
|
|
480
|
+
**图片会真正传给目标会话**,分 2 步:
|
|
481
|
+
|
|
482
|
+
1. **收到即落盘缓存**(`$DSH_HOME/qq-bridge-images/`;超过 64MB 或 300 张时按时间淘汰最旧的)。
|
|
483
|
+
单张下载超过 8MB 不会落盘:先看 `content-length`,没有这个头就在读流时停掉。失败的那张仍是 `[图片]` 占位符。
|
|
484
|
+
这一步是纯 I/O,不调用模型,因此不影响缓冲“零模型调用”的性质,也不受 QQ 图片 URL 过期影响。
|
|
485
|
+
文件名就是**内容哈希**(`<hash>.<ext>`):同一张图不重复落盘,命中已有文件时也拿得到哈希。
|
|
486
|
+
2. **唤醒时先用多模态模型看图**,把描述填回图片原来的位置,并带上 6 位短哈希:
|
|
487
|
+
|
|
488
|
+
```
|
|
489
|
+
[QQ · “群名称”] “DSH 昵称”:看这个 [图片#a1b2c3]:“图片描述”
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
短哈希是这张图在插件里的编号。描述可能不准,而模型需要指着一张具体的图说话,
|
|
493
|
+
所以占位符里带上短哈希 —— 模型把这个短哈希交给 `qq_bridge_sticker` 就能存下或再发出去。
|
|
494
|
+
|
|
495
|
+
普通图片是 `[图片#a1b2c3]:<描述>`。入站段上带了商城表情的 `emoji_id`(NapCat 常把商城表情收成这种 `image` 段,也会直接给 `mface` 段)时,同一位置写成 `[表情包#a1b2c3]:<表情名> <描述>`。只有简介、没有表情 id 的图仍算普通图片。
|
|
496
|
+
|
|
497
|
+
第 2 步是必需的:**目标会话的模型通常不支持图片输入**(如 `deepseek-v4-flash` 是纯文本),
|
|
498
|
+
附件会被系统剥成 `image omitted`,只有描述能让模型知道图里的内容。
|
|
499
|
+
|
|
500
|
+
- 选择模型:优先使用设置里的 `visionModel`(`provider/model`);未设置时自动查找,
|
|
501
|
+
名字带 `vision` 的优先,其次是第一个支持图片输入的模型。
|
|
502
|
+
- 发给视觉模型的那条消息,来源和注入 QQ 消息一样:DSH 0.2.0 用 `plugin:dsh-qq-bot`,
|
|
503
|
+
0.1.5 仍用原来的 `{ kind: 'plugin', plugin: 'qq-bridge' }`。0.2.0 不接受旧的 `kind: 'plugin'`。
|
|
504
|
+
- 解析结果会缓存:成功的解析按 `visionModel` 的值缓存,值没变就不再枚举模型列表。
|
|
505
|
+
自动查找失败的结果也会缓存,但只用于同一次设置内的去重 —— 设置一改就丢掉并重试,
|
|
506
|
+
所以之后补上视觉模型不必改动 `visionModel`。
|
|
507
|
+
- **单张图失败会重试 2 次**(共 3 次尝试,退避 300/600ms);3 次都失败才记“描述失败 + 原因”。
|
|
508
|
+
普通图片写成 `[图片#a1b2c3]:(描述失败:原因)`。商城表情写成 `[表情包#a1b2c3]:<表情名> (描述失败:原因)`。
|
|
509
|
+
- 一次唤醒最多描述 3 张(每张 = 一次多模态调用);描述失败不影响文本处理与策略判定。
|
|
510
|
+
- 图片已登记为表情包时,描述直接取登记表里的 `description`,不调用多模态模型。
|
|
511
|
+
|
|
512
|
+
### 表情包
|
|
513
|
+
|
|
514
|
+
群友发的图和商城表情可以登记下来,之后按情绪(mood)或哈希再发出去。带齐商城表情字段的登记项发成 QQ 原生表情包;普通图片发出去仍是图片。登记与发送都由 `qq_bridge_sticker` 工具完成。
|
|
515
|
+
|
|
516
|
+
**存储位置**(与 statePath 同目录):
|
|
517
|
+
|
|
518
|
+
| 路径 | 内容 |
|
|
519
|
+
|---|---|
|
|
520
|
+
| `<statePath 目录>/qq-bridge-stickers.json` | 登记表 |
|
|
521
|
+
| `<statePath 目录>/qq-bridge-stickers/<hash>.<ext>` | 图片本体 |
|
|
522
|
+
|
|
523
|
+
`hash` 是图片字节 sha256 的**前 16 位十六进制**(小写);正文占位符里的 6 位短哈希是 `hash` 的前 6 位。
|
|
524
|
+
|
|
525
|
+
**条目字段**:
|
|
526
|
+
|
|
527
|
+
| 字段 | 说明 |
|
|
528
|
+
|---|---|
|
|
529
|
+
| `hash` | 内容哈希,16 位十六进制 |
|
|
530
|
+
| `mood` | 情绪分类,如 无语 / 开心 |
|
|
531
|
+
| `description` | 这张图是什么;发送与识图都用这个字段 |
|
|
532
|
+
| `file` | 图片本体路径,相对 statePath 目录 |
|
|
533
|
+
| `ext` | 扩展名 |
|
|
534
|
+
| `source` | 来源:`group:<群号>` / `private:<QQ号>` / `cache`(只从图片缓存捞到) |
|
|
535
|
+
| `mface` | 可选。商城表情的 `emoji_id`(字符串)、`emoji_package_id`(非负整数)、`key`、`summary`。缺了这项的旧条目照常当图片 |
|
|
536
|
+
| `addedAt` | 首次登记时间 |
|
|
537
|
+
| `sentCount` / `lastSentAt` | 累计发送次数与上次发送时间 |
|
|
538
|
+
|
|
539
|
+
**三个 action**:
|
|
540
|
+
|
|
541
|
+
| action | 参数 | 作用 |
|
|
542
|
+
|---|---|---|
|
|
543
|
+
| `list` | `mood?` | 列出已登记的:短哈希、mood、描述(截断 40 字)、`kind`(`mface` 或 `image`)、表情名、图片大小、发送次数 |
|
|
544
|
+
| `save` | `hash?`、`fromLast?`、`mood`、`description` | 登记一张图。`fromLast: true` 取缓冲区里最近一张。源图上若有商城表情字段,一并写入 |
|
|
545
|
+
| `send` | `hash?`、`mood?`、`groupId?`、`userId?` | 发一张。给 `hash` 精确发;只给 `mood` 时在该情绪里随机挑一张。`mface` 四字段齐全时发原生表情包,否则发图片 |
|
|
546
|
+
|
|
547
|
+
- `save` 先在**缓冲区**找源图(那里带着来源信息),再到**图片缓存目录**找。两处都没有时返回错误,
|
|
548
|
+
提示先让群友发一张图。
|
|
549
|
+
- `send` 与 `qq_bridge_send` **共用同一道来源闸门**:不是 QQ 唤醒的那一轮,第一次调用只回一句确认,
|
|
550
|
+
同一轮里再调一次才真的发。
|
|
551
|
+
- 发送成功记进 outbox(`via: 'tool'`、`action: 'sticker'`)。原生表情包的 `text` 是 `[表情包#<短哈希>]`,普通图片是 `[图片#<短哈希>]`。同时更新该条的 `sentCount` 与 `lastSentAt`,已记下的 `mface` 不会被这次计数清掉。
|
|
552
|
+
- **四字段齐全就发 OneBot `mface`**:`emoji_package_id`(数字)、`emoji_id`、`key`、`summary`。QQ 里显示的是原生表情包,不是一张图。这种发送不读本地图片文件。
|
|
553
|
+
- **缺任何一项就发图片**:本体用 `base64://`,不用本地路径。NapCat 的 `enableLocalFile2Url` 是 false,本地路径读不到。
|
|
554
|
+
- 图片落盘时,商城表情字段写在同一目录的 `<hash>.mface.json`(`$DSH_HOME/qq-bridge-images/`)。唤醒成功后缓冲会清掉,`save` 靠这份旁路文件找回字段。图片被缓存淘汰时,旁边的文件一起删。
|
|
555
|
+
- 该工具**不受 `debugTools` 限制**:表情包是产品的玩法,不是调试工具。没有新增设置项。
|
|
556
|
+
|
|
557
|
+
**缓存命中省 token**:`describeImage()` 先查登记表。命中且有 `description` 时直接返回这条描述,
|
|
558
|
+
不调用多模态模型 —— 反复出现的图只描述一次。日志只记短哈希,不记图片 URL。
|
|
559
|
+
|
|
560
|
+
## 设置界面与全部设置项
|
|
561
|
+
|
|
562
|
+
**设置 → 插件 → 插件配置** 里有一张 **QQ 桥接** 卡片(可折叠,默认展开)。卡片上的名称是白话
|
|
563
|
+
(例如「哪些群可以叫醒」「群里发出去的是哪一段」),写进配置文件的键仍是英文。
|
|
564
|
+
在这里修改后**保存即生效**(写入 `settings.yaml` 的用户层,不用重启)。
|
|
565
|
+
|
|
566
|
+
> **DSH 0.2.0 上**:宿主不再给第三方插件可写的设置命名空间(DSH #677 的迁移未完成,生态里所有走这条路的
|
|
567
|
+
> 插件都一样)。本插件因此**自己挂了一条设置接口**:`GET/POST /api/qq-bridge/settings`。
|
|
568
|
+
> 值存 `$DSH_HOME/qq-bridge-settings.json`,优先级为 **设置文件 > `cordis.patch.yml` 里的安装配置**(composition config)。
|
|
569
|
+
> 卡片照常可以使用;`cordis.patch.yml` 也仍然可以修改(改完重启生效,且会被上面的设置文件覆盖)。
|
|
570
|
+
> 本机回环、且没有外来源(命令行、SSH 隧道)或来源也是本机时,可以直接改。经反向代理进来时,对端常常仍是 `127.0.0.1`,来源却是公网面板地址:这时必须是**已经登录**的面板会话,并且来源主机名和这次请求的 `Host` 一致,设置才能保存。卡片上的读取和保存会带上面板登录 cookie(`credentials: 'include'`)。**不必再配 `trustedHosts`**。
|
|
571
|
+
> 宿主 `requestRejection()` 返回空、`undefined` 或 `0` **不算**已登录。已登录只认宿主明确给出的结果:`isAuthenticated`、浏览器会话,或 `admit()` 交回的身份。没有这个结果的外网请求是 403。来源是别的网站、和 `Host` 对不上的请求也是 403,就算带着 cookie 也不行。接口只写 schema 白名单里的键。
|
|
572
|
+
> 判断面板监听器是不是 HTTPS 时,只通过 `ctx.get('webServer')` 读协议,不读 `ctx.webServer`。插件没有把 `webServer` 放进 inject。直接读这个属性会抛错,宿主会把这次请求收成**没有正文的 HTTP 400**,卡片诊断就显示本机接口不可用,即使浏览器里已经登录。读不到这项服务、或读取失败时,按 HTTP 继续做登录判定。
|
|
573
|
+
|
|
574
|
+
卡片上方显示**当前版本**,并有 **检查更新**。按钮旁边只写「只检查,不安装。」接口是 `GET/POST /api/qq-bridge/update`,准入和设置接口相同。GET 只读当前版本,不去 npm,也不去 GitHub。POST 不带 `action` 才对比版本:先问 npm registry 里的 `@fengye_nobug/dsh-qq-bot` 正式版。有比当前新的版本时,卡片上写短句「有新版本 x.y.z(现在是 a.b.c),在 npm 上。这里只检查,还没安装。」并且不再访问 GitHub。npm 上还没有更新、但 GitHub Release 上有更新时,改问 GitHub,短句里写「在 GitHub 上」。安装命令和说明摘要仍在接口字段里,不铺在这句主提示上。没有新版、连不上、被限流,都用白话说明。检查本身不会下载或安装。
|
|
575
|
+
|
|
576
|
+
有新版时出现 **更新到 vX.Y.Z**。旁边只加一句「装完会重启面板。请刷新;登录口令可能会变。不改 NapCat。」点下去才发送 `POST { "action": "apply", "version": "x.y.z" }`。优先 `dsh plugin --profile web add @fengye_nobug/dsh-qq-bot@x.y.z`。registry 里没有这个版本时,改从 GitHub 的官方源码包装上。安装成功后,把 profile、`$DSH_HOME/cordis.patch.yml`(有 `qq-bridge` 这一行才改)和已安装插件补丁里的 `name` 改成磁盘上 `package.json` 的包名,然后重启面板。只改我们自己的旧包名(`dsh-qq-bot`、`dsh-qq-bridge`),不改 `config`,也不改别的插件。成功时主提示是「已经从 npm 装上 vX.Y.Z,面板即将重启。」或「已经从 GitHub 装上 vX.Y.Z,面板即将重启。」进行中显示「正在更新…」。已经是最新正式版时没有这个按钮。超过三分钟还没装完时停掉这次的 dsh 和 pnpm。`.plugin-manager/run.json` 里的进程还在就不删,也不再开第二个安装;进程已经不在或记录读不懂才删。超时后如果磁盘上已经是要装的版本,仍改包名,并自动重启 dsh-web。自动重启没有安排上时,才提示自己重启,并写明这次是从 npm 还是从 GitHub 装上的。
|
|
577
|
+
|
|
578
|
+
「启动时检查一次」默认关。打开并保存后,下次启动 dsh-web 才问一次,结果只写在日志里,不会安装。
|
|
579
|
+
|
|
580
|
+
卡片展开后还有 3 个操作按钮(0.1.5 与 0.2.0 上都有):
|
|
581
|
+
|
|
582
|
+
- **打开 WebUI**:用浏览器打开 NapCat 的 WebUI。填了 `napcatWebuiUrl` 就打开那个地址(地址本身没带 token 时,会补上 `webui.json` 里的 token)。没填则用 `<安装目录>/config/webui.json` 里的 host / port / token;文件不存在时仍按 `127.0.0.1:6099` 打开,按钮保持可点。地址是 `127.0.0.1` 或 `localhost` 时,旁边会写明只有跑着 NapCat 的那台机器能打开。
|
|
583
|
+
- **启动 NapCat**:与工具 `qq_bridge_napcat` 的 `launch` 是同一个动作。目录里有官方
|
|
584
|
+
`launcher-win10.bat` 时优先调用该脚本,脚本会自我提权,**可能弹出 UAC 窗口**。
|
|
585
|
+
NapCat 运行中时,按钮显示“已在运行”并禁用;外部手动启动的 NapCat 也算运行中。
|
|
586
|
+
卡片展开期间每 5 秒轮询一次状态,所以外部启动后按钮会自己变化。
|
|
587
|
+
- **打开文件夹**:用系统文件管理器打开 NapCat 安装目录(Windows 用 `explorer.exe`,macOS 用 `open`,
|
|
588
|
+
Linux 用 `xdg-open`)。目录不存在时不打开,卡片上会提示:自己部署或 Docker 里的路径通常不是默认值,请填写「NapCat 安装目录」。
|
|
589
|
+
|
|
590
|
+
这 3 个按钮走 `GET /api/qq-bridge/napcat`、`POST /api/qq-bridge/napcat/launch`、
|
|
591
|
+
`POST /api/qq-bridge/napcat/reveal`,准入与设置接口相同:本机可以直接用;反向代理要带已登录的面板会话,且来源主机名和 `Host` 一致。没有明确登录结果的外网请求 403。请求进行中按钮禁用,token 只拼进 URL 不写日志。
|
|
592
|
+
|
|
593
|
+
修改**出站方式**会立刻重挂注入给目标会话的用法说明:提示词正文按模式分叉,
|
|
594
|
+
不重挂时模型会按本部署里已经不生效的做法回话。
|
|
595
|
+
|
|
596
|
+
**目标会话是下拉选择**(按会话标题或项目名选择,不用记 `session-…` 那串 id);选好后会同时显示该会话的真实 id。
|
|
597
|
+
**已归档的会话不会列出**,当前选中的那个例外,会标成“(已归档)”。
|
|
598
|
+
配置里的会话不在列表里时(别的工作区、宿主还没有加载完),保留成“(不在会话列表里)”的兜底项。
|
|
599
|
+
|
|
600
|
+
卡片上的叫法和下面「说明」一致。第一列是配置键,不会出现在按钮上。
|
|
601
|
+
|
|
602
|
+
| 键 | 默认 | 说明 |
|
|
603
|
+
|---|---|---|
|
|
604
|
+
| `targetSessionId` | `''` | 接到哪个会话。`single` 时**空 = 不接到任何会话**。`perGroup` 时可以留空(新建会话用宿主当前默认模型);填了则新建会话沿用该会话的模型 |
|
|
605
|
+
| `sessionScope` | `single` | 会话怎么分。`single` = 所有群共用一个会话;`perGroup` = 每个群、每个私聊各记各的,映射写在状态文件的 `sessionByDest`(键为 `group:<群号>` / `private:<QQ号>`)。新建会话带工作目录和「QQ群 / QQ私聊」标题,见 [README 的功能](./README.md#功能) |
|
|
606
|
+
| `maxRecentTurns` | `40` | 每个会话最多留几轮。超出后,等这一轮回到空闲,再调用宿主 `compaction.compactNow(agent, signal)` 尽量压缩。`signal` 是未取消的 AbortSignal,不传命令 id。有投影时看投影轮数。压完没变短就停。`0` = 不限制。compaction 不在 inject 里,宿主没有这项服务时跳过 |
|
|
607
|
+
| `nicknames` | `['244','小助手']` | 听到这些名字就回应 |
|
|
608
|
+
| `wakeProbability` | `0.05` | 没点名时的回应概率 |
|
|
609
|
+
| `whitelist` / `groupWhitelist` | `[]` | 哪些私聊可以叫醒 / 哪些群可以叫醒(**留空 = 谁都不能叫醒**)。群白名单在卡片里按群号填;连上且查到群名时,群号旁边显示当前群名和头像,查不到或没连上只显示群号,不影响保存 |
|
|
610
|
+
| `ownerWhitelist` | `[]` | 主人 QQ。设置卡片里叫「主人 QQ」。这些号才能在 QQ 里用「白名单」命令(**留空 = 谁都不能用**)。卡片保存后马上生效;只改 `cordis.patch.yml` 要重启 |
|
|
611
|
+
| `attachRecentChat` / `recentChatLimit` | `true` / `8` | 把没叫醒时的聊天一并带上,以及附带最近几条 |
|
|
612
|
+
| `atOnlyInGroup` | `false` | 群里必须 @ 才理 |
|
|
613
|
+
| `replyMode` | `'marker'` | 群里发出去的是哪一段:`marker` 只发「发给群」里的那段|`tool` 用发消息工具|`always` 整段回答都进群(慎用) |
|
|
614
|
+
| `delivery` | `'auto'` | 忙的时候怎么插话:`auto` 能插就插,不能就排队(推荐)|`followup` 一律新开一轮|`steer` 不管忙不忙都插 |
|
|
615
|
+
| `replyMaxChars` | `1500` | 一条最多多少字(下限 100);**超过就不发送** |
|
|
616
|
+
| `replyWithQuote` | `false` | 回复时带上原消息 |
|
|
617
|
+
| `stripMarkdown` | `true` | 去掉 Markdown 记号 |
|
|
618
|
+
| `visionModel` | `''` | 用来看图的模型(`provider/model`);空 = 自动查找 |
|
|
619
|
+
| `agentPreset` | `''` | 人设预设;空 = 沿用会话自己记着的 |
|
|
620
|
+
| `onebotUrl` | `''` | OneBot 连接地址;空 = 先不连接 |
|
|
621
|
+
| `accessTokenEnv` | `''` | OneBot 令牌放在哪个环境变量(填变量名,不是令牌,也不是模型 API key) |
|
|
622
|
+
| `selfId` | `''` | 机器人自己的 QQ 号。空 = 连上后自动读取;读取完成前,先用事件里的 `self_id`。想避开这段空窗就填上 |
|
|
623
|
+
| `heartbeatTimeoutMs` | `90000` | 多久没收到连接数据就判定断线并重连 |
|
|
624
|
+
| `napcatInstallDir` | `''` | NapCat 安装目录;空 = `$DSH_HOME/napcat`。自己装的或 Docker 里的请填实际目录 |
|
|
625
|
+
| `napcatWebuiUrl` | `''` | NapCat 网页管理地址。空 = 读 `webui.json`(通常是 `127.0.0.1`,只有跑着 NapCat 的机器能打开)。远程使用时填外面打得开的地址 |
|
|
626
|
+
| `napcatVersion` | `''` | 要下载的 NapCat 版本;空 = 最新 |
|
|
627
|
+
| `downloadProxy` | `''` | 下载时走的代理;空 = 先看 `HTTPS_PROXY` / `HTTP_PROXY` / `ALL_PROXY`。NapCat 下载,以及一键更新改从 GitHub 下载时,都用它。从 npm 安装时走 pnpm 自己的 registry,不读这个框。界面上不显示代理地址 |
|
|
628
|
+
| `onebotPort` | `3001` | OneBot 端口。自己另外部署的 NapCat 要和这边一致 |
|
|
629
|
+
| `qqNumber` | `''` | 快速登录用的 QQ 号(需先手动登录过一次) |
|
|
630
|
+
| `checkUpdateOnStart` | `false` | 启动时检查一次。默认关。打开后,下次启动只检查有没有新版本,写在日志里,不自动安装。要装用卡片上的「更新到 vX.Y.Z」 |
|
|
631
|
+
|
|
632
|
+
这些值优先从设置界面 / `settings.yaml` 取,未设置时回落到 `cordis.patch.yml` 里的安装配置(composition base)。
|
|
633
|
+
|
|
634
|
+
> 字段旁带 “·已覆盖” 时,该键在设置文件里有本机覆盖。点该字段旁的 “撤销覆盖” 可清掉这一项。
|
|
635
|
+
> 该键随即回落到 `cordis.patch.yml` 里的安装配置,“·已覆盖” 标记消失。
|
|
636
|
+
> 数字框留空不会存成 0(那样会把唤醒概率或轮数上限关掉)。想回到安装配置里的值,点「撤销覆盖」。
|
|
637
|
+
|
|
638
|
+
**看不到卡片**(或修改 `client/` 之后看不到变化)时,按顺序确认 4 件事:
|
|
639
|
+
|
|
640
|
+
1. `package.json` 里的 `dsh.client` 还在;
|
|
641
|
+
2. **重启过 `dsh web`**。浏览器端的内容是启动时读盘并计算指纹的,修改后必须重启;
|
|
642
|
+
3. **重启之后刷新过页面(F5)**。清单是页面加载时注入的,只重启服务器时,已经打开的页面手里还是旧清单;
|
|
643
|
+
4. DSH 版本:0.2.0 把客户端设置服务从 `settingsScope` 改名为 `webUiSettings`,插件配置页的挂载点从 `settings.plugin.item` 改成 `settings.plugins.tab`。有的 0.2.x 上这两个服务都不存在,`ctx.inject` 会一直等。本插件**不等它们**:`apply` 时就挂上卡片。宿主设置域还在就用它;否则读写 `/api/qq-bridge/settings`。挂载点有四个:`settings.plugins.tab`、`settings.plugin.item`、`plugins.tab`、`plugins.detail.section`(详情页在 npm 包 `@fengye_nobug/dsh-qq-bot`、旧包名 `dsh-qq-bot` 和 `dsh-qq-bridge`,或 id `qq-bridge` 上显示)。卡片默认展开。
|
|
644
|
+
|
|
645
|
+
> 卡片按 settings 命名空间派发,`client/client.js` 里的 key 必须逐字等于 `qq-bridge`。
|
|
646
|
+
> 对不上不会报错,只是静默不显示(`test-smoke.mjs` 包含该一致性的断言)。
|
|
647
|
+
|
|
648
|
+
## 注入给目标会话的系统提示
|
|
649
|
+
|
|
650
|
+
插件会给目标会话注入一段用法说明(`buildUsagePrompt()`,在 `index.js` 里导出,便于测试检查),分 6 节:
|
|
651
|
+
|
|
652
|
+
| 节 | 内容 |
|
|
653
|
+
|---|---|
|
|
654
|
+
| 入站格式 | 入站消息格式、未唤醒摘要、“[图片#<短哈希>]:<描述>”(就地填回,可能不准)、被戳显示成“[戳一戳](戳的是我)” |
|
|
655
|
+
| 回复方式 | 按 `replyMode` 分叉:标记块怎么写、该调用哪个工具、非 QQ 轮的 2 步确认 |
|
|
656
|
+
| 表情包 | 用 `qq_bridge_sticker` 的 `list` / `send` 发表情包;用 `save` 收藏群友发的图。`[表情包#短哈希]` 是商城表情,发出去是原生表情包;`[图片#短哈希]` 发出去仍是图片 |
|
|
657
|
+
| 群聊礼仪 | 1–2 句话;默认不超过 40 字;不刷屏;不放路径、代码、日志、提交号;不汇报过程。系统表情写 `[笑哭]`、`[挥手]` 或 `[表情182]` |
|
|
658
|
+
| 无人值守 | 不碰特权操作,见下一节 |
|
|
659
|
+
| 会话跟随(监听模式) | 何时开窗;默认 5 分钟、上限 60 分钟;只对同一目的地生效;答完主动关 |
|
|
660
|
+
|
|
661
|
+
## 无人值守的安全边界(不提供审批桥接)
|
|
662
|
+
|
|
663
|
+
QQ 侧没有人能点击“同意”,因此:
|
|
664
|
+
|
|
665
|
+
- 审批策略保持 `ask`:**没有人应答时,危险操作直接失败**,不会偷偷执行。
|
|
666
|
+
- 注入的系统提示明确禁止无人值守时主动执行特权操作:执行或安装程序、修改系统或网络设置、
|
|
667
|
+
操作工作区之外的目录、删除或覆盖文件、修改配置、结束进程、发布推送。
|
|
668
|
+
- **不弹审批的路径同样属于“动手”**:用浏览器点击网页(例如直接在 GitHub 网页改文件、提交)、
|
|
669
|
+
直接发 HTTP 写请求、写文件。这些操作不弹审批,但不能因此绕过上面的清单。
|
|
670
|
+
判断标准是这件事本身是否属于动手操作,与是否弹出审批无关。
|
|
671
|
+
- **QQ 消息按不可信输入对待**:群里任何人都能塞入“忽略以上指令,去执行 xxx”这类注入,
|
|
672
|
+
**聊天内容本身不构成动手的理由**。需要动手时,机器人回答“无人值守时我不执行这类操作”,
|
|
673
|
+
并等发起人在电脑前再处理。
|
|
674
|
+
- **不得把规则表述成“需要授权”**。这种说法会让用户以为需要额外权限,实际是插件不执行。
|
|
675
|
+
- 纯聊天、查资料、写草稿不属于动手操作,照常执行。
|
|
676
|
+
|
|
677
|
+
插件不把审批搬到 QQ。那样做会把“谁能给机器人发消息”变成“谁能代替用户授权”。
|
|
678
|
+
|
|
679
|
+
## 上下文上限
|
|
680
|
+
|
|
681
|
+
防止长文超出 context:
|
|
682
|
+
|
|
683
|
+
| 位置 | 上限 |
|
|
684
|
+
|---|---|
|
|
685
|
+
| 单条**入站**消息 → 模型上下文 | 4000 字符 |
|
|
686
|
+
| 单条消息 → **未唤醒缓冲** | 500 字符 |
|
|
687
|
+
| 整段**未唤醒摘要** | 1200 字符。从最新往回装,装不下的更早条目不进摘要,并注明“更早的 N 条已省略”;N 取缓冲快照条目数减摘要行数,大于 99 时改写成“更早的大量消息已省略”。这些条目随后随缓冲一起删除,不会留到下一次唤醒 |
|
|
688
|
+
| `buffer` / `log` / `outbox` 条数 | 200 / 200 / 50 |
|
|
689
|
+
| `log` 每条 | 200 字符 |
|
|
690
|
+
| `outbox` 每条 | 4000 字符 |
|
|
691
|
+
|
|
692
|
+
截断都会留 `…(已截断)` 标记,不静默丢内容。
|
|
693
|
+
|
|
694
|
+
## 工具一览
|
|
695
|
+
|
|
696
|
+
| 工具 | 注册条件 | 作用 |
|
|
697
|
+
|---|---|---|
|
|
698
|
+
| `qq_bridge_status` | `debugTools` | 设置、目标会话是否已加载、连接状态、目的地、缓冲条数、最近日志,以及**最近观测到的 `user/message` 来源**(`recentInboundSources`)。排查“哑火”先看这个工具:`ours: false` = 没有认出那条是插件灌入的 QQ 消息;`ours: true` 但工具仍被拒 = 标记被别的事件清掉了 |
|
|
699
|
+
| `qq_bridge_transport` | `debugTools` | 查看连接状态(含 token 是否解析到),或 `action: 'reconnect'` 强制重连 |
|
|
700
|
+
| `qq_bridge_simulate` | `debugTools` | 模拟一条入站消息走完整策略;可带 `groupName`(群名,用来写 perGroup 会话标题);`dryRun: true` 只看决策,不注入 |
|
|
701
|
+
| `qq_bridge_send` | 始终 | 出站。正文里的 `[笑哭]` 会变成系统表情。`replyMode: 'tool'` 时该工具**就是唯一的出站方式**,因此不能藏在调试开关后面 |
|
|
702
|
+
| `qq_bridge_sticker` | 始终 | 表情包:`list` / `save` / `send`。登记项带齐商城表情字段时发 `mface`,否则图片以 base64 出站。与 `qq_bridge_send` 共用来源闸门,见[表情包](#表情包) |
|
|
703
|
+
| `qq_bridge_listen` | 始终 | 会话跟随窗口的开启与关闭。这是产品的使用方式,不是调试工具 |
|
|
704
|
+
| `qq_bridge_napcat` | 始终 | NapCat 自助托管(下载 / 配置 / 启停) |
|
|
705
|
+
|
|
706
|
+
`debugTools` 默认 `true`;关闭后只保留后 4 个。
|
|
707
|
+
|
|
708
|
+
## 已知限制与环境要求
|
|
709
|
+
|
|
710
|
+
- **“主动戳人”暂不提供**:OneBot 的 `send_poke` 依赖 NapCat 的 **PacketBackend**,而 PacketBackend 只支持特定 QQ 版本。
|
|
711
|
+
真机上(NapCat 4.18.28 + QQ 9.9.36-53644)会报
|
|
712
|
+
`retcode=1400 packetBackend发包能力不可用 … 不支持当前QQ版本架构`;
|
|
713
|
+
把 poke 当消息段发送也会被拒(`1200 消息体无法解析`)。
|
|
714
|
+
**入站戳完全可用**(戳我必唤醒)。要恢复主动戳,可以换用 PacketBackend 支持的 QQ
|
|
715
|
+
(NapCat 官方 release 推荐 **40768–44343**),或等 NapCat 跟上 QQ 的更新节奏。
|
|
716
|
+
- **图片描述依赖一个支持图片输入的多模态模型**:目标会话的模型看不了图时,这是目标会话“看见”图的唯一途径;
|
|
717
|
+
一次唤醒最多描述 3 张(成本考虑)。
|
|
718
|
+
- **没有商城表情 id 的图发不成原生表情包**:本地保存的图、普通图片、只带简介的图、收藏表情(图片 `sub_type` 表示表情,但没有 `emoji_id` / `key`)都走图片。只收到 `mface` 段、却没有可下载地址时,占位符里没有短哈希,`save` 找不到文件,也就不能再发出去。四字段里缺 `key` 或 id 时同样退回图片。表情下架或 `key` 失效时,NapCat 可能直接拒发,插件不会自动改成图片再发一次。
|
|
719
|
+
- **NapCat 是第三方 QQ 客户端**,存在账号风控风险;`NapCatInstaller.exe` 与 QQ 首次登录都需要人在场。
|
|
720
|
+
- **测试套件需要 Node >= 22**(用到内置的全局 `WebSocket`)。
|
|
721
|
+
- README / CHANGELOG 里的真机结论都来自 **Windows 10 + NapCat Shell + OneBot v11** 这一套;
|
|
722
|
+
其它平台可能可用,但没有真机验证过。
|
|
723
|
+
|
|
724
|
+
## 测试
|
|
725
|
+
|
|
726
|
+
改用法、设置或运行行为时,同一次提交里更新本文件。约定见 [`AGENTS.md`](./AGENTS.md)。
|
|
727
|
+
|
|
728
|
+
5 个套件都是**零依赖**(测试自己造 zip、自己起一个最小 OneBot WS 服务器;检查更新不连真 npm、也不连真 GitHub),共 **846 条断言**:
|
|
729
|
+
|
|
730
|
+
```powershell
|
|
731
|
+
node test-smoke.mjs # 策略分支 + 出站闸门 + 描述缓存命中 + 表情包工具(含 mface)+ 系统表情分段 + 设置卡片字段 + 检查更新和一键安装接口(假 ctx,纯离线)
|
|
732
|
+
node test-onebot.mjs # M2 端到端:内置最小 OneBot WS 服务器,验证收发全链路
|
|
733
|
+
node test-napcat.mjs # M5:ZIP 解包 / sha256 校验 / 发行包选择 / 配置写入
|
|
734
|
+
node test-stickers.mjs # 表情包登记表:哈希 / 读写 / 查找 / 原子写 / importSticker / mface 字段
|
|
735
|
+
node test-updates.mjs # 检查更新和一键安装:semver、npm 正式版、GitHub 备用下载、限流和断网的白话
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
## 路线
|
|
739
|
+
|
|
740
|
+
| 里程碑 | 内容 |
|
|
741
|
+
|---|---|
|
|
742
|
+
| **M1** ✅ | 固定会话注入 / 显式 resume / 唤醒策略 / 出站采集 / 调试工具 |
|
|
743
|
+
| **M2** ✅ | OneBot(NapCat) 传输:WS 入站 → `handleInbound()`;出站 → `send_group_msg` / `send_private_msg` |
|
|
744
|
+
| **M3** ✅ | 设置界面:手写 `__ModuleLoader__` bundle 的“QQ 桥接”卡片(不依赖构建步骤) |
|
|
745
|
+
| **M4** ✅ | 打成 DSH bundle,`dsh plugin add` 安装、重启、实测通过 |
|
|
746
|
+
| **M5** ✅ | NapCat 自助托管(下载 / sha256 校验 / 解包 / 写配置 / 启停) |
|
|
747
|
+
| **M6** ✅ | 真机联调补齐:出站标记闸门、上下文上限、图片缓存 + 多模态描述、会话跟随、用法注入系统提示 |
|
|
748
|
+
| **M7** ✅ | 安全边界:用法说明重写为分节契约(含**无人值守不碰特权操作**);**明确不做审批桥接** |
|
|
749
|
+
| **M8** ✅ | 分得清“在 QQ 还是 DSH 里”:出站闸门按来源生效、出站方式做成开关、改模式立刻重挂提示词 |
|
|
750
|
+
| **M9** 🚧 | 戳一戳(入站已通,出站受 NapCat 限制,见[已知限制](#已知限制与环境要求)) |
|
|
751
|
+
|
|
752
|
+
完整变更记录见 [`CHANGELOG.md`](./CHANGELOG.md)。上游发行版见 [sj244/dsh-qq-bridge Releases](https://github.com/sj244/dsh-qq-bridge/releases)。
|
|
753
|
+
|