@adversity/coding-tool-x 3.0.6 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-ZrK_s7ma.js +1 -0
- package/dist/web/assets/Home-B8YfhZ3c.js +1 -0
- package/dist/web/assets/Home-Di2qsylF.css +1 -0
- package/dist/web/assets/PluginManager-BD7QUZbU.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-DRb1DuHV.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-lZ0LKzfT.js +1 -0
- package/dist/web/assets/SkillManager-C1xG5B4Q.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/Terminal-DksBo_lM.js +1 -0
- package/dist/web/assets/WorkspaceManager-Burx7XOo.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-Ufv5rCa5.css +1 -0
- package/dist/web/assets/index-lAkrRC3h.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +92 -13
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/ui.js +8 -1
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +39 -2
- package/src/config/loader.js +74 -8
- package/src/config/paths.js +105 -33
- package/src/index.js +67 -4
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +198 -0
- package/src/server/api/opencode-sessions.js +403 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +32 -19
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +17 -3
- package/src/server/index.js +164 -48
- package/src/server/opencode-proxy-server.js +4375 -0
- package/src/server/proxy-server.js +30 -19
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +70 -12
- package/src/server/services/codex-channels.js +61 -23
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +26 -12
- package/src/server/services/env-manager.js +126 -18
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +37 -15
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +206 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +663 -0
- package/src/server/services/opencode-settings-manager.js +342 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +132 -3
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-BxudHPiX.js +0 -1
- package/dist/web/assets/index-D2VfwJBa.js +0 -14
- package/dist/web/assets/index-oXBzu0bd.css +0 -41
- package/dist/web/assets/naive-ui-DT-Uur8K.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/permissions.js +0 -385
- package/src/server/services/permission-templates-service.js +0 -308
|
@@ -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
|
|
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 &&
|
|
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 ||
|
|
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
|
-
|
|
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 ||
|
|
669
|
+
defaultPort: config.ports?.codexProxy || 20089,
|
|
657
670
|
startTime,
|
|
658
671
|
runtime
|
|
659
672
|
};
|
package/src/server/dev-server.js
CHANGED
|
@@ -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 ||
|
|
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 ||
|
|
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
|
-
|
|
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 ||
|
|
568
|
+
defaultPort: config.ports?.geminiProxy || 20090,
|
|
555
569
|
startTime,
|
|
556
570
|
runtime
|
|
557
571
|
};
|
package/src/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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 ||
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
258
|
+
// 附加 WebSocket 服务器到同一个端口
|
|
259
|
+
attachWebSocketServer(server, { host, allowRemoteTerminal });
|
|
260
|
+
console.log(` ws://localhost:${port}/ws\n`);
|
|
180
261
|
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
});
|
|
269
|
+
// 自动恢复代理状态
|
|
270
|
+
autoRestoreProxies();
|
|
187
271
|
|
|
188
|
-
//
|
|
189
|
-
|
|
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(), '.
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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
|
// 启动时执行健康检查
|