aws-runtime-bridge 1.9.120 → 1.9.124

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 (105) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +18 -0
  2. package/dist/adapter/AcodeSdkAdapter.js +5 -5
  3. package/dist/adapter/types.d.ts +4 -0
  4. package/dist/adapter/types.js +1 -1
  5. package/dist/browser/browser-ws-bridge.js +3 -3
  6. package/dist/browser/built-in-browser-tools.js +1 -1
  7. package/dist/desktop/built-in-desktop-tools.d.ts +49 -0
  8. package/dist/desktop/built-in-desktop-tools.js +2 -0
  9. package/dist/desktop/desktop-mcp-client.d.ts +47 -0
  10. package/dist/desktop/desktop-mcp-client.js +3 -0
  11. package/dist/index.js +1 -1
  12. package/dist/remote-desktop/index.js +2 -2
  13. package/dist/routes/dashboard.js +521 -17
  14. package/dist/routes/git.d.ts +39 -0
  15. package/dist/routes/git.js +33 -25
  16. package/dist/routes/instance.js +1 -1
  17. package/dist/routes/jdks.d.ts +2 -0
  18. package/dist/routes/jdks.js +2 -0
  19. package/dist/routes/launch.d.ts +6 -0
  20. package/dist/routes/launch.js +30 -0
  21. package/dist/routes/pty.js +3 -3
  22. package/dist/routes/runtime-binding.js +1 -1
  23. package/dist/routes/terminal.js +30 -30
  24. package/dist/services/access-control.d.ts +18 -0
  25. package/dist/services/access-control.js +5 -0
  26. package/dist/services/agent-process-manager.js +1 -1
  27. package/dist/services/auto-register.js +1 -1
  28. package/dist/services/change-cache-retention.d.ts +5 -0
  29. package/dist/services/change-cache-retention.js +2 -0
  30. package/dist/services/desktop-mcp-manager.d.ts +47 -0
  31. package/dist/services/desktop-mcp-manager.js +2 -0
  32. package/dist/services/instance-features.d.ts +6 -0
  33. package/dist/services/instance-features.js +1 -1
  34. package/dist/services/jdk-scanner.d.ts +20 -0
  35. package/dist/services/jdk-scanner.js +2 -0
  36. package/dist/services/launch-manager.d.ts +181 -0
  37. package/dist/services/launch-manager.js +12 -0
  38. package/dist/services/launch-metrics.d.ts +89 -0
  39. package/dist/services/launch-metrics.js +4 -0
  40. package/dist/services/launch-streams.d.ts +24 -0
  41. package/dist/services/launch-streams.js +11 -0
  42. package/dist/services/mcp-launch-binding-queue.js +1 -1
  43. package/dist/services/panel-auth.d.ts +11 -1
  44. package/dist/services/panel-auth.js +1 -1
  45. package/dist/services/panel-env.d.ts +5 -0
  46. package/dist/services/panel-env.js +2 -0
  47. package/dist/services/port-detector.d.ts +3 -0
  48. package/dist/services/port-detector.js +6 -0
  49. package/dist/services/runtime-lifecycle-policy.d.ts +1 -0
  50. package/dist/services/runtime-lifecycle-policy.js +1 -1
  51. package/dist/services/system-metrics.js +1 -1
  52. package/dist/services/tunnel-client.d.ts +4 -0
  53. package/dist/services/tunnel-client.js +1 -1
  54. package/dist/services/workspace-files.d.ts +3 -0
  55. package/dist/services/workspace-files.js +1 -1
  56. package/dist/services/workspaceTaskChangeWatcher.d.ts +2 -0
  57. package/dist/services/workspaceTaskChangeWatcher.js +3 -3
  58. package/dist/utils/config-write-queue.d.ts +3 -0
  59. package/dist/utils/config-write-queue.js +2 -0
  60. package/package/acode/dist/built-in-file-tools.d.ts +2 -0
  61. package/package/acode/dist/runtime.d.ts +20 -0
  62. package/package/acode/dist/runtime.js +27 -27
  63. package/package/acode/dist/sub-agent-manager.d.ts +4 -0
  64. package/package/acode/dist/sub-agent-manager.js +1 -1
  65. package/package/acode/dist/sub-agent-tools.d.ts +6 -0
  66. package/package/acode/dist/sub-agent-tools.js +39 -10
  67. package/package/acode/dist/sub-agent-types.d.ts +2 -0
  68. package/package/acode/dist/tool-allocation.d.ts +55 -0
  69. package/package/acode/dist/tool-allocation.js +2 -0
  70. package/package/acode/dist/types.d.ts +6 -0
  71. package/package/aws-client-agent-mcp/dist/agent-client.d.ts +24 -0
  72. package/package/aws-client-agent-mcp/dist/agent-client.js +1 -1
  73. package/package/aws-client-agent-mcp/dist/agent-client.test.js +19 -5
  74. package/package/aws-client-agent-mcp/dist/context-manager.test.js +6 -6
  75. package/package/aws-client-agent-mcp/dist/http-client.d.ts +2 -0
  76. package/package/aws-client-agent-mcp/dist/http-client.js +1 -1
  77. package/package/aws-client-agent-mcp/dist/index.js +1 -1
  78. package/package/aws-client-agent-mcp/dist/launch-config-tools.test.d.ts +1 -0
  79. package/package/aws-client-agent-mcp/dist/launch-config-tools.test.js +106 -0
  80. package/package/aws-client-agent-mcp/dist/logger.d.ts +4 -0
  81. package/package/aws-client-agent-mcp/dist/logger.js +2 -2
  82. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +21 -0
  83. package/package/aws-client-agent-mcp/dist/mcp-server.js +25 -25
  84. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +24 -16
  85. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +20 -0
  86. package/package/aws-client-agent-mcp/dist/mcp-tools.js +8 -8
  87. package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +55 -1
  88. package/package/aws-client-agent-mcp/dist/memory-store.test.js +5 -1
  89. package/package/aws-client-agent-mcp/dist/parent-lifecycle.d.ts +10 -0
  90. package/package/aws-client-agent-mcp/dist/parent-lifecycle.js +2 -0
  91. package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.d.ts +1 -0
  92. package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.js +81 -0
  93. package/package/aws-client-agent-mcp/dist/status-reporter.js +1 -1
  94. package/package/aws-client-agent-mcp/dist/types.d.ts +4 -0
  95. package/package/aws-client-agent-mcp/dist/watchdog-worker.d.ts +1 -0
  96. package/package/aws-client-agent-mcp/dist/watchdog-worker.js +3 -0
  97. package/package/aws-client-agent-mcp/dist/watchdog.d.ts +17 -0
  98. package/package/aws-client-agent-mcp/dist/watchdog.js +2 -0
  99. package/package/aws-client-agent-mcp/dist/watchdog.test.d.ts +1 -0
  100. package/package/aws-client-agent-mcp/dist/watchdog.test.js +124 -0
  101. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +4 -0
  102. package/package/aws-client-agent-mcp/dist/websocket-client.js +1 -1
  103. package/package/aws-client-agent-mcp/dist/websocket-client.test.js +83 -6
  104. package/package/aws-client-agent-mcp/package.json +1 -1
  105. package/package.json +2 -1
