"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= 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, 'NetMesh' + '' + '

NetMesh 远程访问

' + '

缺少或错误的 token 参数

' + '

请从 NetMesh 设置页获取完整 URL

' + '' ); return; } // 读 dist/index.html 并在 前注入 token + 同步设置预加载 // 同步 XHR 确保 React SPA 启动前 localStorage 已有权威设置值 try { const htmlPath = path.join(resolveDistDir(), "index.html"); let html = fs.readFileSync(htmlPath, "utf-8"); const inject = ``; html = html.replace("", inject + ""); sendHtml(res, 200, html); } catch (e) { sendHtml(res, 500, '

NetMesh 远程访问

dist/index.html 加载失败: ' + e.message + '

' ); } }); 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 };