aws-runtime-bridge 1.9.66 → 1.9.71
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/README.md +17 -1
- package/dist/config.d.ts +8 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +56 -12
- package/dist/index.js +14 -7
- package/dist/routes/file-browser.d.ts.map +1 -1
- package/dist/routes/file-browser.js +7 -10
- package/dist/routes/instance.js +6 -3
- package/dist/services/auto-register.d.ts +25 -1
- package/dist/services/auto-register.d.ts.map +1 -1
- package/dist/services/auto-register.js +220 -33
- package/dist/services/cli-commands.d.ts.map +1 -1
- package/dist/services/cli-commands.js +1 -0
- package/dist/services/event-delta-pusher.js +1 -1
- package/dist/services/lifecycle-state.js +1 -1
- package/dist/services/mcp-launch-binding-queue.js +1 -1
- package/dist/services/orphan-monitor.d.ts +5 -0
- package/dist/services/orphan-monitor.d.ts.map +1 -1
- package/dist/services/orphan-monitor.js +48 -4
- package/dist/services/runtime-binding.d.ts +4 -5
- package/dist/services/runtime-binding.d.ts.map +1 -1
- package/dist/services/runtime-binding.js +14 -16
- package/dist/services/runtime-lifecycle-policy.d.ts +1 -0
- package/dist/services/runtime-lifecycle-policy.d.ts.map +1 -1
- package/dist/services/runtime-lifecycle-policy.js +1 -0
- package/dist/services/session-output.js +1 -1
- package/dist/services/startup-config-wizard.d.ts.map +1 -1
- package/dist/services/startup-config-wizard.js +10 -4
- package/dist/services/tunnel-client.d.ts +21 -0
- package/dist/services/tunnel-client.d.ts.map +1 -0
- package/dist/services/tunnel-client.js +398 -0
- package/dist/services/workspaceTaskChangeWatcher.js +1 -1
- package/dist/utils/file-utils.js +1 -1
- package/package.json +1 -1
|
@@ -117,15 +117,14 @@ function normalizeRuntimeTokenServerUrl(serverBaseUrl) {
|
|
|
117
117
|
}
|
|
118
118
|
return raw.replace(/\/+$/, "").toLowerCase();
|
|
119
119
|
}
|
|
120
|
-
export function buildRuntimeTokenKey(
|
|
121
|
-
const normalizedUserId = String(userId || "").trim();
|
|
120
|
+
export function buildRuntimeTokenKey(serverBaseUrl) {
|
|
122
121
|
const serverUrl = normalizeRuntimeTokenServerUrl(serverBaseUrl);
|
|
123
|
-
if (!
|
|
124
|
-
throw new Error("
|
|
122
|
+
if (!serverUrl) {
|
|
123
|
+
throw new Error("serverBaseUrl is required to build runtime token key");
|
|
125
124
|
}
|
|
126
125
|
return crypto
|
|
127
126
|
.createHash("md5")
|
|
128
|
-
.update(
|
|
127
|
+
.update(serverUrl, "utf8")
|
|
129
128
|
.digest("hex");
|
|
130
129
|
}
|
|
131
130
|
function loadRuntimeTokenStore() {
|
|
@@ -168,30 +167,30 @@ export function saveScopedRuntimeAccessToken(input) {
|
|
|
168
167
|
if (accessToken.length < 16) {
|
|
169
168
|
throw new Error("accessToken must be at least 16 characters");
|
|
170
169
|
}
|
|
171
|
-
const key = buildRuntimeTokenKey(input.
|
|
170
|
+
const key = buildRuntimeTokenKey(input.serverBaseUrl);
|
|
172
171
|
const tokens = loadRuntimeTokenStore();
|
|
173
172
|
tokens.set(key, accessToken);
|
|
174
173
|
saveRuntimeTokenStore(tokens);
|
|
175
174
|
return key;
|
|
176
175
|
}
|
|
177
|
-
export function getScopedRuntimeAccessToken(
|
|
178
|
-
if (!
|
|
176
|
+
export function getScopedRuntimeAccessToken(serverBaseUrl) {
|
|
177
|
+
if (!serverBaseUrl) {
|
|
179
178
|
return undefined;
|
|
180
179
|
}
|
|
181
180
|
try {
|
|
182
|
-
const key = buildRuntimeTokenKey(
|
|
181
|
+
const key = buildRuntimeTokenKey(serverBaseUrl);
|
|
183
182
|
return loadRuntimeTokenStore().get(key);
|
|
184
183
|
}
|
|
185
184
|
catch {
|
|
186
185
|
return undefined;
|
|
187
186
|
}
|
|
188
187
|
}
|
|
189
|
-
export function clearScopedRuntimeAccessToken(
|
|
190
|
-
if (!
|
|
188
|
+
export function clearScopedRuntimeAccessToken(serverBaseUrl) {
|
|
189
|
+
if (!serverBaseUrl) {
|
|
191
190
|
return false;
|
|
192
191
|
}
|
|
193
192
|
try {
|
|
194
|
-
const key = buildRuntimeTokenKey(
|
|
193
|
+
const key = buildRuntimeTokenKey(serverBaseUrl);
|
|
195
194
|
const tokens = loadRuntimeTokenStore();
|
|
196
195
|
const removed = tokens.delete(key);
|
|
197
196
|
if (removed) {
|
|
@@ -256,8 +255,8 @@ export function validateRuntimeBindingToken(token) {
|
|
|
256
255
|
}
|
|
257
256
|
return validateRuntimeTokenStore(candidate);
|
|
258
257
|
}
|
|
259
|
-
export function getRuntimeAccessToken(
|
|
260
|
-
const scopedToken = getScopedRuntimeAccessToken(
|
|
258
|
+
export function getRuntimeAccessToken(serverBaseUrl) {
|
|
259
|
+
const scopedToken = getScopedRuntimeAccessToken(serverBaseUrl);
|
|
261
260
|
if (scopedToken) {
|
|
262
261
|
return scopedToken;
|
|
263
262
|
}
|
|
@@ -306,9 +305,8 @@ export function saveRuntimeBinding(input) {
|
|
|
306
305
|
encoding: "utf8",
|
|
307
306
|
mode: 0o600,
|
|
308
307
|
});
|
|
309
|
-
if (next.
|
|
308
|
+
if (next.schedulerBaseUrl) {
|
|
310
309
|
const key = saveScopedRuntimeAccessToken({
|
|
311
|
-
userId: next.userId,
|
|
312
310
|
serverBaseUrl: next.schedulerBaseUrl,
|
|
313
311
|
accessToken,
|
|
314
312
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-lifecycle-policy.d.ts","sourceRoot":"","sources":["../../src/services/runtime-lifecycle-policy.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,OAAO,CAAC;IAC9B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,0BAA0B,EAAE,OAAO,CAAC;IACpC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,sBAAsB,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime-lifecycle-policy.d.ts","sourceRoot":"","sources":["../../src/services/runtime-lifecycle-policy.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,OAAO,CAAC;IAC9B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,0BAA0B,EAAE,OAAO,CAAC;IACpC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,sBAAsB,EAAE,OAAO,CAAC;IAChC,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,gBAAgB,EAAE,OAAO,GACxB,oBAAoB,CAWtB;AAED;;;;GAIG;AACH,wBAAgB,uCAAuC,CACrD,gBAAgB,EAAE,OAAO,EACzB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAMT;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAET"}
|
|
@@ -11,6 +11,7 @@ export function buildGracefulShutdownPlan(preserveSessions) {
|
|
|
11
11
|
terminateResidualProcesses: shouldCleanRuntimeState,
|
|
12
12
|
unregisterInstance: shouldCleanRuntimeState,
|
|
13
13
|
clearPersistedSessions: shouldCleanRuntimeState,
|
|
14
|
+
stopTunnelClient: shouldCleanRuntimeState,
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
@@ -20,7 +20,7 @@ import { getRuntimeAccessToken } from "./runtime-binding.js";
|
|
|
20
20
|
export const bridgeEventBuffer = new RingBuffer(2000);
|
|
21
21
|
function runtimeAuthHeaders(options, callbackBaseUrl = resolveRuntimeCallbackBaseUrl(options)) {
|
|
22
22
|
const token = options?.runtimeAccessToken ||
|
|
23
|
-
getRuntimeAccessToken(
|
|
23
|
+
getRuntimeAccessToken(callbackBaseUrl) ||
|
|
24
24
|
getRuntimeAccessToken();
|
|
25
25
|
return token ? { "X-Runtime-Token": token } : undefined;
|
|
26
26
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"startup-config-wizard.d.ts","sourceRoot":"","sources":["../../src/services/startup-config-wizard.ts"],"names":[],"mappings":"AAuBA,UAAU,QAAQ;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,UAAU,0BAA0B;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,yBAAyB,GACjC,QAAQ,GACR,SAAS,GACT,YAAY,GACZ,SAAS,GACT,iBAAiB,GACjB,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"startup-config-wizard.d.ts","sourceRoot":"","sources":["../../src/services/startup-config-wizard.ts"],"names":[],"mappings":"AAuBA,UAAU,QAAQ;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,IAAI,CAAC;CACf;AAED,UAAU,0BAA0B;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,qBAAqB,CAAC,EAAE,MAAM,MAAM,CAAC;CACtC;AAED,MAAM,MAAM,yBAAyB,GACjC,QAAQ,GACR,SAAS,GACT,YAAY,GACZ,SAAS,GACT,iBAAiB,GACjB,QAAQ,CAAC;AAgLb,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CAiDpC;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,yBAAyB,CAAC,CA0BpC"}
|
|
@@ -80,22 +80,28 @@ async function collectStartupConfig(prompt, existingConfig = {}) {
|
|
|
80
80
|
const existingTarget = existingConfig.autoRegisterTargets?.[0] || {};
|
|
81
81
|
const defaultServerUrl = "http://127.0.0.1:8080";
|
|
82
82
|
const defaultInstanceName = os.hostname() || "AgentsWork Runtime Bridge";
|
|
83
|
-
const
|
|
83
|
+
const defaultRegisterAddr = "127.0.0.1";
|
|
84
|
+
// 读取已有 registerAddr;若老配置只有 registerIp 则迁移过来
|
|
85
|
+
const existingRegisterAddr = existingTarget.registerAddr ||
|
|
86
|
+
existingTarget.registerIp ||
|
|
87
|
+
defaultRegisterAddr;
|
|
84
88
|
const connectionKey = await askRequired(prompt, "connectionKey(面板连接 Bridge 的密钥)", existingConfig.connectionKey);
|
|
85
89
|
const serverUrl = await askOptional(prompt, "serverUrl(调度中心地址)", existingTarget.serverUrl || defaultServerUrl);
|
|
86
90
|
const instanceName = await askOptional(prompt, "instanceName(实例名称)", existingTarget.instanceName || defaultInstanceName);
|
|
87
91
|
const userKey = await askRequired(prompt, "userKey(用户 API Key)", existingTarget.userKey);
|
|
88
|
-
const
|
|
92
|
+
const registerAddr = await askOptional(prompt, "registerAddr(Bridge 对外访问地址,支持 host:port / 纯 host / 完整 URL)", existingRegisterAddr);
|
|
93
|
+
// 显式剥离老 registerIp 字段,避免输出中两字段共存
|
|
94
|
+
const { registerIp: _legacyRegisterIp, ...targetWithoutRegisterIp } = existingTarget;
|
|
89
95
|
return {
|
|
90
96
|
...existingConfig,
|
|
91
97
|
connectionKey,
|
|
92
98
|
autoRegisterTargets: [
|
|
93
99
|
{
|
|
94
|
-
...
|
|
100
|
+
...targetWithoutRegisterIp,
|
|
95
101
|
serverUrl,
|
|
96
102
|
instanceName,
|
|
97
103
|
userKey,
|
|
98
|
-
|
|
104
|
+
registerAddr,
|
|
99
105
|
},
|
|
100
106
|
],
|
|
101
107
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge 反向隧道客户端
|
|
3
|
+
*
|
|
4
|
+
* 当 bridge 位于 NAT/内网(无公网),server 有公网时:bridge 注册虚拟隧道地址
|
|
5
|
+
*(形如 http(s)://server/tunnel/{instanceId}),注册成功后通过本客户端主动连出到
|
|
6
|
+
* server 的 /ws/bridge-tunnel,server 对 bridge 的 HTTP/WebSocket 调用均经此隧道转发。
|
|
7
|
+
*
|
|
8
|
+
* 多 server 支持:autoRegisterTargets 配置多个 server 时,每个 server 注册成功后
|
|
9
|
+
* 各自建立一条独立隧道,按归一化 serverUrl 为 key 并存于 tunnels Map,互不干扰;
|
|
10
|
+
* 同一 server 重复注册(重试/token 轮换)时先拆旧隧道再以最新 instanceId 重建。
|
|
11
|
+
*
|
|
12
|
+
* 协议帧格式见 BridgeTunnelService(server 端)。
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* 启动(或重启)到指定 server 的隧道。
|
|
16
|
+
* 同一 serverUrl 已存在隧道时先拆旧重建,不同 serverUrl 的隧道并存。
|
|
17
|
+
*/
|
|
18
|
+
export declare function startTunnelClient(serverUrl: string, instanceId: string): void;
|
|
19
|
+
/** 停止全部隧道(graceful shutdown 用) */
|
|
20
|
+
export declare function stopTunnelClient(): void;
|
|
21
|
+
//# sourceMappingURL=tunnel-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-client.d.ts","sourceRoot":"","sources":["../../src/services/tunnel-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAgGH;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAuB7E;AAED,kCAAkC;AAClC,wBAAgB,gBAAgB,IAAI,IAAI,CAMvC"}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge 反向隧道客户端
|
|
3
|
+
*
|
|
4
|
+
* 当 bridge 位于 NAT/内网(无公网),server 有公网时:bridge 注册虚拟隧道地址
|
|
5
|
+
*(形如 http(s)://server/tunnel/{instanceId}),注册成功后通过本客户端主动连出到
|
|
6
|
+
* server 的 /ws/bridge-tunnel,server 对 bridge 的 HTTP/WebSocket 调用均经此隧道转发。
|
|
7
|
+
*
|
|
8
|
+
* 多 server 支持:autoRegisterTargets 配置多个 server 时,每个 server 注册成功后
|
|
9
|
+
* 各自建立一条独立隧道,按归一化 serverUrl 为 key 并存于 tunnels Map,互不干扰;
|
|
10
|
+
* 同一 server 重复注册(重试/token 轮换)时先拆旧隧道再以最新 instanceId 重建。
|
|
11
|
+
*
|
|
12
|
+
* 协议帧格式见 BridgeTunnelService(server 端)。
|
|
13
|
+
*/
|
|
14
|
+
import axios, { AxiosError } from "axios";
|
|
15
|
+
import WebSocket from "ws";
|
|
16
|
+
import { getRuntimeAccessToken, normalizeSchedulerBaseUrl, } from "./runtime-binding.js";
|
|
17
|
+
import { loadConfigs, loadPrimaryLifecycleConfig } from "./auto-register.js";
|
|
18
|
+
import { logger } from "../utils/logger.js";
|
|
19
|
+
import { port } from "../config.js";
|
|
20
|
+
const PING_INTERVAL_MS = 15_000;
|
|
21
|
+
const RECONNECT_DELAY_MS = 5_000;
|
|
22
|
+
/** 本地 WS open 前的消息排队上限,防止异常情况下队列无限增长 */
|
|
23
|
+
const MAX_PENDING_WS_MESSAGES = 200;
|
|
24
|
+
/** server 端鉴权失败的关闭码(见 BridgeTunnelWebSocketHandler.CLOSE_AUTH_FAILED) */
|
|
25
|
+
const CLOSE_CODE_AUTH_FAILED = 4001;
|
|
26
|
+
/** 活跃隧道:key = 归一化 serverUrl,value = 该 server 的独立隧道状态 */
|
|
27
|
+
const tunnels = new Map();
|
|
28
|
+
function resolveTunnelEndpoint(state) {
|
|
29
|
+
const targetUrl = state.serverUrl;
|
|
30
|
+
const instanceId = state.instanceId;
|
|
31
|
+
if (!targetUrl || !instanceId) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const base = new URL(targetUrl);
|
|
36
|
+
const wsProto = base.protocol === "https:" ? "wss:" : "ws:";
|
|
37
|
+
const scopedToken = getRuntimeAccessToken(targetUrl);
|
|
38
|
+
// userKey 回退必须按触发注册的 server 匹配,避免多 target 时错用第一个 target 的凭据
|
|
39
|
+
const matchedConfig = loadConfigs().find((item) => normalizeSchedulerBaseUrl(item.serverUrl) === targetUrl) || loadPrimaryLifecycleConfig();
|
|
40
|
+
const runtimeAccessToken = scopedToken || matchedConfig.userKey;
|
|
41
|
+
if (scopedToken) {
|
|
42
|
+
logger.debug("[TunnelClient] 使用 runtime access token 连接隧道");
|
|
43
|
+
}
|
|
44
|
+
else if (matchedConfig.userKey) {
|
|
45
|
+
logger.warn("[TunnelClient] getRuntimeAccessToken 返回空,回退到 userKey(非 runtime token,鉴权可能失败): targetUrl=", targetUrl);
|
|
46
|
+
}
|
|
47
|
+
if (!runtimeAccessToken) {
|
|
48
|
+
logger.warn("[TunnelClient] 缺少 runtime token,无法建立隧道: targetUrl=", targetUrl);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// token 走 X-Runtime-Token 握手头,不进 URL,避免写入 server access log
|
|
52
|
+
const query = new URLSearchParams({ instanceId });
|
|
53
|
+
return {
|
|
54
|
+
url: `${wsProto}//${base.host}/ws/bridge-tunnel?${query.toString()}`,
|
|
55
|
+
token: runtimeAccessToken,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
logger.warn("[TunnelClient] 无法解析 serverUrl:", String(e));
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 启动(或重启)到指定 server 的隧道。
|
|
65
|
+
* 同一 serverUrl 已存在隧道时先拆旧重建,不同 serverUrl 的隧道并存。
|
|
66
|
+
*/
|
|
67
|
+
export function startTunnelClient(serverUrl, instanceId) {
|
|
68
|
+
const normalized = normalizeSchedulerBaseUrl(serverUrl);
|
|
69
|
+
if (!normalized) {
|
|
70
|
+
logger.warn("[TunnelClient] serverUrl 无效,无法启动隧道:", serverUrl);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// 同一 server 已有隧道:先拆掉,避免重复注册导致同 server 多连接并存、
|
|
74
|
+
// 旧连接的 close 回调误清掉新连接的本地流
|
|
75
|
+
const existing = tunnels.get(normalized);
|
|
76
|
+
if (existing) {
|
|
77
|
+
existing.shuttingDown = true;
|
|
78
|
+
teardownConnection(existing);
|
|
79
|
+
tunnels.delete(normalized);
|
|
80
|
+
}
|
|
81
|
+
const state = {
|
|
82
|
+
serverUrl: normalized,
|
|
83
|
+
instanceId,
|
|
84
|
+
streams: new Map(),
|
|
85
|
+
shuttingDown: false,
|
|
86
|
+
};
|
|
87
|
+
tunnels.set(normalized, state);
|
|
88
|
+
connect(state);
|
|
89
|
+
}
|
|
90
|
+
/** 停止全部隧道(graceful shutdown 用) */
|
|
91
|
+
export function stopTunnelClient() {
|
|
92
|
+
for (const state of tunnels.values()) {
|
|
93
|
+
state.shuttingDown = true;
|
|
94
|
+
teardownConnection(state);
|
|
95
|
+
}
|
|
96
|
+
tunnels.clear();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 拆除指定隧道连接:清定时器、关闭全部本地流、关闭隧道 WS。
|
|
100
|
+
* 关闭前移除隧道 WS 的事件监听,防止其 close/error 回调再次触发
|
|
101
|
+
* cleanup/scheduleReconnect 干扰后续新连接。
|
|
102
|
+
*/
|
|
103
|
+
function teardownConnection(state) {
|
|
104
|
+
if (state.timer) {
|
|
105
|
+
clearInterval(state.timer);
|
|
106
|
+
state.timer = undefined;
|
|
107
|
+
}
|
|
108
|
+
if (state.reconnectTimer) {
|
|
109
|
+
clearTimeout(state.reconnectTimer);
|
|
110
|
+
state.reconnectTimer = undefined;
|
|
111
|
+
}
|
|
112
|
+
for (const [id, stream] of state.streams.entries()) {
|
|
113
|
+
try {
|
|
114
|
+
stream.ws.close(1000, "tunnel client stopped");
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// ignore
|
|
118
|
+
}
|
|
119
|
+
state.streams.delete(id);
|
|
120
|
+
}
|
|
121
|
+
if (state.ws) {
|
|
122
|
+
state.ws.removeAllListeners();
|
|
123
|
+
try {
|
|
124
|
+
state.ws.close(1000, "tunnel client stopped");
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// ignore
|
|
128
|
+
}
|
|
129
|
+
state.ws = undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function connect(state) {
|
|
133
|
+
if (state.shuttingDown)
|
|
134
|
+
return;
|
|
135
|
+
const instanceId = state.instanceId;
|
|
136
|
+
if (!instanceId) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const endpoint = resolveTunnelEndpoint(state);
|
|
140
|
+
if (!endpoint) {
|
|
141
|
+
scheduleReconnect(state);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
logger.info("[TunnelClient] 连接隧道服务端:", endpoint.url);
|
|
145
|
+
const ws = new WebSocket(endpoint.url, {
|
|
146
|
+
handshakeTimeout: 15_000,
|
|
147
|
+
headers: { "X-Runtime-Token": endpoint.token },
|
|
148
|
+
});
|
|
149
|
+
state.ws = ws;
|
|
150
|
+
ws.on("open", () => {
|
|
151
|
+
logger.info("[TunnelClient] 隧道连接已建立: server=", state.serverUrl, "instanceId=", instanceId);
|
|
152
|
+
state.reconnectTimer && clearTimeout(state.reconnectTimer);
|
|
153
|
+
state.reconnectTimer = undefined;
|
|
154
|
+
state.timer = setInterval(() => {
|
|
155
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
156
|
+
ws.send(JSON.stringify({ t: "ping" }));
|
|
157
|
+
}
|
|
158
|
+
}, PING_INTERVAL_MS);
|
|
159
|
+
});
|
|
160
|
+
ws.on("unexpected-response", (req, res) => {
|
|
161
|
+
logger.error("[TunnelClient] 服务端拒绝连接(非 101 响应): server=", state.serverUrl, "statusCode=", res.statusCode, "statusMessage=", res.statusMessage);
|
|
162
|
+
});
|
|
163
|
+
ws.on("upgrade", (res) => {
|
|
164
|
+
logger.debug("[TunnelClient] 收到升级响应: statusCode=", res.statusCode);
|
|
165
|
+
});
|
|
166
|
+
ws.on("message", (raw) => {
|
|
167
|
+
let text;
|
|
168
|
+
if (typeof raw === "string") {
|
|
169
|
+
text = raw;
|
|
170
|
+
}
|
|
171
|
+
else if (Buffer.isBuffer(raw)) {
|
|
172
|
+
text = raw.toString("utf-8");
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
text = Buffer.from(raw).toString("utf-8");
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const msg = JSON.parse(text);
|
|
179
|
+
handleMessage(state, msg);
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
logger.warn("[TunnelClient] 解析隧道消息失败:", String(e));
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
ws.on("close", (code, reason) => {
|
|
186
|
+
logger.warn("[TunnelClient] 隧道连接关闭: server=", state.serverUrl, "code=", code, "reason=", String(reason));
|
|
187
|
+
cleanup(state);
|
|
188
|
+
if (code === CLOSE_CODE_AUTH_FAILED) {
|
|
189
|
+
// 鉴权失败:继续重连只会每 5s 刷一次鉴权失败日志。
|
|
190
|
+
// 停止自动重连,等下次注册成功(autoRegisterSingle)显式重启隧道。
|
|
191
|
+
logger.warn("[TunnelClient] 鉴权失败(4001),停止自动重连;待重新注册后再启动隧道: server=", state.serverUrl);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
scheduleReconnect(state);
|
|
195
|
+
});
|
|
196
|
+
ws.on("error", (err) => {
|
|
197
|
+
logger.warn("[TunnelClient] 隧道连接错误: server=", state.serverUrl, "err=", err.message, "code=", err.code);
|
|
198
|
+
cleanup(state);
|
|
199
|
+
scheduleReconnect(state);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function cleanup(state) {
|
|
203
|
+
if (state.timer) {
|
|
204
|
+
clearInterval(state.timer);
|
|
205
|
+
state.timer = undefined;
|
|
206
|
+
}
|
|
207
|
+
// 关闭本地 WS 流
|
|
208
|
+
for (const [id, stream] of state.streams.entries()) {
|
|
209
|
+
try {
|
|
210
|
+
stream.ws.close(1011, "tunnel disconnected");
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// ignore
|
|
214
|
+
}
|
|
215
|
+
state.streams.delete(id);
|
|
216
|
+
}
|
|
217
|
+
state.ws = undefined;
|
|
218
|
+
}
|
|
219
|
+
function scheduleReconnect(state) {
|
|
220
|
+
if (state.shuttingDown || state.reconnectTimer)
|
|
221
|
+
return;
|
|
222
|
+
state.reconnectTimer = setTimeout(() => {
|
|
223
|
+
state.reconnectTimer = undefined;
|
|
224
|
+
connect(state);
|
|
225
|
+
}, RECONNECT_DELAY_MS);
|
|
226
|
+
}
|
|
227
|
+
function handleMessage(state, msg) {
|
|
228
|
+
switch (msg.t) {
|
|
229
|
+
case "req":
|
|
230
|
+
handleHttpRequest(state, msg);
|
|
231
|
+
break;
|
|
232
|
+
case "wsopen":
|
|
233
|
+
handleWsOpen(state, msg);
|
|
234
|
+
break;
|
|
235
|
+
case "wsmsg":
|
|
236
|
+
handleWsMessage(state, msg);
|
|
237
|
+
break;
|
|
238
|
+
case "wsclose":
|
|
239
|
+
handleWsClose(state, msg);
|
|
240
|
+
break;
|
|
241
|
+
case "ping":
|
|
242
|
+
// server 端心跳探测,回复 pong 保持连接活跃
|
|
243
|
+
sendJson(state, { t: "pong" });
|
|
244
|
+
break;
|
|
245
|
+
case "pong":
|
|
246
|
+
break;
|
|
247
|
+
default:
|
|
248
|
+
logger.debug("[TunnelClient] 未知消息类型: t=", msg.t);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
async function handleHttpRequest(state, msg) {
|
|
252
|
+
const id = msg.id;
|
|
253
|
+
const method = msg.method || "GET";
|
|
254
|
+
const path = msg.path || "/";
|
|
255
|
+
const localUrl = `http://127.0.0.1:${port}${path}`;
|
|
256
|
+
try {
|
|
257
|
+
const bodyBuffer = msg.body ? Buffer.from(msg.body, "base64") : undefined;
|
|
258
|
+
const headers = {};
|
|
259
|
+
if (msg.headers) {
|
|
260
|
+
for (const [key, values] of Object.entries(msg.headers)) {
|
|
261
|
+
if (values && values.length > 0) {
|
|
262
|
+
headers[key] = values[0];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const response = await axios.request({
|
|
267
|
+
url: localUrl,
|
|
268
|
+
method,
|
|
269
|
+
headers,
|
|
270
|
+
data: bodyBuffer,
|
|
271
|
+
timeout: 120_000,
|
|
272
|
+
responseType: "arraybuffer",
|
|
273
|
+
validateStatus: () => true,
|
|
274
|
+
});
|
|
275
|
+
const respHeaders = {};
|
|
276
|
+
for (const [key, val] of Object.entries(response.headers)) {
|
|
277
|
+
if (val === undefined)
|
|
278
|
+
continue;
|
|
279
|
+
respHeaders[key] = Array.isArray(val) ? val : [String(val)];
|
|
280
|
+
}
|
|
281
|
+
const bodyBase64 = Buffer.isBuffer(response.data)
|
|
282
|
+
? response.data.toString("base64")
|
|
283
|
+
: "";
|
|
284
|
+
sendJson(state, {
|
|
285
|
+
t: "res",
|
|
286
|
+
id,
|
|
287
|
+
status: response.status,
|
|
288
|
+
headers: respHeaders,
|
|
289
|
+
body: bodyBase64 || null,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
catch (err) {
|
|
293
|
+
const errorMsg = err instanceof AxiosError ? err.message : String(err);
|
|
294
|
+
logger.warn("[TunnelClient] 本地转发失败:", method, path, "->", errorMsg);
|
|
295
|
+
sendJson(state, {
|
|
296
|
+
t: "res",
|
|
297
|
+
id,
|
|
298
|
+
status: 502,
|
|
299
|
+
headers: { "content-type": ["text/plain; charset=utf-8"] },
|
|
300
|
+
body: Buffer.from(errorMsg).toString("base64"),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function handleWsOpen(state, msg) {
|
|
305
|
+
const id = msg.id;
|
|
306
|
+
const path = msg.path || "/";
|
|
307
|
+
const localWsUrl = `ws://127.0.0.1:${port}${path}`;
|
|
308
|
+
let localWs;
|
|
309
|
+
try {
|
|
310
|
+
localWs = new WebSocket(localWsUrl, { handshakeTimeout: 10_000 });
|
|
311
|
+
}
|
|
312
|
+
catch (e) {
|
|
313
|
+
logger.warn("[TunnelClient] 本地 WS 连接失败:", String(e));
|
|
314
|
+
sendJson(state, { t: "wsclose", id, code: 1011, reason: "local ws connect failed" });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const stream = { ws: localWs, pending: [] };
|
|
318
|
+
state.streams.set(id, stream);
|
|
319
|
+
localWs.on("open", () => {
|
|
320
|
+
// 先 flush open 前排队消息(保持到达顺序),再通知 server 流已就绪
|
|
321
|
+
for (const item of stream.pending) {
|
|
322
|
+
sendToLocal(stream, item.data, item.bin);
|
|
323
|
+
}
|
|
324
|
+
stream.pending = [];
|
|
325
|
+
sendJson(state, { t: "wsopened", id });
|
|
326
|
+
});
|
|
327
|
+
localWs.on("message", (data, isBinary) => {
|
|
328
|
+
// ws 库通过第二参数 isBinary 区分文本/二进制帧;data 类型始终是 Buffer。
|
|
329
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
330
|
+
sendJson(state, {
|
|
331
|
+
t: "wsmsg",
|
|
332
|
+
id,
|
|
333
|
+
bin: isBinary,
|
|
334
|
+
d: buffer.toString("base64"),
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
localWs.on("close", (code, reason) => {
|
|
338
|
+
state.streams.delete(id);
|
|
339
|
+
sendJson(state, { t: "wsclose", id, code, reason: String(reason) });
|
|
340
|
+
});
|
|
341
|
+
localWs.on("error", (err) => {
|
|
342
|
+
logger.warn("[TunnelClient] 本地 WS 错误: id=", id, "err=", err.message);
|
|
343
|
+
state.streams.delete(id);
|
|
344
|
+
try {
|
|
345
|
+
localWs.close(1011, err.message);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// ignore
|
|
349
|
+
}
|
|
350
|
+
sendJson(state, { t: "wsclose", id, code: 1011, reason: err.message });
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function sendToLocal(stream, data, bin) {
|
|
354
|
+
if (bin) {
|
|
355
|
+
stream.ws.send(data);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
stream.ws.send(data.toString("utf-8"));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function handleWsMessage(state, msg) {
|
|
362
|
+
const stream = state.streams.get(msg.id);
|
|
363
|
+
if (!stream)
|
|
364
|
+
return;
|
|
365
|
+
const data = msg.d ? Buffer.from(msg.d, "base64") : Buffer.alloc(0);
|
|
366
|
+
if (stream.ws.readyState !== WebSocket.OPEN) {
|
|
367
|
+
// 本地 WS 尚未 open:排队等待,避免 PTY 打开瞬间的输入被丢弃
|
|
368
|
+
if (stream.pending.length >= MAX_PENDING_WS_MESSAGES) {
|
|
369
|
+
stream.pending.shift();
|
|
370
|
+
logger.warn("[TunnelClient] 本地 WS 未就绪,排队消息超上限,丢弃最旧一条: id=", msg.id);
|
|
371
|
+
}
|
|
372
|
+
stream.pending.push({ data, bin: Boolean(msg.bin) });
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
sendToLocal(stream, data, Boolean(msg.bin));
|
|
376
|
+
}
|
|
377
|
+
function handleWsClose(state, msg) {
|
|
378
|
+
const stream = state.streams.get(msg.id);
|
|
379
|
+
if (!stream)
|
|
380
|
+
return;
|
|
381
|
+
state.streams.delete(msg.id);
|
|
382
|
+
try {
|
|
383
|
+
stream.ws.close(msg.code ?? 1000, msg.reason || "");
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// ignore
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function sendJson(state, payload) {
|
|
390
|
+
if (!state.ws || state.ws.readyState !== WebSocket.OPEN)
|
|
391
|
+
return;
|
|
392
|
+
try {
|
|
393
|
+
state.ws.send(JSON.stringify(payload));
|
|
394
|
+
}
|
|
395
|
+
catch (e) {
|
|
396
|
+
logger.warn("[TunnelClient] sendJson 失败:", String(e));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
@@ -376,7 +376,7 @@ async function flushChange(fullPath) {
|
|
|
376
376
|
async function sendChangeRecord(ctx, record, cacheKey) {
|
|
377
377
|
try {
|
|
378
378
|
const callbackBaseUrl = resolveSchedulerBaseUrl().url;
|
|
379
|
-
const runtimeAccessToken = getRuntimeAccessToken(
|
|
379
|
+
const runtimeAccessToken = getRuntimeAccessToken(callbackBaseUrl) || getRuntimeAccessToken();
|
|
380
380
|
if (!runtimeAccessToken) {
|
|
381
381
|
log.warn('Cannot send change record: no auth token');
|
|
382
382
|
return;
|
package/dist/utils/file-utils.js
CHANGED
|
@@ -87,7 +87,7 @@ export async function downloadSkillZip(skillPackage, targetDir) {
|
|
|
87
87
|
? String(skillPackage.fileName).trim()
|
|
88
88
|
: 'skill-package.zip';
|
|
89
89
|
const outputPath = path.join(targetDir, fileName);
|
|
90
|
-
const runtimeAccessToken = getRuntimeAccessToken(
|
|
90
|
+
const runtimeAccessToken = getRuntimeAccessToken(schedulerBaseUrl) || getRuntimeAccessToken();
|
|
91
91
|
if (!runtimeAccessToken) {
|
|
92
92
|
throw new Error('runtime access token is required to download skill package');
|
|
93
93
|
}
|