@adversity/coding-tool-x 3.0.6 → 3.1.1
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/CHANGELOG.md +38 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
- package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
- package/dist/web/assets/Home-Di2qsylF.css +1 -0
- package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
- package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
- package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-Ufv5rCa5.css +1 -0
- package/dist/web/assets/index-lAkrRC3h.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +92 -13
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/ui.js +8 -1
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +39 -2
- package/src/config/loader.js +74 -8
- package/src/config/paths.js +105 -33
- package/src/index.js +67 -4
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +198 -0
- package/src/server/api/opencode-sessions.js +403 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +32 -19
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +17 -3
- package/src/server/index.js +164 -48
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +30 -19
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +70 -12
- package/src/server/services/codex-channels.js +61 -23
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +26 -12
- package/src/server/services/env-manager.js +126 -18
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +37 -15
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +206 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +663 -0
- package/src/server/services/opencode-settings-manager.js +342 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +132 -3
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-BxudHPiX.js +0 -1
- package/dist/web/assets/index-D2VfwJBa.js +0 -14
- package/dist/web/assets/index-oXBzu0bd.css +0 -41
- package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/permissions.js +0 -385
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -6,7 +6,7 @@ const os = require('os');
|
|
|
6
6
|
* 统计服务 - 数据采集和存储
|
|
7
7
|
*
|
|
8
8
|
* 文件结构:
|
|
9
|
-
* ~/.
|
|
9
|
+
* ~/.cc-tool/
|
|
10
10
|
* ├── statistics.json # 总体统计(实时更新)
|
|
11
11
|
* ├── daily-stats/
|
|
12
12
|
* │ ├── 2025-11-22.json # 每日汇总统计
|
|
@@ -20,7 +20,7 @@ const os = require('os');
|
|
|
20
20
|
|
|
21
21
|
// 获取基础目录
|
|
22
22
|
function getBaseDir() {
|
|
23
|
-
const dir = path.join(os.homedir(), '.
|
|
23
|
+
const dir = path.join(os.homedir(), '.cc-tool');
|
|
24
24
|
if (!fs.existsSync(dir)) {
|
|
25
25
|
fs.mkdirSync(dir, { recursive: true });
|
|
26
26
|
}
|
|
@@ -8,7 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const os = require('os');
|
|
9
9
|
|
|
10
10
|
// 配置文件路径
|
|
11
|
-
const CONFIG_DIR = path.join(os.homedir(), '.
|
|
11
|
+
const CONFIG_DIR = path.join(os.homedir(), '.cc-tool');
|
|
12
12
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'terminal-commands.json');
|
|
13
13
|
|
|
14
14
|
// 默认命令配置
|
|
@@ -30,6 +30,12 @@ const DEFAULT_COMMANDS = {
|
|
|
30
30
|
newSession: 'gemini',
|
|
31
31
|
resumeSession: 'gemini -r {sessionId}',
|
|
32
32
|
description: 'Google Gemini CLI'
|
|
33
|
+
},
|
|
34
|
+
opencode: {
|
|
35
|
+
name: 'OpenCode',
|
|
36
|
+
newSession: 'opencode',
|
|
37
|
+
resumeSession: 'opencode -r {sessionId}',
|
|
38
|
+
description: 'OpenCode AI coding agent for the terminal'
|
|
33
39
|
}
|
|
34
40
|
};
|
|
35
41
|
|
|
@@ -58,7 +64,8 @@ function loadTerminalCommands() {
|
|
|
58
64
|
return {
|
|
59
65
|
claude: { ...DEFAULT_COMMANDS.claude, ...saved.claude },
|
|
60
66
|
codex: { ...DEFAULT_COMMANDS.codex, ...saved.codex },
|
|
61
|
-
gemini: { ...DEFAULT_COMMANDS.gemini, ...saved.gemini }
|
|
67
|
+
gemini: { ...DEFAULT_COMMANDS.gemini, ...saved.gemini },
|
|
68
|
+
opencode: { ...DEFAULT_COMMANDS.opencode, ...saved.opencode }
|
|
62
69
|
};
|
|
63
70
|
}
|
|
64
71
|
} catch (err) {
|
|
@@ -79,7 +86,7 @@ function saveTerminalCommands(commands) {
|
|
|
79
86
|
|
|
80
87
|
// 验证配置格式
|
|
81
88
|
const validated = {};
|
|
82
|
-
for (const channel of ['claude', 'codex', 'gemini']) {
|
|
89
|
+
for (const channel of ['claude', 'codex', 'gemini', 'opencode']) {
|
|
83
90
|
if (commands[channel]) {
|
|
84
91
|
validated[channel] = {
|
|
85
92
|
name: commands[channel].name || DEFAULT_COMMANDS[channel].name,
|
|
@@ -7,7 +7,7 @@ const { detectAvailableTerminals, getDefaultTerminal, getSystemShell } = require
|
|
|
7
7
|
* 获取配置文件路径
|
|
8
8
|
*/
|
|
9
9
|
function getConfigFilePath() {
|
|
10
|
-
const ccToolDir = path.join(os.homedir(), '.
|
|
10
|
+
const ccToolDir = path.join(os.homedir(), '.cc-tool');
|
|
11
11
|
if (!fs.existsSync(ccToolDir)) {
|
|
12
12
|
fs.mkdirSync(ccToolDir, { recursive: true });
|
|
13
13
|
}
|
|
@@ -2,7 +2,7 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
|
|
5
|
-
const UI_CONFIG_DIR = path.join(os.homedir(), '.
|
|
5
|
+
const UI_CONFIG_DIR = path.join(os.homedir(), '.cc-tool');
|
|
6
6
|
const UI_CONFIG_FILE = path.join(UI_CONFIG_DIR, 'ui-config.json');
|
|
7
7
|
|
|
8
8
|
// Default UI config
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
// 多项目工作区服务
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const {
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
5
5
|
const { PATHS } = require('../../config/paths');
|
|
6
6
|
const configTemplatesService = require('./config-templates-service');
|
|
7
|
-
const permissionTemplatesService = require('./permission-templates-service');
|
|
8
7
|
|
|
9
8
|
// 工作区配置文件路径
|
|
10
9
|
const WORKSPACES_CONFIG = path.join(PATHS.base, 'workspaces.json');
|
|
11
10
|
|
|
11
|
+
function runGitCommand(args, options = {}) {
|
|
12
|
+
const execOptions = {
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
15
|
+
...options
|
|
16
|
+
};
|
|
17
|
+
return execFileSync('git', args, execOptions);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveCurrentBranch(repoPath) {
|
|
21
|
+
try {
|
|
22
|
+
return runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
23
|
+
cwd: repoPath
|
|
24
|
+
}).trim();
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return 'main';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
/**
|
|
13
31
|
* 生成唯一工作区 ID
|
|
14
32
|
*/
|
|
@@ -110,9 +128,8 @@ function getGitWorktrees(repoPath) {
|
|
|
110
128
|
if (!isGitRepo(repoPath)) {
|
|
111
129
|
return [];
|
|
112
130
|
}
|
|
113
|
-
const output =
|
|
114
|
-
cwd: repoPath
|
|
115
|
-
encoding: 'utf8'
|
|
131
|
+
const output = runGitCommand(['worktree', 'list', '--porcelain'], {
|
|
132
|
+
cwd: repoPath
|
|
116
133
|
});
|
|
117
134
|
|
|
118
135
|
const worktrees = [];
|
|
@@ -152,7 +169,7 @@ function getGitWorktrees(repoPath) {
|
|
|
152
169
|
* @param {Array} options.projects - 项目列表 [{sourcePath, name, createWorktree, branch}]
|
|
153
170
|
*/
|
|
154
171
|
function createWorkspace(options) {
|
|
155
|
-
const { name, description = '', baseDir, projects = [], configTemplateId
|
|
172
|
+
const { name, description = '', baseDir, projects = [], configTemplateId } = options;
|
|
156
173
|
|
|
157
174
|
if (!name || name.trim() === '') {
|
|
158
175
|
throw new Error('工作区名称不能为空');
|
|
@@ -208,10 +225,7 @@ function createWorkspace(options) {
|
|
|
208
225
|
let targetBranch = branch;
|
|
209
226
|
if (!targetBranch) {
|
|
210
227
|
try {
|
|
211
|
-
targetBranch =
|
|
212
|
-
cwd: sourcePath,
|
|
213
|
-
encoding: 'utf8'
|
|
214
|
-
}).trim();
|
|
228
|
+
targetBranch = resolveCurrentBranch(sourcePath);
|
|
215
229
|
} catch (e) {
|
|
216
230
|
targetBranch = 'main';
|
|
217
231
|
}
|
|
@@ -279,10 +293,7 @@ function createWorkspace(options) {
|
|
|
279
293
|
let targetBranch = branch;
|
|
280
294
|
if (!targetBranch) {
|
|
281
295
|
try {
|
|
282
|
-
targetBranch =
|
|
283
|
-
cwd: sourcePath,
|
|
284
|
-
encoding: 'utf8'
|
|
285
|
-
}).trim();
|
|
296
|
+
targetBranch = resolveCurrentBranch(sourcePath);
|
|
286
297
|
} catch (e) {
|
|
287
298
|
targetBranch = 'main';
|
|
288
299
|
}
|
|
@@ -305,9 +316,8 @@ function createWorkspace(options) {
|
|
|
305
316
|
} else {
|
|
306
317
|
try {
|
|
307
318
|
// 尝试检出已有分支
|
|
308
|
-
|
|
309
|
-
cwd: sourcePath
|
|
310
|
-
stdio: 'pipe'
|
|
319
|
+
runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
|
|
320
|
+
cwd: sourcePath
|
|
311
321
|
});
|
|
312
322
|
|
|
313
323
|
targetPath = worktreePath;
|
|
@@ -318,17 +328,12 @@ function createWorkspace(options) {
|
|
|
318
328
|
} catch (error) {
|
|
319
329
|
// 如果分支不存在,尝试创建新分支
|
|
320
330
|
try {
|
|
321
|
-
|
|
322
|
-
let worktreeCmd = `git worktree add "${worktreePath}" -b "${targetBranch}"`;
|
|
323
|
-
|
|
324
|
-
// 如果指定了基础分支,添加到命令中
|
|
331
|
+
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
325
332
|
if (baseBranch && baseBranch.trim()) {
|
|
326
|
-
|
|
333
|
+
worktreeArgs.push(baseBranch.trim());
|
|
327
334
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
cwd: sourcePath,
|
|
331
|
-
stdio: 'pipe'
|
|
335
|
+
runGitCommand(worktreeArgs, {
|
|
336
|
+
cwd: sourcePath
|
|
332
337
|
});
|
|
333
338
|
targetPath = worktreePath;
|
|
334
339
|
worktrees.push({
|
|
@@ -387,55 +392,6 @@ function createWorkspace(options) {
|
|
|
387
392
|
}
|
|
388
393
|
}
|
|
389
394
|
|
|
390
|
-
// 应用权限模板(如果指定)
|
|
391
|
-
let permissionInfo = null;
|
|
392
|
-
if (permissionTemplate) {
|
|
393
|
-
try {
|
|
394
|
-
// 从权限模板服务获取模板
|
|
395
|
-
const template = permissionTemplatesService.getTemplateById(permissionTemplate);
|
|
396
|
-
|
|
397
|
-
if (template && template.permissions) {
|
|
398
|
-
// 为工作区中的每个项目应用权限设置
|
|
399
|
-
for (const proj of workspaceProjects) {
|
|
400
|
-
const projSettingsDir = path.join(proj.targetPath, '.claude');
|
|
401
|
-
const projSettingsFile = path.join(projSettingsDir, 'settings.json');
|
|
402
|
-
|
|
403
|
-
// 确保 .claude 目录存在
|
|
404
|
-
if (!fs.existsSync(projSettingsDir)) {
|
|
405
|
-
fs.mkdirSync(projSettingsDir, { recursive: true });
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// 读取现有设置或创建新的
|
|
409
|
-
let settings = {};
|
|
410
|
-
if (fs.existsSync(projSettingsFile)) {
|
|
411
|
-
try {
|
|
412
|
-
settings = JSON.parse(fs.readFileSync(projSettingsFile, 'utf8'));
|
|
413
|
-
} catch (e) {
|
|
414
|
-
settings = {};
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// 更新权限设置
|
|
419
|
-
settings.permissions = {
|
|
420
|
-
allow: template.permissions.allow || [],
|
|
421
|
-
deny: template.permissions.deny || []
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
// 保存设置
|
|
425
|
-
fs.writeFileSync(projSettingsFile, JSON.stringify(settings, null, 2), 'utf8');
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
permissionInfo = {
|
|
429
|
-
template: permissionTemplate,
|
|
430
|
-
appliedAt: new Date().toISOString()
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
} catch (permError) {
|
|
434
|
-
console.warn('应用权限模板失败:', permError.message);
|
|
435
|
-
// 不中断工作区创建流程
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
395
|
// 保存工作区配置
|
|
440
396
|
const workspaceId = generateWorkspaceId();
|
|
441
397
|
const workspace = {
|
|
@@ -445,7 +401,6 @@ function createWorkspace(options) {
|
|
|
445
401
|
path: workspacePath,
|
|
446
402
|
projects: workspaceProjects,
|
|
447
403
|
configTemplate: templateInfo,
|
|
448
|
-
permissionTemplate: permissionInfo,
|
|
449
404
|
createdAt: new Date().toISOString(),
|
|
450
405
|
lastUsed: new Date().toISOString()
|
|
451
406
|
};
|
|
@@ -494,9 +449,8 @@ function deleteWorkspace(id, removeFiles = false) {
|
|
|
494
449
|
if (wt.path && wt.path.includes('-ws-')) {
|
|
495
450
|
try {
|
|
496
451
|
console.log(`清理 worktree: ${wt.path}`);
|
|
497
|
-
|
|
498
|
-
cwd: proj.sourcePath
|
|
499
|
-
stdio: 'pipe'
|
|
452
|
+
runGitCommand(['worktree', 'remove', wt.path, '--force'], {
|
|
453
|
+
cwd: proj.sourcePath
|
|
500
454
|
});
|
|
501
455
|
} catch (error) {
|
|
502
456
|
console.error(`删除 worktree 失败: ${wt.path}`, error.message);
|
|
@@ -585,10 +539,7 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
|
|
|
585
539
|
let targetBranch = branch;
|
|
586
540
|
if (!targetBranch) {
|
|
587
541
|
try {
|
|
588
|
-
targetBranch =
|
|
589
|
-
cwd: sourcePath,
|
|
590
|
-
encoding: 'utf8'
|
|
591
|
-
}).trim();
|
|
542
|
+
targetBranch = resolveCurrentBranch(sourcePath);
|
|
592
543
|
} catch (e) {
|
|
593
544
|
targetBranch = 'main';
|
|
594
545
|
}
|
|
@@ -605,9 +556,8 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
|
|
|
605
556
|
worktrees.push({ branch: targetBranch, path: worktreePath });
|
|
606
557
|
} else {
|
|
607
558
|
try {
|
|
608
|
-
|
|
609
|
-
cwd: sourcePath
|
|
610
|
-
stdio: 'pipe'
|
|
559
|
+
runGitCommand(['worktree', 'add', worktreePath, targetBranch], {
|
|
560
|
+
cwd: sourcePath
|
|
611
561
|
});
|
|
612
562
|
targetPath = worktreePath;
|
|
613
563
|
worktrees.push({ branch: targetBranch, path: worktreePath });
|
|
@@ -626,17 +576,12 @@ function addProjectToWorkspace(workspaceId, projectConfig) {
|
|
|
626
576
|
|
|
627
577
|
// Branch doesn't exist, try creating it
|
|
628
578
|
try {
|
|
629
|
-
|
|
630
|
-
let worktreeCmd = `git worktree add "${worktreePath}" -b "${targetBranch}"`;
|
|
631
|
-
|
|
632
|
-
// 如果指定了基础分支,添加到命令中
|
|
579
|
+
const worktreeArgs = ['worktree', 'add', worktreePath, '-b', targetBranch];
|
|
633
580
|
if (baseBranch && baseBranch.trim()) {
|
|
634
|
-
|
|
581
|
+
worktreeArgs.push(baseBranch.trim());
|
|
635
582
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
cwd: sourcePath,
|
|
639
|
-
stdio: 'pipe'
|
|
583
|
+
runGitCommand(worktreeArgs, {
|
|
584
|
+
cwd: sourcePath
|
|
640
585
|
});
|
|
641
586
|
targetPath = worktreePath;
|
|
642
587
|
worktrees.push({ branch: targetBranch, path: worktreePath });
|
|
@@ -711,9 +656,8 @@ function removeProjectFromWorkspace(workspaceId, projectName, removeWorktrees =
|
|
|
711
656
|
for (const wt of project.worktrees) {
|
|
712
657
|
if (fs.existsSync(wt.path)) {
|
|
713
658
|
try {
|
|
714
|
-
|
|
715
|
-
cwd: project.sourcePath
|
|
716
|
-
stdio: 'pipe'
|
|
659
|
+
runGitCommand(['worktree', 'remove', wt.path, '--force'], {
|
|
660
|
+
cwd: project.sourcePath
|
|
717
661
|
});
|
|
718
662
|
} catch (error) {
|
|
719
663
|
console.error(`删除 worktree 失败: ${wt.path}`, error.message);
|
|
@@ -739,8 +683,10 @@ async function getAllAvailableProjects() {
|
|
|
739
683
|
const sessionsService = require('./sessions');
|
|
740
684
|
const codexSessionsService = require('./codex-sessions');
|
|
741
685
|
const geminiSessionsService = require('./gemini-sessions');
|
|
686
|
+
const opencodeSessionsService = require('./opencode-sessions');
|
|
742
687
|
const { isCodexInstalled } = require('./codex-config');
|
|
743
688
|
const { isGeminiInstalled } = require('./gemini-config');
|
|
689
|
+
const { isOpenCodeInstalled } = require('./opencode-sessions');
|
|
744
690
|
|
|
745
691
|
const allProjects = [];
|
|
746
692
|
const seenKeys = new Set();
|
|
@@ -800,6 +746,16 @@ async function getAllAvailableProjects() {
|
|
|
800
746
|
console.error('获取 gemini 项目失败:', error.message);
|
|
801
747
|
}
|
|
802
748
|
|
|
749
|
+
try {
|
|
750
|
+
if (isOpenCodeInstalled()) {
|
|
751
|
+
const opencodeProjects = opencodeSessionsService.getProjects();
|
|
752
|
+
const list = Array.isArray(opencodeProjects) ? opencodeProjects : [];
|
|
753
|
+
list.forEach(project => addProject('opencode', project));
|
|
754
|
+
}
|
|
755
|
+
} catch (error) {
|
|
756
|
+
console.error('获取 opencode 项目失败:', error.message);
|
|
757
|
+
}
|
|
758
|
+
|
|
803
759
|
// 按最后使用时间排序
|
|
804
760
|
allProjects.sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0));
|
|
805
761
|
|
|
@@ -809,7 +765,7 @@ async function getAllAvailableProjects() {
|
|
|
809
765
|
/**
|
|
810
766
|
* 在工作区中启动 CLI 工具
|
|
811
767
|
* @param {string} workspaceId 工作区 ID
|
|
812
|
-
* @param {string} tool 工具名称 (claude/codex/gemini)
|
|
768
|
+
* @param {string} tool 工具名称 (claude/codex/gemini/opencode)
|
|
813
769
|
* @param {string} projectName 可选,工作区内的项目名
|
|
814
770
|
* @returns {object} 启动信息
|
|
815
771
|
*/
|
|
@@ -840,7 +796,8 @@ function getLaunchCommand(workspaceId, tool, projectName = null) {
|
|
|
840
796
|
const commands = {
|
|
841
797
|
claude: 'claude',
|
|
842
798
|
codex: 'codex',
|
|
843
|
-
gemini: 'gemini'
|
|
799
|
+
gemini: 'gemini',
|
|
800
|
+
opencode: 'opencode'
|
|
844
801
|
};
|
|
845
802
|
|
|
846
803
|
const cmd = commands[tool];
|
|
@@ -5,6 +5,11 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const { loadConfig } = require('../config/loader');
|
|
7
7
|
const { ptyManager } = require('./services/pty-manager');
|
|
8
|
+
const {
|
|
9
|
+
normalizeAddress,
|
|
10
|
+
isLoopbackAddress,
|
|
11
|
+
isLoopbackRequest
|
|
12
|
+
} = require('./services/network-access');
|
|
8
13
|
|
|
9
14
|
const MAX_PERSISTED_LOGS = 500;
|
|
10
15
|
|
|
@@ -24,10 +29,116 @@ function getMaxLogsLimit() {
|
|
|
24
29
|
|
|
25
30
|
let wss = null;
|
|
26
31
|
let wsClients = new Set();
|
|
32
|
+
let websocketOptions = {
|
|
33
|
+
host: '127.0.0.1',
|
|
34
|
+
allowRemoteTerminal: false
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function parseHostHeader(hostHeader) {
|
|
38
|
+
const value = String(hostHeader || '').trim();
|
|
39
|
+
if (!value) {
|
|
40
|
+
return { hostname: '', port: '' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (value.startsWith('[')) {
|
|
44
|
+
const closingBracket = value.indexOf(']');
|
|
45
|
+
if (closingBracket > 0) {
|
|
46
|
+
const hostname = value.slice(1, closingBracket);
|
|
47
|
+
const rest = value.slice(closingBracket + 1);
|
|
48
|
+
const port = rest.startsWith(':') ? rest.slice(1) : '';
|
|
49
|
+
return { hostname, port };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const separator = value.lastIndexOf(':');
|
|
54
|
+
if (separator > -1 && value.indexOf(':') === separator) {
|
|
55
|
+
return {
|
|
56
|
+
hostname: value.slice(0, separator),
|
|
57
|
+
port: value.slice(separator + 1)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { hostname: value, port: '' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function defaultPortForProtocol(protocol) {
|
|
65
|
+
if (protocol === 'https:') {
|
|
66
|
+
return '443';
|
|
67
|
+
}
|
|
68
|
+
return '80';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isAllowedWebSocketOrigin(req) {
|
|
72
|
+
if (!req || !req.headers) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const originHeader = req.headers.origin;
|
|
77
|
+
if (!originHeader) {
|
|
78
|
+
// 非浏览器客户端通常不会携带 Origin,仅允许本机来源
|
|
79
|
+
return isLoopbackRequest(req);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let originUrl;
|
|
83
|
+
try {
|
|
84
|
+
originUrl = new URL(originHeader);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (originUrl.protocol !== 'http:' && originUrl.protocol !== 'https:') {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const requestHost = parseHostHeader(req.headers.host);
|
|
94
|
+
const requestProtocol = req.socket && req.socket.encrypted ? 'https:' : 'http:';
|
|
95
|
+
const requestHostname = normalizeAddress(requestHost.hostname).toLowerCase();
|
|
96
|
+
const requestPort = requestHost.port || defaultPortForProtocol(requestProtocol);
|
|
97
|
+
|
|
98
|
+
const originHostname = normalizeAddress(originUrl.hostname).toLowerCase();
|
|
99
|
+
const originPort = originUrl.port || defaultPortForProtocol(originUrl.protocol);
|
|
100
|
+
|
|
101
|
+
if (!requestHostname || !originHostname) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 同源直接放行
|
|
106
|
+
if (originHostname === requestHostname && originPort === requestPort) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 允许本机开发代理(例如 Vite 5000 -> 19999)
|
|
111
|
+
if (isLoopbackRequest(req) && isLoopbackAddress(originHostname) && isLoopbackAddress(requestHostname)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function installOriginGuard(server) {
|
|
119
|
+
if (!server || typeof server.shouldHandle !== 'function') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const originalShouldHandle = server.shouldHandle.bind(server);
|
|
124
|
+
server.shouldHandle = (req) => {
|
|
125
|
+
if (!originalShouldHandle(req)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const allowed = isAllowedWebSocketOrigin(req);
|
|
130
|
+
if (!allowed) {
|
|
131
|
+
const origin = req.headers.origin || 'unknown';
|
|
132
|
+
const clientIp = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : 'unknown';
|
|
133
|
+
console.warn(`[WebSocket] Rejected connection from ${clientIp}, origin: ${origin}`);
|
|
134
|
+
}
|
|
135
|
+
return allowed;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
27
138
|
|
|
28
139
|
// 日志持久化文件路径
|
|
29
140
|
function getLogsFilePath() {
|
|
30
|
-
const ccToolDir = path.join(os.homedir(), '.
|
|
141
|
+
const ccToolDir = path.join(os.homedir(), '.cc-tool');
|
|
31
142
|
if (!fs.existsSync(ccToolDir)) {
|
|
32
143
|
fs.mkdirSync(ccToolDir, { recursive: true });
|
|
33
144
|
}
|
|
@@ -141,12 +252,17 @@ function saveLogsToFile(logs) {
|
|
|
141
252
|
let logsCache = [];
|
|
142
253
|
|
|
143
254
|
// 启动 WebSocket 服务器(附加到现有的 HTTP 服务器)
|
|
144
|
-
function startWebSocketServer(httpServer) {
|
|
255
|
+
function startWebSocketServer(httpServer, options = {}) {
|
|
145
256
|
if (wss) {
|
|
146
257
|
console.log('WebSocket server already running');
|
|
147
258
|
return;
|
|
148
259
|
}
|
|
149
260
|
|
|
261
|
+
websocketOptions = {
|
|
262
|
+
host: options.host || '127.0.0.1',
|
|
263
|
+
allowRemoteTerminal: options.allowRemoteTerminal === true
|
|
264
|
+
};
|
|
265
|
+
|
|
150
266
|
// 加载持久化的日志到缓存
|
|
151
267
|
logsCache = loadPersistedLogs();
|
|
152
268
|
const counts = logsCache.reduce((acc, log) => {
|
|
@@ -163,15 +279,17 @@ function startWebSocketServer(httpServer) {
|
|
|
163
279
|
server: httpServer,
|
|
164
280
|
path: '/ws' // 指定 WebSocket 路径
|
|
165
281
|
});
|
|
282
|
+
installOriginGuard(wss);
|
|
166
283
|
console.log(`✅ WebSocket server attached to HTTP server at /ws`);
|
|
167
284
|
} else {
|
|
168
285
|
// 创建独立的 WebSocket 服务器,使用配置的 webUI 端口
|
|
169
286
|
const config = loadConfig();
|
|
170
|
-
const port = config.ports?.webUI ||
|
|
287
|
+
const port = config.ports?.webUI || 19999;
|
|
171
288
|
wss = new WebSocket.Server({
|
|
172
289
|
port,
|
|
173
290
|
path: '/ws'
|
|
174
291
|
});
|
|
292
|
+
installOriginGuard(wss);
|
|
175
293
|
console.log(`✅ WebSocket server started on ws://127.0.0.1:${port}/ws`);
|
|
176
294
|
}
|
|
177
295
|
|
|
@@ -185,6 +303,9 @@ function startWebSocketServer(httpServer) {
|
|
|
185
303
|
ws.isAlive = true;
|
|
186
304
|
// 终端绑定信息
|
|
187
305
|
ws.terminalId = null;
|
|
306
|
+
const isLoopback = isLoopbackRequest(req);
|
|
307
|
+
const lanMode = websocketOptions.host === '0.0.0.0';
|
|
308
|
+
ws.terminalAllowed = !(lanMode && !isLoopback && !websocketOptions.allowRemoteTerminal);
|
|
188
309
|
|
|
189
310
|
// 发送历史日志给新连接的客户端
|
|
190
311
|
if (logsCache.length > 0) {
|
|
@@ -400,6 +521,14 @@ const { getWebTerminalShellConfig } = require('./services/terminal-config');
|
|
|
400
521
|
function handleTerminalMessage(ws, message) {
|
|
401
522
|
const { type } = message;
|
|
402
523
|
|
|
524
|
+
if (String(type || '').startsWith('terminal:') && ws.terminalAllowed === false) {
|
|
525
|
+
ws.send(JSON.stringify({
|
|
526
|
+
type: 'terminal:error',
|
|
527
|
+
error: '出于安全考虑,LAN 模式下仅允许本机使用 Web 终端。可设置 CC_TOOL_ALLOW_REMOTE_TERMINAL=true 覆盖。'
|
|
528
|
+
}));
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
403
532
|
switch (type) {
|
|
404
533
|
case 'terminal:create':
|
|
405
534
|
handleTerminalCreate(ws, message);
|
package/src/ui/menu.js
CHANGED
|
@@ -3,6 +3,49 @@ const inquirer = require('inquirer');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const packageInfo = require('../../package.json');
|
|
5
5
|
|
|
6
|
+
function normalizeCliType(type) {
|
|
7
|
+
if (type === 'claude' || type === 'codex' || type === 'gemini' || type === 'opencode') {
|
|
8
|
+
return type;
|
|
9
|
+
}
|
|
10
|
+
return 'claude';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function pickActiveChannel(channels) {
|
|
14
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return channels.find(channel => channel.enabled !== false) || channels[0];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getChannelAndProxyStatus(cliType) {
|
|
21
|
+
const currentType = normalizeCliType(cliType);
|
|
22
|
+
|
|
23
|
+
if (currentType === 'claude') {
|
|
24
|
+
const { getCurrentChannel } = require('../server/services/channels');
|
|
25
|
+
const { getProxyStatus } = require('../server/proxy-server');
|
|
26
|
+
return { channel: getCurrentChannel(), proxyStatus: getProxyStatus() };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (currentType === 'codex') {
|
|
30
|
+
const { getChannels } = require('../server/services/codex-channels');
|
|
31
|
+
const { getCodexProxyStatus } = require('../server/codex-proxy-server');
|
|
32
|
+
const data = getChannels();
|
|
33
|
+
return { channel: pickActiveChannel(data?.channels), proxyStatus: getCodexProxyStatus() };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (currentType === 'gemini') {
|
|
37
|
+
const { getChannels } = require('../server/services/gemini-channels');
|
|
38
|
+
const { getGeminiProxyStatus } = require('../server/gemini-proxy-server');
|
|
39
|
+
const data = getChannels();
|
|
40
|
+
return { channel: pickActiveChannel(data?.channels), proxyStatus: getGeminiProxyStatus() };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const { getChannels } = require('../server/services/opencode-channels');
|
|
44
|
+
const { getOpenCodeProxyStatus } = require('../server/opencode-proxy-server');
|
|
45
|
+
const data = getChannels();
|
|
46
|
+
return { channel: pickActiveChannel(data?.channels), proxyStatus: getOpenCodeProxyStatus() };
|
|
47
|
+
}
|
|
48
|
+
|
|
6
49
|
/**
|
|
7
50
|
* 显示主菜单
|
|
8
51
|
*/
|
|
@@ -15,10 +58,11 @@ async function showMainMenu(config) {
|
|
|
15
58
|
const cliTypes = {
|
|
16
59
|
claude: { name: 'Claude Code', color: 'cyan' },
|
|
17
60
|
codex: { name: 'Codex', color: 'green' },
|
|
18
|
-
gemini: { name: 'Gemini', color: 'magenta' }
|
|
61
|
+
gemini: { name: 'Gemini', color: 'magenta' },
|
|
62
|
+
opencode: { name: 'OpenCode', color: 'yellow' }
|
|
19
63
|
};
|
|
20
|
-
const currentType = config.currentCliType || 'claude';
|
|
21
|
-
const typeInfo = cliTypes[currentType];
|
|
64
|
+
const currentType = normalizeCliType(config.currentCliType || 'claude');
|
|
65
|
+
const typeInfo = cliTypes[currentType] || cliTypes.claude;
|
|
22
66
|
console.log(chalk[typeInfo.color](`当前类型: ${typeInfo.name}`));
|
|
23
67
|
|
|
24
68
|
const projectName = config.currentProject
|
|
@@ -28,27 +72,7 @@ async function showMainMenu(config) {
|
|
|
28
72
|
|
|
29
73
|
// 显示当前渠道和代理状态(根据类型显示对应的渠道和代理)
|
|
30
74
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (currentType === 'claude') {
|
|
34
|
-
const { getCurrentChannel } = require('../server/services/channels');
|
|
35
|
-
const { getProxyStatus } = require('../server/proxy-server');
|
|
36
|
-
getCurrentChannelFunc = getCurrentChannel;
|
|
37
|
-
getProxyStatusFunc = getProxyStatus;
|
|
38
|
-
} else if (currentType === 'codex') {
|
|
39
|
-
const { getActiveCodexChannel } = require('../server/services/codex-channels');
|
|
40
|
-
const { getCodexProxyStatus } = require('../server/codex-proxy-server');
|
|
41
|
-
getCurrentChannelFunc = getActiveCodexChannel;
|
|
42
|
-
getProxyStatusFunc = getCodexProxyStatus;
|
|
43
|
-
} else if (currentType === 'gemini') {
|
|
44
|
-
const { getActiveGeminiChannel } = require('../server/services/gemini-channels');
|
|
45
|
-
const { getGeminiProxyStatus } = require('../server/gemini-proxy-server');
|
|
46
|
-
getCurrentChannelFunc = getActiveGeminiChannel;
|
|
47
|
-
getProxyStatusFunc = getGeminiProxyStatus;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const currentChannel = getCurrentChannelFunc();
|
|
51
|
-
const proxyStatus = getProxyStatusFunc();
|
|
75
|
+
const { channel: currentChannel, proxyStatus } = getChannelAndProxyStatus(currentType);
|
|
52
76
|
|
|
53
77
|
if (currentChannel) {
|
|
54
78
|
console.log(chalk.gray(`当前渠道: ${currentChannel.name}`));
|
|
@@ -68,22 +92,7 @@ async function showMainMenu(config) {
|
|
|
68
92
|
// 获取代理状态,用于显示动态切换的状态(根据当前类型)
|
|
69
93
|
let proxyStatusText = '未开启';
|
|
70
94
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (currentType === 'claude') {
|
|
74
|
-
// 清除缓存确保获取最新状态
|
|
75
|
-
delete require.cache[require.resolve('../server/proxy-server')];
|
|
76
|
-
const { getProxyStatus } = require('../server/proxy-server');
|
|
77
|
-
proxyStatus = getProxyStatus();
|
|
78
|
-
} else if (currentType === 'codex') {
|
|
79
|
-
delete require.cache[require.resolve('../server/codex-proxy-server')];
|
|
80
|
-
const { getCodexProxyStatus } = require('../server/codex-proxy-server');
|
|
81
|
-
proxyStatus = getCodexProxyStatus();
|
|
82
|
-
} else if (currentType === 'gemini') {
|
|
83
|
-
delete require.cache[require.resolve('../server/gemini-proxy-server')];
|
|
84
|
-
const { getGeminiProxyStatus } = require('../server/gemini-proxy-server');
|
|
85
|
-
proxyStatus = getGeminiProxyStatus();
|
|
86
|
-
}
|
|
95
|
+
const { proxyStatus } = getChannelAndProxyStatus(currentType);
|
|
87
96
|
|
|
88
97
|
if (proxyStatus && proxyStatus.running) {
|
|
89
98
|
proxyStatusText = '已开启';
|