codexmate 0.0.4 → 0.0.5
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/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
- package/CHANGELOG.md +7 -0
- package/CHANGELOG.zh-CN.md +7 -0
- package/README.md +89 -54
- package/README.zh-CN.md +97 -58
- package/cli.js +1594 -411
- package/package.json +11 -5
- package/tests/e2e/recent-health.e2e.js +135 -0
- package/tests/e2e/run.js +294 -0
- package/web-ui.html +987 -548
package/cli.js
CHANGED
|
@@ -2,34 +2,37 @@
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const toml = require('@iarna/toml');
|
|
7
|
+
const JSON5 = require('json5');
|
|
7
8
|
const { exec, execSync } = require('child_process');
|
|
8
9
|
const http = require('http');
|
|
9
10
|
const https = require('https');
|
|
10
11
|
const readline = require('readline');
|
|
11
12
|
|
|
12
|
-
const
|
|
13
|
+
const DEFAULT_WEB_PORT = 3737;
|
|
13
14
|
|
|
14
15
|
// ============================================================================
|
|
15
16
|
// 配置
|
|
16
17
|
// ============================================================================
|
|
17
|
-
const CONFIG_DIR = path.join(os.homedir(), '.codex');
|
|
18
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml');
|
|
19
|
-
const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
|
|
20
|
-
const MODELS_FILE = path.join(CONFIG_DIR, 'models.json');
|
|
21
|
-
const CURRENT_MODELS_FILE = path.join(CONFIG_DIR, 'provider-current-models.json');
|
|
22
|
-
const INIT_MARK_FILE = path.join(CONFIG_DIR, 'codexmate-init.json');
|
|
23
|
-
const CODEX_SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
|
|
24
|
-
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
25
|
-
const OPENCLAW_CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json');
|
|
26
|
-
const OPENCLAW_WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
|
|
27
|
-
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
28
|
-
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
29
|
-
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
18
|
+
const CONFIG_DIR = path.join(os.homedir(), '.codex');
|
|
19
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml');
|
|
20
|
+
const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
|
|
21
|
+
const MODELS_FILE = path.join(CONFIG_DIR, 'models.json');
|
|
22
|
+
const CURRENT_MODELS_FILE = path.join(CONFIG_DIR, 'provider-current-models.json');
|
|
23
|
+
const INIT_MARK_FILE = path.join(CONFIG_DIR, 'codexmate-init.json');
|
|
24
|
+
const CODEX_SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
|
|
25
|
+
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
26
|
+
const OPENCLAW_CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json');
|
|
27
|
+
const OPENCLAW_WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
|
|
28
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
29
|
+
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
30
|
+
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
31
|
+
const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
|
|
30
32
|
|
|
31
33
|
const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gpt-4'];
|
|
32
34
|
const SPEED_TEST_TIMEOUT_MS = 8000;
|
|
35
|
+
const HEALTH_CHECK_TIMEOUT_MS = 6000;
|
|
33
36
|
const MAX_SESSION_LIST_SIZE = 300;
|
|
34
37
|
const MAX_EXPORT_MESSAGES = 1000;
|
|
35
38
|
const DEFAULT_SESSION_DETAIL_MESSAGES = 300;
|
|
@@ -38,14 +41,19 @@ const SESSION_TITLE_READ_BYTES = 64 * 1024;
|
|
|
38
41
|
const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
|
|
39
42
|
const SESSION_LIST_CACHE_TTL_MS = 4000;
|
|
40
43
|
const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
|
|
41
|
-
const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
42
|
-
const DEFAULT_CONTENT_SCAN_LIMIT = 10;
|
|
44
|
+
const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
45
|
+
const DEFAULT_CONTENT_SCAN_LIMIT = 10;
|
|
43
46
|
const SESSION_SCAN_FACTOR = 4;
|
|
44
47
|
const SESSION_SCAN_MIN_FILES = 800;
|
|
45
|
-
const MAX_SESSION_PATH_LIST_SIZE = 2000;
|
|
46
|
-
const AGENTS_FILE_NAME = 'AGENTS.md';
|
|
47
|
-
const UTF8_BOM = '\ufeff';
|
|
48
|
-
const
|
|
48
|
+
const MAX_SESSION_PATH_LIST_SIZE = 2000;
|
|
49
|
+
const AGENTS_FILE_NAME = 'AGENTS.md';
|
|
50
|
+
const UTF8_BOM = '\ufeff';
|
|
51
|
+
const MODELS_CACHE_TTL_MS = 60 * 1000;
|
|
52
|
+
const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
|
|
53
|
+
const MODELS_CACHE_MAX_ENTRIES = 50;
|
|
54
|
+
const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
55
|
+
const MAX_RECENT_CONFIGS = 3;
|
|
56
|
+
const BOOTSTRAP_TEXT_MARKERS = [
|
|
49
57
|
'agents.md instructions',
|
|
50
58
|
'<instructions>',
|
|
51
59
|
'<environment_context>',
|
|
@@ -53,6 +61,17 @@ const BOOTSTRAP_TEXT_MARKERS = [
|
|
|
53
61
|
'codex cli'
|
|
54
62
|
];
|
|
55
63
|
|
|
64
|
+
const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
|
|
65
|
+
const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
|
|
66
|
+
|
|
67
|
+
function resolveWebPort() {
|
|
68
|
+
const raw = process.env.CODEXMATE_PORT;
|
|
69
|
+
if (!raw) return DEFAULT_WEB_PORT;
|
|
70
|
+
const parsed = parseInt(raw, 10);
|
|
71
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_WEB_PORT;
|
|
72
|
+
return parsed;
|
|
73
|
+
}
|
|
74
|
+
|
|
56
75
|
const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex"
|
|
57
76
|
model_reasoning_effort = "high"
|
|
58
77
|
disable_response_storage = true
|
|
@@ -75,6 +94,8 @@ stream_idle_timeout_ms = 300000
|
|
|
75
94
|
|
|
76
95
|
let g_initNotice = '';
|
|
77
96
|
let g_sessionListCache = new Map();
|
|
97
|
+
let g_modelsCache = new Map();
|
|
98
|
+
let g_modelsInFlight = new Map();
|
|
78
99
|
|
|
79
100
|
// ============================================================================
|
|
80
101
|
// 工具函数
|
|
@@ -154,12 +175,88 @@ function readJsonFile(filePath, fallback = null) {
|
|
|
154
175
|
}
|
|
155
176
|
}
|
|
156
177
|
|
|
178
|
+
function readJsonArrayFile(filePath, fallback = []) {
|
|
179
|
+
if (!fs.existsSync(filePath)) {
|
|
180
|
+
return Array.isArray(fallback) ? [...fallback] : [];
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
184
|
+
if (!content.trim()) {
|
|
185
|
+
return Array.isArray(fallback) ? [...fallback] : [];
|
|
186
|
+
}
|
|
187
|
+
const parsed = JSON.parse(content);
|
|
188
|
+
return Array.isArray(parsed) ? parsed : (Array.isArray(fallback) ? [...fallback] : []);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return Array.isArray(fallback) ? [...fallback] : [];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
157
194
|
function ensureDir(dirPath) {
|
|
158
195
|
if (!fs.existsSync(dirPath)) {
|
|
159
196
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
160
197
|
}
|
|
161
198
|
}
|
|
162
199
|
|
|
200
|
+
function expandHomePath(value) {
|
|
201
|
+
if (typeof value !== 'string') {
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
204
|
+
const trimmed = value.trim();
|
|
205
|
+
if (!trimmed) {
|
|
206
|
+
return '';
|
|
207
|
+
}
|
|
208
|
+
if (trimmed === '~') {
|
|
209
|
+
return os.homedir();
|
|
210
|
+
}
|
|
211
|
+
if (trimmed.startsWith(`~${path.sep}`) || trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
212
|
+
return path.resolve(os.homedir(), trimmed.slice(2));
|
|
213
|
+
}
|
|
214
|
+
return trimmed;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolveExistingDir(candidates = [], fallback = '') {
|
|
218
|
+
for (const raw of candidates) {
|
|
219
|
+
const candidate = expandHomePath(raw);
|
|
220
|
+
if (!candidate) continue;
|
|
221
|
+
try {
|
|
222
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
223
|
+
return candidate;
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {}
|
|
226
|
+
}
|
|
227
|
+
return fallback;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getCodexSessionsDir() {
|
|
231
|
+
const candidates = [];
|
|
232
|
+
const envCodexHome = process.env.CODEX_HOME;
|
|
233
|
+
if (envCodexHome) {
|
|
234
|
+
candidates.push(path.join(envCodexHome, 'sessions'));
|
|
235
|
+
}
|
|
236
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
237
|
+
if (xdgConfig) {
|
|
238
|
+
candidates.push(path.join(xdgConfig, 'codex', 'sessions'));
|
|
239
|
+
}
|
|
240
|
+
candidates.push(path.join(os.homedir(), '.config', 'codex', 'sessions'));
|
|
241
|
+
candidates.push(CODEX_SESSIONS_DIR);
|
|
242
|
+
return resolveExistingDir(candidates, CODEX_SESSIONS_DIR);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getClaudeProjectsDir() {
|
|
246
|
+
const candidates = [];
|
|
247
|
+
const envClaudeHome = process.env.CLAUDE_HOME || process.env.CLAUDE_CONFIG_DIR;
|
|
248
|
+
if (envClaudeHome) {
|
|
249
|
+
candidates.push(path.join(envClaudeHome, 'projects'));
|
|
250
|
+
}
|
|
251
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
252
|
+
if (xdgConfig) {
|
|
253
|
+
candidates.push(path.join(xdgConfig, 'claude', 'projects'));
|
|
254
|
+
}
|
|
255
|
+
candidates.push(path.join(os.homedir(), '.config', 'claude', 'projects'));
|
|
256
|
+
candidates.push(CLAUDE_PROJECTS_DIR);
|
|
257
|
+
return resolveExistingDir(candidates, CLAUDE_PROJECTS_DIR);
|
|
258
|
+
}
|
|
259
|
+
|
|
163
260
|
function hasUtf8Bom(text) {
|
|
164
261
|
return typeof text === 'string' && text.charCodeAt(0) === 0xfeff;
|
|
165
262
|
}
|
|
@@ -183,6 +280,239 @@ function normalizeLineEnding(text, lineEnding) {
|
|
|
183
280
|
return lineEnding === '\r\n' ? normalized.replace(/\n/g, '\r\n') : normalized;
|
|
184
281
|
}
|
|
185
282
|
|
|
283
|
+
function isValidProviderName(name) {
|
|
284
|
+
return typeof name === 'string' && /^[a-zA-Z0-9._-]+$/.test(name.trim());
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildModelsCandidates(baseUrl) {
|
|
288
|
+
const trimmed = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
|
289
|
+
if (!trimmed) return [];
|
|
290
|
+
if (/\/models\/?$/.test(trimmed)) {
|
|
291
|
+
return [trimmed];
|
|
292
|
+
}
|
|
293
|
+
const normalized = trimmed.replace(/\/+$/, '');
|
|
294
|
+
const candidates = [];
|
|
295
|
+
const pushUnique = (url) => {
|
|
296
|
+
if (url && !candidates.includes(url)) {
|
|
297
|
+
candidates.push(url);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
if (/\/v1$/i.test(normalized)) {
|
|
302
|
+
pushUnique(normalized + '/models');
|
|
303
|
+
} else {
|
|
304
|
+
pushUnique(normalized + '/v1/models');
|
|
305
|
+
pushUnique(normalized + '/models');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
pushUnique(trimmed);
|
|
309
|
+
return candidates;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function extractModelNames(payload) {
|
|
313
|
+
if (!payload || typeof payload !== 'object') return [];
|
|
314
|
+
const data = Array.isArray(payload.data)
|
|
315
|
+
? payload.data
|
|
316
|
+
: (Array.isArray(payload.models) ? payload.models : []);
|
|
317
|
+
const names = [];
|
|
318
|
+
for (const item of data) {
|
|
319
|
+
if (typeof item === 'string') {
|
|
320
|
+
if (item.trim()) names.push(item.trim());
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (!item || typeof item !== 'object') continue;
|
|
324
|
+
const name = item.id || item.name || item.model || '';
|
|
325
|
+
if (typeof name === 'string' && name.trim()) {
|
|
326
|
+
names.push(name.trim());
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return Array.from(new Set(names));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function hasModelsListPayload(payload) {
|
|
333
|
+
if (!payload || typeof payload !== 'object') return false;
|
|
334
|
+
return Array.isArray(payload.data) || Array.isArray(payload.models);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function hashModelsCacheValue(value) {
|
|
338
|
+
if (!value) return '';
|
|
339
|
+
try {
|
|
340
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return '';
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function buildModelsCacheKey(baseUrl, apiKey) {
|
|
347
|
+
const normalizedUrl = typeof baseUrl === 'string'
|
|
348
|
+
? baseUrl.trim().replace(/\/+$/, '')
|
|
349
|
+
: '';
|
|
350
|
+
const apiKeyHash = hashModelsCacheValue(apiKey);
|
|
351
|
+
return `${normalizedUrl}|${apiKeyHash}`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function readModelsCacheEntry(cacheKey) {
|
|
355
|
+
if (!cacheKey) return null;
|
|
356
|
+
const entry = g_modelsCache.get(cacheKey);
|
|
357
|
+
if (!entry) return null;
|
|
358
|
+
if (Date.now() >= entry.expiresAt) {
|
|
359
|
+
g_modelsCache.delete(cacheKey);
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
g_modelsCache.delete(cacheKey);
|
|
363
|
+
g_modelsCache.set(cacheKey, entry);
|
|
364
|
+
return entry.result || null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function writeModelsCacheEntry(cacheKey, result) {
|
|
368
|
+
if (!cacheKey) return;
|
|
369
|
+
const isNegative = !!(result && (result.error || result.unlimited));
|
|
370
|
+
const ttl = isNegative ? MODELS_NEGATIVE_CACHE_TTL_MS : MODELS_CACHE_TTL_MS;
|
|
371
|
+
const entry = {
|
|
372
|
+
result,
|
|
373
|
+
expiresAt: Date.now() + ttl
|
|
374
|
+
};
|
|
375
|
+
if (g_modelsCache.has(cacheKey)) {
|
|
376
|
+
g_modelsCache.delete(cacheKey);
|
|
377
|
+
}
|
|
378
|
+
g_modelsCache.set(cacheKey, entry);
|
|
379
|
+
while (g_modelsCache.size > MODELS_CACHE_MAX_ENTRIES) {
|
|
380
|
+
const oldestKey = g_modelsCache.keys().next().value;
|
|
381
|
+
g_modelsCache.delete(oldestKey);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function fetchModelsFromBaseUrl(baseUrl, apiKey) {
|
|
386
|
+
const cacheKey = buildModelsCacheKey(baseUrl, apiKey);
|
|
387
|
+
const cached = readModelsCacheEntry(cacheKey);
|
|
388
|
+
if (cached) return cached;
|
|
389
|
+
|
|
390
|
+
const inFlight = g_modelsInFlight.get(cacheKey);
|
|
391
|
+
if (inFlight) return inFlight;
|
|
392
|
+
|
|
393
|
+
const promise = (async () => {
|
|
394
|
+
const result = await fetchModelsFromBaseUrlCore(baseUrl, apiKey);
|
|
395
|
+
writeModelsCacheEntry(cacheKey, result);
|
|
396
|
+
return result;
|
|
397
|
+
})();
|
|
398
|
+
|
|
399
|
+
g_modelsInFlight.set(cacheKey, promise);
|
|
400
|
+
promise.finally(() => {
|
|
401
|
+
g_modelsInFlight.delete(cacheKey);
|
|
402
|
+
});
|
|
403
|
+
return promise;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function fetchModelsFromBaseUrlCore(baseUrl, apiKey) {
|
|
407
|
+
const candidates = buildModelsCandidates(baseUrl);
|
|
408
|
+
if (candidates.length === 0) return { error: 'Provider missing URL' };
|
|
409
|
+
|
|
410
|
+
let lastError = '';
|
|
411
|
+
for (const modelsUrl of candidates) {
|
|
412
|
+
let parsed;
|
|
413
|
+
try {
|
|
414
|
+
parsed = new URL(modelsUrl);
|
|
415
|
+
} catch (e) {
|
|
416
|
+
lastError = 'Invalid URL';
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
421
|
+
const agent = parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT;
|
|
422
|
+
const headers = {
|
|
423
|
+
'User-Agent': 'codexmate-models',
|
|
424
|
+
'Accept': 'application/json'
|
|
425
|
+
};
|
|
426
|
+
if (apiKey) {
|
|
427
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const result = await new Promise((innerResolve) => {
|
|
431
|
+
let settled = false;
|
|
432
|
+
const finish = (payload) => {
|
|
433
|
+
if (settled) return;
|
|
434
|
+
settled = true;
|
|
435
|
+
innerResolve(payload);
|
|
436
|
+
};
|
|
437
|
+
const req = transport.request(parsed, { method: 'GET', headers, agent }, (res) => {
|
|
438
|
+
const status = res.statusCode || 0;
|
|
439
|
+
const contentType = String(res.headers['content-type'] || '').toLowerCase();
|
|
440
|
+
if (status === 404 || status === 405 || status === 501) {
|
|
441
|
+
res.resume();
|
|
442
|
+
return finish({ unavailable: true });
|
|
443
|
+
}
|
|
444
|
+
let body = '';
|
|
445
|
+
let receivedBytes = 0;
|
|
446
|
+
res.on('data', chunk => {
|
|
447
|
+
receivedBytes += chunk.length || 0;
|
|
448
|
+
if (receivedBytes > MODELS_RESPONSE_MAX_BYTES) {
|
|
449
|
+
res.destroy();
|
|
450
|
+
return finish({ unavailable: true });
|
|
451
|
+
}
|
|
452
|
+
body += chunk;
|
|
453
|
+
});
|
|
454
|
+
res.on('end', () => {
|
|
455
|
+
if (settled) return;
|
|
456
|
+
if (status >= 400) {
|
|
457
|
+
return finish({ error: `Request failed: ${status}` });
|
|
458
|
+
}
|
|
459
|
+
if (contentType && !contentType.includes('application/json')) {
|
|
460
|
+
return finish({ unavailable: true });
|
|
461
|
+
}
|
|
462
|
+
try {
|
|
463
|
+
const payload = JSON.parse(body || '{}');
|
|
464
|
+
if (!hasModelsListPayload(payload)) {
|
|
465
|
+
return finish({ unavailable: true });
|
|
466
|
+
}
|
|
467
|
+
const models = extractModelNames(payload);
|
|
468
|
+
return finish({ models });
|
|
469
|
+
} catch (e) {
|
|
470
|
+
return finish({ unavailable: true });
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
req.setTimeout(SPEED_TEST_TIMEOUT_MS, () => {
|
|
476
|
+
req.destroy(new Error('timeout'));
|
|
477
|
+
});
|
|
478
|
+
req.on('error', (err) => {
|
|
479
|
+
finish({ error: err.message || 'Request failed' });
|
|
480
|
+
});
|
|
481
|
+
req.end();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
if (result && Array.isArray(result.models)) {
|
|
485
|
+
return { models: result.models };
|
|
486
|
+
}
|
|
487
|
+
if (result && result.error) {
|
|
488
|
+
lastError = result.error;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (lastError) {
|
|
494
|
+
return { error: lastError };
|
|
495
|
+
}
|
|
496
|
+
return { unlimited: true };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function fetchProviderModels(providerName, overrides = {}) {
|
|
500
|
+
const { config } = readConfigOrVirtualDefault();
|
|
501
|
+
const targetProvider = providerName || config.model_provider || '';
|
|
502
|
+
if (!targetProvider) return { error: '未设置当前提供商' };
|
|
503
|
+
|
|
504
|
+
const providers = config.model_providers || {};
|
|
505
|
+
const provider = providers[targetProvider];
|
|
506
|
+
if (!provider) return { error: `提供商不存在: ${targetProvider}` };
|
|
507
|
+
|
|
508
|
+
const baseUrl = overrides.baseUrl || provider.base_url || '';
|
|
509
|
+
const apiKey = overrides.apiKey ?? provider.preferred_auth_method ?? '';
|
|
510
|
+
const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
|
|
511
|
+
if (res.unlimited) return { models: [], provider: targetProvider, unlimited: true };
|
|
512
|
+
if (res.error) return { error: res.error };
|
|
513
|
+
return { models: res.models || [], provider: targetProvider, unlimited: false };
|
|
514
|
+
}
|
|
515
|
+
|
|
186
516
|
function resolveAgentsFilePath(params = {}) {
|
|
187
517
|
const baseDir = typeof params.baseDir === 'string' && params.baseDir.trim()
|
|
188
518
|
? params.baseDir.trim()
|
|
@@ -232,256 +562,840 @@ function readAgentsFile(params = {}) {
|
|
|
232
562
|
}
|
|
233
563
|
}
|
|
234
564
|
|
|
235
|
-
function applyAgentsFile(params = {}) {
|
|
236
|
-
const filePath = resolveAgentsFilePath(params);
|
|
237
|
-
const dirCheck = validateAgentsBaseDir(filePath);
|
|
238
|
-
if (dirCheck.error) {
|
|
239
|
-
return { error: dirCheck.error };
|
|
565
|
+
function applyAgentsFile(params = {}) {
|
|
566
|
+
const filePath = resolveAgentsFilePath(params);
|
|
567
|
+
const dirCheck = validateAgentsBaseDir(filePath);
|
|
568
|
+
if (dirCheck.error) {
|
|
569
|
+
return { error: dirCheck.error };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const content = typeof params.content === 'string' ? params.content : '';
|
|
573
|
+
const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
|
|
574
|
+
const normalized = normalizeLineEnding(content, lineEnding);
|
|
575
|
+
const finalContent = ensureUtf8Bom(normalized);
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
fs.writeFileSync(filePath, finalContent, 'utf-8');
|
|
579
|
+
return { success: true, path: filePath };
|
|
580
|
+
} catch (e) {
|
|
581
|
+
return { error: `写入 AGENTS.md 失败: ${e.message}` };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function resolveHomePath(input) {
|
|
586
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
587
|
+
if (!raw) return '';
|
|
588
|
+
if (raw === '~') return os.homedir();
|
|
589
|
+
if (raw.startsWith('~/') || raw.startsWith('~\\')) {
|
|
590
|
+
return path.join(os.homedir(), raw.slice(2));
|
|
591
|
+
}
|
|
592
|
+
return raw;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function resolveOpenclawWorkspaceDir(config) {
|
|
596
|
+
const workspace = config
|
|
597
|
+
&& config.agents
|
|
598
|
+
&& config.agents.defaults
|
|
599
|
+
&& typeof config.agents.defaults.workspace === 'string'
|
|
600
|
+
? config.agents.defaults.workspace
|
|
601
|
+
: '';
|
|
602
|
+
const resolved = resolveHomePath(workspace);
|
|
603
|
+
if (!resolved) {
|
|
604
|
+
return OPENCLAW_WORKSPACE_DIR;
|
|
605
|
+
}
|
|
606
|
+
if (path.isAbsolute(resolved)) {
|
|
607
|
+
return resolved;
|
|
608
|
+
}
|
|
609
|
+
return path.join(OPENCLAW_DIR, resolved);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function readOpenclawConfigFile() {
|
|
613
|
+
const filePath = OPENCLAW_CONFIG_FILE;
|
|
614
|
+
if (!fs.existsSync(filePath)) {
|
|
615
|
+
return {
|
|
616
|
+
exists: false,
|
|
617
|
+
path: filePath,
|
|
618
|
+
content: '',
|
|
619
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n'
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
try {
|
|
624
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
625
|
+
return {
|
|
626
|
+
exists: true,
|
|
627
|
+
path: filePath,
|
|
628
|
+
content: stripUtf8Bom(raw),
|
|
629
|
+
lineEnding: detectLineEnding(raw)
|
|
630
|
+
};
|
|
631
|
+
} catch (e) {
|
|
632
|
+
return { error: `读取 OpenClaw 配置失败: ${e.message}` };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function parseOpenclawConfigText(content) {
|
|
637
|
+
const raw = stripUtf8Bom(typeof content === 'string' ? content : '');
|
|
638
|
+
if (!raw.trim()) {
|
|
639
|
+
return { ok: false, error: 'OpenClaw 配置内容不能为空' };
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
const parsed = JSON5.parse(raw);
|
|
643
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
644
|
+
return { ok: false, error: '配置格式错误(根节点必须是对象)' };
|
|
645
|
+
}
|
|
646
|
+
return { ok: true, data: parsed };
|
|
647
|
+
} catch (e) {
|
|
648
|
+
return { ok: false, error: `配置解析失败: ${e.message}` };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function getOpenclawWorkspaceInfo() {
|
|
653
|
+
const readResult = readOpenclawConfigFile();
|
|
654
|
+
let workspaceDir = OPENCLAW_WORKSPACE_DIR;
|
|
655
|
+
let configError = readResult.error || '';
|
|
656
|
+
if (!configError && readResult.exists && readResult.content.trim()) {
|
|
657
|
+
const parsed = parseOpenclawConfigText(readResult.content);
|
|
658
|
+
if (parsed.ok) {
|
|
659
|
+
workspaceDir = resolveOpenclawWorkspaceDir(parsed.data);
|
|
660
|
+
} else {
|
|
661
|
+
configError = parsed.error || '';
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return {
|
|
665
|
+
workspaceDir,
|
|
666
|
+
configError,
|
|
667
|
+
configPath: readResult.path || OPENCLAW_CONFIG_FILE
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function readOpenclawAgentsFile() {
|
|
672
|
+
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
673
|
+
const baseDir = workspaceInfo.workspaceDir;
|
|
674
|
+
const filePath = path.join(baseDir, AGENTS_FILE_NAME);
|
|
675
|
+
|
|
676
|
+
if (!fs.existsSync(baseDir)) {
|
|
677
|
+
return {
|
|
678
|
+
exists: false,
|
|
679
|
+
path: filePath,
|
|
680
|
+
content: '',
|
|
681
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
|
|
682
|
+
workspaceDir: baseDir,
|
|
683
|
+
configError: workspaceInfo.configError,
|
|
684
|
+
baseDirMissing: true
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const readResult = readAgentsFile({ baseDir });
|
|
689
|
+
return {
|
|
690
|
+
...readResult,
|
|
691
|
+
workspaceDir: baseDir,
|
|
692
|
+
configError: workspaceInfo.configError
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function applyOpenclawAgentsFile(params = {}) {
|
|
697
|
+
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
698
|
+
const baseDir = workspaceInfo.workspaceDir;
|
|
699
|
+
ensureDir(baseDir);
|
|
700
|
+
const result = applyAgentsFile({
|
|
701
|
+
...params,
|
|
702
|
+
baseDir
|
|
703
|
+
});
|
|
704
|
+
return {
|
|
705
|
+
...result,
|
|
706
|
+
workspaceDir: baseDir,
|
|
707
|
+
configError: workspaceInfo.configError
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function applyOpenclawConfig(params = {}) {
|
|
712
|
+
const content = typeof params.content === 'string' ? params.content : '';
|
|
713
|
+
const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
|
|
714
|
+
const normalized = normalizeLineEnding(content, lineEnding);
|
|
715
|
+
const parsed = parseOpenclawConfigText(normalized);
|
|
716
|
+
if (!parsed.ok) {
|
|
717
|
+
return { success: false, error: parsed.error };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
ensureDir(OPENCLAW_DIR);
|
|
722
|
+
const backupPath = backupFileIfNeededOnce(OPENCLAW_CONFIG_FILE);
|
|
723
|
+
fs.writeFileSync(OPENCLAW_CONFIG_FILE, normalized, 'utf-8');
|
|
724
|
+
const result = {
|
|
725
|
+
success: true,
|
|
726
|
+
targetPath: OPENCLAW_CONFIG_FILE
|
|
727
|
+
};
|
|
728
|
+
if (backupPath) {
|
|
729
|
+
result.backupPath = backupPath;
|
|
730
|
+
}
|
|
731
|
+
return result;
|
|
732
|
+
} catch (e) {
|
|
733
|
+
return {
|
|
734
|
+
success: false,
|
|
735
|
+
error: e.message || '写入 OpenClaw 配置失败'
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function readJsonObjectFromFile(filePath, fallback = {}) {
|
|
741
|
+
if (!fs.existsSync(filePath)) {
|
|
742
|
+
return { ok: true, exists: false, data: { ...fallback } };
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
try {
|
|
746
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
747
|
+
if (!content.trim()) {
|
|
748
|
+
return { ok: true, exists: true, data: { ...fallback } };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const parsed = JSON.parse(content);
|
|
752
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
753
|
+
return {
|
|
754
|
+
ok: false,
|
|
755
|
+
exists: true,
|
|
756
|
+
error: `配置文件格式错误(根节点必须是对象): ${filePath}`
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
return { ok: true, exists: true, data: parsed };
|
|
760
|
+
} catch (e) {
|
|
761
|
+
return {
|
|
762
|
+
ok: false,
|
|
763
|
+
exists: true,
|
|
764
|
+
error: `配置文件解析失败: ${e.message}`
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function backupFileIfNeededOnce(filePath, backupPrefix = 'codexmate-backup') {
|
|
770
|
+
if (!fs.existsSync(filePath)) {
|
|
771
|
+
return '';
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const dirPath = path.dirname(filePath);
|
|
775
|
+
const baseName = path.basename(filePath);
|
|
776
|
+
const existingPrefix = `${baseName}.${backupPrefix}-`;
|
|
777
|
+
const hasBackup = fs.readdirSync(dirPath).some(fileName =>
|
|
778
|
+
fileName.startsWith(existingPrefix) && fileName.endsWith('.bak')
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
if (hasBackup) {
|
|
782
|
+
return '';
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const backupPath = path.join(dirPath, `${existingPrefix}${formatTimestampForFileName()}.bak`);
|
|
786
|
+
fs.copyFileSync(filePath, backupPath);
|
|
787
|
+
return backupPath;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function writeJsonAtomic(filePath, data) {
|
|
791
|
+
const dirPath = path.dirname(filePath);
|
|
792
|
+
ensureDir(dirPath);
|
|
793
|
+
|
|
794
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
795
|
+
const content = `${JSON.stringify(data, null, 2)}\n`;
|
|
796
|
+
|
|
797
|
+
try {
|
|
798
|
+
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
799
|
+
try {
|
|
800
|
+
fs.renameSync(tmpPath, filePath);
|
|
801
|
+
} catch (renameError) {
|
|
802
|
+
if (process.platform === 'win32') {
|
|
803
|
+
fs.copyFileSync(tmpPath, filePath);
|
|
804
|
+
fs.unlinkSync(tmpPath);
|
|
805
|
+
} else {
|
|
806
|
+
throw renameError;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
} catch (e) {
|
|
810
|
+
if (fs.existsSync(tmpPath)) {
|
|
811
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
812
|
+
}
|
|
813
|
+
throw new Error(`写入 JSON 文件失败: ${e.message}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function normalizeRecentConfigs(items) {
|
|
818
|
+
if (!Array.isArray(items)) return [];
|
|
819
|
+
const output = [];
|
|
820
|
+
const seen = new Set();
|
|
821
|
+
for (const item of items) {
|
|
822
|
+
if (!item || typeof item !== 'object') continue;
|
|
823
|
+
const provider = typeof item.provider === 'string' ? item.provider.trim() : '';
|
|
824
|
+
const model = typeof item.model === 'string' ? item.model.trim() : '';
|
|
825
|
+
if (!provider || !model) continue;
|
|
826
|
+
const key = `${provider}::${model}`;
|
|
827
|
+
if (seen.has(key)) continue;
|
|
828
|
+
seen.add(key);
|
|
829
|
+
output.push({
|
|
830
|
+
provider,
|
|
831
|
+
model,
|
|
832
|
+
usedAt: typeof item.usedAt === 'string' ? item.usedAt : ''
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
return output;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function readRecentConfigs() {
|
|
839
|
+
return normalizeRecentConfigs(readJsonArrayFile(RECENT_CONFIGS_FILE, []));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function writeRecentConfigs(items) {
|
|
843
|
+
writeJsonAtomic(RECENT_CONFIGS_FILE, items);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function recordRecentConfig(provider, model) {
|
|
847
|
+
const providerName = typeof provider === 'string' ? provider.trim() : '';
|
|
848
|
+
const modelName = typeof model === 'string' ? model.trim() : '';
|
|
849
|
+
if (!providerName || !modelName) return;
|
|
850
|
+
|
|
851
|
+
const now = new Date().toISOString();
|
|
852
|
+
const current = readRecentConfigs();
|
|
853
|
+
const next = [{
|
|
854
|
+
provider: providerName,
|
|
855
|
+
model: modelName,
|
|
856
|
+
usedAt: now
|
|
857
|
+
}];
|
|
858
|
+
|
|
859
|
+
for (const item of current) {
|
|
860
|
+
if (item.provider === providerName && item.model === modelName) continue;
|
|
861
|
+
next.push(item);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const trimmed = next.slice(0, MAX_RECENT_CONFIGS);
|
|
865
|
+
writeRecentConfigs(trimmed);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function isValidHttpUrl(value) {
|
|
869
|
+
if (typeof value !== 'string' || !value.trim()) return false;
|
|
870
|
+
try {
|
|
871
|
+
const parsed = new URL(value);
|
|
872
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
873
|
+
} catch (e) {
|
|
874
|
+
return false;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function normalizeBaseUrl(value) {
|
|
879
|
+
if (typeof value !== 'string') return '';
|
|
880
|
+
return value.trim().replace(/\/+$/g, '');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function joinApiUrl(baseUrl, pathSuffix) {
|
|
884
|
+
const trimmed = normalizeBaseUrl(baseUrl);
|
|
885
|
+
if (!trimmed) return '';
|
|
886
|
+
const safeSuffix = String(pathSuffix || '').replace(/^\/+/g, '');
|
|
887
|
+
if (!safeSuffix) return trimmed;
|
|
888
|
+
if (/\/v1$/i.test(trimmed)) {
|
|
889
|
+
return `${trimmed}/${safeSuffix}`;
|
|
890
|
+
}
|
|
891
|
+
return `${trimmed}/v1/${safeSuffix}`;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function buildModelsProbeUrl(baseUrl) {
|
|
895
|
+
return joinApiUrl(baseUrl, 'models');
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function normalizeWireApi(value) {
|
|
899
|
+
const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
900
|
+
if (!raw) return 'responses';
|
|
901
|
+
return raw.replace(/[\s-]/g, '_');
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
function buildModelProbeSpec(provider, modelName, baseUrl) {
|
|
905
|
+
const model = typeof modelName === 'string' ? modelName.trim() : '';
|
|
906
|
+
if (!model) return null;
|
|
907
|
+
|
|
908
|
+
const wireApi = normalizeWireApi(provider && provider.wire_api);
|
|
909
|
+
if (wireApi === 'chat_completions' || wireApi === 'chat') {
|
|
910
|
+
return {
|
|
911
|
+
url: joinApiUrl(baseUrl, 'chat/completions'),
|
|
912
|
+
body: {
|
|
913
|
+
model,
|
|
914
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
915
|
+
max_tokens: 1,
|
|
916
|
+
temperature: 0
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (wireApi === 'completions') {
|
|
922
|
+
return {
|
|
923
|
+
url: joinApiUrl(baseUrl, 'completions'),
|
|
924
|
+
body: {
|
|
925
|
+
model,
|
|
926
|
+
prompt: 'ping',
|
|
927
|
+
max_tokens: 1,
|
|
928
|
+
temperature: 0
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
url: joinApiUrl(baseUrl, 'responses'),
|
|
935
|
+
body: {
|
|
936
|
+
model,
|
|
937
|
+
input: 'ping',
|
|
938
|
+
max_output_tokens: 1
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function probeUrl(targetUrl, options = {}) {
|
|
944
|
+
return new Promise((resolve) => {
|
|
945
|
+
let parsed;
|
|
946
|
+
try {
|
|
947
|
+
parsed = new URL(targetUrl);
|
|
948
|
+
} catch (e) {
|
|
949
|
+
return resolve({ ok: false, error: 'Invalid URL' });
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
953
|
+
const headers = {
|
|
954
|
+
'User-Agent': 'codexmate-health-check',
|
|
955
|
+
'Accept': 'application/json'
|
|
956
|
+
};
|
|
957
|
+
if (options.apiKey) {
|
|
958
|
+
headers['Authorization'] = `Bearer ${options.apiKey}`;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
962
|
+
const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
|
|
963
|
+
const start = Date.now();
|
|
964
|
+
const req = transport.request(parsed, { method: 'GET', headers }, (res) => {
|
|
965
|
+
const chunks = [];
|
|
966
|
+
let size = 0;
|
|
967
|
+
res.on('data', (chunk) => {
|
|
968
|
+
if (!chunk) return;
|
|
969
|
+
size += chunk.length;
|
|
970
|
+
if (size <= maxBytes) {
|
|
971
|
+
chunks.push(chunk);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
res.on('end', () => {
|
|
975
|
+
const body = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
976
|
+
resolve({
|
|
977
|
+
ok: true,
|
|
978
|
+
status: res.statusCode || 0,
|
|
979
|
+
durationMs: Date.now() - start,
|
|
980
|
+
body
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
req.setTimeout(timeoutMs, () => {
|
|
986
|
+
req.destroy(new Error('timeout'));
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
req.on('error', (err) => {
|
|
990
|
+
resolve({
|
|
991
|
+
ok: false,
|
|
992
|
+
error: err.message || 'request failed',
|
|
993
|
+
durationMs: Date.now() - start
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
req.end();
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function probeJsonPost(targetUrl, body, options = {}) {
|
|
1002
|
+
return new Promise((resolve) => {
|
|
1003
|
+
let parsed;
|
|
1004
|
+
try {
|
|
1005
|
+
parsed = new URL(targetUrl);
|
|
1006
|
+
} catch (e) {
|
|
1007
|
+
return resolve({ ok: false, error: 'Invalid URL' });
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
1011
|
+
const headers = {
|
|
1012
|
+
'User-Agent': 'codexmate-health-check',
|
|
1013
|
+
'Accept': 'application/json',
|
|
1014
|
+
'Content-Type': 'application/json'
|
|
1015
|
+
};
|
|
1016
|
+
if (options.apiKey) {
|
|
1017
|
+
headers['Authorization'] = `Bearer ${options.apiKey}`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const payload = JSON.stringify(body || {});
|
|
1021
|
+
headers['Content-Length'] = Buffer.byteLength(payload);
|
|
1022
|
+
|
|
1023
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
1024
|
+
const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
|
|
1025
|
+
const start = Date.now();
|
|
1026
|
+
const req = transport.request(parsed, { method: 'POST', headers }, (res) => {
|
|
1027
|
+
const chunks = [];
|
|
1028
|
+
let size = 0;
|
|
1029
|
+
res.on('data', (chunk) => {
|
|
1030
|
+
if (!chunk) return;
|
|
1031
|
+
size += chunk.length;
|
|
1032
|
+
if (size <= maxBytes) {
|
|
1033
|
+
chunks.push(chunk);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
res.on('end', () => {
|
|
1037
|
+
const bodyText = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
1038
|
+
resolve({
|
|
1039
|
+
ok: true,
|
|
1040
|
+
status: res.statusCode || 0,
|
|
1041
|
+
durationMs: Date.now() - start,
|
|
1042
|
+
body: bodyText
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
req.setTimeout(timeoutMs, () => {
|
|
1048
|
+
req.destroy(new Error('timeout'));
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
req.on('error', (err) => {
|
|
1052
|
+
resolve({
|
|
1053
|
+
ok: false,
|
|
1054
|
+
error: err.message || 'request failed',
|
|
1055
|
+
durationMs: Date.now() - start
|
|
1056
|
+
});
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
req.write(payload);
|
|
1060
|
+
req.end();
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function extractModelIds(payload) {
|
|
1065
|
+
const ids = [];
|
|
1066
|
+
const pushValue = (value) => {
|
|
1067
|
+
if (typeof value === 'string' && value.trim()) {
|
|
1068
|
+
ids.push(value.trim());
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
if (!payload) return ids;
|
|
1073
|
+
|
|
1074
|
+
if (Array.isArray(payload)) {
|
|
1075
|
+
for (const item of payload) {
|
|
1076
|
+
if (item && typeof item === 'object') {
|
|
1077
|
+
pushValue(item.id);
|
|
1078
|
+
pushValue(item.model);
|
|
1079
|
+
pushValue(item.name);
|
|
1080
|
+
} else {
|
|
1081
|
+
pushValue(item);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return ids;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (Array.isArray(payload.data)) {
|
|
1088
|
+
for (const item of payload.data) {
|
|
1089
|
+
if (item && typeof item === 'object') {
|
|
1090
|
+
pushValue(item.id);
|
|
1091
|
+
pushValue(item.model);
|
|
1092
|
+
pushValue(item.name);
|
|
1093
|
+
} else {
|
|
1094
|
+
pushValue(item);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (Array.isArray(payload.models)) {
|
|
1100
|
+
for (const item of payload.models) {
|
|
1101
|
+
if (item && typeof item === 'object') {
|
|
1102
|
+
pushValue(item.id);
|
|
1103
|
+
pushValue(item.model);
|
|
1104
|
+
pushValue(item.name);
|
|
1105
|
+
} else {
|
|
1106
|
+
pushValue(item);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
return ids;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async function runRemoteHealthCheck(provider, modelName, options = {}) {
|
|
1115
|
+
const issues = [];
|
|
1116
|
+
const results = {};
|
|
1117
|
+
const baseUrl = normalizeBaseUrl(provider && provider.base_url ? provider.base_url : '');
|
|
1118
|
+
if (!baseUrl) {
|
|
1119
|
+
issues.push({
|
|
1120
|
+
code: 'remote-skip-base-url',
|
|
1121
|
+
message: '无法进行远程探测:base_url 为空',
|
|
1122
|
+
suggestion: '补全 base_url 或关闭远程探测'
|
|
1123
|
+
});
|
|
1124
|
+
return { issues, results };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const requiresAuth = provider && provider.requires_openai_auth !== false;
|
|
1128
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
1129
|
+
? provider.preferred_auth_method.trim()
|
|
1130
|
+
: '';
|
|
1131
|
+
const authValue = requiresAuth ? apiKey : (apiKey || '');
|
|
1132
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
1133
|
+
|
|
1134
|
+
const baseProbe = await probeUrl(baseUrl, { apiKey: authValue, timeoutMs });
|
|
1135
|
+
results.base = {
|
|
1136
|
+
url: baseUrl,
|
|
1137
|
+
status: baseProbe.status || 0,
|
|
1138
|
+
ok: baseProbe.ok,
|
|
1139
|
+
durationMs: baseProbe.durationMs || 0
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
if (!baseProbe.ok) {
|
|
1143
|
+
issues.push({
|
|
1144
|
+
code: 'remote-unreachable',
|
|
1145
|
+
message: `远程探测失败:${baseProbe.error || '无法连接'}`,
|
|
1146
|
+
suggestion: '检查网络与 base_url 可达性'
|
|
1147
|
+
});
|
|
1148
|
+
return { issues, results };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (baseProbe.status === 401 || baseProbe.status === 403) {
|
|
1152
|
+
issues.push({
|
|
1153
|
+
code: 'remote-auth-failed',
|
|
1154
|
+
message: '远程探测鉴权失败(401/403)',
|
|
1155
|
+
suggestion: '检查 API Key 或认证方式'
|
|
1156
|
+
});
|
|
1157
|
+
} else if (baseProbe.status >= 400) {
|
|
1158
|
+
issues.push({
|
|
1159
|
+
code: 'remote-http-error',
|
|
1160
|
+
message: `远程探测返回异常状态: ${baseProbe.status}`,
|
|
1161
|
+
suggestion: '检查 base_url 是否正确'
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const modelsUrl = buildModelsProbeUrl(baseUrl);
|
|
1166
|
+
if (modelsUrl) {
|
|
1167
|
+
const modelsProbe = await probeUrl(modelsUrl, { apiKey: authValue, timeoutMs, maxBytes: 256 * 1024 });
|
|
1168
|
+
results.models = {
|
|
1169
|
+
url: modelsUrl,
|
|
1170
|
+
status: modelsProbe.status || 0,
|
|
1171
|
+
ok: modelsProbe.ok,
|
|
1172
|
+
durationMs: modelsProbe.durationMs || 0
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
if (!modelsProbe.ok) {
|
|
1176
|
+
issues.push({
|
|
1177
|
+
code: 'remote-models-unreachable',
|
|
1178
|
+
message: `模型列表探测失败:${modelsProbe.error || '无法连接'}`,
|
|
1179
|
+
suggestion: '检查 base_url 是否包含 /v1 或关闭远程探测'
|
|
1180
|
+
});
|
|
1181
|
+
} else if (modelsProbe.status === 401 || modelsProbe.status === 403) {
|
|
1182
|
+
issues.push({
|
|
1183
|
+
code: 'remote-models-auth-failed',
|
|
1184
|
+
message: '模型列表鉴权失败(401/403)',
|
|
1185
|
+
suggestion: '检查 API Key 或认证方式'
|
|
1186
|
+
});
|
|
1187
|
+
} else if (modelsProbe.status >= 400) {
|
|
1188
|
+
issues.push({
|
|
1189
|
+
code: 'remote-models-http-error',
|
|
1190
|
+
message: `模型列表返回异常状态: ${modelsProbe.status}`,
|
|
1191
|
+
suggestion: '确认 /v1/models 可用'
|
|
1192
|
+
});
|
|
1193
|
+
} else {
|
|
1194
|
+
let payload = null;
|
|
1195
|
+
try {
|
|
1196
|
+
payload = modelsProbe.body ? JSON.parse(modelsProbe.body) : null;
|
|
1197
|
+
} catch (e) {
|
|
1198
|
+
issues.push({
|
|
1199
|
+
code: 'remote-models-parse',
|
|
1200
|
+
message: '模型列表解析失败(非 JSON)',
|
|
1201
|
+
suggestion: '确认 /v1/models 返回 JSON'
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (payload) {
|
|
1206
|
+
const ids = extractModelIds(payload);
|
|
1207
|
+
if (ids.length === 0) {
|
|
1208
|
+
issues.push({
|
|
1209
|
+
code: 'remote-models-empty',
|
|
1210
|
+
message: '模型列表为空或结构无法识别',
|
|
1211
|
+
suggestion: '确认 provider 是否兼容 /v1/models'
|
|
1212
|
+
});
|
|
1213
|
+
} else if (modelName && !ids.includes(modelName)) {
|
|
1214
|
+
issues.push({
|
|
1215
|
+
code: 'remote-model-unavailable',
|
|
1216
|
+
message: `远程模型列表中未找到: ${modelName}`,
|
|
1217
|
+
suggestion: '切换模型或确认模型名称'
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const modelProbeSpec = buildModelProbeSpec(provider, modelName, baseUrl);
|
|
1225
|
+
if (modelProbeSpec && modelProbeSpec.url) {
|
|
1226
|
+
const modelProbe = await probeJsonPost(modelProbeSpec.url, modelProbeSpec.body, {
|
|
1227
|
+
apiKey: authValue,
|
|
1228
|
+
timeoutMs,
|
|
1229
|
+
maxBytes: 256 * 1024
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
results.modelProbe = {
|
|
1233
|
+
url: modelProbeSpec.url,
|
|
1234
|
+
status: modelProbe.status || 0,
|
|
1235
|
+
ok: modelProbe.ok,
|
|
1236
|
+
durationMs: modelProbe.durationMs || 0
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
if (!modelProbe.ok) {
|
|
1240
|
+
issues.push({
|
|
1241
|
+
code: 'remote-model-probe-unreachable',
|
|
1242
|
+
message: `模型可用性探测失败:${modelProbe.error || '无法连接'}`,
|
|
1243
|
+
suggestion: '检查网络或模型接口是否可用'
|
|
1244
|
+
});
|
|
1245
|
+
} else if (modelProbe.status === 401 || modelProbe.status === 403) {
|
|
1246
|
+
issues.push({
|
|
1247
|
+
code: 'remote-model-probe-auth-failed',
|
|
1248
|
+
message: '模型可用性探测鉴权失败(401/403)',
|
|
1249
|
+
suggestion: '检查 API Key 或认证方式'
|
|
1250
|
+
});
|
|
1251
|
+
} else if (modelProbe.status >= 400) {
|
|
1252
|
+
issues.push({
|
|
1253
|
+
code: 'remote-model-probe-http-error',
|
|
1254
|
+
message: `模型可用性探测返回异常状态: ${modelProbe.status}`,
|
|
1255
|
+
suggestion: '检查模型或接口路径'
|
|
1256
|
+
});
|
|
1257
|
+
} else {
|
|
1258
|
+
let payload = null;
|
|
1259
|
+
try {
|
|
1260
|
+
payload = modelProbe.body ? JSON.parse(modelProbe.body) : null;
|
|
1261
|
+
} catch (e) {
|
|
1262
|
+
issues.push({
|
|
1263
|
+
code: 'remote-model-probe-parse',
|
|
1264
|
+
message: '模型可用性探测解析失败(非 JSON)',
|
|
1265
|
+
suggestion: '确认模型接口返回 JSON'
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
if (payload && payload.error) {
|
|
1269
|
+
const message = typeof payload.error.message === 'string'
|
|
1270
|
+
? payload.error.message
|
|
1271
|
+
: '模型接口返回错误';
|
|
1272
|
+
issues.push({
|
|
1273
|
+
code: 'remote-model-probe-error',
|
|
1274
|
+
message: `模型可用性探测失败:${message}`,
|
|
1275
|
+
suggestion: '检查模型名与权限'
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return { issues, results };
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async function buildConfigHealthReport(params = {}) {
|
|
1285
|
+
const issues = [];
|
|
1286
|
+
const status = readConfigOrVirtualDefault();
|
|
1287
|
+
const config = status.config || {};
|
|
1288
|
+
|
|
1289
|
+
if (status.isVirtual) {
|
|
1290
|
+
issues.push({
|
|
1291
|
+
code: 'config-missing',
|
|
1292
|
+
message: status.reason || '未检测到 config.toml',
|
|
1293
|
+
suggestion: '在模板编辑器中确认应用配置,生成可用的 config.toml'
|
|
1294
|
+
});
|
|
240
1295
|
}
|
|
241
1296
|
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
1297
|
+
const providerName = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
1298
|
+
const modelName = typeof config.model === 'string' ? config.model.trim() : '';
|
|
1299
|
+
if (!providerName) {
|
|
1300
|
+
issues.push({
|
|
1301
|
+
code: 'provider-missing',
|
|
1302
|
+
message: '当前 provider 未设置',
|
|
1303
|
+
suggestion: '在模板中设置 model_provider'
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
246
1306
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function resolveHomePath(input) {
|
|
256
|
-
const raw = typeof input === 'string' ? input.trim() : '';
|
|
257
|
-
if (!raw) return '';
|
|
258
|
-
if (raw === '~') return os.homedir();
|
|
259
|
-
if (raw.startsWith('~/') || raw.startsWith('~\\')) {
|
|
260
|
-
return path.join(os.homedir(), raw.slice(2));
|
|
261
|
-
}
|
|
262
|
-
return raw;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function resolveOpenclawWorkspaceDir(config) {
|
|
266
|
-
const workspace = config
|
|
267
|
-
&& config.agents
|
|
268
|
-
&& config.agents.defaults
|
|
269
|
-
&& typeof config.agents.defaults.workspace === 'string'
|
|
270
|
-
? config.agents.defaults.workspace
|
|
271
|
-
: '';
|
|
272
|
-
const resolved = resolveHomePath(workspace);
|
|
273
|
-
if (!resolved) {
|
|
274
|
-
return OPENCLAW_WORKSPACE_DIR;
|
|
275
|
-
}
|
|
276
|
-
if (path.isAbsolute(resolved)) {
|
|
277
|
-
return resolved;
|
|
278
|
-
}
|
|
279
|
-
return path.join(OPENCLAW_DIR, resolved);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function readOpenclawConfigFile() {
|
|
283
|
-
const filePath = OPENCLAW_CONFIG_FILE;
|
|
284
|
-
if (!fs.existsSync(filePath)) {
|
|
285
|
-
return {
|
|
286
|
-
exists: false,
|
|
287
|
-
path: filePath,
|
|
288
|
-
content: '',
|
|
289
|
-
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n'
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
try {
|
|
294
|
-
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
295
|
-
return {
|
|
296
|
-
exists: true,
|
|
297
|
-
path: filePath,
|
|
298
|
-
content: stripUtf8Bom(raw),
|
|
299
|
-
lineEnding: detectLineEnding(raw)
|
|
300
|
-
};
|
|
301
|
-
} catch (e) {
|
|
302
|
-
return { error: `读取 OpenClaw 配置失败: ${e.message}` };
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function parseOpenclawConfigText(content) {
|
|
307
|
-
const raw = stripUtf8Bom(typeof content === 'string' ? content : '');
|
|
308
|
-
if (!raw.trim()) {
|
|
309
|
-
return { ok: false, error: 'OpenClaw 配置内容不能为空' };
|
|
310
|
-
}
|
|
311
|
-
try {
|
|
312
|
-
const parsed = JSON5.parse(raw);
|
|
313
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
314
|
-
return { ok: false, error: '配置格式错误(根节点必须是对象)' };
|
|
315
|
-
}
|
|
316
|
-
return { ok: true, data: parsed };
|
|
317
|
-
} catch (e) {
|
|
318
|
-
return { ok: false, error: `配置解析失败: ${e.message}` };
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function getOpenclawWorkspaceInfo() {
|
|
323
|
-
const readResult = readOpenclawConfigFile();
|
|
324
|
-
let workspaceDir = OPENCLAW_WORKSPACE_DIR;
|
|
325
|
-
let configError = readResult.error || '';
|
|
326
|
-
if (!configError && readResult.exists && readResult.content.trim()) {
|
|
327
|
-
const parsed = parseOpenclawConfigText(readResult.content);
|
|
328
|
-
if (parsed.ok) {
|
|
329
|
-
workspaceDir = resolveOpenclawWorkspaceDir(parsed.data);
|
|
330
|
-
} else {
|
|
331
|
-
configError = parsed.error || '';
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
return {
|
|
335
|
-
workspaceDir,
|
|
336
|
-
configError,
|
|
337
|
-
configPath: readResult.path || OPENCLAW_CONFIG_FILE
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function readOpenclawAgentsFile() {
|
|
342
|
-
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
343
|
-
const baseDir = workspaceInfo.workspaceDir;
|
|
344
|
-
const filePath = path.join(baseDir, AGENTS_FILE_NAME);
|
|
345
|
-
|
|
346
|
-
if (!fs.existsSync(baseDir)) {
|
|
347
|
-
return {
|
|
348
|
-
exists: false,
|
|
349
|
-
path: filePath,
|
|
350
|
-
content: '',
|
|
351
|
-
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
|
|
352
|
-
workspaceDir: baseDir,
|
|
353
|
-
configError: workspaceInfo.configError,
|
|
354
|
-
baseDirMissing: true
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const readResult = readAgentsFile({ baseDir });
|
|
359
|
-
return {
|
|
360
|
-
...readResult,
|
|
361
|
-
workspaceDir: baseDir,
|
|
362
|
-
configError: workspaceInfo.configError
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function applyOpenclawAgentsFile(params = {}) {
|
|
367
|
-
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
368
|
-
const baseDir = workspaceInfo.workspaceDir;
|
|
369
|
-
ensureDir(baseDir);
|
|
370
|
-
const result = applyAgentsFile({
|
|
371
|
-
...params,
|
|
372
|
-
baseDir
|
|
373
|
-
});
|
|
374
|
-
return {
|
|
375
|
-
...result,
|
|
376
|
-
workspaceDir: baseDir,
|
|
377
|
-
configError: workspaceInfo.configError
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function applyOpenclawConfig(params = {}) {
|
|
382
|
-
const content = typeof params.content === 'string' ? params.content : '';
|
|
383
|
-
const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
|
|
384
|
-
const normalized = normalizeLineEnding(content, lineEnding);
|
|
385
|
-
const parsed = parseOpenclawConfigText(normalized);
|
|
386
|
-
if (!parsed.ok) {
|
|
387
|
-
return { success: false, error: parsed.error };
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
try {
|
|
391
|
-
ensureDir(OPENCLAW_DIR);
|
|
392
|
-
const backupPath = backupFileIfNeededOnce(OPENCLAW_CONFIG_FILE);
|
|
393
|
-
fs.writeFileSync(OPENCLAW_CONFIG_FILE, normalized, 'utf-8');
|
|
394
|
-
const result = {
|
|
395
|
-
success: true,
|
|
396
|
-
targetPath: OPENCLAW_CONFIG_FILE
|
|
397
|
-
};
|
|
398
|
-
if (backupPath) {
|
|
399
|
-
result.backupPath = backupPath;
|
|
400
|
-
}
|
|
401
|
-
return result;
|
|
402
|
-
} catch (e) {
|
|
403
|
-
return {
|
|
404
|
-
success: false,
|
|
405
|
-
error: e.message || '写入 OpenClaw 配置失败'
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
}
|
|
1307
|
+
if (!modelName) {
|
|
1308
|
+
issues.push({
|
|
1309
|
+
code: 'model-missing',
|
|
1310
|
+
message: '当前模型未设置',
|
|
1311
|
+
suggestion: '在模板中设置 model'
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
409
1314
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1315
|
+
const providers = config.model_providers && typeof config.model_providers === 'object'
|
|
1316
|
+
? config.model_providers
|
|
1317
|
+
: {};
|
|
1318
|
+
const provider = providerName ? providers[providerName] : null;
|
|
1319
|
+
if (providerName && !provider) {
|
|
1320
|
+
issues.push({
|
|
1321
|
+
code: 'provider-not-found',
|
|
1322
|
+
message: `当前 provider 未在配置中找到: ${providerName}`,
|
|
1323
|
+
suggestion: '检查 model_providers 是否包含该 provider 配置块'
|
|
1324
|
+
});
|
|
413
1325
|
}
|
|
414
1326
|
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
if (!
|
|
418
|
-
|
|
1327
|
+
if (provider && typeof provider === 'object') {
|
|
1328
|
+
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
1329
|
+
if (!isValidHttpUrl(baseUrl)) {
|
|
1330
|
+
issues.push({
|
|
1331
|
+
code: 'base-url-invalid',
|
|
1332
|
+
message: '当前 provider 的 base_url 无效',
|
|
1333
|
+
suggestion: '请设置为 http/https 的完整 URL'
|
|
1334
|
+
});
|
|
419
1335
|
}
|
|
420
1336
|
|
|
421
|
-
const
|
|
422
|
-
if (
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
1337
|
+
const requiresAuth = provider.requires_openai_auth;
|
|
1338
|
+
if (requiresAuth !== false) {
|
|
1339
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
1340
|
+
? provider.preferred_auth_method.trim()
|
|
1341
|
+
: '';
|
|
1342
|
+
if (!apiKey) {
|
|
1343
|
+
issues.push({
|
|
1344
|
+
code: 'api-key-missing',
|
|
1345
|
+
message: '当前 provider 未配置 API Key',
|
|
1346
|
+
suggestion: '在模板中设置 preferred_auth_method'
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
428
1349
|
}
|
|
429
|
-
return { ok: true, exists: true, data: parsed };
|
|
430
|
-
} catch (e) {
|
|
431
|
-
return {
|
|
432
|
-
ok: false,
|
|
433
|
-
exists: true,
|
|
434
|
-
error: `配置文件解析失败: ${e.message}`
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function backupFileIfNeededOnce(filePath, backupPrefix = 'codexmate-backup') {
|
|
440
|
-
if (!fs.existsSync(filePath)) {
|
|
441
|
-
return '';
|
|
442
1350
|
}
|
|
443
1351
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1352
|
+
if (modelName) {
|
|
1353
|
+
const models = readModels();
|
|
1354
|
+
if (!models.includes(modelName)) {
|
|
1355
|
+
issues.push({
|
|
1356
|
+
code: 'model-unavailable',
|
|
1357
|
+
message: `模型未在可用列表中找到: ${modelName}`,
|
|
1358
|
+
suggestion: '在模型列表中添加该模型或切换到已有模型'
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
453
1361
|
}
|
|
454
1362
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1363
|
+
const remoteEnabled = !!params.remote;
|
|
1364
|
+
let remote = null;
|
|
1365
|
+
if (remoteEnabled) {
|
|
1366
|
+
const baseUrl = provider && typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
1367
|
+
if (!provider) {
|
|
1368
|
+
issues.push({
|
|
1369
|
+
code: 'remote-skip-provider',
|
|
1370
|
+
message: '无法进行远程探测:provider 未找到',
|
|
1371
|
+
suggestion: '检查 model_provider 配置或关闭远程探测'
|
|
1372
|
+
});
|
|
1373
|
+
} else if (!isValidHttpUrl(baseUrl)) {
|
|
1374
|
+
issues.push({
|
|
1375
|
+
code: 'remote-skip-base-url',
|
|
1376
|
+
message: '无法进行远程探测:base_url 无效',
|
|
1377
|
+
suggestion: '补全 base_url 或关闭远程探测'
|
|
1378
|
+
});
|
|
1379
|
+
} else {
|
|
1380
|
+
const timeoutMs = Number.isFinite(params.timeoutMs)
|
|
1381
|
+
? Math.max(1000, Number(params.timeoutMs))
|
|
1382
|
+
: HEALTH_CHECK_TIMEOUT_MS;
|
|
1383
|
+
remote = await runRemoteHealthCheck(provider, modelName, { timeoutMs });
|
|
1384
|
+
if (remote && Array.isArray(remote.issues)) {
|
|
1385
|
+
issues.push(...remote.issues);
|
|
477
1386
|
}
|
|
478
1387
|
}
|
|
479
|
-
} catch (e) {
|
|
480
|
-
if (fs.existsSync(tmpPath)) {
|
|
481
|
-
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
482
|
-
}
|
|
483
|
-
throw new Error(`写入 JSON 文件失败: ${e.message}`);
|
|
484
1388
|
}
|
|
1389
|
+
|
|
1390
|
+
return {
|
|
1391
|
+
ok: issues.length === 0,
|
|
1392
|
+
issues,
|
|
1393
|
+
summary: {
|
|
1394
|
+
currentProvider: providerName,
|
|
1395
|
+
currentModel: modelName
|
|
1396
|
+
},
|
|
1397
|
+
remote
|
|
1398
|
+
};
|
|
485
1399
|
}
|
|
486
1400
|
|
|
487
1401
|
function formatTimestampForFileName(value) {
|
|
@@ -689,6 +1603,8 @@ function applyConfigTemplate(params = {}) {
|
|
|
689
1603
|
currentModels[activeProvider] = parsed.model;
|
|
690
1604
|
writeCurrentModels(currentModels);
|
|
691
1605
|
|
|
1606
|
+
recordRecentConfig(activeProvider, parsed.model);
|
|
1607
|
+
|
|
692
1608
|
return { success: true };
|
|
693
1609
|
}
|
|
694
1610
|
|
|
@@ -810,9 +1726,24 @@ function consumeInitNotice() {
|
|
|
810
1726
|
return notice;
|
|
811
1727
|
}
|
|
812
1728
|
|
|
1729
|
+
function normalizePathForCompare(targetPath, options = {}) {
|
|
1730
|
+
const ignoreCase = !!options.ignoreCase;
|
|
1731
|
+
let resolved = '';
|
|
1732
|
+
try {
|
|
1733
|
+
resolved = fs.realpathSync.native ? fs.realpathSync.native(targetPath) : fs.realpathSync(targetPath);
|
|
1734
|
+
} catch (e) {
|
|
1735
|
+
resolved = path.resolve(targetPath);
|
|
1736
|
+
}
|
|
1737
|
+
return ignoreCase ? resolved.toLowerCase() : resolved;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
813
1740
|
function isPathInside(targetPath, rootPath) {
|
|
814
|
-
|
|
815
|
-
|
|
1741
|
+
if (!targetPath || !rootPath) {
|
|
1742
|
+
return false;
|
|
1743
|
+
}
|
|
1744
|
+
const ignoreCase = process.platform === 'win32';
|
|
1745
|
+
const resolvedTarget = normalizePathForCompare(targetPath, { ignoreCase });
|
|
1746
|
+
const resolvedRoot = normalizePathForCompare(rootPath, { ignoreCase });
|
|
816
1747
|
if (resolvedTarget === resolvedRoot) {
|
|
817
1748
|
return true;
|
|
818
1749
|
}
|
|
@@ -1155,23 +2086,23 @@ function scanSessionContentForQuery(session, tokens, options = {}) {
|
|
|
1155
2086
|
? Math.max(0, Number(options.snippetLimit))
|
|
1156
2087
|
: 0;
|
|
1157
2088
|
|
|
1158
|
-
const messages = [];
|
|
1159
|
-
for (const record of records) {
|
|
1160
|
-
const message = extractMessageFromRecord(record, session.source);
|
|
1161
|
-
if (!message || !message.text) {
|
|
1162
|
-
continue;
|
|
1163
|
-
}
|
|
1164
|
-
messages.push(message);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
const filteredMessages = roleFilter === 'system'
|
|
1168
|
-
? messages
|
|
1169
|
-
: removeLeadingSystemMessage(messages);
|
|
1170
|
-
|
|
1171
|
-
let count = 0;
|
|
1172
|
-
const snippets = [];
|
|
1173
|
-
|
|
1174
|
-
for (const message of filteredMessages) {
|
|
2089
|
+
const messages = [];
|
|
2090
|
+
for (const record of records) {
|
|
2091
|
+
const message = extractMessageFromRecord(record, session.source);
|
|
2092
|
+
if (!message || !message.text) {
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
messages.push(message);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
const filteredMessages = roleFilter === 'system'
|
|
2099
|
+
? messages
|
|
2100
|
+
: removeLeadingSystemMessage(messages);
|
|
2101
|
+
|
|
2102
|
+
let count = 0;
|
|
2103
|
+
const snippets = [];
|
|
2104
|
+
|
|
2105
|
+
for (const message of filteredMessages) {
|
|
1175
2106
|
if (roleFilter !== 'all' && message.role !== roleFilter) {
|
|
1176
2107
|
continue;
|
|
1177
2108
|
}
|
|
@@ -1198,11 +2129,11 @@ function applySessionQueryFilter(sessions, options = {}) {
|
|
|
1198
2129
|
}
|
|
1199
2130
|
|
|
1200
2131
|
const mode = normalizeQueryMode(options.queryMode);
|
|
1201
|
-
const scope = normalizeQueryScope(options.queryScope);
|
|
1202
|
-
const roleFilter = normalizeRoleFilter(options.roleFilter);
|
|
1203
|
-
const contentScanLimit = Number.isFinite(Number(options.contentScanLimit))
|
|
1204
|
-
? Math.max(1, Number(options.contentScanLimit))
|
|
1205
|
-
: DEFAULT_CONTENT_SCAN_LIMIT;
|
|
2132
|
+
const scope = normalizeQueryScope(options.queryScope);
|
|
2133
|
+
const roleFilter = normalizeRoleFilter(options.roleFilter);
|
|
2134
|
+
const contentScanLimit = Number.isFinite(Number(options.contentScanLimit))
|
|
2135
|
+
? Math.max(1, Number(options.contentScanLimit))
|
|
2136
|
+
: DEFAULT_CONTENT_SCAN_LIMIT;
|
|
1206
2137
|
const contentScanBytes = Number.isFinite(Number(options.contentScanBytes))
|
|
1207
2138
|
? Math.max(1024, Number(options.contentScanBytes))
|
|
1208
2139
|
: SESSION_CONTENT_READ_BYTES;
|
|
@@ -1254,7 +2185,7 @@ function applySessionQueryFilter(sessions, options = {}) {
|
|
|
1254
2185
|
|
|
1255
2186
|
return results;
|
|
1256
2187
|
}
|
|
1257
|
-
function collectRecentJsonlFiles(rootDir, options = {}) {
|
|
2188
|
+
function collectRecentJsonlFiles(rootDir, options = {}) {
|
|
1258
2189
|
if (!fs.existsSync(rootDir)) {
|
|
1259
2190
|
return [];
|
|
1260
2191
|
}
|
|
@@ -1516,6 +2447,7 @@ function parseClaudeSessionSummary(filePath) {
|
|
|
1516
2447
|
}
|
|
1517
2448
|
|
|
1518
2449
|
function listCodexSessions(limit, options = {}) {
|
|
2450
|
+
const codexSessionsDir = getCodexSessionsDir();
|
|
1519
2451
|
const scanFactor = Number.isFinite(Number(options.scanFactor))
|
|
1520
2452
|
? Math.max(1, Number(options.scanFactor))
|
|
1521
2453
|
: SESSION_SCAN_FACTOR;
|
|
@@ -1531,7 +2463,7 @@ function listCodexSessions(limit, options = {}) {
|
|
|
1531
2463
|
const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
|
|
1532
2464
|
? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
|
|
1533
2465
|
: Math.max(scanCount * 2, minFiles);
|
|
1534
|
-
const files = collectRecentJsonlFiles(
|
|
2466
|
+
const files = collectRecentJsonlFiles(codexSessionsDir, {
|
|
1535
2467
|
returnCount: scanCount,
|
|
1536
2468
|
maxFilesScanned
|
|
1537
2469
|
});
|
|
@@ -1552,7 +2484,8 @@ function listCodexSessions(limit, options = {}) {
|
|
|
1552
2484
|
}
|
|
1553
2485
|
|
|
1554
2486
|
function listClaudeSessions(limit, options = {}) {
|
|
1555
|
-
|
|
2487
|
+
const claudeProjectsDir = getClaudeProjectsDir();
|
|
2488
|
+
if (!fs.existsSync(claudeProjectsDir)) {
|
|
1556
2489
|
return [];
|
|
1557
2490
|
}
|
|
1558
2491
|
|
|
@@ -1575,9 +2508,9 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
1575
2508
|
const sessions = [];
|
|
1576
2509
|
let projectDirs = [];
|
|
1577
2510
|
try {
|
|
1578
|
-
projectDirs = fs.readdirSync(
|
|
2511
|
+
projectDirs = fs.readdirSync(claudeProjectsDir, { withFileTypes: true })
|
|
1579
2512
|
.filter(entry => entry.isDirectory())
|
|
1580
|
-
.map(entry => path.join(
|
|
2513
|
+
.map(entry => path.join(claudeProjectsDir, entry.name));
|
|
1581
2514
|
} catch (e) {
|
|
1582
2515
|
projectDirs = [];
|
|
1583
2516
|
}
|
|
@@ -1597,6 +2530,11 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
1597
2530
|
let filePath = typeof entry.fullPath === 'string' && entry.fullPath
|
|
1598
2531
|
? entry.fullPath
|
|
1599
2532
|
: path.join(projectDir, `${sessionId}.jsonl`);
|
|
2533
|
+
filePath = expandHomePath(filePath);
|
|
2534
|
+
if (filePath && !path.isAbsolute(filePath)) {
|
|
2535
|
+
filePath = path.join(projectDir, filePath);
|
|
2536
|
+
}
|
|
2537
|
+
filePath = filePath ? path.resolve(filePath) : '';
|
|
1600
2538
|
|
|
1601
2539
|
if (!fs.existsSync(filePath)) {
|
|
1602
2540
|
continue;
|
|
@@ -1652,7 +2590,7 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
1652
2590
|
}
|
|
1653
2591
|
|
|
1654
2592
|
if (sessions.length === 0) {
|
|
1655
|
-
const fallbackFiles = collectRecentJsonlFiles(
|
|
2593
|
+
const fallbackFiles = collectRecentJsonlFiles(claudeProjectsDir, {
|
|
1656
2594
|
returnCount: scanCount,
|
|
1657
2595
|
maxFilesScanned,
|
|
1658
2596
|
ignoreSubPath: `${path.sep}subagents${path.sep}`
|
|
@@ -1712,18 +2650,18 @@ function listAllSessions(params = {}) {
|
|
|
1712
2650
|
sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
|
|
1713
2651
|
}
|
|
1714
2652
|
|
|
1715
|
-
let result = sessions;
|
|
1716
|
-
if (hasQuery) {
|
|
1717
|
-
result = applySessionQueryFilter(result, {
|
|
1718
|
-
tokens: queryTokens,
|
|
1719
|
-
queryMode: params.queryMode,
|
|
1720
|
-
queryScope: params.queryScope,
|
|
1721
|
-
roleFilter: params.roleFilter,
|
|
1722
|
-
contentScanLimit: params.contentScanLimit,
|
|
1723
|
-
contentScanBytes: params.contentScanBytes
|
|
1724
|
-
});
|
|
1725
|
-
}
|
|
1726
|
-
result = mergeAndLimitSessions(result, limit);
|
|
2653
|
+
let result = sessions;
|
|
2654
|
+
if (hasQuery) {
|
|
2655
|
+
result = applySessionQueryFilter(result, {
|
|
2656
|
+
tokens: queryTokens,
|
|
2657
|
+
queryMode: params.queryMode,
|
|
2658
|
+
queryScope: params.queryScope,
|
|
2659
|
+
roleFilter: params.roleFilter,
|
|
2660
|
+
contentScanLimit: params.contentScanLimit,
|
|
2661
|
+
contentScanBytes: params.contentScanBytes
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
result = mergeAndLimitSessions(result, limit);
|
|
1727
2665
|
if (!hasQuery) {
|
|
1728
2666
|
setSessionListCache(cacheKey, result);
|
|
1729
2667
|
}
|
|
@@ -1784,14 +2722,15 @@ function listSessionPaths(params = {}) {
|
|
|
1784
2722
|
}
|
|
1785
2723
|
|
|
1786
2724
|
function resolveSessionFilePath(source, filePath, sessionId) {
|
|
1787
|
-
const root = source === 'claude' ?
|
|
2725
|
+
const root = source === 'claude' ? getClaudeProjectsDir() : getCodexSessionsDir();
|
|
1788
2726
|
if (!root || !fs.existsSync(root)) {
|
|
1789
2727
|
return '';
|
|
1790
2728
|
}
|
|
1791
2729
|
|
|
1792
2730
|
if (typeof filePath === 'string' && filePath.trim()) {
|
|
1793
|
-
const
|
|
1794
|
-
|
|
2731
|
+
const expandedPath = expandHomePath(filePath.trim());
|
|
2732
|
+
const targetPath = expandedPath ? path.resolve(expandedPath) : '';
|
|
2733
|
+
if (targetPath && fs.existsSync(targetPath) && isPathInside(targetPath, root)) {
|
|
1795
2734
|
return targetPath;
|
|
1796
2735
|
}
|
|
1797
2736
|
}
|
|
@@ -2320,18 +3259,263 @@ function runSpeedTest(targetUrl, apiKey) {
|
|
|
2320
3259
|
// 命令
|
|
2321
3260
|
// ============================================================================
|
|
2322
3261
|
|
|
3262
|
+
// 交互式配置向导
|
|
3263
|
+
async function cmdSetup() {
|
|
3264
|
+
console.log('\n交互式配置向导');
|
|
3265
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3266
|
+
const lineQueue = [];
|
|
3267
|
+
let lineResolver = null;
|
|
3268
|
+
let rlClosed = false;
|
|
3269
|
+
rl.on('line', (line) => {
|
|
3270
|
+
if (lineResolver) {
|
|
3271
|
+
const resolve = lineResolver;
|
|
3272
|
+
lineResolver = null;
|
|
3273
|
+
resolve(line);
|
|
3274
|
+
} else {
|
|
3275
|
+
lineQueue.push(line);
|
|
3276
|
+
}
|
|
3277
|
+
});
|
|
3278
|
+
rl.on('close', () => {
|
|
3279
|
+
rlClosed = true;
|
|
3280
|
+
if (lineResolver) {
|
|
3281
|
+
const resolve = lineResolver;
|
|
3282
|
+
lineResolver = null;
|
|
3283
|
+
resolve('');
|
|
3284
|
+
}
|
|
3285
|
+
});
|
|
3286
|
+
const ask = async (question) => {
|
|
3287
|
+
if (question) {
|
|
3288
|
+
process.stdout.write(question);
|
|
3289
|
+
}
|
|
3290
|
+
if (lineQueue.length > 0) {
|
|
3291
|
+
return lineQueue.shift();
|
|
3292
|
+
}
|
|
3293
|
+
if (rlClosed) {
|
|
3294
|
+
return '';
|
|
3295
|
+
}
|
|
3296
|
+
return await new Promise(resolve => {
|
|
3297
|
+
lineResolver = resolve;
|
|
3298
|
+
});
|
|
3299
|
+
};
|
|
3300
|
+
|
|
3301
|
+
let providerName = '';
|
|
3302
|
+
let baseUrl = '';
|
|
3303
|
+
let apiKey = '';
|
|
3304
|
+
let modelName = '';
|
|
3305
|
+
let isCustomProvider = false;
|
|
3306
|
+
|
|
3307
|
+
try {
|
|
3308
|
+
const { config } = readConfigOrVirtualDefault();
|
|
3309
|
+
const providers = config.model_providers || {};
|
|
3310
|
+
const providerNames = Object.keys(providers);
|
|
3311
|
+
const defaultProvider = config.model_provider || providerNames[0] || '';
|
|
3312
|
+
let availableModels = [];
|
|
3313
|
+
let defaultModel = config.model || '';
|
|
3314
|
+
let modelFetchUnlimited = false;
|
|
3315
|
+
|
|
3316
|
+
while (true) {
|
|
3317
|
+
console.log('\n选择提供商:');
|
|
3318
|
+
if (providerNames.length > 0) {
|
|
3319
|
+
providerNames.forEach((name, index) => {
|
|
3320
|
+
console.log(` ${index + 1}. ${name}`);
|
|
3321
|
+
});
|
|
3322
|
+
console.log(` ${providerNames.length + 1}. 自定义`);
|
|
3323
|
+
} else {
|
|
3324
|
+
console.log(' (暂无提供商,需自定义)');
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
const suffix = defaultProvider ? ` (默认 ${defaultProvider})` : '';
|
|
3328
|
+
const input = (await ask(`请输入序号或名称${suffix}: `)).trim();
|
|
3329
|
+
|
|
3330
|
+
if (!input) {
|
|
3331
|
+
if (defaultProvider) {
|
|
3332
|
+
providerName = defaultProvider;
|
|
3333
|
+
isCustomProvider = false;
|
|
3334
|
+
break;
|
|
3335
|
+
}
|
|
3336
|
+
isCustomProvider = true;
|
|
3337
|
+
break;
|
|
3338
|
+
}
|
|
3339
|
+
|
|
3340
|
+
if (/^\d+$/.test(input)) {
|
|
3341
|
+
const index = parseInt(input, 10);
|
|
3342
|
+
if (index >= 1 && index <= providerNames.length) {
|
|
3343
|
+
providerName = providerNames[index - 1];
|
|
3344
|
+
isCustomProvider = false;
|
|
3345
|
+
break;
|
|
3346
|
+
}
|
|
3347
|
+
if (index === providerNames.length + 1) {
|
|
3348
|
+
isCustomProvider = true;
|
|
3349
|
+
break;
|
|
3350
|
+
}
|
|
3351
|
+
console.log('提示: 序号无效,请重试。');
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
if (providers[input]) {
|
|
3356
|
+
providerName = input;
|
|
3357
|
+
isCustomProvider = false;
|
|
3358
|
+
break;
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
if (isValidProviderName(input)) {
|
|
3362
|
+
providerName = input;
|
|
3363
|
+
isCustomProvider = true;
|
|
3364
|
+
break;
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
console.log('提示: 名称仅支持字母/数字/._-');
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
if (isCustomProvider && !providerName) {
|
|
3371
|
+
while (true) {
|
|
3372
|
+
const nameInput = (await ask('请输入自定义提供商名称(字母/数字/._-): ')).trim();
|
|
3373
|
+
if (!nameInput) {
|
|
3374
|
+
console.log('提示: 名称不能为空。');
|
|
3375
|
+
continue;
|
|
3376
|
+
}
|
|
3377
|
+
if (!isValidProviderName(nameInput)) {
|
|
3378
|
+
console.log('提示: 名称仅支持字母/数字/._-');
|
|
3379
|
+
continue;
|
|
3380
|
+
}
|
|
3381
|
+
providerName = nameInput;
|
|
3382
|
+
break;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
if (isCustomProvider) {
|
|
3387
|
+
while (true) {
|
|
3388
|
+
const urlInput = (await ask('Base URL: ')).trim();
|
|
3389
|
+
if (!urlInput) {
|
|
3390
|
+
console.log('提示: Base URL 不能为空。');
|
|
3391
|
+
continue;
|
|
3392
|
+
}
|
|
3393
|
+
baseUrl = urlInput;
|
|
3394
|
+
break;
|
|
3395
|
+
}
|
|
3396
|
+
apiKey = (await ask('API Key (可空): ')).trim();
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
let modelFetchError = '';
|
|
3400
|
+
if (providerName) {
|
|
3401
|
+
if (isCustomProvider) {
|
|
3402
|
+
const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
|
|
3403
|
+
if (res.unlimited) {
|
|
3404
|
+
modelFetchUnlimited = true;
|
|
3405
|
+
} else if (res.error) {
|
|
3406
|
+
modelFetchError = res.error;
|
|
3407
|
+
} else {
|
|
3408
|
+
availableModels = res.models || [];
|
|
3409
|
+
}
|
|
3410
|
+
} else {
|
|
3411
|
+
const res = await fetchProviderModels(providerName);
|
|
3412
|
+
if (res.unlimited) {
|
|
3413
|
+
modelFetchUnlimited = true;
|
|
3414
|
+
} else if (res.error) {
|
|
3415
|
+
modelFetchError = res.error;
|
|
3416
|
+
} else {
|
|
3417
|
+
availableModels = res.models || [];
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
if (modelFetchUnlimited) {
|
|
3422
|
+
console.log('提示: 提供商未提供模型列表,视为不限,请手动输入。');
|
|
3423
|
+
} else if (modelFetchError) {
|
|
3424
|
+
console.log(`提示: 获取模型列表失败: ${modelFetchError},请手动输入。`);
|
|
3425
|
+
}
|
|
3426
|
+
if (availableModels.length > 0) {
|
|
3427
|
+
if (!defaultModel || !availableModels.includes(defaultModel)) {
|
|
3428
|
+
defaultModel = availableModels[0];
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
while (true) {
|
|
3433
|
+
console.log('\n选择模型:');
|
|
3434
|
+
if (availableModels.length > 0) {
|
|
3435
|
+
availableModels.forEach((name, index) => {
|
|
3436
|
+
console.log(` ${index + 1}. ${name}`);
|
|
3437
|
+
});
|
|
3438
|
+
} else {
|
|
3439
|
+
console.log(' (暂无模型,将使用自定义输入)');
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
const suffix = defaultModel ? ` (默认 ${defaultModel})` : '';
|
|
3443
|
+
const input = (await ask(`请输入序号或名称${suffix}: `)).trim();
|
|
3444
|
+
|
|
3445
|
+
if (!input) {
|
|
3446
|
+
if (defaultModel) {
|
|
3447
|
+
modelName = defaultModel;
|
|
3448
|
+
break;
|
|
3449
|
+
}
|
|
3450
|
+
console.log('提示: 模型不能为空。');
|
|
3451
|
+
continue;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
if (/^\d+$/.test(input)) {
|
|
3455
|
+
const index = parseInt(input, 10);
|
|
3456
|
+
if (index >= 1 && index <= availableModels.length) {
|
|
3457
|
+
modelName = availableModels[index - 1];
|
|
3458
|
+
break;
|
|
3459
|
+
}
|
|
3460
|
+
console.log('提示: 序号无效,请重试。');
|
|
3461
|
+
continue;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
modelName = input;
|
|
3465
|
+
break;
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
console.log('\n即将应用:');
|
|
3469
|
+
console.log(' 提供商:', providerName);
|
|
3470
|
+
if (isCustomProvider) {
|
|
3471
|
+
console.log(' Base URL:', baseUrl);
|
|
3472
|
+
}
|
|
3473
|
+
console.log(' 模型:', modelName);
|
|
3474
|
+
|
|
3475
|
+
const confirm = (await ask('确认应用? (Y/n): ')).trim().toLowerCase();
|
|
3476
|
+
if (confirm === 'n' || confirm === 'no') {
|
|
3477
|
+
console.log('已取消');
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
if (isCustomProvider) {
|
|
3482
|
+
if (providers[providerName]) {
|
|
3483
|
+
cmdUpdate(providerName, baseUrl, apiKey, true);
|
|
3484
|
+
} else {
|
|
3485
|
+
cmdAdd(providerName, baseUrl, apiKey, true);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
const latestModels = readModels();
|
|
3490
|
+
if (modelName && !latestModels.includes(modelName)) {
|
|
3491
|
+
cmdAddModel(modelName, true);
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
cmdSwitch(providerName, true);
|
|
3495
|
+
cmdUseModel(modelName, true);
|
|
3496
|
+
|
|
3497
|
+
console.log('✓ 已应用配置');
|
|
3498
|
+
console.log(' 提供商:', providerName);
|
|
3499
|
+
console.log(' 模型:', modelName);
|
|
3500
|
+
console.log();
|
|
3501
|
+
} catch (e) {
|
|
3502
|
+
console.error('错误:', e.message || e);
|
|
3503
|
+
process.exitCode = 1;
|
|
3504
|
+
} finally {
|
|
3505
|
+
rl.close();
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
|
|
2323
3509
|
// 显示当前状态
|
|
2324
3510
|
function cmdStatus() {
|
|
2325
3511
|
const { config, isVirtual } = readConfigOrVirtualDefault();
|
|
2326
3512
|
const current = config.model_provider || '未设置';
|
|
2327
3513
|
const currentModel = config.model || '未设置';
|
|
2328
|
-
const models = readModels();
|
|
2329
|
-
const currentModels = readCurrentModels();
|
|
2330
3514
|
|
|
2331
3515
|
console.log('\n当前状态:');
|
|
2332
3516
|
console.log(' 提供商:', current);
|
|
2333
3517
|
console.log(' 模型:', currentModel);
|
|
2334
|
-
console.log(' 模型列表:
|
|
3518
|
+
console.log(' 模型列表: 接口提供');
|
|
2335
3519
|
if (isVirtual) {
|
|
2336
3520
|
console.log(' 说明: 当前为虚拟默认配置(config.toml 尚未创建)');
|
|
2337
3521
|
}
|
|
@@ -2371,21 +3555,30 @@ function cmdList() {
|
|
|
2371
3555
|
}
|
|
2372
3556
|
|
|
2373
3557
|
// 列出所有模型
|
|
2374
|
-
function cmdModels() {
|
|
2375
|
-
const
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
console.log(
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
3558
|
+
async function cmdModels() {
|
|
3559
|
+
const res = await fetchProviderModels('');
|
|
3560
|
+
if (res.error) {
|
|
3561
|
+
console.error('错误: 获取模型列表失败:', res.error);
|
|
3562
|
+
process.exitCode = 1;
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
if (res.unlimited) {
|
|
3566
|
+
const label = res.provider ? ` (${res.provider})` : '';
|
|
3567
|
+
console.log(`\n可用模型${label}:`);
|
|
3568
|
+
console.log(' (接口未提供,视为不限)');
|
|
3569
|
+
console.log();
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
const models = Array.isArray(res.models) ? res.models : [];
|
|
3573
|
+
const label = res.provider ? ` (${res.provider})` : '';
|
|
3574
|
+
console.log(`\n可用模型${label}:`);
|
|
3575
|
+
if (models.length === 0) {
|
|
3576
|
+
console.log(' (空)');
|
|
3577
|
+
} else {
|
|
3578
|
+
models.forEach((m, i) => {
|
|
3579
|
+
console.log(` ${i + 1}. ${m}`);
|
|
3580
|
+
});
|
|
3581
|
+
}
|
|
2389
3582
|
console.log();
|
|
2390
3583
|
}
|
|
2391
3584
|
|
|
@@ -2430,19 +3623,20 @@ function cmdSwitch(providerName, silent = false) {
|
|
|
2430
3623
|
console.log('✓ 当前模型:', targetModel);
|
|
2431
3624
|
console.log();
|
|
2432
3625
|
}
|
|
3626
|
+
recordRecentConfig(providerName, targetModel);
|
|
2433
3627
|
return targetModel;
|
|
2434
3628
|
}
|
|
2435
3629
|
|
|
2436
3630
|
// 切换模型
|
|
2437
3631
|
function cmdUseModel(modelName, silent = false) {
|
|
3632
|
+
if (!modelName) {
|
|
3633
|
+
if (!silent) console.error('错误: 模型名称必填');
|
|
3634
|
+
throw new Error('模型名称必填');
|
|
3635
|
+
}
|
|
2438
3636
|
const models = readModels();
|
|
2439
3637
|
if (!models.includes(modelName)) {
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
console.log('\n可用的模型:');
|
|
2443
|
-
models.forEach(m => console.log(' -', m));
|
|
2444
|
-
}
|
|
2445
|
-
throw new Error('模型不存在');
|
|
3638
|
+
models.push(modelName);
|
|
3639
|
+
writeModels(models);
|
|
2446
3640
|
}
|
|
2447
3641
|
|
|
2448
3642
|
const config = readConfig();
|
|
@@ -2469,6 +3663,7 @@ function cmdUseModel(modelName, silent = false) {
|
|
|
2469
3663
|
console.log('✓ 已切换模型:', modelName);
|
|
2470
3664
|
console.log();
|
|
2471
3665
|
}
|
|
3666
|
+
recordRecentConfig(currentProvider, modelName);
|
|
2472
3667
|
}
|
|
2473
3668
|
|
|
2474
3669
|
// 添加提供商
|
|
@@ -2682,52 +3877,6 @@ function maskKey(key) {
|
|
|
2682
3877
|
return key.substring(0, 4) + '...' + key.substring(key.length - 4);
|
|
2683
3878
|
}
|
|
2684
3879
|
|
|
2685
|
-
// 应用到系统环境变量
|
|
2686
|
-
function applyToSystemEnv(config = {}) {
|
|
2687
|
-
try {
|
|
2688
|
-
const apiKey = config.apiKey || '';
|
|
2689
|
-
|
|
2690
|
-
// Windows 使用 setx 命令设置用户环境变量
|
|
2691
|
-
if (process.platform === 'win32') {
|
|
2692
|
-
const envVars = [
|
|
2693
|
-
['ANTHROPIC_API_KEY', apiKey],
|
|
2694
|
-
['ANTHROPIC_AUTH_TOKEN', apiKey],
|
|
2695
|
-
['ANTHROPIC_BASE_URL', config.baseUrl || 'https://open.bigmodel.cn/api/anthropic'],
|
|
2696
|
-
['CLAUDE_CODE_USE_KEY', '1'],
|
|
2697
|
-
['ANTHROPIC_MODEL', config.model || 'glm-4.7']
|
|
2698
|
-
];
|
|
2699
|
-
|
|
2700
|
-
const errors = [];
|
|
2701
|
-
for (const [key, value] of envVars) {
|
|
2702
|
-
try {
|
|
2703
|
-
// 转义值中的双引号,防止命令注入
|
|
2704
|
-
const safeValue = value.replace(/"/g, '""');
|
|
2705
|
-
execSync(`setx ${key} "${safeValue}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
2706
|
-
} catch (e) {
|
|
2707
|
-
errors.push(`${key}: ${e.message || '设置失败'}`);
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
|
-
|
|
2711
|
-
if (errors.length > 0) {
|
|
2712
|
-
return {
|
|
2713
|
-
success: false,
|
|
2714
|
-
mode: 'env-vars',
|
|
2715
|
-
error: `部分环境变量设置失败:\n${errors.join('\n')}`
|
|
2716
|
-
};
|
|
2717
|
-
}
|
|
2718
|
-
return {
|
|
2719
|
-
success: true,
|
|
2720
|
-
mode: 'env-vars',
|
|
2721
|
-
updatedKeys: envVars.map(([key]) => key)
|
|
2722
|
-
};
|
|
2723
|
-
} else {
|
|
2724
|
-
return { success: false, mode: 'env-vars', error: '仅支持 Windows 系统' };
|
|
2725
|
-
}
|
|
2726
|
-
} catch (e) {
|
|
2727
|
-
return { success: false, mode: 'env-vars', error: e.message };
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
3880
|
// 应用到 Claude Code settings.json(跨平台)
|
|
2732
3881
|
function applyToClaudeSettings(config = {}) {
|
|
2733
3882
|
try {
|
|
@@ -2751,11 +3900,11 @@ function applyToClaudeSettings(config = {}) {
|
|
|
2751
3900
|
const nextEnv = {
|
|
2752
3901
|
...currentEnv,
|
|
2753
3902
|
ANTHROPIC_API_KEY: apiKey,
|
|
2754
|
-
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
2755
3903
|
ANTHROPIC_BASE_URL: baseUrl,
|
|
2756
|
-
ANTHROPIC_MODEL: model
|
|
2757
|
-
CLAUDE_CODE_USE_KEY: '1'
|
|
3904
|
+
ANTHROPIC_MODEL: model
|
|
2758
3905
|
};
|
|
3906
|
+
delete nextEnv.ANTHROPIC_AUTH_TOKEN;
|
|
3907
|
+
delete nextEnv.CLAUDE_CODE_USE_KEY;
|
|
2759
3908
|
|
|
2760
3909
|
const nextSettings = {
|
|
2761
3910
|
...currentSettings,
|
|
@@ -2772,10 +3921,8 @@ function applyToClaudeSettings(config = {}) {
|
|
|
2772
3921
|
targetPath: CLAUDE_SETTINGS_FILE,
|
|
2773
3922
|
updatedKeys: [
|
|
2774
3923
|
'env.ANTHROPIC_API_KEY',
|
|
2775
|
-
'env.ANTHROPIC_AUTH_TOKEN',
|
|
2776
3924
|
'env.ANTHROPIC_BASE_URL',
|
|
2777
|
-
'env.ANTHROPIC_MODEL'
|
|
2778
|
-
'env.CLAUDE_CODE_USE_KEY'
|
|
3925
|
+
'env.ANTHROPIC_MODEL'
|
|
2779
3926
|
]
|
|
2780
3927
|
};
|
|
2781
3928
|
if (backupPath) {
|
|
@@ -3004,7 +4151,31 @@ function cmdStart() {
|
|
|
3004
4151
|
};
|
|
3005
4152
|
break;
|
|
3006
4153
|
case 'models':
|
|
3007
|
-
|
|
4154
|
+
{
|
|
4155
|
+
const providerName = params && typeof params.provider === 'string' ? params.provider : '';
|
|
4156
|
+
const res = await fetchProviderModels(providerName);
|
|
4157
|
+
if (res.error) {
|
|
4158
|
+
result = { error: res.error, models: [], source: 'remote' };
|
|
4159
|
+
} else if (res.unlimited) {
|
|
4160
|
+
result = { models: [], source: 'remote', provider: res.provider || '', unlimited: true };
|
|
4161
|
+
} else {
|
|
4162
|
+
result = { models: res.models || [], source: 'remote', provider: res.provider || '' };
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
break;
|
|
4166
|
+
case 'models-by-url':
|
|
4167
|
+
{
|
|
4168
|
+
const baseUrl = params && typeof params.baseUrl === 'string' ? params.baseUrl : '';
|
|
4169
|
+
const apiKey = params && typeof params.apiKey === 'string' ? params.apiKey : '';
|
|
4170
|
+
const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
|
|
4171
|
+
if (res.error) {
|
|
4172
|
+
result = { error: res.error, models: [], source: 'remote' };
|
|
4173
|
+
} else if (res.unlimited) {
|
|
4174
|
+
result = { models: [], source: 'remote', unlimited: true };
|
|
4175
|
+
} else {
|
|
4176
|
+
result = { models: res.models || [], source: 'remote' };
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
3008
4179
|
break;
|
|
3009
4180
|
case 'get-config-template':
|
|
3010
4181
|
result = getConfigTemplate(params || {});
|
|
@@ -3012,27 +4183,33 @@ function cmdStart() {
|
|
|
3012
4183
|
case 'apply-config-template':
|
|
3013
4184
|
result = applyConfigTemplate(params || {});
|
|
3014
4185
|
break;
|
|
3015
|
-
case 'get-
|
|
3016
|
-
result =
|
|
3017
|
-
break;
|
|
3018
|
-
case '
|
|
3019
|
-
result =
|
|
3020
|
-
break;
|
|
3021
|
-
case 'get-
|
|
3022
|
-
result =
|
|
3023
|
-
break;
|
|
3024
|
-
case 'apply-
|
|
3025
|
-
result =
|
|
3026
|
-
break;
|
|
3027
|
-
case 'get-openclaw-
|
|
3028
|
-
result =
|
|
3029
|
-
break;
|
|
3030
|
-
case 'apply-openclaw-
|
|
3031
|
-
result =
|
|
3032
|
-
break;
|
|
3033
|
-
case '
|
|
3034
|
-
|
|
3035
|
-
|
|
4186
|
+
case 'get-recent-configs':
|
|
4187
|
+
result = { items: readRecentConfigs() };
|
|
4188
|
+
break;
|
|
4189
|
+
case 'config-health-check':
|
|
4190
|
+
result = await buildConfigHealthReport(params || {});
|
|
4191
|
+
break;
|
|
4192
|
+
case 'get-agents-file':
|
|
4193
|
+
result = readAgentsFile(params || {});
|
|
4194
|
+
break;
|
|
4195
|
+
case 'apply-agents-file':
|
|
4196
|
+
result = applyAgentsFile(params || {});
|
|
4197
|
+
break;
|
|
4198
|
+
case 'get-openclaw-config':
|
|
4199
|
+
result = readOpenclawConfigFile();
|
|
4200
|
+
break;
|
|
4201
|
+
case 'apply-openclaw-config':
|
|
4202
|
+
result = applyOpenclawConfig(params || {});
|
|
4203
|
+
break;
|
|
4204
|
+
case 'get-openclaw-agents-file':
|
|
4205
|
+
result = readOpenclawAgentsFile();
|
|
4206
|
+
break;
|
|
4207
|
+
case 'apply-openclaw-agents-file':
|
|
4208
|
+
result = applyOpenclawAgentsFile(params || {});
|
|
4209
|
+
break;
|
|
4210
|
+
case 'switch':
|
|
4211
|
+
case 'use':
|
|
4212
|
+
case 'add':
|
|
3036
4213
|
case 'delete':
|
|
3037
4214
|
case 'update':
|
|
3038
4215
|
result = { error: 'Codex 配置改动已切换为模板确认模式,请使用模板编辑器并手动确认应用。' };
|
|
@@ -3048,9 +4225,6 @@ function cmdStart() {
|
|
|
3048
4225
|
case 'apply-claude-config':
|
|
3049
4226
|
result = applyToClaudeSettings(params.config);
|
|
3050
4227
|
break;
|
|
3051
|
-
case 'apply-env':
|
|
3052
|
-
result = applyToSystemEnv(params.config);
|
|
3053
|
-
break;
|
|
3054
4228
|
case 'export-config':
|
|
3055
4229
|
result = {
|
|
3056
4230
|
data: buildExportPayload(!!params.includeKeys)
|
|
@@ -3102,14 +4276,15 @@ function cmdStart() {
|
|
|
3102
4276
|
}
|
|
3103
4277
|
});
|
|
3104
4278
|
|
|
3105
|
-
|
|
3106
|
-
|
|
4279
|
+
const port = resolveWebPort();
|
|
4280
|
+
server.listen(port, () => {
|
|
4281
|
+
console.log('\n✓ Web UI 已启动: http://localhost:' + port);
|
|
3107
4282
|
console.log(' 按 Ctrl+C 退出\n');
|
|
3108
4283
|
|
|
3109
4284
|
// 打开浏览器
|
|
3110
4285
|
const platform = process.platform;
|
|
3111
4286
|
let command;
|
|
3112
|
-
const url = `http://localhost:${
|
|
4287
|
+
const url = `http://localhost:${port}`;
|
|
3113
4288
|
|
|
3114
4289
|
if (platform === 'win32') {
|
|
3115
4290
|
command = `start "" "${url}"`;
|
|
@@ -3119,16 +4294,19 @@ function cmdStart() {
|
|
|
3119
4294
|
command = `xdg-open "${url}"`;
|
|
3120
4295
|
}
|
|
3121
4296
|
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
4297
|
+
const disableBrowser = process.env.CODEXMATE_NO_BROWSER === '1';
|
|
4298
|
+
if (!disableBrowser) {
|
|
4299
|
+
exec(command, (error) => {
|
|
4300
|
+
if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
|
|
4301
|
+
});
|
|
4302
|
+
}
|
|
3125
4303
|
});
|
|
3126
4304
|
}
|
|
3127
4305
|
|
|
3128
4306
|
// ============================================================================
|
|
3129
4307
|
// 主程序
|
|
3130
4308
|
// ============================================================================
|
|
3131
|
-
function main() {
|
|
4309
|
+
async function main() {
|
|
3132
4310
|
const bootstrap = ensureManagedConfigBootstrap();
|
|
3133
4311
|
if (bootstrap && bootstrap.notice) {
|
|
3134
4312
|
console.log(`\n[Init] ${bootstrap.notice}`);
|
|
@@ -3139,6 +4317,7 @@ function main() {
|
|
|
3139
4317
|
console.log('\nCodex Mate - Codex 提供商管理工具');
|
|
3140
4318
|
console.log('\n用法:');
|
|
3141
4319
|
console.log(' codexmate status 显示当前状态');
|
|
4320
|
+
console.log(' codexmate setup 交互式配置向导');
|
|
3142
4321
|
console.log(' codexmate list 列出所有提供商');
|
|
3143
4322
|
console.log(' codexmate models 列出所有模型');
|
|
3144
4323
|
console.log(' codexmate switch <名称> 切换提供商');
|
|
@@ -3158,8 +4337,9 @@ function main() {
|
|
|
3158
4337
|
|
|
3159
4338
|
switch (command) {
|
|
3160
4339
|
case 'status': cmdStatus(); break;
|
|
4340
|
+
case 'setup': await cmdSetup(); break;
|
|
3161
4341
|
case 'list': cmdList(); break;
|
|
3162
|
-
case 'models': cmdModels(); break;
|
|
4342
|
+
case 'models': await cmdModels(); break;
|
|
3163
4343
|
case 'switch': cmdSwitch(args[1]); break;
|
|
3164
4344
|
case 'use': cmdUseModel(args[1]); break;
|
|
3165
4345
|
case 'add': cmdAdd(args[1], args[2], args[3]); break;
|
|
@@ -3190,4 +4370,7 @@ function main() {
|
|
|
3190
4370
|
}
|
|
3191
4371
|
}
|
|
3192
4372
|
|
|
3193
|
-
main()
|
|
4373
|
+
main().catch((err) => {
|
|
4374
|
+
console.error('错误:', err && err.message ? err.message : err);
|
|
4375
|
+
process.exit(1);
|
|
4376
|
+
});
|