- name
- elf-mcp-book
- description
- Elf Express 書籍素材(mcpjungle/books/<書名>/source/)與「挑一份上架成 docs-mcp 語料」的規範。
當任務涉及 mcpjungle/books/<書名>/source/ 下的檔案、新增或整理一本書的素材、把某本書的某個語言上架成語料、
寫或修改某本書的 import.ts/transform.ts、
書籍圖片(一般 git、-text -diff)、.gitattributes 的素材規則、語料的 sources.json、168小隊譯文審查,
或排查「書上架後查不到 / CI 擋大檔」時觸發。
- metadata
- {"version":"1.1.0","owner":"Elf Express"}
# 書籍素材與語料上架(Elf Express)
> 參考實作:`elf-express/mcp-library` 的 `mcpjungle/books/<書名>/source/`(素材區,commit `f05dcb1` 建立、`9465d9d` 搬到現址)、
> `mcpjungle/books/<書名>/{import,transform}.ts`(產生語料)、`mcpjungle/books/<書名>/corpus/`(語料區)、
> 根 `.gitattributes`(圖片與素材政策)。量測基準:`origin/release` @ `73b8206`。
> 核心觀念來自 `.gitattributes:10` 的註解原文:**「書是素材,不是原始碼。放進這裡不代表要上架成語料 —— 上架是另一回事,由人挑哪一種形式。」**
相關 skill:
- **`elf-mcp-knowledge`** — 上架流程的主規範(Step 0–7:語料、`corpus.json`、smoke test、註冊、部署、文件同步)。
**本 skill 只管「素材區怎麼放」與「挑哪一份上架」;真的要上架時照它走。**
- `elf-mcp-gateway` — 註冊與部署
- `elf-mcp-server` — 語料模式放不下時才寫新 server
- 權威目錄規範:repo 的 `mcpjungle/books/README.md`(語料 id、目錄樹、`corpus.json` 欄位、內容規則)
---
## 1. 何時使用
- 新增一本書的素材到 `mcpjungle/books/<書名>/source/`,或整理 / 改名既有的書
- 把某本書的某個語言**上架**成 docs-mcp 語料(挑哪一份、排除哪些)
- 寫或修改某本書的 `import.ts`/`transform.ts`(篩選與轉換)
- 處理書籍圖片(一般 git、`-text -diff`)、`.gitattributes` 的素材規則
- 產生或修補語料的 `sources.json`
- 執行或延續 168小隊的譯文審查
- 排查「素材有、AI 查不到」「CI 擋大檔」
### 素材區與語料區的分界(先認清這條線)
| | 素材區 `mcpjungle/books/<書名>/source/` | 語料區 `mcpjungle/books/<書名>/corpus/<id>/` |
|---|---|---|
| 目的 | 人閱讀、翻譯、審稿的工作區 | 餵給 MCP 的純文字語料 |
| 保留範圍 | **完整**:原文、譯文、雙語對照、回譯、合併全書、圖片 | **只取一種形式**,且只放文字 |
| 圖片 | 有,走一般 git(`-text -diff`) | **沒有**,md 內的 `../images/…` 刻意留成失效連結,或由 `transform.ts` 改成官方網址連結(證據 B4) |
| 進不進 Docker image / npm 套件 | 不進(`mcpjungle/.dockerignore:7`) | 進 Docker image(所以有容量上限);npm 套件的 `files` 只有 `dist`、`README.md`,不含語料(`docs-mcp-server/package.json:10-13`) |
| 誰決定內容 | 擷取工具產出,原樣保留 | **人**決定篩選規則,寫進 `transform.ts` 的 `isIncluded`;`import.ts` 產生、勿手改(`books/README.md:74`) |
現況(`origin/release` @ `73b8206`,實測 2026-10-17)。篇數用 `git -c core.quotepath=false ls-files -- "mcpjungle/books/<書>/source/<語言>" | grep -c '\.md$'` 量,
不受本機被 `.gitignore` 排除的合併檔影響;圖片欄同法量 `\.(png|jpe?g|gif|webp)$`:
| 書 | 語言資料夾(md 篇數) | 圖片(png/jpg/gif/webp,一般 git) | 已上架成語料? |
|---|---|---|---|
| `multica` | `zh-CN` 92、`zh-TW` 92 | 60(png/jpg 1、gif/webp 59) | 否 |
| `nginx` | `en` 355、`en+zh-TW` 355 | 22 | 是:`nginx-en` 149 篇 |
| `opnsense` | `en` 379、`zh-TW` 379、`en+zh-TW` 378、`en-translated` 379 | 399 + 6 svg | 是:`opnsense-en` / `opnsense-zh-tw` 各 377 篇 |
| `portabase` | `en` 110、`en+zh-TW` 110 | 57 | 否 |
| `stalwart` | `en` 478、`en+zh-TW` 569 | 105(png/jpg 93、gif/webp 12) | 是:`stalwart-en` 487 篇 |
| `fc`、`sqlsugar` | 舊式語料,沒有 `import.ts`(第 3 節「舊式語料」) | 0 | 是:`fc-zh-tw` 133 篇、`sqlsugar-zh-tw` 74 篇 |
`books/README.md:36-45` 的現有語料表與實測一致(6 個),見第 7 節待確認 1。
---
## 2. 固定規則
### 素材區(`mcpjungle/books/<書名>/source/`)
1. **MUST** 目錄結構 `mcpjungle/books/<書名>/source/<語言>/*.md` + 同層 `mcpjungle/books/<書名>/source/images/`;
書資料夾另有 `import.ts`、`transform.ts`(產生語料)與 `corpus/`(語料區)。
`<書名>` 用小寫、可含 `-`,且**與未來語料 id 的書名部分同一個字串**(`opnsense` → `opnsense-en` / `opnsense-zh-tw`)。
WHY:素材與語料靠這個字串對應;`mcpjungle/books/README.md` 規定 id 為 `<書名>-<語言>`,書名不一致就無法回溯來源;
registrar 的 `gen-book-configs.mjs:52` 要求 `corpus.json` 的 `book` 等於書資料夾名,`:53-55` 要求 id 等於 `<book>-<language 小寫>`,或 `<book>-bi`(中英混排,`gen-book-configs.mjs:54-55`)。
現況(`73b8206`):stalwart 的 `source/en/` 不是扁平的,底下是巢狀子目錄(`auth/`、`cluster/`、`email/`…);它的 `isIncluded` 收所有不以 `_` 開頭的 `.md`/`.mdx`(`stalwart/transform.ts:9-12`,`_` 開頭的是 Starlight 片段)。
2. **MUST** 語言資料夾用擷取工具的原樣命名,現況五種:`en`、`zh-TW`、`zh-CN`(原文 / 譯文)、`en+zh-TW`(雙語對照)、`en-translated`(回譯)。
**MUST NOT** 把 `en+zh-TW`、`en-translated` 這類衍生物刪掉或壓成一份。
WHY:`.gitattributes:10-11` 註解明文「素材區保留完整(含 en+zh-TW 雙語對照、en-translated 等衍生物),語料只取其中一種」;
`en+zh-TW` 是 168小隊審稿的**輸入**(見規則 16),刪了審查就沒有對照基準。
3. **MUST** 圖片走一般 git,不走 LFS:`.gitattributes` 對 `mcpjungle/books/*/source/**` 的 `png`/`jpg`/`jpeg`/`gif`/`webp` 標 `-text -diff`(`:26-30`);svg 不另設規則。**MUST NOT** 改回 Git LFS。
WHY:mcp-library `21786bc` 推送時 GitHub 回 `This repository exceeded its LFS budget`,配額是帳號層級、repo 內解不了,整個 push 失敗;這批圖單檔都遠小於 GitHub 100 MB 上限,不需要 LFS(`.gitattributes:13-23` 註解原文,證據 B2)。`-text -diff` 讓二進位圖不做換行轉換、diff 不展開。
圖片容量只靠規則 5 的 CI 5 MB 檢查擋,git 設定本身不擋。驗證:`git check-attr text diff -- "mcpjungle/books/<書>/source/images/<檔>.png"` 應回 `text: unset`、`diff: unset`。
4. **MUST** 保留 `mcpjungle/books/*/source/** linguist-vendored`(`.gitattributes:34`)。
WHY:`.gitattributes:32-33` 註解——2600+ 篇書會把這個 repo 標成 Markdown 專案,蓋掉真正的 TypeScript 原始碼;也讓 diff 預設折疊。
5. **MUST** 單一檔案 < 5 MB。CI `basics` 的檢查是 `ci.yml:77` 對**整棵樹**的 `find . -type f -size +5M -not -path "./.git/*" -not -path "*/node_modules/*"`(只排除 `.git/` 與 `node_modules/`),
不是只看變更檔,也**不**排除 `mcpjungle/books/`;不分副檔名,對圖片也有效。
**現況(新書不要再犯)**:4 個超標合併檔都不在版控——`nginx/source/en/000 全書 (nginx.org) en.md` 12 MB、`nginx/source/en+zh-TW/000 全書 (nginx.org) en+zh-TW.md` 16 MB、
`opnsense/source/en+zh-TW/000 全書 (docs.opnsense.org) en+zh-TW.md` 7.3 MB 由 `4554bdf` 移除;`stalwart/source/en+zh-TW/000 全書 (stalw.art) en+zh-TW.md` 從未進版控、由 `920892f` 列名。
四個都靠 `.gitignore:13-20` **逐檔列名**排除,不是通用規則;新書再出現超標檔只有 CI 擋得住。見第 7 節待確認 2。
自查:
```bash
# 在 mcp-library repo 根目錄執行(CI 掃的是整棵樹)
find . -type f -size +5M -not -path "./.git/*" -not -path "*/node_modules/*"
```
6. **MUST** 擷取產出的 md 保持與來源一致(YAML front matter、`[⬆ 目錄]` / 上下篇導航行都留著)。
WHY:front matter 的 `source:` 是 `transform.ts` 去引號或產 `sources.json`(規則 9)與日後重新同步的唯一依據。
### 挑哪一份上架
7. **MUST** 一個語言資料夾 = 一個語料,id = `<書名>-<語言小寫>`(`opnsense/source/zh-TW/` → `opnsense-zh-tw`)。
命名的完整規則(禁底線、禁點、≤ 30 字元、禁泛名、全 gateway 唯一)以 `mcpjungle/books/README.md` 第一節(`:12-34`)為**權威**,不要在這裡另立一套;
(`:12-34` 指第一節的命名規則:語言後綴表與規則表;同一節的 `:36-45` 是「現有語料」表,不屬命名規則。)
registrar 會實際檢查(`gen-book-configs.mjs:10-11,38-40,53-55`)。
8. **MUST NOT** 上架下列四類,它們只留在素材區:
| 不上架 | 原因(權威出處) |
|---|---|
| `en+zh-TW/`(雙語對照) | `books/README.md:31`(第一節):「不可再放第三份雙語合併版」——同內容存三份、搜三次;要雙語就讓 AI 分別查兩個語料 |
| `en-translated/`(回譯) | 衍生物,內容與 `en/` 重複;`.gitattributes` 把它歸在素材區 |
| `000 全書 *.md`(合併全書) | `books/README.md:201-202`(第六節):合併檔是重複內容,每次搜尋都多命中一篇超大檔,**一律排除**;也是規則 5 的超標來源 |
| `images/` 與任何非文字檔 | `books/README.md:199-200`(第六節):語料只放文字;md 內的 `../images/…` 失效連結可接受,但要在 `corpus.json` 的 `description` 註明 |
由 `transform.ts` 的 `isIncluded` 執行(opnsense `transform.ts:7` 排除 `000 `、nginx `transform.ts:17-19` 只收 `NNN [章節] 標題.md`)。
9. **MUST** 每篇都要抽得到正確的來源網址,結尾不能多帶 `"`,也不能是空值。做法二擇一:
① 新書由 `transform.ts` 把 front matter 的 `source: "…"` 去引號(nginx `transform.ts:67-68`、opnsense `transform.ts:56-59`;stalwart 由 `sourceUrl()` 產生,`transform.ts:22`);
② 舊式語料(fc、sqlsugar),或 `transform.ts` 無法去引號時,用 `templates/make-sources-json.mjs` 產生 `sources.json`。
WHY:`mcpjungle/books/README.md` 第五節(`:183-193`)——自動抽取只掃前 15 行,front matter 帶引號時會把結尾的 `"` 一起吃進網址(`:190`);`sources.json` 優先於自動抽取(`:187`);匯入腳本去引號的例外寫在 `:193`。
驗證:第 3 節的上架前檢查,「來源問題」必須是 0 篇。
10. **MUST** 要不要收目錄頁(`000 目錄.md`)由 `import.ts` 的 `isIncluded` 決定;有收的話,目錄頁同樣要通過規則 9 的來源檢查。
WHY:`books/README.md` 第六節(`:202`)允許保留目錄頁;但它沒有 front matter,開頭是 `# …`+`> 網站:https://…`(證據 B12),收了卻抽不到來源就違反規則 9。
現況(`73b8206`):opnsense `transform.ts:7` 排除 `000 `、nginx `transform.ts:17-19` 只收有章節標籤的檔名,兩本都不收目錄頁。
stalwart 的 `isIncluded`(`stalwart/transform.ts:9-12`)收所有不以 `_` 開頭的 `.md`/`.mdx`,但它的 `source/en/` 是巢狀子目錄、沒有 `000 目錄.md` 這類目錄頁,所以不涉及本條。
11. **MUST** `import.ts` 產出的語料不得有泛名標題:`page`、`index`、`untitled` 或空標題。
標題取法:先取 front matter 的 `title:`,沒有才取第一個 `# ` 標題,去掉頭尾空白,不分大小寫。(`title:` 的值依 YAML 解析,外層引號不算標題的一部分。)重複標題只列出,不算失敗。
WHY:`docs_search`/`docs_outline` 回傳的是檔名與標題,那是 AI 選檔的唯一線索。
驗證:第 3 節的上架前檢查,「泛名標題」必須是 0 篇。舊式語料另有已知的例外,見 evidence。
注意:nginx 的 `source/en` 有 41 篇 `page`,語料裡是 0 篇,靠的是 `transform.ts:6-14` 的章節白名單間接擋下,不是標題檢查(證據 B11)。
12. **MUST** 上架是**人工挑選**的決定,不是把素材區自動同步過去。
WHY:`.gitattributes:10` 註解明文「放進這裡不代表要上架成語料 —— 上架是另一回事,由人挑哪一種形式」。
13. **MUST** 上架動作本身照 **`elf-mcp-knowledge` 第 3 節 Step 1–7** 走完:
素材放進 `source/` → `import.ts` 產生 `corpus/<id>/` → `corpus.json`(`book` / `language` / `source` / `title` / `description` / `capabilities` 六欄)→ 本機驗證 + smoke test + 上架前檢查
→ 註冊(見 `elf-mcp-gateway` 第 2 節〈註冊〉)→ 部署 → 驗證 gateway 曝露工具 → 同 PR 更新文件。
**MUST NOT** 手放 md 進 `corpus/` 當作上架,也 **MUST NOT** 手改 `import.ts` 產出的 md(`books/README.md:74`)。fc、sqlsugar 見第 3 節「舊式語料」。
14. **MUST** 顧容量:`books/*/corpus/` 總量上限約 **200 MB**(`books/README.md:204-206`;語料會打包進 `docs-mcp-server` 的 Docker image,每次 clone 都下載)。
目前總量 17 MB(`du -sh --total mcpjungle/books/*/corpus`,`73b8206`:`opnsense-en` 4.6、`opnsense-zh-tw` 4.4、`stalwart-en` 3.8、`nginx-en` 2.2、`fc-zh-tw` 1.0、`sqlsugar-zh-tw` 0.8 MB)。
超過時把大型語料拆到獨立 repo、以 `DOCS_CORPORA_DIR` 掛載,**不要**繼續塞進本 repo。
15. **MUST** 授權與品質如實註明:
- 上游要求保留聲明(BSD、MIT…)→ 語料根目錄放 `LICENSE` 全文,並在 `corpus.json` 的 `license` 欄註明。
- 機器翻譯 / 未校稿 → 在 `description` 寫明,並指出以哪個語料為準。範例(`books/README.md:142-144` 的 `opnsense-zh-tw`):
「機器翻譯(google_v2),168 小隊審稿中,術語以 opnsense-en 原文為準」。
- 刻意失效的連結(圖片、被排除的合併檔)→ 一併寫進 `description`。
WHY:`corpus.json` 的 `description` 是 `docs_list_corpora` 唯一會顯示給 AI 的品質說明;不寫=AI 把機翻當權威。
### 168小隊(譯文審查)
16. **MUST** 譯文審查在**素材區**做;目前只針對 opnsense:在 `mcpjungle/books/opnsense/source` 開工作階段(`168小隊-啟動提示.md:6`),
產出放 `../168小隊/`(即 `mcpjungle/books/opnsense/168小隊/`,`73b8206` 上尚未建立)。
**MUST NOT** 直接改 `corpus/`(`import.ts` 產出,勿手改)。啟動方式見 `mcpjungle/books/opnsense/168小隊-啟動提示.md`:
建 agent team(不是只用子代理)、
隊員 `168-terminologist` ×1、`168-reviewer` ×3(依 `000 目錄.md` 把章節平均切三段、範圍不重疊;共 377 篇,`:13`)、
`168-devils-advocate` ×1、`168-format-checker` ×1,審稿任務依賴「術語表完成」。
WHY:審查的輸入是 `en/` + `zh-TW/` + `en+zh-TW/` 三者對照(規則 2),語料區只有一份、無法對照。
17. **MUST NOT** 在啟動提示裡寫模型,也**MUST NOT** 把 `168-*` agent 改成 `elf-*` 的形狀(預載 skill、改 tools)。
WHY:啟動提示原文(`168小隊-啟動提示.md:7`)——「提示裡不要寫模型,否則會蓋過定義檔中的 model 設定」;
skillshare 的 `AGENTS.md` 明文:`168-*` 與 `elf-*` 慣例無關、不預載 skill,**不要**為了一致性去改它們。
---
## 3. 標準範本
| 檔案 | 用途 |
|---|---|
| `templates/make-sources-json.mjs` | 舊式語料,或 `transform.ts` 無法去引號時使用(規則 9):從 front matter 的 `source:` 產生 `sources.json`(零依賴、跳過 `examples/` 與 `images/`;已在 opnsense / nginx 素材上實測,證據 B12) |
| `templates/corpus-book.json` | 書籍語料的 `corpus.json`(六個必填欄位 + `license`,含機翻 / 失效連結的註明範例) |
素材區與語料區目錄樹:
```text
mcpjungle/books/
├── README.md # 語料命名與目錄結構的唯一權威版本
└── <書名>/
├── import.ts # 在 mcpjungle/docs-mcp-server 執行 npx tsx ../books/<書名>/import.ts
├── transform.ts # isIncluded(篩選)+轉換,純函式
├── source/ # 素材區;不進映像(mcpjungle/.dockerignore:7)
│ ├── en/ # 原文 → 可產生 <書名>-en
│ │ ├── 000 全書 (<來源網域>) en.md # ✗ 不上架(合併檔;超過 5 MB 要逐檔列進 .gitignore)
│ │ ├── 000 目錄.md # 收不收由 isIncluded 決定(規則 10)
│ │ └── 001 … .md
│ ├── zh-TW/ # 譯文 → 可產生 <書名>-zh-tw
│ ├── en+zh-TW/ # ✗ 不上架(雙語對照,168小隊審稿用)
│ ├── en-translated/ # ✗ 不上架(回譯衍生物)
│ └── images/ # ✗ 不上架(一般 git,-text -diff)
└── corpus/
└── <書名>-<語言>/ # import.ts 產出的 md 勿手改;corpus.json、LICENSE 手寫
```
opnsense 另有 `168小隊-啟動提示.md`(譯文審查的啟動提示,貼給新工作階段)、`ONBOARDING.md`,以及審查產出(術語表、報告)的目錄 `168小隊/`(`73b8206` 上尚未建立)。
上架一本書(素材 → 語料),指令照抄:
```bash
# 所在目錄:0)、1) 在 mcp-library repo 根目錄;2)–4) 在 mcpjungle/docs-mcp-server;5) 回到 repo 根目錄
# 0) 選 id 並查撞名(規則 7;權威規則見 mcpjungle/books/README.md 第一節)
ls mcpjungle/books/*/corpus/ mcpjungle/registry/
docker compose exec mcpjungle /mcpjungle list servers # 以 gateway 執行期清單為準
# 1) 素材放進 mcpjungle/books/<書名>/source/<語言>/,寫 transform.ts(isIncluded 排除規則 8 的四類)與 import.ts
# 寫法照 mcpjungle/books/nginx/{import,transform}.ts;來源已是 md 時,import.ts 只做篩選與複製
# 2) 產生語料:全部轉換成功才清掉舊 md 再寫入,任何錯誤都不寫檔
cd mcpjungle/docs-mcp-server
npm ci
npx tsx ../books/<書名>/import.ts
# corpus/<id>/corpus.json(與 LICENSE)手寫;import.ts 只清 md,不會刪它們
# 3) 確認沒有漏進非文字檔與超標檔
عرض على GitHub