@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.
Files changed (142) hide show
  1. package/CHANGELOG.md +39 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
  5. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  6. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  7. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  8. package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
  13. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  14. package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
  15. package/dist/web/assets/Terminal-BasTyDut.js +1 -0
  16. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  17. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  18. package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  21. package/dist/web/assets/index-CryrSLv8.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 +41 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/model-metadata.js +415 -0
  49. package/src/config/model-pricing.js +23 -93
  50. package/src/config/paths.js +105 -33
  51. package/src/index.js +64 -3
  52. package/src/plugins/constants.js +3 -2
  53. package/src/plugins/plugin-api.js +1 -1
  54. package/src/reset-config.js +4 -2
  55. package/src/server/api/agents.js +57 -14
  56. package/src/server/api/channels.js +112 -33
  57. package/src/server/api/codex-channels.js +111 -18
  58. package/src/server/api/codex-proxy.js +14 -8
  59. package/src/server/api/commands.js +71 -18
  60. package/src/server/api/config-export.js +0 -6
  61. package/src/server/api/config-registry.js +11 -3
  62. package/src/server/api/config.js +376 -5
  63. package/src/server/api/convert.js +133 -0
  64. package/src/server/api/dashboard.js +22 -6
  65. package/src/server/api/gemini-channels.js +107 -18
  66. package/src/server/api/gemini-proxy.js +14 -8
  67. package/src/server/api/gemini-sessions.js +1 -1
  68. package/src/server/api/health-check.js +4 -3
  69. package/src/server/api/mcp.js +3 -3
  70. package/src/server/api/opencode-channels.js +497 -0
  71. package/src/server/api/opencode-projects.js +99 -0
  72. package/src/server/api/opencode-proxy.js +207 -0
  73. package/src/server/api/opencode-sessions.js +345 -0
  74. package/src/server/api/opencode-statistics.js +57 -0
  75. package/src/server/api/plugins.js +66 -19
  76. package/src/server/api/prompts.js +2 -2
  77. package/src/server/api/proxy.js +7 -4
  78. package/src/server/api/sessions.js +3 -0
  79. package/src/server/api/settings.js +111 -0
  80. package/src/server/api/skills.js +69 -18
  81. package/src/server/api/workspaces.js +78 -6
  82. package/src/server/codex-proxy-server.js +36 -22
  83. package/src/server/dev-server.js +1 -1
  84. package/src/server/gemini-proxy-server.js +21 -7
  85. package/src/server/index.js +174 -58
  86. package/src/server/opencode-proxy-server.js +5486 -0
  87. package/src/server/proxy-server.js +33 -22
  88. package/src/server/services/agents-service.js +61 -24
  89. package/src/server/services/channel-scheduler.js +9 -5
  90. package/src/server/services/channels.js +64 -37
  91. package/src/server/services/codex-channels.js +56 -43
  92. package/src/server/services/codex-sessions.js +105 -6
  93. package/src/server/services/codex-settings-manager.js +271 -49
  94. package/src/server/services/codex-statistics-service.js +2 -2
  95. package/src/server/services/commands-service.js +84 -25
  96. package/src/server/services/config-export-service.js +7 -45
  97. package/src/server/services/config-registry-service.js +63 -17
  98. package/src/server/services/config-sync-manager.js +160 -7
  99. package/src/server/services/config-templates-service.js +204 -51
  100. package/src/server/services/env-checker.js +50 -13
  101. package/src/server/services/env-manager.js +155 -19
  102. package/src/server/services/favorites.js +5 -3
  103. package/src/server/services/gemini-channels.js +33 -44
  104. package/src/server/services/gemini-statistics-service.js +2 -2
  105. package/src/server/services/mcp-service.js +350 -9
  106. package/src/server/services/model-detector.js +707 -221
  107. package/src/server/services/network-access.js +80 -0
  108. package/src/server/services/opencode-channels.js +208 -0
  109. package/src/server/services/opencode-gateway-converter.js +639 -0
  110. package/src/server/services/opencode-sessions.js +931 -0
  111. package/src/server/services/opencode-settings-manager.js +478 -0
  112. package/src/server/services/opencode-statistics-service.js +255 -0
  113. package/src/server/services/plugins-service.js +479 -22
  114. package/src/server/services/prompts-service.js +53 -11
  115. package/src/server/services/proxy-runtime.js +1 -1
  116. package/src/server/services/repo-scanner-base.js +1 -1
  117. package/src/server/services/response-decoder.js +21 -0
  118. package/src/server/services/security-config.js +1 -1
  119. package/src/server/services/session-cache.js +1 -1
  120. package/src/server/services/skill-service.js +300 -46
  121. package/src/server/services/speed-test.js +464 -186
  122. package/src/server/services/statistics-service.js +2 -2
  123. package/src/server/services/terminal-commands.js +10 -3
  124. package/src/server/services/terminal-config.js +1 -1
  125. package/src/server/services/ui-config.js +1 -1
  126. package/src/server/services/workspace-service.js +57 -100
  127. package/src/server/websocket-server.js +156 -8
  128. package/src/ui/menu.js +49 -40
  129. package/src/utils/port-helper.js +22 -8
  130. package/src/utils/session.js +5 -4
  131. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  132. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  133. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  134. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  135. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  136. package/src/server/api/oauth.js +0 -294
  137. package/src/server/api/permissions.js +0 -385
  138. package/src/server/config/oauth-providers.js +0 -68
  139. package/src/server/services/oauth-callback-server.js +0 -284
  140. package/src/server/services/oauth-service.js +0 -378
  141. package/src/server/services/oauth-token-storage.js +0 -135
  142. 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,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(), '.claude', 'cc-tool');
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 || 10099;
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
- logsCache.forEach(log => {
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);