969c3f2bcf
2026-08-18 出貨線第一次真的把 arcrun-rag 的 collector/ 同步過來,
盲目鏡射蓋掉/刪掉了三個**本來就該只屬於這個 repo** 的檔:
.gitignore 21 行的「不要再長回來」閘 → 被換成 arcrun-rag 那份的一行
🔴 最嚴重:那份清單正是擋 43 MB 建置產物的東西,
它被蓋掉之後,下一趟同步就會把產物搬進來
README.md 介紹這個獨立 repo 的 → 被換成 arcrun-rag 子目錄的 README
cmd/collector/main.go 正常源碼,被誤判成建置產物刪掉
(.gitignore 的 `collector` 一行是指根目錄那顆執行檔,
但無斜線樣式在任何層級都命中 ⇒ 連 cmd/collector/ 也中)
出貨線那側已修(installer/scripts/line-source-repo.mjs):
· destOwned:目的 repo 的門面檔不覆蓋、不刪除,且同步前檢查它們還在不在
· 已被目的 repo 追蹤的檔案不算「被忽略」(git 自己的語意)⇒ main.go 不再被誤刪
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
154 lines
11 KiB
Markdown
154 lines
11 KiB
Markdown
# arcrun-collector — 桌面小幫手(Arcrun RAG Desktop)
|
||
|
||
> **這個 repo 是「桌面小幫手」的家:原始碼、版本說明、每一版的成品,全部在這裡。**
|
||
>
|
||
> - **要下載桌面小幫手** → 右邊的「版本發布 / Releases」,最新一版就是最上面那筆。
|
||
> 每一筆都掛著當版的三個成品:macOS 的 `.dmg`、Windows 的 `.exe` 與 `.msix`。
|
||
> - **這一版改了什麼** → 同一筆 release 的內文,原稿是 [`CHANGELOG.md`](CHANGELOG.md)。
|
||
> - **版本號長什麼樣** → `0.18.x`([`DAEMON_LINE`](DAEMON_LINE) 宣告的那條線)。
|
||
> 雲端引擎是 `1.4.x`,**是另一個產品、另一個 repo**(見下)。
|
||
>
|
||
> ## 跟 arcrun-rag 的關係
|
||
>
|
||
> [`inkstone/arcrun-rag`](https://git.uncle6.me/inkstone/arcrun-rag) 是**雲端引擎**(`1.4.x`)——
|
||
> 跑在使用者自己 Cloudflare 帳號上的那些 worker。桌面小幫手會連上它,但兩者是兩個產品、
|
||
> 兩條版本線、兩份版本說明。
|
||
>
|
||
> 🔴 **為什麼分開**(D95,leo 2026-08-18):在此之前兩個產品的版本發布疊在 arcrun-rag
|
||
> 同一條歷史上(`0.18.33`、`0.18.30` 跟 `1.4.49`、`1.4.48` 並排),而且出貨線每跑一次就多疊一筆。
|
||
> leo 原話:「**我強調了不要扭曲,這就是扭曲,把一個差很多的東西塞進去別人的歷史裡。**」
|
||
> 兩個產品共用一條版本歷史,「最新版是哪一個」就沒有答案。
|
||
>
|
||
> 📌 過渡狀態(誠實記著):原始碼目前仍同時存在於 `arcrun-rag` 的 `collector/`,
|
||
> 由出貨線每次出貨同步過來。**把 `collector/` 從 arcrun-rag 拆掉**要動 20 幾支讀那個路徑的
|
||
> 腳本,另立一輪處理(票:`inkstone/InkStoneCo#40`)。
|
||
> 在那之前,**發佈的落點以本 repo 為準**。
|
||
|
||
---
|
||
|
||
> ✂️ **legacy Node 版(Gitea push 鏈)已於 2026-07-19 刪除**(SDD `ingest-hash-trigger` task 4:
|
||
> 「Gitea webhook 接收端程式碼與路由已刪除(git 記錄可查)」)。`index.js`/`transform.js`/
|
||
> `git-sync.js`/`config.js` 那套 watch → git commit/push → Gitea webhook 鏈要考古請看 git 歷史
|
||
> (本檔同 commit 之前的版本有完整說明)。現役=Go 版 hash 偵測 collector,
|
||
> **不經 git、不經 Gitea**:scan → R2 上傳 → 直打 arcrun named-webhook。
|
||
> Markitdown 轉檔(docx/pptx/pdf→md)能力隨 legacy 一併退場,回歸產品化段 task 11 重做進 Go 版。
|
||
|
||
## Go 版:hash 偵測 collector(SDD ingest-hash-trigger)
|
||
|
||
```
|
||
collector scan --root <知識資料夾> --manifest <manifest.json> [--max-removed-ratio 0.4] [--dry-run]
|
||
collector upload --root <知識資料夾> --manifest <manifest.json> [--max-removed-ratio 0.4] [--dry-run]
|
||
collector sync --root <知識資料夾> --manifest <manifest.json> [--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`,
|
||
由 `sync` 在觸發回 2xx 後呼叫);掃描與 R2 上傳都不寫它,所以「偵測過但未成功 ingest」的檔
|
||
每輪都會重發 added/modified——這是設計(design §2),不是 bug;R2 端靠存在檢查 no-op,
|
||
不會重複上傳。
|
||
- daemon 常駐(launchd)=產品化段 task 11。
|
||
|
||
### `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/<sha256hex>`**(不含 `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": <collector-trigger payload>, "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 相符,全通。
|
||
|
||
|
||
### `sync`:一條龍(SDD task 4)= scan → upload → 觸發 ingest → 成功回寫
|
||
|
||
`sync`=`upload` 再加最後一步:把整輪的 collector-trigger.v1 payload POST 到
|
||
`ARCRUN_TRIGGER_URL`(arcrun named-webhook 完整 URL,如
|
||
`{cypher}/webhooks/named/{ns}/rag_ingest/trigger`——端點是 arcrun 原生觸發機制,
|
||
design 鐵律段明言保留;被刪掉的是「Gitea push 事件」這個來源語意)。
|
||
|
||
- **回寫語意**:HTTP 2xx 才對本輪送出的 added/modified/renamed 檔呼叫 `Manifest.MarkIngested`;
|
||
非 2xx/網路錯不回寫=下輪自然重試(exit 1)。
|
||
- **上傳失敗的事件不送**:schema 約定 added/modified 的 `r2_key`=原稿已在 R2,上傳失敗還送
|
||
=叫消費端去 404 → 該事件本輪擋下(`dispatch.dropped_paths`),下輪重試補送;同路徑的
|
||
renamed 也不回寫(防「內容從未上 R2 卻被標 ingested」)。
|
||
- **防呆警告輪照送**:mass_delete_guard 觸發時 removed 事件已被壓下,但 payload 連同
|
||
`warnings[]` 照送——消費端看得到警告、不執行下架;notify 呈現歸 collector 端輸出/daemon。
|
||
- **無變更輪不發送**(`skipped_no_changes`)。
|
||
- 輸出 JSON:`{"trigger": <實際送出的 payload>, "uploads": [...], "dispatch":
|
||
{status, http_status?, error?, marked_count, dropped_paths?}}`。
|
||
|
||
設定=upload 的三個環境變數+`ARCRUN_TRIGGER_URL`(皆絕不落 repo/code)。
|
||
|
||
測試:`go test ./...` 19/19——掃描 7+上傳 7+sync/trigger 5(成功回寫+無變更輪不重發/
|
||
失敗不回寫+修復後重試成功/防呆警告輪照送 warnings 零事件/上傳失敗事件擋下+renamed
|
||
連坐不回寫/URL env 驗證),全部 httptest mock(**未實跑雲端 e2e**——等切換日與 leo 一起驗)。
|
||
|
||
## `direct`:daemon 直送萃取、無 R2 同步模式(SDD task 11)
|
||
|
||
`direct`=**產品承諾核心的最短路徑**:監看資料夾 → 偵測新增/改動檔(沿用 `Scan` 的 hash 差異
|
||
偵測,與 `sync` 同一套)→ 讀檔內容 **inline POST** 進實例的 `rag_ingest_direct` workflow
|
||
(LLM 萃卡 → 機械切塊 → 寫 kbdb,**全在 Arcrun workflow**)。刪檔 → POST `{page_name, path}`
|
||
進 `rag_takedown_direct`(按 page_name 標 kbdb blocks/triplets `deprecated`)。
|
||
|
||
**繞開 R2/Gitea**:既有 `sync` 走 collector→R2→`rag_ingest`(要 R2 bucket=綁卡),`rag_extract_one`
|
||
又要 Gitea repo 落卡。`direct` 兩者都不要——落地「丟檔進資料夾 → AI 查得到」的零綁卡版。
|
||
|
||
```
|
||
collector direct --config <config.json> [--once] [--dry-run]
|
||
```
|
||
|
||
- `--once`:掃一輪即退出(測試/cron);預設常駐輪詢(`poll_interval_sec`,純 stdlib ticker,跨平台)。
|
||
- 設定檔(JSON,見 `install/direct-config.sample.json`):`watch_folder` / `manifest` /
|
||
`cypher_url` / `namespace` / `api_key`(空=namespace)/ `library`(空=kb)/
|
||
`poll_interval_sec`(空=5)/ `max_removed_ratio`(空=0.4)/ `ingest_workflow`
|
||
(空=`rag_ingest_direct`)/ `removed_workflow`(空=`rag_takedown_direct`)。
|
||
- **dogfooding(D29 daemon 薄殼豁免)**:本模式只「監看/讀檔/算 hash/HTTP POST」原生 Go——
|
||
萃取/切塊/RAG 一律在實例 workflow,daemon 內零 LLM/切塊邏輯。
|
||
- **回寫語意**:POST 回 2xx 才 `Manifest.MarkIngested`(下輪不重送);非 2xx 不回寫=下輪重試。
|
||
- **大量刪除防呆**:沿用 `Scan` 的 `mass_delete_guard`(removed > manifest×`max_removed_ratio`
|
||
→ 本輪不下架、只回報警告)。
|
||
|
||
**配套 workflow**(`workflows/rag-ingest-direct.local.yaml` / `rag-takedown-direct.local.yaml`;
|
||
用 `install/push-demo-workflow.sh` 推,env 指向目標實例的 CYPHER/KBDB/HTTPREQ/CODE/LLM_MODEL/LIBRARY):
|
||
blocks/triplets 寫法與 `rag_ingest` 逐欄一致 → `rag_chat` 一視同仁檢索得到。
|
||
> ⚠️ 下架另立 `rag_takedown_direct` 而非重用 `rag_ingest` removed 分支:後者在 `collect_changed`
|
||
> 有 `__CARDS_PREFIX__` 閘,direct 模式的檔在資料夾根會被擋掉零下架(2026-07-20 live e2e 實撞)。
|
||
|
||
**youlin live e2e 實證(2026-07-20,非 mock)**:
|
||
- 丟 `請假規則.md`/`差旅政策.md` → daemon `ingested`(200) → `rag_chat` 帶出處正確作答。
|
||
- 刪 `差旅政策.md` → daemon `removed`(200) → `rag_chat`「知識庫裡沒有這方面資料」(blocks 全 deprecated)。
|
||
|
||
**跨平台**:純 stdlib、零 CGo、輪詢式偵測(不依賴 fsnotify)→ `GOOS=darwin GOARCH=arm64` /
|
||
`GOOS=windows GOARCH=amd64` 直接交叉編譯(各約 5.6M / 5.9M)。托盤殼(Wails)+Mac `.app`
|
||
簽章/TCC 是下一刀,且 Mac 打包要在 Mac 上做。
|