From 38b0c07367920f5b6989dc4766afea7f51d2c1a4 Mon Sep 17 00:00:00 2001 From: leoqlin Date: Sat, 31 Jan 2026 22:18:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=AE=8C=E6=95=B4chu?= =?UTF-8?q?nk=E7=9B=B4=E6=8E=A5=E5=8F=91=E9=80=81=E6=B6=88=E6=81=AF,?= =?UTF-8?q?=E9=80=82=E9=85=8D=E4=B8=BB=E5=8A=A8=E6=B6=88=E6=81=AF=E5=8F=91?= =?UTF-8?q?=E9=80=81=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++- src/channel.ts | 20 +++++++++++++++++++ src/gateway.ts | 28 ++++++++++++++++++-------- src/outbound.ts | 53 +++++++++++++++++++++++++++++++++++-------------- 4 files changed, 80 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 76add87..0fe2f49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -dist \ No newline at end of file +dist +distnode_modules diff --git a/src/channel.ts b/src/channel.ts index ec179d7..42a9036 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -26,6 +26,26 @@ export const qqbotPlugin: ChannelPlugin = { reload: { configPrefixes: ["channels.qqbot"] }, // CLI onboarding wizard onboarding: qqbotOnboardingAdapter, + // 消息目标解析 + messaging: { + normalizeTarget: (target) => { + // 支持格式: qqbot:c2c:xxx, qqbot:group:xxx, c2c:xxx, group:xxx, openid + const normalized = target.replace(/^qqbot:/i, ""); + return { ok: true, to: normalized }; + }, + targetResolver: { + looksLikeId: (id) => { + // 先去掉 qqbot: 前缀 + const normalized = id.replace(/^qqbot:/i, ""); + // 支持 c2c:xxx, group:xxx, channel:xxx 格式 + if (normalized.startsWith("c2c:") || normalized.startsWith("group:") || normalized.startsWith("channel:")) return true; + // 支持纯 openid(32位十六进制) + if (/^[A-F0-9]{32}$/i.test(normalized)) return true; + return false; + }, + hint: "c2c: or group:", + }, + }, config: { listAccountIds: (cfg) => listQQBotAccountIds(cfg), resolveAccount: (cfg, accountId) => resolveQQBotAccount(cfg, accountId), diff --git a/src/gateway.ts b/src/gateway.ts index bde105c..f4255f4 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -320,7 +320,7 @@ export async function startGateway(ctx: GatewayContext): Promise { const fromAddress = event.type === "guild" ? `qqbot:channel:${event.channelId}` : event.type === "group" ? `qqbot:group:${event.groupOpenid}` - : `qqbot:${event.senderId}`; + : `qqbot:c2c:${event.senderId}`; const toAddress = fromAddress; const ctxPayload = pluginRuntime.channel.reply.finalizeInboundContext({ @@ -345,6 +345,9 @@ export async function startGateway(ctx: GatewayContext): Promise { QQGroupOpenid: event.groupOpenid, }); + // 打印 ctxPayload 详细信息(便于调试) + log?.info(`[qqbot:${account.accountId}] ctxPayload: From=${fromAddress}, To=${toAddress}, SessionKey=${route.sessionKey}, AccountId=${route.accountId}, ChatType=${isGroup ? "group" : "direct"}, SenderId=${event.senderId}, MessageSid=${event.messageId}, BodyLen=${body?.length ?? 0}`); + // 发送消息的辅助函数,带 token 过期重试 const sendWithTokenRetry = async (sendFn: (token: string) => Promise) => { try { @@ -386,7 +389,7 @@ export async function startGateway(ctx: GatewayContext): Promise { // 追踪是否有响应 let hasResponse = false; - const responseTimeout = 30000; // 30秒超时 + const responseTimeout = 300000; // 30秒超时 let timeoutId: ReturnType | null = null; const timeoutPromise = new Promise((_, reject) => { @@ -397,20 +400,26 @@ export async function startGateway(ctx: GatewayContext): Promise { }, responseTimeout); }); + // 调用 dispatchReply + log?.info(`[qqbot:${account.accountId}] dispatchReply: agentId=${route.agentId}, prefix=${messagesConfig.responsePrefix ?? "(none)"}`); + const dispatchPromise = pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({ ctx: ctxPayload, cfg, dispatcherOptions: { responsePrefix: messagesConfig.responsePrefix, - deliver: async (payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string }) => { + deliver: async (payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string }, info: { kind: string }) => { hasResponse = true; if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } - log?.info(`[qqbot:${account.accountId}] deliver called, payload keys: ${Object.keys(payload).join(", ")}`); - + log?.info(`[qqbot:${account.accountId}] deliver(${info.kind}): textLen=${payload.text?.length ?? 0}, mediaUrls=${payload.mediaUrls?.length ?? 0}, mediaUrl=${payload.mediaUrl ? "yes" : "no"}`); + if (payload.text) { + log?.info(`[qqbot:${account.accountId}] text preview: ${payload.text.slice(0, 150).replace(/\n/g, "\\n")}...`); + } + let replyText = payload.text ?? ""; // 收集所有图片路径 @@ -535,15 +544,16 @@ export async function startGateway(ctx: GatewayContext): Promise { try { await sendWithTokenRetry(async (token) => { if (event.type === "c2c") { + log?.info(`[qqbot:${account.accountId}] sendC2CImage -> ${event.senderId}`); await sendC2CImageMessage(token, event.senderId, imageUrl, event.messageId); } else if (event.type === "group" && event.groupOpenid) { + log?.info(`[qqbot:${account.accountId}] sendGroupImage -> ${event.groupOpenid}`); await sendGroupImageMessage(token, event.groupOpenid, imageUrl, event.messageId); } // 频道消息暂不支持富媒体,跳过图片 }); - log?.info(`[qqbot:${account.accountId}] Sent image: ${imageUrl.slice(0, 50)}...`); } catch (imgErr) { - log?.error(`[qqbot:${account.accountId}] Failed to send image: ${imgErr}`); + log?.error(`[qqbot:${account.accountId}] Image send failed: ${imgErr}`); // 图片发送失败时,显示错误信息而不是 URL const errMsg = String(imgErr).slice(0, 200); replyText = `[图片发送失败: ${errMsg}]\n${replyText}`; @@ -554,14 +564,16 @@ export async function startGateway(ctx: GatewayContext): Promise { if (replyText.trim()) { await sendWithTokenRetry(async (token) => { if (event.type === "c2c") { + log?.info(`[qqbot:${account.accountId}] sendC2CText -> ${event.senderId}, len=${replyText.length}`); await sendC2CMessage(token, event.senderId, replyText, event.messageId); } else if (event.type === "group" && event.groupOpenid) { + log?.info(`[qqbot:${account.accountId}] sendGroupText -> ${event.groupOpenid}, len=${replyText.length}`); await sendGroupMessage(token, event.groupOpenid, replyText, event.messageId); } else if (event.channelId) { + log?.info(`[qqbot:${account.accountId}] sendChannelText -> ${event.channelId}, len=${replyText.length}`); await sendChannelMessage(token, event.channelId, replyText, event.messageId); } }); - log?.info(`[qqbot:${account.accountId}] Sent text reply`); } pluginRuntime.channel.activity.record({ diff --git a/src/outbound.ts b/src/outbound.ts index a8f9809..4d7ac47 100644 --- a/src/outbound.ts +++ b/src/outbound.ts @@ -26,32 +26,38 @@ export interface OutboundResult { /** * 解析目标地址 * 格式: - * - openid (32位十六进制) -> C2C 单聊 + * - c2c:xxx -> C2C 单聊 * - group:xxx -> 群聊 * - channel:xxx -> 频道 - * - 纯数字 -> 频道 + * - 无前缀 -> 默认当作 C2C 单聊 */ function parseTarget(to: string): { type: "c2c" | "group" | "channel"; id: string } { - if (to.startsWith("group:")) { - return { type: "group", id: to.slice(6) }; + // 去掉 qqbot: 前缀 + let id = to.replace(/^qqbot:/i, ""); + + if (id.startsWith("c2c:")) { + return { type: "c2c", id: id.slice(4) }; } - if (to.startsWith("channel:")) { - return { type: "channel", id: to.slice(8) }; + if (id.startsWith("group:")) { + return { type: "group", id: id.slice(6) }; } - // openid 通常是 32 位十六进制 - if (/^[A-F0-9]{32}$/i.test(to)) { - return { type: "c2c", id: to }; + if (id.startsWith("channel:")) { + return { type: "channel", id: id.slice(8) }; } - // 默认当作频道 ID - return { type: "channel", id: to }; + // 默认当作 c2c(私聊) + return { type: "c2c", id }; } /** - * 发送文本消息(被动回复,需要 replyToId) + * 发送文本消息 + * - 有 replyToId: 被动回复,无配额限制 + * - 无 replyToId: 主动发送,有配额限制(每月4条/用户/群) */ export async function sendText(ctx: OutboundContext): Promise { const { to, text, replyToId, account } = ctx; + console.log("[qqbot] sendText ctx:", JSON.stringify({ to, text: text?.slice(0, 50), replyToId, accountId: account.accountId }, null, 2)); + if (!account.appId || !account.clientSecret) { return { channel: "qqbot", error: "QQBot not configured (missing appId or clientSecret)" }; } @@ -59,15 +65,32 @@ export async function sendText(ctx: OutboundContext): Promise { try { const accessToken = await getAccessToken(account.appId, account.clientSecret); const target = parseTarget(to); + console.log("[qqbot] sendText target:", JSON.stringify(target)); + // 如果没有 replyToId,使用主动发送接口 + if (!replyToId) { + if (target.type === "c2c") { + const result = await sendProactiveC2CMessage(accessToken, target.id, text); + return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp }; + } else if (target.type === "group") { + const result = await sendProactiveGroupMessage(accessToken, target.id, text); + return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp }; + } else { + // 频道暂不支持主动消息 + const result = await sendChannelMessage(accessToken, target.id, text); + return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp }; + } + } + + // 有 replyToId,使用被动回复接口 if (target.type === "c2c") { - const result = await sendC2CMessage(accessToken, target.id, text, replyToId ?? undefined); + const result = await sendC2CMessage(accessToken, target.id, text, replyToId); return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp }; } else if (target.type === "group") { - const result = await sendGroupMessage(accessToken, target.id, text, replyToId ?? undefined); + const result = await sendGroupMessage(accessToken, target.id, text, replyToId); return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp }; } else { - const result = await sendChannelMessage(accessToken, target.id, text, replyToId ?? undefined); + const result = await sendChannelMessage(accessToken, target.id, text, replyToId); return { channel: "qqbot", messageId: result.id, timestamp: result.timestamp }; } } catch (err) {