@qwen-code/channel-qqbot 0.26.4 → 0.26.5

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.
@@ -62,6 +62,13 @@ export declare class QQChannel extends ChannelBase {
62
62
  /** Track the latest user messageId per chatId for proper reply (msg_id). */
63
63
  private replyMsgId;
64
64
  private replyContextByMessageId;
65
+ /**
66
+ * Recent inbound messages per chat, newest last, used by the moderation
67
+ * tools to resolve "that member's last message" into a message id the
68
+ * platform can recall. The ring is bounded by the per-chat count cap and,
69
+ * when `moderationRecallWindowMs` is positive, by that age.
70
+ */
71
+ private recentInbound;
65
72
  private inboundReplyContext;
66
73
  /** msg_seq counter per user messageId, for multi-block streaming. */
67
74
  private msgSeqMap;
@@ -77,6 +84,16 @@ export declare class QQChannel extends ChannelBase {
77
84
  private replyMsgIdCleanupTimer;
78
85
  /** 5-minute TTL for replyMsgId entries and seenMessages dedup. */
79
86
  private static readonly REPLY_MSG_ID_TTL_MS;
87
+ /**
88
+ * How long the ring keeps an entry, from `moderationRecallWindowMs`: a
89
+ * positive value is an age in ms, -1 keeps entries until the count cap
90
+ * evicts them, and 0 disables the ring (and member-targeted recall).
91
+ */
92
+ private moderationRecallWindowMs;
93
+ /** Per-chat cap for the recent-inbound ring. */
94
+ private static readonly MAX_RECENT_INBOUND;
95
+ /** Sweep idle chats out of the ring once it holds this many. */
96
+ private static readonly MAX_RECENT_INBOUND_CHATS;
80
97
  /** Idle-flush timeout: buffer is sent after this many ms of silence. */
81
98
  private static readonly IDLE_FLUSH_MS;
82
99
  /** Max consecutive send failures before the stream is abandoned. */
@@ -154,6 +171,14 @@ export declare class QQChannel extends ChannelBase {
154
171
  * chunks onto its own msg_id (see PR #6457 review).
155
172
  */
156
173
  private sessionReplyMsgId;
174
+ /**
175
+ * Chat the turn in progress is about, per session, recorded by
176
+ * onPromptStart. Under a shared `sessionScope` a session serves many chats,
177
+ * so the router target frozen at session creation is whichever chat created
178
+ * it — not the chat a tool call belongs to. The moderation tools resolve
179
+ * their chat from here instead. Cleared on prompt end and session teardown.
180
+ */
181
+ private sessionTurnChat;
157
182
  /**
158
183
  * Monotonic turn counter per session, bumped on every onPromptStart.
159
184
  * streamState entries carry the turn they were created in; when a new
@@ -285,7 +310,8 @@ export declare class QQChannel extends ChannelBase {
285
310
  runCronFlow(fn: () => Promise<void>): Promise<void>;
286
311
  /**
287
312
  * Override setBridge to re-attach the permanent `_cronTextHandler`
288
- * after bridge crash-recovery.
313
+ * after bridge crash-recovery, and to re-register the moderation tools:
314
+ * they live on the bridge instance, so a fresh bridge starts without them.
289
315
  */
290
316
  setBridge(bridge: ChannelAgentBridge): void;
291
317
  connect(): Promise<void>;
@@ -331,7 +357,7 @@ export declare class QQChannel extends ChannelBase {
331
357
  * Proactive turns (loop/webhook/cron) have no triggering message; their
332
358
  * replies must go out as active messages, so clear any stale anchor.
333
359
  */
334
- protected onPromptStart(_chatId: string, sessionId: string, messageId?: string): void;
360
+ protected onPromptStart(chatId: string, sessionId: string, messageId?: string): void;
335
361
  /**
336
362
  * ChannelBase invokes this unconditionally in a finally block — including
337
363
  * when the prompt was cancelled, in which case onResponseComplete is
@@ -811,6 +837,42 @@ export declare class QQChannel extends ChannelBase {
811
837
  * holder remains.
812
838
  */
813
839
  private setReplyMsgId;
840
+ /**
841
+ * Track an inbound message for the moderation tools. Kept in memory only:
842
+ * the ring does not survive a restart, so member-targeted recall only
843
+ * reaches messages received since the process started.
844
+ */
845
+ private recordRecentInbound;
846
+ /**
847
+ * Drop one recalled message from the ring so the next member-targeted recall
848
+ * resolves the previous message instead of re-targeting the destroyed one.
849
+ * Splices in place: `recentMemberMessage`'s backward scan relies on the ring
850
+ * staying ascending by `at`.
851
+ */
852
+ private dropRecentMessage;
853
+ /** Keep the ring's chat keys bounded: stale chats first, then LRU. */
854
+ private sweepRecentInbound;
855
+ /** Newest tracked message id from `senderId` in `chatId`. */
856
+ private recentMemberMessage;
857
+ /**
858
+ * The chat a moderation call may touch, resolved from the turn in progress.
859
+ * Under a shared `sessionScope` one session serves every group and DM, and
860
+ * the router target frozen at session creation is whichever chat happened to
861
+ * create it — using it would recall a message in a chat nobody asked about.
862
+ * ChannelBase calls `onPromptStart` for every turn with that turn's envelope
863
+ * chat, which is the only chat the turn is about.
864
+ */
865
+ private resolveModerationChat;
866
+ /**
867
+ * Expose the QQ group-management surface to the agent as client MCP tools.
868
+ * Opt-in: a no-op unless the channel sets moderationTools: true, and also a
869
+ * no-op when the bridge has no client MCP tool support. Tools are owned by
870
+ * this channel instance: one bridge can serve several channels, and the
871
+ * owner is what keeps each channel's sessions on their own credentials.
872
+ */
873
+ private registerModerationTools;
874
+ /** Single REST entry point for the moderation tools. */
875
+ private requestModerationApi;
814
876
  private deleteReplyContext;
815
877
  /**
816
878
  * Start periodic cleanup of expired replyMsgId entries.
@@ -1 +1 @@
1
- {"version":3,"file":"QQChannel.d.ts","sourceRoot":"","sources":["../src/QQChannel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"QQChannel.d.ts","sourceRoot":"","sources":["../src/QQChannel.ts"],"names":[],"mappings":"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"}
package/dist/QQChannel.js CHANGED
@@ -23,7 +23,8 @@ import { OpCode, Intent } from './types.js';
23
23
  import { QQ_FILE_MAX_BYTES, QQ_IMAGE_MAX_BYTES, QQ_MAX_ATTACHMENTS, QQ_VIDEO_MAX_BYTES, QQ_VOICE_MAX_BYTES, classifyQQAttachment, downloadQQAttachment, } from './media.js';
24
24
  import { getCredsFilePath, loadCredentials, saveCredentials, } from './accounts.js';
25
25
  import { qrCodeLogin } from './login.js';
26
- import { fetchAccessToken, fetchGatewayUrl, getApiBase, sendQQMessage, } from './api.js';
26
+ import { fetchAccessToken, fetchGatewayUrl, getApiBase, requestQQApi, sendQQMessage, } from './api.js';
27
+ import { buildModerationTools, } from './moderation-tools.js';
27
28
  /** QQ Bot OPENID format: exactly 32 uppercase hex chars (bot and sender OPENIDs). */
28
29
  const QQ_OPENID_RE = /^[A-F0-9]{32}$/i;
29
30
  export class DeliveryError extends Error {
@@ -152,6 +153,13 @@ export class QQChannel extends ChannelBase {
152
153
  /** Track the latest user messageId per chatId for proper reply (msg_id). */
153
154
  replyMsgId = new Map();
154
155
  replyContextByMessageId = new Map();
156
+ /**
157
+ * Recent inbound messages per chat, newest last, used by the moderation
158
+ * tools to resolve "that member's last message" into a message id the
159
+ * platform can recall. The ring is bounded by the per-chat count cap and,
160
+ * when `moderationRecallWindowMs` is positive, by that age.
161
+ */
162
+ recentInbound = new Map();
155
163
  inboundReplyContext = new AsyncLocalStorage();
156
164
  /** msg_seq counter per user messageId, for multi-block streaming. */
157
165
  msgSeqMap = new Map();
@@ -167,6 +175,16 @@ export class QQChannel extends ChannelBase {
167
175
  replyMsgIdCleanupTimer = null;
168
176
  /** 5-minute TTL for replyMsgId entries and seenMessages dedup. */
169
177
  static REPLY_MSG_ID_TTL_MS = 300_000;
178
+ /**
179
+ * How long the ring keeps an entry, from `moderationRecallWindowMs`: a
180
+ * positive value is an age in ms, -1 keeps entries until the count cap
181
+ * evicts them, and 0 disables the ring (and member-targeted recall).
182
+ */
183
+ moderationRecallWindowMs;
184
+ /** Per-chat cap for the recent-inbound ring. */
185
+ static MAX_RECENT_INBOUND = 50;
186
+ /** Sweep idle chats out of the ring once it holds this many. */
187
+ static MAX_RECENT_INBOUND_CHATS = 256;
170
188
  /** Idle-flush timeout: buffer is sent after this many ms of silence. */
171
189
  static IDLE_FLUSH_MS = 2000;
172
190
  /** Max consecutive send failures before the stream is abandoned. */
@@ -246,6 +264,14 @@ export class QQChannel extends ChannelBase {
246
264
  * chunks onto its own msg_id (see PR #6457 review).
247
265
  */
248
266
  sessionReplyMsgId = new Map();
267
+ /**
268
+ * Chat the turn in progress is about, per session, recorded by
269
+ * onPromptStart. Under a shared `sessionScope` a session serves many chats,
270
+ * so the router target frozen at session creation is whichever chat created
271
+ * it — not the chat a tool call belongs to. The moderation tools resolve
272
+ * their chat from here instead. Cleared on prompt end and session teardown.
273
+ */
274
+ sessionTurnChat = new Map();
249
275
  /**
250
276
  * Monotonic turn counter per session, bumped on every onPromptStart.
251
277
  * streamState entries carry the turn they were created in; when a new
@@ -409,6 +435,15 @@ export class QQChannel extends ChannelBase {
409
435
  process.stderr.write(`[QQ:${this.name}] WARNING: invalid bufferFlushLength=${raw}, using default ${QQChannel.MAX_BUFFER_LENGTH}\n`);
410
436
  this.qqConfig.bufferFlushLength = QQChannel.MAX_BUFFER_LENGTH;
411
437
  }
438
+ const recallWindow = this.qqConfig.moderationRecallWindowMs;
439
+ if (recallWindow !== undefined &&
440
+ (!Number.isInteger(recallWindow) || recallWindow < -1)) {
441
+ process.stderr.write(`[QQ:${this.name}] WARNING: invalid moderationRecallWindowMs=${recallWindow}, using -1 (no time limit)\n`);
442
+ this.moderationRecallWindowMs = -1;
443
+ }
444
+ else {
445
+ this.moderationRecallWindowMs = recallWindow ?? -1;
446
+ }
412
447
  this.qqStatePath = join(stateDir, `${safeName}-state.json`);
413
448
  // In standalone mode (no external router), use the per-channel
414
449
  // sessions path so the channel owns its own session file.
@@ -425,6 +460,7 @@ export class QQChannel extends ChannelBase {
425
460
  this._cronTextHandler = (sid, t) => this.handleCronTextChunk(sid, t);
426
461
  this.attachCronHandler();
427
462
  }
463
+ this.registerModerationTools();
428
464
  // Seal a stashed head on every bridge boundary, including the ones
429
465
  // ChannelBase suppresses while a cancel is pending.
430
466
  this.attachBridgeBoundarySeal();
@@ -688,13 +724,15 @@ export class QQChannel extends ChannelBase {
688
724
  }
689
725
  /**
690
726
  * Override setBridge to re-attach the permanent `_cronTextHandler`
691
- * after bridge crash-recovery.
727
+ * after bridge crash-recovery, and to re-register the moderation tools:
728
+ * they live on the bridge instance, so a fresh bridge starts without them.
692
729
  */
693
730
  setBridge(bridge) {
694
731
  this.detachCronHandler();
695
732
  this.detachBridgeBoundarySeal();
696
733
  super.setBridge(bridge);
697
734
  this.attachCronHandler();
735
+ this.registerModerationTools();
698
736
  this.attachBridgeBoundarySeal();
699
737
  }
700
738
  // ── ChannelBase interface ──────────────────────────────────────
@@ -1141,6 +1179,7 @@ export class QQChannel extends ChannelBase {
1141
1179
  }
1142
1180
  this.streamState.clear();
1143
1181
  this.sessionReplyMsgId.clear();
1182
+ this.sessionTurnChat.clear();
1144
1183
  this.turnCounter.clear();
1145
1184
  this.flushingSessions.clear();
1146
1185
  this.pendingStreamDelete.clear();
@@ -1181,7 +1220,13 @@ export class QQChannel extends ChannelBase {
1181
1220
  * Proactive turns (loop/webhook/cron) have no triggering message; their
1182
1221
  * replies must go out as active messages, so clear any stale anchor.
1183
1222
  */
1184
- onPromptStart(_chatId, sessionId, messageId) {
1223
+ onPromptStart(chatId, sessionId, messageId) {
1224
+ // A shared sessionScope maps many chats onto one session, so the chat a
1225
+ // tool call belongs to is this turn's, not the session's routed target.
1226
+ this.sessionTurnChat.set(sessionId, {
1227
+ chatId,
1228
+ isGroup: this.chatTypeFor(chatId) === 'group',
1229
+ });
1185
1230
  this.activePromptSessions.add(sessionId);
1186
1231
  // Bump the turn generation: streamState entries created by a previous
1187
1232
  // turn on this session (e.g. one left behind by a deferred send) are now
@@ -1235,6 +1280,7 @@ export class QQChannel extends ChannelBase {
1235
1280
  * trip the .then() identity guard and silently drop the residual buffer.
1236
1281
  */
1237
1282
  onPromptEnd(chatId, sessionId, _messageId) {
1283
+ this.sessionTurnChat.delete(sessionId);
1238
1284
  // Always clear the prompt-in-flight marker first: the PR #8241 teardown
1239
1285
  // below has early returns (deferred flush chains) that must not leave
1240
1286
  // this session marked as an active prompt for the cron discriminator.
@@ -3140,6 +3186,7 @@ export class QQChannel extends ChannelBase {
3140
3186
  this.streamOrphanBuffer.set(sessionId, merged);
3141
3187
  }
3142
3188
  onSessionDied(sessionId) {
3189
+ this.sessionTurnChat.delete(sessionId);
3143
3190
  const state = this.streamState.get(sessionId);
3144
3191
  if (state?.timer) {
3145
3192
  clearTimeout(state.timer);
@@ -4021,6 +4068,198 @@ export class QQChannel extends ChannelBase {
4021
4068
  this.replyContextByMessageId.set(msgId, { chatId, msgId, timestamp });
4022
4069
  this.saveQQState();
4023
4070
  }
4071
+ /**
4072
+ * Track an inbound message for the moderation tools. Kept in memory only:
4073
+ * the ring does not survive a restart, so member-targeted recall only
4074
+ * reaches messages received since the process started.
4075
+ */
4076
+ recordRecentInbound(chatId, senderId, messageId) {
4077
+ // Nothing can resolve a member's message when the tools are off or the
4078
+ // window is 0, so do not spend memory recording one.
4079
+ const window = this.moderationRecallWindowMs;
4080
+ if (this.qqConfig.moderationTools !== true || window === 0)
4081
+ return;
4082
+ if (!senderId || !messageId)
4083
+ return;
4084
+ const prior = this.recentInbound.get(chatId) ?? [];
4085
+ // First write wins for one message id. The same platform id is redelivered
4086
+ // on reconnect replay and arrives twice when a message is both
4087
+ // GROUP_AT_MESSAGE_CREATE and GROUP_MESSAGE_CREATE; re-stamping it would
4088
+ // make an older message outrank a newer one and extend its lifetime.
4089
+ if (prior.some((entry) => entry.messageId === messageId))
4090
+ return;
4091
+ const now = Date.now();
4092
+ const entries = window > 0 ? prior.filter((entry) => now - entry.at < window) : prior;
4093
+ entries.push({ messageId, senderId, at: now });
4094
+ if (entries.length > QQChannel.MAX_RECENT_INBOUND) {
4095
+ entries.splice(0, entries.length - QQChannel.MAX_RECENT_INBOUND);
4096
+ }
4097
+ this.recentInbound.set(chatId, entries);
4098
+ if (this.recentInbound.size > QQChannel.MAX_RECENT_INBOUND_CHATS) {
4099
+ this.sweepRecentInbound(now);
4100
+ }
4101
+ }
4102
+ /**
4103
+ * Drop one recalled message from the ring so the next member-targeted recall
4104
+ * resolves the previous message instead of re-targeting the destroyed one.
4105
+ * Splices in place: `recentMemberMessage`'s backward scan relies on the ring
4106
+ * staying ascending by `at`.
4107
+ */
4108
+ dropRecentMessage(chatId, messageId) {
4109
+ const entries = this.recentInbound.get(chatId);
4110
+ if (!entries)
4111
+ return;
4112
+ const index = entries.findIndex((entry) => entry.messageId === messageId);
4113
+ if (index === -1)
4114
+ return;
4115
+ entries.splice(index, 1);
4116
+ if (entries.length === 0)
4117
+ this.recentInbound.delete(chatId);
4118
+ }
4119
+ /** Keep the ring's chat keys bounded: stale chats first, then LRU. */
4120
+ sweepRecentInbound(now) {
4121
+ const window = this.moderationRecallWindowMs;
4122
+ if (window > 0) {
4123
+ for (const [chatId, entries] of this.recentInbound) {
4124
+ if (entries.every((entry) => now - entry.at >= window)) {
4125
+ this.recentInbound.delete(chatId);
4126
+ }
4127
+ }
4128
+ }
4129
+ while (this.recentInbound.size > QQChannel.MAX_RECENT_INBOUND_CHATS) {
4130
+ let oldestChat;
4131
+ let oldestAt = Infinity;
4132
+ for (const [chatId, entries] of this.recentInbound) {
4133
+ const last = entries[entries.length - 1]?.at ?? 0;
4134
+ if (last < oldestAt) {
4135
+ oldestAt = last;
4136
+ oldestChat = chatId;
4137
+ }
4138
+ }
4139
+ if (oldestChat === undefined)
4140
+ return;
4141
+ this.recentInbound.delete(oldestChat);
4142
+ }
4143
+ }
4144
+ /** Newest tracked message id from `senderId` in `chatId`. */
4145
+ recentMemberMessage(chatId, senderId) {
4146
+ const now = Date.now();
4147
+ const window = this.moderationRecallWindowMs;
4148
+ const entries = this.recentInbound.get(chatId) ?? [];
4149
+ for (let i = entries.length - 1; i >= 0; i--) {
4150
+ const entry = entries[i];
4151
+ if (window > 0 && now - entry.at >= window)
4152
+ break;
4153
+ if (entry.senderId === senderId)
4154
+ return entry.messageId;
4155
+ }
4156
+ return undefined;
4157
+ }
4158
+ /**
4159
+ * The chat a moderation call may touch, resolved from the turn in progress.
4160
+ * Under a shared `sessionScope` one session serves every group and DM, and
4161
+ * the router target frozen at session creation is whichever chat happened to
4162
+ * create it — using it would recall a message in a chat nobody asked about.
4163
+ * ChannelBase calls `onPromptStart` for every turn with that turn's envelope
4164
+ * chat, which is the only chat the turn is about.
4165
+ */
4166
+ resolveModerationChat(sessionId) {
4167
+ const turn = this.sessionTurnChat.get(sessionId);
4168
+ if (turn)
4169
+ return turn;
4170
+ // No turn in progress. A per-chat scope still maps the session to one
4171
+ // chat, so its routed target is that chat — keep the channel-name guard so
4172
+ // this instance's token cannot act on another channel's chat when several
4173
+ // channels share one router. 'single' maps every chat onto one session, so
4174
+ // its frozen target cannot say which chat this is: fail closed.
4175
+ if (this.config.sessionScope === 'single')
4176
+ return undefined;
4177
+ const target = this.router.getTarget(sessionId);
4178
+ if (!target || target.channelName !== this.name)
4179
+ return undefined;
4180
+ return { chatId: target.chatId, isGroup: target.isGroup === true };
4181
+ }
4182
+ /**
4183
+ * Expose the QQ group-management surface to the agent as client MCP tools.
4184
+ * Opt-in: a no-op unless the channel sets moderationTools: true, and also a
4185
+ * no-op when the bridge has no client MCP tool support. Tools are owned by
4186
+ * this channel instance: one bridge can serve several channels, and the
4187
+ * owner is what keeps each channel's sessions on their own credentials.
4188
+ */
4189
+ registerModerationTools() {
4190
+ if (this.qqConfig.moderationTools !== true)
4191
+ return;
4192
+ const register = this.bridge.registerChannelTool?.bind(this.bridge);
4193
+ if (!register)
4194
+ return;
4195
+ for (const tool of buildModerationTools({
4196
+ resolveChat: (sessionId) => this.resolveModerationChat(sessionId),
4197
+ memberRecallDisabled: this.moderationRecallWindowMs === 0,
4198
+ currentSenderId: (sessionId) => this.getResponseSenderId(sessionId),
4199
+ recentMemberMessage: (chatId, senderId) => this.recentMemberMessage(chatId, senderId),
4200
+ dropRecentMessage: (chatId, messageId) => this.dropRecentMessage(chatId, messageId),
4201
+ request: (input) => this.requestModerationApi(input),
4202
+ log: (message) => process.stderr.write(`[QQ:${this.name}] ${sanitizeLogText(message, 200)}\n`),
4203
+ })) {
4204
+ register({ ...tool, owner: this.name });
4205
+ }
4206
+ }
4207
+ /** Single REST entry point for the moderation tools. */
4208
+ async requestModerationApi(input) {
4209
+ if (this.disposed) {
4210
+ return { ok: false, status: 0, message: 'channel disposed' };
4211
+ }
4212
+ if (!this.accessToken || Date.now() >= this.tokenExpiresAt) {
4213
+ try {
4214
+ await this.fetchToken();
4215
+ }
4216
+ catch (error) {
4217
+ return {
4218
+ ok: false,
4219
+ status: 0,
4220
+ message: `token refresh failed: ${error instanceof Error ? error.message : String(error)}`,
4221
+ };
4222
+ }
4223
+ }
4224
+ if (!this.accessToken) {
4225
+ return { ok: false, status: 0, message: 'no QQ access token' };
4226
+ }
4227
+ try {
4228
+ const response = await requestQQApi(getApiBase(Boolean(this.qqConfig.sandbox)), input.method, input.path, this.accessToken, input.body);
4229
+ const text = await response.text().catch(() => '');
4230
+ let data;
4231
+ if (text) {
4232
+ try {
4233
+ data = JSON.parse(text);
4234
+ }
4235
+ catch {
4236
+ data = text;
4237
+ }
4238
+ }
4239
+ if (!response.ok) {
4240
+ const record = (typeof data === 'object' && data !== null ? data : {});
4241
+ const rawCode = record['code'] ?? record['err_code'];
4242
+ const code = typeof rawCode === 'number' ? rawCode : undefined;
4243
+ const message = typeof record['message'] === 'string' && record['message']
4244
+ ? record['message']
4245
+ : text || `HTTP ${response.status}`;
4246
+ return {
4247
+ ok: false,
4248
+ status: response.status,
4249
+ ...(code === undefined ? {} : { code }),
4250
+ message,
4251
+ };
4252
+ }
4253
+ return { ok: true, status: response.status, data };
4254
+ }
4255
+ catch (error) {
4256
+ return {
4257
+ ok: false,
4258
+ status: 0,
4259
+ message: error instanceof Error ? error.message : String(error),
4260
+ };
4261
+ }
4262
+ }
4024
4263
  deleteReplyContext(context) {
4025
4264
  this.replyContextByMessageId.delete(context.msgId);
4026
4265
  // Drop the chat-level naming entry BEFORE the reclaim: while it is still
@@ -4845,6 +5084,9 @@ export class QQChannel extends ChannelBase {
4845
5084
  }
4846
5085
  this.chatTypeMap.set(chatId, 'c2c');
4847
5086
  this.setReplyMsgId(chatId, event.id);
5087
+ // Not recorded in the recent-inbound ring: it is read only by the
5088
+ // group-only member-targeted recall, so a DM entry could never be
5089
+ // resolved and would consume the ring's shared 256-chat budget.
4848
5090
  const senderName = event.author.username || event.author.id || 'QQ User';
4849
5091
  const safeName = sanitizeSenderName(senderName);
4850
5092
  const cleanText = rawContent.trim();
@@ -4931,6 +5173,7 @@ export class QQChannel extends ChannelBase {
4931
5173
  return;
4932
5174
  }
4933
5175
  this.setReplyMsgId(chatId, event.id);
5176
+ this.recordRecentInbound(chatId, senderId, event.id);
4934
5177
  const envelope = {
4935
5178
  channelName: this.name,
4936
5179
  senderId,
@@ -4974,6 +5217,9 @@ export class QQChannel extends ChannelBase {
4974
5217
  this.chatTypeMap.set(chatId, 'group');
4975
5218
  if (isNewGroup)
4976
5219
  this.saveQQState();
5220
+ // Track before the policy gates below: a 'log'-policy message is still a
5221
+ // message the bot received, and its id is what a later recall needs.
5222
+ this.recordRecentInbound(chatId, event.author.user_openid || event.author.id || event.author.member_openid, event.id);
4977
5223
  const result = this.prepareGroupMessage(event, chatId);
4978
5224
  if (!result)
4979
5225
  return;
@@ -5119,6 +5365,7 @@ export class QQChannel extends ChannelBase {
5119
5365
  }
5120
5366
  this.botOpenIdByGroup.delete(groupId);
5121
5367
  this._lastKeywordNoMatchLog.delete(groupId);
5368
+ this.recentInbound.delete(groupId);
5122
5369
  // Clean up cron buffers targeting this group (always, regardless of config flag)
5123
5370
  let cleanedCron = 0;
5124
5371
  for (const [sid, entry] of this.cronBuffer) {