@zaofan/dsh-qqbot 1.8.8 → 1.9.2

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.
Files changed (59) hide show
  1. package/README.md +113 -26
  2. package/README_EN.md +122 -8
  3. package/dist/features/ext-capabilities.d.ts +85 -0
  4. package/dist/features/ext-capabilities.d.ts.map +1 -1
  5. package/dist/features/ext-capabilities.js +4 -0
  6. package/dist/features/ext-capabilities.js.map +1 -1
  7. package/dist/features/ext-hooks.d.ts +84 -0
  8. package/dist/features/ext-hooks.d.ts.map +1 -0
  9. package/dist/features/ext-hooks.js +299 -0
  10. package/dist/features/ext-hooks.js.map +1 -0
  11. package/dist/features/ext-inbound-context.d.ts +202 -0
  12. package/dist/features/ext-inbound-context.d.ts.map +1 -0
  13. package/dist/features/ext-inbound-context.js +724 -0
  14. package/dist/features/ext-inbound-context.js.map +1 -0
  15. package/dist/features/ext-inbound-state.d.ts +153 -0
  16. package/dist/features/ext-inbound-state.d.ts.map +1 -0
  17. package/dist/features/ext-inbound-state.js +419 -0
  18. package/dist/features/ext-inbound-state.js.map +1 -0
  19. package/dist/features/ext-inbound.d.ts +117 -0
  20. package/dist/features/ext-inbound.d.ts.map +1 -0
  21. package/dist/features/ext-inbound.js +518 -0
  22. package/dist/features/ext-inbound.js.map +1 -0
  23. package/dist/features/ext-reload.d.ts +22 -0
  24. package/dist/features/ext-reload.d.ts.map +1 -1
  25. package/dist/features/ext-reload.js +32 -0
  26. package/dist/features/ext-reload.js.map +1 -1
  27. package/dist/features/extension-store.d.ts +13 -0
  28. package/dist/features/extension-store.d.ts.map +1 -1
  29. package/dist/features/extension-store.js +32 -4
  30. package/dist/features/extension-store.js.map +1 -1
  31. package/dist/gateway/bootstrap.d.ts.map +1 -1
  32. package/dist/gateway/bootstrap.js +44 -0
  33. package/dist/gateway/bootstrap.js.map +1 -1
  34. package/dist/gateway/debounce.d.ts +43 -1
  35. package/dist/gateway/debounce.d.ts.map +1 -1
  36. package/dist/gateway/debounce.js +71 -35
  37. package/dist/gateway/debounce.js.map +1 -1
  38. package/dist/gateway/middleware-setup.d.ts.map +1 -1
  39. package/dist/gateway/middleware-setup.js +88 -1
  40. package/dist/gateway/middleware-setup.js.map +1 -1
  41. package/dist/session/session-manager.d.ts.map +1 -1
  42. package/dist/session/session-manager.js +7 -3
  43. package/dist/session/session-manager.js.map +1 -1
  44. package/dist/shared/utils.d.ts +6 -0
  45. package/dist/shared/utils.d.ts.map +1 -1
  46. package/dist/shared/utils.js +6 -2
  47. package/dist/shared/utils.js.map +1 -1
  48. package/dist/transport/inbound.d.ts.map +1 -1
  49. package/dist/transport/inbound.js +23 -3
  50. package/dist/transport/inbound.js.map +1 -1
  51. package/docs/USER-GUIDE.md +92 -35
  52. package/docs/botplay-contract.md +2 -0
  53. package/examples/ext-hooks/README.md +95 -0
  54. package/examples/ext-hooks/hello-hook.mjs +54 -0
  55. package/examples/ext-hooks/session-watch-hook.mjs +119 -0
  56. package/examples/ext-inbound/README.md +230 -0
  57. package/examples/ext-inbound/class-advisor.mjs +306 -0
  58. package/examples/ext-inbound/no-wake-on-child-quote.mjs +192 -0
  59. package/package.json +1 -1
