@team-work/sandbox-client 0.1.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/cli/config-cmd.d.ts +3 -0
- package/dist/cli/config-cmd.js +60 -0
- package/dist/cli/config-cmd.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +30 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/list-cmd.d.ts +3 -0
- package/dist/cli/list-cmd.js +36 -0
- package/dist/cli/list-cmd.js.map +1 -0
- package/dist/cli/log-cmd.d.ts +3 -0
- package/dist/cli/log-cmd.js +118 -0
- package/dist/cli/log-cmd.js.map +1 -0
- package/dist/cli/start-cmd.d.ts +3 -0
- package/dist/cli/start-cmd.js +69 -0
- package/dist/cli/start-cmd.js.map +1 -0
- package/dist/cli/status-cmd.d.ts +3 -0
- package/dist/cli/status-cmd.js +46 -0
- package/dist/cli/status-cmd.js.map +1 -0
- package/dist/cli/stop-cmd.d.ts +3 -0
- package/dist/cli/stop-cmd.js +115 -0
- package/dist/cli/stop-cmd.js.map +1 -0
- package/dist/cli/update-cmd.d.ts +3 -0
- package/dist/cli/update-cmd.js +16 -0
- package/dist/cli/update-cmd.js.map +1 -0
- package/dist/client/frames.d.ts +111 -0
- package/dist/client/frames.js +22 -0
- package/dist/client/frames.js.map +1 -0
- package/dist/client/handshake.d.ts +7 -0
- package/dist/client/handshake.js +83 -0
- package/dist/client/handshake.js.map +1 -0
- package/dist/client/protocol.d.ts +36 -0
- package/dist/client/protocol.js +39 -0
- package/dist/client/protocol.js.map +1 -0
- package/dist/client/ws-client.d.ts +48 -0
- package/dist/client/ws-client.js +272 -0
- package/dist/client/ws-client.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +68 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/paths.d.ts +14 -0
- package/dist/config/paths.js +41 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.d.ts +17 -0
- package/dist/config/schema.js +52 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/daemon/detach.d.ts +17 -0
- package/dist/daemon/detach.js +30 -0
- package/dist/daemon/detach.js.map +1 -0
- package/dist/daemon/launch.d.ts +15 -0
- package/dist/daemon/launch.js +70 -0
- package/dist/daemon/launch.js.map +1 -0
- package/dist/daemon/signal.d.ts +7 -0
- package/dist/daemon/signal.js +22 -0
- package/dist/daemon/signal.js.map +1 -0
- package/dist/handlers/rpc/exec-shell.d.ts +13 -0
- package/dist/handlers/rpc/exec-shell.js +52 -0
- package/dist/handlers/rpc/exec-shell.js.map +1 -0
- package/dist/handlers/rpc/file-ops.d.ts +17 -0
- package/dist/handlers/rpc/file-ops.js +163 -0
- package/dist/handlers/rpc/file-ops.js.map +1 -0
- package/dist/handlers/rpc/router.d.ts +9 -0
- package/dist/handlers/rpc/router.js +98 -0
- package/dist/handlers/rpc/router.js.map +1 -0
- package/dist/handlers/terminal/manager.d.ts +20 -0
- package/dist/handlers/terminal/manager.js +75 -0
- package/dist/handlers/terminal/manager.js.map +1 -0
- package/dist/handlers/terminal/pty-session.d.ts +17 -0
- package/dist/handlers/terminal/pty-session.js +76 -0
- package/dist/handlers/terminal/pty-session.js.map +1 -0
- package/dist/handlers/terminal/router.d.ts +6 -0
- package/dist/handlers/terminal/router.js +25 -0
- package/dist/handlers/terminal/router.js.map +1 -0
- package/dist/instance/id.d.ts +5 -0
- package/dist/instance/id.js +17 -0
- package/dist/instance/id.js.map +1 -0
- package/dist/instance/store.d.ts +33 -0
- package/dist/instance/store.js +155 -0
- package/dist/instance/store.js.map +1 -0
- package/dist/instance/types.d.ts +28 -0
- package/dist/instance/types.js +7 -0
- package/dist/instance/types.js.map +1 -0
- package/dist/logging/logger.d.ts +11 -0
- package/dist/logging/logger.js +42 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/system/report.d.ts +2 -0
- package/dist/system/report.js +61 -0
- package/dist/system/report.js.map +1 -0
- package/dist/update/self-update.d.ts +8 -0
- package/dist/update/self-update.js +41 -0
- package/dist/update/self-update.js.map +1 -0
- package/dist/utils/info.d.ts +11 -0
- package/dist/utils/info.js +21 -0
- package/dist/utils/info.js.map +1 -0
- package/dist/utils/pem.d.ts +15 -0
- package/dist/utils/pem.js +31 -0
- package/dist/utils/pem.js.map +1 -0
- package/package.json +44 -0
- package/scripts/fix-pty-perms.js +30 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildUpdateCommand = buildUpdateCommand;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const self_update_js_1 = require("../update/self-update.js");
|
|
6
|
+
/** `tw update`:自更新 CLI 本身 */
|
|
7
|
+
function buildUpdateCommand() {
|
|
8
|
+
const cmd = new commander_1.Command('update');
|
|
9
|
+
cmd.description('自更新 tw CLI(npm i -g)').action(() => {
|
|
10
|
+
const result = (0, self_update_js_1.selfUpdate)();
|
|
11
|
+
console.log(result.message);
|
|
12
|
+
process.exit(result.changed ? 0 : 0);
|
|
13
|
+
});
|
|
14
|
+
return cmd;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=update-cmd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-cmd.js","sourceRoot":"","sources":["../../src/cli/update-cmd.ts"],"names":[],"mappings":";;AAKA,gDAQC;AAbD,yCAAoC;AAEpC,6DAAsD;AAEtD,6BAA6B;AAC7B,SAAgB,kBAAkB;IAChC,MAAM,GAAG,GAAG,IAAI,mBAAO,CAAC,QAAQ,CAAC,CAAC;IAClC,GAAG,CAAC,WAAW,CAAC,sBAAsB,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE;QAClD,MAAM,MAAM,GAAG,IAAA,2BAAU,GAAE,CAAC;QAC5B,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC5B,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ws 帧类型定义,严格对齐 sandbox 服务端 src/common/interfaces.ts。
|
|
3
|
+
* CLI → server:CliFrame;server → CLI:ServerToCliFrame。
|
|
4
|
+
* 传输格式:UTF-8 文本帧,每帧一个 JSON 对象,按 `type` 字段区分。
|
|
5
|
+
*/
|
|
6
|
+
/** 机器画像,对齐 sandbox SandboxConfig */
|
|
7
|
+
export interface SandboxConfig {
|
|
8
|
+
ip: string;
|
|
9
|
+
os: string;
|
|
10
|
+
cpu: string;
|
|
11
|
+
memory: number;
|
|
12
|
+
storage: number;
|
|
13
|
+
cliVersion: string;
|
|
14
|
+
}
|
|
15
|
+
export interface HelloFrame {
|
|
16
|
+
type: 'hello';
|
|
17
|
+
sandboxId: string;
|
|
18
|
+
privateKey: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ReportFrame {
|
|
21
|
+
type: 'report';
|
|
22
|
+
config: SandboxConfig;
|
|
23
|
+
}
|
|
24
|
+
export interface PingFrame {
|
|
25
|
+
type: 'ping';
|
|
26
|
+
}
|
|
27
|
+
export interface RpcResultFrame {
|
|
28
|
+
type: 'rpc-result';
|
|
29
|
+
reqId: string;
|
|
30
|
+
ok: boolean;
|
|
31
|
+
data?: unknown;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface RpcStreamFrame {
|
|
35
|
+
type: 'rpc-stream';
|
|
36
|
+
reqId: string;
|
|
37
|
+
stream: 'stdout' | 'stderr';
|
|
38
|
+
data: string;
|
|
39
|
+
}
|
|
40
|
+
export interface RpcEndFrame {
|
|
41
|
+
type: 'rpc-end';
|
|
42
|
+
reqId: string;
|
|
43
|
+
exitCode?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface TerminalDataFrame {
|
|
46
|
+
type: 'terminal.data';
|
|
47
|
+
sessionId: string;
|
|
48
|
+
data: string;
|
|
49
|
+
}
|
|
50
|
+
export interface TerminalExitFrame {
|
|
51
|
+
type: 'terminal.exit';
|
|
52
|
+
sessionId: string;
|
|
53
|
+
exitCode: number;
|
|
54
|
+
}
|
|
55
|
+
export type CliFrame = HelloFrame | ReportFrame | PingFrame | RpcResultFrame | RpcStreamFrame | RpcEndFrame | TerminalDataFrame | TerminalExitFrame;
|
|
56
|
+
export interface HelloAckFrame {
|
|
57
|
+
type: 'hello-ack';
|
|
58
|
+
ok: boolean;
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface ReportAckFrame {
|
|
62
|
+
type: 'report-ack';
|
|
63
|
+
ok: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface PongFrame {
|
|
66
|
+
type: 'pong';
|
|
67
|
+
}
|
|
68
|
+
export interface RpcFrame {
|
|
69
|
+
type: 'rpc';
|
|
70
|
+
reqId: string;
|
|
71
|
+
action: string;
|
|
72
|
+
params: Record<string, unknown>;
|
|
73
|
+
}
|
|
74
|
+
export interface TerminalOpenFrame {
|
|
75
|
+
type: 'terminal.open';
|
|
76
|
+
sessionId: string;
|
|
77
|
+
cols: number;
|
|
78
|
+
rows: number;
|
|
79
|
+
}
|
|
80
|
+
export interface TerminalInputFrame {
|
|
81
|
+
type: 'terminal.input';
|
|
82
|
+
sessionId: string;
|
|
83
|
+
data: string;
|
|
84
|
+
}
|
|
85
|
+
export interface TerminalResizeFrame {
|
|
86
|
+
type: 'terminal.resize';
|
|
87
|
+
sessionId: string;
|
|
88
|
+
cols: number;
|
|
89
|
+
rows: number;
|
|
90
|
+
}
|
|
91
|
+
export interface TerminalCloseFrame {
|
|
92
|
+
type: 'terminal.close';
|
|
93
|
+
sessionId: string;
|
|
94
|
+
}
|
|
95
|
+
export type ServerToCliFrame = HelloAckFrame | ReportAckFrame | PongFrame | RpcFrame | TerminalOpenFrame | TerminalInputFrame | TerminalResizeFrame | TerminalCloseFrame;
|
|
96
|
+
export declare function serializeFrame(frame: CliFrame): string;
|
|
97
|
+
export declare function parseServerFrame(text: string): ServerToCliFrame | null;
|
|
98
|
+
export interface DirEntry {
|
|
99
|
+
name: string;
|
|
100
|
+
isDir: boolean;
|
|
101
|
+
size: number;
|
|
102
|
+
}
|
|
103
|
+
export interface StatResult {
|
|
104
|
+
size: number;
|
|
105
|
+
isDir: boolean;
|
|
106
|
+
mtime: number;
|
|
107
|
+
}
|
|
108
|
+
export interface OkPathResult {
|
|
109
|
+
ok: true;
|
|
110
|
+
path: string;
|
|
111
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ws 帧类型定义,严格对齐 sandbox 服务端 src/common/interfaces.ts。
|
|
4
|
+
* CLI → server:CliFrame;server → CLI:ServerToCliFrame。
|
|
5
|
+
* 传输格式:UTF-8 文本帧,每帧一个 JSON 对象,按 `type` 字段区分。
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.serializeFrame = serializeFrame;
|
|
9
|
+
exports.parseServerFrame = parseServerFrame;
|
|
10
|
+
// ───────── 序列化 ─────────
|
|
11
|
+
function serializeFrame(frame) {
|
|
12
|
+
return JSON.stringify(frame);
|
|
13
|
+
}
|
|
14
|
+
function parseServerFrame(text) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(text);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=frames.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frames.js","sourceRoot":"","sources":["../../src/client/frames.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAuHH,wCAEC;AAED,4CAMC;AAZD,0BAA0B;AAE1B,SAAgB,cAAc,CAAC,KAAe;IAC5C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC;AAED,SAAgB,gBAAgB,CAAC,IAAY;IAC3C,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAqB,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import { Logger } from '../logging/logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* 握手编排:hello → hello-ack → report → report-ack。
|
|
5
|
+
* 返回 authed;鉴权失败/超时 reject。
|
|
6
|
+
*/
|
|
7
|
+
export declare function handshake(ws: WebSocket, sandboxId: string, privateKey: string, logger: Logger): Promise<boolean>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handshake = handshake;
|
|
4
|
+
const frames_js_1 = require("./frames.js");
|
|
5
|
+
const report_js_1 = require("../system/report.js");
|
|
6
|
+
const protocol_js_1 = require("./protocol.js");
|
|
7
|
+
/**
|
|
8
|
+
* 握手编排:hello → hello-ack → report → report-ack。
|
|
9
|
+
* 返回 authed;鉴权失败/超时 reject。
|
|
10
|
+
*/
|
|
11
|
+
function handshake(ws, sandboxId, privateKey, logger) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
const authTimer = setTimeout(() => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
reject(new Error('auth timeout'));
|
|
19
|
+
}, protocol_js_1.AUTH_TIMEOUT_MS);
|
|
20
|
+
const onMessage = (raw) => {
|
|
21
|
+
let text;
|
|
22
|
+
if (Buffer.isBuffer(raw))
|
|
23
|
+
text = raw.toString('utf8');
|
|
24
|
+
else if (Array.isArray(raw))
|
|
25
|
+
text = Buffer.concat(raw).toString('utf8');
|
|
26
|
+
else if (typeof raw === 'string')
|
|
27
|
+
text = raw;
|
|
28
|
+
else
|
|
29
|
+
text = Buffer.from(raw).toString('utf8');
|
|
30
|
+
let frame;
|
|
31
|
+
try {
|
|
32
|
+
frame = JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (frame.type === 'hello-ack') {
|
|
38
|
+
if (frame.ok) {
|
|
39
|
+
// 进入 report 阶段
|
|
40
|
+
const report = { type: 'report', config: (0, report_js_1.collectConfig)() };
|
|
41
|
+
ws.send((0, frames_js_1.serializeFrame)(report), (err) => {
|
|
42
|
+
if (err)
|
|
43
|
+
logger.error({ err: err.message }, 'send report failed');
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
settled = true;
|
|
48
|
+
clearTimeout(authTimer);
|
|
49
|
+
ws.off('message', onMessage);
|
|
50
|
+
reject(new Error(frame.error || 'hello-ack ok=false'));
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (frame.type === 'report-ack') {
|
|
55
|
+
if (settled)
|
|
56
|
+
return;
|
|
57
|
+
settled = true;
|
|
58
|
+
clearTimeout(authTimer);
|
|
59
|
+
ws.off('message', onMessage);
|
|
60
|
+
resolve(true);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
ws.on('message', onMessage);
|
|
64
|
+
// 发 hello
|
|
65
|
+
const hello = { type: 'hello', sandboxId, privateKey };
|
|
66
|
+
ws.send((0, frames_js_1.serializeFrame)(hello), (err) => {
|
|
67
|
+
if (err && !settled) {
|
|
68
|
+
settled = true;
|
|
69
|
+
clearTimeout(authTimer);
|
|
70
|
+
reject(new Error(`send hello failed: ${err.message}`));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// 提前 close
|
|
74
|
+
ws.once('close', () => {
|
|
75
|
+
if (!settled) {
|
|
76
|
+
settled = true;
|
|
77
|
+
clearTimeout(authTimer);
|
|
78
|
+
reject(new Error('socket closed during handshake'));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=handshake.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handshake.js","sourceRoot":"","sources":["../../src/client/handshake.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 协议常量,严格对齐 sandbox 服务端 src/common/constants.ts。
|
|
3
|
+
*/
|
|
4
|
+
/** ws 关闭码 */
|
|
5
|
+
export declare const WS_CLOSE_CODE: {
|
|
6
|
+
readonly AUTH_FAILED: 4001;
|
|
7
|
+
readonly KILLED: 4002;
|
|
8
|
+
readonly SESSION_INVALID: 4003;
|
|
9
|
+
readonly SANDBOX_OFFLINE: 4004;
|
|
10
|
+
readonly INTERNAL_ERROR: 4500;
|
|
11
|
+
};
|
|
12
|
+
/** RPC action 字符串,对齐 sandbox RpcAction 枚举 */
|
|
13
|
+
export declare const RpcAction: {
|
|
14
|
+
readonly READ_FILE: "read_file";
|
|
15
|
+
readonly WRITE_FILE: "write_file";
|
|
16
|
+
readonly LIST_DIR: "list_dir";
|
|
17
|
+
readonly MAKE_DIR: "make_dir";
|
|
18
|
+
readonly REMOVE: "remove";
|
|
19
|
+
readonly STAT: "stat";
|
|
20
|
+
readonly EXEC_SHELL: "exec_shell";
|
|
21
|
+
readonly UPLOAD_FILE: "upload_file";
|
|
22
|
+
readonly DOWNLOAD_FILE: "download_file";
|
|
23
|
+
};
|
|
24
|
+
export type RpcActionType = (typeof RpcAction)[keyof typeof RpcAction];
|
|
25
|
+
/** 心跳间隔(sandbox 约 90s 超时,25s 留余量) */
|
|
26
|
+
export declare const PING_INTERVAL_MS = 25000;
|
|
27
|
+
/** 心跳超时:连续 2 个 ping 周期未收到 pong 即判定失联(50s) */
|
|
28
|
+
export declare const PONG_TIMEOUT_MS: number;
|
|
29
|
+
/** 连续未收到 pong 的重连阈值 */
|
|
30
|
+
export declare const MAX_MISSED_PONG = 3;
|
|
31
|
+
/** 鉴权超时(sandbox 侧 10s) */
|
|
32
|
+
export declare const AUTH_TIMEOUT_MS = 12000;
|
|
33
|
+
/** 首次连接失败立即退出;运行中断线重连 */
|
|
34
|
+
export declare const MAX_RECONNECT_ATTEMPTS = 10;
|
|
35
|
+
/** 指数退避:1→2→4→8→…→60s 封顶 */
|
|
36
|
+
export declare const MAX_RECONNECT_DELAY_MS = 60000;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 协议常量,严格对齐 sandbox 服务端 src/common/constants.ts。
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MAX_RECONNECT_DELAY_MS = exports.MAX_RECONNECT_ATTEMPTS = exports.AUTH_TIMEOUT_MS = exports.MAX_MISSED_PONG = exports.PONG_TIMEOUT_MS = exports.PING_INTERVAL_MS = exports.RpcAction = exports.WS_CLOSE_CODE = void 0;
|
|
7
|
+
/** ws 关闭码 */
|
|
8
|
+
exports.WS_CLOSE_CODE = {
|
|
9
|
+
AUTH_FAILED: 4001,
|
|
10
|
+
KILLED: 4002,
|
|
11
|
+
SESSION_INVALID: 4003,
|
|
12
|
+
SANDBOX_OFFLINE: 4004,
|
|
13
|
+
INTERNAL_ERROR: 4500,
|
|
14
|
+
};
|
|
15
|
+
/** RPC action 字符串,对齐 sandbox RpcAction 枚举 */
|
|
16
|
+
exports.RpcAction = {
|
|
17
|
+
READ_FILE: 'read_file',
|
|
18
|
+
WRITE_FILE: 'write_file',
|
|
19
|
+
LIST_DIR: 'list_dir',
|
|
20
|
+
MAKE_DIR: 'make_dir',
|
|
21
|
+
REMOVE: 'remove',
|
|
22
|
+
STAT: 'stat',
|
|
23
|
+
EXEC_SHELL: 'exec_shell',
|
|
24
|
+
UPLOAD_FILE: 'upload_file',
|
|
25
|
+
DOWNLOAD_FILE: 'download_file',
|
|
26
|
+
};
|
|
27
|
+
/** 心跳间隔(sandbox 约 90s 超时,25s 留余量) */
|
|
28
|
+
exports.PING_INTERVAL_MS = 25_000;
|
|
29
|
+
/** 心跳超时:连续 2 个 ping 周期未收到 pong 即判定失联(50s) */
|
|
30
|
+
exports.PONG_TIMEOUT_MS = exports.PING_INTERVAL_MS * 2;
|
|
31
|
+
/** 连续未收到 pong 的重连阈值 */
|
|
32
|
+
exports.MAX_MISSED_PONG = 3;
|
|
33
|
+
/** 鉴权超时(sandbox 侧 10s) */
|
|
34
|
+
exports.AUTH_TIMEOUT_MS = 12_000;
|
|
35
|
+
/** 首次连接失败立即退出;运行中断线重连 */
|
|
36
|
+
exports.MAX_RECONNECT_ATTEMPTS = 10;
|
|
37
|
+
/** 指数退避:1→2→4→8→…→60s 封顶 */
|
|
38
|
+
exports.MAX_RECONNECT_DELAY_MS = 60_000;
|
|
39
|
+
//# sourceMappingURL=protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/client/protocol.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAEH,aAAa;AACA,QAAA,aAAa,GAAG;IAC3B,WAAW,EAAE,IAAI;IACjB,MAAM,EAAE,IAAI;IACZ,eAAe,EAAE,IAAI;IACrB,eAAe,EAAE,IAAI;IACrB,cAAc,EAAE,IAAI;CACZ,CAAC;AAEX,6CAA6C;AAChC,QAAA,SAAS,GAAG;IACvB,SAAS,EAAE,WAAW;IACtB,UAAU,EAAE,YAAY;IACxB,QAAQ,EAAE,UAAU;IACpB,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,QAAQ;IAChB,IAAI,EAAE,MAAM;IACZ,UAAU,EAAE,YAAY;IACxB,WAAW,EAAE,aAAa;IAC1B,aAAa,EAAE,eAAe;CACtB,CAAC;AAIX,qCAAqC;AACxB,QAAA,gBAAgB,GAAG,MAAM,CAAC;AAEvC,6CAA6C;AAChC,QAAA,eAAe,GAAG,wBAAgB,GAAG,CAAC,CAAC;AAEpD,uBAAuB;AACV,QAAA,eAAe,GAAG,CAAC,CAAC;AAEjC,0BAA0B;AACb,QAAA,eAAe,GAAG,MAAM,CAAC;AAEtC,yBAAyB;AACZ,QAAA,sBAAsB,GAAG,EAAE,CAAC;AAEzC,4BAA4B;AACf,QAAA,sBAAsB,GAAG,MAAM,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Logger } from '../logging/logger.js';
|
|
2
|
+
import { CliFrame } from './frames.js';
|
|
3
|
+
import { WsState } from '../instance/types.js';
|
|
4
|
+
export type ExitReason = 'auth-failed' | 'reconnect-exhausted' | 'signal' | 'kicked';
|
|
5
|
+
export interface WsClientOptions {
|
|
6
|
+
serverUrl: string;
|
|
7
|
+
sandboxId: string;
|
|
8
|
+
privateKey: string;
|
|
9
|
+
logger: Logger;
|
|
10
|
+
onWsState?: (s: WsState) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* WS 客户端 + 重连状态机 + 心跳。
|
|
14
|
+
*
|
|
15
|
+
* 状态:
|
|
16
|
+
* CONNECTING → AUTHING → CONNECTED → (断线) → RECONNECTING → CONNECTING ...
|
|
17
|
+
* 首次建连/鉴权失败 → 立即退出(spec:连接失败就退出)
|
|
18
|
+
* 运行中断线 → 指数退避重连,最多 MAX_RECONNECT_ATTEMPTS 次
|
|
19
|
+
*/
|
|
20
|
+
export declare class WsClient {
|
|
21
|
+
private ws;
|
|
22
|
+
private terminal;
|
|
23
|
+
private heartbeatTimer;
|
|
24
|
+
private missedPong;
|
|
25
|
+
private pongDeadline;
|
|
26
|
+
private firstAttempt;
|
|
27
|
+
private authed;
|
|
28
|
+
private shuttingDown;
|
|
29
|
+
private opts;
|
|
30
|
+
constructor(opts: WsClientOptions);
|
|
31
|
+
/**
|
|
32
|
+
* 主循环,resolve 时表示应退出。
|
|
33
|
+
* 失败计数器:连续失败累加,成功连接('closed' = 曾 authed 后断开)重置为 0。
|
|
34
|
+
* - 首次建连/鉴权失败 → 立即 exit(spec:连接失败就退出)
|
|
35
|
+
* - 运行中断线 → 指数退避重连,连续失败超 MAX_RECONNECT_ATTEMPTS 则退出
|
|
36
|
+
*/
|
|
37
|
+
run(): Promise<ExitReason>;
|
|
38
|
+
/** 优雅关闭 */
|
|
39
|
+
shutdown(): void;
|
|
40
|
+
private setState;
|
|
41
|
+
private connectOnce;
|
|
42
|
+
private onMessage;
|
|
43
|
+
private dispatch;
|
|
44
|
+
private send;
|
|
45
|
+
private startHeartbeat;
|
|
46
|
+
private stopHeartbeat;
|
|
47
|
+
}
|
|
48
|
+
export type { CliFrame };
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WsClient = void 0;
|
|
4
|
+
const ws_1 = require("ws");
|
|
5
|
+
const frames_js_1 = require("./frames.js");
|
|
6
|
+
const protocol_js_1 = require("./protocol.js");
|
|
7
|
+
const handshake_js_1 = require("./handshake.js");
|
|
8
|
+
const router_js_1 = require("../handlers/rpc/router.js");
|
|
9
|
+
const manager_js_1 = require("../handlers/terminal/manager.js");
|
|
10
|
+
const router_js_2 = require("../handlers/terminal/router.js");
|
|
11
|
+
/**
|
|
12
|
+
* WS 客户端 + 重连状态机 + 心跳。
|
|
13
|
+
*
|
|
14
|
+
* 状态:
|
|
15
|
+
* CONNECTING → AUTHING → CONNECTED → (断线) → RECONNECTING → CONNECTING ...
|
|
16
|
+
* 首次建连/鉴权失败 → 立即退出(spec:连接失败就退出)
|
|
17
|
+
* 运行中断线 → 指数退避重连,最多 MAX_RECONNECT_ATTEMPTS 次
|
|
18
|
+
*/
|
|
19
|
+
class WsClient {
|
|
20
|
+
ws = null;
|
|
21
|
+
terminal = null;
|
|
22
|
+
heartbeatTimer = null;
|
|
23
|
+
missedPong = 0;
|
|
24
|
+
pongDeadline = 0;
|
|
25
|
+
firstAttempt = true;
|
|
26
|
+
authed = false;
|
|
27
|
+
shuttingDown = false;
|
|
28
|
+
opts;
|
|
29
|
+
constructor(opts) {
|
|
30
|
+
this.opts = opts;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 主循环,resolve 时表示应退出。
|
|
34
|
+
* 失败计数器:连续失败累加,成功连接('closed' = 曾 authed 后断开)重置为 0。
|
|
35
|
+
* - 首次建连/鉴权失败 → 立即 exit(spec:连接失败就退出)
|
|
36
|
+
* - 运行中断线 → 指数退避重连,连续失败超 MAX_RECONNECT_ATTEMPTS 则退出
|
|
37
|
+
*/
|
|
38
|
+
async run() {
|
|
39
|
+
let attempts = 0;
|
|
40
|
+
while (true) {
|
|
41
|
+
if (this.shuttingDown)
|
|
42
|
+
return 'signal';
|
|
43
|
+
if (attempts === 0) {
|
|
44
|
+
this.setState(this.firstAttempt ? 'connecting' : 'reconnecting');
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const delay = Math.min(protocol_js_1.MAX_RECONNECT_DELAY_MS, 1000 * 2 ** (attempts - 1));
|
|
48
|
+
this.setState('reconnecting');
|
|
49
|
+
this.opts.logger.info({ attempt: attempts, delayMs: delay }, 'reconnect attempt');
|
|
50
|
+
await sleep(delay);
|
|
51
|
+
if (this.shuttingDown)
|
|
52
|
+
return 'signal';
|
|
53
|
+
}
|
|
54
|
+
const result = await this.connectOnce();
|
|
55
|
+
if (this.shuttingDown)
|
|
56
|
+
return 'signal';
|
|
57
|
+
if (result === 'auth-failed') {
|
|
58
|
+
if (this.firstAttempt)
|
|
59
|
+
return 'auth-failed';
|
|
60
|
+
attempts++;
|
|
61
|
+
if (attempts > protocol_js_1.MAX_RECONNECT_ATTEMPTS)
|
|
62
|
+
return 'reconnect-exhausted';
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (result === 'closed') {
|
|
66
|
+
// 曾成功 authed 后断开:重置失败计数,开启新一轮重连
|
|
67
|
+
this.firstAttempt = false;
|
|
68
|
+
attempts = 0;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** 优雅关闭 */
|
|
74
|
+
shutdown() {
|
|
75
|
+
this.shuttingDown = true;
|
|
76
|
+
this.setState('exiting');
|
|
77
|
+
this.stopHeartbeat();
|
|
78
|
+
this.terminal?.closeAll();
|
|
79
|
+
if (this.ws) {
|
|
80
|
+
try {
|
|
81
|
+
this.ws.close();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// ignore
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// ───── 内部 ─────
|
|
89
|
+
setState(s) {
|
|
90
|
+
this.opts.onWsState?.(s);
|
|
91
|
+
}
|
|
92
|
+
connectOnce() {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
let ws;
|
|
95
|
+
try {
|
|
96
|
+
ws = new ws_1.WebSocket(this.opts.serverUrl);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
this.opts.logger.error({ err: err instanceof Error ? err.message : String(err) }, 'ws create failed');
|
|
100
|
+
resolve('auth-failed');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.ws = ws;
|
|
104
|
+
this.setState(this.firstAttempt ? 'connecting' : 'reconnecting');
|
|
105
|
+
let settled = false;
|
|
106
|
+
const cleanup = () => {
|
|
107
|
+
// 握手阶段失败/超时:关闭并清理,避免泄漏残留监听
|
|
108
|
+
try {
|
|
109
|
+
ws.close();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// 已关闭则忽略
|
|
113
|
+
}
|
|
114
|
+
ws.removeAllListeners();
|
|
115
|
+
};
|
|
116
|
+
const done = (r) => {
|
|
117
|
+
if (settled)
|
|
118
|
+
return;
|
|
119
|
+
settled = true;
|
|
120
|
+
// auth-failed 与 signal 需要主动清理;closed 时 ws 已关
|
|
121
|
+
if (r !== 'closed')
|
|
122
|
+
cleanup();
|
|
123
|
+
resolve(r);
|
|
124
|
+
};
|
|
125
|
+
const onOpen = () => {
|
|
126
|
+
this.setState('authing');
|
|
127
|
+
(0, handshake_js_1.handshake)(ws, this.opts.sandboxId, this.opts.privateKey, this.opts.logger)
|
|
128
|
+
.then((ok) => {
|
|
129
|
+
if (!ok) {
|
|
130
|
+
done('auth-failed');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.authed = true;
|
|
134
|
+
this.firstAttempt = false;
|
|
135
|
+
this.setState('connected');
|
|
136
|
+
this.startHeartbeat(ws);
|
|
137
|
+
// 注册业务消息处理
|
|
138
|
+
this.terminal = new manager_js_1.TerminalManager((f) => this.send(ws, f), this.opts.logger);
|
|
139
|
+
ws.on('message', (raw) => this.onMessage(ws, raw));
|
|
140
|
+
ws.once('close', () => {
|
|
141
|
+
this.stopHeartbeat();
|
|
142
|
+
this.terminal?.closeAll();
|
|
143
|
+
this.terminal = null;
|
|
144
|
+
this.authed = false;
|
|
145
|
+
if (this.shuttingDown)
|
|
146
|
+
done('signal');
|
|
147
|
+
else
|
|
148
|
+
done('closed');
|
|
149
|
+
});
|
|
150
|
+
})
|
|
151
|
+
.catch((err) => {
|
|
152
|
+
this.opts.logger.error({ err: err instanceof Error ? err.message : String(err) }, 'handshake failed');
|
|
153
|
+
done('auth-failed');
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
ws.once('open', onOpen);
|
|
157
|
+
ws.once('error', (err) => {
|
|
158
|
+
this.opts.logger.warn({ err: err.message }, 'ws error');
|
|
159
|
+
if (!this.authed)
|
|
160
|
+
done('auth-failed');
|
|
161
|
+
// 已 authed 的运行中错误会触发 close,由 close 处理
|
|
162
|
+
});
|
|
163
|
+
ws.once('close', () => {
|
|
164
|
+
// 握手前就 close
|
|
165
|
+
if (!this.authed)
|
|
166
|
+
done('auth-failed');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
onMessage(ws, raw) {
|
|
171
|
+
let text;
|
|
172
|
+
if (Buffer.isBuffer(raw))
|
|
173
|
+
text = raw.toString('utf8');
|
|
174
|
+
else if (Array.isArray(raw))
|
|
175
|
+
text = Buffer.concat(raw).toString('utf8');
|
|
176
|
+
else if (typeof raw === 'string')
|
|
177
|
+
text = raw;
|
|
178
|
+
else
|
|
179
|
+
text = Buffer.from(raw).toString('utf8');
|
|
180
|
+
const frame = (0, frames_js_1.parseServerFrame)(text);
|
|
181
|
+
if (!frame) {
|
|
182
|
+
this.opts.logger.warn({ text: text.slice(0, 200) }, 'invalid frame');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
this.dispatch(ws, frame);
|
|
186
|
+
}
|
|
187
|
+
dispatch(ws, frame) {
|
|
188
|
+
const send = (f) => this.send(ws, f);
|
|
189
|
+
switch (frame.type) {
|
|
190
|
+
case 'pong':
|
|
191
|
+
this.missedPong = 0;
|
|
192
|
+
return;
|
|
193
|
+
case 'rpc':
|
|
194
|
+
(0, router_js_1.handleRpc)(frame, send, this.opts.logger);
|
|
195
|
+
return;
|
|
196
|
+
case 'terminal.open':
|
|
197
|
+
case 'terminal.input':
|
|
198
|
+
case 'terminal.resize':
|
|
199
|
+
case 'terminal.close':
|
|
200
|
+
if (this.terminal)
|
|
201
|
+
(0, router_js_2.handleTerminalFrame)(frame, this.terminal);
|
|
202
|
+
return;
|
|
203
|
+
case 'hello-ack':
|
|
204
|
+
case 'report-ack':
|
|
205
|
+
// 握手阶段已处理,理论上不会再到这
|
|
206
|
+
return;
|
|
207
|
+
default:
|
|
208
|
+
this.opts.logger.warn({ type: frame.type }, 'unknown frame type, ignore');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
send(ws, frameStr) {
|
|
212
|
+
if (ws.readyState !== ws_1.WebSocket.OPEN)
|
|
213
|
+
return;
|
|
214
|
+
ws.send(frameStr, (err) => {
|
|
215
|
+
if (err)
|
|
216
|
+
this.opts.logger.warn({ err: err.message }, 'send frame failed');
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
startHeartbeat(ws) {
|
|
220
|
+
this.missedPong = 0;
|
|
221
|
+
this.pongDeadline = 0;
|
|
222
|
+
this.heartbeatTimer = setInterval(() => {
|
|
223
|
+
if (ws.readyState !== ws_1.WebSocket.OPEN)
|
|
224
|
+
return;
|
|
225
|
+
// 半开连接兜底:服务端进程消失但 TCP 无 FIN 时,ws.send 的 callback 永不回调,
|
|
226
|
+
// missedPong 计数停滞、close 永不触发 → 卡死。用 terminate() 强制关底层 TCP(发 RST),
|
|
227
|
+
// 必然触发 close → 进入重连。上一轮 ping 超过 PONG_TIMEOUT_MS 未回即判定失联。
|
|
228
|
+
if (this.pongDeadline > 0 && Date.now() > this.pongDeadline) {
|
|
229
|
+
this.opts.logger.warn({ deadline: this.pongDeadline }, 'pong timeout, force terminate socket');
|
|
230
|
+
try {
|
|
231
|
+
ws.terminate();
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
// ignore
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const ping = { type: 'ping' };
|
|
239
|
+
ws.send((0, frames_js_1.serializeFrame)(ping), (err) => {
|
|
240
|
+
if (err) {
|
|
241
|
+
this.opts.logger.warn({ err: err.message }, 'ping send failed');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
// 兜底:ping 已发出但服务端无响应,50s 后强制 terminate 触发重连
|
|
245
|
+
this.pongDeadline = Date.now() + protocol_js_1.PONG_TIMEOUT_MS;
|
|
246
|
+
});
|
|
247
|
+
this.missedPong++;
|
|
248
|
+
if (this.missedPong >= protocol_js_1.MAX_MISSED_PONG) {
|
|
249
|
+
this.opts.logger.warn({ missed: this.missedPong }, 'too many missed pongs, force reconnect');
|
|
250
|
+
// 主动断开触发 close → 重连
|
|
251
|
+
try {
|
|
252
|
+
ws.close(protocol_js_1.WS_CLOSE_CODE.KILLED, 'heartbeat timeout');
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// ignore
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}, protocol_js_1.PING_INTERVAL_MS);
|
|
259
|
+
}
|
|
260
|
+
stopHeartbeat() {
|
|
261
|
+
if (this.heartbeatTimer) {
|
|
262
|
+
clearInterval(this.heartbeatTimer);
|
|
263
|
+
this.heartbeatTimer = null;
|
|
264
|
+
}
|
|
265
|
+
this.pongDeadline = 0;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
exports.WsClient = WsClient;
|
|
269
|
+
function sleep(ms) {
|
|
270
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=ws-client.js.map
|