ec-web 0.0.5 → 0.0.7
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/fs-utils.js +89 -65
- package/dist/paths.js +1 -1
- package/dist/server.js +16 -1
- package/dist/sources/msg.js +218 -34
- package/dist/sources/session-codex.js +72 -6
- package/dist/sources/session.js +84 -11
- package/dist/sources/stats.js +1 -1
- package/dist/sources/transcript-window.js +35 -0
- package/dist/static/app.js +196 -21
- package/dist/static/index.html +1 -1
- package/dist/static/style.css +98 -62
- package/package.json +1 -1
package/dist/fs-utils.js
CHANGED
|
@@ -14,72 +14,104 @@ export function encodeSegment(s) {
|
|
|
14
14
|
export function decodeDirSegment(seg) {
|
|
15
15
|
return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
16
16
|
}
|
|
17
|
+
function encodeChannelSessionComponent(value) {
|
|
18
|
+
const encoded = encodeURIComponent(value).replace(/\*/g, '%2A');
|
|
19
|
+
return encoded.endsWith('.') ? `${encoded.slice(0, -1)}%2E` : encoded;
|
|
20
|
+
}
|
|
21
|
+
function formatChannelSessionKeyParts(channelType, channelName, channelId) {
|
|
22
|
+
if (!channelType || !channelName || !channelId)
|
|
23
|
+
throw new Error('channel session key components must not be empty');
|
|
24
|
+
const key = [channelType, channelName, channelId].map(encodeChannelSessionComponent).join('#');
|
|
25
|
+
if (Buffer.byteLength(key, 'utf8') > 255)
|
|
26
|
+
throw new Error('channel session key exceeds the 255-byte filesystem limit');
|
|
27
|
+
return key;
|
|
28
|
+
}
|
|
29
|
+
export function formatChannelSessionKey(channelType, channelId, channelKey) {
|
|
30
|
+
const parts = channelKey?.split('#');
|
|
31
|
+
const channelName = parts?.length === 3 && parts[0] === channelType && parts[2]?.trim()
|
|
32
|
+
? parts[2].trim()
|
|
33
|
+
: 'main';
|
|
34
|
+
return formatChannelSessionKeyParts(channelType, channelName, channelId);
|
|
35
|
+
}
|
|
36
|
+
export function tryParseChannelSessionKey(key) {
|
|
37
|
+
const parts = key.split('#');
|
|
38
|
+
if (parts.length !== 3 || parts.some(part => !part))
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = {
|
|
42
|
+
channelType: decodeURIComponent(parts[0]),
|
|
43
|
+
channelName: decodeURIComponent(parts[1]),
|
|
44
|
+
channelId: decodeURIComponent(parts[2]),
|
|
45
|
+
};
|
|
46
|
+
if (!parsed.channelType || !parsed.channelName || !parsed.channelId)
|
|
47
|
+
return undefined;
|
|
48
|
+
return formatChannelSessionKeyParts(parsed.channelType, parsed.channelName, parsed.channelId) === key
|
|
49
|
+
? parsed
|
|
50
|
+
: undefined;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
17
56
|
/** 与 evolcore cross-platform.ts encodePath 一致(CC transcript 目录名) */
|
|
18
57
|
export function encodePath(p) {
|
|
19
58
|
const isWindows = process.platform === 'win32';
|
|
20
59
|
const norm = isWindows ? p.replace(/\\/g, '/') : p;
|
|
21
60
|
return encodeURIComponent(norm).replace(/%2F/gi, '-').replace(/%3A/gi, '-').replace(/^-/, '');
|
|
22
61
|
}
|
|
62
|
+
function hasSessionPayload(dirPath) {
|
|
63
|
+
try {
|
|
64
|
+
return fs.readdirSync(dirPath, { withFileTypes: true }).some(entry => (entry.isFile() && (entry.name === 'active.json'
|
|
65
|
+
|| entry.name === 'messages.jsonl'
|
|
66
|
+
|| (entry.name.startsWith('meta_') && entry.name.endsWith('.jsonl')))) || (entry.isDirectory() && entry.name === '_threads'));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
23
72
|
export function scanChatDirs(sessionsDir) {
|
|
24
73
|
const results = [];
|
|
25
|
-
let
|
|
74
|
+
let agents;
|
|
26
75
|
try {
|
|
27
|
-
|
|
76
|
+
agents = fs.readdirSync(sessionsDir, { withFileTypes: true });
|
|
28
77
|
}
|
|
29
78
|
catch {
|
|
30
79
|
return results;
|
|
31
80
|
}
|
|
32
|
-
for (const
|
|
33
|
-
if (!
|
|
81
|
+
for (const agent of agents) {
|
|
82
|
+
if (!agent.isDirectory())
|
|
34
83
|
continue;
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
let
|
|
84
|
+
const selfAID = decodeDirSegment(agent.name);
|
|
85
|
+
const agentSessions = path.join(sessionsDir, agent.name, 'sessions');
|
|
86
|
+
let sessions;
|
|
38
87
|
try {
|
|
39
|
-
|
|
88
|
+
sessions = fs.readdirSync(agentSessions, { withFileTypes: true });
|
|
40
89
|
}
|
|
41
90
|
catch {
|
|
42
91
|
continue;
|
|
43
92
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
channelId: decodeDirSegment(peerEnc.name),
|
|
64
|
-
selfAID,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
for (const chanEnc of level2) {
|
|
71
|
-
if (!chanEnc.isDirectory())
|
|
72
|
-
continue;
|
|
73
|
-
results.push({
|
|
74
|
-
dirPath: path.join(ctDir, chanEnc.name),
|
|
75
|
-
channelType,
|
|
76
|
-
channelId: decodeDirSegment(chanEnc.name),
|
|
77
|
-
selfAID: '',
|
|
78
|
-
});
|
|
79
|
-
}
|
|
93
|
+
for (const session of sessions) {
|
|
94
|
+
if (!session.isDirectory())
|
|
95
|
+
continue;
|
|
96
|
+
const parsed = tryParseChannelSessionKey(session.name);
|
|
97
|
+
if (!parsed)
|
|
98
|
+
continue;
|
|
99
|
+
const dirPath = path.join(agentSessions, session.name);
|
|
100
|
+
if (!hasSessionPayload(dirPath))
|
|
101
|
+
continue;
|
|
102
|
+
results.push({
|
|
103
|
+
dirPath,
|
|
104
|
+
channelType: parsed.channelType,
|
|
105
|
+
channelKey: parsed.channelType === 'daemon'
|
|
106
|
+
? 'daemon'
|
|
107
|
+
: `${parsed.channelType}#${selfAID}#${parsed.channelName}`,
|
|
108
|
+
channelId: parsed.channelId,
|
|
109
|
+
channelSessionKey: session.name,
|
|
110
|
+
selfAID,
|
|
111
|
+
});
|
|
80
112
|
}
|
|
81
113
|
}
|
|
82
|
-
return results;
|
|
114
|
+
return results.sort((left, right) => left.dirPath < right.dirPath ? -1 : left.dirPath > right.dirPath ? 1 : 0);
|
|
83
115
|
}
|
|
84
116
|
export function readJsonFile(filePath) {
|
|
85
117
|
try {
|
|
@@ -123,34 +155,26 @@ export function isTransientProtocolMessage(entry) {
|
|
|
123
155
|
return transient(entry.payloadType) || transient(entry.msgType);
|
|
124
156
|
}
|
|
125
157
|
export function getSessionsAunDir() {
|
|
126
|
-
return
|
|
158
|
+
return resolvePaths().sessionsDir;
|
|
127
159
|
}
|
|
128
160
|
export function listLocalAids(aunDir) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
.map(e => decodeDirSegment(e.name));
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
161
|
+
return [...new Set(scanChatDirs(aunDir)
|
|
162
|
+
.filter(chat => chat.channelType === 'aun')
|
|
163
|
+
.map(chat => chat.selfAID))].sort();
|
|
137
164
|
}
|
|
138
165
|
export function listPeers(aunDir, localAid) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return [];
|
|
147
|
-
}
|
|
166
|
+
return [...new Set(scanChatDirs(aunDir)
|
|
167
|
+
.filter(chat => chat.channelType === 'aun' && chat.selfAID === localAid)
|
|
168
|
+
.map(chat => chat.channelId))].sort();
|
|
169
|
+
}
|
|
170
|
+
function aunChatDir(aunDir, localAid, peerId) {
|
|
171
|
+
return path.join(aunDir, encodeSegment(localAid), 'sessions', formatChannelSessionKey('aun', peerId, `aun#${localAid}#main`));
|
|
148
172
|
}
|
|
149
173
|
export function readMessages(aunDir, localAid, peerId) {
|
|
150
|
-
return readAllJsonlLines(path.join(aunDir,
|
|
174
|
+
return readAllJsonlLines(path.join(aunChatDir(aunDir, localAid, peerId), 'messages.jsonl')).filter(m => !isTransientProtocolMessage(m));
|
|
151
175
|
}
|
|
152
176
|
function readPeerName(aunDir, localAid, peerId) {
|
|
153
|
-
return readJsonFile(path.join(aunDir,
|
|
177
|
+
return readJsonFile(path.join(aunChatDir(aunDir, localAid, peerId), 'active.json'))?.metadata?.peerName ?? null;
|
|
154
178
|
}
|
|
155
179
|
export function loadAidInfo(aunDir, aid) {
|
|
156
180
|
const peers = listPeers(aunDir, aid);
|
package/dist/paths.js
CHANGED
|
@@ -42,7 +42,7 @@ export function resolvePaths() {
|
|
|
42
42
|
const root = resolveRoot();
|
|
43
43
|
return {
|
|
44
44
|
root,
|
|
45
|
-
sessionsDir: path.join(root, '
|
|
45
|
+
sessionsDir: path.join(root, 'agents'),
|
|
46
46
|
instanceDir: path.join(root, 'data', 'instance'),
|
|
47
47
|
dataDir: path.join(root, 'data'),
|
|
48
48
|
logs: path.join(root, 'logs'),
|
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 自动续期)
|
|
@@ -553,8 +554,14 @@ function handleConnection(ws, req, log) {
|
|
|
553
554
|
params.aid = msg.aid;
|
|
554
555
|
if (msg.peer)
|
|
555
556
|
params.peer = msg.peer;
|
|
557
|
+
if (msg.before != null)
|
|
558
|
+
params.before = msg.before;
|
|
556
559
|
if (msg.sessionId)
|
|
557
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;
|
|
558
565
|
if (msg.project)
|
|
559
566
|
params.project = msg.project;
|
|
560
567
|
if (msg.agent)
|
|
@@ -569,6 +576,14 @@ function handleConnection(ws, req, log) {
|
|
|
569
576
|
await switchSubscription(msg.view, params);
|
|
570
577
|
return;
|
|
571
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
|
+
}
|
|
572
587
|
if (msg.type === 'menu' && msg.payload) {
|
|
573
588
|
const p = resolvePaths();
|
|
574
589
|
const timeoutMs = Number.isFinite(Number(msg.timeoutMs)) && Number(msg.timeoutMs) > 0
|
package/dist/sources/msg.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 消息数据源:扫描
|
|
2
|
+
* 消息数据源:扫描 agents/<aid>/sessions 下所有渠道的 messages.jsonl。
|
|
3
3
|
*
|
|
4
4
|
* 左列按 agent 聚合;中列列出该 agent 下所有渠道的私聊/群聊会话。
|
|
5
5
|
* 兼容旧参数名 aid/peer:前端的 aid 实际上传 scope id。
|
|
@@ -9,13 +9,14 @@ import path from 'path';
|
|
|
9
9
|
import { ipcQuery } from '../ipc-client.js';
|
|
10
10
|
import { resolvePaths } from '../paths.js';
|
|
11
11
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
12
|
-
import { isTransientProtocolMessage, readAllJsonlLines, readJsonFile, } from '../fs-utils.js';
|
|
12
|
+
import { decodeDirSegment, isTransientProtocolMessage, readAllJsonlLines, readJsonFile, scanChatDirs, tryParseChannelSessionKey, } from '../fs-utils.js';
|
|
13
13
|
let contextModulesPromise = null;
|
|
14
14
|
const sessionAdapterPromises = new Map();
|
|
15
15
|
let peerDisplayResolverPromise = null;
|
|
16
16
|
const aunGroupNameCache = new Map();
|
|
17
17
|
const aunGroupNameRequests = new Map();
|
|
18
18
|
let agentDisplayNamesCache = null;
|
|
19
|
+
const messageFileCache = new Map();
|
|
19
20
|
function getContextModules() {
|
|
20
21
|
if (!contextModulesPromise) {
|
|
21
22
|
contextModulesPromise = Promise.all([
|
|
@@ -119,13 +120,27 @@ function getAunGroupNames(aid) {
|
|
|
119
120
|
aunGroupNameRequests.set(aid, request);
|
|
120
121
|
return request;
|
|
121
122
|
}
|
|
122
|
-
async function hydrateAunGroupNames(chats) {
|
|
123
|
+
async function hydrateAunGroupNames(chats, waitForRefresh = false) {
|
|
123
124
|
const aids = [...new Set(chats
|
|
124
125
|
.filter(chat => chat.channelType === 'aun' && chat.chatType === 'group')
|
|
125
126
|
.map(chat => chat.selfAID))];
|
|
126
127
|
if (!aids.length)
|
|
127
128
|
return;
|
|
128
|
-
const directories = new Map(
|
|
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
|
+
}
|
|
129
144
|
for (const chat of chats) {
|
|
130
145
|
if (chat.channelType !== 'aun' || chat.chatType !== 'group')
|
|
131
146
|
continue;
|
|
@@ -186,26 +201,135 @@ function getSessionAdapter(baseagent) {
|
|
|
186
201
|
function safeId(value) {
|
|
187
202
|
return Buffer.from(value, 'utf8').toString('base64url');
|
|
188
203
|
}
|
|
189
|
-
function findMessageFiles(root) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
export function findMessageFiles(root) {
|
|
205
|
+
return scanChatDirs(root)
|
|
206
|
+
.map(chat => path.join(chat.dirPath, 'messages.jsonl'))
|
|
207
|
+
.filter(file => fs.existsSync(file));
|
|
208
|
+
}
|
|
209
|
+
function fileSignature(file) {
|
|
210
|
+
try {
|
|
211
|
+
const stat = fs.statSync(file);
|
|
212
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function chatMetadataSignature(dirPath) {
|
|
219
|
+
const parts = [
|
|
220
|
+
`active:${fileSignature(path.join(dirPath, 'active.json'))}`,
|
|
221
|
+
];
|
|
222
|
+
let rootEntries = [];
|
|
223
|
+
try {
|
|
224
|
+
rootEntries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
225
|
+
}
|
|
226
|
+
catch { }
|
|
227
|
+
for (const entry of rootEntries) {
|
|
228
|
+
if (entry.isFile() && entry.name.startsWith('meta_') && entry.name.endsWith('.jsonl')) {
|
|
229
|
+
parts.push(`${entry.name}:${fileSignature(path.join(dirPath, entry.name))}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const threadsDir = path.join(dirPath, '_threads');
|
|
233
|
+
let threadEntries = [];
|
|
234
|
+
try {
|
|
235
|
+
threadEntries = fs.readdirSync(threadsDir, { withFileTypes: true });
|
|
236
|
+
}
|
|
237
|
+
catch { }
|
|
238
|
+
for (const entry of threadEntries) {
|
|
239
|
+
if (entry.isFile() && (entry.name === 'thread-index.json' || entry.name.endsWith('.jsonl'))) {
|
|
240
|
+
parts.push(`threads/${entry.name}:${fileSignature(path.join(threadsDir, entry.name))}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return parts.sort().join('|');
|
|
244
|
+
}
|
|
245
|
+
function appendMessageRows(file, cached, metrics) {
|
|
246
|
+
let stat;
|
|
247
|
+
try {
|
|
248
|
+
stat = fs.statSync(file);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (stat.size < cached.offset || (stat.size === cached.offset && cached.mtimeMs && stat.mtimeMs !== cached.mtimeMs)) {
|
|
254
|
+
cached.offset = 0;
|
|
255
|
+
cached.messages = [];
|
|
256
|
+
}
|
|
257
|
+
if (stat.size === cached.offset)
|
|
258
|
+
return false;
|
|
259
|
+
const bytes = stat.size - cached.offset;
|
|
260
|
+
const buffer = Buffer.allocUnsafe(bytes);
|
|
261
|
+
let fd = null;
|
|
262
|
+
try {
|
|
263
|
+
fd = fs.openSync(file, 'r');
|
|
264
|
+
fs.readSync(fd, buffer, 0, bytes, cached.offset);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
if (fd != null)
|
|
271
|
+
try {
|
|
272
|
+
fs.closeSync(fd);
|
|
273
|
+
}
|
|
274
|
+
catch { }
|
|
275
|
+
}
|
|
276
|
+
let completeBytes = buffer.length;
|
|
277
|
+
const lastBreak = buffer.lastIndexOf(0x0a);
|
|
278
|
+
if (lastBreak !== buffer.length - 1) {
|
|
279
|
+
const trailing = buffer.subarray(lastBreak + 1).toString('utf8').trim();
|
|
193
280
|
try {
|
|
194
|
-
|
|
281
|
+
JSON.parse(trailing);
|
|
195
282
|
}
|
|
196
283
|
catch {
|
|
197
|
-
|
|
284
|
+
completeBytes = lastBreak + 1;
|
|
198
285
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
286
|
+
}
|
|
287
|
+
if (completeBytes <= 0)
|
|
288
|
+
return false;
|
|
289
|
+
let parsedLines = 0;
|
|
290
|
+
for (const line of buffer.subarray(0, completeBytes).toString('utf8').split(/\r?\n/)) {
|
|
291
|
+
if (!line)
|
|
292
|
+
continue;
|
|
293
|
+
try {
|
|
294
|
+
const message = JSON.parse(line);
|
|
295
|
+
parsedLines++;
|
|
296
|
+
if (!isTransientProtocolMessage(message))
|
|
297
|
+
cached.messages.push(message);
|
|
205
298
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
299
|
+
catch { }
|
|
300
|
+
}
|
|
301
|
+
cached.offset += completeBytes;
|
|
302
|
+
cached.mtimeMs = stat.mtimeMs;
|
|
303
|
+
metrics.changedFiles++;
|
|
304
|
+
metrics.parsedLines += parsedLines;
|
|
305
|
+
metrics.parsedBytes += completeBytes;
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
function cachedMessageFile(file, metrics) {
|
|
309
|
+
let cached = messageFileCache.get(file);
|
|
310
|
+
if (!cached) {
|
|
311
|
+
cached = {
|
|
312
|
+
offset: 0,
|
|
313
|
+
mtimeMs: 0,
|
|
314
|
+
messages: [],
|
|
315
|
+
active: {},
|
|
316
|
+
threadSessions: [],
|
|
317
|
+
mainSessionIds: new Set(),
|
|
318
|
+
metadataSignature: '',
|
|
319
|
+
};
|
|
320
|
+
messageFileCache.set(file, cached);
|
|
321
|
+
}
|
|
322
|
+
appendMessageRows(file, cached, metrics);
|
|
323
|
+
const dirPath = path.dirname(file);
|
|
324
|
+
const metadataSignature = chatMetadataSignature(dirPath);
|
|
325
|
+
if (metadataSignature !== cached.metadataSignature) {
|
|
326
|
+
cached.active = readJsonFile(path.join(dirPath, 'active.json')) ?? {};
|
|
327
|
+
cached.threadSessions = loadThreadSessions(dirPath);
|
|
328
|
+
cached.mainSessionIds = loadMainSessionIds(dirPath, cached.active);
|
|
329
|
+
cached.metadataSignature = metadataSignature;
|
|
330
|
+
metrics.metadataRefreshes++;
|
|
331
|
+
}
|
|
332
|
+
return cached;
|
|
209
333
|
}
|
|
210
334
|
function readLastJsonlObject(file) {
|
|
211
335
|
const rows = readAllJsonlLines(file);
|
|
@@ -348,9 +472,6 @@ export async function getAgentSelfNames() {
|
|
|
348
472
|
agentDisplayNamesCache = { expiresAt: Date.now() + 5000, names };
|
|
349
473
|
return names;
|
|
350
474
|
}
|
|
351
|
-
function decodeDirSegment(seg) {
|
|
352
|
-
return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
353
|
-
}
|
|
354
475
|
function parseChannelKey(value) {
|
|
355
476
|
if (typeof value !== 'string')
|
|
356
477
|
return null;
|
|
@@ -375,10 +496,38 @@ function parseLegacySessionKey(value, channelType) {
|
|
|
375
496
|
return null;
|
|
376
497
|
return { type: parts[0], selfAID: parts[1], name: parts[2] || 'main' };
|
|
377
498
|
}
|
|
378
|
-
function pathIdentity(sessionsDir, dirPath) {
|
|
499
|
+
export function pathIdentity(sessionsDir, dirPath) {
|
|
379
500
|
const relParts = path.relative(sessionsDir, dirPath).split(path.sep).filter(Boolean);
|
|
380
501
|
if (!relParts.length)
|
|
381
502
|
return {};
|
|
503
|
+
// Current layout: agents/<aid>/sessions/<channelSessionKey>/
|
|
504
|
+
if (relParts.length === 3 && relParts[1] === 'sessions') {
|
|
505
|
+
const selfAID = decodeDirSegment(relParts[0]);
|
|
506
|
+
const parsed = tryParseChannelSessionKey(relParts[2]);
|
|
507
|
+
if (parsed) {
|
|
508
|
+
return {
|
|
509
|
+
channelType: parsed.channelType,
|
|
510
|
+
channel: parsed.channelType === 'daemon'
|
|
511
|
+
? 'daemon'
|
|
512
|
+
: `${parsed.channelType}#${selfAID}#${parsed.channelName}`,
|
|
513
|
+
channelId: parsed.channelId,
|
|
514
|
+
selfAID,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Immediately preceding Agent layout, retained for inspecting backups.
|
|
519
|
+
if (relParts.length >= 4 && relParts[1] === 'sessions') {
|
|
520
|
+
const selfAID = decodeDirSegment(relParts[0]);
|
|
521
|
+
const channelKey = decodeDirSegment(relParts[2]);
|
|
522
|
+
const parsed = parseChannelKey(channelKey);
|
|
523
|
+
return {
|
|
524
|
+
channelType: parsed?.type || channelKey,
|
|
525
|
+
channel: channelKey,
|
|
526
|
+
channelId: decodeDirSegment(relParts[3]),
|
|
527
|
+
selfAID: parsed?.selfAID || selfAID,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
// Pre-migration path fallback for viewing a backup directory only.
|
|
382
531
|
const firstKey = parseChannelKey(decodeDirSegment(relParts[0]));
|
|
383
532
|
if (firstKey) {
|
|
384
533
|
return {
|
|
@@ -467,17 +616,22 @@ function chatDisplayId(active, messages) {
|
|
|
467
616
|
groupName: null,
|
|
468
617
|
};
|
|
469
618
|
}
|
|
470
|
-
function loadChats(peerDisplayResolver) {
|
|
619
|
+
function loadChats(peerDisplayResolver, metrics) {
|
|
471
620
|
const paths = resolvePaths();
|
|
472
621
|
const sessionsDir = paths.sessionsDir;
|
|
473
622
|
const configuredAids = listConfiguredAgentAids(paths.root);
|
|
474
623
|
const files = findMessageFiles(sessionsDir);
|
|
624
|
+
const liveFiles = new Set(files);
|
|
625
|
+
for (const file of messageFileCache.keys()) {
|
|
626
|
+
if (!liveFiles.has(file))
|
|
627
|
+
messageFileCache.delete(file);
|
|
628
|
+
}
|
|
629
|
+
metrics.files = files.length;
|
|
475
630
|
const chats = [];
|
|
476
631
|
for (const file of files) {
|
|
477
632
|
const dirPath = path.dirname(file);
|
|
478
|
-
const
|
|
479
|
-
const messages =
|
|
480
|
-
.filter(m => !isTransientProtocolMessage(m));
|
|
633
|
+
const cached = cachedMessageFile(file, metrics);
|
|
634
|
+
const { active, messages } = cached;
|
|
481
635
|
if (!messages.length)
|
|
482
636
|
continue;
|
|
483
637
|
const identity = normalizeChatIdentity(sessionsDir, dirPath, active, messages);
|
|
@@ -489,9 +643,9 @@ function loadChats(peerDisplayResolver) {
|
|
|
489
643
|
const peerName = chatType === 'group'
|
|
490
644
|
? display.peerName
|
|
491
645
|
: resolveContactPeerName(peerDisplayResolver, identity, display.peerId, display.peerName);
|
|
492
|
-
const threadSessions =
|
|
646
|
+
const threadSessions = cached.threadSessions;
|
|
493
647
|
const threadById = new Map(threadSessions.map(item => [item.threadId, item]));
|
|
494
|
-
const mainSessionIds =
|
|
648
|
+
const mainSessionIds = cached.mainSessionIds;
|
|
495
649
|
for (const item of partitionMessages(messages, threadSessions, mainSessionIds)) {
|
|
496
650
|
if (!item.messages.length)
|
|
497
651
|
continue;
|
|
@@ -650,7 +804,7 @@ function selectedChats(chats, scope) {
|
|
|
650
804
|
return [];
|
|
651
805
|
return chats.filter(chat => scopeId(chat) === scope);
|
|
652
806
|
}
|
|
653
|
-
function messagesFor(chats, peer) {
|
|
807
|
+
function messagesFor(chats, peer, before, limit) {
|
|
654
808
|
const selected = peer
|
|
655
809
|
? chats.filter(chat => chatKey(chat) === peer)
|
|
656
810
|
: chats;
|
|
@@ -665,7 +819,13 @@ function messagesFor(chats, peer) {
|
|
|
665
819
|
threadName: chat.threadName,
|
|
666
820
|
})));
|
|
667
821
|
messages.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
668
|
-
|
|
822
|
+
const end = before == null ? messages.length : Math.min(Math.max(before, 0), messages.length);
|
|
823
|
+
const start = Math.max(0, end - limit);
|
|
824
|
+
return {
|
|
825
|
+
messages: messages.slice(start, end),
|
|
826
|
+
nextBefore: start > 0 ? start : null,
|
|
827
|
+
hasMore: start > 0,
|
|
828
|
+
};
|
|
669
829
|
}
|
|
670
830
|
function latestInbound(chat) {
|
|
671
831
|
for (let i = chat.messages.length - 1; i >= 0; i--) {
|
|
@@ -745,8 +905,20 @@ async function buildSessionContext(chat) {
|
|
|
745
905
|
};
|
|
746
906
|
}
|
|
747
907
|
async function buildSnapshot(params) {
|
|
908
|
+
const startedAt = Date.now();
|
|
909
|
+
const metrics = {
|
|
910
|
+
files: 0,
|
|
911
|
+
changedFiles: 0,
|
|
912
|
+
parsedLines: 0,
|
|
913
|
+
parsedBytes: 0,
|
|
914
|
+
metadataRefreshes: 0,
|
|
915
|
+
scanMs: 0,
|
|
916
|
+
totalMs: 0,
|
|
917
|
+
};
|
|
748
918
|
const peerDisplayResolver = await getPeerDisplayResolver();
|
|
749
|
-
const
|
|
919
|
+
const scanStartedAt = Date.now();
|
|
920
|
+
const chats = loadChats(peerDisplayResolver, metrics);
|
|
921
|
+
metrics.scanMs = Date.now() - scanStartedAt;
|
|
750
922
|
const scopes = buildScopes(chats, await getAgentSelfNames());
|
|
751
923
|
const requestedScope = params.scope || params.aid || null;
|
|
752
924
|
const scope = requestedScope && !scopes.some(s => s.id === requestedScope)
|
|
@@ -759,18 +931,30 @@ async function buildSnapshot(params) {
|
|
|
759
931
|
if (scope)
|
|
760
932
|
await hydrateAunGroupNames(inScope);
|
|
761
933
|
const peers = scope ? mergePeerInfo(inScope) : [];
|
|
762
|
-
const
|
|
934
|
+
const requestedLimit = Number(params.limit);
|
|
935
|
+
const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(Math.trunc(requestedLimit), 20), 200) : 100;
|
|
936
|
+
const requestedBefore = params.before == null ? Number.NaN : Number(params.before);
|
|
937
|
+
const before = Number.isFinite(requestedBefore) && requestedBefore >= 0 ? Math.trunc(requestedBefore) : null;
|
|
938
|
+
const page = scope && peer ? messagesFor(inScope, peer, before, limit) : { messages: [], nextBefore: null, hasMore: false };
|
|
763
939
|
const currentChat = peer ? inScope.find(chat => chatKey(chat) === peer) : undefined;
|
|
764
940
|
const sessionContext = await buildSessionContext(currentChat);
|
|
941
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
765
942
|
return {
|
|
766
943
|
scopes,
|
|
767
944
|
aids: scopes,
|
|
768
945
|
peers,
|
|
769
|
-
messages,
|
|
946
|
+
messages: page.messages,
|
|
947
|
+
messagePage: {
|
|
948
|
+
limit,
|
|
949
|
+
before,
|
|
950
|
+
nextBefore: page.nextBefore,
|
|
951
|
+
hasMore: page.hasMore,
|
|
952
|
+
},
|
|
770
953
|
scope,
|
|
771
954
|
aid: scope,
|
|
772
955
|
peer,
|
|
773
956
|
sessionContext,
|
|
957
|
+
metrics,
|
|
774
958
|
};
|
|
775
959
|
}
|
|
776
960
|
export const msgSource = {
|