xuyue
|
d25b5dbe6d
|
update
Deploy / deploy (push) Canceled after 0s
|
2026-09-27 19:36:28 +08:00 |
|
 xuyueandClaude Opus 5.5
|
5ae830a7f9
|
refactor: 一批名不副实的命名改掉(只动一端,不碰契约)
后端:
- getUserByToken → authenticateToken:它会删会话、续 TTL、记在线,不是纯读
- todayStart → dayStart(instant):本来就接受任意时刻,不只是「今天」
- objectValue → asRecord,删掉 4 份一模一样的私有拷贝和 `as toObject` 别名
- 报错文案 "does not exists" / "Already be a public problem" 改通顺
前端:
- utils/types 里 Contest / User / Problem 三个别名删掉:Contest 原来指后台
比赛、契约里同名的却是学生侧。现在 Contest = 学生侧、AdminContest = 后台,
后台用户表单叫 AdminUserForm,题目详情直接用 ProblemDetail
- SubmissionInfo → ContestSubmissionInfo,免得和 submission.info 撞名
- utoa / atou → compressToBase64 / decompressFromBase64(实际是 zlib + base64),
encode / decode → base64EncodeUtf8 / base64DecodeUtf8
- filterResult → toProblemRow、toProblemListItem → toAdminProblemRow,
行字段 submission / rate → submissionCount / acRate
- 组件:Modal → AddFromLibraryModal、Form → EditorToolbar、
ContestEditor → BasicEditor(题单也在用它)、rank 的 Index → RankMedal、
Name → UserNameCell、Password → PasswordCell、ContestType → ContestAccessTag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
2026-09-27 16:08:21 +08:00 |
|
 xuyueandClaude Opus 5
|
8b4d8899f9
|
feat(判题机): 自建镜像升级工具链,语言收敛到 C / C++ / Python
Deploy / deploy (push) Canceled after 0s
上游 QingdaoU/JudgeServer 停更在 2024-04(registry 上的 latest 和 1.6.1 是同一份
镜像,编译器停在 gcc-13),没有新版可拉,所以自己重编。docker/judge/ 是只改工具链
的 Dockerfile 分叉,server/ 和 Judger/ 从上游固定 commit b28aa56 拉,一行没动。
镜像 oj2-judge-2(不在任何 registry 上:本机 build.sh --save → scp → docker load)
- gcc/g++ 13 → 14.2,Python 3.12 → 3.13.5,都是 trixie 默认
- Go / JDK / Node 整套删掉:前端的题目语言复选框从来只给 C / C++ / Python / SQL,
12 万条提交里 Java 44 条、Golang 15、JavaScript 3,全是很早以前的
- 体积 1.1GB → 433MB;默认走清华源,构建 12 分钟 → 40 秒(--no-mirror 换回官方)
- deploy.sh 加一道自检:镜像不在本机就中止,并打印该跑的三条命令
C 的编译参数加三个 -Wno-error(implicit-function-declaration / int-conversion /
incompatible-pointer-types):gcc-14 把它们从 warning 提成了 error,而 -w 压不住。
语言值统一成 Python(迁移 0019 / 0020)
- 0019:Python3(104527 条提交)与 Python2(3 条)并成 Python,一并改掉 937 道题的
languages、75 个 template 键、15 个 ast_rules 键、257 条 answers、1235 个用户的
成就指标 _languages(languages_used 重算,总和 1928 → 1925,少的 3 个是同时用过
两种 Python 的人)
- 0020:把 Java / JavaScript / Golang 从 84 道题的可选语言里摘掉 —— 不摘的话那些题
的语言下拉还能选 Java,提交必 SYSTEM_ERROR
- 契约新增 normalizeLanguage() 别名表,判题侧一律走 judgeConfigFor():旧客户端
localStorage 里的 Python3、迁移前排进队列的任务都还能判;协作的语言归一也走它,
否则上线那一刻学生页面里的 Python3 会静默落到 C
- 回滚要连数据一起回,只滚代码会让所有 Python 提交变 SYSTEM_ERROR
实跑
- 判题冒烟 docker/judge/smoke.ts 13 条全过:三种语言、六种状态码、gcc 宽松度
- 拿备份里的真实代码逐文件比对新旧镜像的编译结果,0 差异:C 提交 1951 份
(1725 过 / 226 CE)、C++ 882 份、Python 2000 份、20 篇 C 教程的 93 个代码块。
不加那三个 -Wno-error 的话,C 有 26 份会从能过变成 CE
- 迁移在灌了 12.4 万行真实数据的一次性库里跑过:0 残留、没有题目被清空;
dev 库用真正的执行器跑通
- check:ast 56 个 target 全过,前后端 typecheck 均 0
顺带记下一个升级之前就有的坑(现在随 Go 一起消失,写在 README 里):GOCACHE 指向
容器的 tmpfs,判题机重启后第一次 Go 提交是冷构建,Go 1.22 要 5.6 秒 CPU、超过 3 秒
的编译预算,于是重启后第一个交 Go 的学生必吃一次 CE,后面的人缓存热了又都正常。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-20 06:24:05 -06:00 |
|
 xuyueandClaude Opus 5
