ec-web 0.0.22 → 0.0.24

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  * 用法:
6
6
  * ec-web [--port 42705] [--home <EVOLCORE_HOME>]
7
7
  *
8
- * 通过 EVOLCORE_HOME 定位 evolcore 数据目录,启动 HTTP+WS 服务,浏览器配对码登录。
8
+ * 通过 EVOLCORE_HOME 定位 evolcore 数据目录,启动 HTTP+WS 服务;配对码和访问 token 由 daemon 管理。
9
9
  * 与 evolcore daemon 通过 IPC socket(live 状态)+ 文件系统(历史数据)旁路通信。
10
10
  */
11
11
  import fs from 'fs';
@@ -106,7 +106,9 @@ for (const list of Object.values(ifaces)) {
106
106
  }
107
107
  }
108
108
  process.stdout.write(`\n${BOLD}${CYAN}EC Web — EvolCore${RST} ${DIM}(home: ${p.root})${RST}\n\n`);
109
- process.stdout.write(` ${BOLD}配对码:${RST} ${GREEN}${BOLD}${handle.pairingCode}${RST} ${DIM}(5 分钟内有效,配对后 token 缓存 30 天,有访问自动续期)${RST}\n\n`);
109
+ process.stdout.write(handle.pairingCode
110
+ ? ` ${BOLD}配对码:${RST} ${GREEN}${BOLD}${handle.pairingCode}${RST} ${DIM}(daemon 提供,5 分钟内有效,配对后 token 缓存 7 天,有访问自动续期)${RST}\n\n`
111
+ : ` ${BOLD}配对码:${RST} ${YELLOW}daemon 暂不可用,请先启动 EvolCore${RST}\n\n`);
110
112
  process.stdout.write(` ${BOLD}本机:${RST} http://localhost:${handle.port}\n`);
111
113
  for (const ip of lanIps)
112
114
  process.stdout.write(` ${BOLD}局域网:${RST} http://${ip}:${handle.port}\n`);
@@ -15,9 +15,14 @@ import path from 'path';
15
15
  const LOCAL_CONTROL_COMMANDS = new Set([
16
16
  'evolagent.list',
17
17
  'menu.exec',
18
+ 'model.catalog',
18
19
  'monitor-snapshot',
19
20
  'monitor-subscribe',
20
21
  'monitor-unsubscribe',
22
+ 'remote-auth.pair-code',
23
+ 'remote-auth.issue-local',
24
+ 'remote-auth.redeem',
25
+ 'remote-auth.validate',
21
26
  ]);
22
27
  export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
23
28
  let request = cmd;
package/dist/server.js CHANGED
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * - HTTP: 静态资源 + 配对 API
5
5
  * - WebSocket: 订阅式实时推送(aid / msg / session)
6
- * - 鉴权: 6 位配对码(5 分钟有效)→ token(30 天,有访问自动续期),持久化到磁盘
7
- * - 安全: 绑定 0.0.0.0(支持远程访问),token 校验;远程连接可执行受控管理操作,不能修改 daemon 配置或读取 secret
6
+ * - 鉴权: daemon 通过本地 IPC 提供 6 位配对码(5 分钟有效)和远程访问 token(7 天滑动续期)
7
+ * - 安全: 绑定 0.0.0.0(支持远程访问);本地自动登录,远程配对后使用 token,登录后权限一致
8
8
  *
9
- * 与 evolcore 的唯一通信:启动时发 ping 检查 protocolVersion(soft 校验)。
9
+ * 与 evolcore daemon 通过 IPC socket(状态、Menu 和远程访问凭据)+ 文件系统(历史数据)旁路通信。
10
10
  */
11
11
  import http from 'http';
12
12
  import fs from 'fs';
@@ -33,13 +33,11 @@ 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 { detectBaseAgents } from './sources/baseagent-detector.js';
36
+ import { detectBaseAgentsAsync } from './sources/baseagent-detector.js';
37
37
  import { readCodexSessionResult } from './sources/session-codex.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
- const TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30天(滑动窗口:每次有效访问刷新 lastActive 自动续期)
42
- const PAIRING_TTL_MS = 5 * 60 * 1000; // 5min
43
41
  const DEFAULT_PORT = 42705;
44
42
  const PROTOCOL_VERSION = 1; // 与 evolcore ping response 对齐的软校验版本
45
43
  function daemonOwnerMenuAuth(localDirect, actorAid) {
@@ -61,14 +59,6 @@ function daemonTriggerControlToken() {
61
59
  return undefined;
62
60
  }
63
61
  }
64
- function matchesDaemonControlToken(value) {
65
- const expected = daemonTriggerControlToken();
66
- if (typeof value !== 'string' || !expected)
67
- return false;
68
- const provided = Buffer.from(value);
69
- const target = Buffer.from(expected);
70
- return provided.length === target.length && crypto.timingSafeEqual(provided, target);
71
- }
72
62
  const SOURCES = { agents: aidSource, msg: msgSource, session: sessionSource, cache: cacheSource, system: systemSource, triggers: triggersSource, monitor: monitorSource, logs: logsSource, gateway: gatewaySource, roles: relationRolesSource, roleDefinitions: roleDefinitionsSource };
73
63
  // ECWeb 自身版本:渲染 System 页时随快照下发(不走 daemon IPC,ECWeb 就是这个进程)。
74
64
  function readEcwebVersion() {
@@ -104,66 +94,21 @@ const MIME = {
104
94
  };
105
95
  const HTML_CACHE_CONTROL = 'no-cache';
106
96
  const STATIC_CACHE_CONTROL = 'public, max-age=0, must-revalidate';
107
- function tokenStorePath() {
108
- const dir = resolvePaths().instanceDir;
109
- const current = path.join(dir, 'ecweb-tokens.json');
110
- const legacy = path.join(dir, 'watch-web-tokens.json');
111
- if (!fs.existsSync(current) && fs.existsSync(legacy)) {
112
- try {
113
- fs.renameSync(legacy, current);
114
- }
115
- catch { }
116
- }
117
- return current;
118
- }
119
- function loadTokens() {
120
- try {
121
- const store = JSON.parse(fs.readFileSync(tokenStorePath(), 'utf-8'));
122
- if (Array.isArray(store.tokens))
123
- return store;
124
- }
125
- catch { }
126
- return { tokens: [] };
127
- }
128
- function saveTokens(store) {
129
- try {
130
- fs.mkdirSync(resolvePaths().instanceDir, { recursive: true });
131
- const tmp = tokenStorePath() + '.tmp';
132
- fs.writeFileSync(tmp, JSON.stringify(store, null, 2));
133
- fs.renameSync(tmp, tokenStorePath());
134
- }
135
- catch { }
136
- }
137
- function pruneExpired(store, now) {
138
- const before = store.tokens.length;
139
- store.tokens = store.tokens.filter(t => now - t.lastActive < TOKEN_TTL_MS);
140
- return store.tokens.length !== before;
141
- }
142
- function validAuthorizedBy(value) {
143
- if (typeof value !== 'string')
144
- return undefined;
145
- const normalized = value.trim();
146
- if (!normalized || normalized.length > 512 || /[\r\n]/.test(normalized))
147
- return undefined;
148
- return normalized;
97
+ async function remoteAuth(type, payload = {}) {
98
+ const response = await ipcQuery(resolvePaths().socket, { type, ...payload }, 2_000);
99
+ return response && typeof response === 'object' ? response : null;
149
100
  }
150
- function getValidTokenRecord(token, now) {
101
+ async function validateRemoteToken(token) {
151
102
  if (!token)
152
103
  return null;
153
- const store = loadTokens();
154
- const changed = pruneExpired(store, now);
155
- const rec = store.tokens.find(t => t.token === token);
156
- if (rec) {
157
- rec.lastActive = now;
158
- saveTokens(store);
159
- return rec;
160
- }
161
- if (changed)
162
- saveTokens(store);
163
- return null;
104
+ const result = await remoteAuth('remote-auth.validate', { token });
105
+ if (!result?.ok || result.valid !== true)
106
+ return null;
107
+ return result.authorizedBy ? { authorizedBy: result.authorizedBy } : {};
164
108
  }
165
- function validateAndRenew(token, now) {
166
- return getValidTokenRecord(token, now) !== null;
109
+ async function issueLocalRemoteToken(label) {
110
+ const result = await remoteAuth('remote-auth.issue-local', { label });
111
+ return result?.ok && typeof result.token === 'string' ? result.token : null;
167
112
  }
168
113
  // ── Static ──
169
114
  // 经 AUN Service Proxy 转发时,proxy-server 注入 x-forwarded-prefix 头,
@@ -469,11 +414,8 @@ function isLocalDirect(req) {
469
414
  const providerAid = req.headers['x-aun-provider-aid'];
470
415
  return !providerAid || (Array.isArray(providerAid) ? providerAid.length === 0 : !providerAid.trim());
471
416
  }
472
- function genPairingCode() {
473
- return String(crypto.randomInt(0, 1000000)).padStart(6, '0');
474
- }
475
417
  // ── Pair handler ──
476
- function handlePair(req, res, pairingCode, pairingExpiry, authorizedBy, log) {
418
+ function handlePair(req, res, log) {
477
419
  let body = '';
478
420
  req.on('data', (chunk) => { body += chunk; if (body.length > 4096)
479
421
  req.destroy(); });
@@ -484,47 +426,34 @@ function handlePair(req, res, pairingCode, pairingExpiry, authorizedBy, log) {
484
426
  code = String(JSON.parse(body).code || '');
485
427
  }
486
428
  catch { }
487
- if (Date.now() > pairingExpiry) {
488
- res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false, reason: '配对码已过期,请重启 watch' }));
489
- log(`✗ 配对失败(码已过期) from ${ip}`);
490
- return;
491
- }
492
- if (code !== pairingCode) {
493
- res.writeHead(403, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false, reason: '配对码错误' }));
494
- log(`✗ 配对失败(码错误: ${code}) from ${ip}`);
495
- return;
496
- }
497
- const now = Date.now();
498
- const token = crypto.randomBytes(32).toString('hex');
499
- const store = loadTokens();
500
- pruneExpired(store, now);
501
- store.tokens.push({
502
- token,
503
- createdAt: now,
504
- lastActive: now,
429
+ void remoteAuth('remote-auth.redeem', {
430
+ code,
505
431
  label: ip,
506
- ...(authorizedBy ? { authorizedBy } : {}),
432
+ }).then(result => {
433
+ if (!result?.ok || typeof result.token !== 'string') {
434
+ const status = result?.code === 'PAIRING_EXPIRED' ? 403 : 403;
435
+ res.writeHead(status, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false, reason: result?.error ?? '配对失败' }));
436
+ log(`✗ 配对失败(${result?.code ?? 'daemon-unavailable'}) from ${ip}`);
437
+ return;
438
+ }
439
+ res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: true, token: result.token }));
440
+ log(`✓ 配对成功 from ${ip}(daemon token 缓存 7 天,有访问自动续期)`);
441
+ }).catch(error => {
442
+ res.writeHead(503, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: false, reason: 'daemon unavailable' }));
443
+ log(`✗ 配对失败(daemon unavailable: ${error instanceof Error ? error.message : String(error)}) from ${ip}`);
507
444
  });
