跳至正文

三个可运行的插件样例

仓里有三个完整可跑的插件,覆盖两类插件、三种形态。建议先把它们跑起来, 再去读契约文档——照着能跑的东西改,比对着说明书从零写快得多。

三个样例都在 flare-im-core/examples/,只依赖开源侧的 gRPC 契约。

两类插件,先分清

Hook 插件能力插件
样例hook_rate_limit / hook_audit_logcapability_link_preview
怎么装写进 hooks.toml重启核心启动时自注册,核心不用重启
谁触发消息链路自动触发客户端显式 dispatch(capability_id)
拔掉之后改配置再重启进程退出即注销

想做「装上就能用」的能力,看第三个样例;想在消息链路上加拦截或审计,看前两个。

Hook 的两种形态

拦截并决定观察既成事实
样例hook_rate_limithook_audit_log
Hook 点pre_sendpost_send
时机消息还没落库消息已经落库,seq 已分配
能做什么拒绝、改写内容只能观察与产生副作用
失败的代价可能拒发正常消息少一条审计
require_successtruefalse

最后一行值得单独说: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。 所以能力插件用什么语言写、部署在哪、什么时候上下线,都与核心无关。

写能力插件的四步

  1. 实现 ExtensionPlugin.Calloperation 就是你的 capability_id
  2. 启动后调 RegisterPluginEndpoint 登记 (租户, plugin_id, capability_id, 地址)
  3. 退出前调 DeregisterPluginEndpoint,否则核心会继续把请求发到死地址。
  4. 返回结构由你定义,核心不解析。

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-pathcapability_service.protoflare-grpc-proto, 它 import 的 errors.protoflare-proto。只给一个路径会报 no such file or directory

相关