@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.
- package/dist/QQChannel.d.ts +64 -2
- package/dist/QQChannel.d.ts.map +1 -1
- package/dist/QQChannel.js +250 -3
- package/dist/QQChannel.js.map +1 -1
- package/dist/api.d.ts +9 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +12 -3
- package/dist/api.js.map +1 -1
- package/dist/moderation-tools.d.ts +52 -0
- package/dist/moderation-tools.d.ts.map +1 -0
- package/dist/moderation-tools.js +957 -0
- package/dist/moderation-tools.js.map +1 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/QQChannel.d.ts
CHANGED
|
@@ -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(
|
|
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.
|
package/dist/QQChannel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QQChannel.d.ts","sourceRoot":"","sources":["../src/QQChannel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,WAAW,EAUZ,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAEV,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,2BAA2B,EAC3B,QAAQ,EACR,aAAa,EAEd,MAAM,yBAAyB,CAAC;
|
|
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(
|
|
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) {
|