From febc6708effea49ae26b283fd23fc292e3daf583 Mon Sep 17 00:00:00 2001 From: richblack Date: Sun, 16 Aug 2026 13:13:59 +0800 Subject: [PATCH] =?UTF-8?q?WIP(#44=20=E2=91=A6=E2=91=A9)=EF=BC=9A=E9=97=9C?= =?UTF-8?q?=E8=81=AF=E6=AE=B5=E6=93=B7=E5=8F=96=E5=BC=8F=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E8=A2=AB=20H3=20=E6=89=93=E6=96=B7=EF=BC=9B=E6=94=B9=E5=90=8D/?= =?UTF-8?q?=E6=90=AC=E7=A7=BB=E5=81=B5=E6=B8=AC=E6=96=BD=E5=B7=A5=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⑦(總管已複驗):雲端 parse_card 的邊界正規式 lookahead 是 \n#, 會停在「## 關聯」底下第一個 ### 之前。而規範形卡(ef5e6c5)正是 ### 內文知識關係 開頭 ⇒ 除機械補的 part_of 外,關係一條都收不到。 agent 對 youlin 實際部署的 rag_ingest_card POST 真卡:卡上 4 條/落地 1 條。 改成只認下一個 H1/H2 後離線重跑同一段 JS:解析出 4 條。 併同重編 installer 兩份 precompiled workflows(只改 yaml 不重編=新用戶拿不到)。 ⑩(施工中):direct.go/manifest.go 的改名搬移下架,尚未驗收。 🔴 尚未送達:這一版還沒部署到 youlin,live after 數字還沒拿到。 不得標 ✅。出貨 CP 步驟①。 未納入本次 commit:collector/cmd/arcrun-app/arcrun-app(20MB 編譯產物) 與兩個探針目錄——那是驗證用的臨時物,不該跟修法一起進版控。 Co-Authored-By: Claude Opus 5 --- direct.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++- manifest.go | 29 +++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/direct.go b/direct.go index 6ddd0a8..05cc95c 100644 --- a/direct.go +++ b/direct.go @@ -1067,6 +1067,21 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool, qs *quotaSta } } + // InkStoneCo#44 ⑩:改名/搬移後,舊路徑在雲端知識庫裡要跟著下架,否則舊卡永久留著 + // (純改檔名)或同一份文件在庫裡變兩套(搬到別的資料夾,新舊頁名相同)。 + // renamed 事件只在 removed×added 配對到的那一輪出現一次(見 scan.go 步驟 3), + // 下一輪不會再有機會補發——所以「這個舊路徑要下架」必須在偵測到的當下就 + // 寫進 manifest(QueueTakedown,持久化),不能只靠事件迴圈處理到才記,否則單輪 + // 上限(perRunCap)把它排除在外、或這輪下架失敗時就會永久遺失這個待辦。 + for _, ev := range payload.Events { + if ev.Type == "renamed" && ev.OldPath != "" { + m.QueueTakedown(ev.OldPath, pageNameOf(ev.OldPath)) + } + } + if len(m.PendingTakedowns) > 0 && !dryRun { + saveManifest() + } + // 2026-08-07 pacing task 1:新改的檔優先+單輪上限。 // - 排序:把 payload.Events 依檔案 mtime 由新到舊重排(不動送雲端的 payload 本身, // 只重排這裡的本機處理佇列——見 sortEventsNewestFirst 註解)。 @@ -1085,7 +1100,9 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool, qs *quotaSta switch ev.Type { case "added", "modified", "renamed": // renamed 在 direct 模式視同 added:內容未變但為求 kbdb 有這頁名的卡,重送一次萃取 - //(頁名可能改變=要新頁名的卡)。冪等由 kbdb 端承擔(同頁名覆蓋語意)。 + //(頁名可能改變=要新頁名的卡)。新路徑這邊的冪等由 kbdb 端承擔(同頁名覆蓋語意); + // 舊路徑那邊不會自動消失——上面已經把 ev.OldPath 排進 m.PendingTakedowns, + // 這裡送完新卡之後、本函式結尾會補打下架(InkStoneCo#44 ⑩)。 res := DirectResult{Type: ev.Type, Path: ev.Path} // 2026-08-07 pacing task 2:帳號還在額度冷卻中 → 這輪連試都不試。 // 這不是這個檔的問題(不記 FailCount/退避——那是「這個檔」的病歷, @@ -1320,6 +1337,49 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool, qs *quotaSta } } + // InkStoneCo#44 ⑩:補打「改名/搬移後還沒下架成功」的舊路徑——包含本輪剛 + // 上面排進去的,以及之前輪次失敗留下的(同一個待辦清單,一次處理完)。 + // 與 orderedEvents 共用同一個節流器(pace),避免一輪多筆改名瞬間打爆雲端。 + if len(m.PendingTakedowns) == 0 { + // no-op:沒有待辦 + } else if dryRun { + for oldPath := range m.PendingTakedowns { + results = append(results, DirectResult{Type: "renamed_takedown", Path: oldPath, Status: "planned"}) + } + } else { + for oldPath, pageName := range m.PendingTakedowns { + pace() + res := DirectResult{Type: "renamed_takedown", Path: oldPath} + status, _, perr := cfg.postJSON(cfg.triggerURL(cfg.RemovedWF), map[string]any{ + "page_name": pageName, + "path": oldPath, + }) + res.HTTPStatus = status + if perr != nil { + res.Status, res.Error = "failed", "改名/搬移後舊頁下架失敗(下輪重試):"+perr.Error() + exit = 1 + } else { + res.Status = "removed" + m.ClearTakedown(oldPath) + // 同步清掉本地舊卡(若還存在)——與「removed」分支同一套清理,見下方。 + if cfg.Extractor != "" { + cardAbs := filepath.Join(absRoot, filepath.FromSlash(cardRelFor(absRoot, pageName))) + if _, serr := os.Stat(cardAbs); serr == nil { + _ = os.Remove(cardAbs) + } + if werr := RemoveWikiDoc(absRoot, oldPath); werr != nil { + results = append(results, DirectResult{ + Type: "warning", Path: oldPath, Status: "skipped", + Error: "wiki 卡收走失敗(不擋下架):" + werr.Error(), + }) + } + } + } + saveManifest() + results = append(results, res) + } + } + // 防呆警告輪:Scan 已壓下 removed 事件,這裡只回報警告不下架。 for _, w := range payload.Warnings { results = append(results, DirectResult{Type: "warning", Status: "skipped", Error: w.Code + ": " + w.Message}) diff --git a/manifest.go b/manifest.go index f0d8b66..eb14e30 100644 --- a/manifest.go +++ b/manifest.go @@ -81,6 +81,35 @@ type Manifest struct { // 雲端一壞就是每 5 秒撞一次。內容變了(雜湊不同)視同新卡,立即可再試。 InventoryFailHash string `json:"inventory_fail_hash,omitempty"` InventoryNextRetry int64 `json:"inventory_next_retry,omitempty"` + + // PendingTakedowns=改名/搬移後「舊路徑」在雲端知識庫裡還沒下架成功的待辦清單 + // (key=舊相對路徑,value=該路徑當時導出的頁名)。InkStoneCo#44 ⑩: + // + // direct 模式把 renamed 事件當 added 處理(用新路徑重送一次萃取),但從未告訴 + // 雲端「舊頁名/舊路徑那份已經死了」——純改檔名時舊頁名的舊卡永久留著;搬到 + // 別的資料夾時(basename 不變 ⇒ 新舊頁名相同)舊的不刪、新的照寫,kbdb 裡同一份 + // 文件變兩套,其中一套指向已不存在的路徑,而且沒有任何機制會回頭發現它。 + // + // 為什麼要持久化而不是「失敗了下一輪自然重試」(removed 事件的作法):removed + // 事件靠「檔案仍然不在」讓 Scan() 每輪重新偵測、重新補發;但 renamed 的配對 + // (removed×added 以 content_hash 配對)只在偵測到的那一輪出現一次,舊路徑已經 + // 不在任何一邊的掃描結果裡,下一輪不會再有 renamed 事件把它帶出來。不記住它, + // 一次下架失敗(雲端剛好那幾秒掛掉)就永久遺失,舊卡從此不會再被清。 + PendingTakedowns map[string]string `json:"pending_takedowns,omitempty"` +} + +// QueueTakedown 記一筆「這個舊路徑(連同當時的頁名)還沒在雲端下架」的待辦。 +// 冪等:同一路徑重複呼叫只覆蓋頁名(理論上不會變,但不假設呼叫端不會重複觸發)。 +func (m *Manifest) QueueTakedown(oldPath, pageName string) { + if m.PendingTakedowns == nil { + m.PendingTakedowns = map[string]string{} + } + m.PendingTakedowns[oldPath] = pageName +} + +// ClearTakedown 下架成功後從待辦清單移除。 +func (m *Manifest) ClearTakedown(oldPath string) { + delete(m.PendingTakedowns, oldPath) } // newUUID 產生 RFC 4122 v4 UUID(純 stdlib)。