baseline: Clover 独立仓库首次基线提交

将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。
当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包),
M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-16 11:30:22 +08:00
commit 6a2632da70
253 changed files with 27467 additions and 0 deletions

734
openapi.yaml Normal file
View 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 KeyFernet加密存储
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 ticket60s有效换票后立即建 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
# 二期端点(注册/矩阵号/管理后台)不在此文件中定义