Skip to content

docs(kb): 知識庫封存包 — 批次建庫的格式、API 與一支會跑的 live check - #706

Open
HYChou0515 wants to merge 4 commits into
masterfrom
worktree-collection-archive-howto
Open

docs(kb): 知識庫封存包 — 批次建庫的格式、API 與一支會跑的 live check#706
HYChou0515 wants to merge 4 commits into
masterfrom
worktree-collection-archive-howto

Conversation

@HYChou0515

@HYChou0515 HYChou0515 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

#701 / #702:平台障礙修完後,缺的是「我手上有一批整理好的知識,要怎麼進系統」的答案。格式一直都在(就是匯出產生的那個 zip),只是沒有人把它寫下來,所以每次都要從路由簽名反推。

格式(這就是全部,可以直接照抄)

--archive-only 產出的範例包解開長這樣:

archive.zip
├── .kb-collection/manifest.json     ← collection 設定 + context cards
├── M4/description.md                ← 一份文件
├── M4/raw-01.png                    ← 又一份
└── M4/annotated-01.png              ← 再一份

除了 manifest,zip 裡每個檔案都會變成一份文件,存放在它在 zip 裡的路徑上。

.kb-collection/manifest.json:

{
  "version": 1,
  "collection": { "name": "缺陷庫", "use_rag": true },
  "context_cards": [
    {
      "keys": ["M4"],
      "title": "M4 — 邊緣崩角",
      "body": "一到兩句的權威定義",
      "reference_paths": ["M4/description.md", "M4/annotated-01.png"]
    }
  ]
}

三件事值得單獨講:

  • 卡片用「路徑」指文件,不是 id。 文件 id 內含所屬 collection,搬 id 會讓連結在匯入到別的 collection 時當場失效;匯入會依目標重新鑄造。
  • reference_paths 是三態的:欄位整個省略 = 保留卡片現有連結、給列表 = 取代、[] = 清空。這是「重匯一次修錯字」不會清掉匯出後才策展上去的連結的原因。
  • manifest 可以完全省略 —— 那樣 zip 就退化成一次資料夾批次上傳。

自己產一份來看:

uv run python scripts/check_collection_archive.py --keep /tmp/sample.zip --archive-only

附件是逐條列出來的,不是系統猜的

reference_paths 裡寫什麼,卡片就掛什麼。沒有命名慣例、不看 title、不看資料夾名。 上面例子把檔案放在 M4/ 底下純粹是好整理;沒被列進去的檔案(例如 raw-01.png)仍是一份正常文件,只是不掛在那張卡上。

⚠️ 路徑打錯不會報錯。 匯入只把路徑編碼成 id,不驗證文件存在,所以一個 typo 會讓連結靜默失效。產生 zip 時讓同一個變數同時決定「檔案放哪」和「reference_paths 寫什麼」,兩邊就不可能對不上。

匯入不會觸發 AI 產生卡片

manifest 裡的卡片是資料,跟文件一樣照收 —— 和「卡片生成」(AI 讀文件草擬提案)是兩件事:

匯入 卡片生成
卡片哪來的 你寫在 manifest 裡 AI 產生的提案
怎麼觸發 上傳 zip Auto-generate,或打開 auto_digest
結果落在哪 直接進 Glossary 先進 Review 等人核准

auto_digest 預設 False,匯入不會設定它,匯出也不帶它。實測一個匯入進來的 collection:auto_digest = False、卡片數 1(只有 manifest 那張)、零 AI 提案。

怎麼送進去(一個 zip、一個請求)

所有後端路由都掛在 /api 底下(根路徑是前端 SPA)。文件、卡片、連結一次全進,不需要逐份上傳再逐張建卡。

BASE=http://<你的伺服器>/api

# 第一次:建成一個新的 collection
curl -X POST "$BASE/kb/collections/import" -F "file=@archive.zip"
# → {"collection_id": "collection:…", "status": "indexing"}

