ec-web 0.0.21 → 0.0.22
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/ipc-client.js +7 -1
- package/dist/paths.js +18 -1
- package/dist/server.js +43 -6
- package/dist/sources/monitor.js +22 -5
- package/dist/sources/relation-roles.js +67 -17
- package/dist/sources/role-definitions.js +27 -3
- package/dist/sources/session-codex.js +18 -6
- package/dist/sources/triggers.js +58 -0
- package/dist/static/app.js +92 -6
- package/dist/static/index.html +1 -1
- package/package.json +1 -1
package/dist/ipc-client.js
CHANGED
|
@@ -12,7 +12,13 @@ import path from 'path';
|
|
|
12
12
|
// Keep this list aligned with evolcore's local-control IPC gate. `evolagent.list`
|
|
13
13
|
// is used by the Agents view and is read-only from EC Web's perspective, but the
|
|
14
14
|
// daemon still requires the local control token before returning its registry.
|
|
15
|
-
const LOCAL_CONTROL_COMMANDS = new Set([
|
|
15
|
+
const LOCAL_CONTROL_COMMANDS = new Set([
|
|
16
|
+
'evolagent.list',
|
|
17
|
+
'menu.exec',
|
|
18
|
+
'monitor-snapshot',
|
|
19
|
+
'monitor-subscribe',
|
|
20
|
+
'monitor-unsubscribe',
|
|
21
|
+
]);
|
|
16
22
|
export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
|
|
17
23
|
let request = cmd;
|
|
18
24
|
if (LOCAL_CONTROL_COMMANDS.has(cmd.type) && typeof cmd.controlToken !== 'string') {
|
package/dist/paths.js
CHANGED
|
@@ -25,10 +25,27 @@ export function resolveRoot() {
|
|
|
25
25
|
}
|
|
26
26
|
return _root;
|
|
27
27
|
}
|
|
28
|
+
function stripTrailingWindowsSeparators(value) {
|
|
29
|
+
const stripped = value.replace(/[\\/]+$/g, '');
|
|
30
|
+
// Keep a drive-root separator. `C:` is a drive-relative path on Windows.
|
|
31
|
+
if (/^[a-zA-Z]:$/.test(stripped) && /^[a-zA-Z]:[\\/]+$/.test(value)) {
|
|
32
|
+
return `${stripped}\\`;
|
|
33
|
+
}
|
|
34
|
+
return stripped;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Canonicalize EVOLCORE_HOME exactly like the main EvolCore package does.
|
|
38
|
+
* The CLI and EC Web may receive the same Windows directory with different
|
|
39
|
+
* casing, slash styles, or a trailing separator; all must select one pipe.
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeWindowsSocketRoot(root) {
|
|
42
|
+
return stripTrailingWindowsSeparators(path.win32.resolve(root)).toLowerCase();
|
|
43
|
+
}
|
|
28
44
|
/** socket 路径派生:与 evolcore resolveInstanceSocketPath 完全一致 */
|
|
29
45
|
function resolveInstanceSocketPath(root) {
|
|
30
46
|
if (isWindows) {
|
|
31
|
-
const
|
|
47
|
+
const canonicalRoot = normalizeWindowsSocketRoot(root);
|
|
48
|
+
const hash = crypto.createHash('sha1').update(canonicalRoot).digest('hex').slice(0, 12);
|
|
32
49
|
return `\\\\.\\pipe\\daemon-${hash}`;
|
|
33
50
|
}
|
|
34
51
|
return path.join(root, 'data', 'instance', 'daemon.sock');
|
package/dist/server.js
CHANGED
|
@@ -555,7 +555,30 @@ function handleConnection(ws, req, log, tokenRecord) {
|
|
|
555
555
|
send({ type: 'error', message: `unknown view: ${view}` });
|
|
556
556
|
return;
|
|
557
557
|
}
|
|
558
|
-
const sourceParams = {
|
|
558
|
+
const sourceParams = {
|
|
559
|
+
...params,
|
|
560
|
+
__menuAuth: daemonOwnerMenuAuth(localDirect, actorAid),
|
|
561
|
+
__ecwebRequestId: crypto.randomUUID(),
|
|
562
|
+
__ecwebEndpoint: `WS snapshot:${view}`,
|
|
563
|
+
};
|
|
564
|
+
let deactivate = null;
|
|
565
|
+
if (source.activate) {
|
|
566
|
+
try {
|
|
567
|
+
deactivate = await source.activate(sourceParams);
|
|
568
|
+
}
|
|
569
|
+
catch (e) {
|
|
570
|
+
if (generation === subscriptionGeneration)
|
|
571
|
+
send({ type: 'error', message: `subscription failed: ${e?.message || e}` });
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (generation !== subscriptionGeneration) {
|
|
575
|
+
deactivate();
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
// Install cleanup before awaiting the initial snapshot, so a view switch
|
|
579
|
+
// or socket close releases the monitor lease immediately.
|
|
580
|
+
unsubscribe = deactivate;
|
|
581
|
+
}
|
|
559
582
|
// System 视图:把 ECWeb 自身版本合并进快照(前端版本卡用)
|
|
560
583
|
const decorate = (data) => (view === 'system' && data ? { ...data, ecwebVersion: ECWEB_VERSION } : data);
|
|
561
584
|
let initialSnapshot;
|
|
@@ -570,16 +593,30 @@ function handleConnection(ws, req, log, tokenRecord) {
|
|
|
570
593
|
return;
|
|
571
594
|
send({ type: 'error', message: `snapshot failed: ${e?.message || e}` });
|
|
572
595
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
596
|
+
let nextUnsubscribe;
|
|
597
|
+
try {
|
|
598
|
+
nextUnsubscribe = source.subscribe(sourceParams, (data) => {
|
|
599
|
+
if (generation === subscriptionGeneration && currentView === view) {
|
|
600
|
+
send({ type: 'delta', view, data: decorate(data) });
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
catch (e) {
|
|
605
|
+
deactivate?.();
|
|
606
|
+
if (generation === subscriptionGeneration) {
|
|
607
|
+
unsubscribe = null;
|
|
608
|
+
send({ type: 'error', message: `subscription failed: ${e?.message || e}` });
|
|
576
609
|
}
|
|
577
|
-
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
578
612
|
if (generation !== subscriptionGeneration) {
|
|
579
613
|
nextUnsubscribe();
|
|
580
614
|
return;
|
|
581
615
|
}
|
|
582
|
-
unsubscribe =
|
|
616
|
+
unsubscribe = () => {
|
|
617
|
+
nextUnsubscribe();
|
|
618
|
+
deactivate?.();
|
|
619
|
+
};
|
|
583
620
|
};
|
|
584
621
|
ws.on('message', async (raw) => {
|
|
585
622
|
let msg;
|
package/dist/sources/monitor.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import fs from 'node:fs';
|
|
15
15
|
import { resolvePaths } from '../paths.js';
|
|
16
16
|
import { ipcQuery } from '../ipc-client.js';
|
|
17
|
+
import crypto from 'node:crypto';
|
|
17
18
|
function processGroupKey(process) {
|
|
18
19
|
return `${process.kind}\u0000${process.name}`;
|
|
19
20
|
}
|
|
@@ -192,9 +193,9 @@ function sampleNetworkRate(ts) {
|
|
|
192
193
|
previousNetworkSample = { ...totals, ts };
|
|
193
194
|
return latestNetworkRate;
|
|
194
195
|
}
|
|
195
|
-
async function fetchSnapshot() {
|
|
196
|
+
async function fetchSnapshot(leaseId) {
|
|
196
197
|
const p = resolvePaths();
|
|
197
|
-
const resp = await ipcQuery(p.socket, { type: 'monitor-snapshot' }, 3000);
|
|
198
|
+
const resp = await ipcQuery(p.socket, { type: 'monitor-snapshot', ...(leaseId ? { leaseId } : {}) }, 3000);
|
|
198
199
|
// resp === null → daemon 离线或 socket 不可达;resp.ok === false → 命令不受支持(旧 daemon)
|
|
199
200
|
if (!resp?.ok) {
|
|
200
201
|
return { daemonRunning: resp !== null, snapshot: null, history: { fine: [], mid: [], coarse: [] } };
|
|
@@ -237,8 +238,24 @@ async function fetchSnapshot() {
|
|
|
237
238
|
}
|
|
238
239
|
export const monitorSource = {
|
|
239
240
|
kind: 'monitor',
|
|
240
|
-
async
|
|
241
|
-
|
|
241
|
+
async activate(params) {
|
|
242
|
+
const leaseId = crypto.randomUUID();
|
|
243
|
+
const p = resolvePaths();
|
|
244
|
+
const response = await ipcQuery(p.socket, { type: 'monitor-subscribe', leaseId }, 3000);
|
|
245
|
+
if (!response?.ok)
|
|
246
|
+
throw new Error('daemon rejected monitor subscription');
|
|
247
|
+
params.__monitorLeaseId = leaseId;
|
|
248
|
+
let released = false;
|
|
249
|
+
return () => {
|
|
250
|
+
if (released)
|
|
251
|
+
return;
|
|
252
|
+
released = true;
|
|
253
|
+
delete params.__monitorLeaseId;
|
|
254
|
+
void ipcQuery(p.socket, { type: 'monitor-unsubscribe', leaseId }, 3000);
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
async snapshot(params) {
|
|
258
|
+
return fetchSnapshot(params?.__monitorLeaseId);
|
|
242
259
|
},
|
|
243
260
|
subscribe(_params, push) {
|
|
244
261
|
let lastJson = '';
|
|
@@ -247,7 +264,7 @@ export const monitorSource = {
|
|
|
247
264
|
if (stopped)
|
|
248
265
|
return;
|
|
249
266
|
try {
|
|
250
|
-
const snap = await fetchSnapshot();
|
|
267
|
+
const snap = await fetchSnapshot(_params.__monitorLeaseId);
|
|
251
268
|
const json = JSON.stringify(snap);
|
|
252
269
|
if (json !== lastJson) {
|
|
253
270
|
lastJson = json;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { resolvePaths } from '../paths.js';
|
|
4
|
-
import { getSessionsAunDir,
|
|
4
|
+
import { getSessionsAunDir, isTransientProtocolMessage, readJsonFile, readAllJsonlLines, scanChatDirs, } from '../fs-utils.js';
|
|
5
5
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
function ecwebAuditContext(auth, aid, operation) {
|
|
8
|
+
return {
|
|
9
|
+
ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
|
|
10
|
+
targetAgentAid: aid,
|
|
11
|
+
endpoint: auth.endpoint ?? 'ECWeb relation roles',
|
|
12
|
+
logicalToolName: operation,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
6
15
|
async function getParentModules() {
|
|
7
16
|
const configPath = resolveParentDistModule('config', 'config-manager.js');
|
|
8
17
|
const rolesPath = resolveParentDistModule('config', 'roles.js');
|
|
@@ -116,6 +125,7 @@ async function authorizeRoleAssignmentRead(modules, aid, auth) {
|
|
|
116
125
|
reason: decision.allow ? undefined : decision.reason,
|
|
117
126
|
matchedRule: decision.matchedRule,
|
|
118
127
|
argsSummary: { self: aid },
|
|
128
|
+
...ecwebAuditContext(auth, aid, 'role.relation.read'),
|
|
119
129
|
});
|
|
120
130
|
if (decision.allow)
|
|
121
131
|
return { allow: true, actorRole: actor.actorRole };
|
|
@@ -150,6 +160,7 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
150
160
|
code: 'IMMUTABLE_STATIC_OWNER',
|
|
151
161
|
reason,
|
|
152
162
|
argsSummary: { ...args, targetRole },
|
|
163
|
+
...ecwebAuditContext(auth, aid, operation),
|
|
153
164
|
});
|
|
154
165
|
return {
|
|
155
166
|
allow: false,
|
|
@@ -194,6 +205,7 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
194
205
|
reason: decision.reason,
|
|
195
206
|
matchedRule: decision.matchedRule,
|
|
196
207
|
argsSummary: { ...args, targetRole },
|
|
208
|
+
...ecwebAuditContext(auth, aid, operation),
|
|
197
209
|
});
|
|
198
210
|
return {
|
|
199
211
|
allow: false,
|
|
@@ -227,6 +239,7 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
227
239
|
reason,
|
|
228
240
|
matchedRule: decision.matchedRule,
|
|
229
241
|
argsSummary: { ...args, targetRole },
|
|
242
|
+
...ecwebAuditContext(auth, aid, operation),
|
|
230
243
|
});
|
|
231
244
|
return {
|
|
232
245
|
allow: false,
|
|
@@ -251,6 +264,7 @@ async function authorizeRoleAssignmentWrite(modules, aid, auth, operation, targe
|
|
|
251
264
|
decision: 'allow',
|
|
252
265
|
matchedRule: decision.matchedRule,
|
|
253
266
|
argsSummary: { ...args, targetRole },
|
|
267
|
+
...ecwebAuditContext(auth, aid, operation),
|
|
254
268
|
});
|
|
255
269
|
return { allow: true, actorRole };
|
|
256
270
|
}
|
|
@@ -323,12 +337,6 @@ function relationTargetFromId(raw) {
|
|
|
323
337
|
}
|
|
324
338
|
return { peerKey: formatPeerKey('aun', value), channelId: value };
|
|
325
339
|
}
|
|
326
|
-
function readActive(aunDir, aid, conversationId) {
|
|
327
|
-
return readJsonFile(path.join(aunDir, encodeSegmentForRoleSource(aid), encodeSegmentForRoleSource(conversationId), 'active.json'));
|
|
328
|
-
}
|
|
329
|
-
function encodeSegmentForRoleSource(s) {
|
|
330
|
-
return s.replace(/[<>:"/\\|?*\x00-\x1F%]/g, ch => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
|
|
331
|
-
}
|
|
332
340
|
function frontmatterValue(frontmatter, key) {
|
|
333
341
|
const re = new RegExp(`^${key}:\\s*(?:"([^"]*)"|'([^']*)'|([^\\r\\n#]*))\\s*(?:#.*)?$`, 'm');
|
|
334
342
|
const match = frontmatter.match(re);
|
|
@@ -380,6 +388,20 @@ function createLocalAgentMdLookup() {
|
|
|
380
388
|
return info;
|
|
381
389
|
};
|
|
382
390
|
}
|
|
391
|
+
function readPeerVerificationStatus(selfAid, peerId) {
|
|
392
|
+
if (!selfAid || !peerId)
|
|
393
|
+
return 'unknown';
|
|
394
|
+
const peerKey = formatPeerKey('aun', peerId);
|
|
395
|
+
const identity = readJsonFile(path.join(resolvePaths().root, 'agents', selfAid, 'relations', peerKey, 'peer-identity.json'));
|
|
396
|
+
const raw = String(identity?.source ?? identity?.verifyStatus ?? identity?.verify_status ?? '').trim().toLowerCase();
|
|
397
|
+
if (raw === 'agentmd' || raw === 'verified')
|
|
398
|
+
return 'agentmd';
|
|
399
|
+
if (raw === 'agentmd-unverified' || raw === 'unverified')
|
|
400
|
+
return 'agentmd-unverified';
|
|
401
|
+
if (raw === 'invalid')
|
|
402
|
+
return 'invalid';
|
|
403
|
+
return 'unknown';
|
|
404
|
+
}
|
|
383
405
|
function latestMessageAt(messages) {
|
|
384
406
|
let lastAt = 0;
|
|
385
407
|
for (const message of messages) {
|
|
@@ -418,7 +440,7 @@ function firstMessagePeerType(messages, peerId) {
|
|
|
418
440
|
function conversationSortName(seed) {
|
|
419
441
|
return (seed.name || seed.peerAid || seed.groupId || seed.conversationId).toLowerCase();
|
|
420
442
|
}
|
|
421
|
-
function groupMembersFromMessages(selfAid, messages, localAgentMdLookup) {
|
|
443
|
+
function groupMembersFromMessages(selfAid, messages, localAgentMdLookup, peerVerificationLookup) {
|
|
422
444
|
const members = new Map();
|
|
423
445
|
for (const message of messages) {
|
|
424
446
|
const sender = message.from;
|
|
@@ -448,6 +470,7 @@ function groupMembersFromMessages(selfAid, messages, localAgentMdLookup) {
|
|
|
448
470
|
const localInfo = localAgentMdLookup(member.peerId);
|
|
449
471
|
member.peerName = localInfo?.name ?? member.peerName;
|
|
450
472
|
member.peerType = localInfo?.peerType ?? member.peerType;
|
|
473
|
+
member.verifyStatus = peerVerificationLookup(selfAid, member.peerId);
|
|
451
474
|
}
|
|
452
475
|
return [...members.values()].sort((a, b) => {
|
|
453
476
|
const lastDiff = (b.lastAt ?? 0) - (a.lastAt ?? 0);
|
|
@@ -456,7 +479,7 @@ function groupMembersFromMessages(selfAid, messages, localAgentMdLookup) {
|
|
|
456
479
|
return (a.peerName || a.peerAid).localeCompare(b.peerName || b.peerAid);
|
|
457
480
|
});
|
|
458
481
|
}
|
|
459
|
-
export function buildConversationSeeds(aid, peerInfos, localAgentMdLookup = () => undefined) {
|
|
482
|
+
export function buildConversationSeeds(aid, peerInfos, localAgentMdLookup = () => undefined, peerVerificationLookup = readPeerVerificationStatus) {
|
|
460
483
|
const seeds = [];
|
|
461
484
|
for (const peer of peerInfos) {
|
|
462
485
|
const messages = peer.messages || [];
|
|
@@ -478,7 +501,7 @@ export function buildConversationSeeds(aid, peerInfos, localAgentMdLookup = () =
|
|
|
478
501
|
lastAt,
|
|
479
502
|
inbound: counts.inbound,
|
|
480
503
|
outbound: counts.outbound,
|
|
481
|
-
members: groupMembersFromMessages(aid, messages, localAgentMdLookup),
|
|
504
|
+
members: groupMembersFromMessages(aid, messages, localAgentMdLookup, peerVerificationLookup),
|
|
482
505
|
});
|
|
483
506
|
continue;
|
|
484
507
|
}
|
|
@@ -499,6 +522,7 @@ export function buildConversationSeeds(aid, peerInfos, localAgentMdLookup = () =
|
|
|
499
522
|
peerKey: peerId,
|
|
500
523
|
peerName,
|
|
501
524
|
peerType,
|
|
525
|
+
verifyStatus: peerVerificationLookup(aid, peerId),
|
|
502
526
|
lastAt,
|
|
503
527
|
inbound: counts.inbound,
|
|
504
528
|
outbound: counts.outbound,
|
|
@@ -515,13 +539,20 @@ export function buildConversationSeeds(aid, peerInfos, localAgentMdLookup = () =
|
|
|
515
539
|
}
|
|
516
540
|
function loadConversationSeeds(aid) {
|
|
517
541
|
const aunDir = getSessionsAunDir();
|
|
542
|
+
const peerInfoById = new Map();
|
|
543
|
+
for (const chat of scanChatDirs(aunDir).filter(item => item.channelType === 'aun' && item.selfAID === aid)) {
|
|
544
|
+
const current = peerInfoById.get(chat.channelId) ?? { conversationId: chat.channelId, messages: [] };
|
|
545
|
+
const active = readJsonFile(path.join(chat.dirPath, 'active.json'));
|
|
546
|
+
if (active && (!current.active || Number(active.updatedAt || 0) >= Number(current.active.updatedAt || 0))) {
|
|
547
|
+
current.active = active;
|
|
548
|
+
}
|
|
549
|
+
current.messages.push(...readAllJsonlLines(path.join(chat.dirPath, 'messages.jsonl'))
|
|
550
|
+
.filter(message => !isTransientProtocolMessage(message)));
|
|
551
|
+
peerInfoById.set(chat.channelId, current);
|
|
552
|
+
}
|
|
518
553
|
const localAgentMdLookup = createLocalAgentMdLookup();
|
|
519
|
-
const peerInfos =
|
|
520
|
-
|
|
521
|
-
active: readActive(aunDir, aid, conversationId),
|
|
522
|
-
messages: readMessages(aunDir, aid, conversationId),
|
|
523
|
-
}));
|
|
524
|
-
return buildConversationSeeds(aid, peerInfos, localAgentMdLookup);
|
|
554
|
+
const peerInfos = [...peerInfoById.values()];
|
|
555
|
+
return buildConversationSeeds(aid, peerInfos, localAgentMdLookup, readPeerVerificationStatus);
|
|
525
556
|
}
|
|
526
557
|
function readRelationConfig(config, aid, peerKey) {
|
|
527
558
|
return config.read(config.ConfigTarget.Relation, { self: aid, peerKey }, { cache: true }) || {};
|
|
@@ -648,6 +679,11 @@ function listRelationRoleAssignments(config, aid) {
|
|
|
648
679
|
return out;
|
|
649
680
|
}
|
|
650
681
|
async function buildSnapshot(auth = {}) {
|
|
682
|
+
auth = {
|
|
683
|
+
...auth,
|
|
684
|
+
ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
|
|
685
|
+
endpoint: auth.endpoint ?? 'WS snapshot:roles',
|
|
686
|
+
};
|
|
651
687
|
const modules = await getParentModules();
|
|
652
688
|
const agents = await getAgentsFromIpc();
|
|
653
689
|
const snapshotAgents = [];
|
|
@@ -678,7 +714,11 @@ export const relationRolesSource = {
|
|
|
678
714
|
kind: 'roles',
|
|
679
715
|
async snapshot(params = {}) {
|
|
680
716
|
try {
|
|
681
|
-
return await buildSnapshot(
|
|
717
|
+
return await buildSnapshot({
|
|
718
|
+
...(params.__menuAuth ?? {}),
|
|
719
|
+
ecwebRequestId: params.__ecwebRequestId,
|
|
720
|
+
endpoint: params.__ecwebEndpoint,
|
|
721
|
+
});
|
|
682
722
|
}
|
|
683
723
|
catch (err) {
|
|
684
724
|
return { agents: [], conversations: [], error: String(err) };
|
|
@@ -765,6 +805,11 @@ function deleteScopedAssignment(configMod, aid, body, fallbackTargetId) {
|
|
|
765
805
|
export async function handleRelationRolesApi(req, res, auth = {}) {
|
|
766
806
|
try {
|
|
767
807
|
const urlPath = (req.url || '').split('?')[0];
|
|
808
|
+
auth = {
|
|
809
|
+
...auth,
|
|
810
|
+
ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
|
|
811
|
+
endpoint: auth.endpoint ?? `${req.method ?? 'UNKNOWN'} ${urlPath}`,
|
|
812
|
+
};
|
|
768
813
|
const modules = await getParentModules();
|
|
769
814
|
if (req.method === 'GET' && urlPath.startsWith('/api/roles/agent/')) {
|
|
770
815
|
const aid = decodeURIComponent(urlPath.split('/').filter(Boolean).pop() || '');
|
|
@@ -802,6 +847,11 @@ export async function handleRelationRolesApi(req, res, auth = {}) {
|
|
|
802
847
|
export async function handlePeerRoleApi(req, res, auth = {}) {
|
|
803
848
|
try {
|
|
804
849
|
const urlPath = (req.url || '').split('?')[0];
|
|
850
|
+
auth = {
|
|
851
|
+
...auth,
|
|
852
|
+
ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
|
|
853
|
+
endpoint: auth.endpoint ?? `${req.method ?? 'UNKNOWN'} ${urlPath}`,
|
|
854
|
+
};
|
|
805
855
|
const target = assignmentFromPath(urlPath);
|
|
806
856
|
if (!target)
|
|
807
857
|
return sendJson(res, 400, { error: 'missing aid or target id' });
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { getModelCatalogSnapshot } from './models.js';
|
|
15
15
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
16
16
|
import { resolvePaths } from '../paths.js';
|
|
17
|
+
import crypto from 'node:crypto';
|
|
17
18
|
// 动态导入 evolcore 主项目的角色配置模块
|
|
18
19
|
async function getRolesModule() {
|
|
19
20
|
const rolesPath = resolveParentDistModule('config', 'roles.js');
|
|
@@ -82,6 +83,14 @@ async function getOperationRegistryModule() {
|
|
|
82
83
|
listOperations: mod.listOperations,
|
|
83
84
|
};
|
|
84
85
|
}
|
|
86
|
+
function ecwebAuditContext(auth, aid, operation) {
|
|
87
|
+
return {
|
|
88
|
+
ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
|
|
89
|
+
targetAgentAid: aid,
|
|
90
|
+
endpoint: auth.endpoint ?? 'ECWeb role policy',
|
|
91
|
+
logicalToolName: operation,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
85
94
|
export function visibleRoleDefinitionOptions(roleOptions) {
|
|
86
95
|
return roleOptions
|
|
87
96
|
.filter(item => item?.kind === 'user' || item?.value === 'admin')
|
|
@@ -142,6 +151,7 @@ async function authorizeRolePolicyOperation(modules, processConfig, auth, aid, o
|
|
|
142
151
|
...args,
|
|
143
152
|
},
|
|
144
153
|
},
|
|
154
|
+
auditMetadata: ecwebAuditContext(auth, aid, operation),
|
|
145
155
|
});
|
|
146
156
|
}
|
|
147
157
|
function isDaemonOwnerAuth(processConfig, auth) {
|
|
@@ -574,6 +584,11 @@ async function buildModelPermissionResponse(roleName, roleDef, override) {
|
|
|
574
584
|
}
|
|
575
585
|
async function buildSnapshot(params = {}, auth = {}) {
|
|
576
586
|
try {
|
|
587
|
+
auth = {
|
|
588
|
+
...auth,
|
|
589
|
+
ecwebRequestId: params.__ecwebRequestId ?? auth.ecwebRequestId ?? crypto.randomUUID(),
|
|
590
|
+
endpoint: params.__ecwebEndpoint ?? auth.endpoint ?? 'WS snapshot:roleDefinitions',
|
|
591
|
+
};
|
|
577
592
|
const agents = await getAgentsFromIpc();
|
|
578
593
|
const aid = resolveRequestedAid(params.aid, agents);
|
|
579
594
|
if (!aid)
|
|
@@ -614,7 +629,11 @@ async function buildSnapshot(params = {}, auth = {}) {
|
|
|
614
629
|
export const roleDefinitionsSource = {
|
|
615
630
|
kind: 'roleDefinitions',
|
|
616
631
|
async snapshot(params = {}) {
|
|
617
|
-
return buildSnapshot(params,
|
|
632
|
+
return buildSnapshot(params, {
|
|
633
|
+
...(params.__menuAuth ?? {}),
|
|
634
|
+
ecwebRequestId: params.__ecwebRequestId,
|
|
635
|
+
endpoint: params.__ecwebEndpoint,
|
|
636
|
+
});
|
|
618
637
|
},
|
|
619
638
|
subscribe(_params, _push) {
|
|
620
639
|
return () => { };
|
|
@@ -625,6 +644,11 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
625
644
|
try {
|
|
626
645
|
const parsedUrl = parseUrl(req.url || '');
|
|
627
646
|
const urlPath = parsedUrl.path;
|
|
647
|
+
auth = {
|
|
648
|
+
...auth,
|
|
649
|
+
ecwebRequestId: auth.ecwebRequestId ?? crypto.randomUUID(),
|
|
650
|
+
endpoint: auth.endpoint ?? `${req.method ?? 'UNKNOWN'} ${urlPath}`,
|
|
651
|
+
};
|
|
628
652
|
const agents = await getAgentsFromIpc();
|
|
629
653
|
const aid = resolveRequestedAid(parsedUrl.query.aid, agents);
|
|
630
654
|
const modules = await getRolesModule();
|
|
@@ -693,7 +717,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
693
717
|
}
|
|
694
718
|
}
|
|
695
719
|
if (req.method === 'GET' && urlPath === '/api/role-definitions') {
|
|
696
|
-
sendJson(res, 200, await buildSnapshot({ aid }, auth));
|
|
720
|
+
sendJson(res, 200, await buildSnapshot({ aid, __ecwebRequestId: auth.ecwebRequestId, __ecwebEndpoint: auth.endpoint }, auth));
|
|
697
721
|
return;
|
|
698
722
|
}
|
|
699
723
|
if (req.method === 'PUT' && urlPath === '/api/role-definitions') {
|
|
@@ -755,7 +779,7 @@ export async function handleRoleDefinitionsApi(req, res, auth = {}) {
|
|
|
755
779
|
}
|
|
756
780
|
writeRoleRegistry(modules, aid, nextDefaults);
|
|
757
781
|
}
|
|
758
|
-
sendJson(res, 200, { ok: true, data: await buildSnapshot({ aid }, auth) });
|
|
782
|
+
sendJson(res, 200, { ok: true, data: await buildSnapshot({ aid, __ecwebRequestId: auth.ecwebRequestId, __ecwebEndpoint: auth.endpoint }, auth) });
|
|
759
783
|
}
|
|
760
784
|
catch (err) {
|
|
761
785
|
console.error('[role-definitions] Failed to update agent role config:', err);
|
|
@@ -581,20 +581,32 @@ function approvalAuditFromPayload(payload) {
|
|
|
581
581
|
}
|
|
582
582
|
function classifyCodexToolRejection(text, payload) {
|
|
583
583
|
const normalized = text.replace(/\\(["'])/g, '$1');
|
|
584
|
-
|
|
584
|
+
const hookMatch = normalized.match(/(?:Command|Tool call) blocked by PreToolUse hook:\s*(.+?)(?:\.\s+(?:Command|Tool):|$)/is);
|
|
585
|
+
const genericRejected = /Rejected\(\s*["']rejected by user["']\s*\)/i.test(normalized);
|
|
586
|
+
if (!hookMatch && !genericRejected)
|
|
585
587
|
return undefined;
|
|
586
588
|
const audit = approvalAuditFromPayload(payload);
|
|
587
|
-
const source = audit.policyCode || audit.decisionSource?.toLowerCase().includes('policy')
|
|
589
|
+
const source = hookMatch || audit.policyCode || audit.decisionSource?.toLowerCase().includes('policy')
|
|
588
590
|
? 'evolcore-policy'
|
|
589
591
|
: 'codex-approval';
|
|
592
|
+
const backendReason = hookMatch?.[1]?.trim()
|
|
593
|
+
|| 'Rejected("rejected by user")';
|
|
594
|
+
const inferredPolicyCode = audit.policyCode
|
|
595
|
+
|| (hookMatch && /工具调用已达到\s*\d+\s*次/.test(backendReason)
|
|
596
|
+
? 'proactive_tool_report_required'
|
|
597
|
+
: hookMatch && /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(backendReason)
|
|
598
|
+
? 'proactive_first_send_required'
|
|
599
|
+
: undefined);
|
|
590
600
|
return {
|
|
591
|
-
kind: 'approval_rejected',
|
|
601
|
+
kind: source === 'evolcore-policy' ? 'policy_blocked' : 'approval_rejected',
|
|
592
602
|
source,
|
|
593
|
-
reason: 'rejected_by_user',
|
|
603
|
+
reason: source === 'evolcore-policy' ? 'policy_denied' : 'rejected_by_user',
|
|
594
604
|
executed: false,
|
|
595
605
|
evidence: audit.available ? 'structured_metadata' : 'backend_text',
|
|
596
|
-
backendReason
|
|
597
|
-
audit
|
|
606
|
+
backendReason,
|
|
607
|
+
audit: inferredPolicyCode && !audit.policyCode
|
|
608
|
+
? { ...audit, policyCode: inferredPolicyCode }
|
|
609
|
+
: audit,
|
|
598
610
|
};
|
|
599
611
|
}
|
|
600
612
|
export function parseCodexToolEvent(event) {
|
package/dist/sources/triggers.js
CHANGED
|
@@ -60,6 +60,63 @@ function normalizeAgent(item) {
|
|
|
60
60
|
baseagent: typeof item?.baseagent === 'string' ? item.baseagent : undefined,
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
function channelTypeFromKey(channelKey) {
|
|
64
|
+
const value = typeof channelKey === 'string' ? channelKey.trim() : '';
|
|
65
|
+
if (!value)
|
|
66
|
+
return '';
|
|
67
|
+
const separator = value.indexOf('#');
|
|
68
|
+
return separator > 0 ? value.slice(0, separator) : value;
|
|
69
|
+
}
|
|
70
|
+
function triggerScheduleView(definition, fallback) {
|
|
71
|
+
const source = definition?.source ?? fallback?.source;
|
|
72
|
+
if (source && typeof source === 'object') {
|
|
73
|
+
switch (source.type) {
|
|
74
|
+
case 'once': return { scheduleType: 'once', scheduleValue: '' };
|
|
75
|
+
case 'delay': return { scheduleType: 'delay', scheduleValue: String(source.afterMs ?? '') };
|
|
76
|
+
case 'at': return { scheduleType: 'at', scheduleValue: String(source.at ?? '') };
|
|
77
|
+
case 'cron': return { scheduleType: 'cron', scheduleValue: String(source.expression ?? '') };
|
|
78
|
+
case 'interval': return { scheduleType: 'interval', scheduleValue: String(source.everyMs ?? '') };
|
|
79
|
+
case 'event': return { scheduleType: 'event', scheduleValue: String(source.eventPattern ?? '') };
|
|
80
|
+
default: break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
scheduleType: String(fallback?.scheduleType ?? ''),
|
|
85
|
+
scheduleValue: String(fallback?.scheduleValue ?? ''),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function triggerDisplayFields(definition, fallback) {
|
|
89
|
+
const feedback = definition?.feedback ?? fallback?.feedback;
|
|
90
|
+
const origin = definition?.origin;
|
|
91
|
+
const target = feedback?.strategy === 'target'
|
|
92
|
+
? (feedback.target ?? fallback?.feedback?.target)
|
|
93
|
+
: feedback?.strategy === 'origin'
|
|
94
|
+
? {
|
|
95
|
+
channelKey: origin?.channelKey,
|
|
96
|
+
channelId: origin?.channelId ?? origin?.peerId,
|
|
97
|
+
channelType: origin?.channelType,
|
|
98
|
+
}
|
|
99
|
+
: null;
|
|
100
|
+
const schedule = triggerScheduleView(definition, fallback);
|
|
101
|
+
const targetChannel = target?.channelKey ?? fallback?.targetChannel ?? '';
|
|
102
|
+
const targetChannelType = target?.channelType
|
|
103
|
+
?? fallback?.targetChannelType
|
|
104
|
+
?? (feedback?.strategy === 'target'
|
|
105
|
+
? channelTypeFromKey(targetChannel)
|
|
106
|
+
: (origin?.channelType ?? channelTypeFromKey(targetChannel)));
|
|
107
|
+
return {
|
|
108
|
+
scheduleType: schedule.scheduleType,
|
|
109
|
+
scheduleValue: schedule.scheduleValue,
|
|
110
|
+
targetChannel,
|
|
111
|
+
targetChannelName: targetChannel || fallback?.targetChannelName || '',
|
|
112
|
+
targetChannelId: target?.channelId ?? fallback?.targetChannelId ?? '',
|
|
113
|
+
targetChannelType,
|
|
114
|
+
createdByPeerId: origin?.peerId ?? fallback?.createdByPeerId ?? '',
|
|
115
|
+
createdByChannel: origin?.channelKey ?? fallback?.createdByChannel ?? '',
|
|
116
|
+
createdAt: definition?.createdAt ?? fallback?.createdAt,
|
|
117
|
+
updatedAt: definition?.updatedAt ?? fallback?.updatedAt,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
63
120
|
async function listTriggersForAgent(agentAid, agentLabel) {
|
|
64
121
|
const p = resolvePaths();
|
|
65
122
|
const controlToken = readTriggerControlToken(p.instanceDir);
|
|
@@ -106,6 +163,7 @@ async function listTriggersForAgent(agentAid, agentLabel) {
|
|
|
106
163
|
revision,
|
|
107
164
|
definitionError,
|
|
108
165
|
subscription,
|
|
166
|
+
...triggerDisplayFields(definition, trigger),
|
|
109
167
|
};
|
|
110
168
|
}));
|
|
111
169
|
}
|
package/dist/static/app.js
CHANGED
|
@@ -2256,6 +2256,12 @@ function subscribeRoleManagementSubview() {
|
|
|
2256
2256
|
function setRoleManagementSubview(subview, options = {}) {
|
|
2257
2257
|
roleManagementSubview = subview === 'assignments' ? 'assignments' : 'definitions';
|
|
2258
2258
|
localStorage.setItem(ROLE_SUBVIEW_KEY, roleManagementSubview);
|
|
2259
|
+
const newRoleButton = $('#btn-new-role');
|
|
2260
|
+
if (newRoleButton) {
|
|
2261
|
+
const visible = roleManagementSubview === 'definitions';
|
|
2262
|
+
newRoleButton.hidden = !visible;
|
|
2263
|
+
newRoleButton.setAttribute('aria-hidden', String(!visible));
|
|
2264
|
+
}
|
|
2259
2265
|
document.querySelectorAll('[data-role-subview]').forEach(tab => {
|
|
2260
2266
|
const active = tab.dataset.roleSubview === roleManagementSubview;
|
|
2261
2267
|
tab.classList.toggle('active', active);
|
|
@@ -3815,7 +3821,7 @@ function renderBlocks(blocks, options = {}) {
|
|
|
3815
3821
|
: '';
|
|
3816
3822
|
out += `<details class="blk blk-tool" open><summary class="tool-head">${icon} <span class="tool-name">${esc(tool)}</span></summary>${toolParams}</details>`;
|
|
3817
3823
|
} else if (b.kind === 'tool_result') {
|
|
3818
|
-
const rejection = b.rejection
|
|
3824
|
+
const rejection = b.rejection && (b.rejection.kind === 'approval_rejected' || b.rejection.kind === 'policy_blocked') ? b.rejection : undefined;
|
|
3819
3825
|
const cls = b.isError ? `blk-result err${rejection ? ' rejection' : ''}` : 'blk-result';
|
|
3820
3826
|
const loadFull = b.truncated && b.resultKey
|
|
3821
3827
|
? `<button class="ctrl-btn result-load-full" type="button" data-result-key="${esc(b.resultKey)}">${t('sessions.loadFullResult') || '加载完整结果'}</button>`
|
|
@@ -3839,7 +3845,7 @@ function renderBlocks(blocks, options = {}) {
|
|
|
3839
3845
|
: '';
|
|
3840
3846
|
return '<div class="result-rejection-meta">' +
|
|
3841
3847
|
`<span class="result-rejection-badge">⚠ ${t('sessions.block.rejection.title')}</span>` +
|
|
3842
|
-
`<span>${rejection.source === 'evolcore-policy' ? t('sessions.block.rejection.source.policy') : t('sessions.block.rejection.source.approval')}</span>` +
|
|
3848
|
+
`<span>${rejection.kind === 'policy_blocked' || rejection.source === 'evolcore-policy' ? t('sessions.block.rejection.source.policy') : t('sessions.block.rejection.source.approval')}</span>` +
|
|
3843
3849
|
`<span>${t('sessions.block.rejection.notExecuted')}</span>` +
|
|
3844
3850
|
`<span>${t(`sessions.block.rejection.evidence.${evidenceKey}`)}</span>` +
|
|
3845
3851
|
backendReason + auditText +
|
|
@@ -9092,11 +9098,39 @@ function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
|
|
|
9092
9098
|
}
|
|
9093
9099
|
|
|
9094
9100
|
function renderRoles(data) {
|
|
9095
|
-
if (!data) return;
|
|
9096
|
-
|
|
9097
9101
|
const agentSelect = $('#roles-agent-select');
|
|
9102
|
+
const tbody = document.querySelector('#relations-table tbody');
|
|
9103
|
+
if (!data) {
|
|
9104
|
+
if (tbody) {
|
|
9105
|
+
tbody.innerHTML = '';
|
|
9106
|
+
const row = tbody.insertRow();
|
|
9107
|
+
const cell = row.insertCell();
|
|
9108
|
+
cell.colSpan = 7;
|
|
9109
|
+
cell.textContent = t('common.loading');
|
|
9110
|
+
cell.style.textAlign = 'center';
|
|
9111
|
+
cell.style.color = 'var(--dim)';
|
|
9112
|
+
cell.style.padding = '40px';
|
|
9113
|
+
}
|
|
9114
|
+
return;
|
|
9115
|
+
}
|
|
9098
9116
|
if (!agentSelect) return;
|
|
9099
9117
|
|
|
9118
|
+
if (data.error) {
|
|
9119
|
+
agentSelect.innerHTML = `<option value="">${esc(t('roles.selectAgentPlaceholder'))}</option>`;
|
|
9120
|
+
rolesCurrentAgent = null;
|
|
9121
|
+
if (tbody) {
|
|
9122
|
+
tbody.innerHTML = '';
|
|
9123
|
+
const row = tbody.insertRow();
|
|
9124
|
+
const cell = row.insertCell();
|
|
9125
|
+
cell.colSpan = 7;
|
|
9126
|
+
cell.textContent = `${t('common.error')}: ${String(data.error)}`;
|
|
9127
|
+
cell.style.textAlign = 'center';
|
|
9128
|
+
cell.style.color = 'var(--red)';
|
|
9129
|
+
cell.style.padding = '40px';
|
|
9130
|
+
}
|
|
9131
|
+
return;
|
|
9132
|
+
}
|
|
9133
|
+
|
|
9100
9134
|
agentSelect.innerHTML = '';
|
|
9101
9135
|
const placeholderOption = document.createElement('option');
|
|
9102
9136
|
placeholderOption.value = '';
|
|
@@ -9152,6 +9186,34 @@ function peerTypeText(peerType) {
|
|
|
9152
9186
|
return '-';
|
|
9153
9187
|
}
|
|
9154
9188
|
|
|
9189
|
+
function roleVerificationStatus(item) {
|
|
9190
|
+
const status = String(item?.verifyStatus || 'unknown').toLowerCase();
|
|
9191
|
+
if (status === 'agentmd' || status === 'verified') return 'verified';
|
|
9192
|
+
if (status === 'agentmd-unverified' || status === 'unverified') return 'agentmd-unverified';
|
|
9193
|
+
if (status === 'invalid') return 'invalid';
|
|
9194
|
+
return 'unknown';
|
|
9195
|
+
}
|
|
9196
|
+
|
|
9197
|
+
function roleVerificationMatches(item, filter) {
|
|
9198
|
+
if (!filter) return true;
|
|
9199
|
+
const status = roleVerificationStatus(item);
|
|
9200
|
+
return filter === 'verified' ? status === 'verified' : status === filter;
|
|
9201
|
+
}
|
|
9202
|
+
|
|
9203
|
+
function roleVerificationBadge(item) {
|
|
9204
|
+
const status = roleVerificationStatus(item);
|
|
9205
|
+
if (status === 'invalid') {
|
|
9206
|
+
return ` <span class="verify-badge verify-invalid" title="${esc(t('roles.verify.invalid'))}">⚠️ ${esc(t('roles.verify.invalidShort'))}</span>`;
|
|
9207
|
+
}
|
|
9208
|
+
if (status === 'agentmd-unverified') {
|
|
9209
|
+
return ` <span class="verify-badge verify-unverified" title="${esc(t('roles.verify.unverified'))}">⚠️</span>`;
|
|
9210
|
+
}
|
|
9211
|
+
if (status === 'unknown') {
|
|
9212
|
+
return ` <span class="verify-badge verify-unknown" title="${esc(t('roles.verify.unknown'))}">❓</span>`;
|
|
9213
|
+
}
|
|
9214
|
+
return '';
|
|
9215
|
+
}
|
|
9216
|
+
|
|
9155
9217
|
function roleClass(role) {
|
|
9156
9218
|
return String(role || 'none').replace(/[^a-z0-9_-]/gi, '-');
|
|
9157
9219
|
}
|
|
@@ -9188,6 +9250,16 @@ function renderRelationsTable(data, filterAid = null) {
|
|
|
9188
9250
|
if (!tbody) return;
|
|
9189
9251
|
|
|
9190
9252
|
tbody.innerHTML = '';
|
|
9253
|
+
if (data?.error) {
|
|
9254
|
+
const row = tbody.insertRow();
|
|
9255
|
+
const cell = row.insertCell();
|
|
9256
|
+
cell.colSpan = 7;
|
|
9257
|
+
cell.textContent = `${t('common.error')}: ${String(data.error)}`;
|
|
9258
|
+
cell.style.textAlign = 'center';
|
|
9259
|
+
cell.style.color = 'var(--red)';
|
|
9260
|
+
cell.style.padding = '40px';
|
|
9261
|
+
return;
|
|
9262
|
+
}
|
|
9191
9263
|
let conversations = data.conversations || [];
|
|
9192
9264
|
|
|
9193
9265
|
if (filterAid) {
|
|
@@ -9215,6 +9287,16 @@ function renderRelationsTable(data, filterAid = null) {
|
|
|
9215
9287
|
conversations = conversations.filter(conv => conv.chatType === rolesChatTypeFilter);
|
|
9216
9288
|
}
|
|
9217
9289
|
|
|
9290
|
+
if (rolesVerifyFilter) {
|
|
9291
|
+
conversations = conversations
|
|
9292
|
+
.map(conv => conv.chatType === 'group'
|
|
9293
|
+
? { ...conv, members: (conv.members || []).filter(member => roleVerificationMatches(member, rolesVerifyFilter)) }
|
|
9294
|
+
: conv)
|
|
9295
|
+
.filter(conv => conv.chatType === 'group'
|
|
9296
|
+
? (conv.members || []).length > 0
|
|
9297
|
+
: roleVerificationMatches(conv, rolesVerifyFilter));
|
|
9298
|
+
}
|
|
9299
|
+
|
|
9218
9300
|
conversations.sort((a, b) => {
|
|
9219
9301
|
const lastDiff = (b.lastAt || 0) - (a.lastAt || 0);
|
|
9220
9302
|
if (lastDiff !== 0) return lastDiff;
|
|
@@ -9244,10 +9326,12 @@ function renderPrivateConversationRow(tbody, conv) {
|
|
|
9244
9326
|
const peerId = conv.peerId || conv.peerAid || conv.peerKey || conv.conversationId;
|
|
9245
9327
|
const name = conv.peerName || conv.name || shortAid(peerId);
|
|
9246
9328
|
const assignedRole = assignedRoleFor('private', conv);
|
|
9329
|
+
const verifyBadge = roleVerificationBadge(conv);
|
|
9330
|
+
const verifyClass = roleVerificationStatus(conv) === 'verified' ? '' : ` peer-${roleVerificationStatus(conv) === 'unknown' ? 'unknown' : 'unverified'}`;
|
|
9247
9331
|
const row = tbody.insertRow();
|
|
9248
9332
|
row.innerHTML = `
|
|
9249
9333
|
<td><span class="chat-tag chat-tag-private">${esc(t('roles.chatType.private') || 'Private')}</span></td>
|
|
9250
|
-
<td><strong>${esc(name)}</strong
|
|
9334
|
+
<td class="${verifyClass.trim()}"><strong>${esc(name)}</strong>${verifyBadge}</td>
|
|
9251
9335
|
<td><code>${esc(peerId)}</code></td>
|
|
9252
9336
|
<td>${esc(peerTypeText(conv.peerType))}</td>
|
|
9253
9337
|
<td><span class="role-badge role-${esc(roleClass(conv.role))}">${esc(displayRole(conv.role))}</span></td>
|
|
@@ -9312,11 +9396,13 @@ function renderGroupMemberRow(tbody, conv, member) {
|
|
|
9312
9396
|
const peerId = member.peerId || member.peerAid || member.peerKey;
|
|
9313
9397
|
const name = member.peerName || shortAid(peerId);
|
|
9314
9398
|
const assignedRole = assignedRoleFor('group-member', member);
|
|
9399
|
+
const verifyBadge = roleVerificationBadge(member);
|
|
9400
|
+
const verifyClass = roleVerificationStatus(member) === 'verified' ? '' : ` peer-${roleVerificationStatus(member) === 'unknown' ? 'unknown' : 'unverified'}`;
|
|
9315
9401
|
const row = tbody.insertRow();
|
|
9316
9402
|
row.className = 'group-member-row';
|
|
9317
9403
|
row.innerHTML = `
|
|
9318
9404
|
<td></td>
|
|
9319
|
-
<td><span class="group-member-indent">${esc(name)}</span
|
|
9405
|
+
<td class="${verifyClass.trim()}"><span class="group-member-indent">${esc(name)}</span>${verifyBadge}</td>
|
|
9320
9406
|
<td><code>${esc(peerId)}</code></td>
|
|
9321
9407
|
<td>${esc(peerTypeText(member.peerType))}</td>
|
|
9322
9408
|
<td><span class="role-badge role-${esc(roleClass(member.role))}">${esc(displayRole(member.role))}</span></td>
|
package/dist/static/index.html
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
</nav>
|
|
51
51
|
<span style="flex:1"></span>
|
|
52
|
-
2026-09-
|
|
52
|
+
2026-09-05 04:51
|
|
53
53
|
<span id="conn-status" class="conn-status" data-i18n="status.connecting">连接中…</span>
|
|
54
54
|
<button id="lang-btn" class="theme-btn" data-i18n-title="language.buttonTitle" title="切换语言">🌐</button>
|
|
55
55
|
<button id="theme-btn" class="theme-btn" data-i18n-title="theme.buttonTitle" title="主题">🌗</button>
|