@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,249 +0,0 @@
1
- /**
2
- * Image dimension helpers for QQ Bot markdown image syntax.
3
- *
4
- * QQ Bot markdown images use `![#widthpx #heightpx](url)`.
5
- */
6
-
7
- import { Buffer } from "node:buffer";
8
- import { getPlatformAdapter } from "../adapter/index.js";
9
- import type { SsrfPolicyConfig } from "../adapter/types.js";
10
- import { formatErrorMessage } from "./format.js";
11
- import { debugLog } from "./log.js";
12
-
13
- interface ImageSize {
14
- width: number;
15
- height: number;
16
- }
17
-
18
- /** Default dimensions used when probing fails. */
19
- const DEFAULT_IMAGE_SIZE: ImageSize = { width: 512, height: 512 };
20
-
21
- /**
22
- * Parse image dimensions from the PNG header.
23
- */
24
- function parsePngSize(buffer: Buffer): ImageSize | null {
25
- // PNG signature: 89 50 4E 47 0D 0A 1A 0A
26
- if (buffer.length < 24) {
27
- return null;
28
- }
29
- if (buffer[0] !== 0x89 || buffer[1] !== 0x50 || buffer[2] !== 0x4e || buffer[3] !== 0x47) {
30
- return null;
31
- }
32
- // The IHDR chunk begins at byte 8, with width/height at 16..23.
33
- const width = buffer.readUInt32BE(16);
34
- const height = buffer.readUInt32BE(20);
35
- return { width, height };
36
- }
37
-
38
- /** Parse image dimensions from JPEG SOF0/SOF2 markers. */
39
- function parseJpegSize(buffer: Buffer): ImageSize | null {
40
- // JPEG signature: FF D8 FF
41
- if (buffer.length < 4) {
42
- return null;
43
- }
44
- if (buffer[0] !== 0xff || buffer[1] !== 0xd8) {
45
- return null;
46
- }
47
-
48
- let offset = 2;
49
- while (offset < buffer.length - 9) {
50
- if (buffer[offset] !== 0xff) {
51
- offset++;
52
- continue;
53
- }
54
-
55
- const marker = buffer[offset + 1];
56
- // SOF0 (0xC0) and SOF2 (0xC2) contain dimensions.
57
- if (marker === 0xc0 || marker === 0xc2) {
58
- // Layout: FF C0 length(2) precision(1) height(2) width(2)
59
- if (offset + 9 <= buffer.length) {
60
- const height = buffer.readUInt16BE(offset + 5);
61
- const width = buffer.readUInt16BE(offset + 7);
62
- return { width, height };
63
- }
64
- }
65
-
66
- // Skip the current block.
67
- if (offset + 3 < buffer.length) {
68
- const blockLength = buffer.readUInt16BE(offset + 2);
69
- offset += 2 + blockLength;
70
- } else {
71
- break;
72
- }
73
- }
74
-
75
- return null;
76
- }
77
-
78
- /** Parse image dimensions from the GIF header. */
79
- function parseGifSize(buffer: Buffer): ImageSize | null {
80
- if (buffer.length < 10) {
81
- return null;
82
- }
83
- const signature = buffer.toString("ascii", 0, 6);
84
- if (signature !== "GIF87a" && signature !== "GIF89a") {
85
- return null;
86
- }
87
- const width = buffer.readUInt16LE(6);
88
- const height = buffer.readUInt16LE(8);
89
- return { width, height };
90
- }
91
-
92
- /** Parse image dimensions from WebP headers. */
93
- function parseWebpSize(buffer: Buffer): ImageSize | null {
94
- if (buffer.length < 30) {
95
- return null;
96
- }
97
-
98
- // Check the RIFF and WEBP signatures.
99
- const riff = buffer.toString("ascii", 0, 4);
100
- const webp = buffer.toString("ascii", 8, 12);
101
- if (riff !== "RIFF" || webp !== "WEBP") {
102
- return null;
103
- }
104
-
105
- const chunkType = buffer.toString("ascii", 12, 16);
106
-
107
- // VP8 (lossy)
108
- if (chunkType === "VP8 ") {
109
- // The VP8 frame header starts at byte 23 and uses the 9D 01 2A signature.
110
- if (buffer.length >= 30 && buffer[23] === 0x9d && buffer[24] === 0x01 && buffer[25] === 0x2a) {
111
- const width = buffer.readUInt16LE(26) & 0x3fff;
112
- const height = buffer.readUInt16LE(28) & 0x3fff;
113
- return { width, height };
114
- }
115
- }
116
-
117
- // VP8L (lossless)
118
- if (chunkType === "VP8L") {
119
- // VP8L signature: 0x2F
120
- if (buffer.length >= 25 && buffer[20] === 0x2f) {
121
- const bits = buffer.readUInt32LE(21);
122
- const width = (bits & 0x3fff) + 1;
123
- const height = ((bits >> 14) & 0x3fff) + 1;
124
- return { width, height };
125
- }
126
- }
127
-
128
- // VP8X (extended format)
129
- if (chunkType === "VP8X") {
130
- if (buffer.length >= 30) {
131
- // Width and height live at 24..26 and 27..29 as 24-bit little-endian values.
132
- const width = (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) + 1;
133
- const height = (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) + 1;
134
- return { width, height };
135
- }
136
- }
137
-
138
- return null;
139
- }
140
-
141
- /** Parse image dimensions from raw image bytes. */
142
- export function parseImageSize(buffer: Buffer): ImageSize | null {
143
- // Try each supported image format in sequence.
144
- return (
145
- parsePngSize(buffer) ?? parseJpegSize(buffer) ?? parseGifSize(buffer) ?? parseWebpSize(buffer)
146
- );
147
- }
148
-
149
- /**
150
- * SSRF policy for image-dimension probing. Generic public-network-only blocking
151
- * (no hostname allowlist) because markdown image URLs can legitimately point to
152
- * any public host, not just QQ-owned CDNs.
153
- */
154
- const IMAGE_PROBE_SSRF_POLICY: SsrfPolicyConfig = {};
155
-
156
- /**
157
- * Fetch image dimensions from a public URL using only the first 64 KB.
158
- *
159
- * Uses {@link fetchRemoteMedia} with SSRF guard to block probes against
160
- * private/reserved/loopback/link-local/metadata destinations.
161
- */
162
- export async function getImageSizeFromUrl(
163
- url: string,
164
- timeoutMs = 5000,
165
- ): Promise<ImageSize | null> {
166
- try {
167
- const controller = new AbortController();
168
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
169
-
170
- try {
171
- const { buffer } = await getPlatformAdapter().fetchMedia({
172
- url,
173
- maxBytes: 65_536,
174
- maxRedirects: 0,
175
- ssrfPolicy: IMAGE_PROBE_SSRF_POLICY,
176
- requestInit: {
177
- signal: controller.signal,
178
- headers: {
179
- Range: "bytes=0-65535",
180
- "User-Agent": "QQBot-Image-Size-Detector/1.0",
181
- },
182
- },
183
- });
184
-
185
- const size = parseImageSize(buffer);
186
- if (size) {
187
- debugLog(
188
- `[image-size] Got size from URL: ${size.width}x${size.height} - ${url.slice(0, 60)}...`,
189
- );
190
- }
191
- return size;
192
- } finally {
193
- clearTimeout(timeoutId);
194
- }
195
- } catch (err) {
196
- debugLog(`[image-size] Error fetching ${url.slice(0, 60)}...: ${formatErrorMessage(err)}`);
197
- return null;
198
- }
199
- }
200
-
201
- /** Parse image dimensions from a Base64 data URL. */
202
- function getImageSizeFromDataUrl(dataUrl: string): ImageSize | null {
203
- try {
204
- // Format: data:image/png;base64,xxxxx
205
- const matches = dataUrl.match(/^data:image\/[^;]+;base64,(.+)$/);
206
- if (!matches) {
207
- return null;
208
- }
209
-
210
- const base64Data = matches[1];
211
- const buffer = Buffer.from(base64Data, "base64");
212
-
213
- const size = parseImageSize(buffer);
214
- if (size) {
215
- debugLog(`[image-size] Got size from Base64: ${size.width}x${size.height}`);
216
- }
217
-
218
- return size;
219
- } catch (err) {
220
- debugLog(`[image-size] Error parsing Base64: ${formatErrorMessage(err)}`);
221
- return null;
222
- }
223
- }
224
-
225
- /**
226
- * Resolve image dimensions from either an HTTP URL or a Base64 data URL.
227
- */
228
- export async function getImageSize(source: string): Promise<ImageSize | null> {
229
- if (source.startsWith("data:")) {
230
- return getImageSizeFromDataUrl(source);
231
- }
232
-
233
- if (source.startsWith("http://") || source.startsWith("https://")) {
234
- return getImageSizeFromUrl(source);
235
- }
236
-
237
- return null;
238
- }
239
-
240
- /** Format a markdown image with QQ Bot width/height annotations. */
241
- export function formatQQBotMarkdownImage(url: string, size: ImageSize | null): string {
242
- const { width, height } = size ?? DEFAULT_IMAGE_SIZE;
243
- return `![#${width}px #${height}px](${url})`;
244
- }
245
-
246
- /** Return true when markdown already contains QQ Bot size annotations. */
247
- export function hasQQBotImageSize(markdownImage: string): boolean {
248
- return /!\[#\d+px\s+#\d+px\]/.test(markdownImage);
249
- }
@@ -1,28 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { debugLog, sanitizeDebugLogValue } from "./log.js";
3
-
4
- const originalDebug = process.env.QQBOT_DEBUG;
5
-
6
- afterEach(() => {
7
- if (originalDebug === undefined) {
8
- delete process.env.QQBOT_DEBUG;
9
- } else {
10
- process.env.QQBOT_DEBUG = originalDebug;
11
- }
12
- vi.restoreAllMocks();
13
- });
14
-
15
- describe("QQBot debug logging", () => {
16
- it("neutralizes control characters in log values", () => {
17
- expect(sanitizeDebugLogValue("before\nforged\r\tentry")).toBe("before forged entry");
18
- });
19
-
20
- it("sanitizes arguments before debug console output", () => {
21
- process.env.QQBOT_DEBUG = "1";
22
- const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
23
-
24
- debugLog("prefix", "line one\nline two");
25
-
26
- expect(logSpy).toHaveBeenCalledWith("prefix line one line two");
27
- });
28
- });
@@ -1,61 +0,0 @@
1
- /**
2
- * QQBot debug logging utilities.
3
- * QQBot 调试日志工具。
4
- *
5
- * Only outputs when the QQBOT_DEBUG environment variable is set,
6
- * preventing user message content from leaking in production logs.
7
- *
8
- * Self-contained within engine/ — no framework SDK dependency.
9
- */
10
-
11
- const isDebug = () => !!process.env.QQBOT_DEBUG;
12
- const MAX_LOG_VALUE_CHARS = 4096;
13
-
14
- export function sanitizeDebugLogValue(value: unknown): string {
15
- let text: string;
16
- if (typeof value === "string") {
17
- text = value;
18
- } else if (value instanceof Error) {
19
- text = value.stack || value.message;
20
- } else {
21
- try {
22
- text = JSON.stringify(value) ?? String(value);
23
- } catch {
24
- text = String(value);
25
- }
26
- }
27
-
28
- const sanitized = text
29
- .replace(/\p{Cc}/gu, " ")
30
- .replace(/\s+/g, " ")
31
- .trim();
32
- if (sanitized.length <= MAX_LOG_VALUE_CHARS) {
33
- return sanitized;
34
- }
35
- return `${sanitized.slice(0, MAX_LOG_VALUE_CHARS)}...`;
36
- }
37
-
38
- function formatDebugLogArgs(args: unknown[]): string {
39
- return args.map(sanitizeDebugLogValue).join(" ");
40
- }
41
-
42
- /** Debug-level log; only outputs when QQBOT_DEBUG is enabled. */
43
- export function debugLog(...args: unknown[]): void {
44
- if (isDebug()) {
45
- console.log(formatDebugLogArgs(args).replace(/\n|\r/g, ""));
46
- }
47
- }
48
-
49
- /** Debug-level warning; only outputs when QQBOT_DEBUG is enabled. */
50
- export function debugWarn(...args: unknown[]): void {
51
- if (isDebug()) {
52
- console.warn(formatDebugLogArgs(args).replace(/\n|\r/g, ""));
53
- }
54
- }
55
-
56
- /** Debug-level error; only outputs when QQBOT_DEBUG is enabled. */
57
- export function debugError(...args: unknown[]): void {
58
- if (isDebug()) {
59
- console.error(formatDebugLogArgs(args).replace(/\n|\r/g, ""));
60
- }
61
- }
@@ -1,32 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { FUZZY_MEDIA_TAG_REGEX, SELF_CLOSING_TAG_REGEX } from "./media-tags.js";
3
-
4
- describe("media-tags with HTML entities", () => {
5
- it("extracts URL from entity-encoded fuzzy tag", () => {
6
- const input = "&lt;qqimg&gt;https://example.com/a.png&lt;/qqimg&gt;";
7
- FUZZY_MEDIA_TAG_REGEX.lastIndex = 0;
8
- const match = FUZZY_MEDIA_TAG_REGEX.exec(input);
9
- expect(match?.[2]).toBe("https://example.com/a.png");
10
- });
11
-
12
- it("extracts URL from mixed entity+plain tag", () => {
13
- const input = "&lt;qqimg&gt;https://example.com/b.png</qqimg>";
14
- FUZZY_MEDIA_TAG_REGEX.lastIndex = 0;
15
- const match = FUZZY_MEDIA_TAG_REGEX.exec(input);
16
- expect(match?.[2]).toBe("https://example.com/b.png");
17
- });
18
-
19
- it("extracts file from entity-encoded self-closing tag", () => {
20
- const input = '&lt;qqmedia file="https://example.com/c.zip" /&gt;';
21
- SELF_CLOSING_TAG_REGEX.lastIndex = 0;
22
- const match = SELF_CLOSING_TAG_REGEX.exec(input);
23
- expect(match?.[2]).toBe("https://example.com/c.zip");
24
- });
25
-
26
- it("does not match invalid input", () => {
27
- const input = "no tag here";
28
- FUZZY_MEDIA_TAG_REGEX.lastIndex = 0;
29
- const match = FUZZY_MEDIA_TAG_REGEX.exec(input);
30
- expect(match).toBeNull();
31
- });
32
- });
@@ -1,177 +0,0 @@
1
- /**
2
- * Media tag normalization for QQ Bot messages.
3
- *
4
- * Normalizes malformed `<qqimg>`, `<qqvoice>`, etc. tags emitted by
5
- * smaller models into canonical wrapped-tag format.
6
- *
7
- * Zero external dependencies.
8
- */
9
-
10
- /** Lowercase and trim a string, returning empty string for falsy input. */
11
- function lc(s: string): string {
12
- return (s ?? "").toLowerCase().trim();
13
- }
14
-
15
- /** Expand `~` prefix to the process home directory. */
16
- function expandTilde(p: string): string {
17
- if (!p) {
18
- return p;
19
- }
20
- const home =
21
- typeof process !== "undefined" ? (process.env.HOME ?? process.env.USERPROFILE) : undefined;
22
- if (!home) {
23
- return p;
24
- }
25
- if (p === "~") {
26
- return home;
27
- }
28
- if (p.startsWith("~/") || p.startsWith("~\\")) {
29
- return `${home}/${p.slice(2)}`;
30
- }
31
- return p;
32
- }
33
-
34
- // Canonical media tags. `qqmedia` is the generic auto-routing tag.
35
- const VALID_TAGS = ["qqimg", "qqvoice", "qqvideo", "qqfile", "qqmedia"] as const;
36
-
37
- // Lowercased aliases that should normalize to the canonical tag set.
38
- const TAG_ALIASES: Record<string, (typeof VALID_TAGS)[number]> = {
39
- qq_img: "qqimg",
40
- qqimage: "qqimg",
41
- qq_image: "qqimg",
42
- qqpic: "qqimg",
43
- qq_pic: "qqimg",
44
- qqpicture: "qqimg",
45
- qq_picture: "qqimg",
46
- qqphoto: "qqimg",
47
- qq_photo: "qqimg",
48
- img: "qqimg",
49
- image: "qqimg",
50
- pic: "qqimg",
51
- picture: "qqimg",
52
- photo: "qqimg",
53
- qq_voice: "qqvoice",
54
- qqaudio: "qqvoice",
55
- qq_audio: "qqvoice",
56
- voice: "qqvoice",
57
- audio: "qqvoice",
58
- qq_video: "qqvideo",
59
- video: "qqvideo",
60
- qq_file: "qqfile",
61
- qqdoc: "qqfile",
62
- qq_doc: "qqfile",
63
- file: "qqfile",
64
- doc: "qqfile",
65
- document: "qqfile",
66
- qq_media: "qqmedia",
67
- media: "qqmedia",
68
- attachment: "qqmedia",
69
- attach: "qqmedia",
70
- qqattachment: "qqmedia",
71
- qq_attachment: "qqmedia",
72
- qqsend: "qqmedia",
73
- qq_send: "qqmedia",
74
- send: "qqmedia",
75
- };
76
-
77
- const ALL_TAG_NAMES = [...VALID_TAGS, ...Object.keys(TAG_ALIASES)];
78
- ALL_TAG_NAMES.sort((a, b) => b.length - a.length);
79
-
80
- const TAG_NAME_PATTERN = ALL_TAG_NAMES.join("|");
81
-
82
- const LEFT_BRACKET = "(?:[<\uff1c\u003c]|&lt;)";
83
- const RIGHT_BRACKET = "(?:[>\uff1e\u003e]|&gt;)";
84
-
85
- /** Match self-closing media-tag syntax with file/src/path/url attributes. */
86
- export const SELF_CLOSING_TAG_REGEX = new RegExp(
87
- "`?" +
88
- LEFT_BRACKET +
89
- "\\s*(" +
90
- TAG_NAME_PATTERN +
91
- ")" +
92
- "(?:\\s+(?!file|src|path|url)[a-z_-]+\\s*=\\s*[\"']?[^\"'\\s\uff1c<>\uff1e>]*?[\"']?)*" +
93
- "\\s+(?:file|src|path|url)\\s*=\\s*" +
94
- "[\"']?" +
95
- "([^\"'\\s>\uff1e]+?)" +
96
- "[\"']?" +
97
- "(?:\\s+[a-z_-]+\\s*=\\s*[\"']?[^\"'\\s\uff1c<>\uff1e>]*?[\"']?)*" +
98
- "\\s*/?" +
99
- "\\s*" +
100
- RIGHT_BRACKET +
101
- "`?",
102
- "gi",
103
- );
104
-
105
- /** Match malformed wrapped media tags that should be normalized. */
106
- export const FUZZY_MEDIA_TAG_REGEX = new RegExp(
107
- "`?" +
108
- LEFT_BRACKET +
109
- "\\s*(" +
110
- TAG_NAME_PATTERN +
111
- ")\\s*" +
112
- RIGHT_BRACKET +
113
- "[\"']?\\s*" +
114
- "([^<\uff1c<\uff1e>\"'`]+?)" +
115
- "\\s*[\"']?" +
116
- LEFT_BRACKET +
117
- "\\s*/?\\s*(?:" +
118
- TAG_NAME_PATTERN +
119
- ")\\s*" +
120
- RIGHT_BRACKET +
121
- "`?",
122
- "gi",
123
- );
124
-
125
- /** Normalize a raw tag name into the canonical tag set. */
126
- function resolveTagName(raw: string): (typeof VALID_TAGS)[number] {
127
- const lower = lc(raw);
128
- if ((VALID_TAGS as readonly string[]).includes(lower)) {
129
- return lower as (typeof VALID_TAGS)[number];
130
- }
131
- return TAG_ALIASES[lower] ?? "qqimg";
132
- }
133
-
134
- /** Match wrapped tags whose bodies need newline and tab cleanup. */
135
- const MULTILINE_TAG_CLEANUP = new RegExp(
136
- "(" +
137
- LEFT_BRACKET +
138
- "\\s*(?:" +
139
- TAG_NAME_PATTERN +
140
- ")\\s*" +
141
- RIGHT_BRACKET +
142
- ")" +
143
- "([\\s\\S]*?)" +
144
- "(" +
145
- LEFT_BRACKET +
146
- "\\s*/?\\s*(?:" +
147
- TAG_NAME_PATTERN +
148
- ")\\s*" +
149
- RIGHT_BRACKET +
150
- ")",
151
- "gi",
152
- );
153
-
154
- /** Normalize malformed media-tag output into canonical wrapped tags. */
155
- export function normalizeMediaTags(text: string): string {
156
- const normalizeWrappedTag = (_match: string, rawTag: string, content: string): string => {
157
- const tag = resolveTagName(rawTag);
158
- const trimmed = content.trim();
159
- if (!trimmed) {
160
- return _match;
161
- }
162
- const expanded = expandTilde(trimmed);
163
- return `<${tag}>${expanded}</${tag}>`;
164
- };
165
-
166
- let cleaned = text.replace(SELF_CLOSING_TAG_REGEX, normalizeWrappedTag);
167
-
168
- cleaned = cleaned.replace(
169
- MULTILINE_TAG_CLEANUP,
170
- (_m, open: string, body: string, close: string) => {
171
- const flat = body.replace(/[\r\n\t]+/g, " ").replace(/ {2,}/g, " ");
172
- return open + flat + close;
173
- },
174
- );
175
-
176
- return cleaned.replace(FUZZY_MEDIA_TAG_REGEX, normalizeWrappedTag);
177
- }
@@ -1,68 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- decodeCronPayload,
4
- encodePayloadForCron,
5
- isCronReminderPayload,
6
- isMediaPayload,
7
- parseQQBotPayload,
8
- type CronReminderPayload,
9
- } from "./payload.js";
10
-
11
- describe("engine/utils/payload", () => {
12
- it("returns original text for non-payload replies", () => {
13
- const result = parseQQBotPayload(" plain reply ");
14
-
15
- expect(result).toEqual({ isPayload: false, text: " plain reply " });
16
- });
17
-
18
- it("parses a media payload", () => {
19
- const result = parseQQBotPayload(
20
- 'QQBOT_PAYLOAD: {"type":"media","mediaType":"image","source":"url","path":"https://example.test/a.png","caption":"cap"}',
21
- );
22
-
23
- expect(result.isPayload).toBe(true);
24
- expect(result.payload).toEqual({
25
- type: "media",
26
- mediaType: "image",
27
- source: "url",
28
- path: "https://example.test/a.png",
29
- caption: "cap",
30
- });
31
- expect(result.payload && isMediaPayload(result.payload)).toBe(true);
32
- });
33
-
34
- it("rejects malformed or incomplete payloads", () => {
35
- expect(parseQQBotPayload("QQBOT_PAYLOAD:").error).toBe("Payload body is empty");
36
- expect(parseQQBotPayload("QQBOT_PAYLOAD: {bad json").error).toContain("Failed to parse JSON");
37
- expect(parseQQBotPayload('QQBOT_PAYLOAD: {"type":"media","mediaType":"image"}').error).toBe(
38
- "media payload is missing required fields (mediaType, source, path)",
39
- );
40
- });
41
-
42
- it("round-trips cron reminder payloads through the stored format", () => {
43
- const payload: CronReminderPayload = {
44
- type: "cron_reminder",
45
- content: "standup",
46
- targetType: "group",
47
- targetAddress: "group-openid",
48
- originalMessageId: "msg-1",
49
- };
50
-
51
- const encoded = encodePayloadForCron(payload);
52
- expect(encoded).toMatch(/^QQBOT_CRON:/);
53
-
54
- const decoded = decodeCronPayload(encoded);
55
- expect(decoded).toEqual({ isCronPayload: true, payload });
56
- expect(decoded.payload && isCronReminderPayload(decoded.payload)).toBe(true);
57
- });
58
-
59
- it("reports cron decode errors without throwing", () => {
60
- expect(decodeCronPayload("plain")).toEqual({ isCronPayload: false });
61
- expect(decodeCronPayload("QQBOT_CRON:").error).toBe("Cron payload body is empty");
62
-
63
- const wrongType = Buffer.from('{"type":"media"}', "utf-8").toString("base64");
64
- expect(decodeCronPayload(`QQBOT_CRON:${wrongType}`).error).toBe(
65
- "Expected type cron_reminder but got media",
66
- );
67
- });
68
- });