From 6154e4f9e12698eddfa2feffabc9f5edf5968acb Mon Sep 17 00:00:00 2001 From: sliverp Date: Wed, 28 Jan 2026 17:36:53 +0800 Subject: [PATCH] 11 --- index.ts | 10 +++- package.json | 3 + src/channel.ts | 28 ++-------- src/gateway.ts | 145 +++++++++++++++++++++++++++++++++++++++++-------- src/runtime.ts | 14 +++++ 5 files changed, 154 insertions(+), 46 deletions(-) create mode 100644 src/runtime.ts diff --git a/index.ts b/index.ts index 2fa41ed..9bfaa73 100644 --- a/index.ts +++ b/index.ts @@ -1,13 +1,21 @@ import type { MoltbotPluginApi } from "clawdbot/plugin-sdk"; import { qqbotPlugin } from "./src/channel.js"; +import { setQQBotRuntime } from "./src/runtime.js"; -export default { +const plugin = { + id: "qqbot", + name: "QQ Bot", + description: "QQ Bot channel plugin", register(api: MoltbotPluginApi) { + setQQBotRuntime(api.runtime); api.registerChannel({ plugin: qqbotPlugin }); }, }; +export default plugin; + export { qqbotPlugin } from "./src/channel.js"; +export { setQQBotRuntime, getQQBotRuntime } from "./src/runtime.js"; export * from "./src/types.js"; export * from "./src/api.js"; export * from "./src/config.js"; diff --git a/package.json b/package.json index 3f55e1a..0057703 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", + "moltbot": { + "extensions": ["./index.ts"] + }, "scripts": { "build": "tsc", "dev": "tsc --watch" diff --git a/src/channel.ts b/src/channel.ts index aab45af..ba4ab70 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1,4 +1,4 @@ -import type { ChannelPlugin, MoltbotPluginApi } from "clawdbot/plugin-sdk"; +import type { ChannelPlugin } from "clawdbot/plugin-sdk"; import type { ResolvedQQBotAccount } from "./types.js"; import { listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig } from "./config.js"; import { sendText } from "./outbound.js"; @@ -39,7 +39,6 @@ export const qqbotPlugin: ChannelPlugin = { setup: { validateInput: ({ input }) => { if (!input.token && !input.tokenFile && !input.useEnv) { - // token 在这里是 appId:clientSecret 格式 return "QQBot requires --token (format: appId:clientSecret) or --use-env"; } return null; @@ -49,7 +48,6 @@ export const qqbotPlugin: ChannelPlugin = { let clientSecret = ""; if (input.token) { - // 支持 appId:clientSecret 格式 const parts = input.token.split(":"); if (parts.length === 2) { appId = parts[0]; @@ -80,32 +78,16 @@ export const qqbotPlugin: ChannelPlugin = { }, gateway: { startAccount: async (ctx) => { - const { account, abortSignal, log, runtime } = ctx; - + const { account, abortSignal, log, cfg } = ctx; + log?.info(`[qqbot:${account.accountId}] Starting gateway`); await startGateway({ account, abortSignal, + cfg, log, - onMessage: (event) => { - log?.info(`[qqbot:${account.accountId}] Message from ${event.senderId}: ${event.content}`); - // 消息处理会通过 runtime 发送到 moltbot 核心 - runtime.emit?.("message", { - channel: "qqbot", - accountId: account.accountId, - chatType: event.type === "c2c" ? "direct" : "group", - senderId: event.senderId, - senderName: event.senderName, - content: event.content, - messageId: event.messageId, - timestamp: event.timestamp, - channelId: event.channelId, - guildId: event.guildId, - raw: event.raw, - }); - }, - onReady: (data) => { + onReady: () => { log?.info(`[qqbot:${account.accountId}] Gateway ready`); ctx.setStatus({ ...ctx.getStatus(), diff --git a/src/gateway.ts b/src/gateway.ts index bd40dbb..ff5a252 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -1,18 +1,18 @@ import WebSocket from "ws"; import type { ResolvedQQBotAccount, WSPayload, C2CMessageEvent, GuildMessageEvent } from "./types.js"; -import { getAccessToken, getGatewayUrl } from "./api.js"; +import { getAccessToken, getGatewayUrl, sendC2CMessage, sendChannelMessage } from "./api.js"; +import { getQQBotRuntime } from "./runtime.js"; // QQ Bot intents const INTENTS = { PUBLIC_GUILD_MESSAGES: 1 << 30, DIRECT_MESSAGE: 1 << 25, - // C2C 私聊在 PUBLIC_GUILD_MESSAGES 里 }; export interface GatewayContext { account: ResolvedQQBotAccount; abortSignal: AbortSignal; - onMessage: (event: GatewayMessageEvent) => void; + cfg: unknown; onReady?: (data: unknown) => void; onError?: (error: Error) => void; log?: { @@ -22,28 +22,17 @@ export interface GatewayContext { }; } -export interface GatewayMessageEvent { - type: "c2c" | "guild" | "dm"; - senderId: string; - senderName?: string; - content: string; - messageId: string; - timestamp: string; - channelId?: string; - guildId?: string; - raw: unknown; -} - /** * 启动 Gateway WebSocket 连接 */ export async function startGateway(ctx: GatewayContext): Promise { - const { account, abortSignal, onMessage, onReady, onError, log } = ctx; + const { account, abortSignal, cfg, onReady, onError, log } = ctx; if (!account.appId || !account.clientSecret) { throw new Error("QQBot not configured (missing appId or clientSecret)"); } + const pluginRuntime = getQQBotRuntime(); const accessToken = await getAccessToken(account.appId, account.clientSecret); const gatewayUrl = await getGatewayUrl(accessToken); @@ -65,6 +54,121 @@ export async function startGateway(ctx: GatewayContext): Promise { abortSignal.addEventListener("abort", cleanup); + // 处理收到的消息 + const handleMessage = async (event: { + type: "c2c" | "guild" | "dm"; + senderId: string; + senderName?: string; + content: string; + messageId: string; + timestamp: string; + channelId?: string; + guildId?: string; + }) => { + log?.info(`[qqbot:${account.accountId}] Processing message from ${event.senderId}: ${event.content}`); + + pluginRuntime.channel.activity.record({ + channel: "qqbot", + accountId: account.accountId, + direction: "inbound", + }); + + const isGroup = event.type === "guild"; + const peerId = isGroup ? `channel:${event.channelId}` : event.senderId; + + const route = pluginRuntime.channel.routing.resolveAgentRoute({ + cfg, + channel: "qqbot", + accountId: account.accountId, + peer: { + kind: isGroup ? "group" : "dm", + id: peerId, + }, + }); + + const envelopeOptions = pluginRuntime.channel.reply.resolveEnvelopeFormatOptions(cfg); + + const body = pluginRuntime.channel.reply.formatInboundEnvelope({ + channel: "QQBot", + from: event.senderName ?? event.senderId, + timestamp: new Date(event.timestamp).getTime(), + body: event.content, + chatType: isGroup ? "group" : "direct", + sender: { + id: event.senderId, + name: event.senderName, + }, + envelope: envelopeOptions, + }); + + const fromAddress = isGroup + ? `qqbot:channel:${event.channelId}` + : `qqbot:${event.senderId}`; + const toAddress = fromAddress; + + const ctxPayload = pluginRuntime.channel.reply.finalizeInboundContext({ + Body: body, + RawBody: event.content, + CommandBody: event.content, + From: fromAddress, + To: toAddress, + SessionKey: route.sessionKey, + AccountId: route.accountId, + ChatType: isGroup ? "group" : "direct", + SenderId: event.senderId, + SenderName: event.senderName, + Provider: "qqbot", + Surface: "qqbot", + MessageSid: event.messageId, + Timestamp: new Date(event.timestamp).getTime(), + OriginatingChannel: "qqbot", + OriginatingTo: toAddress, + // QQBot 特有字段 + QQChannelId: event.channelId, + QQGuildId: event.guildId, + }); + + // 分发到 AI 系统 + try { + const messagesConfig = pluginRuntime.channel.reply.resolveEffectiveMessagesConfig(cfg, route.agentId); + + await pluginRuntime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({ + ctx: ctxPayload, + cfg, + dispatcherOptions: { + responsePrefix: messagesConfig.responsePrefix, + deliver: async (payload: { text?: string }) => { + const replyText = payload.text ?? ""; + if (!replyText.trim()) return; + + try { + if (event.type === "c2c") { + await sendC2CMessage(accessToken, event.senderId, replyText, event.messageId); + } else if (event.channelId) { + await sendChannelMessage(accessToken, event.channelId, replyText, event.messageId); + } + log?.info(`[qqbot:${account.accountId}] Sent reply`); + + pluginRuntime.channel.activity.record({ + channel: "qqbot", + accountId: account.accountId, + direction: "outbound", + }); + } catch (err) { + log?.error(`[qqbot:${account.accountId}] Send failed: ${err}`); + } + }, + onError: (err: unknown) => { + log?.error(`[qqbot:${account.accountId}] Dispatch error: ${err}`); + }, + }, + replyOptions: {}, + }); + } catch (err) { + log?.error(`[qqbot:${account.accountId}] Message processing failed: ${err}`); + } + }; + ws.on("open", () => { log?.info(`[qqbot:${account.accountId}] WebSocket connected`); }); @@ -105,17 +209,16 @@ export async function startGateway(ctx: GatewayContext): Promise { onReady?.(d); } else if (t === "C2C_MESSAGE_CREATE") { const event = d as C2CMessageEvent; - onMessage({ + await handleMessage({ type: "c2c", senderId: event.author.user_openid, content: event.content, messageId: event.id, timestamp: event.timestamp, - raw: event, }); } else if (t === "AT_MESSAGE_CREATE") { const event = d as GuildMessageEvent; - onMessage({ + await handleMessage({ type: "guild", senderId: event.author.id, senderName: event.author.username, @@ -124,11 +227,10 @@ export async function startGateway(ctx: GatewayContext): Promise { timestamp: event.timestamp, channelId: event.channel_id, guildId: event.guild_id, - raw: event, }); } else if (t === "DIRECT_MESSAGE_CREATE") { const event = d as GuildMessageEvent; - onMessage({ + await handleMessage({ type: "dm", senderId: event.author.id, senderName: event.author.username, @@ -136,7 +238,6 @@ export async function startGateway(ctx: GatewayContext): Promise { messageId: event.id, timestamp: event.timestamp, guildId: event.guild_id, - raw: event, }); } break; diff --git a/src/runtime.ts b/src/runtime.ts new file mode 100644 index 0000000..414e19c --- /dev/null +++ b/src/runtime.ts @@ -0,0 +1,14 @@ +import type { PluginRuntime } from "clawdbot/plugin-sdk"; + +let runtime: PluginRuntime | null = null; + +export function setQQBotRuntime(next: PluginRuntime) { + runtime = next; +} + +export function getQQBotRuntime(): PluginRuntime { + if (!runtime) { + throw new Error("QQBot runtime not initialized"); + } + return runtime; +}