docs: spec 补 12 条工程执行项(上传挂载卷/知识库上限/备份backupAPI/迁移保状态/日志封顶/零同步IO/WS限流/WAL池/保留策略/安全四项/运维三项/告警二期)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-12 14:43:43 +08:00
parent 026eb056ce
commit 163bd3900a
@@ -33,6 +33,7 @@
- 提示词沙箱试聊(用草稿配置开真实对话预览)——需要"草稿态"整套机制,二期;现阶段版本回滚 + 出厂重置够用。 - 提示词沙箱试聊(用草稿配置开真实对话预览)——需要"草稿态"整套机制,二期;现阶段版本回滚 + 出厂重置够用。
- 换 PostgreSQL、超管/租户之外更细的角色权限、租户自助注册、计费——量级不到。 - 换 PostgreSQL、超管/租户之外更细的角色权限、租户自助注册、计费——量级不到。
- 引擎行为重构(conversation.py 结构重写)——只做租户化依赖注入,不动结构。 - 引擎行为重构(conversation.py 结构重写)——只做租户化依赖注入,不动结构。
- 推送失败自动告警——先靠仪表盘推送成功率卡片人工观察,二期视需要再加。
## 3. 已确认的关键决策 ## 3. 已确认的关键决策
@@ -121,6 +122,11 @@ platform_settings(key, value) -- 平台级默认:集团 AI 凭据、默认 S
- **external_key 前缀全局唯一(E1)**:数据库约束 + 保存校验,防 CRM 线索空间串号。 - **external_key 前缀全局唯一(E1)**:数据库约束 + 保存校验,防 CRM 线索空间串号。
- **出厂档案**:`seed_profiles/auto/`、`seed_profiles/jiancai/`(提示词 md + 词表 json + 品牌 json),由提取脚本从两个老项目生成;新租户创建时选出厂档案或空白。 - **出厂档案**:`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. 引擎改造与代码合并
### 6.1 TenantContext——重构主轴 ### 6.1 TenantContext——重构主轴
@@ -155,6 +161,12 @@ class TenantRegistry:
| rate_limit.py | key=ip+device | key=(tenant_id, ip, device) | | rate_limit.py | key=ip+device | key=(tenant_id, ip, device) |
| admin 路由 | 内存 token + 单密码 | platform.db 认证 + 会话超时 + 登录失败限流 | | 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 为主线) ### 6.2 合并清单(jc 为主线)
**取自 jc**(10-08 两条分叉线中更完整一方): **取自 jc**(10-08 两条分叉线中更完整一方):
@@ -174,6 +186,7 @@ class TenantRegistry:
1. widget.js:启动时 `fetch /api/public/brand`(Host 已识别租户,免鉴权返回品牌 JSON,**响应携带 tenant slug**)→ 与 data-* 合并(data-* 优先);失败时 data-*/中性默认兜底;no-cache。 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 同理——同页双挂件根治。 2. 客户端 key 全部租户化(`<tenant>` 取 brand 响应里的 tenant slug):`cc-widget-host-<tenant>`、`widget_invite_dismissed_<tenant>_<path>`、chat 页 localStorage 会话 key 同理——同页双挂件根治。
3. widget.js 与 chat.html 保持零构建原生 JS(嵌入官网资产必须轻量零依赖);主题派生单元机制不变,默认值来自品牌配置。 3. widget.js 与 chat.html 保持零构建原生 JS(嵌入官网资产必须轻量零依赖);主题派生单元机制不变,默认值来自品牌配置。
4. WS 通道补限流:现有限流只覆盖 REST 消息接口;平台化后单 IP 可对所有租户域名各开一圈 WS 耗尽单进程资源——加每 IP 并发 WS 连接上限(如 10,内存计数,超限拒绝)。
### 6.4 目标目录结构 ### 6.4 目标目录结构
@@ -207,11 +220,13 @@ all-kefu/
- `admin/` 独立 SPA;开发时 vite dev proxy 到 FastAPI;生产两阶段 Docker(node 阶段 pnpm build → 产物拷入 Python 镜像),服务器零 node 依赖。 - `admin/` 独立 SPA;开发时 vite dev proxy 到 FastAPI;生产两阶段 Docker(node 阶段 pnpm build → 产物拷入 Python 镜像),服务器零 node 依赖。
- admin 静态文件打进镜像(延续"改前端必须 build"的既有教训);由 FastAPI 在管理域名下同源服务,无 CORS。 - admin 静态文件打进镜像(延续"改前端必须 build"的既有教训);由 FastAPI 在管理域名下同源服务,无 CORS。
- 只有 admin 走前端工程化;widget/chat 保持原生 JS。 - 只有 admin 走前端工程化;widget/chat 保持原生 JS。
- admin SPA 用 history 路由;FastAPI 仅在**管理域名**下做 index.html 回退,不影响租户域名路径。
### 7.2 认证与权限 ### 7.2 认证与权限
- admin_users 表(超管 tenant_id 为空);token + 会话超时 + 登录失败限流 + 超管强制改初始密码。 - admin_users 表(超管 tenant_id 为空);token + 会话超时 + 登录失败限流 + 超管强制改初始密码。
- art-design-pro 路由守卫/权限菜单映射两级角色:菜单级权限(超管可见租户管理/平台配置)+ 后端强制的数据级隔离(租户查询自动 scope)。 - art-design-pro 路由守卫/权限菜单映射两级角色:菜单级权限(超管可见租户管理/平台配置)+ 后端强制的数据级隔离(租户查询自动 scope)。
- admin_users 密码用 bcrypt/argon2 哈希;SPA token 走 Authorization 头而非 Cookie(规避 CSRF 整类问题)。
### 7.3 页面清单 ### 7.3 页面清单
@@ -235,11 +250,13 @@ all-kefu/
后台 SPA 走 `/api/platform/*`(新增)与租户化 admin 数据端点;聊天引擎端点(`/ws/chat`、`/api/chat/*`、`/api/public/brand`)契约不变——widget 嵌入方完全无感。 后台 SPA 走 `/api/platform/*`(新增)与租户化 admin 数据端点;聊天引擎端点(`/ws/chat`、`/api/chat/*`、`/api/public/brand`)契约不变——widget 嵌入方完全无感。
`/api/public/brand` 用**显式字段白名单**序列化(只吐品牌字段,绝不序列化含密钥的整个配置 JSON);该端点被客户官网跨域 fetch,响应需带 `Access-Control-Allow-Origin: *`(与 widget.js 现行 CORS 策略一致)。
## 8. 数据迁移 ## 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/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 条会话抽查)。 **scripts/import_legacy.py**:老库 5 张表原结构导入对应租户库;**保留原 conversation_id**(external_key 连续,UniService 不产生重复线索);session_id/page_token/status 等字段全部原样保留(切换时刻客户凭页面令牌无缝续聊);老库 status=pending 的推送日志导入即标 failed 并加注释(切换前未完成态,新平台不重试);导入后对账报告(两会话数/消息数/推送日志数 + 最近 10 条会话抽查)。
**切换顺序**:新平台部署验证(不切域名)→ 导存量 → 租户域名反代从老容器切到新平台 → 观察一周(沿用 breach_rate 双向偏差观察法)→ 老项目归档下线。 **切换顺序**:新平台部署验证(不切域名)→ 导存量 → 租户域名反代从老容器切到新平台 → 观察一周(沿用 breach_rate 双向偏差观察法)→ 老项目归档下线。
@@ -251,10 +268,13 @@ chat2.agent.hnxinyouhe.com ─┼→ 1Panel 各站点反代 → 127.0.0.1:8018
admin.xxx.com(新,具体值部署前由需求方确定)─┘ admin.xxx.com(新,具体值部署前由需求方确定)─┘
``` ```
- 一个容器一个 upstream;compose 固定 `name: all-kefu`(互删根因根治——只剩一个 compose 项目);WS 升级头单套(426 教训);`/static/widget.js` no-cache(缓存教训);`--workers 1`。 - 一个容器一个 upstream;compose 固定 `name: all-kefu`(互删根因根治——只剩一个 compose 项目);WS 升级头单套(426 教训);`/static/widget.js` no-cache(缓存教训);`--workers 1`;容器 `TZ=Asia/Shanghai`(仪表盘"今日"边界与业务时区一致)。
- 日志: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 秒切回上一版,流程写入部署文档。
- 加租户 = 后台建租户 + 1Panel 加一条反代,不起容器。 - 加租户 = 后台建租户 + 1Panel 加一条反代,不起容器。
- 备份:platform.db(敏感件)+ tenants/*.db 每日拷贝 + SQLite backup API 快照,写入部署文档。
- `/health` 升级:进程状态 + 各租户 engine 状态 + 域名表版本。
## 10. 测试策略 ## 10. 测试策略
@@ -273,6 +293,7 @@ admin.xxx.com(新,具体值部署前由需求方确定)─┘
| 单进程升级影响全部租户 | 秒级重启可接受;engine/ 与 platform/ 隔离保留拆分路径 | | 单进程升级影响全部租户 | 秒级重启可接受;engine/ 与 platform/ 隔离保留拆分路径 |
| 密钥入库 | 掩码显示、platform.db 按敏感件备份 | | 密钥入库 | 掩码显示、platform.db 按敏感件备份 |
| 后台暴露面变大 | 登录失败限流、超管强制改初始密码、会话超时、admin 独立域名 | | 后台暴露面变大 | 登录失败限流、超管强制改初始密码、会话超时、admin 独立域名 |
| 事件循环被同步 IO 阻塞(单进程生死线:一个租户的慢调用卡住所有租户) | 零同步 IO 纪律:智谱流式走 executor、推送 HTTP 异步化、SMTP 已异步;合并时逐点审计 |
## 12. 附录:两项目差异实证(配置 Schema 的来源) ## 12. 附录:两项目差异实证(配置 Schema 的来源)