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

674 lines
62 KiB
Markdown
Raw 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.
## 13. 数据模型、存储、备份与迁移
### 13.1 存储总则(必须遵守)
1. 本应用是**纯本地单机应用**,所有设置、档案、课表、组件、自动化、提醒历史、主题、插件配置必须存在本机,**不得**依赖任何服务器;断网、无账号、首次启动都必须完整可用。
2. 存储根目录必须是 `app.getPath('userData')`(Windows 为 `%APPDATA%\ClassIsland`,macOS 为 `~/Library/Application Support/ClassIsland`,Linux 为 `~/.config/ClassIsland`)。**不得**把用户数据写进安装目录(安装目录可能只读、且升级会被整体替换)。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/config.ts:165-167`(`path.join(app.getPath('userData'), 'config.json')`)与 `repos/ClassIsland/ClassIsland.Core/CommonDirectories.cs:16-28`(ClassIsland 的 `AppRootFolderPath`/`Config`/`Logs` 划分)。
3. 所有持久化文件必须是 **UTF-8 无 BOM**、LF 换行、两空格缩进的 JSON;字段名大小写必须按第 13.3/13.4 节的约定(ClassIsland 兼容文件用 PascalCase,本应用运行期配置用 camelCase)。读入时必须**同时容忍**紧凑格式(无缩进、无换行)与带注释之外的一切合法 JSON;**不得**写注释(ClassIsland 的 `examples/example.json` 带注释只是示例,真实加载器不支持注释,见 `repos/ClassIsland/examples/example.json:1-2`)。
4. 所有写入必须**原子化**:写 `<file>.tmp` → `fsync` → `rename` 覆盖目标;禁止直接 `writeFile` 覆盖正在被读的文件。依据:`repos/ClassIsland/ClassIsland.Shared/Helpers/ConfigureFileHelper.cs:182-188`(`FileStream.Flush(true)` 后关闭)。
5. 每次成功读取 JSON 后必须把原文件复制为 `<file>.bak`;读取失败时自动回退加载 `.bak`,成功后把 `.bak` 复制回目标(对齐 `ConfigureFileHelper.LoadConfigUnWrapped` 的语义,见 `ConfigureFileHelper.cs:63-96`)。若 `.bak` 也失败,必须使用**全默认值**并把错误加入"配置错误列表"(`ConfigError{ Path, Code, IsFallback, Exception }`,见 `repos/ClassIsland/ClassIsland.Shared/Models/ConfigError.cs`),在设置页"诊断/存储"中展示。
6. 所有配置对象必须"**缺失字段用默认值补齐、未知字段忽略、越界值夹紧**"。禁止因为新增字段就让老配置文件加载失败。依据:`repos/ClassIsland/ClassIsland/Services/SettingsService.cs:63-118`(加载失败只记日志并继续)与 `config.ts:198-233`(逐字段归一化)。
7. 所有写入必须经过**单一写入入口**(`storage.ts` 的 `saveXxx()`),禁止业务代码直接 `fs.writeFile`;该入口负责合并写、备份、事件通知。依据:`config.ts:235-346`。
### 13.2 目录与文件清单(必须逐项命名一致)
```text
<userData>/ # app.getPath('userData')
├─ config.json # 运行期设置(camelCase;对应 ClassIsland Settings.json)
├─ Settings.json # ClassIsland 格式兼容文件(PascalCase;仅只读导入/导出,运行期不读写)
├─ data/
│ ├─ profiles/
│ │ ├─ Default.json # Profile 档案(PascalCase,与 ClassIsland 同构)
│ │ └─ <档案名>.json
│ ├─ component-layouts/
│ │ └─ Default.json # ComponentProfile(PascalCase)
│ ├─ automations/
│ │ └─ Default.json # Workflow[] 数组(PascalCase)
│ ├─ themes/
│ │ ├─ <主题Id>/ # 主题包解压目录(manifest、样式、资源)
│ │ └─ EnabledThemes.json # string[],启用中的主题 Id
│ ├─ plugins/ # 插件解压目录:<插件Id>/
│ ├─ plugin-data/ # 插件私有存储:<插件Id>/storage.json
│ ├─ notification-history.json # 通知历史(用于提醒历史视图)
│ └─ generated/
│ └─ default-subjects.json # 首次启动写入的默认科目表
├─ backups/ # 自动/手动备份 zip
├─ logs/
│ └─ main.log # 超过 1MB 轮转为 main.log.1
├─ cache/
│ ├─ plugin-index/ # 插件市场索引缓存
│ ├─ theme-packages/ # 主题包下载缓存 <id>.zip
│ └─ update/ # 更新清单/元数据缓存
└─ updates/ # 待安装更新包、policy.json、install-result.json
```
依据:`repos/ClassIsland/ClassIsland/Services/PluginService.cs:35-44`(插件三目录)、`XamlThemeService.cs:47-49`(主题两目录 + `EnabledThemes.json`)、`AutomationService.cs:27-32`(自动化目录 + `<CurrentAutomationConfig>.json`)、`ComponentsService.cs:28-58`(`ComponentLayouts/<CurrentComponentConfig>.json`)、`ProfileService.cs:48-58`(`Profiles/` 与 `<SelectedProfile>`)、`FileFolderService.cs:23-49`(需要预创建的目录清单)、`auto-update.ts:131-133`(`updates/`)、`logger.ts:32-39`(`logs/main.log`)、`config.ts:165-167`(`config.json`)。
### 13.3 `config.json` 完整 schema(字段级,含默认值)
`config.json` 是 ClassIsland `Settings.json` 的超集;下表的"ClassIsland 对应字段"必须保留语义,默认值必须与源码一致。**任何字段缺失都必须落到默认值,任何越界值都必须夹紧到合法区间。**
**`Settings.json` 兼容层(强制)**:运行期只读写 `config.json`;`Settings.json` 只作为「ClassIsland 格式导入/导出兼容层」存在——导入时**只读**解析(`PascalCase` → 本实现字段映射),导出时生成一份 ClassIsland 可识别的 `Settings.json`(`PascalCase`)。**禁止双向双写**(不得同时写 `config.json` 与 `Settings.json`,否则两份文件必然会分叉)。导入映射必须保留历史拼写,尤其是 `IsIslandSeperated`(ClassIsland 源码就是少一个 `a`,`repos/ClassIsland/ClassIsland/Models/Settings.cs:1852`)——本实现内部字段可写作 `isIslandSeparated`,但导入/导出映射表必须双向接受原拼写。**【推断】**
#### 13.3.1 外观与主界面
| 字段 | 类型 | 默认值 | 合法区间/枚举 | ClassIsland 依据 |
|---|---|---|---|---|
| `theme` | number | `2` | `0` 亮 / `1` 暗 / `2` 跟随系统 | `Settings.cs:32` |
| `primaryColor` | string | `#00BFFF` | `#RRGGBBAA` | `Settings.cs:35`(DeepSkyBlue) |
| `secondaryColor` | string | `#7FFFD4` | 同上 | `Settings.cs:36`(Aquamarine) |
| `isCustomBackgroundColorEnabled` | boolean | `false` | — | `Settings.cs:33` |
| `backgroundColor` | string | `#000000FF` | 同上 | `Settings.cs:34`(Colors.Black) |
| `isCustomForegroundColorEnabled` | boolean | `false` | — | `Settings.cs:202` |
| `customForegroundColor` | string | `#1E90FF` | 同上 | `Settings.cs:203`(DodgerBlue) |
| `opacity` | number | `0.5` | `0.1`–`1` | `Settings.cs:64` |
| `scale` | number | `1.0` | `0.75`–`2.5` | `Settings.cs:86` |
| `radiusX` / `radiusY` | number | `8` / `8` | `0`–`20` | `Settings.cs:183-184` |
| `mainWindowFont` | string | 跟随系统默认字体 | 合法字体族名 | `Settings.cs:95` |
| `mainWindowFontWeight` | number | `500`(Medium) | `100`–`900` | `Settings.cs:101` |
| `mainWindowSecondaryFontSize` | number | `14` | `8`–`96` | `Settings.cs:197` |
| `mainWindowBodyFontSize` | number | `16` | 同上 | `Settings.cs:198` |
| `mainWindowEmphasizedFontSize` | number | `18` | 同上 | `Settings.cs:199` |
| `mainWindowLargeFontSize` | number | `20` | 同上 | `Settings.cs:200` |
| `mainWindowLineVerticalMargin` | number | `5` | `0`–`150` | `Settings.cs:219` |
| `isIslandSeperated` | boolean | `false` | — | `Settings.cs:1852`(拼写保留) |
| `reduceProgressAccuracy` | boolean | `true` | — | `Settings.cs:1853` |
| `animationLevel` | number | `1` | `0` 关闭 / `1` 标准 / `2` 全部 | `Settings.cs:1851` |
| `isScreenRecordingModeEnabled` | boolean | `false` | — | `Settings.cs:1861` |
| `isWindowCaptureBlockingEnabled` | boolean | `false` | — | `Settings.cs:1862` |
#### 13.3.2 窗口、停靠与隐藏
| 字段 | 类型 | 默认值 | 说明 | 依据 |
|---|---|---|---|---|
| `windowDockingLocation` | number | `1` | `0`–`5`:左上/中上/右上/左下/中下/右下 | `Settings.cs:48` |
| `windowDockingOffsetX/Y` | number | `0`/`0` | 停靠偏移(px) | `Settings.cs:49-50` |
| `windowDockingMonitorIndex` | number | `0` | 多显示器索引 | `Settings.cs:51` |
| `windowLayer` | number | `1` | 窗口层级(普通/置顶/桌面) | `Settings.cs:52` |
| `isMouseClickingEnabled` | boolean | `false` | 鼠标穿透(true = 点击穿透) | `Settings.cs:53` |
| `hideOnClass` | boolean | `false` | 上课自动隐藏 | `Settings.cs:45` |
| `hideOnMaxWindow` | boolean | `false` | 有最大化窗口时隐藏 | `Settings.cs:63` |
| `hideOnFullscreen` | boolean | `false` | 全屏时隐藏 | `Settings.cs:54` |
| `excludedFullscreenWindow` | string[] | `[]` | 全屏检测排除的进程名 | `Settings.cs:56-59` |
| `hideMode` | number | `0` | `0` 基本(上课/最大化/全屏);`1` 高级(规则集) | `Settings.cs:185` |
| `hideRules` | Ruleset | 空规则集 | 见 13.5.3 | `Settings.cs:186` |
| `useRawInput` | boolean | `false` | 原始输入(兼容某些输入法) | `Settings.cs:192` |
| `isMouseInFadingEnabled` | boolean | `true` | 鼠标进入时淡化 | `Settings.cs:193` |
| `isMouseInFadingReversed` | boolean | `false` | 反向淡化 | `Settings.cs:194` |
| `touchInFadingDurationMs` | number | `0` | 触摸淡化时长 | `Settings.cs:195` |
| `isCompatibleWindowTransparentEnabled` | boolean | `false` | 兼容透明(老显卡) | `Settings.cs:196` |
| `windowTopmostRecheckMode` | number | `0` | 置顶复查策略 | `Settings.cs:1860` |
| `showCurrentLessonOnlyOnClass` | boolean | `false` | — | `Settings.cs:209` |
| `isSwapMode` | boolean | `true` | 换课模式 | `Settings.cs:210` |
**坐标口径(强制)**:`windowDockingOffsetX/Y`、`scale`、`radiusX/Y`、所有字号与组件尺寸/间距字段一律以 **DIP(CSS px)** 存储与解释;窗口 bounds 也按 DIP 交给 Electron。**禁止**把 `devicePixelRatio` 乘进这些字段;只有截图/像素级视觉验收时才用 `devicePixelRatio` 把 DIP 换算成物理像素(与 04 部分同一口径)。
#### 13.3.3 课程与时间
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `selectedProfile` | string | `"Default.json"` | `Settings.cs:66` |
| `singleWeekStartTime` | string(ISO) | 首次启动时写入当天 00:00 | `Settings.cs:60`(与其相关的周次计算见 ProfileService) |
| `multiWeekRotationOffset` | number[5] | `[-1,-1,0,0,0]` | `Settings.cs:60` |
| `multiWeekRotationMaxCycle` | number | `4` | `Settings.cs:61` |
| `classPrepareNotifySeconds` | number | `60` | `Settings.cs:43` |
| `isClassPrepareNotificationEnabled` | boolean | `true` | `Settings.cs:47` |
| `isClassChangingNotificationEnabled` | boolean | `true` | `Settings.cs:46` |
| `isClassOffNotificationEnabled` | boolean | `true` | `Settings.cs:55` |
| `showDate` | boolean | `true` | `Settings.cs:44` |
| `showExtraInfoOnTimePoint` | boolean | `true` | `Settings.cs:103` |
| `extraInfoType` | number | `0` | `Settings.cs:104`(0 持续时间/1 已过/2 剩余/3 已过百分比/4 剩余时间/5 倒计时) |
| `isCountdownEnabled` | boolean | `true` | `Settings.cs:105` |
| `countdownSeconds` | number | `60` | `Settings.cs:106` |
| `extraInfo4ShowSecondsSeconds` | number | `300` | `Settings.cs:107` |
| `defaultOnClassTimePointMinutes` | number | `40` | `Settings.cs:108` |
| `defaultBreakingTimePointMinutes` | number | `10` | `Settings.cs:109` |
| `isNonExactCountdownEnabled` | boolean | `false` | `Settings.cs:221` |
#### 13.3.4 提醒、语音与时间同步
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `isNotificationEnabled` | boolean | `true` | `Settings.cs:91` |
| `notificationProvidersEnableStates` | map<string,bool> | `{}` | `Settings.cs:88` |
| `notificationProvidersPriority` | string[] | `[]` | `Settings.cs:89` |
| `notificationProvidersSettings` | map<string,any> | `{}` | `Settings.cs:90` |
| `notificationProvidersNotifySettings` | map<string,NotificationSettings> | `{}` | `Settings.cs:143` |
| `notificationChannelsNotifySettings` | map<string,NotificationSettings> | `{}` | `Settings.cs:1846` |
| `isNotificationEffectEnabled` | boolean | `true` | `Settings.cs:142` |
| `isNotificationTopmostEnabled` | boolean | `true` | `Settings.cs:149` |
| `notificationEffectRenderingScale` | number | `1.0` | `Settings.cs:150` |
| `isNotificationEffectRenderingScaleAutoSet` | boolean | `false` | `Settings.cs:151` |
| `isNotificationSoundEnabled` | boolean | `true` | `Settings.cs:144` |
| `notificationSoundPath` | string | `""` | `Settings.cs:145` |
| `notificationSoundVolume` | number | `1.0` | `Settings.cs:182` |
| `allowNotificationSpeech/Effect/Sound/Topmost` | boolean | `false`/`true`/`false`/`true` | `Settings.cs:153-156` |
| `notificationUseStandaloneEffectUiThread` | boolean | `true` | `Settings.cs:1837` |
| `isSpeechEnabled` | boolean | `true` | `Settings.cs:135` |
| `speechVolume` | number | `1.0` | `Settings.cs:136` |
| `speechSource` | number | `0` | `Settings.cs:137` |
| `selectedSpeechProvider` | string | `"classisland.speech.edgeTts"` | `Settings.cs:1847` |
| `edgeTtsVoiceName` | string | `"zh-CN-XiaoxiaoNeural"` | `Settings.cs:138` |
| `isSystemSpeechSystemExist` | boolean | `false`(运行时探测) | `Settings.cs:133` |
| `exactTimeServer` | string | `"ntp.aliyun.com"` | `Settings.cs:139` |
| `isExactTimeEnabled` | boolean | `true` | `Settings.cs:140` |
| `timeOffsetSeconds` | number | `0` | `Settings.cs:141` |
| `isTimeAutoAdjustEnabled` | boolean | `false` | `Settings.cs:146` |
| `timeAutoAdjustSeconds` | number | `0` | `Settings.cs:147` |
| `lastTimeAdjustDateTime` | string(ISO) | 当前时间 | `Settings.cs:148` |
#### 13.3.5 组件、自动化、主题
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `currentComponentConfig` | string | `"Default"` | `Settings.cs:159` |
| `isAutomationEnabled` | boolean | `false` | `Settings.cs:160` |
| `currentAutomationConfig` | string | `"Default"` | `Settings.cs:162` |
| `isAutomationWarningVisible` | boolean | `true` | `Settings.cs:161` |
| `selectedPlatte` | string | `#1E90FFFF` | `Settings.cs:80`(DodgerBlue) |
| `selectedPlatteIndex` | number | `0` | `Settings.cs:81` |
| `wallpaperColorPlatte` | string[] | 5 个 DodgerBlue | `Settings.cs:82` |
| `colorSource` | number | `2` | `Settings.cs:83`(0 无/1 壁纸/2 手动) |
| `wallpaperClassName` | string | `""` | `Settings.cs:84` |
| `targetLightValue` | number | `0.6` | `Settings.cs:85` |
| `isWallpaperAutoUpdateEnabled` | boolean | `false` | `Settings.cs:92` |
| `wallpaperAutoUpdateIntervalSeconds` | number | `60` | `Settings.cs:93` |
| `weatherIconId` | string | `"classisland.weatherIcons.lucide"` | `Settings.cs:1849` |
| `isRollingComponentWarningVisible` | boolean | `true` | `Settings.cs:1850` |
| `isThemeWarningVisible` | boolean | `true` | `Settings.cs:1848` |
| `isThemeSeparateInfoVisible` | boolean | `true` | `Settings.cs:1875` |
#### 13.3.6 天气
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `cityId` | string | `"weathercn:101010100"` | `Settings.cs:99` |
| `cityName` | string | `"北京 (北京, 中国)"` | `Settings.cs:100` |
| `lastWeatherInfo` | object | `{}` | `Settings.cs:98` |
| `weatherLatitude` / `weatherLongitude` | number | `0`/`0` | `Settings.cs:1838-1839` |
| `weatherLocationSource` | number | `0` | `Settings.cs:1840` |
| `autoRefreshWeatherLocation` | boolean | `false` | `Settings.cs:1841` |
| `noTLSWeatherRequests` | boolean | `false` | `Settings.cs:1842` |
| `excludedWeatherAlerts` | string[] | `[]` | `Settings.cs:158` |
| `selectedMiniInfoProvider` | string? | 内置天气提供者 Guid | `Settings.cs:97` |
| `miniInfoProviderSettings` | map<string,any> | `{}` | `Settings.cs:96` |
#### 13.3.7 更新、插件、备份、诊断
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `updateMode` | number | `3` | `Settings.cs:77` |
| `autoInstallUpdateNextStartup` | boolean | `true` | `Settings.cs:78` |
| `selectedUpdateChannelV2` | string | `"stable"` | `Settings.cs:217` |
| `selectedUpdateMirrorV2` | string | `"main"` | `Settings.cs:216` |
| `selectedUpdateChannelV3` | string(Guid) | `Guid.Empty` | `Settings.cs:1854` |
| `lastCheckUpdateTime` | string(ISO) | `DateTime.MinValue` | `Settings.cs:75` |
| `lastUpdateStatus` | number | `0`(UpToDate) | `Settings.cs:76` |
| `updateArtifactHash` | string | `""` | `Settings.cs:157` |
| `ignoredUpdateVersion` | string | `""` | `config.ts:76`(本应用字段,兼容旧行为) |
| `userPluginIndexes` | string[] | `[]` | `Settings.cs:167` |
| `additionalPluginIndexes` | map<string,string> | `{}` | `Settings.cs:168` |
| `pluginIndexes` | object[] | `[]` | `Settings.cs:169` |
| `pluginIndexSelectedMirrors` | map<string,string> | `{}` | `Settings.cs:166` |
| `officialSelectedMirror` | string | `"github"` | `Settings.cs:170` |
| `officialIndexMirrors` | map<string,string> | `{github: https://github.com, ghproxy: …, moeyy: …}` | `Settings.cs:171-176` |
| `ignoreSslForPluginMirrors` | boolean | `false` | `Settings.cs:177` |
| `isPluginMarketWarningVisible` | boolean | `true` | `Settings.cs:204` |
| `isPluginsAutoUpdateEnabled` | boolean | `true` | `Settings.cs:1858` |
| `isPluginsUpdateNotificationEnabled` | boolean | `true` | `Settings.cs:1859` |
| `autoDisableCorruptPlugins` | boolean | `true` | `Settings.cs:1844` |
| `corruptPluginsDisabledLastSession` | boolean | `false` | `Settings.cs:1845` |
| `lastRefreshPluginSourceTime` | string(ISO) | `MinValue` | `Settings.cs:180` |
| `isAutoBackupEnabled` | boolean | `true` | `Settings.cs:187` |
| `lastAutoBackupTime` | string(ISO) | 当前时间 | `Settings.cs:188` |
| `autoBackupLimit` | number | `16` | `Settings.cs:190` |
| `autoBackupIntervalDays` | number | `7` | `Settings.cs:191` |
| `backupFilesSize` | string | `"计算中..."` | `Settings.cs:189` |
| `firstLaunchTime` | string(ISO) | 当前时间 | `Settings.cs:127` |
| `diagnosticStartupCount` | number | `0` | `Settings.cs:128` |
| `diagnosticCrashCount` | number | `0` | `Settings.cs:129` |
| `diagnosticLastCrashTime` | string(ISO) | `MinValue` | `Settings.cs:130` |
| `diagnosticMemoryKillCount` | number | `0` | `Settings.cs:131` |
| `diagnosticLastMemoryKillTime` | string(ISO) | 当前时间 | `Settings.cs:132` |
| `developerMode` | boolean | `false` | `config.ts:79`(本应用字段,决定是否显示开发者页) |
| `careMode` | boolean | `false` | `config.ts:80`(关怀模式) |
| `isDebugEnabled` | boolean | `false` | `Settings.cs:65` |
| `isDebugOptionsEnabled` | boolean | `false` | `Settings.cs:79` |
| `isDebugConsoleEnabled` | boolean | `false` | `Settings.cs:118` |
| `debugAnimationScale` | number | `1.0` | `Settings.cs:110` |
| `debugTimeSpeed` | number | `1.0` | `Settings.cs:111` |
| `debugTimeOffsetSeconds` | number | `0` | `Settings.cs:112` |
| `isSplashEnabled` | boolean | `false` | `Settings.cs:115` |
| `splashCustomText` | string | `""` | `Settings.cs:116` |
| `showDetailedStatusOnSplash` | boolean | `false` | `Settings.cs:222` |
| `isWelcomeWindowShowed` | boolean | `false` | `Settings.cs:68` |
| `onboardingVersion` | number | `0` | `config.ts:41` |
| `isReportingEnabled` | boolean | `true` | `Settings.cs:69` |
| `isFallbackModeEnabled` | boolean | `true` | `Settings.cs:94` |
| `isCriticalSafeMode` | boolean | `false` | `Settings.cs:207` |
| `criticalSafeModeMethod` | number | `0` | `Settings.cs:1836` |
| `trustedProfileIds` | string(Guid)[] | `[]` | `Settings.cs:220` |
| `taskBarIconClickBehavior` | number | Windows `0`,其它 `4` | `Settings.cs:102` |
| `settingsPagesCachePolicy` | number | `0` | `Settings.cs:213` |
| `isRefreshingToastEnabled` | boolean | `true` | `Settings.cs:1865` |
| `refreshingToastThresholdDays` | number | `20` | `Settings.cs:1866` |
| `maxRefreshingToastCounts` | number | `5` | `Settings.cs:1873` |
**字段级兼容规则(必须实现)**:
1. 新增字段一律有默认值,缺失即用默认值(`SettingsService.LoadSettingsAsync` 缺失文件时直接返回 `new Settings()`,见 `SettingsService.cs:73-77`)。
2. 字段类型不匹配时用默认值覆盖并记 warn,不得整体抛错(对齐 `config.ts:198-233` 每个字段逐个 `typeof` 判断)。
3. 枚举越界值回落到默认值(如 `theme` 非 `0/1/2` → `2`)。
4. 语义变更或废弃的字段必须保留读取(忽略)一个版本,禁止删除后导致老配置迁移失败。**不得**删除 `isIslandSeperated` 这类历史拼写字段(`Settings.cs:1852`)。
5. `deviceKey` 类"机器身份"字段一旦生成必须永不变;如由外部传入不同值必须拒绝并记 warn(对齐 `config.ts:501-505`)。
### 13.4 档案(Profile)schema — 与 ClassIsland 完全同构
档案文件:`data/profiles/<SelectedProfile>`,默认 `Default.json`。**字段名必须保留 PascalCase**,以便直接导入/导出 ClassIsland 档案。
#### 13.4.1 Profile 根对象
| 字段 | 类型 | 默认值 | 说明 | 依据 |
|---|---|---|---|---|
| `Name` | string | `""` | 档案名称 | `Profile.cs:227-236` |
| `Id` | string(Guid) | 新建 Guid | 档案 ID(用于"信任档案"判断) | `Profile.cs:452-463` |
| `TimeLayouts` | map<Guid,TimeLayout> | `{}` | 时间表字典,键为 Guid | `Profile.cs:241-250` |
| `ClassPlans` | map<Guid,ClassPlan> | `{}` | 课表字典 | `Profile.cs:255-270` |
| `Subjects` | map<Guid,Subject> | 首次启动写入默认科目表 | 科目字典 | `Profile.cs:284-293` |
| `EditingSubjects` | Subject[] | `[JsonIgnore]` 不落盘 | 编辑期临时集合 | `Profile.cs:298-308` |
| `IsOverlayClassPlanEnabled` | boolean | `false` | 是否启用临时层课表 | `Profile.cs:313-322` |
| `OverlayClassPlanId` | string(Guid)? | `null` | 当前临时层课表 ID | `Profile.cs:327-337` |
| `TempClassPlanId` | string(Guid)? | `null` | 临时课表 ID | `Profile.cs:342-351` |
| `TempClassPlanSetupTime` | string(ISO) | 当前时间 | 临时课表设置时间 | `Profile.cs:356-365` |
| `ClassPlanGroups` | map<Guid,ClassPlanGroup> | 必含默认群 + 全局群 | 课表群 | `Profile.cs:370-379` |
| `SelectedClassPlanGroupId` | string(Guid) | 默认群 Guid `ACAF4EF0-E261-4262-B941-34EA93CB4369` | 当前选中群 | `Profile.cs:384-393`、`ClassPlanGroup.cs:47-52` |
| `TempClassPlanGroupId` | string(Guid)? | `null` | 临时课表群 | `Profile.cs:398-407` |
| `TempClassPlanGroupExpireTime` | string(ISO) | 当前时间 | 临时群失效时间 | `Profile.cs:412-421` |
| `IsTempClassPlanGroupEnabled` | boolean | `false` | — | `Profile.cs:426-435` |
| `TempClassPlanGroupType` | number | `Inherit` | `Inherit`/`Override` 等(枚举) | `Profile.cs:440-449` |
| `OrderedSchedules` | map<DateTime,OrderedSchedule> | `{}` | 预定课表(日期→课表 ID) | `Profile.cs:468-477` |
**初始化规则**:新档案必须自动加入两个群:默认群(`ACAF4EF0-E261-4262-B941-34EA93CB4369`,名称"默认")与全局群(`Guid.Empty`,名称"全局课表群",`IsGlobal=true`)。依据:`Profile.cs:44-59`。
#### 13.4.2 TimeLayout(时间表)
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `Name` | string | `"新时间表"` | `TimeLayout.cs:17` |
| `Layouts` | TimeLayoutItem[] | `[]` | `TimeLayout.cs:254-267` |
| `IsOverlay` | boolean | `false` | `TimeLayout.cs:26-35` |
| `OverlaySourceId` | string(Guid)? | `null` | `TimeLayout.cs:40-49` |
| `IsActivated` / `IsActivatedManually` | boolean | `[JsonIgnore]` 不落盘 | `TimeLayout.cs:272-297` |
#### 13.4.3 TimeLayoutItem(时间点)
| 字段 | 类型 | 默认值 | 说明 | 依据 |
|---|---|---|---|---|
| `StartTime` | TimeSpan 字符串 | `"00:00:00"` | 开始时间(当天内的偏移) | `TimeLayoutItem.cs:62-77` |
| `EndTime` | TimeSpan 字符串 | `"00:00:00"` | 结束时间;`TimeType` 为 2/3 时强制等于 `StartTime` | `TimeLayoutItem.cs:82-93`、`:70-73` |
| `TimeType` | number | `0` | `0` 上课 / `1` 课间 / `2` 分割线 / `3` 行动 | `TimeLayoutItem.cs:128-138` |
| `IsHideDefault` | boolean | `false` | 默认隐藏(仅上课类型有效) | `TimeLayoutItem.cs:143-153` |
| `DefaultClassId` | string(Guid) | `Guid.Empty`(空字符串也解析为 Empty) | 默认科目 | `TimeLayoutItem.cs:158-169`、`GuidEmptyFallbackConverter` |
| `BreakName` | string | `""` | 自定义课间名;展示用 `BreakNameText`(空则"课间休息") | `TimeLayoutItem.cs:180-191,174-175` |
| `ActionSet` | ActionSet? | `null` | 仅 `TimeType=3` 时有意义 | `TimeLayoutItem.cs:196-205` |
| `StartSecond` / `EndSecond` | string | `[Obsolete]` | 旧版字段,读取时忽略、写出时不得生成 | `TimeLayoutItem.cs:26-57` |
| `attachedObjects` | map<Guid, object> | `{}`(导入时接受 `AttachedObjects` 原拼写) | **一等公民的附加设置字典**(课级提醒/天气/课程显示等),见 13.4.8 | `repos/ClassIsland/ClassIsland.Shared/AttachableSettingsObject.cs:10-15` |
**校验规则**:`StartTime > EndTime` 必须拒绝(对齐 `TimeLayoutItem.EnsureTime`,`TimeLayoutItem.cs:95-101`);同一时间表内时间点必须按 `StartTime` 升序,重叠允许但必须保留排序;分割线/行动点不占课程位(见 13.4.4 的"有效时间点")。
#### 13.4.4 ClassPlan(课表)
| 字段 | 类型 | 默认值 | 说明 | 依据 |
|---|---|---|---|---|
| `Name` | string | `"新课表"` | — | `ClassPlan.cs:440-449` |
| `TimeLayoutId` | string(Guid) | `Guid.Empty` | 引用时间表 | `ClassPlan.cs:396-407` |
| `TimeRule` | TimeRule | `{WeekDay:0, WeekCountDiv:0, WeekCountDivTotal:2}` | 周次规则 | `ClassPlan.cs:412-421`、`TimeRule.cs:13-15` |
| `Classes` | ClassInfo[] | `[]` | 课程列表,长度 = 时间表中上课类型点数 | `ClassPlan.cs:426-435,451-491` |
| `IsActivated` | boolean | `[JsonIgnore]` 不落盘 | 当前激活 | `ClassPlan.cs:527-537` |
| `IsOverlay` | boolean | `false` | 临时层课表 | `ClassPlan.cs:542-551` |
| `OverlaySourceId` | string(Guid)? | `null` | 临时层源课表 | `ClassPlan.cs:556-565` |
| `OverlaySetupTime` | string(ISO) | 当前时间 | — | `ClassPlan.cs:570-579` |
| `IsEnabled` | boolean | `true` | 是否默认启用 | `ClassPlan.cs:584-593` |
| `AssociatedGroup` | string(Guid) | 默认群 Guid | 所属课表群 | `ClassPlan.cs:598-607` |
**有效时间点规则**(必须实现 `ValidTimeLayoutItems`):只取 `TimeType ∈ {0,1,2}`;从前后两个方向扫描 `ClassInfo.IsEnabled`,把被禁用课程"夹住"的时间点全部隐藏;`TimeType=3` 的行动点永不显示。依据:`ClassPlan.cs:54-98`。
#### 13.4.5 ClassInfo(一节课)
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `SubjectId` | string(Guid) | `Guid.Empty`(空串也解析为 Empty) | `ClassInfo.cs:70-80` |
| `IsChangedClass` | boolean | `false` | `ClassInfo.cs:85-94` |
| `IsEnabled` | boolean | `true` | `ClassInfo.cs:99-108` |
| `Index` / `CurrentTimeLayout` / `IsEmpty` | — | `[JsonIgnore]` 不落盘 | `ClassInfo.cs:21-64,110-121` |
#### 13.4.6 Subject(科目)
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `Name` | string | `""` | `Subject.cs:19-30` |
| `Initial` | string | `""`(`Name` 变化且 `Initial` 为空时自动取首字) | `Subject.cs:35-44` |
| `TeacherName` | string | `""` | `Subject.cs:49-58` |
| `IsOutDoor` | boolean | `false` | `Subject.cs:63-72` |
内置常量必须实现:`Fallback = {Name:"???", Initial:"?"}`、`Empty = {Name:"", Initial:""}`、`Breaking = {Name:"课间休息", Initial:"休"}`。依据:`Subject.cs:79-101`。教师姓氏提取必须支持复姓表(万俟/司马/上官/欧阳……)与英文姓名取最后一个单词。依据:`Subject.cs:14,107-140`。
#### 13.4.7 课表群与预定
- `ClassPlanGroup { Name: "新课表群", IsGlobal: false }`;默认群 Guid `ACAF4EF0-E261-4262-B941-34EA93CB4369`;全局群 Guid `Guid.Empty`;解散群时课表移到默认群,删除群时群内课表一并删除。依据:`ClassPlanGroup.cs`、`Profile.cs:104-137`。
- `OrderedSchedule { ClassPlanId: Guid }`;`OrderedSchedules` 的键是**日期**(本地日期,序列化为 `"2026-11-12T00:00:00"`)。
- 临时层规则:每天启动/跨天时必须 `CleanExpiredTempClassPlan()`:删除今天以前的 `OrderedSchedules`;删除未被任何预定引用的临时课表;删除未被引用的临时时间表。依据:`ProfileService.cs:295-328`。
#### 13.4.8 附加设置(`attachedObjects`)——一等公民
**定位**:附加设置**不是** 1.7 遗留字段,而是 ClassIsland 2.x 仍在使用的核心机制:可附着对象(`AttachableSettingsObject`)自带 `Dictionary<Guid, object?> AttachedObjects`,键是"附加设置提供者的 Guid",值是设置对象;界面通过 `AttachedSettingsControlPresenter` 读写它(`repos/ClassIsland/ClassIsland.Shared/AttachableSettingsObject.cs:10-15`、`repos/ClassIsland/ClassIsland.Core/Controls/AttachedSettingsControlPresenter.axaml.cs:241-253`)。本实现必须把它升级为**一等公民字段**,供 06/07 部分的"课级提醒/课级附加行动/天气提醒"直接引用。
**承载对象**(均必须有 `attachedObjects` 字段):
| 对象 | 目标枚举 | 说明 |
|---|---|---|
| `Profile`(档案级) | — | 本实现扩展的档案级附加设置;ClassIsland 2.x 的可附着基类未覆盖档案,档案级为本实现新增【推断】 |
| `ClassPlan`(课表级) | `ClassPlan` | 整张课表的附加设置 |
| `ClassInfo`(课级) | `Lesson` | 单节课的附加设置(最常用,例如这节课用自定义提醒文案) |
| `TimeLayout`(时间表级) | `TimeLayout` | 整张时间表的附加设置 |
| `TimeLayoutItem`(时间点级) | `TimePoint` | 单个时间点的附加设置 |
| `Subject`(科目级) | `Subject` | 科目的附加设置 |
字段定义:`attachedObjects: map<GuidString, object>`,默认 `{}`;序列化字段名 **`attachedObjects`(camelCase)**;导入 ClassIsland 档案时**接受原拼写 `AttachedObjects`**(PascalCase),导出/写出统一 camelCase。值必须是普通 JSON 对象(结构化克隆可序列化),不得存函数或类实例。依据:`AttachableSettingsObject.cs:12-15`(字典键为 Guid);`AttachedSettingsControlPresenter.axaml.cs:245-253`(目标对象 → `AttachedSettingsTargets` 的映射)。
**已知附加设置 Guid 与设置对象类型(必须内置映射)**:
| Guid | 名称 | 设置对象类型 | 允许附着目标 | 依据 |
|---|---|---|---|---|
| `58e5b69a-764a-472b-bcf7-003b6a8c7fdf` | 课程显示设置 | `LessonControlAttachedSettings` | ClassPlan / TimeLayout / Lesson / Subject / TimePoint | `LessonControlAttachedSettingsControl.axaml.cs:10-14`、`AttachedSettingsTargets.cs` |
| `08f0d9c3-c770-4093-a3d0-02f3d90c24bc` | 上课提醒设置 | `ClassNotificationAttachedSettings` | ClassPlan / TimeLayout / Lesson / Subject / TimePoint | `ClassNotificationAttachedSettingsControl.axaml.cs:11-15` |
| `8fbc3a26-6d20-44dd-b895-b9411e3ddc51` | 放学提醒设置 | `AfterSchoolNotificationAttachedSettings` | ClassPlan / TimeLayout | `AfterSchoolNotificationAttachedSettingsControl.axaml.cs:10-12` |
| `7625de96-38aa-4b71-b478-3f156dd9458d` | 天气提醒设置 | `WeatherNotificationAttachedSettings` | TimePoint | `WeatherNotificationAttachedSettingsControl.axaml.cs:10-12` |
**设置对象字段(默认值必须逐项一致)**:
- `LessonControlAttachedSettings`:`IsAttachSettingsEnabled=false`、`ShowExtraInfoOnTimePoint=true`、`ExtraInfoType=0`、`IsCountdownEnabled=true`、`CountdownSeconds=60`、`ExtraInfo4ShowSecondsSeconds=0`、`ScheduleSpacing=1`、`ShowCurrentLessonOnlyOnClass=false`、`IsNonExactCountdownEnabled=false`。依据:`repos/ClassIsland/ClassIsland.Core/Models/AttachedSettings/LessonControlAttachedSettings.cs:7-17`。
- `ClassNotificationAttachedSettings`:`IsClassOnNotificationEnabled=true`、`IsClassOnPreparingNotificationEnabled=true`、`IsClassOffNotificationEnabled=true`、`ClassPreparingDeltaTime=60`、`ClassOnPreparingText="准备上课,请回到座位并保持安静,做好上课准备。"`、`OutdoorClassOnPreparingText="下节课程为户外课程,请合理规划时间,做好上课准备。"`、`ClassOnPreparingMaskText="即将上课"`、`OutdoorClassOnPreparingMaskText="即将上课"`、`ClassOnMaskText="上课"`、`ClassOffMaskText="课间休息"`、`ClassOffOverlayText=""`、`IsAttachSettingsEnabled=false`。依据:`repos/ClassIsland/ClassIsland/Models/AttachedSettings/ClassNotificationAttachedSettings.cs:7-24`。
- `AfterSchoolNotificationAttachedSettings`:`IsEnabled=true`、`NotificationMsg="今天的课程已结束,请同学们有序离开。"`、`IsAttachSettingsEnabled=false`。依据:`repos/ClassIsland/ClassIsland/Models/AttachedSettings/AfterSchoolNotificationAttachedSettings.cs:7-14`。
- `WeatherNotificationAttachedSettings`:`AlertShowMode=Default`、`ForecastShowMode=Default`、`IsAttachSettingsEnabled=true`。依据:`repos/ClassIsland/ClassIsland/Models/AttachedSettings/WeatherNotificationAttachedSettings.cs:7-17`。
- 所有设置对象都带 `IsAttachSettingsEnabled`:**只有它为 true 时附加设置才生效**;为 false 时按空对象处理并允许回落到全局设置。
**读写与兼容规则(强制)**:
1. 读取时必须"按目标对象类型过滤 Guid":若某 Guid 允许的目标不包含当前对象类型,保留数据但**不得生效**(不能因为档案来自别处就崩溃)。
2. 未知 Guid 必须原样保留(不删除、不报错),以便插件卸载后重装仍能恢复设置;UI 显示为"未知附加设置(可删除)"。
3. 值里出现 `null` 时按"未设置"处理;导出的 JSON 必须省略值为 `null` 的键(对齐 `object?` 语义)。
4. 导入 ClassIsland 档案的 `AttachedObjects` 时:键 Guid 统一小写、值按上表类型反序列化;未知 Guid 的 `JsonElement` 原样保留;**不得**丢弃。
5. 导出为 ClassIsland 档案时字段名写 `AttachedObjects`(PascalCase),以保持双向可导入。
6. 附加设置必须参与 `ProfileService.SaveProfile` 的保存触发(对象属性变化 → 档案保存),并在 UI 中提供"清除该对象全部附加设置"的入口。
### 13.5 组件布局(ComponentProfile)schema
文件:`data/component-layouts/<CurrentComponentConfig>.json`,默认 `Default.json`。依据:`ComponentsService.cs:28-58,122-166`。
#### 13.5.1 顶层
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `Lines` | MainWindowLineSettings[] | 1 行 + 2 个默认组件 | 主界面行集合 |
默认组件配置的固定内容:一行,含两个组件,`Id` 分别为 `df3f8295-21f6-482e-bada-fa0e5f14bb66`(日期)与 `1db2017d-e374-4bc6-9d57-0b4adf03a6b8`(时间)。依据:`ComponentsService.cs:33-52`。**这两个 GUID 必须保留**(导入 ClassIsland 组件布局时按 GUID 匹配)。
#### 13.5.2 MainWindowLineSettings(主界面行)
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `Children` | ComponentSettings[] | `[]` | `MainWindowLineSettings.cs` |
| `IsMainLine` | boolean | `false` | 同上 |
| `IsNotificationEnabled` | boolean | `true` | 同上 |
| `IsResourceOverridingEnabled` | boolean | `false` | 同上 |
| `MainWindowSecondaryFontSize` / `Body` / `Emphasized` / `Large` | number | `14/16/18/20` | 同上 |
| `IsCustomForegroundColorEnabled` / `ForegroundColor` | boolean / string | `false` / `#1E90FFFF` | 同上 |
| `BackgroundOpacity` / `IsCustomBackgroundOpacityEnabled` | number / boolean | `0.5` / `false` | 同上 |
| `BackgroundColor` / `IsCustomBackgroundColorEnabled` | string / boolean | `#000000FF` / `false` | 同上 |
| `CustomCornerRadius` / `IsCustomCornerRadiusEnabled` | number / boolean | `0` / `false` | 同上 |
| `Opacity` | number | `1.0` | 同上 |
| `IsVisible` | boolean | `true` | 同上 |
| `HideOnRule` | boolean | `false` | 同上 |
| `HidingRules` | Ruleset | 空规则集 | 同上 |
| `IslandSeparationMode` | number | `0` | 同上(`0` 默认 / `1` 禁用 / `2` 启用) |
#### 13.5.3 ComponentSettings(单个组件)
| 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|
| `Id` | string | 组件 GUID(小写) | `ComponentSettings.cs:56-66` |
| `NameCache` | string | `""`(组件未加载时显示) | `:71-80` |
| `Settings` | object? | 组件专属设置对象 | `:85-95` |
| `HideOnRule` | boolean | `false` | `:100-109` |
| `HidingRules` | Ruleset | 空规则集 | `:114-123` |
| `IsResourceOverridingEnabled` | boolean | `false` | `:149-158` |
| `MainWindowSecondaryFontSize` / `Body` / `Emphasized` / `Large` | number | `14/16/18/20` | `:161-180` 起 |
| `ForegroundColor` / `IsCustomForegroundColorEnabled` | string / boolean | `#1E90FFFF` / `false` | `:28-29` |
| `IsMinWidthEnabled` / `MinWidth` | boolean / number | `false` / `100` | `:30-31` |
| `IsMaxWidthEnabled` / `MaxWidth` | boolean / number | `false` / `300` | `:32-33` |
| `IsFixedWidthEnabled` / `FixedWidth` | boolean / number | `false` / `200` | `:34-35` |
| `HorizontalAlignment` | enum | `Stretch` | `:36`(`Left/Center/Right/Stretch`) |
| `RelativeLineNumber` | number | `0` | `:37` |
| `IsVisible` | boolean | `true` | `:38` |
| `BackgroundOpacity` / `IsCustomBackgroundOpacityEnabled` | number / boolean | `0.5` / `false` | `:40-41` |
| `BackgroundColor` / `IsCustomBackgroundColorEnabled` | string / boolean | `#000000FF` / `false` | `:42-43` |
| `CustomCornerRadius` / `IsCustomCornerRadiusEnabled` | number / boolean | `8` / `false` | `:44-45` |
| `Opacity` | number | `1.0` | `:46` |
| `IsCustomMarginEnabled` + `MarginLeft/Top/Right/Bottom` | boolean + number×4 | `false` + `0` | `:47-51` |
组件专属设置对象必须在 `data/component-layouts/` 同一文件内序列化,其字段名按组件设置模型:`ClockComponentSettings`、`CountDownComponentSettings`、`LessonControlSettings`、`RollingComponentSettings`、`SlideComponentSettings`、`StackComponentSettings`、`GroupComponentSettings`、`TextComponentSettings`、`WeatherComponentSettings`。依据:`repos/ClassIsland/ClassIsland/Models/ComponentSettings/*.cs`。**组件设置的字段名与默认值必须逐项对齐这 9 个模型**(具体见第 5 部分组件规格)。
**组件迁移规则**:加载组件布局时,若组件 `Id` 命中 `ComponentRegistryService.MigrationPairs`,必须把 `Id` 改成新 GUID、`IsMigrated=true`、`MigrationSource=旧 GUID`(两者均 `[JsonIgnore]` 不落盘),迁移后立即保存。依据:`ComponentsService.cs:140-161`、`ComponentSettings.cs:125-128`。
### 13.6 自动化配置 schema
文件:`data/automations/<CurrentAutomationConfig>.json`,默认 `Default.json`;顶层类型是 **Workflow 数组**。依据:`AutomationService.cs:27-32,173-208,441`。
| 层级 | 字段 | 类型 | 默认值 | 依据 |
|---|---|---|---|---|
| Workflow | `Triggers` | TriggerSettings[] | `[]` | `Workflow.cs:12-14` |
| Workflow | `IsConditionEnabled` | boolean | `false` | `Workflow.cs:17-18` |
| Workflow | `Ruleset` | Ruleset | 空规则集 | `Workflow.cs:21-22` |
| Workflow | `ActionSet` | ActionSet | 新行动组 | `Workflow.cs:25-26` |
| TriggerSettings | `Id` | string | `""` | `TriggerSettings.cs:14-30` |
| TriggerSettings | `Settings` | object? | `null` | `TriggerSettings.cs:33-45` |
| ActionSet | `Name` | string | `"新行动组"` | `ActionSet.cs:74-77` |
| ActionSet | `Actions` | ActionItem[] | `[]` | `ActionSet.cs:79-83`(序列化名必须为 `Actions`) |
| ActionSet | `IsEnabled` | boolean | `true` | `ActionSet.cs:88` |
| ActionSet | `IsRevertEnabled` | boolean | `false` | `ActionSet.cs:93` |
| ActionSet | `Status` | number | `Normal` | `ActionSet.cs:104-105` |
| ActionSet | `Guid` | string(Guid) | 新建 Guid | `ActionSet.cs:115` |
| ActionItem | `Id` | string | `""` | `ActionItem.cs:43` |
| ActionItem | `Settings` | object? | `null`(null 时不写该字段) | `ActionItem.cs:50-51` |
**规则集(Ruleset)嵌套结构**(必须精确实现):
```json
{
"Mode": 0,
"IsReversed": false,
"Groups": [
{
"Rules": [{ "IsReversed": false, "Id": "classisland.lessons.currentTimeStateChanged", "Settings": {} }],
"Mode": 1,
"IsReversed": false,
"IsEnabled": true
}
]
}
```
- `Ruleset.Mode` 默认 `Or`(`Ruleset.cs:14`)、`RuleGroup.Mode` 默认 `And`(`RuleGroup.cs:14`);两者均支持 `IsReversed`;`RuleGroup.IsEnabled` 默认 `true`;`Ruleset.Groups` 默认含一个"一个空规则"的组(`Ruleset.cs:16`)。`State` 是运行期字段(`[JsonIgnore]`),不得落盘。
- 触发器 ID 必须使用以下注册表 ID(导入 ClassIsland 自动化时按此匹配):
| 触发器 | ID | 依据 |
|---|---|---|
| 上课时 | `classisland.lessons.onClass` | `OnClassTrigger.cs:8` |
| 课间休息时 | `classisland.lessons.onBreakingTime` | `OnBreakingTimeTrigger.cs:8` |
| 放学时 | `classisland.lessons.onAfterSchool` | `OnAfterSchoolTrigger.cs:8` |
| 当前时间状态变化 | `classisland.lessons.currentTimeStateChanged` | `CurrentTimeStateChangedTrigger.cs:8` |
| 特定时间点前 | `classisland.lessons.preTimePoint` | `PreTimePointTrigger.cs:13` |
| 应用启动时 | `classisland.lifetime.startup` | `AppStartupTrigger.cs:10` |
| 应用退出时 | `classisland.lifetime.stopping` | `AppStoppingTrigger.cs:8` |
| cron | `classisland.cron` | `CronTrigger.cs:15` |
| 收到信号时 | `classisland.signal` | `SignalTrigger.cs:8` |
| 调用 Uri 时 | `classisland.uri` | `UriTrigger.cs:8` |
| 从托盘菜单运行 | `classisland.trayMenu` | `TrayMenuTrigger.cs:11` |
| 规则集更新时 | `classisland.ruleSet.rulesetChanged` | `RulesetChangedTrigger.cs:8` |
行动 ID:`classisland.showNotification`(显示提醒)、`classisland.notification.weather`(天气提醒)、`classisland.os.run`(运行)、`classisland.action.sleep`(等待)、`classisland.app.quit`(退出)、`classisland.app.restart`(重启)、`classisland.settings`(应用设置)。依据:`repos/ClassIsland/ClassIsland/Services/Automation/Actions/*.cs` 的 `ActionInfo` 特性。
### 13.7 提醒数据
1. **运行期队列不落盘**(重启丢失可接受);**通知历史必须落盘**到 `data/notification-history.json`,结构为:
```json
{
"schemaVersion": 1,
"items": [
{
"id": "uuid",
"providerId": "classisland.notification.class",
"title": "上课提醒",
"content": "语文课 08:00-08:45",
"priority": 3,
"channelId": "classisland.channel.desktop",
"createdAt": "2026-11-12T07:59:00",
"read": false,
"speech": "准备上课",
"overlayDurationMs": 5000,
"maskDurationMs": 5000
}
]
}
```
2. 历史保留条数由设置项 `notificationHistoryLimit` 控制:默认 `500`,合法区间 `100–5000`;超限按最旧优先滚动删除(不得一次性清空)。"清空通知"必须二次确认并写审计日志。落盘必须**批量节流**:累积 ≥ 5 条或距上次写入 ≥ 5 秒才落盘一次,退出时强制 flush;不得每来一条通知就写一次盘。**【推断】**(数量上限为工程决策,ClassIsland 不落盘历史。)
3. 提醒设置对象 `NotificationSettings` 的字段与默认值必须为:`IsNotificationEnabled=true`、`IsSpeechEnabled=false`、`IsNotificationEffectEnabled=true`、`IsNotificationSoundEnabled=false`、`NotificationSoundPath=""`、`IsSettingsEnabled=false`、`IsNotificationTopmostEnabled=false`。依据:`repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationSettings.cs:11-17`。
4. 提醒播放会话字段:`OverlayContent`、`MaskContent`、`OverlayDuration`(默认 5s)、`MaskDuration`(默认 5s)、`TargetOverlayEndTime`、`TargetMaskEndTime`、`OverlaySpeechContent`、`MaskSpeechContent`、`IsSpeechEnabled`(默认 true)、`RequestNotificationSettings`、`ChannelId`、`State`、`LeftProgress`(默认 1.0)。依据:`repos/ClassIsland/ClassIsland.Core/Models/Notification/NotificationRequest.cs:14-160`、`repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationRequest.cs:16-20`。
5. 优先级排序必须与 ClassIsland 一致:`IsPriorityOverride` 最高(插队);否则先比 `Priority`(数值小优先),再比入队 `Index`。依据:`NotificationPriority.CompareTo`(`repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationPriority.cs`)。
### 13.8 主题数据
1. 主题目录:`data/themes/<主题Id>/`(解压后),启用列表 `data/themes/EnabledThemes.json`(`string[]`),主题包缓存 `cache/theme-packages/<主题Id>.zip`。依据:`XamlThemeService.cs:47-49,392-440`。
2. 主题清单 `ThemeManifest` 字段与默认值:`Id=""`、`Name=""`、`Author=""`、`Url=""`、`Description=""`、`Version="0.0.0.0"`、`Banner="banner.png"`、`VerticalSafeAreaPx=0`。依据:`repos/ClassIsland/ClassIsland.Core/Models/XamlTheme/ThemeManifest.cs`。
3. 启用/禁用主题后必须重建样式表并持久化;**禁用全部主题时必须回落到内置主题**,不得白屏。
4. 主题包安装必须:校验 zip 路径安全 → 校验清单必填字段 → 解压到 `data/themes/<Id>/` → 若已存在则保留用户自定义值(按字段合并,不整目录覆盖)。
5. **默认离线(强制)**:主题市场与插件市场同一策略——安装包**不内置在线官方索引**,只内置离线示例索引;用户必须手动添加索引 URL 并二次确认后才联网;网络失败不得影响本机已安装主题与 `EnabledThemes.json`。依据:与 12.3 第 7 条同一产品决策【推断】。
### 13.9 其他存储
1. **机器级全局存储**:`Global.json`(键值对字符串),ClassIsland 放在 `%LOCALAPPDATA%\ClassIsland\Global\Global.json`;本实现放 `<userData>/global.json`,用于"上次运行版本、是否首次启动、Sentry 开关、MLE 标记"等不适合放进用户配置的信息。依据:`repos/ClassIsland/ClassIsland.Core/Services/GlobalStorageService.cs`。
2. **设置叠层**:自动化/插件临时覆盖设置时必须记录"原值 + 覆盖者 Guid",移除覆盖者后按栈顶恢复;不得直接改配置后忘记恢复。依据:`repos/ClassIsland/ClassIsland/Services/SettingsService.cs:191-259`。
3. **信任档案列表**:`trustedProfileIds` 默认 `[]`;首次加载未信任档案时必须提示"该档案来自外部,是否信任";不允许自动化在未信任档案下运行(对齐 `AutomationService` 对 `IsCurrentProfileTrusted` 的检查)。依据:`ProfileService.cs:166-176,398-422`。
4. **缓存目录可安全清空**:`cache/` 下任何内容删除后不得影响功能(下次自动重建);设置页"清理缓存"不得删除 `data/`、`backups/`、`config.json`。
### 13.10 备份、恢复与迁移(必须实现)
**加载时备份**:
1. 每次读取 JSON 成功 → 复制为 `.bak`;失败 → 用 `.bak` 恢复并记入错误列表。依据:`ConfigureFileHelper.cs:63-96`。
2. `.bak` 只保留一份(覆盖式),不做多版本滚动;多版本由 `backups/` 承担。
**整包备份**:
1. 备份内容固定为:`config.json`、`Settings.json`(若存在)、`data/` 全目录、`data/themes/EnabledThemes.json`;**不得**包含 `cache/`、`logs/`、`updates/`、插件包缓存。依据:`repos/ClassIsland/ClassIsland/Services/FileFolderService.cs:115-168`(备份 `Config/` + `Profiles/` + `Settings.json`,排除缓存/日志)。
2. 备份文件名:自动备份 `Auto_Backup_yy-MMM-dd_HH-mm-ss.zip`,手动备份 `Backup_yy-MMM-dd_HH-mm-ss.zip`。依据:`FileFolderService.cs:128-132`。
3. 自动备份策略:`isAutoBackupEnabled=true` 时,距上次备份 ≥ `autoBackupIntervalDays`(默认 7 天)才执行;只保留最近 `autoBackupLimit`(默认 16)份 `Auto_*`,按文件修改时间倒序删除多余。依据:`FileFolderService.cs:83-113`。
4. 备份写入必须"先写 `.tmp` 再 rename";备份失败不得影响启动与当前数据(只记日志并在设置页提示)。
5. 恢复流程:选择备份 → **先自动创建一份"恢复前快照"** → 校验 zip 结构(必须含 `config.json` 或 `Settings.json` + `data/profiles/Default.json`)→ 解压到临时目录 → 原子替换 → 提示"重启后生效" → 重启应用。恢复失败必须回滚到快照。
6. 备份必须可在资源管理器里直接解压查看(不得加密、不得用私有格式)。
**版本迁移**:
1. 必须在 `config.json` 写入 `schemaVersion`(当前 `1`),并在 `data/profiles/*.json` 内写 `SchemaVersion`(当前 `1`,允许缺省视为 1)。**这是本实现的工程必需字段**(ClassIsland 没有,但迁移流水线需要显式版本号)。**【推断】**
2. 迁移执行顺序:读取文件 → 记录 `fromVersion` → 逐级执行 `migrations[fromVersion → fromVersion+1]` → 写回 → 备份迁移前文件到 `backups/migrate-<from>-to-<to>-<时间>.zip`。任一步失败必须中止并保留原文件,提示"迁移失败,已保留原配置"。
3. ClassIsland 形迁移规则必须实现(依据 `repos/ClassIsland/ClassIsland/Services/SettingsService.cs:134-168`):
- 旧版本 `LastAppVersion < 2.0.0.2`:重新登记 URL 协议(`classisland://`)。
- 旧版本 `LastAppVersion < 2.0.3.0`:重置教程初始化标记(`WillMigrateInitTutorial`)。
- 旧版本 `LastAppVersion < 2.0.3.3`:重写开机自启快捷方式(先禁用再启用)。
- 迁移完成后必须把 `LastAppVersion` 更新为当前版本(`SettingsService.cs:112`)。
4. 从 ClassIsland 导入必须支持两种入口:`--importV1 <dir>`(1.x)、`--importV2 <dir>`(2.x);导入前必须自动备份当前数据;导入后必须展示"导入结果清单"(成功项/跳过项/失败项及原因),不得静默覆盖。依据:`repos/ClassIsland/ClassIsland/Program.cs:60-65`。
5. 组件迁移:`ComponentRegistryService.MigrationPairs` 必须内置一份"旧 GUID → 新 GUID"映射;导入旧组件布局时按映射改写并保存。依据:`ComponentsService.cs:140-161`。
6. 空 GUID 兼容:所有 `Guid` 字段读到 `""` 必须解析为 `Guid.Empty`;写出时 `Guid.Empty` 写成 `""`(对齐 `GuidEmptyFallbackConverter` 的双向规则)。依据:`repos/ClassIsland/ClassIsland.Shared/JsonConverters/GuidEmptyFallbackConverter.cs`。
7. 颜色兼容:读入支持 `#RRGGBB`、`#RRGGBBAA`、`#AARRGGBB` 三种写法(ClassIsland 写出为 `#RRGGBBAA`,Alpha 在尾部);写出统一 `#RRGGBBAA` 大写。依据:`repos/ClassIsland/ClassIsland.Core/Converters/ColorHexJsonConverter.cs`(`AlphaComponentPosition.Trailing`)。
8. 时间兼容:`DateTime` 写出为 ISO `yyyy-MM-ddTHH:mm:ss`(无时区,本地时间);`TimeSpan` 写出为 `hh:mm:ss`(超过 24h 用 `d.hh:mm:ss`);读入必须同时接受带时区与不带时区的 ISO 字符串,并按本地时区归一。依据:`repos/ClassIsland/examples/example.json:10-11` 与 System.Text.Json 默认 `TimeSpan` 格式。
9. 枚举兼容:所有枚举按**数字**落盘(如 `TimeType`、`UpdateStatus`、`RulesetLogicalMode`),不得写出名字;读到未知数字必须夹紧到合法范围并记 warn。依据:ClassIsland 使用默认 `System.Text.Json` 序列化(`ConfigureFileHelper.SerializerOptions = new()`,`ConfigureFileHelper.cs:30`),枚举默认输出数字。
10. 迁移必须**幂等**:同一版本重复执行不得产生二次修改(用 `schemaVersion` 与字段存在性双重判断)。
**Excel 导入导出(P1 必须实现,xlsx 重建)**:
11. 必须用 `xlsx`(SheetJS)在**主进程/utilityProcess** 中重建 ClassIsland 的 Excel 导入导出能力;ClassIsland 的 `unvell.ReoGrid` + WPF 窗口实现(`repos/ClassIsland/ClassIsland/Views/ExcelImportWindow.xaml.cs:29-44`、`ExcelExportWindow.xaml.cs:23-43`)**只作字段与流程参考**,不得照搬 UI。
12. 导入最低能力:读取 `.xlsx` → 选择工作表 → 识别"行 = 节次 / 列 = 星期"的网格 → 生成 `ClassPlan` + 对应 `TimeLayout`;必须支持合并单元格、空节次、科目名去重、教师名写入 `Subject.TeacherName`;解析失败必须给出行列级错误提示,且**不得**覆盖现有档案(先写入临时档案,用户确认后替换)。
13. 导出最低能力:把当前档案/课表写成 `.xlsx`(一个工作表一张课表,首行星期、首列节次与起止时间),首行冻结;导出文件默认落 `下载/ClassIsland-课表-<日期>.xlsx`,由用户选择路径。
14. 导入导出必须遵守 P1 数据兼容:导出的 xlsx 再导入必须得到等价档案(科目/时间表/课表/周次规则不丢);单文件上限 8MB(对齐 ClassHelper 服务端表格上限,见 `repos/ClassHelper-master/AGENTS.md` §2);导入前自动备份当前档案。
### 13.11 读写一致性与错误处理
1. 所有加载函数必须返回"数据 + 警告列表",不得只返回数据;设置页与诊断页必须能展示"哪些文件回退了备份、哪些字段被重置"。
2. 阻止"写入正在读取的文件":每个文件维护内存版本号,写入前比较版本;发现外部修改(用户在资源管理器里改了 JSON)必须提示"配置已被外部修改,是否重新加载",禁止静默覆盖用户改动。**【推断】**
3. 任何写入失败必须:记 error(含路径、错误码、可用磁盘空间)→ 提示用户 → 保留旧文件不变(不得留下半截文件)。
4. 磁盘空间不足(< 100MB)时禁止自动备份并提示"清理磁盘";备份与导出前必须检查可用空间。
5. 读取时必须限制单文件大小(配置文件 ≤ 8MB、档案 ≤ 32MB、备份 zip ≤ 512MB),超限拒绝并提示,防止恶意文件把内存打爆。**【推断】**
### 13.12 天气数据 schema(与 07 天气服务对齐)
1. **缓存位置与字段**:最后一份天气必须持久化在 `config.json` 的 `lastWeatherInfo`(与 ClassIsland 把 `Settings.LastWeatherInfo` 当缓存一致,见 `repos/ClassIsland/ClassIsland/Services/WeatherService.cs:48-51,387`),并附加 `lastWeatherInfoUpdatedAt`(ISO 本地时间)与 `lastWeatherInfoStale`(boolean)。缓存**不得**因查询失败被清空:失败时保留旧值并标记 `stale`,由 UI 展示"天气数据可能已过期"。
2. **刷新节奏**:天气服务必须每 5 分钟检查一次(`WeatherService.cs:41-44`)、启动后立即查询一次(`:71`);查询失败按 30s/60s/120s 退避重试 3 次后停到下个周期。请求默认使用 `https://`;仅当用户显式开启 `noTLSWeatherRequests` 时才允许 `http://`(对齐 `WeatherService.cs:34`)。
3. **来源抽象**:不得把第三方接口写死在渲染层。必须定义 `WeatherProvider` 接口(`id`、`name`、`requiresNetwork`、`fetch()`),内置"小米天气"参考实现并允许替换:城市信息 `GET {base}/location/city/info?locationKey=<cityId>&locale=zh_cn`、全量天气 `GET {base}/weather/all?latitude=<lat>&longitude=<lon>&locationKey=<cityId>&days=15&appKey=<key>&sign=<sign>&isGlobal=false&locale=zh_cn`(依据 `WeatherService.cs:327,358-360`)。`base/appKey/sign` 必须可配置、不得出现在渲染进程产物中;默认天气提示"需联网"。
4. **预警清洗**:查询结果必须按 `excludedWeatherAlerts`(默认 `[]`)过滤——`Title` 或 `Detail` 包含排除词即剔除(`WeatherService.cs:363-374`);同一 `Type` 只保留 `PubTime` 最新一条(`:376-385`);`alerts` 按 `pubTime` 倒序展示。
5. **图标与码表**:`weatherIconId` 默认 `classisland.weatherIcons.lucide`(`Settings.cs:1849`);天气码到文案的映射必须来自内置资源 `Assets/XiaomiWeather/xiaomi_weather_status.json`(`WeatherService.cs:228-233`),并提供 `getWeatherTextByCode(code)` 等价函数。
6. **数据结构(字段名必须与 07 一致,JSON 键为 camelCase;ClassIsland 原始模型用 `[JsonPropertyName]` 指定小写键,可直接对齐)**:
| 类型 | 字段 | 类型 | 默认值 |
|---|---|---|---|
| `WeatherInfo` | `current` | `CurrentWeather` | 空对象 |
| | `alerts` | `WeatherAlert[]` | `[]` |
| | `updateTime` | number(Unix 毫秒) | `0` |
| | `forecastDaily` | `ForecastDaily` | 空对象 |
| | `forecastHourly` | `ForecastHourly` | 空对象 |
| | `minutely` | `Minutely` | 空对象 |
| | `aqi` | `AqiInfo` | 空对象 |
| `CurrentWeather` | `feelsLike` / `humidity` / `pressure` / `temperature` / `visibility` | `ValueUnitPair {value:string, unit:string}` | `{"",""}` |
| | `weather` | string(天气码) | `"99"` |
| | `pubTime` | string(ISO) | 查询时间 |
| | `wind` | `WindInfo {direction:ValueUnitPair, speed:ValueUnitPair}` | 空对象 |
| `WeatherAlert` | `locationKey` / `alertId` / `title` / `type` / `level` / `detail` | string | `""` |
| | `pubTime` | string(ISO) | 当前时间 |
| | `images` | `map<string,string>` | `{}`(至少含 `icon`) |
| `ForecastDaily` | `precipitationProbability` | `StatusValueBase<string[]>` | `{value:[]}` |
| | `temperature` / `weather` / `sunRiseSet` | `StatusValueBase<RangedValue[]>`(`RangedValue {from,to}`) | `{value:[]}` |
| `ForecastHourly` | `temperature` / `weather` | `StatusValueBase<number[]>` | `{value:[]}` |
| `Minutely` | `precipitation` | `MinutelyPrecipitation {value:number[]}` | `{value:[]}` |
| `AqiInfo` | `aqi` | string | `"0.0"` |
| `WeatherQueryResult` | `isSuccess` / `isPrecisionDegraded` | boolean | `false` |
| | `errorMessage` | string? | `null` |
| | `degradedPrecision` | number | `0` |
7. **派生字段(不得落盘,读取时计算)**:`WeatherInfo.updateTime` → 本地时间;`MinutelyPrecipitation.rainRemainingMinutes`(首值为 0 时返回"接下来多少分钟开始下雨",首值 >0 时返回负的"还有多少分钟停雨",算法见 `repos/ClassIsland/ClassIsland.Core/Models/Weather/MinutelyPrecipitation.cs`);`AqiInfo.aqiLevel`(1–6,按 50/100/150/200/300 分档,见 `AqiInfo.cs`)。
8. **与 07 的契约**:07 天气服务必须复用本节字段名与派生字段;规则集 handler ID 必须为 `classisland.weather.currentWeather`、`classisland.weather.tomorrowWeather`、`classisland.weather.hasWeatherAlert`、`classisland.weather.rainTime`、`classisland.weather.sunRiseSet`(`WeatherService.cs:64-68`);提醒文案与图标渲染由 07 定义,但**读取的数据结构只能是本节 schema**。
### 13.13 验收标准(第 13 部分)
1. 首次启动能在 `<userData>/` 下生成完整的目录结构与默认 `config.json`、`Default.json` 档案、`Default.json` 组件布局、默认科目表;目录清单与 13.2 完全一致。
2. 删除 `config.json` 后应用能正常启动并重建默认配置;删除 `data/` 后能重建默认档案与组件布局,不白屏、不崩溃。
3. 故意把 `config.json` 改成非法 JSON 后启动:应用回退到 `.bak`;若 `.bak` 也非法则使用默认值并进入"配置错误"提示,不得启动失败。
4. `Settings.json`/档案的字段名、类型、默认值与 13.3/13.4 表一致;用 ClassIsland 的真实档案 JSON 导入后,时间表、课表、科目、周次规则全部正确解析(含 `TimeType=3` 与 `IsHideDefault`、`AttachedObjects`)。**并且**:运行期只写 `config.json`,`Settings.json` 只读导入/单独导出、不双向双写;`IsIslandSeperated` 原拼写能被导入与导出映射。
5. 写入过程中断电(模拟 `rename` 前杀进程)不会损坏原文件;`.tmp` 残留有清理机制。
6. 自动备份:把 `lastAutoBackupTime` 改为 8 天前,启动后生成 `Auto_Backup_*.zip`;备份超过 16 份时最旧的 `Auto_*` 被删除,手动 `Backup_*` 不受影响。
7. 备份 zip 内必须含 `config.json`、`data/profiles/Default.json`、`data/component-layouts/Default.json`、`data/automations/Default.json`,且不含 `cache/`、`logs/`、`updates/`。
8. 恢复备份:选择备份 → 自动生成恢复前快照 → 恢复 → 重启后数据与备份一致;中途失败能回滚。
9. 迁移流水线:把 `schemaVersion` 改为 0 的旧结构文件,启动后能迁移到 1 并保留原字段;迁移失败时原文件不被破坏。
10. 组件布局导入旧 GUID 后按 `MigrationPairs` 改写,且迁移后立即保存、不重复迁移。
11. 空 GUID、`#RRGGBB`、带时区 ISO、数字枚举四类兼容用例全部通过(读写往返不丢信息)。
12. 档案切换/删除后 `config.selectedProfile` 指向有效文件;指向不存在文件时回落到 `Default.json` 并提示。
13. 自动化配置按 `Workflow[]` 落盘;触发器/行动 ID 与 13.6 表一致;规则集 `Mode`/`IsReversed`/`IsEnabled` 往返不丢。
14. 通知历史落盘、默认上限 500(可配 100–5000)、超限滚动删除、写入节流(≥5 条或 ≥5s)、清空需二次确认;历史文件损坏时回退为空列表并记警告,不影响提醒播放。
15. 所有写入路径都能在世界可读的 JSON 中直接看到内容(无私有封装、无加密),且 UTF-8 无 BOM、LF 换行。
16. 附加设置:Profile/ClassPlan/ClassInfo/TimeLayout/TimeLayoutItem/Subject 都能读写 `attachedObjects`;4 个已知 Guid 的设置对象默认值正确;导入 `AttachedObjects`(PascalCase)后可按目标生效;未知 Guid 不丢失;导出写回 `AttachedObjects`。
17. 天气:`lastWeatherInfo` 缓存可持久化、失败时保留旧值并标 `stale`;`WeatherInfo`/`WeatherAlert`/`ForecastDaily`/`ForecastHourly`/`Minutely`/`AqiInfo`/`WeatherQueryResult` 字段与 13.12 完全一致;预警按排除词过滤并按 Type 保留最新;5 个天气规则 ID 可被 07 使用。
18. Excel:能从 `.xlsx` 导入生成课表(先临时档案、确认后替换、导入前备份),能导出 `.xlsx` 再导入得到等价档案;8MB 上限;所有解析在 utilityProcess 或 worker 中执行,不阻塞主进程。
19. 坐标:`config.json` 中所有几何字段(`windowDockingOffsetX/Y`、`scale`、`radiusX/Y`、字号、间距)均为 DIP 数值;把 `scale` 由 1 改为 1.5 后窗口与岛按 CSS px 等比放大,不出现 devicePixelRatio 二次换算。
20. 主题市场默认离线:无内置在线官方索引;手动添加 URL 需二次确认;断网时本机已安装主题与 `EnabledThemes.json` 正常工作。