# DataHub API 接口规范

本文档面向 **DataHub 服务端开发者**，说明如何实现符合 DataHub SDK 调用规范的 HTTP 接口。
实现这些接口后，DataHub SDK（Python）及其他语言客户端即可直接调用。

---

## 概览

DataHub SDK 依赖以下 HTTP 接口完成「搜索 → 执行」工作流：

| 接口 | 方法 | 路径 | 说明 |
|------|------|------|------|
| 工具搜索 | `GET` | `/api/search` | 根据语义查询匹配可用工具 |
| 工具执行 | `POST` | `/api/execute` | 调用指定工具并返回结果 |

```
SDK 调用流程：

用户意图 (query)
    │
    ▼
GET /api/search?query=...&top_k=3    ← 搜索匹配的工具
    │
    ▼
SDK 侧使用 LLM 根据工具参数定义自动构建 args
    │
    ▼
POST /api/execute { tool, args }     ← 执行工具
    │
    ▼
返回结果 ExecuteResponse
```

---

## 认证方式

DataHub SDK 在请求中可携带两种认证凭据，服务端应按需验证。

### 请求头总览

```http
Content-Type: application/json
X-APP-NAME: <应用标识>                   # 可选，标识调用方应用
Authorization: Bearer <api_key>         # API Key 认证
X-META-INFO: <jwt_token>               # MetaInfo JWT 认证
```

### 方式一：API Key 认证

**请求头**：`Authorization: Bearer <api_key>`

**服务端验证逻辑**：

1. 从请求头提取 `Authorization` 字段
2. 校验格式为 `Bearer <token>`
3. 验证 API Key 有效性（查库或缓存）
4. 检查权限和有效期

**错误响应**（建议）：

- API Key 缺失或格式错误 → `401 Unauthorized`
- API Key 无效或已过期 → `403 Forbidden`

### 方式二：MetaInfo JWT 认证

**请求头**：`X-META-INFO: <jwt_token>`

**JWT 结构**：

| 部分 | 说明 |
|------|------|
| Header | `{"alg": "EdDSA", "typ": "JWT"}` |
| Payload | MetaInfo 字段（见下表）+ `exp` 过期时间 |
| Signature | Ed25519 数字签名 |

**MetaInfo Payload 字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `subject` | string | 数据主体名称，如 `"鮨大山"` |
| `subject_code` | string | 主体唯一编码。支持单主体（如 `"yidashan"`）或多级主体，使用 `/` 分隔层级（如 `"yidashan/market_dept"`、`"jiangsu/jintan"`） |
| `caller_id` | string | 调用者标识，用于匹配公钥 |
| `data_coverage` | string | 数据周期，格式 `yyyyMM-yyyyMM` 或 `yyyyMMdd-yyyyMMdd` |
| `creation_slug` | string | 报告类型标识，如 `"single_store_profit_model"` |
| `creation_name` | string | 报告类型名称，如 `"单店盈利模型"` |
| `exp` | integer | JWT 过期时间戳（UNIX 秒） |

**服务端验证逻辑**：

1. 从请求头提取 `X-META-INFO` 字段
2. 解析 JWT Token（三段式 Base64）
3. 根据 Payload 中的 `caller_id` 查找对应的 Ed25519 公钥
4. 使用公钥验证签名
5. 校验 `exp` 是否过期
6. 提取 MetaInfo 供后续业务使用

**错误响应**（建议）：

- JWT 缺失 → `401 Unauthorized`
- 签名验证失败 → `401 Unauthorized`
- Token 已过期 → `401 Unauthorized`（建议在 body 中区分 `expired` 错误码）

### 两种方式的关系

- 两种认证可**同时使用**，也可**只用其中一种**，取决于服务端安全策略
- **API Key** 用于标识调用方身份和权限
- **MetaInfo JWT** 用于传递业务上下文（如数据主体、报告类型等），同时提供签名防篡改

---

## 接口 1：工具搜索

### 请求

```http
GET /api/search?query={query}&top_k={top_k}
```

**查询参数**：

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `query` | string | ✅ | — | 搜索查询，通常是自然语言意图描述 |
| `top_k` | integer | ❌ | 3 | 返回最匹配的工具数量上限 |

