Files
ClassIsland Prompt Kit 22c1784bab feat: ClassIsland / ClassHelper 灵动岛提示词套件
- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构)
- ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本)
- 投喂指南、分卷、逐节文件
- 调研记录、一致性基线、生成与质检脚本
2026-11-12 21:59:58 +08:00

388 lines
41 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 5. 数据契约、schema v2 与配置迁移
### 5.0 总则(必须遵守)
1. 本章定义重构后的**唯一数据契约**。所有读写岛配置的代码(主进程 `config.ts`、岛渲染进程、编辑器、设置页、冒烟)必须只经过 `packages/shared/src/island/schema.ts` 的类型与 `normalize.ts`/`migrate.ts` 的函数,不得各自解析 JSON。
2. 旧配置必须**无损升级**:`config.json` 现有的 `islandLayout`(fixed/custom、schemes、nodes、expandedRows、residentEnabled、pointerFade、lessonReminders)与 `island`(appearance 全字段)一个都不许丢;升级失败必须可回滚。
3. 新增 `appearance.family`:取值 `'classisland' | 'winisland'`。**新装默认 `classisland`;已存在配置的老用户升级默认 `winisland`**。`fixed` 的语义随 family 分叉:`classisland` 下 fixed = ClassIsland 信息行(默认 1 行 = 日期 + 课程表,行高 40、圆角 8、不透明度 0.5、3px 进度条、消息行内气泡);`winisland` 下 fixed = 原胶囊 + 详情卡。
4. `islandLayout.version` 必须写入;当前版本 `2`。版本号是**唯一迁移判据**,不得用"字段是否存在"猜版本(字段判断只作为迁移内部的兜底)。
5. 写入必须沿用现有 `config.json` 的"内存缓存 + 120ms 合并写 + 退出前 flush"语义(`packages/desktop-client/src/main/config.ts:235-346`),**不得**为迁移新增同步写盘路径;迁移本身例外(必须一次写成功并立即 flush)。
6. 客户端不得把 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}` | **仅诊断用**,不参与版本判断、不是第二套版本字段 |
1. **只保留 `islandLayout.version` 一个落盘版本字段**:不得新增 `config.schemaVersion` 或其它文件级版本号;迁移判定只看 `islandLayout.version`(缺失=v1)。
2. 迁移必须在 `app.whenReady()` 后、`island.init()` 之前完成一次;不得由渲染进程触发,也不得在岛窗口创建后再迁移(否则窗口会先按旧数据渲染一帧)。
3. 迁移成功后必须立即 `flushConfigSync()`;失败时不得写回半成品:保留原 `config.json`,删除 `.tmp`,应用以内存默认 v2 运行并在设置页显示"迁移失败,已保留原配置"。
4. 读到 `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 也用停靠与边距 |
1. **字段职责必须严格区分**:`family` 只决定 **fixed** 用哪套渲染器(ClassIsland 信息行 / WinIsland 胶囊);`winislandStyle`(兼容镜像 `style`)只决定 winisland 的调色板;`theme` 只决定 classisland fixed 的 Fluent/Classic;`colorScheme` 只决定浅色/深色/跟随。四者不得互相覆盖、不得合并。`custom` 不使用 `family`/`theme`/`winislandStyle`(始终 ClassIsland 规格),但仍读取 `colorScheme` 决定明暗。
2. `family` 归一化:只接受两个值,非法值回落 `'classisland'`(新装默认);迁移函数负责把老用户写成 `'winisland'`(见 5.4)。
3. `colorScheme` 归一化:只接受 `light/dark/system`,非法回落 `system`;缺失补 `system`。
4. `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。
5. `fontSizes` 归一化:每项 8–96;显式值优先于派生;缺失项按第 4 条补齐。
6. `theme` 仅 classisland fixed 生效;非法值回落 `'fluent'`;`winisland` 下忽略但不删除该字段。
7. `effectEnabled` 必须为 boolean(缺省 true);`effectScale` 必须夹取到 `[1, 0.75, 0.5]`(缺省 1,非法值取 1);`progressBar.height` 夹取 1–8、`progressBar.show` 缺省 true;`progressBar` 缺失时整对象补默认。
8. `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'|'next'|'today'`;新增 `today` 折叠规则由此驱动 |
| `settings.schedule.progressStyle` | 新增:`'bar'|'line'`,默认 `'line'`(3px) |
| `settings.schedule.showProgress` | 沿用;默认 `true` |
| `settings.schedule.showPeriod`/`count` | 沿用 |
| `settings.mode`(仅 `kind='rolling'`) | 新增:`'continuous' | 'pause'`,默认 `'continuous'`;`pause` 表示滚动到边界后停顿,停顿时长由同组件的 `settings.interval`(秒,1–300)控制 |
| `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 迁移算法(必须逐步实现,可重入)
**输入判定**:
1. 读取 `config.json`(不存在 → 全新安装:直接构造 v2 默认,`family='classisland'`,**不写迁移标记**)。
2. 文件存在但 `islandLayout.version` 缺失:视为 v1(唯一版本判据;**不得**再引入 `schemaVersion` 作为第二套版本号)。
3. 老用户判定:`islandLayout.version` 缺失 **或** 文件里存在 `islandLayout`/`island` 任一字段 → 迁移时 `family='winisland'`(保持原外观,不惊吓用户)。
4. 全新安装判定:文件不存在、或既无 `islandLayout` 又无 `island` → `family='classisland'`。
**步骤(必须按序、每步失败可中止)**:
```text
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"
```
1. **可重入/幂等**:若 `islandLayout.version===2`,迁移函数必须直接返回原对象(不写盘、不改字段);重复执行不得产生二次修改。
2. **老用户 fixed 不变**:迁移后 `family='winisland'`,fixed 仍渲染原胶囊+详情卡(`ClassicIsland.vue`),因此老用户升级后视觉 0 变化;只有用户主动在设置页切到 `classisland` 或新装用户才看到信息行。
3. **custom 老用户**:`custom` **始终按 ClassIsland 规格渲染、不受 family 影响**;迁移只升级数据(补行级默认、滚动 mode 等),不改 schema 结构。老 custom 用户升级后视觉会变为 ClassIsland 行规格(这是定稿行为),发布说明与设置页必须提示;fixed 用户(默认 winisland)不受影响。
4. 编辑器打开时必须先读迁移后的 v2;若 `islandLayout.version` 不匹配(例如用户用旧版应用改过文件),必须触发一次"再迁移"而不是把 v1 当 v2 解析。
5. 迁移必须记录 warning 列表到内存并在设置页"存储/诊断"可查看(哪几项被夹取、哪几个未知组件被保留)。
### 5.5 校验与夹取(`normalizeIslandLayoutV2` / `normalizeAppearanceV2`)
必须在 shared 中实现,主进程保存前、编辑器加载后、IPC 存入前**三处共用同一个函数**。规则:
1. `version`:仅接受 `2`;缺失/非法按迁移处理,不得静默改成 2 后继续用旧结构。
2. `layout`:`'fixed'|'custom'`,非法回落 `'fixed'`。
3. `family`:`'classisland'|'winisland'`,非法回落 `'classisland'`。
4. 数量上限:行 ≤12/方案、节点 ≤200/全部方案、嵌套深度 ≤6、方案 ≤20;`main` 行唯一(多余取消)。
5. 数值夹取:`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 说明)。
6. 未知 kind:**不得丢弃**。把原始节点 JSON 放入 `scheme.unrecognizedNodes`(最多 200 条),渲染时忽略并显示"未知组件(已保留)";这样用旧版应用打开再保存也不会丢数据。
7. 颜色:统一 `#rrggbb` 小写;非法回落默认;透明度字段独立,不把 `#rrggbbaa` 塞进颜色字段。
8. id:空/非字符串/重复 → 重新生成(`node-<n>`/`row-<n>`/`scheme-<n>`);id 最长 100 字符。
9. 文本:`settings.text` ≤2000 字符、`locationName/countdownTitle` ≤64;超长截断并记 warning。
10. 经纬度:`latitude -90..90`、`longitude -180..180`,可空;空表示未配置(组件显示"请设置天气地点")。
11. 未知字段:v2 对象里出现但类型未定义的字段必须**原样保留**在对象上(不删除),以便前向兼容。
12. `lessonReminders`:`prepareSeconds 5–1800`、`endingSeconds 5–600`(依据现有 `packages/shared/src/island-layout.ts:390-398`)。
13. `winislandStyle` / `style`:两者都只接受 `black/glass/tinted`,非法回落 `black`;读入优先 `winislandStyle`、回退 `style`,写出两者同步(兼容镜像);**不得**因 family 迁移而删除 `style`;family 与调色板相互独立(family 决定 fixed 用哪套渲染,winislandStyle 决定 winisland 的调色板)。
14. `theme`:只接受 `fluent/classic`,非法回落 `fluent`;具体色板/圆角/阴影 token 由 06 定义,05 只登记字段与默认值。
15. `colorScheme`:只接受 `light/dark/system`,非法回落 `system`;`system` 时跟随应用 `html.dark` 状态决定岛明暗(映射逻辑由渲染层实现,数据层只存枚举)。
16. `fontSize`:8–96,缺省按 family 取(classisland=16 正文基准、winisland=13 旧基线);`fontSizes` 缺失时按 5.2 第 4 条派生;显式 `fontSizes` 优先;迁移不得覆盖旧 `fontSize`。
17. `effectEnabled`:boolean,缺省 `true`;`effectScale`:只接受 `1/0.75/0.5`,缺省 `1`,非法取 `1`;`progressBar`:`{height:1–8, show:boolean}`,缺省 `{3,true}`。
18. 进度条高度优先级:`行 progress.height` > `方案 progressBarHeight` > `appearance.progressBar.height`;任一缺失沿上一级回落,最终默认 3。`progressBar.show=false` 时所有行都不显示(行级 `enabled=false` 只关该行)。
19. 滚动组件:`settings.mode` 只接受 `continuous/pause`,缺省 `continuous`;`pause` 时停顿秒数复用 `settings.interval`(夹取 1–300);`continuous` 时 `speed` 夹取 1–200(沿用现有范围)。
20. 渲染/样式命名空间**只由 layout(fixed 的 family)决定,不写入数据**:`fixed + family='winisland'` → winisland 命名空间;`fixed + family='classisland'` → classisland 命名空间;**`custom` 始终 classisland 命名空间、不受 family 影响**;normalize 不得改写样式类名/选择器相关字段。
21. `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` | 折叠时隐藏、轮到时展开 |
1. 上述映射只用于**渲染语义对齐**,不得把 `ScheduleDto` 落盘成 ClassIsland 档案;数据仍以服务端解析结果为准(02 §2.12)。
2. `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` 表示无 |
1. `IslandState` 的新字段只存在于**运行期推送**(`island:state`),不得写进 `config.json`、不得被编辑器保存;旧渲染进程缺字段时必须按默认值渲染(前向兼容)。
2. `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 版本与兼容
1. IPC payload **可以**携带 `schemaVersion:2` 仅用于运行时校验/调试,**不得落盘、不得作为配置版本判据**;配置版本只认 `islandLayout.version`。`island:state`/`island:appearance`/`island:get-layout` 三个高频 payload 建议带上该字段(旧字段保留,新增字段可选)。
2. 接收方规则:**未知字段忽略、缺字段回落默认**;不得因为缺 `family` 就拒绝渲染(老渲染进程遇到新主进程时应按 `winisland` 降级,反之按 `classisland`)。
3. `island:save-layout` 必须返回归一化后的 v2 结果(编辑器以返回值为准,避免"本地看着对、主进程夹取后不一致")。
4. 主进程 `getConfig()` 返回给渲染进程的 `islandLayout` 必须是 v2;`island` 必须含 `family` 与新增外观字段(`theme/effectEnabled/effectScale/progressBar`)。
5. 旧通道名全部保留(见 04 §4.4);新增通道必须同时在 `shared/island/ipc.ts` 与 preload 中登记,不得字符串散落。
6. 作业指纹相关 payload(见 04 §4.3.13)必须带 `source`(`realtime/homework-local/replay/call`)与 `dedupeKey`;主进程按 key 去重,渲染进程保留现有"本机刚录作业免上岛"快速路径。
### 5.8 编辑器预览与临时状态(不持久化)
1. 编辑器的内嵌预览、缩放、选中项、拖拽态属于**临时状态**,不得写入 `config.json`;只有"应用/保存"才写 v2。
2. "无消息展开内容(expandedRows)"与常态 `rows` 分开编辑、分开保存;`activeSchemeId` 只在切换方案时写。
3. 设置页"测试灵动岛"仍走 `islandPush` + `context.preview=true`(30 秒预览、失焦不收起,见 02 §2.15),预览消息 id 必须带前缀(如 `appearance-test-`)以便清理;预览不写历史、不改已读。
4. 编辑器打开时若检测到迁移 warning,必须在顶部显示"部分旧设置已按新格式调整(可查看详情)",不得静默。
### 5.9 回滚与降级
1. **回滚入口**:设置页"灵动岛 → 恢复升级前布局"必须存在;回滚步骤:先备份当前 `config.json` → 把 `config.json.v1.bak` 覆盖回去(该文件天然没有 `islandLayout.version`,即 v1)→ 提示重启 → 重启后旧版本行为恢复(family=winisland/fixed=胶囊)。
2. **迁移失败降级**:应用不得崩溃;以内存默认 v2 启动(不写盘),岛显示默认信息行,设置页显示错误与"恢复备份"按钮。
3. **前向兼容**:读到 `islandLayout.version>2` 时,应用只读该配置(不覆盖写),在设置页提示"配置由更高版本创建";不存在其他版本字段可用于判断。
4. **文件损坏**:`config.json` 非法 JSON 时沿用现有 `.bak` 回退逻辑(`config.ts` 现状:读取失败回落默认),回滚后仍必须按 `islandLayout.version` 缺失执行一次迁移判定;不得用损坏文件覆盖 `.bak`。
5. **旧版应用打开新文件**:不可避免(旧版不认识 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 | 迁移前覆盖写;回滚后仍保留,供二次回滚 |
**兼容矩阵(读写方向必须明确)**:
1. **新读旧**(v2 代码读 v1 文件):允许,走 5.4 迁移;迁移后内存中只存在 v2。
2. **新读新**:允许,走 normalize;`islandLayout.version` 更高时只读。
3. **旧读新**(旧代码读 v2 文件):旧代码会忽略未知字段继续运行(因为 v2 把新字段放在其不认识的键上),但**旧代码保存后会丢新字段**;因此必须在文档与发布说明中写明"不要用旧版应用反复编辑新配置"。
4. **新写旧**:禁止。v2 代码写盘必须带 `islandLayout.version:2`,不得输出 v1 结构,也不得写任何文件级版本号。
5. **迁移与编辑器并发**:编辑器打开时若检测到 `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 部分)
1. 新装启动后 `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 信息行。
2. 用一份旧 v1 配置(fixed、custom 各一份,含 schemes/nodes/appearance/residentEnabled)启动:迁移成功、无字段丢失、`family:'winisland'`、旧胶囊外观不变、`fontSize` 保留原值、`style`/`winislandStyle` 一致;`config.json.v1.bak` 存在。
3. 迁移后再次启动:不重复迁移、不重复写盘(可用文件 mtime/内容哈希断言幂等)。
4. 迁移失败模拟(把 `config.json` 改坏/只读)时:原文件不被破坏、应用以默认 v2 启动、设置页显示错误与恢复入口。
5. "恢复升级前布局"可把 v1 备份恢复回来并重启生效;恢复前自动再备份当前配置。
6. `normalizeIslandLayoutV2` 对行数>12、节点>200、深度>6、方案>20、非法颜色/数值/kind 全部夹取或保留到 `unrecognizedNodes`;不崩溃、不丢未知组件。
7. 老用户切到 `classisland` 后 fixed 变信息行、切回 `winisland` 恢复胶囊;**family 切换时 `style`/`winislandStyle` 与 `fontSize` 保持用户原值不变**;custom 始终 ClassIsland 规格、切换 family 不改变 custom 渲染。
8. 编辑器中常态行/展开行分开保存;应用后 `islandLayout` 为 v2 且 `island:save-layout` 返回值与本地一致。
9. `IslandState`/`IslandNotification`/`ScheduleDto`/`IslandTimePointDto` 的映射表逐项实现:优先级、上课态、时间点类型、单双周、作业/叫人特殊语义均可按 5.6 追踪。
10. IPC 兼容:旧通道全部可用;payload 可选带 `schemaVersion` 仅作校验(不落盘);缺字段时按 family 降级渲染而不是报错。
11. 编辑器/设置页不把预览、缩放、选中项写入配置;预览消息不写历史、不改已读。
12. 读到 `islandLayout.version>2` 时应用只读并提示,不得覆盖用户文件。
13. 外观新字段:关闭 `effectEnabled` 后强提醒特效不再渲染(行内气泡/普通提醒仍工作);`effectScale` 只接受 1/0.75/0.5 并实际改变特效尺寸;`progressBar.show=false` 时所有行进度条隐藏,行级高度覆盖方案/外观。
14. 运行期状态:`IslandState` 含 `family/pendingStaged/bubblePhase/bubbleDeadline` 且**不写入 config.json**;上课暂存时 `pendingStaged` 与队列数一致;气泡阶段按 `idle→mask-in→content-in→out` 推进。
15. 派生字段:`IslandNotification.level` 按 `URGENT→emergency/HIGH→high/NORMAL→normal/LOW→low` 生成且不落盘;`IslandLessonReminder.progress` 在 0–1、`leftSeconds` 与蓝色圆形徽标一致(剩余 1 秒显示 `-1`)。
16. fixed(classisland)下 `prepare/on-class/break` 以行内容接管呈现、`ending` 只显示秒数徽标;custom 下不接管(课程组件 + 徽标);两种 family 的提醒开关语义一致。
17. fixed 行内气泡同一时刻只显示 1 条 active;多条折叠为「还有 N 条」并可打开应用内通知中心;队列/去重/暂存规则不变。
18. `family='winisland'` 只影响 fixed(保持现有胶囊观感);custom 始终按 ClassIsland 规格渲染、不受 family 影响、不引用 winisland 样式命名空间;`family='classisland'` 的 fixed 使用 classisland 命名空间。
19. 经 `pnpm verify:desktop`(默认无头 quick)验证时,所有既有冒烟注入点仍可用:`setTestInputPassthrough`/`setHitTestCursor`/`getWindowBounds`/`getHitRect`/`getInteractive`/`setBlurCollapseEnabled`/`setTouchMode`/`getTouchMode`/`isRecovering` 等名称与语义不变(改造归属 part 11 测试基线)。
20. `colorScheme` 三种取值均生效:`system` 跟随应用深浅色(`html.dark`)、`light`/`dark` 强制;非法值回落 `system`;切换后岛即时重渲染且不改主窗口主题。
21. `fontSize` 语义按 family:classisland 下改 `fontSize` 会派生 14/18/20 阶梯并整体缩放行文字;winisland 下保持旧"基准字号"行为;迁移用户旧值不被覆盖。
22. 滚动组件 `settings.mode`:`continuous` 持续滚动、`pause` 到边界停顿 `settings.interval` 秒;normalize 后取值合法,旧配置缺失补 `continuous`。