coding-tool-x 3.4.4 → 3.4.6

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 (32) hide show
  1. package/dist/web/assets/{Analytics-_Byi9M6y.js → Analytics-0PgPv5qO.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-DIwosdtG.js → ConfigTemplates-pBGoYbCP.js} +1 -1
  3. package/dist/web/assets/{Home-DdNMuQ9c.js → Home-BRN882om.js} +1 -1
  4. package/dist/web/assets/{PluginManager-iuY24cnW.js → PluginManager-am97Huts.js} +1 -1
  5. package/dist/web/assets/{ProjectList-DSkMulzL.js → ProjectList-CXS9KJN1.js} +1 -1
  6. package/dist/web/assets/{SessionList-B6pGquIr.js → SessionList-BZyrzH7J.js} +1 -1
  7. package/dist/web/assets/{SkillManager-CHtQX5r8.js → SkillManager-p1CI0tYa.js} +1 -1
  8. package/dist/web/assets/{WorkspaceManager-gNPs-VaI.js → WorkspaceManager-CUPvLoba.js} +1 -1
  9. package/dist/web/assets/index-B4Wl3JfR.js +2 -0
  10. package/dist/web/assets/{index-pMqqe9ei.css → index-Bgt_oqoE.css} +1 -1
  11. package/dist/web/index.html +2 -2
  12. package/package.json +2 -2
  13. package/src/server/api/claude-hooks.js +1 -0
  14. package/src/server/api/codex-channels.js +26 -0
  15. package/src/server/api/oauth-credentials.js +23 -1
  16. package/src/server/api/opencode-proxy.js +0 -2
  17. package/src/server/api/plugins.js +161 -14
  18. package/src/server/api/skills.js +62 -7
  19. package/src/server/codex-proxy-server.js +10 -2
  20. package/src/server/gemini-proxy-server.js +10 -2
  21. package/src/server/opencode-proxy-server.js +10 -2
  22. package/src/server/proxy-server.js +10 -2
  23. package/src/server/services/codex-channels.js +64 -21
  24. package/src/server/services/codex-env-manager.js +44 -28
  25. package/src/server/services/native-oauth-adapters.js +94 -10
  26. package/src/server/services/oauth-credentials-service.js +44 -2
  27. package/src/server/services/opencode-channels.js +0 -2
  28. package/src/server/services/plugins-service.js +1060 -235
  29. package/src/server/services/proxy-runtime.js +129 -5
  30. package/src/server/services/server-shutdown.js +79 -0
  31. package/src/server/services/skill-service.js +142 -17
  32. package/dist/web/assets/index-DGjGCo37.js +0 -2
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>CC-TOOL - ClaudeCode增强工作助手</title>
8
- <script type="module" crossorigin src="/assets/index-DGjGCo37.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-B4Wl3JfR.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-DyTJGI4N.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-3bf-fPGP.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/vendors-CKPV1OAU.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-Bdxp09n2.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/icons-B5Pl4lrD.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-pMqqe9ei.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-Bgt_oqoE.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.4.4",
3
+ "version": "3.4.6",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "start": "node bin/ctx.js",
11
11
  "test": "npm run test:basic && npm run test:api && npm run test:codex-agents && npm run test:skills && npm run test:plugins-market",
12
- "test:basic": "node scripts/test-basic.js",
12
+ "test:basic": "node scripts/test-basic.js && npm run test:unit",
13
13
  "test:api": "node scripts/test-api-consistency.js",
14
14
  "test:codex-agents": "node scripts/test-codex-agents.js",
15
15
  "test:skills": "node scripts/test-skill-providers.js",
