@openclaw/qqbot 2026.6.5-beta.2 → 2026.6.5-beta.3

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.
@@ -1,6 +1,6 @@
1
1
  import { c as getPlatformAdapter, i as normalizeOptionalString, s as sanitizeFileName } from "./string-normalize-R_0cKO7Q.js";
2
+ import { a as formatErrorMessage, n as debugLog, r as debugWarn, t as debugError } from "./log-SDfMMBWe.js";
2
3
  import * as fs$1 from "node:fs";
3
- import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
4
4
  import os from "node:os";
5
5
  import * as crypto$1 from "node:crypto";
6
6
  import crypto from "node:crypto";
@@ -10,6 +10,18 @@ import * as path$1 from "node:path";
10
10
  import { mimeTypeFromFilePath } from "openclaw/plugin-sdk/media-mime";
11
11
  import { readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
12
12
  import { asDateTimestampMs, isFutureDateTimestampMs, parseStrictPositiveInteger, resolveExpiresAtMsFromDurationSeconds, resolveTimestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
13
+ //#region \0rolldown/runtime.js
14
+ var __defProp = Object.defineProperty;
15
+ var __exportAll = (all, no_symbols) => {
16
+ let target = {};
17
+ for (var name in all) __defProp(target, name, {
18
+ get: all[name],
19
+ enumerable: true
20
+ });
21
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
22
+ return target;
23
+ };
24
+ //#endregion
13
25
  //#region extensions/qqbot/src/engine/types.ts
14
26
  /**
15
27
  * Core API layer public types.
@@ -46,60 +58,6 @@ const StreamInputState = {
46
58
  /** Stream message content type. */
47
59
  const StreamContentType = { MARKDOWN: "markdown" };
48
60
  //#endregion
49
- //#region extensions/qqbot/src/engine/utils/format.ts
50
- /**
51
- * General formatting and string utilities.
52
- * 通用格式化与字符串工具。
53
- *
54
- * Pure utility functions with zero external dependencies.
55
- * Replaces `openclaw/plugin-sdk/error-runtime` and `text-runtime`
56
- * helpers for use inside engine/.
57
- *
58
- * NOTE: The framework `formatErrorMessage` also applies `redactSensitiveText()`
59
- * for token masking. We intentionally omit that here — the framework's log
60
- * pipeline handles redaction at a higher level.
61
- */
62
- /**
63
- * Format any error object into a readable string.
64
- * 将任意错误对象格式化为可读字符串。
65
- *
66
- * Traverses the `.cause` chain for nested Error objects to include
67
- * the full error context (e.g. network errors wrapped inside HTTP errors).
68
- */
69
- function formatErrorMessage(err) {
70
- if (err instanceof Error) {
71
- let formatted = err.message || err.name || "Error";
72
- let cause = err.cause;
73
- const seen = new Set([err]);
74
- while (cause && !seen.has(cause)) {
75
- seen.add(cause);
76
- if (cause instanceof Error) {
77
- if (cause.message) formatted += ` | ${cause.message}`;
78
- cause = cause.cause;
79
- } else if (typeof cause === "string") {
80
- formatted += ` | ${cause}`;
81
- break;
82
- } else break;
83
- }
84
- return formatted;
85
- }
86
- if (typeof err === "string") return err;
87
- if (err === null || err === void 0 || typeof err === "number" || typeof err === "boolean" || typeof err === "bigint") return String(err);
88
- try {
89
- return JSON.stringify(err);
90
- } catch {
91
- return Object.prototype.toString.call(err);
92
- }
93
- }
94
- /** Format a millisecond duration into a human-readable string (e.g. "5m 30s"). */
95
- function formatDuration(durationMs) {
96
- const seconds = Math.round(durationMs / 1e3);
97
- if (seconds < 60) return `${seconds}s`;
98
- const minutes = Math.floor(seconds / 60);
99
- const remainSeconds = seconds % 60;
100
- return remainSeconds > 0 ? `${minutes}m ${remainSeconds}s` : `${minutes}m`;
101
- }
102
- //#endregion
103
61
  //#region extensions/qqbot/src/engine/api/api-client.ts
104
62
  /**
105
63
  * Core HTTP client for the QQ Open Platform REST API.
@@ -1562,58 +1520,6 @@ var MessageApi = class {
1562
1520
  }
1563
1521
  };
1564
1522
  //#endregion
1565
- //#region extensions/qqbot/src/engine/utils/log.ts
1566
- /**
1567
- * QQBot debug logging utilities.
1568
- * QQBot 调试日志工具。
1569
- *
1570
- * Only outputs when the QQBOT_DEBUG environment variable is set,
1571
- * preventing user message content from leaking in production logs.
1572
- *
1573
- * Self-contained within engine/ — no framework SDK dependency.
1574
- */
1575
- function isQqbotDebugEnabled() {
1576
- const value = process.env.QQBOT_DEBUG;
1577
- if (typeof value !== "string") return false;
1578
- switch (value.trim().toLowerCase()) {
1579
- case "1":
1580
- case "on":
1581
- case "true":
1582
- case "yes": return true;
1583
- default: return false;
1584
- }
1585
- }
1586
- const isDebug = () => isQqbotDebugEnabled();
1587
- const MAX_LOG_VALUE_CHARS = 4096;
1588
- function sanitizeDebugLogValue(value) {
1589
- let text;
1590
- if (typeof value === "string") text = value;
1591
- else if (value instanceof Error) text = value.stack || value.message;
1592
- else try {
1593
- text = JSON.stringify(value) ?? String(value);
1594
- } catch {
1595
- text = String(value);
1596
- }
1597
- const sanitized = text.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").trim();
1598
- if (sanitized.length <= MAX_LOG_VALUE_CHARS) return sanitized;
1599
- return `${sanitized.slice(0, MAX_LOG_VALUE_CHARS)}...`;
1600
- }
1601
- function formatDebugLogArgs(args) {
1602
- return args.map(sanitizeDebugLogValue).join(" ");
1603
- }
1604
- /** Debug-level log; only outputs when QQBOT_DEBUG is enabled. */
1605
- function debugLog(...args) {
1606
- if (isDebug()) console.log(formatDebugLogArgs(args).replace(/\n|\r/g, ""));
1607
- }
1608
- /** Debug-level warning; only outputs when QQBOT_DEBUG is enabled. */
1609
- function debugWarn(...args) {
1610
- if (isDebug()) console.warn(formatDebugLogArgs(args).replace(/\n|\r/g, ""));
1611
- }
1612
- /** Debug-level error; only outputs when QQBOT_DEBUG is enabled. */
1613
- function debugError(...args) {
1614
- if (isDebug()) console.error(formatDebugLogArgs(args).replace(/\n|\r/g, ""));
1615
- }
1616
- //#endregion
1617
1523
  //#region extensions/qqbot/src/engine/utils/upload-cache.ts
1618
1524
  /**
1619
1525
  * Cache `file_info` values returned by the QQ Bot API so identical uploads can be reused
@@ -1693,6 +1599,28 @@ function setCachedFileInfo(contentHash, scope, targetId, fileType, fileInfo, fil
1693
1599
  * always go through exported functions that resolve the correct
1694
1600
  * `AccountContext` by appId.
1695
1601
  */
1602
+ var sender_exports = /* @__PURE__ */ __exportAll({
1603
+ accountToCreds: () => accountToCreds,
1604
+ acknowledgeInteraction: () => acknowledgeInteraction,
1605
+ buildDeliveryTarget: () => buildDeliveryTarget,
1606
+ clearTokenCache: () => clearTokenCache,
1607
+ createRawInputNotifyFn: () => createRawInputNotifyFn,
1608
+ getAccessToken: () => getAccessToken,
1609
+ getGatewayUrl: () => getGatewayUrl,
1610
+ getMessageApi: () => getMessageApi,
1611
+ getPluginUserAgent: () => getPluginUserAgent,
1612
+ initApiConfig: () => initApiConfig,
1613
+ initSender: () => initSender,
1614
+ onMessageSent: () => onMessageSent,
1615
+ registerAccount: () => registerAccount,
1616
+ sendInputNotify: () => sendInputNotify,
1617
+ sendMedia: () => sendMedia,
1618
+ sendText: () => sendText,
1619
+ setOpenClawVersion: () => setOpenClawVersion,
1620
+ startBackgroundTokenRefresh: () => startBackgroundTokenRefresh,
1621
+ stopBackgroundTokenRefresh: () => stopBackgroundTokenRefresh,
1622
+ withTokenRetry: () => withTokenRetry
1623
+ });
1696
1624
  let pluginVersion = "unknown";
1697
1625
  let openclawVersion = "unknown";
1698
1626
  /** Build the User-Agent string from the current plugin and framework versions. */
@@ -2129,19 +2057,4 @@ function supportsRichMedia(targetType) {
2129
2057
  return targetType === "c2c" || targetType === "group";
2130
2058
  }
2131
2059
  //#endregion
2132
- //#region extensions/qqbot/src/bridge/runtime.ts
2133
- const { setRuntime: _setRuntime, clearRuntime: resetQQBotRuntimeForTest, getRuntime: getQQBotRuntime } = createPluginRuntimeStore({
2134
- pluginId: "qqbot",
2135
- errorMessage: "QQBot runtime not initialized"
2136
- });
2137
- /** Set the QQBot runtime and inject the framework version into the User-Agent. */
2138
- function setQQBotRuntime(runtime) {
2139
- _setRuntime(runtime);
2140
- setOpenClawVersion(runtime.version);
2141
- }
2142
- /** Type-narrowed getter for engine/ modules that need GatewayPluginRuntime. */
2143
- function getQQBotRuntimeForEngine() {
2144
- return getQQBotRuntime();
2145
- }
2146
- //#endregion
2147
- export { checkFileSize as A, StreamContentType as B, debugError as C, getNextMsgSeq as D, UploadDailyLimitExceededError as E, getImageMimeType as F, StreamInputState as H, getMaxUploadSize as I, readFileAsync as L, fileExistsAsync as M, formatFileSize as N, UPLOAD_PREPARE_FALLBACK_CODE as O, getFileTypeName as P, formatDuration as R, withTokenRetry as S, debugWarn as T, StreamInputMode as V, sendInputNotify as _, acknowledgeInteraction as a, startBackgroundTokenRefresh as b, createRawInputNotifyFn as c, getMessageApi as d, getPluginUserAgent as f, registerAccount as g, onMessageSent as h, accountToCreds as i, downloadFile as j, openLocalFile as k, getAccessToken as l, initSender as m, getQQBotRuntimeForEngine as n, buildDeliveryTarget as o, initApiConfig as p, setQQBotRuntime as r, clearTokenCache as s, getQQBotRuntime as t, getGatewayUrl as u, sendMedia as v, debugLog as w, stopBackgroundTokenRefresh as x, sendText as y, formatErrorMessage as z };
2060
+ export { getFileTypeName as A, getNextMsgSeq as C, downloadFile as D, checkFileSize as E, StreamInputMode as F, StreamInputState as I, __exportAll as L, getMaxUploadSize as M, readFileAsync as N, fileExistsAsync as O, StreamContentType as P, UploadDailyLimitExceededError as S, openLocalFile as T, sender_exports as _, createRawInputNotifyFn as a, stopBackgroundTokenRefresh as b, getMessageApi as c, initSender as d, onMessageSent as f, sendText as g, sendMedia as h, clearTokenCache as i, getImageMimeType as j, formatFileSize as k, getPluginUserAgent as l, sendInputNotify as m, acknowledgeInteraction as n, getAccessToken as o, registerAccount as p, buildDeliveryTarget as r, getGatewayUrl as s, accountToCreds as t, initApiConfig as u, setOpenClawVersion as v, UPLOAD_PREPARE_FALLBACK_CODE as w, withTokenRetry as x, startBackgroundTokenRefresh as y };
@@ -1,2 +1,2 @@
1
- import { t as qqbotSetupPlugin } from "./channel.setup-xBlmSU4X.js";
1
+ import { t as qqbotSetupPlugin } from "./channel.setup-CQ_DFfPx.js";
2
2
  export { qqbotSetupPlugin };
@@ -1,10 +1,9 @@
1
- import { o as getHomeDir, p as isWindows, s as getQQBotDataDir, u as getQQBotMediaPath } from "./channel-BLMrGYfg.js";
2
- import { w as debugLog } from "./runtime-BSgtMZ6G.js";
1
+ import { n as debugLog } from "./log-SDfMMBWe.js";
2
+ import { a as getHomeDir, f as isWindows, l as getQQBotMediaPath, o as getQQBotDataDir } from "./target-parser-BdCUmxK7.js";
3
3
  import { uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { loadJsonFile } from "openclaw/plugin-sdk/json-store";
7
- import { AsyncLocalStorage } from "node:async_hooks";
8
7
  //#region extensions/qqbot/src/engine/commands/builtin/state.ts
9
8
  let resolveVersionGetter = () => "unknown";
10
9
  let approveRuntimeGetter = null;
@@ -899,45 +898,4 @@ function getFrameworkVersion() {
899
898
  return getFrameworkVersionString();
900
899
  }
901
900
  //#endregion
902
- //#region extensions/qqbot/src/engine/utils/request-context.ts
903
- /**
904
- * Request-level context using AsyncLocalStorage.
905
- *
906
- * Provides ambient context (accountId, target openid, chat type, etc.)
907
- * throughout the request lifecycle without explicit parameter threading.
908
- *
909
- * Gateway establishes the scope around each inbound message via
910
- * `runWithRequestContext()`; any async code within that scope (including
911
- * AI agent calls and tool `execute` callbacks) can retrieve the current
912
- * request via `getRequestContext()` without racing with concurrent
913
- * inbound messages.
914
- *
915
- * This is a pure Node.js module with zero framework dependencies,
916
- * making it trivially portable between the built-in and standalone
917
- * versions of QQBot.
918
- */
919
- const store = new AsyncLocalStorage();
920
- /**
921
- * Execute an async function with request-scoped context.
922
- *
923
- * All code running within `fn` (including nested async calls) can
924
- * retrieve the context via `getRequestContext()`.
925
- *
926
- * @param ctx - The context to attach to this request.
927
- * @param fn - The async function to run within the context.
928
- * @returns The return value of `fn`.
929
- */
930
- function runWithRequestContext(ctx, fn) {
931
- return store.run(ctx, fn);
932
- }
933
- /**
934
- * Retrieve the current request context.
935
- *
936
- * Returns `undefined` when called outside of a `runWithRequestContext`
937
- * scope.
938
- */
939
- function getRequestContext() {
940
- return store.getStore();
941
- }
942
- //#endregion
943
- export { getPluginVersion as a, getFrameworkVersion as i, runWithRequestContext as n, initCommands as o, getFrameworkCommands as r, matchSlashCommand as s, getRequestContext as t };
901
+ export { matchSlashCommand as a, initCommands as i, getFrameworkVersion as n, getPluginVersion as r, getFrameworkCommands as t };
@@ -0,0 +1,285 @@
1
+ import { c as getPlatformAdapter } from "./string-normalize-R_0cKO7Q.js";
2
+ import { a as formatErrorMessage, n as debugLog, r as debugWarn } from "./log-SDfMMBWe.js";
3
+ import * as fs$1 from "node:fs";
4
+ import * as os$1 from "node:os";
5
+ import * as path$1 from "node:path";
6
+ //#region extensions/qqbot/src/engine/utils/platform.ts
7
+ /**
8
+ * Cross-platform path and detection helpers for core/ modules.
9
+ *
10
+ * Provides home/data/media directory helpers, platform detection,
11
+ * silk-wasm availability checks — all without importing `openclaw/plugin-sdk`.
12
+ * The temp-directory fallback is delegated to the PlatformAdapter.
13
+ */
14
+ /**
15
+ * Resolve the current user's OS home directory safely across platforms.
16
+ *
17
+ * Priority:
18
+ * 1. `os.homedir()`
19
+ * 2. `$HOME` or `%USERPROFILE%`
20
+ * 3. PlatformAdapter.getTempDir() as a last resort
21
+ *
22
+ * This is the *operating-system* home and intentionally ignores
23
+ * `OPENCLAW_HOME`. QQ Bot still checks this tree for legacy state imports and
24
+ * media-path remaps from older releases.
25
+ */
26
+ function getHomeDir() {
27
+ try {
28
+ const home = os$1.homedir();
29
+ if (home && fs$1.existsSync(home)) return home;
30
+ } catch {}
31
+ const envHome = process.env.HOME || process.env.USERPROFILE;
32
+ if (envHome && fs$1.existsSync(envHome)) return envHome;
33
+ return getPlatformAdapter().getTempDir();
34
+ }
35
+ /**
36
+ * Resolve the effective OpenClaw home directory.
37
+ *
38
+ * Mirrors the contract from core (`src/infra/home-dir.ts::resolveEffectiveHomeDir`)
39
+ * so QQ Bot media roots live under the same tree the rest of OpenClaw treats as
40
+ * `~`. The extension cannot import the core helper directly (it is a separate
41
+ * package with `openclaw` as a peer dependency), so this re-implements the
42
+ * minimal contract:
43
+ *
44
+ * 1. `OPENCLAW_HOME` when set (with `~` / `~/...` expanded against the OS home).
45
+ * 2. Otherwise fall back to {@link getHomeDir} so existing single-home
46
+ * deployments are unaffected.
47
+ *
48
+ * Empty / `"undefined"` / `"null"` strings are treated as unset to match how
49
+ * core normalizes the variable.
50
+ */
51
+ function resolveOpenClawHome() {
52
+ const raw = process.env.OPENCLAW_HOME?.trim();
53
+ if (!raw || raw === "undefined" || raw === "null") return getHomeDir();
54
+ if (raw === "~" || raw.startsWith("~/") || raw.startsWith("~\\")) {
55
+ const osHome = getHomeDir();
56
+ if (raw === "~") return osHome;
57
+ return path$1.join(osHome, raw.slice(2));
58
+ }
59
+ return raw;
60
+ }
61
+ /**
62
+ * Return a legacy path under `~/.openclaw/qqbot` without creating it.
63
+ *
64
+ * Current QQ Bot runtime state lives in plugin SQLite KV. This path remains for
65
+ * legacy imports and media-path remaps from older releases.
66
+ */
67
+ function getQQBotDataPath(...subPaths) {
68
+ return path$1.join(getHomeDir(), ".openclaw", "qqbot", ...subPaths);
69
+ }
70
+ /** Return a path under `~/.openclaw/qqbot`, creating it on demand. */
71
+ function getQQBotDataDir(...subPaths) {
72
+ const dir = getQQBotDataPath(...subPaths);
73
+ if (!fs$1.existsSync(dir)) fs$1.mkdirSync(dir, { recursive: true });
74
+ return dir;
75
+ }
76
+ /**
77
+ * Return a path under `<openclaw-home>/.openclaw/media/qqbot` without creating it.
78
+ *
79
+ * Unlike `getQQBotDataPath`, this lives under OpenClaw's core media allowlist
80
+ * so downloaded images and audio can be accessed by framework media tooling.
81
+ * The base honors `OPENCLAW_HOME` (when set) so files written by agents into
82
+ * the OpenClaw-managed media tree are reachable by this plugin even when
83
+ * `HOME` and `OPENCLAW_HOME` differ (Docker, multi-user hosts). Fixes #83562.
84
+ */
85
+ function getQQBotMediaPath(...subPaths) {
86
+ return path$1.join(resolveOpenClawHome(), ".openclaw", "media", "qqbot", ...subPaths);
87
+ }
88
+ /** Return a path under `<openclaw-home>/.openclaw/media/qqbot`, creating it on demand. */
89
+ function getQQBotMediaDir(...subPaths) {
90
+ const dir = getQQBotMediaPath(...subPaths);
91
+ if (!fs$1.existsSync(dir)) fs$1.mkdirSync(dir, { recursive: true });
92
+ return dir;
93
+ }
94
+ /**
95
+ * Return `<openclaw-home>/.openclaw/media`, OpenClaw's shared media root.
96
+ *
97
+ * This mirrors the directory that core's `buildMediaLocalRoots` exposes as an
98
+ * allowlisted location (see `openclaw/src/media/local-roots.ts`). Using it as a
99
+ * QQ Bot payload root lets the plugin trust framework-produced files that live
100
+ * in sibling subdirectories such as `outbound/` (written by
101
+ * `saveMediaBuffer(..., "outbound", ...)`) or `inbound/`, while still keeping
102
+ * the check anchored to a single, well-known directory. Like
103
+ * {@link getQQBotMediaPath}, the base honors `OPENCLAW_HOME`.
104
+ */
105
+ function getOpenClawMediaDir() {
106
+ return path$1.join(resolveOpenClawHome(), ".openclaw", "media");
107
+ }
108
+ function isWindows() {
109
+ return process.platform === "win32";
110
+ }
111
+ /** Return the preferred temporary directory. */
112
+ function getTempDir() {
113
+ return getPlatformAdapter().getTempDir();
114
+ }
115
+ let silkWasmAvailable = null;
116
+ /** Check whether silk-wasm can run in the current environment. */
117
+ async function checkSilkWasmAvailable() {
118
+ if (silkWasmAvailable !== null) return silkWasmAvailable;
119
+ try {
120
+ const { isSilk } = await import("silk-wasm");
121
+ isSilk(new Uint8Array(0));
122
+ silkWasmAvailable = true;
123
+ debugLog("[platform] silk-wasm: available");
124
+ } catch (err) {
125
+ silkWasmAvailable = false;
126
+ debugWarn(`[platform] silk-wasm: NOT available (${formatErrorMessage(err)})`);
127
+ }
128
+ return silkWasmAvailable;
129
+ }
130
+ /** Expand `~` to the current user's home directory. */
131
+ function expandTilde(p) {
132
+ if (!p) return p;
133
+ if (p === "~") return getHomeDir();
134
+ if (p.startsWith("~/") || p.startsWith("~\\")) return path$1.join(getHomeDir(), p.slice(2));
135
+ return p;
136
+ }
137
+ /** Normalize a user-provided path by trimming, stripping `file://`, and expanding `~`. */
138
+ function normalizePath(p) {
139
+ let result = p.trim();
140
+ if (result.startsWith("file://")) {
141
+ result = result.slice(7);
142
+ try {
143
+ result = decodeURIComponent(result);
144
+ } catch {}
145
+ }
146
+ return expandTilde(result);
147
+ }
148
+ /** Return true when the string looks like a local filesystem path rather than a URL. */
149
+ function isLocalPath(p) {
150
+ if (!p) return false;
151
+ if (p.startsWith("file://")) return true;
152
+ if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) return true;
153
+ if (p.startsWith("/")) return true;
154
+ if (/^[a-zA-Z]:[\\/]/.test(p)) return true;
155
+ if (p.startsWith("\\\\")) return true;
156
+ if (p.startsWith("./") || p.startsWith("../")) return true;
157
+ if (p.startsWith(".\\") || p.startsWith("..\\")) return true;
158
+ return false;
159
+ }
160
+ function isPathWithinRoot(candidate, root) {
161
+ const relative = path$1.relative(root, candidate);
162
+ return relative === "" || !relative.startsWith("..") && !path$1.isAbsolute(relative);
163
+ }
164
+ /** Remap legacy or hallucinated QQ Bot local media paths to real files when possible. */
165
+ function resolveQQBotLocalMediaPath(p) {
166
+ const normalized = normalizePath(p);
167
+ if (!isLocalPath(normalized) || fs$1.existsSync(normalized)) return normalized;
168
+ const osHomeDir = getHomeDir();
169
+ const openclawHomeDir = resolveOpenClawHome();
170
+ const mediaRoot = getQQBotMediaPath();
171
+ const dataRoot = getQQBotDataPath();
172
+ const candidateRoots = [
173
+ ...Array.from(new Set([path$1.join(osHomeDir, ".openclaw", "workspace", "qqbot"), path$1.join(openclawHomeDir, ".openclaw", "workspace", "qqbot")])).map((from) => ({
174
+ from,
175
+ to: mediaRoot
176
+ })),
177
+ {
178
+ from: dataRoot,
179
+ to: mediaRoot
180
+ },
181
+ {
182
+ from: mediaRoot,
183
+ to: dataRoot
184
+ }
185
+ ];
186
+ for (const { from, to } of candidateRoots) {
187
+ if (!isPathWithinRoot(normalized, from)) continue;
188
+ const relative = path$1.relative(from, normalized);
189
+ const candidate = path$1.join(to, relative);
190
+ if (fs$1.existsSync(candidate)) {
191
+ debugWarn(`[platform] Remapped missing QQBot media path ${normalized} -> ${candidate}`);
192
+ return candidate;
193
+ }
194
+ }
195
+ return normalized;
196
+ }
197
+ /**
198
+ * Resolve a structured-payload local file path and enforce that it stays within
199
+ * QQ Bot-owned storage roots.
200
+ */
201
+ function resolveQQBotPayloadLocalFilePath(p) {
202
+ const candidate = resolveQQBotLocalMediaPath(p);
203
+ if (!candidate.trim()) return null;
204
+ const resolvedCandidate = path$1.resolve(candidate);
205
+ if (!fs$1.existsSync(resolvedCandidate)) return null;
206
+ const canonicalCandidate = fs$1.realpathSync(resolvedCandidate);
207
+ const allowedRoots = [getOpenClawMediaDir(), getQQBotMediaPath()];
208
+ for (const root of allowedRoots) {
209
+ const resolvedRoot = path$1.resolve(root);
210
+ if (isPathWithinRoot(canonicalCandidate, fs$1.existsSync(resolvedRoot) ? fs$1.realpathSync(resolvedRoot) : resolvedRoot)) return canonicalCandidate;
211
+ }
212
+ return null;
213
+ }
214
+ //#endregion
215
+ //#region extensions/qqbot/src/engine/messaging/target-parser.ts
216
+ /**
217
+ * Parse a qqbot target string into a structured delivery target.
218
+ *
219
+ * Supported formats:
220
+ * - `qqbot:c2c:openid` → C2C direct message
221
+ * - `qqbot:group:groupid` → Group message
222
+ * - `qqbot:channel:channelid` → Channel message
223
+ * - `c2c:openid` → C2C (without qqbot: prefix)
224
+ * - `group:groupid` → Group (without qqbot: prefix)
225
+ * - `channel:channelid` → Channel (without qqbot: prefix)
226
+ * - `openid` → C2C (bare openid, default)
227
+ *
228
+ * @param to - Raw target string.
229
+ * @returns Parsed target with type and id.
230
+ * @throws {Error} When the target format is invalid.
231
+ */
232
+ function parseTarget(to) {
233
+ const id = to.replace(/^qqbot:/i, "");
234
+ if (id.startsWith("c2c:")) {
235
+ const userId = id.slice(4);
236
+ if (!userId) throw new Error(`Invalid c2c target format: ${to} - missing user ID`);
237
+ return {
238
+ type: "c2c",
239
+ id: userId
240
+ };
241
+ }
242
+ if (id.startsWith("group:")) {
243
+ const groupId = id.slice(6);
244
+ if (!groupId) throw new Error(`Invalid group target format: ${to} - missing group ID`);
245
+ return {
246
+ type: "group",
247
+ id: groupId
248
+ };
249
+ }
250
+ if (id.startsWith("channel:")) {
251
+ const channelId = id.slice(8);
252
+ if (!channelId) throw new Error(`Invalid channel target format: ${to} - missing channel ID`);
253
+ return {
254
+ type: "channel",
255
+ id: channelId
256
+ };
257
+ }
258
+ if (!id) throw new Error(`Invalid target format: ${to} - empty ID after removing qqbot: prefix`);
259
+ return {
260
+ type: "c2c",
261
+ id
262
+ };
263
+ }
264
+ /**
265
+ * Normalize a QQ Bot target string into the canonical `qqbot:...` form.
266
+ *
267
+ * Returns `undefined` when the target does not look like a QQ Bot address.
268
+ */
269
+ function normalizeTarget(target) {
270
+ const id = target.replace(/^qqbot:/i, "");
271
+ if (id.startsWith("c2c:") || id.startsWith("group:") || id.startsWith("channel:")) return `qqbot:${id}`;
272
+ if (/^[0-9a-fA-F]{32}$/.test(id)) return `qqbot:c2c:${id}`;
273
+ 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}`;
274
+ }
275
+ /**
276
+ * Return true when the string looks like a QQ Bot target ID.
277
+ */
278
+ function looksLikeQQBotTarget(id) {
279
+ if (/^qqbot:(c2c|group|channel):/i.test(id)) return true;
280
+ if (/^(c2c|group|channel):/i.test(id)) return true;
281
+ if (/^[0-9a-fA-F]{32}$/.test(id)) return true;
282
+ 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);
283
+ }
284
+ //#endregion
285
+ export { getHomeDir as a, getQQBotMediaDir as c, isLocalPath as d, isWindows as f, checkSilkWasmAvailable as i, getQQBotMediaPath as l, resolveQQBotPayloadLocalFilePath as m, normalizeTarget as n, getQQBotDataDir as o, normalizePath as p, parseTarget as r, getQQBotDataPath as s, looksLikeQQBotTarget as t, getTempDir as u };