ai-account-switch 1.7.1 → 1.9.0
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 +40 -6
- package/README_EN.md +40 -6
- package/package.json +1 -1
- package/src/commands/account.js +155 -23
- package/src/config.js +258 -8
- package/src/index.js +27 -24
- package/src/ui-server.js +46 -0
- package/CHANGELOG-v1.7.0.md +0 -176
package/README.md
CHANGED
|
@@ -73,11 +73,11 @@ npm link
|
|
|
73
73
|
| 命令 | 别名 | 描述 |
|
|
74
74
|
|------|------|------|
|
|
75
75
|
| `ais add [name]` | - | 添加新的账户配置 |
|
|
76
|
-
| `ais list` | `ls` |
|
|
77
|
-
| `ais use [name]` | - | 为当前项目设置账户 |
|
|
76
|
+
| `ais list` | `ls` | 列出所有可用账户(含ID) |
|
|
77
|
+
| `ais use [name-or-id]` | - | 通过名称或ID为当前项目设置账户 |
|
|
78
78
|
| `ais info` | - | 显示当前项目的账户信息 |
|
|
79
79
|
| `ais current` | - | 显示当前账户名称 |
|
|
80
|
-
| `ais remove [name]` | `rm` | 删除账户 |
|
|
80
|
+
| `ais remove [name-or-id]` | `rm` | 通过名称或ID删除账户 |
|
|
81
81
|
| `ais model list` | `ls` | 列出当前账户的所有模型组 |
|
|
82
82
|
| `ais model add [name]` | - | 添加新的模型组 |
|
|
83
83
|
| `ais model use <name>` | - | 切换到不同的模型组 |
|
|
@@ -95,7 +95,7 @@ npm link
|
|
|
95
95
|
| `ais ui` | - | 启动 Web UI 管理界面 |
|
|
96
96
|
| `ais paths` | - | 显示配置文件路径 |
|
|
97
97
|
| `ais doctor` | - | 诊断 Claude Code 配置问题 |
|
|
98
|
-
| `ais export <name>` | - | 导出账户为 JSON 格式 |
|
|
98
|
+
| `ais export <name-or-id>` | - | 通过名称或ID导出账户为 JSON 格式 |
|
|
99
99
|
| `ais help` | - | 显示帮助信息 |
|
|
100
100
|
| `ais --version` | - | 显示版本号 |
|
|
101
101
|
|
|
@@ -128,7 +128,7 @@ ais add my-claude-account
|
|
|
128
128
|
|
|
129
129
|
#### 2. 列出所有账户
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
查看所有已配置的账户及其ID:
|
|
132
132
|
|
|
133
133
|
```bash
|
|
134
134
|
ais list
|
|
@@ -136,14 +136,33 @@ ais list
|
|
|
136
136
|
ais ls
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
输出示例:
|
|
140
|
+
```
|
|
141
|
+
📋 Available Accounts (可用账号):
|
|
142
|
+
|
|
143
|
+
● [1] my-claude-account
|
|
144
|
+
Type: Claude
|
|
145
|
+
API Key: sk-a****xyz
|
|
146
|
+
...
|
|
147
|
+
|
|
148
|
+
[2] work-codex
|
|
149
|
+
Type: Codex
|
|
150
|
+
API Key: sk-b****abc
|
|
151
|
+
...
|
|
152
|
+
```
|
|
153
|
+
|
|
139
154
|
当前项目激活的账户将用绿色圆点标记。
|
|
140
155
|
|
|
141
156
|
#### 3. 为当前项目切换账户
|
|
142
157
|
|
|
143
|
-
|
|
158
|
+
设置当前项目使用的账户(可以使用账户名或ID):
|
|
144
159
|
|
|
145
160
|
```bash
|
|
161
|
+
# 使用账户名
|
|
146
162
|
ais use my-claude-account
|
|
163
|
+
|
|
164
|
+
# 或使用ID(更快捷)
|
|
165
|
+
ais use 1
|
|
147
166
|
```
|
|
148
167
|
|
|
149
168
|
或交互式选择:
|
|
@@ -1006,6 +1025,21 @@ MIT License - 欢迎在你的项目中使用此工具!
|
|
|
1006
1025
|
|
|
1007
1026
|
## 更新日志
|
|
1008
1027
|
|
|
1028
|
+
### v1.9.0
|
|
1029
|
+
- **账号ID功能**:
|
|
1030
|
+
- 为每个账号自动分配唯一数字ID(从1开始递增)
|
|
1031
|
+
- 支持通过ID快速切换账号:`ais use 1`
|
|
1032
|
+
- 支持通过ID删除账号:`ais remove 2`
|
|
1033
|
+
- 支持通过ID导出账号:`ais export 3`
|
|
1034
|
+
- 账号列表展示格式优化:`[ID] AccountName`
|
|
1035
|
+
- ID使用黄色高亮显示,更加清晰可见
|
|
1036
|
+
- 完全向后兼容:已有账号自动分配ID
|
|
1037
|
+
- 交互式选择账号时同时显示ID和名称
|
|
1038
|
+
- **文档更新**:
|
|
1039
|
+
- 更新所有命令说明,添加ID使用示例
|
|
1040
|
+
- 中英文README同步更新
|
|
1041
|
+
- Help命令输出优化
|
|
1042
|
+
|
|
1009
1043
|
### v1.7.0
|
|
1010
1044
|
- **MCP Web UI 管理**:
|
|
1011
1045
|
- 通过 Web UI 完整管理 MCP 服务器
|
package/README_EN.md
CHANGED
|
@@ -66,11 +66,11 @@ npm link
|
|
|
66
66
|
| Command | Alias | Description |
|
|
67
67
|
|---------|-------|-------------|
|
|
68
68
|
| `ais add [name]` | - | Add a new account configuration |
|
|
69
|
-
| `ais list` | `ls` | List all available accounts |
|
|
70
|
-
| `ais use [name]` | - | Set account for current project |
|
|
69
|
+
| `ais list` | `ls` | List all available accounts with IDs |
|
|
70
|
+
| `ais use [name-or-id]` | - | Set account for current project by name or ID |
|
|
71
71
|
| `ais info` | - | Show current project's account info |
|
|
72
72
|
| `ais current` | - | Show current account name |
|
|
73
|
-
| `ais remove [name]` | `rm` | Remove an account |
|
|
73
|
+
| `ais remove [name-or-id]` | `rm` | Remove an account by name or ID |
|
|
74
74
|
| `ais model list` | `ls` | List all model groups for current account |
|
|
75
75
|
| `ais model add [name]` | - | Add a new model group |
|
|
76
76
|
| `ais model use <name>` | - | Switch to a different model group |
|
|
@@ -88,7 +88,7 @@ npm link
|
|
|
88
88
|
| `ais ui` | - | Start web-based UI manager |
|
|
89
89
|
| `ais paths` | - | Show configuration file paths |
|
|
90
90
|
| `ais doctor` | - | Diagnose configuration issues |
|
|
91
|
-
| `ais export <name>` | - | Export account as JSON |
|
|
91
|
+
| `ais export <name-or-id>` | - | Export account as JSON by name or ID |
|
|
92
92
|
| `ais help` | - | Display help information |
|
|
93
93
|
| `ais --version` | - | Show version number |
|
|
94
94
|
|
|
@@ -122,7 +122,7 @@ You'll be prompted to enter:
|
|
|
122
122
|
|
|
123
123
|
#### 2. List All Accounts
|
|
124
124
|
|
|
125
|
-
View all your configured accounts:
|
|
125
|
+
View all your configured accounts with their IDs:
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
128
|
ais list
|
|
@@ -130,14 +130,33 @@ ais list
|
|
|
130
130
|
ais ls
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
Example output:
|
|
134
|
+
```
|
|
135
|
+
📋 Available Accounts:
|
|
136
|
+
|
|
137
|
+
● [1] my-claude-account
|
|
138
|
+
Type: Claude
|
|
139
|
+
API Key: sk-a****xyz
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
[2] work-codex
|
|
143
|
+
Type: Codex
|
|
144
|
+
API Key: sk-b****abc
|
|
145
|
+
...
|
|
146
|
+
```
|
|
147
|
+
|
|
133
148
|
The active account for the current project will be marked with a green dot.
|
|
134
149
|
|
|
135
150
|
#### 3. Switch Account for Current Project
|
|
136
151
|
|
|
137
|
-
Set which account to use in your current project:
|
|
152
|
+
Set which account to use in your current project (by name or ID):
|
|
138
153
|
|
|
139
154
|
```bash
|
|
155
|
+
# Use account by name
|
|
140
156
|
ais use my-claude-account
|
|
157
|
+
|
|
158
|
+
# Or use account by ID (faster)
|
|
159
|
+
ais use 1
|
|
141
160
|
```
|
|
142
161
|
|
|
143
162
|
Or select interactively:
|
|
@@ -997,6 +1016,21 @@ MIT License - feel free to use this tool in your projects!
|
|
|
997
1016
|
|
|
998
1017
|
## Changelog
|
|
999
1018
|
|
|
1019
|
+
### v1.9.0
|
|
1020
|
+
- **Account ID Feature**:
|
|
1021
|
+
- Automatically assign unique numeric ID to each account (starting from 1)
|
|
1022
|
+
- Quick account switching by ID: `ais use 1`
|
|
1023
|
+
- Remove account by ID: `ais remove 2`
|
|
1024
|
+
- Export account by ID: `ais export 3`
|
|
1025
|
+
- Optimized account list display format: `[ID] AccountName`
|
|
1026
|
+
- ID displayed in yellow color for better visibility
|
|
1027
|
+
- Fully backward compatible: existing accounts automatically assigned IDs
|
|
1028
|
+
- Interactive selection shows both ID and name
|
|
1029
|
+
- **Documentation Updates**:
|
|
1030
|
+
- Updated all command descriptions with ID usage examples
|
|
1031
|
+
- Synchronized Chinese and English README
|
|
1032
|
+
- Optimized help command output
|
|
1033
|
+
|
|
1000
1034
|
### v1.7.0
|
|
1001
1035
|
- **MCP Web UI Management**:
|
|
1002
1036
|
- Complete MCP server management through Web UI
|
package/package.json
CHANGED
package/src/commands/account.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const chalk = require("chalk");
|
|
2
2
|
const inquirer = require("inquirer");
|
|
3
3
|
const ConfigManager = require("../config");
|
|
4
|
+
const { WIRE_API_MODES, DEFAULT_WIRE_API, ACCOUNT_TYPES } = require("../config");
|
|
4
5
|
const { maskApiKey } = require("./helpers");
|
|
5
6
|
const { promptForModelGroup } = require("./model");
|
|
6
7
|
|
|
@@ -53,6 +54,9 @@ async function addAccount(name, options) {
|
|
|
53
54
|
},
|
|
54
55
|
]);
|
|
55
56
|
|
|
57
|
+
// Store wire_api selection for Codex accounts
|
|
58
|
+
let wireApiSelection = null;
|
|
59
|
+
|
|
56
60
|
// Show configuration tips based on account type
|
|
57
61
|
if (typeAnswer.type === "Codex") {
|
|
58
62
|
console.log(
|
|
@@ -83,6 +87,44 @@ async function addAccount(name, options) {
|
|
|
83
87
|
" • Codex uses OpenAI-compatible API format (Codex 使用 OpenAI 兼容的 API 格式)\n"
|
|
84
88
|
)
|
|
85
89
|
);
|
|
90
|
+
|
|
91
|
+
// Prompt for wire_api mode selection
|
|
92
|
+
const wireApiAnswer = await inquirer.prompt([
|
|
93
|
+
{
|
|
94
|
+
type: "list",
|
|
95
|
+
name: "wireApi",
|
|
96
|
+
message: "Select wire_api mode (请选择 wire_api 模式):",
|
|
97
|
+
choices: [
|
|
98
|
+
{
|
|
99
|
+
name: "chat - Use API key in HTTP headers (OpenAI-compatible) (在 HTTP 头中使用 API key,OpenAI 兼容)",
|
|
100
|
+
value: WIRE_API_MODES.CHAT
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "responses - Use API key in auth.json (requires_openai_auth) (在 auth.json 中使用 API key)",
|
|
104
|
+
value: WIRE_API_MODES.RESPONSES
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
default: DEFAULT_WIRE_API
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
wireApiSelection = wireApiAnswer.wireApi;
|
|
112
|
+
|
|
113
|
+
// Validate input
|
|
114
|
+
if (!Object.values(WIRE_API_MODES).includes(wireApiSelection)) {
|
|
115
|
+
console.log(
|
|
116
|
+
chalk.yellow(
|
|
117
|
+
`⚠ Invalid wire_api mode, using default: ${DEFAULT_WIRE_API} (无效的模式,使用默认值)`
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
wireApiSelection = DEFAULT_WIRE_API;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(
|
|
124
|
+
chalk.cyan(
|
|
125
|
+
`\n✓ Selected wire_api mode (已选择模式): ${wireApiSelection}\n`
|
|
126
|
+
)
|
|
127
|
+
);
|
|
86
128
|
} else if (typeAnswer.type === "Droids") {
|
|
87
129
|
console.log(
|
|
88
130
|
chalk.cyan("\n📝 Droids Configuration Tips (Droids 配置提示):")
|
|
@@ -165,6 +207,11 @@ async function addAccount(name, options) {
|
|
|
165
207
|
// Merge type into accountData
|
|
166
208
|
accountData.type = typeAnswer.type;
|
|
167
209
|
|
|
210
|
+
// Add wire_api selection for Codex accounts
|
|
211
|
+
if (typeAnswer.type === "Codex" && wireApiSelection) {
|
|
212
|
+
accountData.wireApi = wireApiSelection;
|
|
213
|
+
}
|
|
214
|
+
|
|
168
215
|
// Handle custom environment variables
|
|
169
216
|
if (accountData.addCustomEnv) {
|
|
170
217
|
accountData.customEnv = {};
|
|
@@ -454,6 +501,27 @@ async function addAccount(name, options) {
|
|
|
454
501
|
)
|
|
455
502
|
);
|
|
456
503
|
console.log(chalk.cyan(` ais use ${name}\n`));
|
|
504
|
+
|
|
505
|
+
// Show wire_api mode specific information
|
|
506
|
+
if (accountData.wireApi === WIRE_API_MODES.RESPONSES) {
|
|
507
|
+
console.log(
|
|
508
|
+
chalk.yellow(
|
|
509
|
+
" ⚠ Note: This account uses 'responses' mode (此账号使用 'responses' 模式)"
|
|
510
|
+
)
|
|
511
|
+
);
|
|
512
|
+
console.log(
|
|
513
|
+
chalk.white(
|
|
514
|
+
" Your API key will be stored in ~/.codex/auth.json (API key 将存储在 ~/.codex/auth.json)\n"
|
|
515
|
+
)
|
|
516
|
+
);
|
|
517
|
+
} else {
|
|
518
|
+
console.log(
|
|
519
|
+
chalk.cyan(
|
|
520
|
+
" ✓ This account uses 'chat' mode (此账号使用 'chat' 模式)\n"
|
|
521
|
+
)
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
457
525
|
console.log(
|
|
458
526
|
chalk.white(
|
|
459
527
|
"2. Use Codex with the generated profile (使用生成的配置文件运行 Codex):"
|
|
@@ -555,13 +623,17 @@ function listAccounts() {
|
|
|
555
623
|
const account = accounts[name];
|
|
556
624
|
const isActive = currentProject && currentProject.name === name;
|
|
557
625
|
const marker = isActive ? chalk.green("● ") : " ";
|
|
626
|
+
|
|
627
|
+
// Display ID and name
|
|
628
|
+
const idDisplay = chalk.yellow(`[${account.id}]`);
|
|
558
629
|
const nameDisplay = isActive
|
|
559
630
|
? chalk.green.bold(name)
|
|
560
631
|
: chalk.cyan(name);
|
|
561
632
|
|
|
562
|
-
console.log(`${marker}${nameDisplay}`);
|
|
633
|
+
console.log(`${marker}${idDisplay} ${nameDisplay}`);
|
|
563
634
|
console.log(` Type: ${account.type}`);
|
|
564
635
|
console.log(` API Key: ${maskApiKey(account.apiKey)}`);
|
|
636
|
+
if (account.apiUrl) console.log(` API URL: ${account.apiUrl}`);
|
|
565
637
|
if (account.email) console.log(` Email: ${account.email}`);
|
|
566
638
|
if (account.description)
|
|
567
639
|
console.log(` Description: ${account.description}`);
|
|
@@ -589,6 +661,11 @@ function listAccounts() {
|
|
|
589
661
|
) {
|
|
590
662
|
console.log(` Model: ${account.model}`);
|
|
591
663
|
}
|
|
664
|
+
// Display wire_api configuration for Codex accounts
|
|
665
|
+
if (account.type === ACCOUNT_TYPES.CODEX) {
|
|
666
|
+
const wireApi = account.wireApi || `${DEFAULT_WIRE_API} (default)`;
|
|
667
|
+
console.log(` Wire API: ${wireApi}`);
|
|
668
|
+
}
|
|
592
669
|
console.log(
|
|
593
670
|
` Created: ${new Date(account.createdAt).toLocaleString()}`
|
|
594
671
|
);
|
|
@@ -613,9 +690,9 @@ function listAccounts() {
|
|
|
613
690
|
/**
|
|
614
691
|
* Switch to a specific account for current project
|
|
615
692
|
*/
|
|
616
|
-
async function useAccount(
|
|
617
|
-
if (!
|
|
618
|
-
// If no name provided, show interactive selection
|
|
693
|
+
async function useAccount(nameOrId) {
|
|
694
|
+
if (!nameOrId) {
|
|
695
|
+
// If no name/ID provided, show interactive selection
|
|
619
696
|
const accounts = config.getAllAccounts();
|
|
620
697
|
const accountNames = Object.keys(accounts);
|
|
621
698
|
|
|
@@ -628,21 +705,32 @@ async function useAccount(name) {
|
|
|
628
705
|
return;
|
|
629
706
|
}
|
|
630
707
|
|
|
708
|
+
// Create choices with ID and name
|
|
709
|
+
const choices = accountNames.map(name => {
|
|
710
|
+
const account = accounts[name];
|
|
711
|
+
return {
|
|
712
|
+
name: `[${account.id}] ${name}`,
|
|
713
|
+
value: name
|
|
714
|
+
};
|
|
715
|
+
});
|
|
716
|
+
|
|
631
717
|
const answers = await inquirer.prompt([
|
|
632
718
|
{
|
|
633
719
|
type: "list",
|
|
634
720
|
name: "accountName",
|
|
635
721
|
message: "Select an account to use (请选择要使用的账号):",
|
|
636
|
-
choices:
|
|
722
|
+
choices: choices,
|
|
637
723
|
},
|
|
638
724
|
]);
|
|
639
725
|
|
|
640
|
-
|
|
726
|
+
nameOrId = answers.accountName;
|
|
641
727
|
}
|
|
642
728
|
|
|
643
|
-
|
|
729
|
+
// Find account by ID or name
|
|
730
|
+
const accountInfo = config.getAccountByIdOrName(nameOrId);
|
|
731
|
+
if (!accountInfo) {
|
|
644
732
|
console.log(
|
|
645
|
-
chalk.red(`✗ Account '${
|
|
733
|
+
chalk.red(`✗ Account '${nameOrId}' not found. (未找到账号 '${nameOrId}'。)`)
|
|
646
734
|
);
|
|
647
735
|
console.log(
|
|
648
736
|
chalk.yellow(
|
|
@@ -652,6 +740,7 @@ async function useAccount(name) {
|
|
|
652
740
|
return;
|
|
653
741
|
}
|
|
654
742
|
|
|
743
|
+
const name = accountInfo.name;
|
|
655
744
|
const success = config.setProjectAccount(name);
|
|
656
745
|
if (success) {
|
|
657
746
|
const fs = require("fs");
|
|
@@ -688,6 +777,27 @@ async function useAccount(name) {
|
|
|
688
777
|
`✓ Codex profile created (Codex 配置文件已创建): ${profileName}`
|
|
689
778
|
)
|
|
690
779
|
);
|
|
780
|
+
|
|
781
|
+
// Display wire_api mode information
|
|
782
|
+
if (account.wireApi === WIRE_API_MODES.RESPONSES) {
|
|
783
|
+
console.log(
|
|
784
|
+
chalk.yellow(
|
|
785
|
+
`✓ Wire API mode: ${WIRE_API_MODES.RESPONSES} (使用 ${WIRE_API_MODES.RESPONSES} 模式)`
|
|
786
|
+
)
|
|
787
|
+
);
|
|
788
|
+
console.log(
|
|
789
|
+
chalk.yellow(
|
|
790
|
+
`✓ API key stored in ~/.codex/auth.json (API key 已存储在 ~/.codex/auth.json)`
|
|
791
|
+
)
|
|
792
|
+
);
|
|
793
|
+
} else {
|
|
794
|
+
console.log(
|
|
795
|
+
chalk.cyan(
|
|
796
|
+
`✓ Wire API mode: ${WIRE_API_MODES.CHAT} (使用 ${WIRE_API_MODES.CHAT} 模式)`
|
|
797
|
+
)
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
691
801
|
console.log("");
|
|
692
802
|
console.log(chalk.bold.cyan("📖 Next Steps (下一步):"));
|
|
693
803
|
console.log(
|
|
@@ -825,6 +935,7 @@ function showInfo() {
|
|
|
825
935
|
console.log(
|
|
826
936
|
chalk.bold("\n📌 Current Project Account Info (当前项目账号信息):\n")
|
|
827
937
|
);
|
|
938
|
+
console.log(`${chalk.cyan("Account ID:")} ${chalk.yellow(`[${projectAccount.id}]`)}`);
|
|
828
939
|
console.log(
|
|
829
940
|
`${chalk.cyan("Account Name:")} ${chalk.green.bold(
|
|
830
941
|
projectAccount.name
|
|
@@ -878,6 +989,10 @@ function showInfo() {
|
|
|
878
989
|
) {
|
|
879
990
|
console.log(`${chalk.cyan("Model:")} ${projectAccount.model}`);
|
|
880
991
|
}
|
|
992
|
+
// Display wire_api configuration for Codex accounts
|
|
993
|
+
if (projectAccount.type === ACCOUNT_TYPES.CODEX && projectAccount.wireApi) {
|
|
994
|
+
console.log(`${chalk.cyan("Wire API:")} ${projectAccount.wireApi}`);
|
|
995
|
+
}
|
|
881
996
|
console.log(
|
|
882
997
|
`${chalk.cyan("Set At:")} ${new Date(
|
|
883
998
|
projectAccount.setAt
|
|
@@ -890,8 +1005,8 @@ function showInfo() {
|
|
|
890
1005
|
/**
|
|
891
1006
|
* Remove an account
|
|
892
1007
|
*/
|
|
893
|
-
async function removeAccount(
|
|
894
|
-
if (!
|
|
1008
|
+
async function removeAccount(nameOrId) {
|
|
1009
|
+
if (!nameOrId) {
|
|
895
1010
|
const accounts = config.getAllAccounts();
|
|
896
1011
|
const accountNames = Object.keys(accounts);
|
|
897
1012
|
|
|
@@ -900,30 +1015,43 @@ async function removeAccount(name) {
|
|
|
900
1015
|
return;
|
|
901
1016
|
}
|
|
902
1017
|
|
|
1018
|
+
// Create choices with ID and name
|
|
1019
|
+
const choices = accountNames.map(name => {
|
|
1020
|
+
const account = accounts[name];
|
|
1021
|
+
return {
|
|
1022
|
+
name: `[${account.id}] ${name}`,
|
|
1023
|
+
value: name
|
|
1024
|
+
};
|
|
1025
|
+
});
|
|
1026
|
+
|
|
903
1027
|
const answers = await inquirer.prompt([
|
|
904
1028
|
{
|
|
905
1029
|
type: "list",
|
|
906
1030
|
name: "accountName",
|
|
907
1031
|
message: "Select an account to remove (请选择要删除的账号):",
|
|
908
|
-
choices:
|
|
1032
|
+
choices: choices,
|
|
909
1033
|
},
|
|
910
1034
|
]);
|
|
911
1035
|
|
|
912
|
-
|
|
1036
|
+
nameOrId = answers.accountName;
|
|
913
1037
|
}
|
|
914
1038
|
|
|
915
|
-
|
|
1039
|
+
// Find account by ID or name
|
|
1040
|
+
const accountInfo = config.getAccountByIdOrName(nameOrId);
|
|
1041
|
+
if (!accountInfo) {
|
|
916
1042
|
console.log(
|
|
917
|
-
chalk.red(`✗ Account '${
|
|
1043
|
+
chalk.red(`✗ Account '${nameOrId}' not found. (未找到账号 '${nameOrId}'。)`)
|
|
918
1044
|
);
|
|
919
1045
|
return;
|
|
920
1046
|
}
|
|
921
1047
|
|
|
1048
|
+
const name = accountInfo.name;
|
|
1049
|
+
|
|
922
1050
|
const { confirm } = await inquirer.prompt([
|
|
923
1051
|
{
|
|
924
1052
|
type: "confirm",
|
|
925
1053
|
name: "confirm",
|
|
926
|
-
message: `Are you sure you want to remove account '${name}'? (确定要删除账号 '${name}' 吗?)`,
|
|
1054
|
+
message: `Are you sure you want to remove account '${name}' (ID: ${accountInfo.id})? (确定要删除账号 '${name}' (ID: ${accountInfo.id}) 吗?)`,
|
|
927
1055
|
default: false,
|
|
928
1056
|
},
|
|
929
1057
|
]);
|
|
@@ -970,30 +1098,34 @@ function showCurrent() {
|
|
|
970
1098
|
/**
|
|
971
1099
|
* Export account configuration
|
|
972
1100
|
*/
|
|
973
|
-
function exportAccount(
|
|
974
|
-
if (!
|
|
1101
|
+
function exportAccount(nameOrId) {
|
|
1102
|
+
if (!nameOrId) {
|
|
975
1103
|
console.log(
|
|
976
|
-
chalk.red("Please specify an account name. (
|
|
1104
|
+
chalk.red("Please specify an account name or ID. (请指定账号名称或 ID。)")
|
|
977
1105
|
);
|
|
978
1106
|
console.log(
|
|
979
1107
|
chalk.cyan(
|
|
980
|
-
"Usage: ais export <account-name> (用法: ais export
|
|
1108
|
+
"Usage: ais export <account-name-or-id> (用法: ais export <账号名或ID>)"
|
|
981
1109
|
)
|
|
982
1110
|
);
|
|
983
1111
|
return;
|
|
984
1112
|
}
|
|
985
1113
|
|
|
986
|
-
|
|
987
|
-
|
|
1114
|
+
// Find account by ID or name
|
|
1115
|
+
const accountInfo = config.getAccountByIdOrName(nameOrId);
|
|
1116
|
+
if (!accountInfo) {
|
|
988
1117
|
console.log(
|
|
989
|
-
chalk.red(`✗ Account '${
|
|
1118
|
+
chalk.red(`✗ Account '${nameOrId}' not found. (未找到账号 '${nameOrId}'。)`)
|
|
990
1119
|
);
|
|
991
1120
|
return;
|
|
992
1121
|
}
|
|
993
1122
|
|
|
1123
|
+
const name = accountInfo.name;
|
|
1124
|
+
const { name: _, ...account } = accountInfo; // Remove the name property from accountInfo
|
|
1125
|
+
|
|
994
1126
|
console.log(
|
|
995
1127
|
chalk.bold(
|
|
996
|
-
`\n📤 Export for account '${name}' (账号 '${name}' 的导出数据):\n`
|
|
1128
|
+
`\n📤 Export for account '${name}' (ID: ${account.id}) (账号 '${name}' 的导出数据):\n`
|
|
997
1129
|
)
|
|
998
1130
|
);
|
|
999
1131
|
console.log(JSON.stringify({ [name]: account }, null, 2));
|
package/src/config.js
CHANGED
|
@@ -2,6 +2,22 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
|
|
5
|
+
// Constants for wire API modes
|
|
6
|
+
const WIRE_API_MODES = {
|
|
7
|
+
CHAT: 'chat',
|
|
8
|
+
RESPONSES: 'responses'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DEFAULT_WIRE_API = WIRE_API_MODES.CHAT;
|
|
12
|
+
|
|
13
|
+
// Constants for account types
|
|
14
|
+
const ACCOUNT_TYPES = {
|
|
15
|
+
CLAUDE: 'Claude',
|
|
16
|
+
CODEX: 'Codex',
|
|
17
|
+
CCR: 'CCR',
|
|
18
|
+
DROIDS: 'Droids'
|
|
19
|
+
};
|
|
20
|
+
|
|
5
21
|
/**
|
|
6
22
|
* Cross-platform configuration manager
|
|
7
23
|
* Stores global accounts in user home directory
|
|
@@ -30,8 +46,11 @@ class ConfigManager {
|
|
|
30
46
|
|
|
31
47
|
// Create global config file if it doesn't exist
|
|
32
48
|
if (!fs.existsSync(this.globalConfigFile)) {
|
|
33
|
-
this.saveGlobalConfig({ accounts: {}, mcpServers: {} });
|
|
49
|
+
this.saveGlobalConfig({ accounts: {}, mcpServers: {}, nextAccountId: 1 });
|
|
34
50
|
}
|
|
51
|
+
|
|
52
|
+
// Migrate existing accounts to have IDs
|
|
53
|
+
this.migrateAccountIds();
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
/**
|
|
@@ -66,9 +85,14 @@ class ConfigManager {
|
|
|
66
85
|
readGlobalConfig() {
|
|
67
86
|
try {
|
|
68
87
|
const data = fs.readFileSync(this.globalConfigFile, 'utf8');
|
|
69
|
-
|
|
88
|
+
const config = JSON.parse(data);
|
|
89
|
+
// Ensure nextAccountId exists
|
|
90
|
+
if (!config.nextAccountId) {
|
|
91
|
+
config.nextAccountId = 1;
|
|
92
|
+
}
|
|
93
|
+
return config;
|
|
70
94
|
} catch (error) {
|
|
71
|
-
return { accounts: {}, mcpServers: {} };
|
|
95
|
+
return { accounts: {}, mcpServers: {}, nextAccountId: 1 };
|
|
72
96
|
}
|
|
73
97
|
}
|
|
74
98
|
|
|
@@ -79,16 +103,84 @@ class ConfigManager {
|
|
|
79
103
|
fs.writeFileSync(this.globalConfigFile, JSON.stringify(config, null, 2), 'utf8');
|
|
80
104
|
}
|
|
81
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Migrate existing accounts to have IDs
|
|
108
|
+
* This ensures backward compatibility by assigning IDs to accounts that don't have one
|
|
109
|
+
*/
|
|
110
|
+
migrateAccountIds() {
|
|
111
|
+
const config = this.readGlobalConfig();
|
|
112
|
+
let needsSave = false;
|
|
113
|
+
|
|
114
|
+
// Ensure nextAccountId exists
|
|
115
|
+
if (!config.nextAccountId) {
|
|
116
|
+
config.nextAccountId = 1;
|
|
117
|
+
needsSave = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Assign IDs to accounts that don't have one
|
|
121
|
+
Object.keys(config.accounts || {}).forEach(name => {
|
|
122
|
+
if (!config.accounts[name].id) {
|
|
123
|
+
config.accounts[name].id = config.nextAccountId;
|
|
124
|
+
config.nextAccountId++;
|
|
125
|
+
needsSave = true;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (needsSave) {
|
|
130
|
+
this.saveGlobalConfig(config);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get account by ID or name
|
|
136
|
+
* @param {string|number} idOrName - Account ID or name
|
|
137
|
+
* @returns {Object|null} - Account object with name property, or null if not found
|
|
138
|
+
*/
|
|
139
|
+
getAccountByIdOrName(idOrName) {
|
|
140
|
+
const accounts = this.getAllAccounts();
|
|
141
|
+
|
|
142
|
+
// Try to parse as ID (number)
|
|
143
|
+
const id = parseInt(idOrName, 10);
|
|
144
|
+
if (!isNaN(id)) {
|
|
145
|
+
// Search by ID
|
|
146
|
+
for (const [name, account] of Object.entries(accounts)) {
|
|
147
|
+
if (account.id === id) {
|
|
148
|
+
return { name, ...account };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Search by name
|
|
154
|
+
const account = accounts[idOrName];
|
|
155
|
+
if (account) {
|
|
156
|
+
return { name: idOrName, ...account };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
82
162
|
/**
|
|
83
163
|
* Add or update an account
|
|
84
164
|
*/
|
|
85
165
|
addAccount(name, accountData) {
|
|
86
166
|
const config = this.readGlobalConfig();
|
|
167
|
+
|
|
168
|
+
// Assign ID for new accounts
|
|
169
|
+
const isNewAccount = !config.accounts[name];
|
|
170
|
+
const accountId = isNewAccount ? config.nextAccountId : config.accounts[name].id;
|
|
171
|
+
|
|
87
172
|
config.accounts[name] = {
|
|
88
173
|
...accountData,
|
|
174
|
+
id: accountId,
|
|
89
175
|
createdAt: config.accounts[name]?.createdAt || new Date().toISOString(),
|
|
90
176
|
updatedAt: new Date().toISOString()
|
|
91
177
|
};
|
|
178
|
+
|
|
179
|
+
// Increment nextAccountId only for new accounts
|
|
180
|
+
if (isNewAccount) {
|
|
181
|
+
config.nextAccountId++;
|
|
182
|
+
}
|
|
183
|
+
|
|
92
184
|
this.saveGlobalConfig(config);
|
|
93
185
|
return true;
|
|
94
186
|
}
|
|
@@ -587,11 +679,26 @@ class ConfigManager {
|
|
|
587
679
|
profileConfig += `base_url = "${baseUrl}"\n`;
|
|
588
680
|
}
|
|
589
681
|
|
|
590
|
-
// Determine wire_api
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
682
|
+
// Determine wire_api based on account configuration (default to chat for backward compatibility)
|
|
683
|
+
const wireApi = account.wireApi || DEFAULT_WIRE_API;
|
|
684
|
+
|
|
685
|
+
if (wireApi === WIRE_API_MODES.CHAT) {
|
|
686
|
+
// Chat mode: use HTTP headers for authentication
|
|
687
|
+
profileConfig += `wire_api = "${WIRE_API_MODES.CHAT}"\n`;
|
|
688
|
+
profileConfig += `http_headers = { "Authorization" = "Bearer ${account.apiKey}" }\n`;
|
|
689
|
+
|
|
690
|
+
// Note: We do NOT clear auth.json here because:
|
|
691
|
+
// 1. auth.json is a global file shared by all projects
|
|
692
|
+
// 2. Other projects may be using responses mode and need the API key
|
|
693
|
+
// 3. Chat mode doesn't use auth.json anyway, so no conflict exists
|
|
694
|
+
} else if (wireApi === WIRE_API_MODES.RESPONSES) {
|
|
695
|
+
// Responses mode: use auth.json for authentication
|
|
696
|
+
profileConfig += `wire_api = "${WIRE_API_MODES.RESPONSES}"\n`;
|
|
697
|
+
profileConfig += `requires_openai_auth = true\n`;
|
|
698
|
+
|
|
699
|
+
// Update auth.json with API key
|
|
700
|
+
this.updateCodexAuthJson(account.apiKey);
|
|
701
|
+
}
|
|
595
702
|
}
|
|
596
703
|
|
|
597
704
|
// Remove all old profiles with the same name (including duplicates)
|
|
@@ -620,6 +727,146 @@ class ConfigManager {
|
|
|
620
727
|
fs.writeFileSync(helperScript, profileName, 'utf8');
|
|
621
728
|
}
|
|
622
729
|
|
|
730
|
+
/**
|
|
731
|
+
* Read auth.json file
|
|
732
|
+
* @private
|
|
733
|
+
* @returns {Object} Parsed auth data or empty object
|
|
734
|
+
*/
|
|
735
|
+
_readAuthJson(authJsonFile) {
|
|
736
|
+
if (!fs.existsSync(authJsonFile)) {
|
|
737
|
+
return {};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
try {
|
|
741
|
+
const content = fs.readFileSync(authJsonFile, 'utf8');
|
|
742
|
+
return JSON.parse(content);
|
|
743
|
+
} catch (parseError) {
|
|
744
|
+
const chalk = require('chalk');
|
|
745
|
+
console.warn(
|
|
746
|
+
chalk.yellow(
|
|
747
|
+
`⚠ Warning: Could not parse existing auth.json, will create new file (警告: 无法解析现有 auth.json,将创建新文件)`
|
|
748
|
+
)
|
|
749
|
+
);
|
|
750
|
+
return {};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Write auth.json file atomically with proper permissions
|
|
756
|
+
* Uses atomic write (temp file + rename) to prevent corruption from concurrent access
|
|
757
|
+
* @private
|
|
758
|
+
* @param {string} authJsonFile - Path to auth.json
|
|
759
|
+
* @param {Object} authData - Auth data to write
|
|
760
|
+
*/
|
|
761
|
+
_writeAuthJson(authJsonFile, authData) {
|
|
762
|
+
const chalk = require('chalk');
|
|
763
|
+
const tempFile = `${authJsonFile}.tmp.${process.pid}`;
|
|
764
|
+
|
|
765
|
+
try {
|
|
766
|
+
// Write to temporary file first (atomic operation)
|
|
767
|
+
fs.writeFileSync(tempFile, JSON.stringify(authData, null, 2), 'utf8');
|
|
768
|
+
|
|
769
|
+
// Set file permissions to 600 (owner read/write only) for security
|
|
770
|
+
if (process.platform !== 'win32') {
|
|
771
|
+
fs.chmodSync(tempFile, 0o600);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Atomically rename temp file to actual file
|
|
775
|
+
// This is atomic on POSIX systems and prevents corruption
|
|
776
|
+
fs.renameSync(tempFile, authJsonFile);
|
|
777
|
+
} catch (error) {
|
|
778
|
+
// Clean up temp file if it exists
|
|
779
|
+
if (fs.existsSync(tempFile)) {
|
|
780
|
+
try {
|
|
781
|
+
fs.unlinkSync(tempFile);
|
|
782
|
+
} catch (cleanupError) {
|
|
783
|
+
// Ignore cleanup errors
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Clear OPENAI_API_KEY in ~/.codex/auth.json for chat mode
|
|
792
|
+
* @deprecated This method is no longer called automatically.
|
|
793
|
+
* Chat mode doesn't require clearing auth.json since it doesn't use it.
|
|
794
|
+
*/
|
|
795
|
+
clearCodexAuthJson() {
|
|
796
|
+
const chalk = require('chalk');
|
|
797
|
+
const codexDir = path.join(os.homedir(), '.codex');
|
|
798
|
+
const authJsonFile = path.join(codexDir, 'auth.json');
|
|
799
|
+
|
|
800
|
+
try {
|
|
801
|
+
// Ensure .codex directory exists
|
|
802
|
+
if (!fs.existsSync(codexDir)) {
|
|
803
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// Read existing auth data
|
|
807
|
+
const authData = this._readAuthJson(authJsonFile);
|
|
808
|
+
|
|
809
|
+
// Clear OPENAI_API_KEY (set to empty string)
|
|
810
|
+
authData.OPENAI_API_KEY = "";
|
|
811
|
+
|
|
812
|
+
// Write atomically with proper permissions
|
|
813
|
+
this._writeAuthJson(authJsonFile, authData);
|
|
814
|
+
|
|
815
|
+
console.log(
|
|
816
|
+
chalk.cyan(
|
|
817
|
+
`✓ Cleared OPENAI_API_KEY in auth.json (chat mode) (已清空 auth.json 中的 OPENAI_API_KEY)`
|
|
818
|
+
)
|
|
819
|
+
);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
console.error(
|
|
822
|
+
chalk.yellow(
|
|
823
|
+
`⚠ Warning: Failed to clear auth.json: ${error.message} (警告: 清空 auth.json 失败)`
|
|
824
|
+
)
|
|
825
|
+
);
|
|
826
|
+
// Don't throw error, just warn - this is not critical for chat mode
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Update ~/.codex/auth.json with API key for responses mode
|
|
832
|
+
* @param {string} apiKey - API key to store in auth.json
|
|
833
|
+
* @throws {Error} If file operations fail
|
|
834
|
+
*/
|
|
835
|
+
updateCodexAuthJson(apiKey) {
|
|
836
|
+
const chalk = require('chalk');
|
|
837
|
+
const codexDir = path.join(os.homedir(), '.codex');
|
|
838
|
+
const authJsonFile = path.join(codexDir, 'auth.json');
|
|
839
|
+
|
|
840
|
+
try {
|
|
841
|
+
// Ensure .codex directory exists
|
|
842
|
+
if (!fs.existsSync(codexDir)) {
|
|
843
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Read existing auth data
|
|
847
|
+
const authData = this._readAuthJson(authJsonFile);
|
|
848
|
+
|
|
849
|
+
// Update OPENAI_API_KEY
|
|
850
|
+
authData.OPENAI_API_KEY = apiKey;
|
|
851
|
+
|
|
852
|
+
// Write atomically with proper permissions
|
|
853
|
+
this._writeAuthJson(authJsonFile, authData);
|
|
854
|
+
|
|
855
|
+
console.log(
|
|
856
|
+
chalk.green(
|
|
857
|
+
`✓ Updated auth.json at: ${authJsonFile} (已更新 auth.json)`
|
|
858
|
+
)
|
|
859
|
+
);
|
|
860
|
+
} catch (error) {
|
|
861
|
+
console.error(
|
|
862
|
+
chalk.red(
|
|
863
|
+
`✗ Failed to update auth.json: ${error.message} (更新 auth.json 失败)`
|
|
864
|
+
)
|
|
865
|
+
);
|
|
866
|
+
throw error;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
623
870
|
/**
|
|
624
871
|
* Get current project's active account
|
|
625
872
|
* Searches upwards from current directory to find project root
|
|
@@ -1160,3 +1407,6 @@ class ConfigManager {
|
|
|
1160
1407
|
}
|
|
1161
1408
|
|
|
1162
1409
|
module.exports = ConfigManager;
|
|
1410
|
+
module.exports.WIRE_API_MODES = WIRE_API_MODES;
|
|
1411
|
+
module.exports.DEFAULT_WIRE_API = DEFAULT_WIRE_API;
|
|
1412
|
+
module.exports.ACCOUNT_TYPES = ACCOUNT_TYPES;
|
package/src/index.js
CHANGED
|
@@ -53,8 +53,8 @@ program
|
|
|
53
53
|
|
|
54
54
|
// Use account command
|
|
55
55
|
program
|
|
56
|
-
.command('use [name]')
|
|
57
|
-
.description('Set the account to use for the current project (设置当前项目使用的账号)')
|
|
56
|
+
.command('use [name-or-id]')
|
|
57
|
+
.description('Set the account to use for the current project by name or ID (通过名称或ID设置当前项目使用的账号)')
|
|
58
58
|
.action(useAccount);
|
|
59
59
|
|
|
60
60
|
// Show info command
|
|
@@ -65,9 +65,9 @@ program
|
|
|
65
65
|
|
|
66
66
|
// Remove account command
|
|
67
67
|
program
|
|
68
|
-
.command('remove [name]')
|
|
68
|
+
.command('remove [name-or-id]')
|
|
69
69
|
.alias('rm')
|
|
70
|
-
.description('Remove an account (删除账号)')
|
|
70
|
+
.description('Remove an account by name or ID (通过名称或ID删除账号)')
|
|
71
71
|
.action(removeAccount);
|
|
72
72
|
|
|
73
73
|
// Show current account command
|
|
@@ -84,8 +84,8 @@ program
|
|
|
84
84
|
|
|
85
85
|
// Export account configuration
|
|
86
86
|
program
|
|
87
|
-
.command('export <name>')
|
|
88
|
-
.description('Export account configuration as JSON (导出账号配置为 JSON)')
|
|
87
|
+
.command('export <name-or-id>')
|
|
88
|
+
.description('Export account configuration as JSON by name or ID (通过名称或ID导出账号配置为 JSON)')
|
|
89
89
|
.action(exportAccount);
|
|
90
90
|
|
|
91
91
|
// Diagnostic command
|
|
@@ -199,36 +199,39 @@ program
|
|
|
199
199
|
console.log(' ais <command> [options]\n');
|
|
200
200
|
|
|
201
201
|
console.log(chalk.bold('COMMANDS (命令):'));
|
|
202
|
-
console.log(' add [name]
|
|
203
|
-
console.log(' list, ls
|
|
204
|
-
console.log(' use [name] Set the account for current project (设置当前项目使用的账号)');
|
|
205
|
-
console.log(' info
|
|
206
|
-
console.log(' current
|
|
207
|
-
console.log(' remove, rm
|
|
208
|
-
console.log(' paths
|
|
209
|
-
console.log(' doctor
|
|
210
|
-
console.log(' export <name> Export account as JSON (导出账号为 JSON)');
|
|
211
|
-
console.log(' ui
|
|
212
|
-
console.log(' model
|
|
213
|
-
console.log(' mcp
|
|
214
|
-
console.log(' help
|
|
215
|
-
console.log(' version
|
|
202
|
+
console.log(' add [name] Add a new account configuration (with custom env vars) (添加新账号配置,支持自定义环境变量)');
|
|
203
|
+
console.log(' list, ls List all available accounts with IDs (列出所有可用账号及其ID)');
|
|
204
|
+
console.log(' use [name-or-id] Set the account for current project by name or ID (通过名称或ID设置当前项目使用的账号)');
|
|
205
|
+
console.log(' info Show current project\'s account info (显示当前项目的账号信息)');
|
|
206
|
+
console.log(' current Show current account name (显示当前账号名称)');
|
|
207
|
+
console.log(' remove, rm [name-or-id] Remove an account by name or ID (通过名称或ID删除账号)');
|
|
208
|
+
console.log(' paths Show configuration file paths (显示配置文件路径)');
|
|
209
|
+
console.log(' doctor Diagnose Claude Code configuration issues (诊断 Claude Code 配置问题)');
|
|
210
|
+
console.log(' export <name-or-id> Export account as JSON by name or ID (通过名称或ID导出账号为 JSON)');
|
|
211
|
+
console.log(' ui Start web-based account manager UI (启动基于 Web 的账号管理界面)');
|
|
212
|
+
console.log(' model Manage model groups (管理模型组)');
|
|
213
|
+
console.log(' mcp Manage MCP servers (管理 MCP 服务器)');
|
|
214
|
+
console.log(' help Display this help message (显示此帮助信息)');
|
|
215
|
+
console.log(' version Show version number (显示版本号)\n');
|
|
216
216
|
|
|
217
217
|
console.log(chalk.bold('EXAMPLES (示例):'));
|
|
218
218
|
console.log(chalk.gray(' # Add a new account interactively (交互式添加新账号)'));
|
|
219
219
|
console.log(' ais add\n');
|
|
220
220
|
console.log(chalk.gray(' # Add a new account with a name (添加带名称的新账号)'));
|
|
221
221
|
console.log(' ais add my-claude-account\n');
|
|
222
|
-
console.log(chalk.gray(' # List all accounts (
|
|
222
|
+
console.log(chalk.gray(' # List all accounts with IDs (列出所有账号及其ID)'));
|
|
223
223
|
console.log(' ais list\n');
|
|
224
|
-
console.log(chalk.gray(' # Use an account
|
|
224
|
+
console.log(chalk.gray(' # Use an account by name (通过名称使用账号)'));
|
|
225
225
|
console.log(' ais use my-claude-account\n');
|
|
226
|
+
console.log(chalk.gray(' # Use an account by ID (通过ID使用账号)'));
|
|
227
|
+
console.log(' ais use 1\n');
|
|
226
228
|
console.log(chalk.gray(' # Show current project info (显示当前项目信息)'));
|
|
227
229
|
console.log(' ais info\n');
|
|
228
230
|
console.log(chalk.gray(' # Diagnose configuration issues (诊断配置问题)'));
|
|
229
231
|
console.log(' ais doctor\n');
|
|
230
|
-
console.log(chalk.gray(' # Remove an account (删除账号)'));
|
|
231
|
-
console.log(' ais remove my-old-account
|
|
232
|
+
console.log(chalk.gray(' # Remove an account by name or ID (通过名称或ID删除账号)'));
|
|
233
|
+
console.log(' ais remove my-old-account');
|
|
234
|
+
console.log(' ais remove 2\n');
|
|
232
235
|
console.log(chalk.gray(' # Start web UI for managing accounts (启动 Web 界面管理账号)'));
|
|
233
236
|
console.log(' ais ui\n');
|
|
234
237
|
|
package/src/ui-server.js
CHANGED
|
@@ -2,6 +2,7 @@ const http = require('http');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const ConfigManager = require('./config');
|
|
5
|
+
const { WIRE_API_MODES, DEFAULT_WIRE_API } = require('./config');
|
|
5
6
|
|
|
6
7
|
class UIServer {
|
|
7
8
|
constructor(port = null) {
|
|
@@ -1491,6 +1492,18 @@ class UIServer {
|
|
|
1491
1492
|
<label for="apiUrl" data-i18n="apiUrl">API URL (可选)</label>
|
|
1492
1493
|
<input type="text" id="apiUrl" data-i18n-placeholder="apiUrlPlaceholder" placeholder="https://api.anthropic.com">
|
|
1493
1494
|
</div>
|
|
1495
|
+
<!-- Wire API selection for Codex accounts -->
|
|
1496
|
+
<div class="form-group" id="wireApiGroup" style="display: none;">
|
|
1497
|
+
<label for="wireApi">Wire API 模式</label>
|
|
1498
|
+
<select id="wireApi">
|
|
1499
|
+
<option value="chat">chat - HTTP Headers 认证 (OpenAI 兼容)</option>
|
|
1500
|
+
<option value="responses">responses - auth.json 认证 (requires_openai_auth)</option>
|
|
1501
|
+
</select>
|
|
1502
|
+
<small style="color: #666; display: block; margin-top: 5px;">
|
|
1503
|
+
chat: API key 存储在 HTTP headers 中<br>
|
|
1504
|
+
responses: API key 存储在 ~/.codex/auth.json 中
|
|
1505
|
+
</small>
|
|
1506
|
+
</div>
|
|
1494
1507
|
<div class="form-group">
|
|
1495
1508
|
<label for="email" data-i18n="email">邮箱 (可选)</label>
|
|
1496
1509
|
<input type="email" id="email" data-i18n-placeholder="emailPlaceholder" placeholder="user@example.com">
|
|
@@ -1615,6 +1628,13 @@ class UIServer {
|
|
|
1615
1628
|
</div>
|
|
1616
1629
|
|
|
1617
1630
|
<script>
|
|
1631
|
+
// Constants for wire API modes (injected from backend)
|
|
1632
|
+
const WIRE_API_MODES = {
|
|
1633
|
+
CHAT: '${WIRE_API_MODES.CHAT}',
|
|
1634
|
+
RESPONSES: '${WIRE_API_MODES.RESPONSES}'
|
|
1635
|
+
};
|
|
1636
|
+
const DEFAULT_WIRE_API = '${DEFAULT_WIRE_API}';
|
|
1637
|
+
|
|
1618
1638
|
// i18n translations
|
|
1619
1639
|
const translations = {
|
|
1620
1640
|
zh: {
|
|
@@ -2021,6 +2041,12 @@ class UIServer {
|
|
|
2021
2041
|
<div class="info-value">\${data.model}</div>
|
|
2022
2042
|
</div>
|
|
2023
2043
|
\` : ''}
|
|
2044
|
+
\${data.type === 'Codex' ? \`
|
|
2045
|
+
<div class="account-info">
|
|
2046
|
+
<div class="info-label">Wire API</div>
|
|
2047
|
+
<div class="info-value">\${data.wireApi || (DEFAULT_WIRE_API + ' (default)')}</div>
|
|
2048
|
+
</div>
|
|
2049
|
+
\` : ''}
|
|
2024
2050
|
\${data.type === 'CCR' && data.ccrConfig ? \`
|
|
2025
2051
|
<div class="account-info">
|
|
2026
2052
|
<div class="info-label">CCR Provider</div>
|
|
@@ -2051,6 +2077,12 @@ class UIServer {
|
|
|
2051
2077
|
const simpleModelGroup = document.getElementById('simpleModelGroup');
|
|
2052
2078
|
const claudeModelGroup = document.getElementById('claudeModelGroup');
|
|
2053
2079
|
const ccrModelGroup = document.getElementById('ccrModelGroup');
|
|
2080
|
+
const wireApiGroup = document.getElementById('wireApiGroup');
|
|
2081
|
+
|
|
2082
|
+
// Show/hide wire_api field for Codex accounts
|
|
2083
|
+
if (wireApiGroup) {
|
|
2084
|
+
wireApiGroup.style.display = accountType === 'Codex' ? 'block' : 'none';
|
|
2085
|
+
}
|
|
2054
2086
|
|
|
2055
2087
|
if (accountType === 'Codex' || accountType === 'Droids') {
|
|
2056
2088
|
simpleModelGroup.style.display = 'block';
|
|
@@ -2117,6 +2149,12 @@ class UIServer {
|
|
|
2117
2149
|
if (account.type === 'Codex' || account.type === 'Droids') {
|
|
2118
2150
|
// Load simple model field
|
|
2119
2151
|
document.getElementById('simpleModel').value = account.model || '';
|
|
2152
|
+
|
|
2153
|
+
// Load wire_api for Codex accounts
|
|
2154
|
+
if (account.type === 'Codex') {
|
|
2155
|
+
document.getElementById('wireApi').value = account.wireApi || DEFAULT_WIRE_API;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2120
2158
|
// Clear model groups and CCR config
|
|
2121
2159
|
document.getElementById('modelGroupsList').innerHTML = '';
|
|
2122
2160
|
modelGroupCount = 0;
|
|
@@ -2379,6 +2417,14 @@ class UIServer {
|
|
|
2379
2417
|
if (simpleModel) {
|
|
2380
2418
|
accountData.model = simpleModel;
|
|
2381
2419
|
}
|
|
2420
|
+
|
|
2421
|
+
// Add wire_api for Codex accounts
|
|
2422
|
+
if (accountType === 'Codex') {
|
|
2423
|
+
const wireApi = document.getElementById('wireApi').value;
|
|
2424
|
+
if (wireApi) {
|
|
2425
|
+
accountData.wireApi = wireApi;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2382
2428
|
} else if (accountType === 'CCR') {
|
|
2383
2429
|
// Collect CCR config
|
|
2384
2430
|
const providerName = document.getElementById('ccrProviderName').value.trim();
|
package/CHANGELOG-v1.7.0.md
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
# Changelog v1.7.0
|
|
2
|
-
|
|
3
|
-
## 🎉 Major Features
|
|
4
|
-
|
|
5
|
-
### MCP (Model Context Protocol) Web UI Management
|
|
6
|
-
- **Full MCP server management through Web UI**
|
|
7
|
-
- Add, edit, delete MCP servers
|
|
8
|
-
- Support for stdio, sse, and http server types
|
|
9
|
-
- Test MCP server connections
|
|
10
|
-
- Enable/disable servers per project
|
|
11
|
-
- Sync configuration to Claude Code
|
|
12
|
-
- Search and filter functionality
|
|
13
|
-
- Complete internationalization (Chinese/English)
|
|
14
|
-
|
|
15
|
-
### Backend API (9 new endpoints)
|
|
16
|
-
- `GET /api/mcp-servers` - Get all MCP servers
|
|
17
|
-
- `POST /api/mcp-servers` - Add new MCP server
|
|
18
|
-
- `PUT /api/mcp-servers/:name` - Update MCP server
|
|
19
|
-
- `DELETE /api/mcp-servers/:name` - Delete MCP server
|
|
20
|
-
- `POST /api/mcp-servers/:name/test` - Test connection
|
|
21
|
-
- `POST /api/mcp-servers/:name/enable` - Enable for project
|
|
22
|
-
- `POST /api/mcp-servers/:name/disable` - Disable for project
|
|
23
|
-
- `GET /api/mcp-servers/enabled` - Get enabled servers
|
|
24
|
-
- `POST /api/mcp-servers/sync` - Sync configuration
|
|
25
|
-
|
|
26
|
-
### Frontend UI Enhancements
|
|
27
|
-
- **Tab Navigation System**
|
|
28
|
-
- Accounts management tab
|
|
29
|
-
- MCP servers management tab
|
|
30
|
-
- Smooth tab switching with event listeners
|
|
31
|
-
|
|
32
|
-
- **MCP Server Management Interface**
|
|
33
|
-
- Card-based layout matching existing design
|
|
34
|
-
- Dynamic forms based on server type
|
|
35
|
-
- Environment variables and headers support
|
|
36
|
-
- Status indicators (enabled/disabled)
|
|
37
|
-
- Real-time search and filtering
|
|
38
|
-
|
|
39
|
-
## 🐛 Bug Fixes
|
|
40
|
-
|
|
41
|
-
### Fixed: Account Data Not Showing
|
|
42
|
-
- **Issue**: After adding tabs, account data disappeared
|
|
43
|
-
- **Cause**: Incorrect HTML structure and indentation
|
|
44
|
-
- **Fix**: Corrected tab div structure and proper indentation
|
|
45
|
-
|
|
46
|
-
### Fixed: switchTab is not defined
|
|
47
|
-
- **Issue**: JavaScript error when clicking tabs
|
|
48
|
-
- **Cause**: Function defined after being used in onclick
|
|
49
|
-
- **Fix**: Replaced onclick attributes with event listeners using DOMContentLoaded
|
|
50
|
-
|
|
51
|
-
### Fixed: Incorrect Search Result Messages
|
|
52
|
-
- **Issue**: When search has no results, showed "Add your first account" message
|
|
53
|
-
- **Cause**: Did not distinguish between "no data" and "no search results"
|
|
54
|
-
- **Fix**: Smart detection of filtered state vs empty state
|
|
55
|
-
- No data: Shows "Add your first..." message
|
|
56
|
-
- No search results: Shows "No matching results found"
|
|
57
|
-
|
|
58
|
-
## ✨ Improvements
|
|
59
|
-
|
|
60
|
-
### User Experience
|
|
61
|
-
- Better error messages for search results
|
|
62
|
-
- Clearer distinction between empty state and filtered state
|
|
63
|
-
- Improved tab navigation with proper event handling
|
|
64
|
-
- Consistent UI design across all features
|
|
65
|
-
|
|
66
|
-
### Code Quality
|
|
67
|
-
- Modern JavaScript practices (event listeners vs onclick)
|
|
68
|
-
- Better separation of concerns
|
|
69
|
-
- Improved maintainability
|
|
70
|
-
- Comprehensive error handling
|
|
71
|
-
|
|
72
|
-
### Internationalization
|
|
73
|
-
- Complete Chinese translations for MCP features
|
|
74
|
-
- Complete English translations for MCP features
|
|
75
|
-
- Consistent translation keys
|
|
76
|
-
|
|
77
|
-
## 📊 Testing
|
|
78
|
-
|
|
79
|
-
### Automated Tests
|
|
80
|
-
- 32/32 tests passing
|
|
81
|
-
- Comprehensive coverage of all features
|
|
82
|
-
- Verification scripts included
|
|
83
|
-
|
|
84
|
-
### Manual Testing
|
|
85
|
-
- Test guides provided
|
|
86
|
-
- Step-by-step instructions
|
|
87
|
-
- Expected results documented
|
|
88
|
-
|
|
89
|
-
## 📁 New Files
|
|
90
|
-
|
|
91
|
-
### Documentation
|
|
92
|
-
- `MCP-UI-IMPLEMENTATION.md` - Complete implementation guide
|
|
93
|
-
- `FIXED-ISSUES.md` - Detailed problem fixes
|
|
94
|
-
- `SEARCH-FIX.md` - Search result fix documentation
|
|
95
|
-
- `QUICK-TEST.md` - Quick testing guide
|
|
96
|
-
- `TEST-NOW.md` - Immediate testing instructions
|
|
97
|
-
- `CHANGELOG-v1.7.0.md` - This file
|
|
98
|
-
|
|
99
|
-
### Testing
|
|
100
|
-
- `verify-implementation.js` - Automated verification script
|
|
101
|
-
- `test-ui.html` - Manual testing guide
|
|
102
|
-
|
|
103
|
-
## 🔧 Technical Details
|
|
104
|
-
|
|
105
|
-
### Modified Files
|
|
106
|
-
- `src/ui-server.js` - Main implementation file
|
|
107
|
-
- Added 9 API handler methods
|
|
108
|
-
- Added MCP tab HTML structure
|
|
109
|
-
- Added MCP modal HTML
|
|
110
|
-
- Added MCP JavaScript functions
|
|
111
|
-
- Added MCP translations (zh/en)
|
|
112
|
-
- Added tab navigation CSS
|
|
113
|
-
- Fixed switchTab function placement
|
|
114
|
-
- Fixed search result messages
|
|
115
|
-
|
|
116
|
-
### Code Statistics
|
|
117
|
-
- Lines added: ~1500
|
|
118
|
-
- API endpoints: +9
|
|
119
|
-
- JavaScript functions: +15
|
|
120
|
-
- Translations: +40 keys
|
|
121
|
-
- Tests: 32 automated tests
|
|
122
|
-
|
|
123
|
-
## 🎯 Migration Guide
|
|
124
|
-
|
|
125
|
-
### For Users
|
|
126
|
-
No migration needed. All existing functionality remains unchanged.
|
|
127
|
-
|
|
128
|
-
### For Developers
|
|
129
|
-
If you've customized the UI:
|
|
130
|
-
1. Tab navigation now uses event listeners instead of onclick
|
|
131
|
-
2. Search results have improved empty state handling
|
|
132
|
-
3. New MCP management APIs available
|
|
133
|
-
|
|
134
|
-
## 🚀 Upgrade Instructions
|
|
135
|
-
|
|
136
|
-
### From v1.6.x to v1.7.0
|
|
137
|
-
|
|
138
|
-
1. **Update package**:
|
|
139
|
-
```bash
|
|
140
|
-
npm install -g ai-account-switch@1.7.0
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
2. **Verify installation**:
|
|
144
|
-
```bash
|
|
145
|
-
ais --version
|
|
146
|
-
# Should show: 1.7.0
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
3. **Test new features**:
|
|
150
|
-
```bash
|
|
151
|
-
ais ui
|
|
152
|
-
# Navigate to MCP Servers tab
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## 📝 Breaking Changes
|
|
156
|
-
|
|
157
|
-
None. This is a backward-compatible release.
|
|
158
|
-
|
|
159
|
-
## 🙏 Acknowledgments
|
|
160
|
-
|
|
161
|
-
- Comprehensive testing and verification
|
|
162
|
-
- User feedback incorporated
|
|
163
|
-
- Production-ready code quality
|
|
164
|
-
|
|
165
|
-
## 📚 Resources
|
|
166
|
-
|
|
167
|
-
- [README.md](README.md) - Main documentation
|
|
168
|
-
- [MCP-UI-IMPLEMENTATION.md](MCP-UI-IMPLEMENTATION.md) - Implementation details
|
|
169
|
-
- [QUICK-TEST.md](QUICK-TEST.md) - Testing guide
|
|
170
|
-
|
|
171
|
-
---
|
|
172
|
-
|
|
173
|
-
**Release Date**: 2025-11-12
|
|
174
|
-
**Version**: 1.7.0
|
|
175
|
-
**Status**: Stable
|
|
176
|
-
**Tests**: 32/32 passing
|