baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
734
openapi.yaml
Normal file
734
openapi.yaml
Normal file
@@ -0,0 +1,734 @@
|
||||
openapi: "3.0.3"
|
||||
info:
|
||||
title: Clover API
|
||||
version: "1.0.0"
|
||||
description: "小红书内容生产平台 Clover — 一期 1A+1B"
|
||||
|
||||
servers:
|
||||
- url: /api/v1
|
||||
|
||||
security:
|
||||
- BearerAuth: []
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
|
||||
schemas:
|
||||
# ── 通用包络 ──
|
||||
SuccessEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: integer, example: 0 }
|
||||
data: { type: object }
|
||||
|
||||
ErrorEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
code: { type: integer, example: 40001 }
|
||||
message: { type: string }
|
||||
|
||||
Pagination:
|
||||
type: object
|
||||
properties:
|
||||
items: { type: array, items: { type: object } }
|
||||
total: { type: integer }
|
||||
page: { type: integer }
|
||||
page_size: { type: integer }
|
||||
|
||||
# ── 认证 ──
|
||||
LoginRequest:
|
||||
type: object
|
||||
required: [username, password]
|
||||
properties:
|
||||
username: { type: string }
|
||||
password: { type: string }
|
||||
|
||||
LoginData:
|
||||
type: object
|
||||
properties:
|
||||
token: { type: string }
|
||||
user:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
username: { type: string }
|
||||
email: { type: string }
|
||||
current_workspace_id: { type: integer }
|
||||
role: { type: string, enum: [admin, supervisor, operator] }
|
||||
|
||||
MeData:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
username: { type: string }
|
||||
email: { type: string }
|
||||
current_workspace_id: { type: integer }
|
||||
role: { type: string }
|
||||
workspace:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
name: { type: string }
|
||||
slug: { type: string }
|
||||
|
||||
SwitchWorkspaceRequest:
|
||||
type: object
|
||||
required: [workspace_id]
|
||||
properties:
|
||||
workspace_id: { type: integer }
|
||||
|
||||
SwitchWorkspaceData:
|
||||
type: object
|
||||
properties:
|
||||
current_workspace_id: { type: integer }
|
||||
role: { type: string }
|
||||
token: { type: string }
|
||||
|
||||
# ── API Key ──
|
||||
ApiKeyItem:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
provider: { type: string, example: openai }
|
||||
key_last4: { type: string, example: ab12 }
|
||||
created_at: { type: string, format: date-time }
|
||||
|
||||
ApiKeyCreateRequest:
|
||||
type: object
|
||||
required: [provider, api_key]
|
||||
properties:
|
||||
provider: { type: string }
|
||||
api_key: { type: string, description: "明文key仅传输,后端Fernet加密存储" }
|
||||
|
||||
# ── 产品档案 ──
|
||||
ProductCreateRequest:
|
||||
type: object
|
||||
required: [name, category, source]
|
||||
properties:
|
||||
name: { type: string }
|
||||
category: { type: string, description: "纯数据字段,不枚举" }
|
||||
source: { type: string, enum: [preset, custom] }
|
||||
selling_points: { type: array, items: { type: string } }
|
||||
style_tone: { type: string }
|
||||
text_angles: { type: array, items: { type: string } }
|
||||
custom_prompt: { type: string, nullable: true }
|
||||
banned_word_ids: { type: array, items: { type: integer } }
|
||||
|
||||
ProductItem:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
name: { type: string }
|
||||
category: { type: string }
|
||||
source: { type: string }
|
||||
selling_points: { type: array, items: { type: string } }
|
||||
style_tone: { type: string }
|
||||
text_angles: { type: array, items: { type: string } }
|
||||
custom_prompt: { type: string, nullable: true }
|
||||
workspace_id: { type: integer }
|
||||
created_at: { type: string, format: date-time }
|
||||
|
||||
# ── 标杆笔记 ──
|
||||
BenchmarkCreateRequest:
|
||||
type: object
|
||||
required: [screenshot_url, highlights]
|
||||
properties:
|
||||
screenshot_url: { type: string }
|
||||
highlights: { type: string }
|
||||
link_url: { type: string, nullable: true }
|
||||
|
||||
BenchmarkItem:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
product_id: { type: integer }
|
||||
screenshot_url: { type: string }
|
||||
highlights: { type: string }
|
||||
link_url: { type: string, nullable: true }
|
||||
created_at: { type: string, format: date-time }
|
||||
|
||||
# ── 违禁词 ──
|
||||
BannedWordItem:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
word: { type: string }
|
||||
level: { type: string, enum: [auto_fix, soft_warn, hard_block] }
|
||||
replacement: { type: string, nullable: true }
|
||||
updatable: { type: boolean }
|
||||
workspace_id: { type: integer }
|
||||
|
||||
BannedWordRequest:
|
||||
type: object
|
||||
required: [word, level]
|
||||
properties:
|
||||
word: { type: string }
|
||||
level: { type: string, enum: [auto_fix, soft_warn, hard_block] }
|
||||
replacement: { type: string, nullable: true }
|
||||
updatable: { type: boolean, default: true }
|
||||
|
||||
# ── 生产任务 ──
|
||||
TaskCreateRequest:
|
||||
type: object
|
||||
required: [product_id, theme, text_count, image_count, track]
|
||||
properties:
|
||||
product_id: { type: integer }
|
||||
benchmark_ids: { type: array, items: { type: integer } }
|
||||
theme: { type: string, description: "今天主题,如'夏日清爽控油'" }
|
||||
text_count: { type: integer, description: "用户设定,不写死" }
|
||||
image_count: { type: integer, description: "用户设定,不写死" }
|
||||
track: { type: string, enum: [ai, import], description: "ai=轨A生成/import=轨B导入" }
|
||||
|
||||
TaskListItem:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
product_id: { type: integer }
|
||||
theme: { type: string }
|
||||
status:
|
||||
type: string
|
||||
enum: [pending, generating, pending_selection, pending_review, approved, rejected, archived]
|
||||
created_at: { type: string, format: date-time }
|
||||
text_count: { type: integer }
|
||||
image_count: { type: integer }
|
||||
|
||||
TextCandidateScore:
|
||||
type: object
|
||||
properties:
|
||||
title: { type: integer, maximum: 25 }
|
||||
emotion: { type: integer, maximum: 25 }
|
||||
selling: { type: integer, maximum: 25 }
|
||||
keyword: { type: integer, maximum: 20 }
|
||||
compliance: { type: integer, maximum: 5 }
|
||||
total: { type: integer, maximum: 100 }
|
||||
|
||||
TextCandidateItem:
|
||||
type: object
|
||||
properties:
|
||||
candidate_id: { type: integer }
|
||||
angle_label: { type: string }
|
||||
content: { type: string }
|
||||
source: { type: string, enum: [ai, import] }
|
||||
score: { $ref: '#/components/schemas/TextCandidateScore' }
|
||||
banned_word_status: { type: string, enum: [pass, auto_fixed, soft_warn, hard_block] }
|
||||
|
||||
ImageCandidateItem:
|
||||
type: object
|
||||
properties:
|
||||
candidate_id: { type: integer }
|
||||
strategy: { type: string, enum: [A, B, C] }
|
||||
url: { type: string }
|
||||
role:
|
||||
type: string
|
||||
enum: [hook, pain, proof, quality, credit, convert, main]
|
||||
description: "storyboard角色,单张图用main"
|
||||
|
||||
SelectCandidateRequest:
|
||||
type: object
|
||||
required: [candidate_id]
|
||||
properties:
|
||||
candidate_id: { type: integer }
|
||||
|
||||
ImportTextRequest:
|
||||
type: object
|
||||
required: [content, angle_label]
|
||||
properties:
|
||||
content: { type: string }
|
||||
angle_label: { type: string }
|
||||
|
||||
RejectRequest:
|
||||
type: object
|
||||
required: [reason]
|
||||
properties:
|
||||
reason: { type: string, description: "原文存 preference_events.reason,不做AI归纳" }
|
||||
|
||||
PreferenceContextData:
|
||||
type: object
|
||||
properties:
|
||||
recent_preference: { type: string, example: "偏好痛点切入角度(已选3次)" }
|
||||
reject_reasons: { type: array, items: { type: string }, maxItems: 3 }
|
||||
injected_count: { type: integer }
|
||||
|
||||
ReviewQueueItem:
|
||||
type: object
|
||||
properties:
|
||||
task_id: { type: integer }
|
||||
product_name: { type: string }
|
||||
theme: { type: string }
|
||||
submitted_at: { type: string, format: date-time }
|
||||
operator_name: { type: string }
|
||||
selected_text:
|
||||
type: object
|
||||
properties:
|
||||
candidate_id: { type: integer }
|
||||
angle_label: { type: string }
|
||||
content: { type: string }
|
||||
selected_image:
|
||||
type: object
|
||||
properties:
|
||||
candidate_id: { type: integer }
|
||||
strategy: { type: string, enum: [A, B, C] }
|
||||
url: { type: string }
|
||||
|
||||
PackageRequest:
|
||||
type: object
|
||||
properties:
|
||||
note_ids: { type: array, items: { type: integer } }
|
||||
|
||||
DeliveryPackageData:
|
||||
type: object
|
||||
properties:
|
||||
id: { type: integer }
|
||||
status: { type: string, enum: [pending, ready, downloaded] }
|
||||
download_url: { type: string, nullable: true }
|
||||
expires_at: { type: string, format: date-time, nullable: true }
|
||||
|
||||
paths:
|
||||
# ── 认证 ──
|
||||
/auth/login:
|
||||
post:
|
||||
summary: 登录获取JWT
|
||||
security: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/LoginRequest' }
|
||||
responses:
|
||||
"200":
|
||||
description: 登录成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SuccessEnvelope'
|
||||
- properties:
|
||||
data: { $ref: '#/components/schemas/LoginData' }
|
||||
|
||||
/auth/me:
|
||||
get:
|
||||
summary: 当前用户信息
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SuccessEnvelope'
|
||||
- properties:
|
||||
data: { $ref: '#/components/schemas/MeData' }
|
||||
|
||||
/workspaces/switch:
|
||||
post:
|
||||
summary: 切换workspace(查membership)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SwitchWorkspaceRequest' }
|
||||
responses:
|
||||
"200":
|
||||
description: 切换成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SuccessEnvelope'
|
||||
- properties:
|
||||
data: { $ref: '#/components/schemas/SwitchWorkspaceData' }
|
||||
|
||||
# ── API Key ──
|
||||
/api-keys:
|
||||
get:
|
||||
summary: 列出API Key(只显provider+后4位)
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
post:
|
||||
summary: 录入API Key(Fernet加密存储)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ApiKeyCreateRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/api-keys/{id}:
|
||||
delete:
|
||||
summary: 删除API Key
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
# ── 品牌库 ──
|
||||
/products:
|
||||
get:
|
||||
summary: 产品档案列表
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
post:
|
||||
summary: 新建产品档案
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ProductCreateRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/products/{id}:
|
||||
get:
|
||||
summary: 产品详情
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
put:
|
||||
summary: 更新产品档案
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ProductCreateRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
delete:
|
||||
summary: 删除产品档案
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/products/{id}/benchmarks:
|
||||
get:
|
||||
summary: 标杆笔记列表
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
post:
|
||||
summary: 新增标杆笔记(截图+手填亮点为主通道)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/BenchmarkCreateRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
# ── 违禁词 ──
|
||||
/banned-words:
|
||||
get:
|
||||
summary: 违禁词库列表
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
post:
|
||||
summary: 新增违禁词
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/BannedWordRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/banned-words/{id}:
|
||||
put:
|
||||
summary: 更新违禁词
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/BannedWordRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
delete:
|
||||
summary: 删除违禁词
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
# ── 生产链 ──
|
||||
/tasks:
|
||||
get:
|
||||
summary: 任务列表(可按status筛选)
|
||||
parameters:
|
||||
- in: query
|
||||
name: status
|
||||
schema: { type: string }
|
||||
- in: query
|
||||
name: page
|
||||
schema: { type: integer, default: 1 }
|
||||
- in: query
|
||||
name: page_size
|
||||
schema: { type: integer, default: 20 }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
post:
|
||||
summary: 发起生产任务(校验key→只推task_id入Celery队列)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/TaskCreateRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
"422":
|
||||
description: 未配API Key或违禁词硬拦截
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ErrorEnvelope' }
|
||||
|
||||
/tasks/{id}:
|
||||
get:
|
||||
summary: 任务详情(含候选+状态机当前态)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/tasks/{id}/sse-ticket:
|
||||
post:
|
||||
summary: 签发一次性 SSE ticket(60s有效,换票后立即建 EventSource)
|
||||
description: "安全红线:SSE 不传 JWT 进 URL,先换 ticket 再连流。"
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
ticket: { type: string }
|
||||
expires_in: { type: integer, example: 60 }
|
||||
|
||||
/tasks/{id}/stream:
|
||||
get:
|
||||
summary: SSE实时进度(ticket认证,断线重连后端补发历史事件)
|
||||
description: "用 ?ticket= 一次性短票认证,禁止 ?token= 直传 JWT。"
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
- in: query
|
||||
name: ticket
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- in: query
|
||||
name: last_seq
|
||||
required: false
|
||||
schema: { type: integer, default: 0 }
|
||||
responses:
|
||||
"200":
|
||||
description: SSE流
|
||||
content:
|
||||
text/event-stream:
|
||||
schema: { type: string }
|
||||
|
||||
/tasks/{id}/text-candidates/select:
|
||||
post:
|
||||
summary: 选文案(飞轮入口1,+3分)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SelectCandidateRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/tasks/{id}/image-candidates/select:
|
||||
post:
|
||||
summary: 选图(飞轮入口2,+3分)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/SelectCandidateRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/tasks/{id}/import-text:
|
||||
post:
|
||||
summary: 轨B:导入外部文案直接进候选池(跳过AI生成)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ImportTextRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/tasks/{id}/regenerate:
|
||||
post:
|
||||
summary: 重新生成(飞轮-1分)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/tasks/{id}/submit-review:
|
||||
post:
|
||||
summary: 提交审核(状态→pending_review)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/tasks/{id}/preference/context:
|
||||
get:
|
||||
summary: 取飞轮偏好上下文(供前端显示"本次已注入"摘要)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SuccessEnvelope'
|
||||
- properties:
|
||||
data: { $ref: '#/components/schemas/PreferenceContextData' }
|
||||
|
||||
# ── 审核 ──
|
||||
/review/queue:
|
||||
get:
|
||||
summary: 待审队列(组长视角)
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/review/{task_id}/approve:
|
||||
post:
|
||||
summary: 审核通过(飞轮+5,状态→approved)
|
||||
parameters:
|
||||
- in: path
|
||||
name: task_id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/review/{task_id}/reject:
|
||||
post:
|
||||
summary: 打回(写原因,-3,回pending_selection,原因下次注入prompt)
|
||||
parameters:
|
||||
- in: path
|
||||
name: task_id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/RejectRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
# ── 交付包 ──
|
||||
/tasks/{id}/package:
|
||||
post:
|
||||
summary: 生成达人素材交付包(去水印+打包)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/PackageRequest' }
|
||||
responses:
|
||||
"200": { description: OK }
|
||||
|
||||
/delivery-packages/{id}/download:
|
||||
get:
|
||||
summary: 下载交付包(status:pending/ready/downloaded)
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/SuccessEnvelope'
|
||||
- properties:
|
||||
data: { $ref: '#/components/schemas/DeliveryPackageData' }
|
||||
|
||||
# SSE事件约定见 启动包/API契约.md §2
|
||||
# 二期端点(注册/矩阵号/管理后台)不在此文件中定义
|
||||
|
||||
Reference in New Issue
Block a user