@openclaw/qqbot 2026.5.14-beta.1 → 2026.5.16-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1102 @@
1
+ import { c as getPlatformAdapter, t as asOptionalObjectRecord } from "./string-normalize-C46CS-F1.js";
2
+ import { a as qqbotSetupAdapterShared, c as listQQBotAccountIds, i as qqbotMeta, m as getBridgeLogger, n as qqbotSetupWizard, o as DEFAULT_ACCOUNT_ID$1, p as ensurePlatformAdapter, r as qqbotConfigAdapter, s as applyQQBotAccountConfig, t as qqbotChannelConfigSchema, u as resolveQQBotAccount } from "./config-schema-BE5YP0NG.js";
3
+ import { T as debugWarn, t as getQQBotRuntime, w as debugLog, z as formatErrorMessage } from "./runtime-BFcYWYuk.js";
4
+ import { getExecApprovalReplyMetadata } from "openclaw/plugin-sdk/approval-runtime";
5
+ import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
6
+ import { createChannelApprovalCapability } from "openclaw/plugin-sdk/approval-delivery-runtime";
7
+ import { createLazyChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
8
+ import { resolveApprovalRequestChannelAccountId, resolveApprovalRequestSessionConversation } from "openclaw/plugin-sdk/approval-native-runtime";
9
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
10
+ import { resolveApprovalApprovers } from "openclaw/plugin-sdk/approval-auth-runtime";
11
+ import { createChannelExecApprovalProfile, isChannelExecApprovalClientEnabledFromConfig, matchesApprovalRequestFilters } from "openclaw/plugin-sdk/approval-client-runtime";
12
+ import { normalizeAccountId } from "openclaw/plugin-sdk/routing";
13
+ import * as fs$1 from "node:fs";
14
+ import fs from "node:fs";
15
+ import * as os$1 from "node:os";
16
+ import { replaceFileAtomicSync } from "openclaw/plugin-sdk/security-runtime";
17
+ import * as path$1 from "node:path";
18
+ import path from "node:path";
19
+ import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
20
+ //#region extensions/qqbot/src/engine/approval/index.ts
21
+ function buildExecApprovalText(request) {
22
+ const expiresIn = Math.max(0, Math.round((request.expiresAtMs - Date.now()) / 1e3));
23
+ const lines = ["🔐 命令执行审批", ""];
24
+ const cmd = request.request.commandPreview ?? request.request.command ?? "";
25
+ if (cmd) lines.push(`\`\`\`\n${cmd.slice(0, 300)}\n\`\`\``);
26
+ if (request.request.cwd) lines.push(`\u{1f4c1} \u76ee\u5f55: ${request.request.cwd}`);
27
+ if (request.request.agentId) lines.push(`\u{1f916} Agent: ${request.request.agentId}`);
28
+ lines.push("", `\u23f1\ufe0f \u8d85\u65f6: ${expiresIn} \u79d2`);
29
+ return lines.join("\n");
30
+ }
31
+ function buildPluginApprovalText(request) {
32
+ const timeoutSec = Math.round((request.request.timeoutMs ?? 12e4) / 1e3);
33
+ const lines = [`${request.request.severity === "critical" ? "🔴" : request.request.severity === "info" ? "🔵" : "🟡"} \u5ba1\u6279\u8bf7\u6c42`, ""];
34
+ lines.push(`\u{1f4cb} ${request.request.title}`);
35
+ if (request.request.description) lines.push(`\u{1f4dd} ${request.request.description}`);
36
+ if (request.request.toolName) lines.push(`\u{1f527} \u5de5\u5177: ${request.request.toolName}`);
37
+ if (request.request.pluginId) lines.push(`\u{1f50c} \u63d2\u4ef6: ${request.request.pluginId}`);
38
+ if (request.request.agentId) lines.push(`\u{1f916} Agent: ${request.request.agentId}`);
39
+ lines.push("", `\u23f1\ufe0f \u8d85\u65f6: ${timeoutSec} \u79d2`);
40
+ return lines.join("\n");
41
+ }
42
+ /**
43
+ * Build the three-button inline keyboard for approval messages.
44
+ *
45
+ * type=1 (Callback): click triggers INTERACTION_CREATE, button_data = data field.
46
+ * group_id "approval": clicking one button grays out the others (mutual exclusion).
47
+ * click_limit=1: each user can only click once.
48
+ * permission.type=2: all users can interact.
49
+ */
50
+ function buildApprovalKeyboard(approvalId, allowedDecisions = [
51
+ "allow-once",
52
+ "allow-always",
53
+ "deny"
54
+ ]) {
55
+ const makeBtn = (id, label, visitedLabel, data, style) => ({
56
+ id,
57
+ render_data: {
58
+ label,
59
+ visited_label: visitedLabel,
60
+ style
61
+ },
62
+ action: {
63
+ type: 1,
64
+ data,
65
+ permission: { type: 2 },
66
+ click_limit: 1
67
+ },
68
+ group_id: "approval"
69
+ });
70
+ const buttons = [];
71
+ if (allowedDecisions.includes("allow-once")) buttons.push(makeBtn("allow", "✅ 允许一次", "已允许", `approve:${approvalId}:allow-once`, 1));
72
+ if (allowedDecisions.includes("allow-always")) buttons.push(makeBtn("always", "⭐ 始终允许", "已始终允许", `approve:${approvalId}:allow-always`, 1));
73
+ if (allowedDecisions.includes("deny")) buttons.push(makeBtn("deny", "❌ 拒绝", "已拒绝", `approve:${approvalId}:deny`, 0));
74
+ return { content: { rows: [{ buttons }] } };
75
+ }
76
+ /**
77
+ * Extract the delivery target from a sessionKey or turnSourceTo string.
78
+ *
79
+ * Expected formats:
80
+ * agent:main:qqbot:direct:OPENID -> { type: "c2c", id: "OPENID" }
81
+ * agent:main:qqbot:c2c:OPENID -> { type: "c2c", id: "OPENID" }
82
+ * agent:main:qqbot:group:GROUPID -> { type: "group", id: "GROUPID" }
83
+ *
84
+ * Returns null if neither field matches the expected pattern.
85
+ */
86
+ function resolveApprovalTarget(sessionKey, turnSourceTo) {
87
+ const sk = sessionKey ?? turnSourceTo;
88
+ if (!sk) return null;
89
+ const m = sk.match(/qqbot:(c2c|direct|group):([A-F0-9]+)/i);
90
+ if (!m) return null;
91
+ return {
92
+ type: m[1].toLowerCase() === "group" ? "group" : "c2c",
93
+ id: m[2]
94
+ };
95
+ }
96
+ /**
97
+ * Parse the button_data string from an INTERACTION_CREATE event.
98
+ *
99
+ * Expected format: `approve:<approvalId>:<decision>`
100
+ * where approvalId may be prefixed with "exec:" or "plugin:".
101
+ *
102
+ * Returns null if the data does not match the approval button format.
103
+ */
104
+ function parseApprovalButtonData(buttonData) {
105
+ const m = buttonData.match(/^approve:((?:(?:exec|plugin):)?[0-9a-f-]+):(allow-once|allow-always|deny)$/i);
106
+ if (!m) return null;
107
+ return {
108
+ approvalId: m[1],
109
+ decision: m[2]
110
+ };
111
+ }
112
+ //#endregion
113
+ //#region extensions/qqbot/src/exec-approvals.ts
114
+ function normalizeApproverId(value) {
115
+ return normalizeOptionalString(String(value)) || void 0;
116
+ }
117
+ function resolveQQBotExecApprovalConfig(params) {
118
+ const account = resolveQQBotAccount(params.cfg, params.accountId);
119
+ const config = account.config.execApprovals;
120
+ if (!config) return;
121
+ return {
122
+ ...config,
123
+ enabled: account.enabled && account.secretSource !== "none" ? config.enabled : false
124
+ };
125
+ }
126
+ function getQQBotExecApprovalApprovers(params) {
127
+ const accountConfig = resolveQQBotAccount(params.cfg, params.accountId).config;
128
+ return resolveApprovalApprovers({
129
+ explicit: resolveQQBotExecApprovalConfig(params)?.approvers,
130
+ allowFrom: accountConfig.allowFrom,
131
+ normalizeApprover: normalizeApproverId
132
+ });
133
+ }
134
+ function countQQBotExecApprovalEligibleAccounts(params) {
135
+ return listQQBotAccountIds(params.cfg).filter((accountId) => {
136
+ const account = resolveQQBotAccount(params.cfg, accountId);
137
+ if (!account.enabled || account.secretSource === "none") return false;
138
+ const config = resolveQQBotExecApprovalConfig({
139
+ cfg: params.cfg,
140
+ accountId
141
+ });
142
+ return isChannelExecApprovalClientEnabledFromConfig({
143
+ enabled: config?.enabled,
144
+ approverCount: getQQBotExecApprovalApprovers({
145
+ cfg: params.cfg,
146
+ accountId
147
+ }).length
148
+ }) && matchesApprovalRequestFilters({
149
+ request: params.request.request,
150
+ agentFilter: config?.agentFilter,
151
+ sessionFilter: config?.sessionFilter,
152
+ fallbackAgentIdFromSessionKey: true
153
+ });
154
+ }).length;
155
+ }
156
+ function matchesQQBotRequestAccount(params) {
157
+ const turnSourceChannel = normalizeLowercaseStringOrEmpty(params.request.request.turnSourceChannel);
158
+ const boundAccountId = resolveApprovalRequestChannelAccountId({
159
+ cfg: params.cfg,
160
+ request: params.request,
161
+ channel: "qqbot"
162
+ });
163
+ if (turnSourceChannel && turnSourceChannel !== "qqbot" && !boundAccountId) return countQQBotExecApprovalEligibleAccounts({
164
+ cfg: params.cfg,
165
+ request: params.request
166
+ }) <= 1;
167
+ return !boundAccountId || !params.accountId || normalizeAccountId(boundAccountId) === normalizeAccountId(params.accountId);
168
+ }
169
+ /**
170
+ * Count QQBot accounts that could actually deliver a native approval
171
+ * message — i.e. accounts that are enabled and have resolvable secrets.
172
+ * Disabled or unconfigured accounts never spawn a handler, so they
173
+ * must not contribute to the single-account shortcut in the fallback
174
+ * ownership check below.
175
+ */
176
+ function countQQBotFallbackEligibleAccounts(cfg) {
177
+ return listQQBotAccountIds(cfg).filter((accountId) => {
178
+ const account = resolveQQBotAccount(cfg, accountId);
179
+ return account.enabled && account.secretSource !== "none";
180
+ }).length;
181
+ }
182
+ /**
183
+ * Fallback account-ownership check — applied when `execApprovals` is NOT
184
+ * configured for any QQBot account. In this mode every enabled account
185
+ * handler would otherwise race to deliver the same approval to its own
186
+ * openid namespace, so we must enforce per-account isolation.
187
+ *
188
+ * Rules:
189
+ * - If the request carries a bound account (via `turnSourceAccountId`
190
+ * or session binding), only the handler whose `accountId` matches it
191
+ * delivers the approval. This is strict: a handler with an unknown
192
+ * `accountId` (null/undefined) must not claim a bound request.
193
+ * - If no account is bound, only deliver when there is a single
194
+ * *eligible* QQBot account (enabled + secret resolved). Disabled or
195
+ * unconfigured accounts never deliver anyway, so they shouldn't
196
+ * block the remaining single account from handling the approval.
197
+ * Multiple eligible accounts cannot safely race because openids are
198
+ * account-scoped — cross-account delivery hits the QQ Bot API with
199
+ * a mismatched token and fails.
200
+ */
201
+ function matchesQQBotFallbackRequestAccount(params) {
202
+ const boundAccountId = resolveApprovalRequestChannelAccountId({
203
+ cfg: params.cfg,
204
+ request: params.request,
205
+ channel: "qqbot"
206
+ });
207
+ if (boundAccountId) {
208
+ if (!params.accountId) return false;
209
+ return normalizeAccountId(boundAccountId) === normalizeAccountId(params.accountId);
210
+ }
211
+ return countQQBotFallbackEligibleAccounts(params.cfg) <= 1;
212
+ }
213
+ /**
214
+ * Unified per-account ownership check used by both the profile and
215
+ * fallback approval paths. Dispatches to the profile rules when the
216
+ * current account has `execApprovals` configured, otherwise uses the
217
+ * fallback rules.
218
+ *
219
+ * This is the single source of truth for "does this QQBot handler own
220
+ * this approval request?" and is consumed by both the capability
221
+ * gate (shouldHandle) and the lazy native runtime adapter.
222
+ */
223
+ function matchesQQBotApprovalAccount(params) {
224
+ const normalized = {
225
+ cfg: params.cfg,
226
+ accountId: params.accountId,
227
+ request: params.request
228
+ };
229
+ if (resolveQQBotExecApprovalConfig(normalized) !== void 0) return matchesQQBotRequestAccount(normalized);
230
+ return matchesQQBotFallbackRequestAccount(normalized);
231
+ }
232
+ const qqbotExecApprovalProfile = createChannelExecApprovalProfile({
233
+ resolveConfig: resolveQQBotExecApprovalConfig,
234
+ resolveApprovers: getQQBotExecApprovalApprovers,
235
+ matchesRequestAccount: matchesQQBotRequestAccount,
236
+ fallbackAgentIdFromSessionKey: true,
237
+ requireClientEnabledForLocalPromptSuppression: false
238
+ });
239
+ const isQQBotExecApprovalClientEnabled = qqbotExecApprovalProfile.isClientEnabled;
240
+ const isQQBotExecApprovalApprover = qqbotExecApprovalProfile.isApprover;
241
+ const isQQBotExecApprovalAuthorizedSender = qqbotExecApprovalProfile.isAuthorizedSender;
242
+ const shouldHandleQQBotExecApprovalRequest = qqbotExecApprovalProfile.shouldHandleRequest;
243
+ function authorizeQQBotApprovalAction(params) {
244
+ if (resolveQQBotExecApprovalConfig(params) === void 0) return { authorized: true };
245
+ return (params.approvalKind === "plugin" ? isQQBotExecApprovalApprover(params) : isQQBotExecApprovalAuthorizedSender(params)) ? { authorized: true } : {
246
+ authorized: false,
247
+ reason: "You are not authorized to approve this request."
248
+ };
249
+ }
250
+ //#endregion
251
+ //#region extensions/qqbot/src/bridge/approval/capability.ts
252
+ /**
253
+ * QQ Bot Approval Capability — entry point.
254
+ *
255
+ * QQBot uses a simpler approval model than Telegram/Slack: when no
256
+ * approver list is configured, the bot sends the approval message to the
257
+ * originating conversation and any participant can approve from there.
258
+ *
259
+ * When `execApprovals` IS configured, it gates which requests are
260
+ * handled natively and who is authorized. When it is NOT configured,
261
+ * QQBot falls back to "always handle, anyone can approve".
262
+ */
263
+ /**
264
+ * When `execApprovals` is configured, delegate to the profile-based
265
+ * check. Otherwise fall back to target-resolvability plus the shared
266
+ * per-account ownership rule in `matchesQQBotApprovalAccount` so that
267
+ * each QQBot account handler only delivers approvals that originated
268
+ * from its own account (openids are account-scoped — cross-account
269
+ * delivery fails with 500 on the QQ Bot API).
270
+ */
271
+ function shouldHandleRequest(params) {
272
+ if (hasExecApprovalConfig(params)) return shouldHandleQQBotExecApprovalRequest(params);
273
+ if (!canResolveTarget(params.request)) return false;
274
+ return matchesQQBotApprovalAccount({
275
+ cfg: params.cfg,
276
+ accountId: params.accountId,
277
+ request: params.request
278
+ });
279
+ }
280
+ function hasExecApprovalConfig(params) {
281
+ return resolveQQBotExecApprovalConfig(params) !== void 0;
282
+ }
283
+ function isNativeDeliveryEnabled(params) {
284
+ if (hasExecApprovalConfig(params)) return isQQBotExecApprovalClientEnabled(params);
285
+ const account = resolveQQBotAccount(params.cfg, params.accountId);
286
+ return account.enabled && account.secretSource !== "none";
287
+ }
288
+ function canResolveTarget(request) {
289
+ if (resolveApprovalTarget(request.request.sessionKey ?? null, request.request.turnSourceTo ?? null)) return true;
290
+ return resolveApprovalRequestSessionConversation({
291
+ request,
292
+ channel: "qqbot",
293
+ bundledFallback: true
294
+ })?.id != null;
295
+ }
296
+ function createQQBotApprovalCapability() {
297
+ return createChannelApprovalCapability({
298
+ authorizeActorAction: ({ cfg, accountId, senderId, approvalKind }) => authorizeQQBotApprovalAction({
299
+ cfg,
300
+ accountId,
301
+ senderId,
302
+ approvalKind
303
+ }),
304
+ getActionAvailabilityState: ({ cfg, accountId }) => {
305
+ return isNativeDeliveryEnabled({
306
+ cfg,
307
+ accountId
308
+ }) ? { kind: "enabled" } : { kind: "disabled" };
309
+ },
310
+ getExecInitiatingSurfaceState: ({ cfg, accountId }) => {
311
+ return isNativeDeliveryEnabled({
312
+ cfg,
313
+ accountId
314
+ }) ? { kind: "enabled" } : { kind: "disabled" };
315
+ },
316
+ describeExecApprovalSetup: ({ accountId }) => {
317
+ return `QQBot native exec approvals are enabled by default. To restrict who can approve, configure \`${accountId && accountId !== "default" ? `channels.qqbot.accounts.${accountId}` : "channels.qqbot"}.execApprovals.approvers\` with QQ user OpenIDs.`;
318
+ },
319
+ delivery: {
320
+ hasConfiguredDmRoute: () => true,
321
+ shouldSuppressForwardingFallback: (input) => {
322
+ const channel = normalizeOptionalString(input.target?.channel);
323
+ if (channel !== "qqbot") return false;
324
+ const accountId = normalizeOptionalString(input.target?.accountId) ?? normalizeOptionalString(input.request?.request?.turnSourceAccountId);
325
+ const result = isNativeDeliveryEnabled({
326
+ cfg: input.cfg,
327
+ accountId
328
+ });
329
+ getBridgeLogger().debug?.(`[qqbot:approval] shouldSuppressForwardingFallback channel=${channel} accountId=${accountId} → ${result}`);
330
+ return result;
331
+ }
332
+ },
333
+ native: {
334
+ describeDeliveryCapabilities: ({ cfg, accountId }) => ({
335
+ enabled: isNativeDeliveryEnabled({
336
+ cfg,
337
+ accountId
338
+ }),
339
+ preferredSurface: "origin",
340
+ supportsOriginSurface: true,
341
+ supportsApproverDmSurface: false,
342
+ notifyOriginWhenDmOnly: false
343
+ }),
344
+ resolveOriginTarget: ({ request }) => {
345
+ const target = resolveApprovalTarget(request.request.sessionKey ?? null, request.request.turnSourceTo ?? null);
346
+ if (target) return { to: `${target.type}:${target.id}` };
347
+ const sessionConversation = resolveApprovalRequestSessionConversation({
348
+ request,
349
+ channel: "qqbot",
350
+ bundledFallback: true
351
+ });
352
+ if (sessionConversation?.id) return { to: `${sessionConversation.kind === "group" ? "group" : "c2c"}:${sessionConversation.id}` };
353
+ return null;
354
+ }
355
+ },
356
+ nativeRuntime: createLazyChannelApprovalNativeRuntimeAdapter({
357
+ eventKinds: ["exec", "plugin"],
358
+ isConfigured: ({ cfg, accountId }) => {
359
+ const result = isNativeDeliveryEnabled({
360
+ cfg,
361
+ accountId
362
+ });
363
+ getBridgeLogger().debug?.(`[qqbot:approval] nativeRuntime.isConfigured accountId=${accountId} → ${result}`);
364
+ return result;
365
+ },
366
+ shouldHandle: ({ cfg, accountId, request }) => {
367
+ const result = shouldHandleRequest({
368
+ cfg,
369
+ accountId,
370
+ request
371
+ });
372
+ getBridgeLogger().debug?.(`[qqbot:approval] nativeRuntime.shouldHandle accountId=${accountId} → ${result}`);
373
+ return result;
374
+ },
375
+ load: async () => {
376
+ ensurePlatformAdapter();
377
+ return (await import("./handler-runtime-CsNYWx6E.js")).qqbotApprovalNativeRuntime;
378
+ }
379
+ })
380
+ });
381
+ }
382
+ const qqbotApprovalCapability = createQQBotApprovalCapability();
383
+ let _cachedCapability;
384
+ function getQQBotApprovalCapability() {
385
+ _cachedCapability ??= qqbotApprovalCapability;
386
+ return _cachedCapability;
387
+ }
388
+ //#endregion
389
+ //#region extensions/qqbot/src/bridge/narrowing.ts
390
+ /**
391
+ * Map resolved plugin account to the engine gateway account shape (single assertion on nested config).
392
+ */
393
+ function toGatewayAccount(account) {
394
+ return {
395
+ accountId: account.accountId,
396
+ appId: account.appId,
397
+ clientSecret: account.clientSecret,
398
+ markdownSupport: account.markdownSupport,
399
+ systemPrompt: account.systemPrompt,
400
+ config: account.config
401
+ };
402
+ }
403
+ /**
404
+ * Persist OpenClaw config through the injected plugin runtime (typed entry point).
405
+ */
406
+ async function writeOpenClawConfigThroughRuntime(runtime, cfg) {
407
+ await runtime.config.replaceConfigFile({
408
+ nextConfig: cfg,
409
+ afterWrite: { mode: "auto" }
410
+ });
411
+ }
412
+ //#endregion
413
+ //#region extensions/qqbot/src/engine/utils/platform.ts
414
+ /**
415
+ * Cross-platform path and detection helpers for core/ modules.
416
+ *
417
+ * Provides home/data/media directory helpers, platform detection,
418
+ * silk-wasm availability checks — all without importing `openclaw/plugin-sdk`.
419
+ * The temp-directory fallback is delegated to the PlatformAdapter.
420
+ */
421
+ /**
422
+ * Resolve the current user's home directory safely across platforms.
423
+ *
424
+ * Priority:
425
+ * 1. `os.homedir()`
426
+ * 2. `$HOME` or `%USERPROFILE%`
427
+ * 3. PlatformAdapter.getTempDir() as a last resort
428
+ */
429
+ function getHomeDir() {
430
+ try {
431
+ const home = os$1.homedir();
432
+ if (home && fs$1.existsSync(home)) return home;
433
+ } catch {}
434
+ const envHome = process.env.HOME || process.env.USERPROFILE;
435
+ if (envHome && fs$1.existsSync(envHome)) return envHome;
436
+ return getPlatformAdapter().getTempDir();
437
+ }
438
+ /** Return a path under `~/.openclaw/qqbot` without creating it. */
439
+ function getQQBotDataPath(...subPaths) {
440
+ return path$1.join(getHomeDir(), ".openclaw", "qqbot", ...subPaths);
441
+ }
442
+ /** Return a path under `~/.openclaw/qqbot`, creating it on demand. */
443
+ function getQQBotDataDir(...subPaths) {
444
+ const dir = getQQBotDataPath(...subPaths);
445
+ if (!fs$1.existsSync(dir)) fs$1.mkdirSync(dir, { recursive: true });
446
+ return dir;
447
+ }
448
+ /**
449
+ * Return a path under `~/.openclaw/media/qqbot` without creating it.
450
+ *
451
+ * Unlike `getQQBotDataPath`, this lives under OpenClaw's core media allowlist so
452
+ * downloaded images and audio can be accessed by framework media tooling.
453
+ */
454
+ function getQQBotMediaPath(...subPaths) {
455
+ return path$1.join(getHomeDir(), ".openclaw", "media", "qqbot", ...subPaths);
456
+ }
457
+ /** Return a path under `~/.openclaw/media/qqbot`, creating it on demand. */
458
+ function getQQBotMediaDir(...subPaths) {
459
+ const dir = getQQBotMediaPath(...subPaths);
460
+ if (!fs$1.existsSync(dir)) fs$1.mkdirSync(dir, { recursive: true });
461
+ return dir;
462
+ }
463
+ /**
464
+ * Return `~/.openclaw/media`, OpenClaw's shared media root.
465
+ *
466
+ * This mirrors the directory that core's `buildMediaLocalRoots` exposes as an
467
+ * allowlisted location (see `openclaw/src/media/local-roots.ts`). Using it as a
468
+ * QQ Bot payload root lets the plugin trust framework-produced files that live
469
+ * in sibling subdirectories such as `outbound/` (written by
470
+ * `saveMediaBuffer(..., "outbound", ...)`) or `inbound/`, while still keeping
471
+ * the check anchored to a single, well-known directory.
472
+ */
473
+ function getOpenClawMediaDir() {
474
+ return path$1.join(getHomeDir(), ".openclaw", "media");
475
+ }
476
+ function isWindows() {
477
+ return process.platform === "win32";
478
+ }
479
+ /** Return the preferred temporary directory. */
480
+ function getTempDir() {
481
+ return getPlatformAdapter().getTempDir();
482
+ }
483
+ let _silkWasmAvailable = null;
484
+ /** Check whether silk-wasm can run in the current environment. */
485
+ async function checkSilkWasmAvailable() {
486
+ if (_silkWasmAvailable !== null) return _silkWasmAvailable;
487
+ try {
488
+ const { isSilk } = await import("silk-wasm");
489
+ isSilk(new Uint8Array(0));
490
+ _silkWasmAvailable = true;
491
+ debugLog("[platform] silk-wasm: available");
492
+ } catch (err) {
493
+ _silkWasmAvailable = false;
494
+ debugWarn(`[platform] silk-wasm: NOT available (${formatErrorMessage(err)})`);
495
+ }
496
+ return _silkWasmAvailable;
497
+ }
498
+ /** Expand `~` to the current user's home directory. */
499
+ function expandTilde(p) {
500
+ if (!p) return p;
501
+ if (p === "~") return getHomeDir();
502
+ if (p.startsWith("~/") || p.startsWith("~\\")) return path$1.join(getHomeDir(), p.slice(2));
503
+ return p;
504
+ }
505
+ /** Normalize a user-provided path by trimming, stripping `file://`, and expanding `~`. */
506
+ function normalizePath(p) {
507
+ let result = p.trim();
508
+ if (result.startsWith("file://")) {
509
+ result = result.slice(7);
510
+ try {
511
+ result = decodeURIComponent(result);
512
+ } catch {}
513
+ }
514
+ return expandTilde(result);
515
+ }
516
+ /** Return true when the string looks like a local filesystem path rather than a URL. */
517
+ function isLocalPath(p) {
518
+ if (!p) return false;
519
+ if (p.startsWith("file://")) return true;
520
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) return true;
521
+ if (p.startsWith("/")) return true;
522
+ if (/^[a-zA-Z]:[\\/]/.test(p)) return true;
523
+ if (p.startsWith("\\\\")) return true;
524
+ if (p.startsWith("./") || p.startsWith("../")) return true;
525
+ if (p.startsWith(".\\") || p.startsWith("..\\")) return true;
526
+ return false;
527
+ }
528
+ function isPathWithinRoot(candidate, root) {
529
+ const relative = path$1.relative(root, candidate);
530
+ return relative === "" || !relative.startsWith("..") && !path$1.isAbsolute(relative);
531
+ }
532
+ /** Remap legacy or hallucinated QQ Bot local media paths to real files when possible. */
533
+ function resolveQQBotLocalMediaPath(p) {
534
+ const normalized = normalizePath(p);
535
+ if (!isLocalPath(normalized) || fs$1.existsSync(normalized)) return normalized;
536
+ const homeDir = getHomeDir();
537
+ const mediaRoot = getQQBotMediaPath();
538
+ const dataRoot = getQQBotDataPath();
539
+ const candidateRoots = [
540
+ {
541
+ from: path$1.join(homeDir, ".openclaw", "workspace", "qqbot"),
542
+ to: mediaRoot
543
+ },
544
+ {
545
+ from: dataRoot,
546
+ to: mediaRoot
547
+ },
548
+ {
549
+ from: mediaRoot,
550
+ to: dataRoot
551
+ }
552
+ ];
553
+ for (const { from, to } of candidateRoots) {
554
+ if (!isPathWithinRoot(normalized, from)) continue;
555
+ const relative = path$1.relative(from, normalized);
556
+ const candidate = path$1.join(to, relative);
557
+ if (fs$1.existsSync(candidate)) {
558
+ debugWarn(`[platform] Remapped missing QQBot media path ${normalized} -> ${candidate}`);
559
+ return candidate;
560
+ }
561
+ }
562
+ return normalized;
563
+ }
564
+ /**
565
+ * Resolve a structured-payload local file path and enforce that it stays within
566
+ * QQ Bot-owned storage roots.
567
+ */
568
+ function resolveQQBotPayloadLocalFilePath(p) {
569
+ const candidate = resolveQQBotLocalMediaPath(p);
570
+ if (!candidate.trim()) return null;
571
+ const resolvedCandidate = path$1.resolve(candidate);
572
+ if (!fs$1.existsSync(resolvedCandidate)) return null;
573
+ const canonicalCandidate = fs$1.realpathSync(resolvedCandidate);
574
+ const allowedRoots = [getOpenClawMediaDir(), getQQBotMediaPath()];
575
+ for (const root of allowedRoots) {
576
+ const resolvedRoot = path$1.resolve(root);
577
+ if (isPathWithinRoot(canonicalCandidate, fs$1.existsSync(resolvedRoot) ? fs$1.realpathSync(resolvedRoot) : resolvedRoot)) return canonicalCandidate;
578
+ }
579
+ return null;
580
+ }
581
+ //#endregion
582
+ //#region extensions/qqbot/src/engine/utils/data-paths.ts
583
+ /**
584
+ * Centralised filename helpers for persisted QQBot state.
585
+ *
586
+ * Every persistence module routes file paths through these helpers so the
587
+ * naming convention stays in sync and legacy migrations are handled
588
+ * consistently.
589
+ *
590
+ * Key design decisions:
591
+ * - Credential backup is keyed only by `accountId` because recovery runs
592
+ * exactly when the appId is missing from config.
593
+ */
594
+ /**
595
+ * Normalise an identifier so it is safe to embed in a filename.
596
+ * Keeps alphanumerics, dot, underscore, dash; everything else becomes `_`.
597
+ */
598
+ function safeName(id) {
599
+ return id.replace(/[^a-zA-Z0-9._-]/g, "_");
600
+ }
601
+ /**
602
+ * Per-accountId credential backup file. Not keyed by appId because the
603
+ * whole point of this file is to recover credentials when appId is
604
+ * missing from the live config.
605
+ */
606
+ function getCredentialBackupFile(accountId) {
607
+ return path.join(getQQBotDataPath("data"), `credential-backup-${safeName(accountId)}.json`);
608
+ }
609
+ /** Legacy single-file credential backup (pre-multi-account-isolation). */
610
+ function getLegacyCredentialBackupFile() {
611
+ return path.join(getQQBotDataPath("data"), "credential-backup.json");
612
+ }
613
+ //#endregion
614
+ //#region extensions/qqbot/src/engine/config/credential-backup.ts
615
+ /**
616
+ * Credential backup & recovery.
617
+ * 凭证暂存与恢复。
618
+ *
619
+ * Solves the "hot-upgrade interrupted, appId/secret vanished from
620
+ * openclaw.json" failure mode.
621
+ *
622
+ * Mechanics:
623
+ * - After each successful gateway start we snapshot the currently
624
+ * resolved `appId` / `clientSecret` to a per-account backup file.
625
+ * - During plugin startup, if the live config has an empty appId or
626
+ * secret, the gateway consults the backup and restores the values
627
+ * via the config mutation API.
628
+ * - Backups live under `~/.openclaw/qqbot/data/` so they survive
629
+ * plugin directory replacement.
630
+ *
631
+ * Safety notes:
632
+ * - Only restore when credentials are **actually empty** — never
633
+ * overwrite a user's intentional config change.
634
+ * - Atomic write (temp file + rename) to avoid torn files.
635
+ * - Per-account file: `credential-backup-<accountId>.json`. We do
636
+ * **not** also key by appId because recovery happens precisely
637
+ * when appId is unknown.
638
+ * - Legacy single `credential-backup.json` is migrated automatically
639
+ * when the stored accountId matches the caller.
640
+ */
641
+ /** Persist a credential snapshot (called once gateway reaches READY). */
642
+ function saveCredentialBackup(accountId, appId, clientSecret) {
643
+ if (!appId || !clientSecret) return;
644
+ try {
645
+ const backupPath = getCredentialBackupFile(accountId);
646
+ const data = {
647
+ accountId,
648
+ appId,
649
+ clientSecret,
650
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
651
+ };
652
+ replaceFileAtomicSync({
653
+ filePath: backupPath,
654
+ content: `${JSON.stringify(data, null, 2)}\n`,
655
+ tempPrefix: ".qqbot-credential-backup"
656
+ });
657
+ } catch {}
658
+ }
659
+ /**
660
+ * Load a credential snapshot for `accountId`.
661
+ *
662
+ * Consults the new per-account file first; falls back to the legacy
663
+ * global backup file and migrates it when the embedded `accountId`
664
+ * matches the request. Returns `null` when no usable backup exists.
665
+ */
666
+ function loadCredentialBackup(accountId) {
667
+ try {
668
+ if (accountId) {
669
+ const data = loadJsonFile(getCredentialBackupFile(accountId));
670
+ if (data?.appId && data.clientSecret) return data;
671
+ }
672
+ const legacy = getLegacyCredentialBackupFile();
673
+ const data = loadJsonFile(legacy);
674
+ if (data) {
675
+ if (!data?.appId || !data?.clientSecret) return null;
676
+ if (accountId && data.accountId !== accountId) return null;
677
+ if (data.accountId) try {
678
+ replaceFileAtomicSync({
679
+ filePath: getCredentialBackupFile(data.accountId),
680
+ content: `${JSON.stringify(data, null, 2)}\n`,
681
+ tempPrefix: ".qqbot-credential-backup"
682
+ });
683
+ fs.unlinkSync(legacy);
684
+ } catch {}
685
+ return data;
686
+ }
687
+ } catch {}
688
+ return null;
689
+ }
690
+ //#endregion
691
+ //#region extensions/qqbot/src/engine/config/credentials.ts
692
+ /**
693
+ * QQBot credential management (pure logic layer).
694
+ * QQBot 凭证管理(纯逻辑层)。
695
+ *
696
+ * Credential clearing and field-level cleanup for logout and setup
697
+ * flows. All functions operate on plain objects (Record<string, unknown>)
698
+ * and stay framework-agnostic.
699
+ */
700
+ /**
701
+ * Remove clientSecret / clientSecretFile from a QQBot account config.
702
+ *
703
+ * Returns a shallow-cloned config with credentials removed, plus flags
704
+ * indicating whether anything actually changed.
705
+ */
706
+ function clearAccountCredentials(cfg, accountId) {
707
+ const nextCfg = { ...cfg };
708
+ const channels = asOptionalObjectRecord(cfg.channels);
709
+ const nextQQBot = channels?.qqbot ? { ...asOptionalObjectRecord(channels.qqbot) } : void 0;
710
+ let cleared = false;
711
+ let changed = false;
712
+ if (nextQQBot) {
713
+ const qqbot = nextQQBot;
714
+ if (accountId === "default") {
715
+ if (qqbot.clientSecret) {
716
+ delete qqbot.clientSecret;
717
+ cleared = true;
718
+ changed = true;
719
+ }
720
+ if (qqbot.clientSecretFile) {
721
+ delete qqbot.clientSecretFile;
722
+ cleared = true;
723
+ changed = true;
724
+ }
725
+ }
726
+ const accounts = qqbot.accounts;
727
+ if (accounts && accountId in accounts) {
728
+ const entry = accounts[accountId];
729
+ if (entry && "clientSecret" in entry) {
730
+ delete entry.clientSecret;
731
+ cleared = true;
732
+ changed = true;
733
+ }
734
+ if (entry && "clientSecretFile" in entry) {
735
+ delete entry.clientSecretFile;
736
+ cleared = true;
737
+ changed = true;
738
+ }
739
+ if (entry && Object.keys(entry).length === 0) {
740
+ delete accounts[accountId];
741
+ changed = true;
742
+ }
743
+ }
744
+ }
745
+ if (changed && nextQQBot) nextCfg.channels = {
746
+ ...channels,
747
+ qqbot: nextQQBot
748
+ };
749
+ return {
750
+ nextCfg,
751
+ cleared,
752
+ changed
753
+ };
754
+ }
755
+ //#endregion
756
+ //#region extensions/qqbot/src/engine/messaging/target-parser.ts
757
+ /**
758
+ * Parse a qqbot target string into a structured delivery target.
759
+ *
760
+ * Supported formats:
761
+ * - `qqbot:c2c:openid` → C2C direct message
762
+ * - `qqbot:group:groupid` → Group message
763
+ * - `qqbot:channel:channelid` → Channel message
764
+ * - `c2c:openid` → C2C (without qqbot: prefix)
765
+ * - `group:groupid` → Group (without qqbot: prefix)
766
+ * - `channel:channelid` → Channel (without qqbot: prefix)
767
+ * - `openid` → C2C (bare openid, default)
768
+ *
769
+ * @param to - Raw target string.
770
+ * @returns Parsed target with type and id.
771
+ * @throws {Error} When the target format is invalid.
772
+ */
773
+ function parseTarget(to) {
774
+ let id = to.replace(/^qqbot:/i, "");
775
+ if (id.startsWith("c2c:")) {
776
+ const userId = id.slice(4);
777
+ if (!userId) throw new Error(`Invalid c2c target format: ${to} - missing user ID`);
778
+ return {
779
+ type: "c2c",
780
+ id: userId
781
+ };
782
+ }
783
+ if (id.startsWith("group:")) {
784
+ const groupId = id.slice(6);
785
+ if (!groupId) throw new Error(`Invalid group target format: ${to} - missing group ID`);
786
+ return {
787
+ type: "group",
788
+ id: groupId
789
+ };
790
+ }
791
+ if (id.startsWith("channel:")) {
792
+ const channelId = id.slice(8);
793
+ if (!channelId) throw new Error(`Invalid channel target format: ${to} - missing channel ID`);
794
+ return {
795
+ type: "channel",
796
+ id: channelId
797
+ };
798
+ }
799
+ if (!id) throw new Error(`Invalid target format: ${to} - empty ID after removing qqbot: prefix`);
800
+ return {
801
+ type: "c2c",
802
+ id
803
+ };
804
+ }
805
+ /**
806
+ * Normalize a QQ Bot target string into the canonical `qqbot:...` form.
807
+ *
808
+ * Returns `undefined` when the target does not look like a QQ Bot address.
809
+ */
810
+ function normalizeTarget(target) {
811
+ const id = target.replace(/^qqbot:/i, "");
812
+ if (id.startsWith("c2c:") || id.startsWith("group:") || id.startsWith("channel:")) return `qqbot:${id}`;
813
+ if (/^[0-9a-fA-F]{32}$/.test(id)) return `qqbot:c2c:${id}`;
814
+ if (/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id)) return `qqbot:c2c:${id}`;
815
+ }
816
+ /**
817
+ * Return true when the string looks like a QQ Bot target ID.
818
+ */
819
+ function looksLikeQQBotTarget(id) {
820
+ if (/^qqbot:(c2c|group|channel):/i.test(id)) return true;
821
+ if (/^(c2c|group|channel):/i.test(id)) return true;
822
+ if (/^[0-9a-fA-F]{32}$/.test(id)) return true;
823
+ return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(id);
824
+ }
825
+ //#endregion
826
+ //#region extensions/qqbot/src/channel.ts
827
+ let gatewayModulePromise;
828
+ function loadGatewayModule() {
829
+ gatewayModulePromise ??= import("./gateway-pvpMZ-Sn.js");
830
+ return gatewayModulePromise;
831
+ }
832
+ function createQQBotSendReceipt(params) {
833
+ const messageId = params.messageId?.trim();
834
+ return createMessageReceiptFromOutboundResults({
835
+ results: messageId ? [{
836
+ channel: "qqbot",
837
+ messageId,
838
+ conversationId: params.target
839
+ }] : [],
840
+ threadId: params.target,
841
+ kind: params.kind
842
+ });
843
+ }
844
+ async function sendQQBotText(params) {
845
+ await loadGatewayModule();
846
+ const account = resolveQQBotAccount(params.cfg, params.accountId);
847
+ const { sendText } = await import("./outbound-DekhyR4u.js").then((n) => n.t);
848
+ const result = await sendText({
849
+ to: params.to,
850
+ text: params.text,
851
+ accountId: params.accountId,
852
+ replyToId: params.replyToId,
853
+ account: toGatewayAccount(account)
854
+ });
855
+ return {
856
+ channel: "qqbot",
857
+ messageId: result.messageId ?? "",
858
+ receipt: createQQBotSendReceipt({
859
+ messageId: result.messageId,
860
+ target: params.to,
861
+ kind: "text"
862
+ }),
863
+ meta: result.error ? { error: result.error } : void 0
864
+ };
865
+ }
866
+ async function sendQQBotMedia(params) {
867
+ await loadGatewayModule();
868
+ const account = resolveQQBotAccount(params.cfg, params.accountId);
869
+ const { sendMedia } = await import("./outbound-DekhyR4u.js").then((n) => n.t);
870
+ const result = await sendMedia({
871
+ to: params.to,
872
+ text: params.text ?? "",
873
+ mediaUrl: params.mediaUrl ?? "",
874
+ accountId: params.accountId,
875
+ replyToId: params.replyToId,
876
+ account: toGatewayAccount(account)
877
+ });
878
+ return {
879
+ channel: "qqbot",
880
+ messageId: result.messageId ?? "",
881
+ receipt: createQQBotSendReceipt({
882
+ messageId: result.messageId,
883
+ target: params.to,
884
+ kind: "media"
885
+ }),
886
+ meta: result.error ? { error: result.error } : void 0
887
+ };
888
+ }
889
+ function toQQBotMessageSendResult(result) {
890
+ return {
891
+ messageId: result.messageId,
892
+ receipt: result.receipt
893
+ };
894
+ }
895
+ const qqbotMessageAdapter = defineChannelMessageAdapter({
896
+ id: "qqbot",
897
+ durableFinal: { capabilities: {
898
+ text: true,
899
+ media: true,
900
+ replyTo: true
901
+ } },
902
+ send: {
903
+ text: async (ctx) => toQQBotMessageSendResult(await sendQQBotText({
904
+ cfg: ctx.cfg,
905
+ to: ctx.to,
906
+ text: ctx.text,
907
+ accountId: ctx.accountId,
908
+ replyToId: ctx.replyToId
909
+ })),
910
+ media: async (ctx) => toQQBotMessageSendResult(await sendQQBotMedia({
911
+ cfg: ctx.cfg,
912
+ to: ctx.to,
913
+ text: ctx.text,
914
+ mediaUrl: ctx.mediaUrl,
915
+ accountId: ctx.accountId,
916
+ replyToId: ctx.replyToId
917
+ }))
918
+ }
919
+ });
920
+ const EXEC_APPROVAL_COMMAND_RE = /\/approve(?:@[^\s]+)?\s+[A-Za-z0-9][A-Za-z0-9._:-]*\s+(?:allow-once|allow-always|always|deny)\b/i;
921
+ function persistAccountCredentialSnapshot(account) {
922
+ if (account.appId && account.clientSecret) saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
923
+ }
924
+ function shouldSuppressLocalQQBotApprovalPrompt(params) {
925
+ if (params.hint?.kind !== "approval-pending" || params.hint.approvalKind !== "exec") return false;
926
+ const account = resolveQQBotAccount(params.cfg, params.accountId);
927
+ if (!account.enabled || account.secretSource === "none") return false;
928
+ if (getExecApprovalReplyMetadata(params.payload)) return true;
929
+ const text = typeof params.payload.text === "string" ? params.payload.text : "";
930
+ return EXEC_APPROVAL_COMMAND_RE.test(text);
931
+ }
932
+ const qqbotPlugin = {
933
+ id: "qqbot",
934
+ setupWizard: qqbotSetupWizard,
935
+ meta: { ...qqbotMeta },
936
+ capabilities: {
937
+ chatTypes: ["direct", "group"],
938
+ media: true,
939
+ reactions: false,
940
+ threads: false,
941
+ blockStreaming: true
942
+ },
943
+ reload: { configPrefixes: ["channels.qqbot"] },
944
+ configSchema: qqbotChannelConfigSchema,
945
+ config: {
946
+ ...qqbotConfigAdapter,
947
+ /**
948
+ * Treat an account as configured when either the live config has
949
+ * credentials OR a recoverable credential backup exists. This mirrors
950
+ * the standalone plugin and lets the gateway survive a hot upgrade
951
+ * that wiped openclaw.json mid-flight.
952
+ */
953
+ isConfigured: (account) => {
954
+ if (qqbotConfigAdapter.isConfigured(account)) return true;
955
+ if (!account) return false;
956
+ const backup = loadCredentialBackup(account.accountId);
957
+ return Boolean(backup?.appId && backup?.clientSecret);
958
+ }
959
+ },
960
+ setup: { ...qqbotSetupAdapterShared },
961
+ approvalCapability: getQQBotApprovalCapability(),
962
+ message: qqbotMessageAdapter,
963
+ messaging: {
964
+ targetPrefixes: ["qqbot"],
965
+ /** Normalize common QQ Bot target formats into the canonical qqbot:... form. */
966
+ normalizeTarget,
967
+ targetResolver: {
968
+ /** Return true when the id looks like a QQ Bot target. */
969
+ looksLikeId: looksLikeQQBotTarget,
970
+ hint: "QQ Bot target format: qqbot:c2c:openid (direct) or qqbot:group:groupid (group)"
971
+ }
972
+ },
973
+ outbound: {
974
+ deliveryMode: "direct",
975
+ chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
976
+ chunkerMode: "markdown",
977
+ textChunkLimit: 5e3,
978
+ shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload, hint }) => shouldSuppressLocalQQBotApprovalPrompt({
979
+ cfg,
980
+ accountId,
981
+ payload,
982
+ hint
983
+ }),
984
+ sendText: async ({ to, text, accountId, replyToId, cfg }) => await sendQQBotText({
985
+ cfg,
986
+ to,
987
+ text,
988
+ accountId,
989
+ replyToId
990
+ }),
991
+ sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => await sendQQBotMedia({
992
+ cfg,
993
+ to,
994
+ text,
995
+ mediaUrl,
996
+ accountId,
997
+ replyToId
998
+ })
999
+ },
1000
+ gateway: {
1001
+ startAccount: async (ctx) => {
1002
+ let { account, cfg } = ctx;
1003
+ const { abortSignal, log } = ctx;
1004
+ if (!account.appId || !account.clientSecret) {
1005
+ const backup = loadCredentialBackup(account.accountId);
1006
+ if (backup?.appId && backup?.clientSecret) try {
1007
+ const nextCfg = applyQQBotAccountConfig(cfg, account.accountId, {
1008
+ appId: backup.appId,
1009
+ clientSecret: backup.clientSecret
1010
+ });
1011
+ await writeOpenClawConfigThroughRuntime(getQQBotRuntime(), nextCfg);
1012
+ cfg = nextCfg;
1013
+ account = resolveQQBotAccount(nextCfg, account.accountId);
1014
+ log?.info(`[qqbot:${account.accountId}] Restored credentials from backup (appId=${account.appId})`);
1015
+ } catch (err) {
1016
+ log?.error(`[qqbot:${account.accountId}] Failed to restore credentials from backup: ${err instanceof Error ? err.message : String(err)}`);
1017
+ }
1018
+ }
1019
+ const { startGateway } = await loadGatewayModule();
1020
+ log?.info(`[qqbot:${account.accountId}] Starting gateway — appId=${account.appId}, enabled=${account.enabled}, name=${account.name ?? "unnamed"}`);
1021
+ await startGateway({
1022
+ account,
1023
+ abortSignal,
1024
+ cfg,
1025
+ log,
1026
+ channelRuntime: ctx.channelRuntime,
1027
+ onReady: () => {
1028
+ log?.info(`[qqbot:${account.accountId}] Gateway ready`);
1029
+ ctx.setStatus({
1030
+ ...ctx.getStatus(),
1031
+ running: true,
1032
+ connected: true,
1033
+ lastConnectedAt: Date.now()
1034
+ });
1035
+ persistAccountCredentialSnapshot(account);
1036
+ },
1037
+ onResumed: () => {
1038
+ log?.info(`[qqbot:${account.accountId}] Gateway resumed`);
1039
+ ctx.setStatus({
1040
+ ...ctx.getStatus(),
1041
+ running: true,
1042
+ connected: true,
1043
+ lastConnectedAt: Date.now()
1044
+ });
1045
+ persistAccountCredentialSnapshot(account);
1046
+ },
1047
+ onError: (error) => {
1048
+ log?.error(`[qqbot:${account.accountId}] Gateway error: ${error.message}`);
1049
+ ctx.setStatus({
1050
+ ...ctx.getStatus(),
1051
+ lastError: error.message
1052
+ });
1053
+ }
1054
+ });
1055
+ },
1056
+ logoutAccount: async ({ accountId, cfg }) => {
1057
+ const { nextCfg, cleared, changed } = clearAccountCredentials(cfg, accountId);
1058
+ if (changed) await writeOpenClawConfigThroughRuntime(getQQBotRuntime(), nextCfg);
1059
+ const loggedOut = resolveQQBotAccount(changed ? nextCfg : cfg, accountId).secretSource === "none";
1060
+ return {
1061
+ ok: true,
1062
+ cleared,
1063
+ envToken: Boolean(process.env.QQBOT_CLIENT_SECRET),
1064
+ loggedOut
1065
+ };
1066
+ }
1067
+ },
1068
+ status: {
1069
+ defaultRuntime: {
1070
+ accountId: DEFAULT_ACCOUNT_ID$1,
1071
+ running: false,
1072
+ connected: false,
1073
+ lastConnectedAt: null,
1074
+ lastError: null,
1075
+ lastInboundAt: null,
1076
+ lastOutboundAt: null
1077
+ },
1078
+ buildChannelSummary: ({ snapshot }) => ({
1079
+ configured: snapshot.configured ?? false,
1080
+ tokenSource: snapshot.tokenSource ?? "none",
1081
+ running: snapshot.running ?? false,
1082
+ connected: snapshot.connected ?? false,
1083
+ lastConnectedAt: snapshot.lastConnectedAt ?? null,
1084
+ lastError: snapshot.lastError ?? null
1085
+ }),
1086
+ buildAccountSnapshot: ({ account, runtime }) => ({
1087
+ accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID$1,
1088
+ name: account?.name,
1089
+ enabled: account?.enabled ?? false,
1090
+ configured: Boolean(account?.appId && account?.clientSecret),
1091
+ tokenSource: account?.secretSource,
1092
+ running: runtime?.running ?? false,
1093
+ connected: runtime?.connected ?? false,
1094
+ lastConnectedAt: runtime?.lastConnectedAt ?? null,
1095
+ lastError: runtime?.lastError ?? null,
1096
+ lastInboundAt: runtime?.lastInboundAt ?? null,
1097
+ lastOutboundAt: runtime?.lastOutboundAt ?? null
1098
+ })
1099
+ }
1100
+ };
1101
+ //#endregion
1102
+ export { parseApprovalButtonData as C, buildPluginApprovalText as S, matchesQQBotApprovalAccount as _, getQQBotDataDir as a, buildApprovalKeyboard as b, getQQBotMediaPath as c, isWindows as d, normalizePath as f, isQQBotExecApprovalClientEnabled as g, authorizeQQBotApprovalAction as h, getHomeDir as i, getTempDir as l, toGatewayAccount as m, parseTarget as n, getQQBotDataPath as o, resolveQQBotPayloadLocalFilePath as p, checkSilkWasmAvailable as r, getQQBotMediaDir as s, qqbotPlugin as t, isLocalPath as u, resolveQQBotExecApprovalConfig as v, resolveApprovalTarget as w, buildExecApprovalText as x, shouldHandleQQBotExecApprovalRequest as y };