diff --git a/domain/plugins/api.py b/domain/plugins/api.py index b3d6e7d..c3b5434 100644 --- a/domain/plugins/api.py +++ b/domain/plugins/api.py @@ -67,10 +67,12 @@ async def delete_plugin(request: Request, key_or_id: str): async def get_bundle(request: Request, key_or_id: str): """获取插件前端 bundle""" path = await PluginService.get_bundle_path(key_or_id) + v = (request.query_params.get("v") or "").strip() + cache_control = "public, max-age=31536000, immutable" if v else "no-cache" return FileResponse( path, media_type="application/javascript", - headers={"Cache-Control": "no-store"}, + headers={"Cache-Control": cache_control}, ) diff --git a/web/src/plugin-frame.ts b/web/src/plugin-frame.ts index d47d842..7638b33 100644 --- a/web/src/plugin-frame.ts +++ b/web/src/plugin-frame.ts @@ -31,6 +31,19 @@ function renderStatus(text: string, isError: boolean = false) { root.appendChild(el); } +function scheduleStatus(text: string, delayMs: number) { + let canceled = false; + const t = window.setTimeout(() => { + if (canceled) return; + renderStatus(text); + }, delayMs); + + return () => { + canceled = true; + window.clearTimeout(t); + }; +} + function getQuery() { const params = new URLSearchParams(window.location.search); const pluginKey = (params.get('pluginKey') || '').trim(); @@ -89,14 +102,25 @@ function getPluginStylePaths(plugin: PluginItem): string[] { return styles.filter((s) => typeof s === 'string' && s.trim().length > 0); } -async function loadPluginStyles(pluginKey: string, plugin: PluginItem) { +function withVersion(url: string, version?: string | null): string { + const v = typeof version === 'string' ? version.trim() : ''; + if (!v) return url; + const u = new URL(url, window.location.origin); + u.searchParams.set('v', v); + return u.pathname + u.search; +} + +async function loadPluginStyles(pluginKey: string, plugin: PluginItem, version?: string | null) { const stylePaths = getPluginStylePaths(plugin); if (stylePaths.length === 0) return; const tasks = stylePaths.map( (p) => new Promise((resolve) => { - const href = `/api/plugins/${pluginKey}/assets/${p.replace(/^\/+/, '')}`; + const href = withVersion( + `/api/plugins/${pluginKey}/assets/${p.replace(/^\/+/, '')}`, + version + ); const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = href; @@ -109,8 +133,8 @@ async function loadPluginStyles(pluginKey: string, plugin: PluginItem) { await Promise.all(tasks); } -async function loadPluginBundle(pluginKey: string): Promise { - const url = `/api/plugins/${pluginKey}/bundle.js`; +async function loadPluginBundle(pluginKey: string, version?: string | null): Promise { + const url = withVersion(`/api/plugins/${pluginKey}/bundle.js`, version); return new Promise((resolve, reject) => { let done = false; @@ -178,32 +202,35 @@ async function main() { return; } - renderStatus('Loading plugin...'); + const cancelLoading = scheduleStatus('Loading plugin...', 200); let plugin: PluginItem; try { plugin = await pluginsApi.get(pluginKey); } catch (e) { const msg = e instanceof Error ? e.message : String(e); + cancelLoading(); renderStatus(`Failed to load plugin info: ${msg}`, true); return; } try { - await loadPluginStyles(pluginKey, plugin); + await loadPluginStyles(pluginKey, plugin, plugin.version); } catch { // ignore } let registered: RegisteredPlugin; try { - registered = await loadPluginBundle(pluginKey); + registered = await loadPluginBundle(pluginKey, plugin.version); } catch (e) { const msg = e instanceof Error ? e.message : String(e); + cancelLoading(); renderStatus(`Failed to load plugin bundle: ${msg}`, true); return; } + cancelLoading(); const host = createHostApi(pluginKey); let cleanup: (() => void) | null = null;