openzca 0.1.22 → 0.1.25
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 +33 -1
- package/dist/cli.js +897 -317
- 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";
|
|
@@ -739,11 +740,444 @@ function collectIdsFromCacheEntries(entries, keys) {
|
|
|
739
740
|
}
|
|
740
741
|
return ids;
|
|
741
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
|
+
}
|
|
742
1176
|
async function resolveUploadThreadType(api, profile, threadId, groupFlag, command) {
|
|
743
1177
|
if (groupFlag) {
|
|
744
1178
|
return { type: ThreadType.Group, reason: "explicit_group_flag" };
|
|
745
1179
|
}
|
|
746
|
-
const autoDetectEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_AUTO_THREAD_TYPE",
|
|
1180
|
+
const autoDetectEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_AUTO_THREAD_TYPE", false);
|
|
747
1181
|
if (!autoDetectEnabled) {
|
|
748
1182
|
return { type: ThreadType.User, reason: "auto_detect_disabled" };
|
|
749
1183
|
}
|
|
@@ -1121,7 +1555,18 @@ async function withUploadListener(api, command, task) {
|
|
|
1121
1555
|
api.listener.off("closed", sinkClosed);
|
|
1122
1556
|
}
|
|
1123
1557
|
}
|
|
1124
|
-
async function
|
|
1558
|
+
async function fetchRecentGroupMessagesViaApi(api, threadId, count) {
|
|
1559
|
+
const historyApi = api.getGroupChatHistory;
|
|
1560
|
+
if (typeof historyApi !== "function") {
|
|
1561
|
+
throw new Error(
|
|
1562
|
+
"Current zca-js build does not expose getGroupChatHistory(). Upgrade zca-js/openzca."
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
const response = await historyApi(threadId, count);
|
|
1566
|
+
const messages = Array.isArray(response?.groupMsgs) ? response.groupMsgs : [];
|
|
1567
|
+
return messages.slice(0, count);
|
|
1568
|
+
}
|
|
1569
|
+
async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
1125
1570
|
return new Promise((resolve, reject) => {
|
|
1126
1571
|
let settled = false;
|
|
1127
1572
|
const collected = [];
|
|
@@ -1148,13 +1593,13 @@ async function fetchRecentMessagesViaListener(api, threadId, threadType, count)
|
|
|
1148
1593
|
};
|
|
1149
1594
|
const onConnected = () => {
|
|
1150
1595
|
try {
|
|
1151
|
-
api.listener.requestOldMessages(
|
|
1596
|
+
api.listener.requestOldMessages(ThreadType.User, null);
|
|
1152
1597
|
} catch (error) {
|
|
1153
1598
|
finish(error);
|
|
1154
1599
|
}
|
|
1155
1600
|
};
|
|
1156
1601
|
const onOldMessages = (messages, type) => {
|
|
1157
|
-
if (type !==
|
|
1602
|
+
if (type !== ThreadType.User) return;
|
|
1158
1603
|
const typedMessages = messages;
|
|
1159
1604
|
for (const message of typedMessages) {
|
|
1160
1605
|
if (message.threadId === threadId) {
|
|
@@ -2462,6 +2907,43 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
2462
2907
|
);
|
|
2463
2908
|
}
|
|
2464
2909
|
await assertFilesExist(attachments);
|
|
2910
|
+
const ipcResult = await tryUploadViaListenerIpc(
|
|
2911
|
+
profile,
|
|
2912
|
+
threadId,
|
|
2913
|
+
threadResolution.type,
|
|
2914
|
+
attachments,
|
|
2915
|
+
command
|
|
2916
|
+
);
|
|
2917
|
+
if (ipcResult.handled) {
|
|
2918
|
+
writeDebugLine(
|
|
2919
|
+
"msg.upload.ipc.done",
|
|
2920
|
+
{
|
|
2921
|
+
threadId,
|
|
2922
|
+
threadType: threadResolution.type === ThreadType.Group ? "group" : "user"
|
|
2923
|
+
},
|
|
2924
|
+
command
|
|
2925
|
+
);
|
|
2926
|
+
output(ipcResult.response, false);
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
writeDebugLine(
|
|
2930
|
+
"msg.upload.ipc.fallback",
|
|
2931
|
+
{
|
|
2932
|
+
threadId,
|
|
2933
|
+
threadType: threadResolution.type === ThreadType.Group ? "group" : "user",
|
|
2934
|
+
reason: ipcResult.reason
|
|
2935
|
+
},
|
|
2936
|
+
command
|
|
2937
|
+
);
|
|
2938
|
+
const enforceSingleOwner = parseBooleanFromEnv("OPENZCA_UPLOAD_ENFORCE_SINGLE_OWNER", true);
|
|
2939
|
+
if (enforceSingleOwner) {
|
|
2940
|
+
const owner = await readActiveListenerOwner(profile);
|
|
2941
|
+
if (owner && owner.pid !== process.pid) {
|
|
2942
|
+
throw new Error(
|
|
2943
|
+
`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.`
|
|
2944
|
+
);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2465
2947
|
const response = await withUploadListener(
|
|
2466
2948
|
api,
|
|
2467
2949
|
command,
|
|
@@ -2481,17 +2963,16 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
2481
2963
|
}
|
|
2482
2964
|
)
|
|
2483
2965
|
);
|
|
2484
|
-
msg.command("recent <threadId>").option("-g, --group", "List recent messages for group thread").option("-n, --count <count>", "Number of messages (default: 20)", "20").option("-j, --json", "JSON output").description("List recent messages
|
|
2966
|
+
msg.command("recent <threadId>").option("-g, --group", "List recent messages for group thread").option("-n, --count <count>", "Number of messages (default: 20)", "20").option("-j, --json", "JSON output").description("List recent messages (group uses direct history API)").action(
|
|
2485
2967
|
wrapAction(
|
|
2486
2968
|
async (threadId, opts, command) => {
|
|
2487
2969
|
const { api } = await requireApi(command);
|
|
2488
2970
|
const parsedCount = Number(opts.count);
|
|
2489
2971
|
const count = Number.isFinite(parsedCount) ? Math.min(Math.max(Math.trunc(parsedCount), 1), 200) : 20;
|
|
2490
2972
|
const threadType = opts.group ? ThreadType.Group : ThreadType.User;
|
|
2491
|
-
const messages = await
|
|
2973
|
+
const messages = opts.group ? await fetchRecentGroupMessagesViaApi(api, threadId, count) : await fetchRecentUserMessagesViaListener(
|
|
2492
2974
|
api,
|
|
2493
2975
|
threadId,
|
|
2494
|
-
threadType,
|
|
2495
2976
|
count
|
|
2496
2977
|
);
|
|
2497
2978
|
const rows = messages.map((message) => ({
|
|
@@ -3124,6 +3605,14 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
3124
3605
|
) ?? 3e4;
|
|
3125
3606
|
const lifecycleEventsEnabled = supervised && Boolean(opts.raw);
|
|
3126
3607
|
const recycleEnabled = !supervised && Boolean(opts.keepAlive) && recycleMs > 0;
|
|
3608
|
+
const keepAliveRestartDelayMs = parsePositiveIntFromEnv(
|
|
3609
|
+
"OPENZCA_LISTEN_KEEPALIVE_RESTART_DELAY_MS",
|
|
3610
|
+
2e3
|
|
3611
|
+
);
|
|
3612
|
+
const keepAliveRestartOnAnyClose = parseBooleanFromEnv(
|
|
3613
|
+
"OPENZCA_LISTEN_KEEPALIVE_RESTART_ON_ANY_CLOSE",
|
|
3614
|
+
false
|
|
3615
|
+
);
|
|
3127
3616
|
const recycleExitCode = 75;
|
|
3128
3617
|
const includeReplyContext = parseToggleDefaultTrue(
|
|
3129
3618
|
process.env.OPENZCA_LISTEN_INCLUDE_QUOTE_CONTEXT
|
|
@@ -3145,212 +3634,229 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
3145
3634
|
})
|
|
3146
3635
|
);
|
|
3147
3636
|
};
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
"content-type": "application/json"
|
|
3637
|
+
const enforceSingleOwner = parseBooleanFromEnv("OPENZCA_LISTEN_ENFORCE_SINGLE_OWNER", true);
|
|
3638
|
+
let ownerLock = null;
|
|
3639
|
+
let ipcServer = null;
|
|
3640
|
+
let ipcSocketPath;
|
|
3641
|
+
let resourcesCleaned = false;
|
|
3642
|
+
const cleanupListenResources = async () => {
|
|
3643
|
+
if (resourcesCleaned) return;
|
|
3644
|
+
resourcesCleaned = true;
|
|
3645
|
+
if (ipcServer) {
|
|
3646
|
+
await ipcServer.close();
|
|
3647
|
+
ipcServer = null;
|
|
3648
|
+
}
|
|
3649
|
+
if (ownerLock) {
|
|
3650
|
+
await ownerLock.release();
|
|
3651
|
+
ownerLock = null;
|
|
3652
|
+
}
|
|
3653
|
+
};
|
|
3654
|
+
const unregisterResourceCleanup = registerShutdownCallback(async () => {
|
|
3655
|
+
await cleanupListenResources();
|
|
3656
|
+
});
|
|
3657
|
+
try {
|
|
3658
|
+
if (enforceSingleOwner) {
|
|
3659
|
+
ownerLock = await acquireListenerOwnerLock(profile, sessionId, command);
|
|
3660
|
+
writeDebugLine(
|
|
3661
|
+
"listen.owner.acquired",
|
|
3662
|
+
{
|
|
3663
|
+
profile,
|
|
3664
|
+
lockPath: ownerLock.lockPath,
|
|
3665
|
+
pid: process.pid,
|
|
3666
|
+
sessionId
|
|
3179
3667
|
},
|
|
3180
|
-
|
|
3181
|
-
});
|
|
3182
|
-
if (!response.ok) {
|
|
3183
|
-
console.error(`Webhook response: ${response.status}`);
|
|
3184
|
-
}
|
|
3185
|
-
} catch (error) {
|
|
3186
|
-
console.error(
|
|
3187
|
-
`Webhook failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3668
|
+
command
|
|
3188
3669
|
);
|
|
3189
3670
|
}
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
console.log("
|
|
3193
|
-
|
|
3194
|
-
"
|
|
3195
|
-
|
|
3196
|
-
profile,
|
|
3197
|
-
sessionId
|
|
3198
|
-
},
|
|
3199
|
-
command
|
|
3200
|
-
);
|
|
3201
|
-
emitLifecycle("connected");
|
|
3202
|
-
});
|
|
3203
|
-
api.listener.on("message", async (message) => {
|
|
3204
|
-
const messageData = message.data;
|
|
3205
|
-
const rawContent = messageData.content;
|
|
3206
|
-
const msgType = getStringCandidate(messageData, ["msgType"]);
|
|
3207
|
-
let quote = normalizeQuoteContext(messageData.quote);
|
|
3208
|
-
const parsedContent = normalizeStructuredContent(rawContent);
|
|
3209
|
-
const hasParsedStructuredContent = parsedContent !== rawContent;
|
|
3210
|
-
const rawText = typeof rawContent === "string" ? rawContent : "";
|
|
3211
|
-
const mediaKind = detectInboundMediaKind(msgType, parsedContent);
|
|
3212
|
-
const maxMediaFiles = parseMaxInboundMediaFiles();
|
|
3213
|
-
const remoteMediaUrls = mediaKind && maxMediaFiles > 0 ? resolvePreferredMediaUrls(mediaKind, parsedContent).slice(0, maxMediaFiles) : [];
|
|
3214
|
-
const quoteRemoteMediaUrls = quote && downloadQuoteMedia && maxMediaFiles > 0 ? (quote.mediaUrls ?? []).slice(0, maxMediaFiles) : [];
|
|
3671
|
+
ipcServer = await startListenerIpcServer(api, profile, sessionId, command);
|
|
3672
|
+
ipcSocketPath = ipcServer?.socketPath;
|
|
3673
|
+
console.log("Listening... Press Ctrl+C to stop.");
|
|
3674
|
+
if (supervised && opts.keepAlive) {
|
|
3675
|
+
console.error("Warning: --supervised ignores internal --keep-alive reconnect ownership.");
|
|
3676
|
+
}
|
|
3215
3677
|
writeDebugLine(
|
|
3216
|
-
"listen.
|
|
3678
|
+
"listen.start",
|
|
3217
3679
|
{
|
|
3218
3680
|
profile,
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3681
|
+
mediaDir: resolveInboundMediaDir(profile),
|
|
3682
|
+
maxMediaBytes: parseMaxInboundMediaBytes(),
|
|
3683
|
+
maxMediaFiles: parseMaxInboundMediaFiles(),
|
|
3684
|
+
includeMediaUrl: process.env.OPENZCA_LISTEN_INCLUDE_MEDIA_URL?.trim() ?? null,
|
|
3685
|
+
keepAlive: Boolean(opts.keepAlive),
|
|
3686
|
+
keepAliveRestartDelayMs: Boolean(opts.keepAlive) ? keepAliveRestartDelayMs : void 0,
|
|
3687
|
+
keepAliveRestartOnAnyClose: Boolean(opts.keepAlive) ? keepAliveRestartOnAnyClose : void 0,
|
|
3688
|
+
supervised,
|
|
3689
|
+
lifecycleEventsEnabled,
|
|
3690
|
+
heartbeatMs: lifecycleEventsEnabled ? heartbeatMs : void 0,
|
|
3691
|
+
recycleMs: recycleEnabled ? recycleMs : void 0,
|
|
3692
|
+
includeReplyContext,
|
|
3693
|
+
downloadQuoteMedia,
|
|
3694
|
+
sessionId,
|
|
3695
|
+
singleOwner: enforceSingleOwner,
|
|
3696
|
+
ipcSocketPath
|
|
3229
3697
|
},
|
|
3230
3698
|
command
|
|
3231
3699
|
);
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
const localEntries = mediaEntries.filter((entry) => Boolean(entry.mediaPath));
|
|
3253
|
-
const mediaPaths = localEntries.map((entry) => entry.mediaPath);
|
|
3254
|
-
const mediaUrls = localEntries.length > 0 ? localEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value)) : mediaEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value));
|
|
3255
|
-
const mediaTypes = localEntries.length > 0 ? localEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value)) : mediaEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value));
|
|
3256
|
-
const mediaPath = mediaPaths[0];
|
|
3257
|
-
const mediaUrl = mediaUrls[0];
|
|
3258
|
-
const mediaType = mediaTypes[0];
|
|
3259
|
-
const quoteLocalEntries = quoteMediaEntries.filter((entry) => Boolean(entry.mediaPath));
|
|
3260
|
-
const quoteMediaPaths = quoteLocalEntries.map((entry) => entry.mediaPath);
|
|
3261
|
-
const quoteMediaUrls = quoteLocalEntries.length > 0 ? quoteLocalEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value)) : quoteMediaEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value));
|
|
3262
|
-
const quoteMediaTypes = quoteLocalEntries.length > 0 ? quoteLocalEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value)) : quoteMediaEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value));
|
|
3263
|
-
const quoteMediaPath = quoteMediaPaths[0];
|
|
3264
|
-
const quoteMediaUrl = quoteMediaUrls[0];
|
|
3265
|
-
const quoteMediaType = quoteMediaTypes[0];
|
|
3266
|
-
if (quote) {
|
|
3267
|
-
quote = {
|
|
3268
|
-
...quote,
|
|
3269
|
-
mediaPath: quoteMediaPath,
|
|
3270
|
-
mediaPaths: quoteMediaPaths.length > 0 ? quoteMediaPaths : void 0,
|
|
3271
|
-
mediaUrl: quoteMediaUrl,
|
|
3272
|
-
mediaUrls: quoteMediaUrls.length > 0 ? quoteMediaUrls : quote.mediaUrls,
|
|
3273
|
-
mediaType: quoteMediaType,
|
|
3274
|
-
mediaTypes: quoteMediaTypes.length > 0 ? quoteMediaTypes : void 0
|
|
3275
|
-
};
|
|
3276
|
-
}
|
|
3277
|
-
const replyContextText = includeReplyContext && quote ? buildReplyContextText(quote) : "";
|
|
3278
|
-
const replyMediaText = includeReplyContext && quoteMediaEntries.length > 0 ? buildReplyMediaAttachedText({ mediaEntries: quoteMediaEntries }) : "";
|
|
3279
|
-
const caption = rawText.trim().length > 0 && !hasParsedStructuredContent ? rawText.trim() : summarizeStructuredContent(msgType, parsedContent);
|
|
3280
|
-
let processedText = mediaEntries.length ? buildMediaAttachedText({
|
|
3281
|
-
mediaEntries,
|
|
3282
|
-
fallbackKind: mediaKind,
|
|
3283
|
-
caption
|
|
3284
|
-
}) : rawText.trim().length > 0 && !hasParsedStructuredContent ? rawText : summarizeStructuredContent(msgType, parsedContent);
|
|
3285
|
-
if (!processedText.trim() && !replyContextText && !replyMediaText) return;
|
|
3286
|
-
if (opts.prefix && processedText.trim().length > 0) {
|
|
3287
|
-
if (!processedText.startsWith(opts.prefix)) return;
|
|
3288
|
-
processedText = processedText.slice(opts.prefix.length).trimStart();
|
|
3700
|
+
emitLifecycle("session_id");
|
|
3701
|
+
let keepAliveRestartTimer = null;
|
|
3702
|
+
async function emitWebhook(payload) {
|
|
3703
|
+
if (!opts.webhook) return;
|
|
3704
|
+
try {
|
|
3705
|
+
const response = await fetch(opts.webhook, {
|
|
3706
|
+
method: "POST",
|
|
3707
|
+
headers: {
|
|
3708
|
+
"content-type": "application/json"
|
|
3709
|
+
},
|
|
3710
|
+
body: JSON.stringify(payload)
|
|
3711
|
+
});
|
|
3712
|
+
if (!response.ok) {
|
|
3713
|
+
console.error(`Webhook response: ${response.status}`);
|
|
3714
|
+
}
|
|
3715
|
+
} catch (error) {
|
|
3716
|
+
console.error(
|
|
3717
|
+
`Webhook failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3718
|
+
);
|
|
3719
|
+
}
|
|
3289
3720
|
}
|
|
3290
|
-
|
|
3291
|
-
|
|
3721
|
+
api.listener.on("connected", () => {
|
|
3722
|
+
console.log("Connected to Zalo websocket.");
|
|
3723
|
+
if (keepAliveRestartTimer) {
|
|
3724
|
+
clearTimeout(keepAliveRestartTimer);
|
|
3725
|
+
keepAliveRestartTimer = null;
|
|
3726
|
+
}
|
|
3727
|
+
writeDebugLine(
|
|
3728
|
+
"listen.connected",
|
|
3729
|
+
{
|
|
3730
|
+
profile,
|
|
3731
|
+
sessionId
|
|
3732
|
+
},
|
|
3733
|
+
command
|
|
3734
|
+
);
|
|
3735
|
+
emitLifecycle("connected");
|
|
3736
|
+
});
|
|
3737
|
+
api.listener.on("message", async (message) => {
|
|
3738
|
+
const messageData = message.data;
|
|
3739
|
+
const rawContent = messageData.content;
|
|
3740
|
+
const msgType = getStringCandidate(messageData, ["msgType"]);
|
|
3741
|
+
let quote = normalizeQuoteContext(messageData.quote);
|
|
3742
|
+
const parsedContent = normalizeStructuredContent(rawContent);
|
|
3743
|
+
const hasParsedStructuredContent = parsedContent !== rawContent;
|
|
3744
|
+
const rawText = typeof rawContent === "string" ? rawContent : "";
|
|
3745
|
+
const mediaKind = detectInboundMediaKind(msgType, parsedContent);
|
|
3746
|
+
const maxMediaFiles = parseMaxInboundMediaFiles();
|
|
3747
|
+
const remoteMediaUrls = mediaKind && maxMediaFiles > 0 ? resolvePreferredMediaUrls(mediaKind, parsedContent).slice(0, maxMediaFiles) : [];
|
|
3748
|
+
const quoteRemoteMediaUrls = quote && downloadQuoteMedia && maxMediaFiles > 0 ? (quote.mediaUrls ?? []).slice(0, maxMediaFiles) : [];
|
|
3749
|
+
writeDebugLine(
|
|
3750
|
+
"listen.media.detected",
|
|
3751
|
+
{
|
|
3752
|
+
profile,
|
|
3753
|
+
threadId: message.threadId,
|
|
3754
|
+
msgType: msgType || void 0,
|
|
3755
|
+
mediaKind,
|
|
3756
|
+
hasParsedStructuredContent,
|
|
3757
|
+
remoteMediaUrls,
|
|
3758
|
+
hasQuote: Boolean(quote),
|
|
3759
|
+
quoteOwnerId: quote?.ownerId,
|
|
3760
|
+
quoteGlobalMsgId: quote?.globalMsgId,
|
|
3761
|
+
quoteCliMsgId: quote?.cliMsgId,
|
|
3762
|
+
quoteRemoteMediaUrls
|
|
3763
|
+
},
|
|
3764
|
+
command
|
|
3765
|
+
);
|
|
3766
|
+
const [mediaEntries, quoteMediaEntries] = await Promise.all([
|
|
3767
|
+
mediaKind ? cacheRemoteMediaEntries({
|
|
3768
|
+
profile,
|
|
3769
|
+
urls: remoteMediaUrls,
|
|
3770
|
+
kind: mediaKind,
|
|
3771
|
+
command,
|
|
3772
|
+
warningLabel: "inbound media",
|
|
3773
|
+
debugErrorEvent: "listen.media.cache_error",
|
|
3774
|
+
debugUrlKey: "mediaUrl"
|
|
3775
|
+
}) : Promise.resolve([]),
|
|
3776
|
+
cacheRemoteMediaEntries({
|
|
3777
|
+
profile,
|
|
3778
|
+
urls: quoteRemoteMediaUrls,
|
|
3779
|
+
kind: "file",
|
|
3780
|
+
command,
|
|
3781
|
+
warningLabel: "quoted media",
|
|
3782
|
+
debugErrorEvent: "listen.quote_media.cache_error",
|
|
3783
|
+
debugUrlKey: "quoteMediaUrl"
|
|
3784
|
+
})
|
|
3785
|
+
]);
|
|
3786
|
+
const localEntries = mediaEntries.filter((entry) => Boolean(entry.mediaPath));
|
|
3787
|
+
const mediaPaths = localEntries.map((entry) => entry.mediaPath);
|
|
3788
|
+
const mediaUrls = localEntries.length > 0 ? localEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value)) : mediaEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value));
|
|
3789
|
+
const mediaTypes = localEntries.length > 0 ? localEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value)) : mediaEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value));
|
|
3790
|
+
const mediaPath = mediaPaths[0];
|
|
3791
|
+
const mediaUrl = mediaUrls[0];
|
|
3792
|
+
const mediaType = mediaTypes[0];
|
|
3793
|
+
const quoteLocalEntries = quoteMediaEntries.filter((entry) => Boolean(entry.mediaPath));
|
|
3794
|
+
const quoteMediaPaths = quoteLocalEntries.map((entry) => entry.mediaPath);
|
|
3795
|
+
const quoteMediaUrls = quoteLocalEntries.length > 0 ? quoteLocalEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value)) : quoteMediaEntries.map((entry) => entry.mediaUrl).filter((value) => Boolean(value));
|
|
3796
|
+
const quoteMediaTypes = quoteLocalEntries.length > 0 ? quoteLocalEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value)) : quoteMediaEntries.map((entry) => entry.mediaType).filter((value) => Boolean(value));
|
|
3797
|
+
const quoteMediaPath = quoteMediaPaths[0];
|
|
3798
|
+
const quoteMediaUrl = quoteMediaUrls[0];
|
|
3799
|
+
const quoteMediaType = quoteMediaTypes[0];
|
|
3800
|
+
if (quote) {
|
|
3801
|
+
quote = {
|
|
3802
|
+
...quote,
|
|
3803
|
+
mediaPath: quoteMediaPath,
|
|
3804
|
+
mediaPaths: quoteMediaPaths.length > 0 ? quoteMediaPaths : void 0,
|
|
3805
|
+
mediaUrl: quoteMediaUrl,
|
|
3806
|
+
mediaUrls: quoteMediaUrls.length > 0 ? quoteMediaUrls : quote.mediaUrls,
|
|
3807
|
+
mediaType: quoteMediaType,
|
|
3808
|
+
mediaTypes: quoteMediaTypes.length > 0 ? quoteMediaTypes : void 0
|
|
3809
|
+
};
|
|
3810
|
+
}
|
|
3811
|
+
const replyContextText = includeReplyContext && quote ? buildReplyContextText(quote) : "";
|
|
3812
|
+
const replyMediaText = includeReplyContext && quoteMediaEntries.length > 0 ? buildReplyMediaAttachedText({ mediaEntries: quoteMediaEntries }) : "";
|
|
3813
|
+
const caption = rawText.trim().length > 0 && !hasParsedStructuredContent ? rawText.trim() : summarizeStructuredContent(msgType, parsedContent);
|
|
3814
|
+
let processedText = mediaEntries.length ? buildMediaAttachedText({
|
|
3815
|
+
mediaEntries,
|
|
3816
|
+
fallbackKind: mediaKind,
|
|
3817
|
+
caption
|
|
3818
|
+
}) : rawText.trim().length > 0 && !hasParsedStructuredContent ? rawText : summarizeStructuredContent(msgType, parsedContent);
|
|
3819
|
+
if (!processedText.trim() && !replyContextText && !replyMediaText) return;
|
|
3820
|
+
if (opts.prefix && processedText.trim().length > 0) {
|
|
3821
|
+
if (!processedText.startsWith(opts.prefix)) return;
|
|
3822
|
+
processedText = processedText.slice(opts.prefix.length).trimStart();
|
|
3823
|
+
}
|
|
3824
|
+
if (replyMediaText) {
|
|
3825
|
+
processedText = processedText.trim() ? `${processedText}
|
|
3292
3826
|
${replyMediaText}` : replyMediaText;
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3827
|
+
}
|
|
3828
|
+
if (replyContextText) {
|
|
3829
|
+
processedText = processedText.trim() ? `${processedText}
|
|
3296
3830
|
${replyContextText}` : replyContextText;
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
threadId: message.threadId,
|
|
3319
|
-
targetId: message.threadId,
|
|
3320
|
-
conversationId: message.threadId,
|
|
3321
|
-
msgId: message.data.msgId,
|
|
3322
|
-
cliMsgId: message.data.cliMsgId,
|
|
3323
|
-
content: processedText,
|
|
3324
|
-
type: message.type,
|
|
3325
|
-
timestamp,
|
|
3326
|
-
msgType: msgType || void 0,
|
|
3327
|
-
quote: quote ?? void 0,
|
|
3328
|
-
quoteMediaPath,
|
|
3329
|
-
quoteMediaPaths: quoteMediaPaths.length > 0 ? quoteMediaPaths : void 0,
|
|
3330
|
-
quoteMediaUrl,
|
|
3331
|
-
quoteMediaUrls: quoteMediaUrls.length > 0 ? quoteMediaUrls : void 0,
|
|
3332
|
-
quoteMediaType,
|
|
3333
|
-
quoteMediaTypes: quoteMediaTypes.length > 0 ? quoteMediaTypes : void 0,
|
|
3334
|
-
mediaPath,
|
|
3335
|
-
mediaPaths: mediaPaths.length > 0 ? mediaPaths : void 0,
|
|
3336
|
-
mediaUrl,
|
|
3337
|
-
mediaUrls: mediaUrls.length > 0 ? mediaUrls : void 0,
|
|
3338
|
-
mediaType,
|
|
3339
|
-
mediaTypes: mediaTypes.length > 0 ? mediaTypes : void 0,
|
|
3340
|
-
mediaKind: mediaKind ?? void 0,
|
|
3341
|
-
mentions: mentions.length > 0 ? mentions : void 0,
|
|
3342
|
-
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
3343
|
-
metadata: {
|
|
3344
|
-
isGroup: message.type === ThreadType.Group,
|
|
3345
|
-
chatType,
|
|
3831
|
+
}
|
|
3832
|
+
const chatType = message.type === ThreadType.Group ? "group" : "user";
|
|
3833
|
+
const senderId = getStringCandidate(messageData, ["uidFrom"]) || message.data.uidFrom;
|
|
3834
|
+
const senderDisplayNameRaw = getStringCandidate(messageData, [
|
|
3835
|
+
"dName",
|
|
3836
|
+
"fromD",
|
|
3837
|
+
"senderName",
|
|
3838
|
+
"displayName"
|
|
3839
|
+
]);
|
|
3840
|
+
const senderDisplayName = senderDisplayNameRaw || void 0;
|
|
3841
|
+
const senderNameForMetadata = message.type === ThreadType.Group ? senderDisplayName : void 0;
|
|
3842
|
+
const toId = getStringCandidate(messageData, ["idTo"]) || void 0;
|
|
3843
|
+
const threadName = typeof messageData.threadName === "string" ? messageData.threadName : typeof messageData.tName === "string" ? messageData.tName : void 0;
|
|
3844
|
+
const mentions = extractInboundMentions({
|
|
3845
|
+
messageData,
|
|
3846
|
+
parsedContent,
|
|
3847
|
+
rawText
|
|
3848
|
+
});
|
|
3849
|
+
const mentionIds = mentions.map((item) => item.uid);
|
|
3850
|
+
const timestamp = toEpochSeconds(message.data.ts);
|
|
3851
|
+
const payload = {
|
|
3346
3852
|
threadId: message.threadId,
|
|
3347
3853
|
targetId: message.threadId,
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3854
|
+
conversationId: message.threadId,
|
|
3855
|
+
msgId: message.data.msgId,
|
|
3856
|
+
cliMsgId: message.data.cliMsgId,
|
|
3857
|
+
content: processedText,
|
|
3858
|
+
type: message.type,
|
|
3859
|
+
timestamp,
|
|
3354
3860
|
msgType: msgType || void 0,
|
|
3355
3861
|
quote: quote ?? void 0,
|
|
3356
3862
|
quoteMediaPath,
|
|
@@ -3359,7 +3865,6 @@ ${replyContextText}` : replyContextText;
|
|
|
3359
3865
|
quoteMediaUrls: quoteMediaUrls.length > 0 ? quoteMediaUrls : void 0,
|
|
3360
3866
|
quoteMediaType,
|
|
3361
3867
|
quoteMediaTypes: quoteMediaTypes.length > 0 ? quoteMediaTypes : void 0,
|
|
3362
|
-
timestamp,
|
|
3363
3868
|
mediaPath,
|
|
3364
3869
|
mediaPaths: mediaPaths.length > 0 ? mediaPaths : void 0,
|
|
3365
3870
|
mediaUrl,
|
|
@@ -3369,147 +3874,222 @@ ${replyContextText}` : replyContextText;
|
|
|
3369
3874
|
mediaKind: mediaKind ?? void 0,
|
|
3370
3875
|
mentions: mentions.length > 0 ? mentions : void 0,
|
|
3371
3876
|
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3877
|
+
metadata: {
|
|
3878
|
+
isGroup: message.type === ThreadType.Group,
|
|
3879
|
+
chatType,
|
|
3880
|
+
threadId: message.threadId,
|
|
3881
|
+
targetId: message.threadId,
|
|
3882
|
+
threadName,
|
|
3883
|
+
senderName: senderNameForMetadata,
|
|
3884
|
+
senderDisplayName,
|
|
3885
|
+
senderId,
|
|
3886
|
+
fromId: senderId,
|
|
3887
|
+
toId,
|
|
3888
|
+
msgType: msgType || void 0,
|
|
3889
|
+
quote: quote ?? void 0,
|
|
3890
|
+
quoteMediaPath,
|
|
3891
|
+
quoteMediaPaths: quoteMediaPaths.length > 0 ? quoteMediaPaths : void 0,
|
|
3892
|
+
quoteMediaUrl,
|
|
3893
|
+
quoteMediaUrls: quoteMediaUrls.length > 0 ? quoteMediaUrls : void 0,
|
|
3894
|
+
quoteMediaType,
|
|
3895
|
+
quoteMediaTypes: quoteMediaTypes.length > 0 ? quoteMediaTypes : void 0,
|
|
3896
|
+
timestamp,
|
|
3897
|
+
mediaPath,
|
|
3898
|
+
mediaPaths: mediaPaths.length > 0 ? mediaPaths : void 0,
|
|
3899
|
+
mediaUrl,
|
|
3900
|
+
mediaUrls: mediaUrls.length > 0 ? mediaUrls : void 0,
|
|
3901
|
+
mediaType,
|
|
3902
|
+
mediaTypes: mediaTypes.length > 0 ? mediaTypes : void 0,
|
|
3903
|
+
mediaKind: mediaKind ?? void 0,
|
|
3904
|
+
mentions: mentions.length > 0 ? mentions : void 0,
|
|
3905
|
+
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
3906
|
+
mentionCount: mentions.length > 0 ? mentions.length : void 0
|
|
3907
|
+
},
|
|
3908
|
+
// Backward-compatible convenience fields.
|
|
3909
|
+
chatType,
|
|
3910
|
+
senderId,
|
|
3911
|
+
senderName: senderDisplayName,
|
|
3912
|
+
senderDisplayName,
|
|
3913
|
+
toId,
|
|
3914
|
+
ts: message.data.ts
|
|
3915
|
+
};
|
|
3916
|
+
if (opts.raw) {
|
|
3917
|
+
console.log(JSON.stringify(payload));
|
|
3918
|
+
} else {
|
|
3919
|
+
console.log(
|
|
3920
|
+
`[${chatType}] ${payload.senderName || payload.senderId} -> ${payload.threadId}: ${payload.content}`
|
|
3400
3921
|
);
|
|
3401
3922
|
}
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
message: error instanceof Error ? error.message : String(error)
|
|
3416
|
-
});
|
|
3417
|
-
console.error(
|
|
3418
|
-
`Listener error: ${error instanceof Error ? error.message : String(error)}`
|
|
3419
|
-
);
|
|
3420
|
-
});
|
|
3421
|
-
await new Promise((resolve) => {
|
|
3422
|
-
let settled = false;
|
|
3423
|
-
let recycleTimer = null;
|
|
3424
|
-
let recycleForceExitTimer = null;
|
|
3425
|
-
let heartbeatTimer = null;
|
|
3426
|
-
let recyclePendingExit = false;
|
|
3427
|
-
let unregisterShutdown = () => {
|
|
3428
|
-
};
|
|
3429
|
-
const finish = () => {
|
|
3430
|
-
if (settled) return;
|
|
3431
|
-
settled = true;
|
|
3432
|
-
if (recycleTimer) {
|
|
3433
|
-
clearTimeout(recycleTimer);
|
|
3434
|
-
recycleTimer = null;
|
|
3435
|
-
}
|
|
3436
|
-
if (recycleForceExitTimer && !recyclePendingExit) {
|
|
3437
|
-
clearTimeout(recycleForceExitTimer);
|
|
3438
|
-
recycleForceExitTimer = null;
|
|
3439
|
-
}
|
|
3440
|
-
if (heartbeatTimer) {
|
|
3441
|
-
clearInterval(heartbeatTimer);
|
|
3442
|
-
heartbeatTimer = null;
|
|
3923
|
+
await emitWebhook(payload);
|
|
3924
|
+
if (opts.echo && rawText.trim().length > 0) {
|
|
3925
|
+
try {
|
|
3926
|
+
await api.sendMessage(
|
|
3927
|
+
{ msg: processedText },
|
|
3928
|
+
message.threadId,
|
|
3929
|
+
message.type
|
|
3930
|
+
);
|
|
3931
|
+
} catch (error) {
|
|
3932
|
+
console.error(
|
|
3933
|
+
`Echo failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3934
|
+
);
|
|
3935
|
+
}
|
|
3443
3936
|
}
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
};
|
|
3447
|
-
resolve();
|
|
3448
|
-
};
|
|
3449
|
-
api.listener.on("closed", (code, reason) => {
|
|
3450
|
-
console.log(`Listener closed (${code}) ${reason || ""}`);
|
|
3937
|
+
});
|
|
3938
|
+
api.listener.on("error", (error) => {
|
|
3451
3939
|
writeDebugLine(
|
|
3452
|
-
"listen.
|
|
3940
|
+
"listen.error",
|
|
3453
3941
|
{
|
|
3454
3942
|
profile,
|
|
3455
|
-
|
|
3456
|
-
reason: reason || void 0,
|
|
3943
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3457
3944
|
sessionId
|
|
3458
3945
|
},
|
|
3459
3946
|
command
|
|
3460
3947
|
);
|
|
3461
|
-
emitLifecycle("
|
|
3462
|
-
|
|
3463
|
-
reason: reason || void 0
|
|
3948
|
+
emitLifecycle("error", {
|
|
3949
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3464
3950
|
});
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3951
|
+
console.error(
|
|
3952
|
+
`Listener error: ${error instanceof Error ? error.message : String(error)}`
|
|
3953
|
+
);
|
|
3468
3954
|
});
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3955
|
+
await new Promise((resolve) => {
|
|
3956
|
+
let settled = false;
|
|
3957
|
+
let recycleTimer = null;
|
|
3958
|
+
let recycleForceExitTimer = null;
|
|
3959
|
+
let heartbeatTimer = null;
|
|
3960
|
+
let recyclePendingExit = false;
|
|
3961
|
+
let unregisterShutdown = () => {
|
|
3962
|
+
};
|
|
3963
|
+
const finish = () => {
|
|
3964
|
+
if (settled) return;
|
|
3965
|
+
settled = true;
|
|
3966
|
+
if (recycleTimer) {
|
|
3967
|
+
clearTimeout(recycleTimer);
|
|
3968
|
+
recycleTimer = null;
|
|
3969
|
+
}
|
|
3970
|
+
if (recycleForceExitTimer && !recyclePendingExit) {
|
|
3971
|
+
clearTimeout(recycleForceExitTimer);
|
|
3972
|
+
recycleForceExitTimer = null;
|
|
3973
|
+
}
|
|
3974
|
+
if (heartbeatTimer) {
|
|
3975
|
+
clearInterval(heartbeatTimer);
|
|
3976
|
+
heartbeatTimer = null;
|
|
3977
|
+
}
|
|
3978
|
+
if (keepAliveRestartTimer) {
|
|
3979
|
+
clearTimeout(keepAliveRestartTimer);
|
|
3980
|
+
keepAliveRestartTimer = null;
|
|
3981
|
+
}
|
|
3982
|
+
unregisterShutdown();
|
|
3983
|
+
unregisterShutdown = () => {
|
|
3984
|
+
};
|
|
3985
|
+
resolve();
|
|
3986
|
+
};
|
|
3987
|
+
api.listener.on("closed", (code, reason) => {
|
|
3988
|
+
console.log(`Listener closed (${code}) ${reason || ""}`);
|
|
3487
3989
|
writeDebugLine(
|
|
3488
|
-
"listen.
|
|
3990
|
+
"listen.closed",
|
|
3489
3991
|
{
|
|
3490
3992
|
profile,
|
|
3491
|
-
|
|
3492
|
-
|
|
3993
|
+
code,
|
|
3994
|
+
reason: reason || void 0,
|
|
3493
3995
|
sessionId
|
|
3494
3996
|
},
|
|
3495
3997
|
command
|
|
3496
3998
|
);
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3999
|
+
emitLifecycle("closed", {
|
|
4000
|
+
code,
|
|
4001
|
+
reason: reason || void 0
|
|
4002
|
+
});
|
|
4003
|
+
if (!opts.keepAlive || supervised) {
|
|
4004
|
+
finish();
|
|
4005
|
+
return;
|
|
4006
|
+
}
|
|
4007
|
+
const shouldRestart = keepAliveRestartOnAnyClose || code === 1e3 || code === 3e3;
|
|
4008
|
+
if (!shouldRestart) return;
|
|
4009
|
+
if (keepAliveRestartTimer) {
|
|
4010
|
+
clearTimeout(keepAliveRestartTimer);
|
|
4011
|
+
}
|
|
4012
|
+
keepAliveRestartTimer = setTimeout(() => {
|
|
4013
|
+
keepAliveRestartTimer = null;
|
|
4014
|
+
writeDebugLine(
|
|
4015
|
+
"listen.keepalive.restart",
|
|
4016
|
+
{
|
|
4017
|
+
profile,
|
|
4018
|
+
code,
|
|
4019
|
+
reason: reason || void 0,
|
|
4020
|
+
delayMs: keepAliveRestartDelayMs,
|
|
4021
|
+
sessionId
|
|
4022
|
+
},
|
|
4023
|
+
command
|
|
4024
|
+
);
|
|
4025
|
+
try {
|
|
4026
|
+
api.listener.start({ retryOnClose: true });
|
|
4027
|
+
} catch (error) {
|
|
4028
|
+
if (!isListenerAlreadyStarted(error)) {
|
|
4029
|
+
writeDebugLine(
|
|
4030
|
+
"listen.keepalive.restart_error",
|
|
4031
|
+
{
|
|
4032
|
+
profile,
|
|
4033
|
+
code,
|
|
4034
|
+
reason: reason || void 0,
|
|
4035
|
+
delayMs: keepAliveRestartDelayMs,
|
|
4036
|
+
message: toErrorText(error),
|
|
4037
|
+
sessionId
|
|
4038
|
+
},
|
|
4039
|
+
command
|
|
4040
|
+
);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
}, keepAliveRestartDelayMs);
|
|
4044
|
+
});
|
|
4045
|
+
const onSignal = () => {
|
|
3504
4046
|
try {
|
|
3505
4047
|
api.listener.stop();
|
|
3506
4048
|
} catch {
|
|
3507
4049
|
}
|
|
3508
4050
|
finish();
|
|
3509
|
-
}
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
4051
|
+
};
|
|
4052
|
+
unregisterShutdown = registerShutdownCallback(onSignal);
|
|
4053
|
+
if (lifecycleEventsEnabled && heartbeatMs > 0) {
|
|
4054
|
+
heartbeatTimer = setInterval(() => {
|
|
4055
|
+
emitLifecycle("heartbeat");
|
|
4056
|
+
}, heartbeatMs);
|
|
4057
|
+
}
|
|
4058
|
+
if (recycleEnabled) {
|
|
4059
|
+
recycleTimer = setTimeout(() => {
|
|
4060
|
+
console.error(
|
|
4061
|
+
`Listener recycle triggered after ${recycleMs}ms to prevent stale session.`
|
|
4062
|
+
);
|
|
4063
|
+
writeDebugLine(
|
|
4064
|
+
"listen.recycle",
|
|
4065
|
+
{
|
|
4066
|
+
profile,
|
|
4067
|
+
recycleMs,
|
|
4068
|
+
exitCode: recycleExitCode,
|
|
4069
|
+
sessionId
|
|
4070
|
+
},
|
|
4071
|
+
command
|
|
4072
|
+
);
|
|
4073
|
+
process.exitCode = recycleExitCode;
|
|
4074
|
+
recyclePendingExit = true;
|
|
4075
|
+
recycleForceExitTimer = setTimeout(() => {
|
|
4076
|
+
recycleForceExitTimer = null;
|
|
4077
|
+
process.exit(recycleExitCode);
|
|
4078
|
+
}, 3e3);
|
|
4079
|
+
recycleForceExitTimer.unref();
|
|
4080
|
+
try {
|
|
4081
|
+
api.listener.stop();
|
|
4082
|
+
} catch {
|
|
4083
|
+
}
|
|
4084
|
+
finish();
|
|
4085
|
+
}, recycleMs);
|
|
4086
|
+
}
|
|
4087
|
+
api.listener.start({ retryOnClose: supervised ? false : Boolean(opts.keepAlive) });
|
|
4088
|
+
});
|
|
4089
|
+
} finally {
|
|
4090
|
+
unregisterResourceCleanup();
|
|
4091
|
+
await cleanupListenResources();
|
|
4092
|
+
}
|
|
3513
4093
|
}
|
|
3514
4094
|
)
|
|
3515
4095
|
);
|