chore: CRM-NEW baseline
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
// 抓取官网 sitemap.xml(www/us),把全量 URL 落到 bridge 库 bridge_site_map_urls。
|
||||
// 数据只在服务端匹配,不进 LLM/向量库;商品搜索无结果时返回真实商品页链接。
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { Client } = 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 password = encodeURIComponent(env.POSTGRES_PASSWORD || '');
|
||||
const PG_URL = process.env.SITEMAP_PG_URL || `postgres://crmnew:${password}@127.0.0.1:5440/bridge`;
|
||||
|
||||
const SITES = {
|
||||
www: { base: 'https://www.yehwang.com', sitemap: 'https://www.yehwang.com/sitemap.xml' },
|
||||
us: { base: 'https://us.yehwang.com', sitemap: 'https://us.yehwang.com/sitemap.xml' },
|
||||
};
|
||||
|
||||
function tokensFor(pathPart) {
|
||||
return pathPart
|
||||
.replace(/\.html$/, '')
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.map((seg) => seg.replace(/-?\d+$/, '').split('-').filter((w) => w && w.length > 1).join(' '))
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
async function fetchSitemap(url) {
|
||||
const res = await fetch(url, {
|
||||
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; YehwangSiteMapBot/1.0)' },
|
||||
signal: AbortSignal.timeout(90000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`sitemap HTTP ${res.status}: ${url}`);
|
||||
const xml = await res.text();
|
||||
return [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1].trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
async function buildSite(client, name) {
|
||||
const cfg = SITES[name];
|
||||
const urls = await fetchSitemap(cfg.sitemap);
|
||||
const rows = [];
|
||||
const seenPaths = new Set();
|
||||
for (const url of urls) {
|
||||
if (!url.startsWith(cfg.base)) continue;
|
||||
const pathPart = url.slice(cfg.base.length).split('?')[0];
|
||||
if (!pathPart || pathPart === '/') continue;
|
||||
if (seenPaths.has(pathPart)) continue;
|
||||
seenPaths.add(pathPart);
|
||||
const isProduct = /\.html$/.test(pathPart);
|
||||
rows.push({
|
||||
path: pathPart,
|
||||
url: cfg.base + pathPart,
|
||||
slug_tokens: tokensFor(pathPart),
|
||||
is_product: isProduct,
|
||||
level: pathPart.split('/').filter(Boolean).length,
|
||||
});
|
||||
}
|
||||
await client.query('BEGIN');
|
||||
await client.query('DELETE FROM bridge_site_map_urls WHERE site = $1', [name]);
|
||||
for (let i = 0; i < rows.length; i += 1000) {
|
||||
const batch = rows.slice(i, i + 1000);
|
||||
const values = [];
|
||||
const params = [];
|
||||
batch.forEach((r, idx) => {
|
||||
const b = idx * 6;
|
||||
values.push(`($${b + 1},$${b + 2},$${b + 3},$${b + 4},$${b + 5},$${b + 6})`);
|
||||
params.push(name, r.path, r.url, r.slug_tokens, r.is_product, r.level);
|
||||
});
|
||||
await client.query(
|
||||
`INSERT INTO bridge_site_map_urls (site, path, url, slug_tokens, is_product, level) VALUES ${values.join(',')}`,
|
||||
params
|
||||
);
|
||||
}
|
||||
await client.query('COMMIT');
|
||||
const productCount = rows.filter((r) => r.is_product).length;
|
||||
console.log(`[site-map] ${name}: total=${rows.length}, products=${productCount}, nav=${rows.length - productCount}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const client = new Client({ connectionString: PG_URL });
|
||||
await client.connect();
|
||||
await client.query('CREATE EXTENSION IF NOT EXISTS pg_trgm');
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS bridge_site_map_urls (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
site TEXT NOT NULL,
|
||||
path TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
slug_tokens TEXT NOT NULL,
|
||||
is_product BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
level INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (site, path)
|
||||
)
|
||||
`);
|
||||
await client.query('CREATE INDEX IF NOT EXISTS bridge_site_map_urls_site_idx ON bridge_site_map_urls (site)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS bridge_site_map_urls_tokens_trgm_idx ON bridge_site_map_urls USING gin (slug_tokens gin_trgm_ops)');
|
||||
const targets = process.argv.slice(2).filter((s) => SITES[s]);
|
||||
for (const site of targets.length ? targets : ['www', 'us']) {
|
||||
await buildSite(client, site);
|
||||
}
|
||||
await client.end();
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('[build-site-map]', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user