merge: 拉取 origin/main(quality/observability/KB 批次 + Docker Hub 镜像)
# Conflicts: # bridge/src/server.js
This commit is contained in:
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
// 从官网 sitemap 生成 KB 类目地图 bridge/kb/11-site-map.md(旧 bridge site-map.md 的移植)。
|
||||
// 只生成导航/类目页 Q/A,不包含商品详情页;生成后需重建 KB 索引。
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const OUT = path.join(ROOT, 'bridge', 'kb', '11-site-map.md');
|
||||
const SITEMAP = process.env.SITE_MAP_URL || 'https://www.yehwang.com/sitemap.xml';
|
||||
const BASE = process.env.SITE_BASE_URL || 'https://www.yehwang.com';
|
||||
const WHITELIST = new Set(['jewelry', 'clothes', 'accessories', 'packing-and-display', 'bestsellers', 'new', 'top-deal', 'campaigns', 'brands', 'wholesale-service', 'factory-direct-wholesale-en', 'dropshipping', 'restock-request']);
|
||||
|
||||
function titleCase(segment) {
|
||||
return String(segment || '')
|
||||
.replace(/-/g, ' ')
|
||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function buildEntries(urls) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const url of urls) {
|
||||
if (!url.startsWith(BASE)) continue;
|
||||
const pathPart = url.slice(BASE.length).split('?')[0];
|
||||
if (!pathPart || pathPart === '/' || /\.html$/i.test(pathPart)) continue;
|
||||
const segments = pathPart.split('/').filter(Boolean);
|
||||
if (!segments.length || !WHITELIST.has(segments[0])) continue;
|
||||
if (segments.length > 4) continue;
|
||||
if (seen.has(pathPart)) continue;
|
||||
seen.add(pathPart);
|
||||
out.push({ path: pathPart, url: BASE + pathPart, name: titleCase(segments[segments.length - 1]) });
|
||||
}
|
||||
out.sort((a, b) => a.path.localeCompare(b.path));
|
||||
return out;
|
||||
}
|
||||
|
||||
function render(entries) {
|
||||
const head = [
|
||||
'---',
|
||||
'title: Yehwang Website Category Map (Site Map)',
|
||||
'category: site-map',
|
||||
`source: ${SITEMAP}`,
|
||||
'generated: true',
|
||||
'source_url:',
|
||||
` - ${SITEMAP}`,
|
||||
`last_updated: ${new Date().toISOString().slice(0, 10)}`,
|
||||
'---',
|
||||
'',
|
||||
'Every Yehwang category page has a direct URL. Use these links to send customers straight to the right page instead of a search query.',
|
||||
'',
|
||||
];
|
||||
const body = entries.map((e) => `### Q: Where can I find ${e.name}?\nBrowse our ${e.name} category: ${e.url}\n`);
|
||||
return head.join('\n') + '\n' + body.join('\n');
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const res = await fetch(SITEMAP, {
|
||||
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36' },
|
||||
signal: AbortSignal.timeout(90000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`sitemap HTTP ${res.status}`);
|
||||
const xml = await res.text();
|
||||
const urls = [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1].trim());
|
||||
const entries = buildEntries(urls);
|
||||
if (!entries.length) throw new Error('generated 0 site-map entries');
|
||||
fs.writeFileSync(OUT, render(entries), 'utf8');
|
||||
console.log(JSON.stringify({ ts: new Date().toISOString(), site_map_kb: 'done', entries: entries.length, out: OUT }));
|
||||
})().catch((err) => { console.error('[build-site-kb]', err.message); process.exit(1); });
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
// RAG 检索评测:不调 LLM,只验证 search_kb 的 top 命中。
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { loadConfig } = require('../bridge/src/config');
|
||||
const { createRetriever } = require('../bridge/src/rag');
|
||||
|
||||
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 });
|
||||
if (!cfg.ragPgUrl) throw new Error('RAG_PG_URL is not configured');
|
||||
|
||||
const CASES = [
|
||||
['minimum order value', 'minimum order'],
|
||||
['how to request a refund', 'refund'],
|
||||
['shipping costs', 'shipping cost'],
|
||||
['pre order', 'pre-order'],
|
||||
['factory direct MOQ', 'minimum order quantity'],
|
||||
['membership discount', 'membership'],
|
||||
['VAT refund', 'vat'],
|
||||
['jewelry discolor', 'discolor'],
|
||||
['最低起订量', 'minimum order'],
|
||||
];
|
||||
|
||||
(async () => {
|
||||
const retriever = createRetriever(cfg);
|
||||
let failed = 0;
|
||||
for (const [query, expected] of CASES) {
|
||||
const hits = await retriever.searchKB(query, 5);
|
||||
const ok = hits.some((h) => `${h.question} ${h.answer}`.toLowerCase().includes(expected.toLowerCase()));
|
||||
if (!ok) failed += 1;
|
||||
console.log(`${ok ? 'PASS' : 'FAIL'} ${query} -> ${expected} ${ok ? '' : JSON.stringify(hits.map((h) => h.question).slice(0, 3))}`);
|
||||
}
|
||||
console.log(`\n${CASES.length - failed}/${CASES.length} passed`);
|
||||
await retriever.pool.end().catch(() => {});
|
||||
process.exit(failed ? 1 : 0);
|
||||
})().catch((err) => { console.error('[eval-retrieval]', err.message); process.exit(1); });
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
// KB 质量审计:数量/父子结构、空答案、重复问题、按来源统计。
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { Pool } = require('../bridge/node_modules/pg');
|
||||
|
||||
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 url = process.env.RAG_PG_URL || env.RAG_PG_URL;
|
||||
if (!url) throw new Error('RAG_PG_URL is required');
|
||||
|
||||
(async () => {
|
||||
const pool = new Pool({ connectionString: url, max: 1 });
|
||||
try {
|
||||
const summary = await pool.query(`
|
||||
SELECT count(*)::int AS total,
|
||||
count(*) FILTER (WHERE chunk_type='parent')::int AS parents,
|
||||
count(*) FILTER (WHERE chunk_type='child')::int AS children,
|
||||
count(*) FILTER (WHERE answer IS NULL OR answer='')::int AS empty_answers,
|
||||
count(*) FILTER (WHERE text_tsv IS NULL)::int AS missing_tsv
|
||||
FROM kb_docs`);
|
||||
const dups = await pool.query(`
|
||||
SELECT lower(regexp_replace(question, '[^a-zA-Z0-9\\u4e00-\\u9fff]+', ' ', 'g')) AS q, count(*)::int AS n
|
||||
FROM kb_docs WHERE chunk_type='parent'
|
||||
GROUP BY 1 HAVING count(*) > 1
|
||||
ORDER BY n DESC LIMIT 10`);
|
||||
const sources = await pool.query(`
|
||||
SELECT source_file, count(*)::int AS n
|
||||
FROM kb_docs WHERE chunk_type='parent'
|
||||
GROUP BY source_file ORDER BY n DESC`);
|
||||
console.log(JSON.stringify({ summary: summary.rows[0], duplicates: dups.rows, sources: sources.rows }, null, 2));
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
})().catch((err) => { console.error('[kb-audit]', err.message); process.exit(1); });
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
// KB 版本管理:查看备份 / 回滚到 kb_docs_v<timestamp>。
|
||||
// 用法:
|
||||
// node scripts/kb-version.js list
|
||||
// node scripts/kb-version.js rollback kb_docs_v20261023032922
|
||||
// 需要 RAG_PG_URL 指向 KB 所在库(宿主机通常是 127.0.0.1:5440/<db>)。
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { Pool } = require('../bridge/node_modules/pg');
|
||||
const { listVersions, rollback } = require('../bridge/src/kb-version');
|
||||
|
||||
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 url = process.env.RAG_PG_URL || env.RAG_PG_URL;
|
||||
if (!url) throw new Error('RAG_PG_URL is required');
|
||||
|
||||
(async () => {
|
||||
const pool = new Pool({ connectionString: url, max: 1 });
|
||||
try {
|
||||
const cmd = process.argv[2];
|
||||
if (cmd === 'list') {
|
||||
const out = await listVersions(pool);
|
||||
console.log(JSON.stringify(out, null, 2));
|
||||
} else if (cmd === 'rollback') {
|
||||
const target = process.argv[3];
|
||||
if (!target) throw new Error('usage: node scripts/kb-version.js rollback <kb_docs_v...>');
|
||||
console.log(JSON.stringify(await rollback(pool, target), null, 2));
|
||||
} else {
|
||||
console.log('usage: node scripts/kb-version.js list | rollback <kb_docs_v...>');
|
||||
}
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
})().catch((err) => { console.error('[kb-version]', err.message); process.exit(1); });
|
||||
Reference in New Issue
Block a user