508
- saveTokens(store);
509
- res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify({ ok: true, token }));
510
- log(`✓ 配对成功 from ${ip}(token 缓存 30 天,有访问自动续期${authorizedBy ? `,批准者 ${authorizedBy}` : ''})`);
511
445
  });
512
446
  }
513
447
  /**
514
448
  * 本地直连免配对:自动发 token。远程(隧道或真远程)需配对码。
515
449
  * 本地直连判定:socket 来源是回环 AND 无 x-aun-provider-aid 头(非隧道)。
516
450
  */
517
- function issueLocalDirectToken(req, log) {
451
+ async function issueLocalDirectToken(req, log) {
518
452
  if (!isLocalDirect(req))
519
453
  return null;
520
- const now = Date.now();
521
- const token = crypto.randomBytes(32).toString('hex');
522
- const store = loadTokens();
523
- pruneExpired(store, now);
524
- const ip = clientIp(req);
525
- store.tokens.push({ token, createdAt: now, lastActive: now, label: `local-direct:${ip}` });
526
- saveTokens(store);
527
- log(`✓ 本地直连自动授权 from ${ip}`);
454
+ const token = await issueLocalRemoteToken(`local-direct:${clientIp(req)}`);
455
+ if (token)
456
+ log(`✓ 本地直连自动授权 from ${clientIp(req)}`);
528
457
  return token;
529
458
  }
530
459
  // ── WebSocket connection ──
@@ -808,26 +737,20 @@ export async function startWatchWebServer(opts = {}) {
808
737
  if (pingResp && pingResp.protocolVersion !== undefined && pingResp.protocolVersion < PROTOCOL_VERSION) {
809
738
  log(`⚠️ evolcore protocolVersion=${pingResp.protocolVersion},watch 期望 >=${PROTOCOL_VERSION},部分功能可能异常`);
810
739
  }
811
- let pairingCode = genPairingCode();
812
- let pairingExpiry = Date.now() + PAIRING_TTL_MS;
813
- let pairingAuthorizedBy;
814
- function freshPairing() {
815
- if (Date.now() > pairingExpiry) {
816
- pairingCode = genPairingCode();
817
- pairingExpiry = Date.now() + PAIRING_TTL_MS;
818
- pairingAuthorizedBy = undefined;
819
- log(`↺ 配对码已刷新:${pairingCode}(5 分钟有效)`);
820
- }
821
- return { code: pairingCode, expiresAt: pairingExpiry };
822
- }
823
- const server = http.createServer((req, res) => {
740
+ const server = http.createServer(async (req, res) => {
824
741
  const pathname = requestPath(req.url || '');
825
742
  const localDirect = isLocalDirect(req);
826
743
  if (req.method === 'GET' && (req.url || '') === '/api/available-baseagents') {
827
744
  // Base agent 可用性检测 API
828
- const available = detectBaseAgents();
829
- res.writeHead(200, { 'Content-Type': 'application/json' });
830
- res.end(JSON.stringify(available));
745
+ try {
746
+ const available = await detectBaseAgentsAsync();
747
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
748
+ res.end(JSON.stringify(available));
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
+ }
831
754
  }
832
755
  else if (req.method === 'GET' && (req.url || '') === '/api/pair-code') {
833
756
  // 取码 API:仅真本地直连可用(socket 回环 + 无 x-aun-provider-aid)。
@@ -838,28 +761,24 @@ export async function startWatchWebServer(opts = {}) {
838
761
  res.end(JSON.stringify({ error: 'forbidden' }));
839
762
  return;
840
763
  }
841
- const { code, expiresAt } = freshPairing();
842
- // daemon 通过 localhost 取码时带上 AUN 已认证的 owner AID。只接受
843
- // 第一次绑定,避免同一配对码在不同 owner 间被重新归属。
844
- const ownerHeader = req.headers['x-evolcore-pair-authorized-by'];
845
- const controlHeader = req.headers['x-evolcore-control-token'];
846
- const requestedOwner = validAuthorizedBy(Array.isArray(ownerHeader) ? ownerHeader[0] : ownerHeader);
847
- const suppliedControlToken = Array.isArray(controlHeader) ? controlHeader[0] : controlHeader;
848
- if (!pairingAuthorizedBy && requestedOwner && matchesDaemonControlToken(suppliedControlToken)) {
849
- pairingAuthorizedBy = requestedOwner;
764
+ const pair = await remoteAuth('remote-auth.pair-code');
765
+ if (!pair?.ok || typeof pair.code !== 'string' || typeof pair.expiresAt !== 'number') {
766
+ res.writeHead(503, { 'Content-Type': 'application/json' });
767
+ res.end(JSON.stringify({ error: 'daemon unavailable' }));
768
+ return;
850
769
  }
851
770
  res.writeHead(200, { 'Content-Type': 'application/json' });
852
- res.end(JSON.stringify({ code, expiresAt }));
771
+ res.end(JSON.stringify({ code: pair.code, expiresAt: pair.expiresAt }));
853
772
  }
854
773
  else if (req.method === 'POST' && (req.url || '').startsWith('/api/pair')) {
855
- // 配对 API:远程(隧道/真远程)需要配对码;本地直连自动发 token 跳过配对。
856
- const autoToken = issueLocalDirectToken(req, log);
774
+ // 配对 API:远程(隧道/真远程)需要 daemon 配对码;本地直连自动发 daemon token。
775
+ const autoToken = await issueLocalDirectToken(req, log);
857
776
  if (autoToken) {
858
777
  res.writeHead(200, { 'Content-Type': 'application/json' });
859
778
  res.end(JSON.stringify({ ok: true, token: autoToken }));
860
779
  return;
861
780
  }
862
- handlePair(req, res, pairingCode, pairingExpiry, pairingAuthorizedBy, log);
781
+ handlePair(req, res, log);
863
782
  }
864
783
  else if (pathname === '/api/basic-settings' || pathname === '/api/basic-settings/secret') {
865
784
  const authHeader = req.headers.authorization || '';
@@ -867,26 +786,17 @@ export async function startWatchWebServer(opts = {}) {
867
786
  const { query } = parseUrl(req.url || '');
868
787
  let token = bearerToken || query.token || '';
869
788
  if (!token) {
870
- const autoToken = issueLocalDirectToken(req, log);
789
+ const autoToken = await issueLocalDirectToken(req, log);
871
790
  if (autoToken)
872
791
  token = autoToken;
873
792
  }
874
- if (!token || !validateAndRenew(token, Date.now())) {
793
+ const tokenRecord = token ? await validateRemoteToken(token) : null;
794
+ if (!token || !tokenRecord) {
875
795
  res.writeHead(401, { 'Content-Type': 'application/json' });
876
796
  res.end(JSON.stringify({ success: false, error: 'unauthorized' }));
877
797
  return;
878
798
  }
879
- if (!localDirect && pathname === '/api/basic-settings/secret') {
880
- res.writeHead(403, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
881
- res.end(JSON.stringify({ success: false, error: 'remote ECWeb connections cannot reveal API keys' }));
882
- return;
883
- }
884
- if (!localDirect && req.method !== 'GET') {
885
- res.writeHead(403, { 'Content-Type': 'application/json' });
886
- res.end(JSON.stringify({ success: false, error: 'remote ECWeb connections cannot modify daemon settings' }));
887
- return;
888
- }
889
- void handleBasicSettingsApi(req, res, { redactConnectionMaps: !localDirect });
799
+ void handleBasicSettingsApi(req, res, { redactConnectionMaps: !isLocalDirect(req), remoteAccess: !isLocalDirect(req) });
890
800
  }
891
801
  else if (req.method === 'GET' && (req.url || '').startsWith('/api/stats/')) {
892
802
  // Stats API — 本地直连自动发 token(免鉴权),远程需鉴权
@@ -895,11 +805,11 @@ export async function startWatchWebServer(opts = {}) {
895
805
  const { query } = parseUrl(req.url || '');
896
806
  let token = bearerToken || query.token || '';
897
807
  if (!token) {
898
- const autoToken = issueLocalDirectToken(req, log);
808
+ const autoToken = await issueLocalDirectToken(req, log);
899
809
  if (autoToken)
900
810
  token = autoToken;
901
811
  }
902
- if (!token || !validateAndRenew(token, Date.now())) {
812
+ if (!token || !(await validateRemoteToken(token))) {
903
813
  res.writeHead(401, { 'Content-Type': 'application/json' });
904
814
  res.end(JSON.stringify({ error: 'unauthorized' }));
905
815
  return;
@@ -912,11 +822,11 @@ export async function startWatchWebServer(opts = {}) {
912
822
  const { query } = parseUrl(req.url || '');
913
823
  let token = bearerToken || query.token || '';
914
824
  if (!token) {
915
- const autoToken = issueLocalDirectToken(req, log);
825
+ const autoToken = await issueLocalDirectToken(req, log);
916
826
  if (autoToken)
917
827
  token = autoToken;
918
828
  }
919
- if (!token || !validateAndRenew(token, Date.now())) {
829
+ if (!token || !(await validateRemoteToken(token))) {
920
830
  res.writeHead(401, { 'Content-Type': 'application/json' });
921
831
  res.end(JSON.stringify({ error: 'unauthorized' }));
922
832
  return;
@@ -930,11 +840,11 @@ export async function startWatchWebServer(opts = {}) {
930
840
  const { query } = parseUrl(req.url || '');
931
841
  let token = bearerToken || query.token || '';
932
842
  if (!token) {
933
- const autoToken = issueLocalDirectToken(req, log);
843
+ const autoToken = await issueLocalDirectToken(req, log);
934
844
  if (autoToken)
935
845
  token = autoToken;
936
846
  }
937
- const tokenRecord = token ? getValidTokenRecord(token, Date.now()) : null;
847
+ const tokenRecord = token ? await validateRemoteToken(token) : null;
938
848
  if (!token || !tokenRecord) {
939
849
  res.writeHead(401, { 'Content-Type': 'application/json' });
940
850
  res.end(JSON.stringify({ error: 'unauthorized' }));
@@ -949,11 +859,11 @@ export async function startWatchWebServer(opts = {}) {
949
859
  const { query } = parseUrl(req.url || '');
950
860
  let token = bearerToken || query.token || '';
951
861
  if (!token) {
952
- const autoToken = issueLocalDirectToken(req, log);
862
+ const autoToken = await issueLocalDirectToken(req, log);
953
863
  if (autoToken)
954
864
  token = autoToken;
955
865
  }
956
- const tokenRecord = token ? getValidTokenRecord(token, Date.now()) : null;
866
+ const tokenRecord = token ? await validateRemoteToken(token) : null;
957
867
  if (!token || !tokenRecord) {
958
868
  res.writeHead(401, { 'Content-Type': 'application/json' });
959
869
  res.end(JSON.stringify({ error: 'unauthorized' }));
@@ -967,11 +877,11 @@ export async function startWatchWebServer(opts = {}) {
967
877
  const { query } = parseUrl(req.url || '');
968
878
  let token = bearerToken || query.token || '';
969
879
  if (!token) {
970
- const autoToken = issueLocalDirectToken(req, log);
880
+ const autoToken = await issueLocalDirectToken(req, log);
971
881
  if (autoToken)
972
882
  token = autoToken;
973
883
  }
974
- const tokenRecord = token ? getValidTokenRecord(token, Date.now()) : null;
884
+ const tokenRecord = token ? await validateRemoteToken(token) : null;
975
885
  if (!token || !tokenRecord) {
976
886
  res.writeHead(401, { 'Content-Type': 'application/json' });
977
887
  res.end(JSON.stringify({ error: 'unauthorized' }));
@@ -984,11 +894,11 @@ export async function startWatchWebServer(opts = {}) {
984
894
  }
985
895
  });
986
896
  const wss = new WebSocketServer({ noServer: true });
987
- server.on('upgrade', (req, socket, head) => {
897
+ server.on('upgrade', async (req, socket, head) => {
988
898
  const { query } = parseUrl(req.url || '');
989
899
  // 本地直连免 token;远程需 token。
990
900
  const localDirect = isLocalDirect(req);
991
- const tokenRecord = localDirect ? null : getValidTokenRecord(query.token || '', Date.now());
901
+ const tokenRecord = localDirect ? null : await validateRemoteToken(query.token || '');
992
902
  const authed = localDirect || tokenRecord !== null;
993
903
  wss.handleUpgrade(req, socket, head, (ws) => {
994
904
  if (!authed) {
@@ -1000,11 +910,12 @@ export async function startWatchWebServer(opts = {}) {
1000
910
  });
1001
911
  });
1002
912
  const { port, displaced } = await bindPort(server, opts.port ?? DEFAULT_PORT, log);
913
+ const initialPair = await remoteAuth('remote-auth.pair-code');
1003
914
  return {
1004
915
  url: `http://0.0.0.0:${port}`,
1005
916
  port,
1006
917
  displaced,
1007
- pairingCode,
918
+ pairingCode: initialPair?.ok && typeof initialPair.code === 'string' ? initialPair.code : '',
1008
919
  close() {
1009
920
  return new Promise((resolve) => {
1010
921
  // 强制断开所有 WS 客户端(graceful close 握手可能永不完成)
@@ -1,117 +1,44 @@
1
1
  /**
2
2
  * Base agent 环境检测 — 检测本机可用的 Claude、Codex、Gemini 和内置 EC Agent。
3
3
  *
4
- * 检测条件:
5
- * 命令行工具优先按 PATH 检测;同时保留会话目录检测,兼容 CLI 不在当前 shell
6
- * PATH 但已经在本机初始化过的情况。ecagent 随 EvolCore 一起提供,始终作为可选
7
- * 基座展示,实际凭据和网关配置由配置校验负责。
4
+ * 检测条件与 ec init 共用:Claude/Gemini 按可执行文件检测,Codex 要求满足
5
+ * 版本及 app-server 条件,ecagent 随 EvolCore 内置。不会仅因历史会话目录存在
6
+ * 就判定 CLI 可用。
8
7
  */
9
- import fs from 'fs';
10
- import path from 'path';
11
- import os from 'os';
12
- import { createRequire } from 'module';
13
- import { spawnSync } from 'child_process';
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 detectBaseAgents() {
96
- if (_cached)
97
- return _cached;
98
- const claude = detectSources('claude').length > 0;
99
- const codex = detectSources('codex').length > 0;
100
- const gemini = detectSources('gemini').length > 0;
101
- const ecagent = true;
102
- _cached = { claude, codex, gemini, ecagent };
103
- return _cached;
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 ['claude', 'codex', 'gemini', 'ecagent'].map(id => {
109
- const detectionSource = detectSources(id);
35
+ return probes.map((probe) => {
36
+ const id = probe.id;
110
37
  return {
111
38
  id,
112
- detected: id === 'ecagent' || detectionSource.length > 0,
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() { }