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

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_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/权限差异)

  1. 响应envelope差异 — Gochat统一用 { success, data }, Chatwoot各端点格式各异; 需评估是否需要为特定端点去掉wrapper
  2. 权限过滤缺失 — UnreadCounts缺少用户权限模式过滤和feature flag门控

P2 — 已知基础设施差异 (可后续迭代)

  1. TRANSCRIPT邮件基础设施 — Gochat无邮件发送能力, 需集成邮件服务
  2. 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迁移需要基础设施支持