@@ -0,0 +1,2 @@
1
+ import{Worker as u}from"node:worker_threads";import{appendStartupLogLine as i,getStartupLogFilePath as A}from"./logger.js";const S=1e3,k=45e3,m=5e3;function _(n,t,r,s){const e=Number.parseInt(String(n||""),10);return!Number.isFinite(e)||e<r||e>s?t:e}function T(n={},t=process.env){const r={tickMs:_(t.AWS_MCP_WATCHDOG_TICK_MS,S,100,1e4),staleThresholdMs:_(t.AWS_MCP_WATCHDOG_STALE_MS,k,5e3,3e5),checkIntervalMs:_(t.AWS_MCP_WATCHDOG_CHECK_MS,m,500,6e4)},s=(e,c,a,h)=>typeof e=="number"&&Number.isFinite(e)&&e>=a&&e<=h?e:c;return{tickMs:s(n.tickMs,r.tickMs,10,6e4),staleThresholdMs:s(n.staleThresholdMs,r.staleThresholdMs,100,6e5),checkIntervalMs:s(n.checkIntervalMs,r.checkIntervalMs,20,12e4)}}let M=!1;function g(n={}){if(M)return;M=!0;const t=String(process.env.AWS_MCP_WATCHDOG_DISABLED||"").trim().toLowerCase();if(t==="1"||t==="true"||t==="yes"||t==="on"){i("info","[Watchdog] \u4E8B\u4EF6\u5FAA\u73AF\u770B\u95E8\u72D7\u5DF2\u901A\u8FC7 AWS_MCP_WATCHDOG_DISABLED \u5173\u95ED");return}const{tickMs:r,staleThresholdMs:s,checkIntervalMs:e}=T(n),c=new SharedArrayBuffer(8),a=new BigInt64Array(c);Atomics.store(a,0,BigInt(Date.now())),setInterval(()=>{Atomics.store(a,0,BigInt(Date.now()))},r).unref();try{const o=new u(new URL("./watchdog-worker.js",import.meta.url),{workerData:{buffer:c,staleThresholdMs:s,checkIntervalMs:e,logFilePath:A()}});o.on("error",l=>{i("warn",`[Watchdog] worker \u5F02\u5E38\uFF08\u770B\u95E8\u72D7\u5931\u6548\uFF0C\u4E0D\u963B\u65AD\u8FD0\u884C\uFF09: ${l.message}`)}),o.unref(),i("info",`[Watchdog] \u4E8B\u4EF6\u5FAA\u73AF\u770B\u95E8\u72D7\u5DF2\u542F\u52A8: tick=${r}ms, stale=${s}ms, check=${e}ms`)}catch(o){i("warn",`[Watchdog] \u542F\u52A8\u5931\u8D25\uFF08\u770B\u95E8\u72D7\u5931\u6548\uFF0C\u4E0D\u963B\u65AD\u8FD0\u884C\uFF09: ${o instanceof Error?o.message:String(o)}`)}}export{T as resolveWatchdogOptions,g as startEventLoopWatchdog};
2
+
@@ -0,0 +1,124 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawn } from 'node:child_process';
3
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { describe, it } from 'node:test';
7
+ import { resolveWatchdogOptions } from './watchdog.js';
8
+ const WATCHDOG_MODULE_URL = new URL('./watchdog.js', import.meta.url).href;
9
+ describe('resolveWatchdogOptions', () => {
10
+ it('uses defaults when nothing is provided', () => {
11
+ const resolved = resolveWatchdogOptions({}, {});
12
+ assert.deepEqual(resolved, {
13
+ tickMs: 1_000,
14
+ staleThresholdMs: 45_000,
15
+ checkIntervalMs: 5_000,
16
+ });
17
+ });
18
+ it('reads environment overrides', () => {
19
+ const resolved = resolveWatchdogOptions({}, {
20
+ AWS_MCP_WATCHDOG_TICK_MS: '200',
21
+ AWS_MCP_WATCHDOG_STALE_MS: '6000',
22
+ AWS_MCP_WATCHDOG_CHECK_MS: '1000',
23
+ });
24
+ assert.deepEqual(resolved, {
25
+ tickMs: 200,
26
+ staleThresholdMs: 6_000,
27
+ checkIntervalMs: 1_000,
28
+ });
29
+ });
30
+ it('falls back to defaults for out-of-range env values', () => {
31
+ const resolved = resolveWatchdogOptions({}, {
32
+ AWS_MCP_WATCHDOG_TICK_MS: '1',
33
+ AWS_MCP_WATCHDOG_STALE_MS: '999999999',
34
+ AWS_MCP_WATCHDOG_CHECK_MS: 'abc',
35
+ });
36
+ assert.deepEqual(resolved, {
37
+ tickMs: 1_000,
38
+ staleThresholdMs: 45_000,
39
+ checkIntervalMs: 5_000,
40
+ });
41
+ });
42
+ it('prefers explicit options over environment', () => {
43
+ const resolved = resolveWatchdogOptions({ tickMs: 50, staleThresholdMs: 500, checkIntervalMs: 100 }, {
44
+ AWS_MCP_WATCHDOG_TICK_MS: '200',
45
+ AWS_MCP_WATCHDOG_STALE_MS: '6000',
46
+ AWS_MCP_WATCHDOG_CHECK_MS: '1000',
47
+ });
48
+ assert.deepEqual(resolved, {
49
+ tickMs: 50,
50
+ staleThresholdMs: 500,
51
+ checkIntervalMs: 100,
52
+ });
53
+ });
54
+ it('falls back to env when explicit option is invalid', () => {
55
+ const resolved = resolveWatchdogOptions({ tickMs: Number.NaN, staleThresholdMs: -1 }, {
56
+ AWS_MCP_WATCHDOG_TICK_MS: '200',
57
+ AWS_MCP_WATCHDOG_STALE_MS: '6000',
58
+ });
59
+ assert.deepEqual(resolved, {
60
+ tickMs: 200,
61
+ staleThresholdMs: 6_000,
62
+ checkIntervalMs: 5_000,
63
+ });
64
+ });
65
+ });
66
+ /**
67
+ * 端到端验证:真实子进程 + 短阈值看门狗。
68
+ * 覆盖两类行为:
69
+ * 1) 主循环被同步死循环冻结 → 看门狗在阈值内强制终止进程(SIGKILL);
70
+ * 2) 主循环健康 → 看门狗不干预,进程正常退出。
71
+ *
72
+ * 注意:子进程必须以「文件」方式启动(node <tmp.mjs>)而非 --eval,
73
+ * 否则 --input-type 等 CLI flag 会被 worker 线程继承,导致看门狗 worker
74
+ * 启动失败(生产环境为 node dist/index.js,不受影响)。
75
+ */
76
+ async function runChild(code, timeoutMs = 10_000) {
77
+ const dir = mkdtempSync(path.join(tmpdir(), 'aws-mcp-watchdog-test-'));
78
+ const file = path.join(dir, 'child.mjs');
79
+ writeFileSync(file, code, 'utf8');
80
+ const child = spawn(process.execPath, [file], {
81
+ stdio: ['ignore', 'pipe', 'pipe'],
82
+ });
83
+ let stderr = '';
84
+ child.stderr.on('data', (chunk) => {
85
+ stderr += chunk.toString();
86
+ });
87
+ const exitCode = await new Promise((resolve) => {
88
+ const timer = setTimeout(() => {
89
+ child.kill('SIGKILL');
90
+ resolve(null);
91
+ }, timeoutMs);
92
+ child.on('exit', (code) => {
93
+ clearTimeout(timer);
94
+ resolve(code);
95
+ });
96
+ });
97
+ rmSync(dir, { recursive: true, force: true });
98
+ return { code: exitCode, stderr };
99
+ }
100
+ describe('startEventLoopWatchdog (integration)', () => {
101
+ it('force-kills the process when the main event loop freezes', async () => {
102
+ // tick=50ms, stale 阈值=300ms, 检查间隔=50ms;300ms 后主循环进入同步死循环。
103
+ const code = `
104
+ import { startEventLoopWatchdog } from ${JSON.stringify(WATCHDOG_MODULE_URL)};
105
+ startEventLoopWatchdog({ tickMs: 50, staleThresholdMs: 300, checkIntervalMs: 50 });
106
+ setTimeout(() => {
107
+ const t = Date.now();
108
+ while (Date.now() - t < 30000) {}
109
+ }, 300);
110
+ `;
111
+ const { code: exitCode, stderr } = await runChild(code);
112
+ assert.notEqual(exitCode, null, `看门狗未在超时内终止冻结进程。stderr: ${stderr}`);
113
+ assert.notEqual(exitCode, 0, '冻结进程应被强杀而非正常退出');
114
+ });
115
+ it('leaves a healthy process untouched', async () => {
116
+ const code = `
117
+ import { startEventLoopWatchdog } from ${JSON.stringify(WATCHDOG_MODULE_URL)};
118
+ startEventLoopWatchdog({ tickMs: 50, staleThresholdMs: 300, checkIntervalMs: 50 });
119
+ setTimeout(() => process.exit(0), 1500);
120
+ `;
121
+ const { code: exitCode, stderr } = await runChild(code);
122
+ assert.equal(exitCode, 0, `健康进程不应被看门狗终止。stderr: ${stderr}`);
123
+ });
124
+ });
@@ -13,6 +13,8 @@ export declare class WebSocketClient extends EventEmitter {
13
13
  private isManualClose;
14
14
  private websocketFactory;
15
15
 
16
+ private openedAt;
17
+
16
18
  private lastRefreshedToken;
17
19
 
18
20
  private consecutiveRefreshFailures;
@@ -49,6 +51,8 @@ export declare class WebSocketClient extends EventEmitter {
49
51
 
50
52
  private handleTokenExpiredAndReconnect;
51
53
 
54
+ private enterSlowReconnect;
55
+
52
56
  private refreshRuntimeTokenOnPolicyRejection;
53
57
 
54
58
  private handleWebSocketError;
@@ -1,2 +1,2 @@
1
- import{EventEmitter as m}from"node:events";import a from"ws";import{logger as r}from"./logger.js";const f=9e3,T=3e4,b=1e3,l=100,u=3,d=3,k=50,g=1e3;class R extends m{ws=null;config;agentId=null;heartbeatTimer=null;heartbeatTimeoutTimer=null;reconnectAttempts=0;reconnectTimer=null;isManualClose=!1;websocketFactory;lastRefreshedToken=null;consecutiveRefreshFailures=0;logBatchQueue=[];logBatchTimer=null;logSequenceNumber=0;pendingLogBatches=new Map;isProcessingLogBatch=!1;processedMsgIds=new Map;constructor(e,t){super(),this.config=e,this.websocketFactory=t??(s=>new a(s))}connect(e){return new Promise((t,s)=>{this.prepareConnection(e);const n=this.buildWebSocketUrl(e);r.info(`[WebSocket] \u6B63\u5728\u8FDE\u63A5: ${this.maskRuntimeTokenInUrl(n)}`);const{rejectOnce:o,resolveOnce:i}=this.createSettledHandlers(t,s);try{this.ws=this.websocketFactory(n),this.setupWebSocketEventHandlers(this.ws,o,i)}catch(c){o(c instanceof Error?c:new Error(String(c)))}})}prepareConnection(e){this.agentId=e,this.isManualClose=!1,this.ws&&this.ws.readyState!==a.CLOSED&&(this.ws.removeAllListeners(),this.ws.terminate())}buildWebSocketUrl(e){const t=new URL(this.config.serverUrl);t.searchParams.set("agentId",e);const s=this.config.runtimeAccessToken?.trim();return s&&t.searchParams.set("runtimeAccessToken",s),t.toString()}maskRuntimeTokenInUrl(e){try{const t=new URL(e);return t.searchParams.has("runtimeAccessToken")&&t.searchParams.set("runtimeAccessToken","***"),t.toString()}catch{return e.replace(/([?&]runtimeAccessToken=)[^&]*/u,"$1***")}}createSettledHandlers(e,t){let s=!1;return{rejectOnce:i=>{s||(s=!0,t(i))},resolveOnce:()=>{s||(s=!0,e())}}}setupWebSocketEventHandlers(e,t,s){let n=!1;e.on("open",()=>{n=!0,this.handleWebSocketOpen(s)}),e.on("message",o=>this.handleWebSocketMessage(o)),e.on("pong",()=>this.markHeartbeatReceived()),e.on("close",(o,i)=>this.handleWebSocketClose(o,i,n,t)),e.on("error",o=>this.handleWebSocketError(o,n,t))}handleWebSocketOpen(e){r.info("[WebSocket] \u8FDE\u63A5\u5DF2\u5EFA\u7ACB"),this.reconnectAttempts=0,this.consecutiveRefreshFailures=0,this.lastRefreshedToken=null,this.clearReconnectTimer(),this.startHeartbeat(),this.emit("connected"),e()}handleWebSocketMessage(e){try{const t=JSON.parse(e.toString()),s=this.extractReliableMessageId(t),n=s!==null&&this.markMsgIdProcessed(s);if(this.acknowledgeReliableMessage(t),n){r.info("[WebSocket] \u5FFD\u7565\u91CD\u590D\u6295\u9012\u7684\u53EF\u9760\u6D88\u606F: msgId=%s, type=%s",s,t.type);return}this.handleMessage(t)}catch(t){const s=e.toString(),n=s.length>200?`${s.slice(0,200)}...(truncated, ${s.length} total)`:s;r.warn("[WebSocket] \u6D88\u606F\u89E3\u6790\u5931\u8D25, dataLength=%s, preview=%s:",s.length,n,t)}}extractReliableMessageId(e){const t=e.data;if(!t||typeof t!="object")return null;const s=t,n=String(s._msgId||"").trim(),o=s._requireAck===!0||s._requireAck==="true";return!n||!o?null:n}markMsgIdProcessed(e){if(this.processedMsgIds.has(e))return!0;if(this.processedMsgIds.set(e,Date.now()),this.processedMsgIds.size>g){let t=0;const s=this.processedMsgIds.size-g;for(const n of this.processedMsgIds.keys()){if(t>=s)break;this.processedMsgIds.delete(n),t++}}return!1}acknowledgeReliableMessage(e){const t=this.extractReliableMessageId(e);t&&this.send({type:"MESSAGE_ACK",data:{msgId:t},timestamp:new Date().toISOString()})}async handleWebSocketClose(e,t,s,n){const o=t.toString(),i=this.ws?.readyState;r.warn(`[WebSocket] \u8FDE\u63A5\u5173\u95ED: code=${e}, reason=${o}, readyState=${i}, opened=${s}, reconnectAttempts=${this.reconnectAttempts}`),this.stopHeartbeat(),this.clearHeartbeatTimeout(),this.emit("disconnected",e,t),s||n(new Error(`WebSocket \u8FDE\u63A5\u5728\u5EFA\u7ACB\u524D\u5173\u95ED: code=${e}, reason=${o}`)),this.isManualClose||(e===1008?await this.handleTokenExpiredAndReconnect():this.scheduleReconnect())}async handleTokenExpiredAndReconnect(){const e=await this.refreshRuntimeTokenOnPolicyRejection();if(e==="refreshed"){r.info("[WebSocket] runtime token \u5DF2\u5237\u65B0\uFF0C\u4F7F\u7528\u65B0 token \u91CD\u8FDE"),this.consecutiveRefreshFailures=0,this.scheduleReconnect();return}if(e==="unchanged"){r.error("[WebSocket] bridge \u8FD4\u56DE token \u672A\u53D8\u5316\uFF0C\u4F46\u670D\u52A1\u7AEF\u4ECD\u4EE5 1008 \u62D2\u7EDD\uFF0C\u505C\u6B62\u91CD\u8FDE\uFF08\u9700\u4EBA\u5DE5\u6216 supervisor \u4ECB\u5165\uFF09"),this.emit("tokenRejected",this.config.runtimeAccessToken);return}if(this.consecutiveRefreshFailures+=1,this.consecutiveRefreshFailures>=d){r.error(`[WebSocket] runtime token \u5237\u65B0\u8FDE\u7EED\u5931\u8D25 ${this.consecutiveRefreshFailures} \u6B21\uFF0C\u505C\u6B62\u91CD\u8FDE\uFF08\u4EA4\u7531 supervisor \u91CD\u542F\uFF09`),this.emit("tokenRefreshFailed",this.consecutiveRefreshFailures);return}r.warn(`[WebSocket] runtime token \u5237\u65B0\u5931\u8D25\uFF0C\u7B2C ${this.consecutiveRefreshFailures}/${d} \u6B21\u5C1D\u8BD5\u91CD\u8FDE`),this.scheduleReconnect()}async refreshRuntimeTokenOnPolicyRejection(){const e=this.config.runtimeBridgeBaseUrl;if(!e||!this.agentId)return"failed";const t=this.config.runtimeAccessToken||"",s=this.config.mcpHttpRequestTimeoutMs??12e4,n=new AbortController,o=setTimeout(()=>n.abort(),s);try{const i=await fetch(`${e}/runtime/binding/refresh-token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agentId:this.agentId,currentToken:t,forceRefresh:!0}),signal:n.signal});if(!i.ok)return r.warn(`[WebSocket] bridge refresh-token \u8FD4\u56DE ${i.status}\uFF0C\u65E0\u6CD5\u5237\u65B0`),"failed";const h=(await i.json()).runtimeAccessToken;return!h||h===t?"unchanged":(this.config.runtimeAccessToken=h,this.lastRefreshedToken=t,r.info("[WebSocket] runtimeAccessToken \u5DF2\u901A\u8FC7 bridge refresh \u66F4\u65B0"),"refreshed")}catch(i){return i instanceof Error&&i.name==="AbortError"?r.warn(`[WebSocket] bridge refresh-token \u8D85\u65F6\uFF08${s}ms\uFF09`):(r.warn("[WebSocket] bridge refresh-token \u5F02\u5E38: %s",i instanceof Error?i.message:String(i)),i instanceof Error&&i.stack&&r.warn("[WebSocket] refresh-token \u5F02\u5E38\u5806\u6808:",i.stack.slice(0,2e3))),"failed"}finally{clearTimeout(o)}}handleWebSocketError(e,t,s){const n=this.ws?.readyState;r.error("[WebSocket] \u8FDE\u63A5\u9519\u8BEF: readyState=%s, opened=%s, reconnectAttempts=%s, message=%s",n,t,this.reconnectAttempts,e.message),r.error("[WebSocket] \u9519\u8BEF\u5806\u6808:",e.stack?.slice(0,2e3)),this.emit("error",e),t||s(e)}disconnect(){this.isManualClose=!0,this.stopHeartbeat(),this.clearHeartbeatTimeout(),this.clearReconnectTimer(),this.ws&&(this.ws.readyState===a.OPEN&&this.ws.close(1e3,"Agent logout"),this.ws=null),this.agentId=null,r.info("[WebSocket] \u5DF2\u65AD\u5F00\u8FDE\u63A5")}send(e){const t=this.ws?.readyState;if(!this.ws||t!==a.OPEN)return r.warn("[WebSocket] \u8FDE\u63A5\u672A\u5EFA\u7ACB\uFF0C\u65E0\u6CD5\u53D1\u9001\u6D88\u606F: readyState=%s, isManualClose=%s, agentId=%s",t,this.isManualClose,this.agentId),!1;try{return this.ws.send(JSON.stringify(e)),!0}catch(s){return r.error("[WebSocket] send() \u53D1\u9001\u6D88\u606F\u5F02\u5E38: readyState=%s, error=%s",t,s),!1}}sendMcpNodeLog(e){return this.isConnected()?(this.logBatchQueue.push(e),this.logBatchTimer||(this.logBatchTimer=setTimeout(()=>{this.logBatchTimer=null,this.flushLogBatch()},l)),!0):!1}flushLogBatch(){if(this.logBatchQueue.length===0||this.isProcessingLogBatch)return;const e=this.logBatchQueue.splice(0,k);if(e.length===0)return;this.isProcessingLogBatch=!0,this.logSequenceNumber+=1;const t=this.logSequenceNumber;new Promise((n,o)=>{this.pendingLogBatches.set(t,{logs:e,sequenceNumber:t,retryCount:0,resolve:n,reject:o})}).then(()=>{this.isProcessingLogBatch=!1,this.logBatchQueue.length>0&&(this.logBatchTimer=setTimeout(()=>{this.logBatchTimer=null,this.flushLogBatch()},l))}).catch(()=>{this.isProcessingLogBatch=!1}),this.sendLogBatch(t)}sendLogBatch(e){const t=this.pendingLogBatches.get(e);if(!t)return;this.send({type:"MCP_NODE_LOG_BATCH",data:{sequenceNumber:e,logs:t.logs},timestamp:new Date().toISOString()})||this.handleLogBatchSendFailure(e)}handleLogBatchSendFailure(e){const t=this.pendingLogBatches.get(e);t&&(t.retryCount+=1,t.retryCount<=u?(r.warn(`[WebSocket] \u65E5\u5FD7\u6279\u6B21 #${e} \u53D1\u9001\u5931\u8D25\uFF0C\u7B2C ${t.retryCount} \u6B21\u91CD\u8BD5`),setTimeout(()=>{this.sendLogBatch(e)},l*t.retryCount)):(r.error(`[WebSocket] \u65E5\u5FD7\u6279\u6B21 #${e} \u53D1\u9001\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${u} \u6B21\uFF0C\u4E22\u5F03`),this.pendingLogBatches.delete(e),t.reject(new Error(`\u65E5\u5FD7\u6279\u6B21 #${e} \u53D1\u9001\u5931\u8D25`))))}handleLogAck(e){const s=Number(e?.ackSequenceNumber);if(!s||!this.pendingLogBatches.has(s))return;const n=this.pendingLogBatches.get(s);n&&(this.pendingLogBatches.delete(s),n.resolve())}isConnected(){return this.ws!==null&&this.ws.readyState===a.OPEN}handleMessage(e){switch(e.type){case"GROUP_MSG":this.emit("groupMessage",e.data);break;case"DM_MSG":this.emit("directMessage",e.data);break;case"ONLINE_CHANGE":this.emit("onlineChange",e.data);break;case"HEARTBEAT":this.markHeartbeatReceived();break;case"PROFILE_INFO":r.info("[WebSocket] \u6536\u5230 PROFILE_INFO \u6D88\u606F"),this.emit("profileInfo",e.data);break;case"AGENT_CONFIG":r.info("[WebSocket] \u6536\u5230 AGENT_CONFIG \u6D88\u606F"),this.emit("agentConfig",e.data);break;case"AGENT_STATUS":r.info("[WebSocket] \u6536\u5230 AGENT_STATUS \u6D88\u606F"),this.emit("agentStatus",e.data);break;case"LOG_ACK":this.handleLogAck(e.data);break;case"GROUP_READ_RECEIPT":case"DM_READ_RECEIPT":case"TERMINAL_OUTPUT":case"TERMINAL_STATUS":case"MCP_NODE_LOG":case"AGENT_EVENTS_DELTA":break;default:r.warn("[WebSocket] \u672A\u77E5\u6D88\u606F\u7C7B\u578B:",e.type)}}startHeartbeat(){this.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{this.isConnected()&&(this.ensureHeartbeatTimeout(),this.sendProtocolPing(),this.send({type:"HEARTBEAT",data:"ping",timestamp:new Date().toISOString()}))},this.config.heartbeatInterval)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null),this.clearHeartbeatTimeout()}sendProtocolPing(){if(!(!this.ws||this.ws.readyState!==a.OPEN))try{this.ws.ping()}catch(e){r.warn("[WebSocket] \u534F\u8BAE\u5C42 ping \u53D1\u9001\u5931\u8D25\uFF0C\u7EC8\u6B62\u8FDE\u63A5\u5E76\u91CD\u8FDE:",e instanceof Error?e.message:String(e)),this.ws.terminate()}}markHeartbeatReceived(){this.clearHeartbeatTimeout()}ensureHeartbeatTimeout(){this.heartbeatTimeoutTimer||(this.heartbeatTimeoutTimer=setTimeout(()=>{this.heartbeatTimeoutTimer=null,!(!this.ws||this.ws.readyState!==a.OPEN)&&(r.warn(`[WebSocket] ${this.resolveHeartbeatTimeout()}ms \u672A\u6536\u5230\u5FC3\u8DF3\u54CD\u5E94\uFF0C\u7EC8\u6B62\u8FDE\u63A5\u5E76\u89E6\u53D1\u91CD\u8FDE`),this.ws.terminate())},this.resolveHeartbeatTimeout()))}clearHeartbeatTimeout(){this.heartbeatTimeoutTimer&&(clearTimeout(this.heartbeatTimeoutTimer),this.heartbeatTimeoutTimer=null)}resolveHeartbeatTimeout(){return this.config.heartbeatTimeout??f}scheduleReconnect(){const e=this.config.maxReconnectAttempts!==-1;if(e&&this.reconnectAttempts>=this.config.maxReconnectAttempts){r.error("[WebSocket] \u91CD\u8FDE\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u653E\u5F03\u91CD\u8FDE"),this.emit("reconnectFailed");return}if(this.reconnectTimer)return;this.reconnectAttempts++;const t=Math.min(this.config.reconnectInterval*Math.pow(2,this.reconnectAttempts-1),this.config.reconnectMaxInterval??T),s=Math.random()*(this.config.reconnectJitterMs??b),n=Math.round(t+s),o=e?`\u7B2C ${this.reconnectAttempts}/${this.config.maxReconnectAttempts} \u6B21`:`\u7B2C ${this.reconnectAttempts} \u6B21\uFF08\u65E0\u9650\u91CD\u8BD5\u6A21\u5F0F\uFF09`;r.warn(`[WebSocket] ${n}ms \u540E\u5C1D\u8BD5${o}\u91CD\u8FDE (base=${Math.round(t)}ms, jitter=+${Math.round(s)}ms)...`),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.agentId&&this.connect(this.agentId).catch(i=>{const c=i instanceof Error?i.message:String(i),h=i instanceof Error?i.stack:void 0;r.warn("[WebSocket] \u91CD\u8FDE\u5931\u8D25 (attempt #%s), \u7EE7\u7EED\u91CD\u8BD5: %s",this.reconnectAttempts,c),h&&r.warn("[WebSocket] \u91CD\u8FDE\u5931\u8D25\u5806\u6808:",h.slice(0,2e3)),this.scheduleReconnect()})},n)}clearReconnectTimer(){this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}}export{R as WebSocketClient};
1
+ import{EventEmitter as m}from"node:events";import c from"ws";import{logger as r}from"./logger.js";const f=9e3,T=3e4,b=1e3,l=100,u=3,d=3,S=5,k=6e4,p=50,g=1e3;class M extends m{ws=null;config;agentId=null;heartbeatTimer=null;heartbeatTimeoutTimer=null;reconnectAttempts=0;reconnectTimer=null;isManualClose=!1;websocketFactory;openedAt=null;lastRefreshedToken=null;consecutiveRefreshFailures=0;logBatchQueue=[];logBatchTimer=null;logSequenceNumber=0;pendingLogBatches=new Map;isProcessingLogBatch=!1;processedMsgIds=new Map;constructor(e,t){super(),this.config=e,this.websocketFactory=t??(s=>new c(s))}connect(e){return new Promise((t,s)=>{this.prepareConnection(e);const n=this.buildWebSocketUrl(e);r.info(`[WebSocket] \u6B63\u5728\u8FDE\u63A5: ${this.maskRuntimeTokenInUrl(n)}`);const{rejectOnce:o,resolveOnce:i}=this.createSettledHandlers(t,s);try{this.ws=this.websocketFactory(n),this.setupWebSocketEventHandlers(this.ws,o,i)}catch(a){o(a instanceof Error?a:new Error(String(a)))}})}prepareConnection(e){this.agentId=e,this.isManualClose=!1,this.ws&&this.ws.readyState!==c.CLOSED&&(this.ws.removeAllListeners(),this.ws.terminate())}buildWebSocketUrl(e){const t=new URL(this.config.serverUrl);t.searchParams.set("agentId",e);const s=this.config.runtimeAccessToken?.trim();return s&&t.searchParams.set("runtimeAccessToken",s),t.toString()}maskRuntimeTokenInUrl(e){try{const t=new URL(e);return t.searchParams.has("runtimeAccessToken")&&t.searchParams.set("runtimeAccessToken","***"),t.toString()}catch{return e.replace(/([?&]runtimeAccessToken=)[^&]*/u,"$1***")}}createSettledHandlers(e,t){let s=!1;return{rejectOnce:i=>{s||(s=!0,t(i))},resolveOnce:()=>{s||(s=!0,e())}}}setupWebSocketEventHandlers(e,t,s){let n=!1;e.on("open",()=>{n=!0,this.handleWebSocketOpen(s)}),e.on("message",o=>this.handleWebSocketMessage(o)),e.on("pong",()=>this.markHeartbeatReceived()),e.on("close",(o,i)=>this.handleWebSocketClose(o,i,n,t)),e.on("error",o=>this.handleWebSocketError(o,n,t))}handleWebSocketOpen(e){r.info("[WebSocket] \u8FDE\u63A5\u5DF2\u5EFA\u7ACB"),this.openedAt=Date.now(),this.lastRefreshedToken=null,this.clearReconnectTimer(),this.startHeartbeat(),this.emit("connected"),e()}handleWebSocketMessage(e){try{const t=JSON.parse(e.toString()),s=this.extractReliableMessageId(t),n=s!==null&&this.markMsgIdProcessed(s);if(this.acknowledgeReliableMessage(t),n){r.info("[WebSocket] \u5FFD\u7565\u91CD\u590D\u6295\u9012\u7684\u53EF\u9760\u6D88\u606F: msgId=%s, type=%s",s,t.type);return}this.handleMessage(t)}catch(t){const s=e.toString(),n=s.length>200?`${s.slice(0,200)}...(truncated, ${s.length} total)`:s;r.warn("[WebSocket] \u6D88\u606F\u89E3\u6790\u5931\u8D25, dataLength=%s, preview=%s:",s.length,n,t)}}extractReliableMessageId(e){const t=e.data;if(!t||typeof t!="object")return null;const s=t,n=String(s._msgId||"").trim(),o=s._requireAck===!0||s._requireAck==="true";return!n||!o?null:n}markMsgIdProcessed(e){if(this.processedMsgIds.has(e))return!0;if(this.processedMsgIds.set(e,Date.now()),this.processedMsgIds.size>g){let t=0;const s=this.processedMsgIds.size-g;for(const n of this.processedMsgIds.keys()){if(t>=s)break;this.processedMsgIds.delete(n),t++}}return!1}acknowledgeReliableMessage(e){const t=this.extractReliableMessageId(e);t&&this.send({type:"MESSAGE_ACK",data:{msgId:t},timestamp:new Date().toISOString()})}async handleWebSocketClose(e,t,s,n){const o=t.toString(),i=this.ws?.readyState;r.warn(`[WebSocket] \u8FDE\u63A5\u5173\u95ED: code=${e}, reason=${o}, readyState=${i}, opened=${s}, reconnectAttempts=${this.reconnectAttempts}`),this.stopHeartbeat(),this.clearHeartbeatTimeout(),this.emit("disconnected",e,t),this.openedAt!==null&&Date.now()-this.openedAt>=k&&(this.reconnectAttempts=0,this.consecutiveRefreshFailures=0),this.openedAt=null,s||n(new Error(`WebSocket \u8FDE\u63A5\u5728\u5EFA\u7ACB\u524D\u5173\u95ED: code=${e}, reason=${o}`)),this.isManualClose||(e===1008?await this.handleTokenExpiredAndReconnect():this.scheduleReconnect())}async handleTokenExpiredAndReconnect(){const e=await this.refreshRuntimeTokenOnPolicyRejection();if(e==="refreshed"){r.info("[WebSocket] runtime token \u5DF2\u5237\u65B0\uFF0C\u4F7F\u7528\u65B0 token \u91CD\u8FDE"),this.consecutiveRefreshFailures=0,this.scheduleReconnect();return}if(e==="unchanged"){r.warn("[WebSocket] bridge \u8FD4\u56DE token \u672A\u53D8\u5316\u4E14\u670D\u52A1\u7AEF 1008 \u62D2\u7EDD\uFF0C\u8FDB\u5165\u6162\u901F\u91CD\u8BD5\u7B49\u5F85\u65B0 token \u7B7E\u53D1"),this.emit("tokenRejected",this.config.runtimeAccessToken),this.enterSlowReconnect();return}if(this.consecutiveRefreshFailures+=1,this.consecutiveRefreshFailures>=d){r.error(`[WebSocket] runtime token \u5237\u65B0\u8FDE\u7EED\u5931\u8D25 ${this.consecutiveRefreshFailures} \u6B21\uFF0C\u964D\u7EA7\u4E3A\u6162\u901F\u91CD\u8BD5\u7B49\u5F85 bridge \u6062\u590D`),this.emit("tokenRefreshFailed",this.consecutiveRefreshFailures),this.enterSlowReconnect();return}r.warn(`[WebSocket] runtime token \u5237\u65B0\u5931\u8D25\uFF0C\u7B2C ${this.consecutiveRefreshFailures}/${d} \u6B21\u5C1D\u8BD5\u91CD\u8FDE`),this.scheduleReconnect()}enterSlowReconnect(){this.reconnectAttempts=Math.max(this.reconnectAttempts,S),this.scheduleReconnect()}async refreshRuntimeTokenOnPolicyRejection(){const e=this.config.runtimeBridgeBaseUrl;if(!e||!this.agentId)return"failed";const t=this.config.runtimeAccessToken||"",s=Math.min(this.config.mcpHttpRequestTimeoutMs??12e4,1e4),n=new AbortController,o=setTimeout(()=>n.abort(),s);try{const i=await fetch(`${e}/runtime/binding/refresh-token`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agentId:this.agentId,currentToken:t,forceRefresh:!0}),signal:n.signal});if(!i.ok)return r.warn(`[WebSocket] bridge refresh-token \u8FD4\u56DE ${i.status}\uFF0C\u65E0\u6CD5\u5237\u65B0`),"failed";const h=(await i.json()).runtimeAccessToken;return!h||h===t?"unchanged":(this.config.runtimeAccessToken=h,this.lastRefreshedToken=t,r.info("[WebSocket] runtimeAccessToken \u5DF2\u901A\u8FC7 bridge refresh \u66F4\u65B0"),"refreshed")}catch(i){return i instanceof Error&&i.name==="AbortError"?r.warn(`[WebSocket] bridge refresh-token \u8D85\u65F6\uFF08${s}ms\uFF09`):(r.warn("[WebSocket] bridge refresh-token \u5F02\u5E38: %s",i instanceof Error?i.message:String(i)),i instanceof Error&&i.stack&&r.warn("[WebSocket] refresh-token \u5F02\u5E38\u5806\u6808:",i.stack.slice(0,2e3))),"failed"}finally{clearTimeout(o)}}handleWebSocketError(e,t,s){const n=this.ws?.readyState;r.error("[WebSocket] \u8FDE\u63A5\u9519\u8BEF: readyState=%s, opened=%s, reconnectAttempts=%s, message=%s",n,t,this.reconnectAttempts,e.message),r.error("[WebSocket] \u9519\u8BEF\u5806\u6808:",e.stack?.slice(0,2e3)),this.emit("error",e),t||s(e)}disconnect(){this.isManualClose=!0,this.stopHeartbeat(),this.clearHeartbeatTimeout(),this.clearReconnectTimer(),this.ws&&(this.ws.readyState===c.OPEN&&this.ws.close(1e3,"Agent logout"),this.ws=null),this.agentId=null,r.info("[WebSocket] \u5DF2\u65AD\u5F00\u8FDE\u63A5")}send(e){const t=this.ws?.readyState;if(!this.ws||t!==c.OPEN)return r.warn("[WebSocket] \u8FDE\u63A5\u672A\u5EFA\u7ACB\uFF0C\u65E0\u6CD5\u53D1\u9001\u6D88\u606F: readyState=%s, isManualClose=%s, agentId=%s",t,this.isManualClose,this.agentId),!1;try{return this.ws.send(JSON.stringify(e)),!0}catch(s){return r.error("[WebSocket] send() \u53D1\u9001\u6D88\u606F\u5F02\u5E38: readyState=%s, error=%s",t,s),!1}}sendMcpNodeLog(e){return this.isConnected()?(this.logBatchQueue.push(e),this.logBatchTimer||(this.logBatchTimer=setTimeout(()=>{this.logBatchTimer=null,this.flushLogBatch()},l)),!0):!1}flushLogBatch(){if(this.logBatchQueue.length===0||this.isProcessingLogBatch)return;const e=this.logBatchQueue.splice(0,p);if(e.length===0)return;this.isProcessingLogBatch=!0,this.logSequenceNumber+=1;const t=this.logSequenceNumber;new Promise((n,o)=>{this.pendingLogBatches.set(t,{logs:e,sequenceNumber:t,retryCount:0,resolve:n,reject:o})}).then(()=>{this.isProcessingLogBatch=!1,this.logBatchQueue.length>0&&(this.logBatchTimer=setTimeout(()=>{this.logBatchTimer=null,this.flushLogBatch()},l))}).catch(()=>{this.isProcessingLogBatch=!1}),this.sendLogBatch(t)}sendLogBatch(e){const t=this.pendingLogBatches.get(e);if(!t)return;this.send({type:"MCP_NODE_LOG_BATCH",data:{sequenceNumber:e,logs:t.logs},timestamp:new Date().toISOString()})||this.handleLogBatchSendFailure(e)}handleLogBatchSendFailure(e){const t=this.pendingLogBatches.get(e);t&&(t.retryCount+=1,t.retryCount<=u?(r.warn(`[WebSocket] \u65E5\u5FD7\u6279\u6B21 #${e} \u53D1\u9001\u5931\u8D25\uFF0C\u7B2C ${t.retryCount} \u6B21\u91CD\u8BD5`),setTimeout(()=>{this.sendLogBatch(e)},l*t.retryCount)):(r.error(`[WebSocket] \u65E5\u5FD7\u6279\u6B21 #${e} \u53D1\u9001\u5931\u8D25\uFF0C\u5DF2\u91CD\u8BD5 ${u} \u6B21\uFF0C\u4E22\u5F03`),this.pendingLogBatches.delete(e),t.reject(new Error(`\u65E5\u5FD7\u6279\u6B21 #${e} \u53D1\u9001\u5931\u8D25`))))}handleLogAck(e){const s=Number(e?.ackSequenceNumber);if(!s||!this.pendingLogBatches.has(s))return;const n=this.pendingLogBatches.get(s);n&&(this.pendingLogBatches.delete(s),n.resolve())}isConnected(){return this.ws!==null&&this.ws.readyState===c.OPEN}handleMessage(e){switch(e.type){case"GROUP_MSG":this.emit("groupMessage",e.data);break;case"DM_MSG":this.emit("directMessage",e.data);break;case"ONLINE_CHANGE":this.emit("onlineChange",e.data);break;case"HEARTBEAT":this.markHeartbeatReceived();break;case"PROFILE_INFO":r.info("[WebSocket] \u6536\u5230 PROFILE_INFO \u6D88\u606F"),this.emit("profileInfo",e.data);break;case"AGENT_CONFIG":r.info("[WebSocket] \u6536\u5230 AGENT_CONFIG \u6D88\u606F"),this.emit("agentConfig",e.data);break;case"AGENT_STATUS":r.info("[WebSocket] \u6536\u5230 AGENT_STATUS \u6D88\u606F"),this.emit("agentStatus",e.data);break;case"LOG_ACK":this.handleLogAck(e.data);break;case"GROUP_READ_RECEIPT":case"DM_READ_RECEIPT":case"TERMINAL_OUTPUT":case"TERMINAL_STATUS":case"MCP_NODE_LOG":case"AGENT_EVENTS_DELTA":break;default:r.warn("[WebSocket] \u672A\u77E5\u6D88\u606F\u7C7B\u578B:",e.type)}}startHeartbeat(){this.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{this.isConnected()&&(this.ensureHeartbeatTimeout(),this.sendProtocolPing(),this.send({type:"HEARTBEAT",data:"ping",timestamp:new Date().toISOString()}))},this.config.heartbeatInterval)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null),this.clearHeartbeatTimeout()}sendProtocolPing(){if(!(!this.ws||this.ws.readyState!==c.OPEN))try{this.ws.ping()}catch(e){r.warn("[WebSocket] \u534F\u8BAE\u5C42 ping \u53D1\u9001\u5931\u8D25\uFF0C\u7EC8\u6B62\u8FDE\u63A5\u5E76\u91CD\u8FDE:",e instanceof Error?e.message:String(e)),this.ws.terminate()}}markHeartbeatReceived(){this.clearHeartbeatTimeout()}ensureHeartbeatTimeout(){this.heartbeatTimeoutTimer||(this.heartbeatTimeoutTimer=setTimeout(()=>{this.heartbeatTimeoutTimer=null,!(!this.ws||this.ws.readyState!==c.OPEN)&&(r.warn(`[WebSocket] ${this.resolveHeartbeatTimeout()}ms \u672A\u6536\u5230\u5FC3\u8DF3\u54CD\u5E94\uFF0C\u7EC8\u6B62\u8FDE\u63A5\u5E76\u89E6\u53D1\u91CD\u8FDE`),this.ws.terminate())},this.resolveHeartbeatTimeout()))}clearHeartbeatTimeout(){this.heartbeatTimeoutTimer&&(clearTimeout(this.heartbeatTimeoutTimer),this.heartbeatTimeoutTimer=null)}resolveHeartbeatTimeout(){return this.config.heartbeatTimeout??f}scheduleReconnect(){const e=this.config.maxReconnectAttempts!==-1;if(e&&this.reconnectAttempts>=this.config.maxReconnectAttempts){r.error("[WebSocket] \u91CD\u8FDE\u6B21\u6570\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u653E\u5F03\u91CD\u8FDE"),this.emit("reconnectFailed");return}if(this.reconnectTimer)return;this.reconnectAttempts++;const t=Math.min(this.config.reconnectInterval*Math.pow(2,this.reconnectAttempts-1),this.config.reconnectMaxInterval??T),s=Math.random()*(this.config.reconnectJitterMs??b),n=Math.round(t+s),o=e?`\u7B2C ${this.reconnectAttempts}/${this.config.maxReconnectAttempts} \u6B21`:`\u7B2C ${this.reconnectAttempts} \u6B21\uFF08\u65E0\u9650\u91CD\u8BD5\u6A21\u5F0F\uFF09`;r.warn(`[WebSocket] ${n}ms \u540E\u5C1D\u8BD5${o}\u91CD\u8FDE (base=${Math.round(t)}ms, jitter=+${Math.round(s)}ms)...`),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.agentId&&this.connect(this.agentId).catch(i=>{const a=i instanceof Error?i.message:String(i),h=i instanceof Error?i.stack:void 0;r.warn("[WebSocket] \u91CD\u8FDE\u5931\u8D25 (attempt #%s), \u7EE7\u7EED\u91CD\u8BD5: %s",this.reconnectAttempts,a),h&&r.warn("[WebSocket] \u91CD\u8FDE\u5931\u8D25\u5806\u6808:",h.slice(0,2e3)),this.scheduleReconnect()})},n)}clearReconnectTimer(){this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null)}}export{M as WebSocketClient};
2
2
 
