VoiceSideStT RocketMQ 消息对接接口文档

  • 文档版本:1.2
  • 发布日期:2026-08-02
  • 适用对象:下游消费端开发者(订阅 RocketMQ Topic、解析 JSON 用于智能填单/质检落库等业务方)
  • 生产端实现:com.pbxbusiness.sidestt.VoiceSideSttNotifyerRocketMQ
  • 消息体定义:com.pbxbusiness.sidestt.json.SideSttMqMessage
  • 变更记录:
    • v1.2 (2026-08-02):新增 voiceFile 字段(当次语音识别对应的录音文件路径),并入可选字段统一推送策略
    • v1.1 (2026-07-20):补充 markedText 字段,明确可选字段统一推送策略
    • v1.0 (2026-07-20):首版发布

1. 概述

TelCRM 在通话过程中对人工坐席侧进行实时语音识别(SideSTT)。每当识别到一段完整的说话内容,TelCRM 会将结果组装为一条 JSON 消息,推送到 RocketMQ 的指定 Topic。

本文档面向消费端开发者,说明:

  • 如何连接并订阅该 Topic;
  • 消息的字段结构与取值规则;
  • 如何解析 JSON;
  • 联调与排错方式。

说明:TelCRM 在此链路中是生产端(Producer),对接方是消费端(Consumer)。


2. 接入前置条件

消费端开始对接前,需向 TelCRM 运维侧获取以下信息:

项目说明
NameServer 地址RocketMQ NameServer 列表,多个以 ; 分隔
Topic 名称由 TelCRM 服务端配置,消息发往的 Topic
Tag消息子主题/标签,用于消费端过滤
Consumer Group消费端自定义,同一组内集群消费
AccessKey / SecretKey仅当服务端开启 ACL 鉴权时需要,由运维分配

以上值由 TelCRM 运维侧分配,本文档不写死具体值。


3. 消费连接参数

参数取值 / 建议备注
NameServer由运维提供多个地址以 ; 分隔
Topic由运维提供例如业务约定的质检 Topic
Tag由运维提供订阅时用 Topic:Tag 形式;接收全部 Tag 用 Topic:*
Consumer Group消费端自定义建议与业务系统命名一致
消费模式CLUSTERING(集群)一条消息在同一 Group 内只被一个消费者消费;广播模式按需选择
ACL 鉴权视服务端配置若开启,需在创建 Consumer 时传入 AclClientRPCHook
消息体编码UTF-8Body 字节流必须按 UTF-8 解码,不要按 GBK 解码(否则中文乱码)

4. 消息协议

4.1 Message 元信息

属性说明
Topic由服务端 rocket.mq.topic 配置
Tag由服务端 rocket.mq.tags 配置
Keys形如 key_1、key_2 ... 的自增序号,便于按 key 查询/去重
BodyUTF-8 编码的 JSON 字符串

4.2 Body 固定字段

每条消息的 JSON Body 都包含以下固定字段:

字段类型必现含义
uuidstring是会话唯一标识(通话 ID),用于关联同一通通话的所有消息
call_hashstring是主叫号码
legstring是通话腿。aleg = 主叫侧说话;bleg = 被叫侧说话
offerstring是来源标识(对应服务端配置的 Topic 名,作为签名/来源标识)
contentstring是本次识别出的语音文本内容;可为空字符串
datestring是消息产生时间,格式 yyyy-MM-dd HH:mm:ss
timestampstring是毫秒级时间戳(1970-01-01 至今的毫秒数)

可选字段(仅在对应能力可用时出现,为空时不输出):

字段类型必现含义
emotionstring否情绪/态度标签,取值为枚举名(大写),见下方取值表。引擎无情绪能力或识别失败时不输出
speechRatenumber(float)否语速,单位字/秒。无法计算时不输出
markedTextstring否带情绪标记的文本,格式 <EMO>句文(如 <HAPPY>您好)。仅 funasr 多段解析产出时出现,其他引擎不输出。与 content 的关系:content 为纯文本,markedText 在每句前附带情绪标签
voiceFilestring否当次语音识别对应的录音文件路径。仅当上游识别流程产出了非空录音路径时输出;为空时省略

emotion 取值表

实际输出为枚举名(大写字符串):

枚举值含义
HAPPY高兴
SAD悲伤
ANGRY愤怒
NEUTRAL中性
FEARFUL恐惧
DISGUSTED厌恶
SURPRISED惊讶
UNKNOWN引擎返回了情绪标签但不在标准集内(识别失败兜底)

可选字段推送策略

