L4 远程 WebView
L4 是插件渲染的最高隔离层级:插件页面运行在独立的 Tauri WebviewWindow(独立 renderer 进程)中,与主界面进程级隔离。即使插件陷入 busy-loop 也只冻结自己的窗口,不影响主 UI(主界面仍可响应、渲染、关闭)。 适合重 UI、不受控的插件(如复杂编辑器、游戏 UI 增强、长驻后台页面)——这类插件在 iframe(L2)里仍与主界面共享同一 WebView,一个死循环就能卡死整个启动器。manifest 声明
满足以下任一条件即启用 L4 渲染:"render": "webview",或layers数组包含"l4"
plugin-loader.tsx useWebview):render === 'webview' || layers.includes('l4')。声明 l4 的插件仍须提供 entry.frontend(否则不会被激活渲染)。
跨窗口桥原理
l4 窗口不是window.open 出来的普通子窗口,而是经 Tauri IPC WebviewWindow 创建的独立 WebView 窗口(opener 为 null),因此不能依赖 window.opener.postMessage。跨窗口通信走 Tauri 事件系统(emitTo / listen,core:event 默认允许):
- 激活:插件启用时
createRemoteWebview打开窗口plugin-webview-{id}(默认 1200×800),地址为插件页 URL 并带pluginWebview=1标记。 - 轻量启动:
App.tsx检测到该标记后不加载主 Layout / 路由 / 插件自动激活,只渲染PluginWebviewPage。 - 渲染:
PluginWebviewPage拉取插件前端资源,经buildPluginDoc注入主题变量与桥脚本,渲染进sandbox="allow-scripts"的 iframe。 - API 转发:iframe 内
__PLUGIN_API__调用发出__plugin_api_call消息 → 页面转发为 Tauri 事件emitTo('main', 'plugin-l4-api-call', {pluginId, id, method, args})。 - 主窗口执行:主窗口
initL4Bridge收到事件 → 调用与 iframe 相同的handleApiCall(METHOD_PERMISSIONS权限校验 +executePluginMethod)→ 响应经emitTo(l4label, 'plugin-l4-api-response')回发 →PluginWebviewPagelisten后postMessage原样回传 iframe。 - 流式中止:
__plugin_api_abort→plugin-l4-api-abort事件 → 主窗口abortStream。
{type,id,method,args} / {type,id,result,error}),仅传输层换成跨窗口事件,插件侧无需感知差异。
非 Tauri 降级
l4 依赖 Tauri WebViewWindow,在非 Tauri 环境自动降级为 iframe(l2):- 纯浏览器 / Vite dev(无
window.__TAURI_INTERNALS__):createRemoteWebview直接走createSandbox(iframe 沙箱)。 - 窗口创建失败(
tauri://error):日志提示「降级为 iframe」,并回退到createSandbox。
plugin-webview-{id} label 去重(已存在则聚焦不重建);用户手动关闭窗口时清理实例表,避免去重误判。
与 iframe(l2) / WASM(l3) 的取舍
- 常规 UI 插件 → 默认 iframe(l2),不要随意声明 l4。
- 插件有重计算或可能卡 UI 的渲染 → 优先把重计算挪到 WASM(l3);只有”整个页面都必须隔离在独立进程”时才用 l4。
- 需要访问主界面 DOM → iframe 无法满足,用
"render": "inline"(与主界面同 window,隔离最弱,慎用)。
已知边界(以代码为准)
registerMethod/callPlugin的插件间调用:l4 窗口经主窗口plugin-registry.ts中转;l4 侧直接使用 registry 的路径标注为 TODO(见 ADR-054),插件间互调建议以主窗口 registry 为准。- 主题推送:l4 窗口无完整主题管线,iframe
onload时一次性推送当前主题变量(__qomicex_theme);主题切换后新值不自动同步(以代码为准)。