# 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 运行前注入渲染进程侧补丁的情况。

现成的 webview 无法提供这些能力。所以我们内置了 CEF。

这个内置运行时位于 [`app/src-tauri/vendor/tauri-cef/`](https://github.com/tinyhumansai/openhuman/tree/main/app/src-tauri/vendor/tauri-cef) （从上游分支 `tauri-cef` 到 `tinyhumansai/tauri-cef:feat/cef-notification-intercept`，当前为 CEF 146.4.1）。每个 Tauri crate 都在 `app/src-tauri/Cargo.toml` 通过 `[patch.crates-io]` 进行补丁，以指向这个分支。内置的 `cargo-tauri` CLI 会把 Chromium 正确打包进 `Contents/Frameworks/`；而标准的 `@tauri-apps/cli` 会生成一个有问题的 bundle，并在 `cef::library_loader::LibraryLoader::new`. [`scripts/ensure-tauri-cli.sh`](https://github.com/tinyhumansai/openhuman/blob/main/scripts/ensure-tauri-cli.sh) 只要这个分支比已安装二进制更新，就会重新安装这个内置 CLI。

## CEF 今天的用途

### 嵌入式第三方 webview

每个作为托管 Web 应用运行的已连接提供方，都会拥有自己的子 CEF webview：

* WhatsApp Web
* Telegram Web
* Slack
* Discord
* Google Meet
* LinkedIn
* Gmail
* Zoom
* browserscan

每个账号的存储都隔离在 `{app_local_data_dir}/webview_accounts/{id}/`。两个 Slack 工作区，就是两个浏览器配置文件。代码： [`app/src-tauri/src/webview_accounts/mod.rs`](https://github.com/tinyhumansai/openhuman/blob/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)。每个扫描器都通过 WebSocket 长连接到 CEF 的 `--remote-debugging-port=19222` 并按固定节奏运行：

| 扫描器                | 频率                                 | 它的作用                                                        |
| ------------------ | ---------------------------------- | ----------------------------------------------------------- |
| `whatsapp_scanner` | 每 2 秒一次 DOM 轮询 + 每 30 秒一次完整 IDB 遍历 | 读取消息存储，提取媒体元数据                                              |
| `telegram_scanner` | 相同                                 | 另外处理 QR 登录并交接给原生 Telegram Desktop                           |
| `slack_scanner`    | 每 30 秒一次 IDB 遍历                    | 纯 IDB —— 不需要 DOM 抓取                                         |
| `discord_scanner`  | 周期性                                | 通过 CDP 获取频道和私信状态                                            |
| `meet_scanner`     | 周期性                                | 通话期间的实时字幕 + 参与者状态                                           |
| `imessage_scanner` | 周期性                                | **没有 webview。** 直接读取 `~/Library/Messages/chat.db` 在 macOS 上 |

每次扫描都会发出 `webview:event` 载荷，并将 POST `openhuman.memory_doc_ingest` 直接发送到核心 RPC，因此无论 UI 窗口是打开还是后台运行，记忆都会增长。

### Google Meet 吉祥物摄像头

最炫的 CEF 技巧。Meet 代理不只是 *加入* 会议，而是会 **广播** 自己作为一台摄像头。之所以能做到，是因为 CEF 让我们可以：

1. 注入一个很小的桥接脚本（`camera_bridge.js`）通过 `Page.addScriptToEvaluateOnNewDocument` 在任何 Meet 代码运行之前。
2. 覆盖 `navigator.mediaDevices.getUserMedia` 使其返回一个 `MediaStream` ，来源是一个隐藏的 640×480 画布，而不是真实摄像头。
3. 在该画布上渲染吉祥物 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).

### 原生通知拦截

位于 `feat/cef-notification-intercept` 的这个分支为以下内容添加了渲染进程侧补丁： `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/blob/main/CLAUDE.md): **已迁移的提供方加载时不会注入任何 JavaScript**。所有抓取都在扫描器侧通过 CDP 原生完成。

这很重要，因为任何由宿主控制、在第三方源内运行的东西，都会带来攻击面风险。在 Slack 里持久存在的 JS 桥接，只要一次 Slack 更新就可能被破坏，也只要一个失误就可能把桥接泄漏给攻击者控制的 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 启动分流排查

CEF 会在引导界面来得及从渲染失败中恢复之前初始化完成。如果 Windows 用户报告静默退出、永久停留在“Connecting...”转圈，或者在第一个可交互窗口出现之前就发生 `tauri-runtime-cef` 断言，请在问题中询问这些细节：

* Windows 版本和完整构建号，尤其是 Insider 版本。
* OpenHuman 版本和安装程序类型（`.msi` 或 `.exe`).
* 在重试之前，是否曾将 `%LOCALAPPDATA%\com.openhuman.app` 移到别处。
* 来自 `[startup]`, `[cef-profile]`，以及 `[cef-startup]`.
* 的启动日志行 `任何提到`.

tauri-runtime-cef/src/lib.rs

## Windows Insider 版本还要确认同一个安装程序是否能在当前稳定版 Windows 上启动。这样可以区分配置文件/缓存问题与 CEF 启动中的操作系统/运行时兼容性回归。

用于 CEF 启动崩溃的 Linux shell 回退方案 `BadWindow` 错误，而此时 CEF 已报告主浏览器上下文。

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

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

在另一个终端中：

```bash
cd app
pnpm dev
```

在普通浏览器中打开 Vite URL，选择 **高级** / 远程核心模式，将 RPC URL 设为 `http://127.0.0.1:7788/rpc`，并使用核心写入的 bearer token。这会绕过仅原生提供的功能，例如托盘、自动更新和嵌入式提供方 webview，但仍保留代理、记忆、技能以及 RPC 接口以便调试。

## 插件审计

新增到 `app/src-tauri/src/lib.rs` 的任何内容都必须审计是否包含 `js_init_script` 调用。 `tauri-plugin-opener` 默认会附带一个初始化脚本（`init-iife.js`），它会添加一个全局点击监听器；我们将其配置为 `.open_js_links_on_click(false)` ，以确保它不会在第三方 webview 中运行。 `tauri-plugin-notification`的初始化脚本同样已从内置副本中移除。

## 未来可能如何演进

CDP 接口是通用目的的。今天它支撑的是来自固定提供方列表的记忆摄取；同样的基础能力还能做更多事。

### 将浏览器自动化作为一等公民代理工具

今天代理拥有 [原生工具](/openhuman/zh/gong-neng/native-tools.md) 用于文件系统、git、网页搜索和网页抓取。下一个显而易见的工具是 **“驱动一个真实的浏览器会话”**：登录用户已经认证过的 SaaS，填写表单，抓取分页表格，下载导出文件。

底层管线已经具备。一个 `@openhuman/browser_task` 技能可以启动一个专用的 CEF webview，通过核心侧 CDP 驱动它，并将结果作为一次工具调用返回。用户现有的每账号配置文件意味着无需重新认证。

### 用于服务器端回放的无头 CEF

同样的扫描器模式（持久 WebSocket → IDB 遍历 + DOM 快照）在没有 UI 的情况下也适用。核心旁车中的无头 CEF 可以按计划回放会话，这对把核心托管在云端、并希望从不提供干净 OAuth API 的来源自动抓取的用户很有用。

### 浏览器进程层的隐私钩子

CEF 的 `CefRequestHandler` 已经允许我们拦截网络请求。从“拦截并记录”到“拦截并重写”只差一步：广告拦截、追踪器拦截、DNS 固定、按提供方重写请求。把隐私作为一等浏览器特性，而不是每个 origin 内部一个容易泄漏的 JS 补丁。

### 由 CDP 驱动的测试框架

扫描器模式、启动 webview、遍历 IDB、快照 DOM、求值一个临时表达式，在结构上与端到端测试编排完全相同。我们可以将 `@openhuman/web_test` 作为一个公开技能发布： `connect_cef → snapshot → evaluate → assert`。使用纯 Rust 编写、针对任意 Web 应用的测试，不依赖 Selenium / Playwright。

### 渲染器 ↔ Rust 消息通道

今天每个 CDP `Runtime.evaluate` 都是一次性发送、无需回传的。一个从渲染器到 Rust 的持久双向通道（就像 Tauri 为宿主应用做 IPC 的方式）将解锁流式场景：实时输入检测、实时选择 / 高亮跟踪、主动提醒。如何设计它而又不违反“第三方源中不允许持久 JS 桥接”这条规则，是一个很有意思的约束。

### 多账号合并

每个已连接账号都有自己的配置文件和自己的 IDB。CDP 可以快照某个账号的 IDB，与另一个账号的内容解密合并，并 upsert 到共享记忆文档中，例如在三个工作区之间形成一个统一的 Slack 记忆。

## 另见

* [`docs/TAURI_CEF_FINDINGS_AND_CHANGES.md`](https://github.com/tinyhumansai/openhuman/blob/main/docs/TAURI_CEF_FINDINGS_AND_CHANGES.md)通知权限深入解析。
* [`CLAUDE.md`](https://github.com/tinyhumansai/openhuman/blob/main/CLAUDE.md)“不注入新 JS”规范。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://tinyhumans.gitbook.io/openhuman/zh/kai-fa/cef.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
