# Doco 开放 API v1

Doco 开放 API 面向 Agent、脚本和第三方应用，用于管理知识库、文件夹、文档正文、块、附件、关系、概念、摘要和多语言文档。

- API 根地址：`{DOCO_ORIGIN}/api/v1`
- 鉴权方式：`Authorization: Bearer <API_TOKEN>`
- OpenAPI 3.1 规范：`{DOCO_ORIGIN}/api/openapi.json`
- 本文 Markdown 原文：`{DOCO_ORIGIN}/api-docs.md`

> 将 `{DOCO_ORIGIN}` 替换为 Doco 实例的**后端入口**地址；生产环境使用 `https://api.doco.showme.talk`。
> 注意：API 只在把 `/api/*` 反代到后端的入口上可用。托管在 Cloudflare Pages 等静态平台上的
> 前端页面域名（如 `doco.showme.talk`）对所有路径返回 SPA 的 HTML 回退页，**不能**当 API 地址用。
> 判别方法：`GET {DOCO_ORIGIN}/api/v1/me` 返回 JSON（`401` 或带 `data`）即正确；返回 `<!doctype html>`
> 说明打到了静态前端，请换用后端入口域名（见部署文档的 DNS 与 Caddy 章节）。
> 开放 API 使用 Bearer Token；浏览器页面使用的 Session Cookie 不能调用 `/api/v1/*`。

## 快速开始

### 0. Agent 一条命令接入（推荐）

给 Claude Code / Cursor 等 Agent 接入 Doco，不需要手动复制 Token：

```bash
# 安装 CLI 并登录（浏览器设备授权，像 GitHub CLI 一样）
npm i -g doco-agent-cli
doco login

# 注册 MCP server，Agent 即获得 29 个文档读写、结构检索、关系遍历、分层摘要、显式概念、多语言与变更感知工具
claude mcp add doco -- npx -y --package doco-agent-cli doco mcp
```

Cursor：在 MCP 设置粘贴 `{ "doco": { "command": "npx", "args": ["-y", "--package", "doco-agent-cli", "doco", "mcp"] } }`。
终端与脚本场景直接用 CLI（`doco docs|blocks|edit`，全局 `--json`）或裸 REST。
`doco login` 走下文「设备授权流程」，默认签发 `read_write`，网页确认时可降级为只读。

### 1. 创建 Token

登录 Doco 后，在右上角账户菜单中打开「API 管理」，创建只读或读写 Token。完整 Token 只显示一次，请勿提交到代码仓库或日志。

### 2. 验证身份

```bash
export DOCO_BASE_URL="https://api.doco.showme.talk"
export DOCO_API_TOKEN="doco_tok_xxx_secret"

curl --fail-with-body \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  "$DOCO_BASE_URL/api/v1/me"
```

成功响应统一包含 `data` 和 `request_id`：

```json
{
  "data": {
    "user": {
      "id": "user_123",
      "email": "agent@example.com",
      "name": "Agent User"
    },
    "scopes": ["documents:read", "documents:write"],
    "token_id": "tok_01..."
  },
  "request_id": "req_01..."
}
```

### 3. 创建知识库和文档

### 多语言文档

普通文档默认保持单语言；人工激活后才创建 `doco://docset/{id}`。每个语言版本拥有独立 `doc_*`、YDoc、稳定块 ID 和 ETag：

```bash
curl -X POST "$DOCO_BASE_URL/api/v1/documents/doc_123/localization/activate" \
  -H "Authorization: Bearer $DOCO_TOKEN" -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: activate-doc-123' \
  -d '{"source_locale":"zh-CN","target_locales":["en-US"],"create_mode":"copy_structure"}'
curl "$DOCO_BASE_URL/api/v1/documents/doc_123/localizations" -H "Authorization: Bearer $DOCO_TOKEN"
curl "$DOCO_BASE_URL/api/v1/documents/doc_123/read?locale=en-US" -H "Authorization: Bearer $DOCO_TOKEN"
```

读取返回 `requested_locale`、`resolved_locale` 和 `fallback_used`；写入始终使用具体 `doc_*`，继续先读 ETag、再使用 `If-Match`。块级翻译状态通过 `/translation-units?locale=en-US` 读取，人工完成后用 `review_status=current|ignored` 标记，冲突不会被静默覆盖。Search v2 支持 `locale=en-US` 或 `locale=all`，SSE 支持 `document_set_id` 与 `locale` 过滤，CLI/MCP 提供对应语言参数和翻译单元工具。

