"use client"; import { ExternalLink, Pause, Play, RefreshCw } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { extractDouyinCurrentRoomFromLog, extractDouyinLiveUrls, extractDouyinRoomHints, } from "@/lib/youtubeConfigUtils"; type TFn = (zh: string, en: string) => string; type StatusPayload = { process_status?: string; recent_log?: string; recent_error?: string; }; function fmtDuration(sec: number): string { if (sec < 60) return `${sec}s`; const m = Math.floor(sec / 60); const s = sec % 60; if (m < 60) return `${m}m ${s}s`; const h = Math.floor(m / 60); const mm = m % 60; return `${h}h ${mm}m`; } function statusLine(t: TFn, raw: string | undefined): string { const s = (raw || "unknown").toLowerCase(); if (s === "online" || s === "running") return t("运行中(PM2/进程在线)", "Running (process online)"); if (s === "stopped" || s === "stopping") return t("已停止", "Stopped"); if (s === "not_found" || s === "errored") return t("未运行或未找到", "Not running / not found"); if (s === "invalid") return t("无效进程名", "Invalid process"); return raw || "—"; } type Props = { t: TFn; currentProc: string; fetchJson: (path: string, init?: RequestInit) => Promise; /** 用于源站房间线索(与 Electron 一致) */ urlListText: string; /** YouTube 密钥尾码;非 YouTube 进程可传空并关 showKeyRow */ keySuffixUi: string; showKeyRow?: boolean; pollMs?: number; }; export function LiveProcessLiveLogCard({ t, currentProc, fetchJson, urlListText, keySuffixUi, showKeyRow = true, pollMs = 1000, }: Props) { const [processStatus, setProcessStatus] = useState(""); const [recentLog, setRecentLog] = useState(""); const [recentError, setRecentError] = useState(""); const [paused, setPaused] = useState(false); const [liveSince, setLiveSince] = useState(null); const [nowTick, setNowTick] = useState(0); const outRef = useRef(null); const errRef = useRef(null); const prevOnlineRef = useRef(false); const douyinHints = extractDouyinRoomHints(urlListText); const onlineNow = /^(online|running)$/i.test(processStatus); const roomFromLog = extractDouyinCurrentRoomFromLog(recentLog); const fallbackUrl = extractDouyinLiveUrls(urlListText)[0] ?? ""; const primaryRoomId = roomFromLog || (fallbackUrl.match(/live\.douyin\.com\/([a-zA-Z0-9_-]+)/i)?.[1] ?? null); const primaryUrl = primaryRoomId ? `https://live.douyin.com/${primaryRoomId}` : fallbackUrl || null; const pull = useCallback(async () => { if (!currentProc) return; try { const data = await fetchJson( `/status?process=${encodeURIComponent(currentProc)}`, ); const ps = data.process_status || "unknown"; setProcessStatus(ps); setRecentLog(data.recent_log || ""); setRecentError(data.recent_error || ""); const online = /^(online|running)$/i.test(ps); if (online && !prevOnlineRef.current) { setLiveSince(Date.now()); } if (!online) { setLiveSince(null); } prevOnlineRef.current = online; } catch { setProcessStatus("error"); } }, [currentProc, fetchJson]); useEffect(() => { void pull(); }, [pull]); useEffect(() => { if (paused || !currentProc) return; const id = window.setInterval(() => void pull(), pollMs); return () => clearInterval(id); }, [paused, currentProc, pull, pollMs]); useEffect(() => { const onVis = () => { if (document.visibilityState === "visible") void pull(); }; document.addEventListener("visibilitychange", onVis); return () => document.removeEventListener("visibilitychange", onVis); }, [pull]); useEffect(() => { if (!liveSince) return; const id = window.setInterval(() => setNowTick((n) => n + 1), 1000); return () => clearInterval(id); }, [liveSince]); useEffect(() => { if (paused) return; const el = outRef.current; if (el) el.scrollTop = el.scrollHeight; }, [recentLog, paused]); useEffect(() => { if (paused) return; const el = errRef.current; if (el) el.scrollTop = el.scrollHeight; }, [recentError, paused]); const durationLabel = liveSince == null ? t("—(未检测到在线)", "— (not online)") : fmtDuration(Math.max(0, Math.floor((Date.now() - liveSince) / 1000))); return (
{nowTick}

{t("当前直播日志", "Live process log")}

{paused ? t("已暂停刷新", "Paused") : t("每秒刷新", "~1s refresh")}
{t("进程状态", "Status")} {statusLine(t, processStatus)}
{t("本次在线时长(估算)", "Online duration (est.)")} {durationLabel}
{showKeyRow ? (
{t("YouTube key 尾码(参考)", "YT key suffix")} {keySuffixUi || "—"}
) : null} {primaryUrl ? (
{roomFromLog ? t("当前直播源站(日志)", "Current source (from log)") : onlineNow ? t("配置中的源站地址", "Configured source URL") : t("源站快捷入口", "Source quick link")} {primaryRoomId || t("打开抖音直播页", "Open Douyin live")}
{douyinHints.length > 1 && roomFromLog ? (

{t("配置中还有", "Also in config:")}{" "} {douyinHints .filter((id) => id !== primaryRoomId) .slice(0, 3) .map((id) => ( {id} ))} {douyinHints.length > 4 ? "…" : null}

) : null}
) : (

{t("(日志与地址列表中暂无抖音房间)", "(No Douyin room in log or URL list)")}

)}
{processStatus !== "not_found" && processStatus !== "invalid" ? (
{t("进程输出(stdout / 合并日志)", "Process output")}
              {recentLog.trim() || t("(暂无)", "(empty)")}
            
{t("异常信息(stderr)", "Stderr")}
              {recentError.trim() || t("(暂无)", "(empty)")}
            
) : (

{t("进程未在 PM2 / 本地注册表中运行,日志区域已隐藏。", "Process not running — log panes hidden.")}

)}
); }