@hopgoldy/agent-bridge 0.1.1 → 0.2.2
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 +24 -5
- package/dist/agent-bridge.js +1801 -133
- package/dist/cli.js +1801 -133
- package/dist/index.d.ts +51 -11
- package/package.json +9 -4
package/dist/cli.js
CHANGED
|
@@ -409,6 +409,8 @@ var GatewayCore = class {
|
|
|
409
409
|
agentSessionId,
|
|
410
410
|
clientSessionId,
|
|
411
411
|
toolName: "toolName" in event ? event.toolName : void 0,
|
|
412
|
+
toolCallId: "toolCallId" in event ? event.toolCallId : void 0,
|
|
413
|
+
toolLabel: "toolLabel" in event ? event.toolLabel : void 0,
|
|
412
414
|
text: event.text
|
|
413
415
|
});
|
|
414
416
|
}
|
|
@@ -427,7 +429,7 @@ var GatewayCore = class {
|
|
|
427
429
|
});
|
|
428
430
|
}
|
|
429
431
|
#isToolRelatedEvent(event) {
|
|
430
|
-
return event.type === "assistant.tool.running" || event.type === "assistant.tool.done" || event.type === "assistant.tool.error" || event.type === "session.compacting";
|
|
432
|
+
return event.type === "assistant.tool.running" || event.type === "assistant.tool.update" || event.type === "assistant.tool.done" || event.type === "assistant.tool.error" || event.type === "session.compacting";
|
|
431
433
|
}
|
|
432
434
|
#touchRuntime(runtime) {
|
|
433
435
|
runtime.lastActiveAt = Date.now();
|
|
@@ -783,11 +785,6 @@ var PiRpcClient = class {
|
|
|
783
785
|
const data = response.data;
|
|
784
786
|
return data ?? {};
|
|
785
787
|
}
|
|
786
|
-
async getLastAssistantText() {
|
|
787
|
-
const response = await this.#send({ type: "get_last_assistant_text" });
|
|
788
|
-
const data = response.data;
|
|
789
|
-
return data?.text ?? null;
|
|
790
|
-
}
|
|
791
788
|
async getState() {
|
|
792
789
|
const response = await this.#send({ type: "get_state" });
|
|
793
790
|
return response.data ?? {};
|
|
@@ -795,26 +792,6 @@ var PiRpcClient = class {
|
|
|
795
792
|
async setSessionName(name) {
|
|
796
793
|
await this.#send({ type: "set_session_name", name });
|
|
797
794
|
}
|
|
798
|
-
waitForSettled(timeoutMs = 10 * 60 * 1e3) {
|
|
799
|
-
if (!this.#process) {
|
|
800
|
-
return Promise.reject(new Error("pi RPC client is not running"));
|
|
801
|
-
}
|
|
802
|
-
return new Promise((resolve, reject) => {
|
|
803
|
-
const timeout = setTimeout(() => {
|
|
804
|
-
reject(new Error(`Timed out waiting for pi agent to settle after ${timeoutMs}ms`));
|
|
805
|
-
}, timeoutMs);
|
|
806
|
-
this.#settledWaiters.push({
|
|
807
|
-
resolve: () => {
|
|
808
|
-
clearTimeout(timeout);
|
|
809
|
-
resolve();
|
|
810
|
-
},
|
|
811
|
-
reject: (error) => {
|
|
812
|
-
clearTimeout(timeout);
|
|
813
|
-
reject(error);
|
|
814
|
-
}
|
|
815
|
-
});
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
795
|
async #send(command) {
|
|
819
796
|
if (!this.#process?.stdin.writable) {
|
|
820
797
|
throw this.#exitError ?? new Error("pi RPC process is not writable");
|
|
@@ -931,6 +908,8 @@ var PiCodingAgentAdapter = class {
|
|
|
931
908
|
#onOutput = null;
|
|
932
909
|
#inputQueue = [];
|
|
933
910
|
#processing = false;
|
|
911
|
+
#toolLabelByCallId = /* @__PURE__ */ new Map();
|
|
912
|
+
#toolInputByCallId = /* @__PURE__ */ new Map();
|
|
934
913
|
constructor({
|
|
935
914
|
agentSessionId,
|
|
936
915
|
cwd,
|
|
@@ -973,6 +952,8 @@ var PiCodingAgentAdapter = class {
|
|
|
973
952
|
}
|
|
974
953
|
async stop() {
|
|
975
954
|
this.#inputQueue.length = 0;
|
|
955
|
+
this.#toolLabelByCallId.clear();
|
|
956
|
+
this.#toolInputByCallId.clear();
|
|
976
957
|
await this.#client?.stop();
|
|
977
958
|
this.#client = null;
|
|
978
959
|
this.#processing = false;
|
|
@@ -1018,20 +999,12 @@ var PiCodingAgentAdapter = class {
|
|
|
1018
999
|
try {
|
|
1019
1000
|
if (event.type === "user.message") {
|
|
1020
1001
|
this.#logger.info(`sending prompt to agent (session=${this.#agentSessionId})`);
|
|
1021
|
-
const startedAt = Date.now();
|
|
1022
1002
|
await this.#emitProgress({
|
|
1023
1003
|
type: "assistant.thinking",
|
|
1024
1004
|
agentSessionId: this.#agentSessionId,
|
|
1025
1005
|
text: "Processing request"
|
|
1026
1006
|
});
|
|
1027
1007
|
await this.#client.prompt(event.text);
|
|
1028
|
-
await this.#client.waitForSettled();
|
|
1029
|
-
const rawText = await this.#client.getLastAssistantText();
|
|
1030
|
-
const { text: text2, attachments } = extractMediaMarkers(rawText ?? "(pi returned no assistant text)");
|
|
1031
|
-
this.#logger.debug(
|
|
1032
|
-
`prompt settled (session=${this.#agentSessionId} durationMs=${Date.now() - startedAt} replyLength=${text2.length} attachments=${attachments.length})`
|
|
1033
|
-
);
|
|
1034
|
-
await this.#emitAssistant(text2, attachments);
|
|
1035
1008
|
return;
|
|
1036
1009
|
}
|
|
1037
1010
|
this.#logger.info(`compacting context (session=${this.#agentSessionId})`);
|
|
@@ -1077,26 +1050,210 @@ var PiCodingAgentAdapter = class {
|
|
|
1077
1050
|
if (!this.#onOutput) {
|
|
1078
1051
|
return;
|
|
1079
1052
|
}
|
|
1053
|
+
if (rpcEvent.type === "message_end") {
|
|
1054
|
+
const message = rpcEvent.message;
|
|
1055
|
+
if (this.#isAssistantMessage(message)) {
|
|
1056
|
+
this.#logger.debug(`assistant message_end content shape (session=${this.#agentSessionId})`, {
|
|
1057
|
+
contentType: Array.isArray(message.content) ? "array" : typeof message.content,
|
|
1058
|
+
contentPreview: this.#summarizeContentShape(message.content)
|
|
1059
|
+
});
|
|
1060
|
+
const rawText = this.#extractMessageText(message.content);
|
|
1061
|
+
const { text: text2, attachments } = extractMediaMarkers(rawText);
|
|
1062
|
+
this.#logger.debug(
|
|
1063
|
+
`assistant message_end received (session=${this.#agentSessionId} textLength=${rawText.length} attachmentCount=${attachments.length})`
|
|
1064
|
+
);
|
|
1065
|
+
if (!text2.trim() && attachments.length === 0) {
|
|
1066
|
+
this.#logger.debug(
|
|
1067
|
+
`ignoring assistant message_end without visible content (session=${this.#agentSessionId})`
|
|
1068
|
+
);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
await this.#emitAssistant(text2, attachments);
|
|
1072
|
+
}
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1080
1075
|
if (rpcEvent.type === "tool_execution_start") {
|
|
1081
1076
|
const toolName = typeof rpcEvent.toolName === "string" ? rpcEvent.toolName : "unknown";
|
|
1077
|
+
const toolCallId = typeof rpcEvent.toolCallId === "string" ? rpcEvent.toolCallId : void 0;
|
|
1078
|
+
const toolInput = "args" in rpcEvent ? rpcEvent.args : void 0;
|
|
1079
|
+
const toolLabel = this.#summarizeToolLabel(toolName, toolInput);
|
|
1080
|
+
if (toolCallId) {
|
|
1081
|
+
if (toolLabel) this.#toolLabelByCallId.set(toolCallId, toolLabel);
|
|
1082
|
+
if (toolInput !== void 0) this.#toolInputByCallId.set(toolCallId, toolInput);
|
|
1083
|
+
}
|
|
1082
1084
|
await this.#emitProgress({
|
|
1083
1085
|
type: "assistant.tool.running",
|
|
1084
1086
|
agentSessionId: this.#agentSessionId,
|
|
1085
1087
|
toolName,
|
|
1088
|
+
toolCallId,
|
|
1089
|
+
toolInput,
|
|
1090
|
+
toolLabel,
|
|
1091
|
+
text: `Running ${toolName}`
|
|
1092
|
+
});
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (rpcEvent.type === "tool_execution_update") {
|
|
1096
|
+
const toolName = typeof rpcEvent.toolName === "string" ? rpcEvent.toolName : "unknown";
|
|
1097
|
+
const toolCallId = typeof rpcEvent.toolCallId === "string" ? rpcEvent.toolCallId : void 0;
|
|
1098
|
+
const toolInput = "args" in rpcEvent ? rpcEvent.args : this.#toolInputForCall(toolCallId);
|
|
1099
|
+
const toolLabel = this.#toolLabelForCall(toolCallId, toolName, toolInput);
|
|
1100
|
+
if (toolCallId) {
|
|
1101
|
+
if (toolLabel) this.#toolLabelByCallId.set(toolCallId, toolLabel);
|
|
1102
|
+
if (toolInput !== void 0) this.#toolInputByCallId.set(toolCallId, toolInput);
|
|
1103
|
+
}
|
|
1104
|
+
await this.#emitProgress({
|
|
1105
|
+
type: "assistant.tool.update",
|
|
1106
|
+
agentSessionId: this.#agentSessionId,
|
|
1107
|
+
toolName,
|
|
1108
|
+
toolCallId,
|
|
1109
|
+
toolInput,
|
|
1110
|
+
toolLabel,
|
|
1111
|
+
partialResult: "partialResult" in rpcEvent ? rpcEvent.partialResult : void 0,
|
|
1086
1112
|
text: `Running ${toolName}`
|
|
1087
1113
|
});
|
|
1088
1114
|
return;
|
|
1089
1115
|
}
|
|
1090
1116
|
if (rpcEvent.type === "tool_execution_end") {
|
|
1091
1117
|
const toolName = typeof rpcEvent.toolName === "string" ? rpcEvent.toolName : "unknown";
|
|
1118
|
+
const toolCallId = typeof rpcEvent.toolCallId === "string" ? rpcEvent.toolCallId : void 0;
|
|
1092
1119
|
const isError = Boolean(rpcEvent.isError);
|
|
1120
|
+
const toolInput = this.#toolInputForCall(toolCallId);
|
|
1121
|
+
const toolLabel = this.#toolLabelForCall(toolCallId, toolName, toolInput);
|
|
1093
1122
|
await this.#emitProgress({
|
|
1094
1123
|
type: isError ? "assistant.tool.error" : "assistant.tool.done",
|
|
1095
1124
|
agentSessionId: this.#agentSessionId,
|
|
1096
1125
|
toolName,
|
|
1126
|
+
toolCallId,
|
|
1127
|
+
toolInput,
|
|
1128
|
+
toolLabel,
|
|
1129
|
+
result: "result" in rpcEvent ? rpcEvent.result : void 0,
|
|
1097
1130
|
text: isError ? `Failed ${toolName}` : `Finished ${toolName}`
|
|
1098
1131
|
});
|
|
1132
|
+
if (toolCallId) {
|
|
1133
|
+
this.#toolLabelByCallId.delete(toolCallId);
|
|
1134
|
+
this.#toolInputByCallId.delete(toolCallId);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
#toolInputForCall(toolCallId) {
|
|
1139
|
+
return toolCallId ? this.#toolInputByCallId.get(toolCallId) : void 0;
|
|
1140
|
+
}
|
|
1141
|
+
#toolLabelForCall(toolCallId, toolName, toolInput) {
|
|
1142
|
+
return (toolCallId ? this.#toolLabelByCallId.get(toolCallId) : void 0) ?? this.#summarizeToolLabel(toolName, toolInput);
|
|
1143
|
+
}
|
|
1144
|
+
#summarizeToolLabel(toolName, toolInput) {
|
|
1145
|
+
if (!toolInput || typeof toolInput !== "object" || Array.isArray(toolInput)) {
|
|
1146
|
+
return void 0;
|
|
1147
|
+
}
|
|
1148
|
+
const input = toolInput;
|
|
1149
|
+
const stringField = (key) => {
|
|
1150
|
+
const value = input[key];
|
|
1151
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
1152
|
+
};
|
|
1153
|
+
const stringArrayField = (key) => {
|
|
1154
|
+
const value = input[key];
|
|
1155
|
+
if (!Array.isArray(value)) return void 0;
|
|
1156
|
+
const items = value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
1157
|
+
return items.length > 0 ? items.map((item) => item.trim()) : void 0;
|
|
1158
|
+
};
|
|
1159
|
+
switch (toolName) {
|
|
1160
|
+
case "bash":
|
|
1161
|
+
return stringField("command");
|
|
1162
|
+
case "read":
|
|
1163
|
+
case "write":
|
|
1164
|
+
case "edit":
|
|
1165
|
+
case "find":
|
|
1166
|
+
case "ls":
|
|
1167
|
+
return stringField("path");
|
|
1168
|
+
case "grep": {
|
|
1169
|
+
const pattern = stringField("pattern");
|
|
1170
|
+
const path7 = stringField("path");
|
|
1171
|
+
if (pattern && path7) return `${pattern} in ${path7}`;
|
|
1172
|
+
return pattern ?? path7;
|
|
1173
|
+
}
|
|
1174
|
+
case "web_search": {
|
|
1175
|
+
const query = stringField("query");
|
|
1176
|
+
const queries = stringArrayField("queries");
|
|
1177
|
+
return query ?? (queries ? queries.join(" | ") : void 0);
|
|
1178
|
+
}
|
|
1179
|
+
case "fetch_content": {
|
|
1180
|
+
const url = stringField("url");
|
|
1181
|
+
const urls = stringArrayField("urls");
|
|
1182
|
+
return url ?? (urls ? urls.join(" | ") : void 0);
|
|
1183
|
+
}
|
|
1184
|
+
default:
|
|
1185
|
+
return this.#truncate(this.#safeJson(toolInput), 120);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
#safeJson(value) {
|
|
1189
|
+
try {
|
|
1190
|
+
return JSON.stringify(value);
|
|
1191
|
+
} catch {
|
|
1192
|
+
return void 0;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
#truncate(value, maxLength) {
|
|
1196
|
+
if (!value) return void 0;
|
|
1197
|
+
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}\u2026` : value;
|
|
1198
|
+
}
|
|
1199
|
+
#isAssistantMessage(value) {
|
|
1200
|
+
if (!value || typeof value !== "object") {
|
|
1201
|
+
return false;
|
|
1099
1202
|
}
|
|
1203
|
+
return value.role === "assistant";
|
|
1204
|
+
}
|
|
1205
|
+
#extractMessageText(content) {
|
|
1206
|
+
if (typeof content === "string") {
|
|
1207
|
+
return content;
|
|
1208
|
+
}
|
|
1209
|
+
if (this.#isTextBlock(content)) {
|
|
1210
|
+
return content.text;
|
|
1211
|
+
}
|
|
1212
|
+
if (!Array.isArray(content)) {
|
|
1213
|
+
return "";
|
|
1214
|
+
}
|
|
1215
|
+
const textParts = [];
|
|
1216
|
+
for (const block of content) {
|
|
1217
|
+
if (this.#isTextBlock(block)) {
|
|
1218
|
+
textParts.push(block.text);
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
if (!block || typeof block !== "object") {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
const candidate = block;
|
|
1225
|
+
if (typeof candidate.content === "string") {
|
|
1226
|
+
textParts.push(candidate.content);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return textParts.join("");
|
|
1230
|
+
}
|
|
1231
|
+
#isTextBlock(value) {
|
|
1232
|
+
if (!value || typeof value !== "object") {
|
|
1233
|
+
return false;
|
|
1234
|
+
}
|
|
1235
|
+
const candidate = value;
|
|
1236
|
+
return candidate.type === "text" && typeof candidate.text === "string";
|
|
1237
|
+
}
|
|
1238
|
+
#summarizeContentShape(content) {
|
|
1239
|
+
if (typeof content === "string") {
|
|
1240
|
+
return { kind: "string", length: content.length, preview: content.slice(0, 200) };
|
|
1241
|
+
}
|
|
1242
|
+
if (!Array.isArray(content)) {
|
|
1243
|
+
return { kind: typeof content };
|
|
1244
|
+
}
|
|
1245
|
+
return content.slice(0, 10).map((block) => {
|
|
1246
|
+
if (!block || typeof block !== "object") {
|
|
1247
|
+
return { kind: typeof block };
|
|
1248
|
+
}
|
|
1249
|
+
const candidate = block;
|
|
1250
|
+
return {
|
|
1251
|
+
type: candidate.type,
|
|
1252
|
+
textLength: typeof candidate.text === "string" ? candidate.text.length : void 0,
|
|
1253
|
+
contentType: Array.isArray(candidate.content) ? "array" : typeof candidate.content,
|
|
1254
|
+
mimeType: candidate.mimeType
|
|
1255
|
+
};
|
|
1256
|
+
});
|
|
1100
1257
|
}
|
|
1101
1258
|
};
|
|
1102
1259
|
|
|
@@ -1165,6 +1322,159 @@ function getTypedAgentModule(config) {
|
|
|
1165
1322
|
return module;
|
|
1166
1323
|
}
|
|
1167
1324
|
|
|
1325
|
+
// src/modules/client/utils/progress-renderer.ts
|
|
1326
|
+
var DEFAULT_COLLAPSE_THRESHOLD = 10;
|
|
1327
|
+
var MAX_TOOL_LABEL_DISPLAY_LENGTH = 15;
|
|
1328
|
+
var NO_PROGRESS_MARKDOWN = "No progress yet.";
|
|
1329
|
+
var ProgressRenderer = class {
|
|
1330
|
+
#collapseThreshold;
|
|
1331
|
+
#entries = /* @__PURE__ */ new Map();
|
|
1332
|
+
#order = [];
|
|
1333
|
+
#status = "running";
|
|
1334
|
+
constructor(options = {}) {
|
|
1335
|
+
this.#collapseThreshold = options.collapseThreshold ?? DEFAULT_COLLAPSE_THRESHOLD;
|
|
1336
|
+
}
|
|
1337
|
+
/** Whether `event` should be recorded as progress (excludes assistant messages and thinking). */
|
|
1338
|
+
isProgressEvent(event) {
|
|
1339
|
+
return event.type !== "assistant.message" && event.type !== "assistant.thinking";
|
|
1340
|
+
}
|
|
1341
|
+
/** Records a progress event, formatting it into a line and collapsing older lines if needed. */
|
|
1342
|
+
takeProgressEvent(event) {
|
|
1343
|
+
const toolEntryId = this.#toolEntryId(event);
|
|
1344
|
+
if (toolEntryId) {
|
|
1345
|
+
this.#upsertToolEntry(toolEntryId, event);
|
|
1346
|
+
} else {
|
|
1347
|
+
this.#appendLineEntry(this.#formatProgressLine(event));
|
|
1348
|
+
}
|
|
1349
|
+
this.#status = this.#progressStatus(event);
|
|
1350
|
+
}
|
|
1351
|
+
/** Returns the current rendered markdown, status, and collapsed-line count. */
|
|
1352
|
+
getCurrentProgress() {
|
|
1353
|
+
const collapsedCount = Math.max(0, this.#order.length - this.#collapseThreshold);
|
|
1354
|
+
return {
|
|
1355
|
+
markdown: this.#renderMarkdown(collapsedCount),
|
|
1356
|
+
status: this.#status,
|
|
1357
|
+
collapsedCount
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
#renderMarkdown(collapsedCount) {
|
|
1361
|
+
const contentLines = [];
|
|
1362
|
+
if (collapsedCount > 0) {
|
|
1363
|
+
contentLines.push(`- Collapsed ${collapsedCount} earlier updates.`);
|
|
1364
|
+
}
|
|
1365
|
+
for (const id of this.#visibleOrder()) {
|
|
1366
|
+
const entry = this.#entries.get(id);
|
|
1367
|
+
if (!entry) continue;
|
|
1368
|
+
contentLines.push(entry.kind === "line" ? entry.line : this.#formatToolEntry(entry));
|
|
1369
|
+
}
|
|
1370
|
+
return contentLines.length > 0 ? contentLines.join("\n") : NO_PROGRESS_MARKDOWN;
|
|
1371
|
+
}
|
|
1372
|
+
#visibleOrder() {
|
|
1373
|
+
return this.#order.slice(-this.#collapseThreshold);
|
|
1374
|
+
}
|
|
1375
|
+
#appendLineEntry(line) {
|
|
1376
|
+
const id = `line:${this.#order.length}`;
|
|
1377
|
+
this.#entries.set(id, { kind: "line", line });
|
|
1378
|
+
this.#order.push(id);
|
|
1379
|
+
}
|
|
1380
|
+
#upsertToolEntry(id, event) {
|
|
1381
|
+
if (!this.#entries.has(id)) {
|
|
1382
|
+
this.#order.push(id);
|
|
1383
|
+
} else {
|
|
1384
|
+
this.#order = this.#order.filter((entryId) => entryId !== id);
|
|
1385
|
+
this.#order.push(id);
|
|
1386
|
+
}
|
|
1387
|
+
this.#entries.set(id, {
|
|
1388
|
+
kind: "tool",
|
|
1389
|
+
toolName: event.toolName,
|
|
1390
|
+
toolLabel: event.toolLabel,
|
|
1391
|
+
status: event.type === "assistant.tool.error" ? "error" : event.type === "assistant.tool.done" ? "done" : "running",
|
|
1392
|
+
text: event.text
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
#toolEntryId(event) {
|
|
1396
|
+
if (event.type === "assistant.tool.running" || event.type === "assistant.tool.update" || event.type === "assistant.tool.done" || event.type === "assistant.tool.error") {
|
|
1397
|
+
return event.toolCallId ? `tool:${event.toolCallId}` : null;
|
|
1398
|
+
}
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
#formatToolEntry(entry) {
|
|
1402
|
+
const subject = this.#formatToolSubject(entry.toolName, entry.toolLabel);
|
|
1403
|
+
switch (entry.status) {
|
|
1404
|
+
case "running":
|
|
1405
|
+
return `- Running ${subject}`;
|
|
1406
|
+
case "done":
|
|
1407
|
+
return `- Finished ${subject}`;
|
|
1408
|
+
case "error":
|
|
1409
|
+
return this.#formatToolErrorLine(subject, entry.text);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
#formatProgressLine(event) {
|
|
1413
|
+
switch (event.type) {
|
|
1414
|
+
case "session.compacting":
|
|
1415
|
+
return `- Compacting session${event.text ? `: ${event.text}` : ""}`;
|
|
1416
|
+
case "assistant.tool.running":
|
|
1417
|
+
case "assistant.tool.update":
|
|
1418
|
+
return `- Running ${this.#formatToolSubject(event.toolName, event.toolLabel)}`;
|
|
1419
|
+
case "assistant.tool.done":
|
|
1420
|
+
return `- Finished ${this.#formatToolSubject(event.toolName, event.toolLabel)}`;
|
|
1421
|
+
case "assistant.tool.error":
|
|
1422
|
+
return this.#formatToolErrorLine(this.#formatToolSubject(event.toolName, event.toolLabel), event.text);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
#formatToolSubject(toolName, toolLabel) {
|
|
1426
|
+
const normalizedLabel = toolLabel?.trim();
|
|
1427
|
+
if (!normalizedLabel) {
|
|
1428
|
+
return toolName;
|
|
1429
|
+
}
|
|
1430
|
+
return `${toolName}: ${this.#truncateToolLabel(normalizedLabel)}`;
|
|
1431
|
+
}
|
|
1432
|
+
#truncateToolLabel(toolLabel) {
|
|
1433
|
+
return toolLabel.length > MAX_TOOL_LABEL_DISPLAY_LENGTH ? `${toolLabel.slice(0, MAX_TOOL_LABEL_DISPLAY_LENGTH)}\u2026` : toolLabel;
|
|
1434
|
+
}
|
|
1435
|
+
#formatToolErrorLine(toolName, text2) {
|
|
1436
|
+
const normalizedText = text2?.trim();
|
|
1437
|
+
if (!normalizedText) {
|
|
1438
|
+
return `- ${this.#humanizeToolError(toolName)}`;
|
|
1439
|
+
}
|
|
1440
|
+
const lowerText = normalizedText.toLowerCase();
|
|
1441
|
+
const lowerToolName = toolName.toLowerCase();
|
|
1442
|
+
if (lowerText === lowerToolName || lowerText === `failed ${lowerToolName}` || lowerText === `running ${lowerToolName}` || lowerText === `finished ${lowerToolName}`) {
|
|
1443
|
+
return `- ${this.#humanizeToolError(toolName)}`;
|
|
1444
|
+
}
|
|
1445
|
+
return `- ${this.#humanizeToolError(toolName)}: ${normalizedText}`;
|
|
1446
|
+
}
|
|
1447
|
+
#humanizeToolError(toolName) {
|
|
1448
|
+
return `Failed ${toolName}`;
|
|
1449
|
+
}
|
|
1450
|
+
#progressStatus(event) {
|
|
1451
|
+
switch (event.type) {
|
|
1452
|
+
case "assistant.tool.error":
|
|
1453
|
+
return "error";
|
|
1454
|
+
case "assistant.tool.done":
|
|
1455
|
+
return "done";
|
|
1456
|
+
default:
|
|
1457
|
+
return "running";
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1461
|
+
|
|
1462
|
+
// src/modules/client/utils/slash-commands.ts
|
|
1463
|
+
function parseSlashCommand(text2, clientSessionId) {
|
|
1464
|
+
switch (text2.toLowerCase()) {
|
|
1465
|
+
case "/new":
|
|
1466
|
+
case "/n":
|
|
1467
|
+
return { type: "command.session.new", clientSessionId };
|
|
1468
|
+
case "/compact":
|
|
1469
|
+
case "/c":
|
|
1470
|
+
return { type: "command.session.compact", clientSessionId };
|
|
1471
|
+
case "/stop":
|
|
1472
|
+
return { type: "command.session.stop", clientSessionId };
|
|
1473
|
+
default:
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1168
1478
|
// src/modules/client/feishu/adapter/feishu-client.ts
|
|
1169
1479
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
1170
1480
|
import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
|
|
@@ -1517,29 +1827,19 @@ var FeishuIMAdapter = class _FeishuIMAdapter {
|
|
|
1517
1827
|
#processing = false;
|
|
1518
1828
|
#lastInboundMessageIdBySession = /* @__PURE__ */ new Map();
|
|
1519
1829
|
#progressStateBySession = /* @__PURE__ */ new Map();
|
|
1520
|
-
static buildProgressCard(
|
|
1830
|
+
static buildProgressCard(markdown) {
|
|
1521
1831
|
return {
|
|
1522
1832
|
schema: "2.0",
|
|
1523
1833
|
body: {
|
|
1524
1834
|
elements: [
|
|
1525
1835
|
{
|
|
1526
1836
|
tag: "markdown",
|
|
1527
|
-
content:
|
|
1837
|
+
content: markdown
|
|
1528
1838
|
}
|
|
1529
1839
|
]
|
|
1530
1840
|
}
|
|
1531
1841
|
};
|
|
1532
1842
|
}
|
|
1533
|
-
static progressBody(lines, collapsedCount) {
|
|
1534
|
-
const contentLines = [];
|
|
1535
|
-
if (collapsedCount > 0) {
|
|
1536
|
-
contentLines.push(`- Collapsed ${collapsedCount} earlier updates.`);
|
|
1537
|
-
}
|
|
1538
|
-
if (lines.length > 0) {
|
|
1539
|
-
contentLines.push(...lines);
|
|
1540
|
-
}
|
|
1541
|
-
return contentLines.length > 0 ? contentLines.join("\n") : "No progress yet.";
|
|
1542
|
-
}
|
|
1543
1843
|
async #notifySendFailure(chatId, error) {
|
|
1544
1844
|
if (!this.#client) {
|
|
1545
1845
|
return;
|
|
@@ -1577,28 +1877,10 @@ ${message}`;
|
|
|
1577
1877
|
this.#resetProgressState(clientSessionId);
|
|
1578
1878
|
await this.#client?.startTyping(chatId, messageId);
|
|
1579
1879
|
const normalizedText = text2.trim();
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
clientSessionId
|
|
1585
|
-
});
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
if (normalizedText === "/compact") {
|
|
1589
|
-
this.#logger.info(`received command /compact (session=${clientSessionId})`);
|
|
1590
|
-
await this.#onOutput({
|
|
1591
|
-
type: "command.session.compact",
|
|
1592
|
-
clientSessionId
|
|
1593
|
-
});
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1596
|
-
if (normalizedText === "/stop") {
|
|
1597
|
-
this.#logger.info(`received command /stop (session=${clientSessionId})`);
|
|
1598
|
-
await this.#onOutput({
|
|
1599
|
-
type: "command.session.stop",
|
|
1600
|
-
clientSessionId
|
|
1601
|
-
});
|
|
1880
|
+
const commandEvent = parseSlashCommand(normalizedText, clientSessionId);
|
|
1881
|
+
if (commandEvent) {
|
|
1882
|
+
this.#logger.info(`received command ${normalizedText} (session=${clientSessionId})`);
|
|
1883
|
+
await this.#onOutput(commandEvent);
|
|
1602
1884
|
return;
|
|
1603
1885
|
}
|
|
1604
1886
|
this.#logger.info(`received user message (session=${clientSessionId}): ${normalizedText}`);
|
|
@@ -1686,25 +1968,17 @@ ${message}`;
|
|
|
1686
1968
|
if (!this.#client) {
|
|
1687
1969
|
return;
|
|
1688
1970
|
}
|
|
1689
|
-
if (!this.#shouldRenderProgressEvent(event)) {
|
|
1690
|
-
return;
|
|
1691
|
-
}
|
|
1692
1971
|
const state = this.#progressStateBySession.get(event.clientSessionId) ?? {
|
|
1972
|
+
renderer: new ProgressRenderer(),
|
|
1693
1973
|
messageId: null,
|
|
1694
|
-
creating: false
|
|
1695
|
-
lines: [],
|
|
1696
|
-
status: "running",
|
|
1697
|
-
turnId: 0,
|
|
1698
|
-
collapsedCount: 0
|
|
1974
|
+
creating: false
|
|
1699
1975
|
};
|
|
1700
|
-
state.lines.push(this.#formatProgressLine(event));
|
|
1701
|
-
if (state.lines.length > 10) {
|
|
1702
|
-
state.collapsedCount += state.lines.length - 10;
|
|
1703
|
-
state.lines.splice(0, state.lines.length - 10);
|
|
1704
|
-
}
|
|
1705
|
-
state.status = this.#progressStatus(event);
|
|
1706
1976
|
this.#progressStateBySession.set(event.clientSessionId, state);
|
|
1707
|
-
|
|
1977
|
+
if (!state.renderer.isProgressEvent(event)) {
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
state.renderer.takeProgressEvent(event);
|
|
1981
|
+
const card = _FeishuIMAdapter.buildProgressCard(state.renderer.getCurrentProgress().markdown);
|
|
1708
1982
|
if (state.messageId) {
|
|
1709
1983
|
await this.#client.updateCard(state.messageId, card);
|
|
1710
1984
|
return;
|
|
@@ -1723,59 +1997,13 @@ ${message}`;
|
|
|
1723
1997
|
state.creating = false;
|
|
1724
1998
|
}
|
|
1725
1999
|
}
|
|
1726
|
-
#shouldRenderProgressEvent(event) {
|
|
1727
|
-
return event.type !== "assistant.thinking";
|
|
1728
|
-
}
|
|
1729
2000
|
#resetProgressState(clientSessionId) {
|
|
1730
|
-
const previous = this.#progressStateBySession.get(clientSessionId);
|
|
1731
2001
|
this.#progressStateBySession.set(clientSessionId, {
|
|
2002
|
+
renderer: new ProgressRenderer(),
|
|
1732
2003
|
messageId: null,
|
|
1733
|
-
creating: false
|
|
1734
|
-
lines: [],
|
|
1735
|
-
status: "running",
|
|
1736
|
-
turnId: (previous?.turnId ?? 0) + 1,
|
|
1737
|
-
collapsedCount: 0
|
|
2004
|
+
creating: false
|
|
1738
2005
|
});
|
|
1739
2006
|
}
|
|
1740
|
-
#formatProgressLine(event) {
|
|
1741
|
-
switch (event.type) {
|
|
1742
|
-
case "assistant.thinking":
|
|
1743
|
-
return "";
|
|
1744
|
-
case "session.compacting":
|
|
1745
|
-
return `- Compacting session${event.text ? `: ${event.text}` : ""}`;
|
|
1746
|
-
case "assistant.tool.running":
|
|
1747
|
-
return `- Running ${event.toolName}`;
|
|
1748
|
-
case "assistant.tool.done":
|
|
1749
|
-
return `- Finished ${event.toolName}`;
|
|
1750
|
-
case "assistant.tool.error":
|
|
1751
|
-
return this.#formatToolErrorLine(event.toolName, event.text);
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
|
-
#formatToolErrorLine(toolName, text2) {
|
|
1755
|
-
const normalizedText = text2?.trim();
|
|
1756
|
-
if (!normalizedText) {
|
|
1757
|
-
return `- ${this.#humanizeToolError(toolName)}`;
|
|
1758
|
-
}
|
|
1759
|
-
const lowerText = normalizedText.toLowerCase();
|
|
1760
|
-
const lowerToolName = toolName.toLowerCase();
|
|
1761
|
-
if (lowerText === lowerToolName || lowerText === `failed ${lowerToolName}`) {
|
|
1762
|
-
return `- ${this.#humanizeToolError(toolName)}`;
|
|
1763
|
-
}
|
|
1764
|
-
return `- ${this.#humanizeToolError(toolName)}: ${normalizedText}`;
|
|
1765
|
-
}
|
|
1766
|
-
#humanizeToolError(toolName) {
|
|
1767
|
-
return `Failed ${toolName}`;
|
|
1768
|
-
}
|
|
1769
|
-
#progressStatus(event) {
|
|
1770
|
-
switch (event.type) {
|
|
1771
|
-
case "assistant.tool.error":
|
|
1772
|
-
return "error";
|
|
1773
|
-
case "assistant.tool.done":
|
|
1774
|
-
return "done";
|
|
1775
|
-
default:
|
|
1776
|
-
return "running";
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
2007
|
};
|
|
1780
2008
|
|
|
1781
2009
|
// src/modules/client/feishu/index.ts
|
|
@@ -1828,9 +2056,1449 @@ var feishuClientModule = {
|
|
|
1828
2056
|
}
|
|
1829
2057
|
};
|
|
1830
2058
|
|
|
2059
|
+
// src/modules/client/wecom/adapter/wecom-client.ts
|
|
2060
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2061
|
+
import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
2062
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2063
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2064
|
+
import { basename as basename2, extname, join as join2 } from "path";
|
|
2065
|
+
import { WSClient } from "@wecom/aibot-node-sdk";
|
|
2066
|
+
var DEFAULT_WEBSOCKET_URL = "wss://openws.work.weixin.qq.com";
|
|
2067
|
+
var MEDIA_TEMP_DIR = join2(tmpdir2(), "agent-bridge-wecom-media");
|
|
2068
|
+
var IMAGE_SIGNATURE_PNG = Buffer.from("89504e470d0a1a0a", "hex");
|
|
2069
|
+
var IMAGE_SIGNATURE_JPG = Buffer.from("ffd8ff", "hex");
|
|
2070
|
+
var PROCESSED_MESSAGE_ID_LIMIT = 500;
|
|
2071
|
+
var KICKED_ERROR_MESSAGE = "WeCom connection was closed by the server because a newer connection was established for the same bot; this instance will not reconnect";
|
|
2072
|
+
function sanitizeFileName(fileName) {
|
|
2073
|
+
return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2074
|
+
}
|
|
2075
|
+
function detectImageExtension(data) {
|
|
2076
|
+
if (data.subarray(0, IMAGE_SIGNATURE_PNG.length).equals(IMAGE_SIGNATURE_PNG)) return ".png";
|
|
2077
|
+
if (data.subarray(0, IMAGE_SIGNATURE_JPG.length).equals(IMAGE_SIGNATURE_JPG)) return ".jpg";
|
|
2078
|
+
return ".jpg";
|
|
2079
|
+
}
|
|
2080
|
+
function decodeBase64(data) {
|
|
2081
|
+
const payload = data.split(",", 1).length > 1 ? data.split(",", 2)[1] : data;
|
|
2082
|
+
return Buffer.from(payload.trim(), "base64");
|
|
2083
|
+
}
|
|
2084
|
+
function buildMarkdownBody(text2) {
|
|
2085
|
+
return {
|
|
2086
|
+
msgtype: "markdown",
|
|
2087
|
+
markdown: {
|
|
2088
|
+
content: text2
|
|
2089
|
+
}
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
async function ensureMediaDir() {
|
|
2093
|
+
if (!existsSync4(MEDIA_TEMP_DIR)) {
|
|
2094
|
+
await mkdir4(MEDIA_TEMP_DIR, { recursive: true });
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
var WecomClient = class {
|
|
2098
|
+
#config;
|
|
2099
|
+
#logger;
|
|
2100
|
+
#onMessage = null;
|
|
2101
|
+
#onKicked = null;
|
|
2102
|
+
#client = null;
|
|
2103
|
+
#kicked = false;
|
|
2104
|
+
#processedMessageIds = /* @__PURE__ */ new Set();
|
|
2105
|
+
#replyReqIdByMessageId = /* @__PURE__ */ new Map();
|
|
2106
|
+
#lastChatReqId = /* @__PURE__ */ new Map();
|
|
2107
|
+
#replyContextByMessageId = /* @__PURE__ */ new Map();
|
|
2108
|
+
#lastReplyContextByChatId = /* @__PURE__ */ new Map();
|
|
2109
|
+
#streamContextByMessageId = /* @__PURE__ */ new Map();
|
|
2110
|
+
#lastStreamContextByChatId = /* @__PURE__ */ new Map();
|
|
2111
|
+
constructor(config, logger3 = createLogger("wecom")) {
|
|
2112
|
+
this.#config = config;
|
|
2113
|
+
this.#logger = logger3;
|
|
2114
|
+
}
|
|
2115
|
+
setOnMessage(onMessage) {
|
|
2116
|
+
this.#onMessage = onMessage;
|
|
2117
|
+
}
|
|
2118
|
+
setOnKicked(onKicked) {
|
|
2119
|
+
this.#onKicked = onKicked;
|
|
2120
|
+
}
|
|
2121
|
+
isKicked() {
|
|
2122
|
+
return this.#kicked;
|
|
2123
|
+
}
|
|
2124
|
+
async connect() {
|
|
2125
|
+
const websocketUrl = this.#config.websocketUrl ?? DEFAULT_WEBSOCKET_URL;
|
|
2126
|
+
this.#kicked = false;
|
|
2127
|
+
const client = new WSClient({
|
|
2128
|
+
botId: this.#config.botId,
|
|
2129
|
+
secret: this.#config.secret,
|
|
2130
|
+
wsUrl: websocketUrl,
|
|
2131
|
+
logger: this.#logger
|
|
2132
|
+
});
|
|
2133
|
+
this.#client = client;
|
|
2134
|
+
this.#registerInboundHandlers(client);
|
|
2135
|
+
await new Promise((resolve, reject) => {
|
|
2136
|
+
let settled = false;
|
|
2137
|
+
const resolveOnce = () => {
|
|
2138
|
+
if (settled) {
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
settled = true;
|
|
2142
|
+
resolve();
|
|
2143
|
+
};
|
|
2144
|
+
const rejectOnce = (error, phase) => {
|
|
2145
|
+
if (settled) {
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
settled = true;
|
|
2149
|
+
reject(this.#toConnectionError(error, websocketUrl, phase));
|
|
2150
|
+
};
|
|
2151
|
+
client.on("authenticated", resolveOnce);
|
|
2152
|
+
client.on("ready", resolveOnce);
|
|
2153
|
+
client.on("error", (error) => rejectOnce(error, "connect"));
|
|
2154
|
+
client.on("auth_error", (error) => rejectOnce(error, "subscribe"));
|
|
2155
|
+
client.on("close", () => {
|
|
2156
|
+
if (!settled) {
|
|
2157
|
+
rejectOnce(new Error("WeCom websocket closed before subscription completed"), "connect");
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
try {
|
|
2161
|
+
client.connect();
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
rejectOnce(error, "connect");
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
#toConnectionError(error, websocketUrl, phase) {
|
|
2168
|
+
if (error instanceof Error) {
|
|
2169
|
+
return new Error(`WeCom websocket ${phase} failed (${websocketUrl}): ${error.message}`);
|
|
2170
|
+
}
|
|
2171
|
+
const event = error;
|
|
2172
|
+
const details = [event?.message, event?.error].filter((value) => typeof value === "string" && value.trim().length > 0).join("; ");
|
|
2173
|
+
const suffix = details ? `: ${details}` : "";
|
|
2174
|
+
return new Error(`WeCom websocket ${phase} failed (${websocketUrl})${suffix}`);
|
|
2175
|
+
}
|
|
2176
|
+
async disconnect() {
|
|
2177
|
+
this.#client?.disconnect();
|
|
2178
|
+
this.#client = null;
|
|
2179
|
+
}
|
|
2180
|
+
#requireClient() {
|
|
2181
|
+
if (!this.#client) {
|
|
2182
|
+
throw new Error("WeCom websocket is not connected");
|
|
2183
|
+
}
|
|
2184
|
+
if (this.#kicked) {
|
|
2185
|
+
throw new Error(KICKED_ERROR_MESSAGE);
|
|
2186
|
+
}
|
|
2187
|
+
return this.#client;
|
|
2188
|
+
}
|
|
2189
|
+
async sendText(chatId, text2, replyToMessageId) {
|
|
2190
|
+
const client = this.#requireClient();
|
|
2191
|
+
const replyContext = this.#replyContextForMessage(replyToMessageId) ?? this.#lastReplyContextByChatId.get(chatId);
|
|
2192
|
+
if (replyContext) {
|
|
2193
|
+
this.#logger.debug(`reply context available for ${chatId}: ${replyContext.reqId}`);
|
|
2194
|
+
await client.reply(replyContext.frame, buildMarkdownBody(text2));
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
await client.sendMessage(chatId, buildMarkdownBody(text2));
|
|
2198
|
+
}
|
|
2199
|
+
async sendStreamText(chatId, text2, options) {
|
|
2200
|
+
const client = this.#requireClient();
|
|
2201
|
+
const replyToMessageId = options?.replyToMessageId;
|
|
2202
|
+
const replyContext = this.#replyContextForMessage(replyToMessageId) ?? this.#lastReplyContextByChatId.get(chatId);
|
|
2203
|
+
if (!replyContext) {
|
|
2204
|
+
await client.sendMessage(chatId, buildMarkdownBody(text2));
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
const streamContext = this.#streamContextFor(chatId, replyToMessageId);
|
|
2208
|
+
const feedback = !streamContext.started && options?.feedbackId ? { id: options.feedbackId } : void 0;
|
|
2209
|
+
await client.replyStream(
|
|
2210
|
+
replyContext.frame,
|
|
2211
|
+
streamContext.streamId,
|
|
2212
|
+
text2,
|
|
2213
|
+
options?.finish ?? false,
|
|
2214
|
+
void 0,
|
|
2215
|
+
feedback
|
|
2216
|
+
);
|
|
2217
|
+
streamContext.started = true;
|
|
2218
|
+
}
|
|
2219
|
+
async sendAttachment(chatId, attachment, replyToMessageId) {
|
|
2220
|
+
const client = this.#requireClient();
|
|
2221
|
+
const upload = await this.#uploadMedia(attachment);
|
|
2222
|
+
const replyContext = this.#replyContextForMessage(replyToMessageId) ?? this.#lastReplyContextByChatId.get(chatId);
|
|
2223
|
+
if (replyContext) {
|
|
2224
|
+
this.#logger.debug(`reply media context available for ${chatId}: ${replyContext.reqId}`);
|
|
2225
|
+
await client.replyMedia(replyContext.frame, upload.type, upload.mediaId);
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
await client.sendMessage(chatId, {
|
|
2229
|
+
msgtype: upload.type,
|
|
2230
|
+
[upload.type]: { media_id: upload.mediaId }
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
async #uploadMedia(attachment) {
|
|
2234
|
+
if (!this.#client) {
|
|
2235
|
+
throw new Error("WeCom websocket is not connected");
|
|
2236
|
+
}
|
|
2237
|
+
const data = await readFile3(attachment.filePath);
|
|
2238
|
+
const type = attachment.kind === "image" ? "image" : "file";
|
|
2239
|
+
const filename = sanitizeFileName(attachment.fileName ?? basename2(attachment.filePath));
|
|
2240
|
+
const response = await this.#client.uploadMedia(data, { type, filename });
|
|
2241
|
+
const mediaId = String(response.media_id ?? "").trim();
|
|
2242
|
+
if (!mediaId) {
|
|
2243
|
+
throw new Error("media upload failed: missing media_id");
|
|
2244
|
+
}
|
|
2245
|
+
return { type, mediaId };
|
|
2246
|
+
}
|
|
2247
|
+
#replyReqIdForMessage(messageId) {
|
|
2248
|
+
if (!messageId) return void 0;
|
|
2249
|
+
return this.#replyReqIdByMessageId.get(messageId);
|
|
2250
|
+
}
|
|
2251
|
+
#replyContextForMessage(messageId) {
|
|
2252
|
+
if (!messageId) return void 0;
|
|
2253
|
+
return this.#replyContextByMessageId.get(messageId);
|
|
2254
|
+
}
|
|
2255
|
+
#streamContextFor(chatId, messageId) {
|
|
2256
|
+
if (messageId) {
|
|
2257
|
+
const existing = this.#streamContextByMessageId.get(messageId);
|
|
2258
|
+
if (existing) {
|
|
2259
|
+
this.#lastStreamContextByChatId.set(chatId, existing);
|
|
2260
|
+
return existing;
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
const fallback = this.#lastStreamContextByChatId.get(chatId);
|
|
2264
|
+
if (fallback) {
|
|
2265
|
+
return fallback;
|
|
2266
|
+
}
|
|
2267
|
+
const created = { streamId: randomUUID2(), started: false };
|
|
2268
|
+
if (messageId) {
|
|
2269
|
+
this.#streamContextByMessageId.set(messageId, created);
|
|
2270
|
+
}
|
|
2271
|
+
this.#lastStreamContextByChatId.set(chatId, created);
|
|
2272
|
+
return created;
|
|
2273
|
+
}
|
|
2274
|
+
#registerInboundHandlers(client) {
|
|
2275
|
+
client.on("event", (payload) => {
|
|
2276
|
+
this.#logger.info(
|
|
2277
|
+
`received SDK event event (reqId=${String(payload?.headers?.req_id ?? "") || "n/a"} eventType=${String(payload?.body?.event?.eventtype ?? "") || "n/a"})`
|
|
2278
|
+
);
|
|
2279
|
+
});
|
|
2280
|
+
client.on("event.enter_chat", (payload) => {
|
|
2281
|
+
this.#logger.info(
|
|
2282
|
+
`received enter_chat event (reqId=${String(payload?.headers?.req_id ?? "") || "n/a"} userId=${String(payload?.body?.from?.userid ?? "") || "n/a"})`
|
|
2283
|
+
);
|
|
2284
|
+
void this.#handleEnterChat(payload);
|
|
2285
|
+
});
|
|
2286
|
+
client.on("event.disconnected_event", (payload) => {
|
|
2287
|
+
this.#kicked = true;
|
|
2288
|
+
this.#logger.error(
|
|
2289
|
+
`connection replaced by a newer connection for the same bot (reqId=${String(payload?.headers?.req_id ?? "") || "n/a"}); this instance will no longer receive or send messages`
|
|
2290
|
+
);
|
|
2291
|
+
this.#onKicked?.();
|
|
2292
|
+
});
|
|
2293
|
+
const specificMsgTypes = /* @__PURE__ */ new Set(["text", "image", "file", "voice", "video", "mixed"]);
|
|
2294
|
+
client.on("message", (payload) => {
|
|
2295
|
+
const msgtype = String(payload?.body?.msgtype ?? "").toLowerCase();
|
|
2296
|
+
if (specificMsgTypes.has(msgtype)) {
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
this.#logger.info(
|
|
2300
|
+
`received SDK event message (reqId=${String(payload?.headers?.req_id ?? "") || "n/a"} msgtype=${msgtype || "n/a"})`
|
|
2301
|
+
);
|
|
2302
|
+
void this.#handleInboundCallback(
|
|
2303
|
+
payload,
|
|
2304
|
+
String(payload?.headers?.req_id ?? "")
|
|
2305
|
+
);
|
|
2306
|
+
});
|
|
2307
|
+
for (const eventName of [
|
|
2308
|
+
"message.text",
|
|
2309
|
+
"message.image",
|
|
2310
|
+
"message.file",
|
|
2311
|
+
"message.voice",
|
|
2312
|
+
"message.video",
|
|
2313
|
+
"message.mixed"
|
|
2314
|
+
]) {
|
|
2315
|
+
client.on(eventName, (payload) => {
|
|
2316
|
+
this.#logger.info(
|
|
2317
|
+
`received SDK event ${eventName} (reqId=${String(payload?.headers?.req_id ?? "") || "n/a"} msgtype=${String(payload?.body?.msgtype ?? "") || "n/a"})`
|
|
2318
|
+
);
|
|
2319
|
+
void this.#handleInboundCallback(
|
|
2320
|
+
payload,
|
|
2321
|
+
String(payload?.headers?.req_id ?? "")
|
|
2322
|
+
);
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
/**
|
|
2327
|
+
* Records a message id as processed. Returns false when the id was already
|
|
2328
|
+
* seen, so callers can drop duplicate callbacks. Bounded to the most recent
|
|
2329
|
+
* PROCESSED_MESSAGE_ID_LIMIT ids (insertion-ordered Set acts as the LRU).
|
|
2330
|
+
*/
|
|
2331
|
+
#markMessageProcessed(messageId) {
|
|
2332
|
+
if (this.#processedMessageIds.has(messageId)) {
|
|
2333
|
+
return false;
|
|
2334
|
+
}
|
|
2335
|
+
this.#processedMessageIds.add(messageId);
|
|
2336
|
+
if (this.#processedMessageIds.size > PROCESSED_MESSAGE_ID_LIMIT) {
|
|
2337
|
+
const oldest = this.#processedMessageIds.values().next().value;
|
|
2338
|
+
if (oldest !== void 0) {
|
|
2339
|
+
this.#processedMessageIds.delete(oldest);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
return true;
|
|
2343
|
+
}
|
|
2344
|
+
async #handleInboundCallback(payload, reqId) {
|
|
2345
|
+
const body = payload.body ?? {};
|
|
2346
|
+
const senderId = String(body.from?.userid ?? "").trim();
|
|
2347
|
+
const chatId = String(body.chatid ?? senderId).trim();
|
|
2348
|
+
if (!chatId) {
|
|
2349
|
+
this.#logger.warn(`dropping inbound callback without chatId (reqId=${reqId || "n/a"})`);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
const messageId = String(body.msgid ?? reqId ?? randomUUID2()).trim();
|
|
2353
|
+
if (!this.#markMessageProcessed(messageId)) {
|
|
2354
|
+
this.#logger.info(
|
|
2355
|
+
`dropping duplicate inbound callback (messageId=${messageId} reqId=${reqId || "n/a"})`
|
|
2356
|
+
);
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
this.#replyReqIdByMessageId.set(messageId, reqId);
|
|
2360
|
+
this.#lastChatReqId.set(chatId, reqId);
|
|
2361
|
+
const replyContext = { reqId, frame: { headers: { req_id: reqId } } };
|
|
2362
|
+
this.#replyContextByMessageId.set(messageId, replyContext);
|
|
2363
|
+
this.#lastReplyContextByChatId.set(chatId, replyContext);
|
|
2364
|
+
const streamContext = { streamId: randomUUID2(), started: false };
|
|
2365
|
+
this.#streamContextByMessageId.set(messageId, streamContext);
|
|
2366
|
+
this.#lastStreamContextByChatId.set(chatId, streamContext);
|
|
2367
|
+
const textParts = [];
|
|
2368
|
+
const plainText = String(body.text?.content ?? "").trim();
|
|
2369
|
+
if (plainText) {
|
|
2370
|
+
textParts.push(plainText);
|
|
2371
|
+
}
|
|
2372
|
+
if (String(body.msgtype ?? "").toLowerCase() === "appmsg") {
|
|
2373
|
+
const title = String(body.appmsg?.title ?? "").trim();
|
|
2374
|
+
if (title) {
|
|
2375
|
+
textParts.push(title);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
if (String(body.msgtype ?? "").toLowerCase() === "mixed") {
|
|
2379
|
+
const items = Array.isArray(body.mixed?.msg_item) ? body.mixed.msg_item : [];
|
|
2380
|
+
for (const item of items) {
|
|
2381
|
+
if (String(item?.msgtype ?? "").toLowerCase() === "text") {
|
|
2382
|
+
const content = String(item?.text?.content ?? "").trim();
|
|
2383
|
+
if (content) textParts.push(content);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
const refs = this.#extractMediaRefs(body);
|
|
2388
|
+
for (const ref of refs) {
|
|
2389
|
+
const localPath = await this.#downloadMediaRef(ref);
|
|
2390
|
+
if (localPath) {
|
|
2391
|
+
textParts.push(`[Received ${ref.kind}: ${localPath}]`);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
const chatType = String(body.chattype ?? "").toLowerCase() === "group" ? "group" : "dm";
|
|
2395
|
+
const rawText = textParts.join("\n").trim();
|
|
2396
|
+
const { text: text2, mentionedBot } = this.#normalizeMention(rawText, chatType);
|
|
2397
|
+
this.#logger.info(
|
|
2398
|
+
`normalized inbound message (chatType=${chatType} chatId=${chatId} messageId=${messageId} textLength=${text2.length})`
|
|
2399
|
+
);
|
|
2400
|
+
await this.#onMessage?.({
|
|
2401
|
+
chatId,
|
|
2402
|
+
chatType,
|
|
2403
|
+
messageId,
|
|
2404
|
+
text: text2,
|
|
2405
|
+
mentionedBot,
|
|
2406
|
+
raw: payload
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
async #handleEnterChat(payload) {
|
|
2410
|
+
if (!this.#client) {
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
const reqId = String(payload?.headers?.req_id ?? "").trim();
|
|
2414
|
+
if (!reqId) {
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
try {
|
|
2418
|
+
await this.#client.replyWelcome(
|
|
2419
|
+
{ headers: { req_id: reqId } },
|
|
2420
|
+
{
|
|
2421
|
+
msgtype: "text",
|
|
2422
|
+
text: {
|
|
2423
|
+
content: "\u60A8\u597D\uFF0C\u6211\u5DF2\u8FDE\u63A5\u6210\u529F\uFF0C\u53EF\u4EE5\u76F4\u63A5\u7ED9\u6211\u53D1\u6D88\u606F\u3002"
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
);
|
|
2427
|
+
this.#logger.info(`sent enter_chat welcome reply (reqId=${reqId})`);
|
|
2428
|
+
} catch (error) {
|
|
2429
|
+
this.#logger.warn(`failed to send enter_chat welcome reply (reqId=${reqId}):`, error);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
#normalizeMention(text2, chatType) {
|
|
2433
|
+
if (chatType !== "group") {
|
|
2434
|
+
return { text: text2, mentionedBot: false };
|
|
2435
|
+
}
|
|
2436
|
+
const match = text2.match(/^@(\S+)\s*(.*)$/s);
|
|
2437
|
+
if (!match) {
|
|
2438
|
+
return { text: text2, mentionedBot: false };
|
|
2439
|
+
}
|
|
2440
|
+
return { text: match[2] ?? "", mentionedBot: true };
|
|
2441
|
+
}
|
|
2442
|
+
#extractMediaRefs(body) {
|
|
2443
|
+
const refs = [];
|
|
2444
|
+
const msgType = String(body.msgtype ?? "").toLowerCase();
|
|
2445
|
+
if (msgType === "image" && body.image) {
|
|
2446
|
+
refs.push({
|
|
2447
|
+
kind: "image",
|
|
2448
|
+
url: typeof body.image.url === "string" ? body.image.url : void 0,
|
|
2449
|
+
aeskey: typeof body.image.aeskey === "string" ? body.image.aeskey : void 0,
|
|
2450
|
+
base64: typeof body.image.base64 === "string" ? body.image.base64 : void 0,
|
|
2451
|
+
fileName: typeof body.image.filename === "string" ? body.image.filename : void 0
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
if (msgType === "file" && body.file) {
|
|
2455
|
+
refs.push({
|
|
2456
|
+
kind: "file",
|
|
2457
|
+
url: typeof body.file.url === "string" ? body.file.url : void 0,
|
|
2458
|
+
aeskey: typeof body.file.aeskey === "string" ? body.file.aeskey : void 0,
|
|
2459
|
+
base64: typeof body.file.base64 === "string" ? body.file.base64 : void 0,
|
|
2460
|
+
fileName: typeof body.file.filename === "string" ? body.file.filename : typeof body.file.name === "string" ? body.file.name : void 0
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
if (msgType === "appmsg" && body.appmsg?.file) {
|
|
2464
|
+
refs.push({
|
|
2465
|
+
kind: "file",
|
|
2466
|
+
url: typeof body.appmsg.file.url === "string" ? body.appmsg.file.url : void 0,
|
|
2467
|
+
aeskey: typeof body.appmsg.file.aeskey === "string" ? body.appmsg.file.aeskey : void 0,
|
|
2468
|
+
base64: typeof body.appmsg.file.base64 === "string" ? body.appmsg.file.base64 : void 0,
|
|
2469
|
+
fileName: typeof body.appmsg.file.filename === "string" ? body.appmsg.file.filename : typeof body.appmsg.title === "string" ? body.appmsg.title : void 0
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
return refs;
|
|
2473
|
+
}
|
|
2474
|
+
async #downloadMediaRef(ref) {
|
|
2475
|
+
try {
|
|
2476
|
+
await ensureMediaDir();
|
|
2477
|
+
let bytes;
|
|
2478
|
+
let fileName = ref.fileName;
|
|
2479
|
+
if (ref.base64) {
|
|
2480
|
+
bytes = decodeBase64(ref.base64);
|
|
2481
|
+
} else if (ref.url) {
|
|
2482
|
+
if (!this.#client) {
|
|
2483
|
+
throw new Error("WeCom websocket is not connected");
|
|
2484
|
+
}
|
|
2485
|
+
const downloaded = await this.#client.downloadFile(ref.url, ref.aeskey);
|
|
2486
|
+
bytes = downloaded.buffer;
|
|
2487
|
+
fileName = fileName ?? downloaded.filename;
|
|
2488
|
+
} else {
|
|
2489
|
+
return null;
|
|
2490
|
+
}
|
|
2491
|
+
const safeName = fileName ? sanitizeFileName(fileName) : ref.kind === "image" ? `${randomUUID2()}${detectImageExtension(bytes)}` : `${randomUUID2()}.bin`;
|
|
2492
|
+
const resolvedName = extname(safeName) ? safeName : ref.kind === "image" ? `${safeName}${detectImageExtension(bytes)}` : `${safeName}.bin`;
|
|
2493
|
+
const outputPath = join2(MEDIA_TEMP_DIR, `${Date.now()}-${resolvedName}`);
|
|
2494
|
+
await writeFile3(outputPath, bytes);
|
|
2495
|
+
return outputPath;
|
|
2496
|
+
} catch (error) {
|
|
2497
|
+
this.#logger.warn(`failed to download ${ref.kind} resource:`, error);
|
|
2498
|
+
return null;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
};
|
|
2502
|
+
|
|
2503
|
+
// src/modules/client/wecom/adapter/wecom-session.ts
|
|
2504
|
+
function buildWecomSessionId(chatType, chatId) {
|
|
2505
|
+
if (chatType === "dm") {
|
|
2506
|
+
return `wecom:dm:${chatId}`;
|
|
2507
|
+
}
|
|
2508
|
+
if (chatType === "group") {
|
|
2509
|
+
return `wecom:group:${chatId}`;
|
|
2510
|
+
}
|
|
2511
|
+
throw new Error(`Unsupported WeCom chat type: ${chatType}`);
|
|
2512
|
+
}
|
|
2513
|
+
function parseWecomSessionId(clientSessionId) {
|
|
2514
|
+
if (clientSessionId.startsWith("wecom:dm:")) {
|
|
2515
|
+
return { platform: "wecom", chatType: "dm", chatId: clientSessionId.slice("wecom:dm:".length) };
|
|
2516
|
+
}
|
|
2517
|
+
if (clientSessionId.startsWith("wecom:group:")) {
|
|
2518
|
+
return { platform: "wecom", chatType: "group", chatId: clientSessionId.slice("wecom:group:".length) };
|
|
2519
|
+
}
|
|
2520
|
+
throw new Error(`Unsupported clientSessionId: ${clientSessionId}`);
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// src/modules/client/wecom/adapter/wecom-im-adapter.ts
|
|
2524
|
+
var MAX_TEXT_CHUNK2 = 4e3;
|
|
2525
|
+
var STARTING_MESSAGE = "Processing...";
|
|
2526
|
+
function chunkText2(text2, maxLen) {
|
|
2527
|
+
if (text2.length <= maxLen) return [text2];
|
|
2528
|
+
const chunks = [];
|
|
2529
|
+
let remaining = text2;
|
|
2530
|
+
while (remaining.length > 0) {
|
|
2531
|
+
if (remaining.length <= maxLen) {
|
|
2532
|
+
chunks.push(remaining);
|
|
2533
|
+
break;
|
|
2534
|
+
}
|
|
2535
|
+
let splitPos = remaining.lastIndexOf("\n", maxLen);
|
|
2536
|
+
if (splitPos <= 0) {
|
|
2537
|
+
splitPos = remaining.lastIndexOf(" ", maxLen);
|
|
2538
|
+
}
|
|
2539
|
+
if (splitPos <= 0) {
|
|
2540
|
+
chunks.push(remaining.slice(0, maxLen));
|
|
2541
|
+
remaining = remaining.slice(maxLen);
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
chunks.push(remaining.slice(0, splitPos + 1));
|
|
2545
|
+
remaining = remaining.slice(splitPos + 1);
|
|
2546
|
+
}
|
|
2547
|
+
return chunks.filter((chunk) => chunk.length > 0);
|
|
2548
|
+
}
|
|
2549
|
+
var WecomIMAdapter = class {
|
|
2550
|
+
#config;
|
|
2551
|
+
#logger;
|
|
2552
|
+
#onOutput = null;
|
|
2553
|
+
#client = null;
|
|
2554
|
+
#egressQueue = [];
|
|
2555
|
+
#processing = false;
|
|
2556
|
+
#lastInboundMessageIdBySession = /* @__PURE__ */ new Map();
|
|
2557
|
+
#progressStateBySession = /* @__PURE__ */ new Map();
|
|
2558
|
+
async #notifySendFailure(chatId, error) {
|
|
2559
|
+
if (!this.#client) {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (this.#client.isKicked()) {
|
|
2563
|
+
this.#logger.debug("skipping send-failure notification, connection was replaced");
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2566
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2567
|
+
const text2 = `[agent-bridge error] Message delivery failed
|
|
2568
|
+
|
|
2569
|
+
${message}`;
|
|
2570
|
+
try {
|
|
2571
|
+
await this.#client.sendText(chatId, text2);
|
|
2572
|
+
} catch (notifyError) {
|
|
2573
|
+
this.#logger.error("failed to notify send failure:", notifyError);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
constructor(config, logger3 = createLogger("wecom")) {
|
|
2577
|
+
this.#config = config;
|
|
2578
|
+
this.#logger = logger3;
|
|
2579
|
+
}
|
|
2580
|
+
async start(onOutput) {
|
|
2581
|
+
this.#onOutput = onOutput;
|
|
2582
|
+
this.#client = new WecomClient(this.#config, this.#logger);
|
|
2583
|
+
this.#client.setOnKicked(() => {
|
|
2584
|
+
this.#logger.error(
|
|
2585
|
+
"this bot connection was replaced by a newer connection (another instance is running with the same bot credentials); this instance will keep running but can no longer receive or send messages \u2014 stop the other instance and restart this process to recover"
|
|
2586
|
+
);
|
|
2587
|
+
});
|
|
2588
|
+
this.#client.setOnMessage(async ({ chatId, chatType, text: text2, messageId, mentionedBot }) => {
|
|
2589
|
+
if (!this.#onOutput) {
|
|
2590
|
+
this.#logger.warn(`dropping inbound message, adapter not ready (chatId=${chatId})`);
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
this.#logger.info(
|
|
2594
|
+
`adapter received inbound message (chatType=${chatType} chatId=${chatId} messageId=${messageId} mentionedBot=${mentionedBot} textLength=${text2.trim().length})`
|
|
2595
|
+
);
|
|
2596
|
+
const clientSessionId = buildWecomSessionId(chatType, chatId);
|
|
2597
|
+
if (chatType === "group" && (this.#config.requireMentionInGroup ?? true) && !mentionedBot) {
|
|
2598
|
+
this.#logger.debug(
|
|
2599
|
+
`ignoring group message without bot mention (session=${clientSessionId} messageId=${messageId})`
|
|
2600
|
+
);
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
const normalizedText = text2.trim();
|
|
2604
|
+
const commandEvent = parseSlashCommand(normalizedText, clientSessionId);
|
|
2605
|
+
if (commandEvent) {
|
|
2606
|
+
await this.#onOutput(commandEvent);
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
this.#lastInboundMessageIdBySession.set(clientSessionId, messageId);
|
|
2610
|
+
this.#resetProgressState(clientSessionId);
|
|
2611
|
+
await this.#announceStart(chatId, messageId, clientSessionId);
|
|
2612
|
+
await this.#onOutput({
|
|
2613
|
+
type: "user.message",
|
|
2614
|
+
clientSessionId,
|
|
2615
|
+
text: text2
|
|
2616
|
+
});
|
|
2617
|
+
});
|
|
2618
|
+
await this.#client.connect();
|
|
2619
|
+
this.#logger.info(
|
|
2620
|
+
`adapter started (websocketUrl=${this.#config.websocketUrl ?? "wss://openws.work.weixin.qq.com"})`
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
async stop() {
|
|
2624
|
+
this.#egressQueue.length = 0;
|
|
2625
|
+
this.#progressStateBySession.clear();
|
|
2626
|
+
if (this.#client) {
|
|
2627
|
+
await this.#client.disconnect();
|
|
2628
|
+
this.#client = null;
|
|
2629
|
+
}
|
|
2630
|
+
this.#processing = false;
|
|
2631
|
+
this.#onOutput = null;
|
|
2632
|
+
this.#logger.info("adapter stopped");
|
|
2633
|
+
}
|
|
2634
|
+
async input(event) {
|
|
2635
|
+
if (!this.#client) {
|
|
2636
|
+
throw new Error("WecomIMAdapter is not started");
|
|
2637
|
+
}
|
|
2638
|
+
this.#egressQueue.push(event);
|
|
2639
|
+
void this.#drainEgressQueue();
|
|
2640
|
+
}
|
|
2641
|
+
async isBusy() {
|
|
2642
|
+
return this.#processing || this.#egressQueue.length > 0;
|
|
2643
|
+
}
|
|
2644
|
+
async #drainEgressQueue() {
|
|
2645
|
+
if (this.#processing) {
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
this.#processing = true;
|
|
2649
|
+
try {
|
|
2650
|
+
while (this.#client && this.#egressQueue.length > 0) {
|
|
2651
|
+
const event = this.#egressQueue.shift();
|
|
2652
|
+
if (!event) continue;
|
|
2653
|
+
try {
|
|
2654
|
+
const target = parseWecomSessionId(event.clientSessionId);
|
|
2655
|
+
if (event.type !== "assistant.message") {
|
|
2656
|
+
await this.#handleProgressEvent(target.chatId, event);
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
const replyToMessageId = this.#lastInboundMessageIdBySession.get(event.clientSessionId);
|
|
2660
|
+
await this.#finishProgressMessage(target.chatId, event.clientSessionId, replyToMessageId);
|
|
2661
|
+
if (event.text.trim().length > 0) {
|
|
2662
|
+
const chunks = chunkText2(event.text, MAX_TEXT_CHUNK2);
|
|
2663
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
2664
|
+
await this.#client.sendText(target.chatId, chunk, index === 0 ? replyToMessageId : void 0);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
for (const attachment of event.attachments ?? []) {
|
|
2668
|
+
try {
|
|
2669
|
+
await this.#client.sendAttachment(target.chatId, attachment, replyToMessageId);
|
|
2670
|
+
} catch (attachmentError) {
|
|
2671
|
+
this.#logger.error("failed to send attachment:", attachmentError);
|
|
2672
|
+
await this.#notifySendFailure(target.chatId, attachmentError);
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
} catch (error) {
|
|
2676
|
+
this.#logger.error("failed to send egress event:", error);
|
|
2677
|
+
try {
|
|
2678
|
+
const target = parseWecomSessionId(event.clientSessionId);
|
|
2679
|
+
await this.#notifySendFailure(target.chatId, error);
|
|
2680
|
+
} catch (notifyError) {
|
|
2681
|
+
this.#logger.error("failed to handle egress send failure:", notifyError);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
} finally {
|
|
2686
|
+
this.#processing = false;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
async #announceStart(chatId, messageId, clientSessionId) {
|
|
2690
|
+
const state = this.#progressStateBySession.get(clientSessionId);
|
|
2691
|
+
if (!state || state.announced || !this.#client) {
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
await this.#client.sendStreamText(chatId, STARTING_MESSAGE, {
|
|
2695
|
+
replyToMessageId: messageId,
|
|
2696
|
+
finish: false
|
|
2697
|
+
});
|
|
2698
|
+
state.announced = true;
|
|
2699
|
+
}
|
|
2700
|
+
async #handleProgressEvent(chatId, event) {
|
|
2701
|
+
if (!this.#client) {
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
const state = this.#progressStateBySession.get(event.clientSessionId) ?? {
|
|
2705
|
+
renderer: new ProgressRenderer(),
|
|
2706
|
+
announced: false
|
|
2707
|
+
};
|
|
2708
|
+
this.#progressStateBySession.set(event.clientSessionId, state);
|
|
2709
|
+
if (!state.renderer.isProgressEvent(event)) {
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
state.renderer.takeProgressEvent(event);
|
|
2713
|
+
const body = state.renderer.getCurrentProgress().markdown;
|
|
2714
|
+
const replyToMessageId = this.#lastInboundMessageIdBySession.get(event.clientSessionId);
|
|
2715
|
+
await this.#client.sendStreamText(chatId, body, {
|
|
2716
|
+
replyToMessageId,
|
|
2717
|
+
finish: false
|
|
2718
|
+
});
|
|
2719
|
+
state.announced = true;
|
|
2720
|
+
}
|
|
2721
|
+
async #finishProgressMessage(chatId, clientSessionId, replyToMessageId) {
|
|
2722
|
+
const state = this.#progressStateBySession.get(clientSessionId);
|
|
2723
|
+
if (!state || !state.announced || !this.#client) {
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
this.#progressStateBySession.delete(clientSessionId);
|
|
2727
|
+
const progress = state.renderer.getCurrentProgress();
|
|
2728
|
+
const body = progress.markdown === NO_PROGRESS_MARKDOWN ? STARTING_MESSAGE : progress.markdown;
|
|
2729
|
+
try {
|
|
2730
|
+
await this.#client.sendStreamText(chatId, body, { replyToMessageId, finish: true });
|
|
2731
|
+
} catch (error) {
|
|
2732
|
+
this.#logger.warn("failed to finish progress message:", error);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
#resetProgressState(clientSessionId) {
|
|
2736
|
+
this.#progressStateBySession.set(clientSessionId, {
|
|
2737
|
+
renderer: new ProgressRenderer(),
|
|
2738
|
+
announced: false
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
};
|
|
2742
|
+
|
|
2743
|
+
// src/modules/client/wecom/index.ts
|
|
2744
|
+
var DEFAULT_WEBSOCKET_URL2 = "wss://openws.work.weixin.qq.com";
|
|
2745
|
+
function createWecomConfigCollector() {
|
|
2746
|
+
return {
|
|
2747
|
+
async collect(ctx) {
|
|
2748
|
+
const botId = await ctx.input("WeCom Bot ID", {
|
|
2749
|
+
required: true,
|
|
2750
|
+
validate: (value) => value ? null : "Bot ID is required"
|
|
2751
|
+
});
|
|
2752
|
+
const secret = await ctx.input("WeCom Secret", {
|
|
2753
|
+
required: true,
|
|
2754
|
+
secret: true,
|
|
2755
|
+
validate: (value) => value ? null : "Secret is required"
|
|
2756
|
+
});
|
|
2757
|
+
const websocketUrl = await ctx.input("WeCom WebSocket URL", {
|
|
2758
|
+
defaultValue: DEFAULT_WEBSOCKET_URL2,
|
|
2759
|
+
validate: (value) => !value || /^wss?:\/\//.test(value) ? null : "WebSocket URL must start with ws:// or wss://"
|
|
2760
|
+
});
|
|
2761
|
+
const requireMentionInGroup = await ctx.confirm("Require @mention in group chats", true);
|
|
2762
|
+
return { botId, secret, websocketUrl, requireMentionInGroup };
|
|
2763
|
+
},
|
|
2764
|
+
validate(config) {
|
|
2765
|
+
if (!config.botId.trim()) {
|
|
2766
|
+
throw new Error("WeCom botId is required");
|
|
2767
|
+
}
|
|
2768
|
+
if (!config.secret.trim()) {
|
|
2769
|
+
throw new Error("WeCom secret is required");
|
|
2770
|
+
}
|
|
2771
|
+
if (config.websocketUrl && !/^wss?:\/\//.test(config.websocketUrl)) {
|
|
2772
|
+
throw new Error("WeCom websocketUrl must start with ws:// or wss://");
|
|
2773
|
+
}
|
|
2774
|
+
},
|
|
2775
|
+
summarize(config) {
|
|
2776
|
+
const masked = config.botId.length > 8 ? `${config.botId.slice(0, 4)}****${config.botId.slice(-4)}` : "****";
|
|
2777
|
+
return `type=wecom botId=${masked} websocketUrl=${config.websocketUrl ?? DEFAULT_WEBSOCKET_URL2} requireMentionInGroup=${config.requireMentionInGroup ?? true}`;
|
|
2778
|
+
}
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
var wecomClientModule = {
|
|
2782
|
+
type: "wecom",
|
|
2783
|
+
createConfigCollector: createWecomConfigCollector,
|
|
2784
|
+
createClientAdapter(config) {
|
|
2785
|
+
return new WecomIMAdapter(config);
|
|
2786
|
+
}
|
|
2787
|
+
};
|
|
2788
|
+
|
|
2789
|
+
// src/modules/client/weixin/adapter/weixin-client.ts
|
|
2790
|
+
import * as fs from "fs/promises";
|
|
2791
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2792
|
+
import { basename as basename3, extname as extname2, join as join3 } from "path";
|
|
2793
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
2794
|
+
import { Client as OpenILinkClient, extractText, TYPING, CANCEL_TYPING } from "@openilink/openilink-sdk-node";
|
|
2795
|
+
var MEDIA_TEMP_DIR2 = join3(tmpdir3(), "agent-bridge-weixin-media");
|
|
2796
|
+
var STALE_SESSION_ERRCODE = -2;
|
|
2797
|
+
var STALE_SESSION_ERRMSG = "unknown error";
|
|
2798
|
+
var WeixinStaleSessionError = class extends Error {
|
|
2799
|
+
constructor(message = "Weixin conversation context became stale; wait for the user to send a fresh message.") {
|
|
2800
|
+
super(message);
|
|
2801
|
+
this.name = "WeixinStaleSessionError";
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
var WeixinClient = class {
|
|
2805
|
+
#config;
|
|
2806
|
+
#logger;
|
|
2807
|
+
#onMessage = null;
|
|
2808
|
+
#client = null;
|
|
2809
|
+
#monitorTask = null;
|
|
2810
|
+
#running = false;
|
|
2811
|
+
#syncBuf = "";
|
|
2812
|
+
constructor(config, logger3 = createLogger("weixin")) {
|
|
2813
|
+
this.#config = config;
|
|
2814
|
+
this.#logger = logger3;
|
|
2815
|
+
}
|
|
2816
|
+
setOnMessage(onMessage) {
|
|
2817
|
+
this.#onMessage = onMessage;
|
|
2818
|
+
}
|
|
2819
|
+
async connect() {
|
|
2820
|
+
if (this.#client) {
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
this.#client = new OpenILinkClient(this.#config.token, {
|
|
2824
|
+
base_url: this.#config.baseUrl,
|
|
2825
|
+
cdn_base_url: this.#config.cdnBaseUrl
|
|
2826
|
+
});
|
|
2827
|
+
this.#running = true;
|
|
2828
|
+
this.#monitorTask = this.#client.monitor(
|
|
2829
|
+
async (message) => {
|
|
2830
|
+
await this.#handleMessage(message);
|
|
2831
|
+
},
|
|
2832
|
+
{
|
|
2833
|
+
initial_buf: this.#syncBuf,
|
|
2834
|
+
on_buf_update: (buf) => {
|
|
2835
|
+
this.#syncBuf = buf;
|
|
2836
|
+
},
|
|
2837
|
+
on_error: (error) => {
|
|
2838
|
+
this.#logger.error("weixin monitor error:", error);
|
|
2839
|
+
},
|
|
2840
|
+
should_continue: () => this.#running
|
|
2841
|
+
}
|
|
2842
|
+
).catch((error) => {
|
|
2843
|
+
if (this.#running) {
|
|
2844
|
+
this.#logger.error("weixin monitor stopped unexpectedly:", error);
|
|
2845
|
+
}
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
async disconnect() {
|
|
2849
|
+
this.#running = false;
|
|
2850
|
+
try {
|
|
2851
|
+
await this.#monitorTask;
|
|
2852
|
+
} catch {
|
|
2853
|
+
}
|
|
2854
|
+
this.#monitorTask = null;
|
|
2855
|
+
this.#client = null;
|
|
2856
|
+
}
|
|
2857
|
+
async sendText(chatId, text2) {
|
|
2858
|
+
if (!this.#client) {
|
|
2859
|
+
throw new Error("Weixin client is not connected");
|
|
2860
|
+
}
|
|
2861
|
+
try {
|
|
2862
|
+
await this.#client.push(chatId, text2);
|
|
2863
|
+
} catch (error) {
|
|
2864
|
+
if (isStaleSessionError(error)) {
|
|
2865
|
+
throw new WeixinStaleSessionError();
|
|
2866
|
+
}
|
|
2867
|
+
throw error;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
async sendAttachment(chatId, attachment) {
|
|
2871
|
+
if (!this.#client) {
|
|
2872
|
+
throw new Error("Weixin client is not connected");
|
|
2873
|
+
}
|
|
2874
|
+
const contextToken = this.#client.getContextToken(chatId);
|
|
2875
|
+
if (!contextToken) {
|
|
2876
|
+
throw new Error(`No context token for Weixin chat ${chatId}`);
|
|
2877
|
+
}
|
|
2878
|
+
const data = await fs.readFile(attachment.filePath);
|
|
2879
|
+
await this.#client.sendMediaFile(
|
|
2880
|
+
chatId,
|
|
2881
|
+
contextToken,
|
|
2882
|
+
data,
|
|
2883
|
+
attachment.fileName ?? basename3(attachment.filePath),
|
|
2884
|
+
attachment.caption ?? ""
|
|
2885
|
+
);
|
|
2886
|
+
}
|
|
2887
|
+
async sendTyping(chatId) {
|
|
2888
|
+
if (!this.#client) {
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2891
|
+
const contextToken = this.#client.getContextToken(chatId);
|
|
2892
|
+
if (!contextToken) {
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
2895
|
+
const config = await this.#client.getConfig(chatId, contextToken);
|
|
2896
|
+
if (!config.typing_ticket) {
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
await this.#client.sendTyping(chatId, config.typing_ticket, TYPING);
|
|
2900
|
+
}
|
|
2901
|
+
async stopTyping(chatId) {
|
|
2902
|
+
if (!this.#client) {
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
const contextToken = this.#client.getContextToken(chatId);
|
|
2906
|
+
if (!contextToken) {
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
const config = await this.#client.getConfig(chatId, contextToken);
|
|
2910
|
+
if (!config.typing_ticket) {
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
await this.#client.sendTyping(chatId, config.typing_ticket, CANCEL_TYPING);
|
|
2914
|
+
}
|
|
2915
|
+
async #handleMessage(message) {
|
|
2916
|
+
const chatType = this.#inferChatType(message);
|
|
2917
|
+
const chatId = this.#resolveChatId(message, chatType);
|
|
2918
|
+
const extractedText = extractText(message);
|
|
2919
|
+
const { text: text2, mentionedBot } = this.#normalizeMention(extractedText, chatType);
|
|
2920
|
+
const attachmentHints = await this.#downloadAttachmentHints(message);
|
|
2921
|
+
const combinedText = [text2, ...attachmentHints].filter((part) => part.trim().length > 0).join("\n").trim();
|
|
2922
|
+
if (!combinedText) {
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
await this.#onMessage?.({
|
|
2926
|
+
chatId,
|
|
2927
|
+
chatType,
|
|
2928
|
+
messageId: String(message.message_id ?? randomUUID3()),
|
|
2929
|
+
text: combinedText,
|
|
2930
|
+
mentionedBot,
|
|
2931
|
+
raw: message
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
#inferChatType(message) {
|
|
2935
|
+
const roomId = String(message.room_id ?? message.chat_room_id ?? "").trim();
|
|
2936
|
+
if (roomId) {
|
|
2937
|
+
return "group";
|
|
2938
|
+
}
|
|
2939
|
+
const fromUserId = String(message.from_user_id ?? "").trim();
|
|
2940
|
+
return fromUserId.endsWith("@chatroom") ? "group" : "dm";
|
|
2941
|
+
}
|
|
2942
|
+
#resolveChatId(message, chatType) {
|
|
2943
|
+
if (chatType === "group") {
|
|
2944
|
+
return String(message.room_id ?? message.chat_room_id ?? message.from_user_id ?? "").trim();
|
|
2945
|
+
}
|
|
2946
|
+
return String(message.from_user_id ?? "").trim();
|
|
2947
|
+
}
|
|
2948
|
+
#normalizeMention(text2, chatType) {
|
|
2949
|
+
if (chatType !== "group") {
|
|
2950
|
+
return { text: text2, mentionedBot: false };
|
|
2951
|
+
}
|
|
2952
|
+
const match = text2.match(/^@(\S+)\s*(.*)$/s);
|
|
2953
|
+
if (!match) {
|
|
2954
|
+
return { text: text2, mentionedBot: false };
|
|
2955
|
+
}
|
|
2956
|
+
return { text: match[2] ?? "", mentionedBot: true };
|
|
2957
|
+
}
|
|
2958
|
+
async #downloadAttachmentHints(message) {
|
|
2959
|
+
if (!this.#client) {
|
|
2960
|
+
return [];
|
|
2961
|
+
}
|
|
2962
|
+
const hints = [];
|
|
2963
|
+
for (const item of Array.isArray(message.item_list) ? message.item_list : []) {
|
|
2964
|
+
const downloaded = await this.#downloadItem(item);
|
|
2965
|
+
if (downloaded) {
|
|
2966
|
+
hints.push(downloaded);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
return hints;
|
|
2970
|
+
}
|
|
2971
|
+
async #downloadItem(item) {
|
|
2972
|
+
if (!this.#client || typeof item?.type !== "number") {
|
|
2973
|
+
return null;
|
|
2974
|
+
}
|
|
2975
|
+
try {
|
|
2976
|
+
switch (item.type) {
|
|
2977
|
+
case 2:
|
|
2978
|
+
return await this.#downloadMediaHint(item.image_item?.media, "image", ".jpg");
|
|
2979
|
+
case 3:
|
|
2980
|
+
return await this.#downloadMediaHint(item.voice_item?.media, "voice", ".silk");
|
|
2981
|
+
case 4:
|
|
2982
|
+
return await this.#downloadMediaHint(
|
|
2983
|
+
item.file_item?.media,
|
|
2984
|
+
"file",
|
|
2985
|
+
extname2(String(item.file_item?.file_name ?? "")) || ".bin",
|
|
2986
|
+
String(item.file_item?.file_name ?? "").trim() || void 0
|
|
2987
|
+
);
|
|
2988
|
+
case 5:
|
|
2989
|
+
return await this.#downloadMediaHint(item.video_item?.media, "video", ".mp4");
|
|
2990
|
+
default:
|
|
2991
|
+
return null;
|
|
2992
|
+
}
|
|
2993
|
+
} catch (error) {
|
|
2994
|
+
this.#logger.warn("failed to download Weixin attachment:", error);
|
|
2995
|
+
return null;
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
async #downloadMediaHint(media, label, fallbackExt, preferredName) {
|
|
2999
|
+
if (!this.#client || !media) {
|
|
3000
|
+
throw new Error("missing media payload");
|
|
3001
|
+
}
|
|
3002
|
+
const bytes = await this.#client.downloadMedia(media);
|
|
3003
|
+
await fs.mkdir(MEDIA_TEMP_DIR2, { recursive: true });
|
|
3004
|
+
const safeName = (preferredName || `${Date.now()}-${randomUUID3()}${fallbackExt}`).replace(/[^a-zA-Z0-9._@-]/g, "_");
|
|
3005
|
+
const resolvedName = extname2(safeName) ? safeName : `${safeName}${fallbackExt}`;
|
|
3006
|
+
const outputPath = join3(MEDIA_TEMP_DIR2, resolvedName);
|
|
3007
|
+
await fs.writeFile(outputPath, Buffer.from(bytes));
|
|
3008
|
+
return `[Received ${label}: ${outputPath}]`;
|
|
3009
|
+
}
|
|
3010
|
+
};
|
|
3011
|
+
function isStaleSessionError(error) {
|
|
3012
|
+
const details = extractErrorDetails(error);
|
|
3013
|
+
if (details.errMsg.toLowerCase() === STALE_SESSION_ERRMSG) {
|
|
3014
|
+
return details.ret === STALE_SESSION_ERRCODE || details.errCode === STALE_SESSION_ERRCODE;
|
|
3015
|
+
}
|
|
3016
|
+
return false;
|
|
3017
|
+
}
|
|
3018
|
+
function extractErrorDetails(error) {
|
|
3019
|
+
const candidate = error;
|
|
3020
|
+
const message = String(candidate?.message ?? "");
|
|
3021
|
+
const ret = typeof candidate?.ret === "number" ? candidate.ret : parseCodeFromText(message, /ret=(-?\d+)/i);
|
|
3022
|
+
const errCode = typeof candidate?.errCode === "number" ? candidate.errCode : parseCodeFromText(message, /errcode=(-?\d+)/i);
|
|
3023
|
+
const errMsg = typeof candidate?.errMsg === "string" ? candidate.errMsg : parseErrMsgFromText(message);
|
|
3024
|
+
return { ret, errCode, errMsg };
|
|
3025
|
+
}
|
|
3026
|
+
function parseCodeFromText(text2, pattern) {
|
|
3027
|
+
const match = text2.match(pattern);
|
|
3028
|
+
if (!match) {
|
|
3029
|
+
return void 0;
|
|
3030
|
+
}
|
|
3031
|
+
const parsed = Number(match[1]);
|
|
3032
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
3033
|
+
}
|
|
3034
|
+
function parseErrMsgFromText(text2) {
|
|
3035
|
+
const match = text2.match(/errmsg=([^\n]+)$/i);
|
|
3036
|
+
return (match?.[1] ?? text2).trim();
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// src/modules/client/weixin/adapter/weixin-session.ts
|
|
3040
|
+
function buildWeixinSessionId(chatType, chatId) {
|
|
3041
|
+
if (chatType === "dm") {
|
|
3042
|
+
return `weixin:dm:${chatId}`;
|
|
3043
|
+
}
|
|
3044
|
+
if (chatType === "group") {
|
|
3045
|
+
return `weixin:group:${chatId}`;
|
|
3046
|
+
}
|
|
3047
|
+
throw new Error(`Unsupported Weixin chat type: ${chatType}`);
|
|
3048
|
+
}
|
|
3049
|
+
function parseWeixinSessionId(clientSessionId) {
|
|
3050
|
+
if (clientSessionId.startsWith("weixin:dm:")) {
|
|
3051
|
+
return { platform: "weixin", chatType: "dm", chatId: clientSessionId.slice("weixin:dm:".length) };
|
|
3052
|
+
}
|
|
3053
|
+
if (clientSessionId.startsWith("weixin:group:")) {
|
|
3054
|
+
return { platform: "weixin", chatType: "group", chatId: clientSessionId.slice("weixin:group:".length) };
|
|
3055
|
+
}
|
|
3056
|
+
throw new Error(`Unsupported clientSessionId: ${clientSessionId}`);
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
// src/modules/client/weixin/adapter/weixin-im-adapter.ts
|
|
3060
|
+
var MAX_TEXT_CHUNK3 = 2e3;
|
|
3061
|
+
var PROGRESS_INTERVAL_MS = 6e4;
|
|
3062
|
+
var MESSAGE_DEDUP_TTL_MS = 5 * 6e4;
|
|
3063
|
+
var TYPING_REFRESH_INTERVAL_MS = 1e4;
|
|
3064
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
3065
|
+
var RATE_LIMIT_THRESHOLD = 2;
|
|
3066
|
+
var RATE_LIMIT_COOLDOWN_MS = 6e4;
|
|
3067
|
+
function chunkText3(text2, maxLen) {
|
|
3068
|
+
if (text2.length <= maxLen) return [text2];
|
|
3069
|
+
const chunks = [];
|
|
3070
|
+
let remaining = text2;
|
|
3071
|
+
while (remaining.length > 0) {
|
|
3072
|
+
if (remaining.length <= maxLen) {
|
|
3073
|
+
chunks.push(remaining);
|
|
3074
|
+
break;
|
|
3075
|
+
}
|
|
3076
|
+
let splitPos = remaining.lastIndexOf("\n", maxLen);
|
|
3077
|
+
if (splitPos <= 0) {
|
|
3078
|
+
splitPos = remaining.lastIndexOf(" ", maxLen);
|
|
3079
|
+
}
|
|
3080
|
+
if (splitPos <= 0) {
|
|
3081
|
+
chunks.push(remaining.slice(0, maxLen));
|
|
3082
|
+
remaining = remaining.slice(maxLen);
|
|
3083
|
+
continue;
|
|
3084
|
+
}
|
|
3085
|
+
chunks.push(remaining.slice(0, splitPos + 1));
|
|
3086
|
+
remaining = remaining.slice(splitPos + 1);
|
|
3087
|
+
}
|
|
3088
|
+
return chunks.filter((chunk) => chunk.length > 0);
|
|
3089
|
+
}
|
|
3090
|
+
var WeixinIMAdapter = class {
|
|
3091
|
+
#config;
|
|
3092
|
+
#logger;
|
|
3093
|
+
#onOutput = null;
|
|
3094
|
+
#client = null;
|
|
3095
|
+
#egressQueue = [];
|
|
3096
|
+
#processing = false;
|
|
3097
|
+
#progressStateBySession = /* @__PURE__ */ new Map();
|
|
3098
|
+
#typingHeartbeatBySession = /* @__PURE__ */ new Map();
|
|
3099
|
+
#recentInboundMessageIds = /* @__PURE__ */ new Map();
|
|
3100
|
+
#recentInboundContentKeys = /* @__PURE__ */ new Map();
|
|
3101
|
+
#rateLimitEvents = [];
|
|
3102
|
+
#rateLimitCircuitUntil = 0;
|
|
3103
|
+
constructor(config, logger3 = createLogger("weixin")) {
|
|
3104
|
+
this.#config = config;
|
|
3105
|
+
this.#logger = logger3;
|
|
3106
|
+
}
|
|
3107
|
+
async start(onOutput) {
|
|
3108
|
+
this.#onOutput = onOutput;
|
|
3109
|
+
this.#client = new WeixinClient(this.#config, this.#logger);
|
|
3110
|
+
this.#client.setOnMessage(async ({ chatId, chatType, text: text2, messageId }) => {
|
|
3111
|
+
if (!this.#onOutput) {
|
|
3112
|
+
this.#logger.warn(`dropping inbound message, adapter not ready (chatId=${chatId})`);
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
const clientSessionId = buildWeixinSessionId(chatType, chatId);
|
|
3116
|
+
if (this.#isDuplicateInbound(chatId, messageId, text2)) {
|
|
3117
|
+
this.#logger.debug(
|
|
3118
|
+
`dropping duplicate inbound message (session=${clientSessionId} messageId=${messageId})`
|
|
3119
|
+
);
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
if (chatType === "group") {
|
|
3123
|
+
this.#logger.debug(`ignoring unsupported Weixin group message (session=${clientSessionId})`);
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
this.#resetProgressState(clientSessionId);
|
|
3127
|
+
const normalizedText = text2.trim();
|
|
3128
|
+
const commandEvent = parseSlashCommand(normalizedText, clientSessionId);
|
|
3129
|
+
if (commandEvent) {
|
|
3130
|
+
await this.#onOutput(commandEvent);
|
|
3131
|
+
return;
|
|
3132
|
+
}
|
|
3133
|
+
await this.#client?.sendTyping(chatId);
|
|
3134
|
+
this.#startTypingHeartbeat(clientSessionId, chatId);
|
|
3135
|
+
await this.#onOutput({
|
|
3136
|
+
type: "user.message",
|
|
3137
|
+
clientSessionId,
|
|
3138
|
+
text: text2
|
|
3139
|
+
});
|
|
3140
|
+
});
|
|
3141
|
+
await this.#client.connect();
|
|
3142
|
+
this.#logger.info(`adapter started (baseUrl=${this.#config.baseUrl ?? "https://ilinkai.weixin.qq.com"})`);
|
|
3143
|
+
}
|
|
3144
|
+
async stop() {
|
|
3145
|
+
this.#egressQueue.length = 0;
|
|
3146
|
+
for (const state of this.#progressStateBySession.values()) {
|
|
3147
|
+
if (state.interval) {
|
|
3148
|
+
clearInterval(state.interval);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
this.#progressStateBySession.clear();
|
|
3152
|
+
for (const timer of this.#typingHeartbeatBySession.values()) {
|
|
3153
|
+
clearInterval(timer);
|
|
3154
|
+
}
|
|
3155
|
+
this.#typingHeartbeatBySession.clear();
|
|
3156
|
+
if (this.#client) {
|
|
3157
|
+
await this.#client.disconnect();
|
|
3158
|
+
this.#client = null;
|
|
3159
|
+
}
|
|
3160
|
+
this.#processing = false;
|
|
3161
|
+
this.#onOutput = null;
|
|
3162
|
+
this.#logger.info("adapter stopped");
|
|
3163
|
+
}
|
|
3164
|
+
async input(event) {
|
|
3165
|
+
if (!this.#client) {
|
|
3166
|
+
throw new Error("WeixinIMAdapter is not started");
|
|
3167
|
+
}
|
|
3168
|
+
this.#egressQueue.push(event);
|
|
3169
|
+
await this.#drainEgressQueue();
|
|
3170
|
+
}
|
|
3171
|
+
async isBusy() {
|
|
3172
|
+
return this.#processing || this.#egressQueue.length > 0;
|
|
3173
|
+
}
|
|
3174
|
+
async #drainEgressQueue() {
|
|
3175
|
+
if (this.#processing) {
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
this.#processing = true;
|
|
3179
|
+
try {
|
|
3180
|
+
while (this.#client && this.#egressQueue.length > 0) {
|
|
3181
|
+
const event = this.#egressQueue.shift();
|
|
3182
|
+
if (!event) continue;
|
|
3183
|
+
try {
|
|
3184
|
+
const target = parseWeixinSessionId(event.clientSessionId);
|
|
3185
|
+
if (event.type === "$progress.flush") {
|
|
3186
|
+
await this.#flushProgressSummary(target.chatId, event.clientSessionId);
|
|
3187
|
+
continue;
|
|
3188
|
+
}
|
|
3189
|
+
if (event.type !== "assistant.message") {
|
|
3190
|
+
this.#recordProgressEvent(event);
|
|
3191
|
+
continue;
|
|
3192
|
+
}
|
|
3193
|
+
this.#stopProgressTimer(event.clientSessionId);
|
|
3194
|
+
if (event.text.trim().length > 0) {
|
|
3195
|
+
const chunks = chunkText3(event.text, MAX_TEXT_CHUNK3);
|
|
3196
|
+
for (const chunk of chunks) {
|
|
3197
|
+
await this.#sendTextWithProtection(target.chatId, chunk);
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
for (const attachment of event.attachments ?? []) {
|
|
3201
|
+
try {
|
|
3202
|
+
await this.#client.sendAttachment(target.chatId, attachment);
|
|
3203
|
+
} catch (attachmentError) {
|
|
3204
|
+
this.#logger.error("failed to send attachment:", attachmentError);
|
|
3205
|
+
await this.#notifySendFailure(target.chatId, attachmentError);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
this.#stopTypingHeartbeat(event.clientSessionId);
|
|
3209
|
+
await this.#client.stopTyping(target.chatId);
|
|
3210
|
+
} catch (error) {
|
|
3211
|
+
this.#logger.error("failed to send egress event:", error);
|
|
3212
|
+
try {
|
|
3213
|
+
const target = parseWeixinSessionId(event.clientSessionId);
|
|
3214
|
+
this.#stopTypingHeartbeat(event.clientSessionId);
|
|
3215
|
+
await this.#client.stopTyping(target.chatId);
|
|
3216
|
+
await this.#notifySendFailure(target.chatId, error);
|
|
3217
|
+
} catch (notifyError) {
|
|
3218
|
+
this.#logger.error("failed to handle egress send failure:", notifyError);
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
} finally {
|
|
3223
|
+
this.#processing = false;
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
#startTypingHeartbeat(clientSessionId, chatId) {
|
|
3227
|
+
this.#stopTypingHeartbeat(clientSessionId);
|
|
3228
|
+
const timer = setInterval(() => {
|
|
3229
|
+
void this.#client?.sendTyping(chatId);
|
|
3230
|
+
}, TYPING_REFRESH_INTERVAL_MS);
|
|
3231
|
+
timer.unref?.();
|
|
3232
|
+
this.#typingHeartbeatBySession.set(clientSessionId, timer);
|
|
3233
|
+
}
|
|
3234
|
+
#stopTypingHeartbeat(clientSessionId) {
|
|
3235
|
+
const timer = this.#typingHeartbeatBySession.get(clientSessionId);
|
|
3236
|
+
if (!timer) {
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
clearInterval(timer);
|
|
3240
|
+
this.#typingHeartbeatBySession.delete(clientSessionId);
|
|
3241
|
+
}
|
|
3242
|
+
async #notifySendFailure(chatId, error) {
|
|
3243
|
+
if (!this.#client) {
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3247
|
+
const text2 = `[agent-bridge error] Message delivery failed
|
|
3248
|
+
|
|
3249
|
+
${message}`;
|
|
3250
|
+
try {
|
|
3251
|
+
await this.#client.sendText(chatId, text2);
|
|
3252
|
+
} catch (notifyError) {
|
|
3253
|
+
this.#logger.error("failed to notify send failure:", notifyError);
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
async #sendTextWithProtection(chatId, text2) {
|
|
3257
|
+
if (!this.#client) {
|
|
3258
|
+
throw new Error("WeixinIMAdapter is not started");
|
|
3259
|
+
}
|
|
3260
|
+
const now = Date.now();
|
|
3261
|
+
if (this.#rateLimitCircuitUntil > now) {
|
|
3262
|
+
throw new Error("Weixin send is cooling down after rate limiting. Please try again shortly.");
|
|
3263
|
+
}
|
|
3264
|
+
if (this.#rateLimitCircuitUntil !== 0 && this.#rateLimitCircuitUntil <= now) {
|
|
3265
|
+
this.#rateLimitCircuitUntil = 0;
|
|
3266
|
+
this.#rateLimitEvents = [];
|
|
3267
|
+
}
|
|
3268
|
+
try {
|
|
3269
|
+
await this.#client.sendText(chatId, text2);
|
|
3270
|
+
this.#resetRateLimitState();
|
|
3271
|
+
} catch (error) {
|
|
3272
|
+
if (this.#isStaleSessionError(error)) {
|
|
3273
|
+
throw error;
|
|
3274
|
+
}
|
|
3275
|
+
if (this.#isRateLimitError(error)) {
|
|
3276
|
+
this.#recordRateLimitEvent(now);
|
|
3277
|
+
if (this.#rateLimitCircuitUntil > now) {
|
|
3278
|
+
throw new Error("Weixin send is cooling down after rate limiting. Please try again shortly.");
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
throw error;
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
#recordProgressEvent(event) {
|
|
3285
|
+
const state = this.#progressStateBySession.get(event.clientSessionId) ?? this.#createProgressState();
|
|
3286
|
+
this.#progressStateBySession.set(event.clientSessionId, state);
|
|
3287
|
+
if (!state.renderer.isProgressEvent(event)) {
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
state.renderer.takeProgressEvent(event);
|
|
3291
|
+
state.dirty = true;
|
|
3292
|
+
}
|
|
3293
|
+
async #flushProgressSummary(chatId, clientSessionId) {
|
|
3294
|
+
const state = this.#progressStateBySession.get(clientSessionId);
|
|
3295
|
+
if (!state || !state.dirty || !this.#client) {
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
await this.#sendTextWithProtection(chatId, state.renderer.getCurrentProgress().markdown);
|
|
3299
|
+
state.dirty = false;
|
|
3300
|
+
}
|
|
3301
|
+
#queueProgressFlush(clientSessionId) {
|
|
3302
|
+
this.#egressQueue.push({ type: "$progress.flush", clientSessionId });
|
|
3303
|
+
void this.#drainEgressQueue();
|
|
3304
|
+
}
|
|
3305
|
+
#createProgressState() {
|
|
3306
|
+
return {
|
|
3307
|
+
renderer: new ProgressRenderer(),
|
|
3308
|
+
dirty: false,
|
|
3309
|
+
interval: null
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
#resetProgressState(clientSessionId) {
|
|
3313
|
+
const previous = this.#progressStateBySession.get(clientSessionId);
|
|
3314
|
+
if (previous?.interval) {
|
|
3315
|
+
clearInterval(previous.interval);
|
|
3316
|
+
}
|
|
3317
|
+
const state = this.#createProgressState();
|
|
3318
|
+
state.interval = setInterval(() => {
|
|
3319
|
+
this.#queueProgressFlush(clientSessionId);
|
|
3320
|
+
}, PROGRESS_INTERVAL_MS);
|
|
3321
|
+
state.interval.unref?.();
|
|
3322
|
+
this.#progressStateBySession.set(clientSessionId, state);
|
|
3323
|
+
}
|
|
3324
|
+
#stopProgressTimer(clientSessionId) {
|
|
3325
|
+
const state = this.#progressStateBySession.get(clientSessionId);
|
|
3326
|
+
if (!state) {
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
if (state.interval) {
|
|
3330
|
+
clearInterval(state.interval);
|
|
3331
|
+
}
|
|
3332
|
+
this.#progressStateBySession.delete(clientSessionId);
|
|
3333
|
+
}
|
|
3334
|
+
#isDuplicateInbound(chatId, messageId, text2) {
|
|
3335
|
+
const now = Date.now();
|
|
3336
|
+
this.#pruneDedupState(now);
|
|
3337
|
+
if (messageId) {
|
|
3338
|
+
const existing = this.#recentInboundMessageIds.get(messageId);
|
|
3339
|
+
if (existing && now - existing < MESSAGE_DEDUP_TTL_MS) {
|
|
3340
|
+
return true;
|
|
3341
|
+
}
|
|
3342
|
+
this.#recentInboundMessageIds.set(messageId, now);
|
|
3343
|
+
}
|
|
3344
|
+
const normalizedText = text2.trim();
|
|
3345
|
+
if (!normalizedText) {
|
|
3346
|
+
return false;
|
|
3347
|
+
}
|
|
3348
|
+
const contentKey = `${chatId}:${normalizedText}`;
|
|
3349
|
+
const existingContent = this.#recentInboundContentKeys.get(contentKey);
|
|
3350
|
+
if (existingContent && now - existingContent < MESSAGE_DEDUP_TTL_MS) {
|
|
3351
|
+
return true;
|
|
3352
|
+
}
|
|
3353
|
+
this.#recentInboundContentKeys.set(contentKey, now);
|
|
3354
|
+
return false;
|
|
3355
|
+
}
|
|
3356
|
+
#pruneDedupState(now) {
|
|
3357
|
+
for (const [messageId, seenAt] of this.#recentInboundMessageIds) {
|
|
3358
|
+
if (now - seenAt >= MESSAGE_DEDUP_TTL_MS) {
|
|
3359
|
+
this.#recentInboundMessageIds.delete(messageId);
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
for (const [contentKey, seenAt] of this.#recentInboundContentKeys) {
|
|
3363
|
+
if (now - seenAt >= MESSAGE_DEDUP_TTL_MS) {
|
|
3364
|
+
this.#recentInboundContentKeys.delete(contentKey);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
#recordRateLimitEvent(now) {
|
|
3369
|
+
this.#rateLimitEvents = this.#rateLimitEvents.filter(
|
|
3370
|
+
(timestamp2) => now - timestamp2 < RATE_LIMIT_WINDOW_MS
|
|
3371
|
+
);
|
|
3372
|
+
this.#rateLimitEvents.push(now);
|
|
3373
|
+
if (this.#rateLimitEvents.length >= RATE_LIMIT_THRESHOLD) {
|
|
3374
|
+
this.#rateLimitCircuitUntil = now + RATE_LIMIT_COOLDOWN_MS;
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
#resetRateLimitState() {
|
|
3378
|
+
this.#rateLimitEvents = [];
|
|
3379
|
+
this.#rateLimitCircuitUntil = 0;
|
|
3380
|
+
}
|
|
3381
|
+
#isRateLimitError(error) {
|
|
3382
|
+
if (!(error instanceof Error)) {
|
|
3383
|
+
return false;
|
|
3384
|
+
}
|
|
3385
|
+
const message = error.message.toLowerCase();
|
|
3386
|
+
if (this.#isStaleSessionError(error)) {
|
|
3387
|
+
return false;
|
|
3388
|
+
}
|
|
3389
|
+
return message.includes("frequency limit") || message.includes("rate limit") || message.includes("freq limit");
|
|
3390
|
+
}
|
|
3391
|
+
#isStaleSessionError(error) {
|
|
3392
|
+
return error instanceof Error && error.name === "WeixinStaleSessionError";
|
|
3393
|
+
}
|
|
3394
|
+
};
|
|
3395
|
+
|
|
3396
|
+
// src/modules/client/weixin/adapter/weixin-qr-login.ts
|
|
3397
|
+
import { Client as OpenILinkClient2 } from "@openilink/openilink-sdk-node";
|
|
3398
|
+
async function loginWithWeixinQr(options = {}) {
|
|
3399
|
+
const { baseUrl = "https://ilinkai.weixin.qq.com", timeoutMs } = options;
|
|
3400
|
+
const renderQr = await loadQrRenderer();
|
|
3401
|
+
const client = new OpenILinkClient2("", {
|
|
3402
|
+
base_url: baseUrl
|
|
3403
|
+
});
|
|
3404
|
+
let lastQrUrl = "";
|
|
3405
|
+
const result = await client.loginWithQr(
|
|
3406
|
+
{
|
|
3407
|
+
on_qrcode(url) {
|
|
3408
|
+
lastQrUrl = url;
|
|
3409
|
+
console.log("\n\u8BF7\u4F7F\u7528\u5FAE\u4FE1\u626B\u63CF\u4EE5\u4E0B\u4E8C\u7EF4\u7801\uFF1A");
|
|
3410
|
+
try {
|
|
3411
|
+
renderQr(url);
|
|
3412
|
+
} catch (error) {
|
|
3413
|
+
console.log(`\uFF08\u7EC8\u7AEF\u4E8C\u7EF4\u7801\u6E32\u67D3\u5931\u8D25: ${String(error)}\uFF09`);
|
|
3414
|
+
console.log(url);
|
|
3415
|
+
}
|
|
3416
|
+
},
|
|
3417
|
+
on_scanned() {
|
|
3418
|
+
console.log("\n\u5DF2\u626B\u7801\uFF0C\u8BF7\u5728\u5FAE\u4FE1\u91CC\u786E\u8BA4...");
|
|
3419
|
+
},
|
|
3420
|
+
on_expired(attempt, maxAttempts) {
|
|
3421
|
+
console.log(`
|
|
3422
|
+
\u4E8C\u7EF4\u7801\u5DF2\u8FC7\u671F\uFF0C\u6B63\u5728\u5237\u65B0... (${attempt}/${maxAttempts})`);
|
|
3423
|
+
}
|
|
3424
|
+
},
|
|
3425
|
+
timeoutMs
|
|
3426
|
+
);
|
|
3427
|
+
if (!result.connected || !result.bot_id || !result.bot_token) {
|
|
3428
|
+
throw new Error(`Weixin QR login failed: ${result.message || "unknown error"}`);
|
|
3429
|
+
}
|
|
3430
|
+
const resolvedBaseUrl = result.base_url || baseUrl;
|
|
3431
|
+
console.log(`
|
|
3432
|
+
\u5FAE\u4FE1\u8FDE\u63A5\u6210\u529F\uFF0CaccountId=${result.bot_id}`);
|
|
3433
|
+
if (!lastQrUrl) {
|
|
3434
|
+
console.log("\u63D0\u793A\uFF1A\u5982\u9700\u91CD\u65B0\u767B\u5F55\uFF0C\u53EF\u518D\u6B21\u89E6\u53D1\u4E8C\u7EF4\u7801\u767B\u5F55\u6D41\u7A0B\u3002");
|
|
3435
|
+
}
|
|
3436
|
+
return {
|
|
3437
|
+
accountId: result.bot_id,
|
|
3438
|
+
token: result.bot_token,
|
|
3439
|
+
baseUrl: resolvedBaseUrl
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
async function loadQrRenderer() {
|
|
3443
|
+
const mod = await import("qrcode-terminal");
|
|
3444
|
+
const api = mod.default ?? mod;
|
|
3445
|
+
if (typeof api.generate !== "function") {
|
|
3446
|
+
throw new Error("qrcode-terminal generate() is unavailable");
|
|
3447
|
+
}
|
|
3448
|
+
return (data) => {
|
|
3449
|
+
api.generate?.(data, { small: true });
|
|
3450
|
+
console.log("\n\u4E8C\u7EF4\u7801\u94FE\u63A5\uFF1A");
|
|
3451
|
+
console.log(data);
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
// src/modules/client/weixin/index.ts
|
|
3456
|
+
var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
|
|
3457
|
+
var DEFAULT_CDN_BASE_URL = "https://novac2c.cdn.weixin.qq.com/c2c";
|
|
3458
|
+
function createWeixinConfigCollector() {
|
|
3459
|
+
return {
|
|
3460
|
+
async collect(_ctx) {
|
|
3461
|
+
const qrCreds = await loginWithWeixinQr({ baseUrl: DEFAULT_BASE_URL });
|
|
3462
|
+
return {
|
|
3463
|
+
accountId: qrCreds.accountId,
|
|
3464
|
+
token: qrCreds.token,
|
|
3465
|
+
baseUrl: qrCreds.baseUrl,
|
|
3466
|
+
cdnBaseUrl: DEFAULT_CDN_BASE_URL
|
|
3467
|
+
};
|
|
3468
|
+
},
|
|
3469
|
+
validate(config) {
|
|
3470
|
+
if (!config.accountId.trim()) {
|
|
3471
|
+
throw new Error("Weixin accountId is required");
|
|
3472
|
+
}
|
|
3473
|
+
if (!config.token.trim()) {
|
|
3474
|
+
throw new Error("Weixin token is required");
|
|
3475
|
+
}
|
|
3476
|
+
if (config.baseUrl && !/^https?:\/\//.test(config.baseUrl)) {
|
|
3477
|
+
throw new Error("Weixin baseUrl must start with http:// or https://");
|
|
3478
|
+
}
|
|
3479
|
+
if (config.cdnBaseUrl && !/^https?:\/\//.test(config.cdnBaseUrl)) {
|
|
3480
|
+
throw new Error("Weixin cdnBaseUrl must start with http:// or https://");
|
|
3481
|
+
}
|
|
3482
|
+
},
|
|
3483
|
+
summarize(config) {
|
|
3484
|
+
const masked = config.accountId.length > 8 ? `${config.accountId.slice(0, 4)}****${config.accountId.slice(-4)}` : "****";
|
|
3485
|
+
return `type=weixin accountId=${masked} baseUrl=${config.baseUrl ?? DEFAULT_BASE_URL}`;
|
|
3486
|
+
}
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
var weixinClientModule = {
|
|
3490
|
+
type: "weixin",
|
|
3491
|
+
createConfigCollector: createWeixinConfigCollector,
|
|
3492
|
+
createClientAdapter(config) {
|
|
3493
|
+
return new WeixinIMAdapter(config);
|
|
3494
|
+
}
|
|
3495
|
+
};
|
|
3496
|
+
|
|
1831
3497
|
// src/modules/client/index.ts
|
|
1832
3498
|
var registry2 = /* @__PURE__ */ new Map([
|
|
1833
|
-
[feishuClientModule.type, feishuClientModule]
|
|
3499
|
+
[feishuClientModule.type, feishuClientModule],
|
|
3500
|
+
[wecomClientModule.type, wecomClientModule],
|
|
3501
|
+
[weixinClientModule.type, weixinClientModule]
|
|
1834
3502
|
]);
|
|
1835
3503
|
function listClientModules() {
|
|
1836
3504
|
return [...registry2.values()];
|
|
@@ -1995,7 +3663,7 @@ async function startChannel(channelName) {
|
|
|
1995
3663
|
}
|
|
1996
3664
|
async function runCli(argv = process2.argv) {
|
|
1997
3665
|
const program = new Command();
|
|
1998
|
-
program.name("agent-bridge").description("IM to
|
|
3666
|
+
program.name("agent-bridge").description("IM to Agent bridge CLI").version("0.1.0");
|
|
1999
3667
|
program.command("add").description("Interactively add a channel").action(async () => {
|
|
2000
3668
|
const config = await loadConfig();
|
|
2001
3669
|
await addChannel(config);
|