@adversity/coding-tool-x 3.1.0 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -18
- package/README.md +8 -8
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
- package/dist/web/assets/Home-BJKPCBuk.css +1 -0
- package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
- package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
- package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
- package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
- package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
- package/dist/web/assets/Terminal-BasTyDut.js +1 -0
- package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
- package/dist/web/assets/icons-kcfLIMBB.js +1 -0
- package/dist/web/assets/index-CoB3zF0K.css +1 -0
- package/dist/web/assets/index-CryrSLv8.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
- package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
- package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
- package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
- package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
- package/dist/web/index.html +8 -6
- package/package.json +4 -2
- package/src/commands/channels.js +48 -1
- package/src/commands/cli-type.js +4 -2
- package/src/commands/daemon.js +81 -12
- package/src/commands/doctor.js +10 -9
- package/src/commands/list.js +1 -1
- package/src/commands/logs.js +6 -4
- package/src/commands/port-config.js +24 -4
- package/src/commands/proxy-control.js +12 -6
- package/src/commands/search.js +1 -1
- package/src/commands/security.js +3 -2
- package/src/commands/stats.js +226 -52
- package/src/commands/switch.js +1 -1
- package/src/commands/toggle-proxy.js +31 -6
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +1 -1
- package/src/config/default.js +41 -2
- package/src/config/loader.js +74 -8
- package/src/config/model-metadata.js +415 -0
- package/src/config/model-pricing.js +23 -93
- package/src/config/paths.js +105 -33
- package/src/index.js +64 -3
- package/src/plugins/constants.js +3 -2
- package/src/plugins/plugin-api.js +1 -1
- package/src/reset-config.js +4 -2
- package/src/server/api/agents.js +57 -14
- package/src/server/api/channels.js +112 -33
- package/src/server/api/codex-channels.js +111 -18
- package/src/server/api/codex-proxy.js +14 -8
- package/src/server/api/commands.js +71 -18
- package/src/server/api/config-export.js +0 -6
- package/src/server/api/config-registry.js +11 -3
- package/src/server/api/config.js +376 -5
- package/src/server/api/convert.js +133 -0
- package/src/server/api/dashboard.js +22 -6
- package/src/server/api/gemini-channels.js +107 -18
- package/src/server/api/gemini-proxy.js +14 -8
- package/src/server/api/gemini-sessions.js +1 -1
- package/src/server/api/health-check.js +4 -3
- package/src/server/api/mcp.js +3 -3
- package/src/server/api/opencode-channels.js +497 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +345 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +66 -19
- package/src/server/api/prompts.js +2 -2
- package/src/server/api/proxy.js +7 -4
- package/src/server/api/sessions.js +3 -0
- package/src/server/api/settings.js +111 -0
- package/src/server/api/skills.js +69 -18
- package/src/server/api/workspaces.js +78 -6
- package/src/server/codex-proxy-server.js +36 -22
- package/src/server/dev-server.js +1 -1
- package/src/server/gemini-proxy-server.js +21 -7
- package/src/server/index.js +174 -58
- package/src/server/opencode-proxy-server.js +5486 -0
- package/src/server/proxy-server.js +33 -22
- package/src/server/services/agents-service.js +61 -24
- package/src/server/services/channel-scheduler.js +9 -5
- package/src/server/services/channels.js +64 -37
- package/src/server/services/codex-channels.js +56 -43
- package/src/server/services/codex-sessions.js +105 -6
- package/src/server/services/codex-settings-manager.js +271 -49
- package/src/server/services/codex-statistics-service.js +2 -2
- package/src/server/services/commands-service.js +84 -25
- package/src/server/services/config-export-service.js +7 -45
- package/src/server/services/config-registry-service.js +63 -17
- package/src/server/services/config-sync-manager.js +160 -7
- package/src/server/services/config-templates-service.js +204 -51
- package/src/server/services/env-checker.js +50 -13
- package/src/server/services/env-manager.js +155 -19
- package/src/server/services/favorites.js +5 -3
- package/src/server/services/gemini-channels.js +33 -44
- package/src/server/services/gemini-statistics-service.js +2 -2
- package/src/server/services/mcp-service.js +350 -9
- package/src/server/services/model-detector.js +707 -221
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +208 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +255 -0
- package/src/server/services/plugins-service.js +479 -22
- package/src/server/services/prompts-service.js +53 -11
- package/src/server/services/proxy-runtime.js +1 -1
- package/src/server/services/repo-scanner-base.js +1 -1
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +1 -1
- package/src/server/services/session-cache.js +1 -1
- package/src/server/services/skill-service.js +300 -46
- package/src/server/services/speed-test.js +464 -186
- package/src/server/services/statistics-service.js +2 -2
- package/src/server/services/terminal-commands.js +10 -3
- package/src/server/services/terminal-config.js +1 -1
- package/src/server/services/ui-config.js +1 -1
- package/src/server/services/workspace-service.js +57 -100
- package/src/server/websocket-server.js +156 -8
- package/src/ui/menu.js +49 -40
- package/src/utils/port-helper.js +22 -8
- package/src/utils/session.js +5 -4
- package/dist/web/assets/icons-CO_2OFES.js +0 -1
- package/dist/web/assets/index-DI8QOi-E.js +0 -14
- package/dist/web/assets/index-uLHGdeZh.css +0 -41
- package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
- package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
- package/src/server/api/oauth.js +0 -294
- package/src/server/api/permissions.js +0 -385
- package/src/server/config/oauth-providers.js +0 -68
- package/src/server/services/oauth-callback-server.js +0 -284
- package/src/server/services/oauth-service.js +0 -378
- package/src/server/services/oauth-token-storage.js +0 -135
- package/src/server/services/permission-templates-service.js +0 -308
package/src/server/api/skills.js
CHANGED
|
@@ -6,7 +6,24 @@ const express = require('express');
|
|
|
6
6
|
const { SkillService } = require('../services/skill-service');
|
|
7
7
|
|
|
8
8
|
const router = express.Router();
|
|
9
|
-
const
|
|
9
|
+
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'opencode'];
|
|
10
|
+
const skillServices = new Map();
|
|
11
|
+
|
|
12
|
+
function resolvePlatform(rawPlatform) {
|
|
13
|
+
return SUPPORTED_PLATFORMS.includes(rawPlatform) ? rawPlatform : 'claude';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getPlatform(req) {
|
|
17
|
+
return resolvePlatform(req.query?.platform || req.body?.platform);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getSkillService(req) {
|
|
21
|
+
const platform = getPlatform(req);
|
|
22
|
+
if (!skillServices.has(platform)) {
|
|
23
|
+
skillServices.set(platform, new SkillService(platform));
|
|
24
|
+
}
|
|
25
|
+
return { platform, service: skillServices.get(platform) };
|
|
26
|
+
}
|
|
10
27
|
|
|
11
28
|
/**
|
|
12
29
|
* 获取技能列表
|
|
@@ -15,10 +32,12 @@ const skillService = new SkillService();
|
|
|
15
32
|
*/
|
|
16
33
|
router.get('/', async (req, res) => {
|
|
17
34
|
try {
|
|
35
|
+
const { platform, service } = getSkillService(req);
|
|
18
36
|
const forceRefresh = req.query.refresh === '1';
|
|
19
|
-
const skills = await
|
|
37
|
+
const skills = await service.listSkills(forceRefresh);
|
|
20
38
|
res.json({
|
|
21
39
|
success: true,
|
|
40
|
+
platform,
|
|
22
41
|
skills,
|
|
23
42
|
total: skills.length,
|
|
24
43
|
installed: skills.filter(s => s.installed).length
|
|
@@ -38,6 +57,7 @@ router.get('/', async (req, res) => {
|
|
|
38
57
|
*/
|
|
39
58
|
router.get('/detail/*', async (req, res) => {
|
|
40
59
|
try {
|
|
60
|
+
const { platform, service } = getSkillService(req);
|
|
41
61
|
const directory = req.params[0]; // 获取通配符匹配的路径
|
|
42
62
|
if (!directory) {
|
|
43
63
|
return res.status(400).json({
|
|
@@ -46,9 +66,10 @@ router.get('/detail/*', async (req, res) => {
|
|
|
46
66
|
});
|
|
47
67
|
}
|
|
48
68
|
|
|
49
|
-
const result = await
|
|
69
|
+
const result = await service.getSkillDetail(directory);
|
|
50
70
|
res.json({
|
|
51
71
|
success: true,
|
|
72
|
+
platform,
|
|
52
73
|
...result
|
|
53
74
|
});
|
|
54
75
|
} catch (err) {
|
|
@@ -66,9 +87,11 @@ router.get('/detail/*', async (req, res) => {
|
|
|
66
87
|
*/
|
|
67
88
|
router.get('/installed', (req, res) => {
|
|
68
89
|
try {
|
|
69
|
-
const
|
|
90
|
+
const { platform, service } = getSkillService(req);
|
|
91
|
+
const skills = service.getInstalledSkills();
|
|
70
92
|
res.json({
|
|
71
93
|
success: true,
|
|
94
|
+
platform,
|
|
72
95
|
skills
|
|
73
96
|
});
|
|
74
97
|
} catch (err) {
|
|
@@ -89,6 +112,7 @@ router.get('/installed', (req, res) => {
|
|
|
89
112
|
*/
|
|
90
113
|
router.post('/install', async (req, res) => {
|
|
91
114
|
try {
|
|
115
|
+
const { platform, service } = getSkillService(req);
|
|
92
116
|
const { directory, fullDirectory, repo } = req.body;
|
|
93
117
|
|
|
94
118
|
if (!directory) {
|
|
@@ -105,7 +129,7 @@ router.post('/install', async (req, res) => {
|
|
|
105
129
|
});
|
|
106
130
|
}
|
|
107
131
|
|
|
108
|
-
const result = await
|
|
132
|
+
const result = await service.installSkill(
|
|
109
133
|
directory,
|
|
110
134
|
{
|
|
111
135
|
owner: repo.owner,
|
|
@@ -117,6 +141,7 @@ router.post('/install', async (req, res) => {
|
|
|
117
141
|
|
|
118
142
|
res.json({
|
|
119
143
|
success: true,
|
|
144
|
+
platform,
|
|
120
145
|
...result
|
|
121
146
|
});
|
|
122
147
|
} catch (err) {
|
|
@@ -135,6 +160,7 @@ router.post('/install', async (req, res) => {
|
|
|
135
160
|
*/
|
|
136
161
|
router.post('/create', (req, res) => {
|
|
137
162
|
try {
|
|
163
|
+
const { platform, service } = getSkillService(req);
|
|
138
164
|
const { name, directory, description, content } = req.body;
|
|
139
165
|
|
|
140
166
|
if (!directory) {
|
|
@@ -159,7 +185,7 @@ router.post('/create', (req, res) => {
|
|
|
159
185
|
});
|
|
160
186
|
}
|
|
161
187
|
|
|
162
|
-
const result =
|
|
188
|
+
const result = service.createCustomSkill({
|
|
163
189
|
name: name || directory,
|
|
164
190
|
directory,
|
|
165
191
|
description: description || '',
|
|
@@ -168,6 +194,7 @@ router.post('/create', (req, res) => {
|
|
|
168
194
|
|
|
169
195
|
res.json({
|
|
170
196
|
success: true,
|
|
197
|
+
platform,
|
|
171
198
|
...result
|
|
172
199
|
});
|
|
173
200
|
} catch (err) {
|
|
@@ -186,6 +213,7 @@ router.post('/create', (req, res) => {
|
|
|
186
213
|
*/
|
|
187
214
|
router.post('/uninstall', (req, res) => {
|
|
188
215
|
try {
|
|
216
|
+
const { platform, service } = getSkillService(req);
|
|
189
217
|
const { directory } = req.body;
|
|
190
218
|
|
|
191
219
|
if (!directory) {
|
|
@@ -195,10 +223,11 @@ router.post('/uninstall', (req, res) => {
|
|
|
195
223
|
});
|
|
196
224
|
}
|
|
197
225
|
|
|
198
|
-
const result =
|
|
226
|
+
const result = service.uninstallSkill(directory);
|
|
199
227
|
|
|
200
228
|
res.json({
|
|
201
229
|
success: true,
|
|
230
|
+
platform,
|
|
202
231
|
...result
|
|
203
232
|
});
|
|
204
233
|
} catch (err) {
|
|
@@ -216,9 +245,11 @@ router.post('/uninstall', (req, res) => {
|
|
|
216
245
|
*/
|
|
217
246
|
router.get('/repos', (req, res) => {
|
|
218
247
|
try {
|
|
219
|
-
const
|
|
248
|
+
const { platform, service } = getSkillService(req);
|
|
249
|
+
const repos = service.loadRepos();
|
|
220
250
|
res.json({
|
|
221
251
|
success: true,
|
|
252
|
+
platform,
|
|
222
253
|
repos
|
|
223
254
|
});
|
|
224
255
|
} catch (err) {
|
|
@@ -238,6 +269,7 @@ router.get('/repos', (req, res) => {
|
|
|
238
269
|
*/
|
|
239
270
|
router.post('/repos', (req, res) => {
|
|
240
271
|
try {
|
|
272
|
+
const { platform, service } = getSkillService(req);
|
|
241
273
|
const { owner, name, branch = 'main', directory = '', enabled = true } = req.body;
|
|
242
274
|
|
|
243
275
|
if (!owner || !name) {
|
|
@@ -247,10 +279,11 @@ router.post('/repos', (req, res) => {
|
|
|
247
279
|
});
|
|
248
280
|
}
|
|
249
281
|
|
|
250
|
-
const repos =
|
|
282
|
+
const repos = service.addRepo({ owner, name, branch, directory, enabled });
|
|
251
283
|
|
|
252
284
|
res.json({
|
|
253
285
|
success: true,
|
|
286
|
+
platform,
|
|
254
287
|
repos
|
|
255
288
|
});
|
|
256
289
|
} catch (err) {
|
|
@@ -269,12 +302,14 @@ router.post('/repos', (req, res) => {
|
|
|
269
302
|
*/
|
|
270
303
|
router.delete('/repos/:owner/:name', (req, res) => {
|
|
271
304
|
try {
|
|
305
|
+
const { platform, service } = getSkillService(req);
|
|
272
306
|
const { owner, name } = req.params;
|
|
273
307
|
const { directory = '' } = req.query;
|
|
274
|
-
const repos =
|
|
308
|
+
const repos = service.removeRepo(owner, name, directory);
|
|
275
309
|
|
|
276
310
|
res.json({
|
|
277
311
|
success: true,
|
|
312
|
+
platform,
|
|
278
313
|
repos
|
|
279
314
|
});
|
|
280
315
|
} catch (err) {
|
|
@@ -294,13 +329,15 @@ router.delete('/repos/:owner/:name', (req, res) => {
|
|
|
294
329
|
*/
|
|
295
330
|
router.put('/repos/:owner/:name/toggle', (req, res) => {
|
|
296
331
|
try {
|
|
332
|
+
const { platform, service } = getSkillService(req);
|
|
297
333
|
const { owner, name } = req.params;
|
|
298
334
|
const { enabled, directory = '' } = req.body;
|
|
299
335
|
|
|
300
|
-
const repos =
|
|
336
|
+
const repos = service.toggleRepo(owner, name, directory, enabled);
|
|
301
337
|
|
|
302
338
|
res.json({
|
|
303
339
|
success: true,
|
|
340
|
+
platform,
|
|
304
341
|
repos
|
|
305
342
|
});
|
|
306
343
|
} catch (err) {
|
|
@@ -321,6 +358,7 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
|
|
|
321
358
|
*/
|
|
322
359
|
router.post('/create-with-files', (req, res) => {
|
|
323
360
|
try {
|
|
361
|
+
const { platform, service } = getSkillService(req);
|
|
324
362
|
const { directory, files } = req.body;
|
|
325
363
|
|
|
326
364
|
if (!directory) {
|
|
@@ -345,10 +383,11 @@ router.post('/create-with-files', (req, res) => {
|
|
|
345
383
|
});
|
|
346
384
|
}
|
|
347
385
|
|
|
348
|
-
const result =
|
|
386
|
+
const result = service.createSkillWithFiles({ directory, files });
|
|
349
387
|
|
|
350
388
|
res.json({
|
|
351
389
|
success: true,
|
|
390
|
+
platform,
|
|
352
391
|
...result
|
|
353
392
|
});
|
|
354
393
|
} catch (err) {
|
|
@@ -366,11 +405,13 @@ router.post('/create-with-files', (req, res) => {
|
|
|
366
405
|
*/
|
|
367
406
|
router.get('/:directory/files', (req, res) => {
|
|
368
407
|
try {
|
|
408
|
+
const { platform, service } = getSkillService(req);
|
|
369
409
|
const { directory } = req.params;
|
|
370
|
-
const files =
|
|
410
|
+
const files = service.getSkillFiles(directory);
|
|
371
411
|
|
|
372
412
|
res.json({
|
|
373
413
|
success: true,
|
|
414
|
+
platform,
|
|
374
415
|
directory,
|
|
375
416
|
files
|
|
376
417
|
});
|
|
@@ -390,6 +431,7 @@ router.get('/:directory/files', (req, res) => {
|
|
|
390
431
|
*/
|
|
391
432
|
router.get('/:directory/file/*', (req, res) => {
|
|
392
433
|
try {
|
|
434
|
+
const { platform, service } = getSkillService(req);
|
|
393
435
|
const { directory } = req.params;
|
|
394
436
|
const filePath = req.params[0];
|
|
395
437
|
|
|
@@ -400,10 +442,11 @@ router.get('/:directory/file/*', (req, res) => {
|
|
|
400
442
|
});
|
|
401
443
|
}
|
|
402
444
|
|
|
403
|
-
const result =
|
|
445
|
+
const result = service.getSkillFileContent(directory, filePath);
|
|
404
446
|
|
|
405
447
|
res.json({
|
|
406
448
|
success: true,
|
|
449
|
+
platform,
|
|
407
450
|
...result
|
|
408
451
|
});
|
|
409
452
|
} catch (err) {
|
|
@@ -422,6 +465,7 @@ router.get('/:directory/file/*', (req, res) => {
|
|
|
422
465
|
*/
|
|
423
466
|
router.post('/:directory/files', (req, res) => {
|
|
424
467
|
try {
|
|
468
|
+
const { platform, service } = getSkillService(req);
|
|
425
469
|
const { directory } = req.params;
|
|
426
470
|
const { files } = req.body;
|
|
427
471
|
|
|
@@ -432,10 +476,11 @@ router.post('/:directory/files', (req, res) => {
|
|
|
432
476
|
});
|
|
433
477
|
}
|
|
434
478
|
|
|
435
|
-
const result =
|
|
479
|
+
const result = service.addSkillFiles(directory, files);
|
|
436
480
|
|
|
437
481
|
res.json({
|
|
438
482
|
success: true,
|
|
483
|
+
platform,
|
|
439
484
|
...result
|
|
440
485
|
});
|
|
441
486
|
} catch (err) {
|
|
@@ -453,6 +498,7 @@ router.post('/:directory/files', (req, res) => {
|
|
|
453
498
|
*/
|
|
454
499
|
router.delete('/:directory/file/*', (req, res) => {
|
|
455
500
|
try {
|
|
501
|
+
const { platform, service } = getSkillService(req);
|
|
456
502
|
const { directory } = req.params;
|
|
457
503
|
const filePath = req.params[0];
|
|
458
504
|
|
|
@@ -463,10 +509,11 @@ router.delete('/:directory/file/*', (req, res) => {
|
|
|
463
509
|
});
|
|
464
510
|
}
|
|
465
511
|
|
|
466
|
-
const result =
|
|
512
|
+
const result = service.deleteSkillFile(directory, filePath);
|
|
467
513
|
|
|
468
514
|
res.json({
|
|
469
515
|
success: true,
|
|
516
|
+
platform,
|
|
470
517
|
...result
|
|
471
518
|
});
|
|
472
519
|
} catch (err) {
|
|
@@ -485,6 +532,7 @@ router.delete('/:directory/file/*', (req, res) => {
|
|
|
485
532
|
*/
|
|
486
533
|
router.put('/:directory/file/*', (req, res) => {
|
|
487
534
|
try {
|
|
535
|
+
const { platform, service } = getSkillService(req);
|
|
488
536
|
const { directory } = req.params;
|
|
489
537
|
const filePath = req.params[0];
|
|
490
538
|
const { content, isBase64 = false } = req.body;
|
|
@@ -503,10 +551,11 @@ router.put('/:directory/file/*', (req, res) => {
|
|
|
503
551
|
});
|
|
504
552
|
}
|
|
505
553
|
|
|
506
|
-
const result =
|
|
554
|
+
const result = service.updateSkillFile(directory, filePath, content, isBase64);
|
|
507
555
|
|
|
508
556
|
res.json({
|
|
509
557
|
success: true,
|
|
558
|
+
platform,
|
|
510
559
|
...result
|
|
511
560
|
});
|
|
512
561
|
} catch (err) {
|
|
@@ -529,6 +578,7 @@ router.put('/:directory/file/*', (req, res) => {
|
|
|
529
578
|
*/
|
|
530
579
|
router.post('/convert', (req, res) => {
|
|
531
580
|
try {
|
|
581
|
+
const { platform, service } = getSkillService(req);
|
|
532
582
|
const { content, targetFormat } = req.body;
|
|
533
583
|
|
|
534
584
|
if (!content) {
|
|
@@ -545,10 +595,11 @@ router.post('/convert', (req, res) => {
|
|
|
545
595
|
});
|
|
546
596
|
}
|
|
547
597
|
|
|
548
|
-
const result =
|
|
598
|
+
const result = service.convertSkillFormat(content, targetFormat);
|
|
549
599
|
|
|
550
600
|
res.json({
|
|
551
601
|
success: true,
|
|
602
|
+
platform,
|
|
552
603
|
...result
|
|
553
604
|
});
|
|
554
605
|
} catch (err) {
|
|
@@ -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,6 +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 { createDecodedStream } = require('./services/response-decoder');
|
|
13
14
|
const { getEnabledChannels, writeCodexConfigForMultiChannel, getEffectiveApiKey } = require('./services/codex-channels');
|
|
14
15
|
const { CLAUDE_MODEL_PRICING } = require('../config/model-pricing');
|
|
15
16
|
|
|
@@ -230,7 +231,7 @@ async function startCodexProxyServer(options = {}) {
|
|
|
230
231
|
|
|
231
232
|
try {
|
|
232
233
|
const config = loadConfig();
|
|
233
|
-
const port = config.ports?.codexProxy ||
|
|
234
|
+
const port = config.ports?.codexProxy || 20089;
|
|
234
235
|
currentPort = port;
|
|
235
236
|
|
|
236
237
|
proxyApp = express();
|
|
@@ -257,7 +258,7 @@ async function startCodexProxyServer(options = {}) {
|
|
|
257
258
|
});
|
|
258
259
|
|
|
259
260
|
proxyReq.removeHeader('authorization');
|
|
260
|
-
const effectiveKey =
|
|
261
|
+
const effectiveKey = req.effectiveApiKey;
|
|
261
262
|
proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
|
|
262
263
|
proxyReq.setHeader('openai-beta', 'responses=experimental');
|
|
263
264
|
if (!proxyReq.getHeader('content-type')) {
|
|
@@ -279,6 +280,33 @@ async function startCodexProxyServer(options = {}) {
|
|
|
279
280
|
const channel = await allocateChannel({ source: 'codex', enableSessionBinding: false });
|
|
280
281
|
req.selectedChannel = channel;
|
|
281
282
|
|
|
283
|
+
const release = (() => {
|
|
284
|
+
let released = false;
|
|
285
|
+
return () => {
|
|
286
|
+
if (released) return;
|
|
287
|
+
released = true;
|
|
288
|
+
releaseChannel(channel.id, 'codex');
|
|
289
|
+
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
290
|
+
};
|
|
291
|
+
})();
|
|
292
|
+
|
|
293
|
+
res.on('close', release);
|
|
294
|
+
res.on('error', release);
|
|
295
|
+
|
|
296
|
+
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
297
|
+
|
|
298
|
+
const effectiveKey = getEffectiveApiKey(channel);
|
|
299
|
+
if (!effectiveKey) {
|
|
300
|
+
release();
|
|
301
|
+
return res.status(401).json({
|
|
302
|
+
error: {
|
|
303
|
+
message: 'API key not configured or expired. Please update your channel key.',
|
|
304
|
+
type: 'authentication_error'
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
req.effectiveApiKey = effectiveKey;
|
|
309
|
+
|
|
282
310
|
// 应用模型重定向(当 proxy 开启时)
|
|
283
311
|
if (req.body && req.body.model) {
|
|
284
312
|
const originalModel = req.body.model;
|
|
@@ -299,21 +327,6 @@ async function startCodexProxyServer(options = {}) {
|
|
|
299
327
|
}
|
|
300
328
|
}
|
|
301
329
|
|
|
302
|
-
const release = (() => {
|
|
303
|
-
let released = false;
|
|
304
|
-
return () => {
|
|
305
|
-
if (released) return;
|
|
306
|
-
released = true;
|
|
307
|
-
releaseChannel(channel.id, 'codex');
|
|
308
|
-
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
309
|
-
};
|
|
310
|
-
})();
|
|
311
|
-
|
|
312
|
-
res.on('close', release);
|
|
313
|
-
res.on('error', release);
|
|
314
|
-
|
|
315
|
-
broadcastSchedulerState('codex', getSchedulerState('codex'));
|
|
316
|
-
|
|
317
330
|
const target = resolveCodexTarget(channel.baseUrl, req.url);
|
|
318
331
|
|
|
319
332
|
proxy.web(req, res, {
|
|
@@ -390,14 +403,15 @@ async function startCodexProxyServer(options = {}) {
|
|
|
390
403
|
totalTokens: 0,
|
|
391
404
|
model: ''
|
|
392
405
|
};
|
|
406
|
+
const parsedStream = createDecodedStream(proxyRes);
|
|
393
407
|
|
|
394
|
-
|
|
408
|
+
parsedStream.on('data', (chunk) => {
|
|
395
409
|
// 如果响应已关闭,停止处理
|
|
396
410
|
if (isResponseClosed) {
|
|
397
411
|
return;
|
|
398
412
|
}
|
|
399
413
|
|
|
400
|
-
buffer += chunk.toString();
|
|
414
|
+
buffer += chunk.toString('utf8');
|
|
401
415
|
|
|
402
416
|
// 检查是否是 SSE 流
|
|
403
417
|
if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
|
|
@@ -463,7 +477,7 @@ async function startCodexProxyServer(options = {}) {
|
|
|
463
477
|
}
|
|
464
478
|
});
|
|
465
479
|
|
|
466
|
-
|
|
480
|
+
parsedStream.on('end', () => {
|
|
467
481
|
// 如果不是流式响应,尝试从完整响应中解析
|
|
468
482
|
if (!proxyRes.headers['content-type']?.includes('text/event-stream')) {
|
|
469
483
|
try {
|
|
@@ -546,7 +560,7 @@ async function startCodexProxyServer(options = {}) {
|
|
|
546
560
|
}
|
|
547
561
|
});
|
|
548
562
|
|
|
549
|
-
|
|
563
|
+
parsedStream.on('error', (err) => {
|
|
550
564
|
// 忽略代理响应错误(可能是网络问题)
|
|
551
565
|
if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
|
|
552
566
|
console.error('Proxy response error:', err);
|
|
@@ -654,7 +668,7 @@ function getCodexProxyStatus() {
|
|
|
654
668
|
return {
|
|
655
669
|
running: !!proxyServer,
|
|
656
670
|
port: currentPort,
|
|
657
|
-
defaultPort: config.ports?.codexProxy ||
|
|
671
|
+
defaultPort: config.ports?.codexProxy || 20089,
|
|
658
672
|
startTime,
|
|
659
673
|
runtime
|
|
660
674
|
};
|
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 { createDecodedStream } = require('./services/response-decoder');
|
|
13
14
|
const { getEffectiveApiKey } = require('./services/gemini-channels');
|
|
14
15
|
|
|
15
16
|
let proxyServer = null;
|
|
@@ -126,7 +127,7 @@ async function startGeminiProxyServer(options = {}) {
|
|
|
126
127
|
|
|
127
128
|
try {
|
|
128
129
|
const config = loadConfig();
|
|
129
|
-
const port = config.ports?.geminiProxy ||
|
|
130
|
+
const port = config.ports?.geminiProxy || 20090;
|
|
130
131
|
currentPort = port;
|
|
131
132
|
|
|
132
133
|
proxyApp = express();
|
|
@@ -153,7 +154,7 @@ async function startGeminiProxyServer(options = {}) {
|
|
|
153
154
|
|
|
154
155
|
proxyReq.removeHeader('authorization');
|
|
155
156
|
proxyReq.removeHeader('x-goog-api-key');
|
|
156
|
-
const effectiveKey =
|
|
157
|
+
const effectiveKey = req.effectiveApiKey;
|
|
157
158
|
proxyReq.setHeader('authorization', `Bearer ${effectiveKey}`);
|
|
158
159
|
if (!proxyReq.getHeader('content-type')) {
|
|
159
160
|
proxyReq.setHeader('content-type', 'application/json');
|
|
@@ -180,6 +181,18 @@ async function startGeminiProxyServer(options = {}) {
|
|
|
180
181
|
|
|
181
182
|
broadcastSchedulerState('gemini', getSchedulerState('gemini'));
|
|
182
183
|
|
|
184
|
+
const effectiveKey = getEffectiveApiKey(channel);
|
|
185
|
+
if (!effectiveKey) {
|
|
186
|
+
release();
|
|
187
|
+
return res.status(401).json({
|
|
188
|
+
error: {
|
|
189
|
+
message: 'API key not configured or expired. Please update your channel key.',
|
|
190
|
+
type: 'authentication_error'
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
req.effectiveApiKey = effectiveKey;
|
|
195
|
+
|
|
183
196
|
// 从 URL 中提取模型名称并应用重定向
|
|
184
197
|
// URL 格式: /models/gemini-2.5-pro:generateContent 或 /v1/models/gemini-2.5-pro:generateContent
|
|
185
198
|
const urlMatch = req.url.match(/\/models\/([\w.-]+)(:[^?]*)?/);
|
|
@@ -277,14 +290,15 @@ async function startGeminiProxyServer(options = {}) {
|
|
|
277
290
|
totalTokens: 0,
|
|
278
291
|
model: ''
|
|
279
292
|
};
|
|
293
|
+
const parsedStream = createDecodedStream(proxyRes);
|
|
280
294
|
|
|
281
|
-
|
|
295
|
+
parsedStream.on('data', (chunk) => {
|
|
282
296
|
// 如果响应已关闭,停止处理
|
|
283
297
|
if (isResponseClosed) {
|
|
284
298
|
return;
|
|
285
299
|
}
|
|
286
300
|
|
|
287
|
-
buffer += chunk.toString();
|
|
301
|
+
buffer += chunk.toString('utf8');
|
|
288
302
|
|
|
289
303
|
// 检查是否是 SSE 流
|
|
290
304
|
if (proxyRes.headers['content-type']?.includes('text/event-stream')) {
|
|
@@ -348,7 +362,7 @@ async function startGeminiProxyServer(options = {}) {
|
|
|
348
362
|
}
|
|
349
363
|
});
|
|
350
364
|
|
|
351
|
-
|
|
365
|
+
parsedStream.on('end', () => {
|
|
352
366
|
// 如果不是流式响应,尝试从完整响应中解析
|
|
353
367
|
if (!proxyRes.headers['content-type']?.includes('text/event-stream')) {
|
|
354
368
|
try {
|
|
@@ -455,7 +469,7 @@ async function startGeminiProxyServer(options = {}) {
|
|
|
455
469
|
}
|
|
456
470
|
});
|
|
457
471
|
|
|
458
|
-
|
|
472
|
+
parsedStream.on('error', (err) => {
|
|
459
473
|
// 忽略代理响应错误(可能是网络问题)
|
|
460
474
|
if (err.code !== 'EPIPE' && err.code !== 'ECONNRESET') {
|
|
461
475
|
console.error('Proxy response error:', err);
|
|
@@ -553,7 +567,7 @@ function getGeminiProxyStatus() {
|
|
|
553
567
|
return {
|
|
554
568
|
running: !!proxyServer,
|
|
555
569
|
port: currentPort,
|
|
556
|
-
defaultPort: config.ports?.geminiProxy ||
|
|
570
|
+
defaultPort: config.ports?.geminiProxy || 20090,
|
|
557
571
|
startTime,
|
|
558
572
|
runtime
|
|
559
573
|
};
|