5 Commits

7 changed files with 274 additions and 5 deletions
+8 -1
View File
@@ -52,11 +52,18 @@ def resolve_email_settings(config_data: dict | None) -> dict:
- smtp 给了 dict → 段内逐键回退(缺键/空串 → 平台设置快照对应键 → - smtp 给了 dict → 段内逐键回退(缺键/空串 → 平台设置快照对应键 →
env 对应值, 租户可只改密码/只换 host); env 对应值, 租户可只改密码/只换 host);
- sender_name 空 → 平台默认(_DEFAULT_SENDER_NAME); - sender_name 空 → 平台默认(_DEFAULT_SENDER_NAME);
- notify_emails 空列表 → env NOTIFY_EMAIL 单元素。 - notify_emails 空列表 → 租户 brand.email 单元素(用户 2026-10-21 要求:
默认取每个租户配置的站点邮箱) → env NOTIFY_EMAIL 单元素。
""" """
email = ((config_data or {}).get("email")) or {} email = ((config_data or {}).get("email")) or {}
smtp = email.get("smtp") or {} smtp = email.get("smtp") or {}
notify = [e for e in (email.get("notify_emails") or []) if e] notify = [e for e in (email.get("notify_emails") or []) if e]
if not notify:
# 回退 1:租户品牌配置里的 email(站点对外展示邮箱——客户留资通知
# 默认发到客户自己的联系邮箱,不需要每个租户显式配 notify_emails)
brand_email = ((config_data or {}).get("brand") or {}).get("email")
if brand_email:
notify = [brand_email]
return { return {
"host": (smtp.get("host") "host": (smtp.get("host")
or settings_store.get("smtp.host") or settings_store.get("smtp.host")
+8 -1
View File
@@ -18,6 +18,8 @@
启动只做校验+建任务——pipeline 由任务模块的后台 aio task 跑,本层不 启动只做校验+建任务——pipeline 由任务模块的后台 aio task 跑,本层不
感知 crawler/generator(测试也因此能整体打桩 task.start/task.get)。 感知 crawler/generator(测试也因此能整体打桩 task.start/task.get)。
""" """
import json
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import select from sqlalchemy import select
@@ -45,11 +47,16 @@ async def start_import(slug: str, body: StartRequest, request: Request,
tenant = await _get_tenant_or_404(session, slug) tenant = await _get_tenant_or_404(session, slug)
# 租户配置快照(终审 Important #2:AI 三级链的租户槽位——传给 # 租户配置快照(终审 Important #2:AI 三级链的租户槽位——传给
# task.start → make_provider;自有 ai.* 配置的租户烧自己配额) # task.start → make_provider;自有 ai.* 配置的租户烧自己配额)
latest = (await session.execute( # config_json 列为 Text——scalar 返回 JSON 串,须 json.loads 成 dict
raw_config = (await session.execute(
select(TenantConfigVersion.config_json) select(TenantConfigVersion.config_json)
.where(TenantConfigVersion.tenant_id == tenant.id) .where(TenantConfigVersion.tenant_id == tenant.id)
.order_by(TenantConfigVersion.version.desc()) .order_by(TenantConfigVersion.version.desc())
.limit(1))).scalar_one_or_none() .limit(1))).scalar_one_or_none()
try:
latest = json.loads(raw_config) if raw_config else None
except (TypeError, json.JSONDecodeError):
latest = None
if tenant.status != "active": if tenant.status != "active":
raise HTTPException( raise HTTPException(
status_code=403, detail=f"租户 {slug!r} 已停用,无法发起导入") status_code=403, detail=f"租户 {slug!r} 已停用,无法发起导入")
+3 -2
View File
@@ -143,8 +143,9 @@ _DEFAULT_TAIL_ZH = "客户经理会把完整报价发您——请留下您的联
_FACTS_RULES_BASE = [ _FACTS_RULES_BASE = [
"- **统一兜底(最高优先)**:任何不清楚、事实卡没有、需要核实的问题——" "- **统一兜底(最高优先)**:任何不清楚、事实卡没有、需要核实的问题——"
"**不编造、不承诺、不否定、不断言**,统一动作只有一个:顺势引导留联系方式转客户经理", "**不编造、不承诺、不否定、不断言**,统一动作只有一个:顺势引导留联系方式转客户经理",
"- **不扩展原则**:回答公司事实**只用本卡原词**——禁止添加规格、容量、参数、" "- **不扩展原则**:回答公司事实**只用本卡的事实**——禁止添加规格、容量、参数、"
"项目等任何卡外细节", "项目等任何卡外细节;**事实卡为中文参考源——回复时中文术语必须翻译为客户的语言**"
"(如产品名、材料名、技术术语),产品英文名照用,数字照抄",
"- 客户问公司情况/产品范围/是否可信时,取上述事实**简短作答(一两句)**,建立信任", "- 客户问公司情况/产品范围/是否可信时,取上述事实**简短作答(一两句)**,建立信任",
"- 绝不主动长篇介绍公司,绝不用成营销腔——风格仍按【说话风格】", "- 绝不主动长篇介绍公司,绝不用成营销腔——风格仍按【说话风格】",
"- 涉及具体价格/交期/MOQ/库存/认证编号 → 仍按【职能边界】转客户经理", "- 涉及具体价格/交期/MOQ/库存/认证编号 → 仍按【职能边界】转客户经理",
@@ -0,0 +1,254 @@
# 老项目(jc/建材 + auto/汽车)迁移计划
- 日期:2026-10-23
- 前置:all-kefu 生产已上线(chat.ai.hnxinyouhe.com,8019 端口,路径模式)
- 迁移工具:scripts/import_legacy.py(已有,幂等、只读源库、四元组对账)
---
## 0. 现状盘点
### 老项目
| | jc/建材 | auto/汽车 |
|---|---|---|
| 容器 | hs-ai-kefu (8017) | cc-ai-kefu (8016) + redis |
| 域名 | chat2.agent.hnxinyouhe.com | chat.agent.hnxinyhe.com |
| 反代指向 | 127.0.0.1:8017 | 127.0.0.1:8016 |
| 数据库 | hesupipe_ai_kefu.db (308KB) | cc_ai_kefu.db (5.9MB) |
| 推送 | **disabled** (PUSH_ENABLED=false) | **enabled** (94 success / 2 failed / 1 skipped) |
| 推送前缀 | ai-kefu-jc | ai-kefu |
| AI | glm-4-flash | glm-4-flash |
| SMTP | 飞书邮箱(已迁移到新平台) | 同 |
### 老数据量
| 表 | jc | auto | 合计 |
|---|---|---|---|
| conversations | 135 | 4,427 | 4,562 |
| messages | 299 | 7,896 | 8,195 |
| customer_profiles | 135 | 4,427 | 4,562 |
| evaluations | 22 | 491 | 513 |
| lead_push_logs | 0 | 97 | 97 |
### 新平台现状(已就绪)
- 租户 auto / jiancai 已 bootstrap(出厂档案配置)
- 推送前缀已对齐:auto=`ai-kefu`,jiancai=`ai-kefu-jc`(与老项目一致——external_key 连续性)
- SMTP 飞书邮箱已配(平台默认)
- 通知邮箱:默认回退 brand.email
- 提示词差异已审计:新 = 老 + 增强(DEFLECTION_POOL 配置化 / CC 区号补全 / 防自拼号码 / 翻译规则)
### 需要补配的(老项目有 → 新平台缺)
| 项 | 说明 |
|---|---|
| auto 租户 push.api_key | 老项目有值 `650e0af0...`,新平台 auto 未配 |
| auto 租户 push.enabled | 需设 true(老项目是 true) |
| jc 租户 push.enabled | 保持 false(老项目就是 false) |
| push.api_url | 老项目 `https://uniservice.server.hnxinyouhe.com/open/v1/inquiry/push`——新平台 push.url 需确认 |
---
## 1. 阶段一:配置补齐(~5 分钟,不停老项目)
### 1a. 补 auto 租户推送配置
```
后台 → 配置中心 → auto → 凭据与通知 → 推送组:
url: https://uniservice.server.hnxinyouhe.com/open/v1/inquiry/push
api_key: (从老项目 env 拷贝)
prefix: ai-kefu(已配,不动)
enabled: true
```
或走 API(curl PUT config/push)。
### 1b. 确认 jc 推送保持 disabled
jc 老项目 PUSH_ENABLED=false——新平台 push.enabled 也应 false(或留空 = 默认 false)。
### 1c. 确认 AI 配置
两个租户走平台默认(ZHIPU_API_KEY in .env = glm-4-flash)——与老项目一致,无需改。
---
## 2. 阶段二:数据导出 + 导入(~5 分钟,不停老项目)
### 2a. 老库导出(VACUUM INTO 不停机快照)
```bash
# 在生产服务器执行
SITE=/opt/1panel/www/sites/chat.ai.hnxinyouhe.com/index
# jc
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()"
docker cp hs-ai-kefu:/app/data/export-jc.db $SITE/data/legacy-jc.db
# auto
docker exec cc-ai-kefu python -c "
import sqlite3; c=sqlite3.connect('/app/data/cc_ai_kefu.db')
c.execute(\"VACUUM INTO '/app/data/export-auto.db'\"); c.close()"
docker cp cc-ai-kefu:/app/data/export-auto.db $SITE/data/legacy-auto.db
```
### 2b. 导入新平台
```bash
cd $SITE
# jc
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
```
### 2c. 对账(脚本自动输出,人工核对)
每表四元组:源 = 导入 + 跳过已存在 = 目标合计
- 源 ≠ 目标 → 非零退出 → 人工排查后才继续
- 最近 10 条会话抽查 id/session_id/status 逐条对照
- MAX(conversation_id) 对照(ID 原样性)
- auto 的 97 条 push_logs:pending(如有)→ failed 转换
- 列差异标注确认无意外
### 2d. 后台验证数据
```
后台 → 会话与线索 → 切到 jiancai 租户 → 会话数 ≈ 135
后台 → 会话与线索 → 切到 auto 租户 → 会话数 ≈ 4,427
后台 → 统计分析 → 两租户统计图有数据
```
---
## 3. 阶段三:双模式域名切换(逐域名,先切一个)
**策略**:老域名反代从老端口(8016/8017)→ 新平台 8019。这样:
- 老嵌入代码(指向 chat./chat2. 域名)继续工作——Host 模式解析到新平台租户
- 新嵌入代码(指向 chat.ai.hnxinyouhe.com/t/xxx/)同时可用——路径模式
- 两模式共存,客户零感知
### 3a. 租户绑定域名(新平台侧)
```
后台 → 租户管理 → jiancai → 详情 → 站点 → main → 绑定域名 → chat2.agent.hnxinyouhe.com
后台 → 租户管理 → auto → 详情 → 站点 → main → 绑定域名 → chat.agent.hnxinyouhe.com
```
### 3b. 1Panel 切反代(先切 jiancai/chat2,观察半天再切 auto/chat)
```
1Panel → 网站 → chat2.agent.hnxinyouhe.com → 设置 → 反向代理
修改 proxy_pass 为 http://127.0.0.1:8019
→ 保存 → nginx -t 确认 → 自动 reload
```
**注意**:该站点的 `proxy/*.conf` 里可能有多个 location(root/ws)——都要改为 8019,或者删掉旧的 proxy 目录让 1Panel 重新生成。
### 3c. 立即线上验证
```bash
# 老域名 → 新平台(Host 模式)
curl -H "Host: chat2.agent.hnxinyouhe.com" http://127.0.0.1:8019/health
curl -H "Host: chat2.agent.hnxinyouhe.com" http://127.0.0.1:8019/ # 聊天页
curl -H "Host: chat2.agent.hnxinyouhe.com" http://127.0.0.1:8019/static/widget.js # 挂件
# 公共域名 → 同一租户(路径模式)
curl http://127.0.0.1:8019/t/jiancai/api/public/brand # 品牌一致
curl http://127.0.0.1:8019/t/jiancai/static/widget.js # 挂件一致
```
线上浏览器验证(真实域名):
- [ ] `https://chat2.agent.hnxinyouhe.com/` 聊天页加载、品牌正确
- [ ] `https://chat2.agent.hnxinyouhe.com/static/widget-test.html` 冒烟
- [ ] `https://chat2.agent.hnxinyouhe.com/t/jiancai/api/public/brand` 与 `/api/public/brand` 同 slug
- [ ] WS 握手 + AI 对话(中英各一句)
### 3d. 观察半天后切 auto/chat
同 3b/3c,域名换成 chat.agent.hnxinyouhe.com → 8019。
### 3e. 客户网站嵌入代码更新(可选,建议但非必须)
```
旧:<script src="https://chat.agent.hnxinyouhe.com/static/widget.js" async></script>
新:<script src="https://chat.ai.hnxinyouhe.com/t/auto/static/widget.js" async></script>
```
不更新也不影响——老域名反代已指向新平台,继续工作。更新的好处:
- 走公共域名(一张证书)
- 为将来删老域名做准备
- 嵌入代码里的品牌色/头像等资源 URL 也走公共域
---
## 4. 阶段四:观察一周(日检清单)
- [ ] `/health`:status=ok,无 degraded
- [ ] 后台统计 → 推送健康:auto 成功率正常(对比老平台 94/97 基线)
- [ ] 询盘邮件到达(检查 hesu@hesuu.com 或 brand.email)
- [ ] `breach_rate` 双向偏差(jc 期基线对比)
```bash
docker compose exec app python scripts/breach_rate.py --tenant jiancai
docker compose exec app python scripts/breach_rate.py --tenant auto
```
- [ ] `docker stats all-kefu-app-1`(内存 < 768m)
- [ ] `docker logs all-kefu-app-1 --tail 100`(无异常)
- [ ] 后台会话视图抽查:老域名来的新会话 + 路径域名来的新会话都正常入列表
---
## 5. 阶段五:收口(观察期满)
- [ ] 老容器 `docker stop hs-ai-kefu cc-ai-kefu cc-ai-kefu-redis`(**不 rm**)
- [ ] 老库文件归档:`cp /opt/1panel/www/sites/chat*/index/data/*.db /备份盘/`
- [ ] 1Panel 老站点(chat./chat2.)保留但不再指向流量(反代已改 8019)
- [ ] 客户网站嵌入代码全部切到公共域名后,可删除老域名站点
- [ ] DNS 如不再需要 chat./chat2. 解析,通知域名管理员
---
## 6. 回滚预案(任一阶段)
| 阶段 | 回滚动作 | 耗时 |
|---|---|---|
| 阶段二(数据导入) | 无需回滚——新平台租户库多了数据不影响老项目运行 | 0 |
| 阶段三(反代切换) | 1Panel 该站点 proxy_pass 改回老端口(8016/8017)→ reload | ~30 秒 |
| 阶段四(观察期) | 同上 | ~30 秒 |
| 阶段五(收口后) | docker start 老容器 + 反代切回 | ~1 分钟 |
**注意**:切换后新平台产生的会话回滚后留在新平台库(老平台没有)——属预期,UniService 线索不重复(external_key 保原样)。
---
## 7. 风险清单
| # | 风险 | 等级 | 缓解 |
|---|---|---|---|
| 1 | auto 数据量大导入慢 | 🟢 低 | 4,427 会话 < 6MB,几秒完 |
| 2 | 老项目配置差异遗漏 | 🟡 中 | 本计划 §1 已列差异清单;导入前对比 |
| 3 | 反代配置遗漏(WS/location 多块) | 🟡 中 | 逐块检查 proxy/*.conf;nginx -t 确认 |
| 4 | 客户网站未更新嵌入代码 | 🟢 低 | 老域名反代兼容过渡,不急 |
| 5 | 新平台提示词行为差异 | 🟢 低 | 新 = 老 + 增强,只多不少;有问题配置中心可调 |
| 6 | 推送 external_key 重复 | 🟢 低 | prefix 一致 + conversation_id 原样 = 不重复 |
| 7 | 通知邮箱地址变化 | 🟡 中 | 确认各租户 brand.email 是正确的通知地址 |
---
## 8. 执行排期(预计总耗时 ~30 分钟 + 观察 1 周)
| 步骤 | 耗时 | 何时执行 |
|---|---|---|
| §1 配置补齐 | 5 分钟 | 随时 |
| §2 数据导出+导入+对账 | 10 分钟 | 低峰(凌晨/早上) |
| §3a 租户绑定域名 | 2 分钟 | 导入验证后 |
| §3b 切 chat2(jc)反代 | 5 分钟 | 上午,观察半天 |
| §3d 切 chat(auto)反代 | 5 分钟 | jc 半天无问题后 |
| §4 观察一周 | 7 天 | 每天检查清单 |
| §5 收口 | 10 分钟 | 观察期满 |
@@ -28,7 +28,7 @@
## 使用规则(与 system.md 红线同级) ## 使用规则(与 system.md 红线同级)
- **统一兜底(最高优先)**:任何不清楚、事实卡没有、需要核实的问题——**不编造、不承诺、不否定、不断言**,统一动作只有一个:顺势引导留联系方式转客户经理——"这个我让我们经理给你准确答复——留个 WhatsApp,他直接联系你" - **统一兜底(最高优先)**:任何不清楚、事实卡没有、需要核实的问题——**不编造、不承诺、不否定、不断言**,统一动作只有一个:顺势引导留联系方式转客户经理——"这个我让我们经理给你准确答复——留个 WhatsApp,他直接联系你"
- **不扩展原则**:回答公司事实**只用本卡原词**——禁止添加规格、标准、项目、地址等任何卡外细节(卡片没写的规格就不提规格)。卡内"等"字系照抄官网原词,不代表可补充未列出项目。卡片没写的就是不知道,一律走统一兜底 - **不扩展原则**:回答公司事实**只用本卡的事实**——禁止添加规格、标准、项目、地址等任何卡外细节(卡片没写的规格就不提规格)。卡内"等"字系照抄官网原词,不代表可补充未列出项目。卡片没写的就是不知道,一律走统一兜底。**事实卡为中文参考源——回复时中文术语必须翻译为客户的语言**(如产品名、材料名、技术术语),产品英文名照用,数字照抄
- 客户问公司情况/产品范围/是否可信时,取上述事实**简短作答(1-2 句)**,建立信任 - 客户问公司情况/产品范围/是否可信时,取上述事实**简短作答(1-2 句)**,建立信任
- **项目出口国与当地网点/库存是两回事,分开答**: - **项目出口国与当地网点/库存是两回事,分开答**:
- 问"do you ship to X"→ 可答官网公开项目案例国事实 + **走统一兜底**(X 具体情况经理确认)——不承诺、不否定 - 问"do you ship to X"→ 可答官网公开项目案例国事实 + **走统一兜底**(X 具体情况经理确认)——不承诺、不否定
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 41 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 41 KiB