脚本缓存,移除版本控制

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

View File

@@ -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 = () => {
</pre>
</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>

View File

@@ -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,

View File

@@ -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('');

View File

@@ -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<string, any>();
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<SourceScriptRegistry> {
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<string, string>
) {
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<string, string>;
}): Promise<SourceScriptTestResult> {
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;
}
})