openzca 0.1.9 → 0.1.11
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 +37 -2
- package/dist/cli.js +364 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
|
|
|
71
71
|
| `openzca msg send <threadId> <message>` | Send text message |
|
|
72
72
|
| `openzca msg image <threadId> [file]` | Send image(s) from file or URL |
|
|
73
73
|
| `openzca msg video <threadId> [file]` | Send video(s) from file or URL |
|
|
74
|
-
| `openzca msg voice <threadId> [file]` | Send voice message from local file or URL |
|
|
74
|
+
| `openzca msg voice <threadId> [file]` | Send voice message from local file or URL (`.aac`, `.mp3`, `.m4a`, `.wav`, `.ogg`) |
|
|
75
75
|
| `openzca msg sticker <threadId> <stickerId>` | Send a sticker |
|
|
76
76
|
| `openzca msg link <threadId> <url>` | Send a link |
|
|
77
77
|
| `openzca msg card <threadId> <contactId>` | Send a contact card |
|
|
@@ -84,6 +84,39 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
|
|
|
84
84
|
| `openzca msg recent <threadId>` | List recent messages (`-n`, `--json`) |
|
|
85
85
|
|
|
86
86
|
Media commands accept local files, `file://` paths, and repeatable `--url` options. Add `--group` for group threads.
|
|
87
|
+
Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
|
|
88
|
+
|
|
89
|
+
### Debug Logging
|
|
90
|
+
|
|
91
|
+
Use debug mode to write copyable logs for support/debugging:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# One-off debug run
|
|
95
|
+
openzca --debug msg image <threadId> ~/Desktop/screenshot.png
|
|
96
|
+
|
|
97
|
+
# Custom debug log path
|
|
98
|
+
openzca --debug --debug-file ~/Desktop/openzca-debug.log msg image <threadId> ~/Desktop/screenshot.png
|
|
99
|
+
|
|
100
|
+
# Or enable by environment
|
|
101
|
+
OPENZCA_DEBUG=1 openzca listen --raw
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Default debug log file:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
~/.openzca/logs/openzca-debug.log
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Useful command to copy recent debug logs:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
tail -n 200 ~/.openzca/logs/openzca-debug.log
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
For media debugging, grep these events in the debug log:
|
|
117
|
+
|
|
118
|
+
- `listen.media.detected`
|
|
119
|
+
- `listen.media.cache_error`
|
|
87
120
|
|
|
88
121
|
### group — Group management
|
|
89
122
|
|
|
@@ -160,13 +193,15 @@ Media commands accept local files, `file://` paths, and repeatable `--url` optio
|
|
|
160
193
|
| `openzca listen --raw` | Output raw JSON per line |
|
|
161
194
|
| `openzca listen --keep-alive` | Auto-reconnect on disconnect |
|
|
162
195
|
|
|
163
|
-
`listen --raw`
|
|
196
|
+
`listen --raw` includes inbound media metadata when available:
|
|
164
197
|
|
|
165
198
|
- `mediaPath`, `mediaPaths`
|
|
166
199
|
- `mediaUrl`, `mediaUrls`
|
|
167
200
|
- `mediaType`, `mediaTypes`
|
|
168
201
|
- `mediaKind`
|
|
169
202
|
|
|
203
|
+
`listen` also normalizes JSON-string message payloads (common for `chat.voice` and `share.file`) so media URLs are extracted/cached instead of being forwarded as raw JSON text.
|
|
204
|
+
|
|
170
205
|
For non-text inbound messages (image/video/audio/file), `content` is emitted as a media note:
|
|
171
206
|
|
|
172
207
|
```text
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import { spawn as spawn2 } from "child_process";
|
|
6
|
+
import fsSync from "fs";
|
|
6
7
|
import fs4 from "fs/promises";
|
|
7
8
|
import os3 from "os";
|
|
8
9
|
import path4 from "path";
|
|
@@ -456,31 +457,65 @@ import path3 from "path";
|
|
|
456
457
|
import { fileURLToPath } from "url";
|
|
457
458
|
var CONTENT_TYPE_EXT = {
|
|
458
459
|
"image/jpeg": ".jpg",
|
|
460
|
+
"image/jpg": ".jpg",
|
|
459
461
|
"image/png": ".png",
|
|
460
462
|
"image/webp": ".webp",
|
|
461
463
|
"image/gif": ".gif",
|
|
464
|
+
"image/heic": ".heic",
|
|
465
|
+
"image/heif": ".heif",
|
|
462
466
|
"video/mp4": ".mp4",
|
|
467
|
+
"video/quicktime": ".mov",
|
|
468
|
+
"video/webm": ".webm",
|
|
463
469
|
"audio/mpeg": ".mp3",
|
|
464
470
|
"audio/mp3": ".mp3",
|
|
471
|
+
"audio/aac": ".aac",
|
|
472
|
+
"audio/x-aac": ".aac",
|
|
465
473
|
"audio/mp4": ".m4a",
|
|
466
474
|
"audio/x-m4a": ".m4a",
|
|
467
|
-
"audio/wav": ".wav"
|
|
475
|
+
"audio/wav": ".wav",
|
|
476
|
+
"audio/ogg": ".ogg",
|
|
477
|
+
"audio/webm": ".webm",
|
|
478
|
+
"application/pdf": ".pdf",
|
|
479
|
+
"application/msword": ".doc",
|
|
480
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
|
481
|
+
"application/vnd.ms-excel": ".xls",
|
|
482
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
483
|
+
"application/vnd.ms-excel.sheet.binary.macroenabled.12": ".xlsb",
|
|
484
|
+
"application/vnd.ms-excel.sheet.macroenabled.12": ".xlsm",
|
|
485
|
+
"application/vnd.ms-powerpoint": ".ppt",
|
|
486
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
|
487
|
+
"application/json": ".json",
|
|
488
|
+
"application/zip": ".zip",
|
|
489
|
+
"application/gzip": ".gz",
|
|
490
|
+
"text/plain": ".txt",
|
|
491
|
+
"text/csv": ".csv",
|
|
492
|
+
"text/tab-separated-values": ".tsv",
|
|
493
|
+
"text/markdown": ".md"
|
|
468
494
|
};
|
|
469
495
|
function collectValues(value, previous) {
|
|
470
496
|
previous.push(value);
|
|
471
497
|
return previous;
|
|
472
498
|
}
|
|
499
|
+
function expandLeadingTilde(value) {
|
|
500
|
+
if (value === "~") {
|
|
501
|
+
return os2.homedir();
|
|
502
|
+
}
|
|
503
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
504
|
+
return path3.join(os2.homedir(), value.slice(2));
|
|
505
|
+
}
|
|
506
|
+
return value;
|
|
507
|
+
}
|
|
473
508
|
function normalizeMediaInput(value) {
|
|
474
509
|
const trimmed = value.trim();
|
|
475
510
|
if (!trimmed) return "";
|
|
476
511
|
if (/^file:\/\//i.test(trimmed)) {
|
|
477
512
|
try {
|
|
478
|
-
return fileURLToPath(trimmed);
|
|
513
|
+
return expandLeadingTilde(fileURLToPath(trimmed));
|
|
479
514
|
} catch {
|
|
480
|
-
return trimmed.replace(/^file:\/\//i, "");
|
|
515
|
+
return expandLeadingTilde(trimmed.replace(/^file:\/\//i, ""));
|
|
481
516
|
}
|
|
482
517
|
}
|
|
483
|
-
return trimmed;
|
|
518
|
+
return expandLeadingTilde(trimmed);
|
|
484
519
|
}
|
|
485
520
|
function normalizeInputList(values) {
|
|
486
521
|
if (!values || values.length === 0) return [];
|
|
@@ -556,12 +591,88 @@ var EMOJI_REACTION_MAP = {
|
|
|
556
591
|
"\u{1F62D}": Reactions.CRY,
|
|
557
592
|
"\u{1F621}": Reactions.ANGRY
|
|
558
593
|
};
|
|
594
|
+
var DEBUG_COMMAND_START = /* @__PURE__ */ new WeakMap();
|
|
595
|
+
function parseDebugFlag(value) {
|
|
596
|
+
if (!value) return false;
|
|
597
|
+
const normalized = value.trim().toLowerCase();
|
|
598
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
599
|
+
}
|
|
600
|
+
function getActionCommand(args) {
|
|
601
|
+
for (let index = args.length - 1; index >= 0; index -= 1) {
|
|
602
|
+
const item = args[index];
|
|
603
|
+
if (item instanceof Command) {
|
|
604
|
+
return item;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return void 0;
|
|
608
|
+
}
|
|
609
|
+
function commandPathLabel(command) {
|
|
610
|
+
if (!command) return void 0;
|
|
611
|
+
const names = [];
|
|
612
|
+
let current = command;
|
|
613
|
+
while (current) {
|
|
614
|
+
const name = current.name();
|
|
615
|
+
if (name) {
|
|
616
|
+
names.unshift(name);
|
|
617
|
+
}
|
|
618
|
+
current = current.parent ?? null;
|
|
619
|
+
}
|
|
620
|
+
return names.join(" ");
|
|
621
|
+
}
|
|
622
|
+
function getDebugOptions(command) {
|
|
623
|
+
if (command) {
|
|
624
|
+
if (typeof command.optsWithGlobals === "function") {
|
|
625
|
+
return command.optsWithGlobals();
|
|
626
|
+
}
|
|
627
|
+
return command.opts();
|
|
628
|
+
}
|
|
629
|
+
if (typeof program.optsWithGlobals === "function") {
|
|
630
|
+
return program.optsWithGlobals();
|
|
631
|
+
}
|
|
632
|
+
return program.opts();
|
|
633
|
+
}
|
|
634
|
+
function resolveDebugEnabled(command) {
|
|
635
|
+
if (parseDebugFlag(process.env.OPENZCA_DEBUG)) {
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
638
|
+
return Boolean(getDebugOptions(command).debug);
|
|
639
|
+
}
|
|
640
|
+
function resolveDebugFilePath(command) {
|
|
641
|
+
const options = getDebugOptions(command);
|
|
642
|
+
const configured = options.debugFile?.trim() || process.env.OPENZCA_DEBUG_FILE?.trim() || path4.join(APP_HOME, "logs", "openzca-debug.log");
|
|
643
|
+
const normalized = normalizeMediaInput(configured);
|
|
644
|
+
return path4.isAbsolute(normalized) ? normalized : path4.resolve(process.cwd(), normalized);
|
|
645
|
+
}
|
|
646
|
+
function writeDebugLine(event, details, command) {
|
|
647
|
+
if (!resolveDebugEnabled(command)) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const payload = details ? JSON.stringify(details) : "";
|
|
651
|
+
const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${event}${payload ? ` ${payload}` : ""}
|
|
652
|
+
`;
|
|
653
|
+
const filePath = resolveDebugFilePath(command);
|
|
654
|
+
try {
|
|
655
|
+
fsSync.mkdirSync(path4.dirname(filePath), { recursive: true });
|
|
656
|
+
fsSync.appendFileSync(filePath, line, "utf8");
|
|
657
|
+
} catch {
|
|
658
|
+
}
|
|
659
|
+
}
|
|
559
660
|
function wrapAction(handler) {
|
|
560
661
|
return async (...args) => {
|
|
662
|
+
const command = getActionCommand(args);
|
|
561
663
|
try {
|
|
562
664
|
await handler(...args);
|
|
563
665
|
} catch (error) {
|
|
564
666
|
const message = error instanceof Error ? error.message : String(error);
|
|
667
|
+
writeDebugLine(
|
|
668
|
+
"command.error",
|
|
669
|
+
{
|
|
670
|
+
command: commandPathLabel(command),
|
|
671
|
+
message,
|
|
672
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
673
|
+
},
|
|
674
|
+
command
|
|
675
|
+
);
|
|
565
676
|
console.error(`Error: ${message}`);
|
|
566
677
|
process.exitCode = 1;
|
|
567
678
|
}
|
|
@@ -835,6 +946,44 @@ function normalizeMessageType(value) {
|
|
|
835
946
|
if (typeof value !== "string") return "";
|
|
836
947
|
return value.trim().toLowerCase();
|
|
837
948
|
}
|
|
949
|
+
function looksLikeStructuredJsonString(value) {
|
|
950
|
+
const trimmed = value.trim();
|
|
951
|
+
if (trimmed.length < 2) return false;
|
|
952
|
+
const first = trimmed[0];
|
|
953
|
+
const last = trimmed[trimmed.length - 1];
|
|
954
|
+
if (first === "{" && last === "}") return true;
|
|
955
|
+
if (first === "[" && last === "]") return true;
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
function normalizeStructuredContent(value, depth = 0) {
|
|
959
|
+
if (depth > 5 || value === null || value === void 0) {
|
|
960
|
+
return value;
|
|
961
|
+
}
|
|
962
|
+
if (typeof value === "string") {
|
|
963
|
+
const trimmed = value.trim();
|
|
964
|
+
if (!looksLikeStructuredJsonString(trimmed)) {
|
|
965
|
+
return value;
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const parsed = JSON.parse(trimmed);
|
|
969
|
+
return normalizeStructuredContent(parsed, depth + 1);
|
|
970
|
+
} catch {
|
|
971
|
+
return value;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (Array.isArray(value)) {
|
|
975
|
+
return value.map((entry) => normalizeStructuredContent(entry, depth + 1));
|
|
976
|
+
}
|
|
977
|
+
const record = asObject(value);
|
|
978
|
+
if (!record) {
|
|
979
|
+
return value;
|
|
980
|
+
}
|
|
981
|
+
const normalized = {};
|
|
982
|
+
for (const [key, nested] of Object.entries(record)) {
|
|
983
|
+
normalized[key] = normalizeStructuredContent(nested, depth + 1);
|
|
984
|
+
}
|
|
985
|
+
return normalized;
|
|
986
|
+
}
|
|
838
987
|
function detectInboundMediaKind(msgType, content) {
|
|
839
988
|
const normalizedType = normalizeMessageType(msgType);
|
|
840
989
|
if (normalizedType.includes("photo") || normalizedType.includes("gif") || normalizedType.includes("sticker")) {
|
|
@@ -846,7 +995,9 @@ function detectInboundMediaKind(msgType, content) {
|
|
|
846
995
|
if (normalizedType.includes("link") || normalizedType.includes("location")) return null;
|
|
847
996
|
const record = asObject(content);
|
|
848
997
|
if (!record) return null;
|
|
849
|
-
if (getStringCandidate(record, ["voiceUrl", "m4aUrl"]))
|
|
998
|
+
if (getStringCandidate(record, ["voiceUrl", "m4aUrl", "audioUrl", "voice_url", "m4a_url", "audio_url"])) {
|
|
999
|
+
return "audio";
|
|
1000
|
+
}
|
|
850
1001
|
if (getStringCandidate(record, ["videoUrl"])) return "video";
|
|
851
1002
|
if (getStringCandidate(record, [
|
|
852
1003
|
"hdUrl",
|
|
@@ -859,15 +1010,22 @@ function detectInboundMediaKind(msgType, content) {
|
|
|
859
1010
|
])) {
|
|
860
1011
|
return "image";
|
|
861
1012
|
}
|
|
862
|
-
if (getStringCandidate(record, ["fileUrl", "fileName", "fileId"])) return "file";
|
|
1013
|
+
if (getStringCandidate(record, ["fileUrl", "fileName", "fileId", "href", "url"])) return "file";
|
|
863
1014
|
return null;
|
|
864
1015
|
}
|
|
865
1016
|
function collectHttpUrls(value, sink, depth = 0) {
|
|
866
1017
|
if (depth > 5 || sink.size >= 16) return;
|
|
867
1018
|
if (typeof value === "string") {
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
1019
|
+
const escapedNormalized = value.replace(/\\\//g, "/");
|
|
1020
|
+
const matches = escapedNormalized.match(/https?:\/\/[^\s"'<>`]+/gi) ?? [];
|
|
1021
|
+
for (const match of matches) {
|
|
1022
|
+
const cleaned = match.replace(/[)\],.;"'`]+$/g, "").trim();
|
|
1023
|
+
if (isHttpUrl(cleaned)) {
|
|
1024
|
+
sink.add(cleaned);
|
|
1025
|
+
}
|
|
1026
|
+
if (sink.size >= 16) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
871
1029
|
}
|
|
872
1030
|
return;
|
|
873
1031
|
}
|
|
@@ -894,6 +1052,8 @@ function preferredMediaKeys(kind) {
|
|
|
894
1052
|
"rawUrl",
|
|
895
1053
|
"oriUrl",
|
|
896
1054
|
"imageUrl",
|
|
1055
|
+
"photoUrl",
|
|
1056
|
+
"fileUrl",
|
|
897
1057
|
"thumbUrl",
|
|
898
1058
|
"thumb",
|
|
899
1059
|
"href",
|
|
@@ -901,11 +1061,48 @@ function preferredMediaKeys(kind) {
|
|
|
901
1061
|
"src"
|
|
902
1062
|
];
|
|
903
1063
|
case "video":
|
|
904
|
-
return [
|
|
1064
|
+
return [
|
|
1065
|
+
"videoUrl",
|
|
1066
|
+
"video_url",
|
|
1067
|
+
"mediaUrl",
|
|
1068
|
+
"streamUrl",
|
|
1069
|
+
"playUrl",
|
|
1070
|
+
"fileUrl",
|
|
1071
|
+
"rawUrl",
|
|
1072
|
+
"href",
|
|
1073
|
+
"url",
|
|
1074
|
+
"src"
|
|
1075
|
+
];
|
|
905
1076
|
case "audio":
|
|
906
|
-
return [
|
|
1077
|
+
return [
|
|
1078
|
+
"voiceUrl",
|
|
1079
|
+
"m4aUrl",
|
|
1080
|
+
"audioUrl",
|
|
1081
|
+
"voice_url",
|
|
1082
|
+
"m4a_url",
|
|
1083
|
+
"audio_url",
|
|
1084
|
+
"mediaUrl",
|
|
1085
|
+
"downloadUrl",
|
|
1086
|
+
"streamUrl",
|
|
1087
|
+
"playUrl",
|
|
1088
|
+
"fileUrl",
|
|
1089
|
+
"rawUrl",
|
|
1090
|
+
"href",
|
|
1091
|
+
"url",
|
|
1092
|
+
"src"
|
|
1093
|
+
];
|
|
907
1094
|
case "file":
|
|
908
|
-
return [
|
|
1095
|
+
return [
|
|
1096
|
+
"fileUrl",
|
|
1097
|
+
"downloadUrl",
|
|
1098
|
+
"rawUrl",
|
|
1099
|
+
"normalUrl",
|
|
1100
|
+
"oriUrl",
|
|
1101
|
+
"fileLink",
|
|
1102
|
+
"href",
|
|
1103
|
+
"url",
|
|
1104
|
+
"src"
|
|
1105
|
+
];
|
|
909
1106
|
}
|
|
910
1107
|
}
|
|
911
1108
|
function resolvePreferredMediaUrls(kind, content) {
|
|
@@ -939,27 +1136,49 @@ function mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind) {
|
|
|
939
1136
|
const normalizedType = mediaType?.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
940
1137
|
const byType = {
|
|
941
1138
|
"image/jpeg": ".jpg",
|
|
1139
|
+
"image/jpg": ".jpg",
|
|
942
1140
|
"image/png": ".png",
|
|
943
1141
|
"image/webp": ".webp",
|
|
944
1142
|
"image/gif": ".gif",
|
|
1143
|
+
"image/heic": ".heic",
|
|
1144
|
+
"image/heif": ".heif",
|
|
945
1145
|
"video/mp4": ".mp4",
|
|
1146
|
+
"video/quicktime": ".mov",
|
|
1147
|
+
"video/webm": ".webm",
|
|
946
1148
|
"audio/mpeg": ".mp3",
|
|
947
1149
|
"audio/mp3": ".mp3",
|
|
1150
|
+
"audio/aac": ".aac",
|
|
1151
|
+
"audio/x-aac": ".aac",
|
|
948
1152
|
"audio/mp4": ".m4a",
|
|
949
1153
|
"audio/x-m4a": ".m4a",
|
|
950
1154
|
"audio/wav": ".wav",
|
|
951
1155
|
"audio/ogg": ".ogg",
|
|
1156
|
+
"audio/webm": ".webm",
|
|
952
1157
|
"application/pdf": ".pdf",
|
|
953
1158
|
"text/plain": ".txt",
|
|
1159
|
+
"text/markdown": ".md",
|
|
954
1160
|
"text/csv": ".csv",
|
|
1161
|
+
"text/tab-separated-values": ".tsv",
|
|
955
1162
|
"application/json": ".json",
|
|
1163
|
+
"application/xml": ".xml",
|
|
1164
|
+
"text/xml": ".xml",
|
|
956
1165
|
"application/zip": ".zip",
|
|
1166
|
+
"application/gzip": ".gz",
|
|
1167
|
+
"application/x-tar": ".tar",
|
|
1168
|
+
"application/x-7z-compressed": ".7z",
|
|
1169
|
+
"application/vnd.rar": ".rar",
|
|
957
1170
|
"application/vnd.ms-excel": ".xls",
|
|
958
1171
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
|
|
1172
|
+
"application/vnd.ms-excel.sheet.binary.macroenabled.12": ".xlsb",
|
|
1173
|
+
"application/vnd.ms-excel.sheet.macroenabled.12": ".xlsm",
|
|
959
1174
|
"application/msword": ".doc",
|
|
960
1175
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
|
|
961
1176
|
"application/vnd.ms-powerpoint": ".ppt",
|
|
962
|
-
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx"
|
|
1177
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
|
|
1178
|
+
"application/rtf": ".rtf",
|
|
1179
|
+
"application/vnd.oasis.opendocument.text": ".odt",
|
|
1180
|
+
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
|
|
1181
|
+
"application/vnd.oasis.opendocument.presentation": ".odp"
|
|
963
1182
|
};
|
|
964
1183
|
const fromType = byType[normalizedType];
|
|
965
1184
|
if (fromType) return fromType;
|
|
@@ -993,8 +1212,9 @@ function resolveOpenClawMediaDir() {
|
|
|
993
1212
|
return path4.join(stateDir, "media");
|
|
994
1213
|
}
|
|
995
1214
|
function resolveInboundMediaDir(profile) {
|
|
996
|
-
const
|
|
997
|
-
if (
|
|
1215
|
+
const configuredRaw = process.env.OPENZCA_LISTEN_MEDIA_DIR?.trim();
|
|
1216
|
+
if (configuredRaw) {
|
|
1217
|
+
const configured = normalizeMediaInput(configuredRaw);
|
|
998
1218
|
return path4.isAbsolute(configured) ? configured : path4.resolve(process.cwd(), configured);
|
|
999
1219
|
}
|
|
1000
1220
|
const legacyRequested = process.env.OPENZCA_LISTEN_MEDIA_LEGACY_DIR?.trim() === "1";
|
|
@@ -1039,8 +1259,11 @@ function summarizeStructuredContent(msgType, content) {
|
|
|
1039
1259
|
"msg",
|
|
1040
1260
|
"message",
|
|
1041
1261
|
"text",
|
|
1262
|
+
"caption",
|
|
1042
1263
|
"title",
|
|
1043
1264
|
"description",
|
|
1265
|
+
"fileName",
|
|
1266
|
+
"name",
|
|
1044
1267
|
"href",
|
|
1045
1268
|
"url",
|
|
1046
1269
|
"src"
|
|
@@ -1161,7 +1384,38 @@ function toEpochSeconds(input) {
|
|
|
1161
1384
|
}
|
|
1162
1385
|
return Math.floor(numeric);
|
|
1163
1386
|
}
|
|
1164
|
-
program.name("openzca").description("Open-source zca-cli compatible wrapper powered by zca-js").version(PKG_VERSION).option("-p, --profile <name>", "Profile name").showHelpAfterError();
|
|
1387
|
+
program.name("openzca").description("Open-source zca-cli compatible wrapper powered by zca-js").version(PKG_VERSION).option("-p, --profile <name>", "Profile name").option("--debug", "Enable debug logging").option("--debug-file <path>", "Debug log file path").showHelpAfterError();
|
|
1388
|
+
program.hook("preAction", (_parent, actionCommand) => {
|
|
1389
|
+
if (!resolveDebugEnabled(actionCommand)) {
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
DEBUG_COMMAND_START.set(actionCommand, Date.now());
|
|
1393
|
+
writeDebugLine(
|
|
1394
|
+
"command.start",
|
|
1395
|
+
{
|
|
1396
|
+
command: commandPathLabel(actionCommand),
|
|
1397
|
+
argv: process.argv.slice(2),
|
|
1398
|
+
cwd: process.cwd(),
|
|
1399
|
+
profileFlag: getDebugOptions(actionCommand).profile ?? null,
|
|
1400
|
+
envProfile: process.env.ZCA_PROFILE ?? null
|
|
1401
|
+
},
|
|
1402
|
+
actionCommand
|
|
1403
|
+
);
|
|
1404
|
+
});
|
|
1405
|
+
program.hook("postAction", (_parent, actionCommand) => {
|
|
1406
|
+
if (!resolveDebugEnabled(actionCommand)) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const startedAt = DEBUG_COMMAND_START.get(actionCommand);
|
|
1410
|
+
writeDebugLine(
|
|
1411
|
+
"command.done",
|
|
1412
|
+
{
|
|
1413
|
+
command: commandPathLabel(actionCommand),
|
|
1414
|
+
durationMs: typeof startedAt === "number" ? Date.now() - startedAt : void 0
|
|
1415
|
+
},
|
|
1416
|
+
actionCommand
|
|
1417
|
+
);
|
|
1418
|
+
});
|
|
1165
1419
|
var account = program.command("account").description("Multi-account profile management");
|
|
1166
1420
|
account.command("list").alias("ls").alias("l").description("List all account profiles").action(
|
|
1167
1421
|
wrapAction(async () => {
|
|
@@ -1248,7 +1502,7 @@ auth.command("login").description("Login with QR code").option("-q, --qr-path <p
|
|
|
1248
1502
|
auth.command("login-cred [file]").alias("login-creds").description("Login using credential JSON file").action(
|
|
1249
1503
|
wrapAction(async (file, command) => {
|
|
1250
1504
|
const profile = await currentProfile(command);
|
|
1251
|
-
const credentials = file ? await parseCredentialFile(path4.resolve(file)) : toCredentials(
|
|
1505
|
+
const credentials = file ? await parseCredentialFile(path4.resolve(normalizeMediaInput(file))) : toCredentials(
|
|
1252
1506
|
await loadCredentials(profile) ?? (() => {
|
|
1253
1507
|
throw new Error(
|
|
1254
1508
|
`No saved credentials for profile "${profile}". Run: openzca auth login`
|
|
@@ -1333,9 +1587,20 @@ msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (rep
|
|
|
1333
1587
|
wrapAction(
|
|
1334
1588
|
async (threadId, file, opts, command) => {
|
|
1335
1589
|
const { api } = await requireApi(command);
|
|
1336
|
-
const
|
|
1590
|
+
const normalizedFile = file ? normalizeMediaInput(file) : void 0;
|
|
1591
|
+
const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
|
|
1337
1592
|
const urlInputs = files.filter((entry) => isHttpUrl(entry));
|
|
1338
1593
|
const localInputs = files.filter((entry) => !isHttpUrl(entry));
|
|
1594
|
+
writeDebugLine(
|
|
1595
|
+
"msg.image.inputs",
|
|
1596
|
+
{
|
|
1597
|
+
threadId,
|
|
1598
|
+
isGroup: Boolean(opts.group),
|
|
1599
|
+
localInputs,
|
|
1600
|
+
urlInputs
|
|
1601
|
+
},
|
|
1602
|
+
command
|
|
1603
|
+
);
|
|
1339
1604
|
const downloaded = await downloadUrlsToTempFiles(urlInputs);
|
|
1340
1605
|
try {
|
|
1341
1606
|
const attachments = [...localInputs, ...downloaded.files];
|
|
@@ -1362,9 +1627,20 @@ msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (rep
|
|
|
1362
1627
|
wrapAction(
|
|
1363
1628
|
async (threadId, file, opts, command) => {
|
|
1364
1629
|
const { api } = await requireApi(command);
|
|
1365
|
-
const
|
|
1630
|
+
const normalizedFile = file ? normalizeMediaInput(file) : void 0;
|
|
1631
|
+
const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
|
|
1366
1632
|
const urlInputs = files.filter((entry) => isHttpUrl(entry));
|
|
1367
1633
|
const localInputs = files.filter((entry) => !isHttpUrl(entry));
|
|
1634
|
+
writeDebugLine(
|
|
1635
|
+
"msg.video.inputs",
|
|
1636
|
+
{
|
|
1637
|
+
threadId,
|
|
1638
|
+
isGroup: Boolean(opts.group),
|
|
1639
|
+
localInputs,
|
|
1640
|
+
urlInputs
|
|
1641
|
+
},
|
|
1642
|
+
command
|
|
1643
|
+
);
|
|
1368
1644
|
const downloaded = await downloadUrlsToTempFiles(urlInputs);
|
|
1369
1645
|
try {
|
|
1370
1646
|
const attachments = [...localInputs, ...downloaded.files];
|
|
@@ -1392,12 +1668,23 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
|
|
|
1392
1668
|
async (threadId, file, opts, command) => {
|
|
1393
1669
|
const { api } = await requireApi(command);
|
|
1394
1670
|
const type = asThreadType(opts.group);
|
|
1395
|
-
const
|
|
1671
|
+
const normalizedFile = file ? normalizeMediaInput(file) : void 0;
|
|
1672
|
+
const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
|
|
1396
1673
|
if (files.length === 0) {
|
|
1397
1674
|
throw new Error("Provide a voice file or --url.");
|
|
1398
1675
|
}
|
|
1399
1676
|
const urlInputs = files.filter((entry) => isHttpUrl(entry));
|
|
1400
1677
|
const localInputs = files.filter((entry) => !isHttpUrl(entry));
|
|
1678
|
+
writeDebugLine(
|
|
1679
|
+
"msg.voice.inputs",
|
|
1680
|
+
{
|
|
1681
|
+
threadId,
|
|
1682
|
+
isGroup: Boolean(opts.group),
|
|
1683
|
+
localInputs,
|
|
1684
|
+
urlInputs
|
|
1685
|
+
},
|
|
1686
|
+
command
|
|
1687
|
+
);
|
|
1401
1688
|
const downloaded = await downloadUrlsToTempFiles(urlInputs);
|
|
1402
1689
|
try {
|
|
1403
1690
|
const attachments = [...localInputs, ...downloaded.files];
|
|
@@ -1411,7 +1698,7 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
|
|
|
1411
1698
|
}
|
|
1412
1699
|
if (results.length === 0) {
|
|
1413
1700
|
throw new Error(
|
|
1414
|
-
"No valid voice attachment generated. Use an audio file (e.g. .mp3, .m4a, .wav, .ogg)."
|
|
1701
|
+
"No valid voice attachment generated. Use an audio file (e.g. .aac, .mp3, .m4a, .wav, .ogg)."
|
|
1415
1702
|
);
|
|
1416
1703
|
}
|
|
1417
1704
|
output(results, false);
|
|
@@ -1547,7 +1834,18 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
1547
1834
|
const urlInputs = inputs.filter((entry) => isHttpUrl(entry));
|
|
1548
1835
|
const localInputs = inputs.filter((entry) => !isHttpUrl(entry));
|
|
1549
1836
|
const [threadId, file] = arg2 ? [arg2, arg1] : [arg1, void 0];
|
|
1550
|
-
const
|
|
1837
|
+
const normalizedFile = file ? normalizeMediaInput(file) : void 0;
|
|
1838
|
+
const localFiles = [normalizedFile, ...localInputs].filter(Boolean);
|
|
1839
|
+
writeDebugLine(
|
|
1840
|
+
"msg.upload.inputs",
|
|
1841
|
+
{
|
|
1842
|
+
threadId,
|
|
1843
|
+
isGroup: Boolean(opts.group),
|
|
1844
|
+
localFiles,
|
|
1845
|
+
urlInputs
|
|
1846
|
+
},
|
|
1847
|
+
command
|
|
1848
|
+
);
|
|
1551
1849
|
const downloaded = await downloadUrlsToTempFiles(urlInputs);
|
|
1552
1850
|
try {
|
|
1553
1851
|
const attachments = [...localFiles, ...downloaded.files];
|
|
@@ -1679,8 +1977,9 @@ group.command("rename <groupId> <name>").description("Rename group").action(
|
|
|
1679
1977
|
group.command("avatar <groupId> <file>").description("Change group avatar").action(
|
|
1680
1978
|
wrapAction(async (groupId, file, command) => {
|
|
1681
1979
|
const { api } = await requireApi(command);
|
|
1682
|
-
|
|
1683
|
-
|
|
1980
|
+
const normalizedFile = normalizeMediaInput(file);
|
|
1981
|
+
await assertFilesExist([normalizedFile]);
|
|
1982
|
+
const response = await api.changeGroupAvatar(normalizedFile, groupId);
|
|
1684
1983
|
output(response, false);
|
|
1685
1984
|
})
|
|
1686
1985
|
);
|
|
@@ -2081,8 +2380,9 @@ me.command("update").option("--name <name>", "Display name").option("--gender <g
|
|
|
2081
2380
|
me.command("avatar <file>").description("Change profile avatar").action(
|
|
2082
2381
|
wrapAction(async (file, command) => {
|
|
2083
2382
|
const { api } = await requireApi(command);
|
|
2084
|
-
|
|
2085
|
-
|
|
2383
|
+
const normalizedFile = normalizeMediaInput(file);
|
|
2384
|
+
await assertFilesExist([normalizedFile]);
|
|
2385
|
+
output(await api.changeAccountAvatar(normalizedFile), false);
|
|
2086
2386
|
})
|
|
2087
2387
|
);
|
|
2088
2388
|
me.command("avatars").option("-j, --json", "JSON output").description("List avatars").action(
|
|
@@ -2124,6 +2424,17 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
2124
2424
|
async (opts, command) => {
|
|
2125
2425
|
const { profile, api } = await requireApi(command);
|
|
2126
2426
|
console.log("Listening... Press Ctrl+C to stop.");
|
|
2427
|
+
writeDebugLine(
|
|
2428
|
+
"listen.start",
|
|
2429
|
+
{
|
|
2430
|
+
profile,
|
|
2431
|
+
mediaDir: resolveInboundMediaDir(profile),
|
|
2432
|
+
maxMediaBytes: parseMaxInboundMediaBytes(),
|
|
2433
|
+
maxMediaFiles: parseMaxInboundMediaFiles(),
|
|
2434
|
+
includeMediaUrl: process.env.OPENZCA_LISTEN_INCLUDE_MEDIA_URL?.trim() ?? null
|
|
2435
|
+
},
|
|
2436
|
+
command
|
|
2437
|
+
);
|
|
2127
2438
|
async function emitWebhook(payload) {
|
|
2128
2439
|
if (!opts.webhook) return;
|
|
2129
2440
|
try {
|
|
@@ -2150,10 +2461,24 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
2150
2461
|
const messageData = message.data;
|
|
2151
2462
|
const rawContent = messageData.content;
|
|
2152
2463
|
const msgType = getStringCandidate(messageData, ["msgType"]);
|
|
2464
|
+
const parsedContent = normalizeStructuredContent(rawContent);
|
|
2465
|
+
const hasParsedStructuredContent = parsedContent !== rawContent;
|
|
2153
2466
|
const rawText = typeof rawContent === "string" ? rawContent : "";
|
|
2154
|
-
const mediaKind = detectInboundMediaKind(msgType,
|
|
2467
|
+
const mediaKind = detectInboundMediaKind(msgType, parsedContent);
|
|
2155
2468
|
const maxMediaFiles = parseMaxInboundMediaFiles();
|
|
2156
|
-
const remoteMediaUrls = mediaKind && maxMediaFiles > 0 ? resolvePreferredMediaUrls(mediaKind,
|
|
2469
|
+
const remoteMediaUrls = mediaKind && maxMediaFiles > 0 ? resolvePreferredMediaUrls(mediaKind, parsedContent).slice(0, maxMediaFiles) : [];
|
|
2470
|
+
writeDebugLine(
|
|
2471
|
+
"listen.media.detected",
|
|
2472
|
+
{
|
|
2473
|
+
profile,
|
|
2474
|
+
threadId: message.threadId,
|
|
2475
|
+
msgType: msgType || void 0,
|
|
2476
|
+
mediaKind,
|
|
2477
|
+
hasParsedStructuredContent,
|
|
2478
|
+
remoteMediaUrls
|
|
2479
|
+
},
|
|
2480
|
+
command
|
|
2481
|
+
);
|
|
2157
2482
|
const mediaEntries = [];
|
|
2158
2483
|
for (const mediaUrl2 of remoteMediaUrls) {
|
|
2159
2484
|
let mediaPath2;
|
|
@@ -2170,6 +2495,15 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
2170
2495
|
console.error(
|
|
2171
2496
|
`Warning: failed to cache inbound media (${error instanceof Error ? error.message : String(error)})`
|
|
2172
2497
|
);
|
|
2498
|
+
writeDebugLine(
|
|
2499
|
+
"listen.media.cache_error",
|
|
2500
|
+
{
|
|
2501
|
+
profile,
|
|
2502
|
+
mediaUrl: mediaUrl2,
|
|
2503
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2504
|
+
},
|
|
2505
|
+
command
|
|
2506
|
+
);
|
|
2173
2507
|
}
|
|
2174
2508
|
mediaEntries.push({
|
|
2175
2509
|
mediaPath: mediaPath2,
|
|
@@ -2184,12 +2518,12 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
2184
2518
|
const mediaPath = mediaPaths[0];
|
|
2185
2519
|
const mediaUrl = mediaUrls[0];
|
|
2186
2520
|
const mediaType = mediaTypes[0];
|
|
2187
|
-
const caption = rawText.trim().length > 0 ? rawText.trim() : summarizeStructuredContent(msgType,
|
|
2521
|
+
const caption = rawText.trim().length > 0 && !hasParsedStructuredContent ? rawText.trim() : summarizeStructuredContent(msgType, parsedContent);
|
|
2188
2522
|
let processedText = mediaEntries.length ? buildMediaAttachedText({
|
|
2189
2523
|
mediaEntries,
|
|
2190
2524
|
fallbackKind: mediaKind,
|
|
2191
2525
|
caption
|
|
2192
|
-
}) : rawText.trim().length > 0 ? rawText : summarizeStructuredContent(msgType,
|
|
2526
|
+
}) : rawText.trim().length > 0 && !hasParsedStructuredContent ? rawText : summarizeStructuredContent(msgType, parsedContent);
|
|
2193
2527
|
if (!processedText.trim()) return;
|
|
2194
2528
|
if (opts.prefix) {
|
|
2195
2529
|
if (!processedText.startsWith(opts.prefix)) return;
|