@nextclaw/channel-extension-qq 0.1.22 → 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.
|
|
260
|
-
console.
|
|
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([
|
|
324
|
-
|
|
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
|
-
|
|
339
|
-
if (error instanceof
|
|
340
|
-
return
|
|
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":"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"}
|
|
@@ -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.
|
|
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.
|
|
21
|
+
"@nextclaw/extension-sdk": "0.2.15-beta.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^20.17.6",
|