ylib-syim 0.0.13 → 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 +308 -20
- package/bridges/runtime-event-reporter.ts +7 -2
- package/bridges/weixin-stdio-bridge.ts +62 -0
- package/package.json +4 -3
- 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 {
|
|
@@ -1208,6 +1378,24 @@ function readEffectiveWhitelistFromConfig(
|
|
|
1208
1378
|
"project_id",
|
|
1209
1379
|
],
|
|
1210
1380
|
},
|
|
1381
|
+
"openclaw-weixin": {
|
|
1382
|
+
fields: [
|
|
1383
|
+
"enabled",
|
|
1384
|
+
"botId",
|
|
1385
|
+
"modelName",
|
|
1386
|
+
"agentId",
|
|
1387
|
+
"gatewayBaseUrl",
|
|
1388
|
+
"gatewayToken",
|
|
1389
|
+
"uploadHost",
|
|
1390
|
+
],
|
|
1391
|
+
runtimeReadonlyFields: [
|
|
1392
|
+
"scopeType",
|
|
1393
|
+
"projectId",
|
|
1394
|
+
"scope_type",
|
|
1395
|
+
"type",
|
|
1396
|
+
"project_id",
|
|
1397
|
+
],
|
|
1398
|
+
},
|
|
1211
1399
|
};
|
|
1212
1400
|
const filePath = process.env.EFFECTIVE_WHITELIST_FILE || "";
|
|
1213
1401
|
const fromRuntime = runtimeConfig?.effectiveWhitelist;
|
|
@@ -1243,7 +1431,9 @@ function normalizeRuntimeConfigByWhitelist(config: Record<string, unknown>): {
|
|
|
1243
1431
|
? "dingtalk-connector"
|
|
1244
1432
|
: channelKey === "feishu"
|
|
1245
1433
|
? "openclaw-lark"
|
|
1246
|
-
: ""
|
|
1434
|
+
: channelKey === "openclaw-weixin"
|
|
1435
|
+
? "openclaw-weixin"
|
|
1436
|
+
: "";
|
|
1247
1437
|
if (!pluginId) return new Set();
|
|
1248
1438
|
const pluginWhitelist = whitelist[pluginId] as Record<string, unknown>;
|
|
1249
1439
|
const fields = Array.isArray(pluginWhitelist?.fields)
|
|
@@ -1338,6 +1528,12 @@ function resolveRawSchema(
|
|
|
1338
1528
|
const schema = (cs?.schema as Record<string, unknown>) || {};
|
|
1339
1529
|
return schema;
|
|
1340
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
|
+
}
|
|
1341
1537
|
return {};
|
|
1342
1538
|
}
|
|
1343
1539
|
|
|
@@ -1402,6 +1598,41 @@ async function getPluginSchemas(): Promise<Array<Record<string, unknown>>> {
|
|
|
1402
1598
|
error: (err as Error).message,
|
|
1403
1599
|
});
|
|
1404
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
|
+
}
|
|
1405
1636
|
return result;
|
|
1406
1637
|
}
|
|
1407
1638
|
|
|
@@ -1820,6 +2051,11 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1820
2051
|
ok: false,
|
|
1821
2052
|
reason: "control_not_available",
|
|
1822
2053
|
},
|
|
2054
|
+
weixin: {
|
|
2055
|
+
attempted: false,
|
|
2056
|
+
ok: false,
|
|
2057
|
+
reason: "control_not_available",
|
|
2058
|
+
},
|
|
1823
2059
|
};
|
|
1824
2060
|
const dingtalkControl = (globalThis as Record<string, unknown>)
|
|
1825
2061
|
.__IM_DINGTALK_BRIDGE_CONTROL__ as
|
|
@@ -1829,11 +2065,18 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1829
2065
|
.__IM_LARK_BRIDGE_CONTROL__ as
|
|
1830
2066
|
| { restart?: () => Promise<void>; stop?: () => Promise<void> }
|
|
1831
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;
|
|
1832
2072
|
const shouldSoftRestartDingtalk =
|
|
1833
2073
|
isChannelEnabled("dingtalk-connector") &&
|
|
1834
2074
|
readChannelAccountCount("dingtalk-connector") > 0;
|
|
1835
2075
|
const shouldSoftRestartLark =
|
|
1836
2076
|
isChannelEnabled("feishu") && readChannelAccountCount("feishu") > 0;
|
|
2077
|
+
const shouldSoftRestartWeixin =
|
|
2078
|
+
isChannelEnabled("openclaw-weixin") &&
|
|
2079
|
+
readChannelAccountCount("openclaw-weixin") > 0;
|
|
1837
2080
|
if (
|
|
1838
2081
|
typeof dingtalkControl?.restart === "function" &&
|
|
1839
2082
|
shouldSoftRestartDingtalk
|
|
@@ -1908,6 +2151,43 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1908
2151
|
);
|
|
1909
2152
|
}
|
|
1910
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
|
+
}
|
|
1911
2191
|
console.log(
|
|
1912
2192
|
"[bridges/main] restart-all step=ensure_bridges_started begin",
|
|
1913
2193
|
);
|
|
@@ -1998,7 +2278,9 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1998
2278
|
const botAccountId = String(body.bot_account_id || "").trim();
|
|
1999
2279
|
const linkStatus = String(body.link_status || "").trim();
|
|
2000
2280
|
if (
|
|
2001
|
-
(platform !== "dingtalk" &&
|
|
2281
|
+
(platform !== "dingtalk" &&
|
|
2282
|
+
platform !== "feishu" &&
|
|
2283
|
+
platform !== "weixin") ||
|
|
2002
2284
|
!botAccountId ||
|
|
2003
2285
|
!linkStatus
|
|
2004
2286
|
) {
|
|
@@ -2006,7 +2288,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
2006
2288
|
res.end(JSON.stringify({ ok: false, error: "invalid payload" }));
|
|
2007
2289
|
return;
|
|
2008
2290
|
}
|
|
2009
|
-
const p = platform as "dingtalk" | "feishu";
|
|
2291
|
+
const p = platform as "dingtalk" | "feishu" | "weixin";
|
|
2010
2292
|
if (!isConfiguredRuntimeAccount(p, botAccountId)) {
|
|
2011
2293
|
console.log(
|
|
2012
2294
|
`[bridges/main] runtime event ignored: unconfigured account platform=${p} account=${botAccountId}`,
|
|
@@ -2072,16 +2354,22 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
2072
2354
|
/**
|
|
2073
2355
|
* 统一桥接入口。
|
|
2074
2356
|
*
|
|
2075
|
-
*
|
|
2357
|
+
* 默认同时启动钉钉、飞书、微信三个 bridge。
|
|
2076
2358
|
* 可通过参数控制:
|
|
2077
2359
|
* --only=dingtalk
|
|
2078
2360
|
* --only=lark
|
|
2361
|
+
* --only=weixin
|
|
2079
2362
|
* --only=all
|
|
2080
2363
|
*/
|
|
2081
|
-
function parseOnlyArg(): "all" | "dingtalk" | "lark" {
|
|
2364
|
+
function parseOnlyArg(): "all" | "dingtalk" | "lark" | "weixin" {
|
|
2082
2365
|
const onlyArg = process.argv.find((arg) => arg.startsWith("--only="));
|
|
2083
2366
|
const onlyValue = (onlyArg?.slice("--only=".length) || "all").toLowerCase();
|
|
2084
|
-
if (
|
|
2367
|
+
if (
|
|
2368
|
+
onlyValue === "dingtalk" ||
|
|
2369
|
+
onlyValue === "lark" ||
|
|
2370
|
+
onlyValue === "weixin" ||
|
|
2371
|
+
onlyValue === "all"
|
|
2372
|
+
) {
|
|
2085
2373
|
return onlyValue;
|
|
2086
2374
|
}
|
|
2087
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": {
|
|
@@ -43,8 +43,9 @@
|
|
|
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
|
+
});
|