AI 评阅:DeepSeek Flash 给老师评分项出建议分和评语,老师采纳后生效
- 学生提交时浏览器给每个窗体截图(运行时同一套 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>
This commit is contained in:
1 parent
302eae4f13
commit
14a82a3e24
25 files changed
+700
-44
No files matched your search
@@ -0,0 +1,46 @@
|
||||
// 规范提示:给老师评"注释、命名"时参考,不计分。
|
||||
// - 控件命名:在代码里用到、却还是默认名字(button1、textBox1……)的控件。书上要求"控件缩写 + 有意义的英文",
|
||||
// 如 btn_Open;只放在界面上、代码里不用的标签、分组框可以保留默认名字(书上也是这样),
|
||||
// 教材自带的设备控件(lamp1、alarm1)书上就这样命名,也不提示。
|
||||
// - 注释:学生自己写的代码文件(不含 Designer.cs、Program.cs)里有多少行注释。
|
||||
import type { SourceFile } from './protocol'
|
||||
|
||||
export interface ConventionHint {
|
||||
level: 'warn' | 'info'
|
||||
text: string
|
||||
}
|
||||
|
||||
const DEFAULT_NAME = /^(button|label|textBox|comboBox|checkBox|radioButton|listBox)\d+$/
|
||||
|
||||
const studentCode = (files: SourceFile[]) => files.filter((f) => f.path.endsWith('.cs') && !f.path.endsWith('.Designer.cs') && f.path !== 'Program.cs')
|
||||
|
||||
export function conventionHints(files: SourceFile[]): ConventionHint[] {
|
||||
const hints: ConventionHint[] = []
|
||||
const code = studentCode(files)
|
||||
const codeText = code.map((f) => f.content).join('\n')
|
||||
|
||||
const designers = files.filter((f) => f.path.endsWith('.Designer.cs')).map((f) => f.content)
|
||||
const controls = designers.flatMap((text) => [...text.matchAll(/^\s*private\s+[\w.]+\s+(\w+);/gm)].map((m) => m[1]))
|
||||
// 绑定了事件(this.button1.Click += ...)或在代码里用到,就算"用到"
|
||||
const withEvents = new Set(designers.flatMap((text) => [...text.matchAll(/this\.(\w+)\.\w+\s*\+=/g)].map((m) => m[1])))
|
||||
const usedInCode = (name: string) => new RegExp(`(?<![\\w])${name}(?![\\w])`).test(codeText)
|
||||
const defaults = controls.filter((name) => DEFAULT_NAME.test(name) && (withEvents.has(name) || usedInCode(name)))
|
||||
if (defaults.length) {
|
||||
hints.push({ level: 'warn', text: `代码里用到的控件还是默认名字:${defaults.join('、')}(书上要求"控件缩写 + 有意义的英文",如 btn_Open)` })
|
||||
}
|
||||
|
||||
let comments = 0
|
||||
let lines = 0
|
||||
for (const line of codeText.split('\n').map((l) => l.trim())) {
|
||||
if (!line || line === '{' || line === '}' || line.startsWith('using ')) continue
|
||||
if (line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) comments++
|
||||
else {
|
||||
lines++
|
||||
if (line.includes('//')) comments++
|
||||
}
|
||||
}
|
||||
if (lines > 0) {
|
||||
hints.push(comments === 0 ? { level: 'warn', text: `没有写注释(代码 ${lines} 行)` } : { level: 'info', text: `注释 ${comments} 行,代码 ${lines} 行` })
|
||||
}
|
||||
return hints
|
||||
}
|
||||
@@ -59,3 +59,23 @@ export interface Review {
|
||||
comment: string
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
/** 提交时浏览器截的窗体图(PNG 的 base64,不含 data: 前缀) */
|
||||
export interface Screenshot {
|
||||
form: string
|
||||
png: string
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 评阅:老师评分项的建议分(和 scoring.manual 一一对应)、理由和给学生的评语。
|
||||
* 只是建议,老师采纳(或修改后保存)才成为老师评分。
|
||||
* status:pending 排队中;done 已评;error 出错;skipped 学生又交了新的,这份不评了
|
||||
*/
|
||||
export interface AiReview {
|
||||
status: 'pending' | 'done' | 'error' | 'skipped'
|
||||
scores: { score: number; reason: string }[]
|
||||
comment: string
|
||||
error?: string
|
||||
model?: string
|
||||
at?: number
|
||||
}
|
||||
@@ -136,3 +136,4 @@ export type ServerMessage =
|
||||
| { type: 'serverError'; message: string }
|
||||
|
||||
export * from './grading'
|
||||
export * from './conventions'
|
||||
Reference in new issue
Block a user