三个可运行的插件样例
仓里有三个完整可跑的插件,覆盖两类插件、三种形态。建议先把它们跑起来, 再去读契约文档——照着能跑的东西改,比对着说明书从零写快得多。
三个样例都在 flare-im-core/examples/,只依赖开源侧的 gRPC 契约。
两类插件,先分清
| Hook 插件 | 能力插件 | |
|---|---|---|
| 样例 | hook_rate_limit / hook_audit_log | capability_link_preview |
| 怎么装 | 写进 hooks.toml,重启核心 | 启动时自注册,核心不用重启 |
| 谁触发 | 消息链路自动触发 | 客户端显式 dispatch(capability_id) |
| 拔掉之后 | 改配置再重启 | 进程退出即注销 |
想做「装上就能用」的能力,看第三个样例;想在消息链路上加拦截或审计,看前两个。
Hook 的两种形态
| 拦截并决定 | 观察既成事实 | |
|---|---|---|
| 样例 | hook_rate_limit | hook_audit_log |
| Hook 点 | pre_send | post_send |
| 时机 | 消息还没落库 | 消息已经落库,seq 已分配 |
| 能做什么 | 拒绝、改写内容 | 只能观察与产生副作用 |
| 失败的代价 | 可能拒发正常消息 | 少一条审计 |
require_success | true | false |
最后一行值得单独说:PostSend 返回失败并不能让消息回退,消息此刻已经落库。 把它配成 require_success = true,只会让「写日志失败」变成「用户看到发送失败」。
样例一:发送频率限制(PreSend)
cd flare-im-core
cargo run --example hook_rate_limit # 默认监听 127.0.0.1:7801
挂到核心:
# config/hooks.toml
[[pre_send]]
name = "rate-limit"
priority = 10
timeout_ms = 200
require_success = true # 插件不可用时拒发;改 false 则放行
[pre_send.transport]
type = "grpc"
endpoint = "http://127.0.0.1:7801"
窗口与阈值走环境变量,不改代码就能试:HOOK_RATE_LIMIT_MAX(默认 5)、 HOOK_RATE_LIMIT_WINDOW_SECS(默认 10)。
超限时返回:
{ "allow": false,
"denyReasonCode": "RATE_LIMITED",
"denyReasonMessage": "发送太频繁了,请稍后再试" }
denyReasonCode 是给代码看的,要保持稳定——客户端会拿它做分支; denyReasonMessage 是给人看的,随便改。
样例二:消息审计落盘(PostSend)
cargo run --example hook_audit_log # 默认写 ./logs/audit.jsonl
[[post_send]]
name = "audit-log"
priority = 0
timeout_ms = 1000
require_success = false # 审计失败不该影响消息
[post_send.transport]
type = "grpc"
endpoint = "http://127.0.0.1:7802"
每条消息落一行 JSON:
{"tenant_id":"0","conversation_id":"c1","operator_user_id":"alice",
"client_message_id":"cm1","message_type":"text","server_seq":1841,
"request_id":"a1"}
有意不记消息正文。审计日志往往比消息本身留存更久,把正文抄一份进去等于 凭空多一个泄漏面;要留证据就留 message_id,需要时回消息库取。
换成投递到 Kafka / ClickHouse,只要替换写文件那一处,其余不动。
样例三:链接预览(动态能力插件)
与前两个的根本不同:它独立运行、自己注册,核心不用重启就多出一个能力。
./scripts/start_server.sh # 1. 先起核心
cargo run --example capability_link_preview # 2. 插件自己注册上去
启动日志会说明注册结果。核心没起时它不会崩,而是明确告诉你 「插件仍在监听,但核心不知道它的存在」——这也是写插件时该有的降级姿态。
别忘了授权
能力插件受权限管控:注册上去不等于谁都能调。直接 dispatch 会得到
PermissionDenied: policy denied: user capability grant missing or expired
先把能力授予用户(或按租户开开关):
grpcurl -plaintext \
-import-path flare-grpc-proto/proto -import-path flare-proto/proto \
-proto capability_service.proto \
-d '{"tenant_id":"0","user_id":"alice","capability_id":"link.preview.v1"}' \
127.0.0.1:50110 flare.capability.v1.CapabilityService/GrantUserCapability
这是设计使然:能力插件是显式调用的对外能力面,默认全员可用会让「装个插件」 无意间扩大权限。
客户端调用(capability_id = link.preview.v1):
// 请求 payload_json
{"url": "https://example.com"}
// 返回(结构由插件自己定义,核心原样透传)
{"url":"https://example.com","title":"Example Domain",
"description":null,"site_name":null}
以上流程已端到端实测:起核心的 capability 服务 → 起插件(自注册成功)→
ListRegisteredPlugins里能查到它 → 授权后Dispatch真的抓到了Example Domain→ Ctrl-C 退出后它已从路由簿摘除。
它是怎么「装上就能用」的
插件进程启动
└─ 调 CapabilityService.RegisterPluginEndpoint 登记自己
└─ 核心的 PluginRouteBook 立刻多一条路由(无需重启)
└─ 客户端 dispatch(capability_id) → 核心转发到本插件
插件退出前 DeregisterPluginEndpoint 摘掉自己
核心不认识这个插件的任何业务语义,它只按 capability_id 转发 JSON。 所以能力插件用什么语言写、部署在哪、什么时候上下线,都与核心无关。
写能力插件的四步
- 实现
ExtensionPlugin.Call:operation就是你的capability_id。 - 启动后调
RegisterPluginEndpoint登记(租户, plugin_id, capability_id, 地址)。 - 退出前调
DeregisterPluginEndpoint,否则核心会继续把请求发到死地址。 - 返回结构由你定义,核心不解析。
capability_id建议带版本后缀(样例用link.preview.v1)。要改返回结构时 注册一个.v2与旧版并存,老客户端不受影响——能力插件是对外契约,改它等于改 API。
一个容易卡住的细节:payload 的
type_url写作type.googleapis.com/flare.capability.v1.PayloadJson,但 proto 里并没有 这个 message —— 它只是核心用来装 JSON 字节的约定。用 grpcurl 直接构造会报unknown message type,得用真实 gRPC 客户端。
自己动手时最容易写错的四处
这四条都写在样例代码的注释里,可以对照着看。
1. 放行时必须原样回传 draft。 核心用回包里的草稿继续往下走,不回传等于把 消息内容清空。改写内容(脱敏、加标签)也在这里做。
2. 不认识的 operation,两类插件的处理恰好相反。
- Hook 插件要放行:它在消息链路上,核心将来新增 hook 点时会送来没见过的
operation,默认拒绝的话,升级核心当天所有消息都会被挡下来。 - 能力插件要报错:它是被显式调用的,收到不认识的
capability_id说明调用方 找错了插件。静默返回空响应会让对方以为成功了。
三个样例里都写了对应的兜底分支,可以对照着看。
3. operation 的命名:Hook 是 flare.hook.v1.<hook 名>,能力插件则直接是 capability_id。 请求与响应都装在 Any 里, 按 type_url 对应的类型解包,回包同理——type_url 写错的话对面解不开。
4. 系统动作的 operator_user_id 是空的。 用户级的限流、配额之类的判断要先 排除它,否则系统消息会被自己的插件挡住。
不要照抄的一处
样例的状态都放在进程内存里(滑动窗口、文件句柄)。这是为了让样例的重点 留在插件契约上,而不是限流算法或日志管道。
真上生产时:限流要换成 Redis 之类的共享存储,否则多副本部署时每个副本各限各的, 实际阈值变成 N 倍;审计要换成有界队列 + 后台写,否则高峰期会把 hook 调用拖慢。
本地验证
不必起整套服务端也能验插件——直接用 gRPC 打它:
grpcurl -plaintext \
-import-path flare-grpc-proto/proto -import-path flare-proto/proto \
-proto capability_service.proto \
-d '{"operation":"flare.hook.v1.pre_send","payload":{"@type":"type.googleapis.com/flare.capability.v1.PreSendHookRequest","context":{"tenantId":"0","operatorUserId":"alice"},"draft":{"conversationId":"c1","messageType":"text"}}}' \
127.0.0.1:7801 flare.capability.v1.HookPlugin/Call
注意两个
-import-path:capability_service.proto在flare-grpc-proto, 它 import 的errors.proto在flare-proto。只给一个路径会报no such file or directory。
相关
- HookPlugin 契约:8 个 Hook 点的完整定义
- Core 全部业务扩展点:还能扩展哪些地方