# Clover API 契约（OpenAPI 初稿 · Lead 冻结用）

> 用途：开工前由 Lead/集成 冻结此契约，前后端按此并行开工，杜绝"前端假设字段、联调爆炸"。
> 铁律：所有 `type: object` 不许留空——冻结前嵌套对象一级子字段必须定义（官网V1坑4）。
> 来源：承接架构方案v0.3（golden standard）端点 + 16表字段，不重复定义，冲突以架构方案为准。
> 范围：一期 1A 生产链 + 1B 飞轮。二期端点（注册/矩阵号/管理后台）标 [二期] 不冻结。

## 0. 全局约定

### 认证
- 除登录/注册外，所有请求头带 `Authorization: Bearer <JWT>`。
- JWT 载荷：`user_id` / `current_workspace_id` / `role`（HS256 签名，每请求验签）。
- 写操作 + 切 workspace 必须查 workspace_members 校验权限（读操作可信 JWT 加速）。

### 统一响应包络
```
成功：{ "code": 0, "data": {...} }
失败：{ "code": <错误码>, "message": "用户可读信息" }
```
HTTP 状态码与业务 code 分离。

### 错误码规范（7 类）
| code | 含义 | HTTP |
|------|------|------|
| 0 | 成功 | 200 |
| 40001 | 参数校验失败 | 400 |
| 40101 | 未认证/JWT失效 | 401 |
| 40301 | 无权限/越权访问 workspace | 403 |
| 40401 | 资源不存在 | 404 |
| 40901 | 状态机非法流转 | 409 |
| 42201 | 业务校验失败(如未配key/违禁词硬拦截) | 422 |
| 50001 | 服务端错误 | 500 |
| 50002 | AI/token站调用失败(归因到个人key) | 502 |

### 分页 & 限流
- 列表统一 `?page=1&page_size=20`，返回 `{items, total, page, page_size}`。
- 写操作与 AI 触发接口限流（防滥用），超限返 429。

## 1. 端点清单（一期 1A+1B）

### 认证 & 账号
| 方法 | 路径 | 说明 | 角色 |
|------|------|------|------|
| POST | /api/v1/auth/login | 登录，返 JWT | 全部 |
| GET | /api/v1/auth/me | 当前用户+workspace+role | 全部 |
| POST | /api/v1/workspaces/switch | 切换当前 workspace（查membership） | 全部 |

### API Key（只录不显余额，CLAUDE.md红线）
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/api-keys | 列表（只显 provider + 后4位，**不显余额/用量**） |
| POST | /api/v1/api-keys | 录入（Fernet加密存，url固定自家站不接收url字段） |
| DELETE | /api/v1/api-keys/{id} | 删除 |

### 品牌库（管理员）
| 方法 | 路径 | 说明 |
|------|------|------|
| GET/POST | /api/v1/products | 产品档案 CRUD（含文案角度+可调prompt+source） |
| GET/PUT/DELETE | /api/v1/products/{id} | 单产品 |
| GET/POST | /api/v1/products/{id}/benchmarks | 标杆笔记（截图+手填亮点为主通道） |
| GET/POST/PUT/DELETE | /api/v1/banned-words | 违禁词库（level三级，workspace可覆盖） |

### 生产链（熟手）
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /api/v1/tasks | 发起任务（选产品+标杆+今天主题+数量），校验有无key→只推task_id入队 |
| GET | /api/v1/tasks | 任务列表（按状态筛） |
| GET | /api/v1/tasks/{id} | 任务详情（含候选+状态机当前态） |
| POST | /api/v1/tasks/{id}/sse-ticket | 换取一次性 SSE ticket（JWT认证→返回60s短票，FE立即建EventSource） |
| GET | /api/v1/tasks/{id}/stream | **SSE** 实时进度（见§2，用 ?ticket= 认证，禁止 ?token=） |
| POST | /api/v1/tasks/{id}/text-candidates/select | 选文案（飞轮入口1，+3） |
| POST | /api/v1/tasks/{id}/image-candidates/select | 选图（飞轮入口2，+3） |
| POST | /api/v1/tasks/{id}/import-text | 轨B：导入外部文案直接进候选池 |
| POST | /api/v1/tasks/{id}/regenerate | 重新生成（飞轮-1） |
| POST | /api/v1/tasks/{id}/submit-review | 提交审核（状态→pending_review） |
| GET | /api/v1/tasks/{id}/preference/context | 取本任务飞轮偏好上下文（最近选中样本+最近3条打回原因），供 prompt 组装；前端仅用于显示"本次已注入"摘要 |

