Platform(平台)
| 项 | 内容 |
|---|---|
| 契约版本 | platform@1 |
| 当前状态 | 实现中 |
| 源码目录 | src/core/platform/ |
| 页面定位 | 宿主抽象与界面层参考:后端如何被选择、资源地址如何重定向、界面如何登记与导航 |
Platform 把「网页」与「Tauri 宿主」的差异收在一个抽象后面,并把外壳的界面层接口登记为大插件契约的一部分。它只记录状态与分发请求,画界面仍归外壳。
职责
负责:
- 宿主抽象:平台宿主信息、能力表、实现位选择、播放区句柄。
- 包内逻辑地址重定向:在 Asset 解析段内调整
pack来源。 - 界面层:界面条目登记与选择、导航栈、皮肤 registry。
- 外壳请求:把「打开存档/菜单/历史/设置」作为通知发出,不回调、不直接调用外壳函数。
不负责:
- 不判断 Tauri、操作系统或浏览器类型;运行环境只在 boot 判断一次,由
PlatformConfig.host传入对应实现。 - 不绘制界面、不实现模态层、不持有用户设置。
- 不实现输入路由、性能分级、无障碍与离线模式;这些属于后续阶段,未在当前根契约中预留空服务。
- 不读取资源字节;资源读取仍由 Kernel 与 Asset 完成。
对外接口
服务
| 名称 | policy | 说明 |
|---|---|---|
platform:host | closed | info() 查询宿主快照;select(name) 仅控制面可用,选择后续装载使用的后端;surface() 只读返回外壳播放区;language() 返回界面语言;fonts()(可选)安装章节字体;resolveAssetUrl({ vpath, as, source }) 执行包内地址重定向,默认原样返回 |
ui:navigator | trusted | open(name, params?) 压栈;back() 弹栈,栈底返回 false;current();subscribe(listener) |
机制插槽
| 名称 | policy | 说明 |
|---|---|---|
platform:resolve-asset-url | trusted | 输入 { vpath, as, source },输出包内来源 { scheme: 'pack', unit, version, path };目标由 Asset 按本次解析快照核对 |
通知
| 名称 | 载荷 | 触发时机 |
|---|---|---|
platform:on-capability-change | 与 info() 相同 | 生效后端变化,包括直接操作实现位、自动选中、撤销登记与单元卸载 |
platform:on-ui-request | { view: 'save' | 'menu' | 'history' | 'settings' } | 渲染器请求外壳打开对应界面;不携带回调,返回值不影响剧本 |
platform:on-asset-url-change | 无 | 重定向插槽的钩子登记、撤销,或生效宿主后端切换 |
ui:on-navigate | { from?, to } | 导航栈每次变化;只广播事实 |
实现位与 registry
| 名称 | 类别 | policy | 基数/默认 | 说明 |
|---|---|---|---|---|
host:backend@1 | 实现位 | public | single/builtin | boot 选定的宿主后端以固定名 builtin 作为内置条目;网页与 Tauri 各自一份,名字相同;卸载回落 fallback |
ui:screen@1 | 实现位 | public | multi/— | 条目 { screen, render(container, { params, navigate, back }) },以自己的实现名登记、声明所充当的界面名;卸载置空 |
ui:skins | registry | public | — | 条目 { name, tokens?, slices?, controls? };文件须为 assets/<ns>/skin/ 下的 .json,至少一项 |
ui:screen-defaults | registry | public | — | 条目 { screen, impl };数据包声明某个界面的默认实现,按装载层次取最后一条声明 |
关键语义
宿主信息与后端选择
info() 返回独立快照:
{
backend: string,
tier: 'web' | 'tauri-desktop' | 'tauri-mobile',
capabilities: Array<{
name: string,
sensitivity: 'T1' | 'T2' | 'T3',
available: boolean
}>
}调用方修改返回的数组或能力描述不会影响宿主原始能力表。能力不可用只表示事实,不表示宿主提供了替代实现。
选择顺序目前为:
- 显式调用
select(name)选中的条目; - 匹配宿主档的最早登记条目;
- 默认条目。
select(undefined) 清除显式选择;未知名称抛出 SlotError,现有选择保留。宿主后端的场景声明与选择持久化尚未接入。重复选择同一生效后端不重复广播;卸载未选中的条目不改变当前后端;选中条目卸载时回落 builtin 并广播。
platform:host.select 与实现位的 select 都属于控制面。装载单元经服务代理代替用户选择会被拒绝;登记一个后端或界面实现不授予替用户选择的权限。
装载期间的后端固定
Kernel 每次调用 load 时通过 Cordis 读取 host:backend@1 的当前条目,并在读取来源前固定该后端。本次准入、裁决后的重新判定、能力创建与模块执行都使用同一对象,分发包中的各单元也沿用本次选择。中途切换或撤销后端登记不改变这次装载;后续装载使用新的当前后端,已装载单元保留原有能力对象。
宿主条目由 HostBackend 定义:
| 成员 | 作用 |
|---|---|
name、tier | 实现名称与宿主档 |
capabilities() | 返回 { name, sensitivity, available }[] |
createCap(name, unit) | 为获准单元创建能力对象;具体契约由能力自身定义 |
importModule(code, label) | 执行已通过装载管线核验的 ESM,返回模块导出 |
language() | 返回界面语言标签;读取失败或为空时返回 und |
fonts()(可选) | 提供章节字体安装能力;缺失时 Text 跳过子集字体并继续使用原字体 |
网页与 Tauri 的差异实现分别在 src/host/web/ 与 src/host/tauri/。info().capabilities 报告 src/host/shared/capabilities.ts 声明的三项:caps:raw-bytes(可用)、caps:fs-read(可用,仅隔离环境)与 caps:net-fetch(不可用)。当前不向普通单元交出能力对象,createCap 一律报错;模块执行分别使用 blob URL 与原生自定义协议。
界面语言与字体安装是宿主适配层提供的普通查询和可选能力;核心层不直接读取 navigator 或 document.fonts。宿主无法报告语言时统一使用 und,没有字体安装能力时由 Text 发出提示并保留原字体。
包内地址重定向
执行位置固定在 Asset 五段管线的 ① 解析段:
- Asset 先经
asset:resolve-path与asset:resolve-source-order得出排好序的候选来源; - 对每个
pack候选经platform:host.resolveAssetUrl重定向; - 此后不再回看来源栈。
重定向目标可以是本次解析快照中任一已装载资源包的文件。包不在快照、版本不符、路径不合法或文件不存在,一律以 redirect-invalid 报错,不静默改读其他文件。
asset:acquire、asset:exists、asset:fingerprint 使用同一重定向;asset:list 仍枚举实际包内路径,不凭空生成别名。缓存键使用重定向后的实际目标;来源包或目标包卸载即令相关逻辑路径失效。Asset 收到 platform:on-asset-url-change 后保守清空资源缓存,并对已缓存路径发 asset:on-invalidate。
界面层
界面名与指令名写法相同,都是 <ns>:<名>。同一界面名下可以有多个条目,按「注册+选择」决定生效者:
- 用户为该界面选过、且目前已登记的实现;
- 数据包通过
ui:screen-defaults声明、且目前已登记的实现; - 内置实现;没有内置实现时,取该界面最早登记的实现。
插件登记条目不会自动替换内置界面。数据包的声明按装载顺序取最后一层,同一层取后写的一条;卸载后恢复前一层的声明。声明指定的实现未登记时继续向内置实现回落,不代替用户装载代码。
screens.ts 提供 candidates(screen)、resolve(screen) 与 picked(screen) 查询候选、生效实现和已记录的选择。choose(screen, name) 记录选择,传 undefined 清除;restore(screen, name) 供启动时恢复尚未登记的选择。这两个写入口仅控制面可用,装载单元经服务代理调用会被拒绝。
外壳长期保存用户的界面实现选择。所选实现卸载时先按默认序列显示,在设置里标「已卸载」,重新装载后恢复选择;直接用 choose 选择不存在或不充当该界面的条目仍报错。
导航语义:
open只接受已有条目的界面;参数须为对象,按结构化克隆复制后逐层冻结,不能克隆的值报错且栈不变;back()在栈底返回false;- 每次栈变化广播
ui:on-navigate,载荷只含{ from?, to }; - 外壳只挂载栈顶界面,切换时撤销旧界面;界面渲染抛错时清空失败容器、提示错误并尝试退回上一层,不把失败界面留在前台。
当前内置界面:
| 界面 | 登记方与行为 |
|---|---|
core:home | Platform 登记,外壳绘制;作为启动时的栈底,展示实际章节与存读档、独立设置入口 |
core:settings | Platform 登记,外壳绘制;包含声音、文字、画面、下载线路与账号五组,后两组置灰 |
core:story | ScriptPlay 登记;参数只收 { chapter: <章节 ID> } 或 { resume: true },挂载即播放,撤销即停播 |
core:story 参数缺失、两种参数并用或含其他字段时拒绝打开;读档由外壳先调用 Save,再以 { resume: true } 进入剧情。正常结束、未知指令停止和运行错误经 script:on-flow-end 通知外壳;主动停播不发该通知。完整载荷见 ScriptPlay。
存读档、播放菜单、历史面板与播放设置抽屉仍经 platform:on-ui-request 打开,不进导航栈。外壳的运行信息、启动状态、任务与单元列表归控制面,不随主页实现一起被替换;独立设置暂时隐藏这些区域,保留顶栏与底栏。
返回键
Esc 与已接入的 Android 物理返回键共用页面路由:常规模态层先关闭自身;剧情对话框隐藏时先恢复,自动/快进开启时先停止该模式,其余情况下打开播放菜单;其他界面退回上一层,栈底静默吸收。剧情通过菜单的「返回主页」离开,Android 返回键不调用 WebView 历史回退或退出应用。
准入裁决或信任管理页显示时,外壳截住 Android 返回键并通过已有通道转交信任页。裁决页只执行拒绝,管理页只关闭;底下的剧情、菜单与导航栈保持原状,返回键不会批准裁决。
皮肤
数据包登记 ui:skins 条目,资源包提供 JSON 与贴图。三份 JSON 都在 assets/<ns>/skin/ 下,逐项校验,不合格的项列入 rejected 并写降级日志,合格部分照常可用:
| 文件 | 内容 | 校验要点 |
|---|---|---|
tokens.json | 设计变量平表,或 { light: {…}, dark: {…} } | 只收白名单颜色变量,不含字体族;值只收 #rgb、#rrggbb、#rrggbbaa、rgb()、rgba();含 url(、var(、;、} 等写法一律拒绝 |
slices.json | 「皮肤名 → { image, slice, repeat? }」 | image 须在 assets/<ns>/skin/ 下;slice 为上、右、下、左 4 个 0–1024 整数;repeat 只取 stretch(默认)或 round |
controls.json | 「皮肤名 → 控件属性」 | 支持受校验的位置、尺寸、填充、图标、颜色、边框、圆角、内边距、字形、阴影、不透明度与层级;未知属性及非法值逐项拒绝 |
平表变量同时用于浅深主题;分主题写法的顶层只收 light、dark,只提供一种主题时,另一种保留内置配色。
皮肤名写作 <界面>.<控件>[.<实例>][:<状态>],状态取 hover、pressed、disabled、active、checked、playing。带状态的各级优先于不带状态的各级,例如:
settings.range.track.bgm:disabled
→ settings.range.track:disabled → settings.range:disabled
→ settings.range.track.bgm → settings.range.track → settings.range九宫格取最先命中的一项,贴图叠在控件原有底色上;半透明贴图不会清空底色,需更改底色时由 controls.json 的 background 显式指定。控件属性从一般到具体逐层叠加,只覆盖本层写出的属性,横屏与竖屏位置分别继承。内置控件的名称与状态集中在 src/shared/skin-names.ts 及其生成表中,页面通过 data-skin 标记控件。
controls.json 的主要约束:
| 属性 | 范围与行为 |
|---|---|
position | landscape/portrait 各自使用 { anchor, x, y };九个方位锚点,偏移 −512–512 CSS px;只写一种朝向时另一种保留内置排版;位置夹回父容器,列表项不接受位置 |
width、height | 1–2560 CSS px;改变外观尺寸,不缩小点击区域下限 |
fill、icon | 图片须在皮肤目录;填充方式为 cover/contain/stretch,图标只作用于带图标占位的控件 |
borderWidth、radius、padding | 边框 0–16 px;圆角与内边距 0–64 px,可用单值或四值 |
fontSize、fontWeight、textAlign | 字号 12–48 px;字重 400/700;对齐 left/center/right |
shadow、opacity、layer | 阴影偏移 −32–32 px、模糊 0–64 px;不透明度 0–1,交互控件套用时至少 0.6;层级为 0–9 整数 |
位置和尺寸按 每单位对应一个 CSS 像素 使用,不跟随 1280×720 舞台的等比缩放;外壳 90%/100%/110%/125% 的显示比例另作用于整个外壳。交互控件以透明区域补足至少 44×44 CSS px,不撑大排版尺寸;补足区位于控件自身内容之下,不遮挡卡片里的按钮,向外补出的部分仍可点击;相邻补足区重叠时上层控件优先。皮肤不改事件绑定、键盘顺序或移除焦点框,信任页等安全关键界面不读取皮肤。
默认皮肤、选择与降级
首次启动尚无皮肤选择记录时,外壳取 ui:skins 中局部名为 default 的条目。当前内置数据包登记「核心内置默认皮肤」,配套资源包提供三份 JSON 与贴图,与其他资源包使用同一 Asset 来源栈。
设置中的皮肤选择长期保存。显式选择「内置样式(不换皮)」后,下次启动仍不换皮;所选皮肤所属单元卸载时保留选择、暂按内置样式显示,再装载后恢复。
画面组恢复默认会清除皮肤选择记录,回到首次启动的默认皮肤选择;缺少默认皮肤时才使用内置样式。同时恢复浅色主题、100% 缩放与「跟随默认」的界面实现。内置皮肤已补齐对话框文字配色,并去掉工具条按钮的固定宽度,避免文字挤出按钮。
文件无法读取时整套皮肤回到内置样式;非法字段只拒绝该项;单张图片失败则略过依赖它的外观项并记日志。缺少默认皮肤或换皮失败都不阻止应用启动。切换或撤下皮肤会清除旧样式及图片地址;不支持的 CSS 选择器会跳过相应规则,不能据此声称旧浏览器已完整支持换皮。
当前实现范围
已完成:
- 宿主抽象双端骨架、能力快照、实现位选择与回落通知、宿主切换联动。
platform:resolve-asset-url与 Asset 的重定向、缓存失效衔接。ui:screen@1、ui:navigator、ui:skins的基础登记、导航与校验逻辑。
测试中:
- 导航外壳、主页/设置/剧情界面、数据包默认声明、用户选择保存与卸载回落已接入。
- 皮肤引擎已读取变量、九宫格与控件属性;内置皮肤已随内置包装载并在首次启动套用。
- Android 普通界面返回键与信任页转发已接入;已有系统 Edge 与 Android 的历史回归记录,验证范围见当前状态与测试,不代表 Phase 3 整体验收。
未实现:
- 输入路由、性能档、无障碍与离线状态未交付。
- 宿主后端的场景声明与选择持久化尚未接入。