codexmate 0.0.12 → 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 +24 -2
- package/README.zh-CN.md +24 -2
- package/{src/cli.js → cli.js} +2689 -256
- package/doc/CHANGELOG.md +6 -0
- package/doc/CHANGELOG.zh-CN.md +6 -0
- package/lib/mcp-stdio.js +440 -0
- package/package.json +56 -53
- package/web-ui/app.js +903 -10
- package/web-ui/index.html +350 -55
- package/web-ui/styles.css +394 -49
- package/src/lib/cli-file-utils.js +0 -151
- package/src/lib/cli-models-utils.js +0 -152
- package/src/lib/cli-network-utils.js +0 -148
- package/src/lib/cli-session-utils.js +0 -121
- package/src/lib/cli-utils.js +0 -139
- package/src/res/json5.min.js +0 -1
- package/src/res/logo.png +0 -0
- package/src/res/screenshot.png +0 -0
- package/src/res/vue.global.js +0 -18552
- package/src/web-ui/app.js +0 -2970
- package/src/web-ui/index.html +0 -1310
- package/src/web-ui/logic.mjs +0 -157
- package/src/web-ui/styles.css +0 -2868
- /package/{src/web-ui.html → web-ui.html} +0 -0
package/{src/cli.js → cli.js}
RENAMED
|
@@ -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;
|
|
@@ -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;
|
|
@@ -1295,9 +1652,16 @@ function addProviderToConfig(params = {}) {
|
|
|
1295
1652
|
const name = typeof params.name === 'string' ? params.name.trim() : '';
|
|
1296
1653
|
const url = typeof params.url === 'string' ? params.url.trim() : '';
|
|
1297
1654
|
const key = typeof params.key === 'string' ? params.key.trim() : '';
|
|
1655
|
+
const allowManaged = !!params.allowManaged;
|
|
1298
1656
|
|
|
1299
1657
|
if (!name) return { error: '名称不能为空' };
|
|
1300
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
|
+
}
|
|
1301
1665
|
|
|
1302
1666
|
ensureConfigDir();
|
|
1303
1667
|
|
|
@@ -1368,14 +1732,21 @@ function updateProviderInConfig(params = {}) {
|
|
|
1368
1732
|
const key = params.key !== undefined && params.key !== null
|
|
1369
1733
|
? String(params.key).trim()
|
|
1370
1734
|
: undefined;
|
|
1735
|
+
const allowManaged = !!params.allowManaged;
|
|
1371
1736
|
|
|
1372
1737
|
if (!name) return { error: '名称不能为空' };
|
|
1373
1738
|
if (!url && key === undefined) {
|
|
1374
1739
|
return { error: 'URL 或密钥至少填写一项' };
|
|
1375
1740
|
}
|
|
1741
|
+
if (isNonEditableProvider(name) && !allowManaged) {
|
|
1742
|
+
if (isDefaultLocalProvider(name)) {
|
|
1743
|
+
return { error: 'local provider 为系统保留项,不可编辑' };
|
|
1744
|
+
}
|
|
1745
|
+
return { error: '本地代理配置为系统内建项,不可编辑' };
|
|
1746
|
+
}
|
|
1376
1747
|
|
|
1377
1748
|
try {
|
|
1378
|
-
cmdUpdate(name, url || undefined, key, true);
|
|
1749
|
+
cmdUpdate(name, url || undefined, key, true, { allowManaged });
|
|
1379
1750
|
return { success: true };
|
|
1380
1751
|
} catch (e) {
|
|
1381
1752
|
return { error: e.message || '更新失败' };
|
|
@@ -1385,6 +1756,12 @@ function updateProviderInConfig(params = {}) {
|
|
|
1385
1756
|
function deleteProviderFromConfig(params = {}) {
|
|
1386
1757
|
const name = typeof params.name === 'string' ? params.name.trim() : '';
|
|
1387
1758
|
if (!name) return { error: '名称不能为空' };
|
|
1759
|
+
if (isNonDeletableProvider(name)) {
|
|
1760
|
+
if (isDefaultLocalProvider(name)) {
|
|
1761
|
+
return { error: 'local provider 为系统保留项,不可删除' };
|
|
1762
|
+
}
|
|
1763
|
+
return { error: '本地代理配置为系统内建项,不可删除' };
|
|
1764
|
+
}
|
|
1388
1765
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
1389
1766
|
return { error: 'config.toml 不存在' };
|
|
1390
1767
|
}
|
|
@@ -1410,6 +1787,13 @@ function deleteProviderFromConfig(params = {}) {
|
|
|
1410
1787
|
|
|
1411
1788
|
function performProviderDeletion(name, options = {}) {
|
|
1412
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
|
+
}
|
|
1413
1797
|
const config = options.config || readConfig();
|
|
1414
1798
|
if (!config.model_providers || !config.model_providers[name]) {
|
|
1415
1799
|
const msg = '提供商不存在';
|
|
@@ -2789,143 +3173,661 @@ function findClaudeSessionIndexPath(sessionFilePath) {
|
|
|
2789
3173
|
return '';
|
|
2790
3174
|
}
|
|
2791
3175
|
|
|
2792
|
-
function
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
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;
|
|
2799
3194
|
}
|
|
2800
|
-
const
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
if (
|
|
2804
|
-
|
|
2805
|
-
}
|
|
2806
|
-
const entrySessionId = typeof entry.sessionId === 'string' ? entry.sessionId : '';
|
|
2807
|
-
if (sessionId && entrySessionId === sessionId) {
|
|
2808
|
-
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;
|
|
2809
3200
|
}
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
return false;
|
|
2815
|
-
}
|
|
3201
|
+
// eslint-disable-next-line no-await-in-loop
|
|
3202
|
+
const ok = await canListenPort(host, candidate);
|
|
3203
|
+
if (ok) {
|
|
3204
|
+
return candidate;
|
|
2816
3205
|
}
|
|
2817
|
-
return true;
|
|
2818
|
-
});
|
|
2819
|
-
if (filtered.length === index.entries.length) {
|
|
2820
|
-
return;
|
|
2821
3206
|
}
|
|
2822
|
-
|
|
2823
|
-
try {
|
|
2824
|
-
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
|
|
2825
|
-
} catch (e) {}
|
|
3207
|
+
return 0;
|
|
2826
3208
|
}
|
|
2827
3209
|
|
|
2828
|
-
|
|
2829
|
-
const
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
}
|
|
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';
|
|
2833
3221
|
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
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
|
+
}
|
|
2838
3231
|
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
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;
|
|
2844
3244
|
}
|
|
3245
|
+
const preferred = typeof preferredProvider === 'string' ? preferredProvider.trim() : '';
|
|
3246
|
+
if (preferred && preferred !== BUILTIN_PROXY_PROVIDER_NAME && providerMap[preferred]) {
|
|
3247
|
+
return preferred;
|
|
3248
|
+
}
|
|
3249
|
+
return providerNames[0] || '';
|
|
3250
|
+
}
|
|
2845
3251
|
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
}
|
|
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)' };
|
|
2851
3264
|
}
|
|
2852
3265
|
|
|
2853
|
-
|
|
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
|
+
}
|
|
2854
3279
|
|
|
2855
3280
|
return {
|
|
2856
3281
|
success: true,
|
|
2857
|
-
|
|
2858
|
-
sessionId,
|
|
2859
|
-
filePath
|
|
3282
|
+
settings: normalized
|
|
2860
3283
|
};
|
|
2861
3284
|
}
|
|
2862
3285
|
|
|
2863
|
-
function
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
}
|
|
2867
|
-
const timePart = Date.now().toString(36);
|
|
2868
|
-
const randomPart = crypto.randomBytes(8).toString('hex');
|
|
2869
|
-
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}`;
|
|
2870
3289
|
}
|
|
2871
3290
|
|
|
2872
|
-
function
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
if (!fs.existsSync(filePath)) {
|
|
2877
|
-
return { sessionId, filePath };
|
|
2878
|
-
}
|
|
3291
|
+
function hasCodexConfigReadyForProxy() {
|
|
3292
|
+
const result = readConfigOrVirtualDefault();
|
|
3293
|
+
if (!result || result.isVirtual) {
|
|
3294
|
+
return false;
|
|
2879
3295
|
}
|
|
2880
|
-
const
|
|
2881
|
-
|
|
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;
|
|
2882
3303
|
}
|
|
2883
3304
|
|
|
2884
|
-
function
|
|
2885
|
-
|
|
2886
|
-
|
|
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' };
|
|
2887
3312
|
}
|
|
2888
|
-
if (
|
|
2889
|
-
|
|
2890
|
-
if (value > 1e9) return value * 1000;
|
|
2891
|
-
return value;
|
|
3313
|
+
if (providerName === BUILTIN_PROXY_PROVIDER_NAME) {
|
|
3314
|
+
return { error: `上游 provider 不能是 ${BUILTIN_PROXY_PROVIDER_NAME}` };
|
|
2892
3315
|
}
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
return parsed;
|
|
2897
|
-
}
|
|
2898
|
-
const numeric = Number(value);
|
|
2899
|
-
if (Number.isFinite(numeric)) {
|
|
2900
|
-
if (numeric > 1e12) return numeric;
|
|
2901
|
-
if (numeric > 1e9) return numeric * 1000;
|
|
2902
|
-
return numeric;
|
|
2903
|
-
}
|
|
3316
|
+
const provider = providers[providerName];
|
|
3317
|
+
if (!provider || !isPlainObject(provider)) {
|
|
3318
|
+
return { error: `上游 provider 不存在: ${providerName}` };
|
|
2904
3319
|
}
|
|
2905
|
-
return null;
|
|
2906
|
-
}
|
|
2907
3320
|
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
return { error: '仅支持 Codex 会话克隆' };
|
|
3321
|
+
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
3322
|
+
if (!baseUrl || !isValidHttpUrl(baseUrl)) {
|
|
3323
|
+
return { error: `上游 provider base_url 无效: ${providerName}` };
|
|
2912
3324
|
}
|
|
2913
3325
|
|
|
2914
|
-
|
|
2915
|
-
if (
|
|
2916
|
-
|
|
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
|
+
}
|
|
2917
3334
|
}
|
|
2918
3335
|
|
|
2919
|
-
let
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
} catch (e) {
|
|
2923
|
-
return { error: `读取会话失败: ${e.message}` };
|
|
3336
|
+
let authHeader = '';
|
|
3337
|
+
if (token) {
|
|
3338
|
+
authHeader = /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
|
|
2924
3339
|
}
|
|
2925
3340
|
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
3341
|
+
return {
|
|
3342
|
+
providerName,
|
|
3343
|
+
baseUrl: normalizeBaseUrl(baseUrl),
|
|
3344
|
+
authHeader
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
|
|
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
|
+
}
|
|
2929
3831
|
|
|
2930
3832
|
const lineEnding = detectLineEnding(content);
|
|
2931
3833
|
const rawLines = content.split(/\r?\n/);
|
|
@@ -3440,6 +4342,9 @@ function buildExportPayload(includeKeys) {
|
|
|
3440
4342
|
const providers = config.model_providers || {};
|
|
3441
4343
|
const providerData = {};
|
|
3442
4344
|
for (const [name, provider] of Object.entries(providers)) {
|
|
4345
|
+
if (isBuiltinProxyProvider(name)) {
|
|
4346
|
+
continue;
|
|
4347
|
+
}
|
|
3443
4348
|
providerData[name] = {
|
|
3444
4349
|
baseUrl: provider.base_url || '',
|
|
3445
4350
|
apiKey: includeKeys ? (provider.preferred_auth_method || '') : null
|
|
@@ -3561,6 +4466,9 @@ function importConfigData(payload, options = {}) {
|
|
|
3561
4466
|
let updatedProviders = 0;
|
|
3562
4467
|
|
|
3563
4468
|
for (const [name, provider] of Object.entries(normalized.providers)) {
|
|
4469
|
+
if (isBuiltinProxyProvider(name)) {
|
|
4470
|
+
continue;
|
|
4471
|
+
}
|
|
3564
4472
|
if (existingProviders[name]) {
|
|
3565
4473
|
if (overwriteProviders) {
|
|
3566
4474
|
const apiKey = typeof provider.apiKey === 'string' && provider.apiKey
|
|
@@ -3592,6 +4500,7 @@ function importConfigData(payload, options = {}) {
|
|
|
3592
4500
|
if (applyCurrentModels && normalized.currentModels) {
|
|
3593
4501
|
const currentModels = readCurrentModels();
|
|
3594
4502
|
for (const [name, model] of Object.entries(normalized.currentModels)) {
|
|
4503
|
+
if (isBuiltinProxyProvider(name)) continue;
|
|
3595
4504
|
if (typeof model !== 'string' || !model) continue;
|
|
3596
4505
|
currentModels[name] = model;
|
|
3597
4506
|
}
|
|
@@ -4108,7 +5017,10 @@ function cmdUseModel(modelName, silent = false) {
|
|
|
4108
5017
|
|
|
4109
5018
|
// 添加提供商
|
|
4110
5019
|
function cmdAdd(name, baseUrl, apiKey, silent = false) {
|
|
4111
|
-
|
|
5020
|
+
const providerName = typeof name === 'string' ? name.trim() : '';
|
|
5021
|
+
const providerBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
|
5022
|
+
|
|
5023
|
+
if (!providerName || !providerBaseUrl) {
|
|
4112
5024
|
if (!silent) {
|
|
4113
5025
|
console.error('用法: codexmate add <名称> <URL> [密钥]');
|
|
4114
5026
|
console.log('\n示例:');
|
|
@@ -4116,17 +5028,21 @@ function cmdAdd(name, baseUrl, apiKey, silent = false) {
|
|
|
4116
5028
|
}
|
|
4117
5029
|
throw new Error('名称和URL必填');
|
|
4118
5030
|
}
|
|
5031
|
+
if (isReservedProviderNameForCreation(providerName)) {
|
|
5032
|
+
if (!silent) console.error('错误: local provider 为系统保留名称,不可新增');
|
|
5033
|
+
throw new Error('local provider 为系统保留名称,不可新增');
|
|
5034
|
+
}
|
|
4119
5035
|
|
|
4120
5036
|
const config = readConfig();
|
|
4121
|
-
if (config.model_providers && config.model_providers[
|
|
4122
|
-
if (!silent) console.error('错误: 提供商已存在:',
|
|
5037
|
+
if (config.model_providers && config.model_providers[providerName]) {
|
|
5038
|
+
if (!silent) console.error('错误: 提供商已存在:', providerName);
|
|
4123
5039
|
throw new Error('提供商已存在');
|
|
4124
5040
|
}
|
|
4125
5041
|
|
|
4126
5042
|
const newBlock = `
|
|
4127
|
-
[model_providers.${
|
|
4128
|
-
name = "${
|
|
4129
|
-
base_url = "${
|
|
5043
|
+
[model_providers.${providerName}]
|
|
5044
|
+
name = "${providerName}"
|
|
5045
|
+
base_url = "${providerBaseUrl}"
|
|
4130
5046
|
wire_api = "responses"
|
|
4131
5047
|
requires_openai_auth = false
|
|
4132
5048
|
preferred_auth_method = "${apiKey || ''}"
|
|
@@ -4140,14 +5056,14 @@ stream_idle_timeout_ms = 300000
|
|
|
4140
5056
|
|
|
4141
5057
|
// 初始化当前模型
|
|
4142
5058
|
const currentModels = readCurrentModels();
|
|
4143
|
-
if (!currentModels[
|
|
4144
|
-
currentModels[
|
|
5059
|
+
if (!currentModels[providerName]) {
|
|
5060
|
+
currentModels[providerName] = readModels()[0];
|
|
4145
5061
|
writeCurrentModels(currentModels);
|
|
4146
5062
|
}
|
|
4147
5063
|
|
|
4148
5064
|
if (!silent) {
|
|
4149
|
-
console.log('✓ 已添加提供商:',
|
|
4150
|
-
console.log(' URL:',
|
|
5065
|
+
console.log('✓ 已添加提供商:', providerName);
|
|
5066
|
+
console.log(' URL:', providerBaseUrl);
|
|
4151
5067
|
console.log();
|
|
4152
5068
|
}
|
|
4153
5069
|
}
|
|
@@ -4168,11 +5084,19 @@ function cmdDelete(name, silent = false) {
|
|
|
4168
5084
|
}
|
|
4169
5085
|
|
|
4170
5086
|
// 更新提供商
|
|
4171
|
-
function cmdUpdate(name, baseUrl, apiKey, silent = false) {
|
|
5087
|
+
function cmdUpdate(name, baseUrl, apiKey, silent = false, options = {}) {
|
|
5088
|
+
const allowManaged = !!(options && options.allowManaged);
|
|
4172
5089
|
if (!name) {
|
|
4173
5090
|
if (!silent) console.error('错误: 提供商名称必填');
|
|
4174
5091
|
throw new Error('提供商名称必填');
|
|
4175
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
|
+
}
|
|
4176
5100
|
|
|
4177
5101
|
const config = readConfig();
|
|
4178
5102
|
if (!config.model_providers || !config.model_providers[name]) {
|
|
@@ -4357,29 +5281,248 @@ function applyToClaudeSettings(config = {}) {
|
|
|
4357
5281
|
}
|
|
4358
5282
|
}
|
|
4359
5283
|
|
|
4360
|
-
function readClaudeSettingsInfo() {
|
|
4361
|
-
const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
|
|
4362
|
-
if (!readResult.ok) {
|
|
5284
|
+
function readClaudeSettingsInfo() {
|
|
5285
|
+
const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
|
|
5286
|
+
if (!readResult.ok) {
|
|
5287
|
+
return {
|
|
5288
|
+
error: readResult.error || '读取 Claude 配置失败',
|
|
5289
|
+
exists: !!readResult.exists,
|
|
5290
|
+
targetPath: CLAUDE_SETTINGS_FILE
|
|
5291
|
+
};
|
|
5292
|
+
}
|
|
5293
|
+
|
|
5294
|
+
const settings = readResult.data || {};
|
|
5295
|
+
const env = (settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env))
|
|
5296
|
+
? settings.env
|
|
5297
|
+
: {};
|
|
5298
|
+
|
|
5299
|
+
return {
|
|
5300
|
+
exists: !!readResult.exists,
|
|
5301
|
+
targetPath: CLAUDE_SETTINGS_FILE,
|
|
5302
|
+
apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
|
|
5303
|
+
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
|
|
5304
|
+
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
|
|
5305
|
+
env
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
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
|
+
|
|
4363
5494
|
return {
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
5495
|
+
success: true,
|
|
5496
|
+
targetDir,
|
|
5497
|
+
appliedFrom: payload.fileName || '',
|
|
5498
|
+
backupPath,
|
|
5499
|
+
backupWarning: backupResult.warning || ''
|
|
4367
5500
|
};
|
|
5501
|
+
} catch (e) {
|
|
5502
|
+
return { error: `导入失败:${e.message}` };
|
|
5503
|
+
} finally {
|
|
5504
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
4368
5505
|
}
|
|
5506
|
+
}
|
|
4369
5507
|
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
:
|
|
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
|
+
}
|
|
4374
5517
|
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
};
|
|
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
|
+
});
|
|
4383
5526
|
}
|
|
4384
5527
|
|
|
4385
5528
|
// CLI: 一行写入 Claude Code 配置
|
|
@@ -4436,18 +5579,196 @@ function commandExists(command, args = '') {
|
|
|
4436
5579
|
}
|
|
4437
5580
|
}
|
|
4438
5581
|
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
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;
|
|
5704
|
+
}
|
|
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;
|
|
5712
|
+
}
|
|
5713
|
+
commandsByTarget[target.id] = {
|
|
5714
|
+
install: `npm install -g ${pkg}`,
|
|
5715
|
+
update: `npm update -g ${pkg}`,
|
|
5716
|
+
uninstall: `npm uninstall -g ${pkg}`
|
|
5717
|
+
};
|
|
5718
|
+
}
|
|
5719
|
+
|
|
5720
|
+
return {
|
|
5721
|
+
packageManager: manager,
|
|
5722
|
+
commandsByTarget
|
|
5723
|
+
};
|
|
5724
|
+
}
|
|
5725
|
+
|
|
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
|
+
};
|
|
5760
|
+
}
|
|
5761
|
+
|
|
5762
|
+
const ZIP_PATHS = [
|
|
5763
|
+
'zip'
|
|
4443
5764
|
];
|
|
4444
5765
|
|
|
4445
|
-
function
|
|
4446
|
-
for (const candidate of
|
|
5766
|
+
function findZipExecutable() {
|
|
5767
|
+
for (const candidate of ZIP_PATHS) {
|
|
4447
5768
|
try {
|
|
4448
|
-
if (candidate === '
|
|
4449
|
-
if (commandExists('
|
|
4450
|
-
return '
|
|
5769
|
+
if (candidate === 'zip') {
|
|
5770
|
+
if (commandExists('zip', '--help')) {
|
|
5771
|
+
return 'zip';
|
|
4451
5772
|
}
|
|
4452
5773
|
} else if (fs.existsSync(candidate)) {
|
|
4453
5774
|
return candidate;
|
|
@@ -4458,18 +5779,14 @@ function findSevenZipExecutable() {
|
|
|
4458
5779
|
}
|
|
4459
5780
|
|
|
4460
5781
|
function resolveZipTool() {
|
|
4461
|
-
const
|
|
4462
|
-
if (
|
|
4463
|
-
return { type: '
|
|
5782
|
+
const zipExe = findZipExecutable();
|
|
5783
|
+
if (zipExe) {
|
|
5784
|
+
return { type: 'zip', cmd: zipExe };
|
|
4464
5785
|
}
|
|
4465
5786
|
return { type: 'lib', cmd: 'zip-lib' };
|
|
4466
5787
|
}
|
|
4467
5788
|
|
|
4468
5789
|
function resolveUnzipTool() {
|
|
4469
|
-
const sevenZipExe = findSevenZipExecutable();
|
|
4470
|
-
if (sevenZipExe) {
|
|
4471
|
-
return { type: '7z', cmd: sevenZipExe };
|
|
4472
|
-
}
|
|
4473
5790
|
return { type: 'lib', cmd: 'zip-lib' };
|
|
4474
5791
|
}
|
|
4475
5792
|
|
|
@@ -4486,7 +5803,7 @@ async function unzipWithLibrary(zipPath, outputDir) {
|
|
|
4486
5803
|
await zipLib.extract(zipPath, outputDir);
|
|
4487
5804
|
}
|
|
4488
5805
|
|
|
4489
|
-
//
|
|
5806
|
+
// 压缩(系统 zip 优先,其次 zip-lib)
|
|
4490
5807
|
async function cmdZip(targetPath, options = {}) {
|
|
4491
5808
|
if (!targetPath) {
|
|
4492
5809
|
console.error('用法: codexmate zip <文件或文件夹路径> [--max:压缩级别]');
|
|
@@ -4516,40 +5833,27 @@ async function cmdZip(targetPath, options = {}) {
|
|
|
4516
5833
|
const outputPath = path.join(outputDir, `${baseName}.zip`);
|
|
4517
5834
|
|
|
4518
5835
|
const zipTool = resolveZipTool();
|
|
5836
|
+
const useZipCmd = zipTool.type === 'zip';
|
|
4519
5837
|
|
|
4520
5838
|
console.log('\n压缩配置:');
|
|
4521
5839
|
console.log(' 源路径:', absPath);
|
|
4522
5840
|
console.log(' 输出文件:', outputPath);
|
|
4523
|
-
console.log('
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
console.log('
|
|
5841
|
+
console.log(' 压缩工具:', useZipCmd ? '系统 zip' : 'zip-lib');
|
|
5842
|
+
if (useZipCmd) {
|
|
5843
|
+
console.log(' 压缩级别:', compressionLevel);
|
|
5844
|
+
} else {
|
|
5845
|
+
console.log(' 压缩级别: 固定(zip-lib 不支持 --max,已忽略)');
|
|
4528
5846
|
}
|
|
4529
5847
|
console.log('\n开始压缩...\n');
|
|
4530
5848
|
|
|
4531
5849
|
try {
|
|
4532
|
-
if (
|
|
4533
|
-
const cmd = `"${zipTool.cmd}"
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
console.log('✓ 压缩完成!');
|
|
4539
|
-
console.log(' 输出文件:', outputPath);
|
|
4540
|
-
if (sizeMatch) {
|
|
4541
|
-
const sizeBytes = parseInt(sizeMatch[1]);
|
|
4542
|
-
const sizeMB = (sizeBytes / 1024 / 1024).toFixed(2);
|
|
4543
|
-
console.log(' 压缩大小:', sizeMB, 'MB');
|
|
4544
|
-
}
|
|
4545
|
-
if (filesMatch) {
|
|
4546
|
-
console.log(' 文件数量:', filesMatch[1]);
|
|
4547
|
-
}
|
|
4548
|
-
console.log();
|
|
4549
|
-
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);
|
|
4550
5855
|
}
|
|
4551
5856
|
|
|
4552
|
-
await zipWithLibrary(absPath, outputPath);
|
|
4553
5857
|
console.log('✓ 压缩完成!');
|
|
4554
5858
|
console.log(' 输出文件:', outputPath);
|
|
4555
5859
|
console.log();
|
|
@@ -4559,7 +5863,7 @@ async function cmdZip(targetPath, options = {}) {
|
|
|
4559
5863
|
}
|
|
4560
5864
|
}
|
|
4561
5865
|
|
|
4562
|
-
// 解压(
|
|
5866
|
+
// 解压(zip-lib)
|
|
4563
5867
|
async function cmdUnzip(zipPath, outputDir) {
|
|
4564
5868
|
if (!zipPath) {
|
|
4565
5869
|
console.error('用法: codexmate unzip <zip文件路径> [输出目录]');
|
|
@@ -4591,24 +5895,10 @@ async function cmdUnzip(zipPath, outputDir) {
|
|
|
4591
5895
|
console.log('\n解压配置:');
|
|
4592
5896
|
console.log(' 源文件:', absZipPath);
|
|
4593
5897
|
console.log(' 输出目录:', absOutputDir);
|
|
4594
|
-
console.log(' 解压工具:',
|
|
4595
|
-
console.log(' 多线程:', unzipTool.type === '7z' ? '启用' : '未启用(JS 库)');
|
|
5898
|
+
console.log(' 解压工具:', 'zip-lib');
|
|
4596
5899
|
console.log('\n开始解压...\n');
|
|
4597
5900
|
|
|
4598
5901
|
try {
|
|
4599
|
-
if (unzipTool.type === '7z') {
|
|
4600
|
-
const cmd = `"${unzipTool.cmd}" x -mmt=on -o"${absOutputDir}" "${absZipPath}" -y`;
|
|
4601
|
-
const result = execSync(cmd, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
|
|
4602
|
-
const filesMatch = result.match(/(\d+)\s*files/);
|
|
4603
|
-
console.log('✓ 解压完成!');
|
|
4604
|
-
console.log(' 输出目录:', absOutputDir);
|
|
4605
|
-
if (filesMatch) {
|
|
4606
|
-
console.log(' 文件数量:', filesMatch[1]);
|
|
4607
|
-
}
|
|
4608
|
-
console.log();
|
|
4609
|
-
return;
|
|
4610
|
-
}
|
|
4611
|
-
|
|
4612
5902
|
await unzipWithLibrary(absZipPath, absOutputDir);
|
|
4613
5903
|
console.log('✓ 解压完成!');
|
|
4614
5904
|
console.log(' 输出目录:', absOutputDir);
|
|
@@ -4899,6 +6189,9 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
4899
6189
|
initNotice: consumeInitNotice()
|
|
4900
6190
|
};
|
|
4901
6191
|
break;
|
|
6192
|
+
case 'install-status':
|
|
6193
|
+
result = buildInstallStatusReport();
|
|
6194
|
+
break;
|
|
4902
6195
|
case 'list':
|
|
4903
6196
|
const listConfigResult = readConfigOrVirtualDefault();
|
|
4904
6197
|
const listConfig = listConfigResult.config;
|
|
@@ -4911,7 +6204,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
4911
6204
|
url: p.base_url || '',
|
|
4912
6205
|
key: maskKey(p.preferred_auth_method || ''),
|
|
4913
6206
|
hasKey: !!(p.preferred_auth_method && p.preferred_auth_method.trim()),
|
|
4914
|
-
current: name === current
|
|
6207
|
+
current: name === current,
|
|
6208
|
+
readOnly: isBuiltinProxyProvider(name),
|
|
6209
|
+
nonDeletable: isNonDeletableProvider(name),
|
|
6210
|
+
nonEditable: isNonEditableProvider(name)
|
|
4915
6211
|
}))
|
|
4916
6212
|
};
|
|
4917
6213
|
break;
|
|
@@ -5082,6 +6378,61 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
5082
6378
|
case 'session-plain':
|
|
5083
6379
|
result = await readSessionPlain(params);
|
|
5084
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;
|
|
5085
6436
|
default:
|
|
5086
6437
|
result = { error: '未知操作' };
|
|
5087
6438
|
}
|
|
@@ -5126,6 +6477,34 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
5126
6477
|
: 'application/octet-stream';
|
|
5127
6478
|
res.writeHead(200, { 'Content-Type': mime });
|
|
5128
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);
|
|
5129
6508
|
} else if (requestPath.startsWith('/res/')) {
|
|
5130
6509
|
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
|
|
5131
6510
|
const filePath = path.join(__dirname, normalized);
|
|
@@ -5229,74 +6608,347 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
|
|
|
5229
6608
|
|
|
5230
6609
|
// 打开 Web UI
|
|
5231
6610
|
function cmdStart(options = {}) {
|
|
5232
|
-
|
|
5233
|
-
const webDirLegacy = path.join(__dirname, 'web-ui');
|
|
5234
|
-
const webDirSrc = path.join(__dirname, 'src', 'web-ui');
|
|
5235
|
-
const webDir = fs.existsSync(webDirSrc) ? webDirSrc : webDirLegacy;
|
|
6611
|
+
const webDir = path.join(__dirname, 'web-ui');
|
|
5236
6612
|
const newHtmlPath = path.join(webDir, 'index.html');
|
|
5237
6613
|
const legacyHtmlPath = path.join(__dirname, 'web-ui.html');
|
|
5238
|
-
const
|
|
5239
|
-
|
|
5240
|
-
let htmlPath = newHtmlPath;
|
|
5241
|
-
if (!fs.existsSync(newHtmlPath)) {
|
|
5242
|
-
htmlPath = fs.existsSync(srcHtmlPath) ? srcHtmlPath : legacyHtmlPath;
|
|
5243
|
-
}
|
|
5244
|
-
const assetsDirLegacy = path.join(__dirname, 'res');
|
|
5245
|
-
const assetsDirSrc = path.join(__dirname, 'src', 'res');
|
|
5246
|
-
const assetsDir = fs.existsSync(assetsDirSrc) ? assetsDirSrc : assetsDirLegacy;
|
|
6614
|
+
const htmlPath = fs.existsSync(newHtmlPath) ? newHtmlPath : legacyHtmlPath;
|
|
6615
|
+
const assetsDir = path.join(__dirname, 'res');
|
|
5247
6616
|
if (!fs.existsSync(htmlPath)) {
|
|
5248
6617
|
console.error('错误: Web UI 页面不存在(尝试路径: web-ui/index.html, web-ui.html)');
|
|
5249
6618
|
process.exit(1);
|
|
5250
6619
|
}
|
|
5251
6620
|
|
|
5252
|
-
const port = resolveWebPort();
|
|
5253
|
-
const host = resolveWebHost(options);
|
|
5254
|
-
|
|
5255
|
-
let serverHandle = createWebServer({
|
|
5256
|
-
htmlPath,
|
|
5257
|
-
assetsDir,
|
|
5258
|
-
webDir,
|
|
5259
|
-
host,
|
|
5260
|
-
port,
|
|
5261
|
-
openBrowser: true
|
|
5262
|
-
});
|
|
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
|
+
}
|
|
6845
|
+
|
|
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);
|
|
6866
|
+
}
|
|
6867
|
+
console.log();
|
|
6868
|
+
return;
|
|
6869
|
+
}
|
|
6870
|
+
|
|
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
|
+
}
|
|
6884
|
+
|
|
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}` : ''}`);
|
|
6896
|
+
}
|
|
6897
|
+
console.log();
|
|
6898
|
+
return;
|
|
6899
|
+
}
|
|
5263
6900
|
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
console.log(`\n~ 侦测到前端变更 (${fileLabel}),重启中...`);
|
|
5269
|
-
console.log(' 正在停止旧服务...');
|
|
5270
|
-
try {
|
|
5271
|
-
await serverHandle.stop();
|
|
5272
|
-
console.log(' 旧服务已停止');
|
|
5273
|
-
} catch (e) {
|
|
5274
|
-
console.warn('! 停止旧服务失败:', e.message || e);
|
|
5275
|
-
}
|
|
5276
|
-
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
5277
|
-
try {
|
|
5278
|
-
serverHandle = createWebServer({
|
|
5279
|
-
htmlPath,
|
|
5280
|
-
assetsDir,
|
|
5281
|
-
webDir,
|
|
5282
|
-
host,
|
|
5283
|
-
port,
|
|
5284
|
-
openBrowser: false
|
|
5285
|
-
});
|
|
5286
|
-
console.log('✓ 已重启 Web UI 服务\n');
|
|
5287
|
-
} catch (e) {
|
|
5288
|
-
console.error('! 重启失败:', e.message || e);
|
|
5289
|
-
}
|
|
6901
|
+
if (subcommand === 'enable' || subcommand === 'default-codex') {
|
|
6902
|
+
const result = await ensureBuiltinProxyForCodexDefault(options);
|
|
6903
|
+
if (result.error) {
|
|
6904
|
+
throw new Error(result.error);
|
|
5290
6905
|
}
|
|
5291
|
-
|
|
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
|
+
}
|
|
5292
6918
|
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
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
|
+
}
|
|
5297
6944
|
|
|
5298
|
-
|
|
5299
|
-
|
|
6945
|
+
if (subcommand === 'stop') {
|
|
6946
|
+
await stopBuiltinProxyRuntime();
|
|
6947
|
+
console.log('✓ 内建代理已停止\n');
|
|
6948
|
+
return;
|
|
6949
|
+
}
|
|
6950
|
+
|
|
6951
|
+
throw new Error(`未知 proxy 子命令: ${subcommand}`);
|
|
5300
6952
|
}
|
|
5301
6953
|
|
|
5302
6954
|
async function runProxyCommand(displayName, binNames, args = [], installTip = '') {
|
|
@@ -5354,6 +7006,16 @@ async function runProxyCommand(displayName, binNames, args = [], installTip = ''
|
|
|
5354
7006
|
}
|
|
5355
7007
|
|
|
5356
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
|
+
}
|
|
5357
7019
|
return runProxyCommand('Codex', 'codex', args);
|
|
5358
7020
|
}
|
|
5359
7021
|
|
|
@@ -5365,16 +7027,783 @@ async function cmdGemini(args = []) {
|
|
|
5365
7027
|
return runProxyCommand('Gemini', ['gemini', 'gemini-cli'], args, 'npm install -g @google/gemini-cli');
|
|
5366
7028
|
}
|
|
5367
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
|
+
|
|
5368
7792
|
// ============================================================================
|
|
5369
7793
|
// 主程序
|
|
5370
7794
|
// ============================================================================
|
|
5371
7795
|
async function main() {
|
|
7796
|
+
const args = process.argv.slice(2);
|
|
7797
|
+
const command = args[0];
|
|
7798
|
+
const isMcpCommand = command === 'mcp';
|
|
5372
7799
|
const bootstrap = ensureManagedConfigBootstrap();
|
|
5373
7800
|
if (bootstrap && bootstrap.notice) {
|
|
5374
|
-
|
|
7801
|
+
// MCP stdio transport requires stdout to be protocol-clean.
|
|
7802
|
+
if (!isMcpCommand) {
|
|
7803
|
+
console.log(`\n[Init] ${bootstrap.notice}`);
|
|
7804
|
+
}
|
|
5375
7805
|
}
|
|
5376
7806
|
|
|
5377
|
-
const args = process.argv.slice(2);
|
|
5378
7807
|
if (args.length === 0) {
|
|
5379
7808
|
console.log('\nCodex Mate - Codex 提供商管理工具');
|
|
5380
7809
|
console.log('\n用法:');
|
|
@@ -5389,19 +7818,20 @@ async function main() {
|
|
|
5389
7818
|
console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
|
|
5390
7819
|
console.log(' codexmate add-model <模型> 添加模型');
|
|
5391
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> 内建代理');
|
|
5392
7823
|
console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
|
|
5393
7824
|
console.log(' codexmate codex [参数...] 等同于 codex --yolo');
|
|
5394
7825
|
console.log(' codexmate qwen [参数...] 等同于 qwen --yolo');
|
|
5395
7826
|
console.log(' codexmate gemini [参数...] 等同于 gemini --yolo');
|
|
7827
|
+
console.log(' codexmate mcp [serve] [--transport stdio] [--allow-write|--read-only]');
|
|
5396
7828
|
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
5397
|
-
console.log(' codexmate zip <路径> [--max:级别]
|
|
5398
|
-
console.log(' codexmate unzip <zip文件> [输出目录] 解压(
|
|
7829
|
+
console.log(' codexmate zip <路径> [--max:级别] 压缩(系统 zip 优先,其次 zip-lib)');
|
|
7830
|
+
console.log(' codexmate unzip <zip文件> [输出目录] 解压(zip-lib)');
|
|
5399
7831
|
console.log('');
|
|
5400
7832
|
process.exit(0);
|
|
5401
7833
|
}
|
|
5402
7834
|
|
|
5403
|
-
const command = args[0];
|
|
5404
|
-
|
|
5405
7835
|
switch (command) {
|
|
5406
7836
|
case 'status': cmdStatus(); break;
|
|
5407
7837
|
case 'setup': await cmdSetup(); break;
|
|
@@ -5414,6 +7844,8 @@ async function main() {
|
|
|
5414
7844
|
case 'claude': cmdClaude(args[1], args[2], args[3]); break;
|
|
5415
7845
|
case 'add-model': cmdAddModel(args[1]); break;
|
|
5416
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;
|
|
5417
7849
|
case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
|
|
5418
7850
|
case 'start':
|
|
5419
7851
|
console.error('错误: 命令已更名为 "run",请使用: codexmate run');
|
|
@@ -5434,6 +7866,7 @@ async function main() {
|
|
|
5434
7866
|
process.exit(exitCode);
|
|
5435
7867
|
break;
|
|
5436
7868
|
}
|
|
7869
|
+
case 'mcp': await cmdMcp(args.slice(1)); break;
|
|
5437
7870
|
case 'export-session': await cmdExportSession(args.slice(1)); break;
|
|
5438
7871
|
case 'zip': {
|
|
5439
7872
|
// 解析 --max:N 参数
|