> 🔴 飞轮**只有读端点**：偏好信号(选文案/选图/通过/打回/重生成)由上方 select / approve / reject / regenerate 接口**内部**写入 `preference_events`，**不暴露独立的 POST /preference/event 给前端**（避免被当成"训练按钮"，且防伪造信号）。

### 审核（组长）
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | /api/v1/review/queue | 待审队列 |
| POST | /api/v1/review/{task_id}/approve | 通过（飞轮+5最重，状态→approved） |
| POST | /api/v1/review/{task_id}/reject | 打回（写原因，-3，回pending_selection，原因下次注入prompt） |

### 交付包
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /api/v1/tasks/{id}/package | 生成达人素材交付包（去水印+打包） |
| GET | /api/v1/delivery-packages/{id}/download | 下载（status: pending/ready/downloaded） |

### [二期] 不冻结
注册(手机验证码)、matrix_accounts矩阵号、管理后台用量统计、preference_profile物化。

## 2. SSE 事件契约（异步生图电影 + 飞轮注入提示）

`GET /api/v1/tasks/{id}/stream` 流式推送，前端据此渲染"逐帧电影"。
断线重连时后端**补发历史事件**（修 banana 漏洞），前端按 event_seq 去重。

> **🔴 SSE 认证安全红线（2026-06-08 倩倩姐拍板）**：EventSource 不支持自定义 header，
> 故 FE 建连前先用 JWT 调 `POST /sse-ticket` 换一张一次性短票（TTL 60s，仅对该 task 有效），
> URL 只带 `?ticket=`。ticket 用后即焚（Redis getdel），就算被日志记下也立即失效。
> **禁止 `?token=` 直传 JWT 进 URL。**

| event 类型 | data 字段（一级子字段必须全定义） | 前端用途 |
|-----------|----------------------------------|---------|
| task_started | `{task_id, total_text, total_image}` | 显示 0/N 进度 |
| analyze_done | `{features:[8特征]}` | 标杆分析完成 |
| text_progress | `{done, total}` | 文案 done/total |
| text_candidate | `{candidate_id, angle_label, content, score}` | 单条文案冒出来 |
| image_progress | `{done, total}` | 图 done/total |
| image_candidate | `{candidate_id, strategy(A/B/C), url, role}` | 单张图冒出来（谁好谁先冒） |
| flywheel_injected | `{recent_preference, reject_reasons:[最近3条]}` | UI显"本次已注入"（飞轮基石E第4点） |
| batch_failed | `{batch, reason, retryable:bool}` | 单批失败可重试，不阻塞其他 |
| task_done | `{task_id, status}` | 全部完成，可离开 |
| error | `{code, message}` | 对接§0错误码 |
| heartbeat | `{ts}` | 保活 |

## 3. 关键 DTO（type:object 不许留空）

### 发起任务 POST /api/v1/tasks 请求体
```
{
  "product_id": 12,
  "benchmark_ids": [3, 5],
  "theme": "今天主题：夏日清爽控油",   // 前端交互设计的"今天主题"字段
  "text_count": 5,                    // 用户设定，不写死(基石A)
  "image_count": 3,
  "track": "ai"                       // ai=轨A生成 / import=轨B导入
}
```

### text_candidate 文案候选（含五维分，挑文案屏用）
```
{
  "candidate_id": 101, "angle_label": "痛点切入", "content": "...",
  "source": "ai",                     // ai / import
  "score": { "title":24, "emotion":23, "selling":25, "keyword":18, "compliance":5, "total":95 },
  "banned_word_status": "pass"        // pass / auto_fixed / soft_warn / hard_block
}
```
> 五维：标题25/情绪25/买点25/关键词20/合规5，过线90（承接产品包评分引擎）。

### 打回 POST /api/v1/review/{task_id}/reject 请求体
```
{ "reason": "标题太硬广，不像素人分享" }   // 原文存 preference_events.reason，不做AI归纳
```

