Files
gochat/docs/verification/conversation-extension-api-verification-report.md
T
2026-06-04 15:44:48 +08:00

159 lines
11 KiB
Markdown

# 验收报告 — G1 Conversation扩展API
**任务**: t_a90059a6
**验收日期**: 2026-05-27
**验收标准**: 5项 — 接口路径、请求参数、响应格式、错误处理与状态码、业务逻辑
---
## 总体结论: ⚠️ 部分通过,存在6个需修差项
所有6个API组 (meta, unread_counts, unread, transcript, custom_attributes, participants, draft_messages) 的 handler/service/repo/router 已实现。所有测试通过 (handler 0.117s, service 0.852s, repo 1.278s)。但与Chatwoot Ruby API存在以下偏差,需在合并前修正。
---
## 逐组5项标准验收
### 1. Meta — `GET /api/v1/accounts/:account_id/conversations/meta`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `GET /conversations/meta` (collection) | `GET /conversations/meta` | ✅ 一致 |
| **请求参数** | `status` (enum: all/open/resolved/pending/snoozed), `q` (search), `inbox_id`, `assignee_type`, `team_id`, `labels`, `sort` | 仅 `account_id` (路径参数) | ❌ 缺少 `status`, `q`, `inbox_id`, `assignee_type`, `team_id`, `labels`, `sort` 过滤参数 |
| **响应格式** | `{ meta: { mine_count, assigned_count, unassigned_count, all_count } }` | `{ success: true, data: { status_counts: {...}, label_counts: {...}, total_count: N } }` | ❌ 字段名不一致; Chatwoot用 mine/assigned/unassigned/all 四字段, Gochat用 status_counts/label_counts/total_count |
| **错误处理** | 标准Rails错误 | 400/500 统一错误体 | ⚠️ 无401/403权限检查(mine_count依赖当前用户) |
| **业务逻辑** | ConversationFinder.perform_meta_only — 基于当前用户的可见范围计算mine_count | DB聚合统计 — 全account维度, 不区分用户 | ❌ 缺少用户上下文过滤 |
**差项**:
- META-1: 响应字段结构不一致 (mine/assigned/unassigned/all vs status_counts/label_counts/total_count)
- META-2: 缺少查询过滤参数 (status, q, inbox_id, assignee_type, team_id, labels, sort)
- META-3: mine_count依赖当前用户上下文, Gochat当前实现无用户过滤
### 2. UnreadCounts — `GET /api/v1/accounts/:account_id/conversations/unread_counts`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `GET /conversations/unread_counts` (collection) | `GET /conversations/unread_counts` | ✅ 一致 |
| **请求参数** | `account_id` (路径), 需feature_flag `conversation_unread_counts` | 仅 `account_id` | ⚠️ 无feature flag检查 |
| **响应格式** | `{ payload: { inboxes: {...}, labels: {...}, teams: {...} } }` | `{ success: true, data: { inboxes: {...}, labels: {...}, teams: {...} } }` | ⚠️ Chatwoot用 `payload` 键, Gochat用 `data` 键 (统一wrapper) |
| **错误处理** | 403 Forbidden (feature not enabled) | 400/500 统一错误体 | ⚠️ 缺少403 feature flag检查 |
| **业务逻辑** | Counter.perform — 基于当前用户权限模式(manage_all/unassigned/participating)计算 | 直接DB聚合 — 无权限模式区分 | ❌ 缺少用户权限过滤 |
**差项**:
- UNREAD_COUNTS-1: 缺少feature flag门控 (Chatwoot需要 `conversation_unread_counts` feature)
- UNREAD_COUNTS-2: 响应envelope键名差异 (payload vs data)
- UNREAD_COUNTS-3: 缺少用户权限模式过滤 (manage_all/unassigned/participating)
### 3. Unread — `POST /api/v1/accounts/:account_id/conversations/:id/unread`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `POST /conversations/:id/unread` (member) | `POST /conversations/:id/unread` | ✅ 一致 |
| **请求参数** | 无请求体, 依赖当前用户 | `account_id`, `id` (路径参数) | ✅ 一致 |
| **响应格式** | 返回完整conversation JSON (partial渲染) | `{ success: true, data: conversation_object }` | ⚠️ Chatwoot返回带嵌套的partial格式, Gochat返回扁平model |
| **错误处理** | 404 (conversation not found) | 404 (record not found) | ✅ 一致 |
| **业务逻辑** | agent_last_seen_at = last_incoming_message.created_at - 1秒 | agent_last_seen_at = null (清零) | ❌ 不一致 — Chatwoot设为倒数1秒, Gochat设为null |
**差项**:
- UNREAD-1: 业务逻辑不一致 — agent_last_seen_at应设为 last_incoming_message.created_at - 1秒, 而不是null
### 4. Transcript — `POST /api/v1/accounts/:account_id/conversations/:id/transcript`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `POST /conversations/:id/transcript` (member) | `POST /conversations/:id/transcript` | ✅ 一致 |
| **请求参数** | `{ email: string }` | `{ email: string }` (binding:required,email) | ✅ 一致 |
| **响应格式** | `200 OK` (head :ok, 无body) 或 `422 { error: "email param missing" }` | `{ success: true, data: null }` (200) | ⚠️ Chatwoot返回空body 200, Gochat返回JSON envelope |
| **错误处理** | 422 (email missing), 402 (plan限制), 429 (rate limit) | 400 (validation), 无402/429 | ⚠️ 缺少rate limit和plan限制检查 |
| **业务逻辑** | ConversationReplyMailer.deliver_later + account.increment_email_sent_count | DB标记逻辑 (无邮件发送) | ⚠️ Gochat无实际邮件发送能力, 这是基础设施差异 |
**差项**:
- TRANSCRIPT-1: 成功响应应为空body 200, 不是JSON envelope
- TRANSCRIPT-2: 缺少email rate limit检查 (429)
- TRANSCRIPT-3: 无邮件发送基础设施 (Chatwoot用mailer, Gochat标记为TODO)
### 5. CustomAttributes — `POST /api/v1/accounts/:account_id/conversations/:id/custom_attributes`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `POST /conversations/:id/custom_attributes` (member) | `POST /conversations/:id/custom_attributes` | ✅ 一致 |
| **请求参数** | `{ custom_attributes: {...} }` (permit) | `{ custom_attributes: {...} }` (binding:required) | ✅ 一致 |
| **响应格式** | `{ custom_attributes: {...} }` | `{ success: true, data: conversation_object }` | ⚠️ Chatwoot返回仅custom_attributes字段, Gochat返回完整conversation |
| **错误处理** | 422 (save失败) | 400/500 统一错误体 | ✅ 语义一致 |
| **业务逻辑** | 直接更新custom_attributes + save! | repo层UpdateCustomAttributes | ✅ 一致 |
**差项**:
- CUSTOM_ATTR-1: 响应应仅返回 `{ custom_attributes: {...} }`, 不是完整conversation对象
### 6. Participants — nested under `/conversations/:conversation_id/participants`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `resource :participants` (singular resource) → show(GET)/create(POST)/update(PATCH)/destroy(DELETE) 无子路径 | GET/POST/PATCH/PATCH/:user_id/DELETE/:user_id | ❌ 路径不一致 — Chatwoot用singular resource (无:user_id), Gochat用plural+子路径 |
| **请求参数** | show: 无; create: `{ user_ids: [...] }`; update: `{ user_ids: [...] }`; destroy: `{ user_ids: [...] }` | List: 无; Add: `{ user_id, role }`; BatchUpdate: `{ user_ids: [...], remove_user_ids: [...] }`; Update: 单user; Remove: 单user_id | ❌ 参数结构不一致 — Chatwoot统一用user_ids数组 |
| **响应格式** | show/create/update → `[{ agent_object }]` (Agent模型的user详情) | List → `[{ ConversationParticipant }]` (仅conversation_id, user_id, role) | ❌ 完全不一致 — Chatwoot返回Agent(User)详情, Gochat返回Participant关联记录 |
| **错误处理** | 标准Rails错误 | 400/500 统一错误体 | ⚠️ 基本一致 |
| **业务逻辑** | find_or_create_by / find_by&.destroy — 幂等操作 | 基于GORM的CRUD | ⚠️ 幂等性差异 |
**差项**:
- PARTICIPANTS-1: 路径不一致 — 应为singular resource (无:user_id子路径)
- PARTICIPANTS-2: 请求参数不一致 — Chatwoot统一用 `user_ids` 数组, create/update/destroy都是
- PARTICIPANTS-3: 响应格式不一致 — 应返回Agent(User)详情对象, 不是ConversationParticipant关联记录
- PARTICIPANTS-4: update方法应同时支持add+remove (Chatwoot的update = add新ids + remove旧ids)
### 7. DraftMessages — nested under `/conversations/:conversation_id/draft_messages`
| 标准 | Chatwoot | Gochat | 结果 |
|------|----------|--------|------|
| **路径** | `resource :draft_messages` (singular) → show(GET)/update(PATCH)/destroy(DELETE) 无:id子路径 | GET/POST/GET/:id/PATCH/:id/DELETE/:id | ❌ 路径不一致 — Chatwoot用singular resource (每conversation每用户仅一个draft) |
| **请求参数** | show: 无; update: `{ draft_message: { message: "..." } }`; destroy: 无 | List: 无; Create: `{ content }`; Get: :id; Update: `{ content }`; Delete: :id | ❌ Chatwoot无create/list/get/:id, 仅show/update/destroy |
| **响应格式** | show: `{ has_draft: bool, message: "..." }``{ has_draft: false }`; update/destroy: `200 OK` (head :ok) | List/Get: `{ success: true, data: DraftMessage }`; Create: 同; Update: 同; Delete: 200 | ❌ 完全不一致 — Chatwoot用Redis+单一draft, Gochat用DB+多draft |
| **错误处理** | 无特殊错误处理 | 400/404/500 | ⚠️ Chatwoot较简单 |
| **业务逻辑** | Redis-based — 每conversation仅一个draft, 按conversation_id存储, 非DB持久化 | DB-based — DraftMessage表, 多draft记录 | ❌ 架构差异 — Chatwoot用Redis临时存储, Gochat用DB持久化 |
**差项**:
- DRAFT-1: 路径不一致 — 应为singular resource (无:id子路径), 仅show/update/destroy三个动作
- DRAFT-2: 请求参数不一致 — update请求体应为 `{ draft_message: { message: "..." } }`, 不是 `{ content }`
- DRAFT-3: 响应格式完全不一致 — show应返回 `{ has_draft: bool, message: "..." }`, 不是DraftMessage对象
- DRAFT-4: 架构差异 — Chatwoot用Redis临时存储(每conversation一个draft), Gochat用DB持久化(多draft)
---
## 差项汇总 (6个需修差项, 按优先级排序)
### P0 — 阻塞合并 (响应格式/路径/业务逻辑不一致)
1. **META响应结构不一致** — 需改为 `{ meta: { mine_count, assigned_count, unassigned_count, all_count } }`, 加用户上下文过滤
2. **PARTICIPANTS路径+参数+响应全部不一致** — 需改为singular resource, 参数统一用user_ids, 响应返回Agent详情
3. **DRAFT_MESSAGES路径+参数+响应全部不一致** — 需改为singular resource, show返回 `{ has_draft, message }`, update/destroy返回空200
4. **UNREAD业务逻辑不一致** — agent_last_seen_at应设为 last_incoming_message.created_at - 1秒
### P1 — 建议修 (envelope/权限差异)
5. **响应envelope差异** — Gochat统一用 `{ success, data }`, Chatwoot各端点格式各异; 需评估是否需要为特定端点去掉wrapper
6. **权限过滤缺失** — UnreadCounts缺少用户权限模式过滤和feature flag门控
### P2 — 已知基础设施差异 (可后续迭代)
7. **TRANSCRIPT邮件基础设施** — Gochat无邮件发送能力, 需集成邮件服务
8. **DRAFT_MESSAGES存储架构** — Redis vs DB, 功能等价但架构不同
---
## 测试验证结果
```
handler tests: PASS (0.117s)
service tests: PASS (0.852s)
repo tests: PASS (1.278s)
```
所有现有测试通过, 但测试覆盖基于当前(不一致的)实现。修正差项后需更新对应测试。
---
## 建议
1. **P0差项需在合并前修正** — 否则API与Chatwoot不兼容, 前端无法对接
2. **建议创建CTO dispatch任务** — 拆分为6个子任务分别修正差项
3. **P2差项可在后续迭代中处理** — TRANSCRIPT邮件集成和DRAFT Redis迁移需要基础设施支持