fix(流程图): AI 点评不再让模型在脑子里走图——拼成程序在判题机上跑,同一张图两次结果一样
Deploy / deploy (push) Canceled after 0s

上一版让模型拿例子在图上「走一遍」,真模型一试:同一张错图两次一次全对一次 C
(看得到应该输出时它会照抄),365 圈的循环多算一圈也判对。现在:
- 结构照学生的结构树拼,AI 只把每个框里的字翻成一句 Python,改不了结构、补不了漏的步骤
- 拼好的程序在本站判题机上跑例子和测试数据,逐行比输出:空格、标点、全角半角不算错,
  题目输出里固定的提示文字(「较大的数是: 」)框里没写也不算错
- 走过哪些框、每行是哪个框输出的在程序里记下来,画布照旧亮路径、标出错的框
- 没走对的原因由代码说:多输出了一行「一级果」、输出的是 38.16 应该是 37.78、
  「输入有无瑕疵 f」读到的数据不对、走不到结束(死循环)
- 结构树挪进契约(flowchart-doc.ts),前端画图、后端评分用同一份

用真模型(deepseek-v4-flash)对 9 张图各评两次,结果两次一致、对错都判对,每张一秒左右。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-12 21:22:15 +08:00
1 parent 099c6d7ba6
commit e4905fcec1
5 files changed
+680 -257

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
/**
* 流程图的数据:一棵结构树,不是「一堆框 + 一堆线」。前端画、后端评分都按它。
*
* 原来的编辑器(vue-flow)存的是自由摆放的节点和学生手拉的连线,能画出断线、孤立的框、
* 一个「是」连出去三根线 —— 学生大半时间花在拖框、对连接点、理线上。现在只有三种结构:
* 顺序、判断(是 / 否两路,走完合回来)、循环(当型,条件成立就走一遍循环体再回来判断),
* 「开始」「结束」固定在两头不进数据。画不出来的只有乱跳的线,中职教的三种结构都画得出。
*
* 存进数据库(flowchart_submission.flowchart_data,压缩过)和本地草稿的都是 FlowDoc。
* 2026-10 之前的提交存的是 vue-flow 的 nodes / edges,parseDoc 认不出来就是 null ——
* 那些只能看 Mermaid 渲染出来的图,载不回画布,评分也只能退回老办法。
*/
export type BoxKind = "input" | "output" | "process"
export type StepKind = BoxKind | "if" | "while"
interface StepBase {
id: string
text: string
}
export type BoxStep = StepBase & { kind: BoxKind }
export type IfStep = StepBase & { kind: "if"; yes: FlowStep[]; no: FlowStep[] }
export type WhileStep = StepBase & { kind: "while"; body: FlowStep[] }
export type FlowStep = BoxStep | IfStep | WhileStep
export interface FlowDoc {
version: 2
steps: FlowStep[]
}
/** 开始、结束在画布上的 id(它们不进数据) */
export const FLOW_START_ID = "__start"
export const FLOW_END_ID = "__end"
export function flowChildren(step: FlowStep): FlowStep[][] {
if (step.kind === "if") return [step.yes, step.no]
if (step.kind === "while") return [step.body]
return []
}
/** 还没写字的框有几个 */
export function countBlankSteps(steps: FlowStep[]): number {
return steps.reduce(
(sum, step) =>
sum +
(step.text.trim() ? 0 : 1) +
flowChildren(step).reduce((n, c) => n + countBlankSteps(c), 0),
0,
)
}
/** 一张图「长什么样」,不带 id:载回旧版、和交过的那一版比,只看结构和字 */
export function flowSignature(doc: FlowDoc): string {
const strip = (steps: FlowStep[]): unknown[] =>
steps.map((step) => [step.kind, step.text.trim(), ...flowChildren(step).map(strip)])
return JSON.stringify(strip(doc.steps))
}
// ==================== 读回来的数据 ====================
const KINDS = new Set<string>(["input", "output", "process", "if", "while"])
function parseSteps(raw: unknown, prefix: string): FlowStep[] | null {
if (!Array.isArray(raw)) return null
const out: FlowStep[] = []
for (const [index, item] of raw.entries()) {
if (!item || typeof item !== "object") return null
const step = item as Record<string, unknown>
const kind = step.kind
if (typeof kind !== "string" || !KINDS.has(kind)) return null
const id = typeof step.id === "string" && step.id ? step.id : `${prefix}${index}`
const text = typeof step.text === "string" ? step.text : ""
if (kind === "if") {
const yes = parseSteps(step.yes ?? [], `${id}y`)
const no = parseSteps(step.no ?? [], `${id}n`)
if (!yes || !no) return null
out.push({ id, text, kind, yes, no })
} else if (kind === "while") {
const body = parseSteps(step.body ?? [], `${id}b`)
if (!body) return null
out.push({ id, text, kind, body })
} else {
out.push({ id, text, kind: kind as BoxKind })
}
}
return out
}
/** 本地草稿、交过的某一版:是新格式就读出来,旧的 vue-flow 格式或者坏数据给 null */
export function parseFlowDoc(raw: unknown): FlowDoc | null {
if (!raw || typeof raw !== "object") return null
const data = raw as Record<string, unknown>
if (data.version !== 2) return null
const steps = parseSteps(data.steps, "p")
return steps ? { version: 2, steps } : null
}
// ==================== Mermaid ====================
/**
* Mermaid 里第 n 个框(nN)是哪一步:开始、各步按先序(判断先「是」再「否」)、结束。
* 和 flowToMermaid 建框的顺序一致 —— 评分报的是 nN,前端靠它在画布上亮出路径
*/
export function mermaidOrder(doc: FlowDoc): string[] {
const order = [FLOW_START_ID]
const walk = (steps: FlowStep[]) => {
for (const step of steps) {
order.push(step.id)
for (const child of flowChildren(step)) walk(child)
}
}
walk(doc.steps)
order.push(FLOW_END_ID)
return order
}
/**
* 学生敲的字直接塞进 "...",一个双引号就会把语法撑破:坏掉的代码还会原样送去打分,
* 学生不知道扣分是因为一个引号。`#` 必须先转,否则本来就有的 `#quot;` 会被当成实体。
*/
function escapeLabel(raw: string) {
return raw.replace(/#/g, "#35;").replace(/"/g, "#quot;").replace(/\r?\n/g, "<br/>")
}
/**
* 结果页签和提交列表里的缩略图、老的四项分评法用的都是这份 Mermaid。
*
* 形状照国标流程图:起止是圆角跑道、输入输出是平行四边形、处理是矩形、判断是菱形。
* class 行留着给前端 useMermaid 上色;classDef 不写 —— 颜色是渲染那边的事。
*/
export function flowToMermaid(doc: FlowDoc): string {
const lines: string[] = ["flowchart TD"]
const classes: string[] = []
let n = 0
const node = (shape: (label: string) => string, text: string, cls: string) => {
const id = `n${n++}`
lines.push(` ${id}${shape(escapeLabel(text.trim() || " "))}`)
classes.push(` class ${id} ${cls}`)
return id
}
/** 还没接上下一步的出口:从哪个框出来、线上写什么 */
type Exit = { from: string; label?: string }
const link = (exits: Exit[], to: string) => {
for (const exit of exits) {
lines.push(
exit.label ? ` ${exit.from} -->|"${exit.label}"| ${to}` : ` ${exit.from} --> ${to}`,
)
}
}
const walk = (steps: FlowStep[], entries: Exit[]): Exit[] => {
let exits = entries
for (const step of steps) {
if (step.kind === "if") {
const id = node((l) => `{"${l}"}`, step.text, "decision")
link(exits, id)
exits = [
...walk(step.yes, [{ from: id, label: "是" }]),
...walk(step.no, [{ from: id, label: "否" }]),
]
} else if (step.kind === "while") {
const id = node((l) => `{"${l}"}`, step.text, "decision")
link(exits, id)
link(walk(step.body, [{ from: id, label: "是" }]), id)
exits = [{ from: id, label: "否" }]
} else {
const shape =
step.kind === "process" ? (l: string) => `["${l}"]` : (l: string) => `[/"${l}"/]`
const id = node(shape, step.text, step.kind)
link(exits, id)
exits = [{ from: id }]
}
}
return exits
}
const start = node((l) => `(["${l}"])`, "开始", "startNode")
const exits = walk(doc.steps, [{ from: start }])
const end = node((l) => `(["${l}"])`, "结束", "endNode")
link(exits, end)
return [...lines, "", ...classes].join("\n")
}
+1
View File
@@ -8,6 +8,7 @@ export * from "./common"
export * from "./content"
export * from "./contest"
export * from "./flowchart"
export * from "./flowchart-doc"
export * from "./judge-status"
export * from "./language"
export * from "./problem"