This commit is contained in:
eric
2026-05-19 11:58:20 +00:00
parent b617555360
commit bae55ec52c
18 changed files with 136 additions and 35 deletions

View File

@@ -776,6 +776,7 @@ ensure_runtime_dirs_owned_by_live() {
backup_config \
.pm2 \
.process_runner \
web-console \
web-console/out \
web-console/.next \
web-console/node_modules \
@@ -784,6 +785,7 @@ ensure_runtime_dirs_owned_by_live() {
path="$ROOT_DIR/$rel"
mkdir -p "$path"
chown -R live:live "$path" 2>/dev/null || true
chmod -R u+rwX,g+rwX "$path" 2>/dev/null || true
done
}
@@ -952,11 +954,14 @@ StartLimitIntervalSec=0
[Service]
User=live
PermissionsStartOnly=true
WorkingDirectory=$ROOT_DIR
Environment=HOST=0.0.0.0
Environment=PORT=$PORT
Environment=LIVE_NOFILE_SOFT=$LIVE_NOFILE_SOFT
LimitNOFILE=$LIVE_NOFILE_SOFT
ExecStartPre=/usr/bin/install -d -o live -g live $ROOT_DIR/config $ROOT_DIR/logs $ROOT_DIR/downloads $ROOT_DIR/backup $ROOT_DIR/backup_config $ROOT_DIR/.pm2 $ROOT_DIR/.process_runner $ROOT_DIR/web-console $ROOT_DIR/services/neko/data
ExecStartPre=/bin/chown -R live:live $ROOT_DIR/config $ROOT_DIR/logs $ROOT_DIR/downloads $ROOT_DIR/backup $ROOT_DIR/backup_config $ROOT_DIR/.pm2 $ROOT_DIR/.process_runner $ROOT_DIR/web-console $ROOT_DIR/services/neko/data
ExecStart=$ROOT_DIR/start.sh
Restart=always
RestartSec=5

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio
import json
import sqlite3
import time
from pathlib import Path
from typing import Any, Callable, Optional
@@ -276,6 +277,11 @@ async def hub_youtube_pro_channels_write(body: YoutubeProChannelsBody) -> dict[s
set_youtube_pro_channels(body.channels)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except sqlite3.Error as exc:
raise HTTPException(
status_code=500,
detail=f"多频道状态数据库写入失败: {exc}; 请确认 config 目录归 live 用户可写",
) from exc
capacity = await asyncio.to_thread(ensure_neko_capacity_for_youtube_channels, _repo_root, body.channels)
return {"message": "ok", "neko": capacity}

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
import ast
import asyncio
import json
import sqlite3
import tempfile
import unittest
from pathlib import Path
@@ -19,6 +20,15 @@ class _JsonRequest:
return self._payload
class _ReadRequest:
headers: dict[str, str] = {}
class _Client:
host = "127.0.0.1"
client = _Client()
def _json_body(resp) -> dict[str, object]: # noqa: ANN001
return json.loads(resp.body)
@@ -180,7 +190,7 @@ class WebDynamicProcessesTests(unittest.TestCase):
with patch("web.allows_url_config", return_value=True):
with patch("web.managed_url_config_relpath", return_value=rel):
with patch("web._sync_youtube_pro_runtime_files", side_effect=AssertionError("unexpected resync")):
payload = asyncio.run(web.get_url_config("youtube"))
payload = asyncio.run(web.get_url_config(_ReadRequest(), process="youtube"))
self.assertEqual(_json_body(payload)["content"], "https://live.douyin.com/new\n")
self.assertEqual(payload.headers.get("cache-control"), "no-store, max-age=0, must-revalidate")
@@ -204,6 +214,26 @@ class WebDynamicProcessesTests(unittest.TestCase):
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
self.assertEqual(captured[-1][0]["urlLines"], "https://live.douyin.com/fresh\n")
def test_save_url_config_returns_json_when_channel_store_readonly(self) -> None:
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube2__lane1", "urlLines": "old\n"}]
with patch("web.allows_url_config", return_value=True):
with patch("web.managed_url_config_relpath", return_value="URL_config.youtube2__lane1.ini"):
with patch("web.governed_save_managed_file"):
with patch("web.get_youtube_pro_channels", return_value=channels):
with patch("web.set_youtube_pro_channels", side_effect=sqlite3.OperationalError("attempt to write a readonly database")):
payload = asyncio.run(
web.save_url_config(
_JsonRequest({"content": "https://live.douyin.com/fresh\n"}),
process="youtube2__lane1",
)
)
body = _json_body(payload)
self.assertEqual(payload.status_code, 500)
self.assertIn("多频道状态数据库写入失败", body["error"])
self.assertIn("config", body["error"])
def test_save_config_updates_matching_youtube_pro_channel_stream_key(self) -> None:
captured: list[list[dict[str, object]]] = []
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube2__lane1", "streamKey": "old-key"}]

View File

@@ -370,6 +370,7 @@ export function LiveYoutubeUnmannedView({
const urlListDirtyRef = useRef(false);
const singleUrlDraftRef = useRef(urlConfig);
const proUrlDraftRef = useRef(proUrlDraft);
const channelsRef = useRef<YoutubeProChannel[]>([]);
const currentProcRef = useRef(currentProc);
const urlRequestSeqRef = useRef(0);
const ytIniRequestSeqRef = useRef(0);
@@ -403,6 +404,27 @@ export function LiveYoutubeUnmannedView({
proUrlDraftRef.current = proUrlDraft;
}, [proUrlDraft]);
useEffect(() => {
channelsRef.current = channels;
}, [channels]);
const updateActiveChannelUrlLines = useCallback((channelId: string, nextUrlLines: string) => {
if (!channelId) return;
setChannels((prev) => {
let changed = false;
const next = prev.map((row) => {
if (row.id !== channelId) return row;
if ((row.urlLines ?? "") === nextUrlLines) return row;
changed = true;
return { ...row, urlLines: nextUrlLines };
});
if (!changed) return prev;
channelsRef.current = next;
saveYoutubeProChannels(next);
return next;
});
}, []);
const pullUrlConfigFromServer = useCallback(async () => {
const targetProc = currentProcRef.current.trim();
if (!targetProc) return;
@@ -418,24 +440,18 @@ export function LiveYoutubeUnmannedView({
if (urlListDirtyRef.current) return;
if (mode === "single") applySingleUrlDraft(c);
else {
const localDraft = channels.find((row) => row.id === activeCh)?.urlLines ?? "";
const localDraft = channelsRef.current.find((row) => row.id === activeCh)?.urlLines ?? "";
if (shouldPreserveLocalUrlDraft(localDraft, c)) {
applyProUrlDraft(localDraft);
return;
}
applyProUrlDraft(c);
if (activeCh) {
setChannels((prev) => {
const next = prev.map((x) => (x.id === activeCh ? { ...x, urlLines: c } : x));
saveYoutubeProChannels(next);
return next;
});
}
updateActiveChannelUrlLines(activeCh, c);
}
} catch {
/* ignore */
}
}, [activeCh, applyProUrlDraft, applySingleUrlDraft, channels, fetchJson, mode]);
}, [activeCh, applyProUrlDraft, applySingleUrlDraft, fetchJson, mode, updateActiveChannelUrlLines]);
useEffect(() => {
if (!currentProc || !urlAutoRefresh) return;
@@ -467,27 +483,21 @@ export function LiveYoutubeUnmannedView({
}
if (mode === "single") applySingleUrlDraft(c);
else {
const localDraft = channels.find((row) => row.id === activeCh)?.urlLines ?? "";
const localDraft = channelsRef.current.find((row) => row.id === activeCh)?.urlLines ?? "";
if (shouldPreserveLocalUrlDraft(localDraft, c)) {
applyProUrlDraft(localDraft);
setUrlFetchNote(t("服务器暂无已保存 URL已保留本地草稿", "Server has no saved URLs, kept the local draft"));
return;
}
applyProUrlDraft(c);
if (activeCh) {
setChannels((prev) => {
const next = prev.map((x) => (x.id === activeCh ? { ...x, urlLines: c } : x));
saveYoutubeProChannels(next);
return next;
});
}
updateActiveChannelUrlLines(activeCh, c);
}
setUrlFetchNote(t("已从服务器读取 URL_config", "Reloaded URL_config from server"));
} catch (e) {
if (requestId !== urlRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
setUrlFetchNote((e as Error).message);
} finally {
if (requestId === urlRequestSeqRef.current && targetProc === currentProcRef.current.trim()) {
if (requestId === urlRequestSeqRef.current) {
setUrlReloading(false);
}
}
@@ -512,6 +522,7 @@ export function LiveYoutubeUnmannedView({
try {
const remote = await fetchYoutubeProChannelsRemote(fetchJson);
if (remote.fetched && remote.channels.length && !cancelled) {
channelsRef.current = remote.channels;
setChannels(remote.channels);
saveYoutubeProChannels(remote.channels);
return;
@@ -537,6 +548,7 @@ export function LiveYoutubeUnmannedView({
},
];
})();
channelsRef.current = initial;
setChannels(initial);
if (!list.length) saveYoutubeProChannels(initial);
})();
@@ -574,6 +586,7 @@ export function LiveYoutubeUnmannedView({
}, [currentProc, currentProcLooksYoutube, mode, preferredSingleProc, setCurrentProc]);
const applyChannelsLocal = useCallback((next: YoutubeProChannel[]) => {
channelsRef.current = next;
setChannels(next);
saveYoutubeProChannels(next);
}, []);
@@ -786,7 +799,7 @@ export function LiveYoutubeUnmannedView({
if (requestId !== ytIniRequestSeqRef.current || targetProc !== currentProcRef.current.trim()) return;
setYtIniStatus((e as Error).message);
} finally {
if (requestId === ytIniRequestSeqRef.current && targetProc === currentProcRef.current.trim()) {
if (requestId === ytIniRequestSeqRef.current) {
setYtIniLoading(false);
}
}

View File

@@ -1,6 +1,6 @@
{
"built_at": 1779182151.9524395,
"source_mtime": 1779180451.7832174,
"built_at": 1779184553.6568675,
"source_mtime": 1779183169.5914183,
"toolchain": "local",
"node_major": 20
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,6 @@
3:I[3946,["500","static/chunks/500-ea37cc8de90441f7.js","246","static/chunks/246-b5d14933cf331e85.js","357","static/chunks/app/android/page-84550dabda18d658.js"],"default",1]
4:I[4707,[],""]
5:I[6423,[],""]
0:["UmN0Ou_Iv2TWI4xcHvY2X",[[["",{"children":["android",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["android",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","android","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
0:["Bqx_FrY3OzE8SRa_XO6pv",[[["",{"children":["android",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["android",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","android","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
1:null

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,6 @@
3:I[6367,["500","static/chunks/500-ea37cc8de90441f7.js","246","static/chunks/246-b5d14933cf331e85.js","443","static/chunks/app/console/page-75d851a154677dbc.js"],"default",1]
4:I[4707,[],""]
5:I[6423,[],""]
0:["UmN0Ou_Iv2TWI4xcHvY2X",[[["",{"children":["console",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["console",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","console","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
0:["Bqx_FrY3OzE8SRa_XO6pv",[[["",{"children":["console",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["console",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","console","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
1:null

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
2:I[9107,[],"ClientPageRoot"]
3:I[1499,["500","static/chunks/500-ea37cc8de90441f7.js","719","static/chunks/719-4dbe9dca4cff8afd.js","246","static/chunks/246-b5d14933cf331e85.js","931","static/chunks/app/page-3c5cf076d3a5e7c5.js"],"default",1]
3:I[1499,["500","static/chunks/500-ea37cc8de90441f7.js","719","static/chunks/719-4dbe9dca4cff8afd.js","246","static/chunks/246-b5d14933cf331e85.js","931","static/chunks/app/page-a84b3c1d3686233c.js"],"default",1]
4:I[4707,[],""]
5:I[6423,[],""]
0:["UmN0Ou_Iv2TWI4xcHvY2X",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
0:["Bqx_FrY3OzE8SRa_XO6pv",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
1:null

File diff suppressed because one or more lines are too long

View File

@@ -2,6 +2,6 @@
3:I[3398,["500","static/chunks/500-ea37cc8de90441f7.js","246","static/chunks/246-b5d14933cf331e85.js","6","static/chunks/app/shellcrash/page-3d8615ae919b9119.js"],"default",1]
4:I[4707,[],""]
5:I[6423,[],""]
0:["UmN0Ou_Iv2TWI4xcHvY2X",[[["",{"children":["shellcrash",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["shellcrash",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","shellcrash","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
0:["Bqx_FrY3OzE8SRa_XO6pv",[[["",{"children":["shellcrash",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["shellcrash",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","shellcrash","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/693f7950f3222bce.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"zh-CN","data-theme":"dark","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__variable_1e4310 __variable_c3aa02 font-sans antialiased","children":[["$","div",null,{"className":"app-bg-layer","aria-hidden":true}],["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]]}]}]],null],null],["$L6",null]]]]
6:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"无人直播系统 · live.local"}],["$","meta","3",{"name":"description","content":"Linux ARM/x86 一键部署的无人直播控制台YouTube 转播、TikTok HDMI、SRS、Neko 与系统服务"}],["$","link","4",{"rel":"icon","href":"/favicon.ico","type":"image/x-icon","sizes":"16x16"}],["$","meta","5",{"name":"next-size-adjust"}]]
1:null

View File

@@ -6,7 +6,9 @@ import json
import os
import re
import signal
import sqlite3
import sys
import time
from contextlib import asynccontextmanager, suppress
from pathlib import Path
from typing import Any, Optional
@@ -362,6 +364,10 @@ def _persist_youtube_pro_channel_edits(
return updated
def _youtube_pro_store_error(exc: sqlite3.Error) -> str:
return f"多频道状态数据库写入失败: {exc}; 请确认 config 目录归 live 用户可写"
def _validate_youtube_runtime_files(process: str) -> list[str]:
return validate_youtube_runtime_files(BASE_DIR, CONFIG_DIR, process, get_youtube_pro_channels())
@@ -553,6 +559,8 @@ async def alias_youtube_pro_channels_post(body: YoutubeProChannelsBody):
set_youtube_pro_channels(body.channels)
except ValueError as exc:
return JSONResponse({"error": str(exc)}, status_code=400)
except sqlite3.Error as exc:
return JSONResponse({"error": _youtube_pro_store_error(exc)}, status_code=500)
capacity = await asyncio.to_thread(ensure_neko_capacity_for_youtube_channels, BASE_DIR, body.channels)
return {"message": "ok", "neko": capacity}
@@ -570,6 +578,35 @@ _NO_STORE_HEADERS = {
"Expires": "0",
}
_READ_CONFIG_MIN_INTERVAL_SECONDS = 0.45
_READ_CONFIG_LAST_SEEN: dict[tuple[str, str, str], float] = {}
def _client_host(request: Request) -> str:
forwarded = (request.headers.get("x-forwarded-for") or "").split(",", 1)[0].strip()
if forwarded:
return forwarded
return request.client.host if request.client else "unknown"
def _reject_config_read_storm(request: Request, endpoint: str, process: str) -> JSONResponse | None:
now = time.monotonic()
if len(_READ_CONFIG_LAST_SEEN) > 2048:
cutoff = now - 30.0
for key, seen_at in list(_READ_CONFIG_LAST_SEEN.items()):
if seen_at < cutoff:
_READ_CONFIG_LAST_SEEN.pop(key, None)
key = (_client_host(request), endpoint, (process or "").strip())
last = _READ_CONFIG_LAST_SEEN.get(key, 0.0)
_READ_CONFIG_LAST_SEEN[key] = now
if now - last >= _READ_CONFIG_MIN_INTERVAL_SECONDS:
return None
return JSONResponse(
{"error": "刷新过快,请稍后重试"},
status_code=429,
headers={**_NO_STORE_HEADERS, "Retry-After": "1"},
)
class ProcessNameBody(BaseModel):
process: str = Field(..., min_length=1, max_length=160)
@@ -1112,7 +1149,10 @@ async def get_system_snapshot():
# ---------------- 配置路由youtube.ini仅 youtube ----------------
@app.get("/get_config")
async def get_config(process: str = Query(..., description="进程名")):
async def get_config(request: Request, process: str = Query(..., description="进程名")):
storm = _reject_config_read_storm(request, "get_config", process)
if storm is not None:
return storm
rel = managed_youtube_ini_relpath(process)
if not rel:
return JSONResponse(
@@ -1156,13 +1196,18 @@ async def save_config(request: Request, process: str = Query(..., description="
governed_save_managed_file("app-config", rel, content)
_persist_youtube_pro_channel_edits(process, youtube_ini_content=content)
return JSONResponse({"message": "配置保存成功"}, headers=_NO_STORE_HEADERS)
except sqlite3.Error as e:
return JSONResponse({"error": _youtube_pro_store_error(e)}, status_code=500)
except OSError as e:
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)
# ---------------- 配置路由URL_config.ini ----------------
@app.get("/get_url_config")
async def get_url_config(process: str = Query(..., description="进程名")):
async def get_url_config(request: Request, process: str = Query(..., description="进程名")):
storm = _reject_config_read_storm(request, "get_url_config", process)
if storm is not None:
return storm
if not allows_url_config(process):
return JSONResponse(
{"error": "仅 tiktok*.py / youtube*.py / douyin_youtube*.py 对应进程支持 URL 配置编辑"},
@@ -1210,6 +1255,8 @@ async def save_url_config(request: Request, process: str = Query(..., descriptio
governed_save_managed_file("app-config", rel, content)
_persist_youtube_pro_channel_edits(process, url_content=content)
return JSONResponse({"message": "配置保存成功"}, headers=_NO_STORE_HEADERS)
except sqlite3.Error as e:
return JSONResponse({"error": _youtube_pro_store_error(e)}, status_code=500)
except OSError as e:
return JSONResponse({"error": f"保存失败: {str(e)}"}, status_code=500)