@chenpu17/tcpmapping 2.1.0 → 2.4.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/bin/tcpmapping.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawn, execSync } = require('child_process');
3
+ const { spawn } = require('child_process');
4
4
  const path = require('path');
5
5
  const fs = require('fs');
6
6
  const os = require('os');
7
+ const http = require('http');
7
8
 
8
9
  const APP_NAME = 'tcpmapping';
9
- const DATA_DIR = path.join(os.homedir(), '.tcpmapping');
10
+ const DATA_DIR = process.env.TCPMAPPING_DATA_DIR || path.join(os.homedir(), '.tcpmapping');
10
11
  const PID_FILE = path.join(DATA_DIR, 'tcpmapping.pid');
11
12
  const LOG_FILE = path.join(DATA_DIR, 'tcpmapping.log');
12
13
  const SERVER_FILE = path.join(__dirname, '..', 'server.js');
@@ -16,254 +17,423 @@ const SETUP_PASSWORD_FILE = path.join(__dirname, '..', 'setup-password.js');
16
17
  const args = process.argv.slice(2);
17
18
  const command = args[0] || 'help';
18
19
 
19
- // 颜色输出
20
+ // 颜色输出(非 TTY 或设置了 NO_COLOR 时自动关闭)
21
+ const useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
22
+ const colorize = (code) => (text) => useColor ? `\x1b[${code}m${text}\x1b[0m` : String(text);
23
+
20
24
  const colors = {
21
- green: (text) => `\x1b[32m${text}\x1b[0m`,
22
- red: (text) => `\x1b[31m${text}\x1b[0m`,
23
- yellow: (text) => `\x1b[33m${text}\x1b[0m`,
24
- blue: (text) => `\x1b[34m${text}\x1b[0m`,
25
- cyan: (text) => `\x1b[36m${text}\x1b[0m`,
26
- gray: (text) => `\x1b[90m${text}\x1b[0m`
25
+ green: colorize(32),
26
+ red: colorize(31),
27
+ yellow: colorize(33),
28
+ blue: colorize(34),
29
+ cyan: colorize(36),
30
+ gray: colorize(90)
27
31
  };
28
32
 
33
+ function delay(ms) {
34
+ return new Promise(resolve => setTimeout(resolve, ms));
35
+ }
36
+
29
37
  // 确保数据目录存在
30
38
  function ensureDataDir() {
31
39
  if (!fs.existsSync(DATA_DIR)) {
32
- fs.mkdirSync(DATA_DIR, { recursive: true });
40
+ fs.mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
33
41
  }
34
42
  }
35
43
 