|
ed56a209ea
|
chore(格式): Prettier 统一到全仓,后端和契约一次性格式化
Deploy / deploy (push) Has been cancelled
原来只有 `apps/web` 在 Prettier 下(配置在 `apps/web/.prettierrc.toml`、脚本在
web 的 package.json),后端和契约从来没格式化过 —— 手写在 100 列上下,`db/schema.ts`
还是 drizzle-kit pull 留下的 tab 缩进。两套口径分叉久了,跨端改一处就得记着「这边
什么风格」。
- 配置搬到根目录 `.prettierrc.toml`,内容不变(`semi=false`,其余全默认,
printWidth 80 —— 和前端已有的格式一致,不另立一套宽度);
- 脚本统一成根目录 `bun run fmt`,覆盖 `apps/*/src`、`apps/web/tests` 和两个构建
配置;web 自己那份 `fmt` 和重复的 prettier 依赖删掉;
- `.prettierignore` 挡掉两类不该碰的:drizzle-kit 生成的 `src/db/meta/` 结构快照
(它是 db:generate 的比对输入,只该由 drizzle-kit 写)、unplugin 每次 dev 都会
重写的 `auto-imports.d.ts` / `components.d.ts`;
- 全量跑了一遍。纯格式,无行为改动:api typecheck / check:routes / check:ast、
前端 type-check 全过,起 api 打了接口确认正常。前端这 39 个文件的小改动是
prettier 版本漂移(类型断言的换行口径变了),不是新配置带来的。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-16 08:27:34 -06:00 |
|
 xuyueandClaude Opus 5
|
ab47e71d6f
|
refactor(契约): 出参不再 parse,后台老题详情和站内信页不再 500
Deploy / deploy (push) Has been cancelled
## 出参改 satisfies
出参是后端自己刚拼出来的字面量,TS 编译期已经验过;再 xxxSchema.parse({...}) 一遍
拿不到任何新信息,唯一可能失败的输入是库里的历史数据,而失败的代价是 500。136 处
全部撤掉,撤的时候当场炸出两个一直存在的线上故障:
- 后台打开任何一道没编辑过的题都是 500 —— problem.last_update_time 是全库唯一可空
的列(961 道题里 470 道是 NULL),而 adminProblemSchema.lastUpdateTime 写的是
z.string();
- 收到过站内信的人打开消息页全是 500 —— embeddedSubmissionSchema 从
submissionDetailSchema 继承了 problemDisplayId 却没 omit,路由只填了同义的
problem;列表为空时才碰巧不炸,所以一直没人报。
两个都是读出侧校验自己造出来的故障,不是它拦住的故障。
## 校验责任挪回写入侧
- db/schema.ts:枚举型的列和几个形状确定的 JSONB 挂 .$type<>()(submission.result /
.language、problem.difficulty / .languages / .template / .astRules / .sqlConfig /
.sqlDisplay、achievement.rarity / .operator、exercise.type、reaction.type、
tutorial.type、problemset.difficulty / .status、flowchart_submission.status、
problemset_badge.condition_type、acm_contest_rank.submission_info)。只影响 TS、
不产生 SQL,断言逐列拿根目录那份生产备份核过全量数据。
- createProblemRequestSchema.languages 收窄成 problemLanguageSchema,兑现
problem.languages 列上的断言。
- 新增 routes/helpers.ts 的 asFilterValue():query 筛选值(result / language /
difficulty / status)要和收窄过的列比较时做纯类型交接,不加校验 —— 在这儿拦一道
会把「筛出空列表」变成「筛条件被忽略、返回全部」。
- 判题产物(submission.info / statistic_info / exercise.data)照旧放行,形状真相
在判题机那边;judge/sql、flowchart/run、events.ts 里对自家产物的 parse 一并撤掉。
- 仍然 parse 的只有 judge/events.ts 的 parseSubmissionEvent —— 从 Redis 收回来的
报文是真边界,失败返回 null 而不是 500。
顺带清掉两处重复的真相:stringArray 原本在 routes/helpers.ts、routes/problem.ts、
routes/submission.ts 各有一份拷贝,5 个调用点全部只作用于 problem.languages,列有类型后
三份一起删;routes/site.ts 里和契约同名同形的本地 interface Quote 也删了 —— loadSentences
读入时已经逐字段守过,那处 parse 同样是多余的。
## 文档
CLAUDE.md 那一节从「契约收紧要挑地方」改写成「出参不 parse,用 satisfies」,写明
三处写入侧闸门(入参 safeParse 58 处、列上 $type、语义校验函数);apps/web/CLAUDE.md
同步 —— 现在收紧字段的后果落在 tsc 编译期,但契约形状仍要对得上存量数据。
## 验证
- 生产备份全量:12.4 万条提交的 result 全在 -2..6,10、961 道题的 languages 均为合法
数组、10050 条榜单条目形状全对,无一例外;
- tsc -p apps/api 与 vue-tsc --noEmit 均 exit 0;check:routes 检查 177 条路由,无遮蔽;
前端 build、单二进制编译并在仓库目录之外启动均通过;
- 实跑 40+ 端点(学生端 / 后台 / AI / 榜单 / 题目回写往返),以及一次完整比赛 e2e:
建比赛 → 复制题目 → 错解 → 正解,把 judge/run.ts 榜单写入的三个分支全走到
(error_number 0→1、is_first_ac + ac_time 671、totalTime 1871 = 671 + 1×20×60),
后台核查页的勾选与 404 分支一并验过,测试数据已清理;
- 两个 500 用抓到的真实响应对着改动前的契约复验:lastUpdateTime 收到 null、
problemDisplayId 收到 undefined,改动后同样两个响应均通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH
|
2026-09-10 18:14:05 -06:00 |
|
 xuyueandClaude Opus 5
