@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,188 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
resolveGroupMessageGate,
|
|
4
|
-
type GroupMessageGateInput,
|
|
5
|
-
type GroupMessageGateResult,
|
|
6
|
-
} from "./message-gating.js";
|
|
7
|
-
|
|
8
|
-
// Compose a full input so each test can override just the interesting axis.
|
|
9
|
-
function input(overrides: Partial<GroupMessageGateInput>): GroupMessageGateInput {
|
|
10
|
-
return {
|
|
11
|
-
ignoreOtherMentions: false,
|
|
12
|
-
hasAnyMention: false,
|
|
13
|
-
wasMentioned: false,
|
|
14
|
-
implicitMention: false,
|
|
15
|
-
allowTextCommands: true,
|
|
16
|
-
isControlCommand: false,
|
|
17
|
-
commandAuthorized: false,
|
|
18
|
-
requireMention: true,
|
|
19
|
-
canDetectMention: true,
|
|
20
|
-
...overrides,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function expectAction(
|
|
25
|
-
result: GroupMessageGateResult,
|
|
26
|
-
action: GroupMessageGateResult["action"],
|
|
27
|
-
): void {
|
|
28
|
-
expect(result.action).toBe(action);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe("engine/group/message-gating", () => {
|
|
32
|
-
describe("Layer 1: ignoreOtherMentions", () => {
|
|
33
|
-
it("drops messages that @other users when enabled", () => {
|
|
34
|
-
const result = resolveGroupMessageGate(
|
|
35
|
-
input({ ignoreOtherMentions: true, hasAnyMention: true }),
|
|
36
|
-
);
|
|
37
|
-
expectAction(result, "drop_other_mention");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("does NOT drop when the bot itself was @-ed", () => {
|
|
41
|
-
const result = resolveGroupMessageGate(
|
|
42
|
-
input({ ignoreOtherMentions: true, hasAnyMention: true, wasMentioned: true }),
|
|
43
|
-
);
|
|
44
|
-
expectAction(result, "pass");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("does NOT drop when implicitly mentioned via quote", () => {
|
|
48
|
-
const result = resolveGroupMessageGate(
|
|
49
|
-
input({ ignoreOtherMentions: true, hasAnyMention: true, implicitMention: true }),
|
|
50
|
-
);
|
|
51
|
-
expectAction(result, "pass");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("is inactive when ignoreOtherMentions is off", () => {
|
|
55
|
-
const result = resolveGroupMessageGate(
|
|
56
|
-
input({ ignoreOtherMentions: false, hasAnyMention: true }),
|
|
57
|
-
);
|
|
58
|
-
// Falls through to mention gate — requireMention on, so skipped.
|
|
59
|
-
expectAction(result, "skip_no_mention");
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("Layer 2: unauthorized control command", () => {
|
|
64
|
-
it("silently blocks an unauthorized /stop", () => {
|
|
65
|
-
const result = resolveGroupMessageGate(
|
|
66
|
-
input({ isControlCommand: true, commandAuthorized: false }),
|
|
67
|
-
);
|
|
68
|
-
expectAction(result, "block_unauthorized_command");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("passes through when sender is authorized", () => {
|
|
72
|
-
const result = resolveGroupMessageGate(
|
|
73
|
-
input({ isControlCommand: true, commandAuthorized: true, wasMentioned: true }),
|
|
74
|
-
);
|
|
75
|
-
expectAction(result, "pass");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("does not trigger when text commands are disabled", () => {
|
|
79
|
-
const result = resolveGroupMessageGate(
|
|
80
|
-
input({
|
|
81
|
-
allowTextCommands: false,
|
|
82
|
-
isControlCommand: true,
|
|
83
|
-
commandAuthorized: false,
|
|
84
|
-
wasMentioned: true,
|
|
85
|
-
}),
|
|
86
|
-
);
|
|
87
|
-
// allowTextCommands=false skips the block, so the mention gate decides.
|
|
88
|
-
expectAction(result, "pass");
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("Layer 3: mention gating", () => {
|
|
93
|
-
it("requires @bot when requireMention is on", () => {
|
|
94
|
-
const result = resolveGroupMessageGate(input({ requireMention: true }));
|
|
95
|
-
expectAction(result, "skip_no_mention");
|
|
96
|
-
expect(result.effectiveWasMentioned).toBe(false);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("passes through when explicitly mentioned", () => {
|
|
100
|
-
const result = resolveGroupMessageGate(input({ requireMention: true, wasMentioned: true }));
|
|
101
|
-
expectAction(result, "pass");
|
|
102
|
-
expect(result.effectiveWasMentioned).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("passes through on implicit mention", () => {
|
|
106
|
-
const result = resolveGroupMessageGate(
|
|
107
|
-
input({ requireMention: true, implicitMention: true }),
|
|
108
|
-
);
|
|
109
|
-
expectAction(result, "pass");
|
|
110
|
-
expect(result.effectiveWasMentioned).toBe(true);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it("passes through when requireMention is off", () => {
|
|
114
|
-
const result = resolveGroupMessageGate(input({ requireMention: false }));
|
|
115
|
-
expectAction(result, "pass");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("passes through when mention cannot be detected (DMs)", () => {
|
|
119
|
-
const result = resolveGroupMessageGate(
|
|
120
|
-
input({ requireMention: true, canDetectMention: false }),
|
|
121
|
-
);
|
|
122
|
-
expectAction(result, "pass");
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe("command bypass", () => {
|
|
127
|
-
it("bypasses mention gate for an authorized control command", () => {
|
|
128
|
-
const result = resolveGroupMessageGate(
|
|
129
|
-
input({
|
|
130
|
-
requireMention: true,
|
|
131
|
-
isControlCommand: true,
|
|
132
|
-
commandAuthorized: true,
|
|
133
|
-
allowTextCommands: true,
|
|
134
|
-
}),
|
|
135
|
-
);
|
|
136
|
-
expectAction(result, "pass");
|
|
137
|
-
expect(result.shouldBypassMention).toBe(true);
|
|
138
|
-
expect(result.effectiveWasMentioned).toBe(true);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("does NOT bypass when the command @-s another user", () => {
|
|
142
|
-
const result = resolveGroupMessageGate(
|
|
143
|
-
input({
|
|
144
|
-
requireMention: true,
|
|
145
|
-
isControlCommand: true,
|
|
146
|
-
commandAuthorized: true,
|
|
147
|
-
hasAnyMention: true,
|
|
148
|
-
}),
|
|
149
|
-
);
|
|
150
|
-
expectAction(result, "skip_no_mention");
|
|
151
|
-
expect(result.shouldBypassMention).toBe(false);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("is a no-op when requireMention is off", () => {
|
|
155
|
-
const result = resolveGroupMessageGate(
|
|
156
|
-
input({
|
|
157
|
-
requireMention: false,
|
|
158
|
-
isControlCommand: true,
|
|
159
|
-
commandAuthorized: true,
|
|
160
|
-
}),
|
|
161
|
-
);
|
|
162
|
-
expectAction(result, "pass");
|
|
163
|
-
// requireMention=false means bypass is unnecessary (condition 1 fails).
|
|
164
|
-
expect(result.shouldBypassMention).toBe(false);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe("priority ordering", () => {
|
|
169
|
-
it("layer 1 wins over layer 2 (ignoreOtherMentions before block)", () => {
|
|
170
|
-
const result = resolveGroupMessageGate(
|
|
171
|
-
input({
|
|
172
|
-
ignoreOtherMentions: true,
|
|
173
|
-
hasAnyMention: true,
|
|
174
|
-
isControlCommand: true,
|
|
175
|
-
commandAuthorized: false,
|
|
176
|
-
}),
|
|
177
|
-
);
|
|
178
|
-
expectAction(result, "drop_other_mention");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("layer 2 wins over layer 3 (unauthorized command before skip)", () => {
|
|
182
|
-
const result = resolveGroupMessageGate(
|
|
183
|
-
input({ requireMention: true, isControlCommand: true, commandAuthorized: false }),
|
|
184
|
-
);
|
|
185
|
-
expectAction(result, "block_unauthorized_command");
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
});
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Group message gate — unified entry point for group inbound gating.
|
|
3
|
-
*
|
|
4
|
-
* Collapses three orthogonal rules that previously lived in ad-hoc spots
|
|
5
|
-
* of the standalone gateway into a single pure function. Callers pass in
|
|
6
|
-
* the message's mention state plus the resolved configuration, and get
|
|
7
|
-
* back a structured action telling them how to handle the message.
|
|
8
|
-
*
|
|
9
|
-
* Evaluation order (short-circuit at the first match):
|
|
10
|
-
* 1. `ignoreOtherMentions` — message @-s someone else but not the bot
|
|
11
|
-
* → `drop_other_mention` (record to history,
|
|
12
|
-
* then drop). Implicit mentions (e.g. quoting
|
|
13
|
-
* a bot reply) still count as @bot.
|
|
14
|
-
* 2. `block_unauthorized_command` — sender is not allowed to run control
|
|
15
|
-
* commands (text starts with `/xxx`)
|
|
16
|
-
* → silently drop.
|
|
17
|
-
* 3. `mention gating` — when `requireMention` is on, non-@bot messages
|
|
18
|
-
* are `skip_no_mention`'d (still buffered to
|
|
19
|
-
* history). Authorized control commands can
|
|
20
|
-
* **bypass** the gate as long as the message does
|
|
21
|
-
* not @anyone else at the same time.
|
|
22
|
-
* 4. Otherwise → `pass` (the message will reach the AI pipeline).
|
|
23
|
-
*
|
|
24
|
-
* All inputs are plain data; there is no I/O and no mutation, so the
|
|
25
|
-
* function is safe to share between the built-in and standalone builds.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
// ────────────────────── Types ──────────────────────
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Structured action returned by {@link resolveGroupMessageGate}.
|
|
32
|
-
*
|
|
33
|
-
* - `drop_other_mention` — message @-s another user but not the bot;
|
|
34
|
-
* record to the group history cache and
|
|
35
|
-
* drop without hitting the AI.
|
|
36
|
-
* - `block_unauthorized_command` — silently refuse a control command from
|
|
37
|
-
* an unauthorized sender (no history
|
|
38
|
-
* write, no AI call).
|
|
39
|
-
* - `skip_no_mention` — `requireMention` is on and the message
|
|
40
|
-
* does not @bot; record to history but
|
|
41
|
-
* skip AI dispatch.
|
|
42
|
-
* - `pass` — forward the message to the AI pipeline.
|
|
43
|
-
*/
|
|
44
|
-
type GroupMessageGateAction =
|
|
45
|
-
| "drop_other_mention"
|
|
46
|
-
| "block_unauthorized_command"
|
|
47
|
-
| "skip_no_mention"
|
|
48
|
-
| "pass";
|
|
49
|
-
|
|
50
|
-
/** Gate evaluation result. */
|
|
51
|
-
export interface GroupMessageGateResult {
|
|
52
|
-
/** The action the caller should take. */
|
|
53
|
-
action: GroupMessageGateAction;
|
|
54
|
-
/**
|
|
55
|
-
* Effective mention state after combining raw mention detection with
|
|
56
|
-
* implicit / bypass signals. Only meaningful when `action === "pass"`.
|
|
57
|
-
*/
|
|
58
|
-
effectiveWasMentioned: boolean;
|
|
59
|
-
/**
|
|
60
|
-
* Whether the control-command bypass was applied to flip a missing
|
|
61
|
-
* mention into `pass`. Only meaningful when `action === "pass"`.
|
|
62
|
-
*/
|
|
63
|
-
shouldBypassMention: boolean;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Input for {@link resolveGroupMessageGate}. */
|
|
67
|
-
export interface GroupMessageGateInput {
|
|
68
|
-
// ---- ignoreOtherMentions layer ----
|
|
69
|
-
/** Per-group config: drop messages that @someone other than the bot. */
|
|
70
|
-
ignoreOtherMentions: boolean;
|
|
71
|
-
/** Whether the message contains *any* @mention (including @other-user). */
|
|
72
|
-
hasAnyMention: boolean;
|
|
73
|
-
/**
|
|
74
|
-
* Whether the QQ event explicitly @-s the bot (via `mentions[].is_you`
|
|
75
|
-
* or `GROUP_AT_MESSAGE_CREATE`).
|
|
76
|
-
*/
|
|
77
|
-
wasMentioned: boolean;
|
|
78
|
-
/**
|
|
79
|
-
* Implicit mention — e.g. the message quotes an earlier bot reply.
|
|
80
|
-
* Treated as equivalent to an explicit @bot for gating purposes.
|
|
81
|
-
*/
|
|
82
|
-
implicitMention: boolean;
|
|
83
|
-
|
|
84
|
-
// ---- Control-command layer ----
|
|
85
|
-
/** Whether text-based control commands are enabled globally. */
|
|
86
|
-
allowTextCommands: boolean;
|
|
87
|
-
/** Whether the current message is recognised as a control command. */
|
|
88
|
-
isControlCommand: boolean;
|
|
89
|
-
/** Whether the sender is authorised to run control commands. */
|
|
90
|
-
commandAuthorized: boolean;
|
|
91
|
-
|
|
92
|
-
// ---- Mention gating layer ----
|
|
93
|
-
/** Per-group config: `requireMention` — bot only replies when @-ed. */
|
|
94
|
-
requireMention: boolean;
|
|
95
|
-
/**
|
|
96
|
-
* Whether the channel can reliably detect @-mentions at all. In C2C chat
|
|
97
|
-
* this should be `false` (DMs don't have mentions); in group chat it
|
|
98
|
-
* should be `true`.
|
|
99
|
-
*/
|
|
100
|
-
canDetectMention: boolean;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ────────────────────── Core logic ──────────────────────
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Base mention-gate evaluation.
|
|
107
|
-
*
|
|
108
|
-
* `effectiveWasMentioned = wasMentioned || implicitMention || bypass`.
|
|
109
|
-
* `shouldSkip = requireMention && canDetectMention && !effectiveWasMentioned`.
|
|
110
|
-
*/
|
|
111
|
-
function resolveMentionGating(input: {
|
|
112
|
-
requireMention: boolean;
|
|
113
|
-
canDetectMention: boolean;
|
|
114
|
-
wasMentioned: boolean;
|
|
115
|
-
implicitMention: boolean;
|
|
116
|
-
shouldBypassMention: boolean;
|
|
117
|
-
}): { effectiveWasMentioned: boolean; shouldSkip: boolean } {
|
|
118
|
-
const effectiveWasMentioned =
|
|
119
|
-
input.wasMentioned || input.implicitMention || input.shouldBypassMention;
|
|
120
|
-
const shouldSkip = input.requireMention && input.canDetectMention && !effectiveWasMentioned;
|
|
121
|
-
return { effectiveWasMentioned, shouldSkip };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Decide whether an authorized control command may bypass the mention gate.
|
|
126
|
-
*
|
|
127
|
-
* All of the following must hold:
|
|
128
|
-
* 1. `requireMention` is on (gate is active)
|
|
129
|
-
* 2. The bot was NOT directly @-ed (otherwise no bypass is needed)
|
|
130
|
-
* 3. The message does NOT @anyone (a `@other-user /stop` should NOT pass
|
|
131
|
-
* — the command wasn't aimed at us)
|
|
132
|
-
* 4. Text commands are enabled
|
|
133
|
-
* 5. Sender is authorised
|
|
134
|
-
* 6. The content is a valid control command
|
|
135
|
-
*/
|
|
136
|
-
function resolveCommandBypass(input: {
|
|
137
|
-
requireMention: boolean;
|
|
138
|
-
wasMentioned: boolean;
|
|
139
|
-
hasAnyMention: boolean;
|
|
140
|
-
allowTextCommands: boolean;
|
|
141
|
-
commandAuthorized: boolean;
|
|
142
|
-
isControlCommand: boolean;
|
|
143
|
-
}): boolean {
|
|
144
|
-
return (
|
|
145
|
-
input.requireMention &&
|
|
146
|
-
!input.wasMentioned &&
|
|
147
|
-
!input.hasAnyMention &&
|
|
148
|
-
input.allowTextCommands &&
|
|
149
|
-
input.commandAuthorized &&
|
|
150
|
-
input.isControlCommand
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// ────────────────────── Unified gate ──────────────────────
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Evaluate the group-message gate.
|
|
158
|
-
*
|
|
159
|
-
* See the module-level docs for the ordering and semantics.
|
|
160
|
-
*/
|
|
161
|
-
export function resolveGroupMessageGate(input: GroupMessageGateInput): GroupMessageGateResult {
|
|
162
|
-
// ---- Layer 1: ignoreOtherMentions ----
|
|
163
|
-
if (
|
|
164
|
-
input.ignoreOtherMentions &&
|
|
165
|
-
input.hasAnyMention &&
|
|
166
|
-
!input.wasMentioned &&
|
|
167
|
-
!input.implicitMention
|
|
168
|
-
) {
|
|
169
|
-
return {
|
|
170
|
-
action: "drop_other_mention",
|
|
171
|
-
effectiveWasMentioned: false,
|
|
172
|
-
shouldBypassMention: false,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ---- Layer 2: unauthorized control command ----
|
|
177
|
-
if (input.allowTextCommands && input.isControlCommand && !input.commandAuthorized) {
|
|
178
|
-
return {
|
|
179
|
-
action: "block_unauthorized_command",
|
|
180
|
-
effectiveWasMentioned: false,
|
|
181
|
-
shouldBypassMention: false,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// ---- Layer 3: mention gate + command bypass ----
|
|
186
|
-
const shouldBypassMention = resolveCommandBypass({
|
|
187
|
-
requireMention: input.requireMention,
|
|
188
|
-
wasMentioned: input.wasMentioned,
|
|
189
|
-
hasAnyMention: input.hasAnyMention,
|
|
190
|
-
allowTextCommands: input.allowTextCommands,
|
|
191
|
-
commandAuthorized: input.commandAuthorized,
|
|
192
|
-
isControlCommand: input.isControlCommand,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const mentionGate = resolveMentionGating({
|
|
196
|
-
requireMention: input.requireMention,
|
|
197
|
-
canDetectMention: input.canDetectMention,
|
|
198
|
-
wasMentioned: input.wasMentioned,
|
|
199
|
-
implicitMention: input.implicitMention,
|
|
200
|
-
shouldBypassMention,
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
if (mentionGate.shouldSkip) {
|
|
204
|
-
return {
|
|
205
|
-
action: "skip_no_mention",
|
|
206
|
-
effectiveWasMentioned: mentionGate.effectiveWasMentioned,
|
|
207
|
-
shouldBypassMention,
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
action: "pass",
|
|
213
|
-
effectiveWasMentioned: mentionGate.effectiveWasMentioned,
|
|
214
|
-
shouldBypassMention,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Media path decoding utility.
|
|
3
|
-
*
|
|
4
|
-
* Extracted from `outbound-deliver.ts` — handles the `MEDIA:` prefix stripping,
|
|
5
|
-
* tilde expansion, octal escape / UTF-8 byte-sequence decoding, and backslash
|
|
6
|
-
* unescaping that media tags require.
|
|
7
|
-
*
|
|
8
|
-
* Zero external dependencies.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { EngineLogger } from "../types.js";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Normalize a file path by expanding `~` to the home directory and trimming.
|
|
15
|
-
*
|
|
16
|
-
* This is a minimal re-implementation of `utils/platform.ts#normalizePath`
|
|
17
|
-
* so that `core/` remains self-contained.
|
|
18
|
-
*/
|
|
19
|
-
function normalizePath(p: string): string {
|
|
20
|
-
let result = p.trim();
|
|
21
|
-
if (result.startsWith("~/") || result === "~") {
|
|
22
|
-
const home =
|
|
23
|
-
typeof process !== "undefined" ? (process.env.HOME ?? process.env.USERPROFILE) : undefined;
|
|
24
|
-
if (home) {
|
|
25
|
-
result = result === "~" ? home : `${home}${result.slice(1)}`;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return result;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Decode a media path by stripping `MEDIA:`, expanding `~`, and unescaping
|
|
33
|
-
* octal/UTF-8 byte sequences.
|
|
34
|
-
*
|
|
35
|
-
* @param raw - Raw path string from a media tag.
|
|
36
|
-
* @param log - Optional logger for decode diagnostics.
|
|
37
|
-
* @returns The decoded, normalized media path.
|
|
38
|
-
*/
|
|
39
|
-
export function decodeMediaPath(raw: string, log?: EngineLogger): string {
|
|
40
|
-
let mediaPath = raw;
|
|
41
|
-
if (mediaPath.startsWith("MEDIA:")) {
|
|
42
|
-
mediaPath = mediaPath.slice("MEDIA:".length);
|
|
43
|
-
}
|
|
44
|
-
mediaPath = normalizePath(mediaPath);
|
|
45
|
-
mediaPath = mediaPath.replace(/\\\\/g, "\\");
|
|
46
|
-
|
|
47
|
-
// Skip octal escape decoding for Windows local paths (e.g. C:\Users\1\file.txt)
|
|
48
|
-
// where backslash-digit sequences like \1, \2 ... \7 are directory separators,
|
|
49
|
-
// not octal escape sequences.
|
|
50
|
-
const isWinLocal = /^[a-zA-Z]:[\\/]/.test(mediaPath) || mediaPath.startsWith("\\\\");
|
|
51
|
-
try {
|
|
52
|
-
const hasOctal = /\\[0-7]{1,3}/.test(mediaPath);
|
|
53
|
-
const hasNonASCII = /[\u0080-\u00FF]/.test(mediaPath);
|
|
54
|
-
|
|
55
|
-
if (!isWinLocal && (hasOctal || hasNonASCII)) {
|
|
56
|
-
log?.debug?.(`Decoding path with mixed encoding: ${mediaPath}`);
|
|
57
|
-
const decoded = mediaPath.replace(/\\([0-7]{1,3})/g, (_: string, octal: string) => {
|
|
58
|
-
return String.fromCharCode(Number.parseInt(octal, 8));
|
|
59
|
-
});
|
|
60
|
-
const bytes: number[] = [];
|
|
61
|
-
for (let i = 0; i < decoded.length; i++) {
|
|
62
|
-
const code = decoded.charCodeAt(i);
|
|
63
|
-
if (code <= 0xff) {
|
|
64
|
-
bytes.push(code);
|
|
65
|
-
} else {
|
|
66
|
-
const charBytes = Buffer.from(decoded[i], "utf8");
|
|
67
|
-
bytes.push(...charBytes);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const buffer = Buffer.from(bytes);
|
|
71
|
-
const utf8Decoded = buffer.toString("utf8");
|
|
72
|
-
if (!utf8Decoded.includes("\uFFFD") || utf8Decoded.length < decoded.length) {
|
|
73
|
-
mediaPath = utf8Decoded;
|
|
74
|
-
log?.debug?.(`Successfully decoded path: ${mediaPath}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch (decodeErr) {
|
|
78
|
-
log?.error(`Path decode error: ${String(decodeErr)}`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return mediaPath;
|
|
82
|
-
}
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified media-source abstraction for the QQ Bot upload pipeline.
|
|
3
|
-
*
|
|
4
|
-
* All rich-media entry points (sender.ts#sendMedia, outbound.ts#send*,
|
|
5
|
-
* reply-dispatcher.ts#handle*Payload) funnel through {@link normalizeSource}
|
|
6
|
-
* before reaching the low-level {@link MediaApi}.
|
|
7
|
-
*
|
|
8
|
-
* ## Why four branches?
|
|
9
|
-
*
|
|
10
|
-
* - `url` — remote http(s) URL that the QQ server can fetch directly.
|
|
11
|
-
* - `base64` — in-memory base64 string (typically from a `data:` URL).
|
|
12
|
-
* - `localPath` — on-disk file; kept as a path so a future chunked-upload
|
|
13
|
-
* implementation can stream it via `fs.createReadStream` without the 4/3×
|
|
14
|
-
* base64 memory overhead.
|
|
15
|
-
* - `buffer` — in-memory raw bytes (e.g. TTS output, downloaded url-fallback).
|
|
16
|
-
*
|
|
17
|
-
* ## Security baseline (localPath branch)
|
|
18
|
-
*
|
|
19
|
-
* `openLocalFile` is the single canonical implementation of "safely open a
|
|
20
|
-
* local file for upload" across the plugin. It merges the previously
|
|
21
|
-
* inconsistent strategies from `reply-dispatcher.ts` (O_NOFOLLOW + size check)
|
|
22
|
-
* and `outbound.ts` (realpath + root containment). Callers are still
|
|
23
|
-
* responsible for *root-whitelist* validation (via
|
|
24
|
-
* `resolveQQBotPayloadLocalFilePath` / `resolveOutboundMediaPath`) before
|
|
25
|
-
* passing the path in; this function enforces *file-level* safety only.
|
|
26
|
-
*
|
|
27
|
-
* Chunked upload is not implemented in this PR, but the contract here already
|
|
28
|
-
* returns `size` metadata so `sendMediaInternal` can route by size without
|
|
29
|
-
* reading the whole file first.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import * as fs from "node:fs";
|
|
33
|
-
import { MAX_UPLOAD_SIZE, formatFileSize, getMimeType } from "../utils/file-utils.js";
|
|
34
|
-
|
|
35
|
-
// ============ Types ============
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Fully normalized media source. Downstream uploaders switch on `kind`.
|
|
39
|
-
*
|
|
40
|
-
* - `url`: remote URL — upload via `file_data=null; url=...`.
|
|
41
|
-
* - `base64`: already-encoded base64 — upload via `file_data=...`.
|
|
42
|
-
* - `localPath`: on-disk file — one-shot path reads it into a buffer;
|
|
43
|
-
* chunked path (future) streams it via `fs.createReadStream`.
|
|
44
|
-
* - `buffer`: raw bytes in memory — same as above minus disk I/O.
|
|
45
|
-
*/
|
|
46
|
-
export type MediaSource =
|
|
47
|
-
| { kind: "url"; url: string }
|
|
48
|
-
| { kind: "base64"; data: string; mime?: string }
|
|
49
|
-
| { kind: "localPath"; path: string; size: number; mime?: string }
|
|
50
|
-
| { kind: "buffer"; buffer: Buffer; fileName?: string; mime?: string };
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Untyped media source accepted from callers.
|
|
54
|
-
*
|
|
55
|
-
* `url` may be either a remote `http(s)://...` URL or a `data:<mime>;base64,...`
|
|
56
|
-
* data URL — {@link normalizeSource} transparently resolves the latter to a
|
|
57
|
-
* `base64` branch.
|
|
58
|
-
*/
|
|
59
|
-
export type RawMediaSource =
|
|
60
|
-
| { url: string }
|
|
61
|
-
| { base64: string; mime?: string }
|
|
62
|
-
| { localPath: string }
|
|
63
|
-
| { buffer: Buffer; fileName?: string; mime?: string };
|
|
64
|
-
|
|
65
|
-
// ============ data: URL ============
|
|
66
|
-
|
|
67
|
-
const DATA_URL_RE = /^data:([^;,]+);base64,(.+)$/i;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Parse a `data:<mime>;base64,<payload>` URL.
|
|
71
|
-
*
|
|
72
|
-
* Returns `null` when the string is not a data URL or does not declare
|
|
73
|
-
* base64 encoding. Non-base64 data URLs are intentionally rejected because
|
|
74
|
-
* the QQ upload API ingests raw base64, not arbitrary URL-encoded payloads.
|
|
75
|
-
*/
|
|
76
|
-
function tryParseDataUrl(value: string): { mime: string; data: string } | null {
|
|
77
|
-
if (!value.startsWith("data:")) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
const m = value.match(DATA_URL_RE);
|
|
81
|
-
if (!m) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
return { mime: m[1], data: m[2] };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ============ Local file safe open ============
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Opened handle to a local file, with metadata already validated against
|
|
91
|
-
* QQ upload limits.
|
|
92
|
-
*
|
|
93
|
-
* Callers MUST call {@link OpenedLocalFile.close} (typically in a `finally`).
|
|
94
|
-
*/
|
|
95
|
-
interface OpenedLocalFile {
|
|
96
|
-
handle: fs.promises.FileHandle;
|
|
97
|
-
size: number;
|
|
98
|
-
close(): Promise<void>;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Open a local file for upload with defense-in-depth:
|
|
103
|
-
*
|
|
104
|
-
* 1. `O_NOFOLLOW` refuses to traverse symlinks (prevents post-whitelist
|
|
105
|
-
* symlink swaps / TOCTOU attacks).
|
|
106
|
-
* 2. `fstat` on the opened descriptor — NOT `fs.stat` on the path —
|
|
107
|
-
* so the size check applies to the exact byte stream we will read.
|
|
108
|
-
* 3. Rejects non-regular files (sockets / devices / directories).
|
|
109
|
-
* 4. Enforces a caller-specified `maxSize` (default {@link MAX_UPLOAD_SIZE})
|
|
110
|
-
* at open time, so oversized files fail fast without allocating a
|
|
111
|
-
* full buffer. Chunked upload callers should pass a larger ceiling
|
|
112
|
-
* (e.g. `CHUNKED_UPLOAD_MAX_SIZE` from `utils/file-utils.js`).
|
|
113
|
-
*
|
|
114
|
-
* The caller receives the open handle plus validated size and is expected
|
|
115
|
-
* to either {@link OpenedLocalFile.handle.readFile} (one-shot path) or
|
|
116
|
-
* stream via `fs.createReadStream` (chunked path).
|
|
117
|
-
*/
|
|
118
|
-
export async function openLocalFile(
|
|
119
|
-
filePath: string,
|
|
120
|
-
opts: { maxSize?: number } = {},
|
|
121
|
-
): Promise<OpenedLocalFile> {
|
|
122
|
-
const maxSize = opts.maxSize ?? MAX_UPLOAD_SIZE;
|
|
123
|
-
const openFlags =
|
|
124
|
-
fs.constants.O_RDONLY | ("O_NOFOLLOW" in fs.constants ? fs.constants.O_NOFOLLOW : 0);
|
|
125
|
-
const handle = await fs.promises.open(filePath, openFlags);
|
|
126
|
-
try {
|
|
127
|
-
const stat = await handle.stat();
|
|
128
|
-
if (!stat.isFile()) {
|
|
129
|
-
throw new Error("Path is not a regular file");
|
|
130
|
-
}
|
|
131
|
-
if (stat.size > maxSize) {
|
|
132
|
-
throw new Error(
|
|
133
|
-
`File is too large (${formatFileSize(stat.size)}); QQ Bot API limit is ${formatFileSize(maxSize)}`,
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
return {
|
|
137
|
-
handle,
|
|
138
|
-
size: stat.size,
|
|
139
|
-
close: () => handle.close(),
|
|
140
|
-
};
|
|
141
|
-
} catch (err) {
|
|
142
|
-
// Close the handle on any validation failure to avoid fd leaks.
|
|
143
|
-
await handle.close().catch(() => undefined);
|
|
144
|
-
throw err;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// ============ Normalization ============
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Normalize a {@link RawMediaSource} into a {@link MediaSource}.
|
|
152
|
-
*
|
|
153
|
-
* - Strings passed via `{ url }` that start with `data:` are auto-resolved
|
|
154
|
-
* to a `base64` branch (this is the unified `data:` URL support that was
|
|
155
|
-
* previously only implemented in `sendImage`).
|
|
156
|
-
* - `localPath` branches open the file with {@link openLocalFile} solely to
|
|
157
|
-
* validate size / regular-file / O_NOFOLLOW invariants. The handle is
|
|
158
|
-
* closed immediately — actual reading is deferred to the uploader so
|
|
159
|
-
* the chunked path can stream without double-reading.
|
|
160
|
-
* - `buffer` branches enforce the same ceiling inline.
|
|
161
|
-
*
|
|
162
|
-
* `maxSize` defaults to {@link MAX_UPLOAD_SIZE} (20MB, one-shot upload limit).
|
|
163
|
-
* Callers that dispatch to the chunked uploader should pass a larger ceiling
|
|
164
|
-
* (e.g. `CHUNKED_UPLOAD_MAX_SIZE`, or a value derived from
|
|
165
|
-
* `getMaxUploadSize(fileType)`).
|
|
166
|
-
*
|
|
167
|
-
* NOTE: Root-whitelist validation (i.e. "this path must live under the
|
|
168
|
-
* allowed QQ Bot media directory") is a caller concern. This function
|
|
169
|
-
* assumes the path has already passed such checks.
|
|
170
|
-
*/
|
|
171
|
-
export async function normalizeSource(
|
|
172
|
-
raw: RawMediaSource,
|
|
173
|
-
opts: { maxSize?: number } = {},
|
|
174
|
-
): Promise<MediaSource> {
|
|
175
|
-
const maxSize = opts.maxSize ?? MAX_UPLOAD_SIZE;
|
|
176
|
-
|
|
177
|
-
if ("url" in raw) {
|
|
178
|
-
const parsed = tryParseDataUrl(raw.url);
|
|
179
|
-
if (parsed) {
|
|
180
|
-
return { kind: "base64", data: parsed.data, mime: parsed.mime };
|
|
181
|
-
}
|
|
182
|
-
return { kind: "url", url: raw.url };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if ("base64" in raw) {
|
|
186
|
-
return { kind: "base64", data: raw.base64, mime: raw.mime };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if ("localPath" in raw) {
|
|
190
|
-
const opened = await openLocalFile(raw.localPath, { maxSize });
|
|
191
|
-
try {
|
|
192
|
-
return {
|
|
193
|
-
kind: "localPath",
|
|
194
|
-
path: raw.localPath,
|
|
195
|
-
size: opened.size,
|
|
196
|
-
mime: getMimeType(raw.localPath),
|
|
197
|
-
};
|
|
198
|
-
} finally {
|
|
199
|
-
await opened.close();
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// buffer branch
|
|
204
|
-
if (raw.buffer.length > maxSize) {
|
|
205
|
-
throw new Error(
|
|
206
|
-
`Buffer is too large (${formatFileSize(raw.buffer.length)}); QQ Bot API limit is ${formatFileSize(maxSize)}`,
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
return {
|
|
210
|
-
kind: "buffer",
|
|
211
|
-
buffer: raw.buffer,
|
|
212
|
-
fileName: raw.fileName,
|
|
213
|
-
mime: raw.mime,
|
|
214
|
-
};
|
|
215
|
-
}
|