ylib-syim 0.0.12 → 0.0.14
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/bridges/main.ts +348 -30
- package/bridges/runtime-event-reporter.ts +7 -2
- package/bridges/weixin-stdio-bridge.ts +62 -0
- package/package.json +6 -5
- package/scripts/weixin-stdio-bridge.ts +663 -0
package/bridges/main.ts
CHANGED
|
@@ -43,7 +43,7 @@ const runtimeErrorLogFallbackLines = Math.max(
|
|
|
43
43
|
);
|
|
44
44
|
|
|
45
45
|
type RuntimeBotStatus = {
|
|
46
|
-
platform: "dingtalk" | "feishu";
|
|
46
|
+
platform: "dingtalk" | "feishu" | "weixin";
|
|
47
47
|
bot_account_id: string;
|
|
48
48
|
link_status:
|
|
49
49
|
| "connecting"
|
|
@@ -70,14 +70,16 @@ let configVersionHash = "";
|
|
|
70
70
|
let configVersionUpdatedAt = nowIso();
|
|
71
71
|
let runtimeState: "running" | "restarting" = "running";
|
|
72
72
|
let runtimeReady = false;
|
|
73
|
-
let startupOnlyMode: "all" | "dingtalk" | "lark" = "all";
|
|
73
|
+
let startupOnlyMode: "all" | "dingtalk" | "lark" | "weixin" = "all";
|
|
74
74
|
let dingtalkBridgeStarted = false;
|
|
75
75
|
let dingtalkBridgeStarting = false;
|
|
76
76
|
let larkBridgeStarted = false;
|
|
77
77
|
let larkBridgeStarting = false;
|
|
78
|
+
let weixinBridgeStarted = false;
|
|
79
|
+
let weixinBridgeStarting = false;
|
|
78
80
|
let pluginTempHomeDir: string | null = null;
|
|
79
81
|
|
|
80
|
-
type RuntimeGatewayChannel = "dingtalk" | "feishu";
|
|
82
|
+
type RuntimeGatewayChannel = "dingtalk" | "feishu" | "weixin";
|
|
81
83
|
|
|
82
84
|
type RuntimeGatewayInvokeResult = {
|
|
83
85
|
ok: boolean;
|
|
@@ -157,7 +159,10 @@ function rotateRuntimeInstanceId(): string {
|
|
|
157
159
|
return runtimeInstanceId;
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
function keyOf(
|
|
162
|
+
function keyOf(
|
|
163
|
+
platform: "dingtalk" | "feishu" | "weixin",
|
|
164
|
+
accountId: string,
|
|
165
|
+
): string {
|
|
161
166
|
return `${platform}:${accountId}`;
|
|
162
167
|
}
|
|
163
168
|
|
|
@@ -263,7 +268,7 @@ function toLogText(value: unknown, limit = 1500): string {
|
|
|
263
268
|
|
|
264
269
|
function readRecentBridgeErrorFromLog(
|
|
265
270
|
accountId: string,
|
|
266
|
-
platform: "dingtalk" | "feishu",
|
|
271
|
+
platform: "dingtalk" | "feishu" | "weixin",
|
|
267
272
|
): string | null {
|
|
268
273
|
try {
|
|
269
274
|
const loggerState = (globalThis as Record<string, unknown>)
|
|
@@ -280,7 +285,9 @@ function readRecentBridgeErrorFromLog(
|
|
|
280
285
|
const platformHints =
|
|
281
286
|
platform === "dingtalk"
|
|
282
287
|
? ["dingtalk-stdio-bridge", "[DingTalk]", "dingtalk"]
|
|
283
|
-
:
|
|
288
|
+
: platform === "feishu"
|
|
289
|
+
? ["lark-stdio-bridge", "[Feishu]", "feishu"]
|
|
290
|
+
: ["weixin-stdio-bridge", "[Weixin]", "weixin"];
|
|
284
291
|
|
|
285
292
|
for (const line of lines) {
|
|
286
293
|
const lower = line.toLowerCase();
|
|
@@ -356,7 +363,9 @@ function readRuntimeLogLines(limit: number): {
|
|
|
356
363
|
}
|
|
357
364
|
}
|
|
358
365
|
|
|
359
|
-
function clearPlatformStatuses(
|
|
366
|
+
function clearPlatformStatuses(
|
|
367
|
+
platform: "dingtalk" | "feishu" | "weixin",
|
|
368
|
+
): void {
|
|
360
369
|
for (const key of Array.from(runtimeStatusRegistry.keys())) {
|
|
361
370
|
if (key.startsWith(`${platform}:`)) {
|
|
362
371
|
runtimeStatusRegistry.delete(key);
|
|
@@ -366,7 +375,7 @@ function clearPlatformStatuses(platform: "dingtalk" | "feishu"): void {
|
|
|
366
375
|
|
|
367
376
|
function readConfiguredAccountIds(
|
|
368
377
|
cfg: Record<string, unknown>,
|
|
369
|
-
channelKey: "dingtalk-connector" | "feishu",
|
|
378
|
+
channelKey: "dingtalk-connector" | "feishu" | "openclaw-weixin",
|
|
370
379
|
): string[] {
|
|
371
380
|
const channels = (cfg.channels as Record<string, unknown>) || {};
|
|
372
381
|
const channel = channels[channelKey] as Record<string, unknown> | undefined;
|
|
@@ -384,13 +393,18 @@ function readConfiguredAccountIds(
|
|
|
384
393
|
}
|
|
385
394
|
|
|
386
395
|
function isConfiguredRuntimeAccount(
|
|
387
|
-
platform: "dingtalk" | "feishu",
|
|
396
|
+
platform: "dingtalk" | "feishu" | "weixin",
|
|
388
397
|
accountId: string,
|
|
389
398
|
): boolean {
|
|
390
399
|
if (!accountId) return false;
|
|
391
400
|
const cfg = loadedConfigForRuntime as Record<string, unknown> | null;
|
|
392
401
|
if (!cfg) return false;
|
|
393
|
-
const channelKey =
|
|
402
|
+
const channelKey =
|
|
403
|
+
platform === "dingtalk"
|
|
404
|
+
? "dingtalk-connector"
|
|
405
|
+
: platform === "feishu"
|
|
406
|
+
? "feishu"
|
|
407
|
+
: "openclaw-weixin";
|
|
394
408
|
const configured = readConfiguredAccountIds(cfg, channelKey);
|
|
395
409
|
if (configured.length === 0) return false;
|
|
396
410
|
return configured.includes(accountId);
|
|
@@ -452,9 +466,30 @@ function markConfiguredBotsAsConnecting(config: Record<string, unknown>): void {
|
|
|
452
466
|
});
|
|
453
467
|
}
|
|
454
468
|
}
|
|
469
|
+
|
|
470
|
+
const weixin = channels["openclaw-weixin"] as
|
|
471
|
+
| Record<string, unknown>
|
|
472
|
+
| undefined;
|
|
473
|
+
const weixinAccounts = (weixin?.accounts as Record<string, unknown>) || {};
|
|
474
|
+
if (Object.keys(weixinAccounts).length > 0) {
|
|
475
|
+
for (const accountId of Object.keys(weixinAccounts)) {
|
|
476
|
+
upsertRuntimeStatus({
|
|
477
|
+
platform: "weixin",
|
|
478
|
+
bot_account_id: accountId,
|
|
479
|
+
link_status: "connecting",
|
|
480
|
+
started_at: nowIso(),
|
|
481
|
+
last_heartbeat_at: nowIso(),
|
|
482
|
+
last_error: null,
|
|
483
|
+
reconnect_count: 0,
|
|
484
|
+
last_event: "config_loaded",
|
|
485
|
+
status_source: "manual",
|
|
486
|
+
last_probe_at: null,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
455
490
|
}
|
|
456
491
|
|
|
457
|
-
function markPlatformStarted(platform: "dingtalk" | "feishu"): void {
|
|
492
|
+
function markPlatformStarted(platform: "dingtalk" | "feishu" | "weixin"): void {
|
|
458
493
|
for (const [key, value] of runtimeStatusRegistry.entries()) {
|
|
459
494
|
if (!key.startsWith(`${platform}:`)) continue;
|
|
460
495
|
runtimeStatusRegistry.set(key, {
|
|
@@ -497,6 +532,7 @@ async function ensureBridgesStartedByConfig(): Promise<void> {
|
|
|
497
532
|
(startupOnlyMode === "all" || startupOnlyMode === "lark") &&
|
|
498
533
|
isChannelEnabled("feishu") &&
|
|
499
534
|
readChannelAccountCount("feishu") > 0;
|
|
535
|
+
const canStartWeixin = startupOnlyMode === "all" || startupOnlyMode === "weixin";
|
|
500
536
|
|
|
501
537
|
const tasks: Array<Promise<void>> = [];
|
|
502
538
|
|
|
@@ -562,11 +598,73 @@ async function ensureBridgesStartedByConfig(): Promise<void> {
|
|
|
562
598
|
);
|
|
563
599
|
}
|
|
564
600
|
|
|
601
|
+
if (!weixinBridgeStarted && !weixinBridgeStarting) {
|
|
602
|
+
if (canStartWeixin) {
|
|
603
|
+
weixinBridgeStarting = true;
|
|
604
|
+
console.log("[bridges/main] starting weixin bridge...");
|
|
605
|
+
tasks.push(
|
|
606
|
+
import("./weixin-stdio-bridge.ts")
|
|
607
|
+
.then(() => {
|
|
608
|
+
weixinBridgeStarted = true;
|
|
609
|
+
markPlatformStarted("weixin");
|
|
610
|
+
console.log("[bridges/main] weixin bridge started");
|
|
611
|
+
})
|
|
612
|
+
.catch((err) => {
|
|
613
|
+
console.error(
|
|
614
|
+
`[bridges/main] weixin bridge start failed, keep alive: ${(err as Error).message}`,
|
|
615
|
+
);
|
|
616
|
+
})
|
|
617
|
+
.finally(() => {
|
|
618
|
+
weixinBridgeStarting = false;
|
|
619
|
+
}),
|
|
620
|
+
);
|
|
621
|
+
} else if (startupOnlyMode === "all" || startupOnlyMode === "weixin") {
|
|
622
|
+
console.log(
|
|
623
|
+
"[bridges/main] skip weixin bridge start: enabled=false",
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
console.log(
|
|
628
|
+
`[bridges/main] skip weixin bridge start: already_started=${String(weixinBridgeStarted)} starting=${String(weixinBridgeStarting)}`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
565
632
|
if (tasks.length > 0) {
|
|
566
633
|
await Promise.all(tasks);
|
|
567
634
|
}
|
|
568
635
|
}
|
|
569
636
|
|
|
637
|
+
async function ensureWeixinBridgeReadyForGatewayInvoke(): Promise<void> {
|
|
638
|
+
if (weixinBridgeStarted) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (!(startupOnlyMode === "all" || startupOnlyMode === "weixin")) {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (weixinBridgeStarting) {
|
|
645
|
+
const deadline = Date.now() + 5000;
|
|
646
|
+
while (weixinBridgeStarting && Date.now() < deadline) {
|
|
647
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
weixinBridgeStarting = true;
|
|
653
|
+
console.log("[bridges/main] lazy starting weixin bridge for gateway invoke...");
|
|
654
|
+
try {
|
|
655
|
+
await import("./weixin-stdio-bridge.ts");
|
|
656
|
+
weixinBridgeStarted = true;
|
|
657
|
+
markPlatformStarted("weixin");
|
|
658
|
+
console.log("[bridges/main] weixin bridge lazy started");
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.error(
|
|
661
|
+
`[bridges/main] weixin bridge lazy start failed: ${(err as Error).message}`,
|
|
662
|
+
);
|
|
663
|
+
} finally {
|
|
664
|
+
weixinBridgeStarting = false;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
570
668
|
function markAllBotsStopped(reason: string): void {
|
|
571
669
|
for (const [key, value] of runtimeStatusRegistry.entries()) {
|
|
572
670
|
runtimeStatusRegistry.set(key, {
|
|
@@ -608,6 +706,9 @@ function normalizeGatewayChannel(input: unknown): RuntimeGatewayChannel | null {
|
|
|
608
706
|
if (raw === "feishu" || raw === "lark" || raw === "openclaw-lark") {
|
|
609
707
|
return "feishu";
|
|
610
708
|
}
|
|
709
|
+
if (raw === "weixin" || raw === "openclaw-weixin") {
|
|
710
|
+
return "weixin";
|
|
711
|
+
}
|
|
611
712
|
return null;
|
|
612
713
|
}
|
|
613
714
|
|
|
@@ -625,7 +726,9 @@ async function invokeGatewayMethodByChannel(args: {
|
|
|
625
726
|
const invokerKey =
|
|
626
727
|
args.channel === "dingtalk"
|
|
627
728
|
? "__IM_DINGTALK_BRIDGE_INVOKE_GATEWAY_METHOD__"
|
|
628
|
-
: "
|
|
729
|
+
: args.channel === "feishu"
|
|
730
|
+
? "__IM_LARK_BRIDGE_INVOKE_GATEWAY_METHOD__"
|
|
731
|
+
: "__IM_WEIXIN_BRIDGE_INVOKE_GATEWAY_METHOD__";
|
|
629
732
|
const invoker = (globalThis as Record<string, unknown>)[invokerKey] as
|
|
630
733
|
| ((payload: {
|
|
631
734
|
method: string;
|
|
@@ -634,7 +737,19 @@ async function invokeGatewayMethodByChannel(args: {
|
|
|
634
737
|
}) => Promise<RuntimeGatewayInvokeResult>)
|
|
635
738
|
| undefined;
|
|
636
739
|
|
|
637
|
-
if (typeof invoker !== "function") {
|
|
740
|
+
if (args.channel === "weixin" && typeof invoker !== "function") {
|
|
741
|
+
await ensureWeixinBridgeReadyForGatewayInvoke();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const ensuredInvoker = (globalThis as Record<string, unknown>)[invokerKey] as
|
|
745
|
+
| ((payload: {
|
|
746
|
+
method: string;
|
|
747
|
+
params?: Record<string, unknown>;
|
|
748
|
+
accountId?: string;
|
|
749
|
+
}) => Promise<RuntimeGatewayInvokeResult>)
|
|
750
|
+
| undefined;
|
|
751
|
+
|
|
752
|
+
if (typeof ensuredInvoker !== "function") {
|
|
638
753
|
return {
|
|
639
754
|
ok: false,
|
|
640
755
|
error: `${args.channel} bridge gateway invoker not ready`,
|
|
@@ -642,7 +757,7 @@ async function invokeGatewayMethodByChannel(args: {
|
|
|
642
757
|
}
|
|
643
758
|
|
|
644
759
|
try {
|
|
645
|
-
const result = await
|
|
760
|
+
const result = await ensuredInvoker({
|
|
646
761
|
method,
|
|
647
762
|
params: args.params || {},
|
|
648
763
|
accountId: args.accountId,
|
|
@@ -902,6 +1017,61 @@ async function probeAndRefreshStatuses(): Promise<void> {
|
|
|
902
1017
|
} catch (err) {
|
|
903
1018
|
console.error("[bridges/main] feishu probe failed", (err as Error).message);
|
|
904
1019
|
}
|
|
1020
|
+
|
|
1021
|
+
try {
|
|
1022
|
+
const configuredIds = readConfiguredAccountIds(cfg, "openclaw-weixin");
|
|
1023
|
+
if (configuredIds.length === 0) {
|
|
1024
|
+
clearPlatformStatuses("weixin");
|
|
1025
|
+
} else {
|
|
1026
|
+
for (const accountId of configuredIds) {
|
|
1027
|
+
const previous = runtimeStatusRegistry.get(keyOf("weixin", accountId));
|
|
1028
|
+
const invoke = await invokeGatewayMethodByChannel({
|
|
1029
|
+
channel: "weixin",
|
|
1030
|
+
method: "weixin.probe",
|
|
1031
|
+
params: { accountId },
|
|
1032
|
+
accountId,
|
|
1033
|
+
});
|
|
1034
|
+
const probeResult =
|
|
1035
|
+
invoke?.result && typeof invoke.result === "object"
|
|
1036
|
+
? (invoke.result as Record<string, unknown>)
|
|
1037
|
+
: null;
|
|
1038
|
+
const probeOk =
|
|
1039
|
+
invoke?.ok === true &&
|
|
1040
|
+
(!probeResult || probeResult.ok === undefined || probeResult.ok !== false);
|
|
1041
|
+
const probeError = probeResult
|
|
1042
|
+
? buildDetailedProbeErrorMessage(probeResult)
|
|
1043
|
+
: String(invoke?.error || "").trim();
|
|
1044
|
+
const probeErrorWithLogFallback = isGenericRuntimeErrorMessage(probeError)
|
|
1045
|
+
? pickMoreSpecificErrorMessage(
|
|
1046
|
+
probeError,
|
|
1047
|
+
readRecentBridgeErrorFromLog(accountId, "weixin"),
|
|
1048
|
+
)
|
|
1049
|
+
: probeError;
|
|
1050
|
+
const linkStatus: RuntimeBotStatus["link_status"] = probeOk
|
|
1051
|
+
? "connected"
|
|
1052
|
+
: weixinBridgeStarted
|
|
1053
|
+
? "error"
|
|
1054
|
+
: "connecting";
|
|
1055
|
+
upsertRuntimeStatus({
|
|
1056
|
+
platform: "weixin",
|
|
1057
|
+
bot_account_id: accountId,
|
|
1058
|
+
link_status: linkStatus,
|
|
1059
|
+
started_at: previous?.started_at || nowIso(),
|
|
1060
|
+
last_heartbeat_at: nowIso(),
|
|
1061
|
+
last_error: probeOk
|
|
1062
|
+
? null
|
|
1063
|
+
: pickMoreSpecificErrorMessage(
|
|
1064
|
+
previous?.last_error || null,
|
|
1065
|
+
probeErrorWithLogFallback || invoke?.error || null,
|
|
1066
|
+
),
|
|
1067
|
+
status_source: "probe",
|
|
1068
|
+
last_probe_at: nowIso(),
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
console.error("[bridges/main] weixin probe failed", (err as Error).message);
|
|
1074
|
+
}
|
|
905
1075
|
}
|
|
906
1076
|
|
|
907
1077
|
function startProbeLoop(): void {
|
|
@@ -1161,15 +1331,25 @@ function readEffectiveWhitelistFromConfig(
|
|
|
1161
1331
|
"enabled",
|
|
1162
1332
|
"appId",
|
|
1163
1333
|
"appSecret",
|
|
1164
|
-
"
|
|
1165
|
-
"
|
|
1334
|
+
"modelName",
|
|
1335
|
+
"agentId",
|
|
1166
1336
|
"gatewayBaseUrl",
|
|
1167
1337
|
"gatewayToken",
|
|
1168
|
-
"separateSessionByConversation",
|
|
1169
|
-
"groupSessionScope",
|
|
1170
1338
|
"streaming",
|
|
1171
1339
|
"uploadHost",
|
|
1172
|
-
|
|
1340
|
+
],
|
|
1341
|
+
runtimeReadonlyFields: [
|
|
1342
|
+
"separateSessionByConversation",
|
|
1343
|
+
"groupSessionScope",
|
|
1344
|
+
"dmPolicy",
|
|
1345
|
+
"allowFrom",
|
|
1346
|
+
"groupPolicy",
|
|
1347
|
+
"requireMention",
|
|
1348
|
+
"scopeType",
|
|
1349
|
+
"projectId",
|
|
1350
|
+
"scope_type",
|
|
1351
|
+
"type",
|
|
1352
|
+
"project_id",
|
|
1173
1353
|
],
|
|
1174
1354
|
},
|
|
1175
1355
|
"dingtalk-connector": {
|
|
@@ -1179,13 +1359,41 @@ function readEffectiveWhitelistFromConfig(
|
|
|
1179
1359
|
"clientSecret",
|
|
1180
1360
|
"gatewayToken",
|
|
1181
1361
|
"modelName",
|
|
1362
|
+
"agentId",
|
|
1182
1363
|
"asyncMode",
|
|
1364
|
+
"gatewayPort",
|
|
1365
|
+
"gatewayBaseUrl",
|
|
1366
|
+
"uploadHost",
|
|
1367
|
+
],
|
|
1368
|
+
runtimeReadonlyFields: [
|
|
1183
1369
|
"separateSessionByConversation",
|
|
1184
1370
|
"groupSessionScope",
|
|
1185
|
-
"
|
|
1371
|
+
"dmPolicy",
|
|
1372
|
+
"allowFrom",
|
|
1373
|
+
"groupPolicy",
|
|
1374
|
+
"scopeType",
|
|
1375
|
+
"projectId",
|
|
1376
|
+
"scope_type",
|
|
1377
|
+
"type",
|
|
1378
|
+
"project_id",
|
|
1379
|
+
],
|
|
1380
|
+
},
|
|
1381
|
+
"openclaw-weixin": {
|
|
1382
|
+
fields: [
|
|
1383
|
+
"enabled",
|
|
1384
|
+
"botId",
|
|
1385
|
+
"modelName",
|
|
1386
|
+
"agentId",
|
|
1186
1387
|
"gatewayBaseUrl",
|
|
1388
|
+
"gatewayToken",
|
|
1187
1389
|
"uploadHost",
|
|
1188
|
-
|
|
1390
|
+
],
|
|
1391
|
+
runtimeReadonlyFields: [
|
|
1392
|
+
"scopeType",
|
|
1393
|
+
"projectId",
|
|
1394
|
+
"scope_type",
|
|
1395
|
+
"type",
|
|
1396
|
+
"project_id",
|
|
1189
1397
|
],
|
|
1190
1398
|
},
|
|
1191
1399
|
};
|
|
@@ -1223,13 +1431,25 @@ function normalizeRuntimeConfigByWhitelist(config: Record<string, unknown>): {
|
|
|
1223
1431
|
? "dingtalk-connector"
|
|
1224
1432
|
: channelKey === "feishu"
|
|
1225
1433
|
? "openclaw-lark"
|
|
1226
|
-
: ""
|
|
1434
|
+
: channelKey === "openclaw-weixin"
|
|
1435
|
+
? "openclaw-weixin"
|
|
1436
|
+
: "";
|
|
1227
1437
|
if (!pluginId) return new Set();
|
|
1228
1438
|
const pluginWhitelist = whitelist[pluginId] as Record<string, unknown>;
|
|
1229
|
-
const fields = pluginWhitelist?.fields
|
|
1230
|
-
|
|
1439
|
+
const fields = Array.isArray(pluginWhitelist?.fields)
|
|
1440
|
+
? pluginWhitelist.fields
|
|
1441
|
+
: [];
|
|
1442
|
+
const runtimeReadonlyFields = Array.isArray(
|
|
1443
|
+
pluginWhitelist?.runtimeReadonlyFields,
|
|
1444
|
+
)
|
|
1445
|
+
? pluginWhitelist.runtimeReadonlyFields
|
|
1446
|
+
: [];
|
|
1447
|
+
// 运行时配置需要同时保留“前端可编辑字段”和“后端只读注入字段”,
|
|
1448
|
+
// 否则项目作用域等系统填充字段会在 bridge 侧被误删。
|
|
1449
|
+
const mergedFields = [...fields, ...runtimeReadonlyFields];
|
|
1450
|
+
if (mergedFields.length === 0) return new Set();
|
|
1231
1451
|
return new Set(
|
|
1232
|
-
|
|
1452
|
+
mergedFields
|
|
1233
1453
|
.map((item) => String(item || "").trim())
|
|
1234
1454
|
.filter((item) => item.length > 0),
|
|
1235
1455
|
);
|
|
@@ -1308,6 +1528,12 @@ function resolveRawSchema(
|
|
|
1308
1528
|
const schema = (cs?.schema as Record<string, unknown>) || {};
|
|
1309
1529
|
return schema;
|
|
1310
1530
|
}
|
|
1531
|
+
if (pluginId === "openclaw-weixin") {
|
|
1532
|
+
const p = pluginModule.default as Record<string, unknown> | undefined;
|
|
1533
|
+
const cs = p?.configSchema as Record<string, unknown> | undefined;
|
|
1534
|
+
const schema = (cs?.schema as Record<string, unknown>) || {};
|
|
1535
|
+
return schema;
|
|
1536
|
+
}
|
|
1311
1537
|
return {};
|
|
1312
1538
|
}
|
|
1313
1539
|
|
|
@@ -1372,6 +1598,41 @@ async function getPluginSchemas(): Promise<Array<Record<string, unknown>>> {
|
|
|
1372
1598
|
error: (err as Error).message,
|
|
1373
1599
|
});
|
|
1374
1600
|
}
|
|
1601
|
+
try {
|
|
1602
|
+
let weixinMod: Record<string, unknown>;
|
|
1603
|
+
try {
|
|
1604
|
+
const weixinPkg = "ylib-openclaw-weixin";
|
|
1605
|
+
weixinMod = (await import(weixinPkg)) as Record<string, unknown>;
|
|
1606
|
+
} catch {
|
|
1607
|
+
weixinMod = (await import("../../openclaw-weixin/index.ts")) as Record<
|
|
1608
|
+
string,
|
|
1609
|
+
unknown
|
|
1610
|
+
>;
|
|
1611
|
+
}
|
|
1612
|
+
const wRawSchema = resolveRawSchema(weixinMod, "openclaw-weixin");
|
|
1613
|
+
result.push({
|
|
1614
|
+
pluginId: "openclaw-weixin",
|
|
1615
|
+
channel: "weixin",
|
|
1616
|
+
rawSchema: wRawSchema,
|
|
1617
|
+
schemaHash: calcSchemaHash(wRawSchema),
|
|
1618
|
+
effectiveWhitelist: (whitelist["openclaw-weixin"] as Record<
|
|
1619
|
+
string,
|
|
1620
|
+
unknown
|
|
1621
|
+
>) || { fields: [] },
|
|
1622
|
+
});
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
result.push({
|
|
1625
|
+
pluginId: "openclaw-weixin",
|
|
1626
|
+
channel: "weixin",
|
|
1627
|
+
rawSchema: {},
|
|
1628
|
+
schemaHash: calcSchemaHash({}),
|
|
1629
|
+
effectiveWhitelist: (whitelist["openclaw-weixin"] as Record<
|
|
1630
|
+
string,
|
|
1631
|
+
unknown
|
|
1632
|
+
>) || { fields: [] },
|
|
1633
|
+
error: (err as Error).message,
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1375
1636
|
return result;
|
|
1376
1637
|
}
|
|
1377
1638
|
|
|
@@ -1790,6 +2051,11 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1790
2051
|
ok: false,
|
|
1791
2052
|
reason: "control_not_available",
|
|
1792
2053
|
},
|
|
2054
|
+
weixin: {
|
|
2055
|
+
attempted: false,
|
|
2056
|
+
ok: false,
|
|
2057
|
+
reason: "control_not_available",
|
|
2058
|
+
},
|
|
1793
2059
|
};
|
|
1794
2060
|
const dingtalkControl = (globalThis as Record<string, unknown>)
|
|
1795
2061
|
.__IM_DINGTALK_BRIDGE_CONTROL__ as
|
|
@@ -1799,11 +2065,18 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1799
2065
|
.__IM_LARK_BRIDGE_CONTROL__ as
|
|
1800
2066
|
| { restart?: () => Promise<void>; stop?: () => Promise<void> }
|
|
1801
2067
|
| undefined;
|
|
2068
|
+
const weixinControl = (globalThis as Record<string, unknown>)
|
|
2069
|
+
.__IM_WEIXIN_BRIDGE_CONTROL__ as
|
|
2070
|
+
| { restart?: () => Promise<void>; stop?: () => Promise<void> }
|
|
2071
|
+
| undefined;
|
|
1802
2072
|
const shouldSoftRestartDingtalk =
|
|
1803
2073
|
isChannelEnabled("dingtalk-connector") &&
|
|
1804
2074
|
readChannelAccountCount("dingtalk-connector") > 0;
|
|
1805
2075
|
const shouldSoftRestartLark =
|
|
1806
2076
|
isChannelEnabled("feishu") && readChannelAccountCount("feishu") > 0;
|
|
2077
|
+
const shouldSoftRestartWeixin =
|
|
2078
|
+
isChannelEnabled("openclaw-weixin") &&
|
|
2079
|
+
readChannelAccountCount("openclaw-weixin") > 0;
|
|
1807
2080
|
if (
|
|
1808
2081
|
typeof dingtalkControl?.restart === "function" &&
|
|
1809
2082
|
shouldSoftRestartDingtalk
|
|
@@ -1878,6 +2151,43 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1878
2151
|
);
|
|
1879
2152
|
}
|
|
1880
2153
|
}
|
|
2154
|
+
if (
|
|
2155
|
+
typeof weixinControl?.restart === "function" &&
|
|
2156
|
+
shouldSoftRestartWeixin
|
|
2157
|
+
) {
|
|
2158
|
+
console.log("[bridges/main] restart-all step=soft_restart weixin");
|
|
2159
|
+
bridgeSoftRestart.weixin.attempted = true;
|
|
2160
|
+
try {
|
|
2161
|
+
await weixinControl.restart();
|
|
2162
|
+
bridgeSoftRestart.weixin.ok = true;
|
|
2163
|
+
bridgeSoftRestart.weixin.reason = "restarted";
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
bridgeSoftRestart.weixin.ok = false;
|
|
2166
|
+
bridgeSoftRestart.weixin.reason =
|
|
2167
|
+
err instanceof Error ? err.message : "restart_failed";
|
|
2168
|
+
}
|
|
2169
|
+
} else if (!shouldSoftRestartWeixin) {
|
|
2170
|
+
if (typeof weixinControl?.stop === "function") {
|
|
2171
|
+
console.log(
|
|
2172
|
+
"[bridges/main] restart-all step=soft_restart weixin stop_only: enabled=false or accounts empty",
|
|
2173
|
+
);
|
|
2174
|
+
bridgeSoftRestart.weixin.attempted = true;
|
|
2175
|
+
try {
|
|
2176
|
+
await weixinControl.stop();
|
|
2177
|
+
bridgeSoftRestart.weixin.ok = true;
|
|
2178
|
+
bridgeSoftRestart.weixin.reason = "stopped_no_accounts";
|
|
2179
|
+
} catch (err) {
|
|
2180
|
+
bridgeSoftRestart.weixin.ok = false;
|
|
2181
|
+
bridgeSoftRestart.weixin.reason =
|
|
2182
|
+
err instanceof Error ? err.message : "stop_failed";
|
|
2183
|
+
}
|
|
2184
|
+
} else {
|
|
2185
|
+
bridgeSoftRestart.weixin.reason = "skipped_no_accounts";
|
|
2186
|
+
console.log(
|
|
2187
|
+
"[bridges/main] restart-all step=soft_restart weixin skipped: enabled=false or accounts empty",
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
1881
2191
|
console.log(
|
|
1882
2192
|
"[bridges/main] restart-all step=ensure_bridges_started begin",
|
|
1883
2193
|
);
|
|
@@ -1968,7 +2278,9 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1968
2278
|
const botAccountId = String(body.bot_account_id || "").trim();
|
|
1969
2279
|
const linkStatus = String(body.link_status || "").trim();
|
|
1970
2280
|
if (
|
|
1971
|
-
(platform !== "dingtalk" &&
|
|
2281
|
+
(platform !== "dingtalk" &&
|
|
2282
|
+
platform !== "feishu" &&
|
|
2283
|
+
platform !== "weixin") ||
|
|
1972
2284
|
!botAccountId ||
|
|
1973
2285
|
!linkStatus
|
|
1974
2286
|
) {
|
|
@@ -1976,7 +2288,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1976
2288
|
res.end(JSON.stringify({ ok: false, error: "invalid payload" }));
|
|
1977
2289
|
return;
|
|
1978
2290
|
}
|
|
1979
|
-
const p = platform as "dingtalk" | "feishu";
|
|
2291
|
+
const p = platform as "dingtalk" | "feishu" | "weixin";
|
|
1980
2292
|
if (!isConfiguredRuntimeAccount(p, botAccountId)) {
|
|
1981
2293
|
console.log(
|
|
1982
2294
|
`[bridges/main] runtime event ignored: unconfigured account platform=${p} account=${botAccountId}`,
|
|
@@ -2042,16 +2354,22 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
2042
2354
|
/**
|
|
2043
2355
|
* 统一桥接入口。
|
|
2044
2356
|
*
|
|
2045
|
-
*
|
|
2357
|
+
* 默认同时启动钉钉、飞书、微信三个 bridge。
|
|
2046
2358
|
* 可通过参数控制:
|
|
2047
2359
|
* --only=dingtalk
|
|
2048
2360
|
* --only=lark
|
|
2361
|
+
* --only=weixin
|
|
2049
2362
|
* --only=all
|
|
2050
2363
|
*/
|
|
2051
|
-
function parseOnlyArg(): "all" | "dingtalk" | "lark" {
|
|
2364
|
+
function parseOnlyArg(): "all" | "dingtalk" | "lark" | "weixin" {
|
|
2052
2365
|
const onlyArg = process.argv.find((arg) => arg.startsWith("--only="));
|
|
2053
2366
|
const onlyValue = (onlyArg?.slice("--only=".length) || "all").toLowerCase();
|
|
2054
|
-
if (
|
|
2367
|
+
if (
|
|
2368
|
+
onlyValue === "dingtalk" ||
|
|
2369
|
+
onlyValue === "lark" ||
|
|
2370
|
+
onlyValue === "weixin" ||
|
|
2371
|
+
onlyValue === "all"
|
|
2372
|
+
) {
|
|
2055
2373
|
return onlyValue;
|
|
2056
2374
|
}
|
|
2057
2375
|
console.warn(
|
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
type Platform = "dingtalk" | "feishu";
|
|
5
|
+
type Platform = "dingtalk" | "feishu" | "weixin";
|
|
6
6
|
|
|
7
7
|
function loadConfig(): Record<string, unknown> | null {
|
|
8
8
|
const configPaths = [
|
|
@@ -26,7 +26,12 @@ function listAccountIds(
|
|
|
26
26
|
cfg: Record<string, unknown>,
|
|
27
27
|
): string[] {
|
|
28
28
|
const channels = (cfg.channels as Record<string, unknown>) || {};
|
|
29
|
-
const channelKey =
|
|
29
|
+
const channelKey =
|
|
30
|
+
platform === "dingtalk"
|
|
31
|
+
? "dingtalk-connector"
|
|
32
|
+
: platform === "feishu"
|
|
33
|
+
? "feishu"
|
|
34
|
+
: "openclaw-weixin";
|
|
30
35
|
const channelCfg = channels[channelKey] as
|
|
31
36
|
| Record<string, unknown>
|
|
32
37
|
| undefined;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 主目录入口:微信 stdio bridge。
|
|
3
|
+
* 真实实现在 scripts 目录,保持与其它 bridge 一致的入口结构。
|
|
4
|
+
*/
|
|
5
|
+
import { setupBridgeLogger } from "./logger.ts";
|
|
6
|
+
import {
|
|
7
|
+
wireBridgeLifecycleReporter,
|
|
8
|
+
installConsoleEventHook,
|
|
9
|
+
reportRuntimeEvent,
|
|
10
|
+
} from "./runtime-event-reporter.ts";
|
|
11
|
+
|
|
12
|
+
function formatErr(err: unknown): string {
|
|
13
|
+
if (err instanceof Error) {
|
|
14
|
+
return `${err.message}\n${err.stack || ""}`.trim();
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(err);
|
|
18
|
+
} catch {
|
|
19
|
+
return String(err);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const logFilePath = setupBridgeLogger("weixin-bridge");
|
|
24
|
+
console.log("[weixin-bridge] log file:", logFilePath);
|
|
25
|
+
|
|
26
|
+
wireBridgeLifecycleReporter("weixin");
|
|
27
|
+
installConsoleEventHook("weixin");
|
|
28
|
+
|
|
29
|
+
process.on("uncaughtException", (err) => {
|
|
30
|
+
console.error("[weixin-bridge] uncaughtException:\n" + formatErr(err));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
process.on("unhandledRejection", (reason) => {
|
|
34
|
+
console.error("[weixin-bridge] unhandledRejection:\n" + formatErr(reason));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
(globalThis as any).__IM_RUNTIME_EVENT_REPORTER__ = (payload: {
|
|
38
|
+
accountId: string;
|
|
39
|
+
linkStatus:
|
|
40
|
+
| "connecting"
|
|
41
|
+
| "connected"
|
|
42
|
+
| "degraded"
|
|
43
|
+
| "disconnected"
|
|
44
|
+
| "error";
|
|
45
|
+
lastError?: string | null;
|
|
46
|
+
}) => {
|
|
47
|
+
return reportRuntimeEvent(
|
|
48
|
+
"weixin",
|
|
49
|
+
payload.accountId,
|
|
50
|
+
payload.linkStatus,
|
|
51
|
+
payload.lastError || null,
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await import("../scripts/weixin-stdio-bridge.ts");
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error(
|
|
59
|
+
"[weixin-bridge] failed to load/start script:\n" + formatErr(err),
|
|
60
|
+
);
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylib-syim",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "多 IM / 多 Agent 的会话路由与上下文管理(支持 /new)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -38,13 +38,14 @@
|
|
|
38
38
|
"build:bridge:all": "node scripts/build-bridge.mjs --all",
|
|
39
39
|
"build:bridge:debug": "node scripts/build-bridge.mjs --no-minify",
|
|
40
40
|
"start:stdio-bridge:bundle": "node dist/dingtalk-stdio-bridge.cjs",
|
|
41
|
-
"local": "export RESTART_ALL_EXIT_PROCESS=\"0\" && export RUNTIME_CONFIG_PULL_URL=\"http://127.0.0.1:
|
|
42
|
-
"remote": "export RESTART_ALL_EXIT_PROCESS=\"0\" && export RUNTIME_CONFIG_PULL_URL=\"http://127.0.0.1:
|
|
41
|
+
"local": "export RESTART_ALL_EXIT_PROCESS=\"0\" && export RUNTIME_CONFIG_PULL_URL=\"http://127.0.0.1:4999/api/v1/yucegpt/im/runtime/config/full\" && export ENABLE_INTERNAL_API=\"1\" && export INTERNAL_CONTROL_PORT=\"18999\" && export INTERNAL_FIXED_TOKEN=\"syim_runtime\" && export LOCAL_CONFIG_ONLY=1 && npm run start:bridges",
|
|
42
|
+
"remote": "export RESTART_ALL_EXIT_PROCESS=\"0\" && export RUNTIME_CONFIG_PULL_URL=\"http://127.0.0.1:4999/api/v1/yucegpt/im/runtime/config/full\" && export ENABLE_INTERNAL_API=\"1\" && export INTERNAL_CONTROL_PORT=\"18999\" && export INTERNAL_FIXED_TOKEN=\"syim_runtime\" && npm run start:bridges"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
46
|
-
"ylib-dingtalk-connector": "0.7.10-beata.
|
|
47
|
-
"ylib-openclaw-lark": "2026.3.17-beata.
|
|
46
|
+
"ylib-dingtalk-connector": "0.7.10-beata.9",
|
|
47
|
+
"ylib-openclaw-lark": "2026.3.17-beata.13",
|
|
48
|
+
"ylib-openclaw-weixin": "2.1.7",
|
|
48
49
|
"axios": "^1.6.0",
|
|
49
50
|
"dingtalk-stream": "^2.1.4",
|
|
50
51
|
"fluent-ffmpeg": "^2.1.3",
|
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const CHANNEL_KEY = "openclaw-weixin";
|
|
6
|
+
|
|
7
|
+
type GatewayMethodHandler = (args: unknown) => Promise<unknown> | unknown;
|
|
8
|
+
|
|
9
|
+
type WeixinGatewayInvokePayload = {
|
|
10
|
+
method: string;
|
|
11
|
+
params?: Record<string, unknown>;
|
|
12
|
+
accountId?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type WeixinGatewayInvokeResult = {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
result?: unknown;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ResolvedAccount = {
|
|
22
|
+
accountId: string;
|
|
23
|
+
config: Record<string, unknown>;
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type WeixinBridgeControl = {
|
|
28
|
+
stop: () => Promise<void>;
|
|
29
|
+
restart: () => Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function emitRuntimeEvent(
|
|
33
|
+
accountId: string,
|
|
34
|
+
linkStatus:
|
|
35
|
+
| "connecting"
|
|
36
|
+
| "connected"
|
|
37
|
+
| "degraded"
|
|
38
|
+
| "disconnected"
|
|
39
|
+
| "error",
|
|
40
|
+
lastError: string | null = null,
|
|
41
|
+
): void {
|
|
42
|
+
try {
|
|
43
|
+
const reporter = (globalThis as any).__IM_RUNTIME_EVENT_REPORTER__ as
|
|
44
|
+
| ((payload: {
|
|
45
|
+
accountId: string;
|
|
46
|
+
linkStatus: string;
|
|
47
|
+
lastError?: string | null;
|
|
48
|
+
}) => Promise<unknown>)
|
|
49
|
+
| undefined;
|
|
50
|
+
if (!reporter) return;
|
|
51
|
+
void reporter({ accountId, linkStatus, lastError });
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toDetailedErrorText(err: unknown): string {
|
|
58
|
+
if (!err) return "unknown error";
|
|
59
|
+
const asAny = err as any;
|
|
60
|
+
const message = String(asAny?.message || err || "unknown error").trim();
|
|
61
|
+
const responseData = asAny?.response?.data;
|
|
62
|
+
const responseStatus = asAny?.response?.status;
|
|
63
|
+
const responseStatusText = asAny?.response?.statusText;
|
|
64
|
+
const code = asAny?.code;
|
|
65
|
+
|
|
66
|
+
const details: string[] = [];
|
|
67
|
+
if (code) details.push(`code=${String(code)}`);
|
|
68
|
+
if (responseStatus) details.push(`status=${String(responseStatus)}`);
|
|
69
|
+
if (responseStatusText)
|
|
70
|
+
details.push(`statusText=${String(responseStatusText)}`);
|
|
71
|
+
if (responseData !== undefined) {
|
|
72
|
+
try {
|
|
73
|
+
details.push(`response=${JSON.stringify(responseData)}`);
|
|
74
|
+
} catch {
|
|
75
|
+
details.push(`response=${String(responseData)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const out =
|
|
80
|
+
details.length > 0 ? `${message}; ${details.join("; ")}` : message;
|
|
81
|
+
return out.slice(0, 1200);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getProjectRoot(): string {
|
|
85
|
+
const main = process.argv[1];
|
|
86
|
+
if (main) {
|
|
87
|
+
const resolved = path.resolve(main);
|
|
88
|
+
const base = path.basename(resolved);
|
|
89
|
+
if (
|
|
90
|
+
base === "weixin-stdio-bridge.ts" ||
|
|
91
|
+
base === "weixin-stdio-bridge.cjs" ||
|
|
92
|
+
base === "weixin-stdio-bridge.mjs"
|
|
93
|
+
) {
|
|
94
|
+
return path.join(path.dirname(resolved), "..");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return process.cwd();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function firstStringInAccounts(
|
|
101
|
+
accounts: Record<string, unknown> | undefined,
|
|
102
|
+
field: string,
|
|
103
|
+
): string {
|
|
104
|
+
if (!accounts || typeof accounts !== "object") return "";
|
|
105
|
+
for (const acc of Object.values(accounts)) {
|
|
106
|
+
if (!acc || typeof acc !== "object") continue;
|
|
107
|
+
const value = (acc as Record<string, unknown>)[field];
|
|
108
|
+
if (typeof value === "string" && value.trim()) {
|
|
109
|
+
return value.trim();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return "";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadOpenClawConfig(): Record<string, unknown> | null {
|
|
116
|
+
const runtimeConfigPath = String(
|
|
117
|
+
(globalThis as Record<string, unknown>).__IM_RUNTIME_CONFIG_PATH__ || "",
|
|
118
|
+
).trim();
|
|
119
|
+
const configPaths = [
|
|
120
|
+
runtimeConfigPath,
|
|
121
|
+
path.join(getProjectRoot(), "syim.json"),
|
|
122
|
+
path.join(os.homedir(), ".syim", "syim.json"),
|
|
123
|
+
].filter(Boolean);
|
|
124
|
+
|
|
125
|
+
for (const configPath of configPaths) {
|
|
126
|
+
if (!fs.existsSync(configPath)) continue;
|
|
127
|
+
try {
|
|
128
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
129
|
+
const config = JSON.parse(content) as Record<string, unknown>;
|
|
130
|
+
console.log("[weixin-stdio-bridge] config loaded:", configPath);
|
|
131
|
+
return config;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.warn(
|
|
134
|
+
"[weixin-stdio-bridge] failed to read config:",
|
|
135
|
+
configPath,
|
|
136
|
+
(err as Error).message,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
console.log("[weixin-stdio-bridge] no syim.json found, fallback to env");
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildCfg(): {
|
|
145
|
+
cfg: Record<string, unknown>;
|
|
146
|
+
gatewayBaseUrl: string;
|
|
147
|
+
gatewayToken: string;
|
|
148
|
+
} {
|
|
149
|
+
const raw = loadOpenClawConfig();
|
|
150
|
+
const channelConfig = (raw?.channels as Record<string, unknown>)?.[
|
|
151
|
+
CHANNEL_KEY
|
|
152
|
+
] as Record<string, unknown> | undefined;
|
|
153
|
+
|
|
154
|
+
const envBase = (
|
|
155
|
+
process.env.WEIXIN_GATEWAY_URL ||
|
|
156
|
+
process.env.GATEWAY_URL ||
|
|
157
|
+
""
|
|
158
|
+
).trim();
|
|
159
|
+
const accountsMap = channelConfig?.accounts as
|
|
160
|
+
| Record<string, unknown>
|
|
161
|
+
| undefined;
|
|
162
|
+
const fileBase =
|
|
163
|
+
(channelConfig?.gatewayBaseUrl as string)?.trim() ||
|
|
164
|
+
firstStringInAccounts(accountsMap, "gatewayBaseUrl") ||
|
|
165
|
+
"";
|
|
166
|
+
const gatewayBaseUrl = (envBase || fileBase).replace(/\/+$/, "");
|
|
167
|
+
|
|
168
|
+
const gatewayToken = (
|
|
169
|
+
process.env.WEIXIN_GATEWAY_TOKEN ||
|
|
170
|
+
process.env.GATEWAY_TOKEN ||
|
|
171
|
+
process.env.OPENCLAW_GATEWAY_TOKEN ||
|
|
172
|
+
process.env.OPENCLAW_GATEWAY_API_KEY ||
|
|
173
|
+
(channelConfig?.gatewayToken as string)?.trim() ||
|
|
174
|
+
firstStringInAccounts(accountsMap, "gatewayToken") ||
|
|
175
|
+
""
|
|
176
|
+
).trim();
|
|
177
|
+
|
|
178
|
+
let cfg: Record<string, unknown>;
|
|
179
|
+
if (channelConfig) {
|
|
180
|
+
const base = { ...channelConfig } as Record<string, unknown>;
|
|
181
|
+
const accounts = base.accounts as
|
|
182
|
+
| Record<string, Record<string, unknown>>
|
|
183
|
+
| undefined;
|
|
184
|
+
delete base.accounts;
|
|
185
|
+
if (gatewayBaseUrl) {
|
|
186
|
+
base.gatewayBaseUrl = gatewayBaseUrl;
|
|
187
|
+
}
|
|
188
|
+
if (gatewayToken) base.gatewayToken = gatewayToken;
|
|
189
|
+
|
|
190
|
+
if (accounts && typeof accounts === "object") {
|
|
191
|
+
const merged: Record<string, Record<string, unknown>> = {};
|
|
192
|
+
for (const [id, acc] of Object.entries(accounts)) {
|
|
193
|
+
if (!acc || typeof acc !== "object") continue;
|
|
194
|
+
const accObj = acc as Record<string, unknown>;
|
|
195
|
+
merged[id] = {
|
|
196
|
+
...base,
|
|
197
|
+
...accObj,
|
|
198
|
+
...(!accObj.gatewayBaseUrl ? { gatewayBaseUrl } : {}),
|
|
199
|
+
...(!accObj.gatewayToken && gatewayToken ? { gatewayToken } : {}),
|
|
200
|
+
};
|
|
201
|
+
if (!gatewayBaseUrl) {
|
|
202
|
+
delete merged[id].gatewayBaseUrl;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
cfg = {
|
|
206
|
+
channels: {
|
|
207
|
+
[CHANNEL_KEY]: {
|
|
208
|
+
...base,
|
|
209
|
+
accounts: merged,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
...(raw?.bindings ? { bindings: raw.bindings } : {}),
|
|
213
|
+
};
|
|
214
|
+
} else {
|
|
215
|
+
cfg = {
|
|
216
|
+
channels: {
|
|
217
|
+
[CHANNEL_KEY]: base,
|
|
218
|
+
},
|
|
219
|
+
...(raw?.bindings ? { bindings: raw.bindings } : {}),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
cfg = {
|
|
224
|
+
channels: {
|
|
225
|
+
[CHANNEL_KEY]: {
|
|
226
|
+
...(gatewayBaseUrl ? { gatewayBaseUrl } : {}),
|
|
227
|
+
gatewayToken: gatewayToken || undefined,
|
|
228
|
+
modelName: "main",
|
|
229
|
+
agentId: "main",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(
|
|
236
|
+
"[weixin-stdio-bridge] gatewayBaseUrl =",
|
|
237
|
+
gatewayBaseUrl || "<absent>",
|
|
238
|
+
);
|
|
239
|
+
console.log(
|
|
240
|
+
"[weixin-stdio-bridge] gatewayToken =",
|
|
241
|
+
gatewayToken ? "<present>" : "<absent>",
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return { cfg, gatewayBaseUrl, gatewayToken };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildMinimalRuntime(
|
|
248
|
+
cfg: Record<string, unknown>,
|
|
249
|
+
): Record<string, unknown> {
|
|
250
|
+
return {
|
|
251
|
+
version: process.env.OPENCLAW_HOST_VERSION || "2026.3.22",
|
|
252
|
+
log: (...args: unknown[]) => console.log("[Weixin]", ...args),
|
|
253
|
+
error: (...args: unknown[]) => console.error("[Weixin][ERR]", ...args),
|
|
254
|
+
exit: (code: number) => process.exit(code),
|
|
255
|
+
gateway: { port: 0 },
|
|
256
|
+
config: {
|
|
257
|
+
loadConfig: () => cfg,
|
|
258
|
+
featureFlags: {},
|
|
259
|
+
},
|
|
260
|
+
channel: {
|
|
261
|
+
commands: {
|
|
262
|
+
shouldComputeCommandAuthorized: () => false,
|
|
263
|
+
resolveCommandAuthorizedFromAuthorizers: async () => false,
|
|
264
|
+
isControlCommandMessage: () => false,
|
|
265
|
+
},
|
|
266
|
+
routing: {
|
|
267
|
+
resolveAgentRoute: ({ accountId, peer }: any) => {
|
|
268
|
+
const peerId = String(peer?.id || "").trim() || "unknown";
|
|
269
|
+
const account =
|
|
270
|
+
String(accountId || "__default__").trim() || "__default__";
|
|
271
|
+
const sessionKey = `weixin:${account}:${peerId}`;
|
|
272
|
+
return {
|
|
273
|
+
agentId: "main",
|
|
274
|
+
sessionKey,
|
|
275
|
+
mainSessionKey: sessionKey,
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
session: {
|
|
280
|
+
resolveStorePath: () => "",
|
|
281
|
+
recordInboundSession: async () => {},
|
|
282
|
+
},
|
|
283
|
+
reply: {
|
|
284
|
+
finalizeInboundContext: (ctx: unknown) => ctx,
|
|
285
|
+
resolveHumanDelayConfig: () => null,
|
|
286
|
+
createReplyDispatcherWithTyping: () => ({
|
|
287
|
+
dispatcher: {
|
|
288
|
+
sendFinalReply: () => false,
|
|
289
|
+
sendBlockReply: () => false,
|
|
290
|
+
sendToolResult: () => false,
|
|
291
|
+
waitForIdle: async () => {},
|
|
292
|
+
getQueuedCounts: () => ({}),
|
|
293
|
+
markComplete: () => {},
|
|
294
|
+
},
|
|
295
|
+
replyOptions: {},
|
|
296
|
+
markDispatchIdle: () => {},
|
|
297
|
+
}),
|
|
298
|
+
withReplyDispatcher: async ({
|
|
299
|
+
run,
|
|
300
|
+
}: {
|
|
301
|
+
run: () => Promise<unknown>;
|
|
302
|
+
}) => {
|
|
303
|
+
await run();
|
|
304
|
+
},
|
|
305
|
+
dispatchReplyFromConfig: async () => {},
|
|
306
|
+
},
|
|
307
|
+
activity: {
|
|
308
|
+
record: () => {},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
system: {
|
|
312
|
+
enqueueSystemEvent: () => {},
|
|
313
|
+
},
|
|
314
|
+
messages: {
|
|
315
|
+
groupChat: { historyLimit: 0 },
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const gatewayMethodHandlers = new Map<string, GatewayMethodHandler>();
|
|
321
|
+
let bridgeRuntimeForGateway: Record<string, unknown> | null = null;
|
|
322
|
+
const bridgeGatewayLog = {
|
|
323
|
+
info: (msg: string) => console.log("[Weixin][GatewayMethod]", msg),
|
|
324
|
+
warn: (msg: string) => console.warn("[Weixin][GatewayMethod]", msg),
|
|
325
|
+
error: (msg: string) => console.error("[Weixin][GatewayMethod]", msg),
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const activeAbortControllers = new Map<string, AbortController>();
|
|
329
|
+
let bridgeCfg: Record<string, unknown> | null = null;
|
|
330
|
+
let bridgeGatewayBaseUrl = "";
|
|
331
|
+
let bridgeGatewayToken = "";
|
|
332
|
+
let bridgeStartAccount: ((ctx: unknown) => Promise<unknown>) | null = null;
|
|
333
|
+
let bridgeListAccountIds: ((cfg: unknown) => string[]) | undefined;
|
|
334
|
+
let bridgeResolveAccount: ((cfg: unknown, id?: string) => unknown) | undefined;
|
|
335
|
+
let bridgeIsConfigured: ((a: unknown) => boolean) | undefined;
|
|
336
|
+
let bridgeRestarting = false;
|
|
337
|
+
|
|
338
|
+
async function invokeGatewayMethod(
|
|
339
|
+
payload: WeixinGatewayInvokePayload,
|
|
340
|
+
): Promise<WeixinGatewayInvokeResult> {
|
|
341
|
+
if (!bridgeCfg) {
|
|
342
|
+
return { ok: false, error: "weixin bridge config not ready" };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const method = String(payload?.method || "").trim();
|
|
346
|
+
if (!method) {
|
|
347
|
+
return { ok: false, error: "method is required" };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const handler = gatewayMethodHandlers.get(method);
|
|
351
|
+
if (!handler) {
|
|
352
|
+
return { ok: false, error: `gateway method not registered: ${method}` };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const params =
|
|
356
|
+
payload?.params && typeof payload.params === "object" ? payload.params : {};
|
|
357
|
+
const accountId = String(
|
|
358
|
+
(params as Record<string, unknown>).accountId || payload?.accountId || "",
|
|
359
|
+
).trim();
|
|
360
|
+
|
|
361
|
+
let hasResponded = false;
|
|
362
|
+
let responseOk = false;
|
|
363
|
+
let responseData: unknown = undefined;
|
|
364
|
+
const respond = (ok: boolean, data?: unknown) => {
|
|
365
|
+
hasResponded = true;
|
|
366
|
+
responseOk = Boolean(ok);
|
|
367
|
+
responseData = data;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
await Promise.resolve(
|
|
372
|
+
handler({
|
|
373
|
+
respond,
|
|
374
|
+
cfg: bridgeCfg,
|
|
375
|
+
params,
|
|
376
|
+
accountId: accountId || undefined,
|
|
377
|
+
log: bridgeGatewayLog,
|
|
378
|
+
runtime: bridgeRuntimeForGateway,
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
if (hasResponded) {
|
|
383
|
+
if (responseOk) {
|
|
384
|
+
return { ok: true, result: responseData };
|
|
385
|
+
}
|
|
386
|
+
const errText =
|
|
387
|
+
responseData && typeof responseData === "object"
|
|
388
|
+
? String(
|
|
389
|
+
(responseData as Record<string, unknown>).error ||
|
|
390
|
+
"gateway method call failed",
|
|
391
|
+
)
|
|
392
|
+
: "gateway method call failed";
|
|
393
|
+
return { ok: false, error: errText, result: responseData };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return { ok: true, result: {} };
|
|
397
|
+
} catch (err) {
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
error: toDetailedErrorText(err),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function sleep(ms: number): Promise<void> {
|
|
406
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function stopAllAccounts(reason: string): Promise<void> {
|
|
410
|
+
const entries = Array.from(activeAbortControllers.entries());
|
|
411
|
+
activeAbortControllers.clear();
|
|
412
|
+
for (const [accountId, controller] of entries) {
|
|
413
|
+
try {
|
|
414
|
+
controller.abort();
|
|
415
|
+
emitRuntimeEvent(accountId, "disconnected", reason);
|
|
416
|
+
} catch (err) {
|
|
417
|
+
const detail = toDetailedErrorText(err);
|
|
418
|
+
console.warn(
|
|
419
|
+
`[weixin-stdio-bridge] stop account failed accountId=${accountId} err=${detail}`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function startConfiguredAccounts(): Promise<void> {
|
|
426
|
+
if (!bridgeCfg || !bridgeStartAccount) return;
|
|
427
|
+
const cfg = bridgeCfg;
|
|
428
|
+
const startAccount = bridgeStartAccount;
|
|
429
|
+
const listAccountIds = bridgeListAccountIds;
|
|
430
|
+
const resolveAccount = bridgeResolveAccount;
|
|
431
|
+
const isConfigured = bridgeIsConfigured;
|
|
432
|
+
|
|
433
|
+
const accountIds = listAccountIds ? listAccountIds(cfg) : ["__default__"];
|
|
434
|
+
if (accountIds.length === 0) {
|
|
435
|
+
console.warn("[weixin-stdio-bridge] no account found");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const log = {
|
|
440
|
+
info: (msg: string) => console.log("[Weixin]", msg),
|
|
441
|
+
warn: (msg: string) => console.warn("[Weixin]", msg),
|
|
442
|
+
error: (msg: string) => console.error("[Weixin][ERR]", msg),
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const cfgChannels = (cfg as Record<string, unknown>).channels as
|
|
446
|
+
| Record<string, unknown>
|
|
447
|
+
| undefined;
|
|
448
|
+
const defaultAccount: ResolvedAccount = {
|
|
449
|
+
accountId: "__default__",
|
|
450
|
+
config: (cfgChannels?.[CHANNEL_KEY] as Record<string, unknown>) ?? {},
|
|
451
|
+
enabled: true,
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
let startedCount = 0;
|
|
455
|
+
for (const accountId of accountIds) {
|
|
456
|
+
const account: ResolvedAccount = (
|
|
457
|
+
resolveAccount ? resolveAccount(cfg, accountId) : defaultAccount
|
|
458
|
+
) as ResolvedAccount;
|
|
459
|
+
|
|
460
|
+
if (!account?.enabled) {
|
|
461
|
+
emitRuntimeEvent(accountId, "disconnected", "account disabled");
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (isConfigured && !isConfigured(account)) {
|
|
465
|
+
emitRuntimeEvent(accountId, "error", "account not configured");
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const accConfig = (account as { config?: Record<string, unknown> }).config;
|
|
470
|
+
if (accConfig && typeof accConfig === "object") {
|
|
471
|
+
if (!accConfig.gatewayBaseUrl && bridgeGatewayBaseUrl)
|
|
472
|
+
accConfig.gatewayBaseUrl = bridgeGatewayBaseUrl;
|
|
473
|
+
if (bridgeGatewayToken && !accConfig.gatewayToken) {
|
|
474
|
+
accConfig.gatewayToken = bridgeGatewayToken;
|
|
475
|
+
}
|
|
476
|
+
if (!accConfig.botId) {
|
|
477
|
+
accConfig.botId = account.accountId;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
emitRuntimeEvent(account.accountId, "connecting", null);
|
|
482
|
+
const abort = new AbortController();
|
|
483
|
+
activeAbortControllers.set(account.accountId, abort);
|
|
484
|
+
startAccount({
|
|
485
|
+
cfg,
|
|
486
|
+
accountId: account.accountId,
|
|
487
|
+
account,
|
|
488
|
+
abortSignal: abort.signal,
|
|
489
|
+
log,
|
|
490
|
+
runtime: log,
|
|
491
|
+
}).catch((err: unknown) => {
|
|
492
|
+
const detail = toDetailedErrorText(err);
|
|
493
|
+
log.error(`[${account.accountId}] start failed: ${detail}`);
|
|
494
|
+
emitRuntimeEvent(account.accountId, "error", detail);
|
|
495
|
+
});
|
|
496
|
+
emitRuntimeEvent(account.accountId, "connected", null);
|
|
497
|
+
startedCount++;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log(
|
|
501
|
+
`[weixin-stdio-bridge] started accounts=${startedCount} total=${accountIds.length}`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function importWeixinModule(): Promise<Record<string, unknown>> {
|
|
506
|
+
try {
|
|
507
|
+
const weixinPkg = "ylib-openclaw-weixin";
|
|
508
|
+
return (await import(weixinPkg)) as Record<string, unknown>;
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.warn(
|
|
511
|
+
`[weixin-stdio-bridge] import ylib-openclaw-weixin failed, fallback to local source: ${String(err)}`,
|
|
512
|
+
);
|
|
513
|
+
return (await import("../../openclaw-weixin/index.ts")) as Record<
|
|
514
|
+
string,
|
|
515
|
+
unknown
|
|
516
|
+
>;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function main(): Promise<void> {
|
|
521
|
+
const { cfg, gatewayBaseUrl, gatewayToken } = buildCfg();
|
|
522
|
+
|
|
523
|
+
const channels = (cfg as Record<string, unknown>).channels as
|
|
524
|
+
| Record<string, unknown>
|
|
525
|
+
| undefined;
|
|
526
|
+
const channelEntry = channels?.[CHANNEL_KEY];
|
|
527
|
+
if (channelEntry && typeof channelEntry === "object") {
|
|
528
|
+
if (gatewayBaseUrl) {
|
|
529
|
+
(channelEntry as Record<string, unknown>).gatewayBaseUrl = gatewayBaseUrl;
|
|
530
|
+
}
|
|
531
|
+
if (gatewayToken) {
|
|
532
|
+
(channelEntry as Record<string, unknown>).gatewayToken = gatewayToken;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const runtime = buildMinimalRuntime(cfg);
|
|
537
|
+
const pluginModule = await importWeixinModule();
|
|
538
|
+
const plugin = pluginModule.default as
|
|
539
|
+
| {
|
|
540
|
+
register?: (api: unknown) => void;
|
|
541
|
+
}
|
|
542
|
+
| undefined;
|
|
543
|
+
|
|
544
|
+
if (typeof plugin?.register !== "function") {
|
|
545
|
+
console.error(
|
|
546
|
+
JSON.stringify({ error: "openclaw-weixin default.register is missing" }),
|
|
547
|
+
);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let registeredChannelPlugin: Record<string, unknown> | null = null;
|
|
552
|
+
bridgeRuntimeForGateway = runtime;
|
|
553
|
+
|
|
554
|
+
plugin.register({
|
|
555
|
+
runtime,
|
|
556
|
+
registerChannel: ({ plugin: channelPlugin }: { plugin: unknown }) => {
|
|
557
|
+
if (channelPlugin && typeof channelPlugin === "object") {
|
|
558
|
+
registeredChannelPlugin = channelPlugin as Record<string, unknown>;
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
registerGatewayMethod: (method: string, handler: GatewayMethodHandler) => {
|
|
562
|
+
const methodName = String(method || "").trim();
|
|
563
|
+
if (!methodName || typeof handler !== "function") return;
|
|
564
|
+
gatewayMethodHandlers.set(methodName, handler);
|
|
565
|
+
console.log(
|
|
566
|
+
`[weixin-stdio-bridge] gateway method registered: ${methodName}`,
|
|
567
|
+
);
|
|
568
|
+
},
|
|
569
|
+
registerTool: () => {},
|
|
570
|
+
registerCommand: () => {},
|
|
571
|
+
registerCli: () => {},
|
|
572
|
+
on: () => {},
|
|
573
|
+
config: cfg,
|
|
574
|
+
logger: {
|
|
575
|
+
info: (...args: unknown[]) => console.log("[Weixin]", ...args),
|
|
576
|
+
warn: (...args: unknown[]) => console.warn("[Weixin]", ...args),
|
|
577
|
+
error: (...args: unknown[]) => console.error("[Weixin][ERR]", ...args),
|
|
578
|
+
debug: () => {},
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
if (!registeredChannelPlugin) {
|
|
583
|
+
console.error(
|
|
584
|
+
JSON.stringify({
|
|
585
|
+
error: "openclaw-weixin channel plugin not registered",
|
|
586
|
+
}),
|
|
587
|
+
);
|
|
588
|
+
process.exit(1);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const channelPlugin = registeredChannelPlugin as Record<string, unknown>;
|
|
593
|
+
|
|
594
|
+
const configApi = (channelPlugin["config"] as Record<string, unknown>) || {};
|
|
595
|
+
const gatewayApi =
|
|
596
|
+
(channelPlugin["gateway"] as Record<string, unknown>) || {};
|
|
597
|
+
|
|
598
|
+
const listAccountIds = configApi.listAccountIds as
|
|
599
|
+
| ((cfg: unknown) => string[])
|
|
600
|
+
| undefined;
|
|
601
|
+
const resolveAccount = configApi.resolveAccount as
|
|
602
|
+
| ((cfg: unknown, accountId?: string) => unknown)
|
|
603
|
+
| undefined;
|
|
604
|
+
const isConfigured = configApi.isConfigured as
|
|
605
|
+
| ((a: unknown) => boolean)
|
|
606
|
+
| undefined;
|
|
607
|
+
const startAccount = gatewayApi.startAccount as
|
|
608
|
+
| ((ctx: unknown) => Promise<unknown>)
|
|
609
|
+
| undefined;
|
|
610
|
+
|
|
611
|
+
if (typeof startAccount !== "function") {
|
|
612
|
+
console.error(
|
|
613
|
+
JSON.stringify({
|
|
614
|
+
error: "openclaw-weixin gateway.startAccount is missing",
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
bridgeCfg = cfg;
|
|
621
|
+
bridgeGatewayBaseUrl = gatewayBaseUrl;
|
|
622
|
+
bridgeGatewayToken = gatewayToken;
|
|
623
|
+
bridgeStartAccount = startAccount;
|
|
624
|
+
bridgeListAccountIds = listAccountIds;
|
|
625
|
+
bridgeResolveAccount = resolveAccount;
|
|
626
|
+
bridgeIsConfigured = isConfigured;
|
|
627
|
+
|
|
628
|
+
const control: WeixinBridgeControl = {
|
|
629
|
+
stop: async () => {
|
|
630
|
+
await stopAllAccounts("manual_stop");
|
|
631
|
+
},
|
|
632
|
+
restart: async () => {
|
|
633
|
+
if (bridgeRestarting) return;
|
|
634
|
+
bridgeRestarting = true;
|
|
635
|
+
try {
|
|
636
|
+
const latest = buildCfg();
|
|
637
|
+
bridgeCfg = latest.cfg;
|
|
638
|
+
bridgeGatewayBaseUrl = latest.gatewayBaseUrl;
|
|
639
|
+
bridgeGatewayToken = latest.gatewayToken;
|
|
640
|
+
await stopAllAccounts("soft_restart");
|
|
641
|
+
await sleep(250);
|
|
642
|
+
await startConfiguredAccounts();
|
|
643
|
+
} finally {
|
|
644
|
+
bridgeRestarting = false;
|
|
645
|
+
}
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
(globalThis as Record<string, unknown>).__IM_WEIXIN_BRIDGE_CONTROL__ =
|
|
650
|
+
control;
|
|
651
|
+
(
|
|
652
|
+
globalThis as Record<string, unknown>
|
|
653
|
+
).__IM_WEIXIN_BRIDGE_INVOKE_GATEWAY_METHOD__ = invokeGatewayMethod;
|
|
654
|
+
|
|
655
|
+
await startConfiguredAccounts();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
main().catch((err) => {
|
|
659
|
+
const detail = toDetailedErrorText(err);
|
|
660
|
+
emitRuntimeEvent("__default__", "error", detail);
|
|
661
|
+
console.error("[weixin-stdio-bridge]", err);
|
|
662
|
+
process.exit(1);
|
|
663
|
+
});
|