ec-web 0.0.4 → 0.0.6
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 +63 -6
- package/dist/sources/msg.js +371 -14
- package/dist/sources/session-codex.js +115 -35
- package/dist/sources/session-origin.js +195 -0
- package/dist/sources/session.js +292 -41
- package/dist/sources/transcript-window.js +35 -0
- package/dist/static/app.js +456 -62
- package/dist/static/index.html +4 -5
- package/dist/static/style.css +153 -63
- package/dist/static/vendor/echarts-5.6.0.min.js +45 -0
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -20,7 +20,7 @@ import { ipcQuery } from './ipc-client.js';
|
|
|
20
20
|
import { setDebugLog, dlog } from './debug-log.js';
|
|
21
21
|
import { aidSource } from './sources/aid.js';
|
|
22
22
|
import { msgSource } from './sources/msg.js';
|
|
23
|
-
import { sessionSource, buildBindMap } from './sources/session.js';
|
|
23
|
+
import { sessionSource, buildBindMap, readClaudeSessionResult } from './sources/session.js';
|
|
24
24
|
import { cacheSource } from './sources/cache.js';
|
|
25
25
|
import { systemSource } from './sources/system.js';
|
|
26
26
|
import { triggersSource } from './sources/triggers.js';
|
|
@@ -34,6 +34,7 @@ import { relationRolesSource, handleRelationRolesApi, handlePeerRoleApi } from '
|
|
|
34
34
|
import { roleDefinitionsSource, handleRoleDefinitionsApi } from './sources/role-definitions.js';
|
|
35
35
|
import { handleModelsApi } from './sources/models.js';
|
|
36
36
|
import { detectBaseAgents } from './sources/baseagent-detector.js';
|
|
37
|
+
import { readCodexSessionResult } from './sources/session-codex.js';
|
|
37
38
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
39
|
const STATIC_DIR = path.join(__dirname, 'static');
|
|
39
40
|
const TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30天(滑动窗口:每次有效访问刷新 lastActive 自动续期)
|
|
@@ -66,10 +67,26 @@ const ECWEB_VERSION = readEcwebVersion();
|
|
|
66
67
|
const MIME = {
|
|
67
68
|
'.html': 'text/html; charset=utf-8',
|
|
68
69
|
'.js': 'text/javascript; charset=utf-8',
|
|
70
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
69
71
|
'.css': 'text/css; charset=utf-8',
|
|
70
72
|
'.json': 'application/json; charset=utf-8',
|
|
73
|
+
'.webmanifest': 'application/manifest+json; charset=utf-8',
|
|
71
74
|
'.svg': 'image/svg+xml',
|
|
75
|
+
'.png': 'image/png',
|
|
76
|
+
'.jpg': 'image/jpeg',
|
|
77
|
+
'.jpeg': 'image/jpeg',
|
|
78
|
+
'.gif': 'image/gif',
|
|
79
|
+
'.webp': 'image/webp',
|
|
80
|
+
'.avif': 'image/avif',
|
|
81
|
+
'.ico': 'image/x-icon',
|
|
82
|
+
'.woff': 'font/woff',
|
|
83
|
+
'.woff2': 'font/woff2',
|
|
84
|
+
'.ttf': 'font/ttf',
|
|
85
|
+
'.otf': 'font/otf',
|
|
86
|
+
'.wasm': 'application/wasm',
|
|
72
87
|
};
|
|
88
|
+
const HTML_CACHE_CONTROL = 'no-cache';
|
|
89
|
+
const STATIC_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
|
|
73
90
|
function tokenStorePath() {
|
|
74
91
|
const dir = resolvePaths().instanceDir;
|
|
75
92
|
const current = path.join(dir, 'ecweb-tokens.json');
|
|
@@ -141,7 +158,32 @@ function injectBaseHref(html, prefix) {
|
|
|
141
158
|
return html.replace(/<base\b[^>]*>/i, tag);
|
|
142
159
|
return html.replace(/<head[^>]*>/i, (m) => `${m}\n ${tag}`);
|
|
143
160
|
}
|
|
144
|
-
function
|
|
161
|
+
function createEtag(content) {
|
|
162
|
+
return `"${crypto.createHash('sha256').update(content).digest('hex')}"`;
|
|
163
|
+
}
|
|
164
|
+
function matchesIfNoneMatch(req, etag) {
|
|
165
|
+
const header = req.headers['if-none-match'];
|
|
166
|
+
const value = Array.isArray(header) ? header.join(',') : header;
|
|
167
|
+
if (!value)
|
|
168
|
+
return false;
|
|
169
|
+
// GET uses weak comparison, so a client may send the validator with W/.
|
|
170
|
+
return value.split(',').some((candidate) => {
|
|
171
|
+
const normalized = candidate.trim().replace(/^W\//, '');
|
|
172
|
+
return normalized === '*' || normalized === etag;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function respondWithValidator(req, res, body, contentType, cacheControl, extraHeaders = {}) {
|
|
176
|
+
const etag = createEtag(body);
|
|
177
|
+
const headers = { 'Content-Type': contentType, 'Cache-Control': cacheControl, ETag: etag, ...extraHeaders };
|
|
178
|
+
if (matchesIfNoneMatch(req, etag)) {
|
|
179
|
+
res.writeHead(304, headers);
|
|
180
|
+
res.end();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
res.writeHead(200, headers);
|
|
184
|
+
res.end(body);
|
|
185
|
+
}
|
|
186
|
+
export function serveStatic(req, res) {
|
|
145
187
|
let urlPath = (req.url || '/').split('?')[0];
|
|
146
188
|
if (urlPath === '/')
|
|
147
189
|
urlPath = '/index.html';
|
|
@@ -161,12 +203,13 @@ function serveStatic(req, res) {
|
|
|
161
203
|
if (ext === '.html') {
|
|
162
204
|
const prefix = forwardedPrefix(req);
|
|
163
205
|
const html = injectBaseHref(data.toString('utf-8'), prefix);
|
|
164
|
-
|
|
165
|
-
res
|
|
206
|
+
// The proxy prefix changes the representation, so shared caches must key on it.
|
|
207
|
+
respondWithValidator(req, res, html, MIME[ext], HTML_CACHE_CONTROL, { Vary: 'X-Forwarded-Prefix' });
|
|
166
208
|
return;
|
|
167
209
|
}
|
|
168
|
-
|
|
169
|
-
|
|
210
|
+
// Static files have stable ETags, including fonts, images, manifests, and vendored libraries.
|
|
211
|
+
// Their fixed URLs must revalidate so deployments never leave clients on an old asset.
|
|
212
|
+
respondWithValidator(req, res, data, MIME[ext] || 'application/octet-stream', STATIC_CACHE_CONTROL);
|
|
170
213
|
});
|
|
171
214
|
}
|
|
172
215
|
// ── Helpers ──
|
|
@@ -511,8 +554,14 @@ function handleConnection(ws, req, log) {
|
|
|
511
554
|
params.aid = msg.aid;
|
|
512
555
|
if (msg.peer)
|
|
513
556
|
params.peer = msg.peer;
|
|
557
|
+
if (msg.before != null)
|
|
558
|
+
params.before = msg.before;
|
|
514
559
|
if (msg.sessionId)
|
|
515
560
|
params.sessionId = msg.sessionId;
|
|
561
|
+
if (msg.turnBefore != null)
|
|
562
|
+
params.turnBefore = msg.turnBefore;
|
|
563
|
+
if (msg.turnLimit != null)
|
|
564
|
+
params.turnLimit = msg.turnLimit;
|
|
516
565
|
if (msg.project)
|
|
517
566
|
params.project = msg.project;
|
|
518
567
|
if (msg.agent)
|
|
@@ -527,6 +576,14 @@ function handleConnection(ws, req, log) {
|
|
|
527
576
|
await switchSubscription(msg.view, params);
|
|
528
577
|
return;
|
|
529
578
|
}
|
|
579
|
+
if (msg.type === 'session.result') {
|
|
580
|
+
const params = msg.params && typeof msg.params === 'object' ? msg.params : {};
|
|
581
|
+
const result = params.baseagent === 'codex'
|
|
582
|
+
? readCodexSessionResult(params)
|
|
583
|
+
: readClaudeSessionResult(params);
|
|
584
|
+
send({ type: 'session.result', requestId: msg.requestId, data: result == null ? null : { text: result } });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
530
587
|
if (msg.type === 'menu' && msg.payload) {
|
|
531
588
|
const p = resolvePaths();
|
|
532
589
|
const timeoutMs = Number.isFinite(Number(msg.timeoutMs)) && Number(msg.timeoutMs) > 0
|
package/dist/sources/msg.js
CHANGED
|
@@ -6,11 +6,17 @@
|
|
|
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;
|
|
19
|
+
const messageFileCache = new Map();
|
|
14
20
|
function getContextModules() {
|
|
15
21
|
if (!contextModulesPromise) {
|
|
16
22
|
contextModulesPromise = Promise.all([
|
|
@@ -21,6 +27,159 @@ function getContextModules() {
|
|
|
21
27
|
}
|
|
22
28
|
return contextModulesPromise;
|
|
23
29
|
}
|
|
30
|
+
function getPeerDisplayResolver() {
|
|
31
|
+
if (!peerDisplayResolverPromise) {
|
|
32
|
+
peerDisplayResolverPromise = Promise.resolve()
|
|
33
|
+
.then(() => import(toFileUrl(resolveParentDistModule('config', 'contact-book.js'))))
|
|
34
|
+
.then(mod => typeof mod.resolvePeerDisplayLabel === 'function' ? mod.resolvePeerDisplayLabel : null)
|
|
35
|
+
.catch(() => null);
|
|
36
|
+
}
|
|
37
|
+
return peerDisplayResolverPromise;
|
|
38
|
+
}
|
|
39
|
+
export function resolveContactPeerName(resolver, identity, peerId, fallback) {
|
|
40
|
+
if (!resolver)
|
|
41
|
+
return fallback;
|
|
42
|
+
try {
|
|
43
|
+
return resolver(identity.selfAID, identity.channelType, peerId, identity.channel) || fallback;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function aunGroupIdVariants(groupId) {
|
|
50
|
+
const id = String(groupId || '').trim();
|
|
51
|
+
if (!id)
|
|
52
|
+
return [];
|
|
53
|
+
const variants = new Set([id]);
|
|
54
|
+
const legacy = /^group\.agentid\.pub\/([^/]+)$/i.exec(id);
|
|
55
|
+
if (legacy)
|
|
56
|
+
variants.add(`${legacy[1]}.agentid.pub`);
|
|
57
|
+
const current = /^([^.]+)\.agentid\.pub$/i.exec(id);
|
|
58
|
+
if (current)
|
|
59
|
+
variants.add(`group.agentid.pub/${current[1]}`);
|
|
60
|
+
return [...variants];
|
|
61
|
+
}
|
|
62
|
+
export function resolveAunGroupName(names, groupId, fallback) {
|
|
63
|
+
for (const id of aunGroupIdVariants(groupId || '')) {
|
|
64
|
+
const name = names.get(id);
|
|
65
|
+
if (name)
|
|
66
|
+
return name;
|
|
67
|
+
}
|
|
68
|
+
return fallback;
|
|
69
|
+
}
|
|
70
|
+
async function fetchAunGroupNames(aid) {
|
|
71
|
+
const names = new Map();
|
|
72
|
+
let cursor = null;
|
|
73
|
+
let page = 0;
|
|
74
|
+
do {
|
|
75
|
+
page += 1;
|
|
76
|
+
const response = await ipcQuery(resolvePaths().socket, {
|
|
77
|
+
type: 'menu.exec',
|
|
78
|
+
payload: {
|
|
79
|
+
type: 'menu.query',
|
|
80
|
+
name: 'connect',
|
|
81
|
+
cmd: '/connect',
|
|
82
|
+
agent: aid,
|
|
83
|
+
id: `ecweb-msg-groups-${Date.now()}-${page}`,
|
|
84
|
+
args: { self: aid, view: 'groups', limit: 200, ...(cursor ? { cursor } : {}) },
|
|
85
|
+
},
|
|
86
|
+
auth: { source: 'ecweb', localDirect: true },
|
|
87
|
+
}, 10_000);
|
|
88
|
+
const data = response?.ok && !response.response?.error ? response.response?.data : null;
|
|
89
|
+
if (!data || !Array.isArray(data.items))
|
|
90
|
+
break;
|
|
91
|
+
for (const item of data.items) {
|
|
92
|
+
const groupId = String(item?.groupId || '').trim();
|
|
93
|
+
const displayName = String(item?.displayName || '').trim();
|
|
94
|
+
if (!groupId || !displayName)
|
|
95
|
+
continue;
|
|
96
|
+
for (const variant of aunGroupIdVariants(groupId))
|
|
97
|
+
names.set(variant, displayName);
|
|
98
|
+
}
|
|
99
|
+
cursor = typeof data.nextCursor === 'string' && data.nextCursor ? data.nextCursor : null;
|
|
100
|
+
} while (cursor && page < 20);
|
|
101
|
+
return names;
|
|
102
|
+
}
|
|
103
|
+
function getAunGroupNames(aid) {
|
|
104
|
+
const cached = aunGroupNameCache.get(aid);
|
|
105
|
+
if (cached && cached.expiresAt > Date.now())
|
|
106
|
+
return Promise.resolve(cached.names);
|
|
107
|
+
const pending = aunGroupNameRequests.get(aid);
|
|
108
|
+
if (pending)
|
|
109
|
+
return pending;
|
|
110
|
+
const request = fetchAunGroupNames(aid)
|
|
111
|
+
.then(names => {
|
|
112
|
+
aunGroupNameCache.set(aid, {
|
|
113
|
+
names,
|
|
114
|
+
expiresAt: Date.now() + (names.size ? 5 * 60_000 : 15_000),
|
|
115
|
+
});
|
|
116
|
+
return names;
|
|
117
|
+
})
|
|
118
|
+
.catch(() => new Map())
|
|
119
|
+
.finally(() => aunGroupNameRequests.delete(aid));
|
|
120
|
+
aunGroupNameRequests.set(aid, request);
|
|
121
|
+
return request;
|
|
122
|
+
}
|
|
123
|
+
async function hydrateAunGroupNames(chats, waitForRefresh = false) {
|
|
124
|
+
const aids = [...new Set(chats
|
|
125
|
+
.filter(chat => chat.channelType === 'aun' && chat.chatType === 'group')
|
|
126
|
+
.map(chat => chat.selfAID))];
|
|
127
|
+
if (!aids.length)
|
|
128
|
+
return;
|
|
129
|
+
const directories = new Map();
|
|
130
|
+
if (waitForRefresh) {
|
|
131
|
+
for (const [aid, names] of await Promise.all(aids.map(async (aid) => [aid, await getAunGroupNames(aid)]))) {
|
|
132
|
+
directories.set(aid, names);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
for (const aid of aids) {
|
|
137
|
+
const cached = aunGroupNameCache.get(aid);
|
|
138
|
+
if (cached)
|
|
139
|
+
directories.set(aid, cached.names);
|
|
140
|
+
if (!cached || cached.expiresAt <= Date.now())
|
|
141
|
+
void getAunGroupNames(aid);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const chat of chats) {
|
|
145
|
+
if (chat.channelType !== 'aun' || chat.chatType !== 'group')
|
|
146
|
+
continue;
|
|
147
|
+
const groupName = resolveAunGroupName(directories.get(chat.selfAID) || new Map(), chat.groupId, chat.groupName);
|
|
148
|
+
if (!groupName)
|
|
149
|
+
continue;
|
|
150
|
+
chat.groupName = groupName;
|
|
151
|
+
chat.peerName = groupName;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export function resolveSessionBindDisplayName(bind, resolver, aunGroupNames = new Map()) {
|
|
155
|
+
const isGroup = bind.chatType === 'group' || !!bind.groupId;
|
|
156
|
+
if (bind.channelType === 'aun' && isGroup) {
|
|
157
|
+
return resolveAunGroupName(aunGroupNames, bind.groupId || bind.channelId, bind.groupName || null);
|
|
158
|
+
}
|
|
159
|
+
if (isGroup)
|
|
160
|
+
return bind.groupName || null;
|
|
161
|
+
return resolveContactPeerName(resolver, {
|
|
162
|
+
selfAID: bind.selfAID,
|
|
163
|
+
channelType: bind.channelType,
|
|
164
|
+
channel: bind.channelKey || bind.channelType,
|
|
165
|
+
channelId: bind.channelId,
|
|
166
|
+
}, bind.peerId || bind.channelId, bind.peerName);
|
|
167
|
+
}
|
|
168
|
+
/** 为会话页绑定标签补齐联系人名或 AUN 群名,不改变原始 channelId。 */
|
|
169
|
+
export async function hydrateSessionBindDisplayNames(binds) {
|
|
170
|
+
const items = [...binds];
|
|
171
|
+
if (!items.length)
|
|
172
|
+
return;
|
|
173
|
+
const resolver = await getPeerDisplayResolver();
|
|
174
|
+
const groupAids = [...new Set(items
|
|
175
|
+
.filter(bind => bind.channelType === 'aun' && (bind.chatType === 'group' || !!bind.groupId))
|
|
176
|
+
.map(bind => bind.selfAID)
|
|
177
|
+
.filter(Boolean))];
|
|
178
|
+
const groupDirectories = new Map(await Promise.all(groupAids.map(async (aid) => [aid, await getAunGroupNames(aid)])));
|
|
179
|
+
for (const bind of items) {
|
|
180
|
+
bind.displayName = resolveSessionBindDisplayName(bind, resolver, groupDirectories.get(bind.selfAID) || new Map());
|
|
181
|
+
}
|
|
182
|
+
}
|
|
24
183
|
function getSessionAdapter(baseagent) {
|
|
25
184
|
if (!sessionAdapterPromises.has(baseagent)) {
|
|
26
185
|
const classNames = {
|
|
@@ -63,6 +222,131 @@ function findMessageFiles(root) {
|
|
|
63
222
|
visit(root);
|
|
64
223
|
return out;
|
|
65
224
|
}
|
|
225
|
+
function fileSignature(file) {
|
|
226
|
+
try {
|
|
227
|
+
const stat = fs.statSync(file);
|
|
228
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return '';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function chatMetadataSignature(dirPath) {
|
|
235
|
+
const parts = [
|
|
236
|
+
`active:${fileSignature(path.join(dirPath, 'active.json'))}`,
|
|
237
|
+
];
|
|
238
|
+
let rootEntries = [];
|
|
239
|
+
try {
|
|
240
|
+
rootEntries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
241
|
+
}
|
|
242
|
+
catch { }
|
|
243
|
+
for (const entry of rootEntries) {
|
|
244
|
+
if (entry.isFile() && entry.name.startsWith('meta_') && entry.name.endsWith('.jsonl')) {
|
|
245
|
+
parts.push(`${entry.name}:${fileSignature(path.join(dirPath, entry.name))}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const threadsDir = path.join(dirPath, '_threads');
|
|
249
|
+
let threadEntries = [];
|
|
250
|
+
try {
|
|
251
|
+
threadEntries = fs.readdirSync(threadsDir, { withFileTypes: true });
|
|
252
|
+
}
|
|
253
|
+
catch { }
|
|
254
|
+
for (const entry of threadEntries) {
|
|
255
|
+
if (entry.isFile() && (entry.name === 'thread-index.json' || entry.name.endsWith('.jsonl'))) {
|
|
256
|
+
parts.push(`threads/${entry.name}:${fileSignature(path.join(threadsDir, entry.name))}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return parts.sort().join('|');
|
|
260
|
+
}
|
|
261
|
+
function appendMessageRows(file, cached, metrics) {
|
|
262
|
+
let stat;
|
|
263
|
+
try {
|
|
264
|
+
stat = fs.statSync(file);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (stat.size < cached.offset || (stat.size === cached.offset && cached.mtimeMs && stat.mtimeMs !== cached.mtimeMs)) {
|
|
270
|
+
cached.offset = 0;
|
|
271
|
+
cached.messages = [];
|
|
272
|
+
}
|
|
273
|
+
if (stat.size === cached.offset)
|
|
274
|
+
return false;
|
|
275
|
+
const bytes = stat.size - cached.offset;
|
|
276
|
+
const buffer = Buffer.allocUnsafe(bytes);
|
|
277
|
+
let fd = null;
|
|
278
|
+
try {
|
|
279
|
+
fd = fs.openSync(file, 'r');
|
|
280
|
+
fs.readSync(fd, buffer, 0, bytes, cached.offset);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
if (fd != null)
|
|
287
|
+
try {
|
|
288
|
+
fs.closeSync(fd);
|
|
289
|
+
}
|
|
290
|
+
catch { }
|
|
291
|
+
}
|
|
292
|
+
let completeBytes = buffer.length;
|
|
293
|
+
const lastBreak = buffer.lastIndexOf(0x0a);
|
|
294
|
+
if (lastBreak !== buffer.length - 1) {
|
|
295
|
+
const trailing = buffer.subarray(lastBreak + 1).toString('utf8').trim();
|
|
296
|
+
try {
|
|
297
|
+
JSON.parse(trailing);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
completeBytes = lastBreak + 1;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (completeBytes <= 0)
|
|
304
|
+
return false;
|
|
305
|
+
let parsedLines = 0;
|
|
306
|
+
for (const line of buffer.subarray(0, completeBytes).toString('utf8').split(/\r?\n/)) {
|
|
307
|
+
if (!line)
|
|
308
|
+
continue;
|
|
309
|
+
try {
|
|
310
|
+
const message = JSON.parse(line);
|
|
311
|
+
parsedLines++;
|
|
312
|
+
if (!isTransientProtocolMessage(message))
|
|
313
|
+
cached.messages.push(message);
|
|
314
|
+
}
|
|
315
|
+
catch { }
|
|
316
|
+
}
|
|
317
|
+
cached.offset += completeBytes;
|
|
318
|
+
cached.mtimeMs = stat.mtimeMs;
|
|
319
|
+
metrics.changedFiles++;
|
|
320
|
+
metrics.parsedLines += parsedLines;
|
|
321
|
+
metrics.parsedBytes += completeBytes;
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
function cachedMessageFile(file, metrics) {
|
|
325
|
+
let cached = messageFileCache.get(file);
|
|
326
|
+
if (!cached) {
|
|
327
|
+
cached = {
|
|
328
|
+
offset: 0,
|
|
329
|
+
mtimeMs: 0,
|
|
330
|
+
messages: [],
|
|
331
|
+
active: {},
|
|
332
|
+
threadSessions: [],
|
|
333
|
+
mainSessionIds: new Set(),
|
|
334
|
+
metadataSignature: '',
|
|
335
|
+
};
|
|
336
|
+
messageFileCache.set(file, cached);
|
|
337
|
+
}
|
|
338
|
+
appendMessageRows(file, cached, metrics);
|
|
339
|
+
const dirPath = path.dirname(file);
|
|
340
|
+
const metadataSignature = chatMetadataSignature(dirPath);
|
|
341
|
+
if (metadataSignature !== cached.metadataSignature) {
|
|
342
|
+
cached.active = readJsonFile(path.join(dirPath, 'active.json')) ?? {};
|
|
343
|
+
cached.threadSessions = loadThreadSessions(dirPath);
|
|
344
|
+
cached.mainSessionIds = loadMainSessionIds(dirPath, cached.active);
|
|
345
|
+
cached.metadataSignature = metadataSignature;
|
|
346
|
+
metrics.metadataRefreshes++;
|
|
347
|
+
}
|
|
348
|
+
return cached;
|
|
349
|
+
}
|
|
66
350
|
function readLastJsonlObject(file) {
|
|
67
351
|
const rows = readAllJsonlLines(file);
|
|
68
352
|
return rows.length ? rows[rows.length - 1] : null;
|
|
@@ -173,6 +457,37 @@ function listConfiguredAgentAids(root) {
|
|
|
173
457
|
}
|
|
174
458
|
return aids;
|
|
175
459
|
}
|
|
460
|
+
function shortAid(value) {
|
|
461
|
+
return String(value || '').split('.')[0] || String(value || '');
|
|
462
|
+
}
|
|
463
|
+
/** Returns local Agent display names, preferring the runtime selfName. */
|
|
464
|
+
export async function getAgentSelfNames() {
|
|
465
|
+
if (agentDisplayNamesCache && agentDisplayNamesCache.expiresAt > Date.now()) {
|
|
466
|
+
return agentDisplayNamesCache.names;
|
|
467
|
+
}
|
|
468
|
+
const names = new Map();
|
|
469
|
+
try {
|
|
470
|
+
const [agentsResp, statsResp] = await Promise.all([
|
|
471
|
+
ipcQuery(resolvePaths().socket, { type: 'evolagent.list' }, 4000),
|
|
472
|
+
ipcQuery(resolvePaths().socket, { type: 'aun-aid-stats' }, 4000),
|
|
473
|
+
]);
|
|
474
|
+
const statsByAid = new Map((statsResp?.stats || []).map(item => [String(item?.aid || ''), item]));
|
|
475
|
+
for (const agent of agentsResp?.agents || []) {
|
|
476
|
+
const aid = String(agent?.aid || '').trim();
|
|
477
|
+
if (!aid)
|
|
478
|
+
continue;
|
|
479
|
+
const stats = statsByAid.get(aid);
|
|
480
|
+
const displayName = String(stats?.selfName || agent?.displayName || shortAid(aid)).trim();
|
|
481
|
+
if (displayName)
|
|
482
|
+
names.set(aid, displayName);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// daemon unavailable: leave the map empty and use the AID fallback below
|
|
487
|
+
}
|
|
488
|
+
agentDisplayNamesCache = { expiresAt: Date.now() + 5000, names };
|
|
489
|
+
return names;
|
|
490
|
+
}
|
|
176
491
|
function decodeDirSegment(seg) {
|
|
177
492
|
return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
178
493
|
}
|
|
@@ -292,17 +607,22 @@ function chatDisplayId(active, messages) {
|
|
|
292
607
|
groupName: null,
|
|
293
608
|
};
|
|
294
609
|
}
|
|
295
|
-
function loadChats() {
|
|
610
|
+
function loadChats(peerDisplayResolver, metrics) {
|
|
296
611
|
const paths = resolvePaths();
|
|
297
612
|
const sessionsDir = paths.sessionsDir;
|
|
298
613
|
const configuredAids = listConfiguredAgentAids(paths.root);
|
|
299
614
|
const files = findMessageFiles(sessionsDir);
|
|
615
|
+
const liveFiles = new Set(files);
|
|
616
|
+
for (const file of messageFileCache.keys()) {
|
|
617
|
+
if (!liveFiles.has(file))
|
|
618
|
+
messageFileCache.delete(file);
|
|
619
|
+
}
|
|
620
|
+
metrics.files = files.length;
|
|
300
621
|
const chats = [];
|
|
301
622
|
for (const file of files) {
|
|
302
623
|
const dirPath = path.dirname(file);
|
|
303
|
-
const
|
|
304
|
-
const messages =
|
|
305
|
-
.filter(m => !isTransientProtocolMessage(m));
|
|
624
|
+
const cached = cachedMessageFile(file, metrics);
|
|
625
|
+
const { active, messages } = cached;
|
|
306
626
|
if (!messages.length)
|
|
307
627
|
continue;
|
|
308
628
|
const identity = normalizeChatIdentity(sessionsDir, dirPath, active, messages);
|
|
@@ -311,9 +631,12 @@ function loadChats() {
|
|
|
311
631
|
continue;
|
|
312
632
|
const chatType = String(active.chatType || messages.find(m => m.chatType)?.chatType || 'private');
|
|
313
633
|
const display = chatDisplayId({ ...active, chatType, channelId }, messages);
|
|
314
|
-
const
|
|
634
|
+
const peerName = chatType === 'group'
|
|
635
|
+
? display.peerName
|
|
636
|
+
: resolveContactPeerName(peerDisplayResolver, identity, display.peerId, display.peerName);
|
|
637
|
+
const threadSessions = cached.threadSessions;
|
|
315
638
|
const threadById = new Map(threadSessions.map(item => [item.threadId, item]));
|
|
316
|
-
const mainSessionIds =
|
|
639
|
+
const mainSessionIds = cached.mainSessionIds;
|
|
317
640
|
for (const item of partitionMessages(messages, threadSessions, mainSessionIds)) {
|
|
318
641
|
if (!item.messages.length)
|
|
319
642
|
continue;
|
|
@@ -331,7 +654,7 @@ function loadChats() {
|
|
|
331
654
|
selfAID,
|
|
332
655
|
chatType,
|
|
333
656
|
peerId: display.peerId,
|
|
334
|
-
peerName
|
|
657
|
+
peerName,
|
|
335
658
|
groupId: display.groupId,
|
|
336
659
|
groupName: display.groupName,
|
|
337
660
|
partition: item.partition,
|
|
@@ -353,7 +676,7 @@ function scopeId(chat) {
|
|
|
353
676
|
function legacyChannelScopeId(chat) {
|
|
354
677
|
return safeId(`${chat.selfAID}\n${chat.channelType}\n${chat.channel}`);
|
|
355
678
|
}
|
|
356
|
-
function buildScopes(chats) {
|
|
679
|
+
function buildScopes(chats, agentDisplayNames = new Map()) {
|
|
357
680
|
const map = new Map();
|
|
358
681
|
const seenChats = new Map();
|
|
359
682
|
const seenChannels = new Map();
|
|
@@ -366,6 +689,7 @@ function buildScopes(chats) {
|
|
|
366
689
|
id,
|
|
367
690
|
aid: id,
|
|
368
691
|
selfAID: chat.selfAID,
|
|
692
|
+
displayName: agentDisplayNames.get(chat.selfAID) || shortAid(chat.selfAID),
|
|
369
693
|
totalIn: 0,
|
|
370
694
|
totalOut: 0,
|
|
371
695
|
peerCount: 0,
|
|
@@ -471,7 +795,7 @@ function selectedChats(chats, scope) {
|
|
|
471
795
|
return [];
|
|
472
796
|
return chats.filter(chat => scopeId(chat) === scope);
|
|
473
797
|
}
|
|
474
|
-
function messagesFor(chats, peer) {
|
|
798
|
+
function messagesFor(chats, peer, before, limit) {
|
|
475
799
|
const selected = peer
|
|
476
800
|
? chats.filter(chat => chatKey(chat) === peer)
|
|
477
801
|
: chats;
|
|
@@ -486,7 +810,13 @@ function messagesFor(chats, peer) {
|
|
|
486
810
|
threadName: chat.threadName,
|
|
487
811
|
})));
|
|
488
812
|
messages.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
489
|
-
|
|
813
|
+
const end = before == null ? messages.length : Math.min(Math.max(before, 0), messages.length);
|
|
814
|
+
const start = Math.max(0, end - limit);
|
|
815
|
+
return {
|
|
816
|
+
messages: messages.slice(start, end),
|
|
817
|
+
nextBefore: start > 0 ? start : null,
|
|
818
|
+
hasMore: start > 0,
|
|
819
|
+
};
|
|
490
820
|
}
|
|
491
821
|
function latestInbound(chat) {
|
|
492
822
|
for (let i = chat.messages.length - 1; i >= 0; i--) {
|
|
@@ -566,8 +896,21 @@ async function buildSessionContext(chat) {
|
|
|
566
896
|
};
|
|
567
897
|
}
|
|
568
898
|
async function buildSnapshot(params) {
|
|
569
|
-
const
|
|
570
|
-
const
|
|
899
|
+
const startedAt = Date.now();
|
|
900
|
+
const metrics = {
|
|
901
|
+
files: 0,
|
|
902
|
+
changedFiles: 0,
|
|
903
|
+
parsedLines: 0,
|
|
904
|
+
parsedBytes: 0,
|
|
905
|
+
metadataRefreshes: 0,
|
|
906
|
+
scanMs: 0,
|
|
907
|
+
totalMs: 0,
|
|
908
|
+
};
|
|
909
|
+
const peerDisplayResolver = await getPeerDisplayResolver();
|
|
910
|
+
const scanStartedAt = Date.now();
|
|
911
|
+
const chats = loadChats(peerDisplayResolver, metrics);
|
|
912
|
+
metrics.scanMs = Date.now() - scanStartedAt;
|
|
913
|
+
const scopes = buildScopes(chats, await getAgentSelfNames());
|
|
571
914
|
const requestedScope = params.scope || params.aid || null;
|
|
572
915
|
const scope = requestedScope && !scopes.some(s => s.id === requestedScope)
|
|
573
916
|
? (chats.find(chat => legacyChannelScopeId(chat) === requestedScope)
|
|
@@ -576,19 +919,33 @@ async function buildSnapshot(params) {
|
|
|
576
919
|
: requestedScope;
|
|
577
920
|
const peer = params.peer || null;
|
|
578
921
|
const inScope = selectedChats(chats, scope);
|
|
922
|
+
if (scope)
|
|
923
|
+
await hydrateAunGroupNames(inScope);
|
|
579
924
|
const peers = scope ? mergePeerInfo(inScope) : [];
|
|
580
|
-
const
|
|
925
|
+
const requestedLimit = Number(params.limit);
|
|
926
|
+
const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(Math.trunc(requestedLimit), 20), 200) : 100;
|
|
927
|
+
const requestedBefore = params.before == null ? Number.NaN : Number(params.before);
|
|
928
|
+
const before = Number.isFinite(requestedBefore) && requestedBefore >= 0 ? Math.trunc(requestedBefore) : null;
|
|
929
|
+
const page = scope && peer ? messagesFor(inScope, peer, before, limit) : { messages: [], nextBefore: null, hasMore: false };
|
|
581
930
|
const currentChat = peer ? inScope.find(chat => chatKey(chat) === peer) : undefined;
|
|
582
931
|
const sessionContext = await buildSessionContext(currentChat);
|
|
932
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
583
933
|
return {
|
|
584
934
|
scopes,
|
|
585
935
|
aids: scopes,
|
|
586
936
|
peers,
|
|
587
|
-
messages,
|
|
937
|
+
messages: page.messages,
|
|
938
|
+
messagePage: {
|
|
939
|
+
limit,
|
|
940
|
+
before,
|
|
941
|
+
nextBefore: page.nextBefore,
|
|
942
|
+
hasMore: page.hasMore,
|
|
943
|
+
},
|
|
588
944
|
scope,
|
|
589
945
|
aid: scope,
|
|
590
946
|
peer,
|
|
591
947
|
sessionContext,
|
|
948
|
+
metrics,
|
|
592
949
|
};
|
|
593
950
|
}
|
|
594
951
|
export const msgSource = {
|