@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.
Files changed (133) hide show
  1. package/CHANGELOG.md +38 -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 +92 -13
  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/ui.js +8 -1
  45. package/src/commands/update.js +97 -0
  46. package/src/commands/workspace.js +1 -1
  47. package/src/config/default.js +39 -2
  48. package/src/config/loader.js +74 -8
  49. package/src/config/paths.js +105 -33
  50. package/src/index.js +67 -4
  51. package/src/plugins/constants.js +3 -2
  52. package/src/plugins/plugin-api.js +1 -1
  53. package/src/reset-config.js +4 -2
  54. package/src/server/api/agents.js +57 -14
  55. package/src/server/api/channels.js +112 -33
  56. package/src/server/api/codex-channels.js +111 -18
  57. package/src/server/api/codex-proxy.js +14 -8
  58. package/src/server/api/commands.js +71 -18
  59. package/src/server/api/config-export.js +0 -6
  60. package/src/server/api/config-registry.js +11 -3
  61. package/src/server/api/config.js +376 -5
  62. package/src/server/api/convert.js +133 -0
  63. package/src/server/api/dashboard.js +22 -6
  64. package/src/server/api/gemini-channels.js +107 -18
  65. package/src/server/api/gemini-proxy.js +14 -8
  66. package/src/server/api/gemini-sessions.js +1 -1
  67. package/src/server/api/health-check.js +4 -3
  68. package/src/server/api/mcp.js +3 -3
  69. package/src/server/api/opencode-channels.js +419 -0
  70. package/src/server/api/opencode-projects.js +99 -0
  71. package/src/server/api/opencode-proxy.js +198 -0
  72. package/src/server/api/opencode-sessions.js +403 -0
  73. package/src/server/api/opencode-statistics.js +57 -0
  74. package/src/server/api/plugins.js +66 -19
  75. package/src/server/api/prompts.js +2 -2
  76. package/src/server/api/proxy.js +7 -4
  77. package/src/server/api/sessions.js +3 -0
  78. package/src/server/api/skills.js +69 -18
  79. package/src/server/api/workspaces.js +78 -6
  80. package/src/server/codex-proxy-server.js +32 -19
  81. package/src/server/dev-server.js +1 -1
  82. package/src/server/gemini-proxy-server.js +17 -3
  83. package/src/server/index.js +164 -48
  84. package/src/server/opencode-proxy-server.js +4375 -0
  85. package/src/server/proxy-server.js +30 -19
  86. package/src/server/services/agents-service.js +61 -24
  87. package/src/server/services/channel-scheduler.js +9 -5
  88. package/src/server/services/channels.js +70 -12
  89. package/src/server/services/codex-channels.js +61 -23
  90. package/src/server/services/codex-settings-manager.js +271 -49
  91. package/src/server/services/codex-statistics-service.js +2 -2
  92. package/src/server/services/commands-service.js +84 -25
  93. package/src/server/services/config-export-service.js +7 -45
  94. package/src/server/services/config-registry-service.js +63 -17
  95. package/src/server/services/config-sync-manager.js +160 -7
  96. package/src/server/services/config-templates-service.js +204 -51
  97. package/src/server/services/env-checker.js +26 -12
  98. package/src/server/services/env-manager.js +126 -18
  99. package/src/server/services/favorites.js +5 -3
  100. package/src/server/services/gemini-channels.js +37 -15
  101. package/src/server/services/gemini-statistics-service.js +2 -2
  102. package/src/server/services/mcp-service.js +350 -9
  103. package/src/server/services/model-detector.js +707 -221
  104. package/src/server/services/network-access.js +80 -0
  105. package/src/server/services/opencode-channels.js +206 -0
  106. package/src/server/services/opencode-gateway-converter.js +639 -0
  107. package/src/server/services/opencode-sessions.js +663 -0
  108. package/src/server/services/opencode-settings-manager.js +342 -0
  109. package/src/server/services/opencode-statistics-service.js +255 -0
  110. package/src/server/services/plugins-service.js +479 -22
  111. package/src/server/services/prompts-service.js +53 -11
  112. package/src/server/services/proxy-runtime.js +1 -1
  113. package/src/server/services/repo-scanner-base.js +1 -1
  114. package/src/server/services/security-config.js +1 -1
  115. package/src/server/services/session-cache.js +1 -1
  116. package/src/server/services/skill-service.js +300 -46
  117. package/src/server/services/speed-test.js +464 -186
  118. package/src/server/services/statistics-service.js +2 -2
  119. package/src/server/services/terminal-commands.js +10 -3
  120. package/src/server/services/terminal-config.js +1 -1
  121. package/src/server/services/ui-config.js +1 -1
  122. package/src/server/services/workspace-service.js +57 -100
  123. package/src/server/websocket-server.js +132 -3
  124. package/src/ui/menu.js +49 -40
  125. package/src/utils/port-helper.js +22 -8
  126. package/src/utils/session.js +5 -4
  127. package/dist/web/assets/icons-BxudHPiX.js +0 -1
  128. package/dist/web/assets/index-D2VfwJBa.js +0 -14
  129. package/dist/web/assets/index-oXBzu0bd.css +0 -41
  130. package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
  131. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  132. package/src/server/api/permissions.js +0 -385
  133. package/src/server/services/permission-templates-service.js +0 -308
