openzca 0.1.57 → 0.1.58
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/README.md +5 -0
- package/dist/cli.js +223 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,6 +125,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
|
|
|
125
125
|
|
|
126
126
|
Media commands accept local files, `file://` paths, and repeatable `--url` options. Add `--group` for group threads.
|
|
127
127
|
`openzca msg video` attempts native video send for a single `.mp4` input by uploading the video and thumbnail to Zalo first. If `ffmpeg` is unavailable, the input is not a single `.mp4`, or native send fails, it falls back to the normal attachment send path. Use `--thumbnail <path-or-url>` to supply the preview image explicitly.
|
|
128
|
+
`openzca msg voice` sends `--url` inputs directly. For local voice files, if both `ffmpeg` and `OPENZCA_VOICE_PUBLISH_CMD` are available, `openzca` normalizes the file to `.m4a`, runs the publish command with the normalized temp file path, expects one public `http(s)` URL on stdout, and sends that URL. Otherwise it falls back to the legacy Zalo upload flow.
|
|
128
129
|
Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
|
|
129
130
|
Group text sends via `openzca msg send --group` resolve unique `@Name` or `@userId` mentions against the current group member list using member ids, display names, and usernames. Mention offsets are computed after formatting markers are parsed, so messages like `**@Alice Nguyen** hello` work. If multiple members share the same label, the command fails instead of guessing.
|
|
130
131
|
When formatted text would produce an oversized outbound payload, `openzca msg send` automatically splits it into multiple sequential text messages using the final outbound text and rebased style/mention offsets. The split happens after formatting is parsed, using both rendered text length and estimated request payload size rather than the raw input string.
|
|
@@ -433,6 +434,10 @@ Upload/listener coordination overrides:
|
|
|
433
434
|
- `OPENZCA_UPLOAD_GROUP_PROBE`: allow `msg upload` to probe `getGroupInfo` when auto thread-type detection is enabled.
|
|
434
435
|
- Default: enabled.
|
|
435
436
|
- Set to `0` to skip probe and rely only on cache matches.
|
|
437
|
+
- `OPENZCA_VOICE_PUBLISH_CMD`: optional command used by `msg voice` for local files.
|
|
438
|
+
- `openzca` passes one normalized `.m4a` temp file path as the first argument.
|
|
439
|
+
- The command must print exactly one public `http(s)` URL to stdout.
|
|
440
|
+
- Requires `ffmpeg`; if unset or `ffmpeg` is unavailable, local voice files keep using the legacy Zalo upload flow.
|
|
436
441
|
|
|
437
442
|
### account — Multi-account profiles
|
|
438
443
|
|
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import { createRequire as createRequire2 } from "module";
|
|
5
5
|
import { spawn as spawn2 } from "child_process";
|
|
6
6
|
import fsSync from "fs";
|
|
7
|
-
import
|
|
7
|
+
import fs7 from "fs/promises";
|
|
8
8
|
import net from "net";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
9
|
+
import os5 from "os";
|
|
10
|
+
import path7 from "path";
|
|
11
11
|
import readline from "readline/promises";
|
|
12
12
|
import util from "util";
|
|
13
13
|
import { Command } from "commander";
|
|
@@ -3066,6 +3066,110 @@ async function sendNativeVideo(params) {
|
|
|
3066
3066
|
}
|
|
3067
3067
|
}
|
|
3068
3068
|
|
|
3069
|
+
// src/lib/voice-send.ts
|
|
3070
|
+
import { execFile as execFile2 } from "child_process";
|
|
3071
|
+
import fs6 from "fs/promises";
|
|
3072
|
+
import os4 from "os";
|
|
3073
|
+
import path6 from "path";
|
|
3074
|
+
import { promisify as promisify2 } from "util";
|
|
3075
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3076
|
+
function sanitizeOutputBasename(filePath) {
|
|
3077
|
+
const parsed = path6.parse(filePath);
|
|
3078
|
+
const base = parsed.name.trim() || "voice";
|
|
3079
|
+
const sanitized = base.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3080
|
+
return sanitized || "voice";
|
|
3081
|
+
}
|
|
3082
|
+
async function runBinary2(command, args) {
|
|
3083
|
+
try {
|
|
3084
|
+
await execFileAsync2(command, args, {
|
|
3085
|
+
encoding: "utf8",
|
|
3086
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3087
|
+
});
|
|
3088
|
+
} catch (error) {
|
|
3089
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
3090
|
+
throw new Error(`${command} is required for voice publish mode`);
|
|
3091
|
+
}
|
|
3092
|
+
if (error instanceof Error && "stderr" in error) {
|
|
3093
|
+
const stderr = typeof error.stderr === "string" ? error.stderr.trim() : "";
|
|
3094
|
+
throw new Error(stderr ? `${command} failed: ${stderr}` : `${command} failed: ${error.message}`);
|
|
3095
|
+
}
|
|
3096
|
+
throw error;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
function getVoicePublishCommandFromEnv(env = process.env) {
|
|
3100
|
+
const configured = env.OPENZCA_VOICE_PUBLISH_CMD?.trim();
|
|
3101
|
+
return configured && configured.length > 0 ? configured : null;
|
|
3102
|
+
}
|
|
3103
|
+
function extractPublishedVoiceUrl(stdout) {
|
|
3104
|
+
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
3105
|
+
const candidate = lines.at(-1);
|
|
3106
|
+
if (!candidate) {
|
|
3107
|
+
throw new Error("Voice publish command did not print a public URL to stdout");
|
|
3108
|
+
}
|
|
3109
|
+
if (!/^https?:\/\/\S+$/i.test(candidate)) {
|
|
3110
|
+
throw new Error(`Voice publish command returned an invalid URL: ${candidate}`);
|
|
3111
|
+
}
|
|
3112
|
+
return candidate;
|
|
3113
|
+
}
|
|
3114
|
+
async function normalizeVoiceForPublish(inputPath) {
|
|
3115
|
+
const dir = await fs6.mkdtemp(path6.join(os4.tmpdir(), "openzca-voice-"));
|
|
3116
|
+
const outputPath = path6.join(dir, `${sanitizeOutputBasename(inputPath)}.m4a`);
|
|
3117
|
+
try {
|
|
3118
|
+
await runBinary2("ffmpeg", [
|
|
3119
|
+
"-y",
|
|
3120
|
+
"-v",
|
|
3121
|
+
"error",
|
|
3122
|
+
"-i",
|
|
3123
|
+
inputPath,
|
|
3124
|
+
"-vn",
|
|
3125
|
+
"-map_metadata",
|
|
3126
|
+
"-1",
|
|
3127
|
+
"-ac",
|
|
3128
|
+
"1",
|
|
3129
|
+
"-ar",
|
|
3130
|
+
"44100",
|
|
3131
|
+
"-c:a",
|
|
3132
|
+
"aac",
|
|
3133
|
+
"-b:a",
|
|
3134
|
+
"64k",
|
|
3135
|
+
"-movflags",
|
|
3136
|
+
"+faststart",
|
|
3137
|
+
outputPath
|
|
3138
|
+
]);
|
|
3139
|
+
await fs6.access(outputPath);
|
|
3140
|
+
} catch (error) {
|
|
3141
|
+
await fs6.rm(dir, { recursive: true, force: true });
|
|
3142
|
+
throw error;
|
|
3143
|
+
}
|
|
3144
|
+
return {
|
|
3145
|
+
path: outputPath,
|
|
3146
|
+
cleanup: async () => {
|
|
3147
|
+
await fs6.rm(dir, { recursive: true, force: true });
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3150
|
+
}
|
|
3151
|
+
async function publishVoiceFile(command, filePath) {
|
|
3152
|
+
try {
|
|
3153
|
+
const { stdout } = await execFileAsync2(
|
|
3154
|
+
"sh",
|
|
3155
|
+
["-c", `${command} "$1"`, "openzca-voice-publish", filePath],
|
|
3156
|
+
{
|
|
3157
|
+
encoding: "utf8",
|
|
3158
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3159
|
+
}
|
|
3160
|
+
);
|
|
3161
|
+
return extractPublishedVoiceUrl(stdout);
|
|
3162
|
+
} catch (error) {
|
|
3163
|
+
if (error instanceof Error && "stderr" in error) {
|
|
3164
|
+
const stderr = typeof error.stderr === "string" ? error.stderr.trim() : "";
|
|
3165
|
+
throw new Error(
|
|
3166
|
+
stderr ? `Voice publish command failed: ${stderr}` : `Voice publish command failed: ${error.message}`
|
|
3167
|
+
);
|
|
3168
|
+
}
|
|
3169
|
+
throw error;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3069
3173
|
// src/lib/reply.ts
|
|
3070
3174
|
import { ThreadType as ThreadType2 } from "zca-js";
|
|
3071
3175
|
function prepareReplyMessage(value, params) {
|
|
@@ -3491,9 +3595,9 @@ function resolveDebugEnabled(command) {
|
|
|
3491
3595
|
}
|
|
3492
3596
|
function resolveDebugFilePath(command) {
|
|
3493
3597
|
const options = getDebugOptions(command);
|
|
3494
|
-
const configured = options.debugFile?.trim() || process.env.OPENZCA_DEBUG_FILE?.trim() ||
|
|
3598
|
+
const configured = options.debugFile?.trim() || process.env.OPENZCA_DEBUG_FILE?.trim() || path7.join(APP_HOME, "logs", "openzca-debug.log");
|
|
3495
3599
|
const normalized = normalizeMediaInput(configured);
|
|
3496
|
-
return
|
|
3600
|
+
return path7.isAbsolute(normalized) ? normalized : path7.resolve(process.cwd(), normalized);
|
|
3497
3601
|
}
|
|
3498
3602
|
function writeDebugLine(event, details, command) {
|
|
3499
3603
|
if (!resolveDebugEnabled(command)) {
|
|
@@ -3504,7 +3608,7 @@ function writeDebugLine(event, details, command) {
|
|
|
3504
3608
|
`;
|
|
3505
3609
|
const filePath = resolveDebugFilePath(command);
|
|
3506
3610
|
try {
|
|
3507
|
-
fsSync.mkdirSync(
|
|
3611
|
+
fsSync.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
3508
3612
|
fsSync.appendFileSync(filePath, line, "utf8");
|
|
3509
3613
|
} catch {
|
|
3510
3614
|
}
|
|
@@ -3611,14 +3715,14 @@ function collectIdsFromCacheEntries(entries, keys) {
|
|
|
3611
3715
|
return ids;
|
|
3612
3716
|
}
|
|
3613
3717
|
function getListenerOwnerLockPath(profile) {
|
|
3614
|
-
return
|
|
3718
|
+
return path7.join(getProfileDir(profile), "listener-owner.json");
|
|
3615
3719
|
}
|
|
3616
3720
|
function getListenIpcSocketPath(profile) {
|
|
3617
3721
|
if (process.platform === "win32") {
|
|
3618
3722
|
const safe = profile.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
3619
3723
|
return `\\\\.\\pipe\\openzca-listen-${safe}`;
|
|
3620
3724
|
}
|
|
3621
|
-
return
|
|
3725
|
+
return path7.join(getProfileDir(profile), "listen.sock");
|
|
3622
3726
|
}
|
|
3623
3727
|
function parsePositiveIntFromUnknown(value) {
|
|
3624
3728
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
@@ -3702,7 +3806,7 @@ function isProcessAlive(pid) {
|
|
|
3702
3806
|
}
|
|
3703
3807
|
async function readListenerOwnerRecord(lockPath) {
|
|
3704
3808
|
try {
|
|
3705
|
-
const raw = await
|
|
3809
|
+
const raw = await fs7.readFile(lockPath, "utf8");
|
|
3706
3810
|
const parsed = JSON.parse(raw);
|
|
3707
3811
|
const pid = parsePositiveIntFromUnknown(parsed.pid);
|
|
3708
3812
|
if (!pid) return null;
|
|
@@ -3722,11 +3826,11 @@ async function readActiveListenerOwner(profile) {
|
|
|
3722
3826
|
const lockPath = getListenerOwnerLockPath(profile);
|
|
3723
3827
|
const record = await readListenerOwnerRecord(lockPath);
|
|
3724
3828
|
if (!record) {
|
|
3725
|
-
await
|
|
3829
|
+
await fs7.rm(lockPath, { force: true });
|
|
3726
3830
|
return null;
|
|
3727
3831
|
}
|
|
3728
3832
|
if (!isProcessAlive(record.pid)) {
|
|
3729
|
-
await
|
|
3833
|
+
await fs7.rm(lockPath, { force: true });
|
|
3730
3834
|
return null;
|
|
3731
3835
|
}
|
|
3732
3836
|
return record;
|
|
@@ -3742,7 +3846,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
3742
3846
|
};
|
|
3743
3847
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
3744
3848
|
try {
|
|
3745
|
-
await
|
|
3849
|
+
await fs7.writeFile(lockPath, `${JSON.stringify(record, null, 2)}
|
|
3746
3850
|
`, {
|
|
3747
3851
|
encoding: "utf8",
|
|
3748
3852
|
flag: "wx"
|
|
@@ -3755,7 +3859,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
3755
3859
|
released = true;
|
|
3756
3860
|
const current = await readListenerOwnerRecord(lockPath);
|
|
3757
3861
|
if (current && current.pid !== process.pid) return;
|
|
3758
|
-
await
|
|
3862
|
+
await fs7.rm(lockPath, { force: true });
|
|
3759
3863
|
writeDebugLine(
|
|
3760
3864
|
"listen.owner.released",
|
|
3761
3865
|
{
|
|
@@ -3776,7 +3880,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
3776
3880
|
`Another openzca listener already owns profile "${profile}" (pid ${owner.pid}).`
|
|
3777
3881
|
);
|
|
3778
3882
|
}
|
|
3779
|
-
await
|
|
3883
|
+
await fs7.rm(lockPath, { force: true });
|
|
3780
3884
|
}
|
|
3781
3885
|
}
|
|
3782
3886
|
throw new Error(`Unable to acquire listener ownership for profile "${profile}".`);
|
|
@@ -3794,7 +3898,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
3794
3898
|
}
|
|
3795
3899
|
const socketPath = getListenIpcSocketPath(profile);
|
|
3796
3900
|
if (process.platform !== "win32") {
|
|
3797
|
-
await
|
|
3901
|
+
await fs7.rm(socketPath, { force: true });
|
|
3798
3902
|
}
|
|
3799
3903
|
const uploadTimeoutMs = parsePositiveIntFromEnv(
|
|
3800
3904
|
"OPENZCA_UPLOAD_IPC_HANDLER_TIMEOUT_MS",
|
|
@@ -3975,7 +4079,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
3975
4079
|
server.close(() => resolve());
|
|
3976
4080
|
});
|
|
3977
4081
|
if (process.platform !== "win32") {
|
|
3978
|
-
await
|
|
4082
|
+
await fs7.rm(socketPath, { force: true });
|
|
3979
4083
|
}
|
|
3980
4084
|
writeDebugLine(
|
|
3981
4085
|
"listen.ipc.stopped",
|
|
@@ -6022,7 +6126,7 @@ function toDbRecordFromRecentMessage(params) {
|
|
|
6022
6126
|
});
|
|
6023
6127
|
}
|
|
6024
6128
|
async function parseCredentialFile(filePath) {
|
|
6025
|
-
const raw = await
|
|
6129
|
+
const raw = await fs7.readFile(filePath, "utf8");
|
|
6026
6130
|
const parsed = JSON.parse(raw);
|
|
6027
6131
|
if (!parsed.imei || !parsed.cookie || !parsed.userAgent) {
|
|
6028
6132
|
throw new Error("Credential file must include imei, cookie, and userAgent.");
|
|
@@ -6043,7 +6147,7 @@ async function waitForFileContent(filePath, timeoutMs) {
|
|
|
6043
6147
|
const startedAt = Date.now();
|
|
6044
6148
|
while (Date.now() - startedAt < timeoutMs) {
|
|
6045
6149
|
try {
|
|
6046
|
-
const data = await
|
|
6150
|
+
const data = await fs7.readFile(filePath);
|
|
6047
6151
|
if (data.length > 0) {
|
|
6048
6152
|
return data;
|
|
6049
6153
|
}
|
|
@@ -6058,8 +6162,8 @@ async function emitQrBase64FromDetachedLogin(profile, qrPath) {
|
|
|
6058
6162
|
if (!scriptPath) {
|
|
6059
6163
|
throw new Error("Cannot resolve CLI entrypoint for QR base64 mode.");
|
|
6060
6164
|
}
|
|
6061
|
-
const tempDir = await
|
|
6062
|
-
const targetPath =
|
|
6165
|
+
const tempDir = await fs7.mkdtemp(path7.join(os5.tmpdir(), "openzca-qr-"));
|
|
6166
|
+
const targetPath = path7.resolve(qrPath ?? path7.join(tempDir, "qr.png"));
|
|
6063
6167
|
const child = spawn2(
|
|
6064
6168
|
process.execPath,
|
|
6065
6169
|
[scriptPath, "--profile", profile, "auth", "login", "--qr-path", targetPath],
|
|
@@ -6337,7 +6441,7 @@ function mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind) {
|
|
|
6337
6441
|
if (fromType) return fromType;
|
|
6338
6442
|
try {
|
|
6339
6443
|
const parsedUrl = new URL(mediaUrl);
|
|
6340
|
-
const ext =
|
|
6444
|
+
const ext = path7.extname(parsedUrl.pathname);
|
|
6341
6445
|
if (ext) return ext;
|
|
6342
6446
|
} catch {
|
|
6343
6447
|
}
|
|
@@ -6368,20 +6472,20 @@ function parseInboundMediaFetchTimeoutMs() {
|
|
|
6368
6472
|
return Math.trunc(parsed);
|
|
6369
6473
|
}
|
|
6370
6474
|
function resolveOpenClawMediaDir() {
|
|
6371
|
-
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() ||
|
|
6372
|
-
return
|
|
6475
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path7.join(os5.homedir(), ".openclaw");
|
|
6476
|
+
return path7.join(stateDir, "media");
|
|
6373
6477
|
}
|
|
6374
6478
|
function resolveInboundMediaDir(profile) {
|
|
6375
6479
|
const configuredRaw = process.env.OPENZCA_LISTEN_MEDIA_DIR?.trim();
|
|
6376
6480
|
if (configuredRaw) {
|
|
6377
6481
|
const configured = normalizeMediaInput(configuredRaw);
|
|
6378
|
-
return
|
|
6482
|
+
return path7.isAbsolute(configured) ? configured : path7.resolve(process.cwd(), configured);
|
|
6379
6483
|
}
|
|
6380
6484
|
const legacyRequested = process.env.OPENZCA_LISTEN_MEDIA_LEGACY_DIR?.trim() === "1";
|
|
6381
6485
|
if (legacyRequested) {
|
|
6382
|
-
return
|
|
6486
|
+
return path7.join(getProfileDir(profile), "inbound-media");
|
|
6383
6487
|
}
|
|
6384
|
-
return
|
|
6488
|
+
return path7.join(resolveOpenClawMediaDir(), "openzca", profile, "inbound");
|
|
6385
6489
|
}
|
|
6386
6490
|
async function cacheInboundMediaToProfile(profile, mediaUrl, kind) {
|
|
6387
6491
|
const maxBytes = parseMaxInboundMediaBytes();
|
|
@@ -6415,11 +6519,11 @@ async function cacheInboundMediaToProfile(profile, mediaUrl, kind) {
|
|
|
6415
6519
|
return null;
|
|
6416
6520
|
}
|
|
6417
6521
|
const dir = resolveInboundMediaDir(profile);
|
|
6418
|
-
await
|
|
6522
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
6419
6523
|
const ext = mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind);
|
|
6420
6524
|
const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
6421
|
-
const mediaPath =
|
|
6422
|
-
await
|
|
6525
|
+
const mediaPath = path7.join(dir, `${id}${ext}`);
|
|
6526
|
+
await fs7.writeFile(mediaPath, data);
|
|
6423
6527
|
return { mediaPath, mediaType };
|
|
6424
6528
|
}
|
|
6425
6529
|
async function cacheRemoteMediaEntries(params) {
|
|
@@ -6929,7 +7033,7 @@ auth.command("login").description("Login with QR code").option("-q, --qr-path <p
|
|
|
6929
7033
|
auth.command("login-cred [file]").alias("login-creds").description("Login using credential JSON file").action(
|
|
6930
7034
|
wrapAction(async (file, command) => {
|
|
6931
7035
|
const profile = await currentProfile(command);
|
|
6932
|
-
const credentials = file ? await parseCredentialFile(
|
|
7036
|
+
const credentials = file ? await parseCredentialFile(path7.resolve(normalizeMediaInput(file))) : toCredentials(
|
|
6933
7037
|
await loadCredentials(profile) ?? (() => {
|
|
6934
7038
|
throw new Error(
|
|
6935
7039
|
`No saved credentials for profile "${profile}". Run: openzca auth login`
|
|
@@ -7036,7 +7140,7 @@ dbCmd.command("reset").option("-y, --yes", "Delete the SQLite DB file for the ac
|
|
|
7036
7140
|
const removedPaths = [];
|
|
7037
7141
|
const deleteIfExists = async (filename) => {
|
|
7038
7142
|
try {
|
|
7039
|
-
await
|
|
7143
|
+
await fs7.unlink(filename);
|
|
7040
7144
|
removedPaths.push(filename);
|
|
7041
7145
|
} catch (error) {
|
|
7042
7146
|
if (error.code !== "ENOENT") {
|
|
@@ -7749,53 +7853,108 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
|
|
|
7749
7853
|
}
|
|
7750
7854
|
const urlInputs = files.filter((entry) => isHttpUrl(entry));
|
|
7751
7855
|
const localInputs = files.filter((entry) => !isHttpUrl(entry));
|
|
7856
|
+
const publishCommand = getVoicePublishCommandFromEnv();
|
|
7857
|
+
const ffmpegAvailable = localInputs.length > 0 && publishCommand ? await isFfmpegAvailable() : false;
|
|
7858
|
+
const usePublishFlow = localInputs.length > 0 && Boolean(publishCommand) && ffmpegAvailable;
|
|
7752
7859
|
writeDebugLine(
|
|
7753
7860
|
"msg.voice.inputs",
|
|
7754
7861
|
{
|
|
7755
7862
|
threadId,
|
|
7756
7863
|
isGroup: Boolean(opts.group),
|
|
7757
7864
|
localInputs,
|
|
7758
|
-
urlInputs
|
|
7865
|
+
urlInputs,
|
|
7866
|
+
publishConfigured: Boolean(publishCommand),
|
|
7867
|
+
ffmpegAvailable: localInputs.length > 0 ? ffmpegAvailable : void 0,
|
|
7868
|
+
mode: usePublishFlow ? "publish" : "legacy"
|
|
7759
7869
|
},
|
|
7760
7870
|
command
|
|
7761
7871
|
);
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
const
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7872
|
+
await assertFilesExist(localInputs);
|
|
7873
|
+
const publishedLocals = [];
|
|
7874
|
+
let uploadedLocals = [];
|
|
7875
|
+
if (usePublishFlow) {
|
|
7876
|
+
for (const localInput of localInputs) {
|
|
7877
|
+
const normalized = await normalizeVoiceForPublish(localInput);
|
|
7878
|
+
try {
|
|
7879
|
+
const mediaUrl = await publishVoiceFile(publishCommand, normalized.path);
|
|
7880
|
+
publishedLocals.push({
|
|
7881
|
+
mediaPath: localInput,
|
|
7882
|
+
mediaUrl
|
|
7883
|
+
});
|
|
7884
|
+
} finally {
|
|
7885
|
+
await normalized.cleanup();
|
|
7771
7886
|
}
|
|
7772
7887
|
}
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7888
|
+
} else if (localInputs.length > 0) {
|
|
7889
|
+
uploadedLocals = await withUploadListener(
|
|
7890
|
+
api,
|
|
7891
|
+
command,
|
|
7892
|
+
async () => api.uploadAttachment(localInputs, threadId, type)
|
|
7893
|
+
);
|
|
7894
|
+
}
|
|
7895
|
+
const pendingPublished = [...publishedLocals];
|
|
7896
|
+
const pendingUploaded = [...uploadedLocals];
|
|
7897
|
+
const outboundVoices = [];
|
|
7898
|
+
for (const entry of files) {
|
|
7899
|
+
if (isHttpUrl(entry)) {
|
|
7900
|
+
outboundVoices.push({
|
|
7901
|
+
mediaUrl: entry
|
|
7902
|
+
});
|
|
7903
|
+
continue;
|
|
7777
7904
|
}
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7905
|
+
if (usePublishFlow) {
|
|
7906
|
+
const nextPublished = pendingPublished.shift();
|
|
7907
|
+
if (!nextPublished) {
|
|
7908
|
+
throw new Error(`Voice publish flow lost local file mapping for: ${entry}`);
|
|
7909
|
+
}
|
|
7910
|
+
outboundVoices.push(nextPublished);
|
|
7911
|
+
continue;
|
|
7912
|
+
}
|
|
7913
|
+
const nextUploaded = pendingUploaded.shift();
|
|
7914
|
+
if (!nextUploaded) {
|
|
7915
|
+
throw new Error(`Voice upload flow lost local file mapping for: ${entry}`);
|
|
7916
|
+
}
|
|
7917
|
+
if (nextUploaded.fileType === "others" || nextUploaded.fileType === "video") {
|
|
7918
|
+
outboundVoices.push({
|
|
7919
|
+
mediaPath: entry,
|
|
7920
|
+
mediaUrl: nextUploaded.fileUrl,
|
|
7921
|
+
rawJson: JSON.stringify(nextUploaded)
|
|
7795
7922
|
});
|
|
7796
7923
|
}
|
|
7797
|
-
}
|
|
7798
|
-
|
|
7924
|
+
}
|
|
7925
|
+
if (outboundVoices.length === 0) {
|
|
7926
|
+
throw new Error(
|
|
7927
|
+
"No valid voice attachment generated. Use an audio file (e.g. .aac, .mp3, .m4a, .wav, .ogg)."
|
|
7928
|
+
);
|
|
7929
|
+
}
|
|
7930
|
+
const results = [];
|
|
7931
|
+
for (const item of outboundVoices) {
|
|
7932
|
+
results.push(await sendVoice({ voiceUrl: item.mediaUrl }, threadId, type));
|
|
7933
|
+
}
|
|
7934
|
+
output(results, false);
|
|
7935
|
+
if (await shouldWriteToDb(profile)) {
|
|
7936
|
+
scheduleDbWrite(profile, command, "msg.voice.db.persist_error", async () => {
|
|
7937
|
+
await persistOutgoingMessageBestEffort({
|
|
7938
|
+
profile,
|
|
7939
|
+
api,
|
|
7940
|
+
threadId,
|
|
7941
|
+
group: opts.group,
|
|
7942
|
+
msgType: "voice",
|
|
7943
|
+
response: results,
|
|
7944
|
+
rawPayload: {
|
|
7945
|
+
mode: usePublishFlow ? "publish" : "legacy",
|
|
7946
|
+
directUrls: urlInputs,
|
|
7947
|
+
published: publishedLocals,
|
|
7948
|
+
uploaded: uploadedLocals
|
|
7949
|
+
},
|
|
7950
|
+
media: outboundVoices.map((item) => ({
|
|
7951
|
+
mediaKind: "voice",
|
|
7952
|
+
mediaPath: item.mediaPath,
|
|
7953
|
+
mediaUrl: item.mediaUrl,
|
|
7954
|
+
rawJson: item.rawJson
|
|
7955
|
+
}))
|
|
7956
|
+
});
|
|
7957
|
+
});
|
|
7799
7958
|
}
|
|
7800
7959
|
}
|
|
7801
7960
|
)
|