openzca 0.1.20 → 0.1.22

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 +176 -5
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -703,6 +703,98 @@ function output(value, asJson = false) {
703
703
  function asThreadType(groupFlag) {
704
704
  return groupFlag ? ThreadType.Group : ThreadType.User;
705
705
  }
706
+ function parseBooleanFromEnv(name, fallback) {
707
+ const raw = process.env[name]?.trim();
708
+ if (!raw) return fallback;
709
+ const normalized = raw.toLowerCase();
710
+ if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") {
711
+ return true;
712
+ }
713
+ if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
714
+ return false;
715
+ }
716
+ return fallback;
717
+ }
718
+ function normalizeCachedId(value) {
719
+ if (typeof value === "string") {
720
+ const trimmed = value.trim();
721
+ return trimmed.length > 0 ? trimmed : null;
722
+ }
723
+ if (typeof value === "number" && Number.isFinite(value)) {
724
+ return String(Math.trunc(value));
725
+ }
726
+ return null;
727
+ }
728
+ function collectIdsFromCacheEntries(entries, keys) {
729
+ const ids = /* @__PURE__ */ new Set();
730
+ for (const entry of entries) {
731
+ if (!entry || typeof entry !== "object") continue;
732
+ const row = entry;
733
+ for (const key of keys) {
734
+ const normalized = normalizeCachedId(row[key]);
735
+ if (normalized) {
736
+ ids.add(normalized);
737
+ }
738
+ }
739
+ }
740
+ return ids;
741
+ }
742
+ async function resolveUploadThreadType(api, profile, threadId, groupFlag, command) {
743
+ if (groupFlag) {
744
+ return { type: ThreadType.Group, reason: "explicit_group_flag" };
745
+ }
746
+ const autoDetectEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_AUTO_THREAD_TYPE", true);
747
+ if (!autoDetectEnabled) {
748
+ return { type: ThreadType.User, reason: "auto_detect_disabled" };
749
+ }
750
+ try {
751
+ const cache = await readCache(profile);
752
+ const groupIds = collectIdsFromCacheEntries(cache.groups, ["groupId", "grid", "threadId", "id"]);
753
+ if (groupIds.has(threadId)) {
754
+ return { type: ThreadType.Group, reason: "cache_group_match" };
755
+ }
756
+ const friendIds = collectIdsFromCacheEntries(cache.friends, ["userId", "uid", "id", "threadId"]);
757
+ if (friendIds.has(threadId)) {
758
+ return { type: ThreadType.User, reason: "cache_friend_match" };
759
+ }
760
+ } catch (error) {
761
+ writeDebugLine(
762
+ "msg.upload.thread_type.cache_error",
763
+ {
764
+ profile,
765
+ threadId,
766
+ message: toErrorText(error)
767
+ },
768
+ command
769
+ );
770
+ }
771
+ const probeEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_GROUP_PROBE", true);
772
+ if (!probeEnabled) {
773
+ return { type: ThreadType.User, reason: "probe_disabled" };
774
+ }
775
+ const probeTimeoutMs = parsePositiveIntFromEnv("OPENZCA_UPLOAD_GROUP_PROBE_TIMEOUT_MS", 5e3);
776
+ try {
777
+ const groupInfo = await withTimeout(
778
+ api.getGroupInfo(threadId),
779
+ probeTimeoutMs,
780
+ `Timed out waiting ${probeTimeoutMs}ms while probing group thread type.`
781
+ );
782
+ if (groupInfo?.gridInfoMap?.[threadId]) {
783
+ return { type: ThreadType.Group, reason: "probe_group_match" };
784
+ }
785
+ } catch (error) {
786
+ writeDebugLine(
787
+ "msg.upload.thread_type.probe_error",
788
+ {
789
+ profile,
790
+ threadId,
791
+ message: toErrorText(error)
792
+ },
793
+ command
794
+ );
795
+ }
796
+ return { type: ThreadType.User, reason: "default_user" };
797
+ }
706
798
  function parseReaction(input) {
707
799
  const normalized = input.trim();
708
800
  if (EMOJI_REACTION_MAP[normalized]) {
@@ -820,6 +912,66 @@ function isListenerAlreadyStarted(error) {
820
912
  function toErrorText(error) {
821
913
  return error instanceof Error ? error.message : String(error);
822
914
  }
915
+ var SHUTDOWN_CALLBACKS = /* @__PURE__ */ new Set();
916
+ var shutdownSignalReceived = null;
917
+ var shutdownRunning = false;
918
+ function signalExitCode(signal) {
919
+ if (signal === "SIGINT") return 130;
920
+ if (signal === "SIGTERM") return 143;
921
+ return 1;
922
+ }
923
+ function registerShutdownCallback(callback) {
924
+ SHUTDOWN_CALLBACKS.add(callback);
925
+ return () => {
926
+ SHUTDOWN_CALLBACKS.delete(callback);
927
+ };
928
+ }
929
+ async function runShutdownCallbacks(signal) {
930
+ if (shutdownRunning) return;
931
+ shutdownRunning = true;
932
+ const callbacks = Array.from(SHUTDOWN_CALLBACKS);
933
+ SHUTDOWN_CALLBACKS.clear();
934
+ writeDebugLine(
935
+ "process.signal",
936
+ {
937
+ signal,
938
+ callbackCount: callbacks.length
939
+ },
940
+ void 0
941
+ );
942
+ for (const callback of callbacks) {
943
+ try {
944
+ await Promise.resolve(callback());
945
+ } catch (error) {
946
+ writeDebugLine(
947
+ "process.signal.callback_error",
948
+ {
949
+ signal,
950
+ message: toErrorText(error)
951
+ },
952
+ void 0
953
+ );
954
+ }
955
+ }
956
+ }
957
+ function installSignalHandler(signal) {
958
+ process.on(signal, () => {
959
+ if (shutdownSignalReceived) return;
960
+ shutdownSignalReceived = signal;
961
+ const exitCode = signalExitCode(signal);
962
+ const forceExitMs = parsePositiveIntFromEnv("OPENZCA_SIGNAL_FORCE_EXIT_MS", 1500);
963
+ const forceTimer = setTimeout(() => {
964
+ process.exit(exitCode);
965
+ }, forceExitMs);
966
+ forceTimer.unref();
967
+ void runShutdownCallbacks(signal).finally(() => {
968
+ clearTimeout(forceTimer);
969
+ process.exit(exitCode);
970
+ });
971
+ });
972
+ }
973
+ installSignalHandler("SIGINT");
974
+ installSignalHandler("SIGTERM");
823
975
  async function withTimeout(task, timeoutMs, message) {
824
976
  let timeoutId;
825
977
  try {
@@ -871,6 +1023,9 @@ async function withUploadListener(api, command, task) {
871
1023
  12e4
872
1024
  );
873
1025
  let startedHere = false;
1026
+ const unregisterSignalCleanup = registerShutdownCallback(async () => {
1027
+ await stopUploadListenerSafely(api, command);
1028
+ });
874
1029
  const sinkError = (error) => {
875
1030
  writeDebugLine(
876
1031
  "msg.upload.listener.error",
@@ -961,6 +1116,7 @@ async function withUploadListener(api, command, task) {
961
1116
  if (startedHere) {
962
1117
  await stopUploadListenerSafely(api, command);
963
1118
  }
1119
+ unregisterSignalCleanup();
964
1120
  api.listener.off("error", sinkError);
965
1121
  api.listener.off("closed", sinkClosed);
966
1122
  }
@@ -2270,18 +2426,28 @@ msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group"
2270
2426
  msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeatable)", collectValues, []).option("-g, --group", "Upload in group").description("Upload and send file(s)").action(
2271
2427
  wrapAction(
2272
2428
  async (arg1, arg2, opts, command) => {
2273
- const { api } = await requireApi(command);
2429
+ const { api, profile } = await requireApi(command);
2274
2430
  const inputs = normalizeInputList(opts.url);
2275
2431
  const urlInputs = inputs.filter((entry) => isHttpUrl(entry));
2276
2432
  const localInputs = inputs.filter((entry) => !isHttpUrl(entry));
2277
2433
  const [threadId, file] = arg2 ? [arg2, arg1] : [arg1, void 0];
2434
+ const threadResolution = await resolveUploadThreadType(
2435
+ api,
2436
+ profile,
2437
+ threadId,
2438
+ opts.group,
2439
+ command
2440
+ );
2278
2441
  const normalizedFile = file ? normalizeMediaInput(file) : void 0;
2279
2442
  const localFiles = [normalizedFile, ...localInputs].filter(Boolean);
2280
2443
  writeDebugLine(
2281
2444
  "msg.upload.inputs",
2282
2445
  {
2283
2446
  threadId,
2284
- isGroup: Boolean(opts.group),
2447
+ explicitGroupFlag: Boolean(opts.group),
2448
+ isGroup: threadResolution.type === ThreadType.Group,
2449
+ threadType: threadResolution.type === ThreadType.Group ? "group" : "user",
2450
+ threadTypeReason: threadResolution.reason,
2285
2451
  localFiles,
2286
2452
  urlInputs
2287
2453
  },
@@ -2305,7 +2471,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
2305
2471
  attachments
2306
2472
  },
2307
2473
  threadId,
2308
- asThreadType(opts.group)
2474
+ threadResolution.type
2309
2475
  )
2310
2476
  );
2311
2477
  output(response, false);
@@ -3258,6 +3424,8 @@ ${replyContextText}` : replyContextText;
3258
3424
  let recycleForceExitTimer = null;
3259
3425
  let heartbeatTimer = null;
3260
3426
  let recyclePendingExit = false;
3427
+ let unregisterShutdown = () => {
3428
+ };
3261
3429
  const finish = () => {
3262
3430
  if (settled) return;
3263
3431
  settled = true;
@@ -3273,6 +3441,9 @@ ${replyContextText}` : replyContextText;
3273
3441
  clearInterval(heartbeatTimer);
3274
3442
  heartbeatTimer = null;
3275
3443
  }
3444
+ unregisterShutdown();
3445
+ unregisterShutdown = () => {
3446
+ };
3276
3447
  resolve();
3277
3448
  };
3278
3449
  api.listener.on("closed", (code, reason) => {
@@ -3295,13 +3466,14 @@ ${replyContextText}` : replyContextText;
3295
3466
  finish();
3296
3467
  }
3297
3468
  });
3298
- const onSigint = () => {
3469
+ const onSignal = () => {
3299
3470
  try {
3300
3471
  api.listener.stop();
3301
3472
  } catch {
3302
3473
  }
3303
3474
  finish();
3304
3475
  };
3476
+ unregisterShutdown = registerShutdownCallback(onSignal);
3305
3477
  if (lifecycleEventsEnabled && heartbeatMs > 0) {
3306
3478
  heartbeatTimer = setInterval(() => {
3307
3479
  emitLifecycle("heartbeat");
@@ -3336,7 +3508,6 @@ ${replyContextText}` : replyContextText;
3336
3508
  finish();
3337
3509
  }, recycleMs);
3338
3510
  }
3339
- process.once("SIGINT", onSigint);
3340
3511
  api.listener.start({ retryOnClose: supervised ? false : Boolean(opts.keepAlive) });
3341
3512
  });
3342
3513
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {