@pikecode/api-key-manager 1.0.32 → 1.0.33
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 +8 -2
- package/bin/akm.js +5 -3
- package/package.json +1 -1
- package/src/commands/add.js +19 -33
- package/src/commands/backup.js +2 -2
- package/src/commands/current.js +14 -12
- package/src/commands/edit.js +51 -7
- package/src/commands/list.js +11 -9
- package/src/commands/remove.js +2 -2
- package/src/commands/switch.js +38 -71
- package/src/config.js +84 -23
- package/src/utils/codex-launcher.js +1 -42
- package/src/utils/env-launcher.js +7 -43
- package/src/utils/env-utils.js +52 -0
- package/src/utils/error-handler.js +1 -1
- package/src/utils/launch-args.js +96 -0
- package/src/utils/provider-status-checker.js +87 -26
- package/src/utils/secrets.js +22 -0
- package/src/utils/update-checker.js +8 -2
- package/src/utils/validator.js +3 -68
- package/src/utils/storage.js +0 -55
package/src/config.js
CHANGED
|
@@ -12,6 +12,17 @@ class ConfigManager {
|
|
|
12
12
|
this.loadPromise = null; // 防止并发加载
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
_normalizeOptionalString(value) {
|
|
16
|
+
if (value === null || value === undefined) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value !== 'string') {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
return trimmed.length === 0 ? null : trimmed;
|
|
24
|
+
}
|
|
25
|
+
|
|
15
26
|
getDefaultConfig() {
|
|
16
27
|
return {
|
|
17
28
|
version: '1.0.0',
|
|
@@ -78,6 +89,8 @@ class ConfigManager {
|
|
|
78
89
|
|
|
79
90
|
// 迁移旧的认证模式
|
|
80
91
|
this._migrateAuthModes();
|
|
92
|
+
// 修复 current 标记与 currentProvider 不一致的问题(兼容旧版本写入的脏数据)
|
|
93
|
+
this._syncCurrentFlags();
|
|
81
94
|
|
|
82
95
|
const stat = await fs.stat(this.configPath);
|
|
83
96
|
this.lastModified = stat.mtime;
|
|
@@ -195,30 +208,78 @@ class ConfigManager {
|
|
|
195
208
|
}
|
|
196
209
|
}
|
|
197
210
|
|
|
211
|
+
_syncCurrentFlags() {
|
|
212
|
+
if (!this.config || !this.config.providers) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const current = this.config.currentProvider;
|
|
217
|
+
const providers = this.config.providers;
|
|
218
|
+
const keys = Object.keys(providers);
|
|
219
|
+
|
|
220
|
+
if (current && providers[current]) {
|
|
221
|
+
keys.forEach((key) => {
|
|
222
|
+
providers[key].current = key === current;
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
keys.forEach((key) => {
|
|
228
|
+
providers[key].current = false;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
198
232
|
async addProvider(name, providerConfig) {
|
|
199
233
|
await this.ensureLoaded();
|
|
200
234
|
|
|
201
|
-
const
|
|
235
|
+
const existing = this.config.providers[name];
|
|
236
|
+
const now = new Date().toISOString();
|
|
237
|
+
|
|
238
|
+
const ideName = providerConfig.ideName || existing?.ideName || 'claude';
|
|
239
|
+
const isCodex = ideName === 'codex';
|
|
240
|
+
|
|
241
|
+
const baseUrl = this._normalizeOptionalString(
|
|
242
|
+
providerConfig.baseUrl !== undefined ? providerConfig.baseUrl : existing?.baseUrl
|
|
243
|
+
);
|
|
202
244
|
|
|
245
|
+
const authToken = providerConfig.authToken !== undefined ? providerConfig.authToken : existing?.authToken;
|
|
246
|
+
|
|
247
|
+
const launchArgs = Array.isArray(providerConfig.launchArgs)
|
|
248
|
+
? providerConfig.launchArgs
|
|
249
|
+
: (existing?.launchArgs || []);
|
|
250
|
+
|
|
251
|
+
// 基础字段
|
|
203
252
|
this.config.providers[name] = {
|
|
204
253
|
name,
|
|
205
|
-
displayName: providerConfig.displayName || name,
|
|
206
|
-
ideName
|
|
207
|
-
baseUrl
|
|
208
|
-
authToken
|
|
209
|
-
launchArgs
|
|
210
|
-
createdAt:
|
|
211
|
-
lastUsed:
|
|
212
|
-
|
|
254
|
+
displayName: providerConfig.displayName || existing?.displayName || name,
|
|
255
|
+
ideName,
|
|
256
|
+
baseUrl,
|
|
257
|
+
authToken,
|
|
258
|
+
launchArgs,
|
|
259
|
+
createdAt: existing?.createdAt || now,
|
|
260
|
+
lastUsed: existing?.lastUsed || now,
|
|
261
|
+
usageCount: existing?.usageCount || 0,
|
|
262
|
+
current: Boolean(existing?.current || this.config.currentProvider === name)
|
|
213
263
|
};
|
|
214
264
|
|
|
215
265
|
// Claude Code 特定字段
|
|
216
266
|
if (!isCodex) {
|
|
217
|
-
|
|
218
|
-
|
|
267
|
+
const authMode = providerConfig.authMode || existing?.authMode || 'api_key';
|
|
268
|
+
const tokenType = authMode === 'api_key'
|
|
269
|
+
? (providerConfig.tokenType ?? existing?.tokenType ?? 'api_key')
|
|
270
|
+
: null;
|
|
271
|
+
const primaryModel = providerConfig.primaryModel !== undefined
|
|
272
|
+
? providerConfig.primaryModel
|
|
273
|
+
: (existing?.models?.primary ?? null);
|
|
274
|
+
const smallFastModel = providerConfig.smallFastModel !== undefined
|
|
275
|
+
? providerConfig.smallFastModel
|
|
276
|
+
: (existing?.models?.smallFast ?? null);
|
|
277
|
+
|
|
278
|
+
this.config.providers[name].authMode = authMode;
|
|
279
|
+
this.config.providers[name].tokenType = tokenType;
|
|
219
280
|
this.config.providers[name].models = {
|
|
220
|
-
primary:
|
|
221
|
-
smallFast:
|
|
281
|
+
primary: primaryModel,
|
|
282
|
+
smallFast: smallFastModel
|
|
222
283
|
};
|
|
223
284
|
} else {
|
|
224
285
|
// Codex 不需要这些字段,设置为 null 以保持向后兼容
|
|
@@ -228,18 +289,15 @@ class ConfigManager {
|
|
|
228
289
|
}
|
|
229
290
|
|
|
230
291
|
// 如果是第一个供应商或设置为默认,则设为当前供应商
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
Object.keys(this.config.providers).forEach(key => {
|
|
234
|
-
this.config.providers[key].current = false;
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
// 设置新的当前供应商
|
|
238
|
-
this.config.providers[name].current = true;
|
|
239
|
-
this.config.providers[name].lastUsed = new Date().toISOString();
|
|
292
|
+
const shouldSetCurrent = (!existing && Object.keys(this.config.providers).length === 1) || providerConfig.setAsDefault;
|
|
293
|
+
if (shouldSetCurrent) {
|
|
240
294
|
this.config.currentProvider = name;
|
|
295
|
+
this.config.providers[name].lastUsed = now;
|
|
241
296
|
}
|
|
242
297
|
|
|
298
|
+
// 保证 current 标记与 currentProvider 一致,避免出现多个 current 或丢失 current 的情况
|
|
299
|
+
this._syncCurrentFlags();
|
|
300
|
+
|
|
243
301
|
return await this.save();
|
|
244
302
|
}
|
|
245
303
|
|
|
@@ -317,4 +375,7 @@ class ConfigManager {
|
|
|
317
375
|
}
|
|
318
376
|
}
|
|
319
377
|
|
|
320
|
-
|
|
378
|
+
// 单例实例
|
|
379
|
+
const configManager = new ConfigManager();
|
|
380
|
+
|
|
381
|
+
module.exports = { ConfigManager, configManager };
|
|
@@ -1,46 +1,5 @@
|
|
|
1
1
|
const spawn = require('cross-spawn');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 清理环境变量值,移除危险字符
|
|
5
|
-
* @param {string} value - 要清理的值
|
|
6
|
-
* @returns {string} 清理后的值
|
|
7
|
-
*/
|
|
8
|
-
function sanitizeEnvValue(value) {
|
|
9
|
-
if (typeof value !== 'string') {
|
|
10
|
-
throw new Error('环境变量值必须是字符串');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// 移除控制字符
|
|
14
|
-
let cleaned = value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
15
|
-
|
|
16
|
-
// 检测可能的 shell 命令注入
|
|
17
|
-
if (/[;&|`$()]/.test(cleaned)) {
|
|
18
|
-
throw new Error('环境变量值包含潜在不安全的字符');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return cleaned;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function clearTerminal() {
|
|
25
|
-
if (!process.stdout || typeof process.stdout.write !== 'function') {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
process.stdout.write('\x1bc');
|
|
31
|
-
} catch (error) {
|
|
32
|
-
// 某些终端可能不支持 RIS 序列,忽略即可
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const sequence = process.platform === 'win32'
|
|
36
|
-
? '\x1b[3J\x1b[2J\x1b[0f'
|
|
37
|
-
: '\x1b[3J\x1b[2J\x1b[H';
|
|
38
|
-
try {
|
|
39
|
-
process.stdout.write(sequence);
|
|
40
|
-
} catch (error) {
|
|
41
|
-
// 忽略清屏失败
|
|
42
|
-
}
|
|
43
|
-
}
|
|
2
|
+
const { sanitizeEnvValue, clearTerminal } = require('./env-utils');
|
|
44
3
|
|
|
45
4
|
/**
|
|
46
5
|
* 构建 Codex CLI 环境变量
|
|
@@ -1,46 +1,5 @@
|
|
|
1
1
|
const spawn = require('cross-spawn');
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 清理环境变量值,移除危险字符
|
|
5
|
-
* @param {string} value - 要清理的值
|
|
6
|
-
* @returns {string} 清理后的值
|
|
7
|
-
*/
|
|
8
|
-
function sanitizeEnvValue(value) {
|
|
9
|
-
if (typeof value !== 'string') {
|
|
10
|
-
throw new Error('环境变量值必须是字符串');
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// 移除控制字符
|
|
14
|
-
let cleaned = value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
15
|
-
|
|
16
|
-
// 检测可能的 shell 命令注入
|
|
17
|
-
if (/[;&|`$()]/.test(cleaned)) {
|
|
18
|
-
throw new Error('环境变量值包含潜在不安全的字符');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return cleaned;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function clearTerminal() {
|
|
25
|
-
if (!process.stdout || typeof process.stdout.write !== 'function') {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
process.stdout.write('\x1bc');
|
|
31
|
-
} catch (error) {
|
|
32
|
-
// 某些终端可能不支持 RIS 序列,忽略即可
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const sequence = process.platform === 'win32'
|
|
36
|
-
? '\x1b[3J\x1b[2J\x1b[0f'
|
|
37
|
-
: '\x1b[3J\x1b[2J\x1b[H';
|
|
38
|
-
try {
|
|
39
|
-
process.stdout.write(sequence);
|
|
40
|
-
} catch (error) {
|
|
41
|
-
// 忽略清屏失败
|
|
42
|
-
}
|
|
43
|
-
}
|
|
2
|
+
const { sanitizeEnvValue, clearTerminal } = require('./env-utils');
|
|
44
3
|
|
|
45
4
|
function buildEnvVariables(config) {
|
|
46
5
|
const env = { ...process.env };
|
|
@@ -50,6 +9,9 @@ function buildEnvVariables(config) {
|
|
|
50
9
|
if (config.authMode === 'oauth_token') {
|
|
51
10
|
env.CLAUDE_CODE_OAUTH_TOKEN = sanitizeEnvValue(config.authToken);
|
|
52
11
|
} else if (config.authMode === 'api_key') {
|
|
12
|
+
if (!config.baseUrl) {
|
|
13
|
+
throw new Error('未配置基础地址');
|
|
14
|
+
}
|
|
53
15
|
env.ANTHROPIC_BASE_URL = sanitizeEnvValue(config.baseUrl);
|
|
54
16
|
// 根据 tokenType 选择设置哪种 token
|
|
55
17
|
if (config.tokenType === 'auth_token') {
|
|
@@ -60,7 +22,9 @@ function buildEnvVariables(config) {
|
|
|
60
22
|
}
|
|
61
23
|
} else {
|
|
62
24
|
// auth_token 模式
|
|
63
|
-
|
|
25
|
+
if (config.baseUrl) {
|
|
26
|
+
env.ANTHROPIC_BASE_URL = sanitizeEnvValue(config.baseUrl);
|
|
27
|
+
}
|
|
64
28
|
env.ANTHROPIC_AUTH_TOKEN = sanitizeEnvValue(config.authToken);
|
|
65
29
|
}
|
|
66
30
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 环境变量工具函数
|
|
3
|
+
* 提供 env-launcher.js 和 codex-launcher.js 共用的功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 清理环境变量值,移除危险字符
|
|
8
|
+
* @param {string} value - 要清理的值
|
|
9
|
+
* @returns {string} 清理后的值
|
|
10
|
+
*/
|
|
11
|
+
function sanitizeEnvValue(value) {
|
|
12
|
+
if (typeof value !== 'string') {
|
|
13
|
+
throw new Error('环境变量值必须是字符串');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 移除控制字符
|
|
17
|
+
let cleaned = value.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '');
|
|
18
|
+
|
|
19
|
+
// 检测可能的 shell 命令注入(允许 $ 因为 token 可能包含)
|
|
20
|
+
// 只禁止明确的命令分隔符和反引号执行
|
|
21
|
+
if (/[;&|`]/.test(cleaned)) {
|
|
22
|
+
throw new Error('环境变量值包含潜在不安全的字符');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return cleaned;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 清屏函数
|
|
30
|
+
*/
|
|
31
|
+
function clearTerminal() {
|
|
32
|
+
if (!process.stdout || typeof process.stdout.write !== 'function') {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
process.stdout.write('\x1bc');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// 某些终端可能不支持 RIS 序列,忽略即可
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sequence = process.platform === 'win32'
|
|
43
|
+
? '\x1b[3J\x1b[2J\x1b[0f'
|
|
44
|
+
: '\x1b[3J\x1b[2J\x1b[H';
|
|
45
|
+
try {
|
|
46
|
+
process.stdout.write(sequence);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
// 忽略清屏失败
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { sanitizeEnvValue, clearTerminal };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 启动参数定义模块
|
|
3
|
+
* 统一管理 Claude Code 和 Codex CLI 的启动参数
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const claudeLaunchArgs = [
|
|
7
|
+
{
|
|
8
|
+
name: '--continue',
|
|
9
|
+
label: '继续上次对话',
|
|
10
|
+
description: '恢复上次的对话记录',
|
|
11
|
+
checked: false
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: '--dangerously-skip-permissions',
|
|
15
|
+
label: '最高权限',
|
|
16
|
+
description: '仅在沙盒环境中使用',
|
|
17
|
+
checked: false
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const codexLaunchArgs = [
|
|
22
|
+
{
|
|
23
|
+
name: 'resume',
|
|
24
|
+
label: '继续上次对话',
|
|
25
|
+
description: '恢复之前的会话',
|
|
26
|
+
checked: false,
|
|
27
|
+
isSubcommand: true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: '--full-auto',
|
|
31
|
+
label: '全自动模式',
|
|
32
|
+
description: '自动批准 + 工作区写入沙盒',
|
|
33
|
+
checked: false,
|
|
34
|
+
exclusive: ['--dangerously-bypass-approvals-and-sandbox']
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: '--dangerously-bypass-approvals-and-sandbox',
|
|
38
|
+
label: '跳过审批和沙盒',
|
|
39
|
+
description: '危险:跳过所有安全检查',
|
|
40
|
+
checked: false,
|
|
41
|
+
exclusive: ['--full-auto']
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: '--search',
|
|
45
|
+
label: '启用网页搜索',
|
|
46
|
+
description: '允许模型搜索网页',
|
|
47
|
+
checked: false
|
|
48
|
+
}
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function getClaudeLaunchArgs() {
|
|
52
|
+
return claudeLaunchArgs.map(arg => ({ ...arg }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getCodexLaunchArgs() {
|
|
56
|
+
return codexLaunchArgs.map(arg => ({ ...arg }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getLaunchArgs(ideName) {
|
|
60
|
+
return ideName === 'codex' ? getCodexLaunchArgs() : getClaudeLaunchArgs();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 检查互斥参数
|
|
65
|
+
* @param {string[]} selectedArgs - 选中的参数列表
|
|
66
|
+
* @param {Array} availableArgs - 可用参数定义
|
|
67
|
+
* @returns {string|null} 冲突错误信息或 null
|
|
68
|
+
*/
|
|
69
|
+
function checkExclusiveArgs(selectedArgs, availableArgs) {
|
|
70
|
+
if (!selectedArgs || selectedArgs.length < 2) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const argDef of availableArgs) {
|
|
75
|
+
if (!argDef.exclusive || !selectedArgs.includes(argDef.name)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const exclusiveArg of argDef.exclusive) {
|
|
80
|
+
if (selectedArgs.includes(exclusiveArg)) {
|
|
81
|
+
const arg1 = availableArgs.find(a => a.name === argDef.name);
|
|
82
|
+
const arg2 = availableArgs.find(a => a.name === exclusiveArg);
|
|
83
|
+
return `"${arg1?.label || argDef.name}" 和 "${arg2?.label || exclusiveArg}" 不能同时选择`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
getClaudeLaunchArgs,
|
|
93
|
+
getCodexLaunchArgs,
|
|
94
|
+
getLaunchArgs,
|
|
95
|
+
checkExclusiveArgs
|
|
96
|
+
};
|
|
@@ -1,28 +1,94 @@
|
|
|
1
1
|
const { Anthropic, APIConnectionTimeoutError, APIConnectionError, APIError } = require('@anthropic-ai/sdk');
|
|
2
2
|
|
|
3
|
+
let authTokenEnvLock = Promise.resolve();
|
|
4
|
+
|
|
5
|
+
// 状态缓存
|
|
6
|
+
const statusCache = new Map();
|
|
7
|
+
const DEFAULT_CACHE_TTL = 30000; // 30秒缓存
|
|
8
|
+
|
|
3
9
|
class ProviderStatusChecker {
|
|
4
10
|
constructor(options = {}) {
|
|
5
11
|
this.timeout = options.timeout ?? 5000;
|
|
6
12
|
this.testMessage = options.testMessage ?? '你好';
|
|
7
13
|
this.maxTokens = options.maxTokens ?? 32;
|
|
8
14
|
this.defaultModel = 'claude-haiku-4-5-20251001';
|
|
15
|
+
this.cacheTTL = options.cacheTTL ?? DEFAULT_CACHE_TTL;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
_getCacheKey(provider) {
|
|
19
|
+
// 基于关键配置生成缓存键
|
|
20
|
+
return `${provider.name}:${provider.authToken}:${provider.baseUrl}:${provider.authMode}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_getCachedStatus(provider) {
|
|
24
|
+
const key = this._getCacheKey(provider);
|
|
25
|
+
const cached = statusCache.get(key);
|
|
26
|
+
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
|
|
27
|
+
return cached.status;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_setCachedStatus(provider, status) {
|
|
33
|
+
const key = this._getCacheKey(provider);
|
|
34
|
+
statusCache.set(key, {
|
|
35
|
+
status,
|
|
36
|
+
timestamp: Date.now()
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
clearCache() {
|
|
41
|
+
statusCache.clear();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async _withAuthTokenEnv(authToken, operation) {
|
|
45
|
+
const previous = authTokenEnvLock;
|
|
46
|
+
let release;
|
|
47
|
+
authTokenEnvLock = new Promise((resolve) => {
|
|
48
|
+
release = resolve;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await previous;
|
|
52
|
+
|
|
53
|
+
const original = process.env.ANTHROPIC_AUTH_TOKEN;
|
|
54
|
+
try {
|
|
55
|
+
process.env.ANTHROPIC_AUTH_TOKEN = authToken;
|
|
56
|
+
return await operation();
|
|
57
|
+
} finally {
|
|
58
|
+
if (original !== undefined) {
|
|
59
|
+
process.env.ANTHROPIC_AUTH_TOKEN = original;
|
|
60
|
+
} else {
|
|
61
|
+
delete process.env.ANTHROPIC_AUTH_TOKEN;
|
|
62
|
+
}
|
|
63
|
+
release();
|
|
64
|
+
}
|
|
9
65
|
}
|
|
10
66
|
|
|
11
|
-
async check(provider) {
|
|
67
|
+
async check(provider, options = {}) {
|
|
12
68
|
if (!provider) {
|
|
13
69
|
return this._result('unknown', '未找到配置', null);
|
|
14
70
|
}
|
|
15
71
|
|
|
72
|
+
// 检查缓存(除非明确跳过)
|
|
73
|
+
if (!options.skipCache) {
|
|
74
|
+
const cached = this._getCachedStatus(provider);
|
|
75
|
+
if (cached) {
|
|
76
|
+
return cached;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
16
80
|
if (provider.ideName === 'codex') {
|
|
17
|
-
|
|
81
|
+
const result = await this._checkCodex(provider);
|
|
82
|
+
this._setCachedStatus(provider, result);
|
|
83
|
+
return result;
|
|
18
84
|
}
|
|
19
85
|
|
|
20
86
|
if (provider.authMode === 'oauth_token') {
|
|
21
87
|
return this._result('unknown', '暂不支持 OAuth 令牌检测', null);
|
|
22
88
|
}
|
|
23
89
|
|
|
24
|
-
// auth_token
|
|
25
|
-
//
|
|
90
|
+
// auth_token 模式可留空 baseUrl(使用官方默认 API)
|
|
91
|
+
// api_key 模式需要 baseUrl(用于自定义端点/代理)
|
|
26
92
|
if (provider.authMode === 'auth_token' && !provider.baseUrl) {
|
|
27
93
|
// 对于官方 Anthropic API 的 auth_token 模式,不需要 baseUrl
|
|
28
94
|
// 直接使用官方 API
|
|
@@ -36,19 +102,12 @@ class ProviderStatusChecker {
|
|
|
36
102
|
|
|
37
103
|
const model = this._resolveModel(provider);
|
|
38
104
|
try {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// 保存原始环境变量
|
|
45
|
-
const originalEnv = {};
|
|
46
|
-
if (provider.authMode === 'auth_token') {
|
|
47
|
-
originalEnv.ANTHROPIC_AUTH_TOKEN = process.env.ANTHROPIC_AUTH_TOKEN;
|
|
48
|
-
process.env.ANTHROPIC_AUTH_TOKEN = provider.authToken;
|
|
49
|
-
}
|
|
105
|
+
const performCheck = async () => {
|
|
106
|
+
const client = this._createClient(provider);
|
|
107
|
+
if (!client) {
|
|
108
|
+
return this._result('unknown', '认证模式不受支持', null);
|
|
109
|
+
}
|
|
50
110
|
|
|
51
|
-
try {
|
|
52
111
|
const start = process.hrtime.bigint();
|
|
53
112
|
const response = await client.messages.create({
|
|
54
113
|
model,
|
|
@@ -68,18 +127,20 @@ class ProviderStatusChecker {
|
|
|
68
127
|
}
|
|
69
128
|
|
|
70
129
|
return this._result('online', `可用 ${latency.toFixed(0)}ms`, latency);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
let result;
|
|
133
|
+
if (provider.authMode === 'auth_token') {
|
|
134
|
+
result = await this._withAuthTokenEnv(provider.authToken, performCheck);
|
|
135
|
+
} else {
|
|
136
|
+
result = await performCheck();
|
|
80
137
|
}
|
|
138
|
+
this._setCachedStatus(provider, result);
|
|
139
|
+
return result;
|
|
81
140
|
} catch (error) {
|
|
82
|
-
|
|
141
|
+
const result = this._handleError(error);
|
|
142
|
+
this._setCachedStatus(provider, result);
|
|
143
|
+
return result;
|
|
83
144
|
}
|
|
84
145
|
}
|
|
85
146
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function maskToken(token) {
|
|
2
|
+
if (!token || typeof token !== 'string') {
|
|
3
|
+
return token;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const value = token.trim();
|
|
7
|
+
if (value.length < 10) {
|
|
8
|
+
return '***';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return value.substring(0, 8) + '***' + value.substring(value.length - 4);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function maybeMaskToken(token, showToken = false) {
|
|
15
|
+
if (!token) {
|
|
16
|
+
return token;
|
|
17
|
+
}
|
|
18
|
+
return showToken ? token : maskToken(token);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { maskToken, maybeMaskToken };
|
|
22
|
+
|
|
@@ -105,8 +105,14 @@ async function checkForUpdates({ packageName, currentVersion }) {
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
console.log(chalk.green('更新成功,正在重启...'));
|
|
108
|
-
const
|
|
109
|
-
|
|
108
|
+
const scriptPath = process.argv[1];
|
|
109
|
+
const restartCommand = scriptPath ? process.execPath : packageName;
|
|
110
|
+
const restartArgs = scriptPath
|
|
111
|
+
? [scriptPath, ...process.argv.slice(2)]
|
|
112
|
+
: process.argv.slice(2);
|
|
113
|
+
|
|
114
|
+
const child = spawn(restartCommand, restartArgs, {
|
|
115
|
+
shell: process.platform === 'win32',
|
|
110
116
|
stdio: 'inherit',
|
|
111
117
|
});
|
|
112
118
|
child.on('close', (code) => {
|
package/src/utils/validator.js
CHANGED
|
@@ -117,75 +117,10 @@ const validator = {
|
|
|
117
117
|
return null;
|
|
118
118
|
},
|
|
119
119
|
|
|
120
|
-
validateLaunchArgs(args) {
|
|
121
|
-
if (!Array.isArray(args)) {
|
|
122
|
-
return '启动参数必须是数组';
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const validArgs = [
|
|
126
|
-
'--dangerously-skip-permissions',
|
|
127
|
-
'--no-confirm',
|
|
128
|
-
'--allow-all',
|
|
129
|
-
'--auto-approve',
|
|
130
|
-
'--yes',
|
|
131
|
-
'--force'
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
for (const arg of args) {
|
|
135
|
-
if (!validArgs.includes(arg)) {
|
|
136
|
-
return `无效的启动参数: ${arg}`;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return null;
|
|
141
|
-
},
|
|
142
|
-
|
|
143
120
|
getAvailableLaunchArgs() {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
name: '--continue',
|
|
147
|
-
label: '继续上次对话',
|
|
148
|
-
description: '恢复上次的对话记录',
|
|
149
|
-
checked: false
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
name: '--dangerously-skip-permissions',
|
|
153
|
-
label: '最高权限',
|
|
154
|
-
description: '仅限沙盒环境使用',
|
|
155
|
-
checked: false
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
name: '--no-confirm',
|
|
159
|
-
label: '直接执行操作',
|
|
160
|
-
description: '跳过确认提示',
|
|
161
|
-
checked: false
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
name: '--allow-all',
|
|
165
|
-
label: '允许全部操作',
|
|
166
|
-
description: '移除安全限制',
|
|
167
|
-
checked: false
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
name: '--auto-approve',
|
|
171
|
-
label: '自动批准请求',
|
|
172
|
-
description: '无需人工同意',
|
|
173
|
-
checked: false
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
name: '--yes',
|
|
177
|
-
label: '默认回答 yes',
|
|
178
|
-
description: '自动同意所有询问',
|
|
179
|
-
checked: false
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
name: '--force',
|
|
183
|
-
label: '强制执行',
|
|
184
|
-
description: '忽略可能的警告',
|
|
185
|
-
checked: false
|
|
186
|
-
}
|
|
187
|
-
];
|
|
121
|
+
const { getClaudeLaunchArgs } = require('./launch-args');
|
|
122
|
+
return getClaudeLaunchArgs();
|
|
188
123
|
}
|
|
189
124
|
};
|
|
190
125
|
|
|
191
|
-
module.exports = { validator };
|
|
126
|
+
module.exports = { validator };
|