跳转到内容

JSON Schema 契约

每个 agent-facing 的 mailagent 命令都有一份机器可读的 JSON Schema 契约。schema 是 agent 调用 CLI 的稳定接口:先读 schema 知道字段形状,再写解析代码,而不是靠”看一眼输出猜结构”。

契约文件落在仓库的 docs/cli-schema/ 目录(与 CLI 源码同仓),共 56 个文件:1 个通用结构 _common.schema.json + 1 个 error-codes.md + 54 个命令级 <command>.schema.json。每个 schema 都是标准 JSON Schema(带 $schema / required / enum / additionalProperties: false),可直接喂给任意 JSON Schema 校验器。

docs/cli-schema/
├── _common.schema.json # wrapper / error / meta 的 $defs(被所有命令 $ref)
├── error-codes.md # 全部 error.code enum + exit_code + 触发场景
├── email-get.schema.json
├── email-list.schema.json
├── email-body.schema.json
├── email-search.schema.json
├── email-resync.schema.json # 单封
├── email-resync-batch.schema.json # 含 partial_failure 形态
├── email-flag.schema.json
├── admin-stats.schema.json
├── admin-stats-v4-rollout.schema.json
├── admin-health.schema.json
├── admin-dead-letter.schema.json
├── llm-run.schema.json
├── llm-stats.schema.json
└── …(其余 attachment / notion / backfill / init / calendar / debug / project-progress)

所有 -o json 输出顶层永远是一个 object(不会直接吐数组),形如:

{
"status": "success", // enum: success | error | partial_failure
"schema_version": 1, // integer,当前恒为 1
"data": { /* … */ }, // success / partial_failure 用 data
"meta": { "duration_ms": 8 } // 永远有 duration_ms,list 类还有 count/total/limit/offset
}
字段类型说明
statusenumsuccess / error / partial_failure
schema_versioninteger契约版本号,初始 1;breaking change 走 major bump
dataobject | array成功时的业务数据(get 类是 object,list/search 类是 array)
errorobject失败时填,与 data 互斥
metaobject至少含 duration_ms;命令特定字段(如 list 的 total/limit/offset

status: "error"error 字段形如:

{
"status": "error",
"schema_version": 1,
"error": {
"code": "E_NOT_FOUND", // enum,见 error-codes.md
"message": "Email with internal_id=99999 not found", // 人类可读
"hint": "Use 'mailagent email list' to find available IDs", // 可选,下一步提示
"context": { "internal_id": 99999 } // 可选,结构化字段供 agent 解析
},
"meta": { "duration_ms": 5 }
}

error.code 的 enum 集中维护在 docs/cli-schema/error-codes.md,与 exit_code 一一对应:

codeexit_code含义
E_NOT_FOUND1资源不存在
E_INVALID_ARG2参数非法 / 互斥 / 范围超出
E_AUTH_FAILED4写命令缺 API key 或 token 不匹配
E_SCHEMA_MISMATCH5DB schema 不一致 / db_version != expected
E_PARTIAL_FAILURE6batch 命令部分成功部分失败
E_ABORTED7SIGINT/SIGTERM 主动退出(长任务第一次 Ctrl-C)
E_MAX_FAILURES8长任务连续失败超 --max-failures 熔断
E_PM2_RUNNING9PM2 mail-sync 在跑,写命令拒绝
E_LLM_FAILED1LLM gateway 调用失败 / 模型链耗尽 / Notion 写失败
E_INTERNAL1兜底,未匹配上述任何 code 的内部异常
E_NOT_IMPLEMENTED2命令存在但当前为 stub

完整退出码语义见退出码契约

email resync --range / backfill body / init 等长任务部分成功时返回 partial_failure

{
"status": "partial_failure",
"schema_version": 1,
"data": {
"succeeded": [ /* … */ ],
"failed": [
{ "internal_id": 53675, "error": { "code": "E_LLM_FAILED", "message": "" } }
],
"summary": { "total": 100, "succeeded": 87, "failed": 13, "aborted_by": null }
},
"meta": { "duration_ms": 145320 }
}

退出码为 6E_PARTIAL_FAILURE),脚本据此分流”全成功 / 部分成功 / 全失败”。

关键设计约定(写解析器前必读)

Section titled “关键设计约定(写解析器前必读)”
  1. 数字字段恒为整数,不拼字符串llm run 的 token 用量拆成独立的 integer key(input_tokens / output_tokens / cache_creation_input_tokens / cache_read_input_tokens),不会出现 "4521/342" 这种拼接串。
  2. 带 emoji/中文 display 的值给 _key + _label 双字段。如优先级同时给 priority_key: "important"(machine enum)和 priority_label: "🟡 重要"(display)。agent 永远读 _key 分支,不要 parse _label
  3. 时间统一 ISO 8601 含时区"2026-05-15T10:23:45+08:00",不再用纯日期或 epoch(个别内部 fetched_at 仍是 epoch float,schema 里标了类型)。
  4. additionalProperties 行为:wrapper 顶层与 error 结构是封闭的(additionalProperties: false);data 内部按各命令 schema 定义。
