Compare commits
9
Commits
ffc9574a45
...
5c1d2b9129
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c1d2b9129 | ||
|
|
cdeb30f5c4 | ||
|
|
589e5f9349 | ||
|
|
d1bd344b96 | ||
|
|
a1d1de03c3 | ||
|
|
5b08bcfc7c | ||
|
|
3b8997b565 | ||
|
|
696916ad50 | ||
|
|
a7a5b4e85c |
@@ -8,6 +8,7 @@ libredesk/backups/
|
|||||||
node_modules/
|
node_modules/
|
||||||
*.log
|
*.log
|
||||||
tmp/
|
tmp/
|
||||||
|
var/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# 子项目自带的 git 历史
|
# 子项目自带的 git 历史
|
||||||
|
|||||||
@@ -229,6 +229,9 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- default
|
||||||
|
- spug-net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:4100/healthz"]
|
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:4100/healthz"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -238,6 +241,9 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
name: crm-new_default
|
name: crm-new_default
|
||||||
|
# 与本工作区的 wangliang-dev 同网络,bridge 可直接按容器名调用 9503/9505/9506/9509
|
||||||
|
spug-net:
|
||||||
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pg-data:
|
pg-data:
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
# crm-ai 在 wangliang 工作区的部署记录
|
||||||
|
|
||||||
|
> 部署时间:2026-10-23 | 部署位置:`/data/dev/wangliang/crm-ai` | 代码版本:`main @ 1ae25f1`
|
||||||
|
>
|
||||||
|
> 本文件记录这套栈**实际跑成什么样**、和仓库默认配置差在哪、怎么运维、以及还缺哪些凭据。
|
||||||
|
> 环境相关的临时信息写在这里,通用约定见 `/data/dev/_shared/NOTES.md`。
|
||||||
|
|
||||||
|
## 1. 交付摘要
|
||||||
|
|
||||||
|
一套自包含的客服 + CRM 栈(与仓库 `docker-compose.yml` 一致,Compose 项目名 `crm-new`):
|
||||||
|
|
||||||
|
| 组件 | 容器 | 宿主端口 | 状态 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| LibreDesk v2.9.0-rc.10(客服工作台 + 内置 AI) | `crmnew-libredesk` | `9000` | ✅ healthy |
|
||||||
|
| Twenty CRM v2.30.1(server) | `crmnew-twenty-server` | `3100` | ✅ healthy |
|
||||||
|
| Twenty worker | `crmnew-twenty-worker` | — | ✅ running |
|
||||||
|
| bridge(业务工具 / RPC 集成) | `crmnew-bridge` | `4100` | ✅ healthy |
|
||||||
|
| PostgreSQL 18 + pgvector | `crmnew-postgres` | `127.0.0.1:5440` | ✅ healthy |
|
||||||
|
| Redis 8.8 | `crmnew-redis` | `127.0.0.1:6390` | ✅ healthy |
|
||||||
|
| IMAP ID 注入代理(163 专用) | `crmnew-imap-proxy` | `127.0.0.1:1993` | ✅ running |
|
||||||
|
|
||||||
|
数据卷:`crm-new_pg-data`、`crm-new_redis-data`、`crm-new_twenty-data`(`docker compose down` 不会删)。
|
||||||
|
|
||||||
|
## 2. 访问地址与账号
|
||||||
|
|
||||||
|
| 入口 | 地址 | 账号 |
|
||||||
|
|---|---|---|
|
||||||
|
| LibreDesk(工作台 / 帮助中心 / widget) | https://wangliang-crm.dev.gaia888.com | `System` /密码见下方命令 |
|
||||||
|
| Twenty CRM | https://wangliang-twenty.dev.gaia888.com | `admin@yehwang.com` /密码见下方命令 |
|
||||||
|
| bridge(调试/集成) | https://wangliang-crmbridge.dev.gaia888.com | 请求头 `X-Tools-Key`,值见下方命令 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /data/dev/wangliang/crm-ai
|
||||||
|
grep '^LIBREDESK_SYSTEM_USER_PASSWORD=' .env # LibreDesk System 密码
|
||||||
|
grep '^TWENTY_ADMIN_PASSWORD=' .env # Twenty 管理员密码
|
||||||
|
grep '^TOOLS_SHARED_KEY=' .env # bridge 工具密钥
|
||||||
|
```
|
||||||
|
|
||||||
|
> 密钥只存在 `.env`(600)与 `libredesk/config/config.toml`(600),两者都已被 `.gitignore` 排除,**不要提交**。
|
||||||
|
|
||||||
|
## 3. 与仓库默认配置的差异(重要)
|
||||||
|
|
||||||
|
| 项 | 仓库默认 | 本环境 | 原因 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `LIBREDESK_IMAGE` | `libredesk/libredesk:v2.9.0-rc.10` | `libredesk/libredesk:v2.9.0-rc.10-**arm64**` | 宿主是 ARM64,`v2.9.0-rc.10` 只有 amd64;官方另发了 `-arm64` 同版本 tag,实测原生 `linux/arm64` 运行 |
|
||||||
|
| `LIBREDESK_BASE_URL` | `http://localhost:9000` | `https://wangliang-crm.dev.gaia888.com` | 走宿主 nginx 的 HTTPS |
|
||||||
|
| `TWENTY_SERVER_URL` | `http://192.168.31.61:3100` | `https://wangliang-twenty.dev.gaia888.com` | 前端 `REACT_APP_SERVER_BASE_URL` 用的就是它(已核对页面内联配置) |
|
||||||
|
| `RECOMMEND_URL` | 测试服地址 | `jsonrpc://wangliang-dev:9503` | 用 **本工作区** 的 product-service |
|
||||||
|
| `ORDER_API_URL` | 测试服地址 | `jsonrpc://wangliang-dev:9505` | trading-service |
|
||||||
|
| `INVENTORY_API_URL` | 测试服地址 | `jsonrpc://wangliang-dev:9509` | eta-service |
|
||||||
|
| `MEMBER_RPC_URL` | 测试服地址 | `jsonrpc://wangliang-dev:9506` | member-service |
|
||||||
|
| `IDENTITY_DEV_MODE` | `false` | `true` | dev 环境方便 widget 联调;**上生产前改回 `false`** |
|
||||||
|
| `PRODUCT_TIMEOUT_MS` | 未透传(bridge 默认 15s) | `30000`(compose 增加透传) | 冷门长查询会偶发 >15s,超时会降级成无价格的站点地图结果,模型易编价 |
|
||||||
|
| `public-gateway` / `cloudflared` | compose 里默认启动 | **未启动** | 已有宿主 nginx 直出 HTTPS,不需要 Cloudflare 临时隧道 |
|
||||||
|
| bridge 网络 | 只挂 `crm-new_default` | 额外接入 `spug-net` | 直连 `wangliang-dev:9503/9505/9506/9509`,去掉对宿主 socat 转发的依赖 |
|
||||||
|
|
||||||
|
RPC 走 **容器网络直连**:compose 把 `bridge` 同时接入外部网络 `spug-net`(与 `wangliang-dev` 同网络),
|
||||||
|
直接按容器名解析到 `wangliang-dev`,不再依赖宿主 `socat` 24503-24509 转发。容器重建后 DNS 名不变,
|
||||||
|
比 socat 需要按 IP 重建更稳。宿主侧的 `conf/dev/rpc-forward.sh` 仍保留(供宿主/不在 spug-net 的容器使用),
|
||||||
|
但本栈已不依赖它。
|
||||||
|
|
||||||
|
## 4. 已完成初始化
|
||||||
|
|
||||||
|
### LibreDesk
|
||||||
|
- `System` 账号可登录(浏览器实测落到 `/inboxes/assigned`)。
|
||||||
|
- **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` 594 行(523 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 /
|
||||||
|
create_opportunity`。
|
||||||
|
- 默认助理 `Yehwang Assistant`(#1,10 个工具、引用与 handoff 开启)。
|
||||||
|
- Live chat Inbox `Website`(#1,uuid `ad25d7a4-722d-4561-aebf-9d8f60952d09`)。
|
||||||
|
- Automation:新会话自动分配给助理(#1,enabled)。
|
||||||
|
|
||||||
|
### Twenty
|
||||||
|
- Workspace `Yehwang`:`2ff5a56b-95d8-40e1-85cf-529ad3a70ed7`(`ACTIVE`)。
|
||||||
|
- API key 已生成并写入 `.env` 的 `TWENTY_API_KEY`;GraphQL `companies` 查询验证通过。
|
||||||
|
- 浏览器实测可登录,首次进入 `/install-apps`(Twenty 的新工作区引导页,正常现象)。
|
||||||
|
|
||||||
|
### bridge
|
||||||
|
- `/healthz` 正常,10 个工具就绪。
|
||||||
|
- 实测 `search_products` 直接命中**本工作区真实商品数据**(如 `Stainless Steel Y-Chain Necklace` / `$6.73` /
|
||||||
|
`product_id=11079`),说明 `wangliang-dev:9503` 的 JSONRPC 直连链路是通的(工具调用 925ms,source=catalog)。
|
||||||
|
- `search_orders` 未验证身份时按设计返回 `verification_required`。
|
||||||
|
|
||||||
|
## 5. 运维命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /data/dev/wangliang/crm-ai
|
||||||
|
|
||||||
|
docker compose ps # 状态
|
||||||
|
docker compose logs -f --tail=100 libredesk
|
||||||
|
docker compose logs -f --tail=100 twenty-server
|
||||||
|
docker compose logs -f --tail=100 bridge
|
||||||
|
|
||||||
|
docker compose start # 启动已有容器
|
||||||
|
docker compose stop # 停止(保留数据)
|
||||||
|
docker compose restart libredesk # 重启单个服务
|
||||||
|
|
||||||
|
# 改 .env 后(bridge 必做;libredesk 也要重建才会读到新 env)
|
||||||
|
docker compose up -d --force-recreate bridge
|
||||||
|
docker compose up -d --force-recreate libredesk
|
||||||
|
|
||||||
|
# 改 LibreDesk 配置模板后
|
||||||
|
node scripts/render-config.js && docker compose up -d --force-recreate libredesk
|
||||||
|
|
||||||
|
# 备份(DB + Twenty 自带数据都在 PG 里;卷里是上传文件)
|
||||||
|
mkdir -p backups
|
||||||
|
docker exec crmnew-postgres pg_dump -U crmnew -Fc libredesk > backups/libredesk-$(date +%F).dump
|
||||||
|
docker exec crmnew-postgres pg_dump -U crmnew -Fc twenty > backups/twenty-$(date +%F).dump
|
||||||
|
docker exec crmnew-postgres pg_dump -U crmnew -Fc bridge > backups/bridge-$(date +%F).dump
|
||||||
|
|
||||||
|
# 恢复
|
||||||
|
docker exec -i crmnew-postgres pg_restore -U crmnew -d libredesk --clean --if-exists < backups/libredesk-YYYY-MM-DD.dump
|
||||||
|
```
|
||||||
|
|
||||||
|
所有容器都是 `restart: unless-stopped`,Docker 守护进程重启后会自己拉起来。
|
||||||
|
|
||||||
|
## 6. 域名反向代理
|
||||||
|
|
||||||
|
宿主 nginx 新增了**独立**配置文件 `/etc/nginx/conf.d/zz-wangliang-crm.conf`:
|
||||||
|
|
||||||
|
- `wangliang-crm.dev.gaia888.com` → `127.0.0.1:9000`
|
||||||
|
- `wangliang-twenty.dev.gaia888.com` → `127.0.0.1:3100`
|
||||||
|
- `wangliang-crmbridge.dev.gaia888.com` → `127.0.0.1:4100`
|
||||||
|
- 80 → 308 跳 HTTPS;证书复用 `*.dev.gaia888.com`(通配一级域名,三个域名都被覆盖)。
|
||||||
|
|
||||||
|
它**没有改** `devstack.conf` 的 map/正则,因此不影响其它工作区。
|
||||||
|
|
||||||
|
改完后的自检 / 重载(query 用户没有 root,用容器代跑):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --privileged --pid=host redis:7-alpine \
|
||||||
|
sh -c 'nsenter -t 1 -m -u -i -n -p /usr/sbin/nginx -t'
|
||||||
|
docker run --rm --privileged --pid=host redis:7-alpine \
|
||||||
|
sh -c 'nsenter -t 1 -m -u -i -n -p /usr/sbin/nginx -s reload'
|
||||||
|
```
|
||||||
|
|
||||||
|
回滚(只需删掉这个文件并 reload,其它站点不受影响):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v /etc/nginx/conf.d:/c redis:7-alpine rm -f /c/zz-wangliang-crm.conf
|
||||||
|
# 再执行上面的 nginx -t / -s reload
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 尚未配置(需要真实凭据,不是代码问题)
|
||||||
|
|
||||||
|
| 项 | 现状 | 补法 |
|
||||||
|
|---|---|---|
|
||||||
|
| ~~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)~~ | ✅ **已配置**:快照导入 `bridge` 库(594 行),`RAG_PG_URL` 已指向,`search_kb` 可用 | 更新:`FORCE=1 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` |
|
||||||
|
|
||||||
|
> `IDENTITY_DEV_MODE=true` 只适合 dev;上生产前改回 `false`。
|
||||||
|
|
||||||
|
## 8. 验证记录(2026-10-23)
|
||||||
|
|
||||||
|
```text
|
||||||
|
$ curl -o /dev/null -w '%{http_code}' https://wangliang-crm.dev.gaia888.com/health # 200
|
||||||
|
$ curl -o /dev/null -w '%{http_code}' https://wangliang-twenty.dev.gaia888.com/healthz # 200
|
||||||
|
$ curl -o /dev/null -w '%{http_code}' https://wangliang-crmbridge.dev.gaia888.com/healthz # 200
|
||||||
|
$ curl -s https://wangliang-crmbridge.dev.gaia888.com/healthz
|
||||||
|
{"ok":true,"service":"crm-tools","tools":[10 个],"sites":["www","us","eu"]}
|
||||||
|
$ curl -s -X POST http://127.0.0.1:4100/tools/search_products -H 'X-Tools-Key: …' -d '{"query":"stainless steel necklace"}'
|
||||||
|
{"ok":true,"products":[{"name":"Stainless Steel Y-Chain Necklace","price":"$6.73","product_id":11079}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
AI 端到端(`node scripts/e2e-widget.js "Show me 3 stainless steel necklaces with prices and stock."`):
|
||||||
|
助手调用 `search_products`(612ms,source=catalog)并返回真实数据 —— Y-Chain `$6.73`/237、Necklace `$6.85`/186、
|
||||||
|
Necklace Star `$5.70`/110,与 RPC 直查一致(首次调试时的编价问题见第 9 节)。
|
||||||
|
|
||||||
|
浏览器(Playwright 真 Chrome,1440×900):
|
||||||
|
|
||||||
|
- LibreDesk 登录成功 → `/inboxes/assigned`(截图 `docs/screenshots/wangliang-libredesk-inbox.png`)
|
||||||
|
- Twenty 登录成功 → `/install-apps` 引导页(截图 `docs/screenshots/wangliang-twenty-onboarding.png`)
|
||||||
|
- 回归:`wangliang-www` 仍 200,`wangliang-agent` 仍 302(宿主 nginx 未被改坏)
|
||||||
|
|
||||||
|
资源占用:约 1.9 GB 内存(Twenty worker 约 1.0 GB + server 0.6 GB + PG/Redis/bridge/LibreDesk 约 0.3 GB)。
|
||||||
|
|
||||||
|
## 9. 上线当天修的一个 AI 质量问题(编造价格)
|
||||||
|
|
||||||
|
首次用 `e2e-widget.js` 问“3 条不锈钢项链的价格和库存”时,助手返回了三款**不存在的商品**和虚高价格。
|
||||||
|
排查(临时把 LibreDesk `log_level` 调到 `debug`)定位到:
|
||||||
|
|
||||||
|
1. 模型把用户**整句**(含 "under 10 dollars" 等)直接当成 `search_products` 的 query;
|
||||||
|
2. 这类长查询偶发 >15s(bridge 的 `PRODUCT_TIMEOUT_MS` 默认 15s),超时后走降级:只返回名字、`price=null`;
|
||||||
|
3. 模型面对 `price=null` 时自行编了价格(LibreDesk 的 guardrails 只是提示词,不是硬拦截)。
|
||||||
|
|
||||||
|
修复(已落地并验证):
|
||||||
|
|
||||||
|
- `docker-compose.yml` 给 bridge 透传 `PRODUCT_TIMEOUT_MS`,`.env` 设为 `30000`;
|
||||||
|
- 助理的 instructions 追加严格规则(`scripts/setup-defaults.js` 同步):短关键词查询、工具输出是唯一事实来源、
|
||||||
|
`price=null`/`stock=null` 时必须说“暂不可用”并建议转人工,禁止估算/换算币种。
|
||||||
|
- 复测:同一条问题,工具调用 612ms 返回真实商品,回复价格/库存与 RPC 完全一致。
|
||||||
|
|
||||||
|
⚠️ 仍存在的结构性问题:LibreDesk 内置 AI 的回复**不经过** bridge 的 `输出护栏`(output_guard)。
|
||||||
|
若后续要硬性拦截虚假价格/链接,需要走 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)✅ 已用最新代码解决
|
||||||
|
|
||||||
|
拉取 `origin/main` 之前,本机确实没有旧 `products.kb_docs`:`upgrade-kb-schema.js` 报
|
||||||
|
`password authentication failed for user "postgres"`,`rebuild-kb-index.sh` 报 `No such container: crm-postgres`
|
||||||
|
(这两个脚本是旧栈专用,新版仓库已不依赖它们)。
|
||||||
|
|
||||||
|
最新代码(`f526696` / `81b49c9`)提供了「无旧 bridge」的标准路径,已按 `docs/go-live.md` 6.3 执行:
|
||||||
|
|
||||||
|
1. 导入仓库内置快照:`bash scripts/import-kb-snapshot.sh`
|
||||||
|
→ 导入到 `crmnew-postgres/bridge`,实测 **367 行(296 parent / 71 child)**,与文档预期一致;
|
||||||
|
后续第二批上游把快照升级到 **594 chunks(523 parent / 71 child)**,用 `FORCE=1 bash scripts/import-kb-snapshot.sh` 重导;
|
||||||
|
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:<password>@127.0.0.1:5440/bridge \
|
||||||
|
node scripts/sync-kb-online.js --local-only --force
|
||||||
|
|
||||||
|
# 在线同步官网 FAQ(建议每周一次;官网有 WAF,失败就退回 --local-only)
|
||||||
|
RAG_PG_URL=postgres://crmnew:<password>@127.0.0.1:5440/bridge \
|
||||||
|
node scripts/sync-kb-online.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## 11. 最新代码(origin/main merge `a1d1de0` + `589e5f9`)怎么运行
|
||||||
|
|
||||||
|
最新 `main` 新增了 KB 快照/在线同步、环境差异文档,并去掉了 5.1 对旧栈的依赖。本机已合并并落地。
|
||||||
|
|
||||||
|
### 日常更新
|
||||||
|
|
||||||
|
```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 快照导入 | `FORCE=1 bash scripts/import-kb-snapshot.sh` | ✅ 594 行(523/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。
|
||||||
|
|
||||||
|
### 第二批(merge `589e5f9`):质量/观测/KB 版本
|
||||||
|
|
||||||
|
新增:Prometheus `/metrics`、RAG reranker、citation checker、三级升级、A/B 实验、`agent_trace_steps`(`TRACE_ENABLED`)、
|
||||||
|
`flywheel_samples`、`chat-log`,以及 `kb-audit.js` / `kb-version.js` / `eval-retrieval.js` / `build-site-kb.js`。
|
||||||
|
KB 快照同步升级到 594 chunks。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 重导新版 KB 快照
|
||||||
|
FORCE=1 bash scripts/import-kb-snapshot.sh
|
||||||
|
|
||||||
|
# 观测与质量
|
||||||
|
curl -s http://127.0.0.1:4100/metrics | head
|
||||||
|
PW=$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2)
|
||||||
|
RAG_PG_URL="postgres://crmnew:${PW}@127.0.0.1:5440/bridge" node scripts/kb-audit.js
|
||||||
|
RAG_PG_URL="postgres://crmnew:${PW}@127.0.0.1:5440/bridge" node scripts/eval-retrieval.js
|
||||||
|
RAG_PG_URL="postgres://crmnew:${PW}@127.0.0.1:5440/bridge" node scripts/kb-version.js list
|
||||||
|
```
|
||||||
|
|
||||||
|
本机实测:
|
||||||
|
|
||||||
|
- `kb-audit`:594 行(523/71)、`empty_answers=0`、`missing_tsv=0`;
|
||||||
|
- `eval-retrieval`:**9/9 PASS**;
|
||||||
|
- `kb-version list`:能看到 `kb_docs_v20261023033455` 备份与 `kb_sync_runs` 记录;
|
||||||
|
- AI 端到端:退款/VAT 问题均按 KB 内容回答(如 VAT refund 给出 `helpdesk@yehwang.com` 与 proof of export 流程)。
|
||||||
|
|
||||||
|
⚠️ 两点范围说明:
|
||||||
|
|
||||||
|
1. `/metrics` 的 `bridge_ai_*` 计数只在 **bridge agent 路径**(webhook 编排)里累加;LibreDesk 内置 AI 不走这条路径,
|
||||||
|
所以当前 `/metrics` 主要只有 uptime/RSS,`bridge_ai_metrics` 也仍是 0 行。
|
||||||
|
2. `build-site-kb.js` 直接 fetch 官网 sitemap,和 `build-site-map.js` 一样会被 WAF 挑战页挡住;
|
||||||
|
如需从本地 sitemap 重新生成 `bridge/kb/11-site-map.md`,要先给脚本加本地 XML 回退(`build-site-map.js` 已有同款逻辑)。
|
||||||
|
|
||||||
|
本地在这一批上补了两个小修复(未推送):
|
||||||
|
|
||||||
|
- `search_kb` 在没有 reranker 时会忽略 `limit`(上游改为先取 8 条给 reranker)→ 现在无 reranker 时尊重 `limit`;
|
||||||
|
- parent/child 可能命中同一篇 → 按 `question+answer` 去重后再重排;
|
||||||
|
- 同时把 `createLLM + createReranker` 接进 HTTP 工具入口,让 LibreDesk 直接调 `search_kb` 也能享受重排。
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -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) {
|
||||||
|
|||||||
@@ -81,7 +81,13 @@ const INSTRUCTIONS = `You are the Yehwang B2B customer assistant for stainless-s
|
|||||||
- For stock / pre-order questions, call search_inventory with the SKU digits.
|
- For stock / pre-order questions, call search_inventory with the SKU digits.
|
||||||
- When you know company + contact name + product/need, call create_opportunity once.
|
- When you know company + contact name + product/need, call create_opportunity once.
|
||||||
- If you cannot help, or the customer asks for a human, hand off to a human agent.
|
- If you cannot help, or the customer asks for a human, hand off to a human agent.
|
||||||
- Never reveal another customer's data.`;
|
- Never reveal another customer's data.
|
||||||
|
|
||||||
|
Tool usage rules (strict):
|
||||||
|
- When calling search_products, pass a SHORT keyword phrase of 2-5 words (e.g. "stainless steel necklace"). Never pass the customer's full sentence, price limits ("under $10"), quantities or filler words ("show me", "please"). This keeps catalog search fast and accurate.
|
||||||
|
- The tool output is the ONLY source of product names, prices, stock and URLs. Never invent or "fill in" a product, price, SKU or link that is not in the tool output.
|
||||||
|
- If a returned product has price=null or stock=null, tell the customer that price/stock is unavailable for now; do NOT estimate, convert currency or guess. Offer to connect a human for an exact quote.
|
||||||
|
- If search_products returns note/catalog_error saying prices are unavailable, list only the returned names/links and explicitly say prices need confirmation.`;
|
||||||
|
|
||||||
const GUARDRAILS = `Never invent prices, discounts, stock or delivery dates.
|
const GUARDRAILS = `Never invent prices, discounts, stock or delivery dates.
|
||||||
Never share another customer's order or personal data.
|
Never share another customer's order or personal data.
|
||||||
|
|||||||
Reference in New Issue
Block a user