Files
ai-kefu-admin/docs/superpowers/specs/2026-10-12-unified-kefu-platform-design.md
T

20 KiB
Raw Blame History

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_<path>)与 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
  admin.xxx.com ────────────→ 统一后台(超管+租户登录)│
                        └─────────────────────────────────────────┘
  • 一个进程:同时承载所有租户的聊天引擎 + 统一管理后台,一个 Docker 容器部署在现有阿里云 ECS(1Panel)上。
  • 租户识别 = Host:数据库存"域名 → 租户"映射;加租户 = 后台加记录 + DNS/反代指过来。
  • 数据分层:platform.db = 租户注册表、域名映射、admin_users、配置版本快照、平台级默认(集团 AI key、默认 SMTP);tenants/<tenant_id>.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)、零号码替换句、触发词正则(边界/预算/索要)、防误杀白名单(建材 DN/PN/SDR vs 汽车 km/年款) 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)

tenants(id, slug, name, status, created_at)
tenant_domains(domain UNIQUE, tenant_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 条目。进行中的会话下一条消息即用新配置(提示词逐轮拼装,天然支持)。不重启、无分发协议——单进程架构红利。

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 线索空间串号。
  • 出厂档案:seed_profiles/auto/、seed_profiles/jiancai/(提示词 md + 词表 json + 品牌 json),由提取脚本从两个老项目生成;新租户创建时选出厂档案或空白。

6. 引擎改造与代码合并

6.1 TenantContext——重构主轴

现状服务层是"进程级全局"(import 时读提示词、全局 Settings、模块级正则、全局 engine)。改造为"随租户走":

@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 认证 + 会话超时 + 登录失败限流

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)→ 与 data-* 合并(data-* 优先);失败时 data-*/中性默认兜底;no-cache。
  2. 客户端 key 全部租户化(<tenant> 取 brand 响应里的 tenant slug):cc-widget-host-<tenant>、widget_invite_dismissed_<tenant>_<path>、chat 页 localStorage 会话 key 同理——同页双挂件根治。
  3. widget.js 与 chat.html 保持零构建原生 JS(嵌入官网资产必须轻量零依赖);主题派生单元机制不变,默认值来自品牌配置。

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 包可整体拎走。

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。

7.2 认证与权限

  • admin_users 表(超管 tenant_id 为空);token + 会话超时 + 登录失败限流 + 超管强制改初始密码。
  • art-design-pro 路由守卫/权限菜单映射两级角色:菜单级权限(超管可见租户管理/平台配置)+ 后端强制的数据级隔离(租户查询自动 scope)。

7.3 页面清单

页面 超管 租户管理员
仪表盘 租户卡片墙(今日会话/线索/推送成功率,ATTACH 跨库聚合)+ 平台总览 只看自己指标
租户管理 创建向导(名称→域名→选出厂档案 auto/jiancai/空白→生成租户管理员)、停用/启用、域名绑定 ✗
会话与线索 顶部租户切换器,看所有租户会话/画像/推送日志 只看自己的
配置中心 编辑任何租户全部配置组 编辑自己全部配置组(全开)
版本历史 快照列表、两版对比、一键回滚(回滚生成新版本) 同
平台配置 集团默认 AI 凭据、默认 SMTP、超管改密 ✗

7.4 配置编辑形态

  • 品牌:表单(颜色选择器、头像上传、文案)+ 挂件实时预览。
  • 提示词:markdown 编辑器(语法高亮),侧栏显示 $DEFLECTION_POOL 等注入变量当前值,保存必须留 comment。
  • 词表:结构化表格(每条正则一行:语言组/表达式/备注)+ 编译校验 + 内嵌试金石。
  • 邮件:SMTP 表单 + 发测试邮件;推送:目标配置 + 连通性探测(只验鉴权,不发真数据)+ 前缀唯一校验。

7.5 API 契约

后台 SPA 走 /api/platform/*(新增)与租户化 admin 数据端点;聊天引擎端点(/ws/chat、/api/chat/*、/api/public/brand)契约不变——widget 嵌入方完全无感。

8. 数据迁移

scripts/extract_config.py:从两个老项目提取首版配置快照,生成 seed_profiles/auto/ 与 seed_profiles/jiancai/(提示词拷 md;词表从 boundary_filter.py/conversation.py 提取正则话术转 JSON;品牌从 chat.html/widget.js/chat.js 三处硬编码 + .env 归一)。此后两个老项目只读。

scripts/import_legacy.py:老库 5 张表原结构导入对应租户库;保留原 conversation_id(external_key 连续,UniService 不产生重复线索);导入后对账报告(两会话数/消息数/推送日志数 + 最近 10 条会话抽查)。

切换顺序:新平台部署验证(不切域名)→ 导存量 → 租户域名反代从老容器切到新平台 → 观察一周(沿用 breach_rate 双向偏差观察法)→ 老项目归档下线。

9. 部署

chat.agent.hnxinyouhe.com  ─┐
chat2.agent.hnxinyouhe.com ─┼→ 1Panel 各站点反代 → 127.0.0.1:8018 → all-kefu 容器
admin.xxx.com(新,具体值部署前由需求方确定)─┘
  • 一个容器一个 upstream;compose 固定 name: all-kefu(互删根因根治——只剩一个 compose 项目);WS 升级头单套(426 教训);/static/widget.js no-cache(缓存教训);--workers 1。
  • 加租户 = 后台建租户 + 1Panel 加一条反代,不起容器。
  • 备份:platform.db(敏感件)+ tenants/*.db 每日拷贝 + SQLite backup API 快照,写入部署文档。
  • /health 升级:进程状态 + 各租户 engine 状态 + 域名表版本。

10. 测试策略

  1. 机制测试全保留:jc 的全部机制测试(约 320 个用例,实测计数;jc 文档旧称 230 项)+ 上游独有测试,移植到统一 conftest——租户夹具(临时 platform.db + auto/jc 两个种子租户 + 各自租户库),断言基本不动;LLM 全 mock 原则不变。
  2. 多租户专项(改造的回归安全网):Host 路由(A 域名进 A、未知 Host 404);跨租户数据隔离(A 的 admin/API 摸不到 B 数据);配置热切换(保存新提示词→下一条消息生效;改词表→墙行为即时变);前缀唯一约束;快照深合并默认值;试金石判定;widget 客户端 key 租户前缀(静态断言);停用租户行为。
  3. 真实 LLM 验收:真实智谱 Key + scripts/mock_uniservice.py 联调流程照旧(沿用 test-checklist 体系)。

11. 风险与对策

风险 对策
conversation.py(1250 行核心编排器)租户化回归 230 项机制测试安全网 + 只做依赖注入不重写
admin 引入 node 构建链 两阶段 Docker 隔离,服务器零 node;widget/chat 零构建
租户改坏提示词/词表 版本回滚 + 出厂重置 + 编译校验 + 试金石;话术池单一事实源消除双改遗漏
迁移导致 CRM 重复线索 保留原 conversation_id + external_key 前缀不变
单进程升级影响全部租户 秒级重启可接受;engine/ 与 platform/ 隔离保留拆分路径
密钥入库 掩码显示、platform.db 按敏感件备份
后台暴露面变大 登录失败限流、超管强制改初始密码、会话超时、admin 独立域名

12. 附录:两项目差异实证(配置 Schema 的来源)

差异点 ai-kefu(auto) ai-kefu-jc(建材)
提示词 4 件套 汽车语义 管道建材语义(多语言墙、规格防误杀)
端口 / DB / 容器 8016 / cc_ai_kefu.db / cc-ai-kefu 8017 / hesupipe_ai_kefu.db / hs-ai-kefu
品牌色 / 发件人 橙 #f08a31 / 【XYH】 蓝 #066fc0 / 【HESUPIPE】
context 种子键 vehicle_name/vehicle_id product_name/product_id
推送键前缀 ai-kefu ai-kefu-jc
cc 实现 whatsapp-cc-completion(旧) cc_completion 判定树(新,主线)
独有功能 input-placeholder 配置、blog.html(弃) dual-contact、CC 强制墙、阿语数字归一
Redis 有容器但代码零引用(弃) 分叉时已移除