@zaofan/dsh-qqbot 1.5.25 → 1.5.30
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/README.md +24 -0
- package/README.md.bak-mobile-1790816881789 +281 -0
- package/README_EN.md +24 -1
- package/client/qqbot-settings.js +57 -16
- package/client/qqbot-settings.js.bak-mobile-css-1790817989692 +4912 -0
- package/client/qqbot-settings.js.bak-touch-1790818115035 +4912 -0
- package/client/qqbot-settings.js.bak-touch2-1790818130561 +4912 -0
- package/dist/channel-tools.d.ts.map +1 -1
- package/dist/channel-tools.js +10 -0
- package/dist/channel-tools.js.map +1 -1
- package/docs/USER-GUIDE.md +118 -115
- package/package.json +2 -2
- package/settings-host.js +64 -11
package/docs/USER-GUIDE.md
CHANGED
|
@@ -72,24 +72,24 @@ npx @deepseek-ai/dsh web --patch /path/to/dsh-qqbot/cordis.dev.yml
|
|
|
72
72
|
|
|
73
73
|
> 思路来源: wang-22-code/dsh-qqbot-bridge 的 QQ 审批设计(宿主 dsh `approval/request` 标准事件,官方 dsh-acp / Web 审批弹窗同款机制)。
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
### 审批卡片:谁可以点(v1.5.12+)
|
|
77
|
-
|
|
78
|
-
发起审批的卡片**发到发起者所在的会话**(群里就发到群里),但**只有下列人能点按钮**:
|
|
79
|
-
|
|
80
|
-
1. **发起者本人**
|
|
81
|
-
2. **主人白名单** —— `groupAdmin.owners`
|
|
82
|
-
|
|
83
|
-
> ⚠️ **群聊场景一定要填白名单**:设置 → QQ 机器人 →「允许操作的主人 openid(逗号分隔,可留空=不校验)」。
|
|
84
|
-
> 否则别人发起的审批,主人点了没反应(卡片只认发起者)。
|
|
85
|
-
> 私聊场景不受影响(本就只有双方)。
|
|
86
|
-
|
|
87
|
-
**这个白名单同时用于**:群管理工具的权限校验 + 审批卡片的可点名单(同一个"主人"概念)。
|
|
88
|
-
|
|
89
|
-
**排障**:若点了按钮后"本轮运行失败",且日志里有
|
|
90
|
-
`SessionFormatError: format v4 message requires a producer-owned source kind` ——
|
|
91
|
-
说明插件版本 < 1.5.12(旧代码用了 dsh V4 禁止的 `source.kind='plugin'`),升级即可。
|
|
92
|
-
|
|
75
|
+
|
|
76
|
+
### 审批卡片:谁可以点(v1.5.12+)
|
|
77
|
+
|
|
78
|
+
发起审批的卡片**发到发起者所在的会话**(群里就发到群里),但**只有下列人能点按钮**:
|
|
79
|
+
|
|
80
|
+
1. **发起者本人**
|
|
81
|
+
2. **主人白名单** —— `groupAdmin.owners`
|
|
82
|
+
|
|
83
|
+
> ⚠️ **群聊场景一定要填白名单**:设置 → QQ 机器人 →「允许操作的主人 openid(逗号分隔,可留空=不校验)」。
|
|
84
|
+
> 否则别人发起的审批,主人点了没反应(卡片只认发起者)。
|
|
85
|
+
> 私聊场景不受影响(本就只有双方)。
|
|
86
|
+
|
|
87
|
+
**这个白名单同时用于**:群管理工具的权限校验 + 审批卡片的可点名单(同一个"主人"概念)。
|
|
88
|
+
|
|
89
|
+
**排障**:若点了按钮后"本轮运行失败",且日志里有
|
|
90
|
+
`SessionFormatError: format v4 message requires a producer-owned source kind` ——
|
|
91
|
+
说明插件版本 < 1.5.12(旧代码用了 dsh V4 禁止的 `source.kind='plugin'`),升级即可。
|
|
92
|
+
|
|
93
93
|
## QQ 群管理(可选)
|
|
94
94
|
|
|
95
95
|
机器人**为群管理员**时,可开启群管理能力:实时接收「入群申请」并自动提醒主人、按申请审批入群、查询/设置群成员禁言。所有操作走腾讯官方 GroupOpenMsg 接口,错误信息已做"人话"映射(如 11703=机器人不是该群管理员、40103004=不能禁言群主/管理员、11255=群已注销)。
|
|
@@ -201,43 +201,43 @@ Invoke-WebRequest "$base/config.json" -OutFile "$dir\config.json"
|
|
|
201
201
|
- **拦截边界**:被 @ 的永远放行;带图一律放行;**只有被拦在唤醒之前**(没花 token)的那次才会**回滚**本群的回复冷却 —— 冷却本来就是用来省 token 的,token 花了就不算白花。
|
|
202
202
|
- **评分记录**:`{dataRoot}/.qqbot/value-scores.jsonl`,一行一条,字段 `score / worth / gate / min / conf / mention / img / lib / agg / top`。面板能直读,也可以让 AI 读它来维护样例库(`{dataRoot}/.qqbot/value-samples.jsonl`:`{"m":"消息文本","y":1|0}`,y=1 表示"她会想接话")。
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
## 🚫 无上下文模式(按会话省 token)(v1.5.11+)
|
|
206
|
-
|
|
207
|
-
让某个 QQ 会话**每轮只记得最近几条对话**,更早的历史自动折叠 —— 群聊省 token 利器。
|
|
208
|
-
|
|
209
|
-
### 怎么开
|
|
210
|
-
|
|
211
|
-
1. 打开 dock 面板(右下角 🛡)→「⚙ **单会话设置**」→ 子标签「🚫 **无上下文**」
|
|
212
|
-
2. 确认面板上显示的"作用对象"是你想设的那个群
|
|
213
|
-
3. 勾选「无上下文模式」,填「带 @ 前 N 条」,点「💾 保存(本会话)」
|
|
214
|
-
- 保存成功会显示「✅ 已保存并回读确认: 开启 / 带 N 条」
|
|
215
|
-
|
|
216
|
-
### 行为说明(几个容易误解的地方)
|
|
217
|
-
|
|
218
|
-
| 现象 | 说明 |
|
|
219
|
-
|---|---|
|
|
220
|
-
| **web 上还能翻到旧对话** | ✅ 正常。折叠的只是「模型视野」,**会话记录本身不删**;web 里会多一行「上下文已压缩 · 已压缩 N 条历史记录」的折叠标记 |
|
|
221
|
-
| **N 条 ≠ N 条 QQ 消息** | ⚠️ N 计的是 **dsh 侧的消息条数**。插件入站时会把"群历史 + 当前消息"**聚合成一条**,所以 1 条可能含多条 QQ 消息(群历史缓冲上限由 `historyLimit` 控制,默认 10) |
|
|
222
|
-
| **群守则每轮都重新注入** | ✅ 正常且必要。压缩会把旧的守则一起折叠,所以每轮补一份;**改了群守则会立刻生效**(去重按内容比较) |
|
|
223
|
-
| **什么时候生效** | 压缩发生在**回合开始**,所以**从下一轮起**才看不到更早的对话(当轮她已经装好上下文了) |
|
|
224
|
-
| **会调额外的模型吗** | ❌ 不会。替身文本是写死的,**零 LLM 调用**(比 dsh 自带 compaction"生成摘要"更省) |
|
|
225
|
-
|
|
226
|
-
### 全局开启(可选)
|
|
227
|
-
|
|
228
|
-
不逐会话设置也可以开全局:
|
|
229
|
-
|
|
230
|
-
```yaml
|
|
231
|
-
contextlessMode: true # 所有会话都启用
|
|
232
|
-
contextlessWindow: 5 # 保留最近 5 条
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### 存储位置
|
|
236
|
-
|
|
237
|
-
`{dataRoot}/.qqbot/contextless.json` —— 键是会话(`qqbot:<appId>:group:<群openid>`),值 `{ enabled, window }`。
|
|
238
|
-
|
|
239
|
-
> ⚠️ 别手改这个文件(面板保存会回读校验);想全关就把每个 key 的 `enabled` 改成 `false`。
|
|
240
|
-
|
|
204
|
+
|
|
205
|
+
## 🚫 无上下文模式(按会话省 token)(v1.5.11+)
|
|
206
|
+
|
|
207
|
+
让某个 QQ 会话**每轮只记得最近几条对话**,更早的历史自动折叠 —— 群聊省 token 利器。
|
|
208
|
+
|
|
209
|
+
### 怎么开
|
|
210
|
+
|
|
211
|
+
1. 打开 dock 面板(右下角 🛡)→「⚙ **单会话设置**」→ 子标签「🚫 **无上下文**」
|
|
212
|
+
2. 确认面板上显示的"作用对象"是你想设的那个群
|
|
213
|
+
3. 勾选「无上下文模式」,填「带 @ 前 N 条」,点「💾 保存(本会话)」
|
|
214
|
+
- 保存成功会显示「✅ 已保存并回读确认: 开启 / 带 N 条」
|
|
215
|
+
|
|
216
|
+
### 行为说明(几个容易误解的地方)
|
|
217
|
+
|
|
218
|
+
| 现象 | 说明 |
|
|
219
|
+
|---|---|
|
|
220
|
+
| **web 上还能翻到旧对话** | ✅ 正常。折叠的只是「模型视野」,**会话记录本身不删**;web 里会多一行「上下文已压缩 · 已压缩 N 条历史记录」的折叠标记 |
|
|
221
|
+
| **N 条 ≠ N 条 QQ 消息** | ⚠️ N 计的是 **dsh 侧的消息条数**。插件入站时会把"群历史 + 当前消息"**聚合成一条**,所以 1 条可能含多条 QQ 消息(群历史缓冲上限由 `historyLimit` 控制,默认 10) |
|
|
222
|
+
| **群守则每轮都重新注入** | ✅ 正常且必要。压缩会把旧的守则一起折叠,所以每轮补一份;**改了群守则会立刻生效**(去重按内容比较) |
|
|
223
|
+
| **什么时候生效** | 压缩发生在**回合开始**,所以**从下一轮起**才看不到更早的对话(当轮她已经装好上下文了) |
|
|
224
|
+
| **会调额外的模型吗** | ❌ 不会。替身文本是写死的,**零 LLM 调用**(比 dsh 自带 compaction"生成摘要"更省) |
|
|
225
|
+
|
|
226
|
+
### 全局开启(可选)
|
|
227
|
+
|
|
228
|
+
不逐会话设置也可以开全局:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
contextlessMode: true # 所有会话都启用
|
|
232
|
+
contextlessWindow: 5 # 保留最近 5 条
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 存储位置
|
|
236
|
+
|
|
237
|
+
`{dataRoot}/.qqbot/contextless.json` —— 键是会话(`qqbot:<appId>:group:<群openid>`),值 `{ enabled, window }`。
|
|
238
|
+
|
|
239
|
+
> ⚠️ 别手改这个文件(面板保存会回读校验);想全关就把每个 key 的 `enabled` 改成 `false`。
|
|
240
|
+
|
|
241
241
|
## 💗 好感度与熟识度(v1.5.0+)
|
|
242
242
|
|
|
243
243
|
两个维度**分开算、互不干扰**:
|
|
@@ -399,7 +399,7 @@ export default {
|
|
|
399
399
|
inputSchema: { // ⚠️ 可选参数不要写 required; 必填才写 required: true
|
|
400
400
|
sides: { type: 'integer', description: '骰子面数, 默认 6' },
|
|
401
401
|
},
|
|
402
|
-
// env: { cwd, manager, sender, replyTarget, exec } —— sender/replyTarget 可发 QQ
|
|
402
|
+
// env: { cwd, manager, sender, replyTarget, exec, ctx, logger } —— sender/replyTarget 可发 QQ 消息; ctx 是宿主上下文
|
|
403
403
|
run: async (args, env) => {
|
|
404
404
|
const sides = Math.max(2, Math.min(1000, Math.round(Number(args.sides) || 6)));
|
|
405
405
|
return { ok: true, msg: `🎲 ${1 + Math.floor(Math.random() * sides)}` };
|
|
@@ -412,8 +412,11 @@ export default {
|
|
|
412
412
|
1. 命令/工具文件都放**账号数据目录**的 `.qqbot-extensions/` 下(dataRoot 优先, 无则 cwd), 别放插件包内。
|
|
413
413
|
→ **升级/重装插件(换 node_modules)只动插件本体, 不会覆盖扩展目录**, 用户的扩展永久保留。
|
|
414
414
|
2. 工具入参 schema 用 JSON Schema 风格; **可选参数不带 required 字段**。
|
|
415
|
-
3. `run(args, env)` 的 `env = { cwd, manager, sender, replyTarget, exec }`:
|
|
415
|
+
3. `run(args, env)` 的 `env = { cwd, manager, sender, replyTarget, exec, ctx, logger }`:
|
|
416
416
|
- `sender` + `replyTarget` 就是内置 `send_media` 用的发送器 → **工具可以自己发 markdown 卡片 / 图片 / 语音 / 文件**, 不用把内容再交回 AI。
|
|
417
|
+
- `ctx` 是**宿主的插件上下文**(与内置工具同源, 1.5.30 起提供) → 扩展工具能自助调用宿主能力, 例如
|
|
418
|
+
`ctx.compaction.compactNow(agent, exec.signal, id)` 压缩上下文、`ctx.get('服务名')` 探测可选服务; `logger` 是对应日志器。
|
|
419
|
+
⚠️ 权限与内置工具**同级** —— 只适合**你自己写在 dataRoot 里的**扩展, 别把 `ctx` 转手给不可信代码。
|
|
417
420
|
- 工具返回 `{ ok, msg }`(msg 作为工具结果回给 AI); 命令返回纯文本。
|
|
418
421
|
4. 卡片正文由**你(AI)直接写 markdown**(标题/加粗/``/代码块), **本插件没有模板引擎, 不需要也不会用配置型模板**。
|
|
419
422
|
5. 生效方式: 工具发 `/tools-reload` 或调 `tools_reload` —— 新工具即时生效; **同名工具改内容会被工具注册表跳过(`already registered`) → 换名或重启宿主**; 命令一律需重启宿主(`/bot-restart`)。
|
|
@@ -489,64 +492,64 @@ sessionKey: `qqbot:${appId}:${scope}:${peerId}`,由 SHA-256 确定性派生 Se
|
|
|
489
492
|
|
|
490
493
|
解析策略:进程内复用 → 持久化恢复 → 全新创建。
|
|
491
494
|
|
|
492
|
-
|
|
493
|
-
## 常见问题(详解)
|
|
494
|
-
|
|
495
|
-
### Q: 升级 dsh 0.1.7 后,恢复会话报「预设缺失」/ 机器人不理人?
|
|
496
|
-
|
|
497
|
-
**原因**:0.1.7 把「Agent 预设」从"目录里的文件"改成了"profile 里的声明行",旧的预设目录不再被扫描。
|
|
498
|
-
|
|
499
|
-
**处理**:用插件自带的迁移脚本把预设迁到 profile 声明里(或重新在「Agent 预设」页里建一次),然后重启 dsh。
|
|
500
|
-
|
|
501
|
-
### Q: 升级 dsh 0.1.7 后,某些插件被「禁用 / 跳过」?
|
|
502
|
-
|
|
503
|
-
**原因**:0.1.7 起按 `peerDependencies` 校验插件与宿主兼容性,声明不匹配的会被跳过(打印 `skipping profile bundle ...`)。
|
|
504
|
-
|
|
505
|
-
**处理**:升级该插件,或按提示用 `dsh plugin allow-version` 显式豁免。
|
|
506
|
-
|
|
507
|
-
### Q: 升级后我自定义的配置(群守则 / 注入规则 / 互动事件)被默认值顶掉了,能找回吗?
|
|
508
|
-
|
|
509
|
-
**能。** 0.1.7 会把 `~/.dsh/settings.yaml` 一次性导入 profile,但**导入不一定落到 entry config 里**;
|
|
510
|
-
插件读不到自定义值就用默认值回写,把你的配置顶掉。
|
|
511
|
-
|
|
512
|
-
**找回步骤**:
|
|
513
|
-
1. 打开 `~/.dsh/settings.yaml.imported`(同目录通常还有 `.bak`)
|
|
514
|
-
2. 找到你实例的 section(如 `im-qqbot-2:`),再找对应字段(`groupPrompt:` / `injectRules:` / `botplayEvents:` …)
|
|
515
|
-
3. 复制内容,回 Web 设置页粘回去保存
|
|
516
|
-
(1.5.9 起配置存在插件自有存储,不会再被默认值覆盖)
|
|
517
|
-
|
|
518
|
-
> ⚠️ 群守则里常含私人信息(主人 openid、进群暗号等),**发布/分享/截图时记得剔除**。
|
|
519
|
-
|
|
520
|
-
### Q: 设置页保存后,为什么 patch 里看不到我的改动?
|
|
521
|
-
|
|
522
|
-
**这是有意设计**。设置页保存**不写 profile 的 `cordis.patch.yml`**,而是写插件自己的存储:
|
|
523
|
-
`{DSH_HOME}/qqbot-settings/<实例 id>.json`。
|
|
524
|
-
|
|
525
|
-
**为什么**:写 patch 会被宿主当成热更新提交 → 插件重新加载 → 若凭据无效又会写 → 形成死循环(实测能把 dsh 启动刷死)。
|
|
526
|
-
另外手拼 YAML 也保不住字段类型(已实测出 `expected string` / `expected array` 等问题)。
|
|
527
|
-
|
|
528
|
-
**读取顺序**:patch 值 + 自有存储覆盖(后者优先)。
|
|
529
|
-
|
|
530
|
-
### Q: 面板上「实时入群事件」开关是干什么的?打开后机器人连不上了?
|
|
531
|
-
|
|
532
|
-
**原因**:这个开关会订阅 QQ 的 `GROUP_JOIN_REQUEST` 事件(intent `1<<24`)。
|
|
533
|
-
**若开放平台没有开通该事件,网关会拒绝连接(close 4914/4915)→ 机器人直接离线**。
|
|
534
|
-
|
|
535
|
-
**正确处理顺序**:**① 平台开通事件 → ② 打开开关 → ③ 重启**。
|
|
536
|
-
|
|
537
|
-
**不想折腾平台**:用「轮询入群申请」(不需要任何授权):
|
|
538
|
-
- `轮询入群申请` 是**总开关**,不勾它,下面的选项都不生效
|
|
539
|
-
- 「唤醒 AI」与「注入群管会话」**互斥且唤醒优先** —— 两个都勾只会唤醒 AI,不会注入群管会话
|
|
540
|
-
- 想要静默注入群管会话,就**只勾「注入群管会话」**
|
|
541
|
-
|
|
542
|
-
### Q: 为什么自有存储里不要出现空对象(`{}`)?
|
|
543
|
-
|
|
544
|
-
**原因**:合并是递归深合并,但**空对象会把整块默认配置顶掉**。
|
|
545
|
-
例如 `sticker: {}` 会顶掉 patch 里的整个 `sticker` 块 → `collectEnabled` 变成 `undefined` → **图片自动下载静默停摆**
|
|
546
|
-
(症状:最后一张自动下载的图停在某个时刻,之后入站图片只剩 QQ 长 URL)。
|
|
547
|
-
|
|
548
|
-
**处理**:**想恢复默认就删掉该字段,不要写 `{}`**。
|
|
549
|
-
|
|
495
|
+
|
|
496
|
+
## 常见问题(详解)
|
|
497
|
+
|
|
498
|
+
### Q: 升级 dsh 0.1.7 后,恢复会话报「预设缺失」/ 机器人不理人?
|
|
499
|
+
|
|
500
|
+
**原因**:0.1.7 把「Agent 预设」从"目录里的文件"改成了"profile 里的声明行",旧的预设目录不再被扫描。
|
|
501
|
+
|
|
502
|
+
**处理**:用插件自带的迁移脚本把预设迁到 profile 声明里(或重新在「Agent 预设」页里建一次),然后重启 dsh。
|
|
503
|
+
|
|
504
|
+
### Q: 升级 dsh 0.1.7 后,某些插件被「禁用 / 跳过」?
|
|
505
|
+
|
|
506
|
+
**原因**:0.1.7 起按 `peerDependencies` 校验插件与宿主兼容性,声明不匹配的会被跳过(打印 `skipping profile bundle ...`)。
|
|
507
|
+
|
|
508
|
+
**处理**:升级该插件,或按提示用 `dsh plugin allow-version` 显式豁免。
|
|
509
|
+
|
|
510
|
+
### Q: 升级后我自定义的配置(群守则 / 注入规则 / 互动事件)被默认值顶掉了,能找回吗?
|
|
511
|
+
|
|
512
|
+
**能。** 0.1.7 会把 `~/.dsh/settings.yaml` 一次性导入 profile,但**导入不一定落到 entry config 里**;
|
|
513
|
+
插件读不到自定义值就用默认值回写,把你的配置顶掉。
|
|
514
|
+
|
|
515
|
+
**找回步骤**:
|
|
516
|
+
1. 打开 `~/.dsh/settings.yaml.imported`(同目录通常还有 `.bak`)
|
|
517
|
+
2. 找到你实例的 section(如 `im-qqbot-2:`),再找对应字段(`groupPrompt:` / `injectRules:` / `botplayEvents:` …)
|
|
518
|
+
3. 复制内容,回 Web 设置页粘回去保存
|
|
519
|
+
(1.5.9 起配置存在插件自有存储,不会再被默认值覆盖)
|
|
520
|
+
|
|
521
|
+
> ⚠️ 群守则里常含私人信息(主人 openid、进群暗号等),**发布/分享/截图时记得剔除**。
|
|
522
|
+
|
|
523
|
+
### Q: 设置页保存后,为什么 patch 里看不到我的改动?
|
|
524
|
+
|
|
525
|
+
**这是有意设计**。设置页保存**不写 profile 的 `cordis.patch.yml`**,而是写插件自己的存储:
|
|
526
|
+
`{DSH_HOME}/qqbot-settings/<实例 id>.json`。
|
|
527
|
+
|
|
528
|
+
**为什么**:写 patch 会被宿主当成热更新提交 → 插件重新加载 → 若凭据无效又会写 → 形成死循环(实测能把 dsh 启动刷死)。
|
|
529
|
+
另外手拼 YAML 也保不住字段类型(已实测出 `expected string` / `expected array` 等问题)。
|
|
530
|
+
|
|
531
|
+
**读取顺序**:patch 值 + 自有存储覆盖(后者优先)。
|
|
532
|
+
|
|
533
|
+
### Q: 面板上「实时入群事件」开关是干什么的?打开后机器人连不上了?
|
|
534
|
+
|
|
535
|
+
**原因**:这个开关会订阅 QQ 的 `GROUP_JOIN_REQUEST` 事件(intent `1<<24`)。
|
|
536
|
+
**若开放平台没有开通该事件,网关会拒绝连接(close 4914/4915)→ 机器人直接离线**。
|
|
537
|
+
|
|
538
|
+
**正确处理顺序**:**① 平台开通事件 → ② 打开开关 → ③ 重启**。
|
|
539
|
+
|
|
540
|
+
**不想折腾平台**:用「轮询入群申请」(不需要任何授权):
|
|
541
|
+
- `轮询入群申请` 是**总开关**,不勾它,下面的选项都不生效
|
|
542
|
+
- 「唤醒 AI」与「注入群管会话」**互斥且唤醒优先** —— 两个都勾只会唤醒 AI,不会注入群管会话
|
|
543
|
+
- 想要静默注入群管会话,就**只勾「注入群管会话」**
|
|
544
|
+
|
|
545
|
+
### Q: 为什么自有存储里不要出现空对象(`{}`)?
|
|
546
|
+
|
|
547
|
+
**原因**:合并是递归深合并,但**空对象会把整块默认配置顶掉**。
|
|
548
|
+
例如 `sticker: {}` 会顶掉 patch 里的整个 `sticker` 块 → `collectEnabled` 变成 `undefined` → **图片自动下载静默停摆**
|
|
549
|
+
(症状:最后一张自动下载的图停在某个时刻,之后入站图片只剩 QQ 长 URL)。
|
|
550
|
+
|
|
551
|
+
**处理**:**想恢复默认就删掉该字段,不要写 `{}`**。
|
|
552
|
+
|
|
550
553
|
## 设计原则
|
|
551
554
|
|
|
552
555
|
- **纯 Cordis 插件** — 遵循 dsh "Plugins, not loop changes" 原则
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zaofan/dsh-qqbot",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.30",
|
|
4
4
|
"description": "QQ Bot IM channel plugin for deepseek-harness (dsh)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./entry.js",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"immediately": true
|
|
83
83
|
},
|
|
84
84
|
"engines": {
|
|
85
|
-
"dsh": "0.1.5-rc.2
|
|
85
|
+
"dsh": ">=0.1.5-rc.2"
|
|
86
86
|
},
|
|
87
87
|
"manifestVersion": 1
|
|
88
88
|
},
|
package/settings-host.js
CHANGED
|
@@ -2888,13 +2888,28 @@ export function apply(ctx) {
|
|
|
2888
2888
|
// ⚠️ **两套都要认**: 老会话里的历史消息是渲染时解析的, 只认新标记会让旧记录显示成一堆裸标记。
|
|
2889
2889
|
const RE_TIME_HEAD = /^\s*\[(?:\d{4}-\d{2}-\d{2}\s[^\]]*|当前时间[^\]]*)\]\s*\r?\n?/;
|
|
2890
2890
|
const isTimeHeadLine = (s) => /^\[(?:\d{4}-\d{2}-\d{2}\s|当前时间)/.test(s);
|
|
2891
|
-
|
|
2892
|
-
const
|
|
2893
|
-
const
|
|
2891
|
+
// ⚠️ 标记行一律**大小写不敏感**(老英文标记会被写成小写), 且老结束标记 `[Chat history ends]` **没有斜杠**。
|
|
2892
|
+
const RE_HIST_BEGIN_LINE = /^\[(?:历史|Chat history begins)\]$/i;
|
|
2893
|
+
const RE_HIST_END_LINE = /^\[\/?(?:历史|Chat history ends)\]$/i;
|
|
2894
|
+
const RE_CURRENT_LINE = /^\[(?:当前|Current message)\]$/i;
|
|
2894
2895
|
const RE_QUOTE_BEGIN = /\[(?:引|Quoted message begins)\]/i;
|
|
2895
2896
|
const RE_QUOTE_END = /\[\/(?:引|Quoted message ends)\]/i;
|
|
2896
|
-
const RE_CURRENT_ANY = /\[(?:当前|Current message)\]\s*/
|
|
2897
|
-
|
|
2897
|
+
const RE_CURRENT_ANY = /\[(?:当前|Current message)\]\s*/gi;
|
|
2898
|
+
// 历史块标记(新旧), 出现在正文里就整段抹掉 —— 内容已由 chatSplitHistoryBlock 拆成独立气泡
|
|
2899
|
+
const RE_HIST_MARK_ANY = /\[(?:\/?历史|\/?(?:Chat history begins|Chat history ends))\]/gi;
|
|
2900
|
+
// 这些词是**标记**不是昵称: 误当发送者时气泡会显示"历史"/"引用"(头像也跟着取错首字)
|
|
2901
|
+
const RE_MARKER_WORD = /^\/?(?:历|历史|当前|引|引用|Quoted message(?: begins| ends)?|Current message|Chat history(?: begins| ends)?)$/i;
|
|
2902
|
+
// ⚠️ 2026-10-01 修(主人 dock 聊天页截图暴露):
|
|
2903
|
+
// 原实现拿**整段 body.trim()** 去套"整行 == [历史]"的正则 → 对
|
|
2904
|
+
// "[历史]\n[昵称] …\n[/历史]\n[当前]\n…" 永远不命中 → 历史打包分支形同虚设, 后果三连:
|
|
2905
|
+
// ① tagM 把第一个方括号 `[历史]` 当成发送者 → 气泡发送者显示"历史"(头像"历");
|
|
2906
|
+
// ② `[/历史]` 原样留在末尾(前端只认 [当前]/[引]…[/引], 不认它);
|
|
2907
|
+
// ③ `[昵称] [图片: D:\…\x.gif]` 整行当正文 → 裸文本 + **本机绝对路径外泄**。
|
|
2908
|
+
// 改为**逐行**判定(老英文标记在行内任意位置也算)。
|
|
2909
|
+
const hasHistoryBlock = (s) => String(s || '').split(/\r?\n/).some((line) => {
|
|
2910
|
+
const t = line.trim();
|
|
2911
|
+
return RE_HIST_BEGIN_LINE.test(t) || /\[Chat history begins\]/i.test(t);
|
|
2912
|
+
});
|
|
2898
2913
|
function chatPeelTimeHead(text) {
|
|
2899
2914
|
// QQ 入站文本头形如 "[2026-09-15 周二 11:06]\n\n"(老版是 "[当前时间 2026-09-05 周六 19:08]"), 整体剥掉
|
|
2900
2915
|
return String(text || '').replace(RE_TIME_HEAD, '');
|
|
@@ -3036,6 +3051,12 @@ export function apply(ctx) {
|
|
|
3036
3051
|
// 优先取 "→ " 之后到行尾(路径里可能有空格); 没有箭头就在行内找绝对路径
|
|
3037
3052
|
const arrow = l.match(/→\s*([\s\S]+?)\s*$/);
|
|
3038
3053
|
let cand = arrow ? arrow[1].trim() : '';
|
|
3054
|
+
if (!cand) {
|
|
3055
|
+
// 2026-10-01: `[图片: <本机路径>]` / `[File: <本机路径>]` 整行 —— 路径可能含空格,
|
|
3056
|
+
// 下面按空白截断的正则会切坏它, 所以先按"冒号后到右括号"整体取一次
|
|
3057
|
+
const br = l.match(/^\s*\[[^\]::]+[::]\s*([\s\S]+?)\s*\]\s*$/);
|
|
3058
|
+
if (br && /(?:[A-Za-z]:[\\/]|^\\\\|\/)/.test(br[1])) cand = br[1].trim();
|
|
3059
|
+
}
|
|
3039
3060
|
if (!cand) {
|
|
3040
3061
|
const m2 = l.match(/([A-Za-z]:[\\/][^\s]+)/);
|
|
3041
3062
|
cand = m2 ? m2[1] : '';
|
|
@@ -3258,11 +3279,32 @@ export function apply(ctx) {
|
|
|
3258
3279
|
function chatPolishOne(body, fallbackSender, nameByMid) {
|
|
3259
3280
|
let sender = fallbackSender;
|
|
3260
3281
|
let b = String(body || '');
|
|
3261
|
-
b = b.replace(RE_CURRENT_ANY, '');
|
|
3262
|
-
|
|
3263
|
-
//
|
|
3264
|
-
|
|
3265
|
-
|
|
3282
|
+
b = b.replace(RE_CURRENT_ANY, ' ');
|
|
3283
|
+
// 2026-10-01: 历史块标记(新旧)先抹掉 —— 内容已被 chatSplitHistoryBlock 拆成独立气泡,
|
|
3284
|
+
// 残留标记会被下面的 tagM 当发送者(气泡显示"历史"), 也会在末尾显示成裸文本
|
|
3285
|
+
b = b.replace(RE_HIST_MARK_ANY, ' ');
|
|
3286
|
+
// 2026-10-01: 引用块**不再折叠成 `[引用]`**(v0.8.0 的老做法) —— 前端 chatRenderText 本来就有
|
|
3287
|
+
// `[引]…[/引]` 的引用气泡样式(左边框+灰底), 折叠后原文丢失、只剩一个裸方括号,
|
|
3288
|
+
// 还会被下面的昵称提取当成发送者(气泡显示"引用")。这里先"摘出来占位",
|
|
3289
|
+
// 等昵称壳剥完再原样放回; 超长截断(被引用的原文可能有几百字)。
|
|
3290
|
+
const quotes = [];
|
|
3291
|
+
b = b.replace(new RegExp(`${RE_QUOTE_BEGIN.source}\\s*([\\s\\S]*?)\\s*${RE_QUOTE_END.source}`, 'g'), (_all, inner) => {
|
|
3292
|
+
const q = String(inner || '').trim();
|
|
3293
|
+
quotes.push('[引]' + (q.length > 300 ? q.slice(0, 300) + '…' : q) + '[/引]');
|
|
3294
|
+
return '\u0000Q' + (quotes.length - 1) + '\u0000';
|
|
3295
|
+
});
|
|
3296
|
+
// 2026-09-12 适配: "[昵称]" 与 "[昵称 (openid)]" 都认; 排除含冒号的方括号(附件标记)
|
|
3297
|
+
// 2026-10-01: **标记词**(`[引]`/`[历史]`/`[当前]`…)不算昵称, 跳过它继续往后找真昵称
|
|
3298
|
+
const reTag = /\[([^\]\n:]*?)(?:\s*\([A-Za-z0-9_-]{6,}\))?\]/g;
|
|
3299
|
+
let tmm;
|
|
3300
|
+
while ((tmm = reTag.exec(b)) !== null) {
|
|
3301
|
+
const nm = tmm[1].trim();
|
|
3302
|
+
if (!nm || RE_MARKER_WORD.test(nm)) continue;
|
|
3303
|
+
sender = nm;
|
|
3304
|
+
b = b.replace(tmm[0], '');
|
|
3305
|
+
break;
|
|
3306
|
+
}
|
|
3307
|
+
if (quotes.length) b = b.replace(/\u0000Q(\d+)\u0000/g, (_all, i) => quotes[Number(i)] || '');
|
|
3266
3308
|
const sysIdx = b.indexOf('[系统提示]');
|
|
3267
3309
|
if (sysIdx >= 0) b = b.slice(0, sysIdx).replace(/\s*$/, '');
|
|
3268
3310
|
const mm = chatSplitMedia(chatDisplayClean(b, nameByMid));
|
|
@@ -3284,9 +3326,20 @@ export function apply(ctx) {
|
|
|
3284
3326
|
const time = typeof ev.time === 'number' ? ev.time : 0;
|
|
3285
3327
|
if (ev.type === 'user/message') {
|
|
3286
3328
|
const src = data.source && typeof data.source === 'object' ? data.source : {};
|
|
3287
|
-
|
|
3329
|
+
// ⚠️ 2026-10-01 修(主人截图: 整段 `<system-reminder>` 系统提示词漏进了 dock 聊天界面):
|
|
3330
|
+
// `user/message` 在 dsh 会话里**不只承载真人发言** —— 插件/宿主的运行时注入也走它:
|
|
3331
|
+
// · 固定通道规则 + 群守则 → source.kind `qqbot:group-rules`(session-manager 拼的 <system-reminder>)
|
|
3332
|
+
// · QQ 审批提醒 → source.kind `plugin:qqbot-approval`
|
|
3333
|
+
// · 上下文压缩检查点 → source.kind `compact-checkpoint`
|
|
3334
|
+
// · 其它插件/扩展工具 → source.kind `plugin:<name>`
|
|
3335
|
+
// 原实现只挡了 `kind === 'plugin'`(还是会话格式 V4 已废弃的写法) → 以上全漏。
|
|
3336
|
+
// 真人发言的 kind 是 `user`; 这里按"插件自有 kind 前缀"黑名单 + 内容特征双保险。
|
|
3337
|
+
const srcKind = typeof src.kind === 'string' ? src.kind : '';
|
|
3338
|
+
if (srcKind === 'plugin' || srcKind.startsWith('plugin:') || srcKind.startsWith('qqbot:') || srcKind === 'compact-checkpoint') return null;
|
|
3288
3339
|
const raw0 = chatTextOf(data.content);
|
|
3289
3340
|
if (!raw0) return null;
|
|
3341
|
+
// 兜底: 运行时上下文(user-role 快照)一律包在 <system-reminder> 里 → 不是对方说的话, 不进聊天视图
|
|
3342
|
+
if (/^\s*<system-reminder>/.test(raw0)) return null;
|
|
3290
3343
|
chatRawDiag(raw0);
|
|
3291
3344
|
const raw = chatPeelTimeHead(raw0);
|
|
3292
3345
|
const isRelay = /^用户代你发送: /.test(raw);
|