### 选择类（飞轮信号）统一体
```
{ "candidate_id": 101 }   // 后端据登录态拿 user/workspace/product，写 preference_events
```

> 凡涉及 workspace_id / product_id 的写入，后端从 JWT + 任务上下文自动带，**前端不传**（防越权）。

## 4. 补全 DTO（冻结补全，官网V1坑4）

### POST /api/v1/auth/login
请求体：
```json
{ "username": "string", "password": "string" }
```
返回 data：
```json
{
  "token": "eyJ...",
  "user": {
    "id": 1, "username": "string", "email": "string",
    "current_workspace_id": 1, "role": "admin|supervisor|operator"
  }
}
```

### GET /api/v1/auth/me 返回 data
```json
{
  "id": 1, "username": "string", "email": "string",
  "current_workspace_id": 1, "role": "admin|supervisor|operator",
  "workspace": { "id": 1, "name": "string", "slug": "string" }
}
```

### POST /api/v1/workspaces/switch
请求体：`{ "workspace_id": 2 }`
返回 data：`{ "current_workspace_id": 2, "role": "string", "token": "eyJ..." }`

### GET /api/v1/api-keys 返回 items 单项
```json
{ "id": 1, "provider": "openai", "key_last4": "ab12", "created_at": "2026-01-01T00:00:00Z" }
```
注：不返回余额/用量（CLAUDE.md前端红线）。

### POST /api/v1/api-keys 请求体
```json
{ "provider": "openai", "api_key": "sk-..." }
```
注：url 字段不接收，后端固定走自家站 endpoint（架构方案§二决策）。

### POST /api/v1/products 请求体
```json
{
  "name": "素颜霜", "category": "string", "source": "preset|custom",
  "selling_points": ["控油", "补水"],
  "style_tone": "素人分享风",
  "text_angles": ["痛点切入", "成分党", "使用场景"],
  "custom_prompt": "string|null",
  "banned_word_ids": [1, 2]
}
```

### POST /api/v1/products/{id}/benchmarks 请求体
```json
{
  "screenshot_url": "string",
  "highlights": "string",
  "link_url": "string|null"
}
```

### GET /api/v1/banned-words 返回 items 单项
```json
{
  "id": 1, "word": "美白", "level": "auto_fix|soft_warn|hard_block",
  "replacement": "提亮|null", "updatable": true, "workspace_id": 1
}
```

### POST/PUT /api/v1/banned-words 请求体
```json
{ "word": "祛斑", "level": "hard_block", "replacement": null, "updatable": true }
```

### GET /api/v1/tasks 返回 items 单项
```json
{
  "id": 1, "product_id": 1, "theme": "string", "status": "pending|generating|pending_selection|pending_review|approved|rejected|archived",
  "created_at": "2026-01-01T00:00:00Z", "text_count": 5, "image_count": 3
}
```

### GET /api/v1/review/queue 返回 items 单项
```json
{
  "task_id": 1, "product_name": "string", "theme": "string",
  "submitted_at": "2026-01-01T00:00:00Z", "operator_name": "string",
  "selected_text": { "candidate_id": 101, "angle_label": "string", "content": "string" },
  "selected_image": { "candidate_id": 201, "strategy": "A|B|C", "url": "string" }
}
```

### GET /api/v1/tasks/{id}/preference/context 返回 data
```json
{
  "recent_preference": "最近偏好摘要：偏好痛点切入角度（已选3次）",
  "reject_reasons": ["标题太硬广", "图换了包装"],
  "injected_count": 3
}
```

### POST /api/v1/tasks/{id}/package 请求体
```json
{ "note_ids": [1, 2, 3] }
```
返回 data：`{ "delivery_package_id": 1, "status": "pending" }`

### GET /api/v1/delivery-packages/{id}/download 返回 data
```json
{ "id": 1, "status": "pending|ready|downloaded", "download_url": "string|null", "expires_at": "string|null" }
```

### image_candidate 的 role 字段说明
`role` = storyboard角色，枚举：`hook`(钩子) / `pain`(痛点) / `proof`(证明) / `quality`(质感) / `credit`(背书) / `convert`(转化)。单张图时 role=`main`。

---

> **冻结戳记**：Lead 冻结，日期 2026-06-08。BE/FE/AIE 按此并行开工，契约变更需所有 agent 确认后方可修改。
