openzca 0.1.56 → 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 +509 -124
- 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) {
|
|
@@ -3291,6 +3395,125 @@ function inferReplyMessageThreadId(params) {
|
|
|
3291
3395
|
return void 0;
|
|
3292
3396
|
}
|
|
3293
3397
|
|
|
3398
|
+
// src/lib/adaptive-batch.ts
|
|
3399
|
+
var RETRYABLE_LOOKUP_ERROR_PATTERNS = [
|
|
3400
|
+
/retry limit/i,
|
|
3401
|
+
/\brate limit/i,
|
|
3402
|
+
/\btoo many requests?\b/i,
|
|
3403
|
+
/\btimeout\b/i,
|
|
3404
|
+
/\btimed out\b/i,
|
|
3405
|
+
/\betimedout\b/i,
|
|
3406
|
+
/\beconnreset\b/i,
|
|
3407
|
+
/\besockettimedout\b/i,
|
|
3408
|
+
/\bsocket hang up\b/i,
|
|
3409
|
+
/\btemporar(?:y|ily)\b/i
|
|
3410
|
+
];
|
|
3411
|
+
var SPLITTABLE_LOOKUP_ERROR_PATTERNS = [
|
|
3412
|
+
/\binvalid param(?:eter)?s?\b/i,
|
|
3413
|
+
/\binvalid request\b/i,
|
|
3414
|
+
/\bbad request\b/i,
|
|
3415
|
+
/tham so khong hop le/i,
|
|
3416
|
+
/tham số không hợp lệ/i
|
|
3417
|
+
];
|
|
3418
|
+
function sleep2(ms) {
|
|
3419
|
+
return new Promise((resolve) => {
|
|
3420
|
+
setTimeout(resolve, ms);
|
|
3421
|
+
});
|
|
3422
|
+
}
|
|
3423
|
+
function chunkKeys(keys, size) {
|
|
3424
|
+
const chunkSize = Math.max(1, Math.trunc(size) || 1);
|
|
3425
|
+
const chunks = [];
|
|
3426
|
+
for (let index = 0; index < keys.length; index += chunkSize) {
|
|
3427
|
+
chunks.push(keys.slice(index, index + chunkSize));
|
|
3428
|
+
}
|
|
3429
|
+
return chunks;
|
|
3430
|
+
}
|
|
3431
|
+
function toErrorText(error) {
|
|
3432
|
+
return error instanceof Error ? error.message : String(error);
|
|
3433
|
+
}
|
|
3434
|
+
function isRetryableLookupError(error) {
|
|
3435
|
+
const message = toErrorText(error);
|
|
3436
|
+
return RETRYABLE_LOOKUP_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
3437
|
+
}
|
|
3438
|
+
function isSplittableLookupError(error) {
|
|
3439
|
+
const message = toErrorText(error);
|
|
3440
|
+
return SPLITTABLE_LOOKUP_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
3441
|
+
}
|
|
3442
|
+
async function runAdaptiveBatch(keys, options) {
|
|
3443
|
+
const maxRetries = Math.max(0, options.maxRetries ?? 2);
|
|
3444
|
+
const initialDelayMs = Math.max(0, options.retryDelayMs ?? 400);
|
|
3445
|
+
const backoffMultiplier = Math.max(1, options.backoffMultiplier ?? 2);
|
|
3446
|
+
const shouldRetry = options.shouldRetry ?? isRetryableLookupError;
|
|
3447
|
+
const shouldSplit = options.shouldSplit ?? isSplittableLookupError;
|
|
3448
|
+
let attempt = 0;
|
|
3449
|
+
let delayMs = initialDelayMs;
|
|
3450
|
+
while (true) {
|
|
3451
|
+
try {
|
|
3452
|
+
return await options.fetchBatch(keys) ?? {};
|
|
3453
|
+
} catch (error) {
|
|
3454
|
+
if (keys.length > 1 && shouldSplit(error)) {
|
|
3455
|
+
throw error;
|
|
3456
|
+
}
|
|
3457
|
+
attempt += 1;
|
|
3458
|
+
if (attempt > maxRetries || !shouldRetry(error)) {
|
|
3459
|
+
throw error;
|
|
3460
|
+
}
|
|
3461
|
+
await options.onRetry?.({
|
|
3462
|
+
keys: [...keys],
|
|
3463
|
+
attempt,
|
|
3464
|
+
maxRetries,
|
|
3465
|
+
delayMs,
|
|
3466
|
+
error
|
|
3467
|
+
});
|
|
3468
|
+
if (delayMs > 0) {
|
|
3469
|
+
await sleep2(delayMs);
|
|
3470
|
+
}
|
|
3471
|
+
delayMs = Math.max(delayMs * backoffMultiplier, delayMs + 1);
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
async function fetchAdaptiveObjectBatches(keys, options) {
|
|
3476
|
+
const uniqueKeys = Array.from(new Set(keys.map((value) => value.trim()).filter(Boolean)));
|
|
3477
|
+
const pending = chunkKeys(uniqueKeys, options.initialBatchSize ?? 5);
|
|
3478
|
+
const values = /* @__PURE__ */ new Map();
|
|
3479
|
+
const errors = [];
|
|
3480
|
+
const shouldRetry = options.shouldRetry ?? isRetryableLookupError;
|
|
3481
|
+
const shouldSplit = options.shouldSplit ?? isSplittableLookupError;
|
|
3482
|
+
const continueOnItemError = options.continueOnItemError ?? true;
|
|
3483
|
+
const batchDelayMs = Math.max(0, options.batchDelayMs ?? 75);
|
|
3484
|
+
while (pending.length > 0) {
|
|
3485
|
+
const batch = pending.shift();
|
|
3486
|
+
if (!batch || batch.length === 0) {
|
|
3487
|
+
continue;
|
|
3488
|
+
}
|
|
3489
|
+
try {
|
|
3490
|
+
const result = await runAdaptiveBatch(batch, options);
|
|
3491
|
+
for (const key of batch) {
|
|
3492
|
+
const value = result[key];
|
|
3493
|
+
if (value !== void 0) {
|
|
3494
|
+
values.set(key, value);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
} catch (error) {
|
|
3498
|
+
if (batch.length > 1 && (shouldSplit(error) || shouldRetry(error))) {
|
|
3499
|
+
pending.unshift(...chunkKeys(batch, Math.ceil(batch.length / 2)));
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
if (!continueOnItemError || batch.length > 1) {
|
|
3503
|
+
throw error;
|
|
3504
|
+
}
|
|
3505
|
+
const itemError = { key: batch[0], error };
|
|
3506
|
+
errors.push(itemError);
|
|
3507
|
+
await options.onItemError?.(itemError);
|
|
3508
|
+
continue;
|
|
3509
|
+
}
|
|
3510
|
+
if (batchDelayMs > 0 && pending.length > 0) {
|
|
3511
|
+
await sleep2(batchDelayMs);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
return { values, errors };
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3294
3517
|
// src/cli.ts
|
|
3295
3518
|
var require3 = createRequire2(import.meta.url);
|
|
3296
3519
|
var { version: PKG_VERSION } = require3("../package.json");
|
|
@@ -3372,9 +3595,9 @@ function resolveDebugEnabled(command) {
|
|
|
3372
3595
|
}
|
|
3373
3596
|
function resolveDebugFilePath(command) {
|
|
3374
3597
|
const options = getDebugOptions(command);
|
|
3375
|
-
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");
|
|
3376
3599
|
const normalized = normalizeMediaInput(configured);
|
|
3377
|
-
return
|
|
3600
|
+
return path7.isAbsolute(normalized) ? normalized : path7.resolve(process.cwd(), normalized);
|
|
3378
3601
|
}
|
|
3379
3602
|
function writeDebugLine(event, details, command) {
|
|
3380
3603
|
if (!resolveDebugEnabled(command)) {
|
|
@@ -3385,7 +3608,7 @@ function writeDebugLine(event, details, command) {
|
|
|
3385
3608
|
`;
|
|
3386
3609
|
const filePath = resolveDebugFilePath(command);
|
|
3387
3610
|
try {
|
|
3388
|
-
fsSync.mkdirSync(
|
|
3611
|
+
fsSync.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
3389
3612
|
fsSync.appendFileSync(filePath, line, "utf8");
|
|
3390
3613
|
} catch {
|
|
3391
3614
|
}
|
|
@@ -3492,14 +3715,14 @@ function collectIdsFromCacheEntries(entries, keys) {
|
|
|
3492
3715
|
return ids;
|
|
3493
3716
|
}
|
|
3494
3717
|
function getListenerOwnerLockPath(profile) {
|
|
3495
|
-
return
|
|
3718
|
+
return path7.join(getProfileDir(profile), "listener-owner.json");
|
|
3496
3719
|
}
|
|
3497
3720
|
function getListenIpcSocketPath(profile) {
|
|
3498
3721
|
if (process.platform === "win32") {
|
|
3499
3722
|
const safe = profile.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
3500
3723
|
return `\\\\.\\pipe\\openzca-listen-${safe}`;
|
|
3501
3724
|
}
|
|
3502
|
-
return
|
|
3725
|
+
return path7.join(getProfileDir(profile), "listen.sock");
|
|
3503
3726
|
}
|
|
3504
3727
|
function parsePositiveIntFromUnknown(value) {
|
|
3505
3728
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
@@ -3532,6 +3755,44 @@ function retrySendMethod(operation, command, metaBuilder) {
|
|
|
3532
3755
|
}
|
|
3533
3756
|
});
|
|
3534
3757
|
}
|
|
3758
|
+
var LOOKUP_BATCH_SIZE = 5;
|
|
3759
|
+
var LOOKUP_RETRY_COUNT = 2;
|
|
3760
|
+
var LOOKUP_RETRY_DELAY_MS = 400;
|
|
3761
|
+
var LOOKUP_BATCH_DELAY_MS = 75;
|
|
3762
|
+
async function fetchGroupInfoRecords(api, groupIds) {
|
|
3763
|
+
const { values } = await fetchAdaptiveObjectBatches(groupIds, {
|
|
3764
|
+
fetchBatch: async (keys) => {
|
|
3765
|
+
const response = await api.getGroupInfo(keys);
|
|
3766
|
+
return response.gridInfoMap ?? {};
|
|
3767
|
+
},
|
|
3768
|
+
initialBatchSize: LOOKUP_BATCH_SIZE,
|
|
3769
|
+
maxRetries: LOOKUP_RETRY_COUNT,
|
|
3770
|
+
retryDelayMs: LOOKUP_RETRY_DELAY_MS,
|
|
3771
|
+
batchDelayMs: LOOKUP_BATCH_DELAY_MS
|
|
3772
|
+
});
|
|
3773
|
+
return values;
|
|
3774
|
+
}
|
|
3775
|
+
async function fetchGroupInfoRecord(api, groupId) {
|
|
3776
|
+
const groups = await fetchGroupInfoRecords(api, [groupId]);
|
|
3777
|
+
const group2 = groups.get(groupId);
|
|
3778
|
+
if (!group2) {
|
|
3779
|
+
throw new Error(`Group not found: ${groupId}`);
|
|
3780
|
+
}
|
|
3781
|
+
return group2;
|
|
3782
|
+
}
|
|
3783
|
+
async function fetchGroupMemberProfiles(api, memberIds) {
|
|
3784
|
+
const { values } = await fetchAdaptiveObjectBatches(memberIds, {
|
|
3785
|
+
fetchBatch: async (keys) => {
|
|
3786
|
+
const response = await api.getGroupMembersInfo(keys);
|
|
3787
|
+
return response.profiles ?? {};
|
|
3788
|
+
},
|
|
3789
|
+
initialBatchSize: LOOKUP_BATCH_SIZE,
|
|
3790
|
+
maxRetries: LOOKUP_RETRY_COUNT,
|
|
3791
|
+
retryDelayMs: LOOKUP_RETRY_DELAY_MS,
|
|
3792
|
+
batchDelayMs: LOOKUP_BATCH_DELAY_MS
|
|
3793
|
+
});
|
|
3794
|
+
return values;
|
|
3795
|
+
}
|
|
3535
3796
|
function isProcessAlive(pid) {
|
|
3536
3797
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
3537
3798
|
try {
|
|
@@ -3545,7 +3806,7 @@ function isProcessAlive(pid) {
|
|
|
3545
3806
|
}
|
|
3546
3807
|
async function readListenerOwnerRecord(lockPath) {
|
|
3547
3808
|
try {
|
|
3548
|
-
const raw = await
|
|
3809
|
+
const raw = await fs7.readFile(lockPath, "utf8");
|
|
3549
3810
|
const parsed = JSON.parse(raw);
|
|
3550
3811
|
const pid = parsePositiveIntFromUnknown(parsed.pid);
|
|
3551
3812
|
if (!pid) return null;
|
|
@@ -3565,11 +3826,11 @@ async function readActiveListenerOwner(profile) {
|
|
|
3565
3826
|
const lockPath = getListenerOwnerLockPath(profile);
|
|
3566
3827
|
const record = await readListenerOwnerRecord(lockPath);
|
|
3567
3828
|
if (!record) {
|
|
3568
|
-
await
|
|
3829
|
+
await fs7.rm(lockPath, { force: true });
|
|
3569
3830
|
return null;
|
|
3570
3831
|
}
|
|
3571
3832
|
if (!isProcessAlive(record.pid)) {
|
|
3572
|
-
await
|
|
3833
|
+
await fs7.rm(lockPath, { force: true });
|
|
3573
3834
|
return null;
|
|
3574
3835
|
}
|
|
3575
3836
|
return record;
|
|
@@ -3585,7 +3846,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
3585
3846
|
};
|
|
3586
3847
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
3587
3848
|
try {
|
|
3588
|
-
await
|
|
3849
|
+
await fs7.writeFile(lockPath, `${JSON.stringify(record, null, 2)}
|
|
3589
3850
|
`, {
|
|
3590
3851
|
encoding: "utf8",
|
|
3591
3852
|
flag: "wx"
|
|
@@ -3598,7 +3859,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
3598
3859
|
released = true;
|
|
3599
3860
|
const current = await readListenerOwnerRecord(lockPath);
|
|
3600
3861
|
if (current && current.pid !== process.pid) return;
|
|
3601
|
-
await
|
|
3862
|
+
await fs7.rm(lockPath, { force: true });
|
|
3602
3863
|
writeDebugLine(
|
|
3603
3864
|
"listen.owner.released",
|
|
3604
3865
|
{
|
|
@@ -3619,7 +3880,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
3619
3880
|
`Another openzca listener already owns profile "${profile}" (pid ${owner.pid}).`
|
|
3620
3881
|
);
|
|
3621
3882
|
}
|
|
3622
|
-
await
|
|
3883
|
+
await fs7.rm(lockPath, { force: true });
|
|
3623
3884
|
}
|
|
3624
3885
|
}
|
|
3625
3886
|
throw new Error(`Unable to acquire listener ownership for profile "${profile}".`);
|
|
@@ -3637,7 +3898,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
3637
3898
|
}
|
|
3638
3899
|
const socketPath = getListenIpcSocketPath(profile);
|
|
3639
3900
|
if (process.platform !== "win32") {
|
|
3640
|
-
await
|
|
3901
|
+
await fs7.rm(socketPath, { force: true });
|
|
3641
3902
|
}
|
|
3642
3903
|
const uploadTimeoutMs = parsePositiveIntFromEnv(
|
|
3643
3904
|
"OPENZCA_UPLOAD_IPC_HANDLER_TIMEOUT_MS",
|
|
@@ -3737,7 +3998,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
3737
3998
|
command
|
|
3738
3999
|
);
|
|
3739
4000
|
} catch (error) {
|
|
3740
|
-
fail(parsed.requestId,
|
|
4001
|
+
fail(parsed.requestId, toErrorText2(error));
|
|
3741
4002
|
writeDebugLine(
|
|
3742
4003
|
"listen.ipc.upload.error",
|
|
3743
4004
|
{
|
|
@@ -3746,7 +4007,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
3746
4007
|
requestId: parsed.requestId,
|
|
3747
4008
|
threadId: parsed.threadId,
|
|
3748
4009
|
threadType: parsed.threadType,
|
|
3749
|
-
message:
|
|
4010
|
+
message: toErrorText2(error)
|
|
3750
4011
|
},
|
|
3751
4012
|
command
|
|
3752
4013
|
);
|
|
@@ -3775,7 +4036,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
3775
4036
|
{
|
|
3776
4037
|
profile,
|
|
3777
4038
|
sessionId,
|
|
3778
|
-
message:
|
|
4039
|
+
message: toErrorText2(error)
|
|
3779
4040
|
},
|
|
3780
4041
|
command
|
|
3781
4042
|
);
|
|
@@ -3787,7 +4048,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
3787
4048
|
{
|
|
3788
4049
|
profile,
|
|
3789
4050
|
sessionId,
|
|
3790
|
-
message:
|
|
4051
|
+
message: toErrorText2(error)
|
|
3791
4052
|
},
|
|
3792
4053
|
command
|
|
3793
4054
|
);
|
|
@@ -3818,7 +4079,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
3818
4079
|
server.close(() => resolve());
|
|
3819
4080
|
});
|
|
3820
4081
|
if (process.platform !== "win32") {
|
|
3821
|
-
await
|
|
4082
|
+
await fs7.rm(socketPath, { force: true });
|
|
3822
4083
|
}
|
|
3823
4084
|
writeDebugLine(
|
|
3824
4085
|
"listen.ipc.stopped",
|
|
@@ -3976,7 +4237,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
3976
4237
|
{
|
|
3977
4238
|
profile,
|
|
3978
4239
|
threadId,
|
|
3979
|
-
message:
|
|
4240
|
+
message: toErrorText2(error)
|
|
3980
4241
|
},
|
|
3981
4242
|
command
|
|
3982
4243
|
);
|
|
@@ -4001,7 +4262,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
4001
4262
|
{
|
|
4002
4263
|
profile,
|
|
4003
4264
|
threadId,
|
|
4004
|
-
message:
|
|
4265
|
+
message: toErrorText2(error)
|
|
4005
4266
|
},
|
|
4006
4267
|
command
|
|
4007
4268
|
);
|
|
@@ -4242,8 +4503,8 @@ async function persistOutgoingMessageBestEffort(params) {
|
|
|
4242
4503
|
});
|
|
4243
4504
|
}
|
|
4244
4505
|
}
|
|
4245
|
-
async function persistGroupMembersSnapshot(profile, groupId, api) {
|
|
4246
|
-
const rows = await listGroupMemberRows(api, groupId);
|
|
4506
|
+
async function persistGroupMembersSnapshot(profile, groupId, api, groupInfo) {
|
|
4507
|
+
const rows = await listGroupMemberRows(api, groupId, groupInfo);
|
|
4247
4508
|
const snapshotAtMs = Date.now();
|
|
4248
4509
|
for (const row of rows) {
|
|
4249
4510
|
await persistContact({
|
|
@@ -4448,7 +4709,15 @@ async function prepareDbGroupTarget(params) {
|
|
|
4448
4709
|
isHidden: params.hiddenIds.has(params.groupId),
|
|
4449
4710
|
rawJson: params.rawJson
|
|
4450
4711
|
});
|
|
4451
|
-
|
|
4712
|
+
if (params.hydrateMembers === false) {
|
|
4713
|
+
return {};
|
|
4714
|
+
}
|
|
4715
|
+
try {
|
|
4716
|
+
await persistGroupMembersSnapshot(params.profile, params.groupId, params.api, params.group);
|
|
4717
|
+
return {};
|
|
4718
|
+
} catch (error) {
|
|
4719
|
+
return { memberSnapshotError: toErrorText2(error) };
|
|
4720
|
+
}
|
|
4452
4721
|
}
|
|
4453
4722
|
function resolveContactDisplayName(params) {
|
|
4454
4723
|
return params.displayName?.trim() || params.zaloName?.trim() || params.fallbackTitle?.trim() || params.userId.trim() || void 0;
|
|
@@ -4550,18 +4819,17 @@ async function hydrateUnknownLiveGroup(params) {
|
|
|
4550
4819
|
}
|
|
4551
4820
|
}
|
|
4552
4821
|
if (group2 || title) {
|
|
4553
|
-
await
|
|
4822
|
+
await prepareDbGroupTarget({
|
|
4554
4823
|
profile: params.profile,
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4824
|
+
api: params.api,
|
|
4825
|
+
groupId: params.groupId,
|
|
4826
|
+
group: group2,
|
|
4558
4827
|
title,
|
|
4559
|
-
rawJson: group2 ? JSON.stringify(group2) : void 0
|
|
4828
|
+
rawJson: group2 ? JSON.stringify(group2) : void 0,
|
|
4829
|
+
pinnedIds: /* @__PURE__ */ new Set(),
|
|
4830
|
+
hiddenIds: /* @__PURE__ */ new Set(),
|
|
4831
|
+
hydrateMembers: Boolean(group2)
|
|
4560
4832
|
});
|
|
4561
|
-
try {
|
|
4562
|
-
await persistGroupMembersSnapshot(params.profile, params.groupId, params.api);
|
|
4563
|
-
} catch {
|
|
4564
|
-
}
|
|
4565
4833
|
return;
|
|
4566
4834
|
}
|
|
4567
4835
|
if (params.fallbackTitle?.trim()) {
|
|
@@ -4631,16 +4899,23 @@ async function syncDbGroupHistoryFull(params) {
|
|
|
4631
4899
|
pagesRequested = result.pagesRequested;
|
|
4632
4900
|
listenerImportedCount = await getStoredGroupMessageCount() - beforeCount;
|
|
4633
4901
|
} catch (error) {
|
|
4634
|
-
stopReason = `fallback_window:${
|
|
4902
|
+
stopReason = `fallback_window:${toErrorText2(error)}`;
|
|
4635
4903
|
completeness = "window";
|
|
4636
4904
|
}
|
|
4637
4905
|
const fallbackCount = 200;
|
|
4638
4906
|
params.progress?.(`merging recent group API window (${fallbackCount} per group)`);
|
|
4639
4907
|
const beforeApiCount = await getStoredGroupMessageCount();
|
|
4908
|
+
const topoffErrors = [];
|
|
4640
4909
|
for (const groupId of params.targetGroupIds) {
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4910
|
+
try {
|
|
4911
|
+
const messages = await fetchRecentGroupMessagesViaApi(params.api, groupId, fallbackCount);
|
|
4912
|
+
await persistMessages(messages);
|
|
4913
|
+
params.progress?.(`group ${groupId}: fetched ${messages.length} message(s) from group history API`);
|
|
4914
|
+
} catch (error) {
|
|
4915
|
+
const message = toErrorText2(error);
|
|
4916
|
+
topoffErrors.push({ groupId, error: message });
|
|
4917
|
+
params.progress?.(`group ${groupId}: group history API skipped (${message})`);
|
|
4918
|
+
}
|
|
4644
4919
|
}
|
|
4645
4920
|
const afterCount = await getStoredGroupMessageCount();
|
|
4646
4921
|
const apiAddedCount = afterCount - beforeApiCount;
|
|
@@ -4670,7 +4945,8 @@ async function syncDbGroupHistoryFull(params) {
|
|
|
4670
4945
|
imported,
|
|
4671
4946
|
completeness,
|
|
4672
4947
|
stopReason,
|
|
4673
|
-
pagesRequested
|
|
4948
|
+
pagesRequested,
|
|
4949
|
+
topoffErrors
|
|
4674
4950
|
});
|
|
4675
4951
|
}
|
|
4676
4952
|
async function syncDbFriendDirectory(params) {
|
|
@@ -4807,25 +5083,70 @@ async function runDbSync(params) {
|
|
|
4807
5083
|
});
|
|
4808
5084
|
}
|
|
4809
5085
|
if (params.mode === "all" || params.mode === "groups") {
|
|
4810
|
-
const groups = await
|
|
5086
|
+
const groups = await api.getAllGroups();
|
|
5087
|
+
const groupIds = Object.keys(groups.gridVerMap ?? {});
|
|
4811
5088
|
const targetGroupIds = /* @__PURE__ */ new Set();
|
|
4812
5089
|
const titleById = /* @__PURE__ */ new Map();
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
5090
|
+
params.progress?.(`syncing group directory for ${groupIds.length} group(s)`);
|
|
5091
|
+
for (const groupId of groupIds) {
|
|
5092
|
+
let group2;
|
|
5093
|
+
let title;
|
|
5094
|
+
try {
|
|
5095
|
+
try {
|
|
5096
|
+
group2 = await fetchGroupInfoRecord(api, groupId);
|
|
5097
|
+
title = extractGroupTitle(group2);
|
|
5098
|
+
} catch (error) {
|
|
5099
|
+
const message = toErrorText2(error);
|
|
5100
|
+
params.progress?.(`group ${groupId}: metadata unavailable (${message}), continuing`);
|
|
5101
|
+
summary.syncState.push({
|
|
5102
|
+
kind: "group",
|
|
5103
|
+
groupId,
|
|
5104
|
+
status: "warning",
|
|
5105
|
+
stage: "metadata",
|
|
5106
|
+
error: message
|
|
5107
|
+
});
|
|
5108
|
+
}
|
|
5109
|
+
const { memberSnapshotError } = await prepareDbGroupTarget({
|
|
5110
|
+
profile,
|
|
5111
|
+
api,
|
|
5112
|
+
groupId,
|
|
5113
|
+
group: group2,
|
|
5114
|
+
title,
|
|
5115
|
+
rawJson: group2 ? JSON.stringify(group2) : void 0,
|
|
5116
|
+
pinnedIds,
|
|
5117
|
+
hiddenIds,
|
|
5118
|
+
hydrateMembers: Boolean(group2)
|
|
5119
|
+
});
|
|
5120
|
+
if (memberSnapshotError) {
|
|
5121
|
+
params.progress?.(`group ${groupId}: member snapshot unavailable (${memberSnapshotError}), continuing`);
|
|
5122
|
+
summary.syncState.push({
|
|
5123
|
+
kind: "group",
|
|
5124
|
+
groupId,
|
|
5125
|
+
status: "warning",
|
|
5126
|
+
stage: "members",
|
|
5127
|
+
error: memberSnapshotError
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
targetGroupIds.add(groupId);
|
|
5131
|
+
titleById.set(groupId, title);
|
|
5132
|
+
} catch (error) {
|
|
5133
|
+
const message = toErrorText2(error);
|
|
5134
|
+
params.progress?.(`group ${groupId}: skipped (${message})`);
|
|
5135
|
+
await setSyncState({
|
|
5136
|
+
profile,
|
|
5137
|
+
scopeThreadId: groupId,
|
|
5138
|
+
threadType: "group",
|
|
5139
|
+
status: "error",
|
|
5140
|
+
error: message
|
|
5141
|
+
});
|
|
5142
|
+
summary.syncState.push({
|
|
5143
|
+
kind: "group",
|
|
5144
|
+
groupId,
|
|
5145
|
+
status: "error",
|
|
5146
|
+
stage: "prepare",
|
|
5147
|
+
error: message
|
|
5148
|
+
});
|
|
5149
|
+
}
|
|
4829
5150
|
}
|
|
4830
5151
|
await syncDbGroupHistoryFull({
|
|
4831
5152
|
profile,
|
|
@@ -4841,18 +5162,29 @@ async function runDbSync(params) {
|
|
|
4841
5162
|
if (!params.groupId) {
|
|
4842
5163
|
throw new Error("Missing group id for db sync group.");
|
|
4843
5164
|
}
|
|
4844
|
-
const
|
|
4845
|
-
const
|
|
4846
|
-
const
|
|
4847
|
-
await prepareDbGroupTarget({
|
|
5165
|
+
const group2 = await fetchGroupInfoRecord(api, params.groupId);
|
|
5166
|
+
const title = extractGroupTitle(group2);
|
|
5167
|
+
const { memberSnapshotError } = await prepareDbGroupTarget({
|
|
4848
5168
|
profile,
|
|
4849
5169
|
api,
|
|
4850
5170
|
groupId: params.groupId,
|
|
5171
|
+
group: group2,
|
|
4851
5172
|
title,
|
|
4852
5173
|
rawJson: group2 ? JSON.stringify(group2) : void 0,
|
|
4853
5174
|
pinnedIds,
|
|
4854
|
-
hiddenIds
|
|
5175
|
+
hiddenIds,
|
|
5176
|
+
hydrateMembers: Boolean(group2)
|
|
4855
5177
|
});
|
|
5178
|
+
if (memberSnapshotError) {
|
|
5179
|
+
params.progress?.(`group ${params.groupId}: member snapshot unavailable (${memberSnapshotError}), continuing`);
|
|
5180
|
+
summary.syncState.push({
|
|
5181
|
+
kind: "group",
|
|
5182
|
+
groupId: params.groupId,
|
|
5183
|
+
status: "warning",
|
|
5184
|
+
stage: "members",
|
|
5185
|
+
error: memberSnapshotError
|
|
5186
|
+
});
|
|
5187
|
+
}
|
|
4856
5188
|
await syncDbGroupHistoryFull({
|
|
4857
5189
|
profile,
|
|
4858
5190
|
api,
|
|
@@ -4911,8 +5243,8 @@ async function buildGroupsDetailed(api) {
|
|
|
4911
5243
|
const groups = await api.getAllGroups();
|
|
4912
5244
|
const ids = Object.keys(groups.gridVerMap ?? {});
|
|
4913
5245
|
if (ids.length === 0) return [];
|
|
4914
|
-
const info = await api
|
|
4915
|
-
return ids.map((id) => info.
|
|
5246
|
+
const info = await fetchGroupInfoRecords(api, ids);
|
|
5247
|
+
return ids.map((id) => info.get(id)).filter((item) => Boolean(item));
|
|
4916
5248
|
}
|
|
4917
5249
|
function normalizeGroupMemberId(value) {
|
|
4918
5250
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
@@ -4923,9 +5255,8 @@ function normalizeGroupMemberId(value) {
|
|
|
4923
5255
|
if (!trimmed) return "";
|
|
4924
5256
|
return trimmed.replace(/_\d+$/, "");
|
|
4925
5257
|
}
|
|
4926
|
-
async function listGroupMemberRows(api, groupId) {
|
|
4927
|
-
const
|
|
4928
|
-
const groupInfo = info.gridInfoMap[groupId];
|
|
5258
|
+
async function listGroupMemberRows(api, groupId, preloadedGroupInfo) {
|
|
5259
|
+
const groupInfo = preloadedGroupInfo ?? await fetchGroupInfoRecord(api, groupId);
|
|
4929
5260
|
if (!groupInfo) {
|
|
4930
5261
|
throw new Error(`Group not found: ${groupId}`);
|
|
4931
5262
|
}
|
|
@@ -4949,10 +5280,9 @@ async function listGroupMemberRows(api, groupId) {
|
|
|
4949
5280
|
...Array.from(currentMemberMap.keys())
|
|
4950
5281
|
])
|
|
4951
5282
|
);
|
|
4952
|
-
const
|
|
4953
|
-
const rawProfileMap = profiles.profiles;
|
|
5283
|
+
const profileLookup = ids.length > 0 ? await fetchGroupMemberProfiles(api, ids) : /* @__PURE__ */ new Map();
|
|
4954
5284
|
const profileMap = /* @__PURE__ */ new Map();
|
|
4955
|
-
for (const [key, profile] of
|
|
5285
|
+
for (const [key, profile] of profileLookup.entries()) {
|
|
4956
5286
|
if (!profile) continue;
|
|
4957
5287
|
const normalizedKey = normalizeGroupMemberId(key);
|
|
4958
5288
|
if (normalizedKey && !profileMap.has(normalizedKey)) {
|
|
@@ -5007,7 +5337,7 @@ function isListenerAlreadyStarted(error) {
|
|
|
5007
5337
|
if (!(error instanceof Error)) return false;
|
|
5008
5338
|
return /already started/i.test(error.message);
|
|
5009
5339
|
}
|
|
5010
|
-
function
|
|
5340
|
+
function toErrorText2(error) {
|
|
5011
5341
|
return error instanceof Error ? error.message : String(error);
|
|
5012
5342
|
}
|
|
5013
5343
|
var SHUTDOWN_CALLBACKS = /* @__PURE__ */ new Set();
|
|
@@ -5045,7 +5375,7 @@ async function runShutdownCallbacks(signal) {
|
|
|
5045
5375
|
"process.signal.callback_error",
|
|
5046
5376
|
{
|
|
5047
5377
|
signal,
|
|
5048
|
-
message:
|
|
5378
|
+
message: toErrorText2(error)
|
|
5049
5379
|
},
|
|
5050
5380
|
void 0
|
|
5051
5381
|
);
|
|
@@ -5128,7 +5458,7 @@ async function withUploadListener(api, command, task) {
|
|
|
5128
5458
|
writeDebugLine(
|
|
5129
5459
|
"msg.upload.listener.error",
|
|
5130
5460
|
{
|
|
5131
|
-
message:
|
|
5461
|
+
message: toErrorText2(error)
|
|
5132
5462
|
},
|
|
5133
5463
|
command
|
|
5134
5464
|
);
|
|
@@ -5170,7 +5500,7 @@ async function withUploadListener(api, command, task) {
|
|
|
5170
5500
|
finish();
|
|
5171
5501
|
};
|
|
5172
5502
|
const onConnectError = (error) => {
|
|
5173
|
-
finish(new Error(`Upload listener connection error: ${
|
|
5503
|
+
finish(new Error(`Upload listener connection error: ${toErrorText2(error)}`));
|
|
5174
5504
|
};
|
|
5175
5505
|
const onConnectClosed = (code, reason) => {
|
|
5176
5506
|
finish(
|
|
@@ -5796,7 +6126,7 @@ function toDbRecordFromRecentMessage(params) {
|
|
|
5796
6126
|
});
|
|
5797
6127
|
}
|
|
5798
6128
|
async function parseCredentialFile(filePath) {
|
|
5799
|
-
const raw = await
|
|
6129
|
+
const raw = await fs7.readFile(filePath, "utf8");
|
|
5800
6130
|
const parsed = JSON.parse(raw);
|
|
5801
6131
|
if (!parsed.imei || !parsed.cookie || !parsed.userAgent) {
|
|
5802
6132
|
throw new Error("Credential file must include imei, cookie, and userAgent.");
|
|
@@ -5808,7 +6138,7 @@ async function parseCredentialFile(filePath) {
|
|
|
5808
6138
|
language: parsed.language
|
|
5809
6139
|
};
|
|
5810
6140
|
}
|
|
5811
|
-
function
|
|
6141
|
+
function sleep3(ms) {
|
|
5812
6142
|
return new Promise((resolve) => {
|
|
5813
6143
|
setTimeout(resolve, ms);
|
|
5814
6144
|
});
|
|
@@ -5817,13 +6147,13 @@ async function waitForFileContent(filePath, timeoutMs) {
|
|
|
5817
6147
|
const startedAt = Date.now();
|
|
5818
6148
|
while (Date.now() - startedAt < timeoutMs) {
|
|
5819
6149
|
try {
|
|
5820
|
-
const data = await
|
|
6150
|
+
const data = await fs7.readFile(filePath);
|
|
5821
6151
|
if (data.length > 0) {
|
|
5822
6152
|
return data;
|
|
5823
6153
|
}
|
|
5824
6154
|
} catch {
|
|
5825
6155
|
}
|
|
5826
|
-
await
|
|
6156
|
+
await sleep3(150);
|
|
5827
6157
|
}
|
|
5828
6158
|
throw new Error(`Timed out waiting for QR image file: ${filePath}`);
|
|
5829
6159
|
}
|
|
@@ -5832,8 +6162,8 @@ async function emitQrBase64FromDetachedLogin(profile, qrPath) {
|
|
|
5832
6162
|
if (!scriptPath) {
|
|
5833
6163
|
throw new Error("Cannot resolve CLI entrypoint for QR base64 mode.");
|
|
5834
6164
|
}
|
|
5835
|
-
const tempDir = await
|
|
5836
|
-
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"));
|
|
5837
6167
|
const child = spawn2(
|
|
5838
6168
|
process.execPath,
|
|
5839
6169
|
[scriptPath, "--profile", profile, "auth", "login", "--qr-path", targetPath],
|
|
@@ -6111,7 +6441,7 @@ function mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind) {
|
|
|
6111
6441
|
if (fromType) return fromType;
|
|
6112
6442
|
try {
|
|
6113
6443
|
const parsedUrl = new URL(mediaUrl);
|
|
6114
|
-
const ext =
|
|
6444
|
+
const ext = path7.extname(parsedUrl.pathname);
|
|
6115
6445
|
if (ext) return ext;
|
|
6116
6446
|
} catch {
|
|
6117
6447
|
}
|
|
@@ -6142,20 +6472,20 @@ function parseInboundMediaFetchTimeoutMs() {
|
|
|
6142
6472
|
return Math.trunc(parsed);
|
|
6143
6473
|
}
|
|
6144
6474
|
function resolveOpenClawMediaDir() {
|
|
6145
|
-
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() ||
|
|
6146
|
-
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");
|
|
6147
6477
|
}
|
|
6148
6478
|
function resolveInboundMediaDir(profile) {
|
|
6149
6479
|
const configuredRaw = process.env.OPENZCA_LISTEN_MEDIA_DIR?.trim();
|
|
6150
6480
|
if (configuredRaw) {
|
|
6151
6481
|
const configured = normalizeMediaInput(configuredRaw);
|
|
6152
|
-
return
|
|
6482
|
+
return path7.isAbsolute(configured) ? configured : path7.resolve(process.cwd(), configured);
|
|
6153
6483
|
}
|
|
6154
6484
|
const legacyRequested = process.env.OPENZCA_LISTEN_MEDIA_LEGACY_DIR?.trim() === "1";
|
|
6155
6485
|
if (legacyRequested) {
|
|
6156
|
-
return
|
|
6486
|
+
return path7.join(getProfileDir(profile), "inbound-media");
|
|
6157
6487
|
}
|
|
6158
|
-
return
|
|
6488
|
+
return path7.join(resolveOpenClawMediaDir(), "openzca", profile, "inbound");
|
|
6159
6489
|
}
|
|
6160
6490
|
async function cacheInboundMediaToProfile(profile, mediaUrl, kind) {
|
|
6161
6491
|
const maxBytes = parseMaxInboundMediaBytes();
|
|
@@ -6189,11 +6519,11 @@ async function cacheInboundMediaToProfile(profile, mediaUrl, kind) {
|
|
|
6189
6519
|
return null;
|
|
6190
6520
|
}
|
|
6191
6521
|
const dir = resolveInboundMediaDir(profile);
|
|
6192
|
-
await
|
|
6522
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
6193
6523
|
const ext = mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind);
|
|
6194
6524
|
const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
6195
|
-
const mediaPath =
|
|
6196
|
-
await
|
|
6525
|
+
const mediaPath = path7.join(dir, `${id}${ext}`);
|
|
6526
|
+
await fs7.writeFile(mediaPath, data);
|
|
6197
6527
|
return { mediaPath, mediaType };
|
|
6198
6528
|
}
|
|
6199
6529
|
async function cacheRemoteMediaEntries(params) {
|
|
@@ -6703,7 +7033,7 @@ auth.command("login").description("Login with QR code").option("-q, --qr-path <p
|
|
|
6703
7033
|
auth.command("login-cred [file]").alias("login-creds").description("Login using credential JSON file").action(
|
|
6704
7034
|
wrapAction(async (file, command) => {
|
|
6705
7035
|
const profile = await currentProfile(command);
|
|
6706
|
-
const credentials = file ? await parseCredentialFile(
|
|
7036
|
+
const credentials = file ? await parseCredentialFile(path7.resolve(normalizeMediaInput(file))) : toCredentials(
|
|
6707
7037
|
await loadCredentials(profile) ?? (() => {
|
|
6708
7038
|
throw new Error(
|
|
6709
7039
|
`No saved credentials for profile "${profile}". Run: openzca auth login`
|
|
@@ -6810,7 +7140,7 @@ dbCmd.command("reset").option("-y, --yes", "Delete the SQLite DB file for the ac
|
|
|
6810
7140
|
const removedPaths = [];
|
|
6811
7141
|
const deleteIfExists = async (filename) => {
|
|
6812
7142
|
try {
|
|
6813
|
-
await
|
|
7143
|
+
await fs7.unlink(filename);
|
|
6814
7144
|
removedPaths.push(filename);
|
|
6815
7145
|
} catch (error) {
|
|
6816
7146
|
if (error.code !== "ENOENT") {
|
|
@@ -7523,53 +7853,108 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
|
|
|
7523
7853
|
}
|
|
7524
7854
|
const urlInputs = files.filter((entry) => isHttpUrl(entry));
|
|
7525
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;
|
|
7526
7859
|
writeDebugLine(
|
|
7527
7860
|
"msg.voice.inputs",
|
|
7528
7861
|
{
|
|
7529
7862
|
threadId,
|
|
7530
7863
|
isGroup: Boolean(opts.group),
|
|
7531
7864
|
localInputs,
|
|
7532
|
-
urlInputs
|
|
7865
|
+
urlInputs,
|
|
7866
|
+
publishConfigured: Boolean(publishCommand),
|
|
7867
|
+
ffmpegAvailable: localInputs.length > 0 ? ffmpegAvailable : void 0,
|
|
7868
|
+
mode: usePublishFlow ? "publish" : "legacy"
|
|
7533
7869
|
},
|
|
7534
7870
|
command
|
|
7535
7871
|
);
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
const
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
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();
|
|
7545
7886
|
}
|
|
7546
7887
|
}
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
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;
|
|
7551
7904
|
}
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
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)
|
|
7569
7922
|
});
|
|
7570
7923
|
}
|
|
7571
|
-
}
|
|
7572
|
-
|
|
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
|
+
});
|
|
7573
7958
|
}
|
|
7574
7959
|
}
|
|
7575
7960
|
)
|
|
@@ -9052,7 +9437,7 @@ ${replyContextText}` : replyContextText;
|
|
|
9052
9437
|
code,
|
|
9053
9438
|
reason: reason || void 0,
|
|
9054
9439
|
delayMs: keepAliveRestartDelayMs,
|
|
9055
|
-
message:
|
|
9440
|
+
message: toErrorText2(error),
|
|
9056
9441
|
sessionId
|
|
9057
9442
|
},
|
|
9058
9443
|
command
|