@zaofan/dsh-qqbot 1.10.6 → 1.10.7

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.
@@ -91,6 +91,27 @@ export function isChildSessionForwardQuote(quotedText) {
91
91
  */
92
92
  const ANNIHILATE_EMPTY_CHILD_FORWARD = false;
93
93
 
94
+ /**
95
+ * ★★ 2026-10-15 主人定:**只要引用了子会话转发消息,整条消灭** ★★
96
+ *
97
+ * 为什么比上面那个开关更强:二档的语义是"主人**完全独立地**跟子会话对话" ——
98
+ * 子会话的内容**一个字都不该进父会话上下文**。而 QQ 的引用会把**被引原文**
99
+ * 一起带进上下文(`buildQuotePart` 会把它拼成引用块),等于**绕过二档**。
100
+ * 更要紧的是:QQ 引用**自带 @ 机器人**,所以这种消息还必然触发一次 @ 判定 ——
101
+ * 与其"剥掉原文/压掉 @"两头补,不如**整条吃掉**(主人原话:懒得删了)。
102
+ *
103
+ * ⚠️ 代价(有意接受):主人在这条消息里**自己写的正文也一起消失** ——
104
+ * 想跟本尊说话就别引用子会话转发的消息,直接说即可。
105
+ * ⚠️ 对**所有人**生效(群友引用子会话转发也会被吃);若只想管主人,
106
+ * 把 ownerOnly 改成 true(下面消灭分支里已留好位置)。
107
+ * ⚠️ 消灭 = 不进群历史、不进上下文、不占群冷却(`annihilate` 的语义)。
108
+ * ⚠️ 主人消息要被吃必须带 `ownerAck: true`(默认白名单保护主人消息,不给就拒绝执行)。
109
+ */
110
+ const ANNIHILATE_ANY_CHILD_QUOTE = true;
111
+
112
+ /** 上面那个开关是否只对主人(owner)生效。false = 群里任何人引用都吃 */
113
+ const ANNIHILATE_ANY_CHILD_QUOTE_OWNER_ONLY = false;
114
+
94
115
  /**
95
116
  * true = 把正文开头的转发头 `【子会话 xxx】` **改写**掉(群历史里存的就是改后文本)。
96
117
  * ⚠️ 默认 **false**:转发头有时是用户自己想要的线索,先看清楚再开。
@@ -108,6 +129,71 @@ function contentWithoutMentions(ctx) {
108
129
  .replace(/\s+/g, '');
109
130
  }
110
131
 
132
+ /**
133
+ * ★ 2026-10-15 新增:与 route-isolate 共用同一个会话库。
134
+ *
135
+ * 用途只有一个 —— **识别"这条消息带不带路由标记"**(`lib.parseRouteKey`)。
136
+ * 带路由标记的消息归 `route-isolate.mjs` 管(它负责"先消灭、再自己投递给子会话"),
137
+ * 本扩展必须**让路**;否则按文件名排序本文件跑在前面,抢先吃掉又 return,
138
+ * route-isolate 永远不会执行 ⇒ 消息既没进父上下文、也没路由到子会话。
139
+ *
140
+ * 引法与 route-isolate 完全一致(带 `?v=mtime` 做 cache-bust,防 ESM 注册表缓存旧模块)。
141
+ */
142
+ let _lib = null;
143
+ async function loadLib() {
144
+ if (_lib) return _lib;
145
+ try {
146
+ _lib = await import(new URL('../tools/lib/session_watch_lib.mjs?v=' + Date.now(), import.meta.url).href);
147
+ } catch {
148
+ _lib = null;
149
+ }
150
+ return _lib;
151
+ }
152
+
153
+ /**
154
+ * ★★ 2026-10-15:诊断必须**落盘** ★★
155
+ * route-isolate 文件头留的教训:"一开始只往 logger 写,而**从来没人注入过 logger**
156
+ * ⇒ 真机上完全静默、无法定位。" 本扩展此前一直用 `ctx.logger.debug` ⇒ 等于瞎的,
157
+ * 导致"引用回流没投递"排查了三轮全靠猜。此后所有关键判定一律 `appendDiag` 落盘。
158
+ */
159
+ const DIAG_FILE = 'D:/newwenjianjia/aiwork/鲸鱼娘/dshqqbot/.qqbot-extensions/data/no-wake-diag.log';
160
+
161
+ async function appendDiag(line) {
162
+ try {
163
+ const { appendFile, mkdir } = await import('node:fs/promises');
164
+ const { dirname } = await import('node:path');
165
+ await mkdir(dirname(DIAG_FILE), { recursive: true });
166
+ await appendFile(DIAG_FILE, new Date().toISOString() + ' ' + line + '\n');
167
+ } catch { /* 诊断失败绝不影响主链 */ }
168
+ }
169
+
170
+ /**
171
+ * ★★ 2026-10-15:稳妥读"被引原文" ★★
172
+ * 契约(`ext-inbound-context.ts` 注释)写的是 `ctx.quoted({ text, fromSelf })`,
173
+ * 而 316 行又说它是"**可调用 + 带属性的对象**(两种读法都支持)"。
174
+ * 之前只按 `.text` 读 ⇒ 若它是函数就读到空串 ⇒ 正则永远不命中 ⇒ 整条分支静默跳过。
175
+ * 这里两种读法都试,并兜到 `ctx.state.quote.text`(晚挂点用的那个)。
176
+ */
177
+ function readQuotedText(ctx) {
178
+ const out = { text: '', via: 'none', type: typeof ctx?.quoted };
179
+ try {
180
+ const q = ctx?.quoted;
181
+ if (typeof q === 'function') {
182
+ const v = q();
183
+ out.via = 'call';
184
+ out.text = String(v?.text ?? '');
185
+ }
186
+ if (!out.text) {
187
+ const t = q?.text ?? ctx?.state?.quote?.text ?? '';
188
+ if (t) out.via = out.via === 'call' ? 'call+prop' : 'prop';
189
+ out.text = String(t ?? '');
190
+ }
191
+ } catch (e) {
192
+ out.via = 'throw:' + (e?.message ?? e);
193
+ }
194
+ return out;
195
+ }
196
+
111
197
  /**
112
198
  * ★ 早挂点 `onInbound`(2026-10-12 新增)★
113
199
  *
@@ -124,9 +210,83 @@ export async function onInbound(ctx, next) {
124
210
  // 这里读不到 ctx.state.mention —— 想看"平台认为这条 @ 了她吗",用 ctx.platform.mentioned
125
211
  // (判据与 SDK 的 mentionGate 完全一致:rawEventType / mentions[].is_you / 正文里的 <@appId>)
126
212
  const mentioned = ctx?.platform?.mentioned === true;
127
- const quoted = ctx?.quoted?.text ?? '';
213
+ // ★ 2026-10-15:改用 readQuotedText(两种读法都试),并把读到的东西**落盘**
214
+ const q = readQuotedText(ctx);
215
+ const quoted = q.text ?? '';
216
+ const hit = isChildSessionForwardQuote(quoted);
217
+ void appendDiag('[enter] text=' + JSON.stringify(String(ctx?.text ?? '').slice(0, 40))
218
+ + ' quotedType=' + q.type + ' via=' + q.via
219
+ + ' quoted=' + JSON.stringify(quoted.slice(0, 60))
220
+ + ' hit=' + hit + ' scope=' + String(ctx?.scope ?? '-'));
128
221
 
129
222
  if (ctx?.scope === 'group' && isChildSessionForwardQuote(quoted)) {
223
+ // ── ★ 2026-10-15 主人定:只要引用了子会话转发 ⇒ **整条消灭**(不再区分有没有正文)★ ──
224
+ // 理由:二档 = 主人完全独立地跟子会话对话,子会话内容一个字都不该进父上下文;
225
+ // 而 QQ 引用会把被引原文带进上下文(绕过二档),且引用自带 @ 机器人 ⇒ 干脆整条吃掉。
226
+ if (ANNIHILATE_ANY_CHILD_QUOTE) {
227
+ // ★★ 2026-10-15 重写(第三版):语义 = 「引用子会话转发消息」等价于「发给那个子会话」★★
228
+ // 要做三件事,缺一不可 —— 照 route-isolate.mjs 的姿势抄:
229
+ // ① 从**被引原文**里解析出子会话 key(`【子会话 bee798c0】`);
230
+ // ② `annihilate({ ownerAck: true })` 登记消灭;
231
+ // ③ **自己投递**(lib.routeToSession)—— 消息被消灭后,原路径的
232
+ // `user/message` 钩子永远不会发生,没人投递就等于消息凭空消失;
233
+ // ④ 末尾 **`return next()`**(★不是"消灭完就 return"★)。
234
+ // 引擎兜底只在"没调 next"时替扩展放行;调了 next,annihilate 的置位
235
+ // 会**直接吃掉那记 next**(链停在这里 + 跳过兜底)⇒ 主人生效且真被消灭。
236
+ // 上一版写成"消灭后直接 return",反而触发了兜底放行、还把投递漏了。
237
+ let routeKey = null;
238
+ try {
239
+ const lib0 = await loadLib();
240
+ routeKey = lib0?.parseRouteKey?.(String(ctx?.text ?? '')) ?? null;
241
+ } catch { routeKey = null; }
242
+
243
+ if (!routeKey) {
244
+ // 从被引原文解析子会话短 id(宽松:不要求右括号,平台可能按 60 字截断)
245
+ const m = /【子会话[\s\u3000]*([0-9a-fA-F]{1,32})/.exec(String(quoted));
246
+ const childKey = m ? m[1] : null;
247
+ const kernel = ctx?.kernel ?? null;
248
+ void appendDiag('[branch] routeKey=- childKey=' + (childKey || '-')
249
+ + ' kernel=' + typeof ctx?.kernel + ' annihilate=' + typeof ctx?.annihilate);
250
+
251
+ if (!childKey) {
252
+ void appendDiag('[skip] 引用的是子会话转发但解析不出 id ⇒ 放行');
253
+ } else {
254
+ const acked = ctx?.annihilate?.({
255
+ reason: '引用子会话转发消息:对父上下文消灭(二档语义),并投递给子会话 ' + childKey,
256
+ ownerAck: true, // 主人消息默认受白名单保护,必须显式解除;只解除这一条
257
+ });
258
+ void appendDiag('[annihilate] key=' + childKey + ' acked=' + acked);
259
+
260
+ if (!acked) {
261
+ // 消灭被拒(熔断/挂点不允许)⇒ 不自己投,交回原路径,避免"我投一次 + 原路径再投一次"
262
+ void appendDiag('[skip] annihilate 被拒 key=' + childKey + ' ⇒ 放行');
263
+ } else {
264
+ // 已登记消灭 ⇒ 必须自己投递(原路径的 user/message 钩子不会跑了)
265
+ let r = null;
266
+ try {
267
+ const lib = await loadLib();
268
+ // ★★ 2026-10-15 关键:**必须把 key 补进文本** ★★
269
+ // `routeToSession` 内部自己 `parseRouteKey(text)`;引用这条路**没有** `[s:xxx]`
270
+ // 标记 ⇒ 会直接返回 `no-key` 而不投递(实测 diag:投递=FAIL(no-key))。
271
+ // 补上标记后它就能解析到目标;随后 `stripRouteMark(text)` 会把标记剥掉,
272
+ // 所以**子会话收到的正文里不会留下这个前缀**。
273
+ const textToRoute = '[s:' + childKey + ']' + String(ctx.text ?? '');
274
+ r = await lib?.routeToSession?.(kernel, textToRoute, { message: ctx?.message });
275
+ } catch (e) {
276
+ r = { ok: false, reason: 'throw:' + (e?.message ?? e) };
277
+ }
278
+ void appendDiag('[done] key=' + childKey
279
+ + ' 已从父上下文消灭;投递=' + (r?.ok ? 'ok(' + r.reason + ')' : 'FAIL(' + r?.reason + ')'));
280
+ }
281
+ }
282
+ } else {
283
+ // 带路由标记 ⇒ 让路给 route-isolate(它负责消灭 + 投递),本扩展什么都不做
284
+ void appendDiag('[pass] 带路由标记 key=' + routeKey + ' → 让路给 route-isolate');
285
+ }
286
+ // ★★ 末尾必须调 next():见上面 ④。annihilate 成功时它会吃掉这记 next。★★
287
+ return next();
288
+ }
289
+
130
290
  // 只读观测(debug 级,别刷屏):这一步群历史**还没记** —— 对照晚挂点那条日志的时间序
131
291
  ctx?.logger?.debug?.(
132
292
  `[ext-inbound:${name}] 早挂点:引用的是子会话转发(平台@=${mentioned},正文"${String(ctx.text).slice(0, 20)}")` +
@@ -144,8 +304,10 @@ export async function onInbound(ctx, next) {
144
304
  // ── 可选动作 ②:消灭(纯转发空壳:没正文、没附件,只剩一个 @)──
145
305
  const atts = Array.isArray(ctx?.message?.attachments) ? ctx.message.attachments : [];
146
306
  if (ANNIHILATE_EMPTY_CHILD_FORWARD && atts.length === 0 && contentWithoutMentions(ctx) === '') {
147
- ctx.annihilate({ reason: '子会话转发空壳(无正文无附件)' });
148
- return; // ★ 不调 next() = 这条不进历史、不进上下文、不占冷却
307
+ // ⚠️ 2026-10-15:消灭后必须**继续交给 next()**(不是直接 return)——
308
+ // 引擎兜底只在"没调 next"时替扩展放行;调了 next,annihilate 会吃掉它。
309
+ ctx.annihilate({ reason: '子会话转发空壳(无正文无附件)', ownerAck: true });
310
+ return next();
149
311
  }
150
312
 
151
313
  // ── 可选动作 ③:强制入站(确定要她看到这条时)──
@@ -662,23 +662,132 @@ export function buildSettlementNotice(input = {}) {
662
662
  async function forwardToQq(entry, text) {
663
663
  const fwd = entry?.fwd;
664
664
  const body = String(text ?? '').trim();
665
- if (!fwd?.sender || !fwd?.replyTarget || !body) return '';
665
+ // ★ 2026-10-15:**不再静默**。之前三个条件任一不满足就 `return ''`,日志里一个字都没有,
666
+ // "重启后那段时间子会话内容悄悄没转发"因此无从定位(主人实测踩到)。
667
+ if (!fwd?.sender || !fwd?.replyTarget || !body) {
668
+ void swDiag('forward-skip short=' + String(entry?.shortId ?? '?')
669
+ + ' fwd=' + (fwd ? 'obj' : String(fwd))
670
+ + ' sender=' + !!fwd?.sender + ' replyTarget=' + !!fwd?.replyTarget
671
+ + ' bodyLen=' + body.length);
672
+ return '';
673
+ }
666
674
  const target = fwd.replyTarget;
667
- try {
668
- if (typeof fwd.sender.sendMarkdown === 'function') {
669
- await fwd.sender.sendMarkdown(target, body);
670
- return 'md';
675
+
676
+ /**
677
+ * ★★ 2026-10-15:**摘掉 msgId 的"主动版" target** ★★
678
+ * 实测真凶:`fwd.replyTarget` 里钉着**绑定那一刻的 msgId**(为走"被动回复"),
679
+ * 而 QQ 的被动回复 **msg_id 会过期** ⇒ 老盯梢(如 bee798c0)发送时报
680
+ * `API Error: 回复消息msg_id已过期`,markdown/纯文本两条降级链**都**因此失败,
681
+ * 内容静默丢失。新建的盯梢 msgId 新鲜 ⇒ 正常。
682
+ * ⇒ 发送失败时摘掉 msgId 再发一次 = 转"主动发送"(会耗主动配额,但总比丢消息好)。
683
+ */
684
+ const activeTarget = (() => {
685
+ try {
686
+ if (!target || typeof target !== 'object') return null;
687
+ const t = { ...target };
688
+ let touched = false;
689
+ for (const k of ['msgId', 'msg_id', 'messageId', 'referenceMessageId']) {
690
+ if (k in t) { delete t[k]; touched = true; }
691
+ }
692
+ return touched ? t : null;
693
+ } catch { return null; }
694
+ })();
695
+
696
+ /**
697
+ * ★ 2026-10-15 主人定:**同一 msgId 用满 4 条就直接转主动**(不等它失败)。
698
+ * 被动回复有两条硬限制:①同一 msg_id 最多 ~4-5 条;②msg_id 会过期。
699
+ * 插件的 outbound.ts 对"AI 正文"有 ADAPTIVE_MAX_PASSIVE=5 的同类计数,但**转发这条路没有**,
700
+ * 所以这里自己数:同一 msgId 发到第 4 条起就摘掉它走主动。
701
+ */
702
+ const _midOf = (t) => String(t?.msgId ?? t?.msg_id ?? t?.messageId ?? '');
703
+ const passiveMid = _midOf(target);
704
+ const counts = (entry.__fwdPassiveCounts ??= new Map());
705
+ const used = passiveMid ? (counts.get(passiveMid) ?? 0) : 0;
706
+ const PASSIVE_LIMIT = 4;
707
+ const skipPassive = !passiveMid || used >= PASSIVE_LIMIT;
708
+ if (skipPassive) {
709
+ void swDiag('forward-passive-skip short=' + String(entry?.shortId ?? '?')
710
+ + ' mid=' + (passiveMid ? passiveMid.slice(0, 12) : '-') + ' used=' + used);
711
+ }
712
+
713
+ const trySend = async (tg) => {
714
+ try {
715
+ if (typeof fwd.sender.sendMarkdown === 'function') {
716
+ await fwd.sender.sendMarkdown(tg, body);
717
+ return 'md';
718
+ }
719
+ } catch (e) {
720
+ const em = String(e?.message ?? e);
721
+ try {
722
+ if (typeof fwd.sender.sendText === 'function') {
723
+ await fwd.sender.sendText(tg, body);
724
+ return 'text';
725
+ }
726
+ } catch (e2) { throw new Error(em + ' | text:' + String(e2?.message ?? e2)); }
727
+ throw new Error(em);
671
728
  }
672
- } catch { /* markdown 失败 → 退纯文本 */ }
729
+ try {
730
+ if (typeof fwd.sender.sendText === 'function') {
731
+ await fwd.sender.sendText(tg, body);
732
+ return 'text';
733
+ }
734
+ } catch (e) { throw new Error(String(e?.message ?? e)); }
735
+ return '';
736
+ };
737
+
738
+ // ① 先按原样(被动回复,省主动配额)
673
739
  try {
674
- if (typeof fwd.sender.sendText === 'function') {
675
- await fwd.sender.sendText(target, body);
676
- return 'text';
740
+ // ★ 同一 msgId 用满 4 条 ⇒ 直接跳过被动、走下面的主动分支(不再等它失败)
741
+ if (skipPassive) throw new Error('skip-passive: mid=' + (passiveMid || '-') + ' used=' + used);
742
+ const how = await trySend(target);
743
+ if (how) {
744
+ if (passiveMid) counts.set(passiveMid, used + 1);
745
+ void swDiag('forward-ok(' + how + ') short=' + String(entry?.shortId ?? '?') + ' len=' + body.length
746
+ + ' passive=' + (passiveMid ? passiveMid.slice(0, 12) + '#' + (used + 1) : '-'));
747
+ return how;
677
748
  }
678
- } catch { /* ignore */ }
749
+ } catch (e1) {
750
+ void swDiag('forward-passive-fail short=' + String(entry?.shortId ?? '?')
751
+ + ' err=' + String(e1?.message ?? e1));
752
+ // ② 失败 ⇒ 摘掉 msgId 重发(主动)。这是"msg_id 过期"的正解。
753
+ if (activeTarget) {
754
+ try {
755
+ const how2 = await trySend(activeTarget);
756
+ if (how2) {
757
+ void swDiag('forward-ok-active(' + how2 + ') short=' + String(entry?.shortId ?? '?')
758
+ + ' len=' + body.length + '(被动失败后转主动)');
759
+ return how2 + '-active';
760
+ }
761
+ } catch (e2) {
762
+ void swDiag('forward-active-fail short=' + String(entry?.shortId ?? '?')
763
+ + ' err=' + String(e2?.message ?? e2));
764
+ }
765
+ } else {
766
+ void swDiag('forward-active-skip short=' + String(entry?.shortId ?? '?')
767
+ + '(target 上没有 msgId 可摘,或 target 形状异常)');
768
+ }
769
+ }
770
+ void swDiag('forward-fail short=' + String(entry?.shortId ?? '?') + ' senderKeys='
771
+ + (fwd?.sender ? Object.keys(fwd.sender).slice(0, 10).join(',') : '-'));
679
772
  return '';
