@openclaw/qqbot 2026.5.2 → 2026.5.3-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.
Files changed (203) hide show
  1. package/dist/api.js +646 -0
  2. package/dist/approval-cg0SVahb.js +94 -0
  3. package/dist/channel-N6Y_Rcjp.js +551 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.setup-ByCRAe5H.js +25 -0
  6. package/dist/config-D6545NkC.js +94 -0
  7. package/dist/config-schema-DFcjQw73.js +309 -0
  8. package/dist/exec-approvals-COUsM6wZ.js +138 -0
  9. package/dist/gateway-CmSUJKSt.js +6726 -0
  10. package/dist/handler-runtime-Bqm6N0WG.js +120 -0
  11. package/dist/index.js +29 -0
  12. package/dist/narrowing-BoieBTIU.js +25 -0
  13. package/dist/outbound-BJfhwrPg.js +1357 -0
  14. package/dist/request-context-DXtpwWui.js +921 -0
  15. package/dist/resolve-D_06fV6-.js +284 -0
  16. package/dist/runtime-BJAS3eXW.js +18 -0
  17. package/dist/runtime-api.js +2 -0
  18. package/dist/secret-contract-api.js +56 -0
  19. package/dist/sender-p-B14eLG.js +1987 -0
  20. package/dist/setup-entry.js +15 -0
  21. package/dist/setup-plugin-api.js +2 -0
  22. package/dist/string-normalize-Ci6NM5DE.js +98 -0
  23. package/dist/target-parser-Y0prnrXD.js +245 -0
  24. package/package.json +15 -6
  25. package/api.ts +0 -56
  26. package/channel-plugin-api.ts +0 -1
  27. package/index.ts +0 -29
  28. package/runtime-api.ts +0 -9
  29. package/setup-entry.ts +0 -9
  30. package/setup-plugin-api.ts +0 -3
  31. package/src/bridge/approval/capability.ts +0 -237
  32. package/src/bridge/approval/handler-runtime.ts +0 -204
  33. package/src/bridge/bootstrap.ts +0 -135
  34. package/src/bridge/channel-entry.ts +0 -18
  35. package/src/bridge/commands/framework-context-adapter.ts +0 -60
  36. package/src/bridge/commands/framework-registration.ts +0 -47
  37. package/src/bridge/commands/from-parser.test.ts +0 -86
  38. package/src/bridge/commands/from-parser.ts +0 -60
  39. package/src/bridge/commands/result-dispatcher.ts +0 -76
  40. package/src/bridge/config-shared.ts +0 -132
  41. package/src/bridge/config.ts +0 -111
  42. package/src/bridge/gateway.ts +0 -174
  43. package/src/bridge/logger.ts +0 -31
  44. package/src/bridge/narrowing.ts +0 -31
  45. package/src/bridge/plugin-version.test.ts +0 -146
  46. package/src/bridge/plugin-version.ts +0 -102
  47. package/src/bridge/runtime.ts +0 -25
  48. package/src/bridge/sdk-adapter.ts +0 -131
  49. package/src/bridge/setup/finalize.ts +0 -144
  50. package/src/bridge/setup/surface.ts +0 -34
  51. package/src/bridge/tools/channel.ts +0 -58
  52. package/src/bridge/tools/index.ts +0 -15
  53. package/src/bridge/tools/remind.test.ts +0 -124
  54. package/src/bridge/tools/remind.ts +0 -91
  55. package/src/channel.setup.ts +0 -33
  56. package/src/channel.ts +0 -288
  57. package/src/command-auth.test.ts +0 -62
  58. package/src/config-schema.ts +0 -84
  59. package/src/config.test.ts +0 -364
  60. package/src/engine/access/access-control.test.ts +0 -198
  61. package/src/engine/access/access-control.ts +0 -226
  62. package/src/engine/access/index.ts +0 -16
  63. package/src/engine/access/resolve-policy.test.ts +0 -59
  64. package/src/engine/access/resolve-policy.ts +0 -57
  65. package/src/engine/access/sender-match.test.ts +0 -60
  66. package/src/engine/access/sender-match.ts +0 -55
  67. package/src/engine/access/types.ts +0 -53
  68. package/src/engine/adapter/audio.port.ts +0 -27
  69. package/src/engine/adapter/commands.port.ts +0 -22
  70. package/src/engine/adapter/history.port.ts +0 -52
  71. package/src/engine/adapter/index.ts +0 -139
  72. package/src/engine/adapter/mention-gate.port.ts +0 -50
  73. package/src/engine/adapter/types.ts +0 -38
  74. package/src/engine/api/api-client.ts +0 -212
  75. package/src/engine/api/media-chunked.test.ts +0 -336
  76. package/src/engine/api/media-chunked.ts +0 -622
  77. package/src/engine/api/media.ts +0 -218
  78. package/src/engine/api/messages.ts +0 -293
  79. package/src/engine/api/retry.ts +0 -217
  80. package/src/engine/api/routes.ts +0 -95
  81. package/src/engine/api/token.ts +0 -271
  82. package/src/engine/approval/index.test.ts +0 -22
  83. package/src/engine/approval/index.ts +0 -224
  84. package/src/engine/commands/builtin/log-helpers.ts +0 -319
  85. package/src/engine/commands/builtin/register-all.ts +0 -17
  86. package/src/engine/commands/builtin/register-approve.ts +0 -201
  87. package/src/engine/commands/builtin/register-basic.ts +0 -95
  88. package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
  89. package/src/engine/commands/builtin/register-logs.ts +0 -20
  90. package/src/engine/commands/builtin/register-streaming.ts +0 -137
  91. package/src/engine/commands/builtin/state.ts +0 -31
  92. package/src/engine/commands/slash-command-auth.ts +0 -48
  93. package/src/engine/commands/slash-command-handler.ts +0 -146
  94. package/src/engine/commands/slash-commands-impl.test.ts +0 -8
  95. package/src/engine/commands/slash-commands-impl.ts +0 -61
  96. package/src/engine/commands/slash-commands.ts +0 -199
  97. package/src/engine/config/credential-backup.test.ts +0 -88
  98. package/src/engine/config/credential-backup.ts +0 -107
  99. package/src/engine/config/credentials.ts +0 -76
  100. package/src/engine/config/group.test.ts +0 -234
  101. package/src/engine/config/group.ts +0 -299
  102. package/src/engine/config/resolve.test.ts +0 -152
  103. package/src/engine/config/resolve.ts +0 -283
  104. package/src/engine/config/setup-logic.ts +0 -84
  105. package/src/engine/engine-import-boundary.test.ts +0 -73
  106. package/src/engine/gateway/codec.ts +0 -47
  107. package/src/engine/gateway/constants.ts +0 -117
  108. package/src/engine/gateway/event-dispatcher.ts +0 -177
  109. package/src/engine/gateway/gateway-connection.ts +0 -371
  110. package/src/engine/gateway/gateway.ts +0 -291
  111. package/src/engine/gateway/inbound-attachments.test.ts +0 -126
  112. package/src/engine/gateway/inbound-attachments.ts +0 -360
  113. package/src/engine/gateway/inbound-context.ts +0 -195
  114. package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
  115. package/src/engine/gateway/inbound-pipeline.ts +0 -235
  116. package/src/engine/gateway/interaction-handler.ts +0 -220
  117. package/src/engine/gateway/message-queue.test.ts +0 -282
  118. package/src/engine/gateway/message-queue.ts +0 -499
  119. package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
  120. package/src/engine/gateway/outbound-dispatch.ts +0 -575
  121. package/src/engine/gateway/reconnect.ts +0 -199
  122. package/src/engine/gateway/stages/access-stage.ts +0 -132
  123. package/src/engine/gateway/stages/assembly-stage.ts +0 -156
  124. package/src/engine/gateway/stages/content-stage.test.ts +0 -77
  125. package/src/engine/gateway/stages/content-stage.ts +0 -77
  126. package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
  127. package/src/engine/gateway/stages/envelope-stage.ts +0 -144
  128. package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
  129. package/src/engine/gateway/stages/index.ts +0 -18
  130. package/src/engine/gateway/stages/quote-stage.ts +0 -113
  131. package/src/engine/gateway/stages/refidx-stage.ts +0 -62
  132. package/src/engine/gateway/stages/stub-contexts.ts +0 -116
  133. package/src/engine/gateway/types.ts +0 -264
  134. package/src/engine/gateway/typing-keepalive.ts +0 -79
  135. package/src/engine/group/activation.test.ts +0 -114
  136. package/src/engine/group/activation.ts +0 -147
  137. package/src/engine/group/history.test.ts +0 -314
  138. package/src/engine/group/history.ts +0 -321
  139. package/src/engine/group/mention.test.ts +0 -141
  140. package/src/engine/group/mention.ts +0 -197
  141. package/src/engine/group/message-gating.test.ts +0 -188
  142. package/src/engine/group/message-gating.ts +0 -216
  143. package/src/engine/messaging/decode-media-path.ts +0 -82
  144. package/src/engine/messaging/media-source.ts +0 -215
  145. package/src/engine/messaging/media-type-detect.ts +0 -37
  146. package/src/engine/messaging/outbound-audio-port.ts +0 -38
  147. package/src/engine/messaging/outbound-deliver.ts +0 -810
  148. package/src/engine/messaging/outbound-media-send.ts +0 -702
  149. package/src/engine/messaging/outbound-reply.ts +0 -27
  150. package/src/engine/messaging/outbound-result-helpers.ts +0 -54
  151. package/src/engine/messaging/outbound-types.ts +0 -45
  152. package/src/engine/messaging/outbound.ts +0 -485
  153. package/src/engine/messaging/reply-dispatcher.ts +0 -597
  154. package/src/engine/messaging/reply-limiter.ts +0 -164
  155. package/src/engine/messaging/sender.ts +0 -729
  156. package/src/engine/messaging/streaming-c2c.ts +0 -1192
  157. package/src/engine/messaging/streaming-media-send.ts +0 -544
  158. package/src/engine/messaging/target-parser.ts +0 -104
  159. package/src/engine/ref/format-message-ref.ts +0 -142
  160. package/src/engine/ref/format-ref-entry.test.ts +0 -60
  161. package/src/engine/ref/format-ref-entry.ts +0 -27
  162. package/src/engine/ref/store.ts +0 -208
  163. package/src/engine/ref/types.ts +0 -27
  164. package/src/engine/session/known-users.ts +0 -137
  165. package/src/engine/session/session-store.ts +0 -204
  166. package/src/engine/tools/channel-api.ts +0 -244
  167. package/src/engine/tools/remind-logic.test.ts +0 -280
  168. package/src/engine/tools/remind-logic.ts +0 -377
  169. package/src/engine/types.ts +0 -313
  170. package/src/engine/utils/attachment-tags.test.ts +0 -186
  171. package/src/engine/utils/attachment-tags.ts +0 -174
  172. package/src/engine/utils/audio.test.ts +0 -250
  173. package/src/engine/utils/audio.ts +0 -585
  174. package/src/engine/utils/data-paths.ts +0 -38
  175. package/src/engine/utils/diagnostics.ts +0 -109
  176. package/src/engine/utils/file-utils.test.ts +0 -72
  177. package/src/engine/utils/file-utils.ts +0 -225
  178. package/src/engine/utils/format.test.ts +0 -68
  179. package/src/engine/utils/format.ts +0 -70
  180. package/src/engine/utils/image-size.test.ts +0 -158
  181. package/src/engine/utils/image-size.ts +0 -249
  182. package/src/engine/utils/log.test.ts +0 -28
  183. package/src/engine/utils/log.ts +0 -61
  184. package/src/engine/utils/media-tags.test.ts +0 -32
  185. package/src/engine/utils/media-tags.ts +0 -177
  186. package/src/engine/utils/payload.test.ts +0 -68
  187. package/src/engine/utils/payload.ts +0 -145
  188. package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
  189. package/src/engine/utils/platform.test.ts +0 -148
  190. package/src/engine/utils/platform.ts +0 -343
  191. package/src/engine/utils/request-context.ts +0 -60
  192. package/src/engine/utils/string-normalize.ts +0 -91
  193. package/src/engine/utils/stt.test.ts +0 -104
  194. package/src/engine/utils/stt.ts +0 -100
  195. package/src/engine/utils/text-parsing.test.ts +0 -29
  196. package/src/engine/utils/text-parsing.ts +0 -155
  197. package/src/engine/utils/upload-cache.ts +0 -96
  198. package/src/engine/utils/voice-text.ts +0 -15
  199. package/src/exec-approvals.ts +0 -218
  200. package/src/manifest-schema.test.ts +0 -56
  201. package/src/qqbot-test-support.ts +0 -29
  202. package/src/types.ts +0 -210
  203. package/tsconfig.json +0 -16
