This commit is contained in:
eric
2026-03-26 01:59:04 -05:00
parent aba40c8cb7
commit 3bc1599763
12 changed files with 629 additions and 72 deletions

View File

@@ -1,7 +1,9 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Alert } from 'react-native'
import { useAuth } from '@/context/AuthContext'
import { useBind } from '@/context/BindContext'
import { apiFetch, apiJson } from '@/lib/api'
import { reportLiveControlEvent } from '@/lib/liveTelemetry'
import type { GpuCaps, YoutubeIniModel } from '@/lib/pcTypes'
import { getMultiChannelPro, setMultiChannelPro } from '@/lib/storage'
import { isObsScript, isTiktokScript, isYoutubeScript, pm2NameFromScript } from '@/lib/scriptUtils'
@@ -72,6 +74,9 @@ function pickProcessForEntries(filtered: Entry[], multi: boolean): string {
export function useRecorder() {
const { bind } = useBind()
const { pbToken, ensureCanControlLive, session } = useAuth()
const pbTokRef = useRef(pbToken)
pbTokRef.current = pbToken
const [entries, setEntries] = useState<Entry[]>([])
const [currentProcess, setCurrentProcess] = useState('')
const [statusMeta, setStatusMeta] = useState<{ line: string; variant: StatusVariant }>({
@@ -135,7 +140,7 @@ export function useRecorder() {
useEffect(() => {
if (!bindKey) return
let cancelled = false
void apiFetch(bindRef.current!, '/host/gpu_status')
void apiFetch(bindRef.current!, '/host/gpu_status', { pbToken: pbTokRef.current })
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status)))))
.then((j: GpuCaps) => {
if (!cancelled)
@@ -166,7 +171,7 @@ export function useRecorder() {
const loadProcessMonitor = useCallback(async () => {
const b = bindRef.current
if (!b) throw new Error('尚未绑定 PC')
const res = await apiFetch(b, '/process_monitor')
const res = await apiFetch(b, '/process_monitor', { pbToken: pbTokRef.current })
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) => ({
@@ -186,6 +191,7 @@ export function useRecorder() {
const d = await apiJson<{ ok?: boolean; error?: string; channels?: typeof proChannelsList }>(
b,
'/relay_pro/channels',
{ pbToken: pbTokRef.current },
)
if (!d.ok) {
setProChannelsError(d.error || '无法加载 Pro 频道')
@@ -220,7 +226,7 @@ export function useRecorder() {
}
setYoutubeStatus('正在将当前配置同步到多频道…')
try {
const res = await apiFetch(b, '/relay_pro/channels')
const res = await apiFetch(b, '/relay_pro/channels', { pbToken: pbTokRef.current })
const d = (await res.json()) as { ok?: boolean; channels?: { id: string }[]; error?: string }
if (!d.ok || !d.channels?.length) throw new Error(d.error || '无法加载频道列表')
const firstId = d.channels[0].id
@@ -231,6 +237,7 @@ export function useRecorder() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channelId: firstId, keys, activeIndex: 0 }),
pbToken: pbTokRef.current,
})
const kj = (await keyRes.json()) as { error?: string }
if (!keyRes.ok) throw new Error(kj.error || '同步密钥失败')
@@ -241,6 +248,7 @@ export function useRecorder() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: urlText }),
pbToken: pbTokRef.current,
},
)
const uj = (await urlRes.json()) as { error?: string }
@@ -258,6 +266,7 @@ export function useRecorder() {
options: ytIniModel.options,
header: ytIniModel.header || '',
}),
pbToken: pbTokRef.current,
},
)
} catch {
@@ -282,6 +291,7 @@ export function useRecorder() {
const res = await apiFetch(
bind,
`${endpoint}?process=${encodeURIComponent(currentProcess)}`,
{ pbToken: pbTokRef.current },
)
if (!res.ok) throw new Error(`连接失败 ${res.status}`)
const data = (await res.json()) as { content?: string }
@@ -310,6 +320,7 @@ export function useRecorder() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content }),
pbToken: pbTokRef.current,
},
)
if (!res.ok) throw new Error(`保存失败 ${res.status}`)
@@ -330,6 +341,7 @@ export function useRecorder() {
const res = await apiFetch(
bind,
`/youtube/ini_model?process=${encodeURIComponent(currentProcess)}`,
{ pbToken: pbTokRef.current },
)
const data = (await res.json()) as Partial<YoutubeIniModel> & { error?: string }
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
@@ -375,6 +387,7 @@ export function useRecorder() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(modelToSave),
pbToken: pbTokRef.current,
},
)
const data = (await res.json()) as { error?: string; message?: string }
@@ -404,6 +417,7 @@ export function useRecorder() {
keys: keysOne,
activeIndex: 0,
}),
pbToken: pbTokRef.current,
})
const data = (await res.json()) as { error?: string; message?: string }
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`)
@@ -450,6 +464,7 @@ export function useRecorder() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: ed.urlText }),
pbToken: pbTokRef.current,
},
)
const data = (await res.json()) as { error?: string; message?: string }
@@ -469,6 +484,7 @@ export function useRecorder() {
const ur = await apiFetch(
b,
`/relay_pro/url_config?channel_id=${encodeURIComponent(proChannelId)}`,
{ pbToken: pbTokRef.current },
)
if (!ur.ok) {
setUrlStatus('读取失败')
@@ -518,9 +534,17 @@ export function useRecorder() {
}
const proc = procForPm2(processOverride)
const isStatus = action === 'status'
if (!isStatus) setLoadingAction(action)
if (!isStatus) {
if (action === 'start' || action === 'restart') {
const allowed = await ensureCanControlLive()
if (!allowed) return
}
setLoadingAction(action)
}
try {
const res = await apiFetch(bindRef.current, `/${action}?process=${encodeURIComponent(proc)}`)
const res = await apiFetch(bindRef.current, `/${action}?process=${encodeURIComponent(proc)}`, {
pbToken: pbTokRef.current,
})
if (!res.ok) throw new Error('无法连接到本机服务')
const data = (await res.json()) as Record<string, unknown>
@@ -551,6 +575,15 @@ export function useRecorder() {
const actionLabel =
action === 'start' ? '开始' : action === 'stop' ? '停止' : action === 'restart' ? '重新开始' : action
setLogText(`已对「${who}」执行:${actionLabel}\n\n${out}`)
if (action === 'start' || action === 'restart' || action === 'stop') {
void reportLiveControlEvent(
bindRef.current,
pbTokRef.current,
session?.record?.id,
proc,
action,
)
}
const statusProc = processOverride ?? proc
setTimeout(() => void runPm2Action('status', statusProc), 1500)
}
@@ -561,18 +594,22 @@ export function useRecorder() {
if (!isStatus) setLoadingAction(null)
}
},
[bindRef, procForPm2, getEntry],
[bindRef, procForPm2, getEntry, ensureCanControlLive, session?.record?.id],
)
const runProChannelStart = useCallback(
async (channelId: string) => {
if (!channelId) return
const allowed = await ensureCanControlLive()
if (!allowed) return
const pm = proChannelPm2Process(channelId)
setLoadingAction('start')
try {
const saved = await saveProChannelKeysFor(channelId)
if (!saved) return
const res = await apiFetch(bindRef.current!, `/start?process=${encodeURIComponent(pm)}`)
const res = await apiFetch(bindRef.current!, `/start?process=${encodeURIComponent(pm)}`, {
pbToken: pbTokRef.current,
})
if (!res.ok) throw new Error('无法连接到本机服务')
const data = (await res.json()) as Record<string, unknown>
const e = getEntry(pm)
@@ -584,6 +621,7 @@ export function useRecorder() {
.filter((x): x is string => typeof x === 'string')
.join('\n') || '已完成'
setLogText(`已对「${who}」执行:开始直播\n\n${out}`)
void reportLiveControlEvent(bindRef.current, pbTokRef.current, session?.record?.id, pm, 'start')
setTimeout(() => void runPm2Action('status', pm), 1500)
} catch (err) {
setLogText(`操作失败:${err instanceof Error ? err.message : String(err)}`)
@@ -591,18 +629,22 @@ export function useRecorder() {
setLoadingAction(null)
}
},
[getEntry, runPm2Action, saveProChannelKeysFor],
[getEntry, runPm2Action, saveProChannelKeysFor, ensureCanControlLive, session?.record?.id],
)
const runProChannelRestart = useCallback(
async (channelId: string) => {
if (!channelId) return
const allowed = await ensureCanControlLive()
if (!allowed) return
const pm = proChannelPm2Process(channelId)
setLoadingAction('restart')
try {
const saved = await saveProChannelKeysFor(channelId)
if (!saved) return
const res = await apiFetch(bindRef.current!, `/restart?process=${encodeURIComponent(pm)}`)
const res = await apiFetch(bindRef.current!, `/restart?process=${encodeURIComponent(pm)}`, {
pbToken: pbTokRef.current,
})
if (!res.ok) throw new Error('无法连接到本机服务')
const data = (await res.json()) as Record<string, unknown>
const e = getEntry(pm)
@@ -614,6 +656,7 @@ export function useRecorder() {
.filter((x): x is string => typeof x === 'string')
.join('\n') || '已完成'
setLogText(`已对「${who}」执行:重新开始\n\n${out}`)
void reportLiveControlEvent(bindRef.current, pbTokRef.current, session?.record?.id, pm, 'restart')
setTimeout(() => void runPm2Action('status', pm), 1500)
} catch (err) {
setLogText(`操作失败:${err instanceof Error ? err.message : String(err)}`)
@@ -621,7 +664,7 @@ export function useRecorder() {
setLoadingAction(null)
}
},
[getEntry, runPm2Action, saveProChannelKeysFor],
[getEntry, runPm2Action, saveProChannelKeysFor, ensureCanControlLive, session?.record?.id],
)
const submitNewChannelDraft = useCallback(async () => {
@@ -637,6 +680,7 @@ export function useRecorder() {
name: `频道 ${proChannelsList.length + 1}`,
youtubeKey: key,
}),
pbToken: pbTokRef.current,
})
const d = (await res.json()) as { error?: string; message?: string; channelId?: string }
if (!res.ok) throw new Error(d.error || `HTTP ${res.status}`)
@@ -675,6 +719,7 @@ export function useRecorder() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ channelId }),
pbToken: pbTokRef.current,
})
const d = (await res.json()) as { error?: string; message?: string }
if (!res.ok) throw new Error(d.error || `HTTP ${res.status}`)
@@ -848,6 +893,7 @@ export function useRecorder() {
const d = await apiJson<{ ok?: boolean; error?: string; channels?: typeof proChannelsList }>(
bind,
'/relay_pro/channels',
{ pbToken: pbTokRef.current },
)
if (cancelled) return
if (!d.ok) {
@@ -878,7 +924,7 @@ export function useRecorder() {
let cancelled = false
const poll = async () => {
try {
const r = await apiFetch(bind, '/relay_pro/live_status')
const r = await apiFetch(bind, '/relay_pro/live_status', { pbToken: pbTokRef.current })
if (!r.ok || cancelled) return
const j = (await r.json()) as ProRelayStatusPayload
if (!cancelled) setProRelayStatus(j)
@@ -910,6 +956,7 @@ export function useRecorder() {
const dr = await apiFetch(
b,
`/relay_pro/channel_detail?channel_id=${encodeURIComponent(c.id)}`,
{ pbToken: pbTokRef.current },
)
if (!dr.ok) continue
const cd = (await dr.json()) as {
@@ -951,6 +998,7 @@ export function useRecorder() {
const ur = await apiFetch(
bind,
`/relay_pro/url_config?channel_id=${encodeURIComponent(cid)}`,
{ pbToken: pbTokRef.current },
)
if (!ur.ok || cancelled) return
const j = (await ur.json()) as { content?: string }