站点架构导入490 字·2026/11/14·被检索 0 次

C-Lab 与题目制作台 · 题库与代码练习

C-Lab 是零依赖 Node 服务,实测监听 127.0.0.1:3103、前缀 /c-lab,提供 C/C++ 在线编译、逐测试点判题、代码版本与 AI 答疑,管理员制作台同进程。题库有本地示例题与笔记站私有题两条来源,题面由 clab_service 账号托管,测试点 24 个。判题走 Windows Job Object 沙箱,逐字节比对;代码笔记以笔记站 NoteVersion 为权威。

挂靠在《四、网站架构总览》之下

版本
使用说明
v1reused-preview2026/11/14 11:16:45

C-Lab 与题目制作台 · 题库与代码练习

一句话:C-Lab 是一个零依赖 Node HTTP 服务,跑在 127.0.0.1:3103、前缀 /c-lab,提供 C/C++ 在线编译、测试点判题、代码版本与 AI 答疑;管理员的「题目制作台」也挂在同一进程里,题库的权威题面则托管在笔记站。

一、定位与入口

项实测值
公网地址https://cag.quest/c-lab/(经 portal 3000 反代)
监听地址127.0.0.1:3103(实测只监听回环,PID 49412)
进程命令node server.js
前缀/c-lab(lib/config.js 的 PREFIX)
计划任务KaoyanCLab,Running;动作 ops\launch.ps1 -Name clab
框架零依赖 Node http,无 Next、无 Express、无 Prisma
数据库c-lab/data/clab.db(node:sqlite,实测 8 张表)
编译器C:\mingw64\bin\gcc.exe / g++.exe
沙箱Python 3.13 + Windows Job Object(lib/runner.py)
笔记站NOTES_BASE=http://127.0.0.1:3102,只走白名单接口

任务描述里提示过 3102/3200,实际都不是:C-Lab 的端口是 3103,3102 是笔记站。这一点已按实测修正。

二、技术栈与目录结构

c-lab/
  server.js                 入口:加载 handlers、路由匹配、静态服务、统一 ctx
  handlers/
    base.js                 GET /healthz、GET /api/me
    notes.js                题库/题面/测试点/笔记站只读通道
    run.js                  POST /api/run、GET /api/runs
    jobs.js                 编译任务轮询
    code.js                 代码笔记读写
    ask.js                  AI 提问
    admin.js                制作台:题目 CRUD、导入、发布、自检、分享码轮换
  lib/
    config.js               端口、前缀、编译器绝对路径、限额
    db.js                   node:sqlite 建表
    identity.js             校验门户头、转发身份头
    notes-client.js         笔记站 HTTP 客户端 + 只读白名单
    problem-notes.js        本地题目 → 笔记站私有题目笔记同步
    problem-admin.js        题目导入/保存/发布
    code-notes.js           代码笔记(笔记站 NoteVersion 为权威)
    compiler.js             编译运行编排
    judge.js                逐测试点评级与汇总
    runner.py               Windows Job Object 沙箱
    queue.js                编译队列(并发 2)
    md-testpoints.js        解析 ```testpoints 区块
    prompt.js / deepseek.js / ratelimit.js
    seed.js                 开箱示例题
  public/
    index.html              单页工作台
    app.js / app.css        前端逻辑与样式
  data/clab.db
  testdata/                 RMQ 20 点压力题与校验脚本
  tools/                    生成、校验、端到端脚本

server.js 启动时扫描 handlers/*.js,把每个模块导出的 routes 表编译成正则;路由 key 形如 'GET /api/problems/',支持 :id 参数,尾斜杠可有可无。新增接口只需加一个 handler 文件,不改入口。

三、数据模型

clab.db 实测 8 张表:

表用途关键字段关系
problems题目索引id、note_id 唯一、title、summary、local_md、service_owned、enablednote_id 指向笔记站 Note;local: 前缀表示尚未同步
test_points测试点problem_id、tp_key、label、stdin、expected、hidden、score、time_limit_ms、memory_mb、stack_kb、language、harness多对一 problems
code_notes用户×题目的代码笔记映射portal_user_id、problem_id、note_id、is_placeholder、current_version唯一 (portal_user_id, problem_id)
runs一次运行portal_user_id、problem_id、language、mode、status、ms一对多 run_points
run_points单测试点结果run_id、tp_key、status、stdout、stderr、exit_code、time_ms、peak_mem_kb多对一 runs
ask_notesAI 提问笔记映射用户、题目、笔记 id指向笔记站
jobs异步编译任务id、status、result前端轮询
settings键值配置key、value—

实测当前:problems 2 条(示例题「求整数平方」与「静态区间最大值查询 RMQ」),test_points 24 条(示例题 4 点、RMQ 20 点),runs 1 条,run_points 3 条,code_notes 与 ask_notes 均为 0(还没有学生提交过代码)。

注意:代码正文不存在 clab.db。lib/db.js 的注释写明「权威来源是笔记站 NoteVersion」,本库只存映射、测试点、运行记录、任务状态。

四、请求链路

以「打开一道题并运行代码」为例:

  1. 浏览器请求 https://cag.quest/c-lab/ → Cloudflare → cloudflared → portal 3000。
  2. portal 命中 /c-lab,proxy(req,res,3103,u=>u),注入 x-portal-secret 与 X-User-*。
  3. C-Lab lib/identity.js 校验密钥,构造 viewer;不匹配则为匿名。
  4. GET /c-lab/api/problems/ 读本地 problems 与 test_points,返回题目列表。
  5. 打开题面时,GET /c-lab/api/notes/detail/?problemId=...:
    • 若 service_owned=0 且 local_md 存在,直接返回本地题面,不碰笔记站;
    • 若 service_owned=1,以 clab_service 服务身份 GET /notes/api/notes/<id>/ 读私有题面;同时取/缓存只读分享码,拼出 notesOpenUrl;
    • 笔记站不可达时回退 local_md 缓存。
  6. 用户写代码后 POST /c-lab/api/run/,requireUser() 要求登录;服务端把任务放进 queue(并发 2),立即返回 jobId。
  7. 前端轮询 GET /c-lab/api/jobs/<id>/,拿到逐测试点结果。
  8. 编译运行:compiler.js 建独立临时目录 → 写入用户代码(函数模式再拼 harness)→ 调 gcc/g++ → 用 runner.py 在 Job Object 里跑每个测试点 → judge.js 比对输出 → 写 runs / run_points。
  9. 若用户保存代码,code-notes.js 以服务身份在笔记站创建 private 子笔记,PUT /notes/api/notes/<id>/content/ 版本 +1。

五、关键机制

1)题目从哪来:两条来源,一个托管账号。

  • 本地示例题:lib/seed.js 在首次启动、problems 为空时插入 prob_demo_square,note_id 为 local:square,题面和测试点都在本地,不写笔记站。
  • 笔记站私有题目:管理员在笔记站建一篇笔记,正文含 ```testpoints 区块;制作台「导入」后,problem-admin.js 解析题面与测试点,problem-notes.js 用 clab_service 账号以 visibility=private、category=c-lab-problem 创建正式题目笔记,再把真实 note_id 写回本地库。

