- 学生提交时浏览器给每个窗体截图(运行时同一套 FormWindow 在屏幕外渲染),随提交保存 - 自动评分后进入 AI 评阅队列:按书上评价表给老师评分项打建议分、写评语(JSON 输出,服务器再校验、限制分数); 发出的内容不含姓名学号;学生代码里像指令的文字一律当作品内容;学生又交了新的只评最新的 - 提示词按教材命名习惯校准("缩写_含义"、书上规定的控件名、界面上不用的标签保留默认名不扣分): 教材参考答案 28/30,默认控件名且没有注释 15/30 - 评阅页:界面截图标签页、AI 建议卡片(采纳、重新评阅)、全部采纳(不覆盖老师已评的分)、评阅后又提交的提醒; 成绩表显示"AI 建议,待确认";学生在老师确认前看不到 AI 的建议 - 规范提示移到 shared,服务端组装提示词时也用 - 配置 DEEPSEEK_API_KEY、AI_MODEL;部署说明提醒 NPM 调大请求体上限 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
// 评分队列:学生提交后立即返回,后台按提交顺序评分(同时最多评 MAX_ACTIVE 份),结果写回数据库。
|
|
// 服务器重启前没评完的提交,启动时继续评。评完调用 onGraded(接着做 AI 评阅)。
|
|
import type { Database } from 'bun:sqlite'
|
|
import { mkdir, rm } from 'node:fs/promises'
|
|
import { dirname, join } from 'node:path'
|
|
import { assignmentChecks, pendingSubmissionIds, saveGrade, submissionForGrading } from '../assignments'
|
|
import { config } from '../config'
|
|
import { safeJoin } from '../run-session'
|
|
import { grade } from './grader'
|
|
|
|
const MAX_ACTIVE = 3
|
|
|
|
export function createGradingQueue(db: Database, onGraded: (submissionId: number) => void = () => {}) {
|
|
const queue: number[] = []
|
|
let active = 0
|
|
let idleWaiters: (() => void)[] = []
|
|
|
|
async function gradeOne(id: number) {
|
|
const submission = submissionForGrading(db, id)
|
|
const checks = submission && assignmentChecks(db, submission.assignmentId)
|
|
if (!submission || !checks) return
|
|
const dir = join(config.runsDir, `grade-${id}`)
|
|
try {
|
|
await rm(dir, { recursive: true, force: true })
|
|
for (const file of submission.files) {
|
|
const target = safeJoin(dir, file.path)
|
|
await mkdir(dirname(target), { recursive: true })
|
|
await Bun.write(target, file.content)
|
|
}
|
|
saveGrade(db, id, await grade(dir, checks))
|
|
onGraded(id)
|
|
} catch (error) {
|
|
console.error(`评分出错(提交 ${id}):`, error)
|
|
saveGrade(db, id, null)
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
function pump() {
|
|
while (active < MAX_ACTIVE && queue.length > 0) {
|
|
const id = queue.shift()!
|
|
active++
|
|
void gradeOne(id).finally(() => {
|
|
active--
|
|
pump()
|
|
})
|
|
}
|
|
if (active === 0 && queue.length === 0) {
|
|
for (const resolve of idleWaiters) resolve()
|
|
idleWaiters = []
|
|
}
|
|
}
|
|
|
|
for (const id of pendingSubmissionIds(db)) queue.push(id)
|
|
pump()
|
|
|
|
return {
|
|
enqueue(id: number) {
|
|
queue.push(id)
|
|
pump()
|
|
},
|
|
/** 等队列全部评完(测试用) */
|
|
idle(): Promise<void> {
|
|
if (active === 0 && queue.length === 0) return Promise.resolve()
|
|
return new Promise((resolve) => idleWaiters.push(resolve))
|
|
},
|
|
}
|
|
}
|
|
|
|
export type GradingQueue = ReturnType<typeof createGradingQueue>
|