coding-tool-x 3.4.3 → 3.4.5

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 (63) hide show
  1. package/dist/web/assets/{Analytics-CbGxotgz.js → Analytics-DFWyPf5C.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-oP6nrFEb.js → ConfigTemplates-BFE7hmKd.js} +1 -1
  3. package/dist/web/assets/{Home-DMntmEvh.js → Home-DZUuCrxk.js} +1 -1
  4. package/dist/web/assets/{PluginManager-BUC_c7nH.js → PluginManager-WyGY2BQN.js} +1 -1
  5. package/dist/web/assets/{ProjectList-CW8J49n7.js → ProjectList-CBc0QawN.js} +1 -1
  6. package/dist/web/assets/{ProjectList-oJIyIRkP.css → ProjectList-DL4JK6ci.css} +1 -1
  7. package/dist/web/assets/{SessionList-7lYnF92v.js → SessionList-CdPR7QLq.js} +1 -1
  8. package/dist/web/assets/{SkillManager-Cs08216i.js → SkillManager-B5-DxQOS.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-CY-oGtyB.js → WorkspaceManager-C7yqFjpi.js} +1 -1
  10. package/dist/web/assets/index-BDsmoSfO.js +2 -0
  11. package/dist/web/assets/{index-5qy5NMIP.css → index-C1pzEgmj.css} +1 -1
  12. package/dist/web/index.html +2 -2
  13. package/package.json +2 -2
  14. package/src/commands/channels.js +13 -13
  15. package/src/commands/cli-type.js +5 -5
  16. package/src/commands/daemon.js +31 -31
  17. package/src/commands/doctor.js +14 -14
  18. package/src/commands/export-config.js +23 -23
  19. package/src/commands/list.js +4 -4
  20. package/src/commands/logs.js +19 -19
  21. package/src/commands/plugin.js +62 -62
  22. package/src/commands/port-config.js +4 -4
  23. package/src/commands/proxy-control.js +35 -35
  24. package/src/commands/proxy.js +28 -28
  25. package/src/commands/resume.js +4 -4
  26. package/src/commands/search.js +9 -9
  27. package/src/commands/security.js +5 -5
  28. package/src/commands/stats.js +18 -18
  29. package/src/commands/switch.js +1 -1
  30. package/src/commands/toggle-proxy.js +18 -18
  31. package/src/commands/ui.js +11 -11
  32. package/src/commands/update.js +9 -9
  33. package/src/commands/workspace.js +11 -11
  34. package/src/index.js +24 -24
  35. package/src/plugins/plugin-installer.js +1 -1
  36. package/src/reset-config.js +9 -9
  37. package/src/server/api/channels.js +1 -1
  38. package/src/server/api/claude-hooks.js +3 -2
  39. package/src/server/api/plugins.js +165 -14
  40. package/src/server/api/pm2-autostart.js +2 -2
  41. package/src/server/api/proxy.js +6 -6
  42. package/src/server/api/skills.js +66 -7
  43. package/src/server/codex-proxy-server.js +10 -2
  44. package/src/server/dev-server.js +2 -2
  45. package/src/server/gemini-proxy-server.js +10 -2
  46. package/src/server/index.js +37 -37
  47. package/src/server/opencode-proxy-server.js +10 -2
  48. package/src/server/proxy-server.js +14 -6
  49. package/src/server/services/codex-channels.js +64 -21
  50. package/src/server/services/codex-env-manager.js +44 -28
  51. package/src/server/services/config-export-service.js +1 -1
  52. package/src/server/services/mcp-service.js +2 -1
  53. package/src/server/services/model-detector.js +2 -2
  54. package/src/server/services/native-keychain.js +1 -0
  55. package/src/server/services/plugins-service.js +1066 -261
  56. package/src/server/services/proxy-runtime.js +129 -5
  57. package/src/server/services/server-shutdown.js +79 -0
  58. package/src/server/services/settings-manager.js +3 -3
  59. package/src/server/services/skill-service.js +146 -29
  60. package/src/server/websocket-server.js +8 -8
  61. package/src/ui/menu.js +2 -2
  62. package/src/ui/prompts.js +5 -5
  63. package/dist/web/assets/index-ClCqKpvX.js +0 -2