|
a5b57d8ab8
|
fix(判题): 任务被遗弃时给提交落一个终态,别永远停在「等待评分」
judgeSubmission 自己的 try/catch 已经把正常路径上的异常都接住、落成 SYSTEM_ERROR
了,但队列的 failed 事件只有一行 console.error。判题队列没配 attempts(flowchart
队列配了 3),失败即终局;worker 进程被杀那种 —— 机房断电、容器 OOM、部署重启 ——
BullMQ 走完 stalled 重入队还是没人接,最后 emit failed,那条提交就永远停在
「等待评分」:学生看着转圈,教师统计里还占着一个「判题中」的名额。
加 failAbandonedSubmission(),在 failed 里调用,复用已有的 markSystemError(只动
PENDING/JUDGING,判完的和重判过的都不会被覆盖)。三种情况实跑验过:不存在的提交
安全返回、卡住的那条变成 result=5 并写入 err_info、已 AC 的那条没被动。
生产库里 3 条卡死的 PENDING(2022-11 / 2026-03 / 2026-04)都是旧栈时代留下的,
OJ2 上没有实例 —— 这次是把口子堵上,不是修已发生的故障。那 3 行和跟着差 1 的三道题
计数器还得手工处理。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqjE6qPo67fqVDKn6Bx7yd
|
2026-09-08 18:29:30 -06:00 |
|
 xuyueandClaude Opus 5
|
9d9e104df6
|
fix(题单): 进度记账挪到判题这一路,不再靠前端回调
前端记账是这条链上最松的一环:SubmitCode.vue 看到 AC 就回调
PUT /problem-set-progress,而且只认路由参数里那一个题单。于是
· 从普通题库入口做出同一道题 → 不计进度
· 网络一抖、页面提前关掉 → 进度静默丢失
· 一道题同时在两个已加入的题单里 → 只有进去的那个记上
旧栈为此专门有个管理命令 fix_problemset_progress 定期按实际提交补账 ——
2026-05-22 00:50 那次一分钟内跨 7 个题单的批量补进度就是它跑的。
改成判题落库之后由 judge/run.ts 记账(recordSolvedProblem),记进该用户**所有**已加入
且包含这道题的题单。位置在最后那条 publishSubmissionUpdate("finished") 之前,所以前端
收到「判完了」时进度已经落库,跳回题单页看到的就是新数据。前端那次回调删掉。
不按 visible / status 过滤:进度是学生自己的记录,老师把题单藏起来不该让它停止累积;
更要紧的是这条口径必须和补账工具一致,否则补账工具会永远「发现」差异。
实跑(本地起 api + worker 真判一次):提交时**完全没带 problemSetId**,判完之后
progress 变成 1/1 100% 已完成、10 分、complete_time 落下、all_problems 奖章发出、
problemset_submission 也记上了。
## 补账那半
backfill-problemsets 前面加一道「按实际 AC 补进度」,移植自旧栈的
fix_problemset_progress,口径和 recordSolvedProblem 逐条对齐(非比赛提交、
ACCEPTED 或 AST_CHECK_FAILED、取最早那次)。补录的格子先并进 detail 再重算,
所以预演里的奖章名单是照着「补完账又重算过」的进度算的,和 --apply 的结果一致。
在生产快照上实跑:
合计:补录 10 道题,进度 497 条要重算(完成 +23 / -0),奖章补发 56 条、收回 0 条
已订正 11 个题单 → 复核通过
user_badge 1180 → 1236,已完成 621 → 644,problemset_submission 7735 → 7745
「未完成但有完成时间」仍是 4 条;重复跑幂等
补录的 10 道分布在题单 4/5/6/15/16(1/1/1/4/3),和离线独立算的数字逐个对上 ——
其中题单 15 那位 AC 了全部 12 题却一直显示未完成。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QqqZwxtXLo2GTqMi51C94D
|
2026-09-01 00:01:38 -06:00 |
|
 xuyueandClaude Opus 5
