coding-tool-x 3.8.9 → 3.8.10

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 (124) hide show
  1. package/README.md +56 -388
  2. package/dist/web/assets/AgentsDrawer-Bam7kBjG.js +1 -0
  3. package/dist/web/assets/Analytics-D5dUWXEa.js +1 -0
  4. package/dist/web/assets/AssetPathField-CLqalYv-.js +1 -0
  5. package/dist/web/assets/{ChatHistoryDrawer-BTmQ1aQr.js → ChatHistoryDrawer-C1At65O3.js} +1 -1
  6. package/dist/web/assets/{CollapseItem-CwqJk3bE.js → CollapseItem-CVWojShK.js} +1 -1
  7. package/dist/web/assets/CommandsDrawer-BEoypGBf.js +1 -0
  8. package/dist/web/assets/ConfigExportDrawer-DBvFDDWK.js +1 -0
  9. package/dist/web/assets/{ConfigTemplates-gyERg9iC.js → ConfigTemplates-BpaWG-iC.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplatesDrawer-B17RqFi4.js → ConfigTemplatesDrawer-C2lvyKMf.js} +1 -1
  11. package/dist/web/assets/ConfigTemplatesPanel-CorOY7kY.js +1 -0
  12. package/dist/web/assets/{Divider-DsdbkHrG.js → Divider-BLLhMTSt.js} +1 -1
  13. package/dist/web/assets/FavoritesDrawer-uW88N6te.js +1 -0
  14. package/dist/web/assets/GatewayConvertDrawer-i5Wt9Dd9.js +1 -0
  15. package/dist/web/assets/Home-BIxWvm29.js +1 -0
  16. package/dist/web/assets/{MarkdownEditor-9TTwTS-U.js → MarkdownEditor-Ul0bSGj-.js} +1 -1
  17. package/dist/web/assets/McpDrawer-Cc2xq77I.js +1 -0
  18. package/dist/web/assets/McpFormDrawer-DLHX_nrI.js +1 -0
  19. package/dist/web/assets/{PluginManager-BuRcWuBj.js → PluginManager-BGQ0xF2F.js} +1 -1
  20. package/dist/web/assets/{PluginsDrawer-_zZENaQn.js → PluginsDrawer-STwHEqiF.js} +1 -1
  21. package/dist/web/assets/PluginsPanel-1zuNP7da.js +1 -0
  22. package/dist/web/assets/ProjectList-_pe2nk9c.js +1 -0
  23. package/dist/web/assets/PromptsDrawer-CJN1xZk7.js +1 -0
  24. package/dist/web/assets/RecentSessionsDrawer-DX1HI8Nm.js +1 -0
  25. package/dist/web/assets/SessionCard-Op6PHvi0.js +1 -0
  26. package/dist/web/assets/SessionList-Da_TAEYr.js +1 -0
  27. package/dist/web/assets/SettingsDrawer-DKLd4Sjy.js +1 -0
  28. package/dist/web/assets/{SkillManager-S698Hns8.js → SkillManager-PcAa5EbF.js} +1 -1
  29. package/dist/web/assets/{SkillsDrawer-Ms4lPCow.js → SkillsDrawer-DPdU4p6C.js} +1 -1
  30. package/dist/web/assets/SkillsPanel-B9qzRqjd.js +1 -0
  31. package/dist/web/assets/Upload-e1M9Yp5z.js +1 -0
  32. package/dist/web/assets/{WorkspaceDrawer-L7HJRc6u.js → WorkspaceDrawer-oS6mB1VZ.js} +1 -1
  33. package/dist/web/assets/{WorkspaceManager-DsoNg2z8.js → WorkspaceManager-6VvRWe3r.js} +1 -1
  34. package/dist/web/assets/WorkspacePanel-CYq0Z8iY.js +1 -0
  35. package/dist/web/assets/client-BgcNg4Bt.js +1 -0
  36. package/dist/web/assets/{headers-B5JWGn3v.js → headers-DbowkmZJ.js} +1 -1
  37. package/dist/web/assets/{icons-DmFe4b5j.js → icons-B-AzZoeE.js} +1 -1
  38. package/dist/web/assets/index-BlUXw84U.js +2 -0
  39. package/dist/web/assets/index-Bn8BUnwP.css +1 -0
  40. package/dist/web/assets/{light-0eavxNvt.js → light-C2X7hegj.js} +1 -1
  41. package/dist/web/assets/platforms-DrzAtnjY.js +1 -0
  42. package/dist/web/assets/platforms-mqqe_82d.js +1 -0
  43. package/dist/web/assets/project-config-BVt5s-pH.js +1 -0
  44. package/dist/web/assets/sessions-53-Fr68H.js +1 -0
  45. package/dist/web/assets/{sessions-D4yQtfKb.js → sessions-CaWUaysM.js} +1 -1
  46. package/dist/web/assets/use-message-DBsCm7FJ.js +1 -0
  47. package/dist/web/assets/{useResponsiveDrawer-V_onl9NV.js → useResponsiveDrawer-ChHZrTfl.js} +1 -1
  48. package/dist/web/assets/{workspaces-mjyPVGS8.js → workspaces-DQuZd4qw.js} +1 -1
  49. package/dist/web/index.html +8 -8
  50. package/package.json +4 -2
  51. package/src/commands/export-config.js +17 -211
  52. package/src/config/model-metadata.json +180 -16
  53. package/src/platforms/channel-auth-service.js +1 -1
  54. package/src/platforms/commands-service.js +28 -0
  55. package/src/platforms/driver-registry.js +1 -0
  56. package/src/platforms/drivers/claude/channels-implementation.js +79 -90
  57. package/src/platforms/drivers/claude/native-config-implementation.js +3 -4
  58. package/src/platforms/drivers/codex/channels-implementation.js +34 -49
  59. package/src/platforms/drivers/codex/native-config-implementation.js +3 -51
  60. package/src/platforms/drivers/dsh/api-operations.js +1 -1
  61. package/src/platforms/drivers/dsh/channels-implementation.js +28 -8
  62. package/src/platforms/drivers/dsh/channels.js +0 -18
  63. package/src/platforms/drivers/dsh/common.js +7 -0
  64. package/src/platforms/drivers/dsh/native-config.js +7 -2
  65. package/src/platforms/drivers/gemini/channels-implementation.js +47 -55
  66. package/src/platforms/drivers/gemini/native-config-implementation.js +8 -20
  67. package/src/platforms/drivers/generic-native-config.js +20 -0
  68. package/src/platforms/drivers/native-config-snapshot.js +30 -4
  69. package/src/platforms/drivers/omp/channels-implementation.js +51 -17
  70. package/src/platforms/drivers/omp/native-config-implementation.js +10 -17
  71. package/src/platforms/drivers/opencode/channels-implementation.js +6 -0
  72. package/src/platforms/drivers/opencode/native-config-implementation.js +3 -67
  73. package/src/platforms/manifest-schema.js +35 -2
  74. package/src/platforms/manifests/claude.json +0 -8
  75. package/src/platforms/manifests/codex.json +0 -8
  76. package/src/platforms/manifests/dsh.json +6 -9
  77. package/src/platforms/manifests/gemini.json +0 -8
  78. package/src/platforms/manifests/omp.json +0 -8
  79. package/src/platforms/manifests/opencode.json +0 -8
  80. package/src/platforms/native-oauth-adapters.js +33 -33
  81. package/src/server/index.js +2 -0
  82. package/src/server/services/bounded-content.js +60 -0
  83. package/src/server/services/channel-balance.js +10 -4
  84. package/src/server/services/config-export-service.js +2 -2
  85. package/src/server/services/config-templates-service.js +5 -1
  86. package/src/server/services/plugins-service.js +162 -85
  87. package/src/server/services/session-history-maintenance-worker.js +71 -0
  88. package/src/server/services/session-history-maintenance.js +166 -0
  89. package/src/server/services/skill-service.js +23 -3
  90. package/src/server/services/speed-test.js +0 -72
  91. package/src/shared/channel-driver.js +16 -0
  92. package/src/shared/driver-factories/api.js +1 -0
  93. package/src/utils/native-config-patcher.js +474 -0
  94. package/dist/web/assets/AgentsDrawer-Cj5CJ4d9.js +0 -1
  95. package/dist/web/assets/Analytics-1doS-8uC.js +0 -1
  96. package/dist/web/assets/AssetPathField-C9TOPl7o.js +0 -1
  97. package/dist/web/assets/CommandsDrawer-CaZWDI_G.js +0 -1
  98. package/dist/web/assets/ConfigExportDrawer-C3vSZEsS.js +0 -1
  99. package/dist/web/assets/ConfigTemplatesPanel-DKIC_FqG.js +0 -1
  100. package/dist/web/assets/FavoritesDrawer-DaziQmBp.js +0 -1
  101. package/dist/web/assets/GatewayConvertDrawer-OMgWUUSc.js +0 -1
  102. package/dist/web/assets/Home-DOSUTfrE.js +0 -1
  103. package/dist/web/assets/McpDrawer-I8rl8lGH.js +0 -1
  104. package/dist/web/assets/McpFormDrawer-DEP3C7ut.js +0 -1
  105. package/dist/web/assets/PluginsPanel-Bs5qZ-TK.js +0 -1
  106. package/dist/web/assets/ProjectList-BSxuL2td.js +0 -1
  107. package/dist/web/assets/PromptsDrawer-CoZ-ZtsY.js +0 -1
  108. package/dist/web/assets/RecentSessionsDrawer-B_17aJBG.js +0 -1
  109. package/dist/web/assets/SessionCard-BL_x0d3t.js +0 -1
  110. package/dist/web/assets/SessionList-DhRYZ6vl.js +0 -1
  111. package/dist/web/assets/SettingsDrawer-fYb0DU7X.js +0 -1
  112. package/dist/web/assets/SkillsPanel-rqnqAOGL.js +0 -1
  113. package/dist/web/assets/SpeedTestDrawer-Bbn38SfV.js +0 -1
  114. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +0 -1
  115. package/dist/web/assets/Upload-CdyVHxCI.js +0 -1
  116. package/dist/web/assets/WorkspacePanel-Be8PVZah.js +0 -1
  117. package/dist/web/assets/client-UDwXBP9i.js +0 -1
  118. package/dist/web/assets/index-3Lp6z8TQ.css +0 -1
  119. package/dist/web/assets/index-BKlAWTH-.js +0 -2
  120. package/dist/web/assets/platforms-BmzzwnSw.js +0 -1
  121. package/dist/web/assets/platforms-DWXgmy2w.js +0 -1
  122. package/dist/web/assets/project-config-B2baPzYL.js +0 -1
  123. package/dist/web/assets/sessions-CC0j-KH_.js +0 -1
  124. package/dist/web/assets/use-message-DqCwefG6.js +0 -1
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { parentPort, workerData } = require('worker_threads');
6
+ const { DatabaseSync } = require('node:sqlite');
7
+
8
+ const MIN_FREE_BYTES = 256 * 1024 * 1024;
9
+ const MIN_FREE_RATIO = 0.2;
10
+
11
+ function readPragmaNumber(db, name) {
12
+ return Number(db.prepare(`PRAGMA ${name}`).get()?.[name]) || 0;
13
+ }
14
+
15
+ function inspectDatabase(dbPath, db) {
16
+ db.exec('PRAGMA busy_timeout = 5000; PRAGMA cache_size = -16384;');
17
+ const pageSize = readPragmaNumber(db, 'page_size');
18
+ const pageCount = readPragmaNumber(db, 'page_count');
19
+ const freePageCount = readPragmaNumber(db, 'freelist_count');
20
+ const fileBytes = fs.statSync(dbPath).size;
21
+ const freeBytes = pageSize * freePageCount;
22
+ const pageBytes = pageSize * pageCount;
23
+
24
+ if (freeBytes < MIN_FREE_BYTES || freeBytes < pageBytes * MIN_FREE_RATIO) {
25
+ return { status: 'below-threshold', beforeBytes: fileBytes, freeBytes };
26
+ }
27
+
28
+ const disk = fs.statfsSync(path.dirname(dbPath));
29
+ const availableBytes = Number(disk.bavail) * Number(disk.bsize);
30
+ if (!Number.isFinite(availableBytes) || availableBytes < fileBytes) {
31
+ return { status: 'insufficient-space', beforeBytes: fileBytes, freeBytes };
32
+ }
33
+
34
+ const checkpoint = db.prepare('PRAGMA wal_checkpoint(TRUNCATE)').get();
35
+ if (Number(checkpoint?.busy) > 0) {
36
+ return { status: 'database-busy', beforeBytes: fileBytes, freeBytes };
37
+ }
38
+
39
+ return { status: 'ready', beforeBytes: fileBytes, freeBytes };
40
+ }
41
+
42
+ function compactDatabase(dbPath) {
43
+ if (!dbPath || !fs.existsSync(dbPath)) return { status: 'missing' };
44
+ const db = new DatabaseSync(dbPath, { timeout: 5000 });
45
+ try {
46
+ const inspection = inspectDatabase(dbPath, db);
47
+ if (inspection.status !== 'ready') return inspection;
48
+
49
+ db.exec('VACUUM;');
50
+ const checkpoint = db.prepare('PRAGMA wal_checkpoint(TRUNCATE)').get();
51
+ const afterBytes = fs.statSync(dbPath).size;
52
+ return {
53
+ status: 'compacted',
54
+ beforeBytes: inspection.beforeBytes,
55
+ afterBytes,
56
+ reclaimedBytes: Math.max(0, inspection.beforeBytes - afterBytes),
57
+ checkpointBusy: Number(checkpoint?.busy) > 0
58
+ };
59
+ } finally {
60
+ db.close();
61
+ }
62
+ }
63
+
64
+ try {
65
+ const result = compactDatabase(workerData?.dbPath);
66
+ parentPort.postMessage({ type: 'done', result });
67
+ parentPort.close();
68
+ } catch (error) {
69
+ parentPort.postMessage({ type: 'error', error: error?.message || String(error) });
70
+ parentPort.close();
71
+ }
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { Worker } = require('worker_threads');
6
+ const { PATHS } = require('../../config/paths');
7
+
8
+ const MAINTENANCE_LOCK_STALE_MS = 30 * 60 * 1000;
9
+ const INITIAL_MAINTENANCE_DELAY_MS = 60 * 1000;
10
+ const RETRY_MAINTENANCE_DELAY_MS = 5 * 60 * 1000;
11
+ const MAINTENANCE_WORKER_PATH = path.join(__dirname, 'session-history-maintenance-worker.js');
12
+
13
+ const scheduledMaintenances = new Map();
14
+
15
+ function isProcessAlive(pid) {
16
+ if (!Number.isInteger(pid) || pid <= 0) return false;
17
+ try {
18
+ process.kill(pid, 0);
19
+ return true;
20
+ } catch (error) {
21
+ return error?.code === 'EPERM';
22
+ }
23
+ }
24
+
25
+ function acquireMaintenanceLock(dbPath) {
26
+ const lockPath = `${path.resolve(dbPath)}.maintenance.lock`;
27
+
28
+ for (let attempt = 0; attempt < 2; attempt += 1) {
29
+ let fd;
30
+ try {
31
+ fd = fs.openSync(lockPath, 'wx', 0o600);
32
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: Date.now() }), 'utf8');
33
+ return { fd, lockPath };
34
+ } catch (error) {
35
+ if (fd !== undefined) {
36
+ try { fs.closeSync(fd); } catch (_) {}
37
+ try { fs.unlinkSync(lockPath); } catch (_) {}
38
+ }
39
+ if (error?.code !== 'EEXIST') throw error;
40
+ }
41
+
42
+ let owner = null;
43
+ let ageMs = 0;
44
+ try {
45
+ owner = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
46
+ ageMs = Math.max(0, Date.now() - fs.statSync(lockPath).mtimeMs);
47
+ } catch (error) {
48
+ if (error?.code === 'ENOENT') continue;
49
+ }
50
+
51
+ const ownerPid = Number(owner?.pid);
52
+ if (isProcessAlive(ownerPid)) return null;
53
+ if ((!Number.isInteger(ownerPid) || ownerPid <= 0) && ageMs < MAINTENANCE_LOCK_STALE_MS) {
54
+ return null;
55
+ }
56
+
57
+ try {
58
+ fs.unlinkSync(lockPath);
59
+ } catch (error) {
60
+ if (error?.code !== 'ENOENT') return null;
61
+ }
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ function releaseMaintenanceLock(lock) {
68
+ if (!lock) return;
69
+ try { fs.closeSync(lock.fd); } catch (_) {}
70
+ try { fs.unlinkSync(lock.lockPath); } catch (_) {}
71
+ }
72
+
73
+ function runMaintenanceWorker(dbPath) {
74
+ return new Promise((resolve, reject) => {
75
+ const worker = new Worker(MAINTENANCE_WORKER_PATH, { workerData: { dbPath } });
76
+ let settled = false;
77
+ worker.unref();
78
+
79
+ const finish = (error, result) => {
80
+ if (settled) return;
81
+ settled = true;
82
+ if (error) reject(error);
83
+ else resolve(result);
84
+ };
85
+
86
+ worker.on('message', (message) => {
87
+ if (message?.type === 'done') {
88
+ finish(null, message.result);
89
+ } else if (message?.type === 'error') {
90
+ finish(new Error(message.error || '会话索引维护线程失败'));
91
+ }
92
+ });
93
+
94
+ worker.once('error', (error) => finish(error));
95
+ worker.once('exit', (code) => {
96
+ if (!settled && code !== 0) {
97
+ finish(new Error(`会话索引维护线程异常退出(代码 ${code})`));
98
+ } else if (!settled) {
99
+ finish(null, { status: 'skipped' });
100
+ }
101
+ });
102
+ });
103
+ }
104
+
105
+ function isRetryableMaintenanceError(error) {
106
+ return /SQLITE_BUSY|database is locked|database table is locked/i.test(error?.message || '');
107
+ }
108
+
109
+ async function runScheduledMaintenance(dbPath) {
110
+ let lock = null;
111
+ try {
112
+ if (!dbPath || !fs.existsSync(dbPath)) return false;
113
+
114
+ lock = acquireMaintenanceLock(dbPath);
115
+ if (!lock) return true;
116
+
117
+ const lockState = require('./session-history-index').cleanupStaleInventoryLocks(dbPath);
118
+ if (lockState.retained.length > 0) return true;
119
+
120
+ const result = await runMaintenanceWorker(dbPath);
121
+ return result.status === 'database-busy';
122
+ } catch (error) {
123
+ return isRetryableMaintenanceError(error);
124
+ } finally {
125
+ releaseMaintenanceLock(lock);
126
+ }
127
+ }
128
+
129
+ function scheduleMaintenanceAttempt(dbPath, delayMs) {
130
+ const key = path.resolve(dbPath);
131
+ const state = scheduledMaintenances.get(key);
132
+ if (!state || state.timer || state.running) return;
133
+
134
+ state.timer = setTimeout(async () => {
135
+ state.timer = null;
136
+ state.running = true;
137
+ let retry = false;
138
+ try {
139
+ retry = await runScheduledMaintenance(key);
140
+ } catch (_) {
141
+ retry = false;
142
+ } finally {
143
+ state.running = false;
144
+ if (retry) {
145
+ scheduleMaintenanceAttempt(key, RETRY_MAINTENANCE_DELAY_MS);
146
+ } else if (scheduledMaintenances.get(key) === state) {
147
+ scheduledMaintenances.delete(key);
148
+ }
149
+ }
150
+ }, delayMs);
151
+ state.timer.unref();
152
+ }
153
+
154
+ function scheduleSessionHistoryMaintenance(dbPath = PATHS.sessionHistoryIndex) {
155
+ if (process.env.NODE_ENV === 'test' || !dbPath) return false;
156
+
157
+ const key = path.resolve(dbPath);
158
+ if (scheduledMaintenances.has(key)) return false;
159
+
160
+ const state = { timer: null, running: false };
161
+ scheduledMaintenances.set(key, state);
162
+ scheduleMaintenanceAttempt(key, INITIAL_MAINTENANCE_DELAY_MS);
163
+ return true;
164
+ }
165
+
166
+ module.exports = { scheduleSessionHistoryMaintenance };
@@ -45,11 +45,14 @@ const { EffectiveControlService } = require('./effective-control-service');
45
45
  const { SkillArtifactStore } = require('./skill-artifact-store');
