openzca 0.1.55 → 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 +182 -16
  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
@@ -5713,7 +5808,7 @@ async function parseCredentialFile(filePath) {
5713
5808
  language: parsed.language
5714
5809
  };
5715
5810
  }
5716
- function sleep(ms) {
5811
+ function sleep2(ms) {
5717
5812
  return new Promise((resolve) => {
5718
5813
  setTimeout(resolve, ms);
5719
5814
  });
@@ -5728,7 +5823,7 @@ async function waitForFileContent(filePath, timeoutMs) {
5728
5823
  }
5729
5824
  } catch {
5730
5825
  }
5731
- await sleep(150);
5826
+ await sleep2(150);
5732
5827
  }
5733
5828
  throw new Error(`Timed out waiting for QR image file: ${filePath}`);
5734
5829
  }
@@ -7081,6 +7176,15 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
7081
7176
  )
7082
7177
  });
7083
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
+ );
7084
7188
  const responses = [];
7085
7189
  const sentPayloads = [];
7086
7190
  for (let index = 0; index < payloadChunks.length; index += 1) {
@@ -7090,7 +7194,7 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
7090
7194
  quote
7091
7195
  } : chunk;
7092
7196
  sentPayloads.push(chunkPayload);
7093
- responses.push(await api.sendMessage(chunkPayload, threadId, threadType));
7197
+ responses.push(await sendMessage(chunkPayload, threadId, threadType));
7094
7198
  }
7095
7199
  const response = responses.length === 1 ? responses[0] : {
7096
7200
  chunked: true,
@@ -7155,6 +7259,16 @@ msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (rep
7155
7259
  wrapAction(
7156
7260
  async (threadId, file, opts, command) => {
7157
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
+ );
7158
7272
  const normalizedFile = file ? normalizeMediaInput(file) : void 0;
7159
7273
  const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
7160
7274
  const urlInputs = files.filter((entry) => isHttpUrl(entry));
@@ -7176,7 +7290,7 @@ msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (rep
7176
7290
  throw new Error("Provide at least one image file or --url.");
7177
7291
  }
7178
7292
  await assertFilesExist(attachments);
7179
- const response = await api.sendMessage(
7293
+ const response = await sendMessage(
7180
7294
  {
7181
7295
  msg: opts.message ?? "",
7182
7296
  attachments
@@ -7218,6 +7332,16 @@ msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (rep
7218
7332
  async (threadId, file, opts, command) => {
7219
7333
  const { api, profile } = await requireApi(command);
7220
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
+ );
7221
7345
  const normalizedFile = file ? normalizeMediaInput(file) : void 0;
7222
7346
  const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
7223
7347
  const urlInputs = files.filter((entry) => isHttpUrl(entry));
@@ -7339,7 +7463,7 @@ msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (rep
7339
7463
  const response = await withUploadListener(
7340
7464
  api,
7341
7465
  command,
7342
- async () => api.sendMessage(
7466
+ async () => sendMessage(
7343
7467
  {
7344
7468
  msg: opts.message ?? "",
7345
7469
  attachments
@@ -7383,6 +7507,15 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
7383
7507
  async (threadId, file, opts, command) => {
7384
7508
  const { api, profile } = await requireApi(command);
7385
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
+ );
7386
7519
  const normalizedFile = file ? normalizeMediaInput(file) : void 0;
7387
7520
  const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
7388
7521
  if (files.length === 0) {
@@ -7408,7 +7541,7 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
7408
7541
  const uploaded = await api.uploadAttachment(attachments, threadId, type);
7409
7542
  for (const item of uploaded) {
7410
7543
  if (item.fileType === "others" || item.fileType === "video") {
7411
- results.push(await api.sendVoice({ voiceUrl: item.fileUrl }, threadId, type));
7544
+ results.push(await sendVoice({ voiceUrl: item.fileUrl }, threadId, type));
7412
7545
  }
7413
7546
  }
7414
7547
  if (results.length === 0) {
@@ -7466,7 +7599,16 @@ msg.command("sticker <threadId> <stickerId>").option("-g, --group", "Send to gro
7466
7599
  msg.command("link <threadId> <url>").option("-g, --group", "Send to group").description("Send link").action(
7467
7600
  wrapAction(async (threadId, url, opts, command) => {
7468
7601
  const { api, profile } = await requireApi(command);
7469
- 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));
7470
7612
  output(response, false);
7471
7613
  if (await shouldWriteToDb(profile)) {
7472
7614
  scheduleDbWrite(profile, command, "msg.link.db.persist_error", async () => {
@@ -7578,6 +7720,15 @@ msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group"
7578
7720
  async (msgId, cliMsgId, threadId, message, opts, command) => {
7579
7721
  const { api } = await requireApi(command);
7580
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
+ );
7581
7732
  const undoResponse = await api.undo(
7582
7733
  {
7583
7734
  msgId,
@@ -7586,7 +7737,7 @@ msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group"
7586
7737
  threadId,
7587
7738
  type
7588
7739
  );
7589
- const sendResponse = await api.sendMessage(message, threadId, type);
7740
+ const sendResponse = await sendMessage(message, threadId, type);
7590
7741
  output(
7591
7742
  {
7592
7743
  mode: "undo+send",
@@ -7603,6 +7754,16 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
7603
7754
  wrapAction(
7604
7755
  async (arg1, arg2, opts, command) => {
7605
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
+ );
7606
7767
  const inputs = normalizeInputList(opts.url);
7607
7768
  const urlInputs = inputs.filter((entry) => isHttpUrl(entry));
7608
7769
  const localInputs = inputs.filter((entry) => !isHttpUrl(entry));
@@ -7678,7 +7839,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
7678
7839
  const response = await withUploadListener(
7679
7840
  api,
7680
7841
  command,
7681
- async () => api.sendMessage(
7842
+ async () => sendMessage(
7682
7843
  {
7683
7844
  msg: "",
7684
7845
  attachments
@@ -8775,12 +8936,17 @@ ${replyContextText}` : replyContextText;
8775
8936
  }
8776
8937
  await emitWebhook(payload);
8777
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
+ );
8778
8948
  try {
8779
- await api.sendMessage(
8780
- { msg: processedText },
8781
- message.threadId,
8782
- message.type
8783
- );
8949
+ await sendMessage({ msg: processedText }, message.threadId, message.type);
8784
8950
  } catch (error) {
8785
8951
  console.error(
8786
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.55",
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": {