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 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 {
@@ -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" && platform !== "feishu") ||
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
- * 默认同时启动钉钉和飞书两个 bridge。
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 (onlyValue === "dingtalk" || onlyValue === "lark" || onlyValue === "all") {
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 = 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.13",
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.8",
47
- "ylib-openclaw-lark": "2026.3.17-beata.12",
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
+ });