This commit is contained in:
eric
2026-03-29 03:01:43 -05:00
parent e3ac026710
commit fbdcf4cc73
5 changed files with 142 additions and 16 deletions

View File

@@ -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"
}
}

View File

@@ -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,
}

View File

@@ -870,8 +870,8 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
</h1>
<p className="mt-3 max-w-3xl text-sm text-slate-400">
{tr(
"ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMIChromium 负责浏览器侧;开发板通过 USB 双头线 ADB 控制 AOSP。各模块独立可分别启停。",
"ShellCrash for egress; FFmpeg for stream or HDMI; Chromium for browser; USB ADB to AOSP. Modules are isolated.",
"ShellCrash 负责代理上网;本机 FFmpeg 处理推流或 HDMINekoDocker 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.",
)}
</p>
</div>
@@ -971,8 +971,8 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
</li>
<li>
{tr(
"「直播业务」里启停 PM2 脚本抖音→YouTube、HDMI 等);「系统服务」看 ShellCrash、SRS、ws-scrcpy 等是否在跑。",
"Use Live ops for PM2 scripts; Services for ShellCrash, SRS, ws-scrcpy, etc.",
"「直播业务」里启停 PM2 脚本抖音→YouTube、HDMI 等);「系统服务」看 ShellCrash、SRS、ws-scrcpy、Neko 等是否在跑。",
"Use Live ops for PM2 scripts; Services for ShellCrash, SRS, ws-scrcpy, Neko, etc.",
)}
</li>
<li>
@@ -995,8 +995,8 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
<h2 className="section-title">{tr("🛰️ 流媒体与桌面入口", "Streaming & desktop")}</h2>
<p className="section-lead">
{tr(
"ws-scrcpy浏览器里控安卓Chromium本机+油猴+RemoteDebug。",
"ws-scrcpy: browser ADB; Chromium: host+Tampermonkey+9222.",
"ws-scrcpy浏览器里控安卓Chromium本机+油猴+RemoteDebugNekoDocker 里跑 Chromium 桌面(非 Firefox 版)。",
"ws-scrcpy: browser ADB; Chromium: host+Tampermonkey+9222; Neko: Chromium desktop in Docker (not the Firefox image).",
)}
</p>
<div className="mt-4 flex max-h-[28rem] flex-col gap-2 overflow-y-auto pr-1 log-scroll">
@@ -1028,6 +1028,11 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
🧪 {tr("Chromium RemoteDebug (9222)", "Chromium RemoteDebug")}
</a>
) : null}
{qlNorm.neko_browser ? (
<a className="link-tile link-tile-warn" href={qlNorm.neko_browser} target="_blank" rel="noreferrer">
🐱 Neko / {tr("Docker Chromium 桌面", "Docker Chromium desktop")}
</a>
) : null}
{qlNorm.webtty ? (
<a className="link-tile" href={qlNorm.webtty} target="_blank" rel="noreferrer">
WebTTY
@@ -1496,8 +1501,8 @@ export function LiveConsoleWorkspace({ embeddedShellCrash = false, portalLang }:
</div>
<p className="mt-2 text-xs leading-relaxed text-slate-500">
{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 在线,并尝试新标签打开NekoChromium在 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.",
)}
</p>
<iframe className="mt-4 h-[30rem] w-full rounded-2xl border border-white/10 bg-black" src={rawAndroidPanelUrl} title="ws-scrcpy" />

View File

@@ -74,6 +74,13 @@ type HubDashboard = {
platform?: string;
ips?: string[];
quick_links?: Record<string, string>;
neko_login?: {
user_login?: string;
user_password?: string;
admin_login?: string;
admin_password?: string;
note_zh?: string;
};
};
services?: Array<{
service_id: string;
@@ -121,6 +128,11 @@ type HubDashboard = {
http?: { ok?: boolean; http_code?: number; reachable?: boolean; latency_ms?: number; error?: string };
http_ui?: { ok?: boolean; http_code?: number; reachable?: boolean; latency_ms?: number; error?: string };
};
neko?: {
port?: number;
tcp?: { ok?: boolean; reachable?: boolean; latency_ms?: number; error?: string };
http?: { ok?: boolean; http_code?: number; reachable?: boolean; latency_ms?: number; error?: string };
};
};
};
@@ -802,6 +814,42 @@ export default function LiveControlApp() {
</div>
) : null}
{dash?.stack?.neko_login ? (
<div className="rounded-2xl border border-fuchsia-500/25 bg-gradient-to-r from-fuchsia-500/[0.08] to-violet-500/[0.06] p-5">
<h3 className="text-sm font-semibold text-white">Neko {t("登录", "sign-in")}</h3>
<p className="mt-1 text-[11px] text-zinc-500">
{dash.stack.neko_login.note_zh ||
t("Neko Chromium :9200 使用下列账号策略。", "Neko Chromium :9200 uses the policy below.")}
</p>
<p className="mt-2 text-[11px] text-amber-200/85">
{t(
"重要:两栏分开填——昵称随意,密码必须是环境变量里的口令(默认 12345678。用错密码会像一直加载勿把「用户名/密码」写在一行。",
"Use two fields: display name is free-form; password must match NEKO_* (default 12345678). Wrong password looks like infinite loading.",
)}
</p>
<div className="mt-4 grid gap-3 sm:grid-cols-2 font-mono text-sm">
<div className="rounded-xl border border-white/10 bg-black/40 px-4 py-3">
<p className="text-[10px] uppercase text-zinc-500">{t("用户", "User")}</p>
<p className="mt-1 text-fuchsia-200">{dash.stack.neko_login.user_login ?? "live"}</p>
<p className="mt-2 text-[10px] uppercase text-zinc-500">{t("口令", "Password")}</p>
<p className="mt-0.5 text-fuchsia-100">{dash.stack.neko_login.user_password ?? "—"}</p>
</div>
<div className="rounded-xl border border-white/10 bg-black/40 px-4 py-3">
<p className="text-[10px] uppercase text-zinc-500">{t("管理员", "Admin")}</p>
<p className="mt-1 text-violet-200">{dash.stack.neko_login.admin_login ?? "admin"}</p>
<p className="mt-2 text-[10px] uppercase text-zinc-500">{t("口令", "Password")}</p>
<p className="mt-0.5 text-violet-100">{dash.stack.neko_login.admin_password ?? "—"}</p>
</div>
</div>
<p className="mt-3 text-[10px] text-zinc-600">
{t(
"Google 无官方 ARM 桌面 Chrome 品牌包;此处为 Neko 自带的 Chromium 桌面(与 Chrome 商标不同)。本栈未启用 Firefox 版 Neko。",
"No official ARM desktop Chrome branding here; this is Nekos bundled Chromium desktop. This stack does not use the Neko Firefox image.",
)}
</p>
</div>
) : null}
{dash?.network?.interfaces?.length ? (
<div className="rounded-2xl border border-cyan-500/15 bg-gradient-to-br from-cyan-500/[0.07] to-transparent p-5">
<h3 className="flex items-center gap-2 text-sm font-semibold text-white">
@@ -962,6 +1010,38 @@ export default function LiveControlApp() {
);
})()
) : null}
{dash.probes.neko ? (
(() => {
const p = dash.probes?.neko;
const tcpOk = p?.tcp?.reachable;
const http = p?.http;
return (
<div className="rounded-2xl border border-white/[0.07] bg-zinc-950/40 p-4 backdrop-blur-sm">
<p className="text-xs font-semibold text-white">Neko Chromium</p>
<p className="mt-1 text-[11px] text-zinc-500">
{t(
"Docker 内 Chromium非 Chrome 品牌arm64/x86 同一镜像。",
"Chromium in Docker (not Chrome branding); multi-arch.",
)}
</p>
<p className="mt-2 font-mono text-[11px] text-zinc-500">
:{p?.port ?? "—"} · TCP {tcpOk ? "OK" : "—"} · HTTP {http?.http_code ?? "—"}
</p>
<p className="mt-1 text-[11px] text-zinc-500">
{t("延迟", "RTT")}{" "}
{http?.latency_ms != null
? `${http.latency_ms} ms`
: p?.tcp?.latency_ms != null
? `${p.tcp.latency_ms} ms`
: "—"}
</p>
{!http?.reachable && http?.error ? (
<p className="mt-1 line-clamp-2 text-[10px] text-amber-200/80">{http.error}</p>
) : null}
</div>
);
})()
) : null}
</div>
) : null}
</div>
@@ -1149,7 +1229,7 @@ export default function LiveControlApp() {
<p className={`mt-2 text-xs ${portalTheme === "light" ? "text-slate-600" : "text-zinc-400"}`}>
{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.",
)}
</p>
<div className="mt-6 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
@@ -1214,7 +1294,7 @@ export default function LiveControlApp() {
</label>
<label className="flex cursor-pointer items-center justify-between gap-4 border-t border-white/[0.06] pt-4">
<div>
<p className="text-sm font-medium text-white">{t("总览显示 SRS 探针", "Show SRS stream probe")}</p>
<p className="text-sm font-medium text-white">{t("总览显示 SRS / Neko 探针", "Show SRS/Neko probes")}</p>
<p className="text-[11px] text-zinc-500">{t("关闭可精简首页", "Hide stream probe cards")}</p>
</div>
<input

View File

@@ -641,10 +641,10 @@ export function LiveYoutubeUnmannedView({
target: "live_youtube",
},
{
label: "Android",
sub: t("ADB 设备", "ADB"),
label: "Neko",
sub: t("Chromium 工作室", "Chromium"),
gradient: "from-emerald-400 to-teal-500",
target: "android",
target: "services",
},
]}
/>