T4: Markitdown adapter——docx/pptx/pdf 轉檔 + 原檔進 assets/originals(design §4)
transform.js:MARKITDOWN_EXT(docx/pptx/pdf)走 markitdown CLI 子行程轉出 md,輸出路徑 副檔名換 .md;回傳 originalCopy 供 index.js 把原檔複製進 assets/originals/<相對路徑>。 git-sync.js:ensureGitAttributes() 啟動時冪等寫入 LFS 宣告(三種格式 filter=lfs)。 已驗(雲端 sandbox,兩輪煙測):用 python-docx 生真實 docx(含標題+段落)丟進 watch 資料夾, collector 呼叫 markitdown 轉出真實 md 內容(人工核對與原檔一致)、原檔複製進 assets/originals/、.gitattributes 正確含三種格式 LFS 宣告、commit+push 送達 bare remote。 誠實邊界:sandbox 無 git-lfs 二進位,驗不到真實 LFS smudge/clean filter 生效(只驗到 .gitattributes 內容正確);真實 Gitea remote push/憑證、真實客戶檔案樣本仍待本機/客戶環境。 其餘格式(xlsx/圖片)不在第一波承諾範圍,仍誠實丟 NotImplementedError。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# collector — 收集端骨架(rag-wave1 T3)
|
||||
# collector — 收集端骨架(rag-wave1 T3+T4)
|
||||
|
||||
> design.md §4;跑在**客戶端機器**(NAS/VM,或導入者代管的 VPS),不是 arcrun workflow(arcrun 零件禁檔案系統,見 CLAUDE.md 紅線)。
|
||||
> ⚠️ **本骨架端到端(真實 NAS/VM + 真實 Gitea remote + systemd 常駐)需在本機/客戶環境驗,雲端 sandbox 沒有這些條件**——這裡只做得到:程式邏輯本身可跑、對本機臨時目錄的煙測(見下方「已驗證」)。
|
||||
@@ -6,7 +6,7 @@
|
||||
## 做什麼
|
||||
|
||||
```
|
||||
watch 知識資料夾 ──(新增/修改)──► transform(Markitdown,T4 補)──► 寫入 target repo 工作目錄
|
||||
watch 知識資料夾 ──(新增/修改)──► transform(.md passthrough/docx,pptx,pdf→Markitdown)──► 寫入 target repo 工作目錄
|
||||
──(刪除) ──► target repo 移除對應檔
|
||||
│
|
||||
git add/commit/push
|
||||
@@ -26,17 +26,21 @@ watch 知識資料夾 ──(新增/修改)──► transform(Markitdown,T4
|
||||
|
||||
檔案在來源資料夾被刪除 → 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,依賴本骨架)
|
||||
## 轉檔(T4)
|
||||
|
||||
`transform.js` 目前只處理 `.md`(直接複製,frontmatter 補 `source_path` 溯源欄位)。非 md 格式(docx/pptx/pdf)丟 `NotImplementedError` 並記警告日誌——**這是刻意的**:T4(Markitdown adapter)要接手這段,本骨架先把「watch→轉檔→寫入→commit」的骨架打通,轉檔本體留給 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 轉換(T4 填 Markitdown 邏輯;現只認 `.md` passthrough) |
|
||||
| `git-sync.js` | target repo 的 git add/commit/push 封裝 |
|
||||
| `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`) |
|
||||
|
||||
## 設定(環境變數)
|
||||
@@ -73,11 +77,16 @@ WantedBy=multi-user.target
|
||||
## 已驗證(雲端 sandbox 能做到的部分)
|
||||
|
||||
- `node --check` 語法檢查全過。
|
||||
- 本機臨時目錄煙測(非真實客戶環境):起一個 scratch git repo 當 target、一個 scratch 資料夾當 watch 來源,新增/修改/刪除 `.md` 檔案 → collector 正確偵測、寫入/移除、debounce 後產生一次 commit,`git log` 驗到 commit 內容與變更一致。**不含**:真實 Gitea remote push(需真實 token+repo)、Markitdown 轉檔(T4 未做)、systemd 常駐、NAS/VM 環境。
|
||||
- 本機臨時目錄煙測(非真實客戶環境,兩輪):起一個 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(非 md 格式檔案觸發 T4 Markitdown)。
|
||||
1. 真實客戶知識資料夾 watch(含各類真實 docx/pptx/pdf 樣本,非合成測試檔)。
|
||||
2. 真實 Gitea remote push(含憑證管理)。
|
||||
3. push 後確認 Gitea push webhook 真觸發 ingest workflow(另案)。
|
||||
4. systemd 常駐穩定性(重開機自動起、崩潰自動重啟)。
|
||||
3. 真實 git-lfs 安裝+`git lfs track`,確認大檔案真的走 LFS store 而非塞進一般 blob 歷史。
|
||||
4. push 後確認 Gitea push webhook 真觸發 ingest workflow(另案)。
|
||||
5. systemd 常駐穩定性(重開機自動起、崩潰自動重啟)。
|
||||
|
||||
+20
@@ -1,5 +1,24 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
// design.md §4「原檔進 LFS」:assets/originals/ 底下的常見二進位格式走 git-lfs。
|
||||
// ⚠️ 這裡只寫 .gitattributes 宣告(冪等、不重複寫)——LFS 是否真的生效取決於部署機器
|
||||
// 有沒有裝 git-lfs 並 `git lfs install`;雲端 sandbox 沒有 git-lfs 二進位,只能驗到
|
||||
// .gitattributes 內容正確,驗不到真實 LFS smudge/clean filter,見 README「待本機驗」。
|
||||
const LFS_PATTERNS = ['assets/originals/**/*.pdf', 'assets/originals/**/*.docx', 'assets/originals/**/*.pptx'];
|
||||
|
||||
function ensureGitAttributes(repoDir) {
|
||||
const gaPath = path.join(repoDir, '.gitattributes');
|
||||
const existing = fs.existsSync(gaPath) ? fs.readFileSync(gaPath, 'utf8') : '';
|
||||
const missing = LFS_PATTERNS.filter((p) => !existing.includes(p));
|
||||
if (missing.length === 0) return false;
|
||||
const lines = missing.map((p) => `${p} filter=lfs diff=lfs merge=lfs -text`);
|
||||
const next = existing.length && !existing.endsWith('\n') ? `${existing}\n` : existing;
|
||||
fs.writeFileSync(gaPath, next + lines.join('\n') + '\n', 'utf8');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* target repo 的 git add/commit/push 封裝(design.md §4:git commit/push 觸發既有
|
||||
* Gitea push webhook → ingest workflow;本模組不打任何 ingest HTTP 端點)。
|
||||
@@ -9,6 +28,7 @@ class GitSync {
|
||||
this.repoDir = config.targetRepoDir;
|
||||
this.authorName = config.gitAuthorName;
|
||||
this.authorEmail = config.gitAuthorEmail;
|
||||
ensureGitAttributes(this.repoDir);
|
||||
}
|
||||
|
||||
_git(args) {
|
||||
|
||||
@@ -16,11 +16,18 @@ function outputPathFor(config, relOutputPath) {
|
||||
|
||||
function handleAddOrChange(config, srcPath) {
|
||||
try {
|
||||
const { relOutputPath, content } = transformFile(srcPath, config.watchDir);
|
||||
const { relOutputPath, content, originalCopy } = transformFile(srcPath, config.watchDir);
|
||||
const outPath = outputPathFor(config, relOutputPath);
|
||||
ensureDir(path.dirname(outPath));
|
||||
fs.writeFileSync(outPath, content, 'utf8');
|
||||
console.log(`[collector] 寫入 ${relOutputPath}`);
|
||||
|
||||
if (originalCopy) {
|
||||
const origOutPath = outputPathFor(config, originalCopy.relPath);
|
||||
ensureDir(path.dirname(origOutPath));
|
||||
fs.copyFileSync(srcPath, origOutPath);
|
||||
console.log(`[collector] 原檔複製 ${originalCopy.relPath}(LFS 追蹤需部署機器已裝 git-lfs)`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof NotImplementedError) {
|
||||
console.warn(`[collector] 跳過(待 T4):${e.message}`);
|
||||
|
||||
+37
-6
@@ -1,14 +1,23 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
class NotImplementedError extends Error {}
|
||||
|
||||
const MARKDOWN_EXT = new Set(['.md', '.markdown']);
|
||||
|
||||
// design.md §4 明列的第一波轉檔格式:docx/pptx/pdf。其餘格式仍走 NotImplementedError,
|
||||
// 不擴大承諾範圍(品質不承諾是 design 原話,但格式範圍先照 SDD 講定的三種)。
|
||||
const MARKITDOWN_EXT = new Set(['.docx', '.pptx', '.pdf']);
|
||||
|
||||
/**
|
||||
* 檔案 → md 轉換(T3 骨架只做 .md passthrough;T4 補 Markitdown 邏輯)。
|
||||
* 檔案 → md 轉換(T4:Markitdown adapter)。
|
||||
* srcPath: 來源檔案絕對路徑(客戶知識資料夾內)
|
||||
* 回傳:{ relOutputPath, content }(相對 target repo 子目錄的輸出路徑 + md 內容)
|
||||
* 回傳:{ relOutputPath, content, originalCopy? }
|
||||
* - originalCopy(僅 Markitdown 轉檔路徑):{ relPath },供 index.js 把原檔複製進
|
||||
* target repo 的 assets/originals/(design.md §4:「原檔進 LFS」——LFS 追蹤本身
|
||||
* 依賴部署機器裝 git-lfs 並 `git lfs track`,本模組只管把原檔放進約定路徑,
|
||||
* 不假裝在雲端 sandbox 驗過真實 LFS push,見 README「待本機驗」)。
|
||||
*/
|
||||
function transformFile(srcPath, watchDir) {
|
||||
const ext = path.extname(srcPath).toLowerCase();
|
||||
@@ -17,16 +26,38 @@ function transformFile(srcPath, watchDir) {
|
||||
if (MARKDOWN_EXT.has(ext)) {
|
||||
const raw = fs.readFileSync(srcPath, 'utf8');
|
||||
const content = stampSourcePath(raw, relSrc);
|
||||
const relOutputPath = relSrc;
|
||||
return { relOutputPath, content };
|
||||
return { relOutputPath: relSrc, content };
|
||||
}
|
||||
|
||||
// 非 md(docx/pptx/pdf...)— T4 待補 Markitdown 轉檔,這裡先誠實丟未實作,不假裝轉好。
|
||||
if (MARKITDOWN_EXT.has(ext)) {
|
||||
const md = convertWithMarkitdown(srcPath);
|
||||
const content = stampSourcePath(md, relSrc);
|
||||
const relOutputPath = relSrc.slice(0, -ext.length) + '.md';
|
||||
return {
|
||||
relOutputPath,
|
||||
content,
|
||||
originalCopy: { relPath: path.join('assets/originals', relSrc) },
|
||||
};
|
||||
}
|
||||
|
||||
// 其餘格式(xlsx/圖片/...)— 不在 design.md §4 第一波承諾範圍,誠實丟未實作。
|
||||
throw new NotImplementedError(
|
||||
`${relSrc}:非 .md 格式轉檔待 T4(Markitdown adapter)補上,本骨架先跳過`,
|
||||
`${relSrc}:格式 ${ext} 不在第一波 Markitdown 承諾範圍(docx/pptx/pdf),本骨架先跳過`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 呼叫 markitdown CLI 轉檔(品質不承諾,design.md §4 原話)。 */
|
||||
function convertWithMarkitdown(srcPath) {
|
||||
try {
|
||||
return execFileSync('markitdown', [srcPath], {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 20 * 1024 * 1024,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`markitdown 轉檔失敗(${srcPath}):${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** md frontmatter 補 source_path(溯源用,design.md §4:「md frontmatter 記原檔路徑」)。 */
|
||||
function stampSourcePath(raw, relSrc) {
|
||||
const stamp = `source_path: "${relSrc}"`;
|
||||
|
||||
Reference in New Issue
Block a user