ylib-syim 0.0.8 → 0.0.10
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 +152 -30
- package/install-ylib.sh +28 -0
- package/package.json +12 -5
- package/scripts/dingtalk-stdio-bridge.ts +100 -1
- package/scripts/lark-stdio-bridge.ts +110 -5
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 = {
|
|
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(
|
|
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(
|
|
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")
|
|
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
|
|
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 || "")
|
|
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 (
|
|
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 (
|
|
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
|
|
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/install-ylib.sh
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
NODE_OPTIONS="--max-old-space-size=512"
|
|
5
|
+
|
|
6
|
+
echo "清理缓存..."
|
|
7
|
+
npm cache clean --force
|
|
8
|
+
rm -rf ~/.npm/_cacache
|
|
9
|
+
|
|
10
|
+
echo "设置优化配置..."
|
|
11
|
+
npm config set maxsockets 1
|
|
12
|
+
npm config set fetch-retry-mintimeout 20000
|
|
13
|
+
npm config set fetch-retry-maxtimeout 120000
|
|
14
|
+
npm config set fund false
|
|
15
|
+
npm config set audit false
|
|
16
|
+
npm config set loglevel=verbose
|
|
17
|
+
|
|
18
|
+
echo "开始安装..."
|
|
19
|
+
# 使用最小化安装
|
|
20
|
+
npm i ylib-syim -g \
|
|
21
|
+
--registry=https://registry.npmmirror.com \
|
|
22
|
+
--ignore-engines \
|
|
23
|
+
--no-optional \
|
|
24
|
+
--no-audit \
|
|
25
|
+
--no-fund \
|
|
26
|
+
--prefer-offline \
|
|
27
|
+
--legacy-peer-deps \
|
|
28
|
+
--verbose
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylib-syim",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "多 IM / 多 Agent 的会话路由与上下文管理(支持 /new)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"scripts",
|
|
19
19
|
"README.md",
|
|
20
20
|
"syim.json.bak",
|
|
21
|
-
"ecosystem.config.cjs"
|
|
21
|
+
"ecosystem.config.cjs",
|
|
22
|
+
"install-ylib.sh"
|
|
22
23
|
],
|
|
23
24
|
"sideEffects": false,
|
|
24
25
|
"scripts": {
|
|
@@ -37,12 +38,13 @@
|
|
|
37
38
|
"build:bridge:all": "node scripts/build-bridge.mjs --all",
|
|
38
39
|
"build:bridge:debug": "node scripts/build-bridge.mjs --no-minify",
|
|
39
40
|
"start:stdio-bridge:bundle": "node dist/dingtalk-stdio-bridge.cjs",
|
|
40
|
-
"local": "export LOCAL_CONFIG_ONLY=1 && npm run start:bridges"
|
|
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
43
|
},
|
|
42
44
|
"dependencies": {
|
|
43
45
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
44
|
-
"ylib-dingtalk-connector": "0.7.10-beata.
|
|
45
|
-
"ylib-openclaw-lark": "2026.3.17-beata.
|
|
46
|
+
"ylib-dingtalk-connector": "0.7.10-beata.5",
|
|
47
|
+
"ylib-openclaw-lark": "2026.3.17-beata.9",
|
|
46
48
|
"axios": "^1.6.0",
|
|
47
49
|
"dingtalk-stream": "^2.1.4",
|
|
48
50
|
"fluent-ffmpeg": "^2.1.3",
|
|
@@ -51,6 +53,11 @@
|
|
|
51
53
|
"pdf-parse": "^1.1.1",
|
|
52
54
|
"tsx": "^4.21.0"
|
|
53
55
|
},
|
|
56
|
+
"overrides": {
|
|
57
|
+
"@hono/node-server": "^1.19.10",
|
|
58
|
+
"@xmldom/xmldom": "^0.8.12",
|
|
59
|
+
"tar": "^7.5.11"
|
|
60
|
+
},
|
|
54
61
|
"devDependencies": {
|
|
55
62
|
"esbuild": "^0.27.4"
|
|
56
63
|
}
|
|
@@ -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: {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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 =",
|