@pikecode/api-key-manager 1.0.16 → 1.0.18
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/package.json +1 -1
- package/src/CommandRegistry.js +1 -1
- package/src/commands/add.js +0 -4
- package/src/commands/switch.js +1 -1
- package/src/config.js +58 -10
- package/src/utils/config-opener.js +7 -2
- package/src/utils/env-launcher.js +55 -24
- package/src/utils/validator.js +40 -6
- package/bin/cc.js +0 -101
package/package.json
CHANGED
package/src/CommandRegistry.js
CHANGED
package/src/commands/add.js
CHANGED
|
@@ -313,12 +313,8 @@ class ProviderAdder extends BaseCommand {
|
|
|
313
313
|
? await this.promptModelConfiguration()
|
|
314
314
|
: { primaryModel: null, smallFastModel: null };
|
|
315
315
|
|
|
316
|
-
// 如果是 Codex 快捷方式,确保 ideName 被设置为 'codex'
|
|
317
|
-
const finalIdeName = forceCodex ? 'codex' : answers.ideName;
|
|
318
|
-
|
|
319
316
|
await this.configManager.addProvider(answers.name, {
|
|
320
317
|
displayName: answers.displayName || answers.name,
|
|
321
|
-
ideName: finalIdeName, // 'claude' 或 'codex'
|
|
322
318
|
baseUrl: answers.baseUrl,
|
|
323
319
|
authToken: answers.authToken,
|
|
324
320
|
authMode: answers.authMode,
|
package/src/commands/switch.js
CHANGED
|
@@ -25,7 +25,7 @@ class EnvSwitcher extends BaseCommand {
|
|
|
25
25
|
await this.configManager.load();
|
|
26
26
|
const provider = this.configManager.getProvider(providerName);
|
|
27
27
|
if (!provider) {
|
|
28
|
-
throw new Error(`供应商 '${providerName}'
|
|
28
|
+
throw new Error(`供应商 '${providerName}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
|
|
29
29
|
}
|
|
30
30
|
return provider;
|
|
31
31
|
}
|
package/src/config.js
CHANGED
|
@@ -50,7 +50,23 @@ class ConfigManager {
|
|
|
50
50
|
async _performLoad() {
|
|
51
51
|
try {
|
|
52
52
|
if (await fs.pathExists(this.configPath)) {
|
|
53
|
-
|
|
53
|
+
// 检查并修复文件权限(仅 Unix 系统)
|
|
54
|
+
await this._checkAndFixPermissions();
|
|
55
|
+
|
|
56
|
+
let data;
|
|
57
|
+
try {
|
|
58
|
+
data = await fs.readJSON(this.configPath);
|
|
59
|
+
} catch (jsonError) {
|
|
60
|
+
// 配置文件损坏,创建备份并重置
|
|
61
|
+
const backupPath = `${this.configPath}.corrupted.${Date.now()}`;
|
|
62
|
+
await fs.copy(this.configPath, backupPath);
|
|
63
|
+
console.log(chalk.yellow('⚠️ 配置文件损坏,已备份到:'), backupPath);
|
|
64
|
+
|
|
65
|
+
this.config = this.getDefaultConfig();
|
|
66
|
+
await this._performSave();
|
|
67
|
+
this.isLoaded = true;
|
|
68
|
+
return this.config;
|
|
69
|
+
}
|
|
54
70
|
|
|
55
71
|
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
56
72
|
// 配置文件被写成非对象内容时,重置为默认配置
|
|
@@ -84,9 +100,35 @@ class ConfigManager {
|
|
|
84
100
|
}
|
|
85
101
|
}
|
|
86
102
|
|
|
103
|
+
async _checkAndFixPermissions() {
|
|
104
|
+
// Windows 不支持 Unix 文件权限
|
|
105
|
+
if (process.platform === 'win32') {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const stat = await fs.stat(this.configPath);
|
|
111
|
+
const mode = stat.mode & 0o777;
|
|
112
|
+
|
|
113
|
+
// 检查权限是否为 600 (仅所有者可读写) 或 400 (仅所有者可读)
|
|
114
|
+
if (mode !== 0o600 && mode !== 0o400) {
|
|
115
|
+
console.log(chalk.yellow('⚠️ 配置文件权限不安全:'), mode.toString(8), chalk.gray('建议: 0600'));
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await fs.chmod(this.configPath, 0o600);
|
|
119
|
+
console.log(chalk.green('✅ 已自动修复文件权限为: 0600'));
|
|
120
|
+
} catch (chmodError) {
|
|
121
|
+
console.log(chalk.red('❌ 无法自动修复权限,请手动执行:'));
|
|
122
|
+
console.log(chalk.gray(` chmod 600 ${this.configPath}`));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// 忽略权限检查错误,不影响主流程
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
87
130
|
_migrateAuthModes() {
|
|
88
|
-
//
|
|
89
|
-
// 同时为旧配置添加 ideName 字段(默认为 'claude')
|
|
131
|
+
// 迁移旧配置以保持向后兼容
|
|
90
132
|
if (this.config.providers) {
|
|
91
133
|
Object.keys(this.config.providers).forEach(key => {
|
|
92
134
|
const provider = this.config.providers[key];
|
|
@@ -96,7 +138,7 @@ class ConfigManager {
|
|
|
96
138
|
provider.authMode = 'auth_token';
|
|
97
139
|
}
|
|
98
140
|
|
|
99
|
-
//
|
|
141
|
+
// 为旧配置添加 ideName 字段(历史兼容性字段,默认为 'claude')
|
|
100
142
|
if (!provider.ideName) {
|
|
101
143
|
provider.ideName = 'claude';
|
|
102
144
|
}
|
|
@@ -130,6 +172,12 @@ class ConfigManager {
|
|
|
130
172
|
// 保存前确保迁移已应用
|
|
131
173
|
this._migrateAuthModes();
|
|
132
174
|
await fs.writeJSON(this.configPath, this.config, { spaces: 2 });
|
|
175
|
+
|
|
176
|
+
// 设置文件权限为 600 (仅所有者可读写)
|
|
177
|
+
if (process.platform !== 'win32') {
|
|
178
|
+
await fs.chmod(this.configPath, 0o600);
|
|
179
|
+
}
|
|
180
|
+
|
|
133
181
|
// 更新最后修改时间
|
|
134
182
|
const stat = await fs.stat(this.configPath);
|
|
135
183
|
this.lastModified = stat.mtime;
|
|
@@ -153,11 +201,11 @@ class ConfigManager {
|
|
|
153
201
|
this.config.providers[name] = {
|
|
154
202
|
name,
|
|
155
203
|
displayName: providerConfig.displayName || name,
|
|
156
|
-
ideName: providerConfig.ideName || 'claude', //
|
|
204
|
+
ideName: providerConfig.ideName || 'claude', // 历史兼容性字段
|
|
157
205
|
baseUrl: providerConfig.baseUrl,
|
|
158
206
|
authToken: providerConfig.authToken,
|
|
159
207
|
authMode: providerConfig.authMode || 'api_key',
|
|
160
|
-
tokenType: providerConfig.tokenType || 'api_key', //
|
|
208
|
+
tokenType: providerConfig.tokenType || 'api_key', // 仅在 authMode 为 'api_key' 时使用
|
|
161
209
|
launchArgs: providerConfig.launchArgs || [],
|
|
162
210
|
models: {
|
|
163
211
|
primary: providerConfig.primaryModel || null,
|
|
@@ -186,9 +234,9 @@ class ConfigManager {
|
|
|
186
234
|
|
|
187
235
|
async removeProvider(name) {
|
|
188
236
|
await this.ensureLoaded();
|
|
189
|
-
|
|
237
|
+
|
|
190
238
|
if (!this.config.providers[name]) {
|
|
191
|
-
throw new Error(`供应商 '${name}'
|
|
239
|
+
throw new Error(`供应商 '${name}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
|
|
192
240
|
}
|
|
193
241
|
|
|
194
242
|
delete this.config.providers[name];
|
|
@@ -203,9 +251,9 @@ class ConfigManager {
|
|
|
203
251
|
|
|
204
252
|
async setCurrentProvider(name) {
|
|
205
253
|
await this.ensureLoaded();
|
|
206
|
-
|
|
254
|
+
|
|
207
255
|
if (!this.config.providers[name]) {
|
|
208
|
-
throw new Error(`供应商 '${name}'
|
|
256
|
+
throw new Error(`供应商 '${name}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
|
|
209
257
|
}
|
|
210
258
|
|
|
211
259
|
// 重置所有供应商的current状态
|
|
@@ -34,9 +34,14 @@ function openFileWithDefaultApp(filePath) {
|
|
|
34
34
|
|
|
35
35
|
async function openAKMConfigFile() {
|
|
36
36
|
if (!(await ensureConfigExists())) {
|
|
37
|
-
throw new Error('
|
|
37
|
+
throw new Error('未找到配置文件 ~/.akm-config.json\n请先运行 \'akm add\' 创建第一个供应商配置');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
await openFileWithDefaultApp(AKM_CONFIG_FILE);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error(`无法打开配置文件: ${error.message}\n配置文件位置: ${AKM_CONFIG_FILE}`);
|
|
38
44
|
}
|
|
39
|
-
await openFileWithDefaultApp(AKM_CONFIG_FILE);
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
module.exports = {
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
const spawn = require('cross-spawn');
|
|
2
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
|
+
|
|
3
24
|
function clearTerminal() {
|
|
4
25
|
if (!process.stdout || typeof process.stdout.write !== 'function') {
|
|
5
26
|
return;
|
|
@@ -24,33 +45,37 @@ function clearTerminal() {
|
|
|
24
45
|
function buildEnvVariables(config) {
|
|
25
46
|
const env = { ...process.env };
|
|
26
47
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
try {
|
|
49
|
+
// Claude Code 配置
|
|
50
|
+
if (config.authMode === 'oauth_token') {
|
|
51
|
+
env.CLAUDE_CODE_OAUTH_TOKEN = sanitizeEnvValue(config.authToken);
|
|
52
|
+
} else if (config.authMode === 'api_key') {
|
|
53
|
+
env.ANTHROPIC_BASE_URL = sanitizeEnvValue(config.baseUrl);
|
|
54
|
+
// 根据 tokenType 选择设置哪种 token
|
|
55
|
+
if (config.tokenType === 'auth_token') {
|
|
56
|
+
env.ANTHROPIC_AUTH_TOKEN = sanitizeEnvValue(config.authToken);
|
|
57
|
+
} else {
|
|
58
|
+
// 默认使用 ANTHROPIC_API_KEY
|
|
59
|
+
env.ANTHROPIC_API_KEY = sanitizeEnvValue(config.authToken);
|
|
60
|
+
}
|
|
35
61
|
} else {
|
|
36
|
-
//
|
|
37
|
-
env.
|
|
62
|
+
// auth_token 模式
|
|
63
|
+
env.ANTHROPIC_BASE_URL = sanitizeEnvValue(config.baseUrl);
|
|
64
|
+
env.ANTHROPIC_AUTH_TOKEN = sanitizeEnvValue(config.authToken);
|
|
38
65
|
}
|
|
39
|
-
} else {
|
|
40
|
-
// auth_token 模式
|
|
41
|
-
env.ANTHROPIC_BASE_URL = config.baseUrl;
|
|
42
|
-
env.ANTHROPIC_AUTH_TOKEN = config.authToken;
|
|
43
|
-
}
|
|
44
66
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
67
|
+
if (config.models && config.models.primary) {
|
|
68
|
+
env.ANTHROPIC_MODEL = sanitizeEnvValue(config.models.primary);
|
|
69
|
+
}
|
|
48
70
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
if (config.models && config.models.smallFast) {
|
|
72
|
+
env.ANTHROPIC_SMALL_FAST_MODEL = sanitizeEnvValue(config.models.smallFast);
|
|
73
|
+
}
|
|
52
74
|
|
|
53
|
-
|
|
75
|
+
return env;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(`配置验证失败: ${error.message}\n请使用 'akm edit ${config.name}' 修复配置`);
|
|
78
|
+
}
|
|
54
79
|
}
|
|
55
80
|
|
|
56
81
|
async function executeWithEnv(config, launchArgs = []) {
|
|
@@ -72,11 +97,17 @@ async function executeWithEnv(config, launchArgs = []) {
|
|
|
72
97
|
if (code === 0) {
|
|
73
98
|
resolve();
|
|
74
99
|
} else {
|
|
75
|
-
reject(new Error(`Claude Code
|
|
100
|
+
reject(new Error(`Claude Code 退出,退出代码: ${code}\n提示: 请检查 API 配置是否正确`));
|
|
76
101
|
}
|
|
77
102
|
});
|
|
78
103
|
|
|
79
|
-
child.on('error',
|
|
104
|
+
child.on('error', (error) => {
|
|
105
|
+
if (error.code === 'ENOENT') {
|
|
106
|
+
reject(new Error('找不到 claude 命令\n请确认已安装 Claude Code (https://claude.com/code)'));
|
|
107
|
+
} else {
|
|
108
|
+
reject(new Error(`启动 Claude Code 失败: ${error.message}`));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
80
111
|
});
|
|
81
112
|
}
|
|
82
113
|
|
package/src/utils/validator.js
CHANGED
|
@@ -3,15 +3,33 @@ const validator = {
|
|
|
3
3
|
if (!name || typeof name !== 'string') {
|
|
4
4
|
return '供应商名称不能为空';
|
|
5
5
|
}
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
if (name.trim().length === 0) {
|
|
8
8
|
return '供应商名称不能为空或只包含空格';
|
|
9
9
|
}
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
// 禁止文件系统特殊字符
|
|
12
|
+
if (/[<>:"/\\|?*\x00-\x1F]/.test(name)) {
|
|
13
|
+
return '供应商名称包含非法字符 (不能包含: < > : " / \\ | ? *)';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 禁止使用保留名称 (Windows)
|
|
17
|
+
const reserved = ['CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
|
|
18
|
+
'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2',
|
|
19
|
+
'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
|
|
20
|
+
if (reserved.includes(name.toUpperCase())) {
|
|
21
|
+
return '供应商名称不能使用系统保留名称';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 禁止以点或空格开头/结尾 (Windows 限制)
|
|
25
|
+
if (/^[. ]|[. ]$/.test(name)) {
|
|
26
|
+
return '供应商名称不能以点或空格开头/结尾';
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
if (name.length > 100) {
|
|
12
30
|
return '供应商名称不能超过100个字符';
|
|
13
31
|
}
|
|
14
|
-
|
|
32
|
+
|
|
15
33
|
return null;
|
|
16
34
|
},
|
|
17
35
|
|
|
@@ -54,11 +72,27 @@ const validator = {
|
|
|
54
72
|
if (!token || typeof token !== 'string') {
|
|
55
73
|
return 'Token不能为空';
|
|
56
74
|
}
|
|
57
|
-
|
|
58
|
-
if (token.length
|
|
75
|
+
|
|
76
|
+
if (token.trim().length === 0) {
|
|
77
|
+
return 'Token不能只包含空格';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (token.trim().length < 10) {
|
|
59
81
|
return 'Token长度不能少于10个字符';
|
|
60
82
|
}
|
|
61
|
-
|
|
83
|
+
|
|
84
|
+
// 检测常见的占位符文本
|
|
85
|
+
const placeholders = [
|
|
86
|
+
'your-key-here', 'your-token', 'your_key', 'your_token',
|
|
87
|
+
'example', 'test-key', 'demo', 'placeholder', 'replace-me',
|
|
88
|
+
'insert-key', 'api-key-here', 'token-here', 'xxx', 'yyy',
|
|
89
|
+
'zzz', 'abc123', '123456'
|
|
90
|
+
];
|
|
91
|
+
const lowerToken = token.toLowerCase();
|
|
92
|
+
if (placeholders.some(p => lowerToken.includes(p))) {
|
|
93
|
+
return 'Token 似乎是占位符,请输入真实的 API Token';
|
|
94
|
+
}
|
|
95
|
+
|
|
62
96
|
return null;
|
|
63
97
|
},
|
|
64
98
|
|
package/bin/cc.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const { program } = require('commander');
|
|
4
|
-
const chalk = require('chalk');
|
|
5
|
-
const { main } = require('../src/index');
|
|
6
|
-
const { registry } = require('../src/CommandRegistry');
|
|
7
|
-
const pkg = require('../package.json');
|
|
8
|
-
const { checkForUpdates } = require('../src/utils/update-checker');
|
|
9
|
-
|
|
10
|
-
// Set up CLI
|
|
11
|
-
program
|
|
12
|
-
.name('akm')
|
|
13
|
-
.description('API密钥管理工具 - Manage and switch multiple API provider configurations')
|
|
14
|
-
.version(pkg.version, '-v, -V, --version', '显示版本号');
|
|
15
|
-
|
|
16
|
-
// Check for updates before any command runs
|
|
17
|
-
program.hook('preAction', async () => {
|
|
18
|
-
await checkForUpdates({ packageName: pkg.name, currentVersion: pkg.version });
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// Default command - show provider selection
|
|
22
|
-
program
|
|
23
|
-
.argument('[provider]', '直接切换到指定供应商')
|
|
24
|
-
.action(async (provider) => {
|
|
25
|
-
try {
|
|
26
|
-
await main(provider);
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error(chalk.red('❌ 执行失败:'), error.message);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Add command
|
|
34
|
-
program
|
|
35
|
-
.command('add')
|
|
36
|
-
.description('添加新供应商配置')
|
|
37
|
-
.action(async () => {
|
|
38
|
-
try {
|
|
39
|
-
await registry.executeCommand('add');
|
|
40
|
-
} catch (error) {
|
|
41
|
-
console.error(chalk.red('❌ 添加失败:'), error.message);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Remove command
|
|
47
|
-
program
|
|
48
|
-
.command('remove')
|
|
49
|
-
.argument('[provider]', '要删除的供应商名称')
|
|
50
|
-
.description('删除供应商配置')
|
|
51
|
-
.action(async (provider) => {
|
|
52
|
-
try {
|
|
53
|
-
await registry.executeCommand('remove', provider);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
console.error(chalk.red('❌ 删除失败:'), error.message);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// List command
|
|
61
|
-
program
|
|
62
|
-
.command('list')
|
|
63
|
-
.description('列出所有供应商')
|
|
64
|
-
.action(async () => {
|
|
65
|
-
try {
|
|
66
|
-
await registry.executeCommand('list');
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.error(chalk.red('❌ 列表失败:'), error.message);
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Current command
|
|
74
|
-
program
|
|
75
|
-
.command('current')
|
|
76
|
-
.description('显示当前配置')
|
|
77
|
-
.action(async () => {
|
|
78
|
-
try {
|
|
79
|
-
await registry.executeCommand('current');
|
|
80
|
-
} catch (error) {
|
|
81
|
-
console.error(chalk.red('❌ 获取当前配置失败:'), error.message);
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Edit command
|
|
87
|
-
program
|
|
88
|
-
.command('edit')
|
|
89
|
-
.argument('[provider]', '要编辑的供应商名称')
|
|
90
|
-
.description('编辑供应商配置')
|
|
91
|
-
.action(async (provider) => {
|
|
92
|
-
try {
|
|
93
|
-
await registry.executeCommand('edit', provider);
|
|
94
|
-
} catch (error) {
|
|
95
|
-
console.error(chalk.red('❌ 编辑失败:'), error.message);
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Parse arguments
|
|
101
|
-
program.parse();
|