### 响应

**成功响应**（`200 OK`）：

```json
{
  "tools": [
    {
      "name": "query_daily_avg_received_amount",
      "desc": "查询指定门店指定月份的日均实收金额",
      "params": {
        "type": "object",
        "desc": "查询参数",
        "props": {
          "store_code": {
            "type": "string",
            "desc": "门店编码"
          },
          "month": {
            "type": "string",
            "desc": "查询月份，格式 yyyyMM"
          }
        },
        "required": ["store_code", "month"]
      },
      "returns": "日均实收金额（元）"
    }
  ]
}
```

### 响应数据模型

#### SearchResponse

| 字段 | 类型 | 说明 |
|------|------|------|
| `tools` | `ToolSchema[]` | 匹配的工具列表，按相关性排序 |

#### ToolSchema

| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | string | 工具唯一名称标识 |
| `desc` | string | 工具功能描述 |
| `params` | `ParameterSchema` | 参数定义（JSON Schema 风格） |
| `returns` | string | 返回值描述 |

#### ParameterSchema

| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | string | 参数类型：`string`、`integer`、`number`、`boolean`、`object`、`array` |
| `desc` | string | 参数描述 |
| `props` | `dict[string, ParameterPropertySchema]` \| null | 当 type 为 `object` 时，定义各属性 |
| `required` | `string[]` \| null | 必填属性名称列表 |

#### ParameterPropertySchema

| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | string | 属性类型：`string`、`integer`、`number`、`boolean`、`object`、`array` |
| `desc` | string | 属性描述 |
| `properties` | `dict[string, ParameterPropertySchema]` \| null | 嵌套对象属性（type 为 `object` 时） |
| `items` | `ParameterPropertySchema` \| null | 数组元素定义（type 为 `array` 时） |
| `enum` | `any[]` \| null | 枚举值列表，限制取值范围 |

### 参数定义示例

**简单参数**：

```json
{
  "type": "object",
  "desc": "查询参数",
  "props": {
    "keyword": {
      "type": "string",
      "desc": "搜索关键词"
    },
    "limit": {
      "type": "integer",
      "desc": "返回数量上限"
    }
  },
  "required": ["keyword"]
}
```

**嵌套对象参数**：

```json
{
  "type": "object",
  "desc": "查询参数",
  "props": {
    "filter": {
      "type": "object",
      "desc": "过滤条件",
      "properties": {
        "category": {
          "type": "string",
          "desc": "分类",
          "enum": ["food", "drink", "dessert"]
        },
        "price_range": {
          "type": "object",
          "desc": "价格区间",
          "properties": {
            "min": { "type": "number", "desc": "最低价" },
            "max": { "type": "number", "desc": "最高价" }
          }
        }
      }
    }
  },
  "required": ["filter"]
}
```

**数组参数**：

```json
{
  "type": "object",
  "desc": "批量查询参数",
  "props": {
    "store_codes": {
      "type": "array",
      "desc": "门店编码列表",
      "items": {
        "type": "string",
        "desc": "门店编码"
      }
    }
  },
  "required": ["store_codes"]
}
```

### 重要说明

⚠️ 参数定义的质量直接影响 SDK 侧 LLM 构建参数的准确性。建议：

- `desc` 尽量详细，包含格式说明（如 `"月份，格式 yyyyMM"`）
- 使用 `enum` 约束有限取值
- 合理标记 `required` 字段
- `returns` 清晰描述返回内容

---

## 接口 2：工具执行

### 请求

```http
POST /api/execute
Content-Type: application/json
```

**请求体**：

```json
{
  "tool": "query_daily_avg_received_amount",
  "args": {
    "store_code": "S001",
    "month": "202410"
  }
}
```

#### ExecuteRequest

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `tool` | string | ✅ | 工具名称 |
| `args` | object | ✅ | 工具参数 |

**字段详细说明**：

##### `tool`

- 必须与 `/api/search` 返回的 `ToolSchema.name` **完全一致**（区分大小写）
- 服务端收到后应先校验该工具是否存在，不存在时在 `ExecuteResponse` 中返回 `success: false`
- 不要对该字段做 trim 或模糊匹配，SDK 侧保证传入的就是搜索结果中的原始值

##### `args`

