openclawapi 1.0.0 → 1.1.1
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 +120 -26
- package/cli.js +1243 -104
- package/lib/config-manager.js +405 -54
- package/package.json +3 -2
package/lib/config-manager.js
CHANGED
|
@@ -1,10 +1,78 @@
|
|
|
1
1
|
const fs = require('fs-extra');
|
|
2
|
+
const JSON5 = require('json5');
|
|
2
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
|
+
}
|
|
3
70
|
|
|
4
71
|
class ConfigManager {
|
|
5
72
|
constructor(configPaths) {
|
|
6
73
|
this.openclawConfigPath = configPaths.openclawConfig;
|
|
7
74
|
this.authProfilesPath = configPaths.authProfiles;
|
|
75
|
+
this.configDir = path.dirname(configPaths.openclawConfig);
|
|
8
76
|
}
|
|
9
77
|
|
|
10
78
|
// 检查配置文件是否存在
|
|
@@ -15,11 +83,64 @@ class ConfigManager {
|
|
|
15
83
|
};
|
|
16
84
|
}
|
|
17
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
|
+
|
|
18
128
|
// 读取 openclaw.json
|
|
19
129
|
async readOpenclawConfig() {
|
|
20
130
|
try {
|
|
21
|
-
|
|
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);
|
|
22
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
|
+
}
|
|
23
144
|
throw new Error(`读取配置文件失败: ${error.message}`);
|
|
24
145
|
}
|
|
25
146
|
}
|
|
@@ -27,8 +148,16 @@ class ConfigManager {
|
|
|
27
148
|
// 写入 openclaw.json
|
|
28
149
|
async writeOpenclawConfig(config) {
|
|
29
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));
|
|
30
156
|
await fs.writeJson(this.openclawConfigPath, config, { spaces: 2 });
|
|
31
157
|
} catch (error) {
|
|
158
|
+
if (error.code === 'EACCES') {
|
|
159
|
+
throw new Error(`没有权限写入配置文件: ${this.openclawConfigPath}`);
|
|
160
|
+
}
|
|
32
161
|
throw new Error(`写入配置文件失败: ${error.message}`);
|
|
33
162
|
}
|
|
34
163
|
}
|
|
@@ -41,6 +170,9 @@ class ConfigManager {
|
|
|
41
170
|
}
|
|
42
171
|
return {};
|
|
43
172
|
} catch (error) {
|
|
173
|
+
if (error.code === 'EACCES') {
|
|
174
|
+
throw new Error(`没有权限读取认证文件: ${this.authProfilesPath}`);
|
|
175
|
+
}
|
|
44
176
|
throw new Error(`读取认证文件失败: ${error.message}`);
|
|
45
177
|
}
|
|
46
178
|
}
|
|
@@ -48,44 +180,87 @@ class ConfigManager {
|
|
|
48
180
|
// 写入 auth-profiles.json
|
|
49
181
|
async writeAuthProfiles(profiles) {
|
|
50
182
|
try {
|
|
51
|
-
|
|
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);
|
|
52
188
|
await fs.writeJson(this.authProfilesPath, profiles, { spaces: 2 });
|
|
53
189
|
} catch (error) {
|
|
190
|
+
if (error.code === 'EACCES') {
|
|
191
|
+
throw new Error(`没有权限写入认证文件: ${this.authProfilesPath}`);
|
|
192
|
+
}
|
|
54
193
|
throw new Error(`写入认证文件失败: ${error.message}`);
|
|
55
194
|
}
|
|
56
195
|
}
|
|
57
196
|
|
|
58
197
|
// 添加中转站
|
|
59
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
|
+
|
|
60
223
|
const config = await this.readOpenclawConfig();
|
|
61
224
|
|
|
62
|
-
const providerName = relayConfig.name;
|
|
225
|
+
const providerName = relayConfig.name.trim();
|
|
63
226
|
const profileKey = `${providerName}:default`;
|
|
64
227
|
|
|
228
|
+
// 检查是否已存在同名中转站
|
|
229
|
+
if (config.models?.providers?.[providerName]) {
|
|
230
|
+
throw new Error(`中转站 "${providerName}" 已存在,请使用其他名称或编辑现有中转站`);
|
|
231
|
+
}
|
|
232
|
+
|
|
65
233
|
// 添加到 models.providers
|
|
66
234
|
if (!config.models) config.models = {};
|
|
67
235
|
if (!config.models.providers) config.models.providers = {};
|
|
236
|
+
if (!config.models.mode && relayConfig.modelsMode) {
|
|
237
|
+
config.models.mode = relayConfig.modelsMode;
|
|
238
|
+
}
|
|
68
239
|
|
|
69
240
|
config.models.providers[providerName] = {
|
|
70
|
-
baseUrl: relayConfig.baseUrl,
|
|
71
|
-
auth: 'api-key',
|
|
72
|
-
api: 'openai-completions',
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
]
|
|
89
264
|
};
|
|
90
265
|
|
|
91
266
|
// 添加到 auth.profiles
|
|
@@ -97,6 +272,17 @@ class ConfigManager {
|
|
|
97
272
|
mode: 'api_key'
|
|
98
273
|
};
|
|
99
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
|
+
|
|
100
286
|
await this.writeOpenclawConfig(config);
|
|
101
287
|
}
|
|
102
288
|
|
|
@@ -104,20 +290,41 @@ class ConfigManager {
|
|
|
104
290
|
async listRelays() {
|
|
105
291
|
const config = await this.readOpenclawConfig();
|
|
106
292
|
const relays = [];
|
|
293
|
+
const primary = config.agents?.defaults?.model?.primary || '';
|
|
294
|
+
const registeredModels = config.agents?.defaults?.models || {};
|
|
107
295
|
|
|
108
296
|
if (config.models && config.models.providers) {
|
|
109
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
|
+
|
|
110
303
|
if (provider.models && provider.models.length > 0) {
|
|
111
304
|
const model = provider.models[0];
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
}
|
|
120
318
|
}
|
|
319
|
+
|
|
320
|
+
relays.push({
|
|
321
|
+
name,
|
|
322
|
+
baseUrl: provider.baseUrl,
|
|
323
|
+
modelId,
|
|
324
|
+
modelName,
|
|
325
|
+
contextWindow,
|
|
326
|
+
maxTokens
|
|
327
|
+
});
|
|
121
328
|
}
|
|
122
329
|
}
|
|
123
330
|
|
|
@@ -132,18 +339,44 @@ class ConfigManager {
|
|
|
132
339
|
throw new Error(`中转站 "${relayName}" 不存在`);
|
|
133
340
|
}
|
|
134
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
|
+
|
|
135
353
|
const provider = config.models.providers[relayName];
|
|
136
354
|
|
|
137
355
|
if (updates.baseUrl) {
|
|
138
|
-
provider.baseUrl = 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();
|
|
139
372
|
}
|
|
140
373
|
|
|
141
374
|
if (provider.models && provider.models.length > 0) {
|
|
142
|
-
if (updates.contextWindow) {
|
|
143
|
-
provider.models[0].contextWindow = updates.contextWindow;
|
|
375
|
+
if (updates.contextWindow !== undefined) {
|
|
376
|
+
provider.models[0].contextWindow = Number(updates.contextWindow);
|
|
144
377
|
}
|
|
145
|
-
if (updates.maxTokens) {
|
|
146
|
-
provider.models[0].maxTokens = updates.maxTokens;
|
|
378
|
+
if (updates.maxTokens !== undefined) {
|
|
379
|
+
provider.models[0].maxTokens = Number(updates.maxTokens);
|
|
147
380
|
}
|
|
148
381
|
}
|
|
149
382
|
|
|
@@ -209,24 +442,31 @@ class ConfigManager {
|
|
|
209
442
|
}
|
|
210
443
|
|
|
211
444
|
// 设置主模型
|
|
212
|
-
async setPrimaryModel(relayName) {
|
|
445
|
+
async setPrimaryModel(relayName, modelIdOverride) {
|
|
213
446
|
const config = await this.readOpenclawConfig();
|
|
214
|
-
|
|
215
|
-
|
|
447
|
+
let modelId = modelIdOverride;
|
|
448
|
+
if (!modelId) {
|
|
449
|
+
const relays = await this.listRelays();
|
|
450
|
+
const relay = relays.find(r => r.name === relayName);
|
|
216
451
|
|
|
217
|
-
|
|
218
|
-
|
|
452
|
+
if (!relay) {
|
|
453
|
+
throw new Error(`中转站 "${relayName}" 不存在`);
|
|
454
|
+
}
|
|
455
|
+
modelId = relay.modelId;
|
|
456
|
+
}
|
|
457
|
+
if (!modelId) {
|
|
458
|
+
throw new Error(`中转站 "${relayName}" 未配置模型 ID`);
|
|
219
459
|
}
|
|
220
460
|
|
|
221
461
|
if (!config.agents) config.agents = {};
|
|
222
462
|
if (!config.agents.defaults) config.agents.defaults = {};
|
|
223
463
|
if (!config.agents.defaults.model) config.agents.defaults.model = {};
|
|
224
464
|
|
|
225
|
-
config.agents.defaults.model.primary = `${relayName}/${
|
|
465
|
+
config.agents.defaults.model.primary = `${relayName}/${modelId}`;
|
|
226
466
|
|
|
227
467
|
// 确保在 models 中注册
|
|
228
468
|
if (!config.agents.defaults.models) config.agents.defaults.models = {};
|
|
229
|
-
const modelKey = `${relayName}/${
|
|
469
|
+
const modelKey = `${relayName}/${modelId}`;
|
|
230
470
|
if (!config.agents.defaults.models[modelKey]) {
|
|
231
471
|
config.agents.defaults.models[modelKey] = {
|
|
232
472
|
alias: relayName
|
|
@@ -268,37 +508,116 @@ class ConfigManager {
|
|
|
268
508
|
|
|
269
509
|
// 设置 API Key
|
|
270
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
|
+
|
|
271
521
|
const authProfiles = await this.readAuthProfiles();
|
|
272
522
|
const profileKey = `${relayName}:default`;
|
|
273
523
|
|
|
524
|
+
// 保留现有的 token 设置
|
|
525
|
+
const existing = authProfiles[profileKey] || {};
|
|
274
526
|
authProfiles[profileKey] = {
|
|
275
|
-
|
|
527
|
+
...existing,
|
|
528
|
+
apiKey: apiKey.trim()
|
|
276
529
|
};
|
|
277
530
|
|
|
278
531
|
await this.writeAuthProfiles(authProfiles);
|
|
279
532
|
}
|
|
280
533
|
|
|
281
|
-
//
|
|
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
|
|
282
561
|
async listApiKeys() {
|
|
562
|
+
const config = await this.readOpenclawConfig();
|
|
283
563
|
const authProfiles = await this.readAuthProfiles();
|
|
284
564
|
const keys = [];
|
|
565
|
+
const providers = new Set();
|
|
285
566
|
|
|
286
|
-
|
|
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] || {};
|
|
287
579
|
keys.push({
|
|
288
|
-
provider
|
|
289
|
-
key:
|
|
580
|
+
provider,
|
|
581
|
+
key: providerConfig.apiKey || profileData.apiKey || null,
|
|
582
|
+
token: profileData.token || null
|
|
290
583
|
});
|
|
291
584
|
}
|
|
292
585
|
|
|
293
586
|
return keys;
|
|
294
587
|
}
|
|
295
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
|
+
|
|
296
604
|
// 删除 API Key
|
|
297
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
|
+
|
|
298
612
|
const authProfiles = await this.readAuthProfiles();
|
|
613
|
+
const profileKey = provider.includes(':') ? provider : `${provider}:default`;
|
|
299
614
|
|
|
300
|
-
if (authProfiles[
|
|
301
|
-
delete authProfiles[
|
|
615
|
+
if (authProfiles[profileKey]) {
|
|
616
|
+
delete authProfiles[profileKey].apiKey;
|
|
617
|
+
// 如果没有其他数据,删除整个 profile
|
|
618
|
+
if (!authProfiles[profileKey].token) {
|
|
619
|
+
delete authProfiles[profileKey];
|
|
620
|
+
}
|
|
302
621
|
await this.writeAuthProfiles(authProfiles);
|
|
303
622
|
}
|
|
304
623
|
}
|
|
@@ -309,7 +628,12 @@ class ConfigManager {
|
|
|
309
628
|
return {
|
|
310
629
|
maxConcurrent: config.agents?.defaults?.maxConcurrent || 4,
|
|
311
630
|
subagentMaxConcurrent: config.agents?.defaults?.subagents?.maxConcurrent || 8,
|
|
312
|
-
workspace: config.agents?.defaults?.workspace ||
|
|
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
|
|
313
637
|
};
|
|
314
638
|
}
|
|
315
639
|
|
|
@@ -317,20 +641,41 @@ class ConfigManager {
|
|
|
317
641
|
async setAdvancedSettings(settings) {
|
|
318
642
|
const config = await this.readOpenclawConfig();
|
|
319
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
|
+
|
|
320
652
|
if (!config.agents) config.agents = {};
|
|
321
653
|
if (!config.agents.defaults) config.agents.defaults = {};
|
|
322
654
|
|
|
323
655
|
if (settings.maxConcurrent !== undefined) {
|
|
324
|
-
config.agents.defaults.maxConcurrent = settings.maxConcurrent;
|
|
656
|
+
config.agents.defaults.maxConcurrent = Number(settings.maxConcurrent);
|
|
325
657
|
}
|
|
326
658
|
|
|
327
659
|
if (settings.subagentMaxConcurrent !== undefined) {
|
|
328
660
|
if (!config.agents.defaults.subagents) config.agents.defaults.subagents = {};
|
|
329
|
-
config.agents.defaults.subagents.maxConcurrent = settings.subagentMaxConcurrent;
|
|
661
|
+
config.agents.defaults.subagents.maxConcurrent = Number(settings.subagentMaxConcurrent);
|
|
330
662
|
}
|
|
331
663
|
|
|
332
664
|
if (settings.workspace) {
|
|
333
|
-
|
|
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();
|
|
334
679
|
}
|
|
335
680
|
|
|
336
681
|
await this.writeOpenclawConfig(config);
|
|
@@ -353,4 +698,10 @@ class ConfigManager {
|
|
|
353
698
|
}
|
|
354
699
|
}
|
|
355
700
|
|
|
356
|
-
module.exports = {
|
|
701
|
+
module.exports = {
|
|
702
|
+
ConfigManager,
|
|
703
|
+
getConfigDir,
|
|
704
|
+
getDefaultWorkspace,
|
|
705
|
+
isValidUrl,
|
|
706
|
+
isValidNumber
|
|
707
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclawapi",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "跨平台 OpenClaw/Clawdbot 配置管理工具 - 管理中转地址、模型切换、API Keys、测速优化",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"inquirer": "^8.2.5",
|
|
31
31
|
"chalk": "^4.1.2",
|
|
32
|
-
"fs-extra": "^11.1.1"
|
|
32
|
+
"fs-extra": "^11.1.1",
|
|
33
|
+
"json5": "^2.2.3"
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
35
36
|
"node": ">=14.0.0"
|