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.
- package/dist/adapter/AcodeSdkAdapter.d.ts +18 -0
- package/dist/adapter/AcodeSdkAdapter.js +5 -5
- package/dist/adapter/types.d.ts +4 -0
- package/dist/adapter/types.js +1 -1
- package/dist/browser/browser-ws-bridge.js +3 -3
- package/dist/browser/built-in-browser-tools.js +1 -1
- package/dist/desktop/built-in-desktop-tools.d.ts +49 -0
- package/dist/desktop/built-in-desktop-tools.js +2 -0
- package/dist/desktop/desktop-mcp-client.d.ts +47 -0
- package/dist/desktop/desktop-mcp-client.js +3 -0
- package/dist/index.js +1 -1
- package/dist/remote-desktop/index.js +2 -2
- package/dist/routes/dashboard.js +521 -17
- package/dist/routes/git.d.ts +39 -0
- package/dist/routes/git.js +33 -25
- package/dist/routes/instance.js +1 -1
- package/dist/routes/jdks.d.ts +2 -0
- package/dist/routes/jdks.js +2 -0
- package/dist/routes/launch.d.ts +6 -0
- package/dist/routes/launch.js +30 -0
- package/dist/routes/pty.js +3 -3
- package/dist/routes/runtime-binding.js +1 -1
- package/dist/routes/terminal.js +30 -30
- package/dist/services/access-control.d.ts +18 -0
- package/dist/services/access-control.js +5 -0
- package/dist/services/agent-process-manager.js +1 -1
- package/dist/services/auto-register.js +1 -1
- package/dist/services/change-cache-retention.d.ts +5 -0
- package/dist/services/change-cache-retention.js +2 -0
- package/dist/services/desktop-mcp-manager.d.ts +47 -0
- package/dist/services/desktop-mcp-manager.js +2 -0
- package/dist/services/instance-features.d.ts +6 -0
- package/dist/services/instance-features.js +1 -1
- package/dist/services/jdk-scanner.d.ts +20 -0
- package/dist/services/jdk-scanner.js +2 -0
- package/dist/services/launch-manager.d.ts +181 -0
- package/dist/services/launch-manager.js +12 -0
- package/dist/services/launch-metrics.d.ts +89 -0
- package/dist/services/launch-metrics.js +4 -0
- package/dist/services/launch-streams.d.ts +24 -0
- package/dist/services/launch-streams.js +11 -0
- package/dist/services/mcp-launch-binding-queue.js +1 -1
- package/dist/services/panel-auth.d.ts +11 -1
- package/dist/services/panel-auth.js +1 -1
- package/dist/services/panel-env.d.ts +5 -0
- package/dist/services/panel-env.js +2 -0
- package/dist/services/port-detector.d.ts +3 -0
- package/dist/services/port-detector.js +6 -0
- package/dist/services/runtime-lifecycle-policy.d.ts +1 -0
- package/dist/services/runtime-lifecycle-policy.js +1 -1
- package/dist/services/system-metrics.js +1 -1
- package/dist/services/tunnel-client.d.ts +4 -0
- package/dist/services/tunnel-client.js +1 -1
- package/dist/services/workspace-files.d.ts +3 -0
- package/dist/services/workspace-files.js +1 -1
- package/dist/services/workspaceTaskChangeWatcher.d.ts +2 -0
- package/dist/services/workspaceTaskChangeWatcher.js +3 -3
- package/dist/utils/config-write-queue.d.ts +3 -0
- package/dist/utils/config-write-queue.js +2 -0
- package/package/acode/dist/built-in-file-tools.d.ts +2 -0
- package/package/acode/dist/runtime.d.ts +20 -0
- package/package/acode/dist/runtime.js +27 -27
- package/package/acode/dist/sub-agent-manager.d.ts +4 -0
- package/package/acode/dist/sub-agent-manager.js +1 -1
- package/package/acode/dist/sub-agent-tools.d.ts +6 -0
- package/package/acode/dist/sub-agent-tools.js +39 -10
- package/package/acode/dist/sub-agent-types.d.ts +2 -0
- package/package/acode/dist/tool-allocation.d.ts +55 -0
- package/package/acode/dist/tool-allocation.js +2 -0
- package/package/acode/dist/types.d.ts +6 -0
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts +24 -0
- package/package/aws-client-agent-mcp/dist/agent-client.js +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.test.js +19 -5
- package/package/aws-client-agent-mcp/dist/context-manager.test.js +6 -6
- package/package/aws-client-agent-mcp/dist/http-client.d.ts +2 -0
- package/package/aws-client-agent-mcp/dist/http-client.js +1 -1
- package/package/aws-client-agent-mcp/dist/index.js +1 -1
- package/package/aws-client-agent-mcp/dist/launch-config-tools.test.d.ts +1 -0
- package/package/aws-client-agent-mcp/dist/launch-config-tools.test.js +106 -0
- package/package/aws-client-agent-mcp/dist/logger.d.ts +4 -0
- package/package/aws-client-agent-mcp/dist/logger.js +2 -2
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +21 -0
- package/package/aws-client-agent-mcp/dist/mcp-server.js +25 -25
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +24 -16
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +20 -0
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +8 -8
- package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +55 -1
- package/package/aws-client-agent-mcp/dist/memory-store.test.js +5 -1
- package/package/aws-client-agent-mcp/dist/parent-lifecycle.d.ts +10 -0
- package/package/aws-client-agent-mcp/dist/parent-lifecycle.js +2 -0
- package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.d.ts +1 -0
- package/package/aws-client-agent-mcp/dist/parent-lifecycle.test.js +81 -0
- package/package/aws-client-agent-mcp/dist/status-reporter.js +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +4 -0
- package/package/aws-client-agent-mcp/dist/watchdog-worker.d.ts +1 -0
- package/package/aws-client-agent-mcp/dist/watchdog-worker.js +3 -0
- package/package/aws-client-agent-mcp/dist/watchdog.d.ts +17 -0
- package/package/aws-client-agent-mcp/dist/watchdog.js +2 -0
- package/package/aws-client-agent-mcp/dist/watchdog.test.d.ts +1 -0
- package/package/aws-client-agent-mcp/dist/watchdog.test.js +124 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +4 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.js +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js +83 -6
- package/package/aws-client-agent-mcp/package.json +1 -1
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
|
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
|
|
170
|
-
client.on('
|
|
171
|
-
|
|
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.
|
|
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.
|
|
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",
|