面向:准备接入 ClavisSinica.org 十一项中文语义能力的第三方开发者
版本:2026-09-10
基址:https://clavissinica.org
接一次,拿十一个能力。 十一项服务共用同一个端点、同一套认证、同一种请求形态、同一套错误码和计费规则。你需要写的分支只有一个:同步返回结果,还是提交后轮询。
POST /v1/chinese/run ← 唯一的业务端点
Authorization: Bearer ck-live_...
{"service_code": "<九选一>", "payload": {...}}
不要在你的代码里硬编码字段规格。 字段名、类型、长度上限、枚举取值、必填与否,全部在 GET /v1/chinese/services 里,零费用、任意有效 Key 可读。它由服务端的同一份定义生成——引擎校验用的就是它。启动时拉一次,比抄一份到本地可靠。
这不是客套话。产品站曾经维护过一份本地手抄的规格表,累计出过五处错误,每一处的根因都一样:规格只存在于服务端的代码里,而本地那份没人会提醒你更新。接上这个接口后他们删掉了本地三张表。
GET /v1/chinese/examples 索引,一次拉全部
GET /v1/chinese/services/{service_code}/example 单个服务
需要 Key,零 ckTok(读的是预先生成的文件,不调引擎)。
每份样例是一次真实调用的完整请求与响应,外加 response_fields——从真实值派生的字段名、类型、是否可空、数组元素的形状。
几个服务给了多个 variant,因为单个样本会隐藏差异:decompose.english_morph 有一个词的 parts 是空数组、一个词的 gloss 是空串、一个是完整形态。只探一个词,前两种都发现不了。
把样例当测试夹具用,不要手写——手写的夹具会和真实响应漂开,而那种失配不报错,只是渲染不出来。
两步,因为一步式发 Key 存在账户接管风险。
# ① 申请 —— 返回 202,不返回 Key
curl -X POST https://clavissinica.org/v1/keys \
-H 'Content-Type: application/json' \
-d '{"email": "you@example.com", "env": "live"}'
# {"ok":true,"status":"verification_sent","expires_in":900}
# ② 点邮件里的链接(15 分钟内有效,只能用一次)
# Key 只在确认响应里出现这一次,服务端只存哈希,取不回来
ck-live_(连字符,不是下划线)。ck-test_ 是沙盒 Key,不扣真实 ckTokpk_live_ 前缀不能用在这里,会返回明确的 wrong_key_prefixAuthorization: Bearer ck-live_xxxxxxxx
只有这一种方式。 没有 X-API-Key 头,没有查询参数。用错了会返回 401 missing_or_invalid_authorization。
payload{
"service_code": "decompose.idiom",
"payload": {"idiom": "守株待兔"},
"idempotency_key": "your-id-001" // 可选
}
payload 对十一个服务全部有效。 键名就是 /v1/chinese/services 里 fields[].name 的值。
另有一种快捷写法 {"input": "..."},只能设置单字段服务的主字段。它存在的唯一理由是历史兼容——如果你在新接入,直接忽略它,统一走 payload 就不会遇到「这个服务能不能用 input」这类判断。
顶层只接受四个键:service_code、input、payload、idempotency_key。其余的会被静默丢弃,但响应里会带一个 warnings 数组告诉你丢了什么:
"warnings": ["请求顶层的这些字段不被支持, 已忽略: explain_lang。..."]
建议把 warnings 打进日志。 有个接入方在顶层发了几个月的 explain_lang,它一直被丢弃,调用一切正常、返回一切正常,没有任何信号——这个 warnings 就是为那件事加的。
/v1/chinese/services 里每个服务都有 execution.mode。
直接返回结果,通常 0.1–1.5 秒。
{
"ok": true,
"service_code": "decompose.idiom",
"data": { ... },
"provenance": {"type": "lookup", "generated_fields": []},
"cktok_charged": 10,
"replayed": false,
"test_mode": false
}
decompose.lesson / decompose.wenyan_lesson)这两项要跑 45–90 秒,不适合挂住 HTTP 连接。
// 提交立即返回
{"ok": true, "task_id": "task_xxx", "status": "pending", "poll_url": "/v1/chinese/tasks/task_xxx"}
然后每几秒轮询一次 GET /v1/chinese/tasks/{task_id},直到 status 是 done 或 error。
provenance:分清查表还是生成每个响应都带:
"provenance": {
"type": "lookup",
"generated_fields": [],
"data_version": "b09a900c0dcb",
"code_version": "4cf2acc3a0d4"
}
type: "lookup" — 结果来自词典库,确定性的,同样输入永远同样输出type: "generative" — 有 LLM 参与,generated_fields 列出 data 里哪些顶层键是实时生成的如果你要把结果存成权威内容,先读这个字段。 lookup 的结果可以直接引用;generative 的应当注明来源或经人工复核。
decompose.lesson 的七层里,六层是生成的,字层不是——意符、声符、笔顺、本义分别来自不同的结构化数据源,本义溯源到 11,114 条古典字库中的具体条目。
拿同一个字去问任何模型,再对一次这里的返回,差别自己就出来了。
响应里另外带两个指纹,各盖各的:
data_version —— 底下那七个字库。它们变化稀少,所以这个号一变就值得处理。你要引用的内容,就存这个号。code_version —— API 层与语言引擎模块。每次部署都可能变。号变了的意思是「有可能影响这个结果的东西动过了」,不是「你的答案变了」。 这里是刻意多报的:误报「变了」最多让你多取一次;误报「没变」会让你拿着已经悄悄漂掉的内容继续引用。两个号都不是语义化版本号,只能比相等,不能比大小。
GET /v1/chinese/services 的 versions 里也有同样两个号,那个端点零 ckTok,所以定期查一次「有没有变」不花钱。
| 层 | 阈值 | 触发时 |
|---|---|---|
| 每 IP | 1200 次/分钟 | nginx 直接 429 |
| 每账户 | 120 次/分钟 | scope: account_requests_per_minute |
| 每账户在途异步任务 | 3 | scope: account_inflight_tasks |
| 全局在途异步任务 | 20 | scope: global_task_queue_full |
{"detail": {"error": "rate_limited", "scope": "account_inflight_tasks",
"msg": "...", "limit": 3, "retry_after": 60}}
响应带 Retry-After 头,请遵守它,不要紧循环重试。 被限流的请求不扣 ckTok。
按 IP 那层刻意设得宽松——如果你是服务器端接入,你全部客户的流量会从同一个出口 IP 出来,按 IP 限额会让他们互相挤占。真正约束你的是按账户那三层。
不用等到被拒才知道自己还剩多少。每次成功调用都带着:
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 60
Reset 是窗口里最老那次调用还有几秒滑出去,也就是「至少多久会空出一个名额」。异步服务另带 X-RateLimit-Async-Remaining(你自己在途的任务,共 3 个)和 X-RateLimit-Queue-Remaining(全局队列,共 20 个)。429 上也带同样这几个头,和 Retry-After 一起。
浏览器端也读得到这几个头以及 X-ckTok-Balance——它们都列在 Access-Control-Expose-Headers 里。
错误码在 error,人类可读信息在 msg。参数类错误额外带 field 和 expected。
| HTTP | error | 含义 | 该不该重试 |
|---|---|---|---|
| 400 | invalid_param |
你的参数有问题,field 说是哪个 |
改了再发 |
| 400 | no_text |
OCR 没识别到文字 | 换张图 |
| 401 | missing_or_invalid_authorization |
认证头缺失或格式错 | 检查 Bearer |
| 401 | wrong_key_prefix |
用了 pk_live_ 等非 ck- 前缀 |
换 Key |
| 401 | invalid_or_revoked_key |
Key 无效或已吊销 | 换 Key |
| 404 | not_found |
接口正常,内容不在词典里 | 不要重试 |
| 413 | payload_too_large |
超过声明的体积上限 | 压缩后再发 |
| 415 | unsupported_media_type |
图片格式不支持 | 转成 JPEG/PNG/WebP |
| 429 | rate_limited |
见第 6 节 | 按 Retry-After |
| 502 | upstream_error |
连不上/超时,引擎根本没被调到 | 可以重试 |
| 502 | upstream_rejected |
上游返回失败且无法分类 | 谨慎重试 |
| 503 | upstream_unavailable |
上游依赖不可用 | 应该重试 |
注意 502/503 和 4xx 的区别:4xx 是你的请求有问题,重试没用;upstream_error 和 upstream_unavailable 跟你的请求无关,退避后重试是合理的。
扣了钱没拿到结果的,一律自动退款,响应里会带 note 说明。
早期版本有个兜底错误码叫 engine_rejected_input,凡是没带明确状态的错误都归到它名下——包括上游超时这种引擎根本没被调用的情况。有个接入方因此把自己脚本的一个粘贴错误,误判成了「服务端字段名和文档不一致」的结构性问题。
现在的错误码按真实成因分类,不会把服务端的故障说成你的输入有问题。
限流 → 参数校验 → 扣费 → 执行 → 失败自动退款
必填参数缺失、格式不对这类错误在扣费之前就被拒,账本上不会留任何记录。
但有三种例外,它们的检查在引擎内部(也就是扣费之后):图片尺寸过小、体积超限(413)、格式不对(415)。这三种是扣了再退——净扣费为零,但账本上会看到扣款与退款两条记录。做对账时要知道这一点。
每个 /v1/chinese/run 响应都带一个头:
X-ckTok-Balance: 432
不用另外查。建议在这个数字低于你最贵那个服务的单价时就告警——decompose.lesson 是 100,余额剩 80 时它对所有用户都会 402,而你的健康检查可能一片绿。
idempotency_key:同一个键重复提交只扣一次,返回第一次的结果,响应里 replayed: trueidempotency_key 配不同的请求体会返回 409,这是真实的误用,不会静默兼容GET /v1/account/balance / ledger / usage幂等的作用域是账户,不是 Key。 你在重试期间轮换了 Key,幂等仍然有效。
价格与调用形态以 GET /v1/chinese/services 为准,下面是选型参考。
| service_code | ckTok | 模式 | 产出 |
|---|---|---|---|
dict.multilingual |
4 | sync | 一个中文字/词 → 它在各语种里的释义。不是通用翻译 |
convert.script |
2 | 同步 | 简繁转换,四种方向 |
ocr.image |
6 | 同步 | 图片 → 文字 |
decompose.english_morph |
8 | 同步 | 英文词缀拆解、词根家族 |
decompose.idiom |
10 | 同步 | 成语释义、例句、出处引证、HSK 级、感情色彩 |
decompose.wenyan |
12 | 同步 | 逐字本义、说文引证、六书分类、古今义对比 |
decompose.sentence |
15 | 同步 | 分词、拼音、语法讲解、逐词下钻 |
decompose.passage |
20 | 同步 | 可读性判定、生词率、HSK 分布、优先字词、句式清单 |
decompose.passage_deep |
45 | 同步 | 一次调用出篇章七层:主旨脉络、可读性、分词、字表、词表、逐句解读、句式 |
decompose.lesson |
100 | 轮询 | 现代文课文七层教案 |
decompose.wenyan_lesson |
100 | 轮询 | 文言课文七层教案 |
decompose.passage 的 target_level 是 HSK 3.0 等级(1–9),不是学校年级。 按年级换算会让难度判定完全错位,而且不会报错decompose.idiom 返回中英双语(meaning_zh / meaning_en)。其余服务要么没有语言维度,要么固定单语decompose.sentence 的语法讲解默认英文,传 explain_lang: "zh" 出中文。只接受这两个值——上游引擎名义上支持十八种语言,但实测只有少数几种经得起换句子采样,其余的宁可不提供,也不提供一个不稳的。响应里会回显 explain_lang,所以你总知道自己拿到的是哪种。它只影响语法讲解,逐词释义仍然是英文decompose.passage 的 priority_words[].gain 是一句排好版的中文(如「本篇出现 3 次」),不是数值。原样展示,排序用 rankdecompose.passage 的句式分析只看前 40 句,grammar_patterns_coverage 会告诉你截断了多少;其余指标是全文的truncated: true 时所有指标都只反映前 3000 字符,不只是句式ocr.image 一次一张,base64 后上限 6MB,不接受 HEIC(iPhone 默认格式,需转 JPEG)。十页课本是十次调用ck-test_ 前缀,不扣真实 ckTok,每服务 50 次/天(decompose.wenyan_lesson 3 次/天)。适合 CI 和联调POST /v1/demo/run,同样的请求体形态,不需要 Key,10 次/天/IP,开放六项(不含两个异步教案服务、ocr.image、dict.multilingual 和 decompose.passage_deep)import time, requests
BASE = "https://clavissinica.org"
class Clavis:
def __init__(self, key):
self.h = {"Authorization": f"Bearer {key}",
"Content-Type": "application/json"}
# 启动时拉一次规格,不在本地硬编码
self.spec = requests.get(f"{BASE}/v1/chinese/services",
headers=self.h, timeout=30).json()
self.mode = {s["service_code"]: s["execution"]["mode"]
for s in self.spec["services"]}
def run(self, code, payload, idem=None, _tries=0):
body = {"service_code": code, "payload": payload}
if idem:
body["idempotency_key"] = idem
r = requests.post(f"{BASE}/v1/chinese/run", headers=self.h,
json=body, timeout=180)
if r.status_code == 429 and _tries < 5:
time.sleep(int(r.headers.get("Retry-After", 30)))
return self.run(code, payload, idem, _tries + 1)
if r.status_code in (502, 503) and _tries < 3:
time.sleep(2 ** _tries)
return self.run(code, payload, idem, _tries + 1)
r.raise_for_status()
out = r.json()
for w in out.get("warnings", []):
print("WARN", w) # 别忽略这个
if self.mode.get(code) == "submit_poll":
return self._poll(out["task_id"])
return out
def _poll(self, task_id, timeout=300):
deadline = time.time() + timeout
while time.time() < deadline:
time.sleep(5)
t = requests.get(f"{BASE}/v1/chinese/tasks/{task_id}",
headers=self.h, timeout=30).json()
if t.get("status") in ("done", "error"):
return t
raise TimeoutError(task_id)
也可以用官方客户端,行为与上面这段完全一致:
pip install clavis-sinica
npm install @clavis-sinica-org/sdk
两个 registry 的命名习惯不同(PyPI 没有 scope 概念),是同一套 API。客户端替你做的就是上面这些:启动拉规格、429 按 Retry-After 退避、5xx 指数退避、异步自动轮询、打印 warnings。直接调 HTTP 同样受支持——客户端没有做任何你自己做不到的事。
写在这里而不是等你自己撞上:
lookup 在数据的某个状态下是确定性的,而字典库更新会改变那个状态;你没法要求「按去年三月的样子回答我」。你能拿到的是 data_version(见 §5)——把它和你引用的内容存在一起,至少知道脚下是什么时候动的。service_code 白名单。设了预算之后,每个响应都带 X-ckTok-Daily-Limit 和 X-ckTok-Daily-Remaining;超出返回 429 并在 Retry-After 里给出到 UTC 零点的秒数,调白名单之外的服务返回 403。还缺的是自助——没有让你自己设置的界面,需要找我们开。沙盒 ck-test_ Key 不计费,预算对它无效;服务范围对它有效。dict.multilingual 的 glosses 对主要语种可用,个别语种(如日语)的源数据存在断词。这不是截断,是词库本身。status.json 里每次实测的时间戳,不要假设一个固定频率。探针跑在 GitHub 的机器上、不在我们的服务器上——我们挂了它照常报告。页面上只有测出来的数字,连"从哪天开始测的"也一并写着,短历史就显示成短历史。GET /v1/chinese/services(零费用,唯一权威)/status.json)pk_live_ 前缀,两边不通用