布置作业、提交与自动评分
- 作业:老师选实验、起点(界面已做好 / 空白工程)、截止时间、每项分值(自动评分项 + 老师评分项); 起点工程由设计器引擎从参考答案生成(去掉事件绑定和事件处理方法),布置时保存起点和评分规则快照 - 提交:后台排队评分(全站同时最多评 3 份、跑 6 个检查项),重启后继续评;评分中不能再交,截止后不能交 - 学生:"我的作业"列表;实验页的作业模式(作业名、截止时间、提交、得分),评分结果弹出逐项显示 - 老师:班级管理页加"作业"标签页,布置、修改、删除作业,查看全班提交情况;改分值后重新算分 - 串口列表为空时提示在 Load 或 Click 事件里填充 - 测试:作业接口(可见范围、排队评分、部分得分、改分值、截止、删除);10 个示例两种起点都能编译 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
37fc747b49
commit
4e6b0d5cb2
23 files changed
+1439
-50
No files matched your search
@@ -1,11 +1,11 @@
|
||||
// 自动评分:编译一次学生工程,每个检查项各启动一次程序,按步骤操作并核对结果。
|
||||
// 服务器只有 2 核,全站同时最多跑 GRADE_CONCURRENCY 个检查,其余排队。
|
||||
// 检查大部分时间在等定时器,CPU 主要花在编译和启动程序上;2 核的服务器全站同时跑 GRADE_CONCURRENCY 个检查,其余排队。
|
||||
import type { CheckResult, ControlNode, GradeResult } from '@iot/shared'
|
||||
import { compileService } from '../compile-service'
|
||||
import type { Check, Expectation, Step } from './checks'
|
||||
import { ProgramDriver } from './driver'
|
||||
|
||||
const GRADE_CONCURRENCY = 2
|
||||
const GRADE_CONCURRENCY = 6
|
||||
/** 程序启动后多久内应该显示出窗体 */
|
||||
const START_TIMEOUT = 10_000
|
||||
|
||||
@@ -92,7 +92,9 @@ async function runStep(driver: ProgramDriver, step: Step) {
|
||||
const items = ((node.props.Items as unknown[] | undefined) ?? []).map(String)
|
||||
const index = items.indexOf(step.item)
|
||||
if (index < 0) {
|
||||
throw new Failure(`下拉框 ${step.control} 里没有"${step.item}"(现在的选项:${items.length ? items.join('、') : '空'})`)
|
||||
// 串口列表是空的:最常见的原因是没有填充
|
||||
const hint = items.length === 0 && /^COM\d+$/.test(step.item) ? '。串口列表要在窗体的 Load 事件或下拉框的 Click 事件里用 SerialPort.GetPortNames() 填充' : ''
|
||||
throw new Failure(`下拉框 ${step.control} 里没有"${step.item}"(现在的选项:${items.length ? items.join('、') : '空'})${hint}`)
|
||||
}
|
||||
driver.act(node, { action: 'select', index })
|
||||
await driver.settle()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// 评分队列:学生提交后立即返回,后台按提交顺序评分(同时最多评 MAX_ACTIVE 份),结果写回数据库。
|
||||
// 服务器重启前没评完的提交,启动时继续评。
|
||||
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) {
|
||||
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))
|
||||
} 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>
|
||||
Reference in new issue
Block a user