原来让 AI 按逻辑 40 / 完整 30 / 规范 20 / 清晰 10 打总分:同一张图交两次能差 35 分 (有一张 C 再交变 S),22% 的提交和上一次一模一样;评语里写 node_9 这种学生看不到的编号。 照画布稿 FlowResFail / FlowResPass: - AI 拿题目的例子和最多 3 组测试数据,从开始照着图走到结束,逐组说输出什么、对不对、 最先错在哪个框;全对 A,全对且写法挑不出毛病 S,走对一半以上 B,否则 C - 结果页签:「3 组走对 2 组」,逐组 ✓ / ✗,点开看「你的图输出 / 应该输出」,画布上按先后 编号亮出这一组走过的框、错的那个标红;图一改就不亮,载回那一版又能亮 - 画布和上次评完的那张一模一样时「交给 AI 点评」灰掉;后端也兜着:同一道题一模一样的图 直接照搬上次走例子的结果,不再问 AI - 题目一组例子、测试数据都没有时退回老的四项分;老提交照旧按四项分显示 - 历次等级、提交列表的胶囊只写等级,不写分数 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
2.9 KiB
TypeScript
72 lines
2.9 KiB
TypeScript
import { readFile } from "node:fs/promises"
|
|
import { resolve } from "node:path"
|
|
|
|
import { config } from "../config"
|
|
import type { schema } from "../db"
|
|
import { asRecord } from "../routes/helpers"
|
|
import { readInfo } from "../services/test-case"
|
|
|
|
/** 拿来在图上走一遍的一组数据 */
|
|
export interface FlowchartCase {
|
|
name: string
|
|
input: string
|
|
expected: string
|
|
}
|
|
|
|
/** 例子之外再从测试数据里挑几组:例子常常覆盖不全(蜜橘分拣的五个例子没有「黄 + 有瑕疵」) */
|
|
const MAX_TESTS = 3
|
|
/** 一共最多走这么多组,再多 AI 就慢了、也更容易走错 */
|
|
const MAX_CASES = 8
|
|
/** 一组数据太长的不拿:流程图题都是几行输入,长的多半是批量数据,走不动 */
|
|
const MAX_TEXT = 300
|
|
|
|
function clean(text: string) {
|
|
return text.replace(/\r\n?/g, "\n").replace(/\s+$/, "")
|
|
}
|
|
|
|
/**
|
|
* 这道题拿哪几组数据来走:题目的例子全拿(学生本来就看得到),再补几组测试数据里和例子
|
|
* 不一样的。测试数据的输入和应该输出会出现在结果页签里 —— 流程图题都是几行输入的小题,
|
|
* 学生看到一两组不构成泄露,而「这一组走错了」比「逻辑正确性 25 / 40」有用得多。
|
|
*/
|
|
export async function flowchartCases(
|
|
problem: typeof schema.problem.$inferSelect,
|
|
): Promise<FlowchartCase[]> {
|
|
const cases: FlowchartCase[] = []
|
|
const seen = new Set<string>()
|
|
const samples = Array.isArray(problem.samples) ? problem.samples : []
|
|
for (const raw of samples) {
|
|
const item = asRecord(raw)
|
|
if (typeof item.input !== "string" || typeof item.output !== "string") continue
|
|
const input = clean(item.input)
|
|
const expected = clean(item.output)
|
|
if (input.length > MAX_TEXT || expected.length > MAX_TEXT) continue
|
|
seen.add(input)
|
|
cases.push({ name: `例子 ${cases.length + 1}`, input, expected })
|
|
if (cases.length >= MAX_CASES) return cases
|
|
}
|
|
|
|
// 测试数据只有服务器上有;本机、或者题目没传测试点时就只用例子
|
|
const info = await readInfo(problem.testCaseId)
|
|
if (!info || info.sql) return cases
|
|
const entries = Object.entries(info.test_cases ?? {}).sort(([a], [b]) => Number(a) - Number(b))
|
|
let tests = 0
|
|
for (const [, entry] of entries) {
|
|
if (tests >= MAX_TESTS || cases.length >= MAX_CASES) break
|
|
if (!entry.input_name || !entry.output_name) continue
|
|
if (entry.input_size > MAX_TEXT || entry.output_size > MAX_TEXT) continue
|
|
try {
|
|
const directory = resolve(config.testCaseDirectory, problem.testCaseId)
|
|
const input = clean(await readFile(resolve(directory, entry.input_name), "utf8"))
|
|
const expected = clean(await readFile(resolve(directory, entry.output_name), "utf8"))
|
|
if (!input || seen.has(input)) continue
|
|
seen.add(input)
|
|
tests++
|
|
cases.push({ name: `测试 ${cases.length + 1}`, input, expected })
|
|
} catch {
|
|
continue
|
|
}
|
|
}
|
|
return cases
|
|
}
|