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 后方可使用。 |