# 之後:併進同一個 collection(把上面回傳的 id 填進 $CID)
curl -X POST "$BASE/kb/collections/$CID/import?mode=overwrite" -F "file=@archive.zip"

mode 只在併進既有 collection 時有意義,而且同時管文件和卡片:

overwrite skip
文件(依路徑判定撞名) 換成新的 保留原有的
卡片(依代號判定撞名) 更新原本那張 原本那張不動

重跑同一包不會長出重複卡片 —— 資料調一調就重匯,不必先清乾淨。(實測:同一包連匯兩次,卡片仍是 2 張、文件仍是 5 份。)

不想寫程式的話,網頁上也有:知識庫列表頁的「匯入」建新的,進到某個知識庫裡的「匯入」則會問你覆蓋或跳過。

進去之後怎麼查

# 查代號:exact key,毫秒級,不經過模型;匯入後立即可用,不必等索引
curl -X POST "$BASE/kb/collections/$CID/context-cards/lookup" \
     -H 'content-type: application/json' -d '{"terms": ["M4", "M7"]}'

丟圖問「這是哪一種」要先建一個綁定該 collection 的 chat,再把圖 base64 夾在訊息裡送出(回傳是 SSE 串流);完整指令在 docs/collection-archive.md這條需要部署有配 kb.vlm_llm,沒有的話會直接回 400。

docs/collection-archive.md

一頁講完:

  • 格式 —— 除了 manifest 之外,zip 裡每個檔案都是一份文件;卡片用路徑指文件(id 內含 collection,搬 id 會當場失效)
  • reference_paths 的三態 —— 欄位省略=保留、給列表=取代、[]=清空。這條是「重匯一次修錯字」不會把匯出後才策展上去的連結清掉的原因
  • mode 對文件和卡片各是什麼意思(文件依路徑、卡片依代號)
  • 兩種查法 —— exact-key 查代號(不經模型、毫秒級、匯入後立即可用),以及夾一張用完即丟的圖提問
  • 會踩到的前提 —— 匯入非同步、圖片需要 VLM、圖搜圖預設關閉,以及 .docx/.xlsx 內嵌圖會靜默丟失而文件仍顯示完成

scripts/check_collection_archive.py

跑起來的服務走同一條路並斷言結果,加入既有的 check_* 家族。文件因此有了「一旦不再為真就會失敗」的東西。

1. imported     collection:21007ca7-…
2. documents    3 — {'error': 2, 'ready': 1}
                images not indexed: [...] — needs a reachable VLM (kb.vlm_llm)
3. lookup       M4 → 1 card(s)
4. card links   2 document(s)
5. re-import    1 card(s) after a second import

第 5 步是 #701 的冪等修正第一次在真服務上被驗證,不只是在測試裡。

跑它才發現、進而修掉文件裡兩個錯誤:

  • 所有後端路由都在 /api 底下 —— 讀路由裝飾器看不出掛載前綴,我第一版的範例全少了它
  • 圖片文件只在 VLM 可連時才索引得起來 —— 腳本會報告但不視為失敗,只有 --ask(呼叫者主張這台有 VLM)才致命。封存包的來回和「讀得懂一張圖」是兩種能力,不該綁在一起

後續修正

--keep 原本寫成「順便留下範例 zip」,但它會接著去連服務,所以「我只想看看格式長怎樣」這個最先會做的動作,反而被「要先把整套跑起來」擋住。加了 --archive-only:寫完就停,不需要任何服務。

這是被人問「範例 zip 在哪」問出來的 —— 答案是他得先有一台服務才生得出來。

附帶(獨立 commit)

PACKAGES 宣告了 rca-tools,而它自己的註解寫著「源碼目錄是 gitignore 的、prebuild 會跳過、所以 fresh clone 沒有它也能乾淨建置」。prebuild 確實用 source.is_dir() 跳過了,但啟動檢查沒有同一個條件,直接拒絕開機,還叫你去重跑那支已經決定跳過它的 prebuild —— 同一個問題兩個地方用不同規則判斷,而且只有一邊符合宣告自己的承諾。

