openzca 0.1.51 → 0.1.53

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 +10 -0
  2. package/dist/cli.js +405 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -48,6 +48,9 @@ openzca msg send USER_ID "Reply text" --reply-id MSG_ID
48
48
  # Reply without DB using a listen --raw payload
49
49
  openzca msg send USER_ID "Reply text" --reply-message '{"threadId":"...","msgId":"...","cliMsgId":"...","content":"...","msgType":"webchat","senderId":"...","toId":"...","ts":"..."}'
50
50
 
51
+ # Inspect how a formatted message expands before sending/chunking
52
+ openzca msg analyze-text GROUP_ID "- item one\n- item two" --group --json
53
+
51
54
  # Listen for incoming messages
52
55
  openzca listen
53
56
 
@@ -100,6 +103,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
100
103
  | Command | Description |
101
104
  |---------|-------------|
102
105
  | `openzca msg send <threadId> <message>` | Send text with formatting (`**bold**`, `*italic*`, `~~strike~~`, etc.), group @mention resolution (`--raw` to skip formatting), and quote replies via `--reply-id` or `--reply-message` |
106
+ | `openzca msg analyze-text <threadId> <message>` | Build and inspect the exact text payload `msg send` would hand to `zca-js`, including rendered text length, style count, mention count, `textProperties` size, and request size estimate |
103
107
  | `openzca msg image <threadId> [file]` | Send image(s) from file or URL |
104
108
  | `openzca msg video <threadId> [file]` | Send video(s) from file or URL; single `.mp4` inputs try native video mode |
105
109
  | `openzca msg voice <threadId> [file]` | Send voice message from local file or URL (`.aac`, `.mp3`, `.m4a`, `.wav`, `.ogg`) |
@@ -123,6 +127,8 @@ Media commands accept local files, `file://` paths, and repeatable `--url` optio
123
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.
124
128
  Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
125
129
  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
+ 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.
131
+ Use `openzca msg analyze-text ... --json` when you need to predict whether a formatted reply will expand into a large `textProperties` payload before attempting delivery.
126
132
  Reply flows:
127
133
 
128
134
  - `--reply-id <id>` resolves a stored message from the local DB by `msgId`, `cliMsgId`, or internal message uid. This requires DB persistence to be enabled for the profile.
@@ -386,6 +392,10 @@ Listener resilience override:
386
392
  - `OPENZCA_RECENT_GROUP_MAX_PAGES`: max websocket history pages to scan for `msg recent -g` when direct group-history path fails.
387
393
  - Default: `20`.
388
394
  - Increase if a group thread is old and not found quickly.
395
+ - `OPENZCA_TEXT_MESSAGE_MAX_LENGTH`: max rendered characters allowed per outbound text chunk before `msg send` splits it.
396
+ - Default: `2000`.
397
+ - `OPENZCA_TEXT_REQUEST_PARAMS_MAX_ESTIMATE`: max estimated serialized request size allowed per outbound text chunk before `msg send` splits it.
398
+ - Default: `4000`.
389
399
  - `OPENZCA_LISTEN_ENFORCE_SINGLE_OWNER`: enforce one `listen` owner process per profile.
390
400
  - Default: enabled.
391
401
  - Set to `0` to allow multiple listeners on the same profile (not recommended).
package/dist/cli.js CHANGED
@@ -1979,9 +1979,6 @@ function parseTimeBoundaryInput(value, _nowMs = Date.now()) {
1979
1979
  return void 0;
1980
1980
  }
1981
1981
 
1982
- // src/lib/text-send.ts
1983
- import { ThreadType } from "zca-js";
1984
-
1985
1982
  // src/lib/group-mentions.ts
1986
1983
  var ALLOWED_START_BOUNDARY_CHARS = /* @__PURE__ */ new Set(["(", "[", "{", "<", '"', ",", ";", ":"]);
1987
1984
  var ALLOWED_END_BOUNDARY_CHARS = /* @__PURE__ */ new Set([",", ";", ":", "!", "?", ")", "]", "}", ">", '"']);
@@ -2086,6 +2083,9 @@ function isMentionStartBoundary(text, atIndex) {
2086
2083
  return /\s/u.test(previous) || ALLOWED_START_BOUNDARY_CHARS.has(previous);
2087
2084
  }
2088
2085
 
2086
+ // src/lib/text-send.ts
2087
+ import { ThreadType } from "zca-js";
2088
+
2089
2089
  // src/lib/text-styles.ts
2090
2090
  import { TextStyle } from "zca-js";
2091
2091
  var TAG_STYLE_MAP = {
@@ -2333,6 +2333,8 @@ function normalizeCodeBlockLeadingWhitespace(line) {
2333
2333
  }
2334
2334
 
2335
2335
  // src/lib/text-send.ts
2336
+ var ZALO_TEXT_MESSAGE_MAX_LENGTH = 2e3;
2337
+ var ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE = 4e3;
2336
2338
  async function buildTextSendPayload(params) {
2337
2339
  if (params.raw) {
2338
2340
  const mentions2 = await resolveGroupMentionsIfNeeded(params, params.message);
@@ -2346,6 +2348,127 @@ async function buildTextSendPayload(params) {
2346
2348
  mentions
2347
2349
  };
2348
2350
  }
2351
+ async function analyzeTextSendPayload(params) {
2352
+ const payload = await buildTextSendPayload(params);
2353
+ const payloadObject = normalizeTextSendPayload(payload);
2354
+ const textProperties = buildTextProperties(payloadObject.styles);
2355
+ const mentionInfo = buildMentionInfo(
2356
+ params.threadType,
2357
+ payloadObject.msg,
2358
+ payloadObject.mentions
2359
+ );
2360
+ const requestParams = omitUndefined({
2361
+ message: payloadObject.msg,
2362
+ clientId: 17e11,
2363
+ mentionInfo,
2364
+ imei: params.threadType === ThreadType.Group ? void 0 : "000000000000000",
2365
+ ttl: 0,
2366
+ visibility: params.threadType === ThreadType.Group ? 0 : void 0,
2367
+ toid: params.threadType === ThreadType.Group ? void 0 : params.threadId,
2368
+ grid: params.threadType === ThreadType.Group ? params.threadId : void 0,
2369
+ textProperties
2370
+ });
2371
+ return {
2372
+ payload,
2373
+ ...buildTextSendPayloadAnalysis({
2374
+ payloadObject,
2375
+ rawInputLength: params.message.length,
2376
+ textProperties,
2377
+ mentionInfo,
2378
+ requestParamsLengthEstimate: JSON.stringify(requestParams).length,
2379
+ threadType: params.threadType
2380
+ })
2381
+ };
2382
+ }
2383
+ function planTextSendPayloadsForDelivery(params) {
2384
+ const maxMessageLength = resolvePositiveLimit(
2385
+ params.maxMessageLength,
2386
+ ZALO_TEXT_MESSAGE_MAX_LENGTH
2387
+ );
2388
+ const maxRequestParamsLengthEstimate = resolvePositiveLimit(
2389
+ params.maxRequestParamsLengthEstimate,
2390
+ ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE
2391
+ );
2392
+ const chunks = [];
2393
+ const analyses = [];
2394
+ const pending = [params.payload];
2395
+ while (pending.length > 0) {
2396
+ const currentPayload = pending.shift();
2397
+ const analysis = analyzePreparedTextSendPayload({
2398
+ payload: currentPayload,
2399
+ threadType: params.threadType,
2400
+ threadId: params.threadId
2401
+ });
2402
+ if (isTextSendPayloadWithinDeliveryLimits(analysis, {
2403
+ maxMessageLength,
2404
+ maxRequestParamsLengthEstimate
2405
+ })) {
2406
+ chunks.push(currentPayload);
2407
+ analyses.push(analysis);
2408
+ continue;
2409
+ }
2410
+ const targetLength = computeNextChunkLength(analysis, {
2411
+ maxMessageLength,
2412
+ maxRequestParamsLengthEstimate
2413
+ });
2414
+ const splitChunks = splitTextSendPayload(currentPayload, targetLength);
2415
+ if (splitChunks.length <= 1) {
2416
+ throw new Error(
2417
+ `Unable to split formatted text payload into deliverable chunks within ${targetLength} characters.`
2418
+ );
2419
+ }
2420
+ pending.unshift(...splitChunks);
2421
+ }
2422
+ return { chunks, analyses };
2423
+ }
2424
+ function splitTextSendPayload(payload, maxLength = ZALO_TEXT_MESSAGE_MAX_LENGTH) {
2425
+ if (!Number.isInteger(maxLength) || maxLength <= 0) {
2426
+ throw new Error("Text chunk size must be a positive integer");
2427
+ }
2428
+ const payloadObject = normalizeTextSendPayload(payload);
2429
+ if (payloadObject.msg.length <= maxLength) {
2430
+ return [payload];
2431
+ }
2432
+ const chunks = [];
2433
+ let start = 0;
2434
+ while (start < payloadObject.msg.length) {
2435
+ const end = findChunkEnd(payloadObject, start, maxLength);
2436
+ chunks.push(sliceTextSendPayload(payloadObject, start, end));
2437
+ start = end;
2438
+ }
2439
+ return chunks;
2440
+ }
2441
+ function analyzePreparedTextSendPayload(params) {
2442
+ const payloadObject = normalizeTextSendPayload(params.payload);
2443
+ const textProperties = buildTextProperties(payloadObject.styles);
2444
+ const mentionInfo = buildMentionInfo(
2445
+ params.threadType,
2446
+ payloadObject.msg,
2447
+ payloadObject.mentions
2448
+ );
2449
+ const requestParams = omitUndefined({
2450
+ message: payloadObject.msg,
2451
+ clientId: 17e11,
2452
+ mentionInfo,
2453
+ imei: params.threadType === ThreadType.Group ? void 0 : "000000000000000",
2454
+ ttl: 0,
2455
+ visibility: params.threadType === ThreadType.Group ? 0 : void 0,
2456
+ toid: params.threadType === ThreadType.Group ? void 0 : params.threadId,
2457
+ grid: params.threadType === ThreadType.Group ? params.threadId : void 0,
2458
+ textProperties
2459
+ });
2460
+ return {
2461
+ payload: params.payload,
2462
+ ...buildTextSendPayloadAnalysis({
2463
+ payloadObject,
2464
+ rawInputLength: payloadObject.msg.length,
2465
+ textProperties,
2466
+ mentionInfo,
2467
+ requestParamsLengthEstimate: JSON.stringify(requestParams).length,
2468
+ threadType: params.threadType
2469
+ })
2470
+ };
2471
+ }
2349
2472
  async function resolveGroupMentionsIfNeeded(params, text) {
2350
2473
  if (params.threadType !== ThreadType.Group) {
2351
2474
  return void 0;
@@ -2360,6 +2483,208 @@ async function resolveGroupMentionsIfNeeded(params, text) {
2360
2483
  const mentions = resolveOutboundGroupMentions(text, members);
2361
2484
  return mentions.length > 0 ? mentions : void 0;
2362
2485
  }
2486
+ function normalizeTextSendPayload(payload) {
2487
+ if (typeof payload === "string") {
2488
+ return { msg: payload };
2489
+ }
2490
+ return payload;
2491
+ }
2492
+ function buildTextSendPayloadAnalysis(params) {
2493
+ return {
2494
+ payloadObject: params.payloadObject,
2495
+ rawInputLength: params.rawInputLength,
2496
+ renderedTextLength: params.payloadObject.msg.length,
2497
+ styleCount: params.payloadObject.styles?.length ?? 0,
2498
+ mentionCount: params.payloadObject.mentions?.length ?? 0,
2499
+ textPropertiesLength: params.textProperties?.length ?? 0,
2500
+ mentionInfoLength: params.mentionInfo?.length ?? 0,
2501
+ requestParamsLengthEstimate: params.requestParamsLengthEstimate,
2502
+ sendPath: params.threadType === ThreadType.Group ? params.mentionInfo ? "mention" : "sendmsg" : "sms"
2503
+ };
2504
+ }
2505
+ function isTextSendPayloadWithinDeliveryLimits(analysis, limits) {
2506
+ return analysis.renderedTextLength <= limits.maxMessageLength && analysis.requestParamsLengthEstimate <= limits.maxRequestParamsLengthEstimate;
2507
+ }
2508
+ function computeNextChunkLength(analysis, limits) {
2509
+ const currentLength = analysis.renderedTextLength;
2510
+ const targetLengths = [limits.maxMessageLength, currentLength - 1].filter((value) => value > 0);
2511
+ if (analysis.requestParamsLengthEstimate > limits.maxRequestParamsLengthEstimate) {
2512
+ targetLengths.push(
2513
+ Math.floor(
2514
+ currentLength * limits.maxRequestParamsLengthEstimate / analysis.requestParamsLengthEstimate
2515
+ )
2516
+ );
2517
+ }
2518
+ const targetLength = Math.max(
2519
+ 1,
2520
+ Math.min(...targetLengths.filter((value) => Number.isFinite(value) && value > 0))
2521
+ );
2522
+ return Math.min(targetLength, currentLength - 1);
2523
+ }
2524
+ function resolvePositiveLimit(value, fallback) {
2525
+ if (!value || !Number.isInteger(value) || value <= 0) {
2526
+ return fallback;
2527
+ }
2528
+ return value;
2529
+ }
2530
+ function sliceTextSendPayload(payloadObject, start, end) {
2531
+ const msg2 = payloadObject.msg.slice(start, end);
2532
+ const styles = sliceStyles(payloadObject.styles, start, end);
2533
+ const mentions = sliceMentions(payloadObject.mentions, start, end);
2534
+ if (!styles && !mentions) {
2535
+ return msg2;
2536
+ }
2537
+ return omitUndefined({
2538
+ msg: msg2,
2539
+ styles,
2540
+ mentions
2541
+ });
2542
+ }
2543
+ function sliceStyles(styles, start, end) {
2544
+ if (!styles || styles.length === 0) {
2545
+ return void 0;
2546
+ }
2547
+ const sliced = [];
2548
+ for (const style of styles) {
2549
+ const styleStart = style.start;
2550
+ const styleEnd = style.start + style.len;
2551
+ const overlapStart = Math.max(styleStart, start);
2552
+ const overlapEnd = Math.min(styleEnd, end);
2553
+ if (overlapStart >= overlapEnd) {
2554
+ continue;
2555
+ }
2556
+ if (style.st === "ind_$") {
2557
+ sliced.push({
2558
+ start: overlapStart - start,
2559
+ len: overlapEnd - overlapStart,
2560
+ st: style.st,
2561
+ indentSize: style.indentSize
2562
+ });
2563
+ continue;
2564
+ }
2565
+ sliced.push({
2566
+ start: overlapStart - start,
2567
+ len: overlapEnd - overlapStart,
2568
+ st: style.st
2569
+ });
2570
+ }
2571
+ return sliced.length > 0 ? sliced : void 0;
2572
+ }
2573
+ function sliceMentions(mentions, start, end) {
2574
+ if (!mentions || mentions.length === 0) {
2575
+ return void 0;
2576
+ }
2577
+ const sliced = mentions.filter((mention) => mention.pos >= start && mention.pos + mention.len <= end).map((mention) => ({
2578
+ pos: mention.pos - start,
2579
+ uid: mention.uid,
2580
+ len: mention.len
2581
+ }));
2582
+ return sliced.length > 0 ? sliced : void 0;
2583
+ }
2584
+ function findChunkEnd(payloadObject, start, maxLength) {
2585
+ const remaining = payloadObject.msg.length - start;
2586
+ if (remaining <= maxLength) {
2587
+ return payloadObject.msg.length;
2588
+ }
2589
+ const maxEnd = start + maxLength;
2590
+ const newlineBreak = findPreferredBreak(payloadObject, start, maxEnd, "\n");
2591
+ if (newlineBreak > start) {
2592
+ return newlineBreak;
2593
+ }
2594
+ const whitespaceBreak = findWhitespaceBreak(payloadObject, start, maxEnd);
2595
+ if (whitespaceBreak > start) {
2596
+ return whitespaceBreak;
2597
+ }
2598
+ for (let cursor = maxEnd; cursor > start; cursor -= 1) {
2599
+ if (isSafeSplitPosition(payloadObject.mentions, cursor)) {
2600
+ return cursor;
2601
+ }
2602
+ }
2603
+ throw new Error(
2604
+ `Unable to split text payload safely within ${maxLength} characters.`
2605
+ );
2606
+ }
2607
+ function findPreferredBreak(payloadObject, start, maxEnd, marker) {
2608
+ for (let cursor = maxEnd; cursor > start; cursor -= 1) {
2609
+ if (!isSafeSplitPosition(payloadObject.mentions, cursor)) {
2610
+ continue;
2611
+ }
2612
+ if (payloadObject.msg[cursor - 1] === marker) {
2613
+ return cursor;
2614
+ }
2615
+ }
2616
+ return start;
2617
+ }
2618
+ function findWhitespaceBreak(payloadObject, start, maxEnd) {
2619
+ for (let cursor = maxEnd; cursor > start; cursor -= 1) {
2620
+ if (!isSafeSplitPosition(payloadObject.mentions, cursor)) {
2621
+ continue;
2622
+ }
2623
+ const previousChar = payloadObject.msg[cursor - 1];
2624
+ if (previousChar === " " || previousChar === " ") {
2625
+ return cursor;
2626
+ }
2627
+ }
2628
+ return start;
2629
+ }
2630
+ function isSafeSplitPosition(mentions, position) {
2631
+ if (!mentions || mentions.length === 0) {
2632
+ return true;
2633
+ }
2634
+ return mentions.every((mention) => {
2635
+ const mentionEnd = mention.pos + mention.len;
2636
+ return position <= mention.pos || position >= mentionEnd;
2637
+ });
2638
+ }
2639
+ function buildTextProperties(styles) {
2640
+ if (!styles || styles.length === 0) {
2641
+ return void 0;
2642
+ }
2643
+ return JSON.stringify({
2644
+ styles: styles.map((style) => {
2645
+ if (style.st === "ind_$") {
2646
+ return omitUndefined({
2647
+ start: style.start,
2648
+ len: style.len,
2649
+ st: `ind_${style.indentSize ?? 1}0`
2650
+ });
2651
+ }
2652
+ return {
2653
+ start: style.start,
2654
+ len: style.len,
2655
+ st: style.st
2656
+ };
2657
+ }),
2658
+ ver: 0
2659
+ });
2660
+ }
2661
+ function buildMentionInfo(threadType, msg2, mentions) {
2662
+ if (threadType !== ThreadType.Group || !mentions || mentions.length === 0) {
2663
+ return void 0;
2664
+ }
2665
+ let totalMentionLen = 0;
2666
+ const mentionsFinal = mentions.filter((mention) => mention.pos >= 0 && Boolean(mention.uid) && mention.len > 0).map((mention) => {
2667
+ totalMentionLen += mention.len;
2668
+ return {
2669
+ pos: mention.pos,
2670
+ uid: mention.uid,
2671
+ len: mention.len,
2672
+ type: mention.uid === "-1" ? 1 : 0
2673
+ };
2674
+ });
2675
+ if (totalMentionLen > msg2.length) {
2676
+ throw new Error("Invalid mentions: total mention characters exceed message length");
2677
+ }
2678
+ if (mentionsFinal.length === 0) {
2679
+ return void 0;
2680
+ }
2681
+ return JSON.stringify(mentionsFinal);
2682
+ }
2683
+ function omitUndefined(value) {
2684
+ return Object.fromEntries(
2685
+ Object.entries(value).filter(([, entry]) => entry !== void 0)
2686
+ );
2687
+ }
2363
2688
 
2364
2689
  // src/lib/video-send.ts
2365
2690
  import { execFile } from "child_process";
@@ -6435,24 +6760,90 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
6435
6760
  ...typeof textPayload === "string" ? { msg: textPayload } : textPayload,
6436
6761
  ...quote ? { quote } : {}
6437
6762
  } : textPayload;
6438
- const response = await api.sendMessage(payload, threadId, threadType);
6763
+ const deliveryPlan = planTextSendPayloadsForDelivery({
6764
+ payload: textPayload,
6765
+ threadType,
6766
+ threadId,
6767
+ maxMessageLength: parsePositiveIntFromEnv(
6768
+ "OPENZCA_TEXT_MESSAGE_MAX_LENGTH",
6769
+ ZALO_TEXT_MESSAGE_MAX_LENGTH
6770
+ ),
6771
+ maxRequestParamsLengthEstimate: parsePositiveIntFromEnv(
6772
+ "OPENZCA_TEXT_REQUEST_PARAMS_MAX_ESTIMATE",
6773
+ ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE
6774
+ )
6775
+ });
6776
+ const payloadChunks = deliveryPlan.chunks;
6777
+ const responses = [];
6778
+ const sentPayloads = [];
6779
+ for (let index = 0; index < payloadChunks.length; index += 1) {
6780
+ const chunk = payloadChunks[index];
6781
+ const chunkPayload = quote && index === 0 ? {
6782
+ ...typeof chunk === "string" ? { msg: chunk } : chunk,
6783
+ quote
6784
+ } : chunk;
6785
+ sentPayloads.push(chunkPayload);
6786
+ responses.push(await api.sendMessage(chunkPayload, threadId, threadType));
6787
+ }
6788
+ const response = responses.length === 1 ? responses[0] : {
6789
+ chunked: true,
6790
+ chunkCount: responses.length,
6791
+ msgId: responses.at(-1)?.message?.msgId?.toString(),
6792
+ response: responses
6793
+ };
6439
6794
  output(response, false);
6440
6795
  if (await shouldWriteToDb(profile)) {
6441
6796
  scheduleDbWrite(profile, command, "msg.send.db.persist_error", async () => {
6442
- await persistOutgoingMessageBestEffort({
6443
- profile,
6444
- api,
6445
- threadId,
6446
- group: opts.group,
6447
- text: message,
6448
- msgType: "text",
6449
- response,
6450
- rawPayload: payload
6451
- });
6797
+ if (payloadChunks.length === 1) {
6798
+ await persistOutgoingMessageBestEffort({
6799
+ profile,
6800
+ api,
6801
+ threadId,
6802
+ group: opts.group,
6803
+ text: message,
6804
+ msgType: "text",
6805
+ response,
6806
+ rawPayload: payload
6807
+ });
6808
+ return;
6809
+ }
6810
+ for (let index = 0; index < payloadChunks.length; index += 1) {
6811
+ const chunk = sentPayloads[index];
6812
+ const chunkText = typeof chunk === "string" ? chunk : chunk.msg;
6813
+ await persistOutgoingMessageBestEffort({
6814
+ profile,
6815
+ api,
6816
+ threadId,
6817
+ group: opts.group,
6818
+ text: chunkText,
6819
+ msgType: "text",
6820
+ response: responses[index],
6821
+ rawPayload: chunk
6822
+ });
6823
+ }
6452
6824
  });
6453
6825
  }
6454
6826
  })
6455
6827
  );
