服务与 Contract
发布可复用的能力,让其他 Curio 通过带版本的契约调用它。
核对日期:2026 年 9 月 11 日
本页目录
声明服务提供方#
公共服务由 Curio Backend 实现,是带版本的可复用能力。示例使用提供方 ID dev.example.text-tools 和服务 ID dev.example.text-tools。这些名称用于演示,不代表系统已安装该服务。
在提供方 Manifest 的真实 Backend 配置旁合并以下片段:
{
"services": {
"provides": [{
"id": "dev.example.text-tools",
"version": "0.0.1-alpha.1",
"contract": "contracts/text-tools.json"
}]
}
}
服务 ID 必须位于提供方 Curio 命名空间中。把 Contract 和可执行文件放入 package.files,进程与协议配置见 Backend 接入。
编写 Rust 合约,导出 JSON#
Rust 是作者维护的唯一合约源,JSON Schema 是交给 Host、CLI 和生成器读取的产物。 不需要手写 JSON Schema,也不需要维护第二份 TypeScript 接口。
Rust Backend 使用实际处理函数的输入、输出类型注册方法,由 Backend Kit 导出,见 Backend 示例。非 Rust Backend 可以把声明放在独立 Rust 导出器中;下面是 contract/src/main.rs,只声明 uppercase 方法与 progress 事件,不包含业务实现:
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use trove_contract::ServiceContract;
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
struct Input { text: String }
#[derive(Serialize, JsonSchema)]
struct Output { text: String }
#[derive(Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct ProgressParams { task_id: String }
#[derive(Serialize, JsonSchema)]
struct Progress { progress: f64 }
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut contract = ServiceContract::new(
"dev.example.text-tools", "0.0.1-alpha.1",
)
.method::<Input, Output>("uppercase")
.event::<ProgressParams, Progress>("progress");
let uppercase = contract.methods.get_mut("uppercase").unwrap();
uppercase.timeout_ms = Some(5_000);
uppercase.idempotent = true;
contract.events.get_mut("progress").unwrap().coalesce = "latest".into();
std::fs::create_dir_all("contracts")?;
std::fs::write(
"contracts/text-tools.json",
serde_json::to_vec_pretty(&contract)?,
)?;
Ok(())
}
独立导出器的 contract/Cargo.toml:
[package]
name = "text-tools-contract"
version = "0.0.1-alpha.1"
edition = "2021"
[dependencies]
trove-contract = { path = "../vendor/trove-backend-0.0.1-alpha.1/contract" }
schemars = "=1.2.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
在 Curio 根目录更新套件,再导出:
trove kit update --backend
cargo run --manifest-path contract/Cargo.toml
把导出命令加入 bindings.generate。导出的 contracts/text-tools.json 随包分发,消费者据此生成可选的客户端。合约导出器没有 Host、Wire 运行时或 TypeScript 解析器依赖;业务 Backend 仍可使用 Python、Go、JavaScript 或其他语言,只需实现相同的 Wire 请求与生命周期。
输入 Schema 按 Rust 的反序列化规则生成;结果与事件载荷按序列化规则生成。因此,输入字段的默认值、可省略字段,以及输出中的显式 null 不会被混成同一套规则。字段约束和文档写在 Rust 类型上,生成的 JSON 不手工修改。
CLI 校验契约结构;默认 Host 检查已注册方法和主题,完整业务 JSON Schema 校验并非默认开启。提供方仍须验证输入并实现声明的行为。idempotent 不会开启自动重试;service/version 须与 services.provides 一致。
声明调用方依赖#
将提供者的真实契约保存到消费方工程,合并以下 Manifest 片段:
{
"services": {
"requires": [{ "id": "dev.example.text-tools", "version": ">=0.0.1-alpha.1 <0.0.2", "optional": false }]
},
"bindings": {
"services": [{
"curio": "dev.example.text-tools",
"contract": "contracts/text-tools.json",
"output": "src/generated/text-tools.ts"
}]
}
}
运行 trove generate 后,在 src/ 中调用:
import { host } from "@trove/plugin-sdk";
const TextTools = await host.curios.get("dev.example.text-tools");
const result = await TextTools.uppercase({ text: "hello" });
console.log(result.text);
get 的参数是 Curio ID。它检查安装、启用、公开能力和已声明的版本要求,然后返回引用;不会启动进程。方法调用和订阅才由 Host 启动目标 Backend,等待 ready 后转发,始终不打开 UI。同一提供者的并发调用共享一次启动。
需要 A、B 等多个引用时,分别按 ID 调用 get,可用 Promise.all 并行准备。未安装在 get 阶段报 curio.not_installed;禁用报 curio.disabled;没有公开能力报 curio.no_public_services。需要提前启动时用 host.curios.start(id)。
services.requires 按服务 ID 声明兼容范围;不会自动安装依赖,也不是访问许可白名单。声明 optional 表示作者设计了缺失时的降级方式,不会把失败调用变成成功。
生成 TypeScript#
运行 trove generate,然后 trove check。generate 执行可选的 bindings.generate 导出器、验证契约并生成文件;dev、pack 自动做同样的准备,check 只读检查,过期时报 bindings.stale。
每条 bindings.services 都生成独立的 createClient / Client / serviceId。设置 curio 后还生成 src/generated/curios.d.ts,为 SDK 的 CurioRegistry 增补 ID → API 类型映射;可用 bindings.curios 自定义声明文件路径。确保该文件在 tsconfig 的 include 范围中。无需手动注册或导入一个运行时注册表。
服务 ID 等于 Curio ID 时,方法直接位于 TextTools.uppercase();服务 ID 为 dev.example.text-tools.files 时,挂在 TextTools.files.xxx()。多个服务组合成同一个 Curio 引用。事件位于各服务的 events 下。命名空间段不能使用 then/events,服务方法不能命名 then/events,方法名不能与子服务名冲突;get 对歧义报 curio.ambiguous_api。这些是该便利引用 API 的限制,单独的契约客户端仍可使用原始服务名。
也可以手写接口并使用 host.curios.get<MyApi>(id);泛型只是编译期声明,不会验证远端兼容性。建议从提供者获取契约并声明 requires。原有独立 createClient 仍可用于按服务组合;它本身不执行按 Curio ID 的检查。私有生成客户端映射 @self,命名和采用与否由作者决定。
有明确需求再加入事件#
订阅参数与事件载荷分开声明。coalesce 可写 none 或 latest,但当前实现不保证自动合并事件;提供者自行实现节流。任务结束时释放订阅:
import { host } from "@trove/plugin-sdk";
const TextTools = await host.curios.get("dev.example.text-tools");
const subscription = await TextTools.events.progress(
{ taskId: "preview" },
event => console.log(event.progress),
);
await subscription.close();
console.log(await subscription.closed);
Rust Kit 用 .event::<Params, Payload>("progress") 声明主题。ctx.emit_for("progress", params, payload) 仅投递给调用方、服务、主题及 JSON 参数完全匹配的订阅。空参数主题可用 ctx.emit。业务事件与 subscription.closed 结束原因分离。跨语言的完整报文见订阅协议。