ylib-syim 0.0.9 → 0.0.11

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
@@ -61,7 +61,10 @@ type RuntimeBotStatus = {
61
61
  };
62
62
 
63
63
  const runtimeStatusRegistry = new Map<string, RuntimeBotStatus>();
64
- let loadedConfigForRuntime: Record<string, unknown> | null = { "channels": {}, "bindings": [] } as { channels: Record<string, unknown>; bindings: any[] };;
64
+ let loadedConfigForRuntime: Record<string, unknown> | null = {
65
+ channels: {},
66
+ bindings: [],
67
+ } as { channels: Record<string, unknown>; bindings: any[] };
65
68
  let loadedConfigPathForRuntime: string | null = null;
66
69
  let configVersionHash = "";
67
70
  let configVersionUpdatedAt = nowIso();
@@ -74,6 +77,14 @@ let larkBridgeStarted = false;
74
77
  let larkBridgeStarting = false;
75
78
  let pluginTempHomeDir: string | null = null;
76
79
 
80
+ type RuntimeGatewayChannel = "dingtalk" | "feishu";
81
+
82
+ type RuntimeGatewayInvokeResult = {
83
+ ok: boolean;
84
+ error?: string;
85
+ result?: unknown;
86
+ };
87
+
77
88
  function pluginHomeConfigPayload(
78
89
  cfg: Record<string, unknown>,
79
90
  ): Record<string, unknown> {
@@ -109,7 +120,9 @@ function applyRemoteRuntimeConfigToPluginHome(): void {
109
120
  if (!fs.existsSync(syimDir)) {
110
121
  fs.mkdirSync(syimDir, { recursive: true });
111
122
  } else {
112
- console.log(`[bridges/main] plugin temp HOME syim dir exists: ${syimDir}`);
123
+ console.log(
124
+ `[bridges/main] plugin temp HOME syim dir exists: ${syimDir}`,
125
+ );
113
126
  }
114
127
  fs.writeFileSync(syimPath, payload, "utf-8");
115
128
  const openclawDir = path.join(pluginTempHomeDir, ".openclaw");
@@ -117,11 +130,14 @@ function applyRemoteRuntimeConfigToPluginHome(): void {
117
130
  if (!fs.existsSync(openclawDir)) {
118
131
  fs.mkdirSync(openclawDir, { recursive: true });
119
132
  } else {
120
- console.log(`[bridges/main] plugin temp HOME openclaw dir exists: ${openclawDir}`);
133
+ console.log(
134
+ `[bridges/main] plugin temp HOME openclaw dir exists: ${openclawDir}`,
135
+ );
121
136
  }
122
137
  fs.writeFileSync(openclawPath, payload, "utf-8");
123
138
  process.env.HOME = pluginTempHomeDir;
124
- if (process.platform === "win32") process.env.USERPROFILE = pluginTempHomeDir;
139
+ if (process.platform === "win32")
140
+ process.env.USERPROFILE = pluginTempHomeDir;
125
141
  console.log(
126
142
  `[bridges/main] plugin temp HOME ready home=${pluginTempHomeDir} syim=${syimPath} openclaw=${openclawPath}`,
127
143
  );
@@ -584,6 +600,65 @@ async function readRequestJson(
584
600
  });
585
601
  }
586
602
 
603
+ function normalizeGatewayChannel(input: unknown): RuntimeGatewayChannel | null {
604
+ const raw = String(input || "")
605
+ .trim()
606
+ .toLowerCase();
607
+ if (raw === "dingtalk" || raw === "dingtalk-connector") return "dingtalk";
608
+ if (raw === "feishu" || raw === "lark" || raw === "openclaw-lark") {
609
+ return "feishu";
610
+ }
611
+ return null;
612
+ }
613
+
614
+ async function invokeGatewayMethodByChannel(args: {
615
+ channel: RuntimeGatewayChannel;
616
+ method: string;
617
+ params?: Record<string, unknown>;
618
+ accountId?: string;
619
+ }): Promise<RuntimeGatewayInvokeResult> {
620
+ const method = String(args.method || "").trim();
621
+ if (!method) {
622
+ return { ok: false, error: "method is required" };
623
+ }
624
+
625
+ const invokerKey =
626
+ args.channel === "dingtalk"
627
+ ? "__IM_DINGTALK_BRIDGE_INVOKE_GATEWAY_METHOD__"
628
+ : "__IM_LARK_BRIDGE_INVOKE_GATEWAY_METHOD__";
629
+ const invoker = (globalThis as Record<string, unknown>)[invokerKey] as
630
+ | ((payload: {
631
+ method: string;
632
+ params?: Record<string, unknown>;
633
+ accountId?: string;
634
+ }) => Promise<RuntimeGatewayInvokeResult>)
635
+ | undefined;
636
+
637
+ if (typeof invoker !== "function") {
638
+ return {
639
+ ok: false,
640
+ error: `${args.channel} bridge gateway invoker not ready`,
641
+ };
642
+ }
643
+
644
+ try {
645
+ const result = await invoker({
646
+ method,
647
+ params: args.params || {},
648
+ accountId: args.accountId,
649
+ });
650
+ if (!result || typeof result !== "object") {
651
+ return { ok: false, error: "invalid gateway invoke response" };
652
+ }
653
+ return result;
654
+ } catch (err) {
655
+ return {
656
+ ok: false,
657
+ error: (err as Error).message || "gateway method invoke failed",
658
+ };
659
+ }
660
+ }
661
+
587
662
  async function probeAndRefreshStatuses(): Promise<void> {
588
663
  if (!loadedConfigForRuntime) return;
589
664
  const cfg = loadedConfigForRuntime;
@@ -615,6 +690,7 @@ async function probeAndRefreshStatuses(): Promise<void> {
615
690
  const probe = statusApi?.probe as
616
691
  | ((args: {
617
692
  cfg: Record<string, unknown>;
693
+ accountId: string;
618
694
  }) => Promise<Record<string, unknown>>)
619
695
  | undefined;
620
696
  const probeAccount = statusApi?.probeAccount as
@@ -623,7 +699,7 @@ async function probeAndRefreshStatuses(): Promise<void> {
623
699
  }) => Promise<Record<string, unknown>>)
624
700
  | undefined;
625
701
 
626
- if (listAccountIds && probe) {
702
+ if (listAccountIds) {
627
703
  const ids = configuredIds;
628
704
  if (resolveAccount && probeAccount) {
629
705
  for (const accountId of ids) {
@@ -690,15 +766,15 @@ async function probeAndRefreshStatuses(): Promise<void> {
690
766
  });
691
767
  }
692
768
  }
693
- } else {
694
- const probeResult = await probe({ cfg });
695
- const ok = Boolean(probeResult?.ok);
696
- if (!ok) {
697
- console.error(
698
- `[bridges/main] dingtalk probe failed raw=${toLogText(probeResult)}`,
699
- );
700
- }
769
+ } else if (probe) {
701
770
  for (const accountId of ids) {
771
+ const probeResult = await probe({ cfg, accountId });
772
+ const ok = Boolean(probeResult?.ok);
773
+ if (!ok) {
774
+ console.error(
775
+ `[bridges/main] dingtalk probe failed raw=${toLogText(probeResult)}`,
776
+ );
777
+ }
702
778
  const previous = runtimeStatusRegistry.get(
703
779
  keyOf("dingtalk", accountId),
704
780
  );
@@ -1354,10 +1430,7 @@ async function startInternalApiServer(): Promise<void> {
1354
1430
  );
1355
1431
  return;
1356
1432
  }
1357
- if (
1358
- req.method === "GET" &&
1359
- reqUrl.pathname === "/internal/runtime/logs"
1360
- ) {
1433
+ if (req.method === "GET" && reqUrl.pathname === "/internal/runtime/logs") {
1361
1434
  const limitRaw = String(reqUrl.searchParams.get("limit") || "100").trim();
1362
1435
  const requestedLimit = Number(limitRaw) || 100;
1363
1436
  const clampedLimit = Math.max(1, Math.min(1000, requestedLimit));
@@ -1397,6 +1470,51 @@ async function startInternalApiServer(): Promise<void> {
1397
1470
  );
1398
1471
  return;
1399
1472
  }
1473
+ if (
1474
+ req.method === "POST" &&
1475
+ (reqUrl.pathname === "/internal/runtime/gateway-method/invoke" ||
1476
+ reqUrl.pathname === "/internal/runtime/plugins/gateway-method/invoke")
1477
+ ) {
1478
+ const body = await readRequestJson(req);
1479
+ const channel = normalizeGatewayChannel(body.channel);
1480
+ const method = String(body.method || "").trim();
1481
+ const params =
1482
+ body.params &&
1483
+ typeof body.params === "object" &&
1484
+ !Array.isArray(body.params)
1485
+ ? (body.params as Record<string, unknown>)
1486
+ : {};
1487
+ const accountId = String(body.accountId || body.account_id || "").trim();
1488
+
1489
+ if (!channel || !method) {
1490
+ res.writeHead(400, { "Content-Type": "application/json" });
1491
+ res.end(
1492
+ JSON.stringify({
1493
+ ok: false,
1494
+ error: "channel and method are required",
1495
+ }),
1496
+ );
1497
+ return;
1498
+ }
1499
+
1500
+ const invokeResult = await invokeGatewayMethodByChannel({
1501
+ channel,
1502
+ method,
1503
+ params,
1504
+ accountId: accountId || undefined,
1505
+ });
1506
+
1507
+ res.writeHead(200, { "Content-Type": "application/json" });
1508
+ res.end(
1509
+ JSON.stringify({
1510
+ ...invokeResult,
1511
+ channel,
1512
+ method,
1513
+ runtime_instance_id: runtimeInstanceId,
1514
+ }),
1515
+ );
1516
+ return;
1517
+ }
1400
1518
  if (
1401
1519
  req.method === "POST" &&
1402
1520
  reqUrl.pathname === "/internal/runtime/config/reload"
@@ -1559,12 +1677,12 @@ async function startInternalApiServer(): Promise<void> {
1559
1677
  reqUrl.pathname === "/internal/runtime/restart-all"
1560
1678
  ) {
1561
1679
  const body = await readRequestJson(req);
1562
- const restartModeRaw = String(body.mode || "").trim().toLowerCase();
1680
+ const restartModeRaw = String(body.mode || "")
1681
+ .trim()
1682
+ .toLowerCase();
1563
1683
  const restartMode = restartModeRaw === "full" ? "full" : "soft";
1564
1684
  // 重启流程:显式 stop 标记 -> 拉取并落盘配置 -> 进入 connecting -> 等待状态收敛。
1565
- console.log(
1566
- `[bridges/main] restart-all requested mode=${restartMode}`,
1567
- );
1685
+ console.log(`[bridges/main] restart-all requested mode=${restartMode}`);
1568
1686
  const restartRequestAt = nowIso();
1569
1687
  console.log(
1570
1688
  `[bridges/main] restart-all step=enter at=${restartRequestAt} runtime_state=${runtimeState} instance=${runtimeInstanceId}`,
@@ -1655,9 +1773,7 @@ async function startInternalApiServer(): Promise<void> {
1655
1773
  );
1656
1774
  }
1657
1775
  if (loadedConfigForRuntime) {
1658
- console.log(
1659
- "[bridges/main] restart-all step=mark_connecting begin",
1660
- );
1776
+ console.log("[bridges/main] restart-all step=mark_connecting begin");
1661
1777
  markConfiguredBotsAsConnecting(loadedConfigForRuntime);
1662
1778
  console.log(
1663
1779
  `[bridges/main] restart-all step=mark_connecting done total=${summarizeBots().total}`,
@@ -1688,7 +1804,10 @@ async function startInternalApiServer(): Promise<void> {
1688
1804
  readChannelAccountCount("dingtalk-connector") > 0;
1689
1805
  const shouldSoftRestartLark =
1690
1806
  isChannelEnabled("feishu") && readChannelAccountCount("feishu") > 0;
1691
- if (typeof dingtalkControl?.restart === "function" && shouldSoftRestartDingtalk) {
1807
+ if (
1808
+ typeof dingtalkControl?.restart === "function" &&
1809
+ shouldSoftRestartDingtalk
1810
+ ) {
1692
1811
  console.log("[bridges/main] restart-all step=soft_restart dingtalk");
1693
1812
  bridgeSoftRestart.dingtalk.attempted = true;
1694
1813
  try {
@@ -1722,7 +1841,10 @@ async function startInternalApiServer(): Promise<void> {
1722
1841
  );
1723
1842
  }
1724
1843
  }
1725
- if (typeof larkControl?.restart === "function" && shouldSoftRestartLark) {
1844
+ if (
1845
+ typeof larkControl?.restart === "function" &&
1846
+ shouldSoftRestartLark
1847
+ ) {
1726
1848
  console.log("[bridges/main] restart-all step=soft_restart lark");
1727
1849
  bridgeSoftRestart.lark.attempted = true;
1728
1850
  try {
@@ -1766,7 +1888,9 @@ async function startInternalApiServer(): Promise<void> {
1766
1888
  );
1767
1889
  } catch (err) {
1768
1890
  const msg =
1769
- err instanceof Error ? err.message : "ensure bridges started failed";
1891
+ err instanceof Error
1892
+ ? err.message
1893
+ : "ensure bridges started failed";
1770
1894
  console.error(
1771
1895
  `[bridges/main] restart-all step=ensure_bridges_started failed err=${msg}`,
1772
1896
  );
@@ -2002,9 +2126,7 @@ function printConfigBootstrapGuide(): void {
2002
2126
  "- channels.feishu.accounts.<id>.appId/appSecret: 飞书应用凭据。",
2003
2127
  );
2004
2128
  console.log("- channels.feishu.accounts.<id>.gatewayBaseUrl: 网关地址。");
2005
- console.log(
2006
- "- channels.feishu.accounts.<id>.gatewayToken: 网关鉴权 token。",
2007
- );
2129
+ console.log("- channels.feishu.accounts.<id>.gatewayToken: 网关鉴权 token。");
2008
2130
  console.log(
2009
2131
  "- channels.feishu.accounts.<id>.dmPolicy/allowFrom: 私聊与来源策略。",
2010
2132
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ylib-syim",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "多 IM / 多 Agent 的会话路由与上下文管理(支持 /new)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -43,8 +43,8 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
46
- "ylib-dingtalk-connector": "0.7.10-beata.4",
47
- "ylib-openclaw-lark": "2026.3.17-beata.8",
46
+ "ylib-dingtalk-connector": "0.7.10-beata.6",
47
+ "ylib-openclaw-lark": "2026.3.17-beata.10",
48
48
  "axios": "^1.6.0",
49
49
  "dingtalk-stream": "^2.1.4",
50
50
  "fluent-ffmpeg": "^2.1.3",
@@ -16,6 +16,19 @@ import os from "node:os";
16
16
  import path from "node:path";
17
17
 
18
18
  const CHANNEL_KEY = "dingtalk-connector";
19
+ type GatewayMethodHandler = (args: unknown) => Promise<unknown> | unknown;
20
+
21
+ type DingtalkGatewayInvokePayload = {
22
+ method: string;
23
+ params?: Record<string, unknown>;
24
+ accountId?: string;
25
+ };
26
+
27
+ type DingtalkGatewayInvokeResult = {
28
+ ok: boolean;
29
+ error?: string;
30
+ result?: unknown;
31
+ };
19
32
 
20
33
  function emitRuntimeEvent(
21
34
  accountId: string,
@@ -299,6 +312,14 @@ type DingtalkBridgeControl = {
299
312
  restart: () => Promise<void>;
300
313
  };
301
314
 
315
+ const gatewayMethodHandlers = new Map<string, GatewayMethodHandler>();
316
+ let bridgeRuntimeForGateway: Record<string, unknown> | null = null;
317
+ const bridgeGatewayLog = {
318
+ info: (msg: string) => console.log("[DingTalk][GatewayMethod]", msg),
319
+ warn: (msg: string) => console.warn("[DingTalk][GatewayMethod]", msg),
320
+ error: (msg: string) => console.error("[DingTalk][GatewayMethod]", msg),
321
+ };
322
+
302
323
  const activeAbortControllers = new Map<string, AbortController>();
303
324
  let bridgeCfg: Record<string, unknown> | null = null;
304
325
  let bridgeGatewayBaseUrl = "";
@@ -309,6 +330,73 @@ let bridgeResolveAccount: ((cfg: unknown, id?: string) => unknown) | undefined;
309
330
  let bridgeIsConfigured: ((a: unknown) => boolean) | undefined;
310
331
  let bridgeRestarting = false;
311
332
 
333
+ async function invokeGatewayMethod(
334
+ payload: DingtalkGatewayInvokePayload,
335
+ ): Promise<DingtalkGatewayInvokeResult> {
336
+ if (!bridgeCfg) {
337
+ return { ok: false, error: "dingtalk bridge config not ready" };
338
+ }
339
+
340
+ const method = String(payload?.method || "").trim();
341
+ if (!method) {
342
+ return { ok: false, error: "method is required" };
343
+ }
344
+
345
+ const handler = gatewayMethodHandlers.get(method);
346
+ if (!handler) {
347
+ return { ok: false, error: `gateway method not registered: ${method}` };
348
+ }
349
+
350
+ const params =
351
+ payload?.params && typeof payload.params === "object" ? payload.params : {};
352
+ const accountId = String(
353
+ (params as Record<string, unknown>).accountId || payload?.accountId || "",
354
+ ).trim();
355
+
356
+ let hasResponded = false;
357
+ let responseOk = false;
358
+ let responseData: unknown = undefined;
359
+ const respond = (ok: boolean, data?: unknown) => {
360
+ hasResponded = true;
361
+ responseOk = Boolean(ok);
362
+ responseData = data;
363
+ };
364
+
365
+ try {
366
+ await Promise.resolve(
367
+ handler({
368
+ respond,
369
+ cfg: bridgeCfg,
370
+ params,
371
+ accountId: accountId || undefined,
372
+ log: bridgeGatewayLog,
373
+ runtime: bridgeRuntimeForGateway,
374
+ }),
375
+ );
376
+
377
+ if (hasResponded) {
378
+ if (responseOk) {
379
+ return { ok: true, result: responseData };
380
+ }
381
+ const errText =
382
+ responseData && typeof responseData === "object"
383
+ ? String(
384
+ (responseData as Record<string, unknown>).error ||
385
+ "gateway method call failed",
386
+ )
387
+ : "gateway method call failed";
388
+ return { ok: false, error: errText, result: responseData };
389
+ }
390
+
391
+ return { ok: true, result: {} };
392
+ } catch (err) {
393
+ return {
394
+ ok: false,
395
+ error: toDetailedErrorText(err),
396
+ };
397
+ }
398
+ }
399
+
312
400
  function sleep(ms: number): Promise<void> {
313
401
  return new Promise((resolve) => setTimeout(resolve, ms));
314
402
  }
@@ -448,10 +536,18 @@ async function main(): Promise<void> {
448
536
  );
449
537
  process.exit(1);
450
538
  }
539
+ bridgeRuntimeForGateway = runtime as Record<string, unknown>;
451
540
  plugin.register({
452
541
  runtime,
453
542
  registerChannel: () => {},
454
- registerGatewayMethod: () => {},
543
+ registerGatewayMethod: (method: string, handler: GatewayMethodHandler) => {
544
+ const methodName = String(method || "").trim();
545
+ if (!methodName || typeof handler !== "function") return;
546
+ gatewayMethodHandlers.set(methodName, handler);
547
+ console.log(
548
+ `[dingtalk-stdio-bridge] gateway method registered: ${methodName}`,
549
+ );
550
+ },
455
551
  });
456
552
  console.log(
457
553
  "[dingtalk-stdio-bridge] 插件与 connector 匹配: 已调用 plugin.register(api)",
@@ -502,6 +598,9 @@ async function main(): Promise<void> {
502
598
  };
503
599
  (globalThis as Record<string, unknown>).__IM_DINGTALK_BRIDGE_CONTROL__ =
504
600
  control;
601
+ (
602
+ globalThis as Record<string, unknown>
603
+ ).__IM_DINGTALK_BRIDGE_INVOKE_GATEWAY_METHOD__ = invokeGatewayMethod;
505
604
  await startConfiguredAccounts();
506
605
  }
507
606
 
@@ -25,6 +25,19 @@ import os from "node:os";
25
25
  import path from "node:path";
26
26
 
27
27
  const CHANNEL_KEY = "feishu";
28
+ type GatewayMethodHandler = (args: unknown) => Promise<unknown> | unknown;
29
+
30
+ type LarkGatewayInvokePayload = {
31
+ method: string;
32
+ params?: Record<string, unknown>;
33
+ accountId?: string;
34
+ };
35
+
36
+ type LarkGatewayInvokeResult = {
37
+ ok: boolean;
38
+ error?: string;
39
+ result?: unknown;
40
+ };
28
41
 
29
42
  function emitRuntimeEvent(
30
43
  accountId: string,
@@ -373,17 +386,95 @@ type LarkBridgeControl = {
373
386
  restart: () => Promise<void>;
374
387
  };
375
388
 
389
+ const gatewayMethodHandlers = new Map<string, GatewayMethodHandler>();
390
+ let gatewayRuntimeForMethods: unknown = null;
391
+ const gatewayMethodLog = {
392
+ info: (msg: string) => console.log("[Feishu][GatewayMethod]", msg),
393
+ warn: (msg: string) => console.warn("[Feishu][GatewayMethod]", msg),
394
+ error: (msg: string) => console.error("[Feishu][GatewayMethod]", msg),
395
+ };
396
+
376
397
  let currentAbortController: AbortController | null = null;
377
398
  let currentMonitorPromise: Promise<void> | null = null;
378
399
  let currentCfg: Record<string, unknown> | null = null;
379
400
  let currentGatewayBaseUrl = "";
380
401
  let currentAccountIds: string[] = [];
381
- let currentRuntime: { log: (...args: unknown[]) => void; error: (...args: unknown[]) => void; exit: (code: number) => void } | null = null;
382
- let currentMonitorFeishuProvider:
383
- | ((opts: unknown) => Promise<void>)
384
- | null = null;
402
+ let currentRuntime: {
403
+ log: (...args: unknown[]) => void;
404
+ error: (...args: unknown[]) => void;
405
+ exit: (code: number) => void;
406
+ } | null = null;
407
+ let currentMonitorFeishuProvider: ((opts: unknown) => Promise<void>) | null =
408
+ null;
385
409
  let bridgeRestarting = false;
386
410
 
411
+ async function invokeGatewayMethod(
412
+ payload: LarkGatewayInvokePayload,
413
+ ): Promise<LarkGatewayInvokeResult> {
414
+ if (!currentCfg) {
415
+ return { ok: false, error: "feishu bridge config not ready" };
416
+ }
417
+
418
+ const method = String(payload?.method || "").trim();
419
+ if (!method) {
420
+ return { ok: false, error: "method is required" };
421
+ }
422
+
423
+ const handler = gatewayMethodHandlers.get(method);
424
+ if (!handler) {
425
+ return { ok: false, error: `gateway method not registered: ${method}` };
426
+ }
427
+
428
+ const params =
429
+ payload?.params && typeof payload.params === "object" ? payload.params : {};
430
+ const accountId = String(
431
+ (params as Record<string, unknown>).accountId || payload?.accountId || "",
432
+ ).trim();
433
+
434
+ let hasResponded = false;
435
+ let responseOk = false;
436
+ let responseData: unknown = undefined;
437
+ const respond = (ok: boolean, data?: unknown) => {
438
+ hasResponded = true;
439
+ responseOk = Boolean(ok);
440
+ responseData = data;
441
+ };
442
+
443
+ try {
444
+ await Promise.resolve(
445
+ handler({
446
+ respond,
447
+ cfg: currentCfg,
448
+ params,
449
+ accountId: accountId || undefined,
450
+ runtime: gatewayRuntimeForMethods,
451
+ log: gatewayMethodLog,
452
+ }),
453
+ );
454
+
455
+ if (hasResponded) {
456
+ if (responseOk) {
457
+ return { ok: true, result: responseData };
458
+ }
459
+ const errText =
460
+ responseData && typeof responseData === "object"
461
+ ? String(
462
+ (responseData as Record<string, unknown>).error ||
463
+ "gateway method call failed",
464
+ )
465
+ : "gateway method call failed";
466
+ return { ok: false, error: errText, result: responseData };
467
+ }
468
+
469
+ return { ok: true, result: {} };
470
+ } catch (err) {
471
+ return {
472
+ ok: false,
473
+ error: toDetailedErrorText(err),
474
+ };
475
+ }
476
+ }
477
+
387
478
  async function stopMonitor(reason: string): Promise<void> {
388
479
  const controller = currentAbortController;
389
480
  currentAbortController = null;
@@ -460,6 +551,7 @@ async function main(): Promise<void> {
460
551
  larkModule as { default?: { register?: (api: unknown) => void } }
461
552
  ).default;
462
553
  const minimalRuntime = buildMinimalRuntime(cfg);
554
+ gatewayRuntimeForMethods = minimalRuntime;
463
555
 
464
556
  if (typeof plugin?.register === "function") {
465
557
  plugin.register({
@@ -468,7 +560,17 @@ async function main(): Promise<void> {
468
560
  registerChannel: () => {},
469
561
  registerTool: () => {},
470
562
  registerCommand: () => {},
471
- registerGatewayMethod: () => {},
563
+ registerGatewayMethod: (
564
+ method: string,
565
+ handler: GatewayMethodHandler,
566
+ ) => {
567
+ const methodName = String(method || "").trim();
568
+ if (!methodName || typeof handler !== "function") return;
569
+ gatewayMethodHandlers.set(methodName, handler);
570
+ console.log(
571
+ `[lark-stdio-bridge] gateway method registered: ${methodName}`,
572
+ );
573
+ },
472
574
  registerCli: () => {},
473
575
  on: () => {},
474
576
  config: cfg,
@@ -539,6 +641,9 @@ async function main(): Promise<void> {
539
641
  },
540
642
  };
541
643
  (globalThis as Record<string, unknown>).__IM_LARK_BRIDGE_CONTROL__ = control;
644
+ (
645
+ globalThis as Record<string, unknown>
646
+ ).__IM_LARK_BRIDGE_INVOKE_GATEWAY_METHOD__ = invokeGatewayMethod;
542
647
 
543
648
  console.log(
544
649
  "[lark-stdio-bridge] monitor started with gatewayBaseUrl =",