coding-tool-x 3.5.6 → 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.
- package/README.md +17 -0
- package/bin/ctx.js +6 -1
- package/dist/web/assets/{Analytics-CRNCHeui.js → Analytics-C6DEmD3D.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-C0erJdo2.js → ConfigTemplates-Cf_iTpC4.js} +1 -1
- package/dist/web/assets/{Home-CL5z6Q4d.js → Home-BtBmYLJ1.js} +1 -1
- package/dist/web/assets/{PluginManager-hDx0XMO_.js → PluginManager-DEk8vSw5.js} +1 -1
- package/dist/web/assets/{ProjectList-BNsz96av.js → ProjectList-BMVhA_Kh.js} +1 -1
- package/dist/web/assets/{SessionList-CG1UhFo3.js → SessionList-B5ioAXxg.js} +1 -1
- package/dist/web/assets/{SkillManager-D6Vwpajh.js → SkillManager-DcZOiiSf.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-C3TjeOPy.js → WorkspaceManager-BHqI8aGV.js} +1 -1
- package/dist/web/assets/{index-GuER-BmS.js → index-CtByKdkA.js} +2 -2
- package/dist/web/index.html +1 -1
- package/docs/Caddyfile.example +19 -0
- package/docs/reverse-proxy-https.md +57 -0
- package/package.json +2 -1
- package/src/commands/daemon.js +33 -5
- package/src/commands/ui.js +12 -3
- package/src/config/paths.js +6 -0
- package/src/index.js +124 -34
- package/src/server/index.js +25 -5
- package/src/server/services/https-cert.js +171 -0
- package/src/server/services/network-access.js +47 -2
- package/src/server/services/notification-hooks.js +8 -2
- package/src/server/services/web-ui-runtime.js +54 -0
- package/src/server/websocket-server.js +11 -4
package/dist/web/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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-
|
|
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">
|
|
@@ -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.
|
|
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"
|
package/src/commands/daemon.js
CHANGED
|
@@ -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:
|
|
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] 地址:
|
|
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
|
};
|
package/src/commands/ui.js
CHANGED
|
@@ -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 =
|
|
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) {
|
package/src/config/paths.js
CHANGED
|
@@ -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,35 +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 启动所有服务(后台运行)');
|
|
42
45
|
console.log(' ctx start --host 启动所有服务(后台运行,允许 LAN 访问)');
|
|
46
|
+
console.log(' ctx start --https 启动所有服务(后台运行,原生 HTTPS)');
|
|
43
47
|
console.log(' ctx stop 停止所有服务');
|
|
44
48
|
console.log(' ctx restart 重启所有服务');
|
|
45
49
|
console.log(' ctx status 查看服务状态\n');
|
|
46
50
|
|
|
47
51
|
console.log(chalk.yellow('[UI] UI 管理:'));
|
|
48
52
|
console.log(' ctx ui 前台启动 Web UI(仅本地访问)');
|
|
53
|
+
console.log(' ctx ui --https 前台启动 Web UI(原生 HTTPS)');
|
|
49
54
|
console.log(' ctx ui --host 前台启动 Web UI(允许 LAN 访问)');
|
|
50
|
-
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)');
|
|
51
57
|
console.log(' ctx ui start --host 后台启动 Web UI(允许 LAN 访问)');
|
|
52
58
|
console.log(' ctx ui stop 停止 Web UI');
|
|
53
59
|
console.log(' ctx ui restart 重启 Web UI\n');
|
|
54
60
|
|
|
55
|
-
console.log(chalk.yellow('[
|
|
61
|
+
console.log(chalk.yellow('[CHANNEL] 渠道代理管理:'));
|
|
56
62
|
console.log(' ctx claude start 启动 Claude 代理');
|
|
57
63
|
console.log(' ctx claude stop 停止 Claude 代理');
|
|
64
|
+
console.log(' ctx claude restart 重启 Claude 代理');
|
|
58
65
|
console.log(' ctx claude status 查看 Claude 代理状态');
|
|
59
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 代理状态');
|
|
60
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 代理状态');
|
|
61
74
|
console.log(' ctx opencode start 启动 OpenCode 代理');
|
|
62
|
-
console.log(
|
|
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');
|
|
63
92
|
|
|
64
93
|
console.log(chalk.yellow('[LOG] 日志管理:'));
|
|
65
94
|
console.log(' ctx logs 查看所有日志');
|
|
66
95
|
console.log(' ctx logs ui 查看 UI 日志');
|
|
67
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 日志');
|
|
68
100
|
console.log(' ctx logs --lines 100 查看最近 100 行');
|
|
69
101
|
console.log(' ctx logs --follow 实时跟踪日志');
|
|
70
102
|
console.log(' ctx logs --clear 清空日志\n');
|
|
@@ -73,18 +105,24 @@ function showHelp() {
|
|
|
73
105
|
console.log(' ctx stats 查看总体统计');
|
|
74
106
|
console.log(' ctx stats claude 查看 Claude 统计');
|
|
75
107
|
console.log(' ctx stats --today 查看今日统计');
|
|
108
|
+
console.log(' ctx stats --week 查看最近 7 天统计');
|
|
109
|
+
console.log(' ctx stats --month 查看最近 30 天统计');
|
|
76
110
|
console.log(' ctx stats export 导出统计数据\n');
|
|
77
111
|
|
|
78
112
|
console.log(chalk.yellow('[TOOL] 其他命令:'));
|
|
79
113
|
console.log(' ctx update 检查并更新到最新版本');
|
|
114
|
+
console.log(' ctx update --check 仅检查更新,不执行安装');
|
|
80
115
|
console.log(' ctx doctor 系统诊断');
|
|
81
116
|
console.log(' ctx port 配置端口');
|
|
82
117
|
console.log(' ctx reset 重置配置');
|
|
83
118
|
console.log(' ctx security reset 关闭访问密码');
|
|
119
|
+
console.log(' ctx security disable 关闭访问密码(别名)');
|
|
120
|
+
console.log(' ctx security off 关闭访问密码(别名)');
|
|
121
|
+
console.log(' ctx help 显示帮助');
|
|
84
122
|
console.log(' ctx --version, -v 显示版本');
|
|
85
123
|
console.log(' ctx --help, -h 显示帮助\n');
|
|
86
124
|
|
|
87
|
-
console.log(chalk.yellow('[
|
|
125
|
+
console.log(chalk.yellow('[PLUGIN] 插件管理:'));
|
|
88
126
|
console.log(' ctx plugin list 列出已安装插件');
|
|
89
127
|
console.log(' ctx plugin install <url> 从 Git 安装插件');
|
|
90
128
|
console.log(' ctx plugin remove <name> 卸载插件');
|
|
@@ -112,21 +150,44 @@ function showHelp() {
|
|
|
112
150
|
console.log(chalk.gray(' 问题: https://github.com/ZeaoZhang/coding-tool/issues\n'));
|
|
113
151
|
}
|
|
114
152
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (err.code === 'EIO' || err.code === 'ENOTTY' || err.code === 'EPIPE') {
|
|
119
|
-
process.exit(0);
|
|
153
|
+
function registerProcessHandlers() {
|
|
154
|
+
if (processHandlersRegistered) {
|
|
155
|
+
return;
|
|
120
156
|
}
|
|
121
|
-
throw err;
|
|
122
|
-
});
|
|
123
157
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
}
|
|
130
191
|
|
|
131
192
|
/**
|
|
132
193
|
* 主函数
|
|
@@ -144,7 +205,7 @@ async function main() {
|
|
|
144
205
|
}
|
|
145
206
|
|
|
146
207
|
// --help 或 -h - 显示帮助信息
|
|
147
|
-
if (args[0] === '--help' || args[0] === '-h') {
|
|
208
|
+
if (args[0] === '--help' || args[0] === '-h' || args[0] === 'help') {
|
|
148
209
|
showHelp();
|
|
149
210
|
return;
|
|
150
211
|
}
|
|
@@ -185,8 +246,8 @@ async function main() {
|
|
|
185
246
|
return;
|
|
186
247
|
}
|
|
187
248
|
|
|
188
|
-
|
|
189
|
-
|
|
249
|
+
showUnknownSubcommand('daemon', subCommand, '支持的命令: start, stop, restart, status, logs');
|
|
250
|
+
exitWithError(1);
|
|
190
251
|
return;
|
|
191
252
|
}
|
|
192
253
|
|
|
@@ -236,16 +297,18 @@ async function main() {
|
|
|
236
297
|
// ui 命令 - Web UI 管理
|
|
237
298
|
if (args[0] === 'ui') {
|
|
238
299
|
const subCommand = args[1];
|
|
239
|
-
if (subCommand
|
|
300
|
+
if (!subCommand || subCommand.startsWith('--')) {
|
|
301
|
+
const { handleUI } = require('./commands/ui');
|
|
302
|
+
await handleUI();
|
|
303
|
+
} else if (subCommand === 'start') {
|
|
240
304
|
await handleStart(); // UI start 实际上就是启动整个服务
|
|
241
305
|
} else if (subCommand === 'stop') {
|
|
242
306
|
await handleStop();
|
|
243
307
|
} else if (subCommand === 'restart') {
|
|
244
308
|
await handleRestart();
|
|
245
309
|
} else {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
await handleUI();
|
|
310
|
+
showUnknownSubcommand('ui', subCommand, '支持的命令: start, stop, restart,或直接使用 ctx ui [--host] [--https]');
|
|
311
|
+
exitWithError(1);
|
|
249
312
|
}
|
|
250
313
|
return;
|
|
251
314
|
}
|
|
@@ -272,6 +335,7 @@ async function main() {
|
|
|
272
335
|
default:
|
|
273
336
|
console.log(chalk.red(`\n[ERROR] 未知操作: ${action}\n`));
|
|
274
337
|
console.log(chalk.gray('支持的操作: start, stop, restart, status\n'));
|
|
338
|
+
exitWithError(1);
|
|
275
339
|
}
|
|
276
340
|
return;
|
|
277
341
|
}
|
|
@@ -357,8 +421,8 @@ async function main() {
|
|
|
357
421
|
return;
|
|
358
422
|
|
|
359
423
|
default:
|
|
360
|
-
|
|
361
|
-
|
|
424
|
+
showUnknownSubcommand('proxy', subCommand, '支持的命令: start, stop, status');
|
|
425
|
+
exitWithError(1);
|
|
362
426
|
return;
|
|
363
427
|
}
|
|
364
428
|
}
|
|
@@ -375,15 +439,17 @@ async function main() {
|
|
|
375
439
|
|
|
376
440
|
// 初始化插件系统
|
|
377
441
|
const pluginResult = PluginManager.initializePlugins({ config, args });
|
|
378
|
-
|
|
442
|
+
const isPluginCommand = args[0] && PluginManager.isPluginCommand(args[0]);
|
|
443
|
+
const shouldShowPluginInitSummary = !args[0] || isPluginCommand;
|
|
444
|
+
if (shouldShowPluginInitSummary && pluginResult.loaded > 0) {
|
|
379
445
|
console.log(chalk.gray(`[Plugin] 已加载 ${pluginResult.loaded} 个插件`));
|
|
380
446
|
}
|
|
381
|
-
if (pluginResult.failed.length > 0) {
|
|
447
|
+
if (shouldShowPluginInitSummary && pluginResult.failed.length > 0) {
|
|
382
448
|
console.log(chalk.yellow(`[Plugin] ${pluginResult.failed.length} 个插件加载失败`));
|
|
383
449
|
}
|
|
384
450
|
|
|
385
451
|
// 检查是否为插件注册的命令
|
|
386
|
-
if (
|
|
452
|
+
if (isPluginCommand) {
|
|
387
453
|
eventBus.emitSync('cli:command:before', { command: args[0], args: args.slice(1), config });
|
|
388
454
|
const result = await PluginManager.executePluginCommand(args[0], args.slice(1));
|
|
389
455
|
eventBus.emitSync('cli:command:after', { command: args[0], args: args.slice(1), result });
|
|
@@ -394,6 +460,12 @@ async function main() {
|
|
|
394
460
|
return;
|
|
395
461
|
}
|
|
396
462
|
|
|
463
|
+
if (args[0]) {
|
|
464
|
+
showUnknownCommand(args[0]);
|
|
465
|
+
exitWithError(1);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
397
469
|
while (true) {
|
|
398
470
|
// 显示主菜单
|
|
399
471
|
const { showMainMenu } = require('./ui/menu');
|
|
@@ -685,8 +757,26 @@ async function main() {
|
|
|
685
757
|
}
|
|
686
758
|
}
|
|
687
759
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
+
};
|
package/src/server/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
+
const https = require('https');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const chalk = require('chalk');
|
|
4
5
|
const { loadConfig } = require('../config/loader');
|
|
@@ -23,6 +24,8 @@ const { startGeminiProxyServer } = require('./gemini-proxy-server');
|
|
|
23
24
|
const { startOpenCodeProxyServer, collectProxyModelList } = require('./opencode-proxy-server');
|
|
24
25
|
const { createRemoteMutationGuard, isRemoteMutationAllowed } = require('./services/network-access');
|
|
25
26
|
const { createApiRequestLogger } = require('./services/request-logger');
|
|
27
|
+
const { getLocalHttpsCredentials } = require('./services/https-cert');
|
|
28
|
+
const { getWebUiProtocol, isHttpsEnabled, getWebUiBaseUrl, getWebSocketBaseUrl } = require('./services/web-ui-runtime');
|
|
26
29
|
|
|
27
30
|
function getInquirer() {
|
|
28
31
|
return require('inquirer');
|
|
@@ -59,6 +62,8 @@ function printPortToolIssue(issue = getPortToolIssue()) {
|
|
|
59
62
|
async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
60
63
|
ensureStorageDirMigrated();
|
|
61
64
|
const config = loadConfig();
|
|
65
|
+
const webUiProtocol = getWebUiProtocol({ https: options.https });
|
|
66
|
+
const httpsEnabled = isHttpsEnabled({ protocol: webUiProtocol });
|
|
62
67
|
// 使用配置的端口,如果没有传入参数
|
|
63
68
|
if (!port) {
|
|
64
69
|
port = config.ports?.webUI || 19999;
|
|
@@ -247,7 +252,18 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
// Start server(确保监听成功后才返回,避免命令误报“已启动”)
|
|
250
|
-
|
|
255
|
+
let httpsCertificate = null;
|
|
256
|
+
const server = httpsEnabled
|
|
257
|
+
? (() => {
|
|
258
|
+
httpsCertificate = getLocalHttpsCredentials();
|
|
259
|
+
const httpsServer = https.createServer({
|
|
260
|
+
key: httpsCertificate.key,
|
|
261
|
+
cert: httpsCertificate.cert
|
|
262
|
+
}, app);
|
|
263
|
+
httpsServer.listen(port, host);
|
|
264
|
+
return httpsServer;
|
|
265
|
+
})()
|
|
266
|
+
: app.listen(port, host);
|
|
251
267
|
await new Promise((resolve) => {
|
|
252
268
|
const onListening = () => {
|
|
253
269
|
server.off('error', onError);
|
|
@@ -274,15 +290,19 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
|
|
|
274
290
|
console.log(`\n[START] Coding-Tool Web UI running at:`);
|
|
275
291
|
if (host === '0.0.0.0') {
|
|
276
292
|
console.log(chalk.yellow(` [WARN] 警告: 服务正在监听所有网络接口 (LAN 可访问)`));
|
|
277
|
-
console.log(`
|
|
278
|
-
console.log(chalk.gray(`
|
|
293
|
+
console.log(` ${getWebUiBaseUrl(port, { protocol: webUiProtocol })}`);
|
|
294
|
+
console.log(chalk.gray(` ${webUiProtocol}://<your-ip>:${port} (LAN 访问)`));
|
|
279
295
|
} else {
|
|
280
|
-
console.log(`
|
|
296
|
+
console.log(` ${getWebUiBaseUrl(port, { protocol: webUiProtocol })}`);
|
|
281
297
|
}
|
|
282
298
|
|
|
283
299
|
// 附加 WebSocket 服务器到同一个端口
|
|
284
300
|
attachWebSocketServer(server, { host });
|
|
285
|
-
console.log(`
|
|
301
|
+
console.log(` ${getWebSocketBaseUrl(port, { protocol: webUiProtocol })}/ws\n`);
|
|
302
|
+
|
|
303
|
+
if (httpsEnabled && httpsCertificate?.generated) {
|
|
304
|
+
console.log(chalk.gray(` [LOCK] 已生成本地 HTTPS 证书: ${httpsCertificate.certPath}`));
|
|
305
|
+
}
|
|
286
306
|
|
|
287
307
|
if (host === '0.0.0.0' && !allowRemoteMutation) {
|
|
288
308
|
console.log(chalk.yellow(' [LOCK] 已禁用 LAN 远程写操作 (CC_TOOL_ALLOW_REMOTE_WRITE=false)'));
|