Files
SenMeshNet/electron/remoteHttpServer.cjs
zhaolei 4417f62d7e
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
feat: register plan management in vault nav + remote HTTP routes
- VaultViewLayout: lazy import + Wrench icon + sidebar menu + planManagement section render

- remoteHttpServer: loadModules 加载 4 新 store + resetAllCaches 重置 + 6 条预案 GET 路由 (templates/plans/plan/:id/command-catalog/aggregation-rules)
2026-10-30 17:00:38 +08:00

1398 lines
57 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/**
* NetMesh 远程 HTTP Server —— 单机版浏览器可通过 URL 访问完整运行监控页面
*
* 零外部依赖,用 Node 内置 http 模块。
* 启动条件:storage 里 "netcatty_remote_access" 的 enabled === true。
*
* 安全模型:
* - 启动时在存储里生成随机 token(24 位 hex)
* - 访问入口(/、/index.html)和 /api/* 都要求 URL query 带 ?token=xxx
* - token 通过注入 script 传到 window.__REMOTE_TOKEN__,让 SPA 内 fetch 自动带上
*/
const http = require("http");
const fs = require("fs");
const path = require("path");
const os = require("os");
const crypto = require("crypto");
// ─── 获取本机局域网 IPv4(优先以太网,过滤虚拟网卡) ────
function getLanIp() {
try {
const nets = os.networkInterfaces();
const candidates = [];
// 虚拟/内部/隧道网卡关键词(命中即跳过)
const VIRTUAL = [
"loopback", "docker", "veth", "vmware", "virtualbox", "hyper-v", "vEthernet",
"vbox", "bridge", "tailscale", "zerotier", "wireguard", "tun", "tap",
"bluetooth", "bth", "wsl", "localhost", "回环", "虚拟", "蓝牙",
];
// WiFi 关键词(降权)
const WIFI = ["wlan", "wi-fi", "wifi", "wireless", "无线"];
// 以太网关键词(优先)
const ETHERNET = ["以太网", "ethernet", "eth", "enp", "ens", "eno", "bond", "team"];
for (const ifaceName of Object.keys(nets)) {
const lower = ifaceName.toLowerCase();
// 过滤虚拟网卡
if (VIRTUAL.some((k) => lower.includes(k.toLowerCase()))) continue;
for (const net of nets[ifaceName] || []) {
if (net.family !== "IPv4" || net.internal || !net.address) continue;
const addr = net.address;
// 只保留内网段 IP
const isPrivate =
/^192\.168\./.test(addr) ||
/^10\./.test(addr) ||
/^172\.(1[6-9]|2\d|3[01])\./.test(addr);
if (!isPrivate) continue;
// 基础分:以太网 +100,WiFi +50,其他 +10
let score = 10;
if (ETHERNET.some((k) => lower.includes(k.toLowerCase()))) score = 100;
else if (WIFI.some((k) => lower.includes(k.toLowerCase()))) score = 50;
candidates.push({ addr, score, iface: ifaceName });
}
}
if (candidates.length === 0) return "127.0.0.1";
candidates.sort((a, b) => b.score - a.score);
console.log("[RemoteHTTP] getLanIp 候选:", candidates.map((c) => `${c.addr}(${c.iface},score=${c.score})`).join(", "));
return candidates[0].addr;
} catch {
return "127.0.0.1";
}
}
let _server = null;
let _port = 8848;
let _token = null;
let _storage = null; // { readStorage, writeStorage }
let _onSettingsChanged = null; // 远程写完设置后通知主窗口
let _distDir = null; // 运行时解析的 dist 绝对路径
const STORAGE_KEY = "netcatty_remote_access";
// MIME 类型映射
const MIME = {
".html": "text/html; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".mjs": "application/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".ico": "image/x-icon",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".eot": "application/vnd.ms-fontobject",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".wasm": "application/wasm",
".map": "application/json; charset=utf-8",
};
function sendJson(res, code, data) {
res.writeHead(code, {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
"Access-Control-Allow-Origin": "*",
});
res.end(JSON.stringify(data));
}
function sendHtml(res, code, html) {
res.writeHead(code, {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-cache",
});
res.end(html);
}
function readBody(req) {
return new Promise((resolve, reject) => {
let buf = "";
req.on("data", (c) => (buf += c));
req.on("end", () => resolve(buf));
req.on("error", reject);
});
}
function parseQuery(urlStr) {
const q = {};
const idx = urlStr.indexOf("?");
if (idx < 0) return { pathname: urlStr, query: q };
const pathname = urlStr.slice(0, idx);
urlStr
.slice(idx + 1)
.split("&")
.forEach((pair) => {
const [k, v] = pair.split("=");
if (k) q[decodeURIComponent(k)] = v ? decodeURIComponent(v) : "";
});
return { pathname, query: q };
}
// ─── dist 路径解析 ────
function resolveDistDir() {
if (_distDir) return _distDir;
try {
const dist = path.join(__dirname, "../dist");
if (fs.existsSync(dist)) {
_distDir = dist;
return _distDir;
}
// 打包后可能在 app.asar 里
const dist2 = path.join(require("electron").app.getAppPath(), "dist");
if (fs.existsSync(dist2)) {
_distDir = dist2;
return _distDir;
}
} catch { /* 没有 electron 模块,可能在独立测试 */ }
// 最后 fallback:项目根 dist
_distDir = path.resolve(__dirname, "..", "dist");
return _distDir;
}
// ─── serve 静态文件 ────
function serveStatic(res, relPath) {
const distDir = resolveDistDir();
// 安全:禁止路径穿越
const safeRel = relPath.replace(/^\/+/, "").replace(/\.\.\//g, "");
const fullPath = path.join(distDir, safeRel);
// 确保文件在 dist 目录内
const real = path.resolve(fullPath);
if (!real.startsWith(path.resolve(distDir))) {
res.writeHead(403, { "Content-Type": "text/plain" });
res.end("Forbidden");
return;
}
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("Not Found");
return;
}
const ext = path.extname(fullPath).toLowerCase();
res.writeHead(200, {
"Content-Type": MIME[ext] || "application/octet-stream",
"Cache-Control": ext === ".html" ? "no-cache" : "public, max-age=3600",
// ES module script 要求 CORS 头(同域请求也需要)
"Access-Control-Allow-Origin": "*",
});
fs.createReadStream(fullPath).pipe(res);
}
// ─── 内部存储模块延迟加载 ────
function loadModules() {
const inspectionStorage = require("./bridges/inspection/inspectionStorage.cjs");
const deviceStore = require("./bridges/inspection/deviceStore.cjs");
const alertRecordStore = require("./bridges/inspection/alertRecordStore.cjs");
const inspectionResultStore = require("./bridges/inspection/inspectionResultStore.cjs");
const alertRuleStore = require("./bridges/inspection/alertRuleStore.cjs");
const reportStore = require("./bridges/inspection/reportStore.cjs");
const dailyReportStore = require("./bridges/inspection/dailyReportStore.cjs");
const reportScheduleStore = require("./bridges/inspection/reportScheduleStore.cjs");
// v1.3 预案管理新增
const planTemplateStore = require("./bridges/inspection/planTemplateStore.cjs");
const devicePlanStore = require("./bridges/inspection/devicePlanStore.cjs");
const commandCatalogStore = require("./bridges/inspection/commandCatalogStore.cjs");
const aggregationEngine = require("./bridges/inspection/aggregationEngine.cjs");
return { inspectionStorage, deviceStore, alertRecordStore, inspectionResultStore, alertRuleStore, reportStore, dailyReportStore, reportScheduleStore, planTemplateStore, devicePlanStore, commandCatalogStore, aggregationEngine };
}
// 每次 API 请求前重置所有 store 的模块级缓存,强制从磁盘重新加载
function resetAllCaches(modules) {
const m = modules || loadModules();
try { m.inspectionStorage.resetCache?.(); } catch {}
try { m.deviceStore._resetForTest?.(); } catch {}
try { m.alertRecordStore._resetForTest?.(); } catch {}
try { m.inspectionResultStore._resetForTest?.(); } catch {}
try { m.alertRuleStore._resetForTest?.(); } catch {}
try { m.reportStore._resetForTest?.(); } catch {}
try { m.dailyReportStore._resetForTest?.(); } catch {}
// v1.3 预案
try { m.planTemplateStore._resetForTest?.(); } catch {}
try { m.devicePlanStore._resetForTest?.(); } catch {}
try { m.commandCatalogStore._resetForTest?.(); } catch {}
}
/**
* 构建 host_id → { label, hostname } 映射,多来源合并
* 1. 主来源:_host_info_cache(inspection scheduler start 时从 vault hostConfigs 缓存)
* 2. Fallback:告警记录里的 device_name(巡检执行时写入,label 优先)
* 3. Fallback:巡检结果里的 device_name / hostname
*
* 这个函数接收 stores 以便在 cache 未就绪时兜底。
*/
function buildHostLabelMap(readStorage, stores = {}) {
const { alertRecordStore, inspectionResultStore } = stores;
const map = {};
// 0) _vault_hosts(前端 Electron 同步过来的完整 vault hosts,含 label/hostname/type/distro)
try {
const raw = readStorage("_vault_hosts");
if (raw && Array.isArray(raw)) {
for (const h of raw) {
if (h?.id) {
map[h.id] = {
label: h.label || "",
hostname: h.hostname || "",
type: h.type || "",
distro: h.distro || "",
};
}
}
}
} catch {}
// 1) _host_info_cache(scheduler start 时构建,可能 label 空)
try {
const raw = readStorage("_host_info_cache");
if (raw) {
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
if (parsed && typeof parsed === "object") {
for (const [id, info] of Object.entries(parsed)) {
if (!map[id]) {
map[id] = { label: "", hostname: "" };
}
if (info?.label && !map[id].label) map[id].label = info.label;
if (info?.hostname && !map[id].hostname) map[id].hostname = info.hostname;
}
}
}
} catch {}
// 2) 告警记录里反查 device_id → device_name(label)
if (alertRecordStore) {
try {
const alerts = alertRecordStore.listAlerts(readStorage, { limit: 500 });
for (const a of alerts || []) {
if (a?.device_id && a?.device_name && !map[a.device_id]?.label) {
map[a.device_id] = map[a.device_id] || {};
map[a.device_id].label = a.device_name;
}
}
} catch {}
}
// 3) 巡检结果里反查(有些 result 也带 device_name)
if (inspectionResultStore) {
try {
const all = inspectionResultStore.getLatestAll(readStorage);
for (const r of all || []) {
if (!r?.device_id) continue;
map[r.device_id] = map[r.device_id] || {};
if (r.device_name && !map[r.device_id].label) {
map[r.device_id].label = r.device_name;
}
if (r.hostname && !map[r.device_id].hostname) {
map[r.device_id].hostname = r.hostname;
}
}
} catch {}
}
return map;
}
function readStorage(key) {
try {
const raw = _storage?.readStorage?.(key);
if (raw === null || raw === undefined) return null;
// 原样返回磁盘上的值:可能是 string(JSON 字符串)、对象、数组
// 不再额外 JSON.stringify——让下游 store 自己决定要不要 parse
return raw;
} catch {
return null;
}
}
// ─── 路由 ────
async function handleApi(pathname, method, body, query) {
// /api/settings 路由不依赖 modules,先快速处理
if (pathname === "/api/settings") {
console.log("[RemoteHTTP]", method, "/api/settings", "body:", typeof body === "string" ? body?.substring(0, 200) : JSON.stringify(body));
try {
if (method === "GET") {
const all = _storage?.readStorage?.("__settings__all__") || {};
let parsed = {};
try { parsed = typeof all === "string" ? JSON.parse(all) : (all || {}); } catch {}
return { ok: true, settings: parsed };
}
if (method === "POST") {
let parsedBody;
try { parsedBody = JSON.parse(body || "{}"); } catch (e) {
console.warn("[RemoteHTTP] POST /api/settings bad JSON:", body);
return { ok: false, error: "invalid JSON: " + e.message };
}
const entries = parsedBody.entries || parsedBody;
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
console.warn("[RemoteHTTP] POST /api/settings bad entries:", typeof entries);
return { ok: false, error: "body must be object of entries" };
}
let current = {};
try {
const raw = _storage?.readStorage?.("__settings__all__");
current = raw ? (typeof raw === "string" ? JSON.parse(raw) : raw) : {};
} catch {}
const merged = { ...current, ...entries };
try { _storage?.writeStorage?.("__settings__all__", JSON.stringify(merged)); } catch (e) {
console.warn("[RemoteHTTP] POST /api/settings writeStorage error:", e.message);
}
try { _onSettingsChanged?.(); } catch {}
console.log("[RemoteHTTP] POST /api/settings OK, keys:", Object.keys(entries).length);
return { ok: true, merged };
}
if (method === "DELETE") {
let parsedBody;
try { parsedBody = JSON.parse(body || "{}"); } catch { return { ok: false, error: "invalid JSON" }; }
const keys = Array.isArray(parsedBody.keys) ? parsedBody.keys : [];
if (!keys.length) return { ok: false, error: "keys array required" };
let current = {};
try {
const raw = _storage?.readStorage?.("__settings__all__");
current = raw ? (typeof raw === "string" ? JSON.parse(raw) : raw) : {};
} catch {}
for (const k of keys) delete current[k];
try { _storage?.writeStorage?.("__settings__all__", JSON.stringify(current)); } catch {}
try { _onSettingsChanged?.(); } catch {}
return { ok: true, remaining: current };
}
return { ok: false, error: "method not allowed" };
} catch (e) {
console.error("[RemoteHTTP] /api/settings error:", e);
return { ok: false, error: String(e?.message || e) };
}
}
const modules = loadModules();
// 每次请求前重置所有 store 缓存,确保读到磁盘最新数据
resetAllCaches(modules);
const { deviceStore, alertRecordStore, inspectionResultStore, alertRuleStore } = modules;
// GET /api/status —— 全局概览
if (method === "GET" && pathname === "/api/status") {
const extMap = deviceStore.getAllExtensions(readStorage);
const extArr = Object.values(extMap);
const enabled = extArr.filter((e) => e.inspection_enabled).length;
const disabled = extArr.length - enabled;
const latestAll = inspectionResultStore.getLatestAll(readStorage);
const okCnt = latestAll.filter((r) => r?.ok).length;
const failCnt = latestAll.filter((r) => r && !r.ok).length;
const alerts = alertRecordStore.listAlerts(readStorage, {
status: "active",
limit: 200,
});
const critical = alerts.filter((a) => a.level === "critical" || a.level === 3).length;
const warning = alerts.filter((a) => a.level === "warning" || a.level === 2).length;
const info = alerts.filter((a) => a.level === "info" || a.level === 1).length;
return {
ok: true,
timestamp: new Date().toISOString(),
devices: { total: extArr.length, enabled, disabled },
latestScan: { total: latestAll.length, ok: okCnt, fail: failCnt },
alerts: { total: alerts.length, critical, warning, info },
};
}
// GET /api/devices —— 设备列表 + 巡检状态
if (method === "GET" && pathname === "/api/devices") {
const extMap = deviceStore.getAllExtensions(readStorage);
const hostMap = buildHostLabelMap(readStorage, { alertRecordStore, inspectionResultStore });
const latestMap = {};
const all = inspectionResultStore.getLatestAll(readStorage);
for (const r of all) if (r?.device_id) latestMap[r.device_id] = r;
// catalog_id → 业务类型(switch/server/firewall)
function catalogTypeToDeviceType(catalogId) {
if (!catalogId) return "server";
if (/switch/i.test(catalogId)) return "switch";
if (/firewall/i.test(catalogId)) return "firewall";
if (/router/i.test(catalogId)) return "router";
return "server";
}
const list = Object.values(extMap).map((ext) => {
const r = latestMap[ext.host_id];
const hi = hostMap[ext.host_id] || {};
const label = hi.label || "";
const hostname = hi.hostname || ext.hostname || "";
const name = label || hostname || ext.host_id;
const catalogType = ext.catalog_id || "";
const deviceType = catalogTypeToDeviceType(catalogType);
return {
host_id: ext.host_id,
label,
hostname,
name,
catalog_id: catalogType,
catalog_type: catalogType, // 原始 catalog id(huawei_h3c_switch/linux 等)
distro: catalogType, // 让 classifyHostType 通过 distro 字段能匹配到
type: deviceType, // switch/server/firewall/router
catalog: catalogType,
tier: ext.tier,
inspection_enabled: ext.inspection_enabled ?? null,
inspection_interval: ext.inspection_interval ?? null,
interval_minutes: ext.inspection_interval || 30,
last_scan_at: ext.last_scan_at || r?.scanned_at || null,
last_ok: r?.ok ?? null,
last_error: r?.ok === false ? (r?.error || "") : null,
metrics: r?.metrics || {},
online: r?.online ?? true,
};
});
return { ok: true, devices: list };
}
// GET /api/alerts —— 活跃告警列表
if (method === "GET" && pathname === "/api/alerts") {
const alerts = alertRecordStore.listAlerts(readStorage, {
status: "active",
limit: 200,
});
return { ok: true, alerts };
}
// GET /api/alert-rules —— 告警规则列表
if (method === "GET" && pathname === "/api/alert-rules") {
const rules = alertRuleStore.listRules(readStorage);
return { ok: true, rules };
}
// POST /api/alert/:id/acknowledge —— 确认告警
const alertActionMatch = pathname.match(/^\/api\/alert\/([\w-]+)\/(acknowledge|resolve|close|note)$/);
if (method === "POST" && alertActionMatch) {
const alertId = alertActionMatch[1];
const action = alertActionMatch[2];
let payload;
try { payload = JSON.parse(body || "{}"); } catch { return { ok: false, error: "invalid JSON body" }; }
const writeFn = (k, v) => _storage?.writeStorage?.(k, v);
try {
let result;
if (action === "acknowledge") {
result = alertRecordStore.acknowledgeAlert(readStorage, writeFn, alertId, payload.operator || "remote");
} else if (action === "resolve") {
result = alertRecordStore.resolveAlert(readStorage, writeFn, alertId, payload.reason || "manual", payload.operator || "remote");
} else if (action === "close") {
result = alertRecordStore.closeAlert(readStorage, writeFn, alertId, payload.note || "");
} else if (action === "note") {
result = alertRecordStore.updateNote(readStorage, writeFn, alertId, payload.note || "");
}
return { ok: true, alert: result };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// GET /api/inspection/results —— 所有设备最新巡检结果(完整结构,monitoring 页面用)
if (method === "GET" && pathname === "/api/inspection/results") {
const all = inspectionResultStore.getLatestAll(readStorage);
return { ok: true, results: Object.values(all) };
}
// GET /api/inspection/results/recent —— 最近巡检结果(支持 query.limit,默认 500 覆盖 7 天历史)
if (method === "GET" && pathname === "/api/inspection/results/recent") {
const limit = Math.min(parseInt(query.limit, 10) || 500, 5000);
const all = inspectionResultStore.getRecent(readStorage, limit);
return { ok: true, results: all };
}
// GET /api/inspection/results/range —— 按时间范围查询巡检历史(工作台柱状图用)
// 查询参数: start=timestamp&end=timestamp
if (method === "GET" && pathname === "/api/inspection/results/range") {
const start = parseInt(query.start, 10);
const end = parseInt(query.end, 10);
if (!start || !end || start >= end) {
return { ok: false, error: "Invalid start/end" };
}
const results = inspectionResultStore.getByTimeRange(readStorage, start, end);
return { ok: true, results };
}
// ════════════════════════════════════════════════════════
// v1.3 预案管理路由(远程浏览器访问)
// ════════════════════════════════════════════════════════
// GET /api/plan-templates —— 预案模板列表
if (method === "GET" && pathname === "/api/plan-templates") {
return { ok: true, templates: planTemplateStore.listTemplates(readStorage) };
}
// GET /api/device-plans —— 设备预案列表(带可选 filter 查询参数)
if (method === "GET" && pathname === "/api/device-plans") {
const filters = {};
if (query.status) filters.status = query.status;
if (query.level) filters.level = query.level;
if (query.device_type) filters.device_type = query.device_type;
return { ok: true, plans: devicePlanStore.listPlans(readStorage, filters) };
}
// GET /api/device-plans/:id —— 单条预案详情
const planMatch = pathname.match(/^\/api\/device-plans\/([^/]+)$/);
if (method === "GET" && planMatch) {
const plan = devicePlanStore.getPlan(readStorage, decodeURIComponent(planMatch[1]));
if (!plan) return { ok: false, error: "Plan not found" };
return { ok: true, plan };
}
// GET /api/command-catalog —— 预置命令目录
if (method === "GET" && pathname === "/api/command-catalog") {
return { ok: true, commands: commandCatalogStore.listCommands(readStorage) };
}
// GET /api/aggregation-rules —— 聚合规则
if (method === "GET" && pathname === "/api/aggregation-rules") {
return { ok: true, rules: aggregationEngine.listRules(readStorage) };
}
// GET /api/devices/enabled —— 已启用巡检的设备列表
if (method === "GET" && pathname === "/api/devices/enabled") {
const enabled = deviceStore.getEnabledDevices(readStorage);
return { ok: true, devices: enabled };
}
// GET /api/topology-positions —— Electron 拖拽保存的拓扑位置(远程浏览器同步)
// 直接读磁盘,绕过 inspectionStorage 内存缓存
if (method === "GET" && pathname === "/api/topology-positions") {
try {
const fs = require("fs");
const path = require("path");
const app = require("electron").app;
const storeFile = path.join(app.getPath("userData"), "inspection-data.json");
if (!fs.existsSync(storeFile)) return { ok: true, positions: {} };
const all = JSON.parse(fs.readFileSync(storeFile, "utf-8") || "{}");
return { ok: true, positions: all._topology_positions || {} };
} catch (e) {
return { ok: true, positions: {} };
}
}
// GET /api/dashboard-title —— 监控大屏标题 + 产品名 + 报告产品名(远程浏览器同步)
if (method === "GET" && pathname === "/api/dashboard-title") {
try {
const { inspectionStorage } = loadModules();
return {
ok: true,
dashboardTitle: inspectionStorage.readStorage("_dashboard_title") || "",
productName: inspectionStorage.readStorage("_product_name") || "",
reportProductName: inspectionStorage.readStorage("_report_product_name") || "",
};
} catch {
return { ok: true, dashboardTitle: "", productName: "", reportProductName: "" };
}
}
// GET /api/topology-links —— 用户手动添加的连线
if (method === "GET" && pathname === "/api/topology-links") {
try {
const fs = require("fs");
const path = require("path");
const app = require("electron").app;
const storeFile = path.join(app.getPath("userData"), "inspection-data.json");
if (!fs.existsSync(storeFile)) return { ok: true, links: [] };
const all = JSON.parse(fs.readFileSync(storeFile, "utf-8") || "{}");
return { ok: true, links: all._topology_links || [] };
} catch {
return { ok: true, links: [] };
}
}
// GET /api/inspection/scheduler-status —— 调度器状态(简化版)
if (method === "GET" && pathname === "/api/inspection/scheduler-status") {
// 从 inspection-data.json 读调度器状态
try {
const storeFile = path.join(
require("electron").app.getPath("userData"),
"inspection-data.json"
);
if (fs.existsSync(storeFile)) {
const all = JSON.parse(fs.readFileSync(storeFile, "utf-8") || "{}");
const sched = all["inspection-scheduler"] || {};
return {
ok: true,
running: !!sched.enabled,
queue_size: 0,
active_scans: 0,
concurrency: sched.concurrency || 3,
};
}
} catch {}
return { ok: true, running: false, queue_size: 0, active_scans: 0, concurrency: 3 };
}
// GET /api/inspection/catalog —— 设备类型目录
if (method === "GET" && pathname === "/api/inspection/catalog") {
try {
const catalog = deviceStore.getCatalog();
return { ok: true, catalog };
} catch (e) {
console.warn("[RemoteHTTP] catalog error:", e.message);
return { ok: true, catalog: [] };
}
}
// GET /api/inspection/sys-config —— 系统配置
if (method === "GET" && pathname === "/api/inspection/sys-config") {
try {
const cfg = deviceStore.getSysConfig(readStorage);
return { ok: true, data: cfg };
} catch {
return { ok: true, data: {} };
}
}
// GET /api/inspection/labels —— 标签字典
if (method === "GET" && pathname === "/api/inspection/labels") {
return {
ok: true,
data: {
alert_levels: { critical: "严重", warning: "警告", info: "提示" },
device_tiers: { core: "核心", aggregation: "汇聚", access: "接入" },
inspection_status: { success: "成功", failed: "失败", timeout: "超时" },
alert_record_status: { active: "活跃", acknowledged: "已确认", resolved: "已解决", closed: "已关闭" },
operators: { ">": ">", "<": "<", "==": "=", "!=": "!=", ">=": ">=", "<=": "<=" },
device_catalog: {},
metrics: {},
},
};
}
// GET /api/alert-stats —— 告警统计
if (method === "GET" && pathname === "/api/alert-stats") {
const all = alertRecordStore.listAlerts(readStorage, { limit: 500 });
const active = all.filter((a) => a.status === "active");
const acknowledged = all.filter((a) => a.status === "acknowledged");
const resolved = all.filter((a) => a.status === "resolved");
const closed = all.filter((a) => a.status === "closed");
return {
ok: true,
data: {
total: all.length,
active: active.length,
active_critical: active.filter((a) => a.level === "critical").length,
active_warning: active.filter((a) => a.level === "warning").length,
active_info: active.filter((a) => a.level === "info").length,
acknowledged: acknowledged.length,
resolved: resolved.length,
closed: closed.length,
},
};
}
// GET /api/snapshots —— 巡检快照列表
if (method === "GET" && pathname === "/api/snapshots") {
try {
const storeFile = path.join(
require("electron").app.getPath("userData"),
"inspection-data.json"
);
if (!fs.existsSync(storeFile)) return { ok: true, snapshots: [] };
const all = JSON.parse(fs.readFileSync(storeFile, "utf-8") || "{}");
const snaps = (all.snapshots || []).slice(-30).reverse();
return { ok: true, snapshots: snaps };
} catch (e) {
return { ok: false, error: e.message, snapshots: [] };
}
}
// GET /api/report/snapshots —— 巡检报告快照列表(走 reportStore)
if (method === "GET" && pathname === "/api/report/snapshots") {
try {
const { listSnapshots } = require("./bridges/inspection/reportStore.cjs");
const list = listSnapshots(readStorage, 50);
return { ok: true, snapshots: list || [] };
} catch (e) {
return { ok: true, snapshots: [] };
}
}
// GET /api/report/snapshot-status —— 是否有快照在运行
if (method === "GET" && pathname === "/api/report/snapshot-status") {
return { ok: true, running: false }; // 远程只读场景不允许执行快照
}
// GET /api/report/schedule —— 报告调度配置
if (method === "GET" && pathname === "/api/report/schedule") {
try {
const { getSchedule } = require("./bridges/inspection/reportScheduleStore.cjs");
const cfg = getSchedule(readStorage);
return { ok: true, schedule: cfg || null };
} catch {
return { ok: true, schedule: null };
}
}
// GET /api/daily-reports —— 日运行报告列表
if (method === "GET" && pathname === "/api/daily-reports") {
try {
const { listDailyReports } = require("./bridges/inspection/dailyReportStore.cjs");
const list = listDailyReports(readStorage, 30);
return { ok: true, reports: list || [] };
} catch (e) {
return { ok: true, reports: [] };
}
}
// GET /api/monitoring —— 运行监控聚合数据(renderer fallback 用)
if (method === "GET" && pathname === "/api/monitoring") {
const extMap = deviceStore.getAllExtensions(readStorage);
const hostMap = buildHostLabelMap(readStorage, { alertRecordStore, inspectionResultStore });
const extArr = Object.values(extMap);
const latestAll = inspectionResultStore.getLatestAll(readStorage);
const latestMap = {};
for (const r of latestAll) if (r?.device_id) latestMap[r.device_id] = r;
const alerts = alertRecordStore.listAlerts(readStorage, {
status: "active",
limit: 200,
});
const rules = alertRuleStore.listRules(readStorage);
// 每台设备的 metrics(cpu_usage / memory_usage / online)和巡检状态
const devices = extArr.map((ext) => {
const r = latestMap[ext.host_id];
const m = r?.metrics || {};
const hi = hostMap[ext.host_id] || {};
return {
id: ext.host_id,
name: hi.label || hi.hostname || ext.hostname || ext.host_id,
catalog_id: ext.catalog_id,
tier: ext.tier,
inspection_enabled: !!ext.inspection_enabled,
last_scan_at: ext.last_scan_at || r?.timestamp || null,
online: r?.online ?? true,
cpu_usage: Number(m.cpu_usage ?? m.cpu_percent ?? m.cpu ?? 0),
memory_usage: Number(m.memory_usage ?? m.mem_percent ?? m.memory ?? 0),
status: r?.status || null,
};
});
return {
ok: true,
devices,
alerts,
rules,
timestamp: Date.now(),
};
}
// POST /api/devices/batch-enable
if (method === "POST" && pathname === "/api/devices/batch-enable") {
try {
const payload = JSON.parse(body || "{}");
const enabled = payload.enabled !== false;
const all = deviceStore.getAllExtensions(readStorage);
let count = 0;
for (const ext of Object.values(all)) {
deviceStore.upsertExtension(readStorage, (k, v) => _storage?.writeStorage?.(k, v), ext.host_id, {
inspection_enabled: enabled,
});
count++;
}
return { ok: true, count, enabled };
} catch (e) {
return { ok: false, error: e.message };
}
}
// GET /api/vault/:key —— Vault 子页面通用只读路由
// 支持 hosts/keys/proxies/port-forwards/snippets/notes/known-hosts/connection-logs/groups/identities
// 敏感数据(keys/identities/hosts)自动脱敏
const _VAULT_ROUTE_MAP = {
hosts: "_vault_hosts",
keys: "_vault_keys",
proxies: "_vault_proxies",
"port-forwards": "_vault_port_forwards",
snippets: "_vault_snippets",
notes: "_vault_notes",
"known-hosts": "_vault_known_hosts",
"connection-logs": "_vault_connection_logs",
groups: "_vault_groups",
identities: "_vault_identities",
};
const _SENSITIVE_FIELDS = new Set(["privateKeyContent", "privateKey", "passphrase", "password", "secret", "apiKey", "private_key", "pass_phrase"]);
function _stripSensitive(obj) {
if (obj === null || obj === undefined) return obj;
if (Array.isArray(obj)) return obj.map(_stripSensitive);
if (typeof obj === "object") {
const out = {};
for (const [k, v] of Object.entries(obj)) {
if (_SENSITIVE_FIELDS.has(k)) continue;
out[k] = _stripSensitive(v);
}
return out;
}
return obj;
}
const vaultMatch = pathname.match(/^\/api\/vault\/([\w-]+)$/);
if (method === "GET" && vaultMatch) {
const key = vaultMatch[1];
const storageKey = _VAULT_ROUTE_MAP[key];
if (!storageKey) return { ok: false, error: "unknown vault key" };
try {
resetAllCaches();
const raw = _storage?.readStorage?.(storageKey);
let data = raw;
if (typeof raw === "string") {
try { data = JSON.parse(raw); } catch { data = raw; }
}
// keys、identities 和 hosts 需要脱敏(hosts 里可能带 password)
if (key === "keys" || key === "identities" || key === "hosts") {
data = _stripSensitive(data);
}
return { ok: true, data: data ?? (Array.isArray(data) ? [] : {}) };
} catch (e) {
return { ok: false, error: e?.message };
}
}
// POST /api/alert-rules —— 批量保存告警规则(远程浏览器编辑用)
if (method === "POST" && pathname === "/api/alert-rules") {
let rules;
try { rules = JSON.parse(body || "[]"); } catch { return { ok: false, error: "invalid JSON body" }; }
if (!Array.isArray(rules)) return { ok: false, error: "rules must be an array" };
try {
alertRuleStore.saveRules(readStorage, (k, v) => _storage?.writeStorage?.(k, v), rules);
return { ok: true, count: rules.length };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// POST /api/alert-rule —— 单条 create/update/delete/toggle
// body: { action: 'create'|'update'|'delete'|'toggle', ...fields }
if (method === "POST" && pathname === "/api/alert-rule") {
let payload;
try { payload = JSON.parse(body || "{}"); } catch { return { ok: false, error: "invalid JSON body" }; }
const write = (k, v) => _storage?.writeStorage?.(k, v);
try {
if (payload.action === "create") {
const rule = alertRuleStore.createRule(readStorage, write, payload.data || payload);
return { ok: true, rule };
} else if (payload.action === "update") {
const rule = alertRuleStore.updateRule(readStorage, write, payload.id, payload.updates || payload.data);
return { ok: true, rule };
} else if (payload.action === "delete") {
const ok = alertRuleStore.deleteRule(readStorage, write, payload.id);
return { ok: !!ok };
} else if (payload.action === "toggle") {
const rule = alertRuleStore.toggleRule(readStorage, write, payload.id, !!payload.enabled);
return { ok: true, rule };
}
return { ok: false, error: "unknown action" };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// POST /api/report/schedule —— 保存报告调度配置
if (method === "POST" && pathname === "/api/report/schedule") {
let config;
try { config = JSON.parse(body || "{}"); } catch { return { ok: false, error: "invalid JSON body" }; }
try {
const { reportScheduleStore } = loadModules();
const merged = reportScheduleStore.saveSchedule((k, v) => _storage?.writeStorage?.(k, v), config);
return { ok: true, schedule: merged };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// POST /api/report/scheduler-toggle —— 开启/关闭自动调度
if (method === "POST" && pathname === "/api/report/scheduler-toggle") {
let payload;
try { payload = JSON.parse(body || "{}"); } catch { return { ok: false, error: "invalid JSON body" }; }
const enabled = payload.enabled !== false;
try {
const { reportScheduleStore, reportScheduler } = loadModules();
const current = reportScheduleStore.getSchedule(readStorage);
const merged = reportScheduleStore.saveSchedule((k, v) => _storage?.writeStorage?.(k, v), { ...current, enabled });
// 尝试触发调度器启停(远程模式下调度器可能不存在,try/catch 兜底)
try {
if (enabled && reportScheduler?.start) reportScheduler.start();
else if (!enabled && reportScheduler?.stop) reportScheduler.stop();
} catch (_) {}
return { ok: true, running: enabled, schedule: merged };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// GET /api/report/scheduler-status —— 调度器状态
if (method === "GET" && pathname === "/api/report/scheduler-status") {
try {
const { reportScheduleStore } = loadModules();
const schedule = reportScheduleStore.getSchedule(readStorage);
return { ok: true, running: !!schedule.enabled, schedule };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// POST /api/inspection/sys-config —— 更新系统配置
if (method === "POST" && pathname === "/api/inspection/sys-config") {
let updates;
try { updates = JSON.parse(body || "{}"); } catch { return { ok: false, error: "invalid JSON body" }; }
try {
const { deviceStore } = loadModules();
deviceStore.updateSysConfig(readStorage, (k, v) => _storage?.writeStorage?.(k, v), updates);
return { ok: true };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// POST /api/devices/:hostId/enabled —— 单台设备巡检开关
const enableMatch = pathname.match(/^\/api\/devices\/([\w-]+)\/enabled$/);
if (method === "POST" && enableMatch) {
const hostId = enableMatch[1];
let enabled = true;
try { enabled = JSON.parse(body || "{}").enabled !== false; } catch {}
try {
const { deviceStore } = loadModules();
deviceStore.upsertExtension(readStorage, (k, v) => _storage?.writeStorage?.(k, v), hostId, { inspection_enabled: enabled });
return { ok: true, hostId, enabled };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// GET /api/report-preview —— 远程浏览器动态生成 PDF 并返回流
// query: mode=snapshot|daily&id=<snapshotId|reportId>
if (method === "GET" && pathname === "/api/report-preview") {
const mode = query.mode;
const id = query.id;
if (!mode || !id) return { ok: false, error: "mode and id required" };
try {
const { app } = require("electron");
const reportStore = require("./bridges/inspection/reportStore.cjs");
const dailyReportStore = require("./bridges/inspection/dailyReportStore.cjs");
const reportExport = require("./bridges/inspection/reportExport.cjs");
const fs = require("fs");
const path = require("path");
const inspectionStorage = require("./bridges/inspection/inspectionStorage.cjs");
// 读产品名
const storageFile = path.join(app.getPath("userData"), "inspection-data.json");
let systemName = "NetMesh 巡检报告";
try {
const all = JSON.parse(fs.readFileSync(storageFile, "utf-8") || "{}");
systemName = all._product_name || all._dashboard_title || systemName;
} catch {}
let filePath;
if (mode === "snapshot") {
reportStore._resetForTest?.();
const snap = reportStore.getSnapshot(readStorage, id);
if (!snap) return { ok: false, error: "快照不存在" };
filePath = await reportExport.exportSnapshotPdf(app, snap, snap.ai_summary || undefined, null, systemName);
} else if (mode === "daily") {
dailyReportStore._resetForTest?.();
const allReports = dailyReportStore.listDailyReports(readStorage, 500);
const report = (allReports || []).find((r) => r?.id === id);
if (!report) return { ok: false, error: "日报告不存在" };
reportStore._resetForTest?.();
const allSnapshots = reportStore.listSnapshots(readStorage, 500);
const pad = (n) => n.toString().padStart(2, "0");
const daySnaps = (allSnapshots || []).filter((s) => {
const d = new Date(s.timestamp);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` === report.date;
});
filePath = await reportExport.exportDailyReportPdf(app, report, null, daySnaps, systemName);
} else {
return { ok: false, error: "未知 mode" };
}
if (!fs.existsSync(filePath)) return { ok: false, error: "PDF 生成失败" };
const stat = fs.statSync(filePath);
const content = fs.readFileSync(filePath);
// 异步清理临时 PDF(30 秒后)
setTimeout(() => { try { fs.unlinkSync(filePath); } catch {} }, 30000);
return {
__raw: true,
writeHead: (res) => {
res.writeHead(200, {
"Content-Type": "application/pdf",
"Content-Length": stat.size,
"Cache-Control": "no-cache",
"Content-Disposition": `inline; filename="${path.basename(filePath)}"`,
});
},
end: (res) => res.end(content),
};
} catch (e) {
console.error("[report-preview] 失败:", e.message);
return { ok: false, error: e.message };
}
}
// POST /api/run-report-ai-summary —— 巡检报告 AI 汇总(异步 setImmediate 执行)
if (method === "POST" && pathname === "/api/run-report-ai-summary") {
let snapshotId, providerId;
try {
const bodyObj = typeof body === "string" ? JSON.parse(body || "{}") : (body || {});
snapshotId = bodyObj.snapshotId;
providerId = bodyObj.providerId;
} catch {}
if (!snapshotId) return { ok: false, error: "snapshotId required" };
try {
// 触发方式:让已注册的 IPC handler 自己被调用
// index.cjs 里 registerInspectionHandlers 注册了 "inspection:runReportAiSummary"
// 我们无法直接 require 到内部函数,但可以用 event sender = null 来触发 handler
setImmediate(async () => {
try {
const { ipcMain } = require("electron");
// ipcMain handlers 不支持 emit 触发,改用 direct call
// 直接 require 并调用 reportStore + aiBridge 自行实现
const reportStore = require("./bridges/inspection/reportStore.cjs");
const bridge = require("./bridges/aiBridge.cjs");
if (!bridge.providerConfigs?.length) {
console.warn("[RemoteHTTP] run-report-ai-summary: 无 AI provider 配置");
return;
}
const readStorageFn = (k) => { try { return _storage?.readStorage?.(k); } catch { return null; } };
const writeStorageFn = (k, v) => { try { _storage?.writeStorage?.(k, v); } catch {} };
const snap = reportStore.getSnapshot(readStorageFn, snapshotId);
if (!snap) { console.warn("[RemoteHTTP] snapshot 不存在"); return; }
reportStore.setSnapshotAiStatus(readStorageFn, writeStorageFn, snapshotId, "running");
const activeProviders = bridge.providerConfigs;
const provider = activeProviders.find(p => p.id === providerId)
|| activeProviders.find(p => p.enabled !== false);
if (!provider) throw new Error("AI provider 未配置");
const resolved = bridge.resolveProviderApiKey(provider.id);
if (!resolved?.apiKey) throw new Error("AI provider API key 未解密");
// 压缩构建 prompt —— 复用 index.cjs 里的 buildSnapshotPrompt
// 但它在内部没导出,简单手动构建:
const compactDevices = (snap.devices || []).map(d => {
const cmds = (d.commands || [])
.filter(c => !c.ok) // 只留失败命令
.concat((d.commands || []).filter(c => c.ok && /cpu|mem|disk|uptime/i.test(c.command)).slice(0, 5))
.map(c => ({ cmd: c.command?.slice(0, 40) || "", ok: c.ok, out: (c.stdout || c.stderr || "").slice(0, 200) }));
return { name: d.label || d.hostname || d.host_id, type: d.device_type || d.catalog_id, ip: d.hostname, cmds };
});
const prompt = JSON.stringify({
system: "你是网络运维专家。分析以下设备巡检数据,输出 JSON:{health_summary, offline_devices[], hidden_risks[], recommendations[]}。禁止输出外网URL、厂商文档链接、外部工具推荐。",
data: { devices: compactDevices, time: new Date(snap.created_at).toISOString() }
});
const url = provider.endpoint || "https://api.minimaxi.com/v1/chat/completions";
const model = provider.model || "MiniMax-M1";
const headers = { "Content-Type": "application/json", "Authorization": `Bearer ${resolved.apiKey}` };
const bodyStr = JSON.stringify({ model, messages: [{ role: "user", content: prompt }], temperature: 0.3, max_tokens: 1500 });
// HTTP fetch
const http = require("http"), https = require("https");
const lib = url.startsWith("https") ? https : http;
const u = new URL(url);
const hreq = lib.request({ hostname: u.hostname, port: u.port || (u.protocol === "https:" ? 443 : 80), path: u.pathname + u.search, method: "POST", headers }, hres => {
let chunks = []; hres.on("data", c => chunks.push(c)); hres.on("end", () => {
try {
const resp = JSON.parse(Buffer.concat(chunks).toString());
const text = resp.choices?.[0]?.message?.content || "";
const js = JSON.parse(text.replace(/```json|```/g, "").trim());
reportStore.updateSnapshotAiSummary(readStorageFn, writeStorageFn, snapshotId, js);
reportStore.setSnapshotAiStatus(readStorageFn, writeStorageFn, snapshotId, "done");
console.log("[RemoteHTTP] AI 汇总完成");
} catch (e) {
reportStore.updateSnapshotAiSummary(readStorageFn, writeStorageFn, snapshotId, null, e.message);
reportStore.setSnapshotAiStatus(readStorageFn, writeStorageFn, snapshotId, "failed", e.message);
console.error("[RemoteHTTP] AI 解析失败:", e.message);
}
});
});
hreq.on("error", e => {
reportStore.setSnapshotAiStatus(readStorageFn, writeStorageFn, snapshotId, "failed", e.message);
console.error("[RemoteHTTP] AI HTTP 错误:", e.message);
});
hreq.write(bodyStr);
hreq.end();
} catch (e) {
console.error("[RemoteHTTP] run-report-ai-summary:", e?.message || String(e));
}
});
return { ok: true };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// POST /api/generate-daily-report —— 日运行报告生成(同步)
if (method === "POST" && pathname === "/api/generate-daily-report") {
let mode = "provisional", trigger = "manual";
try {
const bodyObj = typeof body === "string" ? JSON.parse(body || "{}") : (body || {});
if (bodyObj.mode) mode = bodyObj.mode;
if (bodyObj.trigger) trigger = bodyObj.trigger;
} catch {}
try {
const reportScheduler = require("./bridges/inspection/reportScheduler.cjs");
const writeStorage = (k, v) => {
try { require("./bridges/inspection/inspectionStorage.cjs").setItem?.(k, v); } catch {}
};
const report = await reportScheduler.triggerDailyReport(readStorage, writeStorage, { mode, trigger });
return { ok: true, report };
} catch (e) {
return { ok: false, error: e?.message || String(e) };
}
}
// POST /api/run-alert-rca —— 远程告警中心 AI 根因分析
// 委托给主进程完整 RCA:Phase 1 AI 规划 + SSH 诊断 + Phase 2 AI 综合
// 用主进程内部的 _hostConfigs 直接拿 SSH 凭据,不需要 renderer 解密
if (method === "POST" && pathname === "/api/run-alert-rca") {
let alertId;
try {
const bodyObj = typeof body === "string" ? JSON.parse(body || "{}") : (body || {});
alertId = bodyObj.alertId;
} catch {}
if (!alertId) return { ok: false, error: "alertId required" };
try {
resetAllCaches();
const inspectionIndex = require("./bridges/inspection/index.cjs");
if (!inspectionIndex.runAlertRCA) {
return { ok: false, error: "RCA handler 未初始化(需重启 Electron 主进程)" };
}
console.log("[RemoteHTTP] 委托主进程执行完整 RCA, alertId:", alertId);
const result = await inspectionIndex.runAlertRCA(alertId);
console.log("[RemoteHTTP] RCA 完成, success:", result?.success);
if (result?.success) {
const r = result.rca || {};
return {
ok: true,
rca: {
summary: r.summary,
confidence: r.confidence,
rootCauses: r.rootCauses,
evidence: r.evidence,
recommendations: r.recommendations,
},
durationMs: result.durationMs,
};
}
return { ok: false, error: result?.error || "RCA 执行失败" };
} catch (e) {
console.error("[RemoteHTTP] RCA 异常:", e.message);
return { ok: false, error: e?.message || String(e) };
}
}
return { ok: false, error: "Not found" };
}
// ─── 主入口 ────
async function start(options) {
_storage = options?.storage;
_onSettingsChanged = options?.onSettingsChanged;
// 读配置
let enabled = false;
let port = 8848;
let savedToken = null;
try {
const raw = _storage?.readStorage?.(STORAGE_KEY);
if (raw) {
const cfg = JSON.parse(raw);
enabled = !!cfg.enabled;
port = Number(cfg.port) || 8848;
savedToken = cfg.token || null;
}
} catch {}
if (!enabled) {
console.log("[RemoteHTTP] 远程访问未启用,跳过启动");
return { started: false };
}
if (_server) {
console.log("[RemoteHTTP] 已在运行,端口", _port);
return { started: true, port: _port, token: _token };
}
_token = savedToken || crypto.randomBytes(12).toString("hex");
_port = port;
// 启动前持久化 token(如果是新生成的)
if (!savedToken) {
try {
_storage?.writeStorage?.(STORAGE_KEY, JSON.stringify({ enabled: true, port: _port, token: _token }));
} catch {}
}
// 校验 dist 是否存在
const distDir = resolveDistDir();
const indexHtml = path.join(distDir, "index.html");
if (!fs.existsSync(indexHtml)) {
console.warn("[RemoteHTTP] dist/index.html 不存在,远程访问页面将不可用。尝试路径:", distDir);
}
_server = http.createServer(async (req, res) => {
const { pathname, query } = parseQuery(req.url);
// favicon.ico — 浏览器自动请求,返回 204 空响应避免 404
if (pathname === "/favicon.ico") {
res.writeHead(204);
res.end();
return;
}
// /api/* —— 要 token
if (pathname.startsWith("/api/")) {
if (query.token !== _token) {
sendJson(res, 401, { ok: false, error: "Unauthorized" });
return;
}
try {
const body = await readBody(req);
const result = await handleApi(pathname, req.method || "GET", body, query);
if (result?.__raw) {
result.writeHead(res);
result.end(res);
} else {
sendJson(res, result.ok ? 200 : (result.error === "Not found" ? 404 : 400), result);
}
} catch (e) {
console.error("[RemoteHTTP] API error:", e);
sendJson(res, 500, { ok: false, error: e.message });
}
return;
}
// 静态入口(/、/index.html、任意 SPA fallback)—— 要 token,且注入 window.__REMOTE_TOKEN__
// 静态资源 /assets/* /monaco/* 不强制 token(已经在 index.html 里,token 校验在入口做)
const isAssetPath =
pathname.startsWith("/assets/") ||
pathname.startsWith("/monaco/") ||
pathname.startsWith("/fonts/") ||
pathname.startsWith("/images/") ||
pathname.startsWith("/public/") ||
pathname.endsWith(".png") ||
pathname.endsWith(".jpg") ||
pathname.endsWith(".jpeg") ||
pathname.endsWith(".svg") ||
pathname.endsWith(".ico") ||
pathname.endsWith(".woff") ||
pathname.endsWith(".woff2") ||
pathname.endsWith(".ttf") ||
pathname.endsWith(".eot") ||
pathname.endsWith(".mp3") ||
pathname.endsWith(".wasm") ||
pathname.endsWith(".map");
if (isAssetPath) {
serveStatic(res, pathname);
return;
}
// SPA 入口 —— 所有非API、非资源的路径都 serve index.html + token 注入
if (query.token !== _token) {
sendHtml(res, 200,
'<!DOCTYPE html><html><head><meta charset="utf-8"><title>NetMesh</title></head>' +
'<body style="padding:40px;font-family:-apple-system,sans-serif;color:#333;">' +
'<h1>NetMesh 远程访问</h1>' +
'<p style="color:#c00;">缺少或错误的 token 参数</p>' +
'<p>请从 NetMesh 设置页获取完整 URL</p>' +
'</body></html>'
);
return;
}
// 读 dist/index.html 并在 </head> 前注入 token + 同步设置预加载
// 同步 XHR 确保 React SPA 启动前 localStorage 已有权威设置值
try {
const htmlPath = path.join(resolveDistDir(), "index.html");
let html = fs.readFileSync(htmlPath, "utf-8");
const inject = `<script>window.__REMOTE_TOKEN__=${JSON.stringify(_token)};window.__REMOTE_BASE__='/';(function(){try{var x=new XMLHttpRequest();x.open('GET','/api/settings?token='+encodeURIComponent(window.__REMOTE_TOKEN__),false);x.send();if(x.status===200){var j=JSON.parse(x.responseText);if(j.ok&&j.settings){for(var k in j.settings){if(j.settings[k]!==null&&j.settings[k]!==undefined){localStorage.setItem(k,String(j.settings[k]));}}}}}catch(e){}var dirtyKeys={};var _si=localStorage.setItem.bind(localStorage);var _ri=localStorage.removeItem.bind(localStorage);localStorage.setItem=function(k,v){var old=localStorage.getItem(k);_si(k,String(v));dirtyKeys[k]=Date.now();if(old!==String(v)){try{globalThis.__NETCATTY_SETTINGS_SYNC__&&globalThis.__NETCATTY_SETTINGS_SYNC__(k,String(v));}catch(e){}}};localStorage.removeItem=function(k){dirtyKeys[k]=Date.now();_ri(k);try{globalThis.__NETCATTY_SETTINGS_SYNC__&&globalThis.__NETCATTY_SETTINGS_SYNC__(k,null);}catch(e){}};(function poll(){try{var x=new XMLHttpRequest();x.open('GET','/api/settings?token='+encodeURIComponent(window.__REMOTE_TOKEN__),true);x.onload=function(){if(x.status===200){var j=JSON.parse(x.responseText);if(j.ok&&j.settings){var now=Date.now();for(var k in j.settings){var authVal=j.settings[k];var curVal=localStorage.getItem(k);if(authVal!==null&&authVal!==undefined){if(curVal!==String(authVal)){var dirty=dirtyKeys[k];if(!dirty||now-dirty>2000){_si(k,String(authVal));try{window.dispatchEvent(new StorageEvent('storage',{key:k,oldValue:curVal,newValue:String(authVal),url:location.href}));}catch(e){}}}}else{if(curVal!==null){var dirty=dirtyKeys[k];if(!dirty||now-dirty>2000){_ri(k);try{window.dispatchEvent(new StorageEvent('storage',{key:k,oldValue:curVal,newValue:null,url:location.href}));}catch(e){}}}}}}}};x.send();}catch(e){}setTimeout(poll,3000);})();})();</script>`;
html = html.replace("</head>", inject + "</head>");
sendHtml(res, 200, html);
} catch (e) {
sendHtml(res, 500,
'<h1>NetMesh 远程访问</h1><p style="color:#c00;">dist/index.html 加载失败: ' + e.message + '</p>'
);
}
});
return new Promise((resolve) => {
_server.listen(_port, "0.0.0.0", () => {
const lanIp = getLanIp();
const url = `http://${lanIp}:${_port}/?token=${_token}`;
console.log("[RemoteHTTP] ✅ 远程 HTTP 服务已启动");
console.log("[RemoteHTTP] 监听端口:", _port);
console.log("[RemoteHTTP] 本机 URL (localhost):", `http://localhost:${_port}/?token=${_token}`);
console.log("[RemoteHTTP] 局域网 URL:", url);
console.log("[RemoteHTTP] dist 目录:", resolveDistDir());
resolve({ started: true, port: _port, token: _token, url, lanIp });
});
_server.on("error", (err) => {
console.error("[RemoteHTTP] 启动失败:", err.message);
_server = null;
resolve({ started: false, error: err.message });
});
});
}
function stop() {
return new Promise((resolve) => {
if (!_server) return resolve({ stopped: false });
_server.close(() => {
console.log("[RemoteHTTP] 已停止");
_server = null;
resolve({ stopped: true });
});
});
}
function getStatus() {
const lanIp = _server ? getLanIp() : null;
return {
running: !!_server,
port: _port,
token: _token,
lanIp,
url: lanIp && _token ? `http://${lanIp}:${_port}/?token=${_token}` : null,
};
}
// IPC handler 设置:enable/disable + restart
async function configure(enabled, newPort) {
try {
// 读现有配置
let cfg = { enabled, port: newPort || 8848, token: null };
const raw = _storage?.readStorage?.(STORAGE_KEY);
if (raw) {
const old = JSON.parse(raw);
cfg.token = old.token || cfg.token;
cfg.port = newPort || old.port || 8848;
}
// 启用但 token 没生成 → 先生成 token 再启动
if (enabled && !cfg.token) cfg.token = crypto.randomBytes(12).toString("hex");
_storage?.writeStorage?.(STORAGE_KEY, JSON.stringify(cfg));
if (enabled) {
await stop();
const info = await start({ storage: _storage });
return { ok: true, ...info };
} else {
await stop();
return { ok: true, stopped: true };
}
} catch (e) {
return { ok: false, error: e.message };
}
}
module.exports = { start, stop, getStatus, configure };