@@ -3,8 +3,35 @@ const express = require('express');
3
3
  const router = express.Router();
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
+ const { execFileSync } = require('child_process');
6
7
  const workspaceService = require('../services/workspace-service');
7
8
 
9
+ function normalizeBranchName(branchName) {
10
+ if (typeof branchName !== 'string') {
11
+ return '';
12
+ }
13
+ return branchName.trim();
14
+ }
15
+
16
+ function validateBranchName(branchName) {
17
+ const normalized = normalizeBranchName(branchName);
18
+ if (!normalized) {
19
+ return { valid: false, normalized, message: '分支名不能为空' };
20
+ }
21
+ if (normalized.length > 255) {
22
+ return { valid: false, normalized, message: '分支名长度不能超过 255 个字符' };
23
+ }
24
+
25
+ try {
26
+ execFileSync('git', ['check-ref-format', '--branch', normalized], {
27
+ stdio: 'ignore'
28
+ });
29
+ return { valid: true, normalized };
30
+ } catch (error) {
31
+ return { valid: false, normalized, message: `非法分支名: ${normalized}` };
32
+ }
33
+ }
34
+
8
35
  /**
9
36
  * GET /api/workspaces
10
37
  * 获取所有工作区列表
@@ -173,7 +200,7 @@ router.get('/:id', (req, res, next) => {
173
200
  */
