Files
crm-ai/scripts/sync-kb-online.js

74 lines
3.1 KiB
JavaScript
Executable File

#!/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);
});