From e8b1156bfdf2d938834417d3868d5603533f87a5 Mon Sep 17 00:00:00 2001
From: MakerZorky <1053714527zhq@gmail.com>
Date: Wed, 30 Apr 2025 13:34:00 +0800
Subject: [PATCH 1/4] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0DoubaoASR=E7=83=AD?=
=?UTF-8?q?=E8=AF=8D=E3=80=81=E6=9B=BF=E6=8D=A2=E8=AF=8D=E9=80=89=E9=A1=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/xiaozhi-server/config.yaml | 2 ++
.../core/providers/asr/doubao.py | 18 +++++++++---------
2 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml
index 21266333..7565179c 100644
--- a/main/xiaozhi-server/config.yaml
+++ b/main/xiaozhi-server/config.yaml
@@ -232,6 +232,8 @@ ASR:
appid: 你的火山引擎语音合成服务appid
access_token: 你的火山引擎语音合成服务access_token
cluster: volcengine_input_common
+ boosting_table_name: 你的热词文件名称
+ correct_table_name: 你的替换词文件名称
output_dir: tmp/
TencentASR:
# token申请地址:https://console.cloud.tencent.com/cam/capi
diff --git a/main/xiaozhi-server/core/providers/asr/doubao.py b/main/xiaozhi-server/core/providers/asr/doubao.py
index 6fba472c..755a19b7 100644
--- a/main/xiaozhi-server/core/providers/asr/doubao.py
+++ b/main/xiaozhi-server/core/providers/asr/doubao.py
@@ -88,6 +88,8 @@ class ASRProvider(ASRProviderBase):
self.appid = config.get("appid")
self.cluster = config.get("cluster")
self.access_token = config.get("access_token")
+ self.boosting_table_name = config.get("boosting_table_name")
+ self.correct_table_name = config.get("correct_table_name")
self.output_dir = config.get("output_dir")
self.delete_audio_file = delete_audio_file
@@ -136,7 +138,13 @@ class ASRProvider(ASRProviderBase):
"user": {
"uid": str(uuid.uuid4()),
},
- "request": {"reqid": reqid, "show_utterances": False, "sequence": 1},
+ "request": {
+ "reqid": reqid,
+ "show_utterances": False,
+ "sequence": 1,
+ "boosting_table_name": self.boosting_table_name,
+ "correct_table_name": self.correct_table_name,
+ },
"audio": {
"format": "raw",
"rate": 16000,
@@ -232,14 +240,6 @@ class ASRProvider(ASRProviderBase):
return pcm_data
- @staticmethod
- def read_wav_info(data: io.BytesIO = None) -> (int, int, int, int, int):
- with io.BytesIO(data) as _f:
- wave_fp = wave.open(_f, "rb")
- nchannels, sampwidth, framerate, nframes = wave_fp.getparams()[:4]
- wave_bytes = wave_fp.readframes(nframes)
- return nchannels, sampwidth, framerate, nframes, len(wave_bytes)
-
@staticmethod
def slice_data(data: bytes, chunk_size: int) -> (list, bool):
"""
From 48f8c6c5b7b09449aa2d2ebb492d012f16877a6c Mon Sep 17 00:00:00 2001
From: goodyhao <865700600@qq.com>
Date: Wed, 30 Apr 2025 17:29:27 +0800
Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0asr,tts=E6=96=87?=
=?UTF-8?q?=E4=BB=B6=E4=B8=8A=E6=8A=A5=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../AgentChatHistoryController.java | 35 ++++++
.../xiaozhi/modules/agent/dao/AgentDao.java | 15 ++-
.../agent/dao/AiAgentChatHistoryDao.java | 16 +++
.../agent/dto/AgentChatHistoryReportDTO.java | 36 ++++++
.../agent/entity/AgentChatHistoryEntity.java | 92 +++++++++++++++
.../service/AgentChatHistoryService.java | 14 +++
.../modules/agent/service/AgentService.java | 10 +-
.../biz/AgentChatHistoryBizService.java | 22 ++++
.../impl/AgentChatHistoryBizServiceImpl.java | 83 ++++++++++++++
.../impl/AgentChatHistoryServiceImpl.java | 19 ++++
.../agent/service/impl/AgentServiceImpl.java | 10 +-
.../modules/security/config/ShiroConfig.java | 1 +
.../resources/db/changelog/202504301634.sql | 18 +++
.../mapper/agent/AiAgentChatHistoryDao.xml | 24 ++++
.../config/manage_api_client.py | 30 +++++
main/xiaozhi-server/core/connection.py | 106 +++++++++++++++++-
.../core/handle/asrReportHandle.py | 100 +++++++++++++++++
.../core/handle/receiveAudioHandle.py | 4 +
main/xiaozhi-server/core/handle/textHandle.py | 5 +
.../core/handle/ttsReportHandle.py | 70 ++++++++++++
main/xiaozhi-server/core/utils/util.py | 2 +-
21 files changed, 706 insertions(+), 6 deletions(-)
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/AgentChatHistoryBizService.java
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java
create mode 100644 main/manager-api/src/main/resources/db/changelog/202504301634.sql
create mode 100644 main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml
create mode 100644 main/xiaozhi-server/core/handle/asrReportHandle.py
create mode 100644 main/xiaozhi-server/core/handle/ttsReportHandle.py
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java
new file mode 100644
index 00000000..7abf3d13
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java
@@ -0,0 +1,35 @@
+package xiaozhi.modules.agent.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import xiaozhi.common.utils.Result;
+import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
+import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
+
+@Tag(name = "智能体聊天历史管理")
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/agent/chat-history")
+public class AgentChatHistoryController {
+ private final AgentChatHistoryBizService agentChatHistoryBizService;
+
+ /**
+ * 小智服务聊天上报请求
+ *
+ * 小智服务聊天上报请求,包含Base64编码的音频数据和相关信息。
+ *
+ * @param request 包含上传文件及相关信息的请求对象
+ */
+ @Operation(summary = "小智服务聊天上报请求")
+ @PostMapping("/report")
+ public Result uploadFile(@Valid @RequestBody AgentChatHistoryReportDTO request) {
+ Boolean result = agentChatHistoryBizService.report(request);
+ return new Result().ok(result);
+ }
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java
index 8c80d6cc..3b4be452 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentEntity;
@@ -15,4 +16,16 @@ public interface AgentDao extends BaseDao {
* @return 设备数量
*/
Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
-}
\ No newline at end of file
+
+ /**
+ * 根据设备MAC地址查询对应设备的默认智能体信息
+ *
+ * @param macAddress 设备MAC地址
+ * @return 默认智能体信息
+ */
+ @Select(" SELECT a.* FROM ai_device d " +
+ " LEFT JOIN ai_agent a ON d.agent_id = a.id " +
+ " WHERE d.mac_address = #{macAddress} " +
+ " ORDER BY d.id DESC LIMIT 1")
+ AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress);
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java
new file mode 100644
index 00000000..9c07be1d
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatHistoryDao.java
@@ -0,0 +1,16 @@
+package xiaozhi.modules.agent.dao;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
+
+/**
+ * {@link AgentChatHistoryEntity} 智能体聊天历史记录Dao对象
+ *
+ * @author Goody
+ * @version 1.0, 2025/4/30
+ * @since 1.0.0
+ */
+@Mapper
+public interface AiAgentChatHistoryDao extends BaseMapper {
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java
new file mode 100644
index 00000000..0f79dda6
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java
@@ -0,0 +1,36 @@
+package xiaozhi.modules.agent.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 小智设备聊天上报请求
+ *
+ * @author Haotian
+ * @version 1.0, 2025/5/8
+ */
+@Data
+@Schema(description = "小智设备聊天上报请求")
+public class AgentChatHistoryReportDTO {
+ @Schema(description = "MAC地址", example = "00:11:22:33:44:55")
+ @NotBlank
+ private String macAddress;
+ @Schema(description = "会话ID", example = "79578c31-f1fb-426a-900e-1e934215f05a")
+ @NotBlank
+ private String sessionId;
+ @Schema(description = "排序值(与session_id对应)", example = "1745566378")
+ @NotNull
+ private Long sort;
+ @Schema(description = "消息类型: 1-用户, 2-智能体", example = "1")
+ @NotNull
+ private Byte chatType;
+ @Schema(description = "聊天内容", example = "你好呀")
+ @NotBlank
+ private String content;
+ @Schema(description = "文件数据(Base64编码)", example = "")
+ private String fileBase64;
+ @Schema(description = "文件扩展名(如wav、mp3等)", example = "wav")
+ private String fileExtension;
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java
new file mode 100644
index 00000000..3e486f43
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java
@@ -0,0 +1,92 @@
+package xiaozhi.modules.agent.entity;
+
+import java.util.Date;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 智能体聊天记录表
+ *
+ * @author Goody
+ * @version 1.0, 2025/4/30
+ * @since 1.0.0
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@TableName(value = "ai_agent_chat_history")
+public class AgentChatHistoryEntity {
+ /**
+ * 主键ID
+ */
+ @TableId(type = IdType.AUTO)
+ private Long id;
+
+ /**
+ * MAC地址
+ */
+ @TableField(value = "mac_address")
+ private String macAddress;
+
+ /**
+ * 智能体id
+ */
+ @TableField(value = "agent_id")
+ private String agentId;
+
+ /**
+ * 会话ID
+ */
+ @TableField(value = "session_id")
+ private String sessionId;
+
+ /**
+ * 排序值(与session_id对应),使用时间戳,方便排序
+ */
+ @TableField(value = "sort")
+ private Long sort;
+
+ /**
+ * 消息类型: 1-用户, 2-智能体
+ */
+ @TableField(value = "chat_type")
+ private Byte chatType;
+
+ /**
+ * 聊天内容
+ */
+ @TableField(value = "content")
+ private String content;
+
+ /**
+ * 音频base64数据
+ */
+ @TableField(value = "audio")
+ private String audio;
+
+ /**
+ * 音频URL
+ */
+ @TableField(value = "audio_url")
+ private String audioUrl;
+
+ /**
+ * 创建时间
+ */
+ @TableField(value = "created_at")
+ private Date createdAt;
+
+ /**
+ * 更新时间
+ */
+ @TableField(value = "updated_at")
+ private Date updatedAt;
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java
new file mode 100644
index 00000000..e852cae3
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatHistoryService.java
@@ -0,0 +1,14 @@
+package xiaozhi.modules.agent.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
+
+/**
+ * 智能体聊天记录表处理service
+ *
+ * @author Goody
+ * @version 1.0, 2025/4/30
+ * @since 1.0.0
+ */
+public interface AgentChatHistoryService extends IService {
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java
index 8d0b2959..48355a4b 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java
@@ -42,4 +42,12 @@ public interface AgentService extends BaseService {
* @return 设备数量
*/
Integer getDeviceCountByAgentId(String agentId);
-}
\ No newline at end of file
+
+ /**
+ * 根据设备MAC地址查询对应设备的默认智能体信息
+ *
+ * @param macAddress 设备MAC地址
+ * @return 默认智能体信息,不存在时返回null
+ */
+ AgentEntity getDefaultAgentByMacAddress(String macAddress);
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/AgentChatHistoryBizService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/AgentChatHistoryBizService.java
new file mode 100644
index 00000000..191b231b
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/AgentChatHistoryBizService.java
@@ -0,0 +1,22 @@
+package xiaozhi.modules.agent.service.biz;
+
+import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
+
+/**
+ * 智能体聊天历史业务逻辑层
+ *
+ * @author Goody
+ * @version 1.0, 2025/4/30
+ * @since 1.0.0
+ */
+public interface AgentChatHistoryBizService {
+
+ /**
+ * 聊天上报方法
+ *
+ * @param agentChatHistoryReportDTO 包含聊天上报所需信息的输入对象
+ * 例如:设备MAC地址、文件类型、内容等
+ * @return 上传结果,true表示成功,false表示失败
+ */
+ Boolean report(AgentChatHistoryReportDTO agentChatHistoryReportDTO);
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java
new file mode 100644
index 00000000..ae99455d
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java
@@ -0,0 +1,83 @@
+package xiaozhi.modules.agent.service.biz.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
+import xiaozhi.modules.agent.entity.AgentEntity;
+import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
+import xiaozhi.modules.agent.service.AgentChatHistoryService;
+import xiaozhi.modules.agent.service.AgentService;
+import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
+
+import javax.annotation.Nullable;
+
+/**
+ * {@link AgentChatHistoryBizService} impl
+ *
+ * @author Goody
+ * @version 1.0, 2025/4/30
+ * @since 1.0.0
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizService {
+ private final AgentService agentService;
+ private final AgentChatHistoryService agentChatHistoryService;
+
+ /**
+ * 处理聊天记录上报,包括文件上传和相关信息记录
+ *
+ * @param report 包含聊天上报所需信息的输入对象
+ * @return 上传结果,true表示成功,false表示失败
+ */
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Boolean report(AgentChatHistoryReportDTO report) {
+ final String macAddress = report.getMacAddress();
+ final Byte chatType = report.getChatType();
+ log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
+
+ // 1. 上传音频文件
+ final String uploadUrl = this.upload(report);
+
+ // 2. 组装上报数据
+ // 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
+ AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
+ if (agentEntity == null) {
+ return false;
+ }
+ final String agentId = agentEntity.getId();
+ log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId());
+
+ // 2.2 构建聊天记录实体
+ final AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
+ .macAddress(macAddress)
+ .agentId(agentId)
+ .sessionId(report.getSessionId())
+ .sort(report.getSort())
+ .chatType(report.getChatType())
+ .content(report.getContent())
+ .audio(report.getFileBase64())
+ .audioUrl(uploadUrl)
+ .build();
+
+ // 3. 保存数据
+ agentChatHistoryService.save(entity);
+ return Boolean.TRUE;
+ }
+
+ /**
+ * 上传文件
+ *
+ * @param report 上报文件数据
+ * @return 上传文件url
+ */
+ @Nullable
+ private String upload(AgentChatHistoryReportDTO report) {
+ // TODO(haotian): 2025/4/30 根据需要自定义完成上传生成url即可
+ return null;
+ }
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java
new file mode 100644
index 00000000..95293a3f
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatHistoryServiceImpl.java
@@ -0,0 +1,19 @@
+package xiaozhi.modules.agent.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
+import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
+import xiaozhi.modules.agent.service.AgentChatHistoryService;
+
+/**
+ * 智能体聊天记录表处理service {@link AgentChatHistoryService} impl
+ *
+ * @author Goody
+ * @version 1.0, 2025/4/30
+ * @since 1.0.0
+ */
+@Service
+public class AgentChatHistoryServiceImpl extends ServiceImpl implements AgentChatHistoryService {
+
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java
index ff284ef0..4c529882 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java
@@ -130,4 +130,12 @@ public class AgentServiceImpl extends BaseServiceImpl imp
return deviceCount != null ? deviceCount : 0;
}
-}
\ No newline at end of file
+
+ @Override
+ public AgentEntity getDefaultAgentByMacAddress(String macAddress) {
+ if (StringUtils.isEmpty(macAddress)) {
+ return null;
+ }
+ return agentDao.getDefaultAgentByMacAddress(macAddress);
+ }
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java
index 62e192b7..b462b2c4 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java
@@ -84,6 +84,7 @@ public class ShiroConfig {
filterMap.put("/user/register", "anon");
// 将config路径使用server服务过滤器
filterMap.put("/config/**", "server");
+ filterMap.put("/agent/chat-history/report", "server");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
diff --git a/main/manager-api/src/main/resources/db/changelog/202504301634.sql b/main/manager-api/src/main/resources/db/changelog/202504301634.sql
new file mode 100644
index 00000000..6a86a417
--- /dev/null
+++ b/main/manager-api/src/main/resources/db/changelog/202504301634.sql
@@ -0,0 +1,18 @@
+-- 初始化智能体聊天记录
+DROP TABLE IF EXISTS ai_agent_chat_history;
+CREATE TABLE ai_agent_chat_history
+(
+ id BIGINT AUTO_INCREMENT COMMENT '主键ID'
+ PRIMARY KEY,
+ mac_address VARCHAR(50) COMMENT 'MAC地址',
+ agent_id BIGINT DEFAULT 0 COMMENT '智能体id',
+ session_id VARCHAR(50) COMMENT '会话ID',
+ sort BIGINT COMMENT '排序值(与session_id对应),使用时间戳,方便排序',
+ chat_type TINYINT(3) COMMENT '消息类型: 1-用户, 2-智能体',
+ content VARCHAR(1024) COMMENT '聊天内容',
+ audio text COMMENT '音频base64数据',
+ audio_url VARCHAR(256) COMMENT '音频URL',
+ created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL COMMENT '创建时间',
+ updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
+ INDEX idx_mac_session (mac_address, sort)
+) COMMENT '智能体聊天记录表';
diff --git a/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml b/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml
new file mode 100644
index 00000000..2d87d8cf
--- /dev/null
+++ b/main/manager-api/src/main/resources/mapper/agent/AiAgentChatHistoryDao.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id, mac_address, agent_id, session_id, sort, chat_type, content, audio, audio_url,
+ created_at, updated_at
+
+
diff --git a/main/xiaozhi-server/config/manage_api_client.py b/main/xiaozhi-server/config/manage_api_client.py
index 770cbf56..ec8fd30a 100644
--- a/main/xiaozhi-server/config/manage_api_client.py
+++ b/main/xiaozhi-server/config/manage_api_client.py
@@ -1,3 +1,4 @@
+import base64
import os
import time
from typing import Optional, Dict
@@ -145,6 +146,35 @@ def get_agent_models(
},
)
+async def report(mac_address: str,
+ session_id: str,
+ sort: int,
+ chat_type: int,
+ content: str,
+ audio,
+ file_extension: str = "wav",
+ need_report: bool = None,
+ report_type: int = None,
+ reported: bool = None) -> Optional[Dict]:
+ """带熔断的业务方法示例"""
+ if not content or not ManageApiClient._instance:
+ return None
+ return await ManageApiClient._instance._execute_request(
+ "POST",
+ f"/agent/chat-history/report",
+ json = {
+ "macAddress": mac_address,
+ "sessionId": session_id,
+ "sort": sort,
+ "chatType": chat_type,
+ "content": content,
+ "fileBase64": base64.b64encode(audio).decode('utf-8'),
+ "fileExtension": file_extension,
+ "needReport": need_report,
+ "reportType": report_type,
+ "reported": reported
+ }
+ )
def init_service(config):
ManageApiClient(config)
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index fbd3569e..1d1caf04 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -30,6 +30,7 @@ from core.mcp.manager import MCPManager
from config.config_loader import get_private_config_from_api
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
from core.utils.output_counter import add_device_output
+from core.handle.ttsReportHandle import enqueue_tts_report
TAG = __name__
@@ -54,6 +55,7 @@ class ConnectionHandler:
self.websocket = None
self.headers = None
+ self.device_id = None
self.client_ip = None
self.client_ip_info = {}
self.session_id = None
@@ -72,6 +74,13 @@ class ConnectionHandler:
self.audio_play_queue = queue.Queue()
self.executor = ThreadPoolExecutor(max_workers=10)
+ # 上报线程标志
+ self.session_open_time = time.time()
+ self.tts_report_queue = queue.Queue()
+ self.asr_report_queue = queue.Queue()
+ self.asr_report_thread = None
+ self.tts_report_thread = None
+
# 依赖的组件
self.vad = _vad
self.asr = _asr
@@ -153,6 +162,7 @@ class ConnectionHandler:
# 认证通过,继续处理
self.websocket = ws
+ self.device_id = self.headers.get("device-id", None)
self.session_id = str(uuid.uuid4())
# 启动超时检查任务
@@ -301,6 +311,26 @@ class ConnectionHandler:
self._initialize_memory()
"""加载意图识别"""
self._initialize_intent()
+ """初始化上报线程"""
+ self._init_report_threads()
+
+ def _init_report_threads(self):
+ """初始化ASR和TTS上报线程"""
+ if self.asr_report_thread is None or not self.asr_report_thread.is_alive():
+ self.asr_report_thread = threading.Thread(
+ target=self._asr_report_worker,
+ daemon=True
+ )
+ self.asr_report_thread.start()
+ self.logger.bind(tag=TAG).info("ASR上报线程已启动")
+
+ if self.tts_report_thread is None or not self.tts_report_thread.is_alive():
+ self.tts_report_thread = threading.Thread(
+ target=self._tts_report_worker,
+ daemon=True
+ )
+ self.tts_report_thread.start()
+ self.logger.bind(tag=TAG).info("TTS上报线程已启动")
def _initialize_private_config(self):
read_config_from_api = self.config.get("read_config_from_api", False)
@@ -427,8 +457,7 @@ class ConnectionHandler:
def _initialize_memory(self):
"""初始化记忆模块"""
- device_id = self.headers.get("device-id", None)
- self.memory.init_memory(device_id, self.llm)
+ self.memory.init_memory(self.device_id, self.llm)
def _initialize_intent(self):
if (
@@ -862,6 +891,9 @@ class ConnectionHandler:
f"TTS生成:文件路径: {tts_file}"
)
if os.path.exists(tts_file):
+ # 在这里上报TTS数据(使用文件路径)
+ enqueue_tts_report(self, text, tts_file)
+
opus_datas, duration = self.tts.audio_to_opus_data(tts_file)
else:
self.logger.bind(tag=TAG).error(
@@ -918,6 +950,72 @@ class ConnectionHandler:
f"audio_play_priority priority_thread: {text} {e}"
)
+ def _asr_report_worker(self):
+ """ASR上报工作线程"""
+ # 提前导入避免循环引用问题
+ from core.handle.asrReportHandle import report_asr
+
+ while not self.stop_event.is_set():
+ try:
+ # 从队列获取数据,设置超时以便定期检查停止事件
+ item = self.asr_report_queue.get(timeout=1)
+ if item is None: # 检测毒丸对象
+ break
+
+ text, file_path = item
+
+ try:
+ # 执行上报(传入文件路径)
+ await_result = report_asr(self, text, file_path)
+
+ # 使用asyncio.run_coroutine_threadsafe执行异步操作
+ future = asyncio.run_coroutine_threadsafe(await_result, self.loop)
+ future.result()
+ except Exception as e:
+ self.logger.bind(tag=TAG).error(f"ASR上报线程异常: {e}")
+ finally:
+ # 标记任务完成
+ self.asr_report_queue.task_done()
+ except queue.Empty:
+ continue
+ except Exception as e:
+ self.logger.bind(tag=TAG).error(f"ASR上报工作线程异常: {e}")
+
+ self.logger.bind(tag=TAG).info("ASR上报线程已退出")
+
+ def _tts_report_worker(self):
+ """TTS上报工作线程"""
+ # 提前导入避免循环引用问题
+ from core.handle.ttsReportHandle import report_tts
+
+ while not self.stop_event.is_set():
+ try:
+ # 从队列获取数据,设置超时以便定期检查停止事件
+ item = self.tts_report_queue.get(timeout=1)
+ if item is None: # 检测毒丸对象
+ break
+
+ text, audio_data = item
+
+ try:
+ # 执行上报(传入二进制数据)
+ await_result = report_tts(self, text, audio_data)
+
+ # 使用asyncio.run_coroutine_threadsafe执行异步操作
+ future = asyncio.run_coroutine_threadsafe(await_result, self.loop)
+ future.result()
+ except Exception as e:
+ self.logger.bind(tag=TAG).error(f"TTS上报线程异常: {e}")
+ finally:
+ # 标记任务完成
+ self.tts_report_queue.task_done()
+ except queue.Empty:
+ continue
+ except Exception as e:
+ self.logger.bind(tag=TAG).error(f"TTS上报工作线程异常: {e}")
+
+ self.logger.bind(tag=TAG).info("TTS上报线程已退出")
+
def speak_and_play(self, text, text_index=0):
if text is None or len(text) <= 0:
self.logger.bind(tag=TAG).info(f"无需tts转换,query为空,{text}")
@@ -963,6 +1061,10 @@ class ConnectionHandler:
self.executor.shutdown(wait=False, cancel_futures=True)
self.executor = None
+ # 添加毒丸对象到上报队列确保线程退出
+ self.asr_report_queue.put(None)
+ self.tts_report_queue.put(None)
+
# 清空任务队列
self.clear_queues()
diff --git a/main/xiaozhi-server/core/handle/asrReportHandle.py b/main/xiaozhi-server/core/handle/asrReportHandle.py
new file mode 100644
index 00000000..70601804
--- /dev/null
+++ b/main/xiaozhi-server/core/handle/asrReportHandle.py
@@ -0,0 +1,100 @@
+"""
+ASR上报功能已集成到ConnectionHandler类中。
+
+上报功能包括:
+1. 每个连接对象拥有自己的上报队列和处理线程
+2. 上报线程的生命周期与连接对象绑定
+3. 使用ConnectionHandler.enqueue_asr_report方法进行上报
+
+具体实现请参考core/connection.py中的相关代码。
+"""
+
+import os
+from config.logger import setup_logging
+from config.manage_api_client import report
+
+TAG = __name__
+logger = setup_logging()
+
+async def report_asr(conn, text, file_path):
+ """执行ASR上报操作
+
+ Args:
+ conn: 连接对象
+ text: 识别文本
+ file_path: 音频文件路径(可以为None或空字符串,表示纯文本上报)
+ """
+ audio_data = None
+ try:
+ # 处理无音频的纯文本上报
+ if not file_path or not os.path.exists(file_path):
+ # 纯文本上报时使用空音频数据
+ result = await report(
+ mac_address=conn.device_id,
+ session_id=conn.session_id,
+ sort=int(conn.session_open_time),
+ chat_type=1, # ASR类型为1
+ content=text,
+ audio=b'', # 空音频数据
+ file_extension="wav"
+ )
+ logger.bind(tag=TAG).info(f"纯文本上报成功: {conn.device_id}, {conn.session_id}")
+ else:
+ # 读取文件为二进制数据
+ with open(file_path, 'rb') as f:
+ audio_data = f.read()
+
+ # 正常ASR上报(带音频)
+ result = await report(
+ mac_address=conn.device_id,
+ session_id=conn.session_id,
+ sort=int(conn.session_open_time),
+ chat_type=1, # ASR类型为1
+ content=text,
+ audio=audio_data,
+ file_extension="wav"
+ )
+ logger.bind(tag=TAG).info(f"ASR上报成功: {conn.device_id}, {conn.session_id},文件: {file_path}")
+
+ return result
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"ASR上报失败: {e}")
+ return None
+ finally:
+ # 清理资源
+ if file_path and os.path.exists(file_path):
+ try:
+ os.remove(file_path)
+ logger.bind(tag=TAG).debug(f"ASR上报后删除文件: {file_path}")
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"ASR上报后删除文件失败: {e}")
+
+ # 手动清理audio_data
+ if audio_data:
+ del audio_data
+
+def enqueue_asr_report(conn, text, audio):
+ """将ASR数据加入上报队列
+
+ Args:
+ conn: 连接对象
+ text: 识别文本
+ audio: 音频数据(可以为空列表,表示纯文本上报)
+ """
+ try:
+ if not audio or len(audio) == 0:
+ # 纯文本上报,不需要保存文件
+ file_path = None
+ else:
+ # 保存音频数据到文件
+ file_path = conn.asr.save_audio_to_file(audio, conn.session_id)
+
+ # 使用连接对象的队列,传入文件路径
+ conn.asr_report_queue.put((text, file_path))
+
+ if not audio or len(audio) == 0:
+ logger.bind(tag=TAG).info(f"纯文本数据已加入上报队列: {conn.device_id}, {text[:20] if text else ''}...")
+ else:
+ logger.bind(tag=TAG).info(f"ASR数据已加入上报队列: {conn.device_id}, 文件: {file_path}")
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"加入ASR上报队列失败: {e}")
diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py
index 485544f7..2d8b0eef 100644
--- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py
+++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py
@@ -4,6 +4,7 @@ from core.utils.util import remove_punctuation_and_length
from core.handle.sendAudioHandle import send_stt_message
from core.handle.intentHandler import handle_user_intent
from core.utils.output_counter import check_device_output_limit
+from core.handle.asrReportHandle import enqueue_asr_report
TAG = __name__
logger = setup_logging()
@@ -40,6 +41,9 @@ async def handleAudioMessage(conn, audio):
logger.bind(tag=TAG).info(f"识别文本: {text}")
text_len, _ = remove_punctuation_and_length(text)
if text_len > 0:
+ # 使用自定义模块进行上报
+ enqueue_asr_report(conn, text, conn.asr_audio)
+
await startToChat(conn, text)
else:
conn.asr_server_receive = True
diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py
index a26f5bd9..fa81eb76 100644
--- a/main/xiaozhi-server/core/handle/textHandle.py
+++ b/main/xiaozhi-server/core/handle/textHandle.py
@@ -6,6 +6,7 @@ from core.utils.util import remove_punctuation_and_length
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
+from core.handle.asrReportHandle import enqueue_asr_report
import asyncio
TAG = __name__
@@ -54,8 +55,12 @@ async def handleTextMessage(conn, message):
await send_stt_message(conn, text)
await send_tts_message(conn, "stop", None)
elif is_wakeup_words:
+ # 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
+ enqueue_asr_report(conn, "嘿,你好呀", [])
await startToChat(conn, "嘿,你好呀")
else:
+ # 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
+ enqueue_asr_report(conn, text, [])
# 否则需要LLM对文字内容进行答复
await startToChat(conn, text)
elif msg_json["type"] == "iot":
diff --git a/main/xiaozhi-server/core/handle/ttsReportHandle.py b/main/xiaozhi-server/core/handle/ttsReportHandle.py
new file mode 100644
index 00000000..ed3a6a76
--- /dev/null
+++ b/main/xiaozhi-server/core/handle/ttsReportHandle.py
@@ -0,0 +1,70 @@
+"""
+TTS上报功能已集成到ConnectionHandler类中。
+
+上报功能包括:
+1. 每个连接对象拥有自己的上报队列和处理线程
+2. 上报线程的生命周期与连接对象绑定
+3. 使用ConnectionHandler.enqueue_tts_report方法进行上报
+
+具体实现请参考core/connection.py中的相关代码。
+"""
+
+import os
+from config.logger import setup_logging
+from config.manage_api_client import report
+
+TAG = __name__
+logger = setup_logging()
+
+async def report_tts(conn, text, audio_data):
+ """执行TTS上报操作
+
+ Args:
+ conn: 连接对象
+ text: 合成文本
+ audio_data: 音频二进制数据
+ """
+ try:
+ # 执行上报
+ result = await report(
+ mac_address=conn.device_id,
+ session_id=conn.session_id,
+ sort=int(conn.session_open_time),
+ chat_type=2, # TTS类型为2
+ content=text,
+ audio=audio_data,
+ file_extension="wav"
+ )
+ logger.bind(tag=TAG).info(f"TTS上报成功: {conn.device_id}, {conn.session_id}, 数据大小: {len(audio_data)} 字节")
+ return result
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"TTS上报失败: {e}")
+ return None
+ finally:
+ # 手动清理audio_data引用,帮助垃圾回收
+ del audio_data
+
+def enqueue_tts_report(conn, text, file_path):
+ """将TTS数据加入上报队列
+
+ Args:
+ conn: 连接对象
+ text: 合成文本
+ file_path: TTS音频文件路径
+ """
+ try:
+ # 检查文件是否存在
+ if not file_path or not os.path.exists(file_path):
+ logger.bind(tag=TAG).error(f"加入TTS上报队列失败: 文件不存在 {file_path}")
+ return
+
+ # 立即读取文件为二进制数据,因为外部会删除文件
+ with open(file_path, 'rb') as f:
+ audio_data = f.read()
+
+ # 使用连接对象的队列,传入文本和二进制数据而非文件路径
+ conn.tts_report_queue.put((text, audio_data))
+
+ logger.bind(tag=TAG).info(f"TTS数据已加入上报队列: {conn.device_id}, 文件大小: {len(audio_data)} 字节")
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {e}, 文件: {file_path}")
diff --git a/main/xiaozhi-server/core/utils/util.py b/main/xiaozhi-server/core/utils/util.py
index 1cc9742b..a1956cee 100644
--- a/main/xiaozhi-server/core/utils/util.py
+++ b/main/xiaozhi-server/core/utils/util.py
@@ -484,4 +484,4 @@ def analyze_emotion(text):
if emotion in top_emotions:
return emotion
- return top_emotions[0] # 如果都不在优先级列表里,返回第一个
\ No newline at end of file
+ return top_emotions[0] # 如果都不在优先级列表里,返回第一个
From 54692859fde8782962d84b87a25a1a91aad3a8a6 Mon Sep 17 00:00:00 2001
From: MakerZorky <152245790+MakerZorky@users.noreply.github.com>
Date: Wed, 30 Apr 2025 19:23:44 +0800
Subject: [PATCH 3/4] =?UTF-8?q?Update:config.yaml=E7=83=AD=E8=AF=8D?=
=?UTF-8?q?=E6=B3=A8=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
main/xiaozhi-server/config.yaml | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml
index 7565179c..6948aebd 100644
--- a/main/xiaozhi-server/config.yaml
+++ b/main/xiaozhi-server/config.yaml
@@ -232,8 +232,9 @@ ASR:
appid: 你的火山引擎语音合成服务appid
access_token: 你的火山引擎语音合成服务access_token
cluster: volcengine_input_common
- boosting_table_name: 你的热词文件名称
- correct_table_name: 你的替换词文件名称
+ # 热词、替换词使用流程:https://www.volcengine.com/docs/6561/155738
+ boosting_table_name: (选填)你的热词文件名称
+ correct_table_name: (选填)你的替换词文件名称
output_dir: tmp/
TencentASR:
# token申请地址:https://console.cloud.tencent.com/cam/capi
From f0e353cc1a9b0a6394942ebc77add67ae74f96ef Mon Sep 17 00:00:00 2001
From: hrz <1710360675@qq.com>
Date: Fri, 2 May 2025 00:07:36 +0800
Subject: [PATCH 4/4] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E5=AF=B9=E8=AF=9D?=
=?UTF-8?q?=E6=95=B0=E6=8D=AE=E4=B8=8A=E4=BC=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 1 +
.../common/service/impl/BaseServiceImpl.java | 2 -
.../common/service/impl/CrudServiceImpl.java | 1 -
.../AgentChatHistoryController.java | 9 +-
.../agent/dao/AiAgentChatAudioDao.java | 18 +++
.../agent/dto/AgentChatHistoryReportDTO.java | 11 +-
.../agent/entity/AgentChatAudioEntity.java | 29 +++++
.../agent/entity/AgentChatHistoryEntity.java | 51 ++++----
.../agent/service/AgentChatAudioService.java | 22 ++++
.../impl/AgentChatHistoryBizServiceImpl.java | 50 ++++----
.../impl/AgentChatAudioServiceImpl.java | 28 +++++
.../config/controller/ConfigController.java | 4 +-
.../service/impl/ConfigServiceImpl.java | 1 -
.../resources/db/changelog/202504301634.sql | 18 ---
.../resources/db/changelog/202505012207.sql | 24 ++++
.../db/changelog/db.changelog-master.yaml | 9 +-
.../config/manage_api_client.py | 66 ++++++-----
main/xiaozhi-server/core/connection.py | 112 ++++++------------
.../core/handle/asrReportHandle.py | 100 ----------------
.../core/handle/receiveAudioHandle.py | 5 +-
main/xiaozhi-server/core/handle/textHandle.py | 23 ++--
.../core/handle/ttsReportHandle.py | 51 ++++----
22 files changed, 295 insertions(+), 340 deletions(-)
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatAudioDao.java
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatAudioEntity.java
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java
create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java
delete mode 100644 main/manager-api/src/main/resources/db/changelog/202504301634.sql
create mode 100644 main/manager-api/src/main/resources/db/changelog/202505012207.sql
delete mode 100644 main/xiaozhi-server/core/handle/asrReportHandle.py
diff --git a/.gitignore b/.gitignore
index e1ee3d0c..f4efaeaa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -165,3 +165,4 @@ main/manager-api/.vscode
main/manager-web/.webpack_cache/
main/xiaozhi-server/mysql
uploadfile
+.vscode
diff --git a/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java
index 8ebb4cd1..9d2ba5d5 100644
--- a/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java
+++ b/main/manager-api/src/main/java/xiaozhi/common/service/impl/BaseServiceImpl.java
@@ -128,12 +128,10 @@ public abstract class BaseServiceImpl, T> implements Bas
return SqlHelper.retBool(result);
}
- @SuppressWarnings("unchecked")
protected Class currentMapperClass() {
return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 0);
}
- @SuppressWarnings("unchecked")
@Override
public Class currentModelClass() {
return (Class) ReflectionKit.getSuperClassGenericType(this.getClass(), BaseServiceImpl.class, 1);
diff --git a/main/manager-api/src/main/java/xiaozhi/common/service/impl/CrudServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/common/service/impl/CrudServiceImpl.java
index 524a8cb2..9a1bf39e 100644
--- a/main/manager-api/src/main/java/xiaozhi/common/service/impl/CrudServiceImpl.java
+++ b/main/manager-api/src/main/java/xiaozhi/common/service/impl/CrudServiceImpl.java
@@ -24,7 +24,6 @@ import xiaozhi.common.utils.ConvertUtils;
public abstract class CrudServiceImpl, T, D> extends BaseServiceImpl
implements CrudService {
- @SuppressWarnings("unchecked")
protected Class currentDtoClass() {
return (Class) ReflectionKit.getSuperClassGenericType(getClass(), CrudServiceImpl.class, 2);
}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java
index 7abf3d13..9d6542cc 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentChatHistoryController.java
@@ -1,13 +1,14 @@
package xiaozhi.modules.agent.controller;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatAudioDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatAudioDao.java
new file mode 100644
index 00000000..355b25ac
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AiAgentChatAudioDao.java
@@ -0,0 +1,18 @@
+package xiaozhi.modules.agent.dao;
+
+import org.apache.ibatis.annotations.Mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
+
+/**
+ * {@link AgentChatAudioEntity} 智能体聊天音频数据Dao对象
+ *
+ * @author Goody
+ * @version 1.0, 2025/5/8
+ * @since 1.0.0
+ */
+@Mapper
+public interface AiAgentChatAudioDao extends BaseMapper {
+}
\ No newline at end of file
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java
index 0f79dda6..8227260d 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentChatHistoryReportDTO.java
@@ -20,17 +20,12 @@ public class AgentChatHistoryReportDTO {
@Schema(description = "会话ID", example = "79578c31-f1fb-426a-900e-1e934215f05a")
@NotBlank
private String sessionId;
- @Schema(description = "排序值(与session_id对应)", example = "1745566378")
- @NotNull
- private Long sort;
@Schema(description = "消息类型: 1-用户, 2-智能体", example = "1")
@NotNull
private Byte chatType;
@Schema(description = "聊天内容", example = "你好呀")
@NotBlank
private String content;
- @Schema(description = "文件数据(Base64编码)", example = "")
- private String fileBase64;
- @Schema(description = "文件扩展名(如wav、mp3等)", example = "wav")
- private String fileExtension;
-}
+ @Schema(description = "文件数据(opus编码)", example = "")
+ private String opusDataBase64;
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatAudioEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatAudioEntity.java
new file mode 100644
index 00000000..ac30d9d4
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatAudioEntity.java
@@ -0,0 +1,29 @@
+package xiaozhi.modules.agent.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import lombok.Data;
+
+/**
+ * 智能体聊天音频数据表
+ *
+ * @author Goody
+ * @version 1.0, 2025/5/8
+ * @since 1.0.0
+ */
+@Data
+@TableName("ai_agent_chat_audio")
+public class AgentChatAudioEntity {
+ /**
+ * 主键ID
+ */
+ @TableId(type = IdType.ASSIGN_UUID)
+ private String id;
+
+ /**
+ * 音频opus数据
+ */
+ private byte[] audio;
+}
\ No newline at end of file
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java
index 3e486f43..96e82885 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentChatHistoryEntity.java
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
+
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -25,68 +26,62 @@ import lombok.NoArgsConstructor;
@TableName(value = "ai_agent_chat_history")
public class AgentChatHistoryEntity {
/**
- * 主键ID
- */
+ * 主键ID
+ */
@TableId(type = IdType.AUTO)
private Long id;
/**
- * MAC地址
- */
+ * MAC地址
+ */
@TableField(value = "mac_address")
private String macAddress;
/**
- * 智能体id
- */
+ * 智能体id
+ */
@TableField(value = "agent_id")
private String agentId;
/**
- * 会话ID
- */
+ * 会话ID
+ */
@TableField(value = "session_id")
private String sessionId;
/**
- * 排序值(与session_id对应),使用时间戳,方便排序
- */
- @TableField(value = "sort")
- private Long sort;
-
- /**
- * 消息类型: 1-用户, 2-智能体
- */
+ * 消息类型: 1-用户, 2-智能体
+ */
@TableField(value = "chat_type")
private Byte chatType;
/**
- * 聊天内容
- */
+ * 聊天内容
+ */
@TableField(value = "content")
private String content;
/**
- * 音频base64数据
- */
- @TableField(value = "audio")
- private String audio;
+ * 音频base64数据
+ */
+ @TableField(value = "audio_id")
+ private String audioId;
/**
- * 音频URL
- */
+ * 音频URL
+ */
@TableField(value = "audio_url")
private String audioUrl;
/**
- * 创建时间
- */
+ * 创建时间
+ */
@TableField(value = "created_at")
private Date createdAt;
/**
- * 更新时间
- */
+ * 更新时间
+ */
@TableField(value = "updated_at")
private Date updatedAt;
}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java
new file mode 100644
index 00000000..fdd0e1f6
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentChatAudioService.java
@@ -0,0 +1,22 @@
+package xiaozhi.modules.agent.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+
+import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
+
+/**
+ * 智能体聊天音频数据表处理service
+ *
+ * @author Goody
+ * @version 1.0, 2025/5/8
+ * @since 1.0.0
+ */
+public interface AgentChatAudioService extends IService {
+ /**
+ * 保存音频数据
+ *
+ * @param audioData 音频数据
+ * @return 音频ID
+ */
+ String saveAudio(byte[] audioData);
+}
\ No newline at end of file
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java
index ae99455d..edd96256 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/biz/impl/AgentChatHistoryBizServiceImpl.java
@@ -1,18 +1,18 @@
package xiaozhi.modules.agent.service.biz.impl;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
-import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
+import xiaozhi.modules.agent.entity.AgentEntity;
+import xiaozhi.modules.agent.service.AgentChatAudioService;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
-import javax.annotation.Nullable;
-
/**
* {@link AgentChatHistoryBizService} impl
*
@@ -26,6 +26,7 @@ import javax.annotation.Nullable;
public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizService {
private final AgentService agentService;
private final AgentChatHistoryService agentChatHistoryService;
+ private final AgentChatAudioService agentChatAudioService;
/**
* 处理聊天记录上报,包括文件上传和相关信息记录
@@ -36,12 +37,23 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean report(AgentChatHistoryReportDTO report) {
- final String macAddress = report.getMacAddress();
- final Byte chatType = report.getChatType();
+ String macAddress = report.getMacAddress();
+ Byte chatType = report.getChatType();
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
- // 1. 上传音频文件
- final String uploadUrl = this.upload(report);
+ // 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
+ String audioId = null;
+ if (report.getOpusDataBase64() != null && !report.getOpusDataBase64().isEmpty()) {
+ try {
+ // TODO: 需要考虑保留什么格式的音频数据,比如是opus还是wave
+ // byte[] audioData = Base64.getDecoder().decode(report.getOpusDataBase64());
+ // audioId = agentChatAudioService.saveAudio(audioData);
+ // log.info("音频数据保存成功,audioId={}", audioId);
+ } catch (Exception e) {
+ log.error("音频数据保存失败", e);
+ return false;
+ }
+ }
// 2. 组装上报数据
// 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
@@ -49,35 +61,21 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
if (agentEntity == null) {
return false;
}
- final String agentId = agentEntity.getId();
+ String agentId = agentEntity.getId();
log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId());
// 2.2 构建聊天记录实体
- final AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
+ AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
.macAddress(macAddress)
.agentId(agentId)
.sessionId(report.getSessionId())
- .sort(report.getSort())
.chatType(report.getChatType())
.content(report.getContent())
- .audio(report.getFileBase64())
- .audioUrl(uploadUrl)
+ .audioId(audioId)
.build();
// 3. 保存数据
agentChatHistoryService.save(entity);
return Boolean.TRUE;
}
-
- /**
- * 上传文件
- *
- * @param report 上报文件数据
- * @return 上传文件url
- */
- @Nullable
- private String upload(AgentChatHistoryReportDTO report) {
- // TODO(haotian): 2025/4/30 根据需要自定义完成上传生成url即可
- return null;
- }
}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java
new file mode 100644
index 00000000..c222abe4
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatAudioServiceImpl.java
@@ -0,0 +1,28 @@
+package xiaozhi.modules.agent.service.impl;
+
+import org.springframework.stereotype.Service;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+
+import xiaozhi.modules.agent.dao.AiAgentChatAudioDao;
+import xiaozhi.modules.agent.entity.AgentChatAudioEntity;
+import xiaozhi.modules.agent.service.AgentChatAudioService;
+
+/**
+ * 智能体聊天音频数据表处理service {@link AgentChatAudioService} impl
+ *
+ * @author Goody
+ * @version 1.0, 2025/5/8
+ * @since 1.0.0
+ */
+@Service
+public class AgentChatAudioServiceImpl extends ServiceImpl
+ implements AgentChatAudioService {
+ @Override
+ public String saveAudio(byte[] audioData) {
+ AgentChatAudioEntity entity = new AgentChatAudioEntity();
+ entity.setAudio(audioData);
+ save(entity);
+ return entity.getId();
+ }
+}
\ No newline at end of file
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java
index 13106869..f83efb3a 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java
@@ -1,6 +1,5 @@
package xiaozhi.modules.config.controller;
-import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -8,12 +7,12 @@ import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.config.dto.AgentModelsDTO;
import xiaozhi.modules.config.service.ConfigService;
-import xiaozhi.modules.sys.service.SysParamsService;
/**
* xiaozhi-server 配置获取
@@ -26,7 +25,6 @@ import xiaozhi.modules.sys.service.SysParamsService;
@AllArgsConstructor
public class ConfigController {
private final ConfigService configService;
- private final SysParamsService sysParamsService;
@PostMapping("server-base")
@Operation(summary = "获取配置")
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java
index e4914029..f0ed4bf1 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java
@@ -153,7 +153,6 @@ public class ConfigServiceImpl implements ConfigService {
* @param paramsList 系统参数列表
* @return 配置信息
*/
- @SuppressWarnings("unchecked")
private Object buildConfig(Map config) {
// 查询所有系统参数
diff --git a/main/manager-api/src/main/resources/db/changelog/202504301634.sql b/main/manager-api/src/main/resources/db/changelog/202504301634.sql
deleted file mode 100644
index 6a86a417..00000000
--- a/main/manager-api/src/main/resources/db/changelog/202504301634.sql
+++ /dev/null
@@ -1,18 +0,0 @@
--- 初始化智能体聊天记录
-DROP TABLE IF EXISTS ai_agent_chat_history;
-CREATE TABLE ai_agent_chat_history
-(
- id BIGINT AUTO_INCREMENT COMMENT '主键ID'
- PRIMARY KEY,
- mac_address VARCHAR(50) COMMENT 'MAC地址',
- agent_id BIGINT DEFAULT 0 COMMENT '智能体id',
- session_id VARCHAR(50) COMMENT '会话ID',
- sort BIGINT COMMENT '排序值(与session_id对应),使用时间戳,方便排序',
- chat_type TINYINT(3) COMMENT '消息类型: 1-用户, 2-智能体',
- content VARCHAR(1024) COMMENT '聊天内容',
- audio text COMMENT '音频base64数据',
- audio_url VARCHAR(256) COMMENT '音频URL',
- created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL COMMENT '创建时间',
- updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
- INDEX idx_mac_session (mac_address, sort)
-) COMMENT '智能体聊天记录表';
diff --git a/main/manager-api/src/main/resources/db/changelog/202505012207.sql b/main/manager-api/src/main/resources/db/changelog/202505012207.sql
new file mode 100644
index 00000000..5003edc7
--- /dev/null
+++ b/main/manager-api/src/main/resources/db/changelog/202505012207.sql
@@ -0,0 +1,24 @@
+-- 初始化智能体聊天记录
+DROP TABLE IF EXISTS ai_chat_history;
+DROP TABLE IF EXISTS ai_agent_chat_history;
+CREATE TABLE ai_agent_chat_history
+(
+ id BIGINT AUTO_INCREMENT COMMENT '主键ID' PRIMARY KEY,
+ mac_address VARCHAR(50) COMMENT 'MAC地址',
+ agent_id VARCHAR(32) COMMENT '智能体id',
+ session_id VARCHAR(50) COMMENT '会话ID',
+ chat_type TINYINT(3) COMMENT '消息类型: 1-用户, 2-智能体',
+ content VARCHAR(1024) COMMENT '聊天内容',
+ audio_id VARCHAR(32) COMMENT '音频ID',
+ created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL COMMENT '创建时间',
+ updated_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) NOT NULL ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
+ INDEX idx_ai_agent_chat_history_mac (mac_address),
+ INDEX idx_ai_agent_chat_history_agent_id (agent_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '智能体聊天记录表';
+
+DROP TABLE IF EXISTS ai_agent_chat_audio;
+CREATE TABLE ai_agent_chat_audio
+(
+ id VARCHAR(32) COMMENT '主键ID' PRIMARY KEY,
+ audio LONGBLOB COMMENT '音频opus数据'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '智能体聊天音频数据表';
\ No newline at end of file
diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
index ffe96a9b..aaef45bc 100755
--- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
+++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -92,4 +92,11 @@ databaseChangeLog:
changes:
- sqlFile:
encoding: utf8
- path: classpath:db/changelog/202504301341.sql
\ No newline at end of file
+ path: classpath:db/changelog/202504301341.sql
+ - changeSet:
+ id: 202505012207
+ author: Goody
+ changes:
+ - sqlFile:
+ encoding: utf8
+ path: classpath:db/changelog/202505012207.sql
\ No newline at end of file
diff --git a/main/xiaozhi-server/config/manage_api_client.py b/main/xiaozhi-server/config/manage_api_client.py
index ec8fd30a..7de4c8fe 100644
--- a/main/xiaozhi-server/config/manage_api_client.py
+++ b/main/xiaozhi-server/config/manage_api_client.py
@@ -1,6 +1,6 @@
-import base64
import os
import time
+import base64
from typing import Optional, Dict
import httpx
@@ -54,7 +54,7 @@ class ManageApiClient:
headers={
"User-Agent": f"PythonClient/2.0 (PID:{os.getpid()})",
"Accept": "application/json",
- "Authorization": "Bearer " + cls._secret
+ "Authorization": "Bearer " + cls._secret,
},
timeout=cls.config.get("timeout", 30), # 默认超时时间30秒
)
@@ -127,9 +127,7 @@ class ManageApiClient:
def get_server_config() -> Optional[Dict]:
"""获取服务器基础配置"""
- return ManageApiClient._instance._execute_request(
- "POST", "/config/server-base"
- )
+ return ManageApiClient._instance._execute_request("POST", "/config/server-base")
def get_agent_models(
@@ -146,35 +144,41 @@ def get_agent_models(
},
)
-async def report(mac_address: str,
- session_id: str,
- sort: int,
- chat_type: int,
- content: str,
- audio,
- file_extension: str = "wav",
- need_report: bool = None,
- report_type: int = None,
- reported: bool = None) -> Optional[Dict]:
+
+def report(
+ mac_address: str, session_id: str, chat_type: int, content: str, opus_data
+) -> Optional[Dict]:
"""带熔断的业务方法示例"""
if not content or not ManageApiClient._instance:
return None
- return await ManageApiClient._instance._execute_request(
- "POST",
- f"/agent/chat-history/report",
- json = {
- "macAddress": mac_address,
- "sessionId": session_id,
- "sort": sort,
- "chatType": chat_type,
- "content": content,
- "fileBase64": base64.b64encode(audio).decode('utf-8'),
- "fileExtension": file_extension,
- "needReport": need_report,
- "reportType": report_type,
- "reported": reported
- }
- )
+ try:
+ # 处理opus_data为列表的情况
+ if isinstance(opus_data, list):
+ # 将列表中的所有bytes数据合并
+ combined_data = b"".join(opus_data)
+ else:
+ combined_data = opus_data
+
+ # 将二进制数据转换为Base64编码的字符串
+ opus_data_base64 = (
+ base64.b64encode(combined_data).decode("utf-8") if combined_data else None
+ )
+
+ return ManageApiClient._instance._execute_request(
+ "POST",
+ f"/agent/chat-history/report",
+ json={
+ "macAddress": mac_address,
+ "sessionId": session_id,
+ "chatType": chat_type,
+ "content": content,
+ "opusDataBase64": opus_data_base64,
+ },
+ )
+ except Exception as e:
+ print(f"TTS上报失败: {e}")
+ return None
+
def init_service(config):
ManageApiClient(config)
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index 87052940..f08a61ff 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -17,7 +17,6 @@ from core.handle.textHandle import handleTextMessage
from core.utils.util import (
get_string_no_punctuation_or_emoji,
extract_json_from_string,
- get_ip_info,
initialize_modules,
)
from concurrent.futures import ThreadPoolExecutor, TimeoutError
@@ -30,7 +29,7 @@ from core.mcp.manager import MCPManager
from config.config_loader import get_private_config_from_api
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
from core.utils.output_counter import add_device_output
-from core.handle.ttsReportHandle import enqueue_tts_report
+from core.handle.ttsReportHandle import enqueue_tts_report, report_tts
TAG = __name__
@@ -43,7 +42,15 @@ class TTSException(RuntimeError):
class ConnectionHandler:
def __init__(
- self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent, server=None
+ self,
+ config: Dict[str, Any],
+ _vad,
+ _asr,
+ _llm,
+ _tts,
+ _memory,
+ _intent,
+ server=None,
):
self.config = config
self.server = server
@@ -52,6 +59,7 @@ class ConnectionHandler:
self.need_bind = False
self.bind_code = None
+ self.read_config_from_api = self.config.get("read_config_from_api", False)
self.websocket = None
self.headers = None
@@ -74,11 +82,8 @@ class ConnectionHandler:
self.audio_play_queue = queue.Queue()
self.executor = ThreadPoolExecutor(max_workers=10)
- # 上报线程标志
- self.session_open_time = time.time()
+ # 上报线程
self.tts_report_queue = queue.Queue()
- self.asr_report_queue = queue.Queue()
- self.asr_report_thread = None
self.tts_report_thread = None
# 依赖的组件
@@ -275,19 +280,27 @@ class ConnectionHandler:
)
except Exception as e:
self.logger.bind(tag=TAG).error(f"模块初始化失败: {str(e)}")
- await self.websocket.send(json.dumps({
- "type": "config_update_response",
- "status": "error",
- "message": f"模块初始化失败: {str(e)}"
- }))
+ await self.websocket.send(
+ json.dumps(
+ {
+ "type": "config_update_response",
+ "status": "error",
+ "message": f"模块初始化失败: {str(e)}",
+ }
+ )
+ )
return
# 返回成功响应
- await self.websocket.send(json.dumps({
- "type": "config_update_response",
- "status": "success",
- "message": f"已更新配置: {', '.join(updated_modules)}"
- }))
+ await self.websocket.send(
+ json.dumps(
+ {
+ "type": "config_update_response",
+ "status": "success",
+ "message": f"已更新配置: {', '.join(updated_modules)}",
+ }
+ )
+ )
def _initialize_components(self, private_config):
"""初始化组件"""
@@ -305,26 +318,18 @@ class ConnectionHandler:
def _init_report_threads(self):
"""初始化ASR和TTS上报线程"""
- if self.asr_report_thread is None or not self.asr_report_thread.is_alive():
- self.asr_report_thread = threading.Thread(
- target=self._asr_report_worker,
- daemon=True
- )
- self.asr_report_thread.start()
- self.logger.bind(tag=TAG).info("ASR上报线程已启动")
-
+ if not self.read_config_from_api:
+ return
if self.tts_report_thread is None or not self.tts_report_thread.is_alive():
self.tts_report_thread = threading.Thread(
- target=self._tts_report_worker,
- daemon=True
+ target=self._tts_report_worker, daemon=True
)
self.tts_report_thread.start()
self.logger.bind(tag=TAG).info("TTS上报线程已启动")
def _initialize_private_config(self):
- read_config_from_api = self.config.get("read_config_from_api", False)
"""如果是从配置文件获取,则进行二次实例化"""
- if not read_config_from_api:
+ if not self.read_config_from_api:
return
"""从接口获取差异化的配置进行二次实例化,非全量重新实例化"""
try:
@@ -880,10 +885,9 @@ class ConnectionHandler:
f"TTS生成:文件路径: {tts_file}"
)
if os.path.exists(tts_file):
+ opus_datas, _ = self.tts.audio_to_opus_data(tts_file)
# 在这里上报TTS数据(使用文件路径)
- enqueue_tts_report(self, text, tts_file)
-
- opus_datas, duration = self.tts.audio_to_opus_data(tts_file)
+ enqueue_tts_report(self, 2, text, opus_datas)
else:
self.logger.bind(tag=TAG).error(
f"TTS出错:文件不存在{tts_file}"
@@ -939,43 +943,8 @@ class ConnectionHandler:
f"audio_play_priority priority_thread: {text} {e}"
)
- def _asr_report_worker(self):
- """ASR上报工作线程"""
- # 提前导入避免循环引用问题
- from core.handle.asrReportHandle import report_asr
-
- while not self.stop_event.is_set():
- try:
- # 从队列获取数据,设置超时以便定期检查停止事件
- item = self.asr_report_queue.get(timeout=1)
- if item is None: # 检测毒丸对象
- break
-
- text, file_path = item
-
- try:
- # 执行上报(传入文件路径)
- await_result = report_asr(self, text, file_path)
-
- # 使用asyncio.run_coroutine_threadsafe执行异步操作
- future = asyncio.run_coroutine_threadsafe(await_result, self.loop)
- future.result()
- except Exception as e:
- self.logger.bind(tag=TAG).error(f"ASR上报线程异常: {e}")
- finally:
- # 标记任务完成
- self.asr_report_queue.task_done()
- except queue.Empty:
- continue
- except Exception as e:
- self.logger.bind(tag=TAG).error(f"ASR上报工作线程异常: {e}")
-
- self.logger.bind(tag=TAG).info("ASR上报线程已退出")
-
def _tts_report_worker(self):
"""TTS上报工作线程"""
- # 提前导入避免循环引用问题
- from core.handle.ttsReportHandle import report_tts
while not self.stop_event.is_set():
try:
@@ -984,15 +953,11 @@ class ConnectionHandler:
if item is None: # 检测毒丸对象
break
- text, audio_data = item
+ type, text, audio_data = item
try:
# 执行上报(传入二进制数据)
- await_result = report_tts(self, text, audio_data)
-
- # 使用asyncio.run_coroutine_threadsafe执行异步操作
- future = asyncio.run_coroutine_threadsafe(await_result, self.loop)
- future.result()
+ report_tts(self, type, text, audio_data)
except Exception as e:
self.logger.bind(tag=TAG).error(f"TTS上报线程异常: {e}")
finally:
@@ -1051,7 +1016,6 @@ class ConnectionHandler:
self.executor = None
# 添加毒丸对象到上报队列确保线程退出
- self.asr_report_queue.put(None)
self.tts_report_queue.put(None)
# 清空任务队列
diff --git a/main/xiaozhi-server/core/handle/asrReportHandle.py b/main/xiaozhi-server/core/handle/asrReportHandle.py
deleted file mode 100644
index 70601804..00000000
--- a/main/xiaozhi-server/core/handle/asrReportHandle.py
+++ /dev/null
@@ -1,100 +0,0 @@
-"""
-ASR上报功能已集成到ConnectionHandler类中。
-
-上报功能包括:
-1. 每个连接对象拥有自己的上报队列和处理线程
-2. 上报线程的生命周期与连接对象绑定
-3. 使用ConnectionHandler.enqueue_asr_report方法进行上报
-
-具体实现请参考core/connection.py中的相关代码。
-"""
-
-import os
-from config.logger import setup_logging
-from config.manage_api_client import report
-
-TAG = __name__
-logger = setup_logging()
-
-async def report_asr(conn, text, file_path):
- """执行ASR上报操作
-
- Args:
- conn: 连接对象
- text: 识别文本
- file_path: 音频文件路径(可以为None或空字符串,表示纯文本上报)
- """
- audio_data = None
- try:
- # 处理无音频的纯文本上报
- if not file_path or not os.path.exists(file_path):
- # 纯文本上报时使用空音频数据
- result = await report(
- mac_address=conn.device_id,
- session_id=conn.session_id,
- sort=int(conn.session_open_time),
- chat_type=1, # ASR类型为1
- content=text,
- audio=b'', # 空音频数据
- file_extension="wav"
- )
- logger.bind(tag=TAG).info(f"纯文本上报成功: {conn.device_id}, {conn.session_id}")
- else:
- # 读取文件为二进制数据
- with open(file_path, 'rb') as f:
- audio_data = f.read()
-
- # 正常ASR上报(带音频)
- result = await report(
- mac_address=conn.device_id,
- session_id=conn.session_id,
- sort=int(conn.session_open_time),
- chat_type=1, # ASR类型为1
- content=text,
- audio=audio_data,
- file_extension="wav"
- )
- logger.bind(tag=TAG).info(f"ASR上报成功: {conn.device_id}, {conn.session_id},文件: {file_path}")
-
- return result
- except Exception as e:
- logger.bind(tag=TAG).error(f"ASR上报失败: {e}")
- return None
- finally:
- # 清理资源
- if file_path and os.path.exists(file_path):
- try:
- os.remove(file_path)
- logger.bind(tag=TAG).debug(f"ASR上报后删除文件: {file_path}")
- except Exception as e:
- logger.bind(tag=TAG).error(f"ASR上报后删除文件失败: {e}")
-
- # 手动清理audio_data
- if audio_data:
- del audio_data
-
-def enqueue_asr_report(conn, text, audio):
- """将ASR数据加入上报队列
-
- Args:
- conn: 连接对象
- text: 识别文本
- audio: 音频数据(可以为空列表,表示纯文本上报)
- """
- try:
- if not audio or len(audio) == 0:
- # 纯文本上报,不需要保存文件
- file_path = None
- else:
- # 保存音频数据到文件
- file_path = conn.asr.save_audio_to_file(audio, conn.session_id)
-
- # 使用连接对象的队列,传入文件路径
- conn.asr_report_queue.put((text, file_path))
-
- if not audio or len(audio) == 0:
- logger.bind(tag=TAG).info(f"纯文本数据已加入上报队列: {conn.device_id}, {text[:20] if text else ''}...")
- else:
- logger.bind(tag=TAG).info(f"ASR数据已加入上报队列: {conn.device_id}, 文件: {file_path}")
- except Exception as e:
- logger.bind(tag=TAG).error(f"加入ASR上报队列失败: {e}")
diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py
index 2d8b0eef..a346fb61 100644
--- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py
+++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py
@@ -1,10 +1,11 @@
from config.logger import setup_logging
import time
+import copy
from core.utils.util import remove_punctuation_and_length
from core.handle.sendAudioHandle import send_stt_message
from core.handle.intentHandler import handle_user_intent
from core.utils.output_counter import check_device_output_limit
-from core.handle.asrReportHandle import enqueue_asr_report
+from core.handle.ttsReportHandle import enqueue_tts_report
TAG = __name__
logger = setup_logging()
@@ -42,7 +43,7 @@ async def handleAudioMessage(conn, audio):
text_len, _ = remove_punctuation_and_length(text)
if text_len > 0:
# 使用自定义模块进行上报
- enqueue_asr_report(conn, text, conn.asr_audio)
+ enqueue_tts_report(conn, 1, text, copy.deepcopy(conn.asr_audio))
await startToChat(conn, text)
else:
diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py
index 08c1c40e..d350f78c 100644
--- a/main/xiaozhi-server/core/handle/textHandle.py
+++ b/main/xiaozhi-server/core/handle/textHandle.py
@@ -6,7 +6,7 @@ from core.utils.util import remove_punctuation_and_length
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
-from core.handle.asrReportHandle import enqueue_asr_report
+from core.handle.ttsReportHandle import enqueue_tts_report
import asyncio
TAG = __name__
@@ -56,11 +56,11 @@ async def handleTextMessage(conn, message):
await send_tts_message(conn, "stop", None)
elif is_wakeup_words:
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
- enqueue_asr_report(conn, "嘿,你好呀", [])
+ enqueue_tts_report(conn, 1, "嘿,你好呀", [])
await startToChat(conn, "嘿,你好呀")
else:
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
- enqueue_asr_report(conn, text, [])
+ enqueue_tts_report(conn, 1, text, [])
# 否则需要LLM对文字内容进行答复
await startToChat(conn, text)
elif msg_json["type"] == "iot":
@@ -70,19 +70,22 @@ async def handleTextMessage(conn, message):
asyncio.create_task(handleIotStatus(conn, msg_json["states"]))
elif msg_json["type"] == "server":
# 如果配置是从API读取的,则需要验证secret
- read_config_from_api = conn.config.get("read_config_from_api", False)
- if not read_config_from_api:
+ if not conn.read_config_from_api:
return
# 获取post请求的secret
post_secret = msg_json.get("content", {}).get("secret", "")
secret = conn.config["manager-api"].get("secret", "")
# 如果secret不匹配,则返回
if post_secret != secret:
- await conn.websocket.send(json.dumps({
- "type": "config_update_response",
- "status": "error",
- "message": "服务器密钥验证失败"
- }))
+ await conn.websocket.send(
+ json.dumps(
+ {
+ "type": "config_update_response",
+ "status": "error",
+ "message": "服务器密钥验证失败",
+ }
+ )
+ )
return
# 动态更新配置
if msg_json["action"] == "update_config":
diff --git a/main/xiaozhi-server/core/handle/ttsReportHandle.py b/main/xiaozhi-server/core/handle/ttsReportHandle.py
index ed3a6a76..3aec8a32 100644
--- a/main/xiaozhi-server/core/handle/ttsReportHandle.py
+++ b/main/xiaozhi-server/core/handle/ttsReportHandle.py
@@ -9,62 +9,51 @@ TTS上报功能已集成到ConnectionHandler类中。
具体实现请参考core/connection.py中的相关代码。
"""
-import os
from config.logger import setup_logging
from config.manage_api_client import report
TAG = __name__
logger = setup_logging()
-async def report_tts(conn, text, audio_data):
+
+def report_tts(conn, type, text, opus_data):
"""执行TTS上报操作
-
+
Args:
conn: 连接对象
+ type: 上报类型,1为用户,2为智能体
text: 合成文本
- audio_data: 音频二进制数据
+ opus_data: opus音频数据
"""
try:
# 执行上报
- result = await report(
+ report(
mac_address=conn.device_id,
session_id=conn.session_id,
- sort=int(conn.session_open_time),
- chat_type=2, # TTS类型为2
+ chat_type=type,
content=text,
- audio=audio_data,
- file_extension="wav"
+ opus_data=opus_data,
)
- logger.bind(tag=TAG).info(f"TTS上报成功: {conn.device_id}, {conn.session_id}, 数据大小: {len(audio_data)} 字节")
- return result
except Exception as e:
logger.bind(tag=TAG).error(f"TTS上报失败: {e}")
- return None
- finally:
- # 手动清理audio_data引用,帮助垃圾回收
- del audio_data
-def enqueue_tts_report(conn, text, file_path):
+
+def enqueue_tts_report(conn, type, text, opus_data):
+ if not conn.read_config_from_api:
+ return
"""将TTS数据加入上报队列
-
+
Args:
conn: 连接对象
text: 合成文本
- file_path: TTS音频文件路径
+ opus_data: opus音频数据
"""
try:
- # 检查文件是否存在
- if not file_path or not os.path.exists(file_path):
- logger.bind(tag=TAG).error(f"加入TTS上报队列失败: 文件不存在 {file_path}")
- return
-
- # 立即读取文件为二进制数据,因为外部会删除文件
- with open(file_path, 'rb') as f:
- audio_data = f.read()
-
# 使用连接对象的队列,传入文本和二进制数据而非文件路径
- conn.tts_report_queue.put((text, audio_data))
-
- logger.bind(tag=TAG).info(f"TTS数据已加入上报队列: {conn.device_id}, 文件大小: {len(audio_data)} 字节")
+ conn.tts_report_queue.put((type, text, opus_data))
+
+ logger.bind(tag=TAG).info(
+ f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
+ )
except Exception as e:
- logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {e}, 文件: {file_path}")
+ logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {text}, {e}")