@tokenaut/opentoken 1.4.5 → 1.4.7
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 +21 -1
- package/package.json +1 -1
- package/src/claude-desktop-config.js +142 -0
- package/src/cli.js +43 -13
- package/src/logo.js +7 -24
- package/src/tools/claude_desktop.js +185 -0
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ npm install -g @tokenaut/opentoken
|
|
|
25
25
|
| 应用 | 子命令(别名) | 配置文件 |
|
|
26
26
|
|------|----------------|----------|
|
|
27
27
|
| **Claude Code** | `cc`、`claude` | `~/.claude/settings.json`(环境变量:`ANTHROPIC_AUTH_TOKEN`、`ANTHROPIC_BASE_URL`、`ANTHROPIC_MODEL`) |
|
|
28
|
+
| **Claude Desktop** | `claude_desktop`、`cd` | `~/Library/Application Support/Claude-3p/configLibrary/*.json`(`inferenceGatewayApiKey`、`inferenceGatewayBaseUrl`) |
|
|
28
29
|
| **Atomcode** | `atomcode`、`ac` | `~/.atomcode/config.toml` |
|
|
29
30
|
| **OpenClaw** | `openclaw`、`oc` | `~/.openclaw/openclaw.json`(合并写入 Opentoken:`models.providers.opentoken`、`agents.defaults.model.primary` 等) |
|
|
30
31
|
| **Hermes** | `hermes`、`hm` | `~/.hermes/config.yaml`(`model.provider`、`model.base_url`、`model.default`)与 `~/.hermes/.env`(`OPENTOKEN_API_KEY`) |
|
|
@@ -39,11 +40,13 @@ OpenCode 会为 **`opentoken`** 提供方写入 **`npm`**(`@ai-sdk/anthropic`
|
|
|
39
40
|
|
|
40
41
|
Codex 会为 **`opentoken`** 提供方写入 **`base_url`**(默认 `https://gw.opentoken.io/v1`)与 **`wire_api = "responses"`**(不写 `env_key`),并只从模型列表中选择 `supported_endpoint_types` 包含 `responses` 的模型。API Key 写入 **`~/.codex/auth.json`** 的 **`OPENAI_API_KEY`**,Codex 命令行与 IDE 插件共用,无需再 export 环境变量。
|
|
41
42
|
|
|
43
|
+
Claude Desktop 会写入 **`inferenceGatewayApiKey`**(API Key)与 **`inferenceGatewayBaseUrl`**(Base URL,默认 `https://gw.opentoken.io`),同时设置 **`inferenceProvider: "gateway"`** 与 **`inferenceGatewayAuthScheme: "bearer"`**。配置文件位于 `configLibrary/` 目录下,通过 UUID 标识的 JSON 文件管理;本工具会自动复用现有配置或创建新条目。Windows 下路径为 `%APPDATA%\Claude\configLibrary\*.json`。
|
|
44
|
+
|
|
42
45
|
---
|
|
43
46
|
|
|
44
47
|
## 快捷一键配置(非交互)
|
|
45
48
|
|
|
46
|
-
以下为各应用在命令行直接写入配置的用法。选项可单独或组合使用:**`--key` / `-k`**,**`--url` / `-u
|
|
49
|
+
以下为各应用在命令行直接写入配置的用法。选项可单独或组合使用:**`--key` / `-k`**,**`--url` / `-u`**(Claude Code、Claude Desktop、Codex),**`--model` / `-m`**。
|
|
47
50
|
|
|
48
51
|
### Claude Code
|
|
49
52
|
|
|
@@ -61,6 +64,23 @@ opentoken cc -k sk-xxx
|
|
|
61
64
|
opentoken cc -k sk-xxx -u https://gw.opentoken.io -m claude-opus-4-5
|
|
62
65
|
```
|
|
63
66
|
|
|
67
|
+
### Claude Desktop
|
|
68
|
+
|
|
69
|
+
写入(或合并)`configLibrary/*.json` 中的第三方推理配置:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
opentoken claude_desktop --key <api_key>
|
|
73
|
+
opentoken cd --url <base_url>
|
|
74
|
+
opentoken cd -k <api_key> -u <base_url>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
示例:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
opentoken cd -k sk-xxx
|
|
81
|
+
opentoken cd -k sk-xxx -u https://gw.opentoken.io
|
|
82
|
+
```
|
|
83
|
+
|
|
64
84
|
### Atomcode
|
|
65
85
|
|
|
66
86
|
Base URL 固定为 Opentoken 的 OpenAI 兼容地址,仅需 Key 与模型:
|
package/package.json
CHANGED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
function getConfigLibraryPath() {
|
|
9
|
+
if (process.platform === 'win32') {
|
|
10
|
+
return path.join(process.env.APPDATA || '', 'Claude', 'configLibrary');
|
|
11
|
+
}
|
|
12
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Claude-3p', 'configLibrary');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getMetaPath() {
|
|
16
|
+
return path.join(getConfigLibraryPath(), '_meta.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getConfigDir() {
|
|
20
|
+
const dir = getConfigLibraryPath();
|
|
21
|
+
if (!fs.existsSync(dir)) {
|
|
22
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readMeta() {
|
|
28
|
+
try {
|
|
29
|
+
const metaPath = getMetaPath();
|
|
30
|
+
if (!fs.existsSync(metaPath)) {
|
|
31
|
+
return { appliedId: null, entries: [] };
|
|
32
|
+
}
|
|
33
|
+
const raw = fs.readFileSync(metaPath, 'utf-8');
|
|
34
|
+
return JSON.parse(raw);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return { appliedId: null, entries: [] };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeMeta(meta) {
|
|
41
|
+
const dir = getConfigDir();
|
|
42
|
+
const metaPath = getMetaPath();
|
|
43
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findOrCreateConfigEntry() {
|
|
47
|
+
const meta = readMeta();
|
|
48
|
+
|
|
49
|
+
if (meta.appliedId && meta.entries && meta.entries.length > 0) {
|
|
50
|
+
const existingEntry = meta.entries.find(e => e.id === meta.appliedId);
|
|
51
|
+
if (existingEntry) {
|
|
52
|
+
return { configPath: path.join(getConfigDir(), meta.appliedId + '.json'), entryId: meta.appliedId, isNew: false };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const newId = crypto.randomUUID();
|
|
57
|
+
const newEntry = { id: newId, name: 'OpenToken' };
|
|
58
|
+
|
|
59
|
+
if (!meta.entries) {
|
|
60
|
+
meta.entries = [];
|
|
61
|
+
}
|
|
62
|
+
meta.entries.push(newEntry);
|
|
63
|
+
meta.appliedId = newId;
|
|
64
|
+
|
|
65
|
+
writeMeta(meta);
|
|
66
|
+
|
|
67
|
+
return { configPath: path.join(getConfigDir(), newId + '.json'), entryId: newId, isNew: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readDesktopConfig() {
|
|
71
|
+
const { configPath } = findOrCreateConfigEntry();
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
if (!fs.existsSync(configPath)) {
|
|
75
|
+
return getDefaultConfig();
|
|
76
|
+
}
|
|
77
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
78
|
+
const parsed = JSON.parse(raw);
|
|
79
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
80
|
+
return getDefaultConfig();
|
|
81
|
+
}
|
|
82
|
+
return parsed;
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return getDefaultConfig();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeDesktopConfig(config) {
|
|
89
|
+
const { configPath, isNew } = findOrCreateConfigEntry();
|
|
90
|
+
const dir = path.dirname(configPath);
|
|
91
|
+
if (!fs.existsSync(dir)) {
|
|
92
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const mergedConfig = { ...getDefaultConfig(), ...config };
|
|
96
|
+
fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), 'utf-8');
|
|
97
|
+
|
|
98
|
+
if (isNew) {
|
|
99
|
+
console.log(' (已创建新的桌面端配置)');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getDefaultConfig() {
|
|
104
|
+
return {
|
|
105
|
+
coworkEgressAllowedHosts: ['*'],
|
|
106
|
+
disableDeploymentModeChooser: true,
|
|
107
|
+
inferenceProvider: 'gateway',
|
|
108
|
+
inferenceGatewayAuthScheme: 'bearer',
|
|
109
|
+
inferenceGatewayBaseUrl: '',
|
|
110
|
+
inferenceGatewayApiKey: '',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getCurrentConfig() {
|
|
115
|
+
const config = readDesktopConfig();
|
|
116
|
+
return {
|
|
117
|
+
apiKey: config.inferenceGatewayApiKey || '',
|
|
118
|
+
baseUrl: config.inferenceGatewayBaseUrl || '',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function applyConfig({ apiKey, baseUrl }) {
|
|
123
|
+
const config = readDesktopConfig();
|
|
124
|
+
|
|
125
|
+
if (apiKey !== undefined) {
|
|
126
|
+
config.inferenceGatewayApiKey = apiKey;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (baseUrl !== undefined) {
|
|
130
|
+
config.inferenceGatewayBaseUrl = baseUrl;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
config.inferenceProvider = 'gateway';
|
|
134
|
+
config.inferenceGatewayAuthScheme = 'bearer';
|
|
135
|
+
|
|
136
|
+
writeDesktopConfig(config);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
getCurrentConfig,
|
|
141
|
+
applyConfig,
|
|
142
|
+
};
|
package/src/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ const inquirer = require('inquirer');
|
|
|
5
5
|
const { printLogo } = require('./logo');
|
|
6
6
|
const { clearScreen } = require('./screen');
|
|
7
7
|
const claude = require('./tools/claude');
|
|
8
|
+
const claudeDesktop = require('./tools/claude_desktop');
|
|
8
9
|
const atomcode = require('./tools/atomcode');
|
|
9
10
|
const hermes = require('./tools/hermes');
|
|
10
11
|
const openclaw = require('./tools/openclaw');
|
|
@@ -36,6 +37,10 @@ function isCodexCommand(tool) {
|
|
|
36
37
|
return tool === 'codex' || tool === 'cx';
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function isClaudeDesktopCommand(tool) {
|
|
41
|
+
return tool === 'claude_desktop' || tool === 'cd';
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
function printVersion() {
|
|
40
45
|
const pkg = require('../package.json');
|
|
41
46
|
console.log(`${pkg.name} ${pkg.version}`);
|
|
@@ -86,20 +91,28 @@ function printHelp() {
|
|
|
86
91
|
console.log(' opentoken cx --model <model> 设置默认模型');
|
|
87
92
|
console.log(' opentoken cx -k <k> -u <u> -m <m> 一键设置 URL 与模型');
|
|
88
93
|
console.log('');
|
|
94
|
+
console.log(' opentoken claude_desktop 进入 Claude Desktop 配置(configLibrary/*.json)');
|
|
95
|
+
console.log(' opentoken cd 同上(缩写)');
|
|
96
|
+
console.log(' opentoken cd --key <apikey> 设置 API Key(inferenceGatewayApiKey)');
|
|
97
|
+
console.log(' opentoken cd --url <baseurl> 设置 Base URL(inferenceGatewayBaseUrl)');
|
|
98
|
+
console.log(' opentoken cd -k <k> -u <u> 一键设置 Key 与 URL');
|
|
99
|
+
console.log('');
|
|
89
100
|
console.log(chalk.bold('快捷别名(--key / -k,--url / -u,--model / -m)'));
|
|
90
101
|
console.log('');
|
|
91
102
|
console.log(chalk.bold('示例:'));
|
|
92
103
|
console.log(' opentoken cc -k sk-xxx');
|
|
93
104
|
console.log(` opentoken cc -k sk-xxx -u ${DEFAULT_BASE_URL} -m claude-opus-4-5`);
|
|
105
|
+
console.log(` opentoken cd -k sk-xxx -u ${DEFAULT_BASE_URL}`);
|
|
94
106
|
console.log('');
|
|
95
107
|
console.log(chalk.bold('主菜单:'));
|
|
96
108
|
console.log(' 1. 配置 Claude Code');
|
|
97
|
-
console.log(' 2. 配置
|
|
98
|
-
console.log(' 3. 配置
|
|
99
|
-
console.log(' 4. 配置
|
|
100
|
-
console.log(' 5. 配置
|
|
101
|
-
console.log(' 6. 配置
|
|
102
|
-
console.log(' 7.
|
|
109
|
+
console.log(' 2. 配置 Claude Desktop');
|
|
110
|
+
console.log(' 3. 配置 Atomcode');
|
|
111
|
+
console.log(' 4. 配置 OpenClaw');
|
|
112
|
+
console.log(' 5. 配置 Hermes');
|
|
113
|
+
console.log(' 6. 配置 OpenCode');
|
|
114
|
+
console.log(' 7. 配置 Codex');
|
|
115
|
+
console.log(' 8. 退出');
|
|
103
116
|
console.log('');
|
|
104
117
|
}
|
|
105
118
|
|
|
@@ -154,14 +167,15 @@ async function mainMenu() {
|
|
|
154
167
|
message: '请选择:',
|
|
155
168
|
choices: [
|
|
156
169
|
{ name: '1. 配置 Claude Code', value: 'claude' },
|
|
157
|
-
{ name: '2. 配置
|
|
158
|
-
{ name: '3. 配置
|
|
159
|
-
{ name: '4. 配置
|
|
160
|
-
{ name: '5. 配置
|
|
161
|
-
{ name: '6. 配置
|
|
162
|
-
{ name: '7.
|
|
170
|
+
{ name: '2. 配置 Claude Desktop', value: 'claudeDesktop' },
|
|
171
|
+
{ name: '3. 配置 Atomcode', value: 'atomcode' },
|
|
172
|
+
{ name: '4. 配置 OpenClaw', value: 'openclaw' },
|
|
173
|
+
{ name: '5. 配置 Hermes', value: 'hermes' },
|
|
174
|
+
{ name: '6. 配置 OpenCode', value: 'opencode' },
|
|
175
|
+
{ name: '7. 配置 Codex', value: 'codex' },
|
|
176
|
+
{ name: '8. 退出', value: 'exit' },
|
|
163
177
|
],
|
|
164
|
-
pageSize:
|
|
178
|
+
pageSize: 9,
|
|
165
179
|
},
|
|
166
180
|
]);
|
|
167
181
|
|
|
@@ -175,6 +189,10 @@ async function mainMenu() {
|
|
|
175
189
|
await claude.run();
|
|
176
190
|
}
|
|
177
191
|
|
|
192
|
+
if (action === 'claudeDesktop') {
|
|
193
|
+
await claudeDesktop.run();
|
|
194
|
+
}
|
|
195
|
+
|
|
178
196
|
if (action === 'atomcode') {
|
|
179
197
|
await atomcode.run();
|
|
180
198
|
}
|
|
@@ -252,6 +270,13 @@ async function run() {
|
|
|
252
270
|
return;
|
|
253
271
|
}
|
|
254
272
|
|
|
273
|
+
if (isClaudeDesktopCommand(args.tool) && (args.key || args.url)) {
|
|
274
|
+
clearScreen();
|
|
275
|
+
printLogo();
|
|
276
|
+
claudeDesktop.quickSet({ key: args.key, url: args.url });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
255
280
|
if (isCcCommand(args.tool)) {
|
|
256
281
|
await claude.run();
|
|
257
282
|
return;
|
|
@@ -282,6 +307,11 @@ async function run() {
|
|
|
282
307
|
return;
|
|
283
308
|
}
|
|
284
309
|
|
|
310
|
+
if (isClaudeDesktopCommand(args.tool)) {
|
|
311
|
+
await claudeDesktop.run();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
285
315
|
await mainMenu();
|
|
286
316
|
}
|
|
287
317
|
|
package/src/logo.js
CHANGED
|
@@ -4,12 +4,12 @@ const chalk = require('chalk');
|
|
|
4
4
|
const stringWidth = require('string-width');
|
|
5
5
|
|
|
6
6
|
const LOGO_LINES = [
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
' ██║ ╚██████╔╝██║ ██╗███████╗██║
|
|
12
|
-
' ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝
|
|
7
|
+
' ██████╗ ██████╗ ███████╗███╗ ██╗████████╗ ██████╗ ██╗ ██╗███████╗███╗ ██╗',
|
|
8
|
+
'██╔═══██╗██╔══██╗██╔════╝████╗ ██║╚══██╔══╝██╔═══██╗██║ ██╔╝██╔════╝████╗ ██║',
|
|
9
|
+
'██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ██║ ██║█████╔╝ █████╗ ██╔██╗ ██║',
|
|
10
|
+
'██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██║ ██║██╔═██╗ ██╔══╝ ██║╚██╗██║',
|
|
11
|
+
'╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ╚██████╔╝██║ ██╗███████╗██║ ╚████║',
|
|
12
|
+
' ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝',
|
|
13
13
|
];
|
|
14
14
|
|
|
15
15
|
function padToDisplayWidth(str, targetCols) {
|
|
@@ -20,21 +20,6 @@ function padToDisplayWidth(str, targetCols) {
|
|
|
20
20
|
return s;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function splitAtDisplayHalf(str, totalDisplayWidth) {
|
|
24
|
-
const half = totalDisplayWidth / 2;
|
|
25
|
-
let acc = 0;
|
|
26
|
-
let i = 0;
|
|
27
|
-
while (i < str.length) {
|
|
28
|
-
const cp = str.codePointAt(i);
|
|
29
|
-
const ch = String.fromCodePoint(cp);
|
|
30
|
-
const w = stringWidth(ch);
|
|
31
|
-
if (acc + w > half) break;
|
|
32
|
-
acc += w;
|
|
33
|
-
i += ch.length;
|
|
34
|
-
}
|
|
35
|
-
return { left: str.slice(0, i), right: str.slice(i) };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
23
|
function printLogo() {
|
|
39
24
|
const logoDisplayWidths = LOGO_LINES.map((l) => stringWidth(l));
|
|
40
25
|
const innerWidth = Math.max(...logoDisplayWidths);
|
|
@@ -46,11 +31,9 @@ function printLogo() {
|
|
|
46
31
|
|
|
47
32
|
for (const line of LOGO_LINES) {
|
|
48
33
|
const padded = padToDisplayWidth(line, innerWidth);
|
|
49
|
-
const { left, right } = splitAtDisplayHalf(padded, innerWidth);
|
|
50
34
|
console.log(
|
|
51
35
|
chalk.cyan('║') +
|
|
52
|
-
chalk.bold(chalk.
|
|
53
|
-
chalk.bold(chalk.white(right)) +
|
|
36
|
+
chalk.bold(chalk.white(padded)) +
|
|
54
37
|
chalk.cyan('║')
|
|
55
38
|
);
|
|
56
39
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { API_NODES, DEFAULT_BASE_URL } = require('../models');
|
|
6
|
+
const { getCurrentConfig, applyConfig } = require('../claude-desktop-config');
|
|
7
|
+
const { printLogo } = require('../logo');
|
|
8
|
+
const { clearScreen } = require('../screen');
|
|
9
|
+
|
|
10
|
+
let pendingConfig = { apiKey: null, baseUrl: null };
|
|
11
|
+
|
|
12
|
+
function resetPending() {
|
|
13
|
+
const disk = getCurrentConfig();
|
|
14
|
+
pendingConfig = {
|
|
15
|
+
apiKey: null,
|
|
16
|
+
baseUrl: disk.baseUrl ? null : DEFAULT_BASE_URL,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function maskApiKey(key) {
|
|
21
|
+
if (!key || key.length < 8) return key || '';
|
|
22
|
+
return key.slice(0, 6) + '****' + key.slice(-4);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function handleConfigApiKey() {
|
|
26
|
+
const { apiKey } = await inquirer.prompt([
|
|
27
|
+
{
|
|
28
|
+
type: 'password',
|
|
29
|
+
name: 'apiKey',
|
|
30
|
+
prefix: '',
|
|
31
|
+
message: '请输入 API Key:',
|
|
32
|
+
mask: '*',
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
if (apiKey && apiKey.trim()) {
|
|
37
|
+
pendingConfig.apiKey = apiKey.trim();
|
|
38
|
+
console.log(chalk.green(' ✓ API Key 已更新'));
|
|
39
|
+
} else {
|
|
40
|
+
console.log(chalk.gray(' (留空,保持不变)'));
|
|
41
|
+
}
|
|
42
|
+
console.log('');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function handleConfigBaseUrl() {
|
|
46
|
+
const { baseUrl: existing } = getCurrentConfig();
|
|
47
|
+
const current = pendingConfig.baseUrl !== null ? pendingConfig.baseUrl : existing;
|
|
48
|
+
|
|
49
|
+
const nodeChoices = API_NODES.map((n) => ({
|
|
50
|
+
...n,
|
|
51
|
+
name: current === n.value ? chalk.green('► ' + n.name) : ' ' + n.name,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
const { selected } = await inquirer.prompt([
|
|
55
|
+
{
|
|
56
|
+
type: 'list',
|
|
57
|
+
name: 'selected',
|
|
58
|
+
prefix: '',
|
|
59
|
+
message: '请选择 Base URL:',
|
|
60
|
+
choices: nodeChoices,
|
|
61
|
+
default: current || API_NODES[0].value,
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
if (selected === '__custom__') {
|
|
66
|
+
const { customUrl } = await inquirer.prompt([
|
|
67
|
+
{
|
|
68
|
+
type: 'input',
|
|
69
|
+
name: 'customUrl',
|
|
70
|
+
prefix: '',
|
|
71
|
+
message: '请输入自定义 Base URL:',
|
|
72
|
+
default: current && current !== '__custom__' ? current : DEFAULT_BASE_URL,
|
|
73
|
+
validate: (v) => v.trim().startsWith('http') || '请输入合法的 URL(以 http 开头)',
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
pendingConfig.baseUrl = customUrl.trim();
|
|
77
|
+
console.log(chalk.green(' ✓ Base URL 已更新:' + pendingConfig.baseUrl));
|
|
78
|
+
} else {
|
|
79
|
+
pendingConfig.baseUrl = selected;
|
|
80
|
+
console.log(chalk.green(' ✓ Base URL 已更新:' + selected));
|
|
81
|
+
}
|
|
82
|
+
console.log('');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getEffectiveConfig() {
|
|
86
|
+
const disk = getCurrentConfig();
|
|
87
|
+
return {
|
|
88
|
+
apiKey: pendingConfig.apiKey !== null ? pendingConfig.apiKey : disk.apiKey,
|
|
89
|
+
baseUrl: pendingConfig.baseUrl !== null ? pendingConfig.baseUrl : disk.baseUrl,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function grayParen(text) {
|
|
94
|
+
return chalk.gray('(' + text + ')');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildMenuChoices() {
|
|
98
|
+
const ev = getEffectiveConfig();
|
|
99
|
+
const keyShow = ev.apiKey ? maskApiKey(ev.apiKey) : '未配置';
|
|
100
|
+
const urlShow = ev.baseUrl ? ev.baseUrl : '未配置';
|
|
101
|
+
|
|
102
|
+
return [
|
|
103
|
+
{ name: '1. 配置 Base URL' + grayParen(urlShow), value: 'apiNode' },
|
|
104
|
+
{ name: '2. 配置 API Key' + grayParen(keyShow), value: 'apiKey' },
|
|
105
|
+
new inquirer.Separator(),
|
|
106
|
+
{ name: '3. 应用配置并保存', value: 'apply' },
|
|
107
|
+
new inquirer.Separator(),
|
|
108
|
+
{ name: '← 返回上级菜单', value: 'back' },
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function doApplyConfig() {
|
|
113
|
+
const toWrite = {};
|
|
114
|
+
if (pendingConfig.apiKey !== null) toWrite.apiKey = pendingConfig.apiKey;
|
|
115
|
+
if (pendingConfig.baseUrl !== null) toWrite.baseUrl = pendingConfig.baseUrl;
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
if (Object.keys(toWrite).length > 0) {
|
|
119
|
+
applyConfig(toWrite);
|
|
120
|
+
console.log(chalk.green(' ✓ 配置已成功写入'));
|
|
121
|
+
} else {
|
|
122
|
+
console.log(chalk.gray(' (无 Key / URL 变更)'));
|
|
123
|
+
}
|
|
124
|
+
console.log('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function paintScreen() {
|
|
128
|
+
clearScreen();
|
|
129
|
+
printLogo();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function run() {
|
|
133
|
+
resetPending();
|
|
134
|
+
|
|
135
|
+
while (true) {
|
|
136
|
+
paintScreen();
|
|
137
|
+
|
|
138
|
+
const { action } = await inquirer.prompt([
|
|
139
|
+
{
|
|
140
|
+
type: 'list',
|
|
141
|
+
name: 'action',
|
|
142
|
+
prefix: '',
|
|
143
|
+
message: '请选择操作:',
|
|
144
|
+
choices: buildMenuChoices(),
|
|
145
|
+
pageSize: 10,
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
switch (action) {
|
|
150
|
+
case 'apiNode':
|
|
151
|
+
paintScreen();
|
|
152
|
+
await handleConfigBaseUrl();
|
|
153
|
+
break;
|
|
154
|
+
case 'apiKey':
|
|
155
|
+
paintScreen();
|
|
156
|
+
await handleConfigApiKey();
|
|
157
|
+
break;
|
|
158
|
+
case 'apply':
|
|
159
|
+
doApplyConfig();
|
|
160
|
+
return;
|
|
161
|
+
case 'back':
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function quickSet({ key, url }) {
|
|
168
|
+
const toWrite = {};
|
|
169
|
+
if (key) toWrite.apiKey = key;
|
|
170
|
+
if (url) toWrite.baseUrl = url;
|
|
171
|
+
|
|
172
|
+
if (Object.keys(toWrite).length === 0) return false;
|
|
173
|
+
|
|
174
|
+
applyConfig(toWrite);
|
|
175
|
+
|
|
176
|
+
console.log('');
|
|
177
|
+
console.log(chalk.bold(chalk.cyan('Claude Desktop 配置已更新:')));
|
|
178
|
+
if (key) console.log(chalk.green(' ✓ API Key : ') + maskApiKey(key));
|
|
179
|
+
if (url) console.log(chalk.green(' ✓ Base URL : ') + url);
|
|
180
|
+
console.log('');
|
|
181
|
+
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { run, quickSet };
|