feat: add monitoring settings tab, workbench view, and inspection alert improvements
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
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
This commit is contained in:
@@ -15,12 +15,63 @@
|
||||
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";
|
||||
@@ -147,7 +198,8 @@ function loadModules() {
|
||||
const alertRuleStore = require("./bridges/inspection/alertRuleStore.cjs");
|
||||
const reportStore = require("./bridges/inspection/reportStore.cjs");
|
||||
const dailyReportStore = require("./bridges/inspection/dailyReportStore.cjs");
|
||||
return { inspectionStorage, deviceStore, alertRecordStore, inspectionResultStore, alertRuleStore, reportStore, dailyReportStore };
|
||||
const reportScheduleStore = require("./bridges/inspection/reportScheduleStore.cjs");
|
||||
return { inspectionStorage, deviceStore, alertRecordStore, inspectionResultStore, alertRuleStore, reportStore, dailyReportStore, reportScheduleStore };
|
||||
}
|
||||
|
||||
// 每次 API 请求前重置所有 store 的模块级缓存,强制从磁盘重新加载
|
||||
@@ -255,6 +307,62 @@ function readStorage(key) {
|
||||
|
||||
// ─── 路由 ────
|
||||
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);
|
||||
@@ -349,18 +457,56 @@ async function handleApi(pathname, method, body, query) {
|
||||
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 —— 最近巡检结果
|
||||
// GET /api/inspection/results/recent —— 最近巡检结果(支持 query.limit,默认 500 覆盖 7 天历史)
|
||||
if (method === "GET" && pathname === "/api/inspection/results/recent") {
|
||||
const all = inspectionResultStore.getRecent(readStorage, 50);
|
||||
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 };
|
||||
}
|
||||
|
||||
// GET /api/devices/enabled —— 已启用巡检的设备列表
|
||||
if (method === "GET" && pathname === "/api/devices/enabled") {
|
||||
const enabled = deviceStore.getEnabledDevices(readStorage);
|
||||
@@ -613,9 +759,10 @@ async function handleApi(pathname, method, body, query) {
|
||||
}
|
||||
|
||||
// GET /api/vault/:key —— Vault 子页面通用只读路由
|
||||
// 支持 keys/proxies/port-forwards/snippets/notes/known-hosts/connection-logs/groups/identities
|
||||
// 敏感数据(keys/identities)自动脱敏
|
||||
// 支持 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",
|
||||
@@ -652,8 +799,8 @@ async function handleApi(pathname, method, body, query) {
|
||||
if (typeof raw === "string") {
|
||||
try { data = JSON.parse(raw); } catch { data = raw; }
|
||||
}
|
||||
// keys 和 identities 需要脱敏
|
||||
if (key === "keys" || key === "identities") {
|
||||
// keys、identities 和 hosts 需要脱敏(hosts 里可能带 password)
|
||||
if (key === "keys" || key === "identities" || key === "hosts") {
|
||||
data = _stripSensitive(data);
|
||||
}
|
||||
return { ok: true, data: data ?? (Array.isArray(data) ? [] : {}) };
|
||||
@@ -662,6 +809,117 @@ async function handleApi(pathname, method, body, query) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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") {
|
||||
@@ -694,11 +952,17 @@ async function handleApi(pathname, method, body, query) {
|
||||
filePath = await reportExport.exportSnapshotPdf(app, snap, snap.ai_summary || undefined, null, systemName);
|
||||
} else if (mode === "daily") {
|
||||
dailyReportStore._resetForTest?.();
|
||||
const report = dailyReportStore.getReport(readStorage, id);
|
||||
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);
|
||||
filePath = await reportExport.exportDailyReportPdf(app, report, null, allSnapshots, systemName);
|
||||
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" };
|
||||
}
|
||||
@@ -887,6 +1151,7 @@ async function handleApi(pathname, method, body, query) {
|
||||
// ─── 主入口 ────
|
||||
async function start(options) {
|
||||
_storage = options?.storage;
|
||||
_onSettingsChanged = options?.onSettingsChanged;
|
||||
|
||||
// 读配置
|
||||
let enabled = false;
|
||||
@@ -1000,12 +1265,12 @@ async function start(options) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 读 dist/index.html 并在 </head> 前注入 token(唯一一处,避免破坏其他标签)
|
||||
// 读 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__='/';</script>`;
|
||||
// 只注入一处:</head> 前最稳妥(Vite 模板里 </head> 只有一处)
|
||||
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) {
|
||||
@@ -1017,12 +1282,14 @@ async function start(options) {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
_server.listen(_port, "0.0.0.0", () => {
|
||||
const url = `http://0.0.0.0:${_port}/?token=${_token}`;
|
||||
const lanIp = getLanIp();
|
||||
const url = `http://${lanIp}:${_port}/?token=${_token}`;
|
||||
console.log("[RemoteHTTP] ✅ 远程 HTTP 服务已启动");
|
||||
console.log("[RemoteHTTP] 监听端口:", _port);
|
||||
console.log("[RemoteHTTP] 访问 URL:", url);
|
||||
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 });
|
||||
resolve({ started: true, port: _port, token: _token, url, lanIp });
|
||||
});
|
||||
_server.on("error", (err) => {
|
||||
console.error("[RemoteHTTP] 启动失败:", err.message);
|
||||
@@ -1044,10 +1311,13 @@ function stop() {
|
||||
}
|
||||
|
||||
function getStatus() {
|
||||
const lanIp = _server ? getLanIp() : null;
|
||||
return {
|
||||
running: !!_server,
|
||||
port: _port,
|
||||
token: _token,
|
||||
lanIp,
|
||||
url: lanIp && _token ? `http://${lanIp}:${_port}/?token=${_token}` : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user