46
46
  const { SkillFormatAdapter } = require('./skill-format-adapters');
47
47
  const { SkillProjectionService } = require('./skill-projection-service');
48
+ const { readTextFileLimited, truncateUtf8 } = require('./bounded-content');
48
49
 
49
50
  const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
50
51
  const DEFAULT_GITHUB_HOST = 'https://github.com';
51
52
  const DEFAULT_GITLAB_HOST = 'https://gitlab.com';
52
53
  const CACHE_TTL = 5 * 60 * 1000;
54
+ const MAX_SKILL_DETAIL_BYTES = 128 * 1024;
55
+ const MAX_PREPARED_SKILL_CACHE_ENTRIES = 32;
53
56
 
54
57
  function sanitizeRefreshError(value) {
55
58
  return String(value || '')
@@ -392,7 +395,16 @@ class SkillService {
392
395
  invocation: skill.invocation || null,
393
396
  ...(skill.whenToUse ? { whenToUse: skill.whenToUse } : {}),
394
397
  ...(skill.metadata ? { metadata: skill.metadata } : {}),
395
- ...(includeContent && skill.content !== undefined ? { content: skill.content } : {})
398
+ ...(includeContent && skill.content !== undefined
399
+ ? (() => {
400
+ const bounded = truncateUtf8(skill.content, MAX_SKILL_DETAIL_BYTES);
401
+ return {
402
+ content: bounded.text,
403
+ contentBytes: bounded.bytes,
404
+ contentTruncated: bounded.truncated
405
+ };
406
+ })()
407
+ : {})
396
408
  };
397
409
  }
