Files
ClassHelper/docs/dev/architecture.md
T
LoveChengke b7cdc7c9ea fix(clients,client,web): 远控指令的两个修复 —— 执行失败不再谎报成功 + 「上次指令」让"点了没反应"可见(1.3.2)
背景:用户报「下发锁屏等等指令不生效」。先把服务端那条链路单独验了一遍
(模拟客户端:登记 → 下发 → 心跳带回 → 设备列表可见 → 回执 → 清除),**整条是通的**;
真正的问题在两处。

客户端:执行失败会谎报成功
- `spawn` 找不到可执行文件 / 没权限时**不抛异常**,而是在下一个 tick 发一个异步 `error` 事件 ——
  只包 `try/catch` 抓不到,代码一路走到 `return { ok: true }`,回执写着「已下达系统命令」,
  Web 端看起来一切正常而机器纹丝不动
- 现在等一个 200ms 的窗口确认没有立刻报错,失败如实回执(`client-agent.ts` 的 `executeCommand`)

Web 端:点了没生效,界面上什么都不说
- 一条命令被**取消 / 过期**之后就从 `pendingCommand` 里消失,页面留白 ——
  而最常见的两种"没反应"恰恰是「本机没人确认(客户端 60 秒自动按取消)」与「下达时机器离线」
- `GET /clients/devices` 增加 `lastCommand`(最近一条,不论状态);
  「客户端」页的设备表多一列「上次指令」,由 `commandStateText()` 翻成一句人话
  (已执行 / 本机用户取消 / 已过期 —— 下达时机器不在线 / 待执行)
- 下发成功的提示补上「60 秒内没人点确认就按取消处理」

文档
- AGENTS.md §5 第 69 条记下这两条,并写明排查同类反馈的顺序:**先看那台机器的客户端版本是否 ≥ 1.3.0**
  (远控命令的处理 `client-agent.ts` 是 1.3.0 才有的,旧客户端连心跳都不发)
- docs/reference.md 4.15 的表格补上「二次确认」的准确口径与新增的「上次指令」列

验收:`lint` / `typecheck` / `check:icons` / `docs:check` 全绿;
后端端到端 **247/247**、联动链路 **44/44**、插件静态契约 **76/76**、
Web 真实点击 **34/34**、客户端冒烟 **90/90**;
远控链路另用模拟客户端跑通,并确认新增的「上次指令」在待执行与已回执两种状态下都显示正确。
2026-10-11 17:00:11 +08:00

13 KiB
Raw Blame History

title, description
title description
架构总览 目录结构、技术栈、模块化设计与几条贯穿全局的工程约定。

架构总览

技术栈

层 选型 版本(package.json 实际声明)
包管理 pnpm workspace 11.8.0
语言 TypeScript 5.9.3(typescript-eslint 要求 < 6.1,因此未用 7.x)
后端 Node.js + Express ≥ 20.19 / 5.2
ORM Prisma + driver adapter 7.10(+ @prisma/adapter-libsql,切 MySQL 用 -mariadb)
实时 Socket.IO 4.8
认证 jsonwebtoken + bcryptjs 9.0 / 3.0
校验 zod 4.6
Web 端 Vue + Vite + Element Plus + Pinia + Vue Router + Axios 3.5 / 8.3 / 2.14 / 4.0 / 5.3 / 1.20
客户端 Electron + Vue + Element Plus + Pinia + Socket.IO Client + IndexedDB 44.3 / 3.5 / 2.14 / 4.0 / 4.8
插件 .NET 8 / C# + Avalonia(ClassIsland 插件 API 2.0) net8.0
打包 electron-builder(nsis / portable)+ NSIS 26.15
文档站 docfx 2.81

目录结构

