@feng3d/ctc 0.0.10 → 0.0.12

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 (42) hide show
  1. package/README.md +17 -32
  2. package/dist/admin-server.d.ts +65 -12
  3. package/dist/admin-server.d.ts.map +1 -1
  4. package/dist/admin-server.js +291 -564
  5. package/dist/admin-server.js.map +1 -1
  6. package/dist/cli.d.ts +12 -3
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +931 -311
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +5 -1
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +20 -11
  13. package/dist/config.js.map +1 -1
  14. package/dist/controller.d.ts +27 -124
  15. package/dist/controller.d.ts.map +1 -1
  16. package/dist/controller.js +77 -139
  17. package/dist/controller.js.map +1 -1
  18. package/dist/data-channel.d.ts +76 -0
  19. package/dist/data-channel.d.ts.map +1 -0
  20. package/dist/data-channel.js +222 -0
  21. package/dist/data-channel.js.map +1 -0
  22. package/dist/forward-proxy.d.ts +100 -0
  23. package/dist/forward-proxy.d.ts.map +1 -0
  24. package/dist/forward-proxy.js +407 -0
  25. package/dist/forward-proxy.js.map +1 -0
  26. package/dist/handlers/unified-handler.d.ts +33 -75
  27. package/dist/handlers/unified-handler.d.ts.map +1 -1
  28. package/dist/handlers/unified-handler.js +191 -315
  29. package/dist/handlers/unified-handler.js.map +1 -1
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +135 -36
  33. package/dist/index.js.map +1 -1
  34. package/dist/ipc-handler.d.ts +52 -0
  35. package/dist/ipc-handler.d.ts.map +1 -0
  36. package/dist/ipc-handler.js +110 -0
  37. package/dist/ipc-handler.js.map +1 -0
  38. package/dist/proxy-manager.d.ts +13 -6
  39. package/dist/proxy-manager.d.ts.map +1 -1
  40. package/dist/proxy-manager.js +58 -23
  41. package/dist/proxy-manager.js.map +1 -1
  42. package/package.json +9 -5
package/dist/cli.js CHANGED
@@ -1,44 +1,171 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * @module cli
4
- * @description 穿透客户端命令行工具模块。
5
- * 提供 `feng3d-ctc` CLI 命令,支持启动、停止和查询客户端状态。
6
- * 单实例模式:只允许一个客户端实例运行,多次 start 会向已运行进程添加端口映射。
4
+ * @description 穿透客户端 CLI - 简洁的命令行工具
5
+ *
6
+ * 使用方法:
7
+ * npx @feng3d/cts start # 启动客户端(后台守护进程)
8
+ * npx @feng3d/cts stop # 停止客户端
9
+ * npx @feng3d/cts restart # 重启客户端
10
+ * npx @feng3d/cts status # 查看状态
11
+ * npx @feng3d/cts proxies # 管理代理映射
12
+ * npx @feng3d/cts config # 管理配置
13
+ * npx @feng3d/cts boot # 管理开机自启动
14
+ * npx @feng3d/cts logs # 查看日志
15
+ * npx @feng3d/cts open # 打开管理页面
7
16
  */
8
17
  import { Command } from 'commander';
9
18
  import chalk from 'chalk';
10
- import { readFileSync, writeFileSync, mkdirSync, unlinkSync, openSync, closeSync } from 'fs';
11
- import { join } from 'path';
19
+ import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync, openSync, closeSync, readSync, statSync } from 'fs';
20
+ import { join, dirname } from 'path';
12
21
  import { homedir, platform } from 'os';
13
22
  import { spawn, execSync } from 'child_process';
14
23
  import { fileURLToPath } from 'url';
15
- import { WebSocket } from 'ws';
16
- import { MessageType, createMessage } from '@feng3d/chuantou-shared';
17
- /** 客户端实例数据目录 */
24
+ import { registerBoot, unregisterBoot, isBootRegistered } from '@feng3d/chuantou-shared';
25
+ /** 数据目录 */
18
26
  const DATA_DIR = join(homedir(), '.chuantou');
19
- /** PID 文件路径 */
20
- const PID_FILE = join(DATA_DIR, 'client.pid');
21
- /** 添加代理请求目录 */
22
- const REQUEST_DIR = join(DATA_DIR, 'proxy-requests');
27
+ /** 客户端目录 */
28
+ const CLIENT_DIR = join(DATA_DIR, 'client');
29
+ /** 配置文件 */
30
+ const CONFIG_FILE = join(CLIENT_DIR, 'config.json');
31
+ /** PID 文件 */
32
+ const PID_FILE = join(CLIENT_DIR, 'client.pid');
33
+ /** 日志文件 */
34
+ const LOG_FILE = join(CLIENT_DIR, 'client.log');
35
+ /** 管理服务器 URL 默认值 */
36
+ const ADMIN_URL_DEFAULT = 'http://127.0.0.1:9001';
37
+ /** 管理服务器端口默认值 */
38
+ const ADMIN_PORT_DEFAULT = 9001;
23
39
  /**
24
- * 写入 PID 文件
40
+ * 帮助文本
25
41
  */
26
- function writePidFile(info) {
27
- mkdirSync(DATA_DIR, { recursive: true });
28
- writeFileSync(PID_FILE, JSON.stringify(info, null, 2));
42
+ const HELP_TEXT = `
43
+ ${chalk.bold('用法:')} cts <${chalk.bold('命令')}> [选项]
44
+
45
+ ${chalk.bold('命令:')}
46
+ ${chalk.cyan('start')} 启动客户端(后台守护进程)
47
+ ${chalk.cyan('stop')} 关闭客户端
48
+ ${chalk.cyan('restart')} 重启客户端
49
+ ${chalk.cyan('status')} 查看运行状态
50
+ ${chalk.cyan('proxies')} 管理反向代理映射
51
+ ${chalk.cyan('forward')} 管理正向穿透代理
52
+ ${chalk.cyan('config')} 管理配置
53
+ ${chalk.cyan('boot')} 管理开机自启动
54
+ ${chalk.cyan('logs')} 查看日志
55
+ ${chalk.cyan('open')} 打开管理页面
56
+
57
+ ${chalk.bold('全局选项:')}
58
+ ${chalk.yellow('-h, --help')} 显示帮助信息
59
+ ${chalk.yellow('-v, --version')} 显示版本号
60
+
61
+ ${chalk.bold('start 命令选项:')}
62
+ ${chalk.yellow('-s, --server <url>')} 服务器地址 (默认: ws://localhost:9000)
63
+ ${chalk.yellow('-t, --token <token>')} 认证令牌
64
+ ${chalk.yellow('-p, --proxies <proxies>')} 代理配置 (如: 8080:3000,8081:3001)
65
+ ${chalk.yellow('--no-boot')} 不注册开机自启动
66
+ ${chalk.yellow('-o, --open')} 启动后打开管理页面
67
+
68
+ ${chalk.bold('proxies 命令(反向代理):')}
69
+ ${chalk.yellow('list')} 列出所有代理映射
70
+ ${chalk.yellow('add <remote:local>')} 添加代理 (如: 8080:3000)
71
+ ${chalk.yellow('remove <port>')} 移除指定端口的代理
72
+ ${chalk.yellow('clear')} 清空所有代理
73
+
74
+ ${chalk.bold('forward 命令(正向穿透):')}
75
+ ${chalk.yellow('list')} 列出所有正向穿透代理
76
+ ${chalk.yellow('add <local:remote:client>')} 添加穿透 (如: 8080:3000:clientB)
77
+ ${chalk.yellow('remove <localPort>')} 移除指定本地端口的穿透
78
+ ${chalk.yellow('clients')} 查看在线客户端列表
79
+ ${chalk.yellow('register')} 注册到服务器启用正向穿透
80
+
81
+ ${chalk.bold('config 命令选项:')}
82
+ ${chalk.yellow('get [key]')} 获取配置项
83
+ ${chalk.yellow('set <key> <value>')} 设置配置项
84
+ ${chalk.yellow('list')} 列出所有配置
85
+ ${chalk.yellow('edit')} 编辑配置文件
86
+
87
+ ${chalk.bold('boot 命令选项:')}
88
+ ${chalk.yellow('enable')} 启用开机自启动
89
+ ${chalk.yellow('disable')} 禁用开机自启动
90
+
91
+ ${chalk.dim('配置文件位置:')} ${CONFIG_FILE}
92
+ ${chalk.dim('日志文件位置:')} ${LOG_FILE}
93
+ ${chalk.dim('管理页面:')} http://127.0.0.1:9001
94
+
95
+ ${chalk.dim('示例:')}
96
+ ${chalk.gray('# 启动客户端')}
97
+ cts start --server ws://192.168.1.100:9000 --token mytoken
98
+
99
+ ${chalk.gray('# 正向穿透模式:注册并添加映射')}
100
+ cts forward register --description "我的电脑"
101
+ cts forward clients
102
+ cts forward add 8080:3000:clientB
103
+ cts forward list
104
+
105
+ ${chalk.gray('# 反向代理模式:暴露本地服务')}
106
+ cts proxies add 8080:3000
107
+ cts proxies list
108
+
109
+ ${chalk.gray('# 管理配置')}
110
+ cts config set serverUrl ws://localhost:9000
111
+ cts config set token mytoken
112
+
113
+ ${chalk.gray('# 开机自启动')}
114
+ cts boot enable
115
+ `;
116
+ /**
117
+ * 版本号
118
+ */
119
+ const VERSION = '0.1.0';
120
+ /**
121
+ * 显示帮助信息
122
+ */
123
+ function showHelp() {
124
+ console.log(HELP_TEXT);
125
+ }
126
+ /**
127
+ * 显示版本号
128
+ */
129
+ function showVersion() {
130
+ console.log(`@feng3d/cts v${VERSION}`);
131
+ }
132
+ /**
133
+ * 读取配置文件
134
+ */
135
+ function readConfig() {
136
+ try {
137
+ const content = readFileSync(CONFIG_FILE, 'utf-8');
138
+ return JSON.parse(content);
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ }
144
+ /**
145
+ * 写入配置文件
146
+ */
147
+ function writeConfig(config) {
148
+ mkdirSync(CLIENT_DIR, { recursive: true });
149
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
29
150
  }
30
151
  /**
31
152
  * 读取 PID 文件
32
153
  */
33
154
  function readPidFile() {
34
155
  try {
35
- const content = readFileSync(PID_FILE, 'utf-8');
36
- return JSON.parse(content);
156
+ return JSON.parse(readFileSync(PID_FILE, 'utf-8'));
37
157
  }
38
158
  catch {
39
159
  return null;
40
160
  }
41
161
  }
162
+ /**
163
+ * 写入 PID 文件
164
+ */
165
+ function writePidFile(info) {
166
+ mkdirSync(CLIENT_DIR, { recursive: true });
167
+ writeFileSync(PID_FILE, JSON.stringify(info, null, 2));
168
+ }
42
169
  /**
43
170
  * 删除 PID 文件
44
171
  */
@@ -62,179 +189,184 @@ function isClientRunning() {
62
189
  return true;
63
190
  }
64
191
  catch {
65
- // 进程不存在,清理 PID 文件
66
- removePidFile();
67
192
  return false;
68
193
  }
69
194
  }
70
195
  /**
71
- * 在浏览器中打开 URL
196
+ * 获取客户端状态
72
197
  */
73
- function openBrowser(url) {
74
- const os = platform();
198
+ async function getClientStatus(adminPort = ADMIN_PORT_DEFAULT) {
75
199
  try {
76
- if (os === 'win32') {
77
- execSync(`cmd.exe /c start "" "${url}"`, { stdio: 'ignore' });
78
- }
79
- else if (os === 'darwin') {
80
- execSync(`open "${url}"`, { stdio: 'ignore' });
81
- }
82
- else {
83
- // Linux
84
- execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
85
- }
200
+ const res = await fetch(`http://127.0.0.1:${adminPort}/_ctc/status`, { method: 'GET' });
201
+ if (!res.ok)
202
+ return null;
203
+ const data = await res.json();
204
+ return data;
86
205
  }
87
206
  catch {
88
- // 忽略错误,浏览器打开失败不影响服务
207
+ return null;
89
208
  }
90
209
  }
91
- /** 通用服务器选项 */
92
- const serverOptions = [
93
- ['-s, --server <url>', '服务器地址 (如 ws://localhost:9000)', 'ws://localhost:9000'],
94
- ['-t, --token <token>', '认证令牌'],
95
- ['-p, --proxies <proxies>', '代理配置 (格式: remotePort:localPort:localHost,...)'],
96
- ['--reconnect-interval <ms>', '重连间隔(毫秒)', '5000'],
97
- ['--max-reconnect <number>', '最大重连次数', '10'],
98
- ];
210
+ /**
211
+ * 脚本路径
212
+ */
213
+ const cliPath = fileURLToPath(import.meta.url);
214
+ // index.js 是实际运行客户端的入口(包含管理服务器)
215
+ const indexPath = join(dirname(cliPath), 'index.js');
216
+ const nodePath = process.execPath;
217
+ /**
218
+ * 主程序入口
219
+ */
99
220
  const program = new Command();
100
221
  program
101
- .name('feng3d-ctc')
222
+ .name('@feng3d/cts')
102
223
  .description(chalk.blue('穿透 - 内网穿透客户端'))
103
- .version('0.0.5');
104
- // ====== start 命令 ======
105
- const startCmd = program.command('start').description('启动客户端(后台运行)');
106
- for (const opt of serverOptions) {
107
- if (opt.length === 3) {
108
- startCmd.option(opt[0], opt[1], opt[2]);
224
+ .version(VERSION)
225
+ .option('-h, --help', '显示帮助信息')
226
+ .hook('preAction', (thisCommand) => {
227
+ const options = thisCommand.opts();
228
+ if (options.help) {
229
+ showHelp();
230
+ process.exit(0);
109
231
  }
110
- else {
111
- startCmd.option(opt[0], opt[1]);
232
+ });
233
+ // ==================== start 命令 ====================
234
+ const startCmd = program.command('start')
235
+ .description('启动客户端(后台守护进程)')
236
+ .option('-s, --server <url>', '服务器地址')
237
+ .option('-t, --token <token>', '认证令牌')
238
+ .option('-p, --proxies <proxies>', '代理配置(格式:remotePort:localPort[:localHost],多个用逗号分隔)')
239
+ .option('--admin-port <port>', '管理页面端口(默认:9001)')
240
+ .option('--no-boot', '不注册开机自启动')
241
+ .option('-o, --open', '启动后打开管理页面')
242
+ .action(async (options) => {
243
+ // 检查是否已在运行
244
+ if (isClientRunning()) {
245
+ const info = readPidFile();
246
+ console.log(chalk.yellow('客户端已在运行中'));
247
+ console.log(chalk.gray(` PID: ${info?.pid}`));
248
+ console.log(chalk.gray(` 服务器: ${info?.serverUrl}`));
249
+ return;
112
250
  }
113
- }
114
- startCmd.option('--no-daemon', '前台运行(不作为后台守护进程)');
115
- startCmd.option('-o, --open', '启动后在浏览器中打开管理页面');
116
- startCmd.action(async (options) => {
117
- const serverUrl = options.server;
118
- const token = options.token;
119
- const proxiesStr = options.proxies;
120
- const shouldOpenBrowser = options.open;
121
- // 解析代理配置
122
- const proxies = [];
123
- if (proxiesStr) {
124
- for (const p of proxiesStr.split(',')) {
125
- const parts = p.trim().split(':');
126
- proxies.push({
127
- remotePort: parseInt(parts[0], 10),
128
- localPort: parseInt(parts[1], 10),
129
- localHost: parts[2] || 'localhost',
130
- });
251
+ // 加载配置
252
+ let serverUrl = options.server;
253
+ let token = options.token;
254
+ const proxiesArg = options.proxies;
255
+ // 如果命令行没有指定,从配置文件读取
256
+ if (!serverUrl || !token) {
257
+ const config = readConfig();
258
+ if (config) {
259
+ if (!serverUrl)
260
+ serverUrl = config.serverUrl;
261
+ if (!token)
262
+ token = config.token || '';
131
263
  }
132
264
  }
133
- // 检查客户端是否已运行
134
- if (isClientRunning()) {
135
- const info = readPidFile();
136
- // 检查服务器地址是否一致
137
- if (info.serverUrl !== serverUrl) {
138
- console.log(chalk.yellow('客户端正在运行,但连接到不同的服务器'));
139
- console.log(chalk.gray(` 当前: ${info.serverUrl}`));
140
- console.log(chalk.gray(` 新请求: ${serverUrl}`));
141
- console.log(chalk.yellow('请先停止当前客户端,或使用相同的服务器地址'));
142
- process.exit(1);
143
- }
144
- // 客户端已运行,添加代理映射
145
- console.log(chalk.green('客户端正在运行,添加新的代理映射...'));
146
- for (const proxy of proxies) {
147
- await addProxyToRunningClient(info.serverUrl, token, proxy);
265
+ // 如果仍然没有服务器地址,使用默认值
266
+ if (!serverUrl) {
267
+ serverUrl = 'ws://localhost:9000';
268
+ console.log(chalk.yellow('未指定服务器地址,使用默认值:ws://localhost:9000'));
269
+ }
270
+ // 解析代理配置
271
+ if (proxiesArg) {
272
+ const defaultConfig = {
273
+ serverUrl: '',
274
+ token: '',
275
+ reconnectInterval: 30000,
276
+ maxReconnectAttempts: 10,
277
+ proxies: []
278
+ };
279
+ const config = readConfig() || defaultConfig;
280
+ config.serverUrl = serverUrl;
281
+ config.token = token;
282
+ // 解析代理配置:remotePort:localPort[:localHost],多个用逗号分隔
283
+ const proxyList = proxiesArg.split(',').map((p) => p.trim()).filter((p) => p);
284
+ for (const proxyStr of proxyList) {
285
+ const parts = proxyStr.split(':');
286
+ if (parts.length < 2) {
287
+ console.log(chalk.yellow(`忽略无效的代理配置: ${proxyStr}`));
288
+ continue;
289
+ }
290
+ const remotePort = parseInt(parts[0], 10);
291
+ const localPort = parseInt(parts[1], 10);
292
+ const localHost = parts.length >= 3 ? parts[2] : 'localhost';
293
+ if (isNaN(remotePort) || isNaN(localPort)) {
294
+ console.log(chalk.yellow(`忽略无效的代理配置: ${proxyStr}`));
295
+ continue;
296
+ }
297
+ if (!config.proxies)
298
+ config.proxies = [];
299
+ // 检查是否已存在相同 remotePort
300
+ const existingIndex = config.proxies.findIndex(p => p.remotePort === remotePort);
301
+ if (existingIndex !== -1) {
302
+ config.proxies[existingIndex] = { remotePort, localPort, localHost };
303
+ }
304
+ else {
305
+ config.proxies.push({ remotePort, localPort, localHost });
306
+ }
148
307
  }
149
- return;
308
+ writeConfig(config);
150
309
  }
151
- // 构建 _serve 参数
152
- const serveArgs = [];
153
- serveArgs.push('--server', serverUrl);
310
+ // 启动参数
311
+ const serveArgs = ['--config', CONFIG_FILE];
312
+ if (serverUrl)
313
+ serveArgs.push('--server', serverUrl);
154
314
  if (token)
155
315
  serveArgs.push('--token', token);
156
- if (proxiesStr)
157
- serveArgs.push('--proxies', proxiesStr);
158
- serveArgs.push('--reconnect-interval', options.reconnectInterval);
159
- serveArgs.push('--max-reconnect', options.maxReconnect);
160
- // 是否后台运行
161
- const daemon = options.daemon !== false;
162
- if (daemon) {
163
- // 后台守护进程模式
164
- const scriptPath = fileURLToPath(import.meta.url);
165
- const nodePath = process.execPath;
166
- const logPath = join(DATA_DIR, 'client.log');
167
- // 打开日志文件
168
- const logFd = openSync(logPath, 'a');
169
- // 启动后台守护进程
170
- const child = spawn(nodePath, [scriptPath, '_serve', ...serveArgs], {
171
- detached: true,
172
- stdio: ['ignore', logFd, logFd],
173
- });
174
- child.unref();
175
- closeSync(logFd);
176
- // 等待一小段时间让进程启动
177
- await new Promise((r) => setTimeout(r, 500));
178
- // 验证进程是否启动成功
179
- const pid = child.pid;
180
- if (pid === undefined) {
181
- console.log(chalk.red('客户端启动失败,请查看日志文件:'));
182
- console.log(chalk.gray(` ${logPath}`));
183
- process.exit(1);
184
- }
185
- try {
186
- process.kill(pid, 0);
187
- }
188
- catch {
189
- console.log(chalk.red('客户端启动失败,请查看日志文件:'));
190
- console.log(chalk.gray(` ${logPath}`));
191
- process.exit(1);
192
- }
193
- // 写入 PID 文件
316
+ const adminPort = options.adminPort ? parseInt(options.adminPort, 10) : ADMIN_PORT_DEFAULT;
317
+ serveArgs.push('--admin-port', adminPort.toString());
318
+ // 打开日志文件
319
+ const logFd = openSync(LOG_FILE, 'a');
320
+ // 启动守护进程
321
+ const child = spawn(nodePath, [indexPath, ...serveArgs], {
322
+ detached: true,
323
+ stdio: ['ignore', logFd, logFd],
324
+ });
325
+ // 写入 PID 文件
326
+ if (child.pid !== undefined) {
194
327
  writePidFile({
328
+ pid: child.pid,
195
329
  serverUrl,
196
- pid,
330
+ adminPort,
197
331
  startedAt: Date.now(),
198
332
  });
199
- console.log(chalk.green('客户端已在后台启动'));
200
- console.log(chalk.gray(` PID: ${pid}`));
201
- console.log(chalk.gray(` 服务器: ${serverUrl}`));
202
- console.log(chalk.gray(` 日志: ${logPath}`));
203
- if (proxies.length > 0) {
204
- console.log(chalk.gray(` 代理映射:`));
205
- for (const proxy of proxies) {
206
- console.log(chalk.gray(` :${proxy.remotePort} -> ${proxy.localHost || 'localhost'}:${proxy.localPort}`));
207
- }
333
+ }
334
+ child.unref();
335
+ closeSync(logFd);
336
+ console.log(chalk.green('客户端已在后台启动'));
337
+ console.log(chalk.gray(` PID: ${child.pid}`));
338
+ console.log(chalk.gray(` 服务器: ${serverUrl}`));
339
+ console.log(chalk.gray(` 管理页面: http://127.0.0.1:${adminPort}`));
340
+ console.log(chalk.gray(` 配置: ${CONFIG_FILE}`));
341
+ console.log(chalk.gray(` 日志: ${LOG_FILE}`));
342
+ // 如果需要开机自启动
343
+ if (options.boot !== false) {
344
+ try {
345
+ registerBoot({
346
+ isServer: false,
347
+ nodePath,
348
+ scriptPath: indexPath,
349
+ args: serveArgs,
350
+ });
351
+ console.log(chalk.green('已启用开机自启动'));
208
352
  }
209
- // 打开浏览器
210
- if (shouldOpenBrowser) {
211
- setTimeout(() => {
212
- openBrowser('http://127.0.0.1:9001/');
213
- }, 2000);
353
+ catch (err) {
354
+ console.log(chalk.yellow(`注册开机自启动失败: ${err.message}`));
214
355
  }
215
356
  }
216
- else {
217
- // 前台运行模式
218
- await runServe(serverUrl, token, proxiesStr, options.reconnectInterval, options.maxReconnect, shouldOpenBrowser);
219
- }
220
- });
221
- // ====== _serve 命令(隐藏,前台运行)======
222
- const serveCmd = program.command('_serve', { hidden: true }).description('前台运行客户端(内部命令)');
223
- for (const opt of serverOptions) {
224
- if (opt.length === 3) {
225
- serveCmd.option(opt[0], opt[1], opt[2]);
226
- }
227
- else {
228
- serveCmd.option(opt[0], opt[1]);
357
+ // 打开管理页面
358
+ if (options.open) {
359
+ setTimeout(() => {
360
+ const adminPage = `http://127.0.0.1:${adminPort}`;
361
+ console.log(chalk.gray(`打开管理页面: ${adminPage}`));
362
+ const openCmd = platform() === 'win32' ? 'start' : 'open';
363
+ execSync(`${openCmd} ${adminPage}`, { stdio: 'ignore' });
364
+ }, 1000);
229
365
  }
230
- }
231
- serveCmd.action(async (options) => {
232
- await runServe(options.server, options.token, options.proxies, options.reconnectInterval, options.maxReconnect);
233
366
  });
234
- // ====== stop 命令 ======
235
- program
236
- .command('stop')
237
- .description('停止客户端')
367
+ // ==================== stop 命令 ====================
368
+ const stopCmd = program.command('stop')
369
+ .description('关闭客户端')
238
370
  .action(async () => {
239
371
  const info = readPidFile();
240
372
  if (!info) {
@@ -245,189 +377,677 @@ program
245
377
  process.kill(info.pid, 'SIGTERM');
246
378
  }
247
379
  catch (err) {
248
- console.log(chalk.yellow(`停止客户端失败: ${err instanceof Error ? err.message : err}`));
380
+ console.log(chalk.yellow(`关闭失败: ${err.message}`));
381
+ return;
249
382
  }
250
383
  removePidFile();
251
- console.log(chalk.green('客户端已停止'));
384
+ // 取消开机自启动
385
+ try {
386
+ unregisterBoot();
387
+ }
388
+ catch (err) {
389
+ // ignore
390
+ }
391
+ console.log(chalk.green('客户端已关闭'));
252
392
  });
253
- // ====== status 命令 ======
254
- program
255
- .command('status')
256
- .description('查询客户端状态')
393
+ // ==================== restart 命令 ====================
394
+ const restartCmd = program.command('restart')
395
+ .description('重启客户端')
396
+ .action(async () => {
397
+ console.log(chalk.blue('正在重启客户端...'));
398
+ const info = readPidFile();
399
+ const pid = info?.pid;
400
+ // 先停止
401
+ if (pid) {
402
+ try {
403
+ process.kill(pid, 'SIGTERM');
404
+ console.log(chalk.gray(`已发送停止信号到 PID ${pid}`));
405
+ }
406
+ catch (err) {
407
+ console.log(chalk.yellow(`停止失败: ${err.message}`));
408
+ }
409
+ }
410
+ // 等待进程结束
411
+ await new Promise(resolve => setTimeout(resolve, 2000));
412
+ // 清除 PID 文件,让 start 命令重新读取
413
+ removePidFile();
414
+ // 重新启动(不带参数,使用配置文件)
415
+ // 启动一个新的 start 命令进程
416
+ const startChild = spawn(nodePath, [cliPath, 'start'], {
417
+ detached: true,
418
+ stdio: 'ignore',
419
+ });
420
+ startChild.unref();
421
+ console.log(chalk.green('客户端已重启'));
422
+ // 等待客户端启动并显示状态
423
+ await new Promise(resolve => setTimeout(resolve, 2000));
424
+ const newInfo = readPidFile();
425
+ if (newInfo) {
426
+ console.log(chalk.gray(` PID: ${newInfo.pid}`));
427
+ console.log(chalk.gray(` 管理页面: http://127.0.0.1:${newInfo.adminPort}`));
428
+ }
429
+ else {
430
+ console.log(chalk.yellow(' 启动状态: 未知'));
431
+ }
432
+ });
433
+ // ==================== status 命令 ====================
434
+ const statusCmd = program.command('status')
435
+ .description('查看运行状态')
257
436
  .action(async () => {
258
437
  const info = readPidFile();
259
438
  if (!info) {
260
439
  console.log(chalk.yellow('客户端未在运行'));
440
+ console.log(chalk.gray('状态: 已停止'));
261
441
  return;
262
442
  }
263
- const uptime = Math.floor((Date.now() - info.startedAt) / 1000);
264
- const minutes = Math.floor(uptime / 60);
265
- const seconds = uptime % 60;
266
443
  console.log(chalk.blue.bold('穿透客户端状态'));
267
- console.log(chalk.gray(` 运行中: 是`));
268
- console.log(chalk.gray(` 服务器: ${info.serverUrl}`));
269
444
  console.log(chalk.gray(` PID: ${info.pid}`));
270
- console.log(chalk.gray(` 运行时长: ${minutes}分${seconds}秒`));
271
- // 尝试获取代理列表状态
445
+ console.log(chalk.gray(` 服务器: ${info.serverUrl}`));
446
+ console.log(chalk.gray(` 管理页面: http://127.0.0.1:${info.adminPort}`));
447
+ console.log(chalk.gray(` 配置: ${CONFIG_FILE}`));
448
+ console.log(chalk.gray(` 日志: ${LOG_FILE}`));
449
+ console.log(chalk.gray(` 启动时间: ${new Date(info.startedAt).toLocaleString('zh-CN')}`));
450
+ console.log(chalk.gray(` 开机启动: ${isBootRegistered() ? '已启用' : '未启用'}`));
451
+ // 获取详细状态
452
+ const status = await getClientStatus(info.adminPort);
453
+ if (status) {
454
+ const uptime = Math.floor(status.uptime / 1000);
455
+ const minutes = Math.floor(uptime / 60);
456
+ const seconds = uptime % 60;
457
+ console.log(chalk.gray(` 连接状态: ${status.authenticated ? '已认证' : status.connected ? '已连接' : '未连接'}`));
458
+ if (uptime > 0) {
459
+ console.log(chalk.gray(` 运行时长: ${minutes}分${seconds}秒`));
460
+ }
461
+ if (status.reconnectAttempts && status.reconnectAttempts > 0) {
462
+ console.log(chalk.gray(` 重连次数: ${status.reconnectAttempts}次`));
463
+ }
464
+ if (status.proxies.length === 0) {
465
+ console.log(chalk.gray(' 代理映射: 无'));
466
+ }
467
+ else {
468
+ console.log(chalk.gray(` 代理映射: ${status.proxies.length}个`));
469
+ for (const proxy of status.proxies) {
470
+ const index = proxy.index ? `#${proxy.index}` : ' -';
471
+ const target = proxy.localHost || 'localhost';
472
+ const registered = proxy.registered === false ? chalk.yellow(' (待注册)') : '';
473
+ console.log(chalk.gray(` ${index} ${proxy.remotePort} -> ${target}:${proxy.localPort}`) + registered);
474
+ }
475
+ }
476
+ }
477
+ else {
478
+ console.log(chalk.yellow('无法获取状态(管理服务器未就绪)'));
479
+ }
480
+ });
481
+ // ==================== proxies 命令 ====================
482
+ const proxiesCmd = program.command('proxies')
483
+ .description('管理代理映射')
484
+ .argument('[command]', '子命令:list, add, remove, clear', { default: 'list' });
485
+ // proxies list 子命令
486
+ proxiesCmd.command('list')
487
+ .description('列出现有代理映射')
488
+ .action(async () => {
489
+ const config = readConfig();
490
+ const proxies = config?.proxies || [];
491
+ if (proxies.length === 0) {
492
+ console.log(chalk.yellow('暂无代理映射'));
493
+ return;
494
+ }
495
+ console.log(chalk.blue.bold('代理映射列表:'));
496
+ for (let i = 0; i < proxies.length; i++) {
497
+ const proxy = proxies[i];
498
+ const index = proxy.index ? `#${proxy.index}` : ' -';
499
+ const target = proxy.localHost || 'localhost';
500
+ console.log(` ${chalk.cyan(index)} ${chalk.cyan(proxy.remotePort.toString())} -> ${chalk.cyan(target)}:${chalk.cyan(proxy.localPort.toString())}`);
501
+ }
502
+ });
503
+ // proxies add 子命令
504
+ proxiesCmd.command('add')
505
+ .description('添加代理映射')
506
+ .argument('<remote:local>', '远程端口:本地端口 (如: 8080:3000 或 8080:3000:192.168.1.100)')
507
+ .option('-h, --host <address>', '本地主机地址 (默认为 localhost)')
508
+ .action(async (remoteLocal, options) => {
509
+ // 解析参数
510
+ const parts = remoteLocal.split(':');
511
+ if (parts.length < 2) {
512
+ console.log(chalk.yellow('参数格式错误,应为:远程端口:本地端口'));
513
+ console.log(chalk.gray('示例:8080:3000 或 8080:3000:192.168.1.100'));
514
+ return;
515
+ }
516
+ const remotePort = parseInt(parts[0], 10);
517
+ let localPort;
518
+ let localHost = 'localhost';
519
+ // 检查是否有第三部分(主机地址)
520
+ if (parts.length >= 3) {
521
+ localPort = parseInt(parts[1], 10);
522
+ localHost = parts[2];
523
+ }
524
+ else {
525
+ localPort = parseInt(parts[1], 10);
526
+ }
527
+ // 验证端口
528
+ if (isNaN(remotePort) || remotePort < 1024 || remotePort > 65535) {
529
+ console.log(chalk.yellow('远程端口无效:1024-65535'));
530
+ return;
531
+ }
532
+ if (isNaN(localPort) || localPort < 1 || localPort > 65535) {
533
+ console.log(chalk.yellow('本地端口无效:1-65535'));
534
+ return;
535
+ }
536
+ // 通过 API 添加
272
537
  try {
273
- const proxies = await getProxiesFromRunningClient(info.serverUrl);
274
- if (proxies.length > 0) {
275
- console.log(chalk.gray(` 代理数量: ${proxies.length}`));
538
+ const info = readPidFile();
539
+ const adminPort = info?.adminPort ?? ADMIN_PORT_DEFAULT;
540
+ const res = await fetch(`http://127.0.0.1:${adminPort}/_ctc/proxies`, {
541
+ method: 'POST',
542
+ headers: { 'Content-Type': 'application/json' },
543
+ body: JSON.stringify({
544
+ remotePort,
545
+ localPort,
546
+ localHost,
547
+ }),
548
+ });
549
+ if (res.ok) {
550
+ console.log(chalk.green('代理映射已添加'));
551
+ // 更新本地配置文件
552
+ const config = readConfig();
553
+ if (config) {
554
+ if (!config.proxies)
555
+ config.proxies = [];
556
+ config.proxies.push({ remotePort, localPort, localHost });
557
+ writeConfig(config);
558
+ }
559
+ const target = localHost || 'localhost';
560
+ console.log(chalk.gray(` ${remotePort} -> ${target}:${localPort}`));
561
+ }
562
+ else if (res.status === 404) {
563
+ console.log(chalk.yellow('代理映射不存在(管理服务器未运行)'));
564
+ }
565
+ else {
566
+ const data = await res.json();
567
+ console.log(chalk.red(`添加失败: ${data.error || '未知错误'}`));
276
568
  }
277
569
  }
278
- catch {
279
- // 无法获取代理列表,可能客户端还未完全启动
570
+ catch (err) {
571
+ console.log(chalk.red(`请求失败: ${err.message}`));
280
572
  }
281
573
  });
282
- // ====== list 命令(列出代理)======
283
- program
284
- .command('list')
285
- .description('列出当前代理映射')
574
+ // proxies remove 子命令
575
+ proxiesCmd.command('remove')
576
+ .description('移除指定端口的代理映射')
577
+ .argument('<port>', '远程端口号')
578
+ .action(async (remotePort) => {
579
+ const port = parseInt(remotePort, 10);
580
+ if (isNaN(port) || port < 1024 || port > 65535) {
581
+ console.log(chalk.yellow('端口号无效:1024-65535'));
582
+ return;
583
+ }
584
+ // 通过 API 删除
585
+ try {
586
+ const info = readPidFile();
587
+ const adminPort = info?.adminPort ?? ADMIN_PORT_DEFAULT;
588
+ const res = await fetch(`http://127.0.0.1:${adminPort}/_ctc/proxies/${port}`, {
589
+ method: 'DELETE',
590
+ });
591
+ if (res.ok) {
592
+ console.log(chalk.green(`端口 ${port} 的代理映射已移除`));
593
+ // 更新本地配置文件
594
+ const config = readConfig();
595
+ if (config && config.proxies) {
596
+ const index = config.proxies.findIndex(p => p.remotePort === port);
597
+ if (index !== -1) {
598
+ config.proxies.splice(index, 1);
599
+ writeConfig(config);
600
+ }
601
+ }
602
+ }
603
+ else if (res.status === 404) {
604
+ console.log(chalk.yellow('代理映射不存在'));
605
+ }
606
+ else {
607
+ const data = await res.json();
608
+ console.log(chalk.red(`移除失败: ${data.error || '未知错误'}`));
609
+ }
610
+ }
611
+ catch (err) {
612
+ console.log(chalk.red(`请求失败: ${err.message}`));
613
+ }
614
+ });
615
+ // proxies clear 子命令
616
+ proxiesCmd.command('clear')
617
+ .description('清空所有代理映射')
286
618
  .action(async () => {
287
- const info = readPidFile();
288
- if (!info) {
289
- console.log(chalk.yellow('客户端未在运行'));
619
+ try {
620
+ const info = readPidFile();
621
+ const adminPort = info?.adminPort ?? ADMIN_PORT_DEFAULT;
622
+ const res = await fetch(`http://127.0.0.1:${adminPort}/_ctc/proxies`, {
623
+ method: 'DELETE',
624
+ });
625
+ if (res.ok) {
626
+ console.log(chalk.green('所有代理映射已清空'));
627
+ // 更新本地配置文件
628
+ const config = readConfig();
629
+ if (config) {
630
+ config.proxies = [];
631
+ writeConfig(config);
632
+ }
633
+ }
634
+ else {
635
+ const data = await res.json();
636
+ console.log(chalk.red(`清空失败: ${data.error || '未知错误'}`));
637
+ }
638
+ }
639
+ catch (err) {
640
+ console.log(chalk.red(`请求失败: ${err.message}`));
641
+ }
642
+ });
643
+ // ==================== config 命令 ====================
644
+ const configCmd = program.command('config')
645
+ .description('管理配置')
646
+ .argument('[command]', '子命令:get, set, list, edit', { default: 'list' });
647
+ // config get 子命令
648
+ configCmd.command('get')
649
+ .description('获取配置项')
650
+ .argument('<key>', '配置项名称 (如: serverUrl, token, reconnectInterval, maxReconnectAttempts)')
651
+ .action(async (key) => {
652
+ const config = readConfig();
653
+ if (!config) {
654
+ console.log(chalk.yellow('配置文件不存在'));
655
+ return;
656
+ }
657
+ const value = config[key];
658
+ if (value === undefined) {
659
+ console.log(chalk.yellow(`配置项 "${key}" 不存在`));
660
+ console.log(chalk.gray('可用配置项:serverUrl, token, reconnectInterval, maxReconnectAttempts, proxies'));
661
+ return;
662
+ }
663
+ // 显示值
664
+ if (typeof value === 'object') {
665
+ console.log(chalk.green(`${key}:`));
666
+ console.log(chalk.gray(JSON.stringify(value, null, 2)));
667
+ }
668
+ else {
669
+ console.log(chalk.green(`${key}: ${value}`));
670
+ }
671
+ });
672
+ // config set 子命令
673
+ configCmd.command('set')
674
+ .description('设置配置项')
675
+ .argument('<key>', '配置项名称')
676
+ .argument('<value>', '配置值')
677
+ .action(async (key, value) => {
678
+ // 读取现有配置
679
+ const config = readConfig() || {};
680
+ const oldValue = config[key];
681
+ // 解析值
682
+ let parsedValue = value;
683
+ if (key === 'serverUrl' || key === 'token') {
684
+ parsedValue = value;
685
+ }
686
+ else if (key === 'reconnectInterval' || key === 'maxReconnectAttempts') {
687
+ parsedValue = parseInt(value, 10);
688
+ }
689
+ else if (key === 'proxies') {
690
+ try {
691
+ parsedValue = JSON.parse(value);
692
+ }
693
+ catch {
694
+ console.log(chalk.yellow('proxies 必须是有效的 JSON 数组'));
695
+ return;
696
+ }
697
+ }
698
+ // 更新配置
699
+ config[key] = parsedValue;
700
+ writeConfig(config);
701
+ if (oldValue !== undefined) {
702
+ console.log(chalk.green(`已更新:${key}`));
703
+ console.log(chalk.gray(` 旧值: ${typeof oldValue === 'object' ? JSON.stringify(oldValue) : oldValue}`));
704
+ console.log(chalk.gray(` 新值: ${typeof parsedValue === 'object' ? JSON.stringify(parsedValue) : parsedValue}`));
705
+ }
706
+ else {
707
+ console.log(chalk.green(`已设置:${key} = ${typeof parsedValue === 'object' ? JSON.stringify(parsedValue) : parsedValue}`));
708
+ }
709
+ });
710
+ // config list 子命令
711
+ configCmd.command('list')
712
+ .description('列出所有配置项')
713
+ .action(async () => {
714
+ const config = readConfig();
715
+ if (!config) {
716
+ console.log(chalk.yellow('配置文件不存在'));
290
717
  return;
291
718
  }
719
+ console.log(chalk.blue.bold('配置列表:'));
720
+ console.log(` ${chalk.cyan('serverUrl')}: ${chalk.gray(config.serverUrl || '(未设置)')}`);
721
+ console.log(` ${chalk.cyan('token')}: ${chalk.gray(config.token || '(未设置)')}`);
722
+ console.log(` ${chalk.cyan('proxies')}: ${chalk.gray(`[${config.proxies?.map(p => `${p.remotePort}:${p.localPort}`).join(', ') || '(空)'}]`)}`);
723
+ console.log(` ${chalk.cyan('reconnectInterval')}: ${chalk.gray(config.reconnectInterval || 30000)}ms`);
724
+ console.log(` ${chalk.cyan('maxReconnectAttempts')}: ${chalk.gray(config.maxReconnectAttempts || 10)}`);
725
+ });
726
+ // config edit 子命令
727
+ configCmd.command('edit')
728
+ .description('编辑配置文件')
729
+ .action(async () => {
730
+ const editors = {
731
+ win32: 'notepad',
732
+ darwin: 'open -a TextEdit',
733
+ linux: 'nano',
734
+ };
735
+ const editor = editors[platform()] || 'vi';
736
+ const configPath = CONFIG_FILE;
737
+ console.log(chalk.gray(`打开配置文件:${configPath}`));
738
+ console.log(chalk.gray(`使用编辑器:${editor}`));
292
739
  try {
293
- const proxies = await getProxiesFromRunningClient(info.serverUrl);
740
+ execSync(`${editor} "${configPath}"`, { stdio: 'ignore' });
741
+ }
742
+ catch (err) {
743
+ console.log(chalk.yellow(`打开失败: ${err.message}`));
744
+ console.log(chalk.gray(`请手动打开:`));
745
+ }
746
+ });
747
+ // ==================== forward 命令(正向穿透模式)====================
748
+ const forwardCmd = program.command('forward')
749
+ .description('管理正向穿透代理(本地端口 -> 远程客户端端口)')
750
+ .argument('[command]', '子命令:list, add, remove, clients, register', { default: 'list' });
751
+ // forward list 子命令
752
+ forwardCmd.command('list')
753
+ .description('列出所有正向穿透代理')
754
+ .action(async () => {
755
+ try {
756
+ const info = readPidFile();
757
+ const adminPort = info?.adminPort ?? ADMIN_PORT_DEFAULT;
758
+ const res = await fetch(`http://127.0.0.1:${adminPort}/_ctc/forward/list`);
759
+ if (!res.ok) {
760
+ console.log(chalk.yellow('无法获取代理列表(客户端未运行或功能不可用)'));
761
+ return;
762
+ }
763
+ const data = await res.json();
764
+ const proxies = data.proxies || [];
294
765
  if (proxies.length === 0) {
295
- console.log(chalk.yellow('没有代理映射'));
766
+ console.log(chalk.yellow('暂无正向穿透代理'));
296
767
  return;
297
768
  }
298
- console.log(chalk.blue.bold('当前代理映射:'));
299
- console.log();
769
+ console.log(chalk.blue.bold('正向穿透代理列表:'));
300
770
  for (const proxy of proxies) {
301
- console.log(chalk.gray(` :${proxy.remotePort} -> ${proxy.localHost || 'localhost'}:${proxy.localPort}`));
771
+ const status = proxy.enabled ? chalk.green('●') : chalk.gray('');
772
+ console.log(` ${status} 本地 :${proxy.localPort} → ${chalk.cyan(proxy.targetClientId)}:${proxy.targetPort}`);
302
773
  }
303
774
  }
304
775
  catch (err) {
305
- console.log(chalk.yellow(`获取代理列表失败: ${err instanceof Error ? err.message : err}`));
776
+ console.log(chalk.red(`请求失败: ${err.message}`));
306
777
  }
307
778
  });
308
- /**
309
- * 向正在运行的客户端添加代理映射
310
- */
311
- async function addProxyToRunningClient(serverUrl, token, proxy) {
312
- return new Promise((resolve, reject) => {
313
- const ws = new WebSocket(serverUrl);
314
- const timeout = setTimeout(() => {
315
- ws.close();
316
- reject(new Error('连接超时'));
317
- }, 5000);
318
- ws.on('open', async () => {
319
- clearTimeout(timeout);
320
- // 发送认证
321
- const authMsg = createMessage(MessageType.AUTH, {
322
- token: token || '',
323
- });
324
- ws.send(JSON.stringify(authMsg));
325
- // 等待认证响应
326
- ws.once('message', (data) => {
327
- try {
328
- const response = JSON.parse(data.toString());
329
- if (response.type !== MessageType.AUTH_RESP || !response.payload.success) {
330
- ws.close();
331
- reject(new Error(`认证失败: ${response.payload.error || '未知错误'}`));
332
- return;
333
- }
334
- // 认证成功,发送注册代理请求
335
- const registerMsg = createMessage(MessageType.REGISTER, {
336
- remotePort: proxy.remotePort,
337
- localPort: proxy.localPort,
338
- localHost: proxy.localHost,
339
- });
340
- ws.send(JSON.stringify(registerMsg));
341
- // 等待注册响应
342
- ws.once('message', (data) => {
343
- try {
344
- const response = JSON.parse(data.toString());
345
- if (response.type !== MessageType.REGISTER_RESP) {
346
- ws.close();
347
- reject(new Error('意外的响应类型'));
348
- return;
349
- }
350
- if (response.payload.success) {
351
- console.log(chalk.green(`代理已添加: :${proxy.remotePort} -> ${proxy.localHost || 'localhost'}:${proxy.localPort}`));
352
- ws.close();
353
- resolve();
354
- }
355
- else {
356
- ws.close();
357
- reject(new Error(`注册代理失败: ${response.payload.error || '未知错误'}`));
358
- }
359
- }
360
- catch (err) {
361
- ws.close();
362
- reject(err);
363
- }
364
- });
365
- }
366
- catch (err) {
367
- ws.close();
368
- reject(err);
369
- }
370
- });
779
+ // forward add 子命令
780
+ forwardCmd.command('add')
781
+ .description('添加正向穿透代理')
782
+ .argument('<mapping>', '映射格式:localPort:remotePort:targetClientId (如: 8080:3000:clientB)')
783
+ .action(async (mapping) => {
784
+ const parts = mapping.split(':');
785
+ if (parts.length !== 3) {
786
+ console.log(chalk.yellow('参数格式错误,应为:localPort:remotePort:targetClientId'));
787
+ console.log(chalk.gray('示例:8080:3000:clientB'));
788
+ return;
789
+ }
790
+ const localPort = parseInt(parts[0], 10);
791
+ const targetPort = parseInt(parts[1], 10);
792
+ const targetClientId = parts[2];
793
+ if (isNaN(localPort) || localPort < 1 || localPort > 65535) {
794
+ console.log(chalk.yellow('本地端口无效:1-65535'));
795
+ return;
796
+ }
797
+ if (isNaN(targetPort) || targetPort < 1 || targetPort > 65535) {
798
+ console.log(chalk.yellow('目标端口无效:1-65535'));
799
+ return;
800
+ }
801
+ if (!targetClientId || targetClientId.length === 0) {
802
+ console.log(chalk.yellow('目标客户端 ID 不能为空'));
803
+ return;
804
+ }
805
+ try {
806
+ const info = readPidFile();
807
+ const adminPort = info?.adminPort ?? ADMIN_PORT_DEFAULT;
808
+ const res = await fetch(`http://127.0.0.1:${adminPort}/_ctc/forward/add`, {
809
+ method: 'POST',
810
+ headers: { 'Content-Type': 'application/json' },
811
+ body: JSON.stringify({ localPort, targetClientId, targetPort }),
371
812
  });
372
- ws.on('error', (err) => {
373
- clearTimeout(timeout);
374
- reject(new Error(`连接失败: ${err.message}`));
813
+ if (res.ok) {
814
+ console.log(chalk.green('正向穿透代理已添加'));
815
+ console.log(chalk.gray(` 本地 :${localPort} → ${targetClientId}:${targetPort}`));
816
+ }
817
+ else if (res.status === 404) {
818
+ console.log(chalk.yellow('客户端未运行或功能不可用'));
819
+ }
820
+ else {
821
+ const data = await res.json();
822
+ console.log(chalk.red(`添加失败: ${data.error || '未知错误'}`));
823
+ }
824
+ }
825
+ catch (err) {
826
+ console.log(chalk.red(`请求失败: ${err.message}`));
827
+ }
828
+ });
829
+ // forward remove 子命令
830
+ forwardCmd.command('remove')
831
+ .description('移除正向穿透代理')
832
+ .argument('<localPort>', '本地端口号')
833
+ .action(async (localPort) => {
834
+ const port = parseInt(localPort, 10);
835
+ if (isNaN(port) || port < 1 || port > 65535) {
836
+ console.log(chalk.yellow('端口号无效:1-65535'));
837
+ return;
838
+ }
839
+ try {
840
+ const info = readPidFile();
841
+ const adminPort = info?.adminPort ?? ADMIN_PORT_DEFAULT;
842
+ const res = await fetch(`http://127.0.0.1:${adminPort}/_ctc/forward/remove`, {
843
+ method: 'POST',
844
+ headers: { 'Content-Type': 'application/json' },
845
+ body: JSON.stringify({ localPort: port }),
375
846
  });
376
- });
377
- }
378
- /**
379
- * 从正在运行的客户端获取代理列表
380
- */
381
- async function getProxiesFromRunningClient(serverUrl) {
382
- return new Promise((resolve, reject) => {
383
- const ws = new WebSocket(serverUrl);
384
- const timeout = setTimeout(() => {
385
- ws.close();
386
- resolve([]);
387
- }, 3000);
388
- ws.on('open', async () => {
389
- // 发送获取代理列表请求(使用心跳消息保持连接)
390
- const heartbeatMsg = createMessage(MessageType.HEARTBEAT, {
391
- timestamp: Date.now(),
392
- });
393
- ws.send(JSON.stringify(heartbeatMsg));
394
- // 这里简化处理,实际上应该有一个专门的 GET_PROXIES 消息类型
395
- // 目前返回空数组
396
- clearTimeout(timeout);
397
- ws.close();
398
- resolve([]);
847
+ if (res.ok) {
848
+ console.log(chalk.green(`本地端口 ${port} 的正向穿透代理已移除`));
849
+ }
850
+ else {
851
+ const data = await res.json();
852
+ console.log(chalk.red(`移除失败: ${data.error || '未知错误'}`));
853
+ }
854
+ }
855
+ catch (err) {
856
+ console.log(chalk.red(`请求失败: ${err.message}`));
857
+ }
858
+ });
859
+ // forward clients 子命令
860
+ forwardCmd.command('clients')
861
+ .description('查看在线客户端列表')
862
+ .action(async () => {
863
+ try {
864
+ const info = readPidFile();
865
+ const adminPort = info?.adminPort ?? ADMIN_PORT_DEFAULT;
866
+ const res = await fetch(`http://127.0.0.1:${adminPort}/_ctc/forward/clients`);
867
+ if (!res.ok) {
868
+ console.log(chalk.yellow('无法获取客户端列表'));
869
+ return;
870
+ }
871
+ const data = await res.json();
872
+ const clients = data.clients || [];
873
+ if (clients.length === 0) {
874
+ console.log(chalk.yellow('暂无在线客户端'));
875
+ return;
876
+ }
877
+ console.log(chalk.blue.bold('在线客户端列表:'));
878
+ for (const client of clients) {
879
+ const desc = client.description ? ` (${client.description})` : '';
880
+ console.log(` ${chalk.cyan(client.id)}${desc}`);
881
+ }
882
+ }
883
+ catch (err) {
884
+ console.log(chalk.red(`请求失败: ${err.message}`));
885
+ }
886
+ });
887
+ // forward register 子命令
888
+ forwardCmd.command('register')
889
+ .description('注册到服务器(启用正向穿透模式)')
890
+ .option('-d, --description <desc>', '客户端描述')
891
+ .action(async (options) => {
892
+ try {
893
+ const info = readPidFile();
894
+ const adminPort = info?.adminPort ?? ADMIN_PORT_DEFAULT;
895
+ const res = await fetch(`http://127.0.0.1:${adminPort}/_ctc/forward/register`, {
896
+ method: 'POST',
897
+ headers: { 'Content-Type': 'application/json' },
898
+ body: JSON.stringify({ description: options.description || '' }),
899
+ });
900
+ if (res.ok) {
901
+ console.log(chalk.green('已注册到服务器,正向穿透模式已启用'));
902
+ }
903
+ else {
904
+ const data = await res.json();
905
+ console.log(chalk.red(`注册失败: ${data.error || '未知错误'}`));
906
+ }
907
+ }
908
+ catch (err) {
909
+ console.log(chalk.red(`请求失败: ${err.message}`));
910
+ }
911
+ });
912
+ // ==================== boot 命令 ====================
913
+ const bootCmd = program.command('boot')
914
+ .description('管理开机自启动')
915
+ .argument('[command]', '子命令:enable, disable, status', { default: 'status' });
916
+ // boot status 子命令
917
+ bootCmd.command('status')
918
+ .description('查看开机自启动状态')
919
+ .action(async () => {
920
+ const registered = isBootRegistered();
921
+ const status = registered ? chalk.green('已启用') : chalk.yellow('未启用');
922
+ console.log(chalk.blue('开机自启动状态:') + status);
923
+ if (registered) {
924
+ const info = readPidFile();
925
+ if (info) {
926
+ console.log(chalk.gray(` 启动文件将运行:${info.serverUrl}`));
927
+ }
928
+ }
929
+ });
930
+ // boot enable 子命令
931
+ bootCmd.command('enable')
932
+ .description('启用开机自启动')
933
+ .action(async () => {
934
+ const info = readPidFile();
935
+ if (!info) {
936
+ console.log(chalk.yellow('客户端未启动过,请先使用 start 命令启动'));
937
+ return;
938
+ }
939
+ try {
940
+ registerBoot({
941
+ isServer: false,
942
+ nodePath,
943
+ scriptPath: indexPath,
944
+ args: info.args || [],
399
945
  });
400
- ws.on('error', () => {
401
- clearTimeout(timeout);
402
- resolve([]);
946
+ console.log(chalk.green('已启用开机自启动'));
947
+ }
948
+ catch (err) {
949
+ console.log(chalk.red(`启用失败: ${err.message}`));
950
+ }
951
+ });
952
+ // boot disable 子命令
953
+ bootCmd.command('disable')
954
+ .description('禁用开机自启动')
955
+ .action(async () => {
956
+ try {
957
+ unregisterBoot();
958
+ console.log(chalk.green('已禁用开机自启动'));
959
+ }
960
+ catch (err) {
961
+ console.log(chalk.red(`禁用失败: ${err.message}`));
962
+ }
963
+ });
964
+ // ==================== logs 命令 ====================
965
+ const logsCmd = program.command('logs')
966
+ .description('查看或跟踪日志')
967
+ .option('-f, --follow', '跟踪日志输出(类似 tail -f)')
968
+ .action(async (options) => {
969
+ if (!existsSync(LOG_FILE)) {
970
+ console.log(chalk.yellow('日志文件不存在'));
971
+ return;
972
+ }
973
+ if (options.follow) {
974
+ console.log(chalk.blue('跟踪日志输出 (Ctrl+C 退出)...'));
975
+ console.log(chalk.dim('---'));
976
+ const logFd = openSync(LOG_FILE, 'r');
977
+ const buffer = Buffer.alloc(1024);
978
+ let pos = 0;
979
+ const readLog = () => {
980
+ try {
981
+ const bytesRead = readSync(logFd, buffer, 0, buffer.length, pos);
982
+ if (bytesRead > 0) {
983
+ const content = buffer.toString('utf-8', 0, bytesRead);
984
+ process.stdout.write(content);
985
+ pos += bytesRead;
986
+ }
987
+ }
988
+ catch {
989
+ // ignore
990
+ }
991
+ };
992
+ // 读取最新内容
993
+ readLog();
994
+ // 监控文件变化
995
+ const watcher = () => {
996
+ try {
997
+ const stat = statSync(LOG_FILE);
998
+ const newSize = stat.size;
999
+ if (newSize > pos) {
1000
+ readLog();
1001
+ }
1002
+ }
1003
+ catch (err) {
1004
+ // ignore
1005
+ }
1006
+ };
1007
+ // 每秒检查一次
1008
+ const interval = setInterval(watcher, 1000);
1009
+ // 处理退出
1010
+ process.on('SIGINT', () => {
1011
+ clearInterval(interval);
1012
+ closeSync(logFd);
1013
+ process.stdout.write('\n');
1014
+ console.log(chalk.gray('\n--- 日志跟踪已结束'));
1015
+ process.exit(0);
403
1016
  });
404
- });
405
- }
406
- /**
407
- * 运行客户端(前台模式)
408
- */
409
- async function runServe(serverUrl, token, proxies, reconnectInterval, maxReconnect, shouldOpenBrowser) {
410
- // 设置 process.argv 供 Config.load 读取
411
- process.argv = [
412
- process.argv[0],
413
- process.argv[1],
414
- '--server', serverUrl,
415
- '--reconnect-interval', reconnectInterval,
416
- '--max-reconnect', maxReconnect,
417
- ];
418
- if (token)
419
- process.argv.push('--token', token);
420
- if (proxies)
421
- process.argv.push('--proxies', proxies);
422
- // 动态导入 index 模块运行
423
- const { run } = await import('./index.js');
424
- // 如果需要打开浏览器,延迟打开
425
- if (shouldOpenBrowser) {
426
- setTimeout(() => {
427
- openBrowser('http://127.0.0.1:9001/');
428
- }, 3000);
429
1017
  }
430
- await run();
431
- }
1018
+ else {
1019
+ // 显示所有日志
1020
+ const content = readFileSync(LOG_FILE, 'utf-8');
1021
+ console.log(chalk.blue('最近日志:'));
1022
+ console.log(chalk.dim('---'));
1023
+ console.log(content.split('\n').slice(-30).join('\n'));
1024
+ console.log(chalk.dim('---'));
1025
+ console.log(chalk.gray(`(日志文件:${LOG_FILE})`));
1026
+ }
1027
+ });
1028
+ // ==================== open 命令 ====================
1029
+ const openCmd = program.command('open')
1030
+ .description('在浏览器中打开管理页面')
1031
+ .action(async () => {
1032
+ const info = readPidFile();
1033
+ const adminPort = info?.adminPort ?? ADMIN_PORT_DEFAULT;
1034
+ const adminPage = `http://127.0.0.1:${adminPort}`;
1035
+ const openCmd = platform() === 'win32' ? 'start' : 'open';
1036
+ console.log(chalk.gray(`打开管理页面: ${adminPage}`));
1037
+ try {
1038
+ execSync(`${openCmd} ${adminPage}`, { stdio: 'ignore' });
1039
+ console.log(chalk.green('管理页面已在浏览器中打开'));
1040
+ }
1041
+ catch (err) {
1042
+ console.log(chalk.yellow(`打开失败: ${err.message}`));
1043
+ console.log(chalk.gray(`请手动访问:${adminPage}`));
1044
+ }
1045
+ });
1046
+ // ==================== 默认处理 ====================
1047
+ // 当没有指定命令时显示帮助
1048
+ program.action(() => {
1049
+ showHelp();
1050
+ });
1051
+ // 解析命令行参数
432
1052
  program.parse();
433
1053
  //# sourceMappingURL=cli.js.map