Appearance
题目包格式规范
统一题目包(Problem Bundle)是 Neuro OJ 的题目导入载体:单个 zip 包含题面、评测内容与评测配置,通过 POST /api/v1/problems/import-bundle(管理界面上传)或 noj problems import 一键导入,创建或更新题目。
本页是强制规范(MUST):导入时系统会强制校验包结构与 manifest 字段,任一不合法都会以 HTTP 400 拒绝,并返回具体字段错误。导入校验只保证结构合法(字段、必填项、ZIP 安全),不保证题面、测试数据与评测脚本的质量——发布前请按题目质量要求完成自测。
包结构
text
编程题包:
<任意名>.zip
├── problem.json # 必需:题目 manifest
├── evaluate.py # 必需:评测脚本(必须位于 zip 根目录)
├── statement.md # 可选:题面 Markdown(与 manifest.description 二选一,文件优先)
├── visible.jsonl # 可选:可见测试数据(推荐约定)
├── hidden.jsonl # 可选:不可见测试数据(推荐约定)
└── assets/ # 可选:其他 evaluate.py 需要的文件
客观题套卷包(is_objective=true):
<任意名>.zip
├── problem.json # 必需:manifest(is_objective: true)
├── questions.json # 必需:小题数组
└── statement.md # 可选:套卷说明evaluate.py必须位于 zip 根级——Judge Worker 将包解压到容器/workspace后路径固定为/workspace/evaluate.py。- 测试数据格式不强制:
visible.jsonl/hidden.jsonl是推荐约定,你可以用cases/*.json、SQLite、CSV 等任何方式组织,只要evaluate.py自己能读取。推荐约定见测试数据与样例规范。 - 模板文件(如
template.py)与参考实现(如submission_sample.py)不要放入包中;problems:build打包时自动排除submission*、manifest 声明的模板文件、__pycache__与.git(noj-cli problem pack同规则)。
三种"根级缺失"会导致导入失败(400)
- 编程题包根级缺
problem.json、evaluate.py,或题面(statement.md与manifest.description皆缺)。 - 客观题包根级缺
problem.json或questions.json。
注意是 zip 根级而非任意子目录;assets/evaluate.py 这类嵌套路径不算数。
manifest(problem.json)
json
{
"format_version": 1,
"number": 1001,
"title": "题目标题",
"difficulty": "easy",
"type": "P",
"tags": ["入门", "LMCC 样例题"],
"runtime_config": {
"evaluator": {
"image": "noj-evaluator-python",
"time_limit_ms": 5000,
"memory_limit_mb": 512
},
"solution": {
"image": "noj-solution-python",
"call_timeout_ms": 5000,
"memory_limit_mb": 512
}
},
"template": "template.py"
}| 字段 | 必填 | 说明 |
|---|---|---|
format_version | ✅ | 当前唯一支持 1;其他值导入返回 400 |
title | ✅ | 非空字符串 |
runtime_config | ✅* | 双容器配置(编程题必填);evaluator.command 可缺省(默认 python3 /workspace/evaluate.py);is_objective=true 时禁止提供 |
is_objective | ❌ | 布尔值,缺省 false;true 表示客观题套卷包,不要求 runtime_config / evaluate.py,必须含 questions.json |
statement.md 文件 | ❌ | 与 manifest.description 二选一(文件优先),二者皆缺 → 400 |
evaluate.py 文件 | ✅* | 编程题根级缺失 → 400;客观题包不要求 |
questions.json 文件 | ✅* | 客观题包根级缺失 → 400;编程题包不要求 |
number | ❌ | 仅 admin 生效:幂等键——按 (type, number) 匹配既有题目则更新;缺省 type 内自动分配 |
difficulty | ❌ | easy / medium / hard,缺省 medium |
type | ❌ | U / P,缺省 U(P 型仅 admin) |
tags | ❌ | 标签名数组,按 name 匹配已有标签;不存在的名字被忽略并 warning(字段缺省则不处理) |
samples | ❌ | 已废弃:该字段从不落库、没有任何消费者。为兼容存量题包,导入仍会容忍但会记录 warning 并忽略;新题包不要写它,题面样例请直接写进题面正文 |
template | ❌ | 模板文件索引(纯文件名,禁止 /、\、..),缺省 "template.py";客观题包禁止提供 |
submission_mode | ❌ | 提交模式 code(缺省)/ artifact;客观题包禁止提供 |
artifact_max_size_mb | ❌ | artifact 提交大小上限(MB),正整数或 null(缺省 null = 用平台硬上限);客观题包禁止提供 |
llm | ❌ | LLM 调用题配置 { max_calls?, max_tokens? }(非 null 即启用,预算均可选);仅 P 型 + evaluator 联网可启用;客观题包禁止提供 |
categories字段已退役,统一使用tags。runtime_config.solution无需配置入口文件名:Solution 入口为评测内部约定,用户代码由 Judge Worker 以硬编码名main.py注入容器,出题人不可见、不可配置。
客观题包禁止提供的字段
is_objective=true 时,manifest 中不得出现 runtime_config、llm、template、submission_mode、artifact_max_size_mb,否则直接 400。客观题没有评测容器,这些字段无意义。
submission_mode: artifact 的入口约定
产物提交题的 zip 由 Judge Worker 解压到 Solution 容器,入口文件固定为 submission.py(代码题才是硬编码的 main.py)。详见Web 题目编辑器 § 产物提交题。
版本与校验
format_version当前唯一支持1;未知版本导入返回 HTTP 400。tags按 name 匹配已有标签;不存在的标签名被忽略并记录 warning(不会因此导入失败)。llm校验:仅 P 型(type="P")可启用,且必须开启 evaluator 网络;只校验可选预算字段,未知键(含存量的provider_id/model)忽略。
ZIP 安全与上传入口约束
- 拒绝路径穿越条目:绝对路径(
/开头)或含..段的条目一律 400。 - 条目数 ≤ 1000、单文件 ≤ 64 MiB、总解压 ≤ 512 MiB。
- 上传 zip 本体(压缩后)另有大小上限 128 MiB(
MAX_SUPPORT_PACKAGE_SIZE),与支持包上传一致。 POST /api/v1/problems/import-bundle读取 multipart 的file字段;缺失或不是文件、扩展名非.zip、Content-Type 不在application/zip/application/x-zip-compressed之内,都会 400。
导入语义与存储
- 上传的 zip 是导入载体;编程题系统剥离
problem.json/statement.md后重建纯净评测包存入存储(noj-storage://),题面/元数据的唯一事实来源是数据库。客观题套卷不产生评测包存储,support_package_storage_url为 NULL。 - 重复导入幂等:admin 提供
number且 (type, number) 匹配既有题目 → 更新元数据并替换评测包(客观题全量替换小题);未命中 → 创建。 - 非 admin 提供
number会被 400 拒绝,普通用户导入仅创建新题(题号自动分配)。
特殊题型
LLM 调用题
在 manifest 中增加 llm 字段,只声明预算;用哪个 Provider、哪个模型由平台全局 默认统一决定(后台「系统设置 → LLM」),因此题包不含部署期 UUID 或模型名,可跨部署 直接导入。
下面是与完整 manifest 合并的片段(runtime_config 也需补齐 image / time_limit_ms / memory_limit_mb,此处省略):
json
{
"type": "P",
"llm": {
"max_calls": 30,
"max_tokens": 20000
},
"runtime_config": {
"evaluator": {
"network": { "enabled": true }
}
}
}llm非 null 即启用;max_calls/max_tokens均可选,若提供必须为正整数。- 旧 manifest 中的
provider_id/model被容忍并忽略(不报错)。 - 必须 P 型 + evaluator 联网,否则导入被拒。
- 安全与配额要求见出 LLM 调用题。
客观题套卷
客观题套卷(is_objective=true)支持通过统一题目包导入:problem.json 中声明 "is_objective": true,根级提供 questions.json(小题数组),不要求 evaluate.py / runtime_config。导入时系统创建/更新套卷并全量替换小题,不产生评测包存储,也不自动重测历史提交。
questions.json 是一个非空数组,每项对应一道小题:
| 字段 | 必填 | 说明 |
|---|---|---|
type | ✅ | single(单选)/ multiple(多选)/ judge(判断) |
prompt | ✅ | 非空题干 |
options | ✅* | { key, text } 数组;judge 型省略(服务端用固定「正确 / 错误」选项) |
answer | ✅ | 标准答案数组:单/多选为选项 key 字符串(单选恰好 1 个、多选不重复);判断题为 [true] / [false] |
explanation | ❌ | 答案解析(判卷后展示) |
sort_order | ❌ | 卷内排序(非负整数,缺省按数组下标;同一份数组内不得重复) |
json
[
{
"type": "single",
"prompt": "1+1=?",
"options": [{ "key": "A", "text": "2" }, { "key": "B", "text": "3" }],
"answer": ["A"],
"explanation": "因为 1+1=2"
}
]客观题的两个附加约束
questions.json必须是非空数组,且每道小题的answer选项必须存在于该题options中(判断题除外)。- 套卷不得关联算法标签(系统强制 400),因为客观题没有"通过"概念。