openapi: 3.0.3
info:
  title: GlobalPulse API
  version: 2.0.0
  description: |
    GlobalPulse 公开 API v1 的 V2.0 文档契约。

    文档版本升级不改变线上路径：生产服务仍使用 /api/v1。
    受保护接口需要 X-API-Key（gpapi_ 前缀）和 User-Agent。
    配额为账户级 10,000 次/月，通用限流为 60 次/分钟。
  contact:
    name: GlobalPulse Support
    email: support@global-pulse.net
    url: https://api.global-pulse.net/docs.html
  license:
    name: Proprietary
    url: https://global-pulse.net/terms
servers:
  - url: https://api.global-pulse.net/api/v1
    description: Production
tags:
  - name: Health
  - name: News
  - name: Market
  - name: AI
security:
  - ApiKeyAuth: []
paths:
  /ping:
    get:
      tags: [Health]
      summary: 健康检查
      description: 无需 Key，不消耗配额。
      security: []
      responses:
        '200':
          description: 服务在线
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PingResponse'
  /news/latest:
    get:
      tags: [News]
      summary: 最新新闻流
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/UserAgentHeader'
        - name: limit
          in: query
          schema: { type: integer, default: 100, minimum: 1, maximum: 1000 }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /news/search:
    get:
      tags: [News]
      summary: 关键词搜索新闻
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/UserAgentHeader'
        - name: q
          in: query
          required: true
          schema: { type: string, minLength: 2 }
        - name: limit
          in: query
          schema: { type: integer, default: 50, minimum: 1, maximum: 200 }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /news/topic/{topic}:
    get:
      tags: [News]
      summary: 主题新闻流
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/UserAgentHeader'
        - name: topic
          in: path
          required: true
          schema: { type: string, example: ai }
          description: 标准主题 ai/chip/ev/energy/biotech/realestate/consumer/finance，或 all/concept/industry/macro 别名。
        - name: limit
          in: query
          schema: { type: integer, default: 100, minimum: 1, maximum: 500 }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /news/topics:
    get:
      tags: [News]
      summary: 主题元数据
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/UserAgentHeader'
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /news/archive:
    get:
      tags: [News]
      summary: 全量新闻档案
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/UserAgentHeader'
        - name: limit
          in: query
          schema: { type: integer, default: 1000, minimum: 1, maximum: 5000 }
        - name: region
          in: query
          schema: { type: string, example: cn }
        - name: since
          in: query
          schema: { type: integer, minimum: 0, description: Unix 时间戳（毫秒） }
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /market/indices:
    get:
      tags: [Market]
      summary: 全球主要指数
      parameters: [{ $ref: '#/components/parameters/ApiKeyHeader' }, { $ref: '#/components/parameters/UserAgentHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /market/sentiment:
    get:
      tags: [Market]
      summary: 市场情绪
      parameters: [{ $ref: '#/components/parameters/ApiKeyHeader' }, { $ref: '#/components/parameters/UserAgentHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /market/limit-stats:
    get:
      tags: [Market]
      summary: 涨跌停统计
      parameters: [{ $ref: '#/components/parameters/ApiKeyHeader' }, { $ref: '#/components/parameters/UserAgentHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /market/north-flow:
    get:
      tags: [Market]
      summary: 北向资金
      description: 非交易日或数据源暂无数据时，today 可为 null，history 可为空数组。
      parameters: [{ $ref: '#/components/parameters/ApiKeyHeader' }, { $ref: '#/components/parameters/UserAgentHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/UpstreamError' }
  /market/concept-graph:
    get:
      tags: [Market]
      summary: 概念图谱 3D
      description: 全景视图消耗 10 次配额，单股视图消耗 1 次；该接口另有 1 次/30 秒独立限频。缓存命中仍遵循接口配额规则。
      parameters:
        - $ref: '#/components/parameters/ApiKeyHeader'
        - $ref: '#/components/parameters/UserAgentHeader'
        - name: code
          in: query
          schema: { type: string, pattern: '^\d{6}$', example: '600519' }
        - name: top
          in: query
          schema: { type: integer, default: 20, minimum: 1, maximum: 50 }
        - name: members
          in: query
          schema: { type: integer, default: 10, minimum: 1, maximum: 30 }
      responses:
        '200': { $ref: '#/components/responses/GraphSuccess' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/GraphRateLimited' }
        '500': { $ref: '#/components/responses/Internal' }
        '503': { $ref: '#/components/responses/UpstreamUnavailable' }
  /ai/narrative:
    get:
      tags: [AI]
      summary: AI 市场叙事
      parameters: [{ $ref: '#/components/parameters/ApiKeyHeader' }, { $ref: '#/components/parameters/UserAgentHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
  /ai/signals:
    get:
      tags: [AI]
      summary: AI 信号复盘
      parameters: [{ $ref: '#/components/parameters/ApiKeyHeader' }, { $ref: '#/components/parameters/UserAgentHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
  /ai/post-scan:
    get:
      tags: [AI]
      summary: 盘后扫描
      description: 兼容别名 /ai/postscan 指向同一处理器。
      parameters: [{ $ref: '#/components/parameters/ApiKeyHeader' }, { $ref: '#/components/parameters/UserAgentHeader' }]
      responses:
        '200': { $ref: '#/components/responses/Success' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/QuotaExceeded' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: PRO 用户 API Key，格式为 gpapi_...；不要放在 URL 或前端公开代码中。
  parameters:
    ApiKeyHeader:
      name: X-API-Key
      in: header
      required: true
      schema: { type: string, pattern: '^gpapi_.{16,}$', example: gpapi_xxxxxxxx_xxxxxxxxxxxx }
    UserAgentHeader:
      name: User-Agent
      in: header
      required: true
      schema: { type: string, minLength: 1, example: my-app/1.0 }
  schemas:
    SuccessEnvelope:
      type: object
      required: [ok, data, ts]
      properties:
        ok: { type: boolean, enum: [true] }
        data: { nullable: true }
        ts: { type: integer, format: int64, description: Unix 时间戳（毫秒） }
        _cost: { type: integer, minimum: 1, description: 本次调用消耗的配额单位 }
    GraphEnvelope:
      allOf:
        - $ref: '#/components/schemas/SuccessEnvelope'
        - type: object
          properties:
            quota_cost: { type: integer, enum: [1, 10] }
    ErrorEnvelope:
      type: object
      required: [ok, code, error]
      properties:
        ok: { type: boolean, enum: [false] }
        code: { type: string }
        error: { type: string }
        details: { type: object, additionalProperties: true }
        retry_after_seconds: { type: integer, minimum: 0 }
    PingResponse:
      type: object
      required: [ok, service, version, ts]
      properties:
        ok: { type: boolean, enum: [true] }
        service: { type: string, example: gp-api-v1 }
        version: { type: string, example: 1.0.0 }
        ts: { type: integer, format: int64 }
  responses:
    Success:
      description: 成功
      content: { application/json: { schema: { $ref: '#/components/schemas/SuccessEnvelope' } } }
    GraphSuccess:
      description: 概念图谱成功
      content: { application/json: { schema: { $ref: '#/components/schemas/GraphEnvelope' } } }
    BadRequest:
      description: 参数错误
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    Unauthorized:
      description: Key 缺失或无效
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    QuotaExceeded:
      description: 月配额不足（HTTP 402）
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    RateLimited:
      description: 通用 60 次/分钟限流
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    GraphRateLimited:
      description: 概念图谱 1 次/30 秒限流
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    InternalError:
      description: 内部错误
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    Internal:
      description: 概念图谱内部错误
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    UpstreamError:
      description: 数据引擎错误（HTTP 502）
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
    UpstreamUnavailable:
      description: 概念图谱数据池尚未就绪
      content: { application/json: { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } }
