cache.py · dedup.py · ingest.py · watch.py · serve.py · security.py · validate.py主線 pipeline(detect→export)是「一次跑完」,而這七支模組是「反覆使用 / 對外服務」時需要的:快取讓重跑便宜、dedup 讓圖乾淨、ingest 進料、watch 盯變化、serve 對外查詢、security/validate 把關。
快取是「圖越大越省」的來源之一:
file_hash(path, root, cache_root) — 算檔案的內容雜湊(_body_content 只算內容、不含路徑——所以改名也能沿用快取;用 _stat_index 以 stat 簽名快取雜湊本身,避免大檔每次重算)。load_cached(path, root, kind) — 依內容雜湊找快取;載入時會把快取內的絕對路徑/ID重新對齊到目前的 root(_relativize_* / _absolutize_*),所以換機器、搬資料夾都能用。save_cached(path, result, root, kind) — 寫快取,同樣做路徑可攜化。cached_files / clear_cache — 列出快取檔 / 清空。除此之外還有 prompt_fingerprint(對 prompt 內容算指紋,語意抽取的快取 key 之一)與 cached_word_count(detect 的字數統計快取)。
段落式說明:與 AST 快取同構,但存的是 LLM 抽取的結果。文件語料每次只把 uncached_files 送 LLM,其餘從快取載入——這是「每天更新的 1000 檔語料」能維持低成本的核心。
這是 設計文件 的落地實作,比設計更周全:
repo 欄位且超過一個 repo → 直接 raise。不同 repo 的 label 撞名是巧合,絕不能靠字串相似度合併。_* helper。_collision_rank 提供「總序」,讓「誰是生還者」與節點到達順序無關——這是去重正確性的關鍵,也是被反覆強調的 issue #1851。段落式說明:
_entropy(label) — 算 label 的資訊熵(bits/char),短歧義名(AI、x)太低就不做模糊比對。_shingles(text, k) / _make_minhash(text) — 3-gram shingles + MinHash(128 排列),供 LSH blocking 用 O(n) 產生候選對。_is_variant_pair(a, b) — 判斷是否為「變體對」(錯字、複數、空格等)。_crossfile_fileanchored_blocked — 跨檔 + 檔錨定的節點不作模糊合併(避免 Account 型別誤併)。graphify add 的實作)。段落式說明:_detect_url_type 先判斷 URL 類型(網頁 / arxiv / tweet / 影音 / 一般二進位),再分派到 _fetch_webpage(HTML→Markdown)、_fetch_arxiv(論文)、_fetch_tweet、_download_binary。所有抓取都走 security 的 safe_fetch。
段落式說明(這支很大,挑重點):
_queue_pending / _drain_pending)把變更先落地、再重建成圖,避免熱循環。_rebuild_lock — 防止併發重建。_reconcile_existing_graph — 把既有圖與新抽取結果調和(增量更新)。_check_shrink — 與 to_json 的 shrink guard 對應,重建結果更小就不覆寫。段落式說明:python -m graphify.serve graphify-out/graph.json 啟動 MCP stdio server,提供 query_graph、get_node、get_neighbors、shortest_path 等工具。
query 不是簡單關鍵字比對,而是一套加權評分:_query_terms 切詞(含中文分詞,jieba)→ _compute_idf 算 IDF → _trigram_index 建三字母索引加速候選 → _score_nodes / _score_query 評分。還有 _GraphContextCache 做多專案圖的快取、_load_graph 對損壞圖的復原訊息(見 SECURITY.md)。
SSRF 防護的解析:
urlparse 檢查 scheme 是否在 _ALLOWED_SCHEMES(http/https)——file://、ftp://、data: 全部擋掉。169.254.169.254 等,_BLOCKED_HOSTS)。socket.getaddrinfo 把 hostname 解析成 IP,逐一檢查是否落在私有/保留網段(_ip_is_blocked,含 127.x、10.x、169.254.x…)。光這樣還不夠——validate_url 只是入口。真正的連線用 _SSRFGuardedHTTP(S)Connection 子類別(下一個 fn-block)。
這是一段很漂亮的進階防禦。註解說得很清楚:不 monkey-patch 全域 socket.getaddrinfo(那在並發下是 thread-safe 的 TOCTOU 隱患),而是子類別化 HTTP(S) connection——每個連線只解析一次 DNS、驗證結果 IP、然後連到那個精確 IP。沒有第二次解析,所以 DNS-rebind 攻擊無法在驗證與連線之間換成私有位址。
教學價值:這說明「驗證 URL」與「安全連線」是兩回事——前者是入口檢查,後者是對抗 TOCTOU 的防禦。
段落式說明:
safe_fetch(url, max_bytes=50MB, timeout=30) — 串流抓取,超過 50MB 中止,非 2xx 拋 HTTPError(錯誤頁不當內容)。safe_fetch_text(url, max_bytes=10MB) — 文字版。validate_graph_path(path, base) — 解析路徑並要求落在 graphify-out/ 內(防 MCP 路徑穿越)。sanitize_label(text) — 去控制字元、上限 256 字元、HTML-escape(防 XSS 與 prompt injection)。check_graph_file_size_cap — graph.json 的 512 MiB 上限。段落式說明:只有 95 行的模組。檢查每個節點/邊的必要欄位(id、label、source_file、source、target、relation),回傳錯誤訊息清單;assert_valid 在錯誤時直接 raise。build_graph() 消費前會先跑它——schema 錯誤就不組圖。
ops 層是圍繞主線 pipeline 的服務:cache(快取)/ dedup(去重)/ ingest(進料)/ watch(監控)/ serve(服務)/ security(安全)/ validate(驗證)。這些模組讓 Graphify 從「能跑」變成「好用」。
cache.py 實作 semantic cache,用內容雜湊為 key,避免重複呼叫 LLM。dedup.py 實作七步去重管線。ingest.py 負責從 URL 下載內容。watch.py 監控目錄變更。serve.py 提供 MCP stdio server。security.py 與 validate.py 負責所有驗證。
安全相關的函數值得特別關注:validate_url() 擋 SSRF、validate_graph_path() 擋路徑穿越、sanitize_label() 擋 XSS 與 prompt injection。
graphify extract my-project/,觀察 token 消耗。graphify extract my-project/。觀察 token 消耗應該為 0(全部從快取讀取)。| 錯誤訊息 | 原因 | 解決方式 |
|---|---|---|
cache: corruption detected | 快取檔案損壞 | 刪除 graphify-out/cache/ 重新執行 |
dedup: MinHash error | datasketch 套件問題 | 執行 pip install datasketch |
ingest: fetch failed | URL 無法存取 | 檢查 URL 是否正確,或是否有網路問題 |
serve: JSON decode error | graph.json 損壞 | 重新執行 graphify extract 產生新的 graph.json |
security: SSRF blocked | 嘗試存取私有 IP | 這是正常的,安全驗證在運作 |
前面是「測試 cache 與 dedup」;這次是把它們串成一個維運系統:你的內部 wiki 有 1000 份 markdown + 幾個 SQLite 資料 dump,每晚從上游同步,你要保證「圖每天新鮮、成本受控、壞了能自癒」。
graphify extract wiki/ 讓 manifest 建起來;之後 nightly 重跑自動走 detect_incremental——只抽 new_files,其餘 semantic cache 命中。changed / cached / deleted;deleted 突增代表上游砍了大量文件,build_merge(prune_sources=deleted) 會自動剪圖。--max-spend;觀察每週 token 統計,若「cached」比例長期偏低,檢查 prompt_fingerprint 是否意外失效。graphify-out/cache/ 重抽;serve 的 _load_graph 對壞 graph.json 印復原訊息而不是當機。dedup 最容易被誤解的細節是「合併時誰活下來」:
_collision_rank 提供總序——每個 ID 只留「定義它的節點」(source_file 就是 ID 編碼的檔),不是「第一個看到的」。這樣 chunk 順序/平行執行順序不會決定誰生還,輸出才可重現。repo 欄位且超過一個 repo → 直接 raise。不同 repo 的 Account 撞名是巧合,絕不能靠字串相似度合併——這是去重的「紅線」。--dedup-llm 仲裁。AI/DB 這種短歧義名(太危險不自動併);而 Jaro-Winkler 只看字元相似,UserRepository 與 UserRepo 可能因「同社群加分」跨過門檻被誤併——尤其當兩者真的在同一個社群(經常 co-occur)時,+0.05 可能就是壓垮駱駝的那根稻草。判讀去重結果,要看「生還者 label 是否涵蓋原意」而不是「看起來像不像」。| 症狀 | 可能原因 | 解決方案 |
|---|---|---|
| cache 命中率長期偏低 | prompt_fingerprint 隨每次 prompt 變動(未版本化 extraction-spec)或內容 hash 含非內容欄位 | 版本化 prompt;確認 file_hash 用 _body_content 不含路徑 |
| dedup 把明顯不同的概念合併 | Jaro-Winkler 太鬆 + 同社群加分疊加跨過門檻 | 調高閾值或檢查社群加分是否該納入;考慮 --dedup-llm 仲裁 |
| 跨 repo 合併圖時 raise | dedup 的跨專案防護啟動(不同 repo 撞名) | 這是紅線防護——改用 merge-graphs 工具而非靠 dedup 併 |
| serve 讀到「損壞的 graph.json」 | 寫入中斷(沒走原子寫)或外部工具改壞格式 | 重新 graphify extract;確認 export 走 write_json_atomic |
watch 熱循環(重建一直觸發) | 重建過程自己寫入 watched 目錄,或 pending 佇列沒 drain | 確認 _queue_pending/_drain_pending 落地;檢查 _rebuild_lock 是否生效 |
_collision_rank 需要補什麼 tiebreak?