36
- // 获取配置文件路径(按优先级)
37
- function getConfigPath() {
38
- // 优先级1: 当前工作目录
39
- const cwdPath = path.join(process.cwd(), 'config.json');
40
- if (fs.existsSync(cwdPath)) {
41
- return cwdPath;
42
- }
44
+ // 读取配置文件
45
+ function readConfig() {
46
+ const candidates = [
47
+ path.join(process.cwd(), 'config.json'),
48
+ path.join(DATA_DIR, 'config.json')
49
+ ];
43
50
 
44
- // 优先级2: 用户数据目录
45
- const dataPath = path.join(DATA_DIR, 'config.json');
46
- if (fs.existsSync(dataPath)) {
47
- return dataPath;
51
+ for (const configPath of candidates) {
52
+ try {
53
+ if (fs.existsSync(configPath)) {
54
+ return { path: configPath, data: JSON.parse(fs.readFileSync(configPath, 'utf8')) };
55
+ }
56
+ } catch (error) {
57
+ console.warn(colors.yellow(`警告: 配置文件解析失败 (${configPath}): ${error.message}`));
58
+ }
48
59
  }
49
60
 
50
- return null;
61
+ return { path: null, data: null };
62
+ }
63
+
64
+ // 获取配置文件路径(按优先级)
65
+ function getConfigPath() {
66
+ return readConfig().path;
51
67
  }
52
68
 
53
69
  // 获取认证配置文件路径
54
70
  function getAuthConfigPath() {
55
- const cwdPath = path.join(process.cwd(), 'auth-config.json');
56
- if (fs.existsSync(cwdPath)) {
57
- return cwdPath;
71
+ const candidates = [
72
+ path.join(process.cwd(), 'auth-config.json'),
73
+ path.join(DATA_DIR, 'auth-config.json')
74
+ ];
75
+
76
+ for (const authPath of candidates) {
77
+ if (fs.existsSync(authPath)) {
78
+ return authPath;
79
+ }
58
80
  }
59
81
 
60
- const dataPath = path.join(DATA_DIR, 'auth-config.json');
61
- if (fs.existsSync(dataPath)) {
62
- return dataPath;
82
+ return null;
83
+ }
84
+
85
+ // 获取 WebUI 访问信息
86
+ function getWebuiInfo() {
87
+ const config = readConfig().data;
88
+ const webui = (config && config.webui) || {};
89
+
90
+ return {
91
+ enabled: webui.enabled !== false,
92
+ host: webui.host || '127.0.0.1',
93
+ port: Number(webui.port) || 9999
94
+ };
95
+ }
96
+
97
+ // 将监听地址转换为可访问地址
98
+ function getAccessHost(host) {
99
+ if (host === '0.0.0.0' || host === '::' || !host) {
100
+ return 'localhost';
101
+ }
102
+ if (host.includes(':')) {
103
+ return `[${host}]`;
63
104
  }
105
+ return host;
106
+ }
64
107
 
65
- return null;
108
+ // 检查进程是否存在
109
+ function isAlive(pid) {
110
+ try {
111
+ process.kill(pid, 0);
112
+ return true;
113
+ } catch (error) {
114
+ // 无权限说明进程存在(属于其他用户)
115
+ return error.code === 'EPERM';
116
+ }
66
117
  }
67
118
 
68
119
  // 检查进程是否运行
69
120
  function isRunning() {
70
121
  ensureDataDir();
122
+
71
123
  if (!fs.existsSync(PID_FILE)) {
72
- return false;
124
+ return null;
73
125
  }
74
126
 
75
- const pid = fs.readFileSync(PID_FILE, 'utf8').trim();
127
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
128
+
129
+ if (Number.isInteger(pid) && pid > 0 && isAlive(pid)) {
130
+ return pid;
131
+ }
76
132
 
133
+ // 进程不存在,清理过期的 PID 文件
77
134
  try {
78
- process.kill(parseInt(pid), 0);
79
- return parseInt(pid);
80
- } catch (e) {
81
- // 进程不存在,清理PID文件
82
135
  fs.unlinkSync(PID_FILE);
83
- return false;
136
+ } catch (error) {
137
+ // 忽略清理失败
138
+ }
139
+ return null;
140
+ }
141
+
142
+ // 读取日志末尾内容
143
+ function readLogTail(lineCount) {
144
+ try {
145
+ if (!fs.existsSync(LOG_FILE)) {
146
+ return null;
147
+ }
148
+
149
+ const stat = fs.statSync(LOG_FILE);
150
+ const maxBytes = 64 * 1024;
151
+ const size = Math.min(stat.size, maxBytes);
152
+ if (size === 0) {
153
+ return '';
154
+ }
155
+
156
+ const buffer = Buffer.alloc(size);
157
+ const fd = fs.openSync(LOG_FILE, 'r');
158
+ try {
159
+ fs.readSync(fd, buffer, 0, size, stat.size - size);
160
+ } finally {
161
+ fs.closeSync(fd);
162
+ }
163
+
164
+ return buffer.toString('utf8').split('\n').slice(-lineCount).join('\n');
165
+ } catch (error) {
166
+ return null;
84
167
  }
85
168
  }
86
169
 
87
170
  // 启动服务
88
- function start() {
171
+ async function start() {
89
172
  const pid = isRunning();
90
173
  if (pid) {
91
- console.log(colors.yellow(`${APP_NAME} is already running (PID: ${pid})`));
174
+ console.log(colors.yellow(`${APP_NAME} 已在运行 (PID: ${pid})`));
92
175
  return;
93
176
  }
94
177
 
95
- // 检查是否已初始化
96
- const configPath = getConfigPath();
97
- const authPath = getAuthConfigPath();
98
-
99
- if (!authPath) {
178
+ if (!getAuthConfigPath()) {
100
179
  console.log(colors.yellow('未设置管理员密码,请先运行:'));
101
180
  console.log(colors.cyan(' tcpmapping setup-password'));
102
181
  return;
103
182
  }
104
183
 
105
184
  ensureDataDir();
106
- console.log(colors.blue(`Starting ${APP_NAME}...`));
185
+ console.log(colors.blue(`正在启动 ${APP_NAME}...`));
107
186
 
108
187
  const out = fs.openSync(LOG_FILE, 'a');
109
188
  const err = fs.openSync(LOG_FILE, 'a');
110
189
 
111
- const child = spawn('node', [SERVER_FILE], {
112
- detached: true,
113
- stdio: ['ignore', out, err],
114
- cwd: process.cwd() // 使用当前目录,以便server可以找到当前目录的config
115
- });
190
+ let child;
191
+ try {
192
+ child = spawn(process.execPath, [SERVER_FILE], {
193
+ detached: true,
194
+ stdio: ['ignore', out, err],
195
+ // 使用当前目录,以便 server 可以找到当前目录的 config.json
196
+ cwd: process.cwd()
197
+ });
198
+ } finally {
199
+ // 子进程已继承文件描述符,父进程可以关闭
200
+ fs.closeSync(out);
201
+ fs.closeSync(err);
202
+ }
116
203
 
117
- fs.writeFileSync(PID_FILE, child.pid.toString());
204
+ fs.writeFileSync(PID_FILE, String(child.pid));
118
205
  child.unref();
119
206
 
120
- console.log(colors.green(`${APP_NAME} started successfully (PID: ${child.pid})`));
121
- console.log(colors.cyan(`WebUI: http://localhost:9999`));
122
- console.log(colors.gray(`Log file: ${LOG_FILE}`));
207
+ // 短暂等待,捕获启动即失败的情况(如端口被占用、配置错误)
208
+ await delay(500);
209
+
210
+ if (!isAlive(child.pid)) {
211
+ try {
212
+ fs.unlinkSync(PID_FILE);
213
+ } catch (error) {
214
+ // 忽略
215
+ }
216
+ console.log(colors.red(`${APP_NAME} 启动失败`));
217
+ const tail = readLogTail(15);
218
+ if (tail) {
219
+ console.log(colors.gray('最近日志:'));
220
+ console.log(tail);
221
+ }
222
+ console.log(colors.gray(`完整日志: ${LOG_FILE}`));
223
+ process.exitCode = 1;
224
+ return;
225
+ }
226
+
227
+ const webui = getWebuiInfo();
228
+ console.log(colors.green(`${APP_NAME} 启动成功 (PID: ${child.pid})`));
229
+ if (webui.enabled) {
230
+ console.log(colors.cyan(`WebUI: http://${getAccessHost(webui.host)}:${webui.port}`));
231
+ if (webui.host === '0.0.0.0' || webui.host === '::') {
232
+ console.log(colors.yellow('提示: WebUI 监听在所有网卡上,请确保使用强密码'));
233
+ }
234
+ }
235
+ console.log(colors.gray(`日志文件: ${LOG_FILE}`));
236
+ const configPath = getConfigPath();
123
237
  if (configPath) {
124
- console.log(colors.gray(`Config: ${configPath}`));
238
+ console.log(colors.gray(`配置文件: ${configPath}`));
125
239
  }
126
240
  }
127
241
 
128
242
  // 停止服务
129
- function stop() {
243
+ async function stop() {
130
244
  const pid = isRunning();
131
245
  if (!pid) {
132
- console.log(colors.yellow(`${APP_NAME} is not running`));
246
+ console.log(colors.yellow(`${APP_NAME} 未在运行`));
133
247
  return;
134
248
  }
135
249
 
136
- console.log(colors.blue(`Stopping ${APP_NAME} (PID: ${pid})...`));
250
+ console.log(colors.blue(`正在停止 ${APP_NAME} (PID: ${pid})...`));
137
251
 
138
252
  try {
139
253
  process.kill(pid, 'SIGTERM');
140
254
 
141
- // 等待进程退出
255
+ // 最多等待 5 秒,让服务优雅退出
142
256
  let attempts = 0;
143
- while (attempts < 10) {
257
+ while (attempts < 20 && isAlive(pid)) {
258
+ await delay(250);
259
+ attempts++;
260
+ }
261
+
262
+ // 仍未退出则强制终止
263
+ if (isAlive(pid)) {
264
+ console.log(colors.yellow('服务未在预期时间内退出,正在强制终止...'));
144
265
  try {
145
- process.kill(pid, 0);
146
- // 进程仍在运行
147
- require('child_process').execSync('sleep 0.5');
148
- attempts++;
149
- } catch (e) {
150
- // 进程已退出
151
- break;
266
+ process.kill(pid, 'SIGKILL');
267
+ } catch (error) {
268
+ // 进程可能已退出
152
269
  }
270
+ await delay(200);
153
271
  }
154
272
 
155
- // 如果进程仍在运行,强制终止
156
273
  try {
157
- process.kill(pid, 0);
158
- process.kill(pid, 'SIGKILL');
159
- } catch (e) {
160
- // 进程已退出
161
- }
162
-
163
- if (fs.existsSync(PID_FILE)) {
164
274
  fs.unlinkSync(PID_FILE);
275
+ } catch (error) {
276
+ // 忽略
165
277
  }
166
278
 
167
- console.log(colors.green(`${APP_NAME} stopped successfully`));
168
- } catch (e) {
169
- console.log(colors.red(`Failed to stop ${APP_NAME}: ${e.message}`));
279
+ console.log(colors.green(`${APP_NAME} 已停止`));
280
+ } catch (error) {
281
+ console.log(colors.red(`停止 ${APP_NAME} 失败: ${error.message}`));
170
282
  }
171
283
  }
172
284
 
173
285
  // 重启服务
174
- function restart() {
175
- console.log(colors.blue(`Restarting ${APP_NAME}...`));
176
- stop();
177
- setTimeout(() => {
178
- start();
179
- }, 1000);
286
+ async function restart() {
287
+ console.log(colors.blue(`正在重启 ${APP_NAME}...`));
288
+ await stop();
289
+ await delay(300);
290
+ await start();
291
+ }
292
+
293
+ // 请求健康检查接口
294
+ function fetchHealth(host, port) {
295
+ return new Promise((resolve) => {
296
+ const request = http.get({
297
+ host: host,
298
+ port: port,
299
+ path: '/api/health',
300
+ timeout: 2000
301
+ }, (res) => {
302
+ let data = '';
303
+ res.on('data', chunk => data += chunk);
304
+ res.on('end', () => {
305
+ try {
306
+ resolve(JSON.parse(data));
307
+ } catch (error) {
308
+ resolve(null);
309
+ }
310
+ });
311
+ });
312
+
313
+ request.on('error', () => resolve(null));
314
+ request.on('timeout', () => {
315
+ request.destroy();
316
+ resolve(null);
317
+ });
318
+ });
180
319
  }
181
320
 
182
321
  // 查看状态
183
- function status() {
322
+ async function status() {
184
323
  const pid = isRunning();
185
324
  const configPath = getConfigPath();
186
325
  const authPath = getAuthConfigPath();
326
+ const webui = getWebuiInfo();
187
327
 
188
328
  console.log(colors.cyan('=== TcpMapping Status ===\n'));
189
329
 
190
- // 服务状态
191
330
  if (pid) {
192
- console.log(colors.green(`Service: Running (PID: ${pid})`));
193
-
194
- // 尝试获取健康状态
195
- try {
196
- const http = require('http');
197
- const req = http.get('http://localhost:9999/api/health', { timeout: 2000 }, (res) => {
198
- let data = '';
199
- res.on('data', chunk => data += chunk);
200
- res.on('end', () => {
201
- try {
202
- const health = JSON.parse(data);
203
- console.log(` Uptime: ${Math.floor(health.uptime / 60)} minutes`);
204
- console.log(` Active Mappings: ${health.activeMappings}`);
205
- console.log(` Active Connections: ${health.activeConnections}`);
206
- } catch (e) {}
207
- showPaths();
208
- });
209
- });
210
- req.on('error', () => showPaths());
211
- req.on('timeout', () => { req.destroy(); showPaths(); });
212
- } catch (e) {
213
- showPaths();
331
+ console.log(colors.green(`服务状态: 运行中 (PID: ${pid})`));
332
+
333
+ if (webui.enabled) {
334
+ const host = webui.host === '0.0.0.0' || webui.host === '::' ? '127.0.0.1' : webui.host;
335
+ const health = await fetchHealth(host, webui.port);
336
+
337
+ if (health) {
338
+ console.log(` WebUI: http://${getAccessHost(webui.host)}:${webui.port}`);
339
+ console.log(` 运行时间: ${Math.floor(health.uptime / 60)} 分钟`);
340
+ console.log(` 活跃映射: ${health.activeMappings}`);
341
+ console.log(` 活跃连接: ${health.activeConnections}`);
342
+ } else {
343
+ console.log(colors.yellow(` 健康检查失败: http://${getAccessHost(webui.host)}:${webui.port}/api/health`));
344
+ }
345
+ } else {
346
+ console.log(colors.gray(' WebUI: 已禁用'));
214
347
  }
215
348
  } else {
216
- console.log(colors.red('Service: Stopped'));
217
- showPaths();
349
+ console.log(colors.red('服务状态: 已停止'));
218
350
  }
219
351
 
220
- function showPaths() {
221
- console.log('');
222
- console.log(colors.cyan('=== Configuration ===\n'));
223
- console.log(`Data Dir: ${DATA_DIR}`);
224
- console.log(`Config: ${configPath || colors.yellow('Not found (run: tcpmapping init)')}`);
225
- console.log(`Auth: ${authPath || colors.yellow('Not found (run: tcpmapping setup-password)')}`);
226
- console.log(`Log: ${LOG_FILE}`);
227
- }
352
+ console.log('');
353
+ console.log(colors.cyan('=== Configuration ===\n'));
354
+ console.log(`数据目录: ${DATA_DIR}`);
355
+ console.log(`配置文件: ${configPath || colors.yellow('未找到 (运行: tcpmapping init)')}`);
356
+ console.log(`认证配置: ${authPath || colors.yellow('未找到 (运行: tcpmapping setup-password)')}`);
357
+ console.log(`日志文件: ${LOG_FILE}`);
228
358
  }
229
359
 
230
360
  // 查看日志
231
361
  function logs(follow = false) {
232
362
  ensureDataDir();
363
+
233
364
  if (!fs.existsSync(LOG_FILE)) {
234
- console.log(colors.yellow('No log file found'));
365
+ console.log(colors.yellow('未找到日志文件'));
235
366
  return;
236
367
  }
237
368
 
238
- if (follow) {
239
- console.log(colors.blue(`Tailing ${LOG_FILE}... (Ctrl+C to exit)`));
240
- const tail = spawn('tail', ['-f', LOG_FILE], { stdio: 'inherit' });
241
- process.on('SIGINT', () => {
242
- tail.kill();
243
- process.exit(0);
244
- });
245
- } else {
246
- const content = fs.readFileSync(LOG_FILE, 'utf8');
247
- const lines = content.split('\n').slice(-50);
248
- console.log(lines.join('\n'));
369
+ if (!follow) {
370
+ const tail = readLogTail(50);
371
+ if (tail !== null) {
372
+ console.log(tail);
373
+ }
374
+ return;
249
375
  }
376
+
377
+ console.log(colors.blue(`正在跟踪 ${LOG_FILE}... (Ctrl+C 退出)`));
378
+
379
+ const tail = readLogTail(20);
380
+ if (tail) {
381
+ console.log(tail);
382
+ }
383
+
384
+ let offset = fs.statSync(LOG_FILE).size;
385
+
386
+ // 使用轮询方式,兼容 Windows / macOS / Linux
387
+ const watcher = (current) => {
388
+ if (current.size < offset) {
389
+ // 文件被截断(例如日志轮转)
390
+ offset = 0;
391
+ }
392
+
393
+ if (current.size > offset) {
394
+ try {
395
+ const length = current.size - offset;
396
+ const buffer = Buffer.alloc(length);
397
+ const fd = fs.openSync(LOG_FILE, 'r');
398
+ try {
399
+ fs.readSync(fd, buffer, 0, length, offset);
400
+ } finally {
401
+ fs.closeSync(fd);
402
+ }
403
+ process.stdout.write(buffer);
404
+ offset = current.size;
405
+ } catch (error) {
406
+ // 文件可能在读取过程中被轮转,忽略本次读取
407
+ }
408
+ }
409
+ };
410
+
411
+ fs.watchFile(LOG_FILE, { interval: 500 }, watcher);
412
+
413
+ const cleanup = () => {
414
+ fs.unwatchFile(LOG_FILE, watcher);
415
+ process.exit(0);
416
+ };
417
+
418
+ process.on('SIGINT', cleanup);
419
+ process.on('SIGTERM', cleanup);
250
420
  }
251
421
 
252
422
  // 设置密码
253
423
  function setupPassword() {
254
424
  ensureDataDir();
255
- console.log(colors.blue('Setting up password...'));
256
- console.log(colors.gray(`Config will be saved to: ${DATA_DIR}`));
425
+ console.log(colors.blue('正在设置管理员密码...'));
426
+ console.log(colors.gray(`配置将保存到: ${DATA_DIR}`));
257
427
  console.log('');
258
428
 
259
- const child = spawn('node', [SETUP_PASSWORD_FILE], {
429
+ const child = spawn(process.execPath, [SETUP_PASSWORD_FILE], {
260
430
  stdio: 'inherit',
261
- cwd: DATA_DIR, // 在数据目录运行,使auth-config.json保存到数据目录
431
+ cwd: DATA_DIR, // 在数据目录运行,使 auth-config.json 保存到数据目录
262
432
  env: { ...process.env, TCPMAPPING_DATA_DIR: DATA_DIR }
263
433
  });
264
434
 
265
435
  child.on('exit', (code) => {
266
- process.exit(code);
436
+ process.exit(code === null ? 1 : code);
267
437
  });
268
438
  }
269
439
 
@@ -271,39 +441,47 @@ function setupPassword() {
271
441
  function init() {
272
442
  ensureDataDir();
273
443
 
274
- // 检查当前目录和数据目录是否已有配置
275
444
  const cwdConfig = path.join(process.cwd(), 'config.json');
276
445
  const dataConfig = path.join(DATA_DIR, 'config.json');
277
446
 
278
447
  if (fs.existsSync(cwdConfig)) {
279
- console.log(colors.yellow(`config.json already exists in current directory`));
448
+ console.log(colors.yellow('当前目录已存在 config.json'));
280
449
  console.log(colors.gray(` ${cwdConfig}`));
281
450
  return;
282
451
  }
283
452
 
284
453
  if (fs.existsSync(dataConfig)) {
285
- console.log(colors.yellow(`config.json already exists in data directory`));
454
+ console.log(colors.yellow('数据目录已存在 config.json'));
286
455
  console.log(colors.gray(` ${dataConfig}`));
287
456
  console.log('');
288
- console.log('To create a local config, copy it to current directory:');
457
+ console.log('如需为当前项目创建独立配置,可复制一份:');
289
458
  console.log(colors.cyan(` cp ${dataConfig} ./config.json`));
290
459
  return;
291
460
  }
292
461
 
293
462
  const defaultConfig = {
294
463
  mappings: [],
295
- webui: { port: 9999 },
296
- logging: { level: "info" }
464
+ webui: {
465
+ enabled: true,
466
+ // 默认只监听本机;如需远程访问管理面板,请改为 "0.0.0.0" 并配置防火墙
467
+ host: '127.0.0.1',
468
+ port: 9999
469
+ },
470
+ logging: {
471
+ enabled: true,
472
+ level: 'info'
473
+ },
474
+ timeouts: {
475
+ connect: 30000,
476
+ idle: 300000
477
+ }
297
478
  };
298
479
 
299
- // 保存到数据目录
300
480
  fs.writeFileSync(dataConfig, JSON.stringify(defaultConfig, null, 2));
301
- console.log(colors.green('Created config.json'));
481
+ console.log(colors.green('已创建 config.json'));
302
482
  console.log(colors.gray(` ${dataConfig}`));
303
483
  console.log('');
304
- console.log('Edit the config file to add your port mappings.');
305
- console.log('Or copy it to current directory for project-specific config:');
306
- console.log(colors.cyan(` cp ${dataConfig} ./config.json`));
484
+ console.log('请编辑配置文件添加端口映射,或启动后通过 WebUI 添加。');
307
485
  }
308
486
 
309
487
  // 显示版本
@@ -317,52 +495,64 @@ function help() {
317
495
  console.log(`
318
496
  ${colors.cyan('TCP Mapping Server')} - TCP端口转发服务器
319
497
 
320
- ${colors.yellow('Usage:')}
498
+ ${colors.yellow('用法:')}
321
499
  tcpmapping <command>
322
500
 
323
- ${colors.yellow('Commands:')}
324
- ${colors.green('start')} Start the server in background
325
- ${colors.green('stop')} Stop the server
326
- ${colors.green('restart')} Restart the server
327
- ${colors.green('status')} Show server status and config paths
328
- ${colors.green('logs')} Show recent logs
329
- ${colors.green('logs -f')} Follow logs in real-time
330
- ${colors.green('setup-password')} Set up admin password
331
- ${colors.green('init')} Create default config.json
332
- ${colors.green('version')} Show version
333
- ${colors.green('help')} Show this help message
334
-
335
- ${colors.yellow('Config Paths (priority order):')}
336
- 1. ./config.json (current directory - project config)
337
- 2. ~/.tcpmapping/ (user data directory - global config)
338
-
339
- ${colors.yellow('Examples:')}
340
- tcpmapping init # Create config file
341
- tcpmapping setup-password # Set admin password
342
- tcpmapping start # Start server
343
- tcpmapping status # Check status and config paths
501
+ ${colors.yellow('命令:')}
502
+ ${colors.green('start')} 后台启动服务
503
+ ${colors.green('stop')} 停止服务
504
+ ${colors.green('restart')} 重启服务
505
+ ${colors.green('status')} 查看服务状态和配置路径
506
+ ${colors.green('logs')} 查看最近日志
507
+ ${colors.green('logs -f')} 实时跟踪日志
508
+ ${colors.green('setup-password')} 设置管理员密码
509
+ ${colors.green('init')} 创建默认 config.json
510
+ ${colors.green('version')} 显示版本
511
+ ${colors.green('help')} 显示帮助
512
+
513
+ ${colors.yellow('配置路径(按优先级):')}
514
+ 1. ./config.json 当前目录(项目配置)
515
+ 2. ~/.tcpmapping/ 用户数据目录(全局配置)
516
+
517
+ ${colors.yellow('示例:')}
518
+ tcpmapping init # 创建配置文件
519
+ tcpmapping setup-password # 设置管理员密码
520
+ tcpmapping start # 启动服务
521
+ tcpmapping status # 查看状态和配置路径
344
522
 
345
523
  ${colors.yellow('WebUI:')}
346
- After starting, visit http://localhost:9999
524
+ 启动后访问 http://localhost:9999(默认仅本机可访问)
347
525
  `);
348
526
  }
349
527
 
350
528
  // 执行命令
351
529
  switch (command) {
352
530
  case 'start':
353
- start();
531
+ start().catch(error => {
532
+ console.error(colors.red(`启动失败: ${error.message}`));
533
+ process.exitCode = 1;
534
+ });
354
535
  break;
355
536
  case 'stop':
356
- stop();
537
+ stop().catch(error => {
538
+ console.error(colors.red(`停止失败: ${error.message}`));
539
+ process.exitCode = 1;
540
+ });
357
541
  break;
358
542
  case 'restart':
359
- restart();
543
+ restart().catch(error => {
544
+ console.error(colors.red(`重启失败: ${error.message}`));
545
+ process.exitCode = 1;
546
+ });
360
547
  break;
361
548
  case 'status':
362
- status();
549
+ status().catch(error => {
550
+ console.error(colors.red(`获取状态失败: ${error.message}`));
551
+ process.exitCode = 1;
552
+ });
363
553
  break;
364
554
  case 'logs':
365
- logs(args[1] === '-f');
555
+ logs(args[1] === '-f' || args[1] === '--follow');
366
556
  break;
367
557
  case 'setup-password':
368
558
  case 'passwd':