fix(t86b): manifest 分實例——換知識庫後同資料夾不再被舊帳跳過
根因:manifestPathFor 雜湊只含資料夾路徑,換實例讀到舊帳本 → 全部檔案被視為已同步 → 新知識庫靜默拿不到資料(leo 機實證:刪 manifest 才通)。 - instanceHostOf:CypherURL 的 host 作為實例鍵 - manifestPathFor:sha256(host+"\n"+absRoot),單根多根統一公式 - migrateManifestIfNeeded:新名不在、舊名在 → rename 過戶(一次性、冪等) - 換回舊實例帳本仍在=不重傳(每實例一本帳,設計目標) 已知殘窗(記 tasks.md):升級後第一次掃描前就換實例,舊帳會被過戶給新實例 (舊格式無實例資訊,無從分辨);常態下升級後首掃已把帳過戶給舊實例,窗口極窄。 測試:新增 6 條(分實例/穩定/單根遷移/多根遷移/冪等)+三模組全綠(總管親跑)。 (實作=子 CC;驗證+commit=總管)
This commit is contained in:
@@ -27,6 +27,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -205,18 +206,57 @@ func (c *DirectConfig) Folders() []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// manifestPathFor 回傳某根的 manifest 路徑。單根=沿用 cfg.Manifest(升級不丟既有狀態);
|
||||
// 多根=每根一份,基底名加 root 絕對路徑的 sha256 前 8 碼尾碼(路徑穩定=尾碼穩定)。
|
||||
func (c *DirectConfig) manifestPathFor(absRoot string) string {
|
||||
folders := c.Folders()
|
||||
if len(folders) <= 1 {
|
||||
return c.Manifest
|
||||
// instanceHostOf extracts the host from a CypherURL to use as a per-instance
|
||||
// distinguisher in manifest paths (t86b). Falls back to the full URL if parsing fails.
|
||||
func instanceHostOf(cypherURL string) string {
|
||||
u, err := url.Parse(cypherURL)
|
||||
if err != nil || u.Host == "" {
|
||||
return cypherURL
|
||||
}
|
||||
sum := sha256.Sum256([]byte(absRoot))
|
||||
return u.Host
|
||||
}
|
||||
|
||||
// manifestPathFor 回傳某根的 manifest 路徑。
|
||||
// t86b:雜湊改為 sha256(instanceHost + "\n" + absRoot),讓每個(實例, 資料夾)組合對應
|
||||
// 獨立帳本——換知識庫實例後同資料夾不再重用舊帳本,避免「全部視為已同步」靜默跳過。
|
||||
// 舊格式遷移見 migrateManifestIfNeeded:啟動/掃描時若新名不存在但舊名存在則 rename 過來。
|
||||
func (c *DirectConfig) manifestPathFor(absRoot string) string {
|
||||
host := instanceHostOf(c.CypherURL)
|
||||
sum := sha256.Sum256([]byte(host + "\n" + absRoot))
|
||||
ext := filepath.Ext(c.Manifest)
|
||||
return strings.TrimSuffix(c.Manifest, ext) + "-" + hex.EncodeToString(sum[:4]) + ext
|
||||
}
|
||||
|
||||
// oldManifestPaths 回傳 t86b 之前版本對同一個 absRoot 會產生的 manifest 路徑,
|
||||
// 供遷移時確認是否有舊帳本需要搬移。
|
||||
// 優先序:①舊多根(只含路徑雜湊)→ ②舊單根(直用 cfg.Manifest)。
|
||||
func (c *DirectConfig) oldManifestPaths(absRoot string) []string {
|
||||
ext := filepath.Ext(c.Manifest)
|
||||
base := strings.TrimSuffix(c.Manifest, ext)
|
||||
// 舊多根公式:sha256(absRoot only)
|
||||
sumPathOnly := sha256.Sum256([]byte(absRoot))
|
||||
pathOnlyPath := base + "-" + hex.EncodeToString(sumPathOnly[:4]) + ext
|
||||
// 舊單根公式:直用 cfg.Manifest
|
||||
return []string{pathOnlyPath, c.Manifest}
|
||||
}
|
||||
|
||||
// migrateManifestIfNeeded 在 newPath 不存在時,把最先找到的舊格式 manifest rename 過來。
|
||||
// 一次性、冪等:newPath 已存在時直接 return;rename 失敗靜默忽略(最多這一輪重傳,不影響正確性)。
|
||||
func (c *DirectConfig) migrateManifestIfNeeded(absRoot, newPath string) {
|
||||
if _, err := os.Stat(newPath); err == nil {
|
||||
return // 新路徑已存在,無需遷移
|
||||
}
|
||||
for _, oldPath := range c.oldManifestPaths(absRoot) {
|
||||
if oldPath == newPath {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(oldPath); err == nil {
|
||||
_ = os.Rename(oldPath, newPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// directHTTP 是 direct 模式共用的 HTTP client(萃取 workflow 可能同步跑 LLM,放寬 timeout)。
|
||||
var directHTTP = &http.Client{Timeout: 300 * time.Second}
|
||||
|
||||
@@ -394,6 +434,7 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool) ([]DirectRes
|
||||
if err != nil {
|
||||
return append(results, DirectResult{Status: "failed", Error: err.Error()}), 1, nil
|
||||
}
|
||||
cfg.migrateManifestIfNeeded(absRoot, absManifest) // t86b:一次性遷移舊格式帳本
|
||||
m, err := LoadManifest(absManifest, absRoot)
|
||||
if err != nil {
|
||||
return append(results, DirectResult{Status: "failed", Error: err.Error()}), 1, nil
|
||||
|
||||
Reference in New Issue
Block a user