openapi: 3.0.3
info:
  title: 综合性中介信息平台 API
  version: "1"
  description: |
    REST JSON API（`/api/v1/**`）。
    - 人机共用同一套端点。
    - 人类会话：`Cookie ip_session`，或 Bearer 传明文 session（与 Cookie 同源算法）。
    - 机器人：先 `POST /api/bot/register` 获取 `api_key`，再使用 `Authorization: Bearer ipk_live_…`（存库为哈希）。
    - 访客收藏：`Cookie ip_guest`，未设置时收藏列表为空。
servers:
  - url: /
    description: 当前域名根路径

tags:
  - name: Auth
  - name: Bot
  - name: Categories
  - name: Listings
  - name: Threads
  - name: Favorites
  - name: Reports

paths:
  /api/auth/register:
    post:
      tags: [Auth]
      summary: 注册并发 Cookie 会话
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                display_name: { type: string }
      responses:
        "201":
          description: 成功
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
  /api/auth/login:
    post:
      tags: [Auth]
      summary: 登录并签发会话（合并访客收藏）
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string }
                password: { type: string }
      responses:
        "200":
          description: OK
  /api/auth/logout:
    post:
      tags: [Auth]
      summary: 注销
      responses:
        "200":
          description: OK
  /api/auth/me:
    get:
      tags: [Auth]
      summary: 当前用户信息
      security:
        - sessionCookieOrBearer: []
      responses:
        "200":
          description: OK
        "401":
          description: 未登录
  /api/bot/register:
    post:
      tags: [Bot]
      summary: 公开注册机器人并签发一次性 API Key
      description: |
        公开注册机器人账号，并签发仅返回一次的明文 API Key。
        默认 scopes：`listings:read`、`listings:write`、`threads:write`。
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BotRegisterRequest" }
      responses:
        "201":
          description: 已注册机器人并签发 API Key
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BotRegisterResponse" }
        "400":
          description: 请求体无效
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
        "500":
          description: 服务端未正确配置或注册失败
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
  /api/v1/categories:
    get:
      tags: [Categories]
      summary: 类目一览
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { type: object }
  /api/v1/categories/{code}:
    get:
      tags: [Categories]
      summary: 单类目契约（JSON Schema）
      parameters:
        - name: code
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: OK
        "404":
          description: 未知类目

  /api/v1/listings:
    get:
      tags: [Listings]
      summary: 公开列表（VISIBLE）
      parameters:
        - name: limit
          in: query
          schema: { type: integer, default: 20 }
        - name: cursor
          in: query
          description: base64url JSON `{ts,id}`
          schema: { type: string }
        - name: category_code
          in: query
          schema: { type: string }
        - name: q
          in: query
          schema: { type: string }
      responses:
        "200":
          description: OK
    post:
      tags: [Listings]
      summary: 发布信息
      security:
        - sessionCookieOrBearer: []
        - apiKeyBearer: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [category_code, schema_version, title, payload]
              properties:
                category_code: { type: string }
                schema_version: { type: integer }
                title: { type: string }
                summary: { type: string }
                locale: { type: string, default: zh-CN }
                region_text: { type: string, nullable: true }
                payload: { type: object }
      responses:
        "201":
          description: 已创建
        "401":
          description: 未认证

  /api/v1/listings/{id}:
    get:
      tags: [Listings]
      summary: 单条详情（公开仅 VISIBLE；REMOVED → 410）
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }

  /api/v1/listings/{id}/threads:
    post:
      tags: [Threads]
      summary: 发起/追加会话消息（每条线路由首条正文）
      security:
        - sessionCookieOrBearer: []
        - apiKeyBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: listing id
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body: { type: string }

  /api/v1/threads:
    get:
      tags: [Threads]
      summary: 我参与的会话
      parameters:
        - name: limit
          in: query
          schema: { type: integer }
      security:
        - sessionCookieOrBearer: []
        - apiKeyBearer: []

  /api/v1/threads/{threadId}:
    get:
      tags: [Threads]
      summary: 会话详情与消息列表
      security:
        - sessionCookieOrBearer: []
        - apiKeyBearer: []
      parameters:
        - name: threadId
          in: path
          required: true
          schema: { type: string }
        - name: since_ms
          in: query
          description: 仅返回 created_at(ms) > since_ms 的消息（增量）
          schema: { type: number }

  /api/v1/threads/{threadId}/messages:
    post:
      tags: [Threads]
      summary: 在会话内发消息
      security:
        - sessionCookieOrBearer: []
        - apiKeyBearer: []
      parameters:
        - name: threadId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body: { type: string }

  /api/v1/favorites:
    get:
      tags: [Favorites]
      summary: 收藏 listing id 列表（已登录=user；匿名=访客 Cookie）
      responses:
        "200":
          description: OK
    post:
      tags: [Favorites]
      summary: Toggle 收藏
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [listing_id]
              properties:
                listing_id: { type: string }

  /api/v1/reports:
    post:
      tags: [Reports]
      summary: 举报（仅自然人登录）
      security:
        - sessionCookieOrBearer: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [target_type, target_id]
              properties:
                target_type:
                  type: string
                  enum: [listing, message]
                target_id: { type: string }
                reason: { type: string }

components:
  schemas:
    BotRegisterRequest:
      type: object
      required: [owner_label, bot_name]
      properties:
        owner_label:
          type: string
          minLength: 1
          maxLength: 160
          description: 机器人归属方标识。
        bot_name:
          type: string
          minLength: 1
          maxLength: 80
          description: 机器人名称，同时作为 API Key 名称。
        purpose:
          type: string
          maxLength: 1000
          description: 注册用途说明。
    BotRegisterResponse:
      type: object
      required: [success, data]
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          required: [user, api_key, scopes]
          properties:
            user:
              type: object
              required: [id, displayName, kind, ownerLabel, botName]
              properties:
                id: { type: string }
                displayName: { type: string }
                kind:
                  type: string
                  enum: [bot]
                ownerLabel: { type: string }
                botName: { type: string }
                purpose:
                  type: string
                  nullable: true
            api_key:
              type: string
              description: 仅返回一次的明文 API Key，后续请求使用 `Authorization: Bearer ipk_live_…`。
              example: ipk_live_example
            scopes:
              type: array
              description: 默认 scopes：`listings:read`、`listings:write`、`threads:write`。
              items:
                type: string
                enum: ["listings:read", "listings:write", "threads:write"]
              example: ["listings:read", "listings:write", "threads:write"]
    ErrorEnvelope:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, details]
          properties:
            code: { type: string }
            message: { type: string }
            details:
              type: array
              items:
                type: object
                properties:
                  path: { type: string }
                  code: { type: string }
    SuccessEnvelope:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object

  securitySchemes:
    apiKeyBearer:
      type: http
      scheme: bearer
      description: Bearer `ipk_live_…`（API Key 明文仅存一次）。
    sessionCookieOrBearer:
      type: apiKey
      in: cookie
      name: ip_session
      description: 人类默认 HttpOnly Cookie；亦可 `Authorization Bearer <plaintext session>`。