398
410
 
@@ -534,7 +546,11 @@ class SkillService {
534
546
  return value.map(skill => ({ ...skill }));
535
547
  }
536
548
  const entry = { value, cachedAt: Date.now(), generation };
549
+ this._preparedSkillsCache.delete(cacheKey);
537
550
  this._preparedSkillsCache.set(cacheKey, entry);
551
+ while (this._preparedSkillsCache.size > MAX_PREPARED_SKILL_CACHE_ENTRIES) {
552
+ this._preparedSkillsCache.delete(this._preparedSkillsCache.keys().next().value);
553
+ }
538
554
  if (cacheKey === 'user:') {
539
555
  this.skillsCache = value;
540
556
  this.cacheTime = entry.cachedAt;
@@ -3663,7 +3679,8 @@ ${content}
3663
3679
  if (!fs.existsSync(skillFile) || fs.lstatSync(skillFile).isSymbolicLink()) {
3664
3680
  throw new Error('Skill detail contains an unavailable or symlinked SKILL.md');
3665
3681
  }
3666
- const content = fs.readFileSync(skillFile, 'utf8');
3682
+ const boundedContent = readTextFileLimited(skillFile, MAX_SKILL_DETAIL_BYTES);
3683
+ const content = boundedContent.text;
3667
3684
  const parsed = this.parseSkillMd(content);
3668
3685
  const bodyMatch = content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/);
