openzca 0.1.46 → 0.1.47
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 +2 -1
- package/dist/cli.js +300 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
|
|
|
69
69
|
|---------|-------------|
|
|
70
70
|
| `openzca msg send <threadId> <message>` | Send text message |
|
|
71
71
|
| `openzca msg image <threadId> [file]` | Send image(s) from file or URL |
|
|
72
|
-
| `openzca msg video <threadId> [file]` | Send video(s) from file or URL |
|
|
72
|
+
| `openzca msg video <threadId> [file]` | Send video(s) from file or URL; single `.mp4` inputs try native video mode |
|
|
73
73
|
| `openzca msg voice <threadId> [file]` | Send voice message from local file or URL (`.aac`, `.mp3`, `.m4a`, `.wav`, `.ogg`) |
|
|
74
74
|
| `openzca msg sticker <threadId> <stickerId>` | Send a sticker |
|
|
75
75
|
| `openzca msg link <threadId> <url>` | Send a link |
|
|
@@ -83,6 +83,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
|
|
|
83
83
|
| `openzca msg recent <threadId>` | List recent messages (`-n`, `--json`, newest-first); group mode prefers direct group-history endpoint (websocket fallback) |
|
|
84
84
|
|
|
85
85
|
Media commands accept local files, `file://` paths, and repeatable `--url` options. Add `--group` for group threads.
|
|
86
|
+
`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.
|
|
86
87
|
Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
|
|
87
88
|
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.
|
|
88
89
|
|
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import { spawn as spawn2 } from "child_process";
|
|
6
6
|
import fsSync from "fs";
|
|
7
|
-
import
|
|
7
|
+
import fs5 from "fs/promises";
|
|
8
8
|
import net from "net";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
9
|
+
import os4 from "os";
|
|
10
|
+
import path5 from "path";
|
|
11
11
|
import util from "util";
|
|
12
12
|
import { Command } from "commander";
|
|
13
13
|
import {
|
|
@@ -1014,6 +1014,187 @@ async function resolveGroupMentionsIfNeeded(params, text) {
|
|
|
1014
1014
|
return mentions.length > 0 ? mentions : void 0;
|
|
1015
1015
|
}
|
|
1016
1016
|
|
|
1017
|
+
// src/lib/video-send.ts
|
|
1018
|
+
import { execFile } from "child_process";
|
|
1019
|
+
import fs4 from "fs/promises";
|
|
1020
|
+
import os3 from "os";
|
|
1021
|
+
import path4 from "path";
|
|
1022
|
+
import { promisify } from "util";
|
|
1023
|
+
var execFileAsync = promisify(execFile);
|
|
1024
|
+
function planVideoSendMode(params) {
|
|
1025
|
+
const { files, ffmpegAvailable } = params;
|
|
1026
|
+
if (!ffmpegAvailable) {
|
|
1027
|
+
return {
|
|
1028
|
+
mode: "attachment",
|
|
1029
|
+
reason: "ffmpeg is unavailable for native video mode"
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
if (files.length !== 1) {
|
|
1033
|
+
return {
|
|
1034
|
+
mode: "attachment",
|
|
1035
|
+
reason: "native-video mode supports one video at a time"
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
const ext = path4.extname(files[0] ?? "").toLowerCase();
|
|
1039
|
+
if (ext !== ".mp4") {
|
|
1040
|
+
return {
|
|
1041
|
+
mode: "attachment",
|
|
1042
|
+
reason: "native-video mode currently supports .mp4 inputs only"
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
return { mode: "native" };
|
|
1046
|
+
}
|
|
1047
|
+
function parseVideoProbeOutput(raw) {
|
|
1048
|
+
const parsed = JSON.parse(raw);
|
|
1049
|
+
const videoStream = parsed.streams?.find((stream) => stream.codec_type === "video");
|
|
1050
|
+
const width = toPositiveInteger(videoStream?.width);
|
|
1051
|
+
const height = toPositiveInteger(videoStream?.height);
|
|
1052
|
+
const durationSeconds = toPositiveNumber(videoStream?.duration) ?? toPositiveNumber(parsed.format?.duration);
|
|
1053
|
+
return {
|
|
1054
|
+
durationMs: durationSeconds ? Math.round(durationSeconds * 1e3) : void 0,
|
|
1055
|
+
width,
|
|
1056
|
+
height
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
function toPositiveInteger(value) {
|
|
1060
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
1061
|
+
return Math.trunc(value);
|
|
1062
|
+
}
|
|
1063
|
+
if (typeof value === "string") {
|
|
1064
|
+
const numeric = Number(value);
|
|
1065
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
1066
|
+
return Math.trunc(numeric);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
return void 0;
|
|
1070
|
+
}
|
|
1071
|
+
function toPositiveNumber(value) {
|
|
1072
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
1073
|
+
return value;
|
|
1074
|
+
}
|
|
1075
|
+
if (typeof value === "string") {
|
|
1076
|
+
const numeric = Number(value);
|
|
1077
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
1078
|
+
return numeric;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return void 0;
|
|
1082
|
+
}
|
|
1083
|
+
async function runBinary(command, args) {
|
|
1084
|
+
try {
|
|
1085
|
+
const { stdout } = await execFileAsync(command, args, {
|
|
1086
|
+
encoding: "utf8",
|
|
1087
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1088
|
+
});
|
|
1089
|
+
return stdout;
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
1092
|
+
throw new Error(`${command} is required for native video mode`);
|
|
1093
|
+
}
|
|
1094
|
+
if (error instanceof Error && "stderr" in error) {
|
|
1095
|
+
const stderr = typeof error.stderr === "string" ? error.stderr.trim() : "";
|
|
1096
|
+
throw new Error(stderr ? `${command} failed: ${stderr}` : `${command} failed: ${error.message}`);
|
|
1097
|
+
}
|
|
1098
|
+
throw error;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
async function isFfmpegAvailable() {
|
|
1102
|
+
try {
|
|
1103
|
+
await execFileAsync("ffmpeg", ["-version"], {
|
|
1104
|
+
encoding: "utf8",
|
|
1105
|
+
maxBuffer: 1024 * 1024
|
|
1106
|
+
});
|
|
1107
|
+
return true;
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
return false;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async function maybeProbeVideoFile(filePath) {
|
|
1116
|
+
try {
|
|
1117
|
+
const raw = await runBinary("ffprobe", [
|
|
1118
|
+
"-v",
|
|
1119
|
+
"error",
|
|
1120
|
+
"-print_format",
|
|
1121
|
+
"json",
|
|
1122
|
+
"-show_format",
|
|
1123
|
+
"-show_streams",
|
|
1124
|
+
filePath
|
|
1125
|
+
]);
|
|
1126
|
+
return parseVideoProbeOutput(raw);
|
|
1127
|
+
} catch {
|
|
1128
|
+
return {};
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
async function generateVideoThumbnail(videoPath) {
|
|
1132
|
+
const dir = await fs4.mkdtemp(path4.join(os3.tmpdir(), "openzca-video-thumb-"));
|
|
1133
|
+
const outputPath = path4.join(dir, "thumbnail.jpg");
|
|
1134
|
+
try {
|
|
1135
|
+
await runBinary("ffmpeg", [
|
|
1136
|
+
"-y",
|
|
1137
|
+
"-ss",
|
|
1138
|
+
"1",
|
|
1139
|
+
"-i",
|
|
1140
|
+
videoPath,
|
|
1141
|
+
"-frames:v",
|
|
1142
|
+
"1",
|
|
1143
|
+
"-q:v",
|
|
1144
|
+
"2",
|
|
1145
|
+
outputPath
|
|
1146
|
+
]);
|
|
1147
|
+
await fs4.access(outputPath);
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
await fs4.rm(dir, { recursive: true, force: true });
|
|
1150
|
+
throw error;
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
path: outputPath,
|
|
1154
|
+
cleanup: async () => {
|
|
1155
|
+
await fs4.rm(dir, { recursive: true, force: true });
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function pickUploadedVideoUrl(uploaded) {
|
|
1160
|
+
if (!uploaded || !("fileUrl" in uploaded) || typeof uploaded.fileUrl !== "string" || uploaded.fileUrl.length === 0) {
|
|
1161
|
+
throw new Error("Video upload did not return a file URL");
|
|
1162
|
+
}
|
|
1163
|
+
return uploaded.fileUrl;
|
|
1164
|
+
}
|
|
1165
|
+
function pickUploadedThumbnailUrl(uploaded) {
|
|
1166
|
+
if (!uploaded || uploaded.fileType !== "image") {
|
|
1167
|
+
throw new Error("Thumbnail upload did not return an image result");
|
|
1168
|
+
}
|
|
1169
|
+
return uploaded.normalUrl || uploaded.hdUrl || uploaded.thumbUrl;
|
|
1170
|
+
}
|
|
1171
|
+
async function sendNativeVideo(params) {
|
|
1172
|
+
const metadata = await maybeProbeVideoFile(params.videoPath);
|
|
1173
|
+
const generatedThumbnail = params.thumbnailPath ? null : await generateVideoThumbnail(params.videoPath);
|
|
1174
|
+
const thumbnailPath = params.thumbnailPath ?? generatedThumbnail?.path;
|
|
1175
|
+
if (!thumbnailPath) {
|
|
1176
|
+
throw new Error("Unable to resolve thumbnail path for native video send");
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
const uploadedVideo = await params.api.uploadAttachment([params.videoPath], params.threadId, params.threadType);
|
|
1180
|
+
const uploadedThumbnail = await params.api.uploadAttachment([thumbnailPath], params.threadId, params.threadType);
|
|
1181
|
+
return await params.api.sendVideo(
|
|
1182
|
+
{
|
|
1183
|
+
msg: params.message ?? "",
|
|
1184
|
+
videoUrl: pickUploadedVideoUrl(uploadedVideo[0]),
|
|
1185
|
+
thumbnailUrl: pickUploadedThumbnailUrl(uploadedThumbnail[0]),
|
|
1186
|
+
duration: metadata.durationMs,
|
|
1187
|
+
width: metadata.width,
|
|
1188
|
+
height: metadata.height
|
|
1189
|
+
},
|
|
1190
|
+
params.threadId,
|
|
1191
|
+
params.threadType
|
|
1192
|
+
);
|
|
1193
|
+
} finally {
|
|
1194
|
+
await generatedThumbnail?.cleanup();
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1017
1198
|
// src/cli.ts
|
|
1018
1199
|
var require2 = createRequire(import.meta.url);
|
|
1019
1200
|
var { version: PKG_VERSION } = require2("../package.json");
|
|
@@ -1076,9 +1257,9 @@ function resolveDebugEnabled(command) {
|
|
|
1076
1257
|
}
|
|
1077
1258
|
function resolveDebugFilePath(command) {
|
|
1078
1259
|
const options = getDebugOptions(command);
|
|
1079
|
-
const configured = options.debugFile?.trim() || process.env.OPENZCA_DEBUG_FILE?.trim() ||
|
|
1260
|
+
const configured = options.debugFile?.trim() || process.env.OPENZCA_DEBUG_FILE?.trim() || path5.join(APP_HOME, "logs", "openzca-debug.log");
|
|
1080
1261
|
const normalized = normalizeMediaInput(configured);
|
|
1081
|
-
return
|
|
1262
|
+
return path5.isAbsolute(normalized) ? normalized : path5.resolve(process.cwd(), normalized);
|
|
1082
1263
|
}
|
|
1083
1264
|
function writeDebugLine(event, details, command) {
|
|
1084
1265
|
if (!resolveDebugEnabled(command)) {
|
|
@@ -1089,7 +1270,7 @@ function writeDebugLine(event, details, command) {
|
|
|
1089
1270
|
`;
|
|
1090
1271
|
const filePath = resolveDebugFilePath(command);
|
|
1091
1272
|
try {
|
|
1092
|
-
fsSync.mkdirSync(
|
|
1273
|
+
fsSync.mkdirSync(path5.dirname(filePath), { recursive: true });
|
|
1093
1274
|
fsSync.appendFileSync(filePath, line, "utf8");
|
|
1094
1275
|
} catch {
|
|
1095
1276
|
}
|
|
@@ -1177,14 +1358,14 @@ function collectIdsFromCacheEntries(entries, keys) {
|
|
|
1177
1358
|
return ids;
|
|
1178
1359
|
}
|
|
1179
1360
|
function getListenerOwnerLockPath(profile) {
|
|
1180
|
-
return
|
|
1361
|
+
return path5.join(getProfileDir(profile), "listener-owner.json");
|
|
1181
1362
|
}
|
|
1182
1363
|
function getListenIpcSocketPath(profile) {
|
|
1183
1364
|
if (process.platform === "win32") {
|
|
1184
1365
|
const safe = profile.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
1185
1366
|
return `\\\\.\\pipe\\openzca-listen-${safe}`;
|
|
1186
1367
|
}
|
|
1187
|
-
return
|
|
1368
|
+
return path5.join(getProfileDir(profile), "listen.sock");
|
|
1188
1369
|
}
|
|
1189
1370
|
function parsePositiveIntFromUnknown(value) {
|
|
1190
1371
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
@@ -1211,7 +1392,7 @@ function isProcessAlive(pid) {
|
|
|
1211
1392
|
}
|
|
1212
1393
|
async function readListenerOwnerRecord(lockPath) {
|
|
1213
1394
|
try {
|
|
1214
|
-
const raw = await
|
|
1395
|
+
const raw = await fs5.readFile(lockPath, "utf8");
|
|
1215
1396
|
const parsed = JSON.parse(raw);
|
|
1216
1397
|
const pid = parsePositiveIntFromUnknown(parsed.pid);
|
|
1217
1398
|
if (!pid) return null;
|
|
@@ -1231,11 +1412,11 @@ async function readActiveListenerOwner(profile) {
|
|
|
1231
1412
|
const lockPath = getListenerOwnerLockPath(profile);
|
|
1232
1413
|
const record = await readListenerOwnerRecord(lockPath);
|
|
1233
1414
|
if (!record) {
|
|
1234
|
-
await
|
|
1415
|
+
await fs5.rm(lockPath, { force: true });
|
|
1235
1416
|
return null;
|
|
1236
1417
|
}
|
|
1237
1418
|
if (!isProcessAlive(record.pid)) {
|
|
1238
|
-
await
|
|
1419
|
+
await fs5.rm(lockPath, { force: true });
|
|
1239
1420
|
return null;
|
|
1240
1421
|
}
|
|
1241
1422
|
return record;
|
|
@@ -1251,7 +1432,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
1251
1432
|
};
|
|
1252
1433
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1253
1434
|
try {
|
|
1254
|
-
await
|
|
1435
|
+
await fs5.writeFile(lockPath, `${JSON.stringify(record, null, 2)}
|
|
1255
1436
|
`, {
|
|
1256
1437
|
encoding: "utf8",
|
|
1257
1438
|
flag: "wx"
|
|
@@ -1264,7 +1445,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
1264
1445
|
released = true;
|
|
1265
1446
|
const current = await readListenerOwnerRecord(lockPath);
|
|
1266
1447
|
if (current && current.pid !== process.pid) return;
|
|
1267
|
-
await
|
|
1448
|
+
await fs5.rm(lockPath, { force: true });
|
|
1268
1449
|
writeDebugLine(
|
|
1269
1450
|
"listen.owner.released",
|
|
1270
1451
|
{
|
|
@@ -1285,7 +1466,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
1285
1466
|
`Another openzca listener already owns profile "${profile}" (pid ${owner.pid}).`
|
|
1286
1467
|
);
|
|
1287
1468
|
}
|
|
1288
|
-
await
|
|
1469
|
+
await fs5.rm(lockPath, { force: true });
|
|
1289
1470
|
}
|
|
1290
1471
|
}
|
|
1291
1472
|
throw new Error(`Unable to acquire listener ownership for profile "${profile}".`);
|
|
@@ -1303,7 +1484,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
1303
1484
|
}
|
|
1304
1485
|
const socketPath = getListenIpcSocketPath(profile);
|
|
1305
1486
|
if (process.platform !== "win32") {
|
|
1306
|
-
await
|
|
1487
|
+
await fs5.rm(socketPath, { force: true });
|
|
1307
1488
|
}
|
|
1308
1489
|
const uploadTimeoutMs = parsePositiveIntFromEnv(
|
|
1309
1490
|
"OPENZCA_UPLOAD_IPC_HANDLER_TIMEOUT_MS",
|
|
@@ -1475,7 +1656,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
1475
1656
|
server.close(() => resolve());
|
|
1476
1657
|
});
|
|
1477
1658
|
if (process.platform !== "win32") {
|
|
1478
|
-
await
|
|
1659
|
+
await fs5.rm(socketPath, { force: true });
|
|
1479
1660
|
}
|
|
1480
1661
|
writeDebugLine(
|
|
1481
1662
|
"listen.ipc.stopped",
|
|
@@ -2418,7 +2599,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
2418
2599
|
});
|
|
2419
2600
|
}
|
|
2420
2601
|
async function parseCredentialFile(filePath) {
|
|
2421
|
-
const raw = await
|
|
2602
|
+
const raw = await fs5.readFile(filePath, "utf8");
|
|
2422
2603
|
const parsed = JSON.parse(raw);
|
|
2423
2604
|
if (!parsed.imei || !parsed.cookie || !parsed.userAgent) {
|
|
2424
2605
|
throw new Error("Credential file must include imei, cookie, and userAgent.");
|
|
@@ -2439,7 +2620,7 @@ async function waitForFileContent(filePath, timeoutMs) {
|
|
|
2439
2620
|
const startedAt = Date.now();
|
|
2440
2621
|
while (Date.now() - startedAt < timeoutMs) {
|
|
2441
2622
|
try {
|
|
2442
|
-
const data = await
|
|
2623
|
+
const data = await fs5.readFile(filePath);
|
|
2443
2624
|
if (data.length > 0) {
|
|
2444
2625
|
return data;
|
|
2445
2626
|
}
|
|
@@ -2454,8 +2635,8 @@ async function emitQrBase64FromDetachedLogin(profile, qrPath) {
|
|
|
2454
2635
|
if (!scriptPath) {
|
|
2455
2636
|
throw new Error("Cannot resolve CLI entrypoint for QR base64 mode.");
|
|
2456
2637
|
}
|
|
2457
|
-
const tempDir = await
|
|
2458
|
-
const targetPath =
|
|
2638
|
+
const tempDir = await fs5.mkdtemp(path5.join(os4.tmpdir(), "openzca-qr-"));
|
|
2639
|
+
const targetPath = path5.resolve(qrPath ?? path5.join(tempDir, "qr.png"));
|
|
2459
2640
|
const child = spawn2(
|
|
2460
2641
|
process.execPath,
|
|
2461
2642
|
[scriptPath, "--profile", profile, "auth", "login", "--qr-path", targetPath],
|
|
@@ -2733,7 +2914,7 @@ function mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind) {
|
|
|
2733
2914
|
if (fromType) return fromType;
|
|
2734
2915
|
try {
|
|
2735
2916
|
const parsedUrl = new URL(mediaUrl);
|
|
2736
|
-
const ext =
|
|
2917
|
+
const ext = path5.extname(parsedUrl.pathname);
|
|
2737
2918
|
if (ext) return ext;
|
|
2738
2919
|
} catch {
|
|
2739
2920
|
}
|
|
@@ -2764,20 +2945,20 @@ function parseInboundMediaFetchTimeoutMs() {
|
|
|
2764
2945
|
return Math.trunc(parsed);
|
|
2765
2946
|
}
|
|
2766
2947
|
function resolveOpenClawMediaDir() {
|
|
2767
|
-
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() ||
|
|
2768
|
-
return
|
|
2948
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path5.join(os4.homedir(), ".openclaw");
|
|
2949
|
+
return path5.join(stateDir, "media");
|
|
2769
2950
|
}
|
|
2770
2951
|
function resolveInboundMediaDir(profile) {
|
|
2771
2952
|
const configuredRaw = process.env.OPENZCA_LISTEN_MEDIA_DIR?.trim();
|
|
2772
2953
|
if (configuredRaw) {
|
|
2773
2954
|
const configured = normalizeMediaInput(configuredRaw);
|
|
2774
|
-
return
|
|
2955
|
+
return path5.isAbsolute(configured) ? configured : path5.resolve(process.cwd(), configured);
|
|
2775
2956
|
}
|
|
2776
2957
|
const legacyRequested = process.env.OPENZCA_LISTEN_MEDIA_LEGACY_DIR?.trim() === "1";
|
|
2777
2958
|
if (legacyRequested) {
|
|
2778
|
-
return
|
|
2959
|
+
return path5.join(getProfileDir(profile), "inbound-media");
|
|
2779
2960
|
}
|
|
2780
|
-
return
|
|
2961
|
+
return path5.join(resolveOpenClawMediaDir(), "openzca", profile, "inbound");
|
|
2781
2962
|
}
|
|
2782
2963
|
async function cacheInboundMediaToProfile(profile, mediaUrl, kind) {
|
|
2783
2964
|
const maxBytes = parseMaxInboundMediaBytes();
|
|
@@ -2811,11 +2992,11 @@ async function cacheInboundMediaToProfile(profile, mediaUrl, kind) {
|
|
|
2811
2992
|
return null;
|
|
2812
2993
|
}
|
|
2813
2994
|
const dir = resolveInboundMediaDir(profile);
|
|
2814
|
-
await
|
|
2995
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
2815
2996
|
const ext = mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind);
|
|
2816
2997
|
const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
2817
|
-
const mediaPath =
|
|
2818
|
-
await
|
|
2998
|
+
const mediaPath = path5.join(dir, `${id}${ext}`);
|
|
2999
|
+
await fs5.writeFile(mediaPath, data);
|
|
2819
3000
|
return { mediaPath, mediaType };
|
|
2820
3001
|
}
|
|
2821
3002
|
async function cacheRemoteMediaEntries(params) {
|
|
@@ -3307,7 +3488,7 @@ auth.command("login").description("Login with QR code").option("-q, --qr-path <p
|
|
|
3307
3488
|
auth.command("login-cred [file]").alias("login-creds").description("Login using credential JSON file").action(
|
|
3308
3489
|
wrapAction(async (file, command) => {
|
|
3309
3490
|
const profile = await currentProfile(command);
|
|
3310
|
-
const credentials = file ? await parseCredentialFile(
|
|
3491
|
+
const credentials = file ? await parseCredentialFile(path5.resolve(normalizeMediaInput(file))) : toCredentials(
|
|
3311
3492
|
await loadCredentials(profile) ?? (() => {
|
|
3312
3493
|
throw new Error(
|
|
3313
3494
|
`No saved credentials for profile "${profile}". Run: openzca auth login`
|
|
@@ -3436,42 +3617,121 @@ msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (rep
|
|
|
3436
3617
|
}
|
|
3437
3618
|
)
|
|
3438
3619
|
);
|
|
3439
|
-
msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (repeatable)", collectValues, []).option("-m, --message <message>", "Caption").option("--thumbnail <
|
|
3620
|
+
msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (repeatable)", collectValues, []).option("-m, --message <message>", "Caption").option("--thumbnail <pathOrUrl>", "Thumbnail image path or URL (optional)").option("-g, --group", "Send to group").description("Send video(s) from file or URL").action(
|
|
3440
3621
|
wrapAction(
|
|
3441
3622
|
async (threadId, file, opts, command) => {
|
|
3442
|
-
const { api } = await requireApi(command);
|
|
3623
|
+
const { api, profile } = await requireApi(command);
|
|
3624
|
+
const threadType = asThreadType(opts.group);
|
|
3443
3625
|
const normalizedFile = file ? normalizeMediaInput(file) : void 0;
|
|
3444
3626
|
const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
|
|
3445
3627
|
const urlInputs = files.filter((entry) => isHttpUrl(entry));
|
|
3446
3628
|
const localInputs = files.filter((entry) => !isHttpUrl(entry));
|
|
3629
|
+
const normalizedThumbnail = opts.thumbnail ? normalizeMediaInput(opts.thumbnail) : void 0;
|
|
3630
|
+
const thumbnailUrlInputs = normalizedThumbnail && isHttpUrl(normalizedThumbnail) ? [normalizedThumbnail] : [];
|
|
3631
|
+
const thumbnailLocalPath = normalizedThumbnail && !isHttpUrl(normalizedThumbnail) ? normalizedThumbnail : void 0;
|
|
3447
3632
|
writeDebugLine(
|
|
3448
3633
|
"msg.video.inputs",
|
|
3449
3634
|
{
|
|
3450
3635
|
threadId,
|
|
3451
3636
|
isGroup: Boolean(opts.group),
|
|
3452
3637
|
localInputs,
|
|
3453
|
-
urlInputs
|
|
3638
|
+
urlInputs,
|
|
3639
|
+
thumbnail: normalizedThumbnail
|
|
3454
3640
|
},
|
|
3455
3641
|
command
|
|
3456
3642
|
);
|
|
3457
3643
|
const downloaded = await downloadUrlsToTempFiles(urlInputs);
|
|
3644
|
+
const downloadedThumbnail = await downloadUrlsToTempFiles(thumbnailUrlInputs);
|
|
3458
3645
|
try {
|
|
3459
3646
|
const attachments = [...localInputs, ...downloaded.files];
|
|
3460
3647
|
if (attachments.length === 0) {
|
|
3461
3648
|
throw new Error("Provide at least one video file or --url.");
|
|
3462
3649
|
}
|
|
3463
3650
|
await assertFilesExist(attachments);
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3651
|
+
if (thumbnailLocalPath) {
|
|
3652
|
+
await assertFilesExist([thumbnailLocalPath]);
|
|
3653
|
+
}
|
|
3654
|
+
const enforceSingleOwner = parseBooleanFromEnv("OPENZCA_UPLOAD_ENFORCE_SINGLE_OWNER", true);
|
|
3655
|
+
if (enforceSingleOwner) {
|
|
3656
|
+
const owner = await readActiveListenerOwner(profile);
|
|
3657
|
+
if (owner && owner.pid !== process.pid) {
|
|
3658
|
+
throw new Error(
|
|
3659
|
+
`Active listener owner detected for profile "${profile}" (pid ${owner.pid}), but video upload IPC is unavailable. Restart \`openzca listen\` with latest version or set OPENZCA_UPLOAD_ENFORCE_SINGLE_OWNER=0 to allow fallback listener startup.`
|
|
3660
|
+
);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
const ffmpegAvailable = await isFfmpegAvailable();
|
|
3664
|
+
const videoPlan = planVideoSendMode({
|
|
3665
|
+
files: attachments,
|
|
3666
|
+
ffmpegAvailable
|
|
3667
|
+
});
|
|
3668
|
+
if (videoPlan.mode === "native") {
|
|
3669
|
+
const thumbnailPath = thumbnailLocalPath ?? downloadedThumbnail.files[0];
|
|
3670
|
+
try {
|
|
3671
|
+
const response2 = await withUploadListener(
|
|
3672
|
+
api,
|
|
3673
|
+
command,
|
|
3674
|
+
async () => sendNativeVideo({
|
|
3675
|
+
api,
|
|
3676
|
+
threadId,
|
|
3677
|
+
threadType,
|
|
3678
|
+
videoPath: attachments[0],
|
|
3679
|
+
message: opts.message,
|
|
3680
|
+
thumbnailPath
|
|
3681
|
+
})
|
|
3682
|
+
);
|
|
3683
|
+
writeDebugLine(
|
|
3684
|
+
"msg.video.native.success",
|
|
3685
|
+
{
|
|
3686
|
+
threadId,
|
|
3687
|
+
isGroup: Boolean(opts.group),
|
|
3688
|
+
videoPath: attachments[0],
|
|
3689
|
+
thumbnailPath: thumbnailPath ?? null
|
|
3690
|
+
},
|
|
3691
|
+
command
|
|
3692
|
+
);
|
|
3693
|
+
output(response2, false);
|
|
3694
|
+
return;
|
|
3695
|
+
} catch (error) {
|
|
3696
|
+
writeDebugLine(
|
|
3697
|
+
"msg.video.native.failed",
|
|
3698
|
+
{
|
|
3699
|
+
threadId,
|
|
3700
|
+
isGroup: Boolean(opts.group),
|
|
3701
|
+
videoPath: attachments[0],
|
|
3702
|
+
thumbnailPath: thumbnailPath ?? null,
|
|
3703
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3704
|
+
},
|
|
3705
|
+
command
|
|
3706
|
+
);
|
|
3707
|
+
}
|
|
3708
|
+
} else {
|
|
3709
|
+
writeDebugLine(
|
|
3710
|
+
"msg.video.native.skipped",
|
|
3711
|
+
{
|
|
3712
|
+
threadId,
|
|
3713
|
+
isGroup: Boolean(opts.group),
|
|
3714
|
+
reason: videoPlan.reason
|
|
3715
|
+
},
|
|
3716
|
+
command
|
|
3717
|
+
);
|
|
3718
|
+
}
|
|
3719
|
+
const response = await withUploadListener(
|
|
3720
|
+
api,
|
|
3721
|
+
command,
|
|
3722
|
+
async () => api.sendMessage(
|
|
3723
|
+
{
|
|
3724
|
+
msg: opts.message ?? "",
|
|
3725
|
+
attachments
|
|
3726
|
+
},
|
|
3727
|
+
threadId,
|
|
3728
|
+
threadType
|
|
3729
|
+
)
|
|
3471
3730
|
);
|
|
3472
3731
|
output(response, false);
|
|
3473
3732
|
} finally {
|
|
3474
3733
|
await downloaded.cleanup();
|
|
3734
|
+
await downloadedThumbnail.cleanup();
|
|
3475
3735
|
}
|
|
3476
3736
|
}
|
|
3477
3737
|
)
|