@adversity/coding-tool-x 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +39 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
  5. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  6. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  7. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  8. package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
  13. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  14. package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
  15. package/dist/web/assets/Terminal-BasTyDut.js +1 -0
  16. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  17. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  18. package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  21. package/dist/web/assets/index-CryrSLv8.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +41 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/model-metadata.js +415 -0
  49. package/src/config/model-pricing.js +23 -93
  50. package/src/config/paths.js +105 -33
  51. package/src/index.js +64 -3
  52. package/src/plugins/constants.js +3 -2
  53. package/src/plugins/plugin-api.js +1 -1
  54. package/src/reset-config.js +4 -2
  55. package/src/server/api/agents.js +57 -14
  56. package/src/server/api/channels.js +112 -33
  57. package/src/server/api/codex-channels.js +111 -18
  58. package/src/server/api/codex-proxy.js +14 -8
  59. package/src/server/api/commands.js +71 -18
  60. package/src/server/api/config-export.js +0 -6
  61. package/src/server/api/config-registry.js +11 -3
  62. package/src/server/api/config.js +376 -5
  63. package/src/server/api/convert.js +133 -0
  64. package/src/server/api/dashboard.js +22 -6
  65. package/src/server/api/gemini-channels.js +107 -18
  66. package/src/server/api/gemini-proxy.js +14 -8
  67. package/src/server/api/gemini-sessions.js +1 -1
  68. package/src/server/api/health-check.js +4 -3
  69. package/src/server/api/mcp.js +3 -3
  70. package/src/server/api/opencode-channels.js +497 -0
  71. package/src/server/api/opencode-projects.js +99 -0
  72. package/src/server/api/opencode-proxy.js +207 -0
  73. package/src/server/api/opencode-sessions.js +345 -0
  74. package/src/server/api/opencode-statistics.js +57 -0
  75. package/src/server/api/plugins.js +66 -19
  76. package/src/server/api/prompts.js +2 -2
  77. package/src/server/api/proxy.js +7 -4
  78. package/src/server/api/sessions.js +3 -0
  79. package/src/server/api/settings.js +111 -0
  80. package/src/server/api/skills.js +69 -18
  81. package/src/server/api/workspaces.js +78 -6
  82. package/src/server/codex-proxy-server.js +36 -22
  83. package/src/server/dev-server.js +1 -1
  84. package/src/server/gemini-proxy-server.js +21 -7
  85. package/src/server/index.js +174 -58
  86. package/src/server/opencode-proxy-server.js +5486 -0
  87. package/src/server/proxy-server.js +33 -22
  88. package/src/server/services/agents-service.js +61 -24
  89. package/src/server/services/channel-scheduler.js +9 -5
  90. package/src/server/services/channels.js +64 -37
  91. package/src/server/services/codex-channels.js +56 -43
  92. package/src/server/services/codex-sessions.js +105 -6
  93. package/src/server/services/codex-settings-manager.js +271 -49
  94. package/src/server/services/codex-statistics-service.js +2 -2
  95. package/src/server/services/commands-service.js +84 -25
  96. package/src/server/services/config-export-service.js +7 -45
  97. package/src/server/services/config-registry-service.js +63 -17
  98. package/src/server/services/config-sync-manager.js +160 -7
  99. package/src/server/services/config-templates-service.js +204 -51
  100. package/src/server/services/env-checker.js +50 -13
  101. package/src/server/services/env-manager.js +155 -19
  102. package/src/server/services/favorites.js +5 -3
  103. package/src/server/services/gemini-channels.js +33 -44
  104. package/src/server/services/gemini-statistics-service.js +2 -2
  105. package/src/server/services/mcp-service.js +350 -9
  106. package/src/server/services/model-detector.js +707 -221
  107. package/src/server/services/network-access.js +80 -0
  108. package/src/server/services/opencode-channels.js +208 -0
  109. package/src/server/services/opencode-gateway-converter.js +639 -0
  110. package/src/server/services/opencode-sessions.js +931 -0
  111. package/src/server/services/opencode-settings-manager.js +478 -0
  112. package/src/server/services/opencode-statistics-service.js +255 -0
  113. package/src/server/services/plugins-service.js +479 -22
  114. package/src/server/services/prompts-service.js +53 -11
  115. package/src/server/services/proxy-runtime.js +1 -1
  116. package/src/server/services/repo-scanner-base.js +1 -1
  117. package/src/server/services/response-decoder.js +21 -0
  118. package/src/server/services/security-config.js +1 -1
  119. package/src/server/services/session-cache.js +1 -1
  120. package/src/server/services/skill-service.js +300 -46
  121. package/src/server/services/speed-test.js +464 -186
  122. package/src/server/services/statistics-service.js +2 -2
  123. package/src/server/services/terminal-commands.js +10 -3
  124. package/src/server/services/terminal-config.js +1 -1
  125. package/src/server/services/ui-config.js +1 -1
  126. package/src/server/services/workspace-service.js +57 -100
  127. package/src/server/websocket-server.js +156 -8
  128. package/src/ui/menu.js +49 -40
  129. package/src/utils/port-helper.js +22 -8
  130. package/src/utils/session.js +5 -4
  131. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  132. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  133. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  134. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  135. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  136. package/src/server/api/oauth.js +0 -294
  137. package/src/server/api/permissions.js +0 -385
  138. package/src/server/config/oauth-providers.js +0 -68
  139. package/src/server/services/oauth-callback-server.js +0 -284
  140. package/src/server/services/oauth-service.js +0 -378
  141. package/src/server/services/oauth-token-storage.js +0 -135
  142. package/src/server/services/permission-templates-service.js +0 -308
@@ -6,7 +6,24 @@ const express = require('express');
6
6
  const { SkillService } = require('../services/skill-service');
7
7
 
8
8
  const router = express.Router();
9
- const skillService = new SkillService();
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 skillService.listSkills(forceRefresh);
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 skillService.getSkillDetail(directory);
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 skills = skillService.getInstalledSkills();
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 skillService.installSkill(
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 = skillService.createCustomSkill({
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 = skillService.uninstallSkill(directory);
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 repos = skillService.loadRepos();
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 = skillService.addRepo({ owner, name, branch, directory, enabled });
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 = skillService.removeRepo(owner, name, directory);
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 = skillService.toggleRepo(owner, name, directory, enabled);
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 = skillService.createSkillWithFiles({ directory, files });
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 = skillService.getSkillFiles(directory);
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 = skillService.getSkillFileContent(directory, filePath);
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 = skillService.addSkillFiles(directory, files);
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 = skillService.deleteSkillFile(directory, filePath);
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 = skillService.updateSkillFile(directory, filePath, content, isBase64);
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 = skillService.convertSkillFormat(content, targetFormat);
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, 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,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 || 10089;
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 = getEffectiveApiKey(activeChannel);
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
- proxyRes.on('data', (chunk) => {
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
- proxyRes.on('end', () => {
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
- proxyRes.on('error', (err) => {
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 || 10089,
671
+ defaultPort: config.ports?.codexProxy || 20089,
658
672
  startTime,
659
673
  runtime
660
674
  };
@@ -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 { 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 || 10090;
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 = getEffectiveApiKey(activeChannel);
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
- proxyRes.on('data', (chunk) => {
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
- proxyRes.on('end', () => {
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
- proxyRes.on('error', (err) => {
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 || 10090,
570
+ defaultPort: config.ports?.geminiProxy || 20090,
557
571
  startTime,
558
572
  runtime
559
573
  };