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

317 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 12. 插件系统、跨进程通信、单实例与自动更新
### 12.1 目标与边界
1. 你必须实现一套**进程隔离、权限受控、可市场分发**的插件系统,能力范围对齐 ClassIsland 的插件体系(组件、提醒提供者、自动化触发器/行动、设置页、主题、信息提供者),但实现形态必须是 Electron 下的「npm 风格动态模块 + utilityProcess 宿主」,不得沿用 .NET 程序集加载。
2. 你必须实现完整的跨进程通信层:主进程 ↔ 应用窗口渲染进程 ↔ 岛渲染进程 ↔ 插件宿主进程 ↔ 更新助手。**任何进程间只传纯数据**,不得传函数、类实例、Proxy。
3. 你必须实现单实例、URI 协议唤起、自动更新、崩溃自愈与看门狗。这些能力的失败模式直接决定"教室里能不能无人值守跑一学期"。
4. 你**不得**把插件能力做成"直接在主进程 eval 用户代码"。插件崩溃/死循环不得拖垮主进程,这是本部分的第一验收项。
### 12.2 ClassIsland 插件体系事实(必须对齐的语义)
**包格式与目录**:
- 插件包扩展名 `.cipx`,本质是 zip。依据:`repos/ClassIsland/ClassIsland.Core/Abstractions/Services/IPluginService.cs:17`、`repos/ClassIsland/ClassIsland/Services/PluginService.cs:79-107`。
- 包内必须含 `manifest.yml`;解压后安装到 `Plugins/<插件Id>/`;包缓存目录为 `Cache/PluginPackages/`;插件自己的设置目录为 `Config/Plugins/<插件Id>/`。依据:`PluginService.cs:35-44`。
- 安装流程:扫描包缓存里的 `*.cipx` → 读取 `manifest.yml`(`IgnoreUnmatchedProperties` + **CamelCase 命名约定**)→ 删除同名旧目录(保留 `.disabled` 状态)→ 解压 → 删除包文件。依据:`PluginService.cs:57-107`。
- 启用/禁用状态用插件目录下的标记文件表达:`.disabled` 存在即禁用;`.uninstall` 存在表示"下次启动时删除该插件目录"。依据:`repos/ClassIsland/ClassIsland.Core/Models/Plugin/PluginInfo.cs:47-100`。
**清单字段(`manifest.yml`,YAML 键为 camelCase)**:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `entranceAssembly` | string | `""` | 入口程序集(本实现改为 `main.mjs` 入口文件) |
| `name` | string | `""` | 显示名称 |
| `id` | string | `""` | 插件唯一 ID(本实现要求反向域名式,如 `com.example.weather`) |
| `description` | string | `""` | 描述 |
| `icon` | string | `icon.png` | 图标相对路径 |
| `readme` | string | `README.md` | 自述文件相对路径 |
| `url` | string? | `null` | 项目地址 |
| `version` | string | `""` | 插件版本(semver) |
| `apiVersion` | string | `""` | 目标宿主 API 版本;本实现要求 `>=2.0.0` |
| `author` | string | `""` | 作者 |
| `dependencies` | `{id, isRequired}[]` | `[]` | 依赖;`isRequired` 默认 `true` |
| `supportedOSPlatforms` | string[] | 全平台 | 有效值 `windows`/`linux`/`osx` |
依据:`repos/ClassIsland/ClassIsland.Core/Models/Plugin/PluginManifest.cs:12-75`、`repos/ClassIsland/ClassIsland.Core/Models/Plugin/PluginDependency.cs:6-17`。
**清单命名兼容(强制)**:本实现统一写 **camelCase**(与 ClassIsland `manifest.yml` 的 CamelCase 约定一致);同时必须**兼容读取** PascalCase 旧清单(`Id`/`Name`/`EntranceAssembly`/`ApiVersion`/`SupportedOSPlatforms` 等)。加载时必须先做 `camelCase ↔ PascalCase` 归一化再校验;同名键以 camelCase 为准(不得让旧拼写覆盖新值);无法识别的键忽略并记 debug 日志。依据:`PluginService.cs:68-72`(`IgnoreUnmatchedProperties` + `CamelCaseNamingConvention`)。
**加载规则**:
- 按依赖拓扑排序加载;必选依赖缺失或不可加载 → 该插件置为 `Error`;检测到循环依赖必须抛错并拒绝整组。依据:`PluginService.cs:288-339`。
- `apiVersion < 2.0.0.0` → 拒绝加载并记录 `Error` 与异常说明(对齐 `PluginService.cs:168-173`)。
- 加载状态枚举固定四种:`NotLoaded`/`Loaded`/`Disabled`/`Error`,并带 `Exception` 供设置页展示。依据:`repos/ClassIsland/ClassIsland.Core/Enums/PluginLoadStatus.cs`。
- 每个插件有独立的"注册上下文":插件在 `Initialize` 期间注册的组件会自动归属该插件。依据:`PluginService.cs:211-221`(`ComponentRegistryService.CurrentRegisteringPlugin`)。
- 加载失败/损坏的插件必须在下次启动自动禁用(`Settings.AutoDisableCorruptPlugins` 默认 `true`,`Settings.CorruptPluginsDisabledLastSession` 记录上次会话是否发生过自动禁用)。依据:`repos/ClassIsland/ClassIsland/Models/Settings.cs:1844-1845`。
**插件市场**:
- 索引结构 `PluginIndex { plugins[], downloadMirrors{ name: root } }`;条目在 `PluginInfo` 之上增加 `downloadUrl` 与 `downloadMd5`。依据:`repos/ClassIsland/ClassIsland.Core/Models/Plugin/PluginIndex.cs:12-21`、`PluginIndexItem.cs:8-19`。
- 下载镜像:镜像根替换下载链接里的模板;内置回退镜像 `github`、`ghproxy`、`moeyy`;官方索引地址带时间戳参数 `https://get.classisland.tech/d/ClassIsland-Ningbo-S3/classisland/plugin/index.zip?time={time}`;索引默认 7 天刷新一次。依据:`repos/ClassIsland/ClassIsland/Services/PluginMarketService.cs:43-48,66-69,212`。
- 设置项:`userPluginIndexes`、`additionalPluginIndexes`、`pluginIndexSelectedMirrors`、`officialSelectedMirror`(默认 `github`)、`ignoreSslForPluginMirrors`(默认 `false`)、`isPluginsAutoUpdateEnabled`(默认 `true`)、`isPluginsUpdateNotificationEnabled`(默认 `true`)、`isPluginMarketWarningVisible`。依据:`Settings.cs:166-178,1858-1859,204`。
- SDK 打包:插件项目 `CreateCipx=true` 时,构建后自动把输出目录压成 `<项目名>.cipx`,并在 `checksums.md` 里写一行 HTML 注释形式的 MD5 JSON(`<!-- CLASSISLAND_PKG_MD5 {...} -->`)。依据:`repos/ClassIsland/ClassIsland.PluginSdk/ClassIsland.PluginSdk.targets`(`CreateCipx` Target)与 `generate-md5.ps1`。
### 12.3 本实现的插件规范(必须逐项落地)
**包与目录**:
1. 插件包扩展名沿用 `.cipx`(zip),内部结构固定:
```text
my-plugin.cipx
├─ manifest.yml # 必须存在;字段见下表
├─ main.mjs # 必须存在;ESM 入口,默认导出 activate/deactivate
├─ icon.png # 可选,默认 icon.png
├─ README.md # 可选
└─ assets/ # 可选静态资源(只允许相对路径引用)
```
2. 运行期目录:`<userData>/plugins/<id>/`(解压后)、`<userData>/plugin-cache/packages/*.cipx`(下载缓存)、`<userData>/plugin-data/<id>/`(插件配置与缓存)、`<userData>/plugin-cache/index.json`(市场索引缓存)。必须与 13 部分的目录规划一致。
3. 启用状态用 `<pluginDir>/.disabled` 标记,卸载用 `<pluginDir>/.uninstall` 标记;**不得**用配置里的布尔值表达启用状态(否则用户手工操作目录后状态会不一致)。依据:`PluginInfo.cs:47-100`。
4. 插件数据写入必须由宿主代理:插件调用 `ctx.storage.set(key, value)`,宿主写到 `<userData>/plugin-data/<id>/storage.json`,**不得**给插件直接 `fs` 访问。
**清单(`manifest.yml`,camelCase,字段与默认值必须与 12.2 表一致,另加以下扩展)**:
```yaml
id: com.example.weather-plus
name: 天气增强
description: 提供逐小时降水提醒
author: Example
version: 1.2.0
apiVersion: 2.0.0
icon: icon.png
readme: README.md
entry: main.mjs
url: https://example.com/weather-plus
supportedOSPlatforms: [windows, linux, osx]
dependencies:
- id: com.classisland.core-miniinfo
isRequired: true
permissions:
- notifications:push # 允许发提醒
- automation:register # 允许注册触发器/行动
- components:register # 允许注册岛组件
- settings:page # 允许注册设置页
- storage:read
- storage:write
- network:api.example.com # 仅允许访问列出的域名(精确匹配,禁止通配)
```
5. `permissions` 是**本实现在 ClassIsland 基础上的强制安全增强**:ClassIsland 的插件是 DLL 全信任,本实现必须默认拒绝、按声明放行。任何未声明的能力调用都必须抛 `PluginPermissionError` 并写 warn 日志;设置页必须在启用前展示权限清单并二次确认。**【推断】**
6. `apiVersion` 必须是 semver;宿主 API 当前版本固定为 `2.0.0`,`apiVersion` 主版本不同即拒绝加载;次版本号高于宿主时给出"可能不兼容"警告但仍允许(与 ClassIsland 的"最高版本门槛"语义相反,此处以兼容性优先【推断】)。
7. 依赖解析必须:先校验所有 `isRequired` 依赖存在且 `Loaded`;再做拓扑排序;循环依赖必须报错并列出环上的 ID;可选依赖缺失只记 info。依据:`PluginService.cs:306-339`。
**生命周期与宿主**:
1. 插件宿主必须是 `utilityProcess.fork(path.join(__dirname,'plugin-host.js'))` 启动的独立进程;插件代码在该进程内用 `await import(pathToFileURL(entry))` 加载。**不得**在主进程 `import()` 插件代码。
2. 宿主暴露给插件的 API 对象(`ctx`)只允许包含:`ctx.manifest`、`ctx.apiVersion`、`ctx.log`、`ctx.events`、`ctx.storage`、`ctx.notifications`、`ctx.components`、`ctx.automation`、`ctx.settings`、`ctx.themes`、`ctx.commands`、`ctx.timers`。禁止暴露 `process`、`require`、`fs`、`child_process`、`net`。
3. 插件导出的入口签名固定:
```js
export async function activate(ctx) { /* 注册能力;不要在这里做耗时 I/O */ }
export async function deactivate(ctx) { /* 清理定时器与监听器 */ }
export const manifest = {/* 可选:与 manifest.yml 一致 */};
```
4. 宿主必须为每个插件维护独立状态机:`discovered → validating → disabled | loading → loaded | error → restart-required`。状态与错误信息通过 IPC `classisland:plugins:list` 交给设置页渲染。
5. 插件加载超时必须为 5 秒;单个插件崩溃后宿主必须重启该插件(最多 2 次),仍失败则置 `error` 并在**下一次启动**写入 `.disabled`(对齐 `AutoDisableCorruptPlugins` 语义)。同一次运行内不得无限重启。
6. 启用/禁用/卸载必须标记"需要重启",并在 UI 上明确显示「重启 ClassIsland 后生效」;不得热替换已加载插件(热替换会留下旧的全局监听器)。依据:`PluginInfo.RestartRequired`(`PluginInfo.cs:159-172`)。
7. 插件注册的扩展点必须进入统一注册表,重复 ID 时后注册者失败并记录错误(不得静默覆盖)。依据:ClassIsland 的 `ComponentRegistryService`/`RulesetRegistryExtensions` 单注册表模式。
**扩展点(必须全部实现并映射)**:
| ClassIsland 扩展点 | 插件 API | 说明 |
|---|---|---|
| 组件(`ComponentBase` 派生 + 注册表) | `ctx.components.register({id, name, group, settingsSchema, render})` | 组件设置必须用声明式 schema;`render` 是可序列化描述或受控的 Vue 组件名,不得注入任意 DOM |
| 提醒提供者(`NotificationProvider`) | `ctx.notifications.registerProvider({id, name, request})` | 提供者只能"请求提醒",是否播放由宿主队列决定 |
| 自动化触发器 | `ctx.automation.registerTrigger({id, name, schema, setup})` | 触发回调可发信号给宿主 |
| 自动化行动 | `ctx.automation.registerAction({id, name, schema, run})` | 行动执行受权限约束(执行外部程序需 `exec` 权限) |
| 设置页 | `ctx.settings.registerPage({id, title, schema, section})` | 只能是声明式 schema,由宿主统一表单渲染;**不得**提供任意 HTML/iframe(见下方安全红线) |
| 主题 | `ctx.themes.registerManifest(themeManifest)` | 主题包格式见 9 部分 |
| 迷你信息提供者 | `ctx.miniInfo.registerProvider({id, name, fetch})` | 天气/温度等文本信息,带超时与缓存 |
| 设置叠层 | `ctx.settings.overlay({key, value})` | 对齐 ClassIsland `AddSettingsOverlay` 的"临时覆盖并自动还原"语义(`repos/ClassIsland/ClassIsland/Services/SettingsService.cs:191-259`) |
**设置页安全红线(强制)**:插件设置页**只能**是声明式 schema(字段名、类型、默认值、校验规则、布局分组、可见性条件),由宿主用统一表单渲染;**禁止**插件注入任意 HTML/CSS/脚本,**禁止**用 iframe/webview 承载插件设置页,**禁止**插件直接访问 DOM 或 `window`。schema 允许的控件仅限:文本、多行文本、数字、开关、下拉、单选、多选、颜色、时间、文件路径(由宿主弹选择器)、按钮(只能触发宿主声明允许的行动)。任何试图绕过 schema 的插件必须加载失败并记 error。
**插件市场(本实现)**:
1. 索引 JSON schema 必须为:
```json
{
"plugins": [{
"id": "com.example.weather-plus",
"name": "天气增强",
"description": "……",
"author": "Example",
"version": "1.2.0",
"apiVersion": "2.0.0",
"supportedOSPlatforms": ["windows"],
"downloadUrl": "{root}/example/weather-plus/releases/download/v1.2.0/weather-plus.cipx",
"downloadMd5": "…32位十六进制…",
"downloadSha256": "…64位十六进制…",
"downloadSize": 1048576,
"repoOwner": "example",
"repoName": "weather-plus",
"assetsRoot": "master",
"artifactName": "weather-plus.cipx"
}],
"downloadMirrors": { "github": "https://github.com", "ghproxy": "https://mirror.ghproxy.com/https://github.com" }
}
```
2. `downloadUrl` 必须支持 `{root}` 模板,按当前选中镜像替换;镜像失败必须按列表回退,不得只试一个。
3. 索引必须支持"多源":**默认不内置在线官方索引**,只有用户源(`userPluginIndexes`);内置源只允许是随安装包分发的离线示例索引。每源独立记录"最后刷新时间/错误/是否过期";7 天未刷新时后台刷新,失败保留旧索引并标 `stale`。依据:`PluginMarketService.cs:43-69,107-120`(ClassIsland 的刷新语义)+ 本应用默认离线策略【推断】。
4. 下载必须**同时校验 MD5 与 SHA-256**:MD5 用于兼容 ClassIsland 生态,SHA-256 用于本实现的安全底线;任一不符即删除文件并报错。不得"只校验 MD5"。
5. 下载完成后必须走与安装包相同的原子流程(`.part` → 校验 → rename),不得把半截 zip 交给解压器。
6. 插件更新必须遵守「上课不打断」:`IsPluginsAutoUpdateEnabled=true` 时,只在**非上课时段**且设置窗口不可见时才后台下载;安装必须等用户确认或下次启动。依据:ClassIsland 默认值(`Settings.cs:1858-1859`)+ 本应用"零打扰"原则。
7. **默认离线(强制)**:安装包只能内置一份离线示例索引(`resources/plugin-index.example.json`,0–2 条示例,默认不可安装或只指向本地文件)。用户必须**手动添加索引 URL 并二次确认**后才允许联网;确认弹窗必须写明"将从该地址下载并执行第三方代码,请确认来源可信"。网络失败、索引 404、源不可达均不得影响本机已安装插件与本地索引。主题市场遵循同一默认策略(详见 9 部分)。
### 12.4 跨进程通信(IPC)设计(必须完整实现)
**通道命名与组织**:
1. 通道名必须为 `classisland:<domain>:<action>` 的 kebab 形式,全部集中在 `src/main/channels.ts`(**零 import 叶子模块**)。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/channels.ts:16-28`。
2. 通道分三类,不得混用:
- `invoke/handle`:请求-响应(读写配置、查询状态、执行命令);
- `send/on`:单向事件(岛事件、心跳、交互上报);
- `webContents.send`:主进程→渲染进程推送(状态、外观、进度)。
3. 必须实现的通道清单(最小集,新增通道必须同表登记):
| 通道 | 方向 | 用途 |
|---|---|---|
| `classisland:config:get` / `:save` / `:reset` | 双向 | 读/局部写/重置配置;`:save` 必须剔除更新安装字段 |
| `classisland:app:info` | 双向 | 版本、Electron/Chrome/Node 版本、平台、userData、配置路径 |
| `classisland:app:get-autolaunch` / `:set-autolaunch` | 双向 | 开机自启(以系统登录项为准) |
| `classisland:app:open-external` / `:open-log-dir` / `:open-devtools` | 双向 | 受控系统能力 |
| `classisland:app:window` | 双向 | 显示/隐藏/最小化/聚焦设置窗口 |
| `classisland:profile:list` / `:load` / `:save` / `:create` / `:delete` / `:import` / `:export` | 双向 | 档案增删改查与导入导出 |
| `classisland:schedule:get` / `:save` | 双向 | 课表/时间表读写 |
| `classisland:notification:list` / `:clear` / `:test` | 双向 | 提醒列表与测试提醒 |
| `classisland:automation:list` / `:save` / `:run` | 双向 | 自动化配置与手动触发 |
| `classisland:plugins:list` / `:enable` / `:disable` / `:uninstall` / `:install-package` / `:market-refresh` | 双向 | 插件管理 |
| `classisland:update:check` / `:download` / `:install` / `:progress` | 双向 | 更新 |
| `classisland:backup:list` / `:create` / `:restore` | 双向 | 备份 |
| `island:push` / `island:remove-notification` / `island:class-state` / `island:get-state` | 双向 | 主窗口→岛 |
| `island:set-appearance` / `island:get-appearance` | 双向 | 外观热更新 |
| `island:action` / `island:set-interactive` / `island:set-hit-rect` / `island:set-touch-mode` / `island:alive` / `island:report-size` / `island:viewport` | 岛→主进程 | 用户操作、命中、触摸、心跳、尺寸上报 |
| `island:state` / `island:appearance` / `island:visibility` / `island:schedules` / `island:time-points` / `island:layout` / `island:editing` | 主进程→岛 | 状态与数据推送 |
| `island:mark-read` / `island:mark-all-read` / `island:open-editor` | 主进程→应用窗口 | 岛侧操作回流到应用窗口/编辑器 |
依据:`repos/ClassHelper-master/packages/desktop-client/src/main/channels.ts:16-28`、`src/preload/island.ts:64-121`、`src/preload/index.ts:25-99`(通道命名与成对关系)。
**Payload 与错误约定**:
4. 所有跨桥接数据必须是结构化克隆可序列化值:无函数、无 Symbol、无 Proxy、无 `Date` 之外的自定义类;`Error` 必须转成 `{code, message, detail?}` 普通对象。依据:`src/preload/index.ts:17-24` 的注释。
5. 单个 IPC 消息必须 ≤ 8MB;大文件(备份、导出档案、主题包)一律"传路径不传内容",由主进程读文件。
6. 所有 `invoke` 处理必须:校验 sender 必须是本应用的窗口(比对 `event.senderFrame.url` 白名单)→ 校验参数(类型、范围、长度)→ 执行业务 → 返回 `{ok:true, data}` 或 `{ok:false, error}`。**不得让异常穿透 IPC 抛给渲染层**(生产构建的堆栈会泄漏内部路径)。
7. 每次 `invoke` 在渲染层必须包一层 10 秒超时;超时必须提示"操作超时,请重试",不得让按钮永久转圈。
8. 高频事件必须节流:外观滑块 ≤ 20 次/秒(50ms)、岛状态推送 100ms 合并(尾部保留最后一次)、心跳 1 次/秒。依据:`island.ts:216-224`、`auto-update.ts:216-219` 的节流写法。
9. 事件订阅必须可取消:preload 上的 `onXxx(handler)` 必须返回 `() => void` 移除监听;组件卸载时调用。依据:`src/preload/index.ts:32-36,56-60`。
**岛窗口专用通信纪律**:
10. 岛渲染进程只允许两类操作:订阅主进程推来的状态/外观/课表,以及上报用户操作与命中信息。不得让岛直接读写配置。依据:`src/preload/island.ts:10-13` 的注释。
11. 命中控制必须"只上报 true,关闭由主进程按真实光标位置决定":双向下发会让窗口在"接收/穿透"之间抖动,表现为"点了没反应"。依据:`src/types/desktop.d.ts:221-228`。
12. 岛必须每秒发一次心跳(`alive()`);主进程超过 3 秒未收到心跳即判定"幽灵窗口",隐藏并重建。依据:`src/main/island.ts:425-430,541-599`(`lastAliveAt`、`checkRendererAlive` 思路)。
13. 触摸屏必须上报 `setTouchMode(navigator.maxTouchPoints > 0)`;触摸模式下窗口必须贴合岛体(岛体+10px 阴影留白)并始终接收输入。依据:`src/main/island.ts:245-316`。
14. 主进程必须对岛窗口做导航白名单与 `window-open(e) = deny`;岛是置顶窗口,被导航走等于把 `window.island` 桥交给陌生页面。依据:`src/main/island.ts:32-50,516-522`。
**可选本地只读 HTTP API(外部程序集成,默认关闭)**:
15. 本实现**不恢复** ClassIsland 的命名管道 IPC;如需让外部程序(脚本、班级大屏看板、第三方展示页)读取当前课程,只能启用"本地只读 HTTP API",且必须满足以下全部约束:
- 只监听 `127.0.0.1`(不得监听 `0.0.0.0`、不得绑定局域网地址);
- 端口**随机分配**(`listen(0)` 后读取实际端口),端口与开关写入 `config.localApi`,默认 `enabled=false`;
- 开启时必须生成**一次性 token**(随机 32 字节,base64url),请求必须带 `Authorization: Bearer <token>`;token 在设置页可见/可重置,关闭即失效;
- **只提供读接口**:`GET /v1/status`(当前时间点/上下课状态/剩余时间)、`GET /v1/schedule/today`(当天课表)、`GET /v1/profile/summary`(档案名/科目表摘要);**不得提供任何写操作**(不得改课表、不得发提醒、不得触发自动化、不得改配置);
- 响应必须含 CORS 白名单(默认不放开跨域;放开需用户在设置页显式勾选并提示风险);
- 每次启用/停用必须写审计日志;非法 token 返回 401 且限流(≥10 次/分钟即临时封禁)。
依据:ClassIsland 的 `IPublicLessonsService`/`IPublicProfileService` 是"读服务为主"的公开契约(`repos/ClassIsland/ClassIsland.Shared.IPC/Abstractions/Services/IPublic*.cs`),本实现以只读 HTTP 等价替代,不引入写能力。**【推断】**
### 12.5 单实例与 URI 协议(必须实现)
1. 单实例:`app.requestSingleInstanceLock()`;未拿到锁必须 `app.quit()` 并立即返回;拿到锁必须监听 `second-instance` → 显示/聚焦主窗口(若被最小化先 `restore`)。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/index.ts:176-182,279-285`。
2. `second-instance` 必须解析新实例的 `argv`:若带 `classisland://` URI,转交现有实例导航;若带 `--hidden` 只做唤醒不抢焦点。参考 `repos/ClassIsland/ClassIsland/Program.cs:88-110,141-159` 的"第二次启动把 URI 通过 IPC 发给已运行实例后退出"语义。
3. 必须注册 `classisland://` 协议:Windows 用 `app.setAsDefaultProtocolClient('classisland')` 并在 NSIS 安装器中登记;macOS 监听 `open-url`;Linux 通过 `.desktop` 的 `MimeType=x-scheme-handler/classisland`。
4. URI 语法固定:`classisland://<host>/<path>?<query>`;允许的 host 与行为:
- `app/settings/<page>` → 打开设置页(例:`classisland://app/settings/update` 对齐 ClassIsland `UpdateService.cs:326`);
- `app/notification` → 打开提醒历史;
- `lesson/current` → 显示当前课程详情;
- `profile/import?path=<urlencoded>` → 导入档案(必须二次确认);
- `action/<id>` → 触发自动化行动(仅当该行动声明 `uriTrigger=true` 且用户在设置中允许)。
未知 host/path 必须忽略并提示「无法识别的链接」,不得静默执行。URI 长度上限 2048,必须 URL 解码后再解析,禁止 `..` 路径穿越。
5. 单实例锁必须与"冒烟/隔离配置"兼容:验收时有独立的 `userData`,否则会被真实实例的锁挡住而"静默退出(退出码 0、无输出)"。依据:`src/main/index.ts:29-32` 注释。
### 12.6 自动更新(必须按此流程实现)
**来源与通道**:
1. 更新通道至少三档:`stable`(默认)、`beta`、`dev`;通道描述含 `id`、`name`、`description`、`rootUrl`。**默认只接受 `https://`**;镜像/自定义源必须由用户显式开启并二次确认"非官方来源风险",默认不启用。清单必须校验 SHA-256;清单签名可选,一旦提供则必须是 Ed25519(带 `keyId`),客户端内置公钥表(支持多 keyId,便于轮换),验签失败必须拒绝整个更新;未签名清单必须在设置页显著标注"未签名发布"并写审计日志。依据:`repos/ClassIsland/ClassIsland/Services/AppUpdating/UpdateService.cs:660-669`(`DetachedSignatureProcessor` 的验签语义)。
2. 检查更新必须支持:启动后延迟 30 秒检查一次、之后每 6 小时一次、设置页手动检查、`classisland://app/settings/update` 深链进入。检查间隔与 `LastCheckUpdateTime` 必须持久化,避免频繁联网。依据:`Settings.cs:75-78`。
3. 更新状态必须走固定枚举:`UpToDate`(已是最新)、`UpdateAvailable`(发现新版)、`UpdateDownloaded`(已下载)、`UpdateDeployed`(已部署待重启);工作状态:`Idle`、`Checking`、`Downloading`、`Extracting`。依据:`repos/ClassIsland/ClassIsland.Shared/Enums/UpdateStatus.cs`、`UpdateWorkingStatus.cs`。
**清单与下载**:
4. 更新清单必须包含:`version`、`releaseNotes`、`pubDate`、`minOsVersion`、`files[{path,size,sha256,url}]`、`installer`(整包)或 `components`(分组件增量);清单可选附带 `signature{alg:"ed25519", keyId, sig}`,只要附带就必须校验通过。**不得**只给一个"最新版本号"就让客户端猜下载地址。
5. 下载必须:先查磁盘空间(≥ 3 倍包大小)→ 写 `.part` → 流式写盘(不得整包读进内存)→ 校验长度 → 校验 SHA-256 → `rename` 为正式文件。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/auto-update.ts:342-394`。
6. 下载必须支持断点续传与超时(单包 10 分钟);网络失败必须按 1/2/4/8 分钟退避重试 3 次,仍失败则置 `failed` 并展示错误码,不得无限重试。依据:`auto-update.ts:124-125,407-414`。
7. 增量更新必须至少支持"按文件 SHA-256 跳过未变化文件"的差分(对齐 `UpdateService.cs:447-485` 的 `AllowDiffUpdate` 语义);差分失败必须自动回退到整包下载,不得让用户停在半更新状态。
**安装时机与安全**:
8. 安装策略三档,必须严格实现:
- `immediate`:下载校验完成后立即安装(仍须满足"不在上课"除非用户显式确认);
- `after-school`:到达 `updateNotBefore`(放学时间)后且不在上课才安装;
- `idle`(默认):不在上课、且主窗口/设置窗口隐藏满 5 分钟(`CLIENT_UPDATE_IDLE_MS`)才安装。依据:`packages/shared/src/constants.ts:677`、`auto-update.ts:474-506`。
9. 关闭自动更新(`autoUpdate=false`)时,**不得**安装任何更新,但仍可展示"有可用更新"的提示;不得静默强装。依据:`auto-update.ts:280-296,458`。
10. 安装必须交给独立助手,并采用**固定安装目录 + 回滚副本**(不做 side-by-side 多版本并存):主进程把安装包路径、SHA-256、版本、回滚目录写入 `update-plan.json` → 生成/校验助手 → 助手就绪回执 → 主进程退出 → 助手等待旧进程结束 → 先把当前安装目录的关键文件(`ClassIsland.exe`、`resources/app.asar` 等)复制到 `updates/rollback/<旧版本>/` → 替换/安装 → 启动新版并检查版本 → 写 `install-result.json`(成功/失败+错误)→ 失败用回滚副本恢复旧版并重新拉起。依据:`auto-update.ts:516-573`(就绪→commit→退出→结果文件)与 `repos/ClassIsland/ClassIsland/Models/AppUpdating/DeploymentLock.cs`(记录已存在文件与组件根目录以便回滚)。
11. 安装过程中必须避免"文件被占用":不得覆盖正在运行的 `exe`/`asar`;助手必须从 `updates/` 目录运行。依据:`auto-update.ts:75-88`(专用目录、只接受目录内 exe)。
12. 安装包路径必须做双重校验:`path.dirname(realpath(file)) === realpath(updatesDir)` 且扩展名 `.exe`;任何不符必须拒绝执行并清空待安装记录。依据:`auto-update.ts:477-490`。
13. 更新失败必须留下可诊断证据:`update.log`(含 HTTP 状态、哈希期望/实际、助手退出码),并在设置页"更新"页展示最近一次失败原因(可复制)。
14. 版本回退必须保留上一版安装包与 `updates/rollback/<旧版本>/` 回滚副本及"上次可用版本"标记;新版启动失败(连续 2 次崩溃)必须提示"回退到上一版本"并给出按钮。**【推断】**
15. 插件更新与主程序更新必须**分开**:插件更新不得要求重启整个应用(除非插件声明 `restartRequired`);主程序更新必须重启。两者进度不得互相覆盖同一个 `policy.json`。
### 12.7 崩溃自愈与看门狗(必须实现)
1. 主进程必须监听以下事件并分别处理(不得合并成一个"崩溃就重启"):
| 事件 | 处理 | 限流 |
|---|---|---|
| 设置窗口 `render-process-gone` | 静默销毁并重建窗口;保留配置与状态 | 10 分钟内最多 3 次,超限只记日志 |
| 设置窗口 `unresponsive` 超过 10 秒 | 重建窗口;期间 `responsive` 恢复则取消 | 同上 |
| 岛 `render-process-gone` | 隐藏死窗口 → 重建岛窗口 → 重推状态 | 同上 |
| 岛心跳超时(>3 秒) | 判定幽灵窗口 → 隐藏并重建 | 同上 |
| `child-process-gone` | 记录 type/reason/exitCode;插件宿主崩溃按插件粒度重启 | — |
| 主进程 `uncaughtException` | 记录完整堆栈;仅当"不在上课且所有窗口不可见"才自动重启 | 10 分钟内最多 3 次 |
| 内存超限 | 1.5GB(`MemoryLimitBytes=1500000000`)时触发一次自重启(带静默参数),并记录 `-psmk` 语义的"上次因 MLE 退出"标记 | 每次启动最多 1 次 |
依据:`repos/ClassHelper-master/packages/desktop-client/src/main/crash-guard.ts:20-25,49-75,103-184`;`repos/ClassIsland/ClassIsland/Services/MemoryWatchDogService.cs:16-70`(60 秒检查一次、1.5GB 阈值、用 `-q -m -psmk` 重启)。
2. 重建窗口前必须 `hide()` 再 `destroy()`,避免留下一帧残影或白屏。依据:`crash-guard.ts:139-145`。
3. 冒烟/自动化验收模式下**绝对禁止** `app.relaunch()`(会把验证父进程搞乱);这条必须写进代码分支并以环境变量控制。依据:`crash-guard.ts:153-159`。
4. 岛窗口自愈必须同时处理"渲染进程没了"和"窗口还在但点不动"两种形态;后者只能靠心跳发现。依据:`island.ts:583-599`。
5. 所有自愈操作必须写日志:`reason`、`exitCode`、本窗口期第几次、是否达到限流上限。日志是教室现场排障的唯一证据。
### 12.8 验收标准(第 12 部分)
1. 插件安装/启用/禁用/卸载全流程可用:安装后重启生效、`.disabled` 标记正确、卸载后目录被删除;同一插件重复安装不丢用户配置。
2. 依赖排序正确:A 依赖 B 时先加载 B;必选依赖缺失时 A 状态为 `Error` 且错误信息可在设置页看到;循环依赖给出明确错误。
3. 插件崩溃不会导致主进程退出:杀死插件宿主进程后,主进程存活、岛继续刷新、设置页显示"插件已停止"。
4. 未声明权限的插件调用受控 API 时必须被拒绝并有日志,且不会抛到渲染进程。
5. 插件市场能读取索引、切换镜像、下载并校验 MD5+SHA-256;校验失败时文件被删除并报错。
6. 通道白名单:渲染进程无法调用未登记通道;`window.desktop` 中不存在 `ipcRenderer`/`fs`/`process`。
7. 任意跨桥接传参若含 Vue 响应式对象,必须抛出可读错误(`An object could not be cloned.`)而不是静默失效;所有调用点已展开为纯数据。
8. 第二次启动应用不产生第二个实例,而是唤起已有窗口;`classisland://app/settings/update` 能在已运行实例中打开更新页。
9. 更新全流程:检查 → 下载 → 校验 → 等待空闲 → 助手安装 → 新版启动写回执;`autoUpdate=false` 时绝不安装;上课时段绝不安装。
10. 更新助手失败时能回滚并写 `install-result.json`,设置页能展示失败原因。
11. 主窗口渲染进程崩溃后 10 分钟内最多重建 3 次,之后停止并可人工介入;日志含原因与次数。
12. 内存看门狗在超过 1.5GB 时自重启,且重启后保留配置、跳过一次引导、记录 MLE 标记。
13. 岛心跳超时能触发重建;重建后状态、课表、外观全部恢复(不出现空白岛)。
14. 所有新增 IPC 通道都登记在 `channels.ts` 与 12.4 表中,且类型定义在 `packages/shared/src/ipc.ts` 中单一来源。
15. 可选本地 HTTP API 默认关闭;开启后只监听 `127.0.0.1`、端口随机、请求必须带一次性 token、只存在 `GET /v1/status`、`GET /v1/schedule/today`、`GET /v1/profile/summary` 三个只读接口,任何写请求必须返回 405;关闭后端口立即不可达。
16. 插件市场默认只有离线示例索引;添加外部索引 URL 必须二次确认后才联网;断网/源不可达时本机插件与本地索引不受影响。
17. 插件设置页只能渲染声明式 schema;插件若尝试注入 HTML/iframe 或直接操作 DOM,必须被拒绝并记 error。
18. 插件清单 camelCase 写入、PascalCase 旧清单可正常加载(同一插件两种清单导入后行为一致)。
19. 更新默认只接受 `https://`;未签名清单在设置页显示"未签名发布"标记;签名清单的 `keyId` 未在公钥表内时拒绝更新。
20. 更新安装使用固定安装目录,替换前生成 `updates/rollback/<旧版本>/` 回滚副本;安装失败能回滚并写 `install-result.json`;磁盘上不出现 side-by-side 并存的多个 `app-<版本>` 目录。