openclaw/extensions/qqbot/index.ts

114 lines
4.5 KiB
TypeScript

import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
import { qqbotPlugin } from "./src/channel.js";
import { resolveQQBotAccount } from "./src/config.js";
import { sendDocument, type MediaTargetContext } from "./src/outbound.js";
import { setQQBotRuntime } from "./src/runtime.js";
import { getFrameworkCommands } from "./src/slash-commands.js";
import { registerChannelTool } from "./src/tools/channel.js";
import { registerRemindTool } from "./src/tools/remind.js";
export { qqbotPlugin } from "./src/channel.js";
export { setQQBotRuntime, getQQBotRuntime } from "./src/runtime.js";
export default defineChannelPluginEntry({
id: "qqbot",
name: "QQ Bot",
description: "QQ Bot channel plugin",
plugin: qqbotPlugin as ChannelPlugin,
setRuntime: setQQBotRuntime,
registerFull(api: OpenClawPluginApi) {
registerChannelTool(api);
registerRemindTool(api);
// Register all requireAuth:true slash commands with the framework so that
// resolveCommandAuthorization() applies commands.allowFrom.qqbot precedence
// and qqbot: prefix normalization before any handler runs.
for (const cmd of getFrameworkCommands()) {
api.registerCommand({
name: cmd.name,
description: cmd.description,
requireAuth: true,
acceptsArgs: true,
handler: async (ctx) => {
// Derive the QQBot message type from ctx.from so that handlers that
// inspect SlashCommandContext.type get the correct value.
// ctx.from format: "qqbot:<type>:<id>" e.g. "qqbot:c2c:<senderId>"
const fromStripped = (ctx.from ?? "").replace(/^qqbot:/i, "");
const rawMsgType = fromStripped.split(":")[0] ?? "c2c";
const msgType: "c2c" | "guild" | "dm" | "group" =
rawMsgType === "group"
? "group"
: rawMsgType === "channel"
? "guild"
: rawMsgType === "dm"
? "dm"
: "c2c";
// Parse target for file sends (same from string).
const colonIdx = fromStripped.indexOf(":");
const targetId = colonIdx !== -1 ? fromStripped.slice(colonIdx + 1) : fromStripped;
const targetType: "c2c" | "group" | "channel" | "dm" =
rawMsgType === "group"
? "group"
: rawMsgType === "channel"
? "channel"
: rawMsgType === "dm"
? "dm"
: "c2c";
// Build a minimal SlashCommandContext from the framework PluginCommandContext.
// commandAuthorized is always true here because the framework has already
// verified the sender via resolveCommandAuthorization().
const slashCtx = {
type: msgType,
senderId: ctx.senderId ?? "",
messageId: "",
eventTimestamp: new Date().toISOString(),
receivedAt: Date.now(),
rawContent: `/${cmd.name}${ctx.args ? ` ${ctx.args}` : ""}`,
args: ctx.args ?? "",
accountId: ctx.accountId ?? "default",
// appId is not available from PluginCommandContext directly; handlers
// that need it should call resolveQQBotAccount(ctx.config, ctx.accountId).
appId: "",
commandAuthorized: true,
queueSnapshot: {
totalPending: 0,
activeUsers: 0,
maxConcurrentUsers: 10,
senderPending: 0,
},
};
const result = await cmd.handler(slashCtx);
// Plain-text result.
if (typeof result === "string") {
return { text: result };
}
// File result: send the file attachment via QQ API, return text summary.
if (result && "filePath" in result) {
try {
const account = resolveQQBotAccount(ctx.config, ctx.accountId ?? undefined);
const mediaCtx: MediaTargetContext = {
targetType,
targetId,
account,
logPrefix: `[qqbot:${account.accountId}]`,
};
await sendDocument(mediaCtx, result.filePath);
} catch {
// File send failed; the text summary is still returned below.
}
return { text: result.text };
}
return { text: "⚠️ 命令返回了意外结果。" };
},
});
}
},
});