T3: collector 骨架——watch→轉檔→git commit/push(design §4)
Node 單檔服務(非 arcrun workflow,跑客戶端機器):chokidar watch 事件驅動、debounce 合併批次 commit、.md passthrough(非 md 格式誠實丟 NotImplemented 待 T4 Markitdown)、 刪檔→target repo 移除對應檔(deprecated 標記留給 ingest workflow,collector 只管檔案鏡像)。 已驗(雲端 sandbox scratch 環境,非真實客戶環境):node --check 全過;起 scratch watch dir + scratch git repo + bare remote,實測 add/change/delete 三種事件皆正確偵測、debounce 正確 合併成一次 commit、git push 真的送達 remote(git log 驗證)、非 md 格式正確跳過並警告。 端到端待本機/客戶環境驗(README.md 已列):真實 NAS/VM watch、真實 Gitea remote+憑證、 Gitea push webhook 真觸發 ingest、systemd 常駐穩定性。
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const chokidar = require('chokidar');
|
||||
|
||||
const { loadConfig } = require('./config');
|
||||
const { transformFile, NotImplementedError } = require('./transform');
|
||||
const { GitSync } = require('./git-sync');
|
||||
|
||||
function ensureDir(p) {
|
||||
fs.mkdirSync(p, { recursive: true });
|
||||
}
|
||||
|
||||
function outputPathFor(config, relOutputPath) {
|
||||
return path.join(config.targetRepoDir, config.targetSubdir, relOutputPath);
|
||||
}
|
||||
|
||||
function handleAddOrChange(config, srcPath) {
|
||||
try {
|
||||
const { relOutputPath, content } = transformFile(srcPath, config.watchDir);
|
||||
const outPath = outputPathFor(config, relOutputPath);
|
||||
ensureDir(path.dirname(outPath));
|
||||
fs.writeFileSync(outPath, content, 'utf8');
|
||||
console.log(`[collector] 寫入 ${relOutputPath}`);
|
||||
} catch (e) {
|
||||
if (e instanceof NotImplementedError) {
|
||||
console.warn(`[collector] 跳過(待 T4):${e.message}`);
|
||||
} else {
|
||||
console.error(`[collector] 轉檔失敗 ${srcPath}:${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleUnlink(config, srcPath) {
|
||||
const relSrc = path.relative(config.watchDir, srcPath);
|
||||
const outPath = outputPathFor(config, relSrc);
|
||||
if (fs.existsSync(outPath)) {
|
||||
fs.unlinkSync(outPath);
|
||||
console.log(`[collector] 移除 ${relSrc}(來源已刪除,deprecated 標記交給 ingest workflow)`);
|
||||
}
|
||||
}
|
||||
|
||||
function startCollector(config, { git = new GitSync(config) } = {}) {
|
||||
ensureDir(path.join(config.targetRepoDir, config.targetSubdir));
|
||||
|
||||
let pendingCount = 0;
|
||||
let debounceTimer = null;
|
||||
|
||||
const flush = () => {
|
||||
if (pendingCount === 0) return;
|
||||
const n = pendingCount;
|
||||
pendingCount = 0;
|
||||
const result = git.commitAndPush(`collector: 同步 ${n} 項變更`);
|
||||
if (result.committed) {
|
||||
console.log(`[collector] 已 commit+push(${n} 項變更)`);
|
||||
}
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
pendingCount += 1;
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(flush, config.debounceMs);
|
||||
};
|
||||
|
||||
const watcher = chokidar.watch(config.watchDir, {
|
||||
ignoreInitial: false, // 首次啟動=首灌,掃過整個資料夾
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
watcher
|
||||
.on('add', (p) => { handleAddOrChange(config, p); schedule(); })
|
||||
.on('change', (p) => { handleAddOrChange(config, p); schedule(); })
|
||||
.on('unlink', (p) => { handleUnlink(config, p); schedule(); });
|
||||
|
||||
console.log(`[collector] watch 中:${config.watchDir} → ${config.targetRepoDir}/${config.targetSubdir}`);
|
||||
return watcher;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const config = loadConfig();
|
||||
startCollector(config);
|
||||
}
|
||||
|
||||
module.exports = { startCollector };
|
||||
Reference in New Issue
Block a user