diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 2069049..b37889c 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -327,11 +327,6 @@ interface StandaloneSourceScript { code: string; createdAt: number; updatedAt: number; - history: Array<{ - version: string; - code: string; - updatedAt: number; - }>; } // 新增站点配置类型 @@ -5991,7 +5986,6 @@ const VideoSourceScriptLab = () => { description: string; code: string; enabled: boolean; - history: StandaloneSourceScript['history']; version?: string; updatedAt?: number; }>({ @@ -6000,7 +5994,6 @@ const VideoSourceScriptLab = () => { description: '', code: '', enabled: true, - history: [], }); const [testHook, setTestHook] = useState<'getSources' | 'search' | 'recommend' | 'detail' | 'resolvePlayUrl'>('getSources'); const [testPayload, setTestPayload] = useState( @@ -6017,7 +6010,6 @@ const VideoSourceScriptLab = () => { description: '', code: template, enabled: true, - history: [], }); setSelectedScriptId(null); return; @@ -6030,7 +6022,6 @@ const VideoSourceScriptLab = () => { description: script.description || '', code: script.code, enabled: script.enabled, - history: script.history || [], version: script.version, updatedAt: script.updatedAt, }); @@ -6067,7 +6058,6 @@ const VideoSourceScriptLab = () => { description: '', code: data.template || '', enabled: true, - history: [], }); setSelectedScriptId(null); } @@ -6090,7 +6080,6 @@ const VideoSourceScriptLab = () => { description: '', code: template, enabled: true, - history: [], }); setTestOutput(''); }; @@ -6243,30 +6232,6 @@ const VideoSourceScriptLab = () => { }); }; - const handleRestore = async (version: string) => { - if (!editor.id) return; - - await withLoading(`restoreSourceScript_${editor.id}`, async () => { - const response = await fetch('/api/admin/source-script', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - action: 'restore', - id: editor.id, - version, - }), - }); - const data = await response.json().catch(() => ({})); - if (!response.ok) { - throw new Error(data.error || '回滚失败'); - } - showSuccess('已回滚到历史版本', showAlert); - await loadScripts(editor.id); - }).catch((error) => { - showError(error instanceof Error ? error.message : '回滚失败', showAlert); - }); - }; - const handleTest = async () => { let payload = {}; try { @@ -6524,36 +6489,6 @@ const VideoSourceScriptLab = () => { - - {editor.history.length > 0 && ( -
-
- 历史版本 -
-
- {editor.history.map((history) => ( -
-
-
{history.version}
-
- {new Date(history.updatedAt).toLocaleString('zh-CN')} -
-
- -
- ))} -
-
- )} diff --git a/src/app/api/admin/source-script/route.ts b/src/app/api/admin/source-script/route.ts index d1ccc2e..f5c3fc1 100644 --- a/src/app/api/admin/source-script/route.ts +++ b/src/app/api/admin/source-script/route.ts @@ -9,7 +9,6 @@ import { getDefaultSourceScriptTemplate, importSourceScripts, listSourceScripts, - restoreSourceScriptHistory, saveSourceScript, testSourceScript, toggleSourceScriptEnabled, @@ -118,17 +117,6 @@ export async function POST(request: NextRequest) { } ); } - case 'restore': { - const item = await restoreSourceScriptHistory(body.id, body.version); - return NextResponse.json( - { ok: true, item }, - { - headers: { - 'Cache-Control': 'no-store', - }, - } - ); - } case 'test': { const result = await testSourceScript({ code: body.code, diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index 0a85dc9..17f2644 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -2117,7 +2117,7 @@ function PlayPageClient() { episodeIndex >= detailData.episodes.length ) { // 这类源统一先走详情懒加载,如果 episodes 为空则跳过 - if (isLazyDetailSource(detailData?.source) && (!detailData.episodes || detailData.episodes.length === 0)) { + if (isLazyDetailSource(detailData?.source) && (!detailData?.episodes || detailData.episodes.length === 0)) { return; } setVideoUrl(''); diff --git a/src/lib/source-script.ts b/src/lib/source-script.ts index 2aaea69..3fe944d 100644 --- a/src/lib/source-script.ts +++ b/src/lib/source-script.ts @@ -6,14 +6,14 @@ import { nanoid } from 'nanoid'; import { db } from '@/lib/db'; const SOURCE_SCRIPT_REGISTRY_KEY = 'source-script:registry'; -const MAX_HISTORY_ITEMS = 10; const DEFAULT_TIMEOUT_MS = 20000; -export interface SourceScriptVersion { - version: string; - code: string; - updatedAt: number; -} +// ---- 内存缓存 ---- +let _registryCache: { data: SourceScriptRegistry; ts: number } | null = null; +const REGISTRY_CACHE_TTL_MS = 5_000; + +const _compiledCache = new Map(); +const MAX_COMPILED_CACHE_SIZE = 50; export interface SourceScriptRecord { id: string; @@ -25,7 +25,6 @@ export interface SourceScriptRecord { code: string; createdAt: number; updatedAt: number; - history: SourceScriptVersion[]; } export interface SourceScriptImportItem { @@ -147,23 +146,36 @@ function buildEmptyRegistry(): SourceScriptRegistry { } async function loadRegistry(): Promise { + if (_registryCache && Date.now() - _registryCache.ts < REGISTRY_CACHE_TTL_MS) { + return _registryCache.data; + } + const raw = await db.getGlobalValue(SOURCE_SCRIPT_REGISTRY_KEY); if (!raw) { - return buildEmptyRegistry(); + const empty = buildEmptyRegistry(); + _registryCache = { data: empty, ts: Date.now() }; + return empty; } try { const parsed = JSON.parse(raw) as SourceScriptRegistry; if (!parsed || !Array.isArray(parsed.items)) { - return buildEmptyRegistry(); + const empty = buildEmptyRegistry(); + _registryCache = { data: empty, ts: Date.now() }; + return empty; } + _registryCache = { data: parsed, ts: Date.now() }; return parsed; } catch { - return buildEmptyRegistry(); + const empty = buildEmptyRegistry(); + _registryCache = { data: empty, ts: Date.now() }; + return empty; } } async function saveRegistry(registry: SourceScriptRegistry) { + _registryCache = null; + _compiledCache.clear(); await db.setGlobalValue( SOURCE_SCRIPT_REGISTRY_KEY, JSON.stringify(registry) @@ -444,12 +456,27 @@ async function getEnabledSourceScriptByKey(key: string) { return item; } +function getOrCompileScript(script: SourceScriptRecord) { + const cacheKey = `${script.id}:${script.version}`; + const cached = _compiledCache.get(cacheKey); + if (cached) return cached; + + const factory = createScriptFactory(script.code); + const compiled = normalizeScript(factory()); + + if (_compiledCache.size >= MAX_COMPILED_CACHE_SIZE) { + const firstKey = _compiledCache.keys().next().value; + if (firstKey) _compiledCache.delete(firstKey); + } + _compiledCache.set(cacheKey, compiled); + return compiled; +} + async function compileSourceScript( script: SourceScriptRecord, configValues?: Record ) { - const factory = createScriptFactory(script.code); - const compiled = normalizeScript(factory()); + const compiled = getOrCompileScript(script); const context = await createScriptContext(script, configValues); return { compiled, @@ -536,12 +563,6 @@ export async function saveSourceScript(input: { } if (existing) { - existing.history.unshift({ - version: existing.version, - code: existing.code, - updatedAt: existing.updatedAt, - }); - existing.history = existing.history.slice(0, MAX_HISTORY_ITEMS); existing.key = input.key; existing.name = input.name; existing.description = input.description || ''; @@ -563,7 +584,6 @@ export async function saveSourceScript(input: { version: getNowVersion(), createdAt: now, updatedAt: now, - history: [], }; registry.items.unshift(created); @@ -587,12 +607,6 @@ export async function importSourceScripts(items: SourceScriptImportItem[]) { const existing = registry.items.find((record) => record.key === item.key); if (existing) { - existing.history.unshift({ - version: existing.version, - code: existing.code, - updatedAt: existing.updatedAt, - }); - existing.history = existing.history.slice(0, MAX_HISTORY_ITEMS); existing.name = item.name; existing.description = item.description || ''; existing.code = item.code; @@ -613,7 +627,6 @@ export async function importSourceScripts(items: SourceScriptImportItem[]) { version: getNowVersion(), createdAt: now, updatedAt: now, - history: [], }; registry.items.unshift(created); imported.push(created); @@ -645,31 +658,6 @@ export async function toggleSourceScriptEnabled(id: string) { return target; } -export async function restoreSourceScriptHistory(id: string, version: string) { - const registry = await loadRegistry(); - const target = registry.items.find((item) => item.id === id); - if (!target) { - throw new Error('脚本不存在'); - } - - const history = target.history.find((item) => item.version === version); - if (!history) { - throw new Error('历史版本不存在'); - } - - target.history.unshift({ - version: target.version, - code: target.code, - updatedAt: target.updatedAt, - }); - target.history = target.history.slice(0, MAX_HISTORY_ITEMS); - target.code = history.code; - target.version = getNowVersion(); - target.updatedAt = Date.now(); - await saveRegistry(registry); - return target; -} - export async function testSourceScript(input: { code: string; hook: SourceScriptHook; @@ -679,6 +667,7 @@ export async function testSourceScript(input: { configValues?: Record; }): Promise { const startedAt = Date.now(); + let collectedLogs: string[] = []; try { const tempScript: SourceScriptRecord = { id: 'test-script', @@ -690,7 +679,6 @@ export async function testSourceScript(input: { code: input.code, createdAt: startedAt, updatedAt: startedAt, - history: [], }; const factory = createScriptFactory(input.code); @@ -701,6 +689,7 @@ export async function testSourceScript(input: { } const { ctx, logs } = await createScriptContext(tempScript, input.configValues); + collectedLogs = logs; const result = await withTimeout( Promise.resolve(hook(ctx, input.payload)), DEFAULT_TIMEOUT_MS @@ -717,7 +706,7 @@ export async function testSourceScript(input: { return { ok: false, durationMs: Date.now() - startedAt, - logs: [], + logs: collectedLogs, error: (error as Error).message, }; } @@ -875,6 +864,17 @@ export async function resolveScriptDetailPlaybacks(input: { }, ]; + // 预检查:编译一次脚本,判断是否实现了 resolvePlayUrl + const script = await getEnabledSourceScriptByKey(input.scriptKey); + const compiled = getOrCompileScript(script); + + if (typeof compiled.resolvePlayUrl !== 'function') { + return input.result; + } + + // 已实现 resolvePlayUrl,创建一个 context 复用 + const { ctx } = await createScriptContext(script); + const resolvedPlaybacks = await Promise.all( playbacks.map(async (playback: any) => { const playbackSourceId = String(playback.sourceId || input.sourceId); @@ -889,23 +889,19 @@ export async function resolveScriptDetailPlaybacks(input: { : String(episode?.playUrl || episode?.url || ''); try { - const execution = await executeSavedSourceScript({ - key: input.scriptKey, - hook: 'resolvePlayUrl', - payload: { - playUrl, - sourceId: playbackSourceId, - lineId, - episodeIndex: index, - }, - }); - - return execution.result?.url || playUrl; - } catch (error) { - const message = error instanceof Error ? error.message : ''; - if (message.includes('未实现 resolvePlayUrl hook')) { - return playUrl; - } + const result = await withTimeout( + Promise.resolve( + compiled.resolvePlayUrl(ctx, { + playUrl, + sourceId: playbackSourceId, + lineId, + episodeIndex: index, + }) + ), + DEFAULT_TIMEOUT_MS + ); + return result?.url || playUrl; + } catch { return playUrl; } })