feat(sitemap): 支持本地 XML 回退(官网 sitemap 受 WAF 保护)+ 记录 5.1-5.3 数据面结果
This commit is contained in:
@@ -7,6 +7,7 @@ libredesk/backups/
|
|||||||
node_modules/
|
node_modules/
|
||||||
*.log
|
*.log
|
||||||
tmp/
|
tmp/
|
||||||
|
var/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# 子项目自带的 git 历史
|
# 子项目自带的 git 历史
|
||||||
|
|||||||
@@ -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` |
|
| ~~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` |
|
| **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) |
|
| **WhatsApp 渠道** | 无 Meta 凭据 | 填 `WA_*` 后 `scripts/add-whatsapp-inbox.js`;需要公网回调(本栈已有 HTTPS,可不用 cloudflared) |
|
||||||
| **public-gateway / cloudflared** | 未启动 | 需要 Cloudflare 临时隧道时再 `docker compose up -d public-gateway 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 侧加后置校验。
|
若后续要硬性拦截虚假价格/链接,需要走 bridge 自己的 agent 路径,或在 LibreDesk 侧加后置校验。
|
||||||
|
|
||||||
验证完已把 `log_level` 恢复为 `info`。
|
验证完已把 `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。
|
||||||
|
|||||||
@@ -33,19 +33,41 @@ function tokensFor(pathPart) {
|
|||||||
.toLowerCase();
|
.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, {
|
const res = await fetch(url, {
|
||||||
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; YehwangSiteMapBot/1.0)' },
|
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; YehwangSiteMapBot/1.0)' },
|
||||||
signal: AbortSignal.timeout(90000),
|
signal: AbortSignal.timeout(90000),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`sitemap HTTP ${res.status}: ${url}`);
|
if (!res.ok) throw new Error(`sitemap HTTP ${res.status}: ${url}`);
|
||||||
const xml = await res.text();
|
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) {
|
async function buildSite(client, name) {
|
||||||
const cfg = SITES[name];
|
const cfg = SITES[name];
|
||||||
const urls = await fetchSitemap(cfg.sitemap);
|
const urls = await fetchSitemap(name, cfg.sitemap);
|
||||||
const rows = [];
|
const rows = [];
|
||||||
const seenPaths = new Set();
|
const seenPaths = new Set();
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
|
|||||||
Reference in New Issue
Block a user