多语言文档集、语言版本和翻译单元是同一份权威结构的派生视图：`doco://doc/{id}` 仍指向具体语言版本，`doco://docset/{id}` 指向语言版本集合。`GET /documents` 默认只列出普通文档和源语言版本；需要同步翻译版本时使用 `include_variants=true` 或指定 `locale`。

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET` | `/document-sets/{id}` | `documents:read` | 读取文档集、源语言、回退语言和全部语言版本 |
| `GET` | `/documents/{id}/localizations` | `documents:read` | 列出文档集及语言版本 |
| `POST` | `/documents/{id}/localization/activate` | `documents:write` | 激活多语言并创建目标语言版本 |
| `POST` | `/documents/{id}/localizations` | `documents:write` | 增加一个目标语言版本 |
| `PATCH` | `/documents/{id}/localizations/{locale}` | `documents:write` | 修改语言版本标题或工作流状态 |
| `DELETE` | `/documents/{id}/localizations/{locale}` | `documents:write` | 删除目标语言版本 |
| `POST` | `/documents/{id}/localization/deactivate` | `documents:write` | 停用尚无目标语言版本的文档集 |
| `GET` | `/documents/{id}/translation-units?locale=en-US` | `documents:read` | 查看翻译单元、目标块和新鲜度 |
| `PATCH` | `/documents/{id}/translation-units/{unitId}` | `documents:write` | 人工确认或忽略翻译单元，需目标文档 `If-Match` |
| `GET` | `/documents/{id}/localization-audit` | `documents:read` | 查看语言版本和翻译审核审计记录 |
| `POST` | `/documents/{id}/translation-jobs` | `documents:write` | 创建异步机器翻译候选，返回 `202` |
| `GET` | `/translation-jobs/{id}` | `documents:read` | 查询候选任务状态、结果和用量成本 |
| `POST` | `/translation-jobs/{id}/apply` | `documents:write` | 选择性应用候选，必须带目标文档 `If-Match` |

#### 机器翻译候选与安全应用

机器翻译只生成候选，不会直接修改目标 YDoc。任务创建是异步的，目标文档必须已经存在；可指定最多 100 个翻译单元，不指定时默认处理 `missing` 或 `source_changed` 单元。任务受工作区每日字符额度限制，并记录输入字符、输出字符和估算成本。

```bash
# 1. 创建机器翻译候选（返回 202，data.status 初始为 pending）
curl --fail-with-body -X POST \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: translate-doc-123-en-us" \
  -d '{"target_locale":"en-US","unit_ids":["tu_01JXYZ..."]}' \
  "$DOCO_BASE_URL/api/v1/documents/doc_123/translation-jobs"

# 2. 轮询任务，直到 succeeded、failed 或 obsolete
curl --fail-with-body \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  "$DOCO_BASE_URL/api/v1/translation-jobs/tjob_01JXYZ..."

# 3. 读取目标文档最新 ETag 后，只应用选中的候选
TARGET_ETAG=$(curl -sSI \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  "$DOCO_BASE_URL/api/v1/documents/doc_en_us/content" | awk -F': ' 'tolower($1)=="etag" {print $2}' | tr -d '\r')
curl --fail-with-body -X POST \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  -H "If-Match: $TARGET_ETAG" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: apply-translate-tjob-01" \
  -d '{"unit_ids":["tu_01JXYZ..."]}' \
  "$DOCO_BASE_URL/api/v1/translation-jobs/tjob_01JXYZ.../apply"
```

应用候选必须携带目标语言文档当前 `If-Match`；也可在请求体使用 `base_target_version` 作为备用形式。缺少版本返回 `428`，目标文档已变化返回 `409`。已人工修改、已确认或已冲突的单元不会被机器候选覆盖；源文档在任务期间变化时任务变为 `obsolete`，应重新生成。

```bash
KB_RESPONSE=$(curl --fail-with-body -sS \
  -X POST \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: create-agent-kb-001" \
  -d '{"name":"Agent 知识库"}' \
  "$DOCO_BASE_URL/api/v1/knowledge-bases")

# 把下方 123 替换为上一步 data.id
curl --fail-with-body \
  -X POST \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: create-agent-doc-001" \
  -d '{
    "title": "API 创建的文档",
    "knowledge_base_id": 123,
    "content": {
      "format": "markdown",
      "content": "# Hello Doco\n\n这篇文档由 API 创建。"
    }
  }' \
  "$DOCO_BASE_URL/api/v1/documents"
```

## 鉴权与权限

Token 支持以下 scopes：

| Scope | 能力 |
| --- | --- |
| `documents:read` | 读取文档、正文和块 |
| `documents:write` | 创建、修改、移动和删除文档与块 |
| `knowledge-bases:read` | 读取知识库、文件夹和目录树 |
| `knowledge-bases:write` | 创建、修改和删除知识库与文件夹 |
| `attachments:read` | 下载附件和读取附件元数据 |
| `attachments:write` | 上传和删除附件 |
| `concepts:read` | 枚举显式概念、按别名解析、遍历关系和查看候选 |
| `concepts:write` | 创建/编辑/合并概念、添加证据和关系、提取及审核确定性候选 |
| `summaries:generate` | 触发可产生外部成本的异步模型摘要；确定性摘要读取不需要此 scope |

每个请求都应携带：

```http
Authorization: Bearer doco_tok_xxx_secret
```

Token 缺失或失效返回 `401`，scope 不足返回 `403`。为避免越权泄露，不属于当前账户的资源通常返回 `404`。

### 设备授权流程（CLI / Agent 登录）

无需手动复制 Token 的登录方式（RFC 8628 风格，`doco login` 即走此流程）：

```bash
# 1. 申请设备码（无需鉴权）
curl -X POST "$DOCO_BASE_URL/api/v1/auth/device/code" \
  -H "Content-Type: application/json" \
  -d '{"client_name":"我的脚本","scopes":"read_write"}'
# → device_code、user_code、verification_uri_complete、expires_in、interval

# 2. 用户在浏览器打开 verification_uri_complete 并确认（可选降级为只读）

# 3. 按 interval 轮询换取 Token（无需鉴权；POST，凭据不走 URL）
curl -X POST "$DOCO_BASE_URL/api/v1/auth/device/token" \
  -H "Content-Type: application/json" \
  -d '{"device_code":"<device_code>"}'
```

用户确认前轮询返回 `400 authorization_pending`；轮询过快返回 `400 slow_down`（附 `Retry-After`）；
授权码过期返回 `400 expired_token`；用户拒绝返回 `400 access_denied`。
确认后轮询返回标准 `doco_tok_` Token（`read_write` 或网页端降级的 `read_only`），每个授权码只能换取一次，
默认 90 天有效（服务端 `DOCO_DEVICE_TOKEN_TTL_DAYS` 可调），到期后重新 `doco login`。

## 通用约定

### 响应结构

单资源成功响应：

```json
{
  "data": {},
  "request_id": "req_01..."
}
```

列表成功响应：

```json
{
  "data": [],
  "page": {
    "cursor": null,
    "next_cursor": null,
    "has_more": false
  },
  "request_id": "req_01..."
}
```

错误响应：

```json
{
  "error": {
    "type": "request_error",
    "code": "invalid_request",
    "message": "可读的错误信息",
    "details": {}
  },
  "request_id": "req_01..."
}
```

### 请求追踪与限频

- 可传 `X-Request-Id`；服务端也会在响应中返回 `X-Request-Id`。
- 限频信息通过 `X-RateLimit-Limit`、`X-RateLimit-Remaining`、`X-RateLimit-Reset` 返回。
- 超过限频返回 `429`，同时提供 `Retry-After`。
- 限频窗口存于 SQLite，由多进程共享，服务重启不会清空窗口状态。
- 请为自动化客户端发送可识别的 `User-Agent`，推荐格式为
  `Doco-Agent/<版本> (<客户端名>)`；Doco CLI 会发送 `doco-agent-cli <版本>`。
  服务端会把客户端名、版本和是否为 Agent 写入 API 审计日志，用于统计 Agent 流量占比。

### 分页

列表接口使用游标分页：

```http
GET /api/v1/documents?limit=50&cursor=<next_cursor>
```

`limit` 范围为 `1` 到 `100`。继续请求时使用上一页 `page.next_cursor`。

### 幂等写入

创建资源、上传附件和批量操作支持：

```http
Idempotency-Key: <最多 128 个字符的稳定键>
```

相同 Token、方法、路径和请求体使用相同键时会返回首次结果；同一个键配合不同请求体返回 `409`。

### 并发控制

正文和块读取响应会返回 `ETag`。替换整篇正文必须通过 `If-Match` 提交当前版本：

```bash
curl -i \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  "$DOCO_BASE_URL/api/v1/documents/doc_123/content?format=markdown"

curl --fail-with-body \
  -X PUT \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  -H 'If-Match: "sha256:从上一步响应取得"' \
  -H "Content-Type: application/json" \
  -d '{"format":"markdown","content":"# 新正文"}' \
  "$DOCO_BASE_URL/api/v1/documents/doc_123/content"
```

缺少必须的 `If-Match` 返回 `428`，版本冲突返回 `409`。收到冲突后应重新读取正文、合并改动，再重试；不要盲目覆盖。

## API 一览

### 身份

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET` | `/me` | 任一有效 Token | 返回调用者和 Token scopes |
| `GET` | `/me/quota` | 任一有效 Token | 当前工作区配额自查：知识库数、文档与文件夹用量与上限、单文档字符上限等 |

### 文档变更推送

`GET /events` 使用 Server-Sent Events 持续推送当前用户有权访问的文档变更，
需要 `documents:read` scope。首次连接从当前时刻开始；断线后把最后处理成功的
事件 ID 作为 `Last-Event-ID` 传回即可续传：

