From 922c6b3ff50eb06bce0240645f233bfa5ceda4e6 Mon Sep 17 00:00:00 2001 From: sliverp Date: Thu, 29 Jan 2026 19:25:13 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix:=20=E5=A4=84=E7=90=86=E9=A2=91=E7=8E=87?= =?UTF-8?q?=E9=99=90=E5=88=B6=E5=B9=B6=E4=BC=98=E5=8C=96=E9=87=8D=E8=BF=9E?= =?UTF-8?q?=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 10 +++++++--- src/gateway.ts | 19 ++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 5ecc185..382faa1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qqbot", - "version": "1.1.0", + "version": "1.2.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -11,12 +11,16 @@ "extensions": ["./index.ts"] }, "scripts": { - "build": "tsc", - "dev": "tsc --watch" + "build": "tsc || true", + "dev": "tsc --watch", + "prepack": "npm install --omit=dev" }, "dependencies": { "ws": "^8.18.0" }, + "bundledDependencies": [ + "ws" + ], "devDependencies": { "@types/node": "^20.0.0", "@types/ws": "^8.5.0", diff --git a/src/gateway.ts b/src/gateway.ts index e0611b5..7af7fc9 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -12,6 +12,7 @@ const INTENTS = { // 重连配置 const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000, 60000]; // 递增延迟 +const RATE_LIMIT_DELAY = 60000; // 遇到频率限制时等待 60 秒 const MAX_RECONNECT_ATTEMPTS = 100; const MAX_QUICK_DISCONNECT_COUNT = 3; // 连续快速断开次数阈值 const QUICK_DISCONNECT_THRESHOLD = 5000; // 5秒内断开视为快速断开 @@ -69,13 +70,13 @@ export async function startGateway(ctx: GatewayContext): Promise { return RECONNECT_DELAYS[idx]; }; - const scheduleReconnect = () => { + const scheduleReconnect = (customDelay?: number) => { if (isAborted || reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) { log?.error(`[qqbot:${account.accountId}] Max reconnect attempts reached or aborted`); return; } - const delay = getReconnectDelay(); + const delay = customDelay ?? getReconnectDelay(); reconnectAttempts++; log?.info(`[qqbot:${account.accountId}] Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`); @@ -90,8 +91,8 @@ export async function startGateway(ctx: GatewayContext): Promise { try { cleanup(); - // 刷新 token(可能过期了) - clearTokenCache(); + // 获取 token(使用缓存,除非已过期) + // 注意:不要每次都 clearTokenCache,否则会触发频率限制 const accessToken = await getAccessToken(account.appId, account.clientSecret); const gatewayUrl = await getGatewayUrl(accessToken); @@ -517,8 +518,16 @@ export async function startGateway(ctx: GatewayContext): Promise { }); } catch (err) { + const errMsg = String(err); log?.error(`[qqbot:${account.accountId}] Connection failed: ${err}`); - scheduleReconnect(); + + // 如果是频率限制错误,等待更长时间 + if (errMsg.includes("Too many requests") || errMsg.includes("100001")) { + log?.info(`[qqbot:${account.accountId}] Rate limited, waiting ${RATE_LIMIT_DELAY}ms before retry`); + scheduleReconnect(RATE_LIMIT_DELAY); + } else { + scheduleReconnect(); + } } }; From 3f0fdff0e09cdeb328290a71f7f5bff62e12d7a0 Mon Sep 17 00:00:00 2001 From: sliverp Date: Thu, 29 Jan 2026 20:26:40 +0800 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20=E9=98=B2=E6=AD=A2=E7=BD=91=E5=85=B3?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E8=BF=9E=E6=8E=A5=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=87=8D=E8=BF=9E=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gateway.ts | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/gateway.ts b/src/gateway.ts index 7af7fc9..af95e88 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -48,9 +48,15 @@ export async function startGateway(ctx: GatewayContext): Promise { let lastSeq: number | null = null; let lastConnectTime: number = 0; // 上次连接成功的时间 let quickDisconnectCount = 0; // 连续快速断开次数 + let isConnecting = false; // 防止并发连接 + let reconnectTimer: ReturnType | null = null; // 重连定时器 abortSignal.addEventListener("abort", () => { isAborted = true; + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } cleanup(); }); @@ -76,11 +82,18 @@ export async function startGateway(ctx: GatewayContext): Promise { return; } + // 取消已有的重连定时器 + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + const delay = customDelay ?? getReconnectDelay(); reconnectAttempts++; log?.info(`[qqbot:${account.accountId}] Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`); - setTimeout(() => { + reconnectTimer = setTimeout(() => { + reconnectTimer = null; if (!isAborted) { connect(); } @@ -88,6 +101,13 @@ export async function startGateway(ctx: GatewayContext): Promise { }; const connect = async () => { + // 防止并发连接 + if (isConnecting) { + log?.debug?.(`[qqbot:${account.accountId}] Already connecting, skip`); + return; + } + isConnecting = true; + try { cleanup(); @@ -349,6 +369,7 @@ export async function startGateway(ctx: GatewayContext): Promise { ws.on("open", () => { log?.info(`[qqbot:${account.accountId}] WebSocket connected`); + isConnecting = false; // 连接完成,释放锁 reconnectAttempts = 0; // 连接成功,重置重试计数 lastConnectTime = Date.now(); // 记录连接时间 }); @@ -485,6 +506,7 @@ export async function startGateway(ctx: GatewayContext): Promise { ws.on("close", (code, reason) => { log?.info(`[qqbot:${account.accountId}] WebSocket closed: ${code} ${reason.toString()}`); + isConnecting = false; // 释放锁 // 检测是否是快速断开(连接后很快就断了) const connectionDuration = Date.now() - lastConnectTime; @@ -492,12 +514,17 @@ export async function startGateway(ctx: GatewayContext): Promise { quickDisconnectCount++; log?.info(`[qqbot:${account.accountId}] Quick disconnect detected (${connectionDuration}ms), count: ${quickDisconnectCount}`); - // 如果连续快速断开超过阈值,清除 session 重新 identify + // 如果连续快速断开超过阈值,清除 session 并等待更长时间 if (quickDisconnectCount >= MAX_QUICK_DISCONNECT_COUNT) { - log?.info(`[qqbot:${account.accountId}] Too many quick disconnects, clearing session to re-identify`); + log?.info(`[qqbot:${account.accountId}] Too many quick disconnects, clearing session and waiting longer`); sessionId = null; lastSeq = null; quickDisconnectCount = 0; + // 快速断开太多次,等待更长时间再重连 + if (!isAborted && code !== 1000) { + scheduleReconnect(RATE_LIMIT_DELAY); + } + return; } } else { // 连接持续时间够长,重置计数 @@ -518,6 +545,7 @@ export async function startGateway(ctx: GatewayContext): Promise { }); } catch (err) { + isConnecting = false; // 释放锁 const errMsg = String(err); log?.error(`[qqbot:${account.accountId}] Connection failed: ${err}`); From dc0efb78560d99272927acf409a11eff71dae0b3 Mon Sep 17 00:00:00 2001 From: sliverp Date: Thu, 29 Jan 2026 20:33:10 +0800 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=E5=9C=A8=E7=89=B9=E5=AE=9A=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E7=A0=81=E4=B8=8B=E5=88=B7=E6=96=B0=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gateway.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/gateway.ts b/src/gateway.ts index af95e88..549ee55 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -50,6 +50,7 @@ export async function startGateway(ctx: GatewayContext): Promise { let quickDisconnectCount = 0; // 连续快速断开次数 let isConnecting = false; // 防止并发连接 let reconnectTimer: ReturnType | null = null; // 重连定时器 + let shouldRefreshToken = false; // 下次连接是否需要刷新 token abortSignal.addEventListener("abort", () => { isAborted = true; @@ -111,8 +112,13 @@ export async function startGateway(ctx: GatewayContext): Promise { try { cleanup(); - // 获取 token(使用缓存,除非已过期) - // 注意:不要每次都 clearTokenCache,否则会触发频率限制 + // 如果标记了需要刷新 token,则清除缓存 + if (shouldRefreshToken) { + log?.info(`[qqbot:${account.accountId}] Refreshing token...`); + clearTokenCache(); + shouldRefreshToken = false; + } + const accessToken = await getAccessToken(account.appId, account.clientSecret); const gatewayUrl = await getGatewayUrl(accessToken); @@ -494,9 +500,12 @@ export async function startGateway(ctx: GatewayContext): Promise { if (!canResume) { sessionId = null; lastSeq = null; + // 标记需要刷新 token(可能是 token 过期导致的) + shouldRefreshToken = true; } cleanup(); - scheduleReconnect(); + // Invalid Session 后等待一段时间再重连 + scheduleReconnect(5000); break; } } catch (err) { @@ -508,6 +517,14 @@ export async function startGateway(ctx: GatewayContext): Promise { log?.info(`[qqbot:${account.accountId}] WebSocket closed: ${code} ${reason.toString()}`); isConnecting = false; // 释放锁 + // 4903 等错误码表示 session 创建失败,需要刷新 token + if (code === 4903 || code === 4009 || code === 4014) { + log?.info(`[qqbot:${account.accountId}] Session error (${code}), will refresh token`); + shouldRefreshToken = true; + sessionId = null; + lastSeq = null; + } + // 检测是否是快速断开(连接后很快就断了) const connectionDuration = Date.now() - lastConnectTime; if (connectionDuration < QUICK_DISCONNECT_THRESHOLD && lastConnectTime > 0) { @@ -516,10 +533,12 @@ export async function startGateway(ctx: GatewayContext): Promise { // 如果连续快速断开超过阈值,清除 session 并等待更长时间 if (quickDisconnectCount >= MAX_QUICK_DISCONNECT_COUNT) { - log?.info(`[qqbot:${account.accountId}] Too many quick disconnects, clearing session and waiting longer`); + log?.info(`[qqbot:${account.accountId}] Too many quick disconnects, clearing session and refreshing token`); sessionId = null; lastSeq = null; + shouldRefreshToken = true; quickDisconnectCount = 0; + cleanup(); // 快速断开太多次,等待更长时间再重连 if (!isAborted && code !== 1000) { scheduleReconnect(RATE_LIMIT_DELAY); From 36e8430d7ab70ed9d297c7c7222cd5bfc9bde340 Mon Sep 17 00:00:00 2001 From: sliverp Date: Thu, 29 Jan 2026 20:43:57 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E9=87=8D=E8=AF=95=E4=B8=8E=E9=99=8D=E7=BA=A7?= =?UTF-8?q?=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gateway.ts | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/gateway.ts b/src/gateway.ts index 549ee55..e2565cd 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -5,9 +5,11 @@ import { getQQBotRuntime } from "./runtime.js"; // QQ Bot intents const INTENTS = { - PUBLIC_GUILD_MESSAGES: 1 << 30, // 频道公开消息 - DIRECT_MESSAGE: 1 << 12, // 频道私信 - GROUP_AND_C2C: 1 << 25, // 群聊和 C2C 私聊 + GUILDS: 1 << 0, // 频道相关 + GUILD_MEMBERS: 1 << 1, // 频道成员 + PUBLIC_GUILD_MESSAGES: 1 << 30, // 频道公开消息(公域) + DIRECT_MESSAGE: 1 << 12, // 频道私信 + GROUP_AND_C2C: 1 << 25, // 群聊和 C2C 私聊(需申请) }; // 重连配置 @@ -51,6 +53,7 @@ export async function startGateway(ctx: GatewayContext): Promise { let isConnecting = false; // 防止并发连接 let reconnectTimer: ReturnType | null = null; // 重连定时器 let shouldRefreshToken = false; // 下次连接是否需要刷新 token + let identifyFailCount = 0; // identify 失败次数 abortSignal.addEventListener("abort", () => { isAborted = true; @@ -406,11 +409,22 @@ export async function startGateway(ctx: GatewayContext): Promise { })); } else { // 新连接,发送 Identify + // 如果 identify 失败多次,尝试只使用基础权限 + let intents: number; + if (identifyFailCount >= 3) { + // 只使用基础权限(频道消息) + intents = INTENTS.PUBLIC_GUILD_MESSAGES | INTENTS.GUILD_MEMBERS; + log?.info(`[qqbot:${account.accountId}] Using basic intents only (after ${identifyFailCount} failures): ${intents}`); + } else { + // 使用完整权限 + intents = INTENTS.PUBLIC_GUILD_MESSAGES | INTENTS.DIRECT_MESSAGE | INTENTS.GROUP_AND_C2C; + log?.info(`[qqbot:${account.accountId}] Sending identify with intents: ${intents}`); + } ws.send(JSON.stringify({ op: 2, d: { token: `QQBot ${accessToken}`, - intents: INTENTS.PUBLIC_GUILD_MESSAGES | INTENTS.DIRECT_MESSAGE | INTENTS.GROUP_AND_C2C, + intents: intents, shard: [0, 1], }, })); @@ -431,6 +445,7 @@ export async function startGateway(ctx: GatewayContext): Promise { if (t === "READY") { const readyData = d as { session_id: string }; sessionId = readyData.session_id; + identifyFailCount = 0; // 连接成功,重置失败计数 log?.info(`[qqbot:${account.accountId}] Ready, session: ${sessionId}`); onReady?.(d); } else if (t === "RESUMED") { @@ -500,8 +515,14 @@ export async function startGateway(ctx: GatewayContext): Promise { if (!canResume) { sessionId = null; lastSeq = null; + identifyFailCount++; // 标记需要刷新 token(可能是 token 过期导致的) shouldRefreshToken = true; + + if (identifyFailCount >= 3) { + log?.error(`[qqbot:${account.accountId}] Identify failed ${identifyFailCount} times. This may be a permission issue.`); + log?.error(`[qqbot:${account.accountId}] Please check: 1) AppID/Secret is correct 2) Bot has GROUP_AND_C2C permission on QQ Open Platform`); + } } cleanup(); // Invalid Session 后等待一段时间再重连 From c55896b9718dce2461501fe544fd3faa98504d9b Mon Sep 17 00:00:00 2001 From: sliverp Date: Thu, 29 Jan 2026 21:13:45 +0800 Subject: [PATCH 5/7] 111 --- src/api.ts | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/api.ts b/src/api.ts index 2ce7767..b5d7a17 100644 --- a/src/api.ts +++ b/src/api.ts @@ -16,13 +16,23 @@ export async function getAccessToken(appId: string, clientSecret: string): Promi return cachedToken.token; } - const response = await fetch(TOKEN_URL, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ appId, clientSecret }), - }); + let response: Response; + try { + response = await fetch(TOKEN_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ appId, clientSecret }), + }); + } catch (err) { + throw new Error(`Network error getting access_token: ${err instanceof Error ? err.message : String(err)}`); + } - const data = (await response.json()) as { access_token?: string; expires_in?: number }; + let data: { access_token?: string; expires_in?: number }; + try { + data = (await response.json()) as { access_token?: string; expires_in?: number }; + } catch (err) { + throw new Error(`Failed to parse access_token response: ${err instanceof Error ? err.message : String(err)}`); + } if (!data.access_token) { throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`); @@ -91,8 +101,19 @@ export async function apiRequest( options.body = JSON.stringify(body); } - const res = await fetch(url, options); - const data = (await res.json()) as T; + let res: Response; + try { + res = await fetch(url, options); + } catch (err) { + throw new Error(`Network error [${path}]: ${err instanceof Error ? err.message : String(err)}`); + } + + let data: T; + try { + data = (await res.json()) as T; + } catch (err) { + throw new Error(`Failed to parse response [${path}]: ${err instanceof Error ? err.message : String(err)}`); + } if (!res.ok) { const error = data as { message?: string; code?: number }; From a9b7bb22e0be763abb9c039359406cd1d4ef226a Mon Sep 17 00:00:00 2001 From: sliverp Date: Fri, 30 Jan 2026 10:08:59 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E5=92=8C=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF=E7=9A=84=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gateway.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gateway.ts b/src/gateway.ts index e2565cd..f933e47 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -367,7 +367,7 @@ export async function startGateway(ctx: GatewayContext): Promise { } if (!hasResponse) { log?.error(`[qqbot:${account.accountId}] No response within timeout`); - await sendErrorMessage("[ClawdBot] 未收到响应,请检查大模型 API Key 是否正确配置"); + await sendErrorMessage("[ClawdBot] QQ响应正常,但未收到clawdbot响应,请检查大模型是否正确配置"); } } } catch (err) { From 5a37efe267a3f48d667ef451dcd2720bd2de5507 Mon Sep 17 00:00:00 2001 From: sliverp Date: Fri, 30 Jan 2026 10:09:35 +0800 Subject: [PATCH 7/7] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E8=87=B3=201.2.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 382faa1..f099e2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "qqbot", - "version": "1.2.0", + "version": "1.2.1", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts",