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
This commit is contained in:
2026-08-28 06:33:25 -06:00
parent ab8fcc2d42
commit 6f873be367
4 changed files with 46 additions and 165 deletions

View File

@@ -69,8 +69,8 @@ export function handleCollabOpen(ws: CollabSocket) {
// 学生(重)连:如果这个账号名下已经有一条请求(掉线重连回来,或者干脆是
// 同账号第二个标签页),把它迁移到这条新连接上,并把当前状态补发回去 ——
// 前端 onConnected 时会先把本地状态清空等着这条补发,不发的话就永远卡在
// idle不迁移 socket 归属的话,旧连接一断handleHelpCancel 的
// `request.socket === ws` 校验就会认不出这条新连接发起的取消
// idle不迁移 socket 归属的话,sendHelpStatus/accept 等后续推送会发到一条
// 已经不用的旧连接上,新连接(新标签页)什么都收不到
const request = getRequest(ws.data.userId)
if (!request) return
request.socket = ws
@@ -234,9 +234,11 @@ async function handleHelpRequest(ws: CollabSocket, problemId: unknown) {
function handleHelpCancel(ws: CollabSocket) {
const request = getRequest(ws.data.userId)
// 同一账号可能开着两个标签页;只能取消自己这条连接发起的请求,不然 B 标签页
// 能把 A 标签页排队中的求助顶掉 —— 和 handleCollabClose 排队分支同一类归属漏洞
if (!request || request.socket !== ws || request.status === "active") return
// 不比对 socket 归属:取消的是这个学生自己的求助,不管从他哪个标签页发起都
// 合法——getRequest(ws.data.userId) 已经把范围锁在这一个用户上了,不是跨用户
// 操作。这里和 handleCollabClose 的排队分支不是同一类问题:那边关闭事件是
// 「顺带」触发的,必须认出是不是本人这条连接;这里是用户主动点了取消
if (!request || request.status === "active") return
removeRequest(ws.data.userId)
broadcastRequests()
}