Java构建飞书消息格式详解

1. 核心依赖与数据结构定义

在 Java 中实现飞书消息格式构造,推荐使用 Jackson 或 Gson 进行 JSON 序列化,以避免手动拼接字符串导致的转义错误。以下定义了对应 Markdown 和交互式卡片的消息体结构。

java 复制代码
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.List;

/**
 * 飞书消息构造工具类
 * 支持 Markdown 和 Interactive Card 两种核心格式
 */
public class FeishuMessageBuilder {

    private static final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 构造 Markdown 格式消息体
     * 适用于包含代码块、LaTeX 公式的答疑场景
     * 
     * @param title 消息标题
     * @param content 支持 Markdown 语法的正文内容
     * @return 符合飞书协议的 JSON 字符串
     * @throws JsonProcessingException 当 JSON 序列化失败时抛出
     */
    public static String buildMarkdownMessage(String title, String content) throws JsonProcessingException {
        ObjectNode root = objectMapper.createObjectNode();
        root.put("msg_type", "markdown");

        ObjectNode contentNode = objectMapper.createObjectNode();
        contentNode.put("title", title);
        contentNode.put("content", content);
        
        // 将 content 对象序列化为字符串放入根节点,符合飞书协议要求
        root.put("content", objectMapper.writeValueAsString(contentNode));
        
        return objectMapper.writeValueAsString(root);
    }