@@ -1,145 +0,0 @@
1
- /**
2
- * Structured payload parsing and encoding for QQ Bot messages.
3
- *
4
- * Handles `QQBOT_PAYLOAD:` (model-emitted structured payloads) and
5
- * `QQBOT_CRON:` (persisted cron reminder payloads).
6
- *
7
- * Zero external dependencies.
8
- */
9
-
10
- import type { ChatScope } from "../types.js";
11
-
12
- /** Structured reminder payload emitted by the model. */
13
- export interface CronReminderPayload {
14
- type: "cron_reminder";
15
- content: string;
16
- targetType: ChatScope;
17
- targetAddress: string;
18
- originalMessageId?: string;
19
- }
20
-
21
- /** Structured media payload emitted by the model. */
22
- export interface MediaPayload {
23
- type: "media";
24
- mediaType: "image" | "audio" | "video" | "file";
25
- source: "url" | "file";
26
- path: string;
27
- caption?: string;
28
- }
29
-
30
- type QQBotPayload = CronReminderPayload | MediaPayload;
31
-
32
- /** Result of parsing model output into a structured payload. */
33
- interface ParseResult {
34
- isPayload: boolean;
35
- payload?: QQBotPayload;
36
- text?: string;
37
- error?: string;
38
- }
39
-
40
- const PAYLOAD_PREFIX = "QQBOT_PAYLOAD:";
41
- const CRON_PREFIX = "QQBOT_CRON:";
42
-
43
- function formatErr(e: unknown): string {
44
- return e instanceof Error ? e.message : String(e);
45
- }
46
-
47
- /** Parse model output that may start with the QQ Bot structured payload prefix. */
48
- export function parseQQBotPayload(text: string): ParseResult {
49
- const trimmedText = text.trim();
50
-
51
- if (!trimmedText.startsWith(PAYLOAD_PREFIX)) {
52
- return { isPayload: false, text };
53
- }
54
-
55
- const jsonContent = trimmedText.slice(PAYLOAD_PREFIX.length).trim();
56
-
57
- if (!jsonContent) {
58
- return { isPayload: true, error: "Payload body is empty" };
59
- }
60
-
61
- try {
62
- const payload = JSON.parse(jsonContent) as QQBotPayload;
63
-
64
- if (!payload.type) {
65
- return { isPayload: true, error: "Payload is missing the type field" };
66
- }
67
-
68
- if (payload.type === "cron_reminder") {
69
- if (!payload.content || !payload.targetType || !payload.targetAddress) {
70
- return {
71
- isPayload: true,
72
- error:
73
- "cron_reminder payload is missing required fields (content, targetType, targetAddress)",
74
- };
75
- }
76
- } else if (payload.type === "media") {
77
- if (!payload.mediaType || !payload.source || !payload.path) {
78
- return {
79
- isPayload: true,
80
- error: "media payload is missing required fields (mediaType, source, path)",
81
- };
82
- }
83
- }
84
-
85
- return { isPayload: true, payload };
86
- } catch (e) {
87
- return { isPayload: true, error: `Failed to parse JSON: ${formatErr(e)}` };
88
- }
89
- }
90
-
91
- /** Encode a cron reminder payload into the stored cron-message format. */
92
- export function encodePayloadForCron(payload: CronReminderPayload): string {
93
- const jsonString = JSON.stringify(payload);
94
- const base64 = Buffer.from(jsonString, "utf-8").toString("base64");
95
- return `${CRON_PREFIX}${base64}`;
96
- }
97
-
98
- /** Decode a stored cron payload. */
99
- export function decodeCronPayload(message: string): {
100
- isCronPayload: boolean;
101
- payload?: CronReminderPayload;
102
- error?: string;
103
- } {
104
- const trimmedMessage = message.trim();
105
-
106
- if (!trimmedMessage.startsWith(CRON_PREFIX)) {
107
- return { isCronPayload: false };
108
- }
109
-
110
- const base64Content = trimmedMessage.slice(CRON_PREFIX.length);
111
-
112
- if (!base64Content) {
113
- return { isCronPayload: true, error: "Cron payload body is empty" };
114
- }
115
-
116
- try {
117
- const jsonString = Buffer.from(base64Content, "base64").toString("utf-8");
118
- const payload = JSON.parse(jsonString) as CronReminderPayload;
119
-
120
- if (payload.type !== "cron_reminder") {
121
- return {
122
- isCronPayload: true,
123
- error: `Expected type cron_reminder but got ${String(payload.type)}`,
124
- };
125
- }
126
-
127
- if (!payload.content || !payload.targetType || !payload.targetAddress) {
128
- return { isCronPayload: true, error: "Cron payload is missing required fields" };
129
- }
130
-
131
- return { isCronPayload: true, payload };
132
- } catch (e) {
133
- return { isCronPayload: true, error: `Failed to decode cron payload: ${formatErr(e)}` };
134
- }
135
- }
136
-
137
- /** Type guard for cron reminder payloads. */
138
- export function isCronReminderPayload(payload: QQBotPayload): payload is CronReminderPayload {
139
- return payload.type === "cron_reminder";
140
- }
141
-
142
- /** Type guard for media payloads. */
143
- export function isMediaPayload(payload: QQBotPayload): payload is MediaPayload {
144
- return payload.type === "media";
145
- }
@@ -1,63 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
-
6
- const createdHomes: string[] = [];
7
-
8
- async function useMockHome(homeDir: string): Promise<void> {
9
- vi.resetModules();
10
- vi.doMock("node:os", async (importOriginal) => {
11
- const actual = await importOriginal<typeof import("node:os")>();
12
- return {
13
- ...actual,
14
- default: { ...actual, homedir: () => homeDir },
15
- homedir: () => homeDir,
16
- };
17
- });
18
- }
19
-
20
- function makeHome(): string {
21
- const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "qqbot-home-"));
22
- createdHomes.push(homeDir);
23
- return homeDir;
24
- }
25
-
26
- describe("qqbot storage laziness", () => {
27
- afterEach(() => {
28
- vi.doUnmock("node:os");
29
- vi.resetModules();
30
- for (const home of createdHomes.splice(0)) {
31
- fs.rmSync(home, { recursive: true, force: true });
32
- }
33
- });
34
-
35
- it("does not create ~/.openclaw/qqbot from module imports or read-only probes", async () => {
36
- const homeDir = makeHome();
37
- await useMockHome(homeDir);
38
-
39
- const qqbotRoot = path.join(homeDir, ".openclaw", "qqbot");
40
-
41
- await import("../session/session-store.js");
42
- await import("../session/known-users.js");
43
- await import("../ref/store.js");
44
- const { loadCredentialBackup } = await import("../config/credential-backup.js");
45
-
46
- expect(loadCredentialBackup("default")).toBeNull();
47
- expect(fs.existsSync(qqbotRoot)).toBe(false);
48
- });
49
-
50
- it("creates storage when qqbot persists runtime state", async () => {
51
- const homeDir = makeHome();
52
- await useMockHome(homeDir);
53
-
54
- const qqbotRoot = path.join(homeDir, ".openclaw", "qqbot");
55
- const { saveCredentialBackup } = await import("../config/credential-backup.js");
56
-
57
- saveCredentialBackup("default", "123456", "secret");
58
-
59
- expect(fs.existsSync(path.join(qqbotRoot, "data", "credential-backup-default.json"))).toBe(
60
- true,
61
- );
62
- });
63
- });
@@ -1,148 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
5
- import {
6
- getHomeDir,
7
- resolveQQBotLocalMediaPath,
8
- resolveQQBotPayloadLocalFilePath,
9
- } from "./platform.js";
10
-
11
- describe("qqbot local media path remapping", () => {
12
- const createdPaths: string[] = [];
13
-
14
- function createOpenClawTestRoot() {
15
- const actualHome = getHomeDir();
16
- const openclawDir = path.join(actualHome, ".openclaw");
17
- fs.mkdirSync(openclawDir, { recursive: true });
18
- const testRoot = fs.mkdtempSync(path.join(openclawDir, "qqbot-platform-test-"));
19
- createdPaths.push(testRoot);
20
- return { actualHome, testRootName: path.basename(testRoot) };
21
- }
22
-
23
- function createQqbotMediaFile(fileName: string) {
24
- const { actualHome, testRootName } = createOpenClawTestRoot();
25
- const mediaFile = path.join(
26
- actualHome,
27
- ".openclaw",
28
- "media",
29
- "qqbot",
30
- "downloads",
31
- testRootName,
32
- fileName,
33
- );
34
- fs.mkdirSync(path.dirname(mediaFile), { recursive: true });
35
- fs.writeFileSync(mediaFile, "image", "utf8");
36
- createdPaths.push(path.dirname(mediaFile));
37
- return { actualHome, testRootName, mediaFile };
38
- }
39
-
40
- afterEach(() => {
41
- vi.restoreAllMocks();
42
- for (const target of createdPaths.splice(0)) {
43
- fs.rmSync(target, { recursive: true, force: true });
44
- }
45
- });
46
-
47
- it("remaps missing workspace media paths to the real media directory", () => {
48
- const { actualHome, testRootName, mediaFile } = createQqbotMediaFile("example.png");
49
-
50
- const missingWorkspacePath = path.join(
51
- actualHome,
52
- ".openclaw",
53
- "workspace",
54
- "qqbot",
55
- "downloads",
56
- testRootName,
57
- "example.png",
58
- );
59
-
60
- expect(resolveQQBotLocalMediaPath(missingWorkspacePath)).toBe(mediaFile);
61
- });
62
-
63
- it("leaves existing media paths unchanged", () => {
64
- const { mediaFile } = createQqbotMediaFile("existing.png");
65
-
66
- expect(resolveQQBotLocalMediaPath(mediaFile)).toBe(mediaFile);
67
- });
68
-
69
- it("blocks structured payload files outside QQ Bot storage", () => {
70
- const outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), "qqbot-platform-outside-"));
71
- createdPaths.push(outsideRoot);
72
-
73
- const outsideFile = path.join(outsideRoot, "secret.txt");
74
- fs.writeFileSync(outsideFile, "secret", "utf8");
75
-
76
- expect(resolveQQBotPayloadLocalFilePath(outsideFile)).toBeNull();
77
- });
78
-
79
- it("blocks structured payload paths that escape QQ Bot media via '..'", () => {
80
- const escapedPath = path.join(
81
- getHomeDir(),
82
- ".openclaw",
83
- "media",
84
- "qqbot",
85
- "..",
86
- "..",
87
- "qqbot-escape.txt",
88
- );
89
-
90
- expect(resolveQQBotPayloadLocalFilePath(escapedPath)).toBeNull();
91
- });
92
-
93
- it("allows structured payload files inside the QQ Bot media directory", () => {
94
- const { mediaFile } = createQqbotMediaFile("allowed.png");
95
-
96
- expect(resolveQQBotPayloadLocalFilePath(mediaFile)).toBe(fs.realpathSync(mediaFile));
97
- });
98
-
99
- it("allows structured payload files inside sibling OpenClaw media subdirectories", () => {
100
- // Core helpers such as `saveMediaBuffer(..., "outbound", ...)` place framework
101
- // attachments under sibling directories of `media/qqbot/`. The plugin must
102
- // trust the shared `~/.openclaw/media` root so auto-routed sends can access
103
- // those files without the path-outside-storage guard firing.
104
- const actualHome = getHomeDir();
105
- const outboundDir = path.join(actualHome, ".openclaw", "media", "outbound");
106
- fs.mkdirSync(outboundDir, { recursive: true });
107
- const outboundFile = fs.mkdtempSync(path.join(outboundDir, "qqbot-outbound-"));
108
- const mediaFile = path.join(outboundFile, "tts.mp3");
109
- fs.writeFileSync(mediaFile, "audio", "utf8");
110
- createdPaths.push(outboundFile);
111
-
112
- expect(resolveQQBotPayloadLocalFilePath(mediaFile)).toBe(fs.realpathSync(mediaFile));
113
- });
114
-
115
- it("blocks structured payload files inside the QQ Bot data directory", () => {
116
- const { actualHome, testRootName } = createOpenClawTestRoot();
117
-
118
- const dataFile = path.join(
119
- actualHome,
120
- ".openclaw",
121
- "qqbot",
122
- "sessions",
123
- testRootName,
124
- "session.json",
125
- );
126
- fs.mkdirSync(path.dirname(dataFile), { recursive: true });
127
- fs.writeFileSync(dataFile, "{}", "utf8");
128
- createdPaths.push(path.dirname(dataFile));
129
-
130
- expect(resolveQQBotPayloadLocalFilePath(dataFile)).toBeNull();
131
- });
132
-
133
- it("allows legacy workspace paths when they remap into QQ Bot media storage", () => {
134
- const { actualHome, testRootName, mediaFile } = createQqbotMediaFile("legacy.png");
135
-
136
- const missingWorkspacePath = path.join(
137
- actualHome,
138
- ".openclaw",
139
- "workspace",
140
- "qqbot",
141
- "downloads",
142
- testRootName,
143
- "legacy.png",
144
- );
145
-
146
- expect(resolveQQBotPayloadLocalFilePath(missingWorkspacePath)).toBe(fs.realpathSync(mediaFile));
147
- });
148
- });
@@ -1,343 +0,0 @@
1
- /**
2
- * Cross-platform path and detection helpers for core/ modules.
3
- *
4
- * Provides home/data/media directory helpers, platform detection,
5
- * ffmpeg/silk-wasm availability checks — all without importing
6
- * `openclaw/plugin-sdk`. The temp-directory fallback is delegated
7
- * to the PlatformAdapter.
8
- */
9
-
10
- import { execFile } from "node:child_process";
11
- import * as fs from "node:fs";
12
- import * as os from "node:os";
13
- import * as path from "node:path";
14
- import { getPlatformAdapter } from "../adapter/index.js";
15
- import { formatErrorMessage } from "./format.js";
16
- import { debugLog, debugWarn } from "./log.js";
17
-
18
- /**
19
- * Resolve the current user's home directory safely across platforms.
20
- *
21
- * Priority:
22
- * 1. `os.homedir()`
23
- * 2. `$HOME` or `%USERPROFILE%`
24
- * 3. PlatformAdapter.getTempDir() as a last resort
25
- */
26
- export function getHomeDir(): string {
27
- try {
28
- const home = os.homedir();
29
- if (home && fs.existsSync(home)) {
30
- return home;
31
- }
32
- } catch {
33
- /* fallback */
34
- }
35
-
36
- const envHome = process.env.HOME || process.env.USERPROFILE;
37
- if (envHome && fs.existsSync(envHome)) {
38
- return envHome;
39
- }
40
-
41
- return getPlatformAdapter().getTempDir();
42
- }
43
-
44
- /** Return a path under `~/.openclaw/qqbot` without creating it. */
45
- export function getQQBotDataPath(...subPaths: string[]): string {
46
- return path.join(getHomeDir(), ".openclaw", "qqbot", ...subPaths);
47
- }
48
-
49
- /** Return a path under `~/.openclaw/qqbot`, creating it on demand. */
50
- export function getQQBotDataDir(...subPaths: string[]): string {
51
- const dir = getQQBotDataPath(...subPaths);
52
- if (!fs.existsSync(dir)) {
53
- fs.mkdirSync(dir, { recursive: true });
54
- }
55
- return dir;
56
- }
57
-
58
- /**
59
- * Return a path under `~/.openclaw/media/qqbot` without creating it.
60
- *
61
- * Unlike `getQQBotDataPath`, this lives under OpenClaw's core media allowlist so
62
- * downloaded images and audio can be accessed by framework media tooling.
63
- */
64
- export function getQQBotMediaPath(...subPaths: string[]): string {
65
- return path.join(getHomeDir(), ".openclaw", "media", "qqbot", ...subPaths);
66
- }
67
-
68
- /** Return a path under `~/.openclaw/media/qqbot`, creating it on demand. */
69
- export function getQQBotMediaDir(...subPaths: string[]): string {
70
- const dir = getQQBotMediaPath(...subPaths);
71
- if (!fs.existsSync(dir)) {
72
- fs.mkdirSync(dir, { recursive: true });
73
- }
74
- return dir;
75
- }
76
-
77
- /**
78
- * Return `~/.openclaw/media`, OpenClaw's shared media root.
79
- *
80
- * This mirrors the directory that core's `buildMediaLocalRoots` exposes as an
81
- * allowlisted location (see `openclaw/src/media/local-roots.ts`). Using it as a
82
- * QQ Bot payload root lets the plugin trust framework-produced files that live
83
- * in sibling subdirectories such as `outbound/` (written by
84
- * `saveMediaBuffer(..., "outbound", ...)`) or `inbound/`, while still keeping
85
- * the check anchored to a single, well-known directory.
86
- */
87
- function getOpenClawMediaDir(): string {
88
- return path.join(getHomeDir(), ".openclaw", "media");
89
- }
90
-
91
- // ---- Basic platform information ----
92
-
93
- type PlatformType = "darwin" | "linux" | "win32" | "other";
94
-
95
- export function getPlatform(): PlatformType {
96
- const p = process.platform;
97
- if (p === "darwin" || p === "linux" || p === "win32") {
98
- return p;
99
- }
100
- return "other";
101
- }
102
-
103
- export function isWindows(): boolean {
104
- return process.platform === "win32";
105
- }
106
-
107
- /** Return the preferred temporary directory. */
108
- export function getTempDir(): string {
109
- return getPlatformAdapter().getTempDir();
110
- }
111
-
112
- // ---- ffmpeg detection ----
113
-
114
- let _ffmpegPath: string | null | undefined;
115
- let _ffmpegCheckPromise: Promise<string | null> | null = null;
116
-
117
- /** Detect ffmpeg and return an executable path when available. */
118
- export function detectFfmpeg(): Promise<string | null> {
119
- if (_ffmpegPath !== undefined) {
120
- return Promise.resolve(_ffmpegPath);
121
- }
122
- if (_ffmpegCheckPromise) {
123
- return _ffmpegCheckPromise;
124
- }
125
-
126
- _ffmpegCheckPromise = (async () => {
127
- const envPath = process.env.FFMPEG_PATH;
128
- if (envPath) {
129
- const ok = await testExecutable(envPath, ["-version"]);
130
- if (ok) {
131
- _ffmpegPath = envPath;
132
- debugLog(`[platform] ffmpeg found via FFMPEG_PATH: ${envPath}`);
133
- return _ffmpegPath;
134
- }
135
- debugWarn(`[platform] FFMPEG_PATH set but not working: ${envPath}`);
136
- }
137
-
138
- const cmd = isWindows() ? "ffmpeg.exe" : "ffmpeg";
139
- const ok = await testExecutable(cmd, ["-version"]);
140
- if (ok) {
141
- _ffmpegPath = cmd;
142
- debugLog(`[platform] ffmpeg detected in PATH`);
143
- return _ffmpegPath;
144
- }
145
-
146
- const commonPaths = isWindows()
147
- ? [
148
- "C:\\ffmpeg\\bin\\ffmpeg.exe",
149
- path.join(process.env.LOCALAPPDATA || "", "Programs", "ffmpeg", "bin", "ffmpeg.exe"),
150
- path.join(process.env.ProgramFiles || "", "ffmpeg", "bin", "ffmpeg.exe"),
151
- ]
152
- : [
153
- "/usr/local/bin/ffmpeg",
154
- "/opt/homebrew/bin/ffmpeg",
155
- "/usr/bin/ffmpeg",
156
- "/snap/bin/ffmpeg",
157
- ];
158
-
159
- for (const p of commonPaths) {
160
- if (p && fs.existsSync(p)) {
161
- const works = await testExecutable(p, ["-version"]);
162
- if (works) {
163
- _ffmpegPath = p;
164
- debugLog(`[platform] ffmpeg found at: ${p}`);
165
- return _ffmpegPath;
166
- }
167
- }
168
- }
169
-
170
- _ffmpegPath = null;
171
- return null;
172
- })().finally(() => {
173
- _ffmpegCheckPromise = null;
174
- });
175
-
176
- return _ffmpegCheckPromise;
177
- }
178
-
179
- /** Return true when an executable responds successfully to the given args. */
180
- function testExecutable(cmd: string, args: string[]): Promise<boolean> {
181
- return new Promise((resolve) => {
182
- execFile(cmd, args, { timeout: 5000 }, (err) => {
183
- resolve(!err);
184
- });
185
- });
186
- }
187
-
188
- // ---- silk-wasm detection ----
189
-
190
- let _silkWasmAvailable: boolean | null = null;
191
-
192
- /** Check whether silk-wasm can run in the current environment. */
193
- export async function checkSilkWasmAvailable(): Promise<boolean> {
194
- if (_silkWasmAvailable !== null) {
195
- return _silkWasmAvailable;
196
- }
197
- try {
198
- const { isSilk } = await import("silk-wasm");
199
- isSilk(new Uint8Array(0));
200
- _silkWasmAvailable = true;
201
- debugLog("[platform] silk-wasm: available");
202
- } catch (err) {
203
- _silkWasmAvailable = false;
204
- debugWarn(`[platform] silk-wasm: NOT available (${formatErrorMessage(err)})`);
205
- }
206
- return _silkWasmAvailable;
207
- }
208
-
209
- // ---- Tilde expansion and path normalization ----
210
-
211
- /** Expand `~` to the current user's home directory. */
212
- function expandTilde(p: string): string {
213
- if (!p) {
214
- return p;
215
- }
216
- if (p === "~") {
217
- return getHomeDir();
218
- }
219
- if (p.startsWith("~/") || p.startsWith("~\\")) {
220
- return path.join(getHomeDir(), p.slice(2));
221
- }
222
- return p;
223
- }
224
-
225
- /** Normalize a user-provided path by trimming, stripping `file://`, and expanding `~`. */
226
- export function normalizePath(p: string): string {
227
- let result = p.trim();
228
- if (result.startsWith("file://")) {
229
- result = result.slice("file://".length);
230
- try {
231
- result = decodeURIComponent(result);
232
- } catch {
233
- // Keep the raw string if decoding fails.
234
- }
235
- }
236
- return expandTilde(result);
237
- }
238
-
239
- // ---- Local path detection ----
240
-
241
- /** Return true when the string looks like a local filesystem path rather than a URL. */
242
- export function isLocalPath(p: string): boolean {
243
- if (!p) {
244
- return false;
245
- }
246
- if (p.startsWith("file://")) {
247
- return true;
248
- }
249
- if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) {
250
- return true;
251
- }
252
- if (p.startsWith("/")) {
253
- return true;
254
- }
255
- if (/^[a-zA-Z]:[\\/]/.test(p)) {
256
- return true;
257
- }
258
- if (p.startsWith("\\\\")) {
259
- return true;
260
- }
261
- if (p.startsWith("./") || p.startsWith("../")) {
262
- return true;
263
- }
264
- if (p.startsWith(".\\") || p.startsWith("..\\")) {
265
- return true;
266
- }
267
- return false;
268
- }
269
-
270
- // ---- QQBot media path resolution ----
271
-
272
- function isPathWithinRoot(candidate: string, root: string): boolean {
273
- const relative = path.relative(root, candidate);
274
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
275
- }
276
-
277
- /** Remap legacy or hallucinated QQ Bot local media paths to real files when possible. */
278
- export function resolveQQBotLocalMediaPath(p: string): string {
279
- const normalized = normalizePath(p);
280
- if (!isLocalPath(normalized) || fs.existsSync(normalized)) {
281
- return normalized;
282
- }
283
-
284
- const homeDir = getHomeDir();
285
- const mediaRoot = getQQBotMediaPath();
286
- const dataRoot = getQQBotDataPath();
287
- const workspaceRoot = path.join(homeDir, ".openclaw", "workspace", "qqbot");
288
- const candidateRoots = [
289
- { from: workspaceRoot, to: mediaRoot },
290
- { from: dataRoot, to: mediaRoot },
291
- { from: mediaRoot, to: dataRoot },
292
- ];
293
-
294
- for (const { from, to } of candidateRoots) {
295
- if (!isPathWithinRoot(normalized, from)) {
296
- continue;
297
- }
298
- const relative = path.relative(from, normalized);
299
- const candidate = path.join(to, relative);
300
- if (fs.existsSync(candidate)) {
301
- debugWarn(`[platform] Remapped missing QQBot media path ${normalized} -> ${candidate}`);
302
- return candidate;
303
- }
304
- }
305
-
306
- return normalized;
307
- }
308
-
309
- /**
310
- * Resolve a structured-payload local file path and enforce that it stays within
311
- * QQ Bot-owned storage roots.
312
- */
313
- export function resolveQQBotPayloadLocalFilePath(p: string): string | null {
314
- const candidate = resolveQQBotLocalMediaPath(p);
315
- if (!candidate.trim()) {
316
- return null;
317
- }
318
-
319
- const resolvedCandidate = path.resolve(candidate);
320
- if (!fs.existsSync(resolvedCandidate)) {
321
- return null;
322
- }
323
-
324
- const canonicalCandidate = fs.realpathSync(resolvedCandidate);
325
- // Trust both the QQ Bot-owned subdirectory and OpenClaw's shared `~/.openclaw/media`
326
- // root. Core helpers like `saveMediaBuffer(..., "outbound", ...)` place framework
327
- // attachments under sibling directories (e.g. `media/outbound/`) that are already
328
- // part of the core media allowlist; we mirror that so auto-routed sends work
329
- // without leaving the plugin's trust boundary.
330
- const allowedRoots = [getOpenClawMediaDir(), getQQBotMediaPath()];
331
-
332
- for (const root of allowedRoots) {
333
- const resolvedRoot = path.resolve(root);
334
- const canonicalRoot = fs.existsSync(resolvedRoot)
335
- ? fs.realpathSync(resolvedRoot)
336
- : resolvedRoot;
337
- if (isPathWithinRoot(canonicalCandidate, canonicalRoot)) {
338
- return canonicalCandidate;
339
- }
340
- }
341
-
342
- return null;
343
- }