今天要解的問題
我的 llm-wiki vault 裡有兩個都叫 navi 的儀表板,統計卻越做越重複,像 raw 堆積就兩邊各算一份,而且算法還不一樣。
一個是 Claude Code 裡的 mod(以下叫 mod navi,做法見 Day26 和 Day27),另一個是 Obsidian 的外掛(以下叫 obsidian navi,見把 navi 搬進 Obsidian)。其中 mod navi 一路長大,到了 1426 行:
- 輸入框上方:今日三頁的橫條
- 狀態列:Inbox、raw 未消化、退場提示、稽核提醒
- 面板:TODO/健檢/漫遊三個分頁,再加一個「問 vault…」輸入框
但這些東西大部分其實跟 Claude Code 無關,像各域 raw/wiki 的統計,在 Obsidian 裡看反而更自然。所以我一開始的想法很直接:想把 mod navi 隱藏,全部轉交給 obsidian navi,先盤點一下哪些還沒搬過去。
會想這樣改,是因為我希望兩邊各做各的:在 Obsidian 專注於看內容,在 mod 專注於與 Claude 的互動。
想法與取捨
隱藏還是瘦身?
盤點完之後,方向從「隱藏」轉成「瘦身」,我決定 mod navi 留下這些:
- 輸入框的標色:
[[連結]]、路徑、指令的顏色 - vault skill 的啟動區:books、art、wiki-query、wiki-build 這些我自己的 skill,放在面板裡
- 稽核提醒:
/claude-doctor的提醒,放在某個地方就好 - 久未互動:很久沒碰的頁
「整個隱藏 mod navi」這條路被否決的理由,後來寫進了分工規則:輸入框標色和 skill 啟動區只能在 Claude Code 裡做;obsidian navi 拿不到目前用的模型,做不了「換模型就要重新稽核」的提醒;久未互動要讀 git。
而規則本身是這樣講的:mod navi 每個 turn 都在 Claude 的畫面上,只放開工時用得到、又只有 Claude Code 拿得到資料的東西;看 vault 長什麼樣子,是在 Obsidian 裡做的事。
用一輪拷問把規則定下來
要留什麼大致有了,但細節很多,像啟動區要列哪些 skill、補完要不要擋新名字。我也不確定該用哪個拷問 skill,所以先問了 /mattpocock-skills:ask-matt,接著跑 /mattpocock-skills:grill-with-docs。十幾個問題一路答下來,大多是「1 note-explore, study, topic-probe 需要 2 A 3 OK…」「11 y 12 y 13 y 14 y」這種很短的回答。
這輪拷問產出兩份文件:
- 名詞表:mod navi、obsidian navi、久未互動三個詞各指什麼。單說「navi」有歧義,所以兩邊都要帶前綴。
- 分工規則:綁在 Claude Code 輸入框、session 或 git 上的功能留在 mod navi,其餘的統計與瀏覽一律放 obsidian navi。
規則最後落成一個問句:新功能先問「它需要 Claude Code 的輸入框、session 或 git 嗎?」,需要才進 mod navi。
久未互動為什麼只留一份?
久未互動是「最後互動距今超過 90 天」的 wiki 或 raw 頁,最後互動是用 git 最後一次非批次 commit 和頁面自己的日期取較晚者。
兩邊各做一份(obsidian navi 改用檔案修改時間)這個做法被否決了:剛 clone 的機器上,所有檔案的修改時間都一樣,清單會是錯的;兩份算法也會讓兩邊的結果對不起來。所以 obsidian navi 不讀 git 是刻意的,規則裡還特別註明不要「補上」。
誰先搬?
規則定了之後,工作拆成兩件:obsidian navi 補功能、mod navi 瘦身。順序是 obsidian navi 先上,mod navi 才拿掉統計,不然中間會有一段時間兩邊都看不到。
啟動區為什麼用手動清單?
而前面留下的 skill 啟動區,一開始照「自動讀 .claude/skills/ 的 frontmatter、排除兩個跟人際關係有關的 skill」設計,但我後來還是改成手動好了。清單就變成一個常數陣列,11 個 skill。
參數欄我希望有路徑的自動完成。但拷問第 10 題我提到,可能會有還沒有現有檔名的狀況,例如一本新書或一個新展覽。所以補完只是建議,打一個不存在的名字照樣可以送出。
判準照抄的例外
原本說好「搬的是算法,判準照抄,不另訂」,但實作時發現兩邊的 raw 堆積判準本來就不一樣:
| 面向 | obsidian navi | mod navi |
|---|---|---|
| 不算堆積 | 被 wiki 連到、有同名 wiki、或寫進 sources | 檔名出現在任何 wiki 內文 |
| ideas 堆積數 | 10 | 6 |
我決定保留 obsidian navi 的判準(它同時也用在健康分數),把這個例外記下來。
實作
obsidian navi 補上的
健診頁多了一區「另列(不扣分)」,放五塊:
- 退場 raw 的四種提示
- 各域 raw/wiki 表
- 跨域連結表
- 書的擴散
- HUB 表
另外趣味頁補齊排行、新增說明分頁,健診頁的「TODO 未勾」也可以點了直接開 TODO.md。