    /**
     * 构造交互式卡片 (Interactive Card) 消息体
     * 适用于结构化展示、添加反馈按钮的场景
     * 
     * @param question 用户原始问题
     * @param answer AI 生成的回答
     * @param sources 引用来源列表 * @param messageId 当前消息 ID (用于回调追踪)
     * @return 符合飞书协议的 JSON 字符串
     * @throws JsonProcessingException 当 JSON 序列化失败时抛出
     */
    public static String buildInteractiveCardMessage(String question, String answer, List<String> sources, String messageId) throws JsonProcessingException {
        ObjectNode root = objectMapper.createObjectNode();
        root.put("msg_type", "interactive_card");

        ObjectNode cardNode = objectMapper.createObjectNode();
        
        // 配置宽屏模式
        ObjectNode configNode = objectMapper.createObjectNode();
        configNode.put("wide_screen_mode", true);
        cardNode.set("config", configNode);

        // 头部信息
        ObjectNode headerNode = objectMapper.createObjectNode();
        headerNode.put("template", "blue");
        ObjectNode titleNode = objectMapper.createObjectNode();
        titleNode.put("tag", "plain_text");
        titleNode.put("content", "关于\"" + question + "\"的解答");
        headerNode.set("title", titleNode);
        cardNode.set("header", headerNode);

        // 元素列表构建
        ArrayNode elementsNode = objectMapper.createArrayNode();

        // 1. 回答内容模块
        ObjectNode answerDiv = objectMapper.createObjectNode();
        answerDiv.put("tag", "div");
        ObjectNode answerText = objectMapper.createObjectNode();
        answerText.put("tag", "lark_md");
        answerText.put("content", "**🤖 回答:**
" + answer);
        answerDiv.set("text", answerText);
        elementsNode.add(answerDiv);

        // 2. 分割线
        ObjectNode hrNode = objectMapper.createObjectNode();
        hrNode.put("tag", "hr");
        elementsNode.add(hrNode);

        // 3. 参考资料模块
        StringBuilder sourcesBuilder = new StringBuilder("**📚 参考资料:**
");
        for (String src : sources) {
            sourcesBuilder.append("- ").append(src).append("
");
        }
        ObjectNode sourceDiv = objectMapper.createObjectNode();
        sourceDiv.put("tag", "div");
        ObjectNode sourceText = objectMapper.createObjectNode();
        sourceText.put("tag", "lark_md");
        sourceText.put("content", sourcesBuilder.toString());
        sourceDiv.set("text", sourceText);
        elementsNode.add(sourceDiv);

        // 4. 操作按钮模块
        ObjectNode actionDiv = objectMapper.createObjectNode();
        actionDiv.put("tag", "action");
        ArrayNode actionsArray = objectMapper.createArrayNode();

        // 👍 有帮助按钮
        ObjectNode likeBtn = createButton("👍 有帮助", "primary", "good", messageId);
        // 👎 需改进按钮
        ObjectNode dislikeBtn = createButton("👎 需改进", "default", "bad", messageId);
        
        actionsArray.add(likeBtn);
        actionsArray.add(dislikeBtn);
        actionDiv.set("actions", actionsArray);
        elementsNode.add(actionDiv);

        cardNode.set("elements", elementsNode);
        
        // 将 card 对象序列化为字符串
        root.put("content", objectMapper.writeValueAsString(cardNode));

        return objectMapper.writeValueAsString(root);
    }

    /**
     * 辅助方法:创建按钮 JSON 节点
     */
    private static ObjectNode createButton(String textContent, String type, String feedbackType, String msgId) {
        ObjectNode btn = objectMapper.createObjectNode();
        btn.put("tag", "button");
        
        ObjectNode btnText = objectMapper.createObjectNode();
        btnText.put("tag", "plain_text");
        btnText.put("content", textContent);
        btn.set("text", btnText);
        
        btn.put("type", type);
        
        ObjectNode valueNode = objectMapper.createObjectNode();
        valueNode.put("action", "feedback");
        valueNode.put("type", feedbackType);
        valueNode.put("msg_id", msgId);
        btn.set("value", valueNode);
        
        return btn;
    }
}

2. 调用示例与输出验证

以下代码演示了如何调用上述工具类生成具体的飞书消息 Payload,并打印结果以便调试。

java 复制代码
import java.util.Arrays;
import java.util.List;

public class FeishuBotDemo {
    public static void main(String[] args) {
        try {
            // 模拟 AI 生成的回答内容 (包含 LaTeX 公式)
            String aiAnswer = "根据牛顿第二定律,$F=ma$。其中 F 代表力 (N),m 代表质量 (kg)。";
 List<String> sources = Arrays.asList("物理教材第八章", "OpenClaw 知识库文档");
            String currentMsgId = "msg_demo_123456"; // 场景 1: 生成 Markdown 格式消息
            String markdownJson = FeishuMessageBuilder.buildMarkdownMessage("智能答疑助手", aiAnswer);
            System.out.println("=== Markdown 格式 Payload ===");
 System.out.println(markdownJson);

            // 场景 2: 生成交互式卡片格式消息
            String cardJson = FeishuMessageBuilder.buildInteractiveCardMessage(
                "牛顿第二定律是什么?", 
                aiAnswer, 
                sources, 
                currentMsgId
            );
            System.out.println("
=== 交互式卡片格式 Payload ===");
            System.out.println(cardJson);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. 关键实现细节说明

关键点 Java 实现策略 注意事项
JSON 嵌套处理 飞书协议要求 content 字段本身是一个序列化后的 JSON 字符串 ,而非 JSON对象。代码中使用 objectMapper.writeValueAsString(contentNode) 二次序列化 。 若直接放入 ObjectNode 会导致飞书解析失败,必须确保外层 content 是 String 类型。
特殊字符转义 使用 Jackson 库自动处理双引号、换行符等敏感字符,避免手动拼接字符串引发的格式错误 。 尤其在 lark_md标签内容中,若包含用户输入的引号,库函数会自动转义。
动态数据绑定 在构建按钮 value 字段时,动态注入 msg_id,确保后端回调能识别具体消息 。 value 字段在飞书回调中会原样回传,适合存放业务状态标识。
多模态扩展 若需插入图片,可在 elements 数组中添加 tag: img 节点,并填入通过飞书上传接口获取的 image_key。 图片需先调用 /open-apis/im/v1/images 接口上传获取 key 后方可使用。

参考来源

相关推荐
Flynn Liang1 小时前
上线后怎么排查 AI 调用?Spring AI Observability:token/延迟/成本埋点与全链路追踪
java·人工智能·spring·监控·可观测性·observability·springai
测试界的小码农1 小时前
【让 AI 生成的测试用例变成 pytest API 自动化脚本】
人工智能·测试用例·pytest
天天代码码天天2 小时前
我开源了一个家庭备份工具:把手机照片和视频,存回自己的电脑
人工智能
AI源码2 小时前
基于机器学习的古籍文字识别与断句校正系统
人工智能·机器学习
用户8339049606952 小时前
记账大师 AIUI购物记账与交互可靠性实践
人工智能
数智人生局2 小时前
配置驱动的通用 ETL 执行引擎:告别"每个需求写一个 Spark 作业"(改配置不发版)
人工智能
Neurocoda2 小时前
基于 NumPy 从零实现 Tiny-AlexNet 在 MNIST 数据集上的验证
人工智能·深度学习
youcans_2 小时前
【嵌入式软件AI编程】(3)面向 AI 协同开发的 STM32 硬件信息标准化
人工智能·stm32·嵌入式硬件·ai编程
AOI小白新手上路2 小时前
深度学习算力购置方案分析:本地 RTX 4090 服务器 vs 云端租赁
服务器·人工智能·深度学习