用 bun fmt 格式化全部代码和文档
- bunfig.toml 的 [fmt]:不写分号、单引号、一行最长 160,和原来的写法一致 - .prettierignore:docs/教材摘要/ 是书上的摘录,不格式化 - 模板里一行写两条语句的事件处理(@keydown.delete、@dblclick)改成调用方法:去掉分号后拆成两行,Vue 编译不过 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d4b8bf9241
commit
de277d3e9d
102 files changed
+1971
-1059
No files matched your search
+11
-7
@@ -64,7 +64,10 @@ function userPrompt(input: {
|
||||
? input.result.results.map((r) => `- ${r.passed ? '通过' : '未通过'}:${r.title}${r.message ? `(${r.message})` : ''}`).join('\n')
|
||||
: '编译没有通过。'
|
||||
: '(还没有自动评分结果)'
|
||||
const hints = conventionHints(input.files).map((h) => `- ${h.text}`).join('\n') || '- 无'
|
||||
const hints =
|
||||
conventionHints(input.files)
|
||||
.map((h) => `- ${h.text}`)
|
||||
.join('\n') || '- 无'
|
||||
const code = input.files
|
||||
.filter((f) => f.path.endsWith('.cs'))
|
||||
.map((f) => `===== ${f.path} =====\n${f.content}`)
|
||||
@@ -75,8 +78,12 @@ function userPrompt(input: {
|
||||
${items}
|
||||
|
||||
${BOOK_CRITERIA}
|
||||
${input.bookNames.length ? `- 本作业书上表格规定的控件名(学生照着用是规范的,不扣分):${input.bookNames.join('、')}
|
||||
` : ''}
|
||||
${
|
||||
input.bookNames.length
|
||||
? `- 本作业书上表格规定的控件名(学生照着用是规范的,不扣分):${input.bookNames.join('、')}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
平台自动评分的结果(功能,供参考,不用再评):
|
||||
${auto}
|
||||
|
||||
@@ -101,10 +108,7 @@ async function callModel(system: string, user: string, screenshots: Screenshot[]
|
||||
{ role: 'system', content: system },
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: user },
|
||||
...screenshots.map((s) => ({ type: 'image_url', image_url: { url: `data:image/png;base64,${s.png}` } })),
|
||||
],
|
||||
content: [{ type: 'text', text: user }, ...screenshots.map((s) => ({ type: 'image_url', image_url: { url: `data:image/png;base64,${s.png}` } }))],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
+27
-4
@@ -259,7 +259,6 @@ export function createApp(db: Database, gradingQueue: GradingQueue, aiQueue: AiQ
|
||||
await next()
|
||||
})
|
||||
|
||||
|
||||
app.get('/api/teacher/classes', (c) => c.json(listClasses(db)))
|
||||
|
||||
app.post('/api/teacher/classes', async (c) => {
|
||||
@@ -308,7 +307,16 @@ export function createApp(db: Database, gradingQueue: GradingQueue, aiQueue: AiQ
|
||||
)
|
||||
|
||||
app.post('/api/teacher/assignments', async (c) => {
|
||||
const a = await body<{ classId: number; sampleId: string; title: string; startMode: StartMode; template: unknown; scoring: Scoring; deadline: number | null; task: string | null }>(c)
|
||||
const a = await body<{
|
||||
classId: number
|
||||
sampleId: string
|
||||
title: string
|
||||
startMode: StartMode
|
||||
template: unknown
|
||||
scoring: Scoring
|
||||
deadline: number | null
|
||||
task: string | null
|
||||
}>(c)
|
||||
if (!listClasses(db).some((cls) => cls.id === a.classId)) return c.json({ message: '班级不存在' }, 404)
|
||||
const checks = typeof a.sampleId === 'string' ? await loadChecks(a.sampleId) : null
|
||||
if (!checks) return c.json({ message: '这个实验还没有评分规则' }, 400)
|
||||
@@ -352,7 +360,13 @@ export function createApp(db: Database, gradingQueue: GradingQueue, aiQueue: AiQ
|
||||
classSubmissions(db, id(c)).map((s) => {
|
||||
const review = getReview(db, id(c), s.userId)
|
||||
const ai = s.submissionId ? getAiReview(db, s.submissionId) : null
|
||||
return { ...s, manualScore: manualScore(review), review, aiStatus: ai?.status ?? null, aiManual: ai?.status === 'done' ? ai.scores.reduce((a, x) => a + x.score, 0) : null }
|
||||
return {
|
||||
...s,
|
||||
manualScore: manualScore(review),
|
||||
review,
|
||||
aiStatus: ai?.status ?? null,
|
||||
aiManual: ai?.status === 'done' ? ai.scores.reduce((a, x) => a + x.score, 0) : null,
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -378,7 +392,16 @@ export function createApp(db: Database, gradingQueue: GradingQueue, aiQueue: AiQ
|
||||
if (!s.submissionId || getReview(db, id(c), s.userId)) continue
|
||||
const ai = getAiReview(db, s.submissionId)
|
||||
if (ai?.status !== 'done') continue
|
||||
if (!saveReview(db, id(c), s.userId, ai.scores.map((x) => x.score), ai.comment).error) adopted++
|
||||
if (
|
||||
!saveReview(
|
||||
db,
|
||||
id(c),
|
||||
s.userId,
|
||||
ai.scores.map((x) => x.score),
|
||||
ai.comment,
|
||||
).error
|
||||
)
|
||||
adopted++
|
||||
}
|
||||
return c.json({ adopted })
|
||||
})
|
||||
|
||||
@@ -121,7 +121,9 @@ export function updateAssignment(
|
||||
id,
|
||||
)
|
||||
// 分值变了,已评分的提交重新算分
|
||||
for (const s of db.query<{ id: number; result: string }, [number]>(`SELECT id, result FROM submissions WHERE assignment_id = ? AND status = 'done'`).all(id)) {
|
||||
for (const s of db
|
||||
.query<{ id: number; result: string }, [number]>(`SELECT id, result FROM submissions WHERE assignment_id = ? AND status = 'done'`)
|
||||
.all(id)) {
|
||||
db.query('UPDATE submissions SET auto_score = ? WHERE id = ?').run(autoScore(JSON.parse(s.result), changes.scoring), s.id)
|
||||
}
|
||||
return {}
|
||||
@@ -180,7 +182,10 @@ export function createSubmission(db: Database, assignmentId: number, userId: num
|
||||
}
|
||||
|
||||
export function pendingSubmissionIds(db: Database): number[] {
|
||||
return db.query<{ id: number }, []>(`SELECT id FROM submissions WHERE status = 'pending' ORDER BY id`).all().map((r) => r.id)
|
||||
return db
|
||||
.query<{ id: number }, []>(`SELECT id FROM submissions WHERE status = 'pending' ORDER BY id`)
|
||||
.all()
|
||||
.map((r) => r.id)
|
||||
}
|
||||
|
||||
export function submissionForGrading(db: Database, id: number) {
|
||||
@@ -211,7 +216,16 @@ export function saveGrade(db: Database, id: number, result: GradeResult | null)
|
||||
export function classSubmissions(db: Database, assignmentId: number) {
|
||||
return db
|
||||
.query<
|
||||
{ user_id: number; username: string; name: string; submission_id: number | null; status: Submission['status'] | null; auto_score: number | null; created_at: number | null; count: number },
|
||||
{
|
||||
user_id: number
|
||||
username: string
|
||||
name: string
|
||||
submission_id: number | null
|
||||
status: Submission['status'] | null
|
||||
auto_score: number | null
|
||||
created_at: number | null
|
||||
count: number
|
||||
},
|
||||
[number]
|
||||
>(
|
||||
`SELECT u.id AS user_id, u.username, u.name, s.id AS submission_id, s.status, s.auto_score, s.created_at,
|
||||
|
||||
+12
-3
@@ -67,7 +67,8 @@ export class Board {
|
||||
* 恢复真实时间是 clock = null。
|
||||
*/
|
||||
setClock(time: string | undefined, speed: number | undefined) {
|
||||
if (time !== undefined && (typeof time !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(time) || Number.isNaN(Date.parse(`${time}Z`)))) return false
|
||||
if (time !== undefined && (typeof time !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/.test(time) || Number.isNaN(Date.parse(`${time}Z`))))
|
||||
return false
|
||||
if (speed !== undefined && (typeof speed !== 'number' || !Number.isFinite(speed) || speed < 1 || speed > 3600)) return false
|
||||
const now = Date.now()
|
||||
this.state.clock = {
|
||||
@@ -124,6 +125,14 @@ export class Board {
|
||||
export function wallNow(now = new Date()) {
|
||||
return new Intl.DateTimeFormat('sv-SE', {
|
||||
timeZone: config.timeZone,
|
||||
year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23',
|
||||
}).format(now).replace(' ', 'T')
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
})
|
||||
.format(now)
|
||||
.replace(' ', 'T')
|
||||
}
|
||||
@@ -68,9 +68,7 @@ export function deleteClass(db: Database, id: number): { error?: string } {
|
||||
}
|
||||
|
||||
export function listStudents(db: Database, classId: number): Student[] {
|
||||
return db
|
||||
.query<Student, [number]>(`SELECT id, username, name, password FROM users WHERE class_id = ? AND role = 'student' ORDER BY username`)
|
||||
.all(classId)
|
||||
return db.query<Student, [number]>(`SELECT id, username, name, password FROM users WHERE class_id = ? AND role = 'student' ORDER BY username`).all(classId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,7 +61,15 @@ export type Expectation =
|
||||
*/
|
||||
const DEFAULT_WITHIN = 5000
|
||||
|
||||
const PROP_KEYS: Record<string, string> = { text: 'Text', ledText: 'LedText', enabled: 'Enabled', checked: 'Checked', visible: 'Visible', on: 'IsOn', value: 'Value' }
|
||||
const PROP_KEYS: Record<string, string> = {
|
||||
text: 'Text',
|
||||
ledText: 'LedText',
|
||||
enabled: 'Enabled',
|
||||
checked: 'Checked',
|
||||
visible: 'Visible',
|
||||
on: 'IsOn',
|
||||
value: 'Value',
|
||||
}
|
||||
const STRING_PROPS = new Set(['Text', 'LedText'])
|
||||
const NUMBER_PROPS = new Set(['Value'])
|
||||
|
||||
@@ -96,7 +104,8 @@ function parseExpectation(raw: Record<string, unknown>, where: string): Expectat
|
||||
if ('media' in raw) return { kind: 'media', name: str(raw.media, where, '播放的文件名') }
|
||||
if ('savedImage' in raw) return { kind: 'savedImage', path: str(raw.savedImage, where, '保存的图片路径应包含的文字') }
|
||||
if ('relay' in raw) return { kind: 'relay', index: Number(raw.relay), on: bool(raw.on, where, 'on') }
|
||||
if ('window' in raw) return { kind: 'window', name: str(raw.window, where, '窗体名 window'), open: raw.open === undefined ? true : bool(raw.open, where, 'open') }
|
||||
if ('window' in raw)
|
||||
return { kind: 'window', name: str(raw.window, where, '窗体名 window'), open: raw.open === undefined ? true : bool(raw.open, where, 'open') }
|
||||
if ('exited' in raw) {
|
||||
if (raw.exited !== true) throw new Error(`${where}:exited 只能写 true`)
|
||||
return { kind: 'exited' }
|
||||
@@ -122,7 +131,10 @@ function parseExpectation(raw: Record<string, unknown>, where: string): Expectat
|
||||
return { kind: 'row', control, cells, present: 'row' in raw }
|
||||
}
|
||||
// image: true 或 image: 资源名;background: 资源名
|
||||
for (const [key, prop] of [['image', 'Image'], ['background', 'BackgroundImage']] as const) {
|
||||
for (const [key, prop] of [
|
||||
['image', 'Image'],
|
||||
['background', 'BackgroundImage'],
|
||||
] as const) {
|
||||
if (!(key in raw)) continue
|
||||
const value = raw[key]
|
||||
if (value === true) return { kind: 'image', control, prop }
|
||||
|
||||
@@ -135,7 +135,8 @@ async function runStep(driver: ProgramDriver, step: Step) {
|
||||
const index = items.indexOf(step.item)
|
||||
if (index < 0) {
|
||||
// 串口列表是空的:最常见的原因是没有填充
|
||||
const hint = items.length === 0 && /^COM\d+$/.test(step.item) ? '。串口列表要在窗体的 Load 事件或下拉框的 Click 事件里用 SerialPort.GetPortNames() 填充' : ''
|
||||
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 })
|
||||
@@ -151,7 +152,14 @@ async function runStep(driver: ProgramDriver, step: Step) {
|
||||
const rows = gridRows(node)
|
||||
if (step.row >= rows.length) throw new Failure(`数据表格 ${step.control} 里没有第 ${step.row + 1} 行(现在有 ${rows.length} 行数据)`)
|
||||
const column = typeof step.column === 'number' ? step.column : gridColumns(node).findIndex((c) => c.header === step.column || c.name === step.column)
|
||||
if (column < 0) throw new Failure(`数据表格 ${step.control} 里没有"${step.column}"列(现在的列:${gridColumns(node).map((c) => c.header).join('、') || '没有'})`)
|
||||
if (column < 0)
|
||||
throw new Failure(
|
||||
`数据表格 ${step.control} 里没有"${step.column}"列(现在的列:${
|
||||
gridColumns(node)
|
||||
.map((c) => c.header)
|
||||
.join('、') || '没有'
|
||||
})`,
|
||||
)
|
||||
driver.act(node, { action: 'cellClick', row: step.row, column })
|
||||
if (step.kind === 'edit') driver.act(node, { action: 'cellEdit', row: step.row, column, value: step.value })
|
||||
await driver.settle()
|
||||
@@ -302,8 +310,10 @@ function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind:
|
||||
case 'prop': {
|
||||
const actual = need(driver, e.control).props[e.prop]
|
||||
if (e.prop === 'Text' || e.prop === 'LedText') return `期望${prefix} ${e.control} ${desc ?? `显示"${e.value}"`},实际是${quote(actual)}`
|
||||
if (e.prop === 'Enabled') return `期望${prefix} ${e.control} ${desc ?? (e.value ? '可用' : '变成灰色不可用')},实际${actual === false ? '不可用' : '可用'}`
|
||||
if (e.prop === 'Visible') return `期望${prefix} ${e.control} ${desc ?? (e.value ? '显示出来' : '隐藏')},实际${driver.isVisible(need(driver, e.control)) ? '显示着' : '是隐藏的'}`
|
||||
if (e.prop === 'Enabled')
|
||||
return `期望${prefix} ${e.control} ${desc ?? (e.value ? '可用' : '变成灰色不可用')},实际${actual === false ? '不可用' : '可用'}`
|
||||
if (e.prop === 'Visible')
|
||||
return `期望${prefix} ${e.control} ${desc ?? (e.value ? '显示出来' : '隐藏')},实际${driver.isVisible(need(driver, e.control)) ? '显示着' : '是隐藏的'}`
|
||||
if (e.prop === 'Checked') return `期望${prefix} ${e.control} ${desc ?? (e.value ? '被选中' : '没有被选中')},实际${actual ? '被选中' : '没有被选中'}`
|
||||
if (e.prop === 'IsOn') return `期望${prefix}窗体上的 ${e.control} ${desc ?? (e.value ? '亮' : '灭')},实际是${actual ? '亮' : '灭'}的`
|
||||
if (e.prop === 'Value') return `期望${prefix} ${e.control} ${desc ?? `的值是 ${String(e.value)}`},实际是 ${String(actual)}`
|
||||
@@ -312,7 +322,8 @@ function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind:
|
||||
case 'contains': {
|
||||
const node = need(driver, e.control)
|
||||
const items = listItems(node)
|
||||
if (items) return `期望${prefix} ${e.control} ${desc ?? `的列表项包含"${e.text}"`},实际${items.length ? `列表里是 ${items.map((i) => `"${i}"`).join('、')}` : '列表是空的'}`
|
||||
if (items)
|
||||
return `期望${prefix} ${e.control} ${desc ?? `的列表项包含"${e.text}"`},实际${items.length ? `列表里是 ${items.map((i) => `"${i}"`).join('、')}` : '列表是空的'}`
|
||||
return `期望${prefix} ${e.control} ${desc ?? `的文字包含"${e.text}"`},实际是${quote(node.props.Text)}`
|
||||
}
|
||||
case 'matches':
|
||||
@@ -354,12 +365,18 @@ function describeFailure(driver: ProgramDriver, e: Exclude<Expectation, { kind:
|
||||
return `期望${prefix}${desc ?? `播放 ${e.name}`},${driver.played.length ? `实际播放的是 ${driver.played.join('、')}` : '实际没有播放声音'}`
|
||||
case 'row': {
|
||||
const node = need(driver, e.control)
|
||||
const wanted = Object.entries(e.cells).map(([k, v]) => `${k}=${v}`).join(',')
|
||||
const wanted = Object.entries(e.cells)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(',')
|
||||
const columns = gridColumns(node)
|
||||
const missing = Object.keys(e.cells).filter((key) => !columns.some((c) => c.header === key || c.name === key))
|
||||
if (missing.length) return `期望${prefix}数据表格 ${e.control} 里有"${missing.join('、')}"列,实际的列是 ${columns.map((c) => c.header).join('、') || '(没有数据)'}`
|
||||
if (missing.length)
|
||||
return `期望${prefix}数据表格 ${e.control} 里有"${missing.join('、')}"列,实际的列是 ${columns.map((c) => c.header).join('、') || '(没有数据)'}`
|
||||
const rows = gridRows(node)
|
||||
const shown = rows.slice(0, 5).map((r) => r.join(' | ')).join(';')
|
||||
const shown = rows
|
||||
.slice(0, 5)
|
||||
.map((r) => r.join(' | '))
|
||||
.join(';')
|
||||
return `期望${prefix}数据表格 ${e.control} ${desc ?? (e.present ? `有一行 ${wanted}` : `没有 ${wanted} 的行`)},实际表格里${rows.length ? `是:${shown}${rows.length > 5 ? ' ……' : ''}` : '没有数据'}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { config } from '../config'
|
||||
import { safeJoin } from '../run-session'
|
||||
import { grade } from './grader'
|
||||
|
||||
|
||||
/** 把作品写进一个干净的工程目录(评分和自测用) */
|
||||
export async function writeProject(dir: string, files: SourceFile[]) {
|
||||
await rm(dir, { recursive: true, force: true })
|
||||
|
||||
+9
-2
@@ -30,7 +30,11 @@ if (sandboxError) console.error(sandboxError)
|
||||
const db = openDatabase()
|
||||
ensureTeacher(db)
|
||||
const aiQueue = createAiQueue(db)
|
||||
const app = createApp(db, createGradingQueue(db, (id) => aiQueue.enqueue(id)), aiQueue)
|
||||
const app = createApp(
|
||||
db,
|
||||
createGradingQueue(db, (id) => aiQueue.enqueue(id)),
|
||||
aiQueue,
|
||||
)
|
||||
|
||||
interface SocketData {
|
||||
userId?: number
|
||||
@@ -58,7 +62,10 @@ const server = Bun.serve({
|
||||
sendPings: true,
|
||||
open(ws) {
|
||||
const userId = ws.data.userId!
|
||||
ws.data.session = new RunSession((message) => ws.send(JSON.stringify(message)), () => userDatabaseDir(userId))
|
||||
ws.data.session = new RunSession(
|
||||
(message) => ws.send(JSON.stringify(message)),
|
||||
() => userDatabaseDir(userId),
|
||||
)
|
||||
},
|
||||
async message(ws, raw) {
|
||||
let message: ClientMessage
|
||||
|
||||
+17
-5
@@ -39,7 +39,8 @@ export function saveReview(db: Database, assignmentId: number, userId: number, s
|
||||
if (!Array.isArray(scores) || scores.length !== manual.length) return { error: `老师评分项应有 ${manual.length} 项` }
|
||||
for (const [i, s] of scores.entries()) {
|
||||
if (s === null) continue
|
||||
if (typeof s !== 'number' || !Number.isFinite(s) || s < 0 || s > manual[i].points) return { error: `"${manual[i].title}"的得分要在 0 到 ${manual[i].points} 之间` }
|
||||
if (typeof s !== 'number' || !Number.isFinite(s) || s < 0 || s > manual[i].points)
|
||||
return { error: `"${manual[i].title}"的得分要在 0 到 ${manual[i].points} 之间` }
|
||||
}
|
||||
db.query(
|
||||
`INSERT INTO reviews (assignment_id, user_id, scores, comment, updated_at) VALUES (?, ?, ?, ?, ?)
|
||||
@@ -53,10 +54,13 @@ export function studentDetail(db: Database, assignmentId: number, userId: number
|
||||
const student = db.query<{ id: number; username: string; name: string }, [number]>('SELECT id, username, name FROM users WHERE id = ?').get(userId)
|
||||
if (!student) return null
|
||||
const latest = latestSubmission(db, assignmentId, userId)
|
||||
const row = latest && db.query<{ files: string; screenshots: string | null }, [number]>('SELECT files, screenshots FROM submissions WHERE id = ?').get(latest.id)
|
||||
const row =
|
||||
latest && db.query<{ files: string; screenshots: string | null }, [number]>('SELECT files, screenshots FROM submissions WHERE id = ?').get(latest.id)
|
||||
const files = row ? (JSON.parse(row.files) as SourceFile[]) : null
|
||||
const screenshots = row?.screenshots ? (JSON.parse(row.screenshots) as Screenshot[]) : []
|
||||
const count = db.query<{ n: number }, [number, number]>('SELECT COUNT(*) AS n FROM submissions WHERE assignment_id = ? AND user_id = ?').get(assignmentId, userId)!.n
|
||||
const count = db
|
||||
.query<{ n: number }, [number, number]>('SELECT COUNT(*) AS n FROM submissions WHERE assignment_id = ? AND user_id = ?')
|
||||
.get(assignmentId, userId)!.n
|
||||
return {
|
||||
student,
|
||||
submission: latest,
|
||||
@@ -77,7 +81,13 @@ export function submissionFiles(db: Database, submissionId: number) {
|
||||
)
|
||||
.get(submissionId)
|
||||
if (!row) return null
|
||||
return { files: JSON.parse(row.files) as SourceFile[], assignmentId: row.assignment_id, assignmentTitle: row.title, studentName: row.name, username: row.username }
|
||||
return {
|
||||
files: JSON.parse(row.files) as SourceFile[],
|
||||
assignmentId: row.assignment_id,
|
||||
assignmentTitle: row.title,
|
||||
studentName: row.name,
|
||||
username: row.username,
|
||||
}
|
||||
}
|
||||
|
||||
/** 全班成绩表:每个学生在每个作业上的自动评分、老师评分和总分 */
|
||||
@@ -91,7 +101,9 @@ export function gradebook(db: Database, classId: number) {
|
||||
return { id: a.id, title: a.title, scoring, total: sum(scoring.auto) + sum(scoring.manual.map((m) => m.points)) }
|
||||
})
|
||||
const students = db
|
||||
.query<{ id: number; username: string; name: string }, [number]>(`SELECT id, username, name FROM users WHERE class_id = ? AND role = 'student' ORDER BY username`)
|
||||
.query<{ id: number; username: string; name: string }, [number]>(
|
||||
`SELECT id, username, name FROM users WHERE class_id = ? AND role = 'student' ORDER BY username`,
|
||||
)
|
||||
.all(classId)
|
||||
return {
|
||||
assignments: assignments.map(({ id, title, total }) => ({ id, title, total })),
|
||||
|
||||
+30
-11
@@ -75,13 +75,20 @@ export function runCommand(projectDir: string, extraEnv: Record<string, string>,
|
||||
const env = runnerEnv({ ...extraEnv, ...(dbDir ? { SIM_DB_DIR: DB_DIR } : {}) })
|
||||
const cmd = [
|
||||
// 学生程序写出死循环时,降低它的优先级,网页服务和编译服务仍然流畅
|
||||
'nice', '-n', '10',
|
||||
'nice',
|
||||
'-n',
|
||||
'10',
|
||||
// CPU 时间用完直接 SIGKILL 结束(软硬上限相同)。软上限更小时内核先发 SIGXCPU,它默认生成 core dump:
|
||||
// 学生每写一次死循环就留下一个转储,本机桌面还会弹出"程序崩溃"
|
||||
'prlimit', `--cpu=${config.cpuLimitSeconds}:${config.cpuLimitSeconds}`, '--core=0',
|
||||
'prlimit',
|
||||
`--cpu=${config.cpuLimitSeconds}:${config.cpuLimitSeconds}`,
|
||||
'--core=0',
|
||||
...bwrapArgs(projectDir),
|
||||
...(dbDir ? ['--bind', dbDir, DB_DIR] : []),
|
||||
'dotnet', `${RUNNER_DIR}/${config.runnerDll.split('/').pop()}`, 'run', APP_DIR,
|
||||
'dotnet',
|
||||
`${RUNNER_DIR}/${config.runnerDll.split('/').pop()}`,
|
||||
'run',
|
||||
APP_DIR,
|
||||
]
|
||||
return { cmd, env }
|
||||
}
|
||||
@@ -105,15 +112,27 @@ function bwrapArgs(projectDir: string) {
|
||||
'--unshare-all',
|
||||
'--die-with-parent',
|
||||
'--new-session',
|
||||
'--ro-bind', '/usr', '/usr',
|
||||
'--ro-bind',
|
||||
'/usr',
|
||||
'/usr',
|
||||
...rootLinks(),
|
||||
'--ro-bind-try', '/etc/localtime', '/etc/localtime',
|
||||
'--ro-bind-try',
|
||||
'/etc/localtime',
|
||||
'/etc/localtime',
|
||||
...dotnetMounts(),
|
||||
'--proc', '/proc',
|
||||
'--dev', '/dev',
|
||||
'--tmpfs', '/tmp',
|
||||
'--ro-bind', dirname(config.runnerDll), RUNNER_DIR,
|
||||
'--ro-bind', projectDir, APP_DIR,
|
||||
'--chdir', APP_DIR,
|
||||
'--proc',
|
||||
'/proc',
|
||||
'--dev',
|
||||
'/dev',
|
||||
'--tmpfs',
|
||||
'/tmp',
|
||||
'--ro-bind',
|
||||
dirname(config.runnerDll),
|
||||
RUNNER_DIR,
|
||||
'--ro-bind',
|
||||
projectDir,
|
||||
APP_DIR,
|
||||
'--chdir',
|
||||
APP_DIR,
|
||||
]
|
||||
}
|
||||
@@ -25,9 +25,7 @@ export interface SqlColumnInfo {
|
||||
}
|
||||
|
||||
export type SqlRequest =
|
||||
| { type: 'exec'; dir: string; database: string; sql: string }
|
||||
| { type: 'script'; dir: string; sql: string }
|
||||
| { type: 'explorer'; dir: string }
|
||||
{ type: 'exec'; dir: string; database: string; sql: string } | { type: 'script'; dir: string; sql: string } | { type: 'explorer'; dir: string }
|
||||
|
||||
/** SQL 报错(编号和信息与 SQL Server 一致) */
|
||||
export class SqlError extends Error {
|
||||
@@ -78,7 +76,13 @@ class SqlService {
|
||||
stdin: 'pipe',
|
||||
stdout: 'pipe',
|
||||
stderr: 'inherit',
|
||||
env: { PATH: process.env.PATH ?? '/usr/bin:/bin', HOME: process.env.HOME ?? '/tmp', DOTNET_CLI_TELEMETRY_OPTOUT: '1', DOTNET_NOLOGO: '1', TZ: config.timeZone },
|
||||
env: {
|
||||
PATH: process.env.PATH ?? '/usr/bin:/bin',
|
||||
HOME: process.env.HOME ?? '/tmp',
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: '1',
|
||||
DOTNET_NOLOGO: '1',
|
||||
TZ: config.timeZone,
|
||||
},
|
||||
})
|
||||
this.proc = proc
|
||||
this.ready = new Promise<void>((resolveReady) => {
|
||||
|
||||
Reference in new issue
Block a user