feat(sitemap): 支持本地 XML 回退(官网 sitemap 受 WAF 保护)+ 记录 5.1-5.3 数据面结果

This commit is contained in:
wangliang
2026-10-23 11:13:24 +08:00
parent 696916ad50
commit 3b8997b565
3 changed files with 88 additions and 4 deletions
+1
View File
@@ -7,6 +7,7 @@ libredesk/backups/
node_modules/
*.log
tmp/
var/
.DS_Store
# 子项目自带的 git 历史
+62 -1
View File
@@ -148,7 +148,7 @@ docker run --rm -v /etc/nginx/conf.d:/c redis:7-alpine rm -f /c/zz-wangliang-crm
|---|---|---|
| ~~AI Provider(DashScope)~~ | ✅ **已配置**(qwen-plus-latest / text-embedding-v4),端到端已验证 | 换 key:`DASHSCOPE_API_KEY=sk-xxx node scripts/configure-ai.js` |
| **SMTP / 163 邮件** | `SMTP_*` 为空,OTP 与邮件渠道不可用 | 填 `.env` 后 `node scripts/configure-email.js` + `node scripts/add-email-inbox.js` |
| **知识库 RAG(search_kb)** | 无 `RAG_PG_URL`,工具返回 503 | 指向旧 KB 库后运行 `scripts/import-kb-helpcenter.js` / `rebuild-kb-index.sh` |
| **知识库 RAG(search_kb)** | 无 `RAG_PG_URL`,工具返回 503;旧 KB 数据源本机不存在(见第 10 节) | 提供旧 `products.kb_docs` dump 或 `customer-service-kb/*.md` 后导入 |
| **WhatsApp 渠道** | 无 Meta 凭据 | 填 `WA_*` 后 `scripts/add-whatsapp-inbox.js`;需要公网回调(本栈已有 HTTPS,可不用 cloudflared) |
| **public-gateway / cloudflared** | 未启动 | 需要 Cloudflare 临时隧道时再 `docker compose up -d public-gateway cloudflared` |
@@ -198,3 +198,64 @@ Necklace Star `$5.70`/110,与 RPC 直查一致(首次调试时的编价问
若后续要硬性拦截虚假价格/链接,需要走 bridge 自己的 agent 路径,或在 LibreDesk 侧加后置校验。
验证完已把 `log_level` 恢复为 `info`。
## 10. 数据面初始化(5.1 KB / 5.2 sitemap / 5.3 质量表)
### 5.3 质量表 ✅
`node scripts/quality-report.js 1`:自动建表成功。
```text
table_exists = bridge_ai_metrics;当前 rows=0
{"summary":{"runs":0,"errors":0,...},"tools":[],"hours":1}
```
⚠️ 表建好了,但**会一直是 0 行**:`bridge_ai_metrics` 只在 bridge 自己的 agent 路径(webhook 编排)里写入,
而本栈的 AI 回复走的是 LibreDesk 内置 assistant。要让质量报表有数据,需要改用 bridge agent 或另接埋点。
### 5.2 全量 sitemap ✅
官网 `sitemap.xml` 受阿里云 WAF 的 JS 挑战保护(直接 fetch 返回 4KB 挑战 HTML、0 个 `<loc>`)。
处理:用真 Chrome(Playwright)过挑战后抓取,缓存到 `var/sitemap-{www,us}.xml`(已 gitignore),
并给 `scripts/build-site-map.js` 增加本地 XML 回退(`var/sitemap-<site>.xml` 或 `SITEMAP_XML_<SITE>`)。
```text
[site-map] www: total=32710, products=32070, nav=640
[site-map] us: total=22644, products=22324, nav=320
```
表 `bridge_site_map_urls` 已核对:
```text
site | total | products
us | 22644 | 22324
www | 32710 | 32070
```
刷新方式(sitemap 会变):先用浏览器重新抓 `var/sitemap-{www,us}.xml`,再 `node scripts/build-site-map.js www us`。
直接跑脚本在 WAF 下会明确报错「返回 0 个 <loc>」,不会静默写空表。
### 5.1 旧 KB(products.kb_docs)❌ 本机无数据源
按清单实际执行的结果:
```text
$ node scripts/upgrade-kb-schema.js
[upgrade-kb-schema] password authentication failed for user "postgres"
$ bash scripts/rebuild-kb-index.sh
Error response from daemon: No such container: crm-postgres
$ ls /home/dev/code/crm/crm-ai-demo/customer-service-kb
No such file or directory
$ docker ps -a | grep -E '^crm-(postgres|bridge)$'
(无)
```
原因:这两个脚本是为**旧栈**写的 —— `products.kb_docs` 在旧 `crm-postgres` 里,
`rebuild-kb-index.sh` 还会 `docker exec crm-bridge` 调旧 bridge 的 `faq/kbIndexer`(新仓库已没有这段代码)。
本工作区是全新栈:宿主 PG 5432 只有 `postgres/storefront` 两个库,没有 `products`;也没有旧容器的数据卷。
要补齐需要**数据源**,二选一:
1. 旧测试服导出 `products.kb_docs`:`pg_dump -t kb_docs products > kb_docs.sql`;
2. 或直接给 `customer-service-kb/*.md`(9 个文件,108 篇文章的来源)。
拿到后我按新栈方式接入(推荐导入 LibreDesk 帮助中心,AI 原生 RAG + 引用;旧的 `search_kb` 可保留为兜底)。
在此之前,商品/库存/订单/商机等 9 个工具不受影响,只有 `search_kb` 返回 503。
+25 -3
View File
@@ -33,19 +33,41 @@ function tokensFor(pathPart) {
.toLowerCase();
}
async function fetchSitemap(url) {
function parseLocs(xml) {
return [...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1].trim()).filter(Boolean);
}
// 官网 sitemap 受阿里云 WAF JS 挑战保护,直接 fetch 会返回挑战 HTML(0 个 <loc>)。
// 支持从本地缓存的 XML 读取:SITEMAP_XML_<SITE> 环境变量,或 var/sitemap-<site>.xml。
function localSitemapFile(name) {
const fromEnv = process.env['SITEMAP_XML_' + name.toUpperCase()];
if (fromEnv && fs.existsSync(fromEnv)) return fromEnv;
const p = path.join(ROOT, 'var', `sitemap-${name}.xml`);
return fs.existsSync(p) ? p : null;
}
async function fetchSitemap(name, url) {
const local = localSitemapFile(name);
if (local) {
console.log(`[site-map] ${name}: 使用本地 sitemap ${local}`);
return parseLocs(await fs.promises.readFile(local, 'utf8'));
}
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);
const locs = parseLocs(xml);
if (!locs.length) {
throw new Error(`sitemap 返回 0 个 <loc>(可能被 WAF 挑战):先下载 XML 到 var/sitemap-${name}.xml,或设置 SITEMAP_XML_${name.toUpperCase()}`);
}
return locs;
}
async function buildSite(client, name) {
const cfg = SITES[name];
const urls = await fetchSitemap(cfg.sitemap);
const urls = await fetchSitemap(name, cfg.sitemap);
const rows = [];
const seenPaths = new Set();
for (const url of urls) {