schema 文件对应命令形态
email-get.schema.jsonemail getobject(含可选 body/attachments
email-list.schema.jsonemail listarray + meta total/limit/offset
email-body.schema.jsonemail bodyobject {format, content, size_bytes}
email-search.schema.jsonemail searcharray(hit 含 snippet/rank
email-resync.schema.jsonemail resync <id>object {action, attachments_uploaded, …}
email-resync-batch.schema.jsonemail resync --range/--idspartial_failure 形态
email-flag.schema.jsonemail flagobject(outbox intent 落库结果)
attachment-list.schema.jsonattachment listarray
attachment-download.schema.jsonattachment download --destobject 元信息(二进制下载本体不进 JSON)
llm-run.schema.jsonllm runobject {labels, usage, writer_summary}
llm-stats.schema.jsonllm statsobject(status 分布 + cost + cache hit)
llm-selftest.schema.jsonllm selftestobject {healthy, …}
admin-stats.schema.jsonadmin statsobject(多 section,各带 _source
admin-stats-v4-rollout.schema.jsonadmin stats(v4 section)object(含 _snapshot_at/_warn_if_stale_sec
admin-health.schema.jsonadmin healthobject {healthy, checks[]}
admin-db-version.schema.jsonadmin db-versionobject {db_version, expected, compatible}
admin-dead-letter.schema.jsonadmin dead-letter list/retryarray / object
notion-page-orphans.schema.jsonnotion page-orphansarray
notion-file-link-audit.schema.jsonnotion file-link-auditarray / object
backfill-body.schema.jsonbackfill bodypartial_failure 形态
init-*.schema.json(7 个)init {fetch-cache,analyze,fix-properties,fix-critical,update-parents,sync-new,all}object
calendar-recurring-*.schema.jsoncalendar recurring {discover,replay}array / object
debug-*.schema.json(5 个)debug {email-source,mail-structure,inline-images,applescript-fetch,notion-page}object
project-progress-sync.schema.jsonproject-progress syncobject

(完整命令明细见 10 大命令组参考。)

CI 校验:用 jsonschema 把输出钉在契约上

Section titled “CI 校验:用 jsonschema 把输出钉在契约上”

把 CLI 输出和契约绑死,最简单的方式是在 CI 里跑 jsonschemapip install -e ".[dev]" 已带 jsonschema>=4.18 + referencing)。schema 之间用 $ref 指向 _common.schema.json,因此校验时要把整个 docs/cli-schema/ 目录作为 registry 加载。

命令行快速校验单条输出:

Terminal window
# 把一次真实调用的输出存下来,对照契约校验
mailagent -o json email get 53675 > /tmp/out.json
jsonschema --instance /tmp/out.json docs/cli-schema/email-get.schema.json
# 退出码 0 = 符合契约;非 0 = 字段漂移,CI 红

$ref 解析的 Python 校验脚本(CI 用):

# scripts/dev/validate_cli_schema.py(示意)
import json, subprocess, pathlib
from jsonschema import Draft202012Validator
from referencing import Registry, Resource
SCHEMA_DIR = pathlib.Path("docs/cli-schema")
# 把目录里所有 schema 注册进 registry,让 $ref 能解析到 _common.schema.json
registry = Registry().with_resources(
(p.name, Resource.from_contents(json.loads(p.read_text())))
for p in SCHEMA_DIR.glob("*.schema.json")
)
def check(cmd: list[str], schema_file: str) -> None:
out = subprocess.run(
["mailagent", "-o", "json", *cmd],
capture_output=True, text=True, check=True,
).stdout
schema = json.loads((SCHEMA_DIR / schema_file).read_text())
validator = Draft202012Validator(schema, registry=registry)
validator.validate(json.loads(out)) # 不符合即抛 ValidationError
# 用一个 seed 库(非生产库)跑代表性命令
check(["email", "get", "1"], "email-get.schema.json")
check(["admin", "health"], "admin-health.schema.json")
check(["email", "search", "test"], "email-search.schema.json")
  • 加字段:additive,schema_version 不变(agent 应对未知新字段宽容)。
  • 删字段 / 改字段类型 / 改 enum 取值:breaking,必须 bump schema_version,同步更新 docs/cli-schema/<command>.schema.json + error-codes.md + src/cli/exceptions.py
  • 新增命令:新落一个 <command>.schema.json + 在 error-codes.md 追加可能的 E_*