From 8bce529255de9b7d87355dd5a613f677dcb4c4b9 Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 29 Mar 2026 01:17:00 -0500 Subject: [PATCH] 's' --- config/system-stack.env.example | 6 +- .../config/defaults/services/neko.json | 4 +- scripts/linux/lib/common.sh | 89 ++++- scripts/linux/service_ctl.sh | 8 +- src/control_plane.py | 8 +- .../console/LiveConsoleWorkspace.tsx | 282 +++++++++------- .../live-product/LiveControlApp.tsx | 291 ++++++++++++----- .../live-product/LiveProcessLiveLogCard.tsx | 71 ++-- .../live-product/LiveYoutubeUnmannedView.tsx | 303 +++++++++++++++--- web-console/lib/youtubeConfigUtils.ts | 13 + 10 files changed, 775 insertions(+), 300 deletions(-) diff --git a/config/system-stack.env.example b/config/system-stack.env.example index 0232679..a51bca9 100644 --- a/config/system-stack.env.example +++ b/config/system-stack.env.example @@ -60,9 +60,9 @@ DISABLE_IPV6=1 # 设为 1 才会安装/启动 Neko 容器;不需要浏览器桌面可改回 0 ENABLE_NEKO=1 NEKO_HTTP_PORT=9200 -# Neko 浏览器登录口令(用户 / 管理) -NEKO_USER_PASS=live -NEKO_ADMIN_PASS=admin +# Neko 浏览器登录:用户名为 live / admin,口令与全站策略一致(默认 12345678) +NEKO_USER_PASS=12345678 +NEKO_ADMIN_PASS=12345678 # 第二套 Neko 桌面:Firefox(Google 无官方 ARM Chrome;此为多架构 Firefox)。与 Chromium 共用上面口令。 ENABLE_NEKO_FF=1 NEKO_FF_HTTP_PORT=9201 diff --git a/live-platform/config/defaults/services/neko.json b/live-platform/config/defaults/services/neko.json index 981df0a..2eba25c 100644 --- a/live-platform/config/defaults/services/neko.json +++ b/live-platform/config/defaults/services/neko.json @@ -7,8 +7,8 @@ "env": { "ENABLE_NEKO": "1", "NEKO_HTTP_PORT": "9200", - "NEKO_USER_PASS": "live", - "NEKO_ADMIN_PASS": "admin", + "NEKO_USER_PASS": "12345678", + "NEKO_ADMIN_PASS": "12345678", "ENABLE_NEKO_FF": "1", "NEKO_FF_HTTP_PORT": "9201", "NEKO_FF_WEBRTC_EPR": "52200-52300", diff --git a/scripts/linux/lib/common.sh b/scripts/linux/lib/common.sh index f20aa1e..ceb6e42 100644 --- a/scripts/linux/lib/common.sh +++ b/scripts/linux/lib/common.sh @@ -16,6 +16,23 @@ log() { printf '[%s] %s\n' "$INSTALL_TAG" "$*" } +# 网络/镜像波动时重试(不退出整段安装) +retry_run() { + local max="${1:-3}" + local delay="${2:-3}" + shift 2 + local n=0 + while [ "$n" -lt "$max" ]; do + if "$@"; then + return 0 + fi + n=$((n + 1)) + log "retry ${n}/${max} failed command: $*" + sleep "$delay" + done + return 1 +} + ensure_root() { if [ "${EUID:-$(id -u)}" -ne 0 ]; then exec sudo -E bash "$0" "$@" @@ -38,6 +55,13 @@ apt_update_once() { apt_install() { apt_update_once + if apt-get install -y --no-install-recommends "$@"; then + return 0 + fi + log "apt install failed; trying dpkg --configure -a and apt -f install" + export DEBIAN_FRONTEND=noninteractive + dpkg --configure -a 2>/dev/null || true + apt-get -f install -y || true apt-get install -y --no-install-recommends "$@" } @@ -123,7 +147,10 @@ ensure_live_user() { useradd -m -s /bin/bash live fi echo "live:12345678" | chpasswd - usermod -aG sudo,audio,video,plugdev,render,gpio,spidev,pwm,i2c,docker live 2>/dev/null || true + usermod -aG sudo,audio,video,plugdev,render,gpio,spidev,pwm,i2c live 2>/dev/null || true + if getent group docker >/dev/null 2>&1; then + usermod -aG docker live 2>/dev/null || true + fi } install_base_packages() { @@ -153,9 +180,23 @@ install_nodejs() { return 0 fi fi - log "Installing Node.js 20" - curl -fsSL https://deb.nodesource.com/setup_20.x | bash - - apt_install nodejs + log "Installing Node.js (prefer 20.x from NodeSource)" + if retry_run 3 5 bash -c 'curl -fsSL https://deb.nodesource.com/setup_20.x | bash -'; then + apt_install nodejs || true + else + log "NodeSource unavailable; using distribution nodejs" + apt_install nodejs npm || apt_install nodejs || true + fi + if have node; then + major="$(node -p "process.versions.node.split('.')[0]" 2>/dev/null || echo 0)" + case "$major" in + ''|*[!0-9]*) log "WARN: could not parse Node major version" ;; + *) [ "$major" -lt 20 ] && log "WARN: Node major=$major (web-console expects >=20); upgrade manually if build fails" ;; + esac + return 0 + fi + log "WARN: Node.js not installed; web-console build may fail on this distro" + return 0 } install_docker_stack() { @@ -174,13 +215,26 @@ install_docker_stack() { prepare_python_runtime() { log "Preparing Python virtual environment" chown -R live:live "$ROOT_DIR" - as_live "python3 -m venv .venv" - as_live ". .venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt" + as_live "python3 -m venv .venv" || { + log "venv create failed; retry once with --clear" + rm -rf "$ROOT_DIR/.venv" + as_live "python3 -m venv .venv" || log "WARN: venv still failed" + } + if ! as_live ". .venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt"; then + log "pip install failed; retry with --no-cache-dir" + as_live ". .venv/bin/activate && pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt" || \ + log "WARN: Python deps incomplete; control plane may fail until fixed manually" + fi } prepare_web_console() { log "Building web console" - as_live "cd web-console && if [ -f package-lock.json ]; then npm ci; else npm install; fi && npm run build" + if as_live "cd web-console && if [ -f package-lock.json ]; then npm ci; else npm install; fi && npm run build"; then + return 0 + fi + log "web console build failed; cleaning node_modules and retrying" + as_live "cd web-console && rm -rf node_modules .next && npm install && npm run build" || \ + log "WARN: web console build still failed — service may serve stale assets until rebuilt" } install_live_console_service() { @@ -383,20 +437,25 @@ install_shellcrash() { return 0 fi log "Installing ShellCrash" - local url + local url ok=0 + rm -f /tmp/install_shellcrash.sh for url in \ 'https://testingcf.jsdelivr.net/gh/juewuy/ShellCrash@dev' \ 'https://raw.githubusercontent.com/juewuy/ShellCrash/dev' do - if wget -q --no-check-certificate -O /tmp/install_shellcrash.sh "$url/install_en.sh" && [ -s /tmp/install_shellcrash.sh ]; then + if retry_run 2 4 wget -q --no-check-certificate -O /tmp/install_shellcrash.sh "$url/install_en.sh" && [ -s /tmp/install_shellcrash.sh ]; then + ok=1 break fi done - if [ ! -s /tmp/install_shellcrash.sh ]; then - log "ShellCrash installer download failed" - return 1 + if [ "$ok" != "1" ] || [ ! -s /tmp/install_shellcrash.sh ]; then + log "ShellCrash installer download failed — continuing hub install (proxy later or set ENABLE_SHELLCRASH=0)" + return 0 + fi + if ! printf '1\n1\n1\n1\n' | bash /tmp/install_shellcrash.sh; then + log "ShellCrash install script returned error — continuing; configure manually under $SHELLCRASH_DIR if needed" + return 0 fi - printf '1\n1\n1\n1\n' | bash /tmp/install_shellcrash.sh } install_browser_stack() { @@ -438,8 +497,8 @@ install_neko_browser() { mkdir -p "$neko_prof" chmod 777 "$neko_prof" 2>/dev/null || true export NEKO_HTTP_PORT="${NEKO_HTTP_PORT:-9200}" - export NEKO_USER_PASS="${NEKO_USER_PASS:-live}" - export NEKO_ADMIN_PASS="${NEKO_ADMIN_PASS:-admin}" + export NEKO_USER_PASS="${NEKO_USER_PASS:-12345678}" + export NEKO_ADMIN_PASS="${NEKO_ADMIN_PASS:-12345678}" export NEKO_PROFILE_HOST_DIR="$neko_prof" export NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1920x1080@30}" export NEKO_WEBRTC_EPR="${NEKO_WEBRTC_EPR:-52000-52100}" diff --git a/scripts/linux/service_ctl.sh b/scripts/linux/service_ctl.sh index ef4d21a..1aeacf8 100644 --- a/scripts/linux/service_ctl.sh +++ b/scripts/linux/service_ctl.sh @@ -531,8 +531,8 @@ service_neko_start() { mkdir -p "$prof" chmod 777 "$prof" 2>/dev/null || true export NEKO_HTTP_PORT="${NEKO_HTTP_PORT:-9200}" - export NEKO_USER_PASS="${NEKO_USER_PASS:-live}" - export NEKO_ADMIN_PASS="${NEKO_ADMIN_PASS:-admin}" + export NEKO_USER_PASS="${NEKO_USER_PASS:-12345678}" + export NEKO_ADMIN_PASS="${NEKO_ADMIN_PASS:-12345678}" export NEKO_PROFILE_HOST_DIR="$prof" export NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1920x1080@30}" export NEKO_WEBRTC_EPR="${NEKO_WEBRTC_EPR:-52000-52100}" @@ -604,8 +604,8 @@ service_neko_firefox_start() { export NEKO_FF_DESKTOP_SCREEN="${NEKO_FF_DESKTOP_SCREEN:-1920x1080@30}" export NEKO_FF_WEBRTC_EPR="${NEKO_FF_WEBRTC_EPR:-52200-52300}" export NEKO_FF_WEBRTC_UDP_RANGE="${NEKO_FF_WEBRTC_UDP_RANGE:-52200-52300}" - export NEKO_USER_PASS="${NEKO_USER_PASS:-live}" - export NEKO_ADMIN_PASS="${NEKO_ADMIN_PASS:-admin}" + export NEKO_USER_PASS="${NEKO_USER_PASS:-12345678}" + export NEKO_ADMIN_PASS="${NEKO_ADMIN_PASS:-12345678}" export NEKO_FF_PROFILE_HOST_DIR="$prof" export NEKO_WEBRTC_ICELITE="${NEKO_WEBRTC_ICELITE:-1}" if [ -z "${NEKO_WEBRTC_NAT1TO1:-}" ]; then diff --git a/src/control_plane.py b/src/control_plane.py index 31c8ae1..da8e100 100644 --- a/src/control_plane.py +++ b/src/control_plane.py @@ -659,9 +659,11 @@ def stack_summary() -> dict: "control_url": f"http://{mdns_host}:{web_port}", "stack_env_present": STACK_ENV.is_file(), "neko_login": { - "user_password": env.get("NEKO_USER_PASS", "live"), - "admin_password": env.get("NEKO_ADMIN_PASS", "admin"), - "note_zh": "浏览器进入 Neko 时的登录口令(可在 system-stack.env 修改 NEKO_USER_PASS / NEKO_ADMIN_PASS)", + "user_login": "live", + "user_password": env.get("NEKO_USER_PASS", "12345678"), + "admin_login": "admin", + "admin_password": env.get("NEKO_ADMIN_PASS", "12345678"), + "note_zh": "与 WebTTY、File Browser 等一致:用户名为 live / admin,口令均为 NEKO_* 环境变量(默认 12345678)。Firefox 若卡在加载:请确认 NEKO_WEBRTC_NAT1TO1 为本机局域网 IP 且 UDP 端口放行。", }, "neko_firefox_enabled": neko_ff_on, "quick_links": ql, diff --git a/web-console/components/console/LiveConsoleWorkspace.tsx b/web-console/components/console/LiveConsoleWorkspace.tsx index 85061d9..4031a42 100644 --- a/web-console/components/console/LiveConsoleWorkspace.tsx +++ b/web-console/components/console/LiveConsoleWorkspace.tsx @@ -191,8 +191,15 @@ function Button({ ); } +export type LiveConsoleWorkspaceProps = { + /** 嵌入门户「网络」页:只渲染 ShellCrash 面板,不改写地址栏、不显示顶栏/Tab/底栏 */ + embeddedShellCrash?: boolean; + /** 与门户首页语言同步 */ + portalLang?: Lang; +}; + /** 全功能控制台:默认从门户 `/` 进入后打开 `/console`。 */ -export function LiveConsoleWorkspace() { +export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }: LiveConsoleWorkspaceProps) { const base = apiBase(); const [lang, setLang] = useState("zh"); const [theme, setTheme] = useState("dark"); @@ -448,6 +455,16 @@ export function LiveConsoleWorkspace() { }, [fetchJson, refreshAndroidDevices, refreshServices]); useEffect(() => { + if (embeddedShellCrash) { + setTab("shellcrash"); + if (portalLang === "zh" || portalLang === "en") { + setLang(portalLang); + } else { + const hub = window.localStorage.getItem("live-hub-lang"); + if (hub === "zh" || hub === "en") setLang(hub); + } + return; + } const params = new URLSearchParams(window.location.search); const pathTab = pathnameToTab(window.location.pathname); const nextTab = params.get("tab"); @@ -459,16 +476,23 @@ export function LiveConsoleWorkspace() { else if (storedLang === "zh" || storedLang === "en") setLang(storedLang); const storedTheme = window.localStorage.getItem("live-console-theme"); if (storedTheme === "light" || storedTheme === "dark") setTheme(storedTheme); - }, []); + }, [embeddedShellCrash, portalLang]); useEffect(() => { + if (embeddedShellCrash) return; document.documentElement.setAttribute("data-theme", theme); window.localStorage.setItem("live-console-theme", theme); - }, [theme]); + }, [theme, embeddedShellCrash]); useEffect(() => { + if (!embeddedShellCrash) return; + if (portalLang === "zh" || portalLang === "en") setLang(portalLang); + }, [embeddedShellCrash, portalLang]); + + useEffect(() => { + if (embeddedShellCrash) return; syncUrl(tab, lang); - }, [lang, syncUrl, tab]); + }, [embeddedShellCrash, lang, syncUrl, tab]); useEffect(() => { void boot(); @@ -483,7 +507,7 @@ export function LiveConsoleWorkspace() { useEffect(() => { if (booting || error) return; - if (tab !== "overview" && tab !== "modules") return; + if (tab !== "overview" && tab !== "modules" && tab !== "shellcrash") return; void refreshServices(); const timer = window.setInterval(() => void refreshServices(), 10000); return () => clearInterval(timer); @@ -809,102 +833,116 @@ export function LiveConsoleWorkspace() { return ( <> -
-
- - ← {tr("返回门户首页", "Back to portal")} - - - {tr("无人直播 · 本页需与 API 同域(推荐 live.local:8001)", "Unmanned live · same origin as API (live.local:8001)")} - + {!embeddedShellCrash ? ( +
+
+ + ← {tr("返回门户首页", "Back to portal")} + + + {tr("无人直播 · 本页需与 API 同域(推荐 live.local:8001)", "Unmanned live · same origin as API (live.local:8001)")} + +
-
-
+ ) : null} +
{toast ? (
{toast}
) : null} -
-
-
-

