一、项目概述

本文基于现有的 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 创建飞书应用

  1. 登录 飞书开放平台
  2. 点击「创建企业自建应用」
  3. 填写应用名称(如 “MCP 智能助手”)和描述
  4. 创建完成后,在「凭证与基础信息」页面获取 App IDApp 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 模式(开发环境)
  1. 进入「事件订阅」页面
  2. 选择「使用长连接接收事件」
  3. 添加事件:im.message.receive_v1(接收消息)
  4. 勾选以下子事件:
    • ✅ 取群组中其他机器人和用户@当前机器人的消息
    • ✅ 读取用户发给机器人的单聊消息
  5. 无需配置回调地址
Webhook 模式(生产环境)
  1. 进入「事件订阅」页面
  2. 选择「将事件发送至开发者服务器」
  3. 配置请求地址:https://your-domain.com/feishu/event/callback
  4. 记录 Encrypt KeyVerification Token
  5. 添加事件:im.message.receive_v1(接收消息)
  6. 勾选以下子事件:
    • ✅ 取群组中其他机器人和用户@当前机器人的消息
    • ✅ 读取用户发给机器人的单聊消息
  7. 飞书会发送验证请求,需确保服务已启动并能正确响应

2.5 发布应用与添加到群

  1. 在「版本管理与发布」中创建版本并提交审核
  2. 审核通过后,将机器人添加到目标飞书群:
    • 打开目标群 → 群设置 → 群机器人 → 添加机器人
    • 选择刚创建的应用机器人

2.6 配置飞书机器人能力

在应用管理页面,进入「应用能力」→「机器人」:

  1. 开启机器人能力
  2. 配置机器人名称和头像
  3. 设置机器人描述

三、依赖配置

3.1 添加飞书 SDK 依赖

pom.xml 中添加飞书官方 SDK:

<dependency>
    <groupId>com.larksuite.oapi</groupId>
    <artifactId>oapi-sdk</artifactId>
    <version>2.4.4</version>
</dependency>

3.2 配置飞书应用信息

application.yml 中添加飞书相关配置:

# ============ 飞书配置 ============
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 封装飞书消息:

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 处理飞书消息事件:

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 协调工具调用流程:

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 封装消息发送:

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 配置飞书客户端:

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 握手鉴权、心跳保活和断线重连:

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 模式时,需要创建回调控制器接收飞书事件推送:

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 注解:

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 最终配置

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 测试流程

  1. 启动 MCP Server(确保 http://localhost:8080/mcp 可访问)
  2. 启动本服务
    mvn spring-boot:run
    
  3. 在飞书群中 @ 机器人
    • @机器人 查询北京今天的天气
    • @机器人 帮我查一下订单 ORDER-123456
    • @机器人 你好

6.2 预期结果

用户消息 LLM 决策 预期回复
@机器人 查询北京今天的天气 自动调用 local_weather_query(city="北京") 北京当前天气信息
@机器人 帮我查一下订单 ORDER-123456 自动调用 MCP Server 的订单查询工具 订单详情
@机器人 你好 不调用工具,直接回复 AI 闲聊回复

七、生产环境部署建议

7.1 切换到 Webhook 模式

修改配置:

feishu:
  event-mode: webhook
  callback-path: /feishu/event/callback

7.2 配置 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 核心优势

  1. 智能决策:LLM 根据工具描述自动选择调用哪个工具,无需硬编码命令匹配
  2. 低延迟:直接桥接模式,端到端延迟 < 5s
  3. 高可用:支持 WebSocket/Webhook 双模式
  4. 易扩展:通过配置即可新增 MCP Server 连接,LLM 自动发现新工具
  5. 优雅降级:完善的异常处理和友好提示
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