docs: test reports + knowledge iteration design + PRDs
提交 OTP/RBAC/Tier0/Tier1/P0+P2 测试报告、方案A E2E 验证、知识库迭代设计(PRD/mermaid/html 原型)、项目状态看板更新; 根配置 docker-compose.yml/mkdocs.yml。
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
# 员工端消息发送延时改造方案
|
||||
|
||||
> 状态:实施中(后端 + 前端改造已完成,待联调 / 部署验证)
|
||||
> 提出日期:2026-07-08
|
||||
> 关联问题:员工端 H5 发送消息有延时(消息长时间停留在"发送中",AI 回复晚到)
|
||||
> 关联文档:`docs/09-部署运维/00-标准故障排查手册.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 问题背景
|
||||
|
||||
员工端 H5 目前发送一条消息时,后端会**同步**完成「消息落库 → 调用 AI 推理(Dify/RAGFlow)→ 返回响应」。由于 AI 推理本身耗时(非流式,一次 3~15 秒甚至更久),整个 HTTP 请求被 AI 阻塞,前端即便做了乐观更新,发送态切换和 AI 回复仍被拖慢,用户感知为"发送有延时"。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现象与用户感知
|
||||
|
||||
| 阶段 | 用户看到的现象 | 期望 |
|
||||
|---|---|---|
|
||||
| 点击发送 | 自己的消息立刻出现,标记"发送中" | 正常(乐观更新已生效) |
|
||||
| 等待期 | "发送中"持续数秒不消失 | 应迅速变为"已发送" |
|
||||
| AI 回复 | 要等好几秒甚至十几秒才出现 | 应尽快出现,最好流式 |
|
||||
| 极端情况 | AI 超时/失败 → 自己的消息卡在 sending 或标 failed | 自己消息不应受 AI 影响 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 根因分析(代码证据)
|
||||
|
||||
### 3.1 后端:发送与 AI 推理串行耦合
|
||||
|
||||
`backend/app/api/h5.py` → `h5_send_message`(821–990 行):
|
||||
|
||||
```python
|
||||
# 第 897 行:同步等待 AI 推理完成,期间整条 HTTP 被阻塞
|
||||
ai_result = await ai_handler.handle_message(
|
||||
content=content,
|
||||
dify_conversation_id=conversation.dify_conversation_id,
|
||||
user_id=employee_id,
|
||||
)
|
||||
# 第 918–989 行:AI 回复落库 + WS 广播 + 返回(都在 await 之后)
|
||||
```
|
||||
|
||||
`ai_handler.handle_message` 内部走的是 **Dify 非流式调用**:
|
||||
|
||||
`backend/app/services/ai_service.py:114`
|
||||
```python
|
||||
"stream": False, # 非流式 —— 必须等 AI 完整生成才返回
|
||||
```
|
||||
|
||||
### 3.2 前端:发送态依赖被阻塞的响应
|
||||
|
||||
`frontend-h5/src/stores/conversation.ts`:
|
||||
|
||||
```ts
|
||||
// 第 463 行:已做乐观更新(自己消息立即显示,标记 sending)
|
||||
status: 'sending',
|
||||
// 第 485 行:但 sending → sent 的切换依赖后端响应返回
|
||||
const resp: SendMessageResponse = await sendMessage(reqData)
|
||||
```
|
||||
|
||||
`frontend-h5/src/api/conversation.ts:284-285`:
|
||||
|
||||
```ts
|
||||
const response = await apiClient.post('/h5/conversations/current/messages', data, {
|
||||
timeout: 30000, // AI 慢时最坏等 30 秒
|
||||
})
|
||||
```
|
||||
|
||||
### 3.3 结论
|
||||
|
||||
> **不是网络慢,是"发消息"和"AI 思考"被绑死在同步请求里。** AI 每慢 1 秒,发送响应就卡 1 秒。
|
||||
|
||||
---
|
||||
|
||||
## 4. 影响范围
|
||||
|
||||
- 所有走 `h5_send_message` 的文本/图片/文件消息(图片文件因还要过 Vision/Dify 更慢)
|
||||
- 坐席端 WS 实时性(AI 回复广播被延后,坐席看到新消息也晚)
|
||||
- 用户体验评分(满意度调查可能受此影响)
|
||||
|
||||
---
|
||||
|
||||
## 5. 可选方案对比
|
||||
|
||||
| 方案 | 做法 | 体验 | 改动量 | 风险 |
|
||||
|---|---|---|---|---|
|
||||
| **A. 异步化 + 流式 WS 推送**(推荐) | 发送接口只存消息立即返回;AI 推理放后台任务,经 WS 流式推送 `ai_reply_chunk` | 发送瞬时完成,AI 打字机式到达 | 中(2.5–3 天) | 需 WS 推送 + 前端消费;后台任务需兜底 |
|
||||
| **B. SSE 流式接口** | 发送接口改用 SSE,AI 回复逐字推回 | 同 A,但走独立 SSE 通道 | 中 | 企微 WebView 对 SSE/长连接兼容性需验证;网关可能截断 |
|
||||
| **C. 拆两接口快速止血** | 新增"仅存消息"接口(不调 AI),AI 结果靠现有 3 秒轮询拉取 | 发送快,AI 延迟 ≈ 轮询间隔+推理 | 小(0.5–1 天) | 轮询本身有 3s 延迟,体验一般;非根本解 |
|
||||
|
||||
**推荐 A**:后端已有 `ai_service.get_reply_stream`(流式,`ai_service.py:196`)和 `ws_manager.broadcast`(已在用),改造可**直接复用现有能力**,不引入新依赖。
|
||||
|
||||
---
|
||||
|
||||
## 6. 推荐方案 A 详细设计
|
||||
|
||||
### 6.1 后端改造
|
||||
|
||||
**① 拆出 AI 推理为后台任务**
|
||||
|
||||
```python
|
||||
# backend/app/api/h5.py
|
||||
|
||||
async def h5_send_message(...):
|
||||
# 1. 查找/创建会话(不变)
|
||||
# 2. 存用户消息(不变,flush)
|
||||
message = Message(...); db.add(message); await db.flush()
|
||||
|
||||
# 3. 【改造】启动后台任务,不阻塞响应
|
||||
background_tasks.add_task(
|
||||
generate_ai_reply,
|
||||
conversation_id=conversation.id,
|
||||
content=content,
|
||||
employee_id=employee_id,
|
||||
dify_conversation_id=conversation.dify_conversation_id,
|
||||
)
|
||||
|
||||
# 4. 立即返回(不含 ai_reply)
|
||||
return success_response(data={
|
||||
"user_message": user_msg_data,
|
||||
"ai_reply": None,
|
||||
"conversation_status": conversation.status,
|
||||
"can_call_agent": conversation.ai_substantive_reply_count >= 3,
|
||||
})
|
||||
|
||||
|
||||
async def generate_ai_reply(conversation_id, content, employee_id, dify_conversation_id):
|
||||
"""后台任务:流式推理 → 落库 → WS 推送"""
|
||||
try:
|
||||
chunks = []
|
||||
async for chunk in ai_service.get_reply_stream(content, conversation_id):
|
||||
chunks.append(chunk)
|
||||
await ws_manager.broadcast({
|
||||
"type": "ai_reply_chunk",
|
||||
"data": {"conversation_id": str(conversation_id), "chunk": chunk},
|
||||
})
|
||||
# 落库 AI 消息
|
||||
ai_msg = Message(conversation_id=conversation_id, sender_type="ai",
|
||||
content="".join(chunks), is_read=True)
|
||||
db.add(ai_msg); await db.flush()
|
||||
# 推送完整消息(供轮询兜底 / 去重)
|
||||
await ws_manager.broadcast({
|
||||
"type": "ai_reply",
|
||||
"data": MessageResponse.model_validate(ai_msg).model_dump(),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"AI 推理失败: {e}")
|
||||
# 兜底:存 system 消息提示 + 必要时转人工
|
||||
...
|
||||
```
|
||||
|
||||
**② 复用点**
|
||||
- `ai_service.get_reply_stream`(`ai_service.py:196`)—— 已实现的流式 AI 接口
|
||||
- `ws_manager.broadcast`(`h5.py:937`)—— 已用于 `new_message` / `conversation_updated`,新增 `ai_reply_chunk` / `ai_reply` 类型即可
|
||||
|
||||
### 6.2 前端改造
|
||||
|
||||
`frontend-h5/src/stores/conversation.ts` 中:
|
||||
- 发送后立即拿到 `user_message`(已 `sent`),无需等待 AI
|
||||
- 新增 WS 监听:
|
||||
|
||||
```ts
|
||||
// WS 连接处新增
|
||||
ws.on('ai_reply_chunk', ({ conversation_id, chunk }) => {
|
||||
appendAiChunk(conversation_id, chunk) // 追加到当前 AI 气泡(打字机)
|
||||
})
|
||||
ws.on('ai_reply', ({ message }) => {
|
||||
ensureAiMessage(message) // 去重追加完整 AI 消息
|
||||
})
|
||||
```
|
||||
|
||||
- `sending → sent` 切换:响应返回即置 `sent`(不再等 AI)
|
||||
|
||||
### 6.3 WS 事件协议
|
||||
|
||||
| 事件 type | 触发时机 | data 字段 | 消费方 |
|
||||
|---|---|---|---|
|
||||
| `ai_reply_chunk` | AI 每生成一个片段 | `conversation_id`, `chunk` | H5 端(打字机拼装) |
|
||||
| `ai_reply` | AI 完整生成并落库 | 完整 AI `Message` 对象 | H5 端(去重/兜底) |
|
||||
| `ai_reply_failed` | AI 推理异常 | `conversation_id`, `reason` | H5 端(提示 + 转人工) |
|
||||
|
||||
### 6.4 失败兜底与降级
|
||||
|
||||
- **AI 推理失败/超时**:后台任务捕获异常 → 存一条 `system` 消息("AI 暂时无法回复,已为你转接人工")并 WS 推送 `ai_reply_failed`;不影响用户消息本身。
|
||||
- **WS 断连期间 AI 完成**:AI 消息已落库,`pollMessages`(现有 3 秒轮询)能拉到,前端去重追加即可 → **天然兜底**。
|
||||
|
||||
#### 6.4.1 ADR-001:单 Worker 作为后台任务可靠性基线(架构决策)
|
||||
|
||||
> **ADR 元信息**
|
||||
> - 编号:ADR-001
|
||||
> - 状态:Accepted(已采纳,2026-07-07 随方案 A 实施落地,见 §12)
|
||||
> - 主题:后台 AI 任务的承载方式 / 后端 worker 进程数
|
||||
> - 关联:方案 A(§6)、实施清单 #0(§11)、决策确认 #2(§11.1)
|
||||
|
||||
---
|
||||
|
||||
##### 背景 Context
|
||||
|
||||
方案 A 把 AI 推理移出 HTTP 请求、改为后台任务,结果需经 `ws_manager` 实时推回员工端。此时**后端 worker 进程模型直接决定"后台任务能否把消息推到员工 WS 连接"**,是方案 A 能否成立的前提。
|
||||
|
||||
事实依据(代码已确认):
|
||||
- 部署配置 `docker-compose.yml:151`:`uvicorn ... --workers 2`(**双进程**)。
|
||||
- `backend/app/services/ws_manager.py:327`:`manager = ConnectionManager()` 是**纯进程内内存单例**,连接存在字典里,**无任何 Redis 跨进程同步**。
|
||||
|
||||
隐患推导:
|
||||
- 客户端 WS 连接随机落在 worker A 或 worker B。
|
||||
- 若 HTTP 请求被 worker B 处理、在其内 `create_task(process_h5_ai_reply)`,推理完成后 worker B 调 `manager.broadcast()`,但目标员工的 WS 连接若恰在 worker A 上 → **广播静默失败,员工收不到 AI 回复(约 50% 概率)**。
|
||||
- 该隐患对现网**所有** WS 推送都成立,只是流量小不明显;方案 A 会把每个 AI 回复都变成"依赖跨进程广播",放大问题。
|
||||
|
||||
##### 决策 Decision
|
||||
|
||||
**采用 P1:后端 `docker-compose.yml` 的 uvicorn 启动参数由 `--workers 2` 改为 `--workers 1`;本期不引入 Redis / Celery / 任务队列。** 后台 AI 推理以进程内 `asyncio.create_task` 承载,结果经进程内 `ws_manager` 单例推回员工端。
|
||||
|
||||
##### 关键判断:单 Worker 是当前最优基线
|
||||
|
||||
- **体量匹配**:IT 智能服务台当前并发个位数、长连接数低,单进程单事件循环(asyncio)即可充分承载,无横向扩展诉求。
|
||||
- **最自然搭配**:进程内 task + 进程内 WS 单例,是"持续把中间片段推给同一 WS 连接"的实时流式对话最自然的实现,无需引入跨进程协调。
|
||||
- **YAGNI / 避免过度设计**:Redis pub/sub 广播层或 Celery/ARQ 属重型基础设施,适合离线批处理,不适合把流式结果绕回 WS 的实时对话场景;当前引入是过度工程。
|
||||
- **顺带修复现网隐患**:原 `--workers 2` + 内存 `ws_manager` 使所有 WS 广播存在 ~50% 跨进程静默丢失,只是低流量下不明显;改单 worker 后该隐患彻底消除(方案 A 的"免费"收益)。
|
||||
- **可演进**:若未来并发显著增长,再升 P2(Redis pub/sub 广播层)或 P3 多 worker,本 ADR 届时由 ADR-002 替代。
|
||||
|
||||
##### 后果 Consequences
|
||||
|
||||
| 变得更容易 / 收益 | 变得更难 / 代价 |
|
||||
|---|---|
|
||||
| AI 回复 100% 经同一进程 WS 推到员工端(不再丢) | 单进程崩溃会丢失在途后台任务 |
|
||||
| 部署配置改动极小(1 行) | 失去多 worker 水平扩展能力(当前不需要) |
|
||||
| 消除现网所有 WS 跨进程广播隐患 | 未来并发激增时需升级到 P2 / P3 |
|
||||
|
||||
> 在途任务丢失由"DB 为真相源 + 3s 轮询兜底"覆盖:WS 推送失败,前端轮询仍能拉到落库后的 AI 消息,可接受。
|
||||
|
||||
##### 备选方案(已评估,未采纳)
|
||||
|
||||
| 路径 | 做法 | 评价 |
|
||||
|---|---|---|
|
||||
| **P2 Redis pub/sub 广播层** | `ws_manager` 引入 redis 订阅,broadcast 走 pub/sub 让所有 worker 收到 | 为未来扩多 worker 准备;已有 Redis、增量成本可接受,但当前 YAGNI |
|
||||
| **P3 进程内 task + 多 worker** | 直接 `create_task` 不改 worker | 不可靠,50% 丢消息,**不采用** |
|
||||
|
||||
### 6.5 并发与顺序
|
||||
|
||||
- 同一会话连续发多条消息会并发启动多个后台任务。前端按 `message_id` / `created_at` 排序展示并去重,保证 AI 回复与用户消息对应正确。
|
||||
- 后台任务内对 `conversation.dify_conversation_id` 的更新需加简单锁或串行化,避免 Dify 多轮上下文错乱。
|
||||
|
||||
---
|
||||
|
||||
## 7. 备选方案说明
|
||||
|
||||
### B. SSE 流式接口
|
||||
- 发送接口改为 `StreamingResponse`,AI 回复逐字经 SSE 推回。
|
||||
- 优点:前端实现简单(EventSource)。
|
||||
- 风险:企微内嵌 WebView(尤其旧版 Android)对 SSE/长连接支持不稳定,且中间网关/反向代理可能缓冲或截断。
|
||||
- 结论:不如直接复用已验证的 WS 通道(方案 A)。
|
||||
|
||||
### C. 拆两接口快速止血
|
||||
- 新增 `POST /h5/conversations/current/messages/quick`(只存消息立即返回,不调 AI)。
|
||||
- AI 结果由现有 `pollMessages` 拉取(AI 消息落库后轮询可见)。
|
||||
- 优点:改动极小,半天可上。
|
||||
- 缺点:AI 回复延迟 = 轮询间隔(3s)+ 推理时间,体验一般,是过渡方案。
|
||||
- 适用:若 A 排期紧张,可先上 C 止血,再迭代到 A。
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险与缓解
|
||||
|
||||
| 风险 | 影响 | 缓解 |
|
||||
|---|---|---|
|
||||
| 跨 worker 广播静默丢失 | AI 回复约 50% 概率推不到员工端 | **硬性前置**:后端改 `--workers 1`(P1)或在 ws_manager 引入 Redis pub/sub(P2)。本方案取 P1 |
|
||||
| 后台任务进程重启丢失 | 个别在途 AI 回复丢失 | 消息已落库,前端 3s 轮询兜底;DB 为真相源,可接受 |
|
||||
| Dify 多轮上下文错乱 | AI 答非所问 | 后台任务内串行更新 `dify_conversation_id` |
|
||||
| WS 推送失败 | 前端看不到 AI 回复 | 现有 3s 轮询兜底;WS 广播已 try/except 不阻塞 |
|
||||
| 流式拼装 UI bug | 气泡重复/错位 | 用 `ai_reply` 完整事件去重校正 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 排期估算
|
||||
|
||||
| 任务 | 工时 |
|
||||
|---|---|
|
||||
| 后端:拆后台任务 + WS 事件 + 兜底 | 1 天 |
|
||||
| 前端:WS 监听 + 打字机拼装 + 状态修正 | 1 天 |
|
||||
| 联调 + 端到端测试(含失败场景) | 0.5–1 天 |
|
||||
| **合计** | **2.5–3 工作日** |
|
||||
|
||||
(若先上 C 止血,可压缩到 0.5 天,后续再迭代 A)
|
||||
|
||||
---
|
||||
|
||||
## 10. 验证方法(端到端,遵守"修复前必须提供真实证据")
|
||||
|
||||
1. **响应耗时对比**:curl 测改造前后 `/api/h5/conversations/current/messages` 的 TTFB。
|
||||
- 改造前:TTFB ≈ AI 推理耗时(3–15s)
|
||||
- 改造后:TTFB < 500ms(立即返回)
|
||||
2. **浏览器真实操作**:发消息 → 自己消息立即 `sent` → 观察 AI 以打字机形式到达。
|
||||
3. **失败场景**:mock AI 超时 → 验证 `ai_reply_failed` 兜底 + 转人工提示。
|
||||
4. **WS 断连**:断开 WS → 发消息 → 验证 3s 轮询能拉到 AI 回复。
|
||||
|
||||
---
|
||||
|
||||
## 11. 决策确认(2026-07-08 第二轮评审)
|
||||
|
||||
用户已就原 4 项待确认事项拍板,结论如下:
|
||||
|
||||
1. **流式 vs 整段推送 → 采用打字机(流式)**。方案 A 的 `ai_reply_chunk` 流式推送保留,前端打字机拼装。
|
||||
2. **后台任务可靠性 → 不引入 Redis/Celery,用进程内 task,硬性前置"单 worker"**。正式决策见 **ADR-001(§6.4.1)**:当前 `--workers 2` + 内存 broadcast 会导致 ~50% 广播丢失,必须先改 `--workers 1`(或后续上 P2 Redis pub/sub);单 worker 在当前体量下即为最优基线。
|
||||
3. **是否先 C 止血 → 否,直接上 A**。不做过渡方案,一步到位完成异步化 + 流式 WS 推送。
|
||||
4. **生产实测 → 跳过**。根因已在代码中确认(h5.py:897 同步 await),无需用真实数据佐证即可动手。详见下方说明。
|
||||
|
||||
### 关于"为什么要测 AI 耗时 / 为什么要测试账号"的说明
|
||||
|
||||
> 这是上一轮提出的**可选**佐证项,本次评估后认为**不是实施前提**:
|
||||
|
||||
- **为何当初提测 AI 耗时**:目的是量化 AI 实际耗时区间(用于设合理超时、判断优化空间),并排除"数据库写入 / WS 是否也有额外耗时"。属锦上添花,非必需。
|
||||
- **为何需要测试账号**:生产发送接口需鉴权(员工 OAuth token),curl 必须带有效 token 才能发真实消息并计时。
|
||||
- **为何可跳过**:根因已由代码铁证锁定(`h5.py:897` 的 `await ai_handler.handle_message` 与 `ai_service.py:114` 的 `stream:False`),无需实测也能修。
|
||||
- **若日后想量化,更轻的替代**(无需测试账号):
|
||||
- 在 `generate_ai_reply` 内对 AI 调用前后打点日志(如 `logger.info(f"AI cost={elapsed}s")`),从现有日志即可看到真实耗时;
|
||||
- 或在 staging / 本地用 dev token 直接 curl。
|
||||
- **结论**:直接进入方案 A 实施,实测步骤不阻塞。
|
||||
|
||||
### 实施前置清单(直接上 A)
|
||||
|
||||
| 顺序 | 动作 | 涉及文件 | 备注 |
|
||||
|---|---|---|---|
|
||||
| 0 | **后端改 `--workers 1`**(ADR-001) | `docker-compose.yml:151` | 硬性前置,否则 50% 收不到 AI 回复;改动 docker-compose 前须对照 `docs/09-部署运维/00-标准故障排查手册.md` 的 Redis 地雷 |
|
||||
| 1 | 后端:拆后台任务 `generate_ai_reply` + 流式 WS 事件 | `backend/app/api/h5.py` | 复用 `ai_service.get_reply_stream` |
|
||||
| 2 | 前端:WS 监听 `ai_reply_chunk/ai_reply/ai_reply_failed` + 打字机拼装 + 状态修正 | `frontend-h5/src/stores/conversation.ts` | 响应返回即置 `sent` |
|
||||
| 3 | 联调 + 端到端验证(含失败/WS 断连场景) | — | 遵守"修复前必须提供真实证据"硬规则 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 实施进度记录(2026-07-07)
|
||||
|
||||
> 用户拍板"直接开工",方案 A 全部改造已落地。下方为各 TASK 实际代码落点 + 验证结论。
|
||||
|
||||
### 12.1 已完成的代码改动
|
||||
|
||||
| TASK | 文件 | 改动要点 |
|
||||
|---|---|---|
|
||||
| #0 单 worker 前置 | `docker-compose.yml:151` | `--workers 2` → `--workers 1`(Redis 段未动,规避"Redis 地雷") |
|
||||
| #3 发送接口异步返回 | `backend/app/api/h5.py` `h5_send_message` | 移除同步 `await handle_message`;仅广播用户消息给坐席 → `asyncio.create_task(process_h5_ai_reply(...))` → 立即返回 `ai_reply: None` |
|
||||
| #4 后台任务模块(新建) | `backend/app/tasks/h5_ai_task.py` | `process_h5_ai_reply`:本地快判断(打招呼/呼叫人工)走同步路径;否则 `get_reply_stream` 逐 chunk 推 `ai_reply_chunk`,结束推 `ai_reply` 终态;异常推 `ai_reply_failed`。独立 DB session(`_get_session_factory`) |
|
||||
| #5 真流式 SSE | `backend/app/services/ai_service.py` `get_reply_stream` | 由"假流式(一次性整段)"改为真 SSE 解析(`stream:True` + 逐行 `data:` 解析);解析失败时降级回非流式,功能不丢 |
|
||||
| #6 WS 事件分发 | `frontend-h5/src/composables/useH5WebSocket.ts` | `handleMessage` switch 新增 `ai_reply_chunk` / `ai_reply` / `ai_reply_failed` 三分支;`onclose` 增加 `cancelStreamingBubble()` 清理半成品气泡 |
|
||||
| #7 前端打字机 store | `frontend-h5/src/stores/conversation.ts` | 新增 `handleAiReplyChunk`(首 chunk 建占位气泡、后续累积 content)、`handleAiReply`(真实消息替换占位 + 去重登记 + 同步计数/可呼叫坐席/状态)、`handleAiReplyFailed`、`cancelStreamingBubble`;`sendNewMessage` 移除对已废弃 `resp.ai_reply` 的依赖 |
|
||||
| #7 接口类型 | `frontend-h5/src/api/conversation.ts` | `SendMessageResponse.ai_reply` 改为 `Message \| null`(后端已恒为 null,AI 回复走 WS) |
|
||||
|
||||
### 12.2 WS 事件协议(最终落地形态,与 6.3 略有差异,以此为准)
|
||||
|
||||
| 事件 type | 消费方 | data 关键字段 |
|
||||
|---|---|---|
|
||||
| `ai_reply_chunk` | H5 员工端 | `conversation_id`, `chunk`(逐字片段) |
|
||||
| `ai_reply` | H5 员工端 | `message_id`, `sender_type`, `content`, `ai_reply_count`, `can_call_agent`, `conversation_status`(扁平字段,非嵌套 Message) |
|
||||
| `ai_reply_failed` | H5 员工端 | `conversation_id`, `message` |
|
||||
|
||||
> 注意:员工端**只**经 `broadcast_to_employees` 收到上述三类事件(`ws_manager.broadcast` 仅发坐席端),故无重复 `new_message` 风险;`handleAiReply` 仍登记真实 `message_id` 到去重集,兼容 3s 轮询兜底重复拉取。
|
||||
|
||||
### 12.3 验证结论(遵守"修复前必须提供真实证据")
|
||||
|
||||
| 层 | 验证手段 | 结果 |
|
||||
|---|---|---|
|
||||
| 后端 | `py_compile` + import `app.tasks.h5_ai_task / app.api.h5 / app.services.ai_service` | IMPORT_OK |
|
||||
| 前端(类型) | `vue-tsc --noEmit`,过滤本次改动文件 | **0 错误**(`conversation.ts` / `useH5WebSocket.ts` / `api/conversation.ts` 全部通过) |
|
||||
| 前端(打包) | `vite build` | EXIT=0,`dist/index.html` 及全部 chunk 产出成功 |
|
||||
|
||||
**遗留已知项(非本次范围,已记录但不在本任务修复):**
|
||||
- `src/api/automation.ts`、`src/api/message.ts`、`src/api/troubleshooting-templates.ts` 共 10 处 `vue-tsc` 类型错误,源于"响应契约方案A"拦截器改造后这些 API 仍把 `AxiosResponse` 强转内层类型,与本次改造无关。不影响 `vite build`(esbuild 不做类型检查),但会导致 `npm run build`(`vue-tsc && vite build`)整体失败。建议单独排期清理。
|
||||
- 浏览器端到端实测(发送→打字机→落定 / 失败兜底 / WS 断连轮询兜底)需部署后经 堡垒机 `jms_ops` + 测试账号验证,用户已决策**跳过生产实测**,故未执行运行时验证。代码路径已具备,待部署后由 QA 走真实 WebView 确认。
|
||||
|
||||
---
|
||||
|
||||
## 附:关键代码位置速查
|
||||
|
||||
| 文件 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| `backend/app/api/h5.py` | `h5_send_message` 821–990 | 发送接口(897 行串行 await AI) |
|
||||
| `backend/app/services/ai_service.py` | 114 / 196 | 非流式调用 / 已存在的流式 `get_reply_stream` |
|
||||
| `backend/app/api/h5.py` | 937–972 | `ws_manager.broadcast` 现有推送 |
|
||||
| `frontend-h5/src/api/conversation.ts` | 281–297 | `sendMessage`(timeout 30s) |
|
||||
| `frontend-h5/src/stores/conversation.ts` | 463 / 485 | 乐观更新 / await 响应 |
|
||||
Reference in New Issue
Block a user