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
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(
|
|
109
|
+
process.stdout.write(handle.pairingCode
|
|
110
|
+
? ` ${BOLD}配对码:${RST} ${GREEN}${BOLD}${handle.pairingCode}${RST} ${DIM}(daemon 提供,5 分钟内有效,配对后 token 缓存 30 天,有访问自动续期)${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`);
|
package/dist/ipc-client.js
CHANGED
|
@@ -12,7 +12,18 @@ 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
|
+
'model.catalog',
|
|
19
|
+
'monitor-snapshot',
|
|
20
|
+
'monitor-subscribe',
|
|
21
|
+
'monitor-unsubscribe',
|
|
22
|
+
'remote-auth.pair-code',
|
|
23
|
+
'remote-auth.issue-local',
|
|
24
|
+
'remote-auth.redeem',
|
|
25
|
+
'remote-auth.validate',
|
|
26
|
+
]);
|
|
16
27
|
export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
|
|
17
28
|
let request = cmd;
|
|
18
29
|
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
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - HTTP: 静态资源 + 配对 API
|
|
5
5
|
* - WebSocket: 订阅式实时推送(aid / msg / session)
|
|
6
|
-
* - 鉴权: 6 位配对码(5
|
|
6
|
+
* - 鉴权: daemon 通过本地 IPC 提供 6 位配对码(5 分钟有效)和远程访问 token(30 天滑动续期)
|
|
7
7
|
* - 安全: 绑定 0.0.0.0(支持远程访问),token 校验;远程连接可执行受控管理操作,不能修改 daemon 配置或读取 secret
|
|
8
8
|
*
|
|
9
|
-
* 与 evolcore
|
|
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 {
|
|
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
|
|
108
|
-
const
|
|
109
|
-
|
|
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;
|
|
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;
|
|
141
100
|
}
|
|
142
|
-
function
|
|
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;
|
|
149
|
-
}
|
|
150
|
-
function getValidTokenRecord(token, now) {
|
|
101
|
+
async function validateRemoteToken(token) {
|
|
151
102
|
if (!token)
|
|
152
103
|
return null;
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
166
|
-
|
|
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,
|
|
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
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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 缓存 30 天,有访问自动续期)`);
|
|
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
|
|
521
|
-
|
|
522
|
-
|
|
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 ──
|
|
@@ -555,7 +484,30 @@ function handleConnection(ws, req, log, tokenRecord) {
|
|
|
555
484
|
send({ type: 'error', message: `unknown view: ${view}` });
|
|
556
485
|
return;
|
|
557
486
|
}
|
|
558
|
-
const sourceParams = {
|
|
487
|
+
const sourceParams = {
|
|
488
|
+
...params,
|
|
489
|
+
__menuAuth: daemonOwnerMenuAuth(localDirect, actorAid),
|
|
490
|
+
__ecwebRequestId: crypto.randomUUID(),
|
|
491
|
+
__ecwebEndpoint: `WS snapshot:${view}`,
|
|
492
|
+
};
|
|
493
|
+
let deactivate = null;
|
|
494
|
+
if (source.activate) {
|
|
495
|
+
try {
|
|
496
|
+
deactivate = await source.activate(sourceParams);
|
|
497
|
+
}
|
|
498
|
+
catch (e) {
|
|
499
|
+
if (generation === subscriptionGeneration)
|
|
500
|
+
send({ type: 'error', message: `subscription failed: ${e?.message || e}` });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (generation !== subscriptionGeneration) {
|
|
504
|
+
deactivate();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
// Install cleanup before awaiting the initial snapshot, so a view switch
|
|
508
|
+
// or socket close releases the monitor lease immediately.
|
|
509
|
+
unsubscribe = deactivate;
|
|
510
|
+
}
|
|
559
511
|
// System 视图:把 ECWeb 自身版本合并进快照(前端版本卡用)
|
|
560
512
|
const decorate = (data) => (view === 'system' && data ? { ...data, ecwebVersion: ECWEB_VERSION } : data);
|
|
561
513
|
let initialSnapshot;
|
|
@@ -570,16 +522,30 @@ function handleConnection(ws, req, log, tokenRecord) {
|
|
|
570
522
|
return;
|
|
571
523
|
send({ type: 'error', message: `snapshot failed: ${e?.message || e}` });
|
|
572
524
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
525
|
+
let nextUnsubscribe;
|
|
526
|
+
try {
|
|
527
|
+
nextUnsubscribe = source.subscribe(sourceParams, (data) => {
|
|
528
|
+
if (generation === subscriptionGeneration && currentView === view) {
|
|
529
|
+
send({ type: 'delta', view, data: decorate(data) });
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
catch (e) {
|
|
534
|
+
deactivate?.();
|
|
535
|
+
if (generation === subscriptionGeneration) {
|
|
536
|
+
unsubscribe = null;
|
|
537
|
+
send({ type: 'error', message: `subscription failed: ${e?.message || e}` });
|
|
576
538
|
}
|
|
577
|
-
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
578
541
|
if (generation !== subscriptionGeneration) {
|
|
579
542
|
nextUnsubscribe();
|
|
580
543
|
return;
|
|
581
544
|
}
|
|
582
|
-
unsubscribe =
|
|
545
|
+
unsubscribe = () => {
|
|
546
|
+
nextUnsubscribe();
|
|
547
|
+
deactivate?.();
|
|
548
|
+
};
|
|
583
549
|
};
|
|
584
550
|
ws.on('message', async (raw) => {
|
|
585
551
|
let msg;
|
|
@@ -771,26 +737,20 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
771
737
|
if (pingResp && pingResp.protocolVersion !== undefined && pingResp.protocolVersion < PROTOCOL_VERSION) {
|
|
772
738
|
log(`⚠️ evolcore protocolVersion=${pingResp.protocolVersion},watch 期望 >=${PROTOCOL_VERSION},部分功能可能异常`);
|
|
773
739
|
}
|
|
774
|
-
|
|
775
|
-
let pairingExpiry = Date.now() + PAIRING_TTL_MS;
|
|
776
|
-
let pairingAuthorizedBy;
|
|
777
|
-
function freshPairing() {
|
|
778
|
-
if (Date.now() > pairingExpiry) {
|
|
779
|
-
pairingCode = genPairingCode();
|
|
780
|
-
pairingExpiry = Date.now() + PAIRING_TTL_MS;
|
|
781
|
-
pairingAuthorizedBy = undefined;
|
|
782
|
-
log(`↺ 配对码已刷新:${pairingCode}(5 分钟有效)`);
|
|
783
|
-
}
|
|
784
|
-
return { code: pairingCode, expiresAt: pairingExpiry };
|
|
785
|
-
}
|
|
786
|
-
const server = http.createServer((req, res) => {
|
|
740
|
+
const server = http.createServer(async (req, res) => {
|
|
787
741
|
const pathname = requestPath(req.url || '');
|
|
788
742
|
const localDirect = isLocalDirect(req);
|
|
789
743
|
if (req.method === 'GET' && (req.url || '') === '/api/available-baseagents') {
|
|
790
744
|
// Base agent 可用性检测 API
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
+
}
|
|
794
754
|
}
|
|
795
755
|
else if (req.method === 'GET' && (req.url || '') === '/api/pair-code') {
|
|
796
756
|
// 取码 API:仅真本地直连可用(socket 回环 + 无 x-aun-provider-aid)。
|
|
@@ -801,28 +761,24 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
801
761
|
res.end(JSON.stringify({ error: 'forbidden' }));
|
|
802
762
|
return;
|
|
803
763
|
}
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const requestedOwner = validAuthorizedBy(Array.isArray(ownerHeader) ? ownerHeader[0] : ownerHeader);
|
|
810
|
-
const suppliedControlToken = Array.isArray(controlHeader) ? controlHeader[0] : controlHeader;
|
|
811
|
-
if (!pairingAuthorizedBy && requestedOwner && matchesDaemonControlToken(suppliedControlToken)) {
|
|
812
|
-
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;
|
|
813
769
|
}
|
|
814
770
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
815
|
-
res.end(JSON.stringify({ code, expiresAt }));
|
|
771
|
+
res.end(JSON.stringify({ code: pair.code, expiresAt: pair.expiresAt }));
|
|
816
772
|
}
|
|
817
773
|
else if (req.method === 'POST' && (req.url || '').startsWith('/api/pair')) {
|
|
818
|
-
// 配对 API
|
|
819
|
-
const autoToken = issueLocalDirectToken(req, log);
|
|
774
|
+
// 配对 API:远程(隧道/真远程)需要 daemon 配对码;本地直连自动发 daemon token。
|
|
775
|
+
const autoToken = await issueLocalDirectToken(req, log);
|
|
820
776
|
if (autoToken) {
|
|
821
777
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
822
778
|
res.end(JSON.stringify({ ok: true, token: autoToken }));
|
|
823
779
|
return;
|
|
824
780
|
}
|
|
825
|
-
handlePair(req, res,
|
|
781
|
+
handlePair(req, res, log);
|
|
826
782
|
}
|
|
827
783
|
else if (pathname === '/api/basic-settings' || pathname === '/api/basic-settings/secret') {
|
|
828
784
|
const authHeader = req.headers.authorization || '';
|
|
@@ -830,11 +786,11 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
830
786
|
const { query } = parseUrl(req.url || '');
|
|
831
787
|
let token = bearerToken || query.token || '';
|
|
832
788
|
if (!token) {
|
|
833
|
-
const autoToken = issueLocalDirectToken(req, log);
|
|
789
|
+
const autoToken = await issueLocalDirectToken(req, log);
|
|
834
790
|
if (autoToken)
|
|
835
791
|
token = autoToken;
|
|
836
792
|
}
|
|
837
|
-
if (!token || !
|
|
793
|
+
if (!token || !(await validateRemoteToken(token))) {
|
|
838
794
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
839
795
|
res.end(JSON.stringify({ success: false, error: 'unauthorized' }));
|
|
840
796
|
return;
|
|
@@ -858,11 +814,11 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
858
814
|
const { query } = parseUrl(req.url || '');
|
|
859
815
|
let token = bearerToken || query.token || '';
|
|
860
816
|
if (!token) {
|
|
861
|
-
const autoToken = issueLocalDirectToken(req, log);
|
|
817
|
+
const autoToken = await issueLocalDirectToken(req, log);
|
|
862
818
|
if (autoToken)
|
|
863
819
|
token = autoToken;
|
|
864
820
|
}
|
|
865
|
-
if (!token || !
|
|
821
|
+
if (!token || !(await validateRemoteToken(token))) {
|
|
866
822
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
867
823
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
868
824
|
return;
|
|
@@ -875,11 +831,11 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
875
831
|
const { query } = parseUrl(req.url || '');
|
|
876
832
|
let token = bearerToken || query.token || '';
|
|
877
833
|
if (!token) {
|
|
878
|
-
const autoToken = issueLocalDirectToken(req, log);
|
|
834
|
+
const autoToken = await issueLocalDirectToken(req, log);
|
|
879
835
|
if (autoToken)
|
|
880
836
|
token = autoToken;
|
|
881
837
|
}
|
|
882
|
-
if (!token || !
|
|
838
|
+
if (!token || !(await validateRemoteToken(token))) {
|
|
883
839
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
884
840
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
885
841
|
return;
|
|
@@ -893,11 +849,11 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
893
849
|
const { query } = parseUrl(req.url || '');
|
|
894
850
|
let token = bearerToken || query.token || '';
|
|
895
851
|
if (!token) {
|
|
896
|
-
const autoToken = issueLocalDirectToken(req, log);
|
|
852
|
+
const autoToken = await issueLocalDirectToken(req, log);
|
|
897
853
|
if (autoToken)
|
|
898
854
|
token = autoToken;
|
|
899
855
|
}
|
|
900
|
-
const tokenRecord = token ?
|
|
856
|
+
const tokenRecord = token ? await validateRemoteToken(token) : null;
|
|
901
857
|
if (!token || !tokenRecord) {
|
|
902
858
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
903
859
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
@@ -912,11 +868,11 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
912
868
|
const { query } = parseUrl(req.url || '');
|
|
913
869
|
let token = bearerToken || query.token || '';
|
|
914
870
|
if (!token) {
|
|
915
|
-
const autoToken = issueLocalDirectToken(req, log);
|
|
871
|
+
const autoToken = await issueLocalDirectToken(req, log);
|
|
916
872
|
if (autoToken)
|
|
917
873
|
token = autoToken;
|
|
918
874
|
}
|
|
919
|
-
const tokenRecord = token ?
|
|
875
|
+
const tokenRecord = token ? await validateRemoteToken(token) : null;
|
|
920
876
|
if (!token || !tokenRecord) {
|
|
921
877
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
922
878
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
@@ -930,11 +886,11 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
930
886
|
const { query } = parseUrl(req.url || '');
|
|
931
887
|
let token = bearerToken || query.token || '';
|
|
932
888
|
if (!token) {
|
|
933
|
-
const autoToken = issueLocalDirectToken(req, log);
|
|
889
|
+
const autoToken = await issueLocalDirectToken(req, log);
|
|
934
890
|
if (autoToken)
|
|
935
891
|
token = autoToken;
|
|
936
892
|
}
|
|
937
|
-
const tokenRecord = token ?
|
|
893
|
+
const tokenRecord = token ? await validateRemoteToken(token) : null;
|
|
938
894
|
if (!token || !tokenRecord) {
|
|
939
895
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
940
896
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
@@ -947,11 +903,11 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
947
903
|
}
|
|
948
904
|
});
|
|
949
905
|
const wss = new WebSocketServer({ noServer: true });
|
|
950
|
-
server.on('upgrade', (req, socket, head) => {
|
|
906
|
+
server.on('upgrade', async (req, socket, head) => {
|
|
951
907
|
const { query } = parseUrl(req.url || '');
|
|
952
908
|
// 本地直连免 token;远程需 token。
|
|
953
909
|
const localDirect = isLocalDirect(req);
|
|
954
|
-
const tokenRecord = localDirect ? null :
|
|
910
|
+
const tokenRecord = localDirect ? null : await validateRemoteToken(query.token || '');
|
|
955
911
|
const authed = localDirect || tokenRecord !== null;
|
|
956
912
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
957
913
|
if (!authed) {
|
|
@@ -963,11 +919,12 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
963
919
|
});
|
|
964
920
|
});
|
|
965
921
|
const { port, displaced } = await bindPort(server, opts.port ?? DEFAULT_PORT, log);
|
|
922
|
+
const initialPair = await remoteAuth('remote-auth.pair-code');
|
|
966
923
|
return {
|
|
967
924
|
url: `http://0.0.0.0:${port}`,
|
|
968
925
|
port,
|
|
969
926
|
displaced,
|
|
970
|
-
pairingCode,
|
|
927
|
+
pairingCode: initialPair?.ok && typeof initialPair.code === 'string' ? initialPair.code : '',
|
|
971
928
|
close() {
|
|
972
929
|
return new Promise((resolve) => {
|
|
973
930
|
// 强制断开所有 WS 客户端(graceful close 握手可能永不完成)
|