@nextclaw/channel-extension-qq 0.1.23-beta.0 → 0.1.23-beta.1

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.
@@ -1,3 +1,4 @@
1
+ import { QQGatewaySessionLimitError, QQGatewayStartupProbeService } from "./qq-gateway-startup-probe.service.js";
1
2
  import { Bot, ReceiverMode, SessionEvents } from "qq-official-bot";
2
3
  //#region src/services/qq-channel.service.ts
3
4
  var QQChannel = class {
@@ -243,6 +244,7 @@ var QQChannel = class {
243
244
  connect = async (trigger) => {
244
245
  let candidate = null;
245
246
  try {
247
+ await this.verifyGatewaySessionAvailability();
246
248
  candidate = this.createBot();
247
249
  await this.startBotWithTimeout(candidate);
248
250
  if (!this.running) {
@@ -256,8 +258,9 @@ var QQChannel = class {
256
258
  if (candidate) await this.safeStopBot(candidate);
257
259
  if (!this.running) return;
258
260
  this.reconnectAttempt += 1;
259
- const delayMs = this.getBackoffDelayMs(this.reconnectAttempt);
260
- console.error(`[qq] start failed (${trigger}, attempt ${this.reconnectAttempt}), retry in ${delayMs}ms: ${this.formatError(error)}`);
261
+ const delayMs = this.getReconnectDelayMs(error, this.reconnectAttempt);
262
+ if (error instanceof QQGatewaySessionLimitError) console.warn(`[qq] startup paused (${trigger}, attempt ${this.reconnectAttempt}); gateway session quota exhausted, retry in ${delayMs}ms: reset_after_ms=${error.resetAfterMs ?? "unknown"}, total=${error.total ?? "unknown"}, max_concurrency=${error.maxConcurrency ?? "unknown"}`);
263
+ else console.error(`[qq] start failed (${trigger}, attempt ${this.reconnectAttempt}), retry in ${delayMs}ms: ${this.formatErrorMessage(error)}`);
261
264
  this.scheduleReconnect(delayMs, `${trigger}-retry`);
262
265
  }
263
266
  };
@@ -281,6 +284,12 @@ var QQChannel = class {
281
284
  });
282
285
  return bot;
283
286
  };
287
+ verifyGatewaySessionAvailability = async () => {
288
+ await new QQGatewayStartupProbeService({
289
+ appId: this.config.appId,
290
+ secret: this.config.secret
291
+ }).verifySessionAvailable();
292
+ };
284
293
  handleSessionDead = async (bot) => {
285
294
  if (!this.running || this.bot !== bot) return;
286
295
  this.bot = null;
@@ -319,14 +328,71 @@ var QQChannel = class {
319
328
  };
320
329
  startBotWithTimeout = async (bot) => {
321
330
  let timer = null;
331
+ const startupFailure = this.watchStartupFailure(bot);
322
332
  try {
323
- await Promise.race([bot.start(), new Promise((_, reject) => {
324
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`QQ bot start timed out after ${this.connectTimeoutMs}ms`)), this.connectTimeoutMs);
325
- })]);
333
+ await Promise.race([
334
+ bot.start(),
335
+ new Promise((_, reject) => {
336
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`QQ bot start timed out after ${this.connectTimeoutMs}ms`)), this.connectTimeoutMs);
337
+ }),
338
+ new Promise((_, reject) => {
339
+ startupFailure.reject = reject;
340
+ })
341
+ ]);
326
342
  } finally {
343
+ startupFailure.stop();
327
344
  if (timer) clearTimeout(timer);
328
345
  }
329
346
  };
347
+ watchStartupFailure = (bot) => {
348
+ const state = {
349
+ rejected: false,
350
+ reject: null,
351
+ stop: () => {}
352
+ };
353
+ const fail = (error) => {
354
+ if (state.rejected) return;
355
+ state.rejected = true;
356
+ state.reject?.(error);
357
+ };
358
+ const onReceiverError = (error) => {
359
+ fail(/* @__PURE__ */ new Error(`QQ bot websocket failed before ready: ${this.formatErrorMessage(error)}`));
360
+ };
361
+ const onReceiverClose = (code, reason) => {
362
+ fail(/* @__PURE__ */ new Error(`QQ bot websocket closed before ready: code=${code}, reason=${this.formatCloseReason(reason)}`));
363
+ };
364
+ const onSessionError = (code, message) => {
365
+ fail(/* @__PURE__ */ new Error(`QQ bot session failed before ready: code=${String(code)}, message=${this.formatErrorMessage(message)}`));
366
+ };
367
+ const onSessionEvent = (data) => {
368
+ const event = data && typeof data === "object" ? data : {};
369
+ if (event.eventType !== SessionEvents.DISCONNECT) return;
370
+ fail(/* @__PURE__ */ new Error(`QQ bot session disconnected before ready: code=${String(event.code ?? "unknown")}, event=${this.formatErrorMessage(event.eventMsg)}`));
371
+ };
372
+ bot.receiver.on("error", onReceiverError);
373
+ bot.receiver.on("close", onReceiverClose);
374
+ bot.sessionManager.on(SessionEvents.ERROR, onSessionError);
375
+ bot.sessionManager.on(SessionEvents.EVENT_WS, onSessionEvent);
376
+ state.stop = () => {
377
+ bot.receiver.off("error", onReceiverError);
378
+ bot.receiver.off("close", onReceiverClose);
379
+ bot.sessionManager.off(SessionEvents.ERROR, onSessionError);
380
+ bot.sessionManager.off(SessionEvents.EVENT_WS, onSessionEvent);
381
+ };
382
+ return state;
383
+ };
384
+ formatCloseReason = (reason) => {
385
+ return Buffer.isBuffer(reason) ? reason.toString() : this.formatErrorMessage(reason);
386
+ };
387
+ formatErrorMessage = (error) => {
388
+ if (error instanceof Error) return error.message;
389
+ if (error && typeof error === "object") try {
390
+ return JSON.stringify(error);
391
+ } catch {
392
+ return String(error);
393
+ }
394
+ return String(error);
395
+ };
330
396
  get isRunning() {
331
397
  return this.bot !== null;
332
398
  }
@@ -335,9 +401,9 @@ var QQChannel = class {
335
401
  const exp = Math.min(this.reconnectMaxMs, this.reconnectBaseMs * 2 ** Math.max(0, attempt - 1));
336
402
  return Math.min(this.reconnectMaxMs, exp + jitter);
337
403
  };
338
- formatError = (error) => {
339
- if (error instanceof Error) return error.stack ?? error.message;
340
- return String(error);
404
+ getReconnectDelayMs = (error, attempt) => {
405
+ if (error instanceof QQGatewaySessionLimitError && typeof error.resetAfterMs === "number") return Math.max(this.reconnectBaseMs, error.resetAfterMs);
406
+ return this.getBackoffDelayMs(attempt);
341
407
  };
342
408
  };