680
773
  }
681
774
 
775
+ /**
776
+ * ★ 2026-10-15:诊断**落盘**。route-isolate 文件头留过教训:
777
+ * "一开始只往 logger 写,而**从来没人注入过 logger** ⇒ 真机上完全静默、无法定位。"
778
+ * 本库涉及"转发到 QQ",静默一次就是一条内容永久丢失,必须能事后追。
779
+ */
780
+ const SW_DIAG_FILE = 'D:/newwenjianjia/aiwork/鲸鱼娘/dshqqbot/.qqbot-extensions/data/session-watch-diag.log';
781
+
782
+ async function swDiag(line) {
783
+ try {
784
+ const { appendFile, mkdir } = await import('node:fs/promises');
785
+ const { dirname } = await import('node:path');
786
+ await mkdir(dirname(SW_DIAG_FILE), { recursive: true });
787
+ await appendFile(SW_DIAG_FILE, new Date().toISOString() + ' ' + line + '\n');
788
+ } catch { /* 诊断失败不影响主链 */ }
789
+ }
790
+
682
791
  /**
683
792
  * 转发到 QQ 之前,把「只给插件自己看的标记」洗掉(主人 2026-10-12 要求:"剥掉 @")。
684
793
  *
@@ -975,11 +1084,40 @@ async function handler(root, session, event) {
975
1084
  w.errors = [];
976
1085
  const mode = Number(w.mode) === 2 ? 2 : (Number(w.mode) === 3 ? 3 : 1);
977
1086
 
1087
+ // ★★ 2026-10-15:**每回合无条件落盘一行** ★★
1088
+ // 实测踩到:bee798c0 重启后"不转发",而日志里连 forward-skip 都没有 ⇒ 说明它压根没
1089
+ // 走到 forwardToQq(`toForward` 为空、for 循环没进)。要区分"没收到正文事件"和"收到但被过滤",
1090
+ // 只能在 turn/end 这里把手上有什么如实记下来。
1091
+ void swDiag('turn-end short=' + String(w.shortId ?? '?')
1092
+ + ' mode=' + mode + ' rawMode=' + JSON.stringify(w.mode)
1093
+ + ' texts=' + allTexts.length + ' errs=' + errors.length
1094
+ + ' textLen=' + String(text ?? '').length
1095
+ + ' seen=' + w.seen + ' reason=' + String(rawReason ?? '-'));
1096
+
978
1097
  // ── 转发到 QQ ──────────────────────────────────────────────
979
1098
  // 一档:只转末段;二/三档:所有正文 + 报错
980
1099
  const toForward = mode === 1
981
1100
  ? (text ? [text] : [])
982
1101
  : [...allTexts, ...(errors.length ? ['⚠️ 报错:\n' + errors.join('\n')] : [])];
1102
+ // ★★ 2026-10-15 主人定:转发前**先刷新 target + 数被动条数**,别钉死绑定时的 msgId ★★
1103
+ // 被动回复(带 msg_id)有两条硬限制:①同一 msg_id 最多回 ~4-5 条;②msg_id 会过期。
1104
+ // 踩到的实例:老盯梢 bee798c0 的 replyTarget 里钉着很久以前的 msgId ⇒ 发送报
1105
+ // `回复消息msg_id已过期`,md/文本两条链全挂、内容静默丢失。
1106
+ // 口径(与插件 outbound.ts 的 ADAPTIVE_MAX_PASSIVE=5 对齐):
1107
+ // · 每次转发前,把 fwd.replyTarget 刷新成**最新注入**的那份(带最新 msgId);
1108
+ // · 同一 msgId 数着用,**到 4 条**就主动转主动(摘掉 msgId),不再等它失败;
1109
+ // · 真失败(过期/超限/其它)⇒ forwardToQq 里还有一层"摘 msgId 重发"兜底。
1110
+ if (toForward.length > 0) {
1111
+ const injected = globalThis[FWD_KEY] ?? null;
1112
+ if (injected?.sender && injected?.replyTarget) {
1113
+ w.fwd = { sender: injected.sender, replyTarget: injected.replyTarget };
1114
+ } else if (!w.fwd?.sender || !w.fwd?.replyTarget) {
1115
+ const hadFwd = !!w.fwd;
1116
+ const ok = hydrateFwd(w, root, session);
1117
+ void swDiag('pre-forward hydrate short=' + String(w.shortId ?? '?')
1118
+ + ' hadFwd=' + hadFwd + ' ok=' + ok + ' mode=' + mode);
1119
+ }
1120
+ }
983
1121
  for (const piece of toForward) {
984
1122
  const body = withOwner(w, piece);
985
1123
  // 去重:同一条内容不重复转发
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaofan/dsh-qqbot",
3
- "version": "1.10.6",
3
+ "version": "1.10.7",
4
4
  "description": "QQ Bot IM channel plugin for deepseek-harness (dsh)",
5
5
  "type": "module",
6
6
  "main": "./entry.js",