Files
beige/openapi.yaml
yangqianqian df1856d793 上线版: 产品表单统一+form嵌套修复+用户管理+部署+三套叙事
- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段);
  修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品
- dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开;
  settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch
- 新增用户管理(users API/admin service/改密页) + alembic 022/023/024
- 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example
- A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 18:08:13 +08:00

883 lines
25 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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] }
must_change_password: { type: boolean }
ChangePasswordRequest:
type: object
required: [current_password, new_password]
properties:
current_password: { type: string }
new_password: { type: string, minLength: 10 }
MeData:
type: object
properties:
id: { type: integer }
username: { type: string }
email: { type: string }
current_workspace_id: { type: integer }
role: { type: string }
must_change_password: { type: boolean }
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
properties:
screenshot_url:
type: string
nullable: true
description: 兼容旧字段;新前端不再把外部链接存到这里
highlights: { type: string }
link_url: { type: string, nullable: true }
BenchmarkItem:
type: object
properties:
id: { type: integer }
product_id: { type: integer }
screenshot_url: { type: string, nullable: true }
highlights: { type: string }
link_url: { type: string, nullable: true }
features:
type: object
nullable: true
additionalProperties: true
analyze_status: { type: string, enum: [pending, analyzing, done, failed] }
analysis_source: { type: string, nullable: true }
analysis_warning: { 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, failed]
created_at: { type: string, format: date-time }
text_count: { type: integer }
image_count: { type: integer }
track: { type: string, enum: [ai, import] }
need_product_image: { type: boolean }
review_status: { type: string, nullable: true }
reject_reason: { type: string, nullable: true }
reviewed_at: { type: string, format: date-time, nullable: true }
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 }
strategy: { type: string, enum: [A, B, C], nullable: true }
title: { type: string }
content: { type: string }
tags: { type: array, items: { type: string } }
cover_title: { type: string }
image_brief: { 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] }
is_selected: { type: boolean }
verdict: { type: string }
summary: { type: string }
edited: { type: boolean }
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"
seq: { type: integer }
is_selected: { type: boolean }
is_regen: { type: boolean }
ai_visual_score: { type: number, nullable: true }
ai_visual_note: { type: string, nullable: true }
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:
allOf:
- $ref: '#/components/schemas/TextCandidateItem'
nullable: true
selected_image:
allOf:
- $ref: '#/components/schemas/ImageCandidateItem'
nullable: true
selected_sets:
type: array
items:
type: object
properties:
strategy: { type: string, enum: [A, B, C] }
selected_text:
allOf:
- $ref: '#/components/schemas/TextCandidateItem'
nullable: true
selected_images:
type: array
items:
$ref: '#/components/schemas/ImageCandidateItem'
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/change-password:
post:
summary: 修改当前登录用户密码(首登强制改密)
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/ChangePasswordRequest' }
responses:
"200": { description: OK }
/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: 新增标杆笔记JSON兼容接口外部链接存 link_url不上传截图
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 }
/products/{id}/benchmarks/upload:
post:
summary: 上传标杆笔记(原始链接 + 截图文件 + 手填亮点)
parameters:
- in: path
name: id
required: true
schema: { type: integer }
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
screenshot:
type: string
format: binary
description: JPEG/PNG/WebP最大 10MB用于视觉分析
highlights:
type: string
description: 手填亮点,可选
link_url:
type: string
description: 小红书或外部原始链接,可选,必须 http/https
responses:
"200": { description: OK }
/benchmarks/{id}/analyze:
post:
summary: 分析标杆笔记(截图视觉分析 + 原始链接/亮点上下文)
parameters:
- in: path
name: id
required: true
schema: { type: integer }
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
code: { type: integer }
message: { type: string }
data: { $ref: '#/components/schemas/BenchmarkItem' }
# ── 违禁词 ──
/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
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- properties:
data:
type: object
properties:
items:
type: array
items: { $ref: '#/components/schemas/ReviewQueueItem' }
/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: 查询交付包状态GET只读无副作用
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' }
/delivery-packages/{id}/mark-downloaded:
post:
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/DeliveryPackageData' }
/delivery-packages/{id}/download-file:
get:
summary: 下载交付包 zip 文件GET只读
parameters:
- in: path
name: id
required: true
schema: { type: integer }
responses:
"200": { description: ZIP 文件 }
# SSE事件约定见 启动包/API契约.md §2
# 二期端点(注册/矩阵号/管理后台)不在此文件中定义