coding-tool-x 3.5.5 → 3.5.7

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 (40) hide show
  1. package/README.md +25 -4
  2. package/bin/ctx.js +6 -1
  3. package/dist/web/assets/{Analytics-gvYu5sCM.js → Analytics-C6DEmD3D.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-CPlH8Ehd.js → ConfigTemplates-Cf_iTpC4.js} +1 -1
  5. package/dist/web/assets/{Home-B-qbu3uk.js → Home-BtBmYLJ1.js} +1 -1
  6. package/dist/web/assets/{PluginManager-B2tQ_YUq.js → PluginManager-DEk8vSw5.js} +1 -1
  7. package/dist/web/assets/{ProjectList-kDadoXXs.js → ProjectList-BMVhA_Kh.js} +1 -1
  8. package/dist/web/assets/{SessionList-eLgITwTV.js → SessionList-B5ioAXxg.js} +1 -1
  9. package/dist/web/assets/{SkillManager-B7zEB5Op.js → SkillManager-DcZOiiSf.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-C-RzB3ud.js → WorkspaceManager-BHqI8aGV.js} +1 -1
  11. package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
  12. package/dist/web/assets/index-CtByKdkA.js +2 -0
  13. package/dist/web/assets/{index-BHeh2z0i.css → index-VGAxnLqi.css} +1 -1
  14. package/dist/web/index.html +3 -3
  15. package/docs/Caddyfile.example +19 -0
  16. package/docs/reverse-proxy-https.md +57 -0
  17. package/package.json +2 -1
  18. package/src/commands/daemon.js +33 -5
  19. package/src/commands/stats.js +41 -4
  20. package/src/commands/ui.js +12 -3
  21. package/src/config/paths.js +6 -0
  22. package/src/index.js +125 -34
  23. package/src/server/api/codex-sessions.js +6 -3
  24. package/src/server/api/dashboard.js +25 -1
  25. package/src/server/api/gemini-sessions.js +6 -3
  26. package/src/server/api/hooks.js +17 -1
  27. package/src/server/api/opencode-sessions.js +6 -3
  28. package/src/server/api/plugins.js +24 -33
  29. package/src/server/api/sessions.js +6 -3
  30. package/src/server/index.js +31 -9
  31. package/src/server/services/codex-sessions.js +107 -9
  32. package/src/server/services/https-cert.js +171 -0
  33. package/src/server/services/network-access.js +61 -2
  34. package/src/server/services/notification-hooks.js +181 -16
  35. package/src/server/services/plugins-service.js +502 -44
  36. package/src/server/services/session-launch-command.js +81 -0
  37. package/src/server/services/sessions.js +103 -33
  38. package/src/server/services/web-ui-runtime.js +54 -0
  39. package/src/server/websocket-server.js +35 -4
  40. package/dist/web/assets/index-DG00t-zy.js +0 -2
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>CC-TOOL - ClaudeCode增强工作助手</title>
8
- <script type="module" crossorigin src="/assets/index-DG00t-zy.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CtByKdkA.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-DyTJGI4N.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-aWwwFAao.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendors-Fza9uSYn.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-BaTCPPL5.js">
13
- <link rel="modulepreload" crossorigin href="/assets/icons-DlxD2wZJ.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/icons-CQuif85v.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-BHeh2z0i.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-VGAxnLqi.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
@@ -0,0 +1,19 @@
1
+ {
2
+ # Keep both HTTP and HTTPS available instead of redirecting all HTTP traffic.
3
+ auto_https disable_redirects
4
+ }
5
+
6
+ # Replace example.com with your public domain.
7
+ http://example.com, https://example.com {
8
+ encode gzip zstd
9
+ reverse_proxy 127.0.0.1:19999
10
+ }
11
+
12
+ # If you only have an IP or internal hostname, Caddy can still terminate TLS:
13
+ # browsers will continue to warn until Caddy's local CA is trusted on the client.
14
+ #
15
+ # http://203.0.113.10, https://203.0.113.10 {
16
+ # tls internal
17
+ # encode gzip zstd
18
+ # reverse_proxy 127.0.0.1:19999
19
+ # }
@@ -0,0 +1,57 @@
1
+ # Reverse Proxy HTTPS
2
+
3
+ This project can stay on its built-in local HTTP server while a reverse proxy provides external HTTPS access.
4
+
5
+ Recommended topology:
6
+
7
+ - Coding Tool listens on `127.0.0.1:19999`
8
+ - Caddy listens on `80/443`
9
+ - Caddy proxies both `http://<domain>` and `https://<domain>` to `127.0.0.1:19999`
10
+
11
+ ## Why this setup
12
+
13
+ - The application keeps its existing local HTTP workflow.
14
+ - HTTPS termination, certificate issuance, and renewal are handled by Caddy.
15
+ - HTTP can remain available for compatibility because redirects are explicitly disabled.
16
+ - WebSocket and API requests work correctly behind the proxy after the forwarded-header compatibility changes in this repo.
17
+
18
+ ## Deployment Steps
19
+
20
+ 1. Point your domain to the server.
21
+
22
+ 2. Open inbound ports `80` and `443`.
23
+
24
+ 3. Start Coding Tool on the server without `--host` so the app only listens locally:
25
+
26
+ ```bash
27
+ ctx ui start
28
+ ```
29
+
30
+ If you prefer foreground mode:
31
+
32
+ ```bash
33
+ ctx ui
34
+ ```
35
+
36
+ 4. Copy [docs/Caddyfile.example](./Caddyfile.example), replace `example.com` with your real domain, then load it into Caddy.
37
+
38
+ Example locations:
39
+
40
+ ```bash
41
+ sudo cp docs/Caddyfile.example /etc/caddy/Caddyfile
42
+ sudo editor /etc/caddy/Caddyfile
43
+ sudo systemctl reload caddy
44
+ ```
45
+
46
+ 5. Verify all three paths you care about:
47
+
48
+ - `http://<domain>` should stay reachable
49
+ - `https://<domain>` should be reachable with TLS
50
+ - `http://127.0.0.1:19999` should still work locally on the server
51
+
52
+ ## Notes
53
+
54
+ - Browsers will still mark plain HTTP as not secure. Keeping HTTP enabled is only for compatibility.
55
+ - To get a trusted browser padlock, use a real domain that Caddy can issue a public certificate for.
56
+ - If you only have an IP address, you can use `tls internal` in Caddy for testing, but browsers will still warn until that CA is trusted manually.
57
+ - A reverse proxy usually makes `ctx ui --host` unnecessary. Leaving the app on `127.0.0.1` is safer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.5.5",
3
+ "version": "3.5.7",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -67,6 +67,7 @@
67
67
  "ora": "^5.4.1",
68
68
  "pm2": "^6.0.14",
69
69
  "rxjs": "^7.8.2",
70
+ "selfsigned": "^3.0.1",
70
71
  "semver": "^7.6.0",
71
72
  "toml": "^3.0.0",
72
73
  "ws": "^8.18.3"
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const { loadConfig } = require('../config/loader');
5
5
  const { PATHS, ensureStorageDirMigrated } = require('../config/paths');
6
+ const { isHttpsEnabled, getWebUiBaseUrl } = require('../server/services/web-ui-runtime');
6
7
  const {
7
8
  findProcessByPort,
8
9
  killProcessByPort,
@@ -117,6 +118,20 @@ function shouldStopPM2Process(status) {
117
118
  return !['stopped', 'errored', 'stopping', 'launching'].includes(String(status || '').toLowerCase());
118
119
  }
119
120
 
121
+ function isHttpsEnabledForProcess(processInfo) {
122
+ const envProtocol = processInfo?.pm2_env?.env?.CC_TOOL_WEB_UI_PROTOCOL;
123
+ if (typeof envProtocol === 'string' && envProtocol.trim()) {
124
+ return String(envProtocol).trim().toLowerCase() === 'https';
125
+ }
126
+
127
+ const args = processInfo?.pm2_env?.args;
128
+ if (Array.isArray(args)) {
129
+ return args.includes('--https');
130
+ }
131
+
132
+ return String(args || '').includes('--https');
133
+ }
134
+
120
135
  function getManagedPorts(config = loadConfig()) {
121
136
  return [
122
137
  config.ports?.webUI || 19999,
@@ -281,10 +296,14 @@ async function handleStart() {
281
296
 
282
297
  // 检查是否启用 LAN 访问 (--host 标志)
283
298
  const enableHost = process.argv.includes('--host');
299
+ const enableHttps = isHttpsEnabled();
284
300
  const pmArgs = ['ui', '--daemon'];
285
301
  if (enableHost) {
286
302
  pmArgs.push('--host');
287
303
  }
304
+ if (enableHttps) {
305
+ pmArgs.push('--https');
306
+ }
288
307
  require('fs').mkdirSync(PATHS.logs, { recursive: true });
289
308
 
290
309
  // 启动 PM2 进程
@@ -297,7 +316,8 @@ async function handleStart() {
297
316
  max_memory_restart: '500M',
298
317
  env: {
299
318
  NODE_ENV: 'production',
300
- CC_TOOL_PORT: port
319
+ CC_TOOL_PORT: port,
320
+ CC_TOOL_WEB_UI_PROTOCOL: enableHttps ? 'https' : 'http'
301
321
  },
302
322
  output: path.join(PATHS.logs, 'cc-tool-out.log'),
303
323
  error: path.join(PATHS.logs, 'cc-tool-out.log'),
@@ -335,10 +355,13 @@ async function handleStart() {
335
355
  }
336
356
 
337
357
  console.log(chalk.green('\n[OK] Coding-Tool 服务已启动(后台运行)\n'));
338
- console.log(chalk.gray(`Web UI: http://localhost:${port}`));
358
+ console.log(chalk.gray(`Web UI: ${getWebUiBaseUrl(port, {
359
+ https: enableHttps,
360
+ hostname: enableHttps ? '127.0.0.1' : 'localhost'
361
+ })}`));
339
362
  printPortToolIssue(readyState.degradedPortCheckIssue);
340
363
  if (enableHost) {
341
- console.log(chalk.yellow(`[WARN] LAN 访问已启用 (http://<your-ip>:${port})`));
364
+ console.log(chalk.yellow(`[WARN] LAN 访问已启用 (${enableHttps ? 'https' : 'http'}://<your-ip>:${port})`));
342
365
  }
343
366
  console.log(chalk.gray('\n可以安全关闭此终端窗口'));
344
367
  console.log(chalk.gray('\n常用命令:'));
@@ -431,8 +454,12 @@ async function handleStatus() {
431
454
  // UI 服务状态
432
455
  console.log(chalk.bold('[UI] Web UI 服务:'));
433
456
  if (existing && existing.pm2_env.status === 'online') {
457
+ const httpsEnabled = isHttpsEnabledForProcess(existing);
434
458
  console.log(chalk.green(' [OK] 状态: 运行中'));
435
- console.log(chalk.gray(` [NET] 地址: http://localhost:${config.ports?.webUI || 19999}`));
459
+ console.log(chalk.gray(` [NET] 地址: ${getWebUiBaseUrl(config.ports?.webUI || 19999, {
460
+ https: httpsEnabled,
461
+ hostname: httpsEnabled ? '127.0.0.1' : 'localhost'
462
+ })}`));
436
463
  console.log(chalk.gray(` [KEY] 进程 ID: ${existing.pid}`));
437
464
  console.log(chalk.gray(` [TIMER] 运行时长: ${formatUptime(existing.pm2_env.pm_uptime)}`));
438
465
  console.log(chalk.gray(` [SAVE] 内存使用: ${formatMemory(existing.monit?.memory)}`));
@@ -514,6 +541,7 @@ module.exports = {
514
541
  _test: {
515
542
  shouldTreatPortOwnershipAsReady,
516
543
  getManagedPorts,
517
- shouldStopPM2Process
544
+ shouldStopPM2Process,
545
+ isHttpsEnabledForProcess
518
546
  }
519
547
  };
@@ -9,6 +9,9 @@ const TOOL_ENDPOINTS = {
9
9
  gemini: '/api/gemini/statistics',
10
10
  opencode: '/api/opencode/statistics'
11
11
  };
12
+ const SHARED_TOOL_TYPE_BY_CLI = {
13
+ claude: 'claude-code'
14
+ };
12
15
 
13
16
  /**
14
17
  * HTTP 请求辅助函数
@@ -154,6 +157,34 @@ function extractSummary(stats) {
154
157
  return summary;
155
158
  }
156
159
 
160
+ function extractToolSummary(stats, toolType) {
161
+ const sharedToolType = SHARED_TOOL_TYPE_BY_CLI[toolType];
162
+ if (!sharedToolType) {
163
+ return extractSummary(stats);
164
+ }
165
+
166
+ // `/api/statistics/*` is shared across tools, so Claude must be scoped explicitly.
167
+ const toolStats = stats?.byToolType?.[sharedToolType] || {};
168
+ return extractSummary({
169
+ summary: {
170
+ requests: normalizeNumber(toolStats.requests),
171
+ tokens: normalizeNumber(toolStats.tokens?.total),
172
+ cost: normalizeNumber(toolStats.cost),
173
+ inputTokens: normalizeNumber(toolStats.tokens?.input),
174
+ outputTokens: normalizeNumber(toolStats.tokens?.output),
175
+ cacheCreation: normalizeNumber(toolStats.tokens?.cacheCreation),
176
+ cacheRead: normalizeNumber(toolStats.tokens?.cacheRead),
177
+ reasoningTokens: normalizeNumber(toolStats.tokens?.reasoning),
178
+ cachedTokens: normalizeNumber(toolStats.tokens?.cached)
179
+ },
180
+ global: {
181
+ totalRequests: normalizeNumber(toolStats.requests),
182
+ totalTokens: normalizeNumber(toolStats.tokens?.total),
183
+ totalCost: normalizeNumber(toolStats.cost)
184
+ }
185
+ });
186
+ }
187
+
157
188
  function mergeSummaries(target, source) {
158
189
  target.requests += normalizeNumber(source.requests);
159
190
  target.tokens += normalizeNumber(source.tokens);
@@ -181,12 +212,12 @@ async function fetchToolStats(toolType, timeRange) {
181
212
 
182
213
  if (timeRange === 'today') {
183
214
  const response = await httpRequest('GET', `${endpointBase}/today`);
184
- return extractSummary(response.data);
215
+ return extractToolSummary(response.data, toolType);
185
216
  }
186
217
 
187
218
  if (timeRange === 'all') {
188
219
  const response = await httpRequest('GET', `${endpointBase}/summary`);
189
- return extractSummary(response.data);
220
+ return extractToolSummary(response.data, toolType);
190
221
  }
191
222
 
192
223
  const days = getRangeDays(timeRange);
@@ -194,7 +225,7 @@ async function fetchToolStats(toolType, timeRange) {
194
225
  for (let i = 0; i < days; i++) {
195
226
  const date = getDateString(i);
196
227
  const response = await httpRequest('GET', `${endpointBase}/daily/${date}`);
197
- const dailySummary = extractSummary(response.data);
228
+ const dailySummary = extractToolSummary(response.data, toolType);
198
229
  mergeSummaries(merged, dailySummary);
199
230
  }
200
231
  return merged;
@@ -394,5 +425,11 @@ async function handleStatsExport(type = null, format = 'json') {
394
425
 
395
426
  module.exports = {
396
427
  handleStats,
397
- handleStatsExport
428
+ handleStatsExport,
429
+ _test: {
430
+ extractSummary,
431
+ extractToolSummary,
432
+ fetchToolStats,
433
+ fetchOverallStats
434
+ }
398
435
  };
@@ -3,6 +3,7 @@ const { startServer } = require('../server');
3
3
  const open = require('open');
4
4
  const { getProxyStatus } = require('../server/proxy-server');
5
5
  const { loadConfig } = require('../config/loader');
6
+ const { isHttpsEnabled, getWebUiBaseUrl } = require('../server/services/web-ui-runtime');
6
7
 
7
8
  async function handleUI() {
8
9
  // 检查是否为 daemon 模式(PM2 启动)
@@ -11,6 +12,8 @@ async function handleUI() {
11
12
  // 检查是否启用 LAN 访问 (--host 标志)
12
13
  const enableHost = process.argv.includes('--host');
13
14
  const host = enableHost ? '0.0.0.0' : '127.0.0.1';
15
+ const httpsEnabled = isHttpsEnabled();
16
+ process.env.CC_TOOL_WEB_UI_PROTOCOL = httpsEnabled ? 'https' : 'http';
14
17
 
15
18
  if (!isDaemon) {
16
19
  console.clear();
@@ -18,15 +21,21 @@ async function handleUI() {
18
21
  if (enableHost) {
19
22
  console.log(chalk.yellow('[WARN] LAN 访问已启用 (--host)\n'));
20
23
  }
24
+ if (httpsEnabled) {
25
+ console.log(chalk.yellow('[LOCK] 已启用原生 HTTPS (--https)\n'));
26
+ }
21
27
  }
22
28
 
23
29
  // 从配置加载端口
24
30
  const config = loadConfig();
25
31
  const port = config.ports?.webUI || 19999;
26
- const url = `http://localhost:${port}`;
32
+ const url = getWebUiBaseUrl(port, {
33
+ https: httpsEnabled,
34
+ hostname: httpsEnabled ? '127.0.0.1' : 'localhost'
35
+ });
27
36
 
28
37
  try {
29
- await startServer(port, host);
38
+ await startServer(port, host, { https: httpsEnabled });
30
39
 
31
40
  // 自动打开浏览器(仅非 daemon 模式)
32
41
  if (!isDaemon) {
@@ -87,7 +96,7 @@ async function handleUI() {
87
96
  console.log(chalk.gray('按 Ctrl+C 停止服务器'));
88
97
  } else {
89
98
  // Daemon 模式:保持运行
90
- console.log(chalk.green(`[OK] Coding-Tool 服务已在后台启动 (端口: ${port})`));
99
+ console.log(chalk.green(`[OK] Coding-Tool 服务已在后台启动 (${httpsEnabled ? 'HTTPS' : 'HTTP'}, 端口: ${port})`));
91
100
  }
92
101
 
93
102
  } catch (error) {
@@ -450,6 +450,12 @@ const PATHS = {
450
450
 
451
451
  // 脚本
452
452
  notifyHook: path.join(SCRIPTS_DIR, 'notify-hook.js'),
453
+ https: {
454
+ dir: path.join(CONFIG_DIR, 'https'),
455
+ key: path.join(CONFIG_DIR, 'https', 'localhost-key.pem'),
456
+ cert: path.join(CONFIG_DIR, 'https', 'localhost-cert.pem'),
457
+ meta: path.join(CONFIG_DIR, 'https', 'localhost-meta.json')
458
+ },
453
459
 
454
460
  // 技能与插件(cc-tool 托管部分)
455
461
  localSkills: {
package/src/index.js CHANGED
@@ -19,6 +19,7 @@ const eventBus = require('./plugins/event-bus');
19
19
  const chalk = require('chalk');
20
20
  const path = require('path');
21
21
  const fs = require('fs');
22
+ let processHandlersRegistered = false;
22
23
 
23
24
  function getInquirer() {
24
25
  return require('inquirer');
@@ -36,34 +37,66 @@ function showHelp() {
36
37
  const version = getVersion();
37
38
  console.log(chalk.cyan.bold(`\nCODING-TOOL v${version}`));
38
39
  console.log(chalk.gray('Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控\n'));
40
+ console.log(chalk.gray('用法: ctx [command] [options]'));
41
+ console.log(chalk.gray('不带参数时进入交互菜单;输入未知命令时会直接报错。\n'));
39
42
 
40
43
  console.log(chalk.yellow('[START] 服务管理:'));
41
44
  console.log(' ctx start 启动所有服务(后台运行)');
45
+ console.log(' ctx start --host 启动所有服务(后台运行,允许 LAN 访问)');
46
+ console.log(' ctx start --https 启动所有服务(后台运行,原生 HTTPS)');
42
47
  console.log(' ctx stop 停止所有服务');
43
48
  console.log(' ctx restart 重启所有服务');
44
49
  console.log(' ctx status 查看服务状态\n');
45
50
 
46
51
  console.log(chalk.yellow('[UI] UI 管理:'));
47
52
  console.log(' ctx ui 前台启动 Web UI(仅本地访问)');
53
+ console.log(' ctx ui --https 前台启动 Web UI(原生 HTTPS)');
48
54
  console.log(' ctx ui --host 前台启动 Web UI(允许 LAN 访问)');
49
- console.log(' ctx ui start 后台启动 Web UI');
55
+ console.log(' ctx ui start 后台启动 Web UI(等同于 ctx start)');
56
+ console.log(' ctx ui start --https 后台启动 Web UI(原生 HTTPS)');
50
57
  console.log(' ctx ui start --host 后台启动 Web UI(允许 LAN 访问)');
51
58
  console.log(' ctx ui stop 停止 Web UI');
52
59
  console.log(' ctx ui restart 重启 Web UI\n');
53
60
 
54
- console.log(chalk.yellow('[PROXY] 代理管理:'));
61
+ console.log(chalk.yellow('[CHANNEL] 渠道代理管理:'));
55
62
  console.log(' ctx claude start 启动 Claude 代理');
56
63
  console.log(' ctx claude stop 停止 Claude 代理');
64
+ console.log(' ctx claude restart 重启 Claude 代理');
57
65
  console.log(' ctx claude status 查看 Claude 代理状态');
58
66
  console.log(' ctx codex start 启动 Codex 代理');
67
+ console.log(' ctx codex stop 停止 Codex 代理');
68
+ console.log(' ctx codex restart 重启 Codex 代理');
69
+ console.log(' ctx codex status 查看 Codex 代理状态');
59
70
  console.log(' ctx gemini start 启动 Gemini 代理');
71
+ console.log(' ctx gemini stop 停止 Gemini 代理');
72
+ console.log(' ctx gemini restart 重启 Gemini 代理');
73
+ console.log(' ctx gemini status 查看 Gemini 代理状态');
60
74
  console.log(' ctx opencode start 启动 OpenCode 代理');
61
- console.log(chalk.gray(' (codex/gemini/opencode 命令与 claude 类似)\n'));
75
+ console.log(' ctx opencode stop 停止 OpenCode 代理');
76
+ console.log(' ctx opencode restart 重启 OpenCode 代理');
77
+ console.log(' ctx opencode status 查看 OpenCode 代理状态\n');
78
+
79
+ console.log(chalk.yellow('[PROXY] 旧版代理命令:'));
80
+ console.log(' ctx proxy 启动旧版 Claude 代理');
81
+ console.log(' ctx proxy start 启动旧版 Claude 代理');
82
+ console.log(' ctx proxy stop 停止旧版 Claude 代理并恢复配置');
83
+ console.log(' ctx proxy status 查看旧版 Claude 代理状态\n');
84
+
85
+ console.log(chalk.yellow('[COMPAT] 兼容命令:'));
86
+ console.log(' ctx daemon start 等同于 ctx start');
87
+ console.log(' ctx daemon stop 等同于 ctx stop');
88
+ console.log(' ctx daemon restart 等同于 ctx restart');
89
+ console.log(' ctx daemon status 等同于 ctx status');
90
+ console.log(' ctx daemon logs 查看 UI 日志');
91
+ console.log(' ctx daemon logs --follow 实时跟踪 UI 日志\n');
62
92
 
63
93
  console.log(chalk.yellow('[LOG] 日志管理:'));
64
94
  console.log(' ctx logs 查看所有日志');
65
95
  console.log(' ctx logs ui 查看 UI 日志');
66
96
  console.log(' ctx logs claude 查看 Claude 日志');
97
+ console.log(' ctx logs codex 查看 Codex 日志');
98
+ console.log(' ctx logs gemini 查看 Gemini 日志');
99
+ console.log(' ctx logs opencode 查看 OpenCode 日志');
67
100
  console.log(' ctx logs --lines 100 查看最近 100 行');
68
101
  console.log(' ctx logs --follow 实时跟踪日志');
69
102
  console.log(' ctx logs --clear 清空日志\n');
@@ -72,18 +105,24 @@ function showHelp() {
72
105
  console.log(' ctx stats 查看总体统计');
73
106
  console.log(' ctx stats claude 查看 Claude 统计');
74
107
  console.log(' ctx stats --today 查看今日统计');
108
+ console.log(' ctx stats --week 查看最近 7 天统计');
109
+ console.log(' ctx stats --month 查看最近 30 天统计');
75
110
  console.log(' ctx stats export 导出统计数据\n');
76
111
 
77
112
  console.log(chalk.yellow('[TOOL] 其他命令:'));
78
113
  console.log(' ctx update 检查并更新到最新版本');
114
+ console.log(' ctx update --check 仅检查更新,不执行安装');
79
115
  console.log(' ctx doctor 系统诊断');
80
116
  console.log(' ctx port 配置端口');
81
117
  console.log(' ctx reset 重置配置');
82
118
  console.log(' ctx security reset 关闭访问密码');
119
+ console.log(' ctx security disable 关闭访问密码(别名)');
120
+ console.log(' ctx security off 关闭访问密码(别名)');
121
+ console.log(' ctx help 显示帮助');
83
122
  console.log(' ctx --version, -v 显示版本');
84
123
  console.log(' ctx --help, -h 显示帮助\n');
85
124
 
86
- console.log(chalk.yellow('[PROXY] 插件管理:'));
125
+ console.log(chalk.yellow('[PLUGIN] 插件管理:'));
87
126
  console.log(' ctx plugin list 列出已安装插件');
88
127
  console.log(' ctx plugin install <url> 从 Git 安装插件');
89
128
  console.log(' ctx plugin remove <name> 卸载插件');
@@ -111,21 +150,44 @@ function showHelp() {
111
150
  console.log(chalk.gray(' 问题: https://github.com/ZeaoZhang/coding-tool/issues\n'));
112
151
  }
113
152
 
114
- // 全局错误处理
115
- process.on('uncaughtException', (err) => {
116
- // 忽略终端相关的错误(通常在 Ctrl+C 时发生)
117
- if (err.code === 'EIO' || err.code === 'ENOTTY' || err.code === 'EPIPE') {
118
- process.exit(0);
153
+ function registerProcessHandlers() {
154
+ if (processHandlersRegistered) {
155
+ return;
119
156
  }
120
- throw err;
121
- });
122
157
 
123
- // 处理 SIGINT 信号(Ctrl+C)
124
- process.on('SIGINT', async () => {
125
- eventBus.emitSync('cli:shutdown', {});
126
- PluginManager.shutdownPlugins();
127
- process.exit(0);
128
- });
158
+ processHandlersRegistered = true;
159
+
160
+ process.on('uncaughtException', (err) => {
161
+ // 忽略终端相关的错误(通常在 Ctrl+C 时发生)
162
+ if (err.code === 'EIO' || err.code === 'ENOTTY' || err.code === 'EPIPE') {
163
+ process.exit(0);
164
+ }
165
+ throw err;
166
+ });
167
+
168
+ process.on('SIGINT', async () => {
169
+ eventBus.emitSync('cli:shutdown', {});
170
+ PluginManager.shutdownPlugins();
171
+ process.exit(0);
172
+ });
173
+ }
174
+
175
+ function exitWithError(code = 1) {
176
+ process.exit(code);
177
+ }
178
+
179
+ function showUnknownCommand(command) {
180
+ console.error(chalk.red(`\n[ERROR] 未知命令: ${command}\n`));
181
+ console.log(chalk.gray('使用 ctx --help 查看完整帮助\n'));
182
+ }
183
+
184
+ function showUnknownSubcommand(command, subCommand, supportedText) {
185
+ console.error(chalk.red(`\n[ERROR] 未知 ${command} 子命令: ${subCommand}\n`));
186
+ if (supportedText) {
187
+ console.log(chalk.gray(`${supportedText}\n`));
188
+ }
189
+ console.log(chalk.gray('使用 ctx --help 查看完整帮助\n'));
190
+ }
129
191
 
130
192
  /**
131
193
  * 主函数
@@ -143,7 +205,7 @@ async function main() {
143
205
  }
144
206
 
145
207
  // --help 或 -h - 显示帮助信息
146
- if (args[0] === '--help' || args[0] === '-h') {
208
+ if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
147
209
  showHelp();
148
210
  return;
149
211
  }
@@ -184,8 +246,8 @@ async function main() {
184
246
  return;
185
247
  }
186
248
 
187
- console.log(chalk.red(`\n[ERROR] 未知 daemon 子命令: ${subCommand}\n`));
188
- console.log(chalk.gray('支持的命令: start, stop, restart, status, logs\n'));
249
+ showUnknownSubcommand('daemon', subCommand, '支持的命令: start, stop, restart, status, logs');
250
+ exitWithError(1);
189
251
  return;
190
252
  }
191
253
 
@@ -235,16 +297,18 @@ async function main() {
235
297
  // ui 命令 - Web UI 管理
236
298
  if (args[0] === 'ui') {
237
299
  const subCommand = args[1];
238
- if (subCommand === 'start') {
300
+ if (!subCommand || subCommand.startsWith('--')) {
301
+ const { handleUI } = require('./commands/ui');
302
+ await handleUI();
303
+ } else if (subCommand === 'start') {
239
304
  await handleStart(); // UI start 实际上就是启动整个服务
240
305
  } else if (subCommand === 'stop') {
241
306
  await handleStop();
242
307
  } else if (subCommand === 'restart') {
243
308
  await handleRestart();
244
309
  } else {
245
- // 默认前台运行
246
- const { handleUI } = require('./commands/ui');
247
- await handleUI();
310
+ showUnknownSubcommand('ui', subCommand, '支持的命令: start, stop, restart,或直接使用 ctx ui [--host] [--https]');
311
+ exitWithError(1);
248
312
  }
249
313
  return;
250
314
  }
@@ -271,6 +335,7 @@ async function main() {
271
335
  default:
272
336
  console.log(chalk.red(`\n[ERROR] 未知操作: ${action}\n`));
273
337
  console.log(chalk.gray('支持的操作: start, stop, restart, status\n'));
338
+ exitWithError(1);
274
339
  }
275
340
  return;
276
341
  }
@@ -356,8 +421,8 @@ async function main() {
356
421
  return;
357
422
 
358
423
  default:
359
- // 默认执行 start
360
- await handleProxyStart();
424
+ showUnknownSubcommand('proxy', subCommand, '支持的命令: start, stop, status');
425
+ exitWithError(1);
361
426
  return;
362
427
  }
363
428
  }
@@ -374,15 +439,17 @@ async function main() {
374
439
 
375
440
  // 初始化插件系统
376
441
  const pluginResult = PluginManager.initializePlugins({ config, args });
377
- if (pluginResult.loaded > 0) {
442
+ const isPluginCommand = args[0] && PluginManager.isPluginCommand(args[0]);
443
+ const shouldShowPluginInitSummary = !args[0] || isPluginCommand;
444
+ if (shouldShowPluginInitSummary && pluginResult.loaded > 0) {
378
445
  console.log(chalk.gray(`[Plugin] 已加载 ${pluginResult.loaded} 个插件`));
379
446
  }
380
- if (pluginResult.failed.length > 0) {
447
+ if (shouldShowPluginInitSummary && pluginResult.failed.length > 0) {
381
448
  console.log(chalk.yellow(`[Plugin] ${pluginResult.failed.length} 个插件加载失败`));
382
449
  }
383
450
 
384
451
  // 检查是否为插件注册的命令
385
- if (args[0] && PluginManager.isPluginCommand(args[0])) {
452
+ if (isPluginCommand) {
386
453
  eventBus.emitSync('cli:command:before', { command: args[0], args: args.slice(1), config });
387
454
  const result = await PluginManager.executePluginCommand(args[0], args.slice(1));
388
455
  eventBus.emitSync('cli:command:after', { command: args[0], args: args.slice(1), result });
@@ -393,6 +460,12 @@ async function main() {
393
460
  return;
394
461
  }
395
462
 
463
+ if (args[0]) {
464
+ showUnknownCommand(args[0]);
465
+ exitWithError(1);
466
+ return;
467
+ }
468
+
396
469
  while (true) {
397
470
  // 显示主菜单
398
471
  const { showMainMenu } = require('./ui/menu');
@@ -684,8 +757,26 @@ async function main() {
684
757
  }
685
758
  }
686
759
 
687
- // 启动应用
688
- main().catch((error) => {
689
- console.error('程序出错:', error);
690
- process.exit(1);
691
- });
760
+ async function runCli() {
761
+ registerProcessHandlers();
762
+ await main();
763
+ }
764
+
765
+ if (require.main === module) {
766
+ runCli().catch((error) => {
767
+ console.error('程序出错:', error);
768
+ process.exit(1);
769
+ });
770
+ }
771
+
772
+ module.exports = {
773
+ main,
774
+ runCli,
775
+ showHelp,
776
+ getVersion,
777
+ _test: {
778
+ showUnknownCommand,
779
+ showUnknownSubcommand,
780
+ registerProcessHandlers
781
+ }
782
+ };
@@ -11,6 +11,7 @@ const {
11
11
  } = require('../services/codex-sessions');
12
12
  const { isCodexInstalled } = require('../services/codex-config');
13
13
  const { loadAliases } = require('../services/alias');
14
+ const { buildLaunchCommand } = require('../services/session-launch-command');
14
15
 
15
16
  const DEBUG_CODEX_PERF = process.env.DEBUG_CODEX_PERF === '1';
16
17
 
@@ -515,9 +516,11 @@ module.exports = (config) => {
515
516
  timestamp: Date.now()
516
517
  });
517
518
 
518
- const command = `codex resume ${sessionId}`;
519
- const quotedCwd = `"${String(cwd).replace(/"/g, '\\"')}"`;
520
- const copyCommand = `cd ${quotedCwd} && ${command}`;
519
+ const { command, copyCommand } = buildLaunchCommand({
520
+ cwd,
521
+ executable: 'codex',
522
+ args: ['resume', sessionId]
523
+ });
521
524
 
522
525
  res.json({
523
526
  success: true,