openapi: 3.1.0
info:
  title: AI 货盘导入器 API
  version: 0.1.0
  description: |
    极简上传导入器后端接口。前端只负责上传文件、查看任务进度和导入报告；
    字段校对和状态流转在飞书多维表格内完成。
    查重规则：商品链接优先；无链接时按品牌+产品名称+规格，并用品牌+产品名称兜底。
servers:
  - url: http://127.0.0.1:8787
    description: local mock server
paths:
  /api/jobs:
    post:
      summary: 创建货盘导入任务
      description: 上传一个或多个货盘资料文件，并创建异步解析任务。
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - files
              properties:
                files:
                  type: array
                  items:
                    type: string
                    format: binary
                brand:
                  type: string
                  description: 可选，品牌或商家名。
                owner:
                  type: string
                  description: 可选，对接人。
                contact:
                  type: string
                  description: 可选，商务联系方式。
                target:
                  type: string
                  enum: [test_base, production_base]
                  default: test_base
                dry_run:
                  type: boolean
                  default: true
                  description: true 时只生成导入报告，不写入飞书；false 时写入非重复候选到测试 Base。
      responses:
        "200":
          description: 任务创建成功。
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobCreated"
  /api/model-config:
    get:
      summary: 获取本地大模型配置状态
      description: 只返回脱敏后的配置状态，不返回 API Key 明文。
      responses:
        "200":
          description: 当前配置状态。
    post:
      summary: 保存本地大模型配置
      description: 本地开发接口。保存 API Key、Base URL 和模型名到 .env。
      responses:
        "200":
          description: 保存成功。
  /api/model-config/models:
    post:
      summary: 检测当前 API Key 可用模型
      description: 请求 OpenAI 兼容接口的 /models，返回可用模型列表并优先推荐 gpt-5.4。
      responses:
        "200":
          description: 模型检测成功。
  /api/analyzer-policy:
    get:
      summary: 获取当前识别策略
      description: 返回后端是否已加载项目专用提示词、JSON schema、查重规则、类目规则和多模态输入策略。
      responses:
        "200":
          description: 识别策略状态。
  /api/jobs/{job_id}:
    get:
      summary: 查询导入任务状态
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "200":
          description: 当前任务状态。
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobStatus"
  /api/jobs/{job_id}/report:
    get:
      summary: 获取导入报告
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "200":
          description: 任务导入报告。
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ImportReport"
  /api/jobs/{job_id}/candidates:
    get:
      summary: 获取候选商品列表
      description: 可选调试接口。第一版主流程不要求前端编辑候选商品。
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "200":
          description: 候选商品列表。
          content:
            application/json:
              schema:
                type: object
                properties:
                  candidates:
                    type: array
                    items:
                      $ref: "#/components/schemas/Candidate"
  /api/jobs/{job_id}/retry:
    post:
      summary: 重新执行失败任务
      parameters:
        - $ref: "#/components/parameters/JobId"
      responses:
        "200":
          description: 已重新排队。
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobStatus"
  /api/import-batches/{batch_id}/records:
    get:
      summary: 查询某个导入批次写入的记录
      parameters:
        - $ref: "#/components/parameters/BatchId"
      responses:
        "200":
          description: 批次记录列表。
  /api/import-batches/{batch_id}/rollback:
    post:
      summary: 回滚某个导入批次
      description: 按 AI/导入批次ID 查找测试 Base 内记录，并批量删除。只接受 imp_YYYYMMDD_HHMMSS_xxxxxx 格式批次 ID。
      parameters:
        - $ref: "#/components/parameters/BatchId"
      responses:
        "200":
          description: 删除结果。
components:
  parameters:
    JobId:
      name: job_id
      in: path
      required: true
      schema:
        type: string
    BatchId:
      name: batch_id
      in: path
      required: true
      schema:
        type: string
        pattern: "^imp_[0-9]{8}_[0-9]{6}_[0-9a-f]{6}$"
  schemas:
    JobCreated:
      type: object
      required: [job_id, status]
      properties:
        job_id:
          type: string
        batch_id:
          type: string
        dry_run:
          type: boolean
        status:
          type: string
          enum: [queued]
    JobStatus:
      type: object
      required: [job_id, status, current_step, steps]
      properties:
        job_id:
          type: string
        status:
          type: string
          enum: [queued, parsing, writing, completed, failed]
        current_step:
          type: integer
          minimum: 0
          maximum: 7
        batch_id:
          type: string
        dry_run:
          type: boolean
        steps:
          type: array
          items:
            type: object
            required: [name, status]
            properties:
              name:
                type: string
              status:
                type: string
                enum: [pending, running, done, failed]
        message:
          type: string
        report_url:
          type: string
          nullable: true
    ImportReport:
      type: object
      required:
        - job_id
        - status
        - dry_run
        - batch_id
        - records_created
        - records_planned
        - records_need_review
        - images_uploaded
        - duplicate_skipped
        - failed_files
        - base_url
      properties:
        job_id:
          type: string
        status:
          type: string
          enum: [completed, failed]
        dry_run:
          type: boolean
          description: true 表示本次只是预演，没有写入飞书。
        batch_id:
          type: string
          description: 本次导入批次 ID，用于追踪、筛选和回滚。
        records_created:
          type: integer
          description: 实际写入飞书的记录数。预演模式为 0。
        records_planned:
          type: integer
          description: 预演或写入前预计可新增的非重复记录数。
        records_need_review:
          type: integer
        images_uploaded:
          type: integer
        duplicate_skipped:
          type: integer
        analysis_preview:
          type: object
          description: 模型候选商品预览，供上传页展示，不作为最终人工校对界面。
        analyzer_policy:
          type: object
          description: 本次任务使用的识别策略摘要。
        write_summary:
          type: object
          description: 候选数、新增数、重复数摘要。
          description: 导入前查重命中并跳过的商品数量。
        failed_files:
          type: integer
        base_url:
          type: string
        dedupe_policy:
          type: string
          description: 本次导入使用的查重规则。
        schema_check:
          type: object
          description: 导入前字段结构校验结果。
        write_strategy:
          type: string
          enum: [dry_run_only, write_non_duplicates]
        notes:
          type: array
          items:
            type: string
    Candidate:
      type: object
      properties:
        product_name:
          type: string
        brand:
          type: string
        first_category:
          type: string
        second_category:
          type: string
        live_price:
          type: string
        commission:
          type: string
        source_file:
          type: string
        source_position:
          type: string
        needs_review:
          type: array
          items:
            type: string
