Skip to content

Commit 5e72e39

Browse files
cxyhhhhhzkd8907neilhwangsliverp
authored
feat(qqbot): extract self-contained engine/ architecture with QR-code onboarding, approval handling (#67960)
* feat(qqbot): add core architecture modules * feat(qqbot): extract engine modules with DI adapters * refactor(qqbot): remove plugin-level TTS, delegate to framework Remove qqbot's internal TTS implementation and unify voice synthesis through the framework's global TTS provider registry. - Delete engine/gateway/tts-config.ts (plugin-specific TTS config) - Simplify TTSProvider interface to textToSpeech + audioFileToSilkBase64 - Remove dual-strategy TTS in handleAudioPayload (plugin + global fallback) - Strip QQBotTtsSchema from config-schema, plugin.json, and tests - Remove TTS diagnostics logging and hasTTS system prompt from gateway - Delete ~260 lines of TTS code from utils/audio-convert.ts Made-with: Cursor * feat(qqbot): extract shared engine modules for config, tools, and audio Add engine-layer modules that are self-contained and portable across both the built-in and standalone qqbot packages: - engine/config: account resolution helpers, field readers - engine/tools: channel API proxy, remind scheduling logic - engine/utils: audio format conversion, duration/error formatting, debug logging Consolidate duplicate utility functions across the codebase: - Merge debug-log.ts into log.ts - Merge error-format.ts into format.ts with full .cause chain support - Unify normalizeLowercase/readNumber/readBoolean/readStringMap into string-normalize.ts, removing private copies in resolve.ts, remind-logic.ts, and audio-convert.ts - Remove dead formatDuration export from audio-convert.ts - Delete unused config/schema.ts and config/helpers.ts Made-with: Cursor * refactor(qqbot): streamline account configuration and credential management Refactor the QQBot account configuration logic by consolidating credential management into dedicated engine modules. Key changes include: - Migrate credential clearing and validation logic to engine/config/credentials.ts. - Simplify setup input validation and application in engine/config/setup-logic.ts. - Enhance account resolution and configuration application in engine/config/resolve.ts. - Update channel and messaging logic to utilize the new credential management functions. This refactor improves code maintainability and clarity by separating concerns and reducing duplication across the codebase. * feat(qqbot): simplify api architecture * feat: 支持扫码绑定QQ机器人 * feat(qqbot): refactor gateway into inbound pipeline + outbound dispatch - Extract handleMessage (620 lines) into three modules: - inbound-context.ts: InboundContext type definition - inbound-pipeline.ts: buildInboundContext() - outbound-dispatch.ts: dispatchOutbound() - gateway.ts handleMessage reduced to ~35 line shell - Unify parseRefIndices: support both ext prefix formats + MSG_TYPE_QUOTE - Add ref/format-message-ref.ts for cache-miss quote formatting - Remove [QQBot] to= from agentBody, use GroupSystemPrompt instead - QueuedMessage: add msgType/msgElements for quote messages * fix(qqbot): fix markdownSupport loss + dynamic User-Agent Root cause: setOpenClawVersion() called _ensureInitialized(true) which cleared _appRegistry, destroying the MessageApi instance created by initApiConfig() with markdownSupport=true. Subsequent block deliver calls created a default markdownSupport=false instance, causing: 1. Markdown messages sent as plain text (msg_type=0 instead of 2) 2. message_reference incorrectly added (only suppressed in MD mode) Fix: ApiClient and TokenManager now accept userAgent as string | (() => string). sender.ts passes the buildUserAgent function reference, so UA changes propagate automatically on next request without rebuilding any objects. - ApiClient: userAgent -> resolveUserAgent getter, called per-request - TokenManager: same pattern - types.ts: ApiClientConfig.userAgent supports string | (() => string) - sender.ts: remove force re-init + _rebuildAppRegistry hack - initSender/setOpenClawVersion only update version variables - _ensureInitialized creates singletons once, never destroys them - _appRegistry is never cleared -> markdownSupport always preserved - runtime.ts: inject framework version via setOpenClawVersion(runtime.version) - gateway.ts: pass openclawVersion to initSender + registerPluginVersion - slash-commands-impl.ts: remove fragile require("../package.json") * feat(qqbot): implement native approval handling and configuration Add a new approval handling system for QQBot that integrates with the existing framework. Key features include: - Introduce `approval-handler.runtime.ts` for managing approval requests via QQ messages with inline keyboard support. - Create `approval-native.ts` as the entry point for QQBot's approval capability, allowing for simplified approval processes without explicit approver lists. - Implement configuration schema for exec approvals, enabling fine-grained control over who can approve requests. - Enhance messaging and interaction handling to support approval decisions through button interactions. This implementation streamlines the approval process, making it more user-friendly and efficient for QQBot users. * refactor(qqbot): enhance error handling across API and messaging modules This update introduces a centralized error formatting utility, `formatErrorMessage`, to improve consistency in error logging throughout the QQBot codebase. Key changes include: - Integration of `formatErrorMessage` in various API client, messaging, and gateway modules to standardize error messages. - Replacement of direct error message handling with the new utility to enhance readability and maintainability. These improvements streamline error reporting and provide clearer insights into issues encountered during operation. * refactor(qqbot): enhance API and messaging structure with type improvements This update refines the API and messaging modules by introducing type enhancements and restructuring function signatures for better clarity and maintainability. Key changes include: - Updated import statements to streamline type usage in and . - Refactored message sending functions to accept options objects, improving readability and flexibility. - Introduced a new method in to facilitate external message-sent notifications. - Enhanced error handling in the retry mechanism to ensure more robust behavior. These modifications aim to improve the overall code quality and developer experience within the QQBot framework. * feat: 优化文案 * refactor(qqbot): unify Logger interfaces + eliminate P0 code smells Logger unification (17 files): - Introduce single EngineLogger interface in engine/types.ts { info, error, warn?, debug? } - Delete 5 fragmented Logger interfaces: GatewayLogger, ReconnectLogger, MessageRefLogger, PathLogger, SenderLogger - Replace all references across engine/ to use EngineLogger directly P0 code smell fixes (sender.ts + messages.ts + outbound-dispatch.ts): - messages.ts: add public notifyMessageSent() method on MessageApi, replacing 8x 'as unknown as { messageSentHook }' private field hack - sender.ts: extract notifyMediaHook() helper, deduplicate 4 media send functions (sendImage/sendVoice/sendVideo/sendFile) - sender.ts: replace magic numbers 1/2/3/4 with MediaFileType enum - sender.ts: remove 4 redundant 'as MessageResponse' type assertions - outbound-dispatch.ts: remove 5 unnecessary 'as never' casts * feat(qqbot): add /bot-clear-storage command + consolidate utils/types into engine/ /bot-clear-storage (slash-commands-impl.ts): - Migrate from standalone version, aligned with its two-step flow: 1. No args: scan ~/.openclaw/media/qqbot/downloads/{appId}/ and display file list with confirmation button 2. --force: delete files + removeEmptyDirs cleanup - C2C only (group chat returns hint) - bot-help: exclude bot-upgrade and bot-clear-storage in group listings Consolidate into engine/: - Delete src/utils/audio-convert.ts (pure re-export shell, zero consumers) - Move 5 test files from src/utils/ to src/engine/utils/ (fix import paths) - Move src/types/silk-wasm.d.ts to src/engine/types/ - Remove empty src/utils/ and src/types/ directories * refactor(qqbot): restructure API and bridge components for improved modularity This update enhances the QQBot framework by reorganizing the API and bridge components, promoting better modularity and maintainability. Key changes include: - Refactored import paths to streamline access to bridge tools and configurations. - Introduced new bridge files for channel entry, runtime, and approval capabilities, centralizing related functionalities. - Updated existing functions to utilize the new bridge structure, ensuring consistency across the codebase. - Removed deprecated functions and types, simplifying the overall architecture. These modifications aim to improve code clarity and facilitate future development within the QQBot ecosystem. * refactor(qqbot): standardize engine log levels and unify log tag prefix - Rename client.ts to api-client.ts to match ApiClient class name - Downgrade ~60 non-critical info logs to debug level across 12 files (token request/response, HTTP request/response, session restore, media tag detection, image classification, quote detection, attachment download/transcode, retry attempts, etc.) - Unify log tag prefix to [qqbot:xxx] format across all engine modules ([core-api] -> [qqbot:api], [token:x] -> [qqbot:token:x], [retry] -> [qqbot:retry], [messages] -> [qqbot:messages], [sender:x] -> [qqbot:x]) - Remove unnecessary reqTs timestamp from api-client.ts log output - Add dispatch event debug log in gateway-connection.ts - Merge sendProactiveMessage into sendText, remove dead code (sendProactiveText import, getRefIdx, QQMessageResult type) - Narrow allow-from.ts type from unknown[] to Array<string | number> * refactor(qqbot): move interaction handler from bridge to engine - Move onInteraction approval handler into engine/gateway.ts as createApprovalInteractionHandler(), eliminating the callback indirection through CoreGatewayContext - Remove onInteraction from CoreGatewayContext interface and its unused InteractionEvent import from gateway/types.ts - Remove getPlatformAdapter, parseApprovalButtonData and InteractionEvent imports from bridge/gateway.ts * refactor(qqbot): route bridge and sender logs through framework logger - Add bridge/logger.ts as a shared logger holder for bridge-layer modules, injected with ctx.log during gateway startup - Replace all console.log/console.error in bridge/ with getBridgeLogger() calls (approval, bootstrap, tools) - Restore framework logger support in sender.ts via initSender() so API-layer logs flow through OpenClaw log system - Remove all direct debugLog/debugError imports from bridge/ * feat(qqbot): per-account isolated resource stack + multi-account logger - sender.ts: global singletons (ApiClient/TokenManager/MediaApi) -> per-account AccountContext - Add _accountRegistry: Map<appId, AccountContext> - Each account owns independent client/tokenMgr/mediaApi/messageApi/logger - registerAccount() atomically sets up all resources - resolveAccount() routes to correct resource stack by appId - Remove _sharedLogger/_loggerRegistry/_appRegistry and old structures - bridge/gateway.ts: createAccountLogger() with auto [accountId] prefix - registerAccount() merges logger + markdownSupport + full API resources - engine-wide: remove ~60 manual [qqbot:${accountId}] log prefixes - Prefixes now auto-injected by per-account logger - Remove prefix/logPrefix parameter chains (outbound/outbound-deliver/typing-keepalive etc) * feat(qqbot): completes fallback path for approval with multi-account isolation When the execApprovals are not configured, multiple QQBot accounts' handlers will attempt to deliver the same approval message. The openid is account-level, and cross-account delivery will trigger a QQ Bot API 500 error. - Add account ownership verification in the fallback shouldHandle: Only match the account's handler when the request includes turnSourceAccountId; if unbound, delivery is only permitted when the number of enabled+secret accounts is ≤1. - Consolidate account ownership determination into the unified export `matchesQQBotApprovalAccount` in `exec-approvals.ts`, with both capability and native runtime paths sharing the same logic to eliminate redundancy. * feat(qqbot): optimize permission validation strategy * feat(qqbot): show plugin version in /bot-version and /bot-help Align /bot-version output with the standalone openclaw-qqbot build so users see both the QQBot plugin version and the OpenClaw framework version. Append the plugin version as a footer in /bot-help as well, matching the standalone UX. Also fix the plugin version lookup that previously rendered as 'vunknown': the old code used a hardcoded '../../package.json' relative path which resolved to 'src/package.json' (non-existent) when executed from raw sources, so the require threw and the default 'unknown' value was retained. The same broken value also leaked into the QQ Bot API User-Agent header. Replace the hardcoded path with a dedicated helper (bridge/plugin-version.ts) that walks up the directory tree from import.meta.url and validates the manifest's name field (@openclaw/qqbot) to avoid misreading the monorepo root package.json. Covered by 6 unit tests. * feat(qqbot): trust shared ~/.openclaw/media root for payload files Add getOpenClawMediaDir() and include it alongside getQQBotMediaDir() in the allowed roots of resolveQQBotPayloadLocalFilePath, so framework-produced attachments under sibling directories (e.g. media/outbound/ written by saveMediaBuffer) are trusted by auto-routed sends without triggering the path-outside-storage guard. Covered by a new test case that verifies files under ~/.openclaw/media/outbound/ resolve successfully. * fix(qqbot): ensure PlatformAdapter is registered before approval delivery After the framework centralized approval handler bootstrap (#62135), the native approval handler is spawned by the framework layer outside the qqbot gateway startAccount context. This means channel.ts's side-effect `import "./bridge/bootstrap.js"` may not have run, leaving PlatformAdapter unregistered when deliverPending calls resolveQQBotAccount -> getPlatformAdapter(). Extract ensurePlatformAdapter() from bootstrap.ts as an idempotent, re-entrant helper and call it in both capability.ts (load callback) and handler-runtime.ts (deliverPending entry) to guarantee the adapter is available regardless of initialization order. * fix(qqbot): add lazy factory for PlatformAdapter to eliminate import-order dependency The bundler splits qqbot code into multiple chunks where the adapter singleton and its consumers may live in different modules. When a consumer chunk evaluates before the bootstrap side-effect chunk, getPlatformAdapter() throws because the singleton is still null. Introduce registerPlatformAdapterFactory() in adapter/index.ts so getPlatformAdapter() can auto-initialize the adapter on first access. bootstrap.ts registers the factory at module evaluation time alongside the existing eager registration path. Also add error logging in downloadFile's catch block to surface fetch failures. * feat(qqbot): add /bot-approve slash command for exec approval config management Add /bot-approve command to the built-in QQBot plugin, ported from the standalone openclaw-qqbot implementation. This command allows users to manage tools.exec.security and tools.exec.ask settings directly from QQ. Supported sub-commands: /bot-approve on - allowlist + on-miss (recommended) /bot-approve off - full + off (no approval) /bot-approve always - allowlist + always (strict mode) /bot-approve reset - remove overrides, restore framework defaults /bot-approve status - show current security/ask values The runtime config API is injected via registerApproveRuntimeGetter() following the existing dependency injection pattern used by registerVersionResolver() and registerPluginVersion(). * fix(qqbot): ACK INTERACTION_CREATE events before processing approval buttons Send PUT /interactions/{id} immediately upon receiving any INTERACTION_CREATE event to prevent QQ from showing a timeout error to the user. The ACK is fire-and-forget and does not block subsequent approval button resolution. Also resolve merge conflict in pnpm-lock.yaml (keep @tencent-connect/[email protected] and newer @thi.ng/[email protected]). * feat(qqbot): enhance reminder functionality with delivery context and credential backup This update improves the QQBot reminder system by introducing a delivery context for reminders, allowing for more flexible target resolution. Key changes include: - Updated reminder logic to utilize a delivery envelope, ensuring that reminders are sent with the correct context. - Implemented credential backup and recovery mechanisms to prevent loss of appId and clientSecret during hot upgrades. - Added tests for credential backup functionality and admin resolver to ensure reliability. - Enhanced the remind tool to automatically resolve the target from the current conversation context when not explicitly provided. These enhancements aim to improve the user experience and reliability of the reminder feature within the QQBot framework. * fix(qqbot): ensure PlatformAdapter is registered before gateway message processing Call ensurePlatformAdapter() at the start of bridge/gateway.ts's startGateway() to guarantee the adapter is available when engine code (e.g. downloadFile in file-utils.ts) calls getPlatformAdapter(). When the bundler splits code into separate chunks, bootstrap.ts's module-level side-effect registration may not have executed yet by the time the gateway processes its first inbound attachment download. Also fix the TS2339 error in registerApproveRuntimeGetter by using getQQBotRuntime() (full PluginRuntime with config) instead of getQQBotRuntimeForEngine() (GatewayPluginRuntime subset without config). * fix(qqbot): make isAudioFile safe when OutboundAudioAdapter is not registered sendMedia() calls isAudioFile() as part of its media-type dispatch logic before any actual audio processing. When the audio adapter is not yet registered (e.g. framework tool calls sendMedia before gateway startup), isAudioFile() would throw 'OutboundAudioAdapter not registered' even for non-audio files like images. Wrap the getAudio() call in isAudioFile() with try/catch to return false when the adapter is unavailable, allowing non-audio media sends to proceed normally. * refactor(qqbot): remove plugin startup/upgrade greeting pipeline Drop the startup / upgrade greeting feature that was folded into the previous reminder + credential-backup commit. The pipeline has proven unnecessary for the fused build and its supporting admin-resolver scaffolding has no other consumers, so both are removed wholesale. - Delete engine/session/startup-greeting.ts and its tests: the first-launch "soul online" / "updated to vX.Y.Z" messages, the per-(accountId, appId) startup marker, the failure cooldown, and the legacy startup-marker.json migration path are all gone. - Delete engine/session/admin-resolver.ts and its tests: admin openid persistence/resolution, upgrade-greeting-target load/clear and the sendStartupGreetings dispatcher only ever served the greeting flow and were not referenced elsewhere. - channel.ts: drop the sendStartupGreetings import and the READY / RESUMED hooks that triggered greetings; credential-backup snapshots stay untouched. - engine/utils/data-paths.ts: remove getAdminMarkerFile / getLegacyAdminMarkerFile / getUpgradeGreetingTargetFile / getStartupMarkerFile / getLegacyStartupMarkerFile along with the now-stale module docblock sections. Credential-backup helpers and safeName are preserved. Net -655 LOC across 6 files. tsc --noEmit passes on extensions/qqbot/tsconfig.json and no references to the removed symbols remain in the workspace. * fix(qqbot): resolve test failures in extension batch, contracts and bundled runtime deps - bootstrap: replace sync require() with static imports for secret-input and temp-path so vitest resolve.alias works correctly (require bypasses vitest aliases causing Cannot find module errors) - format: handle null/undefined in formatErrorMessage before JSON.stringify since JSON.stringify(undefined) returns JS undefined, not a string - gateway/types: reword comment to avoid triggering the channel-import guardrail regex that forbids quoted openclaw/plugin-sdk references - package.json: mirror @tencent-connect/qqbot-connector ^1.1.0 in root dependencies as required by bundled plugin runtime dependency checks * chore: revert non-qqbot changes to align with upstream main Revert modifications to src/agents/system-prompt, src/auto-reply/reply/dispatch-from-config, and src/canvas-host/a2ui build artifacts that were inadvertently included in the qqbot feature branch. Also fix .gitignore Core/ pattern to match subdirectories. * fix(qqbot): remove unused logUnsupportedStructuredMediaTarget after API simplification * fix(qqbot): restore channel-plugin-api.ts for bundled plugin surface convention * fix(qqbot): update CI lint allowlists for restructured engine paths - Update raw fetch() allowlist in check-no-raw-channel-fetch.mjs to reflect engine/ directory restructure (src/api.ts → src/engine/api/api-client.ts, etc.) - Remove stale qqbot allowlist entry for deleted src/utils/audio-convert.ts * fix(qqbot): eliminate os.tmpdir() in engine layer via adapter injection - Make hasPlatformAdapter() also check for registered factory, so adapter is always discoverable once bootstrap has run - Remove os.tmpdir() fallbacks in platform.ts getHomeDir()/getTempDir(), delegate entirely to PlatformAdapter.getTempDir() which calls resolvePreferredOpenClawTmpDir() under the hood - Keeps engine/ layer free of openclaw/plugin-sdk imports * chore(qqbot): update CHANGELOG for engine architecture refactor (#67960) (thanks @cxyhhhhh) --------- Co-authored-by: Bobby <[email protected]> Co-authored-by: neilhwang <[email protected]> Co-authored-by: sliverp <[email protected]>
1 parent 38aaa23 commit 5e72e39

149 files changed

Lines changed: 14878 additions & 10006 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
1010
- Channels/preview streaming: stream tool-progress updates into live preview edits for Discord, Slack, and Telegram so in-flight replies show incremental tool state in the same preview message before finalization. (#69611) Thanks @thewilloftheshadow.
1111
- Ollama/onboard: populate the cloud-only model list from `ollama.com/api/tags` so `openclaw onboard` reflects the live cloud catalog instead of a static three-model seed; cap the discovered list at 500 and fall back to the previous hardcoded suggestions when ollama.com is unreachable or returns no models. (#68463) Thanks @BruceMacD.
1212
- Matrix/startup: narrow Matrix runtime registration and defer setup/doctor surfaces so cold plugin registration spends about 1.8s less in `setChannelRuntime`. (#69782) Thanks @gumadeiras.
13+
- QQBot: extract a self-contained `engine/` architecture with QR-code onboarding, native approval handling via `/bot-approve`, per-account isolated resource stacks and multi-account logger, credential backup/restore, shared `~/.openclaw/media` payload root, and unified API/bridge/gateway modules. (#67960) Thanks @cxyhhhhh.
1314

1415
### Fixes
1516

extensions/qqbot/api.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
export { qqbotPlugin } from "./src/channel.js";
22
export { qqbotSetupPlugin } from "./src/channel.setup.js";
3-
export { getFrameworkCommands } from "./src/slash-commands.js";
4-
export { registerChannelTool } from "./src/tools/channel.js";
5-
export { registerRemindTool } from "./src/tools/remind.js";
3+
export { getFrameworkCommands } from "./src/engine/commands/slash-commands-impl.js";
4+
export { registerChannelTool } from "./src/bridge/tools/channel.js";
5+
export { registerRemindTool } from "./src/bridge/tools/remind.js";
6+
export { registerQQBotTools } from "./src/bridge/tools/index.js";
7+
export { registerQQBotFull } from "./src/bridge/channel-entry.js";
68
export * from "./src/types.js";
7-
export * from "./src/config.js";
8-
export * from "./src/outbound.js";
9-
export * from "./src/proactive.js";
9+
export * from "./src/bridge/config.js";
10+
export * from "./src/engine/messaging/outbound.js";

extensions/qqbot/index.ts

Lines changed: 3 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -2,89 +2,12 @@ import {
22
defineBundledChannelEntry,
33
loadBundledEntryExportSync,
44
type OpenClawPluginApi,
5-
type PluginCommandContext,
65
} from "openclaw/plugin-sdk/channel-entry-contract";
76

8-
type QQBotAccount = {
9-
accountId: string;
10-
appId: string;
11-
config: unknown;
12-
};
13-
14-
type MediaTargetContext = {
15-
targetType: "c2c" | "group" | "channel" | "dm";
16-
targetId: string;
17-
account: QQBotAccount;
18-
logPrefix: string;
19-
};
20-
type SendDocumentOptions = {
21-
allowQQBotDataDownloads?: boolean;
22-
};
23-
24-
type QQBotFrameworkCommandResult =
25-
| string
26-
| {
27-
text: string;
28-
filePath?: string;
29-
}
30-
| null
31-
| undefined;
32-
33-
type QQBotFrameworkCommand = {
34-
name: string;
35-
description: string;
36-
handler: (ctx: Record<string, unknown>) => Promise<QQBotFrameworkCommandResult>;
37-
};
38-
39-
function resolveQQBotAccount(config: unknown, accountId?: string): QQBotAccount {
40-
const resolve = loadBundledEntryExportSync<(config: unknown, accountId?: string) => QQBotAccount>(
41-
import.meta.url,
42-
{
43-
specifier: "./api.js",
44-
exportName: "resolveQQBotAccount",
45-
},
46-
);
47-
return resolve(config, accountId);
48-
}
49-
50-
function sendDocument(
51-
context: MediaTargetContext,
52-
filePath: string,
53-
options?: SendDocumentOptions,
54-
) {
55-
const send = loadBundledEntryExportSync<
56-
(
57-
context: MediaTargetContext,
58-
filePath: string,
59-
options?: SendDocumentOptions,
60-
) => Promise<unknown>
61-
>(import.meta.url, {
62-
specifier: "./api.js",
63-
exportName: "sendDocument",
64-
});
65-
return send(context, filePath, options);
66-
}
67-
68-
function getFrameworkCommands(): QQBotFrameworkCommand[] {
69-
const getCommands = loadBundledEntryExportSync<() => QQBotFrameworkCommand[]>(import.meta.url, {
70-
specifier: "./api.js",
71-
exportName: "getFrameworkCommands",
72-
});
73-
return getCommands();
74-
}
75-
76-
function registerChannelTool(api: OpenClawPluginApi): void {
7+
function registerQQBotFull(api: OpenClawPluginApi): void {
778
const register = loadBundledEntryExportSync<(api: OpenClawPluginApi) => void>(import.meta.url, {
789
specifier: "./api.js",
79-
exportName: "registerChannelTool",
80-
});
81-
register(api);
82-
}
83-
84-
function registerRemindTool(api: OpenClawPluginApi): void {
85-
const register = loadBundledEntryExportSync<(api: OpenClawPluginApi) => void>(import.meta.url, {
86-
specifier: "./api.js",
87-
exportName: "registerRemindTool",
10+
exportName: "registerQQBotFull",
8811
});
8912
register(api);
9013
}
@@ -102,108 +25,5 @@ export default defineBundledChannelEntry({
10225
specifier: "./runtime-api.js",
10326
exportName: "setQQBotRuntime",
10427
},
105-
registerFull(api: OpenClawPluginApi) {
106-
registerChannelTool(api);
107-
registerRemindTool(api);
108-
109-
// Register all requireAuth:true slash commands with the framework so that
110-
// resolveCommandAuthorization() applies commands.allowFrom.qqbot precedence
111-
// and qqbot: prefix normalization before any handler runs.
112-
for (const cmd of getFrameworkCommands()) {
113-
api.registerCommand({
114-
name: cmd.name,
115-
description: cmd.description,
116-
requireAuth: true,
117-
acceptsArgs: true,
118-
handler: async (ctx: PluginCommandContext) => {
119-
// Derive the QQBot message type from ctx.from so that handlers that
120-
// inspect SlashCommandContext.type get the correct value.
121-
// ctx.from format: "qqbot:<type>:<id>" e.g. "qqbot:c2c:<senderId>"
122-
const fromStripped = (ctx.from ?? "").replace(/^qqbot:/i, "");
123-
const rawMsgType = fromStripped.split(":")[0] ?? "c2c";
124-
const msgType: "c2c" | "guild" | "dm" | "group" =
125-
rawMsgType === "group"
126-
? "group"
127-
: rawMsgType === "channel"
128-
? "guild"
129-
: rawMsgType === "dm"
130-
? "dm"
131-
: "c2c";
132-
133-
// Parse target for file sends (same from string).
134-
const colonIdx = fromStripped.indexOf(":");
135-
const targetId = colonIdx !== -1 ? fromStripped.slice(colonIdx + 1) : fromStripped;
136-
const targetType: "c2c" | "group" | "channel" | "dm" =
137-
rawMsgType === "group"
138-
? "group"
139-
: rawMsgType === "channel"
140-
? "channel"
141-
: rawMsgType === "dm"
142-
? "dm"
143-
: "c2c";
144-
const account = resolveQQBotAccount(ctx.config, ctx.accountId ?? undefined);
145-
146-
// Build a minimal SlashCommandContext from the framework PluginCommandContext.
147-
// commandAuthorized is always true here because the framework has already
148-
// verified the sender via resolveCommandAuthorization().
149-
const slashCtx = {
150-
type: msgType,
151-
senderId: ctx.senderId ?? "",
152-
messageId: "",
153-
eventTimestamp: new Date().toISOString(),
154-
receivedAt: Date.now(),
155-
rawContent: `/${cmd.name}${ctx.args ? ` ${ctx.args}` : ""}`,
156-
args: ctx.args ?? "",
157-
accountId: account.accountId,
158-
// appId is not available from PluginCommandContext directly; handlers
159-
// that need it should call resolveQQBotAccount(ctx.config, ctx.accountId).
160-
appId: account.appId,
161-
accountConfig: account.config,
162-
commandAuthorized: true,
163-
queueSnapshot: {
164-
totalPending: 0,
165-
activeUsers: 0,
166-
maxConcurrentUsers: 10,
167-
senderPending: 0,
168-
},
169-
};
170-
171-
const result = await cmd.handler(slashCtx);
172-
173-
// Plain-text result.
174-
if (typeof result === "string") {
175-
return { text: result };
176-
}
177-
178-
// File result: send the file attachment via QQ API, return text summary.
179-
if (result && typeof result === "object" && "filePath" in result) {
180-
try {
181-
const mediaCtx: MediaTargetContext = {
182-
targetType,
183-
targetId,
184-
account,
185-
logPrefix: `[qqbot:${account.accountId}]`,
186-
};
187-
await sendDocument(mediaCtx, String(result.filePath), {
188-
allowQQBotDataDownloads: true,
189-
});
190-
} catch {
191-
// File send failed; the text summary is still returned below.
192-
}
193-
return { text: result.text };
194-
}
195-
196-
return {
197-
text:
198-
result &&
199-
typeof result === "object" &&
200-
"text" in result &&
201-
typeof result.text === "string"
202-
? result.text
203-
: "⚠️ 命令返回了意外结果。",
204-
};
205-
},
206-
});
207-
}
208-
},
28+
registerFull: registerQQBotFull,
20929
});

extensions/qqbot/openclaw.plugin.json

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,6 @@
2424
"transcodeEnabled": { "type": "boolean" }
2525
}
2626
},
27-
"speechQueryParams": {
28-
"type": "object",
29-
"additionalProperties": {
30-
"type": "string"
31-
}
32-
},
33-
"tts": {
34-
"type": "object",
35-
"additionalProperties": false,
36-
"properties": {
37-
"enabled": { "type": "boolean" },
38-
"provider": { "type": "string" },
39-
"baseUrl": { "type": "string" },
40-
"apiKey": { "type": "string" },
41-
"model": { "type": "string" },
42-
"voice": { "type": "string" },
43-
"authStyle": {
44-
"type": "string",
45-
"enum": ["bearer", "api-key"]
46-
},
47-
"queryParams": { "$ref": "#/$defs/speechQueryParams" },
48-
"speed": { "type": "number" }
49-
}
50-
},
5127
"stt": {
5228
"type": "object",
5329
"additionalProperties": false,
@@ -139,7 +115,6 @@
139115
"items": { "type": "string" }
140116
},
141117
"audioFormatPolicy": { "$ref": "#/$defs/audioFormatPolicy" },
142-
"tts": { "$ref": "#/$defs/tts" },
143118
"stt": { "$ref": "#/$defs/stt" },
144119
"urlDirectUpload": { "type": "boolean" },
145120
"upgradeUrl": { "type": "string" },

extensions/qqbot/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"description": "OpenClaw QQ Bot channel plugin",
66
"type": "module",
77
"dependencies": {
8+
"@tencent-connect/qqbot-connector": "^1.1.0",
89
"mpg123-decoder": "^1.0.3",
910
"silk-wasm": "^3.7.1",
1011
"ws": "^8.20.0",

extensions/qqbot/runtime-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ export type {
66
PluginLogger,
77
} from "openclaw/plugin-sdk/core";
88
export type { ResolvedQQBotAccount, QQBotAccountConfig } from "./src/types.js";
9-
export { getQQBotRuntime, setQQBotRuntime } from "./src/runtime.js";
9+
export { getQQBotRuntime, setQQBotRuntime } from "./src/bridge/runtime.js";

extensions/qqbot/skills/qqbot-remind/SKILL.md

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,16 @@ metadata: { "openclaw": { "emoji": "⏰", "requires": { "config": ["channels.qqb
4949
> **payload.kind 必须是 `"agentTurn"`,绝对不能用 `"systemEvent"`**
5050
> `systemEvent` 只在 AI 会话内部注入文本,用户收不到 QQ 消息。
5151
52-
**5 个不可更改字段**
52+
**不可更改字段**
5353

54-
| 字段 | 固定值 | 原因 |
55-
| ----------------- | ------------- | ---------------------------- |
56-
| `payload.kind` | `"agentTurn"` | `systemEvent` 不会发 QQ 消息 |
57-
| `payload.deliver` | `true` | 否则不投递 |
58-
| `payload.channel` | `"qqbot"` | QQ 通道标识 |
59-
| `payload.to` | 用户 openid |`To` 字段获取 |
60-
| `sessionTarget` | `"isolated"` | 隔离会话避免污染 |
54+
| 字段 | 固定值 | 原因 |
55+
| -------------------- | ------------- | ---------------------------- |
56+
| `payload.kind` | `"agentTurn"` | `systemEvent` 不会发 QQ 消息 |
57+
| `delivery.mode` | `"announce"` | 主动投递模式 |
58+
| `delivery.channel` | `"qqbot"` | QQ 通道标识 |
59+
| `delivery.to` | 目标地址 | 从当前会话上下文获取 |
60+
| `delivery.accountId` | 当前账户 ID | 多账号场景下不可省略 |
61+
| `sessionTarget` | `"isolated"` | 隔离会话避免污染 |
6162

6263
> `schedule.atMs` 必须是**绝对毫秒时间戳**(如 `1770733800000`),不支持 `"5m"` 等相对字符串。
6364
> 计算方式:`当前时间戳ms + 延迟毫秒`
@@ -75,10 +76,13 @@ metadata: { "openclaw": { "emoji": "⏰", "requires": { "config": ["channels.qqb
7576
"deleteAfterRun": true,
7677
"payload": {
7778
"kind": "agentTurn",
78-
"message": "你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:{提醒内容}。要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 (3) 直接输出一条暖心的提醒消息 (4) 可以加一句简短的鸡汤或关怀的话 (5) 控制在2-3句话以内 (6) 用emoji点缀",
79-
"deliver": true,
79+
"message": "你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:{提醒内容}。要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 (3) 直接输出一条暖心的提醒消息 (4) 可以加一句简短的鸡汤或关怀的话 (5) 控制在2-3句话以内 (6) 用emoji点缀"
80+
},
81+
"delivery": {
82+
"mode": "announce",
8083
"channel": "qqbot",
81-
"to": "{openid}"
84+
"to": "qqbot:c2c:{openid}",
85+
"accountId": "{accountId}"
8286
}
8387
}
8488
}
@@ -96,16 +100,20 @@ metadata: { "openclaw": { "emoji": "⏰", "requires": { "config": ["channels.qqb
96100
"wakeMode": "now",
97101
"payload": {
98102
"kind": "agentTurn",
99-
"message": "你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:{提醒内容}。要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 (3) 直接输出一条暖心的提醒消息 (4) 可以加一句简短的鸡汤或关怀的话 (5) 控制在2-3句话以内 (6) 用emoji点缀",
100-
"deliver": true,
103+
"message": "你是一个暖心的提醒助手。请用温暖、有趣的方式提醒用户:{提醒内容}。要求:(1) 不要回复HEARTBEAT_OK (2) 不要解释你是谁 (3) 直接输出一条暖心的提醒消息 (4) 可以加一句简短的鸡汤或关怀的话 (5) 控制在2-3句话以内 (6) 用emoji点缀"
104+
},
105+
"delivery": {
106+
"mode": "announce",
101107
"channel": "qqbot",
102-
"to": "{openid}"
108+
"to": "qqbot:c2c:{openid}",
109+
"accountId": "{accountId}"
103110
}
104111
}
105112
}
106113
```
107114

108-
> 周期任务**不加** `deleteAfterRun`。群聊 `to` 格式为 `"group:{group_openid}"`
115+
> 周期任务**不加** `deleteAfterRun`。群聊 `delivery.to` 格式为 `"qqbot:group:{group_openid}"`
116+
> 若通过 `qqbot_remind` 工具生成 cronParams,**必须**原样传给 `cron` 工具,不要修改或省略任何字段,特别是 `delivery.accountId`
109117
110118
---
111119

0 commit comments

Comments
 (0)