feat: 支持主动发送消息和附件处理

新增 `sendProactiveC2CMessage` 和 `sendProactiveGroupMessage` API,支持无需 msg_id 的主动消息发送(有月限额)。在网关中增加附件处理逻辑,支持解析图片和普通附件,并将附件信息传递给上下文。同时优化了 `sendText` 的目标地址解析逻辑,支持 `group:` 和 `channel:` 前缀,并新增 `sendProactiveMessage` 方法。
This commit is contained in:
sliverp
2026-01-29 16:17:06 +08:00
parent af31a001e9
commit 869519de7c
4 changed files with 170 additions and 10 deletions

View File

@@ -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();
// 非正常关闭则重连