17 KiB
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,值见下方命令 |
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-v41024 维(embedding),configure-ai.js的两项连通性测试均通过。密钥只存在 LibreDesk 库内与.env(600),未写入文档。 - 助理指令已加固(见第 9 节):
search_products必须用 2-5 个词的关键词、不得编造价格/商品,price=null时明确说“暂不可用”。 - 知识库已接入:
bridge库kb_docs367 行(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 / create_opportunity。 - 默认助理
Yehwang Assistant(#1,10 个工具、引用与 handoff 开启)。 - Live chat Inbox
Website(#1,uuidad25d7a4-722d-4561-aebf-9d8f60952d09)。 - Automation:新会话自动分配给助理(#1,enabled)。
Twenty
- Workspace
Yehwang:2ff5a56b-95d8-40e1-85cf-529ad3a70ed7(ACTIVE)。 - API key 已生成并写入
.env的TWENTY_API_KEY;GraphQLcompanies查询验证通过。 - 浏览器实测可登录,首次进入
/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. 运维命令
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:9000wangliang-twenty.dev.gaia888.com→127.0.0.1:3100wangliang-crmbridge.dev.gaia888.com→127.0.0.1:4100- 80 → 308 跳 HTTPS;证书复用
*.dev.gaia888.com(通配一级域名,三个域名都被覆盖)。
它没有改 devstack.conf 的 map/正则,因此不影响其它工作区。
改完后的自检 / 重载(query 用户没有 root,用容器代跑):
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,其它站点不受影响):
docker run --rm -v /etc/nginx/conf.d:/c redis:7-alpine rm -f /c/zz-wangliang-crm.conf
# 再执行上面的 nginx -t / -s reload
7. 尚未配置(需要真实凭据,不是代码问题)
| 项 | 现状 | 补法 |
|---|---|---|
| ✅ 已配置(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 |
✅ 已配置:快照导入 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 |
IDENTITY_DEV_MODE=true只适合 dev;上生产前改回false。
8. 验证记录(2026-10-23)
$ 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)定位到:
- 模型把用户整句(含 "under 10 dollars" 等)直接当成
search_products的 query; - 这类长查询偶发 >15s(bridge 的
PRODUCT_TIMEOUT_MS默认 15s),超时后走降级:只返回名字、price=null; - 模型面对
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:自动建表成功。
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>)。
[site-map] www: total=32710, products=32070, nav=640
[site-map] us: total=22644, products=22324, nav=320
表 bridge_site_map_urls 已核对:
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 个 」,不会静默写空表。
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 执行:
- 导入仓库内置快照:
bash scripts/import-kb-snapshot.sh→ 导入到crmnew-postgres/bridge,实测 367 行(296 parent / 71 child),与文档预期一致; .env增加RAG_PG_URL=postgres://crmnew:***@postgres:5432/bridge,docker compose up -d --build --force-recreate bridge;- 修了一个上游接线 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)。 - 验证:
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_docsparent 记录一致;
- 新索引器实测:
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仍正常。
维护方式:
# 从 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)怎么运行
最新 main 新增了 KB 快照/在线同步、环境差异文档,并去掉了 5.1 对旧栈的依赖。本机已合并并落地。
日常更新
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。