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
package/dist/sources/msg.js
CHANGED
|
@@ -1,38 +1,372 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 消息数据源:扫描 data/sessions 下所有渠道的 messages.jsonl。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - 有 aid 无 peer: 该 AID 的对端列表 + 全部消息
|
|
7
|
-
* - 有 aid 有 peer: 该对端的消息
|
|
8
|
-
* subscribe: fs.watch(sessions/aun, recursive),messages.jsonl 变化时防抖 150ms 后重推。
|
|
4
|
+
* 左列按 agent 聚合;中列列出该 agent 下所有渠道的私聊/群聊会话。
|
|
5
|
+
* 兼容旧参数名 aid/peer:前端的 aid 实际上传 scope id。
|
|
9
6
|
*/
|
|
10
7
|
import fs from 'fs';
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { resolvePaths } from '../paths.js';
|
|
10
|
+
import { readAllJsonlLines, readJsonFile, } from '../fs-utils.js';
|
|
11
|
+
function safeId(value) {
|
|
12
|
+
return Buffer.from(value, 'utf8').toString('base64url');
|
|
13
|
+
}
|
|
14
|
+
function findMessageFiles(root) {
|
|
15
|
+
const out = [];
|
|
16
|
+
const visit = (dir) => {
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const full = path.join(dir, entry.name);
|
|
26
|
+
if (entry.isDirectory())
|
|
27
|
+
visit(full);
|
|
28
|
+
else if (entry.isFile() && entry.name === 'messages.jsonl')
|
|
29
|
+
out.push(full);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
visit(root);
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
function listConfiguredAgentAids(root) {
|
|
36
|
+
const agentsDir = path.join(root, 'agents');
|
|
37
|
+
const aids = new Set();
|
|
38
|
+
let entries;
|
|
39
|
+
try {
|
|
40
|
+
entries = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
16
41
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
catch {
|
|
43
|
+
return aids;
|
|
44
|
+
}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!entry.isDirectory())
|
|
47
|
+
continue;
|
|
48
|
+
const config = readJsonFile(path.join(agentsDir, entry.name, 'config.json'));
|
|
49
|
+
const aid = typeof config?.aid === 'string' ? config.aid : entry.name;
|
|
50
|
+
if (looksLikeAID(aid))
|
|
51
|
+
aids.add(aid);
|
|
52
|
+
}
|
|
53
|
+
return aids;
|
|
54
|
+
}
|
|
55
|
+
function decodeDirSegment(seg) {
|
|
56
|
+
return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
57
|
+
}
|
|
58
|
+
function parseChannelKey(value) {
|
|
59
|
+
if (typeof value !== 'string')
|
|
60
|
+
return null;
|
|
61
|
+
const parts = value.split('#');
|
|
62
|
+
if (parts.length !== 3)
|
|
63
|
+
return null;
|
|
64
|
+
const [type, selfAID, name] = parts;
|
|
65
|
+
if (!type || !selfAID || !name)
|
|
66
|
+
return null;
|
|
67
|
+
return { type, selfAID, name };
|
|
68
|
+
}
|
|
69
|
+
function looksLikeAID(value) {
|
|
70
|
+
return !!value && value !== 'self' && value !== '_unknown' && /^[^./#\s]+\.[^/#\s]+/.test(value);
|
|
71
|
+
}
|
|
72
|
+
function parseLegacySessionKey(value, channelType) {
|
|
73
|
+
if (typeof value !== 'string')
|
|
74
|
+
return null;
|
|
75
|
+
const parts = value.split('#');
|
|
76
|
+
if (parts.length < 3 || parts[0] !== channelType)
|
|
77
|
+
return null;
|
|
78
|
+
if (!looksLikeAID(parts[1]))
|
|
79
|
+
return null;
|
|
80
|
+
return { type: parts[0], selfAID: parts[1], name: parts[2] || 'main' };
|
|
81
|
+
}
|
|
82
|
+
function pathIdentity(sessionsDir, dirPath) {
|
|
83
|
+
const relParts = path.relative(sessionsDir, dirPath).split(path.sep).filter(Boolean);
|
|
84
|
+
if (!relParts.length)
|
|
85
|
+
return {};
|
|
86
|
+
const firstKey = parseChannelKey(decodeDirSegment(relParts[0]));
|
|
87
|
+
if (firstKey) {
|
|
88
|
+
return {
|
|
89
|
+
channelType: firstKey.type,
|
|
90
|
+
channel: `${firstKey.type}#${firstKey.selfAID}#${firstKey.name}`,
|
|
91
|
+
channelId: relParts[1] ? decodeDirSegment(relParts[1]) : undefined,
|
|
92
|
+
selfAID: firstKey.selfAID,
|
|
93
|
+
};
|
|
24
94
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
95
|
+
if (relParts[0] === 'aun' && relParts.length >= 3) {
|
|
96
|
+
const selfAID = decodeDirSegment(relParts[1]);
|
|
97
|
+
return {
|
|
98
|
+
channelType: 'aun',
|
|
99
|
+
channel: looksLikeAID(selfAID) ? `aun#${selfAID}#main` : undefined,
|
|
100
|
+
channelId: decodeDirSegment(relParts[2]),
|
|
101
|
+
selfAID: looksLikeAID(selfAID) ? selfAID : undefined,
|
|
102
|
+
};
|
|
31
103
|
}
|
|
32
|
-
|
|
33
|
-
|
|
104
|
+
return {
|
|
105
|
+
channelType: decodeDirSegment(relParts[0]),
|
|
106
|
+
channelId: relParts[1] ? decodeDirSegment(relParts[1]) : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function inferSelfAID(active, messages, hints) {
|
|
110
|
+
if (looksLikeAID(active?.selfAID))
|
|
111
|
+
return String(active.selfAID);
|
|
112
|
+
if (looksLikeAID(hints.selfAID))
|
|
113
|
+
return hints.selfAID;
|
|
114
|
+
const channelKey = parseChannelKey(active?.channel) || parseChannelKey(active?.channelType);
|
|
115
|
+
if (channelKey)
|
|
116
|
+
return channelKey.selfAID;
|
|
117
|
+
const sessionKey = parseLegacySessionKey(active?.sessionKey, hints.channelType || active?.channelType || '');
|
|
118
|
+
if (sessionKey)
|
|
119
|
+
return sessionKey.selfAID;
|
|
120
|
+
const out = messages.find(m => m.dir === 'out' && m.from && m.from !== 'self');
|
|
121
|
+
if (out?.from)
|
|
122
|
+
return String(out.from);
|
|
123
|
+
const inboundTo = messages.find(m => m.dir === 'in' && m.to && m.to !== 'self');
|
|
124
|
+
if (inboundTo?.to)
|
|
125
|
+
return String(inboundTo.to);
|
|
126
|
+
return 'unknown';
|
|
127
|
+
}
|
|
128
|
+
function normalizeChatIdentity(sessionsDir, dirPath, active, messages) {
|
|
129
|
+
const pathHints = pathIdentity(sessionsDir, dirPath);
|
|
130
|
+
const activeChannelKey = parseChannelKey(active?.channel);
|
|
131
|
+
const activeTypeKey = parseChannelKey(active?.channelType);
|
|
132
|
+
const msgChannelType = messages.find(m => m.channelType)?.channelType;
|
|
133
|
+
const rawType = String(activeChannelKey?.type
|
|
134
|
+
|| activeTypeKey?.type
|
|
135
|
+
|| active?.channelType
|
|
136
|
+
|| msgChannelType
|
|
137
|
+
|| pathHints.channelType
|
|
138
|
+
|| 'unknown');
|
|
139
|
+
const channelType = parseChannelKey(rawType)?.type || rawType;
|
|
140
|
+
const selfAID = inferSelfAID(active, messages, { ...pathHints, channelType });
|
|
141
|
+
const rawChannel = typeof active?.channel === 'string' && active.channel
|
|
142
|
+
? active.channel
|
|
143
|
+
: pathHints.channel || `${channelType}#${selfAID}#main`;
|
|
144
|
+
const channelKey = parseChannelKey(rawChannel);
|
|
145
|
+
const channel = channelKey
|
|
146
|
+
? `${channelKey.type}#${channelKey.selfAID}#${channelKey.name}`
|
|
147
|
+
: (selfAID !== 'unknown' ? `${channelType}#${selfAID}#main` : rawChannel);
|
|
148
|
+
const channelId = String(active.channelId || pathHints.channelId || path.basename(dirPath));
|
|
149
|
+
return { channelType, channel, channelId, selfAID };
|
|
150
|
+
}
|
|
151
|
+
function chatDisplayId(active, messages) {
|
|
152
|
+
const metadata = active?.metadata ?? {};
|
|
153
|
+
const chatType = active?.chatType || messages.find(m => m.chatType)?.chatType || 'private';
|
|
154
|
+
const groupId = chatType === 'group'
|
|
155
|
+
? String(metadata.groupId || active?.channelId || messages.find(m => m.groupId)?.groupId || '')
|
|
156
|
+
: null;
|
|
157
|
+
const groupName = chatType === 'group' && metadata.groupName ? String(metadata.groupName) : null;
|
|
158
|
+
if (chatType === 'group') {
|
|
159
|
+
return {
|
|
160
|
+
peerId: groupId || String(active?.channelId || 'group'),
|
|
161
|
+
peerName: groupName,
|
|
162
|
+
groupId,
|
|
163
|
+
groupName,
|
|
164
|
+
};
|
|
34
165
|
}
|
|
35
|
-
|
|
166
|
+
const peerId = String(metadata.peerId || active?.channelId || messages.find(m => m.dir === 'in')?.from || messages.find(m => m.dir === 'out')?.to || 'unknown');
|
|
167
|
+
return {
|
|
168
|
+
peerId,
|
|
169
|
+
peerName: metadata.peerName ? String(metadata.peerName) : null,
|
|
170
|
+
groupId: null,
|
|
171
|
+
groupName: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function loadChats() {
|
|
175
|
+
const paths = resolvePaths();
|
|
176
|
+
const sessionsDir = paths.sessionsDir;
|
|
177
|
+
const configuredAids = listConfiguredAgentAids(paths.root);
|
|
178
|
+
const files = findMessageFiles(sessionsDir);
|
|
179
|
+
const chats = [];
|
|
180
|
+
for (const file of files) {
|
|
181
|
+
const dirPath = path.dirname(file);
|
|
182
|
+
const active = readJsonFile(path.join(dirPath, 'active.json')) ?? {};
|
|
183
|
+
const messages = readAllJsonlLines(file);
|
|
184
|
+
if (!messages.length)
|
|
185
|
+
continue;
|
|
186
|
+
const identity = normalizeChatIdentity(sessionsDir, dirPath, active, messages);
|
|
187
|
+
const { channelType, channel, channelId, selfAID } = identity;
|
|
188
|
+
if (configuredAids.size && !configuredAids.has(selfAID))
|
|
189
|
+
continue;
|
|
190
|
+
const chatType = String(active.chatType || messages.find(m => m.chatType)?.chatType || 'private');
|
|
191
|
+
const display = chatDisplayId({ ...active, chatType, channelId }, messages);
|
|
192
|
+
const lastMsgAt = messages.reduce((max, m) => Math.max(max, m.ts || 0), 0);
|
|
193
|
+
const updatedAt = Math.max(Number(active.updatedAt || 0), lastMsgAt);
|
|
194
|
+
chats.push({
|
|
195
|
+
id: safeId(`${selfAID}\n${channelType}\n${channelId}\n${chatType}`),
|
|
196
|
+
dirPath,
|
|
197
|
+
channelType,
|
|
198
|
+
channel,
|
|
199
|
+
channelId,
|
|
200
|
+
selfAID,
|
|
201
|
+
chatType,
|
|
202
|
+
peerId: display.peerId,
|
|
203
|
+
peerName: display.peerName,
|
|
204
|
+
groupId: display.groupId,
|
|
205
|
+
groupName: display.groupName,
|
|
206
|
+
updatedAt,
|
|
207
|
+
messages,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return chats;
|
|
211
|
+
}
|
|
212
|
+
function scopeId(chat) {
|
|
213
|
+
return safeId(chat.selfAID);
|
|
214
|
+
}
|
|
215
|
+
function legacyChannelScopeId(chat) {
|
|
216
|
+
return safeId(`${chat.selfAID}\n${chat.channelType}\n${chat.channel}`);
|
|
217
|
+
}
|
|
218
|
+
function buildScopes(chats) {
|
|
219
|
+
const map = new Map();
|
|
220
|
+
const seenChats = new Map();
|
|
221
|
+
const seenChannels = new Map();
|
|
222
|
+
const seenChannelTypes = new Map();
|
|
223
|
+
for (const chat of chats) {
|
|
224
|
+
const id = scopeId(chat);
|
|
225
|
+
let scope = map.get(id);
|
|
226
|
+
if (!scope) {
|
|
227
|
+
scope = {
|
|
228
|
+
id,
|
|
229
|
+
aid: id,
|
|
230
|
+
selfAID: chat.selfAID,
|
|
231
|
+
totalIn: 0,
|
|
232
|
+
totalOut: 0,
|
|
233
|
+
peerCount: 0,
|
|
234
|
+
groupCount: 0,
|
|
235
|
+
channelCount: 0,
|
|
236
|
+
channelTypes: [],
|
|
237
|
+
channels: [],
|
|
238
|
+
lastAt: 0,
|
|
239
|
+
};
|
|
240
|
+
map.set(id, scope);
|
|
241
|
+
seenChats.set(id, new Set());
|
|
242
|
+
seenChannels.set(id, new Set());
|
|
243
|
+
seenChannelTypes.set(id, new Set());
|
|
244
|
+
}
|
|
245
|
+
const channelSeen = seenChannels.get(id);
|
|
246
|
+
if (!channelSeen.has(chat.channel)) {
|
|
247
|
+
channelSeen.add(chat.channel);
|
|
248
|
+
scope.channels.push(chat.channel);
|
|
249
|
+
scope.channelCount = channelSeen.size;
|
|
250
|
+
}
|
|
251
|
+
const typeSeen = seenChannelTypes.get(id);
|
|
252
|
+
if (!typeSeen.has(chat.channelType)) {
|
|
253
|
+
typeSeen.add(chat.channelType);
|
|
254
|
+
scope.channelTypes.push(chat.channelType);
|
|
255
|
+
scope.channelTypes.sort();
|
|
256
|
+
}
|
|
257
|
+
let hasIn = false;
|
|
258
|
+
let hasOut = false;
|
|
259
|
+
for (const msg of chat.messages) {
|
|
260
|
+
if (msg.dir === 'in') {
|
|
261
|
+
scope.totalIn++;
|
|
262
|
+
hasIn = true;
|
|
263
|
+
}
|
|
264
|
+
else if (msg.dir === 'out') {
|
|
265
|
+
scope.totalOut++;
|
|
266
|
+
hasOut = true;
|
|
267
|
+
}
|
|
268
|
+
if (msg.ts > scope.lastAt)
|
|
269
|
+
scope.lastAt = msg.ts;
|
|
270
|
+
}
|
|
271
|
+
if (hasIn || hasOut) {
|
|
272
|
+
const chatKey = `${chat.channel}\n${chat.channelId}\n${chat.chatType}`;
|
|
273
|
+
const seen = seenChats.get(id);
|
|
274
|
+
if (!seen.has(chatKey)) {
|
|
275
|
+
seen.add(chatKey);
|
|
276
|
+
scope.peerCount++;
|
|
277
|
+
if (chat.chatType === 'group')
|
|
278
|
+
scope.groupCount++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (chat.updatedAt > scope.lastAt)
|
|
282
|
+
scope.lastAt = chat.updatedAt;
|
|
283
|
+
}
|
|
284
|
+
return Array.from(map.values())
|
|
285
|
+
.sort((a, b) => b.lastAt - a.lastAt);
|
|
286
|
+
}
|
|
287
|
+
function channelName(channel) {
|
|
288
|
+
return parseChannelKey(channel)?.name ?? null;
|
|
289
|
+
}
|
|
290
|
+
function chatKey(chat) {
|
|
291
|
+
return safeId(`${chat.channelType}\n${chat.channel}\n${chat.channelId}\n${chat.chatType}`);
|
|
292
|
+
}
|
|
293
|
+
function mergePeerInfo(chats) {
|
|
294
|
+
const map = new Map();
|
|
295
|
+
for (const chat of chats) {
|
|
296
|
+
const key = chatKey(chat);
|
|
297
|
+
let peer = map.get(key);
|
|
298
|
+
if (!peer) {
|
|
299
|
+
peer = {
|
|
300
|
+
id: key,
|
|
301
|
+
peerId: key,
|
|
302
|
+
peerName: chat.chatType === 'group' ? (chat.groupName || chat.groupId || chat.channelId) : (chat.peerName || chat.peerId),
|
|
303
|
+
channel: chat.channel,
|
|
304
|
+
channelType: chat.channelType,
|
|
305
|
+
channelName: channelName(chat.channel),
|
|
306
|
+
chatType: chat.chatType,
|
|
307
|
+
groupId: chat.groupId,
|
|
308
|
+
groupName: chat.groupName,
|
|
309
|
+
inbound: 0,
|
|
310
|
+
outbound: 0,
|
|
311
|
+
lastAt: 0,
|
|
312
|
+
};
|
|
313
|
+
map.set(key, peer);
|
|
314
|
+
}
|
|
315
|
+
for (const msg of chat.messages) {
|
|
316
|
+
if (msg.dir === 'in')
|
|
317
|
+
peer.inbound++;
|
|
318
|
+
else if (msg.dir === 'out')
|
|
319
|
+
peer.outbound++;
|
|
320
|
+
if (msg.ts > peer.lastAt)
|
|
321
|
+
peer.lastAt = msg.ts;
|
|
322
|
+
}
|
|
323
|
+
if (chat.updatedAt > peer.lastAt)
|
|
324
|
+
peer.lastAt = chat.updatedAt;
|
|
325
|
+
}
|
|
326
|
+
return Array.from(map.values()).sort((a, b) => b.lastAt - a.lastAt);
|
|
327
|
+
}
|
|
328
|
+
function selectedChats(chats, scope) {
|
|
329
|
+
if (!scope)
|
|
330
|
+
return [];
|
|
331
|
+
return chats.filter(chat => scopeId(chat) === scope);
|
|
332
|
+
}
|
|
333
|
+
function messagesFor(chats, peer) {
|
|
334
|
+
const selected = peer
|
|
335
|
+
? chats.filter(chat => chatKey(chat) === peer)
|
|
336
|
+
: chats;
|
|
337
|
+
const messages = selected.flatMap(chat => chat.messages.map(msg => ({
|
|
338
|
+
...msg,
|
|
339
|
+
channel: chat.channel,
|
|
340
|
+
channelType: chat.channelType,
|
|
341
|
+
selfAID: chat.selfAID,
|
|
342
|
+
peerName: chat.peerName,
|
|
343
|
+
groupName: chat.groupName,
|
|
344
|
+
})));
|
|
345
|
+
messages.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
346
|
+
return messages.length > 1000 ? messages.slice(-1000) : messages;
|
|
347
|
+
}
|
|
348
|
+
function buildSnapshot(params) {
|
|
349
|
+
const chats = loadChats();
|
|
350
|
+
const scopes = buildScopes(chats);
|
|
351
|
+
const requestedScope = params.scope || params.aid || null;
|
|
352
|
+
const scope = requestedScope && !scopes.some(s => s.id === requestedScope)
|
|
353
|
+
? (chats.find(chat => legacyChannelScopeId(chat) === requestedScope)
|
|
354
|
+
? scopeId(chats.find(chat => legacyChannelScopeId(chat) === requestedScope))
|
|
355
|
+
: requestedScope)
|
|
356
|
+
: requestedScope;
|
|
357
|
+
const peer = params.peer || null;
|
|
358
|
+
const inScope = selectedChats(chats, scope);
|
|
359
|
+
const peers = scope ? mergePeerInfo(inScope) : [];
|
|
360
|
+
const messages = scope ? messagesFor(inScope, peer) : [];
|
|
361
|
+
return {
|
|
362
|
+
scopes,
|
|
363
|
+
aids: scopes,
|
|
364
|
+
peers,
|
|
365
|
+
messages,
|
|
366
|
+
scope,
|
|
367
|
+
aid: scope,
|
|
368
|
+
peer,
|
|
369
|
+
};
|
|
36
370
|
}
|
|
37
371
|
export const msgSource = {
|
|
38
372
|
kind: 'msg',
|
|
@@ -40,7 +374,7 @@ export const msgSource = {
|
|
|
40
374
|
return buildSnapshot(params);
|
|
41
375
|
},
|
|
42
376
|
subscribe(params, push) {
|
|
43
|
-
const
|
|
377
|
+
const sessionsDir = resolvePaths().sessionsDir;
|
|
44
378
|
let watcher = null;
|
|
45
379
|
let debounce = null;
|
|
46
380
|
const fire = () => {
|
|
@@ -54,12 +388,13 @@ export const msgSource = {
|
|
|
54
388
|
}, 150);
|
|
55
389
|
};
|
|
56
390
|
try {
|
|
57
|
-
watcher = fs.watch(
|
|
58
|
-
|
|
391
|
+
watcher = fs.watch(sessionsDir, { recursive: true }, (_evt, filename) => {
|
|
392
|
+
const name = String(filename || '');
|
|
393
|
+
if (name.endsWith('messages.jsonl') || name.endsWith('active.json'))
|
|
59
394
|
fire();
|
|
60
395
|
});
|
|
61
396
|
}
|
|
62
|
-
catch { /*
|
|
397
|
+
catch { /* sessionsDir may not exist yet */ }
|
|
63
398
|
return () => {
|
|
64
399
|
if (watcher)
|
|
65
400
|
watcher.close();
|