> 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-zhong/cef.md).

# Chromium 嵌入式框架（历史）

OpenHuman 曾自带 Chromium（CEF）运行时期间的历史设计笔记。当前 shell 运行在标准 Tauri（Wry）上；此处仅作为背景保留。

{% hint style="warning" %}
**历史背景。** 桌面外壳不再捆绑 CEF。 `crates/openhuman-app/` 基于上游 Tauri 的原生 webview 运行时（Wry：WKWebView / WebView2 / WebKitGTK），并且 `AGENTS.md` 禁止恢复 CEF 或 CDP 扫描器的假设。原生 iMessage 扫描器仍然保持独立，因为它直接读取 `chat.db` 。下面的内容都描述已退役的运行时，仅作为设计背景保留；它引用的文件路径（`crates/openhuman-app/src/webview_accounts/`, `scripts/ensure-tauri-cli.sh`, `vendor/tauri-cef`）已不再存在。
{% endhint %}

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 作为 vendored 依赖。

被 vendored 的运行时位于 `crates/openhuman-app/vendor/tauri-cef/` （从上游 `tauri-cef` 分支 fork 自 `tinyhumansai/tauri-cef:feat/cef-notification-intercept`，当前为 CEF 146.4.1）。每个 Tauri crate 都通过 `crates/openhuman-app/Cargo.toml` 通过 `[patch.crates-io]` 指向这个 fork。vendored 的 `cargo-tauri` CLI 能正确将 Chromium 捆绑进 `Contents/Frameworks/`；而标准的 `@tauri-apps/cli` 生成的是一个有缺陷的 bundle，会在 `cef::library_loader::LibraryLoader::new`. `scripts/ensure-tauri-cli.sh` （已移除）当 fork 比已安装二进制更新时，会重新安装 vendored CLI。

## CEF 目前用于什么

### 内嵌的第三方 webview

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

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

每个账户的存储彼此隔离，位于 `{app_local_data_dir}/webview_accounts/{id}/`。两个 Slack 工作区，就对应两个浏览器配置文件。代码（已移除）： `crates/openhuman-app/src/webview_accounts/mod.rs`.

### 由 CDP 驱动的扫描器

每个提供商都有一个 **扫描器模块** 位于 `crates/openhuman-app/src/` （已全部移除，除了 `imessage_scanner/`）。每个扫描器都维护一个到 CEF 的长连接 WebSocket，连接到 `--remote-debugging-port=19222` ，并按固定计划运行：

| 扫描器                 | 频率                         | 做什么                                                             |
| ------------------- | -------------------------- | --------------------------------------------------------------- |
| `whatsapp_scanner`  | 2 秒 DOM 轮询 + 30 秒完整 IDB 遍历 | 读取消息存储，抓取媒体元数据                                                  |
| `telegram_scanner`  | 相同                         | 另外通过二维码登录切换到原生 Telegram Desktop                                 |
| `slack_scanner`     | 30 秒 IDB 遍历                | 纯 IDB——无需 DOM 抓取                                                |
| `discord_scanner`   | 周期性                        | 通过 CDP 获取频道和私信状态                                                |
| `meet_scanner`      | 周期性                        | 通话期间的实时字幕 + 参会者状态                                               |
| `wechat_scanner`    | 周期性                        | 通过 CDP 抓取 WeChat Web 聊天列表和当前会话的 DOM                             |
| `gmessages_scanner` | 周期性                        | Google Messages Web 只读 IndexedDB 遍历                             |
| `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 canvas，而不是真实摄像头。
3. 在该 canvas 上渲染吉祥物 SVG，并通过 `window.__openhumanSetMood(...)` 由 Rust 通过 CDP 驱动切换心情状态（空闲、思考、说话）。

还有一条构建时路径，会将吉祥物 SVG 光栅化为 Y4M，并使用 CEF 原生的 `--use-file-for-fake-video-capture` 标志位，这是一个完全原生的假摄像头源，完全不需要 JS。

代码（已移除）： `crates/openhuman-app/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`](https://github.com/tinyhumansai/openhuman/tree/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()` 之前销毁，以避免退出时的竞态。参见 `crates/openhuman-app/src/lib.rs` 中有关预热 + 关闭生命周期的部分。

## Windows 启动故障排查

CEF 会在引导界面来得及从渲染器故障中恢复之前完成初始化。如果 Windows 用户报告静默退出、永久卡在“Connecting...”转圈，或者 `tauri-runtime-cef` 在第一个可交互窗口出现前的断言错误，请在 issue 中索要以下细节：

* Windows 版本和完整构建号，尤其是 Insider 构建。
* OpenHuman 版本和安装包类型（`.msi` 或 `.exe`).
* 是否 `%LOCALAPPDATA%\\com.openhuman.app` 在重试前已被移走。
* 来自以下日志行： `[startup]`, `[cef-profile]`，以及 `[cef-startup]`.
* 任何提到 `tauri-runtime-cef/src/lib.rs`.

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

如果日志指向的是 GPU 进程启动失败，而不是过期的 CEF 配置文件锁，请设置 `OPENHUMAN_DISABLE_GPU=1` 在启动 OpenHuman 之前。在 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 密集型界面。

## CEF 启动崩溃时的 Linux shell 备用方案

在某些 Linux 桌面上，尤其是在 Wayland/XWayland 下使用 NVIDIA 专有驱动的环境中，Tauri/CEF 外壳可能会在原生窗口配置阶段失败，导致 React 应用尚未可用。一个已知症状是 X11 `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 接口可用于调试。

## 插件审计

任何新增到 `crates/openhuman-app/src/lib.rs` 都必须审计其 `js_init_script` 调用。 `tauri-plugin-opener` 默认附带一个 init 脚本（`init-iife.js`），其中会添加一个全局点击监听器；我们通过 `.open_js_links_on_click(false)` 将其配置为不在第三方 webview 中运行。 `tauri-plugin-notification`的 init 脚本也同样从 vendored 副本中移除了。

## 这可能如何演进

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 固定、按提供商重写请求。把隐私作为一等浏览器功能，而不是每个源内部一个容易泄漏的 JS shim。

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

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

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

今天每个 CDP `Runtime.evaluate` 都是发出即忘。一个从渲染器到 Rust 的长连接双向通道（就像 Tauri 为宿主应用做 IPC 那样）将解锁流式用例：实时输入检测、实时选择/高亮跟踪、主动提示。如何设计它而不违反“第三方源中不允许持久 JS 桥接”这一规则，才是有趣的约束。

### 多账户合并

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

## 另请参见

* [`CLAUDE.md`](https://github.com/tinyhumansai/openhuman/tree/main/CLAUDE.md)。“不再新增 JS 注入”这一规范规则。
