fix(collab): 动态 import 竞态、切走页面的语义、演示模式的身份错位

- collabDoc.start 要 await 六个动态 import,房间可能在这期间就关了(机房首次
  加载 y* 那几个 chunk 正是最慢的时候)。stop() 先跑完面对的是 doc === null,
  什么也拆不到;import 回来之后 start 的后半段照样建文档、装 binaryHandler、
  挂 yCollab、发 SyncStep1——给一个不存在的房间。加会话代号,过期就整个放弃。

- SyncCodeEditor 卸载(切走题目页、把语言切成流程图)原来只是悄悄 stop(),
  房间和 active 都还留着,老师那边模态框照开、字照敲,一个也到不了;watch
  没有 immediate,学生切回来也不会重建。改成明确 leave() 结束协作。
  另外 @ready 也作为起点之一:学生排队时切走、老师这期间接了单,切回来能接上。

- 演示模式下 isTeacherOrAbove 被强制 false,服务端却按库里的 adminType 照样
  把他算作在线老师:学生因此拿到 pending 而不是 no_teacher,排队等一个顶栏里
  根本没有求助列表的人;他自己看到的求助按钮点下去,服务端回「教师不能发起求助」。
  两头都不对,索性对演示模式整个关掉这个功能。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016DHxhKNxXfG89JnVzHbvgj
This commit is contained in:
2026-08-30 09:38:04 -06:00
parent 66a564710f
commit 4abaf9c7e4
4 changed files with 48 additions and 22 deletions

View File

@@ -32,8 +32,21 @@ export function useCollabDoc() {
let view: EditorView | null = null
let detachDocUpdate: (() => void) | null = null
let detachAwarenessUpdate: (() => void) | null = null
/**
* 会话代号。每次 start / stop 都推进一格。
*
* start 要 await 六个动态 import房间可能在这期间就关了机房首次加载
* y* 那几个 chunk 正是最慢的时候。stop() 先跑完的话它面对的是 doc === null
* 什么也拆不到;等 import 回来 start 的后半段照样建文档、装 binaryHandler、
* 往编辑器里挂 yCollab、还发一轮 SyncStep1 —— 给一个已经不存在的房间。
*/
let generation = 0
async function start({ editorView, seedContent }: StartOptions) {
// 已经有一份在跑就先收掉。重复 start 只可能来自时序竞争,叠加没有意义
if (doc) stop()
const myGeneration = ++generation
const [Y, awarenessProtocol, syncProtocol, encoding, decoding, { yCollab }] =
await Promise.all([
import("yjs"),
@@ -44,6 +57,9 @@ export function useCollabDoc() {
import("y-codemirror.next"),
])
// 等 chunk 的这段时间里房间关了(或者又开了新的一轮),整个放弃
if (myGeneration !== generation) return
view = editorView
doc = new Y.Doc()
const ytext = doc.getText("codemirror")
@@ -131,6 +147,7 @@ export function useCollabDoc() {
}
function stop() {
generation += 1
collabStore.setBinaryHandler(null)
detachDocUpdate?.()
detachAwarenessUpdate?.()