openclawapi 1.1.0 → 1.2.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/cli.js +342 -1659
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -6,1794 +6,477 @@ const fs = require('fs');
|
|
|
6
6
|
const JSON5 = require('json5');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'yunyi-codex': [
|
|
23
|
-
{ name: '国内节点', url: 'https://yunyi.skem.cn' },
|
|
24
|
-
{ name: 'CF国外节点1', url: 'https://yunyi.cfd' },
|
|
25
|
-
{ name: 'CF国外节点2', url: 'https://cdn1.yunyi.cfd' },
|
|
26
|
-
{ name: 'CF国外节点3', url: 'https://cdn2.yunyi.cfd' },
|
|
27
|
-
{ name: '备用节点1', url: 'http://47.99.42.193' },
|
|
28
|
-
{ name: '备用节点2', url: 'http://47.97.100.10' }
|
|
29
|
-
]
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const MODEL_PRESETS = {
|
|
33
|
-
'yunyi-claude': [
|
|
34
|
-
{ id: 'claude-opus-4-5', name: 'Claude Opus 4.5' },
|
|
35
|
-
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
|
|
36
|
-
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' }
|
|
37
|
-
],
|
|
38
|
-
'yunyi-codex': [
|
|
39
|
-
{ id: 'gpt-5.2', name: 'GPT 5.2' },
|
|
40
|
-
{ id: 'gpt-5.2-codex', name: 'GPT 5.2 Codex' }
|
|
41
|
-
]
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
function normalizeRelayKey(relayName) {
|
|
45
|
-
if (!relayName) return '';
|
|
46
|
-
if (relayName.startsWith('yunyi-claude')) return 'yunyi-claude';
|
|
47
|
-
if (relayName.startsWith('yunyi-codex')) return 'yunyi-codex';
|
|
48
|
-
return relayName;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function getRelaySuffix(relayName) {
|
|
52
|
-
const key = normalizeRelayKey(relayName);
|
|
53
|
-
if (key === 'yunyi-claude') return '/claude/v1/messages';
|
|
54
|
-
if (key === 'yunyi-codex') return '/codex/response';
|
|
55
|
-
return '';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function buildRelayBaseUrl(baseUrl, relayName) {
|
|
59
|
-
const suffix = getRelaySuffix(relayName);
|
|
60
|
-
if (!suffix) return baseUrl;
|
|
61
|
-
if (!baseUrl) return baseUrl;
|
|
62
|
-
const trimmed = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
63
|
-
|
|
64
|
-
if (suffix === '/claude/v1/messages') {
|
|
65
|
-
if (/\/claude\/v1\/messages$/.test(trimmed) || /\/claude\/v1\/message$/.test(trimmed)) {
|
|
66
|
-
return trimmed;
|
|
67
|
-
}
|
|
68
|
-
if (/\/claude\/v1$/.test(trimmed)) {
|
|
69
|
-
return `${trimmed}/messages`;
|
|
70
|
-
}
|
|
71
|
-
if (/\/claude$/.test(trimmed)) {
|
|
72
|
-
return `${trimmed}/v1/messages`;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (suffix === '/codex/response') {
|
|
77
|
-
if (/\/codex\/response$/.test(trimmed)) {
|
|
78
|
-
return trimmed;
|
|
79
|
-
}
|
|
80
|
-
if (/\/codex$/.test(trimmed)) {
|
|
81
|
-
return `${trimmed}/response`;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
9
|
+
const https = require('https');
|
|
10
|
+
const http = require('http');
|
|
11
|
+
|
|
12
|
+
// ============ 节点预设 (同步自 yunyi-activator) ============
|
|
13
|
+
const ENDPOINTS = [
|
|
14
|
+
{ name: '国内节点1', url: 'https://yunyi.rdzhvip.com' },
|
|
15
|
+
{ name: '国内节点2', url: 'https://yunyi.skem.cn' },
|
|
16
|
+
{ name: 'CF国外节点1', url: 'https://yunyi.cfd' },
|
|
17
|
+
{ name: 'CF国外节点2', url: 'https://cdn1.yunyi.cfd' },
|
|
18
|
+
{ name: 'CF国外节点3', url: 'https://cdn2.yunyi.cfd' },
|
|
19
|
+
{ name: '备用节点1', url: 'http://47.99.42.193' },
|
|
20
|
+
{ name: '备用节点2', url: 'http://47.97.100.10' }
|
|
21
|
+
];
|
|
84
22
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
23
|
+
// 模型预设
|
|
24
|
+
const MODELS = [
|
|
25
|
+
{ id: 'claude-opus-4-5', name: 'Claude Opus 4.5' },
|
|
26
|
+
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' },
|
|
27
|
+
{ id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' }
|
|
28
|
+
];
|
|
88
29
|
|
|
89
|
-
|
|
90
|
-
|
|
30
|
+
// 备份文件名
|
|
31
|
+
const BACKUP_FILENAME = 'openclaw-default.json.bak';
|
|
32
|
+
|
|
33
|
+
// ============ 测速功能 ============
|
|
34
|
+
async function testSpeed(url) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
const urlObj = new URL(url);
|
|
38
|
+
const protocol = urlObj.protocol === 'https:' ? https : http;
|
|
39
|
+
const timeout = 5000;
|
|
40
|
+
|
|
41
|
+
const req = protocol.get(url, { timeout, rejectUnauthorized: false }, (res) => {
|
|
42
|
+
const endTime = Date.now();
|
|
43
|
+
res.on('data', () => {});
|
|
44
|
+
res.on('end', () => {
|
|
45
|
+
resolve({ success: true, latency: endTime - startTime });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
91
48
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
49
|
+
req.on('timeout', () => {
|
|
50
|
+
req.destroy();
|
|
51
|
+
resolve({ success: false, latency: null, error: '超时' });
|
|
52
|
+
});
|
|
95
53
|
|
|
96
|
-
|
|
97
|
-
|
|
54
|
+
req.on('error', () => {
|
|
55
|
+
resolve({ success: false, latency: null, error: '连接失败' });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
98
58
|
}
|
|
99
59
|
|
|
100
|
-
function
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
cacheRead: 0,
|
|
114
|
-
cacheWrite: 0
|
|
115
|
-
},
|
|
116
|
-
contextWindow: defaults.contextWindow,
|
|
117
|
-
maxTokens: defaults.maxTokens
|
|
118
|
-
};
|
|
60
|
+
async function testAllEndpoints() {
|
|
61
|
+
const results = [];
|
|
62
|
+
for (const endpoint of ENDPOINTS) {
|
|
63
|
+
process.stdout.write(chalk.gray(` 测试 ${endpoint.name}...`));
|
|
64
|
+
const result = await testSpeed(endpoint.url);
|
|
65
|
+
results.push({ ...endpoint, ...result });
|
|
66
|
+
if (result.success) {
|
|
67
|
+
console.log(chalk.green(` ${result.latency}ms`));
|
|
68
|
+
} else {
|
|
69
|
+
console.log(chalk.red(` ${result.error}`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
119
73
|
}
|
|
120
74
|
|
|
121
|
-
//
|
|
75
|
+
// ============ 配置路径 ============
|
|
122
76
|
function getConfigPath() {
|
|
123
77
|
const homeDir = os.homedir();
|
|
124
78
|
const openclawStateDir = process.env.OPENCLAW_STATE_DIR || path.join(homeDir, '.openclaw');
|
|
125
79
|
const clawdbotStateDir = process.env.CLAWDBOT_STATE_DIR || path.join(homeDir, '.clawdbot');
|
|
126
80
|
|
|
127
|
-
const envConfig =
|
|
128
|
-
process.env.OPENCLAW_CONFIG_PATH ||
|
|
129
|
-
process.env.CLAWDBOT_CONFIG_PATH ||
|
|
130
|
-
process.env.OPENCLAW_CONFIG;
|
|
81
|
+
const envConfig = process.env.OPENCLAW_CONFIG_PATH || process.env.CLAWDBOT_CONFIG_PATH;
|
|
131
82
|
|
|
132
83
|
const openclawConfig = envConfig || ([
|
|
133
84
|
path.join(openclawStateDir, 'openclaw.json'),
|
|
134
85
|
path.join(clawdbotStateDir, 'openclaw.json'),
|
|
135
|
-
path.join(clawdbotStateDir, 'clawdbot.json')
|
|
136
|
-
path.join(clawdbotStateDir, 'moltbot.json')
|
|
86
|
+
path.join(clawdbotStateDir, 'clawdbot.json')
|
|
137
87
|
].find(p => fs.existsSync(p)) || path.join(openclawStateDir, 'openclaw.json'));
|
|
138
88
|
|
|
139
89
|
const configDir = path.dirname(openclawConfig);
|
|
140
|
-
const authProfiles = resolveAuthProfilesPath({
|
|
141
|
-
configDir,
|
|
142
|
-
openclawStateDir,
|
|
143
|
-
clawdbotStateDir
|
|
144
|
-
});
|
|
145
90
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const envAgentDir =
|
|
154
|
-
process.env.OPENCLAW_AGENT_DIR ||
|
|
155
|
-
process.env.CLAWDBOT_AGENT_DIR ||
|
|
156
|
-
process.env.PI_CODING_AGENT_DIR;
|
|
157
|
-
const envAgentName =
|
|
158
|
-
process.env.OPENCLAW_AGENT ||
|
|
159
|
-
process.env.CLAWDBOT_AGENT ||
|
|
160
|
-
process.env.PI_CODING_AGENT;
|
|
161
|
-
|
|
162
|
-
const candidates = [];
|
|
91
|
+
// 查找 auth-profiles 路径
|
|
92
|
+
const authCandidates = [
|
|
93
|
+
path.join(openclawStateDir, 'agents', 'main', 'agent', 'auth-profiles.json'),
|
|
94
|
+
path.join(clawdbotStateDir, 'agents', 'main', 'agent', 'auth-profiles.json'),
|
|
95
|
+
path.join(clawdbotStateDir, 'agent', 'auth-profiles.json')
|
|
96
|
+
];
|
|
97
|
+
const authProfiles = authCandidates.find(p => fs.existsSync(p)) || authCandidates[0];
|
|
163
98
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
99
|
+
return { openclawConfig, authProfiles, configDir };
|
|
100
|
+
}
|
|
167
101
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
102
|
+
// ============ 配置读写 ============
|
|
103
|
+
function readConfig(configPath) {
|
|
104
|
+
if (fs.existsSync(configPath)) {
|
|
105
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
106
|
+
return JSON5.parse(raw);
|
|
171
107
|
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
172
110
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Clawdbot default + legacy paths
|
|
178
|
-
candidates.push(path.join(clawdbotStateDir, 'agent', 'auth-profiles.json'));
|
|
179
|
-
candidates.push(path.join(clawdbotStateDir, 'agents', 'main', 'agent', 'auth-profiles.json'));
|
|
180
|
-
|
|
181
|
-
// Fallback to config directory if user stores alongside config
|
|
182
|
-
candidates.push(path.join(configDir, 'agent', 'auth-profiles.json'));
|
|
183
|
-
candidates.push(path.join(configDir, 'agents', 'main', 'agent', 'auth-profiles.json'));
|
|
184
|
-
|
|
185
|
-
const inferred = inferSingleAgentAuthPath(openclawStateDir, clawdbotStateDir);
|
|
186
|
-
if (inferred) {
|
|
187
|
-
candidates.unshift(inferred);
|
|
111
|
+
function writeConfig(configPath, config) {
|
|
112
|
+
const dir = path.dirname(configPath);
|
|
113
|
+
if (!fs.existsSync(dir)) {
|
|
114
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
188
115
|
}
|
|
189
|
-
|
|
190
|
-
return candidates.find(p => fs.existsSync(p)) || candidates[0];
|
|
116
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
191
117
|
}
|
|
192
118
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (dirs.length === 1) {
|
|
200
|
-
return path.join(openclawAgentsDir, dirs[0], 'agent', 'auth-profiles.json');
|
|
201
|
-
}
|
|
119
|
+
// ============ 备份/恢复 ============
|
|
120
|
+
function backupOriginalConfig(configPath, configDir) {
|
|
121
|
+
const backupPath = path.join(configDir, BACKUP_FILENAME);
|
|
122
|
+
if (!fs.existsSync(backupPath) && fs.existsSync(configPath)) {
|
|
123
|
+
fs.copyFileSync(configPath, backupPath);
|
|
124
|
+
return true;
|
|
202
125
|
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
203
128
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (dirs.length === 1) {
|
|
210
|
-
return path.join(clawdbotAgentsDir, dirs[0], 'agent', 'auth-profiles.json');
|
|
211
|
-
}
|
|
129
|
+
function restoreDefaultConfig(configPath, configDir) {
|
|
130
|
+
const backupPath = path.join(configDir, BACKUP_FILENAME);
|
|
131
|
+
if (fs.existsSync(backupPath)) {
|
|
132
|
+
fs.copyFileSync(backupPath, configPath);
|
|
133
|
+
return true;
|
|
212
134
|
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
213
137
|
|
|
214
|
-
|
|
138
|
+
// ============ URL 构建 ============
|
|
139
|
+
function buildFullUrl(baseUrl) {
|
|
140
|
+
const trimmed = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
141
|
+
if (trimmed.includes('/claude/v1/messages')) return trimmed;
|
|
142
|
+
if (trimmed.includes('/claude')) return trimmed + '/v1/messages';
|
|
143
|
+
return trimmed + '/claude/v1/messages';
|
|
215
144
|
}
|
|
216
145
|
|
|
146
|
+
// ============ 主程序 ============
|
|
217
147
|
async function main() {
|
|
218
148
|
console.clear();
|
|
219
|
-
console.log(chalk.cyan.bold('\n🔧 OpenClaw
|
|
149
|
+
console.log(chalk.cyan.bold('\n🔧 OpenClaw 配置工具 (简化版)\n'));
|
|
220
150
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
console.log(chalk.gray(`平台: ${platformInfo[process.platform] || process.platform}`));
|
|
228
|
-
const configPaths = getConfigPath();
|
|
229
|
-
console.log(chalk.gray(`配置目录: ${path.dirname(configPaths.openclawConfig)}`));
|
|
230
|
-
console.log(chalk.gray(`配置文件: ${configPaths.openclawConfig}\n`));
|
|
231
|
-
console.log(chalk.gray(`认证文件: ${configPaths.authProfiles}\n`));
|
|
232
|
-
const configManager = new ConfigManager(configPaths);
|
|
233
|
-
|
|
234
|
-
// 检查配置文件是否存在,如果不存在则自动初始化
|
|
235
|
-
const exists = await configManager.checkConfigExists();
|
|
236
|
-
if (!exists.openclaw) {
|
|
237
|
-
displayInfo('配置文件不存在,正在初始化...');
|
|
238
|
-
try {
|
|
239
|
-
await configManager.initializeConfig();
|
|
240
|
-
displaySuccess('配置文件初始化成功!');
|
|
241
|
-
} catch (error) {
|
|
242
|
-
displayError(`初始化失败: ${error.message}`);
|
|
243
|
-
displayInfo('请检查目录权限或手动创建配置文件');
|
|
244
|
-
process.exit(1);
|
|
245
|
-
}
|
|
151
|
+
const paths = getConfigPath();
|
|
152
|
+
console.log(chalk.gray(`配置文件: ${paths.openclawConfig}\n`));
|
|
153
|
+
|
|
154
|
+
// 首次运行备份
|
|
155
|
+
if (backupOriginalConfig(paths.openclawConfig, paths.configDir)) {
|
|
156
|
+
console.log(chalk.green('✓ 已备份原始配置\n'));
|
|
246
157
|
}
|
|
247
158
|
|
|
248
159
|
while (true) {
|
|
249
|
-
const { action } = await inquirer.prompt([
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
{ name: '⚙️ 高级设置', value: 'advanced' },
|
|
264
|
-
{ name: '📄 查看当前配置', value: 'view' },
|
|
265
|
-
new inquirer.Separator(),
|
|
266
|
-
{ name: '❌ 退出', value: 'exit' }
|
|
267
|
-
]
|
|
268
|
-
}
|
|
269
|
-
]);
|
|
160
|
+
const { action } = await inquirer.prompt([{
|
|
161
|
+
type: 'list',
|
|
162
|
+
name: 'action',
|
|
163
|
+
message: '请选择操作:',
|
|
164
|
+
choices: [
|
|
165
|
+
{ name: '🌐 选择节点 (测速 + 自动配置备用)', value: 'select_node' },
|
|
166
|
+
{ name: '🤖 选择默认模型', value: 'select_model' },
|
|
167
|
+
{ name: '🔑 设置 API Key', value: 'set_apikey' },
|
|
168
|
+
{ name: '📋 查看当前配置', value: 'view_config' },
|
|
169
|
+
{ name: '🔄 恢复默认配置', value: 'restore' },
|
|
170
|
+
new inquirer.Separator(),
|
|
171
|
+
{ name: '❌ 退出', value: 'exit' }
|
|
172
|
+
]
|
|
173
|
+
}]);
|
|
270
174
|
|
|
271
|
-
|
|
272
|
-
console.log(chalk.green('\n👋 再见!\n'));
|
|
273
|
-
process.exit(0);
|
|
274
|
-
}
|
|
175
|
+
console.log('');
|
|
275
176
|
|
|
276
177
|
try {
|
|
277
178
|
switch (action) {
|
|
278
|
-
case '
|
|
279
|
-
await
|
|
280
|
-
break;
|
|
281
|
-
case 'relay':
|
|
282
|
-
await manageRelay(configManager);
|
|
283
|
-
break;
|
|
284
|
-
case 'endpoint':
|
|
285
|
-
await quickSwitchEndpoint(configManager);
|
|
286
|
-
break;
|
|
287
|
-
case 'model':
|
|
288
|
-
await manageModel(configManager);
|
|
179
|
+
case 'select_node':
|
|
180
|
+
await selectNode(paths);
|
|
289
181
|
break;
|
|
290
|
-
case '
|
|
291
|
-
await
|
|
182
|
+
case 'select_model':
|
|
183
|
+
await selectModel(paths);
|
|
292
184
|
break;
|
|
293
|
-
case '
|
|
294
|
-
await
|
|
185
|
+
case 'set_apikey':
|
|
186
|
+
await setApiKey(paths);
|
|
295
187
|
break;
|
|
296
|
-
case '
|
|
297
|
-
await
|
|
188
|
+
case 'view_config':
|
|
189
|
+
await viewConfig(paths);
|
|
298
190
|
break;
|
|
299
|
-
case '
|
|
300
|
-
await
|
|
301
|
-
break;
|
|
302
|
-
case 'view':
|
|
303
|
-
await viewConfig(configManager);
|
|
191
|
+
case 'restore':
|
|
192
|
+
await restore(paths);
|
|
304
193
|
break;
|
|
194
|
+
case 'exit':
|
|
195
|
+
console.log(chalk.cyan('👋 再见!\n'));
|
|
196
|
+
process.exit(0);
|
|
305
197
|
}
|
|
306
198
|
} catch (error) {
|
|
307
|
-
|
|
199
|
+
console.log(chalk.red(`错误: ${error.message}\n`));
|
|
308
200
|
}
|
|
309
201
|
|
|
310
|
-
console.log('
|
|
202
|
+
console.log('');
|
|
311
203
|
}
|
|
312
204
|
}
|
|
313
205
|
|
|
314
|
-
//
|
|
315
|
-
async function
|
|
316
|
-
console.log(chalk.cyan
|
|
317
|
-
console.log(chalk.gray('此向导将帮助您快速配置中转站的 URL、API Key、Token 和工作区\n'));
|
|
318
|
-
|
|
319
|
-
const defaultWorkspace = getDefaultWorkspace();
|
|
206
|
+
// ============ 选择节点 ============
|
|
207
|
+
async function selectNode(paths) {
|
|
208
|
+
console.log(chalk.cyan('📡 节点测速中...\n'));
|
|
320
209
|
|
|
321
|
-
const
|
|
322
|
-
{ name: 'Claude (云逸 国内) https://yunyi.skem.cn/claude', value: 'yunyi-claude-cn' },
|
|
323
|
-
{ name: 'Claude (云逸 海外) https://yunyi.cfd/claude', value: 'yunyi-claude-global' },
|
|
324
|
-
{ name: 'Codex (云逸 国内) https://yunyi.skem.cn/codex', value: 'yunyi-codex-cn' },
|
|
325
|
-
{ name: 'Codex (云逸 海外) https://yunyi.cfd/codex', value: 'yunyi-codex-global' },
|
|
326
|
-
{ name: '自定义 (其他模型)', value: 'custom' }
|
|
327
|
-
];
|
|
210
|
+
const results = await testAllEndpoints();
|
|
328
211
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
modelType: 'claude',
|
|
334
|
-
api: 'anthropic-messages',
|
|
335
|
-
defaultModelId: 'claude-opus-4-5',
|
|
336
|
-
compactionMode: 'safeguard'
|
|
337
|
-
},
|
|
338
|
-
'yunyi-claude-global': {
|
|
339
|
-
name: 'yunyi-claude',
|
|
340
|
-
baseUrl: 'https://yunyi.cfd/claude/v1/messages',
|
|
341
|
-
modelType: 'claude',
|
|
342
|
-
api: 'anthropic-messages',
|
|
343
|
-
defaultModelId: 'claude-opus-4-5',
|
|
344
|
-
compactionMode: 'safeguard'
|
|
345
|
-
},
|
|
346
|
-
'yunyi-codex-cn': {
|
|
347
|
-
name: 'yunyi-codex',
|
|
348
|
-
baseUrl: 'https://yunyi.skem.cn/codex/response',
|
|
349
|
-
modelType: 'codex',
|
|
350
|
-
api: 'openai-responses',
|
|
351
|
-
defaultModelId: 'gpt-5.2',
|
|
352
|
-
defaultModelName: 'GPT 5.2',
|
|
353
|
-
compactionMode: 'safeguard'
|
|
354
|
-
},
|
|
355
|
-
'yunyi-codex-global': {
|
|
356
|
-
name: 'yunyi-codex',
|
|
357
|
-
baseUrl: 'https://yunyi.cfd/codex/response',
|
|
358
|
-
modelType: 'codex',
|
|
359
|
-
api: 'openai-responses',
|
|
360
|
-
defaultModelId: 'gpt-5.2',
|
|
361
|
-
defaultModelName: 'GPT 5.2',
|
|
362
|
-
compactionMode: 'safeguard'
|
|
363
|
-
},
|
|
364
|
-
custom: {}
|
|
365
|
-
};
|
|
212
|
+
// 按延迟排序
|
|
213
|
+
const sorted = results
|
|
214
|
+
.filter(r => r.success)
|
|
215
|
+
.sort((a, b) => a.latency - b.latency);
|
|
366
216
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
// 步骤 1: 中转站名称和 URL
|
|
373
|
-
const step1 = await inquirer.prompt([
|
|
374
|
-
{
|
|
375
|
-
type: 'input',
|
|
376
|
-
name: 'name',
|
|
377
|
-
message: '中转站名称 (例如: my-relay):',
|
|
378
|
-
default: presetConfig.name || 'default-relay',
|
|
379
|
-
validate: (input) => input.trim() !== '' || '名称不能为空'
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
type: 'input',
|
|
383
|
-
name: 'baseUrl',
|
|
384
|
-
message: '中转站 URL (例如: https://api.example.com/v1):',
|
|
385
|
-
default: presetConfig.baseUrl || '',
|
|
386
|
-
validate: (input) => {
|
|
387
|
-
try {
|
|
388
|
-
const url = new URL(input);
|
|
389
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
390
|
-
return '请输入有效的 HTTP/HTTPS URL';
|
|
391
|
-
}
|
|
392
|
-
return true;
|
|
393
|
-
} catch {
|
|
394
|
-
return '请输入有效的 URL';
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
]);
|
|
217
|
+
if (sorted.length === 0) {
|
|
218
|
+
console.log(chalk.red('\n所有节点都无法访问!'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
399
221
|
|
|
400
|
-
|
|
222
|
+
console.log(chalk.green(`\n🏆 最快节点: ${sorted[0].name} (${sorted[0].latency}ms)\n`));
|
|
401
223
|
|
|
402
|
-
|
|
403
|
-
const {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
{
|
|
417
|
-
type: 'input',
|
|
418
|
-
name: 'name',
|
|
419
|
-
message: '备用中转站名称:',
|
|
420
|
-
default: `${step1.name}-backup`,
|
|
421
|
-
validate: (input) => input.trim() !== '' || '名称不能为空'
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
type: 'input',
|
|
425
|
-
name: 'baseUrl',
|
|
426
|
-
message: '备用端点 Base URL:',
|
|
427
|
-
default: fallbackEndpoint ? fallbackEndpoint.url : '',
|
|
428
|
-
validate: (input) => {
|
|
429
|
-
try {
|
|
430
|
-
const url = new URL(input);
|
|
431
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
432
|
-
return '请输入有效的 HTTP/HTTPS URL';
|
|
433
|
-
}
|
|
434
|
-
return true;
|
|
435
|
-
} catch {
|
|
436
|
-
return '请输入有效的 URL';
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
]);
|
|
441
|
-
backupConfig = {
|
|
442
|
-
name: backupAnswers.name.trim(),
|
|
443
|
-
baseUrl: buildRelayBaseUrl(backupAnswers.baseUrl.trim(), step1.name)
|
|
444
|
-
};
|
|
445
|
-
}
|
|
224
|
+
// 选择主节点
|
|
225
|
+
const { selectedIndex } = await inquirer.prompt([{
|
|
226
|
+
type: 'list',
|
|
227
|
+
name: 'selectedIndex',
|
|
228
|
+
message: '选择主节点 (其他自动设为备用):',
|
|
229
|
+
choices: [
|
|
230
|
+
{ name: `🚀 使用最快节点 (${sorted[0].name})`, value: -1 },
|
|
231
|
+
new inquirer.Separator('--- 或手动选择 ---'),
|
|
232
|
+
...sorted.map((e, i) => ({
|
|
233
|
+
name: `${e.name} - ${e.latency}ms`,
|
|
234
|
+
value: i
|
|
235
|
+
}))
|
|
236
|
+
]
|
|
237
|
+
}]);
|
|
446
238
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const result = await inquirer.prompt([
|
|
451
|
-
{
|
|
452
|
-
type: 'list',
|
|
453
|
-
name: 'modelType',
|
|
454
|
-
message: '选择模型类型:',
|
|
455
|
-
choices: [
|
|
456
|
-
{ name: 'Claude (Anthropic)', value: 'claude' },
|
|
457
|
-
{ name: 'Codex / GPT (OpenAI Responses)', value: 'codex' },
|
|
458
|
-
{ name: '其他/自定义', value: 'other' }
|
|
459
|
-
]
|
|
460
|
-
}
|
|
461
|
-
]);
|
|
462
|
-
modelType = result.modelType;
|
|
463
|
-
}
|
|
239
|
+
const primaryIndex = selectedIndex === -1 ? 0 : selectedIndex;
|
|
240
|
+
const primaryEndpoint = sorted[primaryIndex];
|
|
241
|
+
const fallbackEndpoints = sorted.filter((_, i) => i !== primaryIndex);
|
|
464
242
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
243
|
+
// 读取或创建配置
|
|
244
|
+
let config = readConfig(paths.openclawConfig) || {};
|
|
468
245
|
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (modelType === 'codex') {
|
|
473
|
-
apiType = 'openai-responses';
|
|
474
|
-
} else if (modelType === 'claude') {
|
|
475
|
-
apiType = 'anthropic-messages';
|
|
476
|
-
} else {
|
|
477
|
-
const result = await inquirer.prompt([
|
|
478
|
-
{
|
|
479
|
-
type: 'list',
|
|
480
|
-
name: 'apiType',
|
|
481
|
-
message: 'API 接口类型:',
|
|
482
|
-
choices: [
|
|
483
|
-
{ name: 'Anthropic Messages (Claude)', value: 'anthropic-messages' },
|
|
484
|
-
{ name: 'OpenAI Responses (Codex / GPT-5+)', value: 'openai-responses' },
|
|
485
|
-
{ name: 'OpenAI Completions', value: 'openai-completions' },
|
|
486
|
-
{ name: 'Google Generative AI (Gemini)', value: 'google-generative-ai' },
|
|
487
|
-
{ name: '自定义', value: 'custom' }
|
|
488
|
-
]
|
|
489
|
-
}
|
|
490
|
-
]);
|
|
491
|
-
apiType = result.apiType;
|
|
492
|
-
if (apiType === 'custom') {
|
|
493
|
-
const custom = await inquirer.prompt([
|
|
494
|
-
{ type: 'input', name: 'customApi', message: '输入 API 类型:', validate: (i) => i.trim() !== '' || '不能为空' }
|
|
495
|
-
]);
|
|
496
|
-
apiType = custom.customApi;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
246
|
+
// 获取当前模型(保持不变)
|
|
247
|
+
const currentModelId = config.agents?.defaults?.model?.primary?.split('/')[1] || MODELS[0].id;
|
|
248
|
+
const modelConfig = MODELS.find(m => m.id === currentModelId) || MODELS[0];
|
|
500
249
|
|
|
501
|
-
//
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
type: 'list',
|
|
507
|
-
name: 'modelChoice',
|
|
508
|
-
message: '选择 Claude 模型:',
|
|
509
|
-
choices: [
|
|
510
|
-
{ name: 'Claude Opus 4.5', value: { id: 'claude-opus-4-5', name: 'Claude Opus 4.5' } },
|
|
511
|
-
{ name: 'Claude Sonnet 4.5', value: { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' } },
|
|
512
|
-
{ name: 'Claude Haiku 4.5', value: { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' } }
|
|
513
|
-
],
|
|
514
|
-
default: presetConfig.defaultModelId
|
|
515
|
-
}
|
|
516
|
-
]);
|
|
517
|
-
modelConfig = modelChoice;
|
|
518
|
-
} else if (modelType === 'codex') {
|
|
519
|
-
const { modelChoice } = await inquirer.prompt([
|
|
520
|
-
{
|
|
521
|
-
type: 'list',
|
|
522
|
-
name: 'modelChoice',
|
|
523
|
-
message: '选择 Codex / GPT 模型:',
|
|
524
|
-
choices: [
|
|
525
|
-
{ name: 'GPT 5.2', value: { id: 'gpt-5.2', name: 'GPT 5.2' } },
|
|
526
|
-
{ name: 'GPT 5.2 Codex', value: { id: 'gpt-5.2-codex', name: 'GPT 5.2 Codex' } }
|
|
527
|
-
]
|
|
528
|
-
}
|
|
529
|
-
]);
|
|
530
|
-
modelConfig = modelChoice;
|
|
531
|
-
} else {
|
|
532
|
-
const custom = await inquirer.prompt([
|
|
533
|
-
{ type: 'input', name: 'id', message: '模型 ID:', validate: (i) => i.trim() !== '' || '不能为空' },
|
|
534
|
-
{ type: 'input', name: 'name', message: '模型名称:', validate: (i) => i.trim() !== '' || '不能为空' }
|
|
535
|
-
]);
|
|
536
|
-
modelConfig = { id: custom.id.trim(), name: custom.name.trim() };
|
|
250
|
+
// 清除旧的 yunyi 配置
|
|
251
|
+
if (config.models?.providers) {
|
|
252
|
+
Object.keys(config.models.providers).forEach(key => {
|
|
253
|
+
if (key.startsWith('yunyi-')) delete config.models.providers[key];
|
|
254
|
+
});
|
|
537
255
|
}
|
|
538
256
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
{
|
|
554
|
-
type: 'number',
|
|
555
|
-
name: 'maxTokens',
|
|
556
|
-
message: '最大输出 tokens:',
|
|
557
|
-
default: defaults.maxTokens
|
|
558
|
-
}
|
|
559
|
-
]);
|
|
560
|
-
modelEntry = {
|
|
257
|
+
// 初始化结构
|
|
258
|
+
if (!config.models) config.models = {};
|
|
259
|
+
if (!config.models.providers) config.models.providers = {};
|
|
260
|
+
if (!config.agents) config.agents = {};
|
|
261
|
+
if (!config.agents.defaults) config.agents.defaults = {};
|
|
262
|
+
if (!config.agents.defaults.model) config.agents.defaults.model = {};
|
|
263
|
+
if (!config.agents.defaults.models) config.agents.defaults.models = {};
|
|
264
|
+
|
|
265
|
+
// 添加主节点
|
|
266
|
+
const primaryName = 'yunyi-001';
|
|
267
|
+
config.models.providers[primaryName] = {
|
|
268
|
+
baseUrl: buildFullUrl(primaryEndpoint.url),
|
|
269
|
+
api: 'anthropic-messages',
|
|
270
|
+
models: [{
|
|
561
271
|
id: modelConfig.id,
|
|
562
272
|
name: modelConfig.name,
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
output: 0,
|
|
568
|
-
cacheRead: 0,
|
|
569
|
-
cacheWrite: 0
|
|
570
|
-
},
|
|
571
|
-
contextWindow,
|
|
572
|
-
maxTokens
|
|
573
|
-
};
|
|
574
|
-
}
|
|
273
|
+
contextWindow: 200000,
|
|
274
|
+
maxTokens: 8192
|
|
275
|
+
}]
|
|
276
|
+
};
|
|
575
277
|
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
{
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
mask: '*'
|
|
589
|
-
},
|
|
590
|
-
{
|
|
591
|
-
type: 'password',
|
|
592
|
-
name: 'gatewayToken',
|
|
593
|
-
message: '网关 Token (留空跳过):',
|
|
594
|
-
mask: '*'
|
|
595
|
-
}
|
|
596
|
-
]);
|
|
597
|
-
|
|
598
|
-
// 步骤 6: 工作区
|
|
599
|
-
const step4 = await inquirer.prompt([
|
|
600
|
-
{
|
|
601
|
-
type: 'input',
|
|
602
|
-
name: 'workspace',
|
|
603
|
-
message: `工作区路径 (留空使用默认):`,
|
|
604
|
-
default: defaultWorkspace
|
|
605
|
-
}
|
|
606
|
-
]);
|
|
607
|
-
|
|
608
|
-
// 执行配置
|
|
609
|
-
console.log(chalk.cyan('\n正在保存配置...\n'));
|
|
610
|
-
|
|
611
|
-
try {
|
|
612
|
-
// 添加中转站
|
|
613
|
-
await configManager.addRelay({
|
|
614
|
-
name: step1.name,
|
|
615
|
-
baseUrl: step1.baseUrl,
|
|
616
|
-
auth: 'api-key',
|
|
617
|
-
api: apiType,
|
|
618
|
-
headers: {},
|
|
619
|
-
authHeader: false,
|
|
620
|
-
modelsMode: 'merge',
|
|
621
|
-
model: {
|
|
278
|
+
// 设置主模型
|
|
279
|
+
config.agents.defaults.model.primary = `${primaryName}/${modelConfig.id}`;
|
|
280
|
+
config.agents.defaults.models[`${primaryName}/${modelConfig.id}`] = { alias: primaryName };
|
|
281
|
+
|
|
282
|
+
// 添加备用节点
|
|
283
|
+
const fallbacks = [];
|
|
284
|
+
fallbackEndpoints.forEach((endpoint, i) => {
|
|
285
|
+
const name = `yunyi-${String(i + 2).padStart(3, '0')}`;
|
|
286
|
+
config.models.providers[name] = {
|
|
287
|
+
baseUrl: buildFullUrl(endpoint.url),
|
|
288
|
+
api: 'anthropic-messages',
|
|
289
|
+
models: [{
|
|
622
290
|
id: modelConfig.id,
|
|
623
291
|
name: modelConfig.name,
|
|
624
|
-
contextWindow:
|
|
625
|
-
maxTokens:
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
models: includeModelsList ? [modelEntry] : []
|
|
631
|
-
});
|
|
632
|
-
console.log(chalk.green(' ✓ 中转站已添加'));
|
|
633
|
-
|
|
634
|
-
// 设置为主模型
|
|
635
|
-
await configManager.setPrimaryModel(step1.name, modelConfig.id);
|
|
636
|
-
console.log(chalk.green(' ✓ 已设为主模型'));
|
|
637
|
-
|
|
638
|
-
if (backupConfig) {
|
|
639
|
-
try {
|
|
640
|
-
await configManager.addRelay({
|
|
641
|
-
name: backupConfig.name,
|
|
642
|
-
baseUrl: backupConfig.baseUrl,
|
|
643
|
-
auth: 'api-key',
|
|
644
|
-
api: apiType,
|
|
645
|
-
headers: {},
|
|
646
|
-
authHeader: false,
|
|
647
|
-
modelsMode: 'merge',
|
|
648
|
-
model: {
|
|
649
|
-
id: modelConfig.id,
|
|
650
|
-
name: modelConfig.name,
|
|
651
|
-
contextWindow: includeModelsList ? modelEntry.contextWindow : undefined,
|
|
652
|
-
maxTokens: includeModelsList ? modelEntry.maxTokens : undefined,
|
|
653
|
-
reasoning: includeModelsList ? modelEntry.reasoning : undefined,
|
|
654
|
-
input: includeModelsList ? modelEntry.input : undefined,
|
|
655
|
-
cost: includeModelsList ? modelEntry.cost : undefined
|
|
656
|
-
},
|
|
657
|
-
models: includeModelsList ? [modelEntry] : []
|
|
658
|
-
});
|
|
659
|
-
console.log(chalk.green(' ✓ 备用中转站已添加'));
|
|
660
|
-
|
|
661
|
-
const existingFallbacks = await configManager.getFallbackModels();
|
|
662
|
-
const fallbackKey = `${backupConfig.name}/${modelConfig.id}`;
|
|
663
|
-
const mergedFallbacks = Array.from(new Set([...existingFallbacks, fallbackKey]));
|
|
664
|
-
await configManager.setFallbackModels(mergedFallbacks);
|
|
665
|
-
console.log(chalk.green(' ✓ 已设置备用模型'));
|
|
666
|
-
|
|
667
|
-
if (step3.apiKey && step3.apiKey.trim() !== '') {
|
|
668
|
-
const { useSameKey } = await inquirer.prompt([
|
|
669
|
-
{
|
|
670
|
-
type: 'confirm',
|
|
671
|
-
name: 'useSameKey',
|
|
672
|
-
message: '是否给备用中转站使用相同 API Key?',
|
|
673
|
-
default: true
|
|
674
|
-
}
|
|
675
|
-
]);
|
|
676
|
-
if (useSameKey) {
|
|
677
|
-
await configManager.setApiKey(backupConfig.name, step3.apiKey);
|
|
678
|
-
console.log(chalk.green(' ✓ 备用中转站 API Key 已保存'));
|
|
679
|
-
} else {
|
|
680
|
-
const { backupApiKey } = await inquirer.prompt([
|
|
681
|
-
{
|
|
682
|
-
type: 'password',
|
|
683
|
-
name: 'backupApiKey',
|
|
684
|
-
message: '输入备用中转站 API Key:',
|
|
685
|
-
mask: '*',
|
|
686
|
-
validate: (input) => input.trim() !== '' || 'API Key 不能为空'
|
|
687
|
-
}
|
|
688
|
-
]);
|
|
689
|
-
await configManager.setApiKey(backupConfig.name, backupApiKey);
|
|
690
|
-
console.log(chalk.green(' ✓ 备用中转站 API Key 已保存'));
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
} catch (error) {
|
|
694
|
-
displayWarning(`备用中转站添加失败: ${error.message}`);
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// 保存 API Key
|
|
699
|
-
if (step3.apiKey && step3.apiKey.trim() !== '') {
|
|
700
|
-
await configManager.setApiKey(step1.name, step3.apiKey);
|
|
701
|
-
console.log(chalk.green(' ✓ API Key 已保存'));
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// 保存中转站 Token
|
|
705
|
-
if (step3.providerToken && step3.providerToken.trim() !== '') {
|
|
706
|
-
await configManager.setToken(step1.name, step3.providerToken);
|
|
707
|
-
console.log(chalk.green(' ✓ 中转站 Token 已保存'));
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// 设置工作区 / 网关 Token / 压缩模式
|
|
711
|
-
await configManager.setAdvancedSettings({
|
|
712
|
-
workspace: step4.workspace || defaultWorkspace,
|
|
713
|
-
compactionMode: presetConfig.compactionMode,
|
|
714
|
-
gatewayToken: step3.gatewayToken
|
|
715
|
-
});
|
|
716
|
-
console.log(chalk.green(' ✓ 高级设置已更新'));
|
|
717
|
-
|
|
718
|
-
displaySuccess('\n🎉 配置完成!您现在可以开始使用了。');
|
|
719
|
-
|
|
720
|
-
// 显示配置摘要
|
|
721
|
-
console.log(chalk.cyan('\n配置摘要:'));
|
|
722
|
-
console.log(` 中转站: ${step1.name}`);
|
|
723
|
-
console.log(` URL: ${step1.baseUrl}`);
|
|
724
|
-
console.log(` 模型: ${modelConfig.name}`);
|
|
725
|
-
console.log(` API: ${apiType}`);
|
|
726
|
-
console.log(` API Key: ${step3.apiKey ? chalk.green('已设置') : chalk.gray('未设置')}`);
|
|
727
|
-
console.log(` 网关 Token: ${step3.gatewayToken ? chalk.green('已设置') : chalk.gray('未设置')}`);
|
|
728
|
-
console.log(` 工作区: ${step4.workspace || defaultWorkspace}`);
|
|
729
|
-
|
|
730
|
-
} catch (error) {
|
|
731
|
-
displayError(`配置失败: ${error.message}`);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// 快速切换中转端点
|
|
736
|
-
async function quickSwitchEndpoint(configManager) {
|
|
737
|
-
const relays = await configManager.listRelays();
|
|
738
|
-
if (relays.length === 0) {
|
|
739
|
-
displayInfo('请先添加中转站');
|
|
740
|
-
return;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const { relayName } = await inquirer.prompt([
|
|
744
|
-
{
|
|
745
|
-
type: 'list',
|
|
746
|
-
name: 'relayName',
|
|
747
|
-
message: '选择要切换端点的中转站:',
|
|
748
|
-
choices: relays.map(r => ({ name: `${r.name} (${r.baseUrl})`, value: r.name }))
|
|
749
|
-
}
|
|
750
|
-
]);
|
|
751
|
-
|
|
752
|
-
const relay = relays.find(r => r.name === relayName);
|
|
753
|
-
const endpoints = getRelayEndpoints(relayName);
|
|
754
|
-
let selectedUrl = '';
|
|
755
|
-
|
|
756
|
-
if (endpoints.length > 0) {
|
|
757
|
-
const { endpoint } = await inquirer.prompt([
|
|
758
|
-
{
|
|
759
|
-
type: 'list',
|
|
760
|
-
name: 'endpoint',
|
|
761
|
-
message: '选择接入端点:',
|
|
762
|
-
choices: [
|
|
763
|
-
...endpoints.map(item => ({ name: item.name, value: item.url })),
|
|
764
|
-
{ name: '自定义', value: '__custom__' }
|
|
765
|
-
]
|
|
766
|
-
}
|
|
767
|
-
]);
|
|
768
|
-
|
|
769
|
-
if (endpoint === '__custom__') {
|
|
770
|
-
const custom = await inquirer.prompt([
|
|
771
|
-
{
|
|
772
|
-
type: 'input',
|
|
773
|
-
name: 'url',
|
|
774
|
-
message: '输入 Base URL:',
|
|
775
|
-
validate: (input) => {
|
|
776
|
-
try {
|
|
777
|
-
const url = new URL(input);
|
|
778
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
779
|
-
return '请输入有效的 HTTP/HTTPS URL';
|
|
780
|
-
}
|
|
781
|
-
return true;
|
|
782
|
-
} catch {
|
|
783
|
-
return '请输入有效的 URL';
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
]);
|
|
788
|
-
selectedUrl = custom.url;
|
|
789
|
-
} else {
|
|
790
|
-
selectedUrl = endpoint;
|
|
791
|
-
}
|
|
792
|
-
} else {
|
|
793
|
-
const custom = await inquirer.prompt([
|
|
794
|
-
{
|
|
795
|
-
type: 'input',
|
|
796
|
-
name: 'url',
|
|
797
|
-
message: '输入 Base URL:',
|
|
798
|
-
default: relay.baseUrl,
|
|
799
|
-
validate: (input) => {
|
|
800
|
-
try {
|
|
801
|
-
const url = new URL(input);
|
|
802
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
803
|
-
return '请输入有效的 HTTP/HTTPS URL';
|
|
804
|
-
}
|
|
805
|
-
return true;
|
|
806
|
-
} catch {
|
|
807
|
-
return '请输入有效的 URL';
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
]);
|
|
812
|
-
selectedUrl = custom.url;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const finalUrl = buildRelayBaseUrl(selectedUrl, relayName);
|
|
816
|
-
if (finalUrl && finalUrl !== relay.baseUrl) {
|
|
817
|
-
await configManager.updateRelay(relayName, { baseUrl: finalUrl });
|
|
818
|
-
displaySuccess(`✅ 已切换 "${relayName}" 的端点`);
|
|
819
|
-
} else {
|
|
820
|
-
displayInfo('端点未变化');
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// 快速切换模型
|
|
825
|
-
async function quickSwitchModel(configManager) {
|
|
826
|
-
const relays = await configManager.listRelays();
|
|
827
|
-
if (relays.length === 0) {
|
|
828
|
-
displayInfo('请先添加中转站');
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
const { relayName } = await inquirer.prompt([
|
|
833
|
-
{
|
|
834
|
-
type: 'list',
|
|
835
|
-
name: 'relayName',
|
|
836
|
-
message: '选择要切换模型的中转站:',
|
|
837
|
-
choices: relays.map(r => ({ name: `${r.name} (${r.modelName})`, value: r.name }))
|
|
838
|
-
}
|
|
839
|
-
]);
|
|
840
|
-
|
|
841
|
-
const config = await configManager.readOpenclawConfig();
|
|
842
|
-
const provider = config.models?.providers?.[relayName] || {};
|
|
843
|
-
const apiType = provider.api || (normalizeRelayKey(relayName) === 'yunyi-codex'
|
|
844
|
-
? 'openai-responses'
|
|
845
|
-
: 'anthropic-messages');
|
|
846
|
-
const providerModels = Array.isArray(provider.models) ? provider.models : [];
|
|
847
|
-
const presetModels = getRelayModelPresets(relayName);
|
|
848
|
-
|
|
849
|
-
const choiceMap = new Map();
|
|
850
|
-
const choices = [];
|
|
851
|
-
|
|
852
|
-
const allowedIds = new Set(presetModels.map(m => m.id));
|
|
853
|
-
|
|
854
|
-
providerModels.forEach(m => {
|
|
855
|
-
const key = m.id;
|
|
856
|
-
if (allowedIds.has(key) && !choiceMap.has(key)) {
|
|
857
|
-
choiceMap.set(key, m);
|
|
858
|
-
choices.push({ name: `${m.name || m.id} (${m.id})`, value: m.id });
|
|
859
|
-
}
|
|
860
|
-
});
|
|
861
|
-
|
|
862
|
-
presetModels.forEach(m => {
|
|
863
|
-
if (!choiceMap.has(m.id)) {
|
|
864
|
-
choiceMap.set(m.id, m);
|
|
865
|
-
choices.push({ name: `${m.name} (${m.id})`, value: m.id });
|
|
866
|
-
}
|
|
292
|
+
contextWindow: 200000,
|
|
293
|
+
maxTokens: 8192
|
|
294
|
+
}]
|
|
295
|
+
};
|
|
296
|
+
fallbacks.push(`${name}/${modelConfig.id}`);
|
|
297
|
+
config.agents.defaults.models[`${name}/${modelConfig.id}`] = { alias: name };
|
|
867
298
|
});
|
|
868
299
|
|
|
869
|
-
|
|
870
|
-
displayInfo('没有可用的模型选项');
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
300
|
+
config.agents.defaults.model.fallbacks = fallbacks;
|
|
873
301
|
|
|
874
|
-
|
|
875
|
-
{
|
|
876
|
-
type: 'list',
|
|
877
|
-
name: 'selected',
|
|
878
|
-
message: '选择模型:',
|
|
879
|
-
choices
|
|
880
|
-
}
|
|
881
|
-
]);
|
|
302
|
+
writeConfig(paths.openclawConfig, config);
|
|
882
303
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
};
|
|
888
|
-
|
|
889
|
-
await configManager.setPrimaryModel(relayName, model.id);
|
|
890
|
-
displaySuccess(`✅ 已切换主模型为 "${relayName}/${model.id}"`);
|
|
891
|
-
|
|
892
|
-
if (apiType !== 'anthropic-messages' && Array.isArray(provider.models)) {
|
|
893
|
-
const exists = providerModels.some(m => m.id === model.id);
|
|
894
|
-
if (!exists) {
|
|
895
|
-
const { shouldAdd } = await inquirer.prompt([
|
|
896
|
-
{
|
|
897
|
-
type: 'confirm',
|
|
898
|
-
name: 'shouldAdd',
|
|
899
|
-
message: '是否将模型写入 providers.models 列表?',
|
|
900
|
-
default: false
|
|
901
|
-
}
|
|
902
|
-
]);
|
|
903
|
-
if (shouldAdd) {
|
|
904
|
-
provider.models.push(buildModelEntry(model, apiType));
|
|
905
|
-
await configManager.writeOpenclawConfig(config);
|
|
906
|
-
displaySuccess('✅ 已更新模型列表');
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
304
|
+
console.log(chalk.green(`\n✅ 配置完成!`));
|
|
305
|
+
console.log(chalk.cyan(` 主节点: ${primaryEndpoint.name} (${primaryEndpoint.url})`));
|
|
306
|
+
console.log(chalk.gray(` 备用节点: ${fallbackEndpoints.length} 个`));
|
|
307
|
+
console.log(chalk.gray(` 当前模型: ${modelConfig.name}`));
|
|
910
308
|
}
|
|
911
309
|
|
|
912
|
-
//
|
|
913
|
-
async function
|
|
914
|
-
|
|
915
|
-
{
|
|
916
|
-
type: 'list',
|
|
917
|
-
name: 'relayAction',
|
|
918
|
-
message: '中转站管理:',
|
|
919
|
-
choices: [
|
|
920
|
-
{ name: '➕ 添加新中转站', value: 'add' },
|
|
921
|
-
{ name: '📝 编辑现有中转站', value: 'edit' },
|
|
922
|
-
{ name: '🗑️ 删除中转站', value: 'delete' },
|
|
923
|
-
{ name: '🔄 切换主中转站', value: 'switch' },
|
|
924
|
-
{ name: '⬅️ 返回', value: 'back' }
|
|
925
|
-
]
|
|
926
|
-
}
|
|
927
|
-
]);
|
|
928
|
-
|
|
929
|
-
if (relayAction === 'back') return;
|
|
930
|
-
|
|
931
|
-
switch (relayAction) {
|
|
932
|
-
case 'add':
|
|
933
|
-
await addRelay(configManager);
|
|
934
|
-
break;
|
|
935
|
-
case 'edit':
|
|
936
|
-
await editRelay(configManager);
|
|
937
|
-
break;
|
|
938
|
-
case 'delete':
|
|
939
|
-
await deleteRelay(configManager);
|
|
940
|
-
break;
|
|
941
|
-
case 'switch':
|
|
942
|
-
await switchRelay(configManager);
|
|
943
|
-
break;
|
|
944
|
-
}
|
|
945
|
-
}
|
|
310
|
+
// ============ 选择模型 ============
|
|
311
|
+
async function selectModel(paths) {
|
|
312
|
+
console.log(chalk.cyan('🤖 选择默认模型\n'));
|
|
946
313
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
type: 'input',
|
|
954
|
-
name: 'name',
|
|
955
|
-
message: '中转站名称 (例如: claude-relay-1):',
|
|
956
|
-
validate: (input) => input.trim() !== '' || '名称不能为空'
|
|
957
|
-
},
|
|
958
|
-
{
|
|
959
|
-
type: 'input',
|
|
960
|
-
name: 'baseUrl',
|
|
961
|
-
message: 'Base URL (例如: https://yunyi.cfd/claude/v1):',
|
|
962
|
-
validate: (input) => {
|
|
963
|
-
try {
|
|
964
|
-
new URL(input);
|
|
965
|
-
return true;
|
|
966
|
-
} catch {
|
|
967
|
-
return '请输入有效的 URL';
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
},
|
|
971
|
-
{
|
|
972
|
-
type: 'list',
|
|
973
|
-
name: 'modelType',
|
|
974
|
-
message: '模型类型:',
|
|
975
|
-
choices: [
|
|
976
|
-
{ name: 'Claude (Anthropic)', value: 'claude' },
|
|
977
|
-
{ name: 'Codex / GPT (OpenAI Responses)', value: 'codex' },
|
|
978
|
-
{ name: '其他/自定义', value: 'other' }
|
|
979
|
-
]
|
|
980
|
-
}
|
|
981
|
-
]);
|
|
982
|
-
|
|
983
|
-
answers.baseUrl = buildRelayBaseUrl(answers.baseUrl, answers.name);
|
|
984
|
-
|
|
985
|
-
let apiType = answers.modelType === 'codex'
|
|
986
|
-
? 'openai-responses'
|
|
987
|
-
: 'anthropic-messages';
|
|
988
|
-
if (answers.modelType === 'other') {
|
|
989
|
-
displayWarning('非 GPT/Claude 模型仅做配置写入,不保证可用,请优先用 OpenClaw 配置页面。');
|
|
990
|
-
const result = await inquirer.prompt([
|
|
991
|
-
{
|
|
992
|
-
type: 'list',
|
|
993
|
-
name: 'apiType',
|
|
994
|
-
message: 'API 接口类型:',
|
|
995
|
-
choices: [
|
|
996
|
-
{ name: 'Anthropic Messages (Claude)', value: 'anthropic-messages' },
|
|
997
|
-
{ name: 'OpenAI Responses (Codex / GPT-5+)', value: 'openai-responses' },
|
|
998
|
-
{ name: 'OpenAI Completions', value: 'openai-completions' },
|
|
999
|
-
{ name: 'Google Generative AI (Gemini)', value: 'google-generative-ai' },
|
|
1000
|
-
{ name: '自定义', value: 'custom' }
|
|
1001
|
-
]
|
|
1002
|
-
}
|
|
1003
|
-
]);
|
|
1004
|
-
apiType = result.apiType;
|
|
1005
|
-
if (apiType === 'custom') {
|
|
1006
|
-
const custom = await inquirer.prompt([
|
|
1007
|
-
{ type: 'input', name: 'customApi', message: '输入 API 类型:', validate: (i) => i.trim() !== '' || '不能为空' }
|
|
1008
|
-
]);
|
|
1009
|
-
apiType = custom.customApi;
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
314
|
+
const { selectedModel } = await inquirer.prompt([{
|
|
315
|
+
type: 'list',
|
|
316
|
+
name: 'selectedModel',
|
|
317
|
+
message: '选择模型:',
|
|
318
|
+
choices: MODELS.map(m => ({ name: m.name, value: m.id }))
|
|
319
|
+
}]);
|
|
1012
320
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
const { modelChoice } = await inquirer.prompt([
|
|
1016
|
-
{
|
|
1017
|
-
type: 'list',
|
|
1018
|
-
name: 'modelChoice',
|
|
1019
|
-
message: '选择 Claude 模型:',
|
|
1020
|
-
choices: [
|
|
1021
|
-
{ name: 'Claude Opus 4.5', value: { id: 'claude-opus-4-5', name: 'Claude Opus 4.5' } },
|
|
1022
|
-
{ name: 'Claude Sonnet 4.5', value: { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5' } },
|
|
1023
|
-
{ name: 'Claude Haiku 4.5', value: { id: 'claude-haiku-4-5', name: 'Claude Haiku 4.5' } }
|
|
1024
|
-
]
|
|
1025
|
-
}
|
|
1026
|
-
]);
|
|
1027
|
-
|
|
1028
|
-
modelConfig = modelChoice;
|
|
1029
|
-
} else if (answers.modelType === 'codex') {
|
|
1030
|
-
const { modelChoice } = await inquirer.prompt([
|
|
1031
|
-
{
|
|
1032
|
-
type: 'list',
|
|
1033
|
-
name: 'modelChoice',
|
|
1034
|
-
message: '选择 Codex / GPT 模型:',
|
|
1035
|
-
choices: [
|
|
1036
|
-
{ name: 'GPT 5.2', value: { id: 'gpt-5.2', name: 'GPT 5.2' } },
|
|
1037
|
-
{ name: 'GPT 5.2 Codex', value: { id: 'gpt-5.2-codex', name: 'GPT 5.2 Codex' } }
|
|
1038
|
-
]
|
|
1039
|
-
}
|
|
1040
|
-
]);
|
|
1041
|
-
modelConfig = modelChoice;
|
|
1042
|
-
} else {
|
|
1043
|
-
const custom = await inquirer.prompt([
|
|
1044
|
-
{ type: 'input', name: 'id', message: '模型 ID:', validate: (i) => i.trim() !== '' || '不能为空' },
|
|
1045
|
-
{ type: 'input', name: 'name', message: '模型名称:', validate: (i) => i.trim() !== '' || '不能为空' }
|
|
1046
|
-
]);
|
|
1047
|
-
modelConfig = { id: custom.id.trim(), name: custom.name.trim() };
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
const includeModelsList = apiType !== 'anthropic-messages';
|
|
1051
|
-
let modelEntry = null;
|
|
1052
|
-
if (includeModelsList) {
|
|
1053
|
-
const defaults = answers.modelType === 'codex'
|
|
1054
|
-
? { contextWindow: 128000, maxTokens: 32768 }
|
|
1055
|
-
: { contextWindow: 200000, maxTokens: 8192 };
|
|
1056
|
-
const { contextWindow, maxTokens } = await inquirer.prompt([
|
|
1057
|
-
{
|
|
1058
|
-
type: 'number',
|
|
1059
|
-
name: 'contextWindow',
|
|
1060
|
-
message: '上下文窗口大小:',
|
|
1061
|
-
default: defaults.contextWindow
|
|
1062
|
-
},
|
|
1063
|
-
{
|
|
1064
|
-
type: 'number',
|
|
1065
|
-
name: 'maxTokens',
|
|
1066
|
-
message: '最大输出 tokens:',
|
|
1067
|
-
default: defaults.maxTokens
|
|
1068
|
-
}
|
|
1069
|
-
]);
|
|
1070
|
-
modelEntry = {
|
|
1071
|
-
id: modelConfig.id,
|
|
1072
|
-
name: modelConfig.name,
|
|
1073
|
-
reasoning: apiType === 'openai-responses',
|
|
1074
|
-
input: apiType === 'openai-responses' ? ['text', 'image'] : ['text'],
|
|
1075
|
-
cost: {
|
|
1076
|
-
input: 0,
|
|
1077
|
-
output: 0,
|
|
1078
|
-
cacheRead: 0,
|
|
1079
|
-
cacheWrite: 0
|
|
1080
|
-
},
|
|
1081
|
-
contextWindow,
|
|
1082
|
-
maxTokens
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
await configManager.addRelay({
|
|
1087
|
-
name: answers.name,
|
|
1088
|
-
baseUrl: answers.baseUrl,
|
|
1089
|
-
auth: 'api-key',
|
|
1090
|
-
api: apiType,
|
|
1091
|
-
headers: {},
|
|
1092
|
-
authHeader: false,
|
|
1093
|
-
modelsMode: 'merge',
|
|
1094
|
-
model: {
|
|
1095
|
-
id: modelConfig.id,
|
|
1096
|
-
name: modelConfig.name,
|
|
1097
|
-
contextWindow: includeModelsList ? modelEntry.contextWindow : undefined,
|
|
1098
|
-
maxTokens: includeModelsList ? modelEntry.maxTokens : undefined,
|
|
1099
|
-
reasoning: includeModelsList ? modelEntry.reasoning : undefined,
|
|
1100
|
-
input: includeModelsList ? modelEntry.input : undefined,
|
|
1101
|
-
cost: includeModelsList ? modelEntry.cost : undefined
|
|
1102
|
-
},
|
|
1103
|
-
models: includeModelsList ? [modelEntry] : []
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
displaySuccess(`✅ 中转站 "${answers.name}" 添加成功!`);
|
|
1107
|
-
}
|
|
321
|
+
const modelConfig = MODELS.find(m => m.id === selectedModel);
|
|
322
|
+
let config = readConfig(paths.openclawConfig);
|
|
1108
323
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const relays = await configManager.listRelays();
|
|
1112
|
-
if (relays.length === 0) {
|
|
1113
|
-
displayInfo('没有可编辑的中转站');
|
|
324
|
+
if (!config?.models?.providers) {
|
|
325
|
+
console.log(chalk.yellow('⚠️ 请先选择节点'));
|
|
1114
326
|
return;
|
|
1115
327
|
}
|
|
1116
328
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
name
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
const relay = relays.find(r => r.name === relayName);
|
|
1127
|
-
const config = await configManager.readOpenclawConfig();
|
|
1128
|
-
const provider = config.models?.providers?.[relayName] || {};
|
|
1129
|
-
const apiTypeDefault = provider.api || (normalizeRelayKey(relayName) === 'yunyi-codex'
|
|
1130
|
-
? 'openai-responses'
|
|
1131
|
-
: 'anthropic-messages');
|
|
1132
|
-
const hasModelList = Array.isArray(provider.models) && provider.models.length > 0;
|
|
1133
|
-
if (!['yunyi-claude', 'yunyi-codex'].includes(normalizeRelayKey(relayName))) {
|
|
1134
|
-
displayWarning('该中转站不在本工具保障范围,仅做配置写入。');
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
const apiChoices = [
|
|
1138
|
-
{ name: 'Anthropic Messages (Claude)', value: 'anthropic-messages' },
|
|
1139
|
-
{ name: 'OpenAI Responses (Codex / GPT-5+)', value: 'openai-responses' },
|
|
1140
|
-
{ name: 'OpenAI Completions', value: 'openai-completions' },
|
|
1141
|
-
{ name: 'Google Generative AI (Gemini)', value: 'google-generative-ai' },
|
|
1142
|
-
{ name: '自定义', value: 'custom' }
|
|
1143
|
-
];
|
|
1144
|
-
|
|
1145
|
-
if (apiTypeDefault && !apiChoices.some(choice => choice.value === apiTypeDefault)) {
|
|
1146
|
-
apiChoices.unshift({ name: `当前 (${apiTypeDefault})`, value: apiTypeDefault });
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
const questions = [
|
|
1150
|
-
{
|
|
1151
|
-
type: 'input',
|
|
1152
|
-
name: 'baseUrl',
|
|
1153
|
-
message: 'Base URL:',
|
|
1154
|
-
default: relay.baseUrl
|
|
1155
|
-
},
|
|
1156
|
-
{
|
|
1157
|
-
type: 'list',
|
|
1158
|
-
name: 'api',
|
|
1159
|
-
message: 'API 接口类型:',
|
|
1160
|
-
default: apiTypeDefault,
|
|
1161
|
-
choices: apiChoices
|
|
1162
|
-
},
|
|
1163
|
-
{
|
|
1164
|
-
type: 'confirm',
|
|
1165
|
-
name: 'authHeader',
|
|
1166
|
-
message: '是否启用 authHeader?',
|
|
1167
|
-
default: !!provider.authHeader
|
|
1168
|
-
},
|
|
1169
|
-
{
|
|
1170
|
-
type: 'input',
|
|
1171
|
-
name: 'headers',
|
|
1172
|
-
message: 'Headers (JSON/JSON5,留空保持不变):',
|
|
1173
|
-
default: provider.headers ? JSON.stringify(provider.headers) : ''
|
|
1174
|
-
},
|
|
1175
|
-
{
|
|
1176
|
-
type: 'password',
|
|
1177
|
-
name: 'apiKey',
|
|
1178
|
-
message: 'API Key (留空保持不变):',
|
|
1179
|
-
mask: '*'
|
|
1180
|
-
}
|
|
1181
|
-
];
|
|
1182
|
-
|
|
1183
|
-
if (hasModelList) {
|
|
1184
|
-
questions.push(
|
|
1185
|
-
{
|
|
1186
|
-
type: 'number',
|
|
1187
|
-
name: 'contextWindow',
|
|
1188
|
-
message: '上下文窗口:',
|
|
1189
|
-
default: relay.contextWindow
|
|
1190
|
-
},
|
|
1191
|
-
{
|
|
1192
|
-
type: 'number',
|
|
1193
|
-
name: 'maxTokens',
|
|
1194
|
-
message: '最大输出 tokens:',
|
|
1195
|
-
default: relay.maxTokens
|
|
1196
|
-
}
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const answers = await inquirer.prompt(questions);
|
|
1201
|
-
|
|
1202
|
-
if (answers.api === 'custom') {
|
|
1203
|
-
const custom = await inquirer.prompt([
|
|
1204
|
-
{ type: 'input', name: 'customApi', message: '输入 API 类型:', validate: (i) => i.trim() !== '' || '不能为空' }
|
|
1205
|
-
]);
|
|
1206
|
-
answers.api = custom.customApi;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
if (answers.headers && answers.headers.trim() !== '') {
|
|
1210
|
-
try {
|
|
1211
|
-
answers.headers = JSON5.parse(answers.headers);
|
|
1212
|
-
} catch (error) {
|
|
1213
|
-
displayError(`Headers 解析失败: ${error.message}`);
|
|
1214
|
-
return;
|
|
329
|
+
// 更新所有 yunyi 节点的模型
|
|
330
|
+
Object.keys(config.models.providers).forEach(name => {
|
|
331
|
+
if (name.startsWith('yunyi-')) {
|
|
332
|
+
config.models.providers[name].models = [{
|
|
333
|
+
id: modelConfig.id,
|
|
334
|
+
name: modelConfig.name,
|
|
335
|
+
contextWindow: 200000,
|
|
336
|
+
maxTokens: 8192
|
|
337
|
+
}];
|
|
1215
338
|
}
|
|
1216
|
-
}
|
|
1217
|
-
delete answers.headers;
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
if (answers.baseUrl) {
|
|
1221
|
-
answers.baseUrl = buildRelayBaseUrl(answers.baseUrl, relayName);
|
|
1222
|
-
}
|
|
339
|
+
});
|
|
1223
340
|
|
|
1224
|
-
|
|
1225
|
-
|
|
341
|
+
// 更新主模型和备用模型引用
|
|
342
|
+
const providers = Object.keys(config.models.providers).filter(k => k.startsWith('yunyi-')).sort();
|
|
343
|
+
if (providers.length > 0) {
|
|
344
|
+
config.agents.defaults.model.primary = `${providers[0]}/${modelConfig.id}`;
|
|
345
|
+
config.agents.defaults.model.fallbacks = providers.slice(1).map(p => `${p}/${modelConfig.id}`);
|
|
346
|
+
config.agents.defaults.models = {};
|
|
347
|
+
providers.forEach(p => {
|
|
348
|
+
config.agents.defaults.models[`${p}/${modelConfig.id}`] = { alias: p };
|
|
349
|
+
});
|
|
1226
350
|
}
|
|
1227
351
|
|
|
1228
|
-
|
|
1229
|
-
|
|
352
|
+
writeConfig(paths.openclawConfig, config);
|
|
353
|
+
console.log(chalk.green(`\n✅ 模型已切换为: ${modelConfig.name}`));
|
|
1230
354
|
}
|
|
1231
355
|
|
|
1232
|
-
//
|
|
1233
|
-
async function
|
|
1234
|
-
|
|
1235
|
-
if (relays.length === 0) {
|
|
1236
|
-
displayInfo('没有可删除的中转站');
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
356
|
+
// ============ 设置 API Key ============
|
|
357
|
+
async function setApiKey(paths) {
|
|
358
|
+
console.log(chalk.cyan('🔑 设置 API Key\n'));
|
|
1239
359
|
|
|
1240
|
-
const {
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
]);
|
|
1248
|
-
|
|
1249
|
-
const { confirm } = await inquirer.prompt([
|
|
1250
|
-
{
|
|
1251
|
-
type: 'confirm',
|
|
1252
|
-
name: 'confirm',
|
|
1253
|
-
message: `确定要删除 "${relayName}" 吗?`,
|
|
1254
|
-
default: false
|
|
1255
|
-
}
|
|
1256
|
-
]);
|
|
360
|
+
const { apiKey } = await inquirer.prompt([{
|
|
361
|
+
type: 'password',
|
|
362
|
+
name: 'apiKey',
|
|
363
|
+
message: '请输入 API Key:',
|
|
364
|
+
mask: '*',
|
|
365
|
+
validate: input => input.trim() !== '' || 'API Key 不能为空'
|
|
366
|
+
}]);
|
|
1257
367
|
|
|
1258
|
-
|
|
1259
|
-
await configManager.deleteRelay(relayName);
|
|
1260
|
-
displaySuccess(`✅ 中转站 "${relayName}" 已删除`);
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
368
|
+
let config = readConfig(paths.openclawConfig);
|
|
1263
369
|
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
const relays = await configManager.listRelays();
|
|
1267
|
-
if (relays.length === 0) {
|
|
1268
|
-
displayInfo('没有可用的中转站');
|
|
370
|
+
if (!config?.models?.providers) {
|
|
371
|
+
console.log(chalk.yellow('⚠️ 请先选择节点'));
|
|
1269
372
|
return;
|
|
1270
373
|
}
|
|
1271
374
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
type: 'list',
|
|
1277
|
-
name: 'relayName',
|
|
1278
|
-
message: '选择主中转站:',
|
|
1279
|
-
choices: relays.map(r => ({
|
|
1280
|
-
name: `${r.name} - ${r.modelName} ${r.name === currentPrimary.provider ? chalk.green('(当前)') : ''}`,
|
|
1281
|
-
value: r.name
|
|
1282
|
-
}))
|
|
375
|
+
// 为所有 yunyi 节点设置 API Key
|
|
376
|
+
Object.keys(config.models.providers).forEach(name => {
|
|
377
|
+
if (name.startsWith('yunyi-')) {
|
|
378
|
+
config.models.providers[name].apiKey = apiKey.trim();
|
|
1283
379
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const selected = relays.find(r => r.name === relayName);
|
|
1287
|
-
let modelId = selected?.modelId;
|
|
1288
|
-
if (!modelId) {
|
|
1289
|
-
const result = await inquirer.prompt([
|
|
1290
|
-
{
|
|
1291
|
-
type: 'input',
|
|
1292
|
-
name: 'modelId',
|
|
1293
|
-
message: '该中转站未配置模型 ID,请输入模型 ID:',
|
|
1294
|
-
validate: (input) => input.trim() !== '' || '模型 ID 不能为空'
|
|
1295
|
-
}
|
|
1296
|
-
]);
|
|
1297
|
-
modelId = result.modelId;
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
await configManager.setPrimaryModel(relayName, modelId);
|
|
1301
|
-
displaySuccess(`✅ 已切换到 "${relayName}"`);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// 管理模型配置
|
|
1305
|
-
async function manageModel(configManager) {
|
|
1306
|
-
const { modelAction } = await inquirer.prompt([
|
|
1307
|
-
{
|
|
1308
|
-
type: 'list',
|
|
1309
|
-
name: 'modelAction',
|
|
1310
|
-
message: '模型管理:',
|
|
1311
|
-
choices: [
|
|
1312
|
-
{ name: '🔄 切换主模型', value: 'switch' },
|
|
1313
|
-
{ name: '📋 管理备用模型', value: 'fallback' },
|
|
1314
|
-
{ name: '⬅️ 返回', value: 'back' }
|
|
1315
|
-
]
|
|
1316
|
-
}
|
|
1317
|
-
]);
|
|
380
|
+
});
|
|
1318
381
|
|
|
1319
|
-
|
|
382
|
+
writeConfig(paths.openclawConfig, config);
|
|
1320
383
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
case 'fallback':
|
|
1326
|
-
await manageFallback(configManager);
|
|
1327
|
-
break;
|
|
384
|
+
// 同时写入 auth-profiles
|
|
385
|
+
const authDir = path.dirname(paths.authProfiles);
|
|
386
|
+
if (!fs.existsSync(authDir)) {
|
|
387
|
+
fs.mkdirSync(authDir, { recursive: true });
|
|
1328
388
|
}
|
|
1329
|
-
}
|
|
1330
389
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
const currentFallbacks = await configManager.getFallbackModels();
|
|
1335
|
-
const availableRelays = relays.filter(r => r.modelId);
|
|
1336
|
-
|
|
1337
|
-
if (availableRelays.length !== relays.length) {
|
|
1338
|
-
displayWarning('部分中转站未配置模型 ID,已跳过');
|
|
390
|
+
let authProfiles = {};
|
|
391
|
+
if (fs.existsSync(paths.authProfiles)) {
|
|
392
|
+
try { authProfiles = JSON.parse(fs.readFileSync(paths.authProfiles, 'utf8')); } catch {}
|
|
1339
393
|
}
|
|
1340
394
|
|
|
1341
|
-
|
|
1342
|
-
{
|
|
1343
|
-
|
|
1344
|
-
name: 'selected',
|
|
1345
|
-
message: '选择备用模型(按优先级排序):',
|
|
1346
|
-
choices: availableRelays.map(r => ({
|
|
1347
|
-
name: `${r.name} - ${r.modelName}`,
|
|
1348
|
-
value: `${r.name}/${r.modelId}`,
|
|
1349
|
-
checked: currentFallbacks.includes(`${r.name}/${r.modelId}`)
|
|
1350
|
-
}))
|
|
395
|
+
Object.keys(config.models.providers).forEach(name => {
|
|
396
|
+
if (name.startsWith('yunyi-')) {
|
|
397
|
+
authProfiles[`${name}:default`] = { apiKey: apiKey.trim() };
|
|
1351
398
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
await configManager.setFallbackModels(selected);
|
|
1355
|
-
displaySuccess('✅ 备用模型配置已更新');
|
|
1356
|
-
}
|
|
1357
|
-
|
|
1358
|
-
// 管理 API Keys 和 Tokens
|
|
1359
|
-
async function manageApiKey(configManager) {
|
|
1360
|
-
const { keyAction } = await inquirer.prompt([
|
|
1361
|
-
{
|
|
1362
|
-
type: 'list',
|
|
1363
|
-
name: 'keyAction',
|
|
1364
|
-
message: 'API Key / Token 管理:',
|
|
1365
|
-
choices: [
|
|
1366
|
-
{ name: '➕ 添加/更新 API Key', value: 'add' },
|
|
1367
|
-
{ name: '🔐 添加/更新 Token', value: 'addToken' },
|
|
1368
|
-
{ name: '👁️ 查看已配置的 Keys/Tokens', value: 'view' },
|
|
1369
|
-
{ name: '🗑️ 删除 API Key', value: 'delete' },
|
|
1370
|
-
{ name: '🗑️ 删除 Token', value: 'deleteToken' },
|
|
1371
|
-
{ name: '⬅️ 返回', value: 'back' }
|
|
1372
|
-
]
|
|
1373
|
-
}
|
|
1374
|
-
]);
|
|
1375
|
-
|
|
1376
|
-
if (keyAction === 'back') return;
|
|
1377
|
-
|
|
1378
|
-
switch (keyAction) {
|
|
1379
|
-
case 'add':
|
|
1380
|
-
await addApiKey(configManager);
|
|
1381
|
-
break;
|
|
1382
|
-
case 'addToken':
|
|
1383
|
-
await addToken(configManager);
|
|
1384
|
-
break;
|
|
1385
|
-
case 'view':
|
|
1386
|
-
await viewApiKeys(configManager);
|
|
1387
|
-
break;
|
|
1388
|
-
case 'delete':
|
|
1389
|
-
await deleteApiKey(configManager);
|
|
1390
|
-
break;
|
|
1391
|
-
case 'deleteToken':
|
|
1392
|
-
await deleteToken(configManager);
|
|
1393
|
-
break;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// 添加 API Key
|
|
1398
|
-
async function addApiKey(configManager) {
|
|
1399
|
-
const relays = await configManager.listRelays();
|
|
1400
|
-
|
|
1401
|
-
if (relays.length === 0) {
|
|
1402
|
-
displayInfo('请先添加中转站');
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
399
|
+
});
|
|
1405
400
|
|
|
1406
|
-
|
|
1407
|
-
{
|
|
1408
|
-
type: 'list',
|
|
1409
|
-
name: 'relayName',
|
|
1410
|
-
message: '选择中转站:',
|
|
1411
|
-
choices: relays.map(r => ({ name: r.name, value: r.name }))
|
|
1412
|
-
}
|
|
1413
|
-
]);
|
|
1414
|
-
|
|
1415
|
-
const { apiKey } = await inquirer.prompt([
|
|
1416
|
-
{
|
|
1417
|
-
type: 'password',
|
|
1418
|
-
name: 'apiKey',
|
|
1419
|
-
message: '输入 API Key:',
|
|
1420
|
-
mask: '*',
|
|
1421
|
-
validate: (input) => input.trim() !== '' || 'API Key 不能为空'
|
|
1422
|
-
}
|
|
1423
|
-
]);
|
|
401
|
+
fs.writeFileSync(paths.authProfiles, JSON.stringify(authProfiles, null, 2), 'utf8');
|
|
1424
402
|
|
|
1425
|
-
|
|
1426
|
-
displaySuccess(`✅ API Key 已保存到 "${relayName}"`);
|
|
403
|
+
console.log(chalk.green('\n✅ API Key 已保存'));
|
|
1427
404
|
}
|
|
1428
405
|
|
|
1429
|
-
//
|
|
1430
|
-
async function
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (relays.length === 0) {
|
|
1434
|
-
displayInfo('请先添加中转站');
|
|
1435
|
-
return;
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
const { relayName } = await inquirer.prompt([
|
|
1439
|
-
{
|
|
1440
|
-
type: 'list',
|
|
1441
|
-
name: 'relayName',
|
|
1442
|
-
message: '选择中转站:',
|
|
1443
|
-
choices: relays.map(r => ({ name: r.name, value: r.name }))
|
|
1444
|
-
}
|
|
1445
|
-
]);
|
|
1446
|
-
|
|
1447
|
-
const { token } = await inquirer.prompt([
|
|
1448
|
-
{
|
|
1449
|
-
type: 'password',
|
|
1450
|
-
name: 'token',
|
|
1451
|
-
message: '输入 Token:',
|
|
1452
|
-
mask: '*',
|
|
1453
|
-
validate: (input) => input.trim() !== '' || 'Token 不能为空'
|
|
1454
|
-
}
|
|
1455
|
-
]);
|
|
1456
|
-
|
|
1457
|
-
await configManager.setToken(relayName, token);
|
|
1458
|
-
displaySuccess(`✅ Token 已保存到 "${relayName}"`);
|
|
1459
|
-
}
|
|
406
|
+
// ============ 查看配置 ============
|
|
407
|
+
async function viewConfig(paths) {
|
|
408
|
+
console.log(chalk.cyan('📋 当前配置\n'));
|
|
1460
409
|
|
|
1461
|
-
|
|
1462
|
-
async function viewApiKeys(configManager) {
|
|
1463
|
-
const keys = await configManager.listApiKeys();
|
|
410
|
+
const config = readConfig(paths.openclawConfig);
|
|
1464
411
|
|
|
1465
|
-
if (
|
|
1466
|
-
|
|
412
|
+
if (!config) {
|
|
413
|
+
console.log(chalk.yellow('配置文件不存在,请先选择节点'));
|
|
1467
414
|
return;
|
|
1468
415
|
}
|
|
1469
416
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
});
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
417
|
+
// 主模型
|
|
418
|
+
const primary = config.agents?.defaults?.model?.primary || '未设置';
|
|
419
|
+
console.log(chalk.yellow('主模型:'));
|
|
420
|
+
console.log(` ${primary}\n`);
|
|
421
|
+
|
|
422
|
+
// 备用模型
|
|
423
|
+
const fallbacks = config.agents?.defaults?.model?.fallbacks || [];
|
|
424
|
+
console.log(chalk.yellow(`备用模型 (${fallbacks.length} 个):`));
|
|
425
|
+
fallbacks.slice(0, 3).forEach((f, i) => console.log(` ${i + 1}. ${f}`));
|
|
426
|
+
if (fallbacks.length > 3) console.log(chalk.gray(` ... 还有 ${fallbacks.length - 3} 个`));
|
|
427
|
+
console.log('');
|
|
428
|
+
|
|
429
|
+
// 节点列表
|
|
430
|
+
console.log(chalk.yellow('节点列表:'));
|
|
431
|
+
if (config.models?.providers) {
|
|
432
|
+
const providers = Object.entries(config.models.providers).filter(([name]) => name.startsWith('yunyi-'));
|
|
433
|
+
providers.slice(0, 5).forEach(([name, provider], i) => {
|
|
434
|
+
const isPrimary = primary.startsWith(name);
|
|
435
|
+
const marker = isPrimary ? chalk.green('★') : chalk.gray('○');
|
|
436
|
+
const hasKey = provider.apiKey ? chalk.green('✓') : chalk.red('✗');
|
|
437
|
+
console.log(` ${marker} ${name}: ${provider.baseUrl}`);
|
|
438
|
+
console.log(` API Key: ${hasKey}`);
|
|
439
|
+
});
|
|
440
|
+
if (providers.length > 5) console.log(chalk.gray(` ... 还有 ${providers.length - 5} 个节点`));
|
|
1488
441
|
}
|
|
442
|
+
console.log('');
|
|
1489
443
|
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
message: '选择要删除的 Token:',
|
|
1495
|
-
choices: keysWithToken.map(k => ({ name: k.provider, value: k.provider }))
|
|
1496
|
-
}
|
|
1497
|
-
]);
|
|
1498
|
-
|
|
1499
|
-
const { confirm } = await inquirer.prompt([
|
|
1500
|
-
{
|
|
1501
|
-
type: 'confirm',
|
|
1502
|
-
name: 'confirm',
|
|
1503
|
-
message: `确定要删除 "${provider}" 的 Token 吗?`,
|
|
1504
|
-
default: false
|
|
1505
|
-
}
|
|
1506
|
-
]);
|
|
1507
|
-
|
|
1508
|
-
if (confirm) {
|
|
1509
|
-
await configManager.deleteToken(provider);
|
|
1510
|
-
displaySuccess(`✅ Token 已删除`);
|
|
1511
|
-
}
|
|
444
|
+
// 备份状态
|
|
445
|
+
const backupPath = path.join(paths.configDir, BACKUP_FILENAME);
|
|
446
|
+
console.log(chalk.yellow('备份状态:'));
|
|
447
|
+
console.log(` ${fs.existsSync(backupPath) ? chalk.green('✓ 已备份') : chalk.gray('未备份')}`);
|
|
1512
448
|
}
|
|
1513
449
|
|
|
1514
|
-
//
|
|
1515
|
-
async function
|
|
1516
|
-
const
|
|
1517
|
-
const keysWithApiKey = keys.filter(k => k.key);
|
|
450
|
+
// ============ 恢复默认配置 ============
|
|
451
|
+
async function restore(paths) {
|
|
452
|
+
const backupPath = path.join(paths.configDir, BACKUP_FILENAME);
|
|
1518
453
|
|
|
1519
|
-
if (
|
|
1520
|
-
|
|
454
|
+
if (!fs.existsSync(backupPath)) {
|
|
455
|
+
console.log(chalk.yellow('⚠️ 没有找到备份文件'));
|
|
1521
456
|
return;
|
|
1522
457
|
}
|
|
1523
458
|
|
|
1524
|
-
const {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
}
|
|
1531
|
-
]);
|
|
1532
|
-
|
|
1533
|
-
const { confirm } = await inquirer.prompt([
|
|
1534
|
-
{
|
|
1535
|
-
type: 'confirm',
|
|
1536
|
-
name: 'confirm',
|
|
1537
|
-
message: `确定要删除 "${provider}" 的 API Key 吗?`,
|
|
1538
|
-
default: false
|
|
1539
|
-
}
|
|
1540
|
-
]);
|
|
459
|
+
const { confirm } = await inquirer.prompt([{
|
|
460
|
+
type: 'confirm',
|
|
461
|
+
name: 'confirm',
|
|
462
|
+
message: '确定要恢复默认配置吗?当前配置将被覆盖。',
|
|
463
|
+
default: false
|
|
464
|
+
}]);
|
|
1541
465
|
|
|
1542
|
-
if (confirm) {
|
|
1543
|
-
|
|
1544
|
-
displaySuccess(`✅ API Key 已删除`);
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
// 测速并切换中转站
|
|
1549
|
-
async function speedTestAndSwitch(configManager) {
|
|
1550
|
-
const relays = await configManager.listRelays();
|
|
1551
|
-
|
|
1552
|
-
if (relays.length === 0) {
|
|
1553
|
-
displayInfo('没有可测速的中转站');
|
|
466
|
+
if (!confirm) {
|
|
467
|
+
console.log(chalk.gray('已取消'));
|
|
1554
468
|
return;
|
|
1555
469
|
}
|
|
1556
470
|
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
// 测试所有中转站
|
|
1560
|
-
const results = await testMultipleRelays(relays);
|
|
1561
|
-
|
|
1562
|
-
// 显示测速结果
|
|
1563
|
-
console.log(chalk.yellow('测速结果:\n'));
|
|
1564
|
-
results.forEach((result, index) => {
|
|
1565
|
-
const status = result.success ? chalk.green('✓') : chalk.red('✗');
|
|
1566
|
-
const latency = result.success
|
|
1567
|
-
? `${result.latency}ms (${formatLatency(result.latency)})`
|
|
1568
|
-
: chalk.red(result.error);
|
|
1569
|
-
|
|
1570
|
-
console.log(` ${status} ${index + 1}. ${result.name}`);
|
|
1571
|
-
console.log(` URL: ${result.url}`);
|
|
1572
|
-
console.log(` 延迟: ${latency}\n`);
|
|
1573
|
-
});
|
|
1574
|
-
|
|
1575
|
-
// 按速度排序
|
|
1576
|
-
const sorted = sortBySpeed(results);
|
|
1577
|
-
|
|
1578
|
-
if (sorted.length === 0) {
|
|
1579
|
-
displayError('所有中转站都无法访问');
|
|
1580
|
-
return;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
// 显示推荐
|
|
1584
|
-
const fastest = sorted[0];
|
|
1585
|
-
console.log(chalk.green(`\n🏆 最快的中转站: ${fastest.name} (${fastest.latency}ms)\n`));
|
|
1586
|
-
|
|
1587
|
-
// 询问是否切换
|
|
1588
|
-
const { action } = await inquirer.prompt([
|
|
1589
|
-
{
|
|
1590
|
-
type: 'list',
|
|
1591
|
-
name: 'action',
|
|
1592
|
-
message: '选择操作:',
|
|
1593
|
-
choices: [
|
|
1594
|
-
{ name: `🚀 切换到最快的中转站 (${fastest.name})`, value: 'fastest' },
|
|
1595
|
-
{ name: '📋 手动选择中转站', value: 'manual' },
|
|
1596
|
-
{ name: '⬅️ 返回', value: 'back' }
|
|
1597
|
-
]
|
|
1598
|
-
}
|
|
1599
|
-
]);
|
|
1600
|
-
|
|
1601
|
-
if (action === 'back') return;
|
|
1602
|
-
|
|
1603
|
-
if (action === 'fastest') {
|
|
1604
|
-
// 切换到最快的
|
|
1605
|
-
const fastestRelay = relays.find(rel => rel.name === fastest.name);
|
|
1606
|
-
let fastestModelId = fastestRelay?.modelId;
|
|
1607
|
-
if (!fastestModelId) {
|
|
1608
|
-
const result = await inquirer.prompt([
|
|
1609
|
-
{
|
|
1610
|
-
type: 'input',
|
|
1611
|
-
name: 'modelId',
|
|
1612
|
-
message: `未找到 "${fastest.name}" 的模型 ID,请输入:`,
|
|
1613
|
-
validate: (input) => input.trim() !== '' || '模型 ID 不能为空'
|
|
1614
|
-
}
|
|
1615
|
-
]);
|
|
1616
|
-
fastestModelId = result.modelId;
|
|
1617
|
-
}
|
|
1618
|
-
await configManager.setPrimaryModel(fastest.name, fastestModelId);
|
|
1619
|
-
|
|
1620
|
-
// 设置其他可用的为备用
|
|
1621
|
-
const fallbacks = sorted.slice(1)
|
|
1622
|
-
.map(r => relays.find(rel => rel.name === r.name))
|
|
1623
|
-
.filter(relay => relay && relay.modelId)
|
|
1624
|
-
.map(relay => `${relay.name}/${relay.modelId}`);
|
|
1625
|
-
await configManager.setFallbackModels(fallbacks);
|
|
1626
|
-
|
|
1627
|
-
displaySuccess(`✅ 已切换到 "${fastest.name}" (${fastest.latency}ms)`);
|
|
1628
|
-
if (fallbacks.length > 0) {
|
|
1629
|
-
console.log(chalk.blue(`备用中转站: ${fallbacks.map(f => f.split('/')[0]).join(', ')}`));
|
|
1630
|
-
}
|
|
1631
|
-
} else if (action === 'manual') {
|
|
1632
|
-
// 手动选择
|
|
1633
|
-
const { selected } = await inquirer.prompt([
|
|
1634
|
-
{
|
|
1635
|
-
type: 'list',
|
|
1636
|
-
name: 'selected',
|
|
1637
|
-
message: '选择要切换的中转站:',
|
|
1638
|
-
choices: sorted.map(r => ({
|
|
1639
|
-
name: `${r.name} - ${r.latency}ms`,
|
|
1640
|
-
value: r.name
|
|
1641
|
-
}))
|
|
1642
|
-
}
|
|
1643
|
-
]);
|
|
1644
|
-
|
|
1645
|
-
const selectedRelay = relays.find(rel => rel.name === selected);
|
|
1646
|
-
let selectedModelId = selectedRelay?.modelId;
|
|
1647
|
-
if (!selectedModelId) {
|
|
1648
|
-
const result = await inquirer.prompt([
|
|
1649
|
-
{
|
|
1650
|
-
type: 'input',
|
|
1651
|
-
name: 'modelId',
|
|
1652
|
-
message: `未找到 "${selected}" 的模型 ID,请输入:`,
|
|
1653
|
-
validate: (input) => input.trim() !== '' || '模型 ID 不能为空'
|
|
1654
|
-
}
|
|
1655
|
-
]);
|
|
1656
|
-
selectedModelId = result.modelId;
|
|
1657
|
-
}
|
|
1658
|
-
await configManager.setPrimaryModel(selected, selectedModelId);
|
|
1659
|
-
|
|
1660
|
-
// 设置其他为备用
|
|
1661
|
-
const fallbacks = sorted
|
|
1662
|
-
.filter(r => r.name !== selected)
|
|
1663
|
-
.map(r => relays.find(rel => rel.name === r.name))
|
|
1664
|
-
.filter(relay => relay && relay.modelId)
|
|
1665
|
-
.map(relay => `${relay.name}/${relay.modelId}`);
|
|
1666
|
-
await configManager.setFallbackModels(fallbacks);
|
|
1667
|
-
|
|
1668
|
-
const selectedResult = results.find(r => r.name === selected);
|
|
1669
|
-
displaySuccess(`✅ 已切换到 "${selected}" (${selectedResult.latency}ms)`);
|
|
1670
|
-
if (fallbacks.length > 0) {
|
|
1671
|
-
console.log(chalk.blue(`备用中转站: ${fallbacks.map(f => f.split('/')[0]).join(', ')}`));
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
// 高级设置
|
|
1677
|
-
async function manageAdvanced(configManager) {
|
|
1678
|
-
const currentConfig = await configManager.getAdvancedSettings();
|
|
1679
|
-
const defaultWorkspace = getDefaultWorkspace();
|
|
1680
|
-
|
|
1681
|
-
console.log(chalk.cyan('\n当前高级设置:'));
|
|
1682
|
-
console.log(` 最大并发: ${currentConfig.maxConcurrent}`);
|
|
1683
|
-
console.log(` 子代理并发: ${currentConfig.subagentMaxConcurrent}`);
|
|
1684
|
-
console.log(` 工作区: ${currentConfig.workspace || chalk.gray('(使用默认)')}`);
|
|
1685
|
-
console.log(` 压缩模式: ${currentConfig.compactionMode || chalk.gray('(未设置)')}`);
|
|
1686
|
-
if (currentConfig.gatewayToken) {
|
|
1687
|
-
const token = currentConfig.gatewayToken;
|
|
1688
|
-
const masked = `${token.substring(0, 6)}...${token.substring(token.length - 4)}`;
|
|
1689
|
-
console.log(` 网关 Token: ${chalk.gray(masked)}`);
|
|
471
|
+
if (restoreDefaultConfig(paths.openclawConfig, paths.configDir)) {
|
|
472
|
+
console.log(chalk.green('\n✅ 已恢复默认配置'));
|
|
1690
473
|
} else {
|
|
1691
|
-
console.log(
|
|
1692
|
-
}
|
|
1693
|
-
console.log(` 默认工作区: ${chalk.gray(defaultWorkspace)}\n`);
|
|
1694
|
-
|
|
1695
|
-
const answers = await inquirer.prompt([
|
|
1696
|
-
{
|
|
1697
|
-
type: 'number',
|
|
1698
|
-
name: 'maxConcurrent',
|
|
1699
|
-
message: '最大并发任务数 (1-100):',
|
|
1700
|
-
default: currentConfig.maxConcurrent || 4,
|
|
1701
|
-
validate: (input) => {
|
|
1702
|
-
const num = Number(input);
|
|
1703
|
-
if (isNaN(num) || num < 1 || num > 100) {
|
|
1704
|
-
return '请输入 1 到 100 之间的数字';
|
|
1705
|
-
}
|
|
1706
|
-
return true;
|
|
1707
|
-
}
|
|
1708
|
-
},
|
|
1709
|
-
{
|
|
1710
|
-
type: 'number',
|
|
1711
|
-
name: 'subagentMaxConcurrent',
|
|
1712
|
-
message: '子代理最大并发数 (1-100):',
|
|
1713
|
-
default: currentConfig.subagentMaxConcurrent || 8,
|
|
1714
|
-
validate: (input) => {
|
|
1715
|
-
const num = Number(input);
|
|
1716
|
-
if (isNaN(num) || num < 1 || num > 100) {
|
|
1717
|
-
return '请输入 1 到 100 之间的数字';
|
|
1718
|
-
}
|
|
1719
|
-
return true;
|
|
1720
|
-
}
|
|
1721
|
-
},
|
|
1722
|
-
{
|
|
1723
|
-
type: 'input',
|
|
1724
|
-
name: 'workspace',
|
|
1725
|
-
message: `工作区路径 (留空使用默认: ${defaultWorkspace}):`,
|
|
1726
|
-
default: currentConfig.workspace || ''
|
|
1727
|
-
},
|
|
1728
|
-
{
|
|
1729
|
-
type: 'list',
|
|
1730
|
-
name: 'compactionMode',
|
|
1731
|
-
message: '压缩模式:',
|
|
1732
|
-
choices: [
|
|
1733
|
-
{ name: '保持不变', value: '__keep__' },
|
|
1734
|
-
{ name: 'safeguard', value: 'safeguard' },
|
|
1735
|
-
{ name: 'auto', value: 'auto' },
|
|
1736
|
-
{ name: 'off', value: 'off' }
|
|
1737
|
-
],
|
|
1738
|
-
default: currentConfig.compactionMode ? currentConfig.compactionMode : '__keep__'
|
|
1739
|
-
},
|
|
1740
|
-
{
|
|
1741
|
-
type: 'password',
|
|
1742
|
-
name: 'gatewayToken',
|
|
1743
|
-
message: '网关 Token (留空保持不变):',
|
|
1744
|
-
mask: '*'
|
|
1745
|
-
}
|
|
1746
|
-
]);
|
|
1747
|
-
|
|
1748
|
-
// 如果用户没有输入工作区路径,使用默认值
|
|
1749
|
-
if (!answers.workspace || answers.workspace.trim() === '') {
|
|
1750
|
-
answers.workspace = defaultWorkspace;
|
|
474
|
+
console.log(chalk.red('\n❌ 恢复失败'));
|
|
1751
475
|
}
|
|
1752
|
-
|
|
1753
|
-
const advancedUpdate = {
|
|
1754
|
-
maxConcurrent: answers.maxConcurrent,
|
|
1755
|
-
subagentMaxConcurrent: answers.subagentMaxConcurrent,
|
|
1756
|
-
workspace: answers.workspace,
|
|
1757
|
-
gatewayToken: answers.gatewayToken
|
|
1758
|
-
};
|
|
1759
|
-
if (answers.compactionMode !== '__keep__') {
|
|
1760
|
-
advancedUpdate.compactionMode = answers.compactionMode;
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
await configManager.setAdvancedSettings(advancedUpdate);
|
|
1764
|
-
displaySuccess('✅ 高级设置已更新');
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// 查看当前配置
|
|
1768
|
-
async function viewConfig(configManager) {
|
|
1769
|
-
const config = await configManager.getCurrentConfig();
|
|
1770
|
-
|
|
1771
|
-
console.log(chalk.cyan('\n📋 当前配置:\n'));
|
|
1772
|
-
console.log(chalk.yellow('主模型:'));
|
|
1773
|
-
console.log(` ${config.primary}\n`);
|
|
1774
|
-
|
|
1775
|
-
console.log(chalk.yellow('备用模型:'));
|
|
1776
|
-
config.fallbacks.forEach((f, i) => {
|
|
1777
|
-
console.log(` ${i + 1}. ${f}`);
|
|
1778
|
-
});
|
|
1779
|
-
|
|
1780
|
-
console.log(chalk.yellow('\n中转站:'));
|
|
1781
|
-
config.relays.forEach(r => {
|
|
1782
|
-
console.log(` ${chalk.green('●')} ${r.name}`);
|
|
1783
|
-
console.log(` URL: ${r.baseUrl}`);
|
|
1784
|
-
console.log(` 模型: ${r.modelName} (${r.modelId})`);
|
|
1785
|
-
const contextInfo = r.contextWindow ? `${r.contextWindow} tokens` : '-';
|
|
1786
|
-
console.log(` 上下文: ${contextInfo}`);
|
|
1787
|
-
});
|
|
1788
|
-
|
|
1789
|
-
console.log(chalk.yellow('\n高级设置:'));
|
|
1790
|
-
console.log(` 最大并发: ${config.advanced.maxConcurrent}`);
|
|
1791
|
-
console.log(` 子代理并发: ${config.advanced.subagentMaxConcurrent}`);
|
|
1792
|
-
console.log(` 工作区: ${config.advanced.workspace}`);
|
|
1793
476
|
}
|
|
1794
477
|
|
|
1795
|
-
//
|
|
478
|
+
// 启动
|
|
1796
479
|
main().catch(error => {
|
|
1797
|
-
|
|
480
|
+
console.error(chalk.red(`程序错误: ${error.message}`));
|
|
1798
481
|
process.exit(1);
|
|
1799
482
|
});
|