> ## Documentation Index
> Fetch the complete documentation index at: https://docs.somark.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# 文档目录树

> 从 SoMark 支持的文档或 SoMark JSON 中提取可读、可复用的文档目录树。

SoMark 可以识别文档中的标题层级，并在 JSON 结果的 `data.result.catalog` 中返回嵌套的目录树。每个目录节点都保留标题内容、标题层级、所在页码和对应内容块编号，可用于构建阅读导航、定位原文，以及按章节切分内容，解决 RAG 固定长度切块易打散章节上下文、难以按主题检索的问题。

| 场景                                    | 推荐方法                              |
| ------------------------------------- | --------------------------------- |
| 正在开发自己的应用或服务                          | 通过解析 API 获取 `data.result.catalog` |
| 已有文档或 SoMark JSON，并使用支持 Skill 的 Agent | 使用 `get-catalog-tree` Skill       |

## 方法一：通过解析 API 获取

<Note>
  目录树不是单独调用的接口。解析时开启 `feature_config.enable_title_level_recognition`，并请求 `json` 输出即可获得。
</Note>

<Steps>
  <Step title="开启标题层级识别">
    在解析请求的 `feature_config` 中将 `enable_title_level_recognition` 设为 `true`。该参数默认值为 `false`。
  </Step>

  <Step title="请求 JSON 输出">
    将 `json` 加入 `output_formats`。解析完成后，从响应的 `data.result.catalog` 读取目录树。
  </Step>

  <Step title="按需使用目录节点">
    直接渲染 `children` 可得到多级导航；使用 `page_num` 和 `block_idx` 可将目录项关联到原文页面和内容块。
  </Step>
</Steps>

以下示例使用同步解析接口：

<CodeGroup dropdown>
  ```python Python theme={null}
  import json
  import requests

  url = "https://somark.cn/api/v1/parse/sync"
  data = {
      "output_formats": ["json"],
      "api_key": "sk-***",
      "feature_config": json.dumps({
          "enable_title_level_recognition": True,
      }),
  }

  with open("example.pdf", "rb") as file:
      response = requests.post(
          url,
          data=data,
          files={"file": ("example.pdf", file)},
      )

  response.raise_for_status()
  catalog = response.json()["data"]["result"]["catalog"]
  print(json.dumps(catalog, ensure_ascii=False, indent=2))
  ```

  ```bash cURL theme={null}
  curl -X POST https://somark.cn/api/v1/parse/sync \
    -F "file=@example.pdf" \
    -F "output_formats=json" \
    -F "api_key=sk-***" \
    -F 'feature_config={"enable_title_level_recognition":true}'
  ```

  ```javascript JavaScript theme={null}
  import { readFile } from "node:fs/promises";

  const form = new FormData();
  form.append(
    "file",
    new Blob([await readFile("example.pdf")], { type: "application/pdf" }),
    "example.pdf",
  );
  form.append("output_formats", "json");
  form.append("api_key", "sk-***");
  form.append("feature_config", JSON.stringify({
    enable_title_level_recognition: true,
  }));

  const response = await fetch("https://somark.cn/api/v1/parse/sync", {
    method: "POST",
    body: form,
  });

  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const result = await response.json();
  console.log(result.data.result.catalog);
  ```

  ```php PHP theme={null}
  <?php
  // Requires: composer require guzzlehttp/guzzle
  require __DIR__ . "/vendor/autoload.php";

  use GuzzleHttp\Client;

  $client = new Client();
  $response = $client->post("https://somark.cn/api/v1/parse/sync", [
      "multipart" => [
          [
              "name" => "file",
              "contents" => fopen("example.pdf", "rb"),
              "filename" => "example.pdf",
          ],
          ["name" => "output_formats", "contents" => "json"],
          ["name" => "api_key", "contents" => "sk-***"],
          [
              "name" => "feature_config",
              "contents" => json_encode([
                  "enable_title_level_recognition" => true,
              ]),
          ],
      ],
  ]);

  $result = json_decode($response->getBody(), true);
  print_r($result["data"]["result"]["catalog"]);
  ```

  ```go Go theme={null}
  package main

  import (
      "bytes"
      "fmt"
      "io"
      "mime/multipart"
      "net/http"
      "os"
      "path/filepath"
  )

  func check(err error) {
      if err != nil {
          panic(err)
      }
  }

  func main() {
      file, err := os.Open("example.pdf")
      check(err)
      defer file.Close()

      var body bytes.Buffer
      writer := multipart.NewWriter(&body)
      part, err := writer.CreateFormFile("file", filepath.Base(file.Name()))
      check(err)
      _, err = io.Copy(part, file)
      check(err)

      check(writer.WriteField("output_formats", "json"))
      check(writer.WriteField("api_key", "sk-***"))
      check(writer.WriteField(
          "feature_config",
          `{"enable_title_level_recognition":true}`,
      ))
      check(writer.Close())

      request, err := http.NewRequest(
          http.MethodPost,
          "https://somark.cn/api/v1/parse/sync",
          &body,
      )
      check(err)
      request.Header.Set("Content-Type", writer.FormDataContentType())

      response, err := http.DefaultClient.Do(request)
      check(err)
      defer response.Body.Close()

      fmt.Println(response.Status)
      _, err = io.Copy(os.Stdout, response.Body)
      check(err)
  }
  ```

  ```java Java theme={null}
  // Requires: org.apache.httpcomponents.client5:httpclient5
  import java.io.File;
  import java.nio.charset.StandardCharsets;

  import org.apache.hc.client5.http.classic.methods.HttpPost;
  import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
  import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
  import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
  import org.apache.hc.client5.http.impl.classic.HttpClients;
  import org.apache.hc.core5.http.ContentType;
  import org.apache.hc.core5.http.io.entity.EntityUtils;

  public class ParseDocument {
      public static void main(String[] args) throws Exception {
          HttpPost request = new HttpPost(
              "https://somark.cn/api/v1/parse/sync"
          );
          request.setEntity(MultipartEntityBuilder.create()
              .addBinaryBody(
                  "file",
                  new File("example.pdf"),
                  ContentType.APPLICATION_PDF,
                  "example.pdf"
              )
              .addTextBody("output_formats", "json")
              .addTextBody("api_key", "sk-***")
              .addTextBody(
                  "feature_config",
                  "{\"enable_title_level_recognition\":true}",
                  ContentType.APPLICATION_JSON
              )
              .build());

          try (
              CloseableHttpClient client = HttpClients.createDefault();
              CloseableHttpResponse response = client.execute(request)
          ) {
              System.out.println(EntityUtils.toString(
                  response.getEntity(),
                  StandardCharsets.UTF_8
              ));
          }
      }
  }
  ```

  ```ruby Ruby theme={null}
  require "json"
  require "net/http"
  require "uri"

  uri = URI("https://somark.cn/api/v1/parse/sync")
  request = Net::HTTP::Post.new(uri)

  File.open("example.pdf", "rb") do |file|
    request.set_form([
      ["file", file, {
        filename: "example.pdf",
        content_type: "application/pdf"
      }],
      ["output_formats", "json"],
      ["api_key", "sk-***"],
      ["feature_config", JSON.generate({
        enable_title_level_recognition: true
      })]
    ], "multipart/form-data")

    response = Net::HTTP.start(
      uri.hostname,
      uri.port,
      use_ssl: uri.scheme == "https"
    ) { |http| http.request(request) }

    catalog = JSON.parse(response.body).dig("data", "result", "catalog")
    puts JSON.pretty_generate(catalog)
  end
  ```
