For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 允许我们:

  1. 注入一个很小的桥接(camera_bridge.js)通过 Page.addScriptToEvaluateOnNewDocument 在任何 Meet 代码运行之前。

  2. 覆盖 navigator.mediaDevices.getUserMedia ,让它返回一个 MediaStream ,来源是一个隐藏的 640×480 canvas,而不是真实摄像头。

  3. 在这个 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 明显更好。

提供方
已迁移?
启动时加载什么

WhatsApp

零 JS

Telegram

零 JS

Slack

零 JS

Discord

零 JS

browserscan

零 JS

Gmail

历史沿用

旧版 runtime.js 桥接

LinkedIn

历史沿用

旧版 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 注入”规则。

最后更新于