feat(collector): 每一則知識都答得出「原稿在哪一台機器上」(inkstone/mira#6)
現況只缺這一格:雲端的麵包屑已經有資料夾多層路徑、檔名、片段錨點 (kb://RFP/design.md#0),唯獨答不出「哪一台機器」。 leo 2026-08-18 拍板的三層,由強到弱: ① 抓得到就用真實可讀的名字 → `<使用者>@<主機名>`(實跑:youlinhsieh@Leo-MBA) ② 使用者要能改成自己看得懂的稱呼 → config.json 的 machine_label(實跑:教育部 Leo 的 Mac) ③ 什麼都抓不到也要保證兩台不同 → unknown@<隨機 8 hex>,持久化在 machine.json 做法照既有的 library 走,不新增第二種: - daemon 逐筆隨 payload 送 machine/machine_label(收卡、總覽卡、下架、舊直送四處) - 雲端寫進 block 的 metadata_json,與 source/source_path/library 並排 - 三元組寫進 machine slot(slot 由 matrix/arcrun 的 seed 補,見該 repo 分支) - upsert/下架的比對規則照抄 arcrun-rag#46 對 library 的處置:**兩邊都有值才收緊** ⇒ 既有資料(沒有這一格的)行為一字不變,不會變兩份、也不會撤不掉 🔴 machine **不混進 source_uri**:portal 的 srcLocalPath() 直接把 kb:// 之後整段當本機 相對路徑用,混進去會吐出不存在的路徑,而且舊資料的第一段會被誤讀成機器名 ——那就是「憑空長出假機器」。分成獨立欄位,舊資料就誠實地空著。 ID 一鑄就不再重算(machine.json):user@host 會變,重算會讓同一台機器的舊知識 突然掛到「新機器」底下。改名走 machine_label,不碰 ID。 驗過(youlin stage,實測輸出見 inkstone/mira#6 回報): - 真 daemon 跑 direct --once → 每個 block 的 metadata 都有 machine=youlinhsieh@Leo-MBA、machine_label=教育部 Leo 的 Mac - 同一相對路徑、兩台機器 → block 各留一份、三元組 4 筆並存,沒有互相刪掉 - 既有資料(無 machine)照樣被同一台機器 upsert 掉,沒有變兩份 - workflows/tests/machine-scope.test.mjs 13 passed;takedown-scope 8 passed;isdoc 21 passed - collector go test ./... 全綠
This commit is contained in:
@@ -107,6 +107,34 @@ type DirectConfig struct {
|
||||
// LintStrict=軟項也擋(測試/CI);預設 false=硬缺拒收、軟項照送標 quality:low。
|
||||
// CLI `--strict` 會覆蓋成 true。
|
||||
LintStrict bool `json:"lint_strict,omitempty"`
|
||||
|
||||
// MachineLabel=這台機器要在雲端顯示成什麼名字(`inkstone/mira#6`,leo 2026-08-18)。
|
||||
// 空=用 machine.json 鑄好的可讀 ID(`youlinhsieh@Leo-MBA`)。
|
||||
// leo:「他再自己去改『教育部 Leo 的 Mac』」——改的是**這一格**,機器 ID 不動,
|
||||
// 所以改名不會讓庫裡憑空多出一台機器。
|
||||
MachineLabel string `json:"machine_label,omitempty"`
|
||||
|
||||
// machine=解析好的機器身分快取(不落 config 檔:ID 的家是 machine.json,
|
||||
// 這裡只是這一輪的記憶體副本。makeAccountSubConfig 的 `sub := *c` 會一起複製,
|
||||
// 所以多帳號同一輪只解析一次、每個帳號送出的值必然一致)。
|
||||
machine *MachineIdentity
|
||||
}
|
||||
|
||||
// machineIdentity 回這台機器的身分(第一次呼叫時解析並鑄檔,之後讀快取)。
|
||||
//
|
||||
// 身分檔跟著 manifest 放(實務上=~/.arcrun-rag/):那是本 daemon 既有的狀態目錄,
|
||||
// 不另立第二個狀態位置。
|
||||
func (c *DirectConfig) machineIdentity() MachineIdentity {
|
||||
if c.machine != nil {
|
||||
return *c.machine
|
||||
}
|
||||
stateDir := "" // 空=不落檔(沒設 manifest 的臨時 config 不該把身分檔亂寫進工作目錄)
|
||||
if strings.TrimSpace(c.Manifest) != "" {
|
||||
stateDir = filepath.Dir(expandHome(c.Manifest))
|
||||
}
|
||||
id := ResolveMachine(stateDir, c.MachineLabel)
|
||||
c.machine = &id
|
||||
return id
|
||||
}
|
||||
|
||||
// librarySlug 把資料夾名轉成合法庫名(A-Za-z0-9_-;中文等非 ASCII 轉為底線分段)。
|
||||
@@ -1002,10 +1030,13 @@ func drainPendingTakedowns(
|
||||
for oldPath, pageName := range m.PendingTakedowns {
|
||||
pace()
|
||||
res := DirectResult{Type: resultType, Path: oldPath}
|
||||
mach := cfg.machineIdentity()
|
||||
status, _, perr := cfg.postJSON(cfg.triggerURL(cfg.RemovedWF), map[string]any{
|
||||
"page_name": pageName,
|
||||
"path": oldPath,
|
||||
"library": cfg.libraryFor(absRoot),
|
||||
"page_name": pageName,
|
||||
"path": oldPath,
|
||||
"library": cfg.libraryFor(absRoot),
|
||||
"machine": mach.ID,
|
||||
"machine_label": mach.Label,
|
||||
})
|
||||
res.HTTPStatus = status
|
||||
if perr != nil {
|
||||
@@ -1457,11 +1488,18 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool, qs *quotaSta
|
||||
if cardIdx > 0 {
|
||||
continue // 概念卡先只落本機 .wiki(品質檢查照跑),上雲等第⑤環
|
||||
}
|
||||
// machine/machine_label(`inkstone/mira#6`):與 library 同一個位置、
|
||||
// 同一個理由——library 分得開「同一台機器的兩個資料夾」,machine 分得開
|
||||
// 「兩台機器的同一個相對路徑」。少送這一維,雲端就只能把兩台的同名檔
|
||||
// 當成同一份(先到的被後到的蓋掉,而且是無聲的)。
|
||||
mach := cfg.machineIdentity()
|
||||
cardBody := map[string]any{
|
||||
"page_name": pageNameOf(ev.Path),
|
||||
"path": ev.Path,
|
||||
"card_content": string(cardData),
|
||||
"library": cfg.libraryFor(absRoot),
|
||||
"page_name": pageNameOf(ev.Path),
|
||||
"path": ev.Path,
|
||||
"card_content": string(cardData),
|
||||
"library": cfg.libraryFor(absRoot),
|
||||
"machine": mach.ID,
|
||||
"machine_label": mach.Label,
|
||||
}
|
||||
if warns := lr.SoftMessages(); len(warns) > 0 {
|
||||
cardBody["quality"] = "low"
|
||||
@@ -1499,11 +1537,18 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool, qs *quotaSta
|
||||
exit = 1
|
||||
continue
|
||||
}
|
||||
// 舊的「原文直送雲端萃取」路(rag_ingest_direct)。它與收卡路送同一組欄位,
|
||||
// 免得日後有人比對兩條路時看到「一條有 machine 一條沒有」而以為是 bug。
|
||||
// ⚠️ 雲端這支 workflow 本輪**沒有跟著改**(youlin stage 上根本沒部署它,
|
||||
// 現役是 rag_ingest_card)——它會忽略這兩個欄位,行為與從前一字不差。
|
||||
machDirect := cfg.machineIdentity()
|
||||
status, _, perr := cfg.postJSON(cfg.triggerURL(cfg.IngestWF), map[string]any{
|
||||
"page_name": pageNameOf(ev.Path),
|
||||
"path": ev.Path,
|
||||
"content": string(content),
|
||||
"library": cfg.libraryFor(absRoot),
|
||||
"page_name": pageNameOf(ev.Path),
|
||||
"path": ev.Path,
|
||||
"content": string(content),
|
||||
"library": cfg.libraryFor(absRoot),
|
||||
"machine": machDirect.ID,
|
||||
"machine_label": machDirect.Label,
|
||||
})
|
||||
res.HTTPStatus = status
|
||||
if perr != nil {
|
||||
@@ -1528,9 +1573,17 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool, qs *quotaSta
|
||||
// 下架=POST {page_name, path} 進 rag_takedown_direct(按 page_name 讀 kbdb blocks
|
||||
// 標 deprecated,不碰 R2;獨立於 rag_ingest 的 __CARDS_PREFIX__ 閘——direct 模式檔在
|
||||
// 資料夾根,會被 rag_ingest 的前綴閘擋掉,故自帶不含前綴閘的下架 workflow)。
|
||||
// machine(`inkstone/mira#6`):這條分支歷來只送 {page_name, path}
|
||||
// (library 是 arcrun-rag#46 只補在 drainPendingTakedowns 那條路上的)。
|
||||
// 這裡只補 machine、**不順手補 library**:machine 已足以擋住「A 機器刪檔
|
||||
// 連坐殺掉 B 機器同名檔」,而多補一維會改變既有的撤除命中範圍——
|
||||
// 那是另一件事,要另外驗(本輪不驗的不做)。
|
||||
machRm := cfg.machineIdentity()
|
||||
status, _, perr := cfg.postJSON(cfg.triggerURL(cfg.RemovedWF), map[string]any{
|
||||
"page_name": pageNameOf(ev.Path),
|
||||
"path": ev.Path,
|
||||
"page_name": pageNameOf(ev.Path),
|
||||
"path": ev.Path,
|
||||
"machine": machRm.ID,
|
||||
"machine_label": machRm.Label,
|
||||
})
|
||||
res.HTTPStatus = status
|
||||
if perr != nil {
|
||||
|
||||
Reference in New Issue
Block a user