343
409
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"qq-channel.service.js","names":[],"sources":["../../src/services/qq-channel.service.ts"],"sourcesContent":["import type { BusChannelMessageBus, BusChannelRuntime } from \"@nextclaw/extension-sdk\";\nimport {\n Bot,\n ReceiverMode,\n SessionEvents,\n type GroupMessageEvent,\n type PrivateMessageEvent\n} from \"qq-official-bot\";\n\nexport type QQChannelConfig = { appId?: string; secret?: string; allowFrom?: string[] };\n\ntype QQMessageEvent = PrivateMessageEvent | GroupMessageEvent;\ntype QQMessageType = \"private\" | \"group\";\ntype QQRawUser = Partial<Record<\n \"id\" | \"user_id\" | \"user_openid\" | \"member_openid\" | \"username\" | \"user_name\" | \"nickname\" | \"nick\" | \"card\",\n string\n>>;\ntype QQRawEvent = { author?: QQRawUser; sender?: QQRawUser; group_openid?: string };\ntype QQIncomingIdentity = { messageId: string; rawEvent: QQRawEvent; senderId: string };\ntype QQIncomingRoute = { chatId: string; metadata: Record<string, unknown> };\n\nexport class QQChannel {\n name = \"qq\";\n protected running = false;\n private bot: Bot | null = null;\n private processedIds: string[] = [];\n private processedSet: Set<string> = new Set();\n private senderNameCache: Map<string, string> = new Map();\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private connectTask: Promise<void> | null = null;\n private reconnectAttempt = 0;\n private readonly reconnectBaseMs = 1000;\n private readonly reconnectMaxMs = 60000;\n protected readonly connectTimeoutMs: number = 90000;\n\n constructor(private readonly config: QQChannelConfig, private readonly bus: BusChannelMessageBus) {}\n\n start = async (): Promise<void> => {\n if (!this.config.appId || !this.config.secret) {\n this.running = false;\n throw new Error(\"QQ appId/appSecret not configured\");\n }\n\n this.running = true;\n this.reconnectAttempt = 0;\n this.clearReconnectTimer();\n this.tryConnect(\"startup\");\n await this.connectTask;\n };\n\n stop = async (): Promise<void> => {\n this.running = false;\n this.clearReconnectTimer();\n this.reconnectAttempt = 0;\n await this.teardownBot();\n if (this.connectTask) {\n await this.connectTask;\n }\n };\n\n send: BusChannelRuntime[\"send\"] = async (msg) => {\n if (!this.bot) {\n return;\n }\n\n const qqMeta = (msg.metadata?.qq as Record<string, unknown> | undefined) ?? {};\n const messageType = (qqMeta.messageType as QQMessageType | undefined) ?? \"private\";\n const replyTo = msg.replyTo ?? (msg.metadata?.message_id as string | undefined);\n const source = replyTo ? { id: replyTo } : undefined;\n const rawContent = msg.content;\n\n try {\n await this.sendByMessageType({ messageType, qqMeta, msg, payload: rawContent, source });\n } catch (error) {\n if (!this.isDisallowedUrlParamError(error)) {\n throw error;\n }\n const safeText = this.toQqSafeText(rawContent, error);\n await this.sendByMessageType({ messageType, qqMeta, msg, payload: safeText, source });\n }\n };\n\n private sendByMessageType = async (params: {\n messageType: QQMessageType;\n qqMeta: Record<string, unknown>;\n msg: Parameters<BusChannelRuntime[\"send\"]>[0];\n payload: unknown;\n source: { id: string } | undefined;\n }): Promise<void> => {\n const { messageType, qqMeta, msg, payload, source } = params;\n if (messageType === \"group\") {\n const groupId = (qqMeta.groupId as string | undefined) ?? msg.chatId;\n await this.sendWithTokenRetry(() => this.bot?.sendGroupMessage(groupId, payload, source));\n return;\n }\n\n const userId = (qqMeta.userId as string | undefined) ?? msg.chatId;\n await this.sendWithTokenRetry(() => this.bot?.sendPrivateMessage(userId, payload, source));\n };\n\n private handleIncoming = async (event: QQMessageEvent): Promise<void> => {\n const identity = this.resolveIncomingIdentity(event);\n if (!identity) {\n return;\n }\n const content = event.raw_message?.trim() || \"[empty message]\";\n const senderName = this.resolveIncomingSenderName(identity.senderId, identity.rawEvent, content);\n const route = this.resolveIncomingRoute(event, identity.rawEvent, identity.senderId, senderName);\n if (!route.chatId || !this.isAllowed(identity.senderId)) {\n return;\n }\n await this.bus.publishInbound({\n channel: this.name,\n senderId: identity.senderId,\n chatId: route.chatId,\n content: this.decorateSpeakerPrefix({\n content,\n senderId: identity.senderId,\n senderName\n }),\n metadata: {\n message_id: identity.messageId,\n qq: route.metadata\n }\n });\n };\n\n private resolveIncomingIdentity = (event: QQMessageEvent): QQIncomingIdentity | null => {\n const messageId = event.message_id || event.id || \"\";\n if (messageId && this.isDuplicate(messageId)) {\n return null;\n }\n const rawEvent = event as unknown as QQRawEvent;\n if (this.isSelfEvent(event)) {\n return null;\n }\n const senderId = this.resolveSenderId(event, rawEvent);\n return senderId ? { messageId, rawEvent, senderId } : null;\n };\n\n private resolveIncomingSenderName = (senderId: string, rawEvent: QQRawEvent, content: string): string | null => {\n const eventSenderName = this.resolveSenderName(rawEvent);\n if (eventSenderName) {\n this.senderNameCache.set(senderId, eventSenderName);\n }\n const declaredName = this.extractDeclaredName(content);\n if (declaredName) {\n this.senderNameCache.set(senderId, declaredName);\n }\n return declaredName ?? eventSenderName ?? this.senderNameCache.get(senderId) ?? null;\n };\n\n private resolveIncomingRoute = (\n event: QQMessageEvent,\n rawEvent: QQRawEvent,\n senderId: string,\n senderName: string | null,\n ): QQIncomingRoute => {\n let chatId = senderId;\n let messageType: QQMessageType = \"private\";\n const qqMeta: Record<string, unknown> = { userId: senderId };\n if (senderName) {\n qqMeta.userName = senderName;\n }\n\n if (event.message_type === \"group\") {\n messageType = \"group\";\n const groupId = event.group_id || rawEvent.group_openid || \"\";\n chatId = groupId;\n qqMeta.groupId = groupId;\n }\n\n qqMeta.messageType = messageType;\n return { chatId, metadata: qqMeta };\n };\n\n private isAllowed = (senderId: string): boolean => {\n const allowList = this.config.allowFrom ?? [];\n if (!allowList.length || allowList.includes(senderId)) {\n return true;\n }\n return senderId.includes(\"|\") && senderId.split(\"|\").some((part) => allowList.includes(part));\n };\n\n private isSelfEvent = (event: QQMessageEvent): boolean => {\n const userId = typeof event.user_id === \"string\" ? event.user_id : \"\";\n const selfId = typeof event.self_id === \"string\" ? event.self_id : \"\";\n return Boolean(userId && selfId && userId === selfId);\n };\n\n private resolveSenderId = (event: QQMessageEvent, rawEvent: QQRawEvent): string => {\n return this.readFirstString([\n event.user_id,\n rawEvent.sender?.member_openid,\n rawEvent.sender?.user_openid,\n rawEvent.sender?.user_id,\n rawEvent.author?.member_openid,\n rawEvent.author?.user_openid,\n rawEvent.author?.id\n ]) ?? \"\";\n };\n\n private resolveSenderName = (rawEvent: QQRawEvent): string | null => {\n return this.readFirstString([\n rawEvent.sender?.card,\n rawEvent.sender?.nickname,\n rawEvent.sender?.nick,\n rawEvent.sender?.username,\n rawEvent.sender?.user_name,\n rawEvent.author?.username\n ]);\n };\n\n private readFirstString = (values: unknown[]): string | null => {\n for (const value of values) {\n if (typeof value !== \"string\") {\n continue;\n }\n const normalized = value.trim();\n if (normalized) {\n return normalized;\n }\n }\n return null;\n };\n\n private decorateSpeakerPrefix = (params: {\n content: string;\n senderId: string;\n senderName: string | null;\n }): string => {\n const { content, senderId, senderName } = params;\n // Always inject sender identity so both group and private QQ sessions can resolve user identity.\n const userId = this.sanitizeSpeakerToken(senderId);\n if (!userId) {\n return content;\n }\n const name = this.sanitizeSpeakerToken(senderName ?? \"\");\n const speakerFields = [`user_id=${userId}`];\n if (name) {\n speakerFields.push(`name=${name}`);\n }\n return `[speaker:${speakerFields.join(\";\")}] ${content}`;\n };\n\n private sanitizeSpeakerToken = (value: string): string => {\n return value.replace(/[\\r\\n;\\]]/g, \" \").trim();\n };\n\n private extractDeclaredName = (content: string): string | null => {\n const trimmed = content.trim();\n const patterns = [\n /^我的昵称是\\s*([^\\s,。!?!?,]{1,24})$/u,\n /^我叫\\s*([^\\s,。!?!?,]{1,24})$/u,\n /^叫我\\s*([^\\s,。!?!?,]{1,24})$/u\n ];\n for (const pattern of patterns) {\n const match = trimmed.match(pattern);\n if (!match) {\n continue;\n }\n const candidate = this.sanitizeSpeakerToken(match[1] ?? \"\");\n if (candidate) {\n return candidate;\n }\n }\n return null;\n };\n\n private isDuplicate = (messageId: string): boolean => {\n if (this.processedSet.has(messageId)) {\n return true;\n }\n this.processedSet.add(messageId);\n this.processedIds.push(messageId);\n if (this.processedIds.length > 1000) {\n const removed = this.processedIds.splice(0, 500);\n for (const id of removed) {\n this.processedSet.delete(id);\n }\n }\n return false;\n };\n\n private sendWithTokenRetry = async (send: () => Promise<unknown> | undefined): Promise<void> => {\n try {\n await send();\n } catch (error) {\n if (!this.isTokenExpiredError(error) || !this.bot) {\n throw error;\n }\n await this.bot.sessionManager.getAccessToken();\n await send();\n }\n };\n\n private isTokenExpiredError = (error: unknown): boolean => {\n const message = error instanceof Error ? error.message : String(error);\n return message.includes(\"code(11244)\") || message.toLowerCase().includes(\"token not exist or expire\");\n };\n\n private isDisallowedUrlParamError = (error: unknown): boolean => {\n const message = error instanceof Error ? error.message : String(error);\n return message.includes(\"code(40034028)\") || message.includes(\"请求参数不允许包含url\");\n };\n\n private toQqSafeText = (content: string, error: unknown): string => {\n let safe = content\n .replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, \"$1\")\n .replace(/https?:\\/\\/\\S+/gi, \"[link]\")\n .replace(/www\\.\\S+/gi, \"[link]\")\n .replace(/\\b[a-z0-9._/-]+\\.md\\b/gi, \"[file]\");\n\n const blocked = this.extractBlockedUrlToken(error);\n if (blocked) {\n safe = safe.replaceAll(blocked, \"[link]\");\n }\n return safe;\n };\n\n private extractBlockedUrlToken = (error: unknown): string | null => {\n const message = error instanceof Error ? error.message : String(error);\n const match = message.match(/包含url\\s+([^\\s]+)/);\n if (!match) {\n return null;\n }\n const token = match[1].trim();\n return token.length > 0 ? token : null;\n };\n\n private tryConnect = (trigger: string): void => {\n if (!this.running || this.bot || this.connectTask) {\n return;\n }\n this.connectTask = this.connect(trigger).finally(() => {\n this.connectTask = null;\n });\n };\n\n private connect = async (trigger: string): Promise<void> => {\n let candidate: Bot | null = null;\n try {\n candidate = this.createBot();\n await this.startBotWithTimeout(candidate);\n if (!this.running) {\n await this.safeStopBot(candidate);\n return;\n }\n this.bot = candidate;\n this.reconnectAttempt = 0;\n // eslint-disable-next-line no-console\n console.log(\"QQ bot connected\");\n } catch (error) {\n if (candidate) {\n await this.safeStopBot(candidate);\n }\n if (!this.running) {\n return;\n }\n this.reconnectAttempt += 1;\n const delayMs = this.getBackoffDelayMs(this.reconnectAttempt);\n // eslint-disable-next-line no-console\n console.error(\n `[qq] start failed (${trigger}, attempt ${this.reconnectAttempt}), retry in ${delayMs}ms: ${this.formatError(error)}`\n );\n this.scheduleReconnect(delayMs, `${trigger}-retry`);\n }\n };\n\n protected createBot = (): Bot => {\n const bot = new Bot({\n appid: this.config.appId!,\n secret: this.config.secret!,\n mode: ReceiverMode.WEBSOCKET,\n intents: [\"C2C_MESSAGE_CREATE\", \"GROUP_AT_MESSAGE_CREATE\"],\n removeAt: true,\n logLevel: \"info\"\n });\n\n bot.on(\"message.private\", async (event) => {\n await this.handleIncoming(event);\n });\n\n bot.on(\"message.group\", async (event) => {\n await this.handleIncoming(event);\n });\n\n bot.sessionManager.on(SessionEvents.DEAD, () => {\n void this.handleSessionDead(bot);\n });\n\n return bot;\n };\n\n private handleSessionDead = async (bot: Bot): Promise<void> => {\n if (!this.running || this.bot !== bot) {\n return;\n }\n this.bot = null;\n await this.safeStopBot(bot);\n this.reconnectAttempt += 1;\n const delayMs = this.getBackoffDelayMs(this.reconnectAttempt);\n // eslint-disable-next-line no-console\n console.error(`[qq] session dead, reconnect in ${delayMs}ms`);\n this.scheduleReconnect(delayMs, \"session-dead\");\n };\n\n private scheduleReconnect = (delayMs: number, trigger: string): void => {\n if (!this.running) {\n return;\n }\n this.clearReconnectTimer();\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.tryConnect(trigger);\n }, delayMs);\n };\n\n private clearReconnectTimer = (): void => {\n if (!this.reconnectTimer) {\n return;\n }\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n };\n\n private teardownBot = async (): Promise<void> => {\n if (!this.bot) {\n return;\n }\n const bot = this.bot;\n this.bot = null;\n await this.safeStopBot(bot);\n };\n\n private safeStopBot = async (bot: Bot): Promise<void> => {\n bot.removeAllListeners(\"message.private\");\n bot.removeAllListeners(\"message.group\");\n bot.sessionManager.removeAllListeners(SessionEvents.DEAD);\n try {\n await bot.stop();\n } catch {\n // ignore cleanup errors\n }\n };\n\n private startBotWithTimeout = async (bot: Bot): Promise<void> => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n try {\n await Promise.race([\n bot.start(),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error(`QQ bot start timed out after ${this.connectTimeoutMs}ms`)), this.connectTimeoutMs);\n })\n ]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n };\n\n get isRunning(): boolean {\n return this.bot !== null;\n }\n\n private getBackoffDelayMs = (attempt: number): number => {\n const jitter = Math.floor(Math.random() * 500);\n const exp = Math.min(this.reconnectMaxMs, this.reconnectBaseMs * 2 ** Math.max(0, attempt - 1));\n return Math.min(this.reconnectMaxMs, exp + jitter);\n };\n\n private formatError = (error: unknown): string => {\n if (error instanceof Error) {\n return error.stack ?? error.message;\n }\n return String(error);\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"qq-channel.service.js","names":[],"sources":["../../src/services/qq-channel.service.ts"],"sourcesContent":["import type { BusChannelMessageBus, BusChannelRuntime } from \"@nextclaw/extension-sdk\";\nimport {\n Bot,\n ReceiverMode,\n SessionEvents,\n type GroupMessageEvent,\n type PrivateMessageEvent\n} from \"qq-official-bot\";\nimport {\n QQGatewaySessionLimitError,\n QQGatewayStartupProbeService\n} from \"./qq-gateway-startup-probe.service.js\";\n\nexport type QQChannelConfig = { appId?: string; secret?: string; allowFrom?: string[] };\n\ntype QQBot = Bot<ReceiverMode.WEBSOCKET>;\ntype QQMessageEvent = PrivateMessageEvent | GroupMessageEvent;\ntype QQMessageType = \"private\" | \"group\";\ntype QQRawUser = Partial<Record<\n \"id\" | \"user_id\" | \"user_openid\" | \"member_openid\" | \"username\" | \"user_name\" | \"nickname\" | \"nick\" | \"card\",\n string\n>>;\ntype QQRawEvent = { author?: QQRawUser; sender?: QQRawUser; group_openid?: string };\ntype QQIncomingIdentity = { messageId: string; rawEvent: QQRawEvent; senderId: string };\ntype QQIncomingRoute = { chatId: string; metadata: Record<string, unknown> };\n\nexport class QQChannel {\n name = \"qq\";\n protected running = false;\n private bot: QQBot | null = null;\n private processedIds: string[] = [];\n private processedSet: Set<string> = new Set();\n private senderNameCache: Map<string, string> = new Map();\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private connectTask: Promise<void> | null = null;\n private reconnectAttempt = 0;\n private readonly reconnectBaseMs = 1000;\n private readonly reconnectMaxMs = 60000;\n protected readonly connectTimeoutMs: number = 90000;\n\n constructor(private readonly config: QQChannelConfig, private readonly bus: BusChannelMessageBus) {}\n\n start = async (): Promise<void> => {\n if (!this.config.appId || !this.config.secret) {\n this.running = false;\n throw new Error(\"QQ appId/appSecret not configured\");\n }\n\n this.running = true;\n this.reconnectAttempt = 0;\n this.clearReconnectTimer();\n this.tryConnect(\"startup\");\n await this.connectTask;\n };\n\n stop = async (): Promise<void> => {\n this.running = false;\n this.clearReconnectTimer();\n this.reconnectAttempt = 0;\n await this.teardownBot();\n if (this.connectTask) {\n await this.connectTask;\n }\n };\n\n send: BusChannelRuntime[\"send\"] = async (msg) => {\n if (!this.bot) {\n return;\n }\n\n const qqMeta = (msg.metadata?.qq as Record<string, unknown> | undefined) ?? {};\n const messageType = (qqMeta.messageType as QQMessageType | undefined) ?? \"private\";\n const replyTo = msg.replyTo ?? (msg.metadata?.message_id as string | undefined);\n const source = replyTo ? { id: replyTo } : undefined;\n const rawContent = msg.content;\n\n try {\n await this.sendByMessageType({ messageType, qqMeta, msg, payload: rawContent, source });\n } catch (error) {\n if (!this.isDisallowedUrlParamError(error)) {\n throw error;\n }\n const safeText = this.toQqSafeText(rawContent, error);\n await this.sendByMessageType({ messageType, qqMeta, msg, payload: safeText, source });\n }\n };\n\n private sendByMessageType = async (params: {\n messageType: QQMessageType;\n qqMeta: Record<string, unknown>;\n msg: Parameters<BusChannelRuntime[\"send\"]>[0];\n payload: unknown;\n source: { id: string } | undefined;\n }): Promise<void> => {\n const { messageType, qqMeta, msg, payload, source } = params;\n if (messageType === \"group\") {\n const groupId = (qqMeta.groupId as string | undefined) ?? msg.chatId;\n await this.sendWithTokenRetry(() => this.bot?.sendGroupMessage(groupId, payload, source));\n return;\n }\n\n const userId = (qqMeta.userId as string | undefined) ?? msg.chatId;\n await this.sendWithTokenRetry(() => this.bot?.sendPrivateMessage(userId, payload, source));\n };\n\n private handleIncoming = async (event: QQMessageEvent): Promise<void> => {\n const identity = this.resolveIncomingIdentity(event);\n if (!identity) {\n return;\n }\n const content = event.raw_message?.trim() || \"[empty message]\";\n const senderName = this.resolveIncomingSenderName(identity.senderId, identity.rawEvent, content);\n const route = this.resolveIncomingRoute(event, identity.rawEvent, identity.senderId, senderName);\n if (!route.chatId || !this.isAllowed(identity.senderId)) {\n return;\n }\n await this.bus.publishInbound({\n channel: this.name,\n senderId: identity.senderId,\n chatId: route.chatId,\n content: this.decorateSpeakerPrefix({\n content,\n senderId: identity.senderId,\n senderName\n }),\n metadata: {\n message_id: identity.messageId,\n qq: route.metadata\n }\n });\n };\n\n private resolveIncomingIdentity = (event: QQMessageEvent): QQIncomingIdentity | null => {\n const messageId = event.message_id || event.id || \"\";\n if (messageId && this.isDuplicate(messageId)) {\n return null;\n }\n const rawEvent = event as unknown as QQRawEvent;\n if (this.isSelfEvent(event)) {\n return null;\n }\n const senderId = this.resolveSenderId(event, rawEvent);\n return senderId ? { messageId, rawEvent, senderId } : null;\n };\n\n private resolveIncomingSenderName = (senderId: string, rawEvent: QQRawEvent, content: string): string | null => {\n const eventSenderName = this.resolveSenderName(rawEvent);\n if (eventSenderName) {\n this.senderNameCache.set(senderId, eventSenderName);\n }\n const declaredName = this.extractDeclaredName(content);\n if (declaredName) {\n this.senderNameCache.set(senderId, declaredName);\n }\n return declaredName ?? eventSenderName ?? this.senderNameCache.get(senderId) ?? null;\n };\n\n private resolveIncomingRoute = (\n event: QQMessageEvent,\n rawEvent: QQRawEvent,\n senderId: string,\n senderName: string | null,\n ): QQIncomingRoute => {\n let chatId = senderId;\n let messageType: QQMessageType = \"private\";\n const qqMeta: Record<string, unknown> = { userId: senderId };\n if (senderName) {\n qqMeta.userName = senderName;\n }\n\n if (event.message_type === \"group\") {\n messageType = \"group\";\n const groupId = event.group_id || rawEvent.group_openid || \"\";\n chatId = groupId;\n qqMeta.groupId = groupId;\n }\n\n qqMeta.messageType = messageType;\n return { chatId, metadata: qqMeta };\n };\n\n private isAllowed = (senderId: string): boolean => {\n const allowList = this.config.allowFrom ?? [];\n if (!allowList.length || allowList.includes(senderId)) {\n return true;\n }\n return senderId.includes(\"|\") && senderId.split(\"|\").some((part) => allowList.includes(part));\n };\n\n private isSelfEvent = (event: QQMessageEvent): boolean => {\n const userId = typeof event.user_id === \"string\" ? event.user_id : \"\";\n const selfId = typeof event.self_id === \"string\" ? event.self_id : \"\";\n return Boolean(userId && selfId && userId === selfId);\n };\n\n private resolveSenderId = (event: QQMessageEvent, rawEvent: QQRawEvent): string => {\n return this.readFirstString([\n event.user_id,\n rawEvent.sender?.member_openid,\n rawEvent.sender?.user_openid,\n rawEvent.sender?.user_id,\n rawEvent.author?.member_openid,\n rawEvent.author?.user_openid,\n rawEvent.author?.id\n ]) ?? \"\";\n };\n\n private resolveSenderName = (rawEvent: QQRawEvent): string | null => {\n return this.readFirstString([\n rawEvent.sender?.card,\n rawEvent.sender?.nickname,\n rawEvent.sender?.nick,\n rawEvent.sender?.username,\n rawEvent.sender?.user_name,\n rawEvent.author?.username\n ]);\n };\n\n private readFirstString = (values: unknown[]): string | null => {\n for (const value of values) {\n if (typeof value !== \"string\") {\n continue;\n }\n const normalized = value.trim();\n if (normalized) {\n return normalized;\n }\n }\n return null;\n };\n\n private decorateSpeakerPrefix = (params: {\n content: string;\n senderId: string;\n senderName: string | null;\n }): string => {\n const { content, senderId, senderName } = params;\n // Always inject sender identity so both group and private QQ sessions can resolve user identity.\n const userId = this.sanitizeSpeakerToken(senderId);\n if (!userId) {\n return content;\n }\n const name = this.sanitizeSpeakerToken(senderName ?? \"\");\n const speakerFields = [`user_id=${userId}`];\n if (name) {\n speakerFields.push(`name=${name}`);\n }\n return `[speaker:${speakerFields.join(\";\")}] ${content}`;\n };\n\n private sanitizeSpeakerToken = (value: string): string => {\n return value.replace(/[\\r\\n;\\]]/g, \" \").trim();\n };\n\n private extractDeclaredName = (content: string): string | null => {\n const trimmed = content.trim();\n const patterns = [\n /^我的昵称是\\s*([^\\s,。!?!?,]{1,24})$/u,\n /^我叫\\s*([^\\s,。!?!?,]{1,24})$/u,\n /^叫我\\s*([^\\s,。!?!?,]{1,24})$/u\n ];\n for (const pattern of patterns) {\n const match = trimmed.match(pattern);\n if (!match) {\n continue;\n }\n const candidate = this.sanitizeSpeakerToken(match[1] ?? \"\");\n if (candidate) {\n return candidate;\n }\n }\n return null;\n };\n\n private isDuplicate = (messageId: string): boolean => {\n if (this.processedSet.has(messageId)) {\n return true;\n }\n this.processedSet.add(messageId);\n this.processedIds.push(messageId);\n if (this.processedIds.length > 1000) {\n const removed = this.processedIds.splice(0, 500);\n for (const id of removed) {\n this.processedSet.delete(id);\n }\n }\n return false;\n };\n\n private sendWithTokenRetry = async (send: () => Promise<unknown> | undefined): Promise<void> => {\n try {\n await send();\n } catch (error) {\n if (!this.isTokenExpiredError(error) || !this.bot) {\n throw error;\n }\n await this.bot.sessionManager.getAccessToken();\n await send();\n }\n };\n\n private isTokenExpiredError = (error: unknown): boolean => {\n const message = error instanceof Error ? error.message : String(error);\n return message.includes(\"code(11244)\") || message.toLowerCase().includes(\"token not exist or expire\");\n };\n\n private isDisallowedUrlParamError = (error: unknown): boolean => {\n const message = error instanceof Error ? error.message : String(error);\n return message.includes(\"code(40034028)\") || message.includes(\"请求参数不允许包含url\");\n };\n\n private toQqSafeText = (content: string, error: unknown): string => {\n let safe = content\n .replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, \"$1\")\n .replace(/https?:\\/\\/\\S+/gi, \"[link]\")\n .replace(/www\\.\\S+/gi, \"[link]\")\n .replace(/\\b[a-z0-9._/-]+\\.md\\b/gi, \"[file]\");\n\n const blocked = this.extractBlockedUrlToken(error);\n if (blocked) {\n safe = safe.replaceAll(blocked, \"[link]\");\n }\n return safe;\n };\n\n private extractBlockedUrlToken = (error: unknown): string | null => {\n const message = error instanceof Error ? error.message : String(error);\n const match = message.match(/包含url\\s+([^\\s]+)/);\n if (!match) {\n return null;\n }\n const token = match[1].trim();\n return token.length > 0 ? token : null;\n };\n\n private tryConnect = (trigger: string): void => {\n if (!this.running || this.bot || this.connectTask) {\n return;\n }\n this.connectTask = this.connect(trigger).finally(() => {\n this.connectTask = null;\n });\n };\n\n private connect = async (trigger: string): Promise<void> => {\n let candidate: QQBot | null = null;\n try {\n await this.verifyGatewaySessionAvailability();\n candidate = this.createBot();\n await this.startBotWithTimeout(candidate);\n if (!this.running) {\n await this.safeStopBot(candidate);\n return;\n }\n this.bot = candidate;\n this.reconnectAttempt = 0;\n // eslint-disable-next-line no-console\n console.log(\"QQ bot connected\");\n } catch (error) {\n if (candidate) {\n await this.safeStopBot(candidate);\n }\n if (!this.running) {\n return;\n }\n this.reconnectAttempt += 1;\n const delayMs = this.getReconnectDelayMs(error, this.reconnectAttempt);\n if (error instanceof QQGatewaySessionLimitError) {\n // eslint-disable-next-line no-console\n console.warn(`[qq] startup paused (${trigger}, attempt ${this.reconnectAttempt}); gateway session quota exhausted, retry in ${delayMs}ms: reset_after_ms=${error.resetAfterMs ?? \"unknown\"}, total=${error.total ?? \"unknown\"}, max_concurrency=${error.maxConcurrency ?? \"unknown\"}`);\n } else {\n // eslint-disable-next-line no-console\n console.error(`[qq] start failed (${trigger}, attempt ${this.reconnectAttempt}), retry in ${delayMs}ms: ${this.formatErrorMessage(error)}`);\n }\n this.scheduleReconnect(delayMs, `${trigger}-retry`);\n }\n };\n\n protected createBot = (): QQBot => {\n const bot = new Bot({\n appid: this.config.appId!,\n secret: this.config.secret!,\n mode: ReceiverMode.WEBSOCKET,\n intents: [\"C2C_MESSAGE_CREATE\", \"GROUP_AT_MESSAGE_CREATE\"],\n removeAt: true,\n logLevel: \"info\"\n });\n\n bot.on(\"message.private\", async (event) => {\n await this.handleIncoming(event);\n });\n\n bot.on(\"message.group\", async (event) => {\n await this.handleIncoming(event);\n });\n\n bot.sessionManager.on(SessionEvents.DEAD, () => {\n void this.handleSessionDead(bot);\n });\n\n return bot;\n };\n\n protected verifyGatewaySessionAvailability = async (): Promise<void> => {\n await new QQGatewayStartupProbeService({\n appId: this.config.appId!,\n secret: this.config.secret!,\n }).verifySessionAvailable();\n };\n\n private handleSessionDead = async (bot: QQBot): Promise<void> => {\n if (!this.running || this.bot !== bot) {\n return;\n }\n this.bot = null;\n await this.safeStopBot(bot);\n this.reconnectAttempt += 1;\n const delayMs = this.getBackoffDelayMs(this.reconnectAttempt);\n // eslint-disable-next-line no-console\n console.error(`[qq] session dead, reconnect in ${delayMs}ms`);\n this.scheduleReconnect(delayMs, \"session-dead\");\n };\n\n private scheduleReconnect = (delayMs: number, trigger: string): void => {\n if (!this.running) {\n return;\n }\n this.clearReconnectTimer();\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.tryConnect(trigger);\n }, delayMs);\n };\n\n private clearReconnectTimer = (): void => {\n if (!this.reconnectTimer) {\n return;\n }\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n };\n\n private teardownBot = async (): Promise<void> => {\n if (!this.bot) {\n return;\n }\n const bot = this.bot;\n this.bot = null;\n await this.safeStopBot(bot);\n };\n\n private safeStopBot = async (bot: QQBot): Promise<void> => {\n bot.removeAllListeners(\"message.private\");\n bot.removeAllListeners(\"message.group\");\n bot.sessionManager.removeAllListeners(SessionEvents.DEAD);\n try {\n await bot.stop();\n } catch {\n // ignore cleanup errors\n }\n };\n\n private startBotWithTimeout = async (bot: QQBot): Promise<void> => {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const startupFailure = this.watchStartupFailure(bot);\n try {\n await Promise.race([\n bot.start(),\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error(`QQ bot start timed out after ${this.connectTimeoutMs}ms`)), this.connectTimeoutMs);\n }),\n new Promise<never>((_, reject) => {\n startupFailure.reject = reject;\n })\n ]);\n } finally {\n startupFailure.stop();\n if (timer) {\n clearTimeout(timer);\n }\n }\n };\n\n private watchStartupFailure = (bot: QQBot): {\n reject: ((error: Error) => void) | null;\n stop: () => void;\n } => {\n const state: {\n rejected: boolean;\n reject: ((error: Error) => void) | null;\n stop: () => void;\n } = {\n rejected: false,\n reject: null,\n stop: () => {},\n };\n const fail = (error: Error): void => {\n if (state.rejected) {\n return;\n }\n state.rejected = true;\n state.reject?.(error);\n };\n const onReceiverError = (error: unknown): void => {\n fail(new Error(`QQ bot websocket failed before ready: ${this.formatErrorMessage(error)}`));\n };\n const onReceiverClose = (code: number, reason: unknown): void => {\n fail(new Error(`QQ bot websocket closed before ready: code=${code}, reason=${this.formatCloseReason(reason)}`));\n };\n const onSessionError = (code: unknown, message: unknown): void => {\n fail(new Error(`QQ bot session failed before ready: code=${String(code)}, message=${this.formatErrorMessage(message)}`));\n };\n const onSessionEvent = (data: unknown): void => {\n const event = data && typeof data === \"object\" ? data as Record<string, unknown> : {};\n if (event.eventType !== SessionEvents.DISCONNECT) {\n return;\n }\n fail(\n new Error(\n `QQ bot session disconnected before ready: code=${String(event.code ?? \"unknown\")}, event=${this.formatErrorMessage(event.eventMsg)}`\n )\n );\n };\n bot.receiver.on(\"error\", onReceiverError);\n bot.receiver.on(\"close\", onReceiverClose);\n bot.sessionManager.on(SessionEvents.ERROR, onSessionError);\n bot.sessionManager.on(SessionEvents.EVENT_WS, onSessionEvent);\n state.stop = () => {\n bot.receiver.off(\"error\", onReceiverError);\n bot.receiver.off(\"close\", onReceiverClose);\n bot.sessionManager.off(SessionEvents.ERROR, onSessionError);\n bot.sessionManager.off(SessionEvents.EVENT_WS, onSessionEvent);\n };\n return state;\n };\n\n private formatCloseReason = (reason: unknown): string => {\n return Buffer.isBuffer(reason) ? reason.toString() : this.formatErrorMessage(reason);\n };\n\n private formatErrorMessage = (error: unknown): string => {\n if (error instanceof Error) {\n return error.message;\n }\n if (error && typeof error === \"object\") {\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n }\n return String(error);\n };\n\n get isRunning(): boolean {\n return this.bot !== null;\n }\n\n private getBackoffDelayMs = (attempt: number): number => {\n const jitter = Math.floor(Math.random() * 500);\n const exp = Math.min(this.reconnectMaxMs, this.reconnectBaseMs * 2 ** Math.max(0, attempt - 1));\n return Math.min(this.reconnectMaxMs, exp + jitter);\n };\n\n private getReconnectDelayMs = (error: unknown, attempt: number): number => {\n if (error instanceof QQGatewaySessionLimitError && typeof error.resetAfterMs === \"number\") {\n return Math.max(this.reconnectBaseMs, error.resetAfterMs);\n }\n return this.getBackoffDelayMs(attempt);\n };\n}\n"],"mappings":";;;AA0BA,IAAa,YAAb,MAAuB;CACrB,OAAO;CACP,UAAoB;CACpB,MAA4B;CAC5B,eAAiC,EAAE;CACnC,+BAAoC,IAAI,KAAK;CAC7C,kCAA+C,IAAI,KAAK;CACxD,iBAA+D;CAC/D,cAA4C;CAC5C,mBAA2B;CAC3B,kBAAmC;CACnC,iBAAkC;CAClC,mBAA8C;CAE9C,YAAY,QAA0C,KAA4C;AAArE,OAAA,SAAA;AAA0C,OAAA,MAAA;;CAEvE,QAAQ,YAA2B;AACjC,MAAI,CAAC,KAAK,OAAO,SAAS,CAAC,KAAK,OAAO,QAAQ;AAC7C,QAAK,UAAU;AACf,SAAM,IAAI,MAAM,oCAAoC;;AAGtD,OAAK,UAAU;AACf,OAAK,mBAAmB;AACxB,OAAK,qBAAqB;AAC1B,OAAK,WAAW,UAAU;AAC1B,QAAM,KAAK;;CAGb,OAAO,YAA2B;AAChC,OAAK,UAAU;AACf,OAAK,qBAAqB;AAC1B,OAAK,mBAAmB;AACxB,QAAM,KAAK,aAAa;AACxB,MAAI,KAAK,YACP,OAAM,KAAK;;CAIf,OAAkC,OAAO,QAAQ;AAC/C,MAAI,CAAC,KAAK,IACR;EAGF,MAAM,SAAU,IAAI,UAAU,MAA8C,EAAE;EAC9E,MAAM,cAAe,OAAO,eAA6C;EACzE,MAAM,UAAU,IAAI,WAAY,IAAI,UAAU;EAC9C,MAAM,SAAS,UAAU,EAAE,IAAI,SAAS,GAAG,KAAA;EAC3C,MAAM,aAAa,IAAI;AAEvB,MAAI;AACF,SAAM,KAAK,kBAAkB;IAAE;IAAa;IAAQ;IAAK,SAAS;IAAY;IAAQ,CAAC;WAChF,OAAO;AACd,OAAI,CAAC,KAAK,0BAA0B,MAAM,CACxC,OAAM;GAER,MAAM,WAAW,KAAK,aAAa,YAAY,MAAM;AACrD,SAAM,KAAK,kBAAkB;IAAE;IAAa;IAAQ;IAAK,SAAS;IAAU;IAAQ,CAAC;;;CAIzF,oBAA4B,OAAO,WAMd;EACnB,MAAM,EAAE,aAAa,QAAQ,KAAK,SAAS,WAAW;AACtD,MAAI,gBAAgB,SAAS;GAC3B,MAAM,UAAW,OAAO,WAAkC,IAAI;AAC9D,SAAM,KAAK,yBAAyB,KAAK,KAAK,iBAAiB,SAAS,SAAS,OAAO,CAAC;AACzF;;EAGF,MAAM,SAAU,OAAO,UAAiC,IAAI;AAC5D,QAAM,KAAK,yBAAyB,KAAK,KAAK,mBAAmB,QAAQ,SAAS,OAAO,CAAC;;CAG5F,iBAAyB,OAAO,UAAyC;EACvE,MAAM,WAAW,KAAK,wBAAwB,MAAM;AACpD,MAAI,CAAC,SACH;EAEF,MAAM,UAAU,MAAM,aAAa,MAAM,IAAI;EAC7C,MAAM,aAAa,KAAK,0BAA0B,SAAS,UAAU,SAAS,UAAU,QAAQ;EAChG,MAAM,QAAQ,KAAK,qBAAqB,OAAO,SAAS,UAAU,SAAS,UAAU,WAAW;AAChG,MAAI,CAAC,MAAM,UAAU,CAAC,KAAK,UAAU,SAAS,SAAS,CACrD;AAEF,QAAM,KAAK,IAAI,eAAe;GAC5B,SAAS,KAAK;GACd,UAAU,SAAS;GACnB,QAAQ,MAAM;GACd,SAAS,KAAK,sBAAsB;IAClC;IACA,UAAU,SAAS;IACnB;IACD,CAAC;GACF,UAAU;IACR,YAAY,SAAS;IACrB,IAAI,MAAM;IACX;GACF,CAAC;;CAGJ,2BAAmC,UAAqD;EACtF,MAAM,YAAY,MAAM,cAAc,MAAM,MAAM;AAClD,MAAI,aAAa,KAAK,YAAY,UAAU,CAC1C,QAAO;EAET,MAAM,WAAW;AACjB,MAAI,KAAK,YAAY,MAAM,CACzB,QAAO;EAET,MAAM,WAAW,KAAK,gBAAgB,OAAO,SAAS;AACtD,SAAO,WAAW;GAAE;GAAW;GAAU;GAAU,GAAG;;CAGxD,6BAAqC,UAAkB,UAAsB,YAAmC;EAC9G,MAAM,kBAAkB,KAAK,kBAAkB,SAAS;AACxD,MAAI,gBACF,MAAK,gBAAgB,IAAI,UAAU,gBAAgB;EAErD,MAAM,eAAe,KAAK,oBAAoB,QAAQ;AACtD,MAAI,aACF,MAAK,gBAAgB,IAAI,UAAU,aAAa;AAElD,SAAO,gBAAgB,mBAAmB,KAAK,gBAAgB,IAAI,SAAS,IAAI;;CAGlF,wBACE,OACA,UACA,UACA,eACoB;EACpB,IAAI,SAAS;EACb,IAAI,cAA6B;EACjC,MAAM,SAAkC,EAAE,QAAQ,UAAU;AAC5D,MAAI,WACF,QAAO,WAAW;AAGpB,MAAI,MAAM,iBAAiB,SAAS;AAClC,iBAAc;GACd,MAAM,UAAU,MAAM,YAAY,SAAS,gBAAgB;AAC3D,YAAS;AACT,UAAO,UAAU;;AAGnB,SAAO,cAAc;AACrB,SAAO;GAAE;GAAQ,UAAU;GAAQ;;CAGrC,aAAqB,aAA8B;EACjD,MAAM,YAAY,KAAK,OAAO,aAAa,EAAE;AAC7C,MAAI,CAAC,UAAU,UAAU,UAAU,SAAS,SAAS,CACnD,QAAO;AAET,SAAO,SAAS,SAAS,IAAI,IAAI,SAAS,MAAM,IAAI,CAAC,MAAM,SAAS,UAAU,SAAS,KAAK,CAAC;;CAG/F,eAAuB,UAAmC;EACxD,MAAM,SAAS,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU;EACnE,MAAM,SAAS,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU;AACnE,SAAO,QAAQ,UAAU,UAAU,WAAW,OAAO;;CAGvD,mBAA2B,OAAuB,aAAiC;AACjF,SAAO,KAAK,gBAAgB;GAC1B,MAAM;GACN,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,SAAS,QAAQ;GAClB,CAAC,IAAI;;CAGR,qBAA6B,aAAwC;AACnE,SAAO,KAAK,gBAAgB;GAC1B,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,SAAS,QAAQ;GACjB,SAAS,QAAQ;GAClB,CAAC;;CAGJ,mBAA2B,WAAqC;AAC9D,OAAK,MAAM,SAAS,QAAQ;AAC1B,OAAI,OAAO,UAAU,SACnB;GAEF,MAAM,aAAa,MAAM,MAAM;AAC/B,OAAI,WACF,QAAO;;AAGX,SAAO;;CAGT,yBAAiC,WAInB;EACZ,MAAM,EAAE,SAAS,UAAU,eAAe;EAE1C,MAAM,SAAS,KAAK,qBAAqB,SAAS;AAClD,MAAI,CAAC,OACH,QAAO;EAET,MAAM,OAAO,KAAK,qBAAqB,cAAc,GAAG;EACxD,MAAM,gBAAgB,CAAC,WAAW,SAAS;AAC3C,MAAI,KACF,eAAc,KAAK,QAAQ,OAAO;AAEpC,SAAO,YAAY,cAAc,KAAK,IAAI,CAAC,IAAI;;CAGjD,wBAAgC,UAA0B;AACxD,SAAO,MAAM,QAAQ,cAAc,IAAI,CAAC,MAAM;;CAGhD,uBAA+B,YAAmC;EAChE,MAAM,UAAU,QAAQ,MAAM;AAM9B,OAAK,MAAM,WALM;GACf;GACA;GACA;GACD,EAC+B;GAC9B,MAAM,QAAQ,QAAQ,MAAM,QAAQ;AACpC,OAAI,CAAC,MACH;GAEF,MAAM,YAAY,KAAK,qBAAqB,MAAM,MAAM,GAAG;AAC3D,OAAI,UACF,QAAO;;AAGX,SAAO;;CAGT,eAAuB,cAA+B;AACpD,MAAI,KAAK,aAAa,IAAI,UAAU,CAClC,QAAO;AAET,OAAK,aAAa,IAAI,UAAU;AAChC,OAAK,aAAa,KAAK,UAAU;AACjC,MAAI,KAAK,aAAa,SAAS,KAAM;GACnC,MAAM,UAAU,KAAK,aAAa,OAAO,GAAG,IAAI;AAChD,QAAK,MAAM,MAAM,QACf,MAAK,aAAa,OAAO,GAAG;;AAGhC,SAAO;;CAGT,qBAA6B,OAAO,SAA4D;AAC9F,MAAI;AACF,SAAM,MAAM;WACL,OAAO;AACd,OAAI,CAAC,KAAK,oBAAoB,MAAM,IAAI,CAAC,KAAK,IAC5C,OAAM;AAER,SAAM,KAAK,IAAI,eAAe,gBAAgB;AAC9C,SAAM,MAAM;;;CAIhB,uBAA+B,UAA4B;EACzD,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,SAAO,QAAQ,SAAS,cAAc,IAAI,QAAQ,aAAa,CAAC,SAAS,4BAA4B;;CAGvG,6BAAqC,UAA4B;EAC/D,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,SAAO,QAAQ,SAAS,iBAAiB,IAAI,QAAQ,SAAS,eAAe;;CAG/E,gBAAwB,SAAiB,UAA2B;EAClE,IAAI,OAAO,QACR,QAAQ,4BAA4B,KAAK,CACzC,QAAQ,oBAAoB,SAAS,CACrC,QAAQ,cAAc,SAAS,CAC/B,QAAQ,2BAA2B,SAAS;EAE/C,MAAM,UAAU,KAAK,uBAAuB,MAAM;AAClD,MAAI,QACF,QAAO,KAAK,WAAW,SAAS,SAAS;AAE3C,SAAO;;CAGT,0BAAkC,UAAkC;EAElE,MAAM,SADU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,EAChD,MAAM,mBAAmB;AAC/C,MAAI,CAAC,MACH,QAAO;EAET,MAAM,QAAQ,MAAM,GAAG,MAAM;AAC7B,SAAO,MAAM,SAAS,IAAI,QAAQ;;CAGpC,cAAsB,YAA0B;AAC9C,MAAI,CAAC,KAAK,WAAW,KAAK,OAAO,KAAK,YACpC;AAEF,OAAK,cAAc,KAAK,QAAQ,QAAQ,CAAC,cAAc;AACrD,QAAK,cAAc;IACnB;;CAGJ,UAAkB,OAAO,YAAmC;EAC1D,IAAI,YAA0B;AAC9B,MAAI;AACF,SAAM,KAAK,kCAAkC;AAC7C,eAAY,KAAK,WAAW;AAC5B,SAAM,KAAK,oBAAoB,UAAU;AACzC,OAAI,CAAC,KAAK,SAAS;AACjB,UAAM,KAAK,YAAY,UAAU;AACjC;;AAEF,QAAK,MAAM;AACX,QAAK,mBAAmB;AAExB,WAAQ,IAAI,mBAAmB;WACxB,OAAO;AACd,OAAI,UACF,OAAM,KAAK,YAAY,UAAU;AAEnC,OAAI,CAAC,KAAK,QACR;AAEF,QAAK,oBAAoB;GACzB,MAAM,UAAU,KAAK,oBAAoB,OAAO,KAAK,iBAAiB;AACtE,OAAI,iBAAiB,2BAEnB,SAAQ,KAAK,wBAAwB,QAAQ,YAAY,KAAK,iBAAiB,+CAA+C,QAAQ,qBAAqB,MAAM,gBAAgB,UAAU,UAAU,MAAM,SAAS,UAAU,oBAAoB,MAAM,kBAAkB,YAAY;OAGtR,SAAQ,MAAM,sBAAsB,QAAQ,YAAY,KAAK,iBAAiB,cAAc,QAAQ,MAAM,KAAK,mBAAmB,MAAM,GAAG;AAE7I,QAAK,kBAAkB,SAAS,GAAG,QAAQ,QAAQ;;;CAIvD,kBAAmC;EACjC,MAAM,MAAM,IAAI,IAAI;GAClB,OAAO,KAAK,OAAO;GACnB,QAAQ,KAAK,OAAO;GACpB,MAAM,aAAa;GACnB,SAAS,CAAC,sBAAsB,0BAA0B;GAC1D,UAAU;GACV,UAAU;GACX,CAAC;AAEF,MAAI,GAAG,mBAAmB,OAAO,UAAU;AACzC,SAAM,KAAK,eAAe,MAAM;IAChC;AAEF,MAAI,GAAG,iBAAiB,OAAO,UAAU;AACvC,SAAM,KAAK,eAAe,MAAM;IAChC;AAEF,MAAI,eAAe,GAAG,cAAc,YAAY;AACzC,QAAK,kBAAkB,IAAI;IAChC;AAEF,SAAO;;CAGT,mCAA6C,YAA2B;AACtE,QAAM,IAAI,6BAA6B;GACrC,OAAO,KAAK,OAAO;GACnB,QAAQ,KAAK,OAAO;GACrB,CAAC,CAAC,wBAAwB;;CAG7B,oBAA4B,OAAO,QAA8B;AAC/D,MAAI,CAAC,KAAK,WAAW,KAAK,QAAQ,IAChC;AAEF,OAAK,MAAM;AACX,QAAM,KAAK,YAAY,IAAI;AAC3B,OAAK,oBAAoB;EACzB,MAAM,UAAU,KAAK,kBAAkB,KAAK,iBAAiB;AAE7D,UAAQ,MAAM,mCAAmC,QAAQ,IAAI;AAC7D,OAAK,kBAAkB,SAAS,eAAe;;CAGjD,qBAA6B,SAAiB,YAA0B;AACtE,MAAI,CAAC,KAAK,QACR;AAEF,OAAK,qBAAqB;AAC1B,OAAK,iBAAiB,iBAAiB;AACrC,QAAK,iBAAiB;AACtB,QAAK,WAAW,QAAQ;KACvB,QAAQ;;CAGb,4BAA0C;AACxC,MAAI,CAAC,KAAK,eACR;AAEF,eAAa,KAAK,eAAe;AACjC,OAAK,iBAAiB;;CAGxB,cAAsB,YAA2B;AAC/C,MAAI,CAAC,KAAK,IACR;EAEF,MAAM,MAAM,KAAK;AACjB,OAAK,MAAM;AACX,QAAM,KAAK,YAAY,IAAI;;CAG7B,cAAsB,OAAO,QAA8B;AACzD,MAAI,mBAAmB,kBAAkB;AACzC,MAAI,mBAAmB,gBAAgB;AACvC,MAAI,eAAe,mBAAmB,cAAc,KAAK;AACzD,MAAI;AACF,SAAM,IAAI,MAAM;UACV;;CAKV,sBAA8B,OAAO,QAA8B;EACjE,IAAI,QAA8C;EAClD,MAAM,iBAAiB,KAAK,oBAAoB,IAAI;AACpD,MAAI;AACF,SAAM,QAAQ,KAAK;IACjB,IAAI,OAAO;IACX,IAAI,SAAgB,GAAG,WAAW;AAChC,aAAQ,iBAAiB,uBAAO,IAAI,MAAM,gCAAgC,KAAK,iBAAiB,IAAI,CAAC,EAAE,KAAK,iBAAiB;MAC7H;IACF,IAAI,SAAgB,GAAG,WAAW;AAChC,oBAAe,SAAS;MACxB;IACH,CAAC;YACM;AACR,kBAAe,MAAM;AACrB,OAAI,MACF,cAAa,MAAM;;;CAKzB,uBAA+B,QAG1B;EACH,MAAM,QAIF;GACF,UAAU;GACV,QAAQ;GACR,YAAY;GACb;EACD,MAAM,QAAQ,UAAuB;AACnC,OAAI,MAAM,SACR;AAEF,SAAM,WAAW;AACjB,SAAM,SAAS,MAAM;;EAEvB,MAAM,mBAAmB,UAAyB;AAChD,wBAAK,IAAI,MAAM,yCAAyC,KAAK,mBAAmB,MAAM,GAAG,CAAC;;EAE5F,MAAM,mBAAmB,MAAc,WAA0B;AAC/D,wBAAK,IAAI,MAAM,8CAA8C,KAAK,WAAW,KAAK,kBAAkB,OAAO,GAAG,CAAC;;EAEjH,MAAM,kBAAkB,MAAe,YAA2B;AAChE,wBAAK,IAAI,MAAM,4CAA4C,OAAO,KAAK,CAAC,YAAY,KAAK,mBAAmB,QAAQ,GAAG,CAAC;;EAE1H,MAAM,kBAAkB,SAAwB;GAC9C,MAAM,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAkC,EAAE;AACrF,OAAI,MAAM,cAAc,cAAc,WACpC;AAEF,wBACE,IAAI,MACF,kDAAkD,OAAO,MAAM,QAAQ,UAAU,CAAC,UAAU,KAAK,mBAAmB,MAAM,SAAS,GACpI,CACF;;AAEH,MAAI,SAAS,GAAG,SAAS,gBAAgB;AACzC,MAAI,SAAS,GAAG,SAAS,gBAAgB;AACzC,MAAI,eAAe,GAAG,cAAc,OAAO,eAAe;AAC1D,MAAI,eAAe,GAAG,cAAc,UAAU,eAAe;AAC7D,QAAM,aAAa;AACjB,OAAI,SAAS,IAAI,SAAS,gBAAgB;AAC1C,OAAI,SAAS,IAAI,SAAS,gBAAgB;AAC1C,OAAI,eAAe,IAAI,cAAc,OAAO,eAAe;AAC3D,OAAI,eAAe,IAAI,cAAc,UAAU,eAAe;;AAEhE,SAAO;;CAGT,qBAA6B,WAA4B;AACvD,SAAO,OAAO,SAAS,OAAO,GAAG,OAAO,UAAU,GAAG,KAAK,mBAAmB,OAAO;;CAGtF,sBAA8B,UAA2B;AACvD,MAAI,iBAAiB,MACnB,QAAO,MAAM;AAEf,MAAI,SAAS,OAAO,UAAU,SAC5B,KAAI;AACF,UAAO,KAAK,UAAU,MAAM;UACtB;AACN,UAAO,OAAO,MAAM;;AAGxB,SAAO,OAAO,MAAM;;CAGtB,IAAI,YAAqB;AACvB,SAAO,KAAK,QAAQ;;CAGtB,qBAA6B,YAA4B;EACvD,MAAM,SAAS,KAAK,MAAM,KAAK,QAAQ,GAAG,IAAI;EAC9C,MAAM,MAAM,KAAK,IAAI,KAAK,gBAAgB,KAAK,kBAAkB,KAAK,KAAK,IAAI,GAAG,UAAU,EAAE,CAAC;AAC/F,SAAO,KAAK,IAAI,KAAK,gBAAgB,MAAM,OAAO;;CAGpD,uBAA+B,OAAgB,YAA4B;AACzE,MAAI,iBAAiB,8BAA8B,OAAO,MAAM,iBAAiB,SAC/E,QAAO,KAAK,IAAI,KAAK,iBAAiB,MAAM,aAAa;AAE3D,SAAO,KAAK,kBAAkB,QAAQ"}
@@ -0,0 +1,55 @@
1
+ //#region src/services/qq-gateway-startup-probe.service.ts
2
+ var QQGatewaySessionLimitError = class extends Error {
3
+ constructor(resetAfterMs, total, maxConcurrency) {
4
+ super(`QQ gateway session start limit exhausted; reset_after_ms=${resetAfterMs ?? "unknown"}, total=${total ?? "unknown"}, max_concurrency=${maxConcurrency ?? "unknown"}`);
5
+ this.resetAfterMs = resetAfterMs;
6
+ this.total = total;
7
+ this.maxConcurrency = maxConcurrency;
8
+ this.name = "QQGatewaySessionLimitError";
9
+ }
10
+ };
11
+ var QQGatewayStartupProbeService = class {
12
+ constructor(config) {
13
+ this.config = config;
14
+ }
15
+ verifySessionAvailable = async () => {
16
+ const accessToken = await this.fetchQqAccessToken();
17
+ const limit = (await this.fetchQqGateway(accessToken)).session_start_limit;
18
+ const remaining = this.readFiniteNumber(limit?.remaining);
19
+ if (remaining === void 0 || remaining > 0) return;
20
+ throw new QQGatewaySessionLimitError(this.readFiniteNumber(limit?.reset_after) ?? null, this.readFiniteNumber(limit?.total) ?? null, this.readFiniteNumber(limit?.max_concurrency) ?? null);
21
+ };
22
+ fetchQqAccessToken = async () => {
23
+ const response = await this.fetchImpl("https://bots.qq.com/app/getAppAccessToken", {
24
+ method: "POST",
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ "User-Agent": "QQBot/1.0"
28
+ },
29
+ body: JSON.stringify({
30
+ appId: this.config.appId,
31
+ clientSecret: this.config.secret
32
+ })
33
+ });
34
+ const payload = await response.json();
35
+ const token = typeof payload.access_token === "string" ? payload.access_token : "";
36
+ if (!response.ok || !token) throw new Error(`QQ access token probe failed: status=${response.status}`);
37
+ return token;
38
+ };
39
+ fetchQqGateway = async (accessToken) => {
40
+ const response = await this.fetchImpl("https://api.sgroup.qq.com/gateway/bot", { headers: { Authorization: `QQBot ${accessToken}` } });
41
+ const payload = await response.json();
42
+ if (!response.ok || typeof payload.url !== "string") throw new Error(`QQ gateway probe failed: status=${response.status}`);
43
+ return payload;
44
+ };
45
+ readFiniteNumber = (value) => {
46
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
47
+ };
48
+ get fetchImpl() {
49
+ return this.config.fetchImpl ?? fetch;
50
+ }
51
+ };
52
+ //#endregion
53
+ export { QQGatewaySessionLimitError, QQGatewayStartupProbeService };
54
+
55
+ //# sourceMappingURL=qq-gateway-startup-probe.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qq-gateway-startup-probe.service.js","names":[],"sources":["../../src/services/qq-gateway-startup-probe.service.ts"],"sourcesContent":["type QQGatewaySessionStartLimit = {\n total?: unknown;\n remaining?: unknown;\n reset_after?: unknown;\n max_concurrency?: unknown;\n};\n\ntype QQGatewayResponse = {\n url?: unknown;\n shards?: unknown;\n session_start_limit?: QQGatewaySessionStartLimit;\n};\n\ntype QQFetch = typeof fetch;\n\ntype QQGatewayStartupProbeConfig = {\n appId: string;\n secret: string;\n fetchImpl?: QQFetch;\n};\n\nexport class QQGatewaySessionLimitError extends Error {\n constructor(readonly resetAfterMs: number | null, readonly total: number | null, readonly maxConcurrency: number | null) {\n super(\n `QQ gateway session start limit exhausted; reset_after_ms=${resetAfterMs ?? \"unknown\"}, total=${total ?? \"unknown\"}, max_concurrency=${maxConcurrency ?? \"unknown\"}`\n );\n this.name = \"QQGatewaySessionLimitError\";\n }\n}\n\nexport class QQGatewayStartupProbeService {\n constructor(private readonly config: QQGatewayStartupProbeConfig) {}\n\n verifySessionAvailable = async (): Promise<void> => {\n const accessToken = await this.fetchQqAccessToken();\n const gateway = await this.fetchQqGateway(accessToken);\n const limit = gateway.session_start_limit;\n const remaining = this.readFiniteNumber(limit?.remaining);\n if (remaining === undefined || remaining > 0) {\n return;\n }\n throw new QQGatewaySessionLimitError(\n this.readFiniteNumber(limit?.reset_after) ?? null,\n this.readFiniteNumber(limit?.total) ?? null,\n this.readFiniteNumber(limit?.max_concurrency) ?? null\n );\n };\n\n private fetchQqAccessToken = async (): Promise<string> => {\n const response = await this.fetchImpl(\"https://bots.qq.com/app/getAppAccessToken\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"User-Agent\": \"QQBot/1.0\",\n },\n body: JSON.stringify({\n appId: this.config.appId,\n clientSecret: this.config.secret,\n }),\n });\n const payload = await response.json() as Record<string, unknown>;\n const token = typeof payload.access_token === \"string\" ? payload.access_token : \"\";\n if (!response.ok || !token) {\n throw new Error(`QQ access token probe failed: status=${response.status}`);\n }\n return token;\n };\n\n private fetchQqGateway = async (accessToken: string): Promise<QQGatewayResponse> => {\n const response = await this.fetchImpl(\"https://api.sgroup.qq.com/gateway/bot\", {\n headers: {\n Authorization: `QQBot ${accessToken}`,\n },\n });\n const payload = await response.json() as QQGatewayResponse;\n if (!response.ok || typeof payload.url !== \"string\") {\n throw new Error(`QQ gateway probe failed: status=${response.status}`);\n }\n return payload;\n };\n\n private readFiniteNumber = (value: unknown): number | undefined => {\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n };\n\n private get fetchImpl(): QQFetch {\n return this.config.fetchImpl ?? fetch;\n }\n}\n"],"mappings":";AAqBA,IAAa,6BAAb,cAAgD,MAAM;CACpD,YAAY,cAAsC,OAA+B,gBAAwC;AACvH,QACE,4DAA4D,gBAAgB,UAAU,UAAU,SAAS,UAAU,oBAAoB,kBAAkB,YAC1J;AAHkB,OAAA,eAAA;AAAsC,OAAA,QAAA;AAA+B,OAAA,iBAAA;AAIxF,OAAK,OAAO;;;AAIhB,IAAa,+BAAb,MAA0C;CACxC,YAAY,QAAsD;AAArC,OAAA,SAAA;;CAE7B,yBAAyB,YAA2B;EAClD,MAAM,cAAc,MAAM,KAAK,oBAAoB;EAEnD,MAAM,SADU,MAAM,KAAK,eAAe,YAAY,EAChC;EACtB,MAAM,YAAY,KAAK,iBAAiB,OAAO,UAAU;AACzD,MAAI,cAAc,KAAA,KAAa,YAAY,EACzC;AAEF,QAAM,IAAI,2BACR,KAAK,iBAAiB,OAAO,YAAY,IAAI,MAC7C,KAAK,iBAAiB,OAAO,MAAM,IAAI,MACvC,KAAK,iBAAiB,OAAO,gBAAgB,IAAI,KAClD;;CAGH,qBAA6B,YAA6B;EACxD,MAAM,WAAW,MAAM,KAAK,UAAU,6CAA6C;GACjF,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,cAAc;IACf;GACD,MAAM,KAAK,UAAU;IACnB,OAAO,KAAK,OAAO;IACnB,cAAc,KAAK,OAAO;IAC3B,CAAC;GACH,CAAC;EACF,MAAM,UAAU,MAAM,SAAS,MAAM;EACrC,MAAM,QAAQ,OAAO,QAAQ,iBAAiB,WAAW,QAAQ,eAAe;AAChF,MAAI,CAAC,SAAS,MAAM,CAAC,MACnB,OAAM,IAAI,MAAM,wCAAwC,SAAS,SAAS;AAE5E,SAAO;;CAGT,iBAAyB,OAAO,gBAAoD;EAClF,MAAM,WAAW,MAAM,KAAK,UAAU,yCAAyC,EAC7E,SAAS,EACP,eAAe,SAAS,eACzB,EACF,CAAC;EACF,MAAM,UAAU,MAAM,SAAS,MAAM;AACrC,MAAI,CAAC,SAAS,MAAM,OAAO,QAAQ,QAAQ,SACzC,OAAM,IAAI,MAAM,mCAAmC,SAAS,SAAS;AAEvE,SAAO;;CAGT,oBAA4B,UAAuC;AACjE,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,MAAM,GAAG,QAAQ,KAAA;;CAGvE,IAAY,YAAqB;AAC/B,SAAO,KAAK,OAAO,aAAa"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-extension-qq",
3
- "version": "0.1.23-beta.0",
3
+ "version": "0.1.23-beta.1",
4
4
  "private": false,
5
5
  "description": "NextClaw QQ channel extension process.",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "qq-official-bot": "1.0.12",
21
- "@nextclaw/extension-sdk": "0.2.15-beta.0"
21
+ "@nextclaw/extension-sdk": "0.2.15-beta.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^20.17.6",