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 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: 新增标杆笔记(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 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 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 # 二期端点(注册/矩阵号/管理后台)不在此文件中定义