fix(collector): 機器寫進筆記庫的檔案一律帶前綴,不再跟使用者的頁面撞名(arcrun-rag#60)
leo 2026-08-12:「我的 Logseq 又被覆蓋⋯⋯**不只是加上 journal,可能所有的檔案都加一個前後綴,比如「wiki」**。」 不是資料被蓋掉,是機器產出用了跟他一樣的命名空間(status.md、日期檔那些) ⇒ 他打開資料夾分不出哪些是自己的。**心理上的覆蓋跟實際覆蓋一樣糟。**
This commit is contained in:
+85
-14
@@ -81,21 +81,47 @@ func cardsRelDirFor(absRoot string) string {
|
||||
return cardsRelDir
|
||||
}
|
||||
|
||||
// snapshotCards 記 cards/ 目前每檔 mtime+size(偵測萃取後的新卡/變卡)。
|
||||
// cardRelFor 回傳「這份原稿萃出來的卡片」相對監看根的完整路徑。
|
||||
//
|
||||
// 🔴 arcrun-rag#60 第二輪:**daemon 在監看根底下產生的檔名一律經過這裡**,
|
||||
// 誰都不准自己拼 `pageName + ".md"`。目前三個呼叫端必須拿到同一個答案,
|
||||
// 少一個對齊就會留下孤兒檔或清不掉的卡:
|
||||
//
|
||||
// ① 落卡(extract_gemma.go/extract_workersai.go)
|
||||
// ② 下架時清本地卡(direct.go 的 removed 分支)
|
||||
// ③ 萃取前後的快照比對(snapshotCards,claude 路用)
|
||||
//
|
||||
// 檔名帶 MachineMark 前綴=Logseq/Obsidian 的頁名變成 `arcrun-<原頁名>`,
|
||||
// 與使用者自己的頁面**永不同名**(上一輪漏的正是這一層,見 machinemark.go 開頭)。
|
||||
func cardRelFor(absRoot, pageName string) string {
|
||||
return filepath.ToSlash(filepath.Join(cardsRelDirFor(absRoot), MarkName(pageName+".md")))
|
||||
}
|
||||
|
||||
// snapshotCards 記卡片產物區目前每檔 mtime+size(偵測萃取後的新卡/變卡)。
|
||||
//
|
||||
// 🔴 這裡原本硬寫 cardsRelDir 一個目錄,兩邊都不對:
|
||||
// - vault 上,落卡其實去了 .arcrun-rag/wiki/cards/ ⇒ snapshot 到一個永遠是空的目錄,
|
||||
// diffCards 算不到新卡,claude 路必定誤報「跑完但沒有新卡片」。
|
||||
// - 而 claude 路的**寫檔位置是 skill(prompt)決定的**,它硬寫 system-dev/wiki/cards/,
|
||||
// 連在 vault 上也一樣 ⇒ 只看 cardsRelDirFor 那一個目錄同樣會漏。
|
||||
//
|
||||
// ⇒ 兩個目錄都掃。誰寫到哪裡都逃不掉,之後由 enforceCardMarks 統一歸位+補標記。
|
||||
func snapshotCards(absRoot string) map[string]fileState {
|
||||
out := map[string]fileState{}
|
||||
base := filepath.Join(absRoot, filepath.FromSlash(cardsRelDir))
|
||||
_ = filepath.Walk(base, func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() || filepath.Ext(p) != ".md" {
|
||||
for _, relDir := range []string{cardsRelDir, vaultCardsRelDir} {
|
||||
base := filepath.Join(absRoot, filepath.FromSlash(relDir))
|
||||
_ = filepath.Walk(base, func(p string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() || filepath.Ext(p) != ".md" {
|
||||
return nil
|
||||
}
|
||||
rel, rerr := filepath.Rel(absRoot, p)
|
||||
if rerr != nil {
|
||||
return nil
|
||||
}
|
||||
out[filepath.ToSlash(rel)] = fileState{size: info.Size(), mtime: info.ModTime().UnixNano()}
|
||||
return nil
|
||||
}
|
||||
rel, rerr := filepath.Rel(absRoot, p)
|
||||
if rerr != nil {
|
||||
return nil
|
||||
}
|
||||
out[filepath.ToSlash(rel)] = fileState{size: info.Size(), mtime: info.ModTime().UnixNano()}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -144,7 +170,52 @@ func ExtractWithClaude(binPath, absRoot, relPath string) ([]string, error) {
|
||||
|
||||
cards := diffCards(before, snapshotCards(absRoot))
|
||||
if len(cards) == 0 {
|
||||
return nil, fmt.Errorf("claude 跑完但 %s 沒有新卡片(輸出尾段:%.200s)", cardsRelDir, string(out))
|
||||
return nil, fmt.Errorf("claude 跑完但 %s 沒有新卡片(輸出尾段:%.200s)", cardsRelDirFor(absRoot), string(out))
|
||||
}
|
||||
return cards, nil
|
||||
return enforceCardMarks(absRoot, cards), nil
|
||||
}
|
||||
|
||||
// enforceCardMarks 把「不是我們親手命名」的卡片**歸位並補上標記**。
|
||||
//
|
||||
// 🔴 為什麼這條路需要它:claude 路的檔名與寫入目錄都是**skill 檔(prompt)決定的**,
|
||||
// 不是 Go 決定的(/rag-extract-file 自己寫 `system-dev/wiki/cards/<頁名>.md`,
|
||||
// 而且不認得 vault)。若放著不管,同一個 daemon 就會出現「gemma 路的卡有前綴、
|
||||
// claude 路的卡沒有」,而且在 vault 上還會落在**可見目錄**——正好踩中
|
||||
// 「一部分加一部分不加,比全不加更難分」那條紅線,且破口在 prompt 裡,改 Go 永遠修不到。
|
||||
//
|
||||
// ⇒ **命名與落點的契約收回 Go 這一側強制執行**:不管上游寫成什麼、寫到哪,
|
||||
// 離開這個函式時一定是 cardRelFor 算出來的那個位置與名字。
|
||||
// 目標已被佔用或搬不動時,照實回報原路徑(不覆蓋、不假裝成功),剩下的交給 tidy。
|
||||
func enforceCardMarks(absRoot string, cards []string) []string {
|
||||
want := cardsRelDirFor(absRoot)
|
||||
out := make([]string, 0, len(cards))
|
||||
for _, rel := range cards {
|
||||
base := filepath.Base(rel)
|
||||
dir := filepath.ToSlash(filepath.Dir(rel))
|
||||
if IsMarked(base) && dir == want {
|
||||
out = append(out, rel)
|
||||
continue
|
||||
}
|
||||
newRel := cardRelFor(absRoot, UnmarkName(strings.TrimSuffix(base, ".md")))
|
||||
if newRel == rel {
|
||||
out = append(out, rel)
|
||||
continue
|
||||
}
|
||||
from := filepath.Join(absRoot, filepath.FromSlash(rel))
|
||||
to := filepath.Join(absRoot, filepath.FromSlash(newRel))
|
||||
if _, err := os.Stat(to); err == nil {
|
||||
out = append(out, rel) // 新位置已被佔用,不覆蓋
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(to), 0o755); err != nil {
|
||||
out = append(out, rel)
|
||||
continue
|
||||
}
|
||||
if err := os.Rename(from, to); err != nil {
|
||||
out = append(out, rel) // 搬不動就照實回報原路徑,不假裝成功
|
||||
continue
|
||||
}
|
||||
out = append(out, newRel)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user