3669
3686
  const body = bodyMatch ? bodyMatch[1].trim() : content;
@@ -3676,6 +3693,8 @@ ${content}
3676
3693
  description: parsed.description || metadata.description || '',
3677
3694
  content: body,
3678
3695
  fullContent: content,
3696
+ contentBytes: boundedContent.bytes,
3697
+ contentTruncated: boundedContent.truncated,
3679
3698
  installed: enabled,
3680
3699
  enabled,
3681
3700
  cached: metadata.cached !== false,
@@ -3881,5 +3900,6 @@ ${content}
3881
3900
  module.exports = {
3882
3901
  SkillService,
3883
3902
  DEFAULT_REPOS: DEFAULT_REPOS_BY_PLATFORM.claude,
3884
- DEFAULT_REPOS_BY_PLATFORM
3903
+ DEFAULT_REPOS_BY_PLATFORM,
3904
+ MAX_SKILL_DETAIL_BYTES
3885
3905
  };
@@ -38,47 +38,6 @@ function sanitizeTimeout(timeout) {
38
38
  return Math.min(Math.max(ms, MIN_TIMEOUT), MAX_TIMEOUT);
39
39
  }
40
40
 
41
- /**
42
- * 规范化批量测速并发度(默认小并发)
43
- */
44
- function sanitizeBatchConcurrency(concurrency, defaultValue = 2) {
45
- const value = Number(concurrency);
46
- if (!Number.isFinite(value) || value <= 0) {
47
- return defaultValue;
48
- }
49
- return Math.min(Math.max(Math.round(value), 1), 5);
50
- }
51
-
52
- /**
53
- * 按并发限制执行异步任务,保持结果顺序与输入一致
54
- */
55
- async function runWithConcurrencyLimit(items, concurrency, taskFn) {
56
- const list = Array.isArray(items) ? items : [];
57
- if (list.length === 0) return [];
58
-
59
- const limit = sanitizeBatchConcurrency(concurrency);
60
- const results = new Array(list.length);
61
- let cursor = 0;
62
-
63
- async function worker() {
64
- while (true) {
65
- const currentIndex = cursor;
66
- cursor += 1;
67
- if (currentIndex >= list.length) {
68
- return;
69
- }
70
- results[currentIndex] = await taskFn(list[currentIndex], currentIndex);
71
- }
72
- }
73
-
74
- const workers = [];
75
- const workerCount = Math.min(limit, list.length);
76
- for (let i = 0; i < workerCount; i += 1) {
77
- workers.push(worker());
78
- }
79
- await Promise.all(workers);
80
- return results;
81
- }
82
41
 
83
42
  function normalizeNonEmptyString(value) {
84
43
  if (typeof value !== 'string') return null;
@@ -768,34 +727,6 @@ async function testAPIFunctionality(baseUrl, apiKey, timeout, channelType = 'cla
768
727
  return primaryResult;
769
728
  }
770
729
 
771
- /**
772
- * 批量测试多个渠道
773
- * @param {Array} channels - 渠道列表
774
- * @param {number} timeout - 超时时间
775
- * @param {string} channelType - 渠道类型:'claude' | 'codex' | 'gemini'
776
- * @returns {Promise<Array>} 测试结果列表
777
- */
778
- async function testMultipleChannels(channels, timeout = DEFAULT_TIMEOUT, channelType = 'claude', concurrency = 2) {
779
- const results = await runWithConcurrencyLimit(
780
- channels,
781
- concurrency,
782
- channel => testChannelSpeed(channel, timeout, channelType)
783
- );
784
-
785
- // 按延迟排序(成功的在前,按延迟升序)
786
- results.sort((a, b) => {
787
- if (a.success && !b.success) return -1;
788
- if (!a.success && b.success) return 1;
789
- if (a.success && b.success) {
790
- const aLatency = (a.latency === null || a.latency === undefined) ? Infinity : a.latency;
791
- const bLatency = (b.latency === null || b.latency === undefined) ? Infinity : b.latency;
792
- return aLatency - bLatency;
793
- }
794
- return 0;
795
- });
796
-
797
- return results;
798
- }
799
730
 
800
731
  /**
801
732
  * 获取缓存的测试结果
@@ -834,10 +765,7 @@ function getLatencyLevel(latency) {
834
765
 
835
766
  module.exports = {
836
767
  testChannelSpeed,
837
- testMultipleChannels,
838
768
  getCachedResult,
839
769
  clearCache,
840
770
  getLatencyLevel,
841
- sanitizeBatchConcurrency,
842
- runWithConcurrencyLimit
843
771
  };
@@ -202,6 +202,22 @@ function createChannelDriver({
202
202
  return failed(platform, capability, operation, error);
203
203
  }
204
204
  };
205
+ driver.speedTest = async (request = {}) => {
206
+ const operation = 'speedTest';
207
+ const channelId = request?.params?.channelId || request?.channelId;
208
+ const body = request?.body && typeof request.body === 'object' ? request.body : request;
209
+ if (!channelId) return invalid(platform, capability, operation, new Error('Channel id is required'));
210
+ try {
211
+ const channel = await findChannelById(channelId);
212
+ if (!channel) return invalid(platform, capability, operation, new Error(`Channel not found: ${channelId}`));
213
+ const { testChannelSpeed } = require('../server/services/speed-test');
214
+ const channelType = resolveModelListType(platform, channel, modelListType);
215
+ const data = await testChannelSpeed(channel, body?.timeout, channelType, { authSourceType: platform });
216
+ return ok(platform, capability, operation, data);
217
+ } catch (error) {
218
+ return failed(platform, capability, operation, error);
219
+ }
220
+ };
205
221
  driver.list = (...args) => call('list', listMethod, args, sanitizeChannels);
206
222
  driver.getEnabled = (...args) => call('getEnabled', 'getEnabledChannels', args, channels => sanitizeChannels(channels).channels);
207
223
  driver.create = (input, ...rest) => {
@@ -71,6 +71,7 @@ function argsFor(capability, operation, context) {
71
71
  if (operation === 'update') return [params.channelId, context.body || {}];
72
72
  if (operation === 'remove') return [params.channelId];
73
73
  if (operation === 'models') return [params.channelId, options];
74
+ if (['speedTest', 'getAuth', 'syncLocalAuth', 'getAuthQuota'].includes(operation)) return [context];
74
75
  if (operation === 'applyToSettings') return [params.channelId];
75
76
  return [context.body || options];
76
77
  }