Appearance
L4 远程 WebView
L4 是插件渲染的最高隔离层级:插件页面运行在独立的 Tauri WebviewWindow(独立 renderer 进程)中,与主界面进程级隔离。即使插件陷入 busy-loop 也只冻结自己的窗口,不影响主 UI(主界面仍可响应、渲染、关闭)。
适合重 UI、不受控的插件(如复杂编辑器、游戏 UI 增强、长驻后台页面)——这类插件在 iframe(L2)里仍与主界面共享同一 WebView,一个死循环就能卡死整个启动器。
manifest 声明
满足以下任一条件即启用 L4 渲染:
json
{
"render": "webview",
"layers": ["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 默认允许):
┌─────────────── 主窗口(main) ───────────────┐ ┌──────── l4 窗口(plugin-webview-{id}) ────────┐
│ plugin-loader: createRemoteWebview() │ │ App 轻量模式 → PluginWebviewPage │
│ ├─ 打开 WebviewWindow(/plugins/p/:id?…) │──────▶ │ ├─ iframe(sandbox) 加载插件 HTML+桥脚本 │
│ └─ initL4Bridge() 监听 plugin-l4-api-call │ │ └─ 插件 __plugin_api_call 消息 │
│ handleApiCall(权限校验+执行) │◀───────│ ├─ emitTo('main', plugin-l4-api-call) │
│ └─ emitTo(l4label, plugin-l4-api-response) ───▶ │ └─ postMessage 回插件 iframe │
└──────────────────────────────────────────────┘ └──────────────────────────────────────────────┘- 激活:插件启用时
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。
消息体复用 sandbox.ts 的 postMessage 协议({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) 的取舍
| 维度 | iframe(l2) | WASM(l3) | 远程 WebView(l4) |
|---|---|---|---|
| 运行位置 | 主窗口内 iframe(opaque origin) | 后端 Wasmtime 沙箱 | 独立 WebViewWindow / renderer 进程 |
| 隔离强度 | DOM/CSS 隔离 | 无 DOM,纯计算 | 进程级隔离,崩溃/死循环互不影响 |
| busy-loop 影响 | 冻结主界面 | 不涉及 UI | 只冻结本窗口 |
| API 调用 | postMessage 桥 | Host API 门控 | Tauri 事件 → 主窗口桥(权限校验一致) |
| 适用场景 | 常规 UI 插件(默认推荐) | 协议解析、Agent、复杂逻辑 | 重 UI / 不受控 / 需强隔离的插件 |
| 运行前提 | 无 | 包内 plugin.wasm | Tauri 桌面环境(非 Tauri 自动降级 iframe) |
选择建议
- 常规 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);主题切换后新值不自动同步(以代码为准)。