openzca 0.1.55 → 0.1.57

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 +466 -74
  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]),
@@ -3224,6 +3291,125 @@ function inferReplyMessageThreadId(params) {
3224
3291
  return void 0;
3225
3292
  }
3226
3293
 
3294
+ // src/lib/adaptive-batch.ts
3295
+ var RETRYABLE_LOOKUP_ERROR_PATTERNS = [
3296
+ /retry limit/i,
3297
+ /\brate limit/i,
3298
+ /\btoo many requests?\b/i,
3299
+ /\btimeout\b/i,
3300
+ /\btimed out\b/i,
3301
+ /\betimedout\b/i,
3302
+ /\beconnreset\b/i,
3303
+ /\besockettimedout\b/i,
3304
+ /\bsocket hang up\b/i,
3305
+ /\btemporar(?:y|ily)\b/i
3306
+ ];
3307
+ var SPLITTABLE_LOOKUP_ERROR_PATTERNS = [
3308
+ /\binvalid param(?:eter)?s?\b/i,
3309
+ /\binvalid request\b/i,
3310
+ /\bbad request\b/i,
3311
+ /tham so khong hop le/i,
3312
+ /tham số không hợp lệ/i
3313
+ ];
3314
+ function sleep2(ms) {
3315
+ return new Promise((resolve) => {
3316
+ setTimeout(resolve, ms);
3317
+ });
3318
+ }
3319
+ function chunkKeys(keys, size) {
3320
+ const chunkSize = Math.max(1, Math.trunc(size) || 1);
3321
+ const chunks = [];
3322
+ for (let index = 0; index < keys.length; index += chunkSize) {
3323
+ chunks.push(keys.slice(index, index + chunkSize));
3324
+ }
3325
+ return chunks;
3326
+ }
3327
+ function toErrorText(error) {
3328
+ return error instanceof Error ? error.message : String(error);
3329
+ }
3330
+ function isRetryableLookupError(error) {
3331
+ const message = toErrorText(error);
3332
+ return RETRYABLE_LOOKUP_ERROR_PATTERNS.some((pattern) => pattern.test(message));
3333
+ }
3334
+ function isSplittableLookupError(error) {
3335
+ const message = toErrorText(error);
3336
+ return SPLITTABLE_LOOKUP_ERROR_PATTERNS.some((pattern) => pattern.test(message));
3337
+ }
3338
+ async function runAdaptiveBatch(keys, options) {
3339
+ const maxRetries = Math.max(0, options.maxRetries ?? 2);
3340
+ const initialDelayMs = Math.max(0, options.retryDelayMs ?? 400);
3341
+ const backoffMultiplier = Math.max(1, options.backoffMultiplier ?? 2);
3342
+ const shouldRetry = options.shouldRetry ?? isRetryableLookupError;
3343
+ const shouldSplit = options.shouldSplit ?? isSplittableLookupError;
3344
+ let attempt = 0;
3345
+ let delayMs = initialDelayMs;
3346
+ while (true) {
3347
+ try {
3348
+ return await options.fetchBatch(keys) ?? {};
3349
+ } catch (error) {
3350
+ if (keys.length > 1 && shouldSplit(error)) {
3351
+ throw error;
3352
+ }
3353
+ attempt += 1;
3354
+ if (attempt > maxRetries || !shouldRetry(error)) {
3355
+ throw error;
3356
+ }
3357
+ await options.onRetry?.({
3358
+ keys: [...keys],
3359
+ attempt,
3360
+ maxRetries,
3361
+ delayMs,
3362
+ error
3363
+ });
3364
+ if (delayMs > 0) {
3365
+ await sleep2(delayMs);
3366
+ }
3367
+ delayMs = Math.max(delayMs * backoffMultiplier, delayMs + 1);
3368
+ }
3369
+ }
3370
+ }
3371
+ async function fetchAdaptiveObjectBatches(keys, options) {
3372
+ const uniqueKeys = Array.from(new Set(keys.map((value) => value.trim()).filter(Boolean)));
3373
+ const pending = chunkKeys(uniqueKeys, options.initialBatchSize ?? 5);
3374
+ const values = /* @__PURE__ */ new Map();
3375
+ const errors = [];
3376
+ const shouldRetry = options.shouldRetry ?? isRetryableLookupError;
3377
+ const shouldSplit = options.shouldSplit ?? isSplittableLookupError;
3378
+ const continueOnItemError = options.continueOnItemError ?? true;
3379
+ const batchDelayMs = Math.max(0, options.batchDelayMs ?? 75);
3380
+ while (pending.length > 0) {
3381
+ const batch = pending.shift();
3382
+ if (!batch || batch.length === 0) {
3383
+ continue;
3384
+ }
3385
+ try {
3386
+ const result = await runAdaptiveBatch(batch, options);
3387
+ for (const key of batch) {
3388
+ const value = result[key];
3389
+ if (value !== void 0) {
3390
+ values.set(key, value);
3391
+ }
3392
+ }
3393
+ } catch (error) {
3394
+ if (batch.length > 1 && (shouldSplit(error) || shouldRetry(error))) {
3395
+ pending.unshift(...chunkKeys(batch, Math.ceil(batch.length / 2)));
3396
+ continue;
3397
+ }
3398
+ if (!continueOnItemError || batch.length > 1) {
3399
+ throw error;
3400
+ }
3401
+ const itemError = { key: batch[0], error };
3402
+ errors.push(itemError);
3403
+ await options.onItemError?.(itemError);
3404
+ continue;
3405
+ }
3406
+ if (batchDelayMs > 0 && pending.length > 0) {
3407
+ await sleep2(batchDelayMs);
3408
+ }
3409
+ }
3410
+ return { values, errors };
3411
+ }
3412
+
3227
3413
  // src/cli.ts
