dsh-qqbot-guard 0.0.0-stage → 1.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 anwesc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,241 @@
1
- # Temporary Holding Version
1
+ # qqbot-guard
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **qqbot 增强层**(伴侣插件)。qqbot 主体仍由用户从插件市场安装(`@tencent-connect/dsh-qqbot`),
4
+ 本插件只做进程内补丁,不修改 qqbot / SDK 的任何磁盘文件。
5
+
6
+ 本仓库是 `dsh-qqbot-guard` 的**唯一真源**(源码、测试、发布都在这里)。
7
+ 两种来源,包名相同:从插件市场 / npm 单独安装(`dsh-qqbot-guard`),
8
+ 或随 [dsh-android](https://github.com/anwesc/dsh-android) APK 内置(构建时从 npm 拉同一版本)。
9
+
10
+ ## 它补什么
11
+
12
+ | 编号 | 现象 | 本插件的处理 |
13
+ | --- | --- | --- |
14
+ | 1 | 群聊必须 @ 才回,关键词(如 `YONGJIE`)被 `mentionGate` 拦下 | 正文命中关键词时给 `msg.mentions` 补 `{is_you:true}`,`mentionGate` / `slashCommand` 都判成被提及 |
15
+ | 2 | 关键词触发时看不到上下文(`inbound.js:209` 只在 `wasMentioned` 时拼历史) | 同上:`wasMentioned` 变 true → `buildAgentBody` 自动带上 `[Chat history]` |
16
+ | 3 | 群历史里只剩文字,图片/语音/文件的条目是空行(`history-buffer.js:90-96` 只存 `content`) | 在链头**预下载**附件(`downloadMediaAttachments`),再包装 history store 的 `list`,给条目补 `[图片「a.png」@/…/media/a_…png、语音]` |
17
+ | 4 | 表情是占位符 `<faceType=6,faceId="0",ext="eyJ0ZXh0IjoiIn0=">`,上游只读 `ext.text`(是空的) | 解码 `ext.text` 有名字就用名字(如 `捂脸`);为空则统一给 `[表情]`,**不按 `faceId` 猜**(见下方第 5 条风险) |
18
+ | 5 | 群里没触发关键词的消息,附件根本不会下载(`attachmentProcessor` 是链上第 13 层,在 `mentionGate` 后面) | guard 自己在链头调 qqbot 的下载器,历史条目因此能带路径 |
19
+ | 6 | 不想动 qqbot 的 `dist` 文件(升级即失效) | 全部走进程内原型补丁,升级 qqbot 不影响本插件文件 |
20
+
21
+ **为什么改成伪造 mentions 而不是往日文里塞 `<@appId>`**:`historyBuffer` 是第 4 层、在门控(第 5 层)
22
+ 之前记录 `ctx.message.content`,往里插标记等于把 `<@102000000>` 写进群历史,再被 `buildAgentBody`
23
+ 原样拼进 prompt。伪造 `mentions[].is_you` 能达到同样效果且不碰正文;真实 @ 的标记同理,会被搬进
24
+ mentions 后从正文摘掉。
25
+
26
+ ## 为什么和加载顺序无关
27
+
28
+ `@tencent-connect/qqbot-nodejs` 的 `handleInboundMessage` 在**每次收到消息时**才拼链
29
+ (`dist/QQBot.js:387` `const chain = [...this.middlewares, downstream]`),而 `middlewares` 是公开数组
30
+ (`dist/QQBot.js:80`)。所以本插件不用抢 `bot.use` 的时机,而是在首条入站消息时
31
+ `this.middlewares.unshift(guard)` —— 即使 qqbot 插件先启动、链已经建好,补丁照样生效。
32
+
33
+ ## 安装
34
+
35
+ 两种来源写的是同一个目录 `node_modules/dsh-qqbot-guard`,装其中一个即可:
36
+
37
+ | 来源 | 怎么装 | 谁负责升级 |
38
+ | --- | --- | --- |
39
+ | 插件市场 / npm | 市场里搜 `dsh-qqbot-guard`,或 `dsh plugin --profile web add dsh-qqbot-guard` | 你自己(市场安装的版本优先) |
40
+ | dsh-android 内置 | 随 APK 的 `plugin-seed` 自动复制进 profile | App 发新版时顺带升级 |
41
+
42
+ `PluginSeed` 每次开机比较「内置版本」与「profile 已装版本」(semver):已装版本有效且不比内置旧就
43
+ **保留不覆盖**;只有缺 `index.js`、没有 `package.json`、版本号非法、或内置版本更新时才用资产覆盖。
44
+ 所以市场装的新版能活过重启,APK 带更新的内置版时也能自动升级。
45
+
46
+ 反过来,从市场卸载不会让它消失:下次启动内置副本会补回(这是「内置插件」的预期行为)。
47
+ 不想让它工作就设 `enabled: false`,而不是卸载。
48
+
49
+ 1. 先装好 qqbot(`@tencent-connect/dsh-qqbot`),本插件靠它才能拿到 bot 实例;
50
+ 2. 装本插件:市场安装,或 `dsh plugin --profile web add dsh-qqbot-guard`;
51
+ 3. 本包自带 `dsh.bundle.patch`(`cordis.patch.yml`,用 `- insert:` 插入 `qqbot-guard` 行),
52
+ 被列入 `dsh.profile.bundles` 后会自动生效——**不要**在 profile 的 `cordis.patch.yml` 里再写一条同 id 的新行;
53
+ 4. 需要改参数时,三选一(最终都落到同一个 `$DSH_HOME/qqbot-guard.json`):
54
+ - **设置页(推荐,手机端也能用)**:DSH 里打开「设置 → QQ 守护」,改完点保存,**立即生效、不用重启**;
55
+ - **JSON 后门**:编辑 `$DSH_HOME/qqbot-guard.json`
56
+ (App 里是 `/root/.dsh/qqbot-guard.json`,首次启动由 `PluginSeed` 生成);
57
+ - **profile patch 覆盖**(能写 YAML 时):
58
+
59
+ ```yaml
60
+ - id: qqbot-guard
61
+ name: 'dsh-qqbot-guard'
62
+ config:
63
+ triggers:
64
+ - '大肥鱼'
65
+ - '小鱼'
66
+ - '鱼鱼'
67
+ qqbotPackage: 'C:/Users/<you>/.dsh/profiles/<profile>/node_modules/@tencent-connect/dsh-qqbot'
68
+ verbose: true
69
+ ```
70
+
71
+ 5. **重启 App**(bundles 变更不走热重载)。
72
+
73
+ > 关键机制(`@deepseek-ai/dsh-app-boot` 的 `applyEntryPatches`):普通 patch 只能改**已存在**的 id,
74
+ > 找不到就 `warn` 并跳过;新增插件行只能用 `insert`。所以「只写一条 `- id: xxx` 到 profile patch」
75
+ > 会静默无效——踩过这个坑。
76
+
77
+ `qqbotPackage` 一般**不用写**:插件会按下面的顺序自动定位 qqbot(命中即停):
78
+
79
+ | 顺序 | 位置 | 说明 |
80
+ | --- | --- | --- |
81
+ | 1 | `config.qqbotPackage` / `DSH_QQBOT_DIR` | 显式指定优先,用于非标准布局 |
82
+ | 2 | 本插件自己所在的 `node_modules` | profile 布局:`<home>/profiles/<name>/node_modules/…`,**与 qqbot 是同一个目录** |
83
+ | 3 | `<home>/profiles/*/node_modules` | 依次枚举各 profile;另含 pnpm 的 `.pnpm` store |
84
+ | 4 | `<home>/node_modules`、`<home>/plugins/*/node_modules` | 插件市场早期的平铺布局 |
85
+ | 5 | 从 `cwd` 按 Node 规则向上找 | 裸部署/仓库内调试 |
86
+
87
+ `<home>` 依次取 `$DSH_HOME`、**从本插件的安装路径反推**(`…/profiles/<name>/node_modules/…` → `…`)、`~/.dsh`。
88
+ 第 2 条是这次修掉的关键:以前候选列表里没有 `profiles/<name>/node_modules` 这一层,手机端(qqbot 由插件市场装进 profile)
89
+ 每次都「没找到」然后**在第一步直接 return**,关键词放行、原型补丁、历史附件全部静默失效。
90
+
91
+ 只有真的找不到时才需要写死路径:
92
+
93
+ ```yaml
94
+ - id: qqbot-guard
95
+ name: 'dsh-qqbot-guard'
96
+ config:
97
+ qqbotPackage: 'C:/Users/<you>/.dsh/profiles/<profile>/node_modules/@tencent-connect/dsh-qqbot'
98
+ ```
99
+
100
+ 找不到时的日志会列出**试过的每一处**安装路径(而不是只报一句「没找到」),可直接照着补 `qqbotPackage`。
101
+
102
+ 插件声明 `inject: ['agents']`,与 qqbot 插件同级,谁先启动都行。
103
+
104
+ > APK 内置时不写 `dependencies`:`PluginSeed` 只把目录拷进 `node_modules` 并登记
105
+ > `dsh.profile.bundles`。`file:` 依赖会绑死容器路径,而 loader 按名字解析,够用。
106
+
107
+ ### 配置项
108
+
109
+ | 字段 | 默认值 | 说明 |
110
+ | --- | --- | --- |
111
+ | `enabled` | `true` | 总开关;`false` 时插件什么都不做,qqbot 行为保持原样 |
112
+ | `triggers` | `['大肥鱼', '肥鱼', '鱼鱼']` | 关键词列表,正文包含任一词即视为对我说话 |
113
+ | `ignoreCase` | `true` | 关键词是否忽略大小写 |
114
+ | `qqbotPackage` | `''` | dsh-qqbot 安装目录或它的 package.json;留空则自动探测(本插件同 profile 的 `node_modules` → `DSH_HOME/profiles/*` → `DSH_HOME` → `cwd` 向上) |
115
+ | `configFile` | `''` | 外部 JSON 配置路径;留空时依次取 `$DSH_QQBOT_GUARD_CONFIG` → `$DSH_HOME/qqbot-guard.json` → `~/.dsh/qqbot-guard.json` |
116
+ | `faces` | `true` | 把 `<faceType=…>` / `[<face,id=N/>]` 还原成 emoji;关掉则保持上游原样(上游会直接删标签) |
117
+ | `historyAttachments` | `true` | 往群历史条目里补附件摘要(`[图片「a.png」]`);关掉则历史里只剩文字 |
118
+ | `strictVersion` | `false` | SDK 版本超出白名单 `>=1.0.5 <2.0.0` 时是否放弃安装 |
119
+ | `verbose` | `true` | 是否打印诊断日志,就绪日志会带上「配置源=…」 |
120
+
121
+ 配置优先级:`DEFAULT_CONFIG` < 外部 JSON 文件 < cordis 里的 `config`。JSON 解析失败只会打一条
122
+ warn 然后忽略,不会让插件挂掉——手机上手改坏了不会变砖。
123
+
124
+ ## 设置页(可视化配置)
125
+
126
+ 包导出 `./client`(`dsh.client.platform = web`),在 DSH 里注册一个设置板块「QQ 守护」:
127
+
128
+ - 顶部状态行:qqbot 是否定位到、装在哪、SDK 版本、history 补丁是否生效——`找不到 qqbot` 这类
129
+ 故障不用再翻 App 日志;
130
+ - 关键词用每行一个的 textarea 编辑(空行忽略、自动去重),开关直接点;
131
+ - 保存写的还是 `$DSH_HOME/qqbot-guard.json`(原子写:临时文件 + rename,不会写坏);
132
+ - guard 每收一条消息前检查文件指纹(mtime + size),变了就重新读——**保存即生效**,
133
+ 在设置页和手改 JSON 之间来回切换也不会打架。
134
+
135
+ 接口是同源的 `GET/PUT /qqbot-guard/config`:只接受回环地址的 Host、拒绝跨站 Origin,
136
+ 请求体上限 64 KB,未知字段直接 400(防止手滑写进错字段还静默忽略)。
137
+
138
+ ## 自检
139
+
140
+ ```bash
141
+ node --test # 定位逻辑 + apply() 端到端回归(不需要真的装 qqbot)
142
+ DSH_QQBOT_DIR=/path/to/node_modules/@tencent-connect/dsh-qqbot node selfcheck.mjs
143
+ ```
144
+
145
+ `discovery.test.mjs` 在 tmp 里造出设备上的真实布局(guard 与 qqbot 同在
146
+ `profiles/web/node_modules`、`DSH_HOME` 未设、cwd 为空的 `work` 目录),断言能定位到包;
147
+ `boot.test.mjs` 用假 qqbot 跑一遍 `apply()`,断言不再在第一步退出、且群消息关键词能补上
148
+ `mentions.is_you`。这两个用例在旧实现上都会失败(失败点正是「没找到 @tencent-connect/dsh-qqbot」)。
149
+
150
+ 自检会:先按 qqbot 原样建 bot(补丁前)→ 装补丁 + 包装 history store → 喂五条消息
151
+ (闲聊 / 关键词 / 真 @ / 表情占位符 / 纯图片),断言链头部是 guard、关键词消息被处理、
152
+ `wasMentioned=true`、正文无 `<@` 标记、闲聊消息仍被拦、群历史已累积、历史条目无 `<@` 残留、
153
+ 空 ext 的表情变 `[表情]`、命名表情解码成名字、老格式表情仍查表、历史里有附件摘要。
154
+
155
+ ### 实测结果(qqbot 0.5.3 + qqbot-nodejs 1.0.5,Node 24.21.0)
156
+
157
+ ```
158
+ qqbot=0.5.3 sdk=1.0.5 chain=14
159
+ PASS 补丁已安装
160
+ PASS 链头部是 guard
161
+ PASS 补丁未改变原链长度(懒注入时机正确)
162
+ PASS 关键词消息被处理
163
+ PASS 关键词消息 wasMentioned=true
164
+ PASS 模型收到的是干净文本(无 <@ 标记)
165
+ PASS 无关键词的消息被门控拦下
166
+ PASS 群历史已累积
167
+ PASS history store 已包装(附件摘要生效)
168
+ PASS 历史条目无 <@ 残留
169
+ PASS 空 ext 的表情标签变成 [表情](历史里)
170
+ PASS 空 ext 不猜 faceId 对照表
171
+ PASS 命名表情解码成名字(264 → 捂脸)
172
+ PASS 老格式 [<face,id=1/>] 仍查表
173
+ PASS 历史里带附件摘要
174
+ PASS 历史里补上附件真实路径
175
+ PASS 附件路径是 POSIX 风格绝对路径
176
+ PASS 附件在 guard 里就预下载(不等 mentionGate)
177
+ PASS 预下载路径直接进历史(不靠 glob 兜底)
178
+ PASS 没有本地文件时不硬写路径
179
+ PASS 表情标签不再以占位符形式出现
180
+ ```
181
+
182
+ 全量模式(`requireMention: false`,群消息全收)下同样有效:history 为
183
+ `["闲聊一句","YONGJIE 关键词","真 @ 一句"," [图片「a.png」]"]`,关键词消息被标成 `wasMentioned=true`。
184
+ 注意全量模式本来就全放行,这里的差别只是「标记成在跟我说话」——模型会看到 `(@you)` 并拿到群历史。
185
+
186
+ ## 必须配套的一项配置
187
+
188
+ 群聊要「只有 @ 或命中关键词才回」,**必须**把 qqbot 的 `requireMention` 设为 `true`
189
+ (`cordis.patch.yml` 里 `im-qqbot` 条目):
190
+
191
+ ```yaml
192
+ - id: im-qqbot
193
+ config:
194
+ requireMention: true
195
+ ```
196
+
197
+ 原因:本插件的做法是把命中关键词的消息「伪装成 @」再交给下游 `mentionGate`;若 `requireMention: false`,
198
+ 门控对所有群消息一律放行,插件就只剩「补标记」作用,群里每句话都会触发回复。
199
+
200
+ 未 @ 的消息不会丢上下文:`dsh-qqbot/dist/gateway/middleware-setup.js:29` 把 `historyBuffer` 放在
201
+ `mentionGate`(`:41`)**之前**,且带 `recordOnSkip: true`,被门控拦下的消息照样计入群历史。
202
+
203
+ ## 已知风险
204
+
205
+ 1. **模块实例一致性**:补丁打在 `@tencent-connect/qqbot-nodejs` 的 `QQBot.prototype` 上,必须和 qqbot 解析到
206
+ 同一个文件。本插件按 Node 的 `node_modules` 向上查找规则、从 qqbot 安装目录出发定位 SDK,正常情况下即同一实例;
207
+ 若装了多份 SDK 且版本不同,可能出现补丁打在另一份上而静默失效(此时 `/消息` 行为与未装插件一致)。
208
+ 2. **`node_modules` 解析**:`qqbot-nodejs` 的 `exports` 只有 `import` 条件,`createRequire().resolve()` 在 Node 24 会抛
209
+ `ERR_PACKAGE_PATH_NOT_EXPORTED`(实测)。本插件因此手工读 package.json 的 `exports['.']`,不依赖该 API。
210
+ 3. **历史带标记(已解决,留档)**:早期版本往正文前置 `<@appId>` 来伪造提及,导致群历史条目里带
211
+ `<@102000000> …` 并被原样拼进 prompt。现在改为伪造 `mentions[].is_you`,并把真实 @ 的标记搬进
212
+ mentions 后从正文摘掉,历史是干净原文。代价是多改一个字段(`msg.mentions`),见「已知风险 1」同款顾虑。
213
+ 4. **附件会被下载两次(仅触发消息)**:关键词触发的消息里若有附件,guard 先下一次,随后
214
+ `attachmentProcessor` 又下一次——media 目录里会多一个同内容的随机名副本。这是为了“没触发的消息也能在
215
+ 历史里点开”付出的代价;`historyMedia: false` 可以关掉预下载(回到只有文件名)。
216
+ 5. **历史里的附件路径只在 24 小时内有效**:上游把附件下载到 `~/.dsh-qqbot/media`,文件名是
217
+ `<原名>_<时间戳>_<随机><ext>`(`attachment.js:60` `uniqueFilename`),后缀随机,**落库时算不出来**,
218
+ 而且下载发生在中间件链之后——所以本插件改成在读历史时按 `<原名>_*` 前缀匹配、取 mtime 最新的那个来补路径
219
+ (`resolveMediaPath`)。代价与边界:
220
+ - 媒体默认 24 小时过期(`config.media.ttlHours` 默认 24,`0`=永不过期,每小时扫一次),过期后历史里的
221
+ 路径点开就是空的——路径只当“可能还在”的线索,别当保证;
222
+ - 只保留最近 200 条带附件的消息摘要(`notes` 上限),更早的条目退化成纯文字;
223
+ - 文件名重复(同名文件在同一次会话里发多次)时取最新的那个,可能张冠李戴。
224
+ 本插件不复制、不移动用户的媒体文件。
225
+ 6. **表情 vs 收藏表情包(实测结论)**:`faceId` 是系统表情编号,`ext.text` 才是语义来源——
226
+ 系统表情会带名字(如 `faceId=264` → `{"text":"捂脸"}`)。**收藏的表情包不走 faceId**:QQ 把它作为
227
+ 图片附件下发(落到 `~/.dsh-qqbot/media/`),同时附一个空文本的 face 标签(`faceType=6,faceId="0"`)。
228
+ 所以本插件对空 `ext.text` 只给 `[表情]`,不再按 faceId 表猜 emoji(早期版本把 0 映射成 😊 是错的)。
229
+ 若想换措辞改 `normalizeFaces` 的兜底值。
230
+ 7. **版本漂移**:只用 qqbot/SDK 的四个公开面 —— `middlewares`、`handleInboundMessage`、`ctx.bot.appId`、
231
+ `ctx.state.mention`,外加 `history-store` 的 `getHistoryStore()`(拿不到就只打 warn,附件摘要自动关)。
232
+ 白名单外的版本仍会尝试安装并打 warn,出问题先跑 `selfcheck.mjs`。
233
+
234
+ ## 边界
235
+
236
+ - 不改 qqbot 源码,不写 `node_modules`,不碰 App 的 Java 层。
237
+ - 媒体缓存、脚本、手册等其余增强项不在本插件范围内(另行安排)。
238
+
239
+ ## License
240
+
241
+ MIT,见 [LICENSE](LICENSE)。Copyright (c) 2026 anwesc。