feat(collab): 教师端弹框补上代码提示,语言跟着学生走
Some checks failed
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
This commit is contained in:
2026-08-30 10:28:36 -06:00
parent 2f9b6dbf03
commit 38b81881d5
8 changed files with 181 additions and 38 deletions

View File

@@ -11,6 +11,7 @@ import {
getRoom,
hasTeacherOnline,
listRequests,
normalizeLanguage,
openRoom,
queueAheadOf,
removeRequest,
@@ -179,7 +180,12 @@ export function handleCollabClose(ws: CollabSocket) {
}
export async function handleCollabMessage(ws: CollabSocket, raw: string) {
let message: { type?: unknown; problemId?: unknown; studentId?: unknown }
let message: {
type?: unknown
problemId?: unknown
studentId?: unknown
language?: unknown
}
try {
message = JSON.parse(raw) as typeof message
} catch {
@@ -201,7 +207,10 @@ export async function handleCollabMessage(ws: CollabSocket, raw: string) {
switch (message.type) {
case "help_request":
await handleHelpRequest(ws, message.problemId)
await handleHelpRequest(ws, message.problemId, message.language)
return
case "help_language":
handleHelpLanguage(ws, message.language)
return
case "help_cancel":
handleHelpCancel(ws)
@@ -220,7 +229,11 @@ export async function handleCollabMessage(ws: CollabSocket, raw: string) {
}
}
async function handleHelpRequest(ws: CollabSocket, problemId: unknown) {
async function handleHelpRequest(
ws: CollabSocket,
problemId: unknown,
language: unknown,
) {
if (typeof problemId !== "string" || !problemId) {
ws.send(JSON.stringify({ type: "error", message: "Invalid problemId" }))
return
@@ -266,6 +279,7 @@ async function handleHelpRequest(ws: CollabSocket, problemId: unknown) {
className: student?.className ?? null,
problemId,
problemTitle: problem.title,
language: normalizeLanguage(language),
createdAt: Date.now(),
status: "pending",
socket: ws,
@@ -274,6 +288,29 @@ async function handleHelpRequest(ws: CollabSocket, problemId: unknown) {
broadcastRequests()
}
/**
* 学生在求助期间换了语言。
*
* 只更新语言,不动队列位置、不动房间 —— 换语言不该让他重新排队。已经在协作中的
* 话再推一条 room_language老师端的高亮和补全立刻跟着换不推的话老师会拿着
* 建房那一刻的语言,对着一套错的补全替学生写代码。
*/
function handleHelpLanguage(ws: CollabSocket, language: unknown) {
const request = getRequest(ws.data.userId)
// 比对 socket 归属:同账号的另一个标签页停在别的题上切语言,不该改这条求助
if (!request || request.socket !== ws) return
const next = normalizeLanguage(language)
if (request.language === next) return
request.language = next
const room = getRoom(ws.data.userId)
if (!room) return
room.language = next
room.teacherSocket.send(
JSON.stringify({ type: "room_language", language: next }),
)
}
function handleHelpCancel(ws: CollabSocket) {
const request = getRequest(ws.data.userId)
// 不比对 socket 归属:取消的是这个学生自己的求助,不管从他哪个标签页发起都
@@ -341,6 +378,7 @@ async function handleAccept(ws: CollabSocket, studentId: unknown) {
studentSocket: request.socket,
teacherSocket: ws,
problemId: request.problemId,
language: request.language,
})
const openFrame = (peerName: string, peerRole: "student" | "teacher") =>
@@ -348,6 +386,7 @@ async function handleAccept(ws: CollabSocket, studentId: unknown) {
type: "room_open",
peer: { name: peerName, role: peerRole },
problemId: request.problemId,
language: request.language,
})
request.socket.send(openFrame(request.teacherName, "teacher"))
ws.send(openFrame(request.studentName, "student"))

View File

@@ -8,6 +8,30 @@
export type CollabSocket = Bun.ServerWebSocket<import("../websocket").SubmissionSocketData>
/**
* 协作支持的语言。和前端 utils/types.ts 里的 LANGUAGE 对齐,去掉 Flowchart ——
* 流程图题没有代码编辑器,求助入口本身就是隐藏的。
*/
export const COLLAB_LANGUAGES = [
"C",
"C++",
"Python2",
"Python3",
"Java",
"JavaScript",
"Golang",
"SQL",
] as const
export type CollabLanguage = (typeof COLLAB_LANGUAGES)[number]
/** 认不出来的一律当 C老客户端不带这个字段而它以前就是写死 C 的 */
export function normalizeLanguage(value: unknown): CollabLanguage {
return (COLLAB_LANGUAGES as readonly string[]).includes(value as string)
? (value as CollabLanguage)
: "C"
}
export interface HelpRequest {
studentId: number
studentName: string
@@ -15,6 +39,11 @@ export interface HelpRequest {
/** 题目的展示号problem._id 列,前端一路用的都是它),不是自增主键 */
problemId: string
problemTitle: string
/**
* 学生编辑器当前的语言。决定教师端弹框用哪套高亮和补全 —— 求助时带上,
* 协作期间学生切语言会用 help_language 更新这里
*/
language: CollabLanguage
createdAt: number
status: "pending" | "active"
teacherId?: number
@@ -83,6 +112,8 @@ export interface Room {
studentSocket: CollabSocket
teacherSocket: CollabSocket
problemId: string
/** 建房那一刻学生的语言,之后跟着 help_language 走 */
language: CollabLanguage
}
const rooms = new Map<number, Room>()