codexmate 0.0.4 → 0.0.6
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/.github/workflows/ci.yml +26 -0
- package/CHANGELOG.md +14 -0
- package/CHANGELOG.zh-CN.md +14 -0
- package/README.md +117 -74
- package/README.zh-CN.md +124 -77
- package/cli.js +2697 -675
- package/package.json +12 -5
- package/tests/e2e/recent-health.e2e.js +136 -0
- package/tests/e2e/run.js +357 -0
- package/web-ui.html +3036 -656
package/cli.js
CHANGED
|
@@ -2,34 +2,39 @@
|
|
|
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');
|
|
8
|
+
const zipLib = require('zip-lib');
|
|
7
9
|
const { exec, execSync } = require('child_process');
|
|
8
10
|
const http = require('http');
|
|
9
11
|
const https = require('https');
|
|
10
12
|
const readline = require('readline');
|
|
11
13
|
|
|
12
|
-
const
|
|
14
|
+
const DEFAULT_WEB_PORT = 3737;
|
|
15
|
+
const DEFAULT_WEB_HOST = '127.0.0.1';
|
|
13
16
|
|
|
14
17
|
// ============================================================================
|
|
15
18
|
// 配置
|
|
16
19
|
// ============================================================================
|
|
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');
|
|
20
|
+
const CONFIG_DIR = path.join(os.homedir(), '.codex');
|
|
21
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml');
|
|
22
|
+
const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
|
|
23
|
+
const MODELS_FILE = path.join(CONFIG_DIR, 'models.json');
|
|
24
|
+
const CURRENT_MODELS_FILE = path.join(CONFIG_DIR, 'provider-current-models.json');
|
|
25
|
+
const INIT_MARK_FILE = path.join(CONFIG_DIR, 'codexmate-init.json');
|
|
26
|
+
const CODEX_SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
|
|
27
|
+
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
28
|
+
const OPENCLAW_CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json');
|
|
29
|
+
const OPENCLAW_WORKSPACE_DIR = path.join(OPENCLAW_DIR, 'workspace');
|
|
30
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
31
|
+
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
32
|
+
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
33
|
+
const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
|
|
30
34
|
|
|
31
35
|
const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gpt-4'];
|
|
32
36
|
const SPEED_TEST_TIMEOUT_MS = 8000;
|
|
37
|
+
const HEALTH_CHECK_TIMEOUT_MS = 6000;
|
|
33
38
|
const MAX_SESSION_LIST_SIZE = 300;
|
|
34
39
|
const MAX_EXPORT_MESSAGES = 1000;
|
|
35
40
|
const DEFAULT_SESSION_DETAIL_MESSAGES = 300;
|
|
@@ -38,13 +43,18 @@ const SESSION_TITLE_READ_BYTES = 64 * 1024;
|
|
|
38
43
|
const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
|
|
39
44
|
const SESSION_LIST_CACHE_TTL_MS = 4000;
|
|
40
45
|
const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
|
|
41
|
-
const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
42
|
-
const DEFAULT_CONTENT_SCAN_LIMIT = 10;
|
|
46
|
+
const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
47
|
+
const DEFAULT_CONTENT_SCAN_LIMIT = 10;
|
|
43
48
|
const SESSION_SCAN_FACTOR = 4;
|
|
44
49
|
const SESSION_SCAN_MIN_FILES = 800;
|
|
45
50
|
const MAX_SESSION_PATH_LIST_SIZE = 2000;
|
|
46
51
|
const AGENTS_FILE_NAME = 'AGENTS.md';
|
|
47
52
|
const UTF8_BOM = '\ufeff';
|
|
53
|
+
const MODELS_CACHE_TTL_MS = 60 * 1000;
|
|
54
|
+
const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
|
|
55
|
+
const MODELS_CACHE_MAX_ENTRIES = 50;
|
|
56
|
+
const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
57
|
+
const MAX_RECENT_CONFIGS = 3;
|
|
48
58
|
const BOOTSTRAP_TEXT_MARKERS = [
|
|
49
59
|
'agents.md instructions',
|
|
50
60
|
'<instructions>',
|
|
@@ -53,6 +63,29 @@ const BOOTSTRAP_TEXT_MARKERS = [
|
|
|
53
63
|
'codex cli'
|
|
54
64
|
];
|
|
55
65
|
|
|
66
|
+
const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
|
|
67
|
+
const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
|
|
68
|
+
|
|
69
|
+
function resolveWebPort() {
|
|
70
|
+
const raw = process.env.CODEXMATE_PORT;
|
|
71
|
+
if (!raw) return DEFAULT_WEB_PORT;
|
|
72
|
+
const parsed = parseInt(raw, 10);
|
|
73
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_WEB_PORT;
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveWebHost(options = {}) {
|
|
78
|
+
const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
|
|
79
|
+
if (optionHost) {
|
|
80
|
+
return optionHost;
|
|
81
|
+
}
|
|
82
|
+
const envHost = typeof process.env.CODEXMATE_HOST === 'string' ? process.env.CODEXMATE_HOST.trim() : '';
|
|
83
|
+
if (envHost) {
|
|
84
|
+
return envHost;
|
|
85
|
+
}
|
|
86
|
+
return DEFAULT_WEB_HOST;
|
|
87
|
+
}
|
|
88
|
+
|
|
56
89
|
const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex"
|
|
57
90
|
model_reasoning_effort = "high"
|
|
58
91
|
disable_response_storage = true
|
|
@@ -75,6 +108,8 @@ stream_idle_timeout_ms = 300000
|
|
|
75
108
|
|
|
76
109
|
let g_initNotice = '';
|
|
77
110
|
let g_sessionListCache = new Map();
|
|
111
|
+
let g_modelsCache = new Map();
|
|
112
|
+
let g_modelsInFlight = new Map();
|
|
78
113
|
|
|
79
114
|
// ============================================================================
|
|
80
115
|
// 工具函数
|
|
@@ -154,12 +189,88 @@ function readJsonFile(filePath, fallback = null) {
|
|
|
154
189
|
}
|
|
155
190
|
}
|
|
156
191
|
|
|
192
|
+
function readJsonArrayFile(filePath, fallback = []) {
|
|
193
|
+
if (!fs.existsSync(filePath)) {
|
|
194
|
+
return Array.isArray(fallback) ? [...fallback] : [];
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
198
|
+
if (!content.trim()) {
|
|
199
|
+
return Array.isArray(fallback) ? [...fallback] : [];
|
|
200
|
+
}
|
|
201
|
+
const parsed = JSON.parse(content);
|
|
202
|
+
return Array.isArray(parsed) ? parsed : (Array.isArray(fallback) ? [...fallback] : []);
|
|
203
|
+
} catch (e) {
|
|
204
|
+
return Array.isArray(fallback) ? [...fallback] : [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
157
208
|
function ensureDir(dirPath) {
|
|
158
209
|
if (!fs.existsSync(dirPath)) {
|
|
159
210
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
160
211
|
}
|
|
161
212
|
}
|
|
162
213
|
|
|
214
|
+
function expandHomePath(value) {
|
|
215
|
+
if (typeof value !== 'string') {
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
const trimmed = value.trim();
|
|
219
|
+
if (!trimmed) {
|
|
220
|
+
return '';
|
|
221
|
+
}
|
|
222
|
+
if (trimmed === '~') {
|
|
223
|
+
return os.homedir();
|
|
224
|
+
}
|
|
225
|
+
if (trimmed.startsWith(`~${path.sep}`) || trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
226
|
+
return path.resolve(os.homedir(), trimmed.slice(2));
|
|
227
|
+
}
|
|
228
|
+
return trimmed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function resolveExistingDir(candidates = [], fallback = '') {
|
|
232
|
+
for (const raw of candidates) {
|
|
233
|
+
const candidate = expandHomePath(raw);
|
|
234
|
+
if (!candidate) continue;
|
|
235
|
+
try {
|
|
236
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
237
|
+
return candidate;
|
|
238
|
+
}
|
|
239
|
+
} catch (e) {}
|
|
240
|
+
}
|
|
241
|
+
return fallback;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getCodexSessionsDir() {
|
|
245
|
+
const candidates = [];
|
|
246
|
+
const envCodexHome = process.env.CODEX_HOME;
|
|
247
|
+
if (envCodexHome) {
|
|
248
|
+
candidates.push(path.join(envCodexHome, 'sessions'));
|
|
249
|
+
}
|
|
250
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
251
|
+
if (xdgConfig) {
|
|
252
|
+
candidates.push(path.join(xdgConfig, 'codex', 'sessions'));
|
|
253
|
+
}
|
|
254
|
+
candidates.push(path.join(os.homedir(), '.config', 'codex', 'sessions'));
|
|
255
|
+
candidates.push(CODEX_SESSIONS_DIR);
|
|
256
|
+
return resolveExistingDir(candidates, CODEX_SESSIONS_DIR);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getClaudeProjectsDir() {
|
|
260
|
+
const candidates = [];
|
|
261
|
+
const envClaudeHome = process.env.CLAUDE_HOME || process.env.CLAUDE_CONFIG_DIR;
|
|
262
|
+
if (envClaudeHome) {
|
|
263
|
+
candidates.push(path.join(envClaudeHome, 'projects'));
|
|
264
|
+
}
|
|
265
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME;
|
|
266
|
+
if (xdgConfig) {
|
|
267
|
+
candidates.push(path.join(xdgConfig, 'claude', 'projects'));
|
|
268
|
+
}
|
|
269
|
+
candidates.push(path.join(os.homedir(), '.config', 'claude', 'projects'));
|
|
270
|
+
candidates.push(CLAUDE_PROJECTS_DIR);
|
|
271
|
+
return resolveExistingDir(candidates, CLAUDE_PROJECTS_DIR);
|
|
272
|
+
}
|
|
273
|
+
|
|
163
274
|
function hasUtf8Bom(text) {
|
|
164
275
|
return typeof text === 'string' && text.charCodeAt(0) === 0xfeff;
|
|
165
276
|
}
|
|
@@ -183,6 +294,239 @@ function normalizeLineEnding(text, lineEnding) {
|
|
|
183
294
|
return lineEnding === '\r\n' ? normalized.replace(/\n/g, '\r\n') : normalized;
|
|
184
295
|
}
|
|
185
296
|
|
|
297
|
+
function isValidProviderName(name) {
|
|
298
|
+
return typeof name === 'string' && /^[a-zA-Z0-9._-]+$/.test(name.trim());
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function buildModelsCandidates(baseUrl) {
|
|
302
|
+
const trimmed = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
|
303
|
+
if (!trimmed) return [];
|
|
304
|
+
if (/\/models\/?$/.test(trimmed)) {
|
|
305
|
+
return [trimmed];
|
|
306
|
+
}
|
|
307
|
+
const normalized = trimmed.replace(/\/+$/, '');
|
|
308
|
+
const candidates = [];
|
|
309
|
+
const pushUnique = (url) => {
|
|
310
|
+
if (url && !candidates.includes(url)) {
|
|
311
|
+
candidates.push(url);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
if (/\/v1$/i.test(normalized)) {
|
|
316
|
+
pushUnique(normalized + '/models');
|
|
317
|
+
} else {
|
|
318
|
+
pushUnique(normalized + '/v1/models');
|
|
319
|
+
pushUnique(normalized + '/models');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
pushUnique(trimmed);
|
|
323
|
+
return candidates;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function extractModelNames(payload) {
|
|
327
|
+
if (!payload || typeof payload !== 'object') return [];
|
|
328
|
+
const data = Array.isArray(payload.data)
|
|
329
|
+
? payload.data
|
|
330
|
+
: (Array.isArray(payload.models) ? payload.models : []);
|
|
331
|
+
const names = [];
|
|
332
|
+
for (const item of data) {
|
|
333
|
+
if (typeof item === 'string') {
|
|
334
|
+
if (item.trim()) names.push(item.trim());
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (!item || typeof item !== 'object') continue;
|
|
338
|
+
const name = item.id || item.name || item.model || '';
|
|
339
|
+
if (typeof name === 'string' && name.trim()) {
|
|
340
|
+
names.push(name.trim());
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return Array.from(new Set(names));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function hasModelsListPayload(payload) {
|
|
347
|
+
if (!payload || typeof payload !== 'object') return false;
|
|
348
|
+
return Array.isArray(payload.data) || Array.isArray(payload.models);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function hashModelsCacheValue(value) {
|
|
352
|
+
if (!value) return '';
|
|
353
|
+
try {
|
|
354
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
355
|
+
} catch (e) {
|
|
356
|
+
return '';
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildModelsCacheKey(baseUrl, apiKey) {
|
|
361
|
+
const normalizedUrl = typeof baseUrl === 'string'
|
|
362
|
+
? baseUrl.trim().replace(/\/+$/, '')
|
|
363
|
+
: '';
|
|
364
|
+
const apiKeyHash = hashModelsCacheValue(apiKey);
|
|
365
|
+
return `${normalizedUrl}|${apiKeyHash}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function readModelsCacheEntry(cacheKey) {
|
|
369
|
+
if (!cacheKey) return null;
|
|
370
|
+
const entry = g_modelsCache.get(cacheKey);
|
|
371
|
+
if (!entry) return null;
|
|
372
|
+
if (Date.now() >= entry.expiresAt) {
|
|
373
|
+
g_modelsCache.delete(cacheKey);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
g_modelsCache.delete(cacheKey);
|
|
377
|
+
g_modelsCache.set(cacheKey, entry);
|
|
378
|
+
return entry.result || null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function writeModelsCacheEntry(cacheKey, result) {
|
|
382
|
+
if (!cacheKey) return;
|
|
383
|
+
const isNegative = !!(result && (result.error || result.unlimited));
|
|
384
|
+
const ttl = isNegative ? MODELS_NEGATIVE_CACHE_TTL_MS : MODELS_CACHE_TTL_MS;
|
|
385
|
+
const entry = {
|
|
386
|
+
result,
|
|
387
|
+
expiresAt: Date.now() + ttl
|
|
388
|
+
};
|
|
389
|
+
if (g_modelsCache.has(cacheKey)) {
|
|
390
|
+
g_modelsCache.delete(cacheKey);
|
|
391
|
+
}
|
|
392
|
+
g_modelsCache.set(cacheKey, entry);
|
|
393
|
+
while (g_modelsCache.size > MODELS_CACHE_MAX_ENTRIES) {
|
|
394
|
+
const oldestKey = g_modelsCache.keys().next().value;
|
|
395
|
+
g_modelsCache.delete(oldestKey);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function fetchModelsFromBaseUrl(baseUrl, apiKey) {
|
|
400
|
+
const cacheKey = buildModelsCacheKey(baseUrl, apiKey);
|
|
401
|
+
const cached = readModelsCacheEntry(cacheKey);
|
|
402
|
+
if (cached) return cached;
|
|
403
|
+
|
|
404
|
+
const inFlight = g_modelsInFlight.get(cacheKey);
|
|
405
|
+
if (inFlight) return inFlight;
|
|
406
|
+
|
|
407
|
+
const promise = (async () => {
|
|
408
|
+
const result = await fetchModelsFromBaseUrlCore(baseUrl, apiKey);
|
|
409
|
+
writeModelsCacheEntry(cacheKey, result);
|
|
410
|
+
return result;
|
|
411
|
+
})();
|
|
412
|
+
|
|
413
|
+
g_modelsInFlight.set(cacheKey, promise);
|
|
414
|
+
promise.finally(() => {
|
|
415
|
+
g_modelsInFlight.delete(cacheKey);
|
|
416
|
+
});
|
|
417
|
+
return promise;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function fetchModelsFromBaseUrlCore(baseUrl, apiKey) {
|
|
421
|
+
const candidates = buildModelsCandidates(baseUrl);
|
|
422
|
+
if (candidates.length === 0) return { error: 'Provider missing URL' };
|
|
423
|
+
|
|
424
|
+
let lastError = '';
|
|
425
|
+
for (const modelsUrl of candidates) {
|
|
426
|
+
let parsed;
|
|
427
|
+
try {
|
|
428
|
+
parsed = new URL(modelsUrl);
|
|
429
|
+
} catch (e) {
|
|
430
|
+
lastError = 'Invalid URL';
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
435
|
+
const agent = parsed.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT;
|
|
436
|
+
const headers = {
|
|
437
|
+
'User-Agent': 'codexmate-models',
|
|
438
|
+
'Accept': 'application/json'
|
|
439
|
+
};
|
|
440
|
+
if (apiKey) {
|
|
441
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const result = await new Promise((innerResolve) => {
|
|
445
|
+
let settled = false;
|
|
446
|
+
const finish = (payload) => {
|
|
447
|
+
if (settled) return;
|
|
448
|
+
settled = true;
|
|
449
|
+
innerResolve(payload);
|
|
450
|
+
};
|
|
451
|
+
const req = transport.request(parsed, { method: 'GET', headers, agent }, (res) => {
|
|
452
|
+
const status = res.statusCode || 0;
|
|
453
|
+
const contentType = String(res.headers['content-type'] || '').toLowerCase();
|
|
454
|
+
if (status === 404 || status === 405 || status === 501) {
|
|
455
|
+
res.resume();
|
|
456
|
+
return finish({ unavailable: true });
|
|
457
|
+
}
|
|
458
|
+
let body = '';
|
|
459
|
+
let receivedBytes = 0;
|
|
460
|
+
res.on('data', chunk => {
|
|
461
|
+
receivedBytes += chunk.length || 0;
|
|
462
|
+
if (receivedBytes > MODELS_RESPONSE_MAX_BYTES) {
|
|
463
|
+
res.destroy();
|
|
464
|
+
return finish({ unavailable: true });
|
|
465
|
+
}
|
|
466
|
+
body += chunk;
|
|
467
|
+
});
|
|
468
|
+
res.on('end', () => {
|
|
469
|
+
if (settled) return;
|
|
470
|
+
if (status >= 400) {
|
|
471
|
+
return finish({ error: `Request failed: ${status}` });
|
|
472
|
+
}
|
|
473
|
+
if (contentType && !contentType.includes('application/json')) {
|
|
474
|
+
return finish({ unavailable: true });
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
const payload = JSON.parse(body || '{}');
|
|
478
|
+
if (!hasModelsListPayload(payload)) {
|
|
479
|
+
return finish({ unavailable: true });
|
|
480
|
+
}
|
|
481
|
+
const models = extractModelNames(payload);
|
|
482
|
+
return finish({ models });
|
|
483
|
+
} catch (e) {
|
|
484
|
+
return finish({ unavailable: true });
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
req.setTimeout(SPEED_TEST_TIMEOUT_MS, () => {
|
|
490
|
+
req.destroy(new Error('timeout'));
|
|
491
|
+
});
|
|
492
|
+
req.on('error', (err) => {
|
|
493
|
+
finish({ error: err.message || 'Request failed' });
|
|
494
|
+
});
|
|
495
|
+
req.end();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
if (result && Array.isArray(result.models)) {
|
|
499
|
+
return { models: result.models };
|
|
500
|
+
}
|
|
501
|
+
if (result && result.error) {
|
|
502
|
+
lastError = result.error;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (lastError) {
|
|
508
|
+
return { error: lastError };
|
|
509
|
+
}
|
|
510
|
+
return { unlimited: true };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function fetchProviderModels(providerName, overrides = {}) {
|
|
514
|
+
const { config } = readConfigOrVirtualDefault();
|
|
515
|
+
const targetProvider = providerName || config.model_provider || '';
|
|
516
|
+
if (!targetProvider) return { error: '未设置当前提供商' };
|
|
517
|
+
|
|
518
|
+
const providers = config.model_providers || {};
|
|
519
|
+
const provider = providers[targetProvider];
|
|
520
|
+
if (!provider) return { error: `提供商不存在: ${targetProvider}` };
|
|
521
|
+
|
|
522
|
+
const baseUrl = overrides.baseUrl || provider.base_url || '';
|
|
523
|
+
const apiKey = overrides.apiKey ?? provider.preferred_auth_method ?? '';
|
|
524
|
+
const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
|
|
525
|
+
if (res.unlimited) return { models: [], provider: targetProvider, unlimited: true };
|
|
526
|
+
if (res.error) return { error: res.error };
|
|
527
|
+
return { models: res.models || [], provider: targetProvider, unlimited: false };
|
|
528
|
+
}
|
|
529
|
+
|
|
186
530
|
function resolveAgentsFilePath(params = {}) {
|
|
187
531
|
const baseDir = typeof params.baseDir === 'string' && params.baseDir.trim()
|
|
188
532
|
? params.baseDir.trim()
|
|
@@ -232,11 +576,11 @@ function readAgentsFile(params = {}) {
|
|
|
232
576
|
}
|
|
233
577
|
}
|
|
234
578
|
|
|
235
|
-
function applyAgentsFile(params = {}) {
|
|
236
|
-
const filePath = resolveAgentsFilePath(params);
|
|
237
|
-
const dirCheck = validateAgentsBaseDir(filePath);
|
|
238
|
-
if (dirCheck.error) {
|
|
239
|
-
return { error: dirCheck.error };
|
|
579
|
+
function applyAgentsFile(params = {}) {
|
|
580
|
+
const filePath = resolveAgentsFilePath(params);
|
|
581
|
+
const dirCheck = validateAgentsBaseDir(filePath);
|
|
582
|
+
if (dirCheck.error) {
|
|
583
|
+
return { error: dirCheck.error };
|
|
240
584
|
}
|
|
241
585
|
|
|
242
586
|
const content = typeof params.content === 'string' ? params.content : '';
|
|
@@ -247,241 +591,971 @@ function applyAgentsFile(params = {}) {
|
|
|
247
591
|
try {
|
|
248
592
|
fs.writeFileSync(filePath, finalContent, 'utf-8');
|
|
249
593
|
return { success: true, path: filePath };
|
|
250
|
-
} catch (e) {
|
|
251
|
-
return { error: `写入 AGENTS.md 失败: ${e.message}` };
|
|
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
|
-
}
|
|
594
|
+
} catch (e) {
|
|
595
|
+
return { error: `写入 AGENTS.md 失败: ${e.message}` };
|
|
596
|
+
}
|
|
597
|
+
}
|
|
409
598
|
|
|
410
|
-
function
|
|
599
|
+
function resolveHomePath(input) {
|
|
600
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
601
|
+
if (!raw) return '';
|
|
602
|
+
if (raw === '~') return os.homedir();
|
|
603
|
+
if (raw.startsWith('~/') || raw.startsWith('~\\')) {
|
|
604
|
+
return path.join(os.homedir(), raw.slice(2));
|
|
605
|
+
}
|
|
606
|
+
return raw;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function resolveOpenclawWorkspaceDir(config) {
|
|
610
|
+
const workspace = config
|
|
611
|
+
&& config.agents
|
|
612
|
+
&& config.agents.defaults
|
|
613
|
+
&& typeof config.agents.defaults.workspace === 'string'
|
|
614
|
+
? config.agents.defaults.workspace
|
|
615
|
+
: '';
|
|
616
|
+
const resolved = resolveHomePath(workspace);
|
|
617
|
+
if (!resolved) {
|
|
618
|
+
return OPENCLAW_WORKSPACE_DIR;
|
|
619
|
+
}
|
|
620
|
+
if (path.isAbsolute(resolved)) {
|
|
621
|
+
return resolved;
|
|
622
|
+
}
|
|
623
|
+
return path.join(OPENCLAW_DIR, resolved);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function normalizeOpenclawWorkspaceFileName(input) {
|
|
627
|
+
const raw = typeof input === 'string' ? input.trim() : '';
|
|
628
|
+
if (!raw) {
|
|
629
|
+
return { error: '文件名不能为空' };
|
|
630
|
+
}
|
|
631
|
+
if (raw.includes('\0')) {
|
|
632
|
+
return { error: '文件名非法' };
|
|
633
|
+
}
|
|
634
|
+
if (raw.includes('/') || raw.includes('\\') || raw.includes('..')) {
|
|
635
|
+
return { error: '文件名非法' };
|
|
636
|
+
}
|
|
637
|
+
const baseName = path.basename(raw);
|
|
638
|
+
if (baseName !== raw) {
|
|
639
|
+
return { error: '文件名非法' };
|
|
640
|
+
}
|
|
641
|
+
if (!raw.toLowerCase().endsWith('.md')) {
|
|
642
|
+
return { error: '仅支持 .md 文件' };
|
|
643
|
+
}
|
|
644
|
+
return { ok: true, name: raw };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function readOpenclawConfigFile() {
|
|
648
|
+
const filePath = OPENCLAW_CONFIG_FILE;
|
|
411
649
|
if (!fs.existsSync(filePath)) {
|
|
412
|
-
return {
|
|
650
|
+
return {
|
|
651
|
+
exists: false,
|
|
652
|
+
path: filePath,
|
|
653
|
+
content: '',
|
|
654
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n'
|
|
655
|
+
};
|
|
413
656
|
}
|
|
414
657
|
|
|
415
658
|
try {
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
659
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
660
|
+
return {
|
|
661
|
+
exists: true,
|
|
662
|
+
path: filePath,
|
|
663
|
+
content: stripUtf8Bom(raw),
|
|
664
|
+
lineEnding: detectLineEnding(raw)
|
|
665
|
+
};
|
|
666
|
+
} catch (e) {
|
|
667
|
+
return { error: `读取 OpenClaw 配置失败: ${e.message}` };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
420
670
|
|
|
421
|
-
|
|
671
|
+
function parseOpenclawConfigText(content) {
|
|
672
|
+
const raw = stripUtf8Bom(typeof content === 'string' ? content : '');
|
|
673
|
+
if (!raw.trim()) {
|
|
674
|
+
return { ok: false, error: 'OpenClaw 配置内容不能为空' };
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
const parsed = JSON5.parse(raw);
|
|
422
678
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
423
|
-
return {
|
|
424
|
-
ok: false,
|
|
425
|
-
exists: true,
|
|
426
|
-
error: `配置文件格式错误(根节点必须是对象): ${filePath}`
|
|
427
|
-
};
|
|
679
|
+
return { ok: false, error: '配置格式错误(根节点必须是对象)' };
|
|
428
680
|
}
|
|
429
|
-
return { ok: true,
|
|
681
|
+
return { ok: true, data: parsed };
|
|
430
682
|
} catch (e) {
|
|
683
|
+
return { ok: false, error: `配置解析失败: ${e.message}` };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function getOpenclawWorkspaceInfo() {
|
|
688
|
+
const readResult = readOpenclawConfigFile();
|
|
689
|
+
let workspaceDir = OPENCLAW_WORKSPACE_DIR;
|
|
690
|
+
let configError = readResult.error || '';
|
|
691
|
+
if (!configError && readResult.exists && readResult.content.trim()) {
|
|
692
|
+
const parsed = parseOpenclawConfigText(readResult.content);
|
|
693
|
+
if (parsed.ok) {
|
|
694
|
+
workspaceDir = resolveOpenclawWorkspaceDir(parsed.data);
|
|
695
|
+
} else {
|
|
696
|
+
configError = parsed.error || '';
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
workspaceDir,
|
|
701
|
+
configError,
|
|
702
|
+
configPath: readResult.path || OPENCLAW_CONFIG_FILE
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function readOpenclawAgentsFile() {
|
|
707
|
+
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
708
|
+
const baseDir = workspaceInfo.workspaceDir;
|
|
709
|
+
const filePath = path.join(baseDir, AGENTS_FILE_NAME);
|
|
710
|
+
|
|
711
|
+
if (!fs.existsSync(baseDir)) {
|
|
431
712
|
return {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
713
|
+
exists: false,
|
|
714
|
+
path: filePath,
|
|
715
|
+
content: '',
|
|
716
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
|
|
717
|
+
workspaceDir: baseDir,
|
|
718
|
+
configError: workspaceInfo.configError,
|
|
719
|
+
baseDirMissing: true
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const readResult = readAgentsFile({ baseDir });
|
|
724
|
+
return {
|
|
725
|
+
...readResult,
|
|
726
|
+
workspaceDir: baseDir,
|
|
727
|
+
configError: workspaceInfo.configError
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function applyOpenclawAgentsFile(params = {}) {
|
|
732
|
+
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
733
|
+
const baseDir = workspaceInfo.workspaceDir;
|
|
734
|
+
ensureDir(baseDir);
|
|
735
|
+
const result = applyAgentsFile({
|
|
736
|
+
...params,
|
|
737
|
+
baseDir
|
|
738
|
+
});
|
|
739
|
+
return {
|
|
740
|
+
...result,
|
|
741
|
+
workspaceDir: baseDir,
|
|
742
|
+
configError: workspaceInfo.configError
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function readOpenclawWorkspaceFile(params = {}) {
|
|
747
|
+
const nameResult = normalizeOpenclawWorkspaceFileName(params.fileName);
|
|
748
|
+
if (nameResult.error) {
|
|
749
|
+
return { error: nameResult.error };
|
|
750
|
+
}
|
|
751
|
+
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
752
|
+
const baseDir = workspaceInfo.workspaceDir;
|
|
753
|
+
const filePath = path.join(baseDir, nameResult.name);
|
|
754
|
+
|
|
755
|
+
if (!fs.existsSync(baseDir)) {
|
|
756
|
+
return {
|
|
757
|
+
exists: false,
|
|
758
|
+
path: filePath,
|
|
759
|
+
content: '',
|
|
760
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
|
|
761
|
+
workspaceDir: baseDir,
|
|
762
|
+
configError: workspaceInfo.configError,
|
|
763
|
+
baseDirMissing: true
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (!fs.existsSync(filePath)) {
|
|
768
|
+
return {
|
|
769
|
+
exists: false,
|
|
770
|
+
path: filePath,
|
|
771
|
+
content: '',
|
|
772
|
+
lineEnding: os.EOL === '\r\n' ? '\r\n' : '\n',
|
|
773
|
+
workspaceDir: baseDir,
|
|
774
|
+
configError: workspaceInfo.configError
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
780
|
+
return {
|
|
781
|
+
exists: true,
|
|
782
|
+
path: filePath,
|
|
783
|
+
content: stripUtf8Bom(raw),
|
|
784
|
+
lineEnding: detectLineEnding(raw),
|
|
785
|
+
workspaceDir: baseDir,
|
|
786
|
+
configError: workspaceInfo.configError
|
|
787
|
+
};
|
|
788
|
+
} catch (e) {
|
|
789
|
+
return { error: `读取 OpenClaw 工作区文件失败: ${e.message}` };
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function applyOpenclawWorkspaceFile(params = {}) {
|
|
794
|
+
const nameResult = normalizeOpenclawWorkspaceFileName(params.fileName);
|
|
795
|
+
if (nameResult.error) {
|
|
796
|
+
return { error: nameResult.error };
|
|
797
|
+
}
|
|
798
|
+
const workspaceInfo = getOpenclawWorkspaceInfo();
|
|
799
|
+
const baseDir = workspaceInfo.workspaceDir;
|
|
800
|
+
ensureDir(baseDir);
|
|
801
|
+
|
|
802
|
+
const content = typeof params.content === 'string' ? params.content : '';
|
|
803
|
+
const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
|
|
804
|
+
const normalized = normalizeLineEnding(content, lineEnding);
|
|
805
|
+
const finalContent = ensureUtf8Bom(normalized);
|
|
806
|
+
const filePath = path.join(baseDir, nameResult.name);
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
fs.writeFileSync(filePath, finalContent, 'utf-8');
|
|
810
|
+
return {
|
|
811
|
+
success: true,
|
|
812
|
+
path: filePath,
|
|
813
|
+
workspaceDir: baseDir,
|
|
814
|
+
configError: workspaceInfo.configError
|
|
815
|
+
};
|
|
816
|
+
} catch (e) {
|
|
817
|
+
return { error: `写入 OpenClaw 工作区文件失败: ${e.message}` };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function applyOpenclawConfig(params = {}) {
|
|
822
|
+
const content = typeof params.content === 'string' ? params.content : '';
|
|
823
|
+
const lineEnding = params.lineEnding === '\r\n' ? '\r\n' : '\n';
|
|
824
|
+
const normalized = normalizeLineEnding(content, lineEnding);
|
|
825
|
+
const parsed = parseOpenclawConfigText(normalized);
|
|
826
|
+
if (!parsed.ok) {
|
|
827
|
+
return { success: false, error: parsed.error };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
ensureDir(OPENCLAW_DIR);
|
|
832
|
+
const backupPath = backupFileIfNeededOnce(OPENCLAW_CONFIG_FILE);
|
|
833
|
+
fs.writeFileSync(OPENCLAW_CONFIG_FILE, normalized, 'utf-8');
|
|
834
|
+
const result = {
|
|
835
|
+
success: true,
|
|
836
|
+
targetPath: OPENCLAW_CONFIG_FILE
|
|
837
|
+
};
|
|
838
|
+
if (backupPath) {
|
|
839
|
+
result.backupPath = backupPath;
|
|
840
|
+
}
|
|
841
|
+
return result;
|
|
842
|
+
} catch (e) {
|
|
843
|
+
return {
|
|
844
|
+
success: false,
|
|
845
|
+
error: e.message || '写入 OpenClaw 配置失败'
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function readJsonObjectFromFile(filePath, fallback = {}) {
|
|
851
|
+
if (!fs.existsSync(filePath)) {
|
|
852
|
+
return { ok: true, exists: false, data: { ...fallback } };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
try {
|
|
856
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
857
|
+
if (!content.trim()) {
|
|
858
|
+
return { ok: true, exists: true, data: { ...fallback } };
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const parsed = JSON.parse(content);
|
|
862
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
863
|
+
return {
|
|
864
|
+
ok: false,
|
|
865
|
+
exists: true,
|
|
866
|
+
error: `配置文件格式错误(根节点必须是对象): ${filePath}`
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
return { ok: true, exists: true, data: parsed };
|
|
870
|
+
} catch (e) {
|
|
871
|
+
return {
|
|
872
|
+
ok: false,
|
|
873
|
+
exists: true,
|
|
874
|
+
error: `配置文件解析失败: ${e.message}`
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function backupFileIfNeededOnce(filePath, backupPrefix = 'codexmate-backup') {
|
|
880
|
+
if (!fs.existsSync(filePath)) {
|
|
881
|
+
return '';
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const dirPath = path.dirname(filePath);
|
|
885
|
+
const baseName = path.basename(filePath);
|
|
886
|
+
const existingPrefix = `${baseName}.${backupPrefix}-`;
|
|
887
|
+
const hasBackup = fs.readdirSync(dirPath).some(fileName =>
|
|
888
|
+
fileName.startsWith(existingPrefix) && fileName.endsWith('.bak')
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
if (hasBackup) {
|
|
892
|
+
return '';
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const backupPath = path.join(dirPath, `${existingPrefix}${formatTimestampForFileName()}.bak`);
|
|
896
|
+
fs.copyFileSync(filePath, backupPath);
|
|
897
|
+
return backupPath;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function writeJsonAtomic(filePath, data) {
|
|
901
|
+
const dirPath = path.dirname(filePath);
|
|
902
|
+
ensureDir(dirPath);
|
|
903
|
+
|
|
904
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
905
|
+
const content = `${JSON.stringify(data, null, 2)}\n`;
|
|
906
|
+
|
|
907
|
+
try {
|
|
908
|
+
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
909
|
+
try {
|
|
910
|
+
fs.renameSync(tmpPath, filePath);
|
|
911
|
+
} catch (renameError) {
|
|
912
|
+
if (process.platform === 'win32') {
|
|
913
|
+
fs.copyFileSync(tmpPath, filePath);
|
|
914
|
+
fs.unlinkSync(tmpPath);
|
|
915
|
+
} else {
|
|
916
|
+
throw renameError;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
} catch (e) {
|
|
920
|
+
if (fs.existsSync(tmpPath)) {
|
|
921
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
922
|
+
}
|
|
923
|
+
throw new Error(`写入 JSON 文件失败: ${e.message}`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function normalizeRecentConfigs(items) {
|
|
928
|
+
if (!Array.isArray(items)) return [];
|
|
929
|
+
const output = [];
|
|
930
|
+
const seen = new Set();
|
|
931
|
+
for (const item of items) {
|
|
932
|
+
if (!item || typeof item !== 'object') continue;
|
|
933
|
+
const provider = typeof item.provider === 'string' ? item.provider.trim() : '';
|
|
934
|
+
const model = typeof item.model === 'string' ? item.model.trim() : '';
|
|
935
|
+
if (!provider || !model) continue;
|
|
936
|
+
const key = `${provider}::${model}`;
|
|
937
|
+
if (seen.has(key)) continue;
|
|
938
|
+
seen.add(key);
|
|
939
|
+
output.push({
|
|
940
|
+
provider,
|
|
941
|
+
model,
|
|
942
|
+
usedAt: typeof item.usedAt === 'string' ? item.usedAt : ''
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
return output;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function readRecentConfigs() {
|
|
949
|
+
return normalizeRecentConfigs(readJsonArrayFile(RECENT_CONFIGS_FILE, []));
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function writeRecentConfigs(items) {
|
|
953
|
+
writeJsonAtomic(RECENT_CONFIGS_FILE, items);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
function recordRecentConfig(provider, model) {
|
|
957
|
+
const providerName = typeof provider === 'string' ? provider.trim() : '';
|
|
958
|
+
const modelName = typeof model === 'string' ? model.trim() : '';
|
|
959
|
+
if (!providerName || !modelName) return;
|
|
960
|
+
|
|
961
|
+
const now = new Date().toISOString();
|
|
962
|
+
const current = readRecentConfigs();
|
|
963
|
+
const next = [{
|
|
964
|
+
provider: providerName,
|
|
965
|
+
model: modelName,
|
|
966
|
+
usedAt: now
|
|
967
|
+
}];
|
|
968
|
+
|
|
969
|
+
for (const item of current) {
|
|
970
|
+
if (item.provider === providerName && item.model === modelName) continue;
|
|
971
|
+
next.push(item);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const trimmed = next.slice(0, MAX_RECENT_CONFIGS);
|
|
975
|
+
writeRecentConfigs(trimmed);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function isValidHttpUrl(value) {
|
|
979
|
+
if (typeof value !== 'string' || !value.trim()) return false;
|
|
980
|
+
try {
|
|
981
|
+
const parsed = new URL(value);
|
|
982
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
983
|
+
} catch (e) {
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function normalizeBaseUrl(value) {
|
|
989
|
+
if (typeof value !== 'string') return '';
|
|
990
|
+
return value.trim().replace(/\/+$/g, '');
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function joinApiUrl(baseUrl, pathSuffix) {
|
|
994
|
+
const trimmed = normalizeBaseUrl(baseUrl);
|
|
995
|
+
if (!trimmed) return '';
|
|
996
|
+
const safeSuffix = String(pathSuffix || '').replace(/^\/+/g, '');
|
|
997
|
+
if (!safeSuffix) return trimmed;
|
|
998
|
+
if (/\/v1$/i.test(trimmed)) {
|
|
999
|
+
return `${trimmed}/${safeSuffix}`;
|
|
1000
|
+
}
|
|
1001
|
+
return `${trimmed}/v1/${safeSuffix}`;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function buildModelsProbeUrl(baseUrl) {
|
|
1005
|
+
return joinApiUrl(baseUrl, 'models');
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function normalizeWireApi(value) {
|
|
1009
|
+
const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1010
|
+
if (!raw) return 'responses';
|
|
1011
|
+
return raw.replace(/[\s-]/g, '_');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function buildModelProbeSpec(provider, modelName, baseUrl) {
|
|
1015
|
+
const model = typeof modelName === 'string' ? modelName.trim() : '';
|
|
1016
|
+
if (!model) return null;
|
|
1017
|
+
|
|
1018
|
+
const wireApi = normalizeWireApi(provider && provider.wire_api);
|
|
1019
|
+
if (wireApi === 'chat_completions' || wireApi === 'chat') {
|
|
1020
|
+
return {
|
|
1021
|
+
url: joinApiUrl(baseUrl, 'chat/completions'),
|
|
1022
|
+
body: {
|
|
1023
|
+
model,
|
|
1024
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
1025
|
+
max_tokens: 1,
|
|
1026
|
+
temperature: 0
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (wireApi === 'completions') {
|
|
1032
|
+
return {
|
|
1033
|
+
url: joinApiUrl(baseUrl, 'completions'),
|
|
1034
|
+
body: {
|
|
1035
|
+
model,
|
|
1036
|
+
prompt: 'ping',
|
|
1037
|
+
max_tokens: 1,
|
|
1038
|
+
temperature: 0
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
return {
|
|
1044
|
+
url: joinApiUrl(baseUrl, 'responses'),
|
|
1045
|
+
body: {
|
|
1046
|
+
model,
|
|
1047
|
+
input: 'ping',
|
|
1048
|
+
max_output_tokens: 1
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function probeUrl(targetUrl, options = {}) {
|
|
1054
|
+
return new Promise((resolve) => {
|
|
1055
|
+
let parsed;
|
|
1056
|
+
try {
|
|
1057
|
+
parsed = new URL(targetUrl);
|
|
1058
|
+
} catch (e) {
|
|
1059
|
+
return resolve({ ok: false, error: 'Invalid URL' });
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
1063
|
+
const headers = {
|
|
1064
|
+
'User-Agent': 'codexmate-health-check',
|
|
1065
|
+
'Accept': 'application/json'
|
|
1066
|
+
};
|
|
1067
|
+
if (options.apiKey) {
|
|
1068
|
+
headers['Authorization'] = `Bearer ${options.apiKey}`;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
1072
|
+
const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
|
|
1073
|
+
const start = Date.now();
|
|
1074
|
+
const req = transport.request(parsed, { method: 'GET', headers }, (res) => {
|
|
1075
|
+
const chunks = [];
|
|
1076
|
+
let size = 0;
|
|
1077
|
+
res.on('data', (chunk) => {
|
|
1078
|
+
if (!chunk) return;
|
|
1079
|
+
size += chunk.length;
|
|
1080
|
+
if (size <= maxBytes) {
|
|
1081
|
+
chunks.push(chunk);
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
res.on('end', () => {
|
|
1085
|
+
const body = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
1086
|
+
resolve({
|
|
1087
|
+
ok: true,
|
|
1088
|
+
status: res.statusCode || 0,
|
|
1089
|
+
durationMs: Date.now() - start,
|
|
1090
|
+
body
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
req.setTimeout(timeoutMs, () => {
|
|
1096
|
+
req.destroy(new Error('timeout'));
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
req.on('error', (err) => {
|
|
1100
|
+
resolve({
|
|
1101
|
+
ok: false,
|
|
1102
|
+
error: err.message || 'request failed',
|
|
1103
|
+
durationMs: Date.now() - start
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
req.end();
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function probeJsonPost(targetUrl, body, options = {}) {
|
|
1112
|
+
return new Promise((resolve) => {
|
|
1113
|
+
let parsed;
|
|
1114
|
+
try {
|
|
1115
|
+
parsed = new URL(targetUrl);
|
|
1116
|
+
} catch (e) {
|
|
1117
|
+
return resolve({ ok: false, error: 'Invalid URL' });
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
1121
|
+
const headers = {
|
|
1122
|
+
'User-Agent': 'codexmate-health-check',
|
|
1123
|
+
'Accept': 'application/json',
|
|
1124
|
+
'Content-Type': 'application/json'
|
|
1125
|
+
};
|
|
1126
|
+
if (options.apiKey) {
|
|
1127
|
+
headers['Authorization'] = `Bearer ${options.apiKey}`;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const payload = JSON.stringify(body || {});
|
|
1131
|
+
headers['Content-Length'] = Buffer.byteLength(payload);
|
|
1132
|
+
|
|
1133
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
1134
|
+
const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
|
|
1135
|
+
const start = Date.now();
|
|
1136
|
+
const req = transport.request(parsed, { method: 'POST', headers }, (res) => {
|
|
1137
|
+
const chunks = [];
|
|
1138
|
+
let size = 0;
|
|
1139
|
+
res.on('data', (chunk) => {
|
|
1140
|
+
if (!chunk) return;
|
|
1141
|
+
size += chunk.length;
|
|
1142
|
+
if (size <= maxBytes) {
|
|
1143
|
+
chunks.push(chunk);
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
res.on('end', () => {
|
|
1147
|
+
const bodyText = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
1148
|
+
resolve({
|
|
1149
|
+
ok: true,
|
|
1150
|
+
status: res.statusCode || 0,
|
|
1151
|
+
durationMs: Date.now() - start,
|
|
1152
|
+
body: bodyText
|
|
1153
|
+
});
|
|
1154
|
+
});
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
req.setTimeout(timeoutMs, () => {
|
|
1158
|
+
req.destroy(new Error('timeout'));
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
req.on('error', (err) => {
|
|
1162
|
+
resolve({
|
|
1163
|
+
ok: false,
|
|
1164
|
+
error: err.message || 'request failed',
|
|
1165
|
+
durationMs: Date.now() - start
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
req.write(payload);
|
|
1170
|
+
req.end();
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function extractModelIds(payload) {
|
|
1175
|
+
const ids = [];
|
|
1176
|
+
const pushValue = (value) => {
|
|
1177
|
+
if (typeof value === 'string' && value.trim()) {
|
|
1178
|
+
ids.push(value.trim());
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
if (!payload) return ids;
|
|
1183
|
+
|
|
1184
|
+
if (Array.isArray(payload)) {
|
|
1185
|
+
for (const item of payload) {
|
|
1186
|
+
if (item && typeof item === 'object') {
|
|
1187
|
+
pushValue(item.id);
|
|
1188
|
+
pushValue(item.model);
|
|
1189
|
+
pushValue(item.name);
|
|
1190
|
+
} else {
|
|
1191
|
+
pushValue(item);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return ids;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
if (Array.isArray(payload.data)) {
|
|
1198
|
+
for (const item of payload.data) {
|
|
1199
|
+
if (item && typeof item === 'object') {
|
|
1200
|
+
pushValue(item.id);
|
|
1201
|
+
pushValue(item.model);
|
|
1202
|
+
pushValue(item.name);
|
|
1203
|
+
} else {
|
|
1204
|
+
pushValue(item);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (Array.isArray(payload.models)) {
|
|
1210
|
+
for (const item of payload.models) {
|
|
1211
|
+
if (item && typeof item === 'object') {
|
|
1212
|
+
pushValue(item.id);
|
|
1213
|
+
pushValue(item.model);
|
|
1214
|
+
pushValue(item.name);
|
|
1215
|
+
} else {
|
|
1216
|
+
pushValue(item);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return ids;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
async function runRemoteHealthCheck(provider, modelName, options = {}) {
|
|
1225
|
+
const issues = [];
|
|
1226
|
+
const results = {};
|
|
1227
|
+
const baseUrl = normalizeBaseUrl(provider && provider.base_url ? provider.base_url : '');
|
|
1228
|
+
if (!baseUrl) {
|
|
1229
|
+
issues.push({
|
|
1230
|
+
code: 'remote-skip-base-url',
|
|
1231
|
+
message: '无法进行远程探测:base_url 为空',
|
|
1232
|
+
suggestion: '补全 base_url 或关闭远程探测'
|
|
1233
|
+
});
|
|
1234
|
+
return { issues, results };
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const requiresAuth = provider && provider.requires_openai_auth !== false;
|
|
1238
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
1239
|
+
? provider.preferred_auth_method.trim()
|
|
1240
|
+
: '';
|
|
1241
|
+
const authValue = requiresAuth ? apiKey : (apiKey || '');
|
|
1242
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
1243
|
+
|
|
1244
|
+
const baseProbe = await probeUrl(baseUrl, { apiKey: authValue, timeoutMs });
|
|
1245
|
+
results.base = {
|
|
1246
|
+
url: baseUrl,
|
|
1247
|
+
status: baseProbe.status || 0,
|
|
1248
|
+
ok: baseProbe.ok,
|
|
1249
|
+
durationMs: baseProbe.durationMs || 0
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
if (!baseProbe.ok) {
|
|
1253
|
+
issues.push({
|
|
1254
|
+
code: 'remote-unreachable',
|
|
1255
|
+
message: `远程探测失败:${baseProbe.error || '无法连接'}`,
|
|
1256
|
+
suggestion: '检查网络与 base_url 可达性'
|
|
1257
|
+
});
|
|
1258
|
+
return { issues, results };
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (baseProbe.status === 401 || baseProbe.status === 403) {
|
|
1262
|
+
issues.push({
|
|
1263
|
+
code: 'remote-auth-failed',
|
|
1264
|
+
message: '远程探测鉴权失败(401/403)',
|
|
1265
|
+
suggestion: '检查 API Key 或认证方式'
|
|
1266
|
+
});
|
|
1267
|
+
} else if (baseProbe.status >= 400) {
|
|
1268
|
+
issues.push({
|
|
1269
|
+
code: 'remote-http-error',
|
|
1270
|
+
message: `远程探测返回异常状态: ${baseProbe.status}`,
|
|
1271
|
+
suggestion: '检查 base_url 是否正确'
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const modelsUrl = buildModelsProbeUrl(baseUrl);
|
|
1276
|
+
if (modelsUrl) {
|
|
1277
|
+
const modelsProbe = await probeUrl(modelsUrl, { apiKey: authValue, timeoutMs, maxBytes: 256 * 1024 });
|
|
1278
|
+
results.models = {
|
|
1279
|
+
url: modelsUrl,
|
|
1280
|
+
status: modelsProbe.status || 0,
|
|
1281
|
+
ok: modelsProbe.ok,
|
|
1282
|
+
durationMs: modelsProbe.durationMs || 0
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
if (!modelsProbe.ok) {
|
|
1286
|
+
issues.push({
|
|
1287
|
+
code: 'remote-models-unreachable',
|
|
1288
|
+
message: `模型列表探测失败:${modelsProbe.error || '无法连接'}`,
|
|
1289
|
+
suggestion: '检查 base_url 是否包含 /v1 或关闭远程探测'
|
|
1290
|
+
});
|
|
1291
|
+
} else if (modelsProbe.status === 401 || modelsProbe.status === 403) {
|
|
1292
|
+
issues.push({
|
|
1293
|
+
code: 'remote-models-auth-failed',
|
|
1294
|
+
message: '模型列表鉴权失败(401/403)',
|
|
1295
|
+
suggestion: '检查 API Key 或认证方式'
|
|
1296
|
+
});
|
|
1297
|
+
} else if (modelsProbe.status >= 400) {
|
|
1298
|
+
issues.push({
|
|
1299
|
+
code: 'remote-models-http-error',
|
|
1300
|
+
message: `模型列表返回异常状态: ${modelsProbe.status}`,
|
|
1301
|
+
suggestion: '确认 /v1/models 可用'
|
|
1302
|
+
});
|
|
1303
|
+
} else {
|
|
1304
|
+
let payload = null;
|
|
1305
|
+
try {
|
|
1306
|
+
payload = modelsProbe.body ? JSON.parse(modelsProbe.body) : null;
|
|
1307
|
+
} catch (e) {
|
|
1308
|
+
issues.push({
|
|
1309
|
+
code: 'remote-models-parse',
|
|
1310
|
+
message: '模型列表解析失败(非 JSON)',
|
|
1311
|
+
suggestion: '确认 /v1/models 返回 JSON'
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (payload) {
|
|
1316
|
+
const ids = extractModelIds(payload);
|
|
1317
|
+
if (ids.length === 0) {
|
|
1318
|
+
issues.push({
|
|
1319
|
+
code: 'remote-models-empty',
|
|
1320
|
+
message: '模型列表为空或结构无法识别',
|
|
1321
|
+
suggestion: '确认 provider 是否兼容 /v1/models'
|
|
1322
|
+
});
|
|
1323
|
+
} else if (modelName && !ids.includes(modelName)) {
|
|
1324
|
+
issues.push({
|
|
1325
|
+
code: 'remote-model-unavailable',
|
|
1326
|
+
message: `远程模型列表中未找到: ${modelName}`,
|
|
1327
|
+
suggestion: '切换模型或确认模型名称'
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const modelProbeSpec = buildModelProbeSpec(provider, modelName, baseUrl);
|
|
1335
|
+
if (modelProbeSpec && modelProbeSpec.url) {
|
|
1336
|
+
const modelProbe = await probeJsonPost(modelProbeSpec.url, modelProbeSpec.body, {
|
|
1337
|
+
apiKey: authValue,
|
|
1338
|
+
timeoutMs,
|
|
1339
|
+
maxBytes: 256 * 1024
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
results.modelProbe = {
|
|
1343
|
+
url: modelProbeSpec.url,
|
|
1344
|
+
status: modelProbe.status || 0,
|
|
1345
|
+
ok: modelProbe.ok,
|
|
1346
|
+
durationMs: modelProbe.durationMs || 0
|
|
435
1347
|
};
|
|
1348
|
+
|
|
1349
|
+
if (!modelProbe.ok) {
|
|
1350
|
+
issues.push({
|
|
1351
|
+
code: 'remote-model-probe-unreachable',
|
|
1352
|
+
message: `模型可用性探测失败:${modelProbe.error || '无法连接'}`,
|
|
1353
|
+
suggestion: '检查网络或模型接口是否可用'
|
|
1354
|
+
});
|
|
1355
|
+
} else if (modelProbe.status === 401 || modelProbe.status === 403) {
|
|
1356
|
+
issues.push({
|
|
1357
|
+
code: 'remote-model-probe-auth-failed',
|
|
1358
|
+
message: '模型可用性探测鉴权失败(401/403)',
|
|
1359
|
+
suggestion: '检查 API Key 或认证方式'
|
|
1360
|
+
});
|
|
1361
|
+
} else if (modelProbe.status >= 400) {
|
|
1362
|
+
issues.push({
|
|
1363
|
+
code: 'remote-model-probe-http-error',
|
|
1364
|
+
message: `模型可用性探测返回异常状态: ${modelProbe.status}`,
|
|
1365
|
+
suggestion: '检查模型或接口路径'
|
|
1366
|
+
});
|
|
1367
|
+
} else {
|
|
1368
|
+
let payload = null;
|
|
1369
|
+
try {
|
|
1370
|
+
payload = modelProbe.body ? JSON.parse(modelProbe.body) : null;
|
|
1371
|
+
} catch (e) {
|
|
1372
|
+
issues.push({
|
|
1373
|
+
code: 'remote-model-probe-parse',
|
|
1374
|
+
message: '模型可用性探测解析失败(非 JSON)',
|
|
1375
|
+
suggestion: '确认模型接口返回 JSON'
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
if (payload && payload.error) {
|
|
1379
|
+
const message = typeof payload.error.message === 'string'
|
|
1380
|
+
? payload.error.message
|
|
1381
|
+
: '模型接口返回错误';
|
|
1382
|
+
issues.push({
|
|
1383
|
+
code: 'remote-model-probe-error',
|
|
1384
|
+
message: `模型可用性探测失败:${message}`,
|
|
1385
|
+
suggestion: '检查模型名与权限'
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
436
1389
|
}
|
|
1390
|
+
|
|
1391
|
+
return { issues, results };
|
|
437
1392
|
}
|
|
438
1393
|
|
|
439
|
-
function
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
1394
|
+
async function buildConfigHealthReport(params = {}) {
|
|
1395
|
+
const issues = [];
|
|
1396
|
+
const status = readConfigOrVirtualDefault();
|
|
1397
|
+
const config = status.config || {};
|
|
443
1398
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
1399
|
+
if (status.isVirtual) {
|
|
1400
|
+
issues.push({
|
|
1401
|
+
code: 'config-missing',
|
|
1402
|
+
message: status.reason || '未检测到 config.toml',
|
|
1403
|
+
suggestion: '在模板编辑器中确认应用配置,生成可用的 config.toml'
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
450
1406
|
|
|
451
|
-
|
|
452
|
-
|
|
1407
|
+
const providerName = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
1408
|
+
const modelName = typeof config.model === 'string' ? config.model.trim() : '';
|
|
1409
|
+
if (!providerName) {
|
|
1410
|
+
issues.push({
|
|
1411
|
+
code: 'provider-missing',
|
|
1412
|
+
message: '当前 provider 未设置',
|
|
1413
|
+
suggestion: '在模板中设置 model_provider'
|
|
1414
|
+
});
|
|
453
1415
|
}
|
|
454
1416
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
1417
|
+
if (!modelName) {
|
|
1418
|
+
issues.push({
|
|
1419
|
+
code: 'model-missing',
|
|
1420
|
+
message: '当前模型未设置',
|
|
1421
|
+
suggestion: '在模板中设置 model'
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
459
1424
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1425
|
+
const providers = config.model_providers && typeof config.model_providers === 'object'
|
|
1426
|
+
? config.model_providers
|
|
1427
|
+
: {};
|
|
1428
|
+
const provider = providerName ? providers[providerName] : null;
|
|
1429
|
+
if (providerName && !provider) {
|
|
1430
|
+
issues.push({
|
|
1431
|
+
code: 'provider-not-found',
|
|
1432
|
+
message: `当前 provider 未在配置中找到: ${providerName}`,
|
|
1433
|
+
suggestion: '检查 model_providers 是否包含该 provider 配置块'
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
463
1436
|
|
|
464
|
-
|
|
465
|
-
|
|
1437
|
+
if (provider && typeof provider === 'object') {
|
|
1438
|
+
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
1439
|
+
if (!isValidHttpUrl(baseUrl)) {
|
|
1440
|
+
issues.push({
|
|
1441
|
+
code: 'base-url-invalid',
|
|
1442
|
+
message: '当前 provider 的 base_url 无效',
|
|
1443
|
+
suggestion: '请设置为 http/https 的完整 URL'
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
466
1446
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1447
|
+
const requiresAuth = provider.requires_openai_auth;
|
|
1448
|
+
if (requiresAuth !== false) {
|
|
1449
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
1450
|
+
? provider.preferred_auth_method.trim()
|
|
1451
|
+
: '';
|
|
1452
|
+
if (!apiKey) {
|
|
1453
|
+
issues.push({
|
|
1454
|
+
code: 'api-key-missing',
|
|
1455
|
+
message: '当前 provider 未配置 API Key',
|
|
1456
|
+
suggestion: '在模板中设置 preferred_auth_method'
|
|
1457
|
+
});
|
|
477
1458
|
}
|
|
478
1459
|
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (modelName) {
|
|
1463
|
+
const models = readModels();
|
|
1464
|
+
if (!models.includes(modelName)) {
|
|
1465
|
+
issues.push({
|
|
1466
|
+
code: 'model-unavailable',
|
|
1467
|
+
message: `模型未在可用列表中找到: ${modelName}`,
|
|
1468
|
+
suggestion: '在模型列表中添加该模型或切换到已有模型'
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const remoteEnabled = !!params.remote;
|
|
1474
|
+
let remote = null;
|
|
1475
|
+
if (remoteEnabled) {
|
|
1476
|
+
const baseUrl = provider && typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
1477
|
+
if (!provider) {
|
|
1478
|
+
issues.push({
|
|
1479
|
+
code: 'remote-skip-provider',
|
|
1480
|
+
message: '无法进行远程探测:provider 未找到',
|
|
1481
|
+
suggestion: '检查 model_provider 配置或关闭远程探测'
|
|
1482
|
+
});
|
|
1483
|
+
} else if (!isValidHttpUrl(baseUrl)) {
|
|
1484
|
+
issues.push({
|
|
1485
|
+
code: 'remote-skip-base-url',
|
|
1486
|
+
message: '无法进行远程探测:base_url 无效',
|
|
1487
|
+
suggestion: '补全 base_url 或关闭远程探测'
|
|
1488
|
+
});
|
|
1489
|
+
} else {
|
|
1490
|
+
const timeoutMs = Number.isFinite(params.timeoutMs)
|
|
1491
|
+
? Math.max(1000, Number(params.timeoutMs))
|
|
1492
|
+
: undefined;
|
|
1493
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
1494
|
+
? provider.preferred_auth_method.trim()
|
|
1495
|
+
: '';
|
|
1496
|
+
const speedResult = await runSpeedTest(baseUrl, apiKey, { timeoutMs });
|
|
1497
|
+
const status = speedResult && typeof speedResult.status === 'number'
|
|
1498
|
+
? speedResult.status
|
|
1499
|
+
: 0;
|
|
1500
|
+
const durationMs = speedResult && typeof speedResult.durationMs === 'number'
|
|
1501
|
+
? speedResult.durationMs
|
|
1502
|
+
: 0;
|
|
1503
|
+
const error = speedResult && speedResult.error ? String(speedResult.error) : '';
|
|
1504
|
+
remote = {
|
|
1505
|
+
type: 'speed-test',
|
|
1506
|
+
url: baseUrl,
|
|
1507
|
+
ok: !!speedResult.ok,
|
|
1508
|
+
status,
|
|
1509
|
+
durationMs,
|
|
1510
|
+
error
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
if (!speedResult.ok) {
|
|
1514
|
+
const errorLower = error.toLowerCase();
|
|
1515
|
+
if (errorLower.includes('timeout')) {
|
|
1516
|
+
issues.push({
|
|
1517
|
+
code: 'remote-speedtest-timeout',
|
|
1518
|
+
message: '远程测速超时',
|
|
1519
|
+
suggestion: '检查网络或 base_url 是否可达'
|
|
1520
|
+
});
|
|
1521
|
+
} else if (errorLower.includes('invalid url')) {
|
|
1522
|
+
issues.push({
|
|
1523
|
+
code: 'remote-speedtest-invalid-url',
|
|
1524
|
+
message: '远程测速失败:base_url 无效',
|
|
1525
|
+
suggestion: '请设置为 http/https 的完整 URL'
|
|
1526
|
+
});
|
|
1527
|
+
} else {
|
|
1528
|
+
issues.push({
|
|
1529
|
+
code: 'remote-speedtest-unreachable',
|
|
1530
|
+
message: `远程测速失败:${error || '无法连接'}`,
|
|
1531
|
+
suggestion: '检查网络或 base_url 是否可用'
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
} else if (status === 401 || status === 403) {
|
|
1535
|
+
issues.push({
|
|
1536
|
+
code: 'remote-speedtest-auth-failed',
|
|
1537
|
+
message: '远程测速鉴权失败(401/403)',
|
|
1538
|
+
suggestion: '检查 API Key 或认证方式'
|
|
1539
|
+
});
|
|
1540
|
+
} else if (status >= 400) {
|
|
1541
|
+
issues.push({
|
|
1542
|
+
code: 'remote-speedtest-http-error',
|
|
1543
|
+
message: `远程测速返回异常状态: ${status}`,
|
|
1544
|
+
suggestion: '检查 base_url 或服务状态'
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
482
1547
|
}
|
|
483
|
-
throw new Error(`写入 JSON 文件失败: ${e.message}`);
|
|
484
1548
|
}
|
|
1549
|
+
|
|
1550
|
+
return {
|
|
1551
|
+
ok: issues.length === 0,
|
|
1552
|
+
issues,
|
|
1553
|
+
summary: {
|
|
1554
|
+
currentProvider: providerName,
|
|
1555
|
+
currentModel: modelName
|
|
1556
|
+
},
|
|
1557
|
+
remote
|
|
1558
|
+
};
|
|
485
1559
|
}
|
|
486
1560
|
|
|
487
1561
|
function formatTimestampForFileName(value) {
|
|
@@ -510,6 +1584,16 @@ function toIsoTime(value, fallback = '') {
|
|
|
510
1584
|
return date.toISOString();
|
|
511
1585
|
}
|
|
512
1586
|
|
|
1587
|
+
function updateLatestIso(currentIso, candidate) {
|
|
1588
|
+
const currentTime = Date.parse(currentIso || '') || 0;
|
|
1589
|
+
const candidateIso = toIsoTime(candidate, '');
|
|
1590
|
+
const candidateTime = Date.parse(candidateIso || '') || 0;
|
|
1591
|
+
if (!candidateTime) {
|
|
1592
|
+
return currentIso;
|
|
1593
|
+
}
|
|
1594
|
+
return candidateTime > currentTime ? candidateIso : currentIso;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
513
1597
|
function truncateText(text, maxLength = 90) {
|
|
514
1598
|
if (!text) return '';
|
|
515
1599
|
const normalized = String(text).replace(/\s+/g, ' ').trim();
|
|
@@ -689,6 +1773,8 @@ function applyConfigTemplate(params = {}) {
|
|
|
689
1773
|
currentModels[activeProvider] = parsed.model;
|
|
690
1774
|
writeCurrentModels(currentModels);
|
|
691
1775
|
|
|
1776
|
+
recordRecentConfig(activeProvider, parsed.model);
|
|
1777
|
+
|
|
692
1778
|
return { success: true };
|
|
693
1779
|
}
|
|
694
1780
|
|
|
@@ -810,9 +1896,24 @@ function consumeInitNotice() {
|
|
|
810
1896
|
return notice;
|
|
811
1897
|
}
|
|
812
1898
|
|
|
1899
|
+
function normalizePathForCompare(targetPath, options = {}) {
|
|
1900
|
+
const ignoreCase = !!options.ignoreCase;
|
|
1901
|
+
let resolved = '';
|
|
1902
|
+
try {
|
|
1903
|
+
resolved = fs.realpathSync.native ? fs.realpathSync.native(targetPath) : fs.realpathSync(targetPath);
|
|
1904
|
+
} catch (e) {
|
|
1905
|
+
resolved = path.resolve(targetPath);
|
|
1906
|
+
}
|
|
1907
|
+
return ignoreCase ? resolved.toLowerCase() : resolved;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
813
1910
|
function isPathInside(targetPath, rootPath) {
|
|
814
|
-
|
|
815
|
-
|
|
1911
|
+
if (!targetPath || !rootPath) {
|
|
1912
|
+
return false;
|
|
1913
|
+
}
|
|
1914
|
+
const ignoreCase = process.platform === 'win32';
|
|
1915
|
+
const resolvedTarget = normalizePathForCompare(targetPath, { ignoreCase });
|
|
1916
|
+
const resolvedRoot = normalizePathForCompare(rootPath, { ignoreCase });
|
|
816
1917
|
if (resolvedTarget === resolvedRoot) {
|
|
817
1918
|
return true;
|
|
818
1919
|
}
|
|
@@ -1155,23 +2256,23 @@ function scanSessionContentForQuery(session, tokens, options = {}) {
|
|
|
1155
2256
|
? Math.max(0, Number(options.snippetLimit))
|
|
1156
2257
|
: 0;
|
|
1157
2258
|
|
|
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) {
|
|
2259
|
+
const messages = [];
|
|
2260
|
+
for (const record of records) {
|
|
2261
|
+
const message = extractMessageFromRecord(record, session.source);
|
|
2262
|
+
if (!message || !message.text) {
|
|
2263
|
+
continue;
|
|
2264
|
+
}
|
|
2265
|
+
messages.push(message);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
const filteredMessages = roleFilter === 'system'
|
|
2269
|
+
? messages
|
|
2270
|
+
: removeLeadingSystemMessage(messages);
|
|
2271
|
+
|
|
2272
|
+
let count = 0;
|
|
2273
|
+
const snippets = [];
|
|
2274
|
+
|
|
2275
|
+
for (const message of filteredMessages) {
|
|
1175
2276
|
if (roleFilter !== 'all' && message.role !== roleFilter) {
|
|
1176
2277
|
continue;
|
|
1177
2278
|
}
|
|
@@ -1198,11 +2299,11 @@ function applySessionQueryFilter(sessions, options = {}) {
|
|
|
1198
2299
|
}
|
|
1199
2300
|
|
|
1200
2301
|
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;
|
|
2302
|
+
const scope = normalizeQueryScope(options.queryScope);
|
|
2303
|
+
const roleFilter = normalizeRoleFilter(options.roleFilter);
|
|
2304
|
+
const contentScanLimit = Number.isFinite(Number(options.contentScanLimit))
|
|
2305
|
+
? Math.max(1, Number(options.contentScanLimit))
|
|
2306
|
+
: DEFAULT_CONTENT_SCAN_LIMIT;
|
|
1206
2307
|
const contentScanBytes = Number.isFinite(Number(options.contentScanBytes))
|
|
1207
2308
|
? Math.max(1024, Number(options.contentScanBytes))
|
|
1208
2309
|
: SESSION_CONTENT_READ_BYTES;
|
|
@@ -1254,7 +2355,7 @@ function applySessionQueryFilter(sessions, options = {}) {
|
|
|
1254
2355
|
|
|
1255
2356
|
return results;
|
|
1256
2357
|
}
|
|
1257
|
-
function collectRecentJsonlFiles(rootDir, options = {}) {
|
|
2358
|
+
function collectRecentJsonlFiles(rootDir, options = {}) {
|
|
1258
2359
|
if (!fs.existsSync(rootDir)) {
|
|
1259
2360
|
return [];
|
|
1260
2361
|
}
|
|
@@ -1366,7 +2467,7 @@ function parseCodexSessionSummary(filePath) {
|
|
|
1366
2467
|
|
|
1367
2468
|
for (const record of records) {
|
|
1368
2469
|
if (record.timestamp) {
|
|
1369
|
-
updatedAt =
|
|
2470
|
+
updatedAt = updateLatestIso(updatedAt, record.timestamp);
|
|
1370
2471
|
}
|
|
1371
2472
|
|
|
1372
2473
|
if (record.type === 'session_meta' && record.payload) {
|
|
@@ -1455,7 +2556,7 @@ function parseClaudeSessionSummary(filePath) {
|
|
|
1455
2556
|
createdAt = toIsoTime(record.timestamp, createdAt);
|
|
1456
2557
|
}
|
|
1457
2558
|
if (record.timestamp) {
|
|
1458
|
-
updatedAt =
|
|
2559
|
+
updatedAt = updateLatestIso(updatedAt, record.timestamp);
|
|
1459
2560
|
}
|
|
1460
2561
|
|
|
1461
2562
|
if (!cwd && record.cwd) {
|
|
@@ -1516,6 +2617,7 @@ function parseClaudeSessionSummary(filePath) {
|
|
|
1516
2617
|
}
|
|
1517
2618
|
|
|
1518
2619
|
function listCodexSessions(limit, options = {}) {
|
|
2620
|
+
const codexSessionsDir = getCodexSessionsDir();
|
|
1519
2621
|
const scanFactor = Number.isFinite(Number(options.scanFactor))
|
|
1520
2622
|
? Math.max(1, Number(options.scanFactor))
|
|
1521
2623
|
: SESSION_SCAN_FACTOR;
|
|
@@ -1531,7 +2633,7 @@ function listCodexSessions(limit, options = {}) {
|
|
|
1531
2633
|
const maxFilesScanned = Number.isFinite(Number(options.maxFilesScanned))
|
|
1532
2634
|
? Math.max(scanCount, Math.floor(Number(options.maxFilesScanned)))
|
|
1533
2635
|
: Math.max(scanCount * 2, minFiles);
|
|
1534
|
-
const files = collectRecentJsonlFiles(
|
|
2636
|
+
const files = collectRecentJsonlFiles(codexSessionsDir, {
|
|
1535
2637
|
returnCount: scanCount,
|
|
1536
2638
|
maxFilesScanned
|
|
1537
2639
|
});
|
|
@@ -1552,7 +2654,8 @@ function listCodexSessions(limit, options = {}) {
|
|
|
1552
2654
|
}
|
|
1553
2655
|
|
|
1554
2656
|
function listClaudeSessions(limit, options = {}) {
|
|
1555
|
-
|
|
2657
|
+
const claudeProjectsDir = getClaudeProjectsDir();
|
|
2658
|
+
if (!fs.existsSync(claudeProjectsDir)) {
|
|
1556
2659
|
return [];
|
|
1557
2660
|
}
|
|
1558
2661
|
|
|
@@ -1575,9 +2678,9 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
1575
2678
|
const sessions = [];
|
|
1576
2679
|
let projectDirs = [];
|
|
1577
2680
|
try {
|
|
1578
|
-
projectDirs = fs.readdirSync(
|
|
2681
|
+
projectDirs = fs.readdirSync(claudeProjectsDir, { withFileTypes: true })
|
|
1579
2682
|
.filter(entry => entry.isDirectory())
|
|
1580
|
-
.map(entry => path.join(
|
|
2683
|
+
.map(entry => path.join(claudeProjectsDir, entry.name));
|
|
1581
2684
|
} catch (e) {
|
|
1582
2685
|
projectDirs = [];
|
|
1583
2686
|
}
|
|
@@ -1597,6 +2700,11 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
1597
2700
|
let filePath = typeof entry.fullPath === 'string' && entry.fullPath
|
|
1598
2701
|
? entry.fullPath
|
|
1599
2702
|
: path.join(projectDir, `${sessionId}.jsonl`);
|
|
2703
|
+
filePath = expandHomePath(filePath);
|
|
2704
|
+
if (filePath && !path.isAbsolute(filePath)) {
|
|
2705
|
+
filePath = path.join(projectDir, filePath);
|
|
2706
|
+
}
|
|
2707
|
+
filePath = filePath ? path.resolve(filePath) : '';
|
|
1600
2708
|
|
|
1601
2709
|
if (!fs.existsSync(filePath)) {
|
|
1602
2710
|
continue;
|
|
@@ -1652,7 +2760,7 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
1652
2760
|
}
|
|
1653
2761
|
|
|
1654
2762
|
if (sessions.length === 0) {
|
|
1655
|
-
const fallbackFiles = collectRecentJsonlFiles(
|
|
2763
|
+
const fallbackFiles = collectRecentJsonlFiles(claudeProjectsDir, {
|
|
1656
2764
|
returnCount: scanCount,
|
|
1657
2765
|
maxFilesScanned,
|
|
1658
2766
|
ignoreSubPath: `${path.sep}subagents${path.sep}`
|
|
@@ -1712,18 +2820,18 @@ function listAllSessions(params = {}) {
|
|
|
1712
2820
|
sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
|
|
1713
2821
|
}
|
|
1714
2822
|
|
|
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);
|
|
2823
|
+
let result = sessions;
|
|
2824
|
+
if (hasQuery) {
|
|
2825
|
+
result = applySessionQueryFilter(result, {
|
|
2826
|
+
tokens: queryTokens,
|
|
2827
|
+
queryMode: params.queryMode,
|
|
2828
|
+
queryScope: params.queryScope,
|
|
2829
|
+
roleFilter: params.roleFilter,
|
|
2830
|
+
contentScanLimit: params.contentScanLimit,
|
|
2831
|
+
contentScanBytes: params.contentScanBytes
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
result = mergeAndLimitSessions(result, limit);
|
|
1727
2835
|
if (!hasQuery) {
|
|
1728
2836
|
setSessionListCache(cacheKey, result);
|
|
1729
2837
|
}
|
|
@@ -1784,14 +2892,15 @@ function listSessionPaths(params = {}) {
|
|
|
1784
2892
|
}
|
|
1785
2893
|
|
|
1786
2894
|
function resolveSessionFilePath(source, filePath, sessionId) {
|
|
1787
|
-
const root = source === 'claude' ?
|
|
2895
|
+
const root = source === 'claude' ? getClaudeProjectsDir() : getCodexSessionsDir();
|
|
1788
2896
|
if (!root || !fs.existsSync(root)) {
|
|
1789
2897
|
return '';
|
|
1790
2898
|
}
|
|
1791
2899
|
|
|
1792
2900
|
if (typeof filePath === 'string' && filePath.trim()) {
|
|
1793
|
-
const
|
|
1794
|
-
|
|
2901
|
+
const expandedPath = expandHomePath(filePath.trim());
|
|
2902
|
+
const targetPath = expandedPath ? path.resolve(expandedPath) : '';
|
|
2903
|
+
if (targetPath && fs.existsSync(targetPath) && isPathInside(targetPath, root)) {
|
|
1795
2904
|
return targetPath;
|
|
1796
2905
|
}
|
|
1797
2906
|
}
|
|
@@ -1808,7 +2917,269 @@ function resolveSessionFilePath(source, filePath, sessionId) {
|
|
|
1808
2917
|
return '';
|
|
1809
2918
|
}
|
|
1810
2919
|
|
|
1811
|
-
function
|
|
2920
|
+
function findClaudeSessionIndexPath(sessionFilePath) {
|
|
2921
|
+
const root = getClaudeProjectsDir();
|
|
2922
|
+
if (!root || !sessionFilePath) {
|
|
2923
|
+
return '';
|
|
2924
|
+
}
|
|
2925
|
+
if (!isPathInside(sessionFilePath, root)) {
|
|
2926
|
+
return '';
|
|
2927
|
+
}
|
|
2928
|
+
let current = path.dirname(sessionFilePath);
|
|
2929
|
+
const resolvedRoot = path.resolve(root);
|
|
2930
|
+
while (current && isPathInside(current, resolvedRoot)) {
|
|
2931
|
+
const candidate = path.join(current, 'sessions-index.json');
|
|
2932
|
+
if (fs.existsSync(candidate)) {
|
|
2933
|
+
return candidate;
|
|
2934
|
+
}
|
|
2935
|
+
const parent = path.dirname(current);
|
|
2936
|
+
if (parent === current) {
|
|
2937
|
+
break;
|
|
2938
|
+
}
|
|
2939
|
+
current = parent;
|
|
2940
|
+
}
|
|
2941
|
+
return '';
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
function updateClaudeSessionIndex(indexPath, sessionFilePath, sessionId) {
|
|
2945
|
+
if (!indexPath || !fs.existsSync(indexPath)) {
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
const index = readJsonFile(indexPath, null);
|
|
2949
|
+
if (!index || !Array.isArray(index.entries)) {
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
const resolvedFile = sessionFilePath ? path.resolve(sessionFilePath) : '';
|
|
2953
|
+
const resolvedLower = resolvedFile ? resolvedFile.toLowerCase() : '';
|
|
2954
|
+
const filtered = index.entries.filter((entry) => {
|
|
2955
|
+
if (!entry || typeof entry !== 'object') {
|
|
2956
|
+
return false;
|
|
2957
|
+
}
|
|
2958
|
+
const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
|
|
2959
|
+
if (sessionId && entrySessionId === sessionId) {
|
|
2960
|
+
return false;
|
|
2961
|
+
}
|
|
2962
|
+
if (entry.fullPath) {
|
|
2963
|
+
const expanded = expandHomePath(entry.fullPath);
|
|
2964
|
+
const entryPath = expanded ? path.resolve(expanded) : '';
|
|
2965
|
+
if (entryPath && resolvedLower && entryPath.toLowerCase() === resolvedLower) {
|
|
2966
|
+
return false;
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
return true;
|
|
2970
|
+
});
|
|
2971
|
+
if (filtered.length === index.entries.length) {
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
index.entries = filtered;
|
|
2975
|
+
try {
|
|
2976
|
+
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
|
|
2977
|
+
} catch (e) {}
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
async function deleteSessionData(params = {}) {
|
|
2981
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
2982
|
+
if (!source) {
|
|
2983
|
+
return { error: 'Invalid source' };
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
2987
|
+
if (!filePath) {
|
|
2988
|
+
return { error: 'Session file not found' };
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
|
|
2992
|
+
try {
|
|
2993
|
+
fs.unlinkSync(filePath);
|
|
2994
|
+
} catch (e) {
|
|
2995
|
+
return { error: `删除会话失败: ${e.message}` };
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
if (source === 'claude') {
|
|
2999
|
+
const indexPath = findClaudeSessionIndexPath(filePath);
|
|
3000
|
+
if (indexPath) {
|
|
3001
|
+
updateClaudeSessionIndex(indexPath, filePath, sessionId);
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
invalidateSessionListCache();
|
|
3006
|
+
|
|
3007
|
+
return {
|
|
3008
|
+
success: true,
|
|
3009
|
+
source,
|
|
3010
|
+
sessionId,
|
|
3011
|
+
filePath
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
function generateCloneSessionId() {
|
|
3016
|
+
if (crypto.randomUUID) {
|
|
3017
|
+
return `clone-${crypto.randomUUID()}`;
|
|
3018
|
+
}
|
|
3019
|
+
const timePart = Date.now().toString(36);
|
|
3020
|
+
const randomPart = crypto.randomBytes(8).toString('hex');
|
|
3021
|
+
return `clone-${timePart}-${randomPart}`;
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
function allocateCloneSessionTarget(dirPath) {
|
|
3025
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
3026
|
+
const sessionId = generateCloneSessionId();
|
|
3027
|
+
const filePath = path.join(dirPath, `${sessionId}.jsonl`);
|
|
3028
|
+
if (!fs.existsSync(filePath)) {
|
|
3029
|
+
return { sessionId, filePath };
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
const fallbackId = `clone-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
3033
|
+
return { sessionId: fallbackId, filePath: path.join(dirPath, `${fallbackId}.jsonl`) };
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
function parseTimestampMs(value) {
|
|
3037
|
+
if (value === undefined || value === null || value === '') {
|
|
3038
|
+
return null;
|
|
3039
|
+
}
|
|
3040
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
3041
|
+
if (value > 1e12) return value;
|
|
3042
|
+
if (value > 1e9) return value * 1000;
|
|
3043
|
+
return value;
|
|
3044
|
+
}
|
|
3045
|
+
if (typeof value === 'string') {
|
|
3046
|
+
const parsed = Date.parse(value);
|
|
3047
|
+
if (Number.isFinite(parsed)) {
|
|
3048
|
+
return parsed;
|
|
3049
|
+
}
|
|
3050
|
+
const numeric = Number(value);
|
|
3051
|
+
if (Number.isFinite(numeric)) {
|
|
3052
|
+
if (numeric > 1e12) return numeric;
|
|
3053
|
+
if (numeric > 1e9) return numeric * 1000;
|
|
3054
|
+
return numeric;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
return null;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
async function cloneCodexSession(params = {}) {
|
|
3061
|
+
const source = params.source === 'codex' ? 'codex' : '';
|
|
3062
|
+
if (!source) {
|
|
3063
|
+
return { error: '仅支持 Codex 会话克隆' };
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3067
|
+
if (!filePath) {
|
|
3068
|
+
return { error: 'Session file not found' };
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
let content = '';
|
|
3072
|
+
try {
|
|
3073
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
3074
|
+
} catch (e) {
|
|
3075
|
+
return { error: `读取会话失败: ${e.message}` };
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
if (!content.trim()) {
|
|
3079
|
+
return { error: 'Session file is empty' };
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
const lineEnding = detectLineEnding(content);
|
|
3083
|
+
const rawLines = content.split(/\r?\n/);
|
|
3084
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') {
|
|
3085
|
+
rawLines.pop();
|
|
3086
|
+
}
|
|
3087
|
+
|
|
3088
|
+
let originalSessionId = typeof params.sessionId === 'string' ? params.sessionId.trim() : '';
|
|
3089
|
+
if (!originalSessionId) {
|
|
3090
|
+
originalSessionId = path.basename(filePath, '.jsonl');
|
|
3091
|
+
}
|
|
3092
|
+
let maxTimestampMs = 0;
|
|
3093
|
+
|
|
3094
|
+
for (const line of rawLines) {
|
|
3095
|
+
const trimmed = line.trim();
|
|
3096
|
+
if (!trimmed) continue;
|
|
3097
|
+
try {
|
|
3098
|
+
const record = JSON.parse(trimmed);
|
|
3099
|
+
if (record && record.type === 'session_meta' && record.payload) {
|
|
3100
|
+
if (record.payload.id) {
|
|
3101
|
+
originalSessionId = record.payload.id;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
if (record && record.timestamp !== undefined) {
|
|
3105
|
+
const ts = parseTimestampMs(record.timestamp);
|
|
3106
|
+
if (Number.isFinite(ts) && ts > maxTimestampMs) {
|
|
3107
|
+
maxTimestampMs = ts;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
} catch (e) {}
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
const sessionsDir = getCodexSessionsDir();
|
|
3114
|
+
ensureDir(sessionsDir);
|
|
3115
|
+
const target = allocateCloneSessionTarget(sessionsDir);
|
|
3116
|
+
const newSessionId = target.sessionId;
|
|
3117
|
+
const newFilePath = target.filePath;
|
|
3118
|
+
const offsetMs = maxTimestampMs ? (Date.now() - maxTimestampMs) : 0;
|
|
3119
|
+
const cloneTime = new Date(Date.now() + 1);
|
|
3120
|
+
const cloneIso = cloneTime.toISOString();
|
|
3121
|
+
|
|
3122
|
+
const outputLines = [];
|
|
3123
|
+
for (const line of rawLines) {
|
|
3124
|
+
const trimmed = line.trim();
|
|
3125
|
+
if (!trimmed) {
|
|
3126
|
+
outputLines.push(line);
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
let record;
|
|
3130
|
+
try {
|
|
3131
|
+
record = JSON.parse(trimmed);
|
|
3132
|
+
} catch (e) {
|
|
3133
|
+
outputLines.push(line);
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
if (originalSessionId && typeof record.sessionId === 'string' && record.sessionId === originalSessionId) {
|
|
3138
|
+
record.sessionId = newSessionId;
|
|
3139
|
+
}
|
|
3140
|
+
if (originalSessionId && typeof record.session_id === 'string' && record.session_id === originalSessionId) {
|
|
3141
|
+
record.session_id = newSessionId;
|
|
3142
|
+
}
|
|
3143
|
+
if (offsetMs && record.timestamp !== undefined) {
|
|
3144
|
+
const ts = parseTimestampMs(record.timestamp);
|
|
3145
|
+
if (Number.isFinite(ts)) {
|
|
3146
|
+
record.timestamp = new Date(ts + offsetMs).toISOString();
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
if (record && record.type === 'session_meta' && record.payload && typeof record.payload === 'object') {
|
|
3150
|
+
record.payload = {
|
|
3151
|
+
...record.payload,
|
|
3152
|
+
id: newSessionId,
|
|
3153
|
+
timestamp: cloneIso
|
|
3154
|
+
};
|
|
3155
|
+
record.timestamp = cloneIso;
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
outputLines.push(JSON.stringify(record));
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
const output = outputLines.join(lineEnding) + lineEnding;
|
|
3162
|
+
try {
|
|
3163
|
+
fs.writeFileSync(newFilePath, output, 'utf-8');
|
|
3164
|
+
} catch (e) {
|
|
3165
|
+
return { error: `写入克隆会话失败: ${e.message}` };
|
|
3166
|
+
}
|
|
3167
|
+
try {
|
|
3168
|
+
fs.utimesSync(newFilePath, cloneTime, cloneTime);
|
|
3169
|
+
} catch (e) {}
|
|
3170
|
+
|
|
3171
|
+
invalidateSessionListCache();
|
|
3172
|
+
|
|
3173
|
+
return {
|
|
3174
|
+
success: true,
|
|
3175
|
+
source,
|
|
3176
|
+
sourceLabel: 'Codex',
|
|
3177
|
+
sessionId: newSessionId,
|
|
3178
|
+
filePath: newFilePath
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
function buildSessionMarkdown(payload) {
|
|
1812
3183
|
const lines = [
|
|
1813
3184
|
'# AI Session Export',
|
|
1814
3185
|
'',
|
|
@@ -1837,24 +3208,75 @@ function buildSessionMarkdown(payload) {
|
|
|
1837
3208
|
lines.push('');
|
|
1838
3209
|
});
|
|
1839
3210
|
|
|
1840
|
-
return lines.join('\n');
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
function
|
|
1844
|
-
if (!
|
|
1845
|
-
return
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
|
|
3211
|
+
return lines.join('\n');
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
function buildSessionPlainText(messages) {
|
|
3215
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
3216
|
+
return '';
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
const lines = [];
|
|
3220
|
+
messages.forEach((message) => {
|
|
3221
|
+
const role = normalizeRole(message && message.role) || 'unknown';
|
|
3222
|
+
const text = message && typeof message.text === 'string' ? message.text : '';
|
|
3223
|
+
lines.push(role);
|
|
3224
|
+
lines.push(text);
|
|
3225
|
+
lines.push('');
|
|
3226
|
+
});
|
|
3227
|
+
|
|
3228
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
3229
|
+
lines.pop();
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
return lines.join('\n');
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
function parseMaxMessagesValue(value) {
|
|
3236
|
+
if (value === Infinity) {
|
|
3237
|
+
return Infinity;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
if (typeof value === 'string') {
|
|
3241
|
+
const trimmed = value.trim();
|
|
3242
|
+
if (!trimmed) {
|
|
3243
|
+
return null;
|
|
3244
|
+
}
|
|
3245
|
+
const lower = trimmed.toLowerCase();
|
|
3246
|
+
if (lower === 'all' || lower === 'infinity' || lower === 'inf') {
|
|
3247
|
+
return Infinity;
|
|
3248
|
+
}
|
|
3249
|
+
const parsed = Number(trimmed);
|
|
3250
|
+
if (Number.isFinite(parsed)) {
|
|
3251
|
+
return parsed;
|
|
3252
|
+
}
|
|
3253
|
+
return null;
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
if (Number.isFinite(value)) {
|
|
3257
|
+
return value;
|
|
3258
|
+
}
|
|
3259
|
+
return null;
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
function resolveMaxMessagesValue(value, fallback) {
|
|
3263
|
+
const parsed = parseMaxMessagesValue(value);
|
|
3264
|
+
if (parsed === null) {
|
|
3265
|
+
return fallback;
|
|
3266
|
+
}
|
|
3267
|
+
if (parsed === Infinity) {
|
|
3268
|
+
return Infinity;
|
|
3269
|
+
}
|
|
3270
|
+
return Math.max(1, Math.floor(parsed));
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
function resolveStateMaxMessages(state) {
|
|
3274
|
+
if (!state || typeof state !== 'object') {
|
|
3275
|
+
return MAX_EXPORT_MESSAGES;
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
return resolveMaxMessagesValue(state.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3279
|
+
}
|
|
1858
3280
|
|
|
1859
3281
|
function canAppendMessage(state) {
|
|
1860
3282
|
const maxMessages = resolveStateMaxMessages(state);
|
|
@@ -1891,7 +3313,7 @@ function extractCodexMessageFromRecord(record, state, lineIndex = -1) {
|
|
|
1891
3313
|
}
|
|
1892
3314
|
}
|
|
1893
3315
|
|
|
1894
|
-
function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
3316
|
+
function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
1895
3317
|
if (record.timestamp) {
|
|
1896
3318
|
state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
|
|
1897
3319
|
}
|
|
@@ -1916,89 +3338,130 @@ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
|
1916
3338
|
recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
|
|
1917
3339
|
});
|
|
1918
3340
|
}
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
function
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
function recordHasCodexMessage(record) {
|
|
3345
|
+
if (!record || record.type !== 'response_item' || !record.payload) {
|
|
3346
|
+
return false;
|
|
3347
|
+
}
|
|
3348
|
+
if (record.payload.type !== 'message') {
|
|
3349
|
+
return false;
|
|
3350
|
+
}
|
|
3351
|
+
const role = normalizeRole(record.payload.role);
|
|
3352
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
3353
|
+
return false;
|
|
3354
|
+
}
|
|
3355
|
+
const text = extractMessageText(record.payload.content);
|
|
3356
|
+
return !!text;
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
function recordHasClaudeMessage(record) {
|
|
3360
|
+
if (!record) {
|
|
3361
|
+
return false;
|
|
3362
|
+
}
|
|
3363
|
+
const role = normalizeRole(record.type);
|
|
3364
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
3365
|
+
return false;
|
|
3366
|
+
}
|
|
3367
|
+
const content = record.message ? record.message.content : '';
|
|
3368
|
+
const text = extractMessageText(content);
|
|
3369
|
+
return !!text;
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
function recordHasMessage(record, source) {
|
|
3373
|
+
return source === 'codex'
|
|
3374
|
+
? recordHasCodexMessage(record)
|
|
3375
|
+
: recordHasClaudeMessage(record);
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
function extractMessagesFromRecords(records, source, options = {}) {
|
|
3379
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3380
|
+
const state = {
|
|
3381
|
+
sessionId: '',
|
|
3382
|
+
cwd: '',
|
|
3383
|
+
updatedAt: '',
|
|
3384
|
+
messages: [],
|
|
3385
|
+
maxMessages,
|
|
3386
|
+
truncated: false
|
|
3387
|
+
};
|
|
3388
|
+
|
|
3389
|
+
for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
|
|
3390
|
+
const record = records[lineIndex];
|
|
3391
|
+
if (source === 'codex') {
|
|
3392
|
+
extractCodexMessageFromRecord(record, state, lineIndex);
|
|
3393
|
+
} else {
|
|
3394
|
+
extractClaudeMessageFromRecord(record, state, lineIndex);
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
|
|
3398
|
+
for (let i = lineIndex + 1; i < records.length; i++) {
|
|
3399
|
+
if (recordHasMessage(records[i], source)) {
|
|
3400
|
+
state.truncated = true;
|
|
3401
|
+
break;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
break;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
return state;
|
|
3409
|
+
}
|
|
1963
3410
|
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
3411
|
+
async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
3412
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3413
|
+
const state = {
|
|
3414
|
+
sessionId: '',
|
|
3415
|
+
cwd: '',
|
|
3416
|
+
updatedAt: '',
|
|
3417
|
+
messages: [],
|
|
3418
|
+
maxMessages,
|
|
3419
|
+
truncated: false
|
|
3420
|
+
};
|
|
1969
3421
|
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
3422
|
+
let stream;
|
|
3423
|
+
let rl;
|
|
3424
|
+
try {
|
|
3425
|
+
stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
3426
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
3427
|
+
|
|
3428
|
+
let lineIndex = 0;
|
|
3429
|
+
let limitReached = false;
|
|
3430
|
+
for await (const line of rl) {
|
|
3431
|
+
const currentLineIndex = lineIndex;
|
|
3432
|
+
lineIndex += 1;
|
|
1974
3433
|
|
|
1975
3434
|
const trimmed = line.trim();
|
|
1976
3435
|
if (!trimmed) continue;
|
|
1977
3436
|
|
|
1978
3437
|
let record;
|
|
1979
|
-
try {
|
|
1980
|
-
record = JSON.parse(trimmed);
|
|
1981
|
-
} catch (e) {
|
|
1982
|
-
continue;
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
if (
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
3438
|
+
try {
|
|
3439
|
+
record = JSON.parse(trimmed);
|
|
3440
|
+
} catch (e) {
|
|
3441
|
+
continue;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
if (limitReached) {
|
|
3445
|
+
if (recordHasMessage(record, source)) {
|
|
3446
|
+
state.truncated = true;
|
|
3447
|
+
break;
|
|
3448
|
+
}
|
|
3449
|
+
continue;
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
if (source === 'codex') {
|
|
3453
|
+
extractCodexMessageFromRecord(record, state, currentLineIndex);
|
|
3454
|
+
} else {
|
|
3455
|
+
extractClaudeMessageFromRecord(record, state, currentLineIndex);
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
|
|
3459
|
+
limitReached = true;
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
} catch (e) {
|
|
3463
|
+
const fallbackRecords = readJsonlRecords(filePath);
|
|
3464
|
+
return extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
2002
3465
|
} finally {
|
|
2003
3466
|
if (rl) {
|
|
2004
3467
|
try { rl.close(); } catch (e) {}
|
|
@@ -2011,7 +3474,7 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
|
2011
3474
|
return state;
|
|
2012
3475
|
}
|
|
2013
3476
|
|
|
2014
|
-
async function readSessionDetail(params = {}) {
|
|
3477
|
+
async function readSessionDetail(params = {}) {
|
|
2015
3478
|
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
2016
3479
|
if (!source) {
|
|
2017
3480
|
return { error: 'Invalid source' };
|
|
@@ -2038,37 +3501,83 @@ async function readSessionDetail(params = {}) {
|
|
|
2038
3501
|
const startIndex = Math.max(0, allMessages.length - messageLimit);
|
|
2039
3502
|
const clippedMessages = allMessages.slice(startIndex);
|
|
2040
3503
|
|
|
2041
|
-
return {
|
|
2042
|
-
source,
|
|
2043
|
-
sourceLabel,
|
|
2044
|
-
sessionId,
|
|
3504
|
+
return {
|
|
3505
|
+
source,
|
|
3506
|
+
sourceLabel,
|
|
3507
|
+
sessionId,
|
|
2045
3508
|
cwd: extracted.cwd || '',
|
|
2046
3509
|
updatedAt: extracted.updatedAt || '',
|
|
2047
3510
|
totalMessages: allMessages.length,
|
|
2048
3511
|
clipped: allMessages.length > clippedMessages.length,
|
|
2049
3512
|
messageLimit,
|
|
2050
3513
|
messages: clippedMessages,
|
|
2051
|
-
filePath
|
|
2052
|
-
};
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
async function
|
|
2056
|
-
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
2057
|
-
if (!source) {
|
|
2058
|
-
return { error: 'Invalid source' };
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
2062
|
-
if (!filePath) {
|
|
2063
|
-
return { error: 'Session file not found' };
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
let extracted;
|
|
2067
|
-
try {
|
|
2068
|
-
extracted = await extractMessagesFromFile(filePath, source);
|
|
2069
|
-
} catch (e) {
|
|
2070
|
-
extracted = null;
|
|
2071
|
-
}
|
|
3514
|
+
filePath
|
|
3515
|
+
};
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
async function readSessionPlain(params = {}) {
|
|
3519
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3520
|
+
if (!source) {
|
|
3521
|
+
return { error: 'Invalid source' };
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3525
|
+
if (!filePath) {
|
|
3526
|
+
return { error: 'Session file not found' };
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
let extracted;
|
|
3530
|
+
try {
|
|
3531
|
+
extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
|
|
3532
|
+
} catch (e) {
|
|
3533
|
+
extracted = null;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
if (!extracted) {
|
|
3537
|
+
return { error: 'Failed to parse session file' };
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
3541
|
+
const fallbackRecords = readJsonlRecords(filePath);
|
|
3542
|
+
if (fallbackRecords.length === 0) {
|
|
3543
|
+
return { error: 'Session file is empty' };
|
|
3544
|
+
}
|
|
3545
|
+
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
3549
|
+
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
3550
|
+
const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
3551
|
+
const text = buildSessionPlainText(messages);
|
|
3552
|
+
|
|
3553
|
+
return {
|
|
3554
|
+
source,
|
|
3555
|
+
sourceLabel,
|
|
3556
|
+
sessionId,
|
|
3557
|
+
title: sessionId,
|
|
3558
|
+
filePath,
|
|
3559
|
+
text
|
|
3560
|
+
};
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
async function exportSessionData(params = {}) {
|
|
3564
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3565
|
+
if (!source) {
|
|
3566
|
+
return { error: 'Invalid source' };
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3570
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3571
|
+
if (!filePath) {
|
|
3572
|
+
return { error: 'Session file not found' };
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
let extracted;
|
|
3576
|
+
try {
|
|
3577
|
+
extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
|
|
3578
|
+
} catch (e) {
|
|
3579
|
+
extracted = null;
|
|
3580
|
+
}
|
|
2072
3581
|
|
|
2073
3582
|
if (!extracted) {
|
|
2074
3583
|
return { error: 'Failed to parse session file' };
|
|
@@ -2076,11 +3585,11 @@ async function exportSessionData(params = {}) {
|
|
|
2076
3585
|
|
|
2077
3586
|
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
2078
3587
|
const fallbackRecords = readJsonlRecords(filePath);
|
|
2079
|
-
if (fallbackRecords.length === 0) {
|
|
2080
|
-
return { error: 'Session file is empty' };
|
|
2081
|
-
}
|
|
2082
|
-
extracted = extractMessagesFromRecords(fallbackRecords, source);
|
|
2083
|
-
}
|
|
3588
|
+
if (fallbackRecords.length === 0) {
|
|
3589
|
+
return { error: 'Session file is empty' };
|
|
3590
|
+
}
|
|
3591
|
+
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
3592
|
+
}
|
|
2084
3593
|
|
|
2085
3594
|
extracted.messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
2086
3595
|
|
|
@@ -2092,25 +3601,29 @@ async function exportSessionData(params = {}) {
|
|
|
2092
3601
|
}
|
|
2093
3602
|
|
|
2094
3603
|
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
2095
|
-
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
2096
|
-
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
2097
|
-
const
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
3604
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
3605
|
+
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
3606
|
+
const truncated = !!extracted.truncated;
|
|
3607
|
+
const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
|
|
3608
|
+
const markdown = buildSessionMarkdown({
|
|
3609
|
+
sourceLabel,
|
|
3610
|
+
sessionId,
|
|
3611
|
+
updatedAt: extracted.updatedAt,
|
|
3612
|
+
cwd: extracted.cwd,
|
|
2102
3613
|
filePath,
|
|
2103
3614
|
messages: extracted.messages
|
|
2104
3615
|
});
|
|
2105
3616
|
|
|
2106
|
-
return {
|
|
2107
|
-
source,
|
|
2108
|
-
sourceLabel,
|
|
2109
|
-
sessionId,
|
|
2110
|
-
fileName: `${source}-session-${safeSessionId}.md`,
|
|
2111
|
-
content: markdown
|
|
2112
|
-
|
|
2113
|
-
|
|
3617
|
+
return {
|
|
3618
|
+
source,
|
|
3619
|
+
sourceLabel,
|
|
3620
|
+
sessionId,
|
|
3621
|
+
fileName: `${source}-session-${safeSessionId}.md`,
|
|
3622
|
+
content: markdown,
|
|
3623
|
+
truncated,
|
|
3624
|
+
maxMessages: maxMessagesLabel
|
|
3625
|
+
};
|
|
3626
|
+
}
|
|
2114
3627
|
|
|
2115
3628
|
function buildExportPayload(includeKeys) {
|
|
2116
3629
|
const { config } = readConfigOrVirtualDefault();
|
|
@@ -2274,7 +3787,7 @@ function resolveSpeedTestTarget(params) {
|
|
|
2274
3787
|
return { error: 'Missing name or url' };
|
|
2275
3788
|
}
|
|
2276
3789
|
|
|
2277
|
-
function runSpeedTest(targetUrl, apiKey) {
|
|
3790
|
+
function runSpeedTest(targetUrl, apiKey, options = {}) {
|
|
2278
3791
|
return new Promise((resolve) => {
|
|
2279
3792
|
let parsed;
|
|
2280
3793
|
try {
|
|
@@ -2283,6 +3796,10 @@ function runSpeedTest(targetUrl, apiKey) {
|
|
|
2283
3796
|
return resolve({ ok: false, error: 'Invalid URL' });
|
|
2284
3797
|
}
|
|
2285
3798
|
|
|
3799
|
+
const timeoutMs = Number.isFinite(options.timeoutMs)
|
|
3800
|
+
? Math.max(1000, Number(options.timeoutMs))
|
|
3801
|
+
: SPEED_TEST_TIMEOUT_MS;
|
|
3802
|
+
|
|
2286
3803
|
const transport = parsed.protocol === 'https:' ? https : http;
|
|
2287
3804
|
const headers = {
|
|
2288
3805
|
'User-Agent': 'codexmate-speed-test',
|
|
@@ -2292,46 +3809,291 @@ function runSpeedTest(targetUrl, apiKey) {
|
|
|
2292
3809
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
2293
3810
|
}
|
|
2294
3811
|
|
|
2295
|
-
const start = Date.now();
|
|
2296
|
-
const req = transport.request(parsed, { method: 'GET', headers }, (res) => {
|
|
2297
|
-
res.on('data', () => {});
|
|
2298
|
-
res.on('end', () => {
|
|
2299
|
-
resolve({
|
|
2300
|
-
ok: true,
|
|
2301
|
-
status: res.statusCode || 0,
|
|
2302
|
-
durationMs: Date.now() - start
|
|
2303
|
-
});
|
|
2304
|
-
});
|
|
2305
|
-
});
|
|
3812
|
+
const start = Date.now();
|
|
3813
|
+
const req = transport.request(parsed, { method: 'GET', headers }, (res) => {
|
|
3814
|
+
res.on('data', () => {});
|
|
3815
|
+
res.on('end', () => {
|
|
3816
|
+
resolve({
|
|
3817
|
+
ok: true,
|
|
3818
|
+
status: res.statusCode || 0,
|
|
3819
|
+
durationMs: Date.now() - start
|
|
3820
|
+
});
|
|
3821
|
+
});
|
|
3822
|
+
});
|
|
3823
|
+
|
|
3824
|
+
req.setTimeout(timeoutMs, () => {
|
|
3825
|
+
req.destroy(new Error('timeout'));
|
|
3826
|
+
});
|
|
3827
|
+
|
|
3828
|
+
req.on('error', (err) => {
|
|
3829
|
+
resolve({ ok: false, error: err.message, durationMs: Date.now() - start });
|
|
3830
|
+
});
|
|
3831
|
+
|
|
3832
|
+
req.end();
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
// ============================================================================
|
|
3837
|
+
// 命令
|
|
3838
|
+
// ============================================================================
|
|
3839
|
+
|
|
3840
|
+
// 交互式配置向导
|
|
3841
|
+
async function cmdSetup() {
|
|
3842
|
+
console.log('\n交互式配置向导');
|
|
3843
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
3844
|
+
const lineQueue = [];
|
|
3845
|
+
let lineResolver = null;
|
|
3846
|
+
let rlClosed = false;
|
|
3847
|
+
rl.on('line', (line) => {
|
|
3848
|
+
if (lineResolver) {
|
|
3849
|
+
const resolve = lineResolver;
|
|
3850
|
+
lineResolver = null;
|
|
3851
|
+
resolve(line);
|
|
3852
|
+
} else {
|
|
3853
|
+
lineQueue.push(line);
|
|
3854
|
+
}
|
|
3855
|
+
});
|
|
3856
|
+
rl.on('close', () => {
|
|
3857
|
+
rlClosed = true;
|
|
3858
|
+
if (lineResolver) {
|
|
3859
|
+
const resolve = lineResolver;
|
|
3860
|
+
lineResolver = null;
|
|
3861
|
+
resolve('');
|
|
3862
|
+
}
|
|
3863
|
+
});
|
|
3864
|
+
const ask = async (question) => {
|
|
3865
|
+
if (question) {
|
|
3866
|
+
process.stdout.write(question);
|
|
3867
|
+
}
|
|
3868
|
+
if (lineQueue.length > 0) {
|
|
3869
|
+
return lineQueue.shift();
|
|
3870
|
+
}
|
|
3871
|
+
if (rlClosed) {
|
|
3872
|
+
return '';
|
|
3873
|
+
}
|
|
3874
|
+
return await new Promise(resolve => {
|
|
3875
|
+
lineResolver = resolve;
|
|
3876
|
+
});
|
|
3877
|
+
};
|
|
3878
|
+
|
|
3879
|
+
let providerName = '';
|
|
3880
|
+
let baseUrl = '';
|
|
3881
|
+
let apiKey = '';
|
|
3882
|
+
let modelName = '';
|
|
3883
|
+
let isCustomProvider = false;
|
|
3884
|
+
|
|
3885
|
+
try {
|
|
3886
|
+
const { config } = readConfigOrVirtualDefault();
|
|
3887
|
+
const providers = config.model_providers || {};
|
|
3888
|
+
const providerNames = Object.keys(providers);
|
|
3889
|
+
const defaultProvider = config.model_provider || providerNames[0] || '';
|
|
3890
|
+
let availableModels = [];
|
|
3891
|
+
let defaultModel = config.model || '';
|
|
3892
|
+
let modelFetchUnlimited = false;
|
|
3893
|
+
|
|
3894
|
+
while (true) {
|
|
3895
|
+
console.log('\n选择提供商:');
|
|
3896
|
+
if (providerNames.length > 0) {
|
|
3897
|
+
providerNames.forEach((name, index) => {
|
|
3898
|
+
console.log(` ${index + 1}. ${name}`);
|
|
3899
|
+
});
|
|
3900
|
+
console.log(` ${providerNames.length + 1}. 自定义`);
|
|
3901
|
+
} else {
|
|
3902
|
+
console.log(' (暂无提供商,需自定义)');
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3905
|
+
const suffix = defaultProvider ? ` (默认 ${defaultProvider})` : '';
|
|
3906
|
+
const input = (await ask(`请输入序号或名称${suffix}: `)).trim();
|
|
3907
|
+
|
|
3908
|
+
if (!input) {
|
|
3909
|
+
if (defaultProvider) {
|
|
3910
|
+
providerName = defaultProvider;
|
|
3911
|
+
isCustomProvider = false;
|
|
3912
|
+
break;
|
|
3913
|
+
}
|
|
3914
|
+
isCustomProvider = true;
|
|
3915
|
+
break;
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
if (/^\d+$/.test(input)) {
|
|
3919
|
+
const index = parseInt(input, 10);
|
|
3920
|
+
if (index >= 1 && index <= providerNames.length) {
|
|
3921
|
+
providerName = providerNames[index - 1];
|
|
3922
|
+
isCustomProvider = false;
|
|
3923
|
+
break;
|
|
3924
|
+
}
|
|
3925
|
+
if (index === providerNames.length + 1) {
|
|
3926
|
+
isCustomProvider = true;
|
|
3927
|
+
break;
|
|
3928
|
+
}
|
|
3929
|
+
console.log('提示: 序号无效,请重试。');
|
|
3930
|
+
continue;
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
if (providers[input]) {
|
|
3934
|
+
providerName = input;
|
|
3935
|
+
isCustomProvider = false;
|
|
3936
|
+
break;
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
if (isValidProviderName(input)) {
|
|
3940
|
+
providerName = input;
|
|
3941
|
+
isCustomProvider = true;
|
|
3942
|
+
break;
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
console.log('提示: 名称仅支持字母/数字/._-');
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
if (isCustomProvider && !providerName) {
|
|
3949
|
+
while (true) {
|
|
3950
|
+
const nameInput = (await ask('请输入自定义提供商名称(字母/数字/._-): ')).trim();
|
|
3951
|
+
if (!nameInput) {
|
|
3952
|
+
console.log('提示: 名称不能为空。');
|
|
3953
|
+
continue;
|
|
3954
|
+
}
|
|
3955
|
+
if (!isValidProviderName(nameInput)) {
|
|
3956
|
+
console.log('提示: 名称仅支持字母/数字/._-');
|
|
3957
|
+
continue;
|
|
3958
|
+
}
|
|
3959
|
+
providerName = nameInput;
|
|
3960
|
+
break;
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
if (isCustomProvider) {
|
|
3965
|
+
while (true) {
|
|
3966
|
+
const urlInput = (await ask('Base URL: ')).trim();
|
|
3967
|
+
if (!urlInput) {
|
|
3968
|
+
console.log('提示: Base URL 不能为空。');
|
|
3969
|
+
continue;
|
|
3970
|
+
}
|
|
3971
|
+
baseUrl = urlInput;
|
|
3972
|
+
break;
|
|
3973
|
+
}
|
|
3974
|
+
apiKey = (await ask('API Key (可空): ')).trim();
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
let modelFetchError = '';
|
|
3978
|
+
if (providerName) {
|
|
3979
|
+
if (isCustomProvider) {
|
|
3980
|
+
const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
|
|
3981
|
+
if (res.unlimited) {
|
|
3982
|
+
modelFetchUnlimited = true;
|
|
3983
|
+
} else if (res.error) {
|
|
3984
|
+
modelFetchError = res.error;
|
|
3985
|
+
} else {
|
|
3986
|
+
availableModels = res.models || [];
|
|
3987
|
+
}
|
|
3988
|
+
} else {
|
|
3989
|
+
const res = await fetchProviderModels(providerName);
|
|
3990
|
+
if (res.unlimited) {
|
|
3991
|
+
modelFetchUnlimited = true;
|
|
3992
|
+
} else if (res.error) {
|
|
3993
|
+
modelFetchError = res.error;
|
|
3994
|
+
} else {
|
|
3995
|
+
availableModels = res.models || [];
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
if (modelFetchUnlimited) {
|
|
4000
|
+
console.log('提示: 提供商未提供模型列表,视为不限,请手动输入。');
|
|
4001
|
+
} else if (modelFetchError) {
|
|
4002
|
+
console.log(`提示: 获取模型列表失败: ${modelFetchError},请手动输入。`);
|
|
4003
|
+
}
|
|
4004
|
+
if (availableModels.length > 0) {
|
|
4005
|
+
if (!defaultModel || !availableModels.includes(defaultModel)) {
|
|
4006
|
+
defaultModel = availableModels[0];
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
while (true) {
|
|
4011
|
+
console.log('\n选择模型:');
|
|
4012
|
+
if (availableModels.length > 0) {
|
|
4013
|
+
availableModels.forEach((name, index) => {
|
|
4014
|
+
console.log(` ${index + 1}. ${name}`);
|
|
4015
|
+
});
|
|
4016
|
+
} else {
|
|
4017
|
+
console.log(' (暂无模型,将使用自定义输入)');
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
const suffix = defaultModel ? ` (默认 ${defaultModel})` : '';
|
|
4021
|
+
const input = (await ask(`请输入序号或名称${suffix}: `)).trim();
|
|
4022
|
+
|
|
4023
|
+
if (!input) {
|
|
4024
|
+
if (defaultModel) {
|
|
4025
|
+
modelName = defaultModel;
|
|
4026
|
+
break;
|
|
4027
|
+
}
|
|
4028
|
+
console.log('提示: 模型不能为空。');
|
|
4029
|
+
continue;
|
|
4030
|
+
}
|
|
4031
|
+
|
|
4032
|
+
if (/^\d+$/.test(input)) {
|
|
4033
|
+
const index = parseInt(input, 10);
|
|
4034
|
+
if (index >= 1 && index <= availableModels.length) {
|
|
4035
|
+
modelName = availableModels[index - 1];
|
|
4036
|
+
break;
|
|
4037
|
+
}
|
|
4038
|
+
console.log('提示: 序号无效,请重试。');
|
|
4039
|
+
continue;
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
modelName = input;
|
|
4043
|
+
break;
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
console.log('\n即将应用:');
|
|
4047
|
+
console.log(' 提供商:', providerName);
|
|
4048
|
+
if (isCustomProvider) {
|
|
4049
|
+
console.log(' Base URL:', baseUrl);
|
|
4050
|
+
}
|
|
4051
|
+
console.log(' 模型:', modelName);
|
|
2306
4052
|
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
4053
|
+
const confirm = (await ask('确认应用? (Y/n): ')).trim().toLowerCase();
|
|
4054
|
+
if (confirm === 'n' || confirm === 'no') {
|
|
4055
|
+
console.log('已取消');
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
2310
4058
|
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
4059
|
+
if (isCustomProvider) {
|
|
4060
|
+
if (providers[providerName]) {
|
|
4061
|
+
cmdUpdate(providerName, baseUrl, apiKey, true);
|
|
4062
|
+
} else {
|
|
4063
|
+
cmdAdd(providerName, baseUrl, apiKey, true);
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
2314
4066
|
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
4067
|
+
const latestModels = readModels();
|
|
4068
|
+
if (modelName && !latestModels.includes(modelName)) {
|
|
4069
|
+
cmdAddModel(modelName, true);
|
|
4070
|
+
}
|
|
2318
4071
|
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
4072
|
+
cmdSwitch(providerName, true);
|
|
4073
|
+
cmdUseModel(modelName, true);
|
|
4074
|
+
|
|
4075
|
+
console.log('✓ 已应用配置');
|
|
4076
|
+
console.log(' 提供商:', providerName);
|
|
4077
|
+
console.log(' 模型:', modelName);
|
|
4078
|
+
console.log();
|
|
4079
|
+
} catch (e) {
|
|
4080
|
+
console.error('错误:', e.message || e);
|
|
4081
|
+
process.exitCode = 1;
|
|
4082
|
+
} finally {
|
|
4083
|
+
rl.close();
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
2322
4086
|
|
|
2323
4087
|
// 显示当前状态
|
|
2324
4088
|
function cmdStatus() {
|
|
2325
4089
|
const { config, isVirtual } = readConfigOrVirtualDefault();
|
|
2326
4090
|
const current = config.model_provider || '未设置';
|
|
2327
4091
|
const currentModel = config.model || '未设置';
|
|
2328
|
-
const models = readModels();
|
|
2329
|
-
const currentModels = readCurrentModels();
|
|
2330
4092
|
|
|
2331
4093
|
console.log('\n当前状态:');
|
|
2332
4094
|
console.log(' 提供商:', current);
|
|
2333
4095
|
console.log(' 模型:', currentModel);
|
|
2334
|
-
console.log(' 模型列表:
|
|
4096
|
+
console.log(' 模型列表: 接口提供');
|
|
2335
4097
|
if (isVirtual) {
|
|
2336
4098
|
console.log(' 说明: 当前为虚拟默认配置(config.toml 尚未创建)');
|
|
2337
4099
|
}
|
|
@@ -2371,21 +4133,30 @@ function cmdList() {
|
|
|
2371
4133
|
}
|
|
2372
4134
|
|
|
2373
4135
|
// 列出所有模型
|
|
2374
|
-
function cmdModels() {
|
|
2375
|
-
const
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
console.log(
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
4136
|
+
async function cmdModels() {
|
|
4137
|
+
const res = await fetchProviderModels('');
|
|
4138
|
+
if (res.error) {
|
|
4139
|
+
console.error('错误: 获取模型列表失败:', res.error);
|
|
4140
|
+
process.exitCode = 1;
|
|
4141
|
+
return;
|
|
4142
|
+
}
|
|
4143
|
+
if (res.unlimited) {
|
|
4144
|
+
const label = res.provider ? ` (${res.provider})` : '';
|
|
4145
|
+
console.log(`\n可用模型${label}:`);
|
|
4146
|
+
console.log(' (接口未提供,视为不限)');
|
|
4147
|
+
console.log();
|
|
4148
|
+
return;
|
|
4149
|
+
}
|
|
4150
|
+
const models = Array.isArray(res.models) ? res.models : [];
|
|
4151
|
+
const label = res.provider ? ` (${res.provider})` : '';
|
|
4152
|
+
console.log(`\n可用模型${label}:`);
|
|
4153
|
+
if (models.length === 0) {
|
|
4154
|
+
console.log(' (空)');
|
|
4155
|
+
} else {
|
|
4156
|
+
models.forEach((m, i) => {
|
|
4157
|
+
console.log(` ${i + 1}. ${m}`);
|
|
4158
|
+
});
|
|
4159
|
+
}
|
|
2389
4160
|
console.log();
|
|
2390
4161
|
}
|
|
2391
4162
|
|
|
@@ -2430,19 +4201,20 @@ function cmdSwitch(providerName, silent = false) {
|
|
|
2430
4201
|
console.log('✓ 当前模型:', targetModel);
|
|
2431
4202
|
console.log();
|
|
2432
4203
|
}
|
|
4204
|
+
recordRecentConfig(providerName, targetModel);
|
|
2433
4205
|
return targetModel;
|
|
2434
4206
|
}
|
|
2435
4207
|
|
|
2436
4208
|
// 切换模型
|
|
2437
4209
|
function cmdUseModel(modelName, silent = false) {
|
|
4210
|
+
if (!modelName) {
|
|
4211
|
+
if (!silent) console.error('错误: 模型名称必填');
|
|
4212
|
+
throw new Error('模型名称必填');
|
|
4213
|
+
}
|
|
2438
4214
|
const models = readModels();
|
|
2439
4215
|
if (!models.includes(modelName)) {
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
console.log('\n可用的模型:');
|
|
2443
|
-
models.forEach(m => console.log(' -', m));
|
|
2444
|
-
}
|
|
2445
|
-
throw new Error('模型不存在');
|
|
4216
|
+
models.push(modelName);
|
|
4217
|
+
writeModels(models);
|
|
2446
4218
|
}
|
|
2447
4219
|
|
|
2448
4220
|
const config = readConfig();
|
|
@@ -2469,6 +4241,7 @@ function cmdUseModel(modelName, silent = false) {
|
|
|
2469
4241
|
console.log('✓ 已切换模型:', modelName);
|
|
2470
4242
|
console.log();
|
|
2471
4243
|
}
|
|
4244
|
+
recordRecentConfig(currentProvider, modelName);
|
|
2472
4245
|
}
|
|
2473
4246
|
|
|
2474
4247
|
// 添加提供商
|
|
@@ -2682,52 +4455,6 @@ function maskKey(key) {
|
|
|
2682
4455
|
return key.substring(0, 4) + '...' + key.substring(key.length - 4);
|
|
2683
4456
|
}
|
|
2684
4457
|
|
|
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
4458
|
// 应用到 Claude Code settings.json(跨平台)
|
|
2732
4459
|
function applyToClaudeSettings(config = {}) {
|
|
2733
4460
|
try {
|
|
@@ -2751,11 +4478,11 @@ function applyToClaudeSettings(config = {}) {
|
|
|
2751
4478
|
const nextEnv = {
|
|
2752
4479
|
...currentEnv,
|
|
2753
4480
|
ANTHROPIC_API_KEY: apiKey,
|
|
2754
|
-
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
2755
4481
|
ANTHROPIC_BASE_URL: baseUrl,
|
|
2756
|
-
ANTHROPIC_MODEL: model
|
|
2757
|
-
CLAUDE_CODE_USE_KEY: '1'
|
|
4482
|
+
ANTHROPIC_MODEL: model
|
|
2758
4483
|
};
|
|
4484
|
+
delete nextEnv.ANTHROPIC_AUTH_TOKEN;
|
|
4485
|
+
delete nextEnv.CLAUDE_CODE_USE_KEY;
|
|
2759
4486
|
|
|
2760
4487
|
const nextSettings = {
|
|
2761
4488
|
...currentSettings,
|
|
@@ -2772,10 +4499,8 @@ function applyToClaudeSettings(config = {}) {
|
|
|
2772
4499
|
targetPath: CLAUDE_SETTINGS_FILE,
|
|
2773
4500
|
updatedKeys: [
|
|
2774
4501
|
'env.ANTHROPIC_API_KEY',
|
|
2775
|
-
'env.ANTHROPIC_AUTH_TOKEN',
|
|
2776
4502
|
'env.ANTHROPIC_BASE_URL',
|
|
2777
|
-
'env.ANTHROPIC_MODEL'
|
|
2778
|
-
'env.CLAUDE_CODE_USE_KEY'
|
|
4503
|
+
'env.ANTHROPIC_MODEL'
|
|
2779
4504
|
]
|
|
2780
4505
|
};
|
|
2781
4506
|
if (backupPath) {
|
|
@@ -2791,8 +4516,67 @@ function applyToClaudeSettings(config = {}) {
|
|
|
2791
4516
|
}
|
|
2792
4517
|
}
|
|
2793
4518
|
|
|
2794
|
-
|
|
2795
|
-
|
|
4519
|
+
function commandExists(command, args = '') {
|
|
4520
|
+
try {
|
|
4521
|
+
execSync(`${command} ${args}`, { stdio: 'ignore' });
|
|
4522
|
+
return true;
|
|
4523
|
+
} catch (e) {
|
|
4524
|
+
return false;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
const SEVEN_ZIP_PATHS = [
|
|
4529
|
+
'C:\\Program Files\\7-Zip\\7z.exe',
|
|
4530
|
+
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
|
|
4531
|
+
'7z'
|
|
4532
|
+
];
|
|
4533
|
+
|
|
4534
|
+
function findSevenZipExecutable() {
|
|
4535
|
+
for (const candidate of SEVEN_ZIP_PATHS) {
|
|
4536
|
+
try {
|
|
4537
|
+
if (candidate === '7z') {
|
|
4538
|
+
if (commandExists('7z', '--help')) {
|
|
4539
|
+
return '7z';
|
|
4540
|
+
}
|
|
4541
|
+
} else if (fs.existsSync(candidate)) {
|
|
4542
|
+
return candidate;
|
|
4543
|
+
}
|
|
4544
|
+
} catch (e) {}
|
|
4545
|
+
}
|
|
4546
|
+
return null;
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
function resolveZipTool() {
|
|
4550
|
+
const sevenZipExe = findSevenZipExecutable();
|
|
4551
|
+
if (sevenZipExe) {
|
|
4552
|
+
return { type: '7z', cmd: sevenZipExe };
|
|
4553
|
+
}
|
|
4554
|
+
return { type: 'lib', cmd: 'zip-lib' };
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
function resolveUnzipTool() {
|
|
4558
|
+
const sevenZipExe = findSevenZipExecutable();
|
|
4559
|
+
if (sevenZipExe) {
|
|
4560
|
+
return { type: '7z', cmd: sevenZipExe };
|
|
4561
|
+
}
|
|
4562
|
+
return { type: 'lib', cmd: 'zip-lib' };
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
async function zipWithLibrary(absPath, outputPath) {
|
|
4566
|
+
const stat = fs.lstatSync(absPath);
|
|
4567
|
+
if (stat.isDirectory()) {
|
|
4568
|
+
await zipLib.archiveFolder(absPath, outputPath);
|
|
4569
|
+
return;
|
|
4570
|
+
}
|
|
4571
|
+
await zipLib.archiveFile(absPath, outputPath);
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
async function unzipWithLibrary(zipPath, outputDir) {
|
|
4575
|
+
await zipLib.extract(zipPath, outputDir);
|
|
4576
|
+
}
|
|
4577
|
+
|
|
4578
|
+
// 压缩(7-Zip 优先)
|
|
4579
|
+
async function cmdZip(targetPath, options = {}) {
|
|
2796
4580
|
if (!targetPath) {
|
|
2797
4581
|
console.error('用法: codexmate zip <文件或文件夹路径> [--max:压缩级别]');
|
|
2798
4582
|
console.log('\n示例:');
|
|
@@ -2820,58 +4604,43 @@ function cmdZip(targetPath, options = {}) {
|
|
|
2820
4604
|
const outputDir = path.dirname(absPath);
|
|
2821
4605
|
const outputPath = path.join(outputDir, `${baseName}.zip`);
|
|
2822
4606
|
|
|
2823
|
-
|
|
2824
|
-
const sevenZipPaths = [
|
|
2825
|
-
'C:\\Program Files\\7-Zip\\7z.exe',
|
|
2826
|
-
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
|
|
2827
|
-
'7z'
|
|
2828
|
-
];
|
|
2829
|
-
|
|
2830
|
-
let sevenZipExe = null;
|
|
2831
|
-
for (const p of sevenZipPaths) {
|
|
2832
|
-
try {
|
|
2833
|
-
if (p === '7z') {
|
|
2834
|
-
execSync('7z --help', { stdio: 'ignore' });
|
|
2835
|
-
sevenZipExe = '7z';
|
|
2836
|
-
break;
|
|
2837
|
-
} else if (fs.existsSync(p)) {
|
|
2838
|
-
sevenZipExe = p;
|
|
2839
|
-
break;
|
|
2840
|
-
}
|
|
2841
|
-
} catch (e) {}
|
|
2842
|
-
}
|
|
2843
|
-
|
|
2844
|
-
if (!sevenZipExe) {
|
|
2845
|
-
console.error('错误: 未找到 7-Zip,请先安装 7-Zip');
|
|
2846
|
-
console.log('下载地址: https://www.7-zip.org/');
|
|
2847
|
-
process.exit(1);
|
|
2848
|
-
}
|
|
4607
|
+
const zipTool = resolveZipTool();
|
|
2849
4608
|
|
|
2850
4609
|
console.log('\n压缩配置:');
|
|
2851
4610
|
console.log(' 源路径:', absPath);
|
|
2852
4611
|
console.log(' 输出文件:', outputPath);
|
|
2853
4612
|
console.log(' 压缩级别:', compressionLevel);
|
|
2854
|
-
console.log('
|
|
4613
|
+
console.log(' 压缩工具:', zipTool.type === '7z' ? '7-Zip' : 'zip-lib');
|
|
4614
|
+
console.log(' 多线程:', zipTool.type === '7z' ? '启用' : '未启用(JS 库)');
|
|
4615
|
+
if (zipTool.type !== '7z') {
|
|
4616
|
+
console.log(' 提示: JS 库不支持压缩级别,已忽略 --max');
|
|
4617
|
+
}
|
|
2855
4618
|
console.log('\n开始压缩...\n');
|
|
2856
4619
|
|
|
2857
4620
|
try {
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
4621
|
+
if (zipTool.type === '7z') {
|
|
4622
|
+
const cmd = `"${zipTool.cmd}" a -tzip -mmt=on -mx=${compressionLevel} "${outputPath}" "${absPath}"`;
|
|
4623
|
+
const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
|
4624
|
+
const sizeMatch = result.match(/Archive size:\s*(\d+)\s*bytes/);
|
|
4625
|
+
const filesMatch = result.match(/(\d+)\s*files/);
|
|
4626
|
+
|
|
4627
|
+
console.log('✓ 压缩完成!');
|
|
4628
|
+
console.log(' 输出文件:', outputPath);
|
|
4629
|
+
if (sizeMatch) {
|
|
4630
|
+
const sizeBytes = parseInt(sizeMatch[1]);
|
|
4631
|
+
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
|
|
4632
|
+
console.log(' 压缩大小:', sizeMB, 'MB');
|
|
4633
|
+
}
|
|
4634
|
+
if (filesMatch) {
|
|
4635
|
+
console.log(' 文件数量:', filesMatch[1]);
|
|
4636
|
+
}
|
|
4637
|
+
console.log();
|
|
4638
|
+
return;
|
|
4639
|
+
}
|
|
2864
4640
|
|
|
4641
|
+
await zipWithLibrary(absPath, outputPath);
|
|
2865
4642
|
console.log('✓ 压缩完成!');
|
|
2866
4643
|
console.log(' 输出文件:', outputPath);
|
|
2867
|
-
if (sizeMatch) {
|
|
2868
|
-
const sizeBytes = parseInt(sizeMatch[1]);
|
|
2869
|
-
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
|
|
2870
|
-
console.log(' 压缩大小:', sizeMB, 'MB');
|
|
2871
|
-
}
|
|
2872
|
-
if (filesMatch) {
|
|
2873
|
-
console.log(' 文件数量:', filesMatch[1]);
|
|
2874
|
-
}
|
|
2875
4644
|
console.log();
|
|
2876
4645
|
} catch (e) {
|
|
2877
4646
|
console.error('压缩失败:', e.message);
|
|
@@ -2879,8 +4648,8 @@ function cmdZip(targetPath, options = {}) {
|
|
|
2879
4648
|
}
|
|
2880
4649
|
}
|
|
2881
4650
|
|
|
2882
|
-
//
|
|
2883
|
-
function cmdUnzip(zipPath, outputDir) {
|
|
4651
|
+
// 解压(7-Zip 优先)
|
|
4652
|
+
async function cmdUnzip(zipPath, outputDir) {
|
|
2884
4653
|
if (!zipPath) {
|
|
2885
4654
|
console.error('用法: codexmate unzip <zip文件路径> [输出目录]');
|
|
2886
4655
|
console.log('\n示例:');
|
|
@@ -2906,65 +4675,254 @@ function cmdUnzip(zipPath, outputDir) {
|
|
|
2906
4675
|
const defaultOutputDir = path.join(path.dirname(absZipPath), baseName);
|
|
2907
4676
|
const absOutputDir = outputDir ? path.resolve(outputDir) : defaultOutputDir;
|
|
2908
4677
|
|
|
2909
|
-
|
|
2910
|
-
const sevenZipPaths = [
|
|
2911
|
-
'C:\\Program Files\\7-Zip\\7z.exe',
|
|
2912
|
-
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
|
|
2913
|
-
'7z'
|
|
2914
|
-
];
|
|
2915
|
-
|
|
2916
|
-
let sevenZipExe = null;
|
|
2917
|
-
for (const p of sevenZipPaths) {
|
|
2918
|
-
try {
|
|
2919
|
-
if (p === '7z') {
|
|
2920
|
-
execSync('7z --help', { stdio: 'ignore' });
|
|
2921
|
-
sevenZipExe = '7z';
|
|
2922
|
-
break;
|
|
2923
|
-
} else if (fs.existsSync(p)) {
|
|
2924
|
-
sevenZipExe = p;
|
|
2925
|
-
break;
|
|
2926
|
-
}
|
|
2927
|
-
} catch (e) {}
|
|
2928
|
-
}
|
|
2929
|
-
|
|
2930
|
-
if (!sevenZipExe) {
|
|
2931
|
-
console.error('错误: 未找到 7-Zip,请先安装 7-Zip');
|
|
2932
|
-
console.log('下载地址: https://www.7-zip.org/');
|
|
2933
|
-
process.exit(1);
|
|
2934
|
-
}
|
|
4678
|
+
const unzipTool = resolveUnzipTool();
|
|
2935
4679
|
|
|
2936
4680
|
console.log('\n解压配置:');
|
|
2937
4681
|
console.log(' 源文件:', absZipPath);
|
|
2938
4682
|
console.log(' 输出目录:', absOutputDir);
|
|
2939
|
-
console.log('
|
|
4683
|
+
console.log(' 解压工具:', unzipTool.type === '7z' ? '7-Zip' : 'zip-lib');
|
|
4684
|
+
console.log(' 多线程:', unzipTool.type === '7z' ? '启用' : '未启用(JS 库)');
|
|
2940
4685
|
console.log('\n开始解压...\n');
|
|
2941
4686
|
|
|
2942
4687
|
try {
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
4688
|
+
if (unzipTool.type === '7z') {
|
|
4689
|
+
const cmd = `"${unzipTool.cmd}" x -mmt=on -o"${absOutputDir}" "${absZipPath}" -y`;
|
|
4690
|
+
const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
|
4691
|
+
const filesMatch = result.match(/(\d+)\s*files/);
|
|
4692
|
+
console.log('✓ 解压完成!');
|
|
4693
|
+
console.log(' 输出目录:', absOutputDir);
|
|
4694
|
+
if (filesMatch) {
|
|
4695
|
+
console.log(' 文件数量:', filesMatch[1]);
|
|
4696
|
+
}
|
|
4697
|
+
console.log();
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
2948
4700
|
|
|
4701
|
+
await unzipWithLibrary(absZipPath, absOutputDir);
|
|
2949
4702
|
console.log('✓ 解压完成!');
|
|
2950
4703
|
console.log(' 输出目录:', absOutputDir);
|
|
2951
|
-
if (filesMatch) {
|
|
2952
|
-
console.log(' 文件数量:', filesMatch[1]);
|
|
2953
|
-
}
|
|
2954
4704
|
console.log();
|
|
2955
4705
|
} catch (e) {
|
|
2956
4706
|
console.error('解压失败:', e.message);
|
|
2957
4707
|
process.exit(1);
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
function resolveExportOutputPath(outputPath, defaultFileName) {
|
|
4712
|
+
const fallback = path.resolve(process.cwd(), defaultFileName);
|
|
4713
|
+
if (typeof outputPath !== 'string' || !outputPath.trim()) {
|
|
4714
|
+
return fallback;
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
const trimmed = outputPath.trim();
|
|
4718
|
+
const resolved = path.resolve(trimmed);
|
|
4719
|
+
const hasTrailingSep = /[\\\/]$/.test(trimmed);
|
|
4720
|
+
if (hasTrailingSep) {
|
|
4721
|
+
ensureDir(resolved);
|
|
4722
|
+
return path.join(resolved, defaultFileName);
|
|
4723
|
+
}
|
|
4724
|
+
|
|
4725
|
+
if (fs.existsSync(resolved)) {
|
|
4726
|
+
try {
|
|
4727
|
+
const stat = fs.statSync(resolved);
|
|
4728
|
+
if (stat.isDirectory()) {
|
|
4729
|
+
return path.join(resolved, defaultFileName);
|
|
4730
|
+
}
|
|
4731
|
+
} catch (e) {}
|
|
4732
|
+
}
|
|
4733
|
+
|
|
4734
|
+
return resolved;
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
function printExportSessionUsage() {
|
|
4738
|
+
console.log('\n用法: codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
4739
|
+
console.log('\n示例:');
|
|
4740
|
+
console.log(' codexmate export-session --source codex --session-id 123456');
|
|
4741
|
+
console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
|
|
4742
|
+
console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4745
|
+
function parseExportSessionArgs(args = []) {
|
|
4746
|
+
const options = {
|
|
4747
|
+
source: '',
|
|
4748
|
+
sessionId: '',
|
|
4749
|
+
filePath: '',
|
|
4750
|
+
output: '',
|
|
4751
|
+
maxMessages: undefined
|
|
4752
|
+
};
|
|
4753
|
+
const errors = [];
|
|
4754
|
+
|
|
4755
|
+
for (let i = 0; i < args.length; i++) {
|
|
4756
|
+
const arg = args[i];
|
|
4757
|
+
if (!arg) continue;
|
|
4758
|
+
|
|
4759
|
+
if (arg.startsWith('--source=')) {
|
|
4760
|
+
options.source = arg.slice('--source='.length);
|
|
4761
|
+
continue;
|
|
4762
|
+
}
|
|
4763
|
+
if (arg === '--source') {
|
|
4764
|
+
options.source = args[i + 1] || '';
|
|
4765
|
+
i += 1;
|
|
4766
|
+
continue;
|
|
4767
|
+
}
|
|
4768
|
+
if (arg.startsWith('--session-id=')) {
|
|
4769
|
+
options.sessionId = arg.slice('--session-id='.length);
|
|
4770
|
+
continue;
|
|
4771
|
+
}
|
|
4772
|
+
if (arg === '--session-id') {
|
|
4773
|
+
options.sessionId = args[i + 1] || '';
|
|
4774
|
+
i += 1;
|
|
4775
|
+
continue;
|
|
4776
|
+
}
|
|
4777
|
+
if (arg.startsWith('--file=')) {
|
|
4778
|
+
options.filePath = arg.slice('--file='.length);
|
|
4779
|
+
continue;
|
|
4780
|
+
}
|
|
4781
|
+
if (arg === '--file') {
|
|
4782
|
+
options.filePath = args[i + 1] || '';
|
|
4783
|
+
i += 1;
|
|
4784
|
+
continue;
|
|
4785
|
+
}
|
|
4786
|
+
if (arg.startsWith('--output=')) {
|
|
4787
|
+
options.output = arg.slice('--output='.length);
|
|
4788
|
+
continue;
|
|
4789
|
+
}
|
|
4790
|
+
if (arg === '--output') {
|
|
4791
|
+
options.output = args[i + 1] || '';
|
|
4792
|
+
i += 1;
|
|
4793
|
+
continue;
|
|
4794
|
+
}
|
|
4795
|
+
if (arg.startsWith('--max-messages=')) {
|
|
4796
|
+
options.maxMessages = arg.slice('--max-messages='.length);
|
|
4797
|
+
continue;
|
|
4798
|
+
}
|
|
4799
|
+
if (arg === '--max-messages') {
|
|
4800
|
+
options.maxMessages = args[i + 1] || '';
|
|
4801
|
+
i += 1;
|
|
4802
|
+
continue;
|
|
4803
|
+
}
|
|
4804
|
+
|
|
4805
|
+
errors.push(`未知参数: ${arg}`);
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
const normalizedSource = options.source.trim().toLowerCase();
|
|
4809
|
+
if (normalizedSource && normalizedSource !== 'codex' && normalizedSource !== 'claude') {
|
|
4810
|
+
errors.push('参数 --source 仅支持 codex 或 claude');
|
|
4811
|
+
}
|
|
4812
|
+
options.source = normalizedSource;
|
|
4813
|
+
|
|
4814
|
+
if (!options.source) {
|
|
4815
|
+
errors.push('缺少 --source');
|
|
4816
|
+
}
|
|
4817
|
+
|
|
4818
|
+
if (!options.sessionId && !options.filePath) {
|
|
4819
|
+
errors.push('必须指定 --session-id 或 --file');
|
|
4820
|
+
}
|
|
4821
|
+
|
|
4822
|
+
if (options.maxMessages !== undefined) {
|
|
4823
|
+
const parsed = parseMaxMessagesValue(options.maxMessages);
|
|
4824
|
+
if (parsed === null) {
|
|
4825
|
+
errors.push('参数 --max-messages 无效');
|
|
4826
|
+
} else {
|
|
4827
|
+
options.maxMessages = parsed === Infinity ? Infinity : Math.max(1, Math.floor(parsed));
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4831
|
+
return {
|
|
4832
|
+
options,
|
|
4833
|
+
error: errors.length > 0 ? errors.join(';') : ''
|
|
4834
|
+
};
|
|
4835
|
+
}
|
|
4836
|
+
|
|
4837
|
+
async function cmdExportSession(args = []) {
|
|
4838
|
+
const parsed = parseExportSessionArgs(args);
|
|
4839
|
+
if (parsed.error) {
|
|
4840
|
+
console.error('错误:', parsed.error);
|
|
4841
|
+
printExportSessionUsage();
|
|
4842
|
+
process.exit(1);
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
const options = parsed.options;
|
|
4846
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
4847
|
+
let result;
|
|
4848
|
+
try {
|
|
4849
|
+
result = await exportSessionData({
|
|
4850
|
+
source: options.source,
|
|
4851
|
+
sessionId: options.sessionId,
|
|
4852
|
+
filePath: options.filePath,
|
|
4853
|
+
maxMessages
|
|
4854
|
+
});
|
|
4855
|
+
} catch (e) {
|
|
4856
|
+
console.error('导出失败:', e.message || e);
|
|
4857
|
+
process.exit(1);
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
if (result && result.error) {
|
|
4861
|
+
console.error('导出失败:', result.error);
|
|
4862
|
+
process.exit(1);
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4865
|
+
const defaultFileName = (result && result.fileName)
|
|
4866
|
+
? result.fileName
|
|
4867
|
+
: `${options.source}-session-${options.sessionId || Date.now()}.md`;
|
|
4868
|
+
const outputPath = resolveExportOutputPath(options.output, defaultFileName);
|
|
4869
|
+
ensureDir(path.dirname(outputPath));
|
|
4870
|
+
fs.writeFileSync(outputPath, (result && result.content) ? result.content : '', 'utf-8');
|
|
4871
|
+
|
|
4872
|
+
console.log('\n✓ 会话已导出:', outputPath);
|
|
4873
|
+
if (result && result.truncated) {
|
|
4874
|
+
const label = maxMessages === Infinity ? 'all' : maxMessages;
|
|
4875
|
+
console.log(`! 已截断: 仅导出前 ${label} 条消息`);
|
|
4876
|
+
console.log(' 可使用 --max-messages=all 导出完整内容');
|
|
4877
|
+
}
|
|
4878
|
+
console.log();
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
function parseStartOptions(args = []) {
|
|
4882
|
+
const options = { host: '' };
|
|
4883
|
+
if (!Array.isArray(args)) {
|
|
4884
|
+
return options;
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
for (let i = 0; i < args.length; i++) {
|
|
4888
|
+
const arg = args[i];
|
|
4889
|
+
if (!arg) continue;
|
|
4890
|
+
if (arg.startsWith('--host=')) {
|
|
4891
|
+
options.host = arg.slice('--host='.length);
|
|
4892
|
+
continue;
|
|
4893
|
+
}
|
|
4894
|
+
if (arg === '--host') {
|
|
4895
|
+
options.host = args[i + 1] || '';
|
|
4896
|
+
i += 1;
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
return options;
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
function isAnyAddressHost(host) {
|
|
4904
|
+
return host === '0.0.0.0' || host === '::';
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
function formatHostForUrl(host) {
|
|
4908
|
+
const value = typeof host === 'string' ? host.trim() : '';
|
|
4909
|
+
if (!value) return '';
|
|
4910
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
4911
|
+
return value;
|
|
4912
|
+
}
|
|
4913
|
+
if (value.includes(':')) {
|
|
4914
|
+
return `[${value}]`;
|
|
4915
|
+
}
|
|
4916
|
+
return value;
|
|
4917
|
+
}
|
|
4918
|
+
|
|
4919
|
+
// 打开 Web UI
|
|
4920
|
+
function cmdStart(options = {}) {
|
|
4921
|
+
const htmlPath = path.join(__dirname, 'web-ui.html');
|
|
4922
|
+
if (!fs.existsSync(htmlPath)) {
|
|
4923
|
+
console.error('错误: web-ui.html 不存在');
|
|
4924
|
+
process.exit(1);
|
|
4925
|
+
}
|
|
2968
4926
|
|
|
2969
4927
|
const server = http.createServer((req, res) => {
|
|
2970
4928
|
if (req.url === '/api') {
|
|
@@ -3004,7 +4962,31 @@ function cmdStart() {
|
|
|
3004
4962
|
};
|
|
3005
4963
|
break;
|
|
3006
4964
|
case 'models':
|
|
3007
|
-
|
|
4965
|
+
{
|
|
4966
|
+
const providerName = params && typeof params.provider === 'string' ? params.provider : '';
|
|
4967
|
+
const res = await fetchProviderModels(providerName);
|
|
4968
|
+
if (res.error) {
|
|
4969
|
+
result = { error: res.error, models: [], source: 'remote' };
|
|
4970
|
+
} else if (res.unlimited) {
|
|
4971
|
+
result = { models: [], source: 'remote', provider: res.provider || '', unlimited: true };
|
|
4972
|
+
} else {
|
|
4973
|
+
result = { models: res.models || [], source: 'remote', provider: res.provider || '' };
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
break;
|
|
4977
|
+
case 'models-by-url':
|
|
4978
|
+
{
|
|
4979
|
+
const baseUrl = params && typeof params.baseUrl === 'string' ? params.baseUrl : '';
|
|
4980
|
+
const apiKey = params && typeof params.apiKey === 'string' ? params.apiKey : '';
|
|
4981
|
+
const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
|
|
4982
|
+
if (res.error) {
|
|
4983
|
+
result = { error: res.error, models: [], source: 'remote' };
|
|
4984
|
+
} else if (res.unlimited) {
|
|
4985
|
+
result = { models: [], source: 'remote', unlimited: true };
|
|
4986
|
+
} else {
|
|
4987
|
+
result = { models: res.models || [], source: 'remote' };
|
|
4988
|
+
}
|
|
4989
|
+
}
|
|
3008
4990
|
break;
|
|
3009
4991
|
case 'get-config-template':
|
|
3010
4992
|
result = getConfigTemplate(params || {});
|
|
@@ -3012,27 +4994,39 @@ function cmdStart() {
|
|
|
3012
4994
|
case 'apply-config-template':
|
|
3013
4995
|
result = applyConfigTemplate(params || {});
|
|
3014
4996
|
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
|
-
|
|
4997
|
+
case 'get-recent-configs':
|
|
4998
|
+
result = { items: readRecentConfigs() };
|
|
4999
|
+
break;
|
|
5000
|
+
case 'config-health-check':
|
|
5001
|
+
result = await buildConfigHealthReport(params || {});
|
|
5002
|
+
break;
|
|
5003
|
+
case 'get-agents-file':
|
|
5004
|
+
result = readAgentsFile(params || {});
|
|
5005
|
+
break;
|
|
5006
|
+
case 'apply-agents-file':
|
|
5007
|
+
result = applyAgentsFile(params || {});
|
|
5008
|
+
break;
|
|
5009
|
+
case 'get-openclaw-config':
|
|
5010
|
+
result = readOpenclawConfigFile();
|
|
5011
|
+
break;
|
|
5012
|
+
case 'apply-openclaw-config':
|
|
5013
|
+
result = applyOpenclawConfig(params || {});
|
|
5014
|
+
break;
|
|
5015
|
+
case 'get-openclaw-agents-file':
|
|
5016
|
+
result = readOpenclawAgentsFile();
|
|
5017
|
+
break;
|
|
5018
|
+
case 'apply-openclaw-agents-file':
|
|
5019
|
+
result = applyOpenclawAgentsFile(params || {});
|
|
5020
|
+
break;
|
|
5021
|
+
case 'get-openclaw-workspace-file':
|
|
5022
|
+
result = readOpenclawWorkspaceFile(params || {});
|
|
5023
|
+
break;
|
|
5024
|
+
case 'apply-openclaw-workspace-file':
|
|
5025
|
+
result = applyOpenclawWorkspaceFile(params || {});
|
|
5026
|
+
break;
|
|
5027
|
+
case 'switch':
|
|
5028
|
+
case 'use':
|
|
5029
|
+
case 'add':
|
|
3036
5030
|
case 'delete':
|
|
3037
5031
|
case 'update':
|
|
3038
5032
|
result = { error: 'Codex 配置改动已切换为模板确认模式,请使用模板编辑器并手动确认应用。' };
|
|
@@ -3048,9 +5042,6 @@ function cmdStart() {
|
|
|
3048
5042
|
case 'apply-claude-config':
|
|
3049
5043
|
result = applyToClaudeSettings(params.config);
|
|
3050
5044
|
break;
|
|
3051
|
-
case 'apply-env':
|
|
3052
|
-
result = applyToSystemEnv(params.config);
|
|
3053
|
-
break;
|
|
3054
5045
|
case 'export-config':
|
|
3055
5046
|
result = {
|
|
3056
5047
|
data: buildExportPayload(!!params.includeKeys)
|
|
@@ -3081,12 +5072,21 @@ function cmdStart() {
|
|
|
3081
5072
|
case 'export-session':
|
|
3082
5073
|
result = await exportSessionData(params);
|
|
3083
5074
|
break;
|
|
3084
|
-
case 'session
|
|
3085
|
-
result = await
|
|
5075
|
+
case 'delete-session':
|
|
5076
|
+
result = await deleteSessionData(params || {});
|
|
3086
5077
|
break;
|
|
3087
|
-
|
|
3088
|
-
result =
|
|
3089
|
-
|
|
5078
|
+
case 'clone-session':
|
|
5079
|
+
result = await cloneCodexSession(params || {});
|
|
5080
|
+
break;
|
|
5081
|
+
case 'session-detail':
|
|
5082
|
+
result = await readSessionDetail(params);
|
|
5083
|
+
break;
|
|
5084
|
+
case 'session-plain':
|
|
5085
|
+
result = await readSessionPlain(params);
|
|
5086
|
+
break;
|
|
5087
|
+
default:
|
|
5088
|
+
result = { error: '未知操作' };
|
|
5089
|
+
}
|
|
3090
5090
|
|
|
3091
5091
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3092
5092
|
res.end(JSON.stringify(result));
|
|
@@ -3102,33 +5102,47 @@ function cmdStart() {
|
|
|
3102
5102
|
}
|
|
3103
5103
|
});
|
|
3104
5104
|
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
5105
|
+
const port = resolveWebPort();
|
|
5106
|
+
const host = resolveWebHost(options);
|
|
5107
|
+
const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
|
|
5108
|
+
const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
|
|
5109
|
+
server.listen(port, host, () => {
|
|
5110
|
+
console.log('\n✓ Web UI 已启动:', openUrl);
|
|
5111
|
+
if (host && host !== openHost) {
|
|
5112
|
+
console.log(' 监听地址:', host);
|
|
5113
|
+
}
|
|
5114
|
+
console.log(' 按 Ctrl+C 退出\n');
|
|
5115
|
+
if (isAnyAddressHost(host)) {
|
|
5116
|
+
console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
|
|
5117
|
+
console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
// 打开浏览器
|
|
5121
|
+
const platform = process.platform;
|
|
5122
|
+
let command;
|
|
5123
|
+
const url = openUrl;
|
|
5124
|
+
|
|
5125
|
+
if (platform === 'win32') {
|
|
5126
|
+
command = `start "" "${url}"`;
|
|
3116
5127
|
} else if (platform === 'darwin') {
|
|
3117
5128
|
command = `open "${url}"`;
|
|
3118
5129
|
} else {
|
|
3119
5130
|
command = `xdg-open "${url}"`;
|
|
3120
5131
|
}
|
|
3121
5132
|
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
5133
|
+
const disableBrowser = process.env.CODEXMATE_NO_BROWSER === '1';
|
|
5134
|
+
if (!disableBrowser) {
|
|
5135
|
+
exec(command, (error) => {
|
|
5136
|
+
if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
|
|
5137
|
+
});
|
|
5138
|
+
}
|
|
3125
5139
|
});
|
|
3126
5140
|
}
|
|
3127
5141
|
|
|
3128
5142
|
// ============================================================================
|
|
3129
5143
|
// 主程序
|
|
3130
5144
|
// ============================================================================
|
|
3131
|
-
function main() {
|
|
5145
|
+
async function main() {
|
|
3132
5146
|
const bootstrap = ensureManagedConfigBootstrap();
|
|
3133
5147
|
if (bootstrap && bootstrap.notice) {
|
|
3134
5148
|
console.log(`\n[Init] ${bootstrap.notice}`);
|
|
@@ -3139,37 +5153,41 @@ function main() {
|
|
|
3139
5153
|
console.log('\nCodex Mate - Codex 提供商管理工具');
|
|
3140
5154
|
console.log('\n用法:');
|
|
3141
5155
|
console.log(' codexmate status 显示当前状态');
|
|
5156
|
+
console.log(' codexmate setup 交互式配置向导');
|
|
3142
5157
|
console.log(' codexmate list 列出所有提供商');
|
|
3143
5158
|
console.log(' codexmate models 列出所有模型');
|
|
3144
5159
|
console.log(' codexmate switch <名称> 切换提供商');
|
|
3145
5160
|
console.log(' codexmate use <模型> 切换模型');
|
|
3146
5161
|
console.log(' codexmate add <名称> <URL> [密钥]');
|
|
3147
5162
|
console.log(' codexmate delete <名称> 删除提供商');
|
|
3148
|
-
console.log(' codexmate add-model <模型> 添加模型');
|
|
3149
|
-
console.log(' codexmate delete-model <模型> 删除模型');
|
|
3150
|
-
console.log(' codexmate start
|
|
3151
|
-
console.log(' codexmate
|
|
3152
|
-
console.log(' codexmate
|
|
3153
|
-
console.log('');
|
|
3154
|
-
|
|
3155
|
-
|
|
5163
|
+
console.log(' codexmate add-model <模型> 添加模型');
|
|
5164
|
+
console.log(' codexmate delete-model <模型> 删除模型');
|
|
5165
|
+
console.log(' codexmate start [--host <HOST>] 启动 Web 界面');
|
|
5166
|
+
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
5167
|
+
console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
|
|
5168
|
+
console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
|
|
5169
|
+
console.log('');
|
|
5170
|
+
process.exit(0);
|
|
5171
|
+
}
|
|
3156
5172
|
|
|
3157
5173
|
const command = args[0];
|
|
3158
5174
|
|
|
3159
5175
|
switch (command) {
|
|
3160
5176
|
case 'status': cmdStatus(); break;
|
|
5177
|
+
case 'setup': await cmdSetup(); break;
|
|
3161
5178
|
case 'list': cmdList(); break;
|
|
3162
|
-
case 'models': cmdModels(); break;
|
|
5179
|
+
case 'models': await cmdModels(); break;
|
|
3163
5180
|
case 'switch': cmdSwitch(args[1]); break;
|
|
3164
5181
|
case 'use': cmdUseModel(args[1]); break;
|
|
3165
5182
|
case 'add': cmdAdd(args[1], args[2], args[3]); break;
|
|
3166
5183
|
case 'delete': cmdDelete(args[1]); break;
|
|
3167
|
-
case 'add-model': cmdAddModel(args[1]); break;
|
|
3168
|
-
case 'delete-model': cmdDeleteModel(args[1]); break;
|
|
3169
|
-
case 'start': cmdStart(); break;
|
|
3170
|
-
case '
|
|
3171
|
-
|
|
3172
|
-
|
|
5184
|
+
case 'add-model': cmdAddModel(args[1]); break;
|
|
5185
|
+
case 'delete-model': cmdDeleteModel(args[1]); break;
|
|
5186
|
+
case 'start': cmdStart(parseStartOptions(args.slice(1))); break;
|
|
5187
|
+
case 'export-session': await cmdExportSession(args.slice(1)); break;
|
|
5188
|
+
case 'zip': {
|
|
5189
|
+
// 解析 --max:N 参数
|
|
5190
|
+
const zipOptions = {};
|
|
3173
5191
|
let targetPath = null;
|
|
3174
5192
|
for (let i = 1; i < args.length; i++) {
|
|
3175
5193
|
const arg = args[i];
|
|
@@ -3179,10 +5197,10 @@ function main() {
|
|
|
3179
5197
|
targetPath = arg;
|
|
3180
5198
|
}
|
|
3181
5199
|
}
|
|
3182
|
-
cmdZip(targetPath, zipOptions);
|
|
5200
|
+
await cmdZip(targetPath, zipOptions);
|
|
3183
5201
|
break;
|
|
3184
5202
|
}
|
|
3185
|
-
case 'unzip': cmdUnzip(args[1], args[2]); break;
|
|
5203
|
+
case 'unzip': await cmdUnzip(args[1], args[2]); break;
|
|
3186
5204
|
default:
|
|
3187
5205
|
console.error('错误: 未知命令:', command);
|
|
3188
5206
|
console.log('运行 "codexmate" 查看帮助');
|
|
@@ -3190,4 +5208,8 @@ function main() {
|
|
|
3190
5208
|
}
|
|
3191
5209
|
}
|
|
3192
5210
|
|
|
3193
|
-
main()
|
|
5211
|
+
main().catch((err) => {
|
|
5212
|
+
console.error('错误:', err && err.message ? err.message : err);
|
|
5213
|
+
process.exit(1);
|
|
5214
|
+
});
|
|
5215
|
+
|