|
|
@@ -443,6 +443,8 @@ import AIWorkModal from './components/AIWorkModal.vue'
|
|
|
import DialoguePanel from './components/DialoguePanel.vue'
|
|
|
import ChoiceStatistics from './components/ChoiceStatistics.vue'
|
|
|
import * as Y from 'yjs'
|
|
|
+import { YjsRedisProvider } from './CustomWebsocketProvider'
|
|
|
+import {parseYjsMessage} from './parseYjsMessage'
|
|
|
import { WebsocketProvider } from 'y-websocket'
|
|
|
import { Refresh } from '@icon-park/vue-next'
|
|
|
import answerTheResult from './components/answerTheResult.vue'
|
|
|
@@ -700,7 +702,8 @@ const yTimerMessages = ref<any | null>(null)
|
|
|
const yLaserMessages = ref<any | null>(null)
|
|
|
const yWritingBoardMessages = ref<any | null>(null)
|
|
|
const yIsResultArrayMessages = ref<any | null>(null)
|
|
|
-const providerSocket = ref<WebsocketProvider | null>(null)
|
|
|
+// const providerSocket = ref<WebsocketProvider | null>(null)
|
|
|
+const providerSocket = ref<YjsRedisProvider | null>(null)
|
|
|
// 学生端画图同步数据
|
|
|
const writingBoardSyncDataURL = ref<string | null>(null)
|
|
|
const writingBoardSyncBlackboard = ref<boolean | null>(null)
|
|
|
@@ -2846,6 +2849,10 @@ const getCourseDetail = async () => {
|
|
|
console.log('操作记录:', opRes1[0])
|
|
|
console.log('操作记录:', opRes.value)
|
|
|
console.log(res, opRes.value)
|
|
|
+ // 初始化WebSocket连接
|
|
|
+ if (api.yweb_socket) {
|
|
|
+ createWebSocketConnection()
|
|
|
+ }
|
|
|
const courseData = res[0][0]
|
|
|
courseDetail.value = courseData
|
|
|
selectWorksStudent()
|
|
|
@@ -3111,8 +3118,13 @@ const checkWorkArrayChanged = (oldArray: WorkItem[], newArray: WorkItem[]): bool
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
+const isSelectCourseSLook = ref(false)
|
|
|
// 查询课程跟随状态
|
|
|
const selectCourseSLook = async (type = 2) => {
|
|
|
+ if (isSelectCourseSLook.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ isSelectCourseSLook.value = true
|
|
|
const res = await api.selectCourseSLook(props.courseid as string)
|
|
|
console.log('selectCourseSLook', res)
|
|
|
if (res[0][0].follow == 2) {
|
|
|
@@ -3138,7 +3150,7 @@ const selectCourseSLook = async (type = 2) => {
|
|
|
if (props.type == '2') {
|
|
|
message.success(isFollowModeActive.value ? lang.ssFollowOnTip : lang.ssFreeOnTip)
|
|
|
}
|
|
|
-
|
|
|
+ isSelectCourseSLook.value = false
|
|
|
checkParentMode()
|
|
|
}
|
|
|
|
|
|
@@ -3258,7 +3270,7 @@ const messageInit = () => {
|
|
|
console.log('🔗 messageInit: doc.guid=', doc.guid, 'boundDoc=', boundDoc?.guid, 'same=', boundDoc === doc)
|
|
|
if (boundDoc === doc) {
|
|
|
// 同 doc 重连:补发上线消息即可
|
|
|
- if (props.type === '2') {
|
|
|
+ if (props.type === '2' && opRes.value.length === 0) {
|
|
|
sendMessage({ type: 'class_begin', courseid: props.courseid })
|
|
|
}
|
|
|
return
|
|
|
@@ -3283,7 +3295,6 @@ const messageInit = () => {
|
|
|
yMessage.value.observe((e: any) => {
|
|
|
// 同时兼容 keysChanged 和 changes.keys(不同 Yjs 版本/事件结构)
|
|
|
const changed = e.keysChanged || e.changes?.keys
|
|
|
- // ★ 打印每个 key 的 action(add/update/delete)和 oldValue
|
|
|
const keysMap = e.keys || e.changes?.keys
|
|
|
if (keysMap && keysMap.get) {
|
|
|
for (const [key, info] of keysMap.entries()) {
|
|
|
@@ -3291,21 +3302,37 @@ const messageInit = () => {
|
|
|
}
|
|
|
}
|
|
|
console.log('🔗 yMessage observe 触发, changed keys:', changed ? Array.from(changed) : 'null')
|
|
|
- if (!changed || !changed.has('latest')) return
|
|
|
- const message = yMessage.value!.get('latest')
|
|
|
- console.log('🔗 yMessage latest:', message, '当前 mId:', mId.value)
|
|
|
- if (message && message.mId !== mId.value) {
|
|
|
- getMessages(message)
|
|
|
+ if (!changed) return
|
|
|
+ // slideIndex 独立通道
|
|
|
+ if (changed.has('slideIndex')) {
|
|
|
+ const slideMsg = yMessage.value!.get('slideIndex')
|
|
|
+ console.log('🔗 yMessage slideIndex:', slideMsg, '当前 mId:', mId.value)
|
|
|
+ if (slideMsg && slideMsg.mId !== mId.value) {
|
|
|
+ getMessages(slideMsg)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 其他普通消息
|
|
|
+ if (changed.has('latest')) {
|
|
|
+ const message = yMessage.value!.get('latest')
|
|
|
+ console.log('🔗 yMessage latest:', message, '当前 mId:', mId.value)
|
|
|
+ if (message && message.mId !== mId.value) {
|
|
|
+ getMessages(message)
|
|
|
+ }
|
|
|
}
|
|
|
})
|
|
|
|
|
|
- // ★ 追平:连接建立后立即读取当前 latest,
|
|
|
+ // ★ 追平:连接建立后立即读取当前 latest 和 slideIndex,
|
|
|
// 防止老师在学生 observe 注册之前发的消息丢失(尤其是当前所在幻灯片)
|
|
|
const latest = yMessage.value.get('latest')
|
|
|
if (latest && latest.mId !== mId.value) {
|
|
|
console.log('🔗 追平当前 latest 消息:', latest)
|
|
|
getMessages(latest)
|
|
|
}
|
|
|
+ const slideIdx = yMessage.value.get('slideIndex')
|
|
|
+ if (slideIdx && slideIdx.mId !== mId.value) {
|
|
|
+ console.log('🔗 追平当前 slideIndex 消息:', slideIdx)
|
|
|
+ getMessages(slideIdx)
|
|
|
+ }
|
|
|
|
|
|
// 计时器消息数组
|
|
|
yTimerMessages.value = doc.getArray('timerMessages')
|
|
|
@@ -3397,8 +3424,11 @@ const messageInit = () => {
|
|
|
if (isFirstEnter.value && isCreator.value && docSocket.value) {
|
|
|
console.log('🧹 首次进入且为创建者,清空所有同步状态')
|
|
|
docSocket.value.transact(() => {
|
|
|
- // ★ 不删除 latest!delete 会传播给学生导致 get('latest') 返回 undefined
|
|
|
- // 旧 latest 会被 sendMessage 的 set('latest', message) 自然覆盖
|
|
|
+ // 清空 latest 消息(创建者进页面时清掉旧消息)
|
|
|
+ if (yMessage.value) {
|
|
|
+ yMessage.value.set('latest', null)
|
|
|
+ yMessage.value.set('slideIndex', null)
|
|
|
+ }
|
|
|
// 清空计时器消息数组
|
|
|
const timerMessagesArray = docSocket.value?.getArray?.('timerMessages')
|
|
|
if (timerMessagesArray) {
|
|
|
@@ -3481,7 +3511,7 @@ const messageInit = () => {
|
|
|
})
|
|
|
|
|
|
// 新 doc 绑定完成,补发上线消息
|
|
|
- if (props.type === '2') {
|
|
|
+ if (props.type === '2' && opRes.value.length === 0) {
|
|
|
sendMessage({ type: 'class_begin', courseid: props.courseid })
|
|
|
}
|
|
|
}
|
|
|
@@ -3635,8 +3665,13 @@ const sendMessage = (obj: any) => {
|
|
|
else {
|
|
|
// 普通消息:只保留最后一条
|
|
|
if (yMessage.value) {
|
|
|
- // 用 Map.set 代替 Array.delete+push,只产生 1 条 update
|
|
|
- yMessage.value.set('latest', message)
|
|
|
+ if (messageType === 'slideIndex') {
|
|
|
+ // slideIndex 单独通道,防止被其他消息挤掉
|
|
|
+ yMessage.value.set('slideIndex', message)
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ yMessage.value.set('latest', message)
|
|
|
+ }
|
|
|
}
|
|
|
else {
|
|
|
console.warn('⚠️ sendMessage: yMessage.value 为 null,消息未写入! type:', messageType, 'docSocket:', !!docSocket.value)
|
|
|
@@ -3653,7 +3688,7 @@ const getMessages = (msgObj: any) => {
|
|
|
console.log('message', msgObj)
|
|
|
|
|
|
// 处理幻灯片切换消息
|
|
|
- if (props.type == '2' && msgObj.type === 'slideIndex') {
|
|
|
+ if (props.type == '2' && msgObj.type === 'slideIndex' && isFollowModeActive.value) {
|
|
|
goToSlide(msgObj.slideIndex)
|
|
|
}
|
|
|
|
|
|
@@ -4101,6 +4136,7 @@ onMounted(() => {
|
|
|
'9. doc 里所有顶层 key': Array.from(doc.share.entries()).map(([k, v]: [string, any]) => k + ':' + v.constructor.name),
|
|
|
'10. provider synced': providerSocket.value?.synced,
|
|
|
'11. ws.readyState': providerSocket.value?.ws?.readyState,
|
|
|
+ '12. messageV3?.get("slideIndex")': msgV3Type?.get?.('slideIndex'),
|
|
|
}
|
|
|
console.log('════════ Yjs 诊断 ════════')
|
|
|
for (const [k, v] of Object.entries(report)) {
|
|
|
@@ -4113,10 +4149,7 @@ onMounted(() => {
|
|
|
console.log('PPTist Student View 已加载,可通过 window.PPTistStudent 访问功能')
|
|
|
console.log('URL参数:', { courseid: props.courseid, type: props.type })
|
|
|
|
|
|
- // 初始化WebSocket连接
|
|
|
- if (api.yweb_socket) {
|
|
|
- createWebSocketConnection()
|
|
|
- }
|
|
|
+
|
|
|
|
|
|
// 创建人离开页面时,广播停止计时
|
|
|
|
|
|
@@ -4275,7 +4308,8 @@ const updateAuthToken = async () => {
|
|
|
authToken.value = newToken
|
|
|
// 更新 provider 的 auth 参数
|
|
|
if (providerSocket.value.params) {
|
|
|
- providerSocket.value.params.yauth = newToken
|
|
|
+ // providerSocket.value.params.yauth = newToken
|
|
|
+ providerSocket.value.updateParams({ ...providerSocket.value.params, yauth: newToken })
|
|
|
}
|
|
|
console.log('🔐 认证 token 已更新')
|
|
|
|
|
|
@@ -4367,107 +4401,42 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
|
|
|
docSocket.value = new Y.Doc()
|
|
|
docSocket.value.gc = true
|
|
|
- providerSocket.value = new WebsocketProvider(
|
|
|
+ providerSocket.value = new YjsRedisProvider(
|
|
|
api.yweb_socket,
|
|
|
'PPT' + props.courseid,
|
|
|
docSocket.value,
|
|
|
{
|
|
|
params: { yauth: authToken.value },
|
|
|
- resyncInterval: 5000, // 每 5 秒用标准协议请求服务器状态(代替畸形心跳)
|
|
|
- maxBackoffTime: 5000, // 重连最长退避 5 秒
|
|
|
+ // resyncInterval: 5000, // 每 5 秒用标准协议请求服务器状态(代替畸形心跳)
|
|
|
+ // maxBackoffTime: 5000, // 重连最长退避 5 秒
|
|
|
}
|
|
|
)
|
|
|
providerSocket.value.awareness.setLocalState(null)
|
|
|
providerSocket.value.awareness.destroy() // 完全禁用 awareness
|
|
|
|
|
|
- // ★ 三层日志之一:raw websocket 消息(确认数据到达浏览器)
|
|
|
- const _ws = (providerSocket.value as any).ws
|
|
|
- if (_ws && !_ws.__logged) {
|
|
|
- _ws.__logged = true // 防止重复绑定
|
|
|
- // 包一层 onmessage,抓 provider 处理异常
|
|
|
- const _origOnMsg = _ws.onmessage
|
|
|
- if (typeof _origOnMsg === 'function') {
|
|
|
- _ws.onmessage = function(ev: MessageEvent) {
|
|
|
- try {
|
|
|
- _origOnMsg.call(_ws, ev)
|
|
|
- }
|
|
|
- catch (e: any) {
|
|
|
- console.error('❌ provider onmessage 异常:', e?.message || e, e?.stack || '')
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- _ws.addEventListener('message', (ev: MessageEvent) => {
|
|
|
- try {
|
|
|
- const data = new Uint8Array(ev.data)
|
|
|
- const msgType = data[0]
|
|
|
- const typeNames: any = { 0: 'sync', 1: 'awareness', 2: 'queryAwareness' }
|
|
|
- let detail = ''
|
|
|
- if (msgType === 0) {
|
|
|
- const sub = data[1]
|
|
|
- detail = ({ 0: 'step1(请求缺失)', 1: 'step2(服务器推送)', 2: 'update' } as any)[sub] || 'unknown'
|
|
|
- }
|
|
|
- console.log(`📥 ws raw: ${typeNames[msgType] || msgType} ${detail} (${data.length} bytes)`)
|
|
|
-
|
|
|
- // ★ 诊断:sync update 到达时手动解码 + applyUpdate,看是不是重复数据
|
|
|
- if (msgType === 0 && data[1] === 2) {
|
|
|
- const doc = docSocket.value
|
|
|
- if (doc) {
|
|
|
- // 解码 varint 提取 update bytes
|
|
|
- let pos = 2 // 跳过 msgType + subType
|
|
|
- let updateLen = 0
|
|
|
- let shift = 0
|
|
|
- while (pos < data.length) {
|
|
|
- const b = data[pos++]
|
|
|
- updateLen |= (b & 0x7f) << shift
|
|
|
- if (b < 0x80) break
|
|
|
- shift += 7
|
|
|
- }
|
|
|
- const updateBytes = data.slice(pos, pos + updateLen)
|
|
|
-
|
|
|
- // ★ 用 Y.decodeUpdate 解码 update(不创建 temp doc,不触发 applyUpdate 的 DELETE)
|
|
|
- try {
|
|
|
- const { structs } = Y.decodeUpdate(updateBytes)
|
|
|
- console.log(`📦 解码: updateBytes=${updateBytes.length}, structs=${structs.length}`)
|
|
|
- for (const struct of structs) {
|
|
|
- const arr = struct?.content?.arr
|
|
|
- console.log(`📦 struct: type=${struct?.content?.constructor?.name}, parentSub=${struct?.parentSub}, arr=${arr ? JSON.stringify(arr)?.slice(0, 150) : '无'}`)
|
|
|
- if (arr && Array.isArray(arr)) {
|
|
|
- for (const val of arr) {
|
|
|
- if (val && typeof val === 'object' && val.type && val.mId) {
|
|
|
- console.log(`📦 ✅ 从 update 提取消息: type=${val.type}, mId=${val.mId}, 当前 mId=${mId.value}`)
|
|
|
- if (val.mId !== mId.value) {
|
|
|
- getMessages(val)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- catch (e) {
|
|
|
- console.log('📦 update 解码失败:', e)
|
|
|
+ // ★ 三层日志之二:Y.Doc update 事件 — 列出所有根类型的 size,看更新到底改了哪个
|
|
|
+ docSocket.value.on('update', (update: Uint8Array, origin: any) => {
|
|
|
+ // origin === providerSocket.value 表示来自网络
|
|
|
+ // origin !== providerSocket.value 表示本地修改
|
|
|
+ if (origin !== providerSocket.value) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ const { structs } = Y.decodeUpdate(update)
|
|
|
+ for (const struct of structs) {
|
|
|
+ const arr = (struct as any)?.content?.arr
|
|
|
+ if (arr && Array.isArray(arr)) {
|
|
|
+ for (const val of arr) {
|
|
|
+ if (val && typeof val === 'object' && val.type && val.mId) {
|
|
|
+ console.log(`📦 提取: type=${val.type}, mId=${val.mId}`)
|
|
|
+ if (val.mId !== mId.value) getMessages(val)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- catch (e) {
|
|
|
- console.log('📥 ws raw: parse error', e)
|
|
|
- }
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- // ★ 三层日志之二:Y.Doc update 事件 — 列出所有根类型的 size,看更新到底改了哪个
|
|
|
- docSocket.value.on('update', (update: Uint8Array, origin: any) => {
|
|
|
- const originName = origin?.constructor?.name || (origin === providerSocket.value ? 'ws-provider' : typeof origin)
|
|
|
- const share = (docSocket.value as any)?.share
|
|
|
- const sizes: string[] = []
|
|
|
- if (share) {
|
|
|
- for (const [name, type] of share.entries()) {
|
|
|
- const sz = type.size ?? type.length ?? '?'
|
|
|
- const latest = type.get?.('latest')
|
|
|
- sizes.push(`${name}=${sz}${latest ? '(' + latest.type + ')' : ''}`)
|
|
|
- }
|
|
|
}
|
|
|
- console.log(`📥 Y.Doc update: ${update.length} bytes, origin: ${originName}, types: [${sizes.join(', ')}]`)
|
|
|
+ catch (e) {
|
|
|
+ console.log('📦 解析失败:', e)
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
// 清理可能残留的旧心跳定时器
|
|
|
@@ -4506,11 +4475,11 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
if (docSocket.value) {
|
|
|
initializeYjsData()
|
|
|
}
|
|
|
- if (type == 1) {
|
|
|
- setTimeout(() => {
|
|
|
- createWebSocketConnection(2)
|
|
|
- }, 1000)
|
|
|
- }
|
|
|
+ // if (type == 1) {
|
|
|
+ // setTimeout(() => {
|
|
|
+ // createWebSocketConnection(2)
|
|
|
+ // }, 1000)
|
|
|
+ // }
|
|
|
}
|
|
|
else if (event.status === 'disconnected') {
|
|
|
console.log('👉 连接断开,等待自动重连...')
|
|
|
@@ -4540,6 +4509,23 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
providerSocket.value.on('connection-close', (event: any) => {
|
|
|
console.log('👉 WebSocket 连接关闭(将自动重连):', event)
|
|
|
})
|
|
|
+
|
|
|
+ // 每条消息到达时触发(用于调试批量消息问题)
|
|
|
+ // ★ 从原始消息里直接解析出用户数据
|
|
|
+ providerSocket.value.on('raw-message', (data: Uint8Array) => {
|
|
|
+ const obj = parseYjsMessage(data)
|
|
|
+ if (!obj) return
|
|
|
+ // 过滤掉不带业务字段的(比如只含 slideIndex 也可能有效,看你的需要)
|
|
|
+ if (!obj.type && obj.slideIndex === undefined) return
|
|
|
+ // mId 过滤,跳过自己发的
|
|
|
+ if (obj.mId && obj.mId === mId.value) return
|
|
|
+
|
|
|
+ console.log('📨 解析业务消息:', obj)
|
|
|
+ // obj = { slideIndex: 1, courseid: 'dd004650-...', type: 'slideIndex', timestamp: '...', mId: '...' }
|
|
|
+
|
|
|
+ // ★ 如果需要走原有的 getMessages 逻辑(推荐保留原有 observe,两路互不干扰):
|
|
|
+ getMessages(obj)
|
|
|
+ })
|
|
|
|
|
|
// ★ synced 事件:在服务器状态同步完成(sync step 2 已应用到 Y.Doc)后触发
|
|
|
// 'connected' 在 onopen 时就触发,此时 sync 尚未完成,messageInit 的 catch-up 读不到数据
|
|
|
@@ -4565,7 +4551,7 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // catch-up:读取服务器同步过来的 latest 消息
|
|
|
+ // catch-up:读取服务器同步过来的 latest 和 slideIndex 消息
|
|
|
if (yMessage.value) {
|
|
|
const latest = yMessage.value.get('latest')
|
|
|
console.log('🔗 synced catch-up latest:', latest)
|
|
|
@@ -4573,6 +4559,12 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
console.log('🔗 synced catch-up: 处理 latest 消息', latest.type)
|
|
|
getMessages(latest)
|
|
|
}
|
|
|
+ const slideIdx = yMessage.value.get('slideIndex')
|
|
|
+ console.log('🔗 synced catch-up slideIndex:', slideIdx)
|
|
|
+ if (slideIdx && slideIdx.mId !== mId.value) {
|
|
|
+ console.log('🔗 synced catch-up: 处理 slideIndex 消息', slideIdx.slideIndex)
|
|
|
+ getMessages(slideIdx)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// catch-up:读取计时器、激光笔、画图状态
|
|
|
@@ -4588,7 +4580,7 @@ const createWebSocketConnection = async (type = 1) => {
|
|
|
}
|
|
|
|
|
|
// 学生端补发上线消息,让老师知道学生已就绪
|
|
|
- if (props.type === '2') {
|
|
|
+ if (props.type === '2' && opRes.value.length === 0) {
|
|
|
console.log('🔗 sync handler: yMessage.value =', yMessage.value ? '已绑定' : 'null', 'doc.guid=', docSocket.value?.guid)
|
|
|
sendMessage({ type: 'class_begin', courseid: props.courseid })
|
|
|
// ★ 确认 class_begin 是否真的写入了 messageV3
|