班级、任务和学生档案按教学班归档;没有名单也可以直接从卷面姓名开始。
Important
当前为 0.1.0 首个公开版本。核心桌面应用已经完成真实上传、视觉模型批改和本地归档链路验证;AI 评分仍须由老师复核,重要数据请保留独立备份。
作文批改台处理的是一个很具体的课堂工作流:
- 老师建立班级并设置本次作文;
- 手机固定在卷面上方,连续拍摄整班作文;
- 电脑本地接收图片并调用老师自己的多模态模型;
- 模型返回姓名、转录、评分和反馈;
- 老师确认身份与结果,随后归档、统计或导出。
它不是云端教务 SaaS,也没有项目方托管后台。项目不要求学生注册账号,不上传老师的班级数据库,不通过“模型自报置信度”掩盖概率模型的不确定性,也不把 AI 结果当成无需复核的最终判定。
| 设计选择 | 当前做法 |
|---|---|
| 名单 | 可选;无名单时可从卷面姓名建档 |
| 模型 | 老师自行提供 OpenAI Chat Completions 兼容端点 |
| 图片与成绩 | 保存在老师电脑 |
| 手机收卷 | 四小时临时 HTTPS 入口 |
| 身份不确定 | 进入人工确认,不编造置信度 |
| 统计 | 只使用真实正式结果,不填充示例数字 |
| 账号与遥测 | 不提供,也不需要 |
flowchart LR
A["建立班级<br/>名单可选"] --> B["设置任务<br/>15 / 25 分模板"]
B --> C["手机连续拍摄<br/>一份一提交"]
C --> D["本机接收<br/>校验并续传"]
D --> E["视觉模型<br/>转录 · 识名 · 评分"]
E --> F["老师复核<br/>身份与结果"]
F --> G["学生档案<br/>班级分析 · CSV"]
classDef paper fill:#fff8d6,stroke:#2a2a28,stroke-width:2px,color:#2a2a28;
classDef local fill:#eaf6ed,stroke:#2f7d4a,stroke-width:2px,color:#214e31;
class A,B,C paper;
class D,E,F,G local;
名单不是开批门槛。卷面姓名清楚时,模型可以直接建立学生档案;导入 Excel / CSV 后,则能进一步处理同名、学号和按名单顺序收卷。
以下图片全部来自当前 Tauri 应用实机运行,不是设计稿或静态原型。
|
|
| 任务设置:题型模板、题目材料、评分约定和收卷入口在同一条路径上。 | 手机上传:选择正在收卷的任务,生成或暂停四小时临时入口。 |
|
|
| 班级总览:区分已收、批改完成、待确认和正式档案,不用半成品成绩污染统计。 | 作文队列:上传、失败、完成状态可见,并提供身份调整、重试、撤回和详情操作。 |
本机设置:姓名归档策略、模型并发、上传端口、更新与数据清理集中管理。
- 管理教学班和临时分组,支持 Excel / CSV 名单预览、列映射与导入。
- 提供读后续写
25分、应用文15分两套任务模板;作文题目和附加要求均可留空。 - 手机端支持连续拍摄、单卷多页、调序、重拍、撤回刚才一份和离线队列。
- 快门点击时同步定格当前视频帧,JPEG 编码不会继续读取已经移动的相机画面。
- 图片自动纠正方向、移除 EXIF、压缩为最长边
3000px、质量0.9的 JPEG。 - 采用
1 MiB分片、SHA-256 校验和服务端偏移续传,单份作文页面最多三路并发。
- 对接 OpenAI Chat Completions 兼容端点,可分别配置主模型、副模型和备用视觉模型。
- 真实检查端点连通性与图片输入能力,而不是只验证配置格式。
- 结构化输出覆盖姓名、学号、转录文本、总分、分项评分、证据、反馈和订正建议。
- 批改队列支持并发、失败重试和应用重启恢复;不会向模型发送人为设定的输出 token 上限。
- 无名单时可按卷面姓名自动建档;无法可靠匹配时留给老师确认。
- 老师可以修订批改结果、确认学生身份、重试失败任务或撤回错误作文。
- 按学生保留历次正式结果;有两次以上结果后才显示成绩趋势。
- 提供班级总览、匿名班级分析、匿名班际比较以及 CSV 成绩导出。
- 学生详情包含原图、转录、能力分项、具体修改和可复制的批改包。
- 清理任务或班级前先计算影响范围,并通过暂存机制同步处理数据库记录与本地图片。
安装包由 GitHub Releases 提供,请按电脑系统与架构下载。当前没有付费 Apple Developer ID 或 Windows Authenticode 证书,因此 macOS 和 Windows 首次打开时可能显示来源提示;请只使用本仓库发布页的附件,并可用同页 SHA256SUMS.txt 核对文件完整性。
进入“API / 档案”,至少配置一个支持图片输入的主模型:
Base URL填到 OpenAI 兼容 API 根路径,例如https://example.com/v1;模型 ID使用服务商要求的完整名称;API Key保存后只进入操作系统凭据库,界面不会再次回显;- 点击“保存并测试”,应用会发送一张可解码的小型 PNG 验证真实视觉能力。
如果主模型只能处理文本,可以启用备用 VLM 先完成图片转录,再交给主模型评分。副模型用于主链路失败后的自动接管。
班级名称是唯一必填项。名单有三种使用方式:
- 卷面自动识名:不导入名单,模型识别姓名后自动建档;
- Excel / CSV 名单:映射姓名、学号和备注列,提高同名匹配稳定性;
- 机序对应名单:卷面不写姓名时,按上传顺序对应名单顺序。
选择读后续写或应用文模板,填写任务名称。题目原文、题目截图、任务系列、关注点和模型备注均为可选项。保存草稿后可以稍后继续;点击“开始收取作文”才会进入收卷状态。
进入“手机上传”,为当前任务生成二维码。应用会:
- 确认本地 Axum 服务正在监听;
- 启动内置
cloudflared; - 等待 Quick Tunnel 确实能够访问本机服务;
- 生成含随机令牌的四小时 HTTPS 地址。
手机浏览器可以选择单份多页或单页连拍。连拍模式保持摄像头常开,点击快门立即定格当前帧,然后后台压缩和上传;老师可以继续对准下一份作文。误拍时可以在手机或电脑撤回。
“作文批改”会展示每份作文的上传、排队、批改、失败与身份状态。只有批改完成且身份确认后的结果才进入正式统计。老师可以打开详情修订结果,旧版本仍留在本地。
“暂停手机入口”只关闭二维码和隧道,不结束任务;“完成收卷”才会将任务设为已结束。结束后的任务可以继续查看和导出,但不能重新上传。
| 角色 | 是否必需 | 输入能力 | 调用时机 |
|---|---|---|---|
| 主模型 | 必需 | 图片 + 文本,或配合备用 VLM 使用纯文本 | 正常批改主链路 |
| 副模型 | 可选 | 与主模型任务相同 | 主模型失败后自动接管 |
| 备用 VLM | 可选 | 图片 + 文本 | 只负责转录,再交给文本评分模型 |
端点需要兼容 POST /chat/completions,响应正文至少包含 choices[0].message.content。批改请求使用低温度和 JSON Object 响应格式;鉴权失败不会重试,限流、服务端错误和网络错误最多重试三次。
模型返回结果会经过 Rust 结构反序列化和业务校验。下面是经过缩减的 schemaVersion=1.0 示例:
{
"schemaVersion": "1.0",
"identity": {
"studentName": "陈一诺",
"studentId": "20240101"
},
"transcription": {
"fullText": "The old man looked out of the window.",
"pages": ["The old man looked out of the window."],
"uncertainSpans": []
},
"grade": {
"total": 22,
"maxScore": 25,
"level": "第二档偏上",
"dimensions": [
{
"key": "language",
"label": "语言",
"score": 4,
"maxScore": 5,
"evidence": ["动作描写清楚"]
}
]
},
"feedback": {
"summary": "情节完整,结尾回扣不足。",
"strengths": ["动作链条清楚"],
"fixes": [
{
"location": "结尾段",
"original": "He smiled.",
"replacement": "He smiled at the warmth returning home.",
"reason": "回扣开头意象",
"category": "cohesion"
}
],
"modelText": "He turned back to the window.",
"outline": "冲突—动作—回扣"
},
"classTags": ["结尾回扣"]
}以下结果会被拒绝而不是直接入档:
schemaVersion不是1.0;- 模型返回满分与任务满分不一致;
- 总分或分项分数越界;
- 没有作文转录正文;
- 没有任何可执行修改建议;
- 返回内容不是有效 JSON。
sequenceDiagram
participant P as 手机浏览器
participant C as Quick Tunnel
participant A as Axum 本地服务
participant S as SQLite / 本地文件
participant W as 批改队列
P->>C: GET /u/{临时令牌}
C->>A: 获取零安装收卷页
P->>A: 创建一份作文
loop 每一页
P->>A: 声明页码、大小与 SHA-256
P->>A: PUT 1 MiB 分片
P->>A: 查询服务端已确认偏移
A->>S: .part 临时文件
end
P->>A: 完成这份作文
A->>S: 校验并原子移动到 images/
A->>W: 加入批改队列
| 约束 | 当前值 |
|---|---|
| 会话有效期 | 4 小时 |
| 单份作文页数 | 1–8 页 |
| 单页处理后大小 | 最大 12 MiB |
| 单会话总流量 | 最大 2 GiB |
| 上传分片 | 1 MiB |
| 单份页面并发 | 最多 3 |
| 请求限流 | 每个令牌每分钟 600 次 |
| 完整性 | 每页 SHA-256 |
| 续传依据 | 服务端已确认字节偏移 |
手机端会把待传作文保存在 IndexedDB 队列中;页面刷新或短暂断网后可以继续处理。完成前的页面存放在 uploads/*.part,只有大小与哈希都正确才会原子移动到正式图片目录。
flowchart TB
Phone["老师手机<br/>浏览器相机"]
Tunnel["Cloudflare Quick Tunnel<br/>临时 HTTPS 传输"]
Model["老师配置的模型端点<br/>OpenAI 兼容 API"]
subgraph PC["老师电脑 · 作文批改台"]
UI["Tauri / React 界面"]
Upload["Axum 本地上传服务<br/>127.0.0.1:8787"]
Worker["Rust 批改队列"]
Store[("SQLite + 本地图片")]
Vault["系统凭据库<br/>API Key"]
UI <--> Store
Upload --> Store
Store <--> Worker
Vault --> Worker
end
Phone -->|作文图片分片| Tunnel
Tunnel --> Upload
Worker -->|图片与任务要求| Model
Model -->|结构化批改结果| Worker
classDef outside fill:#fff3df,stroke:#bf6435,stroke-width:2px,color:#66351f;
classDef inside fill:#edf7ef,stroke:#2f7d4a,stroke-width:2px,color:#214e31;
class Phone,Tunnel,Model outside;
class UI,Upload,Worker,Store,Vault inside;
| 数据 | 保存位置 / 传输边界 |
|---|---|
| 班级、名单、任务、成绩 | 老师电脑的 SQLite 数据库 |
| 原始作文图片 | 老师电脑的系统应用数据目录 |
| API Key | macOS Keychain、Windows Credential Manager 或 Linux Secret Service |
| 手机上传流量 | 经 Cloudflare Quick Tunnel 转发到老师电脑 |
| 模型请求 | 只发送给老师自行配置的模型端点 |
| 班级与班际分析 | 只向模型发送匿名聚合统计 |
| 账号、遥测、云同步 | 不存在 |
应用数据目录内部结构如下:
app-data/
├── essay-grader.sqlite # 班级、任务、作文状态与批改结果
├── images/ # 已完成校验的题目和作文图片
├── uploads/ # 尚未完成的 .part 分片文件
├── trash/ # 数据清理事务的临时暂存区
└── cloudflared-empty.yml # 禁止读取用户全局 Tunnel 配置的空文件
Quick Tunnel 是匿名临时服务,没有可用性承诺。其官方限制包括最多 200 个并发请求且不支持 SSE;本项目不使用 SSE,并将页面上传并发固定为最多三路。数据库只保存上传令牌的 SHA-256 哈希。
| 层 | 主要技术 | 负责内容 |
|---|---|---|
| 桌面外壳 | Tauri 2 | 原生窗口、IPC、打包、自动更新 |
| 前端 | React 19、TypeScript、Vite | 教师工作台、任务与结果复核 |
| 本地核心 | Rust、Tokio、SQLx、SQLite | 数据、队列、模型调用、报告 |
| 手机入口 | Axum、原生 Web API | 相机连拍、分片上传、断点续传 |
| 临时通道 | cloudflared Quick Tunnel | 手机到教师电脑的 HTTPS 入口 |
| 可视化 | ECharts | 成绩分布、趋势和班级比较 |
EssayGrader/
├── src/
│ ├── pages/ # 班级、任务、上传、成绩、对比与设置
│ ├── components/ # 固定应用壳、对话框、提示与空状态
│ └── lib/ # Tauri IPC 类型与调用封装
├── src-tauri/
│ ├── src/
│ │ ├── db.rs # SQLite 读写与业务约束
│ │ ├── upload.rs # 本地服务、Quick Tunnel 与分片协议
│ │ ├── grading.rs # 批改队列、转录、识名与保存
│ │ ├── model_gateway.rs # OpenAI 兼容请求、重试和错误映射
│ │ ├── cleanup.rs # 可回滚的数据与图片清理
│ │ └── reports.rs # 匿名班级和班际分析
│ ├── mobile/uploader.html # 零安装手机收卷页
│ └── migrations/ # SQLite 迁移
├── scripts/ # cloudflared 下载、校验与冒烟测试
└── .github/workflows/ # CI 与六平台发布矩阵
应用启动时会依次打开数据库、执行迁移、启动仅监听回环地址的手机服务,再启动后台批改 worker。退出时主动停止当前 Quick Tunnel。上次退出时处于 processing 的作文,会在下次启动时恢复为 queued。
| 平台 | 架构 | 发布目标 | 当前验证状态 |
|---|---|---|---|
| macOS | Apple Silicon | .app / .dmg |
本地构建和启动已验证 |
| macOS | Intel | .app / .dmg |
Release 工作流已配置 |
| Windows | x64 | NSIS | Release 工作流已配置 |
| Windows | ARM64 | NSIS | Release 工作流已配置;cloudflared 从固定源码构建 |
| Linux | x64 | AppImage / .deb |
Release 工作流已配置 |
| Linux | ARM64 | AppImage / .deb |
Release 工作流已配置 |
当前不包含付费 Apple Developer ID 或 Windows Authenticode 证书。macOS 本地包使用 ad-hoc 签名,Windows 首次运行可能显示来源警告。自动更新包使用独立的 Tauri updater 签名密钥。
推送 v* 标签或手动触发发布工作流后,会建立草稿 Release、上传安装包和更新包,并生成 SHA256SUMS.txt;全部构建通过后再公开发布。
- Node.js
24 - pnpm
10.27 - Rust
stable,包含rustfmt与clippy - Tauri 对应平台的系统依赖
pnpm install
pnpm prepare:sidecar
pnpm tauri devprepare:sidecar 会从 Cloudflare 官方 2026.7.2 Release 下载当前目标的 cloudflared,核对官方 SHA-256 后写入 Git 忽略目录。Windows ARM64 没有对应官方资产,发布工作流会从同一标签的固定提交构建。
只构建桌面二进制、不生成安装包:
pnpm tauri build --no-bundle完整 Release 构建需要 TAURI_SIGNING_PRIVATE_KEY;本机没有更新签名私钥时,即使 .app 已生成,创建 updater artifact 的最后一步仍会报错。
Note
Tauri、WebKit、SQLite、TLS 和网络栈会让第一次 Rust 全量编译较慢,并产生数 GB 的 src-tauri/target 缓存。后续增量编译会复用它。需要释放空间时,可以运行官方命令:
cargo clean --manifest-path src-tauri/Cargo.toml# 前端静态检查、组件与功能测试
pnpm lint
pnpm test
pnpm build
# Rust 格式、静态检查和测试
cargo fmt --manifest-path src-tauri/Cargo.toml -- --check
cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
cargo test --manifest-path src-tauri/Cargo.toml
# 内置 cloudflared 是否能建立 Quick Tunnel
pnpm smoke:sidecar当前自动测试重点覆盖:
- 班级创建、任务生命周期和页面跳转;
- 端点保存、视觉探针与密钥不回显;
- 模型错误分类、重试和 JSON 结构校验;
- 无名单自动建档、跨班同名与人工确认;
- 上传分片、偏移续传、限流、会话隔离与撤回;
- 应用重启后的批改队列恢复;
- 清理操作失败时恢复暂存图片;
- 机序调整后学生与作文顺序同步。
仓库提供两张完全虚构的高考英语答题卡风格图片,可以直接用于手机相册上传或本地链路测试:
素材说明和预期身份见 英语作文测试素材。两张卷面均包含少量语言问题和手写划改,不是标准答案,也不包含真实学生信息。
CI 在每次推送到 main 和每个 Pull Request 上执行完整前后端检查。浏览器端到端测试没有塞进 CI;真实手机、真实 Quick Tunnel 和真实模型链路仍应在发布前进行人工验收。
为什么第一次编译很慢,而且 target 有好几 GB?
Rust 会保存依赖、调试信息和增量编译中间产物。Tauri 再叠加 WebKit、SQLite、TLS、Axum 与多平台库格式后,debug 缓存达到数 GB 并不罕见。保留缓存会明显加快后续编译;阶段性结束或磁盘紧张时运行 cargo clean 即可。
macOS 为什么询问是否允许读取 Keychain?
API Key 使用系统 Keychain,不写入应用目录。开发阶段的 ad-hoc 签名和二进制频繁变化,可能让 macOS 再次询问。选择“始终允许”可减少同一构建的重复提示;正式分发仍建议使用稳定的 Developer ID 签名。
二维码生成失败或遇到 Cloudflare 1033 怎么办?
确认网络可以访问 Cloudflare,等待数秒后重新生成。应用会在拿到 trycloudflare.com 地址后继续探测,直到公网请求确实抵达本机服务才展示二维码。如果本地服务本身启动失败,请检查设置中的端口是否被占用。
暂停手机入口以后,为什么任务仍显示“收卷中”?
暂停只关闭二维码和 Quick Tunnel,方便中途停止外部访问;它不会改变任务生命周期。全部作文收完后,需要点击“完成收卷”正式结束任务。
换电脑复制应用,会把 API Key 一起带过去吗?
不会。应用包不包含 API Key,复制 .app 或安装包也不会复制操作系统凭据库。班级数据库和图片同样位于系统应用数据目录,而不是应用二进制内部。
AI 识别的姓名和分数可以完全相信吗?
不应该。模型输出是概率结果,应用会校验结构、分数范围和必填内容,但无法证明语义一定正确。身份无法稳定匹配时会进入待确认;正式交付学生前仍应由老师复核原图、转录和反馈。
- macOS ARM64 本地应用构建、签名检查和启动
- 前后端 CI 与多架构 Release 工作流
- 手机连续拍摄、分片续传和双端撤回
- 结构化批改、人工身份确认和本地归档
- 使用完整测试班级跑通真实手机 → Tunnel → 模型 → 复核 → 导出
- 在 Windows 和 Linux 真机验证安装、凭据库和系统 WebView
- 配置正式 updater 私钥和发布 Secrets
- 如面向普通教师分发,补充 Apple / Windows 商业代码签名
- 发布首个带 SHA-256 清单的公开版本
问题和改进建议可以提交到 Issues。涉及收卷、数据清理或模型输出的改动,请同时补充相应测试,并在提交前运行完整检查。
提交信息使用 Conventional Commits,例如:
feat(手机上传): 支持单页连续拍摄
fix(任务): 统一暂停入口与完成收卷状态
docs(readme): 补充模型协议与平台说明
项目使用 AGPL-3.0-only。
内置 cloudflared 由 Cloudflare 提供,相关许可证与分发说明见 THIRD-PARTY-NOTICES.md。Tauri、React、Rust crates 与其他前端依赖各自遵循其上游许可证。





