ec-web 0.0.25 → 0.0.27
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/LICENSE +21 -21
- package/README.md +17 -17
- package/dist/index.js +1 -1
- package/dist/paths.js +12 -1
- package/dist/server.js +42 -25
- package/dist/sources/aid.js +28 -13
- package/dist/sources/basic-settings.js +7 -27
- package/dist/sources/msg.js +21 -4
- package/dist/sources/relation-roles.js +23 -9
- package/dist/sources/role-definitions.js +59 -34
- package/dist/sources/session-codex.js +34 -16
- package/dist/sources/session-ecagent.js +665 -0
- package/dist/sources/session-origin.js +16 -0
- package/dist/sources/session.js +69 -165
- package/dist/sources/stats.js +92 -92
- package/dist/sources/system.js +1 -33
- package/dist/sources/triggers.js +1 -1
- package/dist/sources/types.js +1 -0
- package/dist/static/app.js +11931 -11691
- package/dist/static/basic-settings.js +3 -3
- package/dist/static/index.html +427 -427
- package/dist/static/style.css +4936 -4899
- package/dist/static/vendor/echarts-5.6.0.min.js +45 -45
- package/ecweb-wrapper.cjs +67 -67
- package/package.json +35 -35
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 EvolCore contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EvolCore contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
# EC Web
|
|
2
|
-
|
|
3
|
-
Web console and monitoring dashboard for EvolCore.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -g ec-web
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Run
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
ec-web --home /path/to/runtime-home
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Use `ec watch web` when EC Web is managed by the main daemon.
|
|
1
|
+
# EC Web
|
|
2
|
+
|
|
3
|
+
Web console and monitoring dashboard for EvolCore.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g ec-web
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Run
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
ec-web --home /path/to/runtime-home
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Use `ec watch web` when EC Web is managed by the main daemon.
|
package/dist/index.js
CHANGED
|
@@ -107,7 +107,7 @@ for (const list of Object.values(ifaces)) {
|
|
|
107
107
|
}
|
|
108
108
|
process.stdout.write(`\n${BOLD}${CYAN}EC Web — EvolCore${RST} ${DIM}(home: ${p.root})${RST}\n\n`);
|
|
109
109
|
process.stdout.write(handle.pairingCode
|
|
110
|
-
? ` ${BOLD}配对码:${RST} ${GREEN}${BOLD}${handle.pairingCode}${RST} ${DIM}(daemon 提供,
|
|
110
|
+
? ` ${BOLD}配对码:${RST} ${GREEN}${BOLD}${handle.pairingCode}${RST} ${DIM}(daemon 提供,10 分钟内有效,仅可配对一次,token 缓存 7 天,有访问自动续期)${RST}\n\n`
|
|
111
111
|
: ` ${BOLD}配对码:${RST} ${YELLOW}daemon 暂不可用,请先启动 EvolCore${RST}\n\n`);
|
|
112
112
|
process.stdout.write(` ${BOLD}本机:${RST} http://localhost:${handle.port}\n`);
|
|
113
113
|
for (const ip of lanIps)
|
package/dist/paths.js
CHANGED
|
@@ -14,6 +14,10 @@ import os from 'os';
|
|
|
14
14
|
import crypto from 'crypto';
|
|
15
15
|
const isWindows = process.platform === 'win32';
|
|
16
16
|
let _root = null;
|
|
17
|
+
/** Test helper: drop the cached root so a new EVOLCORE_HOME is picked up. */
|
|
18
|
+
export function _resetRoot() {
|
|
19
|
+
_root = null;
|
|
20
|
+
}
|
|
17
21
|
export function resolveRoot() {
|
|
18
22
|
if (_root)
|
|
19
23
|
return _root;
|
|
@@ -48,7 +52,14 @@ function resolveInstanceSocketPath(root) {
|
|
|
48
52
|
const hash = crypto.createHash('sha1').update(canonicalRoot).digest('hex').slice(0, 12);
|
|
49
53
|
return `\\\\.\\pipe\\daemon-${hash}`;
|
|
50
54
|
}
|
|
51
|
-
|
|
55
|
+
const filesystemPath = path.join(root, 'data', 'instance', 'daemon.sock');
|
|
56
|
+
if (Buffer.byteLength(filesystemPath) < 100)
|
|
57
|
+
return filesystemPath;
|
|
58
|
+
const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 20);
|
|
59
|
+
const shortFilesystemPath = path.join(os.tmpdir(), `.ec-${hash.slice(0, 15)}.sock`);
|
|
60
|
+
if (Buffer.byteLength(shortFilesystemPath) < 100)
|
|
61
|
+
return shortFilesystemPath;
|
|
62
|
+
return `\0evolcore-${hash}`;
|
|
52
63
|
}
|
|
53
64
|
export function resolvePaths() {
|
|
54
65
|
const root = resolveRoot();
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - HTTP: 静态资源 + 配对 API
|
|
5
5
|
* - WebSocket: 订阅式实时推送(aid / msg / session)
|
|
6
|
-
* - 鉴权: daemon 通过本地 IPC 提供 6
|
|
6
|
+
* - 鉴权: daemon 通过本地 IPC 提供 6 位一次性配对码(10 分钟有效)和远程访问 token(7 天滑动续期)
|
|
7
7
|
* - 安全: 绑定 0.0.0.0(支持远程访问);本地自动登录,远程配对后使用 token,登录后权限一致
|
|
8
8
|
*
|
|
9
9
|
* 与 evolcore daemon 通过 IPC socket(状态、Menu 和远程访问凭据)+ 文件系统(历史数据)旁路通信。
|
|
@@ -20,7 +20,7 @@ import { ipcQuery } from './ipc-client.js';
|
|
|
20
20
|
import { setDebugLog, dlog } from './debug-log.js';
|
|
21
21
|
import { aidSource } from './sources/aid.js';
|
|
22
22
|
import { msgSource } from './sources/msg.js';
|
|
23
|
-
import { sessionSource, buildBindMap, readClaudeSessionResult } from './sources/session.js';
|
|
23
|
+
import { sessionSource, buildBindMap, readClaudeSessionResult, resolveSessionBaseagent, refreshConfiguredBaseagent } from './sources/session.js';
|
|
24
24
|
import { cacheSource } from './sources/cache.js';
|
|
25
25
|
import { systemSource } from './sources/system.js';
|
|
26
26
|
import { triggersSource } from './sources/triggers.js';
|
|
@@ -33,13 +33,14 @@ import { ccProjectsDir } from './paths.js';
|
|
|
33
33
|
import { relationRolesSource, handleRelationRolesApi, handlePeerRoleApi } from './sources/relation-roles.js';
|
|
34
34
|
import { roleDefinitionsSource, handleRoleDefinitionsApi } from './sources/role-definitions.js';
|
|
35
35
|
import { handleModelsApi } from './sources/models.js';
|
|
36
|
-
import { detectBaseAgentsAsync } from './sources/baseagent-detector.js';
|
|
37
36
|
import { readCodexSessionResult } from './sources/session-codex.js';
|
|
37
|
+
import { readEcagentSessionResult } from './sources/session-ecagent.js';
|
|
38
38
|
import { handleBasicSettingsApi } from './sources/basic-settings.js';
|
|
39
39
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
40
40
|
const STATIC_DIR = path.join(__dirname, 'static');
|
|
41
41
|
const DEFAULT_PORT = 42705;
|
|
42
42
|
const PROTOCOL_VERSION = 1; // 与 evolcore ping response 对齐的软校验版本
|
|
43
|
+
const ECWEB_TRIGGER_IPC_TYPES = new Set(['trigger.history', 'trigger.eventCatalog']);
|
|
43
44
|
function daemonOwnerMenuAuth(localDirect, actorAid) {
|
|
44
45
|
return {
|
|
45
46
|
localDirect,
|
|
@@ -48,6 +49,7 @@ function daemonOwnerMenuAuth(localDirect, actorAid) {
|
|
|
48
49
|
// authentication gate (local-direct exemption or a valid bearer token).
|
|
49
50
|
// ECWeb is the daemon-owner management surface, so remote tokens receive
|
|
50
51
|
// the same persistent fullaccess approval capability as local requests.
|
|
52
|
+
daemonOwner: true,
|
|
51
53
|
canApprovePersistentFullAccess: true,
|
|
52
54
|
};
|
|
53
55
|
}
|
|
@@ -583,16 +585,30 @@ function handleConnection(ws, req, log, tokenRecord) {
|
|
|
583
585
|
params.source = msg.source;
|
|
584
586
|
if (msg.limit)
|
|
585
587
|
params.limit = msg.limit;
|
|
588
|
+
if (msg.archived !== undefined)
|
|
589
|
+
params.archived = msg.archived;
|
|
586
590
|
dlog(`▸ 订阅 ${msg.view}${msg.aid ? ` aid=${String(msg.aid).split('.')[0]}` : ''}${msg.peer ? ` peer=${String(msg.peer).split('.')[0]}` : ''}${msg.sessionId ? ` session=${String(msg.sessionId).slice(0, 8)}` : ''}${msg.baseagent ? ` baseagent=${msg.baseagent}` : ''} from ${ip}`);
|
|
587
591
|
await switchSubscription(msg.view, params);
|
|
588
592
|
return;
|
|
589
593
|
}
|
|
590
594
|
if (msg.type === 'session.result') {
|
|
591
595
|
const params = msg.params && typeof msg.params === 'object' ? msg.params : {};
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
+
if (!params.baseagent)
|
|
597
|
+
await refreshConfiguredBaseagent();
|
|
598
|
+
const baseagent = resolveSessionBaseagent(params);
|
|
599
|
+
const result = baseagent === 'codex'
|
|
600
|
+
? readCodexSessionResult({ ...params, baseagent })
|
|
601
|
+
: baseagent === 'ecagent'
|
|
602
|
+
? readEcagentSessionResult({ ...params, baseagent })
|
|
603
|
+
: baseagent
|
|
604
|
+
? readClaudeSessionResult({ ...params, baseagent })
|
|
605
|
+
: Promise.resolve(null);
|
|
606
|
+
void Promise.resolve(result)
|
|
607
|
+
.then(text => send({ type: 'session.result', requestId: msg.requestId, data: text == null ? null : { text } }))
|
|
608
|
+
.catch(error => {
|
|
609
|
+
dlog(`session.result failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
610
|
+
send({ type: 'session.result', requestId: msg.requestId, data: null });
|
|
611
|
+
});
|
|
596
612
|
return;
|
|
597
613
|
}
|
|
598
614
|
if (msg.type === 'menu' && msg.payload) {
|
|
@@ -614,7 +630,7 @@ function handleConnection(ws, req, log, tokenRecord) {
|
|
|
614
630
|
return;
|
|
615
631
|
}
|
|
616
632
|
if (msg.type === 'ipc' && msg.payload) {
|
|
617
|
-
if (typeof msg.payload.type !== 'string' || !msg.payload.type
|
|
633
|
+
if (typeof msg.payload.type !== 'string' || !ECWEB_TRIGGER_IPC_TYPES.has(msg.payload.type)) {
|
|
618
634
|
send({ type: 'ipc.response', requestId: msg.requestId, data: { ok: false, error: 'unsupported ipc payload' } });
|
|
619
635
|
return;
|
|
620
636
|
}
|
|
@@ -622,9 +638,7 @@ function handleConnection(ws, req, log, tokenRecord) {
|
|
|
622
638
|
const timeoutMs = Number.isFinite(Number(msg.timeoutMs)) && Number(msg.timeoutMs) > 0
|
|
623
639
|
? Math.min(Number(msg.timeoutMs), 120_000)
|
|
624
640
|
: 10000;
|
|
625
|
-
const payload = msg.payload
|
|
626
|
-
? { ...msg.payload, controlToken: daemonTriggerControlToken() }
|
|
627
|
-
: msg.payload;
|
|
641
|
+
const payload = { ...msg.payload, controlToken: daemonTriggerControlToken() };
|
|
628
642
|
const resp = await ipcQuery(p.socket, payload, timeoutMs);
|
|
629
643
|
send({ type: 'ipc.response', requestId: msg.requestId, data: resp ?? { ok: false, error: 'daemon unreachable' } });
|
|
630
644
|
return;
|
|
@@ -731,6 +745,8 @@ function bindPort(server, preferred, log) {
|
|
|
731
745
|
export async function startWatchWebServer(opts = {}) {
|
|
732
746
|
const log = opts.log || (() => { });
|
|
733
747
|
setDebugLog(log);
|
|
748
|
+
// 会话读取器默认后端来自 defaults.json;启动时同步一次。
|
|
749
|
+
await refreshConfiguredBaseagent();
|
|
734
750
|
// soft 版本校验:ping daemon 检查 protocolVersion
|
|
735
751
|
const p = resolvePaths();
|
|
736
752
|
const pingResp = await ipcQuery(p.socket, { type: 'ping' }, 1000);
|
|
@@ -740,17 +756,12 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
740
756
|
const server = http.createServer(async (req, res) => {
|
|
741
757
|
const pathname = requestPath(req.url || '');
|
|
742
758
|
const localDirect = isLocalDirect(req);
|
|
743
|
-
if (req.method === 'GET' && (req.url || '') === '/api/
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
catch (error) {
|
|
751
|
-
res.writeHead(500, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
752
|
-
res.end(JSON.stringify({ success: false, error: error?.message || String(error) }));
|
|
753
|
-
}
|
|
759
|
+
if (req.method === 'GET' && (req.url || '') === '/api/health') {
|
|
760
|
+
// Keep readiness checks independent from daemon auth and pairing state.
|
|
761
|
+
// The endpoint is intentionally small so supervisors and proxies can
|
|
762
|
+
// distinguish ECWeb from an unrelated process on the configured port.
|
|
763
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
764
|
+
res.end(JSON.stringify({ ok: true, service: 'ecweb', version: ECWEB_VERSION }));
|
|
754
765
|
}
|
|
755
766
|
else if (req.method === 'GET' && (req.url || '') === '/api/pair-code') {
|
|
756
767
|
// 取码 API:仅真本地直连可用(socket 回环 + 无 x-aun-provider-aid)。
|
|
@@ -761,7 +772,9 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
761
772
|
res.end(JSON.stringify({ error: 'forbidden' }));
|
|
762
773
|
return;
|
|
763
774
|
}
|
|
764
|
-
const pair = await remoteAuth('remote-auth.pair-code'
|
|
775
|
+
const pair = await remoteAuth('remote-auth.pair-code', {
|
|
776
|
+
source: 'ecweb-local',
|
|
777
|
+
});
|
|
765
778
|
if (!pair?.ok || typeof pair.code !== 'string' || typeof pair.expiresAt !== 'number') {
|
|
766
779
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
767
780
|
res.end(JSON.stringify({ error: 'daemon unavailable' }));
|
|
@@ -796,7 +809,9 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
796
809
|
res.end(JSON.stringify({ success: false, error: 'unauthorized' }));
|
|
797
810
|
return;
|
|
798
811
|
}
|
|
799
|
-
|
|
812
|
+
// Pairing grants daemon-owner management authority, just like local
|
|
813
|
+
// access. Authorize above; do not downgrade settings by network origin.
|
|
814
|
+
void handleBasicSettingsApi(req, res);
|
|
800
815
|
}
|
|
801
816
|
else if (req.method === 'GET' && (req.url || '').startsWith('/api/stats/')) {
|
|
802
817
|
// Stats API — 本地直连自动发 token(免鉴权),远程需鉴权
|
|
@@ -910,7 +925,9 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
910
925
|
});
|
|
911
926
|
});
|
|
912
927
|
const { port, displaced } = await bindPort(server, opts.port ?? DEFAULT_PORT, log);
|
|
913
|
-
const initialPair = await remoteAuth('remote-auth.pair-code'
|
|
928
|
+
const initialPair = await remoteAuth('remote-auth.pair-code', {
|
|
929
|
+
source: 'ecweb-local',
|
|
930
|
+
});
|
|
914
931
|
return {
|
|
915
932
|
url: `http://0.0.0.0:${port}`,
|
|
916
933
|
port,
|
package/dist/sources/aid.js
CHANGED
|
@@ -65,28 +65,43 @@ function scanAidActivity() {
|
|
|
65
65
|
}
|
|
66
66
|
return result;
|
|
67
67
|
}
|
|
68
|
-
// evolcore
|
|
69
|
-
//
|
|
68
|
+
// evolcore 版本:使用 /system 的轻量 version 视图。daemon 进程级、整轮 watch 不变,
|
|
69
|
+
// 故缓存一次并合并 in-flight 请求,避免每秒重复发 IPC。
|
|
70
70
|
const versionCache = new Map();
|
|
71
|
+
const versionInFlight = new Map();
|
|
71
72
|
function authContextKey(params = {}) {
|
|
72
73
|
const auth = watchMenuRequestAuth(params);
|
|
73
74
|
return `${auth.localDirect ? 'local' : 'remote'}:${auth.actorAid || ''}`;
|
|
74
75
|
}
|
|
75
76
|
async function fetchVersion(socket, params = {}) {
|
|
76
|
-
const key = authContextKey(params)
|
|
77
|
+
const key = `${socket}\0${authContextKey(params)}`;
|
|
77
78
|
const cached = versionCache.get(key);
|
|
78
79
|
if (cached)
|
|
79
80
|
return cached;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
const pending = versionInFlight.get(key);
|
|
82
|
+
if (pending)
|
|
83
|
+
return pending;
|
|
84
|
+
const request = (async () => {
|
|
85
|
+
try {
|
|
86
|
+
const r = await ipcQuery(socket, {
|
|
87
|
+
type: 'menu.exec',
|
|
88
|
+
payload: { type: 'menu.query', id: 'aid-sys-q', name: 'system', args: { view: 'version' } },
|
|
89
|
+
auth: watchMenuRequestAuth(params),
|
|
90
|
+
}, 4000);
|
|
91
|
+
const v = r?.ok ? r.response?.data?.version : null;
|
|
92
|
+
if (v)
|
|
93
|
+
versionCache.set(key, v);
|
|
94
|
+
return v ?? null;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
versionInFlight.delete(key);
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
versionInFlight.set(key, request);
|
|
104
|
+
return request;
|
|
90
105
|
}
|
|
91
106
|
async function buildSnapshot(fallback = null, params = {}) {
|
|
92
107
|
const p = resolvePaths();
|
|
@@ -580,7 +580,7 @@ export function restartRequiredForChanges(file, changes) {
|
|
|
580
580
|
const prefixes = file === 'daemon' ? RESTART_DAEMON_PREFIXES : RESTART_DEFAULTS_PREFIXES;
|
|
581
581
|
return changes.map(change => change.path).filter(fieldPath => prefixes.some(prefix => fieldPath === prefix || fieldPath.startsWith(prefix)));
|
|
582
582
|
}
|
|
583
|
-
function redactSecrets(config, file, secrets, secretRefs
|
|
583
|
+
function redactSecrets(config, file, secrets, secretRefs) {
|
|
584
584
|
const redacted = clone(config);
|
|
585
585
|
if (file !== 'defaults' || !isRecord(redacted.baseagents))
|
|
586
586
|
return redacted;
|
|
@@ -600,26 +600,6 @@ function redactSecrets(config, file, secrets, secretRefs, options = {}) {
|
|
|
600
600
|
raw.apiKey = '******';
|
|
601
601
|
}
|
|
602
602
|
}
|
|
603
|
-
// Headers and query parameters are also common credential channels. They
|
|
604
|
-
// remain visible to local callers for editing, but remote snapshots must
|
|
605
|
-
// not disclose any map value, including values under custom key names.
|
|
606
|
-
if (options.redactConnectionMaps) {
|
|
607
|
-
for (const field of ['headers', 'queryParams']) {
|
|
608
|
-
if (!Object.prototype.hasOwnProperty.call(raw, field))
|
|
609
|
-
continue;
|
|
610
|
-
const fieldPath = `defaults.baseagents.${name}.${field}`;
|
|
611
|
-
if (isRecord(raw[field])) {
|
|
612
|
-
raw[field] = Object.fromEntries(Object.keys(raw[field]).map(key => {
|
|
613
|
-
secrets[`${fieldPath}.${key}`] = true;
|
|
614
|
-
return [key, '******'];
|
|
615
|
-
}));
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
secrets[fieldPath] = true;
|
|
619
|
-
raw[field] = '******';
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
603
|
}
|
|
624
604
|
return redacted;
|
|
625
605
|
}
|
|
@@ -670,7 +650,7 @@ export function collectLogTypeStats(configured, logsDir = resolvePaths().logs) {
|
|
|
670
650
|
export function collectLogTypes(configured, logsDir = resolvePaths().logs) {
|
|
671
651
|
return collectLogTypeStats(configured, logsDir).types;
|
|
672
652
|
}
|
|
673
|
-
async function readSnapshot(
|
|
653
|
+
async function readSnapshot() {
|
|
674
654
|
const modules = await getConfigManager();
|
|
675
655
|
const daemonFile = modules.read(modules.ConfigTarget.Process, undefined, { cache: false, expand: false });
|
|
676
656
|
const defaultsFile = modules.read(modules.ConfigTarget.Defaults, undefined, { cache: false, expand: false });
|
|
@@ -682,8 +662,8 @@ async function readSnapshot(options = {}) {
|
|
|
682
662
|
const secretRefs = {};
|
|
683
663
|
const logTypeStats = collectLogTypeStats(rawDaemon?.watch?.logTypes);
|
|
684
664
|
return {
|
|
685
|
-
daemon: redactSecrets(rawDaemon, 'daemon', secrets, secretRefs
|
|
686
|
-
defaults: redactSecrets(rawDefaults, 'defaults', secrets, secretRefs
|
|
665
|
+
daemon: redactSecrets(rawDaemon, 'daemon', secrets, secretRefs),
|
|
666
|
+
defaults: redactSecrets(rawDefaults, 'defaults', secrets, secretRefs),
|
|
687
667
|
revisions: {
|
|
688
668
|
daemon: basicSettingsRevision(rawDaemon),
|
|
689
669
|
defaults: basicSettingsRevision(rawDefaults),
|
|
@@ -758,12 +738,12 @@ function basicSettingsPath(rawUrl) {
|
|
|
758
738
|
return rawUrl.split('?')[0].replace(/\/+$/, '') || '/';
|
|
759
739
|
}
|
|
760
740
|
}
|
|
761
|
-
export async function handleBasicSettingsApi(req, res
|
|
741
|
+
export async function handleBasicSettingsApi(req, res) {
|
|
762
742
|
try {
|
|
763
743
|
const url = new URL(req.url || '', 'http://localhost');
|
|
764
744
|
const pathname = basicSettingsPath(req.url || '');
|
|
765
745
|
if (req.method === 'GET' && pathname === '/api/basic-settings') {
|
|
766
|
-
sendJson(res, 200, { success: true, data: await readSnapshot(
|
|
746
|
+
sendJson(res, 200, { success: true, data: await readSnapshot() });
|
|
767
747
|
return;
|
|
768
748
|
}
|
|
769
749
|
if (req.method === 'GET' && pathname === '/api/basic-settings/secret') {
|
|
@@ -816,7 +796,7 @@ export async function handleBasicSettingsApi(req, res, options = {}) {
|
|
|
816
796
|
changed: mutation.changed,
|
|
817
797
|
restartRequired: mutation.changed && restartPaths.length > 0,
|
|
818
798
|
restartPaths: mutation.changed ? restartPaths : [],
|
|
819
|
-
data: await readSnapshot(
|
|
799
|
+
data: await readSnapshot(),
|
|
820
800
|
});
|
|
821
801
|
return;
|
|
822
802
|
}
|
package/dist/sources/msg.js
CHANGED
|
@@ -10,6 +10,13 @@ import { ipcQuery } from '../ipc-client.js';
|
|
|
10
10
|
import { resolvePaths } from '../paths.js';
|
|
11
11
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
12
12
|
import { decodeDirSegment, isTransientProtocolMessage, readAllJsonlLines, readJsonFile, scanChatDirs, tryParseChannelSessionKey, } from '../fs-utils.js';
|
|
13
|
+
// ECWeb builds as a separate package. Keep this wire contract aligned with session-mapper.ts.
|
|
14
|
+
function topicMatchesArchiveFilter(chat, archived) {
|
|
15
|
+
if (archived === 'all')
|
|
16
|
+
return true;
|
|
17
|
+
const isArchived = chat.partition === 'thread' && chat.lable === '归档';
|
|
18
|
+
return archived === true ? isArchived : !isArchived;
|
|
19
|
+
}
|
|
13
20
|
let contextModulesPromise = null;
|
|
14
21
|
const sessionAdapterPromises = new Map();
|
|
15
22
|
let peerDisplayResolverPromise = null;
|
|
@@ -96,6 +103,7 @@ function getSessionAdapter(baseagent) {
|
|
|
96
103
|
claude: 'ClaudeSessionFileAdapter',
|
|
97
104
|
codex: 'CodexSessionFileAdapter',
|
|
98
105
|
gemini: 'GeminiSessionFileAdapter',
|
|
106
|
+
ecagent: 'EcagentSessionFileAdapter',
|
|
99
107
|
};
|
|
100
108
|
const className = classNames[baseagent];
|
|
101
109
|
const promise = !className
|
|
@@ -581,6 +589,8 @@ function loadChats(peerDisplayResolver, metrics) {
|
|
|
581
589
|
threadName: item.partition === 'thread'
|
|
582
590
|
? String(partitionActive.name || partitionActive.metadata?.topicName || item.threadId)
|
|
583
591
|
: null,
|
|
592
|
+
lable: item.partition === 'thread' && typeof partitionActive.lable === 'string'
|
|
593
|
+
? partitionActive.lable || null : null,
|
|
584
594
|
updatedAt,
|
|
585
595
|
messages: item.messages,
|
|
586
596
|
active: partitionActive,
|
|
@@ -690,6 +700,7 @@ function mergePeerInfo(chats) {
|
|
|
690
700
|
partition: chat.partition,
|
|
691
701
|
threadId: chat.threadId,
|
|
692
702
|
threadName: chat.threadName,
|
|
703
|
+
lable: chat.lable,
|
|
693
704
|
inbound: 0,
|
|
694
705
|
outbound: 0,
|
|
695
706
|
lastAt: 0,
|
|
@@ -766,7 +777,7 @@ async function sessionTurns(active, messages) {
|
|
|
766
777
|
if (baseagent && agentSessionId && projectPath) {
|
|
767
778
|
try {
|
|
768
779
|
const adapter = await getSessionAdapter(baseagent);
|
|
769
|
-
const turns = Number(adapter?.getFileInfo(projectPath, agentSessionId)?.turns);
|
|
780
|
+
const turns = Number(adapter?.getFileInfo(projectPath, agentSessionId, active.selfAID)?.turns);
|
|
770
781
|
if (Number.isFinite(turns) && turns > 0)
|
|
771
782
|
return turns;
|
|
772
783
|
}
|
|
@@ -820,6 +831,7 @@ async function buildSessionContext(chat) {
|
|
|
820
831
|
sessionId: active.id || null,
|
|
821
832
|
evolcoreSessionId: active.id || null,
|
|
822
833
|
name: active.name || null,
|
|
834
|
+
lable: chat.lable,
|
|
823
835
|
baseagent: active.baseagent || active.agentType || null,
|
|
824
836
|
chatMode,
|
|
825
837
|
permissionMode,
|
|
@@ -830,6 +842,10 @@ async function buildSessionContext(chat) {
|
|
|
830
842
|
};
|
|
831
843
|
}
|
|
832
844
|
async function buildSnapshot(params) {
|
|
845
|
+
const archived = params.archived;
|
|
846
|
+
if (archived !== undefined && archived !== false && archived !== true && archived !== 'all') {
|
|
847
|
+
throw new Error('archived 必须是布尔值或 "all"');
|
|
848
|
+
}
|
|
833
849
|
const startedAt = Date.now();
|
|
834
850
|
const metrics = {
|
|
835
851
|
files: 0,
|
|
@@ -853,13 +869,14 @@ async function buildSnapshot(params) {
|
|
|
853
869
|
: requestedScope;
|
|
854
870
|
const peer = params.peer || null;
|
|
855
871
|
const inScope = selectedChats(chats, scope);
|
|
856
|
-
const
|
|
872
|
+
const visibleChats = inScope.filter(chat => topicMatchesArchiveFilter(chat, archived));
|
|
873
|
+
const peers = scope ? mergePeerInfo(visibleChats) : [];
|
|
857
874
|
const requestedLimit = Number(params.limit);
|
|
858
875
|
const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(Math.trunc(requestedLimit), 20), 200) : 100;
|
|
859
876
|
const requestedBefore = params.before == null ? Number.NaN : Number(params.before);
|
|
860
877
|
const before = Number.isFinite(requestedBefore) && requestedBefore >= 0 ? Math.trunc(requestedBefore) : null;
|
|
861
|
-
const page = scope && peer ? messagesFor(
|
|
862
|
-
const currentChat = peer ?
|
|
878
|
+
const page = scope && peer ? messagesFor(visibleChats, peer, before, limit) : { messages: [], nextBefore: null, hasMore: false };
|
|
879
|
+
const currentChat = peer ? visibleChats.find(chat => chatKey(chat) === peer) : undefined;
|
|
863
880
|
const sessionContext = await buildSessionContext(currentChat);
|
|
864
881
|
metrics.totalMs = Date.now() - startedAt;
|
|
865
882
|
return {
|
|
@@ -24,9 +24,11 @@ async function getParentModules() {
|
|
|
24
24
|
async function getCommandModules() {
|
|
25
25
|
const permissionPath = resolveParentDistModule('core', 'auth', 'operation-authorizer.js');
|
|
26
26
|
const auditPath = resolveParentDistModule('core', 'auth', 'authorization-audit.js');
|
|
27
|
+
const runtimePolicyPath = resolveParentDistModule('core', 'role', 'runtime-policy.js');
|
|
27
28
|
const permission = await import(toFileUrl(permissionPath));
|
|
28
29
|
const audit = await import(toFileUrl(auditPath));
|
|
29
|
-
|
|
30
|
+
const runtimePolicy = await import(toFileUrl(runtimePolicyPath));
|
|
31
|
+
return { permission, audit, runtimePolicy };
|
|
30
32
|
}
|
|
31
33
|
export const ROLE_ASSIGNMENT_POLICY = {
|
|
32
34
|
owner: {
|
|
@@ -38,15 +40,17 @@ export const ROLE_ASSIGNMENT_POLICY = {
|
|
|
38
40
|
revoke: 'user-roles',
|
|
39
41
|
},
|
|
40
42
|
};
|
|
41
|
-
function isManagementRoleName(role) {
|
|
42
|
-
return role === 'owner' || role === 'admin';
|
|
43
|
-
}
|
|
44
43
|
function isAssignableUserRole(role) {
|
|
45
|
-
|
|
44
|
+
// `admin` is a management role, but it is still a valid relation role.
|
|
45
|
+
// `owner` and `daemon-owner` remain reserved for process/Agent config and
|
|
46
|
+
// can never be assigned through a relation.
|
|
47
|
+
return typeof role === 'string' && !!role && role !== 'owner' && role !== 'daemon-owner';
|
|
46
48
|
}
|
|
47
49
|
export function canAssignRole(actorRole, targetRole) {
|
|
48
50
|
if (!isAssignableUserRole(targetRole))
|
|
49
51
|
return false;
|
|
52
|
+
if (targetRole === 'admin')
|
|
53
|
+
return actorRole === 'owner';
|
|
50
54
|
return actorRole === 'owner' || actorRole === 'admin';
|
|
51
55
|
}
|
|
52
56
|
export function canRevokeRole(actorRole, targetRole) {
|
|
@@ -54,6 +58,8 @@ export function canRevokeRole(actorRole, targetRole) {
|
|
|
54
58
|
return true;
|
|
55
59
|
if (actorRole !== 'admin' || !targetRole)
|
|
56
60
|
return false;
|
|
61
|
+
if (targetRole === 'admin')
|
|
62
|
+
return false;
|
|
57
63
|
return isAssignableUserRole(targetRole);
|
|
58
64
|
}
|
|
59
65
|
async function getAgentsFromIpc() {
|
|
@@ -74,7 +80,8 @@ function resolveEcwebActor(modules, aid, auth) {
|
|
|
74
80
|
const processOwners = Array.isArray(processConfig?.owners)
|
|
75
81
|
? processConfig.owners.map((value) => String(value || '').trim()).filter(Boolean)
|
|
76
82
|
: [];
|
|
77
|
-
const daemonOwner =
|
|
83
|
+
const daemonOwner = auth.daemonOwner === true || !!auth.localDirect
|
|
84
|
+
|| (!!auth.actorAid && processOwners.includes(auth.actorAid));
|
|
78
85
|
const actorRole = daemonOwner
|
|
79
86
|
? 'owner'
|
|
80
87
|
: auth.actorAid
|
|
@@ -89,11 +96,11 @@ function resolveEcwebActor(modules, aid, auth) {
|
|
|
89
96
|
return {
|
|
90
97
|
daemonOwner,
|
|
91
98
|
actorRole,
|
|
92
|
-
actorId: auth.localDirect ? 'local-direct' : auth.actorAid || undefined,
|
|
99
|
+
actorId: auth.localDirect ? 'local-direct' : auth.actorAid || (daemonOwner ? 'ecweb-remote' : undefined),
|
|
93
100
|
};
|
|
94
101
|
}
|
|
95
102
|
async function authorizeRoleAssignmentRead(modules, aid, auth) {
|
|
96
|
-
const { permission, audit } = await getCommandModules();
|
|
103
|
+
const { permission, audit, runtimePolicy } = await getCommandModules();
|
|
97
104
|
const actor = resolveEcwebActor(modules, aid, auth);
|
|
98
105
|
const decision = permission.authorizeCommand({
|
|
99
106
|
intent: {
|
|
@@ -118,6 +125,7 @@ async function authorizeRoleAssignmentRead(modules, aid, auth) {
|
|
|
118
125
|
actorId: actor.actorId,
|
|
119
126
|
selfAid: aid,
|
|
120
127
|
role: actor.actorRole,
|
|
128
|
+
permissionMode: runtimePolicy.resolveRuntimePermissionMode({ selfAid: aid, role: actor.actorRole }).effectiveValue,
|
|
121
129
|
processRole: actor.daemonOwner ? 'daemon-owner' : 'none',
|
|
122
130
|
isDaemonOwner: actor.daemonOwner,
|
|
123
131
|
decision: decision.allow ? 'allow' : 'deny',
|
|
@@ -696,11 +704,17 @@ async function buildSnapshot(auth = {}) {
|
|
|
696
704
|
if (!readDecision.allow)
|
|
697
705
|
continue;
|
|
698
706
|
const roleConfig = modules.roles.readRolesConfig(aid);
|
|
707
|
+
const availableRoles = Object.keys(roleConfig.roles || {});
|
|
708
|
+
// Management roles are resolved separately from user-role definitions.
|
|
709
|
+
// Include the assignable management role in the ECWeb snapshot so relation
|
|
710
|
+
// editors can offer it; owner remains reserved for config.json owners[].
|
|
711
|
+
if (roleExists(modules.roles, aid, 'admin'))
|
|
712
|
+
availableRoles.push('admin');
|
|
699
713
|
snapshotAgents.push({
|
|
700
714
|
aid,
|
|
701
715
|
displayName: agent.displayName ?? agent.personalName,
|
|
702
716
|
name: agent.name,
|
|
703
|
-
roles:
|
|
717
|
+
roles: [...new Set(availableRoles)].filter(role => isAssignableUserRole(role)),
|
|
704
718
|
defaultRoles: roleConfig.defaultRoles ?? { private: null, group: null },
|
|
705
719
|
assignments: listRelationRoleAssignments(modules.config, aid),
|
|
706
720
|
});
|