# collector > ⚠️ **本目錄有兩代 collector 並存(2026-07-19)**: > > - **新(現役開發線)= Go 版 hash 偵測 collector**(`*.go`,SDD `ingest-hash-trigger` task 2)—— > 變更偵測回歸檔案系統,manifest+content hash,**不經 git、不經 Gitea**。見下方「Go 版」段。 > - **舊(legacy,待 SDD task 4 拆除)= Node 版 Gitea push 鏈**(`index.js`/`transform.js`/`git-sync.js`/`config.js`, > rag-wave1 T3/T4 產物)——watch → git commit/push → Gitea webhook。這正是 de-Gitea 要取代的鏈, > 保留只為現行本機 demo 鏈還在用;新功能一律做在 Go 版。 ## Go 版:hash 偵測 collector(SDD ingest-hash-trigger) ``` collector scan --root <知識資料夾> --manifest [--max-removed-ratio 0.4] [--dry-run] collector upload --root <知識資料夾> --manifest [--max-removed-ratio 0.4] [--dry-run] ``` 一次掃描:走訪資料夾(先只認 .md/.markdown/.txt/.docx/.pptx/.pdf)→ mtime+size fast-path (沒變→沿用 manifest hash;變了才算 sha256)→ 對照 manifest 產出事件 → 事件 JSON (符合 `schemas/collector-trigger.v1.schema.json`)輸出 stdout → 更新 manifest(`--dry-run` 不寫)。 - **事件分類順序(design §3)**:先把本輪 removed×added 以 content_hash 配對成 `renamed` (只更新路徑映射,不 retire、不重萃);再分 added / modified / removed。 - **大量刪除防呆(R6)**:removed 數 > manifest 條目 × 40%(`--max-removed-ratio` 可調)→ removed 全部不執行、manifest 條目保留、輸出 `mass_delete_guard` 警告。 - **重試語意**:`ingested_hash` 只會在整條 ingest 鏈成功後回寫(回寫鉤子=`Manifest.MarkIngested`, 由 task 4 觸發鏈呼叫);掃描與 R2 上傳都不寫它,所以「偵測過但未成功 ingest」的檔每輪都會 重發 added/modified——這是設計(design §2),不是 bug;R2 端靠存在檢查 no-op,不會重複上傳。 - 打 arcrun named-webhook 觸發 ingest、daemon 常駐(launchd)=之後的 task (SDD task 4、journeys/user-onboarding 環 6)。 ### `upload`:R2 content-addressed 原稿上傳(SDD task 3,design §4) `upload`=`scan`+把本輪 **added/modified** 的原稿上傳 R2(renamed/removed 內容未變/已留底,不上傳)。 - **走 Cloudflare REST API**(`PUT /accounts/{account_id}/r2/buckets/{bucket}/objects/{key}`, Bearer token)——不用 S3 sigv4,token 模型跟產品其他部分一致(客戶本來就有 CF API token)。 - **key=`raw/`**(不含 `sha256:` 前綴,對齊 `schemas/collector-trigger.v1.schema.json` 的 `r2_key`)。 - **冪等**:每 key 先做存在檢查,已存在=`skipped_exists` 不重傳(content-addressed 天然去重)。 ⚠️ CF REST API 的 objects 端點**不支援 HEAD**(2026-07-19 live 實測回 405), 存在檢查走 `GET`+`Range: bytes=0-0`(存在=200/206 只讀 1 byte,404=不存在)。 - **完整性**:上傳前重算 sha256 核對事件 hash;檔案在掃描後被改動=該筆 `failed` 不上傳 (不能把新內容塞進舊 hash 的 key),下輪重掃自然帶新 hash。 - **失敗語意**:任一筆 `failed` → exit code 1;manifest 照存(content_hash 反映現況、 `ingested_hash` 不動)=下輪自動重試。上傳成功也**不**標 ingested——上傳只是鏈的第一環。 - `--dry-run`:不碰網路、不寫 manifest,只列 `planned` 上傳清單。 - 輸出 JSON:`{"trigger": , "uploads": [{path, r2_key, status, error?}]}`, status=`uploaded`/`skipped_exists`/`failed`/`planned`。 設定(**只走環境變數,絕不落 repo/code**): | 變數 | 說明 | |---|---| | `CF_ACCOUNT_ID` | Cloudflare 帳號 ID | | `CF_API_TOKEN` | 有該 bucket R2 read+write 權的 API token | | `R2_BUCKET` | 目的 bucket 名(demo=`arcrun-rag-raw-demo`) | | `CF_API_BASE` | 選填,API 基底覆蓋(測試用;預設 `https://api.cloudflare.com/client/v4`) | 測試:`go test ./...`——掃描七項(五情境+fast-path+manifest 往返)+上傳七項 (新檔上傳/同 hash 重傳 no-op/上傳失敗不標 ingested+重試/非內容事件不上傳/ hash 不符不上傳/env 缺漏報錯/MarkIngested 鉤子),httptest mock 對齊真 API 行為(HEAD 405)。 live e2e(2026-07-19):uncle6 帳號 `arcrun-rag-raw-demo` bucket 真上傳→重傳 no-op→ `wrangler r2 object get --remote` 下載 diff 一致、sha256 與 key 相符,全通。 --- # (legacy)Node 版收集端骨架(rag-wave1 T3+T4) > design.md §4;跑在**客戶端機器**(NAS/VM,或導入者代管的 VPS),不是 arcrun workflow(arcrun 零件禁檔案系統,見 CLAUDE.md 紅線)。 > ⚠️ **本骨架端到端(真實 NAS/VM + 真實 Gitea remote + systemd 常駐)需在本機/客戶環境驗,雲端 sandbox 沒有這些條件**——這裡只做得到:程式邏輯本身可跑、對本機臨時目錄的煙測(見下方「已驗證」)。 ## 做什麼 ``` watch 知識資料夾 ──(新增/修改)──► transform(.md passthrough/docx,pptx,pdf→Markitdown)──► 寫入 target repo 工作目錄 ──(刪除) ──► target repo 移除對應檔 │ git add/commit/push │ Gitea push webhook(既有機制,非本骨架自建) │ arcrun ingest workflow(另案,如 km_wiki_ingest_drain 同款模式) ``` **關鍵設計判斷**:collector 的責任止於「commit + push」。design.md §4 說的「打 ingest webhook」= Gitea 自己的 push webhook 機制(同 `Leo/Arcrun` `registry/examples/km-wiki-ingest` 已驗證的模式:Gitea push webhook → arcrun workflow),**不是** collector 自己再打一支 HTTP webhook。collector 不需要知道 ingest 的內部細節(哪個 workflow、哪個 KBDB),它只管「客戶檔案的忠實鏡像」進 Gitea repo。 ## 事件驅動、不輪詢 用 `chokidar`(Node)watch 檔案系統事件(inotify/FSEvents,非 polling),對齊「單一 repo、事件驅動」的鐵律。debounce 視窗(預設 3 秒)把同一批變更合併成一次 commit,避免每個檔案獨立 commit 洗歷史。 ## 刪檔語意 檔案在來源資料夾被刪除 → collector 在 target repo 對應路徑也 `git rm` → commit → push。**deprecated 標記不是 collector 的責任**——ingest workflow 收到 Gitea push event 裡的 `removed` 檔案清單後,自己去 KBDB 把對應 entry 標 `status: deprecated`(append-only,不物理刪,見 km-wiki-ingest description.md 冪等設計)。collector 只管檔案鏡像忠實,不碰 KBDB。 ## 轉檔(T4) `transform.js`:`.md`/`.markdown` passthrough(frontmatter 補 `source_path` 溯源欄位);`.docx`/`.pptx`/`.pdf` 呼叫 `markitdown` CLI(子行程,需部署機器已 `pip install markitdown[docx,pptx,pdf]`)轉出 md,輸出路徑副檔名換成 `.md`,同時把**原檔**複製進 target repo 的 `assets/originals/<相對路徑>`(design.md §4「原檔進 LFS」)。`git-sync.js` 的 `ensureGitAttributes()` 在啟動時冪等寫入 `.gitattributes`(`assets/originals/**/*.{pdf,docx,pptx} filter=lfs ...`)。 ⚠️ **LFS 是否真的生效待本機驗**:`.gitattributes` 宣告本身雲端驗過內容正確,但 LFS smudge/clean filter 要部署機器裝了 `git-lfs` 並 `git lfs install` 才真的把大檔案存進 LFS store(否則 git 仍會把二進位檔案當一般 blob 存進版控歷史——功能上檔案還是會進 repo,只是沒享受到 LFS 的空間/頻寬優化)。雲端 sandbox 沒有 `git-lfs` 二進位,這段驗不到。 其餘格式(xlsx、圖片等)不在 design.md §4 第一波承諾範圍,仍丟 `NotImplementedError` 並記警告日誌,誠實不假裝轉好。 ## 檔案 | 檔案 | 職責 | |---|---| | `index.js` | 進入點:watch+debounce+事件分派+原檔複製 | | `transform.js` | 檔案 → md 轉換:`.md` passthrough/`docx,pptx,pdf` 走 Markitdown | | `git-sync.js` | target repo 的 git add/commit/push 封裝+`.gitattributes` LFS 宣告 | | `config.js` | 環境變數讀取(`WATCH_DIR`/`TARGET_REPO_DIR`/`DEBOUNCE_MS`) | ## 設定(環境變數) | 變數 | 說明 | 預設 | |---|---|---| | `WATCH_DIR` | 客戶知識資料夾(來源) | 必填 | | `TARGET_REPO_DIR` | 已 clone 好、有 push 權限的 Gitea repo 工作目錄(去向) | 必填 | | `TARGET_SUBDIR` | 在 target repo 內落地的子目錄 | `collected/` | | `DEBOUNCE_MS` | 合併變更的等待視窗 | `3000` | | `GIT_AUTHOR_NAME` / `GIT_AUTHOR_EMAIL` | commit 署名 | `collector` / `collector@localhost` | ## 部署(客戶端機器,設計稿——本輪未實際跑 systemd) ```ini # /etc/systemd/system/arcrun-rag-collector.service(範本,未部署未測) [Unit] Description=arcrun-rag collector After=network.target [Service] Environment=WATCH_DIR=/mnt/knowledge Environment=TARGET_REPO_DIR=/opt/collector-repo ExecStart=/usr/bin/node /opt/arcrun-rag/collector/index.js Restart=always User=collector [Install] WantedBy=multi-user.target ``` `TARGET_REPO_DIR` 需事先 `git clone`+設好有 push 權限的 remote(credential 用該機器的 git credential helper 或 SSH key,不是本骨架管的事)。 ## 已驗證(雲端 sandbox 能做到的部分) - `node --check` 語法檢查全過。 - 本機臨時目錄煙測(非真實客戶環境,兩輪):起一個 scratch git repo 當 target、一個 scratch 資料夾當 watch 來源: 1. **T3 事件機制**:新增/修改/刪除 `.md` 檔案 → collector 正確偵測、寫入/移除、debounce 後產生一次 commit,`git log` 驗到 commit 內容與變更一致。 2. **T4 Markitdown**:用 `python-docx` 生一份真實 `.docx`(含標題+段落)丟進 watch 資料夾 → collector 呼叫 `markitdown` 轉出真實 md 內容(人工核對文字與原檔一致)、原檔複製進 `assets/originals/`、`.gitattributes` 正確寫入三種格式的 LFS 宣告、整批 commit+push 送達 bare remote(`git log` 驗證)。 **不含**(待本機/客戶環境):真實 Gitea remote push(需真實 token+repo)、真實 git-lfs smudge/clean filter(sandbox 無 `git-lfs` 二進位)、systemd 常駐、NAS/VM 環境、Gitea push webhook 是否真觸發 ingest workflow。 ## 待本機/客戶環境驗(端到端) 1. 真實客戶知識資料夾 watch(含各類真實 docx/pptx/pdf 樣本,非合成測試檔)。 2. 真實 Gitea remote push(含憑證管理)。 3. 真實 git-lfs 安裝+`git lfs track`,確認大檔案真的走 LFS store 而非塞進一般 blob 歷史。 4. push 後確認 Gitea push webhook 真觸發 ingest workflow(另案)。 5. systemd 常駐穩定性(重開機自動起、崩潰自動重啟)。