Commit Graph
238 Commits
Author SHA1 Message Date
xuyueandClaude Opus 5.5 61ec7d7f8e fix(提交列表): 不在班级列表里的班(毕业、换届摘掉的老班)在班级下拉里选不到,筛不出它的提交
Deploy / deploy (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:31:50 +08:00
xuyueandClaude Opus 5.5 923516ad36 fix(课堂看板): 这节课整班在画流程图时,点格子、点名字进到的是一片空的代码提交列表
Deploy / deploy (push) Canceled after 0s
看板格子的次数原来是代码和流程图加在一起的,链接却总是去代码那边;名字链接看的「今天交过」
还把比赛提交算了进去。现在格子分开给代码 / 流程图次数、学生分开给今天交过代码 / 画过流程图,
只画了流程图的跳到提交列表的「流程图」那边。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:02:33 +08:00
xuyueandClaude Opus 5.5 52bf2d6111 feat(提交列表): 题号填错一道时顶上提示「没有这道题」;← → 也能翻上一条下一条
Deploy / deploy (push) Canceled after 0s
- 列表接口回带查不到的题号(公开列表只认公开题,比赛列表只认本场的题),
  一次填好几道时打错的那道原来被悄悄忽略
- 方向键恢复成四个都能翻(9 月加的时候就是这样,老师已经用惯了)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:58:31 +08:00
xuyueandClaude Opus 5.5 26c8d65e89 fix(判题/统计): 老师重判一次题目提交数就多一;统计选 231 班会把 2311、2312 班也算进来
- 重判带上原来的结果,落库只做差量:提交数不变、旧结果那一格减一新结果加一,
  通过数按新旧结果调整(原来重判当新提交记账,连点三次就多三)。重判时一起清掉 info,
  判题中那几秒不再按旧结果显示「通过 2/3」
- 三个统计接口加 className,按 class_name 精确匹配(原来拼 ks231 做包含匹配)
- 流程图统计查几道题时要每道都拿到 A/S 才算完成;「每人最好的一次」截断时有标记,
  题号上限 20 道和代码统计一致;流程图列表支持按整名匹配用户
- 删掉统计弹框走后没人用的展开行明细接口,以及每次都白算的「语法未过题数」「最近一次错在哪」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:52:58 +08:00
xuyueandClaude Opus 5.5 f09da88ff7 feat(统计): 统计接口补上每人每道题的每一次提交、流程图每人最好的一次,流程图也能一次查几道题
- 新增 GET /submissions/statistics/grid:范围内每个学生的每次提交(只含普通学生,最多 5000 条),
  没选班级时附带各班花名册人数,统计页按「人 × 题」画方块串、按班级汇总
- /flowcharts/statistics 题号可以一次填几道,多返回 people(每人分数最高的那张)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:28:27 +08:00
xuyueandClaude Opus 5.5 ea480d5e0c feat(提交列表): 老师也能重新判题;列表能按班级、一次按几道题筛,还带「通过几个测试点」
- 重新判题从只放超管改成老师及以上:前端一直给老师亮着按钮,点了只会「权限不足」
- /submissions、/flowcharts 加 className(user.class_name 精确匹配),题号可以一次填几道
- 列表每行带 caseSummary,口径和详情一致,在库里数完只回两个数
- 新增 GET /classroom/lesson:提交列表「这节课」按钮用,班级和题的取法同课堂看板

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:08:49 +08:00
xuyueandClaude Opus 5.5 eb32676de4 feat(题目): 「运行例子」「自己输入」改用和提交同一台判题机——例子上跑出什么样,提交在这几个例子上就是什么样
Deploy / deploy (push) Canceled after 0s
原来试运行走另一台 Judge0:C 是 GCC 9、编译参数也不一样,Python 的语法错误回来的是运行错误,
没有题目的时间内存限制和代码模板,例子上的结果常和提交对不上。现在走本站判题机
(POST /trial-runs),同一套编译命令、这道题的限制和模板,对不对也由判题机比。

- 不落库、不算提交、不进判题队列:api 进程里自己排队,全站同时 TRIAL_CONCURRENCY 批
  (服务器 2、机房 4),每人最多两批,排满或等超过 30 秒就请学生过一会儿再点
- 死循环不停打印时判题机回包会很大,超过 16MB 就掐断,告诉学生输出太多、停不下来
- Python 语法错误用和提交一样的说明:第几行、中文标点标红、一键换成英文
- 运行时错误和提交用同一个解析,说清楚第几行、为什么(下标越界、int() 转换失败、段错误……);
  比赛里照旧只给按结果码的那一句
- 语法没过、服务没跑成时,标题不再说「N 个例子没对上」
- 后台「生成测试点」一并迁过来,编译警告不会再写进 .out;Judge0 的地址和调用全部删掉

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 08:09:44 +08:00
xuyueandClaude Opus 5.5 e7b95d211d fix(题目): 修掉题目页重构审查查出的问题——提交途中换题会把结果算到新题上、运行例子把对的菱形判错、点评完「下一题」不跳等
题目页重构之后按「功能对照 / 状态 / 结果页 / 布局与后端」四路审查了一遍,查出的问题一起修:

提交与换题
- 提交、画流程图途中换题:A 题格式化好的代码会盖掉 B 的草稿,A 判对了把 B 标成已解决、
  弹 B 的点评、题单里跳回题单。现在每个请求回来先核对题目,轮询和推送回来的也核对提交编号
- 画着流程图换到不能画的题,语言退到题目的第一门、还被记成偏好;现在回到学生习惯的那门
- 强制点评评完点「继续 / 下一题」,学生之前在看「题目」页签的话会停在原题
- 离开题目页之后判题、流程图的 WebSocket 一直连着,现在闲 15 分钟断开

运行例子
- 输出开头的空格被削掉,打印菱形这类题对的代码也显示「没对上」
- C 的编译警告拼进了「你的输出」(后台生成测试点也会写进 .out)
- Python 的语法错误(中文标点)被说成「下标有没有越界、除数是不是 0」
- 比赛里仍然把空格画成「·」、把不同的地方标红

入口与协作
- 比赛里不给「课堂统计」、不绑协作:房间和统计都按题号找,比赛题号和公开题撞号
- 「看他交过什么」按整名匹配,不再混进学号相近的同学
- 协作中点上下文条、相似题、下一题开新标签,协作不断
- 题单入口给管理员的「⋯」里留「本题提交」

结果与题面
- 「去自己输入拿 51 这些数试试」取到了题面 HTML 里的颜色值;「题目原话」露出 &quot;
- 流程图 A 级的学生标题下还写着「还没交过」
- 「我的提交」上方补回「答案错误 × 3」这样的计数
- 流程图历次评分能点开看那一版的图;读分数失败不再说「还没交过流程图」;手机上能看流程图点评
- 「结果」页签有新结果没看时加粗上色,看过就恢复
- 空画布拖进第一个节点画布会跳一下;画布底下空着一条
- 比赛没开始写成「已结束」、倒计时超过一个月算错;分隔条拖到头工具栏被挤坏;题单里的题目页有备案页脚
- 流程图历次分数只读需要的几列(一个学生一道题 23 次提交:96 kB → 2 kB)
- 比赛密码、比赛没开始等几条学生看得到的英文报错换成中文

设计文档照实际做法更新了第 5.5、5.6、6、8、9、10、11 节。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:07:16 +08:00
xuyueandClaude Opus 5.5 ee66cef367 feat(课堂): 只画了流程图的学生也算来上过课、做完了题——推断这节课的题、班上几人做完、最近来了几次都认流程图
原来课堂那边只有「我的状态」和看板格子认流程图 A/S,其余统计只数代码提交:
全班画流程图的那节课推断不出题,课堂条和看板都是空的;只画了图的学生在看板上
「最近来了 0 次」。现在代码提交和流程图提交合成一张表来数,口径统一:
流程图 A/S 算做完,交过流程图(含评分中、评失败)算做过。

- 推断这节课的题、「班上 M 人做完」、最近几节课是哪几天
- 看板「最近来了几次」、最后提交时间、各题尝试次数
- 看板没选班时猜正在上课的班
- 班级对比、个人资料的通过数不在这次范围,仍然只数代码

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:06:54 +08:00
xuyueandClaude Opus 5.5 83b6b7a81a fix(题目): 修掉代码审查查出的几处问题——流程图拿到的徽章不弹、点评弹窗读失败时关不掉等
- 流程图画到 A / S 拿到的题单徽章、成就原来不弹:流程图那条 WebSocket 只认评分完成 / 失败,
  成就消息被丢掉了,要等下次换路由拉 pending 才出来
- 强制点评弹窗读不出点评、或者一直交不上时,「下一题 / 继续」按钮也给出来:原来弹窗关不掉,
  只能按浏览器后退;拦下换题时就记上「继续」,按钮文字不会写错
- 「放回编辑器」走正常的换语言路子(画流程图时切回代码编辑器、读那门语言的草稿);
  这道题已经不收的语言(0020 摘掉的 Java / JS / Go)置灰,只能复制
- 举着手时刚交对、1.5 秒内点下一题:原来「确定要换题吗」要问两遍(点评完再问一次),
  现在同意过的那个地址不再问
- 课堂看板上评分中、评失败的流程图不再算「尝试过」,和学生的课堂条一个口径;
  新加的流程图查询和其他查询并行,不再多等一轮数据库
- 课堂条的数据只认最后发出的那次请求:进页面那次后到的话会把刚做完的那道盖回「没做」
- 「A / S 算做完」收进契约,前后端只留一份;协作中老师的判断统一用 useTeacherCollab;
  几处放错位置、说法过时的注释

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 19:35:07 +08:00
xuyueandClaude Opus 5.5 b4afcbd47f chore(流程图): 注明 flowchart/current、history 两个接口为什么还留着、什么时候可以删
Deploy / deploy (push) Canceled after 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:50:05 +08:00
xuyueandClaude Opus 5.5 b5d2f1589e feat(课堂看板): 流程图画到 A / S 在老师的看板上也算做完,只画了图的学生不再显示「没开始」
和学生首页的课堂条同一个口径:格子的状态、做对的时间、尝试次数都把流程图提交算进去;
「最后一次提交」也算上今天画的图。有的题这节课整个班都在画流程图,原来看板上是满屏「没开始」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:42:53 +08:00
xuyueandClaude Opus 5.5 0c2ff68e9b feat(协作): 老师协作时右栏顶上一条协作条,结果页签先摆学生最近一次交的,抽屉变成「他的提交」
- 协作条:「正在帮 谁 · 语言 · 看他交过什么 ↗ · 结束协作」,从工具栏里挪出来,一眼看得出
  编辑器里是学生的代码。「看他交过什么」带上学生用户名、开新标签(原来只带题号,还得再筛一遍)
- 老师接单后「结果」页签原来是空的:现在摆学生在这道题上最近一次交的结果,可刷新;
  不给 AI 提示(那是按登录的人自己的失败次数解锁的)
- 抽屉「我的提交」在协作中变成「他的提交」,列的是学生的提交,只能复制、不能放回编辑器
  (放回去会经协作通道直接盖掉学生正在写的代码),也不显示老师自己的排名
- 学生排队求助中、或者老师正在帮他时换题:先确认一句。换题会断开协作 / 撤掉举手,
  原来一点「下一题」就把正在帮他的老师踢出去了
- 求助按钮文案:「举手求助」→「已举手 · 前面 2 人」→「某某 老师在帮你」
- 提交列表加 exactUsername=1:用户名精确匹配。原来只有「包含」,ks24a1 会把 ks24a10 的提交
  也捞进来

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:28:53 +08:00
xuyueandClaude Opus 5.5 3fe3caa944 feat(流程图): 流程图画到 A / S 算这道题做完——题单进度、课堂条都认;加入题单之前画的图先藏起来
Deploy / deploy (push) Canceled after 0s
- 流程图评到 A 或 S,记进所有已加入、且包含这道题的题单(和判题那一路同一个口径,不看从哪个
  入口交的),徽章和题单类成就照样推送。原来题单进度只在判代码时记,流程图作业在题单里永远是
  「没做」。流程图提交的 id 进不了 problemset_submission(外键指向代码提交表),所以只写
  progress_detail —— 解锁旧提交的那道闸看的也是它
- 课堂条(/me/class-activity)的 myStatus:流程图 A / S 也算 accepted,评完的非 A/S 算 tried
- 题单防抄闸门补到流程图上:加入题单之前画的图,对本人藏起来(列表 showLink、详情 404、
  current / history 不数它),规则和代码提交同一份(problemSetJoinTimes 挪进 services/problemset)。
  原来加入之前画到 A 的那张图点一下就能载回画布、交上去就是 A
- 新增 GET /problems/:id/flowchart/scores:一次给出自己评完的每一次的分数和等级,外加被藏起来
  的次数(原来 history 一次只给一条,看分数走势得一页一页翻)
- 「A/S 算过关」和按分数定等级收进 flowchart/grade.ts;判题那边的记账 + 通知抽成
  recordSolvedAndNotify,两条路共用

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:56:35 +08:00
xuyueandClaude Opus 5.5 71670e609b feat(成就): 新增「最长连续新通过周数」指标,按课表的节奏算连续;修掉比赛成就重扫因孤儿提交失败
Deploy / deploy (push) Canceled after 0s
按天的连续成就对这里的学生基本拿不到:2025 秋最长连续交题天数的中位数是 1 天,
连续 7 天的只有 3 人。按周算,连续 2/4/8 周分别有约 73%/49%/8% 的学生做到过。

- 新指标 max_ac_week_streak:连续几个自然周(周一起、东八区)每周都至少新通过一道题
  (不含比赛)。判题结算时增量累计(比按天那段多一道判断:重判更早的提交不会把连续清零);
  后台建成就触发重扫时,先从提交记录全量补算(refreshAcWeekStreakForAll),同 contest_joined
- 老的「坚持三天」「坚持一周」不动:下架的成就连已获得的人也看不到
- 修:库里 28 个已不存在的用户留下了 935 条提交(其中 11 人有比赛提交),全量补算往
  user_stat 写时撞外键,整次失败——比赛类成就的重扫因此一直是失败的;两处都改为只算
  还在的用户。本地修好后重扫「初入赛场」补发了 3 人
- 本地验证:连续 2/4/8 周补发 1086/618/123 人,与独立 SQL 一致

线上要在后台手动建成就(指标选「最长连续新通过周数」),建的时候自动补发。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 08:27:25 +08:00
xuyueandClaude Opus 5.5 a7f6ebaf49 feat(激励): 知识点地图——每个知识点做对几道、在哪一档、再做几道升级,首页一句话提示本周进步
Deploy / deploy (push) Canceled after 0s
现有 38 个成就全是累计量(通过几题、提交几次、连续几天),起步的人人都有、往上的够不着,
而且没有一个在说「你学会了什么」。连续打卡在这里结构上做不到:2025 秋学生最长连续交题
天数的中位数是 1 天,连续 7 天的只有 3 人。

- GET /me/knowledge:题数 ≥10 的知识点标签(10 个),每个给做对几道、档位、周一时的档位、
  下一档门槛;四档 入门 1 / 会了 3 / 熟练 题数 40%(≤10)/ 精通 75%(≤20)。
  回测 2025 秋:中位数学生一学期升 5 档;「入门」一档是给只做对一两道的 71 人加的
- 同班 ≥30% 点亮过的知识点才会出现在「还没碰过」里,免得 Python 班看到「C 语言」
- 个人主页加「我的知识点」,只在看自己时显示;首页一句话:本周升级 > 离下一档最近的 >
  去点亮一个班里在学的,点了去按知识点筛的题目列表

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 23:17:43 +08:00
xuyueandClaude Opus 5.5 a6bf845e06 feat(课堂看板): 标出最近几节课经常没交的学生,求助的学生置顶,点一下打开求助列表
Deploy / deploy (push) Canceled after 0s
2025 秋至少上过 5 节课的学生里,12.5% 七成以上的课一道都没交,37% 的学生贡献了
一半以上的「没交」;当堂那份「今天还没交过」把偶尔没来的和一直不动手的排在同一行。

- 看板接口给每个学生加 recentAttended:最近几节课(最多 5 节、不含今天、45 天内;
  一节课 = 同班同日 ≥5 人做同一题)里交过几节,另给 recentLessons
- 「今天还没交过」拆成「经常没交」(≥3 节可比且最多交过 1 节)和其他;表格加一列
  「最近 N 节」;只给老师看,投影模式不出现
- 求助读老师端常驻 WebSocket 的 collabStore.requests(不改后端):求助的学生置顶,
  状态显示「举手了 / 老师在帮」,点一下打开求助列表;求助提示不论布没布置题都显示

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:47:24 +08:00
xuyueandClaude Opus 5.5 80106eaa04 feat(课堂): 老师的课堂看板——布置这节课的题、看全班谁还没开始谁卡住了,布置的题直接出现在学生首页
2025 秋平均每节课有 14 个平时在用的学生一道都没交(三分之一),交了 3 次以上还没过的
平均不到 1 个;而老师原来只能一道题一道题打开统计弹窗看。

- 新表 class_lesson(迁移 0023,仅建表):一个班一天一行,存老师布置的 problem.id 与顺序;
  day 存东八区日历日文本,created_by CASCADE
- GET /classroom/board:这个班今天的题 × 全班学生(通过与否不限今天、今天交了几次、
  最后提交时间);不给班级时猜最近两小时提交人数最多的班,不记老师上次选的
- PUT /classroom/lesson:按展示题号布置,对不上的题号点名拒绝,空数组 = 清掉
- /me/class-activity 优先返回老师布置的题(source = teacher),否则照旧推断
- 前端 /classroom:没交过的名单置顶,按 没交过 → 卡住 → 在做 → 做完 排序,15 秒刷新;
  投影模式只显示每题完成人数、不出名字;用户菜单给老师加入口
- 学生首页:老师布置的标题写「今天老师布置的题」,没人交时不显示 0/0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:37:59 +08:00
xuyueandClaude Opus 5.5 7e964fdbdd feat(题目): 答案错误时拿公开样例重跑,并排显示正确输出和你的输出,按差异类型给中文提示
Deploy / deploy (push) Canceled after 0s
原来答案错误只有四个字。6901 条历史答案错误在公开样例上重跑过:73% 在样例上就错了,
其中约三分之一只差格式(没输出、input() 提示文字、空格换行、中英文标点、小数位数)。

- judge/run.ts 的 checkSamples:判出答案错误后以内联测试点重跑题目样例(output: true),
  记下第一个没过的样例和学生在它上面的输出,写进 statistic_info.sample_check;
  样例全过则只记 passed。全是公开信息,不碰隐藏测试点;比赛不跑(有期末考试)
- 前端按差异归类(数字先于「多了内容」判,免得 007 被当成多打了文字),
  并排显示输入 / 正确输出 / 你的输出,标出第一处不同的行;只差空白时把空格、换行画出来,
  末尾空白不画(判题本来就忽略)
- 提示给具体值(「正确输出是 84.10,你输出的是 84.1」),input("…") 单独提示

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:14:24 +08:00
xuyueandClaude Opus 5.5 db70ab62e8 feat(题目): 运行时错误告诉学生第几行、为什么出错(Python 看回溯,C 看信号),不泄露测试数据
原来学生只看到「运行时错误」四个字:主判题 output: false,Python 回溯没回来;
C 的信号值在 info 里,而 info 只给管理员。

- Python 判出 RE 时,拿第一个失败测试点单独重跑一次(内联测试点 + output: true),
  从回溯取行号(扣掉模板前置行)、异常类型和按句式归的类别。主判题仍不开 output,
  免得每个点最多 16MB 的输出回到 worker
- C / C++ 取失败点的信号和退出码(段错误提示 scanf 漏 &、return 非 0 等)
- 写进 statistic_info.runtime_error;不存异常原文(里面常带测试点输入),名字只在
  出现在学生代码里时才存——raise Exception(input()) 实测只落下类型
- 前端按类型翻成中文、摆出出错那行并在编辑器标红;比赛提交不诊断(ACM 只报对错)
- 没有诊断的 RE 不再说「代码格式写错了」

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:01:30 +08:00
xuyueandClaude Opus 5.5 2a4405d2ea feat(题目): Python 语法错误改成中文说明,在编辑器里标出位置,中文标点可以一键换成英文
Deploy / deploy (push) Canceled after 0s
中职学生看不懂英文报错。原来提交前用浏览器里的 Skulpt 拦语法错误,只弹一句
「第 N 行存在语法错误」——Skulpt 九成报的是 bad input / bad token,没有列号。

- 提交前的语法检查挪到服务端:/code/format 先用 CPython 编译一遍(只编译不执行),
  api 镜像加 python3-minimal(3.13,与判题机同版本,+17MB),前端删掉 Skulpt
- 报错按规则表翻成中文(pythonError.ts),生产库 2695 条 Python 编译错误覆盖 99.7%;
  提交前拦下的和判题回来的编译错误共用同一张卡片
- 编辑器里标红出错行和 ^ 指的位置
- 中文标点(历史编译错误的 22%)一键换成英文,跳过字符串和注释、处理 f 字符串花括号;
  594 条里修好 593 条,经 tokenize 核对没有改到字符串内容;走编辑器事务,可 Ctrl+Z

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:47:01 +08:00
xuyueandClaude Opus 5.5 039992dc86 feat(前台): 顶栏可以输入题号直达;首页列出班里最近一次课一起做的题和自己的完成情况
Deploy / deploy (push) Canceled after 0s
课上老师报题号、全班去找,是 2025 秋七成非比赛提交的来路,界面上原本没有它的入口。

- 顶栏(桌面端)加题号框,首页「找题」同一套逻辑:题号对上直接进题,否则去列表搜
- 新接口 GET /me/class-activity:同班同一天 ≥5 人做过的题,取最近 7 天里最近的一天,
  附带我自己的状态与班里通过人数;首页据此显示「今天班里在做」卡片,没有就不占位

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:58:15 +08:00
xuyueandClaude Opus 5.5 d07459928d feat(标签): 题目列表的标签分成「知识点」「主题」两组,新建的标签在后台标出待归类
Deploy / deploy (push) Canceled after 0s
- 迁移 0022:problem_tag 加 category(knowledge / theme)和 category_confirmed;
  存量 25 个题材、题集类标签归主题,其余归知识点,全部记为已确认
- 前台题目列表按两组显示标签,组内按题目数从多到少
- 后台标签管理:每行切换分类;出题时新建的标签默认知识点、标「待归类」并排在最前,
  页头提示待归类数量,可点「确认」保留默认;加分类筛选
- 新接口 PUT /admin/problem-tags/:id/category,设置分类即视为确认

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 20:38:43 +08:00
xuyue d25b5dbe6d update
Deploy / deploy (push) Canceled after 0s
2026-09-27 19:36:28 +08:00
xuyueandClaude Opus 5.5 23844ae301 fix(班级筛选): 输入 ks251 会把 2511 班的学生也算进来
Deploy / deploy (push) Canceled after 0s
班级号 3~4 位并存,按用户名前缀/包含匹配时 ks251 会捞到 ks2510~ks2519。
新增 classPrefixCondition:ks + 3 位以上数字按具体班级匹配(后面不能再跟数字),
1~2 位仍是年级前缀。后台首页的用户排名、随机抽签,以及题单的学生进度筛选
(只填数字时补上 ks)都改用它。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 19:34:28 +08:00
xuyueandClaude Opus 5.5 9412ba3cf6 refactor(契约): 展示题号统一叫 problemDisplayId,题单 id 统一叫 problemSetId
Deploy / deploy (push) Canceled after 0s
前后端同时改,部署前开着的旧页面要刷新一次(题号列会是空的、提交筛选会失效)。

- 挂在非题目对象上的展示题号,原来叫 problem(字符串)或 displayId,统一成
  problemDisplayId:提交列表、流程图列表、统计的 lastFailure / items /
  今日最热题、站内信里的提交、后台题单的题目列表。别处的 problem 都是对象
  或数字 id,这几处是字符串,最容易看错
- 列表筛选的 query 参数 ?problemId= 实际传的是展示题号,改成
  ?problemDisplayId=(提交列表、流程图列表、三个统计接口)
- 契约里 problemsetId / problemSetId、problemset / problemSet 两种拼法并存,
  统一成 problemSetId / problemSet,和 ProblemSet* 类型名、problem-sets
  路径、前端路由参数一致。drizzle 的列键跟表名走,没动
- aiDetail.user(用户名字符串,而契约里别的 user 都是对象)→ username。
  ai_analysis.data 里存的历史快照仍是 user,没有代码读回它
- 后台题单题目表的列是按字符串键 'displayId' 取的,类型检查抓不到,一并改

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 16:08:31 +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.5 572a818d49 style: 按 Oxfmt 重新格式化
Deploy / deploy (push) Canceled after 0s
换格式化器带来的差异,只有 5 个文件:长联合类型改成每项一行、以 | 开头。
和上一个提交分开,方便 blame 时跳过(git blame --ignore-rev)。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:37:23 +08:00
xuyueandClaude Opus 5.5 f8b04af4ea chore: 加 oxlint,现存 19 条 warning 清零
Deploy / deploy (push) Canceled after 0s
原来只有 Prettier,没有任何 lint。根目录加 `bun run lint`(oxlint,覆盖后端、前端、
契约,--deny-warnings:有一条就算失败),CLAUDE.md 常用检查里加上。

现存 19 条顺手清掉:
- 11 处未使用的 import / catch 参数 / 回调参数
- oj/store/problem.ts:`...problem.value?.languages` 展开可能是 undefined
  (外层 if 已经保证非空,去掉 ?.)
- admin/problem/detail.vue:`contestId = contestId` 自赋值,删掉
- judge/sql/engine.ts:注释里的 `/*x*/` 示例原来靠零宽空格防止提前闭合注释,
  改成 `*\/`
- 其余:多余的 !!、new Array(n)、展开里多余的 `|| {}`

两边类型检查、前端构建、后端单二进制编译都过。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:25:27 +08:00
xuyueandClaude Opus 5.5 e17da63fc6 fix(上传): 头像和题面插图按文件头认格式,不信扩展名
原来只看扩展名:改个后缀什么都能落盘,再由 /public/* 按扩展名配 content-type
发出去。线上 Caddy 有 nosniff 兜着,但不该把非图片存到盘上。

- services/image.ts:按 PNG / JPEG / GIF / BMP 的文件头识别,返回落盘用的扩展名 ——
  以内容为准,改名成 .png 的 JPEG 存成 .jpg,content-type 才对得上
- 头像上传、后台插图上传都走它;插图那边扩展名白名单保留当第一道快速筛
- serveUpload 自己也带 x-content-type-options: nosniff,不只靠 Caddy

实跑核过:真 PNG / GIF 正常;改名成 .png 的 JPEG 存成 .jpg;改名成 .png 的 HTML
两个入口都拒;/public/upload 回 image/png + nosniff。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:15:36 +08:00
xuyueandClaude Opus 5.5 b105fe3cbd fix(测试点): 解压前按条目头拦大小,防 zip bomb
原来 unzipSync 先把整包解进内存,再逐个查大小 —— 检查之前就已经解完了。
实测一个 300KB 的 zip(10 个 30MB 的全零文件)让进程 RSS 涨 455MB,
容器只有 512MB。

- 第一遍 unzipSync 的 filter 只读目录、恒返回 false,按条目头的 originalSize
  累计,单个超 32MB / 总计超 128MB 就抛;第二遍才真解,而且只解 N.in / N.out /
  N.sql(N ≤ 500),压缩包里别的文件一律不解
- originalSize 可以造假,但 fflate 按它分配输出缓冲、不扩容(解多了只截断),
  所以按它限额就是按实际内存限额

实测:同一个 zip bomb 被拦下时 RSS 只涨 1MB;单文件 300MB 的包按单文件上限拦;
正常测试点照常落盘、CRLF 照常归一、带 ../ 的条目名照常被忽略。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:14:12 +08:00
xuyueandClaude Opus 5.5 6406794971 refactor(AI): 学情统计查询从 routes/ai.ts 挪到 services/learning-stats.ts
routes/ai.ts 1149 行里约六成是 buildSolved / buildDetail / buildDuration 这些查询
构造器,它们不碰 Context,只是查询。挪出去之后 route 文件剩 585 行,只管
「鉴权 → 取参 → 调统计 → 回包」和 AI 调用本身;targetUser / throttleAi 依赖
Context,留在 route 里。

纯搬移,函数体一行没改。实跑核过:同一个用户的 /ai/detail、/ai/duration(按月、
按周)、/ai/solved、/ai/heatmap 五个响应,改前改后逐字节一致。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:09:21 +08:00
xuyueandClaude Opus 5.5 925cecb389 refactor(题目): 新建 / 复制题目收成 validateProblem、insertProblem、copyProblem
admin/problem.ts 里四处 insert(schema.problem) 其实是两对重复:

- 新建公开题 / 新建比赛题:公共校验 + SQL 展示数据 + 插入 + 写标签,逐行一样,
  只差 contestId。编辑题目那条也抄了同一段校验。收成 validateProblem(校验 +
  展示数据)和 insertProblem(插入 + 标签,一个事务)。比赛题当初就是抄漏了
  sqlDisplay,现在三条路径只有一份
- 比赛题转公开 / 从公开题库拉进比赛:复制整行 + 统计清零 + 复制标签,收成 copyProblem

顺带 db/index.ts 加 DbOrTx 类型,四处 `tx as unknown as typeof db` 去掉。

唯一的行为差异:编辑题目时 SQL 展示数据的生成挪到了题号查重之前(和新建一致),
两项同时出错时报的是前一个。

实跑核过:新建公开题、编辑、重复题号 409、新建比赛题、比赛题转公开(不可见、
标签跟过去)、从公开题库拉进比赛(可见、标签跟过去),SQL 题测试点缺失时照样拦下。
测试数据已删。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:07:13 +08:00
xuyueandClaude Opus 5.5 f2ca34fd4b refactor(api): 请求体解析收成 readJson / parseBody
54 处 handler 都手写同一段:
  const parsed = X.safeParse(await c.req.json().catch(() => null))
  if (!parsed.success) return failure(c, 400, "invalid-request", ...)
漏写 .catch 就是一个畸形请求打出 500。

- http.ts 加 readJson(读 + 校验)和 parseBody(再加上 400)。parseBody 不给
  message 时回 zod 的第一条 issue —— 后台表单原来就是这么写的,fallback 文案
  「Invalid payload」/「参数错误」只在 issues 为空时才用得上,实际走不到
- 51 处换成 parseBody;另 3 处失败时带别的错误码或和路径参数一起判,换成 readJson
- 错误码、状态码、文案、判断顺序都不变

实跑核过:非 JSON 请求体回原来的固定文案,公告标题为空回 zod issue。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:04:19 +08:00
xuyueandClaude Opus 5.5 09030240b8 fix(AI): 流式调用加空闲超时、客户端断开即停上游、错误原因不下发
- streamChat 加 45 秒空闲超时(每收到一块重新计时,不掐长回答),原来 provider
  卡住这条请求就永远挂着
- 加 cancel():客户端断开时 abort 上游 fetch,原来会把 provider 的回答读完白烧 token,
  还把「客户端走了」记成 AI 失败;现在留痕写「客户端已断开」
- 推给前端的 error 事件改成固定中文文案(缺 AI_KEY 时是「AI 功能未开启」)。原来
  error.message 原样下发,里面可能是 provider 的 HTTP 响应体或数据库报错;
  真实原因只进 console.error 和 onError 留痕

用假 provider 实跑核过:正常流式、客户端 1 秒断开时上游被取消、provider 500 时
客户端只拿到固定文案(响应体只在日志里)、provider 挂住 45 秒后收到 error + end。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:01:43 +08:00
xuyueandClaude Opus 5.5 04c0ccd304 fix(AI): Bun.serve 空闲超时调到 60 秒,AI 流不再在 10 秒静默后被掐断
Bun.serve 默认 idleTimeout 是 10 秒:连接上 10 秒没有字节往来就直接断开,不报任何错。
AI 的两条 SSE 都会静默超过 10 秒 —— streamWhole(AI 提示)在 produce 期间每 15 秒
才发一次心跳,一次生成加过滤重生成常常超过 10 秒;streamChat 等首 token 也常过 10 秒。
学生看到的就是「AI 提示生成失败」。

对齐反代链上最短的一环(NPM proxy_read_timeout 60s)。实测:provider 挂住时原来
10 秒整连接被断、客户端收不到 error 事件;改后按 streamChat 自己的 45 秒空闲超时收尾。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:01:43 +08:00
xuyueandClaude Opus 5.5 b38ad8ccee feat(配置): 启动时用 zod 一次解析环境变量,有错拒绝启动
原来是 `Number(env ?? 默认值)` 散写:JUDGE_CONCURRENCY=two 得到 NaN 照样起;
DATABASE_URL 漏配就静默连上开发库的默认账号;AI_KEY 缺了要等第一次 AI 调用才报错。

- config.ts 一张 envSchema 覆盖全部变量,失败时列出所有问题后 exit 1
- 编译形态(线上)下 DATABASE_URL / REDIS_URL / JUDGE_SERVER_TOKEN 必须显式给,
  dev 默认值指向本机 5433 / 6380,放进容器里只会连不上或连错
- 空字符串按未设置处理(compose 的 ${X:-} 展开就是空串)
- AI_KEY 为空启动时告警(AI 是可选功能,不退出)
- db/index.ts 改读 config.databaseUrl;flowchart/run.ts 的 provider / model 改读 config

实跑核过:非法值被列出并拒绝启动;编译产物在仓库外无 env 时报三项必填,
给齐后 /health 正常;dev 的 serve / worker 照常起。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:53:51 +08:00
xuyueandClaude Opus 5.5 fb240d4254 feat(限流): 登录、注册、比赛密码防爆破
三处原来都不限次数。学生初始密码弱,/classes/:name/usernames 又能公开列出整班
用户名,等于现成的撞库名单。

services/throttling.ts 加固定窗口计数(INCR + EXPIRE NX,锁定时长就是剩余 TTL),
何时计数由调用方决定:

- 登录按用户名 15 分钟 10 次失败,成功清零;按 IP 15 分钟 100 次失败,成功不清零
  (否则拿自己的号登一次就能重置)。机房一个班共用出口 IP,所以 IP 那道放得宽
- 注册按 IP 每小时 100 个成功注册
- 比赛密码按「人 × 比赛」10 分钟 10 次失败
- IP 取 X-Real-IP:api 端口不对外,Caddy 用 {client_ip} 覆盖写入,可信

前端登录、注册、比赛密码三处按新错误码给中文提示。

本机实跑核过:第 11 次错误登录被拦、成功清零(用户名大小写不敏感)、同 IP 换
100 个用户名被拦且别的 IP 不受影响、比赛密码第 11 次被拦。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 14:51:52 +08:00
xuyueandClaude Opus 5.5 9d271942aa feat(AI 提示): 题面补上输入输出说明和样例,HTML 去标签
Deploy / deploy (push) Canceled after 0s
两段(诊断 / 生成提示)原来只喂 description,而且是 HTML 原文:模型拿不到输出格式
要求,「输出格式不对」这类最常见的 WA 基本看不出来。

- problemBrief:描述 + 输入说明 + 输出说明 + 最多 3 组样例,末尾点明判题逐字比对。
  全是学生看得到的内容,两段都放;标准答案和测试点仍只进诊断
- plainText:去标签、段落 / <br> 换行、表格单元格补空格、解码命名和数字实体
  (&#039; 这类带前导零的也认),图片留「[图片]」占位;样例原样不去空白
- 阶梯 prompt 版本 3 / 4 → 5 / 6,旧常量标停用保留;编译失败那一档(1 / 2)不动

用 2026-09-14 的生产备份核过:962 道题的题面无残留标签 / 实体、无裸 `<`。
另有 341 道题面带图片、模型看不到,本次未处理。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-27 14:28:21 +08:00
xuyueandClaude Opus 5.5 22fc8d63e2 feat(AI 提示): 阶梯延到 L3 思路 / L4 示例
Deploy / deploy (push) Canceled after 0s
- 契约 HINT_LEVELS 加两级,HINT_MAX_LEVEL 2 → 4。前端的等级标签和「再多一点提示」
  按钮本来就跟着契约和 canEscalate 走,不用改
- L3 分步骤讲思路(中文伪代码),沿用 L0~L2 的零代码规则
- L4 是阶梯上唯一放开代码的一级,上限两行:过滤不再「出现代码就拦」,而是把代码块
  非空行、摊平进正文的类代码行、过长的行内代码合起来数,超过 KEY_LINES_MAX 才拦;
  没闭合的围栏按开到结尾算。重生成措辞和兜底话术按级补齐
- prompt 版本沿用 3 / 4:LEVEL_COMMON 拆成三句再拼回,L0~L2 的 system 逐字未变
  (已对照改前逐字核过),新两级靠 ai_hint.level 区分。不需要迁移

老师按班级 / 作业设最高等级拆到 2e,未做。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 09:20:24 +08:00
xuyueandClaude Opus 5 b16e11e85d fix(题目推荐): 已 AC 过滤下推 SQL、难度按语义排序、难度标签渲染
Deploy / deploy (push) Canceled after 0s
相似题目推荐(GET /problems/:displayId/similar)三个 bug:

- 排除已 AC 的题发生在 limit(5) 之后,候选被筛掉几条就少几条,甚至清零让整块
  不渲染。而这个接口只在刚 AC 或连挂三次时才调用,正是最容易全中的时候。
  (老栈是 .exclude(...) 在切片之前,重写时引入的回归)
- order by difficulty 是 text 字典序,High < Low < Mid,「由易到难」实际是最难的
  先上。改成显式 case 排名,并加上共享标签数作为第一排序键、id 兜稳定序。
- 前端 ref<any[]> 盖住了类型错误:getSimilarProblems 已过 filterResult,难度是
  中文,模板却在比 'Low' / 'High' 再查 DIFFICULTY 表,结果每条推荐都渲染成黄色
  的「中等」。

顺带:比赛题不再发这个请求(接口只在公开题库按 displayId 找,撞号会在比赛中把
题库列给学生);多余的 id in (子查询) 换成 join 中间表。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 09:24:46 +08:00
xuyueandClaude Opus 5 954797a9a5 feat(AI 提示): 提示分级(L0 反问 / L1 定位 / L2 概念)与输出后过滤
Deploy / deploy (push) Canceled after 0s
提示不再一上来就把话说完。等级记在「学生 × 题目」上,没有单独的表 —— 它就是
ai_hint.level 的历史,当前等级 = 这道题上(最近一次 AC 之后)给过的最高一级。

阶梯(services/hint-level.ts)
- 只有学生点「再多一点提示」才升级(请求带 more),不带就按当前等级再生成一次
- 升一级要先再交一次:锚点是「这一级是**什么时候**开出来的」,也就是这一级最早那条
  提示的 ai_hint.create_time,必须有比这个时刻更新的提交才准 +1。锚点不能用提示所在
  那条提交的时间 —— 端点谁的提交 id 都认(只校验归属),拿一条老提交去要提示,锚点
  就退回到那条老提交的时间,连点两下 more 就能从 L0 爬到 L2,一次新提交都不用交
- AC 之后清零;编译失败自成一档(level = -1),既不消耗也不推进阶梯
- HINT_MIN_FAILURES 3 → 1:门槛的活由阶梯接走了,第一次失败只开放 L0,而 L0 只反问、
  什么都不泄露,拦着它没有意义
- canEscalate 由后端算好在 done 事件里给,前端不自己推阶梯

输出后过滤(services/hint-filter.ts)
- 「不要给代码」写在 prompt 里只是软约束,模型忍不住一次就把这一级的意义废掉了。
  所以整段生成、过滤通过才推给前端,逐字显示改由前端模拟 —— 边流式边过滤做不到,
  发现违规时内容已经在学生屏幕上了
- 判定只用客观、低误报的信号:代码块、过长的行内代码、整行不含中文的类代码行、
  和标准答案重合 3 行以上、L0 一句问句都没有
- 违规就重生成一次,只重一次,再不过发写死的兜底话术。重试措辞按档分叉:编译档本来
  就允许给片段,对它说「不要出现任何代码」等于用阶梯的标准把这一档也砍了
- 两次都留痕(filter_attempt / filter_blocked / filter_reason),7.5 的输出过滤触发率
  就是从这三列出来的

services/ai.ts 加 streamWhole:事件形状和 streamChat 一样,前端不分叉。produce 期间
每 15 秒发一行 SSE 注释当心跳 —— 这条流中间有一大段静默(诊断 20s + 生成 60s +
重生成 60s,最坏 140 秒),而 NPM / nginx 的 proxy_read_timeout 默认 60 秒,超了学生
看到「请求失败」,后端却还在烧第二次调用,那条提示照样落库、照样把等级推上去。

prompt 版本另开 3 / 4(阶梯上每一级都换了 system),编译档仍走 1 / 2 的单段式基线,
两批数据不混在一起。迁移 0021 给 ai_hint 加四列,都可空、不带默认值,已有的行留 null
表示「分级上线前」。

实跑
- 阶梯:在 dev 库上用真实行驱动 decideHintLevel。正常路径 S1→S2→S3 走出 L0→L1→L2,
  同级连点 more 不升,AC 之后回 L0,编译档给 L-1 且不推进阶梯。把旧锚点规则复刻出来
  跑同一组数据做对照:只拿最老那条提交反复 POST,旧规则 L0→L1→L2(零新提交),
  新规则钉死在 L0,正常路径两者行为一致
- 过滤:起假 AI 服务端走完整条链路。L1 摊平代码→重生成后合规(attempt 2 / 未拦),
  L0 两次都甩代码块→兜底话术(blocked,reason 两条相连),编译档抄标程→命中「和标准
  答案重合 3 行」且追加的是分叉后的措辞
- streamWhole:produce 拖 16.5 秒收到 1 条心跳;同一份流喂给前端 consumeJSONEventStream
  只解析出 delta + done(注释行被静默跳过,前端零改动);中途 cancel 断开后 produce
  跑完不抛
- api / web typecheck、check:routes、fmt 全过

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 19:03:21 -06: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
xuyue a872e8365b fix
Deploy / deploy (push) Canceled after 0s
2026-09-19 07:10:49 -06:00
xuyueandClaude Opus 5 b4c0f89291 fix(个人主页): 总共学习天数改为有提交的天数
Deploy / deploy (push) Has been cancelled
原来是首末提交之间跨了多久,现在按东八区日期对提交去重计数,比赛提交也算。
首末提交时间仍只看比赛外的提交,口径不变。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 07:07:57 -06:00
xuyueandClaude Opus 5 06ad6745b7 feat(AI 提示): 两段式提示——先诊断出错误标签,再生成提示
Deploy / deploy (push) Has been cancelled
AI 时代 OJ 设计的 2b。标准答案能让提示准得多,但不能进生成提示的 prompt
(学生代码里一段注释就能把它套走),所以拆成两段:
- 诊断:看得到标准答案、第一个没过的测试点、带行号的学生代码;出参只允许
  { tag, lines, confidence },safeParse 过闸、多余字段剥掉,没有自由文本通道
- 生成提示:看不到标准答案和测试点原文,只多一句「问题定位:X,大约在第 a 行」
- 诊断失败(20 秒超时、不是 JSON、校验不过)退回单段式;同一条提交复用诊断;
  编译失败不诊断
- 契约新增 HINT_ERROR_TAGS(13 个,落库值,只增不改)与 hintDiagnosisSchema
- 迁移 0018:ai_hint 加 diagnosis / diagnosis_error 两列
- 单段式 prompt 原样搬进 services/hint-diagnosis.ts,记版本 1;两段式记版本 2
- completeChat 支持 JSON 模式和自定义超时,现有调用不受影响
- 开关 AI_HINT_DIAGNOSE 默认关(2a 的基线还在攒),两套生产 compose 透传

实跑(一次性库 + 本地假 LLM):开关关时请求与 2a 逐字节相同;开时正常 /
复用 / 坏 JSON / 非法标签 / 行号越界 / 超时 / 编译失败七种场景符合预期;
12 次模型请求里诊断全都带标准答案和测试点,生成全都不带。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:56:38 -06:00
xuyueandClaude Opus 5 47d8f46bdb feat(AI 提示): 提示落库并收集学生评价,落进 ai_hint
Deploy / deploy (push) Has been cancelled
AI 时代 OJ 设计的 2a:先把现有提示记下来,为后面的分级和两段式诊断攒对照数据。
提示本身的行为不变,发给模型的 prompt 一字未改。
- 迁移 0017 建 ai_hint:模型、prompt 版本、内容、错误、耗时、学生评价;
  成功和失败都记(失败率是 AI 悄悄变差时最先动的数)。挂在 submission 上 CASCADE
- streamChat 第三个参数改成 { onComplete, onError }:onComplete 的返回值并进
  done 事件,提示 id 由此带回前端;班级学情分析那处调用同步改写,行为不变
- 落库失败只记日志,不影响学生拿到提示
- 新增 POST /ai/hint/:id/feedback:只能评自己的提示(别人的一律 404),可以改票
- 前端提示生成完出「有帮助 / 没帮助」,选中的高亮;后端没存上时不出按钮
- 实跑:本地假 LLM 验证成功 / provider 断开 / 缺 AI_KEY 三条路径的落库,
  评价接口六种请求,浏览器里按钮出现、改票、重复点不发请求

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:00:10 -06:00
xuyueandClaude Opus 5 a0ef204bd2 feat(提交): 采集编辑过程信号,落进 submission_trace
Deploy / deploy (push) Has been cancelled
AI 时代 OJ 设计的第 1 步:给「可信 AC」和学情分析攒数据,本身不判任何事。
- 契约:提交请求加可选的 trace(活跃时长、键入/粘贴/删除字符数、切后台次数等,
  只有计数、不含按键内容);写成 .optional().catch(undefined),坏了就当没带,
  不让附带数据把提交挡成 400
- 迁移 0016 建 submission_trace,与 submission 一对一、CASCADE;since_prev_ms
  由服务端在同一条 INSERT 里算(排掉自身),bigint —— 实测已有 44 天的间隔,int4 装不下
- 后端写 trace 失败只记日志,不影响提交
- 前端 oj/problem/utils/editTrace.ts 是模块单例(扩展对象不能过 Pinia 的响应式代理),
  只数带 userEvent 的事务:格式化回写 / 载入草稿 / 协作对方的改动天然排除;
  closeBrackets 越过右括号时是原样替换,按 no-op 跳过。比赛编辑器同样挂上
- 实跑:后端四种请求、前端浏览器里键入/粘贴/删除/setCode 回写/切后台/提交后清零,
  计数与预期逐项一致

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:46:55 -06:00
xuyueandClaude Sonnet 5 c228b164cf refactor(提交): 教师统计路由拆出 submission-statistics.ts,check:routes 支持嵌套挂载
Deploy / deploy (push) Has been cancelled
- submission.ts 1416 行降到 751 行;统计三条路由拆成子路由,挂在原位置,
  仍排在 /submissions/:id 之前
- check-route-shadowing 原来只认 index.ts 直接挂载,拆分后漏检 3 条(178→175);
  现在按挂载位置展开嵌套的 .route(),恢复 178 条、无遮蔽

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 04:40:30 -06:00
xuyueandClaude Sonnet 5 b5ba56ccd0 refactor(契约): 判题状态码收进契约唯一一份,并收紧 18 处 as any
- 状态码常量与 judgeStatusSchema 移到 packages/contract/src/judge-status.ts,
  后端 judge/status.ts 只再导出;前端 SubmissionStatus 枚举加编译期断言对齐契约
  (实测改坏一个码会当场类型检查失败)
- 类型逃逸 22 处降到 4 处:collab/handler、pagination、configUpdate、
  ExerciseManager、ProblemSubmission、pk.vue tooltip;剩下的是词云插件无类型、
  生成的 .d.ts、skulpt 和 TextEditor

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 04:40:30 -06:00