@openclaw/qqbot 2026.5.2-beta.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.
- package/dist/api.js +646 -0
- package/dist/approval-cg0SVahb.js +94 -0
- package/dist/channel-N6Y_Rcjp.js +551 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.setup-ByCRAe5H.js +25 -0
- package/dist/config-D6545NkC.js +94 -0
- package/dist/config-schema-DFcjQw73.js +309 -0
- package/dist/exec-approvals-COUsM6wZ.js +138 -0
- package/dist/gateway-CmSUJKSt.js +6726 -0
- package/dist/handler-runtime-Bqm6N0WG.js +120 -0
- package/dist/index.js +29 -0
- package/dist/narrowing-BoieBTIU.js +25 -0
- package/dist/outbound-BJfhwrPg.js +1357 -0
- package/dist/request-context-DXtpwWui.js +921 -0
- package/dist/resolve-D_06fV6-.js +284 -0
- package/dist/runtime-BJAS3eXW.js +18 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-api.js +56 -0
- package/dist/sender-p-B14eLG.js +1987 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/string-normalize-Ci6NM5DE.js +98 -0
- package/dist/target-parser-Y0prnrXD.js +245 -0
- package/package.json +15 -6
- package/api.ts +0 -56
- package/channel-plugin-api.ts +0 -1
- package/index.ts +0 -29
- package/runtime-api.ts +0 -9
- package/setup-entry.ts +0 -9
- package/setup-plugin-api.ts +0 -3
- package/src/bridge/approval/capability.ts +0 -237
- package/src/bridge/approval/handler-runtime.ts +0 -204
- package/src/bridge/bootstrap.ts +0 -135
- package/src/bridge/channel-entry.ts +0 -18
- package/src/bridge/commands/framework-context-adapter.ts +0 -60
- package/src/bridge/commands/framework-registration.ts +0 -47
- package/src/bridge/commands/from-parser.test.ts +0 -86
- package/src/bridge/commands/from-parser.ts +0 -60
- package/src/bridge/commands/result-dispatcher.ts +0 -76
- package/src/bridge/config-shared.ts +0 -132
- package/src/bridge/config.ts +0 -111
- package/src/bridge/gateway.ts +0 -174
- package/src/bridge/logger.ts +0 -31
- package/src/bridge/narrowing.ts +0 -31
- package/src/bridge/plugin-version.test.ts +0 -146
- package/src/bridge/plugin-version.ts +0 -102
- package/src/bridge/runtime.ts +0 -25
- package/src/bridge/sdk-adapter.ts +0 -131
- package/src/bridge/setup/finalize.ts +0 -144
- package/src/bridge/setup/surface.ts +0 -34
- package/src/bridge/tools/channel.ts +0 -58
- package/src/bridge/tools/index.ts +0 -15
- package/src/bridge/tools/remind.test.ts +0 -124
- package/src/bridge/tools/remind.ts +0 -91
- package/src/channel.setup.ts +0 -33
- package/src/channel.ts +0 -288
- package/src/command-auth.test.ts +0 -62
- package/src/config-schema.ts +0 -84
- package/src/config.test.ts +0 -364
- package/src/engine/access/access-control.test.ts +0 -198
- package/src/engine/access/access-control.ts +0 -226
- package/src/engine/access/index.ts +0 -16
- package/src/engine/access/resolve-policy.test.ts +0 -59
- package/src/engine/access/resolve-policy.ts +0 -57
- package/src/engine/access/sender-match.test.ts +0 -60
- package/src/engine/access/sender-match.ts +0 -55
- package/src/engine/access/types.ts +0 -53
- package/src/engine/adapter/audio.port.ts +0 -27
- package/src/engine/adapter/commands.port.ts +0 -22
- package/src/engine/adapter/history.port.ts +0 -52
- package/src/engine/adapter/index.ts +0 -139
- package/src/engine/adapter/mention-gate.port.ts +0 -50
- package/src/engine/adapter/types.ts +0 -38
- package/src/engine/api/api-client.ts +0 -212
- package/src/engine/api/media-chunked.test.ts +0 -336
- package/src/engine/api/media-chunked.ts +0 -622
- package/src/engine/api/media.ts +0 -218
- package/src/engine/api/messages.ts +0 -293
- package/src/engine/api/retry.ts +0 -217
- package/src/engine/api/routes.ts +0 -95
- package/src/engine/api/token.ts +0 -271
- package/src/engine/approval/index.test.ts +0 -22
- package/src/engine/approval/index.ts +0 -224
- package/src/engine/commands/builtin/log-helpers.ts +0 -319
- package/src/engine/commands/builtin/register-all.ts +0 -17
- package/src/engine/commands/builtin/register-approve.ts +0 -201
- package/src/engine/commands/builtin/register-basic.ts +0 -95
- package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
- package/src/engine/commands/builtin/register-logs.ts +0 -20
- package/src/engine/commands/builtin/register-streaming.ts +0 -137
- package/src/engine/commands/builtin/state.ts +0 -31
- package/src/engine/commands/slash-command-auth.ts +0 -48
- package/src/engine/commands/slash-command-handler.ts +0 -146
- package/src/engine/commands/slash-commands-impl.test.ts +0 -8
- package/src/engine/commands/slash-commands-impl.ts +0 -61
- package/src/engine/commands/slash-commands.ts +0 -199
- package/src/engine/config/credential-backup.test.ts +0 -88
- package/src/engine/config/credential-backup.ts +0 -107
- package/src/engine/config/credentials.ts +0 -76
- package/src/engine/config/group.test.ts +0 -234
- package/src/engine/config/group.ts +0 -299
- package/src/engine/config/resolve.test.ts +0 -152
- package/src/engine/config/resolve.ts +0 -283
- package/src/engine/config/setup-logic.ts +0 -84
- package/src/engine/engine-import-boundary.test.ts +0 -73
- package/src/engine/gateway/codec.ts +0 -47
- package/src/engine/gateway/constants.ts +0 -117
- package/src/engine/gateway/event-dispatcher.ts +0 -177
- package/src/engine/gateway/gateway-connection.ts +0 -371
- package/src/engine/gateway/gateway.ts +0 -291
- package/src/engine/gateway/inbound-attachments.test.ts +0 -126
- package/src/engine/gateway/inbound-attachments.ts +0 -360
- package/src/engine/gateway/inbound-context.ts +0 -195
- package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
- package/src/engine/gateway/inbound-pipeline.ts +0 -235
- package/src/engine/gateway/interaction-handler.ts +0 -220
- package/src/engine/gateway/message-queue.test.ts +0 -282
- package/src/engine/gateway/message-queue.ts +0 -499
- package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
- package/src/engine/gateway/outbound-dispatch.ts +0 -575
- package/src/engine/gateway/reconnect.ts +0 -199
- package/src/engine/gateway/stages/access-stage.ts +0 -132
- package/src/engine/gateway/stages/assembly-stage.ts +0 -156
- package/src/engine/gateway/stages/content-stage.test.ts +0 -77
- package/src/engine/gateway/stages/content-stage.ts +0 -77
- package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
- package/src/engine/gateway/stages/envelope-stage.ts +0 -144
- package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
- package/src/engine/gateway/stages/index.ts +0 -18
- package/src/engine/gateway/stages/quote-stage.ts +0 -113
- package/src/engine/gateway/stages/refidx-stage.ts +0 -62
- package/src/engine/gateway/stages/stub-contexts.ts +0 -116
- package/src/engine/gateway/types.ts +0 -264
- package/src/engine/gateway/typing-keepalive.ts +0 -79
- package/src/engine/group/activation.test.ts +0 -114
- package/src/engine/group/activation.ts +0 -147
- package/src/engine/group/history.test.ts +0 -314
- package/src/engine/group/history.ts +0 -321
- package/src/engine/group/mention.test.ts +0 -141
- package/src/engine/group/mention.ts +0 -197
- package/src/engine/group/message-gating.test.ts +0 -188
- package/src/engine/group/message-gating.ts +0 -216
- package/src/engine/messaging/decode-media-path.ts +0 -82
- package/src/engine/messaging/media-source.ts +0 -215
- package/src/engine/messaging/media-type-detect.ts +0 -37
- package/src/engine/messaging/outbound-audio-port.ts +0 -38
- package/src/engine/messaging/outbound-deliver.ts +0 -810
- package/src/engine/messaging/outbound-media-send.ts +0 -702
- package/src/engine/messaging/outbound-reply.ts +0 -27
- package/src/engine/messaging/outbound-result-helpers.ts +0 -54
- package/src/engine/messaging/outbound-types.ts +0 -45
- package/src/engine/messaging/outbound.ts +0 -485
- package/src/engine/messaging/reply-dispatcher.ts +0 -597
- package/src/engine/messaging/reply-limiter.ts +0 -164
- package/src/engine/messaging/sender.ts +0 -729
- package/src/engine/messaging/streaming-c2c.ts +0 -1192
- package/src/engine/messaging/streaming-media-send.ts +0 -544
- package/src/engine/messaging/target-parser.ts +0 -104
- package/src/engine/ref/format-message-ref.ts +0 -142
- package/src/engine/ref/format-ref-entry.test.ts +0 -60
- package/src/engine/ref/format-ref-entry.ts +0 -27
- package/src/engine/ref/store.ts +0 -208
- package/src/engine/ref/types.ts +0 -27
- package/src/engine/session/known-users.ts +0 -137
- package/src/engine/session/session-store.ts +0 -204
- package/src/engine/tools/channel-api.ts +0 -244
- package/src/engine/tools/remind-logic.test.ts +0 -280
- package/src/engine/tools/remind-logic.ts +0 -377
- package/src/engine/types.ts +0 -313
- package/src/engine/utils/attachment-tags.test.ts +0 -186
- package/src/engine/utils/attachment-tags.ts +0 -174
- package/src/engine/utils/audio.test.ts +0 -250
- package/src/engine/utils/audio.ts +0 -585
- package/src/engine/utils/data-paths.ts +0 -38
- package/src/engine/utils/diagnostics.ts +0 -109
- package/src/engine/utils/file-utils.test.ts +0 -72
- package/src/engine/utils/file-utils.ts +0 -225
- package/src/engine/utils/format.test.ts +0 -68
- package/src/engine/utils/format.ts +0 -70
- package/src/engine/utils/image-size.test.ts +0 -158
- package/src/engine/utils/image-size.ts +0 -249
- package/src/engine/utils/log.test.ts +0 -28
- package/src/engine/utils/log.ts +0 -61
- package/src/engine/utils/media-tags.test.ts +0 -32
- package/src/engine/utils/media-tags.ts +0 -177
- package/src/engine/utils/payload.test.ts +0 -68
- package/src/engine/utils/payload.ts +0 -145
- package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
- package/src/engine/utils/platform.test.ts +0 -148
- package/src/engine/utils/platform.ts +0 -343
- package/src/engine/utils/request-context.ts +0 -60
- package/src/engine/utils/string-normalize.ts +0 -91
- package/src/engine/utils/stt.test.ts +0 -104
- package/src/engine/utils/stt.ts +0 -100
- package/src/engine/utils/text-parsing.test.ts +0 -29
- package/src/engine/utils/text-parsing.ts +0 -155
- package/src/engine/utils/upload-cache.ts +0 -96
- package/src/engine/utils/voice-text.ts +0 -15
- package/src/exec-approvals.ts +0 -218
- package/src/manifest-schema.test.ts +0 -56
- package/src/qqbot-test-support.ts +0 -29
- package/src/types.ts +0 -210
- 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 ``.
|
|
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 ``;
|
|
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
|
-
});
|
package/src/engine/utils/log.ts
DELETED
|
@@ -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 = "<qqimg>https://example.com/a.png</qqimg>";
|
|
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 = "<qqimg>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 = '<qqmedia file="https://example.com/c.zip" />';
|
|
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]|<)";
|
|
83
|
-
const RIGHT_BRACKET = "(?:[>\uff1e\u003e]|>)";
|
|
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
|
-
});
|