```bash
curl -N \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  "$DOCO_BASE_URL/api/v1/events"

curl -N \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  -H "Last-Event-ID: 42" \
  "$DOCO_BASE_URL/api/v1/events"
```

文档事件包括：

- `document.created`
- `document.metadata.updated`
- `document.content.updated`
- `document.deleted`

每条事件都有递增的 `id`，`data` 中包含 `event_id`、`workspace_id`、
`document_id`、`created_at` 和文档基本信息。服务端每 15 秒发送一次注释心跳，
并建议客户端断线 5 秒后重连。

事件默认保留 7 天，可通过 `DOCO_EVENT_RETENTION_DAYS` 调整。游标早于保留窗口时，
服务端发送 `sync.required`；此时先用
`GET /documents?updated_since=<last_sync_time>&sort=updated_at` 补齐，再使用
`sync.required` 的 `resume_from` 继续监听。Token 被撤销或过期后，流会发送
`auth.revoked` 并关闭（撤销/过期至多在 1 个心跳周期 `DOCO_EVENT_HEARTBEAT_MS` 内生效，默认上界 15 秒）。单 Token 默认最多 3 条并发流，可通过
`DOCO_SSE_MAX_CONNECTIONS_PER_TOKEN` 调整。

### 块级变更水位

`GET /documents/{id}/changes` 把开放 API 与浏览器 Hocuspocus/Yjs 写入统一为
顶层稳定块的 `added`、`removed`、`modified`、`moved` 变更。首次不传 `after`，
响应返回当前 `manifest` 和不透明 `cursor`；保存该游标，后续原样传回：

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  "$DOCO_BASE_URL/api/v1/documents/doc_123/changes"

curl --fail-with-body --get \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  --data-urlencode "after=<上次返回的 cursor>" \
  "$DOCO_BASE_URL/api/v1/documents/doc_123/changes"
