ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入解析 anarlog 的 chrome-native-host:用 Rust 桥接 Chrome 扩展与桌面应用的 Native Messaging 实践

深入解析 anarlog 的 chrome-native-host:用 Rust 桥接 Chrome 扩展与桌面应用的 Native Messaging 实践 深入解析 anarlog 的 chrome-native-host用 Rust 桥接 Chrome 扩展与桌面应用的 Native Messaging 实践【免费下载链接】anarlogOpen source Granola AI Alternative项目地址: https://gitcode.com/GitHub_Trending/hy/anarlog导读chrome-native-host是 anarlog开源 Granola AI 替代品中一个短小精悍的 Rust 二进制程序它注册为 Chrome Native Messaging host被 Chrome 以子进程方式拉起通过 stdin/stdout 与浏览器扩展通信并把 Google Meet 的会议状态原子化地落盘到chrome_state.json供桌面端detect模块轮询消费。读完本文你将掌握 Chrome Native Messaging 的完整协议实现、消息契约设计、原子写入原理以及它在 Tauri 打包流程中的集成方式并可以直接阅读 crates/chrome-native-host/src/main.rs 验证每一项细节。一、模块定位Chrome 扩展与桌面应用之间的“翻译层”在 anarlog 的整体架构中浏览器端Chrome 扩展的background.jsservice worker负责从 Google Meet 标签页采集会议状态而桌面端Tauri 插件与 React/Zustand 前端需要消费这些状态。两者运行在完全不同的进程边界里chrome-native-host就是连接它们的桥梁Google Meet tab │ chrome.runtime.sendMessage(payload) ▼ background.js (service worker) │ port.postMessage(payload) [Chrome Native Messaging] ▼ char-chrome-native-host ← this binary │ writes atomically ▼ chrome_state.json (on disk) │ polled every 500 ms ▼ GoogleMeetWatcher (crates/detect) │ fires DetectEvent ▼ Tauri plugin → React/Zustand这条链路的关键设计决策是浏览器端与桌面端之间不直接进行 IPC而是通过一个磁盘文件作为解耦介质。Chrome 侧只负责把最新状态写进文件桌面端以 500 ms 的固定间隔轮询文件、比较时间戳来判断状态变化从而避免了跨平台、跨进程通信的大量复杂性。在 crates/detect/src/meeting_ax/platform.rs 中可以看到detect模块正是通过host meet.google.com来识别 Google Meet 平台与本模块的 URL 校验逻辑完全对应。二、包结构与构建产物该模块的 Cargo 包定义在 crates/chrome-native-host/Cargo.toml要点如下[package] name chrome-native-host version 0.1.0 edition 2024 [[bin]] name char-chrome-native-host path src/main.rs [dependencies] dirs { workspace true } serde { workspace true, features [derive] } serde_json { workspace true } tempfile { workspace true }值得注意包名是chrome-native-host但可执行文件名是char-chrome-native-host通过[[bin]]显式指定这是为了让 Chrome 的 Native Messaging manifest 能按精确的 host 名称定位到该二进制。依赖极简dirs用于解析平台数据目录serde/serde_json负责消息的序列化与反序列化tempfile用于实现原子写入。整个模块只有一个源文件 crates/chrome-native-host/src/main.rs逻辑高度内聚。三、Native Messaging 线协议4 字节长度前缀 UTF-8 JSONChrome Native Messaging 使用stdin/stdout作为传输通道消息采用固定帧格式┌─────────────────┬──────────────────────────────┐ │ 4 bytes LE u32 │ N bytes UTF-8 JSON │ │ (message len) │ (message body) │ └─────────────────┴──────────────────────────────┘规则如下长度字段是小端序little-endian无符号 32 位整数消息体是 UTF-8 编码的 JSON 对象二进制程序在一个循环中持续读取消息直到读到 EOF即 Chrome 关闭了端口。在源码中read_message函数main.rs精确实现了这一帧格式并且加入了防御性限制const MAX_MESSAGE_SIZE: usize 256 * 1024; fn read_message(reader: mut impl Read) - io::ResultOptionVecu8 { let mut len_buf [0u8; 4]; match reader.read_exact(mut len_buf) { Ok(()) {} Err(e) if e.kind() io::ErrorKind::UnexpectedEof return Ok(None), Err(e) return Err(e), } let len u32::from_le_bytes(len_buf) as usize; if len MAX_MESSAGE_SIZE { return Err(io::Error::new( io::ErrorKind::InvalidData, format!(message too large: {len}), )); } let mut buf vec![0u8; len]; reader.read_exact(mut buf)?; Ok(Some(buf)) }实现细节EOF 语义当读不到 4 字节头时返回None主循环据此break退出对应 Chrome 关闭端口大小上限单条消息超过256 * 1024字节直接报错防止恶意或异常扩展撑爆内存截断保护read_exact保证长度头与实际体一致体不完整会返回错误测试test_read_message_truncated_body_errors专门验证了这一点。对应的测试用例在 main.rstest_read_message_valid、test_read_message_eof_returns_none、test_read_message_rejects_oversized_message分别覆盖了正常帧、EOF 与超限帧三种情况。四、入站消息契约meeting_state与meeting_endedbackground.js只发送两种消息类型Rust 端通过IncomingMessage结构体反序列化main.rs#[derive(Debug, Deserialize)] struct IncomingMessage { #[serde(rename type)] msg_type: String, url: OptionString, is_active: Optionbool, muted: Optionbool, participants: OptionVecParticipant, }4.1meeting_state— 会议进行中{ type: meeting_state, url: https://meet.google.com/abc-defg-hij, is_active: true, muted: false, participants: [ { name: Alice, is_self: true }, { name: Bob, is_self: false } ] }4.2meeting_ended— 标签页关闭或导航离开{ type: meeting_ended, url: https://meet.google.com/abc-defg-hij, is_active: false }4.3 处理语义与防御性校验process_messagemain.rs是核心的“纯函数”逻辑不涉及任何 I/O未知type一律静默忽略ProcessedMessage::Ignore不会清空已有状态测试test_run_unknown_type_does_not_clear_existing_state验证meeting_state且is_active为 false等价于会议结束输出Update(None)清空状态meeting_ended且is_active为 true视为矛盾消息直接忽略。URL 与参与者都经过归一化处理const MAX_URL_LENGTH: usize 2048; const MAX_PARTICIPANTS: usize 30; const MAX_PARTICIPANT_NAME_LENGTH: usize 80; fn normalize_url(url: OptionString) - OptionString { let value url?.trim().to_owned(); if value.is_empty() || value.len() MAX_URL_LENGTH { return None; } if !value.starts_with(https://meet.google.com/) { return None; } Some(value) }URL 必须以https://meet.google.com/为前缀否则整条消息被忽略test_process_invalid_url_is_ignored用https://example.com/abc验证了这一点这是安全边界即使扩展被劫持host 进程也不会接受非 Meet 的 URL参与者名字会 trim 空白、过滤空名、截断超过 80 字符的名字且最多保留 30 人test_process_participants_are_sanitized验证了名字清洗逻辑muted缺省时默认falsetest_process_defaults_muted_false验证。五、输出契约原子写入chrome_state.json每条有效消息处理后状态会被写入{data_dir}/char/chrome_state.json。data_dir由dirs::data_dir()解析各平台位置如下macOS~/Library/Application SupportLinux~/.local/shareWindowsC:\Users\{user}\AppData\Roaming5.1 磁盘 Schema{ version: 1, timestamp_ms: 1700000000000, meeting: { url: https://meet.google.com/abc-defg-hij, is_active: true, muted: false, participants: [ { name: Alice, is_self: true } ] } }对应 Rust 结构体main.rsstruct ChromeState { version: u32, timestamp_ms: u64, meeting: OptionMeetingState, } struct MeetingState { url: String, is_active: bool, muted: bool, participants: VecParticipant, }关键语义meeting为null表示会议已结束或is_active为 falsetimestamp_ms在每次写入时都被刷新为当前 Unix 毫秒时间戳crates/detect中的 watcher 会把超过 30 秒未更新的状态视为过期stale因此即使扩展意外崩溃桌面端也能通过时间戳判断状态已失效。5.2 原子写入实现写入是原子操作先在目标目录创建NamedTempFile写入并flush后再persistrename到最终路径因此 watcher 永远不会读到写了一半的残缺文件fn write_state(state: ChromeState, path: Path) - io::Result() { if let Some(parent) path.parent() { std::fs::create_dir_all(parent)?; } let dir path.parent().unwrap(); let mut tmp tempfile::NamedTempFile::new_in(dir)?; serde_json::to_writer(mut tmp, state)?; tmp.as_file_mut().flush()?; tmp.persist(path).map_err(|e| e.error)?; Ok(()) }两个细节值得注意create_dir_all会自动创建缺失的父目录保证首次运行时无需预建目录测试test_write_state_creates_parent_dirs验证了嵌套目录场景persist在 POSIX 上是原子 rename读取方要么看到旧文件要么看到新文件不存在中间态。5.3 主循环read → process → writerun函数main.rs把三个阶段串成完整循环read_message从 stdin 读一条带帧消息JSON 反序列化失败则continue跳过测试test_run_invalid_json_is_skipped验证坏消息不会中断后续处理process_message返回Ignore则继续读下一条返回Update则组装ChromeState并原子写入读到 EOF 或发生读取错误时退出。main函数main.rs锁定 stdin 并解析默认状态路径后直接调用run逻辑非常直白。六、可测试单元纯函数优先的设计AGENTS.md 将模块拆成了四个可独立测试的单元这一设计思路I/O 与逻辑分离非常值得借鉴函数职责测试方式read_message从Read解码带帧字节喂入CursorVecu8process_message把IncomingMessage映射为OptionMeetingState纯函数无 I/Owrite_state把 JSON 原子写入Path传入tempdir路径run完整循环读 → 处理 → 写用Cursor管道传入编码消息断言文件内容测试代码全部内联在 main.rs共覆盖帧读取的 4 种场景正常、EOF、截断、超限process_message的 7 种语义分支活跃、结束、inactive 标志、muted 缺省、未知类型、非法 URL、参与者清洗原子写入与父目录创建完整 round-trip连续两条消息、坏 JSON 跳过、未知类型不清状态。其中test_run_meeting_ended_clears_meeting完整演示了“先写活跃状态、再写结束消息、最终meeting为 null”的端到端行为。运行测试的命令cargo test --package chrome-native-host七、打包与分发TauriexternalBin集成生产环境下该二进制被打包进应用 bundle作为 Tauri 的externalBin在tauri.conf.json中声明最终位于Contents/MacOS/char-chrome-native-host与主可执行文件同目录。7.1 关键prepare-binaries.mjs在beforeBundleCommand中构建AGENTS.md 详细说明了构建脚本的工作流程读取TAURI_ENV_TARGET_TRIPLE这是 Tauri 设置的环境变量表示实际构建目标而非宿主架构。这一点对交叉编译至关重要——CI 的 macOS 任务在单个 ARM runner 上同时构建aarch64-apple-darwin和x86_64-apple-darwin因此在 x86_64 构建时宿主 ≠ 目标执行cargo build --release --target $triple -p chrome-native-hostcwd设为apps/desktop/src-tauri/使.cargo/config.toml中的target-dir target生效产物落在apps/desktop/src-tauri/target/$triple/release/char-chrome-native-host[.exe]复制到binaries/char-chrome-native-host-$triple[.exe]这正是externalBin: [binaries/char-chrome-native-host]所期望的路径。这一流程不需要任何 CI 步骤或build.rs修改——beforeBundleCommand在tauri build的 Cargo 步骤之后运行交叉编译工具链此时已就绪天然解决了“为其他架构预编译 Native Messaging host”的问题。7.2 开发模式debug无需打包在debug_assertions开发构建下运行时直接解析到target/debug/char-chrome-native-host——该二进制由 Cargo 作为 workspace 的一部分构建无需任何打包步骤。这保证了开发迭代时改完代码cargo build即可生效与生产 bundle 流程完全解耦。八、设计启示与最佳实践总结从chrome-native-host这个约 200 行的二进制中可以提炼出几条具有通用价值的工程实践文件即协议磁盘即总线用轮询文件替代跨进程 IPC让浏览器端与桌面端彼此零依赖、可独立发布代价只是最多 500 ms 的延迟对于“会议是否在进行”这类低频状态完全可接受纯函数与 I/O 分离process_message是无 I/O 的纯函数所有边界条件都可以用Cursor 断言穷举测试测试成本极低、覆盖率极高防御式输入校验单条消息 256 KB 上限、URL 白名单前缀https://meet.google.com/、参与者数量与长度限制任何一个环节都在对抗“不可信浏览器端”的异常输入原子写入 时间戳保鲜NamedTempFile rename 保证读侧永远看不到半截文件timestamp_ms让消费方crates/detect可以基于 30 秒过期阈值优雅处理扩展崩溃版本化 schemaversion: 1字段为未来协议演进预留了迁移空间同时meeting: null的显式空态让“无会议”与“会议数据缺失”可以被明确区分。如果你正在为浏览器扩展与桌面应用之间设计桥接层或者需要理解 anarlog 的会议自动记录链路crates/chrome-native-host/AGENTS.md 与 crates/chrome-native-host/src/main.rs 是目前仓库中最直接、最完整的参考实现。【免费下载链接】anarlogOpen source Granola AI Alternative项目地址: https://gitcode.com/GitHub_Trending/hy/anarlog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表