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 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(platform: "dingtalk" | "feishu", accountId: string): string {
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
- : ["lark-stdio-bridge", "[Feishu]", "feishu"];
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(platform: "dingtalk" | "feishu"): void {
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 = platform === "dingtalk" ? "dingtalk-connector" : "feishu";
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
- : "__IM_LARK_BRIDGE_INVOKE_GATEWAY_METHOD__";
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 invoker({
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
- "dmPolicy",
1165
- "allowFrom",
1334
+ "modelName",
1335
+ "agentId",
1166
1336
  "gatewayBaseUrl",
1167
1337
  "gatewayToken",
1168
- "separateSessionByConversation",
1169
- "groupSessionScope",
1170
1338
  "streaming",
1171
1339
  "uploadHost",
1172
- // "yuceImageCookieStrictOrigin",
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
- "gatewayPort",
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
- // "yuceImageCookieStrictOrigin",
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
- if (!Array.isArray(fields) || fields.length === 0) return new Set();
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
- fields
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" && platform !== "feishu") ||
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
- * 默认同时启动钉钉和飞书两个 bridge。
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 (onlyValue === "dingtalk" || onlyValue === "lark" || onlyValue === "all") {
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 = platform === "dingtalk" ? "dingtalk-connector" : "feishu";
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.12",
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:3999/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:3999/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"
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.7",
47
- "ylib-openclaw-lark": "2026.3.17-beata.11",
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
+ });