# AGENTS.md — ClassHelper(Class Helper) 本文件写给在本仓库里干活的 AI 编码代理。先读这里,再动代码。 **2026-10-16 更新**:当前仓库位于 `F:\ClassHelper`,F 盘可用。ClassIsland 联动插件、设备令牌、提醒通道与镜像接口已退役;离线课表 JSON 导入和界面设计参考保留。下文历史联动部署记录不再适用。 客户端维护只走 `/api/clients/*`,页面为 `ClientsView.vue`;默认权限按数据库中对应角色的默认组生效。 **2026-10-17 本批功能约定**:课表业务真值为 `ClassPlan / ClassPlanEntry / TimeLayoutItem`, 旧 `Schedule` 仅保留迁移审计来源,`/api/schedules/*` 是新模型的兼容门面。统一解析器在 `lib/class-plan.ts`,不得在客户端另写在线启用规则。自定义岛方案在本机 `config.json.islandLayout`, 默认 `layout=fixed`;新尺寸上报仅在 custom 模式执行。详见 `docs/dev/20261017-implementation.md`。 **2026-10-17 更新**:日常 `verify:desktop` / `verify:packaged` 默认是后台快速检查,不创建可见灵动岛或托盘; 完整桌面动画、透明度与截图验收必须显式加 `--visual`(`pnpm verify:desktop:visual`)。 下文关于置顶验证窗口、鼠标穿透与截图时序的历史约定仅适用于视觉模式。不要为了日常修改反复占用用户桌面。 联网冒烟显式启用,使用临时教师和班级;禁止回退到真实班级、重置其密码或向其发送测试通知。 本次 PowerShell 会话的 PATH 不含 pnpm;受限工具进程读取启动器可能需要工具级提权,不能据此判断未安装。 本次 PowerShell 会话的 PATH 不含 pnpm:需要先把 `C:\Users\hck16\AppData\Local\deepseek-harness\bin` 加到 PATH 前面,再按下文设置 `DSH_NODE` 和 `DSH_PNPM_BIN`。Git Bash 外层找不到 pnpm 时可执行: ```bash export DSH_NODE='D:\Node.js\node.exe' export DSH_PNPM_BIN='C:\Users\hck16\AppData\Local\node\corepack\v1\pnpm\11.8.0\bin\pnpm.cjs' node "C:/Users/hck16/AppData/Local/node/corepack/v1/pnpm/11.8.0/bin/pnpm.cjs" typecheck ``` 外层直接调用用于解决找不到命令;环境变量让内部嵌套的 pnpm 仍使用真实运行时。 文中「本机环境」「已知坑」两节按**实测**写成;若与本机现状不符,以本机实测为准并顺手改掉这里。 --- ## 1. 这是什么 班级信息管理系统,**pnpm monorepo**,一份代码产出多个交付物: | 交付物 | 位置 | 形态 | | ---------- | ------------------------- | -------------------------------------------------------- | | 后端服务 | `packages/server` | Express 5 + Prisma 7 + Socket.IO,`/api` 前缀 | | Web 管理端 | `packages/web-admin` | Vue 3 + Vite + Element Plus(教师/管理员用,PWA 可安装) | | 桌面客户端 | `packages/desktop-client` | Electron + Vue 3(学生用,含「灵动岛」浮窗) | | 共享契约 | `packages/shared` | 三端共用的类型 / 常量 / 权限 / 工具函数 | 核心链路: ``` 教师在 Web 端发布内容 │ REST API(JWT 鉴权) ▼ 后端服务(Express + Prisma) │ 写库 + 按班级房间广播 ▼ Socket.IO ──► class:{classId} 房间 ──► 学生桌面客户端实时更新 UI ``` - 远端:`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);ignores 含 releases/ / release-server/ / src/generated/ .prettierrc.json 单引号 / printWidth 110 / trailingComma all / LF / 2 空格 build/classhelper.{png,ico} 图标源(**设计导出、手工放入**;`pnpm icons` 从它派生其余全部图标,见 §5 第 62 条) scripts/ use-database.mjs SQLite ⇄ MySQL 切换(**改写 schema.prisma 里的 provider**,没有第二份 schema) generate-icons.mjs 启动 Electron 跑 icons/render.cjs(= `pnpm icons`) icons/render.cjs 把 classhelper.png 重采样成各端图标(CommonJS,见 §5 第 62 条) dist-server.mjs 服务端 + Web 管理端打包(产物落在 releases/server/<版本>/) nsis/server-installer.nsi 服务端 NSIS 安装脚本 verify-packaged.mjs 对**打包后/已安装**的客户端 EXE 复跑冒烟 check-icons.mjs 图标注册表静态门禁(漏注册会静默渲染空白,见 §5 第 53 条) version.mjs 版本号单一来源的读写与一致性校验(= `pnpm version:*`,见 §4.4) ui-smoke/{run,main.cjs,live-probe.mjs} Web 管理端真实点击回归(Electron 驱动) lib/{electron-env,node-runtime}.mjs 启动 Electron / 定位真实 node.exe deploy/ Dockerfile / docker-compose{,.sqlite}.yml / nginx.conf / package.runtime.json install.sh(Linux 一键安装器,TUI)/ install-linux.sh(转发壳) classhelper(运维命令本体)/ verify-linux.sh(Linux 验收,自统计 N/N) tools/admin-cli.mjs(离线改密 / SQLite 一致性快照) profile.d/ logrotate.d/ systemd/(装到 /etc 的模板) Dockerfile.linux-package(安装包内那份,--mode docker 用) classhelper.service(systemd 模板,5 个占位符)/ .env{,.sqlite}.example docs/ **文档站**(docfx 项目;不进 pnpm workspace) ⚠ 与官网(website/)共用**同一个** GitHub Pages 站点:官网在根、 文档站在 /docs/ 下。一个仓库在 Pages 上只有一个站点, 拼装由 scripts/build-pages.mjs 完成,见 §4.4「官网与文档站怎么发布」。 docfx.json / toc.yml(顶栏)+ 各章节自己的 toc.yml(侧栏) get-started/ app/ management/ dev/(四章内容) management/{production,linux-deploy,mysql}.md(部署与换库) dev/winisland-design-tokens.md images/(**展示图**:文档站 / README / 官网共用的同一份,1× 逻辑像素) screenshots/(回归留档的截图:island / client / web-mobile / classisland, 页面不展示;两者分工见 docs/dev/testing.md「截图留档」) templates/classhelper/(模板覆盖:_master.tmpl + public/main.css + 图标) build.mjs(构建,注入版本号与 404 的 base)/ serve.mjs(本地预览 :5181) _site/(构建产物,gitignored) ⚠ 站点的顶栏版本徽标取自根 package.json(build.mjs 用 --metadata 注入), 不要在文档里再写一份版本号。 ⚠ 页面里的照片一律指向 images/ —— 它就是 README 与官网吃的那一份。 别再往 screenshots/ 引图:那是冒烟脚本滚动的留档,随时被下一次验证覆盖。 ⚠ 模板目录里的 logo.png / favicon.png 由 `pnpm icons` 派生(见 §5 第 62 条), 别手工替换;改图标只改 build/classhelper.png 再跑 pnpm icons。 ⚠ 文档站只发布 docs/ 这一棵树,页面里**不要**写指向 docs/ 之外的相对链接 (如 ../AGENTS.md)—— 发布出去全是 404,要用 GitHub 的绝对 URL。 website/ 产品官网(**纯静态、无构建步骤**,不进 pnpm workspace) index.html(门面)/ download.html(下载页,首页的「立即下载」跳到它) assets/{styles.css,motion.js,main.js,download.js,icon.png,shots/} serve.mjs(本地预览)/ tools/capture-shots.mjs(实机图采集) 404.html(**站点根** 404,含 {{BASE}} 占位符,由 build-pages.mjs 注入) ⚠ serve.mjs 把 ../docs/_site 挂在 /docs/ 下,为的是让本地与线上同形 —— 否则页脚那几个「文档站」链接在本地全是 404。文档站得先 pnpm docs:build。 ⚠ 首屏标题第二行是 .reveal-grad:**它不参与逐字揭示** (main.js 会跳过它)。拆成 per-char 的 inline-block 后每个字各自建一层合成, 祖先的 background-clip: text 就穿不过去,渐变会整行失效。 ⚠ assets/motion.js 是动效层:曲线与弹簧 token 逐条取自 beUI (beui.dev/components/motion,见 docs/reference.md 的「官网」一节)。 改官网动效请改 token 表,不要在组件里另写 duration / cubic-bezier。 ⚠ assets/shots/ 里全是**跑起来截的**实机图,重采集步骤见 docs/dev/testing.md 的「截图留档」;客户端那两张要配合冒烟的 ELECTRON_CLIENT_SHOTS_DIR / ELECTRON_SMOKE_ISLAND_* 开关。 ⚠ download.html 的版本清单是**当场**读 GitHub Releases 的 (assets/download.js):版本分段控件 → 一行状态 → 「装什么」分段控件 (客户端 / 服务端 / 插件,**默认停在客户端**)→ 卡片。卡片是拉到数据之后 才建的,建完会派发一次 ch:content,由 main.js 补挂按压反馈 (新增动态内容照这个来)。两个分段控件共用同一套指示器实现。 卡片排版照 ClassIsland 官网那页:图标 + 名称 + 系统要求 + 主按钮 + 变体下拉 (
,点别处或 Esc 收起);插件那张只有一个文件,不摆点不开的箭头。 读不到时退回 并按发布命名约定拼直链 —— 那个 meta 是 version.mjs 的落点之一,发版必须一起改。 命名约定与 README「开始使用」表里那三个名字要对齐 (ClassHelper-<版本>-x64-client-setup.exe / -client-portable.exe / -x64-server-setup.exe),它们是离线兜底链接的唯一依据。 packages/shared/src/ types.ts / constants.ts / permissions.ts / motion.ts / utils.ts / index.ts packages/server/ prisma/schema.prisma 数据模型(29 个 model,无 enum、无 @db.*;连接串在 prisma.config.ts) prisma/migrations/ 17 个迁移(到 add_schedule_source 为止是旧模型;之后 6 个是 2026-10-06 的角色与权限重构:add_student_table / add_student_grade_query / add_grade_level / add_classhelper_heartbeat / add_archives / add_term_weeks; 最后 1 个是 2026-10-10 的 add_permission_groups_and_clients: 权限组 3 表 + AppSetting + 客户端设备/命令/安装包 + AiAudit + Notification.assignDate + User.careMode) prisma/seed.ts 种子数据(会清空业务表后重建演示数据) prisma.config.ts Prisma 7 配置:schema / migrations / seed / 连接串 scripts/verify-e2e.mjs 后端端到端验收(条数由脚本统计,2026-10-06 实测 205 项) scripts/verify-classisland.mjs 联动链路验收(实测 44 项) scripts/apply-column-migrations.cjs 旧库补列(手工覆盖 dist 部署时用) src/app.ts Express 装配(探针 → 限流 → 模块挂载 → 静态托管 → 兜底) src/index.ts 启动入口(自检 + HTTP + Socket.IO + 优雅退出) src/config/env.ts zod 环境变量校验 + 生产自检 src/lib/ access(RBAC) class-account db db-bootstrap http jwt logger mappers password schemas session term version(读 package.json) web-static src/middleware/ auth validate error security src/realtime/ socket.ts(房间)+ bus.ts(事件总线) src/modules/ 20 个功能模块 + registry.ts + module.types.ts (含 archives 毕业归档、term 学期周次、permissions 权限组、 clients 客户端心跳与远控、system 开发者模式/开机自启、ai 大模型助手) packages/web-admin/src/ api stores router layouts views(18) components(5) composables styles config.ts (含 ArchivesView 毕业归档、TermWeeksView 学期周次、PermissionsView 权限组、 ClientsView 客户端设备、AssistantView/aiSettingsView 大模型助手) packages/desktop-client/ src/main/ 主进程:index / config(含作业短语与看板偏好)/ ipc / island / tray / update / logger / smoke src/preload/ contextBridge 白名单桥(index.ts + island.ts,**不暴露 ipcRenderer 本体**) src/island/ 灵动岛渲染进程:IslandApp.vue / spring.ts / squircle.ts / main.ts src/renderer/ ClassHelper 班级端渲染进程:api / cache(IndexedDB) / stores / views / island / router src/types/desktop.d.ts 主进程 ↔ 渲染进程契约 scripts/ build-main / dev / smoke / dist-win ``` --- ## 3. 本机环境(2026-09-30 实测) | 项 | 状态 | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Node | **v22.15.0**(`D:\Node.js\node.exe`;根 `engines` 要求 ≥ 20.19,Prisma 7 的 engine 约束是 `^20.19 \|\| ^22.12 \|\| >=24.0`,v22.15 都满足) | | pnpm | 11.8.0(`packageManager` 锁定 11.8.0)。本机可用的本体在 `C:\Users\hck16\AppData\Local\node\corepack\v1\pnpm\11.8.0\bin\pnpm.cjs`;PATH 里那个 `pnpm.ps1` 是 DSH 桌面端的转发壳,**别绕它**(见下) | | npm registry | `https://registry.npmjs.org/`(仓库内无 `.npmrc`,取全局配置) | | 依赖 | 已装(707 包,`pnpm install` 实测 2m45s) | | 后端 `.env` | 已由 `.env.example` 生成(**gitignored**),`JWT_SECRET` 为本机随机值,`TERM_START_DATE=2026-09-07` | | Prisma Client | 已生成到 `packages/server/src/generated/prisma`(gitignored) | | 数据库 | SQLite `packages/server/prisma/dev.db`:10 个迁移已应用、种子已写入(用户 18 / 班级 3 / 课程 54 / 课表 93 / 作业 15 / 通知 12 / 成绩 90) | | `shared/dist` | 已构建(`pnpm build:shared`)—— **它是三端与 seed 的前置产物,缺了会 `ERR_MODULE_NOT_FOUND`** | | Electron 二进制 | 已手动装好(`node_modules/.pnpm/electron@44.3.0/node_modules/electron/dist/electron.exe`,246 MB) | | .NET SDK | ✅ **8.0.425**(`C:\Program Files\dotnet\sdk`,`dotnet --list-sdks` 有输出)⇒ 插件可编译/打包;**别再照旧文档说"没装"**(`dotnet` 在 PATH 里,`--list-sdks` 才是权威判据) | | ClassIsland | **2.1.0.1,装在 `D:\Classisland`**(启动器 `D:\Classisland\ClassIsland.exe`,实际进程 `D:\Classisland\app-2.1.0.1-0\ClassIsland.Desktop.exe`) | | ClassIsland 数据 | 数据根 **`D:\Classisland\data`**:`Config/`、`Profiles/`、`Plugins/`(当前为空,尚未装本插件)、`Logs/`、`Cache/`、`Settings.json` | | 桌面会话 | 有(`ClassIsland.Desktop.exe` 跑在 session 1);**Electron GUI 在 AI 会话里也能跑**(需先修完整性标签,见 §7 第 7 条) | > **旧文档里的 `F:\classisland` / `F:\data` 已失效**:本机只有 `C:` 与 `D:`,`F:` 盘不存在。 > 另有一份旧副本在 `C:\Users\胡\Desktop\classisland`(数据在它自己的 `data/` 下,2026-09-18 用过),当前以 `D:\Classisland` 为准。 #### AI 会话里跑 pnpm:别撞上 DSH 的转发壳(2026-10-11 实测) 在 DSH 的 shell 工具里直接写 `pnpm xxx` 会拿到 `pnpm.ps1 : Node.js runtime not found. Please run DeepSeek Harness Desktop to install it first.`, 而同一台机器上人在普通终端里跑得好好的。原因与绕过办法: - PATH 里排在前面的是 **DSH 桌面端的转发壳** `C:\Users\hck16\AppData\Local\deepseek-harness\bin\pnpm.ps1` (`.cmd` 同理)。它有两道会挡住的判据:① 认 `$env:DSH_NODE` / PATH 上的 `node`, **但要求 `major ≥ 24` 或 `22.19+`**,本机的 `v22.15.0` 卡在门外; ② 它自己的兜底 `%APPDATA%\dsh-tauri\runtime\node.exe` 与 bundled pnpm `%APPDATA%\dsh-tauri\dependencies\pnpm\bin\pnpm.cjs` **本机都不存在**。 - **正确解法是喂给这个壳两个环境变量**(它自己就认这两个覆盖开关,见 shim 第 10、52 行), 把 `node` 与 `pnpm` 都指到本机真实的那份,然后就**照常写 `pnpm xxx`**: ```powershell $env:DSH_NODE = 'D:\Node.js\node.exe' $env:DSH_PNPM_BIN = 'C:\Users\hck16\AppData\Local\node\corepack\v1\pnpm\11.8.0\bin\pnpm.cjs' pnpm typecheck # 与人在终端里跑完全等价 pnpm build:shared ``` - `DSH_NODE` 必须是**绝对路径**:壳拿到它就不再走"发现 PATH 上的 node 再卡版本"那段。 - `DSH_PNPM_BIN` 是**本体 .cjs 的路径**(不是目录,也不是 `pnpm.ps1`);壳会 `& $node $pnpmBin @args` 直接执行它。它同时替掉那条会失败的 `pnpmBin` 存在性检查。 - **`pnpm` 的嵌套调用**(`typecheck` = `build:shared && pnpm -r --no-bail typecheck`)靠这两个变量就够了: 子进程继承同一份环境,于是又落进同一个壳、又被同一对覆盖接住。 - **这三条都是踩过的坑**(同一个会话里依次试错,前两条各失败一次): - 不要用 `DSH_PNPM`,那是"用户 pnpm 的可执行路径":壳转发给它时**仍然先过 ② 的版本门槛**, 而且**残留的 `DSH_PNPM` 会污染后面所有调用**——它与 `DSH_PNPM_BIN` 同时存在时壳会转发进 corepack 的 `pnpm.ps1`,那个壳又把它重置回不存在的 bundled 路径,于是原本能跑的 `typecheck` 报出 `[pnpm] pnpm not found`。要么别设,要么先 `Remove-Item Env:DSH_PNPM`。 - 也不要用 `node