codexmate 0.0.7 → 0.0.8
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/README.md +48 -40
- package/README.zh-CN.md +48 -40
- package/cli.js +791 -1211
- package/lib/cli-file-utils.js +149 -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 +3 -2
- package/tests/e2e/helpers.js +214 -0
- package/tests/e2e/recent-health.e2e.js +6 -0
- package/tests/e2e/run.js +84 -302
- package/tests/e2e/test-claude.js +21 -0
- package/tests/e2e/test-config.js +124 -0
- package/tests/e2e/test-health-speed.js +75 -0
- package/tests/e2e/test-openclaw.js +47 -0
- package/tests/e2e/test-sessions.js +60 -0
- package/tests/e2e/test-setup.js +90 -0
- package/web-ui.html +912 -423
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;
|
|
@@ -49,7 +92,6 @@ 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,252 +774,6 @@ function recordRecentConfig(provider, model) {
|
|
|
975
774
|
writeRecentConfigs(trimmed);
|
|
976
775
|
}
|
|
977
776
|
|
|
978
|
-
function isValidHttpUrl(value) {
|
|
979
|
-
if (typeof value !== 'string' || !value.trim()) return false;
|
|
980
|
-
try {
|
|
981
|
-
const parsed = new URL(value);
|
|
982
|
-
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
983
|
-
} catch (e) {
|
|
984
|
-
return false;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function normalizeBaseUrl(value) {
|
|
989
|
-
if (typeof value !== 'string') return '';
|
|
990
|
-
return value.trim().replace(/\/+$/g, '');
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function joinApiUrl(baseUrl, pathSuffix) {
|
|
994
|
-
const trimmed = normalizeBaseUrl(baseUrl);
|
|
995
|
-
if (!trimmed) return '';
|
|
996
|
-
const safeSuffix = String(pathSuffix || '').replace(/^\/+/g, '');
|
|
997
|
-
if (!safeSuffix) return trimmed;
|
|
998
|
-
if (/\/v1$/i.test(trimmed)) {
|
|
999
|
-
return `${trimmed}/${safeSuffix}`;
|
|
1000
|
-
}
|
|
1001
|
-
return `${trimmed}/v1/${safeSuffix}`;
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
function buildModelsProbeUrl(baseUrl) {
|
|
1005
|
-
return joinApiUrl(baseUrl, 'models');
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
function normalizeWireApi(value) {
|
|
1009
|
-
const raw = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
1010
|
-
if (!raw) return 'responses';
|
|
1011
|
-
return raw.replace(/[\s-]/g, '_');
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
function buildModelProbeSpec(provider, modelName, baseUrl) {
|
|
1015
|
-
const model = typeof modelName === 'string' ? modelName.trim() : '';
|
|
1016
|
-
if (!model) return null;
|
|
1017
|
-
|
|
1018
|
-
const wireApi = normalizeWireApi(provider && provider.wire_api);
|
|
1019
|
-
if (wireApi === 'chat_completions' || wireApi === 'chat') {
|
|
1020
|
-
return {
|
|
1021
|
-
url: joinApiUrl(baseUrl, 'chat/completions'),
|
|
1022
|
-
body: {
|
|
1023
|
-
model,
|
|
1024
|
-
messages: [{ role: 'user', content: 'ping' }],
|
|
1025
|
-
max_tokens: 1,
|
|
1026
|
-
temperature: 0
|
|
1027
|
-
}
|
|
1028
|
-
};
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
if (wireApi === 'completions') {
|
|
1032
|
-
return {
|
|
1033
|
-
url: joinApiUrl(baseUrl, 'completions'),
|
|
1034
|
-
body: {
|
|
1035
|
-
model,
|
|
1036
|
-
prompt: 'ping',
|
|
1037
|
-
max_tokens: 1,
|
|
1038
|
-
temperature: 0
|
|
1039
|
-
}
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
return {
|
|
1044
|
-
url: joinApiUrl(baseUrl, 'responses'),
|
|
1045
|
-
body: {
|
|
1046
|
-
model,
|
|
1047
|
-
input: 'ping',
|
|
1048
|
-
max_output_tokens: 1
|
|
1049
|
-
}
|
|
1050
|
-
};
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
function probeUrl(targetUrl, options = {}) {
|
|
1054
|
-
return new Promise((resolve) => {
|
|
1055
|
-
let parsed;
|
|
1056
|
-
try {
|
|
1057
|
-
parsed = new URL(targetUrl);
|
|
1058
|
-
} catch (e) {
|
|
1059
|
-
return resolve({ ok: false, error: 'Invalid URL' });
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
const transport = parsed.protocol === 'https:' ? https : http;
|
|
1063
|
-
const headers = {
|
|
1064
|
-
'User-Agent': 'codexmate-health-check',
|
|
1065
|
-
'Accept': 'application/json'
|
|
1066
|
-
};
|
|
1067
|
-
if (options.apiKey) {
|
|
1068
|
-
headers['Authorization'] = `Bearer ${options.apiKey}`;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
1072
|
-
const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
|
|
1073
|
-
const start = Date.now();
|
|
1074
|
-
const req = transport.request(parsed, { method: 'GET', headers }, (res) => {
|
|
1075
|
-
const chunks = [];
|
|
1076
|
-
let size = 0;
|
|
1077
|
-
res.on('data', (chunk) => {
|
|
1078
|
-
if (!chunk) return;
|
|
1079
|
-
size += chunk.length;
|
|
1080
|
-
if (size <= maxBytes) {
|
|
1081
|
-
chunks.push(chunk);
|
|
1082
|
-
}
|
|
1083
|
-
});
|
|
1084
|
-
res.on('end', () => {
|
|
1085
|
-
const body = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
1086
|
-
resolve({
|
|
1087
|
-
ok: true,
|
|
1088
|
-
status: res.statusCode || 0,
|
|
1089
|
-
durationMs: Date.now() - start,
|
|
1090
|
-
body
|
|
1091
|
-
});
|
|
1092
|
-
});
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
req.setTimeout(timeoutMs, () => {
|
|
1096
|
-
req.destroy(new Error('timeout'));
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
req.on('error', (err) => {
|
|
1100
|
-
resolve({
|
|
1101
|
-
ok: false,
|
|
1102
|
-
error: err.message || 'request failed',
|
|
1103
|
-
durationMs: Date.now() - start
|
|
1104
|
-
});
|
|
1105
|
-
});
|
|
1106
|
-
|
|
1107
|
-
req.end();
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function probeJsonPost(targetUrl, body, options = {}) {
|
|
1112
|
-
return new Promise((resolve) => {
|
|
1113
|
-
let parsed;
|
|
1114
|
-
try {
|
|
1115
|
-
parsed = new URL(targetUrl);
|
|
1116
|
-
} catch (e) {
|
|
1117
|
-
return resolve({ ok: false, error: 'Invalid URL' });
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
const transport = parsed.protocol === 'https:' ? https : http;
|
|
1121
|
-
const headers = {
|
|
1122
|
-
'User-Agent': 'codexmate-health-check',
|
|
1123
|
-
'Accept': 'application/json',
|
|
1124
|
-
'Content-Type': 'application/json'
|
|
1125
|
-
};
|
|
1126
|
-
if (options.apiKey) {
|
|
1127
|
-
headers['Authorization'] = `Bearer ${options.apiKey}`;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
const payload = JSON.stringify(body || {});
|
|
1131
|
-
headers['Content-Length'] = Buffer.byteLength(payload);
|
|
1132
|
-
|
|
1133
|
-
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : HEALTH_CHECK_TIMEOUT_MS;
|
|
1134
|
-
const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
|
|
1135
|
-
const start = Date.now();
|
|
1136
|
-
const req = transport.request(parsed, { method: 'POST', headers }, (res) => {
|
|
1137
|
-
const chunks = [];
|
|
1138
|
-
let size = 0;
|
|
1139
|
-
res.on('data', (chunk) => {
|
|
1140
|
-
if (!chunk) return;
|
|
1141
|
-
size += chunk.length;
|
|
1142
|
-
if (size <= maxBytes) {
|
|
1143
|
-
chunks.push(chunk);
|
|
1144
|
-
}
|
|
1145
|
-
});
|
|
1146
|
-
res.on('end', () => {
|
|
1147
|
-
const bodyText = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
|
|
1148
|
-
resolve({
|
|
1149
|
-
ok: true,
|
|
1150
|
-
status: res.statusCode || 0,
|
|
1151
|
-
durationMs: Date.now() - start,
|
|
1152
|
-
body: bodyText
|
|
1153
|
-
});
|
|
1154
|
-
});
|
|
1155
|
-
});
|
|
1156
|
-
|
|
1157
|
-
req.setTimeout(timeoutMs, () => {
|
|
1158
|
-
req.destroy(new Error('timeout'));
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
req.on('error', (err) => {
|
|
1162
|
-
resolve({
|
|
1163
|
-
ok: false,
|
|
1164
|
-
error: err.message || 'request failed',
|
|
1165
|
-
durationMs: Date.now() - start
|
|
1166
|
-
});
|
|
1167
|
-
});
|
|
1168
|
-
|
|
1169
|
-
req.write(payload);
|
|
1170
|
-
req.end();
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function extractModelIds(payload) {
|
|
1175
|
-
const ids = [];
|
|
1176
|
-
const pushValue = (value) => {
|
|
1177
|
-
if (typeof value === 'string' && value.trim()) {
|
|
1178
|
-
ids.push(value.trim());
|
|
1179
|
-
}
|
|
1180
|
-
};
|
|
1181
|
-
|
|
1182
|
-
if (!payload) return ids;
|
|
1183
|
-
|
|
1184
|
-
if (Array.isArray(payload)) {
|
|
1185
|
-
for (const item of payload) {
|
|
1186
|
-
if (item && typeof item === 'object') {
|
|
1187
|
-
pushValue(item.id);
|
|
1188
|
-
pushValue(item.model);
|
|
1189
|
-
pushValue(item.name);
|
|
1190
|
-
} else {
|
|
1191
|
-
pushValue(item);
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
return ids;
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
if (Array.isArray(payload.data)) {
|
|
1198
|
-
for (const item of payload.data) {
|
|
1199
|
-
if (item && typeof item === 'object') {
|
|
1200
|
-
pushValue(item.id);
|
|
1201
|
-
pushValue(item.model);
|
|
1202
|
-
pushValue(item.name);
|
|
1203
|
-
} else {
|
|
1204
|
-
pushValue(item);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
if (Array.isArray(payload.models)) {
|
|
1210
|
-
for (const item of payload.models) {
|
|
1211
|
-
if (item && typeof item === 'object') {
|
|
1212
|
-
pushValue(item.id);
|
|
1213
|
-
pushValue(item.model);
|
|
1214
|
-
pushValue(item.name);
|
|
1215
|
-
} else {
|
|
1216
|
-
pushValue(item);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
return ids;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
777
|
async function runRemoteHealthCheck(provider, modelName, options = {}) {
|
|
1225
778
|
const issues = [];
|
|
1226
779
|
const results = {};
|
|
@@ -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;
|
|
@@ -3179,7 +2663,7 @@ async function cloneCodexSession(params = {}) {
|
|
|
3179
2663
|
};
|
|
3180
2664
|
}
|
|
3181
2665
|
|
|
3182
|
-
function buildSessionMarkdown(payload) {
|
|
2666
|
+
function buildSessionMarkdown(payload) {
|
|
3183
2667
|
const lines = [
|
|
3184
2668
|
'# AI Session Export',
|
|
3185
2669
|
'',
|
|
@@ -3208,75 +2692,37 @@ function buildSessionMarkdown(payload) {
|
|
|
3208
2692
|
lines.push('');
|
|
3209
2693
|
});
|
|
3210
2694
|
|
|
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
|
-
}
|
|
2695
|
+
return lines.join('\n');
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
function buildSessionPlainText(messages) {
|
|
2699
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
2700
|
+
return '';
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
const lines = [];
|
|
2704
|
+
messages.forEach((message) => {
|
|
2705
|
+
const role = normalizeRole(message && message.role) || 'unknown';
|
|
2706
|
+
const text = message && typeof message.text === 'string' ? message.text : '';
|
|
2707
|
+
lines.push(role);
|
|
2708
|
+
lines.push(text);
|
|
2709
|
+
lines.push('');
|
|
2710
|
+
});
|
|
2711
|
+
|
|
2712
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
2713
|
+
lines.pop();
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
return lines.join('\n');
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
function resolveStateMaxMessages(state) {
|
|
2720
|
+
if (!state || typeof state !== 'object') {
|
|
2721
|
+
return MAX_EXPORT_MESSAGES;
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
return resolveMaxMessagesValue(state.maxMessages, MAX_EXPORT_MESSAGES);
|
|
2725
|
+
}
|
|
3280
2726
|
|
|
3281
2727
|
function canAppendMessage(state) {
|
|
3282
2728
|
const maxMessages = resolveStateMaxMessages(state);
|
|
@@ -3313,7 +2759,7 @@ function extractCodexMessageFromRecord(record, state, lineIndex = -1) {
|
|
|
3313
2759
|
}
|
|
3314
2760
|
}
|
|
3315
2761
|
|
|
3316
|
-
function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
2762
|
+
function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
3317
2763
|
if (record.timestamp) {
|
|
3318
2764
|
state.updatedAt = toIsoTime(record.timestamp, state.updatedAt);
|
|
3319
2765
|
}
|
|
@@ -3338,130 +2784,130 @@ function extractClaudeMessageFromRecord(record, state, lineIndex = -1) {
|
|
|
3338
2784
|
recordLineIndex: Number.isInteger(lineIndex) ? lineIndex : -1
|
|
3339
2785
|
});
|
|
3340
2786
|
}
|
|
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
|
-
}
|
|
3410
|
-
|
|
3411
|
-
async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
3412
|
-
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3413
|
-
const state = {
|
|
3414
|
-
sessionId: '',
|
|
3415
|
-
cwd: '',
|
|
3416
|
-
updatedAt: '',
|
|
3417
|
-
messages: [],
|
|
3418
|
-
maxMessages,
|
|
3419
|
-
truncated: false
|
|
3420
|
-
};
|
|
3421
|
-
|
|
3422
|
-
let stream;
|
|
3423
|
-
let rl;
|
|
3424
|
-
try {
|
|
3425
|
-
stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
3426
|
-
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
3427
|
-
|
|
3428
|
-
let lineIndex = 0;
|
|
3429
|
-
let limitReached = false;
|
|
3430
|
-
for await (const line of rl) {
|
|
3431
|
-
const currentLineIndex = lineIndex;
|
|
3432
|
-
lineIndex += 1;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
function recordHasCodexMessage(record) {
|
|
2791
|
+
if (!record || record.type !== 'response_item' || !record.payload) {
|
|
2792
|
+
return false;
|
|
2793
|
+
}
|
|
2794
|
+
if (record.payload.type !== 'message') {
|
|
2795
|
+
return false;
|
|
2796
|
+
}
|
|
2797
|
+
const role = normalizeRole(record.payload.role);
|
|
2798
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
const text = extractMessageText(record.payload.content);
|
|
2802
|
+
return !!text;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
function recordHasClaudeMessage(record) {
|
|
2806
|
+
if (!record) {
|
|
2807
|
+
return false;
|
|
2808
|
+
}
|
|
2809
|
+
const role = normalizeRole(record.type);
|
|
2810
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'system') {
|
|
2811
|
+
return false;
|
|
2812
|
+
}
|
|
2813
|
+
const content = record.message ? record.message.content : '';
|
|
2814
|
+
const text = extractMessageText(content);
|
|
2815
|
+
return !!text;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
function recordHasMessage(record, source) {
|
|
2819
|
+
return source === 'codex'
|
|
2820
|
+
? recordHasCodexMessage(record)
|
|
2821
|
+
: recordHasClaudeMessage(record);
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
function extractMessagesFromRecords(records, source, options = {}) {
|
|
2825
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
2826
|
+
const state = {
|
|
2827
|
+
sessionId: '',
|
|
2828
|
+
cwd: '',
|
|
2829
|
+
updatedAt: '',
|
|
2830
|
+
messages: [],
|
|
2831
|
+
maxMessages,
|
|
2832
|
+
truncated: false
|
|
2833
|
+
};
|
|
2834
|
+
|
|
2835
|
+
for (let lineIndex = 0; lineIndex < records.length; lineIndex++) {
|
|
2836
|
+
const record = records[lineIndex];
|
|
2837
|
+
if (source === 'codex') {
|
|
2838
|
+
extractCodexMessageFromRecord(record, state, lineIndex);
|
|
2839
|
+
} else {
|
|
2840
|
+
extractClaudeMessageFromRecord(record, state, lineIndex);
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
|
|
2844
|
+
for (let i = lineIndex + 1; i < records.length; i++) {
|
|
2845
|
+
if (recordHasMessage(records[i], source)) {
|
|
2846
|
+
state.truncated = true;
|
|
2847
|
+
break;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
break;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
return state;
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
2858
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
2859
|
+
const state = {
|
|
2860
|
+
sessionId: '',
|
|
2861
|
+
cwd: '',
|
|
2862
|
+
updatedAt: '',
|
|
2863
|
+
messages: [],
|
|
2864
|
+
maxMessages,
|
|
2865
|
+
truncated: false
|
|
2866
|
+
};
|
|
2867
|
+
|
|
2868
|
+
let stream;
|
|
2869
|
+
let rl;
|
|
2870
|
+
try {
|
|
2871
|
+
stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
2872
|
+
rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
2873
|
+
|
|
2874
|
+
let lineIndex = 0;
|
|
2875
|
+
let limitReached = false;
|
|
2876
|
+
for await (const line of rl) {
|
|
2877
|
+
const currentLineIndex = lineIndex;
|
|
2878
|
+
lineIndex += 1;
|
|
2879
|
+
|
|
2880
|
+
const trimmed = line.trim();
|
|
2881
|
+
if (!trimmed) continue;
|
|
2882
|
+
|
|
2883
|
+
let record;
|
|
2884
|
+
try {
|
|
2885
|
+
record = JSON.parse(trimmed);
|
|
2886
|
+
} catch (e) {
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
if (limitReached) {
|
|
2891
|
+
if (recordHasMessage(record, source)) {
|
|
2892
|
+
state.truncated = true;
|
|
2893
|
+
break;
|
|
2894
|
+
}
|
|
2895
|
+
continue;
|
|
2896
|
+
}
|
|
3433
2897
|
|
|
3434
|
-
|
|
3435
|
-
|
|
2898
|
+
if (source === 'codex') {
|
|
2899
|
+
extractCodexMessageFromRecord(record, state, currentLineIndex);
|
|
2900
|
+
} else {
|
|
2901
|
+
extractClaudeMessageFromRecord(record, state, currentLineIndex);
|
|
2902
|
+
}
|
|
3436
2903
|
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
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 });
|
|
2904
|
+
if (state.maxMessages !== Infinity && state.messages.length >= state.maxMessages) {
|
|
2905
|
+
limitReached = true;
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
} catch (e) {
|
|
2909
|
+
const fallbackRecords = readJsonlRecords(filePath);
|
|
2910
|
+
return extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
3465
2911
|
} finally {
|
|
3466
2912
|
if (rl) {
|
|
3467
2913
|
try { rl.close(); } catch (e) {}
|
|
@@ -3474,7 +2920,7 @@ async function extractMessagesFromFile(filePath, source, options = {}) {
|
|
|
3474
2920
|
return state;
|
|
3475
2921
|
}
|
|
3476
2922
|
|
|
3477
|
-
async function readSessionDetail(params = {}) {
|
|
2923
|
+
async function readSessionDetail(params = {}) {
|
|
3478
2924
|
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3479
2925
|
if (!source) {
|
|
3480
2926
|
return { error: 'Invalid source' };
|
|
@@ -3501,83 +2947,83 @@ async function readSessionDetail(params = {}) {
|
|
|
3501
2947
|
const startIndex = Math.max(0, allMessages.length - messageLimit);
|
|
3502
2948
|
const clippedMessages = allMessages.slice(startIndex);
|
|
3503
2949
|
|
|
3504
|
-
return {
|
|
3505
|
-
source,
|
|
3506
|
-
sourceLabel,
|
|
3507
|
-
sessionId,
|
|
2950
|
+
return {
|
|
2951
|
+
source,
|
|
2952
|
+
sourceLabel,
|
|
2953
|
+
sessionId,
|
|
3508
2954
|
cwd: extracted.cwd || '',
|
|
3509
2955
|
updatedAt: extracted.updatedAt || '',
|
|
3510
2956
|
totalMessages: allMessages.length,
|
|
3511
2957
|
clipped: allMessages.length > clippedMessages.length,
|
|
3512
2958
|
messageLimit,
|
|
3513
2959
|
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
|
-
}
|
|
2960
|
+
filePath
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
async function readSessionPlain(params = {}) {
|
|
2965
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
2966
|
+
if (!source) {
|
|
2967
|
+
return { error: 'Invalid source' };
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
2971
|
+
if (!filePath) {
|
|
2972
|
+
return { error: 'Session file not found' };
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
let extracted;
|
|
2976
|
+
try {
|
|
2977
|
+
extracted = await extractMessagesFromFile(filePath, source, { maxMessages: Infinity });
|
|
2978
|
+
} catch (e) {
|
|
2979
|
+
extracted = null;
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
if (!extracted) {
|
|
2983
|
+
return { error: 'Failed to parse session file' };
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
2987
|
+
const fallbackRecords = readJsonlRecords(filePath);
|
|
2988
|
+
if (fallbackRecords.length === 0) {
|
|
2989
|
+
return { error: 'Session file is empty' };
|
|
2990
|
+
}
|
|
2991
|
+
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages: Infinity });
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
const sessionId = extracted.sessionId || params.sessionId || path.basename(filePath, '.jsonl');
|
|
2995
|
+
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
2996
|
+
const messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
2997
|
+
const text = buildSessionPlainText(messages);
|
|
2998
|
+
|
|
2999
|
+
return {
|
|
3000
|
+
source,
|
|
3001
|
+
sourceLabel,
|
|
3002
|
+
sessionId,
|
|
3003
|
+
title: sessionId,
|
|
3004
|
+
filePath,
|
|
3005
|
+
text
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
async function exportSessionData(params = {}) {
|
|
3010
|
+
const source = params.source === 'claude' ? 'claude' : (params.source === 'codex' ? 'codex' : '');
|
|
3011
|
+
if (!source) {
|
|
3012
|
+
return { error: 'Invalid source' };
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
const maxMessages = resolveMaxMessagesValue(params.maxMessages, MAX_EXPORT_MESSAGES);
|
|
3016
|
+
const filePath = resolveSessionFilePath(source, params.filePath, params.sessionId);
|
|
3017
|
+
if (!filePath) {
|
|
3018
|
+
return { error: 'Session file not found' };
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
let extracted;
|
|
3022
|
+
try {
|
|
3023
|
+
extracted = await extractMessagesFromFile(filePath, source, { maxMessages });
|
|
3024
|
+
} catch (e) {
|
|
3025
|
+
extracted = null;
|
|
3026
|
+
}
|
|
3581
3027
|
|
|
3582
3028
|
if (!extracted) {
|
|
3583
3029
|
return { error: 'Failed to parse session file' };
|
|
@@ -3585,11 +3031,11 @@ async function exportSessionData(params = {}) {
|
|
|
3585
3031
|
|
|
3586
3032
|
if ((!extracted.messages || extracted.messages.length === 0) && !extracted.sessionId && !extracted.cwd) {
|
|
3587
3033
|
const fallbackRecords = readJsonlRecords(filePath);
|
|
3588
|
-
if (fallbackRecords.length === 0) {
|
|
3589
|
-
return { error: 'Session file is empty' };
|
|
3590
|
-
}
|
|
3591
|
-
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
3592
|
-
}
|
|
3034
|
+
if (fallbackRecords.length === 0) {
|
|
3035
|
+
return { error: 'Session file is empty' };
|
|
3036
|
+
}
|
|
3037
|
+
extracted = extractMessagesFromRecords(fallbackRecords, source, { maxMessages });
|
|
3038
|
+
}
|
|
3593
3039
|
|
|
3594
3040
|
extracted.messages = removeLeadingSystemMessage(Array.isArray(extracted.messages) ? extracted.messages : []);
|
|
3595
3041
|
|
|
@@ -3601,29 +3047,29 @@ async function exportSessionData(params = {}) {
|
|
|
3601
3047
|
}
|
|
3602
3048
|
|
|
3603
3049
|
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,
|
|
3050
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
3051
|
+
const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
|
|
3052
|
+
const truncated = !!extracted.truncated;
|
|
3053
|
+
const maxMessagesLabel = maxMessages === Infinity ? 'all' : maxMessages;
|
|
3054
|
+
const markdown = buildSessionMarkdown({
|
|
3055
|
+
sourceLabel,
|
|
3056
|
+
sessionId,
|
|
3057
|
+
updatedAt: extracted.updatedAt,
|
|
3058
|
+
cwd: extracted.cwd,
|
|
3613
3059
|
filePath,
|
|
3614
3060
|
messages: extracted.messages
|
|
3615
3061
|
});
|
|
3616
3062
|
|
|
3617
|
-
return {
|
|
3618
|
-
source,
|
|
3619
|
-
sourceLabel,
|
|
3620
|
-
sessionId,
|
|
3621
|
-
fileName: `${source}-session-${safeSessionId}.md`,
|
|
3622
|
-
content: markdown,
|
|
3623
|
-
truncated,
|
|
3624
|
-
maxMessages: maxMessagesLabel
|
|
3625
|
-
};
|
|
3626
|
-
}
|
|
3063
|
+
return {
|
|
3064
|
+
source,
|
|
3065
|
+
sourceLabel,
|
|
3066
|
+
sessionId,
|
|
3067
|
+
fileName: `${source}-session-${safeSessionId}.md`,
|
|
3068
|
+
content: markdown,
|
|
3069
|
+
truncated,
|
|
3070
|
+
maxMessages: maxMessagesLabel
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3627
3073
|
|
|
3628
3074
|
function buildExportPayload(includeKeys) {
|
|
3629
3075
|
const { config } = readConfigOrVirtualDefault();
|
|
@@ -3646,6 +3092,54 @@ function buildExportPayload(includeKeys) {
|
|
|
3646
3092
|
};
|
|
3647
3093
|
}
|
|
3648
3094
|
|
|
3095
|
+
function buildClaudeSharePayload(config = {}) {
|
|
3096
|
+
const apiKey = typeof config.apiKey === 'string' ? config.apiKey : '';
|
|
3097
|
+
const baseUrl = typeof config.baseUrl === 'string' ? config.baseUrl : '';
|
|
3098
|
+
const model = typeof config.model === 'string' ? config.model : '';
|
|
3099
|
+
|
|
3100
|
+
if (!baseUrl) return { error: 'Claude Base URL 未设置' };
|
|
3101
|
+
if (!apiKey) return { error: 'Claude API 密钥未设置' };
|
|
3102
|
+
|
|
3103
|
+
return {
|
|
3104
|
+
payload: {
|
|
3105
|
+
baseUrl: baseUrl.trim(),
|
|
3106
|
+
apiKey: apiKey.trim(),
|
|
3107
|
+
model: (model && model.trim()) || DEFAULT_CLAUDE_MODEL
|
|
3108
|
+
}
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
function buildProviderSharePayload(params = {}) {
|
|
3113
|
+
const name = typeof params.name === 'string' ? params.name.trim() : '';
|
|
3114
|
+
if (!name) {
|
|
3115
|
+
return { error: '缺少提供商名称' };
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
const { config } = readConfigOrVirtualDefault();
|
|
3119
|
+
const providers = config.model_providers || {};
|
|
3120
|
+
const provider = providers[name];
|
|
3121
|
+
if (!provider || typeof provider !== 'object') {
|
|
3122
|
+
return { error: `提供商不存在: ${name}` };
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
|
|
3126
|
+
const apiKey = typeof provider.preferred_auth_method === 'string'
|
|
3127
|
+
? provider.preferred_auth_method
|
|
3128
|
+
: '';
|
|
3129
|
+
|
|
3130
|
+
if (!baseUrl) {
|
|
3131
|
+
return { error: `提供商 ${name} 缺少 base_url` };
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
return {
|
|
3135
|
+
payload: {
|
|
3136
|
+
name,
|
|
3137
|
+
baseUrl,
|
|
3138
|
+
apiKey
|
|
3139
|
+
}
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3649
3143
|
function normalizeImportPayload(payload) {
|
|
3650
3144
|
if (!payload || typeof payload !== 'object') {
|
|
3651
3145
|
return { error: 'Invalid import payload' };
|
|
@@ -4464,7 +3958,7 @@ function applyToClaudeSettings(config = {}) {
|
|
|
4464
3958
|
}
|
|
4465
3959
|
|
|
4466
3960
|
const baseUrl = (config.baseUrl || 'https://open.bigmodel.cn/api/anthropic').trim();
|
|
4467
|
-
const model = (config.model ||
|
|
3961
|
+
const model = (config.model || DEFAULT_CLAUDE_MODEL).trim();
|
|
4468
3962
|
const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
|
|
4469
3963
|
if (!readResult.ok) {
|
|
4470
3964
|
return { success: false, mode: 'settings-file', error: readResult.error };
|
|
@@ -4516,6 +4010,76 @@ function applyToClaudeSettings(config = {}) {
|
|
|
4516
4010
|
}
|
|
4517
4011
|
}
|
|
4518
4012
|
|
|
4013
|
+
function readClaudeSettingsInfo() {
|
|
4014
|
+
const readResult = readJsonObjectFromFile(CLAUDE_SETTINGS_FILE, {});
|
|
4015
|
+
if (!readResult.ok) {
|
|
4016
|
+
return {
|
|
4017
|
+
error: readResult.error || '读取 Claude 配置失败',
|
|
4018
|
+
exists: !!readResult.exists,
|
|
4019
|
+
path: CLAUDE_SETTINGS_FILE
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
const settings = readResult.data || {};
|
|
4024
|
+
const env = (settings.env && typeof settings.env === 'object' && !Array.isArray(settings.env))
|
|
4025
|
+
? settings.env
|
|
4026
|
+
: {};
|
|
4027
|
+
|
|
4028
|
+
return {
|
|
4029
|
+
exists: !!readResult.exists,
|
|
4030
|
+
path: CLAUDE_SETTINGS_FILE,
|
|
4031
|
+
apiKey: typeof env.ANTHROPIC_API_KEY === 'string' ? env.ANTHROPIC_API_KEY : '',
|
|
4032
|
+
baseUrl: typeof env.ANTHROPIC_BASE_URL === 'string' ? env.ANTHROPIC_BASE_URL : '',
|
|
4033
|
+
model: typeof env.ANTHROPIC_MODEL === 'string' ? env.ANTHROPIC_MODEL : '',
|
|
4034
|
+
env
|
|
4035
|
+
};
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
// CLI: 一行写入 Claude Code 配置
|
|
4039
|
+
function cmdClaude(baseUrl, apiKey, model, silent = false) {
|
|
4040
|
+
const normalizedBaseUrl = typeof baseUrl === 'string' ? baseUrl.trim() : '';
|
|
4041
|
+
const normalizedKey = typeof apiKey === 'string' ? apiKey.trim() : '';
|
|
4042
|
+
const normalizedModel = typeof model === 'string' && model.trim()
|
|
4043
|
+
? model.trim()
|
|
4044
|
+
: DEFAULT_CLAUDE_MODEL;
|
|
4045
|
+
|
|
4046
|
+
if (!normalizedBaseUrl || !normalizedKey) {
|
|
4047
|
+
if (!silent) {
|
|
4048
|
+
console.error('用法: codexmate claude <BaseURL> <API密钥> [模型]');
|
|
4049
|
+
console.log('\n示例:');
|
|
4050
|
+
console.log(' codexmate claude https://open.bigmodel.cn/api/anthropic sk-ant-xxx glm-4.7');
|
|
4051
|
+
}
|
|
4052
|
+
throw new Error('BaseURL 和 API 密钥必填');
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
const result = applyToClaudeSettings({
|
|
4056
|
+
baseUrl: normalizedBaseUrl,
|
|
4057
|
+
apiKey: normalizedKey,
|
|
4058
|
+
model: normalizedModel
|
|
4059
|
+
});
|
|
4060
|
+
|
|
4061
|
+
if (!result || result.success === false) {
|
|
4062
|
+
const message = (result && result.error) || '应用 Claude 配置失败';
|
|
4063
|
+
if (!silent) console.error('错误:', message);
|
|
4064
|
+
throw new Error(message);
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
if (!silent) {
|
|
4068
|
+
console.log('✓ 已写入 Claude Code 配置');
|
|
4069
|
+
console.log(' Base URL:', normalizedBaseUrl);
|
|
4070
|
+
console.log(' 模型:', normalizedModel);
|
|
4071
|
+
if (result.targetPath) {
|
|
4072
|
+
console.log(' 目标文件:', result.targetPath);
|
|
4073
|
+
}
|
|
4074
|
+
if (result.backupPath) {
|
|
4075
|
+
console.log(' 已自动备份:', result.backupPath);
|
|
4076
|
+
}
|
|
4077
|
+
console.log();
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
return result;
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4519
4083
|
function commandExists(command, args = '') {
|
|
4520
4084
|
try {
|
|
4521
4085
|
execSync(`${command} ${args}`, { stdio: 'ignore' });
|
|
@@ -4649,7 +4213,7 @@ async function cmdZip(targetPath, options = {}) {
|
|
|
4649
4213
|
}
|
|
4650
4214
|
|
|
4651
4215
|
// 解压(7-Zip 优先)
|
|
4652
|
-
async function cmdUnzip(zipPath, outputDir) {
|
|
4216
|
+
async function cmdUnzip(zipPath, outputDir) {
|
|
4653
4217
|
if (!zipPath) {
|
|
4654
4218
|
console.error('用法: codexmate unzip <zip文件路径> [输出目录]');
|
|
4655
4219
|
console.log('\n示例:');
|
|
@@ -4705,224 +4269,224 @@ async function cmdUnzip(zipPath, outputDir) {
|
|
|
4705
4269
|
} catch (e) {
|
|
4706
4270
|
console.error('解压失败:', e.message);
|
|
4707
4271
|
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
|
-
if (!fs.existsSync(htmlPath)) {
|
|
4923
|
-
console.error('错误: web-ui.html 不存在');
|
|
4924
|
-
process.exit(1);
|
|
4925
|
-
}
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
function resolveExportOutputPath(outputPath, defaultFileName) {
|
|
4276
|
+
const fallback = path.resolve(process.cwd(), defaultFileName);
|
|
4277
|
+
if (typeof outputPath !== 'string' || !outputPath.trim()) {
|
|
4278
|
+
return fallback;
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
const trimmed = outputPath.trim();
|
|
4282
|
+
const resolved = path.resolve(trimmed);
|
|
4283
|
+
const hasTrailingSep = /[\\\/]$/.test(trimmed);
|
|
4284
|
+
if (hasTrailingSep) {
|
|
4285
|
+
ensureDir(resolved);
|
|
4286
|
+
return path.join(resolved, defaultFileName);
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4289
|
+
if (fs.existsSync(resolved)) {
|
|
4290
|
+
try {
|
|
4291
|
+
const stat = fs.statSync(resolved);
|
|
4292
|
+
if (stat.isDirectory()) {
|
|
4293
|
+
return path.join(resolved, defaultFileName);
|
|
4294
|
+
}
|
|
4295
|
+
} catch (e) {}
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4298
|
+
return resolved;
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
function printExportSessionUsage() {
|
|
4302
|
+
console.log('\n用法: codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
4303
|
+
console.log('\n示例:');
|
|
4304
|
+
console.log(' codexmate export-session --source codex --session-id 123456');
|
|
4305
|
+
console.log(' codexmate export-session --source claude --file "~/.claude/projects/demo/session.jsonl"');
|
|
4306
|
+
console.log(' codexmate export-session --source codex --session-id 123456 --max-messages=all');
|
|
4307
|
+
}
|
|
4308
|
+
|
|
4309
|
+
function parseExportSessionArgs(args = []) {
|
|
4310
|
+
const options = {
|
|
4311
|
+
source: '',
|
|
4312
|
+
sessionId: '',
|
|
4313
|
+
filePath: '',
|
|
4314
|
+
output: '',
|
|
4315
|
+
maxMessages: undefined
|
|
4316
|
+
};
|
|
4317
|
+
const errors = [];
|
|
4318
|
+
|
|
4319
|
+
for (let i = 0; i < args.length; i++) {
|
|
4320
|
+
const arg = args[i];
|
|
4321
|
+
if (!arg) continue;
|
|
4322
|
+
|
|
4323
|
+
if (arg.startsWith('--source=')) {
|
|
4324
|
+
options.source = arg.slice('--source='.length);
|
|
4325
|
+
continue;
|
|
4326
|
+
}
|
|
4327
|
+
if (arg === '--source') {
|
|
4328
|
+
options.source = args[i + 1] || '';
|
|
4329
|
+
i += 1;
|
|
4330
|
+
continue;
|
|
4331
|
+
}
|
|
4332
|
+
if (arg.startsWith('--session-id=')) {
|
|
4333
|
+
options.sessionId = arg.slice('--session-id='.length);
|
|
4334
|
+
continue;
|
|
4335
|
+
}
|
|
4336
|
+
if (arg === '--session-id') {
|
|
4337
|
+
options.sessionId = args[i + 1] || '';
|
|
4338
|
+
i += 1;
|
|
4339
|
+
continue;
|
|
4340
|
+
}
|
|
4341
|
+
if (arg.startsWith('--file=')) {
|
|
4342
|
+
options.filePath = arg.slice('--file='.length);
|
|
4343
|
+
continue;
|
|
4344
|
+
}
|
|
4345
|
+
if (arg === '--file') {
|
|
4346
|
+
options.filePath = args[i + 1] || '';
|
|
4347
|
+
i += 1;
|
|
4348
|
+
continue;
|
|
4349
|
+
}
|
|
4350
|
+
if (arg.startsWith('--output=')) {
|
|
4351
|
+
options.output = arg.slice('--output='.length);
|
|
4352
|
+
continue;
|
|
4353
|
+
}
|
|
4354
|
+
if (arg === '--output') {
|
|
4355
|
+
options.output = args[i + 1] || '';
|
|
4356
|
+
i += 1;
|
|
4357
|
+
continue;
|
|
4358
|
+
}
|
|
4359
|
+
if (arg.startsWith('--max-messages=')) {
|
|
4360
|
+
options.maxMessages = arg.slice('--max-messages='.length);
|
|
4361
|
+
continue;
|
|
4362
|
+
}
|
|
4363
|
+
if (arg === '--max-messages') {
|
|
4364
|
+
options.maxMessages = args[i + 1] || '';
|
|
4365
|
+
i += 1;
|
|
4366
|
+
continue;
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
errors.push(`未知参数: ${arg}`);
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
const normalizedSource = options.source.trim().toLowerCase();
|
|
4373
|
+
if (normalizedSource && normalizedSource !== 'codex' && normalizedSource !== 'claude') {
|
|
4374
|
+
errors.push('参数 --source 仅支持 codex 或 claude');
|
|
4375
|
+
}
|
|
4376
|
+
options.source = normalizedSource;
|
|
4377
|
+
|
|
4378
|
+
if (!options.source) {
|
|
4379
|
+
errors.push('缺少 --source');
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
if (!options.sessionId && !options.filePath) {
|
|
4383
|
+
errors.push('必须指定 --session-id 或 --file');
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
if (options.maxMessages !== undefined) {
|
|
4387
|
+
const parsed = parseMaxMessagesValue(options.maxMessages);
|
|
4388
|
+
if (parsed === null) {
|
|
4389
|
+
errors.push('参数 --max-messages 无效');
|
|
4390
|
+
} else {
|
|
4391
|
+
options.maxMessages = parsed === Infinity ? Infinity : Math.max(1, Math.floor(parsed));
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
|
|
4395
|
+
return {
|
|
4396
|
+
options,
|
|
4397
|
+
error: errors.length > 0 ? errors.join(';') : ''
|
|
4398
|
+
};
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
async function cmdExportSession(args = []) {
|
|
4402
|
+
const parsed = parseExportSessionArgs(args);
|
|
4403
|
+
if (parsed.error) {
|
|
4404
|
+
console.error('错误:', parsed.error);
|
|
4405
|
+
printExportSessionUsage();
|
|
4406
|
+
process.exit(1);
|
|
4407
|
+
}
|
|
4408
|
+
|
|
4409
|
+
const options = parsed.options;
|
|
4410
|
+
const maxMessages = resolveMaxMessagesValue(options.maxMessages, MAX_EXPORT_MESSAGES);
|
|
4411
|
+
let result;
|
|
4412
|
+
try {
|
|
4413
|
+
result = await exportSessionData({
|
|
4414
|
+
source: options.source,
|
|
4415
|
+
sessionId: options.sessionId,
|
|
4416
|
+
filePath: options.filePath,
|
|
4417
|
+
maxMessages
|
|
4418
|
+
});
|
|
4419
|
+
} catch (e) {
|
|
4420
|
+
console.error('导出失败:', e.message || e);
|
|
4421
|
+
process.exit(1);
|
|
4422
|
+
}
|
|
4423
|
+
|
|
4424
|
+
if (result && result.error) {
|
|
4425
|
+
console.error('导出失败:', result.error);
|
|
4426
|
+
process.exit(1);
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
const defaultFileName = (result && result.fileName)
|
|
4430
|
+
? result.fileName
|
|
4431
|
+
: `${options.source}-session-${options.sessionId || Date.now()}.md`;
|
|
4432
|
+
const outputPath = resolveExportOutputPath(options.output, defaultFileName);
|
|
4433
|
+
ensureDir(path.dirname(outputPath));
|
|
4434
|
+
fs.writeFileSync(outputPath, (result && result.content) ? result.content : '', 'utf-8');
|
|
4435
|
+
|
|
4436
|
+
console.log('\n✓ 会话已导出:', outputPath);
|
|
4437
|
+
if (result && result.truncated) {
|
|
4438
|
+
const label = maxMessages === Infinity ? 'all' : maxMessages;
|
|
4439
|
+
console.log(`! 已截断: 仅导出前 ${label} 条消息`);
|
|
4440
|
+
console.log(' 可使用 --max-messages=all 导出完整内容');
|
|
4441
|
+
}
|
|
4442
|
+
console.log();
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
function parseStartOptions(args = []) {
|
|
4446
|
+
const options = { host: '' };
|
|
4447
|
+
if (!Array.isArray(args)) {
|
|
4448
|
+
return options;
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4451
|
+
for (let i = 0; i < args.length; i++) {
|
|
4452
|
+
const arg = args[i];
|
|
4453
|
+
if (!arg) continue;
|
|
4454
|
+
if (arg.startsWith('--host=')) {
|
|
4455
|
+
options.host = arg.slice('--host='.length);
|
|
4456
|
+
continue;
|
|
4457
|
+
}
|
|
4458
|
+
if (arg === '--host') {
|
|
4459
|
+
options.host = args[i + 1] || '';
|
|
4460
|
+
i += 1;
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
return options;
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
function isAnyAddressHost(host) {
|
|
4468
|
+
return host === '0.0.0.0' || host === '::';
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
function formatHostForUrl(host) {
|
|
4472
|
+
const value = typeof host === 'string' ? host.trim() : '';
|
|
4473
|
+
if (!value) return '';
|
|
4474
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
4475
|
+
return value;
|
|
4476
|
+
}
|
|
4477
|
+
if (value.includes(':')) {
|
|
4478
|
+
return `[${value}]`;
|
|
4479
|
+
}
|
|
4480
|
+
return value;
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
// 打开 Web UI
|
|
4484
|
+
function cmdStart(options = {}) {
|
|
4485
|
+
const htmlPath = path.join(__dirname, 'web-ui.html');
|
|
4486
|
+
if (!fs.existsSync(htmlPath)) {
|
|
4487
|
+
console.error('错误: web-ui.html 不存在');
|
|
4488
|
+
process.exit(1);
|
|
4489
|
+
}
|
|
4926
4490
|
|
|
4927
4491
|
const server = http.createServer((req, res) => {
|
|
4928
4492
|
if (req.url === '/api') {
|
|
@@ -4937,9 +4501,11 @@ function cmdStart(options = {}) {
|
|
|
4937
4501
|
case 'status':
|
|
4938
4502
|
const statusConfigResult = readConfigOrVirtualDefault();
|
|
4939
4503
|
const config = statusConfigResult.config;
|
|
4504
|
+
const serviceTier = typeof config.service_tier === 'string' ? config.service_tier.trim() : '';
|
|
4940
4505
|
result = {
|
|
4941
4506
|
provider: config.model_provider || '未设置',
|
|
4942
4507
|
model: config.model || '未设置',
|
|
4508
|
+
serviceTier,
|
|
4943
4509
|
configReady: !statusConfigResult.isVirtual,
|
|
4944
4510
|
configNotice: statusConfigResult.reason || '',
|
|
4945
4511
|
initNotice: consumeInitNotice()
|
|
@@ -5039,9 +4605,18 @@ function cmdStart(options = {}) {
|
|
|
5039
4605
|
cmdDeleteModel(params.model, true);
|
|
5040
4606
|
result = { success: true };
|
|
5041
4607
|
break;
|
|
4608
|
+
case 'get-claude-settings':
|
|
4609
|
+
result = readClaudeSettingsInfo();
|
|
4610
|
+
break;
|
|
5042
4611
|
case 'apply-claude-config':
|
|
5043
4612
|
result = applyToClaudeSettings(params.config);
|
|
5044
4613
|
break;
|
|
4614
|
+
case 'export-claude-share':
|
|
4615
|
+
result = buildClaudeSharePayload(params && params.config ? params.config : {});
|
|
4616
|
+
break;
|
|
4617
|
+
case 'export-provider':
|
|
4618
|
+
result = buildProviderSharePayload(params || {});
|
|
4619
|
+
break;
|
|
5045
4620
|
case 'export-config':
|
|
5046
4621
|
result = {
|
|
5047
4622
|
data: buildExportPayload(!!params.includeKeys)
|
|
@@ -5075,18 +4650,18 @@ function cmdStart(options = {}) {
|
|
|
5075
4650
|
case 'delete-session':
|
|
5076
4651
|
result = await deleteSessionData(params || {});
|
|
5077
4652
|
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
|
-
}
|
|
4653
|
+
case 'clone-session':
|
|
4654
|
+
result = await cloneCodexSession(params || {});
|
|
4655
|
+
break;
|
|
4656
|
+
case 'session-detail':
|
|
4657
|
+
result = await readSessionDetail(params);
|
|
4658
|
+
break;
|
|
4659
|
+
case 'session-plain':
|
|
4660
|
+
result = await readSessionPlain(params);
|
|
4661
|
+
break;
|
|
4662
|
+
default:
|
|
4663
|
+
result = { error: '未知操作' };
|
|
4664
|
+
}
|
|
5090
4665
|
|
|
5091
4666
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
5092
4667
|
res.end(JSON.stringify(result));
|
|
@@ -5102,28 +4677,28 @@ function cmdStart(options = {}) {
|
|
|
5102
4677
|
}
|
|
5103
4678
|
});
|
|
5104
4679
|
|
|
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}"`;
|
|
4680
|
+
const port = resolveWebPort();
|
|
4681
|
+
const host = resolveWebHost(options);
|
|
4682
|
+
const openHost = isAnyAddressHost(host) ? DEFAULT_WEB_HOST : host;
|
|
4683
|
+
const openUrl = `http://${formatHostForUrl(openHost)}:${port}`;
|
|
4684
|
+
server.listen(port, host, () => {
|
|
4685
|
+
console.log('\n✓ Web UI 已启动:', openUrl);
|
|
4686
|
+
if (host && host !== openHost) {
|
|
4687
|
+
console.log(' 监听地址:', host);
|
|
4688
|
+
}
|
|
4689
|
+
console.log(' 按 Ctrl+C 退出\n');
|
|
4690
|
+
if (isAnyAddressHost(host)) {
|
|
4691
|
+
console.warn('! 安全提示: 当前监听所有网卡(无鉴权)。');
|
|
4692
|
+
console.warn(' 建议仅在可信网络使用,或改用 --host 127.0.0.1。');
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4695
|
+
// 打开浏览器
|
|
4696
|
+
const platform = process.platform;
|
|
4697
|
+
let command;
|
|
4698
|
+
const url = openUrl;
|
|
4699
|
+
|
|
4700
|
+
if (platform === 'win32') {
|
|
4701
|
+
command = `start "" "${url}"`;
|
|
5127
4702
|
} else if (platform === 'darwin') {
|
|
5128
4703
|
command = `open "${url}"`;
|
|
5129
4704
|
} else {
|
|
@@ -5136,46 +4711,46 @@ function cmdStart(options = {}) {
|
|
|
5136
4711
|
if (error) console.warn('无法自动打开浏览器,请手动访问:', url);
|
|
5137
4712
|
});
|
|
5138
4713
|
}
|
|
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() {
|
|
4714
|
+
});
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
async function cmdCodex(args = []) {
|
|
4718
|
+
const extraArgs = Array.isArray(args) ? args.filter(arg => arg !== undefined) : [];
|
|
4719
|
+
const hasYolo = extraArgs.includes('--yolo');
|
|
4720
|
+
const finalArgs = hasYolo ? extraArgs : ['--yolo', ...extraArgs];
|
|
4721
|
+
|
|
4722
|
+
return new Promise((resolve, reject) => {
|
|
4723
|
+
const child = spawn('codex', finalArgs, {
|
|
4724
|
+
stdio: 'inherit',
|
|
4725
|
+
shell: process.platform === 'win32'
|
|
4726
|
+
});
|
|
4727
|
+
|
|
4728
|
+
child.on('error', (err) => {
|
|
4729
|
+
reject(new Error(`无法启动 codex,请确认已安装并在 PATH 中: ${err.message}`));
|
|
4730
|
+
});
|
|
4731
|
+
|
|
4732
|
+
child.on('exit', (code, signal) => {
|
|
4733
|
+
if (typeof code === 'number') {
|
|
4734
|
+
resolve(code);
|
|
4735
|
+
return;
|
|
4736
|
+
}
|
|
4737
|
+
if (signal === 'SIGINT') {
|
|
4738
|
+
resolve(130);
|
|
4739
|
+
return;
|
|
4740
|
+
}
|
|
4741
|
+
if (signal === 'SIGTERM') {
|
|
4742
|
+
resolve(143);
|
|
4743
|
+
return;
|
|
4744
|
+
}
|
|
4745
|
+
resolve(1);
|
|
4746
|
+
});
|
|
4747
|
+
});
|
|
4748
|
+
}
|
|
4749
|
+
|
|
4750
|
+
// ============================================================================
|
|
4751
|
+
// 主程序
|
|
4752
|
+
// ============================================================================
|
|
4753
|
+
async function main() {
|
|
5179
4754
|
const bootstrap = ensureManagedConfigBootstrap();
|
|
5180
4755
|
if (bootstrap && bootstrap.notice) {
|
|
5181
4756
|
console.log(`\n[Init] ${bootstrap.notice}`);
|
|
@@ -5193,16 +4768,17 @@ async function main() {
|
|
|
5193
4768
|
console.log(' codexmate use <模型> 切换模型');
|
|
5194
4769
|
console.log(' codexmate add <名称> <URL> [密钥]');
|
|
5195
4770
|
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
|
-
|
|
4771
|
+
console.log(' codexmate claude <BaseURL> <API密钥> [模型] 写入 Claude Code 配置');
|
|
4772
|
+
console.log(' codexmate add-model <模型> 添加模型');
|
|
4773
|
+
console.log(' codexmate delete-model <模型> 删除模型');
|
|
4774
|
+
console.log(' codexmate run [--host <HOST>] 启动 Web 界面');
|
|
4775
|
+
console.log(' codexmate codex [参数...] 等同于 codex --yolo');
|
|
4776
|
+
console.log(' codexmate export-session --source <codex|claude> (--session-id <ID>|--file <PATH>) [--output <PATH>] [--max-messages <N|all|Infinity>]');
|
|
4777
|
+
console.log(' codexmate zip <路径> [--max:级别] 压缩(7-Zip 优先)');
|
|
4778
|
+
console.log(' codexmate unzip <zip文件> [输出目录] 解压(7-Zip 优先)');
|
|
4779
|
+
console.log('');
|
|
4780
|
+
process.exit(0);
|
|
4781
|
+
}
|
|
5206
4782
|
|
|
5207
4783
|
const command = args[0];
|
|
5208
4784
|
|
|
@@ -5215,18 +4791,23 @@ async function main() {
|
|
|
5215
4791
|
case 'use': cmdUseModel(args[1]); break;
|
|
5216
4792
|
case 'add': cmdAdd(args[1], args[2], args[3]); break;
|
|
5217
4793
|
case 'delete': cmdDelete(args[1]); break;
|
|
5218
|
-
case '
|
|
5219
|
-
case '
|
|
5220
|
-
case '
|
|
5221
|
-
case '
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
case '
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
4794
|
+
case 'claude': cmdClaude(args[1], args[2], args[3]); break;
|
|
4795
|
+
case 'add-model': cmdAddModel(args[1]); break;
|
|
4796
|
+
case 'delete-model': cmdDeleteModel(args[1]); break;
|
|
4797
|
+
case 'run': cmdStart(parseStartOptions(args.slice(1))); break;
|
|
4798
|
+
case 'start':
|
|
4799
|
+
console.error('错误: 命令已更名为 "run",请使用: codexmate run');
|
|
4800
|
+
process.exit(1);
|
|
4801
|
+
break;
|
|
4802
|
+
case 'codex': {
|
|
4803
|
+
const exitCode = await cmdCodex(args.slice(1));
|
|
4804
|
+
process.exit(exitCode);
|
|
4805
|
+
break;
|
|
4806
|
+
}
|
|
4807
|
+
case 'export-session': await cmdExportSession(args.slice(1)); break;
|
|
4808
|
+
case 'zip': {
|
|
4809
|
+
// 解析 --max:N 参数
|
|
4810
|
+
const zipOptions = {};
|
|
5230
4811
|
let targetPath = null;
|
|
5231
4812
|
for (let i = 1; i < args.length; i++) {
|
|
5232
4813
|
const arg = args[i];
|
|
@@ -5251,4 +4832,3 @@ main().catch((err) => {
|
|
|
5251
4832
|
console.error('错误:', err && err.message ? err.message : err);
|
|
5252
4833
|
process.exit(1);
|
|
5253
4834
|
});
|
|
5254
|
-
|