啟動檢查改用 prebuild 的同一個條件。守門沒有變鬆:源碼卻沒建,仍是硬錯誤。

__main__.py 在 coverage 的 omit 清單裡(composition glue),所以這條沒有測試——我不假裝有。驗證方式是實際跑:修之前乾淨 worktree 起不來,修之後起得來。

驗證

mkdocs build --strict 通過(CI 用的同一道指令);tests/tooling + KB 封存包相關 365 passed;ruff check / ruff format --check / ty check 全過。

🤖 Generated with Claude Code

https://claude.ai/code/session_01FSofnZxfrcuvYbDDcP4axC

HYChou0515 and others added 4 commits August 11, 2026 08:46
`PACKAGES` declares `rca-tools`, and its own comment says the source dir is
gitignored, that the prebuild skips packages whose source is missing, and that
"a fresh clone without this directory still builds cleanly". The prebuild does
skip it — `source.is_dir()` — but startup demanded every declared name and
refused to boot, telling you to re-run the prebuild that had already decided to
skip it. Two places judging the same question by different rules, and only one
of them matching what the declaration promises.

The startup check now asks the same question the prebuild asks. The guard still
bites where it earns its keep: a package whose source IS present but was never
built is still a hard error.

Not covered by a test — `__main__.py` is composition glue and is in coverage's
omit list. Verified by running it: `python -m workspace_app` failed on a clean
worktree before this and starts after it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSofnZxfrcuvYbDDcP4axC
…k that runs it

There was no written answer to "I have a knowledge base in another system — how
do I get it in?", so the answer was being reconstructed from route signatures
each time. The archive format already existed; what was missing was saying so.

`docs/collection-archive.md` states the format, the tri-state on
`reference_paths` (absent KEEPS, a list replaces, `[]` clears — the rule that
stops a re-import from erasing links curated since the export), what `mode`
means for cards as against documents, and the two ways to query what lands: an
exact-key lookup with no model in the loop, and a question with a throwaway
image attached.

`scripts/check_collection_archive.py` walks the same flow against a running
server and asserts on it, so the document has something that fails when it
stops being true. It joins the `check_*` family. Running it is also how two
things in this document were corrected before shipping: every backend route
lives under `/api`, which reading the route decorators does not tell you, and
an image document only indexes when a VLM is reachable — reported by the script
but fatal only under `--ask`, since the archive round-trip and the ability to
read a picture are separate capabilities.

Also written down: `.docx` / `.xlsx` drop their embedded images silently, with
the document still reporting success. Anyone bulk-loading a picture-heavy
knowledge base hits that first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSofnZxfrcuvYbDDcP4axC
…server

`--keep` was documented as "also leave a sample zip behind", but the run then
went on to contact a server and exited non-zero when there wasn't one — so the
one thing you reach for FIRST, to see what the format even looks like, was
gated behind having the whole stack up. `--archive-only` writes it and stops.

Found by someone asking where the sample archive was: the answer was that they
would have needed a server to produce it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSofnZxfrcuvYbDDcP4axC
…port never generates cards

Two questions a reader hit immediately, neither answered by the page as written.

**"How does it decide which attachments belong to a card — by title?"** It does
not decide anything: `reference_paths` is a list you write. Nothing reads the
title or the folder name, and a file left off the list is still a document, just
not that card's exhibit. The tidy `M4/` folder in the example invites the
opposite assumption, so the page now says so outright. It also warns that a
mistyped path fails SILENTLY — import encodes the path into an id without
checking a document is there — and shows the one habit that prevents it: derive
the file's location and its `reference_paths` entry from the same variable
rather than spelling the path twice.

**"There's a card, but I never asked for card generation — does it always
generate?"** No. A manifest card is data, filed like a document. Card generation
is a different feature behind its own button, `auto_digest` defaults to False,
import never sets it, and export does not carry it. The page now contrasts the
two side by side, including where each result lands (Glossary vs Review).

Both were explained once in conversation and would have been lost there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSofnZxfrcuvYbDDcP4axC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant