From 751c4c964d406882006a34c49f9bb6d8b6cd1c6d Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sun, 25 Oct 2026 18:24:41 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20CI=EF=BC=9A=E8=AF=84?= =?UTF-8?q?=E5=88=86=E6=93=8D=E4=BD=9C=E6=8E=A7=E4=BB=B6=E5=89=8D=E7=AD=89?= =?UTF-8?q?=E5=AE=83=E5=87=BA=E7=8E=B0=EF=BC=9BREADME=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=BE=85=E5=8A=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 点击、输入、选择、选日期、点单元格前最多等 3 秒让控件出现:上一步刚打开的窗体 (ShowDialog 前在 Load 里查数据库)在 CI 上来不及显示,p8-t5"按时间段查找"报找不到 dateTimePicker1 - README"尚未完成"去掉已完成的项目 8,列出试用和第 4 阶段的内容 Co-Authored-By: Claude Opus 5.5 --- README.md | 5 +++-- server/src/grading/grader.ts | 18 +++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3456c68..e298e09 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,9 @@ Docker Compose,HTTPS 由服务器上的 Nginx Proxy Manager 负责。推送到 - 多窗体:新建、删除窗体;运行时多个窗口可切换前后。 - 部署:bubblewrap 沙箱(断网、只读、看不到其他进程,限制内存、CPU 时间和运行时长)、编译时禁止网络 / 反射 / 启动进程等 API、Docker Compose、GitHub Actions 自动部署。 - 第 2 阶段(进行中):账号登录(本机开发时老师账号为 admin / admin)、班级管理与 CSV 名单导入、学生作品保存在服务器上;自动评分(每个示例的 `checks.yaml`);布置作业、学生提交后自动评分;老师评阅(看代码和界面截图、运行学生的程序、老师评分项打分、评语)、AI 评阅建议(DeepSeek,老师采纳后生效)、全班成绩表与 CSV 导出。 -- 第 3 阶段(进行中):教材项目 5 用到的旧版线程 API(`Thread.Suspend` / `Resume` / `Abort`)编译时改写成平台的协作式实现(`runtime/Sim.Runner/LegacyThreadRewriter.cs`、`runtime/Sim.WinForms/Host/LegacyThreads.cs`);网络摄像机(画面帧由 `runtime/Sim.Devices/Camera/build-frames.ts` 从 `scene.svg` 生成,需要 rsvg-convert 和 ImageMagick)。 +- 第 3 阶段(教材项目 4–8 已全部完成):教材项目 5 用到的旧版线程 API(`Thread.Suspend` / `Resume` / `Abort`)编译时改写成平台的协作式实现(`runtime/Sim.Runner/LegacyThreadRewriter.cs`、`runtime/Sim.WinForms/Host/LegacyThreads.cs`);网络摄像机(画面帧由 `runtime/Sim.Devices/Camera/build-frames.ts` 从 `scene.svg` 生成,需要 rsvg-convert 和 ImageMagick)。 尚未完成: -- 教材项目 8(需要图表、无边框窗体、Panel 停靠、背景图片等)。 +- 一个班试用。 +- 书上的扩展练习("边想边做""自主探究")、导出 VS 工程、校园街景场景、故障注入题库(第 4 阶段)。 diff --git a/server/src/grading/grader.ts b/server/src/grading/grader.ts index 16f52c4..6cb916b 100644 --- a/server/src/grading/grader.ts +++ b/server/src/grading/grader.ts @@ -12,6 +12,8 @@ import { ProgramDriver } from './driver' const GRADE_CONCURRENCY = 6 /** 点数据表格的单元格前,最多等多久让那一行出现(程序可能还在查询数据库、刷新表格) */ const GRID_WAIT = 5000 +/** 操作控件前最多等多久让它出现 */ +const CONTROL_WAIT = 3000 /** 程序启动后多久内应该显示出窗体 */ const START_TIMEOUT = 10_000 @@ -100,10 +102,16 @@ function need(driver: ProgramDriver, name: string): ControlNode { return node } +/** 操作控件前先等它出现:上一步可能刚打开窗体(ShowDialog 前要在 Load 里查数据库,机器忙时会晚一点) */ +async function needSoon(driver: ProgramDriver, name: string): Promise { + await driver.waitFor(() => driver.find(name) !== undefined, CONTROL_WAIT) + return need(driver, name) +} + async function runStep(driver: ProgramDriver, step: Step) { switch (step.kind) { case 'click': { - const node = need(driver, step.control) + const node = await needSoon(driver, step.control) if (node.props.Enabled === false) throw new Failure(`${step.control} 是灰色的(Enabled 为 false),点不了`) // 和浏览器一样:图片框先收到鼠标按下、松开,再收到单击(教材项目 8 的方向键在按下、松开时转动摄像机) if (node.type === 'PictureBox') { @@ -115,11 +123,11 @@ async function runStep(driver: ProgramDriver, step: Step) { break } case 'text': - driver.act(need(driver, step.control), { action: 'text', value: step.value }) + driver.act(await needSoon(driver, step.control), { action: 'text', value: step.value }) await driver.settle() break case 'select': { - const node = need(driver, step.control) + const node = await needSoon(driver, step.control) if (node.type !== 'ComboBox') throw new Failure(`${step.control} 不是下拉框`) // 和浏览器一样先展开(教材在下拉框的 Click 里填充串口列表),再选 driver.act(node, { action: 'dropDown' }) @@ -137,7 +145,7 @@ async function runStep(driver: ProgramDriver, step: Step) { } case 'cell': case 'edit': { - const node = need(driver, step.control) + const node = await needSoon(driver, step.control) if (node.type !== 'DataGridView') throw new Failure(`${step.control} 不是数据表格`) // 上一步(如保存后重新查询)可能还在刷新表格:等这一行出现 await driver.waitFor(() => gridRows(need(driver, step.control)).length > step.row, GRID_WAIT) @@ -151,7 +159,7 @@ async function runStep(driver: ProgramDriver, step: Step) { break } case 'pick': { - const node = need(driver, step.control) + const node = await needSoon(driver, step.control) if (node.type !== 'DateTimePicker') throw new Failure(`${step.control} 不是日期时间选择器`) // 只给日期时:时间取中午,查询"某天到某天"时不受时分影响 const [date, time = '12:00:00'] = step.value.split(' ')