- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
41 KiB
5. 数据契约、schema v2 与配置迁移
5.0 总则(必须遵守)
- 本章定义重构后的唯一数据契约。所有读写岛配置的代码(主进程
config.ts、岛渲染进程、编辑器、设置页、冒烟)必须只经过packages/shared/src/island/schema.ts的类型与normalize.ts/migrate.ts的函数,不得各自解析 JSON。 - 旧配置必须无损升级:
config.json现有的islandLayout(fixed/custom、schemes、nodes、expandedRows、residentEnabled、pointerFade、lessonReminders)与island(appearance 全字段)一个都不许丢;升级失败必须可回滚。 - 新增
appearance.family:取值'classisland' | 'winisland'。新装默认classisland;已存在配置的老用户升级默认winisland。fixed的语义随 family 分叉:classisland下 fixed = ClassIsland 信息行(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、不透明度 0.5、3px 进度条、消息行内气泡);winisland下 fixed = 原胶囊 + 详情卡。 islandLayout.version必须写入;当前版本2。版本号是唯一迁移判据,不得用"字段是否存在"猜版本(字段判断只作为迁移内部的兜底)。- 写入必须沿用现有
config.json的"内存缓存 + 120ms 合并写 + 退出前 flush"语义(packages/desktop-client/src/main/config.ts:235-346),不得为迁移新增同步写盘路径;迁移本身例外(必须一次写成功并立即 flush)。 - 客户端不得把 v2 数据写回给旧版本可见的字段后又不写版本号;写 v2 必须同时写
version:2,保证降级/回滚可判断。
5.1 落点与版本治理
| 数据 | 位置 | 版本字段 | 说明 |
|---|---|---|---|
| 运行期配置 | <userData>/config.json |
无独立文件版本 | 版本只在 islandLayout.version 上表达(唯一版本字段) |
| 岛布局 | config.json.islandLayout |
version(新增,当前 2) |
方案/行/节点/外观的结构版本;唯一迁移判据 |
| 岛外观 | config.json.island |
无独立版本 | 增加 family、theme、effectEnabled、effectScale、progressBar、fontSizes;style 保留为 surface 层 |
| 迁移前备份 | <userData>/config.json.v1.bak |
— | 迁移前完整拷贝,只保留一份(覆盖式) |
| 迁移标记 | <userData>/config.island-migration.json |
{from,to,at,appVersion} |
仅诊断用,不参与版本判断、不是第二套版本字段 |
- 只保留
islandLayout.version一个落盘版本字段:不得新增config.schemaVersion或其它文件级版本号;迁移判定只看islandLayout.version(缺失=v1)。 - 迁移必须在
app.whenReady()后、island.init()之前完成一次;不得由渲染进程触发,也不得在岛窗口创建后再迁移(否则窗口会先按旧数据渲染一帧)。 - 迁移成功后必须立即
flushConfigSync();失败时不得写回半成品:保留原config.json,删除.tmp,应用以内存默认 v2 运行并在设置页显示"迁移失败,已保留原配置"。 - 读到
islandLayout.version > 2时应用必须只读(不写回),并在设置页提示"配置来自更高版本"。
5.2 appearance v2(config.json.island)
在现有 IslandAppearance 基础上只增不改,旧字段全部保留(packages/shared/src/types.ts:1145-1195)。新增/语义变化如下:
| 字段 | 类型 | 默认值(新装) | 说明与依据 |
|---|---|---|---|
family |
'classisland' | 'winisland' |
'classisland'(老用户迁移为 'winisland') |
风格族字段的唯一落点;作用域只限 fixed(classisland=信息行;winisland=旧胶囊+详情);custom 始终 ClassIsland 规格、不受影响 |
colorScheme |
'light' | 'dark' | 'system' |
'system' |
岛配色模式:跟随应用主题(system,默认)/强制浅色/强制深色;只影响岛自身渲染,不改主窗口主题 |
theme |
'fluent' | 'classic' |
'fluent' |
仅 family='classisland' 的 fixed 使用;对应 ClassIsland Fluent/Classic 两套主题;具体 token 由 06 定义 |
effectEnabled |
boolean | true |
强提醒特效总开关(对应 ClassIsland 的提醒特效 Topmost/Effect 是否启用);关闭后只保留行内气泡/普通提醒 |
effectScale |
1 | 0.75 | 0.5 |
1 |
强提醒特效渲染缩放(对应 ClassIsland NotificationEffectRenderingScale,默认 1.0);只允许三档 |
progressBar |
{height:number, show:boolean} |
{height:3, show:true} |
岛内进度条统一配置:高度默认 3px,总开关默认显示;语义见 5.3 行级进度条说明 |
fontSize |
number | classisland=16(正文基准,派生 14/18/20 阶梯);winisland=13(旧语义) |
family='classisland' 时它是正文基准字号,fontSizes 由它派生;family='winisland' 保持旧语义;迁移时旧配置保留原值,新安装默认 16 |
fontSizes |
{secondary:number, body:number, emphasized:number, large:number} |
由 fontSize 派生:classisland 14/16/18/20;winisland ≈14/16/18/20(按 fontSize/13 比例) |
ClassIsland 四档字号;显式配置优先于派生(见 5.4) |
height / width |
number | 44 / 268 |
仅 family='winisland' 的胶囊尺寸;classisland 下不作为信息行尺寸 |
radius |
number | 20(winisland)/8(classisland 行为默认) |
classisland 下作为行默认圆角;解析时取 family 对应默认 |
opacity |
number | 1 |
岛整体不透明度;classisland 下行背景不透明度另见行设置(默认 0.5) |
winislandStyle |
'black' | 'glass' | 'tinted' |
'black' |
winisland 调色板字段的 canonical 名:纯黑/半透明/主题色渐变;仅 family='winisland' 的 fixed 使用 |
style |
'black' | 'glass' | 'tinted' |
镜像 winislandStyle |
兼容镜像字段(不删除):旧配置读入时若没有 winislandStyle 则用它;写出时同时写 winislandStyle 与 style,保持旧版本可读;建议 v3 再评估废弃 |
position/alwaysOnTop/animations/speed/idleSliver/marginX/marginY/followCursorDisplay/accent |
不变 | 不变 | fixed(两种 family)与 custom 共用;classisland fixed 也用停靠与边距 |
- 字段职责必须严格区分:
family只决定 fixed 用哪套渲染器(ClassIsland 信息行 / WinIsland 胶囊);winislandStyle(兼容镜像style)只决定 winisland 的调色板;theme只决定 classisland fixed 的 Fluent/Classic;colorScheme只决定浅色/深色/跟随。四者不得互相覆盖、不得合并。custom不使用family/theme/winislandStyle(始终 ClassIsland 规格),但仍读取colorScheme决定明暗。 family归一化:只接受两个值,非法值回落'classisland'(新装默认);迁移函数负责把老用户写成'winisland'(见 5.4)。colorScheme归一化:只接受light/dark/system,非法回落system;缺失补system。fontSize归一化:8–96;classisland 下作为正文基准,fontSizes缺失时由它派生(body=fontSize,secondary=round(fontSize*14/16),emphasized=round(fontSize*18/16),large=round(fontSize*20/16));winisland 下保持旧语义(fontSizes缺失时按fontSize/13 × 14/16/18/20推导)。迁移时旧fontSize原值保留,不强行改成 16。fontSizes归一化:每项 8–96;显式值优先于派生;缺失项按第 4 条补齐。theme仅 classisland fixed 生效;非法值回落'fluent';winisland下忽略但不删除该字段。effectEnabled必须为 boolean(缺省 true);effectScale必须夹取到[1, 0.75, 0.5](缺省 1,非法值取 1);progressBar.height夹取 1–8、progressBar.show缺省 true;progressBar缺失时整对象补默认。winislandStyle归一化:仅black/glass/tinted合法,非法回落black;style作为镜像字段同步写;读入优先winislandStyle,回退style。切换 family 时保留用户winislandStyle原值,不重置(UI 默认值提示归 09)。
5.3 islandLayout v2(config.json.islandLayout)
v2 顶层(IslandLayoutV2):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
version |
number | 2 |
必须存在 |
layout |
'fixed' | 'custom' |
'fixed' |
布局模式(沿用旧字段) |
fixed |
FixedSchemeV2 |
见下 | classisland fixed 信息行配置;winisland 下忽略 |
schemes |
IslandLayoutSchemeV2[] |
1 个默认方案 | 自定义方案(沿用旧结构并扩展) |
activeSchemeId |
string | 'default' |
当前方案 |
residentEnabled |
boolean | layout==='custom' |
兼容字段;不得再作为独立开关 |
pointerFadeEnabled / pointerFadeReversed |
boolean | true / false |
鼠标避让淡化(沿用) |
lessonReminders |
LessonReminderSettings |
沿用旧默认 | 准备/上课/课间/结束/课中进度开关与秒数 |
updatedAt |
number | 0 |
最近保存时间(仅诊断) |
FixedSchemeV2(classisland fixed 的信息行):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
rows |
IslandRowV2[] |
1 行 = 日期 + 课程表 | 信息行;默认只有一行 |
mainRowId |
string | 首行 id | 主行(承载消息气泡/课程提醒) |
rowHeight |
number | 40 |
ClassIsland 行高(32–64 夹取) |
rowSpacing |
number | 5 |
行间距(0–150) |
backgroundOpacity |
number | 0.5 |
行背景不透明度 |
cornerRadius |
number | 8 |
行圆角(0–20) |
progressBarHeight |
number | 3 |
行内进度条高度(3px) |
messageBubble |
'inline' | 'card' |
'inline' |
fixed 下消息表现:inline=行内气泡,card=老式详情卡(仅回退用) |
maxRows |
number | 3 |
信息行最多行数(自动换行/溢出策略由 07 定义) |
IslandLayoutSchemeV2(在 v1 方案上扩展):
| 字段 | 类型 | 默认值 | 变化 |
|---|---|---|---|
id/name/rows/expandedRows |
沿用 | 沿用 | 结构不变;行内容升级为 IslandRowV2 |
scale |
number | 1 |
0.75–2.5(沿用于 CLASSISLAND_ISLAND_METRICS.scaleMin/Max) |
color/foreground/opacity/radius/separated/spacing |
沿用 | 沿用 | radius 0–20;spacing 0–150 |
rowHeight |
number | 40 |
新增:行高 32–64 |
rowSpacing |
number | 5 |
新增:与 spacing 同步;spacing 保留兼容 |
theme |
'fluent' | 'classic' |
继承 appearance.theme |
新增:方案级主题覆盖 |
fontSizes |
{secondary,body,emphasized,large} |
继承 appearance.fontSizes |
新增:方案级字号覆盖 |
progressBarHeight |
number | 3 |
新增:行内进度条高度 |
unrecognizedNodes |
object[] |
[] |
新增:未知 kind 的原始节点(不得丢弃,见 5.5 第 6 条) |
IslandRowV2(行):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
id/name/main/allowNotifications/gap/nodes |
沿用 | 沿用 | main 行唯一;gap 0–64 |
appearance |
RowAppearance |
见下 | 行级外观(新增) |
hideRules |
RowHideRules |
全 false | 行级隐藏规则(新增) |
progress |
RowProgress |
见下 | 行级进度条(新增) |
RowAppearance:backgroundEnabled=false、backgroundColor='#000000'、backgroundOpacity=0.5、foregroundColor='#ffffff'、cornerRadius=8、opacity=1、fontSizes(继承方案/外观)。
RowHideRules:inClass=false、noMessage=false、noClass=false、fullscreen=false;全部为 false 表示不隐藏。语义对应 ClassIsland 的行隐藏规则(repos/ClassIsland/ClassIsland.Core/Models/Components/MainWindowLineSettings.cs 的 HideOnRule/HidingRules)。
RowProgress:enabled=true、style='line'、height=3、color='accent'、showRemaining=true。
行内 3px 进度条的语义(数据来源不在本层实现):进度条表示"当前显示内容的时间进度"——消息气泡显示时 = 该消息剩余显示时间;否则 = 当前课程进度。具体数据来源与取值规则由 07/08 定义,本 schema 只保证 progress(行级)与 appearance.progressBar(全局)提供 height/show 两个开关;行设置抽屉的完整字段由 09 定义,本章只登记字段名(appearance/hideRules/progress)并要求与 09 对齐,不得在 05 里发明抽屉字段。
IslandNodeV2(节点,在 v1 IslandComponentNode 上扩展,字段名不变):
| 字段 | 变化 |
|---|---|
id/kind/appearance/advanced/children |
沿用(appearance 四档字号默认改为继承行/方案) |
settings.schedule.display |
沿用 `'current' |
settings.schedule.progressStyle |
新增:`'bar' |
settings.schedule.showProgress |
沿用;默认 true |
settings.schedule.showPeriod/count |
沿用 |
settings.mode(仅 kind='rolling') |
新增:`'continuous' |
advanced.hideRules |
沿用 in-class/no-message/no-class;新增 fullscreen(与行级一致) |
advanced.rowSpan |
新增:仅容器组件使用,默认 1 |
默认方案(新装/重置):layout:'fixed'、family:'classisland'、fixed.rows=[{main:true, allowNotifications:true, gap:12, nodes:[date, schedule]}],schemes=[默认方案:main 行含 date/clock/schedule/notification]。数值依据:packages/shared/src/island-layout.ts:217-255(现有默认方案)+ CLASSISLAND_ISLAND_METRICS(:19-36)。注意:schemes 是 custom 布局数据,描述 ClassIsland 规格且不随 family 改变;fixed 配置只在 family='classisland' 时由 fixed 渲染器读取。
5.4 v1 → v2 迁移算法(必须逐步实现,可重入)
输入判定:
- 读取
config.json(不存在 → 全新安装:直接构造 v2 默认,family='classisland',不写迁移标记)。 - 文件存在但
islandLayout.version缺失:视为 v1(唯一版本判据;不得再引入schemaVersion作为第二套版本号)。 - 老用户判定:
islandLayout.version缺失 或 文件里存在islandLayout/island任一字段 → 迁移时family='winisland'(保持原外观,不惊吓用户)。 - 全新安装判定:文件不存在、或既无
islandLayout又无island→family='classisland'。
步骤(必须按序、每步失败可中止):
step 0 备份:把 config.json 原样复制到 config.json.v1.bak(存在则覆盖);写 config.island-migration.json {from,to,at}(仅诊断,不是版本字段)
step 1 外观迁移(appearance):
- family := 'winisland'(老用户)| 'classisland'(新装)
- colorScheme := 'system'(缺失补默认;已有值原样保留)
- winislandStyle := 旧 style(缺省 'black');style := winislandStyle(兼容镜像)
- theme := 'fluent'(新装默认;老用户也写 'fluent',仅 classisland fixed 生效)
- effectEnabled := true(缺失补默认)
- effectScale := 1(缺失补默认;只允许 1/0.75/0.5)
- progressBar := { height:3, show:true }(缺失补默认)
- fontSize:迁移保留旧值(classisland 新装写 16;老用户写原值)
- fontSizes := 由 fontSize 派生:
老用户(winisland):secondary=round(fontSize*14/13), body=round(fontSize*16/13),
emphasized=round(fontSize*18/13), large=round(fontSize*20/13)
新装(classisland):14/16/18/20(fontSize=16 为正文基准)
- 所有旧字段原样保留(height/width/radius/opacity/accent/fontSize/style/...)
step 2 布局迁移(islandLayout):
- version := 2(唯一落盘版本字段)
- layout := 旧 layout(缺省 'fixed')
- residentEnabled := (layout==='custom')(覆盖旧的独立值,保证一致)
- pointerFadeEnabled/pointerFadeReversed/lessonReminders 原样保留
- fixed := 若旧文件无 fixed 字段 → 生成 FixedSchemeV2 默认(1 行 date+schedule;progressBar 继承 appearance.progressBar)
- schemes := 旧 schemes 逐个升级:
rowHeight := 40;rowSpacing := 旧 spacing;theme := 'fluent';
fontSizes := 继承 appearance.fontSizes;progressBarHeight := appearance.progressBar.height;unrecognizedNodes := []
每行补 appearance/hideRules/progress 默认;
每节点补 settings.schedule.progressStyle='line'、advanced.rowSpan=1;
若节点 kind='rolling' 补 settings.mode='continuous'(pause 保留旧 interval 语义)
- 未知 kind 的原节点移入该方案的 unrecognizedNodes(保留原始 JSON,不参与渲染)
step 3 校验与夹取:调用 normalizeIslandLayoutV2 / normalizeAppearanceV2(见 5.5);
校验失败项收集成 warnings,不得中止;结构性错误(schemes 为空等)用默认方案兜底
step 4 写回:saveConfig({ islandLayout, island }) 后立即 flushConfigSync()(**不写 schemaVersion**)
step 5 读回校验:重新读取并 deep-equal 比较关键字段(islandLayout.version/family/layout/方案数/节点数);
不一致 → 从 config.json.v1.bak 恢复、删除迁移标记、报错并降级为"只读默认 v2"
- 可重入/幂等:若
islandLayout.version===2,迁移函数必须直接返回原对象(不写盘、不改字段);重复执行不得产生二次修改。 - 老用户 fixed 不变:迁移后
family='winisland',fixed 仍渲染原胶囊+详情卡(ClassicIsland.vue),因此老用户升级后视觉 0 变化;只有用户主动在设置页切到classisland或新装用户才看到信息行。 - custom 老用户:
custom始终按 ClassIsland 规格渲染、不受 family 影响;迁移只升级数据(补行级默认、滚动 mode 等),不改 schema 结构。老 custom 用户升级后视觉会变为 ClassIsland 行规格(这是定稿行为),发布说明与设置页必须提示;fixed 用户(默认 winisland)不受影响。 - 编辑器打开时必须先读迁移后的 v2;若
islandLayout.version不匹配(例如用户用旧版应用改过文件),必须触发一次"再迁移"而不是把 v1 当 v2 解析。 - 迁移必须记录 warning 列表到内存并在设置页"存储/诊断"可查看(哪几项被夹取、哪几个未知组件被保留)。
5.5 校验与夹取(normalizeIslandLayoutV2 / normalizeAppearanceV2)
必须在 shared 中实现,主进程保存前、编辑器加载后、IPC 存入前三处共用同一个函数。规则:
version:仅接受2;缺失/非法按迁移处理,不得静默改成 2 后继续用旧结构。layout:'fixed'|'custom',非法回落'fixed'。family:'classisland'|'winisland',非法回落'classisland'。- 数量上限:行 ≤12/方案、节点 ≤200/全部方案、嵌套深度 ≤6、方案 ≤20;
main行唯一(多余取消)。 - 数值夹取:
rowHeight 32–64、rowSpacing 0–150、gap 0–64、radius 0–20、opacity 0–1、backgroundOpacity 0–1、fontSizes 8–96、scale 0.75–2.5、rowSpan 1–6、progressBarHeight 1–8;非法/NaN 回落默认值(禁止Math.max(min, NaN)这种写法,参考src/main/island.ts:2225-2241的 fallback 说明)。 - 未知 kind:不得丢弃。把原始节点 JSON 放入
scheme.unrecognizedNodes(最多 200 条),渲染时忽略并显示"未知组件(已保留)";这样用旧版应用打开再保存也不会丢数据。 - 颜色:统一
#rrggbb小写;非法回落默认;透明度字段独立,不把#rrggbbaa塞进颜色字段。 - id:空/非字符串/重复 → 重新生成(
node-<n>/row-<n>/scheme-<n>);id 最长 100 字符。 - 文本:
settings.text≤2000 字符、locationName/countdownTitle≤64;超长截断并记 warning。 - 经纬度:
latitude -90..90、longitude -180..180,可空;空表示未配置(组件显示"请设置天气地点")。 - 未知字段:v2 对象里出现但类型未定义的字段必须原样保留在对象上(不删除),以便前向兼容。
lessonReminders:prepareSeconds 5–1800、endingSeconds 5–600(依据现有packages/shared/src/island-layout.ts:390-398)。winislandStyle/style:两者都只接受black/glass/tinted,非法回落black;读入优先winislandStyle、回退style,写出两者同步(兼容镜像);不得因 family 迁移而删除style;family 与调色板相互独立(family 决定 fixed 用哪套渲染,winislandStyle 决定 winisland 的调色板)。theme:只接受fluent/classic,非法回落fluent;具体色板/圆角/阴影 token 由 06 定义,05 只登记字段与默认值。colorScheme:只接受light/dark/system,非法回落system;system时跟随应用html.dark状态决定岛明暗(映射逻辑由渲染层实现,数据层只存枚举)。fontSize:8–96,缺省按 family 取(classisland=16 正文基准、winisland=13 旧基线);fontSizes缺失时按 5.2 第 4 条派生;显式fontSizes优先;迁移不得覆盖旧fontSize。effectEnabled:boolean,缺省true;effectScale:只接受1/0.75/0.5,缺省1,非法取1;progressBar:{height:1–8, show:boolean},缺省{3,true}。- 进度条高度优先级:
行 progress.height>方案 progressBarHeight>appearance.progressBar.height;任一缺失沿上一级回落,最终默认 3。progressBar.show=false时所有行都不显示(行级enabled=false只关该行)。 - 滚动组件:
settings.mode只接受continuous/pause,缺省continuous;pause时停顿秒数复用settings.interval(夹取 1–300);continuous时speed夹取 1–200(沿用现有范围)。 - 渲染/样式命名空间只由 layout(fixed 的 family)决定,不写入数据:
fixed + family='winisland'→ winisland 命名空间;fixed + family='classisland'→ classisland 命名空间;custom始终 classisland 命名空间、不受 family 影响;normalize 不得改写样式类名/选择器相关字段。 IslandNotification.level、IslandLessonReminder.progress/leftSeconds、IslandState.family/pendingStaged/bubblePhase/bubbleDeadline均为运行期派生字段,normalize 不处理、不落盘。
5.6 DTO 映射(ClassHelper 数据 → ClassIsland 语义)
fixed(classisland)行内气泡规则:同一时刻只显示 1 条 active 气泡;点击展开详情/操作;有多条未处理消息时不并列显示,而是折叠为「还有 N 条」,点击打开应用内通知中心(沿用 ClassHelper 现有通知中心路由);消息队列、去重、上课暂存、已读回执规则完全不变(02 §2.5/§2.12)。
IslandState → ClassIsland 主界面状态:
| ClassHelper 字段 | ClassIsland 语义 | 渲染映射(classisland fixed) |
|---|---|---|
mode:'hidden' |
岛不可见(仅提醒特效时临时显示) | 信息行隐藏/仅保留细缝 |
mode:'pill' |
紧凑态 | 行内气泡/铃铛提示 |
mode:'expanded' |
展开态 | 行展开 + 消息面板 |
active/queued |
提醒队列(当前播放/待播) | 行内气泡列表,按 PRIORITY_RANK 排序 |
inClass |
TimeState=OnClass |
行隐藏规则 inClass 生效、提醒遮罩可用 |
currentPeriodEnd |
当前时间点结束 | 进度条/倒计时取值 |
reason |
提醒来源(新消息/课后补发/紧急/叫人/收回中) | 气泡入场动画与角标 |
listExpanded |
列表是否展开更多 | 行内"展开更多" |
lessonReminder |
课级提醒(本地) | 行覆盖层(遮罩/秒数徽标) |
coursewareTransfer |
课件提示(覆盖态) | 最高优先级覆盖全部行 |
IslandNotification → ClassIsland 提醒语义:
| ClassHelper 字段 | ClassIsland 对应 | 规则 |
|---|---|---|
priority: LOW/NORMAL/HIGH/URGENT |
提醒队列优先级(NotificationPriority) |
URGENT=插队立即播放;HIGH/NORMAL/LOW 按排序入队 |
kind:'notification' |
普通提醒(Mask+Overlay 两阶段) | 两阶段时长取 5s/5s(repos/ClassIsland/...NotificationRequest.cs:17-20) |
kind:'homework' |
作业提醒(本地合成 id homework-<id>) |
不进入已读回执;不可"标为已读" |
kind:'call' |
叫人(Call) | 上课时保持可点、不自动展开;紧急叫人按 URGENT |
title/content/teacherName/courseName/createdAt |
提醒标题/正文/来源/时间 | 气泡第一行标题、第二行来源+时间 |
id |
去重键 | 双链路(通知/叫人)合并时 id 相同即合并(见 02 §2.5 第 3 条) |
ScheduleDto → ClassIsland 课表语义(仅映射,不落 Profile 文件):
| ClassHelper 字段 | ClassIsland 概念 | 映射规则 |
|---|---|---|
id |
ClassInfo/条目标识 |
仅内部使用 |
course.courseName |
Subject.Name |
直接展示;null 显示"—" |
dayOfWeek(1–7) |
TimeRule.WeekDay |
1=周一…7=周日 |
startTime/endTime(HH:mm) |
TimeLayoutItem.StartTime/EndTime |
转 HH:mm:00;只用于显示与进度计算 |
weekStart/weekEnd |
周次范围 | 用于"本周是否有课" |
weekParity: ALL/ODD/EVEN |
TimeRule.WeekCountDiv(0/1/2)+ WeekCountDivTotal=2 |
ALL→0;ODD→1;EVEN→2 |
location |
附加信息 | 仅在气泡/详情展示,不参与时间点 |
IslandTimePointDto → TimeLayoutItem:
| ClassHelper | ClassIsland | 规则 |
|---|---|---|
timeType: LESSON/BREAK/DIVIDER/ACTION |
TimeType: 0/1/2/3 |
一一对应(repos/ClassIsland/...TimeLayoutItem.cs:119-138) |
name(课间名/时间点名) |
BreakName/名称 |
课间空时显示"课间休息" |
courseName |
默认课程 | null 等价 Guid.Empty(无课) |
startTime/endTime |
开始/结束 | 用于行内课程表折叠与进度 |
isHideDefault |
IsHideDefault |
折叠时隐藏、轮到时展开 |
- 上述映射只用于渲染语义对齐,不得把
ScheduleDto落盘成 ClassIsland 档案;数据仍以服务端解析结果为准(02 §2.12)。 CoursewareTransfer映射为"最高优先级覆盖态",不进入提醒队列、不参与已读;映射字段name/category/teacherName/receivedBytes/totalBytes/progress/stage/error直接使用(packages/shared/src/courseware.ts、src/island/CoursewareTransfer.vue:12-65)。
运行期状态扩展(IslandState,不落盘,必须随状态推送):
| 字段 | 类型 | 默认值 | 语义 |
|---|---|---|---|
family |
'classisland' | 'winisland' |
当前 appearance.family |
当前生效风格;渲染进程据此切换 CSS 命名空间 |
pendingStaged |
number | 0 |
上课期间被暂存、下课后待补弹的消息数(对应现有 queued 中因上课被压住的数量;queued.length 不变) |
bubblePhase |
'idle' | 'mask-in' | 'content-in' | 'out' |
'idle' |
行内气泡/提醒的两阶段播放阶段(固定 5s/5s 时长的可视化映射) |
bubbleDeadline |
number | 0 |
当前气泡自动收起的时间戳(毫秒);0 表示无 |
IslandState的新字段只存在于运行期推送(island:state),不得写进config.json、不得被编辑器保存;旧渲染进程缺字段时必须按默认值渲染(前向兼容)。pendingStaged必须由主进程根据现有上课暂存逻辑计算(02 §2.5 第 2 条),不得让渲染进程自行推断。
IslandNotification.level(展示层级,派生字段):
priority |
level |
用途 |
|---|---|---|
URGENT |
'emergency' |
紧急气泡/强提醒特效可触发 |
HIGH |
'high' |
高亮气泡/角标 |
NORMAL |
'normal' |
普通气泡 |
LOW |
'low' |
弱化展示 |
level 由 priority 派生,不落库、不持久化;通知进入岛队列时在 main/island/messages.ts 统一计算,渲染层只读取。
IslandLessonReminder 扩展字段(运行期,不落盘):
| 字段 | 类型 | 语义 |
|---|---|---|
progress |
number (0–1) | 当前提醒/课程进度:ending/in-class = (now-lessonStartsAt)/(lessonEndsAt-lessonStartsAt);其余 = (endsAt-now)/(endsAt-startedAt)(与现有 LessonReminderContent.vue:36-50 计算一致) |
leftSeconds |
number | 下课前剩余秒(ceil((lessonEndsAt-now)/1000),clamp ≥0),供蓝色圆形秒数徽标与遮罩标题使用;现有 LessonReminderContent.vue:24-35 已有同义计算 |
kind |
不变 | 仍为 prepare/on-class/break/ending/in-class |
fixed(classisland)下的提醒叠加口径:prepare/on-class/break 以行内容接管呈现(临时替换课程表内容,动画由 08/10 定义);ending 不接管行内容,只在课程名右侧显示蓝色圆形秒数徽标(leftSeconds 驱动);custom 模式不接管,保持课程组件 + 秒数徽标。该口径只影响渲染,不改变 02 §2.8 的五类提醒计算与开关语义。
5.7 IPC payload 版本与兼容
- IPC payload 可以携带
schemaVersion:2仅用于运行时校验/调试,不得落盘、不得作为配置版本判据;配置版本只认islandLayout.version。island:state/island:appearance/island:get-layout三个高频 payload 建议带上该字段(旧字段保留,新增字段可选)。 - 接收方规则:未知字段忽略、缺字段回落默认;不得因为缺
family就拒绝渲染(老渲染进程遇到新主进程时应按winisland降级,反之按classisland)。 island:save-layout必须返回归一化后的 v2 结果(编辑器以返回值为准,避免"本地看着对、主进程夹取后不一致")。- 主进程
getConfig()返回给渲染进程的islandLayout必须是 v2;island必须含family与新增外观字段(theme/effectEnabled/effectScale/progressBar)。 - 旧通道名全部保留(见 04 §4.4);新增通道必须同时在
shared/island/ipc.ts与 preload 中登记,不得字符串散落。 - 作业指纹相关 payload(见 04 §4.3.13)必须带
source(realtime/homework-local/replay/call)与dedupeKey;主进程按 key 去重,渲染进程保留现有"本机刚录作业免上岛"快速路径。
5.8 编辑器预览与临时状态(不持久化)
- 编辑器的内嵌预览、缩放、选中项、拖拽态属于临时状态,不得写入
config.json;只有"应用/保存"才写 v2。 - "无消息展开内容(expandedRows)"与常态
rows分开编辑、分开保存;activeSchemeId只在切换方案时写。 - 设置页"测试灵动岛"仍走
islandPush+context.preview=true(30 秒预览、失焦不收起,见 02 §2.15),预览消息 id 必须带前缀(如appearance-test-)以便清理;预览不写历史、不改已读。 - 编辑器打开时若检测到迁移 warning,必须在顶部显示"部分旧设置已按新格式调整(可查看详情)",不得静默。
5.9 回滚与降级
- 回滚入口:设置页"灵动岛 → 恢复升级前布局"必须存在;回滚步骤:先备份当前
config.json→ 把config.json.v1.bak覆盖回去(该文件天然没有islandLayout.version,即 v1)→ 提示重启 → 重启后旧版本行为恢复(family=winisland/fixed=胶囊)。 - 迁移失败降级:应用不得崩溃;以内存默认 v2 启动(不写盘),岛显示默认信息行,设置页显示错误与"恢复备份"按钮。
- 前向兼容:读到
islandLayout.version>2时,应用只读该配置(不覆盖写),在设置页提示"配置由更高版本创建";不存在其他版本字段可用于判断。 - 文件损坏:
config.json非法 JSON 时沿用现有.bak回退逻辑(config.ts现状:读取失败回落默认),回滚后仍必须按islandLayout.version缺失执行一次迁移判定;不得用损坏文件覆盖.bak。 - 旧版应用打开新文件:不可避免(旧版不认识 v2 字段),因此 v2 必须保持"未知字段保留"原则(5.5 第 11 条)并把关键新数据放在旧版会忽略而不是报错的位置;旧版保存后新字段可能丢失,需在文档提示用户不要跨版本反复切换。
5.10 版本变更记录与兼容矩阵
必须维护一张可追溯的变更表(代码里以常量数组或文档形式存在),用于判断"某个字段从哪版开始有、哪版废弃":
| 变更 | 版本 | 兼容策略 |
|---|---|---|
islandLayout.version 新增(唯一落盘版本字段) |
v2 | 缺失=1,按 5.4 迁移 |
appearance.family 新增 |
v2 | 缺失时:老配置迁移=winisland;新装=classisland |
appearance.theme(fluent/classic)新增 |
v2 | 缺失=fluent;仅 classisland 生效;具体 token 由 06 定义 |
appearance.effectEnabled / effectScale 新增 |
v2 | 缺失 true/1;effectScale 只允许 1/0.75/0.5 |
appearance.progressBar {height,show} 新增 |
v2 | 缺失 {3,true};高度优先级 行 > 方案 > 外观 |
appearance.fontSizes 新增 |
v2 | 缺失时按旧 fontSize 比例推导(5.4 step1) |
fixed(信息行)新增 |
v2 | 缺失时生成默认 1 行(date+schedule);仅 classisland fixed 使用 |
行级 appearance/hideRules/progress 新增 |
v2 | 缺失时补默认;字段名与 09 对齐;winisland fixed 下保留数据但忽略(custom 始终使用) |
方案级 rowHeight/rowSpacing/theme/fontSizes/progressBarHeight 新增 |
v2 | 缺失时继承 appearance 默认 |
appearance.colorScheme 新增 |
v2 | 缺失=system;非法回落 system |
appearance.fontSize 语义按 family 分叉 |
v2 | classisland=正文基准(新装 16,派生 14/18/20);winisland=旧语义(13);迁移保留旧值 |
appearance.winislandStyle 新增,style 转兼容镜像 |
v2 | 读优先 winislandStyle、回退 style;写两者同步;style 不删除;切换 family 保留原值 |
滚动组件 settings.mode 新增 |
v2 | 缺失=continuous;非法回落 continuous;pause 复用 settings.interval |
residentEnabled 转纯兼容 |
v2 | 读出后与 layout==='custom' 对齐;不再作为独立开关 |
family 作用域 |
v2 | 只影响 fixed;custom 始终 ClassIsland 规格、不受 family 影响 |
旧 style 字段 |
保留镜像 | 本轮不删除;与 family/winislandStyle 独立;建议 v3 再评估废弃 |
迁移标记文件 config.island-migration.json |
2 | 仅诊断用途;不是版本字段、不参与运行逻辑 |
回滚文件 config.json.v1.bak |
2 | 迁移前覆盖写;回滚后仍保留,供二次回滚 |
兼容矩阵(读写方向必须明确):
- 新读旧(v2 代码读 v1 文件):允许,走 5.4 迁移;迁移后内存中只存在 v2。
- 新读新:允许,走 normalize;
islandLayout.version更高时只读。 - 旧读新(旧代码读 v2 文件):旧代码会忽略未知字段继续运行(因为 v2 把新字段放在其不认识的键上),但旧代码保存后会丢新字段;因此必须在文档与发布说明中写明"不要用旧版应用反复编辑新配置"。
- 新写旧:禁止。v2 代码写盘必须带
islandLayout.version:2,不得输出 v1 结构,也不得写任何文件级版本号。 - 迁移与编辑器并发:编辑器打开时若检测到
config.json被迁移,必须重新加载(比较updatedAt/mtime或读取返回值),不得用旧草稿覆盖迁移结果;保存前再次getConfig核对islandLayout.version。
兼容性红线(不得违反):
- 不得删除任何仍被旧配置使用的字段名(如
style、residentEnabled、spacing、expandedRows、pointerFadeEnabled)。 - 不得把对象结构从"方案数组"改成"对象字典"或反之(旧编辑器/旧冒烟会解析失败)。
- 不得改变
ISLAND_LIST_METRICS与ISLAND_SIZE_DELTA的语义(可以新增 classisland 专用 metrics,但旧常量必须继续导出且数值不变),因为大量冒烟断言按旧值计算(packages/shared/src/constants.ts:344-351,379-402)。 - 不得让 v2 的
rows与 v1 的rows在同一键上结构不兼容:v2 行只增加字段,不重命名已有字段。
5.11 验收标准(第 5 部分)
- 新装启动后
config.json不含任何schemaVersion;含island.family:'classisland'、island.colorScheme:'system'、island.fontSize:16、island.theme:'fluent'、island.effectEnabled:true、island.effectScale:1、island.progressBar:{height:3,show:true}、islandLayout.version:2、fixed.rows默认 1 行(日期+课程表);layout:'fixed',界面呈现 ClassIsland 信息行。 - 用一份旧 v1 配置(fixed、custom 各一份,含 schemes/nodes/appearance/residentEnabled)启动:迁移成功、无字段丢失、
family:'winisland'、旧胶囊外观不变、fontSize保留原值、style/winislandStyle一致;config.json.v1.bak存在。 - 迁移后再次启动:不重复迁移、不重复写盘(可用文件 mtime/内容哈希断言幂等)。
- 迁移失败模拟(把
config.json改坏/只读)时:原文件不被破坏、应用以默认 v2 启动、设置页显示错误与恢复入口。 - "恢复升级前布局"可把 v1 备份恢复回来并重启生效;恢复前自动再备份当前配置。
normalizeIslandLayoutV2对行数>12、节点>200、深度>6、方案>20、非法颜色/数值/kind 全部夹取或保留到unrecognizedNodes;不崩溃、不丢未知组件。- 老用户切到
classisland后 fixed 变信息行、切回winisland恢复胶囊;family 切换时style/winislandStyle与fontSize保持用户原值不变;custom 始终 ClassIsland 规格、切换 family 不改变 custom 渲染。 - 编辑器中常态行/展开行分开保存;应用后
islandLayout为 v2 且island:save-layout返回值与本地一致。 IslandState/IslandNotification/ScheduleDto/IslandTimePointDto的映射表逐项实现:优先级、上课态、时间点类型、单双周、作业/叫人特殊语义均可按 5.6 追踪。- IPC 兼容:旧通道全部可用;payload 可选带
schemaVersion仅作校验(不落盘);缺字段时按 family 降级渲染而不是报错。 - 编辑器/设置页不把预览、缩放、选中项写入配置;预览消息不写历史、不改已读。
- 读到
islandLayout.version>2时应用只读并提示,不得覆盖用户文件。 - 外观新字段:关闭
effectEnabled后强提醒特效不再渲染(行内气泡/普通提醒仍工作);effectScale只接受 1/0.75/0.5 并实际改变特效尺寸;progressBar.show=false时所有行进度条隐藏,行级高度覆盖方案/外观。 - 运行期状态:
IslandState含family/pendingStaged/bubblePhase/bubbleDeadline且不写入 config.json;上课暂存时pendingStaged与队列数一致;气泡阶段按idle→mask-in→content-in→out推进。 - 派生字段:
IslandNotification.level按URGENT→emergency/HIGH→high/NORMAL→normal/LOW→low生成且不落盘;IslandLessonReminder.progress在 0–1、leftSeconds与蓝色圆形徽标一致(剩余 1 秒显示-1)。 - fixed(classisland)下
prepare/on-class/break以行内容接管呈现、ending只显示秒数徽标;custom 下不接管(课程组件 + 徽标);两种 family 的提醒开关语义一致。 - fixed 行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;队列/去重/暂存规则不变。
family='winisland'只影响 fixed(保持现有胶囊观感);custom 始终按 ClassIsland 规格渲染、不受 family 影响、不引用 winisland 样式命名空间;family='classisland'的 fixed 使用 classisland 命名空间。- 经
pnpm verify:desktop(默认无头 quick)验证时,所有既有冒烟注入点仍可用:setTestInputPassthrough/setHitTestCursor/getWindowBounds/getHitRect/getInteractive/setBlurCollapseEnabled/setTouchMode/getTouchMode/isRecovering等名称与语义不变(改造归属 part 11 测试基线)。 colorScheme三种取值均生效:system跟随应用深浅色(html.dark)、light/dark强制;非法值回落system;切换后岛即时重渲染且不改主窗口主题。fontSize语义按 family:classisland 下改fontSize会派生 14/18/20 阶梯并整体缩放行文字;winisland 下保持旧"基准字号"行为;迁移用户旧值不被覆盖。- 滚动组件
settings.mode:continuous持续滚动、pause到边界停顿settings.interval秒;normalize 后取值合法,旧配置缺失补continuous。