Skip to content

Commit 5ccf179

Browse files
cxyhhhhhzkd8907sliverp
authored
feat(qqbot): group chat support, C2C streaming, chunked media upload, and architecture refactor (#70624)
* feat(qqbot): implement unified media upload handling and introduce chunked upload support This commit enhances the media upload functionality by introducing a unified `sendMedia` method that consolidates the previous separate methods for sending images, voice messages, videos, and files. Key changes include: - Added `uploadChunked` function for future chunked media uploads, currently marked as not implemented. - Introduced `MediaSource` abstraction to handle various media types (URLs, base64, local files, buffers) uniformly. - Updated existing media handling logic to utilize the new `sendMedia` method, ensuring consistent media processing across different types. - Enhanced error handling and validation for media uploads, including MIME type checks and file size limits. These changes aim to streamline the media upload process and prepare for future enhancements in handling larger files through chunked uploads. * feat(qqbot): enhance media upload capabilities with chunked upload support This commit updates the media upload functionality by implementing chunked upload support for larger files. Key changes include: - Revised the `SKILL.md` documentation to clarify media file size limits and local file path requirements. - Introduced a new test suite for the chunked media upload functionality, ensuring robust error handling and upload processes. - Updated the media handling logic to enforce per-file-type upload ceilings, allowing for seamless integration of chunked uploads. - Enhanced error handling for daily upload limits, providing user-friendly messages when limits are exceeded. These improvements aim to streamline the media upload process and accommodate larger files effectively. * feat(qqbot): add C2C streaming API support for message delivery This commit introduces support for the QQ C2C official `stream_messages` API, enabling single-message typing-style updates. Key changes include: - Updated the configuration schema to include a new `c2cStreamApi` boolean option for enabling the C2C streaming API. - Enhanced the `QQBotAccountConfig` interface to accommodate the new streaming option. - Implemented a `StreamingController` to manage the lifecycle of C2C stream messages, ensuring proper handling of media tags and message boundaries. - Updated the outbound dispatch logic to utilize the new streaming capabilities, allowing for more dynamic message delivery in one-to-one chats. These enhancements aim to improve the responsiveness and interactivity of message delivery within the QQBot framework. * feat(qqbot): implement group chat support and unify adapter/DI architecture - Implement group message history tracking with pending history buffer (record on skip, render on @-mention reply) - Add mention detection and gating: explicit @bot, implicit quote-reply, ignoreOtherMentions, configurable activation mode (mention/always) - Add group activation resolution with session store persistence - Add message queue with per-peer FIFO and group message merging (batch multiple rapid messages into one merged payload) - Add deliver debounce to merge rapid outbound text bursts into single messages, with media flush and maxWait cap - Add group config resolution: per-group prompt, history limit, wildcard and specific group overrides - Enrich history attachments with local paths from processAttachments so that history context renders downloaded paths instead of ephemeral QQ CDN URLs - Merge ports/ directory into adapter/ as single entry point - Expand EngineAdapters to 5 required ports: history, mentionGate, audioConvert, outboundAudio, commands - Remove global register/get singletons in favor of constructor injection and one-time init - Add createEngineAdapters() in bridge/gateway.ts as single assembly point - Extract monolithic buildInboundContext into 11 discrete stages: access, content, quote, refidx, group-gate, envelope, assembly - Extract group chat modules: history, mention, activation, message-gating, deliver-debounce - Extract config/group.ts, utils/attachment-tags.ts * feat(qqbot): add /bot-streaming command for C2C message streaming control This commit introduces the `/bot-streaming` command, allowing users to enable or disable streaming for message delivery in C2C chats. Key changes include: - Implementation of the `isStreamingConfigEnabled` function to check the current streaming configuration. - Command handler for `/bot-streaming` that provides usage instructions and manages the streaming state. - Updates to the command's response messages to inform users of the current streaming status and how to toggle it. These enhancements aim to improve user experience by providing a straightforward way to manage streaming message delivery in private chats. * feat(qqbot): extract interaction handler and add remote config query/update support - Extract INTERACTION_CREATE handler from gateway.ts into a dedicated interaction-handler.ts module for better separation of concerns - Add config query (type=2001) and config update (type=2002) interaction branches that read/write claw_cfg via runtime.config API - Register INTERACTION intent (1<<26) in FULL_INTENTS to receive INTERACTION_CREATE events from the gateway - Add InteractionType constants (CONFIG_QUERY, CONFIG_UPDATE) - Extend GatewayPluginRuntime with optional config API (loadConfig, writeConfigFile) for interaction handler access - Add QQBotAccountConfigView interface for typed config field access - Extend acknowledgeInteraction to accept optional data payload for rich ACK responses (e.g. claw_cfg snapshot) - Export getFrameworkVersion from slash-commands-impl for version reporting in config snapshots - Remove unused eslint-disable directive in streaming-media-send.ts * feat(qqbot): enhance account management and logging capabilities - Introduced `toGatewayAccount` function to map resolved QQBot accounts to the engine's gateway account structure. - Added `persistAccountCredentialSnapshot` function to streamline credential backup during gateway events. - Updated the `qqbotPlugin` to utilize the new account mapping and credential persistence functions, improving the handling of account data. - Enhanced logging functionality by modifying the `EngineLogger` interface to support metadata in log messages. - Implemented new commands for managing logs and clearing storage, providing users with better control over their data and system resources. - Registered multiple built-in commands for improved user interaction, including `/bot-logs` for exporting logs and `/bot-clear-storage` for managing downloaded files. - Updated configuration schemas to reflect new options and improve clarity for users. * fix(qqbot): resolve oxlint errors and update raw-fetch allowlist - Replace unnecessary `else` after `return` in outbound-media-send.ts (6 occurrences) - Use `Number.parseInt` instead of global `parseInt` in outbound.ts and streaming-media-send.ts - Use `Number.isNaN` instead of global `isNaN` in register-basic.ts - Prefer `**` over `Math.pow` in media-chunked.ts - Convert interface with call signature to function type in commands.port.ts - Update api-client.ts allowlist line number (108→124) and add media-chunked.ts:552 to raw-fetch allowlist * docs(qqbot): translate streaming-c2c.ts header comments to English * feat(qqbot): add voiceMediaTypes * feat: restore dispatch changes * fix(qqbot): align test files with updated engine interfaces after rebase - inbound-attachments.test: replace removed registerAudioConvertAdapter with AudioConvertPort, pass audioConvert in ProcessContext - inbound-pipeline.self-echo.test: add required adapters field to InboundPipelineDeps mock (history, mentionGate, audioConvert, outboundAudio, commands) - outbound-dispatch.test: add required skipped field to InboundContext * fix(qqbot): update test assertions to match refactored engine interfaces - inbound-pipeline.self-echo.test: self-echo blocking was moved upstream; update test to expect non-blocked pipeline behavior - outbound-dispatch.test: TTS voice path now uses unified sendMedia instead of sendVoiceMessage; add sendMedia mock and update assertion - format-ref-entry.test: attachment format changed from [image: ...] to MEDIA: tag syntax via renderAttachmentTags; update expected output * refactor(qqbot): migrate from deprecated config API to current/replaceConfigFile Replace all usages of deprecated runtime config methods: - loadConfig() → current() - writeConfigFile(cfg) → replaceConfigFile({ nextConfig, afterWrite }) Updated files: - bridge/narrowing.ts: writeOpenClawConfigThroughRuntime - adapter/commands.port.ts: ApproveRuntimeGetter type signature - commands/builtin/register-approve.ts: loadExecConfig, writeExecConfig, reset - commands/builtin/register-streaming.ts: config read/write - gateway/interaction-handler.ts: config query/update handlers - gateway/types.ts: GatewayPluginRuntime.config interface * feat(qqbot): update package.json * fix(qqbot): replace deprecated config-runtime import with config-types subpath Bundled plugin lint requires focused plugin-sdk subpaths. - gateway.ts: openclaw/plugin-sdk/config-runtime → config-types - narrowing.ts: openclaw/plugin-sdk/config-runtime → config-types * feat(qqbot): group chat support, C2C streaming, chunked media upload, and architecture refactor (#70624) (thanks @cxyhhhhh) --------- Co-authored-by: Bobby <[email protected]> Co-authored-by: sliverp <[email protected]>
1 parent 8304635 commit 5ccf179

89 files changed

Lines changed: 11957 additions & 3084 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
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
77
### Changes
88

99
- Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (`openclaw-plugin-yuanbao`) in the official channel catalog, contract suites, and community plugin docs, with a new `docs/channels/yuanbao.md` quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay.
10+
- Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C `stream_messages` streaming with a `StreamingController` lifecycle manager, unified `sendMedia` with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via `createEngineAdapters()`. (#70624) Thanks @cxyhhhhh.
1011

1112
## 2026.4.26
1213

extensions/qqbot/openclaw.plugin.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@
9191
"type": "string",
9292
"enum": ["off", "partial"],
9393
"default": "partial"
94+
},
95+
"c2cStreamApi": {
96+
"type": "boolean",
97+
"description": "Use QQ C2C official stream_messages API (single-message typing-style updates)."
9498
}
9599
}
96100
}
@@ -136,6 +140,10 @@
136140
"type": "string",
137141
"enum": ["off", "partial"],
138142
"default": "partial"
143+
},
144+
"c2cStreamApi": {
145+
"type": "boolean",
146+
"description": "Use QQ C2C official stream_messages API (single-message typing-style updates)."
139147
}
140148
}
141149
}

