refactor: 改进网关权限降级机制
This commit is contained in:
@@ -4,15 +4,39 @@ import { getAccessToken, getGatewayUrl, sendC2CMessage, sendChannelMessage, send
|
|||||||
import { getQQBotRuntime } from "./runtime.js";
|
import { getQQBotRuntime } from "./runtime.js";
|
||||||
import { startImageServer, saveImage, isImageServerRunning, type ImageServerConfig } from "./image-server.js";
|
import { startImageServer, saveImage, isImageServerRunning, type ImageServerConfig } from "./image-server.js";
|
||||||
|
|
||||||
// QQ Bot intents
|
// QQ Bot intents - 按权限级别分组
|
||||||
const INTENTS = {
|
const INTENTS = {
|
||||||
|
// 基础权限(默认有)
|
||||||
GUILDS: 1 << 0, // 频道相关
|
GUILDS: 1 << 0, // 频道相关
|
||||||
GUILD_MEMBERS: 1 << 1, // 频道成员
|
GUILD_MEMBERS: 1 << 1, // 频道成员
|
||||||
PUBLIC_GUILD_MESSAGES: 1 << 30, // 频道公开消息(公域)
|
PUBLIC_GUILD_MESSAGES: 1 << 30, // 频道公开消息(公域)
|
||||||
|
// 需要申请的权限
|
||||||
DIRECT_MESSAGE: 1 << 12, // 频道私信
|
DIRECT_MESSAGE: 1 << 12, // 频道私信
|
||||||
GROUP_AND_C2C: 1 << 25, // 群聊和 C2C 私聊(需申请)
|
GROUP_AND_C2C: 1 << 25, // 群聊和 C2C 私聊(需申请)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 权限级别:从高到低依次尝试
|
||||||
|
const INTENT_LEVELS = [
|
||||||
|
// Level 0: 完整权限(群聊 + 私信 + 频道)
|
||||||
|
{
|
||||||
|
name: "full",
|
||||||
|
intents: INTENTS.PUBLIC_GUILD_MESSAGES | INTENTS.DIRECT_MESSAGE | INTENTS.GROUP_AND_C2C,
|
||||||
|
description: "群聊+私信+频道",
|
||||||
|
},
|
||||||
|
// Level 1: 群聊 + 频道(无私信)
|
||||||
|
{
|
||||||
|
name: "group+channel",
|
||||||
|
intents: INTENTS.PUBLIC_GUILD_MESSAGES | INTENTS.GROUP_AND_C2C,
|
||||||
|
description: "群聊+频道",
|
||||||
|
},
|
||||||
|
// Level 2: 仅频道(基础权限)
|
||||||
|
{
|
||||||
|
name: "channel-only",
|
||||||
|
intents: INTENTS.PUBLIC_GUILD_MESSAGES | INTENTS.GUILD_MEMBERS,
|
||||||
|
description: "仅频道消息",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// 重连配置
|
// 重连配置
|
||||||
const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000, 60000]; // 递增延迟
|
const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000, 60000]; // 递增延迟
|
||||||
const RATE_LIMIT_DELAY = 60000; // 遇到频率限制时等待 60 秒
|
const RATE_LIMIT_DELAY = 60000; // 遇到频率限制时等待 60 秒
|
||||||
@@ -85,7 +109,8 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|||||||
let isConnecting = false; // 防止并发连接
|
let isConnecting = false; // 防止并发连接
|
||||||
let reconnectTimer: ReturnType<typeof setTimeout> | null = null; // 重连定时器
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null; // 重连定时器
|
||||||
let shouldRefreshToken = false; // 下次连接是否需要刷新 token
|
let shouldRefreshToken = false; // 下次连接是否需要刷新 token
|
||||||
let identifyFailCount = 0; // identify 失败次数
|
let intentLevelIndex = 0; // 当前尝试的权限级别索引
|
||||||
|
let lastSuccessfulIntentLevel = -1; // 上次成功的权限级别
|
||||||
|
|
||||||
abortSignal.addEventListener("abort", () => {
|
abortSignal.addEventListener("abort", () => {
|
||||||
isAborted = true;
|
isAborted = true;
|
||||||
@@ -503,22 +528,15 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
// 新连接,发送 Identify
|
// 新连接,发送 Identify
|
||||||
// 如果 identify 失败多次,尝试只使用基础权限
|
// 如果有上次成功的级别,直接使用;否则从当前级别开始尝试
|
||||||
let intents: number;
|
const levelToUse = lastSuccessfulIntentLevel >= 0 ? lastSuccessfulIntentLevel : intentLevelIndex;
|
||||||
if (identifyFailCount >= 3) {
|
const intentLevel = INTENT_LEVELS[Math.min(levelToUse, INTENT_LEVELS.length - 1)];
|
||||||
// 只使用基础权限(频道消息)
|
log?.info(`[qqbot:${account.accountId}] Sending identify with intents: ${intentLevel.intents} (${intentLevel.description})`);
|
||||||
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({
|
ws.send(JSON.stringify({
|
||||||
op: 2,
|
op: 2,
|
||||||
d: {
|
d: {
|
||||||
token: `QQBot ${accessToken}`,
|
token: `QQBot ${accessToken}`,
|
||||||
intents: intents,
|
intents: intentLevel.intents,
|
||||||
shard: [0, 1],
|
shard: [0, 1],
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
@@ -539,8 +557,10 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|||||||
if (t === "READY") {
|
if (t === "READY") {
|
||||||
const readyData = d as { session_id: string };
|
const readyData = d as { session_id: string };
|
||||||
sessionId = readyData.session_id;
|
sessionId = readyData.session_id;
|
||||||
identifyFailCount = 0; // 连接成功,重置失败计数
|
// 记录成功的权限级别
|
||||||
log?.info(`[qqbot:${account.accountId}] Ready, session: ${sessionId}`);
|
lastSuccessfulIntentLevel = intentLevelIndex;
|
||||||
|
const successLevel = INTENT_LEVELS[intentLevelIndex];
|
||||||
|
log?.info(`[qqbot:${account.accountId}] Ready with ${successLevel.description}, session: ${sessionId}`);
|
||||||
onReady?.(d);
|
onReady?.(d);
|
||||||
} else if (t === "RESUMED") {
|
} else if (t === "RESUMED") {
|
||||||
log?.info(`[qqbot:${account.accountId}] Session resumed`);
|
log?.info(`[qqbot:${account.accountId}] Session resumed`);
|
||||||
@@ -605,22 +625,27 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
|||||||
|
|
||||||
case 9: // Invalid Session
|
case 9: // Invalid Session
|
||||||
const canResume = d as boolean;
|
const canResume = d as boolean;
|
||||||
log?.error(`[qqbot:${account.accountId}] Invalid session, can resume: ${canResume}, raw: ${rawData}`);
|
const currentLevel = INTENT_LEVELS[intentLevelIndex];
|
||||||
|
log?.error(`[qqbot:${account.accountId}] Invalid session (${currentLevel.description}), can resume: ${canResume}, raw: ${rawData}`);
|
||||||
|
|
||||||
if (!canResume) {
|
if (!canResume) {
|
||||||
sessionId = null;
|
sessionId = null;
|
||||||
lastSeq = 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.`);
|
if (intentLevelIndex < INTENT_LEVELS.length - 1) {
|
||||||
log?.error(`[qqbot:${account.accountId}] Please check: 1) AppID/Secret is correct 2) Bot has GROUP_AND_C2C permission on QQ Open Platform`);
|
intentLevelIndex++;
|
||||||
|
const nextLevel = INTENT_LEVELS[intentLevelIndex];
|
||||||
|
log?.info(`[qqbot:${account.accountId}] Downgrading intents to: ${nextLevel.description}`);
|
||||||
|
} else {
|
||||||
|
// 已经是最低权限级别了
|
||||||
|
log?.error(`[qqbot:${account.accountId}] All intent levels failed. Please check AppID/Secret.`);
|
||||||
|
shouldRefreshToken = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cleanup();
|
cleanup();
|
||||||
// Invalid Session 后等待一段时间再重连
|
// Invalid Session 后等待一段时间再重连
|
||||||
scheduleReconnect(5000);
|
scheduleReconnect(3000);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user