@@ -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
@@ -58,7 +94,11 @@ router.get('/market', async (req, res) => {
58
94
  try {
59
95
  const { platform, service } = getPluginsService(req);
60
96
  const forceRefresh = req.query.refresh === '1';
97
+ if (forceRefresh) {
98
+ console.log(`[Plugins API] Refreshing market plugins for ${platform}...`);
99
+ }
61
100
  const plugins = await service.getMarketPlugins(forceRefresh);
101
+ console.log(`[Plugins API] ${platform}: ${plugins.length} market plugins loaded (refresh=${forceRefresh})`);
62
102
 
63
103
  res.json({
64
104
  success: true,
@@ -89,7 +129,7 @@ router.post('/install', async (req, res) => {
89
129
  if (source) {
90
130
  installUrl = source;
91
131
  } else if (directory && repo) {
92
- installUrl = `https://github.com/${repo.owner}/${repo.name}/tree/${repo.branch || 'main'}/${directory}`;
132
+ installUrl = '';
93
133
  } else if (gitUrl) {
94
134
  installUrl = gitUrl;
95
135
  } else {
@@ -99,7 +139,15 @@ router.post('/install', async (req, res) => {
99
139
  });
100
140
  }
101
141
 
102
- 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
+ );
103
151
 
104
152
  if (!result.success) {
105
153
  return res.status(400).json({
@@ -136,7 +184,7 @@ router.get('/repos', (req, res) => {
136
184
  res.json({
137
185
  success: true,
138
186
  platform,
139
- repos
187
+ repos: sanitizeRepos(service, repos)
140
188
  });
141
189
  } catch (err) {
142
190
  console.error('[Plugins API] Get repos error:', err);
@@ -155,12 +203,13 @@ router.get('/repos', (req, res) => {
155
203
  router.post('/repos', (req, res) => {
156
204
  try {
157
205
  const { platform, service } = getPluginsService(req);
158
- const repo = req.body;
206
+ const repo = extractRepoPayload(req.body);
207
+ repo.enabled = req.body.enabled !== false;
159
208
 
160
- if (!repo || !repo.url) {
209
+ if (!repo.localPath && !repo.projectPath && (!repo.owner || !repo.name) && !repo.repoUrl) {
161
210
  return res.status(400).json({
162
211
  success: false,
163
- message: 'Repository URL is required'
212
+ message: 'Missing repo info'
164
213
  });
165
214
  }
166
215
 
@@ -169,7 +218,7 @@ router.post('/repos', (req, res) => {
169
218
  res.json({
170
219
  success: true,
171
220
  platform,
172
- repos,
221
+ repos: sanitizeRepos(service, repos),
173
222
  message: 'Repository added successfully'
174
223
  });
175
224
  } catch (err) {
@@ -181,6 +230,54 @@ router.post('/repos', (req, res) => {
181
230
  }
182
231
  });
183
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
+
184
281
  /**
185
282
  * 删除插件仓库
186
283
  * DELETE /api/plugins/repos/:owner/:name
@@ -189,13 +286,14 @@ router.delete('/repos/:owner/:name', (req, res) => {
189
286
  try {
190
287
  const { platform, service } = getPluginsService(req);
191
288
  const { owner, name } = req.params;
289
+ const { id = '' } = req.query;
192
290
 
193
- const repos = service.removeRepo(owner, name);
291
+ const repos = service.removeRepo(owner, name, id);
194
292
 
195
293
  res.json({
196
294
  success: true,
197
295
  platform,
198
- repos,
296
+ repos: sanitizeRepos(service, repos),
199
297
  message: 'Repository removed successfully'
200
298
  });
201
299
  } catch (err) {
@@ -216,7 +314,7 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
216
314
  try {
217
315
  const { platform, service } = getPluginsService(req);
218
316
  const { owner, name } = req.params;
219
- const { enabled } = req.body;
317
+ const { enabled, id = '' } = req.body;
220
318
 
221
319
  if (typeof enabled !== 'boolean') {
222
320
  return res.status(400).json({
@@ -225,12 +323,12 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
225
323
  });
226
324
  }
227
325
 
228
- const repos = service.toggleRepo(owner, name, enabled);
326
+ const repos = service.toggleRepo(owner, name, enabled, id);
229
327
 
230
328
  res.json({
231
329
  success: true,
232
330
  platform,
233
- repos,
331
+ repos: sanitizeRepos(service, repos),
234
332
  message: `Repository ${enabled ? 'enabled' : 'disabled'} successfully`
235
333
  });
236
334
  } catch (err) {
@@ -242,6 +340,40 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
242
340
  }
243
341
  });
244
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
+
245
377
  /**
246
378
  * 同步仓库到 Claude Code marketplace
247
379
  * POST /api/plugins/repos/sync
@@ -299,16 +431,35 @@ router.get('/:name/readme', async (req, res) => {
299
431
  try {
300
432
  const { platform, service } = getPluginsService(req);
301
433
  const { name } = req.params;
302
- 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;
303
448
 
304
449
  const pluginInfo = {
305
450
  name,
451
+ repoId,
452
+ repoProvider,
453
+ repoHost,
306
454
  repoOwner,
307
455
  repoName,
308
456
  repoBranch,
309
457
  directory,
310
458
  source,
311
- repoUrl
459
+ repoUrl,
460
+ repoProjectPath,
461
+ repoLocalPath,
462
+ installPath
312
463
  };
313
464
 
314
465
  const readme = await service.getPluginReadme(pluginInfo);
@@ -10,9 +10,9 @@ const execAsync = promisify(exec);
10
10
 
11
11
  function getExecOptions(timeout = 30000, runtimePlatform = process.platform) {
12
12
  if (runtimePlatform === 'win32') {
13
- return { timeout };
13
+ return { timeout, windowsHide: true };
14
14
  }
15
- return { shell: '/bin/bash', timeout };
15
+ return { shell: '/bin/bash', timeout, windowsHide: true };
16
16
  }
17
17
 
18
18
  /**
@@ -208,7 +208,7 @@ router.post('/start', async (req, res) => {
208
208
 
209
209
  // 3. 保存当前激活渠道ID(用于代理模式)
210
210
  saveActiveChannelId(currentChannel.id);
211
- console.log(`✅ Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
211
+ console.log(`[OK] Saved active channel: ${currentChannel.name} (${currentChannel.id})`);
212
212
 
213
213
  // 4. 启动代理服务器
214
214
  const proxyResult = await startProxyServer();
@@ -256,11 +256,11 @@ router.post('/stop', async (req, res) => {
256
256
  if (restoredChannel) {
257
257
  if (hadBackup) {
258
258
  deleteBackup();
259
- console.log(' Discarded backup snapshot');
259
+ console.log('[OK] Discarded backup snapshot');
260
260
  }
261
261
  } else if (hadBackup) {
262
262
  restoreSettings();
263
- console.log(' Restored settings from backup');
263
+ console.log('[OK] Restored settings from backup');
264
264
  const channels = getAllChannels();
265
265
  const currentSettings = require('../services/channels').getCurrentSettings();
266
266
  if (currentSettings) {
@@ -274,7 +274,7 @@ router.post('/stop', async (req, res) => {
274
274
  if (restoredChannel) {
275
275
  const { applyChannelToSettings } = require('../services/channels');
276
276
  applyChannelToSettings(restoredChannel.id);
277
- console.log(`✅ Single-channel mode restored: ${restoredChannel.name}`);
277
+ console.log(`[OK] Single-channel mode restored: ${restoredChannel.name}`);
278
278
  }
279
279
 
280
280
  // 3. 删除备份文件和active-channel.json
@@ -282,14 +282,14 @@ router.post('/stop', async (req, res) => {
282
282
  const backupPath = NATIVE_PATHS.claude.settingsBackup;
283
283
  if (fs.existsSync(backupPath)) {
284
284
  fs.unlinkSync(backupPath);
285
- console.log(' Removed backup file');
285
+ console.log('[OK] Removed backup file');
286
286
  }
287
287
  }
288
288
 
289
289
  const activeChannelPath = PATHS.activeChannel.claude;
290
290
  if (fs.existsSync(activeChannelPath)) {
291
291
  fs.unlinkSync(activeChannelPath);
292
- console.log(' Removed active-channel.json');
292
+ console.log('[OK] Removed active-channel.json');
293
293
  }
294
294
 
295
295
  // 4. 通过 WebSocket 推送代理状态更新
@@ -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
@@ -50,7 +70,11 @@ router.get('/', async (req, res) => {
50
70
  try {
51
71
  const { platform, service } = getSkillService(req);
52
72
  const forceRefresh = req.query.refresh === '1';
73
+ if (forceRefresh) {
74
+ console.log(`[Skills API] Refreshing skills for ${platform}...`);
75
+ }
53
76
  const skills = await service.listSkills(forceRefresh);
77
+ console.log(`[Skills API] ${platform}: ${skills.length} skills loaded (refresh=${forceRefresh})`);
54
78
  res.json({
55
79
  success: true,
56
80
  platform,
@@ -286,7 +310,7 @@ router.get('/repos', (req, res) => {
286
310
  res.json({
287
311
  success: true,
288
312
  platform,
289
- repos
313
+ repos: sanitizeRepos(service, repos)
290
314
  });
291
315
  } catch (err) {
292
316
  console.error('[Skills API] Get repos error:', err);
@@ -321,7 +345,7 @@ router.post('/repos', (req, res) => {
321
345
  res.json({
322
346
  success: true,
323
347
  platform,
324
- repos
348
+ repos: sanitizeRepos(service, repos)
325
349
  });
326
350
  } catch (err) {
327
351
  console.error('[Skills API] Add repo error:', err);
@@ -341,7 +365,7 @@ router.delete('/repos', (req, res) => {
341
365
  res.json({
342
366
  success: true,
343
367
  platform,
344
- repos
368
+ repos: sanitizeRepos(service, repos)
345
369
  });
346
370
  } catch (err) {
347
371
  console.error('[Skills API] Remove repo error:', err);
@@ -362,7 +386,7 @@ router.put('/repos/toggle', (req, res) => {
362
386
  res.json({
363
387
  success: true,
364
388
  platform,
365
- repos
389
+ repos: sanitizeRepos(service, repos)
366
390
  });
367
391
  } catch (err) {
368
392
  console.error('[Skills API] Toggle repo error:', err);
@@ -373,6 +397,41 @@ router.put('/repos/toggle', (req, res) => {
373
397
  }
374
398
  });
375
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
+
376
435
  /**
377
436
  * 删除仓库
378
437
  * DELETE /api/skills/repos/:owner/:name
@@ -388,7 +447,7 @@ router.delete('/repos/:owner/:name', (req, res) => {
388
447
  res.json({
389
448
  success: true,
390
449
  platform,
391
- repos
450
+ repos: sanitizeRepos(service, repos)
392
451
  });
393
452
  } catch (err) {
394
453
  console.error('[Skills API] Remove repo error:', err);
@@ -416,7 +475,7 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
416
475
  res.json({
417
476
  success: true,
418
477
  platform,
419
- repos
478
+ repos: sanitizeRepos(service, repos)
420
479
  });
421
480
  } catch (err) {
422
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,
@@ -13,12 +13,12 @@ const chalk = require('chalk');
13
13
  const config = loadConfig();
14
14
  const port = config.ports?.webUI || 19999;
15
15
 
16
- console.log(chalk.cyan('\n🔧 开发模式:启动后端 API 服务器...\n'));
16
+ console.log(chalk.cyan('\n[FIX] 开发模式:启动后端 API 服务器...\n'));
17
17
 
18
18
  (async () => {
19
19
  await startServer(port);
20
20
 
21
- console.log(chalk.yellow('💡 开发提示:'));
21
+ console.log(chalk.yellow('[TIP] 开发提示:'));
22
22
  console.log(chalk.gray(` - 后端 API: http://localhost:${port}/api`));
23
23
  console.log(chalk.gray(' - 前端开发服务器: http://localhost:5000'));
24
24
  console.log(chalk.gray(' - 修改后端代码会自动重启 (nodemon)'));
@@ -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,