mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-16 03:10:47 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
209
openclaw/extensions/qqbot/index.ts
Normal file
209
openclaw/extensions/qqbot/index.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import {
|
||||
defineBundledChannelEntry,
|
||||
loadBundledEntryExportSync,
|
||||
type OpenClawPluginApi,
|
||||
type PluginCommandContext,
|
||||
} from "openclaw/plugin-sdk/channel-entry-contract";
|
||||
|
||||
type QQBotAccount = {
|
||||
accountId: string;
|
||||
appId: string;
|
||||
config: unknown;
|
||||
};
|
||||
|
||||
type MediaTargetContext = {
|
||||
targetType: "c2c" | "group" | "channel" | "dm";
|
||||
targetId: string;
|
||||
account: QQBotAccount;
|
||||
logPrefix: string;
|
||||
};
|
||||
type SendDocumentOptions = {
|
||||
allowQQBotDataDownloads?: boolean;
|
||||
};
|
||||
|
||||
type QQBotFrameworkCommandResult =
|
||||
| string
|
||||
| {
|
||||
text: string;
|
||||
filePath?: string;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
type QQBotFrameworkCommand = {
|
||||
name: string;
|
||||
description: string;
|
||||
handler: (ctx: Record<string, unknown>) => Promise<QQBotFrameworkCommandResult>;
|
||||
};
|
||||
|
||||
function resolveQQBotAccount(config: unknown, accountId?: string): QQBotAccount {
|
||||
const resolve = loadBundledEntryExportSync<(config: unknown, accountId?: string) => QQBotAccount>(
|
||||
import.meta.url,
|
||||
{
|
||||
specifier: "./api.js",
|
||||
exportName: "resolveQQBotAccount",
|
||||
},
|
||||
);
|
||||
return resolve(config, accountId);
|
||||
}
|
||||
|
||||
function sendDocument(
|
||||
context: MediaTargetContext,
|
||||
filePath: string,
|
||||
options?: SendDocumentOptions,
|
||||
) {
|
||||
const send = loadBundledEntryExportSync<
|
||||
(
|
||||
context: MediaTargetContext,
|
||||
filePath: string,
|
||||
options?: SendDocumentOptions,
|
||||
) => Promise<unknown>
|
||||
>(import.meta.url, {
|
||||
specifier: "./api.js",
|
||||
exportName: "sendDocument",
|
||||
});
|
||||
return send(context, filePath, options);
|
||||
}
|
||||
|
||||
function getFrameworkCommands(): QQBotFrameworkCommand[] {
|
||||
const getCommands = loadBundledEntryExportSync<() => QQBotFrameworkCommand[]>(import.meta.url, {
|
||||
specifier: "./api.js",
|
||||
exportName: "getFrameworkCommands",
|
||||
});
|
||||
return getCommands();
|
||||
}
|
||||
|
||||
function registerChannelTool(api: OpenClawPluginApi): void {
|
||||
const register = loadBundledEntryExportSync<(api: OpenClawPluginApi) => void>(import.meta.url, {
|
||||
specifier: "./api.js",
|
||||
exportName: "registerChannelTool",
|
||||
});
|
||||
register(api);
|
||||
}
|
||||
|
||||
function registerRemindTool(api: OpenClawPluginApi): void {
|
||||
const register = loadBundledEntryExportSync<(api: OpenClawPluginApi) => void>(import.meta.url, {
|
||||
specifier: "./api.js",
|
||||
exportName: "registerRemindTool",
|
||||
});
|
||||
register(api);
|
||||
}
|
||||
|
||||
export default defineBundledChannelEntry({
|
||||
id: "qqbot",
|
||||
name: "QQ Bot",
|
||||
description: "QQ Bot channel plugin",
|
||||
importMetaUrl: import.meta.url,
|
||||
plugin: {
|
||||
specifier: "./api.js",
|
||||
exportName: "qqbotPlugin",
|
||||
},
|
||||
runtime: {
|
||||
specifier: "./runtime-api.js",
|
||||
exportName: "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: PluginCommandContext) => {
|
||||
// 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";
|
||||
const account = resolveQQBotAccount(ctx.config, ctx.accountId ?? undefined);
|
||||
|
||||
// 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: account.accountId,
|
||||
// appId is not available from PluginCommandContext directly; handlers
|
||||
// that need it should call resolveQQBotAccount(ctx.config, ctx.accountId).
|
||||
appId: account.appId,
|
||||
accountConfig: account.config,
|
||||
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 && typeof result === "object" && "filePath" in result) {
|
||||
try {
|
||||
const mediaCtx: MediaTargetContext = {
|
||||
targetType,
|
||||
targetId,
|
||||
account,
|
||||
logPrefix: `[qqbot:${account.accountId}]`,
|
||||
};
|
||||
await sendDocument(mediaCtx, String(result.filePath), {
|
||||
allowQQBotDataDownloads: true,
|
||||
});
|
||||
} catch {
|
||||
// File send failed; the text summary is still returned below.
|
||||
}
|
||||
return { text: result.text };
|
||||
}
|
||||
|
||||
return {
|
||||
text:
|
||||
result &&
|
||||
typeof result === "object" &&
|
||||
"text" in result &&
|
||||
typeof result.text === "string"
|
||||
? result.text
|
||||
: "⚠️ 命令返回了意外结果。",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue