openzca 0.1.54 → 0.1.56

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 (2) hide show
  1. package/dist/cli.js +338 -137
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2824,6 +2824,72 @@ import fs5 from "fs/promises";
2824
2824
  import os3 from "os";
2825
2825
  import path5 from "path";
2826
2826
  import { promisify } from "util";
2827
+
2828
+ // src/lib/send-retry.ts
2829
+ var DEFAULT_SEND_RETRY_COUNT = 1;
2830
+ var DEFAULT_SEND_RETRY_DELAY_MS = 750;
2831
+ var RETRYABLE_SEND_ERROR_PATTERNS = [
2832
+ /retry limit/i,
2833
+ /\btimeout\b/i,
2834
+ /\btimed out\b/i,
2835
+ /\betimedout\b/i,
2836
+ /\beconnreset\b/i,
2837
+ /\besockettimedout\b/i,
2838
+ /\bsocket hang up\b/i,
2839
+ /\btemporar(?:y|ily)\b/i
2840
+ ];
2841
+ function sleep(ms) {
2842
+ return new Promise((resolve) => {
2843
+ setTimeout(resolve, ms);
2844
+ });
2845
+ }
2846
+ function parsePositiveIntEnv(value, fallback) {
2847
+ const raw = value?.trim();
2848
+ if (!raw) return fallback;
2849
+ if (raw === "0") return 0;
2850
+ const parsed = Number.parseInt(raw, 10);
2851
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
2852
+ }
2853
+ function getSendRetryConfigFromEnv(env = process.env) {
2854
+ return {
2855
+ number: parsePositiveIntEnv(env.OPENZCA_SEND_RETRY_COUNT, DEFAULT_SEND_RETRY_COUNT),
2856
+ delayMs: parsePositiveIntEnv(env.OPENZCA_SEND_RETRY_DELAY_MS, DEFAULT_SEND_RETRY_DELAY_MS)
2857
+ };
2858
+ }
2859
+ function isRetryableSendError(error) {
2860
+ const message = error instanceof Error ? error.message : String(error ?? "");
2861
+ return RETRYABLE_SEND_ERROR_PATTERNS.some((pattern) => pattern.test(message));
2862
+ }
2863
+ function retryable(operation, options) {
2864
+ const maxRetries = Math.max(0, options?.number ?? DEFAULT_SEND_RETRY_COUNT);
2865
+ const delayMs = Math.max(0, options?.delayMs ?? DEFAULT_SEND_RETRY_DELAY_MS);
2866
+ const shouldRetry = options?.on ?? isRetryableSendError;
2867
+ return async (...args) => {
2868
+ let attempt = 0;
2869
+ while (true) {
2870
+ try {
2871
+ return await operation(...args);
2872
+ } catch (error) {
2873
+ attempt += 1;
2874
+ if (attempt > maxRetries || !shouldRetry(error)) {
2875
+ throw error;
2876
+ }
2877
+ await options?.onRetry?.({
2878
+ attempt,
2879
+ maxRetries,
2880
+ delayMs,
2881
+ error,
2882
+ args
2883
+ });
2884
+ if (delayMs > 0) {
2885
+ await sleep(delayMs);
2886
+ }
2887
+ }
2888
+ }
2889
+ };
2890
+ }
2891
+
2892
+ // src/lib/video-send.ts
2827
2893
  var execFileAsync = promisify(execFile);
2828
2894
  function planVideoSendMode(params) {
2829
2895
  const { files, ffmpegAvailable } = params;
@@ -2982,7 +3048,8 @@ async function sendNativeVideo(params) {
2982
3048
  try {
2983
3049
  const uploadedVideo = await params.api.uploadAttachment([params.videoPath], params.threadId, params.threadType);
2984
3050
  const uploadedThumbnail = await params.api.uploadAttachment([thumbnailPath], params.threadId, params.threadType);
2985
- return await params.api.sendVideo(
3051
+ const sendVideo = retryable(params.api.sendVideo.bind(params.api), getSendRetryConfigFromEnv());
3052
+ return await sendVideo(
2986
3053
  {
2987
3054
  msg: params.message ?? "",
2988
3055
  videoUrl: pickUploadedVideoUrl(uploadedVideo[0]),
@@ -3446,6 +3513,25 @@ function parsePositiveIntFromUnknown(value) {
3446
3513
  }
3447
3514
  return null;
3448
3515
  }
3516
+ function retrySendMethod(operation, command, metaBuilder) {
3517
+ const config = getSendRetryConfigFromEnv();
3518
+ return retryable(operation, {
3519
+ ...config,
3520
+ onRetry: ({ attempt, maxRetries, delayMs, error, args }) => {
3521
+ writeDebugLine(
3522
+ "send.retry",
3523
+ {
3524
+ ...metaBuilder(...args),
3525
+ attempt,
3526
+ maxRetries,
3527
+ delayMs,
3528
+ message: error instanceof Error ? error.message : String(error)
3529
+ },
3530
+ command
3531
+ );
3532
+ }
3533
+ });
3534
+ }
3449
3535
  function isProcessAlive(pid) {
3450
3536
  if (!Number.isInteger(pid) || pid <= 0) return false;
3451
3537
  try {
@@ -3598,6 +3684,15 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
3598
3684
  }
3599
3685
  const threadType = parsed.threadType === "group" ? ThreadType3.Group : ThreadType3.User;
3600
3686
  const requestTimeoutMs = parsePositiveIntFromUnknown(parsed.uploadTimeoutMs) ?? uploadTimeoutMs;
3687
+ const sendMessage = retrySendMethod(
3688
+ api.sendMessage.bind(api),
3689
+ command,
3690
+ (_payload, threadId, threadTypeArg) => ({
3691
+ kind: "listen.ipc.upload",
3692
+ threadId,
3693
+ threadType: threadTypeArg === ThreadType3.Group ? "group" : "user"
3694
+ })
3695
+ );
3601
3696
  writeDebugLine(
3602
3697
  "listen.ipc.upload.start",
3603
3698
  {
@@ -3613,7 +3708,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
3613
3708
  );
3614
3709
  try {
3615
3710
  const response = await withTimeout(
3616
- api.sendMessage(
3711
+ sendMessage(
3617
3712
  {
3618
3713
  msg: "",
3619
3714
  attachments: parsed.attachments
@@ -4414,6 +4509,19 @@ async function persistLiveDmContact(params) {
4414
4509
  rawJson
4415
4510
  });
4416
4511
  }
4512
+ function extractGroupTitle(record) {
4513
+ if (!record) {
4514
+ return void 0;
4515
+ }
4516
+ return typeof record.name === "string" && record.name.trim() ? record.name.trim() : typeof record.groupName === "string" && record.groupName.trim() ? record.groupName.trim() : void 0;
4517
+ }
4518
+ async function findGroupDirectoryEntry(api, groupId) {
4519
+ const groups = await buildGroupsDetailed(api);
4520
+ return groups.find((item) => {
4521
+ const record = item;
4522
+ return normalizeCachedId(record.groupId ?? record.grid ?? record.threadId ?? record.id) === groupId;
4523
+ });
4524
+ }
4417
4525
  async function hydrateUnknownLiveGroup(params) {
4418
4526
  const existing = await getThreadInfo({
4419
4527
  profile: params.profile,
@@ -4423,10 +4531,25 @@ async function hydrateUnknownLiveGroup(params) {
4423
4531
  if (existing && (existing.title || typeof existing.memberCount === "number" && existing.memberCount > 0)) {
4424
4532
  return;
4425
4533
  }
4534
+ let group2;
4535
+ let title = params.fallbackTitle?.trim() || void 0;
4426
4536
  try {
4427
4537
  const info = await params.api.getGroupInfo(params.groupId);
4428
- const group2 = info.gridInfoMap[params.groupId];
4429
- const title = typeof group2?.name === "string" && group2.name.trim() ? group2.name.trim() : typeof group2?.groupName === "string" && group2.groupName.trim() ? group2.groupName.trim() : params.fallbackTitle?.trim() || void 0;
4538
+ group2 = info.gridInfoMap[params.groupId];
4539
+ title = extractGroupTitle(group2) ?? title;
4540
+ } catch {
4541
+ }
4542
+ if (!group2 || !title) {
4543
+ try {
4544
+ const directoryGroup = await findGroupDirectoryEntry(params.api, params.groupId);
4545
+ if (directoryGroup) {
4546
+ group2 = group2 ?? directoryGroup;
4547
+ title = extractGroupTitle(directoryGroup) ?? title;
4548
+ }
4549
+ } catch {
4550
+ }
4551
+ }
4552
+ if (group2 || title) {
4430
4553
  await persistThread({
4431
4554
  profile: params.profile,
4432
4555
  scopeThreadId: params.groupId,
@@ -4435,17 +4558,20 @@ async function hydrateUnknownLiveGroup(params) {
4435
4558
  title,
4436
4559
  rawJson: group2 ? JSON.stringify(group2) : void 0
4437
4560
  });
4438
- await persistGroupMembersSnapshot(params.profile, params.groupId, params.api);
4439
- } catch {
4440
- if (params.fallbackTitle?.trim()) {
4441
- await persistThread({
4442
- profile: params.profile,
4443
- scopeThreadId: params.groupId,
4444
- rawThreadId: params.groupId,
4445
- threadType: "group",
4446
- title: params.fallbackTitle.trim()
4447
- });
4561
+ try {
4562
+ await persistGroupMembersSnapshot(params.profile, params.groupId, params.api);
4563
+ } catch {
4448
4564
  }
4565
+ return;
4566
+ }
4567
+ if (params.fallbackTitle?.trim()) {
4568
+ await persistThread({
4569
+ profile: params.profile,
4570
+ scopeThreadId: params.groupId,
4571
+ rawThreadId: params.groupId,
4572
+ threadType: "group",
4573
+ title: params.fallbackTitle.trim()
4574
+ });
4449
4575
  }
4450
4576
  }
4451
4577
  async function syncDbGroupHistoryFull(params) {
@@ -4654,128 +4780,132 @@ async function syncDbChatsBestEffort(params) {
4654
4780
  }
4655
4781
  async function runDbSync(params) {
4656
4782
  const { profile, api } = await requireApi(params.command);
4657
- const dbPath = await resolveDbPath(profile);
4658
- params.progress?.(`starting sync for profile ${profile}`);
4659
- const summary = createDbSyncSummary(
4660
- profile,
4661
- dbPath,
4662
- params.mode === "all" || params.mode === "chats" || params.mode === "chat" ? params.count : void 0
4663
- );
4664
- const selfId = api.getOwnId();
4665
- const selfInfo = normalizeMeInfoOutput(await api.fetchAccountInfo());
4666
- await persistSelfProfile({
4667
- profile,
4668
- userId: selfId,
4669
- displayName: typeof selfInfo.displayName === "string" && selfInfo.displayName.trim() ? selfInfo.displayName.trim() : void 0,
4670
- infoJson: JSON.stringify(selfInfo)
4671
- });
4672
- const { pinnedIds, hiddenIds } = await collectConversationIds(api);
4673
- let friendNames = /* @__PURE__ */ new Map();
4674
- if (params.mode === "all" || params.mode === "friends" || params.mode === "chats") {
4675
- friendNames = await syncDbFriendDirectory({
4783
+ try {
4784
+ const dbPath = await resolveDbPath(profile);
4785
+ params.progress?.(`starting sync for profile ${profile}`);
4786
+ const summary = createDbSyncSummary(
4676
4787
  profile,
4677
- api,
4678
- summary,
4679
- progress: params.progress
4788
+ dbPath,
4789
+ params.mode === "all" || params.mode === "chats" || params.mode === "chat" ? params.count : void 0
4790
+ );
4791
+ const selfId = api.getOwnId();
4792
+ const selfInfo = normalizeMeInfoOutput(await api.fetchAccountInfo());
4793
+ await persistSelfProfile({
4794
+ profile,
4795
+ userId: selfId,
4796
+ displayName: typeof selfInfo.displayName === "string" && selfInfo.displayName.trim() ? selfInfo.displayName.trim() : void 0,
4797
+ infoJson: JSON.stringify(selfInfo)
4680
4798
  });
4681
- }
4682
- if (params.mode === "all" || params.mode === "groups") {
4683
- const groups = await buildGroupsDetailed(api);
4684
- const targetGroupIds = /* @__PURE__ */ new Set();
4685
- const titleById = /* @__PURE__ */ new Map();
4686
- for (const group2 of groups) {
4687
- const record = group2;
4688
- const groupId = normalizeCachedId(record.groupId);
4689
- if (!groupId) continue;
4690
- const title = typeof record.name === "string" && record.name.trim() ? record.name.trim() : typeof record.groupName === "string" && record.groupName.trim() ? record.groupName.trim() : void 0;
4691
- targetGroupIds.add(groupId);
4692
- titleById.set(groupId, title);
4799
+ const { pinnedIds, hiddenIds } = await collectConversationIds(api);
4800
+ let friendNames = /* @__PURE__ */ new Map();
4801
+ if (params.mode === "all" || params.mode === "friends" || params.mode === "chats") {
4802
+ friendNames = await syncDbFriendDirectory({
4803
+ profile,
4804
+ api,
4805
+ summary,
4806
+ progress: params.progress
4807
+ });
4808
+ }
4809
+ if (params.mode === "all" || params.mode === "groups") {
4810
+ const groups = await buildGroupsDetailed(api);
4811
+ const targetGroupIds = /* @__PURE__ */ new Set();
4812
+ const titleById = /* @__PURE__ */ new Map();
4813
+ for (const group2 of groups) {
4814
+ const record = group2;
4815
+ const groupId = normalizeCachedId(record.groupId);
4816
+ if (!groupId) continue;
4817
+ const title = typeof record.name === "string" && record.name.trim() ? record.name.trim() : typeof record.groupName === "string" && record.groupName.trim() ? record.groupName.trim() : void 0;
4818
+ targetGroupIds.add(groupId);
4819
+ titleById.set(groupId, title);
4820
+ await prepareDbGroupTarget({
4821
+ profile,
4822
+ api,
4823
+ groupId,
4824
+ title,
4825
+ rawJson: JSON.stringify(group2),
4826
+ pinnedIds,
4827
+ hiddenIds
4828
+ });
4829
+ }
4830
+ await syncDbGroupHistoryFull({
4831
+ profile,
4832
+ api,
4833
+ selfId,
4834
+ targetGroupIds,
4835
+ titleById,
4836
+ summary,
4837
+ progress: params.progress
4838
+ });
4839
+ }
4840
+ if (params.mode === "group") {
4841
+ if (!params.groupId) {
4842
+ throw new Error("Missing group id for db sync group.");
4843
+ }
4844
+ const groupInfo = await api.getGroupInfo(params.groupId);
4845
+ const group2 = groupInfo.gridInfoMap[params.groupId];
4846
+ const title = typeof group2?.name === "string" && group2.name.trim() ? group2.name.trim() : void 0;
4693
4847
  await prepareDbGroupTarget({
4694
4848
  profile,
4695
4849
  api,
4696
- groupId,
4850
+ groupId: params.groupId,
4697
4851
  title,
4698
- rawJson: JSON.stringify(group2),
4852
+ rawJson: group2 ? JSON.stringify(group2) : void 0,
4699
4853
  pinnedIds,
4700
4854
  hiddenIds
4701
4855
  });
4856
+ await syncDbGroupHistoryFull({
4857
+ profile,
4858
+ api,
4859
+ selfId,
4860
+ targetGroupIds: /* @__PURE__ */ new Set([params.groupId]),
4861
+ titleById: /* @__PURE__ */ new Map([[params.groupId, title]]),
4862
+ summary,
4863
+ progress: params.progress
4864
+ });
4702
4865
  }
4703
- await syncDbGroupHistoryFull({
4704
- profile,
4705
- api,
4706
- selfId,
4707
- targetGroupIds,
4708
- titleById,
4709
- summary,
4710
- progress: params.progress
4711
- });
4712
- }
4713
- if (params.mode === "group") {
4714
- if (!params.groupId) {
4715
- throw new Error("Missing group id for db sync group.");
4716
- }
4717
- const groupInfo = await api.getGroupInfo(params.groupId);
4718
- const group2 = groupInfo.gridInfoMap[params.groupId];
4719
- const title = typeof group2?.name === "string" && group2.name.trim() ? group2.name.trim() : void 0;
4720
- await prepareDbGroupTarget({
4721
- profile,
4722
- api,
4723
- groupId: params.groupId,
4724
- title,
4725
- rawJson: group2 ? JSON.stringify(group2) : void 0,
4726
- pinnedIds,
4727
- hiddenIds
4728
- });
4729
- await syncDbGroupHistoryFull({
4730
- profile,
4731
- api,
4732
- selfId,
4733
- targetGroupIds: /* @__PURE__ */ new Set([params.groupId]),
4734
- titleById: /* @__PURE__ */ new Map([[params.groupId, title]]),
4735
- summary,
4736
- progress: params.progress
4737
- });
4738
- }
4739
- if (params.mode === "chat") {
4740
- if (!params.threadId) {
4741
- throw new Error("Missing chat id for db sync chat.");
4742
- }
4743
- if (friendNames.size === 0) {
4744
- friendNames = await persistFriendDirectory(profile, api);
4866
+ if (params.mode === "chat") {
4867
+ if (!params.threadId) {
4868
+ throw new Error("Missing chat id for db sync chat.");
4869
+ }
4870
+ if (friendNames.size === 0) {
4871
+ friendNames = await persistFriendDirectory(profile, api);
4872
+ }
4873
+ await syncDbChatThread({
4874
+ profile,
4875
+ api,
4876
+ selfId,
4877
+ threadId: params.threadId,
4878
+ count: params.count,
4879
+ title: friendNames.get(params.threadId),
4880
+ pinnedIds,
4881
+ hiddenIds,
4882
+ summary,
4883
+ progress: params.progress
4884
+ });
4745
4885
  }
4746
- await syncDbChatThread({
4747
- profile,
4748
- api,
4749
- selfId,
4750
- threadId: params.threadId,
4751
- count: params.count,
4752
- title: friendNames.get(params.threadId),
4753
- pinnedIds,
4754
- hiddenIds,
4755
- summary,
4756
- progress: params.progress
4757
- });
4758
- }
4759
- if (params.mode === "all" || params.mode === "chats") {
4760
- if (friendNames.size === 0) {
4761
- friendNames = await persistFriendDirectory(profile, api);
4886
+ if (params.mode === "all" || params.mode === "chats") {
4887
+ if (friendNames.size === 0) {
4888
+ friendNames = await persistFriendDirectory(profile, api);
4889
+ }
4890
+ await syncDbChatsBestEffort({
4891
+ profile,
4892
+ api,
4893
+ selfId,
4894
+ count: params.count,
4895
+ titleById: friendNames,
4896
+ pinnedIds,
4897
+ hiddenIds,
4898
+ summary,
4899
+ progress: params.progress
4900
+ });
4762
4901
  }
4763
- await syncDbChatsBestEffort({
4764
- profile,
4765
- api,
4766
- selfId,
4767
- count: params.count,
4768
- titleById: friendNames,
4769
- pinnedIds,
4770
- hiddenIds,
4771
- summary,
4772
- progress: params.progress
4773
- });
4902
+ params.progress?.(
4903
+ `done: groups=${summary.groupsSynced}, groupMessages=${summary.groupMessagesImported}, friends=${summary.friendsSynced}, chats=${summary.chatsSynced}, dmMessages=${summary.dmMessagesImported}`
4904
+ );
4905
+ return summary;
4906
+ } finally {
4907
+ await closeDb(profile);
4774
4908
  }
4775
- params.progress?.(
4776
- `done: groups=${summary.groupsSynced}, groupMessages=${summary.groupMessagesImported}, friends=${summary.friendsSynced}, chats=${summary.chatsSynced}, dmMessages=${summary.dmMessagesImported}`
4777
- );
4778
- return summary;
4779
4909
  }
4780
4910
  async function buildGroupsDetailed(api) {
4781
4911
  const groups = await api.getAllGroups();
@@ -5678,7 +5808,7 @@ async function parseCredentialFile(filePath) {
5678
5808
  language: parsed.language
5679
5809
  };
5680
5810
  }
5681
- function sleep(ms) {
5811
+ function sleep2(ms) {
5682
5812
  return new Promise((resolve) => {
5683
5813
  setTimeout(resolve, ms);
5684
5814
  });
@@ -5693,7 +5823,7 @@ async function waitForFileContent(filePath, timeoutMs) {
5693
5823
  }
5694
5824
  } catch {
5695
5825
  }
5696
- await sleep(150);
5826
+ await sleep2(150);
5697
5827
  }
5698
5828
  throw new Error(`Timed out waiting for QR image file: ${filePath}`);
5699
5829
  }
@@ -7046,6 +7176,15 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
7046
7176
  )
7047
7177
  });
7048
7178
  const payloadChunks = deliveryPlan.chunks;
7179
+ const sendMessage = retrySendMethod(
7180
+ api.sendMessage.bind(api),
7181
+ command,
7182
+ (_payload, targetThreadId, targetThreadType) => ({
7183
+ kind: "msg.send",
7184
+ threadId: targetThreadId,
7185
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user"
7186
+ })
7187
+ );
7049
7188
  const responses = [];
7050
7189
  const sentPayloads = [];
7051
7190
  for (let index = 0; index < payloadChunks.length; index += 1) {
@@ -7055,7 +7194,7 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
7055
7194
  quote
7056
7195
  } : chunk;
7057
7196
  sentPayloads.push(chunkPayload);
7058
- responses.push(await api.sendMessage(chunkPayload, threadId, threadType));
7197
+ responses.push(await sendMessage(chunkPayload, threadId, threadType));
7059
7198
  }
7060
7199
  const response = responses.length === 1 ? responses[0] : {
7061
7200
  chunked: true,
@@ -7120,6 +7259,16 @@ msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (rep
7120
7259
  wrapAction(
7121
7260
  async (threadId, file, opts, command) => {
7122
7261
  const { api, profile } = await requireApi(command);
7262
+ const sendMessage = retrySendMethod(
7263
+ api.sendMessage.bind(api),
7264
+ command,
7265
+ (payload, targetThreadId, targetThreadType) => ({
7266
+ kind: "msg.image",
7267
+ threadId: targetThreadId,
7268
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user",
7269
+ attachmentCount: payload && typeof payload === "object" && Array.isArray(payload.attachments) ? payload.attachments.length : void 0
7270
+ })
7271
+ );
7123
7272
  const normalizedFile = file ? normalizeMediaInput(file) : void 0;
7124
7273
  const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
7125
7274
  const urlInputs = files.filter((entry) => isHttpUrl(entry));
@@ -7141,7 +7290,7 @@ msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (rep
7141
7290
  throw new Error("Provide at least one image file or --url.");
7142
7291
  }
7143
7292
  await assertFilesExist(attachments);
7144
- const response = await api.sendMessage(
7293
+ const response = await sendMessage(
7145
7294
  {
7146
7295
  msg: opts.message ?? "",
7147
7296
  attachments
@@ -7183,6 +7332,16 @@ msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (rep
7183
7332
  async (threadId, file, opts, command) => {
7184
7333
  const { api, profile } = await requireApi(command);
7185
7334
  const threadType = asThreadType(opts.group);
7335
+ const sendMessage = retrySendMethod(
7336
+ api.sendMessage.bind(api),
7337
+ command,
7338
+ (payload, targetThreadId, targetThreadType) => ({
7339
+ kind: "msg.video.fallback",
7340
+ threadId: targetThreadId,
7341
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user",
7342
+ attachmentCount: payload && typeof payload === "object" && Array.isArray(payload.attachments) ? payload.attachments.length : void 0
7343
+ })
7344
+ );
7186
7345
  const normalizedFile = file ? normalizeMediaInput(file) : void 0;
7187
7346
  const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
7188
7347
  const urlInputs = files.filter((entry) => isHttpUrl(entry));
@@ -7304,7 +7463,7 @@ msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (rep
7304
7463
  const response = await withUploadListener(
7305
7464
  api,
7306
7465
  command,
7307
- async () => api.sendMessage(
7466
+ async () => sendMessage(
7308
7467
  {
7309
7468
  msg: opts.message ?? "",
7310
7469
  attachments
@@ -7348,6 +7507,15 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
7348
7507
  async (threadId, file, opts, command) => {
7349
7508
  const { api, profile } = await requireApi(command);
7350
7509
  const type = asThreadType(opts.group);
7510
+ const sendVoice = retrySendMethod(
7511
+ api.sendVoice.bind(api),
7512
+ command,
7513
+ (_payload, targetThreadId, targetThreadType) => ({
7514
+ kind: "msg.voice",
7515
+ threadId: targetThreadId,
7516
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user"
7517
+ })
7518
+ );
7351
7519
  const normalizedFile = file ? normalizeMediaInput(file) : void 0;
7352
7520
  const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
7353
7521
  if (files.length === 0) {
@@ -7373,7 +7541,7 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
7373
7541
  const uploaded = await api.uploadAttachment(attachments, threadId, type);
7374
7542
  for (const item of uploaded) {
7375
7543
  if (item.fileType === "others" || item.fileType === "video") {
7376
- results.push(await api.sendVoice({ voiceUrl: item.fileUrl }, threadId, type));
7544
+ results.push(await sendVoice({ voiceUrl: item.fileUrl }, threadId, type));
7377
7545
  }
7378
7546
  }
7379
7547
  if (results.length === 0) {
@@ -7431,7 +7599,16 @@ msg.command("sticker <threadId> <stickerId>").option("-g, --group", "Send to gro
7431
7599
  msg.command("link <threadId> <url>").option("-g, --group", "Send to group").description("Send link").action(
7432
7600
  wrapAction(async (threadId, url, opts, command) => {
7433
7601
  const { api, profile } = await requireApi(command);
7434
- const response = await api.sendLink({ link: url }, threadId, asThreadType(opts.group));
7602
+ const sendLink = retrySendMethod(
7603
+ api.sendLink.bind(api),
7604
+ command,
7605
+ (_payload, targetThreadId, targetThreadType) => ({
7606
+ kind: "msg.link",
7607
+ threadId: targetThreadId,
7608
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user"
7609
+ })
7610
+ );
7611
+ const response = await sendLink({ link: url }, threadId, asThreadType(opts.group));
7435
7612
  output(response, false);
7436
7613
  if (await shouldWriteToDb(profile)) {
7437
7614
  scheduleDbWrite(profile, command, "msg.link.db.persist_error", async () => {
@@ -7543,6 +7720,15 @@ msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group"
7543
7720
  async (msgId, cliMsgId, threadId, message, opts, command) => {
7544
7721
  const { api } = await requireApi(command);
7545
7722
  const type = asThreadType(opts.group);
7723
+ const sendMessage = retrySendMethod(
7724
+ api.sendMessage.bind(api),
7725
+ command,
7726
+ (_payload, targetThreadId, targetThreadType) => ({
7727
+ kind: "msg.edit.resend",
7728
+ threadId: targetThreadId,
7729
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user"
7730
+ })
7731
+ );
7546
7732
  const undoResponse = await api.undo(
7547
7733
  {
7548
7734
  msgId,
@@ -7551,7 +7737,7 @@ msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group"
7551
7737
  threadId,
7552
7738
  type
7553
7739
  );
7554
- const sendResponse = await api.sendMessage(message, threadId, type);
7740
+ const sendResponse = await sendMessage(message, threadId, type);
7555
7741
  output(
7556
7742
  {
7557
7743
  mode: "undo+send",
@@ -7568,6 +7754,16 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
7568
7754
  wrapAction(
7569
7755
  async (arg1, arg2, opts, command) => {
7570
7756
  const { api, profile } = await requireApi(command);
7757
+ const sendMessage = retrySendMethod(
7758
+ api.sendMessage.bind(api),
7759
+ command,
7760
+ (payload, targetThreadId, targetThreadType) => ({
7761
+ kind: "msg.upload",
7762
+ threadId: targetThreadId,
7763
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user",
7764
+ attachmentCount: payload && typeof payload === "object" && Array.isArray(payload.attachments) ? payload.attachments.length : void 0
7765
+ })
7766
+ );
7571
7767
  const inputs = normalizeInputList(opts.url);
7572
7768
  const urlInputs = inputs.filter((entry) => isHttpUrl(entry));
7573
7769
  const localInputs = inputs.filter((entry) => !isHttpUrl(entry));
@@ -7643,7 +7839,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
7643
7839
  const response = await withUploadListener(
7644
7840
  api,
7645
7841
  command,
7646
- async () => api.sendMessage(
7842
+ async () => sendMessage(
7647
7843
  {
7648
7844
  msg: "",
7649
7845
  attachments
@@ -8740,12 +8936,17 @@ ${replyContextText}` : replyContextText;
8740
8936
  }
8741
8937
  await emitWebhook(payload);
8742
8938
  if (opts.echo && rawText.trim().length > 0) {
8939
+ const sendMessage = retrySendMethod(
8940
+ api.sendMessage.bind(api),
8941
+ command,
8942
+ (_sendPayload, targetThreadId, targetThreadType) => ({
8943
+ kind: "listen.echo",
8944
+ threadId: targetThreadId,
8945
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user"
8946
+ })
8947
+ );
8743
8948
  try {
8744
- await api.sendMessage(
8745
- { msg: processedText },
8746
- message.threadId,
8747
- message.type
8748
- );
8949
+ await sendMessage({ msg: processedText }, message.threadId, message.type);
8749
8950
  } catch (error) {
8750
8951
  console.error(
8751
8952
  `Echo failed: ${error instanceof Error ? error.message : String(error)}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {