// DSH host RSI — the "界面 / 记忆系统"-like status view, two surfaces (bili-style):
//
// 1) LOOPBACK dashboard (startDashboardServer): a tiny self-hosted http server on an
// ephemeral port; the "打开 Web UI" target the user can open in a browser.
// 2) DSH-WEB SERVER routes (rsiWebserverHandler): `/rsi`, `/rsi/status.json`,
// `/rsi/records.json` registered on DSH's OWN webserver (same-origin as the GUI),
// so the Settings-tab can live-fetch status with no CORS + the page can be embedded.
//
// The status PAGE (DASHBOARD_HTML) is self-contained: vanilla HTML/CSS/JS, dark theme to
// match DSH, no external assets. It polls `/status.json` every 5s and renders:
// 插件状态徽章(启用中/已停用/仅内存) · 教训统计(可信/隔离/轨迹/总数) · 分层分布 · 最近记录 · 存储/最近活动.
// Guarded everywhere so a bind/registration failure never disturbs the host.
import http from 'node:http';
import { snapshot, records as fetchRecords } from './status.js';
/**
* The self-contained status page. `basePath` is prepended to the status.json fetch so the
* same page works both on the loopback dashboard ("" → /status.json) and DSH-web routes
* ("/rsi/" → /rsi/status.json).
* @param {string} [basePath]
* @returns {string}
*/
export function dashboardHtml(basePath = "") {
const base = JSON.stringify(basePath);
return `
DSH HOST RSI记忆
`;
}
/**
* node:http handler for the DSH-webserver `/rsi` prefix route (bili-style same-origin routes).
* Serves: `/rsi` → status page HTML; `/rsi/status.json` → live snapshot; `/rsi/records.json` → recent records.
* @param {object} store openStore() result
* @param {object} cfg
* @returns {(req:object,res:object)=>void}
*/
export function rsiWebserverHandler(store, cfg = {}, getSnapshot = null) {
const d = cfg.dashboard || {};
const html = dashboardHtml("/rsi/");
let lastSnap = null;
let lastAt = 0;
const snap = () => {
const now = Date.now();
if (!lastSnap || now - lastAt > 3000) {
lastSnap = getSnapshot ? getSnapshot() : snapshot(store, { enabled: cfg.enabled, recent: d.recent ?? 15, version: cfg.version ?? null });
lastAt = now;
}
return lastSnap;
};
return (req, res) => {
try {
const url = req?.url || "/rsi";
const path = url.split("?")[0];
if (path === "/rsi/status.json") {
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
res.end(JSON.stringify(snap()));
return;
}
if (path === "/rsi/records.json") {
const qs = new URL(url.startsWith("http") ? url : `http://x${url}`, "http://x");
const q = qs.searchParams;
const out = fetchRecords(store, {
limit: Number(q.get("limit")) || 15,
trustedOnly: q.get("trustedOnly") === "true",
domain: q.get("domain") || undefined,
});
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(out));
return;
}
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(html);
} catch (e) {
try {
res.writeHead(500, { "Content-Type": "text/plain" });
res.end(String(e && e.message));
} catch { /* ignore */ }
}
};
}
/**
* Start the self-hosted LOOPBACK dashboard (fallback + "打开 Web UI" target).
* @param {object} store
* @param {object} cfg
* @returns {Promise<{url:string, port:number, host:string, close:()=>void}>}
*/
export async function startDashboardServer(store, cfg = {}, getSnapshot = null) {
const d = cfg.dashboard || {};
const host = d.host ?? "127.0.0.1";
const port = d.port ?? 0;
const handler = rsiWebserverHandler(store, { ...cfg, _rsiRewriteRoot: true });
// Rewrite the request URL mapping: loopback serves "/" (not "/rsi").
let lastSnap = null;
let lastAt = 0;
const server = http.createServer((req, res) => {
try {
const path = (req.url || "/").split("?")[0];
if (path === "/status.json" || path === "/") {
if (path === "/status.json") {
const now = Date.now();
if (!lastSnap || now - lastAt > 3000) {
lastSnap = getSnapshot ? getSnapshot() : snapshot(store, { enabled: cfg.enabled, recent: d.recent ?? 15, version: cfg.version ?? null });
lastAt = now;
}
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
res.end(JSON.stringify(lastSnap));
} else {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(dashboardHtml(""));
}
return;
}
if (path === "/records.json") {
const q = new URL(req.url, "http://x").searchParams;
const out = fetchRecords(store, { limit: Number(q.get("limit")) || 15, trustedOnly: q.get("trustedOnly") === "true", domain: q.get("domain") || undefined });
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(out));
return;
}
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("not found");
} catch (e) {
try {
res.writeHead(500);
res.end(String(e && e.message));
} catch { /* ignore */ }
}
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(port, host, () => resolve());
});
const actualPort = server.address().port;
if (typeof server.unref === "function") server.unref();
return {
url: `http://${host}:${actualPort}`,
host,
port: actualPort,
close() {
try {
server.close();
} catch { /* ignore */ }
},
};
}
export default { dashboardHtml, rsiWebserverHandler, startDashboardServer };