- 键值对结构，key 为参数名，value 为参数值
- 参数名和类型应与 `/api/search` 返回的 `ToolSchema.params.props` 定义一致
- **值类型映射**：
  | params 定义中的 type | args 中的 JSON 类型 | 示例 |
  |---------------------|---------------------|------|
  | `string` | string | `"S001"` |
  | `integer` | number（整数） | `42` |
  | `number` | number（含小数） | `3.14` |
  | `boolean` | boolean | `true` |
  | `object` | object | `{"min": 1, "max": 10}` |
  | `array` | array | `["S001", "S002"]` |
- SDK 侧由 LLM 自动构建 `args`，因此服务端应对参数做**校验和容错**：
  - 缺少必填参数时 → 返回 `success: false`，`error` 中说明缺少哪些参数
  - 参数类型不匹配时 → 尽量做合理转换（如字符串 `"42"` → 整数 `42`），转换失败再报错
  - 包含多余参数时 → 建议忽略，不影响执行

**请求体示例**：

简单参数：

```json
{
  "tool": "query_daily_avg_received_amount",
  "args": {
    "store_code": "S001",
    "month": "202410"
  }
}
```

嵌套对象参数：

```json
{
  "tool": "query_sales_report",
  "args": {
    "filter": {
      "category": "food",
      "price_range": { "min": 50, "max": 200 }
    }
  }
}
```

数组参数：

```json
{
  "tool": "batch_query_stores",
  "args": {
    "store_codes": ["S001", "S002", "S003"]
  }
}
```

空参数（工具不需要参数时）：

```json
{
  "tool": "get_all_brands",
  "args": {}
}
```

### 响应

#### ExecuteResponse

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `success` | boolean | `true` | 执行是否成功 |
| `data` | object \| array | `{}` | 执行结果数据 |
| `count` | integer | `0` | 结果数量 |
| `error` | string | `""` | 错误信息 |
| `desc` | string | `""` | 结果描述 |

**字段详细说明**：

##### `success`

- `true`：工具执行成功，结果在 `data` 中
- `false`：工具执行失败，原因在 `error` 中
- ⚠️ **无论成功或失败，HTTP 状态码均为 `200`**。SDK 通过此字段（而非 HTTP 状态码）判断工具执行结果
- 仅在请求层面的错误（认证失败、参数格式错误等）才使用非 200 状态码

##### `data`

- 承载实际业务数据，支持两种 JSON 类型：
  - **object（字典）**：单条结果、聚合统计、键值对形式的结果
  - **array（列表）**：多条记录的结果集
- 成功时 `data` 包含业务数据；失败时 `data` 应为空对象 `{}` 或空列表 `[]`
- **data 内部结构由各工具自行定义**，SDK 不对 data 内部做结构校验，会将其原样传递给调用方
- 建议 data 中的 key 使用 **snake_case** 命名

  **返回字典**（单条 / 聚合）：

  ```json
  {
    "success": true,
    "data": {
      "daily_avg_amount": 12580.50,
      "store_code": "S001",
      "month": "202410"
    },
    "count": 1,
    "error": "",
    "desc": "返回指定门店指定月份的日均实收金额。daily_avg_amount 为日均实收（元），store_code 为门店编码，month 为查询月份。"
  }
  ```

  **返回列表**（多条记录）：

  ```json
  {
    "success": true,
    "data": [
      { "store_code": "S001", "name": "旗舰店", "amount": 12580.50 },
      { "store_code": "S002", "name": "分店A", "amount": 8320.00 }
    ],
    "count": 2,
    "error": "",
    "desc": "返回门店列表及其营收数据。每条记录包含 store_code（门店编码）、name（门店名称）、amount（营收金额，单位：元）。"
  }
  ```

  **复杂业务场景**（多维度数据）：

  ```json
  {
    "success": true,
    "data": {
      "store_code": "S001",
      "store_name": "鮨大山·旗舰店",
      "period": "202410",
      "summary": {
        "total_revenue": 389950.00,
        "daily_avg_revenue": 12580.50,
        "total_orders": 1862,
        "daily_avg_orders": 60,
        "avg_ticket_size": 209.42
      },
      "category_breakdown": [
        { "category": "寿司", "revenue": 195000.00, "ratio": 0.50 },
        { "category": "刺身", "revenue": 97500.00, "ratio": 0.25 },
        { "category": "酒水", "revenue": 58500.00, "ratio": 0.15 },
        { "category": "其他", "revenue": 39000.00, "ratio": 0.10 }
      ],
      "compared_to_last_month": {
        "revenue_change": 0.12,
        "order_change": 0.08
      }
    },
    "count": 1,
    "error": "",
    "desc": "返回指定门店指定月份的经营分析报告。store_code/store_name 为门店信息，period 为统计月份。summary 包含总营收（total_revenue，元）、日均实收（daily_avg_revenue，元）、总订单数（total_orders）、日均订单数（daily_avg_orders）、客单价（avg_ticket_size，元）。category_breakdown 为品类营收明细，每项包含品类名（category）、营收（revenue，元）、占比（ratio，0-1）。compared_to_last_month 为环比数据，revenue_change/order_change 为环比变化率（正数为增长）。"
  }
  ```

