openapi: 3.0.3
info:
  title: SRDC AI 平台 API
  version: 1.0.0
  description: |
    石資中心 AI 平台對前端 REST API(v1)。

    - 認證:`POST /api/v1/auth/login` 取得 JWT,之後所有請求帶 `Authorization: Bearer <token>`
    - 回應格式:成功 `{ "data": ... }`;失敗 `{ "error": { "code", "message" } }`
    - 進度查詢:MVP 以 polling(`GET /api/v1/jobs/{id}`)為準,SSE 評估中
    - 跨源:`/api/*` 支援 CORS。token 以 `Authorization` header 帶,**不使用 cookie**,
      因此請勿設定 `credentials: "include"`
    - 變更管理:破壞性變更會升版並於 changelog 公告
servers:
  - url: https://srdc-api.ink.net.tw
    description: |
      整合環境(staging)—— 前端目前對接此環境。
      注意:未設定 Gemini API key,AI 產出為 stub 假資料;點數、佇列、狀態流轉皆為真實行為。
  - url: /
    description: 同源部署(正式網址 https://ai.srdc.org.tw 待 DNS 設定)
tags:
  - name: auth
  - name: credits
  - name: task-definitions
  - name: jobs
  - name: assets

paths:
  /api/v1/auth/login:
    post:
      tags: [auth]
      summary: 登入取得 JWT
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string }
      responses:
        "200":
          description: 登入成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      token: { type: string, description: "JWT,效期 24h" }
                      user: { $ref: "#/components/schemas/User" }
        "401": { $ref: "#/components/responses/Error" }

  /api/v1/credits:
    get:
      tags: [credits]
      summary: 今日點數餘額與動作價目表
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      balance: { $ref: "#/components/schemas/Balance" }
                      actionPrices:
                        type: object
                        additionalProperties: { type: integer }
                        example: { generate_image: 20, generate_text: 10, compose_article: 10, publish_ecommerce: 10 }
        "401": { $ref: "#/components/responses/Error" }

  /api/v1/credits/ledger:
    get:
      tags: [credits]
      summary: 點數異動明細(最新 50 筆)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      entries:
                        type: array
                        items: { $ref: "#/components/schemas/LedgerEntry" }
        "401": { $ref: "#/components/responses/Error" }

  /api/v1/task-definitions:
    get:
      tags: [task-definitions]
      summary: 任務定義清單(表單 schema,前端動態渲染依據)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      taskDefinitions:
                        type: array
                        items: { $ref: "#/components/schemas/TaskDefinition" }
        "401": { $ref: "#/components/responses/Error" }

  /api/v1/jobs:
    post:
      tags: [jobs]
      summary: 提交生成任務(依動作組合計價並預扣點數)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                taskDefinitionId:
                  type: string
                  format: uuid
                  description: 任務定義 id;動作組合預設取自定義
                actions:
                  type: array
                  items: { $ref: "#/components/schemas/ActionType" }
                  description: 明確指定動作組合(可覆寫任務定義)
                payload:
                  type: object
                  description: 表單填答(prompt / imagePrompt / sourceImageKey 等)
      responses:
        "201":
          description: 已預扣點數並入佇列
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      job: { $ref: "#/components/schemas/Job" }
                      credits:
                        type: object
                        properties:
                          total: { type: integer, description: 預扣總點數 }
                          balanceAfter: { type: integer }
        "400": { $ref: "#/components/responses/Error" }
        "402":
          description: 點數不足(code=INSUFFICIENT_CREDITS)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorBody" }
        "401": { $ref: "#/components/responses/Error" }
    get:
      tags: [jobs]
      summary: 任務列表(最新 50 筆)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      jobs:
                        type: array
                        items: { $ref: "#/components/schemas/Job" }
        "401": { $ref: "#/components/responses/Error" }

  /api/v1/jobs/{id}:
    get:
      tags: [jobs]
      summary: 任務進度(polling)與結果
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      job: { $ref: "#/components/schemas/Job" }
                      assets:
                        type: array
                        items: { $ref: "#/components/schemas/Asset" }
        "404": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }

  /api/v1/assets:
    get:
      tags: [assets]
      summary: 生成歷史資產(供「自歷程帶入」,最新 50 筆)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      assets:
                        type: array
                        items: { $ref: "#/components/schemas/Asset" }
        "401": { $ref: "#/components/responses/Error" }

  /api/v1/assets/{id}/content:
    get:
      tags: [assets]
      summary: 下載圖片資產內容(image 類專用)
      description: |
        `kind: image` 的資產只在列表回 `storageKey`,儲存層不對外;要顯示圖片請打這支。
        回傳為圖片位元組(非 JSON),需帶 `Authorization` header,因此無法直接放進
        `<img src>` —— 前端請以 fetch 取回後轉 blob URL。
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: 圖片位元組
          content:
            image/png: { schema: { type: string, format: binary } }
            image/jpeg: { schema: { type: string, format: binary } }
        "400":
          description: 該資產為文字內容(code=NOT_BINARY),請讀 `content` 欄位
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorBody" }
        "404":
          description: 資產不存在(NOT_FOUND)或內容已遺失(ASSET_CONTENT_MISSING)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorBody" }
        "401": { $ref: "#/components/responses/Error" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  responses:
    Error:
      description: 錯誤
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorBody" }
  schemas:
    ErrorBody:
      type: object
      properties:
        error:
          type: object
          properties:
            code: { type: string, example: UNAUTHORIZED }
            message: { type: string }
    ActionType:
      type: string
      enum: [generate_image, generate_text, compose_article, publish_ecommerce]
    User:
      type: object
      properties:
        id: { type: string, format: uuid }
        email: { type: string }
        name: { type: string }
        role: { type: string, enum: [super_admin, tenant_admin, tenant_member] }
        tenantId: { type: string, format: uuid, nullable: true }
    Balance:
      type: object
      properties:
        quotaDate: { type: string, format: date, description: Asia/Taipei 當地日期 }
        baseQuota: { type: integer, description: 每日基數(預設 1000) }
        used: { type: integer }
        purchasedBalance: { type: integer, description: 加購餘額(不隨日重置) }
        available: { type: integer, description: 基數 - 已用 + 加購 }
    LedgerEntry:
      type: object
      properties:
        id: { type: string, format: uuid }
        type: { type: string, enum: [grant, purchase, deduct, refund, reset] }
        action: { $ref: "#/components/schemas/ActionType" }
        amount: { type: integer, description: 正為入點、負為扣點 }
        balanceAfter: { type: integer }
        refJobId: { type: string, format: uuid, nullable: true }
        createdAt: { type: string, format: date-time }
    TaskDefinition:
      type: object
      properties:
        id: { type: string, format: uuid }
        slug: { type: string, example: craft-3-marketing-copy }
        industry: { type: string, enum: [craft, agrifood] }
        title: { type: string }
        formSchema:
          type: object
          description: 表單欄位(type:text / single_select / multi_select / cascading_select / image_card / range_quick / upload / guided)
        actions:
          type: array
          items: { $ref: "#/components/schemas/ActionType" }
        sharedTemplateKey: { type: string, nullable: true }
        version: { type: integer }
    Job:
      type: object
      properties:
        id: { type: string, format: uuid }
        taskDefinitionId: { type: string, format: uuid, nullable: true }
        actions:
          type: array
          items: { $ref: "#/components/schemas/ActionType" }
        status:
          type: string
          enum: [pending, processing, succeeded, partially_failed, failed]
        heldCredits: { type: integer }
        progress:
          type: array
          nullable: true
          description: 逐動作進度(每完成一動作即更新)
          items:
            type: object
            properties:
              action: { $ref: "#/components/schemas/ActionType" }
              ok: { type: boolean }
              assetId: { type: string, format: uuid, nullable: true }
              error: { type: string, nullable: true }
        error: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    Asset:
      type: object
      properties:
        id: { type: string, format: uuid }
        jobId: { type: string, format: uuid, nullable: true }
        kind: { type: string, enum: [text, image, article] }
        content: { type: string, nullable: true, description: text/article 的內容 }
        storageKey: { type: string, nullable: true, description: image 的儲存 key }
        createdAt: { type: string, format: date-time }

security:
  - bearerAuth: []
