钉钉openclaw插件调研及本地案例学习系列-消息表情反馈功能
点击上方蓝字关注我们


一、背景与概述
1.1 调研背景
调研目的
在研发内部 AI Chat 项目时,需要实现一个用户体验优化功能:当用户发送消息后,系统能够立即在消息上贴上回复表情,向用户反馈消息已被接收并正在处理。这个功能可以提升用户体验,让用户无需等待就能知道消息的状态,避免重复发送或不确定消息是否已被接收的情况。
场景需求
-
• 用户发送消息后,立即在消息上贴上「处理中」或「思考中」表情 -
• 消息处理完成后,自动撤回表情或更新为「已完成」状态 -
• 支持队列场景:当系统繁忙时,消息进入队列时也应贴上表情反馈 -
• 表情操作失败不应影响主消息处理流程
开源项目来源
本次调研基于钉钉官方开源项目:
-
• 官方仓库: https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector -
• 项目描述: DingTalk Channel for OpenClaw AI Agent Platform -
• 项目类型: 钉钉机器人集成插件,支持 Stream 模式消息接收
该项目已经实现了完整的表情反馈功能,包括贴表情、撤回表情、队列优化等,是我们学习和参考的优秀案例。
1.2 功能概述
钉钉openclaw-connector实现了一个用户体验优化功能:当用户发送消息后,机器人会立即在消息上贴上「🤔思考中」表情,用于表示机器人正在处理该消息。处理完成后,机器人会自动撤回这个表情。这个功能让用户能够直观地知道消息是否被机器人接收和处理,提升了交互体验。
1.3 功能特性
-
• 即时反馈: 消息发送后立即贴表情,用户无需等待 -
• 自动撤回: 消息处理完成后自动移除表情 -
• 队列优化: 队列繁忙时提前贴表情,配合排队ACK Card提供更好的用户体验 -
• 容错机制: 表情操作失败不影响主消息处理流程 -
• 完整日志: 所有操作都有详细的日志记录,便于问题排查
1.4 技术栈
-
• 编程语言: TypeScript / Java -
• HTTP 客户端: 自定义 dingtalkHttp (基于 axios) / 钉钉官方 Java SDK -
• API 钉钉开放平台: 钉钉机器人 API -
• 日志: 自定义 logger / Slf4j
1.5 相关文档
-
• RELEASE_NOTES_V0.7.7.md – v0.7.7 版本表情反馈功能的详细说明 -
• RELEASE_NOTES_V0.8.3.md – v0.8.3 版本排队场景下的表情优化
二、核心实现
2.1 实现架构
核心文件
|
|
|
|---|---|
src/utils/utils-legacy.ts |
|
src/core/message-handler.ts |
|
src/core/connection.ts |
|
tests/utils-legacy/utils-legacy.test.ts |
|
工作流程
用户发送消息 ↓WebSocket 连接层接收消息 ↓消息处理入口 (message-handler.ts) ↓会话队列管理 ├─ 队列空闲 → 直接进入处理流程 └─ 队列繁忙 → 创建排队ACK Card + 立即贴表情 ↓ 消息处理内部 ↓ 贴表情检查 (emotionAlreadyAdded参数) ├─ 未贴表情 → 调用 addEmotionReply() └─ 已贴表情 → 跳过(队列繁忙场景) ↓ 处理消息内容(AI响应) ↓ finally 块中调用 recallEmotionReply() ↓ 撤回表情
2.2 核心代码实现
贴表情函数
文件: src/utils/utils-legacy.ts:397-425
/** * 在用户消息上贴 🤔思考中 表情,表示正在处理 */export async function addEmotionReply(config: DingtalkConfig, data: any, log?: any): Promise<void> { if (!data.msgId || !data.conversationId) return; try { const token = await getAccessToken(config); const { dingtalkHttp } = await import('./http-client.ts'); await dingtalkHttp.post(`${DINGTALK_API}/v1.0/robot/emotion/reply`, { robotCode: data.robotCode ?? config.clientId, openMsgId: data.msgId, openConversationId: data.conversationId, emotionType: 2, emotionName: '🤔思考中', textEmotion: { emotionId: '2659900', emotionName: '🤔思考中', text: '🤔思考中', backgroundId: 'im_bg_1', }, }, { headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' }, timeout: 5_000, }); log?.info?.(`[DingTalk][Emotion] 贴表情成功: msgId=${data.msgId}`); } catch (err: any) { log?.warn?.(`[DingTalk][Emotion] 贴表情失败(不影响主流程): ${err.message}`); }}
关键点:
-
• 检查 msgId和conversationId是否存在,不存在则直接返回 -
• 获取 Access Token 用于 API 调用 -
• 调用钉钉机器人表情回复 API -
• 设置 5 秒超时,防止长时间阻塞 -
• 失败只记录警告日志,不影响主流程
撤回表情函数
文件: src/utils/utils-legacy.ts:427-455
/** * 撤回用户消息上的 🤔思考中 表情 */export async function recallEmotionReply(config: DingtalkConfig, data: any, log?: any): Promise<void> { if (!data.msgId || !data.conversationId) return; try { const token = await getAccessToken(config); const { dingtalkHttp } = await import('./http-client.ts'); await dingtalkHttp.post(`${DINGTALK_API}/v1.0/robot/emotion/recall`, { robotCode: data.robotCode ?? config.clientId, openMsgId: data.msgId, openConversationId: data.conversationId, emotionType: 2, emotionName: '🤔思考中', textEmotion: { emotionId: '2659900', emotionName: '🤔思考中', text: '🤔思考中', backgroundId: 'im_bg_1', }, }, { headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' }, timeout: 5_000, }); log?.info?.(`[DingTalk][Emotion] 撤回表情成功: msgId=${data.msgId}`); } catch (err: any) { log?.warn?.(`[DingTalk][Emotion] 撤回表情失败(不影响主流程): ${err.message}`); }}
关键点:
-
• 与贴表情函数结构相同 -
• 调用钉钉机器人表情撤回 API -
• 同样使用 5 秒超时 -
• 失败只记录警告日志
消息处理中的表情逻辑
文件: src/core/message-handler.ts:1296-1300
// ===== 贴处理中表情 =====// 若队列繁忙时已在入队阶段提前贴过表情,此处跳过,避免重复贴if (!params.emotionAlreadyAdded) { addEmotionReply(config, data, log).catch(err => { log?.warn?.(`贴表情失败: ${err.message}`); });}
文件: src/core/message-handler.ts:1546-1551
// ===== 撤回处理中表情 =====// 使用 await 确保表情撤销完成后再结束函数try { await recallEmotionReply(config, data, log);} catch (err: any) { log?.warn?.(`撤回表情异常: ${err.message}`);}
文件: src/core/message-handler.ts:1644-1646
// 在发送 ACK 的同时立即贴上思考中表情,让用户知道消息已被接收addEmotionReply(config, data, log).catch(err => { log?.warn?.(`[队列] 贴排队表情失败: ${err.message}`);});
2.3 API 接口详情
贴表情 API
端点: POST https://api.dingtalk.com/v1.0/robot/emotion/reply
请求头:
x-acs-dingtalk-access-token: {access_token}Content-Type: application/json
请求体:
{ "robotCode": "机器人编码", "openMsgId": "消息ID", "openConversationId": "会话ID", "emotionType": 2, "emotionName": "🤔思考中", "textEmotion": { "emotionId": "2659900", "emotionName": "🤔思考中", "text": "🤔思考中", "backgroundId": "im_bg_1" }}
参数说明:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
撤回表情 API
端点: POST https://api.dingtalk.com/v1.0/robot/emotion/recall
请求头: 与贴表情 API 相同
请求体: 与贴表情 API 相同
2.4 官方文档情况
API 确认
从钉钉相关项目和插件中确认以下 API 确实存在:
-
• POST https://api.dingtalk.com/v1.0/robot/emotion/reply– 贴表情 -
• POST https://api.dingtalk.com/v1.0/robot/emotion/recall– 撤回表情
这些 API 在多个钉钉相关的开源项目中被使用,证明了其真实性和可用性。
相关功能提及
在 openclaw issue #22519 中提到:
“这个功能属于钉钉服务端 API 的概念,用于控制机器人消息回复时允许使用的 emoji 表情范围”
该 issue 还提到了相关链接:
-
• https://open.dingtalk.com/document/orgapp/emoji-reaction-scopes
文档获取建议
由于钉钉开放平台的文档网站可能存在访问限制,以下方式可以帮助获取官方文档:
-
1. 钉钉开发者后台:登录钉钉开发者后台,在 API Explorer 中搜索 emotion相关 API -
2. 技术支持工单:向钉钉提交技术支持工单,咨询 emotion API 的详细文档 -
3. 钉钉开放社区:在钉钉开放社区或钉钉开发者论坛提问 -
4. 钉钉官方 SDK:查看钉钉官方 GitHub 仓库,可能包含 API 使用示例
2.5 测试用例
文件: tests/utils-legacy/utils-legacy.test.ts:101-120
it("addEmotionReply and recallEmotionReply skip without ids", async () => { const { addEmotionReply, recallEmotionReply } = await import("../../src/utils/utils-legacy"); const cfg = { clientId: "c", clientSecret: "s" } as any; await addEmotionReply(cfg, {}); await recallEmotionReply(cfg, {}); expect(mockHttpPost).not.toHaveBeenCalled();});it("addEmotionReply posts and handles error", async () => { const { addEmotionReply } = await import("../../src/utils/utils-legacy"); mockHttpPost .mockResolvedValueOnce({ data: { accessToken: "tk", expireIn: 7200 } }) .mockResolvedValueOnce({ data: {} }); const cfg = { clientId: "em-id", clientSecret: "sec" } as any; await addEmotionReply(cfg, { msgId: "m1", conversationId: "c1" }); expect(mockHttpPost).toHaveBeenCalledTimes(2); mockHttpPost.mockRejectedValueOnce(new Error("fail")); const log = { warn: vi.fn(), info: vi.fn(), error: vi.fn() }; await addEmotionReply(cfg, { msgId: "m2", conversationId: "c2" }, log); expect(log.warn).toHaveBeenCalled();});
测试覆盖:
-
• 缺少 msgId 或 conversationId 时的跳过逻辑 -
• 正常贴表情的 API 调用 -
• 错误处理和日志记录
2.6 设计亮点
容错性设计
表情操作失败不会影响主消息处理流程,这是非常重要的设计决策:
-
• 网络问题或 API 错误时,机器人仍能正常处理和回复消息 -
• 用户体验不会因为表情功能异常而受到影响
去重机制
通过 emotionAlreadyAdded 参数实现去重:
-
• 队列繁忙时在入队阶段提前贴表情 -
• 内部处理时跳过重复贴,避免 API 调用浪费
超时控制
API 调用设置 5 秒超时:
-
• 防止网络问题导致长时间阻塞 -
• 确保消息处理流程的及时性
日志完善
所有操作都有详细的日志记录:
-
• 成功操作记录 info 日志 -
• 失败操作记录 warn 日志 -
• 便于问题排查和性能监控
用户体验优化
队列繁忙时的优化处理:
-
• 在创建排队 ACK Card 的同时贴表情 -
• 让用户第一时间知道消息已被接收并排队 -
• 配合排队卡片提供完整的排队状态反馈
2.7 版本历史
-
• v0.7.7 (2026-03-13): 首次引入「🤔思考中」表情反馈功能 -
• v0.8.3 (2026-03-20): 优化队列繁忙时的表情处理逻辑,配合排队 ACK Card 提供更好的用户体验
三、实践应用
3.1 Java 实现案例
实现说明
在内部 AI Chat 项目中,基于钉钉官方 Java SDK (dingtalkrobot_1_0) 实现了表情反馈功能。由于官方 SDK 尚未直接封装表情回复相关的 Request/Response 类,我们通过通用的 doROARequest 方式在 RobotPrivateMessageService 中手动实现这两个接口。
核心代码
文件: RobotPrivateMessageService.java
// 定义表情常量private static final String EMOTION_THINKING_ID = "2659900";private static final String EMOTION_THINKING_NAME = "🤔思考中";private static final String EMOTION_COMPLETED_ID = "133501"; // 示例ID,对应“已完成”类表情private static final String EMOTION_COMPLETED_NAME = "👌搞定啦";/** * 贴“🤔思考中”表情 */public void addThinkingEmotion(String openMsgId, String openConversationId) { processEmotion(openMsgId, openConversationId, "/v1.0/robot/emotion/reply", "AddThinking", EMOTION_THINKING_ID, EMOTION_THINKING_NAME);}/** * 撤回“🤔思考中”表情 */public void recallThinkingEmotion(String openMsgId, String openConversationId) { processEmotion(openMsgId, openConversationId, "/v1.0/robot/emotion/recall", "RecallThinking", EMOTION_THINKING_ID, EMOTION_THINKING_NAME);}/** * 贴“✅已完成”表情 */public void addCompletedEmotion(String openMsgId, String openConversationId) { processEmotion(openMsgId, openConversationId, "/v1.0/robot/emotion/reply", "AddCompleted", EMOTION_COMPLETED_ID, EMOTION_COMPLETED_NAME);}/** * 通用逻辑封装 */private void processEmotion(String openMsgId, String openConversationId, String apiPath, String actionName, String emotionId, String emotionName) { if (openMsgId == null || openConversationId == null) return; try { Map<String, Object> body = new HashMap<>(); body.put("robotCode", robotCode); body.put("openMsgId", openMsgId); body.put("openConversationId", openConversationId); body.put("emotionType", 2); body.put("emotionName", emotionName); Map<String, Object> textEmotion = new HashMap<>(); textEmotion.put("emotionId", emotionId); textEmotion.put("emotionName", emotionName); textEmotion.put("text", emotionName); textEmotion.put("backgroundId", "im_bg_1"); body.put("textEmotion", textEmotion); Map<String, String> headers = new HashMap<>(); headers.put("x-acs-dingtalk-access-token", getLatestAccessToken()); headers.put("Content-Type", "application/json"); com.aliyun.teaopenapi.models.OpenApiRequest req = new com.aliyun.teaopenapi.models.OpenApiRequest() .setHeaders(headers) .setBody(com.aliyun.openapiutil.Client.parseToMap(body)); RuntimeOptions runtime = new RuntimeOptions(); runtime.connectTimeout = 5000; runtime.readTimeout = 5000; robotClient.doROARequest(actionName, "robot_1.0", "HTTP", "POST", "AK", apiPath, "json", req, runtime); log.debug("[Emotion] {} 成功: {}", actionName, openMsgId); } catch (Exception e) { log.warn("[Emotion] {} 失败: {}", actionName, e.getMessage()); }}
使用示例
文件: ChatBotCallbackListener.java
// 开始处理前贴表情String msgId = message.getMsgId();String conversationId = message.getConversationId();// 1. 开始处理前贴表情robotPrivateMessageService.addThinkingEmotion(msgId, conversationId);try { String answer = aiMessageProcessor.process(memoryId, userQuestion); // 2. 发送消息(私聊或群聊) if (!CollectionUtils.isEmpty(atUsers)) { robotPrivateMessageService.sendGroupMessage(answer, message.getConversationId()); } else { robotPrivateMessageService.sendPrivateMessage(userId, answer); }}finally { // 3. 无论成功失败,处理完成后撤回表情 // 4. 处理完成:撤回“思考中”,贴上“已完成” robotPrivateMessageService.recallThinkingEmotion(msgId, conversationId); robotPrivateMessageService.addCompletedEmotion(msgId, conversationId);}
封装要点说明
-
• 兼容性: 使用了 robotClient.doROARequest,这是阿里云 TEA 架构下最基础的调用方式,能够绕过官方 SDK 未定义特定 Model 的限制。 -
• 超时控制: 在 RuntimeOptions中显式设置了 5000ms 的超时时间,与原 TS 代码逻辑一致。 -
• 异常处理: 采用 log.warn记录异常且不向上抛出,确保贴表情失败不会导致回复流程中断。
3.2、本地测试
参考钉钉官方openclaw-dingding插件的效果实现,我们本地测试效果为:
当用户进行发送消息后,钉钉机器人会给它标注一个思考中:

当ai思考完成后,则会给出一个回答,此时就会撤回思考中表情,完成后则撤回思考中表情,并添加一个表情说明搞定啦。

总结
钉钉 openclaw-connector 的表情反馈功能是一个精心设计的用户体验优化功能。通过在用户消息上贴「🤔思考中」表情,让用户能够直观地知道消息是否被机器人接收和处理。
该功能的实现体现了以下工程实践:
-
• 容错性设计: 非核心功能失败不影响主流程 -
• 性能优化: 去重机制和超时控制 -
• 用户体验: 即时反馈和队列优化 -
• 可维护性: 完善的日志和测试覆盖
这个功能虽然是小细节,但对用户体验的提升非常明显,值得在其他聊天机器人项目中借鉴和应用。通过本次调研和实践,我们成功将这个功能集成到了内部的 AI Chat 项目中,对于接入钉钉后可为用户提供了更好的交互体验。


END

扫码二维码
获取更多精彩
感谢您的关注



夜雨聆风