3228
3414
  var require3 = createRequire2(import.meta.url);
3229
3415
  var { version: PKG_VERSION } = require3("../package.json");
@@ -3446,6 +3632,63 @@ function parsePositiveIntFromUnknown(value) {
3446
3632
  }
3447
3633
  return null;
3448
3634
  }
3635
+ function retrySendMethod(operation, command, metaBuilder) {
3636
+ const config = getSendRetryConfigFromEnv();
3637
+ return retryable(operation, {
3638
+ ...config,
3639
+ onRetry: ({ attempt, maxRetries, delayMs, error, args }) => {
3640
+ writeDebugLine(
3641
+ "send.retry",
3642
+ {
3643
+ ...metaBuilder(...args),
3644
+ attempt,
3645
+ maxRetries,
3646
+ delayMs,
3647
+ message: error instanceof Error ? error.message : String(error)
3648
+ },
3649
+ command
3650
+ );
3651
+ }
3652
+ });
3653
+ }
3654
+ var LOOKUP_BATCH_SIZE = 5;
3655
+ var LOOKUP_RETRY_COUNT = 2;
3656
+ var LOOKUP_RETRY_DELAY_MS = 400;
3657
+ var LOOKUP_BATCH_DELAY_MS = 75;
3658
+ async function fetchGroupInfoRecords(api, groupIds) {
3659
+ const { values } = await fetchAdaptiveObjectBatches(groupIds, {
3660
+ fetchBatch: async (keys) => {
3661
+ const response = await api.getGroupInfo(keys);
3662
+ return response.gridInfoMap ?? {};
3663
+ },
3664
+ initialBatchSize: LOOKUP_BATCH_SIZE,
3665
+ maxRetries: LOOKUP_RETRY_COUNT,
3666
+ retryDelayMs: LOOKUP_RETRY_DELAY_MS,
3667
+ batchDelayMs: LOOKUP_BATCH_DELAY_MS
3668
+ });
3669
+ return values;
3670
+ }
3671
+ async function fetchGroupInfoRecord(api, groupId) {
3672
+ const groups = await fetchGroupInfoRecords(api, [groupId]);
3673
+ const group2 = groups.get(groupId);
3674
+ if (!group2) {
3675
+ throw new Error(`Group not found: ${groupId}`);
3676
+ }
3677
+ return group2;
3678
+ }
3679
+ async function fetchGroupMemberProfiles(api, memberIds) {
3680
+ const { values } = await fetchAdaptiveObjectBatches(memberIds, {
3681
+ fetchBatch: async (keys) => {
3682
+ const response = await api.getGroupMembersInfo(keys);
3683
+ return response.profiles ?? {};
3684
+ },
3685
+ initialBatchSize: LOOKUP_BATCH_SIZE,
3686
+ maxRetries: LOOKUP_RETRY_COUNT,
3687
+ retryDelayMs: LOOKUP_RETRY_DELAY_MS,
3688
+ batchDelayMs: LOOKUP_BATCH_DELAY_MS
3689
+ });
3690
+ return values;
3691
+ }
3449
3692
  function isProcessAlive(pid) {
3450
3693
  if (!Number.isInteger(pid) || pid <= 0) return false;
3451
3694
  try {
@@ -3598,6 +3841,15 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
3598
3841
  }
3599
3842
  const threadType = parsed.threadType === "group" ? ThreadType3.Group : ThreadType3.User;
3600
3843
  const requestTimeoutMs = parsePositiveIntFromUnknown(parsed.uploadTimeoutMs) ?? uploadTimeoutMs;
3844
+ const sendMessage = retrySendMethod(
3845
+ api.sendMessage.bind(api),
3846
+ command,
3847
+ (_payload, threadId, threadTypeArg) => ({
3848
+ kind: "listen.ipc.upload",
3849
+ threadId,
3850
+ threadType: threadTypeArg === ThreadType3.Group ? "group" : "user"
3851
+ })
3852
+ );
3601
3853
  writeDebugLine(
3602
3854
  "listen.ipc.upload.start",
3603
3855
  {
@@ -3613,7 +3865,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
3613
3865
  );
3614
3866
  try {
3615
3867
  const response = await withTimeout(
3616
- api.sendMessage(
3868
+ sendMessage(
3617
3869
  {
3618
3870
  msg: "",
3619
3871
  attachments: parsed.attachments
@@ -3642,7 +3894,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
3642
3894
  command
3643
3895
  );
3644
3896
  } catch (error) {
3645
- fail(parsed.requestId, toErrorText(error));
3897
+ fail(parsed.requestId, toErrorText2(error));
3646
3898
  writeDebugLine(
3647
3899
  "listen.ipc.upload.error",
3648
3900
  {
@@ -3651,7 +3903,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
3651
3903
  requestId: parsed.requestId,
3652
3904
  threadId: parsed.threadId,
3653
3905
  threadType: parsed.threadType,
3654
- message: toErrorText(error)
3906
+ message: toErrorText2(error)
3655
3907
  },
3656
3908
  command
3657
3909
  );
@@ -3680,7 +3932,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
3680
3932
  {
3681
3933
  profile,
3682
3934
  sessionId,
3683
- message: toErrorText(error)
3935
+ message: toErrorText2(error)
3684
3936
  },
3685
3937
  command
3686
3938
  );
@@ -3692,7 +3944,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
3692
3944
  {
3693
3945
  profile,
3694
3946
  sessionId,
3695
- message: toErrorText(error)
3947
+ message: toErrorText2(error)
3696
3948
  },
3697
3949
  command
3698
3950
  );
@@ -3881,7 +4133,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
3881
4133
  {
3882
4134
  profile,
3883
4135
  threadId,
3884
- message: toErrorText(error)
4136
+ message: toErrorText2(error)
3885
4137
  },
3886
4138
  command
3887
4139
  );
@@ -3906,7 +4158,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
3906
4158
  {
3907
4159
  profile,
3908
4160
  threadId,
3909
- message: toErrorText(error)
4161
+ message: toErrorText2(error)
3910
4162
  },
3911
4163
  command
3912
4164
  );
@@ -4147,8 +4399,8 @@ async function persistOutgoingMessageBestEffort(params) {
4147
4399
  });
4148
4400
  }
