文章目录
-
- 一、项目概述
-
- [1.1 核心设计理念](#1.1 核心设计理念)
- [1.2 整体架构](#1.2 整体架构)
- [1.3 数据流向](#1.3 数据流向)
- [1.4 集成目标](#1.4 集成目标)
- [1.5 效果展示](#1.5 效果展示)
- 二、飞书应用创建与配置
-
- [2.1 创建飞书应用](#2.1 创建飞书应用)
- [2.2 事件接收模式选型:WebSocket vs Webhook](#2.2 事件接收模式选型:WebSocket vs Webhook)
- [2.3 配置应用权限](#2.3 配置应用权限)
- [2.4 配置事件订阅](#2.4 配置事件订阅)
-
- [WebSocket 模式(开发环境)](#WebSocket 模式(开发环境))
- [Webhook 模式(生产环境)](#Webhook 模式(生产环境))
- [2.5 发布应用与添加到群](#2.5 发布应用与添加到群)
- [2.6 配置飞书机器人能力](#2.6 配置飞书机器人能力)
- 三、依赖配置
-
- [3.1 添加飞书 SDK 依赖](#3.1 添加飞书 SDK 依赖)
- [3.2 配置飞书应用信息](#3.2 配置飞书应用信息)
- 四、核心代码实现
-
- [4.1 飞书消息模型](#4.1 飞书消息模型)
- [4.2 飞书事件监听器](#4.2 飞书事件监听器)
- [4.3 MCP 执行器](#4.3 MCP 执行器)
- [4.4 飞书消息发送器](#4.4 飞书消息发送器)
- [4.5 飞书客户端配置](#4.5 飞书客户端配置)
- [4.6 WebSocket 客户端实现](#4.6 WebSocket 客户端实现)
- [4.7 Webhook 回调控制器(生产环境)](#4.7 Webhook 回调控制器(生产环境))
- [4.8 启用异步支持](#4.8 启用异步支持)
- 五、完整配置清单
-
- [5.1 application.yml 最终配置](#5.1 application.yml 最终配置)
- [5.2 环境变量说明](#5.2 环境变量说明)
- 六、测试与验证
-
- [6.1 测试流程](#6.1 测试流程)
- [6.2 预期结果](#6.2 预期结果)
- 七、生产环境部署建议
-
- [7.1 切换到 Webhook 模式](#7.1 切换到 Webhook 模式)
- [7.2 配置 Nginx 反向代理](#7.2 配置 Nginx 反向代理)
- [7.3 配置飞书事件订阅](#7.3 配置飞书事件订阅)
- 八、总结
-
- [8.1 项目结构](#8.1 项目结构)
- [8.2 核心优势](#8.2 核心优势)
一、项目概述
本文基于现有的 MCP Client Demo 项目(可以参考之前的博文Spring AI MCP Client 实战),详细介绍了如何将其与飞书群机器人集成,实现"飞书消息 → LLM 智能决策 → MCP 工具调用 → 结果回复"的完整链路。
1.1 核心设计理念
工具选择由大模型(LLM)自主决策,而非硬编码的命令匹配。Spring AI 的 ChatClient 已内置工具调用能力,LLM 会根据工具描述自动选择调用哪个工具。
1.2 整体架构

1.3 数据流向

1.4 集成目标
在现有架构基础上,增加飞书机器人模块,实现:
| 功能 | 说明 |
|---|---|
| 事件接收 | 接收飞书群聊 @ 机器人消息 + 个人单聊消息 |
| 智能决策 | LLM 根据工具描述自动选择调用哪个工具 |
| 工具执行 | 调用本地/MCP Server 工具 |
| 结果回复 | 将执行结果发回飞书群或单聊 |
1.5 效果展示
p2p

group

二、飞书应用创建与配置
2.1 创建飞书应用
- 登录 飞书开放平台
- 点击「创建企业自建应用」
- 填写应用名称(如 "MCP 智能助手")和描述
- 创建完成后,在「凭证与基础信息」页面获取 App ID 和 App Secret
2.2 事件接收模式选型:WebSocket vs Webhook
飞书 SDK 支持两种事件接收模式,对比如下:
| 对比项 | WebSocket 长连接 | Webhook 回调 |
|---|---|---|
| 网络要求 | 无需公网 IP/域名 | 需要公网可访问的 HTTPS 地址 |
| 部署复杂度 | 低,开箱即用 | 高,需配置域名、SSL、反向代理 |
| 适用场景 | 开发调试、内网部署、NAT 后服务 | 生产环境、高可用集群 |
| 实时性 | 高,服务端主动推送 | 高,飞书主动回调 |
| 可靠性 | 依赖长连接稳定性,需处理断线重连 | 依赖公网稳定性,飞书有重试机制 |
| 扩展性 | 单实例受限 | 可配合负载均衡多实例部署 |
| 防火墙 | 需允许出站 WebSocket | 需允许入站 HTTPS |
推荐方案:
- 开发/测试环境:使用 WebSocket,零配置即可接收事件
- 生产环境:使用 Webhook,配合域名和负载均衡
2.3 配置应用权限
在飞书开放平台的应用管理页面,进入「权限管理」,添加以下权限:
| 权限名称 | 权限标识 | 用途 | 必须 |
|---|---|---|---|
| 获取与发送单聊、群组消息 | im:message |
发送消息到群/单聊 | 是 |
| 读取消息 | im:message:readonly |
接收消息事件 | 是 |
| 获取群组信息 | im:chat:readonly |
获取群信息 | 否 |
| 获取用户信息 | contact:user.id:readonly |
识别消息发送者 | 否 |
2.4 配置事件订阅
WebSocket 模式(开发环境)
- 进入「事件订阅」页面
- 选择「使用长连接接收事件」
- 添加事件:
im.message.receive_v1(接收消息) - 勾选以下子事件:
- ✅ 取群组中其他机器人和用户@当前机器人的消息
- ✅ 读取用户发给机器人的单聊消息
- 无需配置回调地址
Webhook 模式(生产环境)
- 进入「事件订阅」页面
- 选择「将事件发送至开发者服务器」
- 配置请求地址:
https://your-domain.com/feishu/event/callback - 记录 Encrypt Key 和 Verification Token
- 添加事件:
im.message.receive_v1(接收消息) - 勾选以下子事件:
- ✅ 取群组中其他机器人和用户@当前机器人的消息
- ✅ 读取用户发给机器人的单聊消息
- 飞书会发送验证请求,需确保服务已启动并能正确响应
2.5 发布应用与添加到群
- 在「版本管理与发布」中创建版本并提交审核
- 审核通过后,将机器人添加到目标飞书群:
- 打开目标群 → 群设置 → 群机器人 → 添加机器人
- 选择刚创建的应用机器人
2.6 配置飞书机器人能力
在应用管理页面,进入「应用能力」→「机器人」:
- 开启机器人能力
- 配置机器人名称和头像
- 设置机器人描述
三、依赖配置
3.1 添加飞书 SDK 依赖
在 pom.xml 中添加飞书官方 SDK:
xml
<dependency>
<groupId>com.larksuite.oapi</groupId>
<artifactId>oapi-sdk</artifactId>
<version>2.4.4</version>
</dependency>
3.2 配置飞书应用信息
在 application.yml 中添加飞书相关配置:
yaml
# ============ 飞书配置 ============
feishu:
app-id: "${FEISHU_APP_ID}"
app-secret: "${FEISHU_APP_SECRET}"
event-mode: websocket # 开发环境用 websocket,生产用 webhook
# Webhook 模式配置(生产环境)
encrypt-key: "${FEISHU_ENCRYPT_KEY:}"
verification-token: "${FEISHU_VERIFICATION_TOKEN:}"
callback-path: /feishu/event/callback
logging:
level:
io.modelcontextprotocol: debug
com.example.mcpclient: debug
四、核心代码实现
4.1 飞书消息模型
创建 FeishuMessage.java 封装飞书消息:
java
package com.example.mcpclient.feishu.model;
import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1;
import lombok.Data;
@Data
public class FeishuMessage {
private String messageId;
private String chatId;
private String chatType; // "p2p" 单聊, "group" 群聊
private String userId;
private String text;
private boolean mentionBot;
public static FeishuMessage from(P2MessageReceiveV1 event) {
FeishuMessage msg = new FeishuMessage();
msg.setMessageId(event.getEvent().getMessage().getMessageId());
msg.setChatId(event.getEvent().getMessage().getChatId());
msg.setChatType(event.getEvent().getMessage().getChatType());
msg.setUserId(event.getEvent().getSender().getSenderId().getOpenId());
// 解析消息内容
String content = event.getEvent().getMessage().getContent();
msg.setText(parseTextContent(content));
// 判断是否 @ 了机器人(群聊需要 @,单聊不需要)
msg.setMentionBot(isMentionBot(event));
return msg;
}
private static String parseTextContent(String content) {
try {
com.fasterxml.jackson.databind.ObjectMapper mapper =
new com.fasterxml.jackson.databind.ObjectMapper();
var node = mapper.readTree(content);
return node.get("text").asText();
} catch (Exception e) {
return content;
}
}
private static boolean isMentionBot(P2MessageReceiveV1 event) {
var mentions = event.getEvent().getMessage().getMentions();
return mentions != null && mentions.length > 0;
}
public String getTextWithoutMention() {
if (!mentionBot) return text;
return text.replaceAll("@\\S+\\s*", "").trim();
}
public boolean isP2P() {
return "p2p".equals(chatType);
}
}
4.2 飞书事件监听器
创建 FeishuEventListener.java 处理飞书消息事件:
java
package com.example.mcpclient.feishu.listener;
import com.example.mcpclient.feishu.executor.McpExecutor;
import com.example.mcpclient.feishu.model.FeishuMessage;
import com.lark.oapi.event.EventDispatcher;
import com.lark.oapi.service.im.ImService;
import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
@Slf4j
public class FeishuEventListener {
private final McpExecutor mcpExecutor;
@Value("${feishu.verification-token:}")
private String verificationToken;
@Value("${feishu.encrypt-key:}")
private String encryptKey;
@Bean
public EventDispatcher eventDispatcher() {
return EventDispatcher.newBuilder(verificationToken, encryptKey)
.onP2MessageReceiveV1(new ImService.P2MessageReceiveV1Handler() {
@Override
public void handle(P2MessageReceiveV1 event) {
handleMessage(event);
}
})
.build();
}
private void handleMessage(P2MessageReceiveV1 event) {
try {
FeishuMessage msg = FeishuMessage.from(event);
// 单聊消息直接处理,群聊消息需要 @ 机器人才处理
if (!msg.isP2P() && !msg.isMentionBot()) {
log.debug("群聊消息未 @ 机器人,忽略: {}", msg.getText());
return;
}
log.info("收到飞书消息 - chatType: {}, chatId: {}, userId: {}, text: {}",
msg.getChatType(), msg.getChatId(), msg.getUserId(), msg.getText());
// 直接将用户消息发给 LLM,由 LLM 自主决策工具调用
String userText = msg.getTextWithoutMention();
mcpExecutor.execute(msg.getChatId(), msg.getUserId(), userText);
} catch (Exception e) {
log.error("处理飞书消息异常", e);
}
}
}
4.3 MCP 执行器
创建 McpExecutor.java 协调工具调用流程:
java
package com.example.mcpclient.feishu.executor;
import com.example.mcpclient.feishu.sender.FeishuMessageSender;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class McpExecutor {
private final ChatClient chatClient;
private final FeishuMessageSender messageSender;
@Async
public void execute(String chatId, String userId, String userMessage) {
long startTime = System.currentTimeMillis();
try {
log.info("开始处理消息 - chatId: {}, userId: {}, message: {}",
chatId, userId, userMessage);
// 直接调用 ChatClient,LLM 会自动决策是否调用工具
String result = chatClient.prompt()
.user(userMessage)
.call()
.content();
// 发送结果回飞书
messageSender.sendTextMessage(chatId, result);
log.info("消息处理完成 - chatId: {}, 耗时: {}ms",
chatId, System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error("消息处理失败 - chatId: {}", chatId, e);
messageSender.sendTextMessage(chatId, "抱歉,服务暂时出现问题,请稍后重试。");
}
}
}
4.4 飞书消息发送器
创建 FeishuMessageSender.java 封装消息发送:
java
package com.example.mcpclient.feishu.sender;
import com.lark.oapi.Client;
import com.lark.oapi.service.im.v1.enums.CreateMessageReceiveIdTypeEnum;
import com.lark.oapi.service.im.v1.enums.MsgTypeEnum;
import com.lark.oapi.service.im.v1.model.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class FeishuMessageSender {
private final Client feishuClient;
public void sendTextMessage(String chatId, String text) {
try {
String safeText = escapeJson(text);
String truncatedText = truncateToLimit(safeText, 7900);
String content = String.format("{\"text\":\"%s\"}", truncatedText);
CreateMessageReq req = CreateMessageReq.newBuilder()
.receiveIdType(CreateMessageReceiveIdTypeEnum.CHAT_ID)
.createMessageReqBody(CreateMessageReqBody.newBuilder()
.receiveId(chatId)
.msgType(MsgTypeEnum.MSG_TYPE_TEXT.getValue())
.content(content)
.build())
.build();
var resp = feishuClient.im().message().create(req);
if (resp.success()) {
log.info("消息发送成功 - chatId: {}, messageId: {}",
chatId, resp.getData().getMessageId());
} else {
log.error("消息发送失败 - code: {}, msg: {}",
resp.getCode(), resp.getMsg());
}
} catch (Exception e) {
log.error("发送飞书消息异常", e);
}
}
private String escapeJson(String text) {
return text.replace("\\", "\\\\").replace("\"", "\\\"")
.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
}
private String truncateToLimit(String text, int limit) {
return text.length() <= limit ? text : text.substring(0, limit - 10) + "\n...(内容已截断)";
}
}
4.5 飞书客户端配置
创建 FeishuClientConfig.java 配置飞书客户端:
java
package com.example.mcpclient.feishu.config;
import com.lark.oapi.Client;
import com.lark.oapi.event.EventDispatcher;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
@Slf4j
public class FeishuClientConfig {
@Value("${feishu.app-id}")
private String appId;
@Value("${feishu.app-secret}")
private String appSecret;
@Bean
public Client feishuClient() {
return Client.newBuilder(appId, appSecret).build();
}
@Bean(initMethod = "start")
@ConditionalOnProperty(name = "feishu.event-mode", havingValue = "websocket")
public FeishuWebSocketClient feishuWebSocketClient(EventDispatcher eventDispatcher) {
log.info("初始化飞书 WebSocket 客户端");
return new FeishuWebSocketClient(appId, appSecret, eventDispatcher);
}
}
4.6 WebSocket 客户端实现
飞书 SDK 内置了 com.lark.oapi.ws.Client,封装了 WSS 握手鉴权、心跳保活和断线重连:
java
package com.example.mcpclient.feishu.config;
import com.lark.oapi.event.EventDispatcher;
import com.lark.oapi.ws.Client;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FeishuWebSocketClient {
private final Client wsClient;
public FeishuWebSocketClient(String appId, String appSecret, EventDispatcher eventDispatcher) {
this.wsClient = new Client.Builder(appId, appSecret)
.eventHandler(eventDispatcher)
.autoReconnect(true)
.build();
}
public void start() {
log.info("飞书 WebSocket 客户端启动,发起 WSS 连接...");
wsClient.start();
}
}
关键点 :
wsClient.start()会发起 WSS 握手鉴权,SDK 内部自动处理心跳和断线重连。如果不调用start(),WebSocket 连接不会建立,事件也无法接收。
4.7 Webhook 回调控制器(生产环境)
当使用 Webhook 模式时,需要创建回调控制器接收飞书事件推送:
java
package com.example.mcpclient.feishu.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lark.oapi.core.request.EventReq;
import com.lark.oapi.event.EventDispatcher;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@ConditionalOnProperty(name = "feishu.event-mode", havingValue = "webhook")
@RequiredArgsConstructor
@Slf4j
public class FeishuWebhookController {
private final EventDispatcher eventDispatcher;
private final ObjectMapper objectMapper = new ObjectMapper();
@Value("${feishu.callback-path:/feishu/event/callback}")
private String callbackPath;
@PostMapping("${feishu.callback-path:/feishu/event/callback}")
public Map<String, String> handleEvent(@RequestBody String body) {
log.debug("收到飞书 Webhook 回调: {}", body);
try {
// 处理 URL 验证挑战
var node = objectMapper.readTree(body);
if (node.has("type") && "url_verification".equals(node.get("type").asText())) {
return Map.of("challenge", node.get("challenge").asText());
}
// 使用 EventDispatcher 解析并分发事件
EventReq eventReq = new EventReq();
eventReq.setBody(body.getBytes());
eventReq.setHttpPath(callbackPath);
eventDispatcher.parseReq(eventReq);
} catch (Exception e) {
log.error("事件处理失败", e);
}
return Map.of("code", "0");
}
}
4.8 启用异步支持
在启动类中添加 @EnableAsync 注解:
java
package com.example.mcpclient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class McpClientApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientApplication.class, args);
}
}
五、完整配置清单
5.1 application.yml 最终配置
yaml
feishu:
app-id: "${FEISHU_APP_ID}"
app-secret: "${FEISHU_APP_SECRET}"
event-mode: websocket
encrypt-key: "${FEISHU_ENCRYPT_KEY:}"
verification-token: "${FEISHU_VERIFICATION_TOKEN:}"
callback-path: /feishu/event/callback
logging:
level:
io.modelcontextprotocol: debug
com.example.mcpclient: debug
com.lark.oapi: debug
5.2 环境变量说明
| 变量名 | 说明 | 示例 |
|---|---|---|
FEISHU_APP_ID |
飞书应用 ID | cli_xxxxx |
FEISHU_APP_SECRET |
飞书应用密钥 | xxxxxx |
FEISHU_ENCRYPT_KEY |
消息加密密钥(Webhook 模式) | xxxxxx |
FEISHU_VERIFICATION_TOKEN |
事件校验令牌(Webhook 模式) | xxxxxx |
MINIMAX_API_KEY |
MiniMax API 密钥 | sk-xxxxx |
六、测试与验证
6.1 测试流程
-
启动 MCP Server (确保
http://localhost:8080/mcp可访问) -
启动本服务 :
bashmvn spring-boot:run -
在飞书群中 @ 机器人 :
@机器人 查询北京今天的天气@机器人 帮我查一下订单 ORDER-123456@机器人 你好
6.2 预期结果
| 用户消息 | LLM 决策 | 预期回复 |
|---|---|---|
@机器人 查询北京今天的天气 |
自动调用 local_weather_query(city="北京") |
北京当前天气信息 |
@机器人 帮我查一下订单 ORDER-123456 |
自动调用 MCP Server 的订单查询工具 | 订单详情 |
@机器人 你好 |
不调用工具,直接回复 | AI 闲聊回复 |
七、生产环境部署建议
7.1 切换到 Webhook 模式
修改配置:
yaml
feishu:
event-mode: webhook
callback-path: /feishu/event/callback
7.2 配置 Nginx 反向代理
nginx
server {
listen 80;
server_name your-domain.com;
location /feishu/event/callback {
proxy_pass http://localhost:8081;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
7.3 配置飞书事件订阅
在飞书开放平台配置:
- 事件订阅地址 :
https://your-domain.com/feishu/event/callback - 加密密钥:与配置文件一致
- 校验令牌:与配置文件一致
八、总结
本文完成了从 0 到 1 的飞书机器人 MCP Client 集成,核心流程如下:
飞书消息 → FeishuEventListener → McpExecutor
→ ChatClient(LLM 自主决策工具调用)→ 工具执行
→ FeishuMessageSender → 飞书群回复
8.1 项目结构
src/main/java/com/example/mcpclient/
└── feishu/ # 飞书模块(新增)
├── config/
│ ├── FeishuClientConfig.java
│ └── FeishuWebSocketClient.java
├── controller/
│ └── FeishuWebhookController.java # Webhook 回调(生产环境)
├── listener/
│ └── FeishuEventListener.java
├── executor/
│ └── McpExecutor.java
├── sender/
│ └── FeishuMessageSender.java
└── model/
└── FeishuMessage.java
8.2 核心优势
- 智能决策:LLM 根据工具描述自动选择调用哪个工具,无需硬编码命令匹配
- 低延迟:直接桥接模式,端到端延迟 < 5s
- 高可用:支持 WebSocket/Webhook 双模式
- 易扩展:通过配置即可新增 MCP Server 连接,LLM 自动发现新工具
- 优雅降级:完善的异常处理和友好提示