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;
}
})