@pikecode/api-key-manager 1.0.26 → 1.0.28
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 -69
- package/bin/akm.js +46 -0
- package/package.json +1 -1
- package/src/CommandRegistry.js +15 -0
- package/src/commands/backup.js +327 -0
- package/src/commands/switch.js +29 -0
package/README.md
CHANGED
|
@@ -1,123 +1,450 @@
|
|
|
1
|
-
# API Key Manager
|
|
1
|
+
# API Key Manager (akm)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
一个强大的 CLI 工具,用于管理和快速切换 **Claude Code** 和 **Codex CLI** 的 API 配置。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@pikecode/api-key-manager)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- 🔐 **安全存储** - 本地安全存储 API 密钥
|
|
9
|
-
- 🌍 **多提供商支持** - 支持多个 API 提供商(Anthropic)
|
|
10
|
-
- 🎯 **灵活配置** - 支持多种认证模式(API Key、Auth Token、OAuth)
|
|
11
|
-
- 🚀 **开箱即用** - 无需复杂配置
|
|
12
|
-
- 💾 **环境变量管理** - 自动设置和管理环境变量
|
|
8
|
+
## ✨ 功能特性
|
|
13
9
|
|
|
14
|
-
|
|
10
|
+
- 🎯 **双 IDE 支持** - 同时管理 Claude Code 和 Codex CLI 配置
|
|
11
|
+
- 🔄 **快速切换** - 一键切换不同的 API 提供商
|
|
12
|
+
- 🔐 **安全存储** - 本地加密存储 API 密钥
|
|
13
|
+
- 🎨 **多认证模式** - 支持 OAuth、API Key、Auth Token
|
|
14
|
+
- 🚀 **启动参数** - 为每个供应商配置专属启动参数
|
|
15
|
+
- 💾 **备份恢复** - 配置导出、导入、备份功能
|
|
16
|
+
- 🏷️ **智能过滤** - 按 IDE 类型过滤供应商列表
|
|
17
|
+
- ⚡ **参数校验** - 自动检测互斥参数冲突
|
|
18
|
+
- 🌍 **跨平台** - macOS / Linux / Windows
|
|
19
|
+
|
|
20
|
+
## 📦 安装
|
|
15
21
|
|
|
16
22
|
```bash
|
|
17
23
|
npm install -g @pikecode/api-key-manager
|
|
18
24
|
```
|
|
19
25
|
|
|
20
|
-
## 快速开始
|
|
26
|
+
## 🚀 快速开始
|
|
21
27
|
|
|
22
28
|
```bash
|
|
23
|
-
#
|
|
24
|
-
akm --help
|
|
25
|
-
|
|
26
|
-
# 添加新的 API 提供商配置
|
|
29
|
+
# 添加第一个配置
|
|
27
30
|
akm add
|
|
28
31
|
|
|
29
|
-
#
|
|
32
|
+
# 切换供应商(交互式)
|
|
30
33
|
akm
|
|
31
34
|
|
|
32
|
-
#
|
|
35
|
+
# 查看所有配置
|
|
36
|
+
akm list
|
|
37
|
+
|
|
38
|
+
# 查看当前激活的配置
|
|
33
39
|
akm current
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 📖 完整命令参考
|
|
43
|
+
|
|
44
|
+
### 基础命令
|
|
45
|
+
|
|
46
|
+
#### `akm` / `akm switch`
|
|
47
|
+
交互式选择和切换供应商
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 显示所有供应商
|
|
51
|
+
akm
|
|
52
|
+
|
|
53
|
+
# 直接切换到指定供应商
|
|
54
|
+
akm my-provider
|
|
34
55
|
|
|
35
|
-
#
|
|
56
|
+
# 仅显示 Codex CLI 供应商
|
|
57
|
+
akm switch --codex
|
|
58
|
+
|
|
59
|
+
# 仅显示 Claude Code 供应商
|
|
60
|
+
akm switch --claude
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### `akm add`
|
|
64
|
+
添加新的 API 供应商配置
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 交互式添加(会询问 IDE 类型)
|
|
68
|
+
akm add
|
|
69
|
+
|
|
70
|
+
# 直接添加 Claude Code 供应商
|
|
71
|
+
akm add --claude
|
|
72
|
+
|
|
73
|
+
# 直接添加 Codex CLI 供应商
|
|
74
|
+
akm add --codex
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**添加过程中可配置:**
|
|
78
|
+
- IDE 类型(Claude Code / Codex CLI)
|
|
79
|
+
- 供应商名称和显示名称
|
|
80
|
+
- 认证模式(仅 Claude Code)
|
|
81
|
+
- API 密钥 / OAuth Token
|
|
82
|
+
- 基础 URL
|
|
83
|
+
- 启动参数
|
|
84
|
+
- 模型配置
|
|
85
|
+
|
|
86
|
+
#### `akm list`
|
|
87
|
+
列出所有已保存的配置
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# 列出所有供应商
|
|
36
91
|
akm list
|
|
92
|
+
|
|
93
|
+
# 仅列出 Codex CLI 供应商
|
|
94
|
+
akm list --codex
|
|
95
|
+
|
|
96
|
+
# 仅列出 Claude Code 供应商
|
|
97
|
+
akm list --claude
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**显示内容:**
|
|
101
|
+
- ✅ 当前激活的供应商
|
|
102
|
+
- 🟢/🟡/🔴 API 可用性状态
|
|
103
|
+
- [Codex]/[Claude] IDE 类型标签
|
|
104
|
+
- 认证模式、环境变量、启动参数
|
|
105
|
+
- 创建时间、最后使用时间
|
|
106
|
+
|
|
107
|
+
#### `akm current`
|
|
108
|
+
显示当前激活的配置
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
akm current
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**显示内容:**
|
|
115
|
+
- 供应商名称和显示名称
|
|
116
|
+
- IDE 类型
|
|
117
|
+
- 认证模式
|
|
118
|
+
- 环境变量设置
|
|
119
|
+
- 启动参数
|
|
120
|
+
- 模型配置
|
|
121
|
+
|
|
122
|
+
#### `akm edit`
|
|
123
|
+
编辑供应商配置
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# 交互式选择要编辑的供应商
|
|
127
|
+
akm edit
|
|
128
|
+
|
|
129
|
+
# 直接编辑指定供应商
|
|
130
|
+
akm edit my-provider
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**可编辑项:**
|
|
134
|
+
- 显示名称
|
|
135
|
+
- 认证令牌
|
|
136
|
+
- 基础 URL
|
|
137
|
+
- 启动参数
|
|
138
|
+
- 模型配置
|
|
139
|
+
|
|
140
|
+
#### `akm remove`
|
|
141
|
+
删除供应商配置
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# 交互式选择要删除的供应商
|
|
145
|
+
akm remove
|
|
146
|
+
|
|
147
|
+
# 直接删除指定供应商
|
|
148
|
+
akm remove my-provider
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 备份与迁移
|
|
152
|
+
|
|
153
|
+
#### `akm export`
|
|
154
|
+
导出配置到文件
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
# 导出到默认文件 (akm-config-{timestamp}.json)
|
|
158
|
+
akm export
|
|
159
|
+
|
|
160
|
+
# 导出到指定文件
|
|
161
|
+
akm export my-backup.json
|
|
162
|
+
|
|
163
|
+
# 导出时脱敏 Token(适合分享配置模板)
|
|
164
|
+
akm export template.json --mask
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**导出格式:**
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"version": "1.0",
|
|
171
|
+
"exportedAt": "2025-12-15T05:30:00.000Z",
|
|
172
|
+
"providers": {
|
|
173
|
+
"my-provider": {
|
|
174
|
+
"name": "my-provider",
|
|
175
|
+
"displayName": "My Provider",
|
|
176
|
+
"ideName": "claude",
|
|
177
|
+
"authMode": "api_key",
|
|
178
|
+
"authToken": "sk-ant-***",
|
|
179
|
+
"baseUrl": "https://api.anthropic.com"
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"currentProvider": "my-provider"
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### `akm import`
|
|
187
|
+
从文件导入配置
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# 导入配置(跳过已存在的供应商)
|
|
191
|
+
akm import my-backup.json
|
|
192
|
+
|
|
193
|
+
# 导入并覆盖已存在的供应商
|
|
194
|
+
akm import my-backup.json --overwrite
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**注意:** 如果导入的配置使用了 `--mask` 脱敏,需要手动编辑 Token。
|
|
198
|
+
|
|
199
|
+
#### `akm backup`
|
|
200
|
+
备份和恢复配置
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# 创建备份(默认保存到 ~/.akm-backups/)
|
|
204
|
+
akm backup
|
|
205
|
+
|
|
206
|
+
# 指定备份目录
|
|
207
|
+
akm backup --dir /path/to/backups
|
|
208
|
+
|
|
209
|
+
# 列出所有备份
|
|
210
|
+
akm backup --list
|
|
211
|
+
|
|
212
|
+
# 从备份恢复
|
|
213
|
+
akm backup --restore akm-backup-2025-12-15T05-30-00.json
|
|
214
|
+
|
|
215
|
+
# 从指定目录的备份恢复
|
|
216
|
+
akm backup --restore backup.json --dir /path/to/backups
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**自动清理:** 默认保留最近 10 个备份,自动删除旧备份。
|
|
220
|
+
|
|
221
|
+
## 🎨 IDE 支持
|
|
222
|
+
|
|
223
|
+
### Claude Code (Anthropic 官方)
|
|
224
|
+
|
|
225
|
+
**认证模式:**
|
|
226
|
+
- **oauth_token** - OAuth 令牌模式(官方推荐)
|
|
227
|
+
- **api_key** - 通用 API 密钥模式
|
|
228
|
+
- **auth_token** - 认证令牌模式
|
|
229
|
+
|
|
230
|
+
**环境变量:**
|
|
231
|
+
- `CLAUDE_CODE_OAUTH_TOKEN` - OAuth 模式
|
|
232
|
+
- `ANTHROPIC_API_KEY` - API Key 模式
|
|
233
|
+
- `ANTHROPIC_AUTH_TOKEN` - Auth Token 模式
|
|
234
|
+
- `ANTHROPIC_BASE_URL` - 自定义 API 端点
|
|
235
|
+
|
|
236
|
+
**启动参数:**
|
|
237
|
+
- `--continue` - 继续上次对话
|
|
238
|
+
- `--dangerously-skip-permissions` - 跳过权限检查(沙盒环境)
|
|
239
|
+
|
|
240
|
+
**配置示例:**
|
|
241
|
+
```bash
|
|
242
|
+
akm add --claude
|
|
243
|
+
# 选择认证模式 -> 输入 Token -> 配置启动参数
|
|
37
244
|
```
|
|
38
245
|
|
|
39
|
-
|
|
246
|
+
### Codex CLI (OpenAI)
|
|
40
247
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
| `akm` | 交互式选择和切换 API 提供商 |
|
|
44
|
-
| `akm add` | 添加新的 API 提供商配置 |
|
|
45
|
-
| `akm list` | 列出所有已保存的配置 |
|
|
46
|
-
| `akm current` | 显示当前激活的配置 |
|
|
47
|
-
| `akm edit <name>` | 编辑指定提供商的配置 |
|
|
48
|
-
| `akm remove <name>` | 删除指定的提供商配置 |
|
|
248
|
+
**认证模式:**
|
|
249
|
+
- 使用 `OPENAI_API_KEY` 和 `OPENAI_BASE_URL` 环境变量
|
|
49
250
|
|
|
50
|
-
|
|
251
|
+
**启动参数:**
|
|
252
|
+
- `resume` - 继续上次对话(子命令)
|
|
253
|
+
- `--full-auto` - 全自动模式(自动批准 + 工作区沙盒)⚠️ 与 `--dangerously-bypass-approvals-and-sandbox` 互斥
|
|
254
|
+
- `--dangerously-bypass-approvals-and-sandbox` - 跳过所有安全检查 ⚠️ 与 `--full-auto` 互斥
|
|
255
|
+
- `--search` - 启用网页搜索
|
|
51
256
|
|
|
52
|
-
|
|
257
|
+
**配置导入:**
|
|
258
|
+
```bash
|
|
259
|
+
akm add --codex
|
|
260
|
+
# 选择 "从 ~/.codex 导入现有配置" 自动读取现有配置
|
|
261
|
+
# 或选择 "手动输入配置" 手动设置
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**配置示例:**
|
|
265
|
+
```bash
|
|
266
|
+
# 方式1:从现有 Codex 配置导入
|
|
267
|
+
akm add --codex
|
|
268
|
+
# -> 选择 "从 ~/.codex 导入现有配置"
|
|
269
|
+
# -> 自动读取 ~/.codex/auth.json 和 config.toml
|
|
53
270
|
|
|
54
|
-
|
|
271
|
+
# 方式2:手动配置
|
|
272
|
+
akm add --codex
|
|
273
|
+
# -> 选择 "手动输入配置"
|
|
274
|
+
# -> 输入 API Key 和 Base URL
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## ⚙️ 配置文件
|
|
278
|
+
|
|
279
|
+
**位置:** `~/.akm-config.json`
|
|
280
|
+
|
|
281
|
+
**完整示例:**
|
|
55
282
|
```json
|
|
56
283
|
{
|
|
57
284
|
"version": "2.0.0",
|
|
58
|
-
"currentProvider": "
|
|
285
|
+
"currentProvider": "my-claude",
|
|
59
286
|
"providers": {
|
|
60
|
-
"claude
|
|
61
|
-
"name": "claude
|
|
62
|
-
"displayName": "Claude Code
|
|
287
|
+
"my-claude": {
|
|
288
|
+
"name": "my-claude",
|
|
289
|
+
"displayName": "Claude Code Official",
|
|
290
|
+
"ideName": "claude",
|
|
63
291
|
"authMode": "oauth_token",
|
|
64
|
-
"authToken": "sk-ant-oat01
|
|
292
|
+
"authToken": "sk-ant-oat01-xxx",
|
|
65
293
|
"baseUrl": null,
|
|
66
294
|
"tokenType": null,
|
|
295
|
+
"launchArgs": ["--continue"],
|
|
67
296
|
"models": {
|
|
68
297
|
"primary": "claude-sonnet-4",
|
|
69
298
|
"smallFast": "claude-haiku-4"
|
|
70
|
-
}
|
|
299
|
+
},
|
|
300
|
+
"createdAt": "2025-12-15T05:00:00.000Z",
|
|
301
|
+
"lastUsed": "2025-12-15T05:30:00.000Z"
|
|
71
302
|
},
|
|
72
|
-
"
|
|
73
|
-
"name": "
|
|
74
|
-
"displayName": "
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
303
|
+
"my-codex": {
|
|
304
|
+
"name": "my-codex",
|
|
305
|
+
"displayName": "Codex CLI",
|
|
306
|
+
"ideName": "codex",
|
|
307
|
+
"authMode": "openai_api_key",
|
|
308
|
+
"authToken": "sk-xxx",
|
|
309
|
+
"baseUrl": "https://api.openai.com",
|
|
310
|
+
"tokenType": null,
|
|
311
|
+
"launchArgs": ["resume", "--full-auto"],
|
|
312
|
+
"createdAt": "2025-12-15T05:00:00.000Z",
|
|
313
|
+
"lastUsed": "2025-12-15T05:25:00.000Z"
|
|
83
314
|
}
|
|
84
315
|
}
|
|
85
316
|
}
|
|
86
317
|
```
|
|
87
318
|
|
|
88
|
-
##
|
|
319
|
+
## 🎯 使用场景
|
|
89
320
|
|
|
90
|
-
###
|
|
91
|
-
- 认证模式:
|
|
92
|
-
- `oauth_token` - OAuth 令牌模式(推荐官方用户)
|
|
93
|
-
- `api_key` - 通用 API 密钥模式(支持 ANTHROPIC_API_KEY 和 ANTHROPIC_AUTH_TOKEN)
|
|
94
|
-
- `auth_token` - 认证令牌模式(仅 ANTHROPIC_AUTH_TOKEN)
|
|
95
|
-
- 环境变量:CLAUDE_CODE_OAUTH_TOKEN、ANTHROPIC_API_KEY、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_BASE_URL
|
|
321
|
+
### 场景 1: 同时使用多个 API Key
|
|
96
322
|
|
|
97
|
-
|
|
323
|
+
```bash
|
|
324
|
+
# 添加工作账号
|
|
325
|
+
akm add --claude
|
|
326
|
+
# 名称: work
|
|
327
|
+
# 显示名称: Work Account
|
|
328
|
+
# Token: sk-ant-work-xxx
|
|
329
|
+
|
|
330
|
+
# 添加个人账号
|
|
331
|
+
akm add --claude
|
|
332
|
+
# 名称: personal
|
|
333
|
+
# 显示名称: Personal Account
|
|
334
|
+
# Token: sk-ant-personal-xxx
|
|
335
|
+
|
|
336
|
+
# 快速切换
|
|
337
|
+
akm # 选择 work 或 personal
|
|
338
|
+
```
|
|
98
339
|
|
|
99
|
-
|
|
100
|
-
|------|-----|------|
|
|
101
|
-
| **oauth_token** | Claude Code | OAuth 令牌模式 |
|
|
102
|
-
| **api_key** | Claude Code | 标准 API 密钥模式 |
|
|
103
|
-
| **auth_token** | Claude Code | 认证令牌模式 |
|
|
340
|
+
### 场景 2: Claude Code 和 Codex CLI 混合使用
|
|
104
341
|
|
|
105
|
-
|
|
342
|
+
```bash
|
|
343
|
+
# 添加 Claude Code 配置
|
|
344
|
+
akm add --claude
|
|
345
|
+
|
|
346
|
+
# 添加 Codex CLI 配置
|
|
347
|
+
akm add --codex
|
|
348
|
+
|
|
349
|
+
# 查看所有配置(带 IDE 标签)
|
|
350
|
+
akm list
|
|
106
351
|
|
|
107
|
-
|
|
108
|
-
|
|
352
|
+
# 仅切换 Codex 供应商
|
|
353
|
+
akm switch --codex
|
|
354
|
+
|
|
355
|
+
# 仅切换 Claude 供应商
|
|
356
|
+
akm switch --claude
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 场景 3: 团队配置分享
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
# 导出配置模板(脱敏)
|
|
363
|
+
akm export team-template.json --mask
|
|
364
|
+
|
|
365
|
+
# 团队成员导入后编辑 Token
|
|
366
|
+
akm import team-template.json
|
|
367
|
+
akm edit my-provider # 设置自己的 Token
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### 场景 4: 配置迁移
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
# 旧机器:导出配置
|
|
374
|
+
akm export my-config.json
|
|
375
|
+
|
|
376
|
+
# 新机器:导入配置
|
|
377
|
+
akm import my-config.json
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### 场景 5: 定期备份
|
|
381
|
+
|
|
382
|
+
```bash
|
|
383
|
+
# 创建备份
|
|
384
|
+
akm backup
|
|
385
|
+
|
|
386
|
+
# 查看备份列表
|
|
387
|
+
akm backup --list
|
|
388
|
+
|
|
389
|
+
# 恢复到某个备份
|
|
390
|
+
akm backup --restore akm-backup-2025-12-15T05-30-00.json
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## ⚠️ 参数互斥说明
|
|
394
|
+
|
|
395
|
+
某些参数不能同时使用,akm 会自动检测并提示:
|
|
396
|
+
|
|
397
|
+
**Codex CLI:**
|
|
398
|
+
- `--full-auto` ⚔️ `--dangerously-bypass-approvals-and-sandbox`
|
|
399
|
+
|
|
400
|
+
如果同时选择互斥参数,会显示警告并要求重新选择。
|
|
401
|
+
|
|
402
|
+
## ⌨️ 快捷键
|
|
403
|
+
|
|
404
|
+
- **↑/↓** - 上下导航
|
|
405
|
+
- **Space** - 切换选中(多选)
|
|
406
|
+
- **Enter** - 确认
|
|
407
|
+
- **ESC** - 返回上级菜单 / 取消操作
|
|
109
408
|
- **Ctrl+C** - 退出程序
|
|
110
409
|
|
|
111
|
-
## 系统要求
|
|
410
|
+
## 🔧 系统要求
|
|
112
411
|
|
|
113
412
|
- Node.js >= 14.0.0
|
|
114
413
|
- macOS / Linux / Windows
|
|
115
414
|
|
|
116
|
-
##
|
|
415
|
+
## 📝 更新日志
|
|
416
|
+
|
|
417
|
+
### v1.0.27 (最新)
|
|
418
|
+
- ✨ 新增参数互斥校验
|
|
419
|
+
- ✨ 新增 `export` / `import` / `backup` 命令
|
|
420
|
+
- 🧪 测试覆盖率提升 46%
|
|
421
|
+
|
|
422
|
+
### v1.0.26
|
|
423
|
+
- ✨ Codex 添加 `resume` 子命令支持
|
|
424
|
+
|
|
425
|
+
### v1.0.25
|
|
426
|
+
- 🐛 修复 Codex 启动参数
|
|
427
|
+
|
|
428
|
+
### v1.0.23
|
|
429
|
+
- ✨ list 和 switch 命令显示 IDE 类型标签
|
|
430
|
+
|
|
431
|
+
### v1.0.22
|
|
432
|
+
- ✨ 完整的 Codex CLI 支持优化
|
|
433
|
+
|
|
434
|
+
## 🤝 贡献
|
|
435
|
+
|
|
436
|
+
欢迎提交 Issue 和 Pull Request!
|
|
437
|
+
|
|
438
|
+
## 📄 许可证
|
|
439
|
+
|
|
440
|
+
MIT License
|
|
441
|
+
|
|
442
|
+
## 🔗 链接
|
|
117
443
|
|
|
118
|
-
|
|
444
|
+
- **GitHub**: https://github.com/pikecode/api-key-manager
|
|
445
|
+
- **NPM**: https://www.npmjs.com/package/@pikecode/api-key-manager
|
|
446
|
+
- **Issues**: https://github.com/pikecode/api-key-manager/issues
|
|
119
447
|
|
|
120
|
-
|
|
448
|
+
---
|
|
121
449
|
|
|
122
|
-
|
|
123
|
-
- NPM: https://www.npmjs.com/package/@pikecode/api-key-manager
|
|
450
|
+
Made with ❤️ by [pikecode](https://github.com/pikecode)
|
package/bin/akm.js
CHANGED
|
@@ -120,5 +120,51 @@ program
|
|
|
120
120
|
}
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
+
// Export command
|
|
124
|
+
program
|
|
125
|
+
.command('export')
|
|
126
|
+
.argument('[file]', '导出文件路径 (默认: akm-config-{timestamp}.json)')
|
|
127
|
+
.description('导出配置到文件')
|
|
128
|
+
.option('--mask', '脱敏 Token (导入后需重新设置)')
|
|
129
|
+
.action(async (file, options) => {
|
|
130
|
+
try {
|
|
131
|
+
await registry.executeCommand('export', file, options);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error(chalk.red('❌ 导出失败:'), error.message);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Import command
|
|
139
|
+
program
|
|
140
|
+
.command('import')
|
|
141
|
+
.argument('<file>', '要导入的配置文件路径')
|
|
142
|
+
.description('从文件导入配置')
|
|
143
|
+
.option('--overwrite', '覆盖已存在的供应商')
|
|
144
|
+
.action(async (file, options) => {
|
|
145
|
+
try {
|
|
146
|
+
await registry.executeCommand('import', file, options);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(chalk.red('❌ 导入失败:'), error.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Backup command
|
|
154
|
+
program
|
|
155
|
+
.command('backup')
|
|
156
|
+
.description('备份和恢复配置')
|
|
157
|
+
.option('-l, --list', '列出所有备份')
|
|
158
|
+
.option('-r, --restore <file>', '从备份恢复')
|
|
159
|
+
.option('-d, --dir <path>', '指定备份目录')
|
|
160
|
+
.action(async (options) => {
|
|
161
|
+
try {
|
|
162
|
+
await registry.executeCommand('backup', options);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error(chalk.red('❌ 备份操作失败:'), error.message);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
123
169
|
// Parse arguments
|
|
124
170
|
program.parse();
|
package/package.json
CHANGED
package/src/CommandRegistry.js
CHANGED
|
@@ -71,4 +71,19 @@ registry.registerLazy('edit', async () => {
|
|
|
71
71
|
return editCommand;
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
registry.registerLazy('export', async () => {
|
|
75
|
+
const { exportCommand } = require('./commands/backup');
|
|
76
|
+
return exportCommand;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
registry.registerLazy('import', async () => {
|
|
80
|
+
const { importCommand } = require('./commands/backup');
|
|
81
|
+
return importCommand;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
registry.registerLazy('backup', async () => {
|
|
85
|
+
const { backupCommand } = require('./commands/backup');
|
|
86
|
+
return backupCommand;
|
|
87
|
+
});
|
|
88
|
+
|
|
74
89
|
module.exports = { CommandRegistry, registry };
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { ConfigManager } = require('../config');
|
|
5
|
+
const { Logger } = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
class BackupManager {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.configManager = new ConfigManager();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 导出配置到指定文件
|
|
14
|
+
* @param {string} outputPath - 输出文件路径
|
|
15
|
+
* @param {object} options - 选项
|
|
16
|
+
*/
|
|
17
|
+
async export(outputPath, options = {}) {
|
|
18
|
+
try {
|
|
19
|
+
await this.configManager.ensureLoaded();
|
|
20
|
+
const config = this.configManager.config;
|
|
21
|
+
|
|
22
|
+
if (!config.providers || Object.keys(config.providers).length === 0) {
|
|
23
|
+
Logger.warning('没有供应商配置可导出');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 准备导出数据
|
|
28
|
+
const exportData = {
|
|
29
|
+
version: '1.0',
|
|
30
|
+
exportedAt: new Date().toISOString(),
|
|
31
|
+
providers: config.providers,
|
|
32
|
+
currentProvider: config.currentProvider
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 如果需要脱敏
|
|
36
|
+
if (options.mask) {
|
|
37
|
+
exportData.providers = this.maskTokens(exportData.providers);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 确定输出路径
|
|
41
|
+
const finalPath = outputPath || `akm-config-${this.getTimestamp()}.json`;
|
|
42
|
+
const absolutePath = path.isAbsolute(finalPath) ? finalPath : path.join(process.cwd(), finalPath);
|
|
43
|
+
|
|
44
|
+
// 写入文件
|
|
45
|
+
await fs.writeJson(absolutePath, exportData, { spaces: 2 });
|
|
46
|
+
|
|
47
|
+
Logger.success(`配置已导出到: ${absolutePath}`);
|
|
48
|
+
console.log(chalk.gray(` 供应商数量: ${Object.keys(config.providers).length}`));
|
|
49
|
+
if (options.mask) {
|
|
50
|
+
console.log(chalk.yellow(' 注意: Token 已脱敏,导入后需要重新设置'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
} catch (error) {
|
|
54
|
+
Logger.error(`导出配置失败: ${error.message}`);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从文件导入配置
|
|
61
|
+
* @param {string} inputPath - 输入文件路径
|
|
62
|
+
* @param {object} options - 选项
|
|
63
|
+
*/
|
|
64
|
+
async import(inputPath, options = {}) {
|
|
65
|
+
try {
|
|
66
|
+
if (!inputPath) {
|
|
67
|
+
Logger.error('请指定要导入的配置文件路径');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const absolutePath = path.isAbsolute(inputPath) ? inputPath : path.join(process.cwd(), inputPath);
|
|
72
|
+
|
|
73
|
+
if (!await fs.pathExists(absolutePath)) {
|
|
74
|
+
Logger.error(`文件不存在: ${absolutePath}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const importData = await fs.readJson(absolutePath);
|
|
79
|
+
|
|
80
|
+
// 验证导入数据
|
|
81
|
+
if (!importData.providers || typeof importData.providers !== 'object') {
|
|
82
|
+
Logger.error('无效的配置文件格式');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await this.configManager.ensureLoaded();
|
|
87
|
+
|
|
88
|
+
const importedCount = Object.keys(importData.providers).length;
|
|
89
|
+
let addedCount = 0;
|
|
90
|
+
let skippedCount = 0;
|
|
91
|
+
|
|
92
|
+
for (const [name, provider] of Object.entries(importData.providers)) {
|
|
93
|
+
const exists = this.configManager.getProvider(name);
|
|
94
|
+
|
|
95
|
+
if (exists && !options.overwrite) {
|
|
96
|
+
Logger.warning(`供应商 "${name}" 已存在,跳过 (使用 --overwrite 覆盖)`);
|
|
97
|
+
skippedCount++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 添加或更新供应商
|
|
102
|
+
this.configManager.config.providers[name] = {
|
|
103
|
+
...provider,
|
|
104
|
+
name,
|
|
105
|
+
importedAt: new Date().toISOString()
|
|
106
|
+
};
|
|
107
|
+
addedCount++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 如果设置了 currentProvider 且该供应商存在
|
|
111
|
+
if (importData.currentProvider && this.configManager.config.providers[importData.currentProvider]) {
|
|
112
|
+
if (!this.configManager.config.currentProvider || options.overwrite) {
|
|
113
|
+
this.configManager.config.currentProvider = importData.currentProvider;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await this.configManager.save();
|
|
118
|
+
|
|
119
|
+
Logger.success(`配置导入完成`);
|
|
120
|
+
console.log(chalk.gray(` 导入文件: ${absolutePath}`));
|
|
121
|
+
console.log(chalk.gray(` 成功导入: ${addedCount} 个供应商`));
|
|
122
|
+
if (skippedCount > 0) {
|
|
123
|
+
console.log(chalk.yellow(` 跳过: ${skippedCount} 个已存在的供应商`));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
Logger.error(`导入配置失败: ${error.message}`);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 备份当前配置
|
|
134
|
+
* @param {string} backupDir - 备份目录
|
|
135
|
+
*/
|
|
136
|
+
async backup(backupDir) {
|
|
137
|
+
try {
|
|
138
|
+
await this.configManager.ensureLoaded();
|
|
139
|
+
|
|
140
|
+
// 确定备份目录
|
|
141
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
142
|
+
const defaultBackupDir = path.join(homeDir, '.akm-backups');
|
|
143
|
+
const finalDir = backupDir || defaultBackupDir;
|
|
144
|
+
|
|
145
|
+
// 确保备份目录存在
|
|
146
|
+
await fs.ensureDir(finalDir);
|
|
147
|
+
|
|
148
|
+
// 生成备份文件名
|
|
149
|
+
const backupFileName = `akm-backup-${this.getTimestamp()}.json`;
|
|
150
|
+
const backupPath = path.join(finalDir, backupFileName);
|
|
151
|
+
|
|
152
|
+
// 读取原始配置文件
|
|
153
|
+
const configPath = this.configManager.configPath;
|
|
154
|
+
if (!await fs.pathExists(configPath)) {
|
|
155
|
+
Logger.warning('配置文件不存在,无需备份');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 复制配置文件
|
|
160
|
+
await fs.copy(configPath, backupPath);
|
|
161
|
+
|
|
162
|
+
Logger.success(`配置已备份到: ${backupPath}`);
|
|
163
|
+
|
|
164
|
+
// 清理旧备份(保留最近 10 个)
|
|
165
|
+
await this.cleanOldBackups(finalDir, 10);
|
|
166
|
+
|
|
167
|
+
} catch (error) {
|
|
168
|
+
Logger.error(`备份配置失败: ${error.message}`);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 列出所有备份
|
|
175
|
+
* @param {string} backupDir - 备份目录
|
|
176
|
+
*/
|
|
177
|
+
async listBackups(backupDir) {
|
|
178
|
+
try {
|
|
179
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
180
|
+
const defaultBackupDir = path.join(homeDir, '.akm-backups');
|
|
181
|
+
const finalDir = backupDir || defaultBackupDir;
|
|
182
|
+
|
|
183
|
+
if (!await fs.pathExists(finalDir)) {
|
|
184
|
+
Logger.warning('备份目录不存在');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const files = await fs.readdir(finalDir);
|
|
189
|
+
const backups = files
|
|
190
|
+
.filter(f => f.startsWith('akm-backup-') && f.endsWith('.json'))
|
|
191
|
+
.sort()
|
|
192
|
+
.reverse();
|
|
193
|
+
|
|
194
|
+
if (backups.length === 0) {
|
|
195
|
+
Logger.warning('没有找到备份文件');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(chalk.blue('\n备份列表:'));
|
|
200
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
201
|
+
|
|
202
|
+
for (const backup of backups) {
|
|
203
|
+
const backupPath = path.join(finalDir, backup);
|
|
204
|
+
const stat = await fs.stat(backupPath);
|
|
205
|
+
const size = (stat.size / 1024).toFixed(2);
|
|
206
|
+
console.log(chalk.white(` ${backup}`));
|
|
207
|
+
console.log(chalk.gray(` 大小: ${size} KB | 时间: ${stat.mtime.toLocaleString()}`));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
211
|
+
console.log(chalk.blue(`总计: ${backups.length} 个备份`));
|
|
212
|
+
console.log(chalk.gray(`备份目录: ${finalDir}`));
|
|
213
|
+
|
|
214
|
+
} catch (error) {
|
|
215
|
+
Logger.error(`列出备份失败: ${error.message}`);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 从备份恢复
|
|
222
|
+
* @param {string} backupFile - 备份文件路径或名称
|
|
223
|
+
*/
|
|
224
|
+
async restore(backupFile) {
|
|
225
|
+
try {
|
|
226
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
227
|
+
const defaultBackupDir = path.join(homeDir, '.akm-backups');
|
|
228
|
+
|
|
229
|
+
let backupPath;
|
|
230
|
+
if (path.isAbsolute(backupFile)) {
|
|
231
|
+
backupPath = backupFile;
|
|
232
|
+
} else if (backupFile.includes('/') || backupFile.includes('\\')) {
|
|
233
|
+
backupPath = path.join(process.cwd(), backupFile);
|
|
234
|
+
} else {
|
|
235
|
+
// 假设是备份目录中的文件名
|
|
236
|
+
backupPath = path.join(defaultBackupDir, backupFile);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!await fs.pathExists(backupPath)) {
|
|
240
|
+
Logger.error(`备份文件不存在: ${backupPath}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 先备份当前配置
|
|
245
|
+
await this.backup();
|
|
246
|
+
|
|
247
|
+
// 恢复备份
|
|
248
|
+
const configPath = this.configManager.configPath;
|
|
249
|
+
await fs.copy(backupPath, configPath);
|
|
250
|
+
|
|
251
|
+
Logger.success(`配置已从备份恢复: ${backupPath}`);
|
|
252
|
+
|
|
253
|
+
} catch (error) {
|
|
254
|
+
Logger.error(`恢复备份失败: ${error.message}`);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 清理旧备份
|
|
261
|
+
*/
|
|
262
|
+
async cleanOldBackups(backupDir, keepCount) {
|
|
263
|
+
const files = await fs.readdir(backupDir);
|
|
264
|
+
const backups = files
|
|
265
|
+
.filter(f => f.startsWith('akm-backup-') && f.endsWith('.json'))
|
|
266
|
+
.sort()
|
|
267
|
+
.reverse();
|
|
268
|
+
|
|
269
|
+
if (backups.length > keepCount) {
|
|
270
|
+
const toDelete = backups.slice(keepCount);
|
|
271
|
+
for (const file of toDelete) {
|
|
272
|
+
await fs.remove(path.join(backupDir, file));
|
|
273
|
+
}
|
|
274
|
+
if (toDelete.length > 0) {
|
|
275
|
+
Logger.info(`已清理 ${toDelete.length} 个旧备份`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Token 脱敏处理
|
|
282
|
+
*/
|
|
283
|
+
maskTokens(providers) {
|
|
284
|
+
const masked = {};
|
|
285
|
+
for (const [name, provider] of Object.entries(providers)) {
|
|
286
|
+
masked[name] = {
|
|
287
|
+
...provider,
|
|
288
|
+
authToken: provider.authToken ? this.maskToken(provider.authToken) : null
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return masked;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
maskToken(token) {
|
|
295
|
+
if (!token || token.length < 10) return '***';
|
|
296
|
+
return token.substring(0, 8) + '***' + token.substring(token.length - 4);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
getTimestamp() {
|
|
300
|
+
const now = new Date();
|
|
301
|
+
return now.toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function exportCommand(outputPath, options) {
|
|
306
|
+
const manager = new BackupManager();
|
|
307
|
+
await manager.export(outputPath, options);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function importCommand(inputPath, options) {
|
|
311
|
+
const manager = new BackupManager();
|
|
312
|
+
await manager.import(inputPath, options);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function backupCommand(options) {
|
|
316
|
+
const manager = new BackupManager();
|
|
317
|
+
|
|
318
|
+
if (options.list) {
|
|
319
|
+
await manager.listBackups(options.dir);
|
|
320
|
+
} else if (options.restore) {
|
|
321
|
+
await manager.restore(options.restore);
|
|
322
|
+
} else {
|
|
323
|
+
await manager.backup(options.dir);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = { exportCommand, importCommand, backupCommand, BackupManager };
|
package/src/commands/switch.js
CHANGED
|
@@ -93,6 +93,13 @@ class EnvSwitcher extends BaseCommand {
|
|
|
93
93
|
|
|
94
94
|
this.removeESCListener(escListener);
|
|
95
95
|
|
|
96
|
+
// 检查互斥参数
|
|
97
|
+
const conflictError = this.checkExclusiveArgs(answers.selectedArgs, availableArgs);
|
|
98
|
+
if (conflictError) {
|
|
99
|
+
Logger.warning(conflictError);
|
|
100
|
+
return await this.showLaunchArgsSelection(providerName);
|
|
101
|
+
}
|
|
102
|
+
|
|
96
103
|
// 选择参数后直接启动
|
|
97
104
|
await this.launchProvider(provider, answers.selectedArgs);
|
|
98
105
|
|
|
@@ -274,6 +281,28 @@ class EnvSwitcher extends BaseCommand {
|
|
|
274
281
|
];
|
|
275
282
|
}
|
|
276
283
|
|
|
284
|
+
checkExclusiveArgs(selectedArgs, availableArgs) {
|
|
285
|
+
if (!selectedArgs || selectedArgs.length < 2) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const argDef of availableArgs) {
|
|
290
|
+
if (!argDef.exclusive || !selectedArgs.includes(argDef.name)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const exclusiveArg of argDef.exclusive) {
|
|
295
|
+
if (selectedArgs.includes(exclusiveArg)) {
|
|
296
|
+
const arg1 = availableArgs.find(a => a.name === argDef.name);
|
|
297
|
+
const arg2 = availableArgs.find(a => a.name === exclusiveArg);
|
|
298
|
+
return `"${arg1?.label || argDef.name}" 和 "${arg2?.label || exclusiveArg}" 不能同时选择`;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
277
306
|
getCodexLaunchArgs() {
|
|
278
307
|
return [
|
|
279
308
|
{
|