clab_service 是 C 站专用账号(CLAB_SERVICE_USER_ID / CLAB_SERVICE_USERNAME),只用于托管题目笔记与代理创建子笔记,role 仍是 user,不提权 admin。题目笔记不授权给普通用户,学生看不到;学生要打开原题时,C 站用只读分享码拼 /notes/note/<id>/?t=<token>,链接即凭证,不进任何列表。

实测笔记站 Note.category 里 c-lab-problem 有 7 篇;code_notes 表还是 0,说明代码笔记链路已实现但尚未产生真实数据。

2)如何渲染:单页 + Markdown + 双栏工作台。

public/index.html 是一个单页应用:顶部导航有「题库 / 笔记站」,管理员额外看到「制作台」。左侧列表选题,中间是题面 Markdown 渲染区与代码编辑器 textarea#code-editor,右侧三个 tab:运行结果、版本、AI 提问。前端 app.js 调 /c-lab/api/*,所有请求都带 /c-lab 前缀,所以直连 3103 也能工作(只要不带身份)。

题面里的 ```testpoints 区块由 lib/md-testpoints.js 解析成结构化测试点;制作台保存时会把「正文 + 表格里的测试点」重新组装,并自动剥离重复区块。测试点数据量大时注意笔记站正文上限:实测 RMQ 题写回正文 127,448 字符,占笔记站 200,000 上限的 63.7%。

3)代码运行与判题:Job Object 沙箱 + 逐点比对。

server.js 的 POST /api/run/ 限制:代码 ≤64KB、测试点 ≤20 个、body ≤1MB。config.js 的限额是:编译 10 秒、单点运行 2 秒、内存 64MB、栈 1MB、活跃进程 4、输出 64KB、编译并发 2。

runner.py 用 ctypes 调 kernel32,把子进程放进 Windows Job Object,设置 JOB_OBJECT_LIMIT_PROCESS_TIME、JOB_OBJECT_LIMIT_JOB_MEMORY、JOB_OBJECT_LIMIT_ACTIVE_PROCESS 等限制;超时杀进程树,输出一个 JSON:{status, exitCode, timeMs, peakMemKB, violationCode, timedOut}。状态可能是 ok、time_limit、stack_overflow、memory_limit、runtime_error。

judge.js 的比对规则很朴素:把 \r\n/\r 归一成 \n,去掉每行行尾空白,去掉末尾多余空行,然后逐字节完全相等才算 accepted;没有浮点误差容忍、没有 special judge。汇总优先级是 compile_error > time_limit > stack_overflow > memory_limit > runtime_error > wrong_answer,部分通过标 partial。

函数模式只提交函数体,系统按测试点的 harness 分组,每组补 main 与常用头后单独编译;不同 harness 的测试点不能混在一份可执行文件里。

4)与笔记站的关联:代码笔记是题目的 private 子笔记。

每个「用户 × 题目」对应一篇代码笔记:

  • 标题格式 [C-Lab·代码] <题目名> · @<用户名>;没有代码时用 [C-Lab·空] 占位;
  • visibility=private,parentId 是题目笔记 id(本地示例题没有父笔记,退化为顶层);
  • 由 clab_service 代理创建,category=c-lab-code;创建后单独 POST /notes/api/notes/<id>/grants/ 把这篇子笔记授权给当前用户 write,题目笔记本身不授权;
  • 每次上传代码走 PUT /notes/api/notes/<id>/content/,笔记站生成新 NoteVersion,旧版永不覆盖;code_notes.current_version 只记最新版号。

AI 提问笔记同理:[C-Lab提问] ... · @<用户名>,private,挂在代码笔记下。这样学生在笔记站里能看到自己的代码版本与提问,但看不到别人的,也看不到题目原文的授权。

5)只读白名单:C 站不能乱改笔记站。

lib/notes-client.js 的 ALLOWED 是硬编码的正则白名单:只允许 GET 笔记/树/白皮书/关键字、POST 检索、POST 建笔记、PUT 正文、POST/GET 授权、GET/POST/DELETE 分享码。其他方法与路径在客户端层直接 403,不会落到笔记站。这是「只调用、不修改笔记站代码」红线的工程实现。

六、发布与运维

C-Lab 没有构建步骤。改完 server.js、handlers/、lib/、public/ 后重启任务:

Stop-ScheduledTask  -TaskName KaoyanCLab
Start-ScheduledTask -TaskName KaoyanCLab

launch.ps1 -Name clab 会先杀 3103 的监听者,再以隐藏窗口执行 node server.js,日志写 logs/clab-server.log。C-Lab 不跑 prisma generate,也不依赖 .next,所以没有笔记站那类引擎 DLL 锁问题。

常见故障:

  • 所有写接口 401:没有经过门户,或者 portal-secret.txt 与 C 站读到的不一致。C 站只监听 127.0.0.1,本机直连也必须带正确密钥才有身份。
  • 编译报找不到 gcc:计划任务的 PATH 不含 Git Bash 工具目录,config.js 用绝对路径 C:\mingw64\bin\gcc.exe;换机器要改这里。
  • 判题全是 runtime_error:优先检查测试点 stdin 是否是字面 \n 而不是真实换行;制作台已做自动还原与预警,但外部脚本灌库仍可能踩。
  • RMQ 保存失败:测试点写回笔记站正文超过 200,000 字符上限;减少测试点数据或改「测试点只存本地库」。
  • 任务卡在 queued:编译队列并发只有 2,前面的编译没释放;查 logs/clab-server.log 与孤儿 gcc 进程。

七、现状与改进点

结构债一:Windows 强绑定。 runner.py 用 kernel32 的 Job Object,gcc 是 C:\mingw64 绝对路径,Python 是 WorkBuddy 自带的 3.13。换 Linux 或容器要重写沙箱层。方案里预留了 Docker(L1),但当前是 L2。

结构债二:单机单进程。 编译队列在进程内,重启丢任务;并发 2 是硬编码;runs 历史没有清理策略。学生量上来后会成为瓶颈。

结构债三:题面与测试点分居两库。 题面权威在笔记站,测试点在本地 test_points,但制作台保存时又把测试点写回笔记站 MD 的 ```testpoints 区块。两份数据靠「导入/保存」同步,存在漂移风险。

风险:判题过于严格。 逐字节比对意味着行尾多一个空格、末尾多一个空行都会 WA;对 C 语言初学者不友好。至少应提供「忽略行尾空白」的默认开关,或按题型选择比对策略。

建议:把编译沙箱抽成可替换后端(本地 Job Object / Docker / 远程 judge),队列改成持久化任务表;测试点只保留一个权威源(建议本地库,笔记站只存题面);给判题增加可配置的比对器与部分分规则。