脚本缓存,移除版本控制

This commit is contained in:
mtvpls
2026-03-22 00:29:02 +08:00
parent 5b6649ce2f
commit 691247cb8e
4 changed files with 67 additions and 148 deletions
-65
View File
@@ -327,11 +327,6 @@ interface StandaloneSourceScript {
code: string; code: string;
createdAt: number; createdAt: number;
updatedAt: number; updatedAt: number;
history: Array<{
version: string;
code: string;
updatedAt: number;
}>;
} }
// 新增站点配置类型 // 新增站点配置类型
@@ -5991,7 +5986,6 @@ const VideoSourceScriptLab = () => {
description: string; description: string;
code: string; code: string;
enabled: boolean; enabled: boolean;
history: StandaloneSourceScript['history'];
version?: string; version?: string;
updatedAt?: number; updatedAt?: number;
}>({ }>({
@@ -6000,7 +5994,6 @@ const VideoSourceScriptLab = () => {
description: '', description: '',
code: '', code: '',
enabled: true, enabled: true,
history: [],
}); });
const [testHook, setTestHook] = useState<'getSources' | 'search' | 'recommend' | 'detail' | 'resolvePlayUrl'>('getSources'); const [testHook, setTestHook] = useState<'getSources' | 'search' | 'recommend' | 'detail' | 'resolvePlayUrl'>('getSources');
const [testPayload, setTestPayload] = useState( const [testPayload, setTestPayload] = useState(
@@ -6017,7 +6010,6 @@ const VideoSourceScriptLab = () => {
description: '', description: '',
code: template, code: template,
enabled: true, enabled: true,
history: [],
}); });
setSelectedScriptId(null); setSelectedScriptId(null);
return; return;
@@ -6030,7 +6022,6 @@ const VideoSourceScriptLab = () => {
description: script.description || '', description: script.description || '',
code: script.code, code: script.code,
enabled: script.enabled, enabled: script.enabled,
history: script.history || [],
version: script.version, version: script.version,
updatedAt: script.updatedAt, updatedAt: script.updatedAt,
}); });
@@ -6067,7 +6058,6 @@ const VideoSourceScriptLab = () => {
description: '', description: '',
code: data.template || '', code: data.template || '',
enabled: true, enabled: true,
history: [],
}); });
setSelectedScriptId(null); setSelectedScriptId(null);
} }
@@ -6090,7 +6080,6 @@ const VideoSourceScriptLab = () => {
description: '', description: '',
code: template, code: template,
enabled: true, enabled: true,
history: [],
}); });
setTestOutput(''); 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 () => { const handleTest = async () => {
let payload = {}; let payload = {};
try { try {
@@ -6524,36 +6489,6 @@ const VideoSourceScriptLab = () => {
</pre> </pre>
</div> </div>
</div> </div>
{editor.history.length > 0 && (
<div className='space-y-3'>
<div className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</div>
<div className='space-y-2'>
{editor.history.map((history) => (
<div
key={history.version}
className='flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900'
>
<div className='text-sm text-gray-700 dark:text-gray-300'>
<div className='font-medium'>{history.version}</div>
<div className='text-xs text-gray-500 dark:text-gray-400'>
{new Date(history.updatedAt).toLocaleString('zh-CN')}
</div>
</div>
<button
onClick={() => handleRestore(history.version)}
disabled={isLoading(`restoreSourceScript_${editor.id}`)}
className={buttonStyles.secondarySmall}
>
</button>
</div>
))}
</div>
</div>
)}
</div> </div>
</div> </div>
-12
View File
@@ -9,7 +9,6 @@ import {
getDefaultSourceScriptTemplate, getDefaultSourceScriptTemplate,
importSourceScripts, importSourceScripts,
listSourceScripts, listSourceScripts,
restoreSourceScriptHistory,
saveSourceScript, saveSourceScript,
testSourceScript, testSourceScript,
toggleSourceScriptEnabled, 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': { case 'test': {
const result = await testSourceScript({ const result = await testSourceScript({
code: body.code, code: body.code,
+1 -1
View File
@@ -2117,7 +2117,7 @@ function PlayPageClient() {
episodeIndex >= detailData.episodes.length episodeIndex >= detailData.episodes.length
) { ) {
// 这类源统一先走详情懒加载,如果 episodes 为空则跳过 // 这类源统一先走详情懒加载,如果 episodes 为空则跳过
if (isLazyDetailSource(detailData?.source) && (!detailData.episodes || detailData.episodes.length === 0)) { if (isLazyDetailSource(detailData?.source) && (!detailData?.episodes || detailData.episodes.length === 0)) {
return; return;
} }
setVideoUrl(''); setVideoUrl('');
+66 -70
View File
@@ -6,14 +6,14 @@ import { nanoid } from 'nanoid';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
const SOURCE_SCRIPT_REGISTRY_KEY = 'source-script:registry'; const SOURCE_SCRIPT_REGISTRY_KEY = 'source-script:registry';
const MAX_HISTORY_ITEMS = 10;
const DEFAULT_TIMEOUT_MS = 20000; const DEFAULT_TIMEOUT_MS = 20000;
export interface SourceScriptVersion { // ---- 内存缓存 ----
version: string; let _registryCache: { data: SourceScriptRegistry; ts: number } | null = null;
code: string; const REGISTRY_CACHE_TTL_MS = 5_000;
updatedAt: number;
} const _compiledCache = new Map<string, any>();
const MAX_COMPILED_CACHE_SIZE = 50;
export interface SourceScriptRecord { export interface SourceScriptRecord {
id: string; id: string;
@@ -25,7 +25,6 @@ export interface SourceScriptRecord {
code: string; code: string;
createdAt: number; createdAt: number;
updatedAt: number; updatedAt: number;
history: SourceScriptVersion[];
} }
export interface SourceScriptImportItem { export interface SourceScriptImportItem {
@@ -147,23 +146,36 @@ function buildEmptyRegistry(): SourceScriptRegistry {
} }
async function loadRegistry(): Promise<SourceScriptRegistry> { async function loadRegistry(): Promise<SourceScriptRegistry> {
if (_registryCache && Date.now() - _registryCache.ts < REGISTRY_CACHE_TTL_MS) {
return _registryCache.data;
}
const raw = await db.getGlobalValue(SOURCE_SCRIPT_REGISTRY_KEY); const raw = await db.getGlobalValue(SOURCE_SCRIPT_REGISTRY_KEY);
if (!raw) { if (!raw) {
return buildEmptyRegistry(); const empty = buildEmptyRegistry();
_registryCache = { data: empty, ts: Date.now() };
return empty;
} }
try { try {
const parsed = JSON.parse(raw) as SourceScriptRegistry; const parsed = JSON.parse(raw) as SourceScriptRegistry;
if (!parsed || !Array.isArray(parsed.items)) { 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; return parsed;
} catch { } catch {
return buildEmptyRegistry(); const empty = buildEmptyRegistry();
_registryCache = { data: empty, ts: Date.now() };
return empty;
} }
} }
async function saveRegistry(registry: SourceScriptRegistry) { async function saveRegistry(registry: SourceScriptRegistry) {
_registryCache = null;
_compiledCache.clear();
await db.setGlobalValue( await db.setGlobalValue(
SOURCE_SCRIPT_REGISTRY_KEY, SOURCE_SCRIPT_REGISTRY_KEY,
JSON.stringify(registry) JSON.stringify(registry)
@@ -444,12 +456,27 @@ async function getEnabledSourceScriptByKey(key: string) {
return item; 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( async function compileSourceScript(
script: SourceScriptRecord, script: SourceScriptRecord,
configValues?: Record<string, string> configValues?: Record<string, string>
) { ) {
const factory = createScriptFactory(script.code); const compiled = getOrCompileScript(script);
const compiled = normalizeScript(factory());
const context = await createScriptContext(script, configValues); const context = await createScriptContext(script, configValues);
return { return {
compiled, compiled,
@@ -536,12 +563,6 @@ export async function saveSourceScript(input: {
} }
if (existing) { 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.key = input.key;
existing.name = input.name; existing.name = input.name;
existing.description = input.description || ''; existing.description = input.description || '';
@@ -563,7 +584,6 @@ export async function saveSourceScript(input: {
version: getNowVersion(), version: getNowVersion(),
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
history: [],
}; };
registry.items.unshift(created); registry.items.unshift(created);
@@ -587,12 +607,6 @@ export async function importSourceScripts(items: SourceScriptImportItem[]) {
const existing = registry.items.find((record) => record.key === item.key); const existing = registry.items.find((record) => record.key === item.key);
if (existing) { 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.name = item.name;
existing.description = item.description || ''; existing.description = item.description || '';
existing.code = item.code; existing.code = item.code;
@@ -613,7 +627,6 @@ export async function importSourceScripts(items: SourceScriptImportItem[]) {
version: getNowVersion(), version: getNowVersion(),
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
history: [],
}; };
registry.items.unshift(created); registry.items.unshift(created);
imported.push(created); imported.push(created);
@@ -645,31 +658,6 @@ export async function toggleSourceScriptEnabled(id: string) {
return target; 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: { export async function testSourceScript(input: {
code: string; code: string;
hook: SourceScriptHook; hook: SourceScriptHook;
@@ -679,6 +667,7 @@ export async function testSourceScript(input: {
configValues?: Record<string, string>; configValues?: Record<string, string>;
}): Promise<SourceScriptTestResult> { }): Promise<SourceScriptTestResult> {
const startedAt = Date.now(); const startedAt = Date.now();
let collectedLogs: string[] = [];
try { try {
const tempScript: SourceScriptRecord = { const tempScript: SourceScriptRecord = {
id: 'test-script', id: 'test-script',
@@ -690,7 +679,6 @@ export async function testSourceScript(input: {
code: input.code, code: input.code,
createdAt: startedAt, createdAt: startedAt,
updatedAt: startedAt, updatedAt: startedAt,
history: [],
}; };
const factory = createScriptFactory(input.code); const factory = createScriptFactory(input.code);
@@ -701,6 +689,7 @@ export async function testSourceScript(input: {
} }
const { ctx, logs } = await createScriptContext(tempScript, input.configValues); const { ctx, logs } = await createScriptContext(tempScript, input.configValues);
collectedLogs = logs;
const result = await withTimeout( const result = await withTimeout(
Promise.resolve(hook(ctx, input.payload)), Promise.resolve(hook(ctx, input.payload)),
DEFAULT_TIMEOUT_MS DEFAULT_TIMEOUT_MS
@@ -717,7 +706,7 @@ export async function testSourceScript(input: {
return { return {
ok: false, ok: false,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
logs: [], logs: collectedLogs,
error: (error as Error).message, 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( const resolvedPlaybacks = await Promise.all(
playbacks.map(async (playback: any) => { playbacks.map(async (playback: any) => {
const playbackSourceId = String(playback.sourceId || input.sourceId); const playbackSourceId = String(playback.sourceId || input.sourceId);
@@ -889,23 +889,19 @@ export async function resolveScriptDetailPlaybacks(input: {
: String(episode?.playUrl || episode?.url || ''); : String(episode?.playUrl || episode?.url || '');
try { try {
const execution = await executeSavedSourceScript({ const result = await withTimeout(
key: input.scriptKey, Promise.resolve(
hook: 'resolvePlayUrl', compiled.resolvePlayUrl(ctx, {
payload: { playUrl,
playUrl, sourceId: playbackSourceId,
sourceId: playbackSourceId, lineId,
lineId, episodeIndex: index,
episodeIndex: index, })
}, ),
}); DEFAULT_TIMEOUT_MS
);
return execution.result?.url || playUrl; return result?.url || playUrl;
} catch (error) { } catch {
const message = error instanceof Error ? error.message : '';
if (message.includes('未实现 resolvePlayUrl hook')) {
return playUrl;
}
return playUrl; return playUrl;
} }
}) })