钉钉openclaw插件调研及本地案例学习系列-消息表情反馈功能

文章目录

前言

博主介绍:✌目前全网粉丝4W+,csdn博客专家、Java领域优质创作者,博客之星、阿里云平台优质作者、专注于Java后端技术领域。

涵盖技术内容:Java后端、大数据、算法、分布式微服务、中间件、前端、运维等。

博主所有博客文件目录索引:博客目录索引(持续更新)

CSDN搜索:长路

视频平台:b站-Coder长路

一、背景与概述

1.1 调研背景

调研目的

在研发内部 AI Chat 项目时,需要实现一个用户体验优化功能:当用户发送消息后,系统能够立即在消息上贴上回复表情,向用户反馈消息已被接收并正在处理。这个功能可以提升用户体验,让用户无需等待就能知道消息的状态,避免重复发送或不确定消息是否已被接收的情况。

场景需求

  • 用户发送消息后,立即在消息上贴上「处理中」或「思考中」表情
  • 消息处理完成后,自动撤回表情或更新为「已完成」状态
  • 支持队列场景:当系统繁忙时,消息进入队列时也应贴上表情反馈
  • 表情操作失败不应影响主消息处理流程

开源项目来源

本次调研基于钉钉官方开源项目:

该项目已经实现了完整的表情反馈功能,包括贴表情、撤回表情、队列优化等,是我们学习和参考的优秀案例。

1.2 功能概述

钉钉openclaw-connector实现了一个用户体验优化功能:当用户发送消息后,机器人会立即在消息上贴上「🤔思考中」表情,用于表示机器人正在处理该消息。处理完成后,机器人会自动撤回这个表情。这个功能让用户能够直观地知道消息是否被机器人接收和处理,提升了交互体验。

1.3 功能特性

  • 即时反馈: 消息发送后立即贴表情,用户无需等待
  • 自动撤回: 消息处理完成后自动移除表情
  • 队列优化: 队列繁忙时提前贴表情,配合排队ACK Card提供更好的用户体验
  • 容错机制: 表情操作失败不影响主消息处理流程
  • 完整日志: 所有操作都有详细的日志记录,便于问题排查

1.4 技术栈

  • 编程语言: TypeScript / Java
  • HTTP 客户端: 自定义 dingtalkHttp (基于 axios) / 钉钉官方 Java SDK
  • API 钉钉开放平台: 钉钉机器人 API
  • 日志: 自定义 logger / Slf4j

1.5 相关文档


二、核心实现

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

typescript 复制代码
/**
 * 在用户消息上贴 🤔思考中 表情,表示正在处理
 */
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}`);
  }
}

关键点:

  • 检查 msgIdconversationId 是否存在,不存在则直接返回
  • 获取 Access Token 用于 API 调用
  • 调用钉钉机器人表情回复 API
  • 设置 5 秒超时,防止长时间阻塞
  • 失败只记录警告日志,不影响主流程

撤回表情函数

文件 : src/utils/utils-legacy.ts:427-455

typescript 复制代码
/**
 * 撤回用户消息上的 🤔思考中 表情
 */
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

typescript 复制代码
// ===== 贴处理中表情 =====
// 若队列繁忙时已在入队阶段提前贴过表情,此处跳过,避免重复贴
if (!params.emotionAlreadyAdded) {
  addEmotionReply(config, data, log).catch(err => {
    log?.warn?.(`贴表情失败: ${err.message}`);
  });
}

文件 : src/core/message-handler.ts:1546-1551

typescript 复制代码
// ===== 撤回处理中表情 =====
// 使用 await 确保表情撤销完成后再结束函数
try {
  await recallEmotionReply(config, data, log);
} catch (err: any) {
  log?.warn?.(`撤回表情异常: ${err.message}`);
}

文件 : src/core/message-handler.ts:1644-1646

typescript 复制代码
// 在发送 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

请求体:

json 复制代码
{
  "robotCode": "机器人编码",
  "openMsgId": "消息ID",
  "openConversationId": "会话ID",
  "emotionType": 2,
  "emotionName": "🤔思考中",
  "textEmotion": {
    "emotionId": "2659900",
    "emotionName": "🤔思考中",
    "text": "🤔思考中",
    "backgroundId": "im_bg_1"
  }
}

参数说明:

参数 类型 必填 说明
robotCode string 机器人编码,即 clientId
openMsgId string 消息的唯一标识
openConversationId string 会话的唯一标识
emotionType number 表情类型,2 表示文本表情
emotionName string 表情名称
textEmotion object 文本表情详细信息

撤回表情 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 还提到了相关链接:

文档获取建议

由于钉钉开放平台的文档网站可能存在访问限制,以下方式可以帮助获取官方文档:

  1. 钉钉开发者后台 :登录钉钉开发者后台,在 API Explorer 中搜索 emotion 相关 API
  2. 技术支持工单:向钉钉提交技术支持工单,咨询 emotion API 的详细文档
  3. 钉钉开放社区:在钉钉开放社区或钉钉开发者论坛提问
  4. 钉钉官方 SDK:查看钉钉官方 GitHub 仓库,可能包含 API 使用示例

2.5 测试用例

文件 : tests/utils-legacy/utils-legacy.test.ts:101-120

typescript 复制代码
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

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

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 项目中,对于接入钉钉后可为用户提供了更好的交互体验。

资料获取

大家点赞、收藏、关注、评论啦~

精彩专栏推荐订阅:在下方专栏👇🏻

更多博客与资料可查看👇🏻获取联系方式👇🏻,🍅文末获取开发资源及更多资源博客获取🍅

相关推荐
sinat_333518871 天前
PrintPDF 软件用户体验设计与操作便捷性分析
用户体验·界面设计·操作便捷性
踏雪羽翼2 天前
android 使用Gemini大模型实现图片处理
android·开发语言·ai聊天·ai抠图·ai生图·gemini大模型
厚积而薄发15287 天前
我复刻了一个“会避嫌”的登录页,还把它开源了
css·开源·用户体验
爱学习的程序媛11 天前
【Web前端】优化Core Web Vitals提升用户体验
前端·ui·web·ux·用户体验
爱学习的程序媛11 天前
【Web前端】前端用户体验优化全攻略
前端·ui·交互·web·ux·用户体验
xianzi202014 天前
TreeSize:从用户体验角度解析这款老牌磁盘工具
用户体验·界面设计·磁盘工具·易用性
sinat_3335188714 天前
PDF24 Tools:从用户体验角度解析这款免费PDF工具标签
用户体验·免费软件·界面设计·pdf工具·易用性
Ulyanov20 天前
基于Celery的分布式雷达电子战仿真系统:架构设计与实战指南
分布式·python·队列处理·雷达电子战仿真
仰望尾迹云1 个月前
Chandra AI与Node.js集成:实时聊天应用开发全攻略
node.js·大语言模型·ai聊天·实时对话