我用 100 行核心代码,做了一个能接入飞书的 Hermes 式智能体
很多人看到 Hermes Agent 的体验,第一反应是:
我也要把自己的 Agent 接进飞书。
然后就开始设计 Webhook、消息签名、事件回调、会话表、流式输出和重试队列。还没让 Agent 回第一句话,网关已经先写了一半。
其实,最简单的方式不是再造一个飞书机器人服务,而是直接让 rpi 接管这条长连接。
rpi 的 rpi-im-message 扩展已经把飞书接入层包起来:飞书消息通过官方 Rust SDK 的 WebSocket 长连接进入 rpi,rpi 负责调用 Agent,模型的最终回复再发回原来的会话。你需要配置飞书应用和模型,不需要从零实现一套消息网关。
这篇文章会分成两层:先用约 100 行代码把 Hermes 式 Agent 的核心循环讲清楚,再告诉你真正接入飞书时为什么不必自己写网关。代码用来理解原理,扩展用来减少接入成本。
先看最终效果
下面是实际运行截图。第一张用于展示 Agent 在飞书中的对话入口,第二张用于展示工具执行过程和最终回复。截图保留了手机端的完整上下文,读者可以重点看消息是否回到了原来的飞书会话,以及工具状态是否单独反馈。

图 1:飞书消息进入 rpi Agent 后的实际回复。入口是飞书,任务执行仍发生在运行 rpi 的工作环境中。具体回复内容取决于当前模型、工作目录和任务。

