s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user