@cjwddz/mirror 0.0.18-alpha → 1.2.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.
Files changed (65) hide show
  1. package/README.md +89 -222
  2. package/dist/cli/client.d.ts +19 -0
  3. package/dist/cli/client.d.ts.map +1 -0
  4. package/dist/cli/client.js +107 -0
  5. package/dist/cli/client.js.map +1 -0
  6. package/dist/cli/index.d.ts +1 -1
  7. package/dist/cli/index.js +88 -14
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/server.d.ts +19 -0
  10. package/dist/cli/server.d.ts.map +1 -0
  11. package/dist/cli/server.js +82 -0
  12. package/dist/cli/server.js.map +1 -0
  13. package/dist/core/http-tunnel-client.d.ts +64 -0
  14. package/dist/core/http-tunnel-client.d.ts.map +1 -0
  15. package/dist/core/http-tunnel-client.js +315 -0
  16. package/dist/core/http-tunnel-client.js.map +1 -0
  17. package/dist/core/http-tunnel-protocol.d.ts +86 -0
  18. package/dist/core/http-tunnel-protocol.d.ts.map +1 -0
  19. package/dist/core/http-tunnel-protocol.js +29 -0
  20. package/dist/core/http-tunnel-protocol.js.map +1 -0
  21. package/dist/core/http-tunnel-server.d.ts +74 -0
  22. package/dist/core/http-tunnel-server.d.ts.map +1 -0
  23. package/dist/core/http-tunnel-server.js +317 -0
  24. package/dist/core/http-tunnel-server.js.map +1 -0
  25. package/dist/core/output-buffer.d.ts +42 -0
  26. package/dist/core/output-buffer.d.ts.map +1 -0
  27. package/dist/core/output-buffer.js +66 -0
  28. package/dist/core/output-buffer.js.map +1 -0
  29. package/dist/core/token-manager.d.ts +29 -0
  30. package/dist/core/token-manager.d.ts.map +1 -0
  31. package/dist/core/token-manager.js +90 -0
  32. package/dist/core/token-manager.js.map +1 -0
  33. package/dist/index.d.ts +4 -8
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +4 -8
  36. package/dist/index.js.map +1 -1
  37. package/package.json +12 -10
  38. package/dist/cli/host-impl.d.ts +0 -58
  39. package/dist/cli/host-impl.d.ts.map +0 -1
  40. package/dist/cli/host-impl.js +0 -547
  41. package/dist/cli/host-impl.js.map +0 -1
  42. package/dist/cli/host.d.ts +0 -9
  43. package/dist/cli/host.d.ts.map +0 -1
  44. package/dist/cli/host.js +0 -57
  45. package/dist/cli/host.js.map +0 -1
  46. package/dist/cli/link.d.ts +0 -6
  47. package/dist/cli/link.d.ts.map +0 -1
  48. package/dist/cli/link.js +0 -899
  49. package/dist/cli/link.js.map +0 -1
  50. package/dist/cli/ui/MirrorUI.d.ts +0 -22
  51. package/dist/cli/ui/MirrorUI.d.ts.map +0 -1
  52. package/dist/cli/ui/MirrorUI.js +0 -71
  53. package/dist/cli/ui/MirrorUI.js.map +0 -1
  54. package/dist/cli/ui/interactive-terminal.d.ts +0 -3
  55. package/dist/cli/ui/interactive-terminal.d.ts.map +0 -1
  56. package/dist/cli/ui/interactive-terminal.js +0 -150
  57. package/dist/cli/ui/interactive-terminal.js.map +0 -1
  58. package/dist/core/process-manager.d.ts +0 -19
  59. package/dist/core/process-manager.d.ts.map +0 -1
  60. package/dist/core/process-manager.js +0 -76
  61. package/dist/core/process-manager.js.map +0 -1
  62. package/dist/transport/pty.d.ts +0 -7
  63. package/dist/transport/pty.d.ts.map +0 -1
  64. package/dist/transport/pty.js +0 -26
  65. package/dist/transport/pty.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,gCAAgC,CAAC;AAC/C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,8BAA8B,CAAC"}
package/dist/index.js CHANGED
@@ -1,11 +1,7 @@
1
1
  /**
2
- * Mirror 包入口
2
+ * Mirror 包入口 - HTTP 隧道工具
3
3
  */
4
- export * from './core/protocol.js';
5
- export * from './core/state-machine.js';
6
- export * from './core/workspace.js';
7
- export * from './core/process-manager.js';
8
- export * from './core/file-sync.js';
9
- export * from './transport/websocket.js';
10
- export * from './transport/pty.js';
4
+ export * from './core/http-tunnel-protocol.js';
5
+ export * from './core/http-tunnel-server.js';
6
+ export * from './core/http-tunnel-client.js';
11
7
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,oBAAoB,CAAC;AACnC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,gCAAgC,CAAC;AAC/C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,8BAA8B,CAAC"}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cjwddz/mirror",
3
- "version": "0.0.18-alpha",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
- "description": "开发态远程工作空间镜像工具",
5
+ "description": "HTTP 隧道工具 - 将本地服务暴露到公网",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
8
  "bin": {
@@ -18,23 +18,25 @@
18
18
  },
19
19
  "keywords": [
20
20
  "mirror",
21
- "remote",
22
- "workspace",
23
- "development"
21
+ "tunnel",
22
+ "http",
23
+ "websocket",
24
+ "localhost",
25
+ "expose"
24
26
  ],
25
27
  "author": "",
26
28
  "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/LeeLejia/npm-packages-hub.git",
32
+ "directory": "packages/mirror"
33
+ },
27
34
  "dependencies": {
28
- "chokidar": "^3.6.0",
29
35
  "commander": "^12.0.0",
30
- "ink": "^6.6.0",
31
- "node-pty": "^1.0.0",
32
- "react": "^19.2.4",
33
36
  "ws": "^8.18.0"
34
37
  },
