@pikecode/api-key-manager 1.0.39 → 1.0.42
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 +396 -2
- package/bin/akm.js +120 -3
- package/package.json +12 -5
- package/src/CommandRegistry.js +25 -0
- package/src/commands/BaseCommand.js +67 -0
- package/src/commands/add.js +125 -152
- package/src/commands/backup.js +14 -1
- package/src/commands/batch.js +314 -0
- package/src/commands/benchmark.js +344 -0
- package/src/commands/current.js +30 -14
- package/src/commands/edit.js +66 -46
- package/src/commands/health.js +222 -0
- package/src/commands/list.js +30 -11
- package/src/commands/remove.js +22 -3
- package/src/commands/stats.js +282 -0
- package/src/commands/switch/launch-args-helper.js +84 -0
- package/src/commands/switch/status-helper.js +124 -0
- package/src/commands/switch.js +224 -163
- package/src/commands/validate.js +310 -0
- package/src/config.js +243 -2
- package/src/constants/index.js +246 -0
- package/src/index.js +10 -3
- package/src/utils/codex-files.js +33 -22
- package/src/utils/config-opener.js +15 -0
- package/src/utils/env-utils.js +1 -1
- package/src/utils/error-handler.js +26 -26
- package/src/utils/health-checker.js +350 -0
- package/src/utils/inquirer-setup.js +11 -0
- package/src/utils/provider-status-checker.js +29 -10
- package/src/utils/ui-helper.js +87 -85
- package/src/utils/update-checker.js +25 -7
- package/src/utils/validator.js +12 -12
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider Validation Command
|
|
3
|
+
* 验证供应商配置的有效性
|
|
4
|
+
* @module commands/validate
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { configManager } = require('../config');
|
|
9
|
+
const { Logger } = require('../utils/logger');
|
|
10
|
+
const { ProviderStatusChecker } = require('../utils/provider-status-checker');
|
|
11
|
+
const { UIHelper } = require('../utils/ui-helper');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 供应商验证器
|
|
15
|
+
* 用于验证 API 供应商的 Token 和配置是否有效
|
|
16
|
+
*/
|
|
17
|
+
class ProviderValidator {
|
|
18
|
+
/**
|
|
19
|
+
* 创建供应商验证器实例
|
|
20
|
+
*/
|
|
21
|
+
constructor() {
|
|
22
|
+
this.configManager = configManager;
|
|
23
|
+
this.statusChecker = new ProviderStatusChecker();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 验证单个供应商
|
|
28
|
+
* @param {string} providerName - 供应商名称或别名
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
async validateOne(providerName) {
|
|
32
|
+
try {
|
|
33
|
+
await this.configManager.ensureLoaded();
|
|
34
|
+
|
|
35
|
+
// 支持别名查找
|
|
36
|
+
let provider = this.configManager.getProvider(providerName);
|
|
37
|
+
if (!provider) {
|
|
38
|
+
provider = this.configManager.getProviderByNameOrAlias(providerName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!provider) {
|
|
42
|
+
Logger.error(`供应商 '${providerName}' 不存在`);
|
|
43
|
+
Logger.info('使用 "akm list" 查看所有已配置的供应商');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(chalk.blue(`\n🔍 正在验证供应商: ${provider.displayName} (${provider.name})`));
|
|
48
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
49
|
+
|
|
50
|
+
const loadingInterval = UIHelper.createLoadingAnimation('验证中...');
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const status = await this.statusChecker.check(provider, { skipCache: true });
|
|
54
|
+
UIHelper.clearLoadingAnimation(loadingInterval);
|
|
55
|
+
|
|
56
|
+
this._printValidationResult(provider, status);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
UIHelper.clearLoadingAnimation(loadingInterval);
|
|
59
|
+
Logger.error(`验证失败: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
} catch (error) {
|
|
63
|
+
Logger.error(`验证供应商失败: ${error.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 验证所有供应商
|
|
69
|
+
* @param {string|null} filter - 过滤器 ('codex', 'claude', 或 null 表示全部)
|
|
70
|
+
* @returns {Promise<void>}
|
|
71
|
+
*/
|
|
72
|
+
async validateAll(filter = null) {
|
|
73
|
+
try {
|
|
74
|
+
await this.configManager.ensureLoaded();
|
|
75
|
+
let providers = this.configManager.listProviders();
|
|
76
|
+
|
|
77
|
+
// 应用过滤器
|
|
78
|
+
if (filter === 'codex') {
|
|
79
|
+
providers = providers.filter(p => p.ideName === 'codex');
|
|
80
|
+
} else if (filter === 'claude') {
|
|
81
|
+
providers = providers.filter(p => p.ideName !== 'codex');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (providers.length === 0) {
|
|
85
|
+
if (filter) {
|
|
86
|
+
const filterName = filter === 'codex' ? 'Codex CLI' : 'Claude Code';
|
|
87
|
+
Logger.warning(`暂无 ${filterName} 供应商配置`);
|
|
88
|
+
} else {
|
|
89
|
+
Logger.warning('暂无配置的供应商');
|
|
90
|
+
}
|
|
91
|
+
Logger.info('请使用 "akm add" 添加供应商配置');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const titleSuffix = filter === 'codex' ? ' (Codex CLI)' : (filter === 'claude' ? ' (Claude Code)' : '');
|
|
96
|
+
console.log(chalk.blue(`\n🔍 正在验证供应商配置${titleSuffix}...`));
|
|
97
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
98
|
+
console.log();
|
|
99
|
+
|
|
100
|
+
let completedCount = 0;
|
|
101
|
+
const total = providers.length;
|
|
102
|
+
|
|
103
|
+
// 创建进度显示
|
|
104
|
+
const progressInterval = setInterval(() => {
|
|
105
|
+
process.stdout.write(`\r验证中... ${completedCount}/${total}`);
|
|
106
|
+
}, 100);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// 使用流式检查,实时显示结果
|
|
110
|
+
const results = await this.statusChecker.checkAllStreaming(providers, (providerName, status) => {
|
|
111
|
+
completedCount++;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
clearInterval(progressInterval);
|
|
115
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r'); // 清除进度显示
|
|
116
|
+
|
|
117
|
+
// 显示结果
|
|
118
|
+
this._printBatchResults(providers, results);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
clearInterval(progressInterval);
|
|
121
|
+
Logger.error(`批量验证失败: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
} catch (error) {
|
|
125
|
+
Logger.error(`验证供应商失败: ${error.message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 打印单个验证结果
|
|
131
|
+
* @private
|
|
132
|
+
* @param {Object} provider - 供应商配置
|
|
133
|
+
* @param {Object} status - 状态结果
|
|
134
|
+
*/
|
|
135
|
+
_printValidationResult(provider, status) {
|
|
136
|
+
const stateIcon = this._getStateIcon(status.state);
|
|
137
|
+
const stateLabel = this._getStateLabel(status.state);
|
|
138
|
+
const stateColor = this._getStateColor(status.state);
|
|
139
|
+
|
|
140
|
+
console.log();
|
|
141
|
+
console.log(`${stateIcon} 状态: ${stateColor(stateLabel)}`);
|
|
142
|
+
|
|
143
|
+
if (status.message) {
|
|
144
|
+
console.log(` 消息: ${status.message}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (status.latency !== null) {
|
|
148
|
+
console.log(` 响应时间: ${status.latency.toFixed(0)}ms`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log();
|
|
152
|
+
|
|
153
|
+
// 配置详情
|
|
154
|
+
console.log(chalk.gray('配置详情:'));
|
|
155
|
+
console.log(chalk.gray(` 供应商名称: ${provider.name}`));
|
|
156
|
+
console.log(chalk.gray(` 显示名称: ${provider.displayName}`));
|
|
157
|
+
if (provider.alias) {
|
|
158
|
+
console.log(chalk.gray(` 别名: ${provider.alias}`));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (provider.ideName === 'codex') {
|
|
162
|
+
console.log(chalk.gray(' IDE: Codex CLI'));
|
|
163
|
+
if (provider.baseUrl) {
|
|
164
|
+
console.log(chalk.gray(` OPENAI_BASE_URL: ${provider.baseUrl}`));
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
console.log(chalk.gray(` IDE: Claude Code`));
|
|
168
|
+
console.log(chalk.gray(` 认证模式: ${provider.authMode}`));
|
|
169
|
+
if (provider.baseUrl) {
|
|
170
|
+
console.log(chalk.gray(` 基础URL: ${provider.baseUrl}`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 根据状态提供建议
|
|
175
|
+
if (status.state === 'offline') {
|
|
176
|
+
console.log();
|
|
177
|
+
console.log(chalk.yellow('💡 建议:'));
|
|
178
|
+
console.log(chalk.yellow(' 1. 检查 Token 是否正确'));
|
|
179
|
+
console.log(chalk.yellow(' 2. 检查基础URL是否可访问'));
|
|
180
|
+
console.log(chalk.yellow(' 3. 检查网络连接是否正常'));
|
|
181
|
+
} else if (status.state === 'unknown') {
|
|
182
|
+
console.log();
|
|
183
|
+
console.log(chalk.yellow('💡 建议:'));
|
|
184
|
+
console.log(chalk.yellow(' 1. 检查配置是否完整'));
|
|
185
|
+
console.log(chalk.yellow(' 2. 使用 "akm edit" 更新配置'));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 打印批量验证结果
|
|
191
|
+
* @private
|
|
192
|
+
* @param {Array} providers - 供应商列表
|
|
193
|
+
* @param {Object} results - 验证结果
|
|
194
|
+
*/
|
|
195
|
+
_printBatchResults(providers, results) {
|
|
196
|
+
const currentProvider = this.configManager.getCurrentProvider();
|
|
197
|
+
|
|
198
|
+
let onlineCount = 0;
|
|
199
|
+
let offlineCount = 0;
|
|
200
|
+
let unknownCount = 0;
|
|
201
|
+
|
|
202
|
+
providers.forEach(provider => {
|
|
203
|
+
const status = results[provider.name];
|
|
204
|
+
const isCurrent = provider.name === currentProvider?.name;
|
|
205
|
+
const statusIcon = isCurrent ? '✅' : '🔹';
|
|
206
|
+
const stateIcon = this._getStateIcon(status.state);
|
|
207
|
+
const stateColor = this._getStateColor(status.state);
|
|
208
|
+
const nameColor = isCurrent ? chalk.green : chalk.white;
|
|
209
|
+
|
|
210
|
+
// IDE 类型标签
|
|
211
|
+
const ideTag = provider.ideName === 'codex'
|
|
212
|
+
? chalk.cyan('[Codex]')
|
|
213
|
+
: chalk.magenta('[Claude]');
|
|
214
|
+
|
|
215
|
+
// 别名
|
|
216
|
+
const aliasText = provider.alias ? chalk.yellow(` [别名: ${provider.alias}]`) : '';
|
|
217
|
+
|
|
218
|
+
const statusText = status.message || this._getStateLabel(status.state);
|
|
219
|
+
|
|
220
|
+
console.log(`${statusIcon} ${stateIcon} ${ideTag} ${nameColor(provider.name)} (${provider.displayName})${aliasText}`);
|
|
221
|
+
console.log(` ${stateColor(statusText)}`);
|
|
222
|
+
|
|
223
|
+
// 统计
|
|
224
|
+
if (status.state === 'online') onlineCount++;
|
|
225
|
+
else if (status.state === 'offline') offlineCount++;
|
|
226
|
+
else unknownCount++;
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// 显示统计
|
|
230
|
+
console.log();
|
|
231
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
232
|
+
console.log(chalk.blue('📊 验证统计:'));
|
|
233
|
+
console.log(` 总计: ${providers.length} 个供应商`);
|
|
234
|
+
console.log(` ${chalk.green('✓')} 可用: ${onlineCount}`);
|
|
235
|
+
console.log(` ${chalk.red('✗')} 不可用: ${offlineCount}`);
|
|
236
|
+
console.log(` ${chalk.yellow('?')} 未知: ${unknownCount}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 获取状态图标
|
|
241
|
+
* @private
|
|
242
|
+
* @param {string} state - 状态
|
|
243
|
+
* @returns {string} 图标
|
|
244
|
+
*/
|
|
245
|
+
_getStateIcon(state) {
|
|
246
|
+
switch (state) {
|
|
247
|
+
case 'online': return '✓';
|
|
248
|
+
case 'offline': return '✗';
|
|
249
|
+
case 'degraded': return '⚠';
|
|
250
|
+
case 'pending': return '⋯';
|
|
251
|
+
default: return '?';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 获取状态标签
|
|
257
|
+
* @private
|
|
258
|
+
* @param {string} state - 状态
|
|
259
|
+
* @returns {string} 标签
|
|
260
|
+
*/
|
|
261
|
+
_getStateLabel(state) {
|
|
262
|
+
switch (state) {
|
|
263
|
+
case 'online': return '可用';
|
|
264
|
+
case 'offline': return '不可用';
|
|
265
|
+
case 'degraded': return '降级';
|
|
266
|
+
case 'pending': return '检测中';
|
|
267
|
+
default: return '未知';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 获取状态颜色
|
|
273
|
+
* @private
|
|
274
|
+
* @param {string} state - 状态
|
|
275
|
+
* @returns {Function} 颜色函数
|
|
276
|
+
*/
|
|
277
|
+
_getStateColor(state) {
|
|
278
|
+
switch (state) {
|
|
279
|
+
case 'online': return chalk.green;
|
|
280
|
+
case 'offline': return chalk.red;
|
|
281
|
+
case 'degraded': return chalk.yellow;
|
|
282
|
+
case 'pending': return chalk.blue;
|
|
283
|
+
default: return chalk.gray;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 验证命令
|
|
290
|
+
* @param {string|null} providerName - 供应商名称或别名
|
|
291
|
+
* @param {Object} options - 选项
|
|
292
|
+
* @param {string|null} options.filter - 过滤器
|
|
293
|
+
* @returns {Promise<void>}
|
|
294
|
+
*/
|
|
295
|
+
async function validateCommand(providerName, options = {}) {
|
|
296
|
+
const validator = new ProviderValidator();
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
if (providerName) {
|
|
300
|
+
await validator.validateOne(providerName);
|
|
301
|
+
} else {
|
|
302
|
+
await validator.validateAll(options.filter || null);
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
Logger.error(`验证失败: ${error.message}`);
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = { validateCommand, ProviderValidator };
|
package/src/config.js
CHANGED
|
@@ -3,15 +3,54 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} ProviderConfig
|
|
8
|
+
* @property {string} name - 供应商名称
|
|
9
|
+
* @property {string} displayName - 显示名称
|
|
10
|
+
* @property {string} ideName - IDE 名称 ('claude' 或 'codex')
|
|
11
|
+
* @property {string} authMode - 认证模式
|
|
12
|
+
* @property {string} authToken - 认证令牌
|
|
13
|
+
* @property {string|null} baseUrl - API 基础 URL
|
|
14
|
+
* @property {string|null} tokenType - Token 类型
|
|
15
|
+
* @property {Object|null} models - 模型配置
|
|
16
|
+
* @property {string[]} launchArgs - 启动参数
|
|
17
|
+
* @property {boolean} current - 是否为当前供应商
|
|
18
|
+
* @property {number} usageCount - 使用次数
|
|
19
|
+
* @property {string} lastUsed - 最后使用时间
|
|
20
|
+
* @property {string} createdAt - 创建时间
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} Config
|
|
25
|
+
* @property {string} version - 配置版本
|
|
26
|
+
* @property {string|null} currentProvider - 当前供应商名称
|
|
27
|
+
* @property {Object.<string, ProviderConfig>} providers - 供应商配置对象
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 配置管理器
|
|
32
|
+
* 管理 API 供应商配置的加载、保存和操作
|
|
33
|
+
*/
|
|
6
34
|
class ConfigManager {
|
|
7
35
|
constructor() {
|
|
36
|
+
/** @type {string} 配置文件路径 */
|
|
8
37
|
this.configPath = path.join(os.homedir(), '.akm-config.json');
|
|
9
|
-
|
|
38
|
+
/** @type {Config|null} 配置数据 */
|
|
39
|
+
this.config = null;
|
|
40
|
+
/** @type {boolean} 是否已加载 */
|
|
10
41
|
this.isLoaded = false;
|
|
42
|
+
/** @type {Date|null} 最后修改时间 */
|
|
11
43
|
this.lastModified = null;
|
|
12
|
-
|
|
44
|
+
/** @type {Promise<Config>|null} 加载 Promise,防止并发加载 */
|
|
45
|
+
this.loadPromise = null;
|
|
13
46
|
}
|
|
14
47
|
|
|
48
|
+
/**
|
|
49
|
+
* 标准化可选字符串值
|
|
50
|
+
* @private
|
|
51
|
+
* @param {*} value - 输入值
|
|
52
|
+
* @returns {string|null} 标准化后的值
|
|
53
|
+
*/
|
|
15
54
|
_normalizeOptionalString(value) {
|
|
16
55
|
if (value === null || value === undefined) {
|
|
17
56
|
return null;
|
|
@@ -23,6 +62,10 @@ class ConfigManager {
|
|
|
23
62
|
return trimmed.length === 0 ? null : trimmed;
|
|
24
63
|
}
|
|
25
64
|
|
|
65
|
+
/**
|
|
66
|
+
* 获取默认配置
|
|
67
|
+
* @returns {Config} 默认配置对象
|
|
68
|
+
*/
|
|
26
69
|
getDefaultConfig() {
|
|
27
70
|
return {
|
|
28
71
|
version: '1.0.0',
|
|
@@ -31,6 +74,11 @@ class ConfigManager {
|
|
|
31
74
|
};
|
|
32
75
|
}
|
|
33
76
|
|
|
77
|
+
/**
|
|
78
|
+
* 加载配置文件
|
|
79
|
+
* @param {boolean} [forceReload=false] - 是否强制重新加载
|
|
80
|
+
* @returns {Promise<Config>} 配置对象
|
|
81
|
+
*/
|
|
34
82
|
async load(forceReload = false) {
|
|
35
83
|
// 如果正在加载,等待当前加载完成
|
|
36
84
|
if (this.loadPromise) {
|
|
@@ -248,14 +296,21 @@ class ConfigManager {
|
|
|
248
296
|
? providerConfig.launchArgs
|
|
249
297
|
: (existing?.launchArgs || []);
|
|
250
298
|
|
|
299
|
+
// 处理别名
|
|
300
|
+
const alias = providerConfig.alias !== undefined
|
|
301
|
+
? this._normalizeOptionalString(providerConfig.alias)
|
|
302
|
+
: (existing?.alias || null);
|
|
303
|
+
|
|
251
304
|
// 基础字段
|
|
252
305
|
this.config.providers[name] = {
|
|
253
306
|
name,
|
|
254
307
|
displayName: providerConfig.displayName || existing?.displayName || name,
|
|
308
|
+
alias,
|
|
255
309
|
ideName,
|
|
256
310
|
baseUrl,
|
|
257
311
|
authToken,
|
|
258
312
|
launchArgs,
|
|
313
|
+
lastUsedArgs: existing?.lastUsedArgs || null,
|
|
259
314
|
createdAt: existing?.createdAt || now,
|
|
260
315
|
lastUsed: existing?.lastUsed || now,
|
|
261
316
|
usageCount: existing?.usageCount || 0,
|
|
@@ -338,6 +393,168 @@ class ConfigManager {
|
|
|
338
393
|
return await this.save();
|
|
339
394
|
}
|
|
340
395
|
|
|
396
|
+
/**
|
|
397
|
+
* 更新供应商的上次使用启动参数
|
|
398
|
+
* @param {string} name - 供应商名称
|
|
399
|
+
* @param {string[]} args - 启动参数数组
|
|
400
|
+
* @returns {Promise<void>}
|
|
401
|
+
*/
|
|
402
|
+
async updateLastUsedArgs(name, args) {
|
|
403
|
+
await this.ensureLoaded();
|
|
404
|
+
|
|
405
|
+
if (!this.config.providers[name]) {
|
|
406
|
+
throw new Error(`供应商 '${name}' 不存在`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 更新上次使用的启动参数
|
|
410
|
+
this.config.providers[name].lastUsedArgs = args;
|
|
411
|
+
this.config.providers[name].lastUsed = new Date().toISOString();
|
|
412
|
+
|
|
413
|
+
// 增加使用次数
|
|
414
|
+
this.config.providers[name].usageCount = (this.config.providers[name].usageCount || 0) + 1;
|
|
415
|
+
|
|
416
|
+
return await this.save();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 记录供应商使用会话
|
|
421
|
+
* @param {string} name - 供应商名称
|
|
422
|
+
* @param {number} durationMs - 使用时长(毫秒)
|
|
423
|
+
*/
|
|
424
|
+
async recordUsageSession(name, durationMs = 0) {
|
|
425
|
+
await this.ensureLoaded();
|
|
426
|
+
|
|
427
|
+
if (!this.config.providers[name]) {
|
|
428
|
+
throw new Error(`供应商 '${name}' 不存在`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const provider = this.config.providers[name];
|
|
432
|
+
|
|
433
|
+
// 初始化统计数据
|
|
434
|
+
if (!provider.stats) {
|
|
435
|
+
provider.stats = {
|
|
436
|
+
totalSessions: 0,
|
|
437
|
+
totalDurationMs: 0,
|
|
438
|
+
averageDurationMs: 0,
|
|
439
|
+
lastSessionDuration: 0,
|
|
440
|
+
firstUsed: new Date().toISOString()
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 更新统计
|
|
445
|
+
provider.stats.totalSessions = (provider.stats.totalSessions || 0) + 1;
|
|
446
|
+
provider.stats.totalDurationMs = (provider.stats.totalDurationMs || 0) + durationMs;
|
|
447
|
+
provider.stats.lastSessionDuration = durationMs;
|
|
448
|
+
provider.stats.averageDurationMs = Math.round(
|
|
449
|
+
provider.stats.totalDurationMs / provider.stats.totalSessions
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
// 更新最后使用时间
|
|
453
|
+
provider.lastUsed = new Date().toISOString();
|
|
454
|
+
|
|
455
|
+
return await this.save();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* 获取使用统计信息
|
|
460
|
+
* @param {string|null} name - 供应商名称,null 表示获取所有
|
|
461
|
+
* @returns {Object} 统计信息
|
|
462
|
+
*/
|
|
463
|
+
getUsageStats(name = null) {
|
|
464
|
+
if (!this.isLoaded) {
|
|
465
|
+
throw new Error('配置未加载,请先调用 load() 方法');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (name) {
|
|
469
|
+
const provider = this.getProvider(name);
|
|
470
|
+
if (!provider) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
name: provider.name,
|
|
475
|
+
displayName: provider.displayName,
|
|
476
|
+
usageCount: provider.usageCount || 0,
|
|
477
|
+
lastUsed: provider.lastUsed || null,
|
|
478
|
+
stats: provider.stats || null
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 返回所有供应商的统计
|
|
483
|
+
const allStats = Object.entries(this.config.providers).map(([name, provider]) => ({
|
|
484
|
+
name,
|
|
485
|
+
displayName: provider.displayName,
|
|
486
|
+
usageCount: provider.usageCount || 0,
|
|
487
|
+
lastUsed: provider.lastUsed || null,
|
|
488
|
+
stats: provider.stats || null
|
|
489
|
+
}));
|
|
490
|
+
|
|
491
|
+
// 按使用次数降序排序
|
|
492
|
+
return allStats.sort((a, b) => (b.usageCount || 0) - (a.usageCount || 0));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* 获取智能推荐的供应商列表
|
|
497
|
+
* @param {Object} options - 选项
|
|
498
|
+
* @param {number} options.limit - 返回数量限制
|
|
499
|
+
* @param {string|null} options.filter - 过滤器 ('codex', 'claude', 或 null)
|
|
500
|
+
* @returns {Array} 推荐的供应商列表
|
|
501
|
+
*/
|
|
502
|
+
getRecommendedProviders(options = {}) {
|
|
503
|
+
if (!this.isLoaded) {
|
|
504
|
+
throw new Error('配置未加载,请先调用 load() 方法');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const { limit = 5, filter = null } = options;
|
|
508
|
+
let providers = this.listProviders();
|
|
509
|
+
|
|
510
|
+
// 应用过滤器
|
|
511
|
+
if (filter === 'codex') {
|
|
512
|
+
providers = providers.filter(p => p.ideName === 'codex');
|
|
513
|
+
} else if (filter === 'claude') {
|
|
514
|
+
providers = providers.filter(p => p.ideName !== 'codex');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// 计算推荐分数
|
|
518
|
+
const scoredProviders = providers.map(provider => {
|
|
519
|
+
let score = 0;
|
|
520
|
+
|
|
521
|
+
// 使用次数权重 (40%)
|
|
522
|
+
const usageCount = provider.usageCount || 0;
|
|
523
|
+
score += usageCount * 0.4;
|
|
524
|
+
|
|
525
|
+
// 最近使用时间权重 (30%)
|
|
526
|
+
if (provider.lastUsed) {
|
|
527
|
+
const daysSinceLastUse = (Date.now() - new Date(provider.lastUsed).getTime()) / (1000 * 60 * 60 * 24);
|
|
528
|
+
// 越近使用分数越高,超过30天分数衰减
|
|
529
|
+
const recencyScore = Math.max(0, 30 - daysSinceLastUse) / 30;
|
|
530
|
+
score += recencyScore * 30;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// 会话平均时长权重 (20%)
|
|
534
|
+
if (provider.stats?.averageDurationMs) {
|
|
535
|
+
// 平均使用时长越长,说明使用越频繁,最高20分
|
|
536
|
+
const avgMinutes = provider.stats.averageDurationMs / (1000 * 60);
|
|
537
|
+
const durationScore = Math.min(avgMinutes / 60, 1) * 20; // 最多1小时算满分
|
|
538
|
+
score += durationScore;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 总会话数权重 (10%)
|
|
542
|
+
if (provider.stats?.totalSessions) {
|
|
543
|
+
score += Math.min(provider.stats.totalSessions / 10, 1) * 10;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
...provider,
|
|
548
|
+
recommendScore: Math.round(score * 100) / 100
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// 按推荐分数降序排序并限制数量
|
|
553
|
+
return scoredProviders
|
|
554
|
+
.sort((a, b) => b.recommendScore - a.recommendScore)
|
|
555
|
+
.slice(0, limit);
|
|
556
|
+
}
|
|
557
|
+
|
|
341
558
|
getProvider(name) {
|
|
342
559
|
// 同步方法,但需要先确保配置已加载
|
|
343
560
|
if (!this.isLoaded) {
|
|
@@ -346,6 +563,30 @@ class ConfigManager {
|
|
|
346
563
|
return this.config.providers[name];
|
|
347
564
|
}
|
|
348
565
|
|
|
566
|
+
/**
|
|
567
|
+
* 通过名称或别名获取供应商
|
|
568
|
+
* @param {string} nameOrAlias - 供应商名称或别名
|
|
569
|
+
* @returns {ProviderConfig|null} 供应商配置对象,未找到返回 null
|
|
570
|
+
*/
|
|
571
|
+
getProviderByNameOrAlias(nameOrAlias) {
|
|
572
|
+
// 同步方法,但需要先确保配置已加载
|
|
573
|
+
if (!this.isLoaded) {
|
|
574
|
+
throw new Error('配置未加载,请先调用 load() 方法');
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// 先尝试按名称查找
|
|
578
|
+
if (this.config.providers[nameOrAlias]) {
|
|
579
|
+
return this.config.providers[nameOrAlias];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 再尝试按别名查找
|
|
583
|
+
const providerEntry = Object.entries(this.config.providers).find(
|
|
584
|
+
([_, provider]) => provider.alias && provider.alias.toLowerCase() === nameOrAlias.toLowerCase()
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
return providerEntry ? providerEntry[1] : null;
|
|
588
|
+
}
|
|
589
|
+
|
|
349
590
|
listProviders() {
|
|
350
591
|
// 同步方法,但需要先确保配置已加载
|
|
351
592
|
if (!this.isLoaded) {
|