Files
ai-kefu-admin/docs/deploy-production.md
T
浅念 9315dba228 docs(deploy): 共享域名模式部署工件
模板:共享域名 server 块注释示例(kefu.example.com 占位,WS 升级头
落 regex location ^/t/[a-z0-9-]+/ws/——路径模式 WS 真实路径带 /t 前缀);
XFF 追加式语义注记(登录限流取末段,勿改覆盖式);--root-path 禁用
边界(root_path 前移 raw_path,路径前缀解析永不命中)。

手册:§3.6 共享域名模式(概念/零 DNS 接入流程/地址形态表/无尾斜杠
404 语义/部署注记三条);§6.5 加租户补共享域名零反代路径;1Panel
注记补 ws.conf regex 形态。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-19 19:44:46 +08:00

497 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 生产部署手册(含切换 runbook)
> 对应工件:`Dockerfile`(两阶段构建)、`docker-compose.yml`(`name: all-kefu`)、
> `deploy/openresty-all-kefu.conf.template`(反代模板)、
> `scripts/{bootstrap_platform,import_legacy,archive,breach_rate}.py`。
> 本文是 **runbook 工件**(Phase 4 交付),不是已执行实录——首次实际部署到阿里云前,
> 按用户全局部署 SOP(`~/.claude/memory/general-deployment-sop.md` + 全局 CLAUDE.md 规程)
> 与用户确认环境与流程后再动手。
>
> 手册继承 jc 项目 `docs/deploy-production.md` 的全部教训类条目(§7 踩坑实录逐条标注来源),
> 部署步骤按本项目实况重写:**单容器多租户**(jc 是每项目一容器),反代从"一站一容器"
> 变为"多域名同 upstream"。
## 0. 线上环境总览(目标形态)
| 项 | 值 |
|---|---|
| 目标服务器 | 阿里云 ECS `47.254.176.10`(Debian + 1Panel,Docker 29.x 满足要求) |
| 应用容器 | `all-kefu-app-1`(宿主端口 `8018:8018`,内存限额 768m,`--workers 1` 硬约束) |
| 租户域名 | `chat.agent.hnxinyouhe.com`(auto)/ `chat2.agent.hnxinyouhe.com`(jiancai)——1Panel 各建一站,全部反代 → `127.0.0.1:8018` |
| 管理后台 | 形态 A(独立管理域名)或形态 B(租户域名下 `/admin` 路径分流),见 §3.3 |
| 代码位置 | 建议 `/opt/all-kefu`(compose 已钉项目名 `all-kefu`,目录名不影响项目名,与 1Panel 站点目录彻底解耦) |
| git 仓库 | `<repo-url>`(**占位——部署时替换**;本仓编写时点未配置 remote) |
| 持久化 | `./data`(platform.db + uploads + archives)、`./tenants`(每租户 `<slug>.db`)两个 bind mount,容器可随手重建 |
**同服务器共存的老项目**(切换期绝对不要删,回滚全靠它们):jc 项目容器
(`chat2.agent.hnxinyouhe.com`,端口 8017)、汽车客服容器(`chat.agent.hnxinyouhe.com`,
端口 8016)。切换 runbook 见 §5;观察期结束前老容器**保留不删**。
## 1. 环境与前置
### 1.1 服务器要求
- **Docker ≥ 24,Docker Compose ≥ 2.24**。Compose 下限是硬要求:本项目
`docker-compose.yml` 的 env_file 用对象式写法(`path: .env` + `required: false`),
2.24 以下直接语法报错。验证:`docker compose version`。
- 服务器**无需 Python / pip / node**——运行时全在镜像内(python:3.13-slim,admin 前端
在构建阶段一由 node:20-alpine 编译,服务器零 node)。`pip install -r requirements.txt`
只是**本地开发**的动作;本地必须装(含 `python-multipart>=0.0.20`——Phase 2 实证缺失
它头像上传直接 500,FastAPI multipart 解析依赖)。
- 磁盘:日志已封顶(json-file 10m×3),主要增长项是 `tenants/*.db` 与
`lead_push_logs.request_payload`(只涨不掉,超 12 个月走归档 §6.3)。
### 1.2 代码获取
```bash
git clone <repo-url> /opt/all-kefu # <repo-url> 部署时替换为真实仓库地址
cd /opt/all-kefu
```
### 1.3 生产 .env 逐项说明
放在仓库根 `./.env`(compose 自动注入容器;`chmod 600 .env`)。缺省也能起
(出厂默认 + 启动日志 WARNING),但生产**必须**提供下面标"必设"的项:
```ini
# ---- 超管引导(bootstrap_platform.py 与应用 lifespan 同源消费)----
ADMIN_USERNAME=admin # 可改;默认 admin
ADMIN_PASSWORD=<强密码> # 必设!默认 admin123——启动检测到默认值会打
# WARNING 日志,且超管首登被强制改密(409 门)
# ---- AI(平台默认层;租户凭据空槽回退到这里)----
AI_PROVIDER=glm
ZHIPU_API_KEY=<智谱 Key> # 必设(聊天引擎无 Key 不可用)
ZHIPU_BASE_URL= # 留空=SDK 默认地址;coding 套餐填
# https://open.bigmodel.cn/api/coding/paas/v4
AI_MODEL=glm-4-flash
# ---- 邮件(平台默认层;租户 config.email 空槽逐键回退)----
SMTP_HOST=<发件主机>
SMTP_PORT=465
SMTP_USER=<发件账号>
SMTP_PASS=<发件密码>
NOTIFY_EMAIL=<兜底询盘通知收件箱> # 租户未配 notify_emails 时的回退
# ---- 询盘推送 UniService(平台默认层;租户 config.push 空槽回退)----
PUSH_ENABLED=false
PUSH_API_URL=https://uniservice.server.hnxinyouhe.com/open/v1/inquiry/push
PUSH_API_KEY=<开启时用>
PUSH_EXTERNAL_KEY_PREFIX=ai-kefu-jc # 平台兜底前缀;两租户已各自配
# ai-kefu(auto)/ ai-kefu-jc(jiancai),
# 与各自老项目线索键**同键空间**(切换/
# 回滚不产生重复线索的依据),勿改;
# 两租户之间互异即可
# ⚠ 推送探测 env 回退(已知项,CLAUDE.md Phase 4 清理单 #2):租户 push URL
# 空槽时回退 env PUSH_API_URL(默认即真 UniService 生产址)——后台凭据页点
# 「探测」会对真实服务发空批量 POST({"messages": []},按契约应为无操作,
# 但属真实出站)。介意则把 env PUSH_API_URL 设为 stub 域名再探测。
# ---- 租户源 ----
TENANT_SOURCE=db # 默认即 db(生产装配:platform.db 注册表);
# file = 降级读 seed_profiles 目录(冷备/本地
# 调试形态,线上非应急勿用)
# ---- 说明 ----
# APP_HOST/APP_PORT 在容器内不生效(Dockerfile CMD 已钉 0.0.0.0:8018);
# APP_DEBUG 当前无消费方(摆设,写了也不起作用);TZ=Asia/Shanghai 由
# compose environment 固定注入,勿动。
```
`.env` 与 `data/`、`tenants/` 都不在 git 里(`.gitignore` 覆盖),发版更新不受影响。
## 2. 首次部署流程
```bash
cd /opt/all-kefu
# 1. 镜像(两阶段构建:node 编译 admin → slim 只带运行时;首次约数分钟)
docker compose build
# 2. 平台引导:建 platform.db schema + 超管账号 + 导入出厂租户(auto + jiancai)
# 幂等可复跑:超管空表才建、已存在 slug 的租户跳过
docker compose run --rm app python scripts/bootstrap_platform.py
# 3. 起容器
docker compose up -d
# 4. 健康检查(分级 JSON,解读见下)
curl -s http://127.0.0.1:8018/health
```
bootstrap 输出应含:`platform.db: /app/data/platform.db`、超管用户名、
`auto: 导入(...)`、`jiancai: 导入(...)`(复跑则显示"跳过(tenants 已存在)")。
platform.db 落在 **data/ bind mount 内**(宿主视角 `/opt/all-kefu/data/platform.db`),
容器重建不丢——这是 P4T1 审查修复项,compose 已落实,勿把库挪回仓库根。
`/health` 分级解读:
```json
{
"status": "ok", // 任一已建租户库探活失败 → "degraded"(仍 200,
// 单租户库损坏不杀容器、不影响其余租户)
"tenants": [
{"slug": "auto", "status": "active", "engine_ok": null},
{"slug": "jiancai", "status": "active", "engine_ok": true}
],
"domains": 3 // 域名绑定总数:auto 1 + jiancai 2
// (chat2.agent.hnxinyouhe.com + localhost.jc,出厂档案如此)
}
```
- `engine_ok: true` = 探活通过;`false` = 该租户库损坏(排查该租户的 db);
`null` = engine 懒建未就绪——**健康检查不触发建库**(设计如此),该租户
来过一次请求后才会变 true,刚启动全是 null 属正常。
- `domains` 是导入完整性的人工核对口径:bootstrap 后应为 **3**(含 localhost.jc)。
浏览器打开 `https://<admin 地址>/admin/`(反代未配时先见 §3;也可临时
`ssh -L 8018:127.0.0.1:8018` 隧道 + `http://127.0.0.1:8018/admin/`),用
`.env` 的超管账号首登——**强制改密**(`must_change_password` 409 门,改完才进)。
租户管理员账号不在 bootstrap 里建:后台"租户管理 → 创建向导/详情"按需补。
## 3. 反代配置(1Panel)
模板:`deploy/openresty-all-kefu.conf.template`(完整可抄 + 1Panel 适配注记)。
要点速览:
### 3.1 租户域名 → 127.0.0.1:8018
每租户域名一个 1Panel 站点(chat. / chat2. 各一站),upstream 全指向
`127.0.0.1:8018`——**多域名同容器**,这是与 jc 时代"一站一容器"的最大差异。
站点片段放 `/opt/1panel/www/sites/<域名>/proxy/{root,ws}.conf`。
### 3.2 必带的三件事(每站)
1. **`proxy_set_header Host $host`**——本项目按 Host 头解析租户,漏了全站静默 404。
2. **WS 升级头单套**(`location /ws/`:Upgrade/Connection + http 1.1 + 3600s)——
1Panel 面板生成的反代若自带一套 Upgrade/Connection,删面板那套,重复两套 =
uvicorn 拒绝握手(HTTP 正常、WS 426,jc 线上事故)。
3. **`client_max_body_size 5m`**——头像上传上限 2MB,nginx 默认 1m 直接 413。
另外两条不改配置只做核对:`/static/widget.js` 响应应带 `Cache-Control: no-cache`
(应用层中间件已下发,反代别加会覆盖它的 expires);**`/api/public/*` 放行**
(widget 头像/品牌端点,按租户 Host 解析——全站反代天然覆盖,按路径收紧配置时
漏掉它的症状是 widget 头像 404)。
### 3.3 管理后台两形态(二选一)
- **形态 A:独立管理域名**。1Panel 为 `<admin 域名>`(部署前由需求方确定)建站,
整域反代 8018。`/admin`(SPA)与 `/api/platform`(后台 API)同容器同 upstream;
入口是 `https://<admin 域名>/admin/`——管理域根路径 `/` 走租户 Host 解析,
404 属预期(可在 1Panel 站点配个跳转)。
已知边界:管理域下 `/api/public/*` 必 404(该前缀按租户 Host 解析,管理域是
未知 Host)——后台"品牌-头像预览"加载不出,**已知项不是故障**(清理单 #3),
预览以页内本地简化预览为准。
- **形态 B:路径分流**。不建独立域名,在(任一)租户域名站点下加
`location /admin` 与 `location /api/platform/` 两块指向 8018——应用层对这两个
前缀豁免租户 Host 解析,挂在租户域名下照常工作,同源零 CORS。已知边界:头像
预览走本域 `/api/public/avatar`,解析到**本域名对应的租户**,跨租户切换配置页
时预览不准(清理单 #3/#4)。
### 3.4 主 conf include 行检查(1Panel 坑)
`/opt/1panel/www/conf.d/<域名>.conf` 必须含
`include /www/sites/<域名>/proxy/*.conf;`——1Panel 建站默认可能不带,面板配了
反代却不生效九成是这行缺失。手工补前备份同目录 `*.conf.bak.<日期>`;改完
`nginx -T` 可 dump 最终生效配置对照。
### 3.5 验证与重载
```bash
# 先 -t 再 reload(reload 平滑,不影响其他站点)
docker exec $(docker ps --filter name=openresty -q | head -1) nginx -t \
&& docker exec $(docker ps --filter name=openresty -q | head -1) nginx -s reload
# 逐域名验证
curl -s https://chat2.agent.hnxinyouhe.com/health # {"status":"ok",...}
curl -sI https://chat2.agent.hnxinyouhe.com/static/widget.js | grep -i cache
curl -s -i --http1.1 -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Version: 13" \
https://chat2.agent.hnxinyouhe.com/ws/chat | head -1 # 期待 HTTP/1.1 101
```
### 3.6 共享域名模式(单域名多租户,可选)
> 设计出处:spec `docs/superpowers/specs/2026-10-19-shared-domain-routing-design.md`。
**概念**:一个共享域名(如 `kefu.hnxinyouhe.com`)承载任意多租户——租户标识
在 URL 路径前缀 `/t/<租户slug>/` 中,与 §3.1 "每租户一域名"的 Host 模式
**共存**,解析优先级:**路径前缀 > Host**(`/t/<slug>/...` 命中即剥前缀按
slug 解析租户;不带前缀的请求照走 Host 解析——共享域名自身不是租户域名,
根路径 404 属预期)。
**接入流程(零 DNS 操作)**:
1. 平台设置(仅超管)配 `shared_widget_domain` = 共享域名(运营填一次);
2. 建租户——域名可整组留空(纯路径模式租户,创建向导支持跳过域名);
3. 配置中心 → 品牌与外观 → 嵌入代码 → 复制「共享域名形态」→ 贴到目标网站。
**地址形态**:
| 项 | 形态 |
|---|---|
| 聊天页 | `https://<共享域名>/t/<slug>/` |
| 挂件脚本 | `https://<共享域名>/t/<slug>/static/widget.js`(脚本地址即租户标识,无需新参数) |
| `data-site` 参数 | 照常可用;缺省取租户首个站点 |
**无尾斜杠语义**:`/t/<slug>`(无 `/`)→ **404**——仅带尾斜杠形态才剥离
前缀(spec §3 防歧义设计,不做 308 重定向);嵌入代码生成的就是带斜杠正确
形态,勿手工"修正"。
**部署注记**:
- 共享域名 = **一条反代**(1Panel 一站),§3.2 三件事同款:透传 Host 头、
WS 升级头单套、`client_max_body_size 5m`;WS 超时 3600s 同既有。模板
`deploy/openresty-all-kefu.conf.template` 有现成注释示例块(`kefu.example.com`
占位)。**WS 位置差异**:路径模式 WS 的真实路径是 `/t/<slug>/ws/chat`——
升级头必须落在 regex location `~ ^/t/[a-z0-9-]+/ws/`(照抄模板共享域名块;
1Panel 同样放 `proxy/ws.conf`),只配 `location /ws/` 匹配不到它(症状:
聊天页能开、WS 连不上)。
- **uvicorn 不得加 `--root-path`**:路径前缀解析在 raw_path 上做,root_path
会前移 raw_path → `/t/` 前缀永不命中,全站退化为 Host 解析 404(同模板注记)。
- **登录限流取 XFF 末段**(`app/platform/routes/auth_routes.py` `_client_ip`)
——依赖反代 `$proxy_add_x_forwarded_for` 的**追加**语义(客户端自带在前、
真实来源追加在尾);勿改成 `X-Forwarded-For $remote_addr` 覆盖式(同模板注记)。
## 4. 存量迁移(老 jc / auto 项目 → 本平台)
**前提**:老项目与本项目同机(47.254.176.10);若跨机,先把导出文件 scp 到
`/opt/all-kefu/data/` 下(compose 已挂载该目录,容器内路径 `/app/data/`;
**注意宿主 `/tmp` 默认不在容器挂载内**,`docker compose run` 看不到宿主 /tmp 的文件)。
### 4.1 老库导出(二选一,禁运行时裸 cp)
```bash
# 方式一(最稳):停老容器再拷
docker stop hs-ai-kefu # jc 容器名,以老项目实录为准
cp /opt/1panel/www/sites/chat2.agent.hnxinyouhe.com/index/data/hesupipe_ai_kefu.db \
/opt/all-kefu/data/legacy-jc.db
# 方式二(不停机):SQLite backup API / VACUUM INTO 导一致性快照
#(老容器内库路径以 jc 实录为准:WORKDIR /app + ./data/hesupipe_ai_kefu.db)
docker exec hs-ai-kefu python -c \
"import sqlite3; c=sqlite3.connect('/app/data/hesupipe_ai_kefu.db'); \
c.execute(\"VACUUM INTO '/app/data/export-jc.db'\"); c.close()"
cp /opt/1panel/www/sites/chat2.agent.hnxinyouhe.com/index/data/export-jc.db \
/opt/all-kefu/data/legacy-jc.db
```
**禁止运行时直接 `cp` 活库文件**:SQLite WAL 模式下可能拷到写一半状态。
停容器或 backup API / VACUUM INTO,没有第三条路。auto 项目同理(库名/容器名
以汽车项目服务器实录为准)。
### 4.2 导入 + 对账
```bash
cd /opt/all-kefu
# 租户须已 bootstrap(§2 已导入 auto/jiancai 档案,门禁自会校验 platform.db)
docker compose run --rm app python scripts/import_legacy.py \
--source /app/data/legacy-jc.db --tenant jiancai
# auto 项目同理:
docker compose run --rm app python scripts/import_legacy.py \
--source /app/data/legacy-auto.db --tenant auto
```
脚本纪律:老库**只读**打开(mode=ro,绝不回写);保 ID/session_id/status/
page_token 原样(external_key 连续,UniService 不产生重复线索);切换前
`pending` 推送日志转 `failed` 并追加备注;幂等(INSERT OR IGNORE,幂等键=主键,
重跑零变化)。
对账报告(stdout)逐项核对:
- 每表行数四元组:源/导入/跳过已存在/目标合计,源=目标才算一致;
- **最近 10 条会话抽查**:id/session/status 逐条对照;
- 各子表 `MAX(conversation_id)` 对照(ID 原样性);
- `pending→failed` 转换条数;
- 列差异标注(老库缺列→默认值填充、老库多列→不迁移)——确认无意外项。
任一不一致 → 脚本非零退出,**人工核查清楚之前不得切换域名**。
## 5. 切换 runbook(独立大节——按序执行,每阶段有出口)
> 原则:域名最后切。切换 = 只改 1Panel 反代 upstream(老容器端口 →
> 127.0.0.1:8018),DNS 不动、老项目不删。
### 阶段一:验证(不切域名)
新平台已部署(§2)+ 已迁移(§4)+ 反代片段已备好但**未指流量**。全功能验证
走"隧道 + hosts",本机把租户域名锚到新平台:
```bash
# 本机(运维机)hosts 加:127.0.0.1 chat.agent.hnxinyouhe.com chat2.agent.hnxinyouhe.com
ssh -L 8018:127.0.0.1:8018 root@47.254.176.10 # 隧道保持
# 浏览器访问 http://chat2.agent.hnxinyouhe.com:8018/ 即为新平台(Host 命中租户)
```
验证清单(逐项过,全绿才进阶段二):
- [ ] `/health`:status=ok、两租户 active、domains=3
- [ ] 登录后台 `/admin/`:超管登录、强制改密流、租户管理员登录(后台建号)
- [ ] **两租户 widget 冒烟**:`http://chat<...>:8018/static/widget-test.html`
邀请卡弹出、面板展开、发一句消息收到 AI 回复(真实 ZHIPU_API_KEY)
- [ ] WS 握手 101(上方 curl 法)+ 面板长连不掉
- [ ] 配置中心:品牌/词表加载、版本历史(bootstrap 写入的版本 1 可见)、试金石跑一轮
- [ ] 统计:迁移数据可见(会话数与老平台对得上)、推送健康表渲染
- [ ] `/api/public/avatar` 200(头像)、`/static/widget.js` 带 no-cache
- [ ] 后台数据视图:翻到迁移导入的最近会话,消息流完整
### 阶段二:切反代(逐域名)
1Panel 逐站点把反代 upstream 从老容器端口改为 `127.0.0.1:8018`
(一个域名一个站点;先切一个租户域名,观察半天再切第二个)。每切一个:
`nginx -t` → reload → 立即线上验证 `/health`、widget 加载、WS 101、一条真实
留言走通(AI 回复 + 询盘邮件到达)。
### 阶段三:观察一周(日检清单)
- [ ] `/health` 无 degraded(有 = 对应租户库问题,立刻查)
- [ ] 后台"统计 → 推送健康"成功率正常(与切换前老平台口径对比)
- [ ] 询盘邮件正常到达(单邮件通道,SMTP 失败不自动补发,头几天盯收件)
- [ ] **breach_rate 双向偏差观察**(沿用 jc 观察法):脚本**已随迁**至本仓
`scripts/breach_rate.py`(多租户适配:`--tenant` 必填,经 platform.db
门禁后只读打开 `tenants/<slug>.db`,默认路径与容器挂载对齐,零额外参数):
```bash
docker compose run --rm app python scripts/breach_rate.py --tenant jiancai
# 容器已起时也可 exec 形态:docker compose exec app python scripts/breach_rate.py --tenant jiancai
```
(漏放/误杀残差与 jc 期基线对比;误杀侧不在脚本口径,用后台会话详情人工复核)
- [ ] 磁盘与日志:`df -h`、`docker stats all-kefu-app-1 --no-stream`
(内存应显著低于 768m 限额)、`docker logs all-kefu-app-1 --tail 100`
### 阶段四:老项目停用归档(观察期满且无回滚诉求)
老容器 `docker stop`(**不 rm**)、镜像留存;老库文件归档备份至备份盘;
1Panel 老站点配置保留(回滚素材)。至此切换闭环。
### 回滚预案(任一阶段发现不可接受问题)
反代切回老容器 = 该域名 1Panel 站点 upstream 改回老端口(8016/8017)→
`nginx -t` → reload,**约 30 秒**。老项目容器/数据在观察期内原样保留,这就是
不删它们的原因。注意:切换后新平台产生的会话在回滚后会留在新平台库(老平台
没有)——属预期,如实告知需求方;UniService 线索不会重复(conversation_id
保原样 + external_key 前缀不变)。
## 6. 日常运维
### 6.1 备份(每日;platform.db 按敏感件对待——内含 SMTP/push/AI 密钥)
用 SQLite backup API 做一致性快照(WAL 安全),**禁运行时直拷 .db**:
```bash
cd /opt/all-kefu
docker compose exec -T app python - <<'PY'
import sqlite3, pathlib, datetime
stamp = datetime.date.today().strftime('%Y%m%d')
targets = [pathlib.Path('/app/data/platform.db'),
*sorted(pathlib.Path('/app/tenants').glob('*.db'))]
for p in targets:
dst = f'/app/data/backups/{stamp}-{p.name}'
pathlib.Path(dst).parent.mkdir(parents=True, exist_ok=True)
src, out = sqlite3.connect(p), sqlite3.connect(dst)
src.backup(out); out.close(); src.close(); print(dst)
PY
# 备份落在宿主 data/backups/(同盘!)——必须再 rsync/scp 出服务器才算备份
```
cron 示例:把上面整段 heredoc 存成 `/root/all-kefu-backup.sh`,cron 挂
`10 4 * * * /root/all-kefu-backup.sh`(快照后再由 rsync/scp 任务拉走,04:30 前后错开)。
**季度恢复演练**(没验证过的备份等于没有备份):取一份历史备份,起临时容器验
`PRAGMA integrity_check` + 关键表行数,与线上对得上才归档:
```bash
docker compose run --rm -v /opt/backups/all-kefu/20260401-platform.db:/tmp/r.db:ro \
app python -c "import sqlite3; c=sqlite3.connect('/tmp/r.db'); \
print(c.execute('PRAGMA integrity_check').fetchone()); \
print(c.execute('SELECT count(*) FROM tenants').fetchone())"
```
### 6.2 日志与 PII 现状
- 封顶已配:json-file `max-size 10m × max-file 3`(Docker 默认不限大小会涨满磁盘,
compose 已落实)。看日志:`docker logs all-kefu-app-1 --tail 100`。
- **PII 现状(如实记录)**:spec §9 承诺"不打聊天内容等 PII",但当前**引擎层
日志含会话内容摘录**(conversation.py 若干 `logger.info/warning` 带消息片段
`%r` 截断输出);平台层(app/platform/)零日志、不落凭据。整改方向:观察期内
收口(摘录降级为 session_id 索引)——在此之前,日志文件按含 PII 对待,
备份/外发时注意。
- 登录限流现状:反代形态下所有限流键同源(反代 IP),退化为按用户名限流
(fail-safe 只更严不更松);XFF 可信解析归后续批次。
### 6.3 数据归档(archive.py)
`lead_push_logs.request_payload` 存完整对话 JSON 只涨不掉;会话超 12 个月
走归档(导出 JSON → 删行 → VACUUM 回收):
```bash
docker compose run --rm app python scripts/archive.py --tenant jiancai --dry-run # 先只列不动
docker compose run --rm app python scripts/archive.py --tenant jiancai # 实际执行
```
导出落 `data/archives/<tenant>/<时间戳>-365d.json`(先落盘后删库,崩溃级安全);
对账不一致即非零退出。**维护窗口建议**:低峰(凌晨)执行——VACUUM 需独占,
被占用锁时脚本自动跳过并提示稍后手动补跑;导出/删除本身单事务安全。
### 6.4 升级发版与回滚
```bash
# 本地:全量测试全绿才发版(后端 pytest + 前端 vitest/build),然后 push
git push origin <branch>
# 服务器:
cd /opt/all-kefu && git pull
docker compose build # 静态/前端改动必须 build(打进镜像,仅 up -d 不够)
docker compose up -d
curl -s http://127.0.0.1:8018/health
```
**版本 tag 纪律(30 秒回滚的来源)**:发版时在 compose 的 app 服务补/改
`image: all-kefu:<版本tag>`(与 `build: .` 并存——build 产物自动打上该 tag),
`up -d` 即切新版;**回滚 = image 改回上一 tag 再 up -d**(旧镜像仍在本地,
30 秒内完成)。当前 compose 尚未带 image 字段(首发后落地此纪律)。
### 6.5 加租户(零容器操作)
**加租户 = 后台建租户 + 1Panel 反代加一条,不起容器**:
后台"租户管理 → 创建向导"(消费出厂档案/空档案)→ 站点绑定新域名 → 1Panel
为新域名建站反代 `127.0.0.1:8018`(§3 同款三件事)→ 完成。同一容器自动承载。
已启用共享域名(§3.6)时更省:新租户零域名、零 DNS、零反代操作——直接按
`/t/<slug>/` 形态取嵌入代码即完成接入。
### 6.6 bootstrap 复跑
`docker compose run --rm app python scripts/bootstrap_platform.py` 幂等:
已存在 slug 的租户跳过、超管空表才建——发版后跑一遍无副作用(也可不跑,
lifespan 启动同样保证 schema/超管就绪,两者同一套幂等实现)。
注意:**容器运行中跑 bootstrap 不会热刷新内存租户注册表**——`/health`
仍显示旧租户列表,重启容器(`docker compose restart app`)后才反映;
首次部署按 §2 顺序(bootstrap 先于 up)则无此问题。
## 7. 踩坑实录(排查手册——每条都是真事故/真审查发现)
| # | 现象 | 根因 | 解法(已内置/纪律) | 来源 |
|---|---|---|---|---|
| 1 | 同机两个客服项目起一个、另一个 502 | 1Panel 站点目录都叫 `index`,compose 默认以目录名为项目名 → 互删容器 | compose 钉死 `name: all-kefu`(根治);且本机只剩这一个 compose 项目 | jc 线上事故 |
| 2 | 改了 widget.js / admin 前端,线上"没生效" | 静态与前端 dist **打进镜像**,`up -d` 不重建镜像 | 发版必须 `docker compose build`;`/static/*` 已由应用层下发 no-cache(用户端 Ctrl+F5 一次即愈) | jc 线上事故 |
| 3 | 1Panel 面板配了反代不生效 | 面板建站主 conf 默认可能不含 `include proxy/*.conf` | 手工补 include 行(§3.4),备份后 `nginx -T` 验证 | jc 线上事故 |
| 4 | WS 握手 426(HTTP 正常) | 反代里 Upgrade/Connection 头重复两套,uvicorn 严格解析拒绝 | 升级头全局只留一套(§3.2);`nginx -T` dump 最终配置定位 | jc 线上事故 |
| 5 | bottom-left 弹出元素溢出屏幕 | widget 弹出层 CSS 硬编码只适配右下角 | 已随 `pos-<position>` 类联动;**新增弹出元素必须同时加联动规则**(静态测试锁死) | jc 线上事故 |
| 6 | mem_limit 为什么是 768m | 单进程(`--workers 1` 硬约束)承载 admin SPA 服务 + 全部租户引擎,较 jc 单租户 512m 上调 | compose 已配;改前先看 `docker stats` 实测 | 本项目 |
| 7 | 容器内时间不对/推送窗口漂移 | 基镜像缺 tzdata 时 TZ 被静默忽略(退 UTC) | compose 固定 `TZ=Asia/Shanghai` + Dockerfile 显式装 tzdata(基镜像漂移保险);仪表盘"今日"边界依赖它 | 本项目 |
| 8 | widget 头像 404 / 后台头像预览空 | `/api/public/*` 按租户 Host 解析:反代按路径收紧时漏配它;admin 独立域名(形态 A)下必 404 | 租户域名站确保全站反代覆盖 `/api/public`;形态 A 预览缺失是已知项(清理单 #3)非故障 | 本项目 |
| 9 | platform.db 随容器重建丢失 | 库曾落仓库根(非卷内) | 已修:PLATFORM_DB 钉 `data/platform.db`(compose bind mount),**勿挪** | 本项目(P4T1 审查) |
| 10 | healthcheck 起不来容器 | slim 镜像无 curl | compose healthcheck 用容器内自带 python urllib 探 /health | 本项目 |
| 11 | admin 页白屏 | dist 挂载在 `/admin` 前缀,资源引用前缀不一致 | `VITE_BASE_URL=/admin/` 钉在 `admin/.env.production`(单一事实源,不靠构建期 env 注入);动它必须重新 build | 本项目 |
| 12 | 反代配好后全站 404(非报错) | 租户路由按 **Host 头**解析,反代漏 `proxy_set_header Host $host` → upstream 收到的是 127.0.0.1 这类未知 Host | 反代三件事之一(§3.2);排障先 curl 直打 8018 带 `-H "Host: <租户域名>"` 对比 | 本项目新增预判 |
| 13 | 头像上传 413 | nginx 默认 `client_max_body_size 1m` < 应用上限 2MB | 反代层放 5m(§3.2) | 本项目新增预判 |
## 附:与 jc 手册的继承关系
- **继承**(教训类,全部进 §7 + §3/§6 相应小节):compose 撞名互删、静态打进
镜像、1Panel include 行、WS 426、widget 缓存、bottom-left 联动、备份禁裸拷、
日志封顶、WS/HTTP 分离的超时策略。
- **不继承**(部署形态不同,已按本项目重写):一站一容器 → 单容器多租户多域名;
deploy.sh 一键脚本 → 未提供(首次部署按 §2 手工,稳定后可自行固化);
jc 的 `/admin` 单租户后台 → 本项目 `/admin/` 多租户 SPA + 两反代形态。
- jc 手册原文:老仓 `D:\refactor\ai-kefu-jc\docs\deploy-production.md`(服务器
布局/跳板链路/凭据来源等实况细节需要时回查)。