yymaxapi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/bin/yymaxapi.js +2728 -0
- package/config/API/350/212/202/347/202/271/350/256/276/347/275/256.md +92 -0
- package/lib/config-manager.js +707 -0
- package/lib/speed-test.js +88 -0
- package/lib/ui.js +38 -0
- package/package.json +43 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const JSON5 = require('json5');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
// 默认配置模板
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
models: {
|
|
9
|
+
providers: {}
|
|
10
|
+
},
|
|
11
|
+
auth: {
|
|
12
|
+
profiles: {}
|
|
13
|
+
},
|
|
14
|
+
agents: {
|
|
15
|
+
defaults: {
|
|
16
|
+
model: {
|
|
17
|
+
primary: '',
|
|
18
|
+
fallbacks: []
|
|
19
|
+
},
|
|
20
|
+
models: {},
|
|
21
|
+
maxConcurrent: 4,
|
|
22
|
+
subagents: {
|
|
23
|
+
maxConcurrent: 8
|
|
24
|
+
},
|
|
25
|
+
workspace: ''
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// 获取跨平台配置目录
|
|
31
|
+
function getConfigDir() {
|
|
32
|
+
const homeDir = os.homedir();
|
|
33
|
+
// Windows: %USERPROFILE%\.clawdbot
|
|
34
|
+
// macOS/Linux: ~/.clawdbot
|
|
35
|
+
return path.join(homeDir, '.clawdbot');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 获取跨平台默认工作区路径
|
|
39
|
+
function getDefaultWorkspace() {
|
|
40
|
+
const homeDir = os.homedir();
|
|
41
|
+
const openclawStateDir = process.env.OPENCLAW_STATE_DIR || path.join(homeDir, '.openclaw');
|
|
42
|
+
const clawdbotStateDir = process.env.CLAWDBOT_STATE_DIR || path.join(homeDir, '.clawdbot');
|
|
43
|
+
const profile = process.env.OPENCLAW_PROFILE;
|
|
44
|
+
const workspaceSuffix = profile && profile !== 'default' ? `-${profile}` : '';
|
|
45
|
+
|
|
46
|
+
if (process.env.OPENCLAW_STATE_DIR || process.env.OPENCLAW_CONFIG_PATH) {
|
|
47
|
+
return path.join(openclawStateDir, `workspace${workspaceSuffix}`);
|
|
48
|
+
}
|
|
49
|
+
if (process.env.CLAWDBOT_STATE_DIR || process.env.CLAWDBOT_CONFIG_PATH) {
|
|
50
|
+
return path.join(clawdbotStateDir, 'workspace');
|
|
51
|
+
}
|
|
52
|
+
return path.join(openclawStateDir, `workspace${workspaceSuffix}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 验证 URL 格式
|
|
56
|
+
function isValidUrl(urlString) {
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(urlString);
|
|
59
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 验证数值范围
|
|
66
|
+
function isValidNumber(value, min, max) {
|
|
67
|
+
const num = Number(value);
|
|
68
|
+
return !isNaN(num) && num >= min && num <= max;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class ConfigManager {
|
|
72
|
+
constructor(configPaths) {
|
|
73
|
+
this.openclawConfigPath = configPaths.openclawConfig;
|
|
74
|
+
this.authProfilesPath = configPaths.authProfiles;
|
|
75
|
+
this.configDir = path.dirname(configPaths.openclawConfig);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 检查配置文件是否存在
|
|
79
|
+
async checkConfigExists() {
|
|
80
|
+
return {
|
|
81
|
+
openclaw: await fs.pathExists(this.openclawConfigPath),
|
|
82
|
+
auth: await fs.pathExists(this.authProfilesPath)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 初始化配置文件(如果不存在则创建)
|
|
87
|
+
async initializeConfig() {
|
|
88
|
+
try {
|
|
89
|
+
// 确保配置目录存在
|
|
90
|
+
await fs.ensureDir(this.configDir);
|
|
91
|
+
|
|
92
|
+
// 检查并创建 openclaw.json
|
|
93
|
+
if (!await fs.pathExists(this.openclawConfigPath)) {
|
|
94
|
+
const defaultConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
95
|
+
defaultConfig.agents.defaults.workspace = getDefaultWorkspace();
|
|
96
|
+
await fs.writeJson(this.openclawConfigPath, defaultConfig, { spaces: 2 });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 确保 auth-profiles.json 目录存在
|
|
100
|
+
const authDir = path.dirname(this.authProfilesPath);
|
|
101
|
+
await fs.ensureDir(authDir);
|
|
102
|
+
|
|
103
|
+
if (!await fs.pathExists(this.authProfilesPath)) {
|
|
104
|
+
await fs.writeJson(this.authProfilesPath, {}, { spaces: 2 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new Error(`初始化配置失败: ${error.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 检查文件写入权限
|
|
114
|
+
async checkWritePermission(filePath) {
|
|
115
|
+
try {
|
|
116
|
+
const dir = path.dirname(filePath);
|
|
117
|
+
await fs.ensureDir(dir);
|
|
118
|
+
// 尝试写入测试文件
|
|
119
|
+
const testFile = path.join(dir, '.write-test-' + Date.now());
|
|
120
|
+
await fs.writeFile(testFile, '');
|
|
121
|
+
await fs.remove(testFile);
|
|
122
|
+
return true;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 读取 openclaw.json
|
|
129
|
+
async readOpenclawConfig() {
|
|
130
|
+
try {
|
|
131
|
+
if (!await fs.pathExists(this.openclawConfigPath)) {
|
|
132
|
+
// 自动初始化配置
|
|
133
|
+
await this.initializeConfig();
|
|
134
|
+
}
|
|
135
|
+
const raw = await fs.readFile(this.openclawConfigPath, 'utf8');
|
|
136
|
+
return JSON5.parse(raw);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error.code === 'EACCES') {
|
|
139
|
+
throw new Error(`没有权限读取配置文件: ${this.openclawConfigPath}`);
|
|
140
|
+
}
|
|
141
|
+
if (error.code === 'ENOENT') {
|
|
142
|
+
throw new Error(`配置文件不存在: ${this.openclawConfigPath}`);
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`读取配置文件失败: ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 写入 openclaw.json
|
|
149
|
+
async writeOpenclawConfig(config) {
|
|
150
|
+
try {
|
|
151
|
+
// 检查写入权限
|
|
152
|
+
if (!await this.checkWritePermission(this.openclawConfigPath)) {
|
|
153
|
+
throw new Error(`没有权限写入配置文件: ${this.openclawConfigPath}`);
|
|
154
|
+
}
|
|
155
|
+
await fs.ensureDir(path.dirname(this.openclawConfigPath));
|
|
156
|
+
await fs.writeJson(this.openclawConfigPath, config, { spaces: 2 });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error.code === 'EACCES') {
|
|
159
|
+
throw new Error(`没有权限写入配置文件: ${this.openclawConfigPath}`);
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`写入配置文件失败: ${error.message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 读取 auth-profiles.json
|
|
166
|
+
async readAuthProfiles() {
|
|
167
|
+
try {
|
|
168
|
+
if (await fs.pathExists(this.authProfilesPath)) {
|
|
169
|
+
return await fs.readJson(this.authProfilesPath);
|
|
170
|
+
}
|
|
171
|
+
return {};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error.code === 'EACCES') {
|
|
174
|
+
throw new Error(`没有权限读取认证文件: ${this.authProfilesPath}`);
|
|
175
|
+
}
|
|
176
|
+
throw new Error(`读取认证文件失败: ${error.message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 写入 auth-profiles.json
|
|
181
|
+
async writeAuthProfiles(profiles) {
|
|
182
|
+
try {
|
|
183
|
+
const authDir = path.dirname(this.authProfilesPath);
|
|
184
|
+
if (!await this.checkWritePermission(this.authProfilesPath)) {
|
|
185
|
+
throw new Error(`没有权限写入认证文件: ${this.authProfilesPath}`);
|
|
186
|
+
}
|
|
187
|
+
await fs.ensureDir(authDir);
|
|
188
|
+
await fs.writeJson(this.authProfilesPath, profiles, { spaces: 2 });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (error.code === 'EACCES') {
|
|
191
|
+
throw new Error(`没有权限写入认证文件: ${this.authProfilesPath}`);
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`写入认证文件失败: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 添加中转站
|
|
198
|
+
async addRelay(relayConfig) {
|
|
199
|
+
// 验证输入
|
|
200
|
+
if (!relayConfig.name || relayConfig.name.trim() === '') {
|
|
201
|
+
throw new Error('中转站名称不能为空');
|
|
202
|
+
}
|
|
203
|
+
if (!relayConfig.baseUrl || !isValidUrl(relayConfig.baseUrl)) {
|
|
204
|
+
throw new Error('请输入有效的 URL (http:// 或 https://)');
|
|
205
|
+
}
|
|
206
|
+
const hasModelsArray = Array.isArray(relayConfig.models);
|
|
207
|
+
const primaryModelId = relayConfig.model?.id || relayConfig.models?.[0]?.id;
|
|
208
|
+
if (!primaryModelId) {
|
|
209
|
+
throw new Error('模型 ID 不能为空');
|
|
210
|
+
}
|
|
211
|
+
if (!hasModelsArray) {
|
|
212
|
+
if (!relayConfig.model || !relayConfig.model.id) {
|
|
213
|
+
throw new Error('模型配置不能为空');
|
|
214
|
+
}
|
|
215
|
+
if (!isValidNumber(relayConfig.model.contextWindow, 1000, 10000000)) {
|
|
216
|
+
throw new Error('上下文窗口大小必须在 1000 到 10000000 之间');
|
|
217
|
+
}
|
|
218
|
+
if (!isValidNumber(relayConfig.model.maxTokens, 100, 1000000)) {
|
|
219
|
+
throw new Error('最大输出 tokens 必须在 100 到 1000000 之间');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const config = await this.readOpenclawConfig();
|
|
224
|
+
|
|
225
|
+
const providerName = relayConfig.name.trim();
|
|
226
|
+
const profileKey = `${providerName}:default`;
|
|
227
|
+
|
|
228
|
+
// 检查是否已存在同名中转站
|
|
229
|
+
if (config.models?.providers?.[providerName]) {
|
|
230
|
+
throw new Error(`中转站 "${providerName}" 已存在,请使用其他名称或编辑现有中转站`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// 添加到 models.providers
|
|
234
|
+
if (!config.models) config.models = {};
|
|
235
|
+
if (!config.models.providers) config.models.providers = {};
|
|
236
|
+
if (!config.models.mode && relayConfig.modelsMode) {
|
|
237
|
+
config.models.mode = relayConfig.modelsMode;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
config.models.providers[providerName] = {
|
|
241
|
+
baseUrl: relayConfig.baseUrl.trim(),
|
|
242
|
+
auth: relayConfig.auth || 'api-key',
|
|
243
|
+
api: relayConfig.api || 'openai-completions',
|
|
244
|
+
headers: relayConfig.headers || {},
|
|
245
|
+
authHeader: relayConfig.authHeader === true,
|
|
246
|
+
models: Array.isArray(relayConfig.models)
|
|
247
|
+
? relayConfig.models
|
|
248
|
+
: [
|
|
249
|
+
{
|
|
250
|
+
id: relayConfig.model.id.trim(),
|
|
251
|
+
name: relayConfig.model.name || relayConfig.model.id,
|
|
252
|
+
reasoning: !!relayConfig.model.reasoning,
|
|
253
|
+
input: relayConfig.model.input || ['text'],
|
|
254
|
+
cost: relayConfig.model.cost || {
|
|
255
|
+
input: 0,
|
|
256
|
+
output: 0,
|
|
257
|
+
cacheRead: 0,
|
|
258
|
+
cacheWrite: 0
|
|
259
|
+
},
|
|
260
|
+
contextWindow: Number(relayConfig.model.contextWindow),
|
|
261
|
+
maxTokens: Number(relayConfig.model.maxTokens)
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// 添加到 auth.profiles
|
|
267
|
+
if (!config.auth) config.auth = {};
|
|
268
|
+
if (!config.auth.profiles) config.auth.profiles = {};
|
|
269
|
+
|
|
270
|
+
config.auth.profiles[profileKey] = {
|
|
271
|
+
provider: providerName,
|
|
272
|
+
mode: 'api_key'
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// 注册到 agents.defaults.models,便于切换与备用
|
|
276
|
+
if (!config.agents) config.agents = {};
|
|
277
|
+
if (!config.agents.defaults) config.agents.defaults = {};
|
|
278
|
+
if (!config.agents.defaults.models) config.agents.defaults.models = {};
|
|
279
|
+
const modelKey = `${providerName}/${primaryModelId}`;
|
|
280
|
+
if (!config.agents.defaults.models[modelKey]) {
|
|
281
|
+
config.agents.defaults.models[modelKey] = {
|
|
282
|
+
alias: providerName
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await this.writeOpenclawConfig(config);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 列出所有中转站
|
|
290
|
+
async listRelays() {
|
|
291
|
+
const config = await this.readOpenclawConfig();
|
|
292
|
+
const relays = [];
|
|
293
|
+
const primary = config.agents?.defaults?.model?.primary || '';
|
|
294
|
+
const registeredModels = config.agents?.defaults?.models || {};
|
|
295
|
+
|
|
296
|
+
if (config.models && config.models.providers) {
|
|
297
|
+
for (const [name, provider] of Object.entries(config.models.providers)) {
|
|
298
|
+
let modelId = '';
|
|
299
|
+
let modelName = 'N/A';
|
|
300
|
+
let contextWindow = undefined;
|
|
301
|
+
let maxTokens = undefined;
|
|
302
|
+
|
|
303
|
+
if (provider.models && provider.models.length > 0) {
|
|
304
|
+
const model = provider.models[0];
|
|
305
|
+
modelId = model.id;
|
|
306
|
+
modelName = model.name || model.id;
|
|
307
|
+
contextWindow = model.contextWindow;
|
|
308
|
+
maxTokens = model.maxTokens;
|
|
309
|
+
} else if (primary.startsWith(`${name}/`)) {
|
|
310
|
+
modelId = primary.split('/')[1] || '';
|
|
311
|
+
modelName = modelId || 'N/A';
|
|
312
|
+
} else {
|
|
313
|
+
const modelKey = Object.keys(registeredModels).find(key => key.startsWith(`${name}/`));
|
|
314
|
+
if (modelKey) {
|
|
315
|
+
modelId = modelKey.split('/')[1] || '';
|
|
316
|
+
modelName = modelId || 'N/A';
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
relays.push({
|
|
321
|
+
name,
|
|
322
|
+
baseUrl: provider.baseUrl,
|
|
323
|
+
modelId,
|
|
324
|
+
modelName,
|
|
325
|
+
contextWindow,
|
|
326
|
+
maxTokens
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return relays;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 更新中转站
|
|
335
|
+
async updateRelay(relayName, updates) {
|
|
336
|
+
const config = await this.readOpenclawConfig();
|
|
337
|
+
|
|
338
|
+
if (!config.models?.providers?.[relayName]) {
|
|
339
|
+
throw new Error(`中转站 "${relayName}" 不存在`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 验证输入
|
|
343
|
+
if (updates.baseUrl && !isValidUrl(updates.baseUrl)) {
|
|
344
|
+
throw new Error('请输入有效的 URL (http:// 或 https://)');
|
|
345
|
+
}
|
|
346
|
+
if (updates.contextWindow !== undefined && !isValidNumber(updates.contextWindow, 1000, 10000000)) {
|
|
347
|
+
throw new Error('上下文窗口大小必须在 1000 到 10000000 之间');
|
|
348
|
+
}
|
|
349
|
+
if (updates.maxTokens !== undefined && !isValidNumber(updates.maxTokens, 100, 1000000)) {
|
|
350
|
+
throw new Error('最大输出 tokens 必须在 100 到 1000000 之间');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const provider = config.models.providers[relayName];
|
|
354
|
+
|
|
355
|
+
if (updates.baseUrl) {
|
|
356
|
+
provider.baseUrl = updates.baseUrl.trim();
|
|
357
|
+
}
|
|
358
|
+
if (updates.api) {
|
|
359
|
+
provider.api = updates.api;
|
|
360
|
+
}
|
|
361
|
+
if (updates.auth) {
|
|
362
|
+
provider.auth = updates.auth;
|
|
363
|
+
}
|
|
364
|
+
if (updates.headers && typeof updates.headers === 'object') {
|
|
365
|
+
provider.headers = updates.headers;
|
|
366
|
+
}
|
|
367
|
+
if (updates.authHeader !== undefined) {
|
|
368
|
+
provider.authHeader = !!updates.authHeader;
|
|
369
|
+
}
|
|
370
|
+
if (updates.apiKey) {
|
|
371
|
+
provider.apiKey = updates.apiKey.trim();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (provider.models && provider.models.length > 0) {
|
|
375
|
+
if (updates.contextWindow !== undefined) {
|
|
376
|
+
provider.models[0].contextWindow = Number(updates.contextWindow);
|
|
377
|
+
}
|
|
378
|
+
if (updates.maxTokens !== undefined) {
|
|
379
|
+
provider.models[0].maxTokens = Number(updates.maxTokens);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
await this.writeOpenclawConfig(config);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 删除中转站
|
|
387
|
+
async deleteRelay(relayName) {
|
|
388
|
+
const config = await this.readOpenclawConfig();
|
|
389
|
+
|
|
390
|
+
// 删除 provider
|
|
391
|
+
if (config.models?.providers?.[relayName]) {
|
|
392
|
+
delete config.models.providers[relayName];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 删除 auth profile
|
|
396
|
+
const profileKey = `${relayName}:default`;
|
|
397
|
+
if (config.auth?.profiles?.[profileKey]) {
|
|
398
|
+
delete config.auth.profiles[profileKey];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 从 agents.defaults.models 中删除
|
|
402
|
+
if (config.agents?.defaults?.models) {
|
|
403
|
+
for (const key of Object.keys(config.agents.defaults.models)) {
|
|
404
|
+
if (key.startsWith(`${relayName}/`)) {
|
|
405
|
+
delete config.agents.defaults.models[key];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 如果是主模型,需要切换到其他模型
|
|
411
|
+
if (config.agents?.defaults?.model?.primary?.startsWith(`${relayName}/`)) {
|
|
412
|
+
const remainingRelays = await this.listRelays();
|
|
413
|
+
if (remainingRelays.length > 0) {
|
|
414
|
+
const firstRelay = remainingRelays[0];
|
|
415
|
+
config.agents.defaults.model.primary = `${firstRelay.name}/${firstRelay.modelId}`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 从 fallbacks 中删除
|
|
420
|
+
if (config.agents?.defaults?.model?.fallbacks) {
|
|
421
|
+
config.agents.defaults.model.fallbacks = config.agents.defaults.model.fallbacks.filter(
|
|
422
|
+
f => !f.startsWith(`${relayName}/`)
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
await this.writeOpenclawConfig(config);
|
|
427
|
+
|
|
428
|
+
// 删除 API Key
|
|
429
|
+
const authProfiles = await this.readAuthProfiles();
|
|
430
|
+
if (authProfiles[profileKey]) {
|
|
431
|
+
delete authProfiles[profileKey];
|
|
432
|
+
await this.writeAuthProfiles(authProfiles);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 获取主模型
|
|
437
|
+
async getPrimaryModel() {
|
|
438
|
+
const config = await this.readOpenclawConfig();
|
|
439
|
+
const primary = config.agents?.defaults?.model?.primary || '';
|
|
440
|
+
const [provider, modelId] = primary.split('/');
|
|
441
|
+
return { provider, modelId, full: primary };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 设置主模型
|
|
445
|
+
async setPrimaryModel(relayName, modelIdOverride) {
|
|
446
|
+
const config = await this.readOpenclawConfig();
|
|
447
|
+
let modelId = modelIdOverride;
|
|
448
|
+
if (!modelId) {
|
|
449
|
+
const relays = await this.listRelays();
|
|
450
|
+
const relay = relays.find(r => r.name === relayName);
|
|
451
|
+
|
|
452
|
+
if (!relay) {
|
|
453
|
+
throw new Error(`中转站 "${relayName}" 不存在`);
|
|
454
|
+
}
|
|
455
|
+
modelId = relay.modelId;
|
|
456
|
+
}
|
|
457
|
+
if (!modelId) {
|
|
458
|
+
throw new Error(`中转站 "${relayName}" 未配置模型 ID`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!config.agents) config.agents = {};
|
|
462
|
+
if (!config.agents.defaults) config.agents.defaults = {};
|
|
463
|
+
if (!config.agents.defaults.model) config.agents.defaults.model = {};
|
|
464
|
+
|
|
465
|
+
config.agents.defaults.model.primary = `${relayName}/${modelId}`;
|
|
466
|
+
|
|
467
|
+
// 确保在 models 中注册
|
|
468
|
+
if (!config.agents.defaults.models) config.agents.defaults.models = {};
|
|
469
|
+
const modelKey = `${relayName}/${modelId}`;
|
|
470
|
+
if (!config.agents.defaults.models[modelKey]) {
|
|
471
|
+
config.agents.defaults.models[modelKey] = {
|
|
472
|
+
alias: relayName
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
await this.writeOpenclawConfig(config);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 获取备用模型
|
|
480
|
+
async getFallbackModels() {
|
|
481
|
+
const config = await this.readOpenclawConfig();
|
|
482
|
+
return config.agents?.defaults?.model?.fallbacks || [];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 设置备用模型
|
|
486
|
+
async setFallbackModels(models) {
|
|
487
|
+
const config = await this.readOpenclawConfig();
|
|
488
|
+
|
|
489
|
+
if (!config.agents) config.agents = {};
|
|
490
|
+
if (!config.agents.defaults) config.agents.defaults = {};
|
|
491
|
+
if (!config.agents.defaults.model) config.agents.defaults.model = {};
|
|
492
|
+
|
|
493
|
+
config.agents.defaults.model.fallbacks = models;
|
|
494
|
+
|
|
495
|
+
// 确保所有模型都在 models 中注册
|
|
496
|
+
if (!config.agents.defaults.models) config.agents.defaults.models = {};
|
|
497
|
+
for (const modelKey of models) {
|
|
498
|
+
if (!config.agents.defaults.models[modelKey]) {
|
|
499
|
+
const [provider] = modelKey.split('/');
|
|
500
|
+
config.agents.defaults.models[modelKey] = {
|
|
501
|
+
alias: provider
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
await this.writeOpenclawConfig(config);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 设置 API Key
|
|
510
|
+
async setApiKey(relayName, apiKey) {
|
|
511
|
+
if (!apiKey || apiKey.trim() === '') {
|
|
512
|
+
throw new Error('API Key 不能为空');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const config = await this.readOpenclawConfig();
|
|
516
|
+
if (config.models?.providers?.[relayName]) {
|
|
517
|
+
config.models.providers[relayName].apiKey = apiKey.trim();
|
|
518
|
+
await this.writeOpenclawConfig(config);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const authProfiles = await this.readAuthProfiles();
|
|
522
|
+
const profileKey = `${relayName}:default`;
|
|
523
|
+
|
|
524
|
+
// 保留现有的 token 设置
|
|
525
|
+
const existing = authProfiles[profileKey] || {};
|
|
526
|
+
authProfiles[profileKey] = {
|
|
527
|
+
...existing,
|
|
528
|
+
apiKey: apiKey.trim()
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
await this.writeAuthProfiles(authProfiles);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 设置 Token
|
|
535
|
+
async setToken(relayName, token) {
|
|
536
|
+
if (!token || token.trim() === '') {
|
|
537
|
+
throw new Error('Token 不能为空');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const authProfiles = await this.readAuthProfiles();
|
|
541
|
+
const profileKey = `${relayName}:default`;
|
|
542
|
+
|
|
543
|
+
// 保留现有的 apiKey 设置
|
|
544
|
+
const existing = authProfiles[profileKey] || {};
|
|
545
|
+
authProfiles[profileKey] = {
|
|
546
|
+
...existing,
|
|
547
|
+
token: token.trim()
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
await this.writeAuthProfiles(authProfiles);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 获取 Token
|
|
554
|
+
async getToken(relayName) {
|
|
555
|
+
const authProfiles = await this.readAuthProfiles();
|
|
556
|
+
const profileKey = `${relayName}:default`;
|
|
557
|
+
return authProfiles[profileKey]?.token || null;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 列出所有 API Keys 和 Tokens
|
|
561
|
+
async listApiKeys() {
|
|
562
|
+
const config = await this.readOpenclawConfig();
|
|
563
|
+
const authProfiles = await this.readAuthProfiles();
|
|
564
|
+
const keys = [];
|
|
565
|
+
const providers = new Set();
|
|
566
|
+
|
|
567
|
+
if (config.models?.providers) {
|
|
568
|
+
Object.keys(config.models.providers).forEach(p => providers.add(p));
|
|
569
|
+
}
|
|
570
|
+
Object.keys(authProfiles).forEach(profile => {
|
|
571
|
+
const provider = profile.split(':')[0];
|
|
572
|
+
providers.add(provider);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
for (const provider of providers) {
|
|
576
|
+
const profileKey = `${provider}:default`;
|
|
577
|
+
const providerConfig = config.models?.providers?.[provider] || {};
|
|
578
|
+
const profileData = authProfiles[profileKey] || {};
|
|
579
|
+
keys.push({
|
|
580
|
+
provider,
|
|
581
|
+
key: providerConfig.apiKey || profileData.apiKey || null,
|
|
582
|
+
token: profileData.token || null
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return keys;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// 删除 Token
|
|
590
|
+
async deleteToken(provider) {
|
|
591
|
+
const authProfiles = await this.readAuthProfiles();
|
|
592
|
+
const profileKey = provider.includes(':') ? provider : `${provider}:default`;
|
|
593
|
+
|
|
594
|
+
if (authProfiles[profileKey] && authProfiles[profileKey].token) {
|
|
595
|
+
delete authProfiles[profileKey].token;
|
|
596
|
+
// 如果没有其他数据,删除整个 profile
|
|
597
|
+
if (!authProfiles[profileKey].apiKey) {
|
|
598
|
+
delete authProfiles[profileKey];
|
|
599
|
+
}
|
|
600
|
+
await this.writeAuthProfiles(authProfiles);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 删除 API Key
|
|
605
|
+
async deleteApiKey(provider) {
|
|
606
|
+
const config = await this.readOpenclawConfig();
|
|
607
|
+
if (config.models?.providers?.[provider]) {
|
|
608
|
+
delete config.models.providers[provider].apiKey;
|
|
609
|
+
await this.writeOpenclawConfig(config);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const authProfiles = await this.readAuthProfiles();
|
|
613
|
+
const profileKey = provider.includes(':') ? provider : `${provider}:default`;
|
|
614
|
+
|
|
615
|
+
if (authProfiles[profileKey]) {
|
|
616
|
+
delete authProfiles[profileKey].apiKey;
|
|
617
|
+
// 如果没有其他数据,删除整个 profile
|
|
618
|
+
if (!authProfiles[profileKey].token) {
|
|
619
|
+
delete authProfiles[profileKey];
|
|
620
|
+
}
|
|
621
|
+
await this.writeAuthProfiles(authProfiles);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// 获取高级设置
|
|
626
|
+
async getAdvancedSettings() {
|
|
627
|
+
const config = await this.readOpenclawConfig();
|
|
628
|
+
return {
|
|
629
|
+
maxConcurrent: config.agents?.defaults?.maxConcurrent || 4,
|
|
630
|
+
subagentMaxConcurrent: config.agents?.defaults?.subagents?.maxConcurrent || 8,
|
|
631
|
+
workspace: config.agents?.defaults?.workspace || getDefaultWorkspace(),
|
|
632
|
+
compactionMode: config.agents?.defaults?.compaction?.mode || '',
|
|
633
|
+
gatewayAuthMode: config.gateway?.auth?.mode || '',
|
|
634
|
+
gatewayToken: config.gateway?.auth?.token || '',
|
|
635
|
+
gatewayPort: config.gateway?.port,
|
|
636
|
+
gatewayBind: config.gateway?.bind
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 设置高级设置
|
|
641
|
+
async setAdvancedSettings(settings) {
|
|
642
|
+
const config = await this.readOpenclawConfig();
|
|
643
|
+
|
|
644
|
+
// 验证输入
|
|
645
|
+
if (settings.maxConcurrent !== undefined && !isValidNumber(settings.maxConcurrent, 1, 100)) {
|
|
646
|
+
throw new Error('最大并发任务数必须在 1 到 100 之间');
|
|
647
|
+
}
|
|
648
|
+
if (settings.subagentMaxConcurrent !== undefined && !isValidNumber(settings.subagentMaxConcurrent, 1, 100)) {
|
|
649
|
+
throw new Error('子代理最大并发数必须在 1 到 100 之间');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (!config.agents) config.agents = {};
|
|
653
|
+
if (!config.agents.defaults) config.agents.defaults = {};
|
|
654
|
+
|
|
655
|
+
if (settings.maxConcurrent !== undefined) {
|
|
656
|
+
config.agents.defaults.maxConcurrent = Number(settings.maxConcurrent);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (settings.subagentMaxConcurrent !== undefined) {
|
|
660
|
+
if (!config.agents.defaults.subagents) config.agents.defaults.subagents = {};
|
|
661
|
+
config.agents.defaults.subagents.maxConcurrent = Number(settings.subagentMaxConcurrent);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (settings.workspace) {
|
|
665
|
+
// 规范化路径(跨平台)
|
|
666
|
+
config.agents.defaults.workspace = path.normalize(settings.workspace.trim());
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (settings.compactionMode) {
|
|
670
|
+
if (!config.agents.defaults.compaction) config.agents.defaults.compaction = {};
|
|
671
|
+
config.agents.defaults.compaction.mode = settings.compactionMode;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (settings.gatewayToken) {
|
|
675
|
+
if (!config.gateway) config.gateway = {};
|
|
676
|
+
if (!config.gateway.auth) config.gateway.auth = {};
|
|
677
|
+
config.gateway.auth.mode = config.gateway.auth.mode || 'token';
|
|
678
|
+
config.gateway.auth.token = settings.gatewayToken.trim();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
await this.writeOpenclawConfig(config);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// 获取当前完整配置
|
|
685
|
+
async getCurrentConfig() {
|
|
686
|
+
const config = await this.readOpenclawConfig();
|
|
687
|
+
const relays = await this.listRelays();
|
|
688
|
+
const primary = await this.getPrimaryModel();
|
|
689
|
+
const fallbacks = await this.getFallbackModels();
|
|
690
|
+
const advanced = await this.getAdvancedSettings();
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
primary: primary.full,
|
|
694
|
+
fallbacks,
|
|
695
|
+
relays,
|
|
696
|
+
advanced
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
module.exports = {
|
|
702
|
+
ConfigManager,
|
|
703
|
+
getConfigDir,
|
|
704
|
+
getDefaultWorkspace,
|
|
705
|
+
isValidUrl,
|
|
706
|
+
isValidNumber
|
|
707
|
+
};
|