# all-kefu 统一 AI 客服平台设计(spec) - 日期:2026-10-12 - 状态:已与需求方逐节确认,待评审 - 来源项目:`D:\refactor\ai-kefu`(Hesu Auto 汽车出口客服,端口 8016)、`D:\refactor\ai-kefu-jc`(HESUPIPE 管道建材客服,端口 8017) - 新项目位置:`D:\refactor\all-kefu`(本仓库) ## 1. 背景与问题 两个现有项目是**同一套 AI 客服引擎的两份拷贝**(2026-09-29 按当时的"方案 A:完整拷贝 + 行业化定制"分叉,约 85% 代码同源)。业务模式相同:AI 只做"需求了解 + 留资获客",价格/交期/MOQ 一律不正面回答、引导留联系方式,线索经邮件 + UniService CRM 推送转人工。技术栈相同:FastAPI + SQLite(aiosqlite) + 智谱 GLM(glm-4-flash)+ 纯静态前端(widget 用 script 标签 + Shadow DOM 嵌入官网)。 复制模式已产生的实际代价(均有实证): 1. **重复劳动**:2026-10-08 同一天,上游实现了 whatsapp-cc-completion,jc 独立实现了更完整的 cc-completion(判定树+状态机,376 行)——同一功能写了两遍且实现分叉。 2. **人肉同步**:上游修 bug 需对照 changelog 手动同步到 jc,规则写在 jc 的 CLAUDE.md 里。 3. **生产事故**:两套 docker-compose 在 1Panel 上项目名撞车(目录都叫 index),起一个容器会删掉另一个(互相 502)。已用 `name:` 修复,但这是"N 份部署"模式的固有风险。 4. **同页冲突**:两个 widget 源自同一代码,`sessionStorage` key(`widget_invite_dismissed_`)与 host id(`cc-widget-host`)同名互扰;集团官网同页挂两个客服时必然出问题。 5. **僵尸配置**:ai-kefu 的 compose 跑着一个 Redis 容器,代码零引用。 每客户的差异只占约 15%(提示词、词表、品牌、环境配置、部署标识、少量代码点),其余 85% 是应该共享的引擎。**每加一个客户复制一套代码不可持续**——本设计把差异收敛为"租户配置",引擎单一份。 ## 2. 目标与非目标 **目标**: 1. 一套代码(all-kefu)承载所有客户(租户),加客户 = 后台建租户 + 填配置 + 加一条反代,零代码复制、不起新容器。 2. 统一管理后台:超管管理所有租户、查看全局数据;租户管理员查看并配置自己租户的一切(含提示词与词表,需求方已确认"租户全开")。 3. 所有配置动态生效(热加载,不重启)。 4. 存量两个项目的数据与线上行为无感迁移(官网嵌入代码零改动、CRM 线索不重复)。 **非目标(明确推迟,YAGNI)**: - 提示词沙箱试聊(用草稿配置开真实对话预览)——需要"草稿态"整套机制,二期;现阶段版本回滚 + 出厂重置够用。 - 换 PostgreSQL、超管/租户之外更细的角色权限、租户自助注册、计费——量级不到。 - 引擎行为重构(conversation.py 结构重写)——只做租户化依赖注入,不动结构。 - 推送失败自动告警——先靠仪表盘推送成功率卡片人工观察,二期视需要再加。 ## 3. 已确认的关键决策 | 决策 | 结论 | 备注 | |---|---|---| | 运行架构 | **单进程多租户**,一个 FastAPI 进程 = 引擎 + 统一后台 | 需求方委托推荐后确认 | | 数据隔离 | 每租户独立 SQLite 文件;平台数据(租户/账号/配置)在 platform.db | 文件级隔离,备份=拷文件 | | 租户识别 | 按 Host 头(域名映射表);官网嵌入代码零改动 | 现有 widget 即从 script.src origin 推导连接地址 | | 后台形态 | 统一后台,超管 + 租户管理员两级登录 | | | 租户权限 | **全开**:租户可改自己租户的一切配置(含提示词、词表) | 配套安全网:版本历史/回滚/试金石/出厂重置 | | 后台前端 | **art-design-pro**(Vue3 + TypeScript + Vite + Element Plus,MIT) | 替代原先"单文件原生 JS 后台"的设想 | | 存量数据 | 导入新平台(写迁移脚本,保留原会话 ID) | | | worker 数 | `--workers 1` 写死 | 内存 token/限流/配置缓存/每租户 engine 缓存均假设单进程 | ## 4. 总体架构 ``` ┌─────────────────────────────────────────┐ │ all-kefu 平台(单进程 FastAPI) │ chat.agent.hnxinyouhe.com ──┐ │ chat2.agent.hnxinyouhe.com ─┼→ 按 Host 识别: ├─ platform.db(租户/站点/域名/账号/配置) ai.hesupipe.com(未来) ──┘ 域名→站点→租户 └─ tenants/auto.db、tenants/jc.db 多站共用域名时:挂件带 data-site 参数选站点 admin.xxx.com ────────────→ 统一后台(超管+租户登录)│ └─────────────────────────────────────────┘ ``` - **一个进程**:同时承载所有租户的聊天引擎 + 统一管理后台,一个 Docker 容器部署在现有阿里云 ECS(1Panel)上。 - **租户识别 = Host,站点在中间**:数据库存"域名 → 站点 → 租户"映射。**站点(site)= 一个租户下的一张门面**:一个租户(一个 AI 脑子 + 一套线索管道)可以挂多个官网,每个官网的 brand(颜色/标题/欢迎语/话题/头像/WhatsApp/邮箱/隐私链接)可各配各的。站点接入两种方式:绑自己的域名(Host 自动识别),或多站共用一个挂件域名 + 嵌入代码 `data-site="es"` 参数选择;**未指定或未知 site 回退租户默认门面**(向后兼容)。页面 data-* 参数仍可覆盖站点门面(优先级:data-* > 站点 > 租户默认)。加租户 = 后台加记录 + DNS/反代指过来。 - **数据分层**:platform.db = 租户注册表、域名映射、admin_users、配置版本快照、平台级默认(集团 AI key、默认 SMTP);tenants/.db = 现有 5 张表(conversations/messages/customer_profiles/evaluations/lead_push_logs),一租户一文件,互不可见。 - **两级登录**:超管(tenant_id 为空)看所有租户数据、增删租户、改任何配置;租户管理员只看自己租户数据、改自己配置。数据级隔离由后端强制。 **边界规则**: - 未知 Host 一律 404,**不做兜底租户**(防 Host 混淆打错租户)。 - admin 后台走独立域名,不依赖 Host 租户解析。 - 停用租户(status=disabled):聊天/公开端点返回中性"服务暂停"页(widget 优雅降级);超管仍可只读其数据;Registry 释放其 DB engine。 ## 5. 租户配置模型 ### 5.1 配置 Schema(两个项目 15% 差异的收敛) | 配置组 | 内容 | 对应现在散落的位置 | |---|---|---| | 基础 | 租户名、启用/停用、**站点列表**(每站点:短代号、名称、绑定域名、门面覆盖层) | compose/openresty 硬编码 | | 品牌(租户默认门面,**站点可逐项覆盖**) | 展示名、主题色、头像、欢迎语、热门话题、输入框占位符、WhatsApp 号、联系邮箱、隐私政策 URL | chat.html / widget.js / chat.js 三处硬编码 | | 提示词 4 件套 | system.md、company_facts.md(知识库)、extractor.md、evaluator.md(markdown) | app/prompts/ | | 安全墙词表 | 价格墙正则组(多语言)、迂回话术池(EN/ZH/ES)、零号码替换句、触发词正则(边界/预算/索要)、**轮中提醒话术**(边界/索要/语言跟随/已留资/CC 指引/邮箱轻补六类) | boundary_filter.py / conversation.py 内嵌 | | 联系方式策略 | 区号补全开关、模板句语言集、轻补邮箱开关 | conversation.py 内嵌参数 | | 邮件 | SMTP(可选"用平台默认")、发件人显示名、通知邮箱列表 | .env + email_service.py 硬编码 | | 推送 | 启用开关、URL、API Key、**external_key 前缀(全局唯一约束)**、超时/重试 | .env | | AI 凭据 | 可选覆盖(不填=平台默认集团 key) | .env | | 引擎杂项 | context 种子键映射(product_name vs vehicle_name)、限流参数、国名映射扩展 | chat_rest.py / cc_completion.py 代码差异 | ### 5.2 存储(platform.db) ```sql tenants(id, slug, name, status, created_at) sites(id, tenant_id, slug, name, status, brand_overlay JSON) -- 站点门面覆盖层(缺省项继承租户 brand) site_domains(domain UNIQUE, site_id) -- 域名→站点(站点→租户随外键) tenant_config_versions(tenant_id, version, config_json, updated_by, comment, created_at) -- 当前生效版 = MAX(version);每次保存存整份快照(几十 KB,不做增量) admin_users(id, tenant_id NULL=超管, username, password_hash, status) platform_settings(key, value) -- 平台级默认:集团 AI 凭据、默认 SMTP ``` 回滚 = 把旧 version 复制成新 version(历史不可变)。提示词与词表在同一快照内,回滚原子。 ### 5.3 生效机制(热加载) 进程内 TenantRegistry:`tenant_id → { config, db_engine, session_factory, manager }`。后台保存 → 校验 → 写 platform.db → **原子替换 Registry 条目**。进行中的会话下一条消息即用新配置(提示词逐轮拼装,天然支持)。不重启、无分发协议——单进程架构红利。并发编辑策略:同一租户被两人同时保存时后保存者覆盖(last-write-wins)——单人维护场景接受,不做乐观锁。 ### 5.4 保存校验 + 试金石(租户可编辑正则的安全网) 1. **编译校验**:每条正则 re.compile 试跑,非法整单拒收。 2. **试金石面板**:编辑页内嵌测试框,贴样例句("What's your price for DN800?"、"预算5000美元")实时显示拦/放/替换判定,发布前眼见为实。 3. **回滚**:版本列表任意旧版一键恢复;每个配置组另有"重置为出厂默认"。 ### 5.5 密钥处理 - 后台读取一律掩码;仅主动修改时明文输入;"用平台默认"状态下租户侧不存密钥。 - platform.db 备份按敏感件对待(含 SMTP/push/AI 密钥),写入部署文档。 ### 5.6 关键设计规则 - **话术单一事实源(R1)**:迂回话术池只存于墙配置;ai_service 组装 system prompt 时以模板变量(`$DEFLECTION_POOL` 等)注入提示词。改一处,提示词与墙同时变——消除"提示词教的说法"与"墙的执法标准"分叉。 - **配置演进(E4)**:Registry 加载 = 快照深合并出厂默认值,引擎升级新增字段不炸旧租户。 - **external_key 前缀全局唯一(E1)**:数据库约束 + 保存校验,防 CRM 线索空间串号。 - **tenant slug 创建后不可变**:客户端存储 key 全部以 slug 命名,改动 slug = 重置所有客户页面的记忆(邀请卡状态、会话令牌)。site slug 同理不可变。 - **站点只覆盖门面**:brand 跟站点走;AI 脑子(提示词/词表/行为开关/AI 凭据)与线索管道(邮件/推送)留租户级——一个租户 = 一个客户经理团队收所有站点的线索,脑子分叉就乱。需要"两套完全不同的 AI"时应建两个租户而非两个站点。会话记录带站点标(后台可按官网筛线索、看各站转化)。 - **出厂档案**:`seed_profiles/auto/`、`seed_profiles/jiancai/`(提示词 md + 词表 json + 品牌 json),由提取脚本从两个老项目生成;新租户创建时选出厂档案或空白。两个概念分清:**重置出厂** = 代码库当前模板(随平台升级演进);**租户创建** = 当时选定档案的快照(不随升级变化)。 ### 5.7 内容与文件上限 - 头像等上传文件存**挂载卷** `data/uploads/`:服务端 uuid 重命名(防路径穿越)+ 类型校验 + 大小 ≤2MB,经接口动态读取——绝不写入镜像静态目录(容器重建即丢,同类教训:改 static 必须 build)。头像按 (租户[, 站点]) 维度解析:站点覆盖 > 租户默认 > 平台兜底图。 - 提示词/知识库保存时校验大小:知识库软上限 50KB,超限拒收并提示精炼——company_facts 每轮整段拼入 system prompt,过大会线性放大每轮 LLM 成本与时延,租户自己无感。 ## 6. 引擎改造与代码合并 ### 6.1 TenantContext——重构主轴 现状服务层是"进程级全局"(import 时读提示词、全局 Settings、模块级正则、全局 engine)。改造为"随租户走": ```python @dataclass class TenantRuntime: tenant_id: str config: TenantConfig # 快照 ⊕ 出厂默认 深合并;词表已编译校验 engine: AsyncEngine # 该租户 SQLite(懒创建,停用时关闭) session_factory: async_sessionmaker manager: ConversationManager class TenantRegistry: resolve(host) -> TenantRuntime # 域名表缓存,后台改域名即失效 reload(tenant_id) # 配置保存后原子换条目 ``` 请求流:中间件按 Host 解析租户 → `request.state` → 路由把 runtime 传进服务。服务层从"读全局"变"接参数"。 逐文件改造点: | 文件 | 现状 | 改造后 | |---|---|---| | config.py | 全局 Settings 读 .env | 拆 ProcessSettings(.env:平台库路径/端口/超管初始密码)+ TenantConfig(platform.db);顺带丢弃 Redis 死配置(REDIS_URL/REDIS_PASSWORD) | | ai_service.py | import 时读 4 提示词 | PromptProvider 挂 TenantRuntime;组装 = system.md + company_facts.md + `$DEFLECTION_POOL` 注入 + 当轮 context reminder | | conversation.py(约 1250 行) | 3 组模块级正则 + 逐字话术 | 正则/话术从 config.wall 编译;Manager 每租户实例化;**只做依赖注入,不重写结构** | | boundary_filter.py | 正则话术焊死 | 纯函数化:接收已编译词表,机制与数据分离 | | email/lead_push | .env 全局配置 | 读租户配置段;启动 sweep_stale_pending 逐租户跑 | | rate_limit.py | key=ip+device | key=(tenant_id, ip, device) | | admin 路由 | 内存 token + 单密码 | platform.db 认证 + 会话超时 + 登录失败限流 | 表外三条硬纪律: - **每租户库打开即 `PRAGMA journal_mode=WAL`**(读写不互斥,优于现在靠 timeout=30s 硬扛);platform.db 同样开启。 - 每租户 engine 连接池收紧至 1-2(SQLite 单文件用不上默认 5 连接;20 租户 × 5 = 100 个句柄纯属浪费)。 - **事件循环零同步 IO**:智谱 SDK 同步流式必须走线程 executor(沿用上游 glm.py 做法);推送 HTTP 改用 httpx.AsyncClient(后台重试的几秒不能卡住全平台);SMTP 已是 aiosmtplib。合并时逐点审计。 ### 6.2 合并清单(jc 为主线) **取自 jc**(10-08 两条分叉线中更完整一方): - cc_completion.py 完整判定树 + 状态机 + extractor cc_reply + e164 出站渲染 - 多语言墙(€/£、西/葡/俄/阿词表、ES 迂回池、建材防误杀白名单) - dual-contact 分步双要(先 WhatsApp 带国家码→轻补邮箱)+ `_email_asked` - CC 强制墙 + 零号码墙回显豁免收窄 - XFF 服务端优先、阿语数字归一、resolve_region_code **从上游吸收**:input-placeholder 可配置化(落进品牌配置组);上游独有测试的断言价值(test_channel_hygiene、test_chat_rest_ip、test_lead_push_cc、test_cc_integration、test_ip_service_region、test_prompt_static 等)。 **不带走**:blog.html(模板残留)、Redis 配置(死代码)、上游旧版 whatsapp-cc-completion(被 jc 版取代,但其测试断言移植过来验证 jc 版行为)。 ### 6.3 前端 1. widget.js:启动时 `fetch /api/public/brand`(Host 已识别租户,免鉴权返回品牌 JSON,**响应携带 tenant slug**;script 标签有 `data-site` 时带 `?site=` 选站点)→ 与 data-* 合并(data-* 优先);失败时 data-*/中性默认兜底;no-cache。 2. 客户端 key 全部租户化(`` 取 brand 响应里的 tenant slug):`cc-widget-host-`、`widget_invite_dismissed__`、chat 页 localStorage 会话 key 同理——同页双挂件根治。 3. widget.js 与 chat.html 保持零构建原生 JS(嵌入官网资产必须轻量零依赖);主题派生单元机制不变,默认值来自品牌配置。 4. WS 通道补限流:现有限流只覆盖 REST 消息接口;平台化后单 IP 可对所有租户域名各开一圈 WS 耗尽单进程资源——加每 IP 并发 WS 连接上限(如 10,内存计数,超限拒绝)。 ### 6.4 目标目录结构 ``` all-kefu/ admin/ # art-design-pro SPA(Vue3+Vite+TS+Element Plus) app/ main.py # 平台入口:租户中间件 + 路由挂载,--workers 1 platform/ # registry、配置服务、校验+试金石、admin 认证、platform.db 模型 engine/ # 合并后的客服引擎(jc+上游),全部租户感知 routes/ # ws/chat、rest/chat(租户作用域) services/ # conversation、boundary_filter、cc_completion、contact_pipeline、 # email、lead_push、ip、rate_limit、ai_service(PromptProvider) providers/ # glm models/ # 租户库 5 张表 + 逐租户 _migrate static/ # widget.js、chat.html、theme(零构建) seed_profiles/ # auto/、jiancai/ tenants/ # 运行期:每租户一个 .db platform.db scripts/ # extract_config.py、import_legacy.py、mock_uniservice tests/ docs/ ``` `platform/` 与 `engine/` 是隔离边界:引擎不感知平台存在,只认 TenantRuntime——将来某租户需独立部署时 engine 包可整体拎走。 运行期产物**绝不进 git**(首次提交前建好 .gitignore):`tenants/*.db`、`platform.db`(含密钥,进了 git 历史就洗不干净)、`data/uploads/`、`admin/node_modules/`、`admin/dist/`。 ## 7. 统一管理后台(art-design-pro) ### 7.1 工程与构建 - `admin/` 独立 SPA;开发时 vite dev proxy 到 FastAPI;生产两阶段 Docker(node 阶段 pnpm build → 产物拷入 Python 镜像),服务器零 node 依赖。 - admin 静态文件打进镜像(延续"改前端必须 build"的既有教训);由 FastAPI 在管理域名下同源服务,无 CORS。 - 只有 admin 走前端工程化;widget/chat 保持原生 JS。 - admin SPA 用 history 路由;FastAPI 仅在**管理域名**下做 index.html 回退,不影响租户域名路径。 ### 7.2 认证与权限 - admin_users 表(超管 tenant_id 为空);token + 会话超时 + 登录失败限流 + 超管强制改初始密码。 - art-design-pro 路由守卫/权限菜单映射两级角色:菜单级权限(超管可见租户管理/平台配置)+ 后端强制的数据级隔离(租户查询自动 scope)。 - admin_users 密码用 bcrypt/argon2 哈希;SPA token 走 Authorization 头而非 Cookie(规避 CSRF 整类问题)。 ### 7.3 页面清单 | 页面 | 超管 | 租户管理员 | |---|---|---| | 仪表盘 | 租户卡片墙(今日会话/线索/推送成功率,**逐库查询 + 应用层合并**——不用 SQLite ATTACH,其默认上限 10 个附加库,租户一多就炸)+ 平台总览 | 只看自己指标 | | 租户管理 | 创建向导(名称→域名→选出厂档案 auto/jiancai/空白→生成租户管理员)、停用/启用、域名绑定 | ✗ | | 会话与线索 | 顶部租户切换器,看所有租户会话/画像/推送日志 | 只看自己的 | | 配置中心 | 编辑任何租户全部配置组 | 编辑自己全部配置组(全开) | | 站点管理 | 租户下站点增删/停用、域名绑定、门面编辑(覆盖层+继承项提示+嵌入代码含 data-site) | 同左(限自己租户) | | 统计分析 | 全租户对比 + 单租户下钻 | 只看自己租户 | | 版本历史 | 快照列表、两版对比、一键回滚(回滚生成新版本) | 同 | | 平台配置 | 集团默认 AI 凭据、默认 SMTP、超管改密 | ✗ | ### 7.4 配置编辑形态 - 品牌:表单(颜色选择器、头像上传、文案)+ 挂件实时预览 + **嵌入代码复制块**(显示本租户 `