diff --git a/live-platform/config/defaults/services/neko.json b/live-platform/config/defaults/services/neko.json new file mode 100644 index 0000000..42b75ec --- /dev/null +++ b/live-platform/config/defaults/services/neko.json @@ -0,0 +1,12 @@ +{ + "id": "neko", + "layer": "infra", + "enabled": true, + "manager": "systemd", + "ports": { "http": 9200 }, + "env": { + "ENABLE_NEKO": "1", + "NEKO_HTTP_PORT": "9200", + "NEKO_PROFILE_HOST_DIR": "/opt/live-modules/neko-profile" + } +} diff --git a/src/control_plane.py b/src/control_plane.py index 93d1892..b1c7ba9 100644 --- a/src/control_plane.py +++ b/src/control_plane.py @@ -89,6 +89,12 @@ SERVICE_SPECS: tuple[ServiceSpec, ...] = ( "Browser ADB control (ws-scrcpy stack; clone URL configurable in system-stack.env)", ), ServiceSpec("chromium", "Chromium 浏览器", "browser", "本机 Chromium + 持久化配置 + Remote DevTools(非 Docker)"), + ServiceSpec( + "neko", + "Neko 浏览器 (Docker)", + "browser", + "Docker 内嵌 Chromium 桌面(官方 ghcr.io/m1k1o/neko/chromium);远程开 YouTube Studio 等;ENABLE_NEKO=1 时安装", + ), ServiceSpec( "android_gateway", "Android Gateway", @@ -566,10 +572,11 @@ def recent_pm2_log_lines(max_lines: int = 14) -> list[str]: async def stack_stream_probes() -> dict[str, Any]: - """本机 SRS HTTP 端口探测(供 live.local 验收)。""" + """本机 SRS / Neko HTTP 端口探测(供 live.local 验收)。""" env = load_stack_env() srs_p = int(env.get("SRS_HTTP_PORT", "8080") or 8080) srs_api_p = int(env.get("SRS_API_PORT", "1985") or 1985) + neko_p = int(env.get("NEKO_HTTP_PORT", "9200") or 9200) loop = asyncio.get_event_loop() def run_srs() -> dict[str, Any]: @@ -589,8 +596,16 @@ async def stack_stream_probes() -> dict[str, Any]: "api_ok": api_ok, } - srs_d = await loop.run_in_executor(None, run_srs) - return {"srs": srs_d} + def run_neko() -> dict[str, Any]: + tcp = _probe_tcp_port("127.0.0.1", neko_p) + http = _probe_http_get(f"http://127.0.0.1:{neko_p}/", timeout=2.5) + return {"port": neko_p, "tcp": tcp, "http": http} + + srs_d, neko_d = await asyncio.gather( + loop.run_in_executor(None, run_srs), + loop.run_in_executor(None, run_neko), + ) + return {"srs": srs_d, "neko": neko_d} def stack_summary() -> dict: @@ -606,6 +621,7 @@ def stack_summary() -> dict: srs_api = env.get("SRS_API_PORT", "1985") android_panel = env.get("ANDROID_PANEL_PORT", "5000") chrome_dbg = env.get("BROWSER_REMOTE_DEBUG_PORT", "9222") + neko_port = env.get("NEKO_HTTP_PORT", "9200") ql: dict[str, str] = { "control_plane": f"http://{mdns_host}:{web_port}", "webtty": f"http://{mdns_host}:{webtty}", @@ -615,6 +631,7 @@ def stack_summary() -> dict: "srs_api": f"http://{mdns_host}:{srs_api}/api/v1/versions", "ws_scrcpy": f"http://{mdns_host}:{android_panel}", "chromium_remote_debug": f"http://{mdns_host}:{chrome_dbg}", + "neko_browser": f"http://{mdns_host}:{neko_port}", } return { "hostname": hostname, @@ -627,6 +644,18 @@ def stack_summary() -> dict: "dashboard_url": f"http://{mdns_host}" if homepage_port in {"80", ""} else f"http://{mdns_host}:{homepage_port}", "control_url": f"http://{mdns_host}:{web_port}", "stack_env_present": STACK_ENV.is_file(), + "neko_login": { + "user_login": "live", + "user_password": env.get("NEKO_USER_PASS", "12345678"), + "admin_login": "admin", + "admin_password": env.get("NEKO_ADMIN_PASS", "12345678"), + "note_zh": ( + "Neko 为 multiuser 模式:「显示昵称」可任意填写(如 live);「密码」必须与 NEKO_USER_PASS / NEKO_ADMIN_PASS " + "完全一致(默认 12345678)。不要把 live/12345678 填进同一格。密码错误时会一直卡在连接中。" + " WebRTC 需 NEKO_WEBRTC_NAT1TO1 为板子局域网 IP,并放行 UDP 端口段。" + " 本仓库 compose 仅启用 Chromium 镜像,不包含 Firefox。" + ), + }, "quick_links": ql, } diff --git a/web-console/components/console/LiveConsoleWorkspace.tsx b/web-console/components/console/LiveConsoleWorkspace.tsx index b7cbc7b..903c33e 100644 --- a/web-console/components/console/LiveConsoleWorkspace.tsx +++ b/web-console/components/console/LiveConsoleWorkspace.tsx @@ -870,8 +870,8 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
{tr( - "ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI;Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP。各模块独立,可分别启停。", - "ShellCrash for egress; FFmpeg for stream or HDMI; Chromium for browser; USB ADB to AOSP. Modules are isolated.", + "ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMI;Neko(Docker Chromium)或本机 Chromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP。各模块独立,可分别启停。", + "ShellCrash for egress; FFmpeg for stream or HDMI; Neko (Docker Chromium) or host Chromium for browser; USB ADB to AOSP. Modules are isolated.", )}
@@ -971,8 +971,8 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:{tr( - "ws-scrcpy:浏览器里控安卓;Chromium:本机+油猴+RemoteDebug。", - "ws-scrcpy: browser ADB; Chromium: host+Tampermonkey+9222.", + "ws-scrcpy:浏览器里控安卓;Chromium:本机+油猴+RemoteDebug;Neko:Docker 里跑 Chromium 桌面(非 Firefox 版)。", + "ws-scrcpy: browser ADB; Chromium: host+Tampermonkey+9222; Neko: Chromium desktop in Docker (not the Firefox image).", )}
{tr( - "内嵌若黑屏:多为 WebSocket 未连上或服务未启动。请先确认「系统服务」里 android_panel 在线,并尝试新标签打开。", - "Black iframe: often WebSocket or service down. Check android_panel in Services, try opening in a new tab.", + "内嵌若黑屏:多为 WebSocket 未连上或服务未启动。请先确认「系统服务」里 android_panel 在线,并尝试新标签打开;Neko(Chromium)在 ENABLE_NEKO=1 且容器运行后一般为 9200 端口。", + "Black iframe: often WebSocket or service down. Check android_panel in Services, try opening in a new tab; Neko Chromium is often :9200 when ENABLE_NEKO=1 and the container is up.", )}
diff --git a/web-console/components/live-product/LiveControlApp.tsx b/web-console/components/live-product/LiveControlApp.tsx index c538e09..3ed4e45 100644 --- a/web-console/components/live-product/LiveControlApp.tsx +++ b/web-console/components/live-product/LiveControlApp.tsx @@ -74,6 +74,13 @@ type HubDashboard = { platform?: string; ips?: string[]; quick_links?: Record+ {dash.stack.neko_login.note_zh || + t("Neko Chromium :9200 使用下列账号策略。", "Neko Chromium :9200 uses the policy below.")} +
++ {t( + "重要:两栏分开填——昵称随意,密码必须是环境变量里的口令(默认 12345678)。用错密码会像一直加载;勿把「用户名/密码」写在一行。", + "Use two fields: display name is free-form; password must match NEKO_* (default 12345678). Wrong password looks like infinite loading.", + )} +
+{t("用户", "User")}
+{dash.stack.neko_login.user_login ?? "live"}
+{t("口令", "Password")}
+{dash.stack.neko_login.user_password ?? "—"}
+{t("管理员", "Admin")}
+{dash.stack.neko_login.admin_login ?? "admin"}
+{t("口令", "Password")}
+{dash.stack.neko_login.admin_password ?? "—"}
++ {t( + "Google 无官方 ARM 桌面 Chrome 品牌包;此处为 Neko 自带的 Chromium 桌面(与 Chrome 商标不同)。本栈未启用 Firefox 版 Neko。", + "No official ARM desktop Chrome branding here; this is Neko’s bundled Chromium desktop. This stack does not use the Neko Firefox image.", + )} +
+Neko Chromium
++ {t( + "Docker 内 Chromium(非 Chrome 品牌);arm64/x86 同一镜像。", + "Chromium in Docker (not Chrome branding); multi-arch.", + )} +
++ :{p?.port ?? "—"} · TCP {tcpOk ? "OK" : "—"} · HTTP {http?.http_code ?? "—"} +
++ {t("延迟", "RTT")}{" "} + {http?.latency_ms != null + ? `${http.latency_ms} ms` + : p?.tcp?.latency_ms != null + ? `${p.tcp.latency_ms} ms` + : "—"} +
+ {!http?.reachable && http?.error ? ( +{http.error}
+ ) : null} +{t( "透明代理与规则分流直接在本页完成,无需跳转;流媒体与其它系统服务请在「服务」页管理。", - "Proxy rules and YAML editing are embedded here. Use Services for SRS, Chromium, etc.", + "Proxy rules and YAML editing are embedded here. Use Services for SRS/Neko/Chromium, etc.", )}