feat: 新增 /run 接口,在 go-judge 沙箱中编译运行 C/C++

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 16:27:02 +08:00
1 parent a5cfdc1bb6
commit 3c3169bd26
5 files changed
+151 -2

No files matched your search

+13
View File
@@ -11,10 +11,13 @@ from schemas import (
DebugRequest, DebugRequest,
FormatRequest, FormatRequest,
FormatResponse, FormatResponse,
RunRequest,
RunResponse,
) )
from database import DatabaseService from database import DatabaseService
from debug_runner import DebugError, run_debug from debug_runner import DebugError, run_debug
from formatter import format_code, FormatError from formatter import format_code, FormatError
from runner import run_code, RunError
from dotenv import load_dotenv from dotenv import load_dotenv
@@ -155,6 +158,16 @@ async def format_code_endpoint(request: FormatRequest) -> FormatResponse:
return FormatResponse(code=formatted) return FormatResponse(code=formatted)
@app.post("/run", response_model=RunResponse)
async def run_code_endpoint(request: RunRequest) -> RunResponse:
"""在 go-judge 沙箱中编译运行 C/C++ 代码"""
try:
result = await run_code(request.language, request.code, request.stdin)
except RunError as e:
raise HTTPException(status_code=502, detail=str(e))
return RunResponse(**result)
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
+1
View File
@@ -14,6 +14,7 @@ dependencies = [
"uvicorn[standard]>=0.38.0", "uvicorn[standard]>=0.38.0",
"ruff>=0.15.17", "ruff>=0.15.17",
"sqlparse>=0.5.5", "sqlparse>=0.5.5",
"httpx>=0.28.1",
] ]
[dependency-groups] [dependency-groups]
+117
View File
@@ -0,0 +1,117 @@
import os
import httpx
# go-judge 沙箱地址,docker-compose 里是 code-judge 容器
GOJUDGE_URL = os.getenv("GOJUDGE_URL", "http://code-judge:5050")
# 状态码沿用前端(原 Judge0)的约定
STATUS_ACCEPTED = 3
STATUS_COMPILE_ERROR = 6
STATUS_RUNTIME_ERROR = 11
SECOND = 1_000_000_000 # go-judge 的时间单位是纳秒
MB = 1024 * 1024
OUTPUT_MAX = 64 * 1024
LANGUAGES = {
"c": {
"source": "main.c",
"compile": ["/usr/bin/gcc", "main.c", "-o", "main", "-lm"],
},
"cpp": {
"source": "main.cpp",
"compile": ["/usr/bin/g++", "main.cpp", "-o", "main"],
},
}
STATUS_MESSAGES = {
"Time Limit Exceeded": "运行超时",
"Memory Limit Exceeded": "内存超出限制",
"Output Limit Exceeded": "输出超出限制",
"Signalled": "程序异常终止",
}
class RunError(Exception):
"""沙箱不可用等非用户代码导致的错误"""
def _cmd(args: list[str], stdin: str, cpu: int, memory: int, **extra) -> dict:
return {
"args": args,
"env": ["PATH=/usr/bin:/bin"],
"files": [
{"content": stdin},
{"name": "stdout", "max": OUTPUT_MAX},
{"name": "stderr", "max": OUTPUT_MAX},
],
"cpuLimit": cpu * SECOND,
"clockLimit": cpu * 2 * SECOND,
"memoryLimit": memory * MB,
"procLimit": 50,
"copyOut": ["stdout", "stderr"],
**extra,
}
async def _run(client: httpx.AsyncClient, cmd: dict) -> dict:
try:
response = await client.post("/run", json={"cmd": [cmd]})
response.raise_for_status()
except httpx.HTTPError as e:
raise RunError(f"判题沙箱不可用:{e}")
result = response.json()[0]
if result["status"] == "Internal Error":
raise RunError(result.get("error") or "判题沙箱内部错误")
return result
def _output(result: dict) -> str:
# 与原来 Judge0 的 redirect_stderr_to_stdout 效果接近:stdout 在前,stderr 在后
files = result.get("files", {})
return (files.get("stdout", "") + files.get("stderr", "")).strip()
async def run_code(language: str, code: str, stdin: str) -> dict:
config = LANGUAGES[language]
async with httpx.AsyncClient(base_url=GOJUDGE_URL, timeout=30) as client:
compiled = await _run(
client,
_cmd(
config["compile"],
"",
cpu=10,
memory=512,
copyIn={config["source"]: {"content": code}},
copyOutCached=["main"],
),
)
if compiled["status"] != "Accepted":
return {"status": STATUS_COMPILE_ERROR, "output": _output(compiled)}
file_id = compiled["fileIds"]["main"]
try:
result = await _run(
client,
_cmd(
["main"],
stdin,
cpu=5,
memory=256,
copyIn={"main": {"fileId": file_id}},
),
)
finally:
try:
await client.delete(f"/file/{file_id}")
except httpx.HTTPError:
pass
output = _output(result)
if result["status"] == "Accepted":
return {"status": STATUS_ACCEPTED, "output": output}
message = STATUS_MESSAGES.get(result["status"])
if message:
output = f"{output}\n{message}".strip()
return {"status": STATUS_RUNTIME_ERROR, "output": output}
+17 -1
View File
@@ -1,5 +1,5 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Dict, Any from typing import List, Dict, Any, Literal
class PresetCodeCreate(BaseModel): class PresetCodeCreate(BaseModel):
@@ -30,6 +30,7 @@ class AIAnalysisRequest(BaseModel):
class DebugRequest(BaseModel): class DebugRequest(BaseModel):
"""调试请求模式,用于调试 Python 代码""" """调试请求模式,用于调试 Python 代码"""
code: str code: str
inputs: List[str] inputs: List[str]
@@ -45,3 +46,18 @@ class FormatResponse(BaseModel):
"""格式化代码的响应模式""" """格式化代码的响应模式"""
code: str code: str
class RunRequest(BaseModel):
"""运行 C/C++ 代码的请求模式"""
code: str
language: Literal["c", "cpp"]
stdin: str = ""
class RunResponse(BaseModel):
"""运行结果,status 沿用 Judge0 的状态码:3 成功、6 编译错误、11 运行错误"""
status: int
output: str
Generated
+3 -1
View File
@@ -1,5 +1,5 @@
version = 1 version = 1
revision = 3 revision = 5
requires-python = ">=3.12" requires-python = ">=3.12"
[[package]] [[package]]
@@ -73,6 +73,7 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "httpx" },
{ name = "openai" }, { name = "openai" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
@@ -91,6 +92,7 @@ dev = [
requires-dist = [ requires-dist = [
{ name = "aiosqlite", specifier = "==0.21" }, { name = "aiosqlite", specifier = "==0.21" },
{ name = "fastapi", specifier = ">=0.119.1" }, { name = "fastapi", specifier = ">=0.119.1" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "openai", specifier = ">=2.6.0" }, { name = "openai", specifier = ">=2.6.0" },
{ name = "pydantic", specifier = ">=2.12.3" }, { name = "pydantic", specifier = ">=2.12.3" },
{ name = "python-dotenv", specifier = ">=1.1.1" }, { name = "python-dotenv", specifier = ">=1.1.1" },