# 验收报告 — 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迁移需要基础设施支持