图 2:工具调用过程在飞书中单独反馈,最终回答完成后再发送结果。截图只证明当次运行中的显示结果,不等于完成了安全审计或性能测试。
先用 100 行代码看懂 Agent 核心
先说明代码的性质:下面不是从扩展里复制出来、可以直接编译的完整文件,而是根据 rpi-im-message 当前源码压缩出的 100 行阅读版。它保留真实的函数职责和调用顺序,省略了配置校验、ABI 字符串管理、线程同步、错误处理等外围代码。
const EVENT_TYPE_MESSAGE: &str = "im.message.receive_v1";
struct MessageHandler {
state: Arc<RuntimeState>,
commands: Sender<Command>,
}
impl EventHandler for MessageHandler {
fn event_type(&self) -> &str {
EVENT_TYPE_MESSAGE
}
fn handle(&self, event: Event) -> HandlerFuture {
let state = self.state.clone();
let commands = self.commands.clone();
Box::pin(async move {
let event = event.event.ok_or("empty event")?;
let message: MessageEvent = serde_json::from_value(event)?;
// 机器人自己的消息不能再次进入 Agent,否则会自我触发。
if message.sender.sender_type.as_deref() == Some("app") {
return Ok(None);
}
let chat_id = message.message.chat_id.clone().unwrap_or_default();
if !state.profile.allow_chats.is_empty()
&& !state.profile.allow_chats.contains(&chat_id)
{
return Ok(None);
}
if state.profile.mention_required
&& message.message.mentions.as_ref().map_or(true, Vec::is_empty)
{
return Ok(None);
}
let text = extract_message_text(
&message.message.message_type,
&message.message.content.clone().unwrap_or_default(),
);
let message_id = message.message.message_id.clone().unwrap_or_default();
// 先让用户知道消息已经收到。
if state.profile.ack_reaction {
commands.send(Command::React {
message_id: message_id.clone(),
emoji_type: random_ack_reaction(),
})?;
}
// 把原始消息放入接收队列,供 receive 动作读取。
state.push_message(json!({
"type": "message",
"provider": "feishu",
"messageId": message_id,
"conversationId": chat_id,
"text": text,
}));
if state.profile.auto_reply && !text.trim().is_empty() {
let runtime = state.host_runtime.ok_or("runtime unavailable")?;
let chat = chat_id.clone();
let model = state.profile.auto_reply_model.clone();
let timeout = state.profile.auto_reply_timeout;
tokio::spawn(async move {
let result = tokio::task::spawn_blocking(move || {
let _lock = AUTO_REPLY_LOCK.lock()?;
let _prompt_scope = FeishuPromptScope::enter();
// 优先进入当前 rpi Harness。
match invoke_runtime_action(
&runtime,
RuntimeActionId::SendUserMessage,
&json!({"text": text}),
) {
Ok(result) => Ok(result),
Err(error) if error.contains("before harness was built") => {
// 没有 Harness 时,使用无 TUI 的 rpi 进程。
invoke_print_with_retry(
&text, &chat, model.as_deref(), timeout,
)
}
Err(error) => Err(error),
}
}).await??;
if let Some(reply) = result["text"].as_str() {
commands.send(Command::SendAsync {
conversation_id: chat,
content: auto_reply_content(reply),
})?;
}
Ok::<_, anyhow::Error>(())
});
}
Ok(None)
})
}
}
async fn run_server(profile: Profile) -> Result<()> {
let client = Client::new(Config::builder(
&profile.app_id, &profile.app_secret
).base_url(FEISHU_BASE_URL).build())?;
let dispatcher = EventDispatcher::new(EventDispatcherConfig::new(), noop_logger());
dispatcher.register_handler(Box::new(MessageHandler {
state: state.clone(),
commands: command_sender.clone(),
})).await;
let stream = client.stream()
.stream_config(StreamConfig::new()
.auto_reconnect(profile.auto_reconnect)
.reconnect_interval(profile.reconnect_interval)
.reconnect_count(-1))
.event_dispatcher(dispatcher)
.build()?;
tokio::select! {
result = stream.start() => result?,
command = commands.recv() => handle_command(&client, command).await?,
}
Ok(())
}这段代码对应的真实源码并不只做“收消息、发回复”两件事。它还处理了会话选择、工具进度、自动重连、消息队列、飞书 Markdown/Card 格式和失败重试。
但核心闭环可以压缩成一句话:
飞书事件 → MessageHandler → rpi Agent → 工具进度 → SendAsync → 飞书回复模型并没有直接操作飞书,也没有直接读取项目文件。扩展负责把飞书事件转换成 rpi 能理解的用户消息,再把 Agent 的结果转换回飞书消息。这正是 Hermes 式智能体最关键的“渠道层 + Agent 层”分工。
源码里的真实链路比上面的 100 行更完整。可以按下面几个位置阅读:
MessageHandler::handle:接收im.message.receive_v1,过滤机器人自己发出的消息、允许的群聊和是否必须 @,然后提取文本。autoReply分支:如果开启自动回复,优先通过 ABI v2 的RuntimeActionId::SendUserMessage把文本交给当前 rpi Agent;如果 Harness 尚未准备好,则退回到受限的rpi --print --no-extensions子进程。Progress::event:把工具开始、成功、失败和未完成状态转换成简短的飞书文本。run_server:创建飞书 SDK 客户端、注册事件分发器、建立 WebSocket 长连接,并消费发送、进度更新和停止命令。send_message:根据text、markdown或card选择飞书消息格式。
这也解释了为什么文章要把“100 行核心代码”和“已有扩展”分开:前者展示 Agent 如何工作,后者解决生产接入中的长连接、队列、会话、重试和消息格式问题。
对应源码:
MessageHandler::handle:https://github.com/pi-rust/rpi-package/blob/main/packages/rpi-im-message/src/lib.rs#L874-L1119start_server与run_server:https://github.com/pi-rust/rpi-package/blob/main/packages/rpi-im-message/src/lib.rs#L1190-L1386- 工具进度状态:https://github.com/pi-rust/rpi-package/blob/main/packages/rpi-im-message/src/progress.rs
- 会话选择与
/new、/sessions、/session:https://github.com/pi-rust/rpi-package/blob/main/packages/rpi-im-message/src/sessions.rs
代码写明白了,飞书接入交给已有扩展
飞书机器人常见的接入方式有两类:Webhook 回调和长连接。
Webhook 需要你提供一个公网可访问的服务端点,处理飞书发来的 HTTP 请求,再自己维护签名校验、响应时限和异步任务。它适合已经有后端服务的团队,但对于“我只是想把 Agent 放进飞书”这个目标来说,外围工作比较多。
rpi-im-message 走的是飞书官方 SDK 支持的 WebSocket 长连接。rpi 进程主动连接飞书,不需要先部署一个公网 HTTP 接收端点,消息接收和发送都由扩展处理。
这并不意味着长连接自动解决了所有问题:进程需要持续运行,飞书应用仍然需要配置权限和事件订阅,模型调用也需要自己的凭据。但接入边界清楚了:
飞书开放平台:负责应用身份和消息权限
rpi-im-message:负责飞书长连接与消息收发
rpi Agent:负责上下文、模型和工具循环
模型服务:负责生成下一步决策和回复第一步:准备飞书应用
登录飞书开放平台,创建一个企业自建应用,并为应用启用机器人能力。
需要准备两项信息:
App ID,通常形如cli_xxx。App Secret,用于应用鉴权。
然后在事件订阅中选择 长连接,订阅接收消息事件 im.message.receive_v1,并为机器人开通发送和接收消息所需的权限。
不同租户、版本和管理员策略下,权限名称可能略有差异。不要只复制一份配置文件就认为接入完成:启动 rpi 后,如果连接成功但收不到消息,优先回到飞书开放平台检查机器人是否启用、事件是否订阅,以及当前账号是否有权使用该应用。
建议先在一个测试群里验证,不要一开始就把机器人加入所有业务群。
第二步:安装 rpi 的飞书扩展
安装 rpi-im-message:
rpi install rpi-im-message扩展的职责不是创建一个新的 Agent,而是把飞书接入到现有 rpi 运行时。它提供消息服务、状态查看、收发消息等动作;打开自动回复后,收到的文本会交给当前 rpi Agent。
如果你的环境需要固定版本,建议根据当前扩展仓库和本地 rpi 版本指定版本号,不要盲目复制旧文章里的版本。扩展仓库地址是:
https://github.com/pi-rust/rpi-package/tree/main/packages/rpi-im-message
第三步:写一份最小配置
可以把配置放在项目的 .rpi/im.json。下面是一份最小示例:
{
"defaultProfile": "feishu-main",
"profiles": {
"feishu-main": {
"provider": "feishu",
"domain": "feishu",
"appId": "cli_xxx",
"appSecretEnv": "RPI_FEISHU_APP_SECRET",
"transport": "long_connection",
"mentionRequired": true,
"autoReply": true,
"autoReconnect": true
}
}
}把密钥放到环境变量中:
export RPI_FEISHU_APP_SECRET='替换成飞书应用的 App Secret'Windows PowerShell 可以这样设置:
$env:RPI_FEISHU_APP_SECRET = "替换成飞书应用的 App Secret"不要把 appSecret 直接提交到 Git。扩展支持直接写入配置,但共享机器和团队仓库更适合使用 appSecretEnv。
配置中的几个字段分别表示:
| 字段 | 作用 |
|---|---|
appId | 飞书应用 ID |
appSecretEnv | 从哪个环境变量读取应用密钥 |
transport | 使用飞书长连接 |
mentionRequired | 群聊中是否必须 @ 机器人 |
autoReply | 是否把收到的消息交给 Agent 自动回复 |
autoReconnect | 长连接断开后是否自动重连 |
启动:让 Agent 留在飞书里工作
用无 TUI 的服务模式启动:
rpi --im-message-server如果配置了多个飞书应用,可以指定 profile:
rpi --im-message-server --im-profile feishu-main看到长连接建立后,把机器人加入测试群,@ 它发送一句简单的问题:
你好,请告诉我你现在能做什么?第一轮不要直接测试复杂的代码修改。先确认三件事:
- 飞书应用能收到消息。
- rpi 能把消息交给模型。
- 模型回复能回到同一个飞书会话。
这三步打通后,再打开文件工具、命令工具或项目 Skills。否则遇到问题时,很难判断是飞书权限、模型配置还是工具执行失败。
它为什么有点像 Hermes
Hermes 让人印象深刻的地方,不只是“可以在聊天软件里问 AI”,而是聊天消息背后仍然是一个会执行任务的 Agent:它能调用工具,维持上下文,处理长任务,还可以从多个渠道进入同一套运行环境。
用 rpi 接入飞书,最先复用的是这条核心路径:
飞书消息
↓
rpi-im-message 长连接
↓
rpi Agent 上下文
↓
模型决定是否调用工具
↓
rpi 执行工具并回传结果
↓
最终回复发送回飞书在工具执行期间,扩展还可以把工具调用状态作为普通文本消息发送,并在工具成功、失败或中断时更新状态。最终回答单独发送,避免用户只看到一条“处理中”然后不知道发生了什么。
这就是推广 rpi 时最值得讲的一点:你接入的不是一个只会回固定文本的机器人,而是一个可以沿用 rpi 工具和会话能力的 Agent 入口。
飞书里的会话怎么处理
自动回复模式下,扩展会为飞书会话维护对应的 rpi session。常用命令包括:
/new 创建并切换到一个新会话
/sessions 查看当前聊天最近的会话
/session 查看当前会话和帮助
/session ID 切换到指定会话这让飞书不只是一个消息入口,也能成为一个轻量的 Agent 工作台:
- 一个群聊用于项目日常问答。
- 一个会话用于排查线上问题。
- 另一个会话用于整理发布清单。
如果要切换模型,可以使用 rpi 支持的模型配置;如果要切换工作方式,可以通过项目里的规则文件和 Skills 约束 Agent。
但要注意,会话隔离不等于权限隔离。不同会话仍可能共享同一个工作目录和工具权限。涉及代码修改、生产文件或敏感数据时,仍然要配置允许范围和人工审批。
适合用它做什么
这个接入方式适合几类场景。
1. 团队内部的项目助手
把 Agent 放在项目群里,成员可以直接问:
- 最近一次构建失败的原因是什么?
- 这个模块的入口在哪里?
- 帮我整理一下当前 TODO。
- 读取变更记录,生成一份发布摘要。
机器人不需要把整个代码库预先塞进 prompt,而是根据任务调用读取和检查工具。
2. 远程处理长任务
Agent 在服务器上运行,开发者通过飞书发起任务、查看进度。即使人不在电脑前,也能知道它正在读取什么、执行什么、最后得到什么结果。
这也是“把 Agent 接进飞书”比“给飞书加一个问答机器人”更有价值的地方:飞书成为入口,真正的执行环境仍然在 rpi 所在的机器上。
3. 把内部流程变成 Skill
例如把发布流程写成 skills/release-checklist.md:
1. 检查工作区是否有未提交改动
2. 读取 CHANGELOG.md
3. 运行项目指定的检查命令
4. 汇总结果,等待人工确认后再发布以后在飞书里说“按发布清单检查一下”,Agent 就可以按这套工作方法执行。
Skill 负责描述流程,工具负责真正执行,飞书只负责让人更容易发起和跟踪任务。三者不要混在一起。
这不是装完就能自动工作的魔法
为了让推广内容可信,有几件事必须说清楚。
第一,仍然需要配置飞书应用。 App ID、App Secret、机器人能力、事件订阅和消息权限缺一不可。
第二,仍然需要配置模型。 rpi-im-message 负责把消息接进来,不会替你提供模型账号,也不会让模型调用成本消失。
第三,长连接进程必须持续运行。 本地测试可以直接启动;团队使用时,需要用服务管理器、容器或其他可靠方式守护进程。
第四,插件接口不是沙箱。 如果 Agent 能读写文件或执行命令,它就拥有对应运行环境的能力。飞书群里的任何成员都不应该默认拥有生产环境操作权限。
第五,自动回复不是自动审批。 涉及删除、发布、发送外部消息等副作用操作,应在工具层增加权限判断和人工确认。
这些限制不会削弱这个方案,反而说明它的边界比较清楚:rpi 负责 Agent 运行机制,飞书负责消息入口,权限和部署由使用者决定。
结尾:真正的反差不是“代码少”
很多人想把 Agent 接进飞书,先想到的是写一个机器人服务;真正应该先问的是:消息接进来以后,谁负责上下文、工具调用、会话和取消?
如果这些能力已经在 rpi 里,飞书接入就不必重新实现一遍 Agent。安装扩展、配置应用、启动长连接,先让机器人在群里回复起来,再逐步增加工具和 Skills。
Hermes 式体验的核心并不在于界面有多复杂,而在于聊天消息背后有一个真正能工作的 Agent。
这次最简单的接入路径可以压缩成三步:
安装 rpi-im-message
→ 配置飞书 App 和长连接
→ 启动 rpi --im-message-server如果你已经在使用 rpi,最短路径就是:
- 安装
rpi-im-message。 - 配置飞书 App ID、App Secret 和长连接事件。
- 打开
autoReply。 - 启动
rpi --im-message-server。
先在测试群里让机器人回复一句话,再逐步开放文件、命令和项目工具。这样可以先验证消息链路,再决定要不要把它变成团队里的项目助手。
参考资料
- rpi 官网与文档:https://rpi.laofu.online/
- rpi 核心仓库:https://github.com/bigfish1913/pi-rust
rpi-im-message扩展:https://github.com/pi-rust/rpi-package/tree/main/packages/rpi-im-message- 飞书开放平台:https://open.feishu.cn/