openapi: "3.1.0"
info:
  title: AITOP Public API
  version: "1.1.0"
  description: >
    Anonymous, token-free read API for AITOP — items, daily reports, RSS-style discovery.
    Rate limit: 60 req/min per IP (app layer). Requires a real User-Agent header (default curl/x.y.z → 403).
  license:
    name: Personal use, fair-use rate limit applies
servers:
  - url: /api/public
components:
  schemas:
    Item:
      type: object
      required: [id, sourceName, url, title, channel, isFeatured, tags]
      properties:
        id:
          type: string
          description: cuid2 (25 chars)
          example: "cm9xyz1234567890abcdefghi"
        sourceName:
          type: string
          example: "OpenAI"
        url:
          type: string
          format: uri
        title:
          type: string
        title_en:
          type: ["string", "null"]
          description: English title; null when source is already English
        summary:
          type: ["string", "null"]
          description: AI-generated summary; null when not yet processed
        category:
          type: ["string", "null"]
          enum: [ai-models, ai-products, industry, paper, tip, null]
        channel:
          type: string
          enum: [firstParty, news, x, community, blog, paper]
        isFeatured:
          type: boolean
        tags:
          type: array
          items:
            type: string
        publishedAt:
          type: ["string", "null"]
          format: date-time
          description: "ISO 8601 UTC (with Z); null when source has no explicit publish time"
        media:
          type: array
          items:
            type: object
            properties:
              kind: { type: string }
              url:  { type: string, format: uri }
              width:  { type: integer }
              height: { type: integer }
    ItemsResponse:
      type: object
      required: [hasMore, nextCursor, items]
      properties:
        hasMore:
          type: boolean
        nextCursor:
          type: ["string", "null"]
          description: Opaque cursor; pass as ?cursor= on next request. null when hasMore=false.
        items:
          type: array
          items:
            $ref: "#/components/schemas/Item"
paths:
  /items:
    get:
      summary: List items (cursor-paginated)
      description: >
        Primary endpoint for agents. Use ?since=24h for recent items (no cursor needed).
        Cursor pagination for historical traversal. take up to 200.
      parameters:
        - name: mode
          in: query
          schema:
            type: string
            enum: [selected, all]
            default: selected
          description: "selected = 精选 (default); all = 全量"
        - name: since
          in: query
          schema:
            type: string
          description: "Time lower bound. Formats: ISO-8601 (2026-05-01T00:00:00Z), date (2026-05-01), relative (24h, 7d)"
          example: "24h"
        - name: take
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 20
          description: Items per page. Agents may use take=100 to reduce round-trips.
        - name: cursor
          in: query
          schema:
            type: string
          description: Opaque cursor from previous response's nextCursor. Omit for first page.
        - name: category
          in: query
          schema:
            type: string
            enum: [ai-models, ai-products, industry, paper, tip]
        - name: channel
          in: query
          schema:
            type: string
            enum: [firstParty, news, x, community, blog, paper]
        - name: q
          in: query
          schema:
            type: string
            maxLength: 100
          description: Keyword search on title + summary (server-side ILIKE, pg_trgm GIN index)
        - name: tag
          in: query
          schema:
            type: string
      responses:
        "200":
          description: OK
          headers:
            Cache-Control:
              schema:
                type: string
                example: "public, s-maxage=60, stale-while-revalidate=120"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ItemsResponse"
        "400":
          description: Invalid parameter
        "429":
          description: Rate limited
          headers:
            Retry-After:
              schema:
                type: integer
  /daily:
    get:
      summary: Latest published daily report
      responses:
        "200":
          description: OK
        "404":
          description: Not found
  /daily/{date}:
    get:
      summary: Daily report by date
      parameters:
        - name: date
          in: path
          required: true
          schema:
            type: string
            pattern: '^\d{4}-\d{2}-\d{2}$'
          example: "2026-05-13"
      responses:
        "200":
          description: OK
        "404":
          description: Not found or not published
  /dailies:
    get:
      summary: List dates that have a published daily report
      parameters:
        - name: take
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 365
            default: 30
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  dailies:
                    type: array
                    items:
                      type: object
                      properties:
                        date:     { type: string, example: "2026-05-13" }
                        headline: { type: string }
                  total: { type: integer }
  /openapi.yaml:
    get:
      summary: This OpenAPI spec (YAML)
      responses:
        "200":
          description: OK
  /openapi.json:
    get:
      summary: This OpenAPI spec (JSON)
      responses:
        "200":
          description: OK
