脚本缓存,移除版本控制
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user