# zm-rag 第三方系统对接 API 文档

> **Base URL**: `http://<host>:8900`
> **协议**: HTTP/HTTPS &nbsp;|&nbsp; **数据格式**: JSON &nbsp;|&nbsp; **字符编码**: UTF-8

---

## 目录

1. [认证说明](#1-认证说明)
2. [三方系统登录（SSO 令牌交换）](#2-三方系统登录sso-令牌交换)
3. [文档入库](#3-文档入库)
   - 3.1 [提交文档入库](#31-提交文档入库)
   - 3.2 [入库任务状态查询](#32-入库任务状态查询)
4. [ES 混合检索](#4-es-混合检索)
5. [QA 智能问答](#5-qa-智能问答)
   - 5.1 [QA 流式问答](#51-qa-流式问答)
   - 5.2 [QA 材料检索](#52-qa-材料检索)
6. [文档详情](#6-文档详情)
7. [关联文件管理](#7-关联文件管理)
8. [服务状态查询](#8-服务状态查询)
9. [附录：错误码说明](#9-附录错误码说明)

---

## 1. 认证说明

除 **SSO 登录** 和 **健康检查** 外，所有接口均需在请求头中携带 JWT Bearer Token：

```
Authorization: Bearer <access_token>
```

Token 通过 [SSO 登录接口](#2-三方系统登录sso-令牌交换) 获取，有效期 **24 小时**。

**JWT Payload 结构**：

| Claim | 类型 | 说明 |
|-------|------|------|
| `sub` | string | 用户唯一标识，已添加 `U_` 前缀（如 `U_1001`） |
| `user_name` | string | 用户姓名 |
| `office_id` | string | 科室 ID，已添加 `O_` 前缀（如 `O_17`） |
| `office_name` | string | 科室名称 |
| `dept_id` | string | 部门 ID，已添加 `D_` 前缀（如 `D_5`） |
| `dept_name` | string | 部门名称 |
| `area_id` | string | 地区 ID，已添加 `A_` 前缀（如 `A_1`） |
| `area_name` | string | 地区名称 |
| `role_ids` | string[] | 角色 ID 列表，已添加 `R_` 前缀（如 `["R_3"]`） |
| `iat` | int | 签发时间 (Unix timestamp) |
| `exp` | int | 过期时间 (Unix timestamp) |
| `iss` | string | 签发者，值为 `"zm-rag"` |

系统根据 JWT 中的 `office_id`、`dept_id`、`area_id`、`role_ids` 自动构建 ACL 权限令牌，用于文档检索和问答时的权限过滤。用户只能访问其权限范围内的文档。`user_name`、`dept_name`、`office_name`、`area_name` 等名称字段用于前端展示，不参与权限计算。

---

## 2. 三方系统登录（SSO 令牌交换）

三方系统将已认证的用户信息传入，换取本系统的 JWT Token。

```
POST /api/v1/auth/sso
Content-Type: application/json
```

**无需认证** — 此接口本身用于获取 Token。

### 请求体

| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `user_id` | string | **是** | 用户唯一标识，传入原始 ID 即可（如 `"1001"`），服务端自动添加 `U_` 前缀 |
| `user_name` | string | 否 | 用户姓名，如 `"张三"` |
| `office_id` | string | 否 | 科室 ID，传入原始 ID（如 `"17"`），服务端自动添加 `O_` 前缀 |
| `office_name` | string | 否 | 科室名称，如 `"财务科"` |
| `dept_id` | string | 否 | 部门 ID，传入原始 ID（如 `"5"`），服务端自动添加 `D_` 前缀 |
| `dept_name` | string | 否 | 部门名称，如 `"市财政局"` |
| `area_id` | string | 否 | 地区 ID，传入原始 ID（如 `"1"`），服务端自动添加 `A_` 前缀 |
| `area_name` | string | 否 | 地区名称，如 `"市本级"` |
| `role_ids` | string[] | 否 | 角色 ID 列表，传入原始 ID（如 `["1", "3"]`），服务端自动添加 `R_` 前缀 |

> **ID 自动前缀**：三方系统通常以纯数字存储各类 ID，本接口会自动添加类型前缀以匹配内部 ACL 权限体系：
>
> | 原始 ID | 前缀 | 转换结果 |
> |---------|------|----------|
> | `user_id: "1001"` | `U_` | `U_1001` |
> | `office_id: "17"` | `O_` | `O_17` |
> | `dept_id: "5"` | `D_` | `D_5` |
> | `area_id: "1"` | `A_` | `A_1` |
> | `role_ids: ["3"]` | `R_` | `["R_3"]` |
>
> 如果传入的 ID 已包含正确前缀（如 `"U_1001"`），不会重复添加。

### 请求示例

```json
{
  "user_id": "1001",
  "user_name": "张三",
  "office_id": "17",
  "office_name": "财务科",
  "dept_id": "5",
  "dept_name": "市财政局",
  "area_id": "1",
  "area_name": "市本级",
  "role_ids": ["3"]
}
```

### 响应体

| 字段 | 类型 | 说明 |
|------|------|------|
| `access_token` | string | JWT 令牌 |
| `token_type` | string | 固定值 `"bearer"` |
| `expires_in` | int | 有效时长（秒），默认 86400（24小时） |
| `user_id` | string | 用户 ID（已添加 `U_` 前缀） |

### 响应示例

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 86400,
  "user_id": "U_1001"
}
```

### 调用流程

```
┌──────────┐         ┌──────────┐         ┌──────────┐
│  三方系统  │──POST──▶│ zm-rag   │──签发──▶│   JWT    │
│          │  用户信息 │ /auth/sso│  Token  │          │
└──────────┘         └──────────┘         └────┬─────┘
                                               │
     前端保存 token，后续请求携带 Authorization  │
     ◀─────────────────────────────────────────┘
```

---

## 3. 文档入库

### 3.1 提交文档入库

将文档提交到入库队列进行异步处理（解析、切片、向量化、知识图谱构建）。

```
POST /api/v1/ingest/trigger
Content-Type: application/json
Authorization: Bearer <token>
```

#### 请求体

| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `doc_id` | string | **是** | 文档唯一标识 |
| `file_path` | string | 否 | 文件路径（相对于服务器存储根目录） |
| `metadata` | object | 否 | 附加元数据，详见下表 |

**metadata 可选字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `title` | string | 文档标题 |
| `doc_number` | string | 文号，如 `"国发〔2024〕1号"` |
| `issuing_org` | string | 发文机关 |
| `doc_type` | string | 公文种类（通知、通报、决定等） |
| `subject_words` | string[] | 主题词列表 |
| `signer` | string | 签发人 |
| `publish_date` | string | 发布日期，格式 `YYYY-MM-DD` |
| `acl_ids` | string[] | 访问控制列表，如 `["O_17", "D_05"]` |
| `knowledge_category` | string | 知识分类 |
| `operator` | string | 操作人 |
| `related_docs` | array | 关联文档列表，每项包含 `doc_id`（关联文档ID）、`title`（关联文档标题）、`relation_type`（关系类型：`"正文"` 或 `"附件"`） |

#### 请求示例

```json
{
  "doc_id": "DOC_20240301_001",
  "file_path": "2024/03/notice_001.pdf",
  "metadata": {
    "title": "关于做好2024年度工作的通知",
    "doc_number": "办发〔2024〕1号",
    "issuing_org": "综合办公室",
    "doc_type": "通知",
    "publish_date": "2024-03-01",
    "acl_ids": ["O_17", "D_05"],
    "knowledge_category": "行政管理"
  }
}
```

#### 响应体

| 字段 | 类型 | 说明 |
|------|------|------|
| `task_id` | string | 异步任务 ID，用于后续状态查询 |
| `status` | string | 初始状态 `"PENDING"` |
| `progress` | float | 进度，初始值 `0.0` |
| `error` | string\|null | 错误信息，初始为 `null` |
| `deduplicated` | bool | 是否走了去重快速路径 |
| `content_hash` | string\|null | 内容哈希，完成后填充 |

#### 响应示例

```json
{
  "task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "PENDING",
  "progress": 0.0,
  "error": null,
  "deduplicated": false,
  "content_hash": null
}
```

---

### 3.2 入库任务状态查询

轮询异步入库任务的处理进度。

```
GET /api/v1/ingest/status/{task_id}
Authorization: Bearer <token>
```

#### 路径参数

| 参数 | 类型 | 说明 |
|------|------|------|
| `task_id` | string | 提交入库时返回的任务 ID |

#### 响应体

| 字段 | 类型 | 说明 |
|------|------|------|
| `task_id` | string | 任务 ID |
| `status` | string | 任务状态（见下表） |
| `progress` | float | 进度 0.0 ~ 1.0 |
| `error` | string\|null | 错误信息（失败时） |
| `deduplicated` | bool | 是否去重处理 |
| `content_hash` | string\|null | 文档内容哈希（完成时） |

#### 任务状态流转

| 状态 | 说明 |
|------|------|
| `PENDING` | 已加入队列，等待处理 |
| `PROCESSING` | 正在处理（解析/切片/向量化/图谱构建） |
| `COMPLETED` | 处理完成 |
| `PARTIAL_FAILED` | 部分步骤失败（如图谱构建失败但文档已入库） |
| `FAILED` | 处理失败 |
| `REVOKED` | 任务已取消 |

```
PENDING ──▶ PROCESSING ──▶ COMPLETED
                │
                ├──▶ PARTIAL_FAILED
                │
                └──▶ FAILED
```

#### 响应示例（处理中）

```json
{
  "task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "PROCESSING",
  "progress": 0.65,
  "error": null,
  "deduplicated": false,
  "content_hash": null
}
```

#### 响应示例（完成）

```json
{
  "task_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "COMPLETED",
  "progress": 1.0,
  "error": null,
  "deduplicated": false,
  "content_hash": "d41d8cd98f00b204e9800998ecf8427e"
}
```

> **轮询建议**：建议每 2~5 秒查询一次状态，直到 `status` 变为 `COMPLETED`、`PARTIAL_FAILED` 或 `FAILED`。

---

## 4. ES 混合检索

基于 BM25 文本匹配 + kNN 向量语义检索的混合搜索，采用 RRF（Reciprocal Rank Fusion）融合排序，自动按用户权限过滤文档。

```
POST /api/v1/search
Content-Type: application/json
Authorization: Bearer <token>
```

### 请求体

| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `query` | string | **是** | 搜索关键词或自然语言问句（1~500字符） |
| `filters` | object | 否 | 筛选条件，详见下表 |
| `page` | int | 否 | 页码，默认 `1`，最小 `1` |
| `page_size` | int | 否 | 每页条数，默认 `20`，范围 `1~100` |

**filters 可选字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `issuing_org` | string \| string[] | 发文机关 |
| `doc_type` | string \| string[] | 公文种类 |
| `knowledge_category` | string \| string[] | 知识分类 |
| `document_scene_type` | string | 文档场景类型 |
| `signer` | string | 签发人 |
| `publish_year` | int | 发布年份（1900~2100） |
| `doc_number` | string | 文号 |
| `date_from` | string | 起始日期，格式 `YYYY-MM-DD` |
| `date_to` | string | 截止日期，格式 `YYYY-MM-DD` |
| `subject_words` | string[] | 主题词 |
| `search_scope` | string | 检索范围：`"all"` \| `"title"` \| `"content"` \| `"doc_number"`，默认 `"all"` |

### 请求示例

```json
{
  "query": "年度预算编制要求",
  "filters": {
    "doc_type": "通知",
    "date_from": "2024-01-01",
    "search_scope": "all"
  },
  "page": 1,
  "page_size": 10
}
```

### 响应体

| 字段 | 类型 | 说明 |
|------|------|------|
| `total` | int | 匹配文档总数 |
| `page` | int | 当前页码 |
| `page_size` | int | 每页条数 |
| `documents` | array | 文档列表，详见下表 |
| `aggregations` | object | 聚合统计信息 |

**documents[] 字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `doc_id` | string | 文档 ID |
| `content_hash` | string | 内容哈希（相同内容共享） |
| `version_count` | int | 同内容不同版本数量 |
| `title` | string | 文档标题 |
| `doc_number` | string\|null | 文号 |
| `knowledge_category` | string\|null | 知识分类 |
| `issuing_org` | string\|null | 发文机关 |
| `doc_type` | string\|null | 公文种类 |
| `publish_date` | string\|null | 发布日期 |
| `score` | float\|null | 综合相关性得分 |
| `bm25_score` | float\|null | BM25 文本匹配得分 |
| `knn_score` | float\|null | kNN 向量相似度得分 |
| `subject_words` | string[] | 主题词列表 |
| `highlights` | string[] | 高亮文本片段（含 `<em>` 标签） |
| `matched_chunks` | array | 命中的文档块详情，详见下表 |

**matched_chunks[] 字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `text` | string | 高亮摘要文本（含 `<em>` 标签） |
| `page_number` | int\|null | 所在页码 |
| `page_numbers` | int[] | 跨页页码列表 |
| `heading_hierarchy` | string[] | 段落层级标题（如 `["4. 推进社会治理应用"]`） |
| `element_type` | string | 元素类型，如 `"text"`、`"table"`、`"list"` 等 |
| `chunk_index` | int\|null | 块序号 |

### 响应示例

```json
{
  "total": 42,
  "page": 1,
  "page_size": 10,
  "documents": [
    {
      "doc_id": "DOC_20240301_001",
      "content_hash": "d41d8cd98f00b204e980",
      "version_count": 1,
      "title": "关于做好2024年度预算编制工作的通知",
      "doc_number": "财预〔2024〕15号",
      "knowledge_category": "财务管理",
      "issuing_org": "财务科",
      "doc_type": "通知",
      "publish_date": "2024-03-01",
      "score": 15.72,
      "bm25_score": 12.34,
      "knn_score": 0.89,
      "subject_words": ["预算", "编制"],
      "highlights": [
        "关于做好2024年度<em>预算编制</em>工作的通知",
        "各单位应按照<em>预算编制要求</em>，认真组织..."
      ],
      "matched_chunks": [
        {
          "text": "各单位应按照<em>预算编制要求</em>，认真组织...",
          "page_number": 2,
          "page_numbers": [],
          "heading_hierarchy": ["二、编制要求"],
          "element_type": "text",
          "chunk_index": 5
        }
      ]
    }
  ],
  "aggregations": {}
}
```

---

## 5. QA 智能问答

提供两种模式：**流式问答**（LLM 生成回答）和**材料检索**（仅返回检索到的文档材料）。两者共享相同的检索流水线（关键词提取 → 图谱规划 → 多路召回 → 合并），区别在于材料检索跳过 LLM 生成，直接返回结构化 JSON。

### 5.1 QA 流式问答

基于 RAG（检索增强生成）的智能问答接口。系统自动检索相关文档，由 LLM 生成回答，以 **SSE（Server-Sent Events）流式** 返回结果。

```
POST /api/v1/qa
Content-Type: application/json
Authorization: Bearer <token>
```

#### 请求体

| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `question` | string | **是** | 问题内容（1~2000字符） |
| `session_id` | string\|null | 否 | 会话 ID，支持多轮对话上下文 |
| `seed_doc_ids` | string[]\|null | 否 | 指定参考文档 ID 列表 |

#### 请求示例

```json
{
  "question": "差旅费报销标准是多少？",
  "session_id": null,
  "seed_doc_ids": null
}
```

#### 响应格式

**Content-Type**: `text/event-stream`

响应为 SSE 流，每个事件帧包含一个 JSON 对象：

```
data: {"type":"thinking","content":"正在检索相关文档...","stage":"retrieval"}

data: {"type":"text","content":"根据相关文件规定，差旅费报销标准如下：..."}

data: {"type":"reference","doc_id":"DOC_001","title":"差旅费管理办法","doc_number":"财行〔2024〕3号","relevance_score":0.92}

data: {"type":"done","content":null}
```

#### 事件类型说明

| type | 说明 | 关键字段 |
|------|------|----------|
| `thinking` | 中间推理过程 | `content`（推理文本）、`stage`（阶段名） |
| `text` | 回答正文（增量输出） | `content`（文本片段） |
| `reference` | 引用的参考文档 | `doc_id`、`title`、`doc_number`、`relevance_score` |
| `progress` | 处理进度 | `content`（进度说明）、`status` |
| `error` | 错误信息 | `content`（错误描述）、`severity` |
| `done` | 流结束标志 | — |

#### 前端消费示例（JavaScript）

```javascript
const response = await fetch('/api/v1/qa', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`
  },
  body: JSON.stringify({ question: '差旅费报销标准是多少？' })
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const text = decoder.decode(value);
  for (const line of text.split('\n')) {
    if (!line.startsWith('data: ')) continue;
    const chunk = JSON.parse(line.slice(6));

    switch (chunk.type) {
      case 'text':
        // 追加回答文本
        appendAnswer(chunk.content);
        break;
      case 'reference':
        // 收集引用文档
        addReference(chunk);
        break;
      case 'done':
        // 流结束
        break;
    }
  }
}
```

---

### 5.2 QA 材料检索

执行与 QA 问答**完全相同的检索流水线**（关键词提取 → 图谱规划 → ES 混合检索 → 图谱补充 → 办事指南 → 合并去重），但**不调用 LLM 生成回答**，直接返回结构化 JSON 结果。

适用场景：三方系统获取检索材料后自行处理（如自定义 Prompt 调用自有 LLM、展示原文等）。

```
POST /api/v1/qa/search
Content-Type: application/json
Authorization: Bearer <token>
```

#### 请求体

| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `question` | string | **是** | 问题内容（1~2000字符） |
| `seed_doc_ids` | string[]\|null | 否 | 指定参考文档 ID 列表 |

#### 请求示例

```json
{
  "question": "差旅费报销标准是多少？",
  "seed_doc_ids": null
}
```

#### 响应体

| 字段 | 类型 | 说明 |
|------|------|------|
| `question` | string | 原始问题（回显） |
| `keywords` | string[] | LLM 提取的关键词 |
| `intent` | string | 图谱意图分类（见下表） |
| `total` | int | 命中文档总数 |
| `documents` | array | 检索到的文档列表，详见下表 |
| `context_text` | string | 格式化的参考材料文本（可直接用于 LLM 输入） |
| `graph_evidence_text` | string | 图谱结构化证据文本（无则为空字符串） |
| `guide_evidence_text` | string | 办事指南证据文本（无则为空字符串） |

**documents[] 字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `doc_id` | string | 文档 ID |
| `title` | string | 文档标题 |
| `doc_number` | string\|null | 文号 |
| `issuing_org` | string\|null | 发文机关 |
| `doc_type` | string\|null | 公文种类 |
| `publish_date` | string\|null | 发布日期 |
| `score` | float\|null | 相关性得分 |
| `passages` | string[] | 匹配的文本摘录（已去除高亮标签） |
| `source_type` | string | 来源类型：`"search"` \| `"seed"` \| `"graph"` \| `"guide"` |
| `source_label` | string\|null | 来源中文标签（混合检索命中 / 图谱补充材料 等） |
| `profile_id` | string\|null | 办事指南 profile ID（仅 guide 类型） |
| `matched_fields` | string[]\|null | 匹配的指南字段（仅 guide 类型） |
| `matched_field_labels` | string[]\|null | 匹配字段中文标签（仅 guide 类型） |
| `summary` | string\|null | 办事指南摘要（仅 guide 类型） |

**intent 取值说明**：

| 值 | 说明 |
|------|------|
| `GENERAL` | 通用问答 |
| `POLICY_CHAIN` | 政策链条追溯（依据、上位法） |
| `REVISION_HISTORY` | 文件修订历史 |
| `ENTITY_DOCS` | 某机构发布的文件 |
| `MATTER_DETAIL` | 办事指南详情（办理、申请、材料） |
| `THEME_EXPLORE` | 主题探索（相关政策） |

#### 响应示例

```json
{
  "question": "差旅费报销标准是多少？",
  "keywords": ["差旅费", "报销", "标准", "财务"],
  "intent": "GENERAL",
  "total": 3,
  "documents": [
    {
      "doc_id": "DOC_20240301_001",
      "title": "关于差旅费管理的通知",
      "doc_number": "财行〔2024〕3号",
      "issuing_org": "财务科",
      "doc_type": "通知",
      "publish_date": "2024-03-01",
      "score": 15.72,
      "passages": [
        "出差人员应当按照规定等级乘坐交通工具……",
        "住宿费限额标准按照地区分类执行……"
      ],
      "source_type": "search",
      "source_label": "混合检索命中",
      "profile_id": null,
      "matched_fields": null,
      "matched_field_labels": null,
      "summary": null
    },
    {
      "doc_id": "DOC_20240215_008",
      "title": "关于调整差旅住宿费标准的通知",
      "doc_number": "财行〔2024〕7号",
      "issuing_org": "财务科",
      "doc_type": "通知",
      "publish_date": "2024-02-15",
      "score": 12.35,
      "passages": [
        "住宿费标准调整为：一类地区每人每天不超过500元……"
      ],
      "source_type": "search",
      "source_label": "混合检索命中",
      "profile_id": null,
      "matched_fields": null,
      "matched_field_labels": null,
      "summary": null
    }
  ],
  "context_text": "[1] 标题：《关于差旅费管理的通知》\n     文号：财行〔2024〕3号\n     发文机关：财务科\n     时间：2024-03-01\n     相关内容摘录：\n     ……出差人员应当按照规定等级乘坐交通工具……\n     ……住宿费限额标准按照地区分类执行……\n\n[2] 标题：《关于调整差旅住宿费标准的通知》\n     文号：财行〔2024〕7号\n     ...",
  "graph_evidence_text": "",
  "guide_evidence_text": ""
}
```

#### 与 QA 流式问答的流程对比

| 处理阶段 | QA 问答 (`/qa`) | QA 材料检索 (`/qa/search`) |
|---------|:-:|:-:|
| JWT 认证 + 权限解析 | ✅ | ✅ |
| LLM 关键词提取 | ✅ | ✅ |
| 图谱意图识别 + 证据收集 | ✅ | ✅ |
| ES 混合检索 (BM25 + kNN) | ✅ | ✅ |
| 种子文档 / 图谱文档补充 | ✅ | ✅ |
| 办事指南证据收集 | ✅ | ✅ |
| 多源文档合并去重 | ✅ | ✅ |
| 参考材料文本格式化 | ✅ | ✅ |
| LLM 生成回答 | ✅ | ❌ 跳过 |
| SSE 流式输出 | ✅ | ❌ JSON 返回 |

> **`context_text` 字段说明**：该字段与 QA 问答内部传给 LLM 的参考材料文本完全一致，三方系统可直接将其嵌入自定义 Prompt 中调用自有 LLM 生成回答。

---

## 6. 文档详情

获取单个文档的完整元数据信息，包括知识分类和关联文件。

```
GET /api/v1/document/{doc_id}
Authorization: Bearer <token>
```

### 路径参数

| 参数 | 类型 | 说明 |
|------|------|------|
| `doc_id` | string | 文档唯一标识 |

### 响应体

| 字段 | 类型 | 说明 |
|------|------|------|
| `doc_id` | string | 文档 ID |
| `content_hash` | string | 内容哈希 |
| `title` | string | 文档标题 |
| `doc_number` | string\|null | 文号 |
| `issuing_org` | string\|null | 发文机关 |
| `doc_type` | string\|null | 公文种类 |
| `knowledge_category` | string\|null | 知识分类（上传时手动指定） |
| `subject_words` | string[] | 主题词列表 |
| `signer` | string\|null | 签发人 |
| `publish_date` | string\|null | 发布日期 |
| `summary` | string\|null | LLM 生成的摘要 |
| `chunk_count` | int | 分块数量 |
| `page_count` | int | 页数 |
| `file_type` | string\|null | 文件类型（pdf、docx 等） |
| `related_docs` | array | 关联文件列表，每项包含 `doc_id`、`title`、`relation_type` |
| `created_at` | string\|null | 创建时间 |
| `updated_at` | string\|null | 更新时间 |

### 响应示例

```json
{
  "doc_id": "DOC_20240301_001",
  "content_hash": "d41d8cd98f00b204e980",
  "title": "关于做好2024年度预算编制工作的通知",
  "doc_number": "财预〔2024〕15号",
  "issuing_org": "财务科",
  "doc_type": "通知",
  "knowledge_category": "财务管理",
  "subject_words": ["预算", "编制"],
  "signer": "张三",
  "publish_date": "2024-03-01",
  "summary": "本通知要求各单位按照规定做好2024年度预算编制工作...",
  "chunk_count": 12,
  "page_count": 5,
  "file_type": "pdf",
  "related_docs": [
    {
      "doc_id": "DOC_20240301_002",
      "title": "2024年度预算编制说明（附件）",
      "relation_type": "附件"
    }
  ],
  "created_at": "2024-03-01T10:00:00",
  "updated_at": "2024-03-01T10:05:00"
}
```

---

## 7. 关联文件管理

声明式全量替换文档的关联文件列表。调用方发送当前文档**最终完整**的关联列表，后端自动计算增删改差异并维护双向关系。

**双向同步规则**：
- 新增关联 → 在目标文档追加反向关联（关系类型取反：正文 ↔ 附件）
- 删除关联 → 同步删除目标文档的反向关联
- 变更关联 → 同步更新目标文档的反向关系类型

**校验规则**：自引用拒绝、重复条目去重、不存在的目标文档过滤。同步为尽力处理，不影响主文档更新。

```
PUT /api/v1/document/{doc_id}/related-docs
Content-Type: application/json
Authorization: Bearer <token>
```

### 路径参数

| 参数 | 类型 | 说明 |
|------|------|------|
| `doc_id` | string | 文档唯一标识 |

### 请求体

| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `related_docs` | array | **是** | 当前文档最终完整的关联文档列表，详见下表 |

**related_docs[] 字段**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `doc_id` | string | **是** | 目标文档ID |
| `title` | string | **是** | 目标文档标题 |
| `relation_type` | string | **是** | 关系类型：`"正文"` 或 `"附件"` |

### 请求示例

```json
{
  "related_docs": [
    {
      "doc_id": "DOC_20240301_002",
      "title": "关于做好2024年度工作的通知（附件）",
      "relation_type": "附件"
    }
  ]
}
```

### 响应体

| 字段 | 类型 | 说明 |
|------|------|------|
| `doc_id` | string | 当前文档ID |
| `related_docs` | array | 更新后的关联文档列表（结构同请求体） |
| `affected_doc_ids` | string[] | 实际成功写入的目标文档 ID 列表 |
| `warnings` | array | 结构化警告列表，详见下表 |

**warnings[] 字段**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `doc_id` | string | 相关文档 ID |
| `code` | string | 警告码（见下方说明） |
| `reason` | string | 中文描述 |

**警告码说明**：

| 警告码 | 含义 |
|--------|------|
| `SELF_REFERENCE` | 关联列表中包含自身文档，已自动忽略 |
| `DUPLICATE` | 关联列表中存在重复条目，已自动去重 |
| `TARGET_NOT_FOUND` | 目标文档在 ES 中不存在，已跳过 |
| `TARGET_UPDATE_FAILED` | 目标文档反向关联写入失败 |
| `SYNC_EXCEPTION` | 双向同步过程中出现异常 |

### 响应示例

```json
{
  "doc_id": "DOC_20240301_001",
  "related_docs": [
    {
      "doc_id": "DOC_20240301_002",
      "title": "关于做好2024年度工作的通知（附件）",
      "relation_type": "附件"
    }
  ],
  "affected_doc_ids": ["DOC_20240301_002"],
  "warnings": []
}
```

### 响应示例（含警告）

```json
{
  "doc_id": "DOC_20240301_001",
  "related_docs": [
    {
      "doc_id": "DOC_20240301_002",
      "title": "关于做好2024年度工作的通知（附件）",
      "relation_type": "附件"
    }
  ],
  "affected_doc_ids": ["DOC_20240301_002"],
  "warnings": [
    {
      "doc_id": "DOC_20240301_001",
      "code": "SELF_REFERENCE",
      "reason": "不能关联自身文档"
    },
    {
      "doc_id": "DOC_NONEXIST",
      "code": "TARGET_NOT_FOUND",
      "reason": "目标文档不存在"
    }
  ]
}
```

---

## 8. 服务状态查询

轻量级健康检查接口，用于监控服务存活状态。

```
GET /health
```

**无需认证**。

### 响应体

| 字段 | 类型 | 说明 |
|------|------|------|
| `status` | string | 服务状态，正常为 `"ok"` |
| `app` | string | 应用名称 `"zm-rag"` |
| `version` | string | 应用版本号 |

### 响应示例

```json
{
  "status": "ok",
  "app": "zm-rag",
  "version": "0.1.0"
}
```

> **注意**：此接口位于根路径 `/health`，不在 `/api/v1` 前缀下。

---

## 9. 附录：错误码说明

所有接口统一使用 HTTP 状态码 + JSON 错误体：

```json
{
  "detail": "错误描述信息"
}
```

| HTTP 状态码 | 含义 | 常见场景 |
|:-----------:|------|----------|
| **400** | 请求参数错误 | 缺少必填字段、字段格式不合法 |
| **401** | 认证失败 | Token 缺失、过期或无效 |
| **403** | 权限不足 | 无权访问目标资源 |
| **404** | 资源不存在 | 文档 ID 或任务 ID 不存在 |
| **422** | 请求体校验失败 | Pydantic 字段校验不通过 |
| **429** | 请求频率超限 | 触发限流（搜索 60次/分钟，问答 10次/分钟） |
| **500** | 服务器内部错误 | 服务异常 |

### 限流策略

| 接口类别 | 限制 |
|---------|------|
| 搜索类 (`/search`) | 60 次/分钟 |
| 问答类 (`/qa`) | 10 次/分钟 |
| 其他接口 | 200 次/分钟 |

限流按用户维度（JWT `sub` claim）计算，未认证请求按客户端 IP 计算。
