coding-tool-x 3.4.4 → 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 (26) hide show
  1. package/dist/web/assets/{Analytics-_Byi9M6y.js → Analytics-DFWyPf5C.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-DIwosdtG.js → ConfigTemplates-BFE7hmKd.js} +1 -1
  3. package/dist/web/assets/{Home-DdNMuQ9c.js → Home-DZUuCrxk.js} +1 -1
  4. package/dist/web/assets/{PluginManager-iuY24cnW.js → PluginManager-WyGY2BQN.js} +1 -1
  5. package/dist/web/assets/{ProjectList-DSkMulzL.js → ProjectList-CBc0QawN.js} +1 -1
  6. package/dist/web/assets/{SessionList-B6pGquIr.js → SessionList-CdPR7QLq.js} +1 -1
  7. package/dist/web/assets/{SkillManager-CHtQX5r8.js → SkillManager-B5-DxQOS.js} +1 -1
  8. package/dist/web/assets/{WorkspaceManager-gNPs-VaI.js → WorkspaceManager-C7yqFjpi.js} +1 -1
  9. package/dist/web/assets/index-BDsmoSfO.js +2 -0
  10. package/dist/web/assets/{index-pMqqe9ei.css → index-C1pzEgmj.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/plugins.js +161 -14
  15. package/src/server/api/skills.js +62 -7
  16. package/src/server/codex-proxy-server.js +10 -2
  17. package/src/server/gemini-proxy-server.js +10 -2
  18. package/src/server/opencode-proxy-server.js +10 -2
  19. package/src/server/proxy-server.js +10 -2
  20. package/src/server/services/codex-channels.js +64 -21
  21. package/src/server/services/codex-env-manager.js +44 -28
  22. package/src/server/services/plugins-service.js +1060 -235
  23. package/src/server/services/proxy-runtime.js +129 -5
  24. package/src/server/services/server-shutdown.js +79 -0
  25. package/src/server/services/skill-service.js +142 -17
  26. 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-BDsmoSfO.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-C1pzEgmj.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.5",
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);
@@ -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,
@@ -22,6 +22,7 @@ const { persistProxyRequestSnapshot, loadClaudeRequestTemplate } = require('./se
22
22
  const { probeModelAvailability, fetchModelsFromProvider } = require('./services/model-detector');
23
23
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
24
24
  const { redirectModel, resolveTargetUrl } = require('./services/base/proxy-utils');
25
+ const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
25
26
 
26
27
  let proxyServer = null;
27
28
  let proxyApp = null;
@@ -4650,6 +4651,7 @@ async function startOpenCodeProxyServer(options = {}) {
4650
4651
 
4651
4652
  // 启动服务器
4652
4653
  proxyServer = http.createServer(proxyApp);
4654
+ attachServerShutdownHandling(proxyServer);
4653
4655
 
4654
4656
  return new Promise((resolve, reject) => {
4655
4657
  proxyServer.listen(port, '127.0.0.1', () => {
@@ -4692,8 +4694,13 @@ async function stopOpenCodeProxyServer(options = {}) {
4692
4694
 
4693
4695
  requestMetadata.clear();
4694
4696
 
4697
+ const shutdownTimer = expediteServerShutdown(proxyServer);
4698
+
4695
4699
  return new Promise((resolve) => {
4696
4700
  proxyServer.close(() => {
4701
+ if (shutdownTimer) {
4702
+ clearTimeout(shutdownTimer);
4703
+ }
4697
4704
  console.log('OpenCode proxy server stopped');
4698
4705
 
4699
4706
  // 清除代理启动时间(仅当明确要求时)
@@ -4713,8 +4720,9 @@ async function stopOpenCodeProxyServer(options = {}) {
4713
4720
  // 获取代理服务器状态
4714
4721
  function getOpenCodeProxyStatus() {
4715
4722
  const config = loadConfig();
4716
- const startTime = getProxyStartTime('opencode');
4717
- const runtime = getProxyRuntime('opencode');
4723
+ const allowRecovery = !!proxyServer;
4724
+ const startTime = getProxyStartTime('opencode', { allowRecovery });
4725
+ const runtime = getProxyRuntime('opencode', { allowRecovery });
4718
4726
 
4719
4727
  return {
4720
4728
  running: !!proxyServer,
@@ -20,6 +20,7 @@ const { getEffectiveApiKey } = require('./services/channels');
20
20
  const { persistProxyRequestSnapshot, persistClaudeRequestTemplate } = require('./services/request-logger');
21
21
  const { publishUsageLog, publishFailureLog } = require('./services/proxy-log-helper');
22
22
  const { redirectModel } = require('./services/base/proxy-utils');
23
+ const { attachServerShutdownHandling, expediteServerShutdown } = require('./services/server-shutdown');
23
24
 
24
25
  let proxyServer = null;
25
26
  let proxyApp = null;
@@ -541,6 +542,7 @@ async function startProxyServer(options = {}) {
541
542
  });
542
543
 
543
544
  proxyServer = http.createServer(proxyApp);
545
+ attachServerShutdownHandling(proxyServer);
544
546
 
545
547
  return new Promise((resolve, reject) => {
546
548
  proxyServer.listen(port, '127.0.0.1', () => {
@@ -580,8 +582,13 @@ async function stopProxyServer(options = {}) {
580
582
 
581
583
  requestMetadata.clear();
582
584
 
585
+ const shutdownTimer = expediteServerShutdown(proxyServer);
586
+
583
587
  return new Promise((resolve) => {
584
588
  proxyServer.close(() => {
589
+ if (shutdownTimer) {
590
+ clearTimeout(shutdownTimer);
591
+ }
585
592
  console.log('[OK] Proxy server stopped');
586
593
  if (clearStartTime) {
587
594
  clearProxyStartTime('claude');
@@ -599,8 +606,9 @@ async function stopProxyServer(options = {}) {
599
606
  // 获取代理服务器状态
600
607
  function getProxyStatus() {
601
608
  const config = loadConfig();
602
- const startTime = getProxyStartTime('claude');
603
- const runtime = getProxyRuntime('claude');
609
+ const allowRecovery = !!proxyServer;
610
+ const startTime = getProxyStartTime('claude', { allowRecovery });
611
+ const runtime = getProxyRuntime('claude', { allowRecovery });
604
612
 
605
613
  return {
606
614
  running: !!proxyServer,