import { z } from "zod" import { paginatedSchema } from "./common" import { judgeStatusSchema, type JudgeStatus } from "./judge-status" import { problemLanguageSchema, runnableLanguageSchema } from "./language" /** * 判题机原始输出(`submission.info` 的 JSONB 原文)。**只是类型,不作运行时校验。** * * 这里曾经是一组 zod schema,按生产库实测的键集收紧过,结果是 124192 条提交里有 * 9163 条(RE 8480/8480、TLE 338/338、MLE 1/1 全中)被判成不符:沙箱在非正常退出 * 的测试点上写 `output_md5: null`,而 SQL 判题(`judge/sql/engine.ts` 的 CaseResult) * 压根没有 `output` 这个键、`error_message` 通过时是 null。收紧当时只对了键集合, * 没对空值。 * * 更糟的是失败方式:`info` 当时是 `union([完整形状, z.object({})])`,对不上的一律 * 落进第二支被剥成 `{}` 且 parse 成功 —— 管理员的测试点表格**静默消失**。 * * 结论:JSONB 的形状真相在**写入侧**(判题机、`judge/run.ts`),在读出侧再校验一遍 * 只会在两边分叉时丢数据。所以 `info` 回到 `z.unknown()`,形状以下面的 TS 类型 * 描述,取值处由 `submissionCaseResults()` 做一次真正需要的运行时判断(有没有 * data 数组)。**改这里的字段时对着判题机改,不要对着采样出来的键集改。** * * 键名是**判题沙箱定的 snake_case**,不要跟着响应字段一起改。 */ export interface JudgeCaseResult { error: number memory: number /** SQL 判题没有这个键 */ output?: string | null result: JudgeStatus signal: number cpu_time: number exit_code: number real_time: number test_case: string /** 非正常退出的测试点上是 null */ output_md5: string | null /** SQL 判题会带上中文原因(通过的测试点是 null),沙箱判题没有这个键 */ error_message?: string | null score?: number } /** * `info` 的完整形状。实际取值还有第三种:**空对象** —— 后端对非管理员下发 * `info: {}`(`routes/submission.ts` 的 `full ? row.submission.info : {}`), * 也是插入待判提交时的初值。所以调用方不能直接 `.data`。 */ export interface JudgeInfo { err: string | null data: JudgeCaseResult[] | null } /** * 判题产出的统计(`submission.statistic_info` 的 JSONB 原文)。 * * 五个键全部可选,依据是生产库实测的出现次数:time_cost / memory_cost 各 112097、 * score 3993、err_info 3153、ast_results 56,另有 27 条空对象。 * * 用 `looseObject`:所有键可选 + 不剥未知键 = **对任何对象都不会失败、也不丢字段**, * 它在这里的作用是给前端一个能读 `err_info` 的类型,而不是一道闸门。判题产物的 * 闸门在写入侧,理由见上面 `JudgeCaseResult`。 */ export const statisticInfoSchema = z.looseObject({ score: z.number().optional(), /** 判题机写进 statistic_info 的错误文本,教师面板的「最近一条错在哪」也读它 */ err_info: z.string().optional(), time_cost: z.number().optional(), memory_cost: z.number().optional(), ast_results: z .array( z.object({ description: z.string(), passed: z.boolean(), /** count_* 规则实际数到的次数,判题机只在这两个引擎上写 */ actual: z.number().optional(), }), ) .optional(), /** * 运行时错误的诊断,judge/runtime-diagnosis.ts 写,前端翻成中文。只在非比赛提交上有。 * * **不存异常的原文消息**:Python 的消息里常带着测试点的输入(`invalid literal for * int() with base 10: '1 2'`),给学生看就等于放出隐藏数据。所以只存归好类的 * `kind`,和确实出现在学生自己代码里的名字。 */ runtime_error: z .object({ /** 学生代码里的行号(已经扣掉题目模板的前置代码),对不上时为 null */ line: z.number().int().nullable().optional(), /** Python 异常类型,如 ValueError */ type: z.string().optional(), /** 细分,如 int-parse / index / str-concat,见 runtime-diagnosis.ts 的 KINDS */ kind: z.string().optional(), /** NameError / AttributeError / 部分 TypeError 点名的那个名字,只在它出现在学生代码里时才存 */ name: z.string().optional(), /** NameError / 参数名写错的「Did you mean」,Python 从作用域或参数表里挑的,不来自输入 */ suggestion: z.string().optional(), /** C / C++:进程收到的信号(11 段错误、8 除零……)和退出码 */ signal: z.number().int().optional(), exit_code: z.number().int().optional(), }) .optional(), /** * 答案错误时拿题目的**公开样例**重跑的结果,judge/run.ts 的 checkSamples 写。 * 只在非比赛提交上有。 * * 这里的东西全是学生本来就看得到的:样例写在题面上,output 是他自己的程序在 * 公开输入上的输出。隐藏测试点的内容一概不碰。三段文本都截断过(见 SAMPLE_TEXT_LIMIT)。 */ sample_check: z .object({ /** 所有样例都过了(错在隐藏测试点上) */ passed: z.boolean(), /** 第一个没过的样例,0 起;passed 为 true 时没有下面这些 */ index: z.number().int().optional(), input: z.string().optional(), expected: z.string().optional(), output: z.string().optional(), /** 样例上的判题结果:多数是答案错误,也可能在样例上就超时、运行出错 */ result: z.number().int().optional(), }) .optional(), }) /** * 编辑过程的聚合信号,落进 `submission_trace`。**只有计数,不含任何按键内容。** * * 口径是「自上次提交以来」的增量(前端每次提交成功后清零,切题也清零), * 所以同一道题连交几次,每条提交各记各的那一段。 * * 全部来自客户端,**可以伪造** —— 这是接受了的:它只用来给「可信 AC」加权、 * 给老师提示「建议关注」,不单独判任何事。服务端自己算的间隔另见 `since_prev_ms`。 */ export const submissionTraceSchema = z.object({ /** 活跃编辑时长:相邻两次编辑间隔不超过 60 秒才累加,页面不可见时不计 */ activeMs: z.number().int().min(0).max(1e8), /** 打开这道题(或上次提交)到这次提交的墙钟时长 */ sinceOpenMs: z.number().int().min(0).max(1e9), /** 键入、输入法上屏、补全插入的字符数 */ typedChars: z.number().int().min(0).max(1e7), /** 粘贴、从外部拖入的字符数 */ pastedChars: z.number().int().min(0).max(1e7), pasteCount: z.number().int().min(0).max(1e5), /** 单次最大粘贴的字符数 */ maxPaste: z.number().int().min(0).max(1e7), deletedChars: z.number().int().min(0).max(1e7), /** 页面切到后台的次数(切标签页、切窗口、最小化)。只作辅助,别单独拿来说事 */ blurCount: z.number().int().min(0).max(1e5), /** 这一段开始时编辑器里已有的字符数(本地草稿 / 模板 / 上次提交后的代码) */ initialLen: z.number().int().min(0).max(1e7), /** 提交时这道题正在课堂协作中。老师替学生交的那条也会是 true,统计时要排掉 */ collab: z.boolean(), }) export const createSubmissionRequestSchema = z.object({ problemId: z.number().int().positive(), /** * 提交的语言。用题目语言的联合而不是 `z.string()` —— 学生能选的语言就是题目 * `languages` 里列出的那些,写宽松了的话,前端把语言拼错(`"C++"`、`"python3"` * 大小写)会一路走到判题机才以 `Unsupported judge language` 报系统错误, * 学生看到的是「系统错误」而不是「语言不对」。 */ language: problemLanguageSchema, code: z .string() .min(1) .max(1024 * 1024), contestId: z.number().int().positive().optional(), /** * 来源题单。学生从 `/problemset/:id/problem/:pid` 那个入口提交时前端带上, * 后端落进 `submission.problemset_id`,提交列表据此标出「这条是刷题单刷出来的」。 * * 只是**来源标记**,不参与判题、也不参与题单进度记账 —— 进度由判完之后的 * `recordSolvedProblem` 记进所有已加入且含这道题的题单,和从哪个入口进来无关。 * 所以这里带错了顶多是标记不准,不会影响成绩。 */ problemSetId: z.number().int().positive().optional(), /** * 编辑过程信号,见 submissionTraceSchema。**坏了就当没带**(`.catch`): * 整个请求体是一把 safeParse,这里要是能 400,一份附带的统计数据就能挡住 * 学生交作业。刷新过页面、老版本前端、脚本提交都会没有它,那是「无数据」, * 不是「可疑」。 */ trace: submissionTraceSchema.optional().catch(undefined), }) export const createSubmissionResponseSchema = z.object({ submissionId: z.string(), }) /** * 通过了几个测试点,给学生看「离 AC 还差多远」。`info` 只给管理员(每个点带 * output_md5),所以这里只算出两个数下发,不放开原文。 * * 为 null 的情形:比赛提交(ACM 只报对错,多给通过数等于变相放水,和 AI 提示同口径)、 * SQL 题(`judge/run.ts` 遇到被杀的测试点会 break,total 偏小)、没有逐点结果 * (待判、编译失败)。 */ export const caseSummarySchema = z .object({ passed: z.number().int(), total: z.number().int(), }) .nullable() export const submissionDetailSchema = z.object({ id: z.string(), createTime: z.string(), userId: z.number().int(), username: z.string(), code: z.string(), result: judgeStatusSchema, /** 判题机原文;未判完或非管理员看时为 `{}`,见 JudgeInfo 的注释 */ info: z.unknown(), language: problemLanguageSchema, statisticInfo: statisticInfoSchema, contestId: z.number().int().nullable(), problemId: z.number().int(), /** * 题目的展示编号(problem._id)。**独立的 /submission/:id 页面要靠它** —— * 那条路由只喂 submissionID,组件拿不到 display id,而「复制回到题目」要用它 * 拼路由。原来只给内部数字 id,于是那个按钮在这条路由上一点就抛 * `Missing required param "problemID"`。 */ problemDisplayId: z.string(), problemTitle: z.string(), /** 提交者**现在**的班级(和提交列表同口径);已删号的为 null */ className: z.string().nullable(), /** 从哪个题单交的,同列表的 problemSet */ problemSet: z.object({ id: z.number().int(), title: z.string() }).nullable(), /** * false = 自己的这份代码在题单布置期内先藏着(见后端 problemSetLockCutoffs):code 为空串、 * statisticInfo / info 为空对象,其余照给,页面照样摆出是谁、哪道题、什么结果。 * 别人的代码看不到时不走这里,直接 403 */ showLink: z.boolean(), /** 通过了几个测试点,口径见 caseSummarySchema */ caseSummary: caseSummarySchema, /** * 同一个人在同一道题、同一场比赛(或都不在比赛里)交过的每一次,按时间先后。只有带 * `?attempts=1` 时才给(独立的提交详情页用),否则为 null —— 题目页每判完一次都要拉一遍 * 详情,用不着这一串。showLink 的口径同上,题单藏着的那几次点不进去 */ attempts: z .array( z.object({ id: z.string(), createTime: z.string(), result: judgeStatusSchema, showLink: z.boolean(), }), ) .nullable(), }) /** * 判题进度推送。**只带前端真正要用的东西**:靠 submissionId 认领、靠 result / * status 决定是继续等还是去拉详情。 * * 这里曾经还带着 time_cost / memory_cost / err_info —— 从 statistic_info 原样 * 抄一份出来,前端一处都没读过。耗时和错误信息在提交详情里本来就有,判完了去 * 拉一次就是了,不必让推送顺带背一份 JSONB 的形状。 */ export const submissionUpdateSchema = z.object({ type: z.literal("submission_update"), submissionId: z.string(), result: judgeStatusSchema, status: z.enum(["pending", "judging", "finished", "error"]), score: z.number().optional(), }) export const submissionListItemSchema = z.object({ id: z.string(), problemDisplayId: z.string(), problemTitle: z.string(), showLink: z.boolean(), createTime: z.string(), userId: z.number().int(), username: z.string(), /** * 提交人现在的班级(user.class_name),没有班级、已删号的为 null。右栏的「只看这个班」 * 用它 —— 网站配置里的班级列表只有在读的班,摘掉的老班在下拉里选不到,从一条提交 * 点过去就能筛 */ className: z.string().nullable(), result: judgeStatusSchema, language: problemLanguageSchema, statisticInfo: statisticInfoSchema, /** * 来源题单,非题单入口提交的为 null。比赛提交恒为 null(比赛题不会进题单)。 * 历史提交里只有「当年首次 AC 那一条」有值 —— 迁移 0007 从 problemset_submission * 回填的就是这些,其余老提交无从判断入口,一律留空。 */ problemSet: z.object({ id: z.number().int(), title: z.string() }).nullable(), /** * 通过了几个测试点,和详情同一个口径(见 caseSummarySchema)。列表上老师一眼要分出 * 「差一个点」和「一个都没过」,不必逐条点开。 */ caseSummary: caseSummarySchema, }) export const submissionListSchema = paginatedSchema(submissionListItemSchema).extend({ /** * 题号框里填了、但查不到的题号(打错了,或不在这个范围里 —— 比赛列表只认本场的题)。 * 一次填好几道时,打错的那道原来被悄悄忽略,老师看到的列表少一道自己却不知道 */ unknownProblems: z.array(z.string()), }) /** * **一条都没交**的学生。`realName` 是从用户名里剥掉 `ks<班级号>` 前缀后剩下的那一段, * 不是 user.real_name 列 —— 与 F2「真名默认不下发」不冲突:这里只有教师能看到, * 且教师面板的用途正是点名谁没做。 * * 注意它不是「未完成」的全部:交了但一次没对的学生在 `dataAttempted` 里。 */ export const unacceptedStudentSchema = z.object({ username: z.string(), realName: z.string(), }) /** * **交了但一次没对**的学生。这批人原来两栏都不在 —— 不在「完成人数」(没 AC), * 也不在「未完成」名单(那一栏只收一条没交的),于是课堂上最该去看一眼的人 * 反而从屏幕上消失了。`submissionCount` 是窗口内的提交次数,教师据此判断 * 「卡了多久」。 */ export const attemptedStudentSchema = unacceptedStudentSchema.extend({ submissionCount: z.number().int(), /** * 已经解决的题数。查多道题时这一栏里混着「一道没对」和「三道做出两道」两种人, * 差几道决定了老师先管谁 —— 所以名字后面要缀 `2/3`。 */ solvedCount: z.number().int(), }) export const submissionStatisticsUserSchema = z.object({ username: z.string(), className: z.string().nullable(), submissionCount: z.number().int(), /** 通过的**提交条数**。correctRate 的分子就是它 */ acceptedCount: z.number().int(), /** * 解决的**题数**(同一道题重复 AC 只算一道)。表格「已解决」那一列显示的是它 —— * 不指定题号查「这节课全班」时,条数和题数能差出好几倍。 */ solvedCount: z.number().int(), /** 这个人还在判题队列里的条数。`submissionCount` 含它,`correctRate` 的分母不含 */ judgingCount: z.number().int(), // 百分比数值,不带 %。旧后端返回 "85.5%" 字符串,展示格式化交给前端。 correctRate: z.number(), /** * 这个人在本次查询的口径下做完了没有(查了 N 道题就要 N 道都解决)。 * * `data` 里**没做完的人也在**,教师才能在同一张表里展开看他错在哪;「完成人数」 * 和完成度算的是 `done` 为真的那些,不是 `data.length`。 */ done: z.boolean(), }) export const submissionStatisticsGridSchema = z.object({ /** 传了题号就按传的顺序;没传就是范围内出现过的题,按第一次有人交的时间排 */ problems: z.array(z.object({ problemDisplayId: z.string(), title: z.string() })), rows: z.array( z.object({ username: z.string(), /** 剥掉 `ks<班级号>` 前缀后的那段,同 unacceptedStudentSchema.realName */ realName: z.string(), className: z.string().nullable(), /** 从早到晚 */ submissions: z.array( z.object({ id: z.string(), problemDisplayId: z.string(), result: judgeStatusSchema, createTime: z.string(), }), ), }), ), classSizes: z.record(z.string(), z.number().int()), truncated: z.boolean(), }) export const submissionStatisticsSchema = z.object({ submissionCount: z.number().int(), acceptedCount: z.number().int(), /** * 还没判完的条数(PENDING / JUDGING)。`submissionCount` 把它算在内, * `correctRate` 的分母不算 —— 全班同时交卷的那几秒,分母涨了分子没涨, * 正确率会凭空掉一截。下发它是为了让教师看得出「那几条还在判」。 */ judgingCount: z.number().int(), correctRate: z.number(), // 花名册人数(未禁用的普通用户)。**只有这一个分母下发**:完成度由前端算, // 因为「请假隐藏」会把请假的人从分母里减掉,那是后端不知道的浏览器本地状态。 personCount: z.number().int(), /** 窗口里交过东西的所有人(做没做完看 `done`),按提交数倒序 */ data: z.array(submissionStatisticsUserSchema), /** 一条都没交的(花名册里的人减去有提交的人) */ dataUnaccepted: z.array(unacceptedStudentSchema), /** * 交了但没做完的(一道没对,或者查三道只做出两道)。 * * 传了用户名时按花名册取,和 dataUnaccepted 同一个范围;不传用户名时没有花名册, * 退回「窗口内有提交但没做完的全部普通学生」—— 否则这批人两栏都不在,看起来 * 就像统计只认成功的提交。dataUnaccepted 没有花名册就真的算不出来,仍然为空。 */ dataAttempted: z.array(attemptedStudentSchema), /** 按判题结果,条数倒序 —— 数字行里那根结果条(含还在判的) */ results: z.array(z.object({ result: judgeStatusSchema, count: z.number().int() })), }) /** * 班级筛选(统计、提交列表、流程图的 `className` 参数)里代表「没填班级」的取值: * 这批号(夏令营、兴趣班、自己注册的)`user.class_name` 是 null,当成一个班来看。 * 真班名全是数字(`253`、`2511`),撞不上 */ export const NO_CLASS = "none" /** * 统计「一行一节课」的总览(GET /submissions/statistics/lessons,设计稿「统计合二为一」B)。 * 没选班、没填学生、没填题号时用它;原来「今日统计」老师那一版并进来了,换成任意时间段。 * * **一节课怎么切**:只算普通学生(和方块串同一个口径),按「班 + 东八区哪一天」分组, * 相邻两条提交隔 30 分钟以上就切开;切出来的一段里有一道题 5 人以上做过,才算一节课。 * 没填班级的号合在一起当一个班切(`className` 为 null)。 * 其余的是零散提交(补做、课后自己练)。这样 26计算机0班 9 月 29 日是 14:04–15:38, * 不会被 16:24、18:42 那两条课后提交拖成「14:04–18:42」。 */ const lessonFailureSchema = z.object({ result: judgeStatusSchema, count: z.number().int() }) export const submissionLessonsSchema = z.object({ /** 最近的 N 节课,按开始时间倒序(界面上按天分组,这节课在最上面) */ lessons: z.array( z.object({ /** null = 没填班级的那批号,点进去按 NO_CLASS 筛 */ className: z.string().nullable(), /** 东八区的日子,`2026-09-29` */ day: z.string(), /** 这一段第一条、最后一条提交 */ start: z.string(), end: z.string(), userCount: z.number().int(), /** 班里的学生数(未禁用的普通用户)。没填班级的谈不上花名册,给 0 */ classSize: z.number().int(), /** 这节课做的题(这一段里 5 人以上做过的),做的人多的在前,最多 20 道 —— 点进去就筛这几道 */ problems: z.array(z.string()), /** 多半是从题单里交的 —— 「题单 7 道」 */ fromProblemSet: z.boolean(), /** 这节课的题(上面 problems)交了几次 —— 和点进去看到的对得上 */ total: z.number().int(), /** 同上,判完的里面对了多少,百分数 */ correctRate: z.number(), /** 最后一条在 15 分钟内:还在上课,界面上引去课堂看板 */ live: z.boolean(), /** 这节课的题里没过次数最多的那道;一次都没错过为 null */ hardest: z .object({ problemDisplayId: z.string(), problemTitle: z.string(), failed: z.number().int(), }) .nullable(), }), ), /** 零散提交,按天、按班(null = 没填班级的号)。只给列出来的这些课覆盖到的日子 */ scattered: z.array( z.object({ day: z.string(), className: z.string().nullable(), userCount: z.number().int(), total: z.number().int(), }), ), /** 整个时间段里错得最多的题,按没过的次数倒序,最多 6 道 */ hardProblems: z.array( z.object({ problemDisplayId: z.string(), problemTitle: z.string(), /** 这道题交得最多的那个班 */ className: z.string().nullable(), total: z.number().int(), accepted: z.number().int(), failures: z.array(lessonFailureSchema), userCount: z.number().int(), acceptedUsers: z.number().int(), }), ), /** 更早还有课没列出来(「这学期」「全部」那种),界面上给「再往前看」 */ hasMore: z.boolean(), }) /** * 提交列表那颗「今日提交数」标签点开之后的统计弹框(GET /submissions/today-statistics)。 * * **公开接口,只下发聚合数** —— 没有用户名、没有代码、没有隐藏题目的标题,学生和 * 匿名访客看到的和教师一样。教师那套按班级/按题号钻取的口径在 * `submissionStatisticsSchema`,两者不是一回事,别把这个当它的简版去加字段。 * * 口径跟着那颗标签走:**东八区今天、非比赛提交、不分语言**(语言分布就是 * `languages` 这张表本身)。 */ const todayFailureSchema = z.object({ result: judgeStatusSchema, count: z.number().int() }) /** * 「今日统计」(提交列表顶上那颗标签点开的),**只剩学生版**:自己今天怎样、大家在做哪几道。 * 老师那一版(今天哪几个班上了课、错得最多的题)2026-09 并进了统计(submissionLessonsSchema), * 老师的提交列表上不再有这颗标签。口径:东八区今天 + 非比赛提交。 */ export const todaySubmissionStatisticsSchema = z.object({ /** 这份数据是几点算的 —— 标题上的「截至 19:40」 */ asOf: z.string(), total: z.number().int(), /** 通过的条数,含 AST_CHECK_FAILED(那也是答案对了) */ accepted: z.number().int(), /** * 还没判完的条数(PENDING / JUDGING)。`total` 把它算在内,`correctRate` 的分母 * 不算 —— 全班同时交卷的那几秒,分母涨了分子没涨,正确率会凭空掉一截。 */ judging: z.number().int(), correctRate: z.number(), /** 今天交过东西的人数,按 user_id 去重 */ userCount: z.number().int(), /** 今天画了几张流程图,0 就不提 */ flowchartCount: z.number().int(), /** 按语言,提交数倒序。只有一种语言时界面上缩成一句话 */ languages: z.array(z.object({ language: problemLanguageSchema, count: z.number().int() })), /** 按判题结果,条数倒序 —— 画成底部那根结果条 */ results: z.array(todayFailureSchema), /** 我今天交了几次、做对几道。没登录为 null */ me: z.object({ total: z.number().int(), solved: z.number().int() }).nullable(), /** * 学生版「今天大家都在做」:按做的人数倒序,最多 8 道。**只含公开可见的题目** —— * 这个接口不需要登录,不能拿它探未发布题目的标题。「提交列表对学生全开」关掉时为空 */ problems: z.array( z.object({ problemDisplayId: z.string(), problemTitle: z.string(), userCount: z.number().int(), acceptedUsers: z.number().int(), /** 我自己今天在这道题上:做对了 / 交了没对 / 没交;没登录为 null */ mine: z.enum(["accepted", "tried", "none"]).nullable(), }), ), }) export const formatCodeRequestSchema = z.object({ code: z.string().max(1024 * 1024), language: z.enum(["python", "c", "cpp", "sql"]), /** C / C++ 格式化前要先编一遍,得套上这道题的模板才编得过 */ problemId: z.number().int().optional(), }) export const formatCodeResponseSchema = z.object({ code: z.string() }) /** 试运行一次最多几组。学生那边只有「运行例子」(一般两三个)和「自己输入」(一个) */ export const TRIAL_MAX_CASES = 10 /** 后台「生成测试点」一次跑的组数上限,只有管理员能用到 */ export const TRIAL_ADMIN_MAX_CASES = 50 /** * 试运行:「运行例子」「自己输入」、后台「生成测试点」。走本站判题机,不落库、不算提交。 * * - 带 `problemId`:按这道题的时间 / 内存限制和代码模板跑,和提交一个口径; * - 带 `tutorialId`:自学教程正文里的代码块「运行」,只能跑一组、限制用默认的; * - 都不带:只有管理员能这么调(后台生成测试点时题目可能还没存)。 * * `output` 是这组的正确输出,给了就由判题机比对(和提交一样去掉末尾空白再比), * 结果是通过 / 答案错误;不给(「自己输入」、生成测试点)就只看有没有跑出错。 */ export const trialRunRequestSchema = z.object({ problemId: z.number().int().positive().optional(), contestId: z.number().int().positive().optional(), tutorialId: z.number().int().positive().optional(), language: runnableLanguageSchema, code: z .string() .min(1) .max(1024 * 1024), cases: z .array( z.object({ input: z.string().max(1024 * 1024), output: z .string() .max(1024 * 1024) .optional(), }), ) .min(1) .max(TRIAL_ADMIN_MAX_CASES), }) /** * - `done`:每组一条,顺序和请求里的 `cases` 一致。`result` 是判题机那套码; * 没给正确输出的组,跑完没出错就是通过。`output` 里 stdout 和 stderr 是混在一起的 * (判题机把两者写进同一个文件),Python 的报错原文就在这里。运行时错误的组另带 * `runtimeError`:和提交的 `statistic_info.runtime_error` 同一个解析(第几行、什么异常), * 解析不出来时没有这个字段。 * - `compile-error`:编译没过(Python 是语法错误),一组都没跑。 * - `too-much-output`:程序输出多到判题机回包超过上限,多半是死循环在不停打印。 */ export type TrialRunResponse = | { status: "done" cases: { result: JudgeStatus output: string runtimeError?: NonNullable }[] } | { status: "compile-error"; message: string } | { status: "too-much-output" } export type StatisticInfo = z.infer export type SubmissionTrace = z.infer export type CreateSubmissionRequest = z.infer export type SubmissionDetail = z.infer export type SubmissionUpdate = z.infer export type SubmissionStatistics = z.infer export type TodaySubmissionStatistics = z.infer export type SubmissionLessons = z.infer export type SubmissionStatisticsUser = z.infer export type SubmissionStatisticsGrid = z.infer export type UnacceptedStudent = z.infer export type AttemptedStudent = z.infer export type SubmissionListItem = z.infer export type SubmissionList = z.infer export type CreateSubmissionResponse = z.infer export type FormatCodeResponse = z.infer export type FormatCodeRequest = z.infer export type TrialRunRequest = z.infer