21 KiB
提交前自动代码格式化 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 用户提交 Python3/C/C++ 代码时,前端先调用新增的 /api/format_code 接口(后端用 ruff format / clang-format)格式化代码,更新编辑器内容后再提交;其他语言原样提交,格式化失败时按语言区分降级。
Architecture: 后端在 submission 应用新增一个同步 APIView(FormatCodeAPI),内部通过 subprocess 调用 ruff format 或 clang-format,返回格式化后的代码或错误码(format-error / server-error)。前端在 SubmitCode.vue 的 submit() 中,对 Python3/C/C++ 调用该接口,成功则写回 codeStore(驱动编辑器与 Yjs 同步),format-error 阻止提交并提示,其他错误静默降级提交原代码。
Tech Stack: Django 6 + DRF (utils.api.APIView), subprocess, ruff CLI, clang-format CLI;Vue 3 + Pinia + axios (utils/http.ts)
注:本项目
CLAUDE.md明确"不写测试"。本计划用手动验证(dev server + curl / 浏览器)代替自动化测试步骤。
Backend Tasks
Task 1: 新建 submission/utils.py — 格式化工具封装
Files:
-
Create:
OnlineJudge/submission/utils.py -
Step 1: 编写格式化函数与异常类
import subprocess
CLANG_FORMAT_STYLE = "{BasedOnStyle: LLVM, IndentWidth: 4, BreakBeforeBraces: Attach}"
class FormatSyntaxError(Exception):
"""格式化工具因代码本身存在语法错误而失败(目前只有 ruff format 会触发)"""
class FormatToolError(Exception):
"""格式化工具执行本身失败:超时、二进制缺失、非预期的非零退出码"""
def format_code(code, language):
"""
:param code: 用户代码
:param language: "python" | "c" | "cpp"
:return: 格式化后的代码字符串
:raises FormatSyntaxError: 代码语法错误导致格式化失败(仅 python)
:raises FormatToolError: 格式化工具本身执行失败
"""
if language == "python":
return _format_with_ruff(code)
return _format_with_clang(code, language)
def _format_with_ruff(code):
try:
result = subprocess.run(
["ruff", "format", "--stdin-filename", "code.py", "-"],
input=code,
capture_output=True,
text=True,
timeout=5,
)
except (subprocess.TimeoutExpired, OSError) as e:
raise FormatToolError(str(e))
if result.returncode != 0:
raise FormatSyntaxError(result.stderr)
return result.stdout
def _format_with_clang(code, language):
filename = "code.c" if language == "c" else "code.cpp"
try:
result = subprocess.run(
[
"clang-format",
f"-assume-filename={filename}",
f"-style={CLANG_FORMAT_STYLE}",
],
input=code,
capture_output=True,
text=True,
timeout=5,
)
except (subprocess.TimeoutExpired, OSError) as e:
raise FormatToolError(str(e))
if result.returncode != 0:
raise FormatToolError(result.stderr)
return result.stdout
- Step 2: 手动验证 ruff 分支
Run:
cd OnlineJudge
uv run python -c "
from submission.utils import format_code
print(repr(format_code('x=1\ndef f( a,b ):\n return a+b\n', 'python')))
"
Expected: 输出格式化后的代码(4 空格缩进、x = 1、空两行分隔函数等),不抛异常。
- Step 3: 手动验证 ruff 语法错误分支
Run:
uv run python -c "
from submission.utils import format_code, FormatSyntaxError
try:
format_code('def f(:\n pass\n', 'python')
except FormatSyntaxError as e:
print('FormatSyntaxError:', e)
"
Expected: 打印 FormatSyntaxError: ... 且包含 ruff 的语法错误信息。
- Step 4: 手动验证 clang-format 分支
Run:
uv run python -c "
from submission.utils import format_code
print(format_code('#include<stdio.h>\nint main(){\nint a=1;\nreturn 0;}\n', 'cpp'))
"
Expected: 输出 4 空格缩进、{ 跟在行尾(Attach 风格)的格式化代码。
- Step 5: Commit
git add submission/utils.py
git commit -m "feat(submission): add ruff/clang-format code formatting helper"
Task 2: 新增 FormatCodeSerializer
Files:
-
Modify:
OnlineJudge/submission/serializers.py -
Step 1: 在文件末尾追加序列化器
在 submission/serializers.py 末尾追加:
class FormatCodeSerializer(serializers.Serializer):
code = serializers.CharField(max_length=1024 * 1024)
language = serializers.ChoiceField(choices=("python", "c", "cpp"))
- Step 2: 手动验证
Run:
cd OnlineJudge
uv run python -c "
from submission.serializers import FormatCodeSerializer
s = FormatCodeSerializer(data={'code': 'x=1', 'language': 'python'})
print(s.is_valid(), s.validated_data)
s2 = FormatCodeSerializer(data={'code': 'x=1', 'language': 'java'})
print(s2.is_valid(), s2.errors)
"
Expected: 第一行 True {'code': 'x=1', 'language': 'python'};第二行 False {'language': [ErrorDetail(string='\"java\" is not a valid choice.', code='invalid_choice')]}
- Step 3: Commit
git add submission/serializers.py
git commit -m "feat(submission): add FormatCodeSerializer"
Task 3: 新增 FormatCodeAPI 视图
Files:
-
Modify:
OnlineJudge/submission/views/oj.py -
Step 1: 在文件顶部导入区添加 logging 和新依赖
当前顶部导入(submission/views/oj.py:1-16):
import ipaddress
from asgiref.sync import sync_to_async
from django.utils import timezone
from account.decorators import check_contest_permission, login_required
from contest.models import ContestStatus
from flowchart.models import FlowchartSubmission
from judge.tasks import judge_task
from options.options import SysOptions
# from judge.dispatcher import JudgeDispatcher
from problem.models import Problem, ProblemRuleType
from utils.api import AsyncAPIView, validate_serializer
from utils.cache import cache
from utils.captcha import Captcha
from utils.throttling import TokenBucket
from ..models import Submission
from ..serializers import (
CreateSubmissionSerializer,
ShareSubmissionSerializer,
SubmissionListSerializer,
SubmissionModelSerializer,
SubmissionSafeModelSerializer,
bulk_fetch_problemset_progress,
)
修改为(新增 import logging、APIView、FormatCodeSerializer、from ..utils import ...):
import ipaddress
import logging
from asgiref.sync import sync_to_async
from django.utils import timezone
from account.decorators import check_contest_permission, login_required
from contest.models import ContestStatus
from flowchart.models import FlowchartSubmission
from judge.tasks import judge_task
from options.options import SysOptions
# from judge.dispatcher import JudgeDispatcher
from problem.models import Problem, ProblemRuleType
from utils.api import APIView, AsyncAPIView, validate_serializer
from utils.cache import cache
from utils.captcha import Captcha
from utils.throttling import TokenBucket
from ..models import Submission
from ..serializers import (
CreateSubmissionSerializer,
FormatCodeSerializer,
ShareSubmissionSerializer,
SubmissionListSerializer,
SubmissionModelSerializer,
SubmissionSafeModelSerializer,
bulk_fetch_problemset_progress,
)
from ..utils import FormatSyntaxError, FormatToolError, format_code
logger = logging.getLogger(__name__)
- Step 2: 在文件末尾追加视图类
在 submission/views/oj.py 文件末尾追加:
class FormatCodeAPI(APIView):
@login_required
@validate_serializer(FormatCodeSerializer)
def post(self, request):
code = request.data["code"]
language = request.data["language"]
try:
formatted = format_code(code, language)
except FormatSyntaxError as e:
return self.error(msg=str(e), err="format-error")
except FormatToolError as e:
logger.exception("format_code tool error: %s", e)
return self.error(msg="format failed", err="server-error")
return self.success({"code": formatted})
- Step 3: 手动验证(需先启动 dev server)
Run(另开终端):
cd OnlineJudge && python dev.py
确认服务起来后,用已登录用户的 session/cookie 验证(替换 <csrftoken>/<sessionid> 为实际 cookie 值,可从浏览器登录后开发者工具中获取):
curl -s -X POST http://localhost:8000/api/format_code \
-H "Content-Type: application/json" \
-H "Cookie: sessionid=<sessionid>; csrftoken=<csrftoken>" \
-H "X-CSRFToken: <csrftoken>" \
-d '{"code": "x=1\ndef f( a,b ):\n return a+b\n", "language": "python"}'
Expected: {"error": null, "data": {"code": "x = 1\n\n\ndef f(a, b):\n return a + b\n"}}
未登录时(不带 Cookie)请求应返回 {"error": "login-required", ...}。
- Step 4: Commit
git add submission/views/oj.py
git commit -m "feat(submission): add FormatCodeAPI view"
Task 4: 注册 URL 路由
Files:
-
Modify:
OnlineJudge/submission/urls/oj.py -
Step 1: 修改文件
当前内容:
from django.urls import path
from ..views.oj import (
ContestSubmissionListAPI,
SubmissionAPI,
SubmissionExistsAPI,
SubmissionListAPI,
SubmissionsTodayCount,
)
urlpatterns = [
path("submission", SubmissionAPI.as_view()),
path("submissions", SubmissionListAPI.as_view()),
path("submissions/today_count", SubmissionsTodayCount.as_view()),
path("submission_exists", SubmissionExistsAPI.as_view()), # DEPRECATED: 前端未调用
path("contest_submissions", ContestSubmissionListAPI.as_view()),
]
改为:
from django.urls import path
from ..views.oj import (
ContestSubmissionListAPI,
FormatCodeAPI,
SubmissionAPI,
SubmissionExistsAPI,
SubmissionListAPI,
SubmissionsTodayCount,
)
urlpatterns = [
path("submission", SubmissionAPI.as_view()),
path("submissions", SubmissionListAPI.as_view()),
path("submissions/today_count", SubmissionsTodayCount.as_view()),
path("submission_exists", SubmissionExistsAPI.as_view()), # DEPRECATED: 前端未调用
path("contest_submissions", ContestSubmissionListAPI.as_view()),
path("format_code", FormatCodeAPI.as_view()),
]
- Step 2: 手动验证
Run(dev server 已启动的前提下):
curl -s -X POST http://localhost:8000/api/format_code -H "Content-Type: application/json" -d '{}'
Expected: 返回 {"error": "login-required", ...}(未登录),说明路由已生效(而不是 404)。
- Step 3: Commit
git add submission/urls/oj.py
git commit -m "feat(submission): register /api/format_code route"
Task 5: 将 ruff 移入主依赖
Files:
-
Modify:
OnlineJudge/pyproject.toml -
Modify:
OnlineJudge/deploy/requirements.txt(通过命令生成,不手写) -
Step 1: 编辑
pyproject.toml
当前 [project] dependencies 列表末尾(pyproject.toml):
"asgiref>=3.11.1",
"jieba>=0.42.1",
]
[dependency-groups]
dev = [
"ruff>=0.15.11",
]
改为(ruff 移入 dependencies,删除 [dependency-groups]):
"asgiref>=3.11.1",
"jieba>=0.42.1",
"ruff>=0.15.11",
]
如果
[dependency-groups]块下还有其他依赖项,只删除"ruff>=0.15.11",这一行并保留其余内容;本仓库当前该块只有 ruff 一项,可整块删除。
- Step 2: 同步 lock 文件与 requirements
Run:
cd OnlineJudge
uv sync
./reqs.sh
Expected: uv.lock 更新(ruff 从 dev group 移到主依赖组),deploy/requirements.txt 重新生成并包含 ruff==... 一行。
- Step 3: 验证 ruff 仍可用
Run:
uv run ruff --version
Expected: 输出版本号(如 ruff 0.15.12),不报错。
- Step 4: Commit
git add pyproject.toml uv.lock deploy/requirements.txt
git commit -m "build: move ruff from dev to main dependencies (needed at runtime for code formatting)"
Task 6: Dockerfile 增加 clang-format
Files:
-
Modify:
OnlineJudge/Dockerfile -
Step 1: 修改
apk add列表
当前(Dockerfile:17-23):
apk add --no-cache \
gcc libc-dev python3-dev \
libpq libpq-dev \
libjpeg-turbo libjpeg-turbo-dev \
zlib zlib-dev \
freetype freetype-dev \
supervisor openssl nginx curl unzip
改为(新增 clang-extra-tools,提供 clang-format 二进制;放在常驻依赖里,不会被后面的 apk del 清理):
apk add --no-cache \
gcc libc-dev python3-dev \
libpq libpq-dev \
libjpeg-turbo libjpeg-turbo-dev \
zlib zlib-dev \
freetype freetype-dev \
supervisor openssl nginx curl unzip \
clang-extra-tools
- Step 2: 验证镜像可构建并包含 clang-format
Run:
cd OnlineJudge
docker build -t oj-backend-format-test .
docker run --rm oj-backend-format-test clang-format --version
Expected: 构建成功,且输出 clang-format version ...(不报 command not found)。
该步骤构建耗时较长,如本地环境无法运行 docker,可跳过实际构建,但需在 PR 描述中说明未验证,留待 CI/部署时确认。
- Step 3: Commit
git add Dockerfile
git commit -m "build: install clang-extra-tools for clang-format in backend image"
Frontend Tasks
Task 7: 新增 formatCode API 函数
Files:
-
Modify:
ojnext/src/oj/api.ts -
Step 1: 在
submitCode函数后追加新函数
当前(ojnext/src/oj/api.ts:61-63):
export function submitCode(data: SubmitCodePayload) {
return http.post("submission", data)
}
改为:
export function submitCode(data: SubmitCodePayload) {
return http.post("submission", data)
}
export function formatCode(data: { code: string; language: string }) {
return http.post<{ code: string }>("format_code", data)
}
- Step 2: 手动验证
Run:
cd ojnext
npx tsc --noEmit
Expected: 无新增类型错误(与修改前结果一致)。
- Step 3: Commit
git add src/oj/api.ts
git commit -m "feat(api): add formatCode endpoint for pre-submit formatting"
Task 8: SubmitCode.vue 集成提交前自动格式化
Files:
-
Modify:
ojnext/src/oj/problem/components/SubmitCode.vue -
Step 1: 添加导入
当前导入区(SubmitCode.vue:1-14):
import { Icon } from "@iconify/vue"
import { storeToRefs } from "pinia"
import { getComment, submitCode, updateProblemSetProgress } from "oj/api"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { useFireworks } from "oj/problem/composables/useFireworks"
import { useSubmissionMonitor } from "oj/problem/composables/useSubmissionMonitor"
import { SubmissionStatus } from "utils/constants"
import type { SubmitCodePayload } from "utils/types"
import SubmissionResult from "./SubmissionResult.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
import { checkPythonSyntax } from "oj/problem/utils/pythonSyntaxCheck"
改为(新增 formatCode 导入、LANGUAGE_FORMAT_VALUE 导入):
import { Icon } from "@iconify/vue"
import { storeToRefs } from "pinia"
import {
formatCode,
getComment,
submitCode,
updateProblemSetProgress,
} from "oj/api"
import { useCodeStore } from "oj/store/code"
import { useProblemStore } from "oj/store/problem"
import { useFireworks } from "oj/problem/composables/useFireworks"
import { useSubmissionMonitor } from "oj/problem/composables/useSubmissionMonitor"
import { LANGUAGE_FORMAT_VALUE, SubmissionStatus } from "utils/constants"
import type { SubmitCodePayload } from "utils/types"
import SubmissionResult from "./SubmissionResult.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
import { checkPythonSyntax } from "oj/problem/utils/pythonSyntaxCheck"
- Step 2: 在
submit()中插入格式化步骤
当前 submit() 函数(SubmitCode.vue:110-139):
async function submit() {
if (!userStore.isAuthed) return
// 0. Python3 语法检测
if (codeStore.code.language === "Python3") {
const syntaxError = checkPythonSyntax(codeStore.code.value)
if (syntaxError) {
message.warning(`第 ${syntaxError.line} 行存在语法错误,请修正后再提交`)
return
}
}
// 1. 构建提交数据
const data: SubmitCodePayload = {
problem_id: problem.value!.id,
language: codeStore.code.language,
code: codeStore.code.value,
}
if (contestID) {
data.contest_id = parseInt(contestID)
}
// 2. 提交代码到后端
const res = await submitCode(data)
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
// 3. 启动冷却 + 监控
startCooldown()
startMonitoring(res.data.submission_id)
showResult.value = true
}
改为(在"1. 构建提交数据"之前插入"0.5 提交前自动格式化"):
async function submit() {
if (!userStore.isAuthed) return
// 0. Python3 语法检测
if (codeStore.code.language === "Python3") {
const syntaxError = checkPythonSyntax(codeStore.code.value)
if (syntaxError) {
message.warning(`第 ${syntaxError.line} 行存在语法错误,请修正后再提交`)
return
}
}
// 0.5 提交前自动格式化(Python3 用 ruff,C/C++ 用 clang-format)
const formatLang = LANGUAGE_FORMAT_VALUE[codeStore.code.language]
if (["python", "c", "cpp"].includes(formatLang)) {
try {
const res = await formatCode({
code: codeStore.code.value,
language: formatLang,
})
codeStore.setCode(res.data.code)
} catch (e: any) {
if (e?.error === "format-error") {
// 仅 Python3 会出现:代码本身存在语法错误
message.warning(`代码格式化失败:${e.data},请检查代码后重试`)
return
}
// server-error / 网络异常:格式化工具问题,静默降级,提交原代码
}
}
// 1. 构建提交数据
const data: SubmitCodePayload = {
problem_id: problem.value!.id,
language: codeStore.code.language,
code: codeStore.code.value,
}
if (contestID) {
data.contest_id = parseInt(contestID)
}
// 2. 提交代码到后端
const res = await submitCode(data)
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
// 3. 启动冷却 + 监控
startCooldown()
startMonitoring(res.data.submission_id)
showResult.value = true
}
- Step 3: 启动前后端 dev server 并手动验证
Run(两个终端):
cd OnlineJudge && python dev.py
cd ojnext && npm start
浏览器打开 http://localhost:5173,登录后进入任意题目页:
- 选择 Python3,编辑器中输入不规范格式的代码(如
x=1\ndef f( a,b ):\n return a+b),点击"提交代码"。 Expected: 编辑器内容自动变为 ruff 格式化后的样式(如x = 1单独成行,函数前空两行,4 空格缩进),随后正常提交并显示判题结果。 - 选择 C++,输入未格式化的代码(如
#include<iostream>\nint main(){\nstd::cout<<"hi";\nreturn 0;}),点击"提交代码"。 Expected: 编辑器内容自动变为 clang-format 格式化后的样式(4 空格缩进,{跟在行尾),随后正常提交。 - 选择 Java,提交任意代码。
Expected: 不触发格式化请求(浏览器 Network 面板中无
/api/format_code请求),直接提交。 - (可选)Python3 下输入有语法错误但能通过
checkPythonSyntax的边界代码,验证若后端返回format-error,会弹出警告且不提交。
- Step 4: Commit
git add src/oj/problem/components/SubmitCode.vue
git commit -m "feat(problem): auto-format Python3/C/C++ code before submit"
Self-Review Notes
- Spec coverage: 接口位置/参数/响应格式(Task 1-4)、依赖与 Docker(Task 5-6)、前端 API + 集成点 + 错误分类处理(Task 7-8)均已覆盖;Java/Go/JS 跳过逻辑通过
["python","c","cpp"].includes(formatLang)判断实现。 - Type consistency:
format_code(code, language)签名、FormatSyntaxError/FormatToolError异常名在 Task 1/3 中一致;前端formatCode({code, language})返回{code: string},与res.data.code用法一致;err取值"format-error"/"server-error"在后端 Task 3 与前端 Task 8 中一致。 - Out of scope(与 spec 一致,未在本计划中实现): 格式化开关、Java/Go/JS 格式化、
CodeEditor.vue只读组件改动。