ec-web 0.0.4 → 0.0.5
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 +47 -5
- package/dist/sources/msg.js +187 -5
- package/dist/sources/session-codex.js +43 -29
- package/dist/sources/session-origin.js +195 -0
- package/dist/sources/session.js +211 -33
- package/dist/static/app.js +285 -43
- package/dist/static/index.html +4 -5
- package/dist/static/style.css +55 -1
- package/dist/static/vendor/echarts-5.6.0.min.js +45 -0
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -66,10 +66,26 @@ const ECWEB_VERSION = readEcwebVersion();
|
|
|
66
66
|
const MIME = {
|
|
67
67
|
'.html': 'text/html; charset=utf-8',
|
|
68
68
|
'.js': 'text/javascript; charset=utf-8',
|
|
69
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
69
70
|
'.css': 'text/css; charset=utf-8',
|
|
70
71
|
'.json': 'application/json; charset=utf-8',
|
|
72
|
+
'.webmanifest': 'application/manifest+json; charset=utf-8',
|
|
71
73
|
'.svg': 'image/svg+xml',
|
|
74
|
+
'.png': 'image/png',
|
|
75
|
+
'.jpg': 'image/jpeg',
|
|
76
|
+
'.jpeg': 'image/jpeg',
|
|
77
|
+
'.gif': 'image/gif',
|
|
78
|
+
'.webp': 'image/webp',
|
|
79
|
+
'.avif': 'image/avif',
|
|
80
|
+
'.ico': 'image/x-icon',
|
|
81
|
+
'.woff': 'font/woff',
|
|
82
|
+
'.woff2': 'font/woff2',
|
|
83
|
+
'.ttf': 'font/ttf',
|
|
84
|
+
'.otf': 'font/otf',
|
|
85
|
+
'.wasm': 'application/wasm',
|
|
72
86
|
};
|
|
87
|
+
const HTML_CACHE_CONTROL = 'no-cache';
|
|
88
|
+
const STATIC_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
|
|
73
89
|
function tokenStorePath() {
|
|
74
90
|
const dir = resolvePaths().instanceDir;
|
|
75
91
|
const current = path.join(dir, 'ecweb-tokens.json');
|
|
@@ -141,7 +157,32 @@ function injectBaseHref(html, prefix) {
|
|
|
141
157
|
return html.replace(/<base\b[^>]*>/i, tag);
|
|
142
158
|
return html.replace(/<head[^>]*>/i, (m) => `${m}\n ${tag}`);
|
|
143
159
|
}
|
|
144
|
-
function
|
|
160
|
+
function createEtag(content) {
|
|
161
|
+
return `"${crypto.createHash('sha256').update(content).digest('hex')}"`;
|
|
162
|
+
}
|
|
163
|
+
function matchesIfNoneMatch(req, etag) {
|
|
164
|
+
const header = req.headers['if-none-match'];
|
|
165
|
+
const value = Array.isArray(header) ? header.join(',') : header;
|
|
166
|
+
if (!value)
|
|
167
|
+
return false;
|
|
168
|
+
// GET uses weak comparison, so a client may send the validator with W/.
|
|
169
|
+
return value.split(',').some((candidate) => {
|
|
170
|
+
const normalized = candidate.trim().replace(/^W\//, '');
|
|
171
|
+
return normalized === '*' || normalized === etag;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function respondWithValidator(req, res, body, contentType, cacheControl, extraHeaders = {}) {
|
|
175
|
+
const etag = createEtag(body);
|
|
176
|
+
const headers = { 'Content-Type': contentType, 'Cache-Control': cacheControl, ETag: etag, ...extraHeaders };
|
|
177
|
+
if (matchesIfNoneMatch(req, etag)) {
|
|
178
|
+
res.writeHead(304, headers);
|
|
179
|
+
res.end();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
res.writeHead(200, headers);
|
|
183
|
+
res.end(body);
|
|
184
|
+
}
|
|
185
|
+
export function serveStatic(req, res) {
|
|
145
186
|
let urlPath = (req.url || '/').split('?')[0];
|
|
146
187
|
if (urlPath === '/')
|
|
147
188
|
urlPath = '/index.html';
|
|
@@ -161,12 +202,13 @@ function serveStatic(req, res) {
|
|
|
161
202
|
if (ext === '.html') {
|
|
162
203
|
const prefix = forwardedPrefix(req);
|
|
163
204
|
const html = injectBaseHref(data.toString('utf-8'), prefix);
|
|
164
|
-
|
|
165
|
-
res
|
|
205
|
+
// The proxy prefix changes the representation, so shared caches must key on it.
|
|
206
|
+
respondWithValidator(req, res, html, MIME[ext], HTML_CACHE_CONTROL, { Vary: 'X-Forwarded-Prefix' });
|
|
166
207
|
return;
|
|
167
208
|
}
|
|
168
|
-
|
|
169
|
-
|
|
209
|
+
// Static files have stable ETags, including fonts, images, manifests, and vendored libraries.
|
|
210
|
+
// Their fixed URLs must revalidate so deployments never leave clients on an old asset.
|
|
211
|
+
respondWithValidator(req, res, data, MIME[ext] || 'application/octet-stream', STATIC_CACHE_CONTROL);
|
|
170
212
|
});
|
|
171
213
|
}
|
|
172
214
|
// ── Helpers ──
|
package/dist/sources/msg.js
CHANGED
|
@@ -6,11 +6,16 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
+
import { ipcQuery } from '../ipc-client.js';
|
|
9
10
|
import { resolvePaths } from '../paths.js';
|
|
10
11
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
11
12
|
import { isTransientProtocolMessage, readAllJsonlLines, readJsonFile, } from '../fs-utils.js';
|
|
12
13
|
let contextModulesPromise = null;
|
|
13
14
|
const sessionAdapterPromises = new Map();
|
|
15
|
+
let peerDisplayResolverPromise = null;
|
|
16
|
+
const aunGroupNameCache = new Map();
|
|
17
|
+
const aunGroupNameRequests = new Map();
|
|
18
|
+
let agentDisplayNamesCache = null;
|
|
14
19
|
function getContextModules() {
|
|
15
20
|
if (!contextModulesPromise) {
|
|
16
21
|
contextModulesPromise = Promise.all([
|
|
@@ -21,6 +26,145 @@ function getContextModules() {
|
|
|
21
26
|
}
|
|
22
27
|
return contextModulesPromise;
|
|
23
28
|
}
|
|
29
|
+
function getPeerDisplayResolver() {
|
|
30
|
+
if (!peerDisplayResolverPromise) {
|
|
31
|
+
peerDisplayResolverPromise = Promise.resolve()
|
|
32
|
+
.then(() => import(toFileUrl(resolveParentDistModule('config', 'contact-book.js'))))
|
|
33
|
+
.then(mod => typeof mod.resolvePeerDisplayLabel === 'function' ? mod.resolvePeerDisplayLabel : null)
|
|
34
|
+
.catch(() => null);
|
|
35
|
+
}
|
|
36
|
+
return peerDisplayResolverPromise;
|
|
37
|
+
}
|
|
38
|
+
export function resolveContactPeerName(resolver, identity, peerId, fallback) {
|
|
39
|
+
if (!resolver)
|
|
40
|
+
return fallback;
|
|
41
|
+
try {
|
|
42
|
+
return resolver(identity.selfAID, identity.channelType, peerId, identity.channel) || fallback;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function aunGroupIdVariants(groupId) {
|
|
49
|
+
const id = String(groupId || '').trim();
|
|
50
|
+
if (!id)
|
|
51
|
+
return [];
|
|
52
|
+
const variants = new Set([id]);
|
|
53
|
+
const legacy = /^group\.agentid\.pub\/([^/]+)$/i.exec(id);
|
|
54
|
+
if (legacy)
|
|
55
|
+
variants.add(`${legacy[1]}.agentid.pub`);
|
|
56
|
+
const current = /^([^.]+)\.agentid\.pub$/i.exec(id);
|
|
57
|
+
if (current)
|
|
58
|
+
variants.add(`group.agentid.pub/${current[1]}`);
|
|
59
|
+
return [...variants];
|
|
60
|
+
}
|
|
61
|
+
export function resolveAunGroupName(names, groupId, fallback) {
|
|
62
|
+
for (const id of aunGroupIdVariants(groupId || '')) {
|
|
63
|
+
const name = names.get(id);
|
|
64
|
+
if (name)
|
|
65
|
+
return name;
|
|
66
|
+
}
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
async function fetchAunGroupNames(aid) {
|
|
70
|
+
const names = new Map();
|
|
71
|
+
let cursor = null;
|
|
72
|
+
let page = 0;
|
|
73
|
+
do {
|
|
74
|
+
page += 1;
|
|
75
|
+
const response = await ipcQuery(resolvePaths().socket, {
|
|
76
|
+
type: 'menu.exec',
|
|
77
|
+
payload: {
|
|
78
|
+
type: 'menu.query',
|
|
79
|
+
name: 'connect',
|
|
80
|
+
cmd: '/connect',
|
|
81
|
+
agent: aid,
|
|
82
|
+
id: `ecweb-msg-groups-${Date.now()}-${page}`,
|
|
83
|
+
args: { self: aid, view: 'groups', limit: 200, ...(cursor ? { cursor } : {}) },
|
|
84
|
+
},
|
|
85
|
+
auth: { source: 'ecweb', localDirect: true },
|
|
86
|
+
}, 10_000);
|
|
87
|
+
const data = response?.ok && !response.response?.error ? response.response?.data : null;
|
|
88
|
+
if (!data || !Array.isArray(data.items))
|
|
89
|
+
break;
|
|
90
|
+
for (const item of data.items) {
|
|
91
|
+
const groupId = String(item?.groupId || '').trim();
|
|
92
|
+
const displayName = String(item?.displayName || '').trim();
|
|
93
|
+
if (!groupId || !displayName)
|
|
94
|
+
continue;
|
|
95
|
+
for (const variant of aunGroupIdVariants(groupId))
|
|
96
|
+
names.set(variant, displayName);
|
|
97
|
+
}
|
|
98
|
+
cursor = typeof data.nextCursor === 'string' && data.nextCursor ? data.nextCursor : null;
|
|
99
|
+
} while (cursor && page < 20);
|
|
100
|
+
return names;
|
|
101
|
+
}
|
|
102
|
+
function getAunGroupNames(aid) {
|
|
103
|
+
const cached = aunGroupNameCache.get(aid);
|
|
104
|
+
if (cached && cached.expiresAt > Date.now())
|
|
105
|
+
return Promise.resolve(cached.names);
|
|
106
|
+
const pending = aunGroupNameRequests.get(aid);
|
|
107
|
+
if (pending)
|
|
108
|
+
return pending;
|
|
109
|
+
const request = fetchAunGroupNames(aid)
|
|
110
|
+
.then(names => {
|
|
111
|
+
aunGroupNameCache.set(aid, {
|
|
112
|
+
names,
|
|
113
|
+
expiresAt: Date.now() + (names.size ? 5 * 60_000 : 15_000),
|
|
114
|
+
});
|
|
115
|
+
return names;
|
|
116
|
+
})
|
|
117
|
+
.catch(() => new Map())
|
|
118
|
+
.finally(() => aunGroupNameRequests.delete(aid));
|
|
119
|
+
aunGroupNameRequests.set(aid, request);
|
|
120
|
+
return request;
|
|
121
|
+
}
|
|
122
|
+
async function hydrateAunGroupNames(chats) {
|
|
123
|
+
const aids = [...new Set(chats
|
|
124
|
+
.filter(chat => chat.channelType === 'aun' && chat.chatType === 'group')
|
|
125
|
+
.map(chat => chat.selfAID))];
|
|
126
|
+
if (!aids.length)
|
|
127
|
+
return;
|
|
128
|
+
const directories = new Map(await Promise.all(aids.map(async (aid) => [aid, await getAunGroupNames(aid)])));
|
|
129
|
+
for (const chat of chats) {
|
|
130
|
+
if (chat.channelType !== 'aun' || chat.chatType !== 'group')
|
|
131
|
+
continue;
|
|
132
|
+
const groupName = resolveAunGroupName(directories.get(chat.selfAID) || new Map(), chat.groupId, chat.groupName);
|
|
133
|
+
if (!groupName)
|
|
134
|
+
continue;
|
|
135
|
+
chat.groupName = groupName;
|
|
136
|
+
chat.peerName = groupName;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function resolveSessionBindDisplayName(bind, resolver, aunGroupNames = new Map()) {
|
|
140
|
+
const isGroup = bind.chatType === 'group' || !!bind.groupId;
|
|
141
|
+
if (bind.channelType === 'aun' && isGroup) {
|
|
142
|
+
return resolveAunGroupName(aunGroupNames, bind.groupId || bind.channelId, bind.groupName || null);
|
|
143
|
+
}
|
|
144
|
+
if (isGroup)
|
|
145
|
+
return bind.groupName || null;
|
|
146
|
+
return resolveContactPeerName(resolver, {
|
|
147
|
+
selfAID: bind.selfAID,
|
|
148
|
+
channelType: bind.channelType,
|
|
149
|
+
channel: bind.channelKey || bind.channelType,
|
|
150
|
+
channelId: bind.channelId,
|
|
151
|
+
}, bind.peerId || bind.channelId, bind.peerName);
|
|
152
|
+
}
|
|
153
|
+
/** 为会话页绑定标签补齐联系人名或 AUN 群名,不改变原始 channelId。 */
|
|
154
|
+
export async function hydrateSessionBindDisplayNames(binds) {
|
|
155
|
+
const items = [...binds];
|
|
156
|
+
if (!items.length)
|
|
157
|
+
return;
|
|
158
|
+
const resolver = await getPeerDisplayResolver();
|
|
159
|
+
const groupAids = [...new Set(items
|
|
160
|
+
.filter(bind => bind.channelType === 'aun' && (bind.chatType === 'group' || !!bind.groupId))
|
|
161
|
+
.map(bind => bind.selfAID)
|
|
162
|
+
.filter(Boolean))];
|
|
163
|
+
const groupDirectories = new Map(await Promise.all(groupAids.map(async (aid) => [aid, await getAunGroupNames(aid)])));
|
|
164
|
+
for (const bind of items) {
|
|
165
|
+
bind.displayName = resolveSessionBindDisplayName(bind, resolver, groupDirectories.get(bind.selfAID) || new Map());
|
|
166
|
+
}
|
|
167
|
+
}
|
|
24
168
|
function getSessionAdapter(baseagent) {
|
|
25
169
|
if (!sessionAdapterPromises.has(baseagent)) {
|
|
26
170
|
const classNames = {
|
|
@@ -173,6 +317,37 @@ function listConfiguredAgentAids(root) {
|
|
|
173
317
|
}
|
|
174
318
|
return aids;
|
|
175
319
|
}
|
|
320
|
+
function shortAid(value) {
|
|
321
|
+
return String(value || '').split('.')[0] || String(value || '');
|
|
322
|
+
}
|
|
323
|
+
/** Returns local Agent display names, preferring the runtime selfName. */
|
|
324
|
+
export async function getAgentSelfNames() {
|
|
325
|
+
if (agentDisplayNamesCache && agentDisplayNamesCache.expiresAt > Date.now()) {
|
|
326
|
+
return agentDisplayNamesCache.names;
|
|
327
|
+
}
|
|
328
|
+
const names = new Map();
|
|
329
|
+
try {
|
|
330
|
+
const [agentsResp, statsResp] = await Promise.all([
|
|
331
|
+
ipcQuery(resolvePaths().socket, { type: 'evolagent.list' }, 4000),
|
|
332
|
+
ipcQuery(resolvePaths().socket, { type: 'aun-aid-stats' }, 4000),
|
|
333
|
+
]);
|
|
334
|
+
const statsByAid = new Map((statsResp?.stats || []).map(item => [String(item?.aid || ''), item]));
|
|
335
|
+
for (const agent of agentsResp?.agents || []) {
|
|
336
|
+
const aid = String(agent?.aid || '').trim();
|
|
337
|
+
if (!aid)
|
|
338
|
+
continue;
|
|
339
|
+
const stats = statsByAid.get(aid);
|
|
340
|
+
const displayName = String(stats?.selfName || agent?.displayName || shortAid(aid)).trim();
|
|
341
|
+
if (displayName)
|
|
342
|
+
names.set(aid, displayName);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// daemon unavailable: leave the map empty and use the AID fallback below
|
|
347
|
+
}
|
|
348
|
+
agentDisplayNamesCache = { expiresAt: Date.now() + 5000, names };
|
|
349
|
+
return names;
|
|
350
|
+
}
|
|
176
351
|
function decodeDirSegment(seg) {
|
|
177
352
|
return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
178
353
|
}
|
|
@@ -292,7 +467,7 @@ function chatDisplayId(active, messages) {
|
|
|
292
467
|
groupName: null,
|
|
293
468
|
};
|
|
294
469
|
}
|
|
295
|
-
function loadChats() {
|
|
470
|
+
function loadChats(peerDisplayResolver) {
|
|
296
471
|
const paths = resolvePaths();
|
|
297
472
|
const sessionsDir = paths.sessionsDir;
|
|
298
473
|
const configuredAids = listConfiguredAgentAids(paths.root);
|
|
@@ -311,6 +486,9 @@ function loadChats() {
|
|
|
311
486
|
continue;
|
|
312
487
|
const chatType = String(active.chatType || messages.find(m => m.chatType)?.chatType || 'private');
|
|
313
488
|
const display = chatDisplayId({ ...active, chatType, channelId }, messages);
|
|
489
|
+
const peerName = chatType === 'group'
|
|
490
|
+
? display.peerName
|
|
491
|
+
: resolveContactPeerName(peerDisplayResolver, identity, display.peerId, display.peerName);
|
|
314
492
|
const threadSessions = loadThreadSessions(dirPath);
|
|
315
493
|
const threadById = new Map(threadSessions.map(item => [item.threadId, item]));
|
|
316
494
|
const mainSessionIds = loadMainSessionIds(dirPath, active);
|
|
@@ -331,7 +509,7 @@ function loadChats() {
|
|
|
331
509
|
selfAID,
|
|
332
510
|
chatType,
|
|
333
511
|
peerId: display.peerId,
|
|
334
|
-
peerName
|
|
512
|
+
peerName,
|
|
335
513
|
groupId: display.groupId,
|
|
336
514
|
groupName: display.groupName,
|
|
337
515
|
partition: item.partition,
|
|
@@ -353,7 +531,7 @@ function scopeId(chat) {
|
|
|
353
531
|
function legacyChannelScopeId(chat) {
|
|
354
532
|
return safeId(`${chat.selfAID}\n${chat.channelType}\n${chat.channel}`);
|
|
355
533
|
}
|
|
356
|
-
function buildScopes(chats) {
|
|
534
|
+
function buildScopes(chats, agentDisplayNames = new Map()) {
|
|
357
535
|
const map = new Map();
|
|
358
536
|
const seenChats = new Map();
|
|
359
537
|
const seenChannels = new Map();
|
|
@@ -366,6 +544,7 @@ function buildScopes(chats) {
|
|
|
366
544
|
id,
|
|
367
545
|
aid: id,
|
|
368
546
|
selfAID: chat.selfAID,
|
|
547
|
+
displayName: agentDisplayNames.get(chat.selfAID) || shortAid(chat.selfAID),
|
|
369
548
|
totalIn: 0,
|
|
370
549
|
totalOut: 0,
|
|
371
550
|
peerCount: 0,
|
|
@@ -566,8 +745,9 @@ async function buildSessionContext(chat) {
|
|
|
566
745
|
};
|
|
567
746
|
}
|
|
568
747
|
async function buildSnapshot(params) {
|
|
569
|
-
const
|
|
570
|
-
const
|
|
748
|
+
const peerDisplayResolver = await getPeerDisplayResolver();
|
|
749
|
+
const chats = loadChats(peerDisplayResolver);
|
|
750
|
+
const scopes = buildScopes(chats, await getAgentSelfNames());
|
|
571
751
|
const requestedScope = params.scope || params.aid || null;
|
|
572
752
|
const scope = requestedScope && !scopes.some(s => s.id === requestedScope)
|
|
573
753
|
? (chats.find(chat => legacyChannelScopeId(chat) === requestedScope)
|
|
@@ -576,6 +756,8 @@ async function buildSnapshot(params) {
|
|
|
576
756
|
: requestedScope;
|
|
577
757
|
const peer = params.peer || null;
|
|
578
758
|
const inScope = selectedChats(chats, scope);
|
|
759
|
+
if (scope)
|
|
760
|
+
await hydrateAunGroupNames(inScope);
|
|
579
761
|
const peers = scope ? mergePeerInfo(inScope) : [];
|
|
580
762
|
const messages = scope ? messagesFor(inScope, peer) : [];
|
|
581
763
|
const currentChat = peer ? inScope.find(chat => chatKey(chat) === peer) : undefined;
|
|
@@ -12,7 +12,9 @@ import path from 'path';
|
|
|
12
12
|
import os from 'os';
|
|
13
13
|
import { createRequire } from 'module';
|
|
14
14
|
import { resolvePaths } from '../paths.js';
|
|
15
|
-
import { encodePath
|
|
15
|
+
import { encodePath } from '../fs-utils.js';
|
|
16
|
+
import { getAgentSelfNames } from './msg.js';
|
|
17
|
+
import { buildSessionBindMap, classifyTranscriptSource, isEvolcoreRoutedMessage, isSyntheticCliPrompt, resolveSessionBindSelfName, } from './session-origin.js';
|
|
16
18
|
const requireFromHere = createRequire(import.meta.url);
|
|
17
19
|
let _dlog = null;
|
|
18
20
|
export function setDebugLog(log) { _dlog = log; }
|
|
@@ -107,7 +109,7 @@ function listProjects() {
|
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
// ── 会话列表 ──
|
|
110
|
-
const CACHE_VERSION =
|
|
112
|
+
const CACHE_VERSION = 2;
|
|
111
113
|
const _metaCache = new Map();
|
|
112
114
|
function cacheDir(encoded) {
|
|
113
115
|
const dataDir = resolvePaths().dataDir;
|
|
@@ -170,6 +172,7 @@ function extractMetaFromRollout(file, id) {
|
|
|
170
172
|
}
|
|
171
173
|
catch { }
|
|
172
174
|
let title = '', firstUser = '', gitBranch = '', version = '', userMsgs = 0, totalMsgs = 0;
|
|
175
|
+
let hasRealUserInput = false;
|
|
173
176
|
const lines = raw.split('\n');
|
|
174
177
|
for (const line of lines) {
|
|
175
178
|
if (!line.trim())
|
|
@@ -187,6 +190,8 @@ function extractMetaFromRollout(file, id) {
|
|
|
187
190
|
if (event.type === 'event_msg' && event.payload?.type === 'user_message' && event.payload.message) {
|
|
188
191
|
totalMsgs++;
|
|
189
192
|
userMsgs++;
|
|
193
|
+
if (!isSyntheticCliPrompt(event.payload.message, 'codex'))
|
|
194
|
+
hasRealUserInput = true;
|
|
190
195
|
if (!firstUser) {
|
|
191
196
|
const text = event.payload.message.trim().replace(/\s+/g, ' ');
|
|
192
197
|
firstUser = text.substring(0, 120);
|
|
@@ -216,15 +221,34 @@ function extractMetaFromRollout(file, id) {
|
|
|
216
221
|
totalMsgs,
|
|
217
222
|
sizeKB: Math.round(stat.size / 1024),
|
|
218
223
|
lastActivity: stat.mtimeMs,
|
|
224
|
+
hasRealUserInput,
|
|
225
|
+
backendSource: '',
|
|
226
|
+
threadSource: '',
|
|
227
|
+
evolcoreRouted: false,
|
|
219
228
|
};
|
|
220
229
|
}
|
|
230
|
+
function withThreadSource(meta, row) {
|
|
231
|
+
return {
|
|
232
|
+
...meta,
|
|
233
|
+
backendSource: typeof row.source === 'string' ? row.source : '',
|
|
234
|
+
threadSource: typeof row.thread_source === 'string' ? row.thread_source : '',
|
|
235
|
+
evolcoreRouted: isEvolcoreRoutedMessage(row.first_user_message),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function isCodexCliCandidate(meta) {
|
|
239
|
+
if (meta.threadSource === 'subagent')
|
|
240
|
+
return false;
|
|
241
|
+
if (meta.backendSource !== 'cli' && meta.backendSource !== 'vscode')
|
|
242
|
+
return false;
|
|
243
|
+
return meta.hasRealUserInput && !meta.evolcoreRouted;
|
|
244
|
+
}
|
|
221
245
|
function listTranscripts(encoded, cwd) {
|
|
222
246
|
const db = getDb();
|
|
223
247
|
if (!db)
|
|
224
248
|
return [];
|
|
225
249
|
try {
|
|
226
250
|
const rows = db.prepare(`
|
|
227
|
-
SELECT id, title, first_user_message, updated_at, rollout_path
|
|
251
|
+
SELECT id, title, first_user_message, updated_at, rollout_path, source, thread_source
|
|
228
252
|
FROM threads
|
|
229
253
|
WHERE cwd = ? AND archived = 0
|
|
230
254
|
ORDER BY updated_at DESC
|
|
@@ -248,6 +272,10 @@ function listTranscripts(encoded, cwd) {
|
|
|
248
272
|
totalMsgs: 0,
|
|
249
273
|
sizeKB: 0,
|
|
250
274
|
lastActivity: (row.updated_at || 0) * 1000,
|
|
275
|
+
hasRealUserInput: !!firstUser && !isSyntheticCliPrompt(firstUser, 'codex'),
|
|
276
|
+
backendSource: typeof row.source === 'string' ? row.source : '',
|
|
277
|
+
threadSource: typeof row.thread_source === 'string' ? row.thread_source : '',
|
|
278
|
+
evolcoreRouted: isEvolcoreRoutedMessage(row.first_user_message),
|
|
251
279
|
});
|
|
252
280
|
continue;
|
|
253
281
|
}
|
|
@@ -261,21 +289,21 @@ function listTranscripts(encoded, cwd) {
|
|
|
261
289
|
const mtime = stat.mtimeMs;
|
|
262
290
|
const mem = _metaCache.get(rolloutPath);
|
|
263
291
|
if (mem && mem.mtime === mtime) {
|
|
264
|
-
out.push(mem.meta);
|
|
292
|
+
out.push(withThreadSource(mem.meta, row));
|
|
265
293
|
hitMem++;
|
|
266
294
|
continue;
|
|
267
295
|
}
|
|
268
296
|
const disk = readDiskCache(encoded, id, mtime, stat.size);
|
|
269
297
|
if (disk) {
|
|
270
298
|
_metaCache.set(rolloutPath, { mtime, meta: disk });
|
|
271
|
-
out.push(disk);
|
|
299
|
+
out.push(withThreadSource(disk, row));
|
|
272
300
|
hitDisk++;
|
|
273
301
|
continue;
|
|
274
302
|
}
|
|
275
303
|
const meta = extractMetaFromRollout(rolloutPath, id);
|
|
276
304
|
_metaCache.set(rolloutPath, { mtime, meta });
|
|
277
305
|
writeDiskCache(encoded, id, mtime, stat.size, meta);
|
|
278
|
-
out.push(meta);
|
|
306
|
+
out.push(withThreadSource(meta, row));
|
|
279
307
|
parsed++;
|
|
280
308
|
}
|
|
281
309
|
dlog(`[codex] listTranscripts ${cwd.slice(-20)}: ${rows.length} threads (mem=${hitMem} disk=${hitDisk} parsed=${parsed})`);
|
|
@@ -288,25 +316,7 @@ function listTranscripts(encoded, cwd) {
|
|
|
288
316
|
}
|
|
289
317
|
// ── 绑定关系 ──
|
|
290
318
|
export function buildBindMap() {
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
const p = resolvePaths();
|
|
294
|
-
for (const dir of scanChatDirs(p.sessionsDir)) {
|
|
295
|
-
const active = readJsonFile(path.join(dir.dirPath, 'active.json'));
|
|
296
|
-
if (!active || !active.agentSessionId)
|
|
297
|
-
continue;
|
|
298
|
-
map.set(active.agentSessionId, {
|
|
299
|
-
channelType: active.channelType || dir.channelType,
|
|
300
|
-
channelId: active.channelId || dir.channelId,
|
|
301
|
-
selfAID: active.selfAID || dir.selfAID,
|
|
302
|
-
peerName: (active.metadata && active.metadata.peerName) || null,
|
|
303
|
-
name: active.name,
|
|
304
|
-
updatedAt: active.updatedAt || 0,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
catch { }
|
|
309
|
-
return map;
|
|
319
|
+
return buildSessionBindMap();
|
|
310
320
|
}
|
|
311
321
|
// ── 项目解析 ──
|
|
312
322
|
function resolveProject(params, projects) {
|
|
@@ -611,7 +621,7 @@ function readTranscriptFile(threadId, cwd) {
|
|
|
611
621
|
};
|
|
612
622
|
}
|
|
613
623
|
// ── Snapshot ──
|
|
614
|
-
function buildSnapshot(params) {
|
|
624
|
+
async function buildSnapshot(params) {
|
|
615
625
|
const projects = listProjects();
|
|
616
626
|
const bindMap = buildBindMap();
|
|
617
627
|
const project = resolveProject(params, projects);
|
|
@@ -626,13 +636,15 @@ function buildSnapshot(params) {
|
|
|
626
636
|
};
|
|
627
637
|
}
|
|
628
638
|
const metas = listTranscripts(project.encoded, project.cwd);
|
|
639
|
+
const selfNames = await getAgentSelfNames();
|
|
629
640
|
const transcripts = metas.map(m => {
|
|
630
641
|
const bind = bindMap.get(m.id);
|
|
631
642
|
return {
|
|
632
643
|
...m,
|
|
644
|
+
sourceType: classifyTranscriptSource(bind, isCodexCliCandidate(m)),
|
|
633
645
|
bound: !!bind,
|
|
634
646
|
boundChannel: bind?.channelType ?? null,
|
|
635
|
-
|
|
647
|
+
boundSelf: resolveSessionBindSelfName(bind, selfNames),
|
|
636
648
|
online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false,
|
|
637
649
|
};
|
|
638
650
|
});
|
|
@@ -655,6 +667,7 @@ function buildSnapshot(params) {
|
|
|
655
667
|
}
|
|
656
668
|
const detail = readTranscriptFile(sessionId, project.cwd);
|
|
657
669
|
const bind = bindMap.get(sessionId);
|
|
670
|
+
const transcript = metas.find(meta => meta.id === sessionId);
|
|
658
671
|
const header = {
|
|
659
672
|
sessionId,
|
|
660
673
|
title: detail.title,
|
|
@@ -670,9 +683,10 @@ function buildSnapshot(params) {
|
|
|
670
683
|
outputTokens: detail.outputTokens,
|
|
671
684
|
contextTokens: detail.contextTokens,
|
|
672
685
|
costUsd: detail.costUsd,
|
|
686
|
+
sourceType: classifyTranscriptSource(bind, transcript ? isCodexCliCandidate(transcript) : false),
|
|
673
687
|
bound: !!bind,
|
|
674
688
|
boundChannel: bind?.channelType ?? null,
|
|
675
|
-
|
|
689
|
+
boundSelf: resolveSessionBindSelfName(bind, selfNames),
|
|
676
690
|
online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false,
|
|
677
691
|
};
|
|
678
692
|
return {
|
|
@@ -699,7 +713,7 @@ export function subscribeCodex(params, push) {
|
|
|
699
713
|
clearTimeout(debounce);
|
|
700
714
|
debounce = setTimeout(() => {
|
|
701
715
|
try {
|
|
702
|
-
|
|
716
|
+
void buildSnapshot(params).then(push).catch(() => { });
|
|
703
717
|
}
|
|
704
718
|
catch { }
|
|
705
719
|
}, 150);
|