@pikecode/api-key-manager 1.0.17 → 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/switch.js +1 -1
- package/src/config.js +54 -5
- package/src/utils/config-opener.js +7 -2
- package/src/utils/env-launcher.js +55 -24
- package/src/utils/validator.js +40 -6
package/package.json
CHANGED
package/src/CommandRegistry.js
CHANGED
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,6 +100,33 @@ 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
131
|
// 迁移旧配置以保持向后兼容
|
|
89
132
|
if (this.config.providers) {
|
|
@@ -129,6 +172,12 @@ class ConfigManager {
|
|
|
129
172
|
// 保存前确保迁移已应用
|
|
130
173
|
this._migrateAuthModes();
|
|
131
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
|
+
|
|
132
181
|
// 更新最后修改时间
|
|
133
182
|
const stat = await fs.stat(this.configPath);
|
|
134
183
|
this.lastModified = stat.mtime;
|
|
@@ -185,9 +234,9 @@ class ConfigManager {
|
|
|
185
234
|
|
|
186
235
|
async removeProvider(name) {
|
|
187
236
|
await this.ensureLoaded();
|
|
188
|
-
|
|
237
|
+
|
|
189
238
|
if (!this.config.providers[name]) {
|
|
190
|
-
throw new Error(`供应商 '${name}'
|
|
239
|
+
throw new Error(`供应商 '${name}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
|
|
191
240
|
}
|
|
192
241
|
|
|
193
242
|
delete this.config.providers[name];
|
|
@@ -202,9 +251,9 @@ class ConfigManager {
|
|
|
202
251
|
|
|
203
252
|
async setCurrentProvider(name) {
|
|
204
253
|
await this.ensureLoaded();
|
|
205
|
-
|
|
254
|
+
|
|
206
255
|
if (!this.config.providers[name]) {
|
|
207
|
-
throw new Error(`供应商 '${name}'
|
|
256
|
+
throw new Error(`供应商 '${name}' 不存在\n使用 'akm list' 查看所有已配置的供应商`);
|
|
208
257
|
}
|
|
209
258
|
|
|
210
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
|
|