uc-engine 是桌面与移动宿主使用完整 P2P 核心的唯一稳定入口。宿主只负责提供私有目录、安全存储、系统剪贴板和文件句柄;数据库、加密存储、搜索、设备身份、连接、传输和后台任务均由核心拥有。
crate 根只保留稳定名称的统一导出,内部按职责分为七层:
| 目录 | 唯一职责 |
|---|---|
contract/ |
配置、宿主能力、操作、结果、事件、脱敏观测约定和稳定错误 |
engine/ |
生命周期、事件流与在途操作管理 |
runtime/ |
生产会话与生命周期资源、操作路由、宿主剪贴板、文件处理和移动上传 |
operations/ |
按空间、剪贴板、历史、设备和设置划分的业务动作 |
subsystems/ |
不关心具体适配器的长期任务和协调逻辑 |
assembly/ |
宿主适配、数据库、网络、加密、搜索、剪贴板和传输组装 |
compatibility/mobile_lan/ |
与完整 P2P 主路径隔离的 LAN 兼容能力 |
开发与验收操作单独位于 dev/,且只在显式启用 dev-tools feature 时编译;正式宿主和发布产物不得启用它。内部宿主契约检查位于 testing/。runtime/mod.rs 只拥有生产会话的建立、后台任务挂接和生命周期资源,具体路由、宿主剪贴板、文件操作与移动上传各自由独立内部模块拥有。uc-infra-* 具体类型只允许出现在 assembly/;业务操作和生产路由只能接收已经组装好的能力。完整导航见 crates/uc-engine/README.md。
外部 crate 只能使用 crate 根导出的稳定名称和 error_codes,不得依赖内部模块路径或源码文件位置。
宿主调用 Engine::start(config, host),成功后同时得到核心实例和一条持续有效的事件流。启动会在当前资料
空间存在时打开其加密存储、恢复同一设备身份、启动完整 P2P 节点并启动核心后台任务;Fresh profile 不会
为了满足接口而伪造一个活动 Space。
需要在启动完成前展示资料升级时,使用兼容的 Engine::start_with_progress 入口,先创建只读进度通道。
快照、失败后的最终结果、计数单位及宿主责任见启动资料升级进度。该能力独立于下文正常运行事件流。
需要在启动期间接受退后台通知时,先调用 StartupLifecycle::channel(),将不可克隆的输入与进度输入交给
Engine::start_with_lifecycle(config, host, progress, lifecycle)。可克隆的控制句柄只提供完整 suspend()、
suspend_with_deadline(deadline) 和 resume(),启动前后复用同一宿主通知队列;只读进度不接收命令。
构造完成后先处理已接收通知再交出实例,因此实例可能以 Suspended 或 Quiesced 状态交出,Ready 不代表正在运行。
期限包括启动等待;超时不撤销通知,也不代表资源已安全释放。启动耗尽原期限后可能仍处于 Quiesced,宿主必须继续
请求完整暂停并确认成功。启动失败或未使用输入被丢弃会结束等待,重试创建新输入;最终关闭后旧控制句柄拒绝恢复。
移动宿主使用 MobileStartupLifecycle 的方式相同:先创建一次性对象,再交给带 lifecycle 的构造入口;如果同时启用
产品分析,使用带 analytics 和 lifecycle 的组合入口。系统回调可以在构造调用尚未返回时从其他线程通知,构造返回后
同一对象仍可继续使用。每次重试必须创建新对象,不能把一次启动的控制对象交给第二次启动。
事件流采用有限容量。消费者落后时不会收到伪造或不完整的数据,而是收到 RefreshRequired(ConsumerLagged),随后应重新查询当前状态。
核心事件包括:
| 事件 | 含义 |
|---|---|
StateChanged |
生命周期状态已经改变 |
IncomingEntry |
收到一条具有完整摘要的新内容 |
OutboundProgressChanged |
发送侧完整进度已变化,宿主重新读取完整视图 |
TransferProgress |
文件传输进度发生变化 |
DeviceTrustChanged { revision } |
正式设备组状态已经变化;宿主重新调用 QueryDeviceGroupChoices 读取完整事实与待处理选择 |
WorkspaceConvergenceChanged |
仅 dev-tools 的内部收敛诊断事件;不进入正式宿主和发布产物 |
NetworkRecoveryChanged |
网络会话恢复开始、等待下一次尝试、成功或最终失败的稳定状态变化 |
ProfileRecoveryChanged |
资料密钥恢复状态发生变化;宿主仍可通过查询重新取得完整快照 |
SettingsChanged { sections } |
一次成功保存使持久化设置发生变化,只列出变化的分区(general、sync、retention_policy、security、pairing、file_sync、network、mobile_sync),不携带设置值;宿主重新调用 QuerySettings。所有写入路径共享同一个通知出口,内容没有变化的保存和被拒绝的更新不产生事件。Engine 设置只含 Engine 拥有的节点行为与本机设备名;宿主产品外壳偏好(启动方式、自动启动、更新、主题、语言、产品分析许可、快捷键、快捷面板)由宿主产品自行保存,不在设置契约和本事件中。旧 settings.json 中残留的这些键在加载时被忽略,即使取值非法也不影响启动,下一次保存时不再写出 |
RePairingRequired { scope } |
旧资料独立化完成,需要产品提示重新配对;all_devices 表示全部旧设备关系均须重新建立 |
RefreshRequired |
宿主必须重新查询当前状态 |
OperationFinished |
一次操作进入成功、失败或取消终态 |
Fatal |
核心遇到不可恢复错误 |
当底层变化事件不包含完整条目摘要时,核心只发送 RefreshRequired(StateInvalidated),不得猜测内容类型、时间或预览。
要求重新查询的事件(DeviceTrustChanged、RefreshRequired,包括消费者积压产生的 RefreshRequired(ConsumerLagged))只在对应状态可查询时送达。Space 切换从拆除旧会话到替换会话安装完成之间,核心暂存这类事件,只保留最大 revision 与各类刷新原因各一次,安装完成后补发;替换会话安装失败时继续暂存,直到后续安装成功。其余事件照常发送。宿主因此不会在收到通知后立即查询却得到切换窗口内的不可用错误。
旧资料独立化完成后,核心发送 RePairingRequired { scope: AllDevices }。产品收到后立即展示完整重新配对引导;若启动时错过事件,则通过 QuerySetupState.re_pairing_required 恢复同一提示。该值为 true 表示仍须重新配对,为 false 表示无需重新配对;成功创建或加入新空间后由 Engine 清除。仅关闭提示不能清除该值。产品不得从设备列表自行推断范围,也不负责清理旧关系。
移动绑定保留默认关闭产品分析的启动方式,并提供一个需要宿主明确传入产品分析能力的启动方式。启用后,核心统一产生脱敏事件并安排匿名身份、空间成员身份和分组身份的切换;Swift 或 Kotlin 宿主只实现一个接收入口,不需要了解核心内部流程。
宿主负责用户许可、第三方服务接入、发送队列和分析身份的持久保存。事件接收应快速完成,网络发送和重试不能阻塞核心。发送失败不会改变业务结果;身份保存失败会作为明确失败返回。绑定传出的属性是核心生成的 JSON 对象,只能用于供应商转发,不得混入剪贴板内容、设备名、文件名、路径、密码、密钥或令牌。产品分析路径不依赖 PostHog、Sentry 或运行诊断发送器;运行诊断使用下面独立的进程级合同。
直接 Rust 宿主通过 uc_engine::observability 使用稳定入口;iOS、Android 和 HarmonyOS 绑定提供对等入口。宿主必须在创建第一个
Engine 前构造 ObservabilityResource 和 ObservabilityConfig,再调用 ProcessObservabilityRuntime::install。相同配置重复安装
复用同一进程运行时,不同配置明确失败;远程诊断许可、Collector 地址和认证只归宿主,不属于 Engine 设置。
Rust 宿主需要实现产品分析能力或识别受管诊断文件时,通过 uc_engine::observability::analytics 和
uc_engine::observability::diagnostics 使用完整合同;不得直接依赖 Engine 内部的 uc-observability-contract 包。
绑定与宿主写日志时使用同一入口再导出的 uc_trace!、uc_debug!、uc_info!、uc_warn!、uc_error! 与 log_id、log_vocab、log_vocab_debug(见运行期观测),它们只是日志写入口,不暴露 Application 或 Core 的内部阶段、状态或标识。
需要保留宿主自身日志层的 Rust 宿主,可在首次安装时调用 ProcessObservabilityRuntime::install_with_host_layers,
传入标准 HostLogLayer。共同运行时负责分组过滤:核心诊断及底层原始网络输出不会绕行到宿主层。
不允许在已有安装上追加或替换宿主层;相同配置的普通安装仍可复用。这个入口仅负责进程日志组合,
不暴露配对、成员或存储内部步骤,也不替宿主授予远程诊断许可。
ProcessObservabilityHandle::health 返回当前本地和远程状态、丢弃数量及失败批次;初始安装结果不能代替运行后的健康查询。
移动暂停和单个 Engine 关闭只调用有界 force_flush,恢复继续复用同一运行时。只有宿主确认进程最终退出时才调用 shutdown;
完成后不得在同一进程复活。刷新、关闭或远程发送失败不改变业务结果,已完成的关闭失败也不能被重复调用掩盖。
本地受管日志固定为 7 天且总量不超过 100,000,000 bytes,可通过 managed_log_files 与现有诊断导出读取。远程只连接宿主提供的
Collector;客户端不直接依赖 PostHog。所有输出默认拒绝普通模块记录,只接受固定字段,不能包含内容、身份、地址、路径、凭据或
错误正文。
合法顺序如下:
Running -> Quiescing -> Quiesced -> Suspended -> Running
Running|Quiescing|Quiesced|Suspended -> ShuttingDown -> Stopped
quiesce(deadline):停止接收新操作,并等待正在执行的操作结束。期限到达后取消剩余操作。suspend():先停止接收操作,等待已取消调用实际退出,再释放节点、会话级后台任务、连接与 profile 安全租约,随后关闭 profile 与 control 数据库的全部 SQLite 连接(仍被借出的连接归还前不关闭、不报告成功);同时暂停进程级 Clipboard 磁盘工作,清除安全缓存并禁止后台重建。若这是进程内最后一个工作中的 Engine,还在发布前让进程网络日志停车(关闭分段、交还lease.lock)。旧操作仍未退出时返回失败,不发布暂停成功。事件流与核心实例保持不变。- 暂停成功(状态读到
Suspended)的含义:本进程不再持有 Engine 自有的文件锁或 SQLite 连接,包括数据库主文件、WAL 与共享内存、内容密钥租约、iroh blob 存储和网络日志运行租约。发布前 Engine 在模块日志写一条核对记录,只含宿主目录内仍打开的数据库文件数open_database_files与租约文件数open_lease_files(预期为 0;不为 0 时为 WARN;无法枚举描述符的平台不写计数)。宿主自有的文件不在此列,例如移动端的运行期所有权锁与宿主自己打开的数据库,由宿主负责。 - 期限:带期限的暂停只结束宿主等待;已接受的暂停在在途工作退出后继续完成上述交接并发布
Suspended,宿主可查询状态确认,无需再次请求。启动后不久暂停时,节点关闭会等待 iroh 地址探测连接排空,实测约 3–5 秒,期间上述文件仍被持有(见后台暂停安全计划)。 resume():先重新打开数据库连接与网络日志,再重新取得 profile 安全租约,使用原持久数据和原设备身份重建节点与后台任务;文件仍被其他使用者占用时保持暂停,允许之后重试。暂停期间其他进程对同一资料的写入在恢复后可读。不会自动重试暂停前被取消的操作。
以上不构成平台总停止时间保证。子运行期的停止预算尚未统一,平台绑定等待超时也不等于底层收尾完成;
完整移动后台安全验收见 后台暂停安全计划。
shutdown(deadline):停止操作、节点和全部后台任务,成功后发布Stopped并关闭事件流。 收尾返回失败或等待超时则保持ShuttingDown和事件流,不报告已停止;再次调用同一入口可继续收尾, 期间不能恢复或执行操作。进入ShuttingDown后,任务独立持有运行期直至实际收尾结束,等待超时或丢弃 Engine 不取消该任务;再次关闭等待当前收尾,已成功关闭则直接成功。排队等待计入本次调用期限,不重置正在执行的预算。 接受关闭后立即进入ShuttingDown,操作排空也由该任务持有;宽限结束后取消剩余操作,确认其实际退出后才关闭运行期。 会话操作超过期限仍未退出时保留旧会话及其依赖,不能把“已通知取消”当作“已经退出”。进程级受管任务超过优雅期限后会被强制结束并等待其实际析构;这类超时结果表示未优雅结束,不表示仍有受管任务继续占用本地资源。 无法表示的极大期限返回InvalidInput,不改变运行状态。实际退出可能超过宿主等待期限,不提前报告停止成功。- 进程被系统结束后,宿主重新调用
start;不得尝试恢复旧内存实例。
除 Running 外的状态均拒绝新操作。生命周期方法和 execute 可从不同线程调用,核心内部负责串行化状态转换。
| 操作 | 当前行为 |
|---|---|
CreateSpace |
创建空间、设备身份和加密存储 |
UnlockSpace |
使用口令恢复当前空间会话 |
QueryProfileRecovery |
查询是否等待口令、正在恢复、已经恢复、只能部分恢复、恢复失败或需要准入资料恢复,以及后台是否真正可用 |
RecoverSession |
按宿主策略从系统安全存储恢复加密与空间会话 |
ChangeEncryptionPassphrase |
当前设备列表只显示有效本机时,把加密口令修改为用户输入并再次确认的新口令,同时撤销此前签发的邀请 |
JoinSpace |
接受完整长邀请或可手输短码,发起或继续同一次空间加入,返回 Active、Pending 或 Rejected,并携带稳定 join_id |
CancelJoinSpace |
请求取消指定的本机加入;只与发起方正式提交点竞争 |
IssueInvitation |
签发一次配对邀请,同时返回指向同一邀请身份的短码与完整长邀请 |
CancelInvitation |
取消当前尚未兑换的配对邀请 |
ResetSpace |
保留本机资料、设置、身份和解锁能力,废弃全部旧设备关系并建立只含本机的新空间 |
FactoryResetSpace |
停止旧运行入口后依次清除密钥材料、空间状态和邀请,并在同一 Engine 内重建全新运行期,使设备可立即重新初始化 |
QuerySetupState |
查询设置是否完成、当前邀请和已保存设备名 |
QueryStorageStats |
查询数据库、密钥库、缓存和日志占用大小,不返回本机目录 |
ClearStorageCache |
清理核心缓存并返回实际释放的字节数 |
QueryLocalDevice |
返回本机设备编号和按设置解析后的显示名 |
RecoverNetwork |
请求当前网络会话恢复;与自动恢复共享同一轮结果,不重复启动 |
QueryNetworkRecoveryStatus |
查询恢复阶段、最近失败是否可重试及下次重试剩余毫秒数;没有等待中的重试时 next_retry_in_ms 为空 |
ListMobileDevices |
列出用户显式启用的 LAN 兼容通道所登记的移动设备 |
RevokeMobileDevice |
撤销一台 LAN 兼容设备的访问凭据 |
AuthenticateMobileRequest |
校验一次 LAN 兼容请求并返回脱敏凭据凭证 |
RevalidateMobileCredential |
复查长连接使用的凭据凭证是否仍然有效 |
QueryMobileSyncSettings |
查询 LAN 兼容通道的持久设置、监听状态和可用安装方式 |
UpdateMobileSyncSettings |
校验并保存 LAN 兼容通道设置,返回最终目标状态和是否发生变化 |
UpdateMobileLanEndpoint |
由桌面外壳报告 LAN listener 已停止、正在监听或绑定失败 |
RegisterMobileDevice |
登记一台 LAN 兼容设备并返回一次性连接凭据和二维码内容 |
UpdateMobileDevice |
修改 LAN 兼容设备的标签、用户名或密码,换密时只返回一次新密码 |
CheckMobileContentAvailable |
按稳定内容编号确认本机是否仍持有可用内容 |
QueryLatestMobileSyncDocument |
返回最新内容的 LAN 兼容文档;没有内容时返回空结果 |
ApplyMobileSyncDocument |
把 LAN 兼容文档交给统一入站、剪贴板、历史和转发流程 |
ReadMobileSyncFile |
按最新文档中的附件名读取文件或图片字节和媒体类型 |
BeginMobileFileUpload |
开始一次分块文件上传并返回不透明上传编号 |
AppendMobileFileUpload |
向同一上传顺序追加一个字节块 |
FinishMobileFileUpload |
完成临时文件并挂入等待对应文档的入站缓冲区 |
AbortMobileFileUpload |
放弃未完成上传并释放临时资源;重复放弃返回未找到活动上传 |
QueryEncryptionState |
查询当前空间是否已初始化、加密会话是否可用 |
LockEncryption |
清除当前加密会话并关闭接收入口 |
VerifySecureStorageAccess |
检查宿主安全存储是否可在当前环境中访问 |
ListDevices |
返回设备编号、显示名和在线状态 |
QueryMemberSyncPreferences |
查询指定成员的发送、接收和内容类型偏好 |
UpdateMemberSyncPreferences |
局部更新指定成员的同步偏好,未提供字段保持不变 |
RemoveMember |
追加一条签名成员历史移除,并立即停止向目标发送新内容 |
QueryDeviceGroupChoices |
返回 revision、完整设备信任快照,以及当前所有待处理设备组问题与可选设备组 |
ChooseDeviceGroup |
按问题编号、选择编号和预期 revision 选择设备组;本机将被移除时要求明确确认 |
QueryMembershipDiagnostics |
仅 dev-tools:返回内部成员分支、epoch、冲突、待执行效果和过渡阶段诊断 |
SearchEntries |
使用关键词、时间、内容类型、来源设备和标签等条件查询加密搜索索引。tag_match 取 any(默认,命中任一标签)或 all(必须同时携带所有标签);其他维度组内取“或”,维度之间取“且” |
CountSearchEntries |
批量统计匹配数:每个查询与 SearchEntries 走同一套解析与索引路径,total 逐项一致,limit、offset 被忽略;单次最多 32 个查询,按输入顺序返回。加密会话未就绪时返回搜索会话锁定错误,索引重建中返回重建错误,不降级为近似值 |
QueryDailyEntryCounts |
按调用方给出的严格递增绝对时间戳边界统计条目数,第 i 个桶为 [b[i], b[i+1]),最多 400 个桶。Engine 不含时区与夏令时规则,日边界由宿主按用户本地时区计算。会话锁定失败关闭,规则与 CountSearchEntries 相同 |
QuerySearchTags |
查询当前索引中的标签和条目数量;本机历史标签以 is_builtin = false 出现,只统计已建索引的条目;会话锁定或索引重建中不返回本机历史标签 |
QuerySearchStatus |
查询索引是否可用及最近重建时间 |
RebuildSearchIndex |
请求重建当前加密搜索索引 |
SendText |
写入加密历史、更新搜索并发送不超过 64 KiB 的文本 |
SendImage |
写入加密历史、更新搜索并发送不超过 64 KiB 的图片 |
QueryHistory |
查询历史并返回稳定分页标记 |
ListHistoryEntries |
按偏移量返回桌面兼容列表所需的完整历史投影 |
GetHistoryEntry |
返回指定文本记录的完整详情 |
DeleteHistoryEntry |
删除指定记录及其关联选择、文件、搜索和 blob 引用 |
SetHistoryEntryFavorite |
设置指定记录的收藏状态 |
ListHistoryTags |
列出本机历史标签及各自关联的条目数,按条目数降序、名称、id 排序;名称无法解密的标签以空名称返回,仍可删除 |
CreateHistoryTag |
按名称创建本机历史标签;规范化后同名(忽略大小写)时返回已有标签和 created = false |
RenameHistoryTag |
修改标签名称;与另一个标签同名时不写入并返回 NameConflict { existing_tag_id };名称密文无法打开的标签不能改名(1404),只能删除 |
AddHistoryTagToEntries |
把一个标签关联到 1 到 1000 条记录;存在的记录在一个事务内生效,不存在的记录跳过并返回 |
RemoveHistoryTagFromEntries |
从 1 到 1000 条记录移除一个标签,规则与关联相同 |
SummarizeHistoryEntryTags |
返回一组记录中仍存在的数量,以及每个标签在其中的携带数量 |
MergeHistoryTags |
把 1 到 100 个来源标签的关联并入目标标签并去重,随后删除来源标签;记录内容不变 |
DeleteHistoryTag |
删除标签及其关联,返回解除的关联数;记录内容不变 |
QueryHistoryStats |
返回历史记录总数和总大小 |
GetHistoryEntryResource |
返回指定记录的资源标识、类型、大小及可用读取方式 |
ReadBlob |
读取指定 blob 的完整字节和媒体类型 |
ReadEntryThumbnail |
Resolve the current entry preview and read or generate bounded WebP thumbnail bytes. See entry thumbnails. |
ReadEntryFile |
读取指定记录的首个已物化文件及下载文件名 |
QueryEntryDelivery |
返回指定记录的来源及每个当前仍有效且可信设备的投递状态;已移除设备不显示,当前成员范围无法确认时查询失败 |
ClearHistory |
清空全部历史,并返回删除数量和未删除条目标识 |
QueryOutboundProgress |
按原源条目和目标设备读取完整发送侧进度;没有保留资料时返回空 |
ListOutboundProgress |
列出当前授权范围内仍保留的完整发送侧进度 |
ReconcileOutboundProgress |
执行完整原任务核对,可指定预期尝试;不启动下载 |
QueryEntryReceiveProgress |
查询指定远端接收任务的当前聚合进度 |
ListEntryReceiveProgress |
列出全部尚未结束的远端接收任务进度 |
CancelEntryReceive |
按记录和尝试编号取消一次远端接收任务 |
CancelInboundTransfer |
按传输编号和稳定原因取消一次正在进行的文件接收 |
CaptureCurrentClipboard |
立即读取系统剪贴板并按正常捕获流程保存 |
QueryActiveClipboard |
查询当前生效的记录编号和触发设备编号,不返回剪贴板内容 |
RestoreClipboard |
以普通、纯文本或文件路径模式把指定历史记录恢复到系统剪贴板 |
ExportEntry |
通过宿主文件句柄分块写出主内容 |
ResendEntry |
重新发送一条本机仍持有内容的历史记录 |
SendFiles |
从宿主句柄分块导入文件,并按现有文件协议发送 |
RecoverSession 的 allow_secure_storage_unlock 由宿主根据当前运行环境决定。值为 false 时核心不得尝试从系统安全存储恢复密钥;值为 true 时,核心统一完成加密会话、空间会话、搜索和接收能力恢复。
当资料和 keyslot 仍在,但自动解锁材料缺失或错误时,Engine::start 返回可用的受限实例,启动进度为 RecoveryAvailable。此时 QueryProfileRecovery、QueryEncryptionState、UnlockSpace 和生命周期关闭可用,业务数据库、网络、搜索、收发与历史操作返回 PROFILE_RECOVERY_REQUIRED;session_ready 必须为 false。宿主继续用 UnlockSpace 提交原口令。错误口令返回 UNLOCK_SPACE_UNAUTHORIZED_CODE 且不写入;正确口令恢复原密钥、启动完整后台并报告 Recovered。缺少旧独立密钥副本时报告 PartiallyRecoverable 及稳定影响类别,不返回已经解锁。损坏、不支持格式和保存失败分别使用原损坏分类、PROFILE_RECOVERY_UNSUPPORTED_CODE 和 PROFILE_RECOVERY_PERSISTENCE_FAILED_CODE,其他启动错误保持原分类。恢复口令通过后若完整后台启动或后续解锁失败,状态必须进入 Failed,can_submit_passphrase=false、restart_required=true;同一进程不得复用已经消费的宿主能力,宿主重启 Engine 后继续。升级备份的安全记录(security-current 与各代 *.record)只是文件副本的派生物,与其记录密钥共用一个生命周期。出厂重置在清除密钥后、清除资料前作废本 profile 的全部安全记录与记录密钥(文件副本保留且仍可恢复);升级准备发现记录密钥确定不存在时,把旧安全记录视为不可用,作废后按“尚无记录”重新建立,文件副本校验门槛不变、不被绕过。只有读取安全记录内容的严格路径(删除备份时核对记录、恢复安全材料)在密钥缺失时仍返回稳定的 PROFILE_UPGRADE_BACKUP_KEY_MISSING_CODE(1224,Unavailable,不可重试),不生成替代材料;存储读取失败、记录损坏或无法解密不属于“密钥缺失”,照常失败。该错误码与邀请域 INVITATION_FAILED_CODE(1224)同号,宿主必须按“启动或备份操作上下文 + Unavailable + 不可重试”匹配,不得按数字全局判断。
本机已有空间、网络身份文件却缺失时,Engine::start 同样返回受限实例:状态为 PartiallyRecoverable,损失类别为 DeviceIdentity,can_submit_passphrase=false。此时不绑定网络,也不生成替代身份;成员历史只认原身份,补发新身份会让其他设备永久拒绝本机。旧版身份目录待改名或存在待应用的配置导入时,身份将在装配阶段写入,不作此判定。受限实例在资料密钥仍可自动打开时接受 FactoryResetSpace:只装配重置所需依赖,不启动后台,完成后报告 restart_required=true,宿主重启 Engine 后以全新资料启动;资料密钥无法自动打开时该操作返回 PROFILE_RECOVERY_REQUIRED。
每次装配业务会话前,Engine 都会完整核验空间准入资料(权威记录、元数据与派生摘要)。已证实无法读取或核验时进入准入受限恢复:Engine::start 返回可用的受限实例,启动进度为 RecoveryAvailable 且 allowed_actions.retry=false;运行期恢复前台或重建会话时发现同类失败,resume 同样以受限实例完成,不报可重试错误。受限状态下 ProfileRecoveryState 为 AdmissionRecoveryRequired,can_submit_passphrase、restart_required 和 background_ready 均为 false,admission 字段给出稳定的 category、stage 和 action,并发出 ProfileRecoveryChanged。只有 QueryProfileRecovery、QueryEncryptionState(session_ready=false)与生命周期操作可用;准入、成员维护、收发和同步等其余操作,包括 UnlockSpace 与 FactoryResetSpace,都返回不可重试的 PROFILE_RECOVERY_REQUIRED。同一进程不重试修复,也不删除、忽略或改写权威记录;宿主按 action 引导用户恢复凭据(restore_credential)、选择可信备份(choose_backup)、重建派生状态(rebuild_derived_state)或导出诊断(export_diagnostics),处理后重启 Engine。类别只在原因可证实时精确给出,无法区分密钥不匹配与密文篡改时统一为 authentication_mismatch;公开结果不含原始错误文本或资料内容。锁定、存储暂不可用和并发变化不属于此状态,按原有可重试错误返回。
正常启动和恢复完成后的系统安全存储只保留当前资料的一条自动解锁材料;独立随机密钥位于 userdata 的加密文件中。完整 userdata 加当前口令可以在空安全存储环境恢复,导出与导入会携带该密文文件。FactoryResetSpace 同时清除两处副本。宿主不得把 GUI 内容锁定解释为此处的真实密钥恢复状态。
单设备修改加密口令采用一个产品动作。产品收集用户自定义的新口令和再次输入值,一并交给 ChangeEncryptionPassphrase;两次输入不一致时不修改任何资料。成功后旧口令不能解锁或通过新配对认证,此前签发的邀请失效,新口令在重启后继续有效。该能力不要求 re_pairing_required,只允许 Space 已解锁、本机成员有效且当前设备列表范围只含本机;存在正常或暂停的其他设备、成员恢复中或成员资料不可确认时均拒绝。它保留现有 MasterKey 和历史内容,不触发批量重加密;已有的重新配对提示仍由新设备实际加入结束。iOS、Android 和 HarmonyOS 绑定公开相同的修改动作,不承担资格判断或恢复。
CancelInvitation 在没有待取消邀请时返回冲突错误。ResetSpace 是用户明确触发的最后兜底:Engine 先停止
旧空间运行,清除未结束加入、待确认发送、恢复、切换、邀请和全部旧设备关系,把仍可读取的本机历史与文件
迁移到只含本机的新空间,并保存全部设备需要重新配对的状态。它不等待网络,不清除一般设置、设备身份、
解锁材料或本机资料;中断和重复调用继续同一个目标空间。FactoryResetSpace 则停止
全部旧运行入口,先清除并确认密钥材料不存在,再清除数据库、空间世代、设置、邀请、关系、准入记录、
导入暂存和受管缓存。成功结果表示同一个 Engine 已按全新资料重建运行期:随后可直接查询空空间、创建或
加入空间,再次离开仍然成功,宿主不需要重新创建 Engine;仍然“关闭后重新创建”的宿主结果相同。重建由
恢复运行期统一负责,全程持有运行期写锁,其间到达的其他操作排队后面对新运行期;宿主级资源(剪贴板变化流)
在运行期之间归还复用,只在 Engine 最终关闭时关闭。重置本身失败时保持原运行期并返回对应的重置错误码,
业务操作在完成前返回 1103,同一实例再次调用 FactoryResetSpace 会从已持久的阶段续做。资料已清除但
新运行期无法装配时,不报告成功:返回不可重试的 FACTORY_RESET_RESTART_REQUIRED_CODE(1375,
Unavailable),发布 restart_required=true 的 ProfileRecoveryChanged,此后除 QueryProfileRecovery
与生命周期关闭外的操作都返回同一错误码,宿主重新创建 Engine 后以全新资料启动。与离开并发的在途操作
可能在旧运行期上以 1103 结束,重试即可。启动遇到未完成清理时会续完清理并返回可重试的 unavailable,宿主
随后再次创建 Engine。QuerySetupState 不返回内部服务状态。
规格 023 的稳定产品外形已经接入:JoinSpace 返回 Active、Pending、Processing、Rejected 四类结果并公开稳定
join_id。Pending 表示加入已经保存但尚未完成本机准备,Processing 表示本机准备完成并等待最终确认收尾;两者跨重启和重复查询都返回同一个 join_id,宿主不另存加入编号或推断后台阶段。Pending、Processing 与 Active 的 peer_upgrade_required 表示这次加入仍需对端升级,首次请求不兼容则以 Rejected 的稳定原因明确返回。
提示不会把已经正式提交或本机已激活的加入回滚成失败;对端升级上线后立即继续同一请求并在成功推进时清除。提示出现、清除或
明确拒绝保存成功后发送 RefreshRequired { StateInvalidated },宿主随后通过 QueryDeviceGroupChoices 重新读取完整事实;普通内部推进
和重复旧端错误不发送。CancelJoinSpace(join_id)
负责本机取消,待激活候选继续使用现有 RemoveMember(device_id)。现有
QueryDeviceGroupChoices 返回的 DeviceGroupChoicesSummary.device_trust 包含 current_join 和
pending_inbound_member;现有 DeviceTrustChanged { revision }
继续只提醒重新查询,revision 在同一 profile 内跨 Space 单调递增。不新增按 join id 查询任意历史操作、
入站取消、待激活专用移除或独立于设备组选择视图的第二份完整快照;内部成员诊断和
WorkspaceConvergenceChanged 事件继续只用于 dev-tools。
对端因自己的另一项准入而暂时忙碌时,当前 JoinSpace 保持同一 Pending 并由 Engine 重试,不变成 Rejected。
取消请求只与发起方正式提交竞争:取消先保存时返回 Rejected 且没有成员新增;正式提交先保存时取消已经
太晚,同一请求继续保持 Processing 直到 Active,不自动生成成员移除。用户仍要退出时从另一台当前成员设备
另行使用现有明确移除。
公开的旧空间迁移进度操作已经删除,空间切换只表现为同一条 JoinSpace 从 Pending 进入 Processing 再到 Active。QuerySetupState 继续只负责
设置、设备名和邀请。profile 级负责人已经在没有活动 Space 时常驻,保存和恢复加入、取消、终态、revision
与 ordinal,并组合零或一个完整活动 Space;Engine 只路由产品动作,不保存内部阶段。同一 profile 的入站
和本机加入共享一个准入槽,Fresh Pending 没有活动 Space 时仍能执行彻底重置。
生产加入统一使用 Candidate、Prepared、Commit、Applied、Complete、CompleteAck、Settled。加入方先验证并保存完整历史和目标 安全状态;邀请方收到 Applied 后只保存待确认资料并发送 Complete,不写正式成员。加入方完成本机准备后返回 CompleteAck,邀请方验证该确认后才在唯一提交点写入正式成员并返回 Settled。跨 Space 时 JoinSpace 先返回 Pending,本机准备后为 Processing;CompleteAck 或 Settled 丢失时从 Engine 保存的同一尝试重发,收到 Settled 并保存后才返回 Active。
同一 Space 重新加入时,邀请方历史可以比本机已保存历史更新,但必须完整包含本机已经确认的连续历史; 缺少记录、倒退或分叉都返回 Rejected,不覆盖本机事实。普通成员上线只交换新版完整历史,不再发送旧版 问候、分页或单独决定消息。双方保存的成员决定可以不同,合并时保留并集,但对端只能新增自己签署的决定。
JoinSpace.preserve_unreadable_history 只影响跨 Space 来源历史。值为 false 且本地发现不可读密文时,
Engine 在联系邀请方前返回 1292 冲突,不创建新的加入尝试。值为 true 时,同一选择随加入记录保存;
不可读密文保留原字节并隔离,Active 结果的 preserved_unreadable_records 返回数量。该选择一旦进入同一
Pending 加入便不能在重试中更改。
QueryStorageStats 和 ClearStorageCache 由核心执行。宿主只能看到分类后的字节数和实际释放量,不能取得数据库、密钥库、缓存或日志的本机路径。
RecoverNetwork 不改变 RefreshPeerConnections 的轻量探测语义。恢复状态事件可能因消费者积压而由 RefreshRequired 代替,宿主随后调用 QueryNetworkRecoveryStatus 获取当前事实;状态和错误不包含设备名、地址或底层网络文本。
QueryLocalDevice 的显示名由应用查询统一读取并规范化;设置缺失、读取失败或名称为空时使用稳定默认名称。调试输出不得包含显示名。
移动兼容操作只服务于用户显式启用的 LAN HTTP 兼容通道,不会在 P2P 失败时自动触发,也不替代完整节点能力。核心统一拥有设置持久化、设备记录、密码校验、登记、编辑和撤销规则;HTTP 监听、网卡选择和端口绑定仍由桌面外壳负责。
UpdateMobileSyncSettings 只校验并保存总开关、LAN 监听开关、广告地址和端口,返回落盘后的目标状态与是否发生变化。桌面外壳根据该结果启动、停止或重新绑定 listener,再用 UpdateMobileLanEndpoint 报告实际结果。仅报告监听地址不能代替用户设置:RegisterMobileDevice 必须同时确认总开关、LAN 监听开关和实际 listener 均已启用。
登记成功会返回设备编号、标签、用户名、一次性明文密码、安装地址、连接地址和二维码内容。UpdateMobileDevice 支持保持密码、自动生成密码或使用宿主提供的新密码;只有发生换密时才返回一次新密码。鉴权成功可以返回标记内容来源所需的设备编号、设备类别和不透明凭据凭证,但设备编号、标签、用户名、地址、二维码、授权头和密码校验材料不得进入调试输出。长连接复查必须回传凭据凭证,宿主不得自行读取或比较持久化的密码散列。
LAN 内容读写复用核心现有的加密历史、系统剪贴板写入、内容去重、活动状态、搜索和对端转发流程,不建立独立内容库。同步文档完整保留类型、正文、附件名、大小、兼容 hash 和稳定 content id 供协议响应使用,但这些字段不得进入调试输出。写入结果明确区分新内容已应用、已有内容重新激活、重复跳过、文档解码失败和附件已缓存等待文档五种状态。
流式上传编号只表示当前核心实例内的一次临时写入,不能解释为路径或底层文件句柄。每个编号只允许串行追加,并且只能完成或放弃一次;伪造、重复使用以及核心暂停后继续使用都会返回找不到。核心暂停或关闭时主动放弃全部未完成上传,恢复后不得继续旧上传。HTTP 请求体大小、媒体类型嗅探和传输进度仍由桌面外壳负责,临时文件落盘、接收关联和最终入站结果由核心负责。
QueryEncryptionState 只返回初始化和会话可用状态。LockEncryption 成功后必须同时关闭接收入口,避免锁定后继续写入加密业务数据。VerifySecureStorageAccess 使用跨平台安全存储语义,宿主接口可按平台显示为 Keychain、Keystore 或对应系统名称。
QueryMemberSyncPreferences 和 UpdateMemberSyncPreferences 只接受稳定设备编号。局部更新中未提供的开关和内容类型必须保持原值。
工作空间收敛由核心完整负责。正式宿主只追加本机移除、通过 QueryDeviceGroupChoices 读取完整设备关系与
待处理设备组问题、对当前问题作一次选择并订阅 DeviceTrustChanged。RemoveMember 保存签名成员历史并
立即限制本机向目标发送;ChooseDeviceGroup 只接受查询结果中的问题编号、选择编号和 revision。宿主不保存
成员历史、不安排消息顺序,也不创建重试队列。
DeviceGroupChoicesSummary 返回统一 revision、完整 DeviceTrustSnapshot 和零个或多个待处理问题。每个
问题公开稳定 issue_id、语言无关的 reason 及其选择;原因包含已验证的类别、关键变更的作者/目标、
接受/拒绝事实及详情完整性,不包含自然语言句子或翻译键。产品负责 i18n,设备名称原样插值。
每个选择公开 choice_id、是否为当前设备组、是否要求重新配对、成员设备编号、含名称/本机标记/候选内
激活状态的 members、名单完整性、证据来源设备编号,以及可选的 impact。影响包含预期同步范围、
暂停、待确认、重新加入设备及本机结果;预期范围不是发送授权,远端选项在恢复前全部标记待确认。
旧资料无法确定远端名单时 members_complete=false、impact=None、原因 unknown,不能显示成空组。
详细责任和持久化边界见成员历史职责。
嵌套的 DeviceTrustSnapshot 返回本机设备及成员状态、当前变化、加入状态、
待激活成员、完整设备关系、恢复可用性、允许动作、稳定阻塞原因和更新时间。设备关系包含用于产品展示的
device id 与 display name,但这些字段不得进入日志或调试输出。
设备关系中的 confirmation_pending 表示对端尚未通过认证回复确认本机最新成员结果。产品必须把它显示为
“设备组尚未确认一致”,不能按正常关系展示;有效确认到达后自动恢复为 consistent。离线、超时、重启或
失去通信路径都不能自行清除该状态,它也不等同于已经取得两份不可比较历史的 diverged。
每个设备关系的可选 group_update 给出本机对该设备尚未确认的队首组密钥更新:epoch、state
(waiting、receiver_unavailable、rejected_unclassified、rejected_by_receiver)、attempts,以及仅在
recovery=automatic 时出现的 next_retry_at_ms。recovery 表示 Engine 自己是否会推进:automatic 按退避自动重试;
automatic_on_reconnect 在该设备重新通过认证后自动重新评估一次(有频率上限,成功应用才清除);review_device
不会自动重发,需要核对该设备。缺省表示本机对它没有未确认的更新,该事实与成员保护状态(QuerySpaceProtection
的 MLS 成员关系)相互独立,宿主不得把“受保护”读成“已确认收到最新 epoch”。automatic 和 automatic_on_reconnect
时宿主的“重新检查”不会改变结果,不应提供;字段缺省或为旧值的宿主按原 space_device_update 展示。
整体状态 space_device_update 的取值不变:任何设备的拒绝(含原因未知的旧拒绝)仍使整体进入
needs_attention/device_security_update_rejected,原因与设备由 group_update 说明。
产品调用 ChooseDeviceGroup 时必须原样回传同一次查询中的 issue_id、choice_id 和 expected_revision。
结果明确区分完成、仍在等待、需要重新配对、已经完成、状态已变化和需要确认移除本机;状态已变化时重新
查询,不得用旧选择覆盖新事实。本机将被移除时,只有在产品取得用户明确确认后才传入
confirm_local_removal = true。状态保存成功后发送 DeviceTrustChanged { revision };事件消费者收到更新
revision 或 RefreshRequired 后重新查询,相同或更小 revision 不覆盖已查询结果。iOS、Android 和
HarmonyOS 绑定必须公开相同字段、结果、错误和提醒。
以下 WorkspaceConvergence 快照和变化事件只在显式 dev-tools 构建中用于内部验收:
phase:locally_applied、converging、complete或recovery_required;revision:只随成功持久化状态变化递增的不透明版本;history_event_count:已保存的签名成员历史条目数量;effective_member_count:当前有效成员实例数量;pending_removal_decision_device_ids:已收到移除、仍等待本机选择的设备稳定标识;按标识稳定排序;pending_removal_decision_event_id:提交接受或拒绝时使用的待决定编号;没有待决定项时为空;diverged_peer_device_ids:已确认成员历史分叉、不能进行普通交换的设备稳定标识;按标识稳定排序;convergence_digest:当前工作空间摘要;尚未形成时为空;removed:本机当前成员实例是否已经观察到自身被移出;updated_at_ms:最近一次成功保存状态的时间;failure_category:可选的稳定类别,不含底层错误原文。
快照不得包含设备名称、成员实例、地址、在线名单、签名、密钥、安全变化正文、邀请资料、网络错误原文或剪贴板内容。待决定和分叉设备标识只用于匹配既有设备条目;宿主和产品端不得根据在线状态、设备列表或缓存推断成员历史关系。
removed 为 true 表示本机已经采用以当前成员实例为目标的成员历史移除。该字段只反映本机已观察到的单一事实,不包含成员列表、收敛摘要、安全代次、密钥或内容;桌面端可据此直接展示“此设备已被移除,需重新配对”,无需自行推断。
locally_applied 表示本机已保存当前成员历史状态;converging 表示正在核对成员历史。网络发送成功或发起操作返回都不足以代表两台设备关系一致。收到此前未知但合法的成员历史后,状态重新进入 converging;连续性、空间、身份或摘要无法验证、发现不可自动解决的分叉或有效成员为空时进入 recovery_required,核心停止自动推进。普通离线、超时和重启不改变成员历史。
内部收敛状态在本机保存成员历史或用户决定、收到成员历史、设备上线以及进入需要恢复时更新。它不进入
正式 iOS、Android 或 HarmonyOS 绑定,也不能代替 QueryDeviceGroupChoices 的产品契约。
搜索查询、标签、状态和重建都由核心执行。搜索结果可以正常返回预览、文件名、文件路径、链接和自定义标签,但这些用户内容不得出现在调试输出或日志中。加密会话锁定时,宿主不得读取搜索结果、标签或状态,也不得触发重建。
空目标设备列表表示向所有符合条件的可信设备发送;非空列表只会缩小目标范围,不能绕过信任、在线状态和发送设置。
SendText 和 SendImage 的内容必须为 1 到 64 KiB,大小按实际字节数计算。超出范围返回输入错误,不会写入历史或进入文件传输缓存。更大内容通过文件入口发送。
发送成功结果同时返回本机记录编号、快照摘要、发送时间、接受、重复、离线、失败、等待中的目标数量,以及已经结束的逐目标结果。逐目标失败说明只用于宿主展示,不得进入核心调试输出或日志。没有在主流程期限内结束的目标计入等待数量,不会伪装为失败。
历史查询每页必须为 1 到 200 条。分页标记是不可解释的稳定字符串,当前版本形如 uc-history-v1:<offset>;宿主必须原样回传,不应自行生成或修改。损坏、未知版本或越界输入返回输入错误。
ListHistoryEntries 是旧桌面列表接口迁移期间使用的完整投影,每次必须请求 1 到 1000 条,并保留预览、收藏、标签、链接、文件大小、图片尺寸和内容可用状态。它不替代带稳定分页标记的 QueryHistory,新宿主仍应优先使用搜索或 QueryHistory。列表、详情和资源结果可以正常携带用户内容,但调试输出不得包含预览、正文、链接、缩略图地址或内联字节。
GetHistoryEntry 只适用于可读取为文本的记录;记录不存在返回 NotFound,内容不支持文本详情返回 Conflict。SetHistoryEntryFavorite 对不存在记录同样返回 NotFound,不能把未修改任何记录当作成功。
本机历史标签是每台设备自己的历史元数据,不进入任何同步载荷,也不随配对传播。标签 id 是与名称无关的不透明 id,内置标签 id 保持不变并以 is_builtin 区分;用户手动关联不是规则标签。名称规则为:去掉首尾空白后转为 NFC,不能为空、不能含控制字符、最多 64 个 Unicode 标量;同名判断忽略大小写,展示保留用户输入;单个 profile 最多 1000 个标签。所有标签操作在加密会话锁定时返回 1405,旧格式 profile 返回 1406;未知标签返回 NotFound,合并的来源与目标相同返回输入错误,重复合并或删除因来源已不存在返回 NotFound。标签名称、创建时间与记录关联只以 MasterKey AEAD 密文保存;搜索索引中的用户标签成员只以搜索密钥 HMAC 后的词项保存,由权威关联派生,因此索引重建会恢复用户标签过滤。搜索结果的 tags 中的用户标签 id 由权威关联补齐;会话锁定时 QuerySearchTags 不返回用户标签。删除记录时其关联随之删除。调试输出与日志不包含名称。
DeleteHistoryEntry 和 ClearHistory 由核心统一清理数据库记录、选择、缓存文件、搜索索引和 blob 引用,宿主不得自行复制清理顺序。批量清空发生部分失败时只返回失败条目标识,不返回底层异常、文件路径或用户内容。
ReadBlob, ReadEntryThumbnail, and ReadEntryFile return binary bytes and the available media type. Entry thumbnails use the breaking entry thumbnail contract; the representation-addressed operation and history thumbnail URL/dimension fields are removed. Internal storage errors and paths never enter public responses or logs. Hosts must bound full original blob/file reads and clear their own resources on content lock.
QueryEntryDelivery 由核心合并记录来源、当前可信设备集合和已发生的投递事实。来源明确区分本机、远端和无法追溯的历史记录;每个目标明确区分待处理、已送达、重复、不可达和失败。待处理既可以来自尚未接受发送的当前设备差集,也可以来自发送开始前已经持久保存、等待最终结果的恢复依据;两者对宿主保持同一状态。正常结果可以携带设备显示名和失败补充说明供界面展示,但这些内容不得进入调试输出或日志。记录不存在返回 NotFound,存储失败只返回脱敏错误。
接收进度只包含记录编号、尝试编号、稳定状态、字节数和项目数,不包含文件名、路径或内容。QueryEntryReceiveProgress 在没有活动任务时返回空结果;ListEntryReceiveProgress 只返回尚未进入终态的任务。
CancelEntryReceive 使用记录编号和尝试编号防止过期请求误取消新任务,并明确区分已请求取消、已取消、未在接收、已经太晚、已经结束和已被新尝试取代。CancelInboundTransfer 是幂等操作:真实撤销返回 Cancelled,没有活动传输或重复取消返回 NotInflight。取消原因使用核心稳定枚举,宿主不得传入底层网络或文件系统错误。
CaptureCurrentClipboard 通过宿主剪贴板能力读取当前内容,并复用正常捕获、去重、加密历史和搜索更新流程。成功时返回记录编号;当前没有可捕获内容时返回空记录编号,这不是错误。宿主不得自行读取内容后绕过核心保存。
QueryActiveClipboard 读取核心已经收敛并持久化的当前状态,只返回记录编号和触发设备编号。尚未发生激活时返回空结果,这不是错误。宿主错过活动剪贴板事件后必须通过该操作恢复状态,不得通过读取剪贴板正文推断当前记录。
RestoreClipboard 的普通模式恢复全部可用格式;纯文本模式优先只恢复纯文本,没有纯文本表示时按既有规则降级为普通模式;文件路径模式只适用于能解析出本机文件路径的记录。成功、内容已经丢失和模式不适用都是稳定业务结果。内容丢失结果保留记录编号、表示编号和状态,模式不适用结果保留稳定原因;记录不存在返回 NotFound,真正的恢复故障返回脱敏错误。宿主不得自行复制恢复、触摸最近使用时间或同步广播的顺序。
ResendEntry 只处理本机来源且属于当前成员范围的历史记录。空目标列表表示全部符合条件的可信设备,
非空列表只缩小目标范围。文件和可能经 blob 交付的大图先核对原任务;剪贴板消息头已送达不代表文件已保存。
confirm_new_transfer=false 采用原任务的运行或真实结果,不建立重复下载;明确确认后,只有允许的核对结果
才交给原重投递及接收 CAS。运行中、已替换、离线或未核实不能由确认标志绕过。
已有任务核对不要求本机原内容仍可用,实际新投递继续要求原材料可用。
ResendReportSummary.reconciled 按目标提供核对分类、可安全采用的完整当前视图、是否请求新传输及重复文件风险;
原已接受、重复、离线、失败、等待计数只统计实际投递。详细决策只在切片规格§5.7维护。
记录不存在、记录不可重发、目标不可信和没有可用目标仍是稳定业务结果;核对的访问拒绝、损坏及不可用
复用发送侧进度的安全错误编号,存储与实际发送继续各自分类。原始底层正文不进入公开响应、Debug 或日志;完整来源继续交给既有安全观测边界。
UniFFI 在原 resend_entry 中接收确认标志并返回同一逐目标结果,N-API 的可选确认标志缺省为 false;
N-API 当前视图的 64 位修订和字节值保持十进制字符串,不经过浮点数。
导出只写宿主传入的目标句柄。核心看不到目标路径,每次最多写 64 KiB,并在全部数据写入后调用完成写入。取消操作时不会在恢复后续写。
公开错误只包含稳定编号、类别和是否可重试,不包含底层原因或用户内容。详细原因只进入脱敏日志。
| 类别 | 含义 |
|---|---|
InvalidInput |
输入、分页标记、文件句柄或内容类型无效 |
InvalidState |
当前生命周期或空间状态不允许该操作 |
Unauthorized |
口令错误、目标未授权或宿主无权限 |
NotFound |
邀请、设备、记录或资源不存在 |
Conflict |
已初始化、没有可发送目标或内容不可重发 |
Unavailable |
网络、索引、宿主能力或临时服务不可用 |
DeadlineExceeded |
操作超过约定期限 |
Internal |
无法向宿主公开细节的内部失败 |
每次被接受的操作都会产生一个 OperationFinished 终态事件。被生命周期期限取消的操作必须产生 Cancelled,不能只返回错误后静默消失。
- 私有数据目录用于数据库、加密内容、文件内容和设备身份。
- 缓存目录中的非文件业务内容同样必须先加密。
- 临时目录不能用于写入明文非文件业务内容。
- 安全存储保存密钥材料,调试输出必须脱敏。
- 剪贴板读取、写入与变化通知通过宿主能力完成。
- 文件只能通过不透明句柄分块读写,句柄不能伪装成本机路径。
- 产品分析是可选宿主能力;未提供时使用关闭实现,不产生外部发送,也不保存分析身份。
宿主不得自行保存剪贴板正文、预览、标题、标签名、文件名或文件路径。除内容类型分类、文件内容本体,以及核心入站受管文件缓存中的经安全清理的原始文件名外,所有持久化业务负载必须先经 MasterKey AEAD 加密;受管缓存文件名只能作为实际文件的 basename,不能包含发送端目录路径。
剪贴板变化通知是可选的单消费者信号流,只表示系统剪贴板可能已经改变,不携带业务内容。核心收到信号后统一完成加密会话门禁、回写来源判断、捕获、去重、活动状态推进、搜索更新和发送;宿主不得重复这些步骤。加密会话尚未可用时,本次变化被忽略,不排队到恢复后自动处理。核心暂停期间收到的变化同样不补发。
平台监听器已经取得完整快照时,宿主必须让下一次核心读取优先消费该快照,不能强制重新读取系统剪贴板。这样可保留 Wayland 等平台的一次性数据源。通知流只允许取出一次;核心关闭时会要求通知流停止,宿主必须释放系统监听器和后台线程。
剪贴板中的文件表示由宿主提供原格式、媒体类型、显示名、大小和不透明句柄。核心通过文件能力按最多 64 KiB 分块读取到匿名临时路径,显示名作为受加密持久化保护的元数据保存,不能成为临时目录或文件名。核心正常关闭时删除临时导入目录,下次启动时也会清除异常退出留下的旧目录。
SendFiles 通过 HostFileAccess 从宿主文件句柄分块读取内容,并交给核心拥有的导入目录和 blob store。文件内容允许按原始字节落盘,以保持现有 P2P 文件格式;文件名、宿主路径和关联元数据仍不得明文持久化。
The host declares additional configuration files with EngineConfig::with_host_configuration_files.
Each HostConfigurationFile contains a basename and a HostConfigurationFileReset owner.
The default declaration is empty. Engine does not parse product values or accept host-selected destination paths from a bundle.
Names must match [a-z0-9][a-z0-9._-]*, end in .json, contain no .., and use at most 64 ASCII bytes.
Names must contain one path component. Duplicate names fail validation, including duplicates with different reset owners.
Engine rejects these reserved names: settings.json, pending-import.json, upgrade-cursor.json,
first-sync-state.json, daemon-run.json, and daemon-last-exit.json.
An invalid declaration stops Engine::start before profile I/O with code 1109 (InvalidInput, not retryable).
Debug output contains the declaration count, not the filenames.
Export includes every declared regular file that exists under the data root. A missing file is optional. A symlink, directory, I/O failure, or file larger than 1 MiB fails export. The declaration must contain only configuration. Hosts must not declare credentials or user content as configuration. The existing encrypted bundle and credential policies remain unchanged.
The archive member is ui-state/<name>. The historical prefix remains unchanged.
Import restores that member to <data_root>/<name> only if the receiving host declares the same name.
The archive and its manifest cannot select any other destination.
Undeclared members do not create host files. A missing member preserves the existing destination file.
Manifest schema 2 keeps the historical ui-state/ member prefix. It requires the complete V3 runtime state.
An incomplete schema-1 bundle is rejected before staging or target mutation. The result is
ConfigImportStageOutcome::Incompatible, with a reason that requires re-export from the retained
source installation. This compatibility policy was explicitly approved. Old readers reject archive schema 2. The pending marker also uses schema 2, so an old boot reader
does not apply the new staging layout as a legacy single-store import.
Do not reconstruct missing members, export independent keys, or copy platform credentials.
The existing archive reader rejects absolute paths and parent traversal. The archive reader uses the last occurrence of a duplicate normalized member path, under the existing total size limit. Host declaration duplicates are rejected separately before I/O. Import writes each host file to a new temporary file, flushes it, then replaces the destination. A destination symlink is replaced; import does not write through the link. A destination directory fails import without removing that directory. Successful files can remain visible if a later copy fails. The import marker and staging remain for the next boot to retry. The boot owner clears the pending marker after all copies and runtime-layout installation succeed. It then removes staging. Each publication is flushed. The operation is not atomic across files. A failed adoption returns no running Engine handle. The next start resumes the same adoption. A retry copies the same staged bytes again. Hosts must not concurrently change files while boot applies an import.
ByEngine files follow the existing factory-reset journal after key wipe and Engine state cleanup.
Engine removes files and unlinks symlinks. Engine refuses directories without recursively removing them.
A failure preserves the reset phase for retry. Earlier file deletions can remain visible.
ByHost files survive both failed and successful Engine reset.
The daemon deletes the product file only after FactoryResetSpace succeeds and owns deletion retry and error visibility.
The host must leave telemetry consent outside the reset declaration.
Upgrade backup keeps its existing whole-root scope and exclusion list. Backup includes declared files, undeclared files, and telemetry when they exist in the root. The declaration does not narrow that scope.
The Application profile-startup owner invokes one complete offline adoption capability before version backup and before reading the lifecycle used for runtime wiring. The Infra import owner performs the following transaction while the host holds exclusive access to the profile:
- Bind a durable local journal to the staged marker and record which fixed Engine components exist.
- Move those components with no-replace renames into a private sibling recovery directory.
- Capture and verify the retired state with
ProfileBackupArchive. Retain its receipt and originals. - Clear the recovery cache. Install the existing encrypted vault, content-key catalog, KEK and cursor.
- Authenticate the source V3 manifest with the imported lifecycle and admission key. Use
ProfileRuntimeLayoutto install both database snapshots and encrypted blobs at its generation paths. - Mark installation durable. Clear the pending marker, remove staging, then clear the local journal.
- Load the imported lifecycle for runtime wiring. Never reuse the target lifecycle or upgrade journal.
Export keeps independent profile and control snapshots. It checks both persistent database revisions
and protected files before sealing. A concurrent source change causes at most three attempts. Other
errors fail immediately. The outer bundle AEAD, KDF, limits and current-profile KEK contract do not change.
The existing encrypted profile vault carries independent runtime secrets. secrets.json does not gain keys.
The retirement list is fixed in Engine code. Neither the archive nor host declarations select it. Unknown root files and telemetry remain in place. Optional absent settings remain in place. Declared host files use their existing overwrite and missing-member rules. Retired Engine directories include unknown children without deleting them. Existing target upgrade backups stay with the old encrypted vault in the retained recovery transaction. Imported keys do not reinterpret their security records.
Recovery data lives at the profile parent's .uc-config-import-recovery/<root-digest>/<transaction>/.
originals/, archive/ and receipt.json remain after success. This transaction does not prune them.
To restore, use the verified archive to create a new isolated profile. Restore retained upgrade backups
to the host-selected backup directory and spool data to the host-selected spool directory separately.
Use the original target passphrase through public UnlockSpace; never copy secure-storage identities.
No-replace retirement requires the state and recovery directory to share a filesystem. A cross-device
move fails with originals retained. Do not substitute an unverified copy-and-delete operation.
Historical 1101 and 1103 failures remain repro evidence. Successful acceptance requires public Engine starts on fresh and existing targets, first and subsequent starts, source Space identity, ready encryption, copy-failure replay, retained target bytes and recovery of the old target with its original passphrase. The original incomplete legacy bundle is accepted only as an explicit rejection with target state unchanged.
Unsupported or incomplete pending imports left by an older Engine are rejected before adoption starts.
Engine retains the original pending-import.json and the entire import-staging/ tree byte for byte.
It does not delete, relocate, copy or rewrite rejected materials. These existing persisted inputs are
also the persistent rejection state. No additional credential file, journal format, backup series or
plaintext status file is introduced. Ordinary startup continues with the original profile. Rejected
staging does not bypass original-profile key recovery. An existing adoption journal or an inspection
I/O failure still follows the recovery/failure path; it must not be treated as a safe rejection.
QueryDiagnostics returns rejected_config_import: Option<RejectedConfigImportSummary>, including a
safe reason (unsupported_format, malformed_marker, or missing_runtime_members) and the fixed
message Import was not completed. Re-export the configuration bundle using the new version. Hosts
must display the rejection and localized re-export guidance after startup, including subsequent starts.
The status is read from persisted inputs on each query; it is not a once-only event or log message.
No source path, member name, profile identity or secret is included. None means there is no rejected
pending input; it does not certify a successful import. Stage success still means pending, not applied.
Mobile hosts use the formal UniFFI method MobileEngine.query_rejected_config_import().
It returns Result<Option<RejectedConfigImport>, BindingError> by executing the real Engine
QueryDiagnostics operation. The record contains the typed ConfigImportRejectionReason and the
same fixed message. It does not read private files or use the process-observability status query.
Engine query errors retain BindingError::Engine { code, category, retryable }; they are never
converted to None. Hosts query after startup and successful session recovery/resume, show a
separate status-read failure when the query fails, and localize by reason rather than parsing message.
Use Swift/Kotlin generated from the matching UniFFI library; regeneration is part of this interface change.
No Mobile write or configuration-import operation is added. Product UI remains host-owned.
The single pending slot remains bounded and retained until an explicit action replaces or removes it.
StageConfigImportInput.replace_rejected_import must be false unless the host has obtained explicit
confirmation to irreversibly replace the retained rejected materials. With false, a new Stage returns
Incompatible and preserves all old bytes. With true, the new bundle and password must first pass
complete validation; only then may the maintained staging writer replace the old slot. Failed validation
never releases old materials. Confirmation must explain that the retained data may be the only source
and that replacement is irreversible. An explicitly requested whole-profile FactoryReset also deletes
this slot under its existing deletion contract. Ordinary startup, diagnostic queries and export do not.
There is no automatic pruning, unbounded rejection archive, or hidden credential-copy recovery path.
Only fixed Engine-owned paths are inspected. Marker-supplied destinations cannot select filesystem paths. Inspection rejects marker, staging-tree or member symlinks and nonregular objects with a storage failure; it does not follow them or convert an I/O fault into permission to continue or replace data. The host must supply an exclusively owned profile root. Adversarial replacement of ancestor directories outside that root is outside the exclusive-profile contract.
Export checks the cumulative tar size, including headers, padding and the AEAD size allowance, before reading blob contents. Oversized exports fail without replacing the destination. Capture and revision verification run in one blocking task with the caller's tracing span. Both database snapshots and blobs stream into the bounded tar. Scratch snapshots are cleaned on success and failure.
Engine owns no log-verbosity preference. The host owns the process log profile and applies it
before it starts Engine. The host reports the profile it installed with EngineConfig::with_host_log_profile.
The default is no declaration.
The label must be 1–32 bytes of [a-z0-9_-]. Engine does not interpret the label or keep a list of
host names. An invalid label stops Engine::start before profile I/O with code 1110 (InvalidInput,
not retryable). The label is not persisted and is not part of a configuration bundle. Each start reports
only its own declaration. The host owns any restart that applies a changed preference.
QueryDiagnostics returns DiagnosticsStatusSummary { effective_log_profile: Option<String> }.
The diagnostic export manifest carries the same value as effectiveLogProfile, or null when
the host declared nothing. One value feeds both. Engine does not read RUST_LOG, UC_LOG_PROFILE,
settings.json or its own build type to guess a profile. Diagnostic export no longer loads settings.
The declaration changes no Engine logging. Module logs follow the standard and detailed capture modes of the process observability runtime. The network-library log rules, budgets and capture windows are unchanged. No profile label starts a detailed capture.
Removed from the Engine contract: Operation::UpdateDebugMode, OperationKind::UpdateDebugMode,
UpdateDebugModeInput, DebugModeUpdateSummary, OperationResult::DebugModeUpdated,
DiagnosticsStatusSummary.debug_mode, DiagnosticsStatusSummary.restart_required,
GeneralSettingsSummary.debug_mode, GeneralSettingsPatch.debug_mode, and the manifest key debugMode.
Error code 1412 is retired for the diagnostics group. It stays assigned to
RECEIVE_UNAVAILABLE_CODE in the receive group. QUERY_DIAGNOSTICS_FAILED_CODE (1411) stays,
because QueryDiagnostics still fails when the retained-import status cannot be read.
A legacy general.debug_mode of any JSON type is ignored on load and not written on the next save.
The settings schema version does not change.
发送侧进度的 OutboundProgressSummary 按原始逻辑项保留完整结果和计数,联系状态独立于业务终态。
失联只把未确认项标为 Unconfirmed,不推断远端失败或取消;失败只含固定类别。视图不暴露协议绑定、producer或线上序号。
ReconcileOutboundProgress 返回 Running、Settled、Superseded、NotRetained、Unsupported、Unreachable或Busy;
授权拒绝、损坏数据及本地读取失败仍走稳定错误,不能伪装成远端离线或没有资料。
expected_attempt_id 已被替换时返回Superseded,宿主无需自行比较协议对象。
OutboundProgressChanged 只提示缓存失效,事件丢失或滞后后可以重读列表;网络采用及ACK不等待宿主渲染。
UniFFI与HarmonyOS绑定调用同一完整操作;HarmonyOS的字节数及修订号使用十进制字符串,避免JavaScript整数精度损失。
Engine owns clipboard write attribution. A host may return an opaque identity in the write receipt only when it can prove that the identity belongs to that successful native write. The host must attach the same identity to each snapshot captured from that write. The identity must differ for every distinct user or application write during the Engine lifetime. It must not contain clipboard data, file names, paths, or business identifiers. The identity is local to the host and must not be persisted or logged.
The host must attach the identity at capture time. A delayed captured snapshot must retain its original identity. A later read of the current clipboard must not assign its identity to that snapshot. The host must return no identity when atomic association is unavailable or ambiguous. A timestamp, a content type, and a time window do not prove identity. A native counter is valid only if the host also proves that its receipt and snapshot refer to the same write, including batching, races, resets, and reuse.
Engine waits for in-flight writes before attributing an identified observation. A failed write does not register an identity. A successful write with no identity leaves subsequent observations as local captures. The host must not synchronously wait for an observation to finish inside its write callback. Engine retains at most 256 identities per runtime; an observation whose identity has been evicted is treated as local. Duplicate observations with a retained matching identity have the same origin. This conservative fallback may resend an echo, but does not suppress an unproven user copy.
Existing hosts use the default methods and return no identity. UniFFI hosts must populate BindingClipboardSnapshot.identity for a captured observation and return the matching optional identity from clipboard_write. Android captured handoff must carry the capture-time identity with the captured data. Swift and Kotlin adapters must prove a platform-specific association before returning an identity. HarmonyOS and Desktop adapters remain on the conservative path until their host boundaries support this contract. Generated declarations do not establish native clipboard acceptance.