From f6bc42f534aad15048523834d5e9db6f54733ec4 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sat, 8 Aug 2026 03:37:45 -0600 Subject: [PATCH] =?UTF-8?q?docs:=20=E7=BB=99=20OJ2=20=E8=A1=A5=E4=B8=80?= =?UTF-8?q?=E4=BB=BD=E8=87=AA=E5=B7=B1=E7=9A=84=20CLAUDE.md=EF=BC=9BDocker?= =?UTF-8?q?file=20=E6=8B=B7=E4=B8=8A=20bunfig.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## bunfig.toml 没拷进容器,才是那批「本地能过、容器过不了」的根因 bunfig.toml 里设了 `linker = "hoisted"`,但 Dockerfile 只拷了 package.json 和 bun.lock,于是容器里用的是默认的 isolated 布局(包都在 node_modules/.bun/ 下), 本地靠"提升"才解析得到的包在容器里一律 Could not resolve —— 而本地构建始终是好的, 只有镜像构建才炸。之前是一个一个补直接依赖补过去的(那是对的、该保留), 这里让两边布局一致,是第二道保险。 带上之后装 622 个包(isolated 是 1238),镜像重建通过,二进制在容器里 serve + healthcheck 正常。 顺手补了 main.ts 帮助文本里漏掉的 healthcheck 子命令(compose 里把 command 写错时,看到的就是这行)。 ## OJ2/CLAUDE.md OJ2 是独立仓库,之前没有自己的项目指引。写了一份,重点是几条「不知道就会踩」的: - **本机 Docker 可用**,整套依赖和上线演练都能在本机跑 —— 别沿用上一代 "本机跑不起来后端"的旧假设 - 单二进制不能依赖 node_modules,`.node` 资源导入 dev 和编译两种形态行为不同, **改完两种形态都要跑** - SQL 判题 spawn 的是二进制自己,入口必须有 argv 分发,那道递归闸不能删 - 判题状态码三处同步、raw_password 要保留、比赛只有 ACM、前端要兼容老 Chrome - 不写迁移:新旧后端跑同一套表结构,这是回滚能成立的前提 Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 106 +++++++++++++++++++++++++++++++++++++++++++ apps/api/src/main.ts | 2 +- docker/Dockerfile | 8 +++- 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3d60007 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ +# CLAUDE.md + +OJ2 是判题狗(Online Judge)的后端重写:Django 6 → Bun + TypeScript,前后端同仓。 +上一代在 `../OnlineJudge/`(Django)和 `../ojnext/`(Vue SPA),**两者都已冻结, +是回滚路径,任何情况下都不要改。** + +设计文档:`docs/specs/2026-08-06-bun-backend-rewrite-design.md` +切换手册:`docs/specs/phase5-cutover-runbook.md` ← 上线当天照这份走 + +## 仓库结构 + +| 目录 | 作用 | +|---|---| +| `apps/api/` | 后端。Hono + Drizzle + BullMQ,编译成单二进制 | +| `apps/web/` | 前端。从 ojnext 原样搬来的 Vue 3 SPA | +| `packages/contract/` | 前后端共用的 Zod 契约 | +| `docker/` | Dockerfile + 三套 compose(dev / debian / school) | +| `docs/specs/` | 设计、端点清单、各阶段评审报告与演练报告 | + +## 本机环境 + +**Docker 可用,全套依赖都能在本机跑起来**(PostgreSQL、Redis、判题沙箱), +镜像也能在本机构建并完整演练上线。这一点和上一代不同,别沿用"本机跑不起来后端" +的旧假设。 + +```bash +bun install +bun run db:up # 起 postgres(5433) / redis(6380) / 判题沙箱(8081) +bun run dev # api(3000) + worker + web(5173) 一起起 +``` + +首次要先建 `.env`(照 `.env.example`)。判题机 token 两边必须一致: +`.env` 的 `JUDGE_SERVER_TOKEN` 和 `docker/.env` 的 `OJ2_JUDGE_TOKEN`。 + +常用检查: + +```bash +bunx tsc --noEmit -p apps/api # 后端类型检查 +cd apps/web && bun run build # 前端构建(vite 不做类型检查,构建即验证) +``` + +**不要写测试** —— 沿用上一代的项目约定。验证靠实跑:起服务、打接口、看结果。 + +## 几件必须知道的事 + +### 单二进制是有代价的 + +`apps/api` 编译成 `bun build --compile` 的单二进制,所以**运行时不能依赖 +node_modules**。任何 `require.resolve` / `Bun.resolveSync` / `__dirname` 去找文件的 +写法,本地都正常、编译后都会炸,而且**只在离开仓库目录后才炸**(在仓库里跑时它顺着 +cwd 摸到了 node_modules,假装没事)。 + +资源要用 `with { type: "file" }` 内嵌。`.node` 原生模块还要额外注意:这个写法 +只有打包器认、`bun run` 不认,所以必须按形态分叉 —— 见 `apps/api/src/vendor/jieba.ts` +的注释,那里把坑写全了。 + +**改完这类代码,dev 和编译两种形态都要跑一遍。** 我吃过亏:只验了编译产物, +dev 直接起不来。 + +### 路径解析看 `runtime.ts` + +编译后 `import.meta.dir` 恒为 `/$bunfs/root`,往上三级就是文件系统根。 +相对路径一律走 `runtime.ts` 的 `pathBase`,别自己拼。 + +### SQL 判题会 spawn「自己」 + +`judge/sql/index.ts` 起的子进程是二进制自身 + `sql-child` 子命令(因为编译后磁盘上 +没有 child.ts 可以 spawn)。所以**入口必须有 argv 分发**,否则「起自己」变成 +「把整个程序再跑一遍」→ 指数级 fork。这不是假想,开发时炸过一次开发机。 +`OJ2_SQL_CHILD` 那道递归闸不要删。 + +### 判题状态码要三处同步 + +`apps/api/src/judge/status.ts`、`apps/web/src/utils/constants.ts`、 +以及上一代的 `../OnlineJudge/submission/models.py`(回滚时要对得上)。 +题目表情 reaction 的语义 key 同理。 + +### 比赛只有 ACM 模式 + +没有 OI。上一代残留的 OI 分支在阶段 0 已经砍掉,不要"顺手补回来"。 + +### 前端要兼容老 Chrome + +机房电脑 Chrome < 94。`mermaid-legacy` 等 fallback 依赖和 vite 的构建 target +不能动,`vite.config.ts` 里有注释说明。 + +## 数据库 + +Drizzle schema 是从生产库 `drizzle-kit pull` 出来的,**不写迁移**。 +新旧后端跑在同一套表结构上(阶段 5 演练逐列比对过,零差异),这是回滚能成立的前提 —— +所以改 schema 前先想清楚回滚怎么办。 + +生产库的几个约定: + +- `raw_password` 明文列**要保留**,老师用它找学生密码。不要"顺手清理"。 +- `judge_server_heartbeat` 保留。 +- `problem.prompt` 是给未来 AI 预留的,当前没接线,不要删。 + +## 部署 + +三套 compose 在 `docker/`:`dev`(本机)、`debian`(服务器)、`school`(机房)。 + +**机房那套没有 postgres,连的是服务器的库。** 两个站点共用一个数据库, +但各有各的 Redis 和判题沙箱 —— 所以上线那天**两边必须一起切**。 + +细节和演练结果都在 `docs/specs/phase5-cutover-runbook.md`。 diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index e5edfdf..2e32c88 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -47,6 +47,6 @@ switch (command) { } } default: - console.error(`未知子命令:${command}\n可用:serve | worker | sql-child`) + console.error(`未知子命令:${command}\n可用:serve | worker | healthcheck | sql-child`) process.exit(2) } diff --git a/docker/Dockerfile b/docker/Dockerfile index e763941..c3313ba 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -21,8 +21,12 @@ WORKDIR /build ARG NPM_REGISTRY=https://registry.npmmirror.com ENV BUN_CONFIG_REGISTRY=${NPM_REGISTRY} -# 先只拷 manifest,依赖没变时这一层能命中缓存 -COPY package.json bun.lock ./ +# 先只拷 manifest,依赖没变时这一层能命中缓存。 +# bunfig.toml 必须一起拷:它设了 linker = "hoisted",漏掉的话容器里会用默认的 +# isolated 布局(包都在 node_modules/.bun/ 下),于是本地靠"提升"才解析得到的包 +# 在容器里一律 Could not resolve —— 而本地构建始终是好的,只有镜像构建才炸。 +# 真正的修法是把直接 import 的包声明成直接依赖(已做),这里让两边布局一致是第二道保险。 +COPY package.json bun.lock bunfig.toml ./ COPY apps/api/package.json apps/api/ COPY apps/web/package.json apps/web/ COPY packages/contract/package.json packages/contract/