|
6ac458e6f3
|
refactor(WebSocket): 判题/流程图推送改用驼峰,去掉没人读的三个字段
submission_id / time_cost / memory_cost / err_info 是 OJ2 两端自己定的线上格式,
没有第三方消费(/ws/submissions 只有 apps/web 一个客户端),没理由留着 snake。
flowchart 那条更别扭:同一个对象里 submission_id 是 snake、criteriaDetails 是驼峰。
三个字段直接删掉而不是改名 —— 它们是从 statistic_info 原样抄出来的一份,前端
一处都没读过(useSubmissionMonitor 只用 submissionId / result / status)。耗时和
错误信息在提交详情里本来就有,判完了去拉一次就是,不必让推送顺带背一份 JSONB
的形状。score 保留,它不涉及大小写。
**没动的都是有外部约束的**,别顺手一起改:
- 发给判题沙箱的请求体(language_config / max_cpu_time / max_memory /
test_case_id / io_mode)和它回的字段(cpu_time / memory / test_case)——
那是沙箱的 API,不是我们的
- 测试点 info 文件的键,沙箱直接读那个文件
- submission.statistic_info 里的 time_cost / err_info / ast_results ——
判题机按这套写,12 万条历史提交就是这形状
- acm_problems_status、progress_detail 这些存量 JSONB
- AST 规则键(for_loop)、成就指标(accepted_count)、reaction 语义键 ——
那是词汇表标识符不是字段名,for_loop 还要映射到 tree-sitter 的 while_statement
验证:起 dev 栈(api + worker + 沙箱),学生账号真提一次代码,抓 /ws/submissions
的帧:
{"type":"submission_update","submissionId":"bf13b7f0…","result":6,"status":"pending"}
{"type":"submission_update","submissionId":"bf13b7f0…","result":7,"status":"judging"}
{"type":"submission_update","submissionId":"bf13b7f0…","result":-2,"status":"finished","score":0}
subscribe 帧也换成 submissionId 并被接受(否则会回一个 error 帧,没有)。
流程图那条路径要 AI 评分才跑得起来,本机没配,只做了类型检查。
前后端同一个 docker 栈一起构建部署,没有版本错配窗口。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-27 00:48:54 -06:00 |
|
 xuyueandClaude Opus 5
|
f919d41199
|
refactor(契约): AST 代码要求收进契约,三份各写各的合成一份
同一个形状原来在三个地方各定义了一份,三份都不一样:
apps/api/src/judge/ast.ts engine target outer inner label message exact min max
apps/web/src/utils/types.ts engine target message min max
AstRulesEditor.vue(本地) engine target label message exact min max
ProblemContent.vue(本地) engine target label message exact min max
判题机那份是真的(evaluateRule 按 engine 分支读哪几个字段),契约里则压根没有,
`astRules: z.unknown()`。后果是后台编辑器写得出 label 和 exact、判题机也认,
但 utils/types.ts 那个类型描述不了它们 —— 生产库 12 道带 AST 规则的题里,
15 条规则带 label、6 条带 exact,全都在类型之外。
现在契约里一份 astRuleSchema,四处都指向它。engine 收成枚举,列全判题机
实现的十种。**存量数据核过**:12 道题逐条过新 schema,12/12 通过。
顺带三件:
- `astRules` 的响应和请求 schema 从 z.unknown() 换成 astRulesSchema。之前
engine 写错一个字母能存进去,判题机 evaluateRule 走 `default: return null`
静默跳过 —— 老师设了规则、规则不生效、没有任何提示。现在保存时就 400,
错误信息把十个合法值列出来。
- judge/run.ts 的 astRulesForLanguage 原来是整片 `rules as AstRule[]` 硬转,
改成逐条 safeParse:认不出的丢掉那一条,行为和 evaluateRule 的 default 分支
一致,只是提前到读取处,也不再骗类型系统。
- 判题机实现了 must_have_nesting,但后台编辑器没有对应选项,目前只能手工造
数据才用得上。枚举里留着并加了注释,没有顺手去补 UI(那是加功能不是清理)。
## 验证
tsc(apps/api) 0 error、check:routes 168 条无遮蔽、vue-tsc 0 error、build 通过。
起服务实打:
- 把生产库那条带 label/exact 的规则种进本地库,后台题目详情 200、字段齐全;
原样 PUT 回去 200,库里 label 和 exact 都在(旧类型描述不了的那两个)。
- engine 传 "must_do_magic" → 400,错误信息列出十个合法值。
- 直接调 checkAst:三条规则(两条合法 + 一条 engine 认不出)进去,判题机收下
两条;两个 if/else 的代码 passed=true,一个的 passed=false,描述文案
「if 条件 出现 2 次」正确用上了 label。
冒烟改动已还原(problem 2 的 ast_rules 复位成 null,测试标签删掉)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-25 23:15:52 -06:00 |
|
 xuyueandClaude Opus 5