##### `count`

- 表示结果数量的语义型字段
- 当 `data` 为列表时，`count` 应等于列表长度
- 当 `data` 为字典时，`count` 由业务语义决定（如查询到 1 条记录则为 `1`，聚合多条数据也可为 `1`）
- 失败时设为 `0`
- SDK 侧可利用此字段快速判断是否有结果，无需解析 `data`

##### `error`

- 成功时必须为空字符串 `""`（不是 `null`）
- 失败时应包含**可机器解析**的错误描述，便于 SDK 侧 LLM 理解并重试
- 建议格式：`"错误类型: 具体说明"`
- 常见错误信息示例：

  | 场景 | error 示例 |
  |------|-----------|
  | 工具不存在 | `"Tool not found: invalid_tool_name"` |
  | 缺少必填参数 | `"Missing required parameter: store_code"` |
  | 参数类型错误 | `"Invalid parameter type: month expects string, got integer"` |
  | 参数值无效 | `"Invalid value for month: '13' is not a valid month"` |
  | 数据源异常 | `"Data source unavailable: database connection timeout"` |
  | 无查询结果 | `"No data found for store_code='S999'"` |

- ⚠️ SDK 的 `make_call` 方法在收到 `success: false` 时会自动重试（默认 3 次）。重试时 LLM 会参考 `error` 内容调整参数，因此 **`error` 信息越具体，重试成功率越高**

##### `desc`

- **对返回结果数据结构的一般性说明**，描述 `data` 中包含哪些字段、各字段的含义和单位
- `desc` 是由工具（接口实现方）预先定义的**固定文案**，不随具体返回值变化，也不需要 LLM 动态生成
- 核心用途：SDK 调用方（通常是 AI Agent）通过 `desc` 理解 `data` 的结构含义，从而正确解读和使用数据
- 失败时可为空字符串 `""`
- 非必需字段，可留空；但提供清晰的 `desc` 能帮助调用方准确理解返回数据

  **撰写要求**：

  - 说明 `data` 中包含哪些字段及其含义
  - 标注关键字段的单位（如「元」「%」「0-1」等）
  - 如有嵌套结构，逐层说明
  - **不要带入具体的结果数值**，只描述数据的结构和语义

  **示例对比**：

  | | desc 内容 |
  |------|-----------|
  | ✅ 正确 | `"返回指定门店指定月份的日均实收金额。daily_avg_amount 为日均实收（元），store_code 为门店编码，month 为查询月份。"` |
  | ❌ 错误 | `"门店 S001 在 2024年10月的日均实收金额为 12,580.50 元"` |

  **不同工具的 desc 示例**：

  | 工具类型 | desc 示例 |
  |----------|-----------|
  | 单值查询 | `"返回指定门店指定月份的日均实收金额。daily_avg_amount 为日均实收（元）。"` |
  | 列表查询 | `"返回门店列表及其营收数据。每条记录包含 store_code（门店编码）、name（门店名称）、amount（营收金额，元）。"` |
  | 聚合报告 | `"返回门店经营分析报告。summary 包含总营收、日均实收、订单数等（单位：元）；category_breakdown 为品类明细（含占比 0-1）；compared_to_last_month 为环比变化率。"` |
  | 品类枚举 | `"返回所有品牌和品类列表。每条记录包含 brand_code（品牌编码）、brand_name（品牌名称）、categories（品类列表）。"` |