@@ -81,6 +81,7 @@ describe('WebSocketClient reconnect behavior', () => {
81
81
  heartbeatInterval: 1000,
82
82
  heartbeatTimeout: 3000,
83
83
  reconnectInterval: 10,
84
+ reconnectJitterMs: 0,
84
85
  maxReconnectAttempts: 2,
85
86
  runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
86
87
  userId: 'user-a',
@@ -129,7 +130,7 @@ describe('WebSocketClient reconnect behavior', () => {
129
130
  globalThis.fetch = originalFetch;
130
131
  }
131
132
  });
132
- it('should not hammer token refresh endpoint after refresh failure for same token', async () => {
133
+ it('should not hammer token refresh endpoint after repeated refresh failures', async () => {
133
134
  let refreshRequests = 0;
134
135
  const originalFetch = globalThis.fetch;
135
136
  globalThis.fetch = (async () => {
@@ -145,6 +146,7 @@ describe('WebSocketClient reconnect behavior', () => {
145
146
  heartbeatInterval: 1000,
146
147
  heartbeatTimeout: 3000,
147
148
  reconnectInterval: 10,
149
+ reconnectJitterMs: 0,
148
150
  maxReconnectAttempts: 3,
149
151
  runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
150
152
  userId: 'user-a',
@@ -165,18 +167,88 @@ describe('WebSocketClient reconnect behavior', () => {
165
167
  // suppress unhandled error event in test environment
166
168
  });
167
169
  await client.connect('agent-test');
170
+ // 每次失败重连触发一次刷新;连续失败达到上限后不再高频重连/刷新。
171
+ // maxReconnectAttempts=3:3 次连接尝试 → 3 次 refresh → reconnectFailed 停止。
168
172
  await new Promise((resolve, reject) => {
169
- const timer = setTimeout(() => reject(new Error('expected repeated reconnects')), 500);
170
- client.on('disconnected', () => {
171
- if (attempt >= 3) {
173
+ const timer = setTimeout(() => reject(new Error('expected reconnectFailed after refresh failures')), 1000);
174
+ client.on('reconnectFailed', () => {
175
+ clearTimeout(timer);
176
+ resolve();
177
+ });
178
+ });
179
+ // 停止后再观察一段窗口,确认没有继续 hammer 刷新接口
180
+ await new Promise((resolve) => setTimeout(resolve, 150));
181
+ assert.equal(attempt, 3, 'should stop reconnecting after refresh failures reach the limit');
182
+ assert.equal(refreshRequests, 3, 'one refresh per reconnect attempt, no extra hammering');
183
+ }
184
+ finally {
185
+ client.disconnect();
186
+ globalThis.fetch = originalFetch;
187
+ }
188
+ });
189
+ it('should keep slow-retrying after unchanged token refresh instead of giving up', async () => {
190
+ let refreshRequests = 0;
191
+ let tokenRejectedCount = 0;
192
+ const originalFetch = globalThis.fetch;
193
+ // bridge 始终返回与当前相同的 token(unchanged):换 token 无法解决 1008
194
+ globalThis.fetch = (async () => {
195
+ refreshRequests += 1;
196
+ return new Response(JSON.stringify({ runtimeAccessToken: 'stale-token' }), {
197
+ status: 200,
198
+ headers: { 'Content-Type': 'application/json' },
199
+ });
200
+ });
201
+ let attempt = 0;
202
+ const client = new WebSocketClient({
203
+ serverUrl: 'ws://localhost:8080/ws/agent',
204
+ heartbeatInterval: 1000,
205
+ heartbeatTimeout: 3000,
206
+ reconnectInterval: 5,
207
+ reconnectJitterMs: 0,
208
+ maxReconnectAttempts: -1,
209
+ runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
210
+ userId: 'user-a',
211
+ runtimeAccessToken: 'stale-token',
212
+ }, () => {
213
+ attempt += 1;
214
+ const socket = new FakeSocket();
215
+ setImmediate(() => {
216
+ socket.readyState = WebSocket.OPEN;
217
+ socket.emit('open');
218
+ socket.readyState = WebSocket.CLOSED;
219
+ socket.emit('close', 1008, Buffer.from('policy violation'));
220
+ });
221
+ return socket;
222
+ });
223
+ try {
224
+ client.on('error', () => {
225
+ // suppress unhandled error event in test environment
226
+ });
227
+ client.on('tokenRejected', () => {
228
+ tokenRejectedCount += 1;
229
+ });
230
+ await client.connect('agent-test');
231
+ // 旧行为:unchanged 后永久放弃重连(Agent 永久离线)。
232
+ // 新行为:慢速重试持续进行——重连与刷新以退避节奏继续,等待新 token 签发自愈。
233
+ await new Promise((resolve, reject) => {
234
+ const timer = setTimeout(() => reject(new Error('expected continued slow retries after unchanged refresh')), 2000);
235
+ const check = setInterval(() => {
236
+ if (attempt >= 4) {
237
+ clearInterval(check);
172
238
  clearTimeout(timer);
173
239
  resolve();
174
240
  }
175
- });
241
+ }, 5);
176
242
  });
177
- assert.equal(refreshRequests, 1);
243
+ assert.ok(attempt >= 4, 'reconnect attempts should continue after unchanged refresh');
244
+ assert.ok(tokenRejectedCount >= 3, 'each 1008 rejection with unchanged token should be observable');
245
+ assert.ok(refreshRequests >= 3, 'refresh should keep being attempted at backoff cadence');
178
246
  }
179
247
  finally {
248
+ client.disconnect();
249
+ // close 事件处理链是 async(token 刷新 → 慢速重连排程),与 disconnect 存在竞态:
250
+ // 等在飞链路落定后再清一次,避免残留的长退避 timer 拖住测试进程
251
+ await new Promise((resolve) => setTimeout(resolve, 50));
180
252
  client.disconnect();
181
253
  globalThis.fetch = originalFetch;
182
254
  }
@@ -196,6 +268,7 @@ describe('WebSocketClient reconnect behavior', () => {
196
268
  heartbeatInterval: 1000,
197
269
  heartbeatTimeout: 3000,
198
270
  reconnectInterval: 10,
271
+ reconnectJitterMs: 0,
199
272
  maxReconnectAttempts: 1,
200
273
  runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
201
274
  userId: 'user-a',
@@ -251,6 +324,7 @@ describe('WebSocketClient reconnect behavior', () => {
251
324
  heartbeatInterval: 1000,
252
325
  heartbeatTimeout: 3000,
253
326
  reconnectInterval: 10,
327
+ reconnectJitterMs: 0,
254
328
  maxReconnectAttempts: 5,
255
329
  runtimeBridgeBaseUrl: 'http://127.0.0.1:18081',
256
330
  userId: 'user-a',
@@ -311,6 +385,7 @@ describe('WebSocketClient reconnect behavior', () => {
311
385
  heartbeatInterval: 1000,
312
386
  heartbeatTimeout: 3000,
313
387
  reconnectInterval: 20,
388
+ reconnectJitterMs: 0,
314
389
  maxReconnectAttempts: -1
315
390
  }, (_url) => {
316
391
  const socket = new FakeSocket();
@@ -366,6 +441,7 @@ describe('WebSocketClient reconnect behavior', () => {
366
441
  heartbeatInterval: 1000,
367
442
  heartbeatTimeout: 3000,
368
443
  reconnectInterval,
444
+ reconnectJitterMs: 0,
369
445
  maxReconnectAttempts: 2,
370
446
  }, (_url) => {
371
447
  const socket = new FakeSocket();
@@ -421,6 +497,7 @@ describe('WebSocketClient reconnect behavior', () => {
421
497
  heartbeatInterval: 10,
422
498
  heartbeatTimeout: 20,
423
499
  reconnectInterval: 10,
500
+ reconnectJitterMs: 0,
424
501
  maxReconnectAttempts: 2,
425
502
  }, (_url) => {
426
503
  attempt += 1;
@@ -15,7 +15,7 @@
15
15
  "build": "tsc && node ../../scripts/minify-dist.mjs",
16
16
  "dev": "tsc --watch",
17
17
  "start": "node dist/index.js",
18
- "test": "npm run build && node --test dist",
18
+ "test": "npm run build && node --test dist/*.test.js",
19
19
  "prepare": "npm run build",
20
20
  "lint": "eslint src --max-warnings 0",
21
21
  "lint:fix": "eslint src --fix",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-runtime-bridge",
3
- "version": "1.9.120",
3
+ "version": "1.9.124",
4
4
  "description": "AgentsWorkStudio runtime bridge service for machine-level agent runtime integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -38,6 +38,7 @@
38
38
  "build:agent-mcp": "npm run build --prefix package/aws-client-agent-mcp",
39
39
  "build:acode": "npm run build --prefix package/acode",
40
40
  "build:sdk": "npm run build --prefix package/cc-switch-sdk",
41
+ "build:desktop": "node scripts/build-desktop-mcp.mjs",
41
42
  "build:all": "npm run build:sdk && npm run build:acode && npm run build:agent-mcp",
42
43
  "prebuild": "npm run build:all",
43
44
  "preinstall": "node scripts/ensure-build-tools.mjs",