# crm-new **完全新建**的一套 CRM 客服栈:LibreDesk(平台 + 内置 AI)+ Twenty(CRM)+ bridge(业务工具/集成)。 与旧项目 `/home/dev/code/crm/crm-ai-demo` 完全隔离:独立目录、独立 Postgres/Redis、独立端口,不迁旧数据。 ## 当前状态(2026-10-21) | 组件 | 状态 | |---|---| | LibreDesk v2.9.0-rc.10 | ✅ 运行中 :9000(System 密码见 `.env`) | | Twenty v2.30.1(全新实例) | ✅ 运行中 :3100,数据库迁移完成(71 张表) | | bridge | ✅ 运行中 :4100,4 个工具,单测 18/18 通过 | | Postgres / Redis | ✅ :5440 / :6390 healthy | | AI Provider | ✅ 已配置(DashScope `qwen-plus-latest` + `text-embedding-v4` 1024 维),测试通过 | | 知识库向量 | ✅ 108 篇文章已索引(embeddings=108) | | AI Tools | ✅ 4 个工具已注册(search_products / search_orders / search_inventory / create_opportunity) | | RAG/引用验证 | ✅ 已验证:问题命中文章并返回编号引用 | | Live chat Inbox | ✅ `Website`(uuid `10fd5207-d235-4c49-beaa-a65eed0ddd0e`) | | AI Assistant | ✅ `Yehwang Assistant`(4 个工具、引用开启、handoff 开启、max_turns=8) | | Automation | ✅ 新会话自动分配给 Assistant(仅 Website inbox) | | 端到端验证 | ✅ widget 发起 → 自动分配 → AI 回复(带文章引用);商品问题已触发 bridge `search_products` | | Twenty Workspace | ✅ 已创建(admin@yehwang.com,workspace 已 ACTIVE) | | Twenty API Key | ✅ 已在库内生成并写入 `.env`,GraphQL 验证通过 | | 商机写入 | ✅ `create_opportunity` 实测创建 company/person/opportunity/note/task | | 业务 RPC | ✅ 本地开发用 SSH 隧道(`scripts/rpc-tunnel.sh`);测试/生产应直连内网 RPC,不需要隧道 | | 邮件通知(OTP) | ✅ 163 SMTP 已配置,实测验证码邮件已送达 163 收件箱 | | Email 渠道 | ✅ 163 收件(imap-proxy 绕过 Unsafe Login)+ AI 回复邮件(模板)已实测 | | IMAP 代理 | ✅ `services/imap-id-proxy`(LibreDesk 不发 IMAP ID,163 会拒绝 SELECT) | | widget JWT | ✅ bridge `/identity` 签发;登录用户端到端验证通过(`search_orders` verified=true) | | 知识库 | ✅ 帮助中心 `yehwang-help`:9 个 collection / 108 篇文章(公开)→ http://localhost:9000/hc/yehwang-help/en | 旧栈 `crm/crm-ai-demo` 不受影响,两套可并行对照。 ## 架构 ``` 访客(Web widget / Email / WhatsApp) │ ▼ LibreDesk :9000 ── 渠道、会话、坐席工作台、内置 AI Assistant、snippets RAG、OTP 验证 │ AI 调用自定义 HTTP tools ▼ bridge :4100 ── 商品/订单/库存查询 + Twenty 商机写入(身份校验在服务端二次执行) │ ▼ Twenty :3100 ── 公司/联系人/商机/备注/任务 ``` 与旧栈的区别:**AI 编排交给 LibreDesk 内置 assistant**,bridge 收缩为业务工具/集成服务。 知道这样会丢失旧桥的评估/飞轮/情绪升级等能力,后续按需在 bridge 里补"质量旁路"(webhook 消费)。 ## 组件与端口 | 组件 | 地址 | 镜像/实现 | |---|---|---| | LibreDesk | http://localhost:9000 | `libredesk:v2.9.0-rc.10`(含 WhatsApp) | | Twenty | http://localhost:3100 | `twentycrm/twenty:v2.30.1`(全新实例) | | bridge | http://localhost:4100 | Node 24,自研 | | Postgres | localhost:5440 | pgvector/pg18,库 `libredesk` / `twenty` / `bridge` | | Redis | localhost:6390 | redis:8.8 | ## 目录 ``` crm-new/ ├── docker-compose.yml # 全栈编排 ├── .env / .env.example # 全栈配置(.env 自动生成密钥) ├── infra/init-databases.sql # 首次初始化三个库 ├── libredesk/ # LibreDesk 配置模板/渲染结果/uploads(legacy/ 为旧单机模式归档) ├── twenty/ # Twenty 部署说明(不需要源码) ├── bridge/ │ └── kb/ # KB 源 Markdown(在线 FAQ 同步会更新这里) # 业务工具服务(原 tools)+ 单测 │ └── config/sites.json # inbox ID → 站点 profile(tenant/语言/类目地图) ├── services/imap-id-proxy/ # IMAP ID 注入代理(163 专用) ├── scripts/ │ ├── bootstrap.sh # 生成 .env + 渲染配置 + 启动 + 健康等待 │ ├── render-config.js │ ├── register-tools.js # 把 4 个工具注册进 LibreDesk(支持 session/API key) │ ├── configure-ai.js # 配置 DashScope Completion + Embedding │ ├── twenty-bootstrap.js # 库里创建 Twenty workspace + API key(不走 UI) │ ├── configure-email.js # 配置 163 SMTP(通知/OTP 邮件) │ ├── add-email-inbox.js # 配置 Email Inbox + 默认外发模板 + Automation │ ├── e2e-widget-jwt.js # 端到端验证 widget JWT + 订单工具 │ ├── rpc-tunnel.sh # 仅本地开发:测试服 RPC 端口 SSH 隧道 │ ├── public-https.sh # 仅本地联调:Cloudflare Quick Tunnel + 白名单网关 │ ├── set-public-url.js # 更新 LibreDesk app.root_url │ ├── import-kb-helpcenter.js # 旧 KB -> 帮助中心文章(幂等) │ ├── verify-rag.js # 临时助手 + preview,验证检索/引用 │ ├── export-kb-snapshot.sh # 导出 kb_docs 快照 │ ├── import-kb-snapshot.sh # 无旧 bridge 时导入 KB 快照 │ ├── sync-kb-online.js # 在线 FAQ 同步 + 重建 KB(不依赖旧 bridge) │ ├── smoke.sh │ └── serve-web.sh # 仅本地预览 demo 页面 ├── web/ # 站点接入:ai-snippet.js + demo.html(widget JWT 演示) ├── docs/ # 架构、LibreDesk 配置、环境差异、上线/运维手册 │ ├── go-live.md # 上线文档(范围/切流/验收/监控/回滚) │ ├── environments.md # 本地开发 vs 测试/生产:RPC/Cloudflare 隧道差异 │ ├── human-routing.md # Email/WhatsApp 人工团队分配 │ └── plan-a.md # 方案 A:bridge 接管 AI 编排进度 ├── deploy/ │ ├── nginx.conf.example │ └── seeds/kb_docs.sql.gz # KB 快照(367 chunks:296 parent + 71 child) services/public-gateway/ # 公网路径白名单反向代理(仅 webhook/帮助中心) ``` ## 快速开始 ```bash cd /home/dev/code/crm-new # 1) 启动全栈(首次 Twenty 迁移 1-3 分钟) bash scripts/bootstrap.sh # 2) 冒烟 bash scripts/smoke.sh ``` > `scripts/rpc-tunnel.sh`、`scripts/public-https.sh`、`scripts/serve-web.sh` 仅本地开发/联调使用;测试/生产环境不需要,见 [docs/environments.md](docs/environments.md)。 启动完成后: 1. **LibreDesk**(`System`,密码见 `.env` 的 `LIBREDESK_SYSTEM_USER_PASSWORD`) - `Admin → AI → Providers`:Completion 用 DashScope `qwen-plus-latest`,Embedding 用 `text-embedding-v4`(1024 维) - 建 Live chat / Email / WhatsApp inbox - 建 AI Assistant + Automation(new_conversation → assign assistant) - 生成 API key 填 `.env`,然后 `node scripts/register-tools.js` 2. **Twenty**(首次访问需注册 workspace) - 注册后在 `Settings → Developers → API Keys` 创建 key,连同 workspaceId 填进 `.env`,再 `docker compose up -d --force-recreate bridge` 3. 回到 LibreDesk 的 Assistant 里勾选 4 个工具,验证一条会话能查单 + 建商机 ## Widget 安装 ```html ``` 登录用户用服务端签的 `userJWT` 绑定身份(见 `docs/libredesk-setup.md` 第 6 节)。 ## 站点接入(widget) ```bash bash scripts/serve-web.sh # http://localhost:8088/demo.html ``` - `web/ai-snippet.js`:设置 `window.YehwangChatConfig` 后引入;有 `siteJWT` 时先调 bridge `/identity` 换 widget JWT - `web/demo.html`:匿名 / 登录身份两种模式的联调页 - 生产:`IDENTITY_DEV_MODE=false`,由站点注入 `siteJWT`;CORS 白名单加真实域名 ## 多站点路由(bridge) `bridge/config/sites.json` 把 LibreDesk inbox 映射到站点 profile: ```json { "defaultSite": "www", "languageIds": { "en": 1, "nl": 2, "de": 3, "es": 4, "fr": 5, "it": 6, "tr": 7, "cn": 10 }, "profiles": { "www": { "site": "www", "tenantId": 2, "language": "en", "searchUrl": "https://www.yehwang.com/search.html?q=", "siteMap": "site-map.json" }, "us": { "site": "us", "tenantId": 2, "language": "en", "searchUrl": "https://us.yehwang.com/search.html?q=", "siteMap": "site-map-us.json" } }, "inboxes": { "1": "www", "2": "www", "3": "www" } } ``` - 工具调用时按 `X-Libredesk-Inbox-ID` 选 profile - RPC 请求会带 `reqContext`:当前默认 `tenantId=2`;不强制币种,金额按 RPC 返回原样展示,`languageId` 按 profile 的 `language` code 映射(`DEFAULT_TENANT_ID` / `DEFAULT_LANGUAGE_ID` 可覆盖) - 商品搜索走真实 RPC(带 `_class` 的 `ReqContext`/`DeviceInfoDto`),RPC 不返回 URL 时退化到官网搜索链接 - 商品兜底用对应站点的 site-map 和搜索地址 - 商机备注会记录 `Site / Tenant / inbox` - 可用 `SITES_JSON` 环境变量整体覆盖 ## 工具清单(bridge) | 工具 | 需要验证 | 作用 | |---|---|---| | `search_kb` | 否 | 旧 pgvector 知识库检索(政策/FAQ),vector-only | | `search_products` | 否 | 商品搜索:价格/库存/图片/product_id/URL;中文词自动映射英文类目 | | `search_orders` | **是** | 订单列表/详情(状态、商品、金额、地址、物流) | | `search_inventory` | 否 | 库存/ETA(优先用 `product_id`) | | `search_email_history` | **是** | 已验证客户自己的邮件历史(旧 `kb_mail_docs`,向量检索,邮箱强制 scope) | | `collect_info` | 否 | 跨轮采集 lead 槽位(company/contact/need/quantity/budget/closeDate) | | `handoff_to_human` | 否 | 私聊备注 + 分配人工用户 | | `send_verification_code` | 否 | 匿名客户邮箱验证码(163 SMTP) | | `verify_email_code` | 否 | 校验验证码,成功后 30 分钟内可查私有数据 | | `create_opportunity` | **是** | 合并槽位后写 Twenty 商机 + Note + 3 天 Task | > `collect_info` 槽位存 Redis DB 3(key `bridge:lead:`,TTL 24h);Redis 不可用时退回进程内存并告警。 ## 运维 环境差异见 [docs/environments.md](docs/environments.md)(RPC 隧道/Cloudflare 隧道仅本地开发用)。 上线清单见 [docs/go-live.md](docs/go-live.md),常用命令见 [docs/runbook.md](docs/runbook.md)(启动/停止/日志/备份/升级/回滚/排错)。 ## 测试 ```bash cd bridge && npm test ``` ## 发送前输出护栏 - 禁止声称已下单/已发邮件/已生成订单号/PDF/已通知团队(没有对应工具) - 回复中的 URL 只允许来自本轮工具结果 + 静态白名单(`/cart`、`/order.html`) - 价格/库存数量必须出现在工具结果里,否则替换为“无法确认”提示 - 命中后自动重写:删除虚假动作句子、替换未验证价格/库存、移除未验证 URL,保留商品/订单等有效内容 - 只有重写后没有有效内容时才回退到固定安全话术 - 命中记录到 `audit.output_guard`(`blocked` + `rewritten`),并打 `agent: output_guard` 日志 ## 关键约束 - **本机无法直连 Docker Hub**:镜像走 `docker.1panel.live` 前缀(`.env` 已按此配置) - **WhatsApp 仅 v2.9 RC 支持**,需要 Meta Cloud API 凭据;RC 版供联调,不直接上生产 - **LibreDesk AI 仍是 beta**,建议低流量验证 - 旧栈(Chatwoot/bridge/Twenty :3000)保持运行,可并行对照,互不影响