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.
Files changed (3) hide show
  1. package/README.md +37 -2
  2. package/dist/cli.js +364 -30
  3. 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` now includes inbound media metadata when available:
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"])) return "audio";
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 trimmed = value.trim();
869
- if (isHttpUrl(trimmed)) {
870
- sink.add(trimmed);
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 ["videoUrl", "fileUrl", "href", "url", "src"];
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 ["voiceUrl", "m4aUrl", "audioUrl", "fileUrl", "href", "url", "src"];
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 ["fileUrl", "href", "url", "src"];
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 configured = process.env.OPENZCA_LISTEN_MEDIA_DIR?.trim();
997
- if (configured) {
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 files = [file, ...normalizeInputList(opts.url)].filter(Boolean);
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 files = [file, ...normalizeInputList(opts.url)].filter(Boolean);
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 files = [file, ...normalizeInputList(opts.url)].filter(Boolean);
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 localFiles = [file, ...localInputs].filter(Boolean);
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
- await assertFilesExist([file]);
1683
- const response = await api.changeGroupAvatar(file, groupId);
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
- await assertFilesExist([file]);
2085
- output(await api.changeAccountAvatar(file), false);
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, rawContent);
2467
+ const mediaKind = detectInboundMediaKind(msgType, parsedContent);
2155
2468
  const maxMediaFiles = parseMaxInboundMediaFiles();
2156
- const remoteMediaUrls = mediaKind && maxMediaFiles > 0 ? resolvePreferredMediaUrls(mediaKind, rawContent).slice(0, maxMediaFiles) : [];
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, rawContent);
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, rawContent);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {