diff --git a/bridge/src/server.js b/bridge/src/server.js index 3aacbae..48b8440 100644 --- a/bridge/src/server.js +++ b/bridge/src/server.js @@ -7,6 +7,9 @@ const { createTools, resolveSiteProfile } = require('./tools'); const { createIdentity } = require('./identity'); const { createLibreDeskWebhook } = require('./libredesk-webhook'); const { createAgent } = require('./agent'); +const { createVerification } = require('./verification'); +const { createLibreDeskClient } = require('./libredesk'); +const { createRetriever } = require('./rag'); const { ToolError } = require('./errors'); const MAX_BODY_BYTES = 256 * 1024; @@ -82,9 +85,14 @@ function contextFromHeaders(headers = {}) { function createServer(options = {}) { const cfg = options.config || loadConfig(); const auther = createAuther(cfg.toolsSharedKey); - const tools = options.tools || createTools(cfg); + // HTTP 工具入口(LibreDesk 直接调用)也必须带上 retriever/verification/client, + // 否则 search_kb / send_verification_code / handoff_to_human 会一直是 not_configured。 + const verification = options.verification || createVerification(cfg); + const client = options.client || createLibreDeskClient(cfg); + const retriever = options.retriever || (cfg.ragPgUrl ? createRetriever(cfg) : null); + const tools = options.tools || createTools(cfg, { verification, retriever, client }); const identity = options.identity || createIdentity(cfg); - const agent = options.agent || createAgent(cfg); + const agent = options.agent || createAgent(cfg, { verification, client, retriever }); const libredeskWebhook = options.libredeskWebhook || createLibreDeskWebhook(cfg, { agent }); const byName = new Map(tools.map((t) => [t.name, t])); const startedAt = Date.now(); @@ -252,7 +260,10 @@ if (require.main === module) { if (!cfg.toolsSharedKey) console.warn('[crm-tools] TOOLS_SHARED_KEY is not set — all /tools/* calls will return 503'); if (cfg.devBypassVerification) console.warn('[crm-tools] TOOLS_DEV_BYPASS=true — verification checks are BYPASSED (local development only)'); }); - const shutdown = () => server.close(() => process.exit(0)); + const shutdown = () => server.close(() => { + if (retriever && retriever.pool) retriever.pool.end().catch(() => {}); + process.exit(0); + }); process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); } diff --git a/docs/deploy-wangliang.md b/docs/deploy-wangliang.md index 85744ba..cc69b2a 100644 --- a/docs/deploy-wangliang.md +++ b/docs/deploy-wangliang.md @@ -66,6 +66,8 @@ RPC 走 **容器网络直连**:compose 把 `bridge` 同时接入外部网络 ` - **AI Provider 已配置**:DashScope `qwen-plus-latest`(completion)+ `text-embedding-v4` 1024 维(embedding), `configure-ai.js` 的两项连通性测试均通过。密钥只存在 LibreDesk 库内与 `.env`(600),未写入文档。 - 助理指令已加固(见第 9 节):`search_products` 必须用 2-5 个词的关键词、不得编造价格/商品,`price=null` 时明确说“暂不可用”。 +- **知识库已接入**:`bridge` 库 `kb_docs` 367 行(296 parent / 71 child,来自 `deploy/seeds/kb_docs.sql.gz`), + `RAG_PG_URL` 已配置,`search_kb` 实测命中最低起订量/退换货等 FAQ,AI 能据此回答。 - `app.root_url = https://wangliang-crm.dev.gaia888.com`(`scripts/set-public-url.js`)。 - 10 个 bridge 工具已注册(`scripts/register-tools.js`):`search_products / search_orders / search_inventory / search_kb / search_email_history / collect_info / send_verification_code / verify_email_code / handoff_to_human / @@ -151,7 +153,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 数据源本机不存在(见第 10 节) | 提供旧 `products.kb_docs` dump 或 `customer-service-kb/*.md` 后导入 | +| ~~知识库 RAG(search_kb)~~ | ✅ **已配置**:快照导入 `bridge` 库(367 行),`RAG_PG_URL` 已指向,`search_kb` 可用 | 更新:`bash scripts/import-kb-snapshot.sh` 或 `node scripts/sync-kb-online.js --local-only --force` | | **WhatsApp 渠道** | 无 Meta 凭据 | 填 `WA_*` 后 `scripts/add-whatsapp-inbox.js`;需要公网回调(本栈已有 HTTPS,可不用 cloudflared) | | **public-gateway / cloudflared** | 未启动 | 需要 Cloudflare 临时隧道时再 `docker compose up -d public-gateway cloudflared` | @@ -236,29 +238,68 @@ table_exists = bridge_ai_metrics;当前 rows=0 刷新方式(sitemap 会变):先用浏览器重新抓 `var/sitemap-{www,us}.xml`,再 `node scripts/build-site-map.js www us`。 直接跑脚本在 WAF 下会明确报错「返回 0 个 」,不会静默写空表。 -### 5.1 旧 KB(products.kb_docs)❌ 本机无数据源 +### 5.1 旧 KB(products.kb_docs)✅ 已用最新代码解决 -按清单实际执行的结果: +拉取 `origin/main` 之前,本机确实没有旧 `products.kb_docs`:`upgrade-kb-schema.js` 报 +`password authentication failed for user "postgres"`,`rebuild-kb-index.sh` 报 `No such container: crm-postgres` +(这两个脚本是旧栈专用,新版仓库已不依赖它们)。 -```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)$' -(无) +最新代码(`f526696` / `81b49c9`)提供了「无旧 bridge」的标准路径,已按 `docs/go-live.md` 6.3 执行: + +1. 导入仓库内置快照:`bash scripts/import-kb-snapshot.sh` + → 导入到 `crmnew-postgres/bridge`,实测 **367 行(296 parent / 71 child)**,与文档预期一致; +2. `.env` 增加 `RAG_PG_URL=postgres://crmnew:***@postgres:5432/bridge`,`docker compose up -d --build --force-recreate bridge`; +3. 修了一个上游接线 bug:`server.js` 调 `createTools(cfg)` 时没传 `retriever/verification/client`, + 导致 HTTP 工具入口的 `search_kb` / `send_verification_code` / `handoff_to_human` 永远返回 `not_configured`。 + 现已按 `agent.js` 的方式创建并传入(本地提交 `fix(bridge): HTTP 工具入口注入 retriever/verification/client`)。 +4. 验证: + - `search_kb "minimum order value"` → 命中 `What is the minimum order value?`(score 0.733,`source_urls` 含 `order.html`); + - AI 端到端 → 回答 `The minimum order value is €30`,内容与 `kb_docs` parent 记录一致; +5. 新索引器实测:`RAG_PG_URL=postgres://...@127.0.0.1:5440/bridge node scripts/sync-kb-online.js --local-only --force` + → `parents=296 children=71`,自动备份 `kb_docs_v20261023033455`,`search_kb` 仍正常。 + +维护方式: + +```bash +# 从 bridge/kb/*.md 重建(宿主机执行,RAG 用 127.0.0.1:5440) +RAG_PG_URL=postgres://crmnew:@127.0.0.1:5440/bridge \ + node scripts/sync-kb-online.js --local-only --force + +# 在线同步官网 FAQ(建议每周一次;官网有 WAF,失败就退回 --local-only) +RAG_PG_URL=postgres://crmnew:@127.0.0.1:5440/bridge \ + node scripts/sync-kb-online.js ``` -原因:这两个脚本是为**旧栈**写的 —— `products.kb_docs` 在旧 `crm-postgres` 里, -`rebuild-kb-index.sh` 还会 `docker exec crm-bridge` 调旧 bridge 的 `faq/kbIndexer`(新仓库已没有这段代码)。 -本工作区是全新栈:宿主 PG 5432 只有 `postgres/storefront` 两个库,没有 `products`;也没有旧容器的数据卷。 +## 11. 最新代码(origin/main merge `a1d1de0`)怎么运行 -要补齐需要**数据源**,二选一: +最新 `main` 新增了 KB 快照/在线同步、环境差异文档,并去掉了 5.1 对旧栈的依赖。本机已合并并落地。 -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。 +```bash +cd /data/dev/wangliang/crm-ai +git pull --ff-only # 或 merge origin/main +docker compose config -q +docker compose up -d +docker compose up -d --build --force-recreate bridge # bridge 代码变更时必须 +docker compose ps +``` + +`bootstrap.sh` 只用于全新环境;已有环境重跑会覆盖 `.env`/配置,**不要跑**。 + +### 数据准备(对应 `docs/go-live.md` 6.3) + +| 任务 | 命令 | 本机结果 | +|---|---|---| +| KB 快照导入 | `bash scripts/import-kb-snapshot.sh` | ✅ 367 行(296/71) | +| KB 本地重建 | `RAG_PG_URL=...127.0.0.1:5440/bridge node scripts/sync-kb-online.js --local-only --force` | ✅ 296/71 + 备份 | +| KB 在线同步 | `RAG_PG_URL=... node scripts/sync-kb-online.js` | 可选(官网 WAF,失败用 `--local-only`) | +| 全量 sitemap | `node scripts/build-site-map.js www us` | ✅ 55,354 行(见 5.2) | +| 质量报表 | `node scripts/quality-report.js 1` | ✅ 表已建(0 行,原因见 5.3) | + +### 环境差异(`docs/environments.md`) + +最新文档明确:`rpc-tunnel.sh` / `public-https.sh` 只用于本地联调;测试/生产直接用内网 RPC 地址 + 正式域名 HTTPS。 +本机现状正好符合: +- RPC 走 `spug-net` 直连 `wangliang-dev:950x`,**没有**运行 `rpc-tunnel.sh`; +- HTTPS 由宿主 nginx + `*.dev.gaia888.com` 证书提供,**没有**用 Cloudflare Quick Tunnel。