导入文档
本指南介绍如何通过卡片 API 把文档放进 Sursa:可以用脚本添加几个文档,也可以通过定期作业导入某个系统的全部文档。每个文档成为合集中的一张卡片。用户可以在搜索中找到它,助手会根据它作答;它和其他卡片一样,有负责人、标签、核实和权限。
如果希望 Sursa 自动判断您的系统中删除了哪些文档,或者要从集成平台推送文档,请改用自定义数据源。它专为全量同步和增量同步而设计。本指南介绍直接写入合集。
需要一个具有 cards:write 权限范围的 API 密钥,且其角色可以写入目标合集(editor 或 admin)。示例使用快速入门中的辅助函数。
选择合适的接口
| 您手头有 | 使用 |
|---|---|
| 您系统中具有稳定 ID 的记录 | PUT /v1/cards-by-external-id/{external_id}:按您的 ID 创建或替换。 |
| 只添加一次、没有自己 ID 的文本 | POST /v1/cards/text |
| 只添加一次的文件 | POST /v1/cards |
| 包含大量文件的 ZIP 压缩包 | POST /v1/cards/archive |
| 超过单个请求限制的文件 | 先分段上传,再使用上述任一接口。 |
| 已有卡片的新内容 | PUT /v1/cards/{card_id}/content |
凡是会导入多次的内容,都请使用 external_id。这样每次导入都可以重复执行:再次运行时会更新之前创建的卡片,而不是添加副本。
按您的 ID 创建或替换卡片
PUT /v1/cards-by-external-id/{external_id} 接受记录在您系统中的 ID。如果没有卡片具有这个 external_id,就创建一张(201)。如果有卡片具有它,就替换该卡片的内容和详细信息(200)。
curl -X PUT https://api.sursa.com/v1/cards-by-external-id/kb-article-1042 \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"collection_id": 3,
"folder_id": "0f8e2d7c-4b6a-4e1f-9c3d-2a5b8e7f1c40",
"title": "Returning a damaged item",
"content_type": "html",
"body": "<h2>Damaged items</h2><p>Send a photo within 7 days and we ship a replacement.</p>",
"tags": ["returns", "shipping"],
"metadata": {"source": "helpdesk", "article_id": "1042"}
}'重复执行时会发生什么:
- 内容没有变化:不会生成新版本。响应为
200,带有"status": "skipped"和"reason": "unchanged"。重复发送请求始终是安全的,即使不带Idempotency-Key。 - 内容有变化:卡片生成新版本,并重新索引。标题、标签、负责人、分类和
metadata采用您发送的值;metadata整体替换。 - 合集和文件夹只在创建卡片时生效。之后要移动卡片,请用
PATCH /v1/cards/{card_id}更改。
与任何内容变更一样,新版本会撤销卡片的核实状态:需要核实人再次确认。
文本格式
content_type | 正文为 |
|---|---|
markdown(默认) | Markdown。 |
html | HTML。会被清理为简单的标记:标题、段落、列表、表格,以及 http、https 和 mailto 链接。脚本、样式、框架、表单和图片连同其内容一起删除。 |
text | 纯文本。段落之间用空行分隔。 |
是否等待卡片就绪
默认情况下,请求在卡片可以被搜索到时才返回,带有 "status": "ready",卡片在 card 中。所需时间从短文本的不到一秒,到长文件的一分钟或更久不等。
批量导入时,请传入 "async": true。响应会立即返回,带有 "status": "queued",索引作业在 job 中(含 job_id)。用 GET /v1/jobs/{job_id} 查看作业,或者在 GET /v1/jobs/stream 中跟踪所有作业。作业结束时 status 为 done、error 或 canceled。
curl https://api.sursa.com/v1/jobs/48213 \
-H "Authorization: Bearer $SURSA_API_KEY"上传文件
文件通过 multipart/form-data 请求的 file 字段上传。Sursa 支持 PDF、Word、Excel、PowerPoint、OpenDocument 电子表格、CSV、Markdown、HTML、AsciiDoc、电子邮件(.eml)、纯文本和常见的图片格式。Sursa 会检查文件名、声明的类型和文件内容是否一致,不一致时以 415 拒绝。
要按您的 ID 创建或替换文件卡片,把文件发送到同一个 PUT /v1/cards-by-external-id/{external_id}:
curl -X PUT https://api.sursa.com/v1/cards-by-external-id/handbook-2026 \
-H "Authorization: Bearer $SURSA_API_KEY" \
-F "file=@employee-handbook.pdf" \
-F "collection_id=3" \
-F "tags=hr,policies" \
-F 'metadata={"owner_team":"people"}'在表单中,tags 是逗号分隔的字符串,metadata 是写成字符串的 JSON 对象。没有 title 的新卡片以文件名作为标题。
要只添加一次文件、不使用自己的 ID,请用同样的表单和 Idempotency-Key 调用 POST /v1/cards。上传合集中已有的文件时,返回 "status": "skipped"、"duplicate": true 和已有的 card_id,除非传入了 external_id。
单个文件的大小受部署的限制(默认 200 MB)。文件更大或网络不稳定时,请先分段上传文件。
替换卡片的内容
已经知道卡片在 Sursa 中的 ID 时,PUT /v1/cards/{card_id}/content 替换卡片的文本或文件,其他一切保持不变:负责人、标签、核实人、核实周期和手动设置的分类。
curl -X PUT https://api.sursa.com/v1/cards/3f2c9a7e-5b1d-4c8e-9f0a-1b2c3d4e5f60/content \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content_type": "markdown", "body": "# Refund policy\n\nRefunds are processed within 10 days."}'- 再次发送相同的内容,返回
"status": "skipped"和"reason": "unchanged"。 - 正在索引的卡片返回
409,错误码为cards.indexing_in_progress。几秒后重试。 - 来自数据源(例如连接器或自定义数据源)的卡片只能在其数据源中更改:
cards.managed_by_source。 - 密钥必须能看到整张卡片;只能看到部分内容的卡片返回
cards.content_whole_access。
导入大量文档
对记录逐个调用 PUT /v1/cards-by-external-id/… 是最简单的批量导入方式,在任何时候停止并重新运行都是安全的。要让它更快,并且不触发速率限制:
- 传入
"async": true,让每个请求在卡片进入队列后立即返回。 - 并行运行几个请求(4–8 个),当
RateLimit头显示剩余请求不多时放慢速度。参见速率限制与配额。 - 遇到
429时等待Retry-After;遇到网络错误或5xx时重试同一个请求。重复请求不会造成影响。 - 记下每条以错误结束的记录的
external_id,最后再重试这些记录。
import { api } from "./sursa.mjs";
async function importAll(records, concurrency = 6) {
const failed = [];
const queue = [...records];
async function worker() {
for (let record = queue.shift(); record; record = queue.shift()) {
try {
await api("PUT", `/v1/cards-by-external-id/${encodeURIComponent(record.id)}`, {
collection_id: 3,
title: record.title,
content_type: "html",
body: record.html,
metadata: { source: "helpdesk" },
async: true,
});
} catch (error) {
failed.push({ id: record.id, error: String(error) });
}
}
}
await Promise.all(Array.from({ length: concurrency }, worker));
return failed;
}对于文件的 ZIP 压缩包,POST /v1/cards/archive 用一个请求把其中每个受支持的文件转换为卡片。它会立即返回处理计划(哪些文件会成为卡片、哪些会被跳过以及原因)和一个作业;GET /v1/archives/{job_id} 报告结果。
查找和删除导入的卡片
- 用
GET /v1/cards-by-external-id/{external_id}按您的 ID 读取卡片。 - 用元数据筛选条件列出某次导入的卡片:
GET /v1/cards?metadata[source]=helpdesk。参见元数据。 - 用
DELETE /v1/cards/{card_id}删除卡片。
常见错误
| 错误码 | 原因 | 处理方法 |
|---|---|---|
collections.not_found | 密钥看不到具有此 ID 或名称的合集。 | 用 GET /v1/collections 检查 collection_id。 |
collections.no_write_access | 密钥不能写入此合集。 | 使用具有 editor 角色并有权访问该合集的密钥。 |
cards.external_id_taken | POST 请求中的 external_id 已被另一张卡片使用。 | 用 PUT /v1/cards-by-external-id/… 替换它。 |
cards.managed_by_source | 卡片属于某个数据源。 | 在其数据源中更改文档。 |
files.type_mismatch | 文件名、类型和内容不一致。 | 用文件的真实扩展名发送文件。 |
files.too_large | 文件超过上传限制或组织的存储空间。 | 使用分段上传,或释放存储空间。 |
limits.cards | 组织已达到其套餐的卡片数量限额。 | 删除不再需要的卡片,或更换套餐。 |
api.metadata_invalid | metadata 超出限制。 | 参见元数据。 |