emotion / speechRate / markedText / voiceFile 四个字段遵循同一推送策略:

  • 有值才推送:仅在对应能力可用且能计算/解析出非空值时才写入消息;
  • 为空则省略:当值为 null 或空字符串时,整个字段不会出现在 JSON 中(而非输出 null);
  • 消费端处理原则:字段缺失 = 该条消息无对应数据(而非异常),按业务默认值处理即可。

注意:不同引擎能力不同。例如非 funasr 引擎通常既无 emotion/speechRate,也无 markedText;voiceFile 是否输出取决于上游识别流程是否产出了当次说话的录音路径,与引擎类型无强绑定。

4.3 动态扩展字段

除上述固定字段外,消息体可能额外携带若干由上游业务注入的扩展字段:

  • 这些字段的 key 与 value 均为字符串,由上游业务逻辑决定;
  • key 与 value 均非空时才会出现;
  • 已排除 opt、extnum 两个内部字段,不会出现在消息中;
  • 字段集不固定,本文档不枚举具体 key。

消费端建议:按「未知字段宽容解析」策略处理——反序列化时忽略未识别的字段,不要因出现文档未列出的字段而报错。


5. JSON 示例

示例 1:主叫侧说话,带情绪/语速/带标记文本/录音路径(funasr 引擎)

{
  "uuid": "CALL-20260720-0001",
  "call_hash": "13800138000",
  "leg": "aleg",
  "offer": "stt_quality_topic",
  "content": "您好,请问需要什么帮助?",
  "date": "2026-07-20 10:23:45",
  "timestamp": "1779003825000",
  "emotion": "NEUTRAL",
  "speechRate": 3.8,
  "markedText": "<NEUTRAL>您好,请问需要什么帮助?",
  "voiceFile": "/record/20260720/CALL-20260720-0001/aleg/1779003825000.wav"
}

示例 2:被叫侧说话,无情绪/语速/带标记文本(非 funasr 引擎)

{
  "uuid": "CALL-20260720-0001",
  "call_hash": "13800138000",
  "leg": "bleg",
  "offer": "stt_quality_topic",
  "content": "我想咨询一下套餐资费",
  "date": "2026-07-20 10:23:48",
  "timestamp": "1779003828000"
}

示例 3:含动态扩展字段

{
  "uuid": "CALL-20260720-0002",
  "call_hash": "13900139000",
  "leg": "aleg",
  "offer": "stt_quality_topic",
  "content": "好的,马上为您办理。",
  "date": "2026-07-20 10:25:10",
  "timestamp": "1779003910000",
  "agentId": "8001",
  "deptCode": "sales"
}

上例中的 agentId、deptCode 即动态扩展字段,仅作示例,实际 key 以上游业务注入为准。


6. 消费端示例代码(Java)

使用 RocketMQ 官方客户端 rocketmq-client,以集群模式订阅,含 ACL 鉴权与 JSON 解析:

import com.alibaba.fastjson.JSON;          // 或 com.google.gson.Gson
import com.google.gson.Gson;
import org.apache.rocketmq.acl.common.AclClientRPCHook;
import org.apache.rocketmq.acl.common.SessionCredentials;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

public class SideSttConsumer {

    public static void main(String[] args) throws Exception {
        String namesrvAddr = "127.0.0.1:9876";          // 由运维提供
        String topic       = "stt_quality_topic";        // 由运维提供
        String tag         = "sidestt";                  // 由运维提供
        String consumerGroup = "cid_sidestt_consumer";

        // 1. 构建 Consumer(若服务端未开启 ACL,去掉 AclClientRPCHook 参数即可)
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(
                consumerGroup,
                new AclClientRPCHook(new SessionCredentials(
                        "YOUR_ACCESS_KEY",   // 由运维提供
                        "YOUR_SECRET_KEY"))); // 由运维提供

        consumer.setNamesrvAddr(namesrvAddr);
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        consumer.subscribe(topic, tag);   // 订阅指定 Tag;接收全部 Tag 用 "*"

        // 2. JSON 解析器(生产端用 Gson 序列化,消费端用 Gson/fastjson 均可)
        Gson gson = new Gson();

        // 3. 注册监听
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                // ★ Body 为 UTF-8 字节流,务必按 UTF-8 解码
                String body = new String(msg.getBody(), "UTF-8");

                // 反序列化到与生产端对称的 POJO(字段名一致即可)
                SideSttMessage dto = gson.fromJson(body, SideSttMessage.class);

                // 业务处理:落库 / 智能填单 / 实时大屏 ...
                handle(dto);
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();
        System.out.println("SideSTT Consumer started.");
    }