資料只取 Obsidian 的 metadataCache,不讀 git。跨域連結的算法照 mod navi 的判準改寫:
// 網路統計用的連結(判準同 mod navi):只算 [[ ]]/![[ ]](含 frontmatter;markdown 式連結不算),
// 目標限 mod navi 索引得到的頁(根目錄、頂層資料夾、各域 wiki/raw/logs 第一層),同一頁連同一目標只算一次
const wikiEdges = new Map()
for (const p of wiki) {
const dests = new Set()
for (const l of [...(p.cache.links || []), ...(p.cache.embeds || []), ...(p.cache.frontmatterLinks || [])]) {
if (l.original && !/^!?\[\[/.test(l.original)) continue
const dest = mc.getFirstLinkpathDest(l.link.split('#')[0].split('|')[0].trim(), p.path)
if (dest && dest.path !== p.path && info.has(dest.path) && indexed(dest.path)) dests.add(dest.path)
}
wikiEdges.set(p.path, [...dests])
}
每篇 wiki 的連結先濾掉不是 [[ ]] 開頭的,再用 Obsidian 自己的 getFirstLinkpathDest 解析到檔案,只留 mod navi 索引得到的頁。
mod navi 剩下的
mod navi 現在只剩四項:
- 輸入框標色:檔名索引排除私人資料夾,補掃學習計畫的資料夾
- skill 啟動區:面板的第一個分頁
- 稽核提醒:狀態列唯一的一項,沒事就不顯示
- 久未互動:照舊讀 git
面板分頁變成啟動/久未互動/說明。

啟動區的清單是手動維護的常數,每個 skill 標明參數是哪一種:
// path=參數是 vault 路徑,給路徑補完;text=自由文字,from 指定從哪些資料夾的檔名給補完;none=點了直接填
export type Launch = { name: string; desc: string; arg: 'path' | 'text' | 'none'; from?: string[] }
export const SKILLS: Launch[] = [
{ name: 'inbox-triage', desc: '分類 Inbox(可指定檔名)', arg: 'path' },
{ name: 'wiki-build', desc: '整合 raw 成 wiki 頁', arg: 'path' },
{ name: 'wiki-query', desc: '問 vault', arg: 'text' },
{ name: 'book-review', desc: '書名(登記/心得/ingest)', arg: 'text', from: ['books/raw'] },
{ name: 'claude-doctor', desc: 'Claude 設定健檢', arg: 'none' },
// ...
]
原本的「問 vault…」輸入框就併進來,變成 /wiki-query 那一列。
mod 的 Input 沒有原生的下拉或 Tab 補完,所以補完是自己做的:onInput 把草稿存進 state,框下列出最多 8 筆相符的路徑(按鈕),點了放進參數欄。排序只是簡單的前綴優先:
// 只是建議:前綴相符的排前面,其次是包含;跟草稿一模一樣的不列(已經打完了)
export function rankHints(candidates: string[], draft: string): string[] {
const q = draft.trim().toLowerCase()
if (!q) return []
const pre = candidates.filter(c => c.toLowerCase().startsWith(q))
const mid = candidates.filter(c => !c.toLowerCase().startsWith(q) && c.toLowerCase().includes(q))
return [...pre, ...mid].filter(c => c !== draft.trim()).slice(0, MAX_HINTS)
}
按 Enter 一律用框裡的原文,把 /{skill} {參數} 填進主輸入框,不送出。標色與補完拆成兩個純函式模組 decorate.ts、launch.ts,方便寫測試。
怎麼驗證
obsidian navi 那邊沒有測試框架,所以 AI 寫了一個假的 Obsidian harness(假 vault、metadataCache、DOM),在真的 vault 上跑 main.js,再把 mod navi 的算法移植成對照腳本,兩邊比數字。節錄一段對照:
domain wiki(obs/mod) raw(obs/mod) pile(obs)/orphans(mod)
ideas 129/129 47/47 10/6
engineer 68/68 90/90 4/4
books 47/47 230/230 0/0
edges obs/mod 1456 1420 cross obs/mod 455 454
各域 wiki、raw 數全部一致;堆積數不同是前面講的判準例外。
mod navi 那邊用引擎自己的測試 kit,在假 vault 上跑整個 mod,測試從 6 個增到 15 個。
前後
| 項目 | 前 | 後 |
|---|---|---|
| mod navi 主檔 | 1426 行 | 486 行 |
| 整體改動 | — | +564/−1191 |
obsidian main.js | — | +290 行 |
| mod navi 測試 | 6 個 | 15 個 |
| 狀態列 | 好幾種提示 | 只剩稽核提醒 |
| 面板分頁 | TODO/健檢/漫遊 | 啟動/久未互動/說明 |
踩到的坑
連結數為什麼對不上?
obsidian navi 第一版算出來的連結數,跟 mod navi 差了一截。原因是 Obsidian 的 resolvedLinks 把 markdown 式的連結也算進去,而且目標不限 mod navi 索引得到的範圍。
改成只算 [[ ]]/![[ ]]、目標限 mod 索引得到的頁之後,跨域連結變成 455 vs 454。總連結數 1456 vs 1420 剩下的差距,來自兩邊解析同名頁的規則不同(Obsidian 自己的解析 vs mod navi 的 wiki 優先),這個就保留了。
「跟 W7 一致」寫錯了
AI 當時把退場 raw「被引用」的判準寫成「被 wiki/hub 連到、有同名 wiki、或寫進 sources,跟 lint 的 W7 一致」。結果 code review 時才發現,W7 其實只認 wiki/hub 頁的連結。
既然決定是照 W7,程式就改成只認 wiki/hub 連結,判準的說明也跟著更正。raw 堆積的判準則不變。
手機版沒有輸入欄
型別檢查報 Input 不在某個 surface 的元素表裡:mobile 根本沒有輸入欄。舊版的 mod 其實也有一樣的錯,只是沒人在手機上開過面板。
一開始只判斷 'Input' in els 不夠,因為測試裡 mobile 照樣畫出 Input,只是被默默丟掉。最後加上 surface 的判斷:
// 手機版沒有 Input:參數型 skill 退成按鈕,只填 `/{skill} `
const Input = e.surface !== 'mobile' && 'Input' in els ? els.Input : undefined
手機上每個 skill 退成一個按鈕,點了只填 /{skill} 。
測試 kit 的路徑
假檔案系統一開始全部回空,因為引擎把 mod 的相對路徑解析成測試工作目錄底下的絕對路徑。在測試裡換回 vault 的相對路徑才通。
被反證的兩個安全疑慮
每輪 review 都有一個 security 和一個 code 的 subagent 平行跑,security 標成「需要驗證」的,再交給另一個 subagent 去反證。兩次都被反證了:一個是斷鏈清單可能顯示私人資料夾的連結文字,一個是 git log 的 pathspec 可能掃到私人資料夾。兩種情況現在都沒有(git 歷史裡 0 筆),邏輯也在改之前就有,但還是順手加了防護。
小結
mod navi 現在只剩四項:輸入框標色、skill 啟動區、稽核提醒、久未互動;看 vault 長什麼樣子的統計,全部交給 obsidian navi。
這也回到我一開始想要的定位:在 Obsidian 專注於看內容,在 mod 專注於與 Claude 的互動。之後要加新功能,就先問那一句:「它需要 Claude Code 的輸入框、session 或 git 嗎?」需要才放進 mod navi,其他都去 Obsidian。
