Files
ai-kefu-admin/docs/superpowers/specs/2026-10-12-unified-kefu-platform-design.md
T
浅念 befdd48c63 docs: 统计分析页入 spec(漏斗/趋势/分布/推送健康,逐库聚合无新表)+ 计划修 3红3黄
- Task3 TenantConfig 站点化(Site dataclass,消除与 Task5 的结构矛盾)
- Task5 拆 5a(Registry 单元级)/5b(装配切换+conftest 迁移+默认租户过渡兜底)
- conftest teardown dispose_all(Windows 文件锁)
- 提取脚本改 AST 解析(不 import 源模块);REST session 收 ?site=
- resolve=resolve_site 便捷形式注明

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-12 15:52:59 +08:00

333 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
多站共用域名时:挂件带 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/<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)、零号码替换句、触发词正则(边界/预算/索要)、**轮中提醒话术**(边界/索要/语言跟随/已留资/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 全部租户化(`<tenant>` 取 brand 响应里的 tenant slug):`cc-widget-host-<tenant>`、`widget_invite_dismissed_<tenant>_<path>`、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 配置编辑形态
- 品牌:表单(颜色选择器、头像上传、文案)+ 挂件实时预览 + **嵌入代码复制块**(显示本租户 `<script>` 标签与域名,复制即用,替代人肉查嵌入文档)。
- 提示词:markdown 编辑器(语法高亮),侧栏显示 `$DEFLECTION_POOL` 等注入变量当前值,保存必须留 comment;预览用安全渲染(sanitize,防 markdown 内嵌 HTML 注入)。
- 词表:结构化表格(每条正则一行:语言组/表达式/备注)+ 编译校验 + 内嵌试金石。
- 邮件:SMTP 表单 + 发测试邮件;推送:目标配置 + 连通性探测(只验鉴权,不发真数据)+ 前缀唯一校验。
### 7.5 API 契约
后台 SPA 走 `/api/platform/*`(新增)与租户化 admin 数据端点;聊天引擎端点(`/ws/chat`、`/api/chat/*`、`/api/public/brand`)契约不变——widget 嵌入方完全无感。
`/api/public/brand` 用**显式字段白名单**序列化(只吐品牌字段,绝不序列化含密钥的整个配置 JSON);支持 `?site=<slug>` 返回"站点覆盖层 ⊕ 租户默认门面"的合并结果,未带或未知 site 回退租户默认;该端点被客户官网跨域 fetch,响应需带 `Access-Control-Allow-Origin: *`(与 widget.js 现行 CORS 策略一致)。
### 7.6 统计分析页(数据全部来自现有五张表,无新表)
仪表盘是速览卡片,统计页是下钻分析。内容:
- **核心卡片**:会话数、线索数(已留联系方式的会话)、高意向数、推送成功率
- **转化漏斗**:会话 → 留资 → 完成评估 → 推送成功(每级流失可见)
- **趋势图**:按日会话量/线索量(近 7/30/90 天 + 自定义区间)
- **分布**:意向三级占比、来源(widget/app)、**站点(哪个官网带来的量)**、国家 Top10(IP 归国)
- **推送健康**:success / failed / skipped / duplicated 计数
- **筛选**:时间范围 + 站点;超管可跨租户对比与单租户下钻
实现:`/api/platform/stats` 聚合端点(**逐库 SQL 聚合 + 应用层合并**,沿用"不用 ATTACH"的决定);前端用 ECharts(Element Plus 生态标配)。统计 API 归 Phase 2 平台层、页面归 Phase 3 后台——写那两份计划时据此展开任务。
二期不做:墙拦截率统计(需在消息层给墙命中做埋点,现阶段继续用 `scripts/breach_rate.py` 离线观察)。
## 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 不产生重复线索);session_id/page_token/status 等字段全部原样保留(切换时刻客户凭页面令牌无缝续聊);老库 status=pending 的推送日志导入即标 failed 并加注释(切换前未完成态,新平台不重试);导入后对账报告(两会话数/消息数/推送日志数 + 最近 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`;容器 `TZ=Asia/Shanghai`(仪表盘"今日"边界与业务时区一致);mem_limit 768m(单进程承载 admin SPA 服务 + 全部租户引擎,较现在单租户 512m 上调)。
- 日志:compose `logging` 设 json-file + `max-size: 10m` + `max-file: 3` 封顶(Docker 默认不限大小,会涨满磁盘);应用日志只进 stdout,且**不打聊天内容/联系方式等 PII**(库内数据有 admin 权限保护,进日志等于裸奔磁盘)。
- 备份:platform.db(敏感件)+ tenants/*.db 用 SQLite backup API(或 VACUUM INTO)做一致性快照,每日执行;**禁止运行时直接拷 .db 文件**(可能拷到写一半状态)。每季度做一次恢复演练(没验证过的备份等于没有备份)。写入部署文档。
- 数据保留:lead_push_logs.request_payload 存完整对话 JSON、只涨不掉;提供 scripts/archive.py(会话超 12 个月导出归档 + 清理 + VACUUM 回收空间),后台手动触发(不做自动任务,YAGNI)。
- `/health` 分级:进程状态 + 各租户 engine 状态 + 域名表版本;单租户库异常时报告"哪个租户不健康",不整体 500(防 1Panel 误判容器死亡反复重启)。
- 发版回滚:镜像打版本 tag,compose 可 30 秒切回上一版,流程写入部署文档;部署文档整体继承 jc 项目 deploy-production.md 踩坑手册(1Panel include 行、面板位置联动、no-cache 等)。
- 加租户 = 后台建租户 + 1Panel 加一条反代,不起容器。
## 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 独立域名 |
| 事件循环被同步 IO 阻塞(单进程生死线:一个租户的慢调用卡住所有租户) | 零同步 IO 纪律:智谱流式走 executor、推送 HTTP 异步化、SMTP 已异步;合并时逐点审计 |
## 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 | 有容器但代码零引用(弃) | 分叉时已移除 |