安装级共享存档
存档是安装组件的持久游戏/工具进度,不是配置、布局或可清理缓存。Services 是唯一提交所有者,沿用安装级存储锁和有界存储队列;读写、刷新不占 UI 或全局事务锁。停用保留存档,卸载按安装所有权清理;布局应用、恢复和包版本回退均不复制或回退存档。
接口
无需 Capability;同一安装的全部实例自动读写同一份存档;不同安装始终隔离。slot 为 1..32 个 ASCII 小写字母、数字、下划线或连字符,每个安装最多 8 个槽位。限定小写避免 Windows 文件名大小写碰撞。作者自行在value中组织版本、校验、兼容和迁移;宿主只校验JSON、容量、修订和落盘完整性。
saves.context {}固定返回{scope:"installation"},不接受安装、实例或档案选择参数。saves.list {}返回{slots: SaveSummary[]},最多 8 条,不含正文。saves.load {slot}返回{found, current: SaveSnapshot|null, previous: SaveSummary|null, recovered}。不存在才返回 found=false;已存在但两份记录均损坏时返回 RESOURCE_UNAVAILABLE,不能当作新游戏。saves.commit {slot,expectedRevision,value}原子发布完整 JSON 快照,返回 SaveSummary。新槽 expectedRevision=null,否则必须等于当前有效修订;不符返回 SAVE_CONFLICT。正文 Stringify 后 UTF-8 最多 256 KiB,当前和上一成功修订各一份,不记录每帧日志。saves.restore {slot,expectedRevision}将上一成功快照作为新修订发布,原当前成为上一条,可再次恢复撤回;没有上一条返回 RESOURCE_UNAVAILABLE。CAS要求同commit;业务兼容由作者判断。
SaveSummary 为 {slot,revision,savedAt},revision 是宿主生成的不透明 UUID,savedAt 为本地 Unix 毫秒时间。SaveSnapshot 额外含 value。时间不是防作弊依据。响应丢失后应 load 确认,不盲目覆盖;提交开始后的取消不能撤回已发布数据。
SDK widgetHost.elapsedSince(savedAt,{now?,maxMs?}) 返回 {elapsedMs,clockMovedBack,capped},默认 now=Date.now()、maxMs=28800000;三个输入须为有限非负数。回拨为零,超限截断。此纯计算工具不保存检查点、不决定奖励或暂停;调用方仍需先提交结算后的进度再继续交互。
落盘与恢复
位于安装 storage 根下的 saves 目录,独立于 KV 索引和管理状态快照。每槽一个主记录,包含当前/上一条;正文作为 JSON 字符串存储并带摘要与修订。另存上一成功记录的独立备份。写入先刷新临时文件再原子替换,成功响应仅在主记录发布后发出。读取优先主记录当前,损坏时检查其中上一条,再检查独立备份;recovered=true 告知作者恢复到了可读成功记录。未发布临时文件从不当成已提交存档。每槽文件读取上限 4 MiB,记录数量和正文均有界。
写前/发布前检查取消及实例失效;失败保留最后可读成功记录,回收本次临时文件。主记录损坏且备份也无效时保留文件并拒绝,宿主不猜测字段、不执行作者迁移代码、不静默重置。校验用于意外损坏检测,不承诺抵御当前用户修改、存储设备故障或防作弊。
作者在重要操作后和低频检查点提交,等待成功后更新最后保存时间;关闭页面不能作为唯一保存时机。导出备份可通过现有可信 exports 接口保存读到的快照,网页仍不能指定路径。实际检查范围见 implementation.md。
可信存档管理
组件页抽屉在组件操作行下方展示“存档”栏,标题下同排放置“导出存档”和“清除”两个按钮,作用于整份安装存档,不依赖显示器或实例是否存在。安装全部实例停用后可操作,隐藏和断屏启用意图仍阻止管理。不展示槽位、修订、时间或恢复历史;无存档或存在损坏槽位时禁用导出,清除可重复执行。点击“清除”弹出包含组件名称及全部存档不可恢复提示的确认框,提供“清除存档”和“取消”,默认选择取消;取消或Esc关闭不清除,确认后才执行。
导出一次包含该安装全部槽位的当前有效正文,JSON为{widgetId,slots:{slot:value}},不附加宿主修订、时间、配置、绑定、权限或本机路径。复用既有有界exports暂存、可信系统保存选择、覆盖确认及同目录原子发布。读取中遇到不可恢复的损坏时拒绝导出并保留原文件;清除仍可用。导出文件属于用户,不随清除或卸载删除;用户取消或票据失效保留已有输出。
清除针对该安装的全部存档槽和内部恢复副本,不清除配置、实例KV、缓存、浏览数据或其他安装数据。Services取得安装存储锁,复核停用、取消及安装身份后,将存档目录原子移至固定清理目录作为提交点;提交前失败保留原存档,提交后不会恢复旧目录。随后回收清理目录,若回收失败明确提示存档已清除但残留待清理,后续访问尝试回收,清理目录不作为可读存档。不创建无限清理历史,重启也不读取已清除目录。
重新启用任一实例、卸载或清除存档会使在途管理导出失效。执行与最终文件投递均检查有效性,UI仅调用应用服务,不在窗口线程读写存档。清除后下次启用由作者按空存档初始化,宿主不执行作者迁移代码。
ContentDialog的按钮与Esc返回行为依据Windows App SDK官方文档;关闭不触发清除。
正常退出前的保存机会
正常退出应用先处理宿主未保存配置,再对最多 64 个已运行实例并行提供共同两秒的关闭机会;不会按实例累加等待。widgetHost.onClosing(async ({reason,remainingMs}) => {...}) 每文档最多登记一份处理器,返回解除函数;reason 当前仅 applicationExit,remainingMs 是剩余毫秒而非系统开机时间。处理器可等待自身已有保存,再提交最后快照,成功后 SDK 自动回应;抛错只标记未完成,不阻止退出。未登记的 SDK 页面立即回应,忽略或永不完成的页面在截止后释放。
底层 lifecycle.closing 事件含 token/reason/remainingMs,lifecycle.finishClose {token,success} 仅能回应当前文档的本次关闭;不能关闭或延长其他实例。关闭期间宿主桥接只允许 saves.、storage.、request.cancel 与关闭回应;取消其他在途调用及选择器,不开启新的宿主网络、账号、导出或系统操作。网页原生直接联网保持既有规则,直到运行资源释放,不承诺此阶段断网。截止后停止接受/执行新保存,已经进入原子发布的提交无法撤回,慢系统调用也不由此获得硬时延保证。宿主不等待 UI 线程上的同步保存、不提供作者退出否决权。
这不是唯一持久化机制,不保证系统强制结束、崩溃、关机/注销或超时页面最后一次保存。保存机会只覆盖正常退出;停用、热重载与版本切换依赖已提交检查点,没有额外关闭保存机会。隐藏保持运行及显式暂停语义不变。
运行时传入其实际包版本标识,候选预热窗口不得提交或恢复正式存档;Services 在捕获生命周期票据时核对当前激活版本,执行和发布前仍检查票据。网页不能在 params 中指定这个可信版本。
多实例读写与布局
所有同安装实例都能直接读写,宿主不设主副角色或自动接管。所有写入仍带 expectedRevision,安装锁串行原子提交,过期修订返回 SAVE_CONFLICT;重读、合并和业务协调由作者处理,不自动覆盖。
布局只保存各显示器的排布和配置。保存、覆盖、应用或恢复布局不复制、切换、回退或清空存档;组件包版本回退同样不回退存档。管理导出和清除要求该安装全部实例停用,重新启用任一实例即使在途管理操作失效。
共享的是 saves.* 数据;配置、实例 KV、权限、缓存与浏览数据保持各自归属。协作消息仅是可丢失的变更提示,不能代替成功存档。