4149
4401
  }
4150
- async function persistGroupMembersSnapshot(profile, groupId, api) {
4151
- const rows = await listGroupMemberRows(api, groupId);
4402
+ async function persistGroupMembersSnapshot(profile, groupId, api, groupInfo) {
4403
+ const rows = await listGroupMemberRows(api, groupId, groupInfo);
4152
4404
  const snapshotAtMs = Date.now();
4153
4405
  for (const row of rows) {
4154
4406
  await persistContact({
@@ -4353,7 +4605,15 @@ async function prepareDbGroupTarget(params) {
4353
4605
  isHidden: params.hiddenIds.has(params.groupId),
4354
4606
  rawJson: params.rawJson
4355
4607
  });
4356
- await persistGroupMembersSnapshot(params.profile, params.groupId, params.api);
4608
+ if (params.hydrateMembers === false) {
4609
+ return {};
4610
+ }
4611
+ try {
4612
+ await persistGroupMembersSnapshot(params.profile, params.groupId, params.api, params.group);
4613
+ return {};
4614
+ } catch (error) {
4615
+ return { memberSnapshotError: toErrorText2(error) };
4616
+ }
4357
4617
  }
4358
4618
  function resolveContactDisplayName(params) {
4359
4619
  return params.displayName?.trim() || params.zaloName?.trim() || params.fallbackTitle?.trim() || params.userId.trim() || void 0;
@@ -4455,18 +4715,17 @@ async function hydrateUnknownLiveGroup(params) {
4455
4715
  }
4456
4716
  }
4457
4717
  if (group2 || title) {
4458
- await persistThread({
4718
+ await prepareDbGroupTarget({
4459
4719
  profile: params.profile,
4460
- scopeThreadId: params.groupId,
4461
- rawThreadId: params.groupId,
4462
- threadType: "group",
4720
+ api: params.api,
4721
+ groupId: params.groupId,
4722
+ group: group2,
4463
4723
  title,
4464
- rawJson: group2 ? JSON.stringify(group2) : void 0
4724
+ rawJson: group2 ? JSON.stringify(group2) : void 0,
4725
+ pinnedIds: /* @__PURE__ */ new Set(),
4726
+ hiddenIds: /* @__PURE__ */ new Set(),
4727
+ hydrateMembers: Boolean(group2)
4465
4728
  });
4466
- try {
4467
- await persistGroupMembersSnapshot(params.profile, params.groupId, params.api);
4468
- } catch {
4469
- }
4470
4729
  return;
4471
4730
  }
4472
4731
  if (params.fallbackTitle?.trim()) {
@@ -4536,16 +4795,23 @@ async function syncDbGroupHistoryFull(params) {
4536
4795
  pagesRequested = result.pagesRequested;
4537
4796
  listenerImportedCount = await getStoredGroupMessageCount() - beforeCount;
4538
4797
  } catch (error) {
4539
- stopReason = `fallback_window:${toErrorText(error)}`;
4798
+ stopReason = `fallback_window:${toErrorText2(error)}`;
4540
4799
  completeness = "window";
4541
4800
  }
4542
4801
  const fallbackCount = 200;
4543
4802
  params.progress?.(`merging recent group API window (${fallbackCount} per group)`);
4544
4803
  const beforeApiCount = await getStoredGroupMessageCount();
4804
+ const topoffErrors = [];
4545
4805
  for (const groupId of params.targetGroupIds) {
4546
- const messages = await fetchRecentGroupMessagesViaApi(params.api, groupId, fallbackCount);
4547
- await persistMessages(messages);
4548
- params.progress?.(`group ${groupId}: fetched ${messages.length} message(s) from group history API`);
4806
+ try {
4807
+ const messages = await fetchRecentGroupMessagesViaApi(params.api, groupId, fallbackCount);
4808
+ await persistMessages(messages);
4809
+ params.progress?.(`group ${groupId}: fetched ${messages.length} message(s) from group history API`);
4810
+ } catch (error) {
4811
+ const message = toErrorText2(error);
4812
+ topoffErrors.push({ groupId, error: message });
4813
+ params.progress?.(`group ${groupId}: group history API skipped (${message})`);
4814
+ }
4549
4815
  }
4550
4816
  const afterCount = await getStoredGroupMessageCount();
4551
4817
  const apiAddedCount = afterCount - beforeApiCount;
@@ -4575,7 +4841,8 @@ async function syncDbGroupHistoryFull(params) {
4575
4841
  imported,
4576
4842
  completeness,
4577
4843
  stopReason,
4578
- pagesRequested
4844
+ pagesRequested,
4845
+ topoffErrors
4579
4846
  });
4580
4847
  }
4581
4848
  async function syncDbFriendDirectory(params) {
@@ -4712,25 +4979,70 @@ async function runDbSync(params) {
4712
4979
  });
4713
4980
  }
4714
4981
  if (params.mode === "all" || params.mode === "groups") {
4715
- const groups = await buildGroupsDetailed(api);
4982
+ const groups = await api.getAllGroups();
4983
+ const groupIds = Object.keys(groups.gridVerMap ?? {});
4716
4984
  const targetGroupIds = /* @__PURE__ */ new Set();
4717
4985
  const titleById = /* @__PURE__ */ new Map();
4718
- for (const group2 of groups) {
4719
- const record = group2;
4720
- const groupId = normalizeCachedId(record.groupId);
4721
- if (!groupId) continue;
4722
- const title = typeof record.name === "string" && record.name.trim() ? record.name.trim() : typeof record.groupName === "string" && record.groupName.trim() ? record.groupName.trim() : void 0;
4723
- targetGroupIds.add(groupId);
4724
- titleById.set(groupId, title);
4725
- await prepareDbGroupTarget({
4726
- profile,
4727
- api,
4728
- groupId,
4729
- title,
4730
- rawJson: JSON.stringify(group2),
4731
- pinnedIds,
4732
- hiddenIds
4733
- });
4986
+ params.progress?.(`syncing group directory for ${groupIds.length} group(s)`);
4987
+ for (const groupId of groupIds) {
4988
+ let group2;
4989
+ let title;
4990
+ try {
4991
+ try {
4992
+ group2 = await fetchGroupInfoRecord(api, groupId);
4993
+ title = extractGroupTitle(group2);
4994
+ } catch (error) {
4995
+ const message = toErrorText2(error);
4996
+ params.progress?.(`group ${groupId}: metadata unavailable (${message}), continuing`);
4997
+ summary.syncState.push({
4998
+ kind: "group",
4999
+ groupId,
5000
+ status: "warning",
5001
+ stage: "metadata",
5002
+ error: message
5003
+ });
5004
+ }
5005
+ const { memberSnapshotError } = await prepareDbGroupTarget({
5006
+ profile,
5007
+ api,
5008
+ groupId,
5009
+ group: group2,
5010
+ title,
5011
+ rawJson: group2 ? JSON.stringify(group2) : void 0,
5012
+ pinnedIds,
5013
+ hiddenIds,
5014
+ hydrateMembers: Boolean(group2)
5015
+ });
5016
+ if (memberSnapshotError) {
5017
+ params.progress?.(`group ${groupId}: member snapshot unavailable (${memberSnapshotError}), continuing`);
5018
+ summary.syncState.push({
5019
+ kind: "group",
5020
+ groupId,
5021
+ status: "warning",
5022
+ stage: "members",
5023
+ error: memberSnapshotError
5024
+ });
5025
+ }
5026
+ targetGroupIds.add(groupId);
5027
+ titleById.set(groupId, title);
5028
+ } catch (error) {
5029
+ const message = toErrorText2(error);
5030
+ params.progress?.(`group ${groupId}: skipped (${message})`);
5031
+ await setSyncState({
5032
+ profile,
5033
+ scopeThreadId: groupId,
5034
+ threadType: "group",
5035
+ status: "error",
5036
+ error: message
5037
+ });
5038
+ summary.syncState.push({
5039
+ kind: "group",
5040
+ groupId,
5041
+ status: "error",
5042
+ stage: "prepare",
5043
+ error: message
5044
+ });
5045
+ }
4734
5046
  }
4735
5047
  await syncDbGroupHistoryFull({
4736
5048
  profile,
@@ -4746,18 +5058,29 @@ async function runDbSync(params) {
4746
5058
  if (!params.groupId) {
4747
5059
  throw new Error("Missing group id for db sync group.");
4748
5060
  }
4749
- const groupInfo = await api.getGroupInfo(params.groupId);
4750
- const group2 = groupInfo.gridInfoMap[params.groupId];
4751
- const title = typeof group2?.name === "string" && group2.name.trim() ? group2.name.trim() : void 0;
4752
- await prepareDbGroupTarget({
5061
+ const group2 = await fetchGroupInfoRecord(api, params.groupId);
5062
+ const title = extractGroupTitle(group2);
5063
+ const { memberSnapshotError } = await prepareDbGroupTarget({
4753
5064
  profile,
4754
5065
  api,
4755
5066
  groupId: params.groupId,
5067
+ group: group2,
4756
5068
  title,
4757
5069
  rawJson: group2 ? JSON.stringify(group2) : void 0,
4758
5070
  pinnedIds,
4759
- hiddenIds
5071
+ hiddenIds,
5072
+ hydrateMembers: Boolean(group2)
4760
5073
  });
5074
+ if (memberSnapshotError) {
5075
+ params.progress?.(`group ${params.groupId}: member snapshot unavailable (${memberSnapshotError}), continuing`);
5076
+ summary.syncState.push({
5077
+ kind: "group",
5078
+ groupId: params.groupId,
5079
+ status: "warning",
5080
+ stage: "members",
5081
+ error: memberSnapshotError
5082
+ });
5083
+ }
4761
5084
  await syncDbGroupHistoryFull({
4762
5085
  profile,
4763
5086
  api,
@@ -4816,8 +5139,8 @@ async function buildGroupsDetailed(api) {
4816
5139
  const groups = await api.getAllGroups();
4817
5140
  const ids = Object.keys(groups.gridVerMap ?? {});
4818
5141
  if (ids.length === 0) return [];
4819
- const info = await api.getGroupInfo(ids);
4820
- return ids.map((id) => info.gridInfoMap?.[id]).filter((item) => Boolean(item));
5142
+ const info = await fetchGroupInfoRecords(api, ids);
5143
+ return ids.map((id) => info.get(id)).filter((item) => Boolean(item));
4821
5144
  }
4822
5145
  function normalizeGroupMemberId(value) {
4823
5146
  if (typeof value === "number" && Number.isFinite(value)) {
@@ -4828,9 +5151,8 @@ function normalizeGroupMemberId(value) {
4828
5151
  if (!trimmed) return "";
4829
5152
  return trimmed.replace(/_\d+$/, "");
4830
5153
  }
4831
- async function listGroupMemberRows(api, groupId) {
4832
- const info = await api.getGroupInfo(groupId);
4833
- const groupInfo = info.gridInfoMap[groupId];
5154
+ async function listGroupMemberRows(api, groupId, preloadedGroupInfo) {
5155
+ const groupInfo = preloadedGroupInfo ?? await fetchGroupInfoRecord(api, groupId);
4834
5156
  if (!groupInfo) {
4835
5157
  throw new Error(`Group not found: ${groupId}`);
4836
5158
  }
@@ -4854,10 +5176,9 @@ async function listGroupMemberRows(api, groupId) {
4854
5176
  ...Array.from(currentMemberMap.keys())
4855
5177
  ])
4856
5178
  );
4857
- const profiles = ids.length > 0 ? await api.getGroupMembersInfo(ids) : { profiles: {} };
4858
- const rawProfileMap = profiles.profiles;
5179
+ const profileLookup = ids.length > 0 ? await fetchGroupMemberProfiles(api, ids) : /* @__PURE__ */ new Map();
4859
5180
  const profileMap = /* @__PURE__ */ new Map();
4860
- for (const [key, profile] of Object.entries(rawProfileMap)) {
5181
+ for (const [key, profile] of profileLookup.entries()) {
4861
5182
  if (!profile) continue;
4862
5183
  const normalizedKey = normalizeGroupMemberId(key);
4863
5184
  if (normalizedKey && !profileMap.has(normalizedKey)) {
@@ -4912,7 +5233,7 @@ function isListenerAlreadyStarted(error) {
4912
5233
  if (!(error instanceof Error)) return false;
4913
5234
  return /already started/i.test(error.message);
4914
5235
  }
4915
- function toErrorText(error) {
5236
+ function toErrorText2(error) {
4916
5237
  return error instanceof Error ? error.message : String(error);
4917
5238
  }
4918
5239
  var SHUTDOWN_CALLBACKS = /* @__PURE__ */ new Set();
@@ -4950,7 +5271,7 @@ async function runShutdownCallbacks(signal) {
4950
5271
  "process.signal.callback_error",
4951
5272
  {
4952
5273
  signal,
4953
- message: toErrorText(error)
5274
+ message: toErrorText2(error)
4954
5275
  },
4955
5276
  void 0
4956
5277
  );
@@ -5033,7 +5354,7 @@ async function withUploadListener(api, command, task) {
5033
5354
  writeDebugLine(
5034
5355
  "msg.upload.listener.error",
5035
5356
  {
5036
- message: toErrorText(error)
5357
+ message: toErrorText2(error)
5037
5358
  },
5038
5359
  command
5039
5360
  );
@@ -5075,7 +5396,7 @@ async function withUploadListener(api, command, task) {
5075
5396
  finish();
5076
5397
  };
5077
5398
  const onConnectError = (error) => {
5078
- finish(new Error(`Upload listener connection error: ${toErrorText(error)}`));
5399
+ finish(new Error(`Upload listener connection error: ${toErrorText2(error)}`));
5079
5400
  };
5080
5401
  const onConnectClosed = (code, reason) => {
5081
5402
  finish(
@@ -5713,7 +6034,7 @@ async function parseCredentialFile(filePath) {
5713
6034
  language: parsed.language
5714
6035
  };
5715
6036
  }
5716
- function sleep(ms) {
6037
+ function sleep3(ms) {
5717
6038
  return new Promise((resolve) => {
5718
6039
  setTimeout(resolve, ms);
5719
6040
  });
@@ -5728,7 +6049,7 @@ async function waitForFileContent(filePath, timeoutMs) {
5728
6049
  }
5729
6050
  } catch {
5730
6051
  }
5731
- await sleep(150);
6052
+ await sleep3(150);
5732
6053
  }
5733
6054
  throw new Error(`Timed out waiting for QR image file: ${filePath}`);
5734
6055
  }
@@ -7081,6 +7402,15 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
7081
7402
  )
7082
7403
  });
7083
7404
  const payloadChunks = deliveryPlan.chunks;
7405
+ const sendMessage = retrySendMethod(
7406
+ api.sendMessage.bind(api),
7407
+ command,
7408
+ (_payload, targetThreadId, targetThreadType) => ({
7409
+ kind: "msg.send",
7410
+ threadId: targetThreadId,
7411
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user"
7412
+ })
7413
+ );
7084
7414
  const responses = [];
7085
7415
  const sentPayloads = [];
7086
7416
  for (let index = 0; index < payloadChunks.length; index += 1) {
@@ -7090,7 +7420,7 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
7090
7420
  quote
7091
7421
  } : chunk;
7092
7422
  sentPayloads.push(chunkPayload);
7093
- responses.push(await api.sendMessage(chunkPayload, threadId, threadType));
7423
+ responses.push(await sendMessage(chunkPayload, threadId, threadType));
7094
7424
  }
7095
7425
  const response = responses.length === 1 ? responses[0] : {
7096
7426
  chunked: true,
@@ -7155,6 +7485,16 @@ msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (rep
7155
7485
  wrapAction(
7156
7486
  async (threadId, file, opts, command) => {
7157
7487
  const { api, profile } = await requireApi(command);
7488
+ const sendMessage = retrySendMethod(
7489
+ api.sendMessage.bind(api),
7490
+ command,
7491
+ (payload, targetThreadId, targetThreadType) => ({
7492
+ kind: "msg.image",
7493
+ threadId: targetThreadId,
7494
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user",
7495
+ attachmentCount: payload && typeof payload === "object" && Array.isArray(payload.attachments) ? payload.attachments.length : void 0
7496
+ })
7497
+ );
7158
7498
  const normalizedFile = file ? normalizeMediaInput(file) : void 0;
7159
7499
  const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
7160
7500
  const urlInputs = files.filter((entry) => isHttpUrl(entry));
@@ -7176,7 +7516,7 @@ msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (rep
7176
7516
  throw new Error("Provide at least one image file or --url.");
7177
7517
  }
7178
7518
  await assertFilesExist(attachments);
7179
- const response = await api.sendMessage(
7519
+ const response = await sendMessage(
7180
7520
  {
7181
7521
  msg: opts.message ?? "",
7182
7522
  attachments
@@ -7218,6 +7558,16 @@ msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (rep
7218
7558
  async (threadId, file, opts, command) => {
7219
7559
  const { api, profile } = await requireApi(command);
7220
7560
  const threadType = asThreadType(opts.group);
7561
+ const sendMessage = retrySendMethod(
7562
+ api.sendMessage.bind(api),
7563
+ command,
7564
+ (payload, targetThreadId, targetThreadType) => ({
7565
+ kind: "msg.video.fallback",
7566
+ threadId: targetThreadId,
7567
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user",
7568
+ attachmentCount: payload && typeof payload === "object" && Array.isArray(payload.attachments) ? payload.attachments.length : void 0
7569
+ })
7570
+ );
7221
7571
  const normalizedFile = file ? normalizeMediaInput(file) : void 0;
7222
7572
  const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
7223
7573
  const urlInputs = files.filter((entry) => isHttpUrl(entry));
@@ -7339,7 +7689,7 @@ msg.command("video <threadId> [file]").option("-u, --url <url>", "Video URL (rep
7339
7689
  const response = await withUploadListener(
7340
7690
  api,
7341
7691
  command,
7342
- async () => api.sendMessage(
7692
+ async () => sendMessage(
7343
7693
  {
7344
7694
  msg: opts.message ?? "",
7345
7695
  attachments
@@ -7383,6 +7733,15 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
7383
7733
  async (threadId, file, opts, command) => {
7384
7734
  const { api, profile } = await requireApi(command);
7385
7735
  const type = asThreadType(opts.group);
7736
+ const sendVoice = retrySendMethod(
7737
+ api.sendVoice.bind(api),
7738
+ command,
7739
+ (_payload, targetThreadId, targetThreadType) => ({
7740
+ kind: "msg.voice",
7741
+ threadId: targetThreadId,
7742
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user"
7743
+ })
7744
+ );
7386
7745
  const normalizedFile = file ? normalizeMediaInput(file) : void 0;
7387
7746
  const files = [normalizedFile, ...normalizeInputList(opts.url)].filter(Boolean);
7388
7747
  if (files.length === 0) {
@@ -7408,7 +7767,7 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
7408
7767
  const uploaded = await api.uploadAttachment(attachments, threadId, type);
7409
7768
  for (const item of uploaded) {
7410
7769
  if (item.fileType === "others" || item.fileType === "video") {
7411
- results.push(await api.sendVoice({ voiceUrl: item.fileUrl }, threadId, type));
7770
+ results.push(await sendVoice({ voiceUrl: item.fileUrl }, threadId, type));
7412
7771
  }
7413
7772
  }
7414
7773
  if (results.length === 0) {
@@ -7466,7 +7825,16 @@ msg.command("sticker <threadId> <stickerId>").option("-g, --group", "Send to gro
7466
7825
  msg.command("link <threadId> <url>").option("-g, --group", "Send to group").description("Send link").action(
7467
7826
  wrapAction(async (threadId, url, opts, command) => {
7468
7827
  const { api, profile } = await requireApi(command);
7469
- const response = await api.sendLink({ link: url }, threadId, asThreadType(opts.group));
7828
+ const sendLink = retrySendMethod(
7829
+ api.sendLink.bind(api),
7830
+ command,
7831
+ (_payload, targetThreadId, targetThreadType) => ({
7832
+ kind: "msg.link",
7833
+ threadId: targetThreadId,
7834
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user"
7835
+ })
7836
+ );
7837
+ const response = await sendLink({ link: url }, threadId, asThreadType(opts.group));
7470
7838
  output(response, false);
7471
7839
  if (await shouldWriteToDb(profile)) {
7472
7840
  scheduleDbWrite(profile, command, "msg.link.db.persist_error", async () => {
@@ -7578,6 +7946,15 @@ msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group"
7578
7946
  async (msgId, cliMsgId, threadId, message, opts, command) => {
7579
7947
  const { api } = await requireApi(command);
7580
7948
  const type = asThreadType(opts.group);
7949
+ const sendMessage = retrySendMethod(
7950
+ api.sendMessage.bind(api),
7951
+ command,
7952
+ (_payload, targetThreadId, targetThreadType) => ({
7953
+ kind: "msg.edit.resend",
7954
+ threadId: targetThreadId,
7955
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user"
7956
+ })
7957
+ );
7581
7958
  const undoResponse = await api.undo(
7582
7959
  {
7583
7960
  msgId,
@@ -7586,7 +7963,7 @@ msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group"
7586
7963
  threadId,
7587
7964
  type
7588
7965
  );
7589
- const sendResponse = await api.sendMessage(message, threadId, type);
7966
+ const sendResponse = await sendMessage(message, threadId, type);
7590
7967
  output(
7591
7968
  {
7592
7969
  mode: "undo+send",
@@ -7603,6 +7980,16 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
7603
7980
  wrapAction(
7604
7981
  async (arg1, arg2, opts, command) => {
7605
7982
  const { api, profile } = await requireApi(command);
7983
+ const sendMessage = retrySendMethod(
7984
+ api.sendMessage.bind(api),
7985
+ command,
7986
+ (payload, targetThreadId, targetThreadType) => ({
7987
+ kind: "msg.upload",
7988
+ threadId: targetThreadId,
7989
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user",
7990
+ attachmentCount: payload && typeof payload === "object" && Array.isArray(payload.attachments) ? payload.attachments.length : void 0
7991
+ })
7992
+ );
7606
7993
  const inputs = normalizeInputList(opts.url);
7607
7994
  const urlInputs = inputs.filter((entry) => isHttpUrl(entry));
7608
7995
  const localInputs = inputs.filter((entry) => !isHttpUrl(entry));
@@ -7678,7 +8065,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
7678
8065
  const response = await withUploadListener(
7679
8066
  api,
7680
8067
  command,
7681
- async () => api.sendMessage(
8068
+ async () => sendMessage(
7682
8069
  {
7683
8070
  msg: "",
7684
8071
  attachments
@@ -8775,12 +9162,17 @@ ${replyContextText}` : replyContextText;
8775
9162
  }
8776
9163
  await emitWebhook(payload);
8777
9164
  if (opts.echo && rawText.trim().length > 0) {
9165
+ const sendMessage = retrySendMethod(
9166
+ api.sendMessage.bind(api),
9167
+ command,
9168
+ (_sendPayload, targetThreadId, targetThreadType) => ({
9169
+ kind: "listen.echo",
9170
+ threadId: targetThreadId,
9171
+ threadType: targetThreadType === ThreadType3.Group ? "group" : "user"
9172
+ })
9173
+ );
8778
9174
  try {
8779
- await api.sendMessage(
8780
- { msg: processedText },
8781
- message.threadId,
8782
- message.type
8783
- );
9175
+ await sendMessage({ msg: processedText }, message.threadId, message.type);
8784
9176
  } catch (error) {
8785
9177
  console.error(
8786
9178
  `Echo failed: ${error instanceof Error ? error.message : String(error)}`
@@ -8886,7 +9278,7 @@ ${replyContextText}` : replyContextText;
8886
9278
  code,
8887
9279
  reason: reason || void 0,
8888
9280
  delayMs: keepAliveRestartDelayMs,
8889
- message: toErrorText(error),
9281
+ message: toErrorText2(error),
8890
9282
  sessionId
8891
9283
  },
8892
9284
  command
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.55",
3
+ "version": "0.1.57",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {