Files
ClassHelper/AGENTS.md
T
LoveChengke 5c27586af9 fix(island,classisland,server,desktop,plugin): 灵动岛去掉整窗背景材质(修卡片外圈白底面板)+ 左右边距只在左/右停靠生效 + 镜像课表补课间
- 灵动岛:移除 win.setBackgroundMaterial() 与 applyBackgroundMaterial/getBackgroundMaterial。它把系统材质刷在整个窗口矩形上(岛是"固定大包围盒窗口",卡片外还有留白),且明暗跟随应用的浅色主题 → 卡片周围出现一圈浅色面板;更糟的是调用过一次(含 none)后窗口就回不到逐像素透明,从 glass 切到 black/tinted 会永久留一圈纯白。glass 因此改为纯 CSS 半透明深色卡(WinIsland 拿不到 host backdrop 时的降级色 rgba(32,32,36,.804))
- 回归用例:新增「个性设置:卡片外圈透出桌面(三种风格都没有白底面板)」——整屏截图取样,并在岛底下垫一块已知颜色底板,让"环 = 底板色"可被精确断言(webContents.capturePage() 拍不到窗口之后的材质,是当初漏掉这个 bug 的原因)
- 边距:左右边距只在"左/右停靠"时生效(默认顶部居中时水平锚点就是屏幕中线),设置页把该滑块置灰并说明,避免被当成"调了不生效"
- 课间:镜像回 ClassIsland 的课表改为每两节之间补一段课间、最后一节之后不补(放学);服务端 synthesizeTimeLayout 同口径,不再依赖导入的时间表里有没有课间点
- 文档:README / AGENTS.md / docs/winisland-design-tokens.md / 插件 README 同步

门禁:pnpm lint 0 error、pnpm typecheck 四包通过。未跑:verify:desktop(需先 build:desktop)、verify:e2e / verify:classisland(需后端启动)。
2026-09-27 18:08:51 +08:00

36 KiB
Raw Blame History

AGENTS.md — 班级小助手(Class Helper)

本文件写给在本仓库里干活的 AI 编码代理。先读这里,再动代码。


1. 这是什么

班级信息管理系统,pnpm monorepo,一份代码产出三个交付物:

交付物 位置 形态
后端服务 packages/server Express + Prisma + Socket.IO,/api 前缀
Web 管理端 packages/web-admin Vue 3 + Vite + Element Plus(教师/管理员用)
桌面客户端 packages/desktop-client Electron + Vue 3(学生用,含「灵动岛」浮窗)
共享契约 packages/shared 三端共用的类型 / 常量 / 权限 / 工具函数
ClassIsland 联动插件 packages/classisland-plugin .NET 8 / C#(装在教室的 ClassIsland 上:上报课表与上课状态、弹出老师发的提醒)

核心链路:

教师在 Web 端发布内容
        │  REST API(JWT 鉴权)
        ▼
   后端服务(Express + Prisma)
        │  写库 + 按班级房间广播
        ▼
 Socket.IO ──► class:{classId} 房间 ──► 学生桌面客户端实时更新 UI

