import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import type { ChannelPlugin } from "openclaw/plugin-sdk/core"; // Register the PlatformAdapter before any core/ module is used. import"./bridge/bootstrap.js"; import { getQQBotApprovalCapability } from "./bridge/approval/capability.js"; import { qqbotConfigAdapter, qqbotMeta, qqbotSetupAdapterShared } from "./bridge/config-shared.js"; import {
applyQQBotAccountConfig,
DEFAULT_ACCOUNT_ID,
resolveQQBotAccount,
} from "./bridge/config.js"; import { getQQBotRuntime } from "./bridge/runtime.js"; import { qqbotSetupWizard } from "./bridge/setup/surface.js"; import { qqbotChannelConfigSchema } from "./config-schema.js"; import { loadCredentialBackup, saveCredentialBackup } from "./engine/config/credential-backup.js"; import { clearAccountCredentials } from "./engine/config/credentials.js"; import {
normalizeTarget as coreNormalizeTarget,
looksLikeQQBotTarget,
} from "./engine/messaging/target-parser.js"; // Re-export text helpers from core/.
export { chunkText, TEXT_CHUNK_LIMIT } from "./engine/utils/text-chunk.js"; import type { ResolvedQQBotAccount } from "./types.js";
// Shared promise so concurrent multi-account startups serialize the dynamic // import of the gateway module, avoiding an ESM circular-dependency race.
let _gatewayModulePromise: Promise<typeofimport("./bridge/gateway.js")> | undefined; function loadGatewayModule(): Promise<typeofimport("./bridge/gateway.js")> {
_gatewayModulePromise ??= import("./bridge/gateway.js"); return _gatewayModulePromise;
}
export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
id: "qqbot",
setupWizard: qqbotSetupWizard,
meta: {
...qqbotMeta,
},
capabilities: {
chatTypes: ["direct", "group"],
media: true,
reactions: false,
threads: false,
blockStreaming: true,
},
reload: { configPrefixes: ["channels.qqbot"] },
configSchema: qqbotChannelConfigSchema,
config: {
...qqbotConfigAdapter, /** * Treat an account as configured when either the live config has * credentials OR a recoverable credential backup exists. This mirrors * the standalone plugin and lets the gateway survive a hot upgrade * that wiped openclaw.json mid-flight.
*/
isConfigured: (account: ResolvedQQBotAccount | undefined) => { if (qqbotConfigAdapter.isConfigured(account)) { returntrue;
} if (!account) { returnfalse;
} const backup = loadCredentialBackup(account.accountId); returnBoolean(backup?.appId && backup?.clientSecret);
},
},
setup: {
...qqbotSetupAdapterShared,
},
approvalCapability: getQQBotApprovalCapability(),
messaging: { /** Normalize common QQ Bot target formats into the canonical qqbot:... form. */
normalizeTarget: coreNormalizeTarget,
targetResolver: { /** Return true when the id looks like a QQ Bot target. */
looksLikeId: looksLikeQQBotTarget,
hint: "QQ Bot target format: qqbot:c2c:openid (direct) or qqbot:group:groupid (group)",
},
},
outbound: {
deliveryMode: "direct",
chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
chunkerMode: "markdown",
textChunkLimit: 5000,
shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload, hint }) =>
shouldSuppressLocalQQBotApprovalPrompt({
cfg,
accountId,
payload,
hint,
}),
sendText: async ({ to, text, accountId, replyToId, cfg }) => { // Ensure bridge/gateway.ts module-level registrations (audio adapter factory, // platform adapter, etc.) have executed before engine code runs.
await loadGatewayModule(); const account = resolveQQBotAccount(cfg, accountId); const { sendText } = await import("./engine/messaging/outbound.js"); const result = await sendText({ to, text, accountId, replyToId, account: account as never }); return {
channel: "qqbot" as const,
messageId: result.messageId ?? "",
meta: result.error ? { error: result.error } : undefined,
};
},
sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => { // Same guard as sendText — ensure adapters are registered.
await loadGatewayModule(); const account = resolveQQBotAccount(cfg, accountId); const { sendMedia } = await import("./engine/messaging/outbound.js"); const result = await sendMedia({
to,
text: text ?? "",
mediaUrl: mediaUrl ?? "",
accountId,
replyToId,
account: account as never,
}); return {
channel: "qqbot" as const,
messageId: result.messageId ?? "",
meta: result.error ? { error: result.error } : undefined,
};
},
},
gateway: {
startAccount: async (ctx) => {
let { account, cfg } = ctx; const { abortSignal, log } = ctx;
// Recover credentials from the per-account backup if the live // config is missing appId/secret (e.g. a hot-upgrade wiped // openclaw.json). We only restore when both fields are empty so a // user's intentional clear isn't silently undone. if (!account.appId || !account.clientSecret) { const backup = loadCredentialBackup(account.accountId); if (backup?.appId && backup?.clientSecret) { try { const nextCfg = applyQQBotAccountConfig(cfg, account.accountId, {
appId: backup.appId,
clientSecret: backup.clientSecret,
}); const runtime = getQQBotRuntime(); const configApi = runtime.config as {
writeConfigFile: (cfg: OpenClawConfig) => Promise<void>;
};
await configApi.writeConfigFile(nextCfg);
cfg = nextCfg;
account = resolveQQBotAccount(nextCfg, account.accountId);
log?.info(
`[qqbot:${account.accountId}] Restored credentials from backup (appId=${account.appId})`,
);
} catch (err) {
log?.error(
`[qqbot:${account.accountId}] Failed to restore credentials from backup: ${err instanceof Error ? err.message : String(err)}`,
);
}
}
}
// Serialize the dynamic import so concurrent multi-account startups // do not hit an ESM circular-dependency race where the gateway chunk's // transitive imports have not finished evaluating yet. const { startGateway } = await loadGatewayModule();
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.