开发者教程 · REST 与 MCP
同步你的知识库
继续在你现有的系统中写作,并把变更发送到 Sonny。本教程会创建一个“账单”分类,同步一篇草稿文章,然后将其发布给一个已验证的客户受众。
1. 选择渠道并创建密钥
- 打开渠道 → 你的渠道。复制其后台 URL 中 /app/sources/ 之后的 ID。API 把这个渠道 ID 称为 sourceId;它不同于聊天组件的 siteId,也不同于帮助中心的公开 slug。
- 在设置 → 开发者 → API 密钥 → 创建密钥中,授予 kb:read 和 kb:write。将渠道访问权限限制为你要同步的渠道。把只显示一次的密钥保存到服务器的密钥管理工具中。
- 在你的后端环境中设置以下变量。这些示例使用 curl;请将大写的 ID 替换为 Sonny 返回的值。同步正式的帮助中心之前,请先在测试渠道上运行。
# Load SONNY_API_KEY from your server's secret manager first.
# SOURCE_ID is the ID in the channel dashboard URL: /app/sources/SOURCE_ID
export SOURCE_ID="YOUR_SOURCE_ID"
export BASE="https://www.usesonny.com/api/v1/sources/$SOURCE_ID"可选的来源查询:GET /api/v1/sources 和 MCP 工具 list_sources 会返回可访问的渠道,需要 conversations:read。如果你已经有渠道 ID,就不需要这个权限范围。连接域名仍需在后台完成。
完整的 API 密钥设置2. 创建或更新分类
在 URL 中使用来源系统中稳定的 ID。再次发送相同的外部 ID 会更新已有分类,而不是创建重复项。首次创建会自动初始化此渠道的帮助中心。
curl --fail-with-body -X PUT "$BASE/categories/by-external-id/billing" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Billing","position":0}'创建时返回 201,更新时返回 200,分类位于 data 下。需要 Sonny 内部的分类 ID 时,请保存 data.id。对外部 ID 进行 URL 编码;即使标题变化,也要保持它们稳定。
3. 将文章同步为草稿
curl --fail-with-body -X PUT "$BASE/articles/by-external-id/billing-guide" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Manage billing","body":"## Find your invoices\n\nOpen **Settings → Billing** in your account.","bodyFormat":"markdown","categoryExternalId":"billing","status":"draft"}'响应包含 data.id,以及 data.body 中经过清理的 HTML。在“渠道 → 你的渠道 → 帮助中心”中打开文章检查格式。Markdown 会被转换为 HTML;也可以通过 bodyFormat: "html"(默认值)直接提交原始 HTML。文章标题与正文中的标题是分开的。
使用 categoryExternalId 引用第 2 步中的分类,或使用 categoryId 引用其 Sonny ID。只能提供其中一个。将任一字段设为 null 即可取消文章的分类。每次 upsert 都需要 title 和 body;部分修改可使用 PATCH。省略 status 时,新文章默认为草稿。
4. 设置访问权限,然后发布
在这个私密帮助中心的示例中,请先连接已验证客户登录。创建受众并保存返回的 data.id:
curl --fail-with-body -X POST "$BASE/audiences" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Pro customers","rules":{"match":"all","conditions":[{"trait":"plan","op":"in","value":["pro","business"]}]}}'替换下面的 AUDIENCE_ID、ARTICLE_ID 和登录 URL。第一个请求会让帮助中心上线并仅限 Pro 客户访问;第二个请求发布文章。你的应用登录流程必须与帮助中心交换已签名的客户 JWT;仅设置 signInUrl 并不会让任何人登录。
curl --fail-with-body -X PATCH "$BASE/help-center" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled":true,"title":"Acme Help Center","audience":"verified","audienceId":"AUDIENCE_ID","signInUrl":"https://app.example.com/login"}'
# Replace ARTICLE_ID and AUDIENCE_ID with the returned data.id values.
curl --fail-with-body -X PATCH "$BASE/articles/ARTICLE_ID" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"audienceId":"AUDIENCE_ID","status":"published"}'使用 GET /help-center 返回的 URL,分别测试已登录读者视图和未登录的浏览器。API 密钥和 MCP 以员工身份操作,可以在其权限范围内读取内容;它们的请求成功并不能证明客户能看到这篇文章。
对于公开帮助中心,请在帮助中心和文章上使用 audience: "everyone" 和 audienceId: null,并检查分类的访问权限。若只想移除某个指定群组,请发送 audienceId: null;除非你显式更改 audience,否则“仅限已验证”的访问限制仍会保留。省略 audienceId 会保留之前的设置。帮助中心、分类和文章的所有限制都必须满足。
命名规则使用 match: "all" 或 "any",最多 10 个条件,值的类型严格校验。运算符:eq、neq、in、not_in、exists、not_exists、gt、gte、lt、lte。数值比较使用数字;in/not_in 使用包含 1–100 个标量值的数组;exists/not_exists 省略 value。缺少特征时比较不成立。查看规则行为、继承和访问权限排查。
5. 保持同步最新
重复写入而不产生重复内容
每当来源内容变化时,使用相同的外部 ID 重复执行 PUT。外部 ID 在一个帮助中心内唯一。更新时省略的字段会保留当前值。之后的 upsert 中省略 status 可保留发布状态;显式发送 draft 会撤回已发布的文章。publishedAt 在首次发布时设置,之后保持不变。它是只读字段,因此无法导入历史发布日期。内容未变化或仅修改元数据时,不会触发不必要的重新向量化。
读取变更和分析数据
curl --fail-with-body --get "$BASE/articles" \
-H "Authorization: Bearer $SONNY_API_KEY" \
--data-urlencode "page=1" --data-urlencode "limit=100" \
--data-urlencode "status=published" \
--data-urlencode "updatedSince=2026-09-01T00:00:00Z"
# Get the full sanitized HTML and read-only analytics for one article.
curl --fail-with-body "$BASE/articles/ARTICLE_ID" \
-H "Authorization: Bearer $SONNY_API_KEY"文章和分类列表返回 data 以及 pagination:page、limit、total。页码从 1 开始;limit 默认 50,最大 100。保持相同的筛选条件逐页请求,直到 page × limit 达到 total。两种列表都接受 externalId 和 updatedSince;文章还接受 categoryId 和 status(draft 或 published)。updatedSince 请使用带时区的 ISO 时间戳;它包含在该时间更新的记录。
读取每篇文章的详情可获得完整的 body、viewCount、helpfulYes 和 helpfulNo。这些分析数据是只读的。updatedSince 列出的是当前记录,不会报告删除。请在来源系统中跟踪删除,并在需要时显式删除对应的 Sonny 记录。
上传图片
curl --fail-with-body -X POST "$BASE/media" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-F "file=@billing.png;type=image/png"在 Markdown 图片语法或 HTML img 元素中使用返回的 data.url,然后同步文章正文。上传支持最大 25 MB 的 JPEG、PNG、GIF 和 WebP,且要求帮助中心已初始化。上传后的 URL 是公开的;文章的受众限制不会保护图片 URL。MCP 使用 upload_kb_media,传入 sourceId、fileName、contentType 和文件的 dataBase64,而不是 multipart 表单数据。
排列文章和分类
curl --fail-with-body -X PUT "$BASE/articles/reorder" \
-H "Authorization: Bearer $SONNY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ids":["ARTICLE_ID_1","ARTICLE_ID_2"]}'先在不带状态、分类或日期筛选的情况下获取所有页面,包括草稿。将示例 ID 替换为每个当前文章 ID,每个恰好出现一次,并按期望的顺序排列。完整的分类列表请使用 /categories/reorder。重新排序成功会返回 204,且没有响应体。如果列表已变化,或包含外来、重复或缺失的 ID,请刷新列表后再重试。你也可以在单独的写入中设置非负的 position。
通过 MCP 使用同样的流程
- 连接你的 MCP 客户端并选择正确的工作区。确认它拥有 kb:read 和 kb:write,并且可以访问你的渠道。
- 使用后台中的渠道 ID;如果你的凭据还有 conversations:read,也可以调用 list_sources。切勿用聊天组件的 siteId 代替。
- 按顺序运行下面前五个示例。将 SOURCE_ID 替换为你的渠道 ID,将 ARTICLE_ID/AUDIENCE_ID 替换为之前各结果中的 data.id。发布前请替换登录 URL 并连接登录。
- 使用 list_kb_articles 查看变更。只有在收集到包括草稿在内的完整、未筛选列表后,才能执行重新排序。写入成功后请刷新列表。
每个示例都显示了工具名称及其参数。upsert 的字段直接放在参数中;update 类工具则把变更的字段放在 updates 中。这些是工具调用的输入,而不是发往 MCP 端点的 HTTP 请求。
1. upsert_kb_category
{
"name": "upsert_kb_category",
"arguments": {
"sourceId": "SOURCE_ID",
"externalId": "billing",
"name": "Billing",
"position": 0
}
}2. upsert_kb_article
{
"name": "upsert_kb_article",
"arguments": {
"sourceId": "SOURCE_ID",
"externalId": "billing-guide",
"title": "Manage billing",
"body": "## Find your invoices\n\nOpen **Settings → Billing** in your account.",
"bodyFormat": "markdown",
"categoryExternalId": "billing",
"status": "draft"
}
}3. create_kb_audience
{
"name": "create_kb_audience",
"arguments": {
"sourceId": "SOURCE_ID",
"name": "Pro customers",
"rules": {
"match": "all",
"conditions": [
{
"trait": "plan",
"op": "in",
"value": [
"pro",
"business"
]
}
]
}
}
}4. update_help_center
{
"name": "update_help_center",
"arguments": {
"sourceId": "SOURCE_ID",
"updates": {
"enabled": true,
"title": "Acme Help Center",
"audience": "verified",
"audienceId": "AUDIENCE_ID",
"signInUrl": "https://app.example.com/login"
}
}
}5. update_kb_article
{
"name": "update_kb_article",
"arguments": {
"sourceId": "SOURCE_ID",
"articleId": "ARTICLE_ID",
"updates": {
"audience": "verified",
"audienceId": "AUDIENCE_ID",
"status": "published"
}
}
}6. list_kb_articles
{
"name": "list_kb_articles",
"arguments": {
"sourceId": "SOURCE_ID",
"page": 1,
"limit": 100,
"updatedSince": "2026-09-01T00:00:00Z"
}
}7. reorder_kb_articles
{
"name": "reorder_kb_articles",
"arguments": {
"sourceId": "SOURCE_ID",
"ids": [
"ARTICLE_ID_1",
"ARTICLE_ID_2"
]
}
}8. update_kb_audience
{
"name": "update_kb_audience",
"arguments": {
"sourceId": "SOURCE_ID",
"audienceId": "AUDIENCE_ID",
"updates": {
"name": "Paid customers"
}
}
}相关操作
以下 REST 路径均相对于 BASE。每个字段和响应结构请参阅 API 参考。
| 任务 | REST | MCP |
|---|---|---|
| 列出 / 获取文章 | GET /articles · GET /articles/{articleId} | list_kb_articles · get_kb_article |
| 列出 / 获取分类 | GET /categories · GET /categories/{categoryId} | list_kb_categories · get_kb_category |
| 不带外部 ID 创建 | POST /articles · POST /categories | create_kb_article · create_kb_category |
| 编辑已有内容 | PATCH /articles/{articleId} · PATCH /categories/{categoryId} | update_kb_article · update_kb_category |
| 排列分类 | PUT /categories/reorder | reorder_kb_categories |
| 读取 / 编辑帮助中心设置 | GET /help-center · PATCH /help-center | get_help_center · update_help_center |
| 列出 / 编辑受众 | GET /audiences · PATCH /audiences/{audienceId} | list_kb_audiences · update_kb_audience |
| 删除 | DELETE /articles/{articleId} · /categories/{categoryId} · /audiences/{audienceId} | delete_kb_article · delete_kb_category · delete_kb_audience |
删除文章是永久性的。删除分类会让其中的文章变为未分类,并移除该分类继承下来的限制。删除受众会移除其特征规则,关联内容仍保持“仅限已验证客户”。删除前请检查受影响的访问权限;如果想保留文章,请改用草稿来撤回。
同步故障排查
- 400 · 请求无效
- 检查必填的 title/body 或 name、字段名是否完全正确、规则类型是否有效,以及重新排序的 ID 是否完整。categoryId 和 categoryExternalId 只能发送一个。
- 401 · 未授权
- 请提供有效的 Bearer 密钥,并检查它是否已过期或被撤销。
- 403 · 禁止访问
- 检查密钥的权限范围、密钥所有者的权限和工作区账单状态。如果 MCP 客户端的工具是只读的,请用所需的权限重新连接。
- 404 · 未找到
- 确认该来源属于所选工作区,且凭据允许访问。新的帮助中心需要先创建内容才能初始化;读取操作不会创建它。
- 409 · 冲突
- 重新获取当前内容,处理冲突的 ID 或过时的排序后再重试。
- 413 · 内容过大
- 将图片缩小到 25 MB 限制以下。
- 429 · 请求过于频繁
- 等待 Retry-After 指定的时间后再重试。使用 RateLimit-Limit、RateLimit-Remaining 和 RateLimit-Reset 控制请求节奏。