@openclaw/qqbot 2026.6.11 → 2026.7.1-beta.2
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 +6 -6
- package/dist/{channel-DvWrqcZB.js → channel-BQM6pDlD.js} +71 -126
- package/dist/{channel-entry-BGXFRmbg.js → channel-entry-C18ypTaG.js} +3 -3
- package/dist/channel-entry-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel.setup-CldhUlTO.js → channel.setup-2ItDYKhz.js} +1 -1
- package/dist/{config-schema-gMW9zKvQ.js → config-schema-JZEf1dvB.js} +2 -5
- package/dist/doctor-contract-api.js +168 -2
- package/dist/{gateway-CCkrdgaK.js → gateway-CjdB3B6n.js} +284 -264
- package/dist/{group-CvVXke_k.js → group-BmS1Qwst.js} +2 -2
- package/dist/{handler-runtime--OTMkFWa.js → handler-runtime-rU03VA43.js} +3 -3
- package/dist/{outbound-BQcPCo-z.js → outbound-BZHwMFUX.js} +496 -44
- package/dist/{runtime-D1hn91-s.js → runtime-B4HWCOa-.js} +1 -1
- package/dist/runtime-api.js +1 -1
- package/dist/{sender-CRZ2af7P.js → sender-BfVLJJYp.js} +16 -14
- package/dist/setup-plugin-api.js +1 -1
- package/dist/{doctor-contract-BYTS8-ia.js → state-keys-CQKlAFyo.js} +7 -1
- package/dist/target-parser-DagYRQkP.js +71 -0
- package/dist/{tools-CeUI9pG-.js → tools-C8kTMXKc.js} +58 -13
- package/dist/tools-api.js +1 -1
- package/npm-shrinkwrap.json +3 -3
- package/package.json +4 -4
- package/skills/qqbot-channel/SKILL.md +54 -41
- package/skills/qqbot-channel/references/api_references.md +16 -8
- package/skills/qqbot-media/SKILL.md +19 -13
- package/skills/qqbot-remind/SKILL.md +15 -14
- package/dist/target-parser-BdCUmxK7.js +0 -285
|
@@ -1,11 +1,226 @@
|
|
|
1
|
-
import { A as getFileTypeName, D as downloadFile, E as checkFileSize, L as __exportAll, M as getMaxUploadSize, N as readFileAsync, O as fileExistsAsync, S as UploadDailyLimitExceededError, g as sendText$1, h as sendMedia$1, j as getImageMimeType, k as formatFileSize, t as accountToCreds, u as initApiConfig, w as UPLOAD_PREPARE_FALLBACK_CODE } from "./sender-
|
|
2
|
-
import { i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, s as sanitizeFileName } from "./string-normalize-R_0cKO7Q.js";
|
|
1
|
+
import { A as getFileTypeName, D as downloadFile, E as checkFileSize, L as __exportAll, M as getMaxUploadSize, N as readFileAsync, O as fileExistsAsync, S as UploadDailyLimitExceededError, g as sendText$1, h as sendMedia$1, j as getImageMimeType, k as formatFileSize, t as accountToCreds, u as initApiConfig, w as UPLOAD_PREPARE_FALLBACK_CODE } from "./sender-BfVLJJYp.js";
|
|
2
|
+
import { c as getPlatformAdapter, i as normalizeOptionalString, n as normalizeLowercaseStringOrEmpty, s as sanitizeFileName } from "./string-normalize-R_0cKO7Q.js";
|
|
3
3
|
import { a as formatErrorMessage, n as debugLog, r as debugWarn, t as debugError } from "./log-SDfMMBWe.js";
|
|
4
|
-
import {
|
|
4
|
+
import { r as parseTarget$1 } from "./target-parser-DagYRQkP.js";
|
|
5
|
+
import * as fs$2 from "node:fs";
|
|
6
|
+
import * as os$1 from "node:os";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
5
8
|
import { pathExistsSync, resolveLocalPathFromRootsSync } from "openclaw/plugin-sdk/security-runtime";
|
|
9
|
+
import * as path$1 from "node:path";
|
|
6
10
|
import path from "node:path";
|
|
7
|
-
import { getFileExtension } from "openclaw/plugin-sdk/media-mime";
|
|
11
|
+
import { extensionForMime, getFileExtension } from "openclaw/plugin-sdk/media-mime";
|
|
12
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
13
|
+
import { loadOutboundMediaFromUrl } from "openclaw/plugin-sdk/outbound-media";
|
|
8
14
|
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/sandbox";
|
|
15
|
+
//#region extensions/qqbot/src/engine/utils/platform.ts
|
|
16
|
+
/**
|
|
17
|
+
* Cross-platform path and detection helpers for core/ modules.
|
|
18
|
+
*
|
|
19
|
+
* Provides home/data/media directory helpers, platform detection,
|
|
20
|
+
* silk-wasm availability checks — all without importing `openclaw/plugin-sdk`.
|
|
21
|
+
* The temp-directory fallback is delegated to the PlatformAdapter.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the current user's OS home directory safely across platforms.
|
|
25
|
+
*
|
|
26
|
+
* Priority:
|
|
27
|
+
* 1. `os.homedir()`
|
|
28
|
+
* 2. `$HOME` or `%USERPROFILE%`
|
|
29
|
+
* 3. PlatformAdapter.getTempDir() as a last resort
|
|
30
|
+
*
|
|
31
|
+
* This is the *operating-system* home and intentionally ignores
|
|
32
|
+
* `OPENCLAW_HOME`. QQ Bot still checks this tree for legacy state imports and
|
|
33
|
+
* media-path remaps from older releases.
|
|
34
|
+
*/
|
|
35
|
+
function getHomeDir() {
|
|
36
|
+
try {
|
|
37
|
+
const home = os$1.homedir();
|
|
38
|
+
if (home && fs$2.existsSync(home)) return home;
|
|
39
|
+
} catch {}
|
|
40
|
+
const envHome = process.env.HOME || process.env.USERPROFILE;
|
|
41
|
+
if (envHome && fs$2.existsSync(envHome)) return envHome;
|
|
42
|
+
return getPlatformAdapter().getTempDir();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the effective OpenClaw home directory.
|
|
46
|
+
*
|
|
47
|
+
* Mirrors the contract from core (`src/infra/home-dir.ts::resolveEffectiveHomeDir`)
|
|
48
|
+
* so QQ Bot media roots live under the same tree the rest of OpenClaw treats as
|
|
49
|
+
* `~`. The extension cannot import the core helper directly (it is a separate
|
|
50
|
+
* package with `openclaw` as a peer dependency), so this re-implements the
|
|
51
|
+
* minimal contract:
|
|
52
|
+
*
|
|
53
|
+
* 1. `OPENCLAW_HOME` when set (with `~` / `~/...` expanded against the OS home).
|
|
54
|
+
* 2. Otherwise fall back to {@link getHomeDir} so existing single-home
|
|
55
|
+
* deployments are unaffected.
|
|
56
|
+
*
|
|
57
|
+
* Empty / `"undefined"` / `"null"` strings are treated as unset to match how
|
|
58
|
+
* core normalizes the variable.
|
|
59
|
+
*/
|
|
60
|
+
function resolveOpenClawHome() {
|
|
61
|
+
const raw = process.env.OPENCLAW_HOME?.trim();
|
|
62
|
+
if (!raw || raw === "undefined" || raw === "null") return getHomeDir();
|
|
63
|
+
if (raw === "~" || raw.startsWith("~/") || raw.startsWith("~\\")) {
|
|
64
|
+
const osHome = getHomeDir();
|
|
65
|
+
if (raw === "~") return osHome;
|
|
66
|
+
return path$1.join(osHome, raw.slice(2));
|
|
67
|
+
}
|
|
68
|
+
return raw;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Return a legacy path under `~/.openclaw/qqbot` without creating it.
|
|
72
|
+
*
|
|
73
|
+
* Current QQ Bot runtime state lives in plugin SQLite KV. This path remains for
|
|
74
|
+
* legacy imports and media-path remaps from older releases.
|
|
75
|
+
*/
|
|
76
|
+
function getQQBotDataPath(...subPaths) {
|
|
77
|
+
return path$1.join(getHomeDir(), ".openclaw", "qqbot", ...subPaths);
|
|
78
|
+
}
|
|
79
|
+
/** Return a path under `~/.openclaw/qqbot`, creating it on demand. */
|
|
80
|
+
function getQQBotDataDir(...subPaths) {
|
|
81
|
+
const dir = getQQBotDataPath(...subPaths);
|
|
82
|
+
if (!fs$2.existsSync(dir)) fs$2.mkdirSync(dir, { recursive: true });
|
|
83
|
+
return dir;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Return a path under `<openclaw-home>/.openclaw/media/qqbot` without creating it.
|
|
87
|
+
*
|
|
88
|
+
* Unlike `getQQBotDataPath`, this lives under OpenClaw's core media allowlist
|
|
89
|
+
* so downloaded images and audio can be accessed by framework media tooling.
|
|
90
|
+
* The base honors `OPENCLAW_HOME` (when set) so files written by agents into
|
|
91
|
+
* the OpenClaw-managed media tree are reachable by this plugin even when
|
|
92
|
+
* `HOME` and `OPENCLAW_HOME` differ (Docker, multi-user hosts). Fixes #83562.
|
|
93
|
+
*/
|
|
94
|
+
function getQQBotMediaPath(...subPaths) {
|
|
95
|
+
return path$1.join(resolveOpenClawHome(), ".openclaw", "media", "qqbot", ...subPaths);
|
|
96
|
+
}
|
|
97
|
+
/** Return a path under `<openclaw-home>/.openclaw/media/qqbot`, creating it on demand. */
|
|
98
|
+
function getQQBotMediaDir(...subPaths) {
|
|
99
|
+
const dir = getQQBotMediaPath(...subPaths);
|
|
100
|
+
if (!fs$2.existsSync(dir)) fs$2.mkdirSync(dir, { recursive: true });
|
|
101
|
+
return dir;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Return `<openclaw-home>/.openclaw/media`, OpenClaw's shared media root.
|
|
105
|
+
*
|
|
106
|
+
* This mirrors the directory that core's `buildMediaLocalRoots` exposes as an
|
|
107
|
+
* allowlisted location (see `openclaw/src/media/local-roots.ts`). Using it as a
|
|
108
|
+
* QQ Bot payload root lets the plugin trust framework-produced files that live
|
|
109
|
+
* in sibling subdirectories such as `outbound/` (written by
|
|
110
|
+
* `saveMediaBuffer(..., "outbound", ...)`) or `inbound/`, while still keeping
|
|
111
|
+
* the check anchored to a single, well-known directory. Like
|
|
112
|
+
* {@link getQQBotMediaPath}, the base honors `OPENCLAW_HOME`.
|
|
113
|
+
*/
|
|
114
|
+
function getOpenClawMediaDir() {
|
|
115
|
+
return path$1.join(resolveOpenClawHome(), ".openclaw", "media");
|
|
116
|
+
}
|
|
117
|
+
function isWindows() {
|
|
118
|
+
return process.platform === "win32";
|
|
119
|
+
}
|
|
120
|
+
/** Return the preferred temporary directory. */
|
|
121
|
+
function getTempDir() {
|
|
122
|
+
return getPlatformAdapter().getTempDir();
|
|
123
|
+
}
|
|
124
|
+
let silkWasmAvailable = null;
|
|
125
|
+
/** Check whether silk-wasm can run in the current environment. */
|
|
126
|
+
async function checkSilkWasmAvailable() {
|
|
127
|
+
if (silkWasmAvailable !== null) return silkWasmAvailable;
|
|
128
|
+
try {
|
|
129
|
+
const { isSilk } = await import("silk-wasm");
|
|
130
|
+
isSilk(new Uint8Array(0));
|
|
131
|
+
silkWasmAvailable = true;
|
|
132
|
+
debugLog("[platform] silk-wasm: available");
|
|
133
|
+
} catch (err) {
|
|
134
|
+
silkWasmAvailable = false;
|
|
135
|
+
debugWarn(`[platform] silk-wasm: NOT available (${formatErrorMessage(err)})`);
|
|
136
|
+
}
|
|
137
|
+
return silkWasmAvailable;
|
|
138
|
+
}
|
|
139
|
+
/** Expand `~` to the current user's home directory. */
|
|
140
|
+
function expandTilde$1(p) {
|
|
141
|
+
if (!p) return p;
|
|
142
|
+
if (p === "~") return getHomeDir();
|
|
143
|
+
if (p.startsWith("~/") || p.startsWith("~\\")) return path$1.join(getHomeDir(), p.slice(2));
|
|
144
|
+
return p;
|
|
145
|
+
}
|
|
146
|
+
/** Normalize a user-provided path by trimming, stripping `file://`, and expanding `~`. */
|
|
147
|
+
function normalizePath$1(p) {
|
|
148
|
+
let result = p.trim();
|
|
149
|
+
if (result.startsWith("file://")) {
|
|
150
|
+
result = result.slice(7);
|
|
151
|
+
try {
|
|
152
|
+
result = decodeURIComponent(result);
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
return expandTilde$1(result);
|
|
156
|
+
}
|
|
157
|
+
/** Return true when the string looks like a local filesystem path rather than a URL. */
|
|
158
|
+
function isLocalPath(p) {
|
|
159
|
+
if (!p) return false;
|
|
160
|
+
if (p.startsWith("file://")) return true;
|
|
161
|
+
if (p === "~" || p.startsWith("~/") || p.startsWith("~\\")) return true;
|
|
162
|
+
if (p.startsWith("/")) return true;
|
|
163
|
+
if (/^[a-zA-Z]:[\\/]/.test(p)) return true;
|
|
164
|
+
if (p.startsWith("\\\\")) return true;
|
|
165
|
+
if (p.startsWith("./") || p.startsWith("../")) return true;
|
|
166
|
+
if (p.startsWith(".\\") || p.startsWith("..\\")) return true;
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
function isPathWithinRoot$1(candidate, root) {
|
|
170
|
+
const relative = path$1.relative(root, candidate);
|
|
171
|
+
return relative === "" || !relative.startsWith("..") && !path$1.isAbsolute(relative);
|
|
172
|
+
}
|
|
173
|
+
/** Remap legacy or hallucinated QQ Bot local media paths to real files when possible. */
|
|
174
|
+
function resolveQQBotLocalMediaPath(p) {
|
|
175
|
+
const normalized = normalizePath$1(p);
|
|
176
|
+
if (!isLocalPath(normalized) || fs$2.existsSync(normalized)) return normalized;
|
|
177
|
+
const osHomeDir = getHomeDir();
|
|
178
|
+
const openclawHomeDir = resolveOpenClawHome();
|
|
179
|
+
const mediaRoot = getQQBotMediaPath();
|
|
180
|
+
const dataRoot = getQQBotDataPath();
|
|
181
|
+
const candidateRoots = [
|
|
182
|
+
...Array.from(new Set([path$1.join(osHomeDir, ".openclaw", "workspace", "qqbot"), path$1.join(openclawHomeDir, ".openclaw", "workspace", "qqbot")])).map((from) => ({
|
|
183
|
+
from,
|
|
184
|
+
to: mediaRoot
|
|
185
|
+
})),
|
|
186
|
+
{
|
|
187
|
+
from: dataRoot,
|
|
188
|
+
to: mediaRoot
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
from: mediaRoot,
|
|
192
|
+
to: dataRoot
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
for (const { from, to } of candidateRoots) {
|
|
196
|
+
if (!isPathWithinRoot$1(normalized, from)) continue;
|
|
197
|
+
const relative = path$1.relative(from, normalized);
|
|
198
|
+
const candidate = path$1.join(to, relative);
|
|
199
|
+
if (fs$2.existsSync(candidate)) {
|
|
200
|
+
debugWarn(`[platform] Remapped missing QQBot media path ${normalized} -> ${candidate}`);
|
|
201
|
+
return candidate;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return normalized;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Resolve a structured-payload local file path and enforce that it stays within
|
|
208
|
+
* QQ Bot-owned storage roots.
|
|
209
|
+
*/
|
|
210
|
+
function resolveQQBotPayloadLocalFilePath(p) {
|
|
211
|
+
const candidate = resolveQQBotLocalMediaPath(p);
|
|
212
|
+
if (!candidate.trim()) return null;
|
|
213
|
+
const resolvedCandidate = path$1.resolve(candidate);
|
|
214
|
+
if (!fs$2.existsSync(resolvedCandidate)) return null;
|
|
215
|
+
const canonicalCandidate = fs$2.realpathSync(resolvedCandidate);
|
|
216
|
+
const allowedRoots = [getOpenClawMediaDir(), getQQBotMediaPath()];
|
|
217
|
+
for (const root of allowedRoots) {
|
|
218
|
+
const resolvedRoot = path$1.resolve(root);
|
|
219
|
+
if (isPathWithinRoot$1(canonicalCandidate, fs$2.existsSync(resolvedRoot) ? fs$2.realpathSync(resolvedRoot) : resolvedRoot)) return canonicalCandidate;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
9
224
|
//#region extensions/qqbot/src/engine/messaging/outbound-audio-port.ts
|
|
10
225
|
let outboundAudioPort = null;
|
|
11
226
|
/**
|
|
@@ -194,6 +409,46 @@ function buildFileTooLargeResult(fileType, fileSize) {
|
|
|
194
409
|
};
|
|
195
410
|
}
|
|
196
411
|
//#endregion
|
|
412
|
+
//#region extensions/qqbot/src/engine/messaging/outbound-media-path.ts
|
|
413
|
+
function mergeMediaLocalRoots(...groups) {
|
|
414
|
+
const roots = groups.flatMap((group) => group ?? []).map((root) => root.trim()).filter(Boolean);
|
|
415
|
+
return roots.length > 0 ? Array.from(new Set(roots)) : void 0;
|
|
416
|
+
}
|
|
417
|
+
function resolveOutboundMediaLocalRoots(ctx) {
|
|
418
|
+
return mergeMediaLocalRoots(ctx.mediaAccess?.localRoots, ctx.mediaLocalRoots);
|
|
419
|
+
}
|
|
420
|
+
function isPathWithinRoot(candidatePath, rootPath) {
|
|
421
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
422
|
+
if (resolvedRoot === path.parse(resolvedRoot).root) return false;
|
|
423
|
+
const relative = path.relative(resolvedRoot, path.resolve(candidatePath));
|
|
424
|
+
return relative === "" || relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
425
|
+
}
|
|
426
|
+
function resolvePathInsideWorkspace(workspaceDir, pathWithinWorkspace) {
|
|
427
|
+
const mappedPath = path.resolve(workspaceDir, pathWithinWorkspace);
|
|
428
|
+
return isPathWithinRoot(mappedPath, workspaceDir) ? mappedPath : null;
|
|
429
|
+
}
|
|
430
|
+
function isVirtualWorkspacePath(normalizedPath) {
|
|
431
|
+
return normalizedPath === "/workspace" || normalizedPath.startsWith("/workspace/");
|
|
432
|
+
}
|
|
433
|
+
function resolveWorkspaceScopedLocalRoots(roots, workspaceDir) {
|
|
434
|
+
if (!roots?.length) return;
|
|
435
|
+
const scopedRoots = roots.map((root) => root.trim()).filter(Boolean).map((root) => workspaceDir && isVirtualWorkspacePath(root) ? resolveWorkspacePathCandidate(root, workspaceDir) : root).filter((root) => Boolean(root));
|
|
436
|
+
return scopedRoots.length > 0 ? Array.from(new Set(scopedRoots)) : void 0;
|
|
437
|
+
}
|
|
438
|
+
function resolveWorkspacePathCandidate(normalizedPath, workspaceDir) {
|
|
439
|
+
if (!workspaceDir) return isVirtualWorkspacePath(normalizedPath) ? null : normalizedPath;
|
|
440
|
+
if (normalizedPath === "/workspace") return workspaceDir;
|
|
441
|
+
if (normalizedPath.startsWith("/workspace/")) return resolvePathInsideWorkspace(workspaceDir, normalizedPath.slice(11));
|
|
442
|
+
if (path.isAbsolute(normalizedPath)) return normalizedPath;
|
|
443
|
+
return resolvePathInsideWorkspace(workspaceDir, normalizedPath);
|
|
444
|
+
}
|
|
445
|
+
function resolveWorkspacePathCandidates(normalizedPath, workspaceDir) {
|
|
446
|
+
const mappedPath = resolveWorkspacePathCandidate(normalizedPath, workspaceDir);
|
|
447
|
+
if (!mappedPath) return [];
|
|
448
|
+
if (mappedPath === normalizedPath) return [normalizedPath];
|
|
449
|
+
return path.isAbsolute(normalizedPath) && !isVirtualWorkspacePath(normalizedPath) ? [normalizedPath, mappedPath] : [mappedPath];
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
197
452
|
//#region extensions/qqbot/src/engine/messaging/trusted-media-path.ts
|
|
198
453
|
let cachedTrustedTmpRoot;
|
|
199
454
|
function trustedOpenClawTmpRoot() {
|
|
@@ -250,11 +505,15 @@ function parseTarget(to) {
|
|
|
250
505
|
/** Build a media target from a normal outbound context. */
|
|
251
506
|
function buildMediaTarget(ctx) {
|
|
252
507
|
const target = parseTarget(ctx.to);
|
|
508
|
+
const mediaLocalRoots = resolveOutboundMediaLocalRoots(ctx);
|
|
253
509
|
return {
|
|
254
510
|
targetType: target.type,
|
|
255
511
|
targetId: target.id,
|
|
256
512
|
account: ctx.account,
|
|
257
|
-
replyToId: ctx.replyToId ?? void 0
|
|
513
|
+
replyToId: ctx.replyToId ?? void 0,
|
|
514
|
+
...mediaLocalRoots ? { mediaLocalRoots } : {},
|
|
515
|
+
...ctx.mediaAccess ? { mediaAccess: ctx.mediaAccess } : {},
|
|
516
|
+
...ctx.mediaReadFile ? { mediaReadFile: ctx.mediaReadFile } : {}
|
|
258
517
|
};
|
|
259
518
|
}
|
|
260
519
|
/** Return true when public URLs should be passed through directly. */
|
|
@@ -268,19 +527,28 @@ const qqBotMediaKindLabel = {
|
|
|
268
527
|
file: "File",
|
|
269
528
|
media: "Media"
|
|
270
529
|
};
|
|
530
|
+
function isHttpUrl(pathValue) {
|
|
531
|
+
return pathValue.startsWith("http://") || pathValue.startsWith("https://");
|
|
532
|
+
}
|
|
533
|
+
function isDataUrl(pathValue) {
|
|
534
|
+
return pathValue.startsWith("data:");
|
|
535
|
+
}
|
|
271
536
|
function isHttpOrDataSource(pathValue) {
|
|
272
|
-
return pathValue
|
|
537
|
+
return isHttpUrl(pathValue) || isDataUrl(pathValue);
|
|
273
538
|
}
|
|
274
|
-
function
|
|
539
|
+
function resolveMissingPathWithinRoots(normalizedPath, allowedRoots) {
|
|
275
540
|
const resolvedCandidate = path.resolve(normalizedPath);
|
|
276
541
|
if (pathExistsSync(resolvedCandidate)) return null;
|
|
277
542
|
return resolveLocalPathFromRootsSync({
|
|
278
543
|
filePath: resolvedCandidate,
|
|
279
|
-
roots:
|
|
280
|
-
label: "QQ Bot
|
|
544
|
+
roots: allowedRoots,
|
|
545
|
+
label: "QQ Bot local roots",
|
|
281
546
|
allowMissing: true
|
|
282
547
|
})?.path ?? null;
|
|
283
548
|
}
|
|
549
|
+
function isPathWithinAnyRoot(candidatePath, allowedRoots) {
|
|
550
|
+
return allowedRoots?.some((root) => root.trim() && isPathWithinRoot(candidatePath, root)) ?? false;
|
|
551
|
+
}
|
|
284
552
|
function resolveExistingPathWithinRoots(normalizedPath, allowedRoots) {
|
|
285
553
|
return resolveLocalPathFromRootsSync({
|
|
286
554
|
filePath: normalizedPath,
|
|
@@ -288,30 +556,166 @@ function resolveExistingPathWithinRoots(normalizedPath, allowedRoots) {
|
|
|
288
556
|
label: "QQ Bot local roots"
|
|
289
557
|
})?.path ?? null;
|
|
290
558
|
}
|
|
559
|
+
function resolveOutboundMediaReadFile(ctx) {
|
|
560
|
+
return ctx.mediaAccess?.readFile ?? ctx.mediaReadFile;
|
|
561
|
+
}
|
|
562
|
+
function resolveHostReadMediaAccess(ctx) {
|
|
563
|
+
const mediaLocalRoots = resolveWorkspaceScopedLocalRoots(resolveOutboundMediaLocalRoots(ctx), ctx.mediaAccess?.workspaceDir);
|
|
564
|
+
if (!ctx.mediaAccess && !mediaLocalRoots) return;
|
|
565
|
+
const { localRoots: _localRoots, ...mediaAccessWithoutRoots } = ctx.mediaAccess ?? {};
|
|
566
|
+
return {
|
|
567
|
+
...mediaAccessWithoutRoots,
|
|
568
|
+
...mediaLocalRoots ? { localRoots: mediaLocalRoots } : {}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function mediaFileTypeForKind(mediaKind) {
|
|
572
|
+
switch (mediaKind) {
|
|
573
|
+
case "image": return 1;
|
|
574
|
+
case "voice": return 3;
|
|
575
|
+
case "video": return 2;
|
|
576
|
+
default: return 4;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function senderKindForLoadedMedia(mediaKind, loadedKind) {
|
|
580
|
+
if (mediaKind === "image") return loadedKind === "image" ? "image" : null;
|
|
581
|
+
if (mediaKind === "video") return loadedKind === "video" ? "video" : null;
|
|
582
|
+
if (mediaKind === "file") return "file";
|
|
583
|
+
if (loadedKind === "image") return "image";
|
|
584
|
+
if (loadedKind === "video") return "video";
|
|
585
|
+
return "file";
|
|
586
|
+
}
|
|
587
|
+
function resolveHostReadMediaPath(ctx, mediaPath) {
|
|
588
|
+
const normalizedPath = normalizePath$1(mediaPath);
|
|
589
|
+
if (path.isAbsolute(normalizedPath)) {
|
|
590
|
+
if (normalizedPath === "/workspace" || normalizedPath.startsWith("/workspace/")) return ctx.mediaAccess?.workspaceDir ? resolveWorkspacePathCandidate(normalizedPath, ctx.mediaAccess.workspaceDir) : null;
|
|
591
|
+
if (isPathWithinAnyRoot(normalizedPath, resolveOutboundMediaLocalRoots(ctx))) return normalizedPath;
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
if (!ctx.mediaAccess?.workspaceDir) return null;
|
|
595
|
+
return resolveWorkspacePathCandidate(normalizedPath, ctx.mediaAccess.workspaceDir);
|
|
596
|
+
}
|
|
597
|
+
async function stageLoadedHostReadVoice(mediaPath, loaded) {
|
|
598
|
+
const stagedDir = getQQBotMediaDir("host-read", "voice");
|
|
599
|
+
await mkdir(stagedDir, { recursive: true });
|
|
600
|
+
const rawFileName = sanitizeFileName(loaded.fileName || path.basename(mediaPath) || "voice");
|
|
601
|
+
const ext = path.extname(rawFileName);
|
|
602
|
+
const inferredExt = extensionForMime(loaded.contentType);
|
|
603
|
+
const baseName = sanitizeFileName(path.basename(rawFileName, ext)) || "voice";
|
|
604
|
+
const stagedPath = path.join(stagedDir, `${baseName}-${randomUUID()}${ext || inferredExt || ".bin"}`);
|
|
605
|
+
await writeFile(stagedPath, loaded.buffer);
|
|
606
|
+
return stagedPath;
|
|
607
|
+
}
|
|
608
|
+
async function stageHostReadVoice(ctx, mediaPath) {
|
|
609
|
+
const mediaReadFile = resolveOutboundMediaReadFile(ctx);
|
|
610
|
+
if (!mediaReadFile || isHttpOrDataSource(mediaPath)) return null;
|
|
611
|
+
const hostReadMediaPath = resolveHostReadMediaPath(ctx, mediaPath);
|
|
612
|
+
if (!hostReadMediaPath) return null;
|
|
613
|
+
const mediaAccess = resolveHostReadMediaAccess(ctx);
|
|
614
|
+
const loaded = await loadOutboundMediaFromUrl(hostReadMediaPath, {
|
|
615
|
+
maxBytes: getMaxUploadSize(3),
|
|
616
|
+
mediaAccess,
|
|
617
|
+
mediaReadFile,
|
|
618
|
+
workspaceDir: mediaAccess?.workspaceDir
|
|
619
|
+
});
|
|
620
|
+
if (loaded.kind !== "audio") throw new Error(`Unsupported voice media type: ${loaded.kind ?? "unknown"}`);
|
|
621
|
+
return await stageLoadedHostReadVoice(mediaPath, loaded);
|
|
622
|
+
}
|
|
623
|
+
async function trySendViaHostRead(ctx, mediaPath, mediaKind) {
|
|
624
|
+
const mediaReadFile = resolveOutboundMediaReadFile(ctx);
|
|
625
|
+
if (!mediaReadFile || isHttpOrDataSource(mediaPath)) return null;
|
|
626
|
+
const hostReadMediaPath = resolveHostReadMediaPath(ctx, mediaPath);
|
|
627
|
+
if (!hostReadMediaPath) return null;
|
|
628
|
+
const mediaAccess = resolveHostReadMediaAccess(ctx);
|
|
629
|
+
try {
|
|
630
|
+
const loaded = await loadOutboundMediaFromUrl(hostReadMediaPath, {
|
|
631
|
+
maxBytes: getMaxUploadSize(mediaFileTypeForKind(mediaKind)),
|
|
632
|
+
mediaAccess,
|
|
633
|
+
mediaReadFile,
|
|
634
|
+
workspaceDir: mediaAccess?.workspaceDir
|
|
635
|
+
});
|
|
636
|
+
const kind = senderKindForLoadedMedia(mediaKind, loaded.kind);
|
|
637
|
+
if (!kind) return {
|
|
638
|
+
channel: "qqbot",
|
|
639
|
+
error: `Unsupported ${mediaKind} media type: ${loaded.kind ?? "unknown"}`
|
|
640
|
+
};
|
|
641
|
+
if (loaded.buffer.length === 0) return {
|
|
642
|
+
channel: "qqbot",
|
|
643
|
+
error: `File is empty: ${hostReadMediaPath}`
|
|
644
|
+
};
|
|
645
|
+
if (mediaKind === "media" && loaded.kind === "audio") {
|
|
646
|
+
const directUploadFormats = ctx.account.config?.audioFormatPolicy?.uploadDirectFormats ?? ctx.account.config?.voiceDirectUploadFormats;
|
|
647
|
+
const transcodeEnabled = ctx.account.config?.audioFormatPolicy?.transcodeEnabled !== false;
|
|
648
|
+
return await sendVoiceFromLocal(ctx, await stageLoadedHostReadVoice(mediaPath, loaded), directUploadFormats, transcodeEnabled);
|
|
649
|
+
}
|
|
650
|
+
const creds = accountToCreds(ctx.account);
|
|
651
|
+
const target = {
|
|
652
|
+
type: ctx.targetType,
|
|
653
|
+
id: ctx.targetId
|
|
654
|
+
};
|
|
655
|
+
if (target.type !== "c2c" && target.type !== "group") return {
|
|
656
|
+
channel: "qqbot",
|
|
657
|
+
error: `${qqBotMediaKindLabel[mediaKind]} not supported in channel`
|
|
658
|
+
};
|
|
659
|
+
const r = await sendMedia$1({
|
|
660
|
+
target,
|
|
661
|
+
creds,
|
|
662
|
+
kind,
|
|
663
|
+
source: {
|
|
664
|
+
buffer: loaded.buffer,
|
|
665
|
+
...loaded.fileName ? { fileName: sanitizeFileName(loaded.fileName) } : {},
|
|
666
|
+
...loaded.contentType ? { mime: loaded.contentType } : {}
|
|
667
|
+
},
|
|
668
|
+
msgId: ctx.replyToId,
|
|
669
|
+
...kind === "file" && loaded.fileName ? { fileName: sanitizeFileName(loaded.fileName) } : {}
|
|
670
|
+
});
|
|
671
|
+
return {
|
|
672
|
+
channel: "qqbot",
|
|
673
|
+
messageId: r.id,
|
|
674
|
+
timestamp: r.timestamp
|
|
675
|
+
};
|
|
676
|
+
} catch (err) {
|
|
677
|
+
if (err instanceof UploadDailyLimitExceededError) return buildDailyLimitExceededResult(err.filePath === "<buffer>" ? new UploadDailyLimitExceededError(hostReadMediaPath, err.fileSize, err.message) : err);
|
|
678
|
+
return {
|
|
679
|
+
channel: "qqbot",
|
|
680
|
+
error: formatErrorMessage(err)
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async function sendAutoDetectedMedia(ctx, mediaPath) {
|
|
685
|
+
const hostReadResult = await trySendViaHostRead(ctx, mediaPath, "media");
|
|
686
|
+
if (hostReadResult) return hostReadResult;
|
|
687
|
+
return await sendDocument(ctx, mediaPath);
|
|
688
|
+
}
|
|
291
689
|
function resolveOutboundMediaPath(rawPath, mediaKind, options = {}) {
|
|
292
690
|
const normalizedPath = normalizePath$1(rawPath);
|
|
293
691
|
if (isHttpOrDataSource(normalizedPath)) return {
|
|
294
692
|
ok: true,
|
|
295
693
|
mediaPath: normalizedPath
|
|
296
694
|
};
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
};
|
|
302
|
-
if (options.extraLocalRoots && options.extraLocalRoots.length > 0) {
|
|
303
|
-
const extraAllowedPath = resolveExistingPathWithinRoots(normalizedPath, options.extraLocalRoots);
|
|
304
|
-
if (extraAllowedPath) return {
|
|
695
|
+
const candidatePaths = resolveWorkspacePathCandidates(normalizedPath, options.workspaceDir);
|
|
696
|
+
for (const candidatePath of candidatePaths) {
|
|
697
|
+
const allowedPath = resolveTrustedOutboundMediaPath(candidatePath, { allowMissing: options.allowMissingLocalPath });
|
|
698
|
+
if (allowedPath) return {
|
|
305
699
|
ok: true,
|
|
306
|
-
mediaPath:
|
|
700
|
+
mediaPath: allowedPath
|
|
307
701
|
};
|
|
702
|
+
if (options.extraLocalRoots && options.extraLocalRoots.length > 0) {
|
|
703
|
+
const extraAllowedPath = resolveExistingPathWithinRoots(candidatePath, options.extraLocalRoots);
|
|
704
|
+
if (extraAllowedPath) return {
|
|
705
|
+
ok: true,
|
|
706
|
+
mediaPath: extraAllowedPath
|
|
707
|
+
};
|
|
708
|
+
}
|
|
308
709
|
}
|
|
309
710
|
if (options.allowMissingLocalPath) {
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
711
|
+
const missingRoots = mergeMediaLocalRoots([getQQBotMediaDir()], options.extraLocalRoots);
|
|
712
|
+
if (missingRoots) for (const candidatePath of candidatePaths) {
|
|
713
|
+
const allowedMissingPath = resolveMissingPathWithinRoots(candidatePath, missingRoots);
|
|
714
|
+
if (allowedMissingPath) return {
|
|
715
|
+
ok: true,
|
|
716
|
+
mediaPath: allowedMissingPath
|
|
717
|
+
};
|
|
718
|
+
}
|
|
315
719
|
}
|
|
316
720
|
debugWarn(`blocked local ${mediaKind} path outside QQ Bot media storage`);
|
|
317
721
|
return {
|
|
@@ -323,15 +727,20 @@ function resolveOutboundMediaPath(rawPath, mediaKind, options = {}) {
|
|
|
323
727
|
* Send a photo from a local file, public URL, or Base64 data URL.
|
|
324
728
|
*/
|
|
325
729
|
async function sendPhoto(ctx, imagePath) {
|
|
326
|
-
const
|
|
730
|
+
const hostReadResult = await trySendViaHostRead(ctx, imagePath, "image");
|
|
731
|
+
if (hostReadResult) return hostReadResult;
|
|
732
|
+
const resolvedMediaPath = resolveOutboundMediaPath(imagePath, "image", {
|
|
733
|
+
extraLocalRoots: resolveOutboundMediaLocalRoots(ctx),
|
|
734
|
+
workspaceDir: ctx.mediaAccess?.workspaceDir
|
|
735
|
+
});
|
|
327
736
|
if (!resolvedMediaPath.ok) return {
|
|
328
737
|
channel: "qqbot",
|
|
329
738
|
error: resolvedMediaPath.error
|
|
330
739
|
};
|
|
331
740
|
const mediaPath = resolvedMediaPath.mediaPath;
|
|
332
741
|
const isLocal = isLocalPath(mediaPath);
|
|
333
|
-
const isHttp =
|
|
334
|
-
const isData = mediaPath
|
|
742
|
+
const isHttp = isHttpUrl(mediaPath);
|
|
743
|
+
const isData = isDataUrl(mediaPath);
|
|
335
744
|
if (isHttp && !shouldDirectUploadUrl(ctx.account)) {
|
|
336
745
|
debugLog(`sendPhoto: urlDirectUpload=false, downloading URL first...`);
|
|
337
746
|
const localFile = await downloadToFallbackDir(mediaPath, "sendPhoto");
|
|
@@ -451,13 +860,29 @@ async function sendPhotoFromLocal(ctx, mediaPath) {
|
|
|
451
860
|
* URL handling respects `urlDirectUpload`, and local files are transcoded when needed.
|
|
452
861
|
*/
|
|
453
862
|
async function sendVoice(ctx, voicePath, directUploadFormats, transcodeEnabled = true) {
|
|
454
|
-
|
|
863
|
+
let stagedHostReadVoice;
|
|
864
|
+
try {
|
|
865
|
+
stagedHostReadVoice = await stageHostReadVoice(ctx, voicePath);
|
|
866
|
+
} catch (err) {
|
|
867
|
+
return {
|
|
868
|
+
channel: "qqbot",
|
|
869
|
+
error: formatErrorMessage(err)
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
const resolvedMediaPath = stagedHostReadVoice ? {
|
|
873
|
+
ok: true,
|
|
874
|
+
mediaPath: stagedHostReadVoice
|
|
875
|
+
} : resolveOutboundMediaPath(voicePath, "voice", {
|
|
876
|
+
allowMissingLocalPath: true,
|
|
877
|
+
extraLocalRoots: resolveOutboundMediaLocalRoots(ctx),
|
|
878
|
+
workspaceDir: ctx.mediaAccess?.workspaceDir
|
|
879
|
+
});
|
|
455
880
|
if (!resolvedMediaPath.ok) return {
|
|
456
881
|
channel: "qqbot",
|
|
457
882
|
error: resolvedMediaPath.error
|
|
458
883
|
};
|
|
459
884
|
const mediaPath = resolvedMediaPath.mediaPath;
|
|
460
|
-
if (
|
|
885
|
+
if (isHttpUrl(mediaPath)) {
|
|
461
886
|
if (shouldDirectUploadUrl(ctx.account)) try {
|
|
462
887
|
const creds = accountToCreds(ctx.account);
|
|
463
888
|
const target = {
|
|
@@ -504,7 +929,8 @@ async function sendVoiceFromLocal(ctx, mediaPath, directUploadFormats, transcode
|
|
|
504
929
|
error: "Voice generate failed"
|
|
505
930
|
};
|
|
506
931
|
if (fileSize > getMaxUploadSize(3)) return buildFileTooLargeResult(3, fileSize);
|
|
507
|
-
const
|
|
932
|
+
const extraLocalRoots = resolveOutboundMediaLocalRoots(ctx);
|
|
933
|
+
const safeMediaPath = resolveTrustedOutboundMediaPath(mediaPath) ?? (extraLocalRoots ? resolveExistingPathWithinRoots(mediaPath, extraLocalRoots) : null);
|
|
508
934
|
if (!safeMediaPath) {
|
|
509
935
|
debugWarn(`sendVoice: blocked local voice path outside QQ Bot media storage`);
|
|
510
936
|
return {
|
|
@@ -567,13 +993,18 @@ async function sendVoiceFromLocal(ctx, mediaPath, directUploadFormats, transcode
|
|
|
567
993
|
}
|
|
568
994
|
/** Send video from either a public URL or a local file. */
|
|
569
995
|
async function sendVideoMsg(ctx, videoPath) {
|
|
570
|
-
const
|
|
996
|
+
const hostReadResult = await trySendViaHostRead(ctx, videoPath, "video");
|
|
997
|
+
if (hostReadResult) return hostReadResult;
|
|
998
|
+
const resolvedMediaPath = resolveOutboundMediaPath(videoPath, "video", {
|
|
999
|
+
extraLocalRoots: resolveOutboundMediaLocalRoots(ctx),
|
|
1000
|
+
workspaceDir: ctx.mediaAccess?.workspaceDir
|
|
1001
|
+
});
|
|
571
1002
|
if (!resolvedMediaPath.ok) return {
|
|
572
1003
|
channel: "qqbot",
|
|
573
1004
|
error: resolvedMediaPath.error
|
|
574
1005
|
};
|
|
575
1006
|
const mediaPath = resolvedMediaPath.mediaPath;
|
|
576
|
-
const isHttp =
|
|
1007
|
+
const isHttp = isHttpUrl(mediaPath);
|
|
577
1008
|
if (isHttp && !shouldDirectUploadUrl(ctx.account)) {
|
|
578
1009
|
debugLog(`sendVideoMsg: urlDirectUpload=false, downloading URL first...`);
|
|
579
1010
|
const localFile = await downloadToFallbackDir(mediaPath, "sendVideoMsg");
|
|
@@ -675,13 +1106,18 @@ async function sendVideoFromLocal(ctx, mediaPath) {
|
|
|
675
1106
|
}
|
|
676
1107
|
/** Send a file from a local path or public URL. */
|
|
677
1108
|
async function sendDocument(ctx, filePath, options = {}) {
|
|
678
|
-
const
|
|
1109
|
+
const hostReadResult = await trySendViaHostRead(ctx, filePath, "file");
|
|
1110
|
+
if (hostReadResult) return hostReadResult;
|
|
1111
|
+
const resolvedMediaPath = resolveOutboundMediaPath(filePath, "file", {
|
|
1112
|
+
extraLocalRoots: mergeMediaLocalRoots(options.allowQQBotDataDownloads ? [getQQBotDataDir("downloads")] : void 0, resolveOutboundMediaLocalRoots(ctx)),
|
|
1113
|
+
workspaceDir: ctx.mediaAccess?.workspaceDir
|
|
1114
|
+
});
|
|
679
1115
|
if (!resolvedMediaPath.ok) return {
|
|
680
1116
|
channel: "qqbot",
|
|
681
1117
|
error: resolvedMediaPath.error
|
|
682
1118
|
};
|
|
683
1119
|
const mediaPath = resolvedMediaPath.mediaPath;
|
|
684
|
-
const isHttp =
|
|
1120
|
+
const isHttp = isHttpUrl(mediaPath);
|
|
685
1121
|
const fileName = sanitizeFileName(path.basename(mediaPath));
|
|
686
1122
|
if (isHttp && !shouldDirectUploadUrl(ctx.account)) {
|
|
687
1123
|
debugLog(`sendDocument: urlDirectUpload=false, downloading URL first...`);
|
|
@@ -706,7 +1142,7 @@ async function sendDocument(ctx, filePath, options = {}) {
|
|
|
706
1142
|
kind: "file",
|
|
707
1143
|
source: { url: mediaPath },
|
|
708
1144
|
msgId: ctx.replyToId,
|
|
709
|
-
fileName
|
|
1145
|
+
...fileName ? { fileName } : {}
|
|
710
1146
|
});
|
|
711
1147
|
return {
|
|
712
1148
|
channel: "qqbot",
|
|
@@ -1223,7 +1659,10 @@ async function sendText(ctx) {
|
|
|
1223
1659
|
const mediaTarget = buildMediaTarget({
|
|
1224
1660
|
to,
|
|
1225
1661
|
account,
|
|
1226
|
-
replyToId
|
|
1662
|
+
replyToId,
|
|
1663
|
+
mediaAccess: ctx.mediaAccess,
|
|
1664
|
+
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
1665
|
+
mediaReadFile: ctx.mediaReadFile
|
|
1227
1666
|
});
|
|
1228
1667
|
let lastResult = { channel: "qqbot" };
|
|
1229
1668
|
for (const item of sendQueue) try {
|
|
@@ -1252,7 +1691,10 @@ async function sendText(ctx) {
|
|
|
1252
1691
|
mediaUrl: item.content,
|
|
1253
1692
|
accountId: account.accountId,
|
|
1254
1693
|
replyToId,
|
|
1255
|
-
account
|
|
1694
|
+
account,
|
|
1695
|
+
mediaAccess: ctx.mediaAccess,
|
|
1696
|
+
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
1697
|
+
mediaReadFile: ctx.mediaReadFile
|
|
1256
1698
|
});
|
|
1257
1699
|
} catch (err) {
|
|
1258
1700
|
const errMsg = formatErrorMessage(err);
|
|
@@ -1314,17 +1756,27 @@ async function sendMedia(ctx) {
|
|
|
1314
1756
|
channel: "qqbot",
|
|
1315
1757
|
error: "mediaUrl is required for sendMedia"
|
|
1316
1758
|
};
|
|
1317
|
-
const
|
|
1759
|
+
const target = buildMediaTarget({
|
|
1760
|
+
to,
|
|
1761
|
+
account,
|
|
1762
|
+
replyToId,
|
|
1763
|
+
mediaAccess: ctx.mediaAccess,
|
|
1764
|
+
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
1765
|
+
mediaReadFile: ctx.mediaReadFile
|
|
1766
|
+
});
|
|
1767
|
+
const resolvedMediaPath = !ctx.mediaAccess?.readFile && !ctx.mediaReadFile ? resolveOutboundMediaPath(ctx.mediaUrl, "media", {
|
|
1768
|
+
allowMissingLocalPath: true,
|
|
1769
|
+
extraLocalRoots: target.mediaLocalRoots ? [...target.mediaLocalRoots] : void 0,
|
|
1770
|
+
workspaceDir: target.mediaAccess?.workspaceDir
|
|
1771
|
+
}) : {
|
|
1772
|
+
ok: true,
|
|
1773
|
+
mediaPath: ctx.mediaUrl
|
|
1774
|
+
};
|
|
1318
1775
|
if (!resolvedMediaPath.ok) return {
|
|
1319
1776
|
channel: "qqbot",
|
|
1320
1777
|
error: resolvedMediaPath.error
|
|
1321
1778
|
};
|
|
1322
1779
|
const mediaUrl = resolvedMediaPath.mediaPath;
|
|
1323
|
-
const target = buildMediaTarget({
|
|
1324
|
-
to,
|
|
1325
|
-
account,
|
|
1326
|
-
replyToId
|
|
1327
|
-
});
|
|
1328
1780
|
if (isAudioFile(mediaUrl, mimeType)) {
|
|
1329
1781
|
const result = await sendVoice(target, mediaUrl, account.config?.audioFormatPolicy?.uploadDirectFormats ?? account.config?.voiceDirectUploadFormats, account.config?.audioFormatPolicy?.transcodeEnabled !== false);
|
|
1330
1782
|
if (!result.error) {
|
|
@@ -1349,7 +1801,7 @@ async function sendMedia(ctx) {
|
|
|
1349
1801
|
return result;
|
|
1350
1802
|
}
|
|
1351
1803
|
if (!isImageFile(mediaUrl, mimeType) && !isAudioFile(mediaUrl, mimeType) && !isVideoFile(mediaUrl, mimeType)) {
|
|
1352
|
-
const result = await
|
|
1804
|
+
const result = await sendAutoDetectedMedia(target, mediaUrl);
|
|
1353
1805
|
if (!result.error && text?.trim()) await sendTextAfterMedia(target, text);
|
|
1354
1806
|
return result;
|
|
1355
1807
|
}
|
|
@@ -1410,4 +1862,4 @@ async function sendCronMessage(account, to, message) {
|
|
|
1410
1862
|
});
|
|
1411
1863
|
}
|
|
1412
1864
|
//#endregion
|
|
1413
|
-
export {
|
|
1865
|
+
export { OUTBOUND_ERROR_CODES as A, normalizePath$1 as B, resolveUserFacingMediaError as C, getMessageReplyStats as D, getMessageReplyConfig as E, getQQBotMediaDir as F, getQQBotMediaPath as I, getTempDir as L, checkSilkWasmAvailable as M, getHomeDir as N, recordMessageReply as O, getQQBotDataDir as P, isLocalPath as R, resolveWorkspaceScopedLocalRoots as S, checkMessageReplyLimit as T, sendVideoMsg as _, sendText as a, resolveOutboundMediaLocalRoots as b, isCronReminderPayload as c, normalizeMediaTags as d, buildMediaTarget as f, sendPhoto as g, sendDocument as h, sendProactiveMessage as i, setOutboundAudioPort as j, DEFAULT_MEDIA_SEND_ERROR as k, isMediaPayload as l, resolveOutboundMediaPath as m, sendCronMessage as n, decodeMediaPath as o, parseTarget as p, sendMedia as r, encodePayloadForCron as s, outbound_exports as t, parseQQBotPayload as u, sendVoice as v, MESSAGE_REPLY_LIMIT as w, resolveWorkspacePathCandidates as x, resolveTrustedOutboundMediaPath as y, isWindows as z };
|