- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
674 lines
62 KiB
Markdown
674 lines
62 KiB
Markdown
## 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` 正常工作。
|