ec-web 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server.js +73 -12
- package/dist/sources/monitor.js +90 -7
- package/dist/sources/msg.js +193 -7
- package/dist/sources/relation-roles.js +2 -24
- package/dist/sources/role-definitions.js +11 -29
- package/dist/sources/session-codex.js +43 -29
- package/dist/sources/session-origin.js +195 -0
- package/dist/sources/session.js +211 -33
- package/dist/static/app.js +421 -152
- package/dist/static/index.html +8 -9
- package/dist/static/style.css +81 -1
- package/dist/static/vendor/echarts-5.6.0.min.js +45 -0
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -66,10 +66,26 @@ const ECWEB_VERSION = readEcwebVersion();
|
|
|
66
66
|
const MIME = {
|
|
67
67
|
'.html': 'text/html; charset=utf-8',
|
|
68
68
|
'.js': 'text/javascript; charset=utf-8',
|
|
69
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
69
70
|
'.css': 'text/css; charset=utf-8',
|
|
70
71
|
'.json': 'application/json; charset=utf-8',
|
|
72
|
+
'.webmanifest': 'application/manifest+json; charset=utf-8',
|
|
71
73
|
'.svg': 'image/svg+xml',
|
|
74
|
+
'.png': 'image/png',
|
|
75
|
+
'.jpg': 'image/jpeg',
|
|
76
|
+
'.jpeg': 'image/jpeg',
|
|
77
|
+
'.gif': 'image/gif',
|
|
78
|
+
'.webp': 'image/webp',
|
|
79
|
+
'.avif': 'image/avif',
|
|
80
|
+
'.ico': 'image/x-icon',
|
|
81
|
+
'.woff': 'font/woff',
|
|
82
|
+
'.woff2': 'font/woff2',
|
|
83
|
+
'.ttf': 'font/ttf',
|
|
84
|
+
'.otf': 'font/otf',
|
|
85
|
+
'.wasm': 'application/wasm',
|
|
72
86
|
};
|
|
87
|
+
const HTML_CACHE_CONTROL = 'no-cache';
|
|
88
|
+
const STATIC_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
|
|
73
89
|
function tokenStorePath() {
|
|
74
90
|
const dir = resolvePaths().instanceDir;
|
|
75
91
|
const current = path.join(dir, 'ecweb-tokens.json');
|
|
@@ -141,7 +157,32 @@ function injectBaseHref(html, prefix) {
|
|
|
141
157
|
return html.replace(/<base\b[^>]*>/i, tag);
|
|
142
158
|
return html.replace(/<head[^>]*>/i, (m) => `${m}\n ${tag}`);
|
|
143
159
|
}
|
|
144
|
-
function
|
|
160
|
+
function createEtag(content) {
|
|
161
|
+
return `"${crypto.createHash('sha256').update(content).digest('hex')}"`;
|
|
162
|
+
}
|
|
163
|
+
function matchesIfNoneMatch(req, etag) {
|
|
164
|
+
const header = req.headers['if-none-match'];
|
|
165
|
+
const value = Array.isArray(header) ? header.join(',') : header;
|
|
166
|
+
if (!value)
|
|
167
|
+
return false;
|
|
168
|
+
// GET uses weak comparison, so a client may send the validator with W/.
|
|
169
|
+
return value.split(',').some((candidate) => {
|
|
170
|
+
const normalized = candidate.trim().replace(/^W\//, '');
|
|
171
|
+
return normalized === '*' || normalized === etag;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function respondWithValidator(req, res, body, contentType, cacheControl, extraHeaders = {}) {
|
|
175
|
+
const etag = createEtag(body);
|
|
176
|
+
const headers = { 'Content-Type': contentType, 'Cache-Control': cacheControl, ETag: etag, ...extraHeaders };
|
|
177
|
+
if (matchesIfNoneMatch(req, etag)) {
|
|
178
|
+
res.writeHead(304, headers);
|
|
179
|
+
res.end();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
res.writeHead(200, headers);
|
|
183
|
+
res.end(body);
|
|
184
|
+
}
|
|
185
|
+
export function serveStatic(req, res) {
|
|
145
186
|
let urlPath = (req.url || '/').split('?')[0];
|
|
146
187
|
if (urlPath === '/')
|
|
147
188
|
urlPath = '/index.html';
|
|
@@ -161,12 +202,13 @@ function serveStatic(req, res) {
|
|
|
161
202
|
if (ext === '.html') {
|
|
162
203
|
const prefix = forwardedPrefix(req);
|
|
163
204
|
const html = injectBaseHref(data.toString('utf-8'), prefix);
|
|
164
|
-
|
|
165
|
-
res
|
|
205
|
+
// The proxy prefix changes the representation, so shared caches must key on it.
|
|
206
|
+
respondWithValidator(req, res, html, MIME[ext], HTML_CACHE_CONTROL, { Vary: 'X-Forwarded-Prefix' });
|
|
166
207
|
return;
|
|
167
208
|
}
|
|
168
|
-
|
|
169
|
-
|
|
209
|
+
// Static files have stable ETags, including fonts, images, manifests, and vendored libraries.
|
|
210
|
+
// Their fixed URLs must revalidate so deployments never leave clients on an old asset.
|
|
211
|
+
respondWithValidator(req, res, data, MIME[ext] || 'application/octet-stream', STATIC_CACHE_CONTROL);
|
|
170
212
|
});
|
|
171
213
|
}
|
|
172
214
|
// ── Helpers ──
|
|
@@ -446,6 +488,7 @@ function handleConnection(ws, req, log) {
|
|
|
446
488
|
const ip = clientIp(req);
|
|
447
489
|
let unsubscribe = null;
|
|
448
490
|
let currentView = null;
|
|
491
|
+
let subscriptionGeneration = 0;
|
|
449
492
|
log(`◆ WS 连接 from ${ip}`);
|
|
450
493
|
const send = (obj) => {
|
|
451
494
|
if (ws.readyState === ws.OPEN)
|
|
@@ -455,6 +498,7 @@ function handleConnection(ws, req, log) {
|
|
|
455
498
|
catch { }
|
|
456
499
|
};
|
|
457
500
|
const switchSubscription = async (view, params) => {
|
|
501
|
+
const generation = ++subscriptionGeneration;
|
|
458
502
|
if (unsubscribe) {
|
|
459
503
|
unsubscribe();
|
|
460
504
|
unsubscribe = null;
|
|
@@ -468,16 +512,28 @@ function handleConnection(ws, req, log) {
|
|
|
468
512
|
const sourceParams = { ...params, __menuAuth: daemonOwnerMenuAuth() };
|
|
469
513
|
// System 视图:把 ECWeb 自身版本合并进快照(前端版本卡用)
|
|
470
514
|
const decorate = (data) => (view === 'system' && data ? { ...data, ecwebVersion: ECWEB_VERSION } : data);
|
|
515
|
+
let initialSnapshot;
|
|
471
516
|
try {
|
|
472
|
-
|
|
517
|
+
initialSnapshot = await source.snapshot(sourceParams);
|
|
518
|
+
if (generation !== subscriptionGeneration)
|
|
519
|
+
return;
|
|
520
|
+
send({ type: 'snapshot', view, data: decorate(initialSnapshot) });
|
|
473
521
|
}
|
|
474
522
|
catch (e) {
|
|
523
|
+
if (generation !== subscriptionGeneration)
|
|
524
|
+
return;
|
|
475
525
|
send({ type: 'error', message: `snapshot failed: ${e?.message || e}` });
|
|
476
526
|
}
|
|
477
|
-
|
|
478
|
-
if (currentView === view)
|
|
527
|
+
const nextUnsubscribe = source.subscribe(sourceParams, (data) => {
|
|
528
|
+
if (generation === subscriptionGeneration && currentView === view) {
|
|
479
529
|
send({ type: 'delta', view, data: decorate(data) });
|
|
530
|
+
}
|
|
480
531
|
});
|
|
532
|
+
if (generation !== subscriptionGeneration) {
|
|
533
|
+
nextUnsubscribe();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
unsubscribe = nextUnsubscribe;
|
|
481
537
|
};
|
|
482
538
|
ws.on('message', async (raw) => {
|
|
483
539
|
let msg;
|
|
@@ -563,10 +619,15 @@ function handleConnection(ws, req, log) {
|
|
|
563
619
|
ws.ping();
|
|
564
620
|
}, 25000);
|
|
565
621
|
ws.on('pong', () => { alive = true; });
|
|
566
|
-
ws.on('close', () => {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
622
|
+
ws.on('close', () => {
|
|
623
|
+
subscriptionGeneration += 1;
|
|
624
|
+
clearInterval(heartbeat);
|
|
625
|
+
if (unsubscribe) {
|
|
626
|
+
unsubscribe();
|
|
627
|
+
unsubscribe = null;
|
|
628
|
+
}
|
|
629
|
+
log(`◇ WS 断开 from ${ip}`);
|
|
630
|
+
});
|
|
570
631
|
ws.on('error', () => { });
|
|
571
632
|
}
|
|
572
633
|
// ── Port binding ──
|
package/dist/sources/monitor.js
CHANGED
|
@@ -8,11 +8,44 @@
|
|
|
8
8
|
* - fine : 2s 桶 × 60 ≈ 近 2 分钟
|
|
9
9
|
* - mid : 10s 桶 × 60 ≈ 近 10 分钟
|
|
10
10
|
* - coarse : 60s 桶 × 60 ≈ 近 1 小时
|
|
11
|
-
* 每个桶内对 CPU
|
|
11
|
+
* 每个桶内对 CPU/内存做均值聚合,关联进程先按类型和名称合并,
|
|
12
|
+
* 避免长周期下点数爆炸,也不让 PID 变化拆散同一条曲线。
|
|
12
13
|
*/
|
|
13
14
|
import fs from 'node:fs';
|
|
14
15
|
import { resolvePaths } from '../paths.js';
|
|
15
16
|
import { ipcQuery } from '../ipc-client.js';
|
|
17
|
+
function processGroupKey(process) {
|
|
18
|
+
return `${process.kind}\u0000${process.name}`;
|
|
19
|
+
}
|
|
20
|
+
/** 同一类、同一可执行名的多个实例在一个采样点中汇总。 */
|
|
21
|
+
export function mergeProcessesByIdentity(processes) {
|
|
22
|
+
const merged = new Map();
|
|
23
|
+
for (const process of processes) {
|
|
24
|
+
const name = String(process.name || 'process');
|
|
25
|
+
const kind = String(process.kind || 'child');
|
|
26
|
+
const key = processGroupKey({ name, kind });
|
|
27
|
+
const current = merged.get(key);
|
|
28
|
+
if (current) {
|
|
29
|
+
current.cpuPercent += Number(process.cpuPercent) || 0;
|
|
30
|
+
current.rss += Number(process.rss) || 0;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
merged.set(key, {
|
|
34
|
+
name,
|
|
35
|
+
kind,
|
|
36
|
+
cpuPercent: Number(process.cpuPercent) || 0,
|
|
37
|
+
rss: Number(process.rss) || 0,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...merged.values()].sort((left, right) => processGroupKey(left).localeCompare(processGroupKey(right)));
|
|
42
|
+
}
|
|
43
|
+
/** 将进程的单核 CPU 百分比换算为整机全部逻辑 CPU 的容量占比。 */
|
|
44
|
+
export function normalizeProcessCpuPercent(cpuPercent, cpuCount) {
|
|
45
|
+
const count = Number(cpuCount);
|
|
46
|
+
const divisor = Number.isFinite(count) && count > 0 ? count : 1;
|
|
47
|
+
return (Number(cpuPercent) || 0) / divisor;
|
|
48
|
+
}
|
|
16
49
|
// 模块级缓冲区——所有订阅者共享同一份累积时序(单一事实源)。
|
|
17
50
|
const resolutions = {
|
|
18
51
|
fine: { bucketMs: 2_000, cap: 60, buf: [] },
|
|
@@ -29,10 +62,12 @@ function ingest(res, sample) {
|
|
|
29
62
|
last.sysCpu = last.sysCpu + (sample.sysCpu - last.sysCpu) / n;
|
|
30
63
|
last.procRss = last.procRss + (sample.procRss - last.procRss) / n;
|
|
31
64
|
last.sysMemUsed = last.sysMemUsed + (sample.sysMemUsed - last.sysMemUsed) / n;
|
|
65
|
+
last.diskReadBps = last.diskReadBps + (sample.diskReadBps - last.diskReadBps) / n;
|
|
66
|
+
last.diskWriteBps = last.diskWriteBps + (sample.diskWriteBps - last.diskWriteBps) / n;
|
|
32
67
|
last.netRxBps = last.netRxBps + (sample.netRxBps - last.netRxBps) / n;
|
|
33
68
|
last.netTxBps = last.netTxBps + (sample.netTxBps - last.netTxBps) / n;
|
|
34
69
|
for (const process of sample.processes) {
|
|
35
|
-
const current = last.processes.find(item => item
|
|
70
|
+
const current = last.processes.find(item => processGroupKey(item) === processGroupKey(process));
|
|
36
71
|
if (current) {
|
|
37
72
|
const processSamples = current._n + 1;
|
|
38
73
|
current.cpuPercent += (process.cpuPercent - current.cpuPercent) / processSamples;
|
|
@@ -61,10 +96,11 @@ function exportBuf(res) {
|
|
|
61
96
|
sysCpu: round1(b.sysCpu),
|
|
62
97
|
procRss: Math.round(b.procRss),
|
|
63
98
|
sysMemUsed: Math.round(b.sysMemUsed),
|
|
99
|
+
diskReadBps: Math.round(b.diskReadBps),
|
|
100
|
+
diskWriteBps: Math.round(b.diskWriteBps),
|
|
64
101
|
netRxBps: Math.round(b.netRxBps),
|
|
65
102
|
netTxBps: Math.round(b.netTxBps),
|
|
66
103
|
processes: b.processes.map(process => ({
|
|
67
|
-
pid: process.pid,
|
|
68
104
|
name: process.name,
|
|
69
105
|
kind: process.kind,
|
|
70
106
|
cpuPercent: round1(process.cpuPercent),
|
|
@@ -73,6 +109,49 @@ function exportBuf(res) {
|
|
|
73
109
|
}));
|
|
74
110
|
}
|
|
75
111
|
function round1(n) { return Math.round(n * 10) / 10; }
|
|
112
|
+
const DISK_SECTOR_BYTES = 512;
|
|
113
|
+
let previousDiskSample = null;
|
|
114
|
+
let latestDiskRate = { readBps: 0, writeBps: 0 };
|
|
115
|
+
/** 解析 Linux diskstats,只累计整盘,避免分区和虚拟映射重复计数。 */
|
|
116
|
+
export function parseLinuxDiskStats(input, isPartition = name => fs.existsSync(`/sys/class/block/${name}/partition`)) {
|
|
117
|
+
let readBytes = 0;
|
|
118
|
+
let writeBytes = 0;
|
|
119
|
+
for (const line of input.split('\n')) {
|
|
120
|
+
const fields = line.trim().split(/\s+/);
|
|
121
|
+
if (fields.length < 10)
|
|
122
|
+
continue;
|
|
123
|
+
const name = fields[2];
|
|
124
|
+
if (!name || /^(loop|ram|zram|fd|sr|dm-|md|zd)/.test(name) || isPartition(name))
|
|
125
|
+
continue;
|
|
126
|
+
readBytes += (Number(fields[5]) || 0) * DISK_SECTOR_BYTES;
|
|
127
|
+
writeBytes += (Number(fields[9]) || 0) * DISK_SECTOR_BYTES;
|
|
128
|
+
}
|
|
129
|
+
return { readBytes, writeBytes };
|
|
130
|
+
}
|
|
131
|
+
function readDiskTotals() {
|
|
132
|
+
try {
|
|
133
|
+
return parseLinuxDiskStats(fs.readFileSync('/proc/diskstats', 'utf8'));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { readBytes: 0, writeBytes: 0 };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function sampleDiskRate(ts) {
|
|
140
|
+
const totals = readDiskTotals();
|
|
141
|
+
if (!previousDiskSample) {
|
|
142
|
+
previousDiskSample = { ...totals, ts };
|
|
143
|
+
return latestDiskRate;
|
|
144
|
+
}
|
|
145
|
+
const elapsedMs = ts - previousDiskSample.ts;
|
|
146
|
+
if (elapsedMs < 250)
|
|
147
|
+
return latestDiskRate;
|
|
148
|
+
latestDiskRate = {
|
|
149
|
+
readBps: Math.max(0, (totals.readBytes - previousDiskSample.readBytes) * 1000 / elapsedMs),
|
|
150
|
+
writeBps: Math.max(0, (totals.writeBytes - previousDiskSample.writeBytes) * 1000 / elapsedMs),
|
|
151
|
+
};
|
|
152
|
+
previousDiskSample = { ...totals, ts };
|
|
153
|
+
return latestDiskRate;
|
|
154
|
+
}
|
|
76
155
|
let previousNetworkSample = null;
|
|
77
156
|
let latestNetworkRate = { rxBps: 0, txBps: 0 };
|
|
78
157
|
function readNetworkTotals() {
|
|
@@ -122,22 +201,26 @@ async function fetchSnapshot() {
|
|
|
122
201
|
}
|
|
123
202
|
const s = resp.snapshot;
|
|
124
203
|
const ts = Number(s.ts) || Date.now();
|
|
204
|
+
const cpuCount = Number(s.cpuCount) || 1;
|
|
205
|
+
const disk = sampleDiskRate(ts);
|
|
125
206
|
const network = sampleNetworkRate(ts);
|
|
126
207
|
const sample = {
|
|
127
208
|
ts,
|
|
128
|
-
procCpu: s.cpuPercent
|
|
209
|
+
procCpu: normalizeProcessCpuPercent(s.cpuPercent, cpuCount),
|
|
129
210
|
sysCpu: s.system?.cpuPercent ?? 0,
|
|
130
211
|
procRss: s.memory?.rss ?? 0,
|
|
131
212
|
sysMemUsed: s.system?.memUsed ?? 0,
|
|
213
|
+
diskReadBps: disk.readBps,
|
|
214
|
+
diskWriteBps: disk.writeBps,
|
|
132
215
|
netRxBps: network.rxBps,
|
|
133
216
|
netTxBps: network.txBps,
|
|
134
|
-
processes: Array.isArray(s.processTree?.processes) ? s.processTree.processes.map((process) => ({
|
|
217
|
+
processes: mergeProcessesByIdentity(Array.isArray(s.processTree?.processes) ? s.processTree.processes.map((process) => ({
|
|
135
218
|
pid: Number(process.pid),
|
|
136
219
|
name: String(process.name || 'process'),
|
|
137
220
|
kind: String(process.kind || 'child'),
|
|
138
|
-
cpuPercent:
|
|
221
|
+
cpuPercent: normalizeProcessCpuPercent(process.cpuPercent, cpuCount),
|
|
139
222
|
rss: Number(process.rss) || 0,
|
|
140
|
-
})) : [],
|
|
223
|
+
})) : []),
|
|
141
224
|
};
|
|
142
225
|
ingest(resolutions.fine, sample);
|
|
143
226
|
ingest(resolutions.mid, sample);
|
package/dist/sources/msg.js
CHANGED
|
@@ -6,11 +6,16 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'fs';
|
|
8
8
|
import path from 'path';
|
|
9
|
+
import { ipcQuery } from '../ipc-client.js';
|
|
9
10
|
import { resolvePaths } from '../paths.js';
|
|
10
11
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
11
12
|
import { isTransientProtocolMessage, readAllJsonlLines, readJsonFile, } from '../fs-utils.js';
|
|
12
13
|
let contextModulesPromise = null;
|
|
13
14
|
const sessionAdapterPromises = new Map();
|
|
15
|
+
let peerDisplayResolverPromise = null;
|
|
16
|
+
const aunGroupNameCache = new Map();
|
|
17
|
+
const aunGroupNameRequests = new Map();
|
|
18
|
+
let agentDisplayNamesCache = null;
|
|
14
19
|
function getContextModules() {
|
|
15
20
|
if (!contextModulesPromise) {
|
|
16
21
|
contextModulesPromise = Promise.all([
|
|
@@ -21,6 +26,145 @@ function getContextModules() {
|
|
|
21
26
|
}
|
|
22
27
|
return contextModulesPromise;
|
|
23
28
|
}
|
|
29
|
+
function getPeerDisplayResolver() {
|
|
30
|
+
if (!peerDisplayResolverPromise) {
|
|
31
|
+
peerDisplayResolverPromise = Promise.resolve()
|
|
32
|
+
.then(() => import(toFileUrl(resolveParentDistModule('config', 'contact-book.js'))))
|
|
33
|
+
.then(mod => typeof mod.resolvePeerDisplayLabel === 'function' ? mod.resolvePeerDisplayLabel : null)
|
|
34
|
+
.catch(() => null);
|
|
35
|
+
}
|
|
36
|
+
return peerDisplayResolverPromise;
|
|
37
|
+
}
|
|
38
|
+
export function resolveContactPeerName(resolver, identity, peerId, fallback) {
|
|
39
|
+
if (!resolver)
|
|
40
|
+
return fallback;
|
|
41
|
+
try {
|
|
42
|
+
return resolver(identity.selfAID, identity.channelType, peerId, identity.channel) || fallback;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function aunGroupIdVariants(groupId) {
|
|
49
|
+
const id = String(groupId || '').trim();
|
|
50
|
+
if (!id)
|
|
51
|
+
return [];
|
|
52
|
+
const variants = new Set([id]);
|
|
53
|
+
const legacy = /^group\.agentid\.pub\/([^/]+)$/i.exec(id);
|
|
54
|
+
if (legacy)
|
|
55
|
+
variants.add(`${legacy[1]}.agentid.pub`);
|
|
56
|
+
const current = /^([^.]+)\.agentid\.pub$/i.exec(id);
|
|
57
|
+
if (current)
|
|
58
|
+
variants.add(`group.agentid.pub/${current[1]}`);
|
|
59
|
+
return [...variants];
|
|
60
|
+
}
|
|
61
|
+
export function resolveAunGroupName(names, groupId, fallback) {
|
|
62
|
+
for (const id of aunGroupIdVariants(groupId || '')) {
|
|
63
|
+
const name = names.get(id);
|
|
64
|
+
if (name)
|
|
65
|
+
return name;
|
|
66
|
+
}
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
async function fetchAunGroupNames(aid) {
|
|
70
|
+
const names = new Map();
|
|
71
|
+
let cursor = null;
|
|
72
|
+
let page = 0;
|
|
73
|
+
do {
|
|
74
|
+
page += 1;
|
|
75
|
+
const response = await ipcQuery(resolvePaths().socket, {
|
|
76
|
+
type: 'menu.exec',
|
|
77
|
+
payload: {
|
|
78
|
+
type: 'menu.query',
|
|
79
|
+
name: 'connect',
|
|
80
|
+
cmd: '/connect',
|
|
81
|
+
agent: aid,
|
|
82
|
+
id: `ecweb-msg-groups-${Date.now()}-${page}`,
|
|
83
|
+
args: { self: aid, view: 'groups', limit: 200, ...(cursor ? { cursor } : {}) },
|
|
84
|
+
},
|
|
85
|
+
auth: { source: 'ecweb', localDirect: true },
|
|
86
|
+
}, 10_000);
|
|
87
|
+
const data = response?.ok && !response.response?.error ? response.response?.data : null;
|
|
88
|
+
if (!data || !Array.isArray(data.items))
|
|
89
|
+
break;
|
|
90
|
+
for (const item of data.items) {
|
|
91
|
+
const groupId = String(item?.groupId || '').trim();
|
|
92
|
+
const displayName = String(item?.displayName || '').trim();
|
|
93
|
+
if (!groupId || !displayName)
|
|
94
|
+
continue;
|
|
95
|
+
for (const variant of aunGroupIdVariants(groupId))
|
|
96
|
+
names.set(variant, displayName);
|
|
97
|
+
}
|
|
98
|
+
cursor = typeof data.nextCursor === 'string' && data.nextCursor ? data.nextCursor : null;
|
|
99
|
+
} while (cursor && page < 20);
|
|
100
|
+
return names;
|
|
101
|
+
}
|
|
102
|
+
function getAunGroupNames(aid) {
|
|
103
|
+
const cached = aunGroupNameCache.get(aid);
|
|
104
|
+
if (cached && cached.expiresAt > Date.now())
|
|
105
|
+
return Promise.resolve(cached.names);
|
|
106
|
+
const pending = aunGroupNameRequests.get(aid);
|
|
107
|
+
if (pending)
|
|
108
|
+
return pending;
|
|
109
|
+
const request = fetchAunGroupNames(aid)
|
|
110
|
+
.then(names => {
|
|
111
|
+
aunGroupNameCache.set(aid, {
|
|
112
|
+
names,
|
|
113
|
+
expiresAt: Date.now() + (names.size ? 5 * 60_000 : 15_000),
|
|
114
|
+
});
|
|
115
|
+
return names;
|
|
116
|
+
})
|
|
117
|
+
.catch(() => new Map())
|
|
118
|
+
.finally(() => aunGroupNameRequests.delete(aid));
|
|
119
|
+
aunGroupNameRequests.set(aid, request);
|
|
120
|
+
return request;
|
|
121
|
+
}
|
|
122
|
+
async function hydrateAunGroupNames(chats) {
|
|
123
|
+
const aids = [...new Set(chats
|
|
124
|
+
.filter(chat => chat.channelType === 'aun' && chat.chatType === 'group')
|
|
125
|
+
.map(chat => chat.selfAID))];
|
|
126
|
+
if (!aids.length)
|
|
127
|
+
return;
|
|
128
|
+
const directories = new Map(await Promise.all(aids.map(async (aid) => [aid, await getAunGroupNames(aid)])));
|
|
129
|
+
for (const chat of chats) {
|
|
130
|
+
if (chat.channelType !== 'aun' || chat.chatType !== 'group')
|
|
131
|
+
continue;
|
|
132
|
+
const groupName = resolveAunGroupName(directories.get(chat.selfAID) || new Map(), chat.groupId, chat.groupName);
|
|
133
|
+
if (!groupName)
|
|
134
|
+
continue;
|
|
135
|
+
chat.groupName = groupName;
|
|
136
|
+
chat.peerName = groupName;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function resolveSessionBindDisplayName(bind, resolver, aunGroupNames = new Map()) {
|
|
140
|
+
const isGroup = bind.chatType === 'group' || !!bind.groupId;
|
|
141
|
+
if (bind.channelType === 'aun' && isGroup) {
|
|
142
|
+
return resolveAunGroupName(aunGroupNames, bind.groupId || bind.channelId, bind.groupName || null);
|
|
143
|
+
}
|
|
144
|
+
if (isGroup)
|
|
145
|
+
return bind.groupName || null;
|
|
146
|
+
return resolveContactPeerName(resolver, {
|
|
147
|
+
selfAID: bind.selfAID,
|
|
148
|
+
channelType: bind.channelType,
|
|
149
|
+
channel: bind.channelKey || bind.channelType,
|
|
150
|
+
channelId: bind.channelId,
|
|
151
|
+
}, bind.peerId || bind.channelId, bind.peerName);
|
|
152
|
+
}
|
|
153
|
+
/** 为会话页绑定标签补齐联系人名或 AUN 群名,不改变原始 channelId。 */
|
|
154
|
+
export async function hydrateSessionBindDisplayNames(binds) {
|
|
155
|
+
const items = [...binds];
|
|
156
|
+
if (!items.length)
|
|
157
|
+
return;
|
|
158
|
+
const resolver = await getPeerDisplayResolver();
|
|
159
|
+
const groupAids = [...new Set(items
|
|
160
|
+
.filter(bind => bind.channelType === 'aun' && (bind.chatType === 'group' || !!bind.groupId))
|
|
161
|
+
.map(bind => bind.selfAID)
|
|
162
|
+
.filter(Boolean))];
|
|
163
|
+
const groupDirectories = new Map(await Promise.all(groupAids.map(async (aid) => [aid, await getAunGroupNames(aid)])));
|
|
164
|
+
for (const bind of items) {
|
|
165
|
+
bind.displayName = resolveSessionBindDisplayName(bind, resolver, groupDirectories.get(bind.selfAID) || new Map());
|
|
166
|
+
}
|
|
167
|
+
}
|
|
24
168
|
function getSessionAdapter(baseagent) {
|
|
25
169
|
if (!sessionAdapterPromises.has(baseagent)) {
|
|
26
170
|
const classNames = {
|
|
@@ -67,6 +211,9 @@ function readLastJsonlObject(file) {
|
|
|
67
211
|
const rows = readAllJsonlLines(file);
|
|
68
212
|
return rows.length ? rows[rows.length - 1] : null;
|
|
69
213
|
}
|
|
214
|
+
export function latestMessageAt(messages) {
|
|
215
|
+
return messages.reduce((latest, message) => Math.max(latest, Number(message.ts) || 0), 0);
|
|
216
|
+
}
|
|
70
217
|
function loadThreadSessions(dirPath) {
|
|
71
218
|
const threadsDir = path.join(dirPath, '_threads');
|
|
72
219
|
const index = readJsonFile(path.join(threadsDir, 'thread-index.json')) || {};
|
|
@@ -170,6 +317,37 @@ function listConfiguredAgentAids(root) {
|
|
|
170
317
|
}
|
|
171
318
|
return aids;
|
|
172
319
|
}
|
|
320
|
+
function shortAid(value) {
|
|
321
|
+
return String(value || '').split('.')[0] || String(value || '');
|
|
322
|
+
}
|
|
323
|
+
/** Returns local Agent display names, preferring the runtime selfName. */
|
|
324
|
+
export async function getAgentSelfNames() {
|
|
325
|
+
if (agentDisplayNamesCache && agentDisplayNamesCache.expiresAt > Date.now()) {
|
|
326
|
+
return agentDisplayNamesCache.names;
|
|
327
|
+
}
|
|
328
|
+
const names = new Map();
|
|
329
|
+
try {
|
|
330
|
+
const [agentsResp, statsResp] = await Promise.all([
|
|
331
|
+
ipcQuery(resolvePaths().socket, { type: 'evolagent.list' }, 4000),
|
|
332
|
+
ipcQuery(resolvePaths().socket, { type: 'aun-aid-stats' }, 4000),
|
|
333
|
+
]);
|
|
334
|
+
const statsByAid = new Map((statsResp?.stats || []).map(item => [String(item?.aid || ''), item]));
|
|
335
|
+
for (const agent of agentsResp?.agents || []) {
|
|
336
|
+
const aid = String(agent?.aid || '').trim();
|
|
337
|
+
if (!aid)
|
|
338
|
+
continue;
|
|
339
|
+
const stats = statsByAid.get(aid);
|
|
340
|
+
const displayName = String(stats?.selfName || agent?.displayName || shortAid(aid)).trim();
|
|
341
|
+
if (displayName)
|
|
342
|
+
names.set(aid, displayName);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// daemon unavailable: leave the map empty and use the AID fallback below
|
|
347
|
+
}
|
|
348
|
+
agentDisplayNamesCache = { expiresAt: Date.now() + 5000, names };
|
|
349
|
+
return names;
|
|
350
|
+
}
|
|
173
351
|
function decodeDirSegment(seg) {
|
|
174
352
|
return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
175
353
|
}
|
|
@@ -289,7 +467,7 @@ function chatDisplayId(active, messages) {
|
|
|
289
467
|
groupName: null,
|
|
290
468
|
};
|
|
291
469
|
}
|
|
292
|
-
function loadChats() {
|
|
470
|
+
function loadChats(peerDisplayResolver) {
|
|
293
471
|
const paths = resolvePaths();
|
|
294
472
|
const sessionsDir = paths.sessionsDir;
|
|
295
473
|
const configuredAids = listConfiguredAgentAids(paths.root);
|
|
@@ -308,6 +486,9 @@ function loadChats() {
|
|
|
308
486
|
continue;
|
|
309
487
|
const chatType = String(active.chatType || messages.find(m => m.chatType)?.chatType || 'private');
|
|
310
488
|
const display = chatDisplayId({ ...active, chatType, channelId }, messages);
|
|
489
|
+
const peerName = chatType === 'group'
|
|
490
|
+
? display.peerName
|
|
491
|
+
: resolveContactPeerName(peerDisplayResolver, identity, display.peerId, display.peerName);
|
|
311
492
|
const threadSessions = loadThreadSessions(dirPath);
|
|
312
493
|
const threadById = new Map(threadSessions.map(item => [item.threadId, item]));
|
|
313
494
|
const mainSessionIds = loadMainSessionIds(dirPath, active);
|
|
@@ -316,8 +497,9 @@ function loadChats() {
|
|
|
316
497
|
continue;
|
|
317
498
|
const thread = item.threadId ? threadById.get(item.threadId) : undefined;
|
|
318
499
|
const partitionActive = item.partition === 'thread' ? thread?.active || {} : item.partition === 'main' ? active : {};
|
|
319
|
-
|
|
320
|
-
|
|
500
|
+
// active.updatedAt also changes for session metadata maintenance (for example,
|
|
501
|
+
// a project-path migration), so it is not a conversation activity timestamp.
|
|
502
|
+
const updatedAt = latestMessageAt(item.messages);
|
|
321
503
|
chats.push({
|
|
322
504
|
id: safeId(`${selfAID}\n${channelType}\n${channelId}\n${chatType}\n${item.partition}\n${item.threadId || ''}`),
|
|
323
505
|
dirPath,
|
|
@@ -327,7 +509,7 @@ function loadChats() {
|
|
|
327
509
|
selfAID,
|
|
328
510
|
chatType,
|
|
329
511
|
peerId: display.peerId,
|
|
330
|
-
peerName
|
|
512
|
+
peerName,
|
|
331
513
|
groupId: display.groupId,
|
|
332
514
|
groupName: display.groupName,
|
|
333
515
|
partition: item.partition,
|
|
@@ -349,7 +531,7 @@ function scopeId(chat) {
|
|
|
349
531
|
function legacyChannelScopeId(chat) {
|
|
350
532
|
return safeId(`${chat.selfAID}\n${chat.channelType}\n${chat.channel}`);
|
|
351
533
|
}
|
|
352
|
-
function buildScopes(chats) {
|
|
534
|
+
function buildScopes(chats, agentDisplayNames = new Map()) {
|
|
353
535
|
const map = new Map();
|
|
354
536
|
const seenChats = new Map();
|
|
355
537
|
const seenChannels = new Map();
|
|
@@ -362,6 +544,7 @@ function buildScopes(chats) {
|
|
|
362
544
|
id,
|
|
363
545
|
aid: id,
|
|
364
546
|
selfAID: chat.selfAID,
|
|
547
|
+
displayName: agentDisplayNames.get(chat.selfAID) || shortAid(chat.selfAID),
|
|
365
548
|
totalIn: 0,
|
|
366
549
|
totalOut: 0,
|
|
367
550
|
peerCount: 0,
|
|
@@ -562,8 +745,9 @@ async function buildSessionContext(chat) {
|
|
|
562
745
|
};
|
|
563
746
|
}
|
|
564
747
|
async function buildSnapshot(params) {
|
|
565
|
-
const
|
|
566
|
-
const
|
|
748
|
+
const peerDisplayResolver = await getPeerDisplayResolver();
|
|
749
|
+
const chats = loadChats(peerDisplayResolver);
|
|
750
|
+
const scopes = buildScopes(chats, await getAgentSelfNames());
|
|
567
751
|
const requestedScope = params.scope || params.aid || null;
|
|
568
752
|
const scope = requestedScope && !scopes.some(s => s.id === requestedScope)
|
|
569
753
|
? (chats.find(chat => legacyChannelScopeId(chat) === requestedScope)
|
|
@@ -572,6 +756,8 @@ async function buildSnapshot(params) {
|
|
|
572
756
|
: requestedScope;
|
|
573
757
|
const peer = params.peer || null;
|
|
574
758
|
const inScope = selectedChats(chats, scope);
|
|
759
|
+
if (scope)
|
|
760
|
+
await hydrateAunGroupNames(inScope);
|
|
575
761
|
const peers = scope ? mergePeerInfo(inScope) : [];
|
|
576
762
|
const messages = scope ? messagesFor(inScope, peer) : [];
|
|
577
763
|
const currentChat = peer ? inScope.find(chat => chatKey(chat) === peer) : undefined;
|
|
@@ -661,30 +661,8 @@ export const relationRolesSource = {
|
|
|
661
661
|
return { agents: [], conversations: [], error: String(err) };
|
|
662
662
|
}
|
|
663
663
|
},
|
|
664
|
-
subscribe(_params,
|
|
665
|
-
|
|
666
|
-
let stopped = false;
|
|
667
|
-
const tick = async () => {
|
|
668
|
-
if (stopped)
|
|
669
|
-
return;
|
|
670
|
-
try {
|
|
671
|
-
const snap = await buildSnapshot(_params.__menuAuth ?? {});
|
|
672
|
-
const json = JSON.stringify(snap);
|
|
673
|
-
if (json !== lastJson) {
|
|
674
|
-
lastJson = json;
|
|
675
|
-
push(snap);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
catch (err) {
|
|
679
|
-
console.error('[relation-roles] polling error:', err);
|
|
680
|
-
}
|
|
681
|
-
};
|
|
682
|
-
tick();
|
|
683
|
-
const timer = setInterval(tick, 2000);
|
|
684
|
-
return () => {
|
|
685
|
-
stopped = true;
|
|
686
|
-
clearInterval(timer);
|
|
687
|
-
};
|
|
664
|
+
subscribe(_params, _push) {
|
|
665
|
+
return () => { };
|
|
688
666
|
},
|
|
689
667
|
};
|
|
690
668
|
function requireScope(body) {
|