@adversity/coding-tool-x 3.1.0 → 3.1.2
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 +39 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
- package/dist/web/assets/Home-BJKPCBuk.css +1 -0
- package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
- package/dist/web/assets/Terminal-BasTyDut.js +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-CoB3zF0K.css +1 -0
- package/dist/web/assets/index-CryrSLv8.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 +81 -12
- 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/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +41 -2
- package/src/config/loader.js +74 -8
- package/src/config/model-metadata.js +415 -0
- package/src/config/model-pricing.js +23 -93
- package/src/config/paths.js +105 -33
- package/src/index.js +64 -3
- 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 +497 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +345 -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/settings.js +111 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +36 -22
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +21 -7
- package/src/server/index.js +174 -58
- package/src/server/opencode-proxy-server.js +5486 -0
- package/src/server/proxy-server.js +33 -22
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- package/src/server/services/codex-sessions.js +105 -6
- 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 +50 -13
- package/src/server/services/env-manager.js +155 -19
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +33 -44
- 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 +208 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -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/response-decoder.js +21 -0
- 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 +156 -8
- 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-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- 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,139 @@ 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
|
+
const HISTORY_CHUNK_SIZE = 50;
|
|
37
|
+
|
|
38
|
+
function sendPersistedLogsInChunks(ws, logs) {
|
|
39
|
+
let index = 0;
|
|
40
|
+
|
|
41
|
+
const sendChunk = () => {
|
|
42
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const end = Math.min(index + HISTORY_CHUNK_SIZE, logs.length);
|
|
47
|
+
for (let i = index; i < end; i++) {
|
|
48
|
+
ws.send(JSON.stringify(logs[i]));
|
|
49
|
+
}
|
|
50
|
+
index = end;
|
|
51
|
+
|
|
52
|
+
if (index < logs.length) {
|
|
53
|
+
setImmediate(sendChunk);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
setImmediate(sendChunk);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseHostHeader(hostHeader) {
|
|
61
|
+
const value = String(hostHeader || '').trim();
|
|
62
|
+
if (!value) {
|
|
63
|
+
return { hostname: '', port: '' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (value.startsWith('[')) {
|
|
67
|
+
const closingBracket = value.indexOf(']');
|
|
68
|
+
if (closingBracket > 0) {
|
|
69
|
+
const hostname = value.slice(1, closingBracket);
|
|
70
|
+
const rest = value.slice(closingBracket + 1);
|
|
71
|
+
const port = rest.startsWith(':') ? rest.slice(1) : '';
|
|
72
|
+
return { hostname, port };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const separator = value.lastIndexOf(':');
|
|
77
|
+
if (separator > -1 && value.indexOf(':') === separator) {
|
|
78
|
+
return {
|
|
79
|
+
hostname: value.slice(0, separator),
|
|
80
|
+
port: value.slice(separator + 1)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { hostname: value, port: '' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function defaultPortForProtocol(protocol) {
|
|
88
|
+
if (protocol === 'https:') {
|
|
89
|
+
return '443';
|
|
90
|
+
}
|
|
91
|
+
return '80';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isAllowedWebSocketOrigin(req) {
|
|
95
|
+
if (!req || !req.headers) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const originHeader = req.headers.origin;
|
|
100
|
+
if (!originHeader) {
|
|
101
|
+
// 非浏览器客户端通常不会携带 Origin,仅允许本机来源
|
|
102
|
+
return isLoopbackRequest(req);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let originUrl;
|
|
106
|
+
try {
|
|
107
|
+
originUrl = new URL(originHeader);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (originUrl.protocol !== 'http:' && originUrl.protocol !== 'https:') {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const requestHost = parseHostHeader(req.headers.host);
|
|
117
|
+
const requestProtocol = req.socket && req.socket.encrypted ? 'https:' : 'http:';
|
|
118
|
+
const requestHostname = normalizeAddress(requestHost.hostname).toLowerCase();
|
|
119
|
+
const requestPort = requestHost.port || defaultPortForProtocol(requestProtocol);
|
|
120
|
+
|
|
121
|
+
const originHostname = normalizeAddress(originUrl.hostname).toLowerCase();
|
|
122
|
+
const originPort = originUrl.port || defaultPortForProtocol(originUrl.protocol);
|
|
123
|
+
|
|
124
|
+
if (!requestHostname || !originHostname) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 同源直接放行
|
|
129
|
+
if (originHostname === requestHostname && originPort === requestPort) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 允许本机开发代理(例如 Vite 5000 -> 19999)
|
|
134
|
+
if (isLoopbackRequest(req) && isLoopbackAddress(originHostname) && isLoopbackAddress(requestHostname)) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function installOriginGuard(server) {
|
|
142
|
+
if (!server || typeof server.shouldHandle !== 'function') {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const originalShouldHandle = server.shouldHandle.bind(server);
|
|
147
|
+
server.shouldHandle = (req) => {
|
|
148
|
+
if (!originalShouldHandle(req)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const allowed = isAllowedWebSocketOrigin(req);
|
|
153
|
+
if (!allowed) {
|
|
154
|
+
const origin = req.headers.origin || 'unknown';
|
|
155
|
+
const clientIp = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : 'unknown';
|
|
156
|
+
console.warn(`[WebSocket] Rejected connection from ${clientIp}, origin: ${origin}`);
|
|
157
|
+
}
|
|
158
|
+
return allowed;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
27
161
|
|
|
28
162
|
// 日志持久化文件路径
|
|
29
163
|
function getLogsFilePath() {
|
|
30
|
-
const ccToolDir = path.join(os.homedir(), '.
|
|
164
|
+
const ccToolDir = path.join(os.homedir(), '.cc-tool');
|
|
31
165
|
if (!fs.existsSync(ccToolDir)) {
|
|
32
166
|
fs.mkdirSync(ccToolDir, { recursive: true });
|
|
33
167
|
}
|
|
@@ -141,12 +275,17 @@ function saveLogsToFile(logs) {
|
|
|
141
275
|
let logsCache = [];
|
|
142
276
|
|
|
143
277
|
// 启动 WebSocket 服务器(附加到现有的 HTTP 服务器)
|
|
144
|
-
function startWebSocketServer(httpServer) {
|
|
278
|
+
function startWebSocketServer(httpServer, options = {}) {
|
|
145
279
|
if (wss) {
|
|
146
280
|
console.log('WebSocket server already running');
|
|
147
281
|
return;
|
|
148
282
|
}
|
|
149
283
|
|
|
284
|
+
websocketOptions = {
|
|
285
|
+
host: options.host || '127.0.0.1',
|
|
286
|
+
allowRemoteTerminal: options.allowRemoteTerminal === true
|
|
287
|
+
};
|
|
288
|
+
|
|
150
289
|
// 加载持久化的日志到缓存
|
|
151
290
|
logsCache = loadPersistedLogs();
|
|
152
291
|
const counts = logsCache.reduce((acc, log) => {
|
|
@@ -163,15 +302,17 @@ function startWebSocketServer(httpServer) {
|
|
|
163
302
|
server: httpServer,
|
|
164
303
|
path: '/ws' // 指定 WebSocket 路径
|
|
165
304
|
});
|
|
305
|
+
installOriginGuard(wss);
|
|
166
306
|
console.log(`✅ WebSocket server attached to HTTP server at /ws`);
|
|
167
307
|
} else {
|
|
168
308
|
// 创建独立的 WebSocket 服务器,使用配置的 webUI 端口
|
|
169
309
|
const config = loadConfig();
|
|
170
|
-
const port = config.ports?.webUI ||
|
|
310
|
+
const port = config.ports?.webUI || 19999;
|
|
171
311
|
wss = new WebSocket.Server({
|
|
172
312
|
port,
|
|
173
313
|
path: '/ws'
|
|
174
314
|
});
|
|
315
|
+
installOriginGuard(wss);
|
|
175
316
|
console.log(`✅ WebSocket server started on ws://127.0.0.1:${port}/ws`);
|
|
176
317
|
}
|
|
177
318
|
|
|
@@ -185,14 +326,13 @@ function startWebSocketServer(httpServer) {
|
|
|
185
326
|
ws.isAlive = true;
|
|
186
327
|
// 终端绑定信息
|
|
187
328
|
ws.terminalId = null;
|
|
329
|
+
const isLoopback = isLoopbackRequest(req);
|
|
330
|
+
const lanMode = websocketOptions.host === '0.0.0.0';
|
|
331
|
+
ws.terminalAllowed = !(lanMode && !isLoopback && !websocketOptions.allowRemoteTerminal);
|
|
188
332
|
|
|
189
333
|
// 发送历史日志给新连接的客户端
|
|
190
334
|
if (logsCache.length > 0) {
|
|
191
|
-
|
|
192
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
193
|
-
ws.send(JSON.stringify(log));
|
|
194
|
-
}
|
|
195
|
-
});
|
|
335
|
+
sendPersistedLogsInChunks(ws, logsCache);
|
|
196
336
|
}
|
|
197
337
|
|
|
198
338
|
// 处理客户端消息
|
|
@@ -400,6 +540,14 @@ const { getWebTerminalShellConfig } = require('./services/terminal-config');
|
|
|
400
540
|
function handleTerminalMessage(ws, message) {
|
|
401
541
|
const { type } = message;
|
|
402
542
|
|
|
543
|
+
if (String(type || '').startsWith('terminal:') && ws.terminalAllowed === false) {
|
|
544
|
+
ws.send(JSON.stringify({
|
|
545
|
+
type: 'terminal:error',
|
|
546
|
+
error: '出于安全考虑,LAN 模式下仅允许本机使用 Web 终端。可设置 CC_TOOL_ALLOW_REMOTE_TERMINAL=true 覆盖。'
|
|
547
|
+
}));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
403
551
|
switch (type) {
|
|
404
552
|
case 'terminal:create':
|
|
405
553
|
handleTerminalCreate(ws, message);
|