- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
62 KiB
13. 数据模型、存储、备份与迁移
13.1 存储总则(必须遵守)
- 本应用是纯本地单机应用,所有设置、档案、课表、组件、自动化、提醒历史、主题、插件配置必须存在本机,不得依赖任何服务器;断网、无账号、首次启动都必须完整可用。
- 存储根目录必须是
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划分)。 - 所有持久化文件必须是 UTF-8 无 BOM、LF 换行、两空格缩进的 JSON;字段名大小写必须按第 13.3/13.4 节的约定(ClassIsland 兼容文件用 PascalCase,本应用运行期配置用 camelCase)。读入时必须同时容忍紧凑格式(无缩进、无换行)与带注释之外的一切合法 JSON;不得写注释(ClassIsland 的
examples/example.json带注释只是示例,真实加载器不支持注释,见repos/ClassIsland/examples/example.json:1-2)。 - 所有写入必须原子化:写
<file>.tmp→fsync→rename覆盖目标;禁止直接writeFile覆盖正在被读的文件。依据:repos/ClassIsland/ClassIsland.Shared/Helpers/ConfigureFileHelper.cs:182-188(FileStream.Flush(true)后关闭)。 - 每次成功读取 JSON 后必须把原文件复制为
<file>.bak;读取失败时自动回退加载.bak,成功后把.bak复制回目标(对齐ConfigureFileHelper.LoadConfigUnWrapped的语义,见ConfigureFileHelper.cs:63-96)。若.bak也失败,必须使用全默认值并把错误加入"配置错误列表"(ConfigError{ Path, Code, IsFallback, Exception },见repos/ClassIsland/ClassIsland.Shared/Models/ConfigError.cs),在设置页"诊断/存储"中展示。 - 所有配置对象必须"缺失字段用默认值补齐、未知字段忽略、越界值夹紧"。禁止因为新增字段就让老配置文件加载失败。依据:
repos/ClassIsland/ClassIsland/Services/SettingsService.cs:63-118(加载失败只记日志并继续)与config.ts:198-233(逐字段归一化)。 - 所有写入必须经过单一写入入口(
storage.ts的saveXxx()),禁止业务代码直接fs.writeFile;该入口负责合并写、备份、事件通知。依据:config.ts:235-346。
13.2 目录与文件清单(必须逐项命名一致)
<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 |
字段级兼容规则(必须实现):
- 新增字段一律有默认值,缺失即用默认值(
SettingsService.LoadSettingsAsync缺失文件时直接返回new Settings(),见SettingsService.cs:73-77)。 - 字段类型不匹配时用默认值覆盖并记 warn,不得整体抛错(对齐
config.ts:198-233每个字段逐个typeof判断)。 - 枚举越界值回落到默认值(如
theme非0/1/2→2)。 - 语义变更或废弃的字段必须保留读取(忽略)一个版本,禁止删除后导致老配置迁移失败。不得删除
isIslandSeperated这类历史拼写字段(Settings.cs:1852)。 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 };默认群 GuidACAF4EF0-E261-4262-B941-34EA93CB4369;全局群 GuidGuid.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 时按空对象处理并允许回落到全局设置。
读写与兼容规则(强制):
- 读取时必须"按目标对象类型过滤 Guid":若某 Guid 允许的目标不包含当前对象类型,保留数据但不得生效(不能因为档案来自别处就崩溃)。
- 未知 Guid 必须原样保留(不删除、不报错),以便插件卸载后重装仍能恢复设置;UI 显示为"未知附加设置(可删除)"。
- 值里出现
null时按"未设置"处理;导出的 JSON 必须省略值为null的键(对齐object?语义)。 - 导入 ClassIsland 档案的
AttachedObjects时:键 Guid 统一小写、值按上表类型反序列化;未知 Guid 的JsonElement原样保留;不得丢弃。 - 导出为 ClassIsland 档案时字段名写
AttachedObjects(PascalCase),以保持双向可导入。 - 附加设置必须参与
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)嵌套结构(必须精确实现):
{
"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 提醒数据
- 运行期队列不落盘(重启丢失可接受);通知历史必须落盘到
data/notification-history.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
}
]
}
- 历史保留条数由设置项
notificationHistoryLimit控制:默认500,合法区间100–5000;超限按最旧优先滚动删除(不得一次性清空)。"清空通知"必须二次确认并写审计日志。落盘必须批量节流:累积 ≥ 5 条或距上次写入 ≥ 5 秒才落盘一次,退出时强制 flush;不得每来一条通知就写一次盘。【推断】(数量上限为工程决策,ClassIsland 不落盘历史。) - 提醒设置对象
NotificationSettings的字段与默认值必须为:IsNotificationEnabled=true、IsSpeechEnabled=false、IsNotificationEffectEnabled=true、IsNotificationSoundEnabled=false、NotificationSoundPath=""、IsSettingsEnabled=false、IsNotificationTopmostEnabled=false。依据:repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationSettings.cs:11-17。 - 提醒播放会话字段:
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。 - 优先级排序必须与 ClassIsland 一致:
IsPriorityOverride最高(插队);否则先比Priority(数值小优先),再比入队Index。依据:NotificationPriority.CompareTo(repos/ClassIsland/ClassIsland.Shared/Models/Notification/NotificationPriority.cs)。
13.8 主题数据
- 主题目录:
data/themes/<主题Id>/(解压后),启用列表data/themes/EnabledThemes.json(string[]),主题包缓存cache/theme-packages/<主题Id>.zip。依据:XamlThemeService.cs:47-49,392-440。 - 主题清单
ThemeManifest字段与默认值:Id=""、Name=""、Author=""、Url=""、Description=""、Version="0.0.0.0"、Banner="banner.png"、VerticalSafeAreaPx=0。依据:repos/ClassIsland/ClassIsland.Core/Models/XamlTheme/ThemeManifest.cs。 - 启用/禁用主题后必须重建样式表并持久化;禁用全部主题时必须回落到内置主题,不得白屏。
- 主题包安装必须:校验 zip 路径安全 → 校验清单必填字段 → 解压到
data/themes/<Id>/→ 若已存在则保留用户自定义值(按字段合并,不整目录覆盖)。 - 默认离线(强制):主题市场与插件市场同一策略——安装包不内置在线官方索引,只内置离线示例索引;用户必须手动添加索引 URL 并二次确认后才联网;网络失败不得影响本机已安装主题与
EnabledThemes.json。依据:与 12.3 第 7 条同一产品决策【推断】。
13.9 其他存储
- 机器级全局存储:
Global.json(键值对字符串),ClassIsland 放在%LOCALAPPDATA%\ClassIsland\Global\Global.json;本实现放<userData>/global.json,用于"上次运行版本、是否首次启动、Sentry 开关、MLE 标记"等不适合放进用户配置的信息。依据:repos/ClassIsland/ClassIsland.Core/Services/GlobalStorageService.cs。 - 设置叠层:自动化/插件临时覆盖设置时必须记录"原值 + 覆盖者 Guid",移除覆盖者后按栈顶恢复;不得直接改配置后忘记恢复。依据:
repos/ClassIsland/ClassIsland/Services/SettingsService.cs:191-259。 - 信任档案列表:
trustedProfileIds默认[];首次加载未信任档案时必须提示"该档案来自外部,是否信任";不允许自动化在未信任档案下运行(对齐AutomationService对IsCurrentProfileTrusted的检查)。依据:ProfileService.cs:166-176,398-422。 - 缓存目录可安全清空:
cache/下任何内容删除后不得影响功能(下次自动重建);设置页"清理缓存"不得删除data/、backups/、config.json。
13.10 备份、恢复与迁移(必须实现)
加载时备份:
- 每次读取 JSON 成功 → 复制为
.bak;失败 → 用.bak恢复并记入错误列表。依据:ConfigureFileHelper.cs:63-96。 .bak只保留一份(覆盖式),不做多版本滚动;多版本由backups/承担。
整包备份:
- 备份内容固定为:
config.json、Settings.json(若存在)、data/全目录、data/themes/EnabledThemes.json;不得包含cache/、logs/、updates/、插件包缓存。依据:repos/ClassIsland/ClassIsland/Services/FileFolderService.cs:115-168(备份Config/+Profiles/+Settings.json,排除缓存/日志)。 - 备份文件名:自动备份
Auto_Backup_yy-MMM-dd_HH-mm-ss.zip,手动备份Backup_yy-MMM-dd_HH-mm-ss.zip。依据:FileFolderService.cs:128-132。 - 自动备份策略:
isAutoBackupEnabled=true时,距上次备份 ≥autoBackupIntervalDays(默认 7 天)才执行;只保留最近autoBackupLimit(默认 16)份Auto_*,按文件修改时间倒序删除多余。依据:FileFolderService.cs:83-113。 - 备份写入必须"先写
.tmp再 rename";备份失败不得影响启动与当前数据(只记日志并在设置页提示)。 - 恢复流程:选择备份 → 先自动创建一份"恢复前快照" → 校验 zip 结构(必须含
config.json或Settings.json+data/profiles/Default.json)→ 解压到临时目录 → 原子替换 → 提示"重启后生效" → 重启应用。恢复失败必须回滚到快照。 - 备份必须可在资源管理器里直接解压查看(不得加密、不得用私有格式)。
版本迁移:
- 必须在
config.json写入schemaVersion(当前1),并在data/profiles/*.json内写SchemaVersion(当前1,允许缺省视为 1)。这是本实现的工程必需字段(ClassIsland 没有,但迁移流水线需要显式版本号)。【推断】 - 迁移执行顺序:读取文件 → 记录
fromVersion→ 逐级执行migrations[fromVersion → fromVersion+1]→ 写回 → 备份迁移前文件到backups/migrate-<from>-to-<to>-<时间>.zip。任一步失败必须中止并保留原文件,提示"迁移失败,已保留原配置"。 - 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)。
- 旧版本
- 从 ClassIsland 导入必须支持两种入口:
--importV1 <dir>(1.x)、--importV2 <dir>(2.x);导入前必须自动备份当前数据;导入后必须展示"导入结果清单"(成功项/跳过项/失败项及原因),不得静默覆盖。依据:repos/ClassIsland/ClassIsland/Program.cs:60-65。 - 组件迁移:
ComponentRegistryService.MigrationPairs必须内置一份"旧 GUID → 新 GUID"映射;导入旧组件布局时按映射改写并保存。依据:ComponentsService.cs:140-161。 - 空 GUID 兼容:所有
Guid字段读到""必须解析为Guid.Empty;写出时Guid.Empty写成""(对齐GuidEmptyFallbackConverter的双向规则)。依据:repos/ClassIsland/ClassIsland.Shared/JsonConverters/GuidEmptyFallbackConverter.cs。 - 颜色兼容:读入支持
#RRGGBB、#RRGGBBAA、#AARRGGBB三种写法(ClassIsland 写出为#RRGGBBAA,Alpha 在尾部);写出统一#RRGGBBAA大写。依据:repos/ClassIsland/ClassIsland.Core/Converters/ColorHexJsonConverter.cs(AlphaComponentPosition.Trailing)。 - 时间兼容:
DateTime写出为 ISOyyyy-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格式。 - 枚举兼容:所有枚举按数字落盘(如
TimeType、UpdateStatus、RulesetLogicalMode),不得写出名字;读到未知数字必须夹紧到合法范围并记 warn。依据:ClassIsland 使用默认System.Text.Json序列化(ConfigureFileHelper.SerializerOptions = new(),ConfigureFileHelper.cs:30),枚举默认输出数字。 - 迁移必须幂等:同一版本重复执行不得产生二次修改(用
schemaVersion与字段存在性双重判断)。
Excel 导入导出(P1 必须实现,xlsx 重建):
- 必须用
xlsx(SheetJS)在主进程/utilityProcess 中重建 ClassIsland 的 Excel 导入导出能力;ClassIsland 的unvell.ReoGrid+ WPF 窗口实现(repos/ClassIsland/ClassIsland/Views/ExcelImportWindow.xaml.cs:29-44、ExcelExportWindow.xaml.cs:23-43)只作字段与流程参考,不得照搬 UI。 - 导入最低能力:读取
.xlsx→ 选择工作表 → 识别"行 = 节次 / 列 = 星期"的网格 → 生成ClassPlan+ 对应TimeLayout;必须支持合并单元格、空节次、科目名去重、教师名写入Subject.TeacherName;解析失败必须给出行列级错误提示,且不得覆盖现有档案(先写入临时档案,用户确认后替换)。 - 导出最低能力:把当前档案/课表写成
.xlsx(一个工作表一张课表,首行星期、首列节次与起止时间),首行冻结;导出文件默认落下载/ClassIsland-课表-<日期>.xlsx,由用户选择路径。 - 导入导出必须遵守 P1 数据兼容:导出的 xlsx 再导入必须得到等价档案(科目/时间表/课表/周次规则不丢);单文件上限 8MB(对齐 ClassHelper 服务端表格上限,见
repos/ClassHelper-master/AGENTS.md§2);导入前自动备份当前档案。
13.11 读写一致性与错误处理
- 所有加载函数必须返回"数据 + 警告列表",不得只返回数据;设置页与诊断页必须能展示"哪些文件回退了备份、哪些字段被重置"。
- 阻止"写入正在读取的文件":每个文件维护内存版本号,写入前比较版本;发现外部修改(用户在资源管理器里改了 JSON)必须提示"配置已被外部修改,是否重新加载",禁止静默覆盖用户改动。【推断】
- 任何写入失败必须:记 error(含路径、错误码、可用磁盘空间)→ 提示用户 → 保留旧文件不变(不得留下半截文件)。
- 磁盘空间不足(< 100MB)时禁止自动备份并提示"清理磁盘";备份与导出前必须检查可用空间。
- 读取时必须限制单文件大小(配置文件 ≤ 8MB、档案 ≤ 32MB、备份 zip ≤ 512MB),超限拒绝并提示,防止恶意文件把内存打爆。【推断】
13.12 天气数据 schema(与 07 天气服务对齐)
- 缓存位置与字段:最后一份天气必须持久化在
config.json的lastWeatherInfo(与 ClassIsland 把Settings.LastWeatherInfo当缓存一致,见repos/ClassIsland/ClassIsland/Services/WeatherService.cs:48-51,387),并附加lastWeatherInfoUpdatedAt(ISO 本地时间)与lastWeatherInfoStale(boolean)。缓存不得因查询失败被清空:失败时保留旧值并标记stale,由 UI 展示"天气数据可能已过期"。 - 刷新节奏:天气服务必须每 5 分钟检查一次(
WeatherService.cs:41-44)、启动后立即查询一次(:71);查询失败按 30s/60s/120s 退避重试 3 次后停到下个周期。请求默认使用https://;仅当用户显式开启noTLSWeatherRequests时才允许http://(对齐WeatherService.cs:34)。 - 来源抽象:不得把第三方接口写死在渲染层。必须定义
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必须可配置、不得出现在渲染进程产物中;默认天气提示"需联网"。 - 预警清洗:查询结果必须按
excludedWeatherAlerts(默认[])过滤——Title或Detail包含排除词即剔除(WeatherService.cs:363-374);同一Type只保留PubTime最新一条(:376-385);alerts按pubTime倒序展示。 - 图标与码表:
weatherIconId默认classisland.weatherIcons.lucide(Settings.cs:1849);天气码到文案的映射必须来自内置资源Assets/XiaomiWeather/xiaomi_weather_status.json(WeatherService.cs:228-233),并提供getWeatherTextByCode(code)等价函数。 - 数据结构(字段名必须与 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 |
- 派生字段(不得落盘,读取时计算):
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)。 - 与 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 部分)
- 首次启动能在
<userData>/下生成完整的目录结构与默认config.json、Default.json档案、Default.json组件布局、默认科目表;目录清单与 13.2 完全一致。 - 删除
config.json后应用能正常启动并重建默认配置;删除data/后能重建默认档案与组件布局,不白屏、不崩溃。 - 故意把
config.json改成非法 JSON 后启动:应用回退到.bak;若.bak也非法则使用默认值并进入"配置错误"提示,不得启动失败。 Settings.json/档案的字段名、类型、默认值与 13.3/13.4 表一致;用 ClassIsland 的真实档案 JSON 导入后,时间表、课表、科目、周次规则全部正确解析(含TimeType=3与IsHideDefault、AttachedObjects)。并且:运行期只写config.json,Settings.json只读导入/单独导出、不双向双写;IsIslandSeperated原拼写能被导入与导出映射。- 写入过程中断电(模拟
rename前杀进程)不会损坏原文件;.tmp残留有清理机制。 - 自动备份:把
lastAutoBackupTime改为 8 天前,启动后生成Auto_Backup_*.zip;备份超过 16 份时最旧的Auto_*被删除,手动Backup_*不受影响。 - 备份 zip 内必须含
config.json、data/profiles/Default.json、data/component-layouts/Default.json、data/automations/Default.json,且不含cache/、logs/、updates/。 - 恢复备份:选择备份 → 自动生成恢复前快照 → 恢复 → 重启后数据与备份一致;中途失败能回滚。
- 迁移流水线:把
schemaVersion改为 0 的旧结构文件,启动后能迁移到 1 并保留原字段;迁移失败时原文件不被破坏。 - 组件布局导入旧 GUID 后按
MigrationPairs改写,且迁移后立即保存、不重复迁移。 - 空 GUID、
#RRGGBB、带时区 ISO、数字枚举四类兼容用例全部通过(读写往返不丢信息)。 - 档案切换/删除后
config.selectedProfile指向有效文件;指向不存在文件时回落到Default.json并提示。 - 自动化配置按
Workflow[]落盘;触发器/行动 ID 与 13.6 表一致;规则集Mode/IsReversed/IsEnabled往返不丢。 - 通知历史落盘、默认上限 500(可配 100–5000)、超限滚动删除、写入节流(≥5 条或 ≥5s)、清空需二次确认;历史文件损坏时回退为空列表并记警告,不影响提醒播放。
- 所有写入路径都能在世界可读的 JSON 中直接看到内容(无私有封装、无加密),且 UTF-8 无 BOM、LF 换行。
- 附加设置:Profile/ClassPlan/ClassInfo/TimeLayout/TimeLayoutItem/Subject 都能读写
attachedObjects;4 个已知 Guid 的设置对象默认值正确;导入AttachedObjects(PascalCase)后可按目标生效;未知 Guid 不丢失;导出写回AttachedObjects。 - 天气:
lastWeatherInfo缓存可持久化、失败时保留旧值并标stale;WeatherInfo/WeatherAlert/ForecastDaily/ForecastHourly/Minutely/AqiInfo/WeatherQueryResult字段与 13.12 完全一致;预警按排除词过滤并按 Type 保留最新;5 个天气规则 ID 可被 07 使用。 - Excel:能从
.xlsx导入生成课表(先临时档案、确认后替换、导入前备份),能导出.xlsx再导入得到等价档案;8MB 上限;所有解析在 utilityProcess 或 worker 中执行,不阻塞主进程。 - 坐标:
config.json中所有几何字段(windowDockingOffsetX/Y、scale、radiusX/Y、字号、间距)均为 DIP 数值;把scale由 1 改为 1.5 后窗口与岛按 CSS px 等比放大,不出现 devicePixelRatio 二次换算。 - 主题市场默认离线:无内置在线官方索引;手动添加 URL 需二次确认;断网时本机已安装主题与
EnabledThemes.json正常工作。