From f919d41199b8fe95f26f1cf148fcc4fe0c8b0293 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 25 Aug 2026 23:15:52 -0600 Subject: [PATCH] =?UTF-8?q?refactor(=E5=A5=91=E7=BA=A6):=20AST=20=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E8=A6=81=E6=B1=82=E6=94=B6=E8=BF=9B=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=EF=BC=8C=E4=B8=89=E4=BB=BD=E5=90=84=E5=86=99=E5=90=84=E7=9A=84?= =?UTF-8?q?=E5=90=88=E6=88=90=E4=B8=80=E4=BB=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 同一个形状原来在三个地方各定义了一份,三份都不一样: apps/api/src/judge/ast.ts engine target outer inner label message exact min max apps/web/src/utils/types.ts engine target message min max AstRulesEditor.vue(本地) engine target label message exact min max ProblemContent.vue(本地) engine target label message exact min max 判题机那份是真的(evaluateRule 按 engine 分支读哪几个字段),契约里则压根没有, `astRules: z.unknown()`。后果是后台编辑器写得出 label 和 exact、判题机也认, 但 utils/types.ts 那个类型描述不了它们 —— 生产库 12 道带 AST 规则的题里, 15 条规则带 label、6 条带 exact,全都在类型之外。 现在契约里一份 astRuleSchema,四处都指向它。engine 收成枚举,列全判题机 实现的十种。**存量数据核过**:12 道题逐条过新 schema,12/12 通过。 顺带三件: - `astRules` 的响应和请求 schema 从 z.unknown() 换成 astRulesSchema。之前 engine 写错一个字母能存进去,判题机 evaluateRule 走 `default: return null` 静默跳过 —— 老师设了规则、规则不生效、没有任何提示。现在保存时就 400, 错误信息把十个合法值列出来。 - judge/run.ts 的 astRulesForLanguage 原来是整片 `rules as AstRule[]` 硬转, 改成逐条 safeParse:认不出的丢掉那一条,行为和 evaluateRule 的 default 分支 一致,只是提前到读取处,也不再骗类型系统。 - 判题机实现了 must_have_nesting,但后台编辑器没有对应选项,目前只能手工造 数据才用得上。枚举里留着并加了注释,没有顺手去补 UI(那是加功能不是清理)。 ## 验证 tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、build 通过。 起服务实打: - 把生产库那条带 label/exact 的规则种进本地库,后台题目详情 200、字段齐全; 原样 PUT 回去 200,库里 label 和 exact 都在(旧类型描述不了的那两个)。 - engine 传 "must_do_magic" → 400,错误信息列出十个合法值。 - 直接调 checkAst:三条规则(两条合法 + 一条 engine 认不出)进去,判题机收下 两条;两个 if/else 的代码 passed=true,一个的 passed=false,描述文案 「if 条件 出现 2 次」正确用上了 label。 冒烟改动已还原(problem 2 的 ast_rules 复位成 null,测试标签删掉)。 Co-Authored-By: Claude Opus 5 --- apps/api/src/judge/ast.ts | 15 ++---- apps/api/src/judge/run.ts | 13 ++++- .../problem/components/AstRulesEditor.vue | 16 ++---- .../oj/problem/components/ProblemContent.vue | 12 +---- apps/web/src/utils/types.ts | 16 +++--- packages/contract/src/admin.ts | 51 ++++++++++++++++++- 6 files changed, 75 insertions(+), 48 deletions(-) diff --git a/apps/api/src/judge/ast.ts b/apps/api/src/judge/ast.ts index 7fc5a4f..2f55fff 100644 --- a/apps/api/src/judge/ast.ts +++ b/apps/api/src/judge/ast.ts @@ -1,3 +1,4 @@ +import type { AstRule } from "@oj2/contract" import { Language, Parser, type Node } from "web-tree-sitter" // 语法 wasm 内嵌成资源。原来是 `Bun.resolveSync(pkg + "/" + name, import.meta.dir)`, // 编译成单二进制后 import.meta.dir 是 /$bunfs/root,解析不到 node_modules。见 vendor/jieba.ts @@ -6,17 +7,9 @@ import pythonWasmPath from "tree-sitter-python/tree-sitter-python.wasm" with { t // web-tree-sitter 自己的运行时 wasm,Parser.init() 要用 import treeSitterWasmPath from "web-tree-sitter/web-tree-sitter.wasm" with { type: "file" } -export interface AstRule { - engine?: string - target?: string - outer?: string - inner?: string - label?: string - message?: string - exact?: number - min?: number - max?: number -} +// AstRule 的形状在契约里(astRuleSchema)—— 原来这份和前端两份各写各的。 +// 这里 re-export,判题机的调用方不用再去 import 契约。 +export type { AstRule } from "@oj2/contract" export interface AstResult { description: string diff --git a/apps/api/src/judge/run.ts b/apps/api/src/judge/run.ts index d2d82f3..b8f7128 100644 --- a/apps/api/src/judge/run.ts +++ b/apps/api/src/judge/run.ts @@ -1,5 +1,6 @@ import { createHash } from "node:crypto" +import { astRuleSchema } from "@oj2/contract" import { and, eq, inArray } from "drizzle-orm" import { config } from "../config" @@ -54,9 +55,19 @@ function templateForLanguage(value: unknown, language: string) { return typeof template === "string" ? template : null } +/** + * 取某个语言的 AST 规则。逐条过 astRuleSchema 而不是整片 `as AstRule[]` 硬转 —— + * 这是从库里读出来的 JSONB,形状不对(比如 engine 是个 evaluateRule 不认识的值) + * 时丢掉那一条,行为和 evaluateRule 的 `default: return null` 一致,只是提前到了 + * 这里、并且不再骗类型系统。 + */ function astRulesForLanguage(value: unknown, language: string): AstRule[] { const rules = objectValue(value)[language] - return Array.isArray(rules) ? (rules as AstRule[]) : [] + if (!Array.isArray(rules)) return [] + return rules.flatMap((rule) => { + const parsed = astRuleSchema.safeParse(rule) + return parsed.success ? [parsed.data] : [] + }) } async function requestJudge( diff --git a/apps/web/src/admin/problem/components/AstRulesEditor.vue b/apps/web/src/admin/problem/components/AstRulesEditor.vue index 3c5aedf..caf906b 100644 --- a/apps/web/src/admin/problem/components/AstRulesEditor.vue +++ b/apps/web/src/admin/problem/components/AstRulesEditor.vue @@ -1,24 +1,14 @@