coding-tool-x 3.8.8 → 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.
- package/README.md +56 -388
- package/dist/web/assets/AgentsDrawer-Bam7kBjG.js +1 -0
- package/dist/web/assets/Analytics-D5dUWXEa.js +1 -0
- package/dist/web/assets/AssetPathField-CLqalYv-.js +1 -0
- package/dist/web/assets/{ChatHistoryDrawer-CrfS0KYV.js → ChatHistoryDrawer-C1At65O3.js} +1 -1
- package/dist/web/assets/{CollapseItem-ClMsJuUu.js → CollapseItem-CVWojShK.js} +1 -1
- package/dist/web/assets/CommandsDrawer-BEoypGBf.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-DBvFDDWK.js +1 -0
- package/dist/web/assets/ConfigTemplates-BpaWG-iC.js +1 -0
- package/dist/web/assets/{ConfigTemplatesDrawer-BqM32Im7.js → ConfigTemplatesDrawer-C2lvyKMf.js} +1 -1
- package/dist/web/assets/ConfigTemplatesPanel-CorOY7kY.js +1 -0
- package/dist/web/assets/{Divider-DsdbkHrG.js → Divider-BLLhMTSt.js} +1 -1
- package/dist/web/assets/FavoritesDrawer-uW88N6te.js +1 -0
- package/dist/web/assets/GatewayConvertDrawer-i5Wt9Dd9.js +1 -0
- package/dist/web/assets/Home-BIxWvm29.js +1 -0
- package/dist/web/assets/{MarkdownEditor-BMMUufSc.js → MarkdownEditor-Ul0bSGj-.js} +1 -1
- package/dist/web/assets/McpDrawer-Cc2xq77I.js +1 -0
- package/dist/web/assets/McpFormDrawer-DLHX_nrI.js +1 -0
- package/dist/web/assets/{PluginManager-C4kJlpVX.js → PluginManager-BGQ0xF2F.js} +1 -1
- package/dist/web/assets/{PluginsDrawer-05R4k15x.js → PluginsDrawer-STwHEqiF.js} +1 -1
- package/dist/web/assets/PluginsPanel-1zuNP7da.js +1 -0
- package/dist/web/assets/ProjectList-VbIYSHbH.css +1 -0
- package/dist/web/assets/ProjectList-_pe2nk9c.js +1 -0
- package/dist/web/assets/PromptsDrawer-CJN1xZk7.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-DX1HI8Nm.js +1 -0
- package/dist/web/assets/SessionCard-Op6PHvi0.js +1 -0
- package/dist/web/assets/SessionList-Da_TAEYr.js +1 -0
- package/dist/web/assets/SettingsDrawer-DKLd4Sjy.js +1 -0
- package/dist/web/assets/{SkillManager-D9jkNuRM.js → SkillManager-PcAa5EbF.js} +1 -1
- package/dist/web/assets/{SkillsDrawer-0Z4IefxC.js → SkillsDrawer-DPdU4p6C.js} +1 -1
- package/dist/web/assets/SkillsPanel-B9qzRqjd.js +1 -0
- package/dist/web/assets/Upload-e1M9Yp5z.js +1 -0
- package/dist/web/assets/{WorkspaceDrawer-eujtWpoq.js → WorkspaceDrawer-oS6mB1VZ.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-CQ-8Ej68.js → WorkspaceManager-6VvRWe3r.js} +1 -1
- package/dist/web/assets/WorkspacePanel-CYq0Z8iY.js +1 -0
- package/dist/web/assets/client-BgcNg4Bt.js +1 -0
- package/dist/web/assets/headers-DbowkmZJ.js +1 -0
- package/dist/web/assets/{icons-DmFe4b5j.js → icons-B-AzZoeE.js} +1 -1
- package/dist/web/assets/index-BlUXw84U.js +2 -0
- package/dist/web/assets/index-Bn8BUnwP.css +1 -0
- package/dist/web/assets/{light-0eavxNvt.js → light-C2X7hegj.js} +1 -1
- package/dist/web/assets/platforms-DrzAtnjY.js +1 -0
- package/dist/web/assets/platforms-mqqe_82d.js +1 -0
- package/dist/web/assets/project-config-BVt5s-pH.js +1 -0
- package/dist/web/assets/sessions-53-Fr68H.js +1 -0
- package/dist/web/assets/{sessions-D4yQtfKb.js → sessions-CaWUaysM.js} +1 -1
- package/dist/web/assets/use-message-DBsCm7FJ.js +1 -0
- package/dist/web/assets/{useResponsiveDrawer-DZd5rNk5.js → useResponsiveDrawer-ChHZrTfl.js} +1 -1
- package/dist/web/assets/{workspaces-mjyPVGS8.js → workspaces-DQuZd4qw.js} +1 -1
- package/dist/web/index.html +8 -8
- package/package.json +4 -2
- package/src/commands/daemon.js +27 -4
- package/src/commands/export-config.js +17 -211
- package/src/config/model-metadata.json +180 -16
- package/src/platforms/channel-auth-registry.js +36 -6
- package/src/platforms/channel-auth-service.js +13 -3
- package/src/platforms/commands-service.js +28 -0
- package/src/platforms/driver-registry.js +1 -0
- package/src/platforms/drivers/claude/channels-implementation.js +79 -90
- package/src/platforms/drivers/claude/native-config-implementation.js +3 -4
- package/src/platforms/drivers/codex/channels-implementation.js +34 -49
- package/src/platforms/drivers/codex/native-config-implementation.js +3 -51
- package/src/platforms/drivers/dsh/api-operations.js +1 -1
- package/src/platforms/drivers/dsh/channels-implementation.js +28 -8
- package/src/platforms/drivers/dsh/channels.js +0 -18
- package/src/platforms/drivers/dsh/common.js +7 -0
- package/src/platforms/drivers/dsh/native-config.js +7 -2
- package/src/platforms/drivers/gemini/channels-implementation.js +47 -55
- package/src/platforms/drivers/gemini/native-config-implementation.js +8 -20
- package/src/platforms/drivers/generic-native-config.js +20 -0
- package/src/platforms/drivers/native-config-snapshot.js +30 -4
- package/src/platforms/drivers/omp/channels-implementation.js +51 -17
- package/src/platforms/drivers/omp/native-config-implementation.js +10 -17
- package/src/platforms/drivers/omp/prompts.js +58 -2
- package/src/platforms/drivers/omp/proxy-implementation.js +25 -1
- package/src/platforms/drivers/opencode/channels-implementation.js +31 -22
- package/src/platforms/drivers/opencode/channels.js +6 -1
- package/src/platforms/drivers/opencode/native-config-implementation.js +3 -67
- package/src/platforms/manifest-schema.js +35 -2
- package/src/platforms/manifests/claude.json +0 -8
- package/src/platforms/manifests/codex.json +0 -8
- package/src/platforms/manifests/dsh.json +6 -9
- package/src/platforms/manifests/gemini.json +0 -8
- package/src/platforms/manifests/omp.json +2 -9
- package/src/platforms/manifests/opencode.json +28 -8
- package/src/platforms/native-oauth-adapters.js +351 -51
- package/src/platforms/oauth-credentials-service.js +344 -13
- package/src/server/index.js +37 -1
- package/src/server/services/bounded-content.js +60 -0
- package/src/server/services/channel-balance.js +10 -4
- package/src/server/services/config-export-service.js +2 -2
- package/src/server/services/config-templates-service.js +5 -1
- package/src/server/services/plugins-service.js +162 -85
- package/src/server/services/session-history-maintenance-worker.js +71 -0
- package/src/server/services/session-history-maintenance.js +166 -0
- package/src/server/services/skill-service.js +23 -3
- package/src/server/services/speed-test.js +0 -72
- package/src/shared/channel-driver.js +16 -0
- package/src/shared/driver-factories/api.js +1 -0
- package/src/utils/native-config-patcher.js +474 -0
- package/dist/web/assets/AgentsDrawer-DRQuwi0m.js +0 -1
- package/dist/web/assets/Analytics-Cf5KwEdj.js +0 -1
- package/dist/web/assets/AssetPathField-DyniM0pF.js +0 -1
- package/dist/web/assets/CommandsDrawer-CzHelf56.js +0 -1
- package/dist/web/assets/ConfigExportDrawer-BEZ8wTby.js +0 -1
- package/dist/web/assets/ConfigTemplates-Crg3xWe5.js +0 -1
- package/dist/web/assets/ConfigTemplatesPanel-lTdutC8H.js +0 -1
- package/dist/web/assets/FavoritesDrawer-DDyWUBJR.js +0 -1
- package/dist/web/assets/GatewayConvertDrawer-Rigtt_5J.js +0 -1
- package/dist/web/assets/Home-tCSKmSHV.js +0 -1
- package/dist/web/assets/McpDrawer-CmE-d7VW.js +0 -1
- package/dist/web/assets/McpFormDrawer-ByvS18od.js +0 -1
- package/dist/web/assets/PluginsPanel-BCoEs7Og.js +0 -1
- package/dist/web/assets/ProjectList-BJt1OIaI.js +0 -1
- package/dist/web/assets/ProjectList-DG3yu8xZ.css +0 -1
- package/dist/web/assets/PromptsDrawer-DnwBf_oW.js +0 -1
- package/dist/web/assets/RecentSessionsDrawer-B7d-38wH.js +0 -1
- package/dist/web/assets/SessionCard-BUgjHQwX.js +0 -1
- package/dist/web/assets/SessionList-B-bXZRFk.js +0 -1
- package/dist/web/assets/SettingsDrawer-BITzjQLl.js +0 -1
- package/dist/web/assets/SkillsPanel-BKIVOj7n.js +0 -1
- package/dist/web/assets/SpeedTestDrawer-Bx1bYf5i.js +0 -1
- package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +0 -1
- package/dist/web/assets/Upload-CzC4rGI4.js +0 -1
- package/dist/web/assets/WorkspacePanel-Cql5qE2R.js +0 -1
- package/dist/web/assets/client-UDwXBP9i.js +0 -1
- package/dist/web/assets/headers-BqDmkMjy.js +0 -1
- package/dist/web/assets/index-Cmmwj3Fx.js +0 -2
- package/dist/web/assets/index-Cnbe696C.css +0 -1
- package/dist/web/assets/platforms-BmzzwnSw.js +0 -1
- package/dist/web/assets/platforms-DWXgmy2w.js +0 -1
- package/dist/web/assets/project-config-B2baPzYL.js +0 -1
- package/dist/web/assets/sessions-DqlbocLJ.js +0 -1
- package/dist/web/assets/use-message-DqCwefG6.js +0 -1
|
@@ -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
|
|
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
|
|
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
|
}
|