# 档案导入 API ## 接口说明 导入一份 Markdown 档案文本,将其规范化为 Archive、Page、Chunk 三层结构,并为每个 chunk 生成全局唯一的 `chunk_uid`。 正式使用时,推荐直接上传原始 Markdown 文件,或者把 Markdown 原文作为请求体发送。系统会根据 Markdown 中的分页标记自动分页、分段和切 chunk。 导入会先写入 PostgreSQL。如果 `title/year/author/tags/summary` 缺失,系统会把 `archive_uid` 推入 Redis 队列,由独立的 `ai_metadata` process 异步请求 AI 补全并更新数据库。 当前版本会把导入结果保存为运行时快照文件: ```text runtime/proofdb/imports/{import_uid}.json ``` 后续接入 MySQL、OpenSearch、Vector DB 时,会沿用当前返回结构中的 UID。 ## 请求方式 ```http POST /api/articles/import ``` 支持三种调用方式: 1. `multipart/form-data` 上传 Markdown 文件。 2. `text/markdown` 或 `text/plain` 直接发送 Markdown 原文。 3. `application/json` 发送 Markdown 字符串,适合程序内部调用。 ## 请求字段 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `file` | file | 否 | Markdown 文件。使用 `multipart/form-data` 时推荐传此字段 | | `archive_uid` | string | 否 | 档案级 ULID。不传时系统自动生成 | | `title` | string | 否 | 档案标题。不传时,系统会优先请求 AI 生成,失败后从 Markdown 标题或文件名推断 | | `year` | int | 否 | 档案年份。不传时,系统会请求 AI 从文本中抽取或推断 | | `author` | string | 否 | 作者或签发人。不传时,系统会请求 AI 从文本中抽取或推断 | | `summary` | string | 否 | 档案摘要。不传时,系统会请求 AI 生成 | | `source` | string | 否 | 来源标识。不传时,系统会使用上传文件名或 `raw-markdown` | | `series` | string | 否 | 所属系列集 | | `tags` | array | 否 | 标签数组。不传时,系统会请求 AI 生成 | | `metadata` | object | 否 | 档案级元数据,例如年份、作者、馆藏信息 | | `content` | string | 否 | Markdown 原文。JSON 调用时使用 | | `pages` | array | 否 | 页数组。兼容字段,一般不需要调用方传入 | | `pages[].page_number` | int|string | 是 | 页码,可传数字或原始页码字符串,例如 `12`、`12a`、`封面` | | `pages[].content` | string | 是 | 当前页 OCR 或转录文本 | | `pages[].metadata` | object | 否 | 页级元数据 | | `paragraphs` | array | 否 | 段落数组。兼容字段,一般不需要调用方传入 | | `paragraphs[].page_number` | int|string | 否 | 当前段落所在页码 | | `paragraphs[].content` | string | 是 | 段落正文 | | `paragraphs[].metadata` | object | 否 | 段落级元数据 | | `chunk_size` | int | 否 | 每个 chunk 的目标最大字符数,默认 `800`,范围 `100-4000` | | `chunk_overlap` | int | 否 | 超长文本硬切时的重叠字符数,默认 `120`,必须小于 `chunk_size` | ## Markdown 分页规则 系统会优先识别以下分页格式: ```markdown ## Page 1 第一页正文... --- ## Page 2 第二页正文... ``` 如果没有 `DOCMASTER:PAGE` 标记,系统会把整份 Markdown 当作单页处理。 ## 分段与 Chunk 切分策略 当前 import 使用 Markdown 预处理 + 页内向量 chunk 切分: 1. 先根据 Markdown 分页标记拆成 page。 2. 每页内部去掉分页标题、水平分隔线等结构性标记。 3. 页眉、页脚、密级、纯页码等噪声块会被过滤,不作为向量 chunk 的正文。 4. 检索证据只定位到页码,因此 chunk 可以跨段落、跨列表项,但不会跨页。 5. 每页内部会按段落/句子/自然停顿形成文本单元,并尽量打包到接近 `chunk_size`。 6. 如果单个文本单元超过 `chunk_size`,才退回固定字符窗口硬切,并使用 `chunk_overlap` 保留上下文。 ## 请求示例 ```bash curl -X POST http://127.0.0.1:8787/api/articles/import \ -F 'title=NSD 76 Disposition of NSC Policy Documents' \ -F 'source=archive://nsc/nsd-76' \ -F 'chunk_size=800' \ -F 'chunk_overlap=120' \ -F 'file=@test/1.test.md;type=text/markdown' ``` 也可以直接发送 Markdown 原文: ```bash curl -X POST 'http://127.0.0.1:8787/api/articles/import?title=NSD%2076&source=archive://nsc/nsd-76' \ -H 'Content-Type: text/markdown' \ --data-binary '@test/1.test.md' ``` JSON 调用示例: ```bash curl -X POST http://127.0.0.1:8787/api/articles/import \ -H 'Content-Type: application/json' \ --data '{ "title": "NSD 76 Disposition of NSC Policy Documents", "source": "archive://nsc/nsd-76", "content": "\n\n## Page 1\n\nMarkdown 正文..." }' ``` ## 成功响应 状态码: ```http 201 Created ``` 响应示例: ```json { "code": 0, "message": "Archive imported.", "data": { "import_uid": "01HX8K7V9Y3QF2Z6M4A1B8C9D0", "archive": { "archive_uid": "01HX8K7V9Y3QF2Z6M4A1B8C9D0", "title": "NSD 76 Disposition of NSC Policy Documents", "year": 1992, "author": "Brent Scowcroft", "source": "archive://nsc/nsd-76", "series": null, "tags": [ "美国国家安全委员会", "政策文件", "NSD 76" ], "summary": "这份档案是 1992 年关于 NSC 政策文件处置的国家安全指令,列明已被取代或已完成、不再有效的政策文件。", "metadata": { "ai_enrichment": { "enabled": true, "attempted": true, "filled": [ "year", "author", "tags", "summary" ], "missing": [], "model": "glm-4.7-flash" } } }, "pages": [ { "page_number": 1, "block_count": 8, "chunk_count": 2, "content_length": 1042, "chunk_uids": [ "01HX8K7V9Y3QF2Z6M4A1B8C9D0_1_28172", "01HX8K7V9Y3QF2Z6M4A1B8C9D0_2_19304" ] } ], "chunks": [ { "chunk_uid": "01HX8K7V9Y3QF2Z6M4A1B8C9D0_1_28172", "chunk_index": 1, "page_start": 1, "page_end": 1, "pages": [ 1 ], "text": "NSD 45 20 AUG 90 U.S. Policy in Response to the Iraqi Invasion of Kuwait (C)\n* *COMMENT** OBE by operations Desert Shield and Desert Storm.", "length": 142, "embedding_ref": null } ], "stats": { "page_count": 8, "page_block_count": 86, "chunk_count": 14, "chunk_size": 800, "chunk_overlap": 120 }, "queue": { "ai_metadata_enqueued": true, "needs_ai_metadata": true } } } ``` ## 错误响应 ### JSON 格式错误 状态码: ```http 400 Bad Request ``` ```json { "code": 400, "message": "Invalid JSON body.", "errors": { "body": "Syntax error" } } ``` ### 参数校验失败 状态码: ```http 422 Unprocessable Entity ``` ```json { "code": 422, "message": "Archive import validation failed.", "errors": { "source": [ "source is required." ], "content": [ "content, file, pages, or paragraphs is required." ] } } ``` ### 快照保存失败 状态码: ```http 500 Internal Server Error ``` ```json { "code": 500, "message": "Archive import snapshot could not be saved.", "errors": { "storage": "具体错误信息" } } ``` ## UID 说明 | UID | 说明 | | --- | --- | | `import_uid` | 本次导入任务 ID,目前等同于档案级 ULID | | `archive_uid` | 档案级 ULID,例如 `01HX8K7V9Y3QF2Z6M4A1B8C9D0` | | `chunk_uid` | chunk 级核心 ID,格式为 `{archive_uid}_{chunk_index}_{short_uid}` | `chunk_uid` 是后续 MySQL、OpenSearch、Vector DB 之间关联的核心字段。 `chunk_index` 从 `1` 开始递增。`short_uid` 是 5 位数字短码,由 chunk 的稳定内容哈希生成,方便人工查看和引用。