為什么你的RAGFlow需要一個Markdown預覽器(油猴腳本方案)
前幾天知識星球中有個提問,關于 RAGFlow 的聊天助手回答引用文件是 markdown 格式時,如何跳轉進行預覽的實現問題。
目前在 RAGFlow 的聊天助手中,如果引用文件格式是 pdf 和 docx,是可以直接點擊跳轉打開新的標簽頁進行預覽的,不過暫時確實不支持 markdown 格式。早在今年 2 月份,在 RAGFlow 的 Github issue 上,就有一個關于“回答中引用的 MD 文檔無法原生預覽”的討論(#4979)。但是目前并沒有官方或者其他開發者給出解決方案。不過既然是前端顯示問題,那就沒什么是一個油猴腳本解決不了的。
這篇試圖說清楚:
如何通過利用油猴腳本的腳本注入和跨域請求能力,攔截用戶對 .md 鏈接的點擊,通過調用 RAGFlow 的內部 API 獲取原始數據,并動態生成一個預覽頁面;以及在實現上述過程中踩到的坑與迭代方向參考。
以下,enjoy:
1、需求場景分析
在追求高精度問答的實際落地場景中,對于企業知識庫中充斥的格式各異、布局復雜的文檔,比如跨越多欄的 PDF 報告、包含復雜嵌套表格的 Word 文檔、或是圖文混排的產品手冊。直接把這些原始文檔投喂給任何 RAG 系統,都可能面臨“Garbage In, Garbage Out”的問題。
而 Ragflow 自帶的 DeepDoc 或者使用 MinerU 解析器雖然強大,但面對這些“臟數據”的時候,也難免會產生語義不連貫、上下文割裂的文本塊,這會直接影響檢索的準確性和最終生成答案的質量。
1.1預處理的主要做法
為了確保送入 RAG 管道的數據是高質量的,文檔預處理幾乎是所有嚴肅 RAG 應用中不可或缺的一步。這一步不只是簡單的格式轉換,更是一個結合文檔特點進行結構化重塑過程。核心目標包括:
- 消除噪音:剔除頁眉、頁腳、頁碼等無關信息。
- 保留結構:正確識別標題層級、列表、表格和代碼塊,將視覺結構轉化為語義結構。
- 確保連貫性:將被物理分頁或分欄打斷的完整段落重新連接起來。
通過定制化的腳本(如 Python 腳本結合 PyMuPDF, python-docx 等庫)進行預處理,可以把一份原始復雜的文檔,清洗并轉化為一份干凈結構化的中間格式。
1.2為什么選擇 Markdown 作為中間格式?
為了更加直觀的對比常見的四種中間格式的適用情形,下面結合這個表格來一起看一下:
特性維度 | TXT (.txt) | Markdown (.md) | HTML (.html) | JSON (.json) |
語義結構保留 | 極低。丟失所有標題、列表、表格等結構,退化為純文本流。 | 良好。能通過簡單標記保留標題層級、列表、代碼塊等核心語義。 | 極高。可以像素級地保留所有原始結構和樣式。 | 靈活可定義??梢詫热莺驮獢祿匀我鈴碗s的結構進行組織。 |
LLM 親和度 | 中等。文本干凈,但缺乏結構會影響 LLM 對上下文層次的理解。 | 高。LLM 對 Markdown 有天生的理解力,結構標記有助于理解上下文。 | 中等。標簽本身是噪音,若不經清洗會嚴重干擾嵌入質量。需要額外處理。 | 高(指 JSON 中的文本值)。需要程序解析后將干凈文本喂給模型。 |
人類可讀性 | 高。非常直觀,所見即所得。 | 極高。既保留了結構,又非常易于人類閱讀和調試。 | 低。充斥著標簽,不便于直接閱讀原始內容。 | 低。是為機器設計的格式,人類難以直接閱讀長篇內容。 |
實現復雜度 | 低。幾乎所有解析庫都能輕松輸出純文本。 | 中等。需要編寫邏輯將文檔結構(如 Word 的 Heading 1)映射到 MD 標記(#)。 | 高。要生成干凈、有效的 HTML,需要復雜的轉換邏輯和嚴格的 XSS 過濾。 | 高。需要為數據定義清晰的 schema(模式),并進行序列化。 |
元數據處理 | 無。無法內嵌元數據(如來源頁碼)。 | 有限??梢酝ㄟ^ YAML Front Matter 注入,但不是原生標準。 | 優秀??梢酝ㄟ^ data-* 屬性將元數據與具體元素綁定。 | 極佳。天生就是為組織“數據和元數據”而設計的。 |
一句話總結 | 適用簡單場景:用于處理純散文、無結構的文章,追求極簡。 | 平衡之選:最適合需要保留核心結構且兼顧人機可讀性的場景。 | 追求高保真:當需要復現復雜表格或布局,且不惜處理成本時使用。 | 系統化管道:用于需要精細控制、攜帶大量元數據的自動化、工程化 RAG 流程。 |
從上表可以看出來,其實并不存在“最好”的格式,只有“最適合”的場景。而之所以在許多實踐中傾向于選擇 Markdown,是因為它在 “保留關鍵結構”、“模型友好度”和“人類可讀性(易于調試)” 這三個對維度上取得了很好的平衡。
注:針對更為復雜的的表格或頁面布局建議還是使用 html 格式,這部分內容預計 7 月初我會在結合歷史文章中提到的 IBM 的 RAG 冠軍賽項目復現文章中進行具體介紹。
2、實現原理解析
這個流程圖展示了 RAGFlow 的聊天助手中,引用文件是 Markdown 時的預覽功能完整實現機制。通過 MutationObserver 實時監聽聊天界面的 DOM 變化,自動為 .md 文件鏈接綁定自定義點擊事件,當用戶點擊時攔截默認跳轉行為并創建新標簽頁,同時從鏈接中提取文檔 ID 并讀取認證憑據,通過 GM_xmlhttpRequest 向后端 /v1/chunk/list 接口請求文檔數據,最后利用 marked.js 庫解析 JSON 響應中的內容塊,動態構建并渲染格式化的 HTML 頁面,實現了無侵入式的文檔預覽功能增強。整個流程采用異步處理和錯誤處理機制,確保使用的流暢性和系統穩定性。
3、核心模塊拆解
3.1動態事件監聽與觸發
由于 Ragflow 是一個現代化的單頁應用 (SPA),其聊天內容是動態加載到頁面中的。傳統的頁面加載事件無法監聽到這些后續出現的內容。因此,腳本的核心入口采用了 MutationObserver API。
實現邏輯
初始化一個 MutationObserver 實例,配置它來監視 document.body 及其所有后代節點 (subtree: true) 的添加或刪除 (childList: true)。
當監聽到有新節點被添加到頁面時,腳本會遍歷這些節點,并使用 querySelectorAll 高效地查找其中所有指向 .md 文件的 (這里有個東西)鏈接。
為了避免重復綁定,腳本為每個處理過的鏈接添加了一個 data-md-preview-handled 屬性作為標記。如果鏈接未被標記,則為其 click 事件綁定核心處理函數 processMarkdownLink。
關鍵函數說明
new MutationObserver(callback): 創建一個觀察者對象,當指定的 DOM 變化發生時,執行回調函數。這是應對動態網頁內容變化的不二之選。
observer.observe(targetNode, options): 啟動觀察者。{ childList: true, subtree: true } 是性能和功能之間的完美平衡,確保了不會錯過任何動態添加的鏈接。
3.2核心處理流程
當用戶點擊目標鏈接后,這個函數會被觸發,并按序執行一系列操作。
實現邏輯
即時響應與阻止默認:立刻調用 event.preventDefault() 和 event.stopPropagation(),阻止瀏覽器執行默認的、無法預覽的跳轉行為。同時,window.open() 打開一個新標簽頁并顯示“加載中”,給予用戶即時反饋。
信息采集:通過正則表達式從鏈接的 href 屬性中精確提取出 doc_id。隨后,從瀏覽器的 localStorage 中獲取 Authorization Token,這是后續與后端 API 進行認證通信的關鍵憑證。
關鍵函數說明
event.preventDefault(): 阻止事件的默認動作,此處即阻止鏈接跳轉。
window.open('', '_blank'): 打開一個新窗口,并保留其句柄(tempWindow),以便后續向其寫入內容。
localStorage.getItem('Authorization'): 從瀏覽器本地存儲中獲取身份驗證令牌。腳本的運行環境與 Ragflow 頁面相同,因此可以直接訪問。
3.3后端數據安全交互
獲取到必要信息后,腳本需要與 Ragflow 的后端進行通信,以拉取文檔的完整內容。
實現邏輯
利用油猴提供的 GM_xmlhttpRequest 發起一個 POST 請求到 Ragflow 的 /v1/chunk/list API 端點。
請求頭中必須包含 Content-Type 和從 localStorage 中獲取的 Authorization Token。
請求體是一個 JSON 對象,包含了需要查詢的 doc_id 和一個較大的 limit 值,以確保能一次性獲取所有內容塊 (chunks)。
通過設置 onload、onerror 和 ontimeout 回調函數,對請求的成功、失敗和超時情況進行全面處理。
關鍵函數說明
GM_xmlhttpRequest(details): 油猴的特權 API,可以突破同源策略 (CORS) 的限制,是實現該功能的核心。腳本頭部的 @connect localhost 和 @connect 127.0.0.1 就是在為此 API 授權。
JSON.stringify(payload): 將 JavaScript 對象序列化為 JSON 字符串,作為請求體發送。
3.4前端動態渲染與呈現
這是把枯燥的數據轉化為美觀頁面的最后一步,也是用戶最終能感知到的部分。
實現邏輯
數據解析:在 onload 回調中,腳本首先解析 API 返回的 JSON 數據,分離出文檔的元數據 (doc) 和內容塊數組 (chunks)。
HTML 結構構建:腳本使用模板字符串動態生成一個完整的 HTML 頁面結構。這包括:
一個美觀的頭部,包含文檔標題。
一個展示文檔 ID、創建時間等信息的“元數據卡片”。
遍歷 chunks 數組,為每個內容塊創建一個獨立的“內容卡片”。一個巧妙的設計是,每個塊的原始 Markdown 內容被存儲在一個隱藏的 <textarea>中,這可以防止瀏覽器錯誤地解析其中的特殊字符。
集成 Markdown 解析器:在生成的 HTML 的<head>部分,通過 CDN 引入了輕量且強大的 marked.js 庫。
最終渲染:頁面主體包含一段內聯的<script>。它在 DOMContentLoaded 事件觸發后執行,遍歷所有的內容卡片,讀取隱藏<textarea>中的 Markdown 原文,使用 marked.parse() 將其轉換為 HTML,最后注入到對應的容器中,完成最終的渲染。
寫入頁面:調用 tempWindow.document.write(),將整個拼接好的 HTML 字符串寫入之前打開的新標簽頁中,瀏覽器會解析并展示這個頁面。
關鍵函數說明
marked.parse(markdownString): marked.js 庫的核心函數,將傳入的 Markdown 格式字符串轉換為 HTML 字符串。
tempWindow.document.write(html): 向指定窗口的文檔流中寫入數據。這是動態創建整個頁面的關鍵。
4、填過的這些坑
4.1鏈接與彈窗的直接對抗
我最開始的想法是攔截鏈接的點擊事件,直接使用 fetch 請求鏈接地址,然后將獲取到的文本內容放入一個自己創建的彈窗中顯示。但是發現彈窗成功彈出,但里面是空的。
通過 F12 開發者工具的“控制臺”和“網絡”面板發現,請求鏈接返回的是整個 Ragflow 應用的 HTML 主頁面。這是第一個碰到的障礙:單頁應用(SPA)的路由機制。就是服務器被配置為將所有無法直接匹配的路徑都重定向到應用的入口 index.html,由前端 JavaScript 來接管后續的路由和數據加載。這說明最初的腳本從一開始就敲錯了門。
4.2尋找真正的 API
既然直接請求鏈接行不通,那一定有一個隱藏的、真正用來獲取文件內容的 API 接口,這就要通過監視 Ragflow 自身的網絡請求來找到它。
通過在知識庫頁面操作,成功的在“網絡”面板捕獲到了一個形如 /v1/chunk/list 的 API 請求。但是當腳本去請求這個新發現的 API 時,服務器卻返回了“401 未授權”的錯誤。測試下來發現其實是兩層防御問題需要克服下:
第一層防御:HttpOnly Cookie
最初嘗試用 document.cookie 手動附加 Cookie,但失敗了。這意味著最關鍵的登錄憑證被存儲在 HttpOnly Cookie 中,JavaScript 無法讀取。
第二層防御:授權令牌 (Authorization Token)
即使讓油猴腳本自動攜帶 Cookie,請求依然失敗。最后發現,Ragflow 使用了更安全的 Token 認證機制。真正的“通行證”不是 Cookie,而是一個存儲在 localStorage 中的、名為 Authorization 的令牌,它必須被放在請求頭中發送。
4.3與前端框架的終極博弈
在擁有了所有正確的鑰匙(API 地址、請求方法、認證令牌、請求參數),成功獲取到了包含 Markdown 內容的 JSON 數據?,F在只需要將它顯示出來即可。但是彈窗就是無法穩定地顯示在頁面上。有時會閃現一下,有時干脆沒有任何反應,控制臺也沒有任何錯誤。
通過使用 debugger 凍結時間的方式發現,彈窗確實被成功添加到了頁面上,但幾乎在同一瞬間就被 Ragflow 的前端框架(React)給“凈化”或移除了,因為它不屬于框架管理的“虛擬 DOM”結構。這個算是碰到了現代前端框架最核心的壁壘——絕對的 DOM 控制權。任何試圖在框架“管轄范圍”之外直接操作 DOM 的行為,都注定會失敗。
4.4放棄對抗,另辟蹊徑
最后我放棄了在原頁面顯示彈窗的方案,選擇和 RAGFlow 的原生文件預覽方式一樣,在新標簽頁中渲染。也就是在腳本攔截點擊后,在后臺完成所有正確的數據請求。然后,它將獲取到的元數據和 Markdown 內容,動態地構建成一個完整的、獨立的 HTML 頁面。最后,通過打開一個新標簽頁來展示這個頁面。
最后展示的效果中,進一步提取了文檔的元數據(如 ID、創建時間等),并采用 iOS 扁平化設計風格,將所有信息以清晰的卡片式布局呈現出來。
5、寫在最后
5.1關于油猴腳本
單頁應用路由、Token 身份認證、前端框架的 DOM 控制權,幾乎是每一個試圖對現代 Web 應用進行個性化增強的開發者都會遇到的“三座大山”。而在 RAG 流程乃至更廣泛的 Web 應用生態中,油猴腳本所代表的客戶端注入模式,積極意義遠超“小打小鬧”的范疇:
敏捷與個性化
官方產品迭代有其固定的節奏和優先級。而作為一線用戶的痛點往往是即時且個性化的。油猴腳本可以快速實現特定工作流中的功能補完,將產品打磨成最適合自己或團隊的形態。
低風險與高兼容性
這種增強方式是非侵入式的,不用修改任何服務端代碼,也不觸碰核心前端應用的文件。這意味著它幾乎不會對原系統的穩定性造成任何風險。只要 Ragflow 的核心 API 保持穩定,即使前端界面升級,腳本大概率也能繼續工作,維護成本極低。
5.2從 UI 增強到工作流自動化
這個 Markdown 預覽腳本只是拋磚引玉,各位可以解決自己在使用 Ragflow 或其他工具時遇到的個性化問題。值得探索的方向還有:
知識庫管理增強
編寫腳本,在知識庫文檔列表頁面,為每個文檔增加“計算預估 Token 數”、“快速查看分塊摘要”等按鈕,在上傳和管理階段提供更多決策支持。
聊天交互的自動化
在聊天輸入框旁,增加一個“常用 Prompt 模板”面板,一鍵發送復雜的指令?;蛘咴黾右粋€“導出對話”按鈕,將當前問答流程以特定格式保存到本地。
跨系統工作流集成
更高階的玩法,是讓腳本成為連接器。例如,在預覽頁面增加一個按鈕,可以將某個重要的 Chunk 內容,連同其元數據,一鍵發送到 Notion、Obsidian 等筆記軟件中。