> For the complete documentation index, see [llms.txt](https://tinyhumans.gitbook.io/openhuman/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tinyhumans.gitbook.io/openhuman/zh/kai-fa/cef.md).

# Chromium 嵌入式框架

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/`](https://github.com/tinyhumansai/openhuman/tree/main/app/src-tauri/vendor/tauri-cef/README.md) （从上游的 `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。`](https://github.com/tinyhumansai/openhuman/tree/main/scripts/ensure-tauri-cli.sh) 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`](https://github.com/tinyhumansai/openhuman/tree/main/app/src-tauri/src/webview_accounts/mod.rs).

### CDP 驱动的扫描器

每个提供方都有一个 **scanner 模块** 在 [`app/src-tauri/src/`](https://github.com/tinyhumansai/openhuman/tree/main/app/src-tauri/src/README.md)。每个扫描器都通过长连接 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/`](https://github.com/tinyhumansai/openhuman/tree/main/app/src-tauri/src/meet_video/README.md).

### 原生通知拦截

位于 `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`](https://github.com/tinyhumansai/openhuman/tree/main/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` 错误。

当核心本身是健康的，你可以通过把核心和前端分开运行来继续开发：

```bash
cargo build --bin openhuman-core
./target/debug/openhuman-core run --port 7788
```

在另一个终端中：

```bash
cd app
pnpm dev
```

在普通浏览器中打开 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 已经有 [原生工具](/openhuman/zh/gong-neng/native-tools.md) 用于文件系统、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`](https://github.com/tinyhumansai/openhuman/tree/main/CLAUDE.md)。 canonical 的“不要新增 JS 注入”规则。
