codexmate 0.0.10 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -12
- package/README.zh-CN.md +52 -12
- package/cli.js +3491 -563
- package/{CHANGELOG.md → doc/CHANGELOG.md} +6 -0
- package/{CHANGELOG.zh-CN.md → doc/CHANGELOG.zh-CN.md} +6 -0
- package/lib/mcp-stdio.js +440 -0
- package/package.json +22 -2
- package/res/logo.png +0 -0
- package/web-ui/app.js +1171 -149
- package/web-ui/index.html +1605 -0
- package/web-ui/logic.mjs +21 -21
- package/web-ui/styles.css +3213 -0
- package/web-ui.html +7 -3967
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -27
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -17
- package/.github/workflows/ci.yml +0 -26
- package/.github/workflows/release.yml +0 -159
- package/.planning/.fix-attempts +0 -1
- package/.planning/.lock +0 -6
- package/.planning/.verify-cache.json +0 -14
- package/.planning/CHECKPOINT.json +0 -46
- package/.planning/DESIGN.md +0 -26
- package/.planning/HISTORY.json +0 -124
- package/.planning/PLAN.md +0 -69
- package/.planning/REVIEW.md +0 -41
- package/.planning/STATE.md +0 -12
- package/.planning/STATS.json +0 -13
- package/.planning/VERIFICATION.md +0 -70
- package/.planning/daude-code-plan.md +0 -51
- package/.planning/research/architecture.md +0 -32
- package/.planning/research/conventions.md +0 -36
- package/.planning/task_1-REVIEW.md +0 -29
- package/.planning/task_1-SUMMARY.md +0 -32
- package/.planning/task_2-REVIEW.md +0 -24
- package/.planning/task_2-SUMMARY.md +0 -37
- package/.planning/task_3-REVIEW.md +0 -25
- package/.planning/task_3-SUMMARY.md +0 -31
- package/cmd/publish-npm.cmd +0 -65
- package/tests/e2e/helpers.js +0 -214
- package/tests/e2e/recent-health.e2e.js +0 -142
- package/tests/e2e/run.js +0 -154
- package/tests/e2e/test-claude.js +0 -21
- package/tests/e2e/test-config.js +0 -124
- package/tests/e2e/test-health-speed.js +0 -79
- package/tests/e2e/test-openclaw.js +0 -47
- package/tests/e2e/test-session-search.js +0 -114
- package/tests/e2e/test-sessions.js +0 -69
- package/tests/e2e/test-setup.js +0 -159
- package/tests/unit/run.mjs +0 -29
- package/tests/unit/web-ui-logic.test.mjs +0 -186
package/cli.js
CHANGED
|
@@ -6,9 +6,10 @@ const crypto = require('crypto');
|
|
|
6
6
|
const toml = require('@iarna/toml');
|
|
7
7
|
const JSON5 = require('json5');
|
|
8
8
|
const zipLib = require('zip-lib');
|
|
9
|
-
const { exec, execSync, spawn } = require('child_process');
|
|
9
|
+
const { exec, execSync, spawn, spawnSync } = require('child_process');
|
|
10
10
|
const http = require('http');
|
|
11
11
|
const https = require('https');
|
|
12
|
+
const net = require('net');
|
|
12
13
|
const readline = require('readline');
|
|
13
14
|
const {
|
|
14
15
|
expandHomePath,
|
|
@@ -52,6 +53,7 @@ const {
|
|
|
52
53
|
parseMaxMessagesValue,
|
|
53
54
|
resolveMaxMessagesValue
|
|
54
55
|
} = require('./lib/cli-session-utils');
|
|
56
|
+
const { createMcpStdioServer } = require('./lib/mcp-stdio');
|
|
55
57
|
|
|
56
58
|
const DEFAULT_WEB_PORT = 3737;
|
|
57
59
|
const DEFAULT_WEB_HOST = '127.0.0.1';
|
|
@@ -62,9 +64,12 @@ const DEFAULT_WEB_HOST = '127.0.0.1';
|
|
|
62
64
|
const CONFIG_DIR = path.join(os.homedir(), '.codex');
|
|
63
65
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml');
|
|
64
66
|
const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
|
|
67
|
+
const AUTH_PROFILES_DIR = path.join(CONFIG_DIR, 'auth-profiles');
|
|
68
|
+
const AUTH_REGISTRY_FILE = path.join(AUTH_PROFILES_DIR, 'registry.json');
|
|
65
69
|
const MODELS_FILE = path.join(CONFIG_DIR, 'models.json');
|
|
66
70
|
const CURRENT_MODELS_FILE = path.join(CONFIG_DIR, 'provider-current-models.json');
|
|
67
71
|
const INIT_MARK_FILE = path.join(CONFIG_DIR, 'codexmate-init.json');
|
|
72
|
+
const BUILTIN_PROXY_SETTINGS_FILE = path.join(CONFIG_DIR, 'codexmate-proxy.json');
|
|
68
73
|
const CODEX_SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
|
|
69
74
|
const OPENCLAW_DIR = path.join(os.homedir(), '.openclaw');
|
|
70
75
|
const OPENCLAW_CONFIG_FILE = path.join(OPENCLAW_DIR, 'openclaw.json');
|
|
@@ -74,6 +79,7 @@ const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
|
74
79
|
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
75
80
|
const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
|
|
76
81
|
const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
|
|
82
|
+
const CODEX_BACKUP_NAME = 'codex-config';
|
|
77
83
|
|
|
78
84
|
const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gpt-4'];
|
|
79
85
|
const SPEED_TEST_TIMEOUT_MS = 8000;
|
|
@@ -85,9 +91,9 @@ const MAX_SESSION_DETAIL_MESSAGES = 1000;
|
|
|
85
91
|
const SESSION_TITLE_READ_BYTES = 64 * 1024;
|
|
86
92
|
const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
|
|
87
93
|
const SESSION_LIST_CACHE_TTL_MS = 4000;
|
|
88
|
-
const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
|
|
89
|
-
const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
90
|
-
const DEFAULT_CONTENT_SCAN_LIMIT = 50;
|
|
94
|
+
const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
|
|
95
|
+
const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
96
|
+
const DEFAULT_CONTENT_SCAN_LIMIT = 50;
|
|
91
97
|
const SESSION_SCAN_FACTOR = 4;
|
|
92
98
|
const SESSION_SCAN_MIN_FILES = 800;
|
|
93
99
|
const MAX_SESSION_PATH_LIST_SIZE = 2000;
|
|
@@ -97,6 +103,16 @@ const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
|
|
|
97
103
|
const MODELS_CACHE_MAX_ENTRIES = 50;
|
|
98
104
|
const MODELS_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
99
105
|
const MAX_RECENT_CONFIGS = 3;
|
|
106
|
+
const MAX_UPLOAD_SIZE = 200 * 1024 * 1024;
|
|
107
|
+
const BUILTIN_PROXY_PROVIDER_NAME = 'codexmate-proxy';
|
|
108
|
+
const DEFAULT_BUILTIN_PROXY_SETTINGS = Object.freeze({
|
|
109
|
+
enabled: false,
|
|
110
|
+
host: '127.0.0.1',
|
|
111
|
+
port: 8318,
|
|
112
|
+
provider: '',
|
|
113
|
+
authSource: 'provider',
|
|
114
|
+
timeoutMs: 30000
|
|
115
|
+
});
|
|
100
116
|
const BOOTSTRAP_TEXT_MARKERS = [
|
|
101
117
|
'agents.md instructions',
|
|
102
118
|
'<instructions>',
|
|
@@ -104,6 +120,20 @@ const BOOTSTRAP_TEXT_MARKERS = [
|
|
|
104
120
|
'you are a coding agent',
|
|
105
121
|
'codex cli'
|
|
106
122
|
];
|
|
123
|
+
const CLI_INSTALL_TARGETS = Object.freeze([
|
|
124
|
+
{
|
|
125
|
+
id: 'claude',
|
|
126
|
+
name: 'Claude Code CLI',
|
|
127
|
+
packageName: '@anthropic-ai/claude-code',
|
|
128
|
+
bins: ['claude']
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'codex',
|
|
132
|
+
name: 'Codex CLI',
|
|
133
|
+
packageName: '@openai/codex',
|
|
134
|
+
bins: ['codex']
|
|
135
|
+
}
|
|
136
|
+
]);
|
|
107
137
|
|
|
108
138
|
const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
|
|
109
139
|
const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
|
|
@@ -152,6 +182,29 @@ let g_initNotice = '';
|
|
|
152
182
|
let g_sessionListCache = new Map();
|
|
153
183
|
let g_modelsCache = new Map();
|
|
154
184
|
let g_modelsInFlight = new Map();
|
|
185
|
+
let g_builtinProxyRuntime = null;
|
|
186
|
+
const DEFAULT_LOCAL_PROVIDER_NAME = 'local';
|
|
187
|
+
|
|
188
|
+
function isBuiltinProxyProvider(providerName) {
|
|
189
|
+
return typeof providerName === 'string' && providerName.trim() === BUILTIN_PROXY_PROVIDER_NAME;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isReservedProviderNameForCreation(providerName) {
|
|
193
|
+
return typeof providerName === 'string'
|
|
194
|
+
&& providerName.trim().toLowerCase() === DEFAULT_LOCAL_PROVIDER_NAME;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isDefaultLocalProvider(providerName) {
|
|
198
|
+
return typeof providerName === 'string' && providerName.trim() === DEFAULT_LOCAL_PROVIDER_NAME;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isNonDeletableProvider(providerName) {
|
|
202
|
+
return isBuiltinProxyProvider(providerName) || isDefaultLocalProvider(providerName);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isNonEditableProvider(providerName) {
|
|
206
|
+
return isBuiltinProxyProvider(providerName) || isDefaultLocalProvider(providerName);
|
|
207
|
+
}
|
|
155
208
|
|
|
156
209
|
// ============================================================================
|
|
157
210
|
// 工具函数
|
|
@@ -220,6 +273,310 @@ function updateAuthJson(apiKey) {
|
|
|
220
273
|
fs.writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2), 'utf-8');
|
|
221
274
|
}
|
|
222
275
|
|
|
276
|
+
function isPlainObject(value) {
|
|
277
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizeAuthProfileName(value) {
|
|
281
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
282
|
+
if (!raw) return '';
|
|
283
|
+
const sanitized = raw
|
|
284
|
+
.replace(/[\\\/:*?"<>|]/g, '-')
|
|
285
|
+
.replace(/\s+/g, '-')
|
|
286
|
+
.replace(/^-+|-+$/g, '')
|
|
287
|
+
.slice(0, 120);
|
|
288
|
+
return sanitized;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizeAuthRegistry(raw) {
|
|
292
|
+
const fallback = { version: 1, current: '', items: [] };
|
|
293
|
+
if (!isPlainObject(raw)) return fallback;
|
|
294
|
+
const items = Array.isArray(raw.items)
|
|
295
|
+
? raw.items.filter(item => isPlainObject(item) && typeof item.name === 'string' && item.name.trim())
|
|
296
|
+
: [];
|
|
297
|
+
return {
|
|
298
|
+
version: 1,
|
|
299
|
+
current: typeof raw.current === 'string' ? raw.current.trim() : '',
|
|
300
|
+
items: items.map((item) => ({
|
|
301
|
+
name: normalizeAuthProfileName(item.name) || item.name.trim(),
|
|
302
|
+
fileName: typeof item.fileName === 'string' ? path.basename(item.fileName) : '',
|
|
303
|
+
type: typeof item.type === 'string' ? item.type : '',
|
|
304
|
+
email: typeof item.email === 'string' ? item.email : '',
|
|
305
|
+
accountId: typeof item.accountId === 'string' ? item.accountId : '',
|
|
306
|
+
expired: typeof item.expired === 'string' ? item.expired : '',
|
|
307
|
+
lastRefresh: typeof item.lastRefresh === 'string' ? item.lastRefresh : '',
|
|
308
|
+
updatedAt: typeof item.updatedAt === 'string' ? item.updatedAt : '',
|
|
309
|
+
importedAt: typeof item.importedAt === 'string' ? item.importedAt : '',
|
|
310
|
+
sourceFile: typeof item.sourceFile === 'string' ? item.sourceFile : ''
|
|
311
|
+
}))
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function readAuthRegistry() {
|
|
316
|
+
const parsed = readJsonFile(AUTH_REGISTRY_FILE, null);
|
|
317
|
+
return normalizeAuthRegistry(parsed);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function writeAuthRegistry(registry) {
|
|
321
|
+
writeJsonAtomic(AUTH_REGISTRY_FILE, normalizeAuthRegistry(registry));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function parseAuthProfileJson(rawContent, label = '') {
|
|
325
|
+
let parsed;
|
|
326
|
+
try {
|
|
327
|
+
parsed = JSON.parse(stripUtf8Bom(String(rawContent || '')));
|
|
328
|
+
} catch (e) {
|
|
329
|
+
throw new Error(`认证文件不是有效 JSON${label ? `: ${label}` : ''}`);
|
|
330
|
+
}
|
|
331
|
+
if (!isPlainObject(parsed)) {
|
|
332
|
+
throw new Error('认证文件根节点必须是对象');
|
|
333
|
+
}
|
|
334
|
+
const hasCredential = ['access_token', 'refresh_token', 'id_token', 'OPENAI_API_KEY']
|
|
335
|
+
.some((key) => typeof parsed[key] === 'string' && parsed[key].trim());
|
|
336
|
+
if (!hasCredential) {
|
|
337
|
+
throw new Error('认证文件缺少可用凭据(access_token / refresh_token / id_token / OPENAI_API_KEY)');
|
|
338
|
+
}
|
|
339
|
+
return parsed;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function buildAuthProfileSummary(name, payload, fileName = '') {
|
|
343
|
+
const safePayload = isPlainObject(payload) ? payload : {};
|
|
344
|
+
return {
|
|
345
|
+
name,
|
|
346
|
+
fileName: fileName || `${name}.json`,
|
|
347
|
+
type: typeof safePayload.type === 'string' ? safePayload.type : '',
|
|
348
|
+
email: typeof safePayload.email === 'string' ? safePayload.email : '',
|
|
349
|
+
accountId: typeof safePayload.account_id === 'string'
|
|
350
|
+
? safePayload.account_id
|
|
351
|
+
: (typeof safePayload.accountId === 'string' ? safePayload.accountId : ''),
|
|
352
|
+
expired: typeof safePayload.expired === 'string' ? safePayload.expired : '',
|
|
353
|
+
lastRefresh: typeof safePayload.last_refresh === 'string'
|
|
354
|
+
? safePayload.last_refresh
|
|
355
|
+
: (typeof safePayload.lastRefresh === 'string' ? safePayload.lastRefresh : ''),
|
|
356
|
+
updatedAt: toIsoTime(Date.now())
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function getAuthProfileNameFallback(payload, fallbackName = '') {
|
|
361
|
+
const fromPayload = isPlainObject(payload)
|
|
362
|
+
? (payload.email || payload.account_id || payload.accountId || '')
|
|
363
|
+
: '';
|
|
364
|
+
const fromFallback = typeof fallbackName === 'string' ? fallbackName : '';
|
|
365
|
+
const resolved = normalizeAuthProfileName(fromPayload) || normalizeAuthProfileName(fromFallback);
|
|
366
|
+
if (resolved) return resolved;
|
|
367
|
+
return `auth-${Date.now()}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function listAuthProfilesInfo() {
|
|
371
|
+
const registry = readAuthRegistry();
|
|
372
|
+
return registry.items.map((item) => ({
|
|
373
|
+
...item,
|
|
374
|
+
current: item.name === registry.current
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function upsertAuthProfile(payload, options = {}) {
|
|
379
|
+
const safePayload = parseAuthProfileJson(JSON.stringify(payload || {}));
|
|
380
|
+
const sourceFile = typeof options.sourceFile === 'string' ? options.sourceFile : '';
|
|
381
|
+
const preferredName = normalizeAuthProfileName(options.name || '');
|
|
382
|
+
const profileName = preferredName || getAuthProfileNameFallback(safePayload, sourceFile);
|
|
383
|
+
const fileName = `${profileName}.json`;
|
|
384
|
+
const profilePath = path.join(AUTH_PROFILES_DIR, fileName);
|
|
385
|
+
|
|
386
|
+
ensureDir(AUTH_PROFILES_DIR);
|
|
387
|
+
writeJsonAtomic(profilePath, safePayload);
|
|
388
|
+
|
|
389
|
+
const registry = readAuthRegistry();
|
|
390
|
+
const meta = buildAuthProfileSummary(profileName, safePayload, fileName);
|
|
391
|
+
meta.importedAt = toIsoTime(Date.now());
|
|
392
|
+
meta.sourceFile = sourceFile || '';
|
|
393
|
+
|
|
394
|
+
const idx = registry.items.findIndex((item) => item.name === profileName);
|
|
395
|
+
if (idx >= 0) {
|
|
396
|
+
registry.items[idx] = {
|
|
397
|
+
...registry.items[idx],
|
|
398
|
+
...meta
|
|
399
|
+
};
|
|
400
|
+
} else {
|
|
401
|
+
registry.items.push(meta);
|
|
402
|
+
}
|
|
403
|
+
registry.items.sort((a, b) => a.name.localeCompare(b.name));
|
|
404
|
+
|
|
405
|
+
const shouldActivate = options.activate !== false;
|
|
406
|
+
if (shouldActivate) {
|
|
407
|
+
writeJsonAtomic(AUTH_FILE, safePayload);
|
|
408
|
+
registry.current = profileName;
|
|
409
|
+
}
|
|
410
|
+
writeAuthRegistry(registry);
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
success: true,
|
|
414
|
+
profile: {
|
|
415
|
+
...meta,
|
|
416
|
+
current: shouldActivate ? true : registry.current === profileName
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function importAuthProfileFromFile(filePath, options = {}) {
|
|
422
|
+
const absPath = path.resolve(String(filePath || ''));
|
|
423
|
+
if (!absPath || !fs.existsSync(absPath) || !fs.statSync(absPath).isFile()) {
|
|
424
|
+
throw new Error('认证文件不存在');
|
|
425
|
+
}
|
|
426
|
+
const raw = fs.readFileSync(absPath, 'utf-8');
|
|
427
|
+
const payload = parseAuthProfileJson(raw, path.basename(absPath));
|
|
428
|
+
const fallbackName = path.basename(absPath, path.extname(absPath));
|
|
429
|
+
return upsertAuthProfile(payload, {
|
|
430
|
+
...options,
|
|
431
|
+
sourceFile: absPath,
|
|
432
|
+
name: options.name || fallbackName
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function importAuthProfileFromUpload(payload = {}) {
|
|
437
|
+
const fileBase64 = typeof payload.fileBase64 === 'string' ? payload.fileBase64.trim() : '';
|
|
438
|
+
if (!fileBase64) {
|
|
439
|
+
return { error: '缺少认证文件内容' };
|
|
440
|
+
}
|
|
441
|
+
let buffer;
|
|
442
|
+
try {
|
|
443
|
+
buffer = Buffer.from(fileBase64, 'base64');
|
|
444
|
+
} catch (e) {
|
|
445
|
+
return { error: '认证文件不是有效的 base64 编码' };
|
|
446
|
+
}
|
|
447
|
+
if (!buffer || buffer.length === 0) {
|
|
448
|
+
return { error: '认证文件为空' };
|
|
449
|
+
}
|
|
450
|
+
if (buffer.length > 10 * 1024 * 1024) {
|
|
451
|
+
return { error: '认证文件过大(>10MB)' };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
const raw = buffer.toString('utf-8');
|
|
456
|
+
const profileData = parseAuthProfileJson(raw, payload.fileName || 'upload.json');
|
|
457
|
+
return upsertAuthProfile(profileData, {
|
|
458
|
+
name: payload.name || path.basename(payload.fileName || '', path.extname(payload.fileName || '')),
|
|
459
|
+
sourceFile: payload.fileName || '',
|
|
460
|
+
activate: payload.activate !== false
|
|
461
|
+
});
|
|
462
|
+
} catch (e) {
|
|
463
|
+
return { error: e.message || '导入认证文件失败' };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function switchAuthProfile(name, options = {}) {
|
|
468
|
+
const profileName = normalizeAuthProfileName(name);
|
|
469
|
+
if (!profileName) {
|
|
470
|
+
throw new Error('认证名称不能为空');
|
|
471
|
+
}
|
|
472
|
+
const registry = readAuthRegistry();
|
|
473
|
+
const profile = registry.items.find((item) => item.name === profileName);
|
|
474
|
+
if (!profile) {
|
|
475
|
+
throw new Error(`认证不存在: ${profileName}`);
|
|
476
|
+
}
|
|
477
|
+
const fileName = profile.fileName || `${profileName}.json`;
|
|
478
|
+
const profilePath = path.join(AUTH_PROFILES_DIR, fileName);
|
|
479
|
+
if (!fs.existsSync(profilePath)) {
|
|
480
|
+
throw new Error(`认证文件不存在: ${fileName}`);
|
|
481
|
+
}
|
|
482
|
+
const raw = fs.readFileSync(profilePath, 'utf-8');
|
|
483
|
+
const profileData = parseAuthProfileJson(raw, fileName);
|
|
484
|
+
writeJsonAtomic(AUTH_FILE, profileData);
|
|
485
|
+
|
|
486
|
+
registry.current = profileName;
|
|
487
|
+
const idx = registry.items.findIndex((item) => item.name === profileName);
|
|
488
|
+
if (idx >= 0) {
|
|
489
|
+
registry.items[idx] = {
|
|
490
|
+
...registry.items[idx],
|
|
491
|
+
updatedAt: toIsoTime(Date.now())
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
writeAuthRegistry(registry);
|
|
495
|
+
|
|
496
|
+
if (!options.silent) {
|
|
497
|
+
console.log(`✓ 已切换认证: ${profileName}`);
|
|
498
|
+
if (profile.email) {
|
|
499
|
+
console.log(` 账号: ${profile.email}`);
|
|
500
|
+
}
|
|
501
|
+
console.log();
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
success: true,
|
|
505
|
+
profile: {
|
|
506
|
+
...profile,
|
|
507
|
+
current: true
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function deleteAuthProfile(name) {
|
|
513
|
+
const profileName = normalizeAuthProfileName(name);
|
|
514
|
+
if (!profileName) {
|
|
515
|
+
return { error: '认证名称不能为空' };
|
|
516
|
+
}
|
|
517
|
+
const registry = readAuthRegistry();
|
|
518
|
+
const idx = registry.items.findIndex((item) => item.name === profileName);
|
|
519
|
+
if (idx < 0) {
|
|
520
|
+
return { error: '认证不存在' };
|
|
521
|
+
}
|
|
522
|
+
const profile = registry.items[idx];
|
|
523
|
+
const fileName = profile.fileName || `${profileName}.json`;
|
|
524
|
+
const profilePath = path.join(AUTH_PROFILES_DIR, fileName);
|
|
525
|
+
|
|
526
|
+
if (fs.existsSync(profilePath)) {
|
|
527
|
+
try {
|
|
528
|
+
fs.unlinkSync(profilePath);
|
|
529
|
+
} catch (e) {
|
|
530
|
+
return { error: `删除认证文件失败: ${e.message}` };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
registry.items.splice(idx, 1);
|
|
535
|
+
let switchedTo = '';
|
|
536
|
+
if (registry.current === profileName) {
|
|
537
|
+
if (registry.items.length > 0) {
|
|
538
|
+
const next = registry.items[0];
|
|
539
|
+
try {
|
|
540
|
+
const nextPath = path.join(AUTH_PROFILES_DIR, next.fileName || `${next.name}.json`);
|
|
541
|
+
const raw = fs.readFileSync(nextPath, 'utf-8');
|
|
542
|
+
const nextData = parseAuthProfileJson(raw, next.fileName || `${next.name}.json`);
|
|
543
|
+
writeJsonAtomic(AUTH_FILE, nextData);
|
|
544
|
+
registry.current = next.name;
|
|
545
|
+
switchedTo = next.name;
|
|
546
|
+
} catch (e) {
|
|
547
|
+
registry.current = '';
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
registry.current = '';
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
writeAuthRegistry(registry);
|
|
554
|
+
return {
|
|
555
|
+
success: true,
|
|
556
|
+
switchedTo
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function resolveAuthTokenFromCurrentProfile() {
|
|
561
|
+
const registry = readAuthRegistry();
|
|
562
|
+
if (!registry.current) return '';
|
|
563
|
+
const profile = registry.items.find((item) => item.name === registry.current);
|
|
564
|
+
if (!profile) return '';
|
|
565
|
+
const filePath = path.join(AUTH_PROFILES_DIR, profile.fileName || `${profile.name}.json`);
|
|
566
|
+
if (!fs.existsSync(filePath)) return '';
|
|
567
|
+
try {
|
|
568
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
569
|
+
const payload = parseAuthProfileJson(raw, profile.fileName || `${profile.name}.json`);
|
|
570
|
+
if (typeof payload.access_token === 'string' && payload.access_token.trim()) {
|
|
571
|
+
return payload.access_token.trim();
|
|
572
|
+
}
|
|
573
|
+
if (typeof payload.OPENAI_API_KEY === 'string' && payload.OPENAI_API_KEY.trim()) {
|
|
574
|
+
return payload.OPENAI_API_KEY.trim();
|
|
575
|
+
}
|
|
576
|
+
} catch (e) {}
|
|
577
|
+
return '';
|
|
578
|
+
}
|
|
579
|
+
|
|
223
580
|
function getCodexSessionsDir() {
|
|
224
581
|
const candidates = [];
|
|
225
582
|
const envCodexHome = process.env.CODEX_HOME;
|
|
@@ -1203,6 +1560,21 @@ function applyServiceTierToTemplate(template, serviceTier) {
|
|
|
1203
1560
|
return `service_tier = "fast"\n${content}`;
|
|
1204
1561
|
}
|
|
1205
1562
|
|
|
1563
|
+
function applyReasoningEffortToTemplate(template, reasoningEffort) {
|
|
1564
|
+
let content = typeof template === 'string' ? template : '';
|
|
1565
|
+
const effort = typeof reasoningEffort === 'string' ? reasoningEffort.trim().toLowerCase() : '';
|
|
1566
|
+
if (!effort) {
|
|
1567
|
+
return content;
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
content = content.replace(/^\s*model_reasoning_effort\s*=\s*["'][^"']*["']\s*\n?/gmi, '');
|
|
1571
|
+
if (effort === 'high' || effort === 'xhigh') {
|
|
1572
|
+
content = content.replace(/^\s*\n*/, '');
|
|
1573
|
+
return `model_reasoning_effort = "${effort}"\n${content}`;
|
|
1574
|
+
}
|
|
1575
|
+
return content;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1206
1578
|
function getConfigTemplate(params = {}) {
|
|
1207
1579
|
let content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
|
|
1208
1580
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
@@ -1219,6 +1591,9 @@ function getConfigTemplate(params = {}) {
|
|
|
1219
1591
|
if (typeof params.serviceTier === 'string') {
|
|
1220
1592
|
template = applyServiceTierToTemplate(template, params.serviceTier);
|
|
1221
1593
|
}
|
|
1594
|
+
if (typeof params.reasoningEffort === 'string') {
|
|
1595
|
+
template = applyReasoningEffortToTemplate(template, params.reasoningEffort);
|
|
1596
|
+
}
|
|
1222
1597
|
return {
|
|
1223
1598
|
template
|
|
1224
1599
|
};
|
|
@@ -1273,6 +1648,248 @@ function applyConfigTemplate(params = {}) {
|
|
|
1273
1648
|
return { success: true };
|
|
1274
1649
|
}
|
|
1275
1650
|
|
|
1651
|
+
function addProviderToConfig(params = {}) {
|
|
1652
|
+
const name = typeof params.name === 'string' ? params.name.trim() : '';
|
|
1653
|
+
const url = typeof params.url === 'string' ? params.url.trim() : '';
|
|
1654
|
+
const key = typeof params.key === 'string' ? params.key.trim() : '';
|
|
1655
|
+
const allowManaged = !!params.allowManaged;
|
|
1656
|
+
|
|
1657
|
+
if (!name) return { error: '名称不能为空' };
|
|
1658
|
+
if (!url) return { error: 'URL 不能为空' };
|
|
1659
|
+
if (isReservedProviderNameForCreation(name)) {
|
|
1660
|
+
return { error: 'local provider 为系统保留名称,不可新增' };
|
|
1661
|
+
}
|
|
1662
|
+
if (isBuiltinProxyProvider(name) && !allowManaged) {
|
|
1663
|
+
return { error: '本地代理配置为系统内建项,不可手动添加' };
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
ensureConfigDir();
|
|
1667
|
+
|
|
1668
|
+
let content = '';
|
|
1669
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
1670
|
+
try {
|
|
1671
|
+
content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
1672
|
+
} catch (e) {
|
|
1673
|
+
return { error: `读取 config.toml 失败: ${e.message}` };
|
|
1674
|
+
}
|
|
1675
|
+
} else {
|
|
1676
|
+
content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
if (!content || !content.trim()) {
|
|
1680
|
+
content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
let parsed;
|
|
1684
|
+
try {
|
|
1685
|
+
parsed = toml.parse(content);
|
|
1686
|
+
} catch (e) {
|
|
1687
|
+
return { error: `config.toml 解析失败: ${e.message}` };
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
if (!parsed.model_providers || typeof parsed.model_providers !== 'object') {
|
|
1691
|
+
parsed.model_providers = {};
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
if (parsed.model_providers[name]) {
|
|
1695
|
+
return { error: '提供商已存在' };
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
const escapeTomlString = (value) => String(value || '')
|
|
1699
|
+
.replace(/\\/g, '\\\\')
|
|
1700
|
+
.replace(/"/g, '\\"');
|
|
1701
|
+
|
|
1702
|
+
const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
|
|
1703
|
+
const safeName = escapeTomlString(name);
|
|
1704
|
+
const safeUrl = escapeTomlString(url);
|
|
1705
|
+
const safeKey = escapeTomlString(key);
|
|
1706
|
+
const block = [
|
|
1707
|
+
`[model_providers.${safeName}]`,
|
|
1708
|
+
`name = "${safeName}"`,
|
|
1709
|
+
`base_url = "${safeUrl}"`,
|
|
1710
|
+
`wire_api = "responses"`,
|
|
1711
|
+
`requires_openai_auth = false`,
|
|
1712
|
+
`preferred_auth_method = "${safeKey}"`,
|
|
1713
|
+
`request_max_retries = 4`,
|
|
1714
|
+
`stream_max_retries = 10`,
|
|
1715
|
+
`stream_idle_timeout_ms = 300000`
|
|
1716
|
+
].join(lineEnding);
|
|
1717
|
+
|
|
1718
|
+
const newContent = content.trimEnd() + lineEnding + lineEnding + block + lineEnding;
|
|
1719
|
+
|
|
1720
|
+
try {
|
|
1721
|
+
writeConfig(newContent);
|
|
1722
|
+
} catch (e) {
|
|
1723
|
+
return { error: `写入配置失败: ${e.message}` };
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
return { success: true };
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function updateProviderInConfig(params = {}) {
|
|
1730
|
+
const name = typeof params.name === 'string' ? params.name.trim() : '';
|
|
1731
|
+
const url = typeof params.url === 'string' ? params.url.trim() : '';
|
|
1732
|
+
const key = params.key !== undefined && params.key !== null
|
|
1733
|
+
? String(params.key).trim()
|
|
1734
|
+
: undefined;
|
|
1735
|
+
const allowManaged = !!params.allowManaged;
|
|
1736
|
+
|
|
1737
|
+
if (!name) return { error: '名称不能为空' };
|
|
1738
|
+
if (!url && key === undefined) {
|
|
1739
|
+
return { error: 'URL 或密钥至少填写一项' };
|
|
1740
|
+
}
|
|
1741
|
+
if (isNonEditableProvider(name) && !allowManaged) {
|
|
1742
|
+
if (isDefaultLocalProvider(name)) {
|
|
1743
|
+
return { error: 'local provider 为系统保留项,不可编辑' };
|
|
1744
|
+
}
|
|
1745
|
+
return { error: '本地代理配置为系统内建项,不可编辑' };
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
try {
|
|
1749
|
+
cmdUpdate(name, url || undefined, key, true, { allowManaged });
|
|
1750
|
+
return { success: true };
|
|
1751
|
+
} catch (e) {
|
|
1752
|
+
return { error: e.message || '更新失败' };
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function deleteProviderFromConfig(params = {}) {
|
|
1757
|
+
const name = typeof params.name === 'string' ? params.name.trim() : '';
|
|
1758
|
+
if (!name) return { error: '名称不能为空' };
|
|
1759
|
+
if (isNonDeletableProvider(name)) {
|
|
1760
|
+
if (isDefaultLocalProvider(name)) {
|
|
1761
|
+
return { error: 'local provider 为系统保留项,不可删除' };
|
|
1762
|
+
}
|
|
1763
|
+
return { error: '本地代理配置为系统内建项,不可删除' };
|
|
1764
|
+
}
|
|
1765
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
1766
|
+
return { error: 'config.toml 不存在' };
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
let config;
|
|
1770
|
+
try {
|
|
1771
|
+
config = readConfig();
|
|
1772
|
+
} catch (e) {
|
|
1773
|
+
return { error: `读取配置失败: ${e.message}` };
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
const result = performProviderDeletion(name, { silent: true, config });
|
|
1777
|
+
if (result.error) {
|
|
1778
|
+
return { error: result.error };
|
|
1779
|
+
}
|
|
1780
|
+
return {
|
|
1781
|
+
success: true,
|
|
1782
|
+
switched: !!result.switched,
|
|
1783
|
+
provider: result.provider || '',
|
|
1784
|
+
model: result.model || ''
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function performProviderDeletion(name, options = {}) {
|
|
1789
|
+
const silent = !!options.silent;
|
|
1790
|
+
if (isNonDeletableProvider(name)) {
|
|
1791
|
+
const msg = isDefaultLocalProvider(name)
|
|
1792
|
+
? 'local provider 为系统保留项,不可删除'
|
|
1793
|
+
: '本地代理配置为系统内建项,不可删除';
|
|
1794
|
+
if (!silent) console.error('错误:', msg);
|
|
1795
|
+
return { error: msg };
|
|
1796
|
+
}
|
|
1797
|
+
const config = options.config || readConfig();
|
|
1798
|
+
if (!config.model_providers || !config.model_providers[name]) {
|
|
1799
|
+
const msg = '提供商不存在';
|
|
1800
|
+
if (!silent) console.error('错误:', msg, name);
|
|
1801
|
+
return { error: msg };
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
1805
|
+
const lineEnding = content.includes('\r\n') ? '\r\n' : '\n';
|
|
1806
|
+
const hasBom = content.charCodeAt(0) === 0xFEFF;
|
|
1807
|
+
const safeName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1808
|
+
const sectionRegex = new RegExp(`\\[\\s*model_providers\\s*\\.\\s*(?:"${safeName}"|'${safeName}'|${safeName})\\s*\\]`);
|
|
1809
|
+
|
|
1810
|
+
const remainingProviders = Object.keys(config.model_providers || {}).filter(item => item !== name);
|
|
1811
|
+
if (remainingProviders.length === 0) {
|
|
1812
|
+
const msg = '删除后将没有可用提供商';
|
|
1813
|
+
if (!silent) console.error('错误:', msg);
|
|
1814
|
+
return { error: msg };
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
1818
|
+
const currentModels = readCurrentModels();
|
|
1819
|
+
const models = readModels();
|
|
1820
|
+
const result = { success: true, switched: false, provider: '', model: '' };
|
|
1821
|
+
|
|
1822
|
+
if (currentModels[name]) {
|
|
1823
|
+
delete currentModels[name];
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
let fallbackProvider = currentProvider;
|
|
1827
|
+
let fallbackModel = typeof config.model === 'string' ? config.model.trim() : '';
|
|
1828
|
+
if (currentProvider === name) {
|
|
1829
|
+
fallbackProvider = remainingProviders[0];
|
|
1830
|
+
fallbackModel = currentModels[fallbackProvider]
|
|
1831
|
+
|| (Array.isArray(models) && models.length > 0 ? models[0] : (DEFAULT_MODELS[0] || ''));
|
|
1832
|
+
result.switched = true;
|
|
1833
|
+
result.provider = fallbackProvider;
|
|
1834
|
+
result.model = fallbackModel;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
const upsertTopLevel = (text, key, value) => {
|
|
1838
|
+
if (!value && value !== '') return text;
|
|
1839
|
+
const regex = new RegExp(`^\\s*${key}\\s*=.*$`, 'm');
|
|
1840
|
+
if (regex.test(text)) {
|
|
1841
|
+
return text.replace(regex, `${key} = "${value}"`);
|
|
1842
|
+
}
|
|
1843
|
+
return `${key} = "${value}"${lineEnding}${text}`;
|
|
1844
|
+
};
|
|
1845
|
+
|
|
1846
|
+
let updatedContent = null;
|
|
1847
|
+
const match = content.match(sectionRegex);
|
|
1848
|
+
if (match) {
|
|
1849
|
+
const startIdx = match.index;
|
|
1850
|
+
const rest = content.slice(startIdx + match[0].length);
|
|
1851
|
+
const nextIdx = rest.indexOf('[');
|
|
1852
|
+
const endIdx = nextIdx === -1 ? content.length : (startIdx + match[0].length + nextIdx);
|
|
1853
|
+
|
|
1854
|
+
const removedContent = (content.slice(0, startIdx) + content.slice(endIdx))
|
|
1855
|
+
.replace(/\n{3,}/g, lineEnding + lineEnding);
|
|
1856
|
+
|
|
1857
|
+
updatedContent = removedContent;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
if (updatedContent) {
|
|
1861
|
+
if (result.switched) {
|
|
1862
|
+
updatedContent = upsertTopLevel(updatedContent, 'model_provider', fallbackProvider);
|
|
1863
|
+
updatedContent = upsertTopLevel(updatedContent, 'model', fallbackModel);
|
|
1864
|
+
currentModels[fallbackProvider] = fallbackModel;
|
|
1865
|
+
}
|
|
1866
|
+
} else {
|
|
1867
|
+
// 回退:重建 TOML,保持行尾风格
|
|
1868
|
+
const rebuilt = JSON.parse(JSON.stringify(config));
|
|
1869
|
+
delete rebuilt.model_providers[name];
|
|
1870
|
+
if (result.switched) {
|
|
1871
|
+
rebuilt.model_provider = fallbackProvider;
|
|
1872
|
+
rebuilt.model = fallbackModel;
|
|
1873
|
+
currentModels[fallbackProvider] = fallbackModel;
|
|
1874
|
+
}
|
|
1875
|
+
const hasMarker = content.includes(CODEXMATE_MANAGED_MARKER);
|
|
1876
|
+
let rebuiltToml = toml.stringify(rebuilt).trimEnd();
|
|
1877
|
+
rebuiltToml = rebuiltToml.replace(/\n/g, lineEnding);
|
|
1878
|
+
if (hasMarker && !rebuiltToml.includes(CODEXMATE_MANAGED_MARKER)) {
|
|
1879
|
+
rebuiltToml = `${CODEXMATE_MANAGED_MARKER}${lineEnding}${rebuiltToml}`;
|
|
1880
|
+
}
|
|
1881
|
+
updatedContent = rebuiltToml + lineEnding;
|
|
1882
|
+
if (hasBom && updatedContent.charCodeAt(0) !== 0xFEFF) {
|
|
1883
|
+
updatedContent = '\uFEFF' + updatedContent;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
writeCurrentModels(currentModels);
|
|
1888
|
+
writeConfig(updatedContent.trimEnd() + lineEnding);
|
|
1889
|
+
|
|
1890
|
+
return result;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1276
1893
|
function ensureSupportFiles(defaultProvider, defaultModel) {
|
|
1277
1894
|
if (!fs.existsSync(MODELS_FILE)) {
|
|
1278
1895
|
writeModels([...DEFAULT_MODELS]);
|
|
@@ -1385,6 +2002,30 @@ function ensureManagedConfigBootstrap() {
|
|
|
1385
2002
|
return { notice: g_initNotice };
|
|
1386
2003
|
}
|
|
1387
2004
|
|
|
2005
|
+
function resetConfigToDefault() {
|
|
2006
|
+
ensureConfigDir();
|
|
2007
|
+
const initializedAt = new Date().toISOString();
|
|
2008
|
+
const defaultProvider = 'openai';
|
|
2009
|
+
const defaultModel = DEFAULT_MODELS[0] || 'gpt-4';
|
|
2010
|
+
|
|
2011
|
+
let backupFile = '';
|
|
2012
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
2013
|
+
backupFile = `config.toml.reset-${formatTimestampForFileName(initializedAt)}.bak`;
|
|
2014
|
+
fs.copyFileSync(CONFIG_FILE, path.join(CONFIG_DIR, backupFile));
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
writeConfig(buildDefaultConfigContent(initializedAt));
|
|
2018
|
+
ensureSupportFiles(defaultProvider, defaultModel);
|
|
2019
|
+
writeInitMark({
|
|
2020
|
+
version: 1,
|
|
2021
|
+
initializedAt,
|
|
2022
|
+
mode: 'manual-reset',
|
|
2023
|
+
backupFile
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
return { success: true, backupFile };
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1388
2029
|
function consumeInitNotice() {
|
|
1389
2030
|
const notice = g_initNotice;
|
|
1390
2031
|
g_initNotice = '';
|
|
@@ -1530,17 +2171,17 @@ function isBootstrapLikeText(text) {
|
|
|
1530
2171
|
return BOOTSTRAP_TEXT_MARKERS.some(marker => normalized.includes(marker));
|
|
1531
2172
|
}
|
|
1532
2173
|
|
|
1533
|
-
function removeLeadingSystemMessage(messages) {
|
|
1534
|
-
if (!Array.isArray(messages) || messages.length === 0) {
|
|
1535
|
-
return [];
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
let startIndex = 0;
|
|
1539
|
-
while (startIndex < messages.length) {
|
|
1540
|
-
const item = messages[startIndex];
|
|
1541
|
-
const role = item ? normalizeRole(item.role) : '';
|
|
1542
|
-
const text = item && typeof item.text === 'string' ? item.text : '';
|
|
1543
|
-
const isSystemRole = role === 'system';
|
|
2174
|
+
function removeLeadingSystemMessage(messages) {
|
|
2175
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
2176
|
+
return [];
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
let startIndex = 0;
|
|
2180
|
+
while (startIndex < messages.length) {
|
|
2181
|
+
const item = messages[startIndex];
|
|
2182
|
+
const role = item ? normalizeRole(item.role) : '';
|
|
2183
|
+
const text = item && typeof item.text === 'string' ? item.text : '';
|
|
2184
|
+
const isSystemRole = role === 'system';
|
|
1544
2185
|
const isBootstrapText = isBootstrapLikeText(text);
|
|
1545
2186
|
if (!item || isSystemRole || isBootstrapText) {
|
|
1546
2187
|
startIndex += 1;
|
|
@@ -1627,102 +2268,119 @@ function matchesSessionPathFilter(session, normalizedFilter) {
|
|
|
1627
2268
|
return cwd.includes(normalizedFilter);
|
|
1628
2269
|
}
|
|
1629
2270
|
|
|
1630
|
-
function normalizeQueryTokens(query) {
|
|
1631
|
-
if (typeof query !== 'string') {
|
|
1632
|
-
return [];
|
|
1633
|
-
}
|
|
1634
|
-
return query
|
|
1635
|
-
.split(/\s+/)
|
|
1636
|
-
.map(item => item.trim())
|
|
1637
|
-
.map(item => item.toLowerCase())
|
|
1638
|
-
.filter(Boolean);
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
function expandSessionQueryTokens(tokens) {
|
|
1642
|
-
const base = Array.isArray(tokens) ? tokens.map(t => String(t || '').toLowerCase()).filter(Boolean) : [];
|
|
1643
|
-
const result = [];
|
|
1644
|
-
const seen = new Set();
|
|
1645
|
-
let hasClaudeAlias = false;
|
|
1646
|
-
let hasDaudeAlias = false;
|
|
1647
|
-
|
|
1648
|
-
for (const token of base) {
|
|
1649
|
-
if (/^claude[-_ ]?code$/.test(token) || token === 'claudecode') {
|
|
1650
|
-
hasClaudeAlias = true;
|
|
1651
|
-
continue;
|
|
1652
|
-
}
|
|
1653
|
-
if (/^daude[-_ ]?code$/.test(token) || token === 'daudecode') {
|
|
1654
|
-
hasDaudeAlias = true;
|
|
1655
|
-
continue;
|
|
1656
|
-
}
|
|
1657
|
-
if (!seen.has(token)) {
|
|
1658
|
-
seen.add(token);
|
|
1659
|
-
result.push(token);
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
const push = (token) => {
|
|
1664
|
-
const normalized = String(token || '').toLowerCase();
|
|
1665
|
-
if (!normalized || seen.has(normalized)) return;
|
|
1666
|
-
seen.add(normalized);
|
|
1667
|
-
result.push(normalized);
|
|
1668
|
-
};
|
|
1669
|
-
|
|
1670
|
-
if (hasClaudeAlias) {
|
|
1671
|
-
push('claude');
|
|
1672
|
-
push('code');
|
|
1673
|
-
}
|
|
1674
|
-
if (hasDaudeAlias) {
|
|
1675
|
-
push('daude');
|
|
1676
|
-
push('code');
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
return result;
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
function normalizeKeywords(value) {
|
|
1683
|
-
if (!Array.isArray(value)) {
|
|
1684
|
-
return [];
|
|
1685
|
-
}
|
|
1686
|
-
const seen = new Set();
|
|
1687
|
-
const result = [];
|
|
1688
|
-
for (const item of value) {
|
|
1689
|
-
const normalized = typeof item === 'string' ? item.trim() : String(item || '').trim();
|
|
1690
|
-
if (!normalized) continue;
|
|
1691
|
-
const lower = normalized.toLowerCase();
|
|
1692
|
-
if (seen.has(lower)) continue;
|
|
1693
|
-
seen.add(lower);
|
|
1694
|
-
result.push(normalized);
|
|
1695
|
-
}
|
|
1696
|
-
return result;
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
function normalizeCapabilities(value) {
|
|
1700
|
-
const result = {};
|
|
1701
|
-
if (!value || typeof value !== 'object') {
|
|
1702
|
-
return result;
|
|
1703
|
-
}
|
|
1704
|
-
if (value.code === true) {
|
|
1705
|
-
result.code = true;
|
|
1706
|
-
}
|
|
1707
|
-
return result;
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
function normalizeQueryMode(mode) {
|
|
1711
|
-
return mode === 'or' ? 'or' : 'and';
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
function normalizeQueryScope(scope) {
|
|
1715
|
-
if (scope === 'content' || scope === 'all' || scope === 'summary') {
|
|
1716
|
-
return scope;
|
|
2271
|
+
function normalizeQueryTokens(query) {
|
|
2272
|
+
if (typeof query !== 'string') {
|
|
2273
|
+
return [];
|
|
1717
2274
|
}
|
|
1718
|
-
return
|
|
2275
|
+
return query
|
|
2276
|
+
.split(/\s+/)
|
|
2277
|
+
.map(item => item.trim())
|
|
2278
|
+
.map(item => item.toLowerCase())
|
|
2279
|
+
.filter(Boolean);
|
|
1719
2280
|
}
|
|
1720
2281
|
|
|
1721
|
-
function
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
2282
|
+
function expandSessionQueryTokens(tokens) {
|
|
2283
|
+
const base = Array.isArray(tokens) ? tokens.map(t => String(t || '').toLowerCase()).filter(Boolean) : [];
|
|
2284
|
+
const result = [];
|
|
2285
|
+
const seen = new Set();
|
|
2286
|
+
let hasClaudeAlias = false;
|
|
2287
|
+
let hasDaudeAlias = false;
|
|
2288
|
+
|
|
2289
|
+
// First pass: detect multi-token aliases (e.g., "claude code", "daude code")
|
|
2290
|
+
for (let i = 0; i < base.length; i++) {
|
|
2291
|
+
const token = base[i];
|
|
2292
|
+
const nextToken = base[i + 1] || '';
|
|
2293
|
+
|
|
2294
|
+
// Check for "claude code" pattern (two separate tokens)
|
|
2295
|
+
if (token === 'claude' && nextToken === 'code') {
|
|
2296
|
+
hasClaudeAlias = true;
|
|
2297
|
+
i++; // Skip next token
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2300
|
+
// Check for "daude code" pattern (two separate tokens)
|
|
2301
|
+
if (token === 'daude' && nextToken === 'code') {
|
|
2302
|
+
hasDaudeAlias = true;
|
|
2303
|
+
i++; // Skip next token
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
// Check for combined patterns (e.g., "claude-code", "claude_code", "claudecode")
|
|
2307
|
+
if (/^claude[-_ ]?code$/.test(token) || token === 'claudecode') {
|
|
2308
|
+
hasClaudeAlias = true;
|
|
2309
|
+
continue;
|
|
2310
|
+
}
|
|
2311
|
+
if (/^daude[-_ ]?code$/.test(token) || token === 'daudecode') {
|
|
2312
|
+
hasDaudeAlias = true;
|
|
2313
|
+
continue;
|
|
2314
|
+
}
|
|
2315
|
+
if (!seen.has(token)) {
|
|
2316
|
+
seen.add(token);
|
|
2317
|
+
result.push(token);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
const push = (token) => {
|
|
2322
|
+
const normalized = String(token || '').toLowerCase();
|
|
2323
|
+
if (!normalized || seen.has(normalized)) return;
|
|
2324
|
+
seen.add(normalized);
|
|
2325
|
+
result.push(normalized);
|
|
2326
|
+
};
|
|
2327
|
+
|
|
2328
|
+
if (hasClaudeAlias) {
|
|
2329
|
+
push('claude');
|
|
2330
|
+
push('code');
|
|
2331
|
+
}
|
|
2332
|
+
if (hasDaudeAlias) {
|
|
2333
|
+
push('daude');
|
|
2334
|
+
push('code');
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
return result;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
function normalizeKeywords(value) {
|
|
2341
|
+
if (!Array.isArray(value)) {
|
|
2342
|
+
return [];
|
|
2343
|
+
}
|
|
2344
|
+
const seen = new Set();
|
|
2345
|
+
const result = [];
|
|
2346
|
+
for (const item of value) {
|
|
2347
|
+
const normalized = typeof item === 'string' ? item.trim() : String(item || '').trim();
|
|
2348
|
+
if (!normalized) continue;
|
|
2349
|
+
const lower = normalized.toLowerCase();
|
|
2350
|
+
if (seen.has(lower)) continue;
|
|
2351
|
+
seen.add(lower);
|
|
2352
|
+
result.push(normalized);
|
|
2353
|
+
}
|
|
2354
|
+
return result;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
function normalizeCapabilities(value) {
|
|
2358
|
+
const result = {};
|
|
2359
|
+
if (!value || typeof value !== 'object') {
|
|
2360
|
+
return result;
|
|
2361
|
+
}
|
|
2362
|
+
if (value.code === true) {
|
|
2363
|
+
result.code = true;
|
|
2364
|
+
}
|
|
2365
|
+
return result;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function normalizeQueryMode(mode) {
|
|
2369
|
+
return mode === 'or' ? 'or' : 'and';
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function normalizeQueryScope(scope) {
|
|
2373
|
+
if (scope === 'content' || scope === 'all' || scope === 'summary') {
|
|
2374
|
+
return scope;
|
|
2375
|
+
}
|
|
2376
|
+
return 'summary';
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
function normalizeRoleFilter(roleFilter) {
|
|
2380
|
+
if (roleFilter === 'all' || roleFilter === undefined || roleFilter === null) {
|
|
2381
|
+
return 'all';
|
|
2382
|
+
}
|
|
2383
|
+
const normalized = normalizeRole(String(roleFilter));
|
|
1726
2384
|
return normalized || 'all';
|
|
1727
2385
|
}
|
|
1728
2386
|
|
|
@@ -1740,22 +2398,22 @@ function matchTokensInText(text, tokens, mode = 'and') {
|
|
|
1740
2398
|
return tokens.every(token => haystack.includes(token));
|
|
1741
2399
|
}
|
|
1742
2400
|
|
|
1743
|
-
function buildSessionSummaryText(session) {
|
|
1744
|
-
if (!session) {
|
|
1745
|
-
return '';
|
|
1746
|
-
}
|
|
1747
|
-
const keywords = Array.isArray(session.keywords) ? session.keywords.join(' ') : '';
|
|
1748
|
-
const provider = typeof session.provider === 'string' ? session.provider : '';
|
|
1749
|
-
return [
|
|
1750
|
-
session.title,
|
|
1751
|
-
session.sessionId,
|
|
1752
|
-
session.cwd,
|
|
1753
|
-
session.filePath,
|
|
1754
|
-
session.sourceLabel,
|
|
1755
|
-
provider,
|
|
1756
|
-
keywords
|
|
1757
|
-
].filter(Boolean).join(' ');
|
|
1758
|
-
}
|
|
2401
|
+
function buildSessionSummaryText(session) {
|
|
2402
|
+
if (!session) {
|
|
2403
|
+
return '';
|
|
2404
|
+
}
|
|
2405
|
+
const keywords = Array.isArray(session.keywords) ? session.keywords.join(' ') : '';
|
|
2406
|
+
const provider = typeof session.provider === 'string' ? session.provider : '';
|
|
2407
|
+
return [
|
|
2408
|
+
session.title,
|
|
2409
|
+
session.sessionId,
|
|
2410
|
+
session.cwd,
|
|
2411
|
+
session.filePath,
|
|
2412
|
+
session.sourceLabel,
|
|
2413
|
+
provider,
|
|
2414
|
+
keywords
|
|
2415
|
+
].filter(Boolean).join(' ');
|
|
2416
|
+
}
|
|
1759
2417
|
|
|
1760
2418
|
function extractMessageFromRecord(record, source) {
|
|
1761
2419
|
if (!record) {
|
|
@@ -1865,39 +2523,39 @@ function applySessionQueryFilter(sessions, options = {}) {
|
|
|
1865
2523
|
? Math.max(1024, Number(options.contentScanBytes))
|
|
1866
2524
|
: SESSION_CONTENT_READ_BYTES;
|
|
1867
2525
|
|
|
1868
|
-
let scanned = 0;
|
|
1869
|
-
const results = [];
|
|
1870
|
-
|
|
1871
|
-
for (const session of sessions) {
|
|
1872
|
-
if (scope === 'content' && scanned >= contentScanLimit) {
|
|
2526
|
+
let scanned = 0;
|
|
2527
|
+
const results = [];
|
|
2528
|
+
|
|
2529
|
+
for (const session of sessions) {
|
|
2530
|
+
if (scope === 'content' && scanned >= contentScanLimit) {
|
|
1873
2531
|
break;
|
|
1874
2532
|
}
|
|
1875
|
-
|
|
1876
|
-
const summaryText = buildSessionSummaryText(session);
|
|
1877
|
-
const summaryHit = scope !== 'content' && matchTokensInText(summaryText, tokens, mode);
|
|
1878
|
-
let contentHit = false;
|
|
1879
|
-
let contentInfo = null;
|
|
1880
|
-
|
|
1881
|
-
const shouldScanContent = scope === 'content' || scope === 'all' || !summaryHit;
|
|
1882
|
-
if (shouldScanContent && scanned < contentScanLimit) {
|
|
1883
|
-
scanned += 1;
|
|
1884
|
-
contentInfo = scanSessionContentForQuery(session, tokens, {
|
|
1885
|
-
mode,
|
|
1886
|
-
roleFilter,
|
|
1887
|
-
maxBytes: contentScanBytes,
|
|
1888
|
-
maxMatches: 1,
|
|
1889
|
-
snippetLimit: 2
|
|
1890
|
-
});
|
|
1891
|
-
contentHit = contentInfo.hit;
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
const hit = scope === 'summary'
|
|
1895
|
-
? summaryHit
|
|
1896
|
-
: (scope === 'content' ? contentHit : (summaryHit || contentHit));
|
|
1897
|
-
|
|
1898
|
-
if (!hit) {
|
|
1899
|
-
continue;
|
|
1900
|
-
}
|
|
2533
|
+
|
|
2534
|
+
const summaryText = buildSessionSummaryText(session);
|
|
2535
|
+
const summaryHit = scope !== 'content' && matchTokensInText(summaryText, tokens, mode);
|
|
2536
|
+
let contentHit = false;
|
|
2537
|
+
let contentInfo = null;
|
|
2538
|
+
|
|
2539
|
+
const shouldScanContent = scope === 'content' || scope === 'all' || !summaryHit;
|
|
2540
|
+
if (shouldScanContent && scanned < contentScanLimit) {
|
|
2541
|
+
scanned += 1;
|
|
2542
|
+
contentInfo = scanSessionContentForQuery(session, tokens, {
|
|
2543
|
+
mode,
|
|
2544
|
+
roleFilter,
|
|
2545
|
+
maxBytes: contentScanBytes,
|
|
2546
|
+
maxMatches: 1,
|
|
2547
|
+
snippetLimit: 2
|
|
2548
|
+
});
|
|
2549
|
+
contentHit = contentInfo.hit;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
const hit = scope === 'summary'
|
|
2553
|
+
? summaryHit
|
|
2554
|
+
: (scope === 'content' ? contentHit : (summaryHit || contentHit));
|
|
2555
|
+
|
|
2556
|
+
if (!hit) {
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
1901
2559
|
|
|
1902
2560
|
const matchInfo = contentInfo && contentInfo.hit
|
|
1903
2561
|
? contentInfo
|
|
@@ -2072,26 +2730,26 @@ function parseCodexSessionSummary(filePath) {
|
|
|
2072
2730
|
}
|
|
2073
2731
|
}
|
|
2074
2732
|
|
|
2075
|
-
messageCount = Math.max(0, messageCount);
|
|
2076
|
-
|
|
2077
|
-
return {
|
|
2078
|
-
source: 'codex',
|
|
2079
|
-
sourceLabel: 'Codex',
|
|
2080
|
-
provider: 'codex',
|
|
2081
|
-
sessionId,
|
|
2082
|
-
title: firstPrompt || sessionId,
|
|
2083
|
-
cwd,
|
|
2084
|
-
createdAt,
|
|
2085
|
-
updatedAt,
|
|
2086
|
-
messageCount,
|
|
2087
|
-
filePath,
|
|
2088
|
-
keywords: [],
|
|
2089
|
-
capabilities: {}
|
|
2090
|
-
};
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
function parseClaudeSessionSummary(filePath) {
|
|
2094
|
-
const records = parseJsonlHeadRecords(filePath);
|
|
2733
|
+
messageCount = Math.max(0, messageCount);
|
|
2734
|
+
|
|
2735
|
+
return {
|
|
2736
|
+
source: 'codex',
|
|
2737
|
+
sourceLabel: 'Codex',
|
|
2738
|
+
provider: 'codex',
|
|
2739
|
+
sessionId,
|
|
2740
|
+
title: firstPrompt || sessionId,
|
|
2741
|
+
cwd,
|
|
2742
|
+
createdAt,
|
|
2743
|
+
updatedAt,
|
|
2744
|
+
messageCount,
|
|
2745
|
+
filePath,
|
|
2746
|
+
keywords: [],
|
|
2747
|
+
capabilities: {}
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
function parseClaudeSessionSummary(filePath) {
|
|
2752
|
+
const records = parseJsonlHeadRecords(filePath);
|
|
2095
2753
|
if (records.length === 0) {
|
|
2096
2754
|
return null;
|
|
2097
2755
|
}
|
|
@@ -2161,23 +2819,23 @@ function parseClaudeSessionSummary(filePath) {
|
|
|
2161
2819
|
}
|
|
2162
2820
|
}
|
|
2163
2821
|
|
|
2164
|
-
messageCount = Math.max(0, messageCount);
|
|
2165
|
-
|
|
2166
|
-
return {
|
|
2167
|
-
source: 'claude',
|
|
2168
|
-
sourceLabel: 'Claude Code',
|
|
2169
|
-
provider: 'claude',
|
|
2170
|
-
sessionId,
|
|
2171
|
-
title: firstPrompt || sessionId,
|
|
2172
|
-
cwd,
|
|
2173
|
-
createdAt,
|
|
2174
|
-
updatedAt,
|
|
2175
|
-
messageCount,
|
|
2176
|
-
filePath,
|
|
2177
|
-
keywords: [],
|
|
2178
|
-
capabilities: { code: true }
|
|
2179
|
-
};
|
|
2180
|
-
}
|
|
2822
|
+
messageCount = Math.max(0, messageCount);
|
|
2823
|
+
|
|
2824
|
+
return {
|
|
2825
|
+
source: 'claude',
|
|
2826
|
+
sourceLabel: 'Claude Code',
|
|
2827
|
+
provider: 'claude',
|
|
2828
|
+
sessionId,
|
|
2829
|
+
title: firstPrompt || sessionId,
|
|
2830
|
+
cwd,
|
|
2831
|
+
createdAt,
|
|
2832
|
+
updatedAt,
|
|
2833
|
+
messageCount,
|
|
2834
|
+
filePath,
|
|
2835
|
+
keywords: [],
|
|
2836
|
+
capabilities: { code: true }
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2181
2839
|
|
|
2182
2840
|
function listCodexSessions(limit, options = {}) {
|
|
2183
2841
|
const codexSessionsDir = getCodexSessionsDir();
|
|
@@ -2278,12 +2936,12 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
2278
2936
|
let title = truncateText(entry.summary || entry.firstPrompt || sessionId, 120);
|
|
2279
2937
|
let messageCount = Number.isFinite(entry.messageCount) ? Math.max(0, entry.messageCount - 1) : 0;
|
|
2280
2938
|
|
|
2281
|
-
const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
|
|
2282
|
-
if (quickRecords.length > 0) {
|
|
2283
|
-
const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
|
|
2284
|
-
if (filteredCount > 0 || messageCount === 0) {
|
|
2285
|
-
messageCount = filteredCount;
|
|
2286
|
-
}
|
|
2939
|
+
const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
|
|
2940
|
+
if (quickRecords.length > 0) {
|
|
2941
|
+
const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
|
|
2942
|
+
if (filteredCount > 0 || messageCount === 0) {
|
|
2943
|
+
messageCount = filteredCount;
|
|
2944
|
+
}
|
|
2287
2945
|
|
|
2288
2946
|
const quickMessages = [];
|
|
2289
2947
|
for (const record of quickRecords) {
|
|
@@ -2292,38 +2950,38 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
2292
2950
|
const content = record.message ? record.message.content : '';
|
|
2293
2951
|
quickMessages.push({ role, text: extractMessageText(content) });
|
|
2294
2952
|
}
|
|
2295
|
-
}
|
|
2296
|
-
const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
|
|
2297
|
-
const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
|
|
2298
|
-
if (firstUser) {
|
|
2299
|
-
title = truncateText(firstUser.text, 120);
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
const provider = typeof entry.provider === 'string' && entry.provider.trim()
|
|
2304
|
-
? entry.provider.trim()
|
|
2305
|
-
: 'claude';
|
|
2306
|
-
const keywords = normalizeKeywords(entry.keywords);
|
|
2307
|
-
const capabilities = normalizeCapabilities(entry.capabilities);
|
|
2308
|
-
|
|
2309
|
-
sessions.push({
|
|
2310
|
-
source: 'claude',
|
|
2311
|
-
sourceLabel: 'Claude Code',
|
|
2312
|
-
provider,
|
|
2313
|
-
sessionId,
|
|
2314
|
-
title,
|
|
2315
|
-
cwd: entry.projectPath || index.originalPath || '',
|
|
2316
|
-
createdAt,
|
|
2317
|
-
updatedAt,
|
|
2318
|
-
messageCount,
|
|
2319
|
-
filePath,
|
|
2320
|
-
keywords,
|
|
2321
|
-
capabilities
|
|
2322
|
-
});
|
|
2323
|
-
|
|
2324
|
-
if (sessions.length >= targetCount) {
|
|
2325
|
-
break;
|
|
2326
|
-
}
|
|
2953
|
+
}
|
|
2954
|
+
const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
|
|
2955
|
+
const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
|
|
2956
|
+
if (firstUser) {
|
|
2957
|
+
title = truncateText(firstUser.text, 120);
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
const provider = typeof entry.provider === 'string' && entry.provider.trim()
|
|
2962
|
+
? entry.provider.trim()
|
|
2963
|
+
: 'claude';
|
|
2964
|
+
const keywords = normalizeKeywords(entry.keywords);
|
|
2965
|
+
const capabilities = normalizeCapabilities(entry.capabilities);
|
|
2966
|
+
|
|
2967
|
+
sessions.push({
|
|
2968
|
+
source: 'claude',
|
|
2969
|
+
sourceLabel: 'Claude Code',
|
|
2970
|
+
provider,
|
|
2971
|
+
sessionId,
|
|
2972
|
+
title,
|
|
2973
|
+
cwd: entry.projectPath || index.originalPath || '',
|
|
2974
|
+
createdAt,
|
|
2975
|
+
updatedAt,
|
|
2976
|
+
messageCount,
|
|
2977
|
+
filePath,
|
|
2978
|
+
keywords,
|
|
2979
|
+
capabilities
|
|
2980
|
+
});
|
|
2981
|
+
|
|
2982
|
+
if (sessions.length >= targetCount) {
|
|
2983
|
+
break;
|
|
2984
|
+
}
|
|
2327
2985
|
}
|
|
2328
2986
|
|
|
2329
2987
|
if (sessions.length >= targetCount) {
|
|
@@ -2356,15 +3014,15 @@ function listAllSessions(params = {}) {
|
|
|
2356
3014
|
const source = params.source === 'codex' || params.source === 'claude'
|
|
2357
3015
|
? params.source
|
|
2358
3016
|
: 'all';
|
|
2359
|
-
const rawLimit = Number(params.limit);
|
|
2360
|
-
const limit = Number.isFinite(rawLimit)
|
|
2361
|
-
? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
|
|
2362
|
-
: 120;
|
|
2363
|
-
const forceRefresh = !!params.forceRefresh;
|
|
2364
|
-
const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
|
|
2365
|
-
const hasPathFilter = !!normalizedPathFilter;
|
|
2366
|
-
const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
|
|
2367
|
-
const hasQuery = queryTokens.length > 0;
|
|
3017
|
+
const rawLimit = Number(params.limit);
|
|
3018
|
+
const limit = Number.isFinite(rawLimit)
|
|
3019
|
+
? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
|
|
3020
|
+
: 120;
|
|
3021
|
+
const forceRefresh = !!params.forceRefresh;
|
|
3022
|
+
const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
|
|
3023
|
+
const hasPathFilter = !!normalizedPathFilter;
|
|
3024
|
+
const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
|
|
3025
|
+
const hasQuery = queryTokens.length > 0;
|
|
2368
3026
|
const cacheKey = hasQuery ? '' : `${source}:${limit}:${normalizedPathFilter}`;
|
|
2369
3027
|
if (!hasQuery) {
|
|
2370
3028
|
const cached = getSessionListCache(cacheKey, forceRefresh);
|
|
@@ -2381,16 +3039,16 @@ function listAllSessions(params = {}) {
|
|
|
2381
3039
|
: {};
|
|
2382
3040
|
|
|
2383
3041
|
let sessions = [];
|
|
2384
|
-
if (source === 'all' || source === 'codex') {
|
|
2385
|
-
sessions = sessions.concat(listCodexSessions(limit, scanOptions));
|
|
2386
|
-
}
|
|
2387
|
-
if (source === 'all' || source === 'claude') {
|
|
2388
|
-
sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
if (hasPathFilter) {
|
|
2392
|
-
sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
|
|
2393
|
-
}
|
|
3042
|
+
if (source === 'all' || source === 'codex') {
|
|
3043
|
+
sessions = sessions.concat(listCodexSessions(limit, scanOptions));
|
|
3044
|
+
}
|
|
3045
|
+
if (source === 'all' || source === 'claude') {
|
|
3046
|
+
sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
if (hasPathFilter) {
|
|
3050
|
+
sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
|
|
3051
|
+
}
|
|
2394
3052
|
|
|
2395
3053
|
let result = sessions;
|
|
2396
3054
|
if (hasQuery) {
|
|
@@ -2411,15 +3069,17 @@ function listAllSessions(params = {}) {
|
|
|
2411
3069
|
}
|
|
2412
3070
|
|
|
2413
3071
|
function listSessionPaths(params = {}) {
|
|
2414
|
-
const source = params.source === '
|
|
2415
|
-
|
|
2416
|
-
|
|
3072
|
+
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
3073
|
+
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
3074
|
+
return [];
|
|
3075
|
+
}
|
|
3076
|
+
const validSource = source === 'codex' || source === 'claude' ? source : 'all';
|
|
2417
3077
|
const rawLimit = Number(params.limit);
|
|
2418
3078
|
const limit = Number.isFinite(rawLimit)
|
|
2419
3079
|
? Math.max(1, Math.min(rawLimit, MAX_SESSION_PATH_LIST_SIZE))
|
|
2420
3080
|
: 500;
|
|
2421
3081
|
const forceRefresh = !!params.forceRefresh;
|
|
2422
|
-
const cacheKey = `paths:${
|
|
3082
|
+
const cacheKey = `paths:${validSource}:${limit}`;
|
|
2423
3083
|
const cached = getSessionListCache(cacheKey, forceRefresh);
|
|
2424
3084
|
if (cached) {
|
|
2425
3085
|
return cached;
|
|
@@ -2433,10 +3093,10 @@ function listSessionPaths(params = {}) {
|
|
|
2433
3093
|
};
|
|
2434
3094
|
|
|
2435
3095
|
let sessions = [];
|
|
2436
|
-
if (
|
|
3096
|
+
if (validSource === 'all' || validSource === 'codex') {
|
|
2437
3097
|
sessions = sessions.concat(listCodexSessions(gatherLimit, scanOptions));
|
|
2438
3098
|
}
|
|
2439
|
-
if (
|
|
3099
|
+
if (validSource === 'all' || validSource === 'claude') {
|
|
2440
3100
|
sessions = sessions.concat(listClaudeSessions(gatherLimit, scanOptions));
|
|
2441
3101
|
}
|
|
2442
3102
|
|
|
@@ -2477,15 +3137,15 @@ function resolveSessionFilePath(source, filePath, sessionId) {
|
|
|
2477
3137
|
}
|
|
2478
3138
|
}
|
|
2479
3139
|
|
|
2480
|
-
if (typeof sessionId === 'string' && sessionId.trim()) {
|
|
2481
|
-
const targetId = sessionId.trim().toLowerCase();
|
|
2482
|
-
const files = collectJsonlFiles(root, 5000);
|
|
2483
|
-
const matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId);
|
|
2484
|
-
if (matchedFile && fs.existsSync(matchedFile)) {
|
|
2485
|
-
return matchedFile;
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
|
|
3140
|
+
if (typeof sessionId === 'string' && sessionId.trim()) {
|
|
3141
|
+
const targetId = sessionId.trim().toLowerCase();
|
|
3142
|
+
const files = collectJsonlFiles(root, 5000);
|
|
3143
|
+
const matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId);
|
|
3144
|
+
if (matchedFile && fs.existsSync(matchedFile)) {
|
|
3145
|
+
return matchedFile;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
|
|
2489
3149
|
return '';
|
|
2490
3150
|
}
|
|
2491
3151
|
|
|
@@ -2513,155 +3173,673 @@ function findClaudeSessionIndexPath(sessionFilePath) {
|
|
|
2513
3173
|
return '';
|
|
2514
3174
|
}
|
|
2515
3175
|
|
|
2516
|
-
function
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
3176
|
+
function canListenPort(host, port) {
|
|
3177
|
+
return new Promise((resolve) => {
|
|
3178
|
+
const tester = net.createServer();
|
|
3179
|
+
tester.unref();
|
|
3180
|
+
tester.once('error', () => {
|
|
3181
|
+
resolve(false);
|
|
3182
|
+
});
|
|
3183
|
+
tester.once('listening', () => {
|
|
3184
|
+
tester.close(() => resolve(true));
|
|
3185
|
+
});
|
|
3186
|
+
tester.listen(port, host);
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
async function findAvailablePort(host, startPort, maxAttempts = 20) {
|
|
3191
|
+
const start = parseInt(String(startPort), 10);
|
|
3192
|
+
if (!Number.isFinite(start) || start <= 0) {
|
|
3193
|
+
return 0;
|
|
2523
3194
|
}
|
|
2524
|
-
const
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
if (
|
|
2528
|
-
|
|
2529
|
-
}
|
|
2530
|
-
const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
|
|
2531
|
-
if (sessionId && entrySessionId === sessionId) {
|
|
2532
|
-
return false;
|
|
3195
|
+
const attempts = Number.isFinite(maxAttempts) && maxAttempts > 0 ? maxAttempts : 20;
|
|
3196
|
+
for (let offset = 0; offset < attempts; offset += 1) {
|
|
3197
|
+
const candidate = start + offset;
|
|
3198
|
+
if (candidate > 65535) {
|
|
3199
|
+
break;
|
|
2533
3200
|
}
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
return false;
|
|
2539
|
-
}
|
|
3201
|
+
// eslint-disable-next-line no-await-in-loop
|
|
3202
|
+
const ok = await canListenPort(host, candidate);
|
|
3203
|
+
if (ok) {
|
|
3204
|
+
return candidate;
|
|
2540
3205
|
}
|
|
2541
|
-
return true;
|
|
2542
|
-
});
|
|
2543
|
-
if (filtered.length === index.entries.length) {
|
|
2544
|
-
return;
|
|
2545
3206
|
}
|
|
2546
|
-
|
|
2547
|
-
try {
|
|
2548
|
-
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
|
|
2549
|
-
} catch (e) {}
|
|
3207
|
+
return 0;
|
|
2550
3208
|
}
|
|
2551
3209
|
|
|
2552
|
-
|
|
2553
|
-
const
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
}
|
|
3210
|
+
function normalizeBuiltinProxySettings(raw) {
|
|
3211
|
+
const merged = {
|
|
3212
|
+
...DEFAULT_BUILTIN_PROXY_SETTINGS,
|
|
3213
|
+
...(isPlainObject(raw) ? raw : {})
|
|
3214
|
+
};
|
|
3215
|
+
const host = typeof merged.host === 'string' ? merged.host.trim() : '';
|
|
3216
|
+
const port = parseInt(String(merged.port), 10);
|
|
3217
|
+
const provider = typeof merged.provider === 'string' ? merged.provider.trim() : '';
|
|
3218
|
+
const authSourceRaw = typeof merged.authSource === 'string' ? merged.authSource.trim().toLowerCase() : '';
|
|
3219
|
+
const timeoutMs = parseInt(String(merged.timeoutMs), 10);
|
|
3220
|
+
const authSource = authSourceRaw === 'profile' || authSourceRaw === 'none' ? authSourceRaw : 'provider';
|
|
2557
3221
|
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
3222
|
+
return {
|
|
3223
|
+
enabled: merged.enabled !== false,
|
|
3224
|
+
host: host || DEFAULT_BUILTIN_PROXY_SETTINGS.host,
|
|
3225
|
+
port: Number.isFinite(port) && port > 0 && port <= 65535 ? port : DEFAULT_BUILTIN_PROXY_SETTINGS.port,
|
|
3226
|
+
provider,
|
|
3227
|
+
authSource,
|
|
3228
|
+
timeoutMs: Number.isFinite(timeoutMs) && timeoutMs >= 1000 ? timeoutMs : DEFAULT_BUILTIN_PROXY_SETTINGS.timeoutMs
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
2562
3231
|
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
3232
|
+
function readBuiltinProxySettings() {
|
|
3233
|
+
const parsed = readJsonFile(BUILTIN_PROXY_SETTINGS_FILE, null);
|
|
3234
|
+
return normalizeBuiltinProxySettings(parsed);
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
function resolveBuiltinProxyProviderName(rawProviderName, providers = {}, preferredProvider = '') {
|
|
3238
|
+
const providerMap = providers && isPlainObject(providers) ? providers : {};
|
|
3239
|
+
const providerNames = Object.keys(providerMap)
|
|
3240
|
+
.filter((name) => name && name !== BUILTIN_PROXY_PROVIDER_NAME);
|
|
3241
|
+
const requested = typeof rawProviderName === 'string' ? rawProviderName.trim() : '';
|
|
3242
|
+
if (requested && requested !== BUILTIN_PROXY_PROVIDER_NAME && providerMap[requested]) {
|
|
3243
|
+
return requested;
|
|
3244
|
+
}
|
|
3245
|
+
const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
|
|
3246
|
+
if (preferred && preferred !== BUILTIN_PROXY_PROVIDER_NAME && providerMap[preferred]) {
|
|
3247
|
+
return preferred;
|
|
2568
3248
|
}
|
|
3249
|
+
return providerNames[0] || '';
|
|
3250
|
+
}
|
|
2569
3251
|
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
}
|
|
3252
|
+
function saveBuiltinProxySettings(payload = {}, options = {}) {
|
|
3253
|
+
const current = readBuiltinProxySettings();
|
|
3254
|
+
const merged = normalizeBuiltinProxySettings({
|
|
3255
|
+
...current,
|
|
3256
|
+
...(isPlainObject(payload) ? payload : {})
|
|
3257
|
+
});
|
|
3258
|
+
|
|
3259
|
+
if (!merged.host) {
|
|
3260
|
+
return { error: '代理 host 不能为空' };
|
|
3261
|
+
}
|
|
3262
|
+
if (!Number.isFinite(merged.port) || merged.port <= 0 || merged.port > 65535) {
|
|
3263
|
+
return { error: '代理端口无效(1-65535)' };
|
|
2575
3264
|
}
|
|
2576
3265
|
|
|
2577
|
-
|
|
3266
|
+
const { config } = readConfigOrVirtualDefault();
|
|
3267
|
+
const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
|
|
3268
|
+
const preferredProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
3269
|
+
const finalProvider = resolveBuiltinProxyProviderName(merged.provider, providers, preferredProvider);
|
|
3270
|
+
|
|
3271
|
+
const normalized = {
|
|
3272
|
+
...merged,
|
|
3273
|
+
provider: finalProvider
|
|
3274
|
+
};
|
|
3275
|
+
|
|
3276
|
+
if (!options.skipWrite) {
|
|
3277
|
+
writeJsonAtomic(BUILTIN_PROXY_SETTINGS_FILE, normalized);
|
|
3278
|
+
}
|
|
2578
3279
|
|
|
2579
3280
|
return {
|
|
2580
3281
|
success: true,
|
|
2581
|
-
|
|
2582
|
-
sessionId,
|
|
2583
|
-
filePath
|
|
3282
|
+
settings: normalized
|
|
2584
3283
|
};
|
|
2585
3284
|
}
|
|
2586
3285
|
|
|
2587
|
-
function
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
}
|
|
2591
|
-
const timePart = Date.now().toString(36);
|
|
2592
|
-
const randomPart = crypto.randomBytes(8).toString('hex');
|
|
2593
|
-
return `clone-${timePart}-${randomPart}`;
|
|
3286
|
+
function buildProxyListenUrl(settings) {
|
|
3287
|
+
const host = formatHostForUrl(settings.host || DEFAULT_BUILTIN_PROXY_SETTINGS.host);
|
|
3288
|
+
return `http://${host}:${settings.port}`;
|
|
2594
3289
|
}
|
|
2595
3290
|
|
|
2596
|
-
function
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
if (!fs.existsSync(filePath)) {
|
|
2601
|
-
return { sessionId, filePath };
|
|
2602
|
-
}
|
|
3291
|
+
function hasCodexConfigReadyForProxy() {
|
|
3292
|
+
const result = readConfigOrVirtualDefault();
|
|
3293
|
+
if (!result || result.isVirtual) {
|
|
3294
|
+
return false;
|
|
2603
3295
|
}
|
|
2604
|
-
const
|
|
2605
|
-
|
|
3296
|
+
const config = result.config || {};
|
|
3297
|
+
if (!isPlainObject(config.model_providers)) {
|
|
3298
|
+
return false;
|
|
3299
|
+
}
|
|
3300
|
+
const providerNames = Object.keys(config.model_providers)
|
|
3301
|
+
.filter((name) => name && name !== BUILTIN_PROXY_PROVIDER_NAME);
|
|
3302
|
+
return providerNames.length > 0;
|
|
2606
3303
|
}
|
|
2607
3304
|
|
|
2608
|
-
function
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
return value;
|
|
3305
|
+
function resolveBuiltinProxyUpstream(settings) {
|
|
3306
|
+
const { config } = readConfigOrVirtualDefault();
|
|
3307
|
+
const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
|
|
3308
|
+
const currentProvider = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
|
|
3309
|
+
const providerName = resolveBuiltinProxyProviderName(settings.provider, providers, currentProvider);
|
|
3310
|
+
if (!providerName) {
|
|
3311
|
+
return { error: '未找到可用的上游 provider,请先添加 provider' };
|
|
2616
3312
|
}
|
|
2617
|
-
if (
|
|
2618
|
-
|
|
2619
|
-
if (Number.isFinite(parsed)) {
|
|
2620
|
-
return parsed;
|
|
2621
|
-
}
|
|
2622
|
-
const numeric = Number(value);
|
|
2623
|
-
if (Number.isFinite(numeric)) {
|
|
2624
|
-
if (numeric > 1e12) return numeric;
|
|
2625
|
-
if (numeric > 1e9) return numeric * 1000;
|
|
2626
|
-
return numeric;
|
|
2627
|
-
}
|
|
3313
|
+
if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
|
|
3314
|
+
return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
|
|
2628
3315
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
async function cloneCodexSession(params = {}) {
|
|
2633
|
-
const source = params.source === 'codex' ? 'codex' : '';
|
|
2634
|
-
if (!source) {
|
|
2635
|
-
return { error: '仅支持 Codex 会话克隆' };
|
|
3316
|
+
const provider = providers[providerName];
|
|
3317
|
+
if (!provider || !isPlainObject(provider)) {
|
|
3318
|
+
return { error: `上游 provider 不存在: ${providerName}` };
|
|
2636
3319
|
}
|
|
2637
3320
|
|
|
2638
|
-
const
|
|
2639
|
-
if (!
|
|
2640
|
-
return { error:
|
|
3321
|
+
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
3322
|
+
if (!baseUrl || !isValidHttpUrl(baseUrl)) {
|
|
3323
|
+
return { error: `上游 provider base_url 无效: ${providerName}` };
|
|
2641
3324
|
}
|
|
2642
3325
|
|
|
2643
|
-
let
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
}
|
|
2647
|
-
|
|
3326
|
+
let token = '';
|
|
3327
|
+
if (settings.authSource === 'profile') {
|
|
3328
|
+
token = resolveAuthTokenFromCurrentProfile();
|
|
3329
|
+
} else if (settings.authSource === 'provider') {
|
|
3330
|
+
token = typeof provider.preferred_auth_method === 'string' ? provider.preferred_auth_method.trim() : '';
|
|
3331
|
+
if (!token) {
|
|
3332
|
+
token = resolveAuthTokenFromCurrentProfile();
|
|
3333
|
+
}
|
|
2648
3334
|
}
|
|
2649
3335
|
|
|
2650
|
-
|
|
2651
|
-
|
|
3336
|
+
let authHeader = '';
|
|
3337
|
+
if (token) {
|
|
3338
|
+
authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
|
|
2652
3339
|
}
|
|
2653
3340
|
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
}
|
|
3341
|
+
return {
|
|
3342
|
+
providerName,
|
|
3343
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
3344
|
+
authHeader
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
2659
3347
|
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
3348
|
+
function createBuiltinProxyServer(settings, upstream) {
|
|
3349
|
+
const connections = new Set();
|
|
3350
|
+
const timeoutMs = settings.timeoutMs;
|
|
3351
|
+
|
|
3352
|
+
const server = http.createServer((req, res) => {
|
|
3353
|
+
let parsedIncoming;
|
|
3354
|
+
try {
|
|
3355
|
+
parsedIncoming = new URL(req.url || '/', 'http://localhost');
|
|
3356
|
+
} catch (e) {
|
|
3357
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
3358
|
+
res.end(JSON.stringify({ error: 'invalid request path' }));
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
const incomingPath = parsedIncoming.pathname || '/';
|
|
3363
|
+
if (incomingPath === '/health' || incomingPath === '/status') {
|
|
3364
|
+
const body = JSON.stringify({
|
|
3365
|
+
ok: true,
|
|
3366
|
+
upstreamProvider: upstream.providerName,
|
|
3367
|
+
upstreamBaseUrl: upstream.baseUrl
|
|
3368
|
+
});
|
|
3369
|
+
res.writeHead(200, {
|
|
3370
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
3371
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
3372
|
+
});
|
|
3373
|
+
res.end(body, 'utf-8');
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
if (!(incomingPath === '/v1' || incomingPath.startsWith('/v1/'))) {
|
|
3378
|
+
const body = JSON.stringify({ error: 'proxy only supports /v1/* paths' });
|
|
3379
|
+
res.writeHead(404, {
|
|
3380
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
3381
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
3382
|
+
});
|
|
3383
|
+
res.end(body, 'utf-8');
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
const suffix = incomingPath === '/v1'
|
|
3388
|
+
? ''
|
|
3389
|
+
: incomingPath.replace(/^\/v1\/?/, '');
|
|
3390
|
+
const targetBase = joinApiUrl(upstream.baseUrl, suffix);
|
|
3391
|
+
if (!targetBase) {
|
|
3392
|
+
const body = JSON.stringify({ error: 'failed to build upstream URL' });
|
|
3393
|
+
res.writeHead(500, {
|
|
3394
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
3395
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
3396
|
+
});
|
|
3397
|
+
res.end(body, 'utf-8');
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
|
|
3401
|
+
let targetUrl;
|
|
3402
|
+
try {
|
|
3403
|
+
targetUrl = new URL(targetBase);
|
|
3404
|
+
targetUrl.search = parsedIncoming.search || '';
|
|
3405
|
+
} catch (e) {
|
|
3406
|
+
const body = JSON.stringify({ error: `invalid upstream URL: ${e.message}` });
|
|
3407
|
+
res.writeHead(500, {
|
|
3408
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
3409
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
3410
|
+
});
|
|
3411
|
+
res.end(body, 'utf-8');
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
const requestHeaders = { ...req.headers };
|
|
3416
|
+
delete requestHeaders.host;
|
|
3417
|
+
delete requestHeaders.connection;
|
|
3418
|
+
delete requestHeaders['content-length'];
|
|
3419
|
+
if (upstream.authHeader) {
|
|
3420
|
+
requestHeaders.authorization = upstream.authHeader;
|
|
3421
|
+
}
|
|
3422
|
+
requestHeaders['x-codexmate-proxy'] = '1';
|
|
3423
|
+
if (!requestHeaders['x-forwarded-for'] && req.socket && req.socket.remoteAddress) {
|
|
3424
|
+
requestHeaders['x-forwarded-for'] = req.socket.remoteAddress;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
const transport = targetUrl.protocol === 'https:' ? https : http;
|
|
3428
|
+
const upstreamReq = transport.request({
|
|
3429
|
+
protocol: targetUrl.protocol,
|
|
3430
|
+
hostname: targetUrl.hostname,
|
|
3431
|
+
port: targetUrl.port || (targetUrl.protocol === 'https:' ? 443 : 80),
|
|
3432
|
+
method: req.method || 'GET',
|
|
3433
|
+
path: `${targetUrl.pathname}${targetUrl.search}`,
|
|
3434
|
+
headers: requestHeaders,
|
|
3435
|
+
agent: targetUrl.protocol === 'https:' ? HTTPS_KEEP_ALIVE_AGENT : HTTP_KEEP_ALIVE_AGENT
|
|
3436
|
+
}, (upstreamRes) => {
|
|
3437
|
+
const responseHeaders = { ...upstreamRes.headers };
|
|
3438
|
+
delete responseHeaders.connection;
|
|
3439
|
+
res.writeHead(upstreamRes.statusCode || 502, responseHeaders);
|
|
3440
|
+
upstreamRes.pipe(res);
|
|
3441
|
+
});
|
|
3442
|
+
|
|
3443
|
+
upstreamReq.setTimeout(timeoutMs, () => {
|
|
3444
|
+
upstreamReq.destroy(new Error(`upstream timeout (${timeoutMs}ms)`));
|
|
3445
|
+
});
|
|
3446
|
+
|
|
3447
|
+
upstreamReq.on('error', (err) => {
|
|
3448
|
+
if (res.headersSent) {
|
|
3449
|
+
try { res.destroy(err); } catch (_) {}
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3452
|
+
const body = JSON.stringify({ error: `proxy request failed: ${err.message}` });
|
|
3453
|
+
res.writeHead(502, {
|
|
3454
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
3455
|
+
'Content-Length': Buffer.byteLength(body, 'utf-8')
|
|
3456
|
+
});
|
|
3457
|
+
res.end(body, 'utf-8');
|
|
3458
|
+
});
|
|
3459
|
+
|
|
3460
|
+
req.pipe(upstreamReq);
|
|
3461
|
+
});
|
|
3462
|
+
|
|
3463
|
+
server.on('connection', (socket) => {
|
|
3464
|
+
connections.add(socket);
|
|
3465
|
+
socket.on('close', () => connections.delete(socket));
|
|
3466
|
+
});
|
|
3467
|
+
|
|
3468
|
+
return new Promise((resolve, reject) => {
|
|
3469
|
+
server.once('error', reject);
|
|
3470
|
+
server.listen(settings.port, settings.host, () => {
|
|
3471
|
+
server.removeListener('error', reject);
|
|
3472
|
+
resolve({
|
|
3473
|
+
server,
|
|
3474
|
+
connections,
|
|
3475
|
+
settings,
|
|
3476
|
+
upstream,
|
|
3477
|
+
startedAt: toIsoTime(Date.now()),
|
|
3478
|
+
listenUrl: buildProxyListenUrl(settings)
|
|
3479
|
+
});
|
|
3480
|
+
});
|
|
3481
|
+
});
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
async function startBuiltinProxyRuntime(payload = {}) {
|
|
3485
|
+
if (g_builtinProxyRuntime) {
|
|
3486
|
+
return {
|
|
3487
|
+
error: '内建代理已在运行',
|
|
3488
|
+
runtime: {
|
|
3489
|
+
listenUrl: g_builtinProxyRuntime.listenUrl,
|
|
3490
|
+
upstreamProvider: g_builtinProxyRuntime.upstream.providerName
|
|
3491
|
+
}
|
|
3492
|
+
};
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
const saveResult = saveBuiltinProxySettings(payload);
|
|
3496
|
+
if (saveResult.error) {
|
|
3497
|
+
return { error: saveResult.error };
|
|
3498
|
+
}
|
|
3499
|
+
const settings = saveResult.settings;
|
|
3500
|
+
const upstream = resolveBuiltinProxyUpstream(settings);
|
|
3501
|
+
if (upstream.error) {
|
|
3502
|
+
return { error: upstream.error };
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
try {
|
|
3506
|
+
g_builtinProxyRuntime = await createBuiltinProxyServer(settings, upstream);
|
|
3507
|
+
return {
|
|
3508
|
+
success: true,
|
|
3509
|
+
running: true,
|
|
3510
|
+
listenUrl: g_builtinProxyRuntime.listenUrl,
|
|
3511
|
+
upstreamProvider: upstream.providerName,
|
|
3512
|
+
settings
|
|
3513
|
+
};
|
|
3514
|
+
} catch (e) {
|
|
3515
|
+
return { error: `启动内建代理失败: ${e.message}` };
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
async function stopBuiltinProxyRuntime() {
|
|
3520
|
+
if (!g_builtinProxyRuntime) {
|
|
3521
|
+
return { success: true, running: false };
|
|
3522
|
+
}
|
|
3523
|
+
const runtime = g_builtinProxyRuntime;
|
|
3524
|
+
g_builtinProxyRuntime = null;
|
|
3525
|
+
|
|
3526
|
+
await new Promise((resolve) => {
|
|
3527
|
+
let settled = false;
|
|
3528
|
+
const finish = () => {
|
|
3529
|
+
if (settled) return;
|
|
3530
|
+
settled = true;
|
|
3531
|
+
resolve();
|
|
3532
|
+
};
|
|
3533
|
+
|
|
3534
|
+
runtime.server.close(() => finish());
|
|
3535
|
+
setTimeout(() => finish(), 1000);
|
|
3536
|
+
});
|
|
3537
|
+
|
|
3538
|
+
for (const socket of runtime.connections) {
|
|
3539
|
+
try { socket.destroy(); } catch (_) {}
|
|
3540
|
+
}
|
|
3541
|
+
runtime.connections.clear();
|
|
3542
|
+
|
|
3543
|
+
return {
|
|
3544
|
+
success: true,
|
|
3545
|
+
running: false
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
function getBuiltinProxyStatus() {
|
|
3550
|
+
const settings = readBuiltinProxySettings();
|
|
3551
|
+
return {
|
|
3552
|
+
running: !!g_builtinProxyRuntime,
|
|
3553
|
+
settings,
|
|
3554
|
+
runtime: g_builtinProxyRuntime
|
|
3555
|
+
? {
|
|
3556
|
+
provider: DEFAULT_LOCAL_PROVIDER_NAME,
|
|
3557
|
+
startedAt: g_builtinProxyRuntime.startedAt,
|
|
3558
|
+
listenUrl: g_builtinProxyRuntime.listenUrl,
|
|
3559
|
+
upstreamProvider: g_builtinProxyRuntime.upstream.providerName,
|
|
3560
|
+
upstreamBaseUrl: g_builtinProxyRuntime.upstream.baseUrl
|
|
3561
|
+
}
|
|
3562
|
+
: null
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
function applyBuiltinProxyProvider(params = {}) {
|
|
3567
|
+
const settings = readBuiltinProxySettings();
|
|
3568
|
+
const hostForUrl = formatHostForUrl(settings.host);
|
|
3569
|
+
const baseUrl = `http://${hostForUrl}:${settings.port}`;
|
|
3570
|
+
|
|
3571
|
+
const { config } = readConfigOrVirtualDefault();
|
|
3572
|
+
const providers = config && isPlainObject(config.model_providers) ? config.model_providers : {};
|
|
3573
|
+
const exists = !!providers[BUILTIN_PROXY_PROVIDER_NAME];
|
|
3574
|
+
const saveResult = exists
|
|
3575
|
+
? updateProviderInConfig({
|
|
3576
|
+
name: BUILTIN_PROXY_PROVIDER_NAME,
|
|
3577
|
+
url: baseUrl,
|
|
3578
|
+
key: '',
|
|
3579
|
+
allowManaged: true
|
|
3580
|
+
})
|
|
3581
|
+
: addProviderToConfig({
|
|
3582
|
+
name: BUILTIN_PROXY_PROVIDER_NAME,
|
|
3583
|
+
url: baseUrl,
|
|
3584
|
+
key: '',
|
|
3585
|
+
allowManaged: true
|
|
3586
|
+
});
|
|
3587
|
+
|
|
3588
|
+
if (saveResult && saveResult.error) {
|
|
3589
|
+
return saveResult;
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
const switchToProxy = params.switchToProxy !== false;
|
|
3593
|
+
let targetModel = '';
|
|
3594
|
+
if (switchToProxy) {
|
|
3595
|
+
try {
|
|
3596
|
+
targetModel = cmdSwitch(BUILTIN_PROXY_PROVIDER_NAME, true) || '';
|
|
3597
|
+
} catch (e) {
|
|
3598
|
+
return { error: `写入代理 provider 成功,但切换失败: ${e.message}` };
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
return {
|
|
3603
|
+
success: true,
|
|
3604
|
+
provider: BUILTIN_PROXY_PROVIDER_NAME,
|
|
3605
|
+
baseUrl,
|
|
3606
|
+
switched: switchToProxy,
|
|
3607
|
+
model: targetModel
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
async function ensureBuiltinProxyForCodexDefault(params = {}) {
|
|
3612
|
+
const payload = isPlainObject(params) ? { ...params } : {};
|
|
3613
|
+
const switchToProxy = payload.switchToProxy !== false;
|
|
3614
|
+
delete payload.switchToProxy;
|
|
3615
|
+
payload.enabled = true;
|
|
3616
|
+
|
|
3617
|
+
const saveResult = saveBuiltinProxySettings(payload);
|
|
3618
|
+
if (saveResult.error) {
|
|
3619
|
+
return { error: saveResult.error };
|
|
3620
|
+
}
|
|
3621
|
+
let nextSettings = saveResult.settings;
|
|
3622
|
+
|
|
3623
|
+
let upstreamResult = resolveBuiltinProxyUpstream(nextSettings);
|
|
3624
|
+
if (upstreamResult.error) {
|
|
3625
|
+
return { error: upstreamResult.error };
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
const runtime = g_builtinProxyRuntime;
|
|
3629
|
+
const shouldRestart = !!runtime && (
|
|
3630
|
+
runtime.settings.host !== nextSettings.host
|
|
3631
|
+
|| runtime.settings.port !== nextSettings.port
|
|
3632
|
+
|| runtime.settings.authSource !== nextSettings.authSource
|
|
3633
|
+
|| runtime.settings.timeoutMs !== nextSettings.timeoutMs
|
|
3634
|
+
|| runtime.upstream.providerName !== upstreamResult.providerName
|
|
3635
|
+
|| runtime.upstream.baseUrl !== upstreamResult.baseUrl
|
|
3636
|
+
|| runtime.upstream.authHeader !== upstreamResult.authHeader
|
|
3637
|
+
);
|
|
3638
|
+
|
|
3639
|
+
if (shouldRestart) {
|
|
3640
|
+
await stopBuiltinProxyRuntime();
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
if (!g_builtinProxyRuntime) {
|
|
3644
|
+
let startRes = await startBuiltinProxyRuntime(nextSettings);
|
|
3645
|
+
if (!startRes.success && /EADDRINUSE/i.test(String(startRes.error || ''))) {
|
|
3646
|
+
const fallbackPort = await findAvailablePort(nextSettings.host, nextSettings.port + 1, 30);
|
|
3647
|
+
if (fallbackPort > 0) {
|
|
3648
|
+
const retrySave = saveBuiltinProxySettings({
|
|
3649
|
+
...nextSettings,
|
|
3650
|
+
port: fallbackPort,
|
|
3651
|
+
enabled: true
|
|
3652
|
+
});
|
|
3653
|
+
if (retrySave.success) {
|
|
3654
|
+
nextSettings = retrySave.settings;
|
|
3655
|
+
upstreamResult = resolveBuiltinProxyUpstream(nextSettings);
|
|
3656
|
+
if (upstreamResult.error) {
|
|
3657
|
+
return { error: upstreamResult.error };
|
|
3658
|
+
}
|
|
3659
|
+
startRes = await startBuiltinProxyRuntime(nextSettings);
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
if (!startRes.success) {
|
|
3664
|
+
return { error: startRes.error || '启动内建代理失败' };
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
let applyRes = {
|
|
3669
|
+
success: true,
|
|
3670
|
+
provider: BUILTIN_PROXY_PROVIDER_NAME,
|
|
3671
|
+
baseUrl: buildProxyListenUrl(nextSettings),
|
|
3672
|
+
switched: false,
|
|
3673
|
+
model: ''
|
|
3674
|
+
};
|
|
3675
|
+
if (switchToProxy) {
|
|
3676
|
+
applyRes = applyBuiltinProxyProvider({ switchToProxy: true });
|
|
3677
|
+
if (applyRes.error) {
|
|
3678
|
+
return applyRes;
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
const status = getBuiltinProxyStatus();
|
|
3683
|
+
return {
|
|
3684
|
+
success: true,
|
|
3685
|
+
provider: applyRes.provider,
|
|
3686
|
+
baseUrl: applyRes.baseUrl,
|
|
3687
|
+
switched: applyRes.switched,
|
|
3688
|
+
model: applyRes.model || '',
|
|
3689
|
+
settings: status.settings,
|
|
3690
|
+
runtime: status.runtime
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
function updateClaudeSessionIndex(indexPath, sessionFilePath, sessionId) {
|
|
3695
|
+
if (!indexPath || !fs.existsSync(indexPath)) {
|
|
3696
|
+
return;
|
|
3697
|
+
}
|
|
3698
|
+
const index = readJsonFile(indexPath, null);
|
|
3699
|
+
if (!index || !Array.isArray(index.entries)) {
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
const resolvedFile = sessionFilePath ? path.resolve(sessionFilePath) : '';
|
|
3703
|
+
const resolvedLower = resolvedFile ? resolvedFile.toLowerCase() : '';
|
|
3704
|
+
const filtered = index.entries.filter((entry) => {
|
|
3705
|
+
if (!entry || typeof entry !== 'object') {
|
|
3706
|
+
return false;
|
|
3707
|
+
}
|
|
3708
|
+
const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
|
|
3709
|
+
if (sessionId && entrySessionId === sessionId) {
|
|
3710
|
+
return false;
|
|
3711
|
+
}
|
|
3712
|
+
if (entry.fullPath) {
|
|
3713
|
+
const expanded = expandHomePath(entry.fullPath);
|
|
3714
|
+
const entryPath = expanded ? path.resolve(expanded) : '';
|
|
3715
|
+
if (entryPath && resolvedLower && entryPath.toLowerCase() === resolvedLower) {
|
|
3716
|
+
return false;
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
return true;
|
|
3720
|
+
});
|
|
3721
|
+
if (filtered.length === index.entries.length) {
|
|
3722
|
+
return;
|
|
3723
|
+
}
|
|
3724
|
+
index.entries = filtered;
|
|
3725
|
+
try {
|
|
3726
|
+
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
|
|
3727
|
+
} catch (e) {}
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
async function deleteSessionData(params = {}) {
|
|
3731
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3732
|
+
if (!source) {
|
|
3733
|
+
return { error: 'Invalid source' };
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3737
|
+
if (!filePath) {
|
|
3738
|
+
return { error: 'Session file not found' };
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
const sessionId = params.sessionId || path.basename(filePath, '.jsonl');
|
|
3742
|
+
try {
|
|
3743
|
+
fs.unlinkSync(filePath);
|
|
3744
|
+
} catch (e) {
|
|
3745
|
+
return { error: `删除会话失败: ${e.message}` };
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
if (source === 'claude') {
|
|
3749
|
+
const indexPath = findClaudeSessionIndexPath(filePath);
|
|
3750
|
+
if (indexPath) {
|
|
3751
|
+
updateClaudeSessionIndex(indexPath, filePath, sessionId);
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
invalidateSessionListCache();
|
|
3756
|
+
|
|
3757
|
+
return {
|
|
3758
|
+
success: true,
|
|
3759
|
+
source,
|
|
3760
|
+
sessionId,
|
|
3761
|
+
filePath
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
function generateCloneSessionId() {
|
|
3766
|
+
if (crypto.randomUUID) {
|
|
3767
|
+
return `clone-${crypto.randomUUID()}`;
|
|
3768
|
+
}
|
|
3769
|
+
const timePart = Date.now().toString(36);
|
|
3770
|
+
const randomPart = crypto.randomBytes(8).toString('hex');
|
|
3771
|
+
return `clone-${timePart}-${randomPart}`;
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
function allocateCloneSessionTarget(dirPath) {
|
|
3775
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
3776
|
+
const sessionId = generateCloneSessionId();
|
|
3777
|
+
const filePath = path.join(dirPath, `${sessionId}.jsonl`);
|
|
3778
|
+
if (!fs.existsSync(filePath)) {
|
|
3779
|
+
return { sessionId, filePath };
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
const fallbackId = `clone-${Date.now().toString(36)}-${crypto.randomBytes(8).toString('hex')}`;
|
|
3783
|
+
return { sessionId: fallbackId, filePath: path.join(dirPath, `${fallbackId}.jsonl`) };
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
function parseTimestampMs(value) {
|
|
3787
|
+
if (value === undefined || value === null || value === '') {
|
|
3788
|
+
return null;
|
|
3789
|
+
}
|
|
3790
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
3791
|
+
if (value > 1e12) return value;
|
|
3792
|
+
if (value > 1e9) return value * 1000;
|
|
3793
|
+
return value;
|
|
3794
|
+
}
|
|
3795
|
+
if (typeof value === 'string') {
|
|
3796
|
+
const parsed = Date.parse(value);
|
|
3797
|
+
if (Number.isFinite(parsed)) {
|
|
3798
|
+
return parsed;
|
|
3799
|
+
}
|
|
3800
|
+
const numeric = Number(value);
|
|
3801
|
+
if (Number.isFinite(numeric)) {
|
|
3802
|
+
if (numeric > 1e12) return numeric;
|
|
3803
|
+
if (numeric > 1e9) return numeric * 1000;
|
|
3804
|
+
return numeric;
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
return null;
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
async function cloneCodexSession(params = {}) {
|
|
3811
|
+
const source = params.source === 'codex' ? 'codex' : '';
|
|
3812
|
+
if (!source) {
|
|
3813
|
+
return { error: '仅支持 Codex 会话克隆' };
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3817
|
+
if (!filePath) {
|
|
3818
|
+
return { error: 'Session file not found' };
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
let content = '';
|
|
3822
|
+
try {
|
|
3823
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
3824
|
+
} catch (e) {
|
|
3825
|
+
return { error: `读取会话失败: ${e.message}` };
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
if (!content.trim()) {
|
|
3829
|
+
return { error: 'Session file is empty' };
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
const lineEnding = detectLineEnding(content);
|
|
3833
|
+
const rawLines = content.split(/\r?\n/);
|
|
3834
|
+
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === '') {
|
|
3835
|
+
rawLines.pop();
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
let originalSessionId = typeof params.sessionId === 'string' ? params.sessionId.trim() : '';
|
|
3839
|
+
if (!originalSessionId) {
|
|
3840
|
+
originalSessionId = path.basename(filePath, '.jsonl');
|
|
3841
|
+
}
|
|
3842
|
+
let maxTimestampMs = 0;
|
|
2665
3843
|
|
|
2666
3844
|
for (const line of rawLines) {
|
|
2667
3845
|
const trimmed = line.trim();
|
|
@@ -3164,6 +4342,9 @@ function buildExportPayload(includeKeys) {
|
|
|
3164
4342
|
const providers = config.model_providers || {};
|
|
3165
4343
|
const providerData = {};
|
|
3166
4344
|
for (const [name, provider] of Object.entries(providers)) {
|
|
4345
|
+
if (isBuiltinProxyProvider(name)) {
|
|
4346
|
+
continue;
|
|
4347
|
+
}
|
|
3167
4348
|
providerData[name] = {
|
|
3168
4349
|
baseUrl: provider.base_url || '',
|
|
3169
4350
|
apiKey: includeKeys ? (provider.preferred_auth_method || '') : null
|
|
@@ -3256,6 +4437,10 @@ function normalizeImportPayload(payload) {
|
|
|
3256
4437
|
}
|
|
3257
4438
|
}
|
|
3258
4439
|
|
|
4440
|
+
if (Object.keys(providers).length === 0 && (!payload.models || payload.models.length === 0)) {
|
|
4441
|
+
return { error: 'Invalid import payload' };
|
|
4442
|
+
}
|
|
4443
|
+
|
|
3259
4444
|
return {
|
|
3260
4445
|
providers,
|
|
3261
4446
|
models: Array.isArray(payload.models) ? payload.models : [],
|
|
@@ -3281,6 +4466,9 @@ function importConfigData(payload, options = {}) {
|
|
|
3281
4466
|
let updatedProviders = 0;
|
|
3282
4467
|
|
|
3283
4468
|
for (const [name, provider] of Object.entries(normalized.providers)) {
|
|
4469
|
+
if (isBuiltinProxyProvider(name)) {
|
|
4470
|
+
continue;
|
|
4471
|
+
}
|
|
3284
4472
|
if (existingProviders[name]) {
|
|
3285
4473
|
if (overwriteProviders) {
|
|
3286
4474
|
const apiKey = typeof provider.apiKey === 'string' && provider.apiKey
|
|
@@ -3312,6 +4500,7 @@ function importConfigData(payload, options = {}) {
|
|
|
3312
4500
|
if (applyCurrentModels && normalized.currentModels) {
|
|
3313
4501
|
const currentModels = readCurrentModels();
|
|
3314
4502
|
for (const [name, model] of Object.entries(normalized.currentModels)) {
|
|
4503
|
+
if (isBuiltinProxyProvider(name)) continue;
|
|
3315
4504
|
if (typeof model !== 'string' || !model) continue;
|
|
3316
4505
|
currentModels[name] = model;
|
|
3317
4506
|
}
|
|
@@ -3828,7 +5017,10 @@ function cmdUseModel(modelName, silent = false) {
|
|
|
3828
5017
|
|
|
3829
5018
|
// 添加提供商
|
|
3830
5019
|
function cmdAdd(name, baseUrl, apiKey, silent = false) {
|
|
3831
|
-
|
|
5020
|
+
const providerName = typeof name === 'string' ? name.trim() : '';
|
|
5021
|
+
const providerBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
|
5022
|
+
|
|
5023
|
+
if (!providerName || !providerBaseUrl) {
|
|
3832
5024
|
if (!silent) {
|
|
3833
5025
|
console.error('用法: codexmate add <名称> <URL> [密钥]');
|
|
3834
5026
|
console.log('\n示例:');
|
|
@@ -3836,17 +5028,21 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
|
|
|
3836
5028
|
}
|
|
3837
5029
|
throw new Error('名称和URL必填');
|
|
3838
5030
|
}
|
|
5031
|
+
if (isReservedProviderNameForCreation(providerName)) {
|
|
5032
|
+
if (!silent) console.error('错误: local provider 为系统保留名称,不可新增');
|
|
5033
|
+
throw new Error('local provider 为系统保留名称,不可新增');
|
|
5034
|
+
}
|
|
3839
5035
|
|
|
3840
5036
|
const config = readConfig();
|
|
3841
|
-
if (config.model_providers && config.model_providers[
|
|
3842
|
-
if (!silent) console.error('错误: 提供商已存在:',
|
|
5037
|
+
if (config.model_providers && config.model_providers[providerName]) {
|
|
5038
|
+
if (!silent) console.error('错误: 提供商已存在:', providerName);
|
|
3843
5039
|
throw new Error('提供商已存在');
|
|
3844
5040
|
}
|
|
3845
5041
|
|
|
3846
5042
|
const newBlock = `
|
|
3847
|
-
[model_providers.${
|
|
3848
|
-
name = "${
|
|
3849
|
-
base_url = "${
|
|
5043
|
+
[model_providers.${providerName}]
|
|
5044
|
+
name = "${providerName}"
|
|
5045
|
+
base_url = "${providerBaseUrl}"
|
|
3850
5046
|
wire_api = "responses"
|
|
3851
5047
|
requires_openai_auth = false
|
|
3852
5048
|
preferred_auth_method = "${apiKey || ''}"
|
|
@@ -3860,60 +5056,47 @@ stream_idle_timeout_ms = 300000
|
|
|
3860
5056
|
|
|
3861
5057
|
// 初始化当前模型
|
|
3862
5058
|
const currentModels = readCurrentModels();
|
|
3863
|
-
if (!currentModels[
|
|
3864
|
-
currentModels[
|
|
5059
|
+
if (!currentModels[providerName]) {
|
|
5060
|
+
currentModels[providerName] = readModels()[0];
|
|
3865
5061
|
writeCurrentModels(currentModels);
|
|
3866
5062
|
}
|
|
3867
5063
|
|
|
3868
5064
|
if (!silent) {
|
|
3869
|
-
console.log('✓ 已添加提供商:',
|
|
3870
|
-
console.log(' URL:',
|
|
5065
|
+
console.log('✓ 已添加提供商:', providerName);
|
|
5066
|
+
console.log(' URL:', providerBaseUrl);
|
|
3871
5067
|
console.log();
|
|
3872
5068
|
}
|
|
3873
5069
|
}
|
|
3874
5070
|
|
|
3875
5071
|
// 删除提供商
|
|
3876
5072
|
function cmdDelete(name, silent = false) {
|
|
3877
|
-
const
|
|
3878
|
-
if (
|
|
3879
|
-
|
|
3880
|
-
throw new Error('提供商不存在');
|
|
3881
|
-
}
|
|
3882
|
-
|
|
3883
|
-
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
3884
|
-
const safeName = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
3885
|
-
const sectionRegex = new RegExp(`\\[\\s*model_providers\\s*\\.\\s*${safeName}\\s*\\]`);
|
|
3886
|
-
const match = content.match(sectionRegex);
|
|
3887
|
-
if (!match) {
|
|
3888
|
-
if (!silent) console.error('错误: 无法找到提供商配置块');
|
|
3889
|
-
throw new Error('无法找到提供商配置块');
|
|
5073
|
+
const res = performProviderDeletion(name, { silent });
|
|
5074
|
+
if (res.error) {
|
|
5075
|
+
throw new Error(res.error);
|
|
3890
5076
|
}
|
|
3891
|
-
|
|
3892
|
-
const startIdx = match.index;
|
|
3893
|
-
const rest = content.slice(startIdx + match[0].length);
|
|
3894
|
-
const nextIdx = rest.indexOf('[');
|
|
3895
|
-
const endIdx = nextIdx === -1 ? content.length : (startIdx + match[0].length + nextIdx);
|
|
3896
|
-
|
|
3897
|
-
const newContent = content.slice(0, startIdx) + content.slice(endIdx);
|
|
3898
|
-
writeConfig(newContent.trim());
|
|
3899
|
-
|
|
3900
|
-
// 删除当前模型记录
|
|
3901
|
-
const currentModels = readCurrentModels();
|
|
3902
|
-
delete currentModels[name];
|
|
3903
|
-
writeCurrentModels(currentModels);
|
|
3904
|
-
|
|
3905
5077
|
if (!silent) {
|
|
3906
5078
|
console.log('✓ 已删除提供商:', name);
|
|
5079
|
+
if (res.switched && res.provider) {
|
|
5080
|
+
console.log(` 已自动切换到 provider: ${res.provider},model: ${res.model || '(未设置)'}`);
|
|
5081
|
+
}
|
|
3907
5082
|
console.log();
|
|
3908
5083
|
}
|
|
3909
5084
|
}
|
|
3910
5085
|
|
|
3911
5086
|
// 更新提供商
|
|
3912
|
-
function cmdUpdate(name, baseUrl, apiKey, silent = false) {
|
|
5087
|
+
function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
|
|
5088
|
+
const allowManaged = !!(options && options.allowManaged);
|
|
3913
5089
|
if (!name) {
|
|
3914
5090
|
if (!silent) console.error('错误: 提供商名称必填');
|
|
3915
5091
|
throw new Error('提供商名称必填');
|
|
3916
5092
|
}
|
|
5093
|
+
if (isNonEditableProvider(name) && !allowManaged) {
|
|
5094
|
+
const msg = isDefaultLocalProvider(name)
|
|
5095
|
+
? 'local provider 为系统保留项,不可编辑'
|
|
5096
|
+
: '本地代理配置为系统内建项,不可编辑';
|
|
5097
|
+
if (!silent) console.error(`错误: ${msg}`);
|
|
5098
|
+
throw new Error(msg);
|
|
5099
|
+
}
|
|
3917
5100
|
|
|
3918
5101
|
const config = readConfig();
|
|
3919
5102
|
if (!config.model_providers || !config.model_providers[name]) {
|
|
@@ -4104,7 +5287,7 @@ function readClaudeSettingsInfo() {
|
|
|
4104
5287
|
return {
|
|
4105
5288
|
error: readResult.error || '读取 Claude 配置失败',
|
|
4106
5289
|
exists: !!readResult.exists,
|
|
4107
|
-
|
|
5290
|
+
targetPath: CLAUDE_SETTINGS_FILE
|
|
4108
5291
|
};
|
|
4109
5292
|
}
|
|
4110
5293
|
|
|
@@ -4115,7 +5298,7 @@ function readClaudeSettingsInfo() {
|
|
|
4115
5298
|
|
|
4116
5299
|
return {
|
|
4117
5300
|
exists: !!readResult.exists,
|
|
4118
|
-
|
|
5301
|
+
targetPath: CLAUDE_SETTINGS_FILE,
|
|
4119
5302
|
apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
|
|
4120
5303
|
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
|
|
4121
5304
|
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
|
|
@@ -4123,6 +5306,225 @@ function readClaudeSettingsInfo() {
|
|
|
4123
5306
|
};
|
|
4124
5307
|
}
|
|
4125
5308
|
|
|
5309
|
+
// API: 打包 Claude 配置目录(系统 zip 可用则使用,否则回退 zip-lib)
|
|
5310
|
+
async function prepareClaudeDirDownload() {
|
|
5311
|
+
try {
|
|
5312
|
+
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
5313
|
+
return { error: 'Claude 配置目录不存在', path: CLAUDE_DIR };
|
|
5314
|
+
}
|
|
5315
|
+
|
|
5316
|
+
const tempDir = os.tmpdir();
|
|
5317
|
+
const timestamp = Date.now();
|
|
5318
|
+
const zipFileName = `claude-config-${timestamp}.zip`;
|
|
5319
|
+
const zipFilePath = path.join(tempDir, zipFileName);
|
|
5320
|
+
|
|
5321
|
+
const zipTool = resolveZipTool();
|
|
5322
|
+
if (zipTool.type === 'zip') {
|
|
5323
|
+
const cmd = `"${zipTool.cmd}" -0 -q -r "${zipFilePath}" "${CLAUDE_DIR}"`;
|
|
5324
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
5325
|
+
} else {
|
|
5326
|
+
await zipLib.archiveFolder(CLAUDE_DIR, zipFilePath);
|
|
5327
|
+
}
|
|
5328
|
+
|
|
5329
|
+
return {
|
|
5330
|
+
success: true,
|
|
5331
|
+
downloadPath: zipFilePath,
|
|
5332
|
+
fileName: zipFileName,
|
|
5333
|
+
sourcePath: CLAUDE_DIR
|
|
5334
|
+
};
|
|
5335
|
+
} catch (e) {
|
|
5336
|
+
return { error: `打包失败:${e.message}` };
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
|
|
5340
|
+
// API: 打包 Codex 配置目录(同策略)
|
|
5341
|
+
async function prepareCodexDirDownload() {
|
|
5342
|
+
try {
|
|
5343
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
5344
|
+
return { error: 'Codex 配置目录不存在', path: CONFIG_DIR };
|
|
5345
|
+
}
|
|
5346
|
+
|
|
5347
|
+
const tempDir = os.tmpdir();
|
|
5348
|
+
const timestamp = Date.now();
|
|
5349
|
+
const zipFileName = `${CODEX_BACKUP_NAME}-${timestamp}.zip`;
|
|
5350
|
+
const zipFilePath = path.join(tempDir, zipFileName);
|
|
5351
|
+
|
|
5352
|
+
const zipTool = resolveZipTool();
|
|
5353
|
+
if (zipTool.type === 'zip') {
|
|
5354
|
+
const cmd = `"${zipTool.cmd}" -0 -q -r "${zipFilePath}" "${CONFIG_DIR}"`;
|
|
5355
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
5356
|
+
} else {
|
|
5357
|
+
await zipLib.archiveFolder(CONFIG_DIR, zipFilePath);
|
|
5358
|
+
}
|
|
5359
|
+
|
|
5360
|
+
return {
|
|
5361
|
+
success: true,
|
|
5362
|
+
downloadPath: zipFilePath,
|
|
5363
|
+
fileName: zipFileName,
|
|
5364
|
+
sourcePath: CONFIG_DIR
|
|
5365
|
+
};
|
|
5366
|
+
} catch (e) {
|
|
5367
|
+
return { error: `打包失败:${e.message}` };
|
|
5368
|
+
}
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
function copyDirRecursive(srcDir, destDir) {
|
|
5372
|
+
ensureDir(destDir);
|
|
5373
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
5374
|
+
for (const entry of entries) {
|
|
5375
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
5376
|
+
const destPath = path.join(destDir, entry.name);
|
|
5377
|
+
if (entry.isDirectory()) {
|
|
5378
|
+
copyDirRecursive(srcPath, destPath);
|
|
5379
|
+
} else if (entry.isSymbolicLink()) {
|
|
5380
|
+
const target = fs.readlinkSync(srcPath);
|
|
5381
|
+
fs.symlinkSync(target, destPath);
|
|
5382
|
+
} else {
|
|
5383
|
+
fs.copyFileSync(srcPath, destPath);
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
|
|
5388
|
+
function writeUploadZip(base64, prefix, originalName = '') {
|
|
5389
|
+
let buffer;
|
|
5390
|
+
try {
|
|
5391
|
+
buffer = Buffer.from(base64 || '', 'base64');
|
|
5392
|
+
} catch (e) {
|
|
5393
|
+
return { error: '备份文件内容不是有效的 base64 编码' };
|
|
5394
|
+
}
|
|
5395
|
+
|
|
5396
|
+
if (!buffer || buffer.length === 0) {
|
|
5397
|
+
return { error: '备份文件为空' };
|
|
5398
|
+
}
|
|
5399
|
+
|
|
5400
|
+
if (buffer.length > MAX_UPLOAD_SIZE) {
|
|
5401
|
+
return { error: `备份文件过大(>${Math.floor(MAX_UPLOAD_SIZE / 1024 / 1024)}MB)` };
|
|
5402
|
+
}
|
|
5403
|
+
|
|
5404
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-`));
|
|
5405
|
+
const fileName = path.basename(originalName && typeof originalName === 'string' ? originalName : `${prefix}.zip`);
|
|
5406
|
+
const zipPath = path.join(tempDir, fileName.toLowerCase().endsWith('.zip') ? fileName : `${fileName}.zip`);
|
|
5407
|
+
fs.writeFileSync(zipPath, buffer);
|
|
5408
|
+
return { tempDir, zipPath };
|
|
5409
|
+
}
|
|
5410
|
+
|
|
5411
|
+
async function extractUploadZip(zipPath, extractDir) {
|
|
5412
|
+
const unzipTool = resolveUnzipTool();
|
|
5413
|
+
ensureDir(extractDir);
|
|
5414
|
+
await unzipWithLibrary(zipPath, extractDir);
|
|
5415
|
+
}
|
|
5416
|
+
|
|
5417
|
+
function findConfigSourceDir(extractedDir, markerDirName, requiredFileName) {
|
|
5418
|
+
const markerPath = path.join(extractedDir, markerDirName);
|
|
5419
|
+
if (fs.existsSync(markerPath) && fs.statSync(markerPath).isDirectory()) {
|
|
5420
|
+
return markerPath;
|
|
5421
|
+
}
|
|
5422
|
+
|
|
5423
|
+
const entries = fs.readdirSync(extractedDir, { withFileTypes: true }).filter((item) => item.isDirectory());
|
|
5424
|
+
if (entries.length === 1) {
|
|
5425
|
+
const onlyDir = path.join(extractedDir, entries[0].name);
|
|
5426
|
+
const nestedMarker = path.join(onlyDir, markerDirName);
|
|
5427
|
+
if (fs.existsSync(nestedMarker) && fs.statSync(nestedMarker).isDirectory()) {
|
|
5428
|
+
return nestedMarker;
|
|
5429
|
+
}
|
|
5430
|
+
if (fs.existsSync(path.join(onlyDir, requiredFileName))) {
|
|
5431
|
+
return onlyDir;
|
|
5432
|
+
}
|
|
5433
|
+
}
|
|
5434
|
+
|
|
5435
|
+
if (fs.existsSync(path.join(extractedDir, requiredFileName))) {
|
|
5436
|
+
return extractedDir;
|
|
5437
|
+
}
|
|
5438
|
+
|
|
5439
|
+
return extractedDir;
|
|
5440
|
+
}
|
|
5441
|
+
|
|
5442
|
+
async function backupDirectoryIfExists(dirPath, prefix) {
|
|
5443
|
+
if (!fs.existsSync(dirPath)) {
|
|
5444
|
+
return { backupPath: '' };
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
const tempDir = os.tmpdir();
|
|
5448
|
+
const timestamp = Date.now();
|
|
5449
|
+
const zipFileName = `${prefix}-${timestamp}.zip`;
|
|
5450
|
+
const zipFilePath = path.join(tempDir, zipFileName);
|
|
5451
|
+
const zipTool = resolveZipTool();
|
|
5452
|
+
|
|
5453
|
+
try {
|
|
5454
|
+
if (zipTool.type === 'zip') {
|
|
5455
|
+
const cmd = `"${zipTool.cmd}" -0 -q -r "${zipFilePath}" "${dirPath}"`;
|
|
5456
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
5457
|
+
} else {
|
|
5458
|
+
await zipLib.archiveFolder(dirPath, zipFilePath);
|
|
5459
|
+
}
|
|
5460
|
+
return { backupPath: zipFilePath, fileName: zipFileName };
|
|
5461
|
+
} catch (e) {
|
|
5462
|
+
return { backupPath: '', warning: `备份失败: ${e.message}` };
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5465
|
+
|
|
5466
|
+
async function restoreConfigDirectoryFromUpload(payload, options) {
|
|
5467
|
+
const { targetDir, requiredFileName, markerDirName, tempPrefix, backupPrefix } = options;
|
|
5468
|
+
if (!payload || typeof payload.fileBase64 !== 'string' || !payload.fileBase64.trim()) {
|
|
5469
|
+
return { error: '缺少备份文件内容' };
|
|
5470
|
+
}
|
|
5471
|
+
|
|
5472
|
+
const upload = writeUploadZip(payload.fileBase64, tempPrefix, payload.fileName);
|
|
5473
|
+
if (upload.error) {
|
|
5474
|
+
return { error: upload.error };
|
|
5475
|
+
}
|
|
5476
|
+
|
|
5477
|
+
const tempDir = upload.tempDir;
|
|
5478
|
+
const extractDir = path.join(tempDir, 'extract');
|
|
5479
|
+
let backupPath = '';
|
|
5480
|
+
try {
|
|
5481
|
+
await extractUploadZip(upload.zipPath, extractDir);
|
|
5482
|
+
const sourceDir = findConfigSourceDir(extractDir, markerDirName, requiredFileName);
|
|
5483
|
+
const requiredPath = path.join(sourceDir, requiredFileName);
|
|
5484
|
+
if (!fs.existsSync(requiredPath)) {
|
|
5485
|
+
return { error: `无效备份,缺少 ${requiredFileName}` };
|
|
5486
|
+
}
|
|
5487
|
+
|
|
5488
|
+
const backupResult = await backupDirectoryIfExists(targetDir, backupPrefix);
|
|
5489
|
+
backupPath = backupResult.backupPath || '';
|
|
5490
|
+
|
|
5491
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
5492
|
+
copyDirRecursive(sourceDir, targetDir);
|
|
5493
|
+
|
|
5494
|
+
return {
|
|
5495
|
+
success: true,
|
|
5496
|
+
targetDir,
|
|
5497
|
+
appliedFrom: payload.fileName || '',
|
|
5498
|
+
backupPath,
|
|
5499
|
+
backupWarning: backupResult.warning || ''
|
|
5500
|
+
};
|
|
5501
|
+
} catch (e) {
|
|
5502
|
+
return { error: `导入失败:${e.message}` };
|
|
5503
|
+
} finally {
|
|
5504
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
|
|
5508
|
+
async function restoreClaudeDir(payload) {
|
|
5509
|
+
return await restoreConfigDirectoryFromUpload(payload, {
|
|
5510
|
+
targetDir: CLAUDE_DIR,
|
|
5511
|
+
requiredFileName: 'settings.json',
|
|
5512
|
+
markerDirName: '.claude',
|
|
5513
|
+
tempPrefix: 'claude-restore',
|
|
5514
|
+
backupPrefix: 'claude-config'
|
|
5515
|
+
});
|
|
5516
|
+
}
|
|
5517
|
+
|
|
5518
|
+
async function restoreCodexDir(payload) {
|
|
5519
|
+
return await restoreConfigDirectoryFromUpload(payload, {
|
|
5520
|
+
targetDir: CONFIG_DIR,
|
|
5521
|
+
requiredFileName: 'config.toml',
|
|
5522
|
+
markerDirName: '.codex',
|
|
5523
|
+
tempPrefix: 'codex-restore',
|
|
5524
|
+
backupPrefix: 'codex-config'
|
|
5525
|
+
});
|
|
5526
|
+
}
|
|
5527
|
+
|
|
4126
5528
|
// CLI: 一行写入 Claude Code 配置
|
|
4127
5529
|
function cmdClaude(baseUrl, apiKey, model, silent = false) {
|
|
4128
5530
|
const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
|
@@ -4152,43 +5554,221 @@ function cmdClaude(baseUrl, apiKey, model, silent = false) {
|
|
|
4152
5554
|
throw new Error(message);
|
|
4153
5555
|
}
|
|
4154
5556
|
|
|
4155
|
-
if (!silent) {
|
|
4156
|
-
console.log('✓ 已写入 Claude Code 配置');
|
|
4157
|
-
console.log(' Base URL:', normalizedBaseUrl);
|
|
4158
|
-
console.log(' 模型:', normalizedModel);
|
|
4159
|
-
if (result.targetPath) {
|
|
4160
|
-
console.log(' 目标文件:', result.targetPath);
|
|
5557
|
+
if (!silent) {
|
|
5558
|
+
console.log('✓ 已写入 Claude Code 配置');
|
|
5559
|
+
console.log(' Base URL:', normalizedBaseUrl);
|
|
5560
|
+
console.log(' 模型:', normalizedModel);
|
|
5561
|
+
if (result.targetPath) {
|
|
5562
|
+
console.log(' 目标文件:', result.targetPath);
|
|
5563
|
+
}
|
|
5564
|
+
if (result.backupPath) {
|
|
5565
|
+
console.log(' 已自动备份:', result.backupPath);
|
|
5566
|
+
}
|
|
5567
|
+
console.log();
|
|
5568
|
+
}
|
|
5569
|
+
|
|
5570
|
+
return result;
|
|
5571
|
+
}
|
|
5572
|
+
|
|
5573
|
+
function commandExists(command, args = '') {
|
|
5574
|
+
try {
|
|
5575
|
+
execSync(`${command} ${args}`, { stdio: 'ignore', shell: process.platform === 'win32' });
|
|
5576
|
+
return true;
|
|
5577
|
+
} catch (e) {
|
|
5578
|
+
return false;
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
|
|
5582
|
+
function detectPreferredPackageManager() {
|
|
5583
|
+
const userAgent = typeof process.env.npm_config_user_agent === 'string'
|
|
5584
|
+
? process.env.npm_config_user_agent.trim().toLowerCase()
|
|
5585
|
+
: '';
|
|
5586
|
+
if (userAgent.startsWith('pnpm/')) return 'pnpm';
|
|
5587
|
+
if (userAgent.startsWith('bun/')) return 'bun';
|
|
5588
|
+
if (userAgent.startsWith('npm/')) return 'npm';
|
|
5589
|
+
|
|
5590
|
+
if (commandExists('pnpm', '--version')) return 'pnpm';
|
|
5591
|
+
if (commandExists('bun', '--version')) return 'bun';
|
|
5592
|
+
return 'npm';
|
|
5593
|
+
}
|
|
5594
|
+
|
|
5595
|
+
function resolveCommandPath(command) {
|
|
5596
|
+
if (!command) return '';
|
|
5597
|
+
const locator = process.platform === 'win32' ? 'where' : 'which';
|
|
5598
|
+
try {
|
|
5599
|
+
const probe = spawnSync(locator, [command], {
|
|
5600
|
+
encoding: 'utf8',
|
|
5601
|
+
windowsHide: true,
|
|
5602
|
+
timeout: 2500
|
|
5603
|
+
});
|
|
5604
|
+
if (probe.error || probe.status !== 0) {
|
|
5605
|
+
return '';
|
|
5606
|
+
}
|
|
5607
|
+
const lines = String(probe.stdout || '')
|
|
5608
|
+
.split(/\r?\n/g)
|
|
5609
|
+
.map((line) => line.trim())
|
|
5610
|
+
.filter(Boolean);
|
|
5611
|
+
return lines[0] || '';
|
|
5612
|
+
} catch (e) {
|
|
5613
|
+
return '';
|
|
5614
|
+
}
|
|
5615
|
+
}
|
|
5616
|
+
|
|
5617
|
+
function parseBinaryVersionOutput(text) {
|
|
5618
|
+
const raw = typeof text === 'string' ? text : '';
|
|
5619
|
+
const line = raw
|
|
5620
|
+
.split(/\r?\n/g)
|
|
5621
|
+
.map((item) => item.trim())
|
|
5622
|
+
.find(Boolean) || '';
|
|
5623
|
+
if (!line) return '';
|
|
5624
|
+
return line.length > 120 ? `${line.slice(0, 117)}...` : line;
|
|
5625
|
+
}
|
|
5626
|
+
|
|
5627
|
+
function probeCliBinary(binName) {
|
|
5628
|
+
const attempts = [['--version'], ['-v'], ['version']];
|
|
5629
|
+
let lastError = '';
|
|
5630
|
+
|
|
5631
|
+
for (const args of attempts) {
|
|
5632
|
+
const argString = args.join(' ').trim();
|
|
5633
|
+
const commandLine = argString ? `${binName} ${argString}` : binName;
|
|
5634
|
+
try {
|
|
5635
|
+
const stdout = execSync(commandLine, {
|
|
5636
|
+
encoding: 'utf8',
|
|
5637
|
+
windowsHide: true,
|
|
5638
|
+
timeout: 5000,
|
|
5639
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
5640
|
+
shell: process.platform === 'win32'
|
|
5641
|
+
});
|
|
5642
|
+
const version = parseBinaryVersionOutput(String(stdout || ''));
|
|
5643
|
+
return {
|
|
5644
|
+
installed: true,
|
|
5645
|
+
bin: binName,
|
|
5646
|
+
version: version || 'unknown',
|
|
5647
|
+
path: resolveCommandPath(binName),
|
|
5648
|
+
error: ''
|
|
5649
|
+
};
|
|
5650
|
+
} catch (error) {
|
|
5651
|
+
const err = error || {};
|
|
5652
|
+
const stdout = typeof err.stdout === 'string' ? err.stdout : String(err.stdout || '');
|
|
5653
|
+
const stderr = typeof err.stderr === 'string' ? err.stderr : String(err.stderr || '');
|
|
5654
|
+
const output = `${stdout}\n${stderr}`.trim();
|
|
5655
|
+
const version = parseBinaryVersionOutput(output);
|
|
5656
|
+
const status = Number.isFinite(err.status) ? err.status : null;
|
|
5657
|
+
if (version && status === 0) {
|
|
5658
|
+
return {
|
|
5659
|
+
installed: true,
|
|
5660
|
+
bin: binName,
|
|
5661
|
+
version,
|
|
5662
|
+
path: resolveCommandPath(binName),
|
|
5663
|
+
error: ''
|
|
5664
|
+
};
|
|
5665
|
+
}
|
|
5666
|
+
if (version) {
|
|
5667
|
+
lastError = status !== null
|
|
5668
|
+
? `${binName} exited with ${status}: ${version}`
|
|
5669
|
+
: `${binName} failed: ${version}`;
|
|
5670
|
+
continue;
|
|
5671
|
+
}
|
|
5672
|
+
const message = err && err.message ? String(err.message) : '';
|
|
5673
|
+
if (message && !/ENOENT/i.test(message)) {
|
|
5674
|
+
lastError = message;
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
|
|
5679
|
+
return {
|
|
5680
|
+
installed: false,
|
|
5681
|
+
bin: binName,
|
|
5682
|
+
version: '',
|
|
5683
|
+
path: '',
|
|
5684
|
+
error: lastError
|
|
5685
|
+
};
|
|
5686
|
+
}
|
|
5687
|
+
|
|
5688
|
+
function resolveInstallCommandsByPackageManager(packageManager) {
|
|
5689
|
+
const normalized = String(packageManager || '').trim().toLowerCase();
|
|
5690
|
+
const manager = normalized === 'pnpm' || normalized === 'bun' || normalized === 'npm'
|
|
5691
|
+
? normalized
|
|
5692
|
+
: 'npm';
|
|
5693
|
+
const commandsByTarget = {};
|
|
5694
|
+
|
|
5695
|
+
for (const target of CLI_INSTALL_TARGETS) {
|
|
5696
|
+
const pkg = target.packageName;
|
|
5697
|
+
if (manager === 'pnpm') {
|
|
5698
|
+
commandsByTarget[target.id] = {
|
|
5699
|
+
install: `pnpm add -g ${pkg}`,
|
|
5700
|
+
update: `pnpm up -g ${pkg}`,
|
|
5701
|
+
uninstall: `pnpm remove -g ${pkg}`
|
|
5702
|
+
};
|
|
5703
|
+
continue;
|
|
4161
5704
|
}
|
|
4162
|
-
if (
|
|
4163
|
-
|
|
5705
|
+
if (manager === 'bun') {
|
|
5706
|
+
commandsByTarget[target.id] = {
|
|
5707
|
+
install: `bun add -g ${pkg}`,
|
|
5708
|
+
update: `bun update -g ${pkg}`,
|
|
5709
|
+
uninstall: `bun remove -g ${pkg}`
|
|
5710
|
+
};
|
|
5711
|
+
continue;
|
|
4164
5712
|
}
|
|
4165
|
-
|
|
5713
|
+
commandsByTarget[target.id] = {
|
|
5714
|
+
install: `npm install -g ${pkg}`,
|
|
5715
|
+
update: `npm update -g ${pkg}`,
|
|
5716
|
+
uninstall: `npm uninstall -g ${pkg}`
|
|
5717
|
+
};
|
|
4166
5718
|
}
|
|
4167
5719
|
|
|
4168
|
-
return
|
|
5720
|
+
return {
|
|
5721
|
+
packageManager: manager,
|
|
5722
|
+
commandsByTarget
|
|
5723
|
+
};
|
|
4169
5724
|
}
|
|
4170
5725
|
|
|
4171
|
-
function
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
5726
|
+
function buildInstallStatusReport() {
|
|
5727
|
+
const packageManager = detectPreferredPackageManager();
|
|
5728
|
+
const targetReports = CLI_INSTALL_TARGETS.map((target) => {
|
|
5729
|
+
let hit = null;
|
|
5730
|
+
let lastError = '';
|
|
5731
|
+
for (const binName of target.bins) {
|
|
5732
|
+
const probe = probeCliBinary(binName);
|
|
5733
|
+
if (probe.installed) {
|
|
5734
|
+
hit = probe;
|
|
5735
|
+
break;
|
|
5736
|
+
}
|
|
5737
|
+
if (probe.error) {
|
|
5738
|
+
lastError = probe.error;
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5741
|
+
return {
|
|
5742
|
+
id: target.id,
|
|
5743
|
+
name: target.name,
|
|
5744
|
+
packageName: target.packageName,
|
|
5745
|
+
installed: !!(hit && hit.installed),
|
|
5746
|
+
bin: hit ? hit.bin : (target.bins[0] || ''),
|
|
5747
|
+
version: hit ? hit.version : '',
|
|
5748
|
+
commandPath: hit ? hit.path : '',
|
|
5749
|
+
error: hit ? '' : lastError
|
|
5750
|
+
};
|
|
5751
|
+
});
|
|
5752
|
+
|
|
5753
|
+
const commandSpec = resolveInstallCommandsByPackageManager(packageManager);
|
|
5754
|
+
return {
|
|
5755
|
+
platform: process.platform,
|
|
5756
|
+
packageManager: commandSpec.packageManager,
|
|
5757
|
+
targets: targetReports,
|
|
5758
|
+
commandsByTarget: commandSpec.commandsByTarget
|
|
5759
|
+
};
|
|
4178
5760
|
}
|
|
4179
5761
|
|
|
4180
|
-
const
|
|
4181
|
-
'
|
|
4182
|
-
'C:\\Program Files (x86)\\7-Zip\\7z.exe',
|
|
4183
|
-
'7z'
|
|
5762
|
+
const ZIP_PATHS = [
|
|
5763
|
+
'zip'
|
|
4184
5764
|
];
|
|
4185
5765
|
|
|
4186
|
-
function
|
|
4187
|
-
for (const candidate of
|
|
5766
|
+
function findZipExecutable() {
|
|
5767
|
+
for (const candidate of ZIP_PATHS) {
|
|
4188
5768
|
try {
|
|
4189
|
-
if (candidate === '
|
|
4190
|
-
if (commandExists('
|
|
4191
|
-
return '
|
|
5769
|
+
if (candidate === 'zip') {
|
|
5770
|
+
if (commandExists('zip', '--help')) {
|
|
5771
|
+
return 'zip';
|
|
4192
5772
|
}
|
|
4193
5773
|
} else if (fs.existsSync(candidate)) {
|
|
4194
5774
|
return candidate;
|
|
@@ -4199,18 +5779,14 @@ function findSevenZipExecutable() {
|
|
|
4199
5779
|
}
|
|
4200
5780
|
|
|
4201
5781
|
function resolveZipTool() {
|
|
4202
|
-
const
|
|
4203
|
-
if (
|
|
4204
|
-
return { type: '
|
|
5782
|
+
const zipExe = findZipExecutable();
|
|
5783
|
+
if (zipExe) {
|
|
5784
|
+
return { type: 'zip', cmd: zipExe };
|
|
4205
5785
|
}
|
|
4206
5786
|
return { type: 'lib', cmd: 'zip-lib' };
|
|
4207
5787
|
}
|
|
4208
5788
|
|
|
4209
5789
|
function resolveUnzipTool() {
|
|
4210
|
-
const sevenZipExe = findSevenZipExecutable();
|
|
4211
|
-
if (sevenZipExe) {
|
|
4212
|
-
return { type: '7z', cmd: sevenZipExe };
|
|
4213
|
-
}
|
|
4214
5790
|
return { type: 'lib', cmd: 'zip-lib' };
|
|
4215
5791
|
}
|
|
4216
5792
|
|
|
@@ -4227,7 +5803,7 @@ async function unzipWithLibrary(zipPath, outputDir) {
|
|
|
4227
5803
|
await zipLib.extract(zipPath, outputDir);
|
|
4228
5804
|
}
|
|
4229
5805
|
|
|
4230
|
-
//
|
|
5806
|
+
// 压缩(系统 zip 优先,其次 zip-lib)
|
|
4231
5807
|
async function cmdZip(targetPath, options = {}) {
|
|
4232
5808
|
if (!targetPath) {
|
|
4233
5809
|
console.error('用法: codexmate zip <文件或文件夹路径> [--max:压缩级别]');
|
|
@@ -4257,40 +5833,27 @@ async function cmdZip(targetPath, options = {}) {
|
|
|
4257
5833
|
const outputPath = path.join(outputDir, `${baseName}.zip`);
|
|
4258
5834
|
|
|
4259
5835
|
const zipTool = resolveZipTool();
|
|
5836
|
+
const useZipCmd = zipTool.type === 'zip';
|
|
4260
5837
|
|
|
4261
5838
|
console.log('\n压缩配置:');
|
|
4262
5839
|
console.log(' 源路径:', absPath);
|
|
4263
5840
|
console.log(' 输出文件:', outputPath);
|
|
4264
|
-
console.log('
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
console.log('
|
|
5841
|
+
console.log(' 压缩工具:', useZipCmd ? '系统 zip' : 'zip-lib');
|
|
5842
|
+
if (useZipCmd) {
|
|
5843
|
+
console.log(' 压缩级别:', compressionLevel);
|
|
5844
|
+
} else {
|
|
5845
|
+
console.log(' 压缩级别: 固定(zip-lib 不支持 --max,已忽略)');
|
|
4269
5846
|
}
|
|
4270
5847
|
console.log('\n开始压缩...\n');
|
|
4271
5848
|
|
|
4272
5849
|
try {
|
|
4273
|
-
if (
|
|
4274
|
-
const cmd = `"${zipTool.cmd}"
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
console.log('✓ 压缩完成!');
|
|
4280
|
-
console.log(' 输出文件:', outputPath);
|
|
4281
|
-
if (sizeMatch) {
|
|
4282
|
-
const sizeBytes = parseInt(sizeMatch[1]);
|
|
4283
|
-
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
|
|
4284
|
-
console.log(' 压缩大小:', sizeMB, 'MB');
|
|
4285
|
-
}
|
|
4286
|
-
if (filesMatch) {
|
|
4287
|
-
console.log(' 文件数量:', filesMatch[1]);
|
|
4288
|
-
}
|
|
4289
|
-
console.log();
|
|
4290
|
-
return;
|
|
5850
|
+
if (useZipCmd) {
|
|
5851
|
+
const cmd = `"${zipTool.cmd}" -${compressionLevel} -q -r "${outputPath}" "${absPath}"`;
|
|
5852
|
+
execSync(cmd, { stdio: 'ignore' });
|
|
5853
|
+
} else {
|
|
5854
|
+
await zipWithLibrary(absPath, outputPath);
|
|
4291
5855
|
}
|
|
4292
5856
|
|
|
4293
|
-
await zipWithLibrary(absPath, outputPath);
|
|
4294
5857
|
console.log('✓ 压缩完成!');
|
|
4295
5858
|
console.log(' 输出文件:', outputPath);
|
|
4296
5859
|
console.log();
|
|
@@ -4300,7 +5863,7 @@ async function cmdZip(targetPath, options = {}) {
|
|
|
4300
5863
|
}
|
|
4301
5864
|
}
|
|
4302
5865
|
|
|
4303
|
-
// 解压(
|
|
5866
|
+
// 解压(zip-lib)
|
|
4304
5867
|
async function cmdUnzip(zipPath, outputDir) {
|
|
4305
5868
|
if (!zipPath) {
|
|
4306
5869
|
console.error('用法: codexmate unzip <zip文件路径> [输出目录]');
|
|
@@ -4332,24 +5895,10 @@ async function cmdUnzip(zipPath, outputDir) {
|
|
|
4332
5895
|
console.log('\n解压配置:');
|
|
4333
5896
|
console.log(' 源文件:', absZipPath);
|
|
4334
5897
|
console.log(' 输出目录:', absOutputDir);
|
|
4335
|
-
console.log(' 解压工具:',
|
|
4336
|
-
console.log(' 多线程:', unzipTool.type === '7z' ? '启用' : '未启用(JS 库)');
|
|
5898
|
+
console.log(' 解压工具:', 'zip-lib');
|
|
4337
5899
|
console.log('\n开始解压...\n');
|
|
4338
5900
|
|
|
4339
5901
|
try {
|
|
4340
|
-
if (unzipTool.type === '7z') {
|
|
4341
|
-
const cmd = `"${unzipTool.cmd}" x -mmt=on -o"${absOutputDir}" "${absZipPath}" -y`;
|
|
4342
|
-
const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
|
4343
|
-
const filesMatch = result.match(/(\d+)\s*files/);
|
|
4344
|
-
console.log('✓ 解压完成!');
|
|
4345
|
-
console.log(' 输出目录:', absOutputDir);
|
|
4346
|
-
if (filesMatch) {
|
|
4347
|
-
console.log(' 文件数量:', filesMatch[1]);
|
|
4348
|
-
}
|
|
4349
|
-
console.log();
|
|
4350
|
-
return;
|
|
4351
|
-
}
|
|
4352
|
-
|
|
4353
5902
|
await unzipWithLibrary(absZipPath, absOutputDir);
|
|
4354
5903
|
console.log('✓ 解压完成!');
|
|
4355
5904
|
console.log(' 输出目录:', absOutputDir);
|
|
@@ -4568,16 +6117,52 @@ function formatHostForUrl(host) {
|
|
|
4568
6117
|
return value;
|
|
4569
6118
|
}
|
|
4570
6119
|
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
const
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
6120
|
+
function watchPathsForRestart(targets, onChange) {
|
|
6121
|
+
const disposers = [];
|
|
6122
|
+
const debounceMs = 300;
|
|
6123
|
+
let timer = null;
|
|
6124
|
+
|
|
6125
|
+
const trigger = (info) => {
|
|
6126
|
+
if (timer) clearTimeout(timer);
|
|
6127
|
+
timer = setTimeout(() => {
|
|
6128
|
+
timer = null;
|
|
6129
|
+
onChange(info);
|
|
6130
|
+
}, debounceMs);
|
|
6131
|
+
};
|
|
6132
|
+
|
|
6133
|
+
const addWatcher = (target, recursive) => {
|
|
6134
|
+
if (!fs.existsSync(target)) return;
|
|
6135
|
+
try {
|
|
6136
|
+
const watcher = fs.watch(target, { recursive }, (eventType, filename) => {
|
|
6137
|
+
if (!filename) return;
|
|
6138
|
+
const lower = filename.toLowerCase();
|
|
6139
|
+
if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
|
|
6140
|
+
trigger({ target, eventType, filename });
|
|
6141
|
+
});
|
|
6142
|
+
disposers.push(() => watcher.close());
|
|
6143
|
+
return true;
|
|
6144
|
+
} catch (e) {
|
|
6145
|
+
return false;
|
|
6146
|
+
}
|
|
6147
|
+
};
|
|
6148
|
+
|
|
6149
|
+
for (const target of targets) {
|
|
6150
|
+
const ok = addWatcher(target, true);
|
|
6151
|
+
if (!ok) {
|
|
6152
|
+
addWatcher(target, false);
|
|
6153
|
+
}
|
|
4579
6154
|
}
|
|
4580
6155
|
|
|
6156
|
+
return () => {
|
|
6157
|
+
for (const dispose of disposers) {
|
|
6158
|
+
try { dispose(); } catch (_) {}
|
|
6159
|
+
}
|
|
6160
|
+
};
|
|
6161
|
+
}
|
|
6162
|
+
|
|
6163
|
+
function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser }) {
|
|
6164
|
+
const connections = new Set();
|
|
6165
|
+
|
|
4581
6166
|
const server = http.createServer((req, res) => {
|
|
4582
6167
|
const requestPath = (req.url || '/').split('?')[0];
|
|
4583
6168
|
if (requestPath === '/api') {
|
|
@@ -4593,15 +6178,20 @@ function cmdStart(options = {}) {
|
|
|
4593
6178
|
const statusConfigResult = readConfigOrVirtualDefault();
|
|
4594
6179
|
const config = statusConfigResult.config;
|
|
4595
6180
|
const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
|
|
6181
|
+
const modelReasoningEffort = typeof config.model_reasoning_effort === 'string' ? config.model_reasoning_effort.trim() : '';
|
|
4596
6182
|
result = {
|
|
4597
6183
|
provider: config.model_provider || '未设置',
|
|
4598
6184
|
model: config.model || '未设置',
|
|
4599
6185
|
serviceTier,
|
|
6186
|
+
modelReasoningEffort,
|
|
4600
6187
|
configReady: !statusConfigResult.isVirtual,
|
|
4601
6188
|
configNotice: statusConfigResult.reason || '',
|
|
4602
6189
|
initNotice: consumeInitNotice()
|
|
4603
6190
|
};
|
|
4604
6191
|
break;
|
|
6192
|
+
case 'install-status':
|
|
6193
|
+
result = buildInstallStatusReport();
|
|
6194
|
+
break;
|
|
4605
6195
|
case 'list':
|
|
4606
6196
|
const listConfigResult = readConfigOrVirtualDefault();
|
|
4607
6197
|
const listConfig = listConfigResult.config;
|
|
@@ -4614,20 +6204,27 @@ function cmdStart(options = {}) {
|
|
|
4614
6204
|
url: p.base_url || '',
|
|
4615
6205
|
key: maskKey(p.preferred_auth_method || ''),
|
|
4616
6206
|
hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
|
|
4617
|
-
current: name === current
|
|
6207
|
+
current: name === current,
|
|
6208
|
+
readOnly: isBuiltinProxyProvider(name),
|
|
6209
|
+
nonDeletable: isNonDeletableProvider(name),
|
|
6210
|
+
nonEditable: isNonEditableProvider(name)
|
|
4618
6211
|
}))
|
|
4619
6212
|
};
|
|
4620
6213
|
break;
|
|
4621
6214
|
case 'models':
|
|
4622
6215
|
{
|
|
4623
6216
|
const providerName = params && typeof params.provider === 'string' ? params.provider : '';
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
result = { error: res.error, models: [], source: 'remote' };
|
|
4627
|
-
} else if (res.unlimited) {
|
|
4628
|
-
result = { models: [], source: 'remote', provider: res.provider || '', unlimited: true };
|
|
6217
|
+
if (!providerName) {
|
|
6218
|
+
result = { error: 'Provider name is required' };
|
|
4629
6219
|
} else {
|
|
4630
|
-
|
|
6220
|
+
const res = await fetchProviderModels(providerName);
|
|
6221
|
+
if (res.error) {
|
|
6222
|
+
result = { error: res.error, models: [], source: 'remote' };
|
|
6223
|
+
} else if (res.unlimited) {
|
|
6224
|
+
result = { models: [], source: 'remote', provider: res.provider || '', unlimited: true };
|
|
6225
|
+
} else {
|
|
6226
|
+
result = { models: res.models || [], source: 'remote', provider: res.provider || '' };
|
|
6227
|
+
}
|
|
4631
6228
|
}
|
|
4632
6229
|
}
|
|
4633
6230
|
break;
|
|
@@ -4635,13 +6232,17 @@ function cmdStart(options = {}) {
|
|
|
4635
6232
|
{
|
|
4636
6233
|
const baseUrl = params && typeof params.baseUrl === 'string' ? params.baseUrl : '';
|
|
4637
6234
|
const apiKey = params && typeof params.apiKey === 'string' ? params.apiKey : '';
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
result = { error: res.error, models: [], source: 'remote' };
|
|
4641
|
-
} else if (res.unlimited) {
|
|
4642
|
-
result = { models: [], source: 'remote', unlimited: true };
|
|
6235
|
+
if (!baseUrl) {
|
|
6236
|
+
result = { error: 'Base URL is required' };
|
|
4643
6237
|
} else {
|
|
4644
|
-
|
|
6238
|
+
const res = await fetchModelsFromBaseUrl(baseUrl, apiKey);
|
|
6239
|
+
if (res.error) {
|
|
6240
|
+
result = { error: res.error, models: [], source: 'remote' };
|
|
6241
|
+
} else if (res.unlimited) {
|
|
6242
|
+
result = { models: [], source: 'remote', unlimited: true };
|
|
6243
|
+
} else {
|
|
6244
|
+
result = { models: res.models || [], source: 'remote' };
|
|
6245
|
+
}
|
|
4645
6246
|
}
|
|
4646
6247
|
}
|
|
4647
6248
|
break;
|
|
@@ -4651,6 +6252,15 @@ function cmdStart(options = {}) {
|
|
|
4651
6252
|
case 'apply-config-template':
|
|
4652
6253
|
result = applyConfigTemplate(params || {});
|
|
4653
6254
|
break;
|
|
6255
|
+
case 'add-provider':
|
|
6256
|
+
result = addProviderToConfig(params || {});
|
|
6257
|
+
break;
|
|
6258
|
+
case 'update-provider':
|
|
6259
|
+
result = updateProviderInConfig(params || {});
|
|
6260
|
+
break;
|
|
6261
|
+
case 'delete-provider':
|
|
6262
|
+
result = deleteProviderFromConfig(params || {});
|
|
6263
|
+
break;
|
|
4654
6264
|
case 'get-recent-configs':
|
|
4655
6265
|
result = { items: readRecentConfigs() };
|
|
4656
6266
|
break;
|
|
@@ -4669,6 +6279,9 @@ function cmdStart(options = {}) {
|
|
|
4669
6279
|
case 'apply-openclaw-config':
|
|
4670
6280
|
result = applyOpenclawConfig(params || {});
|
|
4671
6281
|
break;
|
|
6282
|
+
case 'reset-config':
|
|
6283
|
+
result = resetConfigToDefault();
|
|
6284
|
+
break;
|
|
4672
6285
|
case 'get-openclaw-agents-file':
|
|
4673
6286
|
result = readOpenclawAgentsFile();
|
|
4674
6287
|
break;
|
|
@@ -4726,14 +6339,29 @@ function cmdStart(options = {}) {
|
|
|
4726
6339
|
break;
|
|
4727
6340
|
}
|
|
4728
6341
|
case 'list-sessions':
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
6342
|
+
{
|
|
6343
|
+
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
6344
|
+
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
6345
|
+
result = { error: 'Invalid source. Must be codex, claude, or all' };
|
|
6346
|
+
} else {
|
|
6347
|
+
result = {
|
|
6348
|
+
sessions: listAllSessions(params),
|
|
6349
|
+
source: source || 'all'
|
|
6350
|
+
};
|
|
6351
|
+
}
|
|
6352
|
+
}
|
|
4732
6353
|
break;
|
|
4733
6354
|
case 'list-session-paths':
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
6355
|
+
{
|
|
6356
|
+
const source = typeof params.source === 'string' ? params.source.trim().toLowerCase() : '';
|
|
6357
|
+
if (source && source !== 'codex' && source !== 'claude' && source !== 'all') {
|
|
6358
|
+
result = { error: 'Invalid source. Must be codex, claude, or all' };
|
|
6359
|
+
} else {
|
|
6360
|
+
result = {
|
|
6361
|
+
paths: listSessionPaths(params)
|
|
6362
|
+
};
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
4737
6365
|
break;
|
|
4738
6366
|
case 'export-session':
|
|
4739
6367
|
result = await exportSessionData(params);
|
|
@@ -4750,15 +6378,78 @@ function cmdStart(options = {}) {
|
|
|
4750
6378
|
case 'session-plain':
|
|
4751
6379
|
result = await readSessionPlain(params);
|
|
4752
6380
|
break;
|
|
6381
|
+
case 'download-claude-dir':
|
|
6382
|
+
result = await prepareClaudeDirDownload();
|
|
6383
|
+
break;
|
|
6384
|
+
case 'download-codex-dir':
|
|
6385
|
+
result = await prepareCodexDirDownload();
|
|
6386
|
+
break;
|
|
6387
|
+
case 'restore-claude-dir':
|
|
6388
|
+
result = await restoreClaudeDir(params || {});
|
|
6389
|
+
break;
|
|
6390
|
+
case 'restore-codex-dir':
|
|
6391
|
+
result = await restoreCodexDir(params || {});
|
|
6392
|
+
break;
|
|
6393
|
+
case 'list-auth-profiles':
|
|
6394
|
+
result = {
|
|
6395
|
+
profiles: listAuthProfilesInfo()
|
|
6396
|
+
};
|
|
6397
|
+
break;
|
|
6398
|
+
case 'import-auth-profile':
|
|
6399
|
+
result = importAuthProfileFromUpload(params || {});
|
|
6400
|
+
break;
|
|
6401
|
+
case 'switch-auth-profile':
|
|
6402
|
+
{
|
|
6403
|
+
const profileName = params && typeof params.name === 'string' ? params.name.trim() : '';
|
|
6404
|
+
if (!profileName) {
|
|
6405
|
+
result = { error: '认证名称不能为空' };
|
|
6406
|
+
} else {
|
|
6407
|
+
try {
|
|
6408
|
+
result = switchAuthProfile(profileName, { silent: true });
|
|
6409
|
+
} catch (e) {
|
|
6410
|
+
result = { error: e.message || '切换认证失败' };
|
|
6411
|
+
}
|
|
6412
|
+
}
|
|
6413
|
+
}
|
|
6414
|
+
break;
|
|
6415
|
+
case 'delete-auth-profile':
|
|
6416
|
+
result = deleteAuthProfile(params && params.name ? params.name : '');
|
|
6417
|
+
break;
|
|
6418
|
+
case 'proxy-status':
|
|
6419
|
+
result = getBuiltinProxyStatus();
|
|
6420
|
+
break;
|
|
6421
|
+
case 'proxy-save-config':
|
|
6422
|
+
result = saveBuiltinProxySettings(params || {});
|
|
6423
|
+
break;
|
|
6424
|
+
case 'proxy-start':
|
|
6425
|
+
result = await startBuiltinProxyRuntime(params || {});
|
|
6426
|
+
break;
|
|
6427
|
+
case 'proxy-stop':
|
|
6428
|
+
result = await stopBuiltinProxyRuntime();
|
|
6429
|
+
break;
|
|
6430
|
+
case 'proxy-enable-codex-default':
|
|
6431
|
+
result = await ensureBuiltinProxyForCodexDefault(params || {});
|
|
6432
|
+
break;
|
|
6433
|
+
case 'proxy-apply-provider':
|
|
6434
|
+
result = applyBuiltinProxyProvider(params || {});
|
|
6435
|
+
break;
|
|
4753
6436
|
default:
|
|
4754
6437
|
result = { error: '未知操作' };
|
|
4755
6438
|
}
|
|
4756
6439
|
|
|
4757
|
-
|
|
4758
|
-
res.
|
|
6440
|
+
const responseBody = JSON.stringify(result, null, 2);
|
|
6441
|
+
res.writeHead(200, {
|
|
6442
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
6443
|
+
'Content-Length': Buffer.byteLength(responseBody, 'utf-8')
|
|
6444
|
+
});
|
|
6445
|
+
res.end(responseBody, 'utf-8');
|
|
4759
6446
|
} catch (e) {
|
|
4760
|
-
|
|
4761
|
-
res.
|
|
6447
|
+
const errorBody = JSON.stringify({ error: e.message }, null, 2);
|
|
6448
|
+
res.writeHead(500, {
|
|
6449
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
6450
|
+
'Content-Length': Buffer.byteLength(errorBody, 'utf-8')
|
|
6451
|
+
});
|
|
6452
|
+
res.end(errorBody, 'utf-8');
|
|
4762
6453
|
}
|
|
4763
6454
|
});
|
|
4764
6455
|
} else if (requestPath.startsWith('/web-ui/')) {
|
|
@@ -4777,6 +6468,8 @@ function cmdStart(options = {}) {
|
|
|
4777
6468
|
const ext = path.extname(filePath).toLowerCase();
|
|
4778
6469
|
const mime = ext === '.js' || ext === '.mjs'
|
|
4779
6470
|
? 'application/javascript; charset=utf-8'
|
|
6471
|
+
: ext === '.html'
|
|
6472
|
+
? 'text/html; charset=utf-8'
|
|
4780
6473
|
: ext === '.css'
|
|
4781
6474
|
? 'text/css; charset=utf-8'
|
|
4782
6475
|
: ext === '.json'
|
|
@@ -4784,6 +6477,34 @@ function cmdStart(options = {}) {
|
|
|
4784
6477
|
: 'application/octet-stream';
|
|
4785
6478
|
res.writeHead(200, { 'Content-Type': mime });
|
|
4786
6479
|
fs.createReadStream(filePath).pipe(res);
|
|
6480
|
+
} else if (requestPath.startsWith('/download/')) {
|
|
6481
|
+
const fileName = requestPath.slice('/download/'.length);
|
|
6482
|
+
const decodedFileName = decodeURIComponent(fileName);
|
|
6483
|
+
const tempDir = os.tmpdir();
|
|
6484
|
+
const filePath = path.join(tempDir, decodedFileName);
|
|
6485
|
+
|
|
6486
|
+
if (!isPathInside(filePath, tempDir)) {
|
|
6487
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
6488
|
+
res.end('Forbidden');
|
|
6489
|
+
return;
|
|
6490
|
+
}
|
|
6491
|
+
if (!fs.existsSync(filePath)) {
|
|
6492
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
6493
|
+
res.end('File Not Found');
|
|
6494
|
+
return;
|
|
6495
|
+
}
|
|
6496
|
+
const stat = fs.statSync(filePath);
|
|
6497
|
+
if (!stat.isFile()) {
|
|
6498
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
6499
|
+
res.end('Not a File');
|
|
6500
|
+
return;
|
|
6501
|
+
}
|
|
6502
|
+
res.writeHead(200, {
|
|
6503
|
+
'Content-Type': 'application/zip',
|
|
6504
|
+
'Content-Disposition': `attachment; filename="${path.basename(filePath)}"`,
|
|
6505
|
+
'Content-Length': stat.size
|
|
6506
|
+
});
|
|
6507
|
+
fs.createReadStream(filePath).pipe(res);
|
|
4787
6508
|
} else if (requestPath.startsWith('/res/')) {
|
|
4788
6509
|
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
|
|
4789
6510
|
const filePath = path.join(__dirname, normalized);
|
|
@@ -4800,6 +6521,8 @@ function cmdStart(options = {}) {
|
|
|
4800
6521
|
const ext = path.extname(filePath).toLowerCase();
|
|
4801
6522
|
const mime = ext === '.js'
|
|
4802
6523
|
? 'application/javascript; charset=utf-8'
|
|
6524
|
+
: ext === '.html'
|
|
6525
|
+
? 'text/html; charset=utf-8'
|
|
4803
6526
|
: ext === '.json'
|
|
4804
6527
|
? 'application/json; charset=utf-8'
|
|
4805
6528
|
: 'application/octet-stream';
|
|
@@ -4810,58 +6533,458 @@ function cmdStart(options = {}) {
|
|
|
4810
6533
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
4811
6534
|
res.end(html);
|
|
4812
6535
|
}
|
|
4813
|
-
});
|
|
6536
|
+
});
|
|
6537
|
+
|
|
6538
|
+
server.on('connection', (socket) => {
|
|
6539
|
+
connections.add(socket);
|
|
6540
|
+
socket.on('close', () => connections.delete(socket));
|
|
6541
|
+
});
|
|
6542
|
+
|
|
6543
|
+
server.once('error', (err) => {
|
|
6544
|
+
if (err && err.code === 'EADDRINUSE') {
|
|
6545
|
+
console.error(`! 启动失败: 端口 ${port} 已被占用,可能有残留的 codexmate run 实例。`);
|
|
6546
|
+
console.error(' 请先停止旧实例或更换端口后重试。');
|
|
6547
|
+
} else {
|
|
6548
|
+
console.error('! 启动 Web UI 失败:', err && err.message ? err.message : err);
|
|
6549
|
+
}
|
|
6550
|
+
process.exit(1);
|
|
6551
|
+
});
|
|
6552
|
+
|
|
6553
|
+
const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
|
|
6554
|
+
const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
|
|
6555
|
+
server.listen(port, host, () => {
|
|
6556
|
+
console.log('\n✓ Web UI 已启动:', openUrl);
|
|
6557
|
+
if (host && host !== openHost) {
|
|
6558
|
+
console.log(' 监听地址:', host);
|
|
6559
|
+
}
|
|
6560
|
+
console.log(' 按 Ctrl+C 退出\n');
|
|
6561
|
+
if (isAnyAddressHost(host)) {
|
|
6562
|
+
console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
|
|
6563
|
+
console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
|
|
6564
|
+
}
|
|
6565
|
+
|
|
6566
|
+
if (!process.env.CODEXMATE_NO_BROWSER && openBrowser) {
|
|
6567
|
+
const platform = process.platform;
|
|
6568
|
+
let command;
|
|
6569
|
+
const url = openUrl;
|
|
6570
|
+
|
|
6571
|
+
if (platform === 'win32') {
|
|
6572
|
+
command = `start \"\" \"${url}\"`;
|
|
6573
|
+
} else if (platform === 'darwin') {
|
|
6574
|
+
command = `open \"${url}\"`;
|
|
6575
|
+
} else {
|
|
6576
|
+
command = `xdg-open \"${url}\"`;
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
exec(command, (error) => {
|
|
6580
|
+
if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
|
|
6581
|
+
});
|
|
6582
|
+
}
|
|
6583
|
+
});
|
|
6584
|
+
|
|
6585
|
+
const stop = () => new Promise((resolve) => {
|
|
6586
|
+
let done = false;
|
|
6587
|
+
const finish = () => {
|
|
6588
|
+
if (done) return;
|
|
6589
|
+
done = true;
|
|
6590
|
+
for (const socket of connections) {
|
|
6591
|
+
try { socket.destroy(); } catch (_) {}
|
|
6592
|
+
}
|
|
6593
|
+
connections.clear();
|
|
6594
|
+
resolve();
|
|
6595
|
+
};
|
|
6596
|
+
|
|
6597
|
+
if (!server.listening) {
|
|
6598
|
+
finish();
|
|
6599
|
+
return;
|
|
6600
|
+
}
|
|
6601
|
+
|
|
6602
|
+
server.close(() => finish());
|
|
6603
|
+
setTimeout(() => finish(), 800);
|
|
6604
|
+
});
|
|
6605
|
+
|
|
6606
|
+
return { server, stop };
|
|
6607
|
+
}
|
|
6608
|
+
|
|
6609
|
+
// 打开 Web UI
|
|
6610
|
+
function cmdStart(options = {}) {
|
|
6611
|
+
const webDir = path.join(__dirname, 'web-ui');
|
|
6612
|
+
const newHtmlPath = path.join(webDir, 'index.html');
|
|
6613
|
+
const legacyHtmlPath = path.join(__dirname, 'web-ui.html');
|
|
6614
|
+
const htmlPath = fs.existsSync(newHtmlPath) ? newHtmlPath : legacyHtmlPath;
|
|
6615
|
+
const assetsDir = path.join(__dirname, 'res');
|
|
6616
|
+
if (!fs.existsSync(htmlPath)) {
|
|
6617
|
+
console.error('错误: Web UI 页面不存在(尝试路径: web-ui/index.html, web-ui.html)');
|
|
6618
|
+
process.exit(1);
|
|
6619
|
+
}
|
|
6620
|
+
|
|
6621
|
+
const port = resolveWebPort();
|
|
6622
|
+
const host = resolveWebHost(options);
|
|
6623
|
+
|
|
6624
|
+
let serverHandle = createWebServer({
|
|
6625
|
+
htmlPath,
|
|
6626
|
+
assetsDir,
|
|
6627
|
+
webDir,
|
|
6628
|
+
host,
|
|
6629
|
+
port,
|
|
6630
|
+
openBrowser: true
|
|
6631
|
+
});
|
|
6632
|
+
|
|
6633
|
+
const proxySettings = readBuiltinProxySettings();
|
|
6634
|
+
const shouldAutoStartProxy = proxySettings.enabled || hasCodexConfigReadyForProxy();
|
|
6635
|
+
if (shouldAutoStartProxy) {
|
|
6636
|
+
ensureBuiltinProxyForCodexDefault({
|
|
6637
|
+
...proxySettings,
|
|
6638
|
+
switchToProxy: false
|
|
6639
|
+
}).then((res) => {
|
|
6640
|
+
if (res && res.success && res.runtime && res.runtime.listenUrl) {
|
|
6641
|
+
const entryProvider = res.runtime.provider || DEFAULT_LOCAL_PROVIDER_NAME;
|
|
6642
|
+
const upstreamLabel = res.runtime.upstreamProvider ? `(上游: ${res.runtime.upstreamProvider})` : '';
|
|
6643
|
+
console.log(`~ 内建代理已启动(${entryProvider}): ${res.runtime.listenUrl}${upstreamLabel}`);
|
|
6644
|
+
} else if (res && res.error) {
|
|
6645
|
+
console.warn(`! 内建代理启动失败: ${res.error}`);
|
|
6646
|
+
}
|
|
6647
|
+
}).catch((err) => {
|
|
6648
|
+
console.warn(`! 内建代理启动失败: ${err && err.message ? err.message : err}`);
|
|
6649
|
+
});
|
|
6650
|
+
}
|
|
6651
|
+
|
|
6652
|
+
const stopWatch = watchPathsForRestart(
|
|
6653
|
+
[webDir, legacyHtmlPath],
|
|
6654
|
+
async (info) => {
|
|
6655
|
+
const fileLabel = info && info.filename ? info.filename : (info && info.target ? path.basename(info.target) : 'unknown');
|
|
6656
|
+
console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`);
|
|
6657
|
+
console.log(' 正在停止旧服务...');
|
|
6658
|
+
try {
|
|
6659
|
+
await serverHandle.stop();
|
|
6660
|
+
console.log(' 旧服务已停止');
|
|
6661
|
+
} catch (e) {
|
|
6662
|
+
console.warn('! 停止旧服务失败:', e.message || e);
|
|
6663
|
+
}
|
|
6664
|
+
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
6665
|
+
try {
|
|
6666
|
+
serverHandle = createWebServer({
|
|
6667
|
+
htmlPath,
|
|
6668
|
+
assetsDir,
|
|
6669
|
+
webDir,
|
|
6670
|
+
host,
|
|
6671
|
+
port,
|
|
6672
|
+
openBrowser: false
|
|
6673
|
+
});
|
|
6674
|
+
console.log('✓ 已重启 Web UI 服务\n');
|
|
6675
|
+
} catch (e) {
|
|
6676
|
+
console.error('! 重启失败:', e.message || e);
|
|
6677
|
+
}
|
|
6678
|
+
}
|
|
6679
|
+
);
|
|
6680
|
+
|
|
6681
|
+
const handleExit = () => {
|
|
6682
|
+
stopWatch();
|
|
6683
|
+
Promise.allSettled([
|
|
6684
|
+
serverHandle.stop(),
|
|
6685
|
+
stopBuiltinProxyRuntime()
|
|
6686
|
+
]).finally(() => process.exit(0));
|
|
6687
|
+
};
|
|
6688
|
+
|
|
6689
|
+
process.on('SIGINT', handleExit);
|
|
6690
|
+
process.on('SIGTERM', handleExit);
|
|
6691
|
+
}
|
|
6692
|
+
|
|
6693
|
+
function cmdAuth(args = []) {
|
|
6694
|
+
const subcommand = (args[0] || 'list').toLowerCase();
|
|
6695
|
+
|
|
6696
|
+
if (subcommand === 'list') {
|
|
6697
|
+
const profiles = listAuthProfilesInfo();
|
|
6698
|
+
if (profiles.length === 0) {
|
|
6699
|
+
console.log('\n认证列表: (空)\n');
|
|
6700
|
+
return;
|
|
6701
|
+
}
|
|
6702
|
+
console.log('\n认证列表:');
|
|
6703
|
+
profiles.forEach((profile) => {
|
|
6704
|
+
const marker = profile.current ? '●' : ' ';
|
|
6705
|
+
const type = profile.type || 'unknown';
|
|
6706
|
+
const email = profile.email || '(无邮箱)';
|
|
6707
|
+
console.log(` ${marker} ${profile.name} [${type}] ${email}`);
|
|
6708
|
+
});
|
|
6709
|
+
console.log();
|
|
6710
|
+
return;
|
|
6711
|
+
}
|
|
6712
|
+
|
|
6713
|
+
if (subcommand === 'status') {
|
|
6714
|
+
const profiles = listAuthProfilesInfo();
|
|
6715
|
+
const current = profiles.find((item) => item.current);
|
|
6716
|
+
if (!current) {
|
|
6717
|
+
console.log('\n当前认证: 未设置\n');
|
|
6718
|
+
return;
|
|
6719
|
+
}
|
|
6720
|
+
console.log('\n当前认证:');
|
|
6721
|
+
console.log(' 名称:', current.name);
|
|
6722
|
+
console.log(' 类型:', current.type || 'unknown');
|
|
6723
|
+
if (current.email) {
|
|
6724
|
+
console.log(' 账号:', current.email);
|
|
6725
|
+
}
|
|
6726
|
+
if (current.expired) {
|
|
6727
|
+
console.log(' 过期时间:', current.expired);
|
|
6728
|
+
}
|
|
6729
|
+
console.log();
|
|
6730
|
+
return;
|
|
6731
|
+
}
|
|
6732
|
+
|
|
6733
|
+
if (subcommand === 'import' || subcommand === 'upload') {
|
|
6734
|
+
const filePath = args[1];
|
|
6735
|
+
const nameArg = args[2] && !args[2].startsWith('--') ? args[2] : '';
|
|
6736
|
+
const noActivate = args.includes('--no-activate');
|
|
6737
|
+
if (!filePath) {
|
|
6738
|
+
throw new Error('用法: codexmate auth import <json文件路径> [名称] [--no-activate]');
|
|
6739
|
+
}
|
|
6740
|
+
const result = importAuthProfileFromFile(filePath, {
|
|
6741
|
+
name: nameArg,
|
|
6742
|
+
activate: !noActivate
|
|
6743
|
+
});
|
|
6744
|
+
console.log(`✓ 已导入认证: ${result.profile.name}`);
|
|
6745
|
+
if (result.profile.email) {
|
|
6746
|
+
console.log(` 账号: ${result.profile.email}`);
|
|
6747
|
+
}
|
|
6748
|
+
if (!noActivate) {
|
|
6749
|
+
console.log(' 已自动切换为当前认证');
|
|
6750
|
+
}
|
|
6751
|
+
console.log();
|
|
6752
|
+
return;
|
|
6753
|
+
}
|
|
6754
|
+
|
|
6755
|
+
if (subcommand === 'switch' || subcommand === 'use') {
|
|
6756
|
+
const name = args[1];
|
|
6757
|
+
if (!name) {
|
|
6758
|
+
throw new Error('用法: codexmate auth switch <名称>');
|
|
6759
|
+
}
|
|
6760
|
+
switchAuthProfile(name);
|
|
6761
|
+
return;
|
|
6762
|
+
}
|
|
6763
|
+
|
|
6764
|
+
if (subcommand === 'delete' || subcommand === 'remove') {
|
|
6765
|
+
const name = args[1];
|
|
6766
|
+
if (!name) {
|
|
6767
|
+
throw new Error('用法: codexmate auth delete <名称>');
|
|
6768
|
+
}
|
|
6769
|
+
const result = deleteAuthProfile(name);
|
|
6770
|
+
if (result.error) {
|
|
6771
|
+
throw new Error(result.error);
|
|
6772
|
+
}
|
|
6773
|
+
console.log(`✓ 已删除认证: ${name}`);
|
|
6774
|
+
if (result.switchedTo) {
|
|
6775
|
+
console.log(` 已自动切换到: ${result.switchedTo}`);
|
|
6776
|
+
}
|
|
6777
|
+
console.log();
|
|
6778
|
+
return;
|
|
6779
|
+
}
|
|
6780
|
+
|
|
6781
|
+
throw new Error(`未知 auth 子命令: ${subcommand}`);
|
|
6782
|
+
}
|
|
6783
|
+
|
|
6784
|
+
function parseProxyCliOptions(args = []) {
|
|
6785
|
+
const payload = {};
|
|
6786
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
6787
|
+
const arg = args[i];
|
|
6788
|
+
if (arg === '--provider') {
|
|
6789
|
+
payload.provider = args[i + 1] || '';
|
|
6790
|
+
i += 1;
|
|
6791
|
+
continue;
|
|
6792
|
+
}
|
|
6793
|
+
if (arg === '--host') {
|
|
6794
|
+
payload.host = args[i + 1] || '';
|
|
6795
|
+
i += 1;
|
|
6796
|
+
continue;
|
|
6797
|
+
}
|
|
6798
|
+
if (arg === '--port') {
|
|
6799
|
+
const raw = args[i + 1];
|
|
6800
|
+
i += 1;
|
|
6801
|
+
if (raw === undefined) {
|
|
6802
|
+
return { error: '--port 缺少值' };
|
|
6803
|
+
}
|
|
6804
|
+
const port = parseInt(raw, 10);
|
|
6805
|
+
if (!Number.isFinite(port)) {
|
|
6806
|
+
return { error: '--port 必须是数字' };
|
|
6807
|
+
}
|
|
6808
|
+
payload.port = port;
|
|
6809
|
+
continue;
|
|
6810
|
+
}
|
|
6811
|
+
if (arg === '--auth-source') {
|
|
6812
|
+
payload.authSource = args[i + 1] || '';
|
|
6813
|
+
i += 1;
|
|
6814
|
+
continue;
|
|
6815
|
+
}
|
|
6816
|
+
if (arg === '--timeout-ms') {
|
|
6817
|
+
const raw = args[i + 1];
|
|
6818
|
+
i += 1;
|
|
6819
|
+
if (raw === undefined) {
|
|
6820
|
+
return { error: '--timeout-ms 缺少值' };
|
|
6821
|
+
}
|
|
6822
|
+
const timeoutMs = parseInt(raw, 10);
|
|
6823
|
+
if (!Number.isFinite(timeoutMs)) {
|
|
6824
|
+
return { error: '--timeout-ms 必须是数字' };
|
|
6825
|
+
}
|
|
6826
|
+
payload.timeoutMs = timeoutMs;
|
|
6827
|
+
continue;
|
|
6828
|
+
}
|
|
6829
|
+
if (arg === '--enable') {
|
|
6830
|
+
payload.enabled = true;
|
|
6831
|
+
continue;
|
|
6832
|
+
}
|
|
6833
|
+
if (arg === '--disable') {
|
|
6834
|
+
payload.enabled = false;
|
|
6835
|
+
continue;
|
|
6836
|
+
}
|
|
6837
|
+
if (arg === '--no-switch') {
|
|
6838
|
+
payload.switchToProxy = false;
|
|
6839
|
+
continue;
|
|
6840
|
+
}
|
|
6841
|
+
return { error: `未知参数: ${arg}` };
|
|
6842
|
+
}
|
|
6843
|
+
return { payload };
|
|
6844
|
+
}
|
|
4814
6845
|
|
|
4815
|
-
|
|
4816
|
-
const
|
|
4817
|
-
const
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
6846
|
+
async function cmdProxy(args = []) {
|
|
6847
|
+
const subcommand = (args[0] || 'status').toLowerCase();
|
|
6848
|
+
const optionResult = parseProxyCliOptions(args.slice(1));
|
|
6849
|
+
if (optionResult.error) {
|
|
6850
|
+
throw new Error(optionResult.error);
|
|
6851
|
+
}
|
|
6852
|
+
const options = optionResult.payload || {};
|
|
6853
|
+
|
|
6854
|
+
if (subcommand === 'status') {
|
|
6855
|
+
const status = getBuiltinProxyStatus();
|
|
6856
|
+
const settings = status.settings || DEFAULT_BUILTIN_PROXY_SETTINGS;
|
|
6857
|
+
console.log('\n内建代理状态:');
|
|
6858
|
+
console.log(' 运行中:', status.running ? '是' : '否');
|
|
6859
|
+
console.log(' 启用:', settings.enabled ? '是' : '否');
|
|
6860
|
+
console.log(' 监听:', buildProxyListenUrl(settings));
|
|
6861
|
+
console.log(' 上游 provider:', settings.provider || '(自动)');
|
|
6862
|
+
console.log(' 鉴权来源:', settings.authSource);
|
|
6863
|
+
if (status.runtime) {
|
|
6864
|
+
console.log(' 实际上游:', status.runtime.upstreamProvider);
|
|
6865
|
+
console.log(' 启动时间:', status.runtime.startedAt);
|
|
4828
6866
|
}
|
|
6867
|
+
console.log();
|
|
6868
|
+
return;
|
|
6869
|
+
}
|
|
4829
6870
|
|
|
4830
|
-
|
|
4831
|
-
const
|
|
4832
|
-
|
|
4833
|
-
|
|
6871
|
+
if (subcommand === 'set' || subcommand === 'config') {
|
|
6872
|
+
const result = saveBuiltinProxySettings(options);
|
|
6873
|
+
if (result.error) {
|
|
6874
|
+
throw new Error(result.error);
|
|
6875
|
+
}
|
|
6876
|
+
const settings = result.settings;
|
|
6877
|
+
console.log('✓ 内建代理配置已保存');
|
|
6878
|
+
console.log(' 监听:', buildProxyListenUrl(settings));
|
|
6879
|
+
console.log(' 上游 provider:', settings.provider || '(自动)');
|
|
6880
|
+
console.log(' 鉴权来源:', settings.authSource);
|
|
6881
|
+
console.log();
|
|
6882
|
+
return;
|
|
6883
|
+
}
|
|
4834
6884
|
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
6885
|
+
if (subcommand === 'apply' || subcommand === 'apply-provider') {
|
|
6886
|
+
const result = applyBuiltinProxyProvider({
|
|
6887
|
+
switchToProxy: options.switchToProxy !== false
|
|
6888
|
+
});
|
|
6889
|
+
if (result.error) {
|
|
6890
|
+
throw new Error(result.error);
|
|
6891
|
+
}
|
|
6892
|
+
console.log(`✓ 已写入本地代理 provider: ${result.provider}`);
|
|
6893
|
+
console.log(` URL: ${result.baseUrl}`);
|
|
6894
|
+
if (result.switched) {
|
|
6895
|
+
console.log(` 已切换到 ${result.provider}${result.model ? ` / ${result.model}` : ''}`);
|
|
4841
6896
|
}
|
|
6897
|
+
console.log();
|
|
6898
|
+
return;
|
|
6899
|
+
}
|
|
4842
6900
|
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
});
|
|
6901
|
+
if (subcommand === 'enable' || subcommand === 'default-codex') {
|
|
6902
|
+
const result = await ensureBuiltinProxyForCodexDefault(options);
|
|
6903
|
+
if (result.error) {
|
|
6904
|
+
throw new Error(result.error);
|
|
4848
6905
|
}
|
|
4849
|
-
|
|
6906
|
+
const listenUrl = result.runtime && result.runtime.listenUrl
|
|
6907
|
+
? result.runtime.listenUrl
|
|
6908
|
+
: buildProxyListenUrl(result.settings || DEFAULT_BUILTIN_PROXY_SETTINGS);
|
|
6909
|
+
console.log('✓ 已启用 Codex 内建代理默认模式');
|
|
6910
|
+
console.log(` 监听: ${listenUrl}`);
|
|
6911
|
+
if (result.runtime && result.runtime.upstreamProvider) {
|
|
6912
|
+
console.log(` 上游 provider: ${result.runtime.upstreamProvider}`);
|
|
6913
|
+
}
|
|
6914
|
+
console.log(` 当前 provider: ${result.provider}${result.model ? ` / ${result.model}` : ''}`);
|
|
6915
|
+
console.log();
|
|
6916
|
+
return;
|
|
6917
|
+
}
|
|
6918
|
+
|
|
6919
|
+
if (subcommand === 'start') {
|
|
6920
|
+
const result = await startBuiltinProxyRuntime({
|
|
6921
|
+
...options,
|
|
6922
|
+
enabled: true
|
|
6923
|
+
});
|
|
6924
|
+
if (result.error) {
|
|
6925
|
+
throw new Error(result.error);
|
|
6926
|
+
}
|
|
6927
|
+
console.log(`✓ 内建代理已启动: ${result.listenUrl}`);
|
|
6928
|
+
console.log(` 上游 provider: ${result.upstreamProvider}`);
|
|
6929
|
+
console.log(' 按 Ctrl+C 停止代理\n');
|
|
6930
|
+
|
|
6931
|
+
await new Promise((resolve) => {
|
|
6932
|
+
let stopping = false;
|
|
6933
|
+
const gracefulStop = async () => {
|
|
6934
|
+
if (stopping) return;
|
|
6935
|
+
stopping = true;
|
|
6936
|
+
await stopBuiltinProxyRuntime();
|
|
6937
|
+
resolve();
|
|
6938
|
+
};
|
|
6939
|
+
process.once('SIGINT', gracefulStop);
|
|
6940
|
+
process.once('SIGTERM', gracefulStop);
|
|
6941
|
+
});
|
|
6942
|
+
return;
|
|
6943
|
+
}
|
|
6944
|
+
|
|
6945
|
+
if (subcommand === 'stop') {
|
|
6946
|
+
await stopBuiltinProxyRuntime();
|
|
6947
|
+
console.log('✓ 内建代理已停止\n');
|
|
6948
|
+
return;
|
|
6949
|
+
}
|
|
6950
|
+
|
|
6951
|
+
throw new Error(`未知 proxy 子命令: ${subcommand}`);
|
|
4850
6952
|
}
|
|
4851
6953
|
|
|
4852
|
-
async function
|
|
6954
|
+
async function runProxyCommand(displayName, binNames, args = [], installTip = '') {
|
|
4853
6955
|
const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
|
|
4854
6956
|
const hasYolo = extraArgs.includes('--yolo');
|
|
4855
6957
|
const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
|
|
4856
6958
|
|
|
6959
|
+
const names = Array.isArray(binNames) ? binNames : [binNames];
|
|
6960
|
+
let selectedBin = names[0];
|
|
6961
|
+
let exists = false;
|
|
6962
|
+
|
|
6963
|
+
// Detect if any of the bin names exist
|
|
6964
|
+
for (const name of names) {
|
|
6965
|
+
if (commandExists(name, '--version')) {
|
|
6966
|
+
selectedBin = name;
|
|
6967
|
+
exists = true;
|
|
6968
|
+
break;
|
|
6969
|
+
}
|
|
6970
|
+
}
|
|
6971
|
+
|
|
6972
|
+
if (!exists) {
|
|
6973
|
+
let msg = `无法启动 ${displayName},请确认已安装并在 PATH 中。`;
|
|
6974
|
+
if (installTip) {
|
|
6975
|
+
msg += `\n安装建议: ${installTip}`;
|
|
6976
|
+
}
|
|
6977
|
+
throw new Error(msg);
|
|
6978
|
+
}
|
|
6979
|
+
|
|
4857
6980
|
return new Promise((resolve, reject) => {
|
|
4858
|
-
const child = spawn(
|
|
6981
|
+
const child = spawn(selectedBin, finalArgs, {
|
|
4859
6982
|
stdio: 'inherit',
|
|
4860
6983
|
shell: process.platform === 'win32'
|
|
4861
6984
|
});
|
|
4862
6985
|
|
|
4863
6986
|
child.on('error', (err) => {
|
|
4864
|
-
reject(new Error(
|
|
6987
|
+
reject(new Error(`运行 ${selectedBin} 失败: ${err.message}`));
|
|
4865
6988
|
});
|
|
4866
6989
|
|
|
4867
6990
|
child.on('exit', (code, signal) => {
|
|
@@ -4882,16 +7005,805 @@ async function cmdCodex(args = []) {
|
|
|
4882
7005
|
});
|
|
4883
7006
|
}
|
|
4884
7007
|
|
|
7008
|
+
async function cmdCodex(args = []) {
|
|
7009
|
+
const ensureResult = await ensureBuiltinProxyForCodexDefault({});
|
|
7010
|
+
if (!ensureResult || ensureResult.success !== true) {
|
|
7011
|
+
const message = ensureResult && ensureResult.error
|
|
7012
|
+
? ensureResult.error
|
|
7013
|
+
: '内建代理准备失败';
|
|
7014
|
+
throw new Error(message);
|
|
7015
|
+
}
|
|
7016
|
+
if (ensureResult.runtime && ensureResult.runtime.listenUrl) {
|
|
7017
|
+
console.log(`~ Codex 默认走内建代理: ${ensureResult.runtime.listenUrl}`);
|
|
7018
|
+
}
|
|
7019
|
+
return runProxyCommand('Codex', 'codex', args);
|
|
7020
|
+
}
|
|
7021
|
+
|
|
7022
|
+
async function cmdQwen(args = []) {
|
|
7023
|
+
return runProxyCommand('Qwen', ['qwen', 'qwen-code'], args, 'npm install -g @qwen-code/qwen-code');
|
|
7024
|
+
}
|
|
7025
|
+
|
|
7026
|
+
async function cmdGemini(args = []) {
|
|
7027
|
+
return runProxyCommand('Gemini', ['gemini', 'gemini-cli'], args, 'npm install -g @google/gemini-cli');
|
|
7028
|
+
}
|
|
7029
|
+
|
|
7030
|
+
function parseMcpOptions(args = []) {
|
|
7031
|
+
const options = {
|
|
7032
|
+
subcommand: 'serve',
|
|
7033
|
+
transport: 'stdio',
|
|
7034
|
+
allowWrite: false,
|
|
7035
|
+
help: false
|
|
7036
|
+
};
|
|
7037
|
+
|
|
7038
|
+
const argv = Array.isArray(args) ? [...args] : [];
|
|
7039
|
+
if (argv.length > 0 && !argv[0].startsWith('-')) {
|
|
7040
|
+
options.subcommand = String(argv.shift() || '').trim().toLowerCase() || 'serve';
|
|
7041
|
+
}
|
|
7042
|
+
|
|
7043
|
+
const envAllowWrite = typeof process.env.CODEXMATE_MCP_ALLOW_WRITE === 'string'
|
|
7044
|
+
&& ['1', 'true', 'yes', 'on'].includes(process.env.CODEXMATE_MCP_ALLOW_WRITE.trim().toLowerCase());
|
|
7045
|
+
options.allowWrite = envAllowWrite;
|
|
7046
|
+
|
|
7047
|
+
for (let i = 0; i < argv.length; i++) {
|
|
7048
|
+
const arg = argv[i];
|
|
7049
|
+
if (!arg) continue;
|
|
7050
|
+
if (arg === '--help' || arg === '-h') {
|
|
7051
|
+
options.help = true;
|
|
7052
|
+
continue;
|
|
7053
|
+
}
|
|
7054
|
+
if (arg === '--allow-write' || arg === '--allow-write-tools') {
|
|
7055
|
+
options.allowWrite = true;
|
|
7056
|
+
continue;
|
|
7057
|
+
}
|
|
7058
|
+
if (arg === '--read-only') {
|
|
7059
|
+
options.allowWrite = false;
|
|
7060
|
+
continue;
|
|
7061
|
+
}
|
|
7062
|
+
if (arg.startsWith('--transport=')) {
|
|
7063
|
+
options.transport = arg.slice('--transport='.length).trim().toLowerCase() || options.transport;
|
|
7064
|
+
continue;
|
|
7065
|
+
}
|
|
7066
|
+
if (arg === '--transport') {
|
|
7067
|
+
options.transport = String(argv[i + 1] || '').trim().toLowerCase() || options.transport;
|
|
7068
|
+
i += 1;
|
|
7069
|
+
continue;
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
|
|
7073
|
+
return options;
|
|
7074
|
+
}
|
|
7075
|
+
|
|
7076
|
+
function toMcpToolResult(payload) {
|
|
7077
|
+
const structured = payload === undefined
|
|
7078
|
+
? {}
|
|
7079
|
+
: (payload && typeof payload === 'object' ? payload : { value: payload });
|
|
7080
|
+
const hasError = !!(structured && typeof structured === 'object' && (
|
|
7081
|
+
(typeof structured.error === 'string' && structured.error.trim())
|
|
7082
|
+
|| structured.success === false
|
|
7083
|
+
));
|
|
7084
|
+
const text = JSON.stringify(structured, null, 2);
|
|
7085
|
+
const result = {
|
|
7086
|
+
content: [{ type: 'text', text }],
|
|
7087
|
+
structuredContent: structured
|
|
7088
|
+
};
|
|
7089
|
+
if (hasError) {
|
|
7090
|
+
result.isError = true;
|
|
7091
|
+
}
|
|
7092
|
+
return result;
|
|
7093
|
+
}
|
|
7094
|
+
|
|
7095
|
+
function buildMcpStatusPayload() {
|
|
7096
|
+
const statusConfigResult = readConfigOrVirtualDefault();
|
|
7097
|
+
const config = statusConfigResult.config;
|
|
7098
|
+
const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
|
|
7099
|
+
const modelReasoningEffort = typeof config.model_reasoning_effort === 'string' ? config.model_reasoning_effort.trim() : '';
|
|
7100
|
+
return {
|
|
7101
|
+
provider: config.model_provider || '未设置',
|
|
7102
|
+
model: config.model || '未设置',
|
|
7103
|
+
serviceTier,
|
|
7104
|
+
modelReasoningEffort,
|
|
7105
|
+
configReady: !statusConfigResult.isVirtual,
|
|
7106
|
+
configNotice: statusConfigResult.reason || '',
|
|
7107
|
+
initNotice: consumeInitNotice()
|
|
7108
|
+
};
|
|
7109
|
+
}
|
|
7110
|
+
|
|
7111
|
+
function buildMcpProviderListPayload() {
|
|
7112
|
+
const listConfigResult = readConfigOrVirtualDefault();
|
|
7113
|
+
const listConfig = listConfigResult.config;
|
|
7114
|
+
const providers = listConfig.model_providers || {};
|
|
7115
|
+
const current = listConfig.model_provider;
|
|
7116
|
+
return {
|
|
7117
|
+
configReady: !listConfigResult.isVirtual,
|
|
7118
|
+
providers: Object.entries(providers).map(([name, p]) => ({
|
|
7119
|
+
name,
|
|
7120
|
+
url: p.base_url || '',
|
|
7121
|
+
key: maskKey(p.preferred_auth_method || ''),
|
|
7122
|
+
hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
|
|
7123
|
+
current: name === current,
|
|
7124
|
+
readOnly: isBuiltinProxyProvider(name),
|
|
7125
|
+
nonDeletable: isNonDeletableProvider(name),
|
|
7126
|
+
nonEditable: isNonEditableProvider(name)
|
|
7127
|
+
}))
|
|
7128
|
+
};
|
|
7129
|
+
}
|
|
7130
|
+
|
|
7131
|
+
function buildMcpClaudeSettingsPayload() {
|
|
7132
|
+
const info = readClaudeSettingsInfo();
|
|
7133
|
+
if (!info || typeof info !== 'object') {
|
|
7134
|
+
return { error: '读取 Claude 配置失败' };
|
|
7135
|
+
}
|
|
7136
|
+
if (info.error) {
|
|
7137
|
+
return info;
|
|
7138
|
+
}
|
|
7139
|
+
|
|
7140
|
+
const apiKey = typeof info.apiKey === 'string' ? info.apiKey : '';
|
|
7141
|
+
const baseUrl = typeof info.baseUrl === 'string' ? info.baseUrl : '';
|
|
7142
|
+
const model = typeof info.model === 'string' ? info.model : '';
|
|
7143
|
+
const maskedApiKey = maskKey(apiKey);
|
|
7144
|
+
|
|
7145
|
+
return {
|
|
7146
|
+
exists: !!info.exists,
|
|
7147
|
+
targetPath: info.targetPath || CLAUDE_SETTINGS_FILE,
|
|
7148
|
+
apiKey: maskedApiKey,
|
|
7149
|
+
apiKeyMasked: maskedApiKey,
|
|
7150
|
+
baseUrl,
|
|
7151
|
+
model,
|
|
7152
|
+
env: {
|
|
7153
|
+
ANTHROPIC_API_KEY: maskedApiKey,
|
|
7154
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
7155
|
+
ANTHROPIC_MODEL: model
|
|
7156
|
+
},
|
|
7157
|
+
redacted: true
|
|
7158
|
+
};
|
|
7159
|
+
}
|
|
7160
|
+
|
|
7161
|
+
function normalizeMcpSource(value) {
|
|
7162
|
+
const source = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
7163
|
+
if (!source) return '';
|
|
7164
|
+
if (source === 'codex' || source === 'claude' || source === 'all') {
|
|
7165
|
+
return source;
|
|
7166
|
+
}
|
|
7167
|
+
return null;
|
|
7168
|
+
}
|
|
7169
|
+
|
|
7170
|
+
function createMcpTools(options = {}) {
|
|
7171
|
+
const allowWrite = !!options.allowWrite;
|
|
7172
|
+
const tools = [];
|
|
7173
|
+
|
|
7174
|
+
const pushTool = (tool) => {
|
|
7175
|
+
if (!tool || typeof tool !== 'object') return;
|
|
7176
|
+
if (!tool.readOnly && !allowWrite) return;
|
|
7177
|
+
tools.push({
|
|
7178
|
+
name: tool.name,
|
|
7179
|
+
description: tool.description,
|
|
7180
|
+
inputSchema: tool.inputSchema || { type: 'object', properties: {}, additionalProperties: false },
|
|
7181
|
+
annotations: {
|
|
7182
|
+
readOnlyHint: !!tool.readOnly
|
|
7183
|
+
},
|
|
7184
|
+
handler: async (args = {}) => {
|
|
7185
|
+
try {
|
|
7186
|
+
const payload = await tool.handler(args || {});
|
|
7187
|
+
return toMcpToolResult(payload);
|
|
7188
|
+
} catch (error) {
|
|
7189
|
+
return toMcpToolResult({
|
|
7190
|
+
error: error && error.message ? error.message : String(error || 'Tool execution failed')
|
|
7191
|
+
});
|
|
7192
|
+
}
|
|
7193
|
+
}
|
|
7194
|
+
});
|
|
7195
|
+
};
|
|
7196
|
+
|
|
7197
|
+
pushTool({
|
|
7198
|
+
name: 'codexmate.status.get',
|
|
7199
|
+
description: 'Get current provider/model status, config readiness and startup notice.',
|
|
7200
|
+
readOnly: true,
|
|
7201
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
7202
|
+
handler: async () => buildMcpStatusPayload()
|
|
7203
|
+
});
|
|
7204
|
+
|
|
7205
|
+
pushTool({
|
|
7206
|
+
name: 'codexmate.provider.list',
|
|
7207
|
+
description: 'List configured providers with masked key and active flags.',
|
|
7208
|
+
readOnly: true,
|
|
7209
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
7210
|
+
handler: async () => buildMcpProviderListPayload()
|
|
7211
|
+
});
|
|
7212
|
+
|
|
7213
|
+
pushTool({
|
|
7214
|
+
name: 'codexmate.model.list',
|
|
7215
|
+
description: 'List models from a provider. If provider is omitted, use current provider.',
|
|
7216
|
+
readOnly: true,
|
|
7217
|
+
inputSchema: {
|
|
7218
|
+
type: 'object',
|
|
7219
|
+
properties: {
|
|
7220
|
+
provider: { type: 'string' }
|
|
7221
|
+
},
|
|
7222
|
+
additionalProperties: false
|
|
7223
|
+
},
|
|
7224
|
+
handler: async (args = {}) => {
|
|
7225
|
+
const rawProvider = typeof args.provider === 'string' ? args.provider.trim() : '';
|
|
7226
|
+
let providerName = rawProvider;
|
|
7227
|
+
if (!providerName) {
|
|
7228
|
+
const cfg = readConfigOrVirtualDefault().config || {};
|
|
7229
|
+
providerName = typeof cfg.model_provider === 'string' ? cfg.model_provider.trim() : '';
|
|
7230
|
+
}
|
|
7231
|
+
if (!providerName) {
|
|
7232
|
+
return { error: 'Provider name is required' };
|
|
7233
|
+
}
|
|
7234
|
+
const res = await fetchProviderModels(providerName);
|
|
7235
|
+
if (res.error) {
|
|
7236
|
+
return { error: res.error, models: [], source: 'remote' };
|
|
7237
|
+
}
|
|
7238
|
+
if (res.unlimited) {
|
|
7239
|
+
return { models: [], source: 'remote', provider: res.provider || '', unlimited: true };
|
|
7240
|
+
}
|
|
7241
|
+
return { models: res.models || [], source: 'remote', provider: res.provider || '' };
|
|
7242
|
+
}
|
|
7243
|
+
});
|
|
7244
|
+
|
|
7245
|
+
pushTool({
|
|
7246
|
+
name: 'codexmate.config.template.get',
|
|
7247
|
+
description: 'Get Codex config template with optional provider/model/service tier/reasoning effort.',
|
|
7248
|
+
readOnly: true,
|
|
7249
|
+
inputSchema: {
|
|
7250
|
+
type: 'object',
|
|
7251
|
+
properties: {
|
|
7252
|
+
provider: { type: 'string' },
|
|
7253
|
+
model: { type: 'string' },
|
|
7254
|
+
serviceTier: { type: 'string' },
|
|
7255
|
+
reasoningEffort: { type: 'string' }
|
|
7256
|
+
},
|
|
7257
|
+
additionalProperties: false
|
|
7258
|
+
},
|
|
7259
|
+
handler: async (args = {}) => getConfigTemplate(args || {})
|
|
7260
|
+
});
|
|
7261
|
+
|
|
7262
|
+
pushTool({
|
|
7263
|
+
name: 'codexmate.claude.settings.get',
|
|
7264
|
+
description: 'Read Claude settings.json env values managed by codexmate.',
|
|
7265
|
+
readOnly: true,
|
|
7266
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
7267
|
+
handler: async () => buildMcpClaudeSettingsPayload()
|
|
7268
|
+
});
|
|
7269
|
+
|
|
7270
|
+
pushTool({
|
|
7271
|
+
name: 'codexmate.openclaw.config.get',
|
|
7272
|
+
description: 'Read OpenClaw config file content and metadata.',
|
|
7273
|
+
readOnly: true,
|
|
7274
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
7275
|
+
handler: async () => readOpenclawConfigFile()
|
|
7276
|
+
});
|
|
7277
|
+
|
|
7278
|
+
pushTool({
|
|
7279
|
+
name: 'codexmate.session.list',
|
|
7280
|
+
description: 'List sessions from codex/claude/all with filters.',
|
|
7281
|
+
readOnly: true,
|
|
7282
|
+
inputSchema: {
|
|
7283
|
+
type: 'object',
|
|
7284
|
+
properties: {
|
|
7285
|
+
source: { type: 'string' },
|
|
7286
|
+
pathFilter: { type: 'string' },
|
|
7287
|
+
query: { type: 'string' },
|
|
7288
|
+
roleFilter: { type: 'string' },
|
|
7289
|
+
timeRangePreset: { type: 'string' },
|
|
7290
|
+
limit: { type: 'number' },
|
|
7291
|
+
forceRefresh: { type: 'boolean' },
|
|
7292
|
+
queryMode: { type: 'string' },
|
|
7293
|
+
queryScope: { type: 'string' },
|
|
7294
|
+
contentScanLimit: { type: 'number' }
|
|
7295
|
+
},
|
|
7296
|
+
additionalProperties: false
|
|
7297
|
+
},
|
|
7298
|
+
handler: async (args = {}) => {
|
|
7299
|
+
const input = args && typeof args === 'object' ? args : {};
|
|
7300
|
+
const source = normalizeMcpSource(input.source);
|
|
7301
|
+
if (source === null) {
|
|
7302
|
+
return { error: 'Invalid source. Must be codex, claude, or all' };
|
|
7303
|
+
}
|
|
7304
|
+
const normalizedInput = {
|
|
7305
|
+
...input,
|
|
7306
|
+
source: source || 'all'
|
|
7307
|
+
};
|
|
7308
|
+
return {
|
|
7309
|
+
sessions: listAllSessions(normalizedInput),
|
|
7310
|
+
source: source || 'all'
|
|
7311
|
+
};
|
|
7312
|
+
}
|
|
7313
|
+
});
|
|
7314
|
+
|
|
7315
|
+
pushTool({
|
|
7316
|
+
name: 'codexmate.session.detail',
|
|
7317
|
+
description: 'Read a session detail by source + sessionId/file.',
|
|
7318
|
+
readOnly: true,
|
|
7319
|
+
inputSchema: {
|
|
7320
|
+
type: 'object',
|
|
7321
|
+
properties: {
|
|
7322
|
+
source: { type: 'string' },
|
|
7323
|
+
sessionId: { type: 'string' },
|
|
7324
|
+
file: { type: 'string' },
|
|
7325
|
+
maxMessages: { type: ['string', 'number'] }
|
|
7326
|
+
},
|
|
7327
|
+
additionalProperties: true
|
|
7328
|
+
},
|
|
7329
|
+
handler: async (args = {}) => readSessionDetail(args || {})
|
|
7330
|
+
});
|
|
7331
|
+
|
|
7332
|
+
pushTool({
|
|
7333
|
+
name: 'codexmate.session.export',
|
|
7334
|
+
description: 'Export session as markdown payload.',
|
|
7335
|
+
readOnly: true,
|
|
7336
|
+
inputSchema: {
|
|
7337
|
+
type: 'object',
|
|
7338
|
+
properties: {
|
|
7339
|
+
source: { type: 'string' },
|
|
7340
|
+
sessionId: { type: 'string' },
|
|
7341
|
+
file: { type: 'string' },
|
|
7342
|
+
maxMessages: { type: ['string', 'number'] }
|
|
7343
|
+
},
|
|
7344
|
+
additionalProperties: true
|
|
7345
|
+
},
|
|
7346
|
+
handler: async (args = {}) => exportSessionData(args || {})
|
|
7347
|
+
});
|
|
7348
|
+
|
|
7349
|
+
pushTool({
|
|
7350
|
+
name: 'codexmate.auth.profile.list',
|
|
7351
|
+
description: 'List codex auth profiles.',
|
|
7352
|
+
readOnly: true,
|
|
7353
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
7354
|
+
handler: async () => ({ profiles: listAuthProfilesInfo() })
|
|
7355
|
+
});
|
|
7356
|
+
|
|
7357
|
+
pushTool({
|
|
7358
|
+
name: 'codexmate.proxy.status',
|
|
7359
|
+
description: 'Get builtin proxy runtime status and persisted config.',
|
|
7360
|
+
readOnly: true,
|
|
7361
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
7362
|
+
handler: async () => getBuiltinProxyStatus()
|
|
7363
|
+
});
|
|
7364
|
+
|
|
7365
|
+
pushTool({
|
|
7366
|
+
name: 'codexmate.config.template.apply',
|
|
7367
|
+
description: 'Apply Codex TOML template and sync auth/model pointers.',
|
|
7368
|
+
readOnly: false,
|
|
7369
|
+
inputSchema: {
|
|
7370
|
+
type: 'object',
|
|
7371
|
+
properties: {
|
|
7372
|
+
template: { type: 'string' }
|
|
7373
|
+
},
|
|
7374
|
+
required: ['template'],
|
|
7375
|
+
additionalProperties: false
|
|
7376
|
+
},
|
|
7377
|
+
handler: async (args = {}) => applyConfigTemplate(args || {})
|
|
7378
|
+
});
|
|
7379
|
+
|
|
7380
|
+
pushTool({
|
|
7381
|
+
name: 'codexmate.provider.add',
|
|
7382
|
+
description: 'Add provider into config.toml model_providers.',
|
|
7383
|
+
readOnly: false,
|
|
7384
|
+
inputSchema: {
|
|
7385
|
+
type: 'object',
|
|
7386
|
+
properties: {
|
|
7387
|
+
name: { type: 'string' },
|
|
7388
|
+
url: { type: 'string' },
|
|
7389
|
+
key: { type: 'string' }
|
|
7390
|
+
},
|
|
7391
|
+
required: ['name', 'url'],
|
|
7392
|
+
additionalProperties: false
|
|
7393
|
+
},
|
|
7394
|
+
handler: async (args = {}) => addProviderToConfig(args || {})
|
|
7395
|
+
});
|
|
7396
|
+
|
|
7397
|
+
pushTool({
|
|
7398
|
+
name: 'codexmate.provider.update',
|
|
7399
|
+
description: 'Update provider url/key.',
|
|
7400
|
+
readOnly: false,
|
|
7401
|
+
inputSchema: {
|
|
7402
|
+
type: 'object',
|
|
7403
|
+
properties: {
|
|
7404
|
+
name: { type: 'string' },
|
|
7405
|
+
url: { type: 'string' },
|
|
7406
|
+
key: { type: 'string' }
|
|
7407
|
+
},
|
|
7408
|
+
required: ['name'],
|
|
7409
|
+
additionalProperties: false
|
|
7410
|
+
},
|
|
7411
|
+
handler: async (args = {}) => updateProviderInConfig(args || {})
|
|
7412
|
+
});
|
|
7413
|
+
|
|
7414
|
+
pushTool({
|
|
7415
|
+
name: 'codexmate.provider.delete',
|
|
7416
|
+
description: 'Delete provider from config.',
|
|
7417
|
+
readOnly: false,
|
|
7418
|
+
inputSchema: {
|
|
7419
|
+
type: 'object',
|
|
7420
|
+
properties: {
|
|
7421
|
+
name: { type: 'string' }
|
|
7422
|
+
},
|
|
7423
|
+
required: ['name'],
|
|
7424
|
+
additionalProperties: false
|
|
7425
|
+
},
|
|
7426
|
+
handler: async (args = {}) => deleteProviderFromConfig(args || {})
|
|
7427
|
+
});
|
|
7428
|
+
|
|
7429
|
+
pushTool({
|
|
7430
|
+
name: 'codexmate.claude.config.apply',
|
|
7431
|
+
description: 'Apply Claude env config into ~/.claude/settings.json.',
|
|
7432
|
+
readOnly: false,
|
|
7433
|
+
inputSchema: {
|
|
7434
|
+
type: 'object',
|
|
7435
|
+
properties: {
|
|
7436
|
+
apiKey: { type: 'string' },
|
|
7437
|
+
baseUrl: { type: 'string' },
|
|
7438
|
+
model: { type: 'string' }
|
|
7439
|
+
},
|
|
7440
|
+
required: ['apiKey'],
|
|
7441
|
+
additionalProperties: false
|
|
7442
|
+
},
|
|
7443
|
+
handler: async (args = {}) => applyToClaudeSettings(args || {})
|
|
7444
|
+
});
|
|
7445
|
+
|
|
7446
|
+
pushTool({
|
|
7447
|
+
name: 'codexmate.openclaw.config.apply',
|
|
7448
|
+
description: 'Apply OpenClaw config content into ~/.openclaw/openclaw.json.',
|
|
7449
|
+
readOnly: false,
|
|
7450
|
+
inputSchema: {
|
|
7451
|
+
type: 'object',
|
|
7452
|
+
properties: {
|
|
7453
|
+
content: { type: 'string' },
|
|
7454
|
+
lineEnding: { type: 'string' }
|
|
7455
|
+
},
|
|
7456
|
+
required: ['content'],
|
|
7457
|
+
additionalProperties: false
|
|
7458
|
+
},
|
|
7459
|
+
handler: async (args = {}) => applyOpenclawConfig(args || {})
|
|
7460
|
+
});
|
|
7461
|
+
|
|
7462
|
+
pushTool({
|
|
7463
|
+
name: 'codexmate.session.delete',
|
|
7464
|
+
description: 'Delete one session or selected records in a session.',
|
|
7465
|
+
readOnly: false,
|
|
7466
|
+
inputSchema: {
|
|
7467
|
+
type: 'object',
|
|
7468
|
+
properties: {
|
|
7469
|
+
source: { type: 'string' },
|
|
7470
|
+
sessionId: { type: 'string' },
|
|
7471
|
+
file: { type: 'string' },
|
|
7472
|
+
recordLineIndex: { type: 'number' },
|
|
7473
|
+
recordLineIndices: { type: 'array', items: { type: 'number' } }
|
|
7474
|
+
},
|
|
7475
|
+
additionalProperties: true
|
|
7476
|
+
},
|
|
7477
|
+
handler: async (args = {}) => deleteSessionData(args || {})
|
|
7478
|
+
});
|
|
7479
|
+
|
|
7480
|
+
pushTool({
|
|
7481
|
+
name: 'codexmate.auth.profile.switch',
|
|
7482
|
+
description: 'Switch active auth profile by name.',
|
|
7483
|
+
readOnly: false,
|
|
7484
|
+
inputSchema: {
|
|
7485
|
+
type: 'object',
|
|
7486
|
+
properties: {
|
|
7487
|
+
name: { type: 'string' }
|
|
7488
|
+
},
|
|
7489
|
+
required: ['name'],
|
|
7490
|
+
additionalProperties: false
|
|
7491
|
+
},
|
|
7492
|
+
handler: async (args = {}) => {
|
|
7493
|
+
const profileName = typeof args.name === 'string' ? args.name.trim() : '';
|
|
7494
|
+
if (!profileName) return { error: '认证名称不能为空' };
|
|
7495
|
+
try {
|
|
7496
|
+
return switchAuthProfile(profileName, { silent: true });
|
|
7497
|
+
} catch (e) {
|
|
7498
|
+
return { error: e.message || '切换认证失败' };
|
|
7499
|
+
}
|
|
7500
|
+
}
|
|
7501
|
+
});
|
|
7502
|
+
|
|
7503
|
+
pushTool({
|
|
7504
|
+
name: 'codexmate.auth.profile.delete',
|
|
7505
|
+
description: 'Delete an auth profile by name.',
|
|
7506
|
+
readOnly: false,
|
|
7507
|
+
inputSchema: {
|
|
7508
|
+
type: 'object',
|
|
7509
|
+
properties: {
|
|
7510
|
+
name: { type: 'string' }
|
|
7511
|
+
},
|
|
7512
|
+
required: ['name'],
|
|
7513
|
+
additionalProperties: false
|
|
7514
|
+
},
|
|
7515
|
+
handler: async (args = {}) => deleteAuthProfile(typeof args.name === 'string' ? args.name : '')
|
|
7516
|
+
});
|
|
7517
|
+
|
|
7518
|
+
pushTool({
|
|
7519
|
+
name: 'codexmate.proxy.start',
|
|
7520
|
+
description: 'Start builtin proxy runtime with optional overrides.',
|
|
7521
|
+
readOnly: false,
|
|
7522
|
+
inputSchema: {
|
|
7523
|
+
type: 'object',
|
|
7524
|
+
properties: {
|
|
7525
|
+
enabled: { type: 'boolean' },
|
|
7526
|
+
host: { type: 'string' },
|
|
7527
|
+
port: { type: 'number' },
|
|
7528
|
+
provider: { type: 'string' },
|
|
7529
|
+
authSource: { type: 'string' },
|
|
7530
|
+
timeoutMs: { type: 'number' }
|
|
7531
|
+
},
|
|
7532
|
+
additionalProperties: false
|
|
7533
|
+
},
|
|
7534
|
+
handler: async (args = {}) => startBuiltinProxyRuntime(args || {})
|
|
7535
|
+
});
|
|
7536
|
+
|
|
7537
|
+
pushTool({
|
|
7538
|
+
name: 'codexmate.proxy.stop',
|
|
7539
|
+
description: 'Stop builtin proxy runtime.',
|
|
7540
|
+
readOnly: false,
|
|
7541
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
7542
|
+
handler: async () => stopBuiltinProxyRuntime()
|
|
7543
|
+
});
|
|
7544
|
+
|
|
7545
|
+
pushTool({
|
|
7546
|
+
name: 'codexmate.proxy.provider.apply',
|
|
7547
|
+
description: 'Apply builtin proxy provider into codex config.',
|
|
7548
|
+
readOnly: false,
|
|
7549
|
+
inputSchema: {
|
|
7550
|
+
type: 'object',
|
|
7551
|
+
properties: {
|
|
7552
|
+
switchToProxy: { type: 'boolean' },
|
|
7553
|
+
provider: { type: 'string' }
|
|
7554
|
+
},
|
|
7555
|
+
additionalProperties: true
|
|
7556
|
+
},
|
|
7557
|
+
handler: async (args = {}) => applyBuiltinProxyProvider(args || {})
|
|
7558
|
+
});
|
|
7559
|
+
|
|
7560
|
+
return tools;
|
|
7561
|
+
}
|
|
7562
|
+
|
|
7563
|
+
function createMcpResources() {
|
|
7564
|
+
return [
|
|
7565
|
+
{
|
|
7566
|
+
uri: 'codexmate://status',
|
|
7567
|
+
name: 'Status',
|
|
7568
|
+
description: 'Current provider/model status snapshot.',
|
|
7569
|
+
mimeType: 'application/json',
|
|
7570
|
+
read: async () => ({
|
|
7571
|
+
contents: [{
|
|
7572
|
+
uri: 'codexmate://status',
|
|
7573
|
+
mimeType: 'application/json',
|
|
7574
|
+
text: JSON.stringify(buildMcpStatusPayload(), null, 2)
|
|
7575
|
+
}]
|
|
7576
|
+
})
|
|
7577
|
+
},
|
|
7578
|
+
{
|
|
7579
|
+
uri: 'codexmate://providers',
|
|
7580
|
+
name: 'Providers',
|
|
7581
|
+
description: 'Configured provider list (masked).',
|
|
7582
|
+
mimeType: 'application/json',
|
|
7583
|
+
read: async () => ({
|
|
7584
|
+
contents: [{
|
|
7585
|
+
uri: 'codexmate://providers',
|
|
7586
|
+
mimeType: 'application/json',
|
|
7587
|
+
text: JSON.stringify(buildMcpProviderListPayload(), null, 2)
|
|
7588
|
+
}]
|
|
7589
|
+
})
|
|
7590
|
+
},
|
|
7591
|
+
{
|
|
7592
|
+
uri: 'codexmate://sessions',
|
|
7593
|
+
name: 'Sessions',
|
|
7594
|
+
description: 'Session listing resource. Query by source/query/pathFilter via URI params.',
|
|
7595
|
+
mimeType: 'application/json',
|
|
7596
|
+
read: async (params = {}) => {
|
|
7597
|
+
const uri = typeof params.uri === 'string' ? params.uri : 'codexmate://sessions';
|
|
7598
|
+
let source = '';
|
|
7599
|
+
let query = '';
|
|
7600
|
+
let pathFilter = '';
|
|
7601
|
+
let roleFilter = '';
|
|
7602
|
+
let timeRangePreset = '';
|
|
7603
|
+
try {
|
|
7604
|
+
const parsed = new URL(uri);
|
|
7605
|
+
source = parsed.searchParams.get('source') || '';
|
|
7606
|
+
query = parsed.searchParams.get('query') || '';
|
|
7607
|
+
pathFilter = parsed.searchParams.get('pathFilter') || '';
|
|
7608
|
+
roleFilter = parsed.searchParams.get('roleFilter') || '';
|
|
7609
|
+
timeRangePreset = parsed.searchParams.get('timeRangePreset') || '';
|
|
7610
|
+
} catch (_) {}
|
|
7611
|
+
const normalizedSource = normalizeMcpSource(source);
|
|
7612
|
+
if (normalizedSource === null) {
|
|
7613
|
+
return {
|
|
7614
|
+
contents: [{
|
|
7615
|
+
uri,
|
|
7616
|
+
mimeType: 'application/json',
|
|
7617
|
+
text: JSON.stringify({ error: 'Invalid source. Must be codex, claude, or all' }, null, 2)
|
|
7618
|
+
}]
|
|
7619
|
+
};
|
|
7620
|
+
}
|
|
7621
|
+
const payload = {
|
|
7622
|
+
source: normalizedSource || 'all',
|
|
7623
|
+
sessions: listAllSessions({
|
|
7624
|
+
source: normalizedSource || 'all',
|
|
7625
|
+
query,
|
|
7626
|
+
pathFilter,
|
|
7627
|
+
roleFilter,
|
|
7628
|
+
timeRangePreset
|
|
7629
|
+
})
|
|
7630
|
+
};
|
|
7631
|
+
return {
|
|
7632
|
+
contents: [{
|
|
7633
|
+
uri,
|
|
7634
|
+
mimeType: 'application/json',
|
|
7635
|
+
text: JSON.stringify(payload, null, 2)
|
|
7636
|
+
}]
|
|
7637
|
+
};
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
];
|
|
7641
|
+
}
|
|
7642
|
+
|
|
7643
|
+
function createMcpPrompts() {
|
|
7644
|
+
return [
|
|
7645
|
+
{
|
|
7646
|
+
name: 'codexmate.diagnose_config',
|
|
7647
|
+
description: 'Generate troubleshooting guidance from current codexmate status/providers.',
|
|
7648
|
+
arguments: [],
|
|
7649
|
+
get: async () => {
|
|
7650
|
+
const status = buildMcpStatusPayload();
|
|
7651
|
+
const providers = buildMcpProviderListPayload();
|
|
7652
|
+
return {
|
|
7653
|
+
messages: [{
|
|
7654
|
+
role: 'user',
|
|
7655
|
+
content: {
|
|
7656
|
+
type: 'text',
|
|
7657
|
+
text: [
|
|
7658
|
+
'请根据以下配置快照进行故障诊断,并给出按优先级排序的修复步骤。',
|
|
7659
|
+
'要求:先给结论,再给操作清单,最后给风险与回滚建议。',
|
|
7660
|
+
'',
|
|
7661
|
+
'[status]',
|
|
7662
|
+
JSON.stringify(status, null, 2),
|
|
7663
|
+
'',
|
|
7664
|
+
'[providers]',
|
|
7665
|
+
JSON.stringify(providers, null, 2)
|
|
7666
|
+
].join('\n')
|
|
7667
|
+
}
|
|
7668
|
+
}]
|
|
7669
|
+
};
|
|
7670
|
+
}
|
|
7671
|
+
},
|
|
7672
|
+
{
|
|
7673
|
+
name: 'codexmate.switch_provider_safely',
|
|
7674
|
+
description: 'Guide safe provider switch with pre-check and rollback plan.',
|
|
7675
|
+
arguments: [{
|
|
7676
|
+
name: 'provider',
|
|
7677
|
+
description: 'Target provider name',
|
|
7678
|
+
required: true
|
|
7679
|
+
}],
|
|
7680
|
+
get: async (args = {}) => {
|
|
7681
|
+
const provider = typeof args.provider === 'string' ? args.provider.trim() : '';
|
|
7682
|
+
return {
|
|
7683
|
+
messages: [{
|
|
7684
|
+
role: 'user',
|
|
7685
|
+
content: {
|
|
7686
|
+
type: 'text',
|
|
7687
|
+
text: [
|
|
7688
|
+
`请为 provider "${provider || '(missing)'}" 生成安全切换步骤。`,
|
|
7689
|
+
'要求:',
|
|
7690
|
+
'1) 先检查 provider 是否存在与 key 是否可用',
|
|
7691
|
+
'2) 给出切换后验证项(模型拉取/健康检查)',
|
|
7692
|
+
'3) 给出失败时回滚流程(回到旧 provider/model)'
|
|
7693
|
+
].join('\n')
|
|
7694
|
+
}
|
|
7695
|
+
}]
|
|
7696
|
+
};
|
|
7697
|
+
}
|
|
7698
|
+
},
|
|
7699
|
+
{
|
|
7700
|
+
name: 'codexmate.export_session_for_issue',
|
|
7701
|
+
description: 'Prepare issue report template from a selected session export.',
|
|
7702
|
+
arguments: [{
|
|
7703
|
+
name: 'source',
|
|
7704
|
+
description: 'Session source: codex or claude',
|
|
7705
|
+
required: true
|
|
7706
|
+
}, {
|
|
7707
|
+
name: 'sessionId',
|
|
7708
|
+
description: 'Session id',
|
|
7709
|
+
required: true
|
|
7710
|
+
}],
|
|
7711
|
+
get: async (args = {}) => {
|
|
7712
|
+
const source = typeof args.source === 'string' ? args.source.trim() : '';
|
|
7713
|
+
const sessionId = typeof args.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
7714
|
+
return {
|
|
7715
|
+
messages: [{
|
|
7716
|
+
role: 'user',
|
|
7717
|
+
content: {
|
|
7718
|
+
type: 'text',
|
|
7719
|
+
text: [
|
|
7720
|
+
'请根据会话导出内容生成 issue 报告草稿。',
|
|
7721
|
+
`source: ${source || '(missing)'}`,
|
|
7722
|
+
`sessionId: ${sessionId || '(missing)'}`,
|
|
7723
|
+
'',
|
|
7724
|
+
'报告需包含:问题现象、复现步骤、预期行为、实际行为、可疑配置项。'
|
|
7725
|
+
].join('\n')
|
|
7726
|
+
}
|
|
7727
|
+
}]
|
|
7728
|
+
};
|
|
7729
|
+
}
|
|
7730
|
+
}
|
|
7731
|
+
];
|
|
7732
|
+
}
|
|
7733
|
+
|
|
7734
|
+
async function cmdMcp(args = []) {
|
|
7735
|
+
const options = parseMcpOptions(args);
|
|
7736
|
+
if (options.help) {
|
|
7737
|
+
console.log('\n用法: codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|
|
7738
|
+
console.log(' 默认 transport=stdio,默认 read-only。');
|
|
7739
|
+
console.log(' 设置环境变量 CODEXMATE_MCP_ALLOW_WRITE=1 可默认开启写工具。');
|
|
7740
|
+
console.log();
|
|
7741
|
+
return;
|
|
7742
|
+
}
|
|
7743
|
+
|
|
7744
|
+
if (options.subcommand !== 'serve') {
|
|
7745
|
+
throw new Error(`未知 mcp 子命令: ${options.subcommand}`);
|
|
7746
|
+
}
|
|
7747
|
+
if (options.transport !== 'stdio') {
|
|
7748
|
+
throw new Error(`当前仅支持 stdio 传输,收到: ${options.transport}`);
|
|
7749
|
+
}
|
|
7750
|
+
|
|
7751
|
+
const packageVersion = (() => {
|
|
7752
|
+
try {
|
|
7753
|
+
const pkg = require('./package.json');
|
|
7754
|
+
return pkg && pkg.version ? pkg.version : '0.0.0';
|
|
7755
|
+
} catch (_) {
|
|
7756
|
+
return '0.0.0';
|
|
7757
|
+
}
|
|
7758
|
+
})();
|
|
7759
|
+
|
|
7760
|
+
const server = createMcpStdioServer({
|
|
7761
|
+
protocolVersion: '2025-11-25',
|
|
7762
|
+
serverInfo: {
|
|
7763
|
+
name: 'codexmate-mcp',
|
|
7764
|
+
version: packageVersion
|
|
7765
|
+
},
|
|
7766
|
+
tools: createMcpTools({ allowWrite: options.allowWrite }),
|
|
7767
|
+
resources: createMcpResources(),
|
|
7768
|
+
prompts: createMcpPrompts(),
|
|
7769
|
+
logger: (level, message) => {
|
|
7770
|
+
const label = level === 'error' ? 'ERR' : 'INFO';
|
|
7771
|
+
console.error(`[MCP ${label}] ${message}`);
|
|
7772
|
+
}
|
|
7773
|
+
});
|
|
7774
|
+
|
|
7775
|
+
server.start();
|
|
7776
|
+
|
|
7777
|
+
await new Promise((resolve) => {
|
|
7778
|
+
let done = false;
|
|
7779
|
+
const finish = () => {
|
|
7780
|
+
if (done) return;
|
|
7781
|
+
done = true;
|
|
7782
|
+
server.stop();
|
|
7783
|
+
stopBuiltinProxyRuntime().finally(() => resolve());
|
|
7784
|
+
};
|
|
7785
|
+
process.once('SIGINT', finish);
|
|
7786
|
+
process.once('SIGTERM', finish);
|
|
7787
|
+
process.stdin.once('end', finish);
|
|
7788
|
+
process.stdin.once('close', finish);
|
|
7789
|
+
});
|
|
7790
|
+
}
|
|
7791
|
+
|
|
4885
7792
|
// ============================================================================
|
|
4886
7793
|
// 主程序
|
|
4887
7794
|
// ============================================================================
|
|
4888
7795
|
async function main() {
|
|
7796
|
+
const args = process.argv.slice(2);
|
|
7797
|
+
const command = args[0];
|
|
7798
|
+
const isMcpCommand = command === 'mcp';
|
|
4889
7799
|
const bootstrap = ensureManagedConfigBootstrap();
|
|
4890
7800
|
if (bootstrap && bootstrap.notice) {
|
|
4891
|
-
|
|
7801
|
+
// MCP stdio transport requires stdout to be protocol-clean.
|
|
7802
|
+
if (!isMcpCommand) {
|
|
7803
|
+
console.log(`\n[Init] ${bootstrap.notice}`);
|
|
7804
|
+
}
|
|
4892
7805
|
}
|
|
4893
7806
|
|
|
4894
|
-
const args = process.argv.slice(2);
|
|
4895
7807
|
if (args.length === 0) {
|
|
4896
7808
|
console.log('\nCodex Mate - Codex 提供商管理工具');
|
|
4897
7809
|
console.log('\n用法:');
|
|
@@ -4906,17 +7818,20 @@ async function main() {
|
|
|
4906
7818
|
console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
|
|
4907
7819
|
console.log(' codexmate add-model <模型> 添加模型');
|
|
4908
7820
|
console.log(' codexmate delete-model <模型> 删除模型');
|
|
7821
|
+
console.log(' codexmate auth <list|import|switch|delete|status> 认证文件管理');
|
|
7822
|
+
console.log(' codexmate proxy <status|set|apply|enable|start|stop> 内建代理');
|
|
4909
7823
|
console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
|
|
4910
7824
|
console.log(' codexmate codex [参数...] 等同于 codex --yolo');
|
|
7825
|
+
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
7826
|
+
console.log(' codexmate gemini [参数...] 等同于 gemini --yolo');
|
|
7827
|
+
console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|
|
4911
7828
|
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
4912
|
-
console.log(' codexmate zip <路径> [--max:级别]
|
|
4913
|
-
console.log(' codexmate unzip <zip文件> [输出目录] 解压(
|
|
7829
|
+
console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
|
|
7830
|
+
console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
|
|
4914
7831
|
console.log('');
|
|
4915
7832
|
process.exit(0);
|
|
4916
7833
|
}
|
|
4917
7834
|
|
|
4918
|
-
const command = args[0];
|
|
4919
|
-
|
|
4920
7835
|
switch (command) {
|
|
4921
7836
|
case 'status': cmdStatus(); break;
|
|
4922
7837
|
case 'setup': await cmdSetup(); break;
|
|
@@ -4929,6 +7844,8 @@ async function main() {
|
|
|
4929
7844
|
case 'claude': cmdClaude(args[1], args[2], args[3]); break;
|
|
4930
7845
|
case 'add-model': cmdAddModel(args[1]); break;
|
|
4931
7846
|
case 'delete-model': cmdDeleteModel(args[1]); break;
|
|
7847
|
+
case 'auth': cmdAuth(args.slice(1)); break;
|
|
7848
|
+
case 'proxy': await cmdProxy(args.slice(1)); break;
|
|
4932
7849
|
case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
|
|
4933
7850
|
case 'start':
|
|
4934
7851
|
console.error('错误: 命令已更名为 "run",请使用: codexmate run');
|
|
@@ -4939,6 +7856,17 @@ async function main() {
|
|
|
4939
7856
|
process.exit(exitCode);
|
|
4940
7857
|
break;
|
|
4941
7858
|
}
|
|
7859
|
+
case 'qwen': {
|
|
7860
|
+
const exitCode = await cmdQwen(args.slice(1));
|
|
7861
|
+
process.exit(exitCode);
|
|
7862
|
+
break;
|
|
7863
|
+
}
|
|
7864
|
+
case 'gemini': {
|
|
7865
|
+
const exitCode = await cmdGemini(args.slice(1));
|
|
7866
|
+
process.exit(exitCode);
|
|
7867
|
+
break;
|
|
7868
|
+
}
|
|
7869
|
+
case 'mcp': await cmdMcp(args.slice(1)); break;
|
|
4942
7870
|
case 'export-session': await cmdExportSession(args.slice(1)); break;
|
|
4943
7871
|
case 'zip': {
|
|
4944
7872
|
// 解析 --max:N 参数
|