feat(题目): 运行时错误告诉学生第几行、为什么出错(Python 看回溯,C 看信号),不泄露测试数据

原来学生只看到「运行时错误」四个字:主判题 output: false,Python 回溯没回来;
C 的信号值在 info 里,而 info 只给管理员。

- Python 判出 RE 时,拿第一个失败测试点单独重跑一次(内联测试点 + output: true),
  从回溯取行号(扣掉模板前置行)、异常类型和按句式归的类别。主判题仍不开 output,
  免得每个点最多 16MB 的输出回到 worker
- C / C++ 取失败点的信号和退出码(段错误提示 scanf 漏 &、return 非 0 等)
- 写进 statistic_info.runtime_error;不存异常原文(里面常带测试点输入),名字只在
  出现在学生代码里时才存——raise Exception(input()) 实测只落下类型
- 前端按类型翻成中文、摆出出错那行并在编辑器标红;比赛提交不诊断(ACM 只报对错)
- 没有诊断的 RE 不再说「代码格式写错了」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 22:01:30 +08:00
1 parent 2a4405d2ea
commit db70ab62e8
7 files changed
+391 -11

No files matched your search

+72 -3
View File
@@ -19,6 +19,11 @@ import { judgeConfigFor } from "./languages"
import { isAccepted, JudgeStatus, type JudgeStatusValue } from "./status" import { isAccepted, JudgeStatus, type JudgeStatusValue } from "./status"
import { parseProblemTemplate } from "./template" import { parseProblemTemplate } from "./template"
import { runSqlCase } from "./sql" import { runSqlCase } from "./sql"
import {
nativeRuntimeError,
parsePythonTraceback,
type RuntimeErrorInfo,
} from "./runtime-diagnosis"
import { readInfo } from "../services/test-case" import { readInfo } from "../services/test-case"
import { readFile } from "node:fs/promises" import { readFile } from "node:fs/promises"
import { resolve as resolvePath } from "node:path" import { resolve as resolvePath } from "node:path"
@@ -63,12 +68,18 @@ function astRulesForLanguage(value: unknown, language: string): AstRule[] {
}) })
} }
/**
* `testCase` 是测试点目录名(正常判题),或者内联的测试点(诊断重跑,见
* diagnoseRuntimeError)。`output` 只在诊断时打开:它让判题机把每个测试点的
* 程序输出原样带回来,正常判题开着的话,死循环打印能把 worker 内存撑爆。
*/
async function requestJudge( async function requestJudge(
language: string, language: string,
code: string, code: string,
timeLimit: number, timeLimit: number,
memoryLimit: number, memoryLimit: number,
testCaseId: string, testCase: string | { input: string; output: string }[],
output = false,
) { ) {
const languageConfig = judgeConfigFor(language) const languageConfig = judgeConfigFor(language)
if (!languageConfig) throw new Error(`Unsupported judge language: ${language}`) if (!languageConfig) throw new Error(`Unsupported judge language: ${language}`)
@@ -85,8 +96,8 @@ async function requestJudge(
src: code, src: code,
max_cpu_time: timeLimit, max_cpu_time: timeLimit,
max_memory: 1024 * 1024 * memoryLimit, max_memory: 1024 * 1024 * memoryLimit,
test_case_id: testCaseId, ...(typeof testCase === "string" ? { test_case_id: testCase } : { test_case: testCase }),
output: false, output,
io_mode: { io_mode: {
io_mode: "Standard IO", io_mode: "Standard IO",
input: "input.txt", input: "input.txt",
@@ -324,6 +335,48 @@ export async function failAbandonedSubmission(submissionId: string, error: unkno
await markSystemError(submissionId, row.userId, error) await markSystemError(submissionId, row.userId, error)
} }
/**
* 运行时错误的诊断,结果写进 `statistic_info.runtime_error`(字段说明见契约)。
*
* C / C++ 直接用失败测试点的信号和退出码。Python 要拿回溯,得**重跑那一个测试点**:
* 主判题不开 output(理由见 requestJudge),所以这里读出第一个失败测试点的输入,
* 以内联测试点的形式单独再跑一次、打开 output。同样的代码、同样的输入,抛的是
* 同一个异常。只多跑一个点,而且只在 Python 判出运行时错误时才跑。
*
* 诊断失败(读不到测试点、判题机没回回溯)就不写这个字段,学生看到的和原来一样。
*/
async function diagnoseRuntimeError(
row: {
submission: typeof schema.submission.$inferSelect
problem: typeof schema.problem.$inferSelect
},
source: string,
prependLines: number,
failed: JudgeCase,
): Promise<RuntimeErrorInfo | null> {
if (row.submission.language !== "Python")
return nativeRuntimeError(failed.signal, failed.exit_code)
const info = await readInfo(row.problem.testCaseId)
const inputName = info?.test_cases?.[failed.test_case]?.input_name
if (!inputName) return null
const input = await readFile(
resolvePath(config.testCaseDirectory, row.problem.testCaseId, inputName),
"utf8",
)
const response = await requestJudge(
row.submission.language,
source,
row.problem.timeLimit,
row.problem.memoryLimit,
[{ input, output: "" }],
true,
)
const output = Array.isArray(response.data) ? response.data[0]?.output : null
if (typeof output !== "string") return null
return parsePythonTraceback(output, row.submission.code, prependLines)
}
export async function judgeSubmission(job: JudgeJobData) { export async function judgeSubmission(job: JudgeJobData) {
const [row] = await db const [row] = await db
.select({ .select({
@@ -404,6 +457,22 @@ export async function judgeSubmission(job: JudgeJobData) {
)?.error_message )?.error_message
if (typeof failedMessage === "string") statisticInfo.err_info = failedMessage if (typeof failedMessage === "string") statisticInfo.err_info = failedMessage
// 比赛不诊断:ACM 只报对错,和「通过几个测试点」、AI 提示不给比赛提交同一个口径
if (
result === JudgeStatus.RUNTIME_ERROR &&
firstFailure &&
row.submission.contestId === null
) {
const prependLines = template ? template.prepend.split("\n").length : 0
const diagnosis = await diagnoseRuntimeError(row, source, prependLines, firstFailure).catch(
(error: unknown) => {
console.warn(`[judge] 运行时错误诊断失败 ${row.submission.id}:`, error)
return null
},
)
if (diagnosis) statisticInfo.runtime_error = diagnosis
}
if (result === JudgeStatus.ACCEPTED) { if (result === JudgeStatus.ACCEPTED) {
const rules = astRulesForLanguage(row.problem.astRules, row.submission.language) const rules = astRulesForLanguage(row.problem.astRules, row.submission.language)
if (rules.length > 0) { if (rules.length > 0) {
+110
View File
@@ -0,0 +1,110 @@
import type { StatisticInfo } from "@oj2/contract"
/**
* 运行时错误的诊断:把判题机的原始结果收成 `statistic_info.runtime_error`,
* 前端再翻成中文(apps/web/src/oj/problem/utils/runtimeError.ts)。
*
* 原来学生拿到的只有「运行时错误」四个字:主判题是 `output: false`,Python 的回溯
* 根本没回来;C 的信号值倒是有,但在 `info` 里,而 `info` 只给管理员。
*
* Python 的回溯要靠**重跑一次**拿(run.ts 的 diagnoseRuntimeError):只拿第一个
* 失败的测试点、打开 output 单独跑。不给主判题开 output —— 那样每个测试点的完整
* 输出(每个最多 16MB)都会回到 worker,一个死循环打印就能把内存撑爆。
*
* **原文消息一律不存**。Python 的异常消息里常带着测试点的输入,比如
* `invalid literal for int() with base 10: '1 2'`,给学生看就等于把隐藏数据放出去 ——
* 故意写 `raise Exception(input())` 就能一个点一个点地套。所以只存:
* - 异常类型和归好类的 `kind`(按消息的**句式**判,不取消息里的值)
* - NameError / AttributeError 点名的名字,而且只在它**原样出现在学生代码里**时才存
*/
export type RuntimeErrorInfo = NonNullable<StatisticInfo["runtime_error"]>
const IDENTIFIER = /^[A-Za-z_]\w*$/
/** 按消息句式细分。键是异常类型,值是 [句式, kind],从上往下取第一个对上的 */
const KINDS: Record<string, [RegExp, string][]> = {
ValueError: [
[/^invalid literal for int\(\)/, "int-parse"],
[/^could not convert string to float/, "float-parse"],
[/^(not enough|too many) values to unpack/, "unpack"],
[/^math domain error/, "math-domain"],
],
TypeError: [
[/^can only concatenate str/, "str-concat"],
[/^unsupported operand type/, "operand"],
[/cannot be interpreted as an integer/, "not-int"],
[/not supported between instances of/, "compare"],
[/object is not callable/, "not-callable"],
[/object is not subscriptable/, "not-subscriptable"],
[/missing \d+ required positional argument/, "missing-arg"],
[/^(list|string|tuple) indices must be integers/, "index-type"],
],
IndexError: [[/out of range/, "index"]],
}
/** 名字在学生自己的代码里原样出现过才算数,否则它可能来自输入(`eval(input())`) */
function nameFromCode(name: string | undefined, code: string) {
if (!name || !IDENTIFIER.test(name)) return undefined
return new RegExp(`\\b${name}\\b`).test(code) ? name : undefined
}
/**
* 从程序输出里解析 Python 回溯。stdout 和 stderr 在判题机里写的是同一个文件,
* 所以回溯接在正常输出后面;只看末尾,取最后一段 Traceback(链式异常取最外层)。
*
* `prependLines` 是题目模板拼在学生代码前面的行数:判题跑的是
* `${prepend}\n${code}\n${append}`,回溯里的行号要扣掉它才对得上编辑器。
*/
export function parsePythonTraceback(
output: string,
code: string,
prependLines: number,
): RuntimeErrorInfo | null {
const tail = output.slice(-16_000)
const start = tail.lastIndexOf("Traceback (most recent call last):")
if (start < 0) return null
const lines = tail.slice(start).trimEnd().split("\n")
const last = lines[lines.length - 1]!
const head = last.match(/^([A-Za-z_][\w.]*)(?::\s?(.*))?$/)
if (!head) return null
const type = head[1]!.split(".").pop()!
const message = head[2] ?? ""
// 最后一个落在 solution.py 里的栈帧:错误发生在标准库里(statistics.mean([]) 之类)时,
// 最后一帧是库文件,学生要看的是自己调它的那一行
let sourceLine: number | null = null
for (const line of lines) {
const frame = line.match(/^\s*File "([^"]*)", line (\d+)/)
if (frame && frame[1]!.endsWith("solution.py")) sourceLine = Number(frame[2])
}
const codeLines = code.split("\n").length
const lineInCode = sourceLine === null ? null : sourceLine - prependLines
const info: RuntimeErrorInfo = {
line: lineInCode !== null && lineInCode >= 1 && lineInCode <= codeLines ? lineInCode : null,
type,
}
const kind = KINDS[type]?.find(([pattern]) => pattern.test(message))?.[1]
if (kind) info.kind = kind
if (type === "NameError" || type === "UnboundLocalError") {
const match = message.match(/^(?:name|cannot access local variable) '([^']+)'/)
const name = nameFromCode(match?.[1], code)
if (name) info.name = name
const suggestion = message.match(/Did you mean: '([^']+)'\?/)?.[1]
if (suggestion && IDENTIFIER.test(suggestion)) info.suggestion = suggestion
} else if (type === "AttributeError") {
const name = nameFromCode(message.match(/has no attribute '([^']+)'/)?.[1], code)
if (name) info.name = name
}
return info
}
/** C / C++:没有回溯,信号和退出码就是全部线索。两个都为 0 时没什么可说的 */
export function nativeRuntimeError(signal: unknown, exitCode: unknown): RuntimeErrorInfo | null {
const info: RuntimeErrorInfo = {}
if (typeof signal === "number" && signal > 0) info.signal = signal
if (typeof exitCode === "number" && exitCode !== 0) info.exit_code = exitCode
return info.signal || info.exit_code ? info : null
}
@@ -63,7 +63,7 @@ onUnmounted(clearErrorMark)
</script> </script>
<template> <template>
<n-card embedded> <n-card embedded class="explain-card">
<n-flex vertical :size="12"> <n-flex vertical :size="12">
<template v-if="explanation"> <template v-if="explanation">
<div class="explain"> <div class="explain">
@@ -95,6 +95,11 @@ onUnmounted(clearErrorMark)
</template> </template>
<style scoped> <style scoped>
/* 结果弹窗不限宽,长句子不折行会把弹窗撑出屏幕 */
.explain-card {
max-width: 560px;
}
.explain { .explain {
font-size: 16px; font-size: 16px;
line-height: 1.7; line-height: 1.7;
@@ -0,0 +1,67 @@
<script setup lang="ts">
import { clearErrorMark, showErrorMark } from "oj/problem/utils/errorMark"
import { explainRuntimeError, type RuntimeErrorInfo } from "oj/problem/utils/runtimeError"
/**
* 运行时错误的中文说明卡片(诊断由后端 judge/runtime-diagnosis.ts 给)。
* 知道是第几行的,就把那一行代码摆出来,并在编辑器里标红;卸掉时清掉标记。
*/
const props = defineProps<{
info: RuntimeErrorInfo
/** 这次提交的代码,用来摆出出错的那一行 */
code: string
}>()
const message = computed(() => explainRuntimeError(props.info))
const sourceLine = computed(() => {
const line = props.info.line
if (!line) return null
const text = props.code.split("\n")[line - 1]
return text?.trim() ? text.trim() : null
})
watch(
() => props.info.line,
(line) => {
if (line) showErrorMark(line, null, null)
else clearErrorMark()
},
{ immediate: true },
)
onUnmounted(clearErrorMark)
</script>
<template>
<n-card embedded class="explain-card">
<n-flex vertical :size="12">
<div class="explain">
<b v-if="info.line">第 {{ info.line }} 行运行时出错:</b>{{ message }}
</div>
<pre v-if="sourceLine" class="explain-code">{{ sourceLine }}</pre>
</n-flex>
</n-card>
</template>
<style scoped>
/* 结果弹窗不限宽,长句子不折行会把弹窗撑出屏幕 */
.explain-card {
max-width: 560px;
}
.explain {
font-size: 16px;
line-height: 1.7;
}
.explain-code {
margin: 0;
padding: 8px 12px;
border-radius: 4px;
font-size: 15px;
white-space: pre-wrap;
word-break: break-all;
background-color: rgba(208, 48, 80, 0.12);
}
</style>
@@ -15,6 +15,7 @@ import type { Submission } from "utils/types"
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue" import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import { useProblemStore } from "oj/store/problem" import { useProblemStore } from "oj/store/problem"
import PythonErrorExplain from "./PythonErrorExplain.vue" import PythonErrorExplain from "./PythonErrorExplain.vue"
import RuntimeErrorExplain from "./RuntimeErrorExplain.vue"
import { useAIStream } from "shared/composables/aiStream" import { useAIStream } from "shared/composables/aiStream"
import { submitHintFeedback } from "oj/api" import { submitHintFeedback } from "oj/api"
import { MdPreview } from "md-editor-v3" import { MdPreview } from "md-editor-v3"
@@ -102,21 +103,34 @@ const pythonCompileError = computed(() => {
}) })
// 错误信息格式化 // 错误信息格式化
/**
* 运行时错误的诊断(行号 + 归类),交给 RuntimeErrorExplain。比赛提交、诊断失败的
* 没有这个字段(见后端 judge/run.ts 的 diagnoseRuntimeError),走下面 msg 的通用提示。
*/
const runtimeError = computed(() => {
const submission = props.submission
if (!submission || submission.result !== SubmissionStatus.runtime_error) return null
return submission.statisticInfo?.runtime_error ?? null
})
const msg = computed(() => { const msg = computed(() => {
if (!props.submission) return "" if (!props.submission) return ""
// 走 PythonErrorExplain 那张中文卡片,英文原文在它的折叠区里 // 走 PythonErrorExplain / RuntimeErrorExplain 那两张中文卡片
if (pythonCompileError.value) return "" if (pythonCompileError.value || runtimeError.value) return ""
let msg = "" let msg = ""
const result = props.submission.result const result = props.submission.result
// 编译错误或运行时错误时给出提示; // 编译错误或运行时错误时给出提示;
// SQL 题的运行错误多半是"查询题里写了增删改"这类被判题拒绝的语句,err_info 已说明原因,不套这句 // SQL 题的运行错误多半是"查询题里写了增删改"这类被判题拒绝的语句,err_info 已说明原因,不套这句
if ( if (props.submission.language !== "SQL") {
(result === SubmissionStatus.compile_error || result === SubmissionStatus.runtime_error) && if (result === SubmissionStatus.compile_error) {
props.submission.language !== "SQL" msg += "请仔细检查,看看代码的格式是不是写错了!\n\n"
) { } else if (result === SubmissionStatus.runtime_error) {
msg += "请仔细检查,看看代码的格式是不是写错了!\n\n" // 原来和编译错误共用「代码格式写错了」那句,可运行时错误恰恰是格式没错、跑起来才出事
msg +=
"程序运行到一半出错,停下来了。检查一下输入是怎么读的、下标有没有越界、除数是不是 0。\n\n"
}
} }
if (result !== SubmissionStatus.ast_check_failed && props.submission.statisticInfo?.err_info) { if (result !== SubmissionStatus.ast_check_failed && props.submission.statisticInfo?.err_info) {
@@ -270,12 +284,14 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
vertical vertical
v-if=" v-if="
pythonCompileError || pythonCompileError ||
runtimeError ||
msg || msg ||
infoTable.length || infoTable.length ||
submission.statisticInfo?.ast_results?.length submission.statisticInfo?.ast_results?.length
" "
> >
<PythonErrorExplain v-if="pythonCompileError" :err-info="pythonCompileError" /> <PythonErrorExplain v-if="pythonCompileError" :err-info="pythonCompileError" />
<RuntimeErrorExplain v-if="runtimeError" :info="runtimeError" :code="submission.code" />
<n-card v-if="submission.statisticInfo?.ast_results?.length" embedded> <n-card v-if="submission.statisticInfo?.ast_results?.length" embedded>
<n-flex vertical :size="8"> <n-flex vertical :size="8">
<n-flex <n-flex
@@ -0,0 +1,89 @@
import type { StatisticInfo } from "@oj2/contract"
/**
* 把运行时错误的诊断(`statistic_info.runtime_error`,后端 judge/runtime-diagnosis.ts
* 写的)翻成给中职学生看的中文。界面上不出现英文的异常名。
*
* 诊断里只有异常类型、归好类的 kind 和学生代码里的名字,**没有异常的原文消息**
* (原文里常带着隐藏测试点的输入),所以这里的说明都是按类型写的通用话,
* 定位靠行号和编辑器里的标红。
*/
export type RuntimeErrorInfo = NonNullable<StatisticInfo["runtime_error"]>
/** 说得最多的一句:input() 读进来的是文字。学生栽在类型上,十有八九是这个 */
const INPUT_IS_TEXT = "input() 读进来的是文字,要计算先用 int() 或 float() 转成数字。"
const BY_KIND: Record<string, string> = {
"int-parse":
"把输入转成整数的时候失败了:读到的内容不是一个整数。常见原因:一行里输入了好几个数,要先用 split() 拆开再转换;或者输入的是小数,要用 float()。",
"float-parse":
"把输入转成数字的时候失败了:读到的内容不是一个数。常见原因:一行里输入了好几个数,要先用 split() 拆开再转换。",
unpack:
"等号左边的变量个数,和右边拆出来的值的个数对不上。比如 a, b = input().split() 要求这一行正好有两个值,对照题目的输入格式看看一行里有几个数。",
"math-domain": "数学函数拿到了不合法的值,比如给负数开平方。",
"str-concat": `文字和数字不能直接用 + 连在一起。${INPUT_IS_TEXT}要拼成一句话,就用 str() 把数字转成文字。`,
operand: `这两个值的类型不能做这种运算,多半是拿文字和数字一起算。${INPUT_IS_TEXT}`,
"not-int":
"这里需要一个整数,给的却是文字或小数。比如 range() 里要放整数,input() 读进来的要先用 int() 转换。",
compare: `文字和数字不能比大小。${INPUT_IS_TEXT}`,
"not-callable":
"把一个不是函数的东西当成函数用了。常见原因:乘法漏写了 *(比如 2(a+b) 要写成 2*(a+b)),或者拿 print、input、sum 这类名字当了变量名。",
"not-subscriptable": "对一个数字用了下标 [ ]。只有列表、字符串这类值才能用 [ ] 取出其中一个。",
"missing-arg": "调用函数的时候少给了参数。",
"index-type": "下标要用整数。input() 读进来的是文字,要先用 int() 转换。",
}
const BY_TYPE: Record<string, string> = {
ValueError: "这里用到的值不符合要求。",
TypeError: `这里的值类型不对,比如把文字当成数字来用。${INPUT_IS_TEXT}`,
IndexError:
"下标越界了:要取的位置超出了列表或字符串的长度。下标从 0 开始,长度是 n 的话,最后一个是 [n-1]。",
UnboundLocalError: "函数里的这个变量还没赋值就先用了。",
ZeroDivisionError: "除数是 0 了(除法 / 、整除 // 或者取余 %)。做除法之前先判断除数是不是 0。",
EOFError:
"程序读的输入比题目给的多:input() 用的次数超过了输入的行数。看看输入格式,如果几个数在同一行,要用一次 input() 读进来,再用 split() 拆开。",
KeyError: "字典里没有这个键。取之前先用 in 判断一下键在不在字典里。",
RecursionError: "函数自己调用自己的层数太多了,检查一下递归有没有写结束条件。",
ModuleNotFoundError: "导入的模块不存在,判题的环境里只有 Python 自带的模块。",
ImportError: "导入的模块或者名字不存在,判题的环境里只有 Python 自带的模块。",
MemoryError: "程序占用的内存太多了。",
OverflowError: "算出来的数太大了,超出了能表示的范围。",
}
function explainPython(info: RuntimeErrorInfo) {
if (info.type === "NameError") {
if (!info.name)
return "用到了一个没有定义过的名字:检查拼写和大小写,或者是不是还没赋值就先用了。"
if (info.suggestion)
return `「${info.name}」这个名字没有定义过,是不是想写「${info.suggestion}」?`
return `「${info.name}」这个名字没有定义过:检查拼写和大小写,或者是不是还没赋值就先用了。`
}
if (info.type === "AttributeError") {
const what = info.name ? `「${info.name}」这个` : "你用的这个"
return `这个值没有${what}方法或属性:可能是拼错了,或者这个值的类型不对。`
}
return (
(info.kind && BY_KIND[info.kind]) ||
(info.type && BY_TYPE[info.type]) ||
"程序运行到这里出错,停下来了。"
)
}
function explainNative(info: RuntimeErrorInfo) {
switch (info.signal) {
case 11:
return "程序访问了不该访问的内存。常见原因:scanf 的变量前面忘了写 &;数组下标越界了;数组开得太大(可以挪到 main 外面定义)。"
case 8:
return "整数除以 0 了:除法 / 或者取余 % 的右边是 0。"
case 6:
return "程序被强制终止了,常见原因是内存访问出错、数组越界,C++ 里也可能是有异常没有处理。"
}
if (info.signal) return "程序运行的时候崩溃了。"
if (info.exit_code)
return `程序结束时返回的是 ${info.exit_code} 而不是 0:main 函数的最后要写 return 0;`
return "程序运行到一半出错,停下来了。"
}
export function explainRuntimeError(info: RuntimeErrorInfo) {
return info.type ? explainPython(info) : explainNative(info)
}
+24
View File
@@ -77,6 +77,30 @@ export const statisticInfoSchema = z.looseObject({
}), }),
) )
.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 点名的那个名字,只在它出现在学生代码里时才存 */
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(),
}) })
/** /**