|
e3faa689e7
|
feat(阶段2补课): SQL 判题链路 + 最后两个后台端点
新后端此前完全没有 SQL 判题(旧 judge/sql_runner.py 378 行 + sql_dispatcher.py
113 行无对应实现),阶段 2 纵切时漏了这条与沙箱完全不同的路径。
judge/sql/engine.ts 判题核心,移植自 sql_runner.py,判定口径逐条对齐
judge/sql/child.ts 子进程入口
judge/sql/index.ts 父进程:spawn + 硬超时
judge/run.ts language === "SQL" 时分流,不经判题沙箱
POST admin/sql-test-cases/preview 题目页展示数据预览
POST admin/sql-test-cases/generate AI 按标准答案倒推初始化脚本
题目保存时重新生成 sqlDisplay(对齐旧 generate_sql_display):取测试点 1 的初始化
脚本 + 标准答案跑一遍,失败一律拦下不让保存 —— 展示数据直接决定学生看到的表结构
与期望结果,宁可不保存也不能存错的。
## 防护换了实现,逐条实测
bun:sqlite 没有 authorizer / progress_handler / setlimit,且实测 Worker.terminate()
杀不掉跑飞的查询(原生代码占着线程)。改用「WASM 引擎 + 独立子进程」:
ATTACH → WASM 无宿主文件系统绑定,结构上够不到(比旧的 authorizer 更强)
查询题只读 → PRAGMA query_only=1
超时 → 子进程外部 SIGKILL
单值内存 → 子进程 ulimit -d
八条提交实测:正确→Accepted;列少一个/漏过滤→Wrong Answer;语法错误→Compile Error;
查询题里 INSERT→运行错误并说明;递归 CTE 死循环→CPU 超时;hex(zeroblob(2e8))→内存超限;
attach '/etc/passwd'→打不开。
## 踩到的两个坑(已写进 docs/specs/phase3-coverage.md)
1. ulimit 必须用 -d 不能用 -v。-v 限虚拟地址空间而 JS 引擎预留巨量地址,实测 -v 之下
Bun 退出时有概率 panic(SIGILL),结果早已写出但进程异常终止,父进程读到空串
误判成超时 —— 6 次里坏 2 次。换 -d 后 12/12 稳定。
2. 子进程写完结果直接 SIGKILL 自己,不走 process.exit()——后者仍有清理会撞限额。
至此 admin/api.ts 已无任何指向旧后端的调用。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-07 17:03:41 -06:00 |
|
 xuyueandClaude Opus 5
|
8c00cdc947
|
feat(阶段3): oj 侧端点铺开(基线提交,未经评审)
由外部 agent (Codex) 在本会话额度中断期间完成。原样提交作为基线,
后续修复单独成 commit,便于区分与回退。
覆盖 oj 侧 65 个端点,新增 9 组路由(account/achievement/ai/classroom/
content/contest/flowchart/problemset/site)与对应 Zod 契约。
已核验:tsc --noEmit 退出码 0;API 可启动;/api/problems 返回真实数据;
judge 与 flowchart worker 均 ready。
未核验:权限边界与数据泄露,评审进行中。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-08-07 01:25:36 -06:00 |
|
xuyue
|
ec274419c3
|
Build Phase 2 judge vertical slice
|
2026-08-06 22:42:39 -06:00 |
|