OpenScreen 原生桥(Native Bridge)架构解析:四层模型、版本化契约与 IPC 传输设计
本文基于 OpenScreen 仓库的架构文档 docs/architecture/native-bridge.md,系统讲解其 Native Bridge 的设计目标、四层架构分层、版本化契约(versioned contracts)与统一结果封装,并结合 src/native/contracts.ts、electron/ipc/nativeBridge.ts 等源码说明各层的实际实现。读完后,你将能够理解 Electron 应用中如何为渲染进程提供一套“单一事实来源、能力可探测、错误可预期”的平台原生能力访问层,并掌握在其中新增 domain/action 的完整路径。
一、设计目标:薄传输、厚契约
文档开宗明义地给出了 Native Bridge 的目标:
Provide a single, resilient source of truth for platform-native capabilities while keeping Electron transport thin and renderer APIs unified. (在保持 Electron 传输层薄、渲染端 API 统一的前提下,为平台原生能力提供一个单一且坚韧的事实来源。)
拆开来看包含三个约束:
- 单一事实来源(Single source of truth):所有运行时原生状态(当前平台、当前项目路径、当前视频路径、光标遥测加载记录等)集中在 Electron 主进程,而不是散落在渲染进程的组件状态里;
- 传输层要薄(Thin transport):IPC 通道不承载业务逻辑,只负责把一个结构化的
NativeBridgeRequest传进主进程、把结构化的NativeBridgeResponse传回来; - 渲染端 API 统一(Unified renderer APIs):React 代码只依赖一个客户端(
src/native/client.ts),不直接绑定零散的 Electron IPC 通道。
这套设计的收益在于:渲染进程永远不需要知道“光标数据到底来自 macOS 的 ScreenCaptureKit helper 还是 Windows 的 WGC 采集器”(参见 electron/native/screencapturekit、electron/native/wgc-capture 两个平台原生实现目录),它只面对统一的 cursor domain API。
二、四层架构:从原生适配器到渲染客户端
文档把整个桥接体系划分为四层,仓库中的目录结构与之一一对应。下面逐层展开,并给出每层的落地代码。
1. Native adapters(原生适配器层)
平台特定的 provider 实现稳定的领域接口,例如光标遥测或系统资产发现。
该层的核心是一份“能力接口”:electron/native-bridge/cursor/adapter.ts 定义了 CursorNativeAdapter 接口:
export interface CursorNativeAdapter {
readonly kind: CursorProviderKind; // "native" | "none"
getCapabilities(): Promise<CursorCapabilities>;
getRecordingData(videoPath?: string | null): Promise<CursorRecordingData>;
getTelemetry(videoPath?: string | null): Promise<CursorTelemetryLoadResult>;
}
关键点在于 kind 字段:适配器必须自报家门是 "native"(真正的平台原生实现)还是 "none"(回退实现)。当前仓库中注册的适配器是 electron/native-bridge/cursor/telemetryCursorAdapter.ts:
export class TelemetryCursorAdapter implements CursorNativeAdapter {
readonly kind = "none" as const;
async getCapabilities(): Promise<CursorCapabilities> {
return { telemetry: true, systemAssets: false, provider: this.kind };
}
// getRecordingData / getTelemetry 内部先 resolveVideoPath,
// 解析不到视频路径时返回空数据,而不是抛错
}
它通过构造函数注入三个依赖(loadRecordingData、resolveVideoPath、loadTelemetry),自身不含任何平台代码。这正是“适配器模式”的价值:上层服务只依赖 CursorNativeAdapter 接口,未来把 macOS/Windows 原生光标采集接进来时,只需新增一个 kind: "native" 的实现并在 electron/ipc/nativeBridge.ts 的装配处替换,服务层与渲染端零改动。
此外,electron/native-bridge/cursor/recording/ 目录下的 factory.ts、windowsNativeRecordingSession.ts、macNativeCursorRecordingSession.ts 等文件,就是各平台录制会话的具体实现载体,由工厂按平台选择。
2. Main-process services(主进程服务层)
服务编排适配器,持有运行时状态,并对外暴露领域级操作。
服务层位于 electron/native-bridge/services/,当前有三个:
| 服务 | 职责 | 关键行为 |
|---|---|---|
| cursorService.ts | 光标遥测与录制数据 | 从适配器取数据后,把“最近一次遥测加载”(视频路径 + 样本数 + 时间戳)写入状态存储 |
| projectService.ts | 项目/视频上下文 | 每个变更类操作(保存、加载、设置当前视频路径等)执行完都会调用 getCurrentContext() 刷新 store 中的项目上下文 |
| systemService.ts | 平台与能力信息 | 聚合平台、光标能力、项目能力,生成 SystemCapabilities 并缓存到 store |
以 CursorService.getTelemetry 为例,可以看到服务层在“取数据”之外承担的两件事:
async getTelemetry(videoPath?: string | null): Promise<CursorTelemetryPoint[]> {
const result = await this.options.adapter.getTelemetry(videoPath);
if (!result.success) {
throw new Error(result.message || result.error || "Failed to load cursor telemetry");
}
const resolvedVideoPath = videoPath ?? this.options.store.getState().project.currentVideoPath;
if (resolvedVideoPath) {
this.options.store.markCursorTelemetryLoaded(resolvedVideoPath, result.samples.length);
}
return result.samples;
}
- 失败语义转换:适配器返回的软失败(
success: false+ 消息)被转换为异常,交由最外层统一封装成INTERNAL_ERROR错误响应; - 状态回写:成功加载后记录
lastTelemetryLoad,使主进程可以回答“当前遥测对应哪个视频、多少样本”。
3. Unified IPC transport(统一 IPC 传输层)
渲染代码只与单个
native-bridge:invoke通道通信,使用版本化契约。
这是整个架构中最“薄”的一层,由两端各半段组成:
Preload 端:electron/preload.ts 中只暴露了一个桥接方法:
contextBridge.exposeInMainWorld("electronAPI", {
invokeNativeBridge: <TData>(request: NativeBridgeRequest) => {
return ipcRenderer.invoke(NATIVE_BRIDGE_CHANNEL, request) as Promise<TData>;
},
// ... 其余为向后兼容的 legacy 方法
});
主进程端:electron/ipc/nativeBridge.ts 的 registerNativeBridgeHandlers 是唯一注册点。它做了三件体现“坚韧性”的事:
- 幂等注册:入口处先
ipcMain.removeHandler(NATIVE_BRIDGE_CHANNEL)再ipcMain.handle,重复调用不会因通道已存在而崩溃; - 入参校验:
isBridgeRequest先确认请求是带domain和action字符串的对象,否则直接返回INVALID_REQUEST错误封装; - 路由 + 兜底:按
domain(system/project/cursor)二级 switch 分发到对应服务,未识别的 domain 或 action 返回UNSUPPORTED_ACTION,任何未捕获异常统一转换为带retryable: true的INTERNAL_ERROR:
ipcMain.handle(NATIVE_BRIDGE_CHANNEL, async (_, request: unknown) => {
if (!isBridgeRequest(request)) {
return createErrorResponse(undefined, "INVALID_REQUEST", "Invalid native bridge request.");
}
// ... domain/action 二级路由 ...
} catch (error) {
return createErrorResponse(requestId, "INTERNAL_ERROR",
error instanceof Error ? error.message : "Unknown native bridge error.", true);
});
注意 NativeBridgeContext 接口(electron/ipc/nativeBridge.ts#L19-L40):处理器并不直接依赖 Electron 的具体实现,而是依赖一组注入的回调(getPlatform、saveProjectFile、loadCursorRecordingData 等)。这种依赖注入使路由逻辑与主进程的窗口管理、文件 I/O 解耦,也更便于测试。
4. Renderer client(渲染端客户端层)
React 代码应当消费
src/native/client.ts,而不是直接绑定临时的 Electron API。
src/native/client.ts 对外导出 nativeBridgeClient,按 domain 组织了三个命名空间:system、project、cursor,外加一个原始入口 rawInvoke:
nativeBridgeClient.cursor.getRecordingData?.(videoPath) // → CursorRecordingData
nativeBridgeClient.system.getCapabilities() // → SystemCapabilities
nativeBridgeClient.project.saveProjectFile(data, name) // → ProjectFileResult
两个细节值得注意:
- 请求追踪:
invokeNativeBridge会在请求未带requestId时自动生成一个(优先crypto.randomUUID(),回退为req-<时间戳>-<随机数>,见 src/native/client.ts#L15-L21),主进程在响应的meta.requestId中原样带回,可用于日志串联; - 两种消费姿势:
invokeNativeBridge返回完整的结果封装(ok判别联合),requireNativeBridgeData则在ok: false时直接抛出Error(response.error.message)。命名空间方法统一采用后者,让调用方拿到“已保证成功”的数据类型;而需要区分错误码的场景(如判断是否可重试)则使用rawInvoke拿原始封装。
客户端对 window.electronAPI.invokeNativeBridge 缺失时会抛出明确的 Native bridge unavailable 错误(src/native/client.ts#L23-L31),提示开发者 preload 未正确暴露传输,属于快速失败(fail fast)设计。
三、版本化契约:Channel、Version 与 Domain/Action 路由
契约文件 src/native/contracts.ts 被主进程与渲染进程共同引用,这是“契约版本化”能成立的前提——两端看到的是同一份 TypeScript 类型:
export const NATIVE_BRIDGE_CHANNEL = "native-bridge:invoke";
export const NATIVE_BRIDGE_VERSION = 1;
请求形态:domain + action + payload
NativeBridgeRequest 是一个判别联合(discriminated union),当前收录了三个 domain 共 13 个 action:
| domain | action | 说明 | 载荷 |
|---|---|---|---|
system |
getPlatform |
归一化后的平台(darwin/win32/linux) |
无 |
system |
getAssetBasePath |
渲染端资源基路径 | 无 |
system |
getCapabilities |
能力总览(含桥版本、平台、各域能力) | 无 |
project |
getCurrentContext |
当前项目路径 + 当前视频路径 | 无 |
project |
saveProjectFile |
保存工程文件 | projectData、suggestedName?、existingProjectPath? |
project |
loadProjectFile |
打开工程文件(可预填目录) | projectFolder? |
project |
loadCurrentProjectFile |
加载当前工程 | 无 |
project |
loadProjectFileFromPath |
按路径加载 | path |
project |
setCurrentVideoPath |
设置当前视频 | path |
project |
getCurrentVideoPath |
查询当前视频路径 | 无 |
project |
clearCurrentVideoPath |
清除当前视频路径 | 无 |
cursor |
getCapabilities |
光标能力 | 无 |
cursor |
getTelemetry |
光标遥测点序列 | videoPath? |
cursor |
getRecordingData |
完整光标录制数据(样本 + 资产) | videoPath? |
主进程对平台做了归一化处理(electron/ipc/nativeBridge.ts#L42-L48):只有 darwin 与 win32 原样保留,其余 Node 平台一律折叠为 linux,保证契约枚举封闭。
结果封装:Envelope 与稳定错误码
文档原则中的“每个响应使用一致的结果封装与稳定错误码”,对应契约中的这组类型:
export type NativeBridgeErrorCode =
| "INVALID_REQUEST" // 入参不是合法的桥请求
| "UNSUPPORTED_ACTION" // domain 存在但 action 未实现
| "NOT_FOUND"
| "UNAVAILABLE"
| "INTERNAL_ERROR"; // 服务端异常,且 retryable: true
export interface NativeBridgeMeta {
version: typeof NATIVE_BRIDGE_VERSION;
requestId: string;
timestampMs: number;
}
export type NativeBridgeResponse<TData = unknown> =
| { ok: true; data: TData; meta: NativeBridgeMeta }
| { ok: false; error: { code: NativeBridgeErrorCode; message: string; retryable: boolean }; meta: NativeBridgeMeta };
这个封装带来三个工程收益:
- 错误可分类:渲染端可以依据
code做差异化处理(例如UNAVAILABLE时切换 UI 降级方案,retryable为 true 时重试),而不必解析错误字符串; - 版本可协商:
meta.version与NATIVE_BRIDGE_VERSION一同下发,未来契约升级时可做新旧版本的兼容性判断; - 可观测性:
requestId贯穿请求-响应两端,便于在主进程日志与渲染端表现之间建立因果。
此外契约还定义了主进程 → 渲染进程的事件名(project.contextChanged、cursor.providerChanged、cursor.telemetryLoaded,见 src/native/contracts.ts#L230-L239),为“推送式”状态变更预留了通道,与“拉取式”的 invoke 请求互补。
四、状态存储:主进程内的 Single Source of Truth
文档第一条原则——“运行时原生状态活在 Electron 主进程”——由 electron/native-bridge/store.ts 中的 NativeBridgeStateStore 落实。它的状态结构按三个 domain 分片:
export interface NativeBridgeState {
system: {
platform: NativePlatform;
capabilities: SystemCapabilities | null;
};
project: ProjectContext; // currentProjectPath / currentVideoPath
cursor: {
capabilities: CursorCapabilities | null;
lastTelemetryLoad: {
videoPath: string;
sampleCount: number;
loadedAt: number;
} | null;
};
}
实现上有两个特点:
- 不可变更新:所有 setter(
setProjectContext、setSystemCapabilities、setCursorCapabilities、markCursorTelemetryLoaded)都通过展开旧对象创建新状态,避免服务层意外持有可变引用; - 共享单例:在 registerNativeBridgeHandlers 中,同一个
store实例被注入三个服务,因此project域的上下文刷新(ProjectService每次变更操作后调用getCurrentContext())能立即被cursor域感知——TelemetryCursorAdapter的resolveVideoPath正是靠这个共享状态在调用方未显式传videoPath时兜底解析出当前视频。
从源码结构看,store 目前尚未订阅任何持久化事件,它是进程内易失状态;跨会话记忆(如上次打开的工程目录)由渲染端偏好存储体系负责,两者职责分离。
五、Capability-first:先探测,再行动
第二条原则“能力优先”在契约层面体现为三级能力查询链:
cursor.getCapabilities返回CursorCapabilities(telemetry是否支持、systemAssets是否提供系统光标资产、当前provider是native还是none);system.getCapabilities将其聚合进SystemCapabilities,连同bridgeVersion、platform和project.currentContext一起下发(systemService.ts#L27-L42);- 渲染端组件据此决定 UI:例如当
provider为"none"时,编辑器不应承诺“逐像素还原系统光标”,而是使用遥测点位 + 自绘光标渲染。
当前 TelemetryCursorAdapter 声明的是 { telemetry: true, systemAssets: false, provider: "none" },意味着本仓库现阶段提供的是遥测级光标能力;而 NativeCursorAsset(含 imageDataUrl、hotspotX/Y、scaleFactor、cursorType 等字段)契约已经就绪,是为 macOS/Windows 原生光标资产(provider: "native")预留的数据结构。
六、当前落地范围与 Legacy 兼容策略
文档“Current rollout”一节列出的初始脚手架,在仓库中均可验证:
| 文档列出的组件 | 仓库中的对应文件 |
|---|---|
| 共享契约 | src/native/contracts.ts |
| 渲染端 SDK | src/native/client.ts |
| 主进程状态存储 | electron/native-bridge/store.ts |
| 光标遥测适配器 | electron/native-bridge/cursor/telemetryCursorAdapter.ts |
| 领域服务 | electron/native-bridge/services/(cursor / project / system 三个服务) |
| 统一处理器注册 | electron/ipc/nativeBridge.ts |
文档同时明确指出:legacy 的 window.electronAPI 表面仍然存在以兼容旧代码,新特性应优先使用统一桥接客户端。这一点在 electron/preload.ts 中可以直观印证——invokeNativeBridge 与大量既有方法(getCursorTelemetry、saveProjectFile、getPlatform 等零散通道)并存于同一个 electronAPI 对象上。因此实际维护时的迁移纪律是:
- 新代码:只 import
src/native/client.ts的nativeBridgeClient,不新增对ipcRenderer直接绑定的 preload 方法; - 旧代码:逐步替换为桥接调用,替换一个删一个 legacy 通道,最终让
electronAPI收敛为只剩invokeNativeBridge(与assetBaseUrl这类非领域性暴露)的极简表面。
七、设计原则小结与扩展路径
回到文档的四条原则,它们分别落到了具体机制上:
| 原则 | 实现机制 |
|---|---|
| Single source of truth | NativeBridgeStateStore 集中持有系统/项目/光标状态,服务层每次变更后回写 |
| Capability-first | cursor / system 两级 getCapabilities,渲染端先探测再行动 |
| Versioned contracts | 单一契约文件被两端共享;NATIVE_BRIDGE_VERSION + meta.version 随响应下发;请求为判别联合,扩展新 action 时编译期即可暴露两端遗漏 |
| Resilience | 统一 NativeBridgeResponse 封装、五个稳定错误码、入参校验、removeHandler 幂等注册、异常兜底为 retryable 的 INTERNAL_ERROR |
基于这套结构,向桥中新增一个 domain(例如 webcam)的路径是明确的:
- 在 src/native/contracts.ts 的
NativeBridgeRequest联合中追加该 domain 的 action 分支,并补充对应响应数据类型与错误码(必要时); - 在 electron/native-bridge/services/ 新增服务类,构造函数注入 store 与具体依赖;
- 在 electron/ipc/nativeBridge.ts 的装配函数中实例化服务,并在
domainswitch 中新增一个 case 分支(未知 action 会自动落入UNSUPPORTED_ACTION); - 在 src/native/client.ts 的
nativeBridgeClient上新增对应命名空间方法,渲染端只通过该命名空间访问。
整个过程中,IPC 通道数量保持为 1,preload 保持零改动——这正是“薄传输、厚契约”架构的最终体现:扩展性来自契约的类型系统,而不是新增传输面。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00