class-helper/
├── package.json                 # 根脚本(dev / build / db:* / verify:* / dist:* / docs:* / icons)
├── pnpm-workspace.yaml          # workspace 定义 + allowBuilds(pnpm 11 依赖构建白名单)
├── tsconfig.base.json           # 共享 TS 基础配置(ESM + NodeNext + strict)
├── eslint.config.mjs            # ESLint 扁平配置(TS + Vue)
├── .prettierrc.json             # 单引号 / 110 列 / LF / 尾逗号
├── build/classhelper.{png,ico}  # **图标源**(设计导出、手工放入);pnpm icons 从它派生其余全部图标
├── scripts/
│   ├── use-database.mjs         # SQLite ⇄ MySQL provider 切换助手
│   ├── generate-icons.mjs       # 启动 Electron 跑 icons/render.cjs(= pnpm icons)
│   ├── icons/render.cjs         # 把 classhelper.png 重采样成各端图标(CommonJS)
│   ├── version.mjs              # 版本号单一来源的读写与一致性校验(= pnpm version:*)
│   ├── dist-server.mjs          # 服务端 + Web 管理端打包(组装运行时 → NSIS 安装程序)
│   ├── verify-packaged.mjs      # 对打包后 / 已安装的客户端 EXE 复跑冒烟
│   ├── lib/                     # electron-env / node-runtime / smoke-class
│   ├── nsis/server-installer.nsi# 安装程序脚本模板(版本号由 dist-server 注入)
│   └── ui-smoke/                # Web 管理端 UI 真实点击回归(Electron 驱动)
├── deploy/                      # 生产部署:Dockerfile / docker-compose{,.sqlite}.yml / nginx.conf
│   ├── install.sh               # Linux 一键安装器(TUI;native/docker 双形态;自带 --check 体检)
│   ├── classhelper              # 运维命令本体(软链 /usr/local/bin/classhelper)
│   ├── verify-linux.sh          # Linux 安装/改密/备份/升级/回滚验收(自统计 N/N 项)
│   ├── tools/admin-cli.mjs      # 离线账号工具(改密、列账号、SQLite 一致性快照)
│   ├── classhelper.service      # systemd 单元模板
│   └── package.runtime.json     # 运行时依赖清单(安装包 / 容器 / Linux 部署共用)
├── docs/                        # **文档站**(docfx 项目,发布到 GitHub Pages)
│   ├── docfx.json               # docfx 配置
│   ├── toc.yml                  # 顶栏导航;各子目录各有一份 toc.yml 作为侧栏
│   ├── index.md                 # 首页
│   ├── get-started/ app/ management/ dev/   # 四个章节
│   ├── templates/classhelper/   # docfx 模板覆盖(_master.tmpl + public/main.css + 图标)
│   ├── images/                  # 展示图:文档站 / README / 官网共用的同一份(1× 逻辑像素)
│   ├── screenshots/             # 回归测试留档的截图(island / client / web-mobile / classisland),页面不展示
│   ├── build.mjs                # 构建入口(注入版本号与 404 的 base)
│   ├── serve.mjs                # 本地预览(:5181)
│   └── _site/                   # 构建产物(gitignore)
├── website/                     # 产品官网(纯静态,无构建;node website/serve.mjs 本地预览)
│   └── assets/{styles.css,motion.js,main.js,icon.png,shots/}
└── packages/
    ├── shared/                  # 三端共享:类型契约、常量、权限矩阵、动效令牌、工具函数
    │   └── src/{types,constants,permissions,motion,island-layout,utils,index}.ts
    ├── server/                  # 后端服务
    │   ├── prisma/schema.prisma # 数据模型(24 个 model,只有一份,切库改 provider)
    │   ├── prisma/migrations/   # 迁移历史(10 个)
    │   ├── prisma/seed.ts       # 种子数据(会清空业务表后重建演示数据)
    │   ├── scripts/             # verify-e2e.mjs / verify-classisland.mjs / apply-column-migrations.cjs
    │   └── src/
    │       ├── app.ts           # Express 装配(探针 + 限流 + 模块挂载 + 静态托管 + 兜底)
    │       ├── index.ts         # 启动入口(自检 + HTTP + Socket.IO + 优雅退出)
    │       ├── config/env.ts    # 环境变量校验(zod)+ 生产配置自检
    │       ├── lib/             # access(RBAC) / snapshot(全库 JSON 快照) / db / db-bootstrap /
    │       │                    #   session / web-static / http / jwt / logger / mappers / version …
    │       ├── tools/apply-snapshot.ts  # 跨库迁移的子进程入口
    │       ├── middleware/      # auth / error / validate / security(helmet + 限流 + 耗时日志)
    │       ├── realtime/        # socket.ts(房间)+ bus.ts(事件总线)
    │       └── modules/         # 15 个功能模块 + registry.ts(模块注册表)
    ├── classisland-plugin/      # ClassIsland 联动插件(.NET 8 / C#,独立于 pnpm workspace)
    │   ├── manifest.yml
    │   ├── src/Plugin.cs
    │   ├── src/Services/        # BridgeService / ScheduleMapper / ClassPlanWriter /
    │   │                        #   ClassHelperNotificationProvider
    │   ├── src/Views/           # BridgeSettingsPage.axaml(Avalonia 设置页)
    │   └── src/Interop/ClassHelperClient.cs   # HTTP 客户端 + 逐字段对齐服务端 zod 的 DTO
    ├── web-admin/               # Web 管理端(Vue 3 + Vite + Element Plus + PWA)
    │   └── src/{api,stores,router,layouts,views,components,composables,styles}
    └── desktop-client/          # 桌面客户端(Electron + Vue 3)
        ├── electron-builder.yml
        ├── scripts/{build-main,dev,smoke,dist-win,check-bundle-secrets}.mjs
        └── src/
            ├── main/            # 主进程:窗口 / 单实例 / 配置持久化 / 托盘 / 灵动岛 / 冒烟
            ├── preload/         # contextBridge 安全桥(index.ts + island.ts)
            ├── island/          # 灵动岛渲染进程(独立透明置顶窗口)
            ├── types/desktop.d.ts   # 主进程 ↔ 渲染进程契约
            └── renderer/        # 渲染进程:api / stores / cache / router / views / island

