This commit is contained in:
root
2026-05-17 04:31:59 +00:00
parent 75a0ca4e31
commit 07aa02bca6
51 changed files with 1322 additions and 435 deletions

View File

@@ -24,7 +24,7 @@ import {
Wifi,
X,
} from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LiveStudioScene3D } from "@/components/LiveStudioScene3D";
import OverviewCharts from "@/components/OverviewCharts";
@@ -148,6 +148,12 @@ type HubDashboard = {
type SrsProbe = NonNullable<HubDashboard["probes"]>["srs"];
type ProcessEntry = { pm2: string; script: string; label: string };
type ProcessMonitorRow = ProcessEntry & {
process_status?: string;
business_status?: string;
business_note?: string;
is_pushing?: boolean;
};
function formatUptime(sec?: number) {
if (sec == null || !Number.isFinite(sec)) return "—";
@@ -208,6 +214,7 @@ export default function LiveControlApp() {
const [toast, setToast] = useState<string | null>(null);
const [entries, setEntries] = useState<ProcessEntry[]>([]);
const [processRows, setProcessRows] = useState<ProcessMonitorRow[]>([]);
const [currentProc, setCurrentProc] = useState("");
const [urlConfig, setUrlConfig] = useState("");
const [apkPath, setApkPath] = useState("/sdcard/app.apk");
@@ -215,6 +222,8 @@ export default function LiveControlApp() {
const [hubCfgSnapshot, setHubCfgSnapshot] = useState<Record<string, unknown> | null>(null);
const [autoRefreshEnabled, setAutoRefreshEnabled] = useState(true);
const [showStreamProbes, setShowStreamProbes] = useState(true);
const dashboardInFlightRef = useRef<Promise<void> | null>(null);
const processMonitorInFlightRef = useRef<Promise<void> | null>(null);
const t = useCallback((zh: string, en: string) => tr(lang, zh, en), [lang]);
@@ -231,22 +240,58 @@ export default function LiveControlApp() {
);
const refreshDashboard = useCallback(async () => {
try {
const d = await fetchJson<HubDashboard>("/hub/dashboard");
setDash(d);
setDashErr(null);
const devs = d.android?.devices || [];
if (devs.length && !androidSerial) {
const ready = devs.find((x) => (x.state || "").toLowerCase() === "device");
setAndroidSerial((ready ?? devs[0]).serial);
if (dashboardInFlightRef.current) return dashboardInFlightRef.current;
const task = (async () => {
try {
const d = await fetchJson<HubDashboard>("/hub/dashboard");
setDash(d);
setDashErr(null);
const devs = d.android?.devices || [];
if (devs.length && !androidSerial) {
const ready = devs.find((x) => (x.state || "").toLowerCase() === "device");
setAndroidSerial((ready ?? devs[0]).serial);
}
} catch (e) {
setDashErr((e as Error).message);
} finally {
setLoading(false);
dashboardInFlightRef.current = null;
}
} catch (e) {
setDashErr((e as Error).message);
} finally {
setLoading(false);
}
})();
dashboardInFlightRef.current = task;
return task;
}, [androidSerial, fetchJson]);
const processMonitorPath = useMemo(() => {
const params = new URLSearchParams({ light: "1" });
if (view === "live_youtube") params.set("family", "youtube");
else if (view === "live_tiktok") params.set("family", "tiktok");
return `/process_monitor?${params.toString()}`;
}, [view]);
const refreshProcessMonitor = useCallback(async () => {
if (processMonitorInFlightRef.current) return processMonitorInFlightRef.current;
const task = (async () => {
try {
const data = await fetchJson<{ entries?: ProcessMonitorRow[] }>(processMonitorPath);
const list = data.entries || [];
setProcessRows(list);
setEntries(list.map((row) => ({ pm2: row.pm2, script: row.script, label: row.label })));
setCurrentProc((prev) => {
if (!list.length) return prev;
if (prev && list.some((row) => row.pm2 === prev)) return prev;
return list[0].pm2;
});
} catch {
/* ignore */
} finally {
processMonitorInFlightRef.current = null;
}
})();
processMonitorInFlightRef.current = task;
return task;
}, [fetchJson, processMonitorPath]);
useEffect(() => {
const s = window.localStorage.getItem("live-hub-lang");
if (s === "zh" || s === "en") setLang(s);
@@ -278,23 +323,27 @@ export default function LiveControlApp() {
useEffect(() => {
void refreshDashboard();
if (!autoRefreshEnabled) return;
const id = window.setInterval(() => void refreshDashboard(), 10000);
return () => clearInterval(id);
}, [refreshDashboard, autoRefreshEnabled]);
}, [refreshDashboard]);
useEffect(() => {
void (async () => {
try {
const data = await fetchJson<{ entries?: ProcessEntry[] }>("/process_monitor");
const list = data.entries || [];
setEntries(list);
if (!currentProc && list[0]) setCurrentProc(list[0].pm2);
} catch {
/* ignore */
}
})();
}, [currentProc, fetchJson]);
void refreshProcessMonitor();
}, [refreshProcessMonitor]);
useEffect(() => {
if (!autoRefreshEnabled || view === "live_youtube" || view === "live_tiktok") return;
const id = window.setInterval(() => {
void refreshDashboard();
}, 10000);
return () => clearInterval(id);
}, [autoRefreshEnabled, refreshDashboard, view]);
useEffect(() => {
if (!autoRefreshEnabled) return;
const id = window.setInterval(() => {
void refreshProcessMonitor();
}, view === "live_youtube" || view === "live_tiktok" ? 700 : 4000);
return () => clearInterval(id);
}, [autoRefreshEnabled, refreshProcessMonitor, view]);
useEffect(() => {
if (!currentProc || view !== "live_youtube") return;
@@ -305,7 +354,7 @@ export default function LiveControlApp() {
);
setUrlConfig(data.content || "");
} catch {
setUrlConfig("");
/* 保留当前草稿,避免临时读取失败把编辑器清空 */
}
})();
}, [currentProc, fetchJson, view]);
@@ -368,7 +417,12 @@ export default function LiveControlApp() {
body: JSON.stringify({ process: proc }),
});
notify(String(data.output || data.message || "OK"));
await refreshDashboard();
await refreshProcessMonitor();
if (view === "live_youtube" || view === "live_tiktok") {
void refreshDashboard();
} else {
await refreshDashboard();
}
} catch (e) {
notify((e as Error).message);
} finally {
@@ -376,12 +430,13 @@ export default function LiveControlApp() {
}
};
const saveUrlConfig = async (overrideContent?: string) => {
if (!currentProc) return;
const saveUrlConfig = async (overrideContent?: string, processOverride?: string) => {
const proc = (processOverride ?? currentProc).trim();
if (!proc) return;
const content = overrideContent ?? urlConfig;
setBusy("save:url");
try {
const params = new URLSearchParams({ process: currentProc });
const params = new URLSearchParams({ process: proc });
await fetchJson(`/save_url_config?${params}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -485,10 +540,10 @@ export default function LiveControlApp() {
</div>
<div>
<p className={`text-[10px] font-semibold uppercase tracking-[0.2em] ${portalTheme === "light" ? "text-slate-500" : "text-zinc-500"}`}>
Live Hub
live.local
</p>
<p className={`text-sm font-semibold ${portalTheme === "light" ? "text-slate-900" : "text-white"}`}>
{t("超级业务中心", "Control Center")}
{t("无人直播系统", "Unmanned Live System")}
</p>
</div>
</div>
@@ -606,7 +661,7 @@ export default function LiveControlApp() {
<button
type="button"
disabled={!!busy}
onClick={() => void refreshDashboard()}
onClick={() => void Promise.all([refreshDashboard(), refreshProcessMonitor()])}
className={`flex items-center gap-2 rounded-xl border px-3 py-2 text-xs font-medium transition disabled:opacity-40 ${
portalTheme === "light"
? "border-slate-200 bg-white text-slate-700 hover:bg-slate-50"
@@ -866,8 +921,8 @@ export default function LiveControlApp() {
</div>
<p className="mt-3 text-[10px] text-zinc-600">
{t(
"x86 默认使用 Neko 的 Chrome 镜像,ARM 默认使用 Chromium 镜像;本栈未启用 Firefox 版 Neko。",
"x86 defaults to Neko’s Chrome image, while ARM defaults to the Chromium image. This stack does not use the Firefox image.",
"x86 默认使用 Neko 的 Chrome 镜像,ARM 默认使用 Chromium 镜像;默认保留管理员会话、开发者工具和 Tampermonkey 持久化。本栈未启用 Firefox 版 Neko。",
"x86 defaults to Neko’s Chrome image, ARM defaults to Chromium, and admin access, DevTools, plus persistent Tampermonkey stay enabled. This stack does not use Firefox Neko.",
)}
</p>
</div>
@@ -1142,9 +1197,9 @@ export default function LiveControlApp() {
disabled={!!busy || !s.available || (s.service_id === "android_gateway" && s.status === "skipped")}
title={
s.service_id === "android_gateway" && s.status === "skipped"
? t("鍗曠綉鍙f棤闇€缃戝叧", "Not needed on single NIC")
? t("单网口无需网关", "Not needed on single NIC")
: !s.available
? s.detail || t("褰撳墠涓嶅彲鎿嶄綔", "Unavailable")
? s.detail || t("当前不可操作", "Unavailable")
: undefined
}
onClick={() => void runService(s.service_id, "install")}
@@ -1172,9 +1227,9 @@ export default function LiveControlApp() {
disabled={!!busy || !s.available || (s.service_id === "android_gateway" && s.status === "skipped")}
title={
s.service_id === "android_gateway" && s.status === "skipped"
? t("鍗曠綉鍙f棤闇€缃戝叧", "Not needed on single NIC")
? t("单网口无需网关", "Not needed on single NIC")
: !s.available
? s.detail || t("褰撳墠涓嶅彲鎿嶄綔", "Unavailable")
? s.detail || t("当前不可操作", "Unavailable")
: undefined
}
onClick={() => void runService(s.service_id, "upgrade")}
@@ -1202,9 +1257,9 @@ export default function LiveControlApp() {
disabled={!!busy || !s.available || (s.service_id === "android_gateway" && s.status === "skipped")}
title={
s.service_id === "android_gateway" && s.status === "skipped"
? t("鍗曠綉鍙f棤闇€缃戝叧", "Not needed on single NIC")
? t("单网口无需网关", "Not needed on single NIC")
: !s.available
? s.detail || t("褰撳墠涓嶅彲鎿嶄綔", "Unavailable")
? s.detail || t("当前不可操作", "Unavailable")
: undefined
}
onClick={() => void runService(s.service_id, "uninstall")}
@@ -1231,9 +1286,9 @@ export default function LiveControlApp() {
urlConfig={urlConfig}
setUrlConfig={setUrlConfig}
onRunProcess={(a, pm) => void runProcess(a, pm)}
onSaveUrlConfig={(c) => saveUrlConfig(c)}
onSaveUrlConfig={(c, pm) => saveUrlConfig(c, pm)}
fetchJson={fetchJson}
liveProcesses={dash?.live?.processes ?? []}
liveProcesses={processRows.length ? processRows : (dash?.live?.processes ?? [])}
notify={notify}
onNavigate={(target: PortalNavTarget) => setView(target)}
/>