35
38
  "devDependencies": {
36
39
  "@types/node": "^20.0.0",
37
- "@types/react": "^19.2.11",
38
40
  "@types/ws": "^8.5.13",
39
41
  "@typescript-eslint/eslint-plugin": "^6.0.0",
40
42
  "@typescript-eslint/parser": "^6.0.0",
@@ -1,58 +0,0 @@
1
- /**
2
- * Host 实现类
3
- */
4
- interface HostOptions {
5
- port: number;
6
- workspaceDir: string;
7
- token?: string;
8
- metadataDir?: string;
9
- }
10
- export declare class Host {
11
- private transport;
12
- private stateMachine;
13
- private workspace;
14
- private processManager;
15
- private currentClientId;
16
- private currentClientWorkspacePath;
17
- private sessionToken;
18
- private options;
19
- private snapshotFiles;
20
- private isSnapshotInProgress;
21
- private serverVersion;
22
- private hasProgressLine;
23
- private isCommandRunning;
24
- constructor(options: HostOptions, serverVersion: string);
25
- private generateToken;
26
- /**
27
- * 辅助日志方法:在输出日志前清除进度条
28
- */
29
- private log;
30
- /**
31
- * 辅助错误日志方法:在输出错误日志前清除进度条
32
- */
33
- private logError;
34
- start(): Promise<void>;
35
- private getHostname;
36
- private handleConnection;
37
- private handleMessage;
38
- private handleHello;
39
- private handleSnapshotStart;
40
- private fileCount;
41
- private snapshotFileCount;
42
- private incrementalSyncCount;
43
- private receivedFileCount;
44
- private writtenFileCount;
45
- private skippedFileCount;
46
- private pendingFileDiffs;
47
- private handleFile;
48
- private handleSnapshotEnd;
49
- private handleFileDiff;
50
- private handleExec;
51
- private handleExecSignal;
52
- private sendError;
53
- private cleanup;
54
- private disconnect;
55
- stop(): Promise<void>;
56
- }
57
- export {};
58
- //# sourceMappingURL=host-impl.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"host-impl.d.ts","sourceRoot":"","sources":["../../src/cli/host-impl.ts"],"names":[],"mappings":"AAAA;;GAEG;AAqBH,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,IAAI;IACf,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,0BAA0B,CAAuB;IACzD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,aAAa,CAA6D;IAClF,OAAO,CAAC,oBAAoB,CAAkB;IAC9C,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,gBAAgB,CAAkB;gBAE9B,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM;IAUvD,OAAO,CAAC,aAAa;IAIrB;;OAEG;IACH,OAAO,CAAC,GAAG;IASX;;OAEG;IACH,OAAO,CAAC,QAAQ;IAaV,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB5B,OAAO,CAAC,WAAW;YAoBL,gBAAgB;YA8DhB,aAAa;YA4Bb,WAAW;YAqDX,mBAAmB;IAsCjC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,gBAAgB,CAAK;IAC7B,OAAO,CAAC,gBAAgB,CAA0B;YAEpC,UAAU;YAuDV,iBAAiB;YA2HjB,cAAc;YAiEd,UAAU;YAoEV,gBAAgB;IAmB9B,OAAO,CAAC,SAAS;YAUH,OAAO;YA0BP,UAAU;IAuBlB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAI5B"}
@@ -1,547 +0,0 @@
1
- /**
2
- * Host 实现类
3
- */
4
- import { randomBytes } from 'crypto';
5
- import { networkInterfaces } from 'os';
6
- import { WebSocketTransport } from '../transport/websocket.js';
7
- import { StateMachine, HostState } from '../core/state-machine.js';
8
- import { Workspace } from '../core/workspace.js';
9
- import { ProcessManager } from '../core/process-manager.js';
10
- import { computeHash, decompressContent } from '../core/file-sync.js';
11
- export class Host {
12
- transport;
13
- stateMachine;
14
- workspace = null;
15
- processManager;
16
- currentClientId = null;
17
- currentClientWorkspacePath = null;
18
- sessionToken;
19
- options;
20
- snapshotFiles = new Map();
21
- isSnapshotInProgress = false;
22
- serverVersion;
23
- hasProgressLine = false; // 标记是否有进度条在显示
24
- isCommandRunning = false; // 独立的命令执行标志位
25
- constructor(options, serverVersion) {
26
- this.transport = new WebSocketTransport();
27
- this.stateMachine = new StateMachine();
28
- this.processManager = new ProcessManager();
29
- this.options = options;
30
- this.serverVersion = serverVersion;
31
- // 如果提供了 token 就使用,否则自动生成
32
- this.sessionToken = options.token || this.generateToken();
33
- }
34
- generateToken() {
35
- return randomBytes(32).toString('base64url');
36
- }
37
- /**
38
- * 辅助日志方法:在输出日志前清除进度条
39
- */
40
- log(message) {
41
- if (this.hasProgressLine) {
42
- // 清除进度条并换行
43
- process.stdout.write('\r\x1b[K');
44
- this.hasProgressLine = false;
45
- }
46
- console.log(message);
47
- }
48
- /**
49
- * 辅助错误日志方法:在输出错误日志前清除进度条
50
- */
51
- logError(message, error) {
52
- if (this.hasProgressLine) {
53
- // 清除进度条并换行
54
- process.stdout.write('\r\x1b[K');
55
- this.hasProgressLine = false;
56
- }
57
- if (error !== undefined) {
58
- console.error(message, error);
59
- }
60
- else {
61
- console.error(message);
62
- }
63
- }
64
- async start() {
65
- const port = this.options.port;
66
- await this.transport.createServer(port, (ws) => {
67
- this.handleConnection(ws);
68
- });
69
- const hostname = this.getHostname();
70
- console.log('Mirror host started');
71
- console.log(`Listening on :${port}`);
72
- console.log('链接指令: ');
73
- console.log(`mirror link "${hostname}:${port}?token=${this.sessionToken}"`);
74
- console.log(`mirror link "wss://mirror.tri-bank.online?token=${this.sessionToken}" (如果部署在服务端)`);
75
- }
76
- getHostname() {
77
- // 尝试获取本机 IP
78
- const interfaces = networkInterfaces();
79
- for (const name of Object.keys(interfaces)) {
80
- const ifaces = interfaces[name];
81
- if (!ifaces)
82
- continue;
83
- for (const iface of ifaces) {
84
- // 跳过内部(即 127.0.0.1)和非 IPv4 地址
85
- if (iface.family === 'IPv4' && !iface.internal) {
86
- return iface.address;
87
- }
88
- }
89
- }
90
- // 如果没有找到外部 IP,返回 localhost
91
- return 'localhost';
92
- }
93
- async handleConnection(ws) {
94
- // 获取客户端地址信息,类型安全地访问 _socket
95
- const socket = Object.getOwnPropertyDescriptor(ws, '_socket')?.value;
96
- const clientAddress = socket?.remoteAddress ?? 'unknown';
97
- const clientPort = socket?.remotePort ?? 'unknown';
98
- this.log(`[${new Date().toISOString()}] 客户端连接: ${clientAddress}:${clientPort}`);
99
- // 清理旧连接(如果有活跃连接)
100
- if (this.currentClientId && this.stateMachine.getState() !== HostState.EMPTY) {
101
- this.log(`[${new Date().toISOString()}] 检测到旧连接,断开旧连接: ${this.currentClientId}`);
102
- // 断开旧连接时保留工作区
103
- await this.disconnect();
104
- }
105
- ws.on('message', async (data) => {
106
- try {
107
- const message = JSON.parse(data.toString());
108
- // 只记录关键消息,FILE 消息太多不记录
109
- if (message.type !== 'FILE') {
110
- if (message.type === 'FILE_DIFF') {
111
- const diffMsg = message;
112
- this.log(`[${new Date().toISOString()}] 收到消息: ${message.type} (${diffMsg.op}: ${diffMsg.path})`);
113
- }
114
- else {
115
- this.log(`[${new Date().toISOString()}] 收到消息: ${message.type}`);
116
- }
117
- }
118
- await this.handleMessage(ws, message);
119
- }
120
- catch (error) {
121
- this.logError(`[${new Date().toISOString()}] 处理消息失败:`, error);
122
- this.sendError(ws, 'Failed to handle message');
123
- }
124
- });
125
- ws.on('close', async () => {
126
- this.log(`[${new Date().toISOString()}] 客户端断开连接: ${this.currentClientId || 'unknown'}`);
127
- // 断开连接时保留工作区
128
- await this.disconnect();
129
- // 清除 currentClientId 和 currentClientWorkspacePath,以便下次连接时判断
130
- this.currentClientId = null;
131
- this.currentClientWorkspacePath = null;
132
- });
133
- ws.on('error', async (error) => {
134
- // 清除进度条后输出错误
135
- if (this.hasProgressLine) {
136
- process.stdout.write('\r\x1b[K');
137
- this.hasProgressLine = false;
138
- }
139
- console.error(`[${new Date().toISOString()}] WebSocket 错误:`, error);
140
- // 错误时也保留工作区
141
- await this.disconnect();
142
- this.currentClientId = null;
143
- this.currentClientWorkspacePath = null;
144
- });
145
- }
146
- async handleMessage(ws, message) {
147
- switch (message.type) {
148
- case 'HELLO':
149
- await this.handleHello(ws, message);
150
- break;
151
- case 'SNAPSHOT_START':
152
- await this.handleSnapshotStart(ws, message);
153
- break;
154
- case 'FILE':
155
- await this.handleFile(ws, message);
156
- break;
157
- case 'SNAPSHOT_END':
158
- await this.handleSnapshotEnd(ws, message);
159
- break;
160
- case 'FILE_DIFF':
161
- await this.handleFileDiff(ws, message);
162
- break;
163
- case 'EXEC':
164
- await this.handleExec(ws, message);
165
- break;
166
- case 'EXEC_SIGNAL':
167
- await this.handleExecSignal(ws, message);
168
- break;
169
- default:
170
- this.sendError(ws, `Unknown message type: ${message.type}`);
171
- }
172
- }
173
- async handleHello(ws, message) {
174
- // 验证 token
175
- if (message.token !== this.sessionToken) {
176
- this.logError(`[${new Date().toISOString()}] Token 验证失败`);
177
- this.sendError(ws, 'Invalid token');
178
- ws.close();
179
- return;
180
- }
181
- // 验证版本
182
- const clientVersion = message.clientVersion || 'unknown';
183
- if (clientVersion !== this.serverVersion) {
184
- this.logError(`[${new Date().toISOString()}] 版本不匹配: 客户端版本=${clientVersion}, 服务端版本=${this.serverVersion}`);
185
- this.sendError(ws, `Version mismatch: client version (${clientVersion}) != server version (${this.serverVersion}). Please update your mirror client.`);
186
- ws.close();
187
- return;
188
- }
189
- const newClientId = message.clientId;
190
- const newClientWorkspacePath = message.workspacePath || null;
191
- // 检查工作区元数据中的工作目录路径(用于判断是否为同一工作目录)
192
- const workspacePath = this.workspace?.getWorkspacePath();
193
- // 如果是不同工作目录连接,清理旧工作区
194
- if (this.workspace && workspacePath !== newClientWorkspacePath) {
195
- console.log(`[${new Date().toISOString()}] 检测到不同工作目录,清理旧工作区 (旧: ${workspacePath || 'unknown'}, 新: ${newClientWorkspacePath || 'unknown'})`);
196
- await this.cleanup();
197
- }
198
- else if (this.workspace && workspacePath === newClientWorkspacePath) {
199
- // 同一个工作目录重连,复用工作区
200
- this.log(`[${new Date().toISOString()}] 检测到同一工作目录重连,复用工作区 (${newClientWorkspacePath || 'unknown'})`);
201
- // 断开连接时已经保存了元数据,这里不需要额外操作
202
- }
203
- this.currentClientId = newClientId;
204
- this.currentClientWorkspacePath = newClientWorkspacePath;
205
- this.log(`[${new Date().toISOString()}] HELLO 成功,客户端 ID: ${newClientId}, 工作目录: ${newClientWorkspacePath || 'unknown'}, 版本: ${message.clientVersion || 'unknown'}`);
206
- this.stateMachine.transitionTo(HostState.SYNCING);
207
- this.snapshotFiles.clear();
208
- this.isSnapshotInProgress = false;
209
- }
210
- async handleSnapshotStart(ws, message) {
211
- if (this.stateMachine.getState() !== HostState.SYNCING) {
212
- this.sendError(ws, 'Invalid state for SNAPSHOT_START');
213
- return;
214
- }
215
- const totalFiles = message.totalFiles || 0;
216
- this.log(`[${new Date().toISOString()}] 开始接收快照,版本: ${message.version}${totalFiles > 0 ? `,预计文件数: ${totalFiles}` : ''}`);
217
- // 创建或复用 workspace(传入 clientId 和工作目录路径以便复用)
218
- // 元数据文件保存在工作区目录之外,避免污染同步目录
219
- if (!this.workspace) {
220
- this.workspace = await Workspace.create(this.options.workspaceDir, false, this.currentClientId || null, this.currentClientWorkspacePath || null, this.options.metadataDir // 传递元数据目录
221
- );
222
- if (this.currentClientId) {
223
- await this.workspace.setClientInfo(this.currentClientId, this.currentClientWorkspacePath || null);
224
- }
225
- }
226
- this.log(`[${new Date().toISOString()}] 工作空间路径: ${this.workspace.getPath()}`);
227
- this.isSnapshotInProgress = true;
228
- this.snapshotFiles.clear();
229
- this.fileCount = totalFiles; // 设置文件总数
230
- this.receivedFileCount = 0; // 重置接收计数
231
- this.writtenFileCount = 0; // 重置写入计数
232
- this.skippedFileCount = 0; // 重置跳过计数
233
- }
234
- fileCount = 0; // 文件计数器(用于显示总文件数)
235
- snapshotFileCount = 0; // 快照文件总数(初始同步的文件数)
236
- incrementalSyncCount = 0; // 增量同步计数器(FILE_DIFF 同步的文件数)
237
- receivedFileCount = 0; // 已接收文件数
238
- writtenFileCount = 0; // 已写入文件数
239
- skippedFileCount = 0; // 已跳过文件数(hash相同)
240
- pendingFileDiffs = new Set(); // 待同步文件集合(用于跟踪 pending 状态)
241
- async handleFile(ws, message) {
242
- if (!this.workspace) {
243
- this.sendError(ws, 'Workspace not initialized');
244
- return;
245
- }
246
- if (!this.isSnapshotInProgress) {
247
- // 忽略快照完成后收到的 FILE 消息(可能是网络延迟或客户端时序问题)
248
- this.log(`[${new Date().toISOString()}] 忽略快照完成后的 FILE 消息: ${message.path}`);
249
- return;
250
- }
251
- let content = Buffer.from(message.content, 'base64');
252
- // 如果是压缩的,先解压
253
- if (message.compressed) {
254
- try {
255
- content = Buffer.from(await decompressContent(content));
256
- }
257
- catch (error) {
258
- this.logError(`[${new Date().toISOString()}] 解压失败: ${message.path}`, error);
259
- this.sendError(ws, `Failed to decompress ${message.path}`);
260
- return;
261
- }
262
- }
263
- const hash = computeHash(content);
264
- // 验证 hash
265
- if (hash !== message.hash) {
266
- this.logError(`[${new Date().toISOString()}] Hash 验证失败: ${message.path}`);
267
- this.sendError(ws, `Hash mismatch for ${message.path}`);
268
- return;
269
- }
270
- // 检查文件是否已存在且 hash 相同(增量同步)
271
- const isSkipped = this.workspace.hasFile(message.path, message.hash);
272
- if (isSkipped) {
273
- this.skippedFileCount++;
274
- }
275
- // 记录到快照中(无论是否需要写入)
276
- this.snapshotFiles.set(message.path, { hash, content });
277
- this.receivedFileCount++;
278
- // 每 10 个文件显示一次接收进度
279
- if (this.receivedFileCount % 10 === 0 || this.receivedFileCount === this.fileCount) {
280
- const skippedInfo = this.skippedFileCount > 0 ? ` (跳过 ${this.skippedFileCount})` : '';
281
- const totalInfo = this.fileCount > 0 ? `/${this.fileCount}` : '';
282
- process.stdout.write(`\r[${new Date().toISOString()}] 已接收 ${this.receivedFileCount}${totalInfo} 个文件${skippedInfo}...`);
283
- this.hasProgressLine = true; // 标记有进度条在显示
284
- }
285
- }
286
- async handleSnapshotEnd(ws, message) {
287
- if (!this.workspace) {
288
- this.sendError(ws, 'Workspace not initialized');
289
- return;
290
- }
291
- if (!this.isSnapshotInProgress) {
292
- this.sendError(ws, 'SNAPSHOT not in progress');
293
- return;
294
- }
295
- // 清除接收进度显示(使用 ANSI 转义序列清除整行)
296
- process.stdout.write('\r\x1b[K');
297
- this.hasProgressLine = false; // 清除标记
298
- console.log(`[${new Date().toISOString()}] 快照接收完成,版本: ${message.version},文件数: ${this.snapshotFiles.size}`);
299
- // 准备写入文件
300
- const filesToWrite = [];
301
- const fileHashes = {};
302
- // 获取旧的文件列表(用于删除已移除的文件)
303
- const oldFiles = this.workspace.getOldFiles();
304
- const newFilePaths = new Set();
305
- for (const [path, { hash, content }] of this.snapshotFiles) {
306
- fileHashes[path] = hash;
307
- newFilePaths.add(path);
308
- // 只写入需要更新的文件(hash 不同的)
309
- if (!this.workspace.hasFile(path, hash)) {
310
- filesToWrite.push([path, content]);
311
- }
312
- }
313
- // 找出需要删除的文件(在旧文件中但不在新快照中)
314
- const filesToDelete = [];
315
- for (const oldPath of Object.keys(oldFiles)) {
316
- if (!newFilePaths.has(oldPath)) {
317
- filesToDelete.push(oldPath);
318
- }
319
- }
320
- const totalFiles = this.snapshotFiles.size;
321
- const needWriteCount = filesToWrite.length;
322
- const actualSkippedCount = totalFiles - needWriteCount;
323
- // 删除已移除的文件
324
- if (filesToDelete.length > 0) {
325
- this.log(`[${new Date().toISOString()}] 检测到 ${filesToDelete.length} 个文件已移除,开始删除...`);
326
- for (const path of filesToDelete) {
327
- await this.workspace.deleteFile(path);
328
- }
329
- this.log(`[${new Date().toISOString()}] 已删除 ${filesToDelete.length} 个文件`);
330
- }
331
- if (needWriteCount > 0) {
332
- this.log(`[${new Date().toISOString()}] 开始写入文件: ${needWriteCount}/${totalFiles} 需要更新...`);
333
- // 写入文件并显示进度
334
- for (let i = 0; i < filesToWrite.length; i++) {
335
- const [path, content] = filesToWrite[i];
336
- await this.workspace.writeFile(path, content);
337
- this.writtenFileCount++;
338
- // 每写入 10 个文件或最后一个文件时显示进度
339
- if ((i + 1) % 10 === 0 || i === filesToWrite.length - 1) {
340
- process.stdout.write(`\r[${new Date().toISOString()}] 已写入 ${this.writtenFileCount}/${needWriteCount} 个文件...`);
341
- this.hasProgressLine = true; // 标记有进度条在显示
342
- }
343
- }
344
- // 清除写入进度显示(使用 ANSI 转义序列清除整行)
345
- process.stdout.write('\r\x1b[K');
346
- this.hasProgressLine = false; // 清除标记
347
- }
348
- // 更新文件列表元数据
349
- await this.workspace.updateFileList(fileHashes);
350
- this.workspace.setVersion(message.version);
351
- this.stateMachine.transitionTo(HostState.READY);
352
- this.isSnapshotInProgress = false;
353
- const skippedInfo = actualSkippedCount > 0 ? ` (跳过 ${actualSkippedCount} 个未变更文件)` : '';
354
- this.log(`[${new Date().toISOString()}] 工作空间已就绪,可以执行命令${skippedInfo}`);
355
- // 记录快照文件数(用于后续的 totalSynced 计算)
356
- this.snapshotFileCount = this.snapshotFiles.size;
357
- // 发送文件同步状态消息(初始同步完成)
358
- ws.send(JSON.stringify({
359
- type: 'FILE_SYNC_STATUS',
360
- status: 'synced',
361
- totalSynced: this.snapshotFileCount,
362
- pendingCount: 0,
363
- }));
364
- // 发送 READY 消息通知客户端
365
- ws.send(JSON.stringify({
366
- type: 'READY',
367
- message: '工作空间已就绪',
368
- }));
369
- // 重置计数器(保留 snapshotFileCount 用于后续增量同步计数)
370
- this.snapshotFiles.clear();
371
- this.fileCount = 0;
372
- this.receivedFileCount = 0;
373
- this.writtenFileCount = 0;
374
- this.skippedFileCount = 0;
375
- this.incrementalSyncCount = 0; // 重置增量同步计数器
376
- this.pendingFileDiffs.clear(); // 清空待同步文件集合
377
- }
378
- async handleFileDiff(ws, message) {
379
- if (!this.workspace) {
380
- this.sendError(ws, 'Workspace not initialized');
381
- return;
382
- }
383
- const state = this.stateMachine.getState();
384
- // 只要状态是 READY,就可以同步文件(不管是否有命令在执行)
385
- if (state === HostState.READY) {
386
- // 通知客户端正在同步
387
- ws.send(JSON.stringify({
388
- type: 'FILE_SYNC_STATUS',
389
- status: 'syncing',
390
- path: message.path,
391
- op: message.op,
392
- }));
393
- // 立即应用文件变更
394
- if (message.op === 'update' && message.content) {
395
- let content = Buffer.from(message.content, 'base64');
396
- // 如果是压缩的,先解压
397
- if (message.compressed) {
398
- try {
399
- content = Buffer.from(await decompressContent(content));
400
- }
401
- catch (error) {
402
- this.logError(`[${new Date().toISOString()}] 解压失败: ${message.path}`, error);
403
- this.sendError(ws, `Failed to decompress ${message.path}`);
404
- return;
405
- }
406
- }
407
- const hash = computeHash(content);
408
- if (hash !== message.hash) {
409
- this.sendError(ws, `Hash mismatch for ${message.path}`);
410
- return;
411
- }
412
- await this.workspace.writeFile(message.path, content);
413
- }
414
- else if (message.op === 'delete') {
415
- await this.workspace.deleteFile(message.path);
416
- }
417
- this.workspace.setVersion(message.version);
418
- // 递增增量同步计数
419
- this.incrementalSyncCount++;
420
- // 通知客户端同步完成,包含统计信息
421
- ws.send(JSON.stringify({
422
- type: 'FILE_SYNC_STATUS',
423
- status: 'synced',
424
- path: message.path,
425
- op: message.op,
426
- totalSynced: this.snapshotFileCount + this.incrementalSyncCount, // 总同步数 = snapshot文件数 + 增量同步数
427
- pendingCount: 0, // 实时同步,无待同步文件
428
- }));
429
- }
430
- else {
431
- this.sendError(ws, `Invalid state for FILE_DIFF: ${state}`);
432
- }
433
- }
434
- async handleExec(ws, message) {
435
- if (!this.workspace) {
436
- this.sendError(ws, 'Workspace not initialized');
437
- return;
438
- }
439
- // 验证状态
440
- if (!this.stateMachine.canExec()) {
441
- this.sendError(ws, `Cannot exec in state: ${this.stateMachine.getState()}`);
442
- return;
443
- }
444
- // 验证版本
445
- if (message.version !== this.workspace.getCurrentVersion()) {
446
- this.sendError(ws, `Version mismatch: expected ${this.workspace.getCurrentVersion()}, got ${message.version}`);
447
- return;
448
- }
449
- console.log(`[${new Date().toISOString()}] 执行命令: ${message.command} (execId: ${message.execId})`);
450
- // 标记命令正在执行
451
- this.isCommandRunning = true;
452
- // 启动进程
453
- const ptyProcess = await this.processManager.spawn(message.execId, message.command, this.workspace.getPath());
454
- // 监听输出
455
- ptyProcess.onData((data) => {
456
- ws.send(JSON.stringify({
457
- type: 'EXEC_OUTPUT',
458
- execId: message.execId,
459
- stream: 'stdout',
460
- data,
461
- }));
462
- });
463
- // 监听退出
464
- ptyProcess.onExit(async (exit) => {
465
- console.log(`[${new Date().toISOString()}] 命令执行完成: ${message.command} (execId: ${message.execId}, exitCode: ${exit.exitCode})`);
466
- ws.send(JSON.stringify({
467
- type: 'EXEC_EXIT',
468
- execId: message.execId,
469
- code: exit.exitCode,
470
- }));
471
- // 清理进程
472
- this.processManager.removeProcess(message.execId);
473
- // 标记命令执行完成
474
- this.isCommandRunning = false;
475
- });
476
- }
477
- async handleExecSignal(ws, message) {
478
- const processInfo = this.processManager.getProcess(message.execId);
479
- if (!processInfo) {
480
- this.sendError(ws, `Process not found: ${message.execId}`);
481
- return;
482
- }
483
- this.log(`[${new Date().toISOString()}] 向进程发送信号: ${message.signal} (execId: ${message.execId})`);
484
- try {
485
- processInfo.process.kill(message.signal);
486
- }
487
- catch (error) {
488
- this.logError(`[${new Date().toISOString()}] 发送信号失败:`, error);
489
- this.sendError(ws, `Failed to send signal to process: ${message.execId}`);
490
- }
491
- }
492
- sendError(ws, message, code) {
493
- ws.send(JSON.stringify({
494
- type: 'ERROR',
495
- message,
496
- code,
497
- }));
498
- }
499
- async cleanup() {
500
- // 清理进程
501
- await this.processManager.killAll();
502
- // 清理 workspace(完全删除)
503
- if (this.workspace) {
504
- await this.workspace.cleanup();
505
- this.workspace = null;
506
- }
507
- // 重置状态
508
- this.stateMachine.reset();
509
- this.currentClientId = null;
510
- this.currentClientWorkspacePath = null;
511
- this.snapshotFiles.clear();
512
- this.isSnapshotInProgress = false;
513
- this.isCommandRunning = false;
514
- this.fileCount = 0;
515
- this.snapshotFileCount = 0;
516
- this.incrementalSyncCount = 0;
517
- this.receivedFileCount = 0;
518
- this.writtenFileCount = 0;
519
- this.skippedFileCount = 0;
520
- this.pendingFileDiffs.clear();
521
- }
522
- async disconnect() {
523
- // 断开连接时保留工作区
524
- // 清理进程
525
- await this.processManager.killAll();
526
- // 保存工作区元数据,但不删除工作区
527
- if (this.workspace) {
528
- await this.workspace.disconnect();
529
- // 不设置为 null,以便重连时复用
530
- }
531
- // 重置状态(但保留 workspace、currentClientId 和 currentClientWorkspacePath,以便识别重连)
532
- this.stateMachine.reset();
533
- this.snapshotFiles.clear();
534
- this.isSnapshotInProgress = false;
535
- this.isCommandRunning = false;
536
- this.fileCount = 0;
537
- this.receivedFileCount = 0;
538
- this.writtenFileCount = 0;
539
- this.skippedFileCount = 0;
540
- // 注意:不断开连接时不清除 currentClientId 和 currentClientWorkspacePath
541
- }
542
- async stop() {
543
- await this.cleanup();
544
- this.transport.close();
545
- }
546
- }
547
- //# sourceMappingURL=host-impl.js.map