ClassIsland(教室机器)──► 班级小助手联动插件 ──► /api/integrations/classisland/*(设备令牌鉴权)
                                   ▲                                  │
                                   └──── 老师下发的提醒(上报返回值里顺带带回)◄┘
  • 远端:LoveChengke/ClassHelper(仓库名大小写不敏感,本地 remote 存的是小写 classhelper.git)
  • 默认分支:master
  • 语言:文档、注释、提交信息、界面文案一律中文

2. 目录速查

package.json                 根脚本(dev / build / db:* / verify:* / dist:* / icons)
pnpm-workspace.yaml          workspace + allowBuilds(pnpm 11 依赖构建白名单,勿随手删条目)
tsconfig.base.json           共享 TS 基础配置(ESM + NodeNext + strict)
eslint.config.mjs            ESLint 扁平配置(TS + Vue)
packages/shared/src/         types / constants / permissions / utils
packages/server/
  prisma/schema.prisma       数据模型(sqlite;另有 schema.mysql.prisma)
  prisma/migrations/         迁移历史(已生成并应用)
  prisma/seed.ts             种子数据(会清空业务表后重建演示数据)
  prisma.config.ts           Prisma 7 配置:schema / migrations / seed / 连接串
  src/app.ts                 Express 装配(探针 → 限流 → 模块挂载 → 静态托管 → 兜底)
  src/index.ts               启动入口(自检 + HTTP + Socket.IO + 优雅退出)
  src/config/env.ts          zod 环境变量校验 + 生产自检
  src/lib/                   db / jwt / access(RBAC) / http / mappers / term / web-static
  src/middleware/            auth / validate / error / security
  src/realtime/              socket.ts(房间)+ bus.ts(事件总线)
  src/modules/               功能模块 + registry.ts(模块注册表)
  scripts/verify-e2e.mjs     后端端到端验收(163 项)
packages/web-admin/src/      api / stores / router / layouts / views / styles / components
packages/classisland-plugin/ .NET 8 插件(独立于 pnpm workspace,不参与 pnpm install)
  manifest.yml               插件清单:id / apiVersion / entranceAssembly / version
  src/Plugin.cs              入口:读 Settings.json → 注册提醒提供方 / 设置页 / BridgeService
  src/Services/              BridgeService(上报·接收·镜像)/ ScheduleMapper / ClassPlanWriter
                             ClassHelperNotificationProvider(把提醒显示到 ClassIsland 上)
  src/Views/                 BridgeSettingsPage.axaml(.cs):Avalonia 设置页
  src/Interop/               ClassHelperClient.cs:HTTP 客户端 + 与服务端逐个字段对齐的 DTO
  scripts/                   build.mjs(编译 / 打包 .cipx)/ verify.mjs(静态契约校验)
packages/desktop-client/
  src/main/                  主进程:窗口 / 单实例 / IPC 配置 / 托盘 / 灵动岛 / smoke
  src/preload/               contextBridge 白名单桥(**不暴露 ipcRenderer 本体**)
  src/island/                灵动岛独立透明置顶窗口的渲染进程
  src/renderer/              学生端渲染进程:api / stores / cache(IndexedDB) / views
  src/types/desktop.d.ts     主进程 ↔ 渲染进程契约
  scripts/                   build-main / dev / smoke / dist-win
scripts/                     use-database.mjs / generate-icons.mjs / dist-server.mjs / ui-smoke/
deploy/                      Dockerfile / docker-compose.yml / nginx.conf / install-linux.sh
docs/                        production.md(生产部署)/ mysql.md / winisland-design-tokens.md
README.md                    完整产品与交付说明(改动交付形态时同步更新)

3. 本机环境(已配置好,可直接开工)

项 状态
Node v22.15.0(要求 ≥ 20.19,Prisma 7 硬性要求)
pnpm 11.19.0(packageManager 锁定 11.8.0)
npm registry https://registry.npmmirror.com/(全局 pnpm 配置;仓库内无 .npmrc)
依赖 pnpm install 已完成(707 包)
后端 .env 已由 .env.example 生成 packages/server/.env(gitignored),JWT_SECRET 为本机随机值
Prisma Client 已生成到 packages/server/src/generated/prisma(gitignored)
数据库 SQLite packages/server/prisma/dev.db,5 个迁移已应用、种子已写入
Electron 二进制 已下载(node_modules/.pnpm/electron@44.3.0/node_modules/electron/dist/electron.exe)
.NET SDK 8.0.303(构建 ClassIsland 插件用;插件与 pnpm workspace 无关,不参与 pnpm install)
ClassIsland 本体 F:\classisland\ClassIsland.Desktop.exe(2.1.0.1,与本插件目标版本一致);数据根目录是 F:\data(配置 F:\data\Config、档案 F:\data\Profiles、日志 F:\data\Logs、插件 F:\data\Plugins\<id>),因此插件可以真跑起来验证

从零重建环境

pnpm install

# 后端环境变量(.env 不入库,必须自己生成)
cp packages/server/.env.example packages/server/.env   # 然后至少改 JWT_SECRET

pnpm db:generate
pnpm db:deploy        # 应用迁移(见 §7 第 6 条:不要用 db:migrate)
pnpm db:seed          # 写演示数据

pnpm build:shared     # 三端都依赖 shared 的 dist,先构建它

Electron 二进制不会随 pnpm install 自动下载(本机 postinstall 被跳过),需要手动补:

$env:ELECTRON_MIRROR="https://cdn.npmmirror.com/binaries/electron/"
$env:ELECTRON_CACHE="F:\ClassHelper\.cache\electron"
$env:NODE_OPTIONS="--use-system-ca"
node install.js   # 工作目录:node_modules/.pnpm/electron@44.3.0/node_modules/electron

演示账号(pnpm db:seed 产出)

角色 用户名 密码
管理员 admin admin123
班主任 teacher1 / teacher2 teacher123
学生个人号 student01 … student15 student123
学生端班级账号 班级码 G101 / G102 / G103 123456

4. 常用命令

命令 说明
pnpm dev 并行启动 shared(tsc watch) + 后端(4000) + Web 端(5173)
pnpm dev:server / pnpm dev:web 只启动后端 / 只启动 Web 端
pnpm dev:desktop 客户端开发模式(Vite 5174 + Electron,热更新)
pnpm build 构建 shared + 后端 + Web 端 + 客户端
pnpm build:shared 只构建 shared(改完 packages/shared 必须跑)
pnpm build:desktop 只构建客户端(esbuild 主进程/preload + Vite 渲染进程)
pnpm build:classisland-plugin 构建 ClassIsland 联动插件(.NET 8;缓存/临时目录自动指到 .cache/)
pnpm dist:classisland-plugin 打包插件为 .cipx 并归集到 releases/classisland-plugin/
pnpm typecheck 全仓库类型检查(含 vue-tsc)
pnpm lint / pnpm lint:fix ESLint
pnpm format / pnpm format:check Prettier
pnpm db:generate 生成 Prisma Client
pnpm db:deploy 应用已有迁移(本机推荐)
pnpm db:seed / pnpm db:reset 写种子 / 重置并重播种子
pnpm db:studio Prisma Studio
pnpm db:switch:mysql / db:switch:sqlite 切换数据库 provider(配合 docs/mysql.md)
pnpm verify:e2e 后端端到端验收 163 项(需后端已启动)
pnpm verify:web Web 管理端真实点击回归 27 项(需后端已启动且 Web 产物已构建)
pnpm verify:desktop 客户端冒烟 77+ 项(Electron,无人工点击)
pnpm verify:packaged 对打包后/已安装的客户端 EXE 跑同一套冒烟
pnpm verify:classisland ClassIsland 联动链路(设备令牌 / 上报 / 提醒下发与回执 / 镜像契约,需后端已启动)
pnpm verify:classisland-plugin 插件静态契约校验(清单一致性 / 注册完整性 / C# DTO ↔ 服务端 zod 与路由)
pnpm dist:server / dist:win / dist:dir / dist:all 打包服务端安装程序 / 客户端安装包 / 免安装目录 / 两者
pnpm icons 生成应用图标(Electron 渲染 SVG → PNG/ICO)

端口

端口 用途
4000 后端 REST + Socket.IO + (生产/构建后)Web 管理端静态托管
5173 Web 管理端开发服务器(/api、/socket.io 代理到 4000)
5174 桌面客户端渲染进程开发服务器

交付产物归集(本机约定:releases/)

打包产物统一归集到仓库根的 releases/(已被忽略,不入库):

releases/
  client/  win-unpacked/                        免安装目录(verify:packaged 直接指它)
           班级小助手-<版本>-x64-setup.exe        学生端 NSIS 安装包
           班级小助手-<版本>-x64-portable.exe     学生端单文件版
  server/  classhelper-server/                  免安装目录(内置 node.exe,双击 start.cmd 即用)
           班级小助手服务端-<版本>-x64-setup.exe  服务端安装程序(含 Web 管理端)
  classisland-plugin/
           ClassHelper.ClassIslandPlugin.cipx    ClassIsland 插件包(从文件安装)
           ClassHelper.ClassIslandPlugin/        免安装目录(直接丢进 ClassIsland 的 Plugins)
# 客户端:electron-builder 支持直接指定输出目录(dist-win.mjs 会把额外参数透传给它)
pnpm build:desktop
node packages/desktop-client/scripts/dist-win.mjs -c.directories.output=<绝对路径>\releases\client

# 服务端:脚本输出路径写死在 release-server/,打完需要自己移进 releases/server/
pnpm dist:server          # 首次会跑 46s 左右的 npm install;重试可加 --reuse-deps 跳过

# ClassIsland 插件:构建脚本自己归集到 releases/classisland-plugin/
pnpm dist:classisland-plugin

5. 架构硬约定(改代码前必须知道)

  1. 模块注册表是唯一入口。 服务端每个功能是 src/modules/<name>/(*.module.ts 路由、 *.schemas.ts zod 校验、*.service.ts 业务)。新增/下线功能只改 src/modules/registry.ts(加一行 / 改 enabled: false),不要动 app.ts。

  2. 统一响应与校验。 用 src/lib/http.ts 的 sendOk / sendCreated / … 返回,用 middleware/validate.ts 挂 zod schema;错误交给 middleware/error.ts,不要各写一套。

  3. 鉴权 = JWT + RBAC + 班级内角色。 路由级用 requireRole('ADMIN', 'TEACHER');班级维度用 @classhelper/shared 的 resolveClassRole → ADMIN / HEAD(班主任)/ SUBJECT(科任)/ STUDENT / NONE, 权限判 canManageSchedule / canPublishContent / canManageRoster 等,不要在页面里另写一套规则。

  4. 学生端主体是「班级账号」。 班级码 + 班级密码登录后,classId 即身份;该设备的「已读 / 完成」 代全班操作,个人学生登录已停用。API 里对此有专门分支,别按个人账号语义改。

  5. 实时推送只走房间。 服务端 src/realtime/(socket.ts 房间 class:{classId}、user:{id};bus.ts 事件总线), 客户端 renderer/stores/realtime.ts 订阅。新增实时事件要同时改三处:服务端 bus/socket、Web 端订阅、 客户端订阅(灵动岛也依赖它)。

  6. 共享契约只在 packages/shared。 三端都 import ... from '@classhelper/shared';它产出 dist, 改完要 pnpm build:shared(或跑着 pnpm dev 的 watch)。类型/常量不要在两端各写一份。

  7. 数据模型跨库通用。 schema.prisma 只用 String / Int / Float / Boolean / DateTime,不用 Prisma enum、 不用 @db.*;角色与优先级存字符串并在 API 层用 zod 校验(这样 SQLite / MySQL 行为一致)。 改模型要同步 schema.mysql.prisma 和 prisma/migrations/。

  8. 周次口径唯一。 当前教学周 = shared.resolveCurrentWeek(TERM_START_DATE, now, MAX_TERM_WEEK), MAX_TERM_WEEK = 30,班级还有自己的 Class.termWeeks(默认 20)。别在别处重新实现周次换算。

  9. Electron 安全边界。 src/preload/ 只通过 contextBridge 暴露白名单方法,不要暴露 ipcRenderer 本体; 主进程 ↔ 渲染进程的类型统一写在 src/types/desktop.d.ts。

    跨桥接只能传纯数据。 contextBridge 在参数跨越"主窗口 → 隔离世界"时就做结构化克隆, 传 Vue 的 ref.value / reactive() 对象(Proxy)会抛 An object could not be cloned.—— 而且它在进入 preload 函数体之前就失败了,所以桥接层无法兜底,只能由调用方展开成 字面量再传(saveConfig({ island: { ...island.value } }))。这类失败是"半静默"的: 界面数字照常变化、只多一行 console error,看起来就是"设置不生效"。设置页曾因此翻车, verify:desktop 里的「设置页真实 UI:拖拽滑块即时改变灵动岛外观」就是为它加的回归。

  10. 客户端离线优先。 渲染进程数据先落 IndexedDB(renderer/cache/),断网回退缓存并在顶部提示离线; 新增页面/数据源要按这个模式接。

  11. Web 端设计令牌集中。 圆角/投影写在 packages/web-admin/src/styles/index.css 的 :root (--ch-radius-* / --ch-shadow-*),组件里不要写死数值;Element Plus 通过覆盖它的 CSS 变量对齐。 手机小屏(≤768px)用抽屉导航 + 卡片内横向滚动,新增页面沿用 useResponsive.ts + .table-card 约定。

  12. ClassIsland 联动是独立鉴权通道,不要塞进 JWT。 插件用设备令牌(X-ClassIsland-Token,库里只存 sha256) 调 /api/integrations/classisland/*;Web 端管理设备仍走 JWT + requireRole。新增插件接口时, 路由挂 integrations.module.ts,并在 integrations.schemas.ts 里写 zod —— 插件侧的 C# DTO 必须逐字段对齐(pnpm verify:classisland-plugin 会校验字段名与路由)。

  13. 课表镜像契约必须带 startTime/endTime。 ClassIsland 的 ClassPlan.Classes[i] 强制与时间表里 第 i 个 TimeType == 0(上课)的时间点对齐:镜像下发少一个时间点,整份课表就会错位, 而且表现为"课表看着有内容、每节课都对不上",极难排查。


  1. 所有发布入口共用同一份 ClassIsland 推送实现(integrations.service.ts 的 pushToClassIsland): 通知发布 / 叫人 / 联动页的下发都走它。新增「发通知」类入口时必须复用,不要再各写一套 —— 否则又会出现「某个页面发的通知不联动 ClassIsland」。

  2. 提醒弹在哪个端由教室客户端选,值存在 Class.notificationChannel(both / client / classisland): 客户端设置页写它(班级账号也有写权限,见 PATCH /api/classes/:id/notification-channel), 服务端据此决定是否推 ClassIsland(shouldPushToClassIsland)。改动这条链路要同时动三处: 客户端设置页与本地配置、lib/mappers 的 DTO、服务端推送判断。

  3. 灵动岛的左右边距只在「左/右停靠」时生效:默认是「顶部居中」,水平锚点就是屏幕中线, 此时调 marginX 不会有任何变化(设置页会把滑块置灰并说明,别把这个提示删掉 —— 用户就是因此以为「调了不生效」)。上下边距与所有停靠位置都相关,不受影响。

  4. 镜像进 ClassIsland 的课表要带课间:每两节之间补一段(上一节下课 → 下一节上课), 最后一节之后不补(放学)。实现见 ClassPlanWriter.BuildTimeLayout(自己按课目时间算), 服务端 synthesizeTimeLayout 同口径。

  5. 提醒分两类,决定"上课时段弹不弹":ClassIslandPush.kind = notification(通知类,上课时段 客户端与插件都暂存、下课补弹)/call(叫人,算「主动通知」,立刻弹);urgent 同样立刻弹。 插件侧的实现在 ClassHelperNotificationProvider(_deferred + FlushDeferred,订阅课程事件), 客户端的实现在 renderer/island/bridge.ts。改这条规则要两端一起改。

  6. 作业按「所属日期」归类,不要用 createdAt 当"哪一天":Homework.assignDate 是本地日期 (YYYY-MM-DD,不带时区),按天查看 / 日期高亮 / 客户端"今天"都看它。 UTC 切片(toISOString().slice(0,10))在晚上的录入会算到第二天 —— 统一用 @classhelper/shared 的 dayKeyLocal / isDayKey / shiftDayKey。新增按天接口时同样用 ?date=YYYY-MM-DD + zod 的 isDayKey 校验。

  7. 教室机器能录入作业:POST /homeworks 的路由故意不加 requireRole,权限在 homeworks.service.createHomework 里判(staff 或本班班级账号)。班级账号的 sub 是班级 id, 写 createdBy 前要换成该班班主任,否则撞 User 外键。

  8. 灵动岛不要用窗口背景材质(win.setBackgroundMaterial())。它看起来像 WinIsland 的 DWMWA_USE_HOSTBACKDROPBRUSH,其实是整窗系统材质:刷在整个窗口矩形上(岛是"固定大包围盒窗口", 卡片之外还有留白),且明暗跟随应用主题(本客户端是浅色 Fluent 主题 ⇒ 材质必然是浅色), 于是卡片周围会出现一圈浅色"白底面板"。更坑的是调用过一次就再也回不去逐像素透明 (含 'none':Electron 会让窗口回落到默认不透明底色 #FFF),所以从 glass 切到 black/tinted 后会永久留一圈纯白。CSS backdrop-filter 也替代不了(透明窗口里采样不到桌面)。 glass 因此用 CSS 半透明深色卡(WinIsland 无 backdrop 时的降级色 rgba(32,32,36,.804))。 回归用例:「个性设置:卡片外圈透出桌面(三种风格都没有白底面板)」—— 它整屏截图取样 (webContents.capturePage() 拍不到窗口之后的材质,是当初漏掉这个 bug 的原因), 并在岛底下垫一块已知颜色的底板窗口,好让"环 = 底板色"可以被精确断言。

6. 代码风格

  • TypeScript ESM + NodeNext:package.json 全是 "type": "module",相对导入必须带 .js 后缀 (写 ./foo.js,即使源文件是 foo.ts)。strict 开启。
  • Prettier:单引号、printWidth: 110、trailingComma: all、arrowParens: always、行尾 LF、缩进 2 空格。
  • ESLint(eslint.config.mjs):no-explicit-any 为 warn;未使用变量为 warn,可用 _ 前缀豁免; consistent-type-imports 建议用 import type。.cjs(Electron 图标渲染、UI 冒烟)允许 require。
  • Vue:视图命名 XxxView.vue,组件多单词;路由入口用 meta.roles 控制(如 ['ADMIN']), 隐藏入口之外服务端也必须拦(前端隐藏 ≠ 权限)。
  • 提交信息:Conventional Commits + 模块 scope,描述用中文,可多 scope,例如 fix(island): 胶囊"点不动"的根因——窗口必须始终可激活、 feat(teachers,classes,schedules,web): 教师录入(仅管理员)。
  • 改动交付形态 / 新增功能后,同步更新 README.md(它兼作产品说明书与验收对照表)。

7. 本机环境已知坑(踩过,别再踩)

  1. 系统代理会拦截 127.0.0.1 的自测请求。 Invoke-WebRequest http://127.0.0.1:4000/api/health 会返回别人的 404(响应带 Alt-Svc: h3=…、空 body), 看起来像后端路由没挂上。实际是走了代理。自测请加 -NoProxy(curl.exe 用 --noproxy '*')。 项目内的 Node 脚本(verify:*)直连,不受影响。 同一个坑对 ClassIsland 插件同样成立:.NET 的 HttpClient 默认跟随系统代理, 教室机器上表现为"填好令牌后立刻 HTTP 404"(其实是代理的错误页)。插件里 ClassHelperClient 用 LocalAwareProxy 对本地/内网地址强制直连, verify:classisland-plugin 有 4 项断言盯着这段逻辑,别删。
  2. pnpm 由 Electron 宿主承载(pnpm config list 里 userAgent 是 node/v24.19.0,而 node -v 是 v22.15.0)。 个别情况下依赖的 .bin 不会进子进程 PATH,pnpm run <script> 报「不是内部或外部命令」。 规避:直接用 Node 调入口,例如 node packages/server/node_modules/prisma/build/index.js generate (prisma.config.ts 里的 seed 命令已按这个思路写成 node ./node_modules/tsx/dist/cli.mjs)。
  3. 启动 Electron 的脚本必须清掉 ELECTRON_RUN_AS_NODE,且不要 import 'electron' 取可执行文件路径。 scripts/lib/electron-env.mjs、scripts/lib/node-runtime.mjs 已封装,新脚本请复用。
  4. Electron 二进制要手动装(见 §3),否则 pnpm dev:desktop / verify:desktop / dist:win 全部失败。
  5. 依赖二进制下载需要 NODE_OPTIONS=--use-system-ca(本机 TLS 中间人证书),否则报 unable to verify the first certificate。
  6. pnpm db:migrate(= prisma migrate dev)在本机不可靠:首次报 Schema engine error:(空消息), 数据库建好后再跑会长时间挂起(无输出,需手动结束进程)。 日常用 pnpm db:deploy;要写种子直接 node ./node_modules/tsx/dist/cli.mjs prisma/seed.ts(cwd 为 packages/server)。
  7. 后端启动时才发现 Web 产物:packages/web-admin/dist 不存在就跳过静态托管,http://127.0.0.1:4000/ 只会返回服务首页(文案「Web 管理端未随本服务托管」)。要跑 verify:web,先 pnpm build 再重启后端。
  8. 原生模块按 Electron ABI 构建:本项目因此用 libSQL 适配器(@libsql/win32-x64-msvc 预编译), 不要为了图快换回 better-sqlite3 之类需要 node-gyp 的方案。
  9. Prisma 版本锁死 7.10.0:npm latest 已指向 8.x-rc;生成器是 prisma-client,输出到 src/generated/prisma(gitignored),连接串在 prisma.config.ts,并且必须走 driver adapter。
  10. 服务端安装包里的 .cmd 提示是英文:cmd.exe 按 GBK 解析脚本,中文会乱码,属有意为之(中文在安装向导与 README.txt)。
  11. pnpm dist:server 生成安装程序时可能报 NSIS Internal compiler error #12345: error creating mmap the size of N。 原因是模板用了 SetCompressor /SOLID lzma,makensis 会在 %TEMP% 所在的盘建一个上百 MB 的 mmap 临时文件; 本机 C: 只剩约 100 MB 时会失败(免安装目录其实已经生成好了,只是最后一步打不出安装包)。 规避:把临时目录指到空闲的盘再重试,并加 --reuse-deps 跳过依赖安装: $env:TEMP='F:\ClassHelper\.cache\tmp'; $env:TMP=$env:TEMP; node scripts/dist-server.mjs --reuse-deps。
  12. 仓库根 .gitignore 是 GBK 编码(含一段乱码注释),apply_patch 与 Prettier 都读不了它。 要补忽略规则请写 .git/info/exclude(本地生效、不入库),别试图整文件重写。 (packages/classisland-plugin/.gitignore 是本仓库里唯一能正常编辑的 .gitignore,用来忽略 bin/obj/cipx。)
  13. dotnet 会把 NuGet 缓存与临时文件写进 C 盘(%USERPROFILE%\.nuget、%TEMP%), C 盘紧张时直接构建失败。pnpm build:classisland-plugin 已把 NUGET_PACKAGES/TEMP/TMP/DOTNET_CLI_HOME 指到仓库内 .cache/;手敲 dotnet build 时请自己带上这几个变量。
  14. .cipx 打包最后一步要 PowerShell:SDK 的 CreateCipx 目标默认调 pwsh, 本机 PATH 里没有 pwsh 时会失败。构建脚本会探测并把结果透传给 MSBuild (-p:PowershellBinaryName=…),直接跑 dotnet build -p:CreateCipx=true 则要自己加。
  15. releases/(复数)必须在 ESLint 忽略列表里:本地跑过 pnpm dist:* 之后, 这个目录里是构建后的压缩 JS,不忽略的话 pnpm lint 会报出几千条与源码无关的 error (eslint.config.mjs 里已补 **/releases/**,别再删掉)。

已知脆弱用例(不是环境问题,别去"修环境")

用例 触发条件
verify:e2e「上课状态接口(at=周一 08:10)」 种子把「周一第 1 节」限制在第 1 周,且用例把 2026-09-07 当第 1 周。TERM_START_DATE 对齐到 2026-09-07 即全绿(本机 .env 已这样配;.env.example 仍是 2026-02-23,重置 .env 时记得改回来)
verify:desktop「课表今天时间轴」 凌晨约 00:00–03:00 时「已结束」探针被夹紧成 0 长度而被过滤,已结束=false 判定失败
verify:web「课表科目为全校统一目录」 种子给每个班建满了全部科目,下拉里没有可自动建课的「统一科目」分组

  1. 改完插件必须更新 F:\data\Plugins\classhelper.classisland.bridge\ 里的 DLL 并重启 ClassIsland, 否则跑的还是旧副本(ClassIsland 运行时会锁住 DLL,必须先退出再覆盖)。 别把该目录下的 Settings.json 覆盖掉 —— 那里面是这台机器的服务器地址与设备令牌。

  2. ClassIsland 的提醒链路有三个"上机才暴露"的硬约束(都已在插件里处理,并写进 verify:classisland-plugin 的静态断言,别删):

    • 提醒内容必须在 UI 线程构造 + 显示,否则 LucideIconSource → AvaloniaObject 构造时抛 Call from invalid thread,表现为"ClassHelper 客户端收到了、ClassIsland 毫无反应";
    • 服务端"回执才算送达",因此插件必须对播放中的提醒按 id 去重,否则轮询间隔 < 提醒时长时会反复弹;
    • Subject.Fallback 的名字就是 ??? 占位符,上报前要归一化成"没有科目"。
  3. 读 ClassIsland 控制台日志要用 GBK:.NET 的控制台输出是**系统 ANSI(GBK)**编码, 用 -Encoding UTF8 读会变成乱码、中文关键字一条都搜不到(只能搜到 ASCII 的类名)。 正确姿势:[System.IO.File]::ReadAllLines(path, [System.Text.Encoding]::GetEncoding(936)); 日志文件被运行中的进程占用时先 Copy-Item 一份再读。

  4. 改插件设置要趁 ClassIsland 退出时改:插件配置在 F:\data\Config\Plugins\<id>\Settings.json, ClassIsland 运行时会在内存里持有并随时回写,运行中改文件会被覆盖。改完 DLL/设置都要重启 ClassIsland。

  5. 打包版服务端有生产自检:NODE_ENV=production 时拒绝 dev.db("生产环境仍在使用 dev.db"), 想用演示数据跑打包版就把库复制成 data/classhelper.db 再启动。

  6. 服务端改了代码要重启才生效(tsx src/index.ts 不热载,pnpm dev:server 才有 watch)。 验证脚本报「某个断言突然不对」时,先确认后端跑的是不是最新代码 —— 这个坑踩过一次。

  7. 灵动岛的位置由「屏幕工作区 + 边距」算,改边距默认值会改外观:appearance.marginX/marginY 默认 必须与老行为一致(8px)—— verify:desktop 的「停靠位置生效(6 个锚点)」断言的是像素坐标, 默认值一动它就红(我先把 marginX 设成 16 踩过一次)。新增边距类设置请只改"可调范围",别动默认值。

8. 交付前检查

pnpm lint                # ESLint(0 error 为底线)
pnpm typecheck           # 全仓库类型检查

pnpm dev:server          # 另开终端
pnpm verify:e2e          # 后端 163 项
pnpm verify:web          # Web 29 项(其中 1 项是"已知脆弱用例",见上表)
pnpm verify:classisland  # ClassIsland 联动链路 24 项
pnpm verify:classisland-plugin          # 插件静态契约 54 项(不需要后端)
pnpm build:desktop && pnpm verify:desktop   # 客户端冒烟

改动到 ClassIsland 插件时,额外跑 pnpm dist:classisland-plugin 确认能打出 .cipx; 并在真机上过一遍(本机就有 ClassIsland,见 §3):

# 1) 退出 ClassIsland(DLL 被占用时无法覆盖)→ 2) 覆盖插件 → 3) 重新启动
Get-Process ClassIsland.Desktop | Stop-Process -Force
Copy-Item packages\classisland-plugin\bin\Release\ClassHelper.ClassIslandPlugin.dll F:\data\Plugins\classhelper.classisland.bridge\ -Force
Start-Process F:\classisland\ClassIsland.Desktop.exe -WorkingDirectory F:\classisland
# 4) 在 Web 端「ClassIsland 联动」发一条提醒,看启动时控制台/日志里依次出现:
#    取到待弹出提醒 → 已弹出提醒 →(播完)已确认提醒

交付时要说明"编译 / 打包 / 静态契约已过,运行期需在装了 ClassIsland 的机器上实测"。

pnpm format:check 在 master 基线上本来就是失败的(175 个文件未过 Prettier,含 tsconfig.base.json 与 scripts/*),不要拿它当门禁、也不要为了让它变绿去批量格式化。只检查你改过的文件: pnpm exec prettier --check <file>。

改动涉及打包/交付形态时,额外跑 pnpm dist:all(或 dist:server + dist:win), 并用 pnpm verify:packaged --exe "<客户端路径>" 对打包后的副本复验 —— 开发产物通过不等于用户机器上的副本通过。

自测本地接口记得 -NoProxy(见 §7 第 1 条)。