import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useBind } from '@/context/BindContext' import { apiFetch, apiJson } from '@/lib/api' import { getMultiChannelPro, setMultiChannelPro } from '@/lib/storage' import { isObsScript, isTiktokScript, isYoutubeScript, pm2NameFromScript } from '@/lib/scriptUtils' type Entry = { script: string; label: string; pm2: string } const UI_DROPDOWN_YOUTUBE_ONLY = true const UI_SHOW_YOUTUBE_GOOGLE_OAUTH = false type StatusVariant = | 'loading' | 'online' | 'stopped' | 'error' | 'warn' | 'offline' | 'unknown' | 'empty' function statusPresentation(processStatus: string): { line: string; variant: StatusVariant } { if (processStatus === 'online') return { line: '正在录制', variant: 'online' } if (processStatus === 'stopped') return { line: '已停止', variant: 'stopped' } if (processStatus === 'errored') return { line: '出现异常', variant: 'error' } if (processStatus === 'launching' || processStatus === 'starting') return { line: '正在启动…', variant: 'warn' } if (processStatus === 'waiting restart') return { line: '等待重启', variant: 'warn' } if (processStatus === 'not_found') return { line: '尚未开始', variant: 'offline' } if (processStatus === 'invalid') return { line: '无法识别', variant: 'error' } return { line: '状态未知', variant: 'unknown' } } export function useRecorder() { const { bind } = useBind() const [entries, setEntries] = useState([]) const [currentProcess, setCurrentProcess] = useState('') const [statusMeta, setStatusMeta] = useState<{ line: string; variant: StatusVariant }>({ line: '—', variant: 'loading', }) const [logText, setLogText] = useState('') const [bootError, setBootError] = useState(null) const [emptyHint, setEmptyHint] = useState(null) const [youtubeText, setYoutubeText] = useState('') const [urlText, setUrlText] = useState('') const [youtubeStatus, setYoutubeStatus] = useState('就绪') const [urlStatus, setUrlStatus] = useState('就绪') const [loadingAction, setLoadingAction] = useState(null) const [saveYoutubeLoading, setSaveYoutubeLoading] = useState(false) const [saveUrlLoading, setSaveUrlLoading] = useState(false) const [multiChannelPro, setMultiChannelProState] = useState(false) const [proChannelId, setProChannelId] = useState('') const [proChannelsList, setProChannelsList] = useState< { id: string; name: string; keyPreview: string; douyinUrl?: string }[] >([]) const [proChannelsError, setProChannelsError] = useState(null) const [proYoutubeKey, setProYoutubeKey] = useState('') const [saveProKeyLoading, setSaveProKeyLoading] = useState(false) const lastLiveStatusRef = useRef<{ process: string; status: string } | null>(null) useEffect(() => { void getMultiChannelPro().then(setMultiChannelProState) }, []) const togglePro = useCallback(async (v: boolean) => { setMultiChannelProState(v) await setMultiChannelPro(v) }, []) const getEntry = useCallback( (name: string) => entries.find((e) => e.pm2 === name) ?? null, [entries], ) const ent = useMemo(() => getEntry(currentProcess), [getEntry, currentProcess]) const script = ent?.script ?? '' const isYoutube = ent ? isYoutubeScript(script) : false const isTiktok = ent ? isTiktokScript(script) : false const isObs = ent ? isObsScript(script) : false const loadProcessMonitor = useCallback(async () => { const res = await apiFetch(bind, '/process_monitor') if (!res.ok) throw new Error(String(res.status)) const data = (await res.json()) as { entries?: { script: string; label?: string; pm2?: string }[] } const raw: Entry[] = (data.entries || []).map((e) => ({ script: e.script, label: e.label || e.script, pm2: e.pm2 || pm2NameFromScript(e.script), })) const filtered = UI_DROPDOWN_YOUTUBE_ONLY ? raw.filter((e) => isYoutubeScript(e.script)) : raw setEntries(filtered) return filtered }, [bind]) const loadConfig = useCallback( async (endpoint: string, setContent: (s: string) => void, setSt: (s: string) => void) => { if (!bind || !currentProcess) return try { const res = await apiFetch( bind, `${endpoint}?process=${encodeURIComponent(currentProcess)}`, ) if (!res.ok) throw new Error(`连接失败 ${res.status}`) const data = (await res.json()) as { content?: string } setContent(data.content || '') setSt('已读到最新内容') } catch (err) { setSt(`读取失败:${err instanceof Error ? err.message : String(err)}`) } }, [bind, currentProcess], ) const saveConfig = useCallback( async ( endpoint: string, content: string, setSt: (s: string) => void, endLoading: () => void, ) => { if (!bind) return try { const res = await apiFetch( bind, `${endpoint}?process=${encodeURIComponent(currentProcess)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }), }, ) if (!res.ok) throw new Error(`保存失败 ${res.status}`) const data = (await res.json()) as { message?: string } setSt(data.message || '已保存') } catch (err) { setSt(`未能保存:${err instanceof Error ? err.message : String(err)}`) } finally { endLoading() } }, [bind, currentProcess], ) const saveProChannelKey = useCallback(async () => { if (!bind || !proChannelId) return setSaveProKeyLoading(true) try { const res = await apiFetch(bind, '/relay_pro/channel_key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ channelId: proChannelId, youtubeKey: proYoutubeKey }), }) const data = (await res.json()) as { error?: string; message?: string } if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`) setYoutubeStatus(data.message || '密钥已保存') } catch (err) { setYoutubeStatus(`保存失败:${err instanceof Error ? err.message : String(err)}`) } finally { setSaveProKeyLoading(false) } }, [bind, proChannelId, proYoutubeKey]) const saveProUrlConfig = useCallback(async () => { if (!bind || !proChannelId) return setSaveUrlLoading(true) try { const res = await apiFetch( bind, `/relay_pro/url_config?channel_id=${encodeURIComponent(proChannelId)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: urlText }), }, ) const data = (await res.json()) as { error?: string; message?: string } if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`) setUrlStatus(data.message || '已保存') } catch (err) { setUrlStatus(`保存失败:${err instanceof Error ? err.message : String(err)}`) } finally { setSaveUrlLoading(false) } }, [bind, proChannelId, urlText]) const runPm2Action = useCallback( async (action: string) => { if (!bind) return const isStatus = action === 'status' if (!isStatus) setLoadingAction(action) try { if (action === 'start' && isYoutube && multiChannelPro) { if (!proChannelId) throw new Error('请先选择一条转播线路') const sync = await apiFetch( bind, `/relay_pro/sync_active?channel_id=${encodeURIComponent(proChannelId)}`, { method: 'POST' }, ) const sj = (await sync.json().catch(() => ({}))) as { error?: string } if (!sync.ok) throw new Error(sj.error || '同步失败') } const res = await apiFetch( bind, `/${action}?process=${encodeURIComponent(currentProcess)}`, ) if (!res.ok) throw new Error('无法连接到本机服务') const data = (await res.json()) as Record if (action === 'status') { const ps = data.process_status as string const prev = lastLiveStatusRef.current const prevPs = prev?.process === currentProcess ? prev.status : undefined if (!prev || prev.process !== currentProcess || prev.status !== ps) { lastLiveStatusRef.current = { process: currentProcess, status: ps } } const { line, variant } = statusPresentation(ps) setStatusMeta({ line, variant }) let textOut = `── 状态 · ${new Date().toLocaleTimeString()} ──\n${data.raw_status}\n\n` const rl = data.recent_log as string const re = data.recent_error as string textOut += `【输出】\n${rl || '(暂无)'}\n\n【异常】\n${re || '(暂无)'}` setLogText(textOut) } else { const e = getEntry(currentProcess) const who = e ? e.label : '当前任务' const out = typeof data.output === 'string' ? data.output : [data.pm2_output, data.message, data.note] .filter((x): x is string => typeof x === 'string') .join('\n') || '已完成' const actionLabel = action === 'start' ? '开始' : action === 'stop' ? '停止' : action === 'restart' ? '重新开始' : action setLogText(`已对「${who}」执行:${actionLabel}\n\n${out}`) setTimeout(() => void runPm2Action('status'), 1500) } } catch (err) { setLogText(`操作失败:${err instanceof Error ? err.message : String(err)}`) setStatusMeta({ line: '状态未知', variant: 'unknown' }) } finally { if (!isStatus) setLoadingAction(null) } }, [bind, currentProcess, getEntry, isYoutube, multiChannelPro, proChannelId], ) useEffect(() => { if (!bind) { setBootError(null) setEmptyHint('请先在「配对」页扫码绑定 PC。') setEntries([]) return } let cancelled = false void (async () => { try { const filtered = await loadProcessMonitor() if (cancelled) return setBootError(null) if (!filtered.length) { setEmptyHint( UI_DROPDOWN_YOUTUBE_ONLY ? '没有发现 YouTube 录制任务。' : '没有发现可用的录制任务。', ) setStatusMeta({ line: '暂无可选任务', variant: 'empty' }) return } setEmptyHint(null) setCurrentProcess(filtered[0].pm2) } catch (e) { setBootError(e instanceof Error ? e.message : String(e)) setStatusMeta({ line: '未连接', variant: 'offline' }) } })() return () => { cancelled = true } }, [bind, loadProcessMonitor]) useEffect(() => { if (!entries.length || !currentProcess || !bind) return void runPm2Action('status') }, [entries.length, currentProcess, bind]) // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!entries.length || !currentProcess || !bind) return const id = setInterval(() => void runPm2Action('status'), 2000) return () => clearInterval(id) }, [entries.length, currentProcess, bind]) // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!ent || !currentProcess || !bind) return if (isYoutube && multiChannelPro) return if (isYoutube) void loadConfig('/get_config', setYoutubeText, setYoutubeStatus) if (isYoutube || isTiktok) void loadConfig('/get_url_config', setUrlText, setUrlStatus) }, [ent, currentProcess, isYoutube, isTiktok, loadConfig, multiChannelPro, bind]) useEffect(() => { if (!bind || !isYoutube || !multiChannelPro) return let cancelled = false void (async () => { try { const d = await apiJson<{ ok?: boolean; error?: string; channels?: typeof proChannelsList }>( bind, '/relay_pro/channels', ) if (cancelled) return if (!d.ok) { setProChannelsError(d.error || '无法加载 Pro 频道') setProChannelsList([]) return } setProChannelsError(null) setProChannelsList(d.channels || []) setProChannelId((prev) => { if (prev && (d.channels || []).some((c) => c.id === prev)) return prev return d.channels?.[0]?.id ?? '' }) } catch (e) { if (!cancelled) { setProChannelsError(e instanceof Error ? e.message : String(e)) setProChannelsList([]) } } })() return () => { cancelled = true } }, [bind, isYoutube, multiChannelPro]) useEffect(() => { if (!bind || !isYoutube || !multiChannelPro || !proChannelId) return let cancelled = false void (async () => { try { const dr = await apiFetch( bind, `/relay_pro/channel_detail?channel_id=${encodeURIComponent(proChannelId)}`, ) if (!dr.ok) { if (!cancelled) setYoutubeStatus('读取线路失败') return } const cd = (await dr.json()) as { youtubeKey?: string } if (cancelled) return setProYoutubeKey(cd.youtubeKey || '') const ur = await apiFetch( bind, `/relay_pro/url_config?channel_id=${encodeURIComponent(proChannelId)}`, ) if (ur.ok) { const u = (await ur.json()) as { content?: string } if (!cancelled) { setUrlText(u.content || '') setUrlStatus('已读取当前线路地址') } } } catch (e) { if (!cancelled) setUrlStatus(e instanceof Error ? e.message : String(e)) } })() return () => { cancelled = true } }, [bind, isYoutube, multiChannelPro, proChannelId]) return { bootError, emptyHint, entries, currentProcess, setCurrentProcess, statusMeta, logText, youtubeText, setYoutubeText, urlText, setUrlText, youtubeStatus, urlStatus, loadingAction, saveYoutubeLoading, saveUrlLoading, multiChannelPro, togglePro, proChannelId, setProChannelId, proChannelsList, proChannelsError, proYoutubeKey, setProYoutubeKey, saveProKeyLoading, saveProUrlConfig, saveProChannelKey, runPm2Action, loadConfig, saveConfig, setSaveYoutubeLoading, setSaveUrlLoading, ent, isYoutube, isTiktok, isObs, UI_SHOW_YOUTUBE_GOOGLE_OAUTH, setYoutubeStatus, setUrlStatus, } }