ec-web 0.0.1 → 0.0.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/README.md +15 -3
- package/dist/debug-log.js +7 -0
- package/dist/fs-utils.js +189 -0
- package/dist/index.js +137 -0
- package/dist/ipc-client.js +40 -0
- package/dist/paths.js +55 -0
- package/dist/process-utils.js +241 -0
- package/dist/server.js +820 -0
- package/dist/sources/aid.js +139 -0
- package/dist/sources/baseagent-detector.js +72 -0
- package/dist/sources/cache.js +43 -0
- package/dist/sources/gateway.js +44 -0
- package/dist/sources/logs.js +396 -0
- package/dist/sources/models.js +75 -0
- package/dist/sources/monitor.js +179 -0
- package/dist/sources/msg.js +625 -0
- package/dist/sources/parent-package.js +77 -0
- package/dist/sources/relation-roles.js +903 -0
- package/dist/sources/role-definitions.js +1195 -0
- package/dist/sources/session-codex.js +731 -0
- package/dist/sources/session.js +489 -0
- package/dist/sources/stats.js +481 -0
- package/dist/sources/system.js +83 -0
- package/dist/sources/triggers.js +138 -0
- package/dist/sources/types.js +10 -0
- package/dist/static/app.js +10753 -0
- package/dist/static/index.html +386 -0
- package/dist/static/style.css +4138 -0
- package/dist/types/cli-permissions.js +90 -0
- package/package.json +20 -11
- package/index.js +0 -7
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息数据源:扫描 data/sessions 下所有渠道的 messages.jsonl。
|
|
3
|
+
*
|
|
4
|
+
* 左列按 agent 聚合;中列列出该 agent 下所有渠道的私聊/群聊会话。
|
|
5
|
+
* 兼容旧参数名 aid/peer:前端的 aid 实际上传 scope id。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { resolvePaths } from '../paths.js';
|
|
10
|
+
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
11
|
+
import { isTransientProtocolMessage, readAllJsonlLines, readJsonFile, } from '../fs-utils.js';
|
|
12
|
+
let contextModulesPromise = null;
|
|
13
|
+
const sessionAdapterPromises = new Map();
|
|
14
|
+
function getContextModules() {
|
|
15
|
+
if (!contextModulesPromise) {
|
|
16
|
+
contextModulesPromise = Promise.all([
|
|
17
|
+
import(toFileUrl(resolveParentDistModule('config', 'peer-role-resolver.js'))),
|
|
18
|
+
import(toFileUrl(resolveParentDistModule('core', 'model', 'config-scope.js'))),
|
|
19
|
+
import(toFileUrl(resolveParentDistModule('core', 'message', 'peer-mode.js'))),
|
|
20
|
+
]).then(([resolver, modelScope, peerMode]) => ({ resolver, modelScope, peerMode }));
|
|
21
|
+
}
|
|
22
|
+
return contextModulesPromise;
|
|
23
|
+
}
|
|
24
|
+
function getSessionAdapter(baseagent) {
|
|
25
|
+
if (!sessionAdapterPromises.has(baseagent)) {
|
|
26
|
+
const classNames = {
|
|
27
|
+
claude: 'ClaudeSessionFileAdapter',
|
|
28
|
+
codex: 'CodexSessionFileAdapter',
|
|
29
|
+
gemini: 'GeminiSessionFileAdapter',
|
|
30
|
+
};
|
|
31
|
+
const className = classNames[baseagent];
|
|
32
|
+
const promise = !className
|
|
33
|
+
? Promise.resolve(null)
|
|
34
|
+
: Promise.resolve()
|
|
35
|
+
.then(() => import(toFileUrl(resolveParentDistModule('core', 'session', 'adapters', `${baseagent}-session-file-adapter.js`))))
|
|
36
|
+
.then(mod => mod[className] ? new mod[className]() : null)
|
|
37
|
+
.catch(() => null);
|
|
38
|
+
sessionAdapterPromises.set(baseagent, promise);
|
|
39
|
+
}
|
|
40
|
+
return sessionAdapterPromises.get(baseagent);
|
|
41
|
+
}
|
|
42
|
+
function safeId(value) {
|
|
43
|
+
return Buffer.from(value, 'utf8').toString('base64url');
|
|
44
|
+
}
|
|
45
|
+
function findMessageFiles(root) {
|
|
46
|
+
const out = [];
|
|
47
|
+
const visit = (dir) => {
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const full = path.join(dir, entry.name);
|
|
57
|
+
if (entry.isDirectory())
|
|
58
|
+
visit(full);
|
|
59
|
+
else if (entry.isFile() && entry.name === 'messages.jsonl')
|
|
60
|
+
out.push(full);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
visit(root);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function readLastJsonlObject(file) {
|
|
67
|
+
const rows = readAllJsonlLines(file);
|
|
68
|
+
return rows.length ? rows[rows.length - 1] : null;
|
|
69
|
+
}
|
|
70
|
+
function loadThreadSessions(dirPath) {
|
|
71
|
+
const threadsDir = path.join(dirPath, '_threads');
|
|
72
|
+
const index = readJsonFile(path.join(threadsDir, 'thread-index.json')) || {};
|
|
73
|
+
return Object.entries(index).map(([threadId, value]) => {
|
|
74
|
+
const sessionId = typeof value === 'string' ? value : value?.sessionId || null;
|
|
75
|
+
const metaFile = typeof value === 'object' && value?.metaFile
|
|
76
|
+
? value.metaFile
|
|
77
|
+
: sessionId ? `${sessionId}.jsonl` : '';
|
|
78
|
+
const metaRows = metaFile ? readAllJsonlLines(path.join(threadsDir, metaFile)) : [];
|
|
79
|
+
const active = metaRows.length ? metaRows[metaRows.length - 1] : {};
|
|
80
|
+
const replyToMessageIds = Array.from(new Set(metaRows.flatMap(row => {
|
|
81
|
+
const replyContext = row?.metadata?.replyContext;
|
|
82
|
+
const ids = [
|
|
83
|
+
replyContext?.replyToMessageId,
|
|
84
|
+
replyContext?.metadata?.refMessageId,
|
|
85
|
+
];
|
|
86
|
+
return ids.filter((id) => typeof id === 'string' && id.length > 0);
|
|
87
|
+
})));
|
|
88
|
+
return { threadId, sessionId, active, replyToMessageIds };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function loadMainSessionIds(dirPath, active) {
|
|
92
|
+
const ids = new Set();
|
|
93
|
+
if (typeof active.id === 'string' && active.id)
|
|
94
|
+
ids.add(active.id);
|
|
95
|
+
let entries;
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return ids;
|
|
101
|
+
}
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!entry.isFile() || !entry.name.startsWith('meta_') || !entry.name.endsWith('.jsonl'))
|
|
104
|
+
continue;
|
|
105
|
+
const latest = readLastJsonlObject(path.join(dirPath, entry.name));
|
|
106
|
+
if (typeof latest?.id === 'string' && !latest.threadId)
|
|
107
|
+
ids.add(latest.id);
|
|
108
|
+
}
|
|
109
|
+
return ids;
|
|
110
|
+
}
|
|
111
|
+
export function partitionMessages(messages, threadSessions, mainSessionIds) {
|
|
112
|
+
const threadBySessionId = new Map(threadSessions
|
|
113
|
+
.filter(item => item.sessionId)
|
|
114
|
+
.map(item => [item.sessionId, item]));
|
|
115
|
+
// A topic session records the inbound message it is replying to in its
|
|
116
|
+
// metadata. Use only unique references; collisions stay unclassified.
|
|
117
|
+
const threadByReplyMessageId = new Map();
|
|
118
|
+
for (const thread of threadSessions) {
|
|
119
|
+
for (const messageId of thread.replyToMessageIds || []) {
|
|
120
|
+
if (threadByReplyMessageId.has(messageId))
|
|
121
|
+
threadByReplyMessageId.set(messageId, null);
|
|
122
|
+
else
|
|
123
|
+
threadByReplyMessageId.set(messageId, thread);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const partitions = new Map();
|
|
127
|
+
const ensurePartition = (key, partition, threadId) => {
|
|
128
|
+
let item = partitions.get(key);
|
|
129
|
+
if (!item) {
|
|
130
|
+
item = { partition, threadId, messages: [] };
|
|
131
|
+
partitions.set(key, item);
|
|
132
|
+
}
|
|
133
|
+
return item;
|
|
134
|
+
};
|
|
135
|
+
for (const message of messages) {
|
|
136
|
+
const hasThreadField = Object.prototype.hasOwnProperty.call(message, 'threadId');
|
|
137
|
+
const explicitThreadId = typeof message.threadId === 'string' && message.threadId ? message.threadId : null;
|
|
138
|
+
const inferredThread = !hasThreadField && message.sessionId ? threadBySessionId.get(message.sessionId) : undefined;
|
|
139
|
+
const inferredReplyThread = message.msgId ? threadByReplyMessageId.get(message.msgId) : undefined;
|
|
140
|
+
if (explicitThreadId || inferredThread || inferredReplyThread) {
|
|
141
|
+
const threadId = explicitThreadId || inferredThread?.threadId || inferredReplyThread.threadId;
|
|
142
|
+
ensurePartition(`thread:${threadId}`, 'thread', threadId).messages.push({ ...message, threadId });
|
|
143
|
+
}
|
|
144
|
+
else if (hasThreadField || threadSessions.length === 0 || (message.sessionId && mainSessionIds.has(message.sessionId))) {
|
|
145
|
+
ensurePartition('main', 'main', null).messages.push({ ...message, threadId: null });
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
ensurePartition('legacy', 'legacy', null).messages.push(message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return Array.from(partitions.values());
|
|
152
|
+
}
|
|
153
|
+
function listConfiguredAgentAids(root) {
|
|
154
|
+
const agentsDir = path.join(root, 'agents');
|
|
155
|
+
const aids = new Set();
|
|
156
|
+
let entries;
|
|
157
|
+
try {
|
|
158
|
+
entries = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return aids;
|
|
162
|
+
}
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
if (!entry.isDirectory())
|
|
165
|
+
continue;
|
|
166
|
+
const config = readJsonFile(path.join(agentsDir, entry.name, 'config.json'));
|
|
167
|
+
const aid = typeof config?.aid === 'string' ? config.aid : entry.name;
|
|
168
|
+
if (looksLikeAID(aid))
|
|
169
|
+
aids.add(aid);
|
|
170
|
+
}
|
|
171
|
+
return aids;
|
|
172
|
+
}
|
|
173
|
+
function decodeDirSegment(seg) {
|
|
174
|
+
return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
175
|
+
}
|
|
176
|
+
function parseChannelKey(value) {
|
|
177
|
+
if (typeof value !== 'string')
|
|
178
|
+
return null;
|
|
179
|
+
const parts = value.split('#');
|
|
180
|
+
if (parts.length !== 3)
|
|
181
|
+
return null;
|
|
182
|
+
const [type, selfAID, name] = parts;
|
|
183
|
+
if (!type || !selfAID || !name)
|
|
184
|
+
return null;
|
|
185
|
+
return { type, selfAID, name };
|
|
186
|
+
}
|
|
187
|
+
function looksLikeAID(value) {
|
|
188
|
+
return !!value && value !== 'self' && value !== '_unknown' && /^[^./#\s]+\.[^/#\s]+/.test(value);
|
|
189
|
+
}
|
|
190
|
+
function parseLegacySessionKey(value, channelType) {
|
|
191
|
+
if (typeof value !== 'string')
|
|
192
|
+
return null;
|
|
193
|
+
const parts = value.split('#');
|
|
194
|
+
if (parts.length < 3 || parts[0] !== channelType)
|
|
195
|
+
return null;
|
|
196
|
+
if (!looksLikeAID(parts[1]))
|
|
197
|
+
return null;
|
|
198
|
+
return { type: parts[0], selfAID: parts[1], name: parts[2] || 'main' };
|
|
199
|
+
}
|
|
200
|
+
function pathIdentity(sessionsDir, dirPath) {
|
|
201
|
+
const relParts = path.relative(sessionsDir, dirPath).split(path.sep).filter(Boolean);
|
|
202
|
+
if (!relParts.length)
|
|
203
|
+
return {};
|
|
204
|
+
const firstKey = parseChannelKey(decodeDirSegment(relParts[0]));
|
|
205
|
+
if (firstKey) {
|
|
206
|
+
return {
|
|
207
|
+
channelType: firstKey.type,
|
|
208
|
+
channel: `${firstKey.type}#${firstKey.selfAID}#${firstKey.name}`,
|
|
209
|
+
channelId: relParts[1] ? decodeDirSegment(relParts[1]) : undefined,
|
|
210
|
+
selfAID: firstKey.selfAID,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (relParts[0] === 'aun' && relParts.length >= 3) {
|
|
214
|
+
const selfAID = decodeDirSegment(relParts[1]);
|
|
215
|
+
return {
|
|
216
|
+
channelType: 'aun',
|
|
217
|
+
channel: looksLikeAID(selfAID) ? `aun#${selfAID}#main` : undefined,
|
|
218
|
+
channelId: decodeDirSegment(relParts[2]),
|
|
219
|
+
selfAID: looksLikeAID(selfAID) ? selfAID : undefined,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
channelType: decodeDirSegment(relParts[0]),
|
|
224
|
+
channelId: relParts[1] ? decodeDirSegment(relParts[1]) : undefined,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function inferSelfAID(active, messages, hints) {
|
|
228
|
+
if (looksLikeAID(active?.selfAID))
|
|
229
|
+
return String(active.selfAID);
|
|
230
|
+
if (looksLikeAID(hints.selfAID))
|
|
231
|
+
return hints.selfAID;
|
|
232
|
+
const channelKey = parseChannelKey(active?.channel) || parseChannelKey(active?.channelType);
|
|
233
|
+
if (channelKey)
|
|
234
|
+
return channelKey.selfAID;
|
|
235
|
+
const sessionKey = parseLegacySessionKey(active?.sessionKey, hints.channelType || active?.channelType || '');
|
|
236
|
+
if (sessionKey)
|
|
237
|
+
return sessionKey.selfAID;
|
|
238
|
+
const out = messages.find(m => m.dir === 'out' && m.from && m.from !== 'self');
|
|
239
|
+
if (out?.from)
|
|
240
|
+
return String(out.from);
|
|
241
|
+
const inboundTo = messages.find(m => m.dir === 'in' && m.to && m.to !== 'self');
|
|
242
|
+
if (inboundTo?.to)
|
|
243
|
+
return String(inboundTo.to);
|
|
244
|
+
return 'unknown';
|
|
245
|
+
}
|
|
246
|
+
function normalizeChatIdentity(sessionsDir, dirPath, active, messages) {
|
|
247
|
+
const pathHints = pathIdentity(sessionsDir, dirPath);
|
|
248
|
+
const activeChannelKey = parseChannelKey(active?.channel);
|
|
249
|
+
const activeTypeKey = parseChannelKey(active?.channelType);
|
|
250
|
+
const msgChannelType = messages.find(m => m.channelType)?.channelType;
|
|
251
|
+
const rawType = String(activeChannelKey?.type
|
|
252
|
+
|| activeTypeKey?.type
|
|
253
|
+
|| active?.channelType
|
|
254
|
+
|| msgChannelType
|
|
255
|
+
|| pathHints.channelType
|
|
256
|
+
|| 'unknown');
|
|
257
|
+
const channelType = parseChannelKey(rawType)?.type || rawType;
|
|
258
|
+
const selfAID = inferSelfAID(active, messages, { ...pathHints, channelType });
|
|
259
|
+
const rawChannel = typeof active?.channel === 'string' && active.channel
|
|
260
|
+
? active.channel
|
|
261
|
+
: pathHints.channel || `${channelType}#${selfAID}#main`;
|
|
262
|
+
const channelKey = parseChannelKey(rawChannel);
|
|
263
|
+
const channel = channelKey
|
|
264
|
+
? `${channelKey.type}#${channelKey.selfAID}#${channelKey.name}`
|
|
265
|
+
: (selfAID !== 'unknown' ? `${channelType}#${selfAID}#main` : rawChannel);
|
|
266
|
+
const channelId = String(active.channelId || pathHints.channelId || path.basename(dirPath));
|
|
267
|
+
return { channelType, channel, channelId, selfAID };
|
|
268
|
+
}
|
|
269
|
+
function chatDisplayId(active, messages) {
|
|
270
|
+
const metadata = active?.metadata ?? {};
|
|
271
|
+
const chatType = active?.chatType || messages.find(m => m.chatType)?.chatType || 'private';
|
|
272
|
+
const groupId = chatType === 'group'
|
|
273
|
+
? String(metadata.groupId || active?.channelId || messages.find(m => m.groupId)?.groupId || '')
|
|
274
|
+
: null;
|
|
275
|
+
const groupName = chatType === 'group' && metadata.groupName ? String(metadata.groupName) : null;
|
|
276
|
+
if (chatType === 'group') {
|
|
277
|
+
return {
|
|
278
|
+
peerId: groupId || String(active?.channelId || 'group'),
|
|
279
|
+
peerName: groupName,
|
|
280
|
+
groupId,
|
|
281
|
+
groupName,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const peerId = String(metadata.peerId || active?.channelId || messages.find(m => m.dir === 'in')?.from || messages.find(m => m.dir === 'out')?.to || 'unknown');
|
|
285
|
+
return {
|
|
286
|
+
peerId,
|
|
287
|
+
peerName: metadata.peerName ? String(metadata.peerName) : null,
|
|
288
|
+
groupId: null,
|
|
289
|
+
groupName: null,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function loadChats() {
|
|
293
|
+
const paths = resolvePaths();
|
|
294
|
+
const sessionsDir = paths.sessionsDir;
|
|
295
|
+
const configuredAids = listConfiguredAgentAids(paths.root);
|
|
296
|
+
const files = findMessageFiles(sessionsDir);
|
|
297
|
+
const chats = [];
|
|
298
|
+
for (const file of files) {
|
|
299
|
+
const dirPath = path.dirname(file);
|
|
300
|
+
const active = readJsonFile(path.join(dirPath, 'active.json')) ?? {};
|
|
301
|
+
const messages = readAllJsonlLines(file)
|
|
302
|
+
.filter(m => !isTransientProtocolMessage(m));
|
|
303
|
+
if (!messages.length)
|
|
304
|
+
continue;
|
|
305
|
+
const identity = normalizeChatIdentity(sessionsDir, dirPath, active, messages);
|
|
306
|
+
const { channelType, channel, channelId, selfAID } = identity;
|
|
307
|
+
if (configuredAids.size && !configuredAids.has(selfAID))
|
|
308
|
+
continue;
|
|
309
|
+
const chatType = String(active.chatType || messages.find(m => m.chatType)?.chatType || 'private');
|
|
310
|
+
const display = chatDisplayId({ ...active, chatType, channelId }, messages);
|
|
311
|
+
const threadSessions = loadThreadSessions(dirPath);
|
|
312
|
+
const threadById = new Map(threadSessions.map(item => [item.threadId, item]));
|
|
313
|
+
const mainSessionIds = loadMainSessionIds(dirPath, active);
|
|
314
|
+
for (const item of partitionMessages(messages, threadSessions, mainSessionIds)) {
|
|
315
|
+
if (!item.messages.length)
|
|
316
|
+
continue;
|
|
317
|
+
const thread = item.threadId ? threadById.get(item.threadId) : undefined;
|
|
318
|
+
const partitionActive = item.partition === 'thread' ? thread?.active || {} : item.partition === 'main' ? active : {};
|
|
319
|
+
const lastMsgAt = item.messages.reduce((max, message) => Math.max(max, message.ts || 0), 0);
|
|
320
|
+
const updatedAt = Math.max(Number(partitionActive.updatedAt || 0), lastMsgAt);
|
|
321
|
+
chats.push({
|
|
322
|
+
id: safeId(`${selfAID}\n${channelType}\n${channelId}\n${chatType}\n${item.partition}\n${item.threadId || ''}`),
|
|
323
|
+
dirPath,
|
|
324
|
+
channelType,
|
|
325
|
+
channel,
|
|
326
|
+
channelId,
|
|
327
|
+
selfAID,
|
|
328
|
+
chatType,
|
|
329
|
+
peerId: display.peerId,
|
|
330
|
+
peerName: display.peerName,
|
|
331
|
+
groupId: display.groupId,
|
|
332
|
+
groupName: display.groupName,
|
|
333
|
+
partition: item.partition,
|
|
334
|
+
threadId: item.threadId,
|
|
335
|
+
threadName: item.partition === 'thread'
|
|
336
|
+
? String(partitionActive.name || partitionActive.metadata?.topicName || item.threadId)
|
|
337
|
+
: null,
|
|
338
|
+
updatedAt,
|
|
339
|
+
messages: item.messages,
|
|
340
|
+
active: partitionActive,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return chats;
|
|
345
|
+
}
|
|
346
|
+
function scopeId(chat) {
|
|
347
|
+
return safeId(chat.selfAID);
|
|
348
|
+
}
|
|
349
|
+
function legacyChannelScopeId(chat) {
|
|
350
|
+
return safeId(`${chat.selfAID}\n${chat.channelType}\n${chat.channel}`);
|
|
351
|
+
}
|
|
352
|
+
function buildScopes(chats) {
|
|
353
|
+
const map = new Map();
|
|
354
|
+
const seenChats = new Map();
|
|
355
|
+
const seenChannels = new Map();
|
|
356
|
+
const seenChannelTypes = new Map();
|
|
357
|
+
for (const chat of chats) {
|
|
358
|
+
const id = scopeId(chat);
|
|
359
|
+
let scope = map.get(id);
|
|
360
|
+
if (!scope) {
|
|
361
|
+
scope = {
|
|
362
|
+
id,
|
|
363
|
+
aid: id,
|
|
364
|
+
selfAID: chat.selfAID,
|
|
365
|
+
totalIn: 0,
|
|
366
|
+
totalOut: 0,
|
|
367
|
+
peerCount: 0,
|
|
368
|
+
groupCount: 0,
|
|
369
|
+
channelCount: 0,
|
|
370
|
+
channelTypes: [],
|
|
371
|
+
channels: [],
|
|
372
|
+
lastAt: 0,
|
|
373
|
+
};
|
|
374
|
+
map.set(id, scope);
|
|
375
|
+
seenChats.set(id, new Set());
|
|
376
|
+
seenChannels.set(id, new Set());
|
|
377
|
+
seenChannelTypes.set(id, new Set());
|
|
378
|
+
}
|
|
379
|
+
const channelSeen = seenChannels.get(id);
|
|
380
|
+
if (!channelSeen.has(chat.channel)) {
|
|
381
|
+
channelSeen.add(chat.channel);
|
|
382
|
+
scope.channels.push(chat.channel);
|
|
383
|
+
scope.channelCount = channelSeen.size;
|
|
384
|
+
}
|
|
385
|
+
const typeSeen = seenChannelTypes.get(id);
|
|
386
|
+
if (!typeSeen.has(chat.channelType)) {
|
|
387
|
+
typeSeen.add(chat.channelType);
|
|
388
|
+
scope.channelTypes.push(chat.channelType);
|
|
389
|
+
scope.channelTypes.sort();
|
|
390
|
+
}
|
|
391
|
+
let hasIn = false;
|
|
392
|
+
let hasOut = false;
|
|
393
|
+
for (const msg of chat.messages) {
|
|
394
|
+
if (msg.dir === 'in') {
|
|
395
|
+
scope.totalIn++;
|
|
396
|
+
hasIn = true;
|
|
397
|
+
}
|
|
398
|
+
else if (msg.dir === 'out') {
|
|
399
|
+
scope.totalOut++;
|
|
400
|
+
hasOut = true;
|
|
401
|
+
}
|
|
402
|
+
if (msg.ts > scope.lastAt)
|
|
403
|
+
scope.lastAt = msg.ts;
|
|
404
|
+
}
|
|
405
|
+
if (hasIn || hasOut) {
|
|
406
|
+
const chatKey = `${chat.channel}\n${chat.channelId}\n${chat.chatType}\n${chat.partition}\n${chat.threadId || ''}`;
|
|
407
|
+
const seen = seenChats.get(id);
|
|
408
|
+
if (!seen.has(chatKey)) {
|
|
409
|
+
seen.add(chatKey);
|
|
410
|
+
scope.peerCount++;
|
|
411
|
+
if (chat.chatType === 'group')
|
|
412
|
+
scope.groupCount++;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (chat.updatedAt > scope.lastAt)
|
|
416
|
+
scope.lastAt = chat.updatedAt;
|
|
417
|
+
}
|
|
418
|
+
return Array.from(map.values())
|
|
419
|
+
.sort((a, b) => b.lastAt - a.lastAt);
|
|
420
|
+
}
|
|
421
|
+
function channelName(channel) {
|
|
422
|
+
return parseChannelKey(channel)?.name ?? null;
|
|
423
|
+
}
|
|
424
|
+
function chatKey(chat) {
|
|
425
|
+
return safeId(`${chat.channelType}\n${chat.channel}\n${chat.channelId}\n${chat.chatType}\n${chat.partition}\n${chat.threadId || ''}`);
|
|
426
|
+
}
|
|
427
|
+
function mergePeerInfo(chats) {
|
|
428
|
+
const map = new Map();
|
|
429
|
+
for (const chat of chats) {
|
|
430
|
+
const key = chatKey(chat);
|
|
431
|
+
let peer = map.get(key);
|
|
432
|
+
if (!peer) {
|
|
433
|
+
peer = {
|
|
434
|
+
id: key,
|
|
435
|
+
peerId: key,
|
|
436
|
+
peerName: chat.chatType === 'group' ? (chat.groupName || chat.groupId || chat.channelId) : (chat.peerName || chat.peerId),
|
|
437
|
+
channel: chat.channel,
|
|
438
|
+
channelType: chat.channelType,
|
|
439
|
+
channelName: channelName(chat.channel),
|
|
440
|
+
chatType: chat.chatType,
|
|
441
|
+
groupId: chat.groupId,
|
|
442
|
+
groupName: chat.groupName,
|
|
443
|
+
partition: chat.partition,
|
|
444
|
+
threadId: chat.threadId,
|
|
445
|
+
threadName: chat.threadName,
|
|
446
|
+
inbound: 0,
|
|
447
|
+
outbound: 0,
|
|
448
|
+
lastAt: 0,
|
|
449
|
+
};
|
|
450
|
+
map.set(key, peer);
|
|
451
|
+
}
|
|
452
|
+
for (const msg of chat.messages) {
|
|
453
|
+
if (msg.dir === 'in')
|
|
454
|
+
peer.inbound++;
|
|
455
|
+
else if (msg.dir === 'out')
|
|
456
|
+
peer.outbound++;
|
|
457
|
+
if (msg.ts > peer.lastAt)
|
|
458
|
+
peer.lastAt = msg.ts;
|
|
459
|
+
}
|
|
460
|
+
if (chat.updatedAt > peer.lastAt)
|
|
461
|
+
peer.lastAt = chat.updatedAt;
|
|
462
|
+
}
|
|
463
|
+
return Array.from(map.values()).sort((a, b) => b.lastAt - a.lastAt);
|
|
464
|
+
}
|
|
465
|
+
function selectedChats(chats, scope) {
|
|
466
|
+
if (!scope)
|
|
467
|
+
return [];
|
|
468
|
+
return chats.filter(chat => scopeId(chat) === scope);
|
|
469
|
+
}
|
|
470
|
+
function messagesFor(chats, peer) {
|
|
471
|
+
const selected = peer
|
|
472
|
+
? chats.filter(chat => chatKey(chat) === peer)
|
|
473
|
+
: chats;
|
|
474
|
+
const messages = selected.flatMap(chat => chat.messages.map(msg => ({
|
|
475
|
+
...msg,
|
|
476
|
+
channel: chat.channel,
|
|
477
|
+
channelType: chat.channelType,
|
|
478
|
+
selfAID: chat.selfAID,
|
|
479
|
+
peerName: chat.peerName,
|
|
480
|
+
groupName: chat.groupName,
|
|
481
|
+
partition: chat.partition,
|
|
482
|
+
threadName: chat.threadName,
|
|
483
|
+
})));
|
|
484
|
+
messages.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
485
|
+
return messages.length > 1000 ? messages.slice(-1000) : messages;
|
|
486
|
+
}
|
|
487
|
+
function latestInbound(chat) {
|
|
488
|
+
for (let i = chat.messages.length - 1; i >= 0; i--) {
|
|
489
|
+
if (chat.messages[i].dir === 'in')
|
|
490
|
+
return chat.messages[i];
|
|
491
|
+
}
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
async function sessionTurns(active, messages) {
|
|
495
|
+
const baseagent = String(active.baseagent || active.agentType || '').toLowerCase();
|
|
496
|
+
const agentSessionId = typeof active.agentSessionId === 'string' ? active.agentSessionId : '';
|
|
497
|
+
const projectPath = typeof active.projectPath === 'string' ? active.projectPath : '';
|
|
498
|
+
if (baseagent && agentSessionId && projectPath) {
|
|
499
|
+
try {
|
|
500
|
+
const adapter = await getSessionAdapter(baseagent);
|
|
501
|
+
const turns = Number(adapter?.getFileInfo(projectPath, agentSessionId)?.turns);
|
|
502
|
+
if (Number.isFinite(turns) && turns > 0)
|
|
503
|
+
return turns;
|
|
504
|
+
}
|
|
505
|
+
catch { }
|
|
506
|
+
}
|
|
507
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
508
|
+
const turns = Number(messages[i].numTurns);
|
|
509
|
+
if (Number.isFinite(turns) && turns >= 0)
|
|
510
|
+
return turns;
|
|
511
|
+
}
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
async function buildSessionContext(chat) {
|
|
515
|
+
if (!chat || chat.partition === 'legacy')
|
|
516
|
+
return null;
|
|
517
|
+
const active = chat.active || {};
|
|
518
|
+
const inbound = latestInbound(chat);
|
|
519
|
+
const actorId = String(inbound?.from
|
|
520
|
+
|| active.metadata?.peerId
|
|
521
|
+
|| chat.peerId
|
|
522
|
+
|| chat.channelId
|
|
523
|
+
|| '');
|
|
524
|
+
const conversationId = String(chat.groupId || chat.channelId || chat.peerId || '');
|
|
525
|
+
let role = inbound?.permMode || null;
|
|
526
|
+
let permissionMode = active.permissionMode || null;
|
|
527
|
+
let chatMode = null;
|
|
528
|
+
const peerKey = `${chat.channelType}#${encodeURIComponent(conversationId)}`;
|
|
529
|
+
try {
|
|
530
|
+
const { resolver, modelScope, peerMode } = await getContextModules();
|
|
531
|
+
const detail = resolver.resolvePeerRoleDetail({
|
|
532
|
+
selfAid: chat.selfAID,
|
|
533
|
+
channelKey: chat.channel,
|
|
534
|
+
channelType: chat.channelType,
|
|
535
|
+
chatType: chat.chatType === 'group' ? 'group' : 'private',
|
|
536
|
+
actorId,
|
|
537
|
+
conversationId,
|
|
538
|
+
peerType: inbound?.peerType || active.metadata?.peerType,
|
|
539
|
+
});
|
|
540
|
+
role = detail.effectiveRole || 'none';
|
|
541
|
+
permissionMode = modelScope.resolvePermissionMode({ self: chat.selfAID, peerKey, role });
|
|
542
|
+
chatMode = peerMode.resolveChatMode({
|
|
543
|
+
self: chat.selfAID,
|
|
544
|
+
peerKey,
|
|
545
|
+
role,
|
|
546
|
+
chatType: chat.chatType,
|
|
547
|
+
peerType: inbound?.peerType || active.metadata?.peerType,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
catch { }
|
|
551
|
+
return {
|
|
552
|
+
sessionId: active.agentSessionId || active.id || null,
|
|
553
|
+
evolcoreSessionId: active.id || null,
|
|
554
|
+
name: active.name || null,
|
|
555
|
+
baseagent: active.baseagent || active.agentType || null,
|
|
556
|
+
chatMode,
|
|
557
|
+
permissionMode,
|
|
558
|
+
role,
|
|
559
|
+
turns: await sessionTurns(active, chat.messages),
|
|
560
|
+
status: active.activeTask ? 'processing' : 'idle',
|
|
561
|
+
updatedAt: Number(active.updatedAt || chat.updatedAt || 0),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
async function buildSnapshot(params) {
|
|
565
|
+
const chats = loadChats();
|
|
566
|
+
const scopes = buildScopes(chats);
|
|
567
|
+
const requestedScope = params.scope || params.aid || null;
|
|
568
|
+
const scope = requestedScope && !scopes.some(s => s.id === requestedScope)
|
|
569
|
+
? (chats.find(chat => legacyChannelScopeId(chat) === requestedScope)
|
|
570
|
+
? scopeId(chats.find(chat => legacyChannelScopeId(chat) === requestedScope))
|
|
571
|
+
: requestedScope)
|
|
572
|
+
: requestedScope;
|
|
573
|
+
const peer = params.peer || null;
|
|
574
|
+
const inScope = selectedChats(chats, scope);
|
|
575
|
+
const peers = scope ? mergePeerInfo(inScope) : [];
|
|
576
|
+
const messages = scope ? messagesFor(inScope, peer) : [];
|
|
577
|
+
const currentChat = peer ? inScope.find(chat => chatKey(chat) === peer) : undefined;
|
|
578
|
+
const sessionContext = await buildSessionContext(currentChat);
|
|
579
|
+
return {
|
|
580
|
+
scopes,
|
|
581
|
+
aids: scopes,
|
|
582
|
+
peers,
|
|
583
|
+
messages,
|
|
584
|
+
scope,
|
|
585
|
+
aid: scope,
|
|
586
|
+
peer,
|
|
587
|
+
sessionContext,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
export const msgSource = {
|
|
591
|
+
kind: 'msg',
|
|
592
|
+
async snapshot(params = {}) {
|
|
593
|
+
return await buildSnapshot(params);
|
|
594
|
+
},
|
|
595
|
+
subscribe(params, push) {
|
|
596
|
+
const sessionsDir = resolvePaths().sessionsDir;
|
|
597
|
+
let watcher = null;
|
|
598
|
+
let debounce = null;
|
|
599
|
+
const fire = () => {
|
|
600
|
+
if (debounce)
|
|
601
|
+
clearTimeout(debounce);
|
|
602
|
+
debounce = setTimeout(async () => {
|
|
603
|
+
try {
|
|
604
|
+
push(await buildSnapshot(params));
|
|
605
|
+
}
|
|
606
|
+
catch { /* ignore */ }
|
|
607
|
+
}, 150);
|
|
608
|
+
};
|
|
609
|
+
try {
|
|
610
|
+
watcher = fs.watch(sessionsDir, { recursive: true }, (_evt, filename) => {
|
|
611
|
+
const name = String(filename || '');
|
|
612
|
+
if (name.endsWith('messages.jsonl') || name.endsWith('active.json')
|
|
613
|
+
|| name.endsWith('thread-index.json') || (name.includes('_threads') && name.endsWith('.jsonl')))
|
|
614
|
+
fire();
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
catch { /* sessionsDir may not exist yet */ }
|
|
618
|
+
return () => {
|
|
619
|
+
if (watcher)
|
|
620
|
+
watcher.close();
|
|
621
|
+
if (debounce)
|
|
622
|
+
clearTimeout(debounce);
|
|
623
|
+
};
|
|
624
|
+
},
|
|
625
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
let packageRootCache = null;
|
|
6
|
+
function isEvolcorePackageRoot(dir) {
|
|
7
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
8
|
+
if (!fs.existsSync(pkgPath))
|
|
9
|
+
return false;
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
12
|
+
return pkg?.name === 'evolcore';
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function findUp(start) {
|
|
19
|
+
let dir = path.resolve(start);
|
|
20
|
+
while (true) {
|
|
21
|
+
if (isEvolcorePackageRoot(dir))
|
|
22
|
+
return dir;
|
|
23
|
+
const parent = path.dirname(dir);
|
|
24
|
+
if (parent === dir)
|
|
25
|
+
return null;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function candidateStarts() {
|
|
30
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const starts = [
|
|
32
|
+
process.env.EVOLCORE_PACKAGE_ROOT,
|
|
33
|
+
here,
|
|
34
|
+
process.cwd(),
|
|
35
|
+
process.argv[1] ? path.dirname(process.argv[1]) : undefined,
|
|
36
|
+
];
|
|
37
|
+
return [...new Set(starts.filter((item) => !!item))];
|
|
38
|
+
}
|
|
39
|
+
export function resolveEvolcorePackageRoot() {
|
|
40
|
+
if (packageRootCache)
|
|
41
|
+
return packageRootCache;
|
|
42
|
+
const searched = [];
|
|
43
|
+
for (const start of candidateStarts()) {
|
|
44
|
+
searched.push(start);
|
|
45
|
+
const root = findUp(start);
|
|
46
|
+
if (root) {
|
|
47
|
+
packageRootCache = root;
|
|
48
|
+
return root;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const require = createRequire(import.meta.url);
|
|
53
|
+
const pkgJson = require.resolve('evolcore/package.json');
|
|
54
|
+
const root = path.dirname(pkgJson);
|
|
55
|
+
if (isEvolcorePackageRoot(root)) {
|
|
56
|
+
packageRootCache = root;
|
|
57
|
+
return root;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Fall through to a diagnostic error.
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`Main package root not found. searched=${searched.join(', ')}`);
|
|
64
|
+
}
|
|
65
|
+
export function resolveParentDistModule(...segments) {
|
|
66
|
+
const root = resolveEvolcorePackageRoot();
|
|
67
|
+
const modulePath = path.join(root, 'dist', ...segments);
|
|
68
|
+
if (!fs.existsSync(modulePath)) {
|
|
69
|
+
throw new Error(`Main package dist module not found: ${modulePath}. Run npm run build in ${root}.`);
|
|
70
|
+
}
|
|
71
|
+
return modulePath;
|
|
72
|
+
}
|
|
73
|
+
export function toFileUrl(p) {
|
|
74
|
+
return process.platform === 'win32'
|
|
75
|
+
? new URL('file:///' + p.replace(/\\/g, '/')).href
|
|
76
|
+
: p;
|
|
77
|
+
}
|