Files
classhelper-island-refactor…/out/sections/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

50 KiB
Raw Blame History

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 直接失败:

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 进程模型(必须五类进程分离)

  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 被调用)。

补充验收项(工程结构层面):

  1. 仓库内不得出现硬编码绝对路径(C:\、D:\、/home/),所有路径必须经 app.getPath、path.join、process.resourcesPath 解析;代码审查用 grep -rn "C:\\\\" packages/ 应为零命中。
  2. 图标资源必须"源文件 + 派生文件"分离:设计导出的 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。
  3. 托盘必须存在且行为固定:左键/双击显示主窗口;右键菜单至少含「打开设置」「编辑岛」「隐藏/显示岛」「重启应用」「退出」;托盘图标缺失时必须用空图标兜底并记 warn,不得崩溃。依据:repos/ClassHelper-master/packages/desktop-client/src/main/tray.ts:31-54。
  4. 开机自启必须"配置值"与"系统登录项"双向一致:设置页读的是 app.getLoginItemSettings().openAtLogin,不是配置文件里的值;写入失败时配置仍保存并提示"下次启动重试"。依据:src/main/ipc.ts:66-99。
  5. 所有代码必须通过 Prettier 检查(LF 换行、单引号),且不得为了过检查对历史文件做批量格式化;提交前 pnpm format:check 必须通过。