openzca 0.1.21 → 0.1.24
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.
- package/README.md +26 -0
- package/dist/cli.js +985 -313
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createRequire } from "module";
|
|
|
5
5
|
import { spawn as spawn2 } from "child_process";
|
|
6
6
|
import fsSync from "fs";
|
|
7
7
|
import fs4 from "fs/promises";
|
|
8
|
+
import net from "net";
|
|
8
9
|
import os3 from "os";
|
|
9
10
|
import path4 from "path";
|
|
10
11
|
import util from "util";
|
|
@@ -703,6 +704,531 @@ function output(value, asJson = false) {
|
|
|
703
704
|
function asThreadType(groupFlag) {
|
|
704
705
|
return groupFlag ? ThreadType.Group : ThreadType.User;
|
|
705
706
|
}
|
|
707
|
+
function parseBooleanFromEnv(name, fallback) {
|
|
708
|
+
const raw = process.env[name]?.trim();
|
|
709
|
+
if (!raw) return fallback;
|
|
710
|
+
const normalized = raw.toLowerCase();
|
|
711
|
+
if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") {
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
return fallback;
|
|
718
|
+
}
|
|
719
|
+
function normalizeCachedId(value) {
|
|
720
|
+
if (typeof value === "string") {
|
|
721
|
+
const trimmed = value.trim();
|
|
722
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
723
|
+
}
|
|
724
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
725
|
+
return String(Math.trunc(value));
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
function collectIdsFromCacheEntries(entries, keys) {
|
|
730
|
+
const ids = /* @__PURE__ */ new Set();
|
|
731
|
+
for (const entry of entries) {
|
|
732
|
+
if (!entry || typeof entry !== "object") continue;
|
|
733
|
+
const row = entry;
|
|
734
|
+
for (const key of keys) {
|
|
735
|
+
const normalized = normalizeCachedId(row[key]);
|
|
736
|
+
if (normalized) {
|
|
737
|
+
ids.add(normalized);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return ids;
|
|
742
|
+
}
|
|
743
|
+
function getListenerOwnerLockPath(profile) {
|
|
744
|
+
return path4.join(getProfileDir(profile), "listener-owner.json");
|
|
745
|
+
}
|
|
746
|
+
function getListenIpcSocketPath(profile) {
|
|
747
|
+
if (process.platform === "win32") {
|
|
748
|
+
const safe = profile.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
749
|
+
return `\\\\.\\pipe\\openzca-listen-${safe}`;
|
|
750
|
+
}
|
|
751
|
+
return path4.join(getProfileDir(profile), "listen.sock");
|
|
752
|
+
}
|
|
753
|
+
function parsePositiveIntFromUnknown(value) {
|
|
754
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
755
|
+
return Math.trunc(value);
|
|
756
|
+
}
|
|
757
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
758
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
759
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
760
|
+
return parsed;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
function isProcessAlive(pid) {
|
|
766
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
767
|
+
try {
|
|
768
|
+
process.kill(pid, 0);
|
|
769
|
+
return true;
|
|
770
|
+
} catch (error) {
|
|
771
|
+
const code = error.code;
|
|
772
|
+
if (code === "EPERM") return true;
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async function readListenerOwnerRecord(lockPath) {
|
|
777
|
+
try {
|
|
778
|
+
const raw = await fs4.readFile(lockPath, "utf8");
|
|
779
|
+
const parsed = JSON.parse(raw);
|
|
780
|
+
const pid = parsePositiveIntFromUnknown(parsed.pid);
|
|
781
|
+
if (!pid) return null;
|
|
782
|
+
return {
|
|
783
|
+
pid,
|
|
784
|
+
profile: String(parsed.profile ?? ""),
|
|
785
|
+
sessionId: typeof parsed.sessionId === "string" ? parsed.sessionId : void 0,
|
|
786
|
+
startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : ""
|
|
787
|
+
};
|
|
788
|
+
} catch (error) {
|
|
789
|
+
const code = error.code;
|
|
790
|
+
if (code === "ENOENT") return null;
|
|
791
|
+
throw error;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async function readActiveListenerOwner(profile) {
|
|
795
|
+
const lockPath = getListenerOwnerLockPath(profile);
|
|
796
|
+
const record = await readListenerOwnerRecord(lockPath);
|
|
797
|
+
if (!record) {
|
|
798
|
+
await fs4.rm(lockPath, { force: true });
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
if (!isProcessAlive(record.pid)) {
|
|
802
|
+
await fs4.rm(lockPath, { force: true });
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
return record;
|
|
806
|
+
}
|
|
807
|
+
async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
808
|
+
await ensureProfile(profile);
|
|
809
|
+
const lockPath = getListenerOwnerLockPath(profile);
|
|
810
|
+
const record = {
|
|
811
|
+
pid: process.pid,
|
|
812
|
+
profile,
|
|
813
|
+
sessionId,
|
|
814
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
815
|
+
};
|
|
816
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
817
|
+
try {
|
|
818
|
+
await fs4.writeFile(lockPath, `${JSON.stringify(record, null, 2)}
|
|
819
|
+
`, {
|
|
820
|
+
encoding: "utf8",
|
|
821
|
+
flag: "wx"
|
|
822
|
+
});
|
|
823
|
+
let released = false;
|
|
824
|
+
return {
|
|
825
|
+
lockPath,
|
|
826
|
+
release: async () => {
|
|
827
|
+
if (released) return;
|
|
828
|
+
released = true;
|
|
829
|
+
const current = await readListenerOwnerRecord(lockPath);
|
|
830
|
+
if (current && current.pid !== process.pid) return;
|
|
831
|
+
await fs4.rm(lockPath, { force: true });
|
|
832
|
+
writeDebugLine(
|
|
833
|
+
"listen.owner.released",
|
|
834
|
+
{
|
|
835
|
+
profile,
|
|
836
|
+
lockPath,
|
|
837
|
+
pid: process.pid
|
|
838
|
+
},
|
|
839
|
+
command
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
} catch (error) {
|
|
844
|
+
const code = error.code;
|
|
845
|
+
if (code !== "EEXIST") throw error;
|
|
846
|
+
const owner = await readActiveListenerOwner(profile);
|
|
847
|
+
if (owner) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`Another openzca listener already owns profile "${profile}" (pid ${owner.pid}).`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
await fs4.rm(lockPath, { force: true });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
throw new Error(`Unable to acquire listener ownership for profile "${profile}".`);
|
|
856
|
+
}
|
|
857
|
+
async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
858
|
+
if (!parseBooleanFromEnv("OPENZCA_LISTEN_IPC", true)) {
|
|
859
|
+
writeDebugLine(
|
|
860
|
+
"listen.ipc.disabled",
|
|
861
|
+
{
|
|
862
|
+
profile
|
|
863
|
+
},
|
|
864
|
+
command
|
|
865
|
+
);
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
const socketPath = getListenIpcSocketPath(profile);
|
|
869
|
+
if (process.platform !== "win32") {
|
|
870
|
+
await fs4.rm(socketPath, { force: true });
|
|
871
|
+
}
|
|
872
|
+
const uploadTimeoutMs = parsePositiveIntFromEnv(
|
|
873
|
+
"OPENZCA_UPLOAD_IPC_HANDLER_TIMEOUT_MS",
|
|
874
|
+
parsePositiveIntFromEnv("OPENZCA_UPLOAD_TIMEOUT_MS", 12e4)
|
|
875
|
+
);
|
|
876
|
+
const server = net.createServer((socket) => {
|
|
877
|
+
socket.setEncoding("utf8");
|
|
878
|
+
let buffer = "";
|
|
879
|
+
let done = false;
|
|
880
|
+
const sendResponse = (response) => {
|
|
881
|
+
if (done) return;
|
|
882
|
+
done = true;
|
|
883
|
+
socket.end(`${JSON.stringify(response)}
|
|
884
|
+
`);
|
|
885
|
+
};
|
|
886
|
+
const fail = (requestId, message) => {
|
|
887
|
+
sendResponse({
|
|
888
|
+
kind: "upload_result",
|
|
889
|
+
requestId,
|
|
890
|
+
ok: false,
|
|
891
|
+
error: message
|
|
892
|
+
});
|
|
893
|
+
};
|
|
894
|
+
const handleRequest = async (line) => {
|
|
895
|
+
if (done) return;
|
|
896
|
+
let parsed;
|
|
897
|
+
try {
|
|
898
|
+
parsed = JSON.parse(line);
|
|
899
|
+
} catch {
|
|
900
|
+
fail("", "Invalid JSON request.");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (parsed.kind !== "upload") {
|
|
904
|
+
fail(parsed.requestId || "", "Unsupported IPC request kind.");
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (parsed.profile !== profile) {
|
|
908
|
+
fail(parsed.requestId, "Profile mismatch.");
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (!parsed.threadId || !Array.isArray(parsed.attachments) || parsed.attachments.length === 0) {
|
|
912
|
+
fail(parsed.requestId, "Invalid upload payload.");
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const threadType = parsed.threadType === "group" ? ThreadType.Group : ThreadType.User;
|
|
916
|
+
const requestTimeoutMs = parsePositiveIntFromUnknown(parsed.uploadTimeoutMs) ?? uploadTimeoutMs;
|
|
917
|
+
writeDebugLine(
|
|
918
|
+
"listen.ipc.upload.start",
|
|
919
|
+
{
|
|
920
|
+
profile,
|
|
921
|
+
sessionId,
|
|
922
|
+
requestId: parsed.requestId,
|
|
923
|
+
threadId: parsed.threadId,
|
|
924
|
+
threadType: parsed.threadType,
|
|
925
|
+
attachmentCount: parsed.attachments.length,
|
|
926
|
+
timeoutMs: requestTimeoutMs
|
|
927
|
+
},
|
|
928
|
+
command
|
|
929
|
+
);
|
|
930
|
+
try {
|
|
931
|
+
const response = await withTimeout(
|
|
932
|
+
api.sendMessage(
|
|
933
|
+
{
|
|
934
|
+
msg: "",
|
|
935
|
+
attachments: parsed.attachments
|
|
936
|
+
},
|
|
937
|
+
parsed.threadId,
|
|
938
|
+
threadType
|
|
939
|
+
),
|
|
940
|
+
requestTimeoutMs,
|
|
941
|
+
`Timed out waiting ${requestTimeoutMs}ms for IPC upload completion.`
|
|
942
|
+
);
|
|
943
|
+
sendResponse({
|
|
944
|
+
kind: "upload_result",
|
|
945
|
+
requestId: parsed.requestId,
|
|
946
|
+
ok: true,
|
|
947
|
+
response
|
|
948
|
+
});
|
|
949
|
+
writeDebugLine(
|
|
950
|
+
"listen.ipc.upload.done",
|
|
951
|
+
{
|
|
952
|
+
profile,
|
|
953
|
+
sessionId,
|
|
954
|
+
requestId: parsed.requestId,
|
|
955
|
+
threadId: parsed.threadId,
|
|
956
|
+
threadType: parsed.threadType
|
|
957
|
+
},
|
|
958
|
+
command
|
|
959
|
+
);
|
|
960
|
+
} catch (error) {
|
|
961
|
+
fail(parsed.requestId, toErrorText(error));
|
|
962
|
+
writeDebugLine(
|
|
963
|
+
"listen.ipc.upload.error",
|
|
964
|
+
{
|
|
965
|
+
profile,
|
|
966
|
+
sessionId,
|
|
967
|
+
requestId: parsed.requestId,
|
|
968
|
+
threadId: parsed.threadId,
|
|
969
|
+
threadType: parsed.threadType,
|
|
970
|
+
message: toErrorText(error)
|
|
971
|
+
},
|
|
972
|
+
command
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
};
|
|
976
|
+
socket.on("data", (chunk) => {
|
|
977
|
+
if (done) return;
|
|
978
|
+
buffer += chunk;
|
|
979
|
+
if (buffer.length > 2e6) {
|
|
980
|
+
fail("", "IPC request too large.");
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
984
|
+
if (newlineIndex === -1) return;
|
|
985
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
986
|
+
buffer = "";
|
|
987
|
+
if (!line) {
|
|
988
|
+
fail("", "Empty IPC request.");
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
void handleRequest(line);
|
|
992
|
+
});
|
|
993
|
+
socket.on("error", (error) => {
|
|
994
|
+
writeDebugLine(
|
|
995
|
+
"listen.ipc.socket_error",
|
|
996
|
+
{
|
|
997
|
+
profile,
|
|
998
|
+
sessionId,
|
|
999
|
+
message: toErrorText(error)
|
|
1000
|
+
},
|
|
1001
|
+
command
|
|
1002
|
+
);
|
|
1003
|
+
});
|
|
1004
|
+
});
|
|
1005
|
+
server.on("error", (error) => {
|
|
1006
|
+
writeDebugLine(
|
|
1007
|
+
"listen.ipc.server_error",
|
|
1008
|
+
{
|
|
1009
|
+
profile,
|
|
1010
|
+
sessionId,
|
|
1011
|
+
message: toErrorText(error)
|
|
1012
|
+
},
|
|
1013
|
+
command
|
|
1014
|
+
);
|
|
1015
|
+
});
|
|
1016
|
+
await new Promise((resolve, reject) => {
|
|
1017
|
+
server.once("error", reject);
|
|
1018
|
+
server.listen(socketPath, () => {
|
|
1019
|
+
server.off("error", reject);
|
|
1020
|
+
resolve();
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
writeDebugLine(
|
|
1024
|
+
"listen.ipc.started",
|
|
1025
|
+
{
|
|
1026
|
+
profile,
|
|
1027
|
+
sessionId,
|
|
1028
|
+
socketPath
|
|
1029
|
+
},
|
|
1030
|
+
command
|
|
1031
|
+
);
|
|
1032
|
+
let closed = false;
|
|
1033
|
+
return {
|
|
1034
|
+
socketPath,
|
|
1035
|
+
close: async () => {
|
|
1036
|
+
if (closed) return;
|
|
1037
|
+
closed = true;
|
|
1038
|
+
await new Promise((resolve) => {
|
|
1039
|
+
server.close(() => resolve());
|
|
1040
|
+
});
|
|
1041
|
+
if (process.platform !== "win32") {
|
|
1042
|
+
await fs4.rm(socketPath, { force: true });
|
|
1043
|
+
}
|
|
1044
|
+
writeDebugLine(
|
|
1045
|
+
"listen.ipc.stopped",
|
|
1046
|
+
{
|
|
1047
|
+
profile,
|
|
1048
|
+
sessionId,
|
|
1049
|
+
socketPath
|
|
1050
|
+
},
|
|
1051
|
+
command
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
async function tryUploadViaListenerIpc(profile, threadId, threadType, attachments, command) {
|
|
1057
|
+
if (!parseBooleanFromEnv("OPENZCA_UPLOAD_IPC", true)) {
|
|
1058
|
+
return { handled: false, reason: "ipc_disabled" };
|
|
1059
|
+
}
|
|
1060
|
+
const socketPath = getListenIpcSocketPath(profile);
|
|
1061
|
+
const requestId = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 10)}`;
|
|
1062
|
+
const connectTimeoutMs = parsePositiveIntFromEnv("OPENZCA_UPLOAD_IPC_CONNECT_TIMEOUT_MS", 1e3);
|
|
1063
|
+
const requestTimeoutMs = parsePositiveIntFromEnv(
|
|
1064
|
+
"OPENZCA_UPLOAD_IPC_TIMEOUT_MS",
|
|
1065
|
+
parsePositiveIntFromEnv("OPENZCA_UPLOAD_TIMEOUT_MS", 12e4) + 5e3
|
|
1066
|
+
);
|
|
1067
|
+
writeDebugLine(
|
|
1068
|
+
"msg.upload.ipc.try",
|
|
1069
|
+
{
|
|
1070
|
+
profile,
|
|
1071
|
+
threadId,
|
|
1072
|
+
threadType: threadType === ThreadType.Group ? "group" : "user",
|
|
1073
|
+
attachmentCount: attachments.length,
|
|
1074
|
+
socketPath,
|
|
1075
|
+
requestId,
|
|
1076
|
+
connectTimeoutMs,
|
|
1077
|
+
requestTimeoutMs
|
|
1078
|
+
},
|
|
1079
|
+
command
|
|
1080
|
+
);
|
|
1081
|
+
return await new Promise((resolve, reject) => {
|
|
1082
|
+
const socket = net.createConnection(socketPath);
|
|
1083
|
+
let connected = false;
|
|
1084
|
+
let settled = false;
|
|
1085
|
+
let responseBuffer = "";
|
|
1086
|
+
let requestSent = false;
|
|
1087
|
+
const finish = (result, error) => {
|
|
1088
|
+
if (settled) return;
|
|
1089
|
+
settled = true;
|
|
1090
|
+
clearTimeout(connectTimer);
|
|
1091
|
+
clearTimeout(requestTimer);
|
|
1092
|
+
if (!socket.destroyed) {
|
|
1093
|
+
socket.destroy();
|
|
1094
|
+
}
|
|
1095
|
+
if (error) {
|
|
1096
|
+
reject(error);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
resolve(result ?? { handled: false, reason: "unknown" });
|
|
1100
|
+
};
|
|
1101
|
+
const connectTimer = setTimeout(() => {
|
|
1102
|
+
finish(void 0, new Error(`Timed out waiting ${connectTimeoutMs}ms to connect upload IPC.`));
|
|
1103
|
+
}, connectTimeoutMs);
|
|
1104
|
+
const requestTimer = setTimeout(() => {
|
|
1105
|
+
finish(void 0, new Error(`Timed out waiting ${requestTimeoutMs}ms for upload IPC response.`));
|
|
1106
|
+
}, requestTimeoutMs);
|
|
1107
|
+
socket.setEncoding("utf8");
|
|
1108
|
+
socket.on("connect", () => {
|
|
1109
|
+
connected = true;
|
|
1110
|
+
clearTimeout(connectTimer);
|
|
1111
|
+
const payload = {
|
|
1112
|
+
kind: "upload",
|
|
1113
|
+
requestId,
|
|
1114
|
+
profile,
|
|
1115
|
+
threadId,
|
|
1116
|
+
threadType: threadType === ThreadType.Group ? "group" : "user",
|
|
1117
|
+
attachments
|
|
1118
|
+
};
|
|
1119
|
+
socket.write(`${JSON.stringify(payload)}
|
|
1120
|
+
`);
|
|
1121
|
+
requestSent = true;
|
|
1122
|
+
});
|
|
1123
|
+
socket.on("data", (chunk) => {
|
|
1124
|
+
responseBuffer += chunk;
|
|
1125
|
+
const newlineIndex = responseBuffer.indexOf("\n");
|
|
1126
|
+
if (newlineIndex === -1) return;
|
|
1127
|
+
const line = responseBuffer.slice(0, newlineIndex).trim();
|
|
1128
|
+
if (!line) {
|
|
1129
|
+
finish(void 0, new Error("Upload IPC returned empty response."));
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
let parsed;
|
|
1133
|
+
try {
|
|
1134
|
+
parsed = JSON.parse(line);
|
|
1135
|
+
} catch {
|
|
1136
|
+
finish(void 0, new Error("Upload IPC returned invalid JSON."));
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
if (parsed.kind !== "upload_result" || parsed.requestId !== requestId) {
|
|
1140
|
+
finish(void 0, new Error("Upload IPC returned mismatched response."));
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (!parsed.ok) {
|
|
1144
|
+
finish(void 0, new Error(parsed.error || "Upload IPC failed."));
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
finish({
|
|
1148
|
+
handled: true,
|
|
1149
|
+
response: parsed.response
|
|
1150
|
+
});
|
|
1151
|
+
});
|
|
1152
|
+
socket.on("error", (error) => {
|
|
1153
|
+
const code = error.code;
|
|
1154
|
+
if (!connected && !requestSent && (code === "ENOENT" || code === "ECONNREFUSED")) {
|
|
1155
|
+
finish({
|
|
1156
|
+
handled: false,
|
|
1157
|
+
reason: code.toLowerCase()
|
|
1158
|
+
});
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
finish(void 0, error);
|
|
1162
|
+
});
|
|
1163
|
+
socket.on("close", () => {
|
|
1164
|
+
if (settled) return;
|
|
1165
|
+
if (!connected && !requestSent) {
|
|
1166
|
+
finish({
|
|
1167
|
+
handled: false,
|
|
1168
|
+
reason: "socket_closed_before_connect"
|
|
1169
|
+
});
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
finish(void 0, new Error("Upload IPC connection closed before response."));
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
async function resolveUploadThreadType(api, profile, threadId, groupFlag, command) {
|
|
1177
|
+
if (groupFlag) {
|
|
1178
|
+
return { type: ThreadType.Group, reason: "explicit_group_flag" };
|
|
1179
|
+
}
|
|
1180
|
+
const autoDetectEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_AUTO_THREAD_TYPE", true);
|
|
1181
|
+
if (!autoDetectEnabled) {
|
|
1182
|
+
return { type: ThreadType.User, reason: "auto_detect_disabled" };
|
|
1183
|
+
}
|
|
1184
|
+
try {
|
|
1185
|
+
const cache = await readCache(profile);
|
|
1186
|
+
const groupIds = collectIdsFromCacheEntries(cache.groups, ["groupId", "grid", "threadId", "id"]);
|
|
1187
|
+
if (groupIds.has(threadId)) {
|
|
1188
|
+
return { type: ThreadType.Group, reason: "cache_group_match" };
|
|
1189
|
+
}
|
|
1190
|
+
const friendIds = collectIdsFromCacheEntries(cache.friends, ["userId", "uid", "id", "threadId"]);
|
|
1191
|
+
if (friendIds.has(threadId)) {
|
|
1192
|
+
return { type: ThreadType.User, reason: "cache_friend_match" };
|
|
1193
|
+
}
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
writeDebugLine(
|
|
1196
|
+
"msg.upload.thread_type.cache_error",
|
|
1197
|
+
{
|
|
1198
|
+
profile,
|
|
1199
|
+
threadId,
|
|
1200
|
+
message: toErrorText(error)
|
|
1201
|
+
},
|
|
1202
|
+
command
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
const probeEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_GROUP_PROBE", true);
|
|
1206
|
+
if (!probeEnabled) {
|
|
1207
|
+
return { type: ThreadType.User, reason: "probe_disabled" };
|
|
1208
|
+
}
|
|
1209
|
+
const probeTimeoutMs = parsePositiveIntFromEnv("OPENZCA_UPLOAD_GROUP_PROBE_TIMEOUT_MS", 5e3);
|
|
1210
|
+
try {
|
|
1211
|
+
const groupInfo = await withTimeout(
|
|
1212
|
+
api.getGroupInfo(threadId),
|
|
1213
|
+
probeTimeoutMs,
|
|
1214
|
+
`Timed out waiting ${probeTimeoutMs}ms while probing group thread type.`
|
|
1215
|
+
);
|
|
1216
|
+
if (groupInfo?.gridInfoMap?.[threadId]) {
|
|
1217
|
+
return { type: ThreadType.Group, reason: "probe_group_match" };
|
|
1218
|
+
}
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
writeDebugLine(
|
|
1221
|
+
"msg.upload.thread_type.probe_error",
|
|
1222
|
+
{
|
|
1223
|
+
profile,
|
|
1224
|
+
threadId,
|
|
1225
|
+
message: toErrorText(error)
|
|
1226
|
+
},
|
|
1227
|
+
command
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
return { type: ThreadType.User, reason: "default_user" };
|
|
1231
|
+
}
|
|
706
1232
|
function parseReaction(input) {
|
|
707
1233
|
const normalized = input.trim();
|
|
708
1234
|
if (EMOJI_REACTION_MAP[normalized]) {
|
|
@@ -2334,18 +2860,28 @@ msg.command("edit <msgId> <cliMsgId> <threadId> <message>").option("-g, --group"
|
|
|
2334
2860
|
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(
|
|
2335
2861
|
wrapAction(
|
|
2336
2862
|
async (arg1, arg2, opts, command) => {
|
|
2337
|
-
const { api } = await requireApi(command);
|
|
2863
|
+
const { api, profile } = await requireApi(command);
|
|
2338
2864
|
const inputs = normalizeInputList(opts.url);
|
|
2339
2865
|
const urlInputs = inputs.filter((entry) => isHttpUrl(entry));
|
|
2340
2866
|
const localInputs = inputs.filter((entry) => !isHttpUrl(entry));
|
|
2341
2867
|
const [threadId, file] = arg2 ? [arg2, arg1] : [arg1, void 0];
|
|
2868
|
+
const threadResolution = await resolveUploadThreadType(
|
|
2869
|
+
api,
|
|
2870
|
+
profile,
|
|
2871
|
+
threadId,
|
|
2872
|
+
opts.group,
|
|
2873
|
+
command
|
|
2874
|
+
);
|
|
2342
2875
|
const normalizedFile = file ? normalizeMediaInput(file) : void 0;
|
|
2343
2876
|
const localFiles = [normalizedFile, ...localInputs].filter(Boolean);
|
|
2344
2877
|
writeDebugLine(
|
|
2345
2878
|
"msg.upload.inputs",
|
|
2346
2879
|
{
|
|
2347
2880
|
threadId,
|
|
2348
|
-
|
|
2881
|
+
explicitGroupFlag: Boolean(opts.group),
|
|
2882
|
+
isGroup: threadResolution.type === ThreadType.Group,
|
|
2883
|
+
threadType: threadResolution.type === ThreadType.Group ? "group" : "user",
|
|
2884
|
+
threadTypeReason: threadResolution.reason,
|
|
2349
2885
|
localFiles,
|
|
2350
2886
|
urlInputs
|
|
2351
2887
|
},
|
|
@@ -2360,6 +2896,43 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
2360
2896
|
);
|
|
2361
2897
|
}
|
|
2362
2898
|
await assertFilesExist(attachments);
|
|
2899
|
+
const ipcResult = await tryUploadViaListenerIpc(
|
|
2900
|
+
profile,
|
|
2901
|
+
threadId,
|
|
2902
|
+
threadResolution.type,
|
|
2903
|
+
attachments,
|
|
2904
|
+
command
|
|
2905
|
+
);
|
|
2906
|
+
if (ipcResult.handled) {
|
|
2907
|
+
writeDebugLine(
|
|
2908
|
+
"msg.upload.ipc.done",
|
|
2909
|
+
{
|
|
2910
|
+
threadId,
|
|
2911
|
+
threadType: threadResolution.type === ThreadType.Group ? "group" : "user"
|
|
2912
|
+
},
|
|
2913
|
+
command
|
|
2914
|
+
);
|
|
2915
|
+
output(ipcResult.response, false);
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
writeDebugLine(
|
|
2919
|
+
"msg.upload.ipc.fallback",
|
|
2920
|
+
{
|
|
2921
|
+
threadId,
|
|
2922
|
+
threadType: threadResolution.type === ThreadType.Group ? "group" : "user",
|
|
2923
|
+
reason: ipcResult.reason
|
|
2924
|
+
},
|
|
2925
|
+
command
|
|
2926
|
+
);
|
|
2927
|
+
const enforceSingleOwner = parseBooleanFromEnv("OPENZCA_UPLOAD_ENFORCE_SINGLE_OWNER", true);
|
|
2928
|
+
if (enforceSingleOwner) {
|
|
2929
|
+
const owner = await readActiveListenerOwner(profile);
|
|
2930
|
+
if (owner && owner.pid !== process.pid) {
|
|
2931
|
+
throw new Error(
|
|
2932
|
+
`Active listener owner detected for profile "${profile}" (pid ${owner.pid}), but upload IPC is unavailable. Restart \`openzca listen\` with latest version or set OPENZCA_UPLOAD_ENFORCE_SINGLE_OWNER=0 to allow fallback listener startup.`
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2363
2936
|
const response = await withUploadListener(
|
|
2364
2937
|
api,
|
|
2365
2938
|
command,
|
|
@@ -2369,7 +2942,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
2369
2942
|
attachments
|
|
2370
2943
|
},
|
|
2371
2944
|
threadId,
|
|
2372
|
-
|
|
2945
|
+
threadResolution.type
|
|
2373
2946
|
)
|
|
2374
2947
|
);
|
|
2375
2948
|
output(response, false);
|
|
@@ -3022,6 +3595,14 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
3022
3595
|
) ?? 3e4;
|
|
3023
3596
|
const lifecycleEventsEnabled = supervised && Boolean(opts.raw);
|
|
3024
3597
|
const recycleEnabled = !supervised && Boolean(opts.keepAlive) && recycleMs > 0;
|
|
3598
|
+
const keepAliveRestartDelayMs = parsePositiveIntFromEnv(
|
|
3599
|
+
"OPENZCA_LISTEN_KEEPALIVE_RESTART_DELAY_MS",
|
|
3600
|
+
2e3
|
|
3601
|
+
);
|
|
3602
|
+
const keepAliveRestartOnAnyClose = parseBooleanFromEnv(
|
|
3603
|
+
"OPENZCA_LISTEN_KEEPALIVE_RESTART_ON_ANY_CLOSE",
|
|
3604
|
+
false
|
|
3605
|
+
);
|
|
3025
3606
|
const recycleExitCode = 75;
|
|
3026
3607
|
const includeReplyContext = parseToggleDefaultTrue(
|
|
3027
3608
|
process.env.OPENZCA_LISTEN_INCLUDE_QUOTE_CONTEXT
|
|
@@ -3043,212 +3624,229 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
3043
3624
|
})
|
|
3044
3625
|
);
|
|
3045
3626
|
};
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
"content-type": "application/json"
|
|
3627
|
+
const enforceSingleOwner = parseBooleanFromEnv("OPENZCA_LISTEN_ENFORCE_SINGLE_OWNER", true);
|
|
3628
|
+
let ownerLock = null;
|
|
3629
|
+
let ipcServer = null;
|
|
3630
|
+
let ipcSocketPath;
|
|
3631
|
+
let resourcesCleaned = false;
|
|
3632
|
+
const cleanupListenResources = async () => {
|
|
3633
|
+
if (resourcesCleaned) return;
|
|
3634
|
+
resourcesCleaned = true;
|
|
3635
|
+
if (ipcServer) {
|
|
3636
|
+
await ipcServer.close();
|
|
3637
|
+
ipcServer = null;
|
|
3638
|
+
}
|
|
3639
|
+
if (ownerLock) {
|
|
3640
|
+
await ownerLock.release();
|
|
3641
|
+
ownerLock = null;
|
|
3642
|
+
}
|
|
3643
|
+
};
|
|
3644
|
+
const unregisterResourceCleanup = registerShutdownCallback(async () => {
|
|
3645
|
+
await cleanupListenResources();
|
|
3646
|
+
});
|
|
3647
|
+
try {
|
|
3648
|
+
if (enforceSingleOwner) {
|
|
3649
|
+
ownerLock = await acquireListenerOwnerLock(profile, sessionId, command);
|
|
3650
|
+
writeDebugLine(
|
|
3651
|
+
"listen.owner.acquired",
|
|
3652
|
+
{
|
|
3653
|
+
profile,
|
|
3654
|
+
lockPath: ownerLock.lockPath,
|
|
3655
|
+
pid: process.pid,
|
|
3656
|
+
sessionId
|
|
3077
3657
|
},
|
|
3078
|
-
|
|
3079
|
-
});
|
|
3080
|
-
if (!response.ok) {
|
|
3081
|
-
console.error(`Webhook response: ${response.status}`);
|
|
3082
|
-
}
|
|
3083
|
-
} catch (error) {
|
|
3084
|
-
console.error(
|
|
3085
|
-
`Webhook failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3658
|
+
command
|
|
3086
3659
|
);
|
|
3087
3660
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
console.log("
|
|
3661
|
+
ipcServer = await startListenerIpcServer(api, profile, sessionId, command);
|
|
3662
|
+
ipcSocketPath = ipcServer?.socketPath;
|
|
3663
|
+
console.log("Listening... Press Ctrl+C to stop.");
|
|
3664
|
+
if (supervised && opts.keepAlive) {
|
|
3665
|
+
console.error("Warning: --supervised ignores internal --keep-alive reconnect ownership.");
|
|
3666
|
+
}
|
|
3091
3667
|
writeDebugLine(
|
|
3092
|
-
"listen.
|
|
3668
|
+
"listen.start",
|
|
3093
3669
|
{
|
|
3094
3670
|
profile,
|
|
3095
|
-
|
|
3671
|
+
mediaDir: resolveInboundMediaDir(profile),
|
|
3672
|
+
maxMediaBytes: parseMaxInboundMediaBytes(),
|
|
3673
|
+
maxMediaFiles: parseMaxInboundMediaFiles(),
|
|
3674
|
+
includeMediaUrl: process.env.OPENZCA_LISTEN_INCLUDE_MEDIA_URL?.trim() ?? null,
|
|
3675
|
+
keepAlive: Boolean(opts.keepAlive),
|
|
3676
|
+
keepAliveRestartDelayMs: Boolean(opts.keepAlive) ? keepAliveRestartDelayMs : void 0,
|
|
3677
|
+
keepAliveRestartOnAnyClose: Boolean(opts.keepAlive) ? keepAliveRestartOnAnyClose : void 0,
|
|
3678
|
+
supervised,
|
|
3679
|
+
lifecycleEventsEnabled,
|
|
3680
|
+
heartbeatMs: lifecycleEventsEnabled ? heartbeatMs : void 0,
|
|
3681
|
+
recycleMs: recycleEnabled ? recycleMs : void 0,
|
|
3682
|
+
includeReplyContext,
|
|
3683
|
+
downloadQuoteMedia,
|
|
3684
|
+
sessionId,
|
|
3685
|
+
singleOwner: enforceSingleOwner,
|
|
3686
|
+
ipcSocketPath
|
|
3096
3687
|
},
|
|
3097
3688
|
command
|
|
3098
3689
|
);
|
|
3099
|
-
emitLifecycle("
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
mediaKind,
|
|
3120
|
-
hasParsedStructuredContent,
|
|
3121
|
-
remoteMediaUrls,
|
|
3122
|
-
hasQuote: Boolean(quote),
|
|
3123
|
-
quoteOwnerId: quote?.ownerId,
|
|
3124
|
-
quoteGlobalMsgId: quote?.globalMsgId,
|
|
3125
|
-
quoteCliMsgId: quote?.cliMsgId,
|
|
3126
|
-
quoteRemoteMediaUrls
|
|
3127
|
-
},
|
|
3128
|
-
command
|
|
3129
|
-
);
|
|
3130
|
-
const [mediaEntries, quoteMediaEntries] = await Promise.all([
|
|
3131
|
-
mediaKind ? cacheRemoteMediaEntries({
|
|
3132
|
-
profile,
|
|
3133
|
-
urls: remoteMediaUrls,
|
|
3134
|
-
kind: mediaKind,
|
|
3135
|
-
command,
|
|
3136
|
-
warningLabel: "inbound media",
|
|
3137
|
-
debugErrorEvent: "listen.media.cache_error",
|
|
3138
|
-
debugUrlKey: "mediaUrl"
|
|
3139
|
-
}) : Promise.resolve([]),
|
|
3140
|
-
cacheRemoteMediaEntries({
|
|
3141
|
-
profile,
|
|
3142
|
-
urls: quoteRemoteMediaUrls,
|
|
3143
|
-
kind: "file",
|
|
3144
|
-
command,
|
|
3145
|
-
warningLabel: "quoted media",
|
|
3146
|
-
debugErrorEvent: "listen.quote_media.cache_error",
|
|
3147
|
-
debugUrlKey: "quoteMediaUrl"
|
|
3148
|
-
})
|
|
3149
|
-
]);
|
|
3150
|
-
const localEntries = mediaEntries.filter((entry) => Boolean(entry.mediaPath));
|
|
3151
|
-
const mediaPaths = localEntries.map((entry) => entry.mediaPath);
|
|
3152
|
-
const mediaUrls = localEntries.length > 0 ? localEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value)) : mediaEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value));
|
|
3153
|
-
const mediaTypes = localEntries.length > 0 ? localEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value)) : mediaEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value));
|
|
3154
|
-
const mediaPath = mediaPaths[0];
|
|
3155
|
-
const mediaUrl = mediaUrls[0];
|
|
3156
|
-
const mediaType = mediaTypes[0];
|
|
3157
|
-
const quoteLocalEntries = quoteMediaEntries.filter((entry) => Boolean(entry.mediaPath));
|
|
3158
|
-
const quoteMediaPaths = quoteLocalEntries.map((entry) => entry.mediaPath);
|
|
3159
|
-
const quoteMediaUrls = quoteLocalEntries.length > 0 ? quoteLocalEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value)) : quoteMediaEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value));
|
|
3160
|
-
const quoteMediaTypes = quoteLocalEntries.length > 0 ? quoteLocalEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value)) : quoteMediaEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value));
|
|
3161
|
-
const quoteMediaPath = quoteMediaPaths[0];
|
|
3162
|
-
const quoteMediaUrl = quoteMediaUrls[0];
|
|
3163
|
-
const quoteMediaType = quoteMediaTypes[0];
|
|
3164
|
-
if (quote) {
|
|
3165
|
-
quote = {
|
|
3166
|
-
...quote,
|
|
3167
|
-
mediaPath: quoteMediaPath,
|
|
3168
|
-
mediaPaths: quoteMediaPaths.length > 0 ? quoteMediaPaths : void 0,
|
|
3169
|
-
mediaUrl: quoteMediaUrl,
|
|
3170
|
-
mediaUrls: quoteMediaUrls.length > 0 ? quoteMediaUrls : quote.mediaUrls,
|
|
3171
|
-
mediaType: quoteMediaType,
|
|
3172
|
-
mediaTypes: quoteMediaTypes.length > 0 ? quoteMediaTypes : void 0
|
|
3173
|
-
};
|
|
3174
|
-
}
|
|
3175
|
-
const replyContextText = includeReplyContext && quote ? buildReplyContextText(quote) : "";
|
|
3176
|
-
const replyMediaText = includeReplyContext && quoteMediaEntries.length > 0 ? buildReplyMediaAttachedText({ mediaEntries: quoteMediaEntries }) : "";
|
|
3177
|
-
const caption = rawText.trim().length > 0 && !hasParsedStructuredContent ? rawText.trim() : summarizeStructuredContent(msgType, parsedContent);
|
|
3178
|
-
let processedText = mediaEntries.length ? buildMediaAttachedText({
|
|
3179
|
-
mediaEntries,
|
|
3180
|
-
fallbackKind: mediaKind,
|
|
3181
|
-
caption
|
|
3182
|
-
}) : rawText.trim().length > 0 && !hasParsedStructuredContent ? rawText : summarizeStructuredContent(msgType, parsedContent);
|
|
3183
|
-
if (!processedText.trim() && !replyContextText && !replyMediaText) return;
|
|
3184
|
-
if (opts.prefix && processedText.trim().length > 0) {
|
|
3185
|
-
if (!processedText.startsWith(opts.prefix)) return;
|
|
3186
|
-
processedText = processedText.slice(opts.prefix.length).trimStart();
|
|
3690
|
+
emitLifecycle("session_id");
|
|
3691
|
+
let keepAliveRestartTimer = null;
|
|
3692
|
+
async function emitWebhook(payload) {
|
|
3693
|
+
if (!opts.webhook) return;
|
|
3694
|
+
try {
|
|
3695
|
+
const response = await fetch(opts.webhook, {
|
|
3696
|
+
method: "POST",
|
|
3697
|
+
headers: {
|
|
3698
|
+
"content-type": "application/json"
|
|
3699
|
+
},
|
|
3700
|
+
body: JSON.stringify(payload)
|
|
3701
|
+
});
|
|
3702
|
+
if (!response.ok) {
|
|
3703
|
+
console.error(`Webhook response: ${response.status}`);
|
|
3704
|
+
}
|
|
3705
|
+
} catch (error) {
|
|
3706
|
+
console.error(
|
|
3707
|
+
`Webhook failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3708
|
+
);
|
|
3709
|
+
}
|
|
3187
3710
|
}
|
|
3188
|
-
|
|
3189
|
-
|
|
3711
|
+
api.listener.on("connected", () => {
|
|
3712
|
+
console.log("Connected to Zalo websocket.");
|
|
3713
|
+
if (keepAliveRestartTimer) {
|
|
3714
|
+
clearTimeout(keepAliveRestartTimer);
|
|
3715
|
+
keepAliveRestartTimer = null;
|
|
3716
|
+
}
|
|
3717
|
+
writeDebugLine(
|
|
3718
|
+
"listen.connected",
|
|
3719
|
+
{
|
|
3720
|
+
profile,
|
|
3721
|
+
sessionId
|
|
3722
|
+
},
|
|
3723
|
+
command
|
|
3724
|
+
);
|
|
3725
|
+
emitLifecycle("connected");
|
|
3726
|
+
});
|
|
3727
|
+
api.listener.on("message", async (message) => {
|
|
3728
|
+
const messageData = message.data;
|
|
3729
|
+
const rawContent = messageData.content;
|
|
3730
|
+
const msgType = getStringCandidate(messageData, ["msgType"]);
|
|
3731
|
+
let quote = normalizeQuoteContext(messageData.quote);
|
|
3732
|
+
const parsedContent = normalizeStructuredContent(rawContent);
|
|
3733
|
+
const hasParsedStructuredContent = parsedContent !== rawContent;
|
|
3734
|
+
const rawText = typeof rawContent === "string" ? rawContent : "";
|
|
3735
|
+
const mediaKind = detectInboundMediaKind(msgType, parsedContent);
|
|
3736
|
+
const maxMediaFiles = parseMaxInboundMediaFiles();
|
|
3737
|
+
const remoteMediaUrls = mediaKind && maxMediaFiles > 0 ? resolvePreferredMediaUrls(mediaKind, parsedContent).slice(0, maxMediaFiles) : [];
|
|
3738
|
+
const quoteRemoteMediaUrls = quote && downloadQuoteMedia && maxMediaFiles > 0 ? (quote.mediaUrls ?? []).slice(0, maxMediaFiles) : [];
|
|
3739
|
+
writeDebugLine(
|
|
3740
|
+
"listen.media.detected",
|
|
3741
|
+
{
|
|
3742
|
+
profile,
|
|
3743
|
+
threadId: message.threadId,
|
|
3744
|
+
msgType: msgType || void 0,
|
|
3745
|
+
mediaKind,
|
|
3746
|
+
hasParsedStructuredContent,
|
|
3747
|
+
remoteMediaUrls,
|
|
3748
|
+
hasQuote: Boolean(quote),
|
|
3749
|
+
quoteOwnerId: quote?.ownerId,
|
|
3750
|
+
quoteGlobalMsgId: quote?.globalMsgId,
|
|
3751
|
+
quoteCliMsgId: quote?.cliMsgId,
|
|
3752
|
+
quoteRemoteMediaUrls
|
|
3753
|
+
},
|
|
3754
|
+
command
|
|
3755
|
+
);
|
|
3756
|
+
const [mediaEntries, quoteMediaEntries] = await Promise.all([
|
|
3757
|
+
mediaKind ? cacheRemoteMediaEntries({
|
|
3758
|
+
profile,
|
|
3759
|
+
urls: remoteMediaUrls,
|
|
3760
|
+
kind: mediaKind,
|
|
3761
|
+
command,
|
|
3762
|
+
warningLabel: "inbound media",
|
|
3763
|
+
debugErrorEvent: "listen.media.cache_error",
|
|
3764
|
+
debugUrlKey: "mediaUrl"
|
|
3765
|
+
}) : Promise.resolve([]),
|
|
3766
|
+
cacheRemoteMediaEntries({
|
|
3767
|
+
profile,
|
|
3768
|
+
urls: quoteRemoteMediaUrls,
|
|
3769
|
+
kind: "file",
|
|
3770
|
+
command,
|
|
3771
|
+
warningLabel: "quoted media",
|
|
3772
|
+
debugErrorEvent: "listen.quote_media.cache_error",
|
|
3773
|
+
debugUrlKey: "quoteMediaUrl"
|
|
3774
|
+
})
|
|
3775
|
+
]);
|
|
3776
|
+
const localEntries = mediaEntries.filter((entry) => Boolean(entry.mediaPath));
|
|
3777
|
+
const mediaPaths = localEntries.map((entry) => entry.mediaPath);
|
|
3778
|
+
const mediaUrls = localEntries.length > 0 ? localEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value)) : mediaEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value));
|
|
3779
|
+
const mediaTypes = localEntries.length > 0 ? localEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value)) : mediaEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value));
|
|
3780
|
+
const mediaPath = mediaPaths[0];
|
|
3781
|
+
const mediaUrl = mediaUrls[0];
|
|
3782
|
+
const mediaType = mediaTypes[0];
|
|
3783
|
+
const quoteLocalEntries = quoteMediaEntries.filter((entry) => Boolean(entry.mediaPath));
|
|
3784
|
+
const quoteMediaPaths = quoteLocalEntries.map((entry) => entry.mediaPath);
|
|
3785
|
+
const quoteMediaUrls = quoteLocalEntries.length > 0 ? quoteLocalEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value)) : quoteMediaEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value));
|
|
3786
|
+
const quoteMediaTypes = quoteLocalEntries.length > 0 ? quoteLocalEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value)) : quoteMediaEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value));
|
|
3787
|
+
const quoteMediaPath = quoteMediaPaths[0];
|
|
3788
|
+
const quoteMediaUrl = quoteMediaUrls[0];
|
|
3789
|
+
const quoteMediaType = quoteMediaTypes[0];
|
|
3790
|
+
if (quote) {
|
|
3791
|
+
quote = {
|
|
3792
|
+
...quote,
|
|
3793
|
+
mediaPath: quoteMediaPath,
|
|
3794
|
+
mediaPaths: quoteMediaPaths.length > 0 ? quoteMediaPaths : void 0,
|
|
3795
|
+
mediaUrl: quoteMediaUrl,
|
|
3796
|
+
mediaUrls: quoteMediaUrls.length > 0 ? quoteMediaUrls : quote.mediaUrls,
|
|
3797
|
+
mediaType: quoteMediaType,
|
|
3798
|
+
mediaTypes: quoteMediaTypes.length > 0 ? quoteMediaTypes : void 0
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
const replyContextText = includeReplyContext && quote ? buildReplyContextText(quote) : "";
|
|
3802
|
+
const replyMediaText = includeReplyContext && quoteMediaEntries.length > 0 ? buildReplyMediaAttachedText({ mediaEntries: quoteMediaEntries }) : "";
|
|
3803
|
+
const caption = rawText.trim().length > 0 && !hasParsedStructuredContent ? rawText.trim() : summarizeStructuredContent(msgType, parsedContent);
|
|
3804
|
+
let processedText = mediaEntries.length ? buildMediaAttachedText({
|
|
3805
|
+
mediaEntries,
|
|
3806
|
+
fallbackKind: mediaKind,
|
|
3807
|
+
caption
|
|
3808
|
+
}) : rawText.trim().length > 0 && !hasParsedStructuredContent ? rawText : summarizeStructuredContent(msgType, parsedContent);
|
|
3809
|
+
if (!processedText.trim() && !replyContextText && !replyMediaText) return;
|
|
3810
|
+
if (opts.prefix && processedText.trim().length > 0) {
|
|
3811
|
+
if (!processedText.startsWith(opts.prefix)) return;
|
|
3812
|
+
processedText = processedText.slice(opts.prefix.length).trimStart();
|
|
3813
|
+
}
|
|
3814
|
+
if (replyMediaText) {
|
|
3815
|
+
processedText = processedText.trim() ? `${processedText}
|
|
3190
3816
|
${replyMediaText}` : replyMediaText;
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3817
|
+
}
|
|
3818
|
+
if (replyContextText) {
|
|
3819
|
+
processedText = processedText.trim() ? `${processedText}
|
|
3194
3820
|
${replyContextText}` : replyContextText;
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
threadId: message.threadId,
|
|
3217
|
-
targetId: message.threadId,
|
|
3218
|
-
conversationId: message.threadId,
|
|
3219
|
-
msgId: message.data.msgId,
|
|
3220
|
-
cliMsgId: message.data.cliMsgId,
|
|
3221
|
-
content: processedText,
|
|
3222
|
-
type: message.type,
|
|
3223
|
-
timestamp,
|
|
3224
|
-
msgType: msgType || void 0,
|
|
3225
|
-
quote: quote ?? void 0,
|
|
3226
|
-
quoteMediaPath,
|
|
3227
|
-
quoteMediaPaths: quoteMediaPaths.length > 0 ? quoteMediaPaths : void 0,
|
|
3228
|
-
quoteMediaUrl,
|
|
3229
|
-
quoteMediaUrls: quoteMediaUrls.length > 0 ? quoteMediaUrls : void 0,
|
|
3230
|
-
quoteMediaType,
|
|
3231
|
-
quoteMediaTypes: quoteMediaTypes.length > 0 ? quoteMediaTypes : void 0,
|
|
3232
|
-
mediaPath,
|
|
3233
|
-
mediaPaths: mediaPaths.length > 0 ? mediaPaths : void 0,
|
|
3234
|
-
mediaUrl,
|
|
3235
|
-
mediaUrls: mediaUrls.length > 0 ? mediaUrls : void 0,
|
|
3236
|
-
mediaType,
|
|
3237
|
-
mediaTypes: mediaTypes.length > 0 ? mediaTypes : void 0,
|
|
3238
|
-
mediaKind: mediaKind ?? void 0,
|
|
3239
|
-
mentions: mentions.length > 0 ? mentions : void 0,
|
|
3240
|
-
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
3241
|
-
metadata: {
|
|
3242
|
-
isGroup: message.type === ThreadType.Group,
|
|
3243
|
-
chatType,
|
|
3821
|
+
}
|
|
3822
|
+
const chatType = message.type === ThreadType.Group ? "group" : "user";
|
|
3823
|
+
const senderId = getStringCandidate(messageData, ["uidFrom"]) || message.data.uidFrom;
|
|
3824
|
+
const senderDisplayNameRaw = getStringCandidate(messageData, [
|
|
3825
|
+
"dName",
|
|
3826
|
+
"fromD",
|
|
3827
|
+
"senderName",
|
|
3828
|
+
"displayName"
|
|
3829
|
+
]);
|
|
3830
|
+
const senderDisplayName = senderDisplayNameRaw || void 0;
|
|
3831
|
+
const senderNameForMetadata = message.type === ThreadType.Group ? senderDisplayName : void 0;
|
|
3832
|
+
const toId = getStringCandidate(messageData, ["idTo"]) || void 0;
|
|
3833
|
+
const threadName = typeof messageData.threadName === "string" ? messageData.threadName : typeof messageData.tName === "string" ? messageData.tName : void 0;
|
|
3834
|
+
const mentions = extractInboundMentions({
|
|
3835
|
+
messageData,
|
|
3836
|
+
parsedContent,
|
|
3837
|
+
rawText
|
|
3838
|
+
});
|
|
3839
|
+
const mentionIds = mentions.map((item) => item.uid);
|
|
3840
|
+
const timestamp = toEpochSeconds(message.data.ts);
|
|
3841
|
+
const payload = {
|
|
3244
3842
|
threadId: message.threadId,
|
|
3245
3843
|
targetId: message.threadId,
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3844
|
+
conversationId: message.threadId,
|
|
3845
|
+
msgId: message.data.msgId,
|
|
3846
|
+
cliMsgId: message.data.cliMsgId,
|
|
3847
|
+
content: processedText,
|
|
3848
|
+
type: message.type,
|
|
3849
|
+
timestamp,
|
|
3252
3850
|
msgType: msgType || void 0,
|
|
3253
3851
|
quote: quote ?? void 0,
|
|
3254
3852
|
quoteMediaPath,
|
|
@@ -3257,7 +3855,6 @@ ${replyContextText}` : replyContextText;
|
|
|
3257
3855
|
quoteMediaUrls: quoteMediaUrls.length > 0 ? quoteMediaUrls : void 0,
|
|
3258
3856
|
quoteMediaType,
|
|
3259
3857
|
quoteMediaTypes: quoteMediaTypes.length > 0 ? quoteMediaTypes : void 0,
|
|
3260
|
-
timestamp,
|
|
3261
3858
|
mediaPath,
|
|
3262
3859
|
mediaPaths: mediaPaths.length > 0 ? mediaPaths : void 0,
|
|
3263
3860
|
mediaUrl,
|
|
@@ -3267,147 +3864,222 @@ ${replyContextText}` : replyContextText;
|
|
|
3267
3864
|
mediaKind: mediaKind ?? void 0,
|
|
3268
3865
|
mentions: mentions.length > 0 ? mentions : void 0,
|
|
3269
3866
|
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3867
|
+
metadata: {
|
|
3868
|
+
isGroup: message.type === ThreadType.Group,
|
|
3869
|
+
chatType,
|
|
3870
|
+
threadId: message.threadId,
|
|
3871
|
+
targetId: message.threadId,
|
|
3872
|
+
threadName,
|
|
3873
|
+
senderName: senderNameForMetadata,
|
|
3874
|
+
senderDisplayName,
|
|
3875
|
+
senderId,
|
|
3876
|
+
fromId: senderId,
|
|
3877
|
+
toId,
|
|
3878
|
+
msgType: msgType || void 0,
|
|
3879
|
+
quote: quote ?? void 0,
|
|
3880
|
+
quoteMediaPath,
|
|
3881
|
+
quoteMediaPaths: quoteMediaPaths.length > 0 ? quoteMediaPaths : void 0,
|
|
3882
|
+
quoteMediaUrl,
|
|
3883
|
+
quoteMediaUrls: quoteMediaUrls.length > 0 ? quoteMediaUrls : void 0,
|
|
3884
|
+
quoteMediaType,
|
|
3885
|
+
quoteMediaTypes: quoteMediaTypes.length > 0 ? quoteMediaTypes : void 0,
|
|
3886
|
+
timestamp,
|
|
3887
|
+
mediaPath,
|
|
3888
|
+
mediaPaths: mediaPaths.length > 0 ? mediaPaths : void 0,
|
|
3889
|
+
mediaUrl,
|
|
3890
|
+
mediaUrls: mediaUrls.length > 0 ? mediaUrls : void 0,
|
|
3891
|
+
mediaType,
|
|
3892
|
+
mediaTypes: mediaTypes.length > 0 ? mediaTypes : void 0,
|
|
3893
|
+
mediaKind: mediaKind ?? void 0,
|
|
3894
|
+
mentions: mentions.length > 0 ? mentions : void 0,
|
|
3895
|
+
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
3896
|
+
mentionCount: mentions.length > 0 ? mentions.length : void 0
|
|
3897
|
+
},
|
|
3898
|
+
// Backward-compatible convenience fields.
|
|
3899
|
+
chatType,
|
|
3900
|
+
senderId,
|
|
3901
|
+
senderName: senderDisplayName,
|
|
3902
|
+
senderDisplayName,
|
|
3903
|
+
toId,
|
|
3904
|
+
ts: message.data.ts
|
|
3905
|
+
};
|
|
3906
|
+
if (opts.raw) {
|
|
3907
|
+
console.log(JSON.stringify(payload));
|
|
3908
|
+
} else {
|
|
3909
|
+
console.log(
|
|
3910
|
+
`[${chatType}] ${payload.senderName || payload.senderId} -> ${payload.threadId}: ${payload.content}`
|
|
3298
3911
|
);
|
|
3299
3912
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
message: error instanceof Error ? error.message : String(error)
|
|
3314
|
-
});
|
|
3315
|
-
console.error(
|
|
3316
|
-
`Listener error: ${error instanceof Error ? error.message : String(error)}`
|
|
3317
|
-
);
|
|
3318
|
-
});
|
|
3319
|
-
await new Promise((resolve) => {
|
|
3320
|
-
let settled = false;
|
|
3321
|
-
let recycleTimer = null;
|
|
3322
|
-
let recycleForceExitTimer = null;
|
|
3323
|
-
let heartbeatTimer = null;
|
|
3324
|
-
let recyclePendingExit = false;
|
|
3325
|
-
let unregisterShutdown = () => {
|
|
3326
|
-
};
|
|
3327
|
-
const finish = () => {
|
|
3328
|
-
if (settled) return;
|
|
3329
|
-
settled = true;
|
|
3330
|
-
if (recycleTimer) {
|
|
3331
|
-
clearTimeout(recycleTimer);
|
|
3332
|
-
recycleTimer = null;
|
|
3333
|
-
}
|
|
3334
|
-
if (recycleForceExitTimer && !recyclePendingExit) {
|
|
3335
|
-
clearTimeout(recycleForceExitTimer);
|
|
3336
|
-
recycleForceExitTimer = null;
|
|
3337
|
-
}
|
|
3338
|
-
if (heartbeatTimer) {
|
|
3339
|
-
clearInterval(heartbeatTimer);
|
|
3340
|
-
heartbeatTimer = null;
|
|
3913
|
+
await emitWebhook(payload);
|
|
3914
|
+
if (opts.echo && rawText.trim().length > 0) {
|
|
3915
|
+
try {
|
|
3916
|
+
await api.sendMessage(
|
|
3917
|
+
{ msg: processedText },
|
|
3918
|
+
message.threadId,
|
|
3919
|
+
message.type
|
|
3920
|
+
);
|
|
3921
|
+
} catch (error) {
|
|
3922
|
+
console.error(
|
|
3923
|
+
`Echo failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3924
|
+
);
|
|
3925
|
+
}
|
|
3341
3926
|
}
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
};
|
|
3345
|
-
resolve();
|
|
3346
|
-
};
|
|
3347
|
-
api.listener.on("closed", (code, reason) => {
|
|
3348
|
-
console.log(`Listener closed (${code}) ${reason || ""}`);
|
|
3927
|
+
});
|
|
3928
|
+
api.listener.on("error", (error) => {
|
|
3349
3929
|
writeDebugLine(
|
|
3350
|
-
"listen.
|
|
3930
|
+
"listen.error",
|
|
3351
3931
|
{
|
|
3352
3932
|
profile,
|
|
3353
|
-
|
|
3354
|
-
reason: reason || void 0,
|
|
3933
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3355
3934
|
sessionId
|
|
3356
3935
|
},
|
|
3357
3936
|
command
|
|
3358
3937
|
);
|
|
3359
|
-
emitLifecycle("
|
|
3360
|
-
|
|
3361
|
-
reason: reason || void 0
|
|
3938
|
+
emitLifecycle("error", {
|
|
3939
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3362
3940
|
});
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3941
|
+
console.error(
|
|
3942
|
+
`Listener error: ${error instanceof Error ? error.message : String(error)}`
|
|
3943
|
+
);
|
|
3366
3944
|
});
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3945
|
+
await new Promise((resolve) => {
|
|
3946
|
+
let settled = false;
|
|
3947
|
+
let recycleTimer = null;
|
|
3948
|
+
let recycleForceExitTimer = null;
|
|
3949
|
+
let heartbeatTimer = null;
|
|
3950
|
+
let recyclePendingExit = false;
|
|
3951
|
+
let unregisterShutdown = () => {
|
|
3952
|
+
};
|
|
3953
|
+
const finish = () => {
|
|
3954
|
+
if (settled) return;
|
|
3955
|
+
settled = true;
|
|
3956
|
+
if (recycleTimer) {
|
|
3957
|
+
clearTimeout(recycleTimer);
|
|
3958
|
+
recycleTimer = null;
|
|
3959
|
+
}
|
|
3960
|
+
if (recycleForceExitTimer && !recyclePendingExit) {
|
|
3961
|
+
clearTimeout(recycleForceExitTimer);
|
|
3962
|
+
recycleForceExitTimer = null;
|
|
3963
|
+
}
|
|
3964
|
+
if (heartbeatTimer) {
|
|
3965
|
+
clearInterval(heartbeatTimer);
|
|
3966
|
+
heartbeatTimer = null;
|
|
3967
|
+
}
|
|
3968
|
+
if (keepAliveRestartTimer) {
|
|
3969
|
+
clearTimeout(keepAliveRestartTimer);
|
|
3970
|
+
keepAliveRestartTimer = null;
|
|
3971
|
+
}
|
|
3972
|
+
unregisterShutdown();
|
|
3973
|
+
unregisterShutdown = () => {
|
|
3974
|
+
};
|
|
3975
|
+
resolve();
|
|
3976
|
+
};
|
|
3977
|
+
api.listener.on("closed", (code, reason) => {
|
|
3978
|
+
console.log(`Listener closed (${code}) ${reason || ""}`);
|
|
3385
3979
|
writeDebugLine(
|
|
3386
|
-
"listen.
|
|
3980
|
+
"listen.closed",
|
|
3387
3981
|
{
|
|
3388
3982
|
profile,
|
|
3389
|
-
|
|
3390
|
-
|
|
3983
|
+
code,
|
|
3984
|
+
reason: reason || void 0,
|
|
3391
3985
|
sessionId
|
|
3392
3986
|
},
|
|
3393
3987
|
command
|
|
3394
3988
|
);
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3989
|
+
emitLifecycle("closed", {
|
|
3990
|
+
code,
|
|
3991
|
+
reason: reason || void 0
|
|
3992
|
+
});
|
|
3993
|
+
if (!opts.keepAlive || supervised) {
|
|
3994
|
+
finish();
|
|
3995
|
+
return;
|
|
3996
|
+
}
|
|
3997
|
+
const shouldRestart = keepAliveRestartOnAnyClose || code === 1e3 || code === 3e3;
|
|
3998
|
+
if (!shouldRestart) return;
|
|
3999
|
+
if (keepAliveRestartTimer) {
|
|
4000
|
+
clearTimeout(keepAliveRestartTimer);
|
|
4001
|
+
}
|
|
4002
|
+
keepAliveRestartTimer = setTimeout(() => {
|
|
4003
|
+
keepAliveRestartTimer = null;
|
|
4004
|
+
writeDebugLine(
|
|
4005
|
+
"listen.keepalive.restart",
|
|
4006
|
+
{
|
|
4007
|
+
profile,
|
|
4008
|
+
code,
|
|
4009
|
+
reason: reason || void 0,
|
|
4010
|
+
delayMs: keepAliveRestartDelayMs,
|
|
4011
|
+
sessionId
|
|
4012
|
+
},
|
|
4013
|
+
command
|
|
4014
|
+
);
|
|
4015
|
+
try {
|
|
4016
|
+
api.listener.start({ retryOnClose: true });
|
|
4017
|
+
} catch (error) {
|
|
4018
|
+
if (!isListenerAlreadyStarted(error)) {
|
|
4019
|
+
writeDebugLine(
|
|
4020
|
+
"listen.keepalive.restart_error",
|
|
4021
|
+
{
|
|
4022
|
+
profile,
|
|
4023
|
+
code,
|
|
4024
|
+
reason: reason || void 0,
|
|
4025
|
+
delayMs: keepAliveRestartDelayMs,
|
|
4026
|
+
message: toErrorText(error),
|
|
4027
|
+
sessionId
|
|
4028
|
+
},
|
|
4029
|
+
command
|
|
4030
|
+
);
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
}, keepAliveRestartDelayMs);
|
|
4034
|
+
});
|
|
4035
|
+
const onSignal = () => {
|
|
3402
4036
|
try {
|
|
3403
4037
|
api.listener.stop();
|
|
3404
4038
|
} catch {
|
|
3405
4039
|
}
|
|
3406
4040
|
finish();
|
|
3407
|
-
}
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
4041
|
+
};
|
|
4042
|
+
unregisterShutdown = registerShutdownCallback(onSignal);
|
|
4043
|
+
if (lifecycleEventsEnabled && heartbeatMs > 0) {
|
|
4044
|
+
heartbeatTimer = setInterval(() => {
|
|
4045
|
+
emitLifecycle("heartbeat");
|
|
4046
|
+
}, heartbeatMs);
|
|
4047
|
+
}
|
|
4048
|
+
if (recycleEnabled) {
|
|
4049
|
+
recycleTimer = setTimeout(() => {
|
|
4050
|
+
console.error(
|
|
4051
|
+
`Listener recycle triggered after ${recycleMs}ms to prevent stale session.`
|
|
4052
|
+
);
|
|
4053
|
+
writeDebugLine(
|
|
4054
|
+
"listen.recycle",
|
|
4055
|
+
{
|
|
4056
|
+
profile,
|
|
4057
|
+
recycleMs,
|
|
4058
|
+
exitCode: recycleExitCode,
|
|
4059
|
+
sessionId
|
|
4060
|
+
},
|
|
4061
|
+
command
|
|
4062
|
+
);
|
|
4063
|
+
process.exitCode = recycleExitCode;
|
|
4064
|
+
recyclePendingExit = true;
|
|
4065
|
+
recycleForceExitTimer = setTimeout(() => {
|
|
4066
|
+
recycleForceExitTimer = null;
|
|
4067
|
+
process.exit(recycleExitCode);
|
|
4068
|
+
}, 3e3);
|
|
4069
|
+
recycleForceExitTimer.unref();
|
|
4070
|
+
try {
|
|
4071
|
+
api.listener.stop();
|
|
4072
|
+
} catch {
|
|
4073
|
+
}
|
|
4074
|
+
finish();
|
|
4075
|
+
}, recycleMs);
|
|
4076
|
+
}
|
|
4077
|
+
api.listener.start({ retryOnClose: supervised ? false : Boolean(opts.keepAlive) });
|
|
4078
|
+
});
|
|
4079
|
+
} finally {
|
|
4080
|
+
unregisterResourceCleanup();
|
|
4081
|
+
await cleanupListenResources();
|
|
4082
|
+
}
|
|
3411
4083
|
}
|
|
3412
4084
|
)
|
|
3413
4085
|
);
|