- {tr("控制平面 · ARM Linux 开发板", "Control plane · ARM Linux SBC")} -

-

- {tr("无人直播控制中心", "Unmanned live control center")} -

-

- {tr( - "ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI 采集;Neko / Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP,采集卡可走视频进 Linux。各模块独立,可分别启停。", - "ShellCrash for egress; FFmpeg for stream or HDMI; Neko/Chromium for browser; USB ADB to AOSP plus optional capture card. Modules are isolated.", - )} -

-
-
-
- {(stack?.mdns_host || "live.local") + - " · " + - (stack?.machine || "unknown") + - " · " + - (stack?.kernel || "unknown")} + {!embeddedShellCrash ? ( +
+
+
+

+ {tr("控制平面 · ARM Linux 开发板", "Control plane · ARM Linux SBC")} +

+

+ {tr("无人直播控制中心", "Unmanned live control center")} +

+

+ {tr( + "ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI 采集;Neko / Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP,采集卡可走视频进 Linux。各模块独立,可分别启停。", + "ShellCrash for egress; FFmpeg for stream or HDMI; Neko/Chromium for browser; USB ADB to AOSP plus optional capture card. Modules are isolated.", + )} +

+
+
+
+ {(stack?.mdns_host || "live.local") + + " · " + + (stack?.machine || "unknown") + + " · " + + (stack?.kernel || "unknown")} +
+ + + + +
- - - - -
-
- -
+ + + ) : ( +
+

ShellCrash

+ +
+ )} {booting ? (
@@ -1546,36 +1584,38 @@ export function LiveConsoleWorkspace() { ) : null}
- + {!embeddedShellCrash ? ( + + ) : null} ); } diff --git a/web-console/components/live-product/LiveControlApp.tsx b/web-console/components/live-product/LiveControlApp.tsx index e556588..8a71af3 100644 --- a/web-console/components/live-product/LiveControlApp.tsx +++ b/web-console/components/live-product/LiveControlApp.tsx @@ -9,27 +9,32 @@ import { Clock, Cpu, HardDrive, + Languages, Loader2, Menu, MonitorPlay, + Moon, Network, RefreshCw, Server, Settings, Smartphone, Sparkles, + Sun, Wifi, X, } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import OverviewCharts from "@/components/OverviewCharts"; +import { LiveConsoleWorkspace } from "@/components/console/LiveConsoleWorkspace"; import { LiveAndroidWorkstation } from "@/components/live-product/LiveAndroidWorkstation"; import { LiveTiktokHdmiView } from "@/components/live-product/LiveTiktokHdmiView"; import { LiveYoutubeUnmannedView } from "@/components/live-product/LiveYoutubeUnmannedView"; import { normalizeBrowserReachableHttpUrl } from "@/lib/panelUrls"; type Lang = "zh" | "en"; +type PortalTheme = "dark" | "light"; type ViewKey = | "dashboard" | "services" @@ -64,7 +69,13 @@ type HubDashboard = { platform?: string; ips?: string[]; quick_links?: Record; - neko_login?: { user_password?: string; admin_password?: string; note_zh?: string }; + neko_login?: { + user_login?: string; + user_password?: string; + admin_login?: string; + admin_password?: string; + note_zh?: string; + }; neko_firefox_enabled?: boolean; }; services?: Array<{ @@ -164,6 +175,7 @@ function statusPill(status: string) { export default function LiveControlApp() { const base = apiBase(); const [lang, setLang] = useState("zh"); + const [portalTheme, setPortalTheme] = useState("dark"); const [view, setView] = useState("dashboard"); const [mobileNav, setMobileNav] = useState(false); const [dash, setDash] = useState(null); @@ -215,12 +227,23 @@ export default function LiveControlApp() { useEffect(() => { const s = window.localStorage.getItem("live-hub-lang"); if (s === "zh" || s === "en") setLang(s); + const th = window.localStorage.getItem("live-hub-theme"); + if (th === "light" || th === "dark") setPortalTheme(th); }, []); useEffect(() => { window.localStorage.setItem("live-hub-lang", lang); }, [lang]); + useEffect(() => { + document.documentElement.setAttribute("data-theme", portalTheme); + try { + window.localStorage.setItem("live-hub-theme", portalTheme); + } catch { + /* ignore */ + } + }, [portalTheme]); + useEffect(() => { try { if (localStorage.getItem("livehub.autorefresh") === "0") setAutoRefreshEnabled(false); @@ -315,7 +338,7 @@ export default function LiveControlApp() { const runProcess = async (action: "start" | "stop" | "restart", processOverride?: string) => { const proc = (processOverride ?? currentProc).trim(); if (!proc) return; - setBusy(`proc:${action}`); + setBusy(`proc:${action}:${proc}`); try { const path = `/${action}`; const data = await fetchJson<{ output?: string; message?: string }>(path, { @@ -394,10 +417,29 @@ export default function LiveControlApp() { const totalServices = dash?.services?.length ?? 0; return ( -
-
-
-
+
+
+
+
{toast ? ( {/* Desktop sidebar */} -