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
# 二期端点（注册/矩阵号/管理后台）不在此文件中定义