    private static void handle(SideSttMessage dto) {
        // TODO: 落库/转发/填单等业务逻辑
    }

    /** 与生产端 SideSttMqMessage(com.pbxbusiness.sidestt.json)字段对称的 POJO。 */
    static class SideSttMessage {
        private String uuid;
        private String call_hash;     // Gson 默认按字段名映射;若改名 callHash 需加 @SerializedName("call_hash")
        private String leg;
        private String offer;
        private String content;
        private String date;
        private String timestamp;
        private String emotion;       // 可空:缺失即无情绪数据
        private Float speechRate;     // 可空:缺失即无语速数据
        private String markedText;    // 可空:缺失即无带标记文本
        private String voiceFile;     // 可空:缺失即无当次录音路径

        // getter / setter 省略...
    }
}

Maven 依赖(版本按服务端 RocketMQ 对齐):

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>4.9.x</version>
</dependency>
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.5</version>
</dependency>

7. 字段稳定性与版本约定

类别字段稳定性
稳定契约uuid、call_hash、leg、offer、content、date、timestamp必现、语义不变,可放心依赖
可选契约emotion、speechRate、markedText、voiceFile条件出现;消费端须处理「字段缺失」情况
开放契约动态扩展字段不保证 key 集合稳定;消费端按未知字段宽容解析

演进原则:

  • 字段只增不改不删:新增字段不会破坏现有反序列化(Gson/fastjson 默认忽略未知字段);
  • 不会改变已有字段的语义与类型;
  • 如遇必须的重大变更,将另行通知并提升文档主版本号。

8. 联调与 Mock 调试

8.1 Mock 模式(不发真实 MQ)

TelCRM 服务端配置项 rocket.mq.mock.send:

取值行为
true不真实发送 MQ,仅在 TelCRM 服务端日志中打印消息体(便于对接方先核对格式)
false(默认)正常发送到 RocketMQ

联调初期可请运维临时置为 true,由 TelCRM 侧在日志中确认消息体格式正确后,再切回 false 实送。

8.2 查看消息体明细

TelCRM 打开 debug 日志后,VoiceSideSttNotifyerRocketMQ 会输出:

[MQ] makeMessage( gbk to utf-8 ):
{"uuid":"...","call_hash":"...","leg":"aleg",...}

可用于核对字段、顺序、编码是否符合预期。


9. 常见问题(FAQ)

Q1:收到消息是乱码? A:确认消费端按 UTF-8 解码 Body(new String(msg.getBody(), "UTF-8"))。生产端 Body 是 UTF-8 字节流,若按 GBK 解码会乱码。

Q2:有的消息没有 emotion / speechRate / markedText / voiceFile 字段? A:这四个是可选字段,遵循同一推送策略——仅在引擎具备对应能力且能计算/解析出非空值时输出。字段缺失即代表无该数据,按业务默认值处理,不要将其当作错误。例如非 funasr 引擎通常前三者都不输出;voiceFile 是否输出取决于上游识别流程是否产出了当次说话的录音路径。

Q2.1:content 和 markedText 有什么区别? A:content 是纯识别文本,始终存在;markedText 是带情绪标签的文本(<EMO>句文 格式),仅 funasr 多段解析产出时出现。两者内容主体一致,markedText 额外携带逐句情绪标记。

Q3:同一通通话的多条消息如何关联? A:用 uuid 字段。同一通通话内,主叫与被叫说话都会产出消息,uuid 相同,通过 leg 区分 aleg/bleg。

Q4:消息里出现了文档没列出的字段? A:那是上游业务注入的动态扩展字段(字符串值)。消费端应宽容解析,忽略未识别字段。opt、extnum 不会出现。

Q5:消息顺序有保证吗? A:同一 Queue 内消息按发送顺序投递。如对顺序敏感,消费端可按 timestamp 字段排序后处理。

Q6:如何判断一条消息对应哪一侧说话? A:看 leg 字段:aleg = 主叫侧,bleg = 被叫侧。


附录:相关源码索引

文件作用
PbxBusiness/src/com/pbxbusiness/sidestt/VoiceSideSttNotifyerRocketMQ.javaMQ 生产端,组装并发送消息
PbxBusiness/src/com/pbxbusiness/sidestt/json/SideSttMqMessage.java消息体 POJO 定义
PbxBusiness/src/com/pbxbusiness/sidestt/VoiceSideSttListenerImpl.javaSideSTT 监听实现,注入扩展字段
PbxBusiness/src/com/pbxbusiness/config/ConfigConstants.javarocket.mq.* 配置项定义