UniClipboardEngine 是 UniClipboard 在 macOS、Windows、Linux、iOS、Android 和 HarmonyOS 上共享的端到端加密 P2P 引擎。仓库统一维护设备身份、空间与配对、剪贴板同步、历史记录、加密搜索、文件传输、本地密文持久化、数据库迁移、跨平台绑定和发布产物。
它不是一个可以直接运行的终端应用。桌面程序和移动应用把它作为核心能力接入,并负责提供系统目录、安全存储、系统剪贴板、文件选择与生命周期通知。
The workspace version is
1.1.0-rc.23. Rust consumers depend only onuc-engine. Mobile consumers must use platform artifacts from the samev*Release. Internal crates and bindings are not published to crates.io.
- 在所有平台使用同一套设备身份、空间、配对和信任关系。
- 默认通过端到端加密的 P2P 通道同步文字、图片和文件。
- 保存加密历史,支持分页、搜索、标签、收藏、资源读取、导出和重发。
- 处理前台、后台、暂停、恢复和关闭,保持设备身份不变。
- 通过宿主安全存储保管密钥,并默认加密所有持久化业务数据。
- 为 iOS、Android 提供 UniFFI 绑定,为 HarmonyOS 提供 N-API 绑定。
- 为所有正式产物生成来源、版本、校验值、依赖许可证和设备验收记录。
| 使用方 | 接入内容 | 支持范围 |
|---|---|---|
| Rust 桌面或服务宿主 | uc-engine |
唯一稳定的 Rust 入口 |
| iOS | UniClipboardEngine.xcframework.zip 和 uc_engine_uniffi.swift |
iOS 16.4 及以上;真机 arm64;模拟器 arm64、x86_64 |
| Android | UniClipboardEngine.aar |
API 24 及以上;arm64-v8a、x86_64;Java 17 |
| HarmonyOS | UniClipboardEngine.har |
API 24 及以上;arm64-v8a |
LAN HTTP 同步位于 compatibility/,拥有独立版本和 uc-mobile-v* 发布线。它只能由用户明确选择,P2P 失败时不会自动切换到 LAN。
首次克隆使用 git clone --recurse-submodules;已有工作区在切换或更新 Engine 提交后执行:
git submodule update --init --recursivethird_party/iroh 固定整个网络栈 fork,内部使用真实路径依赖。Rust 宿主只依赖 uc-engine;
Cargo 获取 Engine Git 依赖时会获取 submodule,宿主不需要 iroh 根补丁。
GitHub 自动生成的源码压缩包不包含 submodule 内容,源码构建应使用完整 Git checkout。
平台应用
├─ 私有目录、安全存储、系统剪贴板、文件句柄
└─ 启动、前后台和退出通知
│
▼
uc-engine
├─ 空间、设备与配对
├─ 加密历史与搜索
├─ P2P 连接与传输
└─ 数据库与后台任务
应用只向 Engine 提供平台能力,不自行复制加密、持久化、配对、传输或数据迁移规则。
User content written to SQLite, disk cache, search indexes, or configuration files must use MasterKey AEAD encryption. Configuration can be stored in plaintext, but must not contain user content or credentials. The existing vault/profile-secrets-v1 exception for runtime key material is described in the security rules.
The current security rules allow these plaintext exceptions:
- 内容类型分类,例如
text、image、file; - 文件内容本体,可以按原始字节写入 Engine 管理的 blob store 或导入目录。
- The sanitized original name of an inbound file, only as the actual basename in the managed file cache.
The search index currently writes file extensions derived from file names and paths in plaintext. This violates the security rule above. The MIME and source-device columns also require review. See the known implementation gap.
安全存储中的密钥材料由宿主平台保护。日志和公开错误不得包含剪贴板内容、密码、密钥、完整令牌、文件名或文件路径。详细规则见 密文持久化规则 和 安全说明。
| 路径 | 用途 |
|---|---|
crates/uc-engine/ |
唯一稳定入口,负责启动、操作、事件和生命周期 |
crates/uc-core/ |
领域模型和平台能力约定 |
crates/uc-application/ |
业务流程编排 |
crates/uc-infra-*/ |
数据库、加密、文件和 P2P 实现(7 个能力 crate,见 ARCHITECTURE.md) |
bindings/uc-engine-uniffi/ |
iOS 与 Android 绑定及打包脚本 |
bindings/uc-ohos-napi/ |
HarmonyOS N-API 绑定与 ArkTS 声明 |
compatibility/ |
独立版本的 LAN 兼容线 |
tests/hosts/ |
iOS、Android、HarmonyOS 验收宿主,不承载产品功能 |
docs/ |
架构、安全和迁移记录 |
scripts/architecture/ |
仓库归属、依赖方向和发布来源检查 |
scripts/release/ |
发布产物归集、清单生成和发布前核验 |
基础开发环境:
- Git;
- Rust 1.95.0。仓库的
rust-toolchain.toml会自动选择版本; - Node.js,用于运行仓库和发布检查脚本。
构建移动产物还需要:
- iOS:macOS、Xcode,以及
aarch64-apple-ios、aarch64-apple-ios-sim、x86_64-apple-iosRust 目标; - Android:Android SDK/NDK、JDK 17、
cargo-ndk,以及aarch64-linux-android、x86_64-linux-androidRust 目标; - HarmonyOS:DevEco Studio 和
aarch64-unknown-linux-ohosRust 目标。
安装 Rust 目标和 Android 构建工具:
rustup target add \
aarch64-apple-ios \
aarch64-apple-ios-sim \
x86_64-apple-ios \
aarch64-linux-android \
x86_64-linux-android \
aarch64-unknown-linux-ohos
just cargo install cargo-ndk --locked只开发 Rust 核心时不需要安装全部移动工具链。
所有 Rust 命令都从仓库根目录运行:
just cargo metadata --locked --format-version 1
just cargo check --workspace --all-targets --locked
just cargo test --workspace --locked
just cargo fmt --all -- --check
node scripts/architecture/check-engine-repository.mjs
git diff --check仓库检查会验证目录归属、依赖方向、唯一公开入口、绑定版本、产物来源、密文持久化规则和 LAN 隔离。
新建 worktree 后,使用 just 调用仓库固定版本的 mbx, 复用其他 worktree 已编译的结果:
just cargo check --workspace --all-targets --locked
just cargo nextest run --profile ci --locked --workspace --all-targets --no-run
bash scripts/testing/run-test-group.sh evidence
just mbx-tool cache stats说明:
- 首次运行会下载固定版本的 mbx 并校验 sha256。
- 缓存位置由环境变量
MBX_CACHE_DIR决定,未设置时使用 mbx 的平台默认位置。 - 仓库 Rust 构建、测试、E2E 与 CI 统一使用 MBX;本地默认 4 个编译任务,环境值与
-j可覆盖。 - 当前 shell 直接运行 Cargo 前执行
source scripts/build-cache/env.sh;不修改全局配置。 - 使用 R2 分布式缓存时执行
just mbx --r2 <Cargo 参数>,本地 worker 与 PR 只读、可信受保护 main CI 写入;凭据与验证条件见本地构建指南。
统一入口、信任与命中边界见 ADR-033。 多 worktree 的输出隔离、并行度和复跑基准见 本地构建指南。
无论接入哪个平台,都按以下顺序进行:
- 选择一个完整的
v*GitHub Release,不使用可变分支上的临时产物。 - 下载目标平台文件、
release-manifest.json、version.txt和source-commit.txt。 - 根据
release-manifest.json校验每个文件的大小和 SHA-256。 - 确认平台产物、生成绑定、版本文件和源码提交来自同一个 Release。
- 实现平台宿主能力,启动 Engine,并立即开始消费事件。
- 先尝试恢复已有会话;无法恢复时再进入创建空间或加入空间流程。
- 把应用前后台切换和退出映射到 Engine 生命周期。
- 在目标平台上完成创建或恢复空间、发送、接收、暂停、恢复和关闭验收。
以下命令以 iOS 产物为例:
asset=UniClipboardEngine.xcframework.zip
expected="$(jq -r --arg name "$asset" \
'.artifacts[] | select(.name == $name) | .sha256' \
release-manifest.json)"
actual="$(shasum -a 256 "$asset" | awk '{print $1}')"
test -n "$expected" && test "$actual" = "$expected"还应检查 release-manifest.json 中的版本、源码提交、最低系统版本和设备验收状态。skipped 只代表未执行,不能视为通过。
| 能力 | 要求 |
|---|---|
| 私有数据目录 | 保存 Engine 管理的数据库、密文和身份;不得由其他应用访问 |
| 缓存目录 | 供 Engine 管理缓存;业务负载仍按敏感数据处理 |
| 临时目录 | 供短期导入和传输使用;不能自行保存明文业务数据 |
| 安全存储 | 以二进制形式读取、写入和删除密钥材料 |
| 系统剪贴板 | 读取和写入多种表示;平台允许时提供变化通知 |
| 文件句柄 | 把平台 URI 或文件描述符登记为不透明句柄,支持元数据和分块读写 |
文件句柄不能直接使用本机路径充当标识。发送或导出结束后,宿主应释放自己登记的句柄。
正常顺序如下:
start -> recover/create/join -> operations
-> suspend -> resume -> operations
-> shutdown
start:打开加密存储,恢复设备身份,启动 P2P 节点和后台任务。quiesce:停止接受新操作,等待已开始的操作结束;只在 Rust 入口公开。suspend:取消剩余工作并释放当前连接;引擎实例和事件通道保留。resume:使用原数据和设备身份重建连接;不会自动重试暂停前取消的操作。shutdown:停止全部工作并关闭事件通道。
移动应用进入后台时调用 suspend,回到前台时调用 resume。进程被系统结束后重新调用 start,不要尝试恢复旧的内存实例。
事件必须持续消费。收到 RefreshRequired 时重新查询当前状态,不要根据旧事件猜测状态。空目标设备列表表示发送给所有符合条件的可信设备;非空列表只缩小目标范围。
uc-engine 不发布到 crates.io。使用方应固定到已经验证的不可变提交:
[dependencies]
uc-engine = {
git = "https://github.com/UniClipboard/core.git",
rev = "<40-character-commit-sha>"
}
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }不要直接依赖 uc-core、uc-application 或任何 uc-infra-* crate。只有明确接入 LAN 兼容通道时才启用 uc-engine/lan-compat,正式产品不得启用 dev-tools。
下面省略了四个平台适配器的具体实现,但展示了稳定入口的完整顺序:
use std::{path::PathBuf, time::Duration};
use uc_engine::{
Engine, EngineConfig, EngineEvent, HostCapabilities, HostDirectories,
Operation, RecoverSessionInput,
};
// MySecureStorage、MyClipboard 和 MyFileAccess 由宿主实现。
let host = HostCapabilities::new(
HostDirectories::new(
PathBuf::from("<private-data-directory>"),
PathBuf::from("<cache-directory>"),
PathBuf::from("<temporary-directory>"),
),
Box::new(MySecureStorage::new()),
Box::new(MyClipboard::new()),
Box::new(MyFileAccess::new()),
);
let config = EngineConfig::new("<host-app-version>")
.with_profile_id("default");
let (engine, mut events) = Engine::start(config, host).await?;
let event_task = tokio::spawn(async move {
while let Some(event) = events.next().await {
match event {
EngineEvent::RefreshRequired { .. } => refresh_current_state().await,
other => handle_engine_event(other).await,
}
}
});
let recovery = engine
.execute(Operation::RecoverSession(RecoverSessionInput {
allow_secure_storage_unlock: true,
}))
.await?;
handle_recovery(recovery).await?;
engine.shutdown(Duration::from_secs(2)).await?;
event_task.await?;宿主适配器分别实现 HostSecureStorage、HostClipboard 和 HostFileAccess。所有业务动作都通过 Engine::execute(Operation) 发起,结果只使用 crate 根导出的稳定类型。完整操作和事件说明见 uc-engine 跨平台核心接口。
UniClipboardEngine.xcframework.zip;uc_engine_uniffi.swift;UniClipboardEngine.xcframework.checksum.txt;release-manifest.json、version.txt、source-commit.txt。
- 解压
UniClipboardEngine.xcframework.zip。 - 把
UniClipboardEngine.xcframework加到应用 target,作为静态库链接并选择Do Not Embed。 - 把
uc_engine_uniffi.swift加到同一 target,并确认 Target Membership 已启用。 - 将最低系统版本设为 iOS 16.4 或更高。
- 链接
Security和SystemConfiguration。宿主若实现系统剪贴板和文件类型处理,还需要UIKit和UniformTypeIdentifiers。 - 不要同时编译另一个版本的
uc_engine_uniffi.swift。
也可以在应用仓库中用 CocoaPods 或 Swift Package 封装这两个文件,但封装必须固定同一个 Release,且构建前再次执行校验。
生成代码提供 BindingHost。iOS 宿主通常使用 Application Support、Caches 和 Temporary 目录,使用 Keychain 保存安全数据,并把 UIPasteboard、文件选择器 URL 和安全作用域访问转换成绑定需要的形式。
let host = AppleEngineHost() // 应用实现 BindingHost
let engine = try MobileEngine.start(
config: BindingConfig(
appVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown",
profileId: "default"
),
host: host
)
let recovery = try engine.recoverSession(allowSecureStorageUnlock: true)
if !recovery.unlocked {
// 展示创建空间或加入空间流程
}应用进入后台时调用 try engine.suspend(),回到前台时调用 try engine.resume(),结束时调用 try engine.shutdown(deadlineMs: 2_000)。用独立任务轮询 nextEvent(timeoutMs:),不要阻塞主线程。
主应用、分享扩展和键盘扩展不能共享同一个内存实例。扩展需要自己的短生命周期实例,并通过 App Group 目录和 Keychain Access Group 访问同一份受保护状态;同一时刻应只有一个进程持有 P2P 运行会话。
UniClipboardEngine.aar;UniClipboardEngine.aar.checksum.txt;UniClipboardEngine.pom和runtime-dependencies.txt;release-manifest.json、version.txt、source-commit.txt。
uc_engine_uniffi.kt 已编译进 AAR。Release 中的独立源码用于审计和自定义封装,不要再加入同一个应用 target,否则会产生重复类。
把 AAR 放入应用的 libs/,然后添加:
android {
defaultConfig {
minSdk 24
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
implementation files('libs/UniClipboardEngine.aar')
implementation 'net.java.dev.jna:jna:5.14.0@aar'
implementation 'org.jetbrains.kotlin:kotlin-stdlib:2.1.20'
}如果把 AAR 和随附 POM 放入内部 Maven 仓库,可以改用坐标 app.uniclipboard:uniclipboard-engine:<version>,让运行依赖由 POM 传递。
当前原生入口要求在首次启动 Engine 前安装 application context。桥接类的包名、类名和方法名必须与下面保持一致:
package expo.modules.ucengine
import android.content.Context
class UcEngineModule {
companion object {
init {
System.loadLibrary("uc_engine_uniffi")
}
@JvmStatic
private external fun nativeInstallAndroidContext(context: Context): Boolean
fun installAndroidContext(context: Context) {
check(nativeInstallAndroidContext(context.applicationContext))
}
}
}安装成功后,使用生成的 Kotlin 类型实现 BindingHost,再启动 Engine:
val engine = MobileEngine.start(
BindingConfig(BuildConfig.VERSION_NAME, "default"),
AndroidEngineHost(applicationContext)
)
val recovery = engine.recoverSession(allowSecureStorageUnlock = true)
if (!recovery.unlocked) {
// 展示创建空间或加入空间流程
}推荐使用 Android Keystore 保护安全存储值,使用应用私有目录和缓存目录,并把 content:// URI 登记成不透明文件句柄。前后台切换分别调用 suspend() 和 resume();服务结束时调用 shutdown(2_000u) 并关闭绑定对象。
Android 对后台运行和剪贴板访问有限制。应用应在有合法前台运行窗口时操作 Engine,不能把系统限制解释成切换到 LAN 的理由。
UniClipboardEngine.har;UniClipboardEngine.har.checksum.txt;release-manifest.json、version.txt、source-commit.txt。
HAR 已包含 arm64-v8a 的 libuc_ohos_napi.so 和 ArkTS 声明。独立的 libuc_ohos_napi.so、index.d.ts 与验收 HAP 主要用于审计、调试和发布验收。
把 HAR 放入模块的 libs/,在 oh-package.json5 中添加本地依赖:
{
dependencies: {
'@uniclipboard/engine': 'file:./libs/UniClipboardEngine.har',
},
}然后安装依赖并在 ArkTS 中导入:
ohpm install --allimport engine from '@uniclipboard/engine'
const host = createEngineHost(context) // 应用实现 OhHost
const preparedHost = engine.prepareHost(host)
const active = await engine.startEngine(
{ appVersion: '1.0.0', profileId: 'default' },
preparedHost
)
const recovery = await active.recoverSession(true)
if (!recovery.unlocked) {
// 展示创建空间或加入空间流程
}OhHost 需要提供三个目录、安全存储、剪贴板和文件句柄方法。由于 ArkTS 数值不能无损表达全部 64 位整数,文件偏移和大小按声明中的字符串传递。应用进入后台时调用 suspend(),回到前台时调用 resume(),退出时调用 shutdown(2000)。
可运行的完整宿主参考位于 tests/hosts/ohos/entry/src/main/ets/host/。
- 启动 Engine。
- 在安全存储可访问时调用
RecoverSession或recoverSession(true)。 - 如果会话已恢复,查询本机设备和当前空间状态。
- 如果没有空间,让用户选择创建空间或输入邀请加入空间。
- 开始消费事件,并按需捕获或发送内容。
当应用处于扩展、设备锁定或其他不允许访问安全存储的环境时,应传入 false,不能强行读取系统安全存储。
已在空间中的设备签发一次邀请,新设备使用邀请码加入。邀请只能使用一次,也可以在被使用前取消。配对结果由操作返回;状态变化后应用重新查询 Engine 的完整设备关系,不应自行维护另一套成员状态。规格 023记录了已落地的严格边界:双方保存并确认同一成员历史和安全状态后才成功,等待流程由 Engine 自动恢复;当前准入 wire/runtime 以规格 028 为准。
- 文字和图片必须为 1 到 64 KiB;更大内容通过文件入口发送。
- 空目标列表表示发送给所有符合条件的可信设备。
- 文件发送先由宿主登记输入句柄,Engine 分块读取;完成后宿主释放句柄。
- 发送结果区分接受、重复、离线、失败和仍在等待,不能把等待中的设备提前记为失败。
宿主可以把系统剪贴板变化通知交给 Engine,也可以显式调用捕获操作。Engine 统一处理解锁检查、回写判断、去重、加密历史、搜索更新和发送,宿主不要重复这些步骤。
宿主先让用户选择目标位置并登记可写句柄,再把条目编号和句柄传给 Engine。Engine 只看到句柄,不读取或持久化用户选择的目标路径;写完后调用宿主的完成写入方法。
两端脚本默认生成正式产物。设置 UC_ENGINE_UNIFFI_BUILD_PROFILE=dev 可生成本地调试产物;只接受 dev 或 release。还可以把另一个 Engine 源码目录作为脚本的第一个参数,使用当前打包工具构建该目录中的固定版本。正式发布只接受标记为 release 的 iOS 与 Android 产物。
正式产物中的源码路径与检出位置无关:工作区代码本来就是相对路径;CARGO_HOME 下的依赖源码记为 /cargo-home/...,本机 rust-src 中的标准库记为 /rustc/<编译器提交>/...,与未安装 rust-src 的工具链一致。脚本通过 --remap-path-prefix 追加这两项,并保留仓库配置与环境变量中已有的 rustflags;依赖中的 C 与汇编源码(如 ring 在 Apple 目标上带调试信息编译的对象)通过追加到 CFLAGS 的 -ffile-prefix-map 做同样映射,CARGO_HOME 或其 registry、git 为符号链接时也映射链接目标。原生库仍含源码目录、编译目录、CARGO_HOME 或 Rust 工具链路径时构建直接失败。iOS 打包另外固定了会随运行变化的元数据:重新归档静态库时成员时间写零,XCFramework 清单按库标识排序切片,zip 中文件时间统一为源码提交时间(UTC)并去除扩展属性条目。因此在相同源码、锁文件与工具链(Rust、NDK、Xcode 与本机 C 编译器、JDK 与 Gradle)下,不同目录构建的 AAR 与 XCFramework 字节一致。panic 位置与 tracing 事件中的依赖路径同样显示为上述固定前缀,本地排查时对应回自己的 CARGO_HOME 与工具链目录。dev 产物不做重映射,保留本机路径供调试器定位源码。
UC_ENGINE_UNIFFI_BUILD_LOCKED=1 \
bindings/uc-engine-uniffi/scripts/build-ios-xcframework.sh默认输出位于 target/uc-engine-uniffi-dist/ios/。脚本默认产出同时含真机与模拟器切片的 XCFramework;设置 UC_ENGINE_UNIFFI_SLICE=device 或 UC_ENGINE_UNIFFI_SLICE=simulator 时只构建对应切片。
UC_ENGINE_UNIFFI_BUILD_LOCKED=1 \
bindings/uc-engine-uniffi/scripts/build-android-aar.sh默认输出位于 target/uc-engine-uniffi-dist/android/。
bindings/uc-engine-uniffi/scripts/prepare-local-artifacts.mjs按设备与模拟器拆分构建 iOS XCFramework,并构建 Android AAR,归集到 .artifacts/local/{ios,ios-sim,android}/,同时生成就绪清单 .artifacts/local/local-prepared.json(平台、版本、提交与主产物 sha256)。该目录不纳入版本控制;移动端本地开发可按运行目标切换使用。
tests/hosts/ohos/build-emulator.sh脚本默认从 /Applications/DevEco-Studio.app/Contents 查找 DevEco Studio。自定义安装位置时设置 DEVECO_STUDIO_CONTENTS。默认发布目录位于临时构建目录,可通过 UC_OHOS_DIST_DIR 指定。
验收宿主只用于证明绑定和平台能力能一起工作,不应被复制成产品业务层。
tests/hosts/ios/build-device.sh <device-id>需要可用的 Xcode 签名身份和真机。未运行真机流程时必须记录为“跳过”。
tests/hosts/android/install-emulator.sh
tests/hosts/android/probe-command.sh '<json-command>'需要正在运行且可通过 adb 访问的 arm64 模拟器。
tests/hosts/ohos/build-emulator.sh该命令同时生成 HAR 和已签名验收 HAP。签名脚本还需要 jq。
正式 Engine 使用 v* 标签,由 .github/workflows/release-engine.yml 从同一个提交构建三端产物。LAN 兼容线使用独立的 uc-mobile-v* 标签和 .github/workflows/release-lan-compat.yml。
正式 Release 包含:
| 类别 | 主要文件 |
|---|---|
| iOS | XCFramework、Swift 绑定、校验值 |
| Android | AAR、Kotlin 绑定、POM、运行依赖、校验值 |
| HarmonyOS | HAR、动态库、ArkTS 声明、验收 HAP、校验值 |
| 通用 | 源码归档、Cargo.lock、许可证、依赖许可证、调试资料 |
| 追溯 | version.txt、source-commit.txt、release-manifest.json |
发布包归集后运行:
node scripts/release/verify-release-bundle.mjs <release-assets-directory>核验通过前不得发布。已经上传的资产不可覆盖;发现问题时发布新版本,并保留旧版本的可追溯记录。详细规则见 发布完整性。
- 选择新的完整
v*Release,并阅读变更说明。 - 同时更新原生库、生成绑定、版本和源码提交记录。
- 重新核对
release-manifest.json,不要沿用旧版本的校验值。 - 检查数据库迁移、最低系统版本和设备验收状态。
- 分别测试已有空间恢复、设备身份保持、发送与接收、文件导出、暂停与恢复。
- iOS 扩展和 Android 后台服务也要使用同一版本,不能只升级主应用。
不要长期保留新旧两套 Engine,也不要在产品仓维护核心补丁副本。核心问题应在本仓修复并通过新 Release 交付。
本项目使用 Apache License 2.0。