evolclaw-web 1.2.0 → 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.
- package/dist/server.js +193 -25
- package/dist/sources/aid.js +4 -2
- package/dist/sources/baseagent-detector.js +72 -0
- package/dist/sources/gateway.js +44 -0
- package/dist/sources/msg.js +366 -31
- package/dist/sources/session-codex.js +618 -0
- package/dist/sources/session.js +25 -12
- package/dist/sources/stats.js +269 -136
- package/dist/sources/system.js +37 -2
- package/dist/static/app.js +2089 -321
- package/dist/static/index.html +122 -57
- package/dist/static/style.css +845 -19
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/sources/session.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 会话数据源 —
|
|
2
|
+
* 会话数据源 — 多 baseagent 支持(Claude / Codex)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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:
|
|
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) {
|
|
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);
|
|
@@ -221,7 +224,7 @@ function resolveProject(params, projects) {
|
|
|
221
224
|
dlog(`[session] resolveProject: cwd project=${curEncoded.slice(-24)} not found → using first=${projects[0]?.encoded.slice(-24)}`);
|
|
222
225
|
return projects[0] || null;
|
|
223
226
|
}
|
|
224
|
-
function buildBindMap() {
|
|
227
|
+
export function buildBindMap() {
|
|
225
228
|
const map = new Map();
|
|
226
229
|
try {
|
|
227
230
|
const p = resolvePaths();
|
|
@@ -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 = {}) {
|
|
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)
|