#!/usr/bin/env node 'use strict'; // 在线 FAQ/KB 同步(不依赖旧 bridge): // 抓官网 FAQ JSON-LD → 更新 bridge/kb/*.md → parent/child + embedding → 原子切换 kb_docs // 用法: // node scripts/sync-kb-online.js [--force] // 需要环境变量: // RAG_PG_URL / EMB_BASE_URL / EMB_API_KEY / EMB_MODEL / EMB_DIM const fs = require('fs'); const path = require('path'); const { loadConfig } = require('../bridge/src/config'); const { createEmbeddingClient } = require('../bridge/src/embedding'); const { createKbIndexer } = require('../bridge/src/kb-indexer'); const { createFaqFetcher, loadFaqConfig } = require('../bridge/src/faq-fetcher'); const { updateMarkdownFile } = require('../bridge/src/faq-update'); const ROOT = path.resolve(__dirname, '..'); const env = {}; for (const line of fs.readFileSync(path.join(ROOT, '.env'), 'utf8').split('\n')) { if (!line.includes('=') || line.trim().startsWith('#')) continue; const i = line.indexOf('='); env[line.slice(0, i).trim()] = line.slice(i + 1).trim(); } const cfg = loadConfig({ ...env, ...process.env }); const force = process.argv.includes('--force'); const localOnly = process.argv.includes('--local-only'); const KB_DIR = path.join(ROOT, 'bridge', 'kb'); const FAQ_CONFIG = path.join(ROOT, 'bridge', 'config', 'faq.json'); (async () => { if (!cfg.ragPgUrl) throw new Error('RAG_PG_URL is not configured'); if (!cfg.embApiKey || !cfg.embBaseUrl) throw new Error('EMB_* is not configured'); const faqCfg = loadFaqConfig(FAQ_CONFIG); if (!faqCfg.enabled) throw new Error('faq.json disabled'); let changedFiles = 0; let added = 0; let updated = 0; if (!localOnly) { const fetcher = createFaqFetcher(faqCfg); const fetched = await fetcher.fetchAll(); for (const page of fetched) { if (page.error || !page.questions.length) { console.log(JSON.stringify({ ts: new Date().toISOString(), faq_fetch: 'skip', file: page.file, error: page.error || 'no_questions' })); continue; } const filePath = path.join(KB_DIR, page.file); const res = updateMarkdownFile(filePath, page.questions); if (res.changed) changedFiles += 1; added += res.added || 0; updated += res.updated || 0; console.log(JSON.stringify({ ts: new Date().toISOString(), faq_fetch: 'ok', file: page.file, questions: page.questions.length, added: res.added || 0, updated: res.updated || 0 })); } } if (!localOnly && !force && changedFiles === 0) { console.log(JSON.stringify({ ts: new Date().toISOString(), kb_sync: 'skipped', reason: 'no_faq_changes' })); return; } const emb = createEmbeddingClient(cfg); const indexer = createKbIndexer({ pool: emb.pool, embeddingClient: emb, kbDir: KB_DIR, dim: cfg.embDim }); try { const result = await indexer.reindex({ note: `sync-kb-online ${new Date().toISOString().slice(0, 10)}` }); console.log(JSON.stringify({ ts: new Date().toISOString(), kb_sync: 'done', changed_files: changedFiles, added, updated, ...result })); } finally { await emb.pool.end().catch(() => {}); } })().catch((err) => { console.error('[sync-kb-online]', err.message); process.exit(1); });