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.
Files changed (34) hide show
  1. package/README.md +17 -1
  2. package/dist/config.d.ts +8 -3
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +56 -12
  5. package/dist/index.js +14 -7
  6. package/dist/routes/file-browser.d.ts.map +1 -1
  7. package/dist/routes/file-browser.js +7 -10
  8. package/dist/routes/instance.js +6 -3
  9. package/dist/services/auto-register.d.ts +25 -1
  10. package/dist/services/auto-register.d.ts.map +1 -1
  11. package/dist/services/auto-register.js +220 -33
  12. package/dist/services/cli-commands.d.ts.map +1 -1
  13. package/dist/services/cli-commands.js +1 -0
  14. package/dist/services/event-delta-pusher.js +1 -1
  15. package/dist/services/lifecycle-state.js +1 -1
  16. package/dist/services/mcp-launch-binding-queue.js +1 -1
  17. package/dist/services/orphan-monitor.d.ts +5 -0
  18. package/dist/services/orphan-monitor.d.ts.map +1 -1
  19. package/dist/services/orphan-monitor.js +48 -4
  20. package/dist/services/runtime-binding.d.ts +4 -5
  21. package/dist/services/runtime-binding.d.ts.map +1 -1
  22. package/dist/services/runtime-binding.js +14 -16
  23. package/dist/services/runtime-lifecycle-policy.d.ts +1 -0
  24. package/dist/services/runtime-lifecycle-policy.d.ts.map +1 -1
  25. package/dist/services/runtime-lifecycle-policy.js +1 -0
  26. package/dist/services/session-output.js +1 -1
  27. package/dist/services/startup-config-wizard.d.ts.map +1 -1
  28. package/dist/services/startup-config-wizard.js +10 -4
  29. package/dist/services/tunnel-client.d.ts +21 -0
  30. package/dist/services/tunnel-client.d.ts.map +1 -0
  31. package/dist/services/tunnel-client.js +398 -0
  32. package/dist/services/workspaceTaskChangeWatcher.js +1 -1
  33. package/dist/utils/file-utils.js +1 -1
  34. 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(userId, serverBaseUrl) {
121
- const normalizedUserId = String(userId || "").trim();
120
+ export function buildRuntimeTokenKey(serverBaseUrl) {
122
121
  const serverUrl = normalizeRuntimeTokenServerUrl(serverBaseUrl);
123
- if (!normalizedUserId || !serverUrl) {
124
- throw new Error("userId and serverBaseUrl are required to build runtime token key");
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(`${normalizedUserId}:${serverUrl}`, "utf8")
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.userId, input.serverBaseUrl);
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(userId, serverBaseUrl) {
178
- if (!userId || !serverBaseUrl) {
176
+ export function getScopedRuntimeAccessToken(serverBaseUrl) {
177
+ if (!serverBaseUrl) {
179
178
  return undefined;
180
179
  }
181
180
  try {
182
- const key = buildRuntimeTokenKey(userId, serverBaseUrl);
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(userId, serverBaseUrl) {
190
- if (!userId || !serverBaseUrl) {
188
+ export function clearScopedRuntimeAccessToken(serverBaseUrl) {
189
+ if (!serverBaseUrl) {
191
190
  return false;
192
191
  }
193
192
  try {
194
- const key = buildRuntimeTokenKey(userId, serverBaseUrl);
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(userId, serverBaseUrl) {
260
- const scopedToken = getScopedRuntimeAccessToken(userId, serverBaseUrl);
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.userId && next.schedulerBaseUrl) {
308
+ if (next.schedulerBaseUrl) {
310
309
  const key = saveScopedRuntimeAccessToken({
311
- userId: next.userId,
312
310
  serverBaseUrl: next.schedulerBaseUrl,
313
311
  accessToken,
314
312
  });
@@ -4,6 +4,7 @@ export interface GracefulShutdownPlan {
4
4
  terminateResidualProcesses: boolean;
5
5
  unregisterInstance: boolean;
6
6
  clearPersistedSessions: boolean;
7
+ stopTunnelClient: boolean;
7
8
  }
8
9
  /**
9
10
  * Build shutdown side-effect policy from the requested restart mode.
@@ -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;CACjC;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,gBAAgB,EAAE,OAAO,GACxB,oBAAoB,CAUtB;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"}
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(undefined, callbackBaseUrl) ||
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;AAoKb,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"}
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 defaultRegisterIp = "127.0.0.1";
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 registerIp = await askOptional(prompt, "registerIp(Bridge 对外访问 IP)", existingTarget.registerIp || defaultRegisterIp);
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
- ...existingTarget,
100
+ ...targetWithoutRegisterIp,
95
101
  serverUrl,
96
102
  instanceName,
97
103
  userKey,
98
- registerIp,
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(undefined, callbackBaseUrl) || 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;
@@ -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(undefined, schedulerBaseUrl) || 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-runtime-bridge",
3
- "version": "1.9.66",
3
+ "version": "1.9.71",
4
4
  "description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",