上一版让模型拿例子在图上「走一遍」,真模型一试:同一张错图两次一次全对一次 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:
1 parent
099c6d7ba6
commit
e4905fcec1
5 files changed
+680
-257
No files matched your search
@@ -1,31 +1,31 @@
|
||||
/**
|
||||
* 流程图的数据:一棵结构树,不是「一堆框 + 一堆线」。
|
||||
*
|
||||
* 原来的编辑器(vue-flow)存的是自由摆放的节点和学生手拉的连线,能画出断线、孤立的框、
|
||||
* 一个「是」连出去三根线 —— 学生大半时间花在拖框、对连接点、理线上。现在只有三种结构:
|
||||
* 顺序、判断(是 / 否两路,走完合回来)、循环(当型,条件成立就走一遍循环体再回来判断),
|
||||
* 「开始」「结束」固定在两头不进数据。画不出来的只有乱跳的线,中职教的三种结构都画得出。
|
||||
*
|
||||
* 存进数据库(flowchart_submission.flowchart_data,压缩过)和本地草稿的都是 FlowDoc。
|
||||
* 2026-10 之前的提交存的是 vue-flow 的 nodes / edges,parseDoc 认不出来就是 null ——
|
||||
* 那些只能看 Mermaid 渲染出来的图,载不回画布。
|
||||
* 画布上改图的那些操作。图的数据结构(结构树)、读回、转 Mermaid 在契约
|
||||
* `@oj2/contract` 的 flowchart-doc.ts —— 后端评分也要按同一棵树把框对上号。
|
||||
* 这里转出去一份短名字,编辑器和结果页签都从这里引。
|
||||
*/
|
||||
import {
|
||||
countBlankSteps,
|
||||
flowChildren,
|
||||
flowSignature,
|
||||
flowToMermaid,
|
||||
FLOW_END_ID,
|
||||
FLOW_START_ID,
|
||||
mermaidOrder,
|
||||
parseFlowDoc,
|
||||
type FlowDoc,
|
||||
type FlowStep,
|
||||
type StepKind,
|
||||
} from "@oj2/contract"
|
||||
|
||||
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[]
|
||||
export type { BoxKind, FlowDoc, FlowStep, StepKind } from "@oj2/contract"
|
||||
export {
|
||||
countBlankSteps as countBlank,
|
||||
flowSignature as signature,
|
||||
flowToMermaid as toMermaid,
|
||||
FLOW_END_ID as END_ID,
|
||||
FLOW_START_ID as START_ID,
|
||||
mermaidOrder,
|
||||
parseFlowDoc as parseDoc,
|
||||
}
|
||||
|
||||
/** 一个步骤列表在树里的位置:主线,或者某个判断 / 循环的哪一支 */
|
||||
@@ -43,10 +43,6 @@ export interface FlowHighlight {
|
||||
bad: string | null
|
||||
}
|
||||
|
||||
/** 开始、结束在画布上的 id(它们不进数据) */
|
||||
export const START_ID = "__start"
|
||||
export const END_ID = "__end"
|
||||
|
||||
export const STEP_LABEL: Record<StepKind, string> = {
|
||||
input: "输入",
|
||||
output: "输出",
|
||||
@@ -84,7 +80,7 @@ export function findStep(
|
||||
for (let index = 0; index < steps.length; index++) {
|
||||
const step = steps[index]!
|
||||
if (step.id === id) return { step, list: steps, index }
|
||||
for (const child of children(step)) {
|
||||
for (const child of flowChildren(step)) {
|
||||
const found = findStep(child, id)
|
||||
if (found) return found
|
||||
}
|
||||
@@ -92,12 +88,6 @@ export function findStep(
|
||||
return null
|
||||
}
|
||||
|
||||
function children(step: FlowStep): FlowStep[][] {
|
||||
if (step.kind === "if") return [step.yes, step.no]
|
||||
if (step.kind === "while") return [step.body]
|
||||
return []
|
||||
}
|
||||
|
||||
function listAt(doc: FlowDoc, parent: string | null, branch: Branch): FlowStep[] | null {
|
||||
if (parent === null) return branch === "main" ? doc.steps : null
|
||||
const found = findStep(doc.steps, parent)
|
||||
@@ -127,149 +117,7 @@ export function removeStep(doc: FlowDoc, id: string): number {
|
||||
|
||||
export function countSteps(steps: FlowStep[]): number {
|
||||
return steps.reduce(
|
||||
(sum, step) => sum + 1 + children(step).reduce((n, c) => n + countSteps(c), 0),
|
||||
(sum, step) => sum + 1 + flowChildren(step).reduce((n, c) => n + countSteps(c), 0),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
/** 还没写字的框有几个 */
|
||||
export function countBlank(steps: FlowStep[]): number {
|
||||
return steps.reduce(
|
||||
(sum, step) =>
|
||||
sum + (step.text.trim() ? 0 : 1) + children(step).reduce((n, c) => n + countBlank(c), 0),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 一张图「长什么样」,不带 id:载回旧版、和交过的那一版比,只看结构和字。
|
||||
*/
|
||||
export function signature(doc: FlowDoc): string {
|
||||
const strip = (steps: FlowStep[]): unknown[] =>
|
||||
steps.map((step) => [step.kind, step.text.trim(), ...children(step).map(strip)])
|
||||
return JSON.stringify(strip(doc.steps))
|
||||
}
|
||||
|
||||
// ==================== 读回来的数据 ====================
|
||||
|
||||
const KINDS = new Set<string>(["input", "output", "process", "if", "while"])
|
||||
|
||||
function parseSteps(raw: unknown): FlowStep[] | null {
|
||||
if (!Array.isArray(raw)) return null
|
||||
const out: FlowStep[] = []
|
||||
for (const item of raw) {
|
||||
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 : newId()
|
||||
const text = typeof step.text === "string" ? step.text : ""
|
||||
if (kind === "if") {
|
||||
const yes = parseSteps(step.yes ?? [])
|
||||
const no = parseSteps(step.no ?? [])
|
||||
if (!yes || !no) return null
|
||||
out.push({ id, text, kind, yes, no })
|
||||
} else if (kind === "while") {
|
||||
const body = parseSteps(step.body ?? [])
|
||||
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 parseDoc(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)
|
||||
return steps ? { version: 2, steps } : null
|
||||
}
|
||||
|
||||
// ==================== Mermaid ====================
|
||||
|
||||
/**
|
||||
* 交给 AI 的、结果页签和提交列表里渲染缩略图的,都是这份 Mermaid。
|
||||
*
|
||||
* 形状照国标流程图:起止是圆角跑道、输入输出是平行四边形、处理是矩形、判断是菱形。
|
||||
* 原来起止画成圆、输入输出和画布上的样子对不上,AI 还会说「输出框应该用矩形」。
|
||||
* class 行留着给 useMermaid 上色;classDef 不写了 —— 颜色是渲染那边的事,写进来
|
||||
* AI 会把「用了颜色样式」也当成优点打分。
|
||||
*
|
||||
* 学生敲的字直接塞进 "...",一个双引号就会把语法撑破:坏掉的代码还会原样送去打分,
|
||||
* 学生不知道扣分是因为一个引号。`#` 必须先转,否则本来就有的 `#quot;` 会被当成实体。
|
||||
*/
|
||||
function escapeLabel(raw: string) {
|
||||
return raw.replace(/#/g, "#35;").replace(/"/g, "#quot;").replace(/\r?\n/g, "<br/>")
|
||||
}
|
||||
|
||||
/**
|
||||
* Mermaid 里第 n 个框(nN)是画布上的哪一步:开始、各步按先序(判断先「是」再「否」)、结束。
|
||||
* 和下面 toMermaid 建框的顺序一致 —— AI 走例子时报的是 nN,结果页签靠它在画布上亮出路径
|
||||
*/
|
||||
export function mermaidOrder(doc: FlowDoc): string[] {
|
||||
const order = [START_ID]
|
||||
const walk = (steps: FlowStep[]) => {
|
||||
for (const step of steps) {
|
||||
order.push(step.id)
|
||||
for (const child of children(step)) walk(child)
|
||||
}
|
||||
}
|
||||
walk(doc.steps)
|
||||
order.push(END_ID)
|
||||
return order
|
||||
}
|
||||
|
||||
export function toMermaid(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")
|
||||
}
|
||||
Reference in new issue
Block a user