ec-web 0.0.21 → 0.0.23
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/index.js +4 -2
- package/dist/ipc-client.js +12 -1
- package/dist/paths.js +18 -1
- package/dist/server.js +111 -154
- package/dist/sources/baseagent-detector.js +27 -102
- package/dist/sources/basic-settings.js +21 -4
- package/dist/sources/models.js +13 -6
- package/dist/sources/monitor.js +22 -5
- package/dist/sources/relation-roles.js +67 -17
- package/dist/sources/role-definitions.js +64 -15
- package/dist/sources/session-codex.js +18 -6
- package/dist/sources/triggers.js +58 -0
- package/dist/static/app.js +173 -39
- package/dist/static/basic-settings.js +81 -14
- package/dist/static/index.html +1 -1
- package/package.json +1 -1
|
@@ -1,117 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Base agent 环境检测 — 检测本机可用的 Claude、Codex、Gemini 和内置 EC Agent。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 基座展示,实际凭据和网关配置由配置校验负责。
|
|
4
|
+
* 检测条件与 ec init 共用:Claude/Gemini 按可执行文件检测,Codex 要求满足
|
|
5
|
+
* 版本及 app-server 条件,ecagent 随 EvolCore 内置。不会仅因历史会话目录存在
|
|
6
|
+
* 就判定 CLI 可用。
|
|
8
7
|
*/
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const requireFromHere = createRequire(import.meta.url);
|
|
15
|
-
let _cached = null;
|
|
16
|
-
/**
|
|
17
|
-
* 检测 node:sqlite 模块是否可用(Node 22.5+)
|
|
18
|
-
*/
|
|
19
|
-
function checkSqliteAvailable() {
|
|
20
|
-
try {
|
|
21
|
-
requireFromHere('node:sqlite');
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return false;
|
|
8
|
+
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
9
|
+
let sharedDetectorPromise = null;
|
|
10
|
+
async function sharedDetector() {
|
|
11
|
+
if (!sharedDetectorPromise) {
|
|
12
|
+
sharedDetectorPromise = import(toFileUrl(resolveParentDistModule('agents', 'baseagent-availability.js')));
|
|
26
13
|
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* 检测 Codex state_*.sqlite 文件是否存在
|
|
30
|
-
*/
|
|
31
|
-
function checkCodexStateDb() {
|
|
32
|
-
const codexHome = path.join(os.homedir(), '.codex');
|
|
33
|
-
if (!fs.existsSync(codexHome))
|
|
34
|
-
return false;
|
|
35
|
-
try {
|
|
36
|
-
const files = fs.readdirSync(codexHome).filter(f => /^state_\d+\.sqlite$/.test(f));
|
|
37
|
-
return files.length > 0;
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* 检测 Claude projects 目录是否存在
|
|
45
|
-
*/
|
|
46
|
-
function checkClaudeProjects() {
|
|
47
|
-
const claudeProjects = path.join(os.homedir(), '.claude', 'projects');
|
|
48
|
-
return fs.existsSync(claudeProjects);
|
|
49
|
-
}
|
|
50
|
-
function commandExists(command) {
|
|
51
|
-
try {
|
|
52
|
-
const result = process.platform === 'win32'
|
|
53
|
-
? spawnSync('where', [command], { stdio: 'ignore', windowsHide: true })
|
|
54
|
-
: spawnSync('sh', ['-lc', `command -v ${JSON.stringify(command)}`], { stdio: 'ignore' });
|
|
55
|
-
return result.status === 0;
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function checkGeminiHome() {
|
|
62
|
-
return fs.existsSync(path.join(os.homedir(), '.gemini'));
|
|
63
|
-
}
|
|
64
|
-
function detectSources(id) {
|
|
65
|
-
if (id === 'ecagent')
|
|
66
|
-
return ['内置'];
|
|
67
|
-
const sources = [];
|
|
68
|
-
if (commandExists(id))
|
|
69
|
-
sources.push('PATH');
|
|
70
|
-
if (id === 'claude' && checkClaudeProjects())
|
|
71
|
-
sources.push('~/.claude/projects');
|
|
72
|
-
if (id === 'codex') {
|
|
73
|
-
if (checkCodexSessions())
|
|
74
|
-
sources.push('~/.codex/sessions');
|
|
75
|
-
if (checkCodexStateDb())
|
|
76
|
-
sources.push('~/.codex/state_*.sqlite');
|
|
77
|
-
if (checkSqliteAvailable())
|
|
78
|
-
sources.push('node:sqlite');
|
|
79
|
-
}
|
|
80
|
-
if (id === 'gemini' && checkGeminiHome())
|
|
81
|
-
sources.push('~/.gemini');
|
|
82
|
-
return sources;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* 检测 Codex sessions 目录是否存在
|
|
86
|
-
*/
|
|
87
|
-
function checkCodexSessions() {
|
|
88
|
-
const codexSessions = path.join(os.homedir(), '.codex', 'sessions');
|
|
89
|
-
return fs.existsSync(codexSessions);
|
|
14
|
+
return sharedDetectorPromise;
|
|
90
15
|
}
|
|
91
16
|
/**
|
|
92
17
|
* 检测所有 base agent 的可用性
|
|
93
|
-
*
|
|
18
|
+
* 每次调用都会重新探测,确保基础设置中的“重新检测”反映当前环境。
|
|
94
19
|
*/
|
|
95
|
-
export function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
20
|
+
export async function detectBaseAgentsAsync() {
|
|
21
|
+
const mod = await sharedDetector();
|
|
22
|
+
const detected = new Set(mod.detectAvailableBaseagents());
|
|
23
|
+
return {
|
|
24
|
+
claude: detected.has('claude'),
|
|
25
|
+
codex: detected.has('codex'),
|
|
26
|
+
gemini: detected.has('gemini'),
|
|
27
|
+
ecagent: detected.has('ecagent'),
|
|
28
|
+
};
|
|
104
29
|
}
|
|
105
30
|
/** Return display-oriented detection details for the Basic Settings API. */
|
|
106
|
-
export function detectBaseAgentDetails(defaults = {}) {
|
|
31
|
+
export async function detectBaseAgentDetails(defaults = {}) {
|
|
32
|
+
const mod = await sharedDetector();
|
|
33
|
+
const probes = mod.detectBaseagentDetails();
|
|
107
34
|
const configured = isRecord(defaults?.baseagents) ? defaults.baseagents : {};
|
|
108
|
-
return
|
|
109
|
-
const
|
|
35
|
+
return probes.map((probe) => {
|
|
36
|
+
const id = probe.id;
|
|
110
37
|
return {
|
|
111
38
|
id,
|
|
112
|
-
detected:
|
|
39
|
+
detected: probe.detected,
|
|
113
40
|
configured: Object.prototype.hasOwnProperty.call(configured, id),
|
|
114
|
-
detectionSource,
|
|
41
|
+
detectionSource: probe.detectionSource,
|
|
115
42
|
};
|
|
116
43
|
});
|
|
117
44
|
}
|
|
@@ -121,6 +48,4 @@ function isRecord(value) {
|
|
|
121
48
|
/**
|
|
122
49
|
* 重置缓存(测试用)
|
|
123
50
|
*/
|
|
124
|
-
export function resetDetectionCache() {
|
|
125
|
-
_cached = null;
|
|
126
|
-
}
|
|
51
|
+
export function resetDetectionCache() { }
|
|
@@ -176,6 +176,10 @@ function baseagentPath(fieldPath) {
|
|
|
176
176
|
return null;
|
|
177
177
|
return { baseagent: match[1], field: match[2] };
|
|
178
178
|
}
|
|
179
|
+
function baseagentBlockPath(fieldPath) {
|
|
180
|
+
const match = fieldPath.match(/^baseagents\.([a-z0-9_-]+)$/);
|
|
181
|
+
return match?.[1] || null;
|
|
182
|
+
}
|
|
179
183
|
export function isAllowedBasicSettingsPath(file, fieldPath) {
|
|
180
184
|
try {
|
|
181
185
|
pathSegments(fieldPath);
|
|
@@ -187,6 +191,9 @@ export function isAllowedBasicSettingsPath(file, fieldPath) {
|
|
|
187
191
|
return DAEMON_EXACT_PATHS.has(fieldPath);
|
|
188
192
|
if (DEFAULTS_EXACT_PATHS.has(fieldPath))
|
|
189
193
|
return true;
|
|
194
|
+
const block = baseagentBlockPath(fieldPath);
|
|
195
|
+
if (block)
|
|
196
|
+
return BASEAGENT_SET.has(block);
|
|
190
197
|
const parsed = baseagentPath(fieldPath);
|
|
191
198
|
return !!parsed && BASEAGENT_SET.has(parsed.baseagent) && BASEAGENT_FIELDS[parsed.baseagent]?.has(parsed.field);
|
|
192
199
|
}
|
|
@@ -450,6 +457,11 @@ function validateDefaultsValue(fieldPath, value) {
|
|
|
450
457
|
const parsed = baseagentPath(fieldPath);
|
|
451
458
|
if (parsed)
|
|
452
459
|
return validateBaseagentValue(parsed.baseagent, parsed.field, fieldPath, value);
|
|
460
|
+
if (baseagentBlockPath(fieldPath)) {
|
|
461
|
+
if (!isRecord(value) || Object.keys(value).length > 0)
|
|
462
|
+
throw new Error(`${fieldPath} must be an empty object`);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
453
465
|
if (fieldPath === 'active_baseagent') {
|
|
454
466
|
if (!BASEAGENT_SET.has(String(value)))
|
|
455
467
|
throw new Error(`${fieldPath} is invalid`);
|
|
@@ -510,8 +522,7 @@ export function validateBasicSettingsChange(file, change) {
|
|
|
510
522
|
else
|
|
511
523
|
validateDefaultsValue(change.path, change.value);
|
|
512
524
|
}
|
|
513
|
-
function validateBaseagentAvailability(
|
|
514
|
-
const details = detectBaseAgentDetails(config);
|
|
525
|
+
function validateBaseagentAvailability(changes, details) {
|
|
515
526
|
const detected = new Set(details.filter(item => item.detected).map(item => item.id));
|
|
516
527
|
const configured = new Set(details.filter(item => item.configured).map(item => item.id));
|
|
517
528
|
for (const change of changes) {
|
|
@@ -522,6 +533,10 @@ function validateBaseagentAvailability(config, changes) {
|
|
|
522
533
|
if (parsed && !detected.has(parsed.baseagent) && !configured.has(parsed.baseagent)) {
|
|
523
534
|
throw Object.assign(new Error(`Base Agent is not available on this machine: ${parsed.baseagent}`), { statusCode: 400 });
|
|
524
535
|
}
|
|
536
|
+
const block = baseagentBlockPath(change.path);
|
|
537
|
+
if (block && change.op === 'set' && !detected.has(block) && !configured.has(block)) {
|
|
538
|
+
throw Object.assign(new Error(`Base Agent is not available on this machine: ${block}`), { statusCode: 400 });
|
|
539
|
+
}
|
|
525
540
|
}
|
|
526
541
|
}
|
|
527
542
|
function setAtPath(root, segments, value) {
|
|
@@ -675,7 +690,7 @@ async function readSnapshot(options = {}) {
|
|
|
675
690
|
},
|
|
676
691
|
secrets,
|
|
677
692
|
secretRefs,
|
|
678
|
-
baseagents: detectBaseAgentDetails(rawDefaults),
|
|
693
|
+
baseagents: await detectBaseAgentDetails(rawDefaults),
|
|
679
694
|
logTypes: logTypeStats.types,
|
|
680
695
|
logTypeCounts: logTypeStats.counts,
|
|
681
696
|
};
|
|
@@ -779,6 +794,8 @@ export async function handleBasicSettingsApi(req, res, options = {}) {
|
|
|
779
794
|
changes.forEach(change => validateBasicSettingsChange(file, change));
|
|
780
795
|
const modules = await getConfigManager();
|
|
781
796
|
const target = file === 'daemon' ? modules.ConfigTarget.Process : modules.ConfigTarget.Defaults;
|
|
797
|
+
const currentConfig = modules.read(target, undefined, { cache: false, expand: false }) ?? {};
|
|
798
|
+
const detection = file === 'defaults' ? await detectBaseAgentDetails(currentConfig) : [];
|
|
782
799
|
const mutation = modules.mutateConfig(target, undefined, config => {
|
|
783
800
|
if (basicSettingsRevision(config) !== body.revision) {
|
|
784
801
|
throw Object.assign(new Error('Configuration changed after it was loaded. Reload and try again.'), {
|
|
@@ -786,7 +803,7 @@ export async function handleBasicSettingsApi(req, res, options = {}) {
|
|
|
786
803
|
});
|
|
787
804
|
}
|
|
788
805
|
if (file === 'defaults')
|
|
789
|
-
validateBaseagentAvailability(
|
|
806
|
+
validateBaseagentAvailability(changes, detection);
|
|
790
807
|
applyBasicSettingsChanges(config, file, changes);
|
|
791
808
|
const errors = modules.validateConfig(target, config);
|
|
792
809
|
if (errors.length > 0) {
|
package/dist/sources/models.js
CHANGED
|
@@ -32,21 +32,27 @@ export function normalizeModelEntries(models) {
|
|
|
32
32
|
owned_by: model.owned_by,
|
|
33
33
|
created: model.created,
|
|
34
34
|
family: inferModelFamily(id),
|
|
35
|
-
status: isAlias ? 'alias' : '
|
|
35
|
+
status: isAlias ? 'alias' : model.verification || 'unverified',
|
|
36
|
+
verification: model.verification,
|
|
37
|
+
reasoningLevels: model.reasoningLevels,
|
|
38
|
+
aliasFor: model.aliasFor,
|
|
36
39
|
isAlias
|
|
37
40
|
});
|
|
38
41
|
}
|
|
39
42
|
return out;
|
|
40
43
|
}
|
|
41
|
-
export async function getModelCatalogSnapshot(baseagent = 'claude') {
|
|
44
|
+
export async function getModelCatalogSnapshot(baseagent = 'claude', self, refresh = false) {
|
|
42
45
|
const { getCatalog } = await getCatalogModule();
|
|
43
|
-
const catalog = await getCatalog(
|
|
46
|
+
const catalog = await getCatalog(self, baseagent, refresh);
|
|
44
47
|
const normalized = normalizeModelEntries(catalog.models || []);
|
|
45
48
|
return {
|
|
46
49
|
models: normalized,
|
|
47
50
|
catalogModels: normalized,
|
|
48
51
|
source: catalog.source || 'unknown',
|
|
49
|
-
lastUpdate:
|
|
52
|
+
lastUpdate: catalog.fetchedAt,
|
|
53
|
+
revision: catalog.revision,
|
|
54
|
+
freshness: catalog.freshness,
|
|
55
|
+
diagnostics: catalog.diagnostics,
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
function sendJson(res, status, payload) {
|
|
@@ -58,11 +64,12 @@ export async function handleModelsApi(req, res) {
|
|
|
58
64
|
const url = new URL(req.url || '', 'http://localhost');
|
|
59
65
|
if (req.method === 'GET' && url.pathname === '/api/models/catalog') {
|
|
60
66
|
const baseagent = url.searchParams.get('baseagent') || 'claude';
|
|
61
|
-
if (
|
|
67
|
+
if (!['claude', 'codex', 'ecagent', 'gemini'].includes(baseagent)) {
|
|
62
68
|
sendJson(res, 400, { success: false, error: 'Invalid baseagent' });
|
|
63
69
|
return;
|
|
64
70
|
}
|
|
65
|
-
const
|
|
71
|
+
const self = url.searchParams.get('aid') || url.searchParams.get('self') || undefined;
|
|
72
|
+
const data = await getModelCatalogSnapshot(baseagent, self, url.searchParams.get('refresh') === 'true');
|
|
66
73
|
sendJson(res, 200, { success: true, data });
|
|
67
74
|
return;
|
|
68
75
|
}
|
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' });
|