文章目录
-
- [1. 先说说那点小冲动](#1. 先说说那点小冲动)
- [2. 换个思路:让长连接自己来](#2. 换个思路:让长连接自己来)
- [3. 100 行代码拆开看](#3. 100 行代码拆开看)
-
- [3.1 事件怎么进来](#3.1 事件怎么进来)
- [3.2 别让机器人自己跟自己聊天](#3.2 别让机器人自己跟自己聊天)
- [3.3 收到消息之后](#3.3 收到消息之后)
- [3.4 闭环就一句话](#3.4 闭环就一句话)
- [3.5 想追源码去哪看](#3.5 想追源码去哪看)
- [4. 网关不用写,扩展都包了](#4. 网关不用写,扩展都包了)
-
- [4.1 Webhook:先有房才能装修](#4.1 Webhook:先有房才能装修)
- [4.2 长连接:主动连上去](#4.2 长连接:主动连上去)
- [5. 第一步:把飞书应用"生"出来](#5. 第一步:把飞书应用"生"出来)
- [6. 第二步:装上 rpi-im-message](#6. 第二步:装上 rpi-im-message)
- [7. 第三步:写一份最小配置](#7. 第三步:写一份最小配置)
- [8. 启动:让机器人正式上班](#8. 启动:让机器人正式上班)
- [9. 它为什么"有点像 Hermes"](#9. 它为什么"有点像 Hermes")
- [10. 飞书里的会话:一个群,多个人格](#10. 飞书里的会话:一个群,多个人格)
- [11. 到底能拿它干嘛](#11. 到底能拿它干嘛)
-
- [11.1 团队内部的项目助手](#11.1 团队内部的项目助手)
- [11.2 远程挂机跑长任务](#11.2 远程挂机跑长任务)
- [11.3 把内部流程变成 Skill](#11.3 把内部流程变成 Skill)
- [12. 泼点冷水,清醒一下](#12. 泼点冷水,清醒一下)
-
- [12.1 仍然要配置飞书应用](#12.1 仍然要配置飞书应用)
- [12.2 仍然要配置模型](#12.2 仍然要配置模型)
- [12.3 长连接进程必须活着](#12.3 长连接进程必须活着)
- [12.4 插件接口不是沙箱](#12.4 插件接口不是沙箱)
- [12.5 自动回复不是自动审批](#12.5 自动回复不是自动审批)
- [13. 结尾:真正的反差不是"代码少"](#13. 结尾:真正的反差不是"代码少")
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看, 传送门https://blog.csdn.net/HHX_01
1. 先说说那点小冲动
每次刷到有人把 Agent 接进飞书的演示,我的手指都很诚实:先收藏,再转发,最后陷入沉思------我也要整一个。
于是开始规划 Webhook、消息签名、事件回调、会话表、流式输出、重试队列。规划到凌晨两点,Agent 连第一句话都没回过,消息网关倒是先写了一半。
这感觉就像你想养只猫,结果先给全屋装了三十个摄像头,猫还没进门。
效果长什么样?你在飞书里发一句"再测一次",它真的去跑命令、读文件,工具进度还单独发消息反馈,最后才把结果甩给你------跟看现场直播一样,比某些项目周报都透明。
2. 换个思路:让长连接自己来
后来我想明白一个事:你根本不需要再造一个飞书机器人服务,你只需要让 rpi 接管这条长连接。
rpi 的 rpi-im-message 扩展,已经把飞书接入层整个打包了。飞书消息通过官方 Rust SDK 的 WebSocket 长连接进到 rpi,rpi 负责把 Agent 叫起来,模型回完话,扩展再把回复塞回原来的会话。
你真正要准备的只有两样:一个飞书应用,一个模型。
翻译成人话:车你不用造,方向盘都给你焊好了,你负责踩油门就行。
3. 100 行代码拆开看
先声明:下面这段不是从扩展里抠出来就能直接编译的完整文件,而是按 rpi-im-message 当前源码压出来的 100 行阅读版。函数职责和调用顺序都是真的,只是把配置校验、ABI 字符串管理、线程同步、错误处理这些外围给省了。
就像给你看发动机,而不是把整个车间都搬过来。
3.1 事件怎么进来
扩展监听的是飞书的事件 im.message.receive_v1。消息一到,MessageHandler 就接手,把事件里的内容解析成一条消息。
rust
const EVENT_TYPE_MESSAGE: &str = "im.message.receive_v1";
struct MessageHandler {
state: Arc,
commands: Sender,
}
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(())
}
3.2 别让机器人自己跟自己聊天
代码里第一个有意思的过滤:机器人自己发的消息不能再次进入 Agent,否则会自我触发。
你想想那个画面:机器人收到自己的回复,又触发自己回复,再触发......俩 AI 隔着飞书互道晚安,一直聊到服务器欠费。程序员版《楚门的世界》,属于是。
后面还有两个开关:一个限定只跟某些群聊,一个要求必须 @ 才理你。有了它们,机器人不会在五百人大群里突然抢答,也不会被一句"在吗"反复调戏。
3.3 收到消息之后
如果开了 ack_reaction,它收到消息会先回一个表情------翻译一下就是:朕已阅,正在干活。
然后消息进队列,交给 rpi Agent。autoReply 分支很有意思:优先走 ABI v2 的 RuntimeActionId::SendUserMessage,把文本递给当前 rpi Agent;如果 Harness 还没准备好,就退回受限的 rpi --print --no-extensions 子进程。
翻译:有主力上主力,主力没热身就派替补,反正不能空门。
3.4 闭环就一句话
整个核心闭环压缩成一句:
飞书事件 → MessageHandler → rpi Agent → 工具进度 → SendAsync → 飞书回复
注意一个细节:模型没有直接碰飞书,也没有直接翻你的项目文件。扩展负责把飞书事件翻译成 rpi 听得懂的用户消息,再把 Agent 的结果翻译回飞书消息。
这就是 Hermes 式智能体最关键的"渠道层 + Agent 层"分工:一个管门,一个管干活,各司其职,互不甩锅。
3.5 想追源码去哪看
真实的链路比这 100 行完整得多,几个入口给你标好:
MessageHandler::handle:接收事件、过滤机器人自己、过滤群聊和 @ 要求,然后提取文本。autoReply分支:优先 ABI v2 交给当前 Agent,Harness 没就绪就退回受限子进程。Progress::event:把工具开始、成功、失败、未完成的状态,转成简短飞书文本。run_server:建 SDK 客户端、注册事件分发器、拉起 WebSocket 长连接,消费发送/进度/停止命令。send_message:按 text、markdown、card 选飞书消息格式。
4. 网关不用写,扩展都包了
飞书机器人接入就两条路:Webhook 回调和长连接。
4.1 Webhook:先有房才能装修
Webhook 需要你提供一个公网能访问的服务端点,自己维护签名校验、响应时限和异步任务。
它适合本来就有后端服务的团队。但如果你只是想"把 Agent 放进飞书",外围工作多到让你怀疑人生。
你得先有一台能上网的服务器------就像先有房才能谈装修,先有对象才能谈分手,先有网关才能谈智能。
4.2 长连接:主动连上去
rpi-im-message 走的是飞书官方 SDK 支持的 WebSocket 长连接,rpi 主动连飞书,不需要公网接收端点,消息的收发都由扩展处理。
省下来的那台服务器,够你买三杯咖啡,加班的时候还能续命。
当然,长连接不等于万事大吉:进程得一直跑,飞书应用该配的权限和事件订阅一样不能少,模型凭据也得自己准备。但接入边界一下就清楚了:
飞书开放平台:负责应用身份和消息权限
rpi-im-message:负责飞书长连接与消息收发
rpi Agent:负责上下文、模型和工具循环
模型服务:负责生成下一步决策和回复
5. 第一步:把飞书应用"生"出来
登录飞书开放平台,创建一个企业自建应用,给机器人上户口。
要准备两样东西:App ID,通常长这样 cli_xxx;App Secret,用来鉴权。
然后在事件订阅里选长连接,订阅接收消息事件 im.message.receive_v1,再给机器人开好收发消息的权限。
不同租户、版本和管理员策略下,权限名称可能略有差异。别以为复制一份配置就完事了------连接成功但收不到消息,优先回开放平台查三件事:机器人启用没有、事件订阅了没有、当前账号有没有权限用这个应用。
建议先在测试群里验证。别一上来就把机器人拉进所有业务群,它连"你好"都没学会,你先让它社死。
6. 第二步:装上 rpi-im-message
安装就一行:
rpi install rpi-im-message
这个扩展的职责不是创建一个新 Agent,而是把飞书接到你现有的 rpi 运行时。它提供消息服务、状态查看、收发消息等动作;打开自动回复后,收到的文本就交给当前 rpi Agent。
要固定版本的话,按当前扩展仓库和你本地 rpi 的版本选,别照抄旧文章里的版本号。
版本会过时,就像去年的流行语,今年再说就有点尬。
7. 第三步:写一份最小配置
配置放在项目的 .rpi/im.json,下面是最小示例:
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 用户这样写:
powershell
$env:RPI_FEISHU_APP_SECRET = "替换成飞书应用的 App Secret"
别把 appSecret 直接提交到 Git。把密钥写进仓库,等于把家门钥匙贴在小区公告栏,还配了个箭头。
扩展支持直接写进配置,但共享机器和团队仓库,用 appSecretEnv 更体面。
几个字段先认识一下:
| 字段 | 作用 |
|---|---|
| appId | 飞书应用 ID |
| appSecretEnv | 从哪个环境变量读取应用密钥 |
| transport | 使用飞书长连接 |
| mentionRequired | 群聊中是否必须 @ 机器人 |
| autoReply | 是否把收到的消息交给 Agent 自动回复 |
| autoReconnect | 长连接断开后是否自动重连 |
8. 启动:让机器人正式上班
用无 TUI 的服务模式启动:
rpi --im-message-server
配置了多个飞书应用的话,可以指定 profile:
rpi --im-message-server --im-profile feishu-main
看到长连接建立后,把机器人拉进测试群,@ 它发一句:
你好,请告诉我你现在能做什么?
第一轮别直接让它改代码。先确认三件事:飞书应用能收到消息、rpi 能把消息交给模型、模型回复能回到同一个会话。
这三步打通了,再开文件工具、命令工具、项目 Skills。否则出了问题,你根本分不清是飞书权限的锅、模型配置的锅,还是工具执行自己摆烂。
9. 它为什么"有点像 Hermes"
Hermes 让人上头的点,不只是"能在聊天软件里问 AI",而是消息背后站着一个真的会干活的 Agent:能调工具、维持上下文、扛长任务,还能从多个渠道进同一套运行环境。
用 rpi 接飞书,最先复用的是这条核心路径:
飞书消息
↓
rpi-im-message 长连接
↓
rpi Agent 上下文
↓
模型决定是否调用工具
↓
rpi 执行工具并回传结果
↓
最终回复发送回飞书
工具执行期间,扩展会把工具调用状态当普通文本发出来,成功、失败、中断都会更新,最终回答单独发送。
你不会只看到一条"处理中"然后干瞪眼------那感觉跟点外卖两小时、商家永远显示"备餐中"一样绝望。
所以最值得讲的一点是:你接入的不是一个只会回固定文本的复读机,而是一个能沿用 rpi 工具和会话能力的 Agent 入口。
10. 飞书里的会话:一个群,多个人格
自动回复模式下,扩展会给飞书会话维护对应的 rpi session。常用命令:
/new 创建并切换到一个新会话
/sessions 查看当前聊天最近的会话
/session 查看当前会话和帮助
/session ID 切换到指定会话
这让飞书不只是消息入口,也能当个轻量 Agent 工作台:一个群聊管日常问答,一个会话排查线上问题,一个会话整理发布清单。三个会话,三种人格,但都是你。
不过要注意:会话隔离不等于权限隔离。不同会话可能共享同一个工作目录和工具权限。
就像同办公室的两个人,聊的虽然是不同话题,但都能用同一台打印机------而且都能打错东西。
涉及代码修改、生产文件或敏感数据时,该配允许范围配允许范围,该人工审批人工审批。
11. 到底能拿它干嘛
11.1 团队内部的项目助手
把 Agent 放在项目群里,成员直接问:最近一次构建为什么挂了?这个模块入口在哪?帮我整理下 TODO;读一下变更记录,出一份发布摘要。
机器人不用把整个代码库预先塞进 prompt,而是按任务调读取和检查工具。需要什么看什么,像个不背课文的学霸,随查随答。
11.2 远程挂机跑长任务
Agent 在服务器上跑,开发者通过飞书发起任务、看进度。人不在电脑前,也知道它正在读什么、执行什么、最后得到什么。
飞书成了入口,执行环境还在 rpi 所在的机器上。从此加班不一定要坐在工位,躺着也能把活干了------不过这话别让老板听见。
11.3 把内部流程变成 Skill
比如把发布流程写成 skills/release-checklist.md:
1. 检查工作区是否有未提交改动
2. 读取 CHANGELOG.md
3. 运行项目指定的检查命令
4. 汇总结果,等待人工确认后再发布
以后在飞书里说"按发布清单检查一下",Agent 就按这套方法执行。
Skill 负责描述流程,工具负责真正执行,飞书负责让人更容易发起和跟踪。三者别混在一起,混在一起就是三体问题。
12. 泼点冷水,清醒一下
这方案不是装上就自动工作的魔法,有几件事必须说清楚。
12.1 仍然要配置飞书应用
App ID、App Secret、机器人能力、事件订阅、消息权限,缺一不可。
就像结婚,光有爱情不够,还得有户口本。
12.2 仍然要配置模型
扩展只负责把消息接进来,不替你出模型账号,也不让调用成本消失。
模型该烧的钱,一分都省不了。
12.3 长连接进程必须活着
本地测试直接启动就行;团队用的话,得上服务管理器、容器或其他靠谱方式守护。
进程一挂,机器人就失联。像极了前任,说不在就不在。
12.4 插件接口不是沙箱
Agent 能读写文件、能执行命令,就拥有对应运行环境的能力。飞书群里的任何成员,都不该默认拥有生产环境的操作权限。
12.5 自动回复不是自动审批
涉及删除、发布、发送外部消息这类副作用操作,该加权限判断加权限判断,该人工确认人工确认。
让 AI 自己批准自己删库,那是恐怖片的剧本,不是技术方案。
这些限制不削弱这个方案,反而说明它边界清楚:rpi 管 Agent 运行机制,飞书管消息入口,权限和部署由使用者决定。
13. 结尾:真正的反差不是"代码少"
很多人想把 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。
先在测试群里让它回一句话,再逐步开放文件、命令和项目工具。
先验证链路,再决定要不要把它变成团队的项目助手。反正代码没写几行,快乐倒是省出来不少。
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/HHX_01