174
201
  router.post('/', (req, res) => {
175
202
  try {
176
- const { name, description, baseDir, projects, configTemplateId, permissionTemplate } = req.body;
203
+ const { name, description, baseDir, projects, configTemplateId } = req.body;
177
204
 
178
205
  if (!name || !name.trim()) {
179
206
  return res.status(400).json({
@@ -204,6 +231,30 @@ router.post('/', (req, res) => {
204
231
  message: '创建 worktree 时必须指定分支名'
205
232
  });
206
233
  }
234
+
235
+ const normalizedBranch = normalizeBranchName(proj.branch);
236
+ if (normalizedBranch) {
237
+ const branchValidation = validateBranchName(normalizedBranch);
238
+ if (!branchValidation.valid) {
239
+ return res.status(400).json({
240
+ success: false,
241
+ message: branchValidation.message
242
+ });
243
+ }
244
+ proj.branch = branchValidation.normalized;
245
+ }
246
+
247
+ const normalizedBaseBranch = normalizeBranchName(proj.baseBranch);
248
+ if (normalizedBaseBranch) {
249
+ const baseBranchValidation = validateBranchName(normalizedBaseBranch);
250
+ if (!baseBranchValidation.valid) {
251
+ return res.status(400).json({
252
+ success: false,
253
+ message: `基础分支不合法: ${baseBranchValidation.normalized}`
254
+ });
255
+ }
256
+ proj.baseBranch = baseBranchValidation.normalized;
257
+ }
207
258
  }
208
259
 
209
260
  const workspace = workspaceService.createWorkspace({
@@ -211,8 +262,7 @@ router.post('/', (req, res) => {
211
262
  description,
212
263
  baseDir,
213
264
  projects,
214
- configTemplateId,
215
- permissionTemplate
265
+ configTemplateId
216
266
  });
217
267
 
218
268
  res.json({
@@ -286,6 +336,8 @@ router.post('/:id/projects', (req, res) => {
286
336
  try {
287
337
  const { id } = req.params;
288
338
  const { sourcePath, name, createWorktree, branch, baseBranch } = req.body;
339
+ const normalizedBranch = normalizeBranchName(branch);
340
+ const normalizedBaseBranch = normalizeBranchName(baseBranch);
289
341
 
290
342
  if (!sourcePath || !sourcePath.trim()) {
291
343
  return res.status(400).json({
@@ -294,19 +346,39 @@ router.post('/:id/projects', (req, res) => {
294
346
  });
295
347
  }
296
348
 
297
- if (createWorktree && (!branch || !branch.trim())) {
349
+ if (createWorktree && !normalizedBranch) {
298
350
  return res.status(400).json({
299
351
  success: false,
300
352
  message: '创建 worktree 时必须指定分支名'
301
353
  });
302
354
  }
303
355
 
356
+ if (normalizedBranch) {
357
+ const branchValidation = validateBranchName(normalizedBranch);
358
+ if (!branchValidation.valid) {
359
+ return res.status(400).json({
360
+ success: false,
361
+ message: branchValidation.message
362
+ });
363
+ }
364
+ }
365
+
366
+ if (normalizedBaseBranch) {
367
+ const baseBranchValidation = validateBranchName(normalizedBaseBranch);
368
+ if (!baseBranchValidation.valid) {
369
+ return res.status(400).json({
370
+ success: false,
371
+ message: `基础分支不合法: ${baseBranchValidation.normalized}`
372
+ });
373
+ }
374
+ }
375
+
304
376
  const workspace = workspaceService.addProjectToWorkspace(id, {
305
377
  sourcePath,
306
378
  name,
307
379
  createWorktree,
308
- branch,
309
- baseBranch
380
+ branch: normalizedBranch || branch,
381
+ baseBranch: normalizedBaseBranch || baseBranch
310
382
  });
311
383
 
312
384
  res.json({
@@ -10,7 +10,7 @@ const DEFAULT_CONFIG = require('../config/default');
10
10
  const { resolvePricing } = require('./utils/pricing');
11
11
  const { recordRequest: recordCodexRequest } = require('./services/codex-statistics-service');
12
12
  const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
13
- const { getEnabledChannels, writeCodexConfigForMultiChannel } = require('./services/codex-channels');
13
+ const { getEnabledChannels, writeCodexConfigForMultiChannel, getEffectiveApiKey } = require('./services/codex-channels');
14
14
  const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
15
15
 
16
16
  let proxyServer = null;
@@ -230,7 +230,7 @@ async function startCodexProxyServer(options = {}) {
230
230
 
231
231
  try {
232
232
  const config = loadConfig();
233
- const port = config.ports?.codexProxy || 10089;
233
+ const port = config.ports?.codexProxy || 20089;
234
234
  currentPort = port;
235
235
 
236
236
  proxyApp = express();
@@ -257,7 +257,8 @@ async function startCodexProxyServer(options = {}) {
257
257
  });
258
258
 
259
259
  proxyReq.removeHeader('authorization');
260
- proxyReq.setHeader('authorization', `Bearer ${activeChannel.apiKey}`);
260
+ const effectiveKey = req.effectiveApiKey;
261
+ proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
261
262
  proxyReq.setHeader('openai-beta', 'responses=experimental');
262
263
  if (!proxyReq.getHeader('content-type')) {
263
264
  proxyReq.setHeader('content-type', 'application/json');
@@ -278,6 +279,33 @@ async function startCodexProxyServer(options = {}) {
278
279
  const channel = await allocateChannel({ source: 'codex', enableSessionBinding: false });
279
280
  req.selectedChannel = channel;
280
281
 
282
+ const release = (() => {
283
+ let released = false;
284
+ return () => {
285
+ if (released) return;
286
+ released = true;
287
+ releaseChannel(channel.id, 'codex');
288
+ broadcastSchedulerState('codex', getSchedulerState('codex'));
289
+ };
290
+ })();
291
+
292
+ res.on('close', release);
293
+ res.on('error', release);
294
+
295
+ broadcastSchedulerState('codex', getSchedulerState('codex'));
296
+
297
+ const effectiveKey = getEffectiveApiKey(channel);
298
+ if (!effectiveKey) {
299
+ release();
300
+ return res.status(401).json({
301
+ error: {
302
+ message: 'API key not configured or expired. Please update your channel key.',
303
+ type: 'authentication_error'
304
+ }
305
+ });
306
+ }
307
+ req.effectiveApiKey = effectiveKey;
308
+
281
309
  // 应用模型重定向(当 proxy 开启时)
282
310
  if (req.body && req.body.model) {
283
311
  const originalModel = req.body.model;
@@ -298,21 +326,6 @@ async function startCodexProxyServer(options = {}) {
298
326
  }
299
327
  }
300
328
 
301
- const release = (() => {
302
- let released = false;
303
- return () => {
304
- if (released) return;
305
- released = true;
306
- releaseChannel(channel.id, 'codex');
307
- broadcastSchedulerState('codex', getSchedulerState('codex'));
308
- };
309
- })();
310
-
311
- res.on('close', release);
312
- res.on('error', release);
313
-
314
- broadcastSchedulerState('codex', getSchedulerState('codex'));
315
-
316
329
  const target = resolveCodexTarget(channel.baseUrl, req.url);
317
330
 
318
331
  proxy.web(req, res, {
@@ -653,7 +666,7 @@ function getCodexProxyStatus() {
653
666
  return {
654
667
  running: !!proxyServer,
655
668
  port: currentPort,
656
- defaultPort: config.ports?.codexProxy || 10089,
669
+ defaultPort: config.ports?.codexProxy || 20089,
657
670
  startTime,
658
671
  runtime
659
672
  };
@@ -11,7 +11,7 @@ const { loadConfig } = require('../config/loader');
11
11
  const chalk = require('chalk');
12
12
 
13
13
  const config = loadConfig();
14
- const port = config.ports?.webUI || 10099;
14
+ const port = config.ports?.webUI || 19999;
15
15
 
16
16
  console.log(chalk.cyan('\n🔧 开发模式:启动后端 API 服务器...\n'));
17
17
 
@@ -10,6 +10,7 @@ const DEFAULT_CONFIG = require('../config/default');
10
10
  const { resolvePricing } = require('./utils/pricing');
11
11
  const { recordRequest: recordGeminiRequest } = require('./services/gemini-statistics-service');
12
12
  const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
13
+ const { getEffectiveApiKey } = require('./services/gemini-channels');
13
14
 
14
15
  let proxyServer = null;
15
16
  let proxyApp = null;
@@ -125,7 +126,7 @@ async function startGeminiProxyServer(options = {}) {
125
126
 
126
127
  try {
127
128
  const config = loadConfig();
128
- const port = config.ports?.geminiProxy || 10090;
129
+ const port = config.ports?.geminiProxy || 20090;
129
130
  currentPort = port;
130
131
 
131
132
  proxyApp = express();
@@ -152,7 +153,8 @@ async function startGeminiProxyServer(options = {}) {
152
153
 
153
154
  proxyReq.removeHeader('authorization');
154
155
  proxyReq.removeHeader('x-goog-api-key');
155
- proxyReq.setHeader('authorization', `Bearer ${activeChannel.apiKey}`);
156
+ const effectiveKey = req.effectiveApiKey;
157
+ proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
156
158
  if (!proxyReq.getHeader('content-type')) {
157
159
  proxyReq.setHeader('content-type', 'application/json');
158
160
  }
@@ -178,6 +180,18 @@ async function startGeminiProxyServer(options = {}) {
178
180
 
179
181
  broadcastSchedulerState('gemini', getSchedulerState('gemini'));
180
182
 
183
+ const effectiveKey = getEffectiveApiKey(channel);
184
+ if (!effectiveKey) {
185
+ release();
186
+ return res.status(401).json({
187
+ error: {
188
+ message: 'API key not configured or expired. Please update your channel key.',
189
+ type: 'authentication_error'
190
+ }
191
+ });
192
+ }
193
+ req.effectiveApiKey = effectiveKey;
194
+
181
195
  // 从 URL 中提取模型名称并应用重定向
182
196
  // URL 格式: /models/gemini-2.5-pro:generateContent 或 /v1/models/gemini-2.5-pro:generateContent
183
197
  const urlMatch = req.url.match(/\/models\/([\w.-]+)(:[^?]*)?/);
@@ -551,7 +565,7 @@ function getGeminiProxyStatus() {
551
565
  return {
552
566
  running: !!proxyServer,
553
567
  port: currentPort,
554
- defaultPort: config.ports?.geminiProxy || 10090,
568
+ defaultPort: config.ports?.geminiProxy || 20090,
555
569
  startTime,
556
570
  runtime
557
571
  };
@@ -3,6 +3,7 @@ const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const inquirer = require('inquirer');
5
5
  const { loadConfig } = require('../config/loader');
6
+ const { ensureStorageDirMigrated } = require('../config/paths');
6
7
  const { startWebSocketServer: attachWebSocketServer } = require('./websocket-server');
7
8
  const { isPortInUse, killProcessByPort, waitForPortRelease } = require('../utils/port-helper');
8
9
  const { isProxyConfig } = require('./services/settings-manager');
@@ -10,41 +11,71 @@ const {
10
11
  isProxyConfig: isCodexProxyConfig,
11
12
  setProxyConfig: setCodexProxyConfig
12
13
  } = require('./services/codex-settings-manager');
14
+ const { setProxyConfig: setOpenCodeProxyConfig } = require('./services/opencode-settings-manager');
13
15
  const { startProxyServer } = require('./proxy-server');
14
16
  const { startCodexProxyServer } = require('./codex-proxy-server');
15
17
  const { startGeminiProxyServer } = require('./gemini-proxy-server');
18
+ const { startOpenCodeProxyServer } = require('./opencode-proxy-server');
19
+ const { createRemoteMutationGuard, createRemoteRouteGuard } = require('./services/network-access');
16
20
 
17
- async function startServer(port) {
21
+ function isInteractivePortConflictMode(options = {}) {
22
+ if (options.interactive === false) {
23
+ return false;
24
+ }
25
+ if (process.argv.includes('--daemon')) {
26
+ return false;
27
+ }
28
+ return Boolean(process.stdin && process.stdin.isTTY && process.stdout && process.stdout.isTTY);
29
+ }
30
+
31
+ function printPortConflictHelp(port) {
32
+ console.log(chalk.yellow('\n💡 解决方案:'));
33
+ console.log(chalk.gray(' 1. 运行 ctx 命令,选择"配置端口"修改端口'));
34
+ console.log(chalk.gray(` 2. 或手动关闭占用端口 ${port} 的程序\n`));
35
+ }
36
+
37
+ async function startServer(port, host = '127.0.0.1', options = {}) {
38
+ ensureStorageDirMigrated();
18
39
  const config = loadConfig();
19
40
  // 使用配置的端口,如果没有传入参数
20
41
  if (!port) {
21
- port = config.ports?.webUI || 10099;
42
+ port = config.ports?.webUI || 19999;
22
43
  }
23
44
 
24
45
  // 检查端口是否被占用
25
- const portInUse = await isPortInUse(port);
46
+ const portInUse = await isPortInUse(port, host);
26
47
  if (portInUse) {
27
48
  console.log(chalk.yellow(`\n⚠️ 端口 ${port} 已被占用\n`));
28
49
 
29
- // 询问用户是否关闭占用端口的进程
30
- const { shouldKill } = await inquirer.prompt([
31
- {
32
- type: 'list',
33
- name: 'shouldKill',
34
- message: '是否关闭占用该端口的进程并启动服务?',
35
- choices: [
36
- { name: '是,关闭进程并启动', value: true },
37
- { name: '否,取消启动', value: false }
38
- ],
39
- default: 0 // 默认选择"是"
40
- }
41
- ]);
50
+ const interactiveMode = isInteractivePortConflictMode(options);
51
+ let shouldKill = false;
52
+
53
+ if (options.forceKillPort === true) {
54
+ shouldKill = true;
55
+ } else if (interactiveMode) {
56
+ // 询问用户是否关闭占用端口的进程
57
+ const answer = await inquirer.prompt([
58
+ {
59
+ type: 'list',
60
+ name: 'shouldKill',
61
+ message: '是否关闭占用该端口的进程并启动服务?',
62
+ choices: [
63
+ { name: '是,关闭进程并启动', value: true },
64
+ { name: '否,取消启动', value: false }
65
+ ],
66
+ default: 0 // 默认选择"是"
67
+ }
68
+ ]);
69
+ shouldKill = answer.shouldKill;
70
+ } else {
71
+ console.error(chalk.red('❌ 当前为非交互模式,无法确认端口清理操作,已取消启动。'));
72
+ printPortConflictHelp(port);
73
+ process.exit(1);
74
+ }
42
75
 
43
76
  if (!shouldKill) {
44
77
  console.log(chalk.gray('\n已取消启动'));
45
- console.log(chalk.yellow('\n💡 解决方案:'));
46
- console.log(chalk.gray(' 1. 运行 ctx 命令,选择"配置端口"修改端口'));
47
- console.log(chalk.gray(` 2. 或手动关闭占用端口 ${port} 的程序\n`));
78
+ printPortConflictHelp(port);
48
79
  process.exit(0);
49
80
  }
50
81
 
@@ -60,7 +91,7 @@ async function startServer(port) {
60
91
 
61
92
  // 等待端口释放
62
93
  console.log(chalk.cyan('等待端口释放...'));
63
- const released = await waitForPortRelease(port);
94
+ const released = await waitForPortRelease(port, 3000, host);
64
95
 
65
96
  if (!released) {
66
97
  console.error(chalk.red('\n❌ 端口释放超时'));
@@ -72,6 +103,9 @@ async function startServer(port) {
72
103
  }
73
104
 
74
105
  const app = express();
106
+ const lanMode = host === '0.0.0.0';
107
+ const allowRemoteMutation = process.env.CC_TOOL_ALLOW_REMOTE_WRITE === 'true';
108
+ const allowRemoteTerminal = process.env.CC_TOOL_ALLOW_REMOTE_TERMINAL === 'true';
75
109
 
76
110
  // Middleware
77
111
  app.use(express.json({ limit: '100mb' }));
@@ -88,6 +122,20 @@ async function startServer(port) {
88
122
  next();
89
123
  });
90
124
 
125
+ if (lanMode) {
126
+ app.use('/api', createRemoteMutationGuard({
127
+ enabled: true,
128
+ allowRemoteMutation,
129
+ message: '出于安全考虑,LAN 模式默认仅允许本机执行写操作。可设置 CC_TOOL_ALLOW_REMOTE_WRITE=true 覆盖。'
130
+ }));
131
+
132
+ app.use('/api/terminal', createRemoteRouteGuard({
133
+ enabled: true,
134
+ allowRemoteAccess: allowRemoteTerminal,
135
+ message: '出于安全考虑,Web 终端仅允许本机访问。可设置 CC_TOOL_ALLOW_REMOTE_TERMINAL=true 覆盖。'
136
+ }));
137
+ }
138
+
91
139
  // API Routes
92
140
  app.use('/api/projects', require('./api/projects')(config));
93
141
  app.use('/api/sessions', require('./api/sessions')(config));
@@ -103,6 +151,13 @@ async function startServer(port) {
103
151
  app.use('/api/gemini/channels', require('./api/gemini-channels')(config));
104
152
  app.use('/api/gemini/proxy', require('./api/gemini-proxy'));
105
153
 
154
+ // OpenCode API Routes
155
+ app.use('/api/opencode/projects', require('./api/opencode-projects')(config));
156
+ app.use('/api/opencode/sessions', require('./api/opencode-sessions')(config));
157
+ app.use('/api/opencode/channels', require('./api/opencode-channels')(config));
158
+ app.use('/api/opencode/proxy', require('./api/opencode-proxy'));
159
+ app.use('/api/opencode/statistics', require('./api/opencode-statistics'));
160
+
106
161
  // 会话格式转换 API
107
162
  app.use('/api/convert', require('./api/convert'));
108
163
 
@@ -145,9 +200,6 @@ async function startServer(port) {
145
200
  // 配置模板 API
146
201
  app.use('/api/config-templates', require('./api/config-templates'));
147
202
 
148
- // 命令执行权限 API
149
- app.use('/api/permissions', require('./api/permissions'));
150
-
151
203
  // 配置导出/导入 API
152
204
  app.use('/api/config-export', require('./api/config-export'));
153
205
 
@@ -169,32 +221,56 @@ async function startServer(port) {
169
221
  });
170
222
  }
171
223
 
172
- // Start server
173
- const server = app.listen(port, () => {
174
- console.log(`\n🚀 Coding-Tool Web UI running at:`);
224
+ // Start server(确保监听成功后才返回,避免命令误报“已启动”)
225
+ const server = app.listen(port, host);
226
+ await new Promise((resolve) => {
227
+ const onListening = () => {
228
+ server.off('error', onError);
229
+ resolve();
230
+ };
231
+
232
+ const onError = (err) => {
233
+ server.off('listening', onListening);
234
+ if (err.code === 'EADDRINUSE') {
235
+ console.error(chalk.red(`\n❌ 端口 ${port} 已被占用`));
236
+ console.error(chalk.yellow('\n💡 解决方案:'));
237
+ console.error(chalk.gray(' 1. 运行 ctx 命令,选择"配置端口"修改端口'));
238
+ console.error(chalk.gray(` 2. 或关闭占用端口 ${port} 的程序\n`));
239
+ } else {
240
+ console.error(chalk.red(`\n❌ 启动服务器失败: ${err.message}\n`));
241
+ }
242
+ process.exit(1);
243
+ };
244
+
245
+ server.once('listening', onListening);
246
+ server.once('error', onError);
247
+ });
248
+
249
+ console.log(`\n🚀 Coding-Tool Web UI running at:`);
250
+ if (host === '0.0.0.0') {
251
+ console.log(chalk.yellow(` ⚠️ 警告: 服务正在监听所有网络接口 (LAN 可访问)`));
175
252
  console.log(` http://localhost:${port}`);
253
+ console.log(chalk.gray(` http://<your-ip>:${port} (LAN 访问)`));
254
+ } else {
255
+ console.log(` http://localhost:${port}`);
256
+ }
176
257
 
177
- // 附加 WebSocket 服务器到同一个端口
178
- attachWebSocketServer(server);
179
- console.log(` ws://localhost:${port}/ws\n`);
258
+ // 附加 WebSocket 服务器到同一个端口
259
+ attachWebSocketServer(server, { host, allowRemoteTerminal });
260
+ console.log(` ws://localhost:${port}/ws\n`);
180
261
 
181
- // 自动恢复代理状态
182
- autoRestoreProxies();
262
+ if (host === '0.0.0.0' && !allowRemoteMutation) {
263
+ console.log(chalk.yellow(' 🔒 已启用 LAN 安全保护:远程写操作默认禁用'));
264
+ }
265
+ if (host === '0.0.0.0' && !allowRemoteTerminal) {
266
+ console.log(chalk.yellow(' 🔒 已启用 LAN 安全保护:远程 Web 终端默认禁用'));
267
+ }
183
268
 
184
- // 启动时执行健康检查
185
- performStartupHealthCheck();
186
- });
269
+ // 自动恢复代理状态
270
+ autoRestoreProxies();
187
271
 
188
- // 监听端口占用错误
189
- server.on('error', (err) => {
190
- if (err.code === 'EADDRINUSE') {
191
- console.error(chalk.red(`\n❌ 端口 ${port} 已被占用`));
192
- console.error(chalk.yellow('\n💡 解决方案:'));
193
- console.error(chalk.gray(' 1. 运行 ctx 命令,选择"配置端口"修改端口'));
194
- console.error(chalk.gray(` 2. 或关闭占用端口 ${port} 的程序\n`));
195
- process.exit(1);
196
- }
197
- });
272
+ // 启动时执行健康检查
273
+ performStartupHealthCheck();
198
274
 
199
275
  return server;
200
276
  }
@@ -206,13 +282,13 @@ function autoRestoreProxies() {
206
282
  const fs = require('fs');
207
283
  const path = require('path');
208
284
 
209
- const ccToolDir = path.join(os.homedir(), '.claude', 'cc-tool');
285
+ const ccToolDir = path.join(os.homedir(), '.cc-tool');
210
286
 
211
287
  // 检查 Claude 代理状态文件
212
288
  const claudeActiveFile = path.join(ccToolDir, 'active-channel.json');
213
289
  if (fs.existsSync(claudeActiveFile)) {
214
290
  console.log(chalk.cyan('\n🔄 检测到 Claude 代理状态文件,正在自动启动...'));
215
- const proxyPort = config.ports?.proxy || 10088;
291
+ const proxyPort = config.ports?.proxy || 20088;
216
292
  startProxyServer(proxyPort)
217
293
  .then(() => {
218
294
  console.log(chalk.green(`✅ Claude 代理已自动启动,端口: ${proxyPort}`));
@@ -226,7 +302,7 @@ function autoRestoreProxies() {
226
302
  const codexActiveFile = path.join(ccToolDir, 'codex-active-channel.json');
227
303
  if (fs.existsSync(codexActiveFile)) {
228
304
  console.log(chalk.cyan('\n🔄 检测到 Codex 代理状态文件,正在自动启动...'));
229
- const codexProxyPort = config.ports?.codexProxy || 10089;
305
+ const codexProxyPort = config.ports?.codexProxy || 20089;
230
306
  startCodexProxyServer(codexProxyPort)
231
307
  .then((result) => {
232
308
  const port = result?.port || codexProxyPort;
@@ -251,7 +327,7 @@ function autoRestoreProxies() {
251
327
  const geminiActiveFile = path.join(ccToolDir, 'gemini-active-channel.json');
252
328
  if (fs.existsSync(geminiActiveFile)) {
253
329
  console.log(chalk.cyan('\n🔄 检测到 Gemini 代理状态文件,正在自动启动...'));
254
- const geminiProxyPort = config.ports?.geminiProxy || 10090;
330
+ const geminiProxyPort = config.ports?.geminiProxy || 20090;
255
331
  startGeminiProxyServer(geminiProxyPort)
256
332
  .then((result) => {
257
333
  if (result.success) {
@@ -266,6 +342,46 @@ function autoRestoreProxies() {
266
342
  } else {
267
343
  console.log(chalk.gray('\n💡 提示: 如需使用 Gemini 代理,请在前端界面激活 Gemini 渠道'));
268
344
  }
345
+
346
+ // 检查 OpenCode 代理状态文件
347
+ const opencodeActiveFile = path.join(ccToolDir, 'opencode-active-channel.json');
348
+ if (fs.existsSync(opencodeActiveFile)) {
349
+ console.log(chalk.cyan('\n🔄 检测到 OpenCode 代理状态文件,正在自动启动...'));
350
+ const opencodeProxyPort = config.ports?.opencodeProxy || 20091;
351
+ startOpenCodeProxyServer(opencodeProxyPort)
352
+ .then((result) => {
353
+ if (result.success) {
354
+ console.log(chalk.green(`✅ OpenCode 代理已自动启动,端口: ${result.port}`));
355
+ try {
356
+ const { getEnabledChannels: getEnabledOpenCodeChannels } = require('./services/opencode-channels');
357
+ const enabledChs = getEnabledOpenCodeChannels();
358
+ const allModels = [];
359
+ const seen = new Set();
360
+ enabledChs.forEach((ch) => {
361
+ [ch.model, ch.speedTestModel].forEach((m) => {
362
+ if (typeof m === 'string' && m.trim() && !seen.has(m.trim().toLowerCase())) {
363
+ seen.add(m.trim().toLowerCase());
364
+ allModels.push(m.trim());
365
+ }
366
+ });
367
+ });
368
+ const firstChannel = enabledChs[0];
369
+ const activeModel = firstChannel && (firstChannel.model || firstChannel.speedTestModel) || null;
370
+ const cfgResult = setOpenCodeProxyConfig(result.port, { model: activeModel, models: allModels });
371
+ if (cfgResult?.success) {
372
+ console.log(chalk.gray(' 已同步 OpenCode 配置文件'));
373
+ }
374
+ } catch (err) {
375
+ console.error(chalk.red(`❌ OpenCode 代理配置同步失败: ${err.message}`));
376
+ }
377
+ } else {
378
+ console.error(chalk.red(`❌ OpenCode 代理启动失败: ${result.error || 'Unknown error'}`));
379
+ }
380
+ })
381
+ .catch((err) => {
382
+ console.error(chalk.red(`❌ OpenCode 代理启动失败: ${err.message}`));
383
+ });
384
+ }
269
385
  }
270
386
 
271
387
  // 启动时执行健康检查