|
|
88b695d34f
|
feat(课堂求助): 教师端接单直接跳题目页协作,删掉 CollabModal
Deploy / deploy (push) Has been cancelled
点求助列表里的一条 = 跳到 /problem/:id,在页面自带的那个编辑器里和学生同步。
原来是弹一个 CollabModal(弹框里再挂一个 CodeMirror + 一份只读题面):
- 按一下 Esc 弹框就关、协作跟着结束(`show` 的 setter 里 leave 是唯一的关闭语义,
Esc 和点遮罩都走它),上课太容易误触;
- 弹框里那份题面不能跑测试、不能提交,老师还是得另开标签页对照。
现在结束协作只有两条明确的路:工具栏的「结束协作」按钮、离开这一页。教师端也少了
一个 CodeMirror 实例 —— setBinaryHandler 那个单例槽位只剩 SyncCodeEditor 一个使用者。
改造中撞出两个真问题,根因是同一个:`/problem/1002` → `/problem/1001` 是同一条路由
换 params,**组件被复用、不卸载**。
- 老师在别的题上接单跳过来,`@ready` 不会再触发,而 room 早在导航前就 open 了 ——
老师停在一个空编辑器上干等。watch 的源从 room 一个变成 [room, roomIsHere] 两个。
- 反过来老师协作中切去别的题,onUnmounted 不触发 → 不发 leave,学生一直挂着
「老师正在帮你」而老师的字一个也过不去。加 `bound` 标记区分「房间从我这儿挪走了」
(补发 leave)和「房间在别处开起来了」(跟我无关,接单那一瞬间就是这种)。
`leave` 分两种语义(协议加 reason):
| 前端 | 求助记录 | 对面看到 |
|---|---|---|
| leave("done") 点「结束协作」 | 删除 | 老师已结束这次帮忙 |
| leave("left") 离开这道题的页面 | 教师走→退回排队;学生走→删除 | 老师暂时离开,已重新排队 |
"left" 复用的是老师掉线那条路(requeueAfterTeacherGone),两件事语义相同。原来两者
都按 done 处理 —— 老师点一下「提交信息」,学生就得重新举手,而他看到的是
「协作已结束」,会以为被处理完了。发起方收到的 reason 单独一份(self_left/done),
不然学生自己切走了却看到「老师已结束这次帮忙」。
其余为这套交互补的闸:
- 学生排队期间离开那道题 → 自动取消求助。那条求助说的是「我卡在这道题」,人走了
就不成立;原来它会一直挂在队列里,老师接进来时学生的编辑器不在这道题上、根本
不会绑,老师对着空编辑器敲字,两边都没提示。
- 协作中**教师的语言选择跟着 room.language 走**(写 codeStore,不只是改高亮),
结束后还原。只改高亮的话老师会拿自己那档语言提交学生的代码(学生写 C、老师选的
是 Python,当场 CE),工具栏还可能显示「提交流程图」。学生端一个字不动 ——
服务端的 room_language 只发给教师,学生本地那份停在建房那一刻。
- 协作中不给「重置代码」:v-model 一写回就顺着 Yjs 同步过去,等于一键清空学生的作业。
- 协作中「提交信息」走新标签 —— 那是教师工具栏上唯一会跳路由的按钮。
- 接单时 resetScreenMode():分屏在「题目」「自测」两档时右侧编辑器根本没挂出来,
停在同一道题上接单会落进一个没有编辑器的页面(跳到别的题时 detail.vue 的 init()
会重置,同页不会)。
- 老师已在一个房间里时前端也拦住接下一单(服务端本来就拦),不拦的话前端已经跳到
新题目上,等于把手上那场协作断掉。
- 文案:协议层那四条英文(Invalid problemId 之类)会被原样弹成 toast,改成中文;
「请先退出当前协作」统一成「结束」;room_closed 的提示按 reason × 角色展开。
验证:本机起 api + web,三个账号两两组合实跑 —— 跨题接单 / 同页接单、双向编辑、
连按 Esc、结束协作、老师跳走后再接、学生跳走、学生排队中切题、学生协作中切语言、
分屏在「题目」档接单、协作中重复接单,以及上面每一句提示的实际文案。
api typecheck / check:routes、前端 type-check / build、fmt 全过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
2026-09-16 19:13:42 -06:00 |
|
|
|
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 |
|
|
|
79bfd28b07
|
chore: 删掉 369 行没有任何调用方的代码
Deploy / deploy (push) Has been cancelled
## utils/functions.ts 砍掉一半(622 → 291 行)
- trickOrTreat():317 行、八种页面恶搞效果(中文乱码、页面翻转、去掉鼠标……),
**全仓零调用**,占这个共享工具文件的一半;
- 文件末尾注释掉的 getChromeVersion / isLowVersion / protocol 六行。
## 其余没有调用方的导出
- services/achievement-metrics.ts 的 RARITIES / OPERATORS —— 和契约的
achievementRaritySchema / achievementOperatorSchema 取值逐字相同,是没人用的第二份;
- collab/state.ts 的 hasRequest;以及 resetCollabState,它的注释写着「仅供进程退出或
测试用」,而本仓库不写测试(项目约定),进程退出那条路径也没调过;
- contract/language.ts 的 JUDGE_LANGUAGES,注释说「前端用它排语言 tab」——前端并没有,
它排 tab 用的是 constants.ts 里以 ProblemLanguage 为键的 SOURCES / LANGUAGE_SHOW_VALUE
(那组映射的完整性 tsc 已经在管);
- web 的 useSimplePagination(usePagination 的一层空壳包装)和 updateProblemSetProgress。
扫描口径:三个包全部 .ts / .vue 里逐个导出符号数出现次数,只在定义处出现的算无引用
(.vue 模板里的引用也计入)。剩下 56 个仍无引用的全部是契约里 `z.infer` 派生的一行
类型(46 个请求类型 + 10 个领域类型)—— 它们是在用的 schema 的类型另一半,成体系的
1:1 镜像,删一部分只会让那个文件变得随意,所以不动。
## 留给你定的一件事
`updateProblemSetProgress` 删掉之后,后端 `PUT /problem-set-progress`(routes/problemset.ts:249)
就没有任何调用方了。题单进度实际是判完之后由 services/problemset.ts 的 recordSolvedProblem
在服务端记账的(schema.ts 里那条注释写明了),这个端点是一条客户端自报进度的平行路径。
没顺手删:删端点是行为变化,而且不能排除有脚本在打它。
tsc、vue-tsc、vite build、单二进制编译、check:routes、check:ast 均通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH
|
2026-09-10 19:38:16 -06:00 |
|
|
|
22a7700b89
|
fix(会话): touchSession 续期时一并续反向索引,否则会话吊销不掉
WebSocket 巡检走的 touchSession 只 EXPIRE `session:<token>`,不碰
`user-sessions:<uid>`。而这条路径存在的理由恰恰是「只开着页面挂 WebSocket、一次
HTTP 请求都不发的人」—— 这种连接碰不到 getUserByToken 里那两条并排的 expire。
于是索引先到期、会话却被巡检一直续着。之后改密码 / 禁用账号走 revokeUserSessions
就 SMEMBERS 不到这张 token:WebSocket 那边还有 publishSessionRevoked 按 userId
兜底能断掉,但 HTTP 一侧拿着那张 cookie 照用不误 —— 而改密码要的恰恰是让 HTTP
立刻失效(学生密码是明文存着给老师查的,改密码是密码泄露后唯一的补救手段)。
签名加一个 userId,两条 EXPIRE 走一次 pipeline,仍然只有一趟往返,原来「比
GET + EXPIRE 少一趟」的理由保住了。三个调用点都有现成的 ws.data.userId。
返回值只看会话那条:反向索引是 498fc1c 才加的,在那之前签发的会话本来就没有索引
键,续不到是正常的,不能因此判定会话已死。
实跑验过四种情况:正常会话两边都续到 7 天;无索引键的存量会话仍判活;会话已删返回
false;空 token 返回 false。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB
|
2026-09-07 07:40:58 -06:00 |
|
|
|
2c4d56b29a
|
refactor(后端): 清掉 Django 遗留的死列与手工级联,角色字符串收成一份
三条迁移,一次部署(0008/0009 含 DROP COLUMN,需要 OJ2_ALLOW_DESTRUCTIVE=1):
- 0008 删 IP 相关:比赛 IP 白名单(前端本来就没有输入框,detail.vue 无条件置空)、
submission.ip(前端从未显示过)、以及一次都没被调用过的 IP 限流桶。
judge_server.ip 是运维数据,保留。
- 0009 删九个只有 Django 时代写过、OJ2 一次都没读过的列:user 的 auth_token /
open_api / open_api_appkey / session_keys,user_profile 的 blog / github /
school / major / language。open_api 后台连开关都没有,那段「已经开着就不重置
appkey」的逻辑从上线起没进过 if。判据是「全仓零读取」而不是「看着没用」——
raw_password 同样刺眼却是在用的,别一起清掉。
- 0010 给 17 条外键补上删除动作,不再是 Django 留下的一律 NO ACTION。父行消失后
必然无意义、且不构成学生留痕的走 CASCADE(中间表、题单/教程/成就的组成部分、
user_profile 与 user_stat);需要人看见的继续拦着——submission.problem_id、
以及 user 的绝大多数外键,删用户撞外键会被 handler 翻译成「请改为禁用账号」,
这是有意的:全 CASCADE 会静默抹掉成就与进度,而 submission.user_id 压根没有
外键,结果是一半删一半留。六处手工级联随之删掉。
角色字符串收进 packages/contract/src/roles.ts:原先 ADMIN_ROLES / TEACHER_ROLES
在两个文件各抄一份、学生口径在四个文件各写一遍、前端 USER_TYPE 是第三份副本。
AuthUser.adminType 与 drizzle 的列都收窄成联合类型,二十多处 `=== "Super Admin"`
从此受编译器管着($type 是纯 TS 层的,generate 确认不产生任何 SQL 变更)。
顺带删掉 db/relations.ts —— drizzle-kit pull 的产物,全仓零引用。
一处行为变化:后台用户列表传非法的 ?type= 回 400,不再静默返回空列表;界面上的
下拉只有合法值,打不到这条。
验证:tsc / vue-tsc / vite build / check:routes 全过;三条迁移在 dev 库执行,
并逐条建 fixture 走 HTTP 接口验过删除连坐与拦截(题单五张子表连坐、user_badge
二级连坐、删有提交的题目仍 409、删有表情的用户仍 409)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeJoYc2t2d7cThVqMBYrBF
|
2026-09-02 22:57:45 -06:00 |
|
|
|
38b81881d5
|
feat(collab): 教师端弹框补上代码提示,语言跟着学生走
Deploy / deploy (push) Has been cancelled
教师端的协作编辑器只有高亮和括号匹配,没装 autocompletion —— 老师替学生
写代码时没有下拉补全,只能盲敲。而且语言写死 cpp(),学生写 Python 时老师
看到的是 C 的高亮。
- 求助协议带上语言:help_request 记到 HelpRequest,accept 时写进 Room,
room_open 发给两端;认不出的语言一律当 C(老客户端不带这个字段,而它
以前就是写死 C 的)
- 新增 help_language / room_language:学生在排队或协作期间切语言,只更新
语言,不动队列位置和房间;已开房就把新语言推给老师,弹框的高亮和补全
实时跟着换
- CollabModal 装上 autocompletion,和学生端用同一套 enhanceCompletion +
completeAnyWord;语言→高亮扩展的映射抽到 shared/extensions/language.ts
两端共用,免得再分叉
- 学生端求助按钮合并状态提示:原来按钮旁边还挂一个 n-tag,一行工具栏在
1280 的机房屏上放不下,改成状态全进 label(已求助 · 待接入 / 已求助 ·
前面 N 人 / xxx 老师帮你中)
扩展数组变了 vue-codemirror 会整体 reconfigure,但 CM6 对已存在的
compartment 取 `compartments.get() || ext.inner`,collabDoc 那个装 yCollab
的 compartment 内容会被沿用,切语言、切主题都不会把协作弄断 —— 实跑验证过
双向同步、补全下拉、协作中切语言三条路径。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015APGLsyaCUa7XFMYAaCho4
|
2026-08-30 10:28:36 -06:00 |
|
|
|
66a564710f
|
fix(collab): 两处会让协作「看起来正常、其实各写各的」的缺陷
都是实测复现出来的,不是推测。
1. 教师第二次接单,看到的是上一个学生的代码拼在自己文档里
CollabModal 的 code ref 跨会话不清。n-modal 默认 display-directive="if",
每次打开都重挂 CodeMirror,拿这个 ref 当初始文档;而 yCollab 只观察 ytext、
从不反过来用 ytext 覆盖编辑器。于是上一轮的代码留在文档里,新学生的内容
作为 delta 插到位置 0,两边偏移从此对不上。
实测三轮会累积成 CCC/BBB/AAA/XXX,且教师敲一行之后,教师看到
「BBB/AAA/XXX」、学生看到「BBB/XXX」——两份不同的文档。
改法:不绑 v-model,文档完全交给 Yjs;并把 start 的起点从「房间打开 +
await nextTick」换成「编辑器 ready」,顺带修掉绑到已 destroy 的旧 view。
2. 学生换连接后房间静默死掉
handleCollabOpen 迁移 socket 时只补发了 help_status,没补 room_open,
而前端每次连接建立都会把 room 清成 null —— 页面显示「老师正在帮你」,
编辑器却早把 yCollab 摘了,老师敲的字一个也到不了。
补发 room_open 也修不好:CRDT 状态跟着旧连接没了,新建 Y.Doc 再 seed
会和老师那份合并成重复文本。所以改成直接拆房、请求退回排队,
老师再点一次 —— 和老师掉线走同一条路子。
顺带修掉验证时挖出来的第三个:握手帧会被丢。
两端挂 yCollab 的时刻不同步(教师等模态框、学生等 chunk),先挂好的那端发的
SyncStep1 到对面时还没有 binaryHandler,服务端转发是成功的、客户端却静静丢掉。
丢的偏偏是握手——y-protocols 里 A 的内容靠 B 发的 Step1 换回来,教师的 Step1
一丢,学生的代码就永远到不了教师那边(单向同步,同样看着像在协作)。
CollabWebSocket 改成 handler 装上之前先把二进制帧缓着,装上再按序放行。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016DHxhKNxXfG89JnVzHbvgj
|
2026-08-30 09:37:41 -06:00 |
|
|
|
6f873be367
|
feat(web): 题目页的「开启同步」换成「求助」
学生发起求助,状态与排队位置显示在按钮旁。
教师端入口不在题目页,所以对教师隐藏这个按钮。
顺带修正 handleHelpCancel:去掉 request.socket !== ws 的校验。
学生取消自己的求助,不管从他哪个标签页发起都合法——
getRequest(ws.data.userId) 已经把范围锁在这一个用户上了,
不是跨用户操作,不需要再比对是不是同一条连接。这层校验此前
会让「第二个标签页点取消」被服务端静默丢弃,前端却已经乐观
地把按钮变回「求助」,是一处 UI 说谎;现在两边状态一致。
handleCollabClose 里排队分支的 socket 归属校验不受影响,
那里的关闭事件是顺带触发的,仍然需要认出是不是本人这条连接。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
|
2026-08-28 06:33:25 -06:00 |
|
|
|
ab8fcc2d42
|
fix(collab): room_closed 无条件归位、断线重连补状态、学生 socket 重连迁移
Critical:room_closed 不再按 reason 挑着重置 helpStatus——学生自己的 socket
发送失败时服务端删了请求却发不出任何纠正帧,store 会卡在陈旧的 active/pending
上出不来。现在一律先归 idle,老师掉线那条路径服务端会紧跟着补一条
help_status:pending,同一条连接消息严格按序到达,不会被这次重置抢跑。
Critical:断线重连没有补状态。前端 CollabWebSocket 加 onConnected 钩子,
每次连接建立(含重连)都清空本地 requests/helpStatus/room,等服务端补发;
后端 handleCollabOpen 对非老师的重连方,如果这个账号名下还有请求,
补发对应的 help_status(pending 带重算的 queueAhead,active 带 teacherName)。
Critical:重连后请求仍绑在旧 socket 上,导致 handleHelpCancel 的归属校验
认不出新连接、后续通知也写进死连接。handleCollabOpen 里把请求和(如果有)
房间迁移到新 socket,并清掉旧 socket 的 roomOwnerId,防止它稍后的 close
反过来拆掉刚迁移出去的房间。
Minor:disconnect() 补齐 queueAhead/teacherName/notice 的重置,不留陈旧值。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
|
2026-08-28 06:01:41 -06:00 |
|
|
|
aef687bcfc
|
fix(api): collab 二进制转发的背压误判与发送失败后的幽灵请求
Important 1:handleCollabBinary 把 send() 返回的 -1 当成失败,实测证明
-1 只是背压——消息已排队,最终照样送达(8MB 帧原样完整到达);只有 0
才是真丢。之前 sent <= 0 一触发背压就拆房间,慢网/大粘贴反而先弄死正常
会话。改成只认 sent === 0;同时空 Uint8Array 的 send() 也回 0(送达和
真丢用同一个返回值分不清),先按长度 0 直接忽略,不转发也不参与失败
判定,否则任意一方发一个空二进制帧就能把房间拆了。
Important 2:发送失败后走 teardownRoom("peer_offline") 从不 removeRequest
(只有 reason === "done" 才删),请求卡在 status: "active" 没有房间,
之后 handleReject / handleHelpCancel / handleLeave / handleAccept 全部
因为状态或归属对不上而拒绝处理,那个学生的后续求助永远被静默吞掉。
补上 offlineSide 参数,和教师断线共用同一条收尾路径:老师那侧消失,
请求退回 pending 并清 teacherId/teacherName,学生收到新的 help_status
pending;学生那侧消失,请求整条清掉。两种情况都发 room_closed 并
broadcastRequests()。
Minor:handleHelpCancel 没有 request.socket === ws 校验,同一账号第二个
标签页能取消第一个标签页排队中的请求——和上一轮修的 close 排队分支是
同一类归属漏洞,补上同样的检查。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
|
2026-08-28 05:28:14 -06:00 |
|
|
|
457df1ef3c
|
fix(api): collab 房间越权与几处时序/静默丢帧缺口
Critical:学生排队断线分支补归属+状态校验,避免同账号双标签页
把 active 请求错删、被另一个学生请求顶替后又被别的老师抢建出
第二间房;handleAccept 补 getRoom(studentId) 兜底同一学生 id
下已有房间的情况。
Important:handleAccept 的库查询是个 await 点,之后补上
teacherSockets().has(ws) 判活,教师在等待期间断线不会再对着
死 socket 建房间。
Minor:handleReject 补上和 handleAccept 一致的库复核,堵住
连接存活期间被降级/禁用的教师继续掐请求的口子;
handleCollabBinary 检查 peer.send() 返回值,转发失败时走既有
teardownRoom 拆房间,不再静默丢帧、悄悄分叉两边的代码。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
|
2026-08-28 02:48:35 -06:00 |
|
|
|
b14639d890
|
feat(api): collab 房间管理与 Yjs 帧转发
accept 时按库里的 adminType 复核身份,房间以学生为键。
服务端只按房间转发二进制帧,不解析内容。
老师掉线请求退回排队,学生掉线请求随人清除。
顺带处理三项 Task 2 评审遗留:
- TEACHER_ROLES 去重,handler.ts 改为从 routes/helpers.ts 导入,不再自留一份
- 补全学生排队中断线(尚未进房间)的清理分支,避免陈旧请求卡死在列表里
- 修正 websocket.ts 里一处过期注释:username/adminType 是三种 kind 都会填,
不是只有 collab 才填
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
|
2026-08-28 02:36:12 -06:00 |
|
|
|
1db2c49b87
|
feat(api): 新增 /ws/collab 通道与课堂求助控制面
学生发起/撤销求助,在线教师收到全量列表。求助只在内存,不落库。
二进制帧的限流单独一档,避免协作输入把连接踢掉。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
|
2026-08-28 02:24:44 -06:00 |
|