dshost-plugin 0.1.13 → 0.1.15
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/lib/core.js +86 -5
- package/lib/index.js +3 -0
- package/package.json +1 -1
package/lib/core.js
CHANGED
|
@@ -375,6 +375,63 @@ export function startAgent(opts) {
|
|
|
375
375
|
|
|
376
376
|
const svcList = normalizeServices(services, log);
|
|
377
377
|
const svcMap = new Map(svcList.map((s) => [s.id, s]));
|
|
378
|
+
// 单业务配置管理(门户托管):宿主显式开启 remoteServices 才接受 relay 下发的
|
|
379
|
+
// 'services:config';下发项与本机声明走同一 normalizeServices 白名单,且只允许
|
|
380
|
+
// auth=open、不接受任何凭据字段;与本机声明同 id 的下发项一律拒绝(本机为准)。
|
|
381
|
+
const remoteServices = opts.remoteServices === true || process.env.DSH_REMOTE_SERVICES === '1';
|
|
382
|
+
let managedList = [];
|
|
383
|
+
let lastApplied = null;
|
|
384
|
+
function servicesFrame() {
|
|
385
|
+
const pub = ({ id, name, desc, icon, auth, entry, spa, category }) => ({ id, name, desc, icon, auth, entry, spa, category });
|
|
386
|
+
return {
|
|
387
|
+
type: 'services',
|
|
388
|
+
servicesOnly: process.env.DSH_SERVICES_ONLY === '1' || undefined,
|
|
389
|
+
capabilities: remoteServices && process.env.DSH_SERVICES_ONLY !== '1' ? { remoteConfig: 1 } : undefined,
|
|
390
|
+
services: [
|
|
391
|
+
...svcList.map((s) => ({ ...pub(s), origin: 'local' })),
|
|
392
|
+
...managedList.map((s) => ({ ...pub(s), origin: 'managed' })),
|
|
393
|
+
],
|
|
394
|
+
applied: lastApplied || undefined,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function applyManagedConfig(raw) {
|
|
398
|
+
const results = [];
|
|
399
|
+
const accepted = [];
|
|
400
|
+
const localIds = new Set(svcList.map((s) => s.id));
|
|
401
|
+
for (const item of Array.isArray(raw) ? raw.slice(0, 32) : []) {
|
|
402
|
+
const id = String(item?.id || '');
|
|
403
|
+
if (localIds.has(id)) { results.push({ id, ok: false, error: '与本机声明的服务同名,以本机为准' }); continue; }
|
|
404
|
+
const warn = [];
|
|
405
|
+
const [svc] = normalizeServices([{
|
|
406
|
+
id, name: item?.name, desc: item?.desc, icon: item?.icon, upstream: item?.upstream,
|
|
407
|
+
entry: item?.entry, spa: item?.spa === true, auth: 'open',
|
|
408
|
+
}], { warn: (m) => warn.push(m) });
|
|
409
|
+
if (!svc) { results.push({ id, ok: false, error: (warn[0] || 'invalid').replace(/^\[agent\] /, '') }); continue; }
|
|
410
|
+
svc.category = typeof item?.category === 'string' ? item.category.slice(0, 16) : '';
|
|
411
|
+
accepted.push(svc);
|
|
412
|
+
results.push({ id, ok: true });
|
|
413
|
+
}
|
|
414
|
+
for (const s of managedList) svcMap.delete(s.id);
|
|
415
|
+
managedList = accepted;
|
|
416
|
+
for (const s of managedList) svcMap.set(s.id, s);
|
|
417
|
+
lastApplied = results;
|
|
418
|
+
log.log?.(`[agent] managed services applied: ${results.map((r) => `${r.id}:${r.ok ? 'ok' : 'rejected'}`).join(', ') || '(none)'}`);
|
|
419
|
+
}
|
|
420
|
+
function probeUpstream(ws, msg) {
|
|
421
|
+
const t0 = Date.now();
|
|
422
|
+
const reply = (r) => send(ws, { type: 'services:probe-result', reqId: msg.reqId, ms: Date.now() - t0, ...r });
|
|
423
|
+
const up = parseUpstream(msg.upstream);
|
|
424
|
+
if (!up) { reply({ ok: false, error: '上游地址格式错误' }); return; }
|
|
425
|
+
if (!upstreamAllowed(up.host)) { reply({ ok: false, error: `${up.host} 不在实例白名单内(回环 / 10.144.144.0/24 / AGENT_UPSTREAM_ALLOW)` }); return; }
|
|
426
|
+
const entry = typeof msg.entry === 'string' && msg.entry.startsWith('/') ? msg.entry : '/';
|
|
427
|
+
const req = http.request({ host: up.host, port: up.port, path: entry, method: 'GET', timeout: 5000, headers: { host: `${up.host}:${up.port}` } }, (res) => {
|
|
428
|
+
res.resume();
|
|
429
|
+
reply({ ok: res.statusCode < 500, status: res.statusCode });
|
|
430
|
+
});
|
|
431
|
+
req.on('timeout', () => { req.destroy(new Error('连接超时(5s)')); });
|
|
432
|
+
req.on('error', (e) => reply({ ok: false, error: e.code === 'ECONNREFUSED' ? '连接被拒绝(端口没有服务在监听)' : e.message }));
|
|
433
|
+
req.end();
|
|
434
|
+
}
|
|
378
435
|
let relayUsername = null; // set on 'registered'; subject of SSO trust headers
|
|
379
436
|
if (svcList.length) {
|
|
380
437
|
log.log?.(`[agent] plugin services: ${svcList.map((s) => `${s.id}->${s.host}:${s.port}${s.auth === 'sso' ? ' (sso)' : ''}`).join(', ')}`);
|
|
@@ -458,7 +515,19 @@ export function startAgent(opts) {
|
|
|
458
515
|
log.log?.(`[agent] Connecting to ${relayUrl} ...`);
|
|
459
516
|
const ws = new WebSocket(relayUrl);
|
|
460
517
|
|
|
518
|
+
// 握手超时看门狗:如果 WS open 事件在 20s 内未到达(TCP 连上但 upgrade
|
|
519
|
+
// 响应丢失——WiFi 恢复期/代理链路抖动时常见),主动 terminate 触发 close
|
|
520
|
+
// → 重连,避免永久挂死在 CONNECTING 状态(旧逻辑的致命缺陷:startHeartbeat
|
|
521
|
+
// 只在 registered 后启动,握手挂死时无任何看门狗看管新连接)。
|
|
522
|
+
const handshakeTimer = setTimeout(() => {
|
|
523
|
+
if (ws.readyState === WebSocket.CONNECTING) {
|
|
524
|
+
log.log?.('[agent] handshake timeout (20s, no WS open), forcing reconnect');
|
|
525
|
+
try { ws.terminate(); } catch (eT) {}
|
|
526
|
+
}
|
|
527
|
+
}, 20000);
|
|
528
|
+
|
|
461
529
|
ws.on('open', () => {
|
|
530
|
+
clearTimeout(handshakeTimer);
|
|
462
531
|
log.log?.('[agent] Connected to relay');
|
|
463
532
|
send(ws, { type: 'register', token, version });
|
|
464
533
|
});
|
|
@@ -475,17 +544,26 @@ export function startAgent(opts) {
|
|
|
475
544
|
send(ws, { type: 'system-info', info: collectSystemInfo(dshVersion) });
|
|
476
545
|
// 插件工作区:上报本实例声明的服务清单(relay 存实例维度,供 /p 路由校验)
|
|
477
546
|
// DSH_SERVICES_ONLY=1:独立 services agent,relay 不注册主实例(服务旁挂到同机 dsh 实例)
|
|
478
|
-
send(ws,
|
|
479
|
-
type: 'services',
|
|
480
|
-
servicesOnly: process.env.DSH_SERVICES_ONLY === '1' || undefined,
|
|
481
|
-
services: svcList.map(({ id, name, desc, icon, auth, entry, spa }) => ({ id, name, desc, icon, auth, entry, spa })),
|
|
482
|
-
});
|
|
547
|
+
send(ws, servicesFrame());
|
|
483
548
|
startHeartbeat(ws);
|
|
484
549
|
return;
|
|
485
550
|
}
|
|
486
551
|
|
|
487
552
|
if (msg.type === 'heartbeat') return;
|
|
488
553
|
|
|
554
|
+
// 门户托管业务下发(仅宿主开启 remoteServices 时接受)
|
|
555
|
+
if (msg.type === 'services:config') {
|
|
556
|
+
if (!remoteServices) { log.warn?.('[agent] services:config ignored (remoteServices disabled)'); return; }
|
|
557
|
+
applyManagedConfig(msg.services);
|
|
558
|
+
send(ws, servicesFrame());
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (msg.type === 'services:probe') {
|
|
562
|
+
if (!remoteServices) { send(ws, { type: 'services:probe-result', reqId: msg.reqId, ok: false, error: 'remoteServices disabled' }); return; }
|
|
563
|
+
probeUpstream(ws, msg);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
489
567
|
if (msg.type === 'http:request') {
|
|
490
568
|
// [Artifact Viewer] 是 dsh UI 注入脚本的专属旁路;插件流不经过它。
|
|
491
569
|
if (!msg.target && serveArtifactIfRequested(ws, msg)) return;
|
|
@@ -545,6 +623,8 @@ export function startAgent(opts) {
|
|
|
545
623
|
});
|
|
546
624
|
|
|
547
625
|
ws.on('close', (code, reason) => {
|
|
626
|
+
clearTimeout(handshakeTimer);
|
|
627
|
+
if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; }
|
|
548
628
|
log.log?.(`[agent] Disconnected (${code}): ${reason}`);
|
|
549
629
|
registered = false;
|
|
550
630
|
for (const [, stream] of streams) {
|
|
@@ -560,6 +640,7 @@ export function startAgent(opts) {
|
|
|
560
640
|
});
|
|
561
641
|
|
|
562
642
|
ws.on('error', (err) => {
|
|
643
|
+
clearTimeout(handshakeTimer);
|
|
563
644
|
log.error?.(`[agent] WS Error: ${err.message}`);
|
|
564
645
|
ws.close();
|
|
565
646
|
});
|
package/lib/index.js
CHANGED
|
@@ -20,6 +20,8 @@ export function apply(ctx, config) {
|
|
|
20
20
|
const dshPort = parseInt(config.dshPort || process.env.DSH_PORT || String(DSH_PORT), 10);
|
|
21
21
|
// 插件工作区服务清单(cordis.patch.yml 的 config.services 数组)
|
|
22
22
|
const services = Array.isArray(config.services) ? config.services : [];
|
|
23
|
+
// 单业务配置管理:允许 dshost.me 门户下发托管业务(显式开启;上游白名单仍在本机强制)
|
|
24
|
+
const remoteServices = config.remoteServices === true;
|
|
23
25
|
|
|
24
26
|
console.log('[dsh-remote] Plugin loaded, token:', token ? '***' : 'none');
|
|
25
27
|
if (services.length) console.log('[dsh-remote] Plugin services:', services.map((s) => s && s.id).filter(Boolean).join(', '));
|
|
@@ -41,6 +43,7 @@ export function apply(ctx, config) {
|
|
|
41
43
|
dshHost,
|
|
42
44
|
dshPort,
|
|
43
45
|
services,
|
|
46
|
+
remoteServices,
|
|
44
47
|
});
|
|
45
48
|
|
|
46
49
|
// dsh >= 0.1.2 web auth: the web UI is gated behind a per-process one-time
|
package/package.json
CHANGED