### 失败响应示例

**工具不存在**：

```json
{
  "success": false,
  "data": {},
  "count": 0,
  "error": "Tool not found: invalid_tool_name",
  "desc": ""
}
```

**缺少必填参数**：

```json
{
  "success": false,
  "data": {},
  "count": 0,
  "error": "Missing required parameter: store_code",
  "desc": ""
}
```

**查询无结果**（这是成功执行，只是没数据）：

```json
{
  "success": true,
  "data": [],
  "count": 0,
  "error": "",
  "desc": "返回门店列表及其营收数据。每条记录包含 store_code（门店编码）、name（门店名称）、amount（营收金额，元）。"
}
```

### 注意事项

1. **成功与失败的界定**：工具正常执行但返回空结果，应视为 `success: true`（只是 `count` 为 0）。`success: false` 仅用于工具本身无法执行的情况（找不到工具、参数非法、数据源异常等）
2. **重试友好**：`error` 字段是 SDK 自动重试的关键依据，请确保错误信息准确且具有指导性
3. **幂等性**：同一 `tool` + `args` 的请求应返回一致的结果（查询类工具），或至少不产生副作用
4. **超时处理**：工具执行耗时较长时，建议服务端设置合理的超时时间，超时后返回 `success: false` 并在 `error` 中说明

---

## 错误处理

### HTTP 状态码

| 状态码 | 场景 |
|--------|------|
| `200` | 请求成功（包括工具执行失败，通过 `success: false` 标识） |
| `400` | 请求参数错误（缺少必填参数、格式错误） |
| `401` | 认证失败（API Key 无效、JWT 签名错误或过期） |
| `403` | 权限不足 |
| `404` | 接口路径不存在 |
| `500` | 服务端内部错误 |

### 错误响应格式（建议）

非 200 的错误响应建议返回统一格式：

```json
{
  "error": "错误码或类型",
  "message": "人类可读的错误描述"
}
```

### 工具执行失败 vs HTTP 错误

- **工具执行失败**：HTTP 状态码仍为 `200`，通过 `ExecuteResponse.success = false` 和 `error` 字段描述失败原因
- **HTTP 错误**：请求层面的错误（认证失败、参数缺失等），使用标准 HTTP 状态码

---

## 完整请求示例

### 工具搜索

```http
GET /api/search?query=查询日均实收金额&top_k=3 HTTP/1.1
Host: datahub.example.com
Content-Type: application/json
Authorization: Bearer your-api-key
X-META-INFO: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
X-APP-NAME: my-app
```

### 工具执行

```http
POST /api/execute HTTP/1.1
Host: datahub.example.com
Content-Type: application/json
Authorization: Bearer your-api-key
X-META-INFO: eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...
X-APP-NAME: my-app

{
  "tool": "query_daily_avg_received_amount",
  "args": {
    "store_code": "S001",
    "month": "202410"
  }
}
```

---

## 实现检查清单

实现 DataHub API 接口时，确认以下事项：

- [ ] 实现 `GET /api/search` 接口，支持 `query` 和 `top_k` 参数
- [ ] 实现 `POST /api/execute` 接口，支持 `ExecuteRequest` 请求体
- [ ] 响应数据结构严格符合 `SearchResponse` 和 `ExecuteResponse` 模型
- [ ] 工具的 `params` 定义完整且 `desc` 清晰（直接影响 AI 参数构建质量）
- [ ] 支持 `Authorization: Bearer <api_key>` 认证
- [ ] 支持 `X-META-INFO` JWT 认证（Ed25519 签名验证）
- [ ] 工具执行失败时返回 `200` + `success: false`，而非 HTTP 错误码
- [ ] 所有响应 `Content-Type` 为 `application/json`

---

## 相关文档

- **[认证方式详解](authentication.md)** — API Key 和 MetaInfo JWT 的完整说明（含密钥生成、SDK 用法）
- **[API 参考](api_reference.md)** — DataHub SDK Python 类和方法文档
- **[基本用法](basic_usage.md)** — SDK 调用示例