6828
+ msg.command("analyze-text <threadId> <message>").option("-g, --group", "Analyze as group text").option("--raw", "Analyze raw text without parsing formatting markers").option("-j, --json", "JSON output").description("Build and analyze the exact text payload that msg send would hand to zca-js. Useful for pre-send chunking/debugging.").action(
6829
+ wrapAction(async (threadId, message, opts, command) => {
6830
+ const threadType = asThreadType(opts.group);
6831
+ const mentionProbeText = opts.raw ? message : parseTextStyles(message).text;
6832
+ let listGroupMembers;
6833
+ if (threadType === ThreadType3.Group && hasPotentialOutboundGroupMention(mentionProbeText)) {
6834
+ const { api } = await requireApi(command);
6835
+ listGroupMembers = (groupId) => listGroupMentionMembers(api, groupId);
6836
+ }
6837
+ const analysis = await analyzeTextSendPayload({
6838
+ message,
6839
+ raw: opts.raw,
6840
+ threadType,
6841
+ threadId,
6842
+ listGroupMembers
6843
+ });
6844
+ output(analysis, shouldOutputJson(opts));
6845
+ })
6846
+ );
6456
6847
  msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (repeatable)", collectValues, []).option("-m, --message <message>", "Caption").option("-g, --group", "Send to group").description("Send image(s) from file or URL").action(
6457
6848
  wrapAction(
6458
6849
  async (threadId, file, opts, command) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {