Chromium 嵌入式框架
为什么 OpenHuman 自带 Chromium 运行时,我们今天用它做什么,以及同一个 CDP 接口下一步能解锁什么。
OpenHuman 不运行在平台内置的 webview 上。它自带自己的 Chromium Embedded Framework(CEF)运行时 通过对 tauri-runtime的一个分支版本,而这个单一决定几乎支撑着产品里所有“OpenHuman 知道你的工具里发生了什么”的功能。
本页解释为什么 CEF 被打包进来、代码库今天用它来做什么,以及同一套表面未来还能走向哪里。
为什么用 CEF,而不是原生 webview
标准版 Tauri 使用每个平台的原生 webview:macOS 上是 WKWebView,Windows 上是 WebView2,Linux 上是 WebKitGTK。它们用于渲染 OpenHuman 应用本身完全没问题。但对我们的用例来说,它们有一个致命限制: 它们都不暴露 Chrome DevTools Protocol(CDP).
CDP 是这个系统的支柱级原语。OpenHuman 里每个“查看 Slack / WhatsApp / Telegram / Discord / Meet 里面正在发生什么”的功能,都是通过 CDP 而不是注入 JavaScript 来和这些嵌入式应用通信的。CDP 让我们可以:
Target.getTargets来发现每个页面和 service worker。IndexedDB.requestDatabaseNames/requestDatabase/requestData来遍历第三方应用的本地存储。DOMSnapshot.captureSnapshot用于只读 DOM 检查,不会触发框架响应式机制。Runtime.evaluate用于一次性的短暂读取(单个固定的 JSON 序列化器,绝不会有持久桥接)。Page.addScriptToEvaluateOnNewDocument用于少数真正需要在页面 JS 运行前就放入渲染器侧 shim 的情况。
原生 webview 无法提供这些能力。所以我们直接内置 CEF。
内置运行时位于 app/src-tauri/vendor/tauri-cef/ (从上游的 tauri-cef 分支 fork 到 tinyhumansai/tauri-cef:feat/cef-notification-intercept,当前版本为 CEF 146.4.1)。每个 Tauri crate 都在 app/src-tauri/Cargo.toml 中通过 [patch.crates-io] 指向这个 fork。内置的 cargo-tauri CLI 能把 Chromium 正确打包进 Contents/Frameworks/;而原版 @tauri-apps/cli 生成的 bundle 是坏的,会在 cef::library_loader::LibraryLoader::new. 时 panic。 scripts/ensure-tauri-cli.sh
CEF 目前的用途
嵌入式第三方 webview
每个以托管 Web 应用形式运行的已连接提供方,都有自己的子 CEF webview:
WhatsApp Web
Telegram Web
Slack
Discord
Google Meet
LinkedIn
Gmail
Zoom
微信
Google Messages
browserscan
每个账户的存储彼此隔离,位于 {app_local_data_dir}/webview_accounts/{id}/。两个 Slack 工作区,两个浏览器配置文件。代码: app/src-tauri/src/webview_accounts/mod.rs.
CDP 驱动的扫描器
每个提供方都有一个 scanner 模块 在 app/src-tauri/src/。每个扫描器都通过长连接 WebSocket 连接到 CEF 的 --remote-debugging-port=19222 ,并按固定节奏运行:
whatsapp_scanner
2 秒 DOM tick + 30 秒完整 IDB 扫描
读取消息存储,提取媒体元数据
telegram_scanner
相同
另外还支持 QR 登录交接到原生 Telegram Desktop
slack_scanner
30 秒 IDB 扫描
纯 IDB - 不需要 DOM 抓取
discord_scanner
周期性
通过 CDP 获取频道和私信状态
meet_scanner
周期性
通话期间的实时字幕 + 参与者状态
wechat_scanner
周期性
通过 CDP 抓取微信 Web 聊天列表 + 当前会话 DOM
gmessages_scanner
周期性
Google Messages Web 只读 IndexedDB 扫描
imessage_scanner
周期性
没有 webview。 读取 ~/Library/Messages/chat.db ,直接在 macOS 上
每次扫描都会发出 webview:event 负载,并将 openhuman.memory_doc_ingest 直接 POST 到核心 RPC,因此无论 UI 窗口是否打开,记忆都会持续增长。
Google Meet 吉祥物摄像头
最炫的 CEF 技巧。Meet 代理不只是 加入 一个会议,它还会 广播 自己作为摄像头。这之所以可行,是因为 CEF 允许我们:
注入一个很小的桥接(
camera_bridge.js)通过Page.addScriptToEvaluateOnNewDocument在任何 Meet 代码运行之前。覆盖
navigator.mediaDevices.getUserMedia,让它返回一个MediaStream,来源是一个隐藏的 640×480 canvas,而不是真实摄像头。在这个 canvas 上渲染吉祥物 SVG,并通过
window.__openhumanSetMood(...)从 Rust 经由 CDP 驱动来切换心情状态(空闲、思考、说话)。
还有一条构建时路径,会把吉祥物 SVG 光栅化为 Y4M,并使用 CEF 的原生 --use-file-for-fake-video-capture 标志,这是一个完全原生的假摄像头源,完全不需要 JS。
代码: app/src-tauri/src/meet_video/.
原生通知拦截
位于 feat/cef-notification-intercept 这个 fork 中的补丁,为以下内容添加了渲染器侧 shim: Notification.permission, Notification.requestPermission(),以及 navigator.permissions.query({name: "notifications"})。这些现在会安装到真实的 tauri-runtime-cef 路径中的每条运行时代码路径上,因此当 Slack 检查自己是否能显示通知时,答案会与 CEF 的权限回调已经授予的内容保持一致。
这就是 docs/TAURI_CEF_FINDINGS_AND_CHANGES.md中的大部分内容。它也是 Slack 在一次会话里不再反复询问同一权限的原因。
“不新增 JS 注入”规则
该规则记录在 CLAUDE.md: 迁移后的提供方加载时不会注入任何 JavaScript。所有抓取都在扫描器端通过 CDP 原生完成。
这很重要,因为任何在第三方 origin 内运行、由宿主管控的东西,都会带来攻击面风险。Slack 里的持久 JS bridge 只需要一次 Slack 更新就可能失效,也只需要一次失误就可能把 bridge 泄漏给攻击者控制的 JS。从渲染器外部使用 CDP 明显更好。
✅
零 JS
Telegram
✅
零 JS
Slack
✅
零 JS
Discord
✅
零 JS
browserscan
✅
零 JS
Gmail
历史沿用
旧版 runtime.js 桥接
历史沿用
旧版 LINKEDIN_RECIPE_JS
Google Meet
历史沿用
摄像头 + 音频 + 字幕桥接
旧式注入应该越来越少,绝不能增加。新提供方直接走纯 CDP 路径。
CEF 预热
一个隐藏的 CEF webview(cef-prewarm)会在应用启动时把浏览器启动好,这样用户点击时第一个子 webview 就能立刻弹出。它会在 cef::shutdown() 之前销毁,以避免退出时发生竞态。参见 app/src-tauri/src/lib.rs 中关于预热 + 关闭生命周期的部分。
Windows 启动排查
在 onboarding UI 来得及从渲染器故障中恢复之前,CEF 就已经初始化了。如果 Windows 用户报告无声退出、永久“Connecting...”转圈,或者在第一个交互窗口出现之前就出现 tauri-runtime-cef 断言失败,请在 issue 中询问这些信息:
Windows 版本和完整 build 号,尤其是 Insider 版本。
OpenHuman 版本和安装包类型(
.msi或.exe).是否
%LOCALAPPDATA%\com.openhuman.app在重试前已被移走。来自
[startup],[cef-profile],以及[cef-startup].任何提到
tauri-runtime-cef/src/lib.rs.
的 panic 文本。对于 Windows Insider 版本,也要确认同一个安装包在当前稳定版 Windows 上是否能启动。这能把配置文件/缓存问题和 CEF 启动时的操作系统/运行时兼容性回归区分开。
如果日志指向的是 GPU 进程启动失败,而不是陈旧的 CEF 配置文件锁,请在启动 OpenHuman 前设置 OPENHUMAN_DISABLE_GPU=1 。在 Windows 上,这会把 CEF 固定到纯软件的 ANGLE/SwiftShader GL 后端(--use-gl=angle --use-angle=swiftshader --enable-unsafe-swiftshader --disable-gpu-compositing),而不是直接使用 --disable-gpu:在 NVIDIA Blackwell / RTX 50 系列栈上,GPU 进程无法初始化,而 --disable-gpu 单独使用时会让 CEF 没有可用的软件 GL 路径,所以 cef::initialize 仍然返回 0(#4294,#4385)。SwiftShader 不需要硬件驱动,所以它能让 CEF 在那些捆绑的 Chromium(当前为 CEF 146.4.1)尚不支持的 GPU 上启动。在其他平台上,同一个环境变量会传递 --disable-gpu 以及 --disable-gpu-compositing ,而不会转发任意 Chromium 参数。正常使用时请保持未设置,因为强制软件渲染会拖慢 WebGL 密集型界面。
Linux 上 CEF 启动崩溃时的 shell 兜底方案
在某些 Linux 桌面环境中,尤其是 Wayland/XWayland 下使用 NVIDIA 专有驱动的配置,Tauri/CEF shell 可能会在原生窗口配置阶段失败,而 React 应用此时还没来得及可用。一个已知症状是在 CEF 报告主浏览器上下文之后出现 X11 BadWindow 错误。
当核心本身是健康的,你可以通过把核心和前端分开运行来继续开发:
在另一个终端中:
在普通浏览器中打开 Vite URL,选择 Advanced / 远程核心模式,把 RPC URL 设为 http://127.0.0.1:7788/rpc,然后使用核心写入的 bearer token。这会绕过仅原生才有的功能,比如托盘、自动更新和嵌入式提供方 webview,但仍保留 agent、memory、skills 和 RPC 表面供调试使用。
插件审计
新增到 app/src-tauri/src/lib.rs 中的任何内容都必须审计其 js_init_script 调用。 tauri-plugin-opener 默认会附带一个 init script(init-iife.js),它会添加一个全局点击监听器;我们通过 .open_js_links_on_click(false) 进行配置,让它不会在第三方 webview 中运行。 tauri-plugin-notification的 init script 也同样从内置副本中移除了。
这将如何演进
CDP 表面是通用的。今天它为一组固定提供方的记忆摄取提供动力;同一个原语还能做更多事。
将浏览器自动化作为一等公民代理工具
今天 agent 已经有 原生工具 用于文件系统、git、网页搜索和网页抓取。下一个显而易见的工具是 “驱动一个真实的浏览器会话”:登录用户已经认证过的 SaaS,填写表单,抓取分页表格,下载导出文件。
相关基础设施已经就位。一个 @openhuman/browser_task 技能可以启动一个专用的 CEF webview,通过核心端的 CDP 来驱动它,并把结果作为一次工具调用暴露出来。用户现有的按账户配置文件意味着无需重新认证。
用于服务端回放的无头 CEF
同样的扫描器模式(长生命周期 WebSocket → IDB 扫描 + DOM 快照)在没有 UI 的情况下也能工作。核心 sidecar 中的无头 CEF 可以按计划回放会话,这对把核心托管在云端、并希望从不提供干净 OAuth API 的来源自动抓取数据的用户很有用。
浏览器进程层面的隐私钩子
CEF 的 CefRequestHandler 已经允许我们拦截网络请求。从“拦截并记录”到“拦截并重写”只是一步之遥:广告拦截、追踪器拦截、DNS pinning、按提供方重写请求。把隐私作为一等浏览器功能,而不是每个 origin 里一个会泄漏的 JS shim。
CDP 驱动的测试框架
扫描器模式——启动 webview、遍历 IDB、快照 DOM、计算一个短暂表达式——在结构上与 E2E 测试编排完全一致。我们可以把 @openhuman/web_test 作为一个公开技能发布: connect_cef → snapshot → evaluate → assert。用纯 Rust 针对任意 web 应用编写测试,不依赖 Selenium / Playwright。
渲染器 ↔ Rust 消息通道
今天每个 CDP Runtime.evaluate 都是一次性发送、无需响应。一个从渲染器到 Rust 的长生命周期双向通道(就像 Tauri 为宿主应用做 IPC 的方式)会解锁流式用例:实时输入检测、实时选择 / 高亮跟踪、主动提醒。关键约束是如何设计它而不违反“第三方 origin 中不允许持久 JS bridge”的规则。
多账户合并
每个已连接账户都有自己的配置文件和自己的 IDB。CDP 可以快照一个账户的 IDB,与另一个账户的 IDB 解密合并,并 upsert 到共享的 memory 文档中,例如在三个工作区之间统一 Slack 记忆。
另请参阅
CLAUDE.md。 canonical 的“不要新增 JS 注入”规则。
最后更新于