Skip to content
ClassIntraPublic

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Cluss

引导式 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:9003
cd client
npm install
npm run build        # 产物输出到 client/dist,由服务端直接托管

不想分两步的话,在仓库根目录一键启动即可(Windows 可直接双击 start.bat):
脚本会自动补装缺失/过期的依赖、前端源码比 client/dist 新时才重新构建,
起服后停在日志上(--open 让它顺手打开浏览器,--force-build 强制重建)。

npm start            # 根目录执行;等同 start.bat / start.sh

首次启动会:

  1. 自动建表(迁移 v1~v16 全量应用;已有旧库则只补跑缺失的版本)
  2. 写入 .env 里配置的默认 AI 模型
  3. 用 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。

多班部署(1 个中继 + N 个节点)

中枢上云、模型统一下发、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 秒上报用量。管理台在中继上,节点上不需要登录管理台。

Cluss Cloud 公共中枢(多校)

不想自己维护中继的学校,可以直接接入公共中枢(一台独立的公共服务端, 只跑服务端 + 托管页面,端口 10003):

  1. 在中枢网页打开「多班部署 / Cloud 服务」页,注册学校;
  2. 拿到「学校编号 + 管理员编号 + 接入密钥」(密钥只显示一次);
  3. 把注册页给出的 .env 模板填到班级节点(多一个 CLUSS_ORG_ID),重启即接入;
  4. 用管理员编号登录中枢网页,就能看到本校各班节点的心跳、用量与额度, 并给本校学生充值 —— 各校数据严格隔离,同步接口按校过滤。

自托管不受影响:中继不配 CLUSS_ORG_ID 走原有单校共享密钥;云端节点带 CLUSS_ORG_ID 走校级密钥(同一份代码,两种形态,见 cluss-cloud-design.md §7.1)。

与 ClassIntra 联动(可选)

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 这边无需任何配置;但必须满足两条, 否则反代过去也是白屏:

  1. 前端资源是相对路径(vite.config.js 的 base: './')—— 写死 / 的话, 页面会去要 /assets/...,那是宿主自己的路径;
  2. 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 是原生模块,装不上时服务会降级为「不压缩图片」,不影响其余功能。

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages