Skip to content

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          │
└──────────────────────────────────────────────┘        └──────────────────────────────────────────────┘
  1. 激活:插件启用时 createRemoteWebview 打开窗口 plugin-webview-{id}(默认 1200×800),地址为插件页 URL 并带 pluginWebview=1 标记。
  2. 轻量启动:App.tsx 检测到该标记后不加载主 Layout / 路由 / 插件自动激活,只渲染 PluginWebviewPage。
  3. 渲染:PluginWebviewPage 拉取插件前端资源,经 buildPluginDoc 注入主题变量与桥脚本,渲染进 sandbox="allow-scripts" 的 iframe。
  4. API 转发:iframe 内 __PLUGIN_API__ 调用发出 __plugin_api_call 消息 → 页面转发为 Tauri 事件 emitTo('main', 'plugin-l4-api-call', {pluginId, id, method, args})。
  5. 主窗口执行:主窗口 initL4Bridge 收到事件 → 调用与 iframe 相同的 handleApiCall(METHOD_PERMISSIONS 权限校验 + executePluginMethod)→ 响应经 emitTo(l4label, 'plugin-l4-api-response') 回发 → PluginWebviewPage listen 后 postMessage 原样回传 iframe。
  6. 流式中止:__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.wasmTauri 桌面环境(非 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);主题切换后新值不自动同步(以代码为准)。