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

412 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 2. 技术栈与工程结构
### 2.1 技术选型总则(必须逐条遵守)
1. 你必须用 **TypeScript + Electron + Vue 3** 实现本应用,工程形态必须是 **pnpm monorepo**,不得做成单包项目、不得用 Webpack、不得用 Electron Forge、不得用 `electron-vite` 之外的脚手架替代本文规定的手写构建管线。
2. 你必须**完整沿用 ClassHelper 桌面客户端的工程约定**:主进程/preload 用 esbuild 打包为 CommonJS,渲染进程用 Vite 多入口打包,打包用 electron-builder;共享类型、常量、纯函数放 `packages/shared`;共享 Vue 控件与样式令牌放 `packages/ui`;Electron 应用全部代码放 `packages/desktop-client`。依据:`repos/ClassHelper-master/pnpm-workspace.yaml:1-2`、`repos/ClassHelper-master/packages/desktop-client/package.json:8-18`。
3. 你**不得**把 ClassHelper 的服务端(Express/Prisma/Socket.IO)引入主流程。ClassIsland 是本地单机应用,一切业务数据必须落在本机;仅当实现「可选管理端联动」时才允许把 `packages/server` 作为可裁剪子包,且主流程不得依赖网络。依据:`repos/ClassHelper-master/packages/server/package.json:19-40` 的依赖清单,以及总纲的 P2 定义。
4. 你必须把「版本」收敛为**单一来源**:根 `package.json` 的 `version` 是唯一手写值,所有子包版本由脚本同步;日常功能提交不得涨版本。依据:`repos/ClassHelper-master/AGENTS.md`「根 package.json 是版本单一来源」与 `repos/ClassHelper-master/docs/dev/release.md:8-10`。
5. 你必须保持「渲染层零 Node 能力」:渲染进程不得 `require('node:fs')`、不得访问 `process` 环境变量之外的 Node API,所有文件、系统、更新、托盘能力一律经主进程 IPC。依据:`repos/ClassHelper-master/AGENTS.md`「preload 仅暴露 contextBridge 白名单,不暴露 ipcRenderer」。
### 2.2 依赖清单(版本必须按此锁定)
以下版本是硬约束,`pnpm-lock.yaml` 必须锁定到这些 semver 区间;不得因为"想用新 API"而升级大版本。
| 用途 | 包名 | 版本 | 证据 |
|---|---|---|---|
| 包管理器 | pnpm | `11.8.0`(`packageManager` 字段) | `repos/ClassHelper-master/package.json:6` |
| Node 运行时 | Node.js | `>=20.19.0` | `repos/ClassHelper-master/package.json:7-9` |
| 桌面壳 | `electron` | `^44.3.0` | `repos/ClassHelper-master/packages/desktop-client/package.json:32` |
| 打包 | `electron-builder` | `^26.15.3` | 同上 `:34` |
| UI 框架 | `vue` | `^3.5.42` | 同上 `:26` |
| 路由 | `vue-router` | `^5.3.1` | 同上 `:27` |
| 状态 | `pinia` | `^4.0.3` | 同上 `:24` |
| 组件库(仅设置/管理界面) | `element-plus` | `^2.14.5` | 同上 `:23` |
| 图标 | `@element-plus/icons-vue` | `^2.3.2` | 同上 `:21` |
| 构建 | `vite` | `^8.3.0` | 同上 `:37` |
| Vue 插件 | `@vitejs/plugin-vue` | `^6.0.8` | 同上 `:32` |
| 主进程打包 | `esbuild` | `^0.28.2` | 同上 `:35` |
| 语言 | `typescript` | `~5.9.3` | 根 `package.json:74` |
| 类型检查 | `vue-tsc` | `^3.3.11` | `desktop-client/package.json:38` |
| Node 类型 | `@types/node` | `^22.20.2` | 同上 `:31` |
允许额外引入的第三方库(必须写进依赖清单并锁版本):`xlsx`(Excel 导入导出;**P1 必须实现**,用 xlsx 库重建 ClassIsland 的 ReoGrid/WPF 版导入导出,ClassHelper 服务端同款 `0.18.5`,见 `packages/server/package.json:38`)、`adm-zip` 或 `yauzl`(`.cipx`/主题包解压)、`yaml`(读取插件 `manifest.yml`,见 `packages/server/package.json:40`)、`electron-log` 可选但**不强制**(本文要求自研 `logger.ts`,理由见 2.8)。禁止引入:`electron-updater`(本文的更新模型是自研的哈希校验 + 独立安装助手,见 2.11 与 12 部分)、`better-sqlite3` 等原生模块(`electron-builder.yml:37-38` 明确 `npmRebuild: false`、无原生模块)、任何需要在线服务的 SDK。
`pnpm-workspace.yaml` 必须显式声明允许执行构建脚本的依赖,否则 pnpm 11 会因 `ERR_PNPM_IGNORED_BUILDS` 直接失败:
```yaml
packages:
- 'packages/*'
allowBuilds:
esbuild: true
electron: true
vue-demi: true
electron-winstaller: false
verifyDepsBeforeRun: false
```
依据:`repos/ClassHelper-master/pnpm-workspace.yaml:1-26`(含注释解释 electron-winstaller 为何必须显式为 false)。
### 2.3 仓库目录结构(必须逐项落地)
你必须在仓库根建立如下结构;标注「新增」的是本应用特有模块,其余照抄 ClassHelper 的组织方式。
```text
classisland-ts/
├─ package.json # 版本单一来源、pnpm@11.8.0、engines>=20.19
├─ pnpm-workspace.yaml
├─ pnpm-lock.yaml
├─ tsconfig.base.json # target ES2023、strict、verbatimModuleSyntax
├─ .prettierrc.json # LF、单引号、100 列
├─ scripts/
│ ├─ version.mjs # 版本唯一修改入口(--bump patch|minor|major)
│ ├─ check-icons.mjs # 图标资源完整性门禁
│ └─ lib/electron-env.mjs # 跨平台解析 electron 可执行文件
├─ packages/
│ ├─ shared/ # 三端共享:类型/常量/schema/纯函数
│ │ └─ src/
│ │ ├─ index.ts # 统一导出(照抄 packages/shared/src/index.ts)
│ │ ├─ constants.ts # 尺寸/默认值/枚举/映射表
│ │ ├─ types.ts # DTO 与持久化类型
│ │ ├─ motion.ts # OK 动效令牌(纯常量+纯函数)
│ │ ├─ island-layout.ts # 岛行/组件方案 schema + normalize(新增扩展)
│ │ ├─ schedule.ts # 时间表/课表/周次纯函数(新增)
│ │ ├─ time.ts # 本地日期/日键/时间补偿(新增)
│ │ ├─ notifications.ts # 提醒 DTO/优先级(新增)
│ │ ├─ automation.ts # 自动化 DTO(新增)
│ │ ├─ plugins.ts # 插件清单/权限/扩展点类型(新增)
│ │ └─ ipc.ts # IPC 通道名与 payload 类型(新增,主进程与 preload 共用)
│ ├─ ui/ # 共享 Vue 控件与样式令牌
│ │ └─ src/
│ │ ├─ index.ts # registerSharedUi(app)
│ │ ├─ tokens.css # 基础色/圆角/间距(照抄)
│ │ ├─ theme.css # 语义色 + html.dark(照抄后扩展)
│ │ └─ *.vue # DatePicker/PageHeader/FullScreenGuide 等
│ └─ desktop-client/ # Electron 应用
│ ├─ package.json # main: dist/main/index.js
│ ├─ electron-builder.yml
│ ├─ index.html # 应用窗口(设置/编辑/OOBE)
│ ├─ island.html # 岛渲染入口(必须独立)
│ ├─ vite.config.mts # 双入口、base './'、outDir dist/renderer、5174
│ ├─ tsconfig.json # 渲染层(DOM lib)
│ ├─ tsconfig.node.json # 主进程/preload(node lib)
│ ├─ scripts/
│ │ ├─ dev.mjs # vite 就绪后起 electron(VITE_DEV_SERVER_URL)
│ │ ├─ build-main.mjs # esbuild -> dist/main + dist/preload
│ │ ├─ build-update-helper.mjs # 编译独立更新助手(新增,见 2.11)
│ │ ├─ dist-win.mjs # electron-builder 包装 + 产物归档
│ │ └─ check-bundle-secrets.mjs # 打包不得含凭据门禁
│ └─ src/
│ ├─ main/
│ │ ├─ index.ts # 启动编排(照抄 ClassHelper 顺序)
│ │ ├─ config.ts # config.json 读写/归一化/合并写
│ │ ├─ channels.ts # IPC 通道名叶子模块(零 import)
│ │ ├─ ipc.ts # ipcMain.handle 注册表
│ │ ├─ island.ts # 岛窗口控制器(ClassIsland 主界面宿主)
│ │ ├─ main-window.ts # 设置/编辑窗口管理(新增)
│ │ ├─ tray.ts # 系统托盘
│ │ ├─ logger.ts # main.log + 1MB 轮转
│ │ ├─ crash-guard.ts # 崩溃自愈
│ │ ├─ auto-update.ts # 下载/校验/空闲安装
│ │ ├─ update-installer.ts # 安装助手就绪/提交/回滚
│ │ ├─ storage.ts # 数据文件读写/备份/迁移(新增,见 13 部分)
│ │ ├─ scheduler.ts # 时间引擎:上课/下课/准备/放学(新增)
│ │ ├─ automation.ts # 自动化运行时(新增)
│ │ ├─ notifications.ts # 提醒队列/宿主(新增)
│ │ ├─ plugins.ts # 插件宿主(新增,见 12 部分)
│ │ └─ platform/ # 开机自启、单实例、DPI、全屏检测
│ ├─ preload/
│ │ ├─ index.ts # contextBridge 暴露 window.desktop
│ │ └─ island.ts # contextBridge 暴露 window.island
│ ├─ island/ # 岛渲染进程(手写 CSS,零 Element Plus)
│ │ ├─ main.ts # 注入动效令牌后 mount
│ │ ├─ LayoutIslandApp.vue # 选择经典/自定义渲染
│ │ ├─ spring.ts # SpringValue 帧积分
│ │ ├─ squircle.ts # 连续圆角路径
│ │ ├─ classic-island.css # 经典岛样式
│ │ └─ components/ # 岛内组件(日期/时钟/课程/进度/提醒)
│ ├─ renderer/ # 应用窗口渲染进程
│ │ ├─ main.ts # 挂载顺序见 2.9
│ │ ├─ router/index.ts # createWebHashHistory
│ │ ├─ stores/ # pinia:ui/notifications/onboarding/profile/settings
│ │ ├─ views/ # 设置中心、档案编辑、OOBE、关于…
│ │ ├─ composables/motion.ts # applyMotionTokens/useReducedMotion/useCountUp…
│ │ ├─ api/ # 本地数据访问(经 preload IPC,不含网络)
│ │ └─ styles/index.css # 语义色令牌 + 全局样式
│ └─ types/desktop.d.ts # 主进程↔渲染进程桥接契约
```
依据:`repos/ClassHelper-master/packages/desktop-client/package.json:7-18`(入口与脚本)、`vite.config.mts`(入口与产物)、`src/main/*`、`src/preload/*`、`src/island/*`、`src/renderer/*` 的既有布局。
### 2.4 进程模型(必须五类进程分离)
1. **主进程(`src/main/index.ts`)**:唯一持有窗口、托盘、配置、定时器、文件系统、自动更新、插件宿主的进程。它必须承载 ClassIsland 的「Core 服务层」职责:时间引擎、课表状态机、提醒宿主、自动化、存储与备份、插件注册表。
2. **应用窗口渲染进程(`index.html`)**:设置中心、档案编辑器、OOBE、关于页、日志、调试、恢复。按需创建;关闭窗口必须只隐藏不退出(ClassIsland 主窗口关闭即隐藏的逻辑改成「设置窗口关闭=销毁,托盘常驻」)。
3. **岛渲染进程(`island.html`)**:常驻内存,只画岛。必须独立 `BrowserWindow`,独立 preload(`dist/preload/island.js`),独立 CSS,**不得**加载 Element Plus。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:480-512`(窗口参数与 preload)、`src/island/main.ts:1-30`(零 UI 库)。
4. **插件/自动化隔离进程(`utilityProcess`,Electron 44 内置)**:插件代码、用户自动化脚本、外部命令执行必须跑在这个进程里,崩溃不得拖垮主进程;与主进程之间用 `MessagePort` 传纯数据。ClassIsland 用 `PluginLoadContext` 做程序集隔离(`repos/ClassIsland/ClassIsland/Services/PluginService.cs:186-235`),在 Electron 下等价物就是 utilityProcess + 动态 `import()`。
5. **独立更新助手(`update-helper.exe`)**:Windows 下由构建脚本生成的小型可执行程序,负责"旧进程退出后替换文件、失败回滚、写 `install-result.json`"。不得由主进程自己替换正在运行的 exe。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/auto-update.ts:516-564`(就绪→commit→退出)与 `scripts/build-main.mjs:12`(构建时产出助手)。
### 2.5 构建管线(命令与产物必须一致)
**主进程与 preload**:用 esbuild 打成 CJS,`platform: 'node'`、`target: 'node20'`、`external: ['electron']`、`define process.env.NODE_ENV`;产物固定为:
- `dist/main/index.js`
- `dist/preload/index.js`
- `dist/preload/island.js`
- `dist/main/update-helper.exe`(构建时由 `build-update-helper.mjs` 生成)
依据:`repos/ClassHelper-master/packages/desktop-client/scripts/build-main.mjs:14-46`。
**渲染进程**:用 Vite 双入口构建:`index.html`(应用窗口)与 `island.html`(岛);`base: './'`(Electron 以 `file://` 加载,必须相对路径)、`outDir: 'dist/renderer'`、`emptyOutDir: true`、`sourcemap: false`、`chunkSizeWarningLimit: 2000`;dev 服务器固定 `127.0.0.1:5174`、`strictPort: true`。依据:`repos/ClassHelper-master/packages/desktop-client/vite.config.mts:13-39`。
**开发模式**:`scripts/dev.mjs` 必须先探测 5174 端口可用,再以 `VITE_DEV_SERVER_URL=http://127.0.0.1:5174` 启动 Electron;主进程读到该变量时 `loadURL` 并打开 DevTools,读不到时 `loadFile('../renderer/index.html')`;岛的开发地址是 `${devUrl}/island.html`。依据:`src/main/index.ts:18`、`:137-145`、`:221`;`scripts/dev.mjs:21-60`。
**打包**:`electron-builder.yml` 必须包含:
| 项 | 值 | 证据 |
|---|---|---|
| appId | `com.classisland.desktop`(从 ClassHelper 改名) | `electron-builder.yml:1` |
| productName | `ClassIsland` | `:2` |
| 语言包 | 仅 `zh-CN`(删除其余 54 个 locales) | `:14-15` |
| files | `dist/**/*`、`package.json`、排除 `node_modules`、排除 `*.map` | `:30-34` |
| asar | `true` | `:36` |
| npmRebuild | `false`(无原生模块) | `:37-38` |
| compression | `normal` | `:39` |
| win target | `nsis` x64 + `portable` x64 | `:42-47` |
| artifactName | `${productName}-${version}-${arch}.${ext}` | `:52` |
| nsis | `oneClick:false`、`perMachine:false`、`allowToChangeInstallationDirectory:true`、桌面+开始菜单快捷方式 | `:54-61` |
| portable | `${productName}-${version}-${arch}-portable.${ext}` | `:63-64` |
**必须建立的门禁脚本**(在 CI/发布前按序执行,任何一步失败即中止):
1. `pnpm build:shared`(先建共享包,再建下游——依据 ClassHelper 根脚本 `package.json:11-17`)。
2. `pnpm typecheck`(`vue-tsc --noEmit` + `tsc --noEmit -p tsconfig.node.json`,依据 `desktop-client/package.json:16`)。
3. `pnpm lint`(ESLint + 自定义 authz/资源检查)。
4. `pnpm check:icons`(图标资源完整性)。
5. `pnpm build:desktop`(= check-icons + build:main + vite build + check:secrets,依据 `desktop-client/package.json:10-13`)。
### 2.6 应用启动时序(必须按此顺序,不得提前读配置)
1. 解析命令行参数(`--hidden` 静默启动、`--safe` 安全模式、`--diagnostic` 诊断模式、`--recovery` 恢复模式、`--importV1/--importV2` 导入)。
2. 若存在 `ELECTRON_SMOKE_PROFILE`(仅自动化验收使用),`app.setPath('userData', profile)`;正常启动不得设置。
3. `app.requestSingleInstanceLock()`;拿不到锁立刻 `app.quit()`,拿到锁后监听 `second-instance` 并聚焦/唤起已有窗口。依据:`src/main/index.ts:176-182`。
4. `app.whenReady()` 之后依次:`logger.setVerbose(config.developerMode)` → `registerIpcHandlers()` → `registerIslandIpc()` → 应用开机自启登录项 → 创建设置窗口(隐藏态可配)→ `installCrashGuard()` → 加载渲染页 → 注册退出回调 → `island.init()` → 创建托盘。依据:`src/main/index.ts:184-262`。
5. 退出时统一资源清理:停止定时器 → `flushConfigSync()`(配置同步落盘)→ 销毁托盘 → 销毁岛 → 销毁全部窗口 → `app.quit()`,并用 `setTimeout(() => app.exit(0), 1500)` 兜底。依据:`src/main/index.ts:291-325`。
**关键纪律**:配置必须在 `app.setPath('userData')` 之后**懒加载**,不得在模块顶层读盘(否则读到错误 profile 的 config.json)。依据:`src/main/config.ts:261-272` 的注释与实现。
### 2.7 窗口参数(像素级基线)
**岛窗口(ClassIsland 主界面)**必须满足:
- `width/height` 使用"固定包围盒 + 窗内形变",**以第 4 部分 4.3 为唯一实现口径**:`winW = 目标屏幕宽(DIP)`,`winH = contentMaxHeight + 2 × safePadding`(`safePadding ≥ 20`,其中包含阴影/光晕余量,参考 ClassHelper 的 `ISLAND_SHADOW_PAD = 10` 双侧口径);窗口在开合、行增删与提醒展开过程中尺寸不变,形变全部发生在渲染层画布内,只有显示器/DPI 变化、行数超出上限、编辑模式进出、偏移/缩放/停靠设置变化时才 `setBounds` 且节流 ≥100ms。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:72-112,480-512`(固定包围盒思想)与第 4 部分 4.3(ClassIsland 全屏宽窗口适配)。
- 窗口标志:`frame:false`、`transparent:true`、`resizable:false`、`movable:false`、`minimizable:false`、`maximizable:false`、`fullscreenable:false`、`skipTaskbar:true`、`hasShadow:false`、`focusable:false`、`alwaysOnTop:true`、`type:'toolbar'`。依据:`src/main/island.ts:480-504`;其中 `focusable` 必须为 `false`(ClassIsland 主窗口不抢焦点,见 04 §4.2)。
- `webPreferences`:`preload` 指向 `dist/preload/island.js`、`contextIsolation:true`、`nodeIntegration:false`、`sandbox:true`、`backgroundThrottling:false`。依据:`:505-511`。
- 创建后必须:`setAlwaysOnTop(appearance.alwaysOnTop,'screen-saver')`、`setVisibleOnAllWorkspaces(true,{visibleOnFullScreen:true})`、`setWindowOpenHandler(()=>({action:'deny'}))`、`will-navigate` 白名单校验、默认 `setIgnoreMouseEvents(true,{forward:true})`、**禁止** `setBackgroundMaterial()`。依据:`:514-527`。
- 定位:按 ClassIsland 的 6 个停靠位(`WindowDockingLocation`:0 左上 / 1 中上 / 2 右上 / 3 左下 / 4 中下 / 5 右下)+ `WindowDockingOffsetX/Y`(默认 0)+ `WindowDockingMonitorIndex`(默认 0);ClassHelper 的 `top-center` 等 6 个枚举与 `marginX/marginY=8` 只作参考,**不得**替代 ClassIsland 的停靠与偏移语义。依据:`repos/ClassIsland/ClassIsland/Models/Settings.cs:1810-1833`。
- **坐标口径(强制)**:所有窗口 bounds、停靠偏移、边距、组件尺寸与间距必须使用 **DIP(CSS px)**;`BrowserWindow` 的 `x/y/width/height`、`screen.getPrimaryDisplay().workArea` 全部按 DIP 传入与读取。**禁止**在业务布局代码里用 `devicePixelRatio` 做坐标换算;只有"截图/像素级视觉验收"时才用 `devicePixelRatio` 把 DIP 换回物理像素。依据:【推断】(与 04 部分统一口径,避免窗口定位与组件布局出现两套坐标系)。
- **非 Windows 平台的命中降级(必须实现)**:`BrowserWindow.setShape()` 仅在 Windows 生效;macOS/Linux 必须降级为"整窗默认 `setIgnoreMouseEvents(true, { forward: true })`,岛可见区域之外不接收鼠标,指针进入岛体时由渲染层上报 `set-interactive` 打开命中",不得为平台差异改变岛的视觉尺寸或停靠位置。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/island.ts:278-316`(`setShape` 只在 `process.platform === 'win32'` 分支执行)。
**设置窗口**必须采用 **ClassIsland 的尺寸(第 10 部分为唯一口径)**:`width 1240`、`height 550`、`minWidth 560`、`frame:false`(Windows 11 21H2+ 可用 Mica,否则退回纯色背景)、`center:true`、`minHeight 550`,并补上 `preload: dist/preload/index.js`、`contextIsolation:true`、`nodeIntegration:false`、`sandbox:true`、`backgroundThrottling:false`。依据:`repos/ClassIsland/ClassIsland/Views/SettingsWindowNew.axaml`(第 10 部分 10.1)与 `src/main/index.ts:63-88`(窗口骨架参考)。
### 2.8 主进程核心模块(ClassIsland 服务层在 Electron 下的等价物)
| ClassIsland 概念 | ClassHelper/Electron 实现位置 | 必须实现的职责 |
|---|---|---|
| `AppBase` / `App` 启动编排 | `src/main/index.ts` | 单实例、窗口、托盘、退出清理、崩溃自愈 |
| `SettingsService`(`Settings.json` → `config.json`) | `src/main/config.ts` | `config.json` 为运行期唯一设置源;`Settings.json` 只做 ClassIsland 格式只读导入/导出兼容层(不双向双写);默认值合并、字段归一化、120ms 合并写、退出同步落盘、字段白名单 |
| `ProfileService`(档案) | `src/main/storage.ts` | 档案/课表/时间表读写、临时层清理(对应 `ProfileService.CleanExpiredTempClassPlan`,`repos/ClassIsland/ClassIsland/Services/ProfileService.cs:295-328`) |
| `ComponentsService` | `src/main/storage.ts` + `src/island/*` | 组件方案 JSON 读写、迁移、注册表驱动渲染 |
| `LessonsService`(时间引擎) | `src/main/scheduler.ts` | 每 200ms 推进一次状态机、计算下一时间点/剩余时间、触发上/下课事件 |
| `NotificationHostService` | `src/main/notifications.ts` | 队列、优先级、播放会话、强提醒遮罩 |
| `AutomationService` | `src/main/automation.ts` | 触发器/行动/规则集、信号与 URI |
| `PluginService` | `src/main/plugins.ts` | 插件发现/加载/启用/卸载/依赖排序 |
| `AppLogger` | `src/main/logger.ts` | 控制台 + `app.getPath('logs')/main.log`,单文件 1MB 轮转、保留 `main.log.1` |
| `IpcService` | `src/main/ipc.ts` + `channels.ts` | 通道注册、事件下发、数据校验 |
日志必须照抄 ClassHelper 的轮转与容错口径:`LOG_MAX_BYTES = 1024*1024`、`main.log → main.log.1`、写入失败静默忽略、`app.getPath('logs')` 在 ready 前调用会抛所以路径必须懒解析。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/logger.ts:21-67`。**不得**用 `console.log` 作为唯一排障手段:打包后的 GUI 没有可见控制台。
### 2.9 渲染层挂载顺序(必须逐行对齐)
应用窗口入口 `src/renderer/main.ts` 必须按以下顺序执行,**顺序错一个都会出现白闪或首帧无动画**:
1. `createApp(App)`;
2. 注册用到的 Element Plus 图标(只注册用到的,不全量 293 个);
3. `app.use(createPinia())`;
4. `app.use(router)`;
5. `app.use(ElementPlus, { locale: zhCn })`;
6. `registerSharedUi(app)`(来自 `@classhelper/ui`);
7. 冒烟钩子(若 `window.desktop?.smokeTest`,用 `await import()` 动态加载,禁止静态 import 进主 chunk);
8. `applyMotionTokens()`(把动效令牌写进 `:root`,必须在 `mount` 之前);
9. `await router.isReady()`;
10. `app.mount('#app')`。
依据:`repos/ClassHelper-master/packages/desktop-client/src/renderer/main.ts:17-57`;动效令牌注入理由见 `docs/dev/motion.md:53-59`。
路由必须用 `createWebHashHistory()`,因为生产环境是 `file://`,history 模式会 404。依据:`src/renderer/router/index.ts:5-8,110-113`。路由守卫必须做两件事:未初始化时进 OOBE;开发者页仅当 `developerMode` 为 true 时可进并做运行时兜底(读取 pinia 状态,不每次跨 contextBridge)。依据:`:115-133`。
### 2.10 岛渲染进程的技术约定(必须与 ClassIsland 视觉对齐)
1. **岛内不得引入 Element Plus、不得引入任何图标字体**;只用 Vue + 手写 CSS + 内联 SVG。依据:`src/island/main.ts:6-10`。
2. `island.html` 必须带与主窗口一致的 CSP,且 `html, body { margin:0; padding:0; background:transparent }`。依据:`repos/ClassHelper-master/packages/desktop-client/island.html:6-21`。
3. 岛必须**再注入一次**动效令牌(两个渲染进程不共享 `:root`),漏掉会让 `transition` 简写整条失效。依据:`src/island/main.ts:13-24`、`docs/dev/motion.md:58-59`。
4. 动画时长与曲线**以第 14 部分为准**(ClassIsland 的 Storyboard/Transition 规格);弹簧动画(`SpringValue`:`stiffness=0.10`、`damping=0.68`、`momentumRetention=0.35`、`maxDistanceRatio=0.2`、`maxDt=6`、`snapThreshold=0.05px`、帧单位 `1000/60`、零过冲钳制)只用于第 14 部分明确标注为「spring/跟手」的交互(如拖拽预览、内容宽度跟随),**不得**用它替代第 14 部分给出确定时长的规格动画。依据:`repos/ClassHelper-master/packages/desktop-client/src/island/spring.ts:26-30,41-47,85-114` 与第 14 部分。
5. 圆角实现:**默认必须使用 CSS `border-radius`(等同 Avalonia `CornerRadius`,半径取 `Settings.RadiusX`,默认 8)**,以保证与 ClassIsland 的直角圆弧逐像素一致;ClassHelper 的连续圆角(squircle:超椭圆指数 `4.2`、每角 `14` 段、半径 `min(r,w/2,h/2)`)只能由主题包显式启用(P2),不得作为默认,否则 8px 角的曲率会与 ClassIsland 原版不同。依据:`repos/ClassIsland/ClassIsland/MainWindow.axaml:282` 与 `XamlThemes/FluentTheme/Styles.axaml:48`;对照实现见 `src/island/squircle.ts:21-24,44-81`。
6. 岛组件的几何基线必须集中到 `packages/shared/src/island-layout.ts` 的 `CLASSISLAND_ISLAND_METRICS`:`rowHeight 40`、`horizontalPadding 12`、`radius 8`、`opacity 0.5`、`scale 1`(0.75–2.5)、`secondaryFontSize 14`、`bodyFontSize 16`、`emphasizedFontSize 18`、`largeFontSize 20`、`rowSpacing 5`、`fadedOpacity 0.05`。依据:`repos/ClassHelper-master/packages/shared/src/island-layout.ts:19-36`。
7. 岛形态尺寸增量必须集中常量:`ISLAND_SIZE_DELTA = { expanded:{w:156,h:186}, urgent:{w:172,h:202}, call:{w:188,h:218} }`、`ISLAND_SHADOW_PAD=10`、`ISLAND_SLIVER_WIDTH=6`。依据:`packages/shared/src/constants.ts:336-351`。
8. **参考值(不是本应用默认值)**:ClassHelper 既有「经典岛」外观默认值 `width 268`、`height 44`、`radius 20`、`fontSize 13`、`opacity 1`、`accent #0a84ff`、`style 'black'`、`position 'top-center'`、`marginX/marginY 8`、`idleSliver false`、`followCursorDisplay false`(范围:高度 36–72、宽 220–420、圆角 8–32、透明度 0.4–1、字号 11–20、速度 0.5–2、marginX 0–200、marginY 0–160,依据 `constants.ts:265-302`)。本应用(ClassIsland 复刻)的默认值**必须**按第 3、4、10 部分:行高 40、圆角 8、字号 14/16/18/20、透明度 0.5、停靠位 `WindowDockingLocation=1`、偏移 0,**不得**套用 ClassHelper 经典岛的 268×44 胶囊默认值。
9. 岛内 `:hover` 规则必须包在 `@media (hover: hover) and (pointer: fine)` 内,否则希沃触摸屏会出现"点过之后永远高亮"的幽灵 hover。依据:`docs/dev/motion.md:80-81`。
10. 阴影常驻、只随形态改参数,不得在形态切换时增删 `filter`(会新建合成表面导致闪帧)。依据:`docs/dev/winisland-design-tokens.md:25-26`。
### 2.11 自动更新与打包(工程结构的一部分)
- 更新目录:`<userData>/updates`;下载先写 `.part` 再校验后 `rename`;`policy.json` 记录服务端下发的更新策略。依据:`src/main/auto-update.ts:59-133`。
- 下载超时 10 分钟;文件名必须匹配 `^[A-Za-z0-9._-]+$`(挡住路径穿越);下载完必须校验 SHA-256,服务端未给哈希则**拒绝安装**。依据:`:124-128,320-391`。
- 安装时机三档:`immediate` 立即;`after-school` 不早于 `updateNotBefore`;`idle` 需主窗口隐藏满 5 分钟(`CLIENT_UPDATE_IDLE_MS = 5*60*1000`)。依据:`packages/shared/src/constants.ts:677`、`auto-update.ts:474-506`。
- 安装必须交给独立助手:先生成/校验助手 → 助手就绪 → 主进程退出 → 助手替换文件 → 写 `install-result.json`(失败时主进程下次启动读回并展示 `failed` 状态)。依据:`auto-update.ts:516-573`。
- **安装目录策略:不做 side-by-side 多版本并存**。必须使用固定安装目录(NSIS 装到固定路径);更新助手在替换前把当前安装目录的关键文件(`ClassIsland.exe`、`resources/app.asar` 等)复制到 `updates/rollback/<版本>/`,替换失败或新版启动失败时用该副本回滚并重新拉起旧版。ClassIsland 的 `app-<版本>` + `.current` 多版本方案(`repos/ClassIsland/ClassIsland.Launcher/Program.cs:30-57`、`UpdateService.cs:841-855`)只作为"可回滚"语义参考,**不照搬目录结构**。**【推断】**
- **来源安全**:更新下载默认只接受 `https://`;镜像/自定义源必须由用户显式开启并提示"非官方来源风险",默认不启用。清单必须校验 SHA-256;签名可选(Ed25519,带 `keyId`,支持公钥轮换),详见 12.6。
- 打包产物必须做"凭据门禁":`dist/renderer` 内不得出现任何账号/口令字面量(`scripts/check-bundle-secrets.mjs`),因为 asar 可直接解包。依据:`repos/ClassHelper-master/packages/desktop-client/package.json:13`、`src/preload/index.ts:43-54`。
### 2.12 ClassIsland → ClassHelper 技术映射决策表(必须按此实现)
| ClassIsland(C#/Avalonia) | 本实现(Electron/Vue) | 关键差异与对策 |
|---|---|---|
| Avalonia `Window` + `TransparencyLevelHint=AcrylicBlur` | `BrowserWindow{transparent:true, frame:false}` + CSS 半透明背景 | Electron 下**禁止** `setBackgroundMaterial()`,也**禁止** `backdrop-filter`(透明窗口下 Chromium 采样不可靠,会闪/发灰);毛玻璃效果用**预混合半透明纯色**实现(ClassHelper 的 glass 同样是半透明色、不采样桌面模糊,见 `docs/dev/winisland-design-tokens.md:23`);编辑模式按第 4 部分用全屏半透明纯色遮罩 |
| XAML `Style`/`ResourceDictionary` 主题 | `packages/ui/src/theme.css` + `html.dark` + CSS 变量 | 主题包改为"CSS 变量清单 + 资源目录 + 预览图",见 9 部分 |
| `Storyboard`/`Transition`(如 `BackgroundWidth 300ms cubic-bezier(0.65,0,0.35,1)`) | CSS `transition`/`@keyframes` + `motion.ts` 令牌 | 令牌数值单一来源放 `packages/shared/src/motion.ts`;不得在组件里手写曲线字面量 |
| `DispatcherTimer` 时间引擎 | 主进程 `setInterval` + `setTimeout` 精确对齐 + 系统休眠唤醒修正 | Node 定时器会漂移:必须以"当前时间与下一时间点差值"重排,不得累加计数 |
| `Mutex("Global\\ClassIsland.Lock")` 单实例 | `app.requestSingleInstanceLock()` | 二次启动必须唤起已有窗口(`second-instance`),不得静默退出 |
| `dotnetCampus.Ipc` 命名管道(`ClassIsland.IPC.v2.Server`) | `ipcMain/ipcRenderer` + contextBridge;**可选**本地只读 HTTP API | 不恢复命名管道。外部只读集成如启用,必须:只监听 `127.0.0.1`、端口随机分配、默认关闭、开启后需一次性 token 鉴权、只提供读接口(当前课程/课表/状态),**不得提供任何写操作**。详见 12.4 |
| 插件 `.cipx`(zip + `manifest.yml` + `.dll`) | 同构 zip(`.cipx`)+ `manifest.yml` + ESM 入口 `.mjs` | 插件必须在 utilityProcess 内 `import()`,不得污染主进程;清单统一 camelCase、兼容读取 PascalCase 旧清单;见 12 部分 |
| `Settings.json`(`CommonDirectories.AppRootFolderPath`) | `<userData>/config.json` 为主 + `Settings.json` 导入/导出兼容层 | 运行期只读写 `config.json`;`Settings.json` 只做 ClassIsland 格式的**只读导入**与导出,**不双向双写**;`IsIslandSeperated` 等历史拼写必须保留(见 13 部分) |
| `Profiles/<name>.json` | `<userData>/data/profiles/<name>.json` | 保留 PascalCase 字段名以兼容导入导出 ClassIsland 档案 |
| `Backups/Auto_*.zip` | `<userData>/backups/` | 自动备份 7 天一次、最多 16 份,包内含 `Config/` + `Profiles/` + `Settings.json`(若存在;见 13 部分) |
| 独立 Launcher + 多版本目录 | electron-builder NSIS + 固定安装目录 + 独立更新助手 + 回滚副本 | 不做 side-by-side 多版本;见 2.11 |
| `PackageRoot` 环境变量 | `app.getPath('userData')` + `process.resourcesPath` | 所有路径必须用 Electron API 解析,禁止硬编码 `C:\` |
| `Color`(Avalonia)序列化为 `#AARRGGBB` | 字符串 `#RRGGBB`/`#AARRGGBB` | 解析必须容忍 6 位与 8 位、容忍大小写,写出统一 8 位(AlphaComponentPosition.Trailing) |
### 2.13 第三方库等价映射(.NET/Avalonia → npm,必须按此实现)
ClassIsland 依赖的一批 .NET 库在本技术栈下必须换成下表等价物;不得因为"找不到同名库"就自研底层协议。
| ClassIsland 依赖 | 本实现替代 | 使用约束 |
|---|---|---|
| `System.Text.Json` + `ConfigureFileHelper` | Node `JSON.parse/stringify` + 自研 `storage.ts` | 必须保留"写入前浅拷贝快照、写入中不阻塞 UI、退出前同步落盘"语义(见 2.16) |
| `YamlDotNet`(插件 `manifest.yml`) | `yaml`(npm,服务端同款) | 解析必须 `ignoreUnknownKeys`:未知字段不得导致插件加载失败(对齐 `repos/ClassIsland/ClassIsland/Services/PluginService.cs:68-72` 的 `IgnoreUnmatchedProperties`) |
| `NAudio`/系统音频播放 | 渲染层 `HTMLAudioElement`,主进程只下发"播放/停止/音量" | 音效文件必须走 `file://` 本地资源;音量 0–1,默认 1.0(`Settings.NotificationSoundVolume`) |
| `System.Speech`/`EdgeTtsSharp` TTS | Windows 用 `System.Speech` 等价方案:优先系统 TTS(Windows `SAPI` 通过 PowerShell 启动会污染进程,改为可插拔 TTS 服务,默认关闭),可选在线 TTS 必须由用户显式开启 | 离线时不得阻塞提醒播放,TTS 失败只记日志 |
| `RestSharp`/`HttpClient` | Node 内置 `fetch`(Electron 44/Node 20+) | 必须显式 `AbortSignal.timeout(...)`、`redirect:'error'`(对齐 `auto-update.ts:193-199`) |
| `Downloader`(分片并行下载) | 自研流式下载 + 4 并发分片 | 必须支持 `.part` 临时文件、断点续传可选、进度节流 250ms(对齐 `auto-update.ts:216-219`) |
| `Octokit`(GitHub API) | 不用:更新源改为自建静态 JSON 清单 + 固定 URL 模板 | 若保留 GitHub 通道,必须只读、无 token、失败回退到主通道 |
| `SharpZipLib`(部署包解压) | `adm-zip` 或 Node `zlib` + 自研 zip 读取 | 解压前必须校验路径不越出目标目录(对齐 `UpdateService.cs:733-743` 的 `IsSafePath`) |
| `CommunityToolkit.Mvvm`(`ObservableObject`/`RelayCommand`) | Vue `ref/reactive/computed` + `watch` | 不得把 Vue 响应式代理跨 contextBridge 传递(见 2.15 数据纯净化) |
| `DynamicData`(集合变换) | `computed` + 普通数组操作 | 大数据量列表必须避免在 `computed` 里做 O(n²) 操作 |
| `MoonSharp`(Lua 教程脚本) | 禁用脚本引擎;教程改为声明式 JSON 步骤 | 不得为了教程引入任意代码执行能力(安全边界见 2.17) |
| `Sentry`(崩溃上报) | 默认关闭;开启时只上报匿名崩溃栈,不含课表与个人信息 | 与 ClassIsland 的 `IsReportingEnabled` 默认值保持一致(默认 true,但必须可在设置中关闭) |
| `dotnetCampus.Ipc` | Electron IPC(见 12 部分) | 通道名必须与 `channels.ts` 单一来源 |
| `HotAvalonia`(热重载) | Vite HMR | 仅开发期启用;生产包不得包含 HMR 客户端 |
### 2.14 共享包(`packages/shared`)的工程约定
1. `packages/shared` 必须是**纯 TypeScript**:`type: module`、`main: ./dist/index.js`、`types: ./dist/index.d.ts`、构建用 `tsc -p tsconfig.json`,产物只含 `dist`。依据:`repos/ClassHelper-master/packages/shared/package.json:4-18`。
2. `tsconfig` 的 `lib` 不得包含 `DOM`:共享包会被主进程(Node)与渲染进程(浏览器)同时消费,任何 `document`/`window`/`localStorage` 引用都会让主进程侧编译失败。依据:`repos/ClassHelper-master/packages/shared/src/motion.ts:10-16` 的显式约束与 `docs/dev/motion.md:30-37`。
3. 入口必须单一:`packages/shared/src/index.ts` 只做 `export *`/`export type *` 转发;新增模块必须同步加一行导出。依据:`packages/shared/src/index.ts:1-16`。
4. 命名约定:类型用 `PascalCase`;常量全大写下划线;函数用 `camelCase`;持久化 JSON 字段必须与 ClassIsland 原名一致(如 `TimeLayouts`、`ClassPlans`、`WeekCountDivTotal`),以便直接导入导出 ClassIsland 档案;IPC 内部 DTO 用 `camelCase` 并在 `types.ts` 注明与持久化字段的映射。**【推断】**(ClassIsland 的 JSON 为 PascalCase,见 `repos/ClassIsland/examples/example.json:4-19`。)
5. 全部"归一化"函数必须放在 shared:例如 `normalizeIslandLayouts(input)` 负责把任意 IPC/导入数据夹紧到合法区间(最多 12 行、200 节点、6 层、颜色必须 `#rrggbb`、未知 kind 丢弃、重复 id 重命名)。依据:`packages/shared/src/island-layout.ts:256-403`。
6. `motion.ts` 必须保持"纯常量 + 纯函数":只导出缓动曲线、弹簧参数、时长令牌、`motionCssVars()`;DOM 注入放各端 `composables/motion.ts`。依据:`packages/shared/src/motion.ts:136-241`。
### 2.15 共享 UI 包(`packages/ui`)与样式令牌工程
1. `packages/ui` 必须以源码形式被消费:`exports` 直接指向 `./src/index.ts`、`./src/theme.css`、`./src/tokens.css`,不产出 dist;`peerDependencies` 只允许 `vue` 与 `element-plus`。依据:`repos/ClassHelper-master/packages/ui/package.json`。
2. `tokens.css` 只放"基础令牌"(品牌色、圆角、间距、阴影),且必须提供 `html.dark` 覆盖;不得在 `html.dark` 里改圆角与动效令牌(几何必须两套主题一致,否则切换时布局跳动)。依据:`packages/ui/src/tokens.css:1-21` 与 `docs/dev/motion.md:123-130`。
3. 基础令牌必须至少包含:`--ch-ui-primary:#5d6bfa`、`--ch-ui-bg:#f7f8fa`、`--ch-ui-surface:#fff`、`--ch-ui-surface-alt:#f3f5f9`、`--ch-ui-border:#e6e9ef`、`--ch-ui-text:#242936`、`--ch-ui-muted:#737c8e`、`--ch-ui-radius:8px`、`--ch-ui-gap:16px`、`--ch-ui-shadow:0 8px 30px rgb(30 40 70 / 8%)`;深色覆盖 `bg #14161a`、`surface #1d2027`、`surface-alt #252934`、`border #343946`、`text #e9edf5`、`muted #a2abbd`、`shadow 0 8px 30px rgb(0 0 0 / 25%)`。依据:`packages/ui/src/tokens.css:1-21`。
4. 语义色令牌必须与总纲第 3 部分一致:`--ch-surface`、`--ch-surface-alt`、`--ch-surface-sunken`、`--ch-surface-hover`、`--ch-text`、`--ch-text-secondary`、`--ch-text-muted`、`--ch-text-placeholder`。组件里一律引用变量,禁止写死 `#fff`/`rgba(0,0,0,...)`(固定深色区域除外:登录页、启动过渡页、岛本身、全屏作业看板、accent 渐变上的白字)。依据:`docs/dev/motion.md:107-138`。
5. 改令牌时必须"只改值",禁止对样式表跑批量替换:把 `#303133` 全量替换成 `var(--ch-text)` 会把令牌定义本身改成自引用,导致整条自定义属性失效。依据:`docs/dev/motion.md:128-130`。
6. Element Plus 的全站过渡必须改写 `--el-transition-*` 变量(300/200ms → 200/140ms,缓动换成 `EASE_OUT`),不得改组件源码。依据:`docs/dev/motion.md:65-66`。
### 2.16 主进程并发与 I/O 纪律(直接决定"卡不卡")
1. **主进程是 Electron 的消息泵**:任何同步文件 I/O 都会同时卡住窗口动画。必须遵守:
- 配置写入必须"内存缓存 + 异步合并写 + 退出同步兜底":写盘合并窗口 `CONFIG_WRITE_DEBOUNCE_MS = 120`,写入串行化(`writing` 标志),写入期间的改动进 `pendingWrite` 再补写一次;`flushConfigSync()` 在 `before-quit` 调用。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/config.ts:235-346`。
- 读取配置必须只读一次内存缓存,不得每次 `getConfig()` 都 `readFileSync + JSON.parse`(ClassHelper 明确记录过这条导致侧栏展开卡顿)。依据:`config.ts:235-250` 的注释。
2. **禁止在主进程做 CPU 重活**:Excel 解析、主题包解压、插件加载、备份打包必须放 `utilityProcess` 或 `node:worker_threads`;主进程只负责调度与 IPC 转发。
3. **定时器纪律**:时间引擎的 tick 周期固定 200ms(ClassIsland 主计时器同量级【推断】),每次 tick 只用"当前时间 - 时间点边界"做判断,禁止累加 count 推断时间;系统休眠唤醒后必须立即重算一次(监听 `powerMonitor` 的 `resume`)。
4. **写入磁盘必须原子**:所有 JSON 先写 `<file>.tmp`,`fsync` 后 `rename` 覆盖;备份进程写 `<Auto_*.zip>.tmp` 再改名。依据:`repos/ClassIsland/ClassIsland/Shared/Helpers/ConfigureFileHelper.cs:182-188`(`FileStream.Flush(true)` 后关闭)。
5. **数据目录结构固定**:`<userData>/` 下只允许出现 `config.json`、`data/`、`logs/`、`updates/`、`backups/`、`themes/`、`plugins/`、`cache/` 八个顶层条目;不得把临时文件写到安装目录(安装目录可能只读)。
6. **退出清理必须幂等**:托盘退出、`before-quit`、`will-quit` 都可能触发同一次清理,清理函数必须可重入(ClassHelper 用 `isQuitting` 标志 + 各资源自带 `isDestroyed()` 判断,见 `src/main/index.ts:287-341`)。
### 2.17 安全基线(不得为了"方便"削弱任何一条)
1. 渲染进程必须启用 `contextIsolation: true`、`nodeIntegration: false`、`sandbox: true`、`webSecurity: true`;两个窗口都要。依据:`src/main/index.ts:79-87`、`src/main/island.ts:505-511`。
2. preload **只**通过 `contextBridge.exposeInMainWorld` 暴露显式白名单方法,绝不暴露 `ipcRenderer` 本体;不得暴露 `require`、`process`、`Buffer`。依据:`src/preload/index.ts:17-101`、`src/preload/island.ts:10-123`。
3. 通道名必须集中在**零依赖叶子模块** `src/main/channels.ts`:preload 在 `sandbox: true` 下不能 `require('node:fs')` 等内建模块,一旦 import 了主进程模块,esbuild 会把整条依赖链打进 preload 导致"加载即失败、`window.desktop === undefined`"。依据:`src/main/channels.ts:1-28` 的注释与实现。
4. 跨桥接只能传**纯数据**:传 Vue `ref`/`reactive`(Proxy)会在进入 contextBridge 之前抛 `An object could not be cloned.`。所有调用点必须展开成字面量,且 preload 返回的订阅函数必须可取消(`onXxx` 返回取消函数或由主进程移除监听)。依据:`src/types/desktop.d.ts:144-152`、`src/preload/index.ts:32-36`。
5. 导航必须白名单精确比对:开发态比对 `origin`,生产态比对"去 hash/search 后的精确 `file://` URL",不得用 `startsWith`。窗口必须 `setWindowOpenHandler(() => ({action:'deny'}))`,外链一律 `shell.openExternal`。依据:`src/main/index.ts:42-61,90-100`、`src/main/island.ts:32-50,516-522`。
6. 两个 HTML 入口都必须带 CSP:`default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; object-src 'none'; frame-src 'none'; form-action 'none'; base-uri 'self'`。依据:`repos/ClassHelper-master/packages/desktop-client/index.html:20-23`、`island.html:6-10`。
7. 敏感数据(登录令牌、管理凭据)必须用 `safeStorage.encryptString` 加密后 base64 落盘;`safeStorage` 不可用时必须降级明文并写 warn 日志(不得静默)。依据:`src/main/config.ts:169-196`。
8. 更新域名/下载 URL 必须来自配置白名单,渲染进程不得指定任意 URL 下载或执行;安装包路径必须校验"位于 `updates` 目录内且 realpath 一致"。依据:`auto-update.ts:477-490`。
9. 插件代码必须在隔离进程运行、只能访问 manifest 声明的权限(文件读写范围、网络域名、能否弹提醒、能否执行外部程序);未声明即拒绝。见 12 部分。
10. 发布前必须跑 `check-bundle-secrets`:`dist/renderer` 与 `dist/main` 中不得出现账号/口令/私钥字面量。依据:`repos/ClassHelper-master/packages/desktop-client/package.json:10-13`。
### 2.18 环境变量、命令行参数与调试开关
| 名称 | 作用 | 约束 |
|---|---|---|
| `ELECTRON_SMOKE_PROFILE` | 把 `userData` 指到隔离目录 | 仅自动化验收使用;正常启动必须不设置 |
| `ELECTRON_SMOKE_TEST=1` | 冒烟模式标记 | 必须只影响"不弹窗、不写登录项、不自动重启"等行为,不得影响业务逻辑 |
| `VITE_DEV_SERVER_URL` | 开发态渲染地址 | 生产构建不得定义;主进程读到它才 `loadURL` |
| `--hidden` | 开机自启静默启动 | 因 Electron 44 在 Windows 上 `setLoginItemSettings` 没有 `openAsHidden`,必须自己带参数(依据 `src/main/index.ts:22-27,147-173`) |
| `--safe` | 安全模式 | 禁用全部第三方插件与主题、跳过自动化、禁用更新安装,仅加载内置能力 |
| `--recovery` | 恢复模式 | 允许从备份还原配置/档案,禁用写操作直到用户确认 |
| `--diagnostic` | 诊断模式 | 日志提到 debug、结束时在桌面生成诊断包(配置、日志、版本、插件列表,**不含**个人信息) |
| `--uri <classisland://...>` | 单实例 URI 导航 | 第二次启动时把 URI 转发给已运行实例后退出(参考 `repos/ClassIsland/ClassIsland/Program.cs:43-68,141-159`) |
| `--importV1/--importV2/<path>` | 导入旧版配置 | 导入前必须备份;失败可回滚 |
环境变量与参数必须在 `app.whenReady()` 之前解析完毕,之后不得再读 `process.argv` 做行为分支(避免窗口创建后又改变模式导致状态不一致)。
### 2.19 性能预算(超预算必须优化,不得降级功能)
| 指标 | 预算 | 测量方法 |
|---|---|---|
| 冷启动到岛首帧 | ≤ 2.5s(SSD 教室机) | 主进程打点:`app.ready` → 岛渲染进程 `did-finish-load` → 首帧 `requestAnimationFrame` 回传 |
| 常驻内存(主进程 + 岛渲染 + 设置窗口) | ≤ 350MB(对齐 ClassHelper 文档 250–350MB,见 `docs/get-started/requirements.md`) | 任务管理器观察 30 分钟 |
| 空闲 CPU | ≤ 1%(岛常驻,1 秒一次时钟 tick) | 任务管理器观察 10 分钟 |
| 岛动画帧率 | 60fps(掉帧率 <1%) | DevTools Performance 录制形变过程 |
| 渲染进程包体 | 应用窗口 ≤ 3.5MB(gzip 前)、岛 ≤ 800KB | `dist/renderer/assets` 体积 |
| 设置页首屏 | ≤ 400ms(路由切换) | Performance 面板 |
| 配置写入 | 单次 ≤ 5ms,且不阻塞主线程 > 1 帧 | 主进程日志打点 |
任何一项超标都必须优化实现(例如把计算移到 `computed` 缓存、拆 chunk、减少重渲染),**不得**通过删除动画、降低帧率、去掉 blur 等方式"达标"。
### 2.20 本地化与文案工程
1. 默认语言 zh-CN;文案必须集中在 `packages/shared/src/i18n/`(或 `locales/zh-CN.json`),组件内禁止硬编码中文字符串作为唯一来源(允许以 key 调用 `t('...')`)。
2. 日期/星期/时间格式必须走 `Intl.DateTimeFormat`(`zh-CN`),星期文本必须为"周一…周日";`Settings.ShowDate` 默认 true(`repos/ClassIsland/ClassIsland/Models/Settings.cs:44`)。
3. 数字与百分比必须用 `Intl.NumberFormat`;倒计时显示格式为 `mm:ss`,超过 1 小时显示 `hh:mm:ss`。
4. 文案长度必须按"最长的中文条目"预留宽度,禁止用英文长度估算导致换行溢出。
5. 所有错误提示必须给出"发生了什么 + 用户可以做什么",不得只抛出异常类名;配置加载失败必须落到"使用默认配置并提示可恢复备份"。
### 2.21 验收标准(第 2 部分)
实现者必须逐条自查,全部通过才算完成本节:
1. `pnpm install` 后可成功执行 `pnpm -r typecheck` 与 `pnpm build:desktop`,产物包含 `dist/main/index.js`、`dist/preload/index.js`、`dist/preload/island.js`、`dist/renderer/index.html`、`dist/renderer/island.html`。
2. 依赖版本与 2.2 表逐项一致(用 `pnpm list -r --depth 0` 核对),Node 版本 `>=20.19.0`,`pnpm --version` 输出 `11.8.0`。
3. 仓库为 pnpm workspace,`packages/*` 至少含 `shared`、`ui`、`desktop-client` 三个包,根 `package.json` 是唯一手写版本号。
4. 主进程启动时序与 2.6 一致;`config.json` 在 `app.setPath('userData')` 之后才被首次读取(代码审查可验证)。
5. 岛窗口参数与 2.7 列表逐项一致(逐行对照 `new BrowserWindow` 选项),且代码中不出现 `setBackgroundMaterial`。
6. 岛渲染进程产物中不含 Element Plus 与图标字体(检查 `dist/renderer/assets` 体积与 import 图)。
7. `applyMotionTokens()` 在应用窗口与岛入口都被调用,且都在 `app.mount()` 之前。
8. 打包产物通过 `electron-builder --win nsis portable` 生成 setup 与 portable 两个 exe,语言包仅含 `zh-CN.pak`,安装包内不含任何凭据字符串。
9. 单实例:第二次启动不产生第二套窗口/托盘,而是唤起已有实例。
10. 主进程日志写入 `app.getPath('logs')/main.log`,超过 1MB 轮转为 `main.log.1`,且退出前配置已同步落盘(`flushConfigSync` 被调用)。
**补充验收项(工程结构层面)**:
11. 仓库内不得出现硬编码绝对路径(`C:\`、`D:\`、`/home/`),所有路径必须经 `app.getPath`、`path.join`、`process.resourcesPath` 解析;代码审查用 `grep -rn "C:\\\\" packages/` 应为零命中。
12. 图标资源必须"源文件 + 派生文件"分离:设计导出的 `build/classisland.ico`/`.png` 为唯一源,其余尺寸由 `pnpm icons` 派生;`electron-builder.yml` 必须显式指定 `icon: build/classisland.ico`,不得依赖 electron-builder 的默认约定。依据:`repos/ClassHelper-master/packages/desktop-client/electron-builder.yml:48-52`。
13. 托盘必须存在且行为固定:左键/双击显示主窗口;右键菜单至少含「打开设置」「编辑岛」「隐藏/显示岛」「重启应用」「退出」;托盘图标缺失时必须用空图标兜底并记 warn,不得崩溃。依据:`repos/ClassHelper-master/packages/desktop-client/src/main/tray.ts:31-54`。
14. 开机自启必须"配置值"与"系统登录项"双向一致:设置页读的是 `app.getLoginItemSettings().openAtLogin`,不是配置文件里的值;写入失败时配置仍保存并提示"下次启动重试"。依据:`src/main/ipc.ts:66-99`。
15. 所有代码必须通过 Prettier 检查(LF 换行、单引号),且不得为了过检查对历史文件做批量格式化;提交前 `pnpm format:check` 必须通过。