feat: 支持主动发送消息和附件处理
新增 `sendProactiveC2CMessage` 和 `sendProactiveGroupMessage` API,支持无需 msg_id 的主动消息发送(有月限额)。在网关中增加附件处理逻辑,支持解析图片和普通附件,并将附件信息传递给上下文。同时优化了 `sendText` 的目标地址解析逻辑,支持 `group:` 和 `channel:` 前缀,并新增 `sendProactiveMessage` 方法。
This commit is contained in:
@@ -13,6 +13,8 @@ const INTENTS = {
|
||||
// 重连配置
|
||||
const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000, 60000]; // 递增延迟
|
||||
const MAX_RECONNECT_ATTEMPTS = 100;
|
||||
const MAX_QUICK_DISCONNECT_COUNT = 3; // 连续快速断开次数阈值
|
||||
const QUICK_DISCONNECT_THRESHOLD = 5000; // 5秒内断开视为快速断开
|
||||
|
||||
export interface GatewayContext {
|
||||
account: ResolvedQQBotAccount;
|
||||
@@ -43,6 +45,8 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
||||
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let sessionId: string | null = null;
|
||||
let lastSeq: number | null = null;
|
||||
let lastConnectTime: number = 0; // 上次连接成功的时间
|
||||
let quickDisconnectCount = 0; // 连续快速断开次数
|
||||
|
||||
abortSignal.addEventListener("abort", () => {
|
||||
isAborted = true;
|
||||
@@ -109,8 +113,12 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
||||
channelId?: string;
|
||||
guildId?: string;
|
||||
groupOpenid?: string;
|
||||
attachments?: Array<{ content_type: string; url: string; filename?: string }>;
|
||||
}) => {
|
||||
log?.info(`[qqbot:${account.accountId}] Processing message from ${event.senderId}: ${event.content}`);
|
||||
if (event.attachments?.length) {
|
||||
log?.info(`[qqbot:${account.accountId}] Attachments: ${event.attachments.length}`);
|
||||
}
|
||||
|
||||
pluginRuntime.channel.activity.record({
|
||||
channel: "qqbot",
|
||||
@@ -141,7 +149,23 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
||||
if (account.systemPrompt) {
|
||||
systemPrompts.push(account.systemPrompt);
|
||||
}
|
||||
const messageBody = `【系统提示】\n${systemPrompts.join("\n")}\n\n【用户输入】\n${event.content}`;
|
||||
|
||||
// 处理附件(图片等)
|
||||
let attachmentInfo = "";
|
||||
const imageUrls: string[] = [];
|
||||
if (event.attachments?.length) {
|
||||
for (const att of event.attachments) {
|
||||
if (att.content_type?.startsWith("image/")) {
|
||||
imageUrls.push(att.url);
|
||||
attachmentInfo += `\n[图片: ${att.url}]`;
|
||||
} else {
|
||||
attachmentInfo += `\n[附件: ${att.filename ?? att.content_type}]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const userContent = event.content + attachmentInfo;
|
||||
const messageBody = `【系统提示】\n${systemPrompts.join("\n")}\n\n【用户输入】\n${userContent}`;
|
||||
|
||||
const body = pluginRuntime.channel.reply.formatInboundEnvelope({
|
||||
channel: "QQBot",
|
||||
@@ -154,6 +178,8 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
||||
name: event.senderName,
|
||||
},
|
||||
envelope: envelopeOptions,
|
||||
// 传递图片 URL 列表
|
||||
...(imageUrls.length > 0 ? { imageUrls } : {}),
|
||||
});
|
||||
|
||||
const fromAddress = event.type === "guild" ? `qqbot:channel:${event.channelId}`
|
||||
@@ -301,6 +327,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
||||
ws.on("open", () => {
|
||||
log?.info(`[qqbot:${account.accountId}] WebSocket connected`);
|
||||
reconnectAttempts = 0; // 连接成功,重置重试计数
|
||||
lastConnectTime = Date.now(); // 记录连接时间
|
||||
});
|
||||
|
||||
ws.on("message", async (data) => {
|
||||
@@ -366,6 +393,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
||||
content: event.content,
|
||||
messageId: event.id,
|
||||
timestamp: event.timestamp,
|
||||
attachments: event.attachments,
|
||||
});
|
||||
} else if (t === "AT_MESSAGE_CREATE") {
|
||||
const event = d as GuildMessageEvent;
|
||||
@@ -378,6 +406,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
||||
timestamp: event.timestamp,
|
||||
channelId: event.channel_id,
|
||||
guildId: event.guild_id,
|
||||
attachments: event.attachments,
|
||||
});
|
||||
} else if (t === "DIRECT_MESSAGE_CREATE") {
|
||||
const event = d as GuildMessageEvent;
|
||||
@@ -389,6 +418,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
||||
messageId: event.id,
|
||||
timestamp: event.timestamp,
|
||||
guildId: event.guild_id,
|
||||
attachments: event.attachments,
|
||||
});
|
||||
} else if (t === "GROUP_AT_MESSAGE_CREATE") {
|
||||
const event = d as GroupMessageEvent;
|
||||
@@ -399,6 +429,7 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
||||
messageId: event.id,
|
||||
timestamp: event.timestamp,
|
||||
groupOpenid: event.group_openid,
|
||||
attachments: event.attachments,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -431,6 +462,25 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
|
||||
|
||||
ws.on("close", (code, reason) => {
|
||||
log?.info(`[qqbot:${account.accountId}] WebSocket closed: ${code} ${reason.toString()}`);
|
||||
|
||||
// 检测是否是快速断开(连接后很快就断了)
|
||||
const connectionDuration = Date.now() - lastConnectTime;
|
||||
if (connectionDuration < QUICK_DISCONNECT_THRESHOLD && lastConnectTime > 0) {
|
||||
quickDisconnectCount++;
|
||||
log?.info(`[qqbot:${account.accountId}] Quick disconnect detected (${connectionDuration}ms), count: ${quickDisconnectCount}`);
|
||||
|
||||
// 如果连续快速断开超过阈值,清除 session 重新 identify
|
||||
if (quickDisconnectCount >= MAX_QUICK_DISCONNECT_COUNT) {
|
||||
log?.info(`[qqbot:${account.accountId}] Too many quick disconnects, clearing session to re-identify`);
|
||||
sessionId = null;
|
||||
lastSeq = null;
|
||||
quickDisconnectCount = 0;
|
||||
}
|
||||
} else {
|
||||
// 连接持续时间够长,重置计数
|
||||
quickDisconnectCount = 0;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
|
||||
// 非正常关闭则重连
|
||||
|
||||
Reference in New Issue
Block a user