@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 CHANGED
@@ -1,123 +1,450 @@
1
- # API Key Manager
1
+ # API Key Manager (akm)
2
2
 
3
- 一个简洁而强大的 CLI 工具,用于管理和快速切换多个 API 提供商配置。
3
+ 一个强大的 CLI 工具,用于管理和快速切换 **Claude Code** 和 **Codex CLI** 的 API 配置。
4
4
 
5
- ## 功能特性
5
+ [![npm version](https://img.shields.io/npm/v/@pikecode/api-key-manager.svg)](https://www.npmjs.com/package/@pikecode/api-key-manager)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
7
 
7
- -**快速切换** - 一键切换不同的 API 提供商配置
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
- # 切换 API 提供商
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
- 配置文件位置:`~/.akm-config.json`
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": "provider-name",
285
+ "currentProvider": "my-claude",
59
286
  "providers": {
60
- "claude-provider": {
61
- "name": "claude-provider",
62
- "displayName": "Claude Code Provider",
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
- "claude-api-key": {
73
- "name": "claude-api-key",
74
- "displayName": "Claude Code - API Key",
75
- "authMode": "api_key",
76
- "authToken": "sk-ant-...",
77
- "baseUrl": "https://api.anthropic.com",
78
- "tokenType": "api_key",
79
- "models": {
80
- "primary": "claude-sonnet-4",
81
- "smallFast": "claude-haiku-4"
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
- ## 支持的 IDE
319
+ ## 🎯 使用场景
89
320
 
90
- ### Claude Code - Anthropic 官方代码编辑器
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
- | 模式 | IDE | 说明 |
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
- - **ESC** - 返回上级菜单
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
- MIT
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
- - GitHub: https://github.com/pikecode/api-key-manager
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikecode/api-key-manager",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "A CLI tool for managing and switching multiple API provider configurations",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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 };
@@ -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
  {