11 KiB
11 KiB
验收报告 — 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_countsfeature) - 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 — 阻塞合并 (响应格式/路径/业务逻辑不一致)
- META响应结构不一致 — 需改为
{ meta: { mine_count, assigned_count, unassigned_count, all_count } }, 加用户上下文过滤 - PARTICIPANTS路径+参数+响应全部不一致 — 需改为singular resource, 参数统一用user_ids, 响应返回Agent详情
- DRAFT_MESSAGES路径+参数+响应全部不一致 — 需改为singular resource, show返回
{ has_draft, message }, update/destroy返回空200 - UNREAD业务逻辑不一致 — agent_last_seen_at应设为 last_incoming_message.created_at - 1秒
P1 — 建议修 (envelope/权限差异)
- 响应envelope差异 — Gochat统一用
{ success, data }, Chatwoot各端点格式各异; 需评估是否需要为特定端点去掉wrapper - 权限过滤缺失 — UnreadCounts缺少用户权限模式过滤和feature flag门控
P2 — 已知基础设施差异 (可后续迭代)
- TRANSCRIPT邮件基础设施 — Gochat无邮件发送能力, 需集成邮件服务
- DRAFT_MESSAGES存储架构 — Redis vs DB, 功能等价但架构不同
测试验证结果
handler tests: PASS (0.117s)
service tests: PASS (0.852s)
repo tests: PASS (1.278s)
所有现有测试通过, 但测试覆盖基于当前(不一致的)实现。修正差项后需更新对应测试。
建议
- P0差项需在合并前修正 — 否则API与Chatwoot不兼容, 前端无法对接
- 建议创建CTO dispatch任务 — 拆分为6个子任务分别修正差项
- P2差项可在后续迭代中处理 — TRANSCRIPT邮件集成和DRAFT Redis迁移需要基础设施支持