引导式 AI 解题应用。可独立部署,也可与 ClassIntra 深度联动。
面向学生:不直接给答案,先分层给提示、检查你的解法,让你真正想通。
面向管理员:学生线下转账后,你在后台给他开通额度,余额用完即停。
同一份代码,靠 .env 的 CLUSS_MODE 切换三种形态:
| 模式 | 用途 | 数据库 | 额度来源 |
|---|---|---|---|
standalone |
单机独立部署(无 ClassIntra) | 自有 cluss.db |
本机管理员直接分配 |
node |
班级节点(每班一台) | 自有 cluss.db |
从中继拉取,本地扣费 |
relay |
中继权威节点(管理员操作台) | 自有 cluss.db |
权威账本,管理台在此 |
现实约束:学生只能访问本班机器,且充值统一由一个人管理。这两条决定了架构:
┌──────────────────────────────────┐
│ 中继权威节点(relay) │
│ 权威额度账本 + 管理台 │
│ 唯一充值入口 │
└───────────────┬──────────────────┘
│ 单向下发(出站拉取)
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
1班 Cluss 节点 2班 Cluss 节点 3班 Cluss 节点
本地扣费 + 本地闸门 本地扣费 + 本地闸门 本地扣费 + 本地闸门
│ │ │
└────── 用量汇总上报(对账用)──────────────┘
关键取舍:
- 扣费本地化:学生用 AI 时不依赖网络往返,断网也能用;也避免跨实例并发扣费的分布式锁问题。
- 充值单点化:只有中继能分配额度,天然唯一权威,不需要多写冲突消解。
- 额度是「分配」不是「共享账本」:可用余额 = 中继下发的分配额 + 同步容错 − 本地消耗。
余额不落库(落库必然与流水漂移),每次由流水聚合推导。 - 同步容错:
CLUSS_SYNC_TOLERANCE_YUAN(默认 1 元)防止「学生刚在别班充值、
本班还没同步到」被误拒。宁可短暂放宽,也不误伤已付款的学生。
| 场景 | 行为 |
|---|---|
| 断网 + 学生用 AI | 正常(本地扣费不依赖网络) |
| 断网 + 管理员充值 | 管理台在中继上,此时本来也访问不到,无一致性问题 |
| 节点拉不到新额度 | 沿用上次同步的分配额 + 容错,恢复后按版本号自动对齐 |
| 节点上报失败 | 用量留在本地 synced=0,下个周期继续重试,不丢账 |
cd server
cp .env.example .env
# 编辑 .env:至少填 JWT_SECRET 与 ADMIN_PASSWORD
npm install
npm start # 默认 http://localhost:9003cd client
npm install
npm run build # 产物输出到 client/dist,由服务端直接托管不想分两步的话,在仓库根目录一键启动即可(Windows 可直接双击 start.bat):
脚本会自动补装缺失/过期的依赖、前端源码比 client/dist 新时才重新构建,
起服后停在日志上(--open 让它顺手打开浏览器,--force-build 强制重建)。
npm start # 根目录执行;等同 start.bat / start.sh首次启动会:
- 自动建表(迁移 v1~v16 全量应用;已有旧库则只补跑缺失的版本)
- 写入
.env里配置的默认 AI 模型 - 用
ADMIN_USER_ID/ADMIN_PASSWORD创建管理员账号(已存在则不覆盖密码)
别忘了在管理台给模型填上 API Key,否则学生发消息会收到「AI 服务未配置完成」。
一键启动只是入口,部署与运维需要的配套全部已经落地,不是「之后再说」的规划:
| 配套 | 现状 |
|---|---|
| 一键启动 | 依赖与构建按需判断、端口占用防重复起服(上文) |
| 幂等迁移 | 起服自动补跑(当前 v1~v16),旧库新库都能直接起 |
| 探活 | GET /api/health 无需鉴权,返回版本 / 模式 / 同步状态,可直接接监控 |
| 优雅退出 | SIGTERM / SIGINT 先关 HTTP 再退出,可交给 systemd / pm2 托管 |
| 备份与清理 | server/scripts/backup.js 在线备份(默认保留 7 份);cleanup-orphans.js 云盘孤儿清理(默认 dry-run) |
| 验证套件 | verify-v2 / v3 端到端断言 + profile / points / pricing / ui-history / cloud-img 专项;只清理自己造的测试数据,可在真实库上反复跑 |
| 测试题库 | qa/ 目录:209 道高中题(9 学科 × 3 难度 × 13 题型),每题带三级引导提示,node qa/check-bank.cjs qa/question-bank-v2.json 一键过质量门禁 |
| 演示环境 | prepare-demo.js 一键造演示数据,reset-demo-data.js 清空业务数据 |
| 安全默认 | JWT 密钥缺失直接拒绝启动;登录 / 注册接口限流;bcrypt 存密码 |
生产环境的 .env 硬性项、升级流程、备份姿势、接入 AI 上游与发版清单,
见 docs/deploy.md。
中枢上云、模型统一下发、Cluss Cloud 公共中枢的完整设计见
docs/cluss-cloud-design.md。
中继节点(管理员操作的机器):
CLUSS_MODE=relay
PORT=9003
CLUSS_INSTANCE_ID=relay
JWT_SECRET=<32位以上随机串>
CLUSS_SHARED_SECRET=<与各节点一致>
ADMIN_PASSWORD=<管理台密码>班级节点(每班一台):
CLUSS_MODE=node
PORT=9003
CLUSS_INSTANCE_ID=class-01 # 每班必须唯一,用于对账区分
JWT_SECRET=<与中继一致>
CLUSS_RELAY_URL=http://<中继地址>:9003
CLUSS_SHARED_SECRET=<与中继一致>
# 接入 Cluss Cloud 公共中枢时额外填学校编号(自托管中继留空)
# CLUSS_ORG_ID=<学校编号>节点会自动:启动立即同步一次 → 之后每 CLUSS_SYNC_INTERVAL 秒拉额度 →
每 CLUSS_REPORT_INTERVAL 秒上报用量。管理台在中继上,节点上不需要登录管理台。
不想自己维护中继的学校,可以直接接入公共中枢(一台独立的公共服务端, 只跑服务端 + 托管页面,端口 10003):
- 在中枢网页打开「多班部署 / Cloud 服务」页,注册学校;
- 拿到「学校编号 + 管理员编号 + 接入密钥」(密钥只显示一次);
- 把注册页给出的 .env 模板填到班级节点(多一个
CLUSS_ORG_ID),重启即接入; - 用管理员编号登录中枢网页,就能看到本校各班节点的心跳、用量与额度, 并给本校学生充值 —— 各校数据严格隔离,同步接口按校过滤。
自托管不受影响:中继不配 CLUSS_ORG_ID 走原有单校共享密钥;云端节点带
CLUSS_ORG_ID 走校级密钥(同一份代码,两种形态,见 cluss-cloud-design.md §7.1)。
ClassIntra 侧:.env 里确保配置了 RELAY_SECRET,然后重启:
/api/cluss-bridge/ping 与 /verify 即生效(未配密钥时返回 503,不影响现有功能)。
Cluss 侧:
CI_BRIDGE_URL=http://<ClassIntra 地址>:9001
CI_BRIDGE_SECRET=<与 ClassIntra 的 RELAY_SECRET 一致>
CI_JWT_SECRET=<与 ClassIntra 的 JWT_SECRET 一致> # 填了才支持 token 互认免登ClassIntra 开 HTTPS 之后:浏览器把 HTTPS 页面里的 HTTP 子资源一律按混合内容
拦截,iframe 内嵌 http://<CI>:9003 会直接失败。ClassIntra 侧用同源反代解决
(/cluss-app/* → 127.0.0.1:9003),Cluss 这边无需任何配置;但必须满足两条,
否则反代过去也是白屏:
- 前端资源是相对路径(
vite.config.js的base: './')—— 写死/的话, 页面会去要/assets/...,那是宿主自己的路径; - API 前缀跟着页面路径走(
client/src/utils/base-path.js)—— 裸写/api会打到宿主的接口上,表现为「页面起来了但一发请求就是 CI 的数据 / 401」。
路由用 hash 模式,因此同一份构建产物同时支持根部署(http://<host>:9003/)与
子路径挂载(https://<host>:<HTTPS_PORT>/cluss-app/,CI 侧默认 HTTPS_PORT=9443,
因为机器的 443 归别的服务用),不需要构建两份。新增的请求一律经
apiUrl() 拼接,不要再出现 /api 字面量(verify-no-magic-api.cjs 会把关)。
学生端(七个入口)
| 入口 | 能做什么 |
|---|---|
| 对话 | 学习模式(引导式 / 直接给答案)与日常模式;可附云盘里的图片与文本文件;一键复制对话全文(可粘贴到笔记软件);会话可重命名、按标题过滤,消息里的图片可放大查看;可引用笔记作为上下文 |
| 学习 | 拍题解答(单题 / 多题 / 跨页题)与作业批改;支持现拍(直接调系统相机);可引用笔记作为上下文 |
| 作文 | 生成范文 / 批改润色 / 列提纲;可选语言、文体、风格、字数;支持看图作文与手写稿批改;可引用笔记作为上下文 |
| 记录 | 解题与批改的历史记录(类型徽章 + 题图缩略图 + 摘要),点击回看完整结果;按标题或题目内容搜索 |
| 笔记 | Markdown 笔记(编辑 / 分栏 / 预览三模式),支持标签、置顶、标题与标签搜索;可插云盘图片、导入云盘 .md/.txt,保存后在云盘可下载;AI 辅助(整理 / 续写 / 提炼);AI 的跨会话记忆在这里查看与删除 |
| 错题本 | 批改出的错题自动收录,可筛选、编辑、标记已掌握、批量操作、导出 CSV;自测模式(先藏答案,想好了再核对) |
| 云盘 | 上传图片与文本、预览、下载、删除;每用户默认 200MB 配额 |
「我的消耗」在右上角用户菜单里(低频页,不占顶栏)。
学生端所有正文结果共用一套 Markdown 渲染:数学公式(KaTeX)、 **流程图 / 思维导图 / 甘特图 / 时序图(mermaid,按需加载,不占首屏)**都直接画出来, 正文里的图表点一下可全屏放大。怎么验证这一层见 docs/verification.md 第 1.6 项。
管理端(四个标签页,/#/admin)
| 标签页 | 能做什么 |
|---|---|
| 学生额度 | 总览指标、学生列表(搜索 / 按班级筛选)、单人充值、批量充值、充值流水、台账导出 |
| 模型与单价 | 增删改任意 OpenAI 兼容模型、设默认、启用/禁用、单价与免费标记、连通性测试 |
| 视觉配置 | 看图总开关、转述模型选择、单次图片数上限、压缩参数 |
| 用量统计 | 全站用量(按学生/模型/类型/天/实例分组)、调用明细、CSV 导出 |
充值流程与管理台日常操作见 docs/deploy.md;
计费为什么用「积分」、人格与话题边界、看图与错题收录的机制、界面心理设计约定,
见 docs/product.md;界面实现与动效约定见 docs/ui.md。
- 流式对话只做「一问一答」,不含上下文摘要压缩。上下文按 token 预算从最新往旧裁剪,
所以很长的会话里最早的内容会被丢掉。若需要「记得前文」,需另做摘要机制。 - 不支持联网搜索(需要的话可参考 ClassIntra 的 ai-chat 移植)。
- 拍题与批改必须有图片:只传文本文件会被明确拒绝,而不是让模型硬答。
- 图片识别依赖上游模型能力,识别错字、公式符号的情况真实存在;
提示词已要求模型对看不清的字符标□(看不清),但不能保证它每次都遵守。 - 节点上报的是明细而非聚合值,学生规模很大时中继上的
cluss_usage_log会持续增长;
建议定期归档(created_at早于某时间的行)。 - 云盘每实例独立、不做跨节点同步,因此磁盘会随拍题量增长。已加每用户配额
(默认 200MB,cluss_meta.cloud_quota_bytes可调);被删引用归零的文件是软删,
物理文件保留在磁盘上,磁盘增长明显时跑node scripts/cleanup-orphans.js清理
(默认 dry-run,--delete执行)。 - 软删的云盘文件若同一 hash 再次上传,会复用原物理文件(内容寻址的必然结果),
这是预期行为,不占额外空间。
深入内容按读者分册,都在 docs/:
| 分册 | 内容 |
|---|---|
| deploy.md | 生产运行要点、接入 AI 上游、版本号与发布、充值流程 |
| product.md | 账号体系、计费与额度、人格与话题边界、看图与错题机制、界面心理约定 |
| ui.md | 界面实现与动效:触屏手感、低端设备性能取舍、深色模式、平板横竖屏 |
| architecture.md | 目录结构、编排器、技术栈与安全基线 |
| verification.md | 端到端验证套件、mock 上游与断言清单 |
| performance.md | 性能基线、测量方法与门槛(npm run perf:gate) |
| cluss-cloud-design.md | 中枢上云与 Cluss Cloud 公共中枢设计 |
本项目为原创实现。设计与实现过程中参考了同仓库体系下的 ClassIntra 项目(同一作者)
的若干工程约定,均为思路与信息结构层面的参考,未复制其代码或素材:
| 参考内容 | 来源 | 说明 |
|---|---|---|
学号 YYNNCC 编码规则 |
ClassIntra 用户体系约定 | 为便于两系统账号对应而沿用同一编码口径 |
| 计费金额单位(微元 / 千 token) | ClassIntra migrations/013_ai_billing.js |
沿用同一口径,便于对账时无需换算 |
| OpenAI 兼容端点归一化规则 | ClassIntra server/src/services/ai-chat.js |
端点补全策略(/v1 自动补 /chat/completions) |
| 路由与登录候选排序思路(学号精确匹配优先) | ClassIntra server/src/routes/auth.js |
应对同名多候选时的判定顺序 |
| 玻璃拟态视觉方向 | 用户提供的设计参考图 | 仅参考视觉风格取向,未复制任何素材 |
| 云盘存储模型(内容寻址 sha256 + 实体/引用两表分离) | ClassIntra server/src/services/cloud-store.js、apps/resource/backend/cloud-routes.js |
仅参考数据模型与去重思路。本项目裁掉了文件夹/分享/跨节点同步,并按自身 foreign_keys=ON 重写了删除顺序 |
| 图片视觉链路(native 直传 / relay 转述回退) | ClassIntra apps/ai-chat/backend/routes.js |
参考三模式判定与「统一转述模型」的设计动机。本项目重写实现:串行转述、独立记账 + trace_id 串联、图片计入 token 预算 |
前端依赖:Vue 2.7、Vue Router、Vuex(MIT);marked(MIT);KaTeX(MIT);DOMPurify(Apache-2.0)。
服务端依赖:Express、better-sqlite3、jsonwebtoken、bcryptjs、axios、compression、cookie-parser、morgan、dotenv、multer、sharp。
sharp 是原生模块,装不上时服务会降级为「不压缩图片」,不影响其余功能。