</CodeGroup>

有关完整请求参数，请参阅[同步解析](/api-reference/endpoint/sync#feature-config)。

## 方法二：使用 get-catalog-tree Skill

`get-catalog-tree` Skill 支持 SoMark 解析接口支持的全部文档格式，包括 PDF、图片、Word、PPT 和 Excel，也可以直接读取 SoMark JSON。你只需把文件交给支持 Skill 的 Agent，即可获得标准嵌套目录 JSON。完整格式列表请参阅[支持的文件格式](/documentation/index#支持的文件格式)。

<Note>
  目录树依赖明确的标题层级。Excel 等通常以表格数据为主、没有目录结构的文档虽然可以解析，但不建议使用此功能，生成的目录树可能为空或缺少有意义的层级。
</Note>

首次使用时，任选一种方式安装 Skill：

<Tabs>
  <Tab title="在 Agent 中安装">
    输入以下提示词：

    ```text theme={null}
    请从 https://github.com/SoMarkAI/skills 安装 get-catalog-tree Skill。
    ```
  </Tab>

  <Tab title="使用命令安装">
    ```bash theme={null}
    npx skills add SoMarkAI/skills --skill get-catalog-tree
    ```
  </Tab>
</Tabs>

安装后，在 Agent 中输入：

```text theme={null}
使用 SoMark get-catalog-tree Skill 为 document.pdf 生成目录树 JSON。
```

如果输入原始文档，Agent 会在发起 SoMark 解析前请求一次确认。Skill 只生成一个文件：

```text theme={null}
document.catalog.json     # 标准嵌套目录 JSON
```

也可以直接提供 SoMark JSON：

```text theme={null}
使用 SoMark get-catalog-tree Skill 读取 result.json，生成嵌套目录树 JSON。
```

目录节点保留 `page_num` 和 `block_idx`，可关联到原始解析 JSON 中对应的标题块，用于后续章节分块和 RAG 处理。

也可以直接运行 Skill 自带的脚本：

```bash theme={null}
python <skill-directory>/scripts/get_catalog_tree.py --file document.pdf --output catalog
python <skill-directory>/scripts/get_catalog_tree.py --json result.json --output catalog
```

## 响应结构

目录树位于 `data.result.catalog`，为目录节点数组。顶层节点通常对应文档中的一级标题，子节点通过 `children` 递归组织。

```json theme={null}
{
  "code": 0,
  "message": "ok",
  "data": {
    "result": {
      "catalog": [
        {
          "page_num": 0,
          "block_idx": 6,
          "title_level": 1,
          "content": "第一章 引言",
          "children": [
            {
              "page_num": 0,
              "block_idx": 14,
              "title_level": 2,
              "content": "1.1 研究背景",
              "children": []
            }
          ]
        }
      ]
    }
  }
}
```

| 字段            | 类型      | 说明                                                |
| ------------- | ------- | ------------------------------------------------- |
| `page_num`    | integer | 标题所在页的页码，从 `0` 开始计数。展示给最终用户时，如需从第 1 页开始计数，可加 `1`。 |
| `block_idx`   | integer | 标题在该页解析结果中的内容块编号，可用于回查和定位对应内容块。                   |
| `title_level` | integer | 标题层级。`1` 表示 H1，`2` 表示 H2，依此类推。                    |
| `content`     | string  | 标题文本。                                             |
| `children`    | array   | 当前标题的下一级标题节点；没有子标题时为空数组。                          |

## 使用建议

* 目录节点已经是嵌套结构，无需再根据 `title_level` 手动组装父子关系。
* 使用目录树进行章节切分时，可将一个节点及其子节点视为同一章节范围；再结合 `page_num`、`block_idx` 从 JSON 内容块中取回正文。
* 标题层级基于文档版式和内容识别。对于原文没有明确标题层次、扫描质量较低或排版不规则的文件，建议在接入前抽样检查结果。
* 如只需要文档内容，不需要章节导航，可保持该开关关闭，以使用默认解析配置。
