- 重构版主提示词(14 parts,面向 ClassHelper 灵动岛重构) - ClassIsland 全量规格提示词(20 parts,含分卷与逐章版本) - 投喂指南、分卷、逐节文件 - 调研记录、一致性基线、生成与质检脚本
50 KiB
2. 技术栈与工程结构
2.1 技术选型总则(必须逐条遵守)
- 你必须用 TypeScript + Electron + Vue 3 实现本应用,工程形态必须是 pnpm monorepo,不得做成单包项目、不得用 Webpack、不得用 Electron Forge、不得用
electron-vite之外的脚手架替代本文规定的手写构建管线。 - 你必须完整沿用 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。 - 你不得把 ClassHelper 的服务端(Express/Prisma/Socket.IO)引入主流程。ClassIsland 是本地单机应用,一切业务数据必须落在本机;仅当实现「可选管理端联动」时才允许把
packages/server作为可裁剪子包,且主流程不得依赖网络。依据:repos/ClassHelper-master/packages/server/package.json:19-40的依赖清单,以及总纲的 P2 定义。 - 你必须把「版本」收敛为单一来源:根
package.json的version是唯一手写值,所有子包版本由脚本同步;日常功能提交不得涨版本。依据:repos/ClassHelper-master/AGENTS.md「根 package.json 是版本单一来源」与repos/ClassHelper-master/docs/dev/release.md:8-10。 - 你必须保持「渲染层零 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 直接失败:
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 的组织方式。
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 进程模型(必须五类进程分离)
- 主进程(
src/main/index.ts):唯一持有窗口、托盘、配置、定时器、文件系统、自动更新、插件宿主的进程。它必须承载 ClassIsland 的「Core 服务层」职责:时间引擎、课表状态机、提醒宿主、自动化、存储与备份、插件注册表。 - 应用窗口渲染进程(
index.html):设置中心、档案编辑器、OOBE、关于页、日志、调试、恢复。按需创建;关闭窗口必须只隐藏不退出(ClassIsland 主窗口关闭即隐藏的逻辑改成「设置窗口关闭=销毁,托盘常驻」)。 - 岛渲染进程(
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 库)。 - 插件/自动化隔离进程(
utilityProcess,Electron 44 内置):插件代码、用户自动化脚本、外部命令执行必须跑在这个进程里,崩溃不得拖垮主进程;与主进程之间用MessagePort传纯数据。ClassIsland 用PluginLoadContext做程序集隔离(repos/ClassIsland/ClassIsland/Services/PluginService.cs:186-235),在 Electron 下等价物就是 utilityProcess + 动态import()。 - 独立更新助手(
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.jsdist/preload/index.jsdist/preload/island.jsdist/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/发布前按序执行,任何一步失败即中止):
pnpm build:shared(先建共享包,再建下游——依据 ClassHelper 根脚本package.json:11-17)。pnpm typecheck(vue-tsc --noEmit+tsc --noEmit -p tsconfig.node.json,依据desktop-client/package.json:16)。pnpm lint(ESLint + 自定义 authz/资源检查)。pnpm check:icons(图标资源完整性)。pnpm build:desktop(= check-icons + build:main + vite build + check:secrets,依据desktop-client/package.json:10-13)。
2.6 应用启动时序(必须按此顺序,不得提前读配置)
- 解析命令行参数(
--hidden静默启动、--safe安全模式、--diagnostic诊断模式、--recovery恢复模式、--importV1/--importV2导入)。 - 若存在
ELECTRON_SMOKE_PROFILE(仅自动化验收使用),app.setPath('userData', profile);正常启动不得设置。 app.requestSingleInstanceLock();拿不到锁立刻app.quit(),拿到锁后监听second-instance并聚焦/唤起已有窗口。依据:src/main/index.ts:176-182。app.whenReady()之后依次:logger.setVerbose(config.developerMode)→registerIpcHandlers()→registerIslandIpc()→ 应用开机自启登录项 → 创建设置窗口(隐藏态可配)→installCrashGuard()→ 加载渲染页 → 注册退出回调 →island.init()→ 创建托盘。依据:src/main/index.ts:184-262。- 退出时统一资源清理:停止定时器 →
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 必须按以下顺序执行,顺序错一个都会出现白闪或首帧无动画:
createApp(App);- 注册用到的 Element Plus 图标(只注册用到的,不全量 293 个);
app.use(createPinia());app.use(router);app.use(ElementPlus, { locale: zhCn });registerSharedUi(app)(来自@classhelper/ui);- 冒烟钩子(若
window.desktop?.smokeTest,用await import()动态加载,禁止静态 import 进主 chunk); applyMotionTokens()(把动效令牌写进:root,必须在mount之前);await router.isReady();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 视觉对齐)
- 岛内不得引入 Element Plus、不得引入任何图标字体;只用 Vue + 手写 CSS + 内联 SVG。依据:
src/island/main.ts:6-10。 island.html必须带与主窗口一致的 CSP,且html, body { margin:0; padding:0; background:transparent }。依据:repos/ClassHelper-master/packages/desktop-client/island.html:6-21��‚- 岛必须再注入一次动效令牌(两个渲染进程不共享
:root),漏掉会让transition简写整条失效。依据:src/island/main.ts:13-24、docs/dev/motion.md:58-59。 - 动画时长与曲线以第 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 部分。 - 圆角实现:默认必须使用 CSS
border-radius(等同 AvaloniaCornerRadius,半径取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。 - 岛组件的几何基线必须集中到
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。 - 岛形态尺寸增量必须集中常量:
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。 - 参考值(不是本应用默认值):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 胶囊默认值。 - 岛内
:hover规则必须包在@media (hover: hover) and (pointer: fine)内,否则希沃触摸屏会出现"点过之后永远高亮"的幽灵 hover。依据:docs/dev/motion.md:80-81。 - 阴影常驻、只随形态改参数,不得在形态切换时增删
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)的工程约定
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。tsconfig的lib不得包含DOM:共享包会被主进程(Node)与渲染进程(浏览器)同时消费,任何document/window/localStorage引用都会让主进程侧编译失败。依据:repos/ClassHelper-master/packages/shared/src/motion.ts:10-16的显式约束与docs/dev/motion.md:30-37。- 入口必须单一:
packages/shared/src/index.ts只做export */export type *转发;新增模块必须同步加一行导出。依据:packages/shared/src/index.ts:1-16。 - 命名约定:类型用
PascalCase;常量全大写下划线;函数用camelCase;持久化 JSON 字段必须与 ClassIsland 原名一致(如TimeLayouts、ClassPlans、WeekCountDivTotal),以便直接导入导出 ClassIsland 档案;IPC 内部 DTO 用camelCase并在types.ts注明与持久化字段的映射。【推断】(ClassIsland 的 JSON 为 PascalCase,见repos/ClassIsland/examples/example.json:4-19。) - 全部"归一化"函数必须放在 shared:例如
normalizeIslandLayouts(input)负责把任意 IPC/导入数据夹紧到合法区间(最多 12 行、200 节点、6 层、颜色必须#rrggbb、未知 kind 丢弃、重复 id 重命名)。依据:packages/shared/src/island-layout.ts:256-403。 motion.ts必须保持"纯常量 + 纯函数":只导出缓动曲线、弹簧参数、时长令牌、motionCssVars();DOM 注入放各端composables/motion.ts。依据:packages/shared/src/motion.ts:136-241。
2.15 共享 UI 包(packages/ui)与样式令牌工程
packages/ui必须以源码形式被消费:exports直接指向./src/index.ts、./src/theme.css、./src/tokens.css,不产出 dist;peerDependencies只允许vue与element-plus。依据:repos/ClassHelper-master/packages/ui/package.json。tokens.css只放"基础令牌"(品牌色、圆角、间距、阴影),且必须提供html.dark覆盖;不得在html.dark里改圆角与动效令牌(几何必须两套主题一致,否则切换时布局跳动)。依据:packages/ui/src/tokens.css:1-21与docs/dev/motion.md:123-130。- 基础令牌必须至少包含:
--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。 - 语义色令牌必须与总纲第 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。 - 改令牌时必须"只改值",禁止对样式表跑批量替换:把
#303133全量替换成var(--ch-text)会把令牌定义本身改成自引用,导致整条自定义属性失效。依据:docs/dev/motion.md:128-130。 - Element Plus 的全站过渡必须改写
--el-transition-*变量(300/200ms → 200/140ms,缓动换成EASE_OUT),不得改组件源码。依据:docs/dev/motion.md:65-66。
2.16 主进程并发与 I/O 纪律(直接决定"卡不卡")
- 主进程是 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的注释。
- 配置写入必须"内存缓存 + 异步合并写 + 退出同步兜底":写盘合并窗口
- 禁止在主进程做 CPU 重活:Excel 解析、主题包解压、插件加载、备份打包必须放
utilityProcess或node:worker_threads;主进程只负责调度与 IPC 转发。 - 定时器纪律:时间引擎的 tick 周期固定 200ms(ClassIsland 主计时器同量级【推断】),每次 tick 只用"当前时间 - 时间点边界"做判断,禁止累加 count 推断时间;系统休眠唤醒后必须立即重算一次(监听
powerMonitor的resume)。 - 写入磁盘必须原子:所有 JSON 先写
<file>.tmp,fsync后rename覆盖;备份进程写<Auto_*.zip>.tmp再改名。依据:repos/ClassIsland/ClassIsland/Shared/Helpers/ConfigureFileHelper.cs:182-188(FileStream.Flush(true)后关闭)。 - 数据目录结构固定:
<userData>/下只允许出现config.json、data/、logs/、updates/、backups/、themes/、plugins/、cache/八个顶层条目;不得把临时文件写到安装目录(安装目录可能只读)。 - 退出清理必须幂等:托盘退出、
before-quit、will-quit都可能触发同一次清理,清理函数必须可重入(ClassHelper 用isQuitting标志 + 各资源自带isDestroyed()判断,见src/main/index.ts:287-341)。
2.17 安全基线(不得为了"方便"削弱任何一条)
- 渲染进程必须启用
contextIsolation: true、nodeIntegration: false、sandbox: true、webSecurity: true;两个窗口都要。依据:src/main/index.ts:79-87、src/main/island.ts:505-511。 - preload 只通过
contextBridge.exposeInMainWorld暴露显式白名单方法,绝不暴露ipcRenderer本体;不得暴露require、process、Buffer。依据:src/preload/index.ts:17-101、src/preload/island.ts:10-123。 - 通道名必须集中在零依赖叶子模块
src/main/channels.ts:preload 在sandbox: true下不能require('node:fs')等内建模块,一旦 import 了主进程模块,esbuild 会把整条依赖链打进 preload 导致"加载即失败、window.desktop === undefined"。依据:src/main/channels.ts:1-28的注释与实现。 - 跨桥接只能传纯数据:传 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。 - 导航必须白名单精确比对:开发态比对
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。 - 两个 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。 - 敏感数据(登录令牌、管理凭据)必须用
safeStorage.encryptString加密后 base64 落盘;safeStorage不可用时必须降级明文并写 warn 日志(不得静默)。依据:src/main/config.ts:169-196。 - 更新域名/下载 URL 必须来自配置白名单,渲染进程不得指定任意 URL 下载或执行;安装包路径必须校验"位于
updates目录内且 realpath 一致"。依据:auto-update.ts:477-490。 - 插件代码必须在隔离进程运行、只能访问 manifest 声明的权限(文件读写范围、网络域名、能否弹提醒、能否执行外部程序);未声明即拒绝。见 12 部分。
- 发布前必须跑
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 本地化与文案工程
- 默认语言 zh-CN;文案必须集中在
packages/shared/src/i18n/(或locales/zh-CN.json),组件内禁止硬编码中文字符串作为唯一来源(允许以 key 调用t('...'))。 - 日期/星期/时间格式必须走
Intl.DateTimeFormat(zh-CN),星期文本必须为"周一…周日";Settings.ShowDate默认 true(repos/ClassIsland/ClassIsland/Models/Settings.cs:44)。 - 数字与百分比必须用
Intl.NumberFormat;倒计时显示格式为mm:ss,超过 1 小时显示hh:mm:ss。 - 文案长度必须按"最长的中文条目"预留宽度,禁止用英文长度估算导致换行溢出。
- 所有错误提示必须给出"发生了什么 + 用户可以做什么",不得只抛出异常类名;配置加载失败必须落到"使用默认配置并提示可恢复备份"。
2.21 验收标准(第 2 部分)
实现者必须逐条自查,全部通过才算完成本节:
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 表逐项一致(用
pnpm list -r --depth 0核对),Node 版本>=20.19.0,pnpm --version输出11.8.0。 - 仓库为 pnpm workspace,
packages/*至少含shared、ui、desktop-client三个包,根package.json是唯一手写版本号。 - 主进程启动时序与 2.6 一致;
config.json在app.setPath('userData')之后才被首次读取(代码审查可验证)。 - 岛窗口参数与 2.7 列表逐项一致(逐行对照
new BrowserWindow选项),且代码中不出现setBackgroundMaterial。 - 岛渲染进程产物中不含 Element Plus 与图标字体(检查
dist/renderer/assets体积与 import 图)。 applyMotionTokens()在应用窗口与岛入口都被调用,且都在app.mount()之前。- 打包产物通过
electron-builder --win nsis portable生成 setup 与 portable 两个 exe,语言包仅含zh-CN.pak,安装包内不含任何凭据字符串。 - 单实例:第二次启动不产生第二套窗口/托盘,而是唤起已有实例。
- 主进程日志写入
app.getPath('logs')/main.log,超过 1MB 轮转为main.log.1,且退出前配置已同步落盘(flushConfigSync被调用)。
补充验收项(工程结构层面):
- 仓库内不得出现硬编码绝对路径(
C:\、D:\、/home/),所有路径必须经app.getPath、path.join、process.resourcesPath解析;代码审查用grep -rn "C:\\\\" packages/应为零命中。 - 图标资源必须"源文件 + 派生文件"分离:设计导出的
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。 - 托盘必须存在且行为固定:左键/双击显示主窗口;右键菜单至少含「打开设置」「编辑岛」「隐藏/显示岛」「重启应用」「退出」;托盘图标缺失时必须用空图标兜底并记 warn,不得崩溃。依据:
repos/ClassHelper-master/packages/desktop-client/src/main/tray.ts:31-54。 - 开机自启必须"配置值"与"系统登录项"双向一致:设置页读的是
app.getLoginItemSettings().openAtLogin,不是配置文件里的值;写入失败时配置仍保存并提示"下次启动重试"。依据:src/main/ipc.ts:66-99。 - 所有代码必须通过 Prettier 检查(LF 换行、单引号),且不得为了过检查对历史文件做批量格式化;提交前
pnpm format:check必须通过。