codexmate 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +122 -8
- package/.planning/.fix-attempts +1 -0
- package/.planning/.lock +6 -0
- package/.planning/.verify-cache.json +14 -0
- package/.planning/CHECKPOINT.json +46 -0
- package/.planning/DESIGN.md +26 -0
- package/.planning/HISTORY.json +124 -0
- package/.planning/PLAN.md +69 -0
- package/.planning/REVIEW.md +41 -0
- package/.planning/STATE.md +12 -0
- package/.planning/STATS.json +13 -0
- package/.planning/VERIFICATION.md +70 -0
- package/.planning/daude-code-plan.md +51 -0
- package/.planning/research/architecture.md +32 -0
- package/.planning/research/conventions.md +36 -0
- package/.planning/task_1-REVIEW.md +29 -0
- package/.planning/task_1-SUMMARY.md +32 -0
- package/.planning/task_2-REVIEW.md +24 -0
- package/.planning/task_2-SUMMARY.md +37 -0
- package/.planning/task_3-REVIEW.md +25 -0
- package/.planning/task_3-SUMMARY.md +31 -0
- package/README.md +58 -52
- package/README.zh-CN.md +68 -56
- package/cli.js +1142 -1427
- package/lib/cli-file-utils.js +151 -0
- package/lib/cli-models-utils.js +152 -0
- package/lib/cli-network-utils.js +148 -0
- package/lib/cli-session-utils.js +121 -0
- package/lib/cli-utils.js +139 -0
- package/package.json +4 -2
- package/res/json5.min.js +1 -0
- package/res/vue.global.js +18552 -0
- package/tests/e2e/helpers.js +214 -0
- package/tests/e2e/recent-health.e2e.js +6 -0
- package/tests/e2e/run.js +103 -306
- package/tests/e2e/test-claude.js +21 -0
- package/tests/e2e/test-config.js +124 -0
- package/tests/e2e/test-health-speed.js +79 -0
- package/tests/e2e/test-openclaw.js +47 -0
- package/tests/e2e/test-session-search.js +114 -0
- package/tests/e2e/test-sessions.js +69 -0
- package/tests/e2e/test-setup.js +159 -0
- package/tests/unit/run.mjs +29 -0
- package/tests/unit/web-ui-logic.test.mjs +186 -0
- package/web-ui/app.js +2841 -0
- package/web-ui/logic.mjs +157 -0
- package/web-ui.html +1045 -2996
package/cli.js
CHANGED
|
@@ -6,13 +6,55 @@ 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 } = require('child_process');
|
|
10
10
|
const http = require('http');
|
|
11
11
|
const https = require('https');
|
|
12
12
|
const readline = require('readline');
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const {
|
|
14
|
+
expandHomePath,
|
|
15
|
+
resolveExistingDir,
|
|
16
|
+
resolveHomePath,
|
|
17
|
+
hasUtf8Bom,
|
|
18
|
+
stripUtf8Bom,
|
|
19
|
+
ensureUtf8Bom,
|
|
20
|
+
detectLineEnding,
|
|
21
|
+
normalizeLineEnding,
|
|
22
|
+
isValidProviderName,
|
|
23
|
+
buildModelsCandidates,
|
|
24
|
+
isValidHttpUrl,
|
|
25
|
+
normalizeBaseUrl,
|
|
26
|
+
joinApiUrl
|
|
27
|
+
} = require('./lib/cli-utils');
|
|
28
|
+
const {
|
|
29
|
+
ensureDir,
|
|
30
|
+
readJsonFile,
|
|
31
|
+
readJsonArrayFile,
|
|
32
|
+
readJsonObjectFromFile,
|
|
33
|
+
backupFileIfNeededOnce,
|
|
34
|
+
writeJsonAtomic,
|
|
35
|
+
formatTimestampForFileName
|
|
36
|
+
} = require('./lib/cli-file-utils');
|
|
37
|
+
const {
|
|
38
|
+
extractModelNames,
|
|
39
|
+
hasModelsListPayload,
|
|
40
|
+
extractModelIds,
|
|
41
|
+
buildModelsProbeUrl,
|
|
42
|
+
buildModelProbeSpec,
|
|
43
|
+
buildModelsCacheKey
|
|
44
|
+
} = require('./lib/cli-models-utils');
|
|
45
|
+
const { probeUrl, probeJsonPost } = require('./lib/cli-network-utils');
|
|
46
|
+
const {
|
|
47
|
+
toIsoTime,
|
|
48
|
+
updateLatestIso,
|
|
49
|
+
truncateText,
|
|
50
|
+
extractMessageText,
|
|
51
|
+
normalizeRole,
|
|
52
|
+
parseMaxMessagesValue,
|
|
53
|
+
resolveMaxMessagesValue
|
|
54
|
+
} = require('./lib/cli-session-utils');
|
|
55
|
+
|
|
56
|
+
const DEFAULT_WEB_PORT = 3737;
|
|
57
|
+
const DEFAULT_WEB_HOST = '127.0.0.1';
|
|
16
58
|
|
|
17
59
|
// ============================================================================
|
|
18
60
|
// 配置
|
|
@@ -31,6 +73,7 @@ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
|
31
73
|
const CLAUDE_SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
32
74
|
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), '.claude', 'projects');
|
|
33
75
|
const RECENT_CONFIGS_FILE = path.join(CONFIG_DIR, 'recent-configs.json');
|
|
76
|
+
const DEFAULT_CLAUDE_MODEL = 'glm-4.7';
|
|
34
77
|
|
|
35
78
|
const DEFAULT_MODELS = ['gpt-5.3-codex', 'gpt-5.1-codex-max', 'gpt-4-turbo', 'gpt-4'];
|
|
36
79
|
const SPEED_TEST_TIMEOUT_MS = 8000;
|
|
@@ -42,14 +85,13 @@ const MAX_SESSION_DETAIL_MESSAGES = 1000;
|
|
|
42
85
|
const SESSION_TITLE_READ_BYTES = 64 * 1024;
|
|
43
86
|
const CODEXMATE_MANAGED_MARKER = '# codexmate-managed: true';
|
|
44
87
|
const SESSION_LIST_CACHE_TTL_MS = 4000;
|
|
45
|
-
const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
|
|
46
|
-
const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
47
|
-
const DEFAULT_CONTENT_SCAN_LIMIT =
|
|
88
|
+
const SESSION_SUMMARY_READ_BYTES = 256 * 1024;
|
|
89
|
+
const SESSION_CONTENT_READ_BYTES = SESSION_SUMMARY_READ_BYTES;
|
|
90
|
+
const DEFAULT_CONTENT_SCAN_LIMIT = 50;
|
|
48
91
|
const SESSION_SCAN_FACTOR = 4;
|
|
49
92
|
const SESSION_SCAN_MIN_FILES = 800;
|
|
50
93
|
const MAX_SESSION_PATH_LIST_SIZE = 2000;
|
|
51
94
|
const AGENTS_FILE_NAME = 'AGENTS.md';
|
|
52
|
-
const UTF8_BOM = '\ufeff';
|
|
53
95
|
const MODELS_CACHE_TTL_MS = 60 * 1000;
|
|
54
96
|
const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
|
|
55
97
|
const MODELS_CACHE_MAX_ENTRIES = 50;
|
|
@@ -66,26 +108,26 @@ const BOOTSTRAP_TEXT_MARKERS = [
|
|
|
66
108
|
const HTTP_KEEP_ALIVE_AGENT = new http.Agent({ keepAlive: true });
|
|
67
109
|
const HTTPS_KEEP_ALIVE_AGENT = new https.Agent({ keepAlive: true });
|
|
68
110
|
|
|
69
|
-
function resolveWebPort() {
|
|
70
|
-
const raw = process.env.CODEXMATE_PORT;
|
|
71
|
-
if (!raw) return DEFAULT_WEB_PORT;
|
|
72
|
-
const parsed = parseInt(raw, 10);
|
|
73
|
-
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_WEB_PORT;
|
|
74
|
-
return parsed;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function resolveWebHost(options = {}) {
|
|
78
|
-
const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
|
|
79
|
-
if (optionHost) {
|
|
80
|
-
return optionHost;
|
|
81
|
-
}
|
|
82
|
-
const envHost = typeof process.env.CODEXMATE_HOST === 'string' ? process.env.CODEXMATE_HOST.trim() : '';
|
|
83
|
-
if (envHost) {
|
|
84
|
-
return envHost;
|
|
85
|
-
}
|
|
86
|
-
return DEFAULT_WEB_HOST;
|
|
87
|
-
}
|
|
88
|
-
|
|
111
|
+
function resolveWebPort() {
|
|
112
|
+
const raw = process.env.CODEXMATE_PORT;
|
|
113
|
+
if (!raw) return DEFAULT_WEB_PORT;
|
|
114
|
+
const parsed = parseInt(raw, 10);
|
|
115
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_WEB_PORT;
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveWebHost(options = {}) {
|
|
120
|
+
const optionHost = typeof options.host === 'string' ? options.host.trim() : '';
|
|
121
|
+
if (optionHost) {
|
|
122
|
+
return optionHost;
|
|
123
|
+
}
|
|
124
|
+
const envHost = typeof process.env.CODEXMATE_HOST === 'string' ? process.env.CODEXMATE_HOST.trim() : '';
|
|
125
|
+
if (envHost) {
|
|
126
|
+
return envHost;
|
|
127
|
+
}
|
|
128
|
+
return DEFAULT_WEB_HOST;
|
|
129
|
+
}
|
|
130
|
+
|
|
89
131
|
const EMPTY_CONFIG_FALLBACK_TEMPLATE = `model = "gpt-5.3-codex"
|
|
90
132
|
model_reasoning_effort = "high"
|
|
91
133
|
disable_response_storage = true
|
|
@@ -178,69 +220,6 @@ function updateAuthJson(apiKey) {
|
|
|
178
220
|
fs.writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2), 'utf-8');
|
|
179
221
|
}
|
|
180
222
|
|
|
181
|
-
function readJsonFile(filePath, fallback = null) {
|
|
182
|
-
if (!fs.existsSync(filePath)) {
|
|
183
|
-
return fallback;
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
187
|
-
} catch (e) {
|
|
188
|
-
return fallback;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function readJsonArrayFile(filePath, fallback = []) {
|
|
193
|
-
if (!fs.existsSync(filePath)) {
|
|
194
|
-
return Array.isArray(fallback) ? [...fallback] : [];
|
|
195
|
-
}
|
|
196
|
-
try {
|
|
197
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
198
|
-
if (!content.trim()) {
|
|
199
|
-
return Array.isArray(fallback) ? [...fallback] : [];
|
|
200
|
-
}
|
|
201
|
-
const parsed = JSON.parse(content);
|
|
202
|
-
return Array.isArray(parsed) ? parsed : (Array.isArray(fallback) ? [...fallback] : []);
|
|
203
|
-
} catch (e) {
|
|
204
|
-
return Array.isArray(fallback) ? [...fallback] : [];
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function ensureDir(dirPath) {
|
|
209
|
-
if (!fs.existsSync(dirPath)) {
|
|
210
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function expandHomePath(value) {
|
|
215
|
-
if (typeof value !== 'string') {
|
|
216
|
-
return '';
|
|
217
|
-
}
|
|
218
|
-
const trimmed = value.trim();
|
|
219
|
-
if (!trimmed) {
|
|
220
|
-
return '';
|
|
221
|
-
}
|
|
222
|
-
if (trimmed === '~') {
|
|
223
|
-
return os.homedir();
|
|
224
|
-
}
|
|
225
|
-
if (trimmed.startsWith(`~${path.sep}`) || trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
226
|
-
return path.resolve(os.homedir(), trimmed.slice(2));
|
|
227
|
-
}
|
|
228
|
-
return trimmed;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function resolveExistingDir(candidates = [], fallback = '') {
|
|
232
|
-
for (const raw of candidates) {
|
|
233
|
-
const candidate = expandHomePath(raw);
|
|
234
|
-
if (!candidate) continue;
|
|
235
|
-
try {
|
|
236
|
-
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
237
|
-
return candidate;
|
|
238
|
-
}
|
|
239
|
-
} catch (e) {}
|
|
240
|
-
}
|
|
241
|
-
return fallback;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
223
|
function getCodexSessionsDir() {
|
|
245
224
|
const candidates = [];
|
|
246
225
|
const envCodexHome = process.env.CODEX_HOME;
|
|
@@ -271,100 +250,6 @@ function getClaudeProjectsDir() {
|
|
|
271
250
|
return resolveExistingDir(candidates, CLAUDE_PROJECTS_DIR);
|
|
272
251
|
}
|
|
273
252
|
|
|
274
|
-
function hasUtf8Bom(text) {
|
|
275
|
-
return typeof text === 'string' && text.charCodeAt(0) === 0xfeff;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function stripUtf8Bom(text) {
|
|
279
|
-
if (!text) return '';
|
|
280
|
-
return hasUtf8Bom(text) ? text.slice(1) : text;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function ensureUtf8Bom(text) {
|
|
284
|
-
const content = typeof text === 'string' ? text : '';
|
|
285
|
-
return hasUtf8Bom(content) ? content : UTF8_BOM + content;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function detectLineEnding(text) {
|
|
289
|
-
return typeof text === 'string' && text.includes('\r\n') ? '\r\n' : '\n';
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function normalizeLineEnding(text, lineEnding) {
|
|
293
|
-
const normalized = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
294
|
-
return lineEnding === '\r\n' ? normalized.replace(/\n/g, '\r\n') : normalized;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function isValidProviderName(name) {
|
|
298
|
-
return typeof name === 'string' && /^[a-zA-Z0-9._-]+$/.test(name.trim());
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function buildModelsCandidates(baseUrl) {
|
|
302
|
-
const trimmed = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
|
303
|
-
if (!trimmed) return [];
|
|
304
|
-
if (/\/models\/?$/.test(trimmed)) {
|
|
305
|
-
return [trimmed];
|
|
306
|
-
}
|
|
307
|
-
const normalized = trimmed.replace(/\/+$/, '');
|
|
308
|
-
const candidates = [];
|
|
309
|
-
const pushUnique = (url) => {
|
|
310
|
-
if (url && !candidates.includes(url)) {
|
|
311
|
-
candidates.push(url);
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
if (/\/v1$/i.test(normalized)) {
|
|
316
|
-
pushUnique(normalized + '/models');
|
|
317
|
-
} else {
|
|
318
|
-
pushUnique(normalized + '/v1/models');
|
|
319
|
-
pushUnique(normalized + '/models');
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
pushUnique(trimmed);
|
|
323
|
-
return candidates;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function extractModelNames(payload) {
|
|
327
|
-
if (!payload || typeof payload !== 'object') return [];
|
|
328
|
-
const data = Array.isArray(payload.data)
|
|
329
|
-
? payload.data
|
|
330
|
-
: (Array.isArray(payload.models) ? payload.models : []);
|
|
331
|
-
const names = [];
|
|
332
|
-
for (const item of data) {
|
|
333
|
-
if (typeof item === 'string') {
|
|
334
|
-
if (item.trim()) names.push(item.trim());
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
if (!item || typeof item !== 'object') continue;
|
|
338
|
-
const name = item.id || item.name || item.model || '';
|
|
339
|
-
if (typeof name === 'string' && name.trim()) {
|
|
340
|
-
names.push(name.trim());
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return Array.from(new Set(names));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function hasModelsListPayload(payload) {
|
|
347
|
-
if (!payload || typeof payload !== 'object') return false;
|
|
348
|
-
return Array.isArray(payload.data) || Array.isArray(payload.models);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function hashModelsCacheValue(value) {
|
|
352
|
-
if (!value) return '';
|
|
353
|
-
try {
|
|
354
|
-
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
355
|
-
} catch (e) {
|
|
356
|
-
return '';
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function buildModelsCacheKey(baseUrl, apiKey) {
|
|
361
|
-
const normalizedUrl = typeof baseUrl === 'string'
|
|
362
|
-
? baseUrl.trim().replace(/\/+$/, '')
|
|
363
|
-
: '';
|
|
364
|
-
const apiKeyHash = hashModelsCacheValue(apiKey);
|
|
365
|
-
return `${normalizedUrl}|${apiKeyHash}`;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
253
|
function readModelsCacheEntry(cacheKey) {
|
|
369
254
|
if (!cacheKey) return null;
|
|
370
255
|
const entry = g_modelsCache.get(cacheKey);
|
|
@@ -439,6 +324,7 @@ async function fetchModelsFromBaseUrlCore(baseUrl, apiKey) {
|
|
|
439
324
|
};
|
|
440
325
|
if (apiKey) {
|
|
441
326
|
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
327
|
+
headers['x-api-key'] = apiKey;
|
|
442
328
|
}
|
|
443
329
|
|
|
444
330
|
const result = await new Promise((innerResolve) => {
|
|
@@ -596,16 +482,6 @@ function applyAgentsFile(params = {}) {
|
|
|
596
482
|
}
|
|
597
483
|
}
|
|
598
484
|
|
|
599
|
-
function resolveHomePath(input) {
|
|
600
|
-
const raw = typeof input === 'string' ? input.trim() : '';
|
|
601
|
-
if (!raw) return '';
|
|
602
|
-
if (raw === '~') return os.homedir();
|
|
603
|
-
if (raw.startsWith('~/') || raw.startsWith('~\\')) {
|
|
604
|
-
return path.join(os.homedir(), raw.slice(2));
|
|
605
|
-
}
|
|
606
|
-
return raw;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
485
|
function resolveOpenclawWorkspaceDir(config) {
|
|
610
486
|
const workspace = config
|
|
611
487
|
&& config.agents
|
|
@@ -847,83 +723,6 @@ function applyOpenclawConfig(params = {}) {
|
|
|
847
723
|
}
|
|
848
724
|
}
|
|
849
725
|
|
|
850
|
-
function readJsonObjectFromFile(filePath, fallback = {}) {
|
|
851
|
-
if (!fs.existsSync(filePath)) {
|
|
852
|
-
return { ok: true, exists: false, data: { ...fallback } };
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
try {
|
|
856
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
857
|
-
if (!content.trim()) {
|
|
858
|
-
return { ok: true, exists: true, data: { ...fallback } };
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
const parsed = JSON.parse(content);
|
|
862
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
863
|
-
return {
|
|
864
|
-
ok: false,
|
|
865
|
-
exists: true,
|
|
866
|
-
error: `配置文件格式错误(根节点必须是对象): ${filePath}`
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
return { ok: true, exists: true, data: parsed };
|
|
870
|
-
} catch (e) {
|
|
871
|
-
return {
|
|
872
|
-
ok: false,
|
|
873
|
-
exists: true,
|
|
874
|
-
error: `配置文件解析失败: ${e.message}`
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
function backupFileIfNeededOnce(filePath, backupPrefix = 'codexmate-backup') {
|
|
880
|
-
if (!fs.existsSync(filePath)) {
|
|
881
|
-
return '';
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
const dirPath = path.dirname(filePath);
|
|
885
|
-
const baseName = path.basename(filePath);
|
|
886
|
-
const existingPrefix = `${baseName}.${backupPrefix}-`;
|
|
887
|
-
const hasBackup = fs.readdirSync(dirPath).some(fileName =>
|
|
888
|
-
fileName.startsWith(existingPrefix) && fileName.endsWith('.bak')
|
|
889
|
-
);
|
|
890
|
-
|
|
891
|
-
if (hasBackup) {
|
|
892
|
-
return '';
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
const backupPath = path.join(dirPath, `${existingPrefix}${formatTimestampForFileName()}.bak`);
|
|
896
|
-
fs.copyFileSync(filePath, backupPath);
|
|
897
|
-
return backupPath;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function writeJsonAtomic(filePath, data) {
|
|
901
|
-
const dirPath = path.dirname(filePath);
|
|
902
|
-
ensureDir(dirPath);
|
|
903
|
-
|
|
904
|
-
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
905
|
-
const content = `${JSON.stringify(data, null, 2)}\n`;
|
|
906
|
-
|
|
907
|
-
try {
|
|
908
|
-
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
909
|
-
try {
|
|
910
|
-
fs.renameSync(tmpPath, filePath);
|
|
911
|
-
} catch (renameError) {
|
|
912
|
-
if (process.platform === 'win32') {
|
|
913
|
-
fs.copyFileSync(tmpPath, filePath);
|
|
914
|
-
fs.unlinkSync(tmpPath);
|
|
915
|
-
} else {
|
|
916
|
-
throw renameError;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
} catch (e) {
|
|
920
|
-
if (fs.existsSync(tmpPath)) {
|
|
921
|
-
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
922
|
-
}
|
|
923
|
-
throw new Error(`写入 JSON 文件失败: ${e.message}`);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
726
|
function normalizeRecentConfigs(items) {
|
|
928
727
|
if (!Array.isArray(items)) return [];
|
|
929
728
|
const output = [];
|
|
@@ -975,330 +774,84 @@ function recordRecentConfig(provider, model) {
|
|
|
975
774
|
writeRecentConfigs(trimmed);
|
|
976
775
|
}
|
|
977
776
|
|
|
978
|
-
function
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
if (typeof value !== 'string') return '';
|
|
990
|
-
return value.trim().replace(/\/+$/g, '');
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function joinApiUrl(baseUrl, pathSuffix) {
|
|
994
|
-
const trimmed = normalizeBaseUrl(baseUrl);
|
|
995
|
-
if (!trimmed) return '';
|
|
996
|
-
const safeSuffix = String(pathSuffix || '').replace(/^\/+/g, '');
|
|
997
|
-
if (!safeSuffix) return trimmed;
|
|
998
|
-
if (/\/v1$/i.test(trimmed)) {
|
|
999
|
-
return `${trimmed}/${safeSuffix}`;
|
|
777
|
+
async function runRemoteHealthCheck(provider, modelName, options = {}) {
|
|
778
|
+
const issues = [];
|
|
779
|
+
const results = {};
|
|
780
|
+
const baseUrl = normalizeBaseUrl(provider && provider.base_url ? provider.base_url : '');
|
|
781
|
+
if (!baseUrl) {
|
|
782
|
+
issues.push({
|
|
783
|
+
code: 'remote-skip-base-url',
|
|
784
|
+
message: '无法进行远程探测:base_url 为空',
|
|
785
|
+
suggestion: '补全 base_url 或关闭远程探测'
|
|
786
|
+
});
|
|
787
|
+
return { issues, results };
|
|
1000
788
|
}
|
|
1001
|
-
return `${trimmed}/v1/${safeSuffix}`;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function buildModelsProbeUrl(baseUrl) {
|
|
1005
|
-
return joinApiUrl(baseUrl, 'models');
|
|
1006
|
-
}
|
|
1007
789
|
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
790
|
+
const requiresAuth = provider && provider.requires_openai_auth !== false;
|
|
791
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
792
|
+
? provider.preferred_auth_method.trim()
|
|
793
|
+
: '';
|
|
794
|
+
const authValue = requiresAuth ? apiKey : (apiKey || '');
|
|
795
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
1013
796
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
797
|
+
const baseProbe = await probeUrl(baseUrl, { apiKey: authValue, timeoutMs });
|
|
798
|
+
results.base = {
|
|
799
|
+
url: baseUrl,
|
|
800
|
+
status: baseProbe.status || 0,
|
|
801
|
+
ok: baseProbe.ok,
|
|
802
|
+
durationMs: baseProbe.durationMs || 0
|
|
803
|
+
};
|
|
1017
804
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
max_tokens: 1,
|
|
1026
|
-
temperature: 0
|
|
1027
|
-
}
|
|
1028
|
-
};
|
|
805
|
+
if (!baseProbe.ok) {
|
|
806
|
+
issues.push({
|
|
807
|
+
code: 'remote-unreachable',
|
|
808
|
+
message: `远程探测失败:${baseProbe.error || '无法连接'}`,
|
|
809
|
+
suggestion: '检查网络与 base_url 可达性'
|
|
810
|
+
});
|
|
811
|
+
return { issues, results };
|
|
1029
812
|
}
|
|
1030
813
|
|
|
1031
|
-
if (
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
814
|
+
if (baseProbe.status === 401 || baseProbe.status === 403) {
|
|
815
|
+
issues.push({
|
|
816
|
+
code: 'remote-auth-failed',
|
|
817
|
+
message: '远程探测鉴权失败(401/403)',
|
|
818
|
+
suggestion: '检查 API Key 或认证方式'
|
|
819
|
+
});
|
|
820
|
+
} else if (baseProbe.status >= 400) {
|
|
821
|
+
issues.push({
|
|
822
|
+
code: 'remote-http-error',
|
|
823
|
+
message: `远程探测返回异常状态: ${baseProbe.status}`,
|
|
824
|
+
suggestion: '检查 base_url 是否正确'
|
|
825
|
+
});
|
|
1041
826
|
}
|
|
1042
827
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
function probeUrl(targetUrl, options = {}) {
|
|
1054
|
-
return new Promise((resolve) => {
|
|
1055
|
-
let parsed;
|
|
1056
|
-
try {
|
|
1057
|
-
parsed = new URL(targetUrl);
|
|
1058
|
-
} catch (e) {
|
|
1059
|
-
return resolve({ ok: false, error: 'Invalid URL' });
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
const transport = parsed.protocol === 'https:' ? https : http;
|
|
1063
|
-
const headers = {
|
|
1064
|
-
'User-Agent': 'codexmate-health-check',
|
|
1065
|
-
'Accept': 'application/json'
|
|
828
|
+
const modelsUrl = buildModelsProbeUrl(baseUrl);
|
|
829
|
+
if (modelsUrl) {
|
|
830
|
+
const modelsProbe = await probeUrl(modelsUrl, { apiKey: authValue, timeoutMs, maxBytes: 256 * 1024 });
|
|
831
|
+
results.models = {
|
|
832
|
+
url: modelsUrl,
|
|
833
|
+
status: modelsProbe.status || 0,
|
|
834
|
+
ok: modelsProbe.ok,
|
|
835
|
+
durationMs: modelsProbe.durationMs || 0
|
|
1066
836
|
};
|
|
1067
|
-
if (options.apiKey) {
|
|
1068
|
-
headers['Authorization'] = `Bearer ${options.apiKey}`;
|
|
1069
|
-
}
|
|
1070
837
|
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
let size = 0;
|
|
1077
|
-
res.on('data', (chunk) => {
|
|
1078
|
-
if (!chunk) return;
|
|
1079
|
-
size += chunk.length;
|
|
1080
|
-
if (size <= maxBytes) {
|
|
1081
|
-
chunks.push(chunk);
|
|
1082
|
-
}
|
|
838
|
+
if (!modelsProbe.ok) {
|
|
839
|
+
issues.push({
|
|
840
|
+
code: 'remote-models-unreachable',
|
|
841
|
+
message: `模型列表探测失败:${modelsProbe.error || '无法连接'}`,
|
|
842
|
+
suggestion: '检查 base_url 是否包含 /v1 或关闭远程探测'
|
|
1083
843
|
});
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
durationMs: Date.now() - start,
|
|
1090
|
-
body
|
|
1091
|
-
});
|
|
844
|
+
} else if (modelsProbe.status === 401 || modelsProbe.status === 403) {
|
|
845
|
+
issues.push({
|
|
846
|
+
code: 'remote-models-auth-failed',
|
|
847
|
+
message: '模型列表鉴权失败(401/403)',
|
|
848
|
+
suggestion: '检查 API Key 或认证方式'
|
|
1092
849
|
});
|
|
1093
|
-
})
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
req.on('error', (err) => {
|
|
1100
|
-
resolve({
|
|
1101
|
-
ok: false,
|
|
1102
|
-
error: err.message || 'request failed',
|
|
1103
|
-
durationMs: Date.now() - start
|
|
1104
|
-
});
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
req.end();
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function probeJsonPost(targetUrl, body, options = {}) {
|
|
1112
|
-
return new Promise((resolve) => {
|
|
1113
|
-
let parsed;
|
|
1114
|
-
try {
|
|
1115
|
-
parsed = new URL(targetUrl);
|
|
1116
|
-
} catch (e) {
|
|
1117
|
-
return resolve({ ok: false, error: 'Invalid URL' });
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
const transport = parsed.protocol === 'https:' ? https : http;
|
|
1121
|
-
const headers = {
|
|
1122
|
-
'User-Agent': 'codexmate-health-check',
|
|
1123
|
-
'Accept': 'application/json',
|
|
1124
|
-
'Content-Type': 'application/json'
|
|
1125
|
-
};
|
|
1126
|
-
if (options.apiKey) {
|
|
1127
|
-
headers['Authorization'] = `Bearer ${options.apiKey}`;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
const payload = JSON.stringify(body || {});
|
|
1131
|
-
headers['Content-Length'] = Buffer.byteLength(payload);
|
|
1132
|
-
|
|
1133
|
-
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
1134
|
-
const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
|
|
1135
|
-
const start = Date.now();
|
|
1136
|
-
const req = transport.request(parsed, { method: 'POST', headers }, (res) => {
|
|
1137
|
-
const chunks = [];
|
|
1138
|
-
let size = 0;
|
|
1139
|
-
res.on('data', (chunk) => {
|
|
1140
|
-
if (!chunk) return;
|
|
1141
|
-
size += chunk.length;
|
|
1142
|
-
if (size <= maxBytes) {
|
|
1143
|
-
chunks.push(chunk);
|
|
1144
|
-
}
|
|
1145
|
-
});
|
|
1146
|
-
res.on('end', () => {
|
|
1147
|
-
const bodyText = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
1148
|
-
resolve({
|
|
1149
|
-
ok: true,
|
|
1150
|
-
status: res.statusCode || 0,
|
|
1151
|
-
durationMs: Date.now() - start,
|
|
1152
|
-
body: bodyText
|
|
1153
|
-
});
|
|
1154
|
-
});
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
req.setTimeout(timeoutMs, () => {
|
|
1158
|
-
req.destroy(new Error('timeout'));
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
req.on('error', (err) => {
|
|
1162
|
-
resolve({
|
|
1163
|
-
ok: false,
|
|
1164
|
-
error: err.message || 'request failed',
|
|
1165
|
-
durationMs: Date.now() - start
|
|
1166
|
-
});
|
|
1167
|
-
});
|
|
1168
|
-
|
|
1169
|
-
req.write(payload);
|
|
1170
|
-
req.end();
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function extractModelIds(payload) {
|
|
1175
|
-
const ids = [];
|
|
1176
|
-
const pushValue = (value) => {
|
|
1177
|
-
if (typeof value === 'string' && value.trim()) {
|
|
1178
|
-
ids.push(value.trim());
|
|
1179
|
-
}
|
|
1180
|
-
};
|
|
1181
|
-
|
|
1182
|
-
if (!payload) return ids;
|
|
1183
|
-
|
|
1184
|
-
if (Array.isArray(payload)) {
|
|
1185
|
-
for (const item of payload) {
|
|
1186
|
-
if (item && typeof item === 'object') {
|
|
1187
|
-
pushValue(item.id);
|
|
1188
|
-
pushValue(item.model);
|
|
1189
|
-
pushValue(item.name);
|
|
1190
|
-
} else {
|
|
1191
|
-
pushValue(item);
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
return ids;
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
if (Array.isArray(payload.data)) {
|
|
1198
|
-
for (const item of payload.data) {
|
|
1199
|
-
if (item && typeof item === 'object') {
|
|
1200
|
-
pushValue(item.id);
|
|
1201
|
-
pushValue(item.model);
|
|
1202
|
-
pushValue(item.name);
|
|
1203
|
-
} else {
|
|
1204
|
-
pushValue(item);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
if (Array.isArray(payload.models)) {
|
|
1210
|
-
for (const item of payload.models) {
|
|
1211
|
-
if (item && typeof item === 'object') {
|
|
1212
|
-
pushValue(item.id);
|
|
1213
|
-
pushValue(item.model);
|
|
1214
|
-
pushValue(item.name);
|
|
1215
|
-
} else {
|
|
1216
|
-
pushValue(item);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
return ids;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
async function runRemoteHealthCheck(provider, modelName, options = {}) {
|
|
1225
|
-
const issues = [];
|
|
1226
|
-
const results = {};
|
|
1227
|
-
const baseUrl = normalizeBaseUrl(provider && provider.base_url ? provider.base_url : '');
|
|
1228
|
-
if (!baseUrl) {
|
|
1229
|
-
issues.push({
|
|
1230
|
-
code: 'remote-skip-base-url',
|
|
1231
|
-
message: '无法进行远程探测:base_url 为空',
|
|
1232
|
-
suggestion: '补全 base_url 或关闭远程探测'
|
|
1233
|
-
});
|
|
1234
|
-
return { issues, results };
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
const requiresAuth = provider && provider.requires_openai_auth !== false;
|
|
1238
|
-
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
1239
|
-
? provider.preferred_auth_method.trim()
|
|
1240
|
-
: '';
|
|
1241
|
-
const authValue = requiresAuth ? apiKey : (apiKey || '');
|
|
1242
|
-
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
1243
|
-
|
|
1244
|
-
const baseProbe = await probeUrl(baseUrl, { apiKey: authValue, timeoutMs });
|
|
1245
|
-
results.base = {
|
|
1246
|
-
url: baseUrl,
|
|
1247
|
-
status: baseProbe.status || 0,
|
|
1248
|
-
ok: baseProbe.ok,
|
|
1249
|
-
durationMs: baseProbe.durationMs || 0
|
|
1250
|
-
};
|
|
1251
|
-
|
|
1252
|
-
if (!baseProbe.ok) {
|
|
1253
|
-
issues.push({
|
|
1254
|
-
code: 'remote-unreachable',
|
|
1255
|
-
message: `远程探测失败:${baseProbe.error || '无法连接'}`,
|
|
1256
|
-
suggestion: '检查网络与 base_url 可达性'
|
|
1257
|
-
});
|
|
1258
|
-
return { issues, results };
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
|
-
if (baseProbe.status === 401 || baseProbe.status === 403) {
|
|
1262
|
-
issues.push({
|
|
1263
|
-
code: 'remote-auth-failed',
|
|
1264
|
-
message: '远程探测鉴权失败(401/403)',
|
|
1265
|
-
suggestion: '检查 API Key 或认证方式'
|
|
1266
|
-
});
|
|
1267
|
-
} else if (baseProbe.status >= 400) {
|
|
1268
|
-
issues.push({
|
|
1269
|
-
code: 'remote-http-error',
|
|
1270
|
-
message: `远程探测返回异常状态: ${baseProbe.status}`,
|
|
1271
|
-
suggestion: '检查 base_url 是否正确'
|
|
1272
|
-
});
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
const modelsUrl = buildModelsProbeUrl(baseUrl);
|
|
1276
|
-
if (modelsUrl) {
|
|
1277
|
-
const modelsProbe = await probeUrl(modelsUrl, { apiKey: authValue, timeoutMs, maxBytes: 256 * 1024 });
|
|
1278
|
-
results.models = {
|
|
1279
|
-
url: modelsUrl,
|
|
1280
|
-
status: modelsProbe.status || 0,
|
|
1281
|
-
ok: modelsProbe.ok,
|
|
1282
|
-
durationMs: modelsProbe.durationMs || 0
|
|
1283
|
-
};
|
|
1284
|
-
|
|
1285
|
-
if (!modelsProbe.ok) {
|
|
1286
|
-
issues.push({
|
|
1287
|
-
code: 'remote-models-unreachable',
|
|
1288
|
-
message: `模型列表探测失败:${modelsProbe.error || '无法连接'}`,
|
|
1289
|
-
suggestion: '检查 base_url 是否包含 /v1 或关闭远程探测'
|
|
1290
|
-
});
|
|
1291
|
-
} else if (modelsProbe.status === 401 || modelsProbe.status === 403) {
|
|
1292
|
-
issues.push({
|
|
1293
|
-
code: 'remote-models-auth-failed',
|
|
1294
|
-
message: '模型列表鉴权失败(401/403)',
|
|
1295
|
-
suggestion: '检查 API Key 或认证方式'
|
|
1296
|
-
});
|
|
1297
|
-
} else if (modelsProbe.status >= 400) {
|
|
1298
|
-
issues.push({
|
|
1299
|
-
code: 'remote-models-http-error',
|
|
1300
|
-
message: `模型列表返回异常状态: ${modelsProbe.status}`,
|
|
1301
|
-
suggestion: '确认 /v1/models 可用'
|
|
850
|
+
} else if (modelsProbe.status >= 400) {
|
|
851
|
+
issues.push({
|
|
852
|
+
code: 'remote-models-http-error',
|
|
853
|
+
message: `模型列表返回异常状态: ${modelsProbe.status}`,
|
|
854
|
+
suggestion: '确认 /v1/models 可用'
|
|
1302
855
|
});
|
|
1303
856
|
} else {
|
|
1304
857
|
let payload = null;
|
|
@@ -1558,84 +1111,6 @@ async function buildConfigHealthReport(params = {}) {
|
|
|
1558
1111
|
};
|
|
1559
1112
|
}
|
|
1560
1113
|
|
|
1561
|
-
function formatTimestampForFileName(value) {
|
|
1562
|
-
const date = value ? new Date(value) : new Date();
|
|
1563
|
-
const normalized = Number.isNaN(date.getTime()) ? new Date() : date;
|
|
1564
|
-
const pad = (num) => String(num).padStart(2, '0');
|
|
1565
|
-
return [
|
|
1566
|
-
normalized.getFullYear(),
|
|
1567
|
-
pad(normalized.getMonth() + 1),
|
|
1568
|
-
pad(normalized.getDate()),
|
|
1569
|
-
'-',
|
|
1570
|
-
pad(normalized.getHours()),
|
|
1571
|
-
pad(normalized.getMinutes()),
|
|
1572
|
-
pad(normalized.getSeconds())
|
|
1573
|
-
].join('');
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
function toIsoTime(value, fallback = '') {
|
|
1577
|
-
if (value === undefined || value === null || value === '') {
|
|
1578
|
-
return fallback;
|
|
1579
|
-
}
|
|
1580
|
-
const date = new Date(value);
|
|
1581
|
-
if (Number.isNaN(date.getTime())) {
|
|
1582
|
-
return fallback;
|
|
1583
|
-
}
|
|
1584
|
-
return date.toISOString();
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
function updateLatestIso(currentIso, candidate) {
|
|
1588
|
-
const currentTime = Date.parse(currentIso || '') || 0;
|
|
1589
|
-
const candidateIso = toIsoTime(candidate, '');
|
|
1590
|
-
const candidateTime = Date.parse(candidateIso || '') || 0;
|
|
1591
|
-
if (!candidateTime) {
|
|
1592
|
-
return currentIso;
|
|
1593
|
-
}
|
|
1594
|
-
return candidateTime > currentTime ? candidateIso : currentIso;
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
function truncateText(text, maxLength = 90) {
|
|
1598
|
-
if (!text) return '';
|
|
1599
|
-
const normalized = String(text).replace(/\s+/g, ' ').trim();
|
|
1600
|
-
if (normalized.length <= maxLength) return normalized;
|
|
1601
|
-
return normalized.slice(0, maxLength - 1) + '…';
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
function extractMessageText(content) {
|
|
1605
|
-
if (typeof content === 'string') {
|
|
1606
|
-
return content.trim();
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
if (Array.isArray(content)) {
|
|
1610
|
-
const parts = content
|
|
1611
|
-
.map(item => extractMessageText(item))
|
|
1612
|
-
.filter(Boolean);
|
|
1613
|
-
return parts.join('\n').trim();
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
if (!content || typeof content !== 'object') {
|
|
1617
|
-
return '';
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
if (typeof content.text === 'string') {
|
|
1621
|
-
return content.text.trim();
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
if (typeof content.value === 'string') {
|
|
1625
|
-
return content.value.trim();
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
if (content.content !== undefined) {
|
|
1629
|
-
return extractMessageText(content.content);
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
if (typeof content.output === 'string') {
|
|
1633
|
-
return content.output.trim();
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
return '';
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
1114
|
function buildDefaultConfigContent(initializedAt) {
|
|
1640
1115
|
const defaultModel = DEFAULT_MODELS[0] || 'gpt-4';
|
|
1641
1116
|
return `${CODEXMATE_MANAGED_MARKER}
|
|
@@ -1712,6 +1187,22 @@ function normalizeTopLevelConfigWithTemplate(template, selectedProvider, selecte
|
|
|
1712
1187
|
return content;
|
|
1713
1188
|
}
|
|
1714
1189
|
|
|
1190
|
+
function applyServiceTierToTemplate(template, serviceTier) {
|
|
1191
|
+
let content = typeof template === 'string' ? template : '';
|
|
1192
|
+
const tier = typeof serviceTier === 'string' ? serviceTier.trim().toLowerCase() : '';
|
|
1193
|
+
if (!tier) {
|
|
1194
|
+
return content;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
content = content.replace(/^\s*service_tier\s*=\s*["'][^"']*["']\s*\n?/gmi, '');
|
|
1198
|
+
if (tier !== 'fast') {
|
|
1199
|
+
return content;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
content = content.replace(/^\s*\n*/, '');
|
|
1203
|
+
return `service_tier = "fast"\n${content}`;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1715
1206
|
function getConfigTemplate(params = {}) {
|
|
1716
1207
|
let content = EMPTY_CONFIG_FALLBACK_TEMPLATE;
|
|
1717
1208
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
@@ -1724,8 +1215,12 @@ function getConfigTemplate(params = {}) {
|
|
|
1724
1215
|
}
|
|
1725
1216
|
const selectedProvider = params.provider || '';
|
|
1726
1217
|
const selectedModel = params.model || '';
|
|
1218
|
+
let template = normalizeTopLevelConfigWithTemplate(content, selectedProvider, selectedModel);
|
|
1219
|
+
if (typeof params.serviceTier === 'string') {
|
|
1220
|
+
template = applyServiceTierToTemplate(template, params.serviceTier);
|
|
1221
|
+
}
|
|
1727
1222
|
return {
|
|
1728
|
-
template
|
|
1223
|
+
template
|
|
1729
1224
|
};
|
|
1730
1225
|
}
|
|
1731
1226
|
|
|
@@ -2022,17 +1517,6 @@ function parseJsonlHeadRecords(filePath, maxBytes = SESSION_SUMMARY_READ_BYTES)
|
|
|
2022
1517
|
return parseJsonlContent(headText);
|
|
2023
1518
|
}
|
|
2024
1519
|
|
|
2025
|
-
function normalizeRole(value) {
|
|
2026
|
-
if (typeof value !== 'string') {
|
|
2027
|
-
return '';
|
|
2028
|
-
}
|
|
2029
|
-
const role = value.trim().toLowerCase();
|
|
2030
|
-
if (role === 'assistant' || role === 'user' || role === 'system') {
|
|
2031
|
-
return role;
|
|
2032
|
-
}
|
|
2033
|
-
return '';
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
1520
|
function isBootstrapLikeText(text) {
|
|
2037
1521
|
if (!text || typeof text !== 'string') {
|
|
2038
1522
|
return false;
|
|
@@ -2046,17 +1530,17 @@ function isBootstrapLikeText(text) {
|
|
|
2046
1530
|
return BOOTSTRAP_TEXT_MARKERS.some(marker => normalized.includes(marker));
|
|
2047
1531
|
}
|
|
2048
1532
|
|
|
2049
|
-
function removeLeadingSystemMessage(messages) {
|
|
2050
|
-
if (!Array.isArray(messages) || messages.length === 0) {
|
|
2051
|
-
return [];
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
let startIndex =
|
|
2055
|
-
while (startIndex < messages.length) {
|
|
2056
|
-
const item = messages[startIndex];
|
|
2057
|
-
const role = item ? normalizeRole(item.role) : '';
|
|
2058
|
-
const text = item && typeof item.text === 'string' ? item.text : '';
|
|
2059
|
-
const isSystemRole = role === 'system';
|
|
1533
|
+
function removeLeadingSystemMessage(messages) {
|
|
1534
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
1535
|
+
return [];
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
let startIndex = 0;
|
|
1539
|
+
while (startIndex < messages.length) {
|
|
1540
|
+
const item = messages[startIndex];
|
|
1541
|
+
const role = item ? normalizeRole(item.role) : '';
|
|
1542
|
+
const text = item && typeof item.text === 'string' ? item.text : '';
|
|
1543
|
+
const isSystemRole = role === 'system';
|
|
2060
1544
|
const isBootstrapText = isBootstrapLikeText(text);
|
|
2061
1545
|
if (!item || isSystemRole || isBootstrapText) {
|
|
2062
1546
|
startIndex += 1;
|
|
@@ -2143,16 +1627,85 @@ function matchesSessionPathFilter(session, normalizedFilter) {
|
|
|
2143
1627
|
return cwd.includes(normalizedFilter);
|
|
2144
1628
|
}
|
|
2145
1629
|
|
|
2146
|
-
function normalizeQueryTokens(query) {
|
|
2147
|
-
if (typeof query !== 'string') {
|
|
2148
|
-
return [];
|
|
2149
|
-
}
|
|
2150
|
-
return query
|
|
2151
|
-
.split(/\s+/)
|
|
2152
|
-
.map(item => item.trim())
|
|
2153
|
-
.map(item => item.toLowerCase())
|
|
2154
|
-
.filter(Boolean);
|
|
2155
|
-
}
|
|
1630
|
+
function normalizeQueryTokens(query) {
|
|
1631
|
+
if (typeof query !== 'string') {
|
|
1632
|
+
return [];
|
|
1633
|
+
}
|
|
1634
|
+
return query
|
|
1635
|
+
.split(/\s+/)
|
|
1636
|
+
.map(item => item.trim())
|
|
1637
|
+
.map(item => item.toLowerCase())
|
|
1638
|
+
.filter(Boolean);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function expandSessionQueryTokens(tokens) {
|
|
1642
|
+
const base = Array.isArray(tokens) ? tokens.map(t => String(t || '').toLowerCase()).filter(Boolean) : [];
|
|
1643
|
+
const result = [];
|
|
1644
|
+
const seen = new Set();
|
|
1645
|
+
let hasClaudeAlias = false;
|
|
1646
|
+
let hasDaudeAlias = false;
|
|
1647
|
+
|
|
1648
|
+
for (const token of base) {
|
|
1649
|
+
if (/^claude[-_ ]?code$/.test(token) || token === 'claudecode') {
|
|
1650
|
+
hasClaudeAlias = true;
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
if (/^daude[-_ ]?code$/.test(token) || token === 'daudecode') {
|
|
1654
|
+
hasDaudeAlias = true;
|
|
1655
|
+
continue;
|
|
1656
|
+
}
|
|
1657
|
+
if (!seen.has(token)) {
|
|
1658
|
+
seen.add(token);
|
|
1659
|
+
result.push(token);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
const push = (token) => {
|
|
1664
|
+
const normalized = String(token || '').toLowerCase();
|
|
1665
|
+
if (!normalized || seen.has(normalized)) return;
|
|
1666
|
+
seen.add(normalized);
|
|
1667
|
+
result.push(normalized);
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
if (hasClaudeAlias) {
|
|
1671
|
+
push('claude');
|
|
1672
|
+
push('code');
|
|
1673
|
+
}
|
|
1674
|
+
if (hasDaudeAlias) {
|
|
1675
|
+
push('daude');
|
|
1676
|
+
push('code');
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
return result;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function normalizeKeywords(value) {
|
|
1683
|
+
if (!Array.isArray(value)) {
|
|
1684
|
+
return [];
|
|
1685
|
+
}
|
|
1686
|
+
const seen = new Set();
|
|
1687
|
+
const result = [];
|
|
1688
|
+
for (const item of value) {
|
|
1689
|
+
const normalized = typeof item === 'string' ? item.trim() : String(item || '').trim();
|
|
1690
|
+
if (!normalized) continue;
|
|
1691
|
+
const lower = normalized.toLowerCase();
|
|
1692
|
+
if (seen.has(lower)) continue;
|
|
1693
|
+
seen.add(lower);
|
|
1694
|
+
result.push(normalized);
|
|
1695
|
+
}
|
|
1696
|
+
return result;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
function normalizeCapabilities(value) {
|
|
1700
|
+
const result = {};
|
|
1701
|
+
if (!value || typeof value !== 'object') {
|
|
1702
|
+
return result;
|
|
1703
|
+
}
|
|
1704
|
+
if (value.code === true) {
|
|
1705
|
+
result.code = true;
|
|
1706
|
+
}
|
|
1707
|
+
return result;
|
|
1708
|
+
}
|
|
2156
1709
|
|
|
2157
1710
|
function normalizeQueryMode(mode) {
|
|
2158
1711
|
return mode === 'or' ? 'or' : 'and';
|
|
@@ -2187,18 +1740,22 @@ function matchTokensInText(text, tokens, mode = 'and') {
|
|
|
2187
1740
|
return tokens.every(token => haystack.includes(token));
|
|
2188
1741
|
}
|
|
2189
1742
|
|
|
2190
|
-
function buildSessionSummaryText(session) {
|
|
2191
|
-
if (!session) {
|
|
2192
|
-
return '';
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
session.
|
|
2198
|
-
session.
|
|
2199
|
-
session.
|
|
2200
|
-
|
|
2201
|
-
|
|
1743
|
+
function buildSessionSummaryText(session) {
|
|
1744
|
+
if (!session) {
|
|
1745
|
+
return '';
|
|
1746
|
+
}
|
|
1747
|
+
const keywords = Array.isArray(session.keywords) ? session.keywords.join(' ') : '';
|
|
1748
|
+
const provider = typeof session.provider === 'string' ? session.provider : '';
|
|
1749
|
+
return [
|
|
1750
|
+
session.title,
|
|
1751
|
+
session.sessionId,
|
|
1752
|
+
session.cwd,
|
|
1753
|
+
session.filePath,
|
|
1754
|
+
session.sourceLabel,
|
|
1755
|
+
provider,
|
|
1756
|
+
keywords
|
|
1757
|
+
].filter(Boolean).join(' ');
|
|
1758
|
+
}
|
|
2202
1759
|
|
|
2203
1760
|
function extractMessageFromRecord(record, source) {
|
|
2204
1761
|
if (!record) {
|
|
@@ -2308,39 +1865,39 @@ function applySessionQueryFilter(sessions, options = {}) {
|
|
|
2308
1865
|
? Math.max(1024, Number(options.contentScanBytes))
|
|
2309
1866
|
: SESSION_CONTENT_READ_BYTES;
|
|
2310
1867
|
|
|
2311
|
-
let scanned = 0;
|
|
2312
|
-
const results = [];
|
|
2313
|
-
|
|
2314
|
-
for (const session of sessions) {
|
|
2315
|
-
if (scope === 'content' && scanned >= contentScanLimit) {
|
|
1868
|
+
let scanned = 0;
|
|
1869
|
+
const results = [];
|
|
1870
|
+
|
|
1871
|
+
for (const session of sessions) {
|
|
1872
|
+
if (scope === 'content' && scanned >= contentScanLimit) {
|
|
2316
1873
|
break;
|
|
2317
1874
|
}
|
|
1875
|
+
|
|
1876
|
+
const summaryText = buildSessionSummaryText(session);
|
|
1877
|
+
const summaryHit = scope !== 'content' && matchTokensInText(summaryText, tokens, mode);
|
|
1878
|
+
let contentHit = false;
|
|
1879
|
+
let contentInfo = null;
|
|
1880
|
+
|
|
1881
|
+
const shouldScanContent = scope === 'content' || scope === 'all' || !summaryHit;
|
|
1882
|
+
if (shouldScanContent && scanned < contentScanLimit) {
|
|
1883
|
+
scanned += 1;
|
|
1884
|
+
contentInfo = scanSessionContentForQuery(session, tokens, {
|
|
1885
|
+
mode,
|
|
1886
|
+
roleFilter,
|
|
1887
|
+
maxBytes: contentScanBytes,
|
|
1888
|
+
maxMatches: 1,
|
|
1889
|
+
snippetLimit: 2
|
|
1890
|
+
});
|
|
1891
|
+
contentHit = contentInfo.hit;
|
|
1892
|
+
}
|
|
2318
1893
|
|
|
2319
|
-
const
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
scanned += 1;
|
|
2327
|
-
contentInfo = scanSessionContentForQuery(session, tokens, {
|
|
2328
|
-
mode,
|
|
2329
|
-
roleFilter,
|
|
2330
|
-
maxBytes: contentScanBytes,
|
|
2331
|
-
maxMatches: 1,
|
|
2332
|
-
snippetLimit: 2
|
|
2333
|
-
});
|
|
2334
|
-
contentHit = contentInfo.hit;
|
|
2335
|
-
}
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
const hit = scope === 'summary'
|
|
2339
|
-
? summaryHit
|
|
2340
|
-
: (scope === 'content' ? contentHit : (summaryHit || contentHit));
|
|
2341
|
-
if (!hit) {
|
|
2342
|
-
continue;
|
|
2343
|
-
}
|
|
1894
|
+
const hit = scope === 'summary'
|
|
1895
|
+
? summaryHit
|
|
1896
|
+
: (scope === 'content' ? contentHit : (summaryHit || contentHit));
|
|
1897
|
+
|
|
1898
|
+
if (!hit) {
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
2344
1901
|
|
|
2345
1902
|
const matchInfo = contentInfo && contentInfo.hit
|
|
2346
1903
|
? contentInfo
|
|
@@ -2515,23 +2072,26 @@ function parseCodexSessionSummary(filePath) {
|
|
|
2515
2072
|
}
|
|
2516
2073
|
}
|
|
2517
2074
|
|
|
2518
|
-
messageCount = Math.max(0, messageCount);
|
|
2519
|
-
|
|
2520
|
-
return {
|
|
2521
|
-
source: 'codex',
|
|
2522
|
-
sourceLabel: 'Codex',
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2075
|
+
messageCount = Math.max(0, messageCount);
|
|
2076
|
+
|
|
2077
|
+
return {
|
|
2078
|
+
source: 'codex',
|
|
2079
|
+
sourceLabel: 'Codex',
|
|
2080
|
+
provider: 'codex',
|
|
2081
|
+
sessionId,
|
|
2082
|
+
title: firstPrompt || sessionId,
|
|
2083
|
+
cwd,
|
|
2084
|
+
createdAt,
|
|
2085
|
+
updatedAt,
|
|
2086
|
+
messageCount,
|
|
2087
|
+
filePath,
|
|
2088
|
+
keywords: [],
|
|
2089
|
+
capabilities: {}
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function parseClaudeSessionSummary(filePath) {
|
|
2094
|
+
const records = parseJsonlHeadRecords(filePath);
|
|
2535
2095
|
if (records.length === 0) {
|
|
2536
2096
|
return null;
|
|
2537
2097
|
}
|
|
@@ -2601,20 +2161,23 @@ function parseClaudeSessionSummary(filePath) {
|
|
|
2601
2161
|
}
|
|
2602
2162
|
}
|
|
2603
2163
|
|
|
2604
|
-
messageCount = Math.max(0, messageCount);
|
|
2605
|
-
|
|
2606
|
-
return {
|
|
2607
|
-
source: 'claude',
|
|
2608
|
-
sourceLabel: 'Claude Code',
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2164
|
+
messageCount = Math.max(0, messageCount);
|
|
2165
|
+
|
|
2166
|
+
return {
|
|
2167
|
+
source: 'claude',
|
|
2168
|
+
sourceLabel: 'Claude Code',
|
|
2169
|
+
provider: 'claude',
|
|
2170
|
+
sessionId,
|
|
2171
|
+
title: firstPrompt || sessionId,
|
|
2172
|
+
cwd,
|
|
2173
|
+
createdAt,
|
|
2174
|
+
updatedAt,
|
|
2175
|
+
messageCount,
|
|
2176
|
+
filePath,
|
|
2177
|
+
keywords: [],
|
|
2178
|
+
capabilities: { code: true }
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2618
2181
|
|
|
2619
2182
|
function listCodexSessions(limit, options = {}) {
|
|
2620
2183
|
const codexSessionsDir = getCodexSessionsDir();
|
|
@@ -2715,12 +2278,12 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
2715
2278
|
let title = truncateText(entry.summary || entry.firstPrompt || sessionId, 120);
|
|
2716
2279
|
let messageCount = Number.isFinite(entry.messageCount) ? Math.max(0, entry.messageCount - 1) : 0;
|
|
2717
2280
|
|
|
2718
|
-
const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
|
|
2719
|
-
if (quickRecords.length > 0) {
|
|
2720
|
-
const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
|
|
2721
|
-
if (filteredCount > 0 || messageCount === 0) {
|
|
2722
|
-
messageCount = filteredCount;
|
|
2723
|
-
}
|
|
2281
|
+
const quickRecords = parseJsonlHeadRecords(filePath, SESSION_TITLE_READ_BYTES);
|
|
2282
|
+
if (quickRecords.length > 0) {
|
|
2283
|
+
const filteredCount = countConversationMessagesInRecords(quickRecords, 'claude');
|
|
2284
|
+
if (filteredCount > 0 || messageCount === 0) {
|
|
2285
|
+
messageCount = filteredCount;
|
|
2286
|
+
}
|
|
2724
2287
|
|
|
2725
2288
|
const quickMessages = [];
|
|
2726
2289
|
for (const record of quickRecords) {
|
|
@@ -2729,29 +2292,38 @@ function listClaudeSessions(limit, options = {}) {
|
|
|
2729
2292
|
const content = record.message ? record.message.content : '';
|
|
2730
2293
|
quickMessages.push({ role, text: extractMessageText(content) });
|
|
2731
2294
|
}
|
|
2732
|
-
}
|
|
2733
|
-
const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
|
|
2734
|
-
const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
|
|
2735
|
-
if (firstUser) {
|
|
2736
|
-
title = truncateText(firstUser.text, 120);
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2295
|
+
}
|
|
2296
|
+
const filteredQuickMessages = removeLeadingSystemMessage(quickMessages);
|
|
2297
|
+
const firstUser = filteredQuickMessages.find(item => item.role === 'user' && item.text);
|
|
2298
|
+
if (firstUser) {
|
|
2299
|
+
title = truncateText(firstUser.text, 120);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
const provider = typeof entry.provider === 'string' && entry.provider.trim()
|
|
2304
|
+
? entry.provider.trim()
|
|
2305
|
+
: 'claude';
|
|
2306
|
+
const keywords = normalizeKeywords(entry.keywords);
|
|
2307
|
+
const capabilities = normalizeCapabilities(entry.capabilities);
|
|
2308
|
+
|
|
2309
|
+
sessions.push({
|
|
2310
|
+
source: 'claude',
|
|
2311
|
+
sourceLabel: 'Claude Code',
|
|
2312
|
+
provider,
|
|
2313
|
+
sessionId,
|
|
2314
|
+
title,
|
|
2315
|
+
cwd: entry.projectPath || index.originalPath || '',
|
|
2316
|
+
createdAt,
|
|
2317
|
+
updatedAt,
|
|
2318
|
+
messageCount,
|
|
2319
|
+
filePath,
|
|
2320
|
+
keywords,
|
|
2321
|
+
capabilities
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2324
|
+
if (sessions.length >= targetCount) {
|
|
2325
|
+
break;
|
|
2326
|
+
}
|
|
2755
2327
|
}
|
|
2756
2328
|
|
|
2757
2329
|
if (sessions.length >= targetCount) {
|
|
@@ -2784,15 +2356,15 @@ function listAllSessions(params = {}) {
|
|
|
2784
2356
|
const source = params.source === 'codex' || params.source === 'claude'
|
|
2785
2357
|
? params.source
|
|
2786
2358
|
: 'all';
|
|
2787
|
-
const rawLimit = Number(params.limit);
|
|
2788
|
-
const limit = Number.isFinite(rawLimit)
|
|
2789
|
-
? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
|
|
2790
|
-
: 120;
|
|
2791
|
-
const forceRefresh = !!params.forceRefresh;
|
|
2792
|
-
const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
|
|
2793
|
-
const hasPathFilter = !!normalizedPathFilter;
|
|
2794
|
-
const queryTokens = normalizeQueryTokens(params.query);
|
|
2795
|
-
const hasQuery = queryTokens.length > 0;
|
|
2359
|
+
const rawLimit = Number(params.limit);
|
|
2360
|
+
const limit = Number.isFinite(rawLimit)
|
|
2361
|
+
? Math.max(1, Math.min(rawLimit, MAX_SESSION_LIST_SIZE))
|
|
2362
|
+
: 120;
|
|
2363
|
+
const forceRefresh = !!params.forceRefresh;
|
|
2364
|
+
const normalizedPathFilter = normalizeSessionPathFilter(params.pathFilter);
|
|
2365
|
+
const hasPathFilter = !!normalizedPathFilter;
|
|
2366
|
+
const queryTokens = expandSessionQueryTokens(normalizeQueryTokens(params.query));
|
|
2367
|
+
const hasQuery = queryTokens.length > 0;
|
|
2796
2368
|
const cacheKey = hasQuery ? '' : `${source}:${limit}:${normalizedPathFilter}`;
|
|
2797
2369
|
if (!hasQuery) {
|
|
2798
2370
|
const cached = getSessionListCache(cacheKey, forceRefresh);
|
|
@@ -2809,16 +2381,16 @@ function listAllSessions(params = {}) {
|
|
|
2809
2381
|
: {};
|
|
2810
2382
|
|
|
2811
2383
|
let sessions = [];
|
|
2812
|
-
if (source === 'all' || source === 'codex') {
|
|
2813
|
-
sessions = sessions.concat(listCodexSessions(limit, scanOptions));
|
|
2814
|
-
}
|
|
2815
|
-
if (source === 'all' || source === 'claude') {
|
|
2816
|
-
sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
if (hasPathFilter) {
|
|
2820
|
-
sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
|
|
2821
|
-
}
|
|
2384
|
+
if (source === 'all' || source === 'codex') {
|
|
2385
|
+
sessions = sessions.concat(listCodexSessions(limit, scanOptions));
|
|
2386
|
+
}
|
|
2387
|
+
if (source === 'all' || source === 'claude') {
|
|
2388
|
+
sessions = sessions.concat(listClaudeSessions(limit, scanOptions));
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
if (hasPathFilter) {
|
|
2392
|
+
sessions = sessions.filter(item => matchesSessionPathFilter(item, normalizedPathFilter));
|
|
2393
|
+
}
|
|
2822
2394
|
|
|
2823
2395
|
let result = sessions;
|
|
2824
2396
|
if (hasQuery) {
|
|
@@ -2905,15 +2477,15 @@ function resolveSessionFilePath(source, filePath, sessionId) {
|
|
|
2905
2477
|
}
|
|
2906
2478
|
}
|
|
2907
2479
|
|
|
2908
|
-
if (typeof sessionId === 'string' && sessionId.trim()) {
|
|
2909
|
-
const targetId = sessionId.trim().toLowerCase();
|
|
2910
|
-
const files = collectJsonlFiles(root, 5000);
|
|
2911
|
-
const matchedFile = files.find(item => path.basename(item).toLowerCase()
|
|
2912
|
-
if (matchedFile && fs.existsSync(matchedFile)) {
|
|
2913
|
-
return matchedFile;
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2480
|
+
if (typeof sessionId === 'string' && sessionId.trim()) {
|
|
2481
|
+
const targetId = sessionId.trim().toLowerCase();
|
|
2482
|
+
const files = collectJsonlFiles(root, 5000);
|
|
2483
|
+
const matchedFile = files.find(item => path.basename(item, '.jsonl').toLowerCase() === targetId);
|
|
2484
|
+
if (matchedFile && fs.existsSync(matchedFile)) {
|
|
2485
|
+
return matchedFile;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2917
2489
|
return '';
|
|
2918
2490
|
}
|
|
2919
2491
|
|
|
@@ -3179,7 +2751,7 @@ async function cloneCodexSession(params = {}) {
|
|
|
3179
2751
|
};
|
|
3180
2752
|
}
|
|
3181
2753
|
|
|
3182
|
-
function buildSessionMarkdown(payload) {
|
|
2754
|
+
function buildSessionMarkdown(payload) {
|
|
3183
2755
|
const lines = [
|
|
3184
2756
|
'# AI Session Export',
|
|
3185
2757
|
'',
|
|
@@ -3208,75 +2780,37 @@ function buildSessionMarkdown(payload) {
|
|
|
3208
2780
|
lines.push('');
|
|
3209
2781
|
});
|
|
3210
2782
|
|
|
3211
|
-
return lines.join('\n');
|
|
3212
|
-
}
|
|
3213
|
-
|
|
3214
|
-
function buildSessionPlainText(messages) {
|
|
3215
|
-
if (!Array.isArray(messages) || messages.length === 0) {
|
|
3216
|
-
return '';
|
|
3217
|
-
}
|
|
3218
|
-
|
|
3219
|
-
const lines = [];
|
|
3220
|
-
messages.forEach((message) => {
|
|
3221
|
-
const role = normalizeRole(message && message.role) || 'unknown';
|
|
3222
|
-
const text = message && typeof message.text === 'string' ? message.text : '';
|
|
3223
|
-
lines.push(role);
|
|
3224
|
-
lines.push(text);
|
|
3225
|
-
lines.push('');
|
|
3226
|
-
});
|
|
3227
|
-
|
|
3228
|
-
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
3229
|
-
lines.pop();
|
|
3230
|
-
}
|
|
3231
|
-
|
|
3232
|
-
return lines.join('\n');
|
|
3233
|
-
}
|
|
3234
|
-
|
|
3235
|
-
function
|
|
3236
|
-
if (
|
|
3237
|
-
return
|
|
3238
|
-
}
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
if (!trimmed) {
|
|
3243
|
-
return null;
|
|
3244
|
-
}
|
|
3245
|
-
const lower = trimmed.toLowerCase();
|
|
3246
|
-
if (lower === 'all' || lower === 'infinity' || lower === 'inf') {
|
|
3247
|
-
return Infinity;
|
|
3248
|
-
}
|
|
3249
|
-
const parsed = Number(trimmed);
|
|
3250
|
-
if (Number.isFinite(parsed)) {
|
|
3251
|
-
return parsed;
|
|
3252
|
-
}
|
|
3253
|
-
return null;
|
|
3254
|
-
}
|
|
3255
|
-
|
|
3256
|
-
if (Number.isFinite(value)) {
|
|
3257
|
-
return value;
|
|
3258
|
-
}
|
|
3259
|
-
return null;
|
|
3260
|
-
}
|
|
3261
|
-
|
|
3262
|
-
function resolveMaxMessagesValue(value, fallback) {
|
|
3263
|
-
const parsed = parseMaxMessagesValue(value);
|
|
3264
|
-
if (parsed === null) {
|
|
3265
|
-
return fallback;
|
|
3266
|
-
}
|
|
3267
|
-
if (parsed === Infinity) {
|
|
3268
|
-
return Infinity;
|
|
3269
|
-
}
|
|
3270
|
-
return Math.max(1, Math.floor(parsed));
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3273
|
-
function resolveStateMaxMessages(state) {
|
|
3274
|
-
if (!state || typeof state !== 'object') {
|
|
3275
|
-
return MAX_EXPORT_MESSAGES;
|
|
3276
|
-
}
|
|
3277
|
-
|
|
3278
|
-
return resolveMaxMessagesValue(state.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3279
|
-
}
|
|
2783
|
+
return lines.join('\n');
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
function buildSessionPlainText(messages) {
|
|
2787
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
2788
|
+
return '';
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
const lines = [];
|
|
2792
|
+
messages.forEach((message) => {
|
|
2793
|
+
const role = normalizeRole(message && message.role) || 'unknown';
|
|
2794
|
+
const text = message && typeof message.text === 'string' ? message.text : '';
|
|
2795
|
+
lines.push(role);
|
|
2796
|
+
lines.push(text);
|
|
2797
|
+
lines.push('');
|
|
2798
|
+
});
|
|
2799
|
+
|
|
2800
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
2801
|
+
lines.pop();
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
return lines.join('\n');
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
function resolveStateMaxMessages(state) {
|
|
2808
|
+
if (!state || typeof state !== 'object') {
|
|
2809
|
+
return MAX_EXPORT_MESSAGES;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
return resolveMaxMessagesValue(state.maxMessages, MAX_EXPORT_MESSAGES);
|
|
2813
|
+
}
|
|
3280
2814
|
|
|
3281
2815
|
function canAppendMessage(state) {
|
|
3282
2816
|
const maxMessages = resolveStateMaxMessages(state);
|
|
@@ -3313,7 +2847,7 @@ function extractCodexMessageFromRecord(record, state, lineIndex = -1) {
|
|
|
3313
2847
|
}
|
|
3314
2848
|
}
|
|
3315
2849
|
|
|
3316
|
-
function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
2850
|
+
function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
3317
2851
|
if (record.timestamp) {
|
|
3318
2852
|
state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
|
|
3319
2853
|
}
|
|
@@ -3338,130 +2872,130 @@ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
|
3338
2872
|
recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
|
|
3339
2873
|
});
|
|
3340
2874
|
}
|
|
3341
|
-
}
|
|
3342
|
-
}
|
|
3343
|
-
|
|
3344
|
-
function recordHasCodexMessage(record) {
|
|
3345
|
-
if (!record || record.type !== 'response_item' || !record.payload) {
|
|
3346
|
-
return false;
|
|
3347
|
-
}
|
|
3348
|
-
if (record.payload.type !== 'message') {
|
|
3349
|
-
return false;
|
|
3350
|
-
}
|
|
3351
|
-
const role = normalizeRole(record.payload.role);
|
|
3352
|
-
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
3353
|
-
return false;
|
|
3354
|
-
}
|
|
3355
|
-
const text = extractMessageText(record.payload.content);
|
|
3356
|
-
return !!text;
|
|
3357
|
-
}
|
|
3358
|
-
|
|
3359
|
-
function recordHasClaudeMessage(record) {
|
|
3360
|
-
if (!record) {
|
|
3361
|
-
return false;
|
|
3362
|
-
}
|
|
3363
|
-
const role = normalizeRole(record.type);
|
|
3364
|
-
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
3365
|
-
return false;
|
|
3366
|
-
}
|
|
3367
|
-
const content = record.message ? record.message.content : '';
|
|
3368
|
-
const text = extractMessageText(content);
|
|
3369
|
-
return !!text;
|
|
3370
|
-
}
|
|
3371
|
-
|
|
3372
|
-
function recordHasMessage(record, source) {
|
|
3373
|
-
return source === 'codex'
|
|
3374
|
-
? recordHasCodexMessage(record)
|
|
3375
|
-
: recordHasClaudeMessage(record);
|
|
3376
|
-
}
|
|
3377
|
-
|
|
3378
|
-
function extractMessagesFromRecords(records, source, options = {}) {
|
|
3379
|
-
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3380
|
-
const state = {
|
|
3381
|
-
sessionId: '',
|
|
3382
|
-
cwd: '',
|
|
3383
|
-
updatedAt: '',
|
|
3384
|
-
messages: [],
|
|
3385
|
-
maxMessages,
|
|
3386
|
-
truncated: false
|
|
3387
|
-
};
|
|
3388
|
-
|
|
3389
|
-
for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
|
|
3390
|
-
const record = records[lineIndex];
|
|
3391
|
-
if (source === 'codex') {
|
|
3392
|
-
extractCodexMessageFromRecord(record, state, lineIndex);
|
|
3393
|
-
} else {
|
|
3394
|
-
extractClaudeMessageFromRecord(record, state, lineIndex);
|
|
3395
|
-
}
|
|
3396
|
-
|
|
3397
|
-
if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
|
|
3398
|
-
for (let i = lineIndex + 1; i < records.length; i++) {
|
|
3399
|
-
if (recordHasMessage(records[i], source)) {
|
|
3400
|
-
state.truncated = true;
|
|
3401
|
-
break;
|
|
3402
|
-
}
|
|
3403
|
-
}
|
|
3404
|
-
break;
|
|
3405
|
-
}
|
|
3406
|
-
}
|
|
3407
|
-
|
|
3408
|
-
return state;
|
|
3409
|
-
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
3410
2877
|
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
2878
|
+
function recordHasCodexMessage(record) {
|
|
2879
|
+
if (!record || record.type !== 'response_item' || !record.payload) {
|
|
2880
|
+
return false;
|
|
2881
|
+
}
|
|
2882
|
+
if (record.payload.type !== 'message') {
|
|
2883
|
+
return false;
|
|
2884
|
+
}
|
|
2885
|
+
const role = normalizeRole(record.payload.role);
|
|
2886
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
2887
|
+
return false;
|
|
2888
|
+
}
|
|
2889
|
+
const text = extractMessageText(record.payload.content);
|
|
2890
|
+
return !!text;
|
|
2891
|
+
}
|
|
3421
2892
|
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
2893
|
+
function recordHasClaudeMessage(record) {
|
|
2894
|
+
if (!record) {
|
|
2895
|
+
return false;
|
|
2896
|
+
}
|
|
2897
|
+
const role = normalizeRole(record.type);
|
|
2898
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
2899
|
+
return false;
|
|
2900
|
+
}
|
|
2901
|
+
const content = record.message ? record.message.content : '';
|
|
2902
|
+
const text = extractMessageText(content);
|
|
2903
|
+
return !!text;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
function recordHasMessage(record, source) {
|
|
2907
|
+
return source === 'codex'
|
|
2908
|
+
? recordHasCodexMessage(record)
|
|
2909
|
+
: recordHasClaudeMessage(record);
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
function extractMessagesFromRecords(records, source, options = {}) {
|
|
2913
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
2914
|
+
const state = {
|
|
2915
|
+
sessionId: '',
|
|
2916
|
+
cwd: '',
|
|
2917
|
+
updatedAt: '',
|
|
2918
|
+
messages: [],
|
|
2919
|
+
maxMessages,
|
|
2920
|
+
truncated: false
|
|
2921
|
+
};
|
|
2922
|
+
|
|
2923
|
+
for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
|
|
2924
|
+
const record = records[lineIndex];
|
|
2925
|
+
if (source === 'codex') {
|
|
2926
|
+
extractCodexMessageFromRecord(record, state, lineIndex);
|
|
2927
|
+
} else {
|
|
2928
|
+
extractClaudeMessageFromRecord(record, state, lineIndex);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
|
|
2932
|
+
for (let i = lineIndex + 1; i < records.length; i++) {
|
|
2933
|
+
if (recordHasMessage(records[i], source)) {
|
|
2934
|
+
state.truncated = true;
|
|
2935
|
+
break;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
break;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
return state;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
2946
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
2947
|
+
const state = {
|
|
2948
|
+
sessionId: '',
|
|
2949
|
+
cwd: '',
|
|
2950
|
+
updatedAt: '',
|
|
2951
|
+
messages: [],
|
|
2952
|
+
maxMessages,
|
|
2953
|
+
truncated: false
|
|
2954
|
+
};
|
|
2955
|
+
|
|
2956
|
+
let stream;
|
|
2957
|
+
let rl;
|
|
2958
|
+
try {
|
|
2959
|
+
stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
2960
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
2961
|
+
|
|
2962
|
+
let lineIndex = 0;
|
|
2963
|
+
let limitReached = false;
|
|
2964
|
+
for await (const line of rl) {
|
|
2965
|
+
const currentLineIndex = lineIndex;
|
|
2966
|
+
lineIndex += 1;
|
|
3433
2967
|
|
|
3434
2968
|
const trimmed = line.trim();
|
|
3435
2969
|
if (!trimmed) continue;
|
|
3436
2970
|
|
|
3437
2971
|
let record;
|
|
3438
|
-
try {
|
|
3439
|
-
record = JSON.parse(trimmed);
|
|
3440
|
-
} catch (e) {
|
|
3441
|
-
continue;
|
|
3442
|
-
}
|
|
3443
|
-
|
|
3444
|
-
if (limitReached) {
|
|
3445
|
-
if (recordHasMessage(record, source)) {
|
|
3446
|
-
state.truncated = true;
|
|
3447
|
-
break;
|
|
3448
|
-
}
|
|
3449
|
-
continue;
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
|
-
if (source === 'codex') {
|
|
3453
|
-
extractCodexMessageFromRecord(record, state, currentLineIndex);
|
|
3454
|
-
} else {
|
|
3455
|
-
extractClaudeMessageFromRecord(record, state, currentLineIndex);
|
|
3456
|
-
}
|
|
3457
|
-
|
|
3458
|
-
if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
|
|
3459
|
-
limitReached = true;
|
|
3460
|
-
}
|
|
3461
|
-
}
|
|
3462
|
-
} catch (e) {
|
|
3463
|
-
const fallbackRecords = readJsonlRecords(filePath);
|
|
3464
|
-
return extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
2972
|
+
try {
|
|
2973
|
+
record = JSON.parse(trimmed);
|
|
2974
|
+
} catch (e) {
|
|
2975
|
+
continue;
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
if (limitReached) {
|
|
2979
|
+
if (recordHasMessage(record, source)) {
|
|
2980
|
+
state.truncated = true;
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2983
|
+
continue;
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
if (source === 'codex') {
|
|
2987
|
+
extractCodexMessageFromRecord(record, state, currentLineIndex);
|
|
2988
|
+
} else {
|
|
2989
|
+
extractClaudeMessageFromRecord(record, state, currentLineIndex);
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
|
|
2993
|
+
limitReached = true;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
} catch (e) {
|
|
2997
|
+
const fallbackRecords = readJsonlRecords(filePath);
|
|
2998
|
+
return extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
3465
2999
|
} finally {
|
|
3466
3000
|
if (rl) {
|
|
3467
3001
|
try { rl.close(); } catch (e) {}
|
|
@@ -3474,7 +3008,7 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
|
3474
3008
|
return state;
|
|
3475
3009
|
}
|
|
3476
3010
|
|
|
3477
|
-
async function readSessionDetail(params = {}) {
|
|
3011
|
+
async function readSessionDetail(params = {}) {
|
|
3478
3012
|
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3479
3013
|
if (!source) {
|
|
3480
3014
|
return { error: 'Invalid source' };
|
|
@@ -3501,83 +3035,37 @@ async function readSessionDetail(params = {}) {
|
|
|
3501
3035
|
const startIndex = Math.max(0, allMessages.length - messageLimit);
|
|
3502
3036
|
const clippedMessages = allMessages.slice(startIndex);
|
|
3503
3037
|
|
|
3504
|
-
return {
|
|
3505
|
-
source,
|
|
3506
|
-
sourceLabel,
|
|
3507
|
-
sessionId,
|
|
3038
|
+
return {
|
|
3039
|
+
source,
|
|
3040
|
+
sourceLabel,
|
|
3041
|
+
sessionId,
|
|
3508
3042
|
cwd: extracted.cwd || '',
|
|
3509
3043
|
updatedAt: extracted.updatedAt || '',
|
|
3510
3044
|
totalMessages: allMessages.length,
|
|
3511
3045
|
clipped: allMessages.length > clippedMessages.length,
|
|
3512
3046
|
messageLimit,
|
|
3513
3047
|
messages: clippedMessages,
|
|
3514
|
-
filePath
|
|
3515
|
-
};
|
|
3516
|
-
}
|
|
3517
|
-
|
|
3518
|
-
async function readSessionPlain(params = {}) {
|
|
3519
|
-
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3520
|
-
if (!source) {
|
|
3521
|
-
return { error: 'Invalid source' };
|
|
3522
|
-
}
|
|
3523
|
-
|
|
3524
|
-
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3525
|
-
if (!filePath) {
|
|
3526
|
-
return { error: 'Session file not found' };
|
|
3527
|
-
}
|
|
3528
|
-
|
|
3529
|
-
let extracted;
|
|
3530
|
-
try {
|
|
3531
|
-
extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
|
|
3532
|
-
} catch (e) {
|
|
3533
|
-
extracted = null;
|
|
3534
|
-
}
|
|
3535
|
-
|
|
3536
|
-
if (!extracted) {
|
|
3537
|
-
return { error: 'Failed to parse session file' };
|
|
3538
|
-
}
|
|
3539
|
-
|
|
3540
|
-
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
3541
|
-
const fallbackRecords = readJsonlRecords(filePath);
|
|
3542
|
-
if (fallbackRecords.length === 0) {
|
|
3543
|
-
return { error: 'Session file is empty' };
|
|
3544
|
-
}
|
|
3545
|
-
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
|
|
3546
|
-
}
|
|
3547
|
-
|
|
3548
|
-
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
3549
|
-
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
3550
|
-
const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
3551
|
-
const text = buildSessionPlainText(messages);
|
|
3552
|
-
|
|
3553
|
-
return {
|
|
3554
|
-
source,
|
|
3555
|
-
sourceLabel,
|
|
3556
|
-
sessionId,
|
|
3557
|
-
title: sessionId,
|
|
3558
|
-
filePath,
|
|
3559
|
-
text
|
|
3560
|
-
};
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
|
-
async function exportSessionData(params = {}) {
|
|
3564
|
-
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3565
|
-
if (!source) {
|
|
3566
|
-
return { error: 'Invalid source' };
|
|
3567
|
-
}
|
|
3568
|
-
|
|
3569
|
-
const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3570
|
-
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3571
|
-
if (!filePath) {
|
|
3572
|
-
return { error: 'Session file not found' };
|
|
3573
|
-
}
|
|
3574
|
-
|
|
3575
|
-
let extracted;
|
|
3576
|
-
try {
|
|
3577
|
-
extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
|
|
3578
|
-
} catch (e) {
|
|
3579
|
-
extracted = null;
|
|
3580
|
-
}
|
|
3048
|
+
filePath
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
async function readSessionPlain(params = {}) {
|
|
3053
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3054
|
+
if (!source) {
|
|
3055
|
+
return { error: 'Invalid source' };
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3059
|
+
if (!filePath) {
|
|
3060
|
+
return { error: 'Session file not found' };
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
let extracted;
|
|
3064
|
+
try {
|
|
3065
|
+
extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
|
|
3066
|
+
} catch (e) {
|
|
3067
|
+
extracted = null;
|
|
3068
|
+
}
|
|
3581
3069
|
|
|
3582
3070
|
if (!extracted) {
|
|
3583
3071
|
return { error: 'Failed to parse session file' };
|
|
@@ -3585,11 +3073,57 @@ async function exportSessionData(params = {}) {
|
|
|
3585
3073
|
|
|
3586
3074
|
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
3587
3075
|
const fallbackRecords = readJsonlRecords(filePath);
|
|
3588
|
-
if (fallbackRecords.length === 0) {
|
|
3589
|
-
return { error: 'Session file is empty' };
|
|
3590
|
-
}
|
|
3591
|
-
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
3592
|
-
}
|
|
3076
|
+
if (fallbackRecords.length === 0) {
|
|
3077
|
+
return { error: 'Session file is empty' };
|
|
3078
|
+
}
|
|
3079
|
+
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
3083
|
+
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
3084
|
+
const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
3085
|
+
const text = buildSessionPlainText(messages);
|
|
3086
|
+
|
|
3087
|
+
return {
|
|
3088
|
+
source,
|
|
3089
|
+
sourceLabel,
|
|
3090
|
+
sessionId,
|
|
3091
|
+
title: sessionId,
|
|
3092
|
+
filePath,
|
|
3093
|
+
text
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
async function exportSessionData(params = {}) {
|
|
3098
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3099
|
+
if (!source) {
|
|
3100
|
+
return { error: 'Invalid source' };
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3104
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3105
|
+
if (!filePath) {
|
|
3106
|
+
return { error: 'Session file not found' };
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
let extracted;
|
|
3110
|
+
try {
|
|
3111
|
+
extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
|
|
3112
|
+
} catch (e) {
|
|
3113
|
+
extracted = null;
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
if (!extracted) {
|
|
3117
|
+
return { error: 'Failed to parse session file' };
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
3121
|
+
const fallbackRecords = readJsonlRecords(filePath);
|
|
3122
|
+
if (fallbackRecords.length === 0) {
|
|
3123
|
+
return { error: 'Session file is empty' };
|
|
3124
|
+
}
|
|
3125
|
+
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
3126
|
+
}
|
|
3593
3127
|
|
|
3594
3128
|
extracted.messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
3595
3129
|
|
|
@@ -3601,29 +3135,29 @@ async function exportSessionData(params = {}) {
|
|
|
3601
3135
|
}
|
|
3602
3136
|
|
|
3603
3137
|
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
3604
|
-
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
3605
|
-
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
3606
|
-
const truncated = !!extracted.truncated;
|
|
3607
|
-
const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
|
|
3608
|
-
const markdown = buildSessionMarkdown({
|
|
3609
|
-
sourceLabel,
|
|
3610
|
-
sessionId,
|
|
3611
|
-
updatedAt: extracted.updatedAt,
|
|
3612
|
-
cwd: extracted.cwd,
|
|
3138
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
3139
|
+
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
3140
|
+
const truncated = !!extracted.truncated;
|
|
3141
|
+
const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
|
|
3142
|
+
const markdown = buildSessionMarkdown({
|
|
3143
|
+
sourceLabel,
|
|
3144
|
+
sessionId,
|
|
3145
|
+
updatedAt: extracted.updatedAt,
|
|
3146
|
+
cwd: extracted.cwd,
|
|
3613
3147
|
filePath,
|
|
3614
3148
|
messages: extracted.messages
|
|
3615
3149
|
});
|
|
3616
3150
|
|
|
3617
|
-
return {
|
|
3618
|
-
source,
|
|
3619
|
-
sourceLabel,
|
|
3620
|
-
sessionId,
|
|
3621
|
-
fileName: `${source}-session-${safeSessionId}.md`,
|
|
3622
|
-
content: markdown,
|
|
3623
|
-
truncated,
|
|
3624
|
-
maxMessages: maxMessagesLabel
|
|
3625
|
-
};
|
|
3626
|
-
}
|
|
3151
|
+
return {
|
|
3152
|
+
source,
|
|
3153
|
+
sourceLabel,
|
|
3154
|
+
sessionId,
|
|
3155
|
+
fileName: `${source}-session-${safeSessionId}.md`,
|
|
3156
|
+
content: markdown,
|
|
3157
|
+
truncated,
|
|
3158
|
+
maxMessages: maxMessagesLabel
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3627
3161
|
|
|
3628
3162
|
function buildExportPayload(includeKeys) {
|
|
3629
3163
|
const { config } = readConfigOrVirtualDefault();
|
|
@@ -3646,6 +3180,54 @@ function buildExportPayload(includeKeys) {
|
|
|
3646
3180
|
};
|
|
3647
3181
|
}
|
|
3648
3182
|
|
|
3183
|
+
function buildClaudeSharePayload(config = {}) {
|
|
3184
|
+
const apiKey = typeof config.apiKey === 'string' ? config.apiKey : '';
|
|
3185
|
+
const baseUrl = typeof config.baseUrl === 'string' ? config.baseUrl : '';
|
|
3186
|
+
const model = typeof config.model === 'string' ? config.model : '';
|
|
3187
|
+
|
|
3188
|
+
if (!baseUrl) return { error: 'Claude Base URL 未设置' };
|
|
3189
|
+
if (!apiKey) return { error: 'Claude API 密钥未设置' };
|
|
3190
|
+
|
|
3191
|
+
return {
|
|
3192
|
+
payload: {
|
|
3193
|
+
baseUrl: baseUrl.trim(),
|
|
3194
|
+
apiKey: apiKey.trim(),
|
|
3195
|
+
model: (model && model.trim()) || DEFAULT_CLAUDE_MODEL
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
function buildProviderSharePayload(params = {}) {
|
|
3201
|
+
const name = typeof params.name === 'string' ? params.name.trim() : '';
|
|
3202
|
+
if (!name) {
|
|
3203
|
+
return { error: '缺少提供商名称' };
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
const { config } = readConfigOrVirtualDefault();
|
|
3207
|
+
const providers = config.model_providers || {};
|
|
3208
|
+
const provider = providers[name];
|
|
3209
|
+
if (!provider || typeof provider !== 'object') {
|
|
3210
|
+
return { error: `提供商不存在: ${name}` };
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
3214
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
3215
|
+
? provider.preferred_auth_method
|
|
3216
|
+
: '';
|
|
3217
|
+
|
|
3218
|
+
if (!baseUrl) {
|
|
3219
|
+
return { error: `提供商 ${name} 缺少 base_url` };
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
return {
|
|
3223
|
+
payload: {
|
|
3224
|
+
name,
|
|
3225
|
+
baseUrl,
|
|
3226
|
+
apiKey
|
|
3227
|
+
}
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3649
3231
|
function normalizeImportPayload(payload) {
|
|
3650
3232
|
if (!payload || typeof payload !== 'object') {
|
|
3651
3233
|
return { error: 'Invalid import payload' };
|
|
@@ -4464,7 +4046,7 @@ function applyToClaudeSettings(config = {}) {
|
|
|
4464
4046
|
}
|
|
4465
4047
|
|
|
4466
4048
|
const baseUrl = (config.baseUrl || 'https://open.bigmodel.cn/api/anthropic').trim();
|
|
4467
|
-
const model = (config.model ||
|
|
4049
|
+
const model = (config.model || DEFAULT_CLAUDE_MODEL).trim();
|
|
4468
4050
|
const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
|
|
4469
4051
|
if (!readResult.ok) {
|
|
4470
4052
|
return { success: false, mode: 'settings-file', error: readResult.error };
|
|
@@ -4516,6 +4098,76 @@ function applyToClaudeSettings(config = {}) {
|
|
|
4516
4098
|
}
|
|
4517
4099
|
}
|
|
4518
4100
|
|
|
4101
|
+
function readClaudeSettingsInfo() {
|
|
4102
|
+
const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
|
|
4103
|
+
if (!readResult.ok) {
|
|
4104
|
+
return {
|
|
4105
|
+
error: readResult.error || '读取 Claude 配置失败',
|
|
4106
|
+
exists: !!readResult.exists,
|
|
4107
|
+
path: CLAUDE_SETTINGS_FILE
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
const settings = readResult.data || {};
|
|
4112
|
+
const env = (settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env))
|
|
4113
|
+
? settings.env
|
|
4114
|
+
: {};
|
|
4115
|
+
|
|
4116
|
+
return {
|
|
4117
|
+
exists: !!readResult.exists,
|
|
4118
|
+
path: CLAUDE_SETTINGS_FILE,
|
|
4119
|
+
apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
|
|
4120
|
+
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
|
|
4121
|
+
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
|
|
4122
|
+
env
|
|
4123
|
+
};
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
// CLI: 一行写入 Claude Code 配置
|
|
4127
|
+
function cmdClaude(baseUrl, apiKey, model, silent = false) {
|
|
4128
|
+
const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
|
4129
|
+
const normalizedKey = typeof apiKey === 'string' ? apiKey.trim() : '';
|
|
4130
|
+
const normalizedModel = typeof model === 'string' && model.trim()
|
|
4131
|
+
? model.trim()
|
|
4132
|
+
: DEFAULT_CLAUDE_MODEL;
|
|
4133
|
+
|
|
4134
|
+
if (!normalizedBaseUrl || !normalizedKey) {
|
|
4135
|
+
if (!silent) {
|
|
4136
|
+
console.error('用法: codexmate claude <BaseURL> <API密钥> [模型]');
|
|
4137
|
+
console.log('\n示例:');
|
|
4138
|
+
console.log(' codexmate claude https://open.bigmodel.cn/api/anthropic sk-ant-xxx glm-4.7');
|
|
4139
|
+
}
|
|
4140
|
+
throw new Error('BaseURL 和 API 密钥必填');
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
const result = applyToClaudeSettings({
|
|
4144
|
+
baseUrl: normalizedBaseUrl,
|
|
4145
|
+
apiKey: normalizedKey,
|
|
4146
|
+
model: normalizedModel
|
|
4147
|
+
});
|
|
4148
|
+
|
|
4149
|
+
if (!result || result.success === false) {
|
|
4150
|
+
const message = (result && result.error) || '应用 Claude 配置失败';
|
|
4151
|
+
if (!silent) console.error('错误:', message);
|
|
4152
|
+
throw new Error(message);
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
if (!silent) {
|
|
4156
|
+
console.log('✓ 已写入 Claude Code 配置');
|
|
4157
|
+
console.log(' Base URL:', normalizedBaseUrl);
|
|
4158
|
+
console.log(' 模型:', normalizedModel);
|
|
4159
|
+
if (result.targetPath) {
|
|
4160
|
+
console.log(' 目标文件:', result.targetPath);
|
|
4161
|
+
}
|
|
4162
|
+
if (result.backupPath) {
|
|
4163
|
+
console.log(' 已自动备份:', result.backupPath);
|
|
4164
|
+
}
|
|
4165
|
+
console.log();
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4168
|
+
return result;
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4519
4171
|
function commandExists(command, args = '') {
|
|
4520
4172
|
try {
|
|
4521
4173
|
execSync(`${command} ${args}`, { stdio: 'ignore' });
|
|
@@ -4649,7 +4301,7 @@ async function cmdZip(targetPath, options = {}) {
|
|
|
4649
4301
|
}
|
|
4650
4302
|
|
|
4651
4303
|
// 解压(7-Zip 优先)
|
|
4652
|
-
async function cmdUnzip(zipPath, outputDir) {
|
|
4304
|
+
async function cmdUnzip(zipPath, outputDir) {
|
|
4653
4305
|
if (!zipPath) {
|
|
4654
4306
|
console.error('用法: codexmate unzip <zip文件路径> [输出目录]');
|
|
4655
4307
|
console.log('\n示例:');
|
|
@@ -4705,227 +4357,230 @@ async function cmdUnzip(zipPath, outputDir) {
|
|
|
4705
4357
|
} catch (e) {
|
|
4706
4358
|
console.error('解压失败:', e.message);
|
|
4707
4359
|
process.exit(1);
|
|
4708
|
-
}
|
|
4709
|
-
}
|
|
4710
|
-
|
|
4711
|
-
function resolveExportOutputPath(outputPath, defaultFileName) {
|
|
4712
|
-
const fallback = path.resolve(process.cwd(), defaultFileName);
|
|
4713
|
-
if (typeof outputPath !== 'string' || !outputPath.trim()) {
|
|
4714
|
-
return fallback;
|
|
4715
|
-
}
|
|
4716
|
-
|
|
4717
|
-
const trimmed = outputPath.trim();
|
|
4718
|
-
const resolved = path.resolve(trimmed);
|
|
4719
|
-
const hasTrailingSep = /[\\\/]$/.test(trimmed);
|
|
4720
|
-
if (hasTrailingSep) {
|
|
4721
|
-
ensureDir(resolved);
|
|
4722
|
-
return path.join(resolved, defaultFileName);
|
|
4723
|
-
}
|
|
4724
|
-
|
|
4725
|
-
if (fs.existsSync(resolved)) {
|
|
4726
|
-
try {
|
|
4727
|
-
const stat = fs.statSync(resolved);
|
|
4728
|
-
if (stat.isDirectory()) {
|
|
4729
|
-
return path.join(resolved, defaultFileName);
|
|
4730
|
-
}
|
|
4731
|
-
} catch (e) {}
|
|
4732
|
-
}
|
|
4733
|
-
|
|
4734
|
-
return resolved;
|
|
4735
|
-
}
|
|
4736
|
-
|
|
4737
|
-
function printExportSessionUsage() {
|
|
4738
|
-
console.log('\n用法: codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
4739
|
-
console.log('\n示例:');
|
|
4740
|
-
console.log(' codexmate export-session --source codex --session-id 123456');
|
|
4741
|
-
console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
|
|
4742
|
-
console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
|
|
4743
|
-
}
|
|
4744
|
-
|
|
4745
|
-
function parseExportSessionArgs(args = []) {
|
|
4746
|
-
const options = {
|
|
4747
|
-
source: '',
|
|
4748
|
-
sessionId: '',
|
|
4749
|
-
filePath: '',
|
|
4750
|
-
output: '',
|
|
4751
|
-
maxMessages: undefined
|
|
4752
|
-
};
|
|
4753
|
-
const errors = [];
|
|
4754
|
-
|
|
4755
|
-
for (let i = 0; i < args.length; i++) {
|
|
4756
|
-
const arg = args[i];
|
|
4757
|
-
if (!arg) continue;
|
|
4758
|
-
|
|
4759
|
-
if (arg.startsWith('--source=')) {
|
|
4760
|
-
options.source = arg.slice('--source='.length);
|
|
4761
|
-
continue;
|
|
4762
|
-
}
|
|
4763
|
-
if (arg === '--source') {
|
|
4764
|
-
options.source = args[i + 1] || '';
|
|
4765
|
-
i += 1;
|
|
4766
|
-
continue;
|
|
4767
|
-
}
|
|
4768
|
-
if (arg.startsWith('--session-id=')) {
|
|
4769
|
-
options.sessionId = arg.slice('--session-id='.length);
|
|
4770
|
-
continue;
|
|
4771
|
-
}
|
|
4772
|
-
if (arg === '--session-id') {
|
|
4773
|
-
options.sessionId = args[i + 1] || '';
|
|
4774
|
-
i += 1;
|
|
4775
|
-
continue;
|
|
4776
|
-
}
|
|
4777
|
-
if (arg.startsWith('--file=')) {
|
|
4778
|
-
options.filePath = arg.slice('--file='.length);
|
|
4779
|
-
continue;
|
|
4780
|
-
}
|
|
4781
|
-
if (arg === '--file') {
|
|
4782
|
-
options.filePath = args[i + 1] || '';
|
|
4783
|
-
i += 1;
|
|
4784
|
-
continue;
|
|
4785
|
-
}
|
|
4786
|
-
if (arg.startsWith('--output=')) {
|
|
4787
|
-
options.output = arg.slice('--output='.length);
|
|
4788
|
-
continue;
|
|
4789
|
-
}
|
|
4790
|
-
if (arg === '--output') {
|
|
4791
|
-
options.output = args[i + 1] || '';
|
|
4792
|
-
i += 1;
|
|
4793
|
-
continue;
|
|
4794
|
-
}
|
|
4795
|
-
if (arg.startsWith('--max-messages=')) {
|
|
4796
|
-
options.maxMessages = arg.slice('--max-messages='.length);
|
|
4797
|
-
continue;
|
|
4798
|
-
}
|
|
4799
|
-
if (arg === '--max-messages') {
|
|
4800
|
-
options.maxMessages = args[i + 1] || '';
|
|
4801
|
-
i += 1;
|
|
4802
|
-
continue;
|
|
4803
|
-
}
|
|
4804
|
-
|
|
4805
|
-
errors.push(`未知参数: ${arg}`);
|
|
4806
|
-
}
|
|
4807
|
-
|
|
4808
|
-
const normalizedSource = options.source.trim().toLowerCase();
|
|
4809
|
-
if (normalizedSource && normalizedSource !== 'codex' && normalizedSource !== 'claude') {
|
|
4810
|
-
errors.push('参数 --source 仅支持 codex 或 claude');
|
|
4811
|
-
}
|
|
4812
|
-
options.source = normalizedSource;
|
|
4813
|
-
|
|
4814
|
-
if (!options.source) {
|
|
4815
|
-
errors.push('缺少 --source');
|
|
4816
|
-
}
|
|
4817
|
-
|
|
4818
|
-
if (!options.sessionId && !options.filePath) {
|
|
4819
|
-
errors.push('必须指定 --session-id 或 --file');
|
|
4820
|
-
}
|
|
4821
|
-
|
|
4822
|
-
if (options.maxMessages !== undefined) {
|
|
4823
|
-
const parsed = parseMaxMessagesValue(options.maxMessages);
|
|
4824
|
-
if (parsed === null) {
|
|
4825
|
-
errors.push('参数 --max-messages 无效');
|
|
4826
|
-
} else {
|
|
4827
|
-
options.maxMessages = parsed === Infinity ? Infinity : Math.max(1, Math.floor(parsed));
|
|
4828
|
-
}
|
|
4829
|
-
}
|
|
4830
|
-
|
|
4831
|
-
return {
|
|
4832
|
-
options,
|
|
4833
|
-
error: errors.length > 0 ? errors.join(';') : ''
|
|
4834
|
-
};
|
|
4835
|
-
}
|
|
4836
|
-
|
|
4837
|
-
async function cmdExportSession(args = []) {
|
|
4838
|
-
const parsed = parseExportSessionArgs(args);
|
|
4839
|
-
if (parsed.error) {
|
|
4840
|
-
console.error('错误:', parsed.error);
|
|
4841
|
-
printExportSessionUsage();
|
|
4842
|
-
process.exit(1);
|
|
4843
|
-
}
|
|
4844
|
-
|
|
4845
|
-
const options = parsed.options;
|
|
4846
|
-
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
4847
|
-
let result;
|
|
4848
|
-
try {
|
|
4849
|
-
result = await exportSessionData({
|
|
4850
|
-
source: options.source,
|
|
4851
|
-
sessionId: options.sessionId,
|
|
4852
|
-
filePath: options.filePath,
|
|
4853
|
-
maxMessages
|
|
4854
|
-
});
|
|
4855
|
-
} catch (e) {
|
|
4856
|
-
console.error('导出失败:', e.message || e);
|
|
4857
|
-
process.exit(1);
|
|
4858
|
-
}
|
|
4859
|
-
|
|
4860
|
-
if (result && result.error) {
|
|
4861
|
-
console.error('导出失败:', result.error);
|
|
4862
|
-
process.exit(1);
|
|
4863
|
-
}
|
|
4864
|
-
|
|
4865
|
-
const defaultFileName = (result && result.fileName)
|
|
4866
|
-
? result.fileName
|
|
4867
|
-
: `${options.source}-session-${options.sessionId || Date.now()}.md`;
|
|
4868
|
-
const outputPath = resolveExportOutputPath(options.output, defaultFileName);
|
|
4869
|
-
ensureDir(path.dirname(outputPath));
|
|
4870
|
-
fs.writeFileSync(outputPath, (result && result.content) ? result.content : '', 'utf-8');
|
|
4871
|
-
|
|
4872
|
-
console.log('\n✓ 会话已导出:', outputPath);
|
|
4873
|
-
if (result && result.truncated) {
|
|
4874
|
-
const label = maxMessages === Infinity ? 'all' : maxMessages;
|
|
4875
|
-
console.log(`! 已截断: 仅导出前 ${label} 条消息`);
|
|
4876
|
-
console.log(' 可使用 --max-messages=all 导出完整内容');
|
|
4877
|
-
}
|
|
4878
|
-
console.log();
|
|
4879
|
-
}
|
|
4880
|
-
|
|
4881
|
-
function parseStartOptions(args = []) {
|
|
4882
|
-
const options = { host: '' };
|
|
4883
|
-
if (!Array.isArray(args)) {
|
|
4884
|
-
return options;
|
|
4885
|
-
}
|
|
4886
|
-
|
|
4887
|
-
for (let i = 0; i < args.length; i++) {
|
|
4888
|
-
const arg = args[i];
|
|
4889
|
-
if (!arg) continue;
|
|
4890
|
-
if (arg.startsWith('--host=')) {
|
|
4891
|
-
options.host = arg.slice('--host='.length);
|
|
4892
|
-
continue;
|
|
4893
|
-
}
|
|
4894
|
-
if (arg === '--host') {
|
|
4895
|
-
options.host = args[i + 1] || '';
|
|
4896
|
-
i += 1;
|
|
4897
|
-
}
|
|
4898
|
-
}
|
|
4899
|
-
|
|
4900
|
-
return options;
|
|
4901
|
-
}
|
|
4902
|
-
|
|
4903
|
-
function isAnyAddressHost(host) {
|
|
4904
|
-
return host === '0.0.0.0' || host === '::';
|
|
4905
|
-
}
|
|
4906
|
-
|
|
4907
|
-
function formatHostForUrl(host) {
|
|
4908
|
-
const value = typeof host === 'string' ? host.trim() : '';
|
|
4909
|
-
if (!value) return '';
|
|
4910
|
-
if (value.startsWith('[') && value.endsWith(']')) {
|
|
4911
|
-
return value;
|
|
4912
|
-
}
|
|
4913
|
-
if (value.includes(':')) {
|
|
4914
|
-
return `[${value}]`;
|
|
4915
|
-
}
|
|
4916
|
-
return value;
|
|
4917
|
-
}
|
|
4918
|
-
|
|
4919
|
-
// 打开 Web UI
|
|
4920
|
-
function cmdStart(options = {}) {
|
|
4921
|
-
const htmlPath = path.join(__dirname, 'web-ui.html');
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
function resolveExportOutputPath(outputPath, defaultFileName) {
|
|
4364
|
+
const fallback = path.resolve(process.cwd(), defaultFileName);
|
|
4365
|
+
if (typeof outputPath !== 'string' || !outputPath.trim()) {
|
|
4366
|
+
return fallback;
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
const trimmed = outputPath.trim();
|
|
4370
|
+
const resolved = path.resolve(trimmed);
|
|
4371
|
+
const hasTrailingSep = /[\\\/]$/.test(trimmed);
|
|
4372
|
+
if (hasTrailingSep) {
|
|
4373
|
+
ensureDir(resolved);
|
|
4374
|
+
return path.join(resolved, defaultFileName);
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
if (fs.existsSync(resolved)) {
|
|
4378
|
+
try {
|
|
4379
|
+
const stat = fs.statSync(resolved);
|
|
4380
|
+
if (stat.isDirectory()) {
|
|
4381
|
+
return path.join(resolved, defaultFileName);
|
|
4382
|
+
}
|
|
4383
|
+
} catch (e) {}
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
return resolved;
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
function printExportSessionUsage() {
|
|
4390
|
+
console.log('\n用法: codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
4391
|
+
console.log('\n示例:');
|
|
4392
|
+
console.log(' codexmate export-session --source codex --session-id 123456');
|
|
4393
|
+
console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
|
|
4394
|
+
console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
function parseExportSessionArgs(args = []) {
|
|
4398
|
+
const options = {
|
|
4399
|
+
source: '',
|
|
4400
|
+
sessionId: '',
|
|
4401
|
+
filePath: '',
|
|
4402
|
+
output: '',
|
|
4403
|
+
maxMessages: undefined
|
|
4404
|
+
};
|
|
4405
|
+
const errors = [];
|
|
4406
|
+
|
|
4407
|
+
for (let i = 0; i < args.length; i++) {
|
|
4408
|
+
const arg = args[i];
|
|
4409
|
+
if (!arg) continue;
|
|
4410
|
+
|
|
4411
|
+
if (arg.startsWith('--source=')) {
|
|
4412
|
+
options.source = arg.slice('--source='.length);
|
|
4413
|
+
continue;
|
|
4414
|
+
}
|
|
4415
|
+
if (arg === '--source') {
|
|
4416
|
+
options.source = args[i + 1] || '';
|
|
4417
|
+
i += 1;
|
|
4418
|
+
continue;
|
|
4419
|
+
}
|
|
4420
|
+
if (arg.startsWith('--session-id=')) {
|
|
4421
|
+
options.sessionId = arg.slice('--session-id='.length);
|
|
4422
|
+
continue;
|
|
4423
|
+
}
|
|
4424
|
+
if (arg === '--session-id') {
|
|
4425
|
+
options.sessionId = args[i + 1] || '';
|
|
4426
|
+
i += 1;
|
|
4427
|
+
continue;
|
|
4428
|
+
}
|
|
4429
|
+
if (arg.startsWith('--file=')) {
|
|
4430
|
+
options.filePath = arg.slice('--file='.length);
|
|
4431
|
+
continue;
|
|
4432
|
+
}
|
|
4433
|
+
if (arg === '--file') {
|
|
4434
|
+
options.filePath = args[i + 1] || '';
|
|
4435
|
+
i += 1;
|
|
4436
|
+
continue;
|
|
4437
|
+
}
|
|
4438
|
+
if (arg.startsWith('--output=')) {
|
|
4439
|
+
options.output = arg.slice('--output='.length);
|
|
4440
|
+
continue;
|
|
4441
|
+
}
|
|
4442
|
+
if (arg === '--output') {
|
|
4443
|
+
options.output = args[i + 1] || '';
|
|
4444
|
+
i += 1;
|
|
4445
|
+
continue;
|
|
4446
|
+
}
|
|
4447
|
+
if (arg.startsWith('--max-messages=')) {
|
|
4448
|
+
options.maxMessages = arg.slice('--max-messages='.length);
|
|
4449
|
+
continue;
|
|
4450
|
+
}
|
|
4451
|
+
if (arg === '--max-messages') {
|
|
4452
|
+
options.maxMessages = args[i + 1] || '';
|
|
4453
|
+
i += 1;
|
|
4454
|
+
continue;
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4457
|
+
errors.push(`未知参数: ${arg}`);
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
const normalizedSource = options.source.trim().toLowerCase();
|
|
4461
|
+
if (normalizedSource && normalizedSource !== 'codex' && normalizedSource !== 'claude') {
|
|
4462
|
+
errors.push('参数 --source 仅支持 codex 或 claude');
|
|
4463
|
+
}
|
|
4464
|
+
options.source = normalizedSource;
|
|
4465
|
+
|
|
4466
|
+
if (!options.source) {
|
|
4467
|
+
errors.push('缺少 --source');
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
if (!options.sessionId && !options.filePath) {
|
|
4471
|
+
errors.push('必须指定 --session-id 或 --file');
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
if (options.maxMessages !== undefined) {
|
|
4475
|
+
const parsed = parseMaxMessagesValue(options.maxMessages);
|
|
4476
|
+
if (parsed === null) {
|
|
4477
|
+
errors.push('参数 --max-messages 无效');
|
|
4478
|
+
} else {
|
|
4479
|
+
options.maxMessages = parsed === Infinity ? Infinity : Math.max(1, Math.floor(parsed));
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
return {
|
|
4484
|
+
options,
|
|
4485
|
+
error: errors.length > 0 ? errors.join(';') : ''
|
|
4486
|
+
};
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
async function cmdExportSession(args = []) {
|
|
4490
|
+
const parsed = parseExportSessionArgs(args);
|
|
4491
|
+
if (parsed.error) {
|
|
4492
|
+
console.error('错误:', parsed.error);
|
|
4493
|
+
printExportSessionUsage();
|
|
4494
|
+
process.exit(1);
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
const options = parsed.options;
|
|
4498
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
4499
|
+
let result;
|
|
4500
|
+
try {
|
|
4501
|
+
result = await exportSessionData({
|
|
4502
|
+
source: options.source,
|
|
4503
|
+
sessionId: options.sessionId,
|
|
4504
|
+
filePath: options.filePath,
|
|
4505
|
+
maxMessages
|
|
4506
|
+
});
|
|
4507
|
+
} catch (e) {
|
|
4508
|
+
console.error('导出失败:', e.message || e);
|
|
4509
|
+
process.exit(1);
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
if (result && result.error) {
|
|
4513
|
+
console.error('导出失败:', result.error);
|
|
4514
|
+
process.exit(1);
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
const defaultFileName = (result && result.fileName)
|
|
4518
|
+
? result.fileName
|
|
4519
|
+
: `${options.source}-session-${options.sessionId || Date.now()}.md`;
|
|
4520
|
+
const outputPath = resolveExportOutputPath(options.output, defaultFileName);
|
|
4521
|
+
ensureDir(path.dirname(outputPath));
|
|
4522
|
+
fs.writeFileSync(outputPath, (result && result.content) ? result.content : '', 'utf-8');
|
|
4523
|
+
|
|
4524
|
+
console.log('\n✓ 会话已导出:', outputPath);
|
|
4525
|
+
if (result && result.truncated) {
|
|
4526
|
+
const label = maxMessages === Infinity ? 'all' : maxMessages;
|
|
4527
|
+
console.log(`! 已截断: 仅导出前 ${label} 条消息`);
|
|
4528
|
+
console.log(' 可使用 --max-messages=all 导出完整内容');
|
|
4529
|
+
}
|
|
4530
|
+
console.log();
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
function parseStartOptions(args = []) {
|
|
4534
|
+
const options = { host: '' };
|
|
4535
|
+
if (!Array.isArray(args)) {
|
|
4536
|
+
return options;
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
for (let i = 0; i < args.length; i++) {
|
|
4540
|
+
const arg = args[i];
|
|
4541
|
+
if (!arg) continue;
|
|
4542
|
+
if (arg.startsWith('--host=')) {
|
|
4543
|
+
options.host = arg.slice('--host='.length);
|
|
4544
|
+
continue;
|
|
4545
|
+
}
|
|
4546
|
+
if (arg === '--host') {
|
|
4547
|
+
options.host = args[i + 1] || '';
|
|
4548
|
+
i += 1;
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
return options;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
function isAnyAddressHost(host) {
|
|
4556
|
+
return host === '0.0.0.0' || host === '::';
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4559
|
+
function formatHostForUrl(host) {
|
|
4560
|
+
const value = typeof host === 'string' ? host.trim() : '';
|
|
4561
|
+
if (!value) return '';
|
|
4562
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
4563
|
+
return value;
|
|
4564
|
+
}
|
|
4565
|
+
if (value.includes(':')) {
|
|
4566
|
+
return `[${value}]`;
|
|
4567
|
+
}
|
|
4568
|
+
return value;
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
// 打开 Web UI
|
|
4572
|
+
function cmdStart(options = {}) {
|
|
4573
|
+
const htmlPath = path.join(__dirname, 'web-ui.html');
|
|
4574
|
+
const assetsDir = path.join(__dirname, 'res');
|
|
4575
|
+
const webDir = path.join(__dirname, 'web-ui');
|
|
4576
|
+
if (!fs.existsSync(htmlPath)) {
|
|
4577
|
+
console.error('错误: web-ui.html 不存在');
|
|
4578
|
+
process.exit(1);
|
|
4579
|
+
}
|
|
4926
4580
|
|
|
4927
4581
|
const server = http.createServer((req, res) => {
|
|
4928
|
-
|
|
4582
|
+
const requestPath = (req.url || '/').split('?')[0];
|
|
4583
|
+
if (requestPath === '/api') {
|
|
4929
4584
|
let body = '';
|
|
4930
4585
|
req.on('data', chunk => body += chunk);
|
|
4931
4586
|
req.on('end', async () => {
|
|
@@ -4937,9 +4592,11 @@ function cmdStart(options = {}) {
|
|
|
4937
4592
|
case 'status':
|
|
4938
4593
|
const statusConfigResult = readConfigOrVirtualDefault();
|
|
4939
4594
|
const config = statusConfigResult.config;
|
|
4595
|
+
const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
|
|
4940
4596
|
result = {
|
|
4941
4597
|
provider: config.model_provider || '未设置',
|
|
4942
4598
|
model: config.model || '未设置',
|
|
4599
|
+
serviceTier,
|
|
4943
4600
|
configReady: !statusConfigResult.isVirtual,
|
|
4944
4601
|
configNotice: statusConfigResult.reason || '',
|
|
4945
4602
|
initNotice: consumeInitNotice()
|
|
@@ -5039,9 +4696,18 @@ function cmdStart(options = {}) {
|
|
|
5039
4696
|
cmdDeleteModel(params.model, true);
|
|
5040
4697
|
result = { success: true };
|
|
5041
4698
|
break;
|
|
4699
|
+
case 'get-claude-settings':
|
|
4700
|
+
result = readClaudeSettingsInfo();
|
|
4701
|
+
break;
|
|
5042
4702
|
case 'apply-claude-config':
|
|
5043
4703
|
result = applyToClaudeSettings(params.config);
|
|
5044
4704
|
break;
|
|
4705
|
+
case 'export-claude-share':
|
|
4706
|
+
result = buildClaudeSharePayload(params && params.config ? params.config : {});
|
|
4707
|
+
break;
|
|
4708
|
+
case 'export-provider':
|
|
4709
|
+
result = buildProviderSharePayload(params || {});
|
|
4710
|
+
break;
|
|
5045
4711
|
case 'export-config':
|
|
5046
4712
|
result = {
|
|
5047
4713
|
data: buildExportPayload(!!params.includeKeys)
|
|
@@ -5075,18 +4741,18 @@ function cmdStart(options = {}) {
|
|
|
5075
4741
|
case 'delete-session':
|
|
5076
4742
|
result = await deleteSessionData(params || {});
|
|
5077
4743
|
break;
|
|
5078
|
-
case 'clone-session':
|
|
5079
|
-
result = await cloneCodexSession(params || {});
|
|
5080
|
-
break;
|
|
5081
|
-
case 'session-detail':
|
|
5082
|
-
result = await readSessionDetail(params);
|
|
5083
|
-
break;
|
|
5084
|
-
case 'session-plain':
|
|
5085
|
-
result = await readSessionPlain(params);
|
|
5086
|
-
break;
|
|
5087
|
-
default:
|
|
5088
|
-
result = { error: '未知操作' };
|
|
5089
|
-
}
|
|
4744
|
+
case 'clone-session':
|
|
4745
|
+
result = await cloneCodexSession(params || {});
|
|
4746
|
+
break;
|
|
4747
|
+
case 'session-detail':
|
|
4748
|
+
result = await readSessionDetail(params);
|
|
4749
|
+
break;
|
|
4750
|
+
case 'session-plain':
|
|
4751
|
+
result = await readSessionPlain(params);
|
|
4752
|
+
break;
|
|
4753
|
+
default:
|
|
4754
|
+
result = { error: '未知操作' };
|
|
4755
|
+
}
|
|
5090
4756
|
|
|
5091
4757
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5092
4758
|
res.end(JSON.stringify(result));
|
|
@@ -5095,6 +4761,50 @@ function cmdStart(options = {}) {
|
|
|
5095
4761
|
res.end(JSON.stringify({ error: e.message }));
|
|
5096
4762
|
}
|
|
5097
4763
|
});
|
|
4764
|
+
} else if (requestPath.startsWith('/web-ui/')) {
|
|
4765
|
+
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
|
|
4766
|
+
const filePath = path.join(__dirname, normalized);
|
|
4767
|
+
if (!isPathInside(filePath, webDir)) {
|
|
4768
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
4769
|
+
res.end('Forbidden');
|
|
4770
|
+
return;
|
|
4771
|
+
}
|
|
4772
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
4773
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
4774
|
+
res.end('Not Found');
|
|
4775
|
+
return;
|
|
4776
|
+
}
|
|
4777
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
4778
|
+
const mime = ext === '.js' || ext === '.mjs'
|
|
4779
|
+
? 'application/javascript; charset=utf-8'
|
|
4780
|
+
: ext === '.css'
|
|
4781
|
+
? 'text/css; charset=utf-8'
|
|
4782
|
+
: ext === '.json'
|
|
4783
|
+
? 'application/json; charset=utf-8'
|
|
4784
|
+
: 'application/octet-stream';
|
|
4785
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
4786
|
+
fs.createReadStream(filePath).pipe(res);
|
|
4787
|
+
} else if (requestPath.startsWith('/res/')) {
|
|
4788
|
+
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
|
|
4789
|
+
const filePath = path.join(__dirname, normalized);
|
|
4790
|
+
if (!isPathInside(filePath, assetsDir)) {
|
|
4791
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
4792
|
+
res.end('Forbidden');
|
|
4793
|
+
return;
|
|
4794
|
+
}
|
|
4795
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
4796
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
4797
|
+
res.end('Not Found');
|
|
4798
|
+
return;
|
|
4799
|
+
}
|
|
4800
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
4801
|
+
const mime = ext === '.js'
|
|
4802
|
+
? 'application/javascript; charset=utf-8'
|
|
4803
|
+
: ext === '.json'
|
|
4804
|
+
? 'application/json; charset=utf-8'
|
|
4805
|
+
: 'application/octet-stream';
|
|
4806
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
4807
|
+
fs.createReadStream(filePath).pipe(res);
|
|
5098
4808
|
} else {
|
|
5099
4809
|
const html = fs.readFileSync(htmlPath, 'utf-8');
|
|
5100
4810
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -5102,28 +4812,28 @@ function cmdStart(options = {}) {
|
|
|
5102
4812
|
}
|
|
5103
4813
|
});
|
|
5104
4814
|
|
|
5105
|
-
const port = resolveWebPort();
|
|
5106
|
-
const host = resolveWebHost(options);
|
|
5107
|
-
const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
|
|
5108
|
-
const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
|
|
5109
|
-
server.listen(port, host, () => {
|
|
5110
|
-
console.log('\n✓ Web UI 已启动:', openUrl);
|
|
5111
|
-
if (host && host !== openHost) {
|
|
5112
|
-
console.log(' 监听地址:', host);
|
|
5113
|
-
}
|
|
5114
|
-
console.log(' 按 Ctrl+C 退出\n');
|
|
5115
|
-
if (isAnyAddressHost(host)) {
|
|
5116
|
-
console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
|
|
5117
|
-
console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
|
|
5118
|
-
}
|
|
5119
|
-
|
|
5120
|
-
// 打开浏览器
|
|
5121
|
-
const platform = process.platform;
|
|
5122
|
-
let command;
|
|
5123
|
-
const url = openUrl;
|
|
5124
|
-
|
|
5125
|
-
if (platform === 'win32') {
|
|
5126
|
-
command = `start "" "${url}"`;
|
|
4815
|
+
const port = resolveWebPort();
|
|
4816
|
+
const host = resolveWebHost(options);
|
|
4817
|
+
const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
|
|
4818
|
+
const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
|
|
4819
|
+
server.listen(port, host, () => {
|
|
4820
|
+
console.log('\n✓ Web UI 已启动:', openUrl);
|
|
4821
|
+
if (host && host !== openHost) {
|
|
4822
|
+
console.log(' 监听地址:', host);
|
|
4823
|
+
}
|
|
4824
|
+
console.log(' 按 Ctrl+C 退出\n');
|
|
4825
|
+
if (isAnyAddressHost(host)) {
|
|
4826
|
+
console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
|
|
4827
|
+
console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
// 打开浏览器
|
|
4831
|
+
const platform = process.platform;
|
|
4832
|
+
let command;
|
|
4833
|
+
const url = openUrl;
|
|
4834
|
+
|
|
4835
|
+
if (platform === 'win32') {
|
|
4836
|
+
command = `start "" "${url}"`;
|
|
5127
4837
|
} else if (platform === 'darwin') {
|
|
5128
4838
|
command = `open "${url}"`;
|
|
5129
4839
|
} else {
|
|
@@ -5136,46 +4846,46 @@ function cmdStart(options = {}) {
|
|
|
5136
4846
|
if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
|
|
5137
4847
|
});
|
|
5138
4848
|
}
|
|
5139
|
-
});
|
|
5140
|
-
}
|
|
5141
|
-
|
|
5142
|
-
async function cmdCodex(args = []) {
|
|
5143
|
-
const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
|
|
5144
|
-
const hasYolo = extraArgs.includes('--yolo');
|
|
5145
|
-
const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
|
|
5146
|
-
|
|
5147
|
-
return new Promise((resolve, reject) => {
|
|
5148
|
-
const child = spawn('codex', finalArgs, {
|
|
5149
|
-
stdio: 'inherit',
|
|
5150
|
-
shell: process.platform === 'win32'
|
|
5151
|
-
});
|
|
5152
|
-
|
|
5153
|
-
child.on('error', (err) => {
|
|
5154
|
-
reject(new Error(`无法启动 codex,请确认已安装并在 PATH 中: ${err.message}`));
|
|
5155
|
-
});
|
|
5156
|
-
|
|
5157
|
-
child.on('exit', (code, signal) => {
|
|
5158
|
-
if (typeof code === 'number') {
|
|
5159
|
-
resolve(code);
|
|
5160
|
-
return;
|
|
5161
|
-
}
|
|
5162
|
-
if (signal === 'SIGINT') {
|
|
5163
|
-
resolve(130);
|
|
5164
|
-
return;
|
|
5165
|
-
}
|
|
5166
|
-
if (signal === 'SIGTERM') {
|
|
5167
|
-
resolve(143);
|
|
5168
|
-
return;
|
|
5169
|
-
}
|
|
5170
|
-
resolve(1);
|
|
5171
|
-
});
|
|
5172
|
-
});
|
|
5173
|
-
}
|
|
5174
|
-
|
|
5175
|
-
// ============================================================================
|
|
5176
|
-
// 主程序
|
|
5177
|
-
// ============================================================================
|
|
5178
|
-
async function main() {
|
|
4849
|
+
});
|
|
4850
|
+
}
|
|
4851
|
+
|
|
4852
|
+
async function cmdCodex(args = []) {
|
|
4853
|
+
const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
|
|
4854
|
+
const hasYolo = extraArgs.includes('--yolo');
|
|
4855
|
+
const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
|
|
4856
|
+
|
|
4857
|
+
return new Promise((resolve, reject) => {
|
|
4858
|
+
const child = spawn('codex', finalArgs, {
|
|
4859
|
+
stdio: 'inherit',
|
|
4860
|
+
shell: process.platform === 'win32'
|
|
4861
|
+
});
|
|
4862
|
+
|
|
4863
|
+
child.on('error', (err) => {
|
|
4864
|
+
reject(new Error(`无法启动 codex,请确认已安装并在 PATH 中: ${err.message}`));
|
|
4865
|
+
});
|
|
4866
|
+
|
|
4867
|
+
child.on('exit', (code, signal) => {
|
|
4868
|
+
if (typeof code === 'number') {
|
|
4869
|
+
resolve(code);
|
|
4870
|
+
return;
|
|
4871
|
+
}
|
|
4872
|
+
if (signal === 'SIGINT') {
|
|
4873
|
+
resolve(130);
|
|
4874
|
+
return;
|
|
4875
|
+
}
|
|
4876
|
+
if (signal === 'SIGTERM') {
|
|
4877
|
+
resolve(143);
|
|
4878
|
+
return;
|
|
4879
|
+
}
|
|
4880
|
+
resolve(1);
|
|
4881
|
+
});
|
|
4882
|
+
});
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
// ============================================================================
|
|
4886
|
+
// 主程序
|
|
4887
|
+
// ============================================================================
|
|
4888
|
+
async function main() {
|
|
5179
4889
|
const bootstrap = ensureManagedConfigBootstrap();
|
|
5180
4890
|
if (bootstrap && bootstrap.notice) {
|
|
5181
4891
|
console.log(`\n[Init] ${bootstrap.notice}`);
|
|
@@ -5193,16 +4903,17 @@ async function main() {
|
|
|
5193
4903
|
console.log(' codexmate use <模型> 切换模型');
|
|
5194
4904
|
console.log(' codexmate add <名称> <URL> [密钥]');
|
|
5195
4905
|
console.log(' codexmate delete <名称> 删除提供商');
|
|
5196
|
-
console.log(' codexmate
|
|
5197
|
-
console.log(' codexmate
|
|
5198
|
-
console.log(' codexmate
|
|
5199
|
-
console.log(' codexmate
|
|
5200
|
-
console.log(' codexmate
|
|
5201
|
-
console.log(' codexmate
|
|
5202
|
-
console.log(' codexmate
|
|
5203
|
-
console.log('');
|
|
5204
|
-
|
|
5205
|
-
|
|
4906
|
+
console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
|
|
4907
|
+
console.log(' codexmate add-model <模型> 添加模型');
|
|
4908
|
+
console.log(' codexmate delete-model <模型> 删除模型');
|
|
4909
|
+
console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
|
|
4910
|
+
console.log(' codexmate codex [参数...] 等同于 codex --yolo');
|
|
4911
|
+
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
4912
|
+
console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
|
|
4913
|
+
console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
|
|
4914
|
+
console.log('');
|
|
4915
|
+
process.exit(0);
|
|
4916
|
+
}
|
|
5206
4917
|
|
|
5207
4918
|
const command = args[0];
|
|
5208
4919
|
|
|
@@ -5215,18 +4926,23 @@ async function main() {
|
|
|
5215
4926
|
case 'use': cmdUseModel(args[1]); break;
|
|
5216
4927
|
case 'add': cmdAdd(args[1], args[2], args[3]); break;
|
|
5217
4928
|
case 'delete': cmdDelete(args[1]); break;
|
|
5218
|
-
case '
|
|
5219
|
-
case '
|
|
5220
|
-
case '
|
|
5221
|
-
case '
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
case '
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
4929
|
+
case 'claude': cmdClaude(args[1], args[2], args[3]); break;
|
|
4930
|
+
case 'add-model': cmdAddModel(args[1]); break;
|
|
4931
|
+
case 'delete-model': cmdDeleteModel(args[1]); break;
|
|
4932
|
+
case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
|
|
4933
|
+
case 'start':
|
|
4934
|
+
console.error('错误: 命令已更名为 "run",请使用: codexmate run');
|
|
4935
|
+
process.exit(1);
|
|
4936
|
+
break;
|
|
4937
|
+
case 'codex': {
|
|
4938
|
+
const exitCode = await cmdCodex(args.slice(1));
|
|
4939
|
+
process.exit(exitCode);
|
|
4940
|
+
break;
|
|
4941
|
+
}
|
|
4942
|
+
case 'export-session': await cmdExportSession(args.slice(1)); break;
|
|
4943
|
+
case 'zip': {
|
|
4944
|
+
// 解析 --max:N 参数
|
|
4945
|
+
const zipOptions = {};
|
|
5230
4946
|
let targetPath = null;
|
|
5231
4947
|
for (let i = 1; i < args.length; i++) {
|
|
5232
4948
|
const arg = args[i];
|
|
@@ -5251,4 +4967,3 @@ main().catch((err) => {
|
|
|
5251
4967
|
console.error('错误:', err && err.message ? err.message : err);
|
|
5252
4968
|
process.exit(1);
|
|
5253
4969
|
});
|
|
5254
|
-
|