```

每条变更包含块 ID、类型、文本摘录、新旧位置、来源版本和对应游标。`freshness=current`
且 `complete=true` 才表示从给定游标到当前水位连续完整。派生链失败、重建或游标早于保留窗口时，
服务端返回 `sync_required=true`；此时重新读取正文并获取新基线，禁止把不完整结果当成穷举。
变更集每篇文档默认保留 1000 个水位，可通过 `DOCO_CHANGE_RETENTION` 调整。

### 显式关系与反向链接

正文中的 Markdown 链接可直接使用稳定 Doco URI：

```markdown
[部署依据](doco://doc/doc_123#block=block_01JXYZ0123456789ABCDEFGHJK)
```

每次正文持久化都会对该文档的内链做全量、幂等重抽，形成 `refers_to` 关系和反向链接。
需要表达明确语义时，可通过 `POST /relations` 创建 `supports`、`depends_on`、`contradicts`
等注册谓词；所有关系都记录来源块、来源版本、创建者和锚文本。

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET` | `/relation-types` | `documents:read` | 列出注册谓词 |
| `GET` | `/documents/{id}/relations?direction=both` | `documents:read` | 查询正向与反向关系，可按 predicate 过滤 |
| `POST` | `/relations` | `documents:write` | 从稳定来源块创建显式关系，建议带 `Idempotency-Key` |
| `DELETE` | `/relations/{id}` | `documents:write` | 删除手工关系；内联关系需编辑正文链接 |

```json
{
  "source_document_id": "doc_source",
  "source_block_id": "block_01JXYZ0123456789ABCDEFGHJK",
  "target_uri": "doco://doc/doc_target#block=block_01JABC0123456789ABCDEFGHJK",
  "predicate": "depends_on",
  "anchor_text": "发布流程依赖回滚预案"
}
```

目标块或文档删除后，关系不会静默消失，而会分别返回 `dangling_target_block` 或
`dangling_target_document`。调用方还必须检查 `projection_freshness`；为 `stale` 时，正文仍是权威事实，
但关系视图不能当作完整结果。反向查询始终经过当前用户权限过滤。

### 知识库

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET` | `/knowledge-bases` | `knowledge-bases:read` | 列出知识库 |
| `POST` | `/knowledge-bases` | `knowledge-bases:write` | 创建知识库 |
| `GET` | `/knowledge-bases/{id}` | `knowledge-bases:read` | 获取知识库 |
| `PATCH` | `/knowledge-bases/{id}` | `knowledge-bases:write` | 重命名知识库 |
| `DELETE` | `/knowledge-bases/{id}` | `knowledge-bases:write` | 删除知识库及其内容，正文需传 `confirm_id` |
| `GET` | `/knowledge-bases/{id}/tree` | `knowledge-bases:read` | 获取完整文件夹与文档树 |
| `GET` | `/knowledge-bases/{id}/export` | `knowledge-bases:read` + `documents:read` | 导出 ZIP |

创建知识库：

```json
{ "name": "产品知识库" }
```

删除知识库：

```json
{ "confirm_id": "kb_123" }
```

### 文件夹

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `POST` | `/folders` | `knowledge-bases:write` | 创建文件夹 |
| `GET` | `/folders/{id}` | `knowledge-bases:read` | 获取文件夹 |
| `PATCH` | `/folders/{id}` | `knowledge-bases:write` | 重命名或移动文件夹 |
| `DELETE` | `/folders/{id}` | `knowledge-bases:write` | 删除文件夹及其内容 |
| `GET` | `/folders/{id}/children` | `knowledge-bases:read` + `documents:read` | 获取直接子文件夹和文档 |

创建或移动文件夹：

```json
{
  "name": "接口设计",
  "knowledge_base_id": 123,
  "parent_id": null
}
```

### 文档

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET` | `/documents` | `documents:read` | 搜索或筛选文档 |
| `POST` | `/documents` | `documents:write` | 创建文档，可同时写入正文 |
| `GET` | `/documents/{id}` | `documents:read` | 获取文档元数据 |
| `PATCH` | `/documents/{id}` | `documents:write` | 修改标题、位置和设置 |
| `DELETE` | `/documents/{id}` | `documents:write` | 删除文档、正文和附件 |
| `GET` | `/documents/{id}/path` | `documents:read` | 获取知识库与文件夹路径 |

文档列表支持：

```http
GET /documents?q=关键词&knowledge_base_id=123&folder_id=456&limit=50
```

增量同步：

```http
GET /documents?updated_since=2026-07-27T00:00:00Z&sort=updated_at
```

- `updated_since`：ISO 8601 时间或毫秒时间戳，仅返回 `updated_at` 不早于该时刻的文档（含边界，客户端按 `id` 去重，宁重不漏）。无时区后缀的 ISO 串统一按 UTC 解释，建议显式带 `Z`（如 `2026-07-27T00:00:00Z`）。
- `sort=updated_at`：按更新时间倒序（默认按 `id` 升序）；该排序下的分页游标与默认排序不通用，混用返回 `400`。

语言筛选：

```http
GET /documents?locale=en-US
GET /documents?include_variants=true&knowledge_base_id=123
```

文档元数据会额外返回 `document_set_id`、`document_set_uri`、`locale`、`variant_role`、
`workflow_status`、`translation_freshness`、`translated_from_document_id` 和
`translated_from_version`；单语言文档对应字段为 `null` 或 `current`。

创建文档请求：

```json
{
  "title": "接口设计",
  "knowledge_base_id": 123,
  "folder_id": null,
  "document_type": "document",
  "heading_numbered": false,
  "background_color": "#faf9f5",
  "collapsed_block_ids": [],
  "content": {
    "format": "markdown",
    "content": "# 接口设计\n\n正文"
  }
}
```

`document_type` 可为 `document` 或 `spreadsheet`。

### 带新鲜度证明的全文搜索

`GET /search/v2` 使用 SQLite FTS5 同时检索标题与正文，并返回目录路径、标题路径、相邻块上下文、
命中次数、BM25 分数解释和搜索投影水位。索引跟随开放 API、浏览器协同写入和回滚同步更新；
只有 `indexed_version` 与正文 `source_version` 一致的结果才会进入 v2 响应。

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET` | `/search/v2?q={关键词}` | `documents:read` | `topk` 快速取前 N 条，或 `exhaustive` + cursor 完整遍历；`include_summary=true` 附文档摘要 |
| `GET` | `/search?q={关键词}` | `documents:read` | 兼容版搜索，仅返回块 ID 与摘要 |

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  "$DOCO_BASE_URL/api/v1/search/v2?q=部署流程&mode=exhaustive&knowledge_base_id=123&limit=20"
```

v2 响应中的 `projection.complete=true` 才表示当前可见范围内的搜索投影全部追上正文水位；
为 `false` 时会返回陈旧文档数量与 ID，不能根据本次结果断言“知识不存在”。正文命中结果包含：

```json
{
  "document_id": "doc_123",
  "title": "生产发布手册",
  "knowledge_base_id": 123,
  "folder_id": 456,
  "document_type": "document",
  "block_id": "block_01JXYZ0123456789ABCDEFGHJK",
  "document_uri": "doco://doc/doc_123",
  "path_text": "运维知识库 / 发布 / 生产发布手册",
  "heading_path": ["发布", "部署流程"],
  "matched_in": "content",
  "context": { "before": "…", "match": "部署流程分为构建、灰度与回滚三步", "after": "…" },
  "source_version": "sha256:…",
  "indexed_version": "sha256:…",
  "freshness": "current",
  "updated_at": 1785236508000
}
```

标题命中时 `matched_in` 为 `title`，`block_id` 为 `null`。同一文档多个块命中时可能返回
多条结果；这让 Agent 能直接选择目标段落，而不必先抓取整篇正文。

### 分层摘要

摘要是带来源版本的派生视图，不是正文真相。章节、文档、文件夹和知识库均可读取摘要；未配置模型时
仍会返回标题、outline 与首段组成的确定性 fallback。`freshness=stale` 必须被显式处理，不能把旧摘要
当作当前事实；`pinned=true` 只阻止自动覆盖，不隐藏来源变化。

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET` | `/documents/{id}/summary` | `documents:read` | 文档摘要；传 `block_id` 读取章节摘要，传 `query` 返回不持久化的 query-focused 摘要 |
| `PUT` | `/documents/{id}/summary` | `documents:write` | 保存人工摘要；必须携当前摘要 ETag 与 `base_source_version` |
| `POST` | `/documents/{id}/summary/rebuild` | `documents:write` | 确定性立即重建；`generator=model` 另需 `summaries:generate`，返回异步 job |
| `GET/PUT/POST` | `/folders/{id}/summary[\/rebuild]` | `knowledge-bases:read/write` | 文件夹摘要与重建 |
| `GET/PUT/POST` | `/knowledge-bases/{id}/summary[\/rebuild]` | `knowledge-bases:read/write` | 知识库摘要与重建 |
| `GET` | `/summary-jobs/{id}` | `documents:read` | 查询模型任务；来源变化或运行期被钉住时状态为 `obsolete` |

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  "$DOCO_BASE_URL/api/v1/documents/doc_123/summary?query=部署风险"
```

响应包含 `source_version`、`current_source_version`、`source_block_ids`、`source_document_ids`、
`coverage`、`generator`、`status`、`freshness`、`pinned` 和 `summary_version`。人工保存采用摘要版本与
来源版本双重保护；模型任务在提交结果前再次校验两者，过期结果不会覆盖当前摘要。

### 显式概念与候选治理

显式概念使用稳定 `doco://concept/{id}` 身份，支持名称、别名、描述、按 BCP 47 语言区分的标签、可选规范文档、来源块、
`alias_of/broader_than/related_to` 关系和 `[valid_from, valid_to)` 时间有效性。显式清单与候选严格分离：
`GET /concepts` 的 `complete=true` 可以声明显式概念枚举完整；候选只说明当前确定性抽取器发现了什么，
未经审核不会进入显式知识。

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET/POST` | `/concepts` | `concepts:read/write` | 按知识库完整枚举或创建显式概念；`q` 同时匹配规范名和别名 |
| `GET/PATCH` | `/concepts/{id}` | `concepts:read/write` | 读取或用 ETag 修改概念；已合并旧 ID 继续解析到规范概念 |
| `POST` | `/concepts/{id}/sources` | `concepts:write` | 添加带文档、块和来源版本的证据 |
| `POST` | `/concepts/{id}/relations` | `concepts:write` | 添加少量注册概念关系，并拒绝自环与 `broader_than` 环 |
| `POST` | `/concepts/{id}/merge` | `concepts:write` | 合并后保留旧 ID、别名、关系和审计历史；同时保护源/目标双版本 |
| `GET` | `/concepts/{id}/traverse?at={毫秒}&direction={方向}&predicate={谓词}` | `concepts:read` | 按时间、方向和注册谓词遍历概念关系 |
| `GET/POST` | `/concept-candidates[\/extract]` | `concepts:read/write` | 按需从文档标题和显式链接锚文本生成候选；不调用模型 |
| `POST` | `/concept-candidates/{id}/accept` | `concepts:write` | 来源仍为 current 时新建概念或并入已有概念 |
| `POST` | `/concept-candidates/{id}/reject` | `concepts:write` | 保留拒绝记录，同一 fingerprint 不反复出现 |

概念 PATCH、添加来源/关系及合并必须携 `If-Match`；候选接受/拒绝必须携候选 ETag。
缺失返回 `428`，冲突或证据已变化返回 `409`。证据响应显式给出 `source_version`、当前版本、
`freshness` 与 dangling 状态。对同一来源再次添加证据会在原关系上更新来源版本；重新提取仍为
`pending` 的同 fingerprint 候选时，也会刷新候选的来源版本并在响应中计入 `refreshed`，避免过期证据
形成无法审核的死路。概念候选首版只有确定性标题/链接来源；模型抽取、Embedding、GraphRAG、
复杂本体和图数据库均不属于本阶段能力。

概念的主名称保持向后兼容；多语言展示名称和描述放在 `labels` 中。创建或修改概念时可传入多个语言标签，同一概念每个 locale 只能有一个标签：

```json
{
  "name": "发布流程",
  "labels": [
    { "locale": "zh-CN", "name": "发布流程", "description": "生产发布的标准步骤" },
    { "locale": "en-US", "name": "Release Process", "description": "Standard production release steps", "origin": "manual" }
  ]
}
```

读取概念时，`data.labels` 返回 `locale`、`name`、`description`、`origin`、创建者和时间戳。修改标签与修改概念本体一样需要读取概念 ETag 并通过 `If-Match` 提交；冲突返回 `409`。

### 正文

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET` | `/documents/{id}/content` | `documents:read` | 读取正文 |
| `GET` | `/documents/{id}/outline` | `documents:read` | 读取标题层级、稳定块 ID 与章节块区间 |
| `GET` | `/documents/{id}/read` | `documents:read` | 按 around / token 预算局部读取并用 cursor 续读 |
| `PUT` | `/documents/{id}/content` | `documents:write` | 替换整篇正文，必须传 `If-Match` |

长文档先调 `/outline`，再以 `around=<block_id>` 读取目标附近，或用 `max_tokens` 从头分页。
局部读取支持 `markdown`、`tiptap-json`、`plain-text`、`outline` 四种 view。`next_cursor` 绑定正文版本；
正文发生变化后旧游标返回 `409 read_cursor_stale`。若单个块本身超过预算，服务端仍返回该块并设置
`budget_exceeded=true`，不会制造无法前进的空页。

读取时可通过 `format` 选择：

- `tiptap-json`：无损标准格式，适合结构化编辑。
- `markdown`：便于 Agent 阅读与生成，复杂节点可能返回降级警告。
- `html`：经过服务端清洗的 HTML。

**Markdown 锚点往返**：`format=markdown&annotate=anchors` 会在每个顶层块前注入锚点注释（标题块附 slug）：

```markdown
<!--@block=block_01JXK3... #部署流程-->
# 部署流程

<!--@block=block_01JXP9...-->
第一段。
```

把这份 Markdown 改完后以 `PUT /content`（`format: markdown`）整篇写回：带锚点的块保留原块 ID，
未标注的新块获得新 ID，重复锚点返回 `422`。这样 Agent 用最母语的 Markdown 读写，也能保持块级寻址不漂移。
注意：锚点归属于其后第一个块——在某个块之前插入新内容时，把新内容写在锚点注释之前。

写入 Markdown：

```json
{
  "format": "markdown",
  "content": "# 标题\n\n- 第一项\n- 第二项"
}
```

写入 Tiptap JSON：

```json
{
  "format": "tiptap-json",
  "document": {
    "type": "doc",
    "content": [
      {
        "type": "paragraph",
        "attrs": { "id": "block_01JXYZ0123456789ABCDEFGHJK" },
        "content": [{ "type": "text", "text": "Hello" }]
      }
    ]
  }
}
```

服务端会为缺少 ID 的块补充稳定 ID。块 ID 格式为 `block_<ULID>`。

### 版本与回滚

每次开放 API 修改正文或块之前，Doco 都会保存当前 YDoc 的精确快照。浏览器中的协同编辑不会创建这些版本；
它们专门用于 Agent 或脚本写错后的恢复。服务端默认保留每篇文档最近 20 份，可通过
`DOCO_VERSION_RETENTION` 调整。

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET` | `/documents/{id}/versions` | `documents:read` | 按 `seq` 从新到旧列出版本；返回时间、创建者和字节数，不返回快照 blob |
| `POST` | `/documents/{id}/versions/{seq}/rollback` | `documents:write` | 恢复指定版本；必须传当前 `If-Match`，回滚前也会保存当前版本 |

列出版本：

```bash
curl --fail-with-body \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  "$DOCO_BASE_URL/api/v1/documents/doc_123/versions"
```

回滚前先读取当前 ETag，再提交目标 `seq`：

```bash
CURRENT_ETAG=$(curl -sSI \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  "$DOCO_BASE_URL/api/v1/documents/doc_123/content" \
  | awk 'tolower($1) == "etag:" { print $2 }' | tr -d '\r')

curl --fail-with-body \
  -X POST \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  -H "If-Match: $CURRENT_ETAG" \
  "$DOCO_BASE_URL/api/v1/documents/doc_123/versions/3/rollback"
```

缺少 `If-Match` 返回 `428`，当前版本已变化返回 `409`，目标 `seq` 不存在返回 `404`。回滚成功后响应
`ETag` 和 `data.version` 都是恢复操作完成后的新版本；它不要求与历史快照的旧 ETag 相同。

### 块

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `GET` | `/documents/{id}/blocks` | `documents:read` | 获取顶层块；`recursive=true` 返回所有层级 |
| `GET` | `/documents/{id}/blocks/{blockId}` | `documents:read` | 获取指定块 |
| `POST` | `/documents/{id}/blocks` | `documents:write` | 插入一个或多个块 |
| `PATCH` | `/documents/{id}/blocks/{blockId}` | `documents:write` | 替换节点、属性或内容 |
| `DELETE` | `/documents/{id}/blocks/{blockId}` | `documents:write` | 删除块 |
| `POST` | `/documents/{id}/batch` | `documents:write` | 在单个 Yjs 事务内执行最多 100 个操作 |

插入块时，`position` 必须且只能指定一种定位方式：`document_start`、`document_end`、`before_block_id`、`after_block_id`、`parent_block_id` 或 `after_heading`。

```json
{
  "position": { "document_end": true },
  "nodes": [
    {
      "type": "paragraph",
      "content": [{ "type": "text", "text": "追加内容" }]
    }
  ]
}
```

`after_heading` 按标题文本定位（告诉它章节名就够了）：服务端在顶层标题块中精确匹配（忽略大小写与多余空白），
退化为首个包含匹配，插入到该标题之后；无匹配返回 `422 heading_not_found`。批量操作 `insert` 同样支持。

```json
{
  "position": { "after_heading": "部署流程" },
  "nodes": [{ "type": "paragraph", "content": [{ "type": "text", "text": "插在小节开头" }] }]
}
```

批量操作：

```json
{
  "base_version": "sha256:当前版本",
  "operations": [
    {
      "op": "insert",
      "position": { "document_end": true },
      "nodes": [{ "type": "paragraph", "content": [{ "type": "text", "text": "新增段落" }] }]
    },
    {
      "op": "delete",
      "block_id": "block_01JXYZ0123456789ABCDEFGHJK"
    }
  ]
}
```

批量操作支持 `insert`、`delete` 和 `replace`。`replace` 需提交 `block_id` 与完整 `node`。整个批次要么全部成功，要么全部失败；修改单个块的属性或内容可使用块 `PATCH` 接口。

### 附件

| 方法 | 路径 | Scope | 说明 |
| --- | --- | --- | --- |
| `POST` | `/attachments` | `attachments:write` | 以 `multipart/form-data` 上传附件 |
| `GET` | `/attachments/{id}` | `attachments:read` | 下载附件 |
| `GET` | `/attachments/{id}/metadata` | `attachments:read` | 读取附件元数据 |
| `DELETE` | `/attachments/{id}` | `attachments:write` + `documents:write` | 删除附件；被正文引用时需 `force=true` |

```bash
curl --fail-with-body \
  -X POST \
  -H "Authorization: Bearer $DOCO_API_TOKEN" \
  -H "Idempotency-Key: upload-cover-001" \
  -F "document_id=doc_123" \
  -F "file=@cover.png" \
  "$DOCO_BASE_URL/api/v1/attachments"
```

正文图片节点应保存 `attachmentId`。服务端会把 `src` 规范化为 `/api/v1/attachments/{id}`。强制删除被引用附件时，服务端也会移除相应图片节点。

## Agent 使用建议

1. 启动时先调用 `/me`，确认 Token 有效及 scopes 足够；需要自查限额时用 `/me/quota`。
2. 已知内容关键词时优先用 `/search/v2`（MCP：`doco_search_v2`），先检查 `projection.complete`，再依据路径、标题路径、前后文与块 ID 定位；不完整结果不能证明“知识不存在”。
3. 陌生长文档先读 `/documents/{id}/outline`，再用 `/documents/{id}/read` 按 `around` 或 token 预算局部读取；旧游标返回 `read_cursor_stale` 时从新版本重读。
4. 整篇阅读使用 `format=markdown`；需要无损结构化修改时使用 `tiptap-json` 或块 API。
5. 整篇 Markdown 读写用 `annotate=anchors` 往返，未改动的块凭锚点保留原 ID；只改局部时优先块 API 或 `after_heading` 定位。
6. 修改前保存 `ETag`；遇到 `409` 重新读取并合并，不要覆盖他人的更新。
7. 大范围修改前可先确认 `/versions` 正常记录；写错后用最新 ETag 回滚，不要绕过并发检查。
8. 对可重试的创建、上传和批量请求使用稳定的 `Idempotency-Key`。
9. 记录 `request_id`，排查问题时可关联服务端审计日志。
10. 读取列表直到 `has_more=false`；增量同步用 `GET /documents?updated_since=…&sort=updated_at` 或块变更游标，不要全量爬库。

## 机器可读规范

完整字段、Schema、状态码和约束以运行时 OpenAPI 3.1（当前版本 `1.9.0`）文档为准：

```bash
curl --fail-with-body "$DOCO_BASE_URL/api/openapi.json"
```

如果本文与机器可读规范不一致，应以 `/api/openapi.json` 和实际响应为准，并提交文档修正。
