@adversity/coding-tool-x 3.1.0 → 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.
Files changed (137) hide show
  1. package/CHANGELOG.md +15 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
  5. package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
  6. package/dist/web/assets/Home-Di2qsylF.css +1 -0
  7. package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
  8. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
  13. package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
  14. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  15. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  16. package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
  17. package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
  18. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-Ufv5rCa5.css +1 -0
  21. package/dist/web/assets/index-lAkrRC3h.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +39 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/paths.js +105 -33
  49. package/src/index.js +64 -3
  50. package/src/plugins/constants.js +3 -2
  51. package/src/plugins/plugin-api.js +1 -1
  52. package/src/reset-config.js +4 -2
  53. package/src/server/api/agents.js +57 -14
  54. package/src/server/api/channels.js +112 -33
  55. package/src/server/api/codex-channels.js +111 -18
  56. package/src/server/api/codex-proxy.js +14 -8
  57. package/src/server/api/commands.js +71 -18
  58. package/src/server/api/config-export.js +0 -6
  59. package/src/server/api/config-registry.js +11 -3
  60. package/src/server/api/config.js +376 -5
  61. package/src/server/api/convert.js +133 -0
  62. package/src/server/api/dashboard.js +22 -6
  63. package/src/server/api/gemini-channels.js +107 -18
  64. package/src/server/api/gemini-proxy.js +14 -8
  65. package/src/server/api/gemini-sessions.js +1 -1
  66. package/src/server/api/health-check.js +4 -3
  67. package/src/server/api/mcp.js +3 -3
  68. package/src/server/api/opencode-channels.js +419 -0
  69. package/src/server/api/opencode-projects.js +99 -0
  70. package/src/server/api/opencode-proxy.js +198 -0
  71. package/src/server/api/opencode-sessions.js +403 -0
  72. package/src/server/api/opencode-statistics.js +57 -0
  73. package/src/server/api/plugins.js +66 -19
  74. package/src/server/api/prompts.js +2 -2
  75. package/src/server/api/proxy.js +7 -4
  76. package/src/server/api/sessions.js +3 -0
  77. package/src/server/api/skills.js +69 -18
  78. package/src/server/api/workspaces.js +78 -6
  79. package/src/server/codex-proxy-server.js +30 -18
  80. package/src/server/dev-server.js +1 -1
  81. package/src/server/gemini-proxy-server.js +15 -3
  82. package/src/server/index.js +165 -58
  83. package/src/server/opencode-proxy-server.js +4375 -0
  84. package/src/server/proxy-server.js +27 -18
  85. package/src/server/services/agents-service.js +61 -24
  86. package/src/server/services/channel-scheduler.js +9 -5
  87. package/src/server/services/channels.js +64 -37
  88. package/src/server/services/codex-channels.js +56 -43
  89. package/src/server/services/codex-settings-manager.js +271 -49
  90. package/src/server/services/codex-statistics-service.js +2 -2
  91. package/src/server/services/commands-service.js +84 -25
  92. package/src/server/services/config-export-service.js +7 -45
  93. package/src/server/services/config-registry-service.js +63 -17
  94. package/src/server/services/config-sync-manager.js +160 -7
  95. package/src/server/services/config-templates-service.js +204 -51
  96. package/src/server/services/env-checker.js +26 -12
  97. package/src/server/services/env-manager.js +126 -18
  98. package/src/server/services/favorites.js +5 -3
  99. package/src/server/services/gemini-channels.js +33 -44
  100. package/src/server/services/gemini-statistics-service.js +2 -2
  101. package/src/server/services/mcp-service.js +350 -9
  102. package/src/server/services/model-detector.js +707 -221
  103. package/src/server/services/network-access.js +80 -0
  104. package/src/server/services/opencode-channels.js +206 -0
  105. package/src/server/services/opencode-gateway-converter.js +639 -0
  106. package/src/server/services/opencode-sessions.js +663 -0
  107. package/src/server/services/opencode-settings-manager.js +342 -0
  108. package/src/server/services/opencode-statistics-service.js +255 -0
  109. package/src/server/services/plugins-service.js +479 -22
  110. package/src/server/services/prompts-service.js +53 -11
  111. package/src/server/services/proxy-runtime.js +1 -1
  112. package/src/server/services/repo-scanner-base.js +1 -1
  113. package/src/server/services/security-config.js +1 -1
  114. package/src/server/services/session-cache.js +1 -1
  115. package/src/server/services/skill-service.js +300 -46
  116. package/src/server/services/speed-test.js +464 -186
  117. package/src/server/services/statistics-service.js +2 -2
  118. package/src/server/services/terminal-commands.js +10 -3
  119. package/src/server/services/terminal-config.js +1 -1
  120. package/src/server/services/ui-config.js +1 -1
  121. package/src/server/services/workspace-service.js +57 -100
  122. package/src/server/websocket-server.js +132 -3
  123. package/src/ui/menu.js +49 -40
  124. package/src/utils/port-helper.js +22 -8
  125. package/src/utils/session.js +5 -4
  126. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  127. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  128. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  129. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  130. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  131. package/src/server/api/oauth.js +0 -294
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/config/oauth-providers.js +0 -68
  134. package/src/server/services/oauth-callback-server.js +0 -284
  135. package/src/server/services/oauth-service.js +0 -378
  136. package/src/server/services/oauth-token-storage.js +0 -135
  137. 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
- * ~/.claude/cc-tool/
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(), '.claude', 'cc-tool');
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(), '.claude', 'cc-tool');
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(), '.claude', 'cc-tool');
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(), '.claude', 'cc-tool');
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 { execSync } = require('child_process');
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 = execSync('git worktree list --porcelain', {
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, permissionTemplate } = options;
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 = execSync('git rev-parse --abbrev-ref HEAD', {
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 = execSync('git rev-parse --abbrev-ref HEAD', {
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
- execSync(`git worktree add "${worktreePath}" "${targetBranch}"`, {
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
- // 构建 git worktree add 命令
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
- worktreeCmd += ` "${baseBranch}"`;
333
+ worktreeArgs.push(baseBranch.trim());
327
334
  }
328
-
329
- execSync(worktreeCmd, {
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
- execSync(`git worktree remove "${wt.path}" --force`, {
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 = execSync('git rev-parse --abbrev-ref HEAD', {
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
- execSync(`git worktree add "${worktreePath}" "${targetBranch}"`, {
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
- // 构建 git worktree add 命令
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
- worktreeCmd += ` "${baseBranch}"`;
581
+ worktreeArgs.push(baseBranch.trim());
635
582
  }
636
-
637
- execSync(worktreeCmd, {
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
- execSync(`git worktree remove "${wt.path}" --force`, {
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(), '.claude', 'cc-tool');
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 || 10099;
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
- let getCurrentChannelFunc, getProxyStatusFunc;
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
- let proxyStatus;
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 = '已开启';