模块化设计

后端每个功能是一个独立目录,统一契约:

// packages/server/src/modules/registry.ts(当前 21 个模块)
export const apiModules: ApiModule[] = [
  authModule,
  aiModule, // 大模型助手(只解析不落库,见 docs/dev/api.md)
  classesModule,
  coursesModule,
  schedulesModule,
  homeworksModule,
  notificationsModule,
  callsModule,
  importsModule,
  integrationsModule,
  gradesModule,
  studentsModule,
  teachersModule,
  archivesModule,
  termModule,
  dashboardModule,
  databaseModule, // 数据库管理(仅管理员)
  updateModule, // 更新检查 + 客户端安装包缓存与统一下发
  permissionsModule, // 权限组管理
  clientsModule, // 客户端设备心跳 / 远控命令
  systemModule, // 开发者模式 / 开机自启 / 运行诊断
].filter((module) => module.enabled !== false);
  • 新增功能:新建 modules/<name>/(*.schemas.ts 校验、*.service.ts 业务、*.module.ts 路由), 在 registry 里加一行即可,app.ts 不用改;
  • 下线功能:删掉那一行,或把 enabled 置为 false,接口立即从路由表消失;
  • 实时推送解耦:业务模块只调 realtime/bus.ts 的 emitToClass/emitToUser,不直接依赖 Socket.IO 实例;
  • 共享契约:所有 DTO 与类型集中在 @classhelper/shared(改完要 pnpm build:shared),三端共用。

几条贯穿全局的硬约定

这些都是长期演进中固化下来的,改代码前值得先看(完整版在 AGENTS.md):

约定 为什么
统一响应与校验 用 lib/http.ts 的 sendOk/sendCreated,用 middleware/validate.ts 挂 zod,错误交给 middleware/error.ts
功能级准入看权限键 路由挂 requirePermission('…') / requireAnyPermission(…),角色只是默认模板(ROLE_DEFAULT_PERMISSIONS)—— 见 docs/dev/permissions.md
权限判在服务层 权限组只管功能级;班级维度与科目维度仍用 lib/access.ts 的断言(assertCanManageSubjectContent 等),权限组不可覆盖 —— 前端隐藏 ≠ 权限
周一为新一周的第一天 周次口径唯一:resolveCurrentWeek(TERM_START_DATE, now, MAX_TERM_WEEK)
作业按 assignDate 归类 用 createdAt(UTC)会在晚上把作业算到第二天
新增实时事件要改三处 服务端 bus/socket、Web 端订阅、客户端订阅(灵动岛也依赖)
跨桥接只传纯数据 传 Vue 的 ref.value 会在进入 preload 之前就抛 could not be cloned
桌面端产物里不得有凭据 asar 可直接解包;构建有 check-bundle-secrets 门禁
图标只有一个图形源 别改 scripts/icons/ 的派生逻辑,换图标就换 build/classhelper.{png,ico}
前端不写死颜色 深色主题靠语义令牌,写死的颜色在深色下会露馅

本地开发

pnpm install
cp packages/server/.env.example packages/server/.env   # 至少改 JWT_SECRET
pnpm db:generate && pnpm --filter @classhelper/server db:deploy
pnpm build:shared        # 必须先构建 shared,否则后续全部 ERR_MODULE_NOT_FOUND
pnpm db:seed             # 演示数据(会清空业务表)
pnpm dev                 # shared watch + 后端 4000 + Web 端 5173

常用命令与端口见安装服务端;验收脚本见验收与测试。