@@ -0,0 +1,192 @@
1
+ /**
2
+ * no-wake-on-child-quote.mjs — 示例入站中间件:引用「子会话转发」的消息时不按 @ 处理
3
+ * (2026-10-12 升级:**同时演示早/晚两个挂点**;晚挂点那段逻辑一个字没改)
4
+ *
5
+ * 放在 `{数据根}/.qqbot-extensions/inbound/no-wake-on-child-quote.mjs`
6
+ * (插件启动时会自动铺一份,只补不覆盖)。
7
+ *
8
+ * ── 它解决什么 ──────────────────────────────────────────────────────────────
9
+ * 群里有人把别的会话(子会话)的转发消息**引用**过来,再顺带 @ 一下她 ——
10
+ * 平台信号上这确实是「被 @ 了」,于是:
11
+ * · 门控放行;· debounce 认为"窗口里有 @" → **绕过群冷却、静默 3 秒后强制唤醒**;
12
+ * · 上下文里那条还会被标成 `(@you)`、结构化标记 `mentioned:true`。
13
+ * 但那条被引用的原文其实只是「另一个会话的转发内容」,不该因此强行叫醒她 ——
14
+ * 她照常看到这条消息(进历史、进上下文),只是**不按"被点名"处理**(该不该开口交给她自己按守则判断)。
15
+ *
16
+ * ── 两个挂点各干什么(本文件同时演示)──────────────────────────────────────
17
+ *
18
+ * ```
19
+ * … → quoteRef → ★onInbound(早,本文件的 export function onInbound)★
20
+ * → 引用图预取 → 群历史缓冲 → … → mentionGate(@ 门控)
21
+ * → ★默认导出 / onDecide(晚,本文件的 export default)★ → contentSanitizer → debounce → …
22
+ * ```
23
+ *
24
+ * | | 早(`onInbound`) | 晚(`export default` / `onDecide`) |
25
+ * |---|---|---|
26
+ * | 能读 | `ctx.message` / `ctx.state.quote` / `ctx.platform.mentioned` | 以上全部 **+ `ctx.state.mention`**(门控判定) |
27
+ * | 能做 | 消灭 · 改写 · 强制入站 · 查/写禁言名单 | 覆盖 @ 判定 · 改写 · 强制入站 · 查/写禁言名单 |
28
+ * | 不能做 | `ctx.decide`(mention 还没写出来) | `ctx.annihilate`(群历史已经记过了,会被拒绝) |
29
+ *
30
+ * 晚挂点怎么做到"降级 @ 判定":链上位置在 mentionGate **之后**,所以这里:
31
+ * ① 读得到 `ctx.state.mention`(门控刚写下的判定);
32
+ * ② **改得动**它 —— `ctx.decide({ mention: 'ignore' })`(等价于直接写 `wasMentioned = false`,
33
+ * 但会落审计、也会把"早挂点误调"这种用法明确拒绝掉);
34
+ * ③ 还能直接不调 `next()` 把这条**整条拦掉**(本示例不用,它只是"降级"不是"拦截")。
35
+ * 被引原文的来源是现成的 `ctx.state.quote.text`(quoteRef 中间件在链前部赋的值,
36
+ * media-history 与 debounce 都在读它)——**别自己写引用解析**。
37
+ *
38
+ * 早挂点在本文件里做什么:**只读观测 + 两个默认关闭的可选动作**。
39
+ * 它是链上唯一"还没记群历史"的位置 —— 所以消灭(`ctx.annihilate`)、改写(`ctx.rewrite`)、
40
+ * 强制入站(`ctx.forceWake`)都只在这里才是"真动作"。本示例默认**不做**破坏性动作
41
+ * (它是插件自动铺给每个用户的示例,不能悄悄吞消息),把写法留在下面两个开关里:
42
+ * 想用就把开关改成 true,改完 `/tools-reload` 立即生效。
43
+ *
44
+ * ── 用法/契约速查 ──────────────────────────────────────────────────────────
45
+ * · 早挂点:`export async function onInbound(ctx, next)`(也可以是 `export const onInbound = …`);
46
+ * · 晚挂点:`export default async (ctx, next)`,**语义等同 `export async function onDecide(ctx, next)`**;
47
+ * 一个文件可以同时导出两个,互不影响;只导出其一也完全可以;
48
+ * · 可选导出 `dispose()`,插件卸载时调用(本示例没有需要收尾的东西,故不导出);
49
+ * · `{数据根}/.qqbot-extensions/inbound/` 目录不存在或为空 = 完全安静(不打任何日志);
50
+ * · 单个扩展抛错只告警并继续 `next()` —— 用户脚本坏了**绝不会**把入站消息吞掉;
51
+ * · 任何动作(annihilate/rewrite/forceWake/decide/muteUser…)内部都有 try/catch,**不会抛到链上**;
52
+ * · 改完文件后:`/tools-reload`(或工具 `tools_reload`)即可热重载;重启插件当然也生效。
53
+ */
54
+
55
+ /** 只用于日志展示(不写就用文件名) */
56
+ export const name = 'no-wake-on-child-quote';
57
+
58
+ /**
59
+ * 子会话转发前缀的宽松匹配。
60
+ *
61
+ * 宽松是刻意的:**不抠 id、不要求右括号** —— 被引原文可能被平台按 60 字截断,
62
+ * 于是 `【子会话 abc1`(无 `】`)与光秃秃的 `【子会话`(正好截在前缀处)都要命中。
63
+ * 普通文本里出现"子会话"三个字但**没有 `【`** 一律不命中(避免误伤正常聊天)。
64
+ */
65
+ const CHILD_SESSION_QUOTE_RE = /【子会话(?:\s|\u3000|$)/;
66
+
67
+ /**
68
+ * 纯函数(可单测、也可被别的扩展/debug 复用):
69
+ * 这段"被引用的原文"是不是一条子会话转发消息?
70
+ *
71
+ * @param {string|null|undefined} quotedText 被引原文(= `ctx.state.quote?.text`)
72
+ * @returns {boolean} true = 命中子会话转发前缀
73
+ */
74
+ export function isChildSessionForwardQuote(quotedText) {
75
+ const t = String(quotedText ?? '');
76
+ if (!t) return false;
77
+ return CHILD_SESSION_QUOTE_RE.test(t);
78
+ }
79
+
80
+ // ══════════════════════════════════════════════════════════════
81
+ // 开关:早挂点里两个**默认关闭**的动作示范
82
+ // ══════════════════════════════════════════════════════════════
83
+
84
+ /**
85
+ * true = 把"纯转发壳"消息**消灭**掉(不进群历史、不进上下文、不占群冷却)。
86
+ *
87
+ * 什么算纯转发壳:正文去掉 @ 之后**什么都不剩**,且**没有附件**。
88
+ * 这种消息对她没有任何信息量,却会占到一次群历史与一枚群冷却。
89
+ * ⚠️ 默认 **false**:本文件是插件自动铺给每个用户的示例,不能悄悄吞消息 ——
90
+ * 想用请自己改成 true,并确认你知道"消灭 = 这条连历史都不进"。
91
+ */
92
+ const ANNIHILATE_EMPTY_CHILD_FORWARD = false;
93
+
94
+ /**
95
+ * true = 把正文开头的转发头 `【子会话 xxx】` **改写**掉(群历史里存的就是改后文本)。
96
+ * ⚠️ 默认 **false**:转发头有时是用户自己想要的线索,先看清楚再开。
97
+ */
98
+ const REWRITE_STRIP_CHILD_PREFIX = false;
99
+
100
+ /** 转发头(比"引用判定"严格:要求带 id 与右括号)—— 只给上面两个开关用 */
101
+ const CHILD_SESSION_PREFIX_RE = /^【子会话\s*[0-9a-fA-F]{1,32}】\s*/;
102
+
103
+ /** 只留"真正的内容":去掉 @ 标记与空白,用来判断"这条是不是空壳" */
104
+ function contentWithoutMentions(ctx) {
105
+ return String(ctx?.text ?? '')
106
+ .replace(/<@!?[^>]+>/g, '') // QQ 的 @ 标记:<@openid> / <@!openid>
107
+ .replace(/@bot/g, '') // 进上下文前后插件会把 @她 换成 @bot(这里只是保险)
108
+ .replace(/\s+/g, '');
109
+ }
110
+
111
+ /**
112
+ * ★ 早挂点 `onInbound`(2026-10-12 新增)★
113
+ *
114
+ * 位置:**quoteRef 之后、记群历史之前**。这一层能读、能做的东西与晚挂点不同:
115
+ * · 读得到 `ctx.message` / `ctx.state.quote`(被引原文)/ `ctx.platform.mentioned`(平台 @ 信号);
116
+ * · **读不到** `ctx.state.mention`(那是后面 mentionGate 才写的,这里永远是 undefined —— 别读);
117
+ * · 只有在这里:`ctx.rewrite()` 能改到**群历史**里存的文本;`ctx.annihilate()` 能真的"消灭"
118
+ * (不调 next() ⇒ 后面的群历史缓冲、门控、评分、冷却全都不会跑)。
119
+ *
120
+ * ⚠️ 要把 `next()` 交出去(`return next()`):除非你**确实**想消灭这条消息。
121
+ */
122
+ export async function onInbound(ctx, next) {
123
+ try {
124
+ // 这里读不到 ctx.state.mention —— 想看"平台认为这条 @ 了她吗",用 ctx.platform.mentioned
125
+ // (判据与 SDK 的 mentionGate 完全一致:rawEventType / mentions[].is_you / 正文里的 <@appId>)
126
+ const mentioned = ctx?.platform?.mentioned === true;
127
+ const quoted = ctx?.quoted?.text ?? '';
128
+
129
+ if (ctx?.scope === 'group' && isChildSessionForwardQuote(quoted)) {
130
+ // 只读观测(debug 级,别刷屏):这一步群历史**还没记** —— 对照晚挂点那条日志的时间序
131
+ ctx?.logger?.debug?.(
132
+ `[ext-inbound:${name}] 早挂点:引用的是子会话转发(平台@=${mentioned},正文"${String(ctx.text).slice(0, 20)}")` +
133
+ ' → 群历史尚未记录,改写/消灭只能在这里做',
134
+ );
135
+
136
+ // ── 可选动作 ①:改写(把转发头从**要进群历史**的正文里去掉)──
137
+ if (REWRITE_STRIP_CHILD_PREFIX && CHILD_SESSION_PREFIX_RE.test(String(ctx.text ?? ''))) {
138
+ ctx.rewrite({
139
+ text: String(ctx.text).replace(CHILD_SESSION_PREFIX_RE, ''),
140
+ reason: '去掉子会话转发头(历史里不必留机器前缀)',
141
+ });
142
+ }
143
+
144
+ // ── 可选动作 ②:消灭(纯转发空壳:没正文、没附件,只剩一个 @)──
145
+ const atts = Array.isArray(ctx?.message?.attachments) ? ctx.message.attachments : [];
146
+ if (ANNIHILATE_EMPTY_CHILD_FORWARD && atts.length === 0 && contentWithoutMentions(ctx) === '') {
147
+ ctx.annihilate({ reason: '子会话转发空壳(无正文无附件)' });
148
+ return; // ★ 不调 next() = 这条不进历史、不进上下文、不占冷却
149
+ }
150
+
151
+ // ── 可选动作 ③:强制入站(确定要她看到这条时)──
152
+ // ctx.forceWake({ reason: '引用里点名了她' }); // 跳过 @门控 / 价值评分 / 群冷却
153
+ }
154
+ } catch {
155
+ /* 兜底:本扩展出任何问题都绝不影响主链 */
156
+ }
157
+ return next();
158
+ }
159
+
160
+ /**
161
+ * ★ 晚挂点(默认导出 = `onDecide`)★ —— 只做编排,判定逻辑在上面的纯函数里。
162
+ *
163
+ * ⚠️ 只在**群聊**生效:私聊的 `wasMentioned=true` 是 SDK 的 passthrough 标记(`reason: 'passthrough'`),
164
+ * 不是"被 @ 了"的判定;压它只会让上下文丢掉 `(@you)` 标签,而私聊本来每条都唤醒 ⇒
165
+ * 既拦不住也不该动。
166
+ *
167
+ * ⚠️ 永远 `await next()`(这里直接 `return next()`):本示例是"降级"不是"拦截",
168
+ * 绝不能把消息吞掉 —— 吞掉会让这条既不进上下文也不进历史。
169
+ */
170
+ export default async function noWakeOnChildQuote(ctx, next) {
171
+ try {
172
+ if (ctx?.message?.kind === 'group') {
173
+ const quoted = ctx?.state?.quote?.text;
174
+ if (isChildSessionForwardQuote(quoted)) {
175
+ const mention = ctx?.state?.mention;
176
+ // 只压「确实是 @ 判定为 true」的那种;别的(本来就没@)不用动
177
+ if (mention && mention.wasMentioned === true) {
178
+ // 走动作 API(而不是直接改 ctx.state.mention):会落一行审计,
179
+ // 而且"在早挂点调 decide"这种用法会被明确拒绝 + 告警。
180
+ ctx.decide({ mention: 'ignore', reason: '引用的是子会话转发消息' });
181
+ // debug 级 —— 别刷屏(群消息多,这条规则会经常命中)
182
+ ctx.log?.debug?.(
183
+ `[ext-inbound:${name}] 引用的是子会话转发消息 → 不按 @ 处理: ${String(quoted).slice(0, 40)}`,
184
+ );
185
+ }
186
+ }
187
+ }
188
+ } catch {
189
+ /* 兜底:本扩展出任何问题都绝不影响主链 */
190
+ }
191
+ return next();
192
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaofan/dsh-qqbot",
3
- "version": "1.8.8",
3
+ "version": "1.9.2",
4
4
  "description": "QQ Bot IM channel plugin for deepseek-harness (dsh)",
5
5
  "type": "module",
6
6
  "main": "./entry.js",