This commit is contained in:
root
2026-05-17 05:22:53 +00:00
parent 07aa02bca6
commit 03dc47a569
24 changed files with 941 additions and 93 deletions

View File

@@ -21,7 +21,9 @@ import {
loadYoutubeProChannels,
newChannelId,
pushYoutubeProChannelsRemote,
resolveYoutubeProChannelPm2,
saveYoutubeProChannels,
sanitizeYoutubePm2Name,
type YoutubeProChannel,
} from "@/lib/youtubeProChannels";
import { normalizeBrowserReachableHttpUrl } from "@/lib/panelUrls";
@@ -120,14 +122,32 @@ function preferredSingleProcessPm2FromRuntime(entries: ProcessEntry[], liveRows:
}
function resolveChannelPm2(channel: YoutubeProChannel): string {
return (channel.pm2Name ?? "").trim() || defaultPm2NameForChannel(channel.id);
return resolveYoutubeProChannelPm2(channel);
}
const MODE_KEY = "live-hub-youtube-mode-v1";
const PRO_PM2_RESERVED = new Set(["tiktok", "obs", "web"]);
/** UI 暂时隐藏「多频道 Pro」切换(DOM 与逻辑保留,改 false 即可恢复) */
const HIDE_YOUTUBE_MULTI_PRO_TOGGLE = false;
type QuickLinkMap = Record<string, string>;
type ProLaneDraft = {
name: string;
streamKey: string;
pm2Name: string;
notes: string;
};
function laneDraftFromChannel(channel: YoutubeProChannel): ProLaneDraft {
return {
name: channel.name,
streamKey: channel.streamKey ?? "",
pm2Name: channel.pm2Name ?? defaultPm2NameForChannel(channel.id),
notes: channel.notes ?? "",
};
}
type LiveProcRow = {
pm2: string;
script?: string;
@@ -171,6 +191,17 @@ function businessStatusBadgeClass(row: LiveProcRow | undefined): string {
return "bg-zinc-600/20 text-zinc-400 ring-zinc-500/25";
}
function nonCommentUrlLinesOf(text: string): string[] {
return text
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#"));
}
function shouldPreserveLocalUrlDraft(localText: string, remoteText: string): boolean {
return nonCommentUrlLinesOf(localText).length > 0 && nonCommentUrlLinesOf(remoteText).length === 0;
}
function parseProcBusy(busy: string | null): { action: string; pm2: string } | null {
if (!busy?.startsWith("proc:")) return null;
const rest = busy.slice(5);
@@ -281,6 +312,7 @@ type Props = {
liveProcesses: LiveProcRow[];
notify: (msg: string) => void;
onNavigate?: (target: PortalNavTarget) => void;
quickLinks?: QuickLinkMap;
};
export function LiveYoutubeUnmannedView({
@@ -297,6 +329,7 @@ export function LiveYoutubeUnmannedView({
liveProcesses,
notify,
onNavigate,
quickLinks,
}: Props) {
const ytEntries = useMemo(() => entries.filter(isYoutubeProcess), [entries]);
const staticYoutubeEntries = useMemo(() => ytEntries.filter((entry) => isStaticYoutubeControlEntry(entry)), [ytEntries]);
@@ -326,6 +359,9 @@ export function LiveYoutubeUnmannedView({
const [urlReloading, setUrlReloading] = useState(false);
const [urlOptimizing, setUrlOptimizing] = useState(false);
const [urlFetchNote, setUrlFetchNote] = useState<string | null>(null);
const [laneDrafts, setLaneDrafts] = useState<Record<string, ProLaneDraft>>({});
const [laneSaving, setLaneSaving] = useState(false);
const [laneStatus, setLaneStatus] = useState<string | null>(null);
const urlListDirtyRef = useRef(false);
useEffect(() => {
@@ -342,6 +378,11 @@ export function LiveYoutubeUnmannedView({
if (urlListDirtyRef.current) return;
if (mode === "single") setUrlConfig(c);
else {
const localDraft = channels.find((row) => row.id === activeCh)?.urlLines ?? "";
if (shouldPreserveLocalUrlDraft(localDraft, c)) {
setProUrlDraft(localDraft);
return;
}
setProUrlDraft(c);
if (activeCh) {
setChannels((prev) => {
@@ -354,7 +395,7 @@ export function LiveYoutubeUnmannedView({
} catch {
/* ignore */
}
}, [activeCh, currentProc, fetchJson, mode, setUrlConfig]);
}, [activeCh, channels, currentProc, fetchJson, mode, setUrlConfig]);
useEffect(() => {
if (!currentProc || !urlAutoRefresh) return;
@@ -381,6 +422,12 @@ export function LiveYoutubeUnmannedView({
}
if (mode === "single") setUrlConfig(c);
else {
const localDraft = channels.find((row) => row.id === activeCh)?.urlLines ?? "";
if (shouldPreserveLocalUrlDraft(localDraft, c)) {
setProUrlDraft(localDraft);
setUrlFetchNote(t("服务器暂无已保存 URL,已保留本地草稿", "Server has no saved URLs, kept the local draft"));
return;
}
setProUrlDraft(c);
if (activeCh) {
setChannels((prev) => {
@@ -480,21 +527,134 @@ export function LiveYoutubeUnmannedView({
}
}, [currentProc, mode, preferredSingleProc, rowForPm2, setCurrentProc, singleProcessOptions]);
const applyChannelsLocal = useCallback((next: YoutubeProChannel[]) => {
setChannels(next);
saveYoutubeProChannels(next);
}, []);
const syncChannelsRemote = useCallback(
async (next: YoutubeProChannel[]) => {
await pushYoutubeProChannelsRemote(fetchJson, next);
},
[fetchJson],
);
const persistChannels = useCallback(
(next: YoutubeProChannel[]) => {
setChannels(next);
saveYoutubeProChannels(next);
void pushYoutubeProChannelsRemote(fetchJson, next).catch((e) => {
applyChannelsLocal(next);
void syncChannelsRemote(next).catch((e) => {
notify(
`${t("多频道列表未同步到服务器", "Channel list not saved on server")}: ${(e as Error).message}`,
);
});
},
[fetchJson, notify, t],
[applyChannelsLocal, notify, syncChannelsRemote, t],
);
const active = channels.find((c) => c.id === activeCh) || channels[0];
const activeSavedUrlDraft = useMemo(() => (active?.urlLines ?? "").trim(), [active?.urlLines]);
const activeLaneDraft = useMemo(
() => (active ? laneDrafts[active.id] ?? laneDraftFromChannel(active) : null),
[active, laneDrafts],
);
useEffect(() => {
if (!active) return;
setLaneDrafts((prev) => {
if (prev[active.id]) return prev;
return { ...prev, [active.id]: laneDraftFromChannel(active) };
});
}, [active]);
useEffect(() => {
setLaneStatus(null);
}, [activeCh]);
const setLaneDraftField = useCallback(
(key: keyof ProLaneDraft, value: string) => {
if (!active) return;
setLaneDrafts((prev) => ({
...prev,
[active.id]: {
...(prev[active.id] ?? laneDraftFromChannel(active)),
[key]: value,
},
}));
setLaneStatus(null);
},
[active],
);
const activeSavedPm2 = active ? resolveChannelPm2(active) : "";
const activeDraftPm2 = active && activeLaneDraft
? resolveYoutubeProChannelPm2({ id: active.id, pm2Name: activeLaneDraft.pm2Name })
: "";
const activePm2PreviewChanged = !!active && !!activeLaneDraft && activeDraftPm2 !== activeSavedPm2;
const activeLaneValidation = useMemo(() => {
if (!active || !activeLaneDraft) {
return {
ok: false,
message: t("请选择线路", "Pick a lane"),
normalizedName: "",
normalizedPm2: "",
normalizedStreamKey: "",
normalizedNotes: "",
};
}
const normalizedName = activeLaneDraft.name.trim() || `${t("线路", "Ch")} ${channels.findIndex((row) => row.id === active.id) + 1}`;
const normalizedPm2 = resolveYoutubeProChannelPm2({ id: active.id, pm2Name: activeLaneDraft.pm2Name });
if (!normalizedPm2) {
return {
ok: false,
message: t("进程名不能为空", "PM2 name is required"),
normalizedName,
normalizedPm2,
normalizedStreamKey: activeLaneDraft.streamKey.trim(),
normalizedNotes: activeLaneDraft.notes.trim(),
};
}
if (PRO_PM2_RESERVED.has(normalizedPm2)) {
return {
ok: false,
message: t("该进程名保留给系统模块,请换一个", "This PM2 name is reserved"),
normalizedName,
normalizedPm2,
normalizedStreamKey: activeLaneDraft.streamKey.trim(),
normalizedNotes: activeLaneDraft.notes.trim(),
};
}
const duplicate = channels.find((row) => row.id !== active.id && resolveChannelPm2(row) === normalizedPm2);
if (duplicate) {
return {
ok: false,
message: t("该进程名已被其它线路占用", "Another lane already uses this PM2 name"),
normalizedName,
normalizedPm2,
normalizedStreamKey: activeLaneDraft.streamKey.trim(),
normalizedNotes: activeLaneDraft.notes.trim(),
};
}
return {
ok: true,
message: "",
normalizedName,
normalizedPm2,
normalizedStreamKey: activeLaneDraft.streamKey.trim(),
normalizedNotes: activeLaneDraft.notes.trim(),
};
}, [active, activeLaneDraft, channels, t]);
const activeLaneDirty = useMemo(() => {
if (!active || !activeLaneDraft) return false;
const savedDraft = laneDraftFromChannel(active);
return (
activeLaneDraft.name !== savedDraft.name ||
activeLaneDraft.streamKey !== savedDraft.streamKey ||
sanitizeYoutubePm2Name(activeLaneDraft.pm2Name) !== sanitizeYoutubePm2Name(savedDraft.pm2Name) ||
activeLaneDraft.notes !== savedDraft.notes
);
}, [active, activeLaneDraft]);
const setModePersist = useCallback(
(m: "single" | "pro") => {
@@ -574,6 +734,11 @@ export function LiveYoutubeUnmannedView({
);
if (cancelled || urlListDirtyRef.current) return;
const c = data.content ?? "";
const localDraft = channels.find((row) => row.id === targetChannelId)?.urlLines ?? "";
if (shouldPreserveLocalUrlDraft(localDraft, c)) {
setProUrlDraft(localDraft);
return;
}
setProUrlDraft(c);
urlListDirtyRef.current = false;
setChannels((prev) => {
@@ -590,7 +755,7 @@ export function LiveYoutubeUnmannedView({
return () => {
cancelled = true;
};
}, [activeCh, activeSavedUrlDraft, currentProc, fetchJson, mode]);
}, [activeCh, activeSavedUrlDraft, channels, currentProc, fetchJson, mode]);
const loadYoutubeIni = useCallback(async () => {
if (!currentProc) return;
@@ -615,10 +780,80 @@ export function LiveYoutubeUnmannedView({
void loadYoutubeIni();
}, [currentProc, loadYoutubeIni]);
const updateActive = (patch: Partial<YoutubeProChannel>) => {
const persistActiveLaneDraft = useCallback(
async () => {
if (!active || !activeLaneDraft) {
throw new Error(t("请选择线路", "Pick a lane"));
}
if (!activeLaneValidation.ok) {
throw new Error(activeLaneValidation.message);
}
const next = channels.map((row) =>
row.id === active.id
? {
...row,
name: activeLaneValidation.normalizedName,
streamKey: activeLaneValidation.normalizedStreamKey,
pm2Name: activeLaneValidation.normalizedPm2,
notes: activeLaneValidation.normalizedNotes,
urlLines: proUrlDraft,
}
: row,
);
applyChannelsLocal(next);
await syncChannelsRemote(next);
setLaneDrafts((prev) => ({
...prev,
[active.id]: {
name: activeLaneValidation.normalizedName,
streamKey: activeLaneValidation.normalizedStreamKey,
pm2Name: activeLaneValidation.normalizedPm2,
notes: activeLaneValidation.normalizedNotes,
},
}));
return {
next,
savedLane:
next.find((row) => row.id === active.id) ??
({
...active,
name: activeLaneValidation.normalizedName,
streamKey: activeLaneValidation.normalizedStreamKey,
pm2Name: activeLaneValidation.normalizedPm2,
notes: activeLaneValidation.normalizedNotes,
urlLines: proUrlDraft,
} satisfies YoutubeProChannel),
pm2: activeLaneValidation.normalizedPm2,
};
},
[
active,
activeLaneDraft,
activeLaneValidation,
applyChannelsLocal,
channels,
proUrlDraft,
syncChannelsRemote,
t,
],
);
const saveProLaneSettings = useCallback(async () => {
if (!active) return;
persistChannels(channels.map((c) => (c.id === active.id ? { ...c, ...patch } : c)));
};
setLaneSaving(true);
setLaneStatus(null);
try {
const saved = await persistActiveLaneDraft();
setCurrentProc(saved.pm2);
setLaneStatus(t("线路设置已保存", "Lane settings saved"));
} catch (e) {
const message = (e as Error).message;
setLaneStatus(message);
notify(message);
} finally {
setLaneSaving(false);
}
}, [active, notify, persistActiveLaneDraft, setCurrentProc, t]);
const saveYoutubeIniToServer = async (content: string, processPm2 = currentProc) => {
const targetPm2 = processPm2.trim();
@@ -682,18 +917,24 @@ export function LiveYoutubeUnmannedView({
const keySuffixUi = useMemo(() => {
if (mode === "single") return parseYoutubeStreamKeySuffix(`key = ${ytFields.key}`);
const k = active?.streamKey ?? "";
const k = activeLaneDraft?.streamKey ?? active?.streamKey ?? "";
return parseYoutubeStreamKeySuffix(`key = ${k}`);
}, [mode, ytFields.key, active?.streamKey]);
}, [mode, ytFields.key, active?.streamKey, activeLaneDraft?.streamKey]);
const saveProUrlAndServer = async () => {
if (!active) return;
updateActive({ urlLines: proUrlDraft });
try {
await Promise.resolve(onSaveUrlConfig(proUrlDraft, resolveChannelPm2(active)));
setLaneSaving(true);
setLaneStatus(null);
const saved = await persistActiveLaneDraft();
setCurrentProc(saved.pm2);
await Promise.resolve(onSaveUrlConfig(proUrlDraft, saved.pm2));
urlListDirtyRef.current = false;
setLaneStatus(t("线路与地址已保存", "Lane settings and URLs saved"));
} catch {
/* parent 已 toast */
} finally {
setLaneSaving(false);
}
};
@@ -725,7 +966,6 @@ export function LiveYoutubeUnmannedView({
if (mode === "single") setUrlConfig(next);
else {
setProUrlDraft(next);
if (active) updateActive({ urlLines: next });
}
urlListDirtyRef.current = true;
setUrlFetchNote(data.message || t("已去重当前 URL 列表", "Deduplicated current URL list"));
@@ -737,76 +977,102 @@ export function LiveYoutubeUnmannedView({
};
const pushProStreamKeyToServer = async () => {
if (!active) return;
if (!active || !activeLaneDraft) return;
const base = parseYoutubeIniFields(youtubeIniText || buildYoutubeIniFromFields(ytFields));
const f: YoutubeIniFields = {
key: (active.streamKey ?? "").trim(),
key: activeLaneDraft.streamKey.trim(),
rtmps: base.rtmps,
bitrate: base.bitrate,
fastAudio: base.fastAudio,
};
const body = buildYoutubeIniFromFields(f);
await saveYoutubeIniToServer(body, resolveChannelPm2(active));
updateActive({ streamKey: f.key });
setLaneSaving(true);
setLaneStatus(null);
try {
const saved = await persistActiveLaneDraft();
setCurrentProc(saved.pm2);
await saveYoutubeIniToServer(body, saved.pm2);
setLaneStatus(t("线路设置与 youtube.ini 已保存", "Lane settings and youtube.ini saved"));
} catch (e) {
const message = (e as Error).message;
setLaneStatus(message);
notify(message);
} finally {
setLaneSaving(false);
}
};
const nonCommentUrlLines = useCallback(
(text: string) =>
text
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && !line.startsWith("#")),
[],
const currentUrlDraft = mode === "single" ? urlConfig : proUrlDraft;
const currentUrlCount = useMemo(() => nonCommentUrlLinesOf(currentUrlDraft).length, [currentUrlDraft]);
const currentDouyinCount = useMemo(() => extractDouyinLiveUrls(currentUrlDraft).length, [currentUrlDraft]);
const laneQuickLinks = useMemo(
() =>
[
quickLinks?.neko_browser,
quickLinks?.chromium_remote_debug,
quickLinks?.srs_api,
].filter((value): value is string => !!value),
[quickLinks],
);
const flushProChannelsBeforeRun = useCallback(
async (targetPm2: string) => {
let next = channels;
const next = channels;
const activePm2 = active ? resolveChannelPm2(active) : "";
if (active && targetPm2 === activePm2) {
next = channels.map((row) => (row.id === active.id ? { ...row, urlLines: proUrlDraft } : row));
const saved = await persistActiveLaneDraft();
return saved;
}
setChannels(next);
saveYoutubeProChannels(next);
await pushYoutubeProChannelsRemote(fetchJson, next);
return next;
applyChannelsLocal(next);
await syncChannelsRemote(next);
return {
next,
savedLane: active ?? null,
pm2: targetPm2,
};
},
[active, channels, fetchJson, proUrlDraft],
[active, applyChannelsLocal, channels, persistActiveLaneDraft, syncChannelsRemote],
);
const runProProcess = useCallback(
async (action: "start" | "stop" | "restart", targetPm2: string) => {
const targetMatchesActiveLane = !!active && resolveChannelPm2(active) === targetPm2;
if (action !== "stop") {
let next = channels;
let effectivePm2 = targetPm2;
try {
next = await flushProChannelsBeforeRun(targetPm2);
const saved = await flushProChannelsBeforeRun(targetPm2);
next = saved.next;
effectivePm2 = saved.pm2;
} catch (e) {
notify(
`${t("多频道列表未同步到服务器", "Channel list not saved on server")}: ${(e as Error).message}`,
`${t("当前线路保存失败", "Failed to save the active lane")}: ${(e as Error).message}`,
);
return;
}
const target = next.find(
(row) => resolveChannelPm2(row) === targetPm2,
(row) => resolveChannelPm2(row) === effectivePm2,
);
const activePm2 = active ? resolveChannelPm2(active) : "";
const urlText = targetPm2 === activePm2 ? proUrlDraft : target?.urlLines ?? "";
const urlText = target?.urlLines ?? "";
if (!((target?.streamKey ?? "").trim())) {
notify(t("请先填写当前线路的推流密钥", "Fill the stream key before starting"));
return;
}
if (!nonCommentUrlLines(urlText).length) {
if (!nonCommentUrlLinesOf(urlText).length) {
notify(t("请先填写当前线路的直播地址", "Fill the source URLs before starting"));
return;
}
targetPm2 = effectivePm2;
}
if (targetMatchesActiveLane) {
setCurrentProc(targetPm2);
}
setCurrentProc(targetPm2);
onRunProcess(action, targetPm2);
},
[active, channels, flushProChannelsBeforeRun, nonCommentUrlLines, notify, onRunProcess, proUrlDraft, setCurrentProc, t],
[active, channels, flushProChannelsBeforeRun, notify, onRunProcess, setCurrentProc, t],
);
const lock = !!busy || ytIniLoading || urlReloading || urlOptimizing;
const lock = !!busy || ytIniLoading || urlReloading || urlOptimizing || laneSaving;
const singleRow = rowForPm2(currentProc);
const singleOnline = pm2StatusOnline(statusForPm2(currentProc));
@@ -869,7 +1135,7 @@ export function LiveYoutubeUnmannedView({
label: "Neko",
sub: t("Chromium 工作室", "Chromium"),
gradient: "from-emerald-400 to-teal-500",
openUrl: normalizeBrowserReachableHttpUrl("http://live.local:9200/"),
openUrl: normalizeBrowserReachableHttpUrl(quickLinks?.neko_browser || "http://live.local:9200/"),
},
]}
/>
@@ -920,6 +1186,41 @@ export function LiveYoutubeUnmannedView({
</div>
</div>
{laneQuickLinks.length ? (
<div className="flex flex-wrap gap-2">
{quickLinks?.neko_browser ? (
<a
href={normalizeBrowserReachableHttpUrl(quickLinks.neko_browser)}
target="_blank"
rel="noreferrer"
className="rounded-lg border border-emerald-500/20 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-100 ring-1 ring-emerald-500/20"
>
{t("打开 Neko 工作室", "Open Neko studio")}
</a>
) : null}
{quickLinks?.chromium_remote_debug ? (
<a
href={normalizeBrowserReachableHttpUrl(quickLinks.chromium_remote_debug)}
target="_blank"
rel="noreferrer"
className="rounded-lg border border-cyan-500/20 bg-cyan-500/10 px-3 py-2 text-xs text-cyan-100 ring-1 ring-cyan-500/20"
>
{t("打开 Chromium 调试", "Open Chromium debug")}
</a>
) : null}
{quickLinks?.srs_api ? (
<a
href={normalizeBrowserReachableHttpUrl(quickLinks.srs_api)}
target="_blank"
rel="noreferrer"
className="rounded-lg border border-violet-500/20 bg-violet-500/10 px-3 py-2 text-xs text-violet-100 ring-1 ring-violet-500/20"
>
{t("打开 SRS API", "Open SRS API")}
</a>
) : null}
</div>
) : null}
<div className="rounded-2xl border border-amber-500/20 bg-zinc-950/60 p-6 ring-1 ring-white/[0.04]">
<div className="flex items-center gap-2 text-amber-200/90">
<Radio className="h-4 w-4" />
@@ -1001,7 +1302,9 @@ export function LiveYoutubeUnmannedView({
const pm = resolveChannelPm2(c);
const row = rowForPm2(pm);
const st = statusForPm2(pm);
const dUrl = extractDouyinLiveUrls((c.urlLines ?? "").trim())[0];
const laneUrls = nonCommentUrlLinesOf((c.urlLines ?? "").trim());
const douyinUrls = extractDouyinLiveUrls((c.urlLines ?? "").trim());
const dUrl = douyinUrls[0];
const suf = parseYoutubeStreamKeySuffix(`key = ${c.streamKey ?? ""}`);
const sel = activeCh === c.id;
const rowOnline = pm2StatusOnline(st);
@@ -1035,6 +1338,14 @@ export function LiveYoutubeUnmannedView({
<span className="truncate" title={pm}>
{pm}
</span>
<span className="text-zinc-600">·</span>
<span>
URL {laneUrls.length}
</span>
<span className="text-zinc-600">·</span>
<span>
DY {douyinUrls.length}
</span>
</div>
{dUrl ? (
<a
@@ -1222,10 +1533,18 @@ export function LiveYoutubeUnmannedView({
{active ? (
<span className="ml-2 font-normal text-zinc-400">· {active.name}</span>
) : null}
{activeLaneDirty ? (
<span className="ml-2 rounded-md bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium text-amber-100 ring-1 ring-amber-500/25">
{t("有未保存修改", "Unsaved")}
</span>
) : null}
</h3>
{active ? (
<p className="mt-0.5 font-mono text-[11px] text-zinc-500">
PM2: {(active.pm2Name ?? "").trim() || defaultPm2NameForChannel(active.id)} · key {keySuffixUi || "—"}
PM2: {activeSavedPm2}
{activePm2PreviewChanged ? ` → ${activeDraftPm2}` : ""}
{" · "}
key {keySuffixUi || "—"}
</p>
) : null}
</div>
@@ -1258,50 +1577,73 @@ export function LiveYoutubeUnmannedView({
<div className="mt-4 space-y-3 border-t border-white/5 pt-4">
<input
className="w-full rounded-xl border border-white/10 bg-black/40 px-3 py-2 text-sm"
value={active.name}
onChange={(e) => updateActive({ name: e.target.value })}
value={activeLaneDraft?.name ?? ""}
onChange={(e) => setLaneDraftField("name", e.target.value)}
placeholder={t("线路名称", "Lane name")}
/>
<label className="block text-xs text-zinc-500">{t("串流密钥", "Stream key")}</label>
<input
className="w-full rounded-xl border border-white/10 bg-black/40 px-3 py-2 font-mono text-sm"
spellCheck={false}
value={active.streamKey ?? ""}
onChange={(e) => updateActive({ streamKey: e.target.value })}
value={activeLaneDraft?.streamKey ?? ""}
onChange={(e) => setLaneDraftField("streamKey", e.target.value)}
placeholder="xxxx-xxxx… / rtmp(s)://…"
/>
<div className="flex flex-wrap gap-2">
<button
type="button"
disabled={lock}
onClick={() => void saveProLaneSettings()}
className="rounded-xl border border-white/10 bg-white/[0.05] px-4 py-2 text-sm text-zinc-100"
>
{laneSaving ? t("保存中...", "Saving...") : t("保存线路设置", "Save lane")}
</button>
<button
type="button"
disabled={lock}
onClick={() => void pushProStreamKeyToServer()}
className="rounded-xl bg-violet-500/25 px-4 py-2 text-sm font-medium text-violet-100 ring-1 ring-violet-500/35"
>
{t("保存", "Save")}
</button>
>
{t("写入 youtube.ini", "Write youtube.ini")}
</button>
</div>
{!activeLaneValidation.ok ? (
<p className="text-xs text-rose-300">{activeLaneValidation.message}</p>
) : null}
<details className="rounded-xl border border-white/5 bg-black/20 p-3">
<summary className="cursor-pointer text-xs text-zinc-500">{t("高级:进程名", "Advanced: process name")}</summary>
<input
className="mt-2 w-full rounded-lg border border-white/10 bg-black/40 px-3 py-2 font-mono text-xs"
value={active.pm2Name ?? defaultPm2NameForChannel(active.id)}
onChange={(e) => updateActive({ pm2Name: e.target.value })}
value={activeLaneDraft?.pm2Name ?? defaultPm2NameForChannel(active.id)}
onChange={(e) => setLaneDraftField("pm2Name", e.target.value)}
spellCheck={false}
placeholder="youtube2__…"
/>
<p className="mt-2 font-mono text-[11px] text-zinc-500">
{t("实际生效", "Effective")}: {activeDraftPm2 || "—"}
</p>
{activeLaneValidation.message && !activeLaneValidation.ok ? (
<p className="mt-2 text-[11px] text-rose-300">{activeLaneValidation.message}</p>
) : null}
</details>
<textarea
className="h-16 w-full resize-y rounded-xl border border-white/10 bg-black/40 p-3 text-xs text-zinc-300"
value={active.notes ?? ""}
onChange={(e) => updateActive({ notes: e.target.value })}
value={activeLaneDraft?.notes ?? ""}
onChange={(e) => setLaneDraftField("notes", e.target.value)}
placeholder={t("备注(可选)", "Notes (optional)")}
/>
{laneStatus ? <p className="text-xs text-zinc-400">{laneStatus}</p> : null}
<button
type="button"
disabled={lock || channels.length < 2}
className="inline-flex items-center gap-1 text-xs text-rose-300 hover:text-rose-200 disabled:opacity-30"
onClick={() => {
const next = channels.filter((c) => c.id !== active.id);
setLaneDrafts((prev) => {
const clone = { ...prev };
delete clone[active.id];
return clone;
});
persistChannels(next);
setActiveCh(next[0]?.id || "");
}}
@@ -1326,6 +1668,14 @@ export function LiveYoutubeUnmannedView({
? managedUrlConfigRelPathForPm2(currentProc)
: "URL_config.ini"}
</p>
<div className="mt-3 flex flex-wrap gap-2 text-[11px]">
<span className="rounded-full border border-cyan-500/20 bg-cyan-500/10 px-2.5 py-1 text-cyan-100">
{t("有效地址", "Valid URLs")}: {currentUrlCount}
</span>
<span className="rounded-full border border-violet-500/20 bg-violet-500/10 px-2.5 py-1 text-violet-100">
Douyin: {currentDouyinCount}
</span>
</div>
{urlToolbar}
{urlFetchNote ? <p className="mt-2 text-xs text-zinc-500">{urlFetchNote}</p> : null}
<textarea
@@ -1358,6 +1708,14 @@ export function LiveYoutubeUnmannedView({
<p className="mt-1 text-xs text-zinc-500">
{t("一行一个地址;可自动刷新或手动重新读取。", "One URL per line; auto-refresh or reload.")}
</p>
<div className="mt-3 flex flex-wrap gap-2 text-[11px]">
<span className="rounded-full border border-cyan-500/20 bg-cyan-500/10 px-2.5 py-1 text-cyan-100">
{t("有效地址", "Valid URLs")}: {currentUrlCount}
</span>
<span className="rounded-full border border-violet-500/20 bg-violet-500/10 px-2.5 py-1 text-violet-100">
Douyin: {currentDouyinCount}
</span>
</div>
{urlToolbar}
{urlFetchNote ? <p className="mt-2 text-xs text-zinc-500">{urlFetchNote}</p> : null}
<textarea

View File

@@ -13,6 +13,16 @@ export type YoutubeProChannel = {
};
const STORAGE_KEY = "live-hub-youtube-pro-v2";
const PM2_SANITIZE_RE = /[^a-zA-Z0-9_.-]+/g;
export function sanitizeYoutubePm2Name(value: string): string {
return (value || "").trim().replace(PM2_SANITIZE_RE, "_");
}
export function resolveYoutubeProChannelPm2(channel: Pick<YoutubeProChannel, "id" | "pm2Name">): string {
const raw = sanitizeYoutubePm2Name(String(channel.pm2Name || ""));
return raw || defaultPm2NameForChannel(String(channel.id || ""));
}
export function parseYoutubeProChannelList(raw: unknown): YoutubeProChannel[] {
if (!Array.isArray(raw)) return [];
@@ -116,5 +126,5 @@ export function newChannelId(): string {
}
export function defaultPm2NameForChannel(channelId: string): string {
return `youtube2__${channelId}`;
return `youtube2__${sanitizeYoutubePm2Name(channelId) || "channel"}`;
}

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-fafe28420dae1d39.js","357","static/chunks/app/android/page-84550dabda18d658.js"],"default",1]
4:I[4707,[],""]
5:I[6423,[],""]
0:["DYGAc5F7lnL_yZfXw32eU",[[["",{"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/dc08974264f65939.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:["lVyIIaGIEEEd2dYJHSxM8",[[["",{"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/91df3d4117d74443.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-fafe28420dae1d39.js","443","static/chunks/app/console/page-75d851a154677dbc.js"],"default",1]
4:I[4707,[],""]
5:I[6423,[],""]
0:["DYGAc5F7lnL_yZfXw32eU",[[["",{"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/dc08974264f65939.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:["lVyIIaGIEEEd2dYJHSxM8",[[["",{"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/91df3d4117d74443.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-fafe28420dae1d39.js","931","static/chunks/app/page-c6e5e4273c433d42.js"],"default",1]
3:I[1499,["500","static/chunks/500-ea37cc8de90441f7.js","719","static/chunks/719-4dbe9dca4cff8afd.js","246","static/chunks/246-fafe28420dae1d39.js","931","static/chunks/app/page-9b99cd3d2c60f238.js"],"default",1]
4:I[4707,[],""]
5:I[6423,[],""]
0:["DYGAc5F7lnL_yZfXw32eU",[[["",{"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/dc08974264f65939.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:["lVyIIaGIEEEd2dYJHSxM8",[[["",{"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/91df3d4117d74443.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-fafe28420dae1d39.js","6","static/chunks/app/shellcrash/page-3d8615ae919b9119.js"],"default",1]
4:I[4707,[],""]
5:I[6423,[],""]
0:["DYGAc5F7lnL_yZfXw32eU",[[["",{"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/dc08974264f65939.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:["lVyIIaGIEEEd2dYJHSxM8",[[["",{"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/91df3d4117d74443.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