feat(adapter): add sub2api platform adapter stack
This commit is contained in:
@@ -51,6 +51,24 @@
|
||||
| `AI_CS_WEBHOOK_SIGNATURE_HEADER` | `X-CS-Signature` | 签名请求头 | 无额外校验 | 可 |
|
||||
| `AI_CS_WEBHOOK_MAX_SKEW_SECONDS` | `300` | 最大时钟偏差(秒) | 必须 > 0 | 需安全确认 |
|
||||
|
||||
### 1.4 Platform Adapters
|
||||
|
||||
| 变量名 | 默认值 | 含义 | 当前代码是否校验 | prod 是否应允许默认值 |
|
||||
|---|---|---|---|---|
|
||||
| `AI_CS_PLATFORM_ADAPTERS_ENABLED` | `false` | 是否启用平台适配入口 | 解析布尔值 | 视接入计划决定 |
|
||||
| `AI_CS_PLATFORM_SUB2API_ENABLED` | `false` | 是否启用 `sub2api` 入站适配 | 解析布尔值 | 视接入计划决定 |
|
||||
| `AI_CS_PLATFORM_SUB2API_INGRESS_SECRET` | 空 | `sub2api` 平台 webhook HMAC secret | 启用 `sub2api` 时必填 | **不允许为空** |
|
||||
| `AI_CS_PLATFORM_SUB2API_CALLBACK_BASE_URL` | 空 | `sub2api` 回调基地址 | 当前仅解析,不强校验 | 视后续出站回调批次决定 |
|
||||
| `AI_CS_PLATFORM_SUB2API_CALLBACK_SECRET` | 空 | `sub2api` 回调签名 secret | 当前仅解析,不强校验 | 视后续出站回调批次决定 |
|
||||
| `AI_CS_PLATFORM_SUB2API_CALLBACK_TIMEOUT_MS` | `3000` | `sub2api` 回调超时(毫秒) | 必须 > 0(启用时) | 可 |
|
||||
| `AI_CS_PLATFORM_SUB2API_CALLBACK_MAX_RETRIES` | `5` | `sub2api` 回调最大重试次数 | 必须 >= 0(启用时) | 可 |
|
||||
| `AI_CS_PLATFORM_NEWAPI_ENABLED` | `false` | 是否启用 `newapi` 入站适配 | 解析布尔值 | 视接入计划决定 |
|
||||
| `AI_CS_PLATFORM_NEWAPI_INGRESS_SECRET` | 空 | `newapi` 平台 webhook HMAC secret | 启用 `newapi` 时必填 | **不允许为空** |
|
||||
| `AI_CS_PLATFORM_NEWAPI_CALLBACK_BASE_URL` | 空 | `newapi` 回调基地址 | 当前仅解析,不强校验 | 视后续出站回调批次决定 |
|
||||
| `AI_CS_PLATFORM_NEWAPI_CALLBACK_SECRET` | 空 | `newapi` 回调签名 secret | 当前仅解析,不强校验 | 视后续出站回调批次决定 |
|
||||
| `AI_CS_PLATFORM_NEWAPI_CALLBACK_TIMEOUT_MS` | `3000` | `newapi` 回调超时(毫秒) | 必须 > 0(启用时) | 可 |
|
||||
| `AI_CS_PLATFORM_NEWAPI_CALLBACK_MAX_RETRIES` | `5` | `newapi` 回调最大重试次数 | 必须 >= 0(启用时) | 可 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前代码已经执行的校验
|
||||
@@ -64,6 +82,9 @@
|
||||
5. `AI_CS_RUNTIME_ENV` 只允许 `production/development/test`
|
||||
6. `AI_CS_RUNTIME_ENV=production` 时,`AI_CS_POSTGRES_ENABLED` 必须为 `true`
|
||||
7. `AI_CS_RUNTIME_ENV=production` 时,`AI_CS_WEBHOOK_SECRET` 不允许为空
|
||||
8. `AI_CS_PLATFORM_ADAPTERS_ENABLED=true` 且对应平台 `*_ENABLED=true` 时,`*_INGRESS_SECRET` 不允许为空
|
||||
9. `AI_CS_PLATFORM_*_CALLBACK_TIMEOUT_MS` 在对应平台启用时必须为正数
|
||||
10. `AI_CS_PLATFORM_*_CALLBACK_MAX_RETRIES` 在对应平台启用时不允许为负数
|
||||
|
||||
---
|
||||
|
||||
@@ -86,6 +107,7 @@
|
||||
- `DATABASE_URL`
|
||||
- `POSTGRES_*`
|
||||
- `WEBHOOK_SECRET`
|
||||
- `AI_CS_PLATFORM_*`
|
||||
- `RATE_LIMIT_*`
|
||||
- `LOG_LEVEL`
|
||||
- `OPENAI_API_KEY`
|
||||
|
||||
107
docs/RUNBOOK_PLATFORM_CALLBACKS.md
Normal file
107
docs/RUNBOOK_PLATFORM_CALLBACKS.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Platform Callback Runbook
|
||||
|
||||
> 适用范围:`sub2api / newapi` 平台适配层的出站 callback 投递
|
||||
> 当前实现事实来源:`internal/store/postgres/platform_event_store.go`、`internal/service/platformdelivery/worker.go`
|
||||
|
||||
---
|
||||
|
||||
## 1. 快速判断
|
||||
|
||||
平台回调链路分三层状态:
|
||||
|
||||
1. **主链成功,outbox 已入库**
|
||||
表:`cs_platform_event_outbox`
|
||||
2. **callback 尝试记录**
|
||||
表:`cs_platform_event_delivery_attempts`
|
||||
3. **重试耗尽进入死信**
|
||||
表:`cs_platform_event_dead_letters`
|
||||
|
||||
如果用户反馈“平台没收到回调”,先按这个顺序查,不要直接看应用日志猜。
|
||||
|
||||
---
|
||||
|
||||
## 2. 常用查询
|
||||
|
||||
### 2.1 查看待投递事件
|
||||
|
||||
```sql
|
||||
SELECT id, platform, event_type, callback_target, status, attempt_count, next_attempt_at, last_error
|
||||
FROM cs_platform_event_outbox
|
||||
WHERE status IN ('pending', 'retrying')
|
||||
ORDER BY next_attempt_at ASC, created_at ASC
|
||||
LIMIT 100;
|
||||
```
|
||||
|
||||
### 2.2 查看最近投递尝试
|
||||
|
||||
```sql
|
||||
SELECT event_id, attempt_no, response_status, error_message, created_at
|
||||
FROM cs_platform_event_delivery_attempts
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100;
|
||||
```
|
||||
|
||||
### 2.3 查看死信事件
|
||||
|
||||
```sql
|
||||
SELECT event_id, platform, event_type, callback_target, attempt_count, final_error, created_at
|
||||
FROM cs_platform_event_dead_letters
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 故障分类
|
||||
|
||||
### 3.1 平台回调失败
|
||||
|
||||
表现:
|
||||
- `cs_platform_event_outbox.status` 为 `retrying` 或 `dead_letter`
|
||||
- `cs_platform_event_delivery_attempts` 有记录
|
||||
|
||||
说明:
|
||||
- 主链已经处理成功
|
||||
- 失败点在平台 callback 出站链路
|
||||
|
||||
### 3.2 主链失败
|
||||
|
||||
表现:
|
||||
- 平台入口直接返回 `500`
|
||||
- `cs_platform_event_outbox` 没有对应事件
|
||||
|
||||
说明:
|
||||
- 失败点在 webhook 入站、dialog 主链或 outbox 写入
|
||||
- 这不属于 callback worker 故障
|
||||
|
||||
---
|
||||
|
||||
## 4. 手动重放
|
||||
|
||||
当前版本没有单独重放脚本,最小操作方式是把死信或重试事件改回可投递状态:
|
||||
|
||||
```sql
|
||||
UPDATE cs_platform_event_outbox
|
||||
SET status = 'pending',
|
||||
next_attempt_at = NOW(),
|
||||
last_error = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE id = '<event_id>';
|
||||
```
|
||||
|
||||
如果事件已经在 `dead_letters`:
|
||||
|
||||
```sql
|
||||
DELETE FROM cs_platform_event_dead_letters
|
||||
WHERE event_id = '<event_id>';
|
||||
```
|
||||
|
||||
再等待 worker 下一轮拉取。
|
||||
|
||||
---
|
||||
|
||||
## 5. 处理原则
|
||||
|
||||
1. 不要手工删除 `outbox` 主记录,除非已经确认平台侧不需要这条事件。
|
||||
2. 优先保留 `delivery_attempts` 和 `dead_letters`,它们是排障证据。
|
||||
3. 如果同一平台持续大量 `retrying`,优先检查 callback 地址、签名 secret 和平台上游可用性。
|
||||
332
docs/plans/2026-05-06-newapi-sub2api-adapter-design.md
Normal file
332
docs/plans/2026-05-06-newapi-sub2api-adapter-design.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# NewAPI / Sub2API 适配增强设计
|
||||
|
||||
> 日期:2026-05-06
|
||||
> 状态:设计稿
|
||||
> 适用项目:`projects/ai-customer-service`
|
||||
> 设计边界:**最小接入层、内置适配器、入站 + 异步全事件流回写、Sub2API 优先、准可靠投递**
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
本设计解决的问题不是“把 `ai-customer-service` 做成另一个 NewAPI/Sub2API”,而是让它能够**稳定挂接在 NewAPI/Sub2API 后面,作为客服能力子系统运行**。当前代码已经具备 webhook、会话、意图、转人工、工单、审计、去重、PostgreSQL 落库、Gate B/Gate C 脚本化验证等底座,缺的是把外部平台原生消息接进来、再把内部处理结果以平台可消费的事件流回推出去的适配层。
|
||||
|
||||
第一版范围严格限制为:
|
||||
|
||||
1. **Sub2API 优先**,NewAPI 保持同构兼容位,不追求双平台一次做满。
|
||||
2. **内置适配器**,不新增外部 shim 作为主路径。
|
||||
3. **入站适配**:把平台原生消息转换为 `UnifiedMessage` 并进入现有主链。
|
||||
4. **出站回写**:把内部处理结果、工单、错误、回调状态转成异步事件回推给上游平台。
|
||||
5. **准可靠投递**:事件持久化、重试、死信/补偿到位,但不追求复杂的跨系统 exactly-once。
|
||||
|
||||
明确不做的内容:
|
||||
|
||||
1. 完整平台级管理后台
|
||||
2. 知识库共享 API 的全量产品化
|
||||
3. NewAPI/Sub2API 全量管理协议一比一兼容
|
||||
4. 任意平台原生结构透传
|
||||
|
||||
结论是:**第一版目标是“可稳定接入和可观测回推”,不是“完整兼容替代”。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体架构
|
||||
|
||||
推荐架构是在现有 HTTP 入口和对话主链之间插入一个**平台适配层(Platform Adapter Layer)**,并在主链处理完成后插入一个**事件出站层(Event Outbox + Delivery Layer)**。这样可以保持当前客服核心逻辑不被平台协议污染,同时把平台差异收口在边缘。
|
||||
|
||||
逻辑结构如下:
|
||||
|
||||
```text
|
||||
Sub2API / NewAPI
|
||||
-> Platform Ingress Handler
|
||||
-> Adapter Registry
|
||||
-> Platform Adapter (normalize)
|
||||
-> UnifiedMessage
|
||||
-> dialog / intent / handoff / ticket / audit / dedup
|
||||
-> Internal Domain Events
|
||||
-> Event Outbox
|
||||
-> Delivery Worker
|
||||
-> Platform Callback Endpoint
|
||||
```
|
||||
|
||||
核心原则:
|
||||
|
||||
1. **核心主链不感知平台细节**
|
||||
`dialog.Service` 继续只消费 `UnifiedMessage`,不直接理解 Sub2API/NewAPI 原生字段。
|
||||
|
||||
2. **适配逻辑边缘化**
|
||||
平台差异集中在 adapter 目录中,用接口抽象隔离。
|
||||
|
||||
3. **事件先落库再投递**
|
||||
所有异步回调事件进入 outbox 后再由 worker 重试发送,避免平台短时不可用导致结果丢失。
|
||||
|
||||
4. **同步 HTTP 只做最小确认**
|
||||
入站请求同步返回“收到并入链”的最小响应,不在主请求路径里等待整条回调链路完成。
|
||||
|
||||
这样做的收益是:现有 webhook 主链、Gate B/Gate C 验证、鉴权、工单状态机都可以复用,不需要重写核心业务。
|
||||
|
||||
---
|
||||
|
||||
## 3. 入站适配设计
|
||||
|
||||
第一版入站适配增加一个新的入口族,而不是强行把平台原生大包塞进现有 `UnifiedMessage` handler。建议新增:
|
||||
|
||||
```text
|
||||
POST /api/v1/customer-service/platforms/{platform}/webhook
|
||||
POST /api/v1/customer-service/platforms/{platform}/webhook/{channel}
|
||||
```
|
||||
|
||||
其中 `{platform}` 第一版支持:
|
||||
|
||||
1. `sub2api`
|
||||
2. `newapi`(保留同构位,可先实现最小 profile)
|
||||
|
||||
当前状态补充:
|
||||
- `sub2api` 已完成第一版最小接入、outbox、callback worker、dead letter 和 E2E 验证
|
||||
- `newapi` 当前仅保留同构 adapter profile,占位返回 `501 profile not implemented`
|
||||
|
||||
新增接口:
|
||||
|
||||
```go
|
||||
type PlatformAdapter interface {
|
||||
Platform() string
|
||||
ParseInbound(*http.Request, []byte, IngressContext) (*message.UnifiedMessage, *PlatformInboundMeta, error)
|
||||
BuildIngressAck(*dialog.Result, *PlatformInboundMeta) any
|
||||
}
|
||||
```
|
||||
|
||||
设计要点:
|
||||
|
||||
1. **平台原生请求体不再直接喂给现有 webhook handler**
|
||||
先在 adapter 里裁剪、校验、映射,再构造 `UnifiedMessage`。
|
||||
|
||||
2. **保留平台元数据**
|
||||
`PlatformInboundMeta` 记录:
|
||||
- platform
|
||||
- tenant / app / upstream endpoint
|
||||
- raw event id
|
||||
- callback target
|
||||
- callback auth profile
|
||||
- source user/session ids
|
||||
|
||||
3. **统一进入现有主链**
|
||||
Adapter 输出只允许是干净的 `UnifiedMessage`,这样 `dialog.Service`、dedup、ticket、audit 无需大改。
|
||||
|
||||
4. **同步确认最小化**
|
||||
入站 HTTP 响应只表达:
|
||||
- `accepted`
|
||||
- `event_id`
|
||||
- `session_id`(如果已生成)
|
||||
不承担完整业务结果回写职责。
|
||||
|
||||
Sub2API 优先意味着第一版先针对 tksea 场景定义一个明确的 inbound profile,而不是试图抽象所有平台差异。
|
||||
|
||||
---
|
||||
|
||||
## 4. 出站全事件流设计
|
||||
|
||||
你明确要求第一版不是只回最终结果,而是做**全事件流异步回调**。这意味着需要在内部定义一个稳定的事件模型,而不是拿日志拼 webhook。
|
||||
|
||||
建议的事件类型:
|
||||
|
||||
1. `message.received`
|
||||
2. `message.rejected`
|
||||
3. `message.deduplicated`
|
||||
4. `message.processing`
|
||||
5. `intent.resolved`
|
||||
6. `handoff.triggered`
|
||||
7. `ticket.created`
|
||||
8. `ticket.assigned`
|
||||
9. `ticket.resolved`
|
||||
10. `ticket.closed`
|
||||
11. `reply.generated`
|
||||
12. `callback.delivered`
|
||||
13. `callback.failed`
|
||||
|
||||
事件统一结构建议:
|
||||
|
||||
```json
|
||||
{
|
||||
"event_id": "uuid",
|
||||
"event_type": "reply.generated",
|
||||
"platform": "sub2api",
|
||||
"occurred_at": "2026-05-06T12:00:00Z",
|
||||
"session_id": "uuid",
|
||||
"ticket_id": "uuid",
|
||||
"source_message_id": "platform-msg-id",
|
||||
"attempt": 1,
|
||||
"payload": {}
|
||||
}
|
||||
```
|
||||
|
||||
关键设计点:
|
||||
|
||||
1. **事件类型稳定、字段尽量固定**
|
||||
2. **事件 payload 面向平台消费,而不是内部 debug**
|
||||
3. **每条事件必须有 `event_id` 供下游幂等**
|
||||
4. **reply / handoff / ticket 是关键事件,必须可补偿重放**
|
||||
|
||||
这样第一版虽然不是完整平台集成,但已经具备后续扩展到状态同步、工单联动和运营侧诊断的事件基础。
|
||||
|
||||
---
|
||||
|
||||
## 5. 准可靠投递设计
|
||||
|
||||
你选择的是“准可靠投递”,这决定了我们不能把异步回调只做成 best-effort。推荐实现是**Outbox + Delivery Worker + Retry Policy + Dead Letter**。
|
||||
|
||||
新增持久化表建议:
|
||||
|
||||
1. `cs_platform_callbacks`
|
||||
- 配置每个 platform target 的回调地址、签名方式、启停状态
|
||||
2. `cs_platform_event_outbox`
|
||||
- 存放待投递事件
|
||||
3. `cs_platform_event_delivery_attempts`
|
||||
- 存放每次尝试结果
|
||||
4. `cs_platform_event_dead_letters`
|
||||
- 存放超出重试上限的事件
|
||||
|
||||
投递策略:
|
||||
|
||||
1. 业务主链中先生成事件并落 `outbox`
|
||||
2. 后台 worker 轮询领取事件
|
||||
3. 成功后标记 delivered
|
||||
4. 失败后指数退避重试
|
||||
5. 达到上限后进入 dead letter
|
||||
6. 提供人工或脚本重放入口
|
||||
|
||||
推荐默认策略:
|
||||
|
||||
1. 首次立即投递
|
||||
2. 之后 `10s / 30s / 60s / 5m / 15m`
|
||||
3. 最多 5 次
|
||||
4. 超过进入 dead letter
|
||||
|
||||
这不是严格 exactly-once,但对第一版已经足够现实:
|
||||
|
||||
- 上游通过 `event_id` 幂等
|
||||
- 我们保证“不轻易丢”
|
||||
- 重试/死信让失败可追踪可恢复
|
||||
|
||||
---
|
||||
|
||||
## 6. 配置与安全设计
|
||||
|
||||
适配层要想落地,配置必须从“单 webhook secret”提升为“平台适配配置”。建议新增:
|
||||
|
||||
```text
|
||||
AI_CS_PLATFORM_ADAPTERS_ENABLED=true
|
||||
AI_CS_PLATFORM_SUB2API_ENABLED=true
|
||||
AI_CS_PLATFORM_SUB2API_INGRESS_SECRET=...
|
||||
AI_CS_PLATFORM_SUB2API_CALLBACK_BASE_URL=...
|
||||
AI_CS_PLATFORM_SUB2API_CALLBACK_SECRET=...
|
||||
AI_CS_PLATFORM_SUB2API_CALLBACK_TIMEOUT_MS=3000
|
||||
AI_CS_PLATFORM_SUB2API_CALLBACK_MAX_RETRIES=5
|
||||
AI_CS_PLATFORM_NEWAPI_ENABLED=false
|
||||
```
|
||||
|
||||
安全要求:
|
||||
|
||||
1. **入站鉴权**
|
||||
平台入口不能复用当前通用 webhook 约束的最小集合就草率上线,必须明确平台级 secret/profile。
|
||||
|
||||
2. **出站签名**
|
||||
回调给 Sub2API/NewAPI 的事件也要带时间戳与签名,避免被伪造。
|
||||
|
||||
3. **最小字段原则**
|
||||
只回推平台真正需要的字段,不把完整上下文、敏感用户数据默认外发。
|
||||
|
||||
4. **审计闭环**
|
||||
所有 callback 失败、重试、死信、重放都进入 `audit` 或独立 delivery attempts 表。
|
||||
|
||||
安全上最重要的一条是:
|
||||
|
||||
> **平台适配层必须是“显式启用、显式配置、显式审计”的能力,不允许默认裸开。**
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试与门禁设计
|
||||
|
||||
第一版适配增强必须新增独立测试层,而不能只靠现有 webhook 测试顺带覆盖。
|
||||
|
||||
建议测试分层:
|
||||
|
||||
1. **Unit**
|
||||
- 平台原生 payload -> `UnifiedMessage` 映射
|
||||
- callback payload 组装
|
||||
- 签名算法
|
||||
- 重试策略
|
||||
|
||||
2. **Integration**
|
||||
- 平台入站请求 -> 主链处理 -> outbox 落库
|
||||
- outbox -> callback mock server
|
||||
- 失败重试 -> dead letter
|
||||
|
||||
3. **E2E**
|
||||
- Sub2API mock 发原生消息
|
||||
- `ai-customer-service` 创建 session / ticket / audit
|
||||
- callback mock 收到全事件流
|
||||
|
||||
第一版阻断门禁建议至少包含:
|
||||
|
||||
1. `sub2api` 最小接入 happy path
|
||||
2. `message_id` 去重 path
|
||||
3. 未知字段/非法签名 path
|
||||
4. callback 5xx 重试 path
|
||||
5. callback 最终 dead letter path
|
||||
6. 回滚后 callback 恢复 path
|
||||
|
||||
这里要特别强调:
|
||||
|
||||
> 当前 `tech/TEST_DESIGN.md` 里 NewAPI/Sub2API 适配验证还是待实现项,第一版增强后必须把它提升为真正可执行的合同测试和联调测试,而不是继续停留在文档层。
|
||||
|
||||
---
|
||||
|
||||
## 8. 分阶段实施建议
|
||||
|
||||
为了不把当前 Phase 1 拖爆,建议按 3 个 implementation batch 执行:
|
||||
|
||||
### Batch 1:Sub2API 入站最小适配
|
||||
|
||||
1. 新增 `/platforms/sub2api/webhook`
|
||||
2. 新增 adapter 接口和 `sub2api` profile
|
||||
3. 原生 payload -> `UnifiedMessage`
|
||||
4. 复用现有主链
|
||||
5. 单测 + 集成测试
|
||||
|
||||
### Batch 2:事件 outbox 与异步回调
|
||||
|
||||
1. 设计事件模型
|
||||
2. 新增 outbox 表
|
||||
3. 新增 worker
|
||||
4. 新增 callback 签名与投递
|
||||
5. 失败重试 + dead letter
|
||||
|
||||
### Batch 3:NewAPI profile 与运维可观测
|
||||
|
||||
1. 新增 `newapi` adapter profile
|
||||
2. 新增 delivery metrics / dashboard
|
||||
3. 新增重放工具与 runbook
|
||||
4. 补 Gate B / Gate C 适配层联调门禁
|
||||
|
||||
这个顺序的理由很简单:
|
||||
|
||||
1. 先把 Sub2API 场景跑通
|
||||
2. 再把异步事件流做稳
|
||||
3. 最后复用同一套抽象支持 NewAPI
|
||||
|
||||
---
|
||||
|
||||
## 9. 最终建议
|
||||
|
||||
我推荐按这份设计推进,因为它满足四个约束:
|
||||
|
||||
1. **符合项目规划**:确实开始支持 NewAPI/Sub2API
|
||||
2. **不破坏当前主链**:平台差异不侵入核心客服逻辑
|
||||
3. **可先解决 tksea / Sub2API 的真实问题**:不是空转设计
|
||||
4. **可灰度实施**:Batch 1 完成就能先验证最小接入
|
||||
|
||||
最终建议一句话概括:
|
||||
|
||||
> **把 NewAPI/Sub2API 支持做成“内置适配器 + 事件 outbox”的最小集成层,而不是把 `ai-customer-service` 重做成另一个平台。**
|
||||
|
||||
下一步如果继续,最合理的是直接基于这份设计拆 implementation plan,而不是直接开写代码。
|
||||
@@ -0,0 +1,754 @@
|
||||
# NewAPI / Sub2API Adapter Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 为 `ai-customer-service` 增加面向 `Sub2API` 优先、`NewAPI` 同构兼容的最小平台适配层,支持入站原生消息适配、异步全事件流回写,以及准可靠投递。
|
||||
|
||||
**Architecture:** 在现有统一 webhook 主链之外新增平台入口 `/platforms/{platform}/webhook`,通过内置 adapter 将平台原生 payload 转换为 `UnifiedMessage`。主链处理后生成内部平台事件,先落库到 outbox,再由后台 worker 进行带重试的异步 callback 投递。
|
||||
|
||||
**Tech Stack:** Go 1.22, net/http, PostgreSQL, HMAC-SHA256, background worker, Go test, httptest
|
||||
|
||||
---
|
||||
|
||||
## 0. 实施原则
|
||||
|
||||
1. **先 Sub2API,后 NewAPI**
|
||||
第一批只要求 Sub2API 真正可跑,NewAPI 只保留 profile 插槽和最小合同测试骨架。
|
||||
|
||||
2. **先入站,后出站,最后可靠性**
|
||||
先打通平台入站 -> 主链,再接 outbox + callback,再补 dead letter / replay。
|
||||
|
||||
3. **适配逻辑边缘化**
|
||||
不改 `dialog.Service` 的核心业务语义;平台差异收在 adapter / callback / outbox 层。
|
||||
|
||||
4. **TDD + 频繁提交**
|
||||
每个 Task 都先写失败测试,再写最小实现,再跑验证,再提交。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 搭好平台适配骨架与路由入口
|
||||
|
||||
**Files:**
|
||||
- Create: `internal/platformadapter/types.go`
|
||||
- Create: `internal/platformadapter/registry.go`
|
||||
- Create: `internal/platformadapter/sub2api_adapter.go`
|
||||
- Create: `internal/platformadapter/newapi_adapter.go`
|
||||
- Create: `internal/http/handlers/platform_webhook_handler.go`
|
||||
- Modify: `internal/http/router.go`
|
||||
- Test: `internal/platformadapter/registry_test.go`
|
||||
- Test: `internal/http/handlers/platform_webhook_handler_test.go`
|
||||
|
||||
**Step 1: 写平台注册表失败测试**
|
||||
|
||||
写测试覆盖:
|
||||
|
||||
```go
|
||||
func TestRegistry_ShouldResolveSub2APIAdapter(t *testing.T) {}
|
||||
func TestRegistry_ShouldRejectUnknownPlatform(t *testing.T) {}
|
||||
```
|
||||
|
||||
**Step 2: 运行测试确认失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/platformadapter ./internal/http/handlers -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL,提示 `platformadapter` 包或 handler 不存在
|
||||
|
||||
**Step 3: 写最小平台类型与注册表**
|
||||
|
||||
新增:
|
||||
|
||||
- `PlatformAdapter` 接口
|
||||
- `IngressContext`
|
||||
- `PlatformInboundMeta`
|
||||
- `Registry`
|
||||
|
||||
最小接口:
|
||||
|
||||
```go
|
||||
type PlatformAdapter interface {
|
||||
Platform() string
|
||||
ParseInbound(r *http.Request, body []byte, ctx IngressContext) (*message.UnifiedMessage, *PlatformInboundMeta, error)
|
||||
BuildIngressAck(result *dialog.Result, meta *PlatformInboundMeta) any
|
||||
}
|
||||
```
|
||||
|
||||
**Step 4: 写最小 handler 骨架**
|
||||
|
||||
`PlatformWebhookHandler` 先只做:
|
||||
|
||||
1. 路径读取 `{platform}` / `{channel}`
|
||||
2. 从 registry 取 adapter
|
||||
3. 读取 body
|
||||
4. 调 adapter
|
||||
5. 调现有 `dialog.Service`
|
||||
6. 返回 adapter ack
|
||||
|
||||
**Step 5: 在 router 增加入口**
|
||||
|
||||
新增:
|
||||
|
||||
- `POST /api/v1/customer-service/platforms/{platform}/webhook`
|
||||
- `POST /api/v1/customer-service/platforms/{platform}/webhook/{channel}`
|
||||
|
||||
**Step 6: 跑测试确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/platformadapter ./internal/http/handlers -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/platformadapter internal/http/handlers/platform_webhook_handler.go internal/http/handlers/platform_webhook_handler_test.go internal/http/router.go
|
||||
git commit -m "feat(adapter): add platform webhook adapter skeleton"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 实现 Sub2API 入站最小适配
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/platformadapter/sub2api_adapter.go`
|
||||
- Create: `internal/platformadapter/sub2api_types.go`
|
||||
- Test: `internal/platformadapter/sub2api_adapter_test.go`
|
||||
- Modify: `internal/http/handlers/platform_webhook_handler_test.go`
|
||||
- Reference: `docs/SUB2API_MINIMAL_WEBHOOK_MAPPING.md`
|
||||
|
||||
**Step 1: 写 Sub2API payload 失败测试**
|
||||
|
||||
覆盖:
|
||||
|
||||
```go
|
||||
func TestSub2APIAdapter_ShouldMapMinimalPayload(t *testing.T) {}
|
||||
func TestSub2APIAdapter_ShouldRejectUnknownEnvelopeFields(t *testing.T) {}
|
||||
func TestSub2APIAdapter_ShouldUseChannelOverrideWhenPresent(t *testing.T) {}
|
||||
func TestSub2APIAdapter_ShouldRequireOpenIDAndContent(t *testing.T) {}
|
||||
```
|
||||
|
||||
**Step 2: 运行测试确认失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/platformadapter -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL,字段映射或校验未实现
|
||||
|
||||
**Step 3: 定义 Sub2API 最小 payload 结构**
|
||||
|
||||
只实现第一版所需字段:
|
||||
|
||||
```go
|
||||
type Sub2APIInboundPayload struct {
|
||||
MessageID string `json:"message_id"`
|
||||
Channel string `json:"channel"`
|
||||
OpenID string `json:"open_id"`
|
||||
UserID string `json:"user_id,omitempty"`
|
||||
Content string `json:"content"`
|
||||
Timestamp time.Time `json:"timestamp,omitempty"`
|
||||
ReplyTo string `json:"reply_to,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
不要一次性吞平台原生大包。
|
||||
|
||||
**Step 4: 实现最小 ParseInbound**
|
||||
|
||||
规则:
|
||||
|
||||
1. 只接受当前最小字段
|
||||
2. 缺 `channel/open_id/content` 返回 `400`
|
||||
3. `{channel}` path override 优先
|
||||
4. 产出 `UnifiedMessage`
|
||||
5. 记录 `PlatformInboundMeta`
|
||||
|
||||
**Step 5: 实现最小 ingress ack**
|
||||
|
||||
同步响应先返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"accepted": true,
|
||||
"platform": "sub2api",
|
||||
"session_id": "...",
|
||||
"ticket_id": "...",
|
||||
"event_id": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Step 6: 跑测试确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/platformadapter ./internal/http/handlers -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/platformadapter/sub2api_adapter.go internal/platformadapter/sub2api_types.go internal/platformadapter/sub2api_adapter_test.go internal/http/handlers/platform_webhook_handler_test.go
|
||||
git commit -m "feat(adapter): add sub2api inbound adapter"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 增加平台级入站鉴权配置
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/config/config.go`
|
||||
- Modify: `internal/config/config_test.go`
|
||||
- Create: `internal/http/handlers/platform_webhook_security.go`
|
||||
- Test: `internal/http/handlers/platform_webhook_security_test.go`
|
||||
- Modify: `internal/http/router.go`
|
||||
- Modify: `docs/CONFIG_CONTRACT_BASELINE.md`
|
||||
|
||||
**Step 1: 先写配置失败测试**
|
||||
|
||||
覆盖:
|
||||
|
||||
```go
|
||||
func TestPlatformAdapterConfig_ShouldFailInProdWhenSub2APIEnabledWithoutIngressSecret(t *testing.T) {}
|
||||
func TestPlatformAdapterConfig_ShouldPassWhenAdaptersDisabled(t *testing.T) {}
|
||||
```
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/config ./internal/http/handlers -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL
|
||||
|
||||
**Step 3: 增加最小平台适配配置**
|
||||
|
||||
新增配置项:
|
||||
|
||||
- `AI_CS_PLATFORM_ADAPTERS_ENABLED`
|
||||
- `AI_CS_PLATFORM_SUB2API_ENABLED`
|
||||
- `AI_CS_PLATFORM_SUB2API_INGRESS_SECRET`
|
||||
- `AI_CS_PLATFORM_SUB2API_CALLBACK_BASE_URL`
|
||||
- `AI_CS_PLATFORM_SUB2API_CALLBACK_SECRET`
|
||||
- `AI_CS_PLATFORM_SUB2API_CALLBACK_TIMEOUT_MS`
|
||||
- `AI_CS_PLATFORM_SUB2API_CALLBACK_MAX_RETRIES`
|
||||
- `AI_CS_PLATFORM_NEWAPI_ENABLED`
|
||||
|
||||
**Step 4: 写平台入口安全包装器**
|
||||
|
||||
实现与现有 `WebhookSecurity` 同构的:
|
||||
|
||||
- `PlatformWebhookSecurity`
|
||||
|
||||
但按 platform profile 选择 secret,不要复用通用 webhook secret。
|
||||
|
||||
**Step 5: 在 router 给平台入口接安全包装**
|
||||
|
||||
平台入口独立挂安全中间件,不与现有 `/webhook` 混用 secret。
|
||||
|
||||
**Step 6: 跑测试确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/config ./internal/http/handlers -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/config/config.go internal/config/config_test.go internal/http/handlers/platform_webhook_security.go internal/http/handlers/platform_webhook_security_test.go internal/http/router.go docs/CONFIG_CONTRACT_BASELINE.md
|
||||
git commit -m "feat(adapter): add platform-specific ingress security config"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 定义平台事件模型与 outbox 表结构
|
||||
|
||||
**Files:**
|
||||
- Create: `db/migration/0002_platform_event_outbox.up.sql`
|
||||
- Create: `internal/domain/platformevent/event.go`
|
||||
- Create: `internal/domain/platformevent/event_test.go`
|
||||
- Create: `internal/store/postgres/platform_event_store.go`
|
||||
- Create: `internal/store/postgres/platform_event_store_test.go`
|
||||
- Reference: `docs/plans/2026-05-06-newapi-sub2api-adapter-design.md`
|
||||
|
||||
**Step 1: 写 store 失败测试**
|
||||
|
||||
覆盖:
|
||||
|
||||
```go
|
||||
func TestPlatformEventStore_ShouldInsertPendingEvent(t *testing.T) {}
|
||||
func TestPlatformEventStore_ShouldListPendingEventsInOrder(t *testing.T) {}
|
||||
```
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/store/postgres -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL
|
||||
|
||||
**Step 3: 定义事件模型**
|
||||
|
||||
新增 `platformevent.Event`:
|
||||
|
||||
- `ID`
|
||||
- `Platform`
|
||||
- `EventType`
|
||||
- `SessionID`
|
||||
- `TicketID`
|
||||
- `SourceMessageID`
|
||||
- `CallbackTarget`
|
||||
- `Payload`
|
||||
- `Status`
|
||||
- `AttemptCount`
|
||||
- `NextAttemptAt`
|
||||
- `CreatedAt`
|
||||
|
||||
**Step 4: 补 migration**
|
||||
|
||||
建表至少包括:
|
||||
|
||||
1. `cs_platform_callbacks`
|
||||
2. `cs_platform_event_outbox`
|
||||
3. `cs_platform_event_delivery_attempts`
|
||||
4. `cs_platform_event_dead_letters`
|
||||
|
||||
第一版不做过度 schema 拆分,优先让 outbox 可用。
|
||||
|
||||
**Step 5: 实现最小 Postgres store**
|
||||
|
||||
支持:
|
||||
|
||||
1. 插入 pending event
|
||||
2. 拉取 due events
|
||||
3. 标记 delivered
|
||||
4. 标记 retry
|
||||
5. 标记 dead letter
|
||||
|
||||
**Step 6: 跑测试确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/domain/platformevent ./internal/store/postgres -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add db/migration/0002_platform_event_outbox.up.sql internal/domain/platformevent internal/store/postgres/platform_event_store.go internal/store/postgres/platform_event_store_test.go
|
||||
git commit -m "feat(adapter): add platform event outbox schema and store"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 在主链接入平台事件生成
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/service/dialog/service.go`
|
||||
- Create: `internal/service/platformevents/builder.go`
|
||||
- Create: `internal/service/platformevents/builder_test.go`
|
||||
- Modify: `internal/http/handlers/platform_webhook_handler.go`
|
||||
- Modify: `internal/http/handlers/platform_webhook_handler_test.go`
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
覆盖:
|
||||
|
||||
```go
|
||||
func TestPlatformWebhookHandler_ShouldEnqueueMessageReceivedAndReplyGenerated(t *testing.T) {}
|
||||
func TestPlatformWebhookHandler_ShouldEnqueueHandoffAndTicketCreatedWhenNeeded(t *testing.T) {}
|
||||
```
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/service/... ./internal/http/handlers -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL
|
||||
|
||||
**Step 3: 新增事件构建器**
|
||||
|
||||
从 `dialog.Result + PlatformInboundMeta` 构建:
|
||||
|
||||
1. `message.received`
|
||||
2. `message.processing`
|
||||
3. `intent.resolved`
|
||||
4. `handoff.triggered`
|
||||
5. `ticket.created`
|
||||
6. `reply.generated`
|
||||
|
||||
**Step 4: 在平台 handler 中落 outbox**
|
||||
|
||||
当前平台入口成功后:
|
||||
|
||||
1. 先调主链
|
||||
2. 再构建事件
|
||||
3. 批量写入 outbox
|
||||
4. 返回 ingress ack
|
||||
|
||||
第一版不要把 outbox 失败静默吞掉;应返回 `500` 并记录日志/审计。
|
||||
|
||||
**Step 5: 跑测试确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/service/... ./internal/http/handlers -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/service/platformevents internal/service/dialog/service.go internal/http/handlers/platform_webhook_handler.go internal/http/handlers/platform_webhook_handler_test.go
|
||||
git commit -m "feat(adapter): enqueue platform outbox events from inbound flow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 实现 callback 投递 worker
|
||||
|
||||
**Files:**
|
||||
- Create: `internal/service/platformdelivery/worker.go`
|
||||
- Create: `internal/service/platformdelivery/signer.go`
|
||||
- Create: `internal/service/platformdelivery/worker_test.go`
|
||||
- Create: `internal/service/platformdelivery/signer_test.go`
|
||||
- Modify: `internal/app/app.go`
|
||||
- Modify: `internal/config/config.go`
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
覆盖:
|
||||
|
||||
```go
|
||||
func TestWorker_ShouldDeliverPendingEventToCallbackServer(t *testing.T) {}
|
||||
func TestWorker_ShouldRetryWhenCallbackReturns5xx(t *testing.T) {}
|
||||
func TestSigner_ShouldProduceStableTimestampAndSignatureHeaders(t *testing.T) {}
|
||||
```
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/service/platformdelivery -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL
|
||||
|
||||
**Step 3: 实现 callback signer**
|
||||
|
||||
为出站事件添加:
|
||||
|
||||
- `X-CS-Timestamp`
|
||||
- `X-CS-Signature`
|
||||
|
||||
算法与平台 callback secret 对齐。
|
||||
|
||||
**Step 4: 实现最小 worker**
|
||||
|
||||
职责:
|
||||
|
||||
1. 拉取 due events
|
||||
2. 发送 callback
|
||||
3. 成功标记 delivered
|
||||
4. 失败按退避设置 `next_attempt_at`
|
||||
|
||||
**Step 5: 在 app 启动 worker**
|
||||
|
||||
只在:
|
||||
|
||||
- `AI_CS_PLATFORM_ADAPTERS_ENABLED=true`
|
||||
|
||||
时启动。
|
||||
|
||||
**Step 6: 跑测试确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/service/platformdelivery ./internal/app -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/service/platformdelivery internal/app/app.go internal/config/config.go
|
||||
git commit -m "feat(adapter): add platform callback delivery worker"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 增加重试、死信和投递尝试审计
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/store/postgres/platform_event_store.go`
|
||||
- Modify: `internal/store/postgres/platform_event_store_test.go`
|
||||
- Modify: `internal/service/platformdelivery/worker.go`
|
||||
- Modify: `internal/service/platformdelivery/worker_test.go`
|
||||
- Create: `docs/RUNBOOK_PLATFORM_CALLBACKS.md`
|
||||
|
||||
**Step 1: 写失败测试**
|
||||
|
||||
覆盖:
|
||||
|
||||
```go
|
||||
func TestWorker_ShouldMoveEventToDeadLetterAfterMaxRetries(t *testing.T) {}
|
||||
func TestWorker_ShouldPersistDeliveryAttemptAudit(t *testing.T) {}
|
||||
```
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/store/postgres ./internal/service/platformdelivery -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL
|
||||
|
||||
**Step 3: 实现尝试记录与死信**
|
||||
|
||||
要求:
|
||||
|
||||
1. 每次 callback 尝试都写 `delivery_attempts`
|
||||
2. 达到最大次数写 `dead_letters`
|
||||
3. outbox 主记录进入 terminal status
|
||||
|
||||
**Step 4: 补运行手册**
|
||||
|
||||
新增 runbook 说明:
|
||||
|
||||
1. 如何查看 pending / failed / dead letter
|
||||
2. 如何手动重放
|
||||
3. 如何区分平台回调失败与主链失败
|
||||
|
||||
**Step 5: 跑测试确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/store/postgres ./internal/service/platformdelivery -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/store/postgres/platform_event_store.go internal/store/postgres/platform_event_store_test.go internal/service/platformdelivery/worker.go internal/service/platformdelivery/worker_test.go docs/RUNBOOK_PLATFORM_CALLBACKS.md
|
||||
git commit -m "feat(adapter): add callback retry audit and dead letter handling"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 新增端到端 Sub2API 接入测试
|
||||
|
||||
**Files:**
|
||||
- Create: `test/integration/sub2api_webhook_flow_test.go`
|
||||
- Create: `test/e2e/sub2api_callback_flow_test.go`
|
||||
- Modify: `tech/TEST_DESIGN.md`
|
||||
- Modify: `test/QA_GATE_STATUS.md`
|
||||
|
||||
**Step 1: 写端到端失败测试**
|
||||
|
||||
覆盖:
|
||||
|
||||
```go
|
||||
func TestSub2APIWebhookFlow_ShouldCreateSessionTicketAndOutboxEvents(t *testing.T) {}
|
||||
func TestSub2APICallbackFlow_ShouldDeliverOrderedEventsWithStableEventIDs(t *testing.T) {}
|
||||
func TestSub2APICallbackFlow_ShouldDeadLetterAfterMaxRetries(t *testing.T) {}
|
||||
```
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./test/integration ./test/e2e -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL
|
||||
|
||||
**Step 3: 接通测试依赖**
|
||||
|
||||
1. 使用 mock callback server
|
||||
2. 使用 Postgres 测试库
|
||||
3. 走真实平台入口 `/platforms/sub2api/webhook`
|
||||
4. 验证 outbox / delivery / dead letter
|
||||
|
||||
**Step 4: 更新测试设计与 QA 文档**
|
||||
|
||||
把原来“NewAPI/Sub2API 适配层验证待实现”改成:
|
||||
|
||||
1. 已有 Sub2API 最小接入联调测试
|
||||
2. NewAPI 同构位待实现
|
||||
|
||||
**Step 5: 跑测试确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./test/integration ./test/e2e -count=1
|
||||
go test ./... -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add test/integration/sub2api_webhook_flow_test.go test/e2e/sub2api_callback_flow_test.go tech/TEST_DESIGN.md test/QA_GATE_STATUS.md
|
||||
git commit -m "test(adapter): add sub2api end-to-end adapter coverage"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 预留 NewAPI profile 与适配扩展点
|
||||
|
||||
**Files:**
|
||||
- Modify: `internal/platformadapter/newapi_adapter.go`
|
||||
- Create: `internal/platformadapter/newapi_adapter_test.go`
|
||||
- Modify: `docs/plans/2026-05-06-newapi-sub2api-adapter-design.md`
|
||||
|
||||
**Step 1: 写最小失败测试**
|
||||
|
||||
覆盖:
|
||||
|
||||
```go
|
||||
func TestNewAPIAdapter_ShouldBeRegisteredButDisabledByDefault(t *testing.T) {}
|
||||
```
|
||||
|
||||
**Step 2: 跑测试确认失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/platformadapter -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL
|
||||
|
||||
**Step 3: 实现同构占位**
|
||||
|
||||
要求:
|
||||
|
||||
1. registry 中可注册 `newapi`
|
||||
2. 默认不开启
|
||||
3. 明确返回“profile not implemented”而不是 silent success
|
||||
|
||||
**Step 4: 跑测试确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
go test ./internal/platformadapter -count=1
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add internal/platformadapter/newapi_adapter.go internal/platformadapter/newapi_adapter_test.go docs/plans/2026-05-06-newapi-sub2api-adapter-design.md
|
||||
git commit -m "feat(adapter): reserve newapi adapter profile extension point"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最终整体验证
|
||||
|
||||
所有 Task 完成后必须执行:
|
||||
|
||||
```bash
|
||||
go test ./... -count=1
|
||||
go test -race ./...
|
||||
go vet ./...
|
||||
bash -n scripts/verify_preprod_gate_b.sh
|
||||
bash -n scripts/verify_gate_c_rollback.sh
|
||||
```
|
||||
|
||||
如果新增了平台脚本,再追加:
|
||||
|
||||
```bash
|
||||
bash scripts/verify_platform_adapter_sub2api.sh
|
||||
```
|
||||
|
||||
Expected:
|
||||
- 全部 PASS
|
||||
|
||||
---
|
||||
|
||||
## 交付完成判定
|
||||
|
||||
满足以下条件才算第一版完成:
|
||||
|
||||
1. `sub2api` 平台入口可用
|
||||
2. 原生 payload 可映射到 `UnifiedMessage`
|
||||
3. 成功创建 session / ticket / audit / dedup
|
||||
4. 全事件流可进入 outbox
|
||||
5. callback worker 可投递、重试、死信
|
||||
6. 端到端测试通过
|
||||
7. QA 文档与 runbook 已更新
|
||||
|
||||
---
|
||||
|
||||
## 风险提醒
|
||||
|
||||
1. **不要一次性做完整平台协议**
|
||||
第一版只做 Sub2API 优先的最小 profile。
|
||||
|
||||
2. **不要把平台字段渗透进核心主链**
|
||||
平台差异只能留在 adapter/meta/event 边缘层。
|
||||
|
||||
3. **不要跳过 outbox 直接同步回调**
|
||||
你已经要求准可靠投递,不能退回 best-effort。
|
||||
|
||||
4. **不要省掉 dead letter**
|
||||
没有 dead letter,就没有真正的可恢复性闭环。
|
||||
Reference in New Issue
Block a user