以 Yjs CRDT 為核心的多人即時共編文件系統,透過 Socket.IO 傳輸增量更新。支援快照式版本歷史、線次評論、剪貼簿圖片上傳,並可一鍵匯出為課程 RAG 知識庫。
Google Docs 之類的協作工具已是大學教學常態,但使用商用工具有兩個痛點:(1) 學生作品與學習歷程散落在外部平台,無法併入研究資料;(2) 無法與課程內的 RAG、Aida、Bloom 分析器整合。
Uedu 共編筆記(UeduNote)內建於平台,讓師生在 Uedu 完成小組討論稿、共同筆記、專題報告草稿。完成後可一鍵匯出到課程 RAG,讓班級的 AI 助教理解這份文件。
Operational Transformation(OT,Google Docs 使用)在處理複雜並發時需要中央伺服器做 transform,實作難度高、邊界 case 多。CRDT(Conflict-free Replicated Data Type)以資料結構保證可交換性,不依賴中心仲裁者,適合快速落地。Yjs 是目前最成熟的 CRDT 實作,帶有 Python 埠 pycrdt,讓後端能直接操作同一份 Y.Doc。
UeduNote 後端使用 pycrdt(Python 版 Yjs),前端使用原版 Yjs(JavaScript)。兩端共用同一份 Y.Doc 二進位表示,意即:
位於 utils/yjs_doc_manager.py。全域單例 yjs_manager 負責:
yjs_state BLOB 欄位載入 Y.Doc 至記憶體get_update_for_client(state_vector) 產生增量更新Namespace:/collab-editor。事件流程遵循 Yjs 標準的三階段交握 + 連續 update:
| 事件 | 方向 | 用途 |
|---|---|---|
join_document | C → S | 加入房間,權限驗證(_collab_socket_room_auth),載入 Y.Doc |
yjs_sync_step1 | C → S | 客戶端送出自己的 state vector |
yjs_sync_step1_response | S → C | 伺服器回傳客戶端缺少的 updates + 伺服器自己的 state vector |
yjs_sync_step2 | C → S | 客戶端送回伺服器缺少的 updates |
yjs_update | C ↔ S | 後續的所有增量編輯,雙向廣播 |
yjs_awareness | C ↔ S | 遠端游標、選取區塊(y-protocols/awareness) |
yjs_save_version | C → S | 手動儲存版本快照 |
yjs_full_reset | S → C | 還原歷史版本後強制客戶端重新同步 |
所有 socket 事件都透過 _collab_socket_room_auth() 快取驗證(一次查 DB、存入 collab_editor_rooms 記憶體結構)。後續事件以純記憶體 lookup 判斷是否允許 edit、comment、view。避免每個事件都打 DB。
核心 schema 定義於 sql/collab_editor.sql 與 sql/collab_editor_yjs.sql:
| 資料表 | 用途 |
|---|---|
collab_documents | 文件主表。UUID、title、content(純文字)、yjs_state(BLOB,CRDT 二進位)、owner、permission、word_count |
collab_document_versions | 版本快照。version_number 自增、完整 content、edit_summary、edited_by |
collab_document_yjs_updates | 增量更新緩衝區(用於未來的 update compaction) |
collab_document_collaborators | 協作者列表。role ∈ {editor, commenter, viewer} |
collab_document_comments | 評論。line_ref(可選的行號錨)、parent_id(threading)、is_resolved |
collab_groups、collab_group_members | 群組化協作(邀請碼機制) |
Y.Doc 的 yjs_state 是二進位,無法被 MySQL 全文索引或直接給 AI 讀取。每次儲存版本或閒置落地時,同步 extract 純文字寫入 content 欄位,供搜尋、匯出、RAG 使用。
採 4 級角色 + 兩個維度:
| 角色 | 動作 |
|---|---|
| admin(擁有者) | 刪除文件、管理協作者、改變 permission 層級 |
| edit | 修改內容、上傳圖片、儲存版本 |
| comment | 新增 / 回覆 / 解決評論 |
| view | 瀏覽內容 |
除了逐人指定的 collab_document_collaborators.collab_role 外,文件有文件層級的 permission 欄位,可設為 editable、commentable、view_only、private。Group members 與社群成員的權限也會並入判斷。
權限計算集中於 _collab_get_doc_perm(doc_uuid, user_id)。結果存入 collab_editor_rooms 記憶體結構,後續 socket 事件純記憶體 lookup,避免 hot path DB 查詢。
yjs_save_version,立即寫入 collab_documents + 新增一筆 collab_document_versions 紀錄yjs_state + content,但不建立版本紀錄POST /api/editor/documents/<doc_uuid>/versions/<version_id>/restore 的行為:
yjs_manager.reset_doc() 取代記憶體版本yjs_full_reset 給所有連線客戶端,強制重新同步早期 UeduNote 僅儲存純文字,無 CRDT。第一次載入舊文件時,會從 content 建立初始 Y.Doc 並寫回 yjs_state,之後以 CRDT 模式運作。此為 lazy migration,無需停機。
評論以行號(line_ref)錨定,而非 CRDT 位置 index。好處是:
代價是:在行內大幅增刪後,評論對應的「那一行」語意可能漂移。實務上適合大學教學場景(評論頻率中等、大段修改後會另開對話)。
反白顏色設定由前端處理,最終寫入 Y.Text 的 attribute,與文字內容一併 CRDT 同步。不在後端額外儲存反白資料。
NAS_UEDU_PATH/collab_editor_images/,測試機為本機 uploads/collab_editor_images/{timestamp}_{uuid}.{ext},避免命名碰撞utils/file_validation.py),防止 polyglot 攻擊paste 事件,將圖片 blob 以 multipart 上傳/api/editor/documents/<doc_uuid>/images/<filename>,驗證 view 權限;secure_filename() 防止 path traversalendpoint:POST /api/editor/documents/<doc_uuid>/export-to-rag。流程:
collab_documents.content 取純文字.md 檔rag_documents 表插入一筆記錄(classroom_id、file_type=md、status=completed)小組完成專題筆記後,教師可匯出到課程 RAG,讓班級的 AI 助教能引用這份共同整理的知識。學生與 AI 的後續對話即建立在「同學們共同建構的理解」之上,實現社會建構主義(Social Constructivism)的數位落實。
目前匯出為單向(文件 → RAG)。未來規劃支援雙向同步:RAG 內容更新時自動通知原筆記擁有者。
共編筆記為 Sociomics 維度(社會互動)提供細粒度資料。可研究的議題:
傳統 Google Docs 的活動紀錄需透過第三方工具(如 Draftback)重建,且內容屬於 Google。UeduNote 的 Y.Doc updates 完整保留於伺服器,每個鍵擊等級的編輯歷史都可重播,是協作學習研究的理想資料來源。
引用本系統時,請標註:「UeduNote: Yjs-based real-time collaborative editor with CRDT replay capability (https://uedu.tw)」。分析編輯歷史時,建議說明所使用的 Y.Doc update 解析工具與時間粒度。