# 插件契约
平台与插件之间只有一条 RPC。这份文档说清它的每个字段是什么意思,以及
哪些约定是**强制的**(违反了会被拒),哪些只是建议。
## 1. 一条 RPC
```protobuf
service ExtensionPlugin {
rpc Call(GenericRequest) returns (GenericResponse);
}
```
平台侧只认两个概念:**能力 id**(一个开放字符串)和 **JSON 载荷**。
它不生成也不依赖你的类型,不知道你内部是 gRPC、HTTP 还是别的什么。
这带来一个直接结论:**JSON 与你原生接口之间的转换发生在你这边**。
放到平台侧就意味着平台要编进你的 proto,那样每加一个插件都得改平台并发版。
## 2. 请求
| `operation` | 能力 id,如 `vendorx.invoice.create` |
| `payload` | `Any`,其 `value` **直接就是 JSON 字节** |
| `metadata` | `tenant_id` / `user_id` / `trace_id` |
| `request_id` | 调用方给的幂等/追踪标识,原样回填到响应里 |
`payload.type_url` 是 `type.googleapis.com/flare.capability.v1.PayloadJson`。
这不是 protobuf 编码的 `Any` —— 别拿 prost 去解,直接当 JSON 字节读。
⚠️ **`metadata` 里的上下文必须由你自己还原。** 平台是进程外调用,不走
HTTP header,所以框架的 Context 中间件不会替你注入。漏掉这步的表现是所有
调用都报「找不到上下文」——看着像鉴权问题,排查方向会完全跑偏。
## 3. 响应
```protobuf
message GenericResponse {
bool ok = 1;
Any payload = 2; // 同样是 JSON 字节
string error_code = 3;
string error_message = 4;
string request_id = 5;
}
```
业务失败要**明确回错**(`ok=false` + `error_code`),别回一个空的成功。
静默成功会让调用方以为业务做了 —— 这类缺陷在链路里几乎无法定位。
## 4. 能力 id 命名
建议 `{域}.{子系统}.{动作}`,例如 `vendorx.invoice.create`。
域用你自己的标识,别占用平台或他人的前缀。这不是技术限制(能力 id 是开放
字符串,平台不校验命名),而是多插件共存时唯一能靠的约定。
## 5. 路由:声明即边界
平台的路由簿按 `(租户, 插件)` 作键 —— 你的进程只有**一条**注册记录,
承接哪些 operation 由 `declared_operations` 决定。
**注册一次即承接全部声明。** `capability_id` 只是这条记录的标签,
填一个最好认的入口即可,它不限制你能接到的调用。
声明之外的调用会被平台拒绝,请求根本到不了你的进程。
> 如果你读到过「按 operation 最长前缀匹配」的说法:那是平台**进程内**
> handler 的路由规则。第三方插件是进程外的,走的是上面这条精确匹配。
## 6. 两类失败方向相反
| **授权失败** | 拒绝,不降级 | 未安装、租户开关关闭、无用户授权 |
| **可用性失败** | 降级,不拒绝 | 你超时、不可达、健康检查未通过 |
授权是 fail-closed 的:拒绝就是拒绝。而你的实例不可用时,平台不会因此拒绝
整条链路,只会按健康状态把你排到候选末尾。
把这两类搞反是最贵的错误:授权侧一放行就是越权,可用性侧一拒绝就是
一个实例抖动拖垮整条业务链。
## 7. 健康检查
默认走通用协议:平台调你的 `ExtensionPlugin.Call`,`operation` 为
`flare.capability.v1.health_check`,返回 `ok: true` 即健康。
需要特殊探活语义时,可在注册的 `labels` 里声明协议名 —— 但对应的探针实现
必须已部署在平台侧,否则平台会把你判为「声明的协议没有探针」而不是静默
降级成通用检查。静默降级等于把装配缺失伪装成健康。
## 8. 注册契约
见 [自己开发一个插件](./build-your-own-plugin.md)。字段全集与语义在
[`flare-plugin-host`](https://docs.rs/flare-plugin-host) 的 API 文档里。