Schema与TypeScript导出
本文承接AppServer V2方法注册机制,面向已经理解 typed request/response 和 experimental gate 的读者。本文回答 Rust 协议类型如何生成 TypeScript、JSON Schema 和 stable/experimental artifact,以及 fixture 为什么需要规范化;不把生成文件当作运行时 handler 的替代品。
1. 生成管线
生成过程先从 Rust 类型导出 TS/JSON,再过滤实验方法和字段,最后写入 fixture 与压缩的预计算 exports。公开 schema 是构建产物,源码和 runtime handler 仍是行为来源。
2. TypeScript导出
源码位置:codex-rs/app-server-protocol/src/export.rs :: generate_ts_with_options
ClientRequest::export_all_to(out_dir)?;
export_client_responses(out_dir)?;
ClientNotification::export_all_to(out_dir)?;
ServerRequest::export_all_to(out_dir)?;
export_server_responses(out_dir)?;
ServerNotification::export_all_to(out_dir)?;
ServerNotificationEnvelope::export_all_to(out_dir)?;
if !options.experimental_api {
filter_experimental_ts(out_dir)?;
}
if options.generate_indices {
generate_index_ts(out_dir)?;
generate_index_ts(&v2_out_dir)?;
}TS 导出覆盖 request、response、notification 和 envelope,并可生成 root/v2 index。stable 模式会删除实验方法 union arm 和实验字段;experimental_api=true 才保留完整类型。
3. JSON Schema聚合
源码位置:codex-rs/app-server-protocol/src/export.rs :: generate_json_with_experimental
let envelope_emitters = vec![
|d| write_json_schema_with_return::<crate::RequestId>(d, "RequestId"),
|d| write_json_schema_with_return::<crate::JSONRPCMessage>(d, "JSONRPCMessage"),
|d| write_json_schema_with_return::<crate::ClientRequest>(d, "ClientRequest"),
|d| write_json_schema_with_return::<crate::ServerRequest>(d, "ServerRequest"),
];
let mut schemas = Vec::new();
for emit in &envelope_emitters { schemas.push(emit(out_dir)?); }
schemas.extend(export_client_param_schemas(out_dir)?);
schemas.extend(export_client_response_schemas(out_dir)?);
schemas.extend(export_server_param_schemas(out_dir)?);
schemas.extend(export_server_response_schemas(out_dir)?);
let mut bundle = build_schema_bundle(schemas)?;
if !experimental_api { filter_experimental_schema(&mut bundle)?; }
write_pretty_json(out_dir.join("codex_app_server_protocol.schemas.json"), &bundle)?;JSON bundle 由 envelope、参数、响应和通知 schema 合并而成;还会生成 flat V2 bundle。V1 allowlist 和实验过滤决定哪些类型进入公开文件。
4. 实验过滤
源码位置:codex-rs/app-server-protocol/src/export.rs :: filter_experimental_ts、filter_experimental_schema
filter_request_ts(out_dir, "ClientRequest.ts", EXPERIMENTAL_CLIENT_METHODS)?;
filter_request_ts(out_dir, "ServerRequest.ts", EXPERIMENTAL_SERVER_METHODS)?;
filter_experimental_type_fields_ts(out_dir, ®istered_fields)?;
remove_generated_type_files(out_dir, &experimental_method_types, "ts")?;过滤不仅删除文件,还会删 request union arm 和类型字段;因此 stable TS 与 experimental TS 不是同一文件的不同注释版本。运行时 gate 仍需单独检查。
5. 预计算导出
源码位置:codex-rs/app-server-protocol/src/precomputed_exports.rs :: generate_types、generate_ts_with_options、generate_json_with_experimental、load_exports
pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> {
generate_ts(out_dir, prettier)?;
generate_json(out_dir)
}
pub fn generate_ts_with_options(
out_dir: &Path,
prettier: Option<&Path>,
options: GenerateTsOptions,
) -> Result<()> {
let export_set = if options.experimental_api {
ExportSet::Experimental
} else {
ExportSet::Stable
};
let exports = load_exports(export_set)?;
let ts_files = write_typescript_exports(out_dir, &exports.typescript, options)?;
if options.run_prettier && let Some(prettier_bin) = prettier && !ts_files.is_empty() {
let status = Command::new(prettier_bin)
.arg("--write")
.arg("--log-level")
.arg("warn")
.args(ts_files.iter().map(PathBuf::as_path))
.status()?;
if !status.success() {
bail!("Prettier failed with status {status}");
}
}
trim_trailing_whitespace_in_ts_files(&ts_files)
}
fn load_exports(export_set: ExportSet) -> Result<PrecomputedExports> {
let compressed = match export_set {
ExportSet::Stable => STABLE_EXPORTS,
ExportSet::Experimental => EXPERIMENTAL_EXPORTS,
};
let bytes = zstd::stream::decode_all(Cursor::new(compressed))?;
serde_json::from_slice(&bytes).context("decode precomputed app-server protocol exports")
}运行时生成 API 与读取预计算 exports 是两条路径:export.rs 从 Rust 类型现场生成,precomputed_exports.rs 从压缩内嵌 bytes 解压并写盘。两者必须通过 fixture 测试保持一致;否则发布包可能携带旧 TS/JSON,而源码已经生成了新契约。
6. Fixture写入
源码位置:codex-rs/app-server-protocol/src/schema_fixtures.rs :: write_schema_fixtures_with_options
ensure_empty_dir(&typescript_out_dir)?;
generate_ts_with_options(&typescript_out_dir, prettier, GenerateTsOptions::default())?;
generate_json_with_experimental(&json_out_dir, false)?;
generate_internal_json_schema(internal_dir.path())?;
let exports = PrecomputedExports {
typescript: collect_export_files_recursive(&typescript_out_dir)?,
json_schema: collect_export_files_recursive(&json_out_dir)?,
internal_json_schema: collect_export_files_recursive(internal_dir.path())?,
};
write_precomputed_exports(schema_root, "stable", &exports)?;fixture 写入先清空输出目录,再生成 stable TS/JSON 和 internal schema,最后压缩成预计算 artifact。experimental 模式使用独立临时目录,避免污染 stable fixture。
7. 稳定排序
源码位置:codex-rs/app-server-protocol/src/schema_fixtures.rs :: canonicalize_json、schema_array_item_sort_key
fn canonicalize_json(value: &Value) -> Value {
match value {
Value::Array(items) => {
let items = items.iter().map(canonicalize_json).collect::<Vec<_>>();
let mut sortable = Vec::with_capacity(items.len());
for item in &items {
let Some(key) = schema_array_item_sort_key(item) else {
return Value::Array(items);
};
let stable = serde_json::to_string(item).unwrap_or_default();
sortable.push((key, stable));
}
let mut items = items.into_iter().zip(sortable).collect::<Vec<_>>();
items.sort_by(|(_, left), (_, right)| left.cmp(right));
Value::Array(items.into_iter().map(|(item, _)| item).collect())
}
Value::Object(map) => {
let mut entries: Vec<_> = map.iter().collect();
entries.sort_by_key(|(key, _)| *key);
let mut sorted = Map::with_capacity(map.len());
for (key, child) in entries {
sorted.insert(key.clone(), canonicalize_json(child));
}
Value::Object(sorted)
}
_ => value.clone(),
}
}规范化只用于 fixture 比较:对象键排序,只有每个数组元素都能得到稳定 key 时才排序数组;无法安全判断顺序是否无关的数组会保留原顺序。它解决的是平台和 HashMap 顺序差异,不会改变生成器或运行时 schema 的语义。
fixture 比较会对 schema 数组做规范化排序,消除 HashMap 顺序和平台差异;这只稳定表示,不改变 runtime 类型语义。无法推导稳定 sort key 的元素会保留序列化字符串作为次级 key。
8. 失败与边界
生成失败可能来自 Rust 类型导出、Prettier、schema 聚合或 fixture 写入;stable 过滤成功也不代表运行时会接受该字段。预计算文件过期时,客户端可能看到旧契约,而 server 已经运行新 handler,必须通过 fixture diff 识别。
9. 源码验证
precomputed_exports_are_written_to_disk 比较 freshly generated TS/JSON 与预计算 exports;schema_fixtures 测试验证 canonicalization 和实验过滤;导出选项测试验证 header、index、Prettier 和 experimental_api 行为。它们证明生成可复现和 artifact 一致,不证明 schema 能覆盖运行时所有条件分支。
源码位置:
codex-rs/app-server-protocol/src/precomputed_exports_tests.rs :: precomputed_exports_are_written_to_diskcodex-rs/app-server-protocol/src/schema_fixtures.rs :: canonicalize_jsoncodex-rs/app-server-protocol/src/export.rs :: GenerateTsOptions
cd codex-rs
cargo test -p codex-app-server-protocol precomputed_exports
cargo test -p codex-app-server-protocol schema_fixtures
cargo test -p codex-app-server-protocol export_options10. 契约排查
遇到客户端类型缺少方法,检查 stable experimental filter 和生成 index;遇到 schema 与 server 行为不一致,比较 fixture 与当前源码;遇到实验字段被接受或拒绝异常,分别检查 runtime gate 和导出 artifact。生成文件是契约快照,不是行为实现。
