evolclaw-web 1.2.2 → 1.2.3

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.
@@ -0,0 +1,618 @@
1
+ /**
2
+ * Codex 会话数据源 — Codex thread 历史展示。
3
+ *
4
+ * 数据源:
5
+ * - ~/.codex/state_*.sqlite(元数据索引)
6
+ * - ~/.codex/sessions/YYYY/MM/DD/*.jsonl(rollout 文件)
7
+ *
8
+ * 与 Claude session source 接口对齐,返回相同的数据结构。
9
+ */
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import os from 'os';
13
+ import { createRequire } from 'module';
14
+ import { resolvePaths } from '../paths.js';
15
+ import { encodePath, scanChatDirs, readJsonFile } from '../fs-utils.js';
16
+ const requireFromHere = createRequire(import.meta.url);
17
+ let _dlog = null;
18
+ export function setDebugLog(log) { _dlog = log; }
19
+ function dlog(line) { if (_dlog)
20
+ try {
21
+ _dlog(line);
22
+ }
23
+ catch { } }
24
+ // ── SQLite 数据库访问 ──
25
+ let sqliteModule; // undefined = not tried, null = unavailable
26
+ function loadSqlite() {
27
+ if (sqliteModule !== undefined)
28
+ return sqliteModule;
29
+ try {
30
+ sqliteModule = requireFromHere('node:sqlite');
31
+ }
32
+ catch {
33
+ sqliteModule = null;
34
+ }
35
+ return sqliteModule;
36
+ }
37
+ function resolveStateDbPath() {
38
+ const codexHome = path.join(os.homedir(), '.codex');
39
+ if (!fs.existsSync(codexHome))
40
+ return null;
41
+ try {
42
+ const files = fs.readdirSync(codexHome)
43
+ .filter(f => /^state_\d+\.sqlite$/.test(f))
44
+ .sort((a, b) => {
45
+ const va = parseInt(a.match(/state_(\d+)/)?.[1] || '0');
46
+ const vb = parseInt(b.match(/state_(\d+)/)?.[1] || '0');
47
+ return vb - va;
48
+ });
49
+ return files.length > 0 ? path.join(codexHome, files[0]) : null;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ let _db = null;
56
+ let _dbInitialized = false;
57
+ function getDb() {
58
+ if (_dbInitialized)
59
+ return _db;
60
+ _dbInitialized = true;
61
+ const sqlite = loadSqlite();
62
+ if (!sqlite)
63
+ return null;
64
+ const dbPath = resolveStateDbPath();
65
+ if (!dbPath)
66
+ return null;
67
+ try {
68
+ _db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
69
+ dlog(`[codex] Opened state DB: ${dbPath}`);
70
+ }
71
+ catch (error) {
72
+ dlog(`[codex] Failed to open state DB: ${dbPath}`);
73
+ _db = null;
74
+ }
75
+ return _db;
76
+ }
77
+ // ── 项目列表 ──
78
+ function listProjects() {
79
+ const db = getDb();
80
+ if (!db)
81
+ return [];
82
+ try {
83
+ const rows = db.prepare(`
84
+ SELECT cwd, COUNT(*) as count, MAX(updated_at) as lastActivity
85
+ FROM threads
86
+ WHERE archived = 0
87
+ GROUP BY cwd
88
+ ORDER BY lastActivity DESC
89
+ `).all();
90
+ return rows.map(r => {
91
+ const cwd = r.cwd || '';
92
+ const label = cwd.replace(/[/\\]+$/, '').split(/[/\\]/).pop() || 'unknown';
93
+ return {
94
+ encoded: encodePath(cwd),
95
+ cwd,
96
+ label,
97
+ count: r.count || 0,
98
+ lastActivity: (r.lastActivity || 0) * 1000, // Codex uses Unix timestamp (seconds)
99
+ };
100
+ });
101
+ }
102
+ catch (error) {
103
+ dlog(`[codex] listProjects failed: ${error}`);
104
+ return [];
105
+ }
106
+ }
107
+ // ── 会话列表 ──
108
+ const CACHE_VERSION = 1;
109
+ const _metaCache = new Map();
110
+ function cacheDir(encoded) {
111
+ const dataDir = resolvePaths().dataDir;
112
+ return path.join(dataDir, 'ecweb-cache', 'codex', encoded);
113
+ }
114
+ function readDiskCache(encoded, id, mtime, size) {
115
+ try {
116
+ const rec = JSON.parse(fs.readFileSync(path.join(cacheDir(encoded), `${id}.json`), 'utf-8'));
117
+ if (rec.v === CACHE_VERSION && rec.mtime === mtime && rec.size === size && rec.meta)
118
+ return rec.meta;
119
+ }
120
+ catch { }
121
+ return null;
122
+ }
123
+ function writeDiskCache(encoded, id, mtime, size, meta) {
124
+ try {
125
+ const dir = cacheDir(encoded);
126
+ fs.mkdirSync(dir, { recursive: true });
127
+ const tmp = path.join(dir, `${id}.json.tmp`);
128
+ fs.writeFileSync(tmp, JSON.stringify({ v: CACHE_VERSION, mtime, size, meta }));
129
+ fs.renameSync(tmp, path.join(dir, `${id}.json`));
130
+ }
131
+ catch { }
132
+ }
133
+ function extractMetaFromRollout(file, id) {
134
+ let raw = '';
135
+ try {
136
+ raw = fs.readFileSync(file, 'utf-8');
137
+ }
138
+ catch { }
139
+ let title = '', firstUser = '', gitBranch = '', version = '', userMsgs = 0, totalMsgs = 0;
140
+ const lines = raw.split('\n');
141
+ for (const line of lines) {
142
+ if (!line.trim())
143
+ continue;
144
+ try {
145
+ const event = JSON.parse(line);
146
+ // 提取 cwd 和 version
147
+ if (event.type === 'session_meta' && event.payload) {
148
+ if (!gitBranch && event.payload.cwd)
149
+ gitBranch = event.payload.cwd;
150
+ if (!version && event.payload.cli_version)
151
+ version = event.payload.cli_version;
152
+ }
153
+ // 提取 title(从第一个 user_message)
154
+ if (event.type === 'event_msg' && event.payload?.type === 'user_message' && event.payload.message) {
155
+ totalMsgs++;
156
+ userMsgs++;
157
+ if (!firstUser) {
158
+ const text = event.payload.message.trim().replace(/\s+/g, ' ');
159
+ firstUser = text.substring(0, 120);
160
+ }
161
+ }
162
+ // 统计 assistant 消息
163
+ if (event.type === 'event_msg' && event.payload?.type === 'assistant_message') {
164
+ totalMsgs++;
165
+ }
166
+ }
167
+ catch { }
168
+ }
169
+ let stat;
170
+ try {
171
+ stat = fs.statSync(file);
172
+ }
173
+ catch {
174
+ stat = { size: 0, mtimeMs: 0 };
175
+ }
176
+ return {
177
+ id,
178
+ title: title || firstUser.substring(0, 50) || 'Untitled',
179
+ firstUser,
180
+ gitBranch,
181
+ version,
182
+ userMsgs,
183
+ totalMsgs,
184
+ sizeKB: Math.round(stat.size / 1024),
185
+ lastActivity: stat.mtimeMs,
186
+ };
187
+ }
188
+ function listTranscripts(encoded, cwd) {
189
+ const db = getDb();
190
+ if (!db)
191
+ return [];
192
+ try {
193
+ const rows = db.prepare(`
194
+ SELECT id, title, first_user_message, updated_at, rollout_path
195
+ FROM threads
196
+ WHERE cwd = ? AND archived = 0
197
+ ORDER BY updated_at DESC
198
+ `).all(cwd);
199
+ const out = [];
200
+ let parsed = 0, hitMem = 0, hitDisk = 0;
201
+ for (const row of rows) {
202
+ const id = row.id;
203
+ const rolloutPath = row.rollout_path;
204
+ if (!rolloutPath || !fs.existsSync(rolloutPath)) {
205
+ // Fallback: 从 DB 直接构造 meta
206
+ const firstUser = (row.first_user_message || '').trim().replace(/\s+/g, ' ').substring(0, 120);
207
+ out.push({
208
+ id,
209
+ title: row.title || firstUser.substring(0, 50) || 'Untitled',
210
+ firstUser,
211
+ gitBranch: cwd,
212
+ version: '',
213
+ userMsgs: 0,
214
+ totalMsgs: 0,
215
+ sizeKB: 0,
216
+ lastActivity: (row.updated_at || 0) * 1000,
217
+ });
218
+ continue;
219
+ }
220
+ let stat;
221
+ try {
222
+ stat = fs.statSync(rolloutPath);
223
+ }
224
+ catch {
225
+ continue;
226
+ }
227
+ const mtime = stat.mtimeMs;
228
+ const mem = _metaCache.get(rolloutPath);
229
+ if (mem && mem.mtime === mtime) {
230
+ out.push(mem.meta);
231
+ hitMem++;
232
+ continue;
233
+ }
234
+ const disk = readDiskCache(encoded, id, mtime, stat.size);
235
+ if (disk) {
236
+ _metaCache.set(rolloutPath, { mtime, meta: disk });
237
+ out.push(disk);
238
+ hitDisk++;
239
+ continue;
240
+ }
241
+ const meta = extractMetaFromRollout(rolloutPath, id);
242
+ _metaCache.set(rolloutPath, { mtime, meta });
243
+ writeDiskCache(encoded, id, mtime, stat.size, meta);
244
+ out.push(meta);
245
+ parsed++;
246
+ }
247
+ dlog(`[codex] listTranscripts ${cwd.slice(-20)}: ${rows.length} threads (mem=${hitMem} disk=${hitDisk} parsed=${parsed})`);
248
+ return out;
249
+ }
250
+ catch (error) {
251
+ dlog(`[codex] listTranscripts failed: ${error}`);
252
+ return [];
253
+ }
254
+ }
255
+ // ── 绑定关系 ──
256
+ export function buildBindMap() {
257
+ const map = new Map();
258
+ try {
259
+ const p = resolvePaths();
260
+ for (const dir of scanChatDirs(p.sessionsDir)) {
261
+ const active = readJsonFile(path.join(dir.dirPath, 'active.json'));
262
+ if (!active || !active.agentSessionId)
263
+ continue;
264
+ map.set(active.agentSessionId, {
265
+ channelType: active.channelType || dir.channelType,
266
+ channelId: active.channelId || dir.channelId,
267
+ selfAID: active.selfAID || dir.selfAID,
268
+ peerName: (active.metadata && active.metadata.peerName) || null,
269
+ name: active.name,
270
+ updatedAt: active.updatedAt || 0,
271
+ });
272
+ }
273
+ }
274
+ catch { }
275
+ return map;
276
+ }
277
+ // ── 项目解析 ──
278
+ function resolveProject(params, projects) {
279
+ if (params.project) {
280
+ const found = projects.find(p => p.encoded === params.project);
281
+ if (found) {
282
+ dlog(`[codex] resolveProject: matched param project=${params.project.slice(-24)}`);
283
+ return found;
284
+ }
285
+ dlog(`[codex] resolveProject: param project=${params.project.slice(-24)} NOT in ${projects.length} projects → falling back`);
286
+ }
287
+ const curEncoded = encodePath(process.cwd());
288
+ const cur = projects.find(p => p.encoded === curEncoded);
289
+ if (cur) {
290
+ dlog(`[codex] resolveProject: default to cwd project=${curEncoded.slice(-24)}`);
291
+ return cur;
292
+ }
293
+ dlog(`[codex] resolveProject: cwd project=${curEncoded.slice(-24)} not found → using first=${projects[0]?.encoded.slice(-24)}`);
294
+ return projects[0] || null;
295
+ }
296
+ // ── 会话详情 ──
297
+ // OpenAI 定价表(2026-06 更新)
298
+ const PRICING = {
299
+ // [input, cache_write, cache_read, output] per 1M tokens
300
+ 'gpt-5.5': [5, 0.5, 0.5, 30],
301
+ 'gpt-5.5-pro': [30, 30, 30, 180],
302
+ 'gpt-5.4': [2.5, 0.25, 0.25, 15],
303
+ 'gpt-5.4-mini': [0.75, 0.075, 0.075, 3.75],
304
+ 'gpt-5.4-nano': [0.25, 0.025, 0.025, 1.25],
305
+ 'gpt-5.2': [1.75, 0.175, 0.175, 14],
306
+ 'gpt-4.1': [2.50, 0.625, 0.125, 10],
307
+ 'gpt-4.1-mini': [0.40, 0.10, 0.10, 1.60],
308
+ 'gpt-4.1-nano': [0.10, 0.025, 0.025, 0.40],
309
+ 'gpt-4o': [2.50, 1.25, 0.25, 10],
310
+ 'gpt-4o-mini': [0.15, 0.075, 0.075, 0.60],
311
+ 'o3': [2.00, 2.00, 2.00, 8.00],
312
+ 'o3-pro': [20.00, 20.00, 20.00, 80.00],
313
+ 'o4-mini': [1.10, 1.10, 1.10, 4.40],
314
+ };
315
+ function pricingFor(model) {
316
+ for (const key of Object.keys(PRICING)) {
317
+ if (model.startsWith(key))
318
+ return PRICING[key];
319
+ }
320
+ // 默认按 gpt-5.4 计算
321
+ return PRICING['gpt-5.4'];
322
+ }
323
+ function readTranscriptFile(threadId, cwd) {
324
+ const empty = {
325
+ turns: [],
326
+ totalTurns: 0,
327
+ userMsgs: 0,
328
+ totalMsgs: 0,
329
+ counts: { userInput: 0, modelOutput: 0, toolCall: 0, toolResult: 0, msgSend: 0 },
330
+ inputTokens: 0,
331
+ outputTokens: 0,
332
+ contextTokens: 0,
333
+ costUsd: 0,
334
+ model: '',
335
+ gitBranch: cwd,
336
+ version: '',
337
+ title: '',
338
+ cwd,
339
+ };
340
+ const db = getDb();
341
+ if (!db)
342
+ return empty;
343
+ let rolloutPath;
344
+ try {
345
+ const row = db.prepare('SELECT rollout_path, title FROM threads WHERE id = ?').get(threadId);
346
+ if (!row || !row.rollout_path)
347
+ return empty;
348
+ rolloutPath = row.rollout_path;
349
+ empty.title = row.title || '';
350
+ }
351
+ catch {
352
+ return empty;
353
+ }
354
+ if (!fs.existsSync(rolloutPath))
355
+ return empty;
356
+ let raw;
357
+ try {
358
+ raw = fs.readFileSync(rolloutPath, 'utf-8');
359
+ }
360
+ catch {
361
+ return empty;
362
+ }
363
+ const turns = [];
364
+ const counts = { userInput: 0, modelOutput: 0, toolCall: 0, toolResult: 0, msgSend: 0 };
365
+ let inTok = 0, outTok = 0, model = '', version = '', userMsgs = 0, totalMsgs = 0, contextTokens = 0, costUsd = 0;
366
+ let lastUsageKey = '';
367
+ for (const line of raw.split('\n')) {
368
+ if (!line.trim())
369
+ continue;
370
+ let event;
371
+ try {
372
+ event = JSON.parse(line);
373
+ }
374
+ catch {
375
+ continue;
376
+ }
377
+ // 提取 session_meta
378
+ if (event.type === 'session_meta' && event.payload) {
379
+ if (!version && event.payload.cli_version)
380
+ version = event.payload.cli_version;
381
+ }
382
+ // 提取 model 信息(从 payload.model)
383
+ if (event.payload?.model && !model) {
384
+ model = event.payload.model;
385
+ }
386
+ // 处理 token_count 事件(Codex 的 usage 信息)
387
+ if (event.type === 'event_msg' && event.payload?.type === 'token_count' && event.payload.info) {
388
+ const u = event.payload.info.last_token_usage;
389
+ if (u) {
390
+ const inp = u.input_tokens || 0;
391
+ const cached = u.cached_input_tokens || 0;
392
+ const out = u.output_tokens || 0;
393
+ // 避免重复计算同一个 usage
394
+ const key = `${inp},${cached},${out}`;
395
+ if (key !== lastUsageKey) {
396
+ lastUsageKey = key;
397
+ inTok += inp;
398
+ outTok += out;
399
+ contextTokens = inp + cached;
400
+ // 计算费用
401
+ const [pi, pcw, pcr, po] = pricingFor(model || 'gpt-5.4');
402
+ // Codex 的 cached_input_tokens 对应 cache_read
403
+ costUsd += (inp * pi + cached * pcr + out * po) / 1_000_000;
404
+ }
405
+ }
406
+ }
407
+ // 处理消息事件
408
+ if (event.type === 'event_msg' && event.payload) {
409
+ const payload = event.payload;
410
+ if (payload.type === 'user_message' && payload.message) {
411
+ totalMsgs++;
412
+ userMsgs++;
413
+ counts.userInput++;
414
+ const text = payload.message.trim();
415
+ turns.push({
416
+ role: 'user',
417
+ ts: event.timestamp ? Date.parse(event.timestamp) : 0,
418
+ uuid: payload.id || '',
419
+ category: 'user_input',
420
+ blocks: [{ kind: 'text', text }],
421
+ });
422
+ }
423
+ else if (payload.type === 'agent_message' && payload.message) {
424
+ totalMsgs++;
425
+ counts.modelOutput++;
426
+ const blocks = [];
427
+ // 提取 message
428
+ if (payload.message) {
429
+ blocks.push({ kind: 'text', text: payload.message });
430
+ }
431
+ turns.push({
432
+ role: 'assistant',
433
+ ts: event.timestamp ? Date.parse(event.timestamp) : 0,
434
+ uuid: payload.id || '',
435
+ category: 'model_output',
436
+ blocks,
437
+ });
438
+ }
439
+ else if (payload.type === 'reasoning' && payload.message) {
440
+ // Codex reasoning 对应 Claude 的 thinking
441
+ const lastTurn = turns[turns.length - 1];
442
+ if (lastTurn && lastTurn.role === 'assistant') {
443
+ lastTurn.blocks.unshift({ kind: 'thinking', text: payload.message });
444
+ }
445
+ }
446
+ }
447
+ // 处理 function_call 事件(对应 tool_use)
448
+ if (event.type === 'event_msg' && event.payload?.type === 'function_call') {
449
+ const payload = event.payload;
450
+ counts.toolCall++;
451
+ const inputStr = payload.arguments ? JSON.stringify(JSON.parse(payload.arguments), null, 2) : '';
452
+ turns.push({
453
+ role: 'assistant',
454
+ ts: event.timestamp ? Date.parse(event.timestamp) : 0,
455
+ uuid: payload.call_id || '',
456
+ category: 'tool_call',
457
+ blocks: [{
458
+ kind: 'tool_use',
459
+ name: payload.name || '',
460
+ input: payload.arguments ? JSON.parse(payload.arguments) : {},
461
+ inputStr,
462
+ }],
463
+ });
464
+ }
465
+ // 处理 function_call_output 事件(对应 tool_result)
466
+ if (event.type === 'event_msg' && event.payload?.type === 'function_call_output') {
467
+ const payload = event.payload;
468
+ counts.toolResult++;
469
+ turns.push({
470
+ role: 'user',
471
+ ts: event.timestamp ? Date.parse(event.timestamp) : 0,
472
+ uuid: payload.call_id || '',
473
+ category: 'tool_result',
474
+ blocks: [{
475
+ kind: 'tool_result',
476
+ text: payload.output || '',
477
+ isError: false,
478
+ }],
479
+ });
480
+ }
481
+ }
482
+ const shown = turns.length > 500 ? turns.slice(-500) : turns;
483
+ return {
484
+ turns: shown,
485
+ totalTurns: turns.length,
486
+ userMsgs,
487
+ totalMsgs,
488
+ counts,
489
+ inputTokens: inTok,
490
+ outputTokens: outTok,
491
+ contextTokens,
492
+ costUsd,
493
+ model,
494
+ gitBranch: empty.gitBranch,
495
+ version,
496
+ title: empty.title,
497
+ cwd,
498
+ };
499
+ }
500
+ // ── Snapshot ──
501
+ function buildSnapshot(params) {
502
+ const projects = listProjects();
503
+ const bindMap = buildBindMap();
504
+ const project = resolveProject(params, projects);
505
+ if (!project) {
506
+ return {
507
+ baseagent: 'codex',
508
+ projects: [],
509
+ project: null,
510
+ transcripts: [],
511
+ turns: [],
512
+ sessionId: null,
513
+ };
514
+ }
515
+ const metas = listTranscripts(project.encoded, project.cwd);
516
+ const transcripts = metas.map(m => {
517
+ const bind = bindMap.get(m.id);
518
+ return {
519
+ ...m,
520
+ bound: !!bind,
521
+ boundChannel: bind?.channelType ?? null,
522
+ boundPeer: bind ? (bind.peerName || bind.channelId) : null,
523
+ online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false,
524
+ };
525
+ });
526
+ const projList = projects.map(p => ({
527
+ encoded: p.encoded,
528
+ label: p.label,
529
+ cwd: p.cwd,
530
+ count: p.count,
531
+ }));
532
+ const sessionId = params.sessionId || null;
533
+ if (!sessionId) {
534
+ return {
535
+ baseagent: 'codex',
536
+ projects: projList,
537
+ project: project.encoded,
538
+ transcripts,
539
+ turns: [],
540
+ sessionId: null,
541
+ };
542
+ }
543
+ const detail = readTranscriptFile(sessionId, project.cwd);
544
+ const bind = bindMap.get(sessionId);
545
+ const header = {
546
+ sessionId,
547
+ title: detail.title,
548
+ model: detail.model,
549
+ gitBranch: detail.gitBranch,
550
+ version: detail.version,
551
+ cwd: detail.cwd,
552
+ totalTurns: detail.totalTurns,
553
+ userMsgs: detail.userMsgs,
554
+ totalMsgs: detail.totalMsgs,
555
+ counts: detail.counts,
556
+ inputTokens: detail.inputTokens,
557
+ outputTokens: detail.outputTokens,
558
+ contextTokens: detail.contextTokens,
559
+ costUsd: detail.costUsd,
560
+ bound: !!bind,
561
+ boundChannel: bind?.channelType ?? null,
562
+ boundPeer: bind ? (bind.peerName || bind.channelId) : null,
563
+ online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false,
564
+ };
565
+ return {
566
+ baseagent: 'codex',
567
+ projects: projList,
568
+ project: project.encoded,
569
+ transcripts,
570
+ turns: detail.turns,
571
+ sessionId,
572
+ header,
573
+ };
574
+ }
575
+ // ── Export ──
576
+ export async function snapshotCodex(params = {}) {
577
+ return buildSnapshot(params);
578
+ }
579
+ export function subscribeCodex(params, push) {
580
+ const p = resolvePaths();
581
+ let dbWatcher = null;
582
+ let sessionWatcher = null;
583
+ let debounce = null;
584
+ const fire = () => {
585
+ if (debounce)
586
+ clearTimeout(debounce);
587
+ debounce = setTimeout(() => {
588
+ try {
589
+ push(buildSnapshot(params));
590
+ }
591
+ catch { }
592
+ }, 150);
593
+ };
594
+ // 监听 state_*.sqlite 变化
595
+ const dbPath = resolveStateDbPath();
596
+ if (dbPath && fs.existsSync(dbPath)) {
597
+ try {
598
+ dbWatcher = fs.watch(dbPath, () => fire());
599
+ }
600
+ catch { }
601
+ }
602
+ // 监听 evolclaw sessionsDir 的 active.json 变化
603
+ try {
604
+ sessionWatcher = fs.watch(p.sessionsDir, { recursive: true }, (_evt, filename) => {
605
+ if (filename && String(filename).endsWith('active.json'))
606
+ fire();
607
+ });
608
+ }
609
+ catch { }
610
+ return () => {
611
+ if (dbWatcher)
612
+ dbWatcher.close();
613
+ if (sessionWatcher)
614
+ sessionWatcher.close();
615
+ if (debounce)
616
+ clearTimeout(debounce);
617
+ };
618
+ }
@@ -1,22 +1,25 @@
1
1
  /**
2
- * 会话数据源 — CC(Claude Code / Agent SDK)transcript 历史展示。
2
+ * 会话数据源 — 多 baseagent 支持(Claude / Codex)。
3
3
  *
4
- * 列表数据源:~/.claude/projects/<encodedProject>/*.jsonl
5
- * 绑定状态交叉标注:evolclaw active.json 中的 agentSessionId。
4
+ * 根据 params.baseagent 参数路由到对应的实现:
5
+ * - claude: ~/.claude/projects/<encodedProject>/*.jsonl
6
+ * - codex: ~/.codex/state_*.sqlite + ~/.codex/sessions/YYYY/MM/DD/*.jsonl
6
7
  *
7
8
  * snapshot:
8
9
  * - 无 sessionId: 项目列表 + 选中项目的全部 transcript 概要
9
10
  * - 有 sessionId: 该 transcript 的结构化轮次 + 会话头统计
10
- * subscribe: 监听选中项目的 CC 目录 .jsonl 变化 + evolclaw sessionsDir active.json 变化,防抖 150ms。
11
- *
12
- * 注:完整复制 evolclaw session source 逻辑(含两层缓存、费用计算、ec msg send 检测)。
11
+ * subscribe: 监听对应目录的文件变化 + evolclaw sessionsDir active.json 变化,防抖 150ms。
13
12
  */
14
13
  import fs from 'fs';
15
14
  import path from 'path';
16
15
  import { resolvePaths, ccProjectsDir } from '../paths.js';
17
16
  import { encodePath, scanChatDirs, readJsonFile } from '../fs-utils.js';
17
+ import { snapshotCodex, subscribeCodex, setDebugLog as setCodexDebugLog } from './session-codex.js';
18
18
  let _dlog = null;
19
- export function setDebugLog(log) { _dlog = log; }
19
+ export function setDebugLog(log) {
20
+ _dlog = log;
21
+ setCodexDebugLog(log); // 同步传递给 Codex 模块
22
+ }
20
23
  function dlog(line) { if (_dlog)
21
24
  try {
22
25
  _dlog(line);
@@ -359,7 +362,7 @@ function buildSnapshot(params) {
359
362
  const bindMap = buildBindMap();
360
363
  const project = resolveProject(params, projects);
361
364
  if (!project)
362
- return { projects: [], project: null, transcripts: [], turns: [], sessionId: null };
365
+ return { baseagent: 'claude', projects: [], project: null, transcripts: [], turns: [], sessionId: null };
363
366
  const metas = listTranscripts(project.encoded);
364
367
  const transcripts = metas.map(m => {
365
368
  const bind = bindMap.get(m.id);
@@ -368,16 +371,26 @@ function buildSnapshot(params) {
368
371
  const projList = projects.map(p => ({ encoded: p.encoded, label: p.label, cwd: p.cwd, count: p.count }));
369
372
  const sessionId = params.sessionId || null;
370
373
  if (!sessionId)
371
- return { projects: projList, project: project.encoded, transcripts, turns: [], sessionId: null };
374
+ return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: [], sessionId: null };
372
375
  const detail = readTranscriptFile(path.join(ccProjectsDir(), project.encoded, `${sessionId}.jsonl`));
373
376
  const bind = bindMap.get(sessionId);
374
377
  const header = { sessionId, title: detail.title, model: detail.model, gitBranch: detail.gitBranch, version: detail.version, cwd: detail.cwd || project.cwd, totalTurns: detail.totalTurns, userMsgs: detail.userMsgs, totalMsgs: detail.totalMsgs, counts: detail.counts, inputTokens: detail.inputTokens, outputTokens: detail.outputTokens, contextTokens: detail.contextTokens, costUsd: detail.costUsd, bound: !!bind, boundChannel: bind?.channelType ?? null, boundPeer: bind ? (bind.peerName || bind.channelId) : null, online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false };
375
- return { projects: projList, project: project.encoded, transcripts, turns: detail.turns, sessionId, header };
378
+ return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: detail.turns, sessionId, header };
376
379
  }
377
380
  export const sessionSource = {
378
381
  kind: 'session',
379
- async snapshot(params = {}) { return buildSnapshot(params); },
382
+ async snapshot(params = {}) {
383
+ const baseagent = params.baseagent || 'claude';
384
+ if (baseagent === 'codex') {
385
+ return snapshotCodex(params);
386
+ }
387
+ return buildSnapshot(params);
388
+ },
380
389
  subscribe(params, push) {
390
+ const baseagent = params.baseagent || 'claude';
391
+ if (baseagent === 'codex') {
392
+ return subscribeCodex(params, push);
393
+ }
381
394
  const p = resolvePaths();
382
395
  let projectWatcher = null, sessionWatcher = null, debounce = null;
383
396
  const fire = () => { if (debounce)