extensions/qqbot/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openclaw/qqbot",
3-
"version": "2026.4.25",
3+
"version": "2026.4.26",
44
"private": false,
55
"description": "OpenClaw QQ Bot channel plugin",
66
"type": "module",
@@ -17,7 +17,7 @@
1717
"openclaw": "workspace:*"
1818
},
1919
"peerDependencies": {
20-
"openclaw": ">=2026.4.25"
20+
"openclaw": ">=2026.4.26"
2121
},
2222
"peerDependenciesMeta": {
2323
"openclaw": {
@@ -46,10 +46,10 @@
4646
"minHostVersion": ">=2026.4.10"
4747
},
4848
"compat": {
49-
"pluginApi": ">=2026.4.25"
49+
"pluginApi": ">=2026.4.26"
5050
},
5151
"build": {
52-
"openclawVersion": "2026.4.25"
52+
"openclawVersion": "2026.4.26"
5353
},
5454
"bundle": {
5555
"stageRuntimeDependencies": true

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ metadata: { "openclaw": { "emoji": "📸", "requires": { "config": ["channels.qq
2929

3030
1. **路径必须是绝对路径**(以 `/``http` 开头)
3131
2. **标签必须用开闭标签包裹路径**`<qqmedia>路径</qqmedia>`
32-
3. **文件大小上限 10MB**
33-
4. **你有能力发送本地图片/文件**,直接用标签包裹路径即可,**不要说"无法发送"**
34-
5. 发送语音时不要重复语音中已朗读的文字
35-
6. 多个媒体用多个标签
36-
7. 以会话上下文中的能力说明为准(如未启用语音则不要发语音)
32+
3. **待发送的本地文件须落在 OpenClaw 媒体目录下**:生成、下载或复制出的文件应写入 **`~/.openclaw/media/qqbot/`**(或其子目录),再写进 `<qqmedia>`。不要只放在 `~/.openclaw/workspace/` 等工作区根目录——平台安全策略只允许从 `~/.openclaw/media/`(含 `media/qqbot`)等受信根路径上传,否则会拦截、发不出去。
33+
4. **文件大小上限**:图片 30MB / 视频 100MB / 文件 100MB / 语音 20MB
34+
5. **你有能力发送本地图片/文件**,直接用标签包裹路径即可,**不要说"无法发送"**
35+
6. 发送语音时不要重复语音中已朗读的文字
36+
7. 多个媒体用多个标签
37+
8. 以会话上下文中的能力说明为准(如未启用语音则不要发语音)

extensions/qqbot/src/bridge/config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,17 @@ export function resolveQQBotAccount(
3737
const base = resolveAccountBase(raw, accountId);
3838

3939
const qqbot = cfg.channels?.qqbot as QQBotChannelConfig | undefined;
40+
/**
41+
* Legacy top-level account uses `channels.qqbot` as the base, but per-account
42+
* fields (allowFrom, streaming, …) often live under `accounts.default`.
43+
* Merge that slice so runtime sees `config.streaming` etc.
44+
*/
4045
const accountConfig: QQBotAccountConfig =
4146
base.accountId === DEFAULT_ACCOUNT_ID
42-
? (qqbot ?? {})
47+
? {
48+
...qqbot,
49+
...qqbot?.accounts?.[DEFAULT_ACCOUNT_ID],
50+
}
4351
: (qqbot?.accounts?.[base.accountId] ?? {});
4452

4553
let clientSecret = "";
Lines changed: 64 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,41 @@
11
/**
2-
* Gateway entry point — thin shell that passes the PluginRuntime to
3-
* core/gateway/gateway.ts.
2+
* Gateway entry point — thin bridge shell that constructs
3+
* {@link EngineAdapters} and passes them to the engine's
4+
* `startGateway`.
45
*
5-
* All module dependencies are imported directly by the core gateway.
6-
* This file only provides the runtime object (which is dynamically
7-
* injected by the framework at startup).
6+
* All adapter dependencies are assembled here in one place.
87
*/
98

109
import { resolveRuntimeServiceVersion } from "openclaw/plugin-sdk/cli-runtime";
1110
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
12-
import {
13-
registerVersionResolver,
14-
registerPluginVersion,
15-
registerApproveRuntimeGetter,
16-
} from "../engine/commands/slash-commands-impl.js";
11+
import type { EngineAdapters } from "../engine/adapter/index.js";
1712
import {
1813
startGateway as coreStartGateway,
1914
type CoreGatewayContext,
2015
} from "../engine/gateway/gateway.js";
21-
import type { GatewayAccount } from "../engine/gateway/types.js";
22-
import { registerOutboundAudioAdapterFactory } from "../engine/messaging/outbound.js";
16+
import type { GatewayPluginRuntime } from "../engine/gateway/types.js";
2317
import { initSender, registerAccount } from "../engine/messaging/sender.js";
2418
import type { EngineLogger } from "../engine/types.js";
2519
import * as _audioModule from "../engine/utils/audio.js";
20+
import { formatDuration } from "../engine/utils/format.js";
2621
import { debugLog, debugError } from "../engine/utils/log.js";
27-
import { registerTextChunker } from "../engine/utils/text-chunk.js";
2822
import type { ResolvedQQBotAccount } from "../types.js";
2923
import { ensurePlatformAdapter } from "./bootstrap.js";
3024
import { setBridgeLogger } from "./logger.js";
25+
import { toGatewayAccount } from "./narrowing.js";
3126
import { resolveQQBotPluginVersion } from "./plugin-version.js";
3227
import { getQQBotRuntime, getQQBotRuntimeForEngine } from "./runtime.js";
28+
import { createSdkHistoryAdapter, createSdkMentionGateAdapter } from "./sdk-adapter.js";
3329

34-
// Register framework SDK version resolver for core/ slash commands.
35-
registerVersionResolver(resolveRuntimeServiceVersion);
30+
// ---- One-time startup initialization (module-level) ----
3631

37-
// Inject plugin + framework versions into sender and into the slash
38-
// command registry. The plugin version is read from this plugin's own
39-
// `package.json` by walking up from this file's URL, which is robust
40-
// against source-vs-dist layout differences.
4132
const _pluginVersion = resolveQQBotPluginVersion(import.meta.url);
4233
initSender({
4334
pluginVersion: _pluginVersion,
4435
openclawVersion: resolveRuntimeServiceVersion(),
4536
});
46-
registerPluginVersion(_pluginVersion);
4737

48-
// Register runtime getter for /bot-approve config management.
49-
registerApproveRuntimeGetter(() => {
50-
const rt = getQQBotRuntime();
51-
return {
52-
config: rt.config as {
53-
current: () => Record<string, unknown>;
54-
replaceConfigFile: (params: {
55-
nextConfig: Record<string, unknown>;
56-
afterWrite: { mode: "auto" };
57-
}) => Promise<unknown>;
58-
},
59-
};
60-
});
61-
62-
// Register audio adapter factory so outbound.sendMedia can lazy-init even
63-
// when startGateway() hasn't run yet (bundler chunk-splitting scenario).
64-
registerOutboundAudioAdapterFactory(() => {
65-
// Use a synchronous require-like approach: the audio module should already
66-
// be loaded by the time the factory is invoked (gateway has started).
67-
// We import it at the top and reference it here.
68-
return {
69-
audioFileToSilkBase64: async (p: string, f?: string[]) =>
70-
(await _audioModule.audioFileToSilkBase64(p, f)) ?? undefined,
71-
isAudioFile: (p: string, m?: string) => _audioModule.isAudioFile(p, m),
72-
shouldTranscodeVoice: (p: string) => _audioModule.shouldTranscodeVoice(p),
73-
waitForFile: (p: string, ms?: number) => _audioModule.waitForFile(p, ms),
74-
};
75-
});
38+
// ============ Public types ============
7639

7740
export interface GatewayContext {
7841
account: ResolvedQQBotAccount;
@@ -99,32 +62,62 @@ export interface GatewayContext {
9962
};
10063
}
10164

65+
// ============ Adapter factory ============
66+
67+
/**
68+
* Create the full set of engine adapters from the bridge layer.
69+
*
70+
* This is the **single assembly point** — all SDK → engine binding
71+
* happens here. The engine receives a fully-populated
72+
* {@link EngineAdapters} object with zero global singletons.
73+
*/
74+
function createEngineAdapters(_runtime: GatewayPluginRuntime): EngineAdapters {
75+
return {
76+
history: createSdkHistoryAdapter(),
77+
mentionGate: createSdkMentionGateAdapter(),
78+
audioConvert: {
79+
convertSilkToWav: _audioModule.convertSilkToWav,
80+
isVoiceAttachment: _audioModule.isVoiceAttachment,
81+
formatDuration,
82+
},
83+
outboundAudio: {
84+
audioFileToSilkBase64: async (p: string, f?: string[]) =>
85+
(await _audioModule.audioFileToSilkBase64(p, f)) ?? undefined,
86+
isAudioFile: (p: string, m?: string) => _audioModule.isAudioFile(p, m),
87+
shouldTranscodeVoice: (p: string) => _audioModule.shouldTranscodeVoice(p),
88+
waitForFile: (p: string, ms?: number) => _audioModule.waitForFile(p, ms),
89+
},
90+
commands: {
91+
resolveVersion: resolveRuntimeServiceVersion,
92+
pluginVersion: _pluginVersion,
93+
approveRuntimeGetter: () => {
94+
const rt = getQQBotRuntime();
95+
return { config: rt.config };
96+
},
97+
},
98+
};
99+
}
100+
101+
// ============ startGateway ============
102+
102103
/**
103104
* Start the Gateway WebSocket connection.
104105
*
105-
* Passes the PluginRuntime to core/gateway/gateway.ts.
106-
* All other dependencies are imported directly by the core module.
106+
* Assembles all adapters and passes them to the engine's core gateway.
107107
*/
108108
export async function startGateway(ctx: GatewayContext): Promise<void> {
109-
// Ensure the PlatformAdapter is registered before any engine code runs.
110-
// When the bundler splits code into separate chunks, bootstrap.ts's
111-
// side-effect registration may not have executed yet at this point.
112109
ensurePlatformAdapter();
113110

114111
const runtime = getQQBotRuntimeForEngine();
115-
116-
// Create per-account logger with auto [qqbot:{accountId}] prefix.
117112
const accountLogger = createAccountLogger(ctx.log, ctx.account.accountId);
118113

119-
// Register into engine sender (per-appId logger + API config) and bridge layer.
114+
// Per-account registration (still global — sender is a leaf utility).
120115
registerAccount(ctx.account.appId, {
121116
logger: accountLogger,
122117
markdownSupport: ctx.account.markdownSupport,
123118
});
124119
setBridgeLogger(accountLogger);
125120

126-
registerTextChunker((text, limit) => runtime.channel.text.chunkMarkdownText(text, limit));
127-
128121
if (ctx.channelRuntime) {
129122
accountLogger.info("Registering approval.native runtime context");
130123
const lease = ctx.channelRuntime.runtimeContexts.register({
@@ -140,44 +133,42 @@ export async function startGateway(ctx: GatewayContext): Promise<void> {
140133
}
141134

142135
const coreCtx: CoreGatewayContext = {
143-
account: ctx.account as unknown as GatewayAccount,
136+
account: toGatewayAccount(ctx.account),
144137
abortSignal: ctx.abortSignal,
145138
cfg: ctx.cfg,
146139
onReady: ctx.onReady,
147140
onResumed: ctx.onResumed,
148141
onError: ctx.onError,
149142
log: accountLogger,
150143
runtime,
144+
adapters: createEngineAdapters(runtime),
151145
};
152146

153147
return coreStartGateway(coreCtx);
154148
}
155149

156150
// ============ Per-account logger factory ============
157151

158-
/**
159-
* Create an EngineLogger that auto-prefixes all messages with `[qqbot:{accountId}]`.
160-
*
161-
* Follows the WhatsApp pattern of per-connection loggers — each account gets
162-
* its own logger instance so multi-account logs are automatically attributed.
163-
*/
164152
function createAccountLogger(
165153
raw: GatewayContext["log"] | undefined,
166154
accountId: string,
167155
): EngineLogger {
168156
const prefix = `[${accountId}]`;
157+
const withMeta = (msg: string, meta?: Record<string, unknown>) =>
158+
meta && Object.keys(meta).length > 0 ? `${msg} ${JSON.stringify(meta)}` : msg;
159+
169160
if (!raw) {
170161
return {
171-
info: (msg) => debugLog(`${prefix} ${msg}`),
172-
error: (msg) => debugError(`${prefix} ${msg}`),
173-
warn: (msg) => debugError(`${prefix} ${msg}`),
174-
debug: (msg) => debugLog(`${prefix} ${msg}`),
162+
info: (msg, meta) => debugLog(`${prefix} ${withMeta(msg, meta)}`),
163+
error: (msg, meta) => debugError(`${prefix} ${withMeta(msg, meta)}`),
164+
warn: (msg, meta) => debugError(`${prefix} ${withMeta(msg, meta)}`),
165+
debug: (msg, meta) => debugLog(`${prefix} ${withMeta(msg, meta)}`),
175166
};
176167
}
177168
return {
178-
info: (msg) => raw.info(`${prefix} ${msg}`),
179-
error: (msg) => raw.error(`${prefix} ${msg}`),
180-
warn: (msg) => raw.error(`${prefix} ${msg}`),
181-
debug: (msg) => raw.debug?.(`${prefix} ${msg}`),
169+
info: (msg, meta) => raw.info(`${prefix} ${withMeta(msg, meta)}`),
170+
error: (msg, meta) => raw.error(`${prefix} ${withMeta(msg, meta)}`),
171+
warn: (msg, meta) => raw.error(`${prefix} ${withMeta(msg, meta)}`),
172+
debug: (msg, meta) => raw.debug?.(`${prefix} ${withMeta(msg, meta)}`),
182173
};
183174
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2+
import type { PluginRuntime } from "openclaw/plugin-sdk/core";
3+
import type { GatewayAccount } from "../engine/types.js";
4+
import type { ResolvedQQBotAccount } from "../types.js";
5+
6+
/**
7+
* Map resolved plugin account to the engine gateway account shape (single assertion on nested config).
8+
*/
9+
export function toGatewayAccount(account: ResolvedQQBotAccount): GatewayAccount {
10+
return {
11+
accountId: account.accountId,
12+
appId: account.appId,
13+
clientSecret: account.clientSecret,
14+
markdownSupport: account.markdownSupport,
15+
systemPrompt: account.systemPrompt,
16+
config: account.config as GatewayAccount["config"],
17+
};
18+
}
19+
20+
/**
21+
* Persist OpenClaw config through the injected plugin runtime (typed entry point).
22+
*/
23+
export async function writeOpenClawConfigThroughRuntime(
24+
runtime: PluginRuntime,
25+
cfg: OpenClawConfig,
26+
): Promise<void> {
27+
await runtime.config.replaceConfigFile({
28+
nextConfig: cfg,
29+
afterWrite: { mode: "auto" },
30+
});
31+
}

extensions/qqbot/src/bridge/runtime.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
33
import type { GatewayPluginRuntime } from "../engine/gateway/types.js";
44
import { setOpenClawVersion } from "../engine/messaging/sender.js";
55

6+
// Single plugin runtime per process — concurrent multi-tenant qqbot runtimes are not supported.
67
const { setRuntime: _setRuntime, getRuntime: getQQBotRuntime } =
78
createPluginRuntimeStore<PluginRuntime>({
89
pluginId: "qqbot",
@@ -20,5 +21,5 @@ export { getQQBotRuntime, setQQBotRuntime };
2021

2122
/** Type-narrowed getter for engine/ modules that need GatewayPluginRuntime. */
2223
export function getQQBotRuntimeForEngine(): GatewayPluginRuntime {
23-
return getQQBotRuntime() as unknown as GatewayPluginRuntime;
24+
return getQQBotRuntime() as GatewayPluginRuntime;
2425
}

0 commit comments

Comments
 (0)