@@ -543,6 +543,7 @@ router.post('/test', (req, res) => {
543
543
  const command = generateSystemNotificationCommand(type || 'notification');
544
544
  const { execSync } = require('child_process');
545
545
  execSync(command, { stdio: 'ignore', windowsHide: true });
546
+ res.json({ success: true, message: '系统测试通知已发送' });
546
547
  }
547
548
  } catch (error) {
548
549
  console.error('Error testing notification:', error);
@@ -25,11 +25,26 @@ const { deleteBackup } = require('../services/codex-settings-manager');
25
25
  const { PATHS } = require('../../config/paths');
26
26
  const { getDefaultSpeedTestModelByToolType } = require('../../config/model-metadata');
27
27
  const CODEX_GATEWAY_SOURCE_TYPE = 'codex';
28
+ const CODEX_PROVIDER_KEY_PATTERN = /^[a-z0-9_-]+$/i;
28
29
 
29
30
  function getDefaultCodexModel() {
30
31
  return getDefaultSpeedTestModelByToolType('codex');
31
32
  }
32
33
 
34
+ function validateCodexProviderKey(value) {
35
+ const normalized = String(value || '').trim();
36
+ if (!normalized) {
37
+ return 'Missing required fields: providerKey';
38
+ }
39
+ if (!CODEX_PROVIDER_KEY_PATTERN.test(normalized)) {
40
+ return 'Invalid providerKey: only letters, numbers, underscores, and hyphens are allowed';
41
+ }
42
+ if (normalized.toLowerCase() === 'openai') {
43
+ return 'Invalid providerKey: "openai" is reserved for the built-in OpenAI provider';
44
+ }
45
+ return '';
46
+ }
47
+
33
48
  module.exports = (config) => {
34
49
  /**
35
50
  * GET /api/codex/channels
@@ -137,6 +152,11 @@ module.exports = (config) => {
137
152
  return res.status(400).json({ error: 'Missing required fields: apiKey' });
138
153
  }
139
154
 
155
+ const providerKeyError = validateCodexProviderKey(providerKey);
156
+ if (providerKeyError) {
157
+ return res.status(400).json({ error: providerKeyError });
158
+ }
159
+
140
160
  // wireApi 固定为 'responses' (OpenAI Responses API 格式)
141
161
  const channel = createChannel(name, providerKey, baseUrl, apiKey, 'responses', {
142
162
  websiteUrl,
@@ -168,6 +188,12 @@ module.exports = (config) => {
168
188
 
169
189
  const { channelId } = req.params;
170
190
  const updates = req.body;
191
+ if (Object.prototype.hasOwnProperty.call(updates, 'providerKey')) {
192
+ const providerKeyError = validateCodexProviderKey(updates.providerKey);
193
+ if (providerKeyError) {
194
+ return res.status(400).json({ error: providerKeyError });
195
+ }
196
+ }
171
197
 
172
198
  const channel = updateChannel(channelId, updates);
173
199
  // 清除该渠道的模型重定向日志缓存,使下次请求时重新打印
@@ -9,6 +9,7 @@ const {
9
9
  setDefaultCredential,
10
10
  deleteCredential,
11
11
  applyStoredCredential,
12
+ disableStoredCredential,
12
13
  clearNativeOAuthState,
13
14
  fetchCredentialUsage
14
15
  } = require('../services/oauth-credentials-service');
@@ -117,10 +118,31 @@ router.post('/:tool/:credentialId/apply', async (req, res) => {
117
118
  assertTool(tool);
118
119
  const result = await applyStoredCredential(tool, credentialId);
119
120
  broadcastToolProxyState(tool);
121
+ const message = tool === 'opencode'
122
+ ? 'opencode 已应用 OAuth 凭证,并保留现有 API providers'
123
+ : `${tool} 已切换到 OAuth 凭证控制`;
120
124
  res.json({
121
125
  tool,
122
126
  ...result,
123
- message: `${tool} 已切换到 OAuth 凭证控制`
127
+ message
128
+ });
129
+ } catch (error) {
130
+ res.status(error.statusCode || 500).json({ error: error.message });
131
+ }
132
+ });
133
+
134
+ router.post('/:tool/:credentialId/disable-native', (req, res) => {
135
+ try {
136
+ const { tool, credentialId } = req.params;
137
+ assertTool(tool);
138
+ const result = disableStoredCredential(tool, credentialId);
139
+ broadcastToolProxyState(tool);
140
+ res.json({
141
+ tool,
142
+ ...result,
143
+ message: tool === 'opencode'
144
+ ? 'opencode OAuth provider 已关闭'
145
+ : `${tool} 本机 OAuth 已关闭`
124
146
  });
125
147
  } catch (error) {
126
148
  res.status(error.statusCode || 500).json({ error: error.message });
@@ -16,7 +16,6 @@ const {
16
16
  getCurrentProxyPort
17
17
  } = require('../services/opencode-settings-manager');
18
18
  const { getChannels, getEnabledChannels, applyChannelToSettings } = require('../services/opencode-channels');
19
- const { clearNativeOAuth } = require('../services/native-oauth-adapters');
20
19
  const { getSchedulerState } = require('../services/channel-scheduler');
21
20
  const { PATHS, ensureStorageDirMigrated } = require('../../config/paths');
22
21
  const fs = require('fs');
@@ -195,7 +194,6 @@ router.post('/start', async (req, res) => {
195
194
  });
196
195
 
197
196
  const activeModel = currentChannel.model || currentChannel.speedTestModel || null;
198
- clearNativeOAuth('opencode');
199
197
  setProxyConfig(proxyResult.port, { channels: channelPayloads, model: activeModel });
200
198
 
201
199
  // 5. 广播状态更新
@@ -6,6 +6,7 @@
6
6
 
7
7
  const express = require('express');
8
8
  const { PluginsService } = require('../services/plugins-service');
9
+ const { maskToken } = require('../services/oauth-utils');
9
10
 
10
11
  const router = express.Router();
11
12
  const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
@@ -27,6 +28,41 @@ function getPluginsService(req) {
27
28
  return { platform, service: pluginServices.get(platform) };
28
29
  }
29
30
 
31
+ function extractRepoPayload(source = {}) {
32
+ const repo = source.repo && typeof source.repo === 'object' ? source.repo : source;
33
+ return {
34
+ id: repo.id || source.repoId || '',
35
+ provider: repo.provider || source.provider || '',
36
+ host: repo.host || source.host || '',
37
+ owner: repo.owner || source.owner || '',
38
+ name: repo.name || source.name || '',
39
+ branch: repo.branch || source.branch || 'main',
40
+ directory: repo.directory || source.directory || '',
41
+ projectPath: repo.projectPath || source.projectPath || '',
42
+ localPath: repo.localPath || source.localPath || '',
43
+ repoUrl: repo.repoUrl || repo.url || source.repoUrl || source.url || '',
44
+ token: repo.token || source.token || ''
45
+ };
46
+ }
47
+
48
+ function sanitizeRepo(repo = {}) {
49
+ const token = String(repo.token || '').trim();
50
+ const sanitized = {
51
+ ...repo,
52
+ hasToken: Boolean(token),
53
+ tokenPreview: token ? maskToken(token) : ''
54
+ };
55
+ delete sanitized.token;
56
+ return sanitized;
57
+ }
58
+
59
+ function sanitizeRepos(service, repos = []) {
60
+ if (typeof service.getReposForClient === 'function') {
61
+ return service.getReposForClient(repos);
62
+ }
63
+ return (Array.isArray(repos) ? repos : []).map(sanitizeRepo);
64
+ }
65
+
30
66
  /**
31
67
  * 获取插件列表
32
68
  * GET /api/plugins
@@ -93,7 +129,7 @@ router.post('/install', async (req, res) => {
93
129
  if (source) {
94
130
  installUrl = source;
95
131
  } else if (directory && repo) {
96
- installUrl = `https://github.com/${repo.owner}/${repo.name}/tree/${repo.branch || 'main'}/${directory}`;
132
+ installUrl = '';
97
133
  } else if (gitUrl) {
98
134
  installUrl = gitUrl;
99
135
  } else {
@@ -103,7 +139,15 @@ router.post('/install', async (req, res) => {
103
139
  });
104
140
  }
105
141
 
106
- const result = await service.installPlugin(installUrl);
142
+ const result = await service.installPlugin(
143
+ installUrl,
144
+ directory && repo
145
+ ? {
146
+ ...extractRepoPayload({ repo }),
147
+ directory
148
+ }
149
+ : null
150
+ );
107
151
 
108
152
  if (!result.success) {
109
153
  return res.status(400).json({
@@ -140,7 +184,7 @@ router.get('/repos', (req, res) => {
140
184
  res.json({
141
185
  success: true,
142
186
  platform,
143
- repos
187
+ repos: sanitizeRepos(service, repos)
144
188
  });
145
189
  } catch (err) {
146
190
  console.error('[Plugins API] Get repos error:', err);
@@ -159,12 +203,13 @@ router.get('/repos', (req, res) => {
159
203
  router.post('/repos', (req, res) => {
160
204
  try {
161
205
  const { platform, service } = getPluginsService(req);
162
- const repo = req.body;
206
+ const repo = extractRepoPayload(req.body);
207
+ repo.enabled = req.body.enabled !== false;
163
208
 
164
- if (!repo || !repo.url) {
209
+ if (!repo.localPath && !repo.projectPath && (!repo.owner || !repo.name) && !repo.repoUrl) {
165
210
  return res.status(400).json({
166
211
  success: false,
167
- message: 'Repository URL is required'
212
+ message: 'Missing repo info'
168
213
  });
169
214
  }
170
215
 
@@ -173,7 +218,7 @@ router.post('/repos', (req, res) => {
173
218
  res.json({
174
219
  success: true,
175
220
  platform,
176
- repos,
221
+ repos: sanitizeRepos(service, repos),
177
222
  message: 'Repository added successfully'
178
223
  });
179
224
  } catch (err) {
@@ -185,6 +230,54 @@ router.post('/repos', (req, res) => {
185
230
  }
186
231
  });
187
232
 
233
+ router.delete('/repos', (req, res) => {
234
+ try {
235
+ const { platform, service } = getPluginsService(req);
236
+ const { id = '', owner = '', name = '' } = req.query;
237
+ const repos = service.removeRepo(owner, name, id);
238
+
239
+ res.json({
240
+ success: true,
241
+ platform,
242
+ repos: sanitizeRepos(service, repos)
243
+ });
244
+ } catch (err) {
245
+ console.error('[Plugins API] Remove repo error:', err);
246
+ res.status(500).json({
247
+ success: false,
248
+ message: err.message
249
+ });
250
+ }
251
+ });
252
+
253
+ router.put('/repos/toggle', (req, res) => {
254
+ try {
255
+ const { platform, service } = getPluginsService(req);
256
+ const { id = '', owner = '', name = '', enabled } = req.body;
257
+
258
+ if (typeof enabled !== 'boolean') {
259
+ return res.status(400).json({
260
+ success: false,
261
+ message: 'enabled must be a boolean'
262
+ });
263
+ }
264
+
265
+ const repos = service.toggleRepo(owner, name, enabled, id);
266
+
267
+ res.json({
268
+ success: true,
269
+ platform,
270
+ repos: sanitizeRepos(service, repos)
271
+ });
272
+ } catch (err) {
273
+ console.error('[Plugins API] Toggle repo error:', err);
274
+ res.status(500).json({
275
+ success: false,
276
+ message: err.message
277
+ });
278
+ }
279
+ });
280
+
188
281
  /**
189
282
  * 删除插件仓库
190
283
  * DELETE /api/plugins/repos/:owner/:name
@@ -193,13 +286,14 @@ router.delete('/repos/:owner/:name', (req, res) => {
193
286
  try {
194
287
  const { platform, service } = getPluginsService(req);
195
288
  const { owner, name } = req.params;
289
+ const { id = '' } = req.query;
196
290
 
197
- const repos = service.removeRepo(owner, name);
291
+ const repos = service.removeRepo(owner, name, id);
198
292
 
199
293
  res.json({
200
294
  success: true,
201
295
  platform,
202
- repos,
296
+ repos: sanitizeRepos(service, repos),
203
297
  message: 'Repository removed successfully'
204
298
  });
205
299
  } catch (err) {
@@ -220,7 +314,7 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
220
314
  try {
221
315
  const { platform, service } = getPluginsService(req);
222
316
  const { owner, name } = req.params;
223
- const { enabled } = req.body;
317
+ const { enabled, id = '' } = req.body;
224
318
 
225
319
  if (typeof enabled !== 'boolean') {
226
320
  return res.status(400).json({
@@ -229,12 +323,12 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
229
323
  });
230
324
  }
231
325
 
232
- const repos = service.toggleRepo(owner, name, enabled);
326
+ const repos = service.toggleRepo(owner, name, enabled, id);
233
327
 
234
328
  res.json({
235
329
  success: true,
236
330
  platform,
237
- repos,
331
+ repos: sanitizeRepos(service, repos),
238
332
  message: `Repository ${enabled ? 'enabled' : 'disabled'} successfully`
239
333
  });
240
334
  } catch (err) {
@@ -246,6 +340,40 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
246
340
  }
247
341
  });
248
342
 
343
+ router.put('/repos/auth', (req, res) => {
344
+ try {
345
+ const { platform, service } = getPluginsService(req);
346
+ const {
347
+ id = '',
348
+ owner = '',
349
+ name = '',
350
+ token = '',
351
+ clearToken = false
352
+ } = req.body;
353
+
354
+ if (!clearToken && !String(token || '').trim()) {
355
+ return res.status(400).json({
356
+ success: false,
357
+ message: 'Missing token'
358
+ });
359
+ }
360
+
361
+ const repos = service.updateRepoAuth(owner, name, token, clearToken, id);
362
+
363
+ res.json({
364
+ success: true,
365
+ platform,
366
+ repos: sanitizeRepos(service, repos)
367
+ });
368
+ } catch (err) {
369
+ console.error('[Plugins API] Update repo auth error:', err);
370
+ res.status(500).json({
371
+ success: false,
372
+ message: err.message
373
+ });
374
+ }
375
+ });
376
+
249
377
  /**
250
378
  * 同步仓库到 Claude Code marketplace
251
379
  * POST /api/plugins/repos/sync
@@ -303,16 +431,35 @@ router.get('/:name/readme', async (req, res) => {
303
431
  try {
304
432
  const { platform, service } = getPluginsService(req);
305
433
  const { name } = req.params;
306
- const { repoOwner, repoName, repoBranch, directory, source, repoUrl } = req.query;
434
+ const {
435
+ repoId,
436
+ repoProvider,
437
+ repoHost,
438
+ repoOwner,
439
+ repoName,
440
+ repoBranch,
441
+ directory,
442
+ source,
443
+ repoUrl,
444
+ repoProjectPath,
445
+ repoLocalPath,
446
+ installPath
447
+ } = req.query;
307
448
 
308
449
  const pluginInfo = {
309
450
  name,
451
+ repoId,
452
+ repoProvider,
453
+ repoHost,
310
454
  repoOwner,
311
455
  repoName,
312
456
  repoBranch,
313
457
  directory,
314
458
  source,
315
- repoUrl
459
+ repoUrl,
460
+ repoProjectPath,
461
+ repoLocalPath,
462
+ installPath
316
463
  };
317
464
 
318
465
  const readme = await service.getPluginReadme(pluginInfo);
@@ -4,6 +4,7 @@
4
4
 
5
5
  const express = require('express');
6
6
  const { SkillService } = require('../services/skill-service');
7
+ const { maskToken } = require('../services/oauth-utils');
7
8
 
8
9
  const router = express.Router();
9
10
  const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
@@ -37,10 +38,29 @@ function extractRepoPayload(source = {}) {
37
38
  directory: repo.directory || source.directory || '',
38
39
  projectPath: repo.projectPath || source.projectPath || '',
39
40
  localPath: repo.localPath || source.localPath || '',
40
- repoUrl: repo.repoUrl || source.repoUrl || ''
41
+ repoUrl: repo.repoUrl || source.repoUrl || '',
42
+ token: repo.token || source.token || ''
41
43
  };
42
44
  }
43
45
 
46
+ function sanitizeRepo(repo = {}) {
47
+ const token = String(repo.token || '').trim();
48
+ const sanitized = {
49
+ ...repo,
50
+ hasToken: Boolean(token),
51
+ tokenPreview: token ? maskToken(token) : ''
52
+ };
53
+ delete sanitized.token;
54
+ return sanitized;
55
+ }
56
+
57
+ function sanitizeRepos(service, repos = []) {
58
+ if (typeof service.getReposForClient === 'function') {
59
+ return service.getReposForClient(repos);
60
+ }
61
+ return (Array.isArray(repos) ? repos : []).map(sanitizeRepo);
62
+ }
63
+
44
64
  /**
45
65
  * 获取技能列表
46
66
  * GET /api/skills
@@ -290,7 +310,7 @@ router.get('/repos', (req, res) => {
290
310
  res.json({
291
311
  success: true,
292
312
  platform,
293
- repos
313
+ repos: sanitizeRepos(service, repos)
294
314
  });
295
315
  } catch (err) {
296
316
  console.error('[Skills API] Get repos error:', err);
@@ -325,7 +345,7 @@ router.post('/repos', (req, res) => {
325
345
  res.json({
326
346
  success: true,
327
347
  platform,
328
- repos
348
+ repos: sanitizeRepos(service, repos)
329
349
  });
330
350
  } catch (err) {
331
351
  console.error('[Skills API] Add repo error:', err);
@@ -345,7 +365,7 @@ router.delete('/repos', (req, res) => {
345
365
  res.json({
346
366
  success: true,
347
367
  platform,
348
- repos
368
+ repos: sanitizeRepos(service, repos)
349
369
  });
350
370
  } catch (err) {
351
371
  console.error('[Skills API] Remove repo error:', err);
@@ -366,7 +386,7 @@ router.put('/repos/toggle', (req, res) => {
366
386
  res.json({
367
387
  success: true,
368
388
  platform,
369
- repos
389
+ repos: sanitizeRepos(service, repos)
370
390
  });
371
391
  } catch (err) {
372
392
  console.error('[Skills API] Toggle repo error:', err);
@@ -377,6 +397,41 @@ router.put('/repos/toggle', (req, res) => {
377
397
  }
378
398
  });
379
399
 
400
+ router.put('/repos/auth', (req, res) => {
401
+ try {
402
+ const { platform, service } = getSkillService(req);
403
+ const {
404
+ id = '',
405
+ owner = '',
406
+ name = '',
407
+ directory = '',
408
+ token = '',
409
+ clearToken = false
410
+ } = req.body;
411
+
412
+ if (!clearToken && !String(token || '').trim()) {
413
+ return res.status(400).json({
414
+ success: false,
415
+ message: 'Missing token'
416
+ });
417
+ }
418
+
419
+ const repos = service.updateRepoAuth(owner, name, directory, token, clearToken, id);
420
+
421
+ res.json({
422
+ success: true,
423
+ platform,
424
+ repos: sanitizeRepos(service, repos)
425
+ });
426
+ } catch (err) {
427
+ console.error('[Skills API] Update repo auth error:', err);
428
+ res.status(500).json({
429
+ success: false,
430
+ message: err.message
431
+ });
432
+ }
433
+ });
434
+
380
435
  /**
381
436
  * 删除仓库
382
437
  * DELETE /api/skills/repos/:owner/:name
@@ -392,7 +447,7 @@ router.delete('/repos/:owner/:name', (req, res) => {
392
447
  res.json({
393
448
  success: true,
394
449
  platform,
395
- repos
450
+ repos: sanitizeRepos(service, repos)
396
451
  });
397
452
  } catch (err) {
398
453
  console.error('[Skills API] Remove repo error:', err);
@@ -420,7 +475,7 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
420
475
  res.json({
421
476
  success: true,
422
477
  platform,
423
- repos
478
+ repos: sanitizeRepos(service, repos)
424
479
  });
425
480
  } catch (err) {
426
481
  console.error('[Skills API] Toggle repo error:', err);
@@ -15,6 +15,7 @@ const { getEffectiveApiKey } = require('./services/codex-channels');
15
15
  const { persistProxyRequestSnapshot } = require('./services/request-logger');
16
16
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
17
17
  const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
18
+ const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
18
19
 
19
20
  let proxyServer = null;
20
21
  let proxyApp = null;
@@ -519,6 +520,7 @@ async function startCodexProxyServer(options = {}) {
519
520
 
520
521
  // 启动服务器
521
522
  proxyServer = http.createServer(proxyApp);
523
+ attachServerShutdownHandling(proxyServer);
522
524
 
523
525
  return new Promise((resolve, reject) => {
524
526
  proxyServer.listen(port, '127.0.0.1', () => {
@@ -559,8 +561,13 @@ async function stopCodexProxyServer(options = {}) {
559
561
 
560
562
  requestMetadata.clear();
561
563
 
564
+ const shutdownTimer = expediteServerShutdown(proxyServer);
565
+
562
566
  return new Promise((resolve) => {
563
567
  proxyServer.close(() => {
568
+ if (shutdownTimer) {
569
+ clearTimeout(shutdownTimer);
570
+ }
564
571
  console.log('Codex proxy server stopped');
565
572
 
566
573
  // 清除代理启动时间(仅当明确要求时)
@@ -580,8 +587,9 @@ async function stopCodexProxyServer(options = {}) {
580
587
  // 获取代理服务器状态
581
588
  function getCodexProxyStatus() {
582
589
  const config = loadConfig();
583
- const startTime = getProxyStartTime('codex');
584
- const runtime = getProxyRuntime('codex');
590
+ const allowRecovery = !!proxyServer;
591
+ const startTime = getProxyStartTime('codex', { allowRecovery });
592
+ const runtime = getProxyRuntime('codex', { allowRecovery });
585
593
 
586
594
  return {
587
595
  running: !!proxyServer,
@@ -15,6 +15,7 @@ const { getEffectiveApiKey } = require('./services/gemini-channels');
15
15
  const { persistProxyRequestSnapshot } = require('./services/request-logger');
16
16
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
17
17
  const { redirectModel: redirectModelBase, resolveTargetUrl } = require('./services/base/proxy-utils');
18
+ const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
18
19
 
19
20
  let proxyServer = null;
20
21
  let proxyApp = null;
@@ -512,6 +513,7 @@ async function startGeminiProxyServer(options = {}) {
512
513
 
513
514
  // 启动服务器
514
515
  proxyServer = http.createServer(proxyApp);
516
+ attachServerShutdownHandling(proxyServer);
515
517
 
516
518
  return new Promise((resolve, reject) => {
517
519
  proxyServer.listen(port, '127.0.0.1', () => {
@@ -552,8 +554,13 @@ async function stopGeminiProxyServer(options = {}) {
552
554
 
553
555
  requestMetadata.clear();
554
556
 
557
+ const shutdownTimer = expediteServerShutdown(proxyServer);
558
+
555
559
  return new Promise((resolve) => {
556
560
  proxyServer.close(() => {
561
+ if (shutdownTimer) {
562
+ clearTimeout(shutdownTimer);
563
+ }
557
564
  console.log('Gemini proxy server stopped');
558
565
 
559
566
  // 清除代理启动时间(仅当明确要求时)
@@ -573,8 +580,9 @@ async function stopGeminiProxyServer(options = {}) {
573
580
  // 获取代理服务器状态
574
581
  function getGeminiProxyStatus() {
575
582
  const config = loadConfig();
576
- const startTime = getProxyStartTime('gemini');
577
- const runtime = getProxyRuntime('gemini');
583
+ const allowRecovery = !!proxyServer;
584
+ const startTime = getProxyStartTime('gemini', { allowRecovery });
585
+ const runtime = getProxyRuntime('gemini', { allowRecovery });
578
586
 
579
587
  return {
580
588
  running: !!proxyServer,