From f92c0313a6f8d07cdcb03cf19498148e8800f8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=89=91=E9=9B=A8?= <2375294554@qq.com> Date: Wed, 30 Apr 2025 09:41:38 +0800 Subject: [PATCH 01/26] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=B5=8B=E8=AF=95=E6=96=B9=E6=B3=95=E5=92=8C?= =?UTF-8?q?=E6=A8=A1=E6=8B=9F=E8=AE=BE=E5=A4=87=E8=BF=9E=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E8=BF=87=E6=9D=A5=E7=9A=84=E6=96=B9=E6=B3=95=EF=BC=8C=E6=96=B9?= =?UTF-8?q?=E4=BE=BF=E6=96=B0=E5=BC=80=E5=8F=91=E8=80=85=E8=B0=83=E8=AF=95?= =?UTF-8?q?=20--DeviceTest.java?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xiaozhi/modules/device/DeviceTest.java | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/main/manager-api/src/test/java/xiaozhi/modules/device/DeviceTest.java b/main/manager-api/src/test/java/xiaozhi/modules/device/DeviceTest.java index 8647692c..4c13bcdb 100644 --- a/main/manager-api/src/test/java/xiaozhi/modules/device/DeviceTest.java +++ b/main/manager-api/src/test/java/xiaozhi/modules/device/DeviceTest.java @@ -10,6 +10,10 @@ import org.springframework.test.context.ActiveProfiles; import lombok.extern.slf4j.Slf4j; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; +import xiaozhi.modules.sys.dto.SysUserDTO; +import xiaozhi.modules.sys.service.SysUserService; + +import java.util.HashMap; @Slf4j @SpringBootTest @@ -19,18 +23,38 @@ public class DeviceTest { @Autowired private RedisUtils redisUtils; + @Autowired + private SysUserService sysUserService; + + @Test + public void testSaveUser() { + SysUserDTO userDTO = new SysUserDTO(); + userDTO.setUsername("13536468486"); + userDTO.setPassword("0218jianyuQ!"); + sysUserService.save(userDTO); + } @Test @DisplayName("测试写入设备信息") public void testWriteDeviceInfo() { log.info("开始测试写入设备信息..."); - // 模拟设备MAC地址 - String macAddress = "00:11:22:33:44:55"; + String macAddress = "00:11:22:33:44:66"; // 模拟设备验证码 String deviceCode = "123456"; - String redisKey = RedisKeys.getDeviceCaptchaKey(deviceCode); + HashMap map = new HashMap<>(); + map.put("mac_address",macAddress); + map.put("activation_code",deviceCode); + map.put("board","硬件型号"); + map.put("app_version","0.3.13"); + + String safeDeviceId = macAddress.replace(":", "_").toLowerCase(); + String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId); + redisUtils.set(cacheDeviceKey, map, 300); + + + String redisKey = "ota:activation:code:"+deviceCode; log.info("Redis Key: {}", redisKey); // 将设备信息写入Redis From 9b0088f4f0a13276c94b78b956dc4d39022614d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=89=91=E9=9B=A8?= <2375294554@qq.com> Date: Wed, 30 Apr 2025 10:44:54 +0800 Subject: [PATCH 02/26] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=AD=A4=E6=99=BA=E8=83=BD=E4=BD=93=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E8=AE=BE=E5=A4=87=E7=9A=84=E6=9C=80=E5=90=8E=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E6=97=B6=E9=97=B4=EF=BC=8C=E6=96=B9=E6=B3=95=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E5=92=8Csql=EF=BC=8C=E4=B8=8D=E4=BD=BF=E7=94=A8mysql-?= =?UTF-8?q?plus=E7=9A=84=E6=96=B9=E6=B3=95=EF=BC=8C=E6=98=AF=E4=B8=BA?= =?UTF-8?q?=E4=BA=86=E5=87=8F=E5=B0=91=E6=95=B0=E6=8D=AE=E5=BA=93=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E4=BC=A0=E8=BE=93=EF=BC=8C=E5=9B=A0=E4=B8=BA=E5=8F=AA?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E6=9C=80=E5=90=8E=E8=BF=9E=E6=8E=A5=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E5=AD=97=E6=AE=B5=20--DeviceDao.java=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=AE=9A=E4=B9=89=20--DeviceDao.xml=20sql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/xiaozhi/modules/device/dao/DeviceDao.java | 10 ++++++++++ .../src/main/resources/mapper/device/DeviceDao.xml | 8 ++++++++ 2 files changed, 18 insertions(+) create mode 100644 main/manager-api/src/main/resources/mapper/device/DeviceDao.xml diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceDao.java b/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceDao.java index 5ede3d6e..abff8ab3 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/dao/DeviceDao.java @@ -6,6 +6,16 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import xiaozhi.modules.device.entity.DeviceEntity; +import java.util.Date; +import java.util.List; + @Mapper public interface DeviceDao extends BaseMapper { + /** + * 获取此智能体全部设备的最后连接时间 + * @param agentId 智能体id + * @return + */ + List getAllLastConnectedAtByAgentId(String agentId); + } \ No newline at end of file diff --git a/main/manager-api/src/main/resources/mapper/device/DeviceDao.xml b/main/manager-api/src/main/resources/mapper/device/DeviceDao.xml new file mode 100644 index 00000000..09f16760 --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/device/DeviceDao.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file From 1d627d570d37446b73a8fbefeaeb88d6120bcb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=89=91=E9=9B=A8?= <2375294554@qq.com> Date: Wed, 30 Apr 2025 10:45:49 +0800 Subject: [PATCH 03/26] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E8=BF=99=E4=B8=AA=E6=99=BA=E8=83=BD=E4=BD=93=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E7=90=86=E7=9A=84=E6=9C=80=E8=BF=91=E7=9A=84=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=97=B6=E9=97=B4=E5=AE=9A=E4=B9=89=E5=92=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=20--DeviceService.java=20=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=20--DeviceServiceImpl.java=20=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/device/service/DeviceService.java | 10 ++++++++++ .../device/service/impl/DeviceServiceImpl.java | 13 +++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java index aff31bff..fb91ff89 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/DeviceService.java @@ -1,5 +1,6 @@ package xiaozhi.modules.device.service; +import java.util.Date; import java.util.List; import xiaozhi.common.page.PageData; @@ -78,4 +79,13 @@ public interface DeviceService extends BaseService { * @return 激活码 */ String geCodeByDeviceId(String deviceId); + + /** + * 获取这个智能体设备理的最近的最后连接时间 + * @param agentId 智能体id + * @return 返回设备最近的最后连接时间 + */ + Date getLatestLastConnectionTime(String agentId); + + } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index 8eb23a93..99b40a38 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -1,12 +1,7 @@ package xiaozhi.modules.device.service.impl; import java.time.Instant; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TimeZone; -import java.util.UUID; +import java.util.*; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -258,6 +253,12 @@ public class DeviceServiceImpl extends BaseServiceImpl return null; } + @Override + public Date getLatestLastConnectionTime(String agentId) { + List list = deviceDao.getAllLastConnectedAtByAgentId(agentId); + return Collections.max(list, Comparator.comparing(Date::getTime)); + } + private String getDeviceCacheKey(String deviceId) { String safeDeviceId = deviceId.replace(":", "_").toLowerCase(); String dataKey = String.format("ota:activation:data:%s", safeDeviceId); From 7d74e853ae0ec674e3b6bc9c20b4a195331e242e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=89=91=E9=9B=A8?= <2375294554@qq.com> Date: Wed, 30 Apr 2025 10:46:34 +0800 Subject: [PATCH 04/26] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93=E6=9C=80=E8=BF=91=E7=9A=84=E6=9C=80=E5=90=8E=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E6=97=B6=E9=97=B4=E4=B8=BA=E7=A9=BA=E7=9A=84bug=20--A?= =?UTF-8?q?gentServiceImpl.java=20=E4=BF=AE=E5=A4=8Dbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/agent/service/impl/AgentServiceImpl.java | 9 +++++++++ 1 file changed, 9 insertions(+) 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..ff883a57 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 @@ -21,6 +21,7 @@ import xiaozhi.modules.agent.dao.AgentDao; import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.model.service.ModelConfigService; import xiaozhi.modules.timbre.service.TimbreService; @@ -31,6 +32,9 @@ public class AgentServiceImpl extends BaseServiceImpl imp @Autowired private TimbreService timbreModelService; + @Autowired + private DeviceService deviceService; + @Autowired private ModelConfigService modelConfigService; @@ -104,6 +108,11 @@ public class AgentServiceImpl extends BaseServiceImpl imp // 获取设备数量 dto.setDeviceCount(getDeviceCountByAgentId(agent.getId())); + // 获取智能体最近的最后连接时长 + dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId())); + + + return dto; }).collect(Collectors.toList()); } From 6f7ff9f858bee0c154aa00ed552bed5636c96a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=89=91=E9=9B=A8?= <2375294554@qq.com> Date: Wed, 30 Apr 2025 11:05:20 +0800 Subject: [PATCH 05/26] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E8=BF=99=E4=B8=AA=E6=99=BA=E8=83=BD=E4=BD=93=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E7=90=86=E7=9A=84=E6=9C=80=E8=BF=91=E7=9A=84=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E6=97=B6=E9=97=B4=E6=96=B9=E6=B3=95=EF=BC=8C?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=97=B6=E9=97=B4=E5=92=8C=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=95=B0=E9=87=8F=20--DeviceServiceImpl.java=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device/service/impl/DeviceServiceImpl.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index 99b40a38..0475f2e9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -255,8 +255,19 @@ public class DeviceServiceImpl extends BaseServiceImpl @Override public Date getLatestLastConnectionTime(String agentId) { + // 查询是否有缓存时间,有则返回 + Date cachedDate = (Date) redisUtils.get(RedisKeys.getAgentDeviceLastConnectedAtById(agentId)); + if (cachedDate != null) { + return cachedDate; + } List list = deviceDao.getAllLastConnectedAtByAgentId(agentId); - return Collections.max(list, Comparator.comparing(Date::getTime)); + Date maxDate = Collections.max(list, Comparator.comparing(Date::getTime)); + // 将结果存入Redis, 存储设备最近最后连接时间,和设备数量 + redisUtils.set(RedisKeys.getAgentDeviceCountById(agentId), list.size(), 60); + if (maxDate != null) { + redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), maxDate, 60); + } + return maxDate; } private String getDeviceCacheKey(String deviceId) { From 09c17b2dc5325754bf579af8d8f30ab83dffa7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=89=91=E9=9B=A8?= <2375294554@qq.com> Date: Wed, 30 Apr 2025 11:07:53 +0800 Subject: [PATCH 06/26] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=B9=E6=B3=95?= =?UTF-8?q?=EF=BC=8C=E6=8A=8A=E8=8E=B7=E5=8F=96=E6=9C=80=E8=BF=91=E6=9C=80?= =?UTF-8?q?=E5=90=8E=E6=97=B6=E9=97=B4=E8=8E=B7=E5=8F=96=EF=BC=8C=E6=94=BE?= =?UTF-8?q?=E5=9C=A8=E8=8E=B7=E5=8F=96=E8=AE=BE=E5=A4=87=E6=95=B0=E9=87=8F?= =?UTF-8?q?=E5=89=8D=E9=9D=A2=E5=89=8D=EF=BC=8C=E5=8F=AF=E4=BB=A5=E5=87=8F?= =?UTF-8?q?=E5=B0=91=E4=B8=80=E6=AC=A1sql=E6=9F=A5=E8=AF=A2=EF=BC=8C?= =?UTF-8?q?=E5=9B=A0=E4=B8=BA=E5=9C=A8=E8=8E=B7=E5=8F=96=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=9A=84=E6=97=B6=E5=80=99=EF=BC=8C=E5=B7=B2=E7=BB=8F=E9=A1=BA?= =?UTF-8?q?=E4=BE=BF=E7=BC=93=E5=AD=98=E7=9A=84=E8=AE=BE=E5=A4=87=E6=95=B0?= =?UTF-8?q?=E9=87=8F=E4=BA=86=20--AgentServiceImpl.java=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=EF=BC=9A=E5=87=8F=E5=B0=91sql=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/agent/service/impl/AgentServiceImpl.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) 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 ff883a57..c731c2fa 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 @@ -105,14 +105,11 @@ public class AgentServiceImpl extends BaseServiceImpl imp // 获取 TTS 音色名称 dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId())); - // 获取设备数量 - dto.setDeviceCount(getDeviceCountByAgentId(agent.getId())); - // 获取智能体最近的最后连接时长 dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId())); - - + // 获取设备数量 + dto.setDeviceCount(getDeviceCountByAgentId(agent.getId())); return dto; }).collect(Collectors.toList()); } From b4944f23ef873b27eb808b23fb97e0e4766d36b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=89=91=E9=9B=A8?= <2375294554@qq.com> Date: Wed, 30 Apr 2025 11:08:59 +0800 Subject: [PATCH 07/26] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=E6=96=B0=E7=9A=84redis:key=20--RedisKeys.java=20?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E8=AE=BE=E5=A4=87=E6=9C=80=E8=BF=91=E6=9C=80?= =?UTF-8?q?=E4=B9=85=E6=97=B6=E9=97=B4=E7=9A=84key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/xiaozhi/common/redis/RedisKeys.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java index 5d3b4f32..5e5ec309 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java +++ b/main/manager-api/src/main/java/xiaozhi/common/redis/RedisKeys.java @@ -62,6 +62,13 @@ public class RedisKeys { return "agent:device:count:" + id; } + /** + * 获取智能体最后连接时间缓存key + */ + public static String getAgentDeviceLastConnectedAtById(String id) { + return "agent:device:lastConnected:" + id; + } + /** * 获取系统配置缓存key */ From ee3f0555d1a0642c30f13c456af94d681776d124 Mon Sep 17 00:00:00 2001 From: caixypromise Date: Sun, 4 May 2025 22:58:43 +0800 Subject: [PATCH 08/26] =?UTF-8?q?fix(app):=20=E9=87=8D=E5=86=99app.py?= =?UTF-8?q?=E5=86=85=E7=9A=84wait=5Ffor=5Fexit()=EF=BC=8C=E4=BB=A5?= =?UTF-8?q?=E6=AD=A4=E8=A7=A3=E5=86=B3Windows=E7=8E=AF=E5=A2=83=E4=B8=8B?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E9=80=80=E5=87=BA=E6=97=B6=EF=BC=8C=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E9=98=BB=E5=A1=9E=E5=8D=A1=E6=AD=BB=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E3=80=82=20=E5=BD=B1=E5=93=8D=20----=20-=20Ctrl?= =?UTF-8?q?=E2=80=91C/kill=E9=80=80=E5=87=BA=E6=97=B6=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=8D=A1=E4=BD=8F=EF=BC=8C=E8=B5=84=E6=BA=90=E5=AE=8C=E5=85=A8?= =?UTF-8?q?=E9=87=8A=E6=94=BE=EF=BC=8C=20-=20Windows=20=E4=B8=8E=20Unix=20?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=E4=B8=80=E8=87=B4=EF=BC=8C=E6=94=B9=E5=8A=A8?= =?UTF-8?q?=E4=B8=8D=E5=BD=B1=E5=93=8D=E6=AD=A3=E5=B8=B8=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/app.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/main/xiaozhi-server/app.py b/main/xiaozhi-server/app.py index 7ed71ae5..4ad57855 100644 --- a/main/xiaozhi-server/app.py +++ b/main/xiaozhi-server/app.py @@ -12,22 +12,26 @@ TAG = __name__ logger = setup_logging() -async def wait_for_exit(): - """Windows 和 Linux 兼容的退出监听""" +async def wait_for_exit() -> None: + """ + 阻塞直到收到 Ctrl‑C / SIGTERM。 + - Unix: 使用 add_signal_handler + - Windows: 依赖 KeyboardInterrupt + """ loop = asyncio.get_running_loop() stop_event = asyncio.Event() - if sys.platform == "win32": - # Windows: 用 sys.stdin.read() 监听 Ctrl + C - await loop.run_in_executor(None, sys.stdin.read) - else: - # Linux/macOS: 用 signal 监听 Ctrl + C - def stop(): - stop_event.set() - - loop.add_signal_handler(signal.SIGINT, stop) - loop.add_signal_handler(signal.SIGTERM, stop) # 支持 kill 进程 + if sys.platform != "win32": # Unix / macOS + for sig in (signal.SIGINT, signal.SIGTERM): + loop.add_signal_handler(sig, stop_event.set) await stop_event.wait() + else: + # Windows:await一个永远pending的fut, + # 让 KeyboardInterrupt 冒泡到 asyncio.run,以此消除遗留普通线程导致进程退出阻塞的问题 + try: + await asyncio.Future() + except KeyboardInterrupt: # Ctrl‑C + pass async def main(): From 0a765f4aaca98486f06bdc83122f8a5996b94ddd Mon Sep 17 00:00:00 2001 From: caixypromise Date: Sun, 4 May 2025 23:01:30 +0800 Subject: [PATCH 09/26] =?UTF-8?q?fix(MCP):=20=E9=87=8D=E6=9E=84MCPClient?= =?UTF-8?q?=E4=B8=BA=E5=90=8E=E5=8F=B0=E5=8D=8F=E7=A8=8B=20+=20AsyncExitSt?= =?UTF-8?q?ack=E7=AE=A1=E7=90=86=EF=BC=8C=E8=A7=A3=E5=86=B3=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E9=80=80=E5=87=BA=E6=97=B6=E7=9A=84=E2=80=9CAttempted?= =?UTF-8?q?=20to=20exit=20cancel=20scope=20in=20a=20different=20task?= =?UTF-8?q?=E2=80=9D=E9=94=99=E8=AF=AF=20#=20=E5=8F=98=E6=9B=B4=20----=20-?= =?UTF-8?q?=20=E5=B0=86=E6=89=80=E6=9C=89stdio=5Fclient=E4=B8=8EClientSess?= =?UTF-8?q?ion=E7=9A=84=E5=88=9B=E5=BB=BA/=E9=94=80=E6=AF=81=E9=83=BD?= =?UTF-8?q?=E6=94=BE=E5=88=B0=E5=90=8C=E4=B8=80=E4=B8=AA=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=20task=20=E4=B8=AD=20-=20=E4=BD=BF=E7=94=A8AsyncExitStack?= =?UTF-8?q?=E6=89=98=E7=AE=A1=E5=BC=82=E6=AD=A5=E8=B5=84=E6=BA=90=EF=BC=8C?= =?UTF-8?q?cleanup=E6=97=B6=E5=9C=A8=E5=90=8C=E4=B8=80task=E5=86=85?= =?UTF-8?q?=E6=89=A7=E8=A1=8Cexit=5Fstack.aclose()=20-=20=E5=A4=96?= =?UTF-8?q?=E9=83=A8=E5=8F=AA=E9=80=9A=E8=BF=87=E4=BA=8B=E4=BB=B6=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E5=90=8E=E5=8F=B0task=E9=80=80=E5=87=BA=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E8=B7=A8=E5=8D=8F=E7=A8=8B=E8=B0=83=E7=94=A8?= =?UTF-8?q?cancel-scope=E5=BC=82=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/mcp/MCPClient.py | 197 ++++++++++++++-------- main/xiaozhi-server/core/mcp/manager.py | 17 +- 2 files changed, 137 insertions(+), 77 deletions(-) diff --git a/main/xiaozhi-server/core/mcp/MCPClient.py b/main/xiaozhi-server/core/mcp/MCPClient.py index 103ac645..e2517f92 100644 --- a/main/xiaozhi-server/core/mcp/MCPClient.py +++ b/main/xiaozhi-server/core/mcp/MCPClient.py @@ -1,86 +1,145 @@ +from __future__ import annotations + +import asyncio, os, shutil, concurrent.futures from datetime import timedelta -from typing import Optional from contextlib import AsyncExitStack -import os, shutil +from typing import Optional, List + from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client - from config.logger import setup_logging TAG = __name__ + class MCPClient: - def __init__(self, config): - # Initialize session and client objects - self.session: Optional[ClientSession] = None - self.exit_stack = AsyncExitStack() + def __init__(self, config: dict): self.logger = setup_logging() self.config = config - self.tolls = [] + + # Back‑worker task & 状态同步 + self._worker_task: Optional[asyncio.Task] = None + self._ready_evt = asyncio.Event() + self._shutdown_evt = asyncio.Event() + + # 运行时资源 + self.session: Optional[ClientSession] = None + self.tools: List = [] async def initialize(self): - args = self.config.get("args", []) + """ + 启动后台 task,并等待其就绪(拿到 `tools`)。 + """ + if self._worker_task: + return # 已经 init 过 - command = ( - shutil.which("npx") - if self.config["command"] == "npx" - else self.config["command"] - ) - - env={**os.environ} - if self.config.get("env"): - env.update(self.config["env"]) - - server_params = StdioServerParameters( - command=command, - args=args, - env=env - ) - - stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) - self.stdio, self.write = stdio_transport - time_out_delta = timedelta(seconds=15) - self.session = await self.exit_stack.enter_async_context(ClientSession(read_stream=self.stdio, write_stream=self.write, read_timeout_seconds=time_out_delta)) - - await self.session.initialize() - - # List available tools - response = await self.session.list_tools() - tools = response.tools - self.tools = tools - self.logger.bind(tag=TAG).info(f"Connected to server with tools:{[tool.name for tool in tools]}") - - def has_tool(self, tool_name): - return any(tool.name == tool_name for tool in self.tools) - - def get_available_tools(self): - available_tools = [{"type": "function", "function":{ - "name": tool.name, - "description": tool.description, - "parameters": tool.inputSchema - } } for tool in self.tools] + # 在当前 loop 创建后台 task + self._worker_task = asyncio.create_task(self._worker(), name="MCPClientWorker") + await self._ready_evt.wait() # 等待 worker 初始化完成 - return available_tools - - async def call_tool(self, tool_name: str, tool_args: dict): - self.logger.bind(tag=TAG).info(f"MCPClient Calling tool {tool_name} with args: {tool_args}") - try: - response = await self.session.call_tool(tool_name, tool_args) - except Exception as e: - self.logger.bind(tag=TAG).error(f"Error calling tool {tool_name}: {e}") - from types import SimpleNamespace - error_content = SimpleNamespace( - type='text', - text=f"Error calling tool {tool_name}: {e}" - ) - error_response = SimpleNamespace( - content=[error_content], - isError=True - ) - return error_response - self.logger.bind(tag=TAG).info(f"MCPClient Response from tool {tool_name}: {response}") - return response + # 此时 tools 已填充 + self.logger.bind(tag=TAG).info( + f"Connected, tools = {[t.name for t in self.tools]}" + ) async def cleanup(self): - """Clean up resources""" - await self.exit_stack.aclose() \ No newline at end of file + """ + 对外关闭接口: + · 只负责发出 “关机信号” + · 等待后台 task 正常退出 + 在任何 loop / task 调用都安全。 + """ + if not self._worker_task: + return + + self._shutdown_evt.set() # 发信号 + try: + await asyncio.wait_for(self._worker_task, timeout=15) + except (asyncio.TimeoutError, Exception) as e: + self.logger.bind(tag=TAG).error(f"worker shutdown err: {e}") + finally: + self._worker_task = None + + # ----------------------------- 工具接口 ----------------------------- + + def has_tool(self, name: str) -> bool: + return any(t.name == name for t in self.tools) + + def get_available_tools(self): + return [ + { + "type": "function", + "function": { + "name": t.name, + "description": t.description, + "parameters": t.inputSchema, + }, + } + for t in self.tools + ] + + async def call_tool(self, name: str, args: dict): + """ + 转发到 session.call_tool。 + 若在 worker 之外的 task 调用,会通过 run_coroutine_threadsafe + 投递到 worker 所在 loop 中执行,保证线程安全。 + """ + if not self.session: # 尚未就绪 + raise RuntimeError("MCPClient not initialized") + + loop = self._worker_task.get_loop() + coro = self.session.call_tool(name, args) + + # 在同一个 loop ➜ 直接 await + if loop is asyncio.get_running_loop(): + return await coro + + # 跨 loop ➜ run_coroutine_threadsafe + fut: concurrent.futures.Future = asyncio.run_coroutine_threadsafe(coro, loop) + return await asyncio.wrap_future(fut) + + # ----------------------------- 后台 task ----------------------------- + + async def _worker(self): + """ + 单线程协程: + 1. 创建所有异步资源 + 2. set_ready → 供外部使用 + 3. 等待 shutdown_evt + 4. 自动随 AsyncExitStack 退出而清理资源 + """ + async with AsyncExitStack() as stack: + try: + # ---------- 启动后端进程 ---------- + cmd = shutil.which("npx") if self.config["command"] == "npx" else self.config["command"] + env = {**os.environ, **self.config.get("env", {})} + params = StdioServerParameters( + command=cmd, + args=self.config.get("args", []), + env=env, + ) + stdio_r, stdio_w = await stack.enter_async_context(stdio_client(params)) + + # ---------- 会话 ---------- + self.session = await stack.enter_async_context( + ClientSession( + read_stream=stdio_r, + write_stream=stdio_w, + read_timeout_seconds=timedelta(seconds=15), + ) + ) + await self.session.initialize() + + # ---------- 工具 ---------- + self.tools = (await self.session.list_tools()).tools + + # 初始化完成,放行外部 + self._ready_evt.set() + + # ---------- 挂起等待关闭 ---------- + await self._shutdown_evt.wait() + + except Exception as e: + self.logger.bind(tag=TAG).error(f"worker error: {e}") + self._ready_evt.set() # 确保外部不会卡死 + raise diff --git a/main/xiaozhi-server/core/mcp/manager.py b/main/xiaozhi-server/core/mcp/manager.py index f212b67a..0a35f357 100644 --- a/main/xiaozhi-server/core/mcp/manager.py +++ b/main/xiaozhi-server/core/mcp/manager.py @@ -1,5 +1,5 @@ """MCP服务管理器""" - +import asyncio import os, json from typing import Dict, Any, List from .MCPClient import MCPClient @@ -94,8 +94,8 @@ class MCPManager: """ for tool in self.tools: if ( - tool.get("function") != None - and tool["function"].get("name") == tool_name + tool.get("function") != None + and tool["function"].get("name") == tool_name ): return True return False @@ -120,12 +120,13 @@ class MCPManager: raise ValueError(f"Tool {tool_name} not found in any MCP server") async def cleanup_all(self) -> None: - for name, client in self.client.items(): + """依次关闭所有 MCPClient,不让异常阻断整体流程。""" + for name, client in list(self.client.items()): try: - await client.cleanup() - self.logger.bind(tag=TAG).info(f"Cleaned up MCP client: {name}") - except Exception as e: + await asyncio.wait_for(client.cleanup(), timeout=20) + self.logger.bind(tag=TAG).info(f"MCP client closed: {name}") + except (asyncio.TimeoutError, Exception) as e: self.logger.bind(tag=TAG).error( - f"Error cleaning up MCP client {name}: {e}" + f"Error closing MCP client {name}: {e}" ) self.client.clear() From 8f266bea0d62d0ab2d2f7e2ad476440aad00622f Mon Sep 17 00:00:00 2001 From: kevin1sMe Date: Sun, 4 May 2025 23:33:07 +0800 Subject: [PATCH 10/26] =?UTF-8?q?feat:=20MCP=20server=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BD=BF=E7=94=A8sse=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/mcp/MCPClient.py | 52 +++++++++++--------- main/xiaozhi-server/core/mcp/manager.py | 4 +- main/xiaozhi-server/mcp_server_settings.json | 3 ++ main/xiaozhi-server/requirements.txt | 2 +- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/main/xiaozhi-server/core/mcp/MCPClient.py b/main/xiaozhi-server/core/mcp/MCPClient.py index 103ac645..7996ff67 100644 --- a/main/xiaozhi-server/core/mcp/MCPClient.py +++ b/main/xiaozhi-server/core/mcp/MCPClient.py @@ -4,6 +4,7 @@ from contextlib import AsyncExitStack import os, shutil from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client +from mcp.client.sse import sse_client from config.logger import setup_logging @@ -21,29 +22,36 @@ class MCPClient: async def initialize(self): args = self.config.get("args", []) - command = ( - shutil.which("npx") - if self.config["command"] == "npx" - else self.config["command"] - ) - - env={**os.environ} - if self.config.get("env"): - env.update(self.config["env"]) - - server_params = StdioServerParameters( - command=command, - args=args, - env=env - ) - - stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) - self.stdio, self.write = stdio_transport - time_out_delta = timedelta(seconds=15) - self.session = await self.exit_stack.enter_async_context(ClientSession(read_stream=self.stdio, write_stream=self.write, read_timeout_seconds=time_out_delta)) - + if "command" in self.config: + command = ( + shutil.which("npx") + if self.config["command"] == "npx" + else self.config["command"] + ) + env = {**os.environ} + if self.config.get("env"): + env.update(self.config["env"]) + server_params = StdioServerParameters( + command=command, + args=args, + env=env + ) + stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) + self.stdio, self.write = stdio_transport + time_out_delta = timedelta(seconds=15) + self.session = await self.exit_stack.enter_async_context( + ClientSession(read_stream=self.stdio, write_stream=self.write, read_timeout_seconds=time_out_delta) + ) + elif "url" in self.config: + sse_transport = await self.exit_stack.enter_async_context(sse_client(self.config["url"])) + self.sse_read, self.sse_write = sse_transport + self.session = await self.exit_stack.enter_async_context( + ClientSession(read_stream=self.sse_read, write_stream=self.sse_write) + ) + else: + raise ValueError("MCPClient config must have 'command' or 'url'.") + await self.session.initialize() - # List available tools response = await self.session.list_tools() tools = response.tools diff --git a/main/xiaozhi-server/core/mcp/manager.py b/main/xiaozhi-server/core/mcp/manager.py index f212b67a..a0e310fa 100644 --- a/main/xiaozhi-server/core/mcp/manager.py +++ b/main/xiaozhi-server/core/mcp/manager.py @@ -50,9 +50,9 @@ class MCPManager: """初始化所有MCP服务""" config = self.load_config() for name, srv_config in config.items(): - if not srv_config.get("command"): + if not srv_config.get("command") and not srv_config.get("url"): self.logger.bind(tag=TAG).warning( - f"Skipping server {name}: command not specified" + f"Skipping server {name}: neither command nor url specified" ) continue diff --git a/main/xiaozhi-server/mcp_server_settings.json b/main/xiaozhi-server/mcp_server_settings.json index a38bbdfb..26e2cf42 100644 --- a/main/xiaozhi-server/mcp_server_settings.json +++ b/main/xiaozhi-server/mcp_server_settings.json @@ -26,6 +26,9 @@ "command": "npx", "args": ["-y", "@simonb97/server-win-cli"], "link": "https://github.com/SimonB97/win-cli-mcp-server" + }, + "perplexity-ask": { + "url": "http://your-server-ip:your-port/sse" } } } diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt index 7da31b6f..7b7237cf 100755 --- a/main/xiaozhi-server/requirements.txt +++ b/main/xiaozhi-server/requirements.txt @@ -22,7 +22,7 @@ mem0ai==0.1.62 bs4==0.0.2 modelscope==1.23.2 sherpa_onnx==1.11.0 -mcp==1.4.1 +mcp==1.7.1 cnlunar==0.2.0 PySocks==1.7.1 dashscope==1.23.1 \ No newline at end of file From 3fcfb65d45fefb086ecb6a375554dee29e6e25ec Mon Sep 17 00:00:00 2001 From: kevin1sMe Date: Sun, 4 May 2025 23:37:17 +0800 Subject: [PATCH 11/26] update: example --- main/xiaozhi-server/mcp_server_settings.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/main/xiaozhi-server/mcp_server_settings.json b/main/xiaozhi-server/mcp_server_settings.json index 26e2cf42..dc173f37 100644 --- a/main/xiaozhi-server/mcp_server_settings.json +++ b/main/xiaozhi-server/mcp_server_settings.json @@ -3,7 +3,8 @@ "在data目录下创建.mcp_server_settings.json文件,可以选择下面的MCP服务,也可以自行添加新的MCP服务。", "后面不断测试补充好用的mcp服务,欢迎大家一起补充。", "记得删除注释行,des属性仅为说明,不会被解析。", - "des和link属性,仅为说明安装方式,方便大家查看原始链接,不是必须项。" + "des和link属性,仅为说明安装方式,方便大家查看原始链接,不是必须项。", + "当前支持stdio/sse两种模式。" ], "mcpServers": { "filesystem": { @@ -26,9 +27,6 @@ "command": "npx", "args": ["-y", "@simonb97/server-win-cli"], "link": "https://github.com/SimonB97/win-cli-mcp-server" - }, - "perplexity-ask": { - "url": "http://your-server-ip:your-port/sse" } } } From a2baef89118c3f3753a8f9a83feafe1089ab84f0 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 6 May 2025 13:09:43 +0800 Subject: [PATCH 12/26] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E9=BB=98=E8=AE=A4=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/ConfigServiceImpl.java | 22 ++++-------------- main/xiaozhi-server/core/websocket_server.py | 23 ++++++++++++------- 2 files changed, 20 insertions(+), 25 deletions(-) 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 f0ed4bf1..a15df002 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 @@ -61,15 +61,15 @@ public class ConfigServiceImpl implements ConfigService { // 构建模块配置 buildModuleConfig( - agent.getAgentName(), + null, null, null, agent.getVadModelId(), agent.getAsrModelId(), - agent.getLlmModelId(), - agent.getTtsModelId(), - agent.getMemModelId(), - agent.getIntentModelId(), + null, + null, + null, + null, result, isCache); @@ -117,18 +117,6 @@ public class ConfigServiceImpl implements ConfigService { if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) { agent.setAsrModelId(null); } - String alreadySelectedLlmModelId = (String) selectedModule.get("LLM"); - if (alreadySelectedLlmModelId != null && alreadySelectedLlmModelId.equals(agent.getLlmModelId())) { - agent.setLlmModelId(null); - } - String alreadySelectedMemModelId = (String) selectedModule.get("Memory"); - if (alreadySelectedMemModelId != null && alreadySelectedMemModelId.equals(agent.getMemModelId())) { - agent.setMemModelId(null); - } - String alreadySelectedIntentModelId = (String) selectedModule.get("Intent"); - if (alreadySelectedIntentModelId != null && alreadySelectedIntentModelId.equals(agent.getIntentModelId())) { - agent.setIntentModelId(null); - } // 构建模块配置 buildModuleConfig( diff --git a/main/xiaozhi-server/core/websocket_server.py b/main/xiaozhi-server/core/websocket_server.py index 4b933865..06f00594 100644 --- a/main/xiaozhi-server/core/websocket_server.py +++ b/main/xiaozhi-server/core/websocket_server.py @@ -13,14 +13,21 @@ class WebSocketServer: self.logger = setup_logging() self.config_lock = asyncio.Lock() modules = initialize_modules( - self.logger, self.config, True, True, True, True, True, True + self.logger, + self.config, + "VAD" in self.config["selected_module"], + "ASR" in self.config["selected_module"], + "LLM" in self.config["selected_module"], + "TTS" in self.config["selected_module"], + "Memory" in self.config["selected_module"], + "Intent" in self.config["selected_module"], ) - self._vad = modules["vad"] - self._asr = modules["asr"] - self._tts = modules["tts"] - self._llm = modules["llm"] - self._intent = modules["intent"] - self._memory = modules["memory"] + self._vad = modules["vad"] if "vad" in modules else None + self._asr = modules["asr"] if "asr" in modules else None + self._tts = modules["tts"] if "tts" in modules else None + self._llm = modules["llm"] if "llm" in modules else None + self._intent = modules["intent"] if "intent" in modules else None + self._memory = modules["memory"] if "memory" in modules else None self.active_connections = set() async def start(self): @@ -44,7 +51,7 @@ class WebSocketServer: self._tts, self._memory, self._intent, - self # 传入当前 WebSocketServer 实例 + self, # 传入当前 WebSocketServer 实例 ) self.active_connections.add(handler) try: From aa77bfdfc4f56e031a81e404050aba3550e3ee42 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 6 May 2025 13:10:26 +0800 Subject: [PATCH 13/26] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E6=9C=AA?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=E7=94=A8=E6=88=B7=E7=9A=84=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 11 +- .../xiaozhi-server/core/handle/helloHandle.py | 2 +- .../core/handle/receiveAudioHandle.py | 9 +- .../core/handle/ttsReportHandle.py | 4 +- .../xiaozhi-server/core/providers/tts/base.py | 53 +- main/xiaozhi-server/core/utils/util.py | 606 +++++++++++++++--- 6 files changed, 535 insertions(+), 150 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index f08a61ff..27b1e0ac 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -151,11 +151,9 @@ class ConnectionHandler: self.headers["device-id"] = query_params["device-id"][0] self.headers["client-id"] = query_params["client-id"][0] else: - self.logger.bind(tag=TAG).error( - "无法从请求头和URL查询参数中获取device-id" - ) + await ws.send("端口正常,如需测试连接,请使用test_page.html") + await self.close(ws) return - # 获取客户端ip地址 self.client_ip = ws.remote_address[0] self.logger.bind(tag=TAG).info( @@ -212,7 +210,8 @@ class ConnectionHandler: async def _save_and_close(self, ws): """保存记忆并关闭连接""" try: - await self.memory.save_memory(self.dialogue.dialogue) + if self.memory: + await self.memory.save_memory(self.dialogue.dialogue) except Exception as e: self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}") finally: @@ -318,7 +317,7 @@ class ConnectionHandler: def _init_report_threads(self): """初始化ASR和TTS上报线程""" - if not self.read_config_from_api: + if not self.read_config_from_api or self.need_bind: return if self.tts_report_thread is None or not self.tts_report_thread.is_alive(): self.tts_report_thread = threading.Thread( diff --git a/main/xiaozhi-server/core/handle/helloHandle.py b/main/xiaozhi-server/core/handle/helloHandle.py index 51782bb9..e84a798f 100644 --- a/main/xiaozhi-server/core/handle/helloHandle.py +++ b/main/xiaozhi-server/core/handle/helloHandle.py @@ -44,7 +44,7 @@ async def checkWakeupWords(conn, text): if file is None: asyncio.create_task(wakeupWordsResponse(conn)) return False - opus_packets, duration = conn.tts.audio_to_opus_data(file) + opus_packets, _ = conn.tts.audio_to_opus_data(file) text_hello = WAKEUP_CONFIG["text"] if not text_hello: text_hello = text diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py index a346fb61..ff6aaad5 100644 --- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py +++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py @@ -6,6 +6,7 @@ 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.ttsReportHandle import enqueue_tts_report +from core.providers.tts.base import audio_to_opus_data TAG = __name__ logger = setup_logging() @@ -110,7 +111,7 @@ async def max_out_size(conn): conn.tts_last_text_index = 0 conn.llm_finish_task = True file_path = "config/assets/max_output_size.wav" - opus_packets, _ = conn.tts.audio_to_opus_data(file_path) + opus_packets, _ = audio_to_opus_data(file_path) conn.audio_play_queue.put((opus_packets, text, 0)) conn.close_after_chat = True @@ -132,7 +133,7 @@ async def check_bind_device(conn): # 播放提示音 music_path = "config/assets/bind_code.wav" - opus_packets, _ = conn.tts.audio_to_opus_data(music_path) + opus_packets, _ = audio_to_opus_data(music_path) conn.audio_play_queue.put((opus_packets, text, 0)) # 逐个播放数字 @@ -140,7 +141,7 @@ async def check_bind_device(conn): try: digit = conn.bind_code[i] num_path = f"config/assets/bind_code/{digit}.wav" - num_packets, _ = conn.tts.audio_to_opus_data(num_path) + num_packets, _ = audio_to_opus_data(num_path) conn.audio_play_queue.put((num_packets, None, i + 1)) except Exception as e: logger.bind(tag=TAG).error(f"播放数字音频失败: {e}") @@ -152,5 +153,5 @@ async def check_bind_device(conn): conn.tts_last_text_index = 0 conn.llm_finish_task = True music_path = "config/assets/bind_not_found.wav" - opus_packets, _ = conn.tts.audio_to_opus_data(music_path) + opus_packets, _ = audio_to_opus_data(music_path) conn.audio_play_queue.put((opus_packets, text, 0)) diff --git a/main/xiaozhi-server/core/handle/ttsReportHandle.py b/main/xiaozhi-server/core/handle/ttsReportHandle.py index 5442a79f..0935b9de 100644 --- a/main/xiaozhi-server/core/handle/ttsReportHandle.py +++ b/main/xiaozhi-server/core/handle/ttsReportHandle.py @@ -95,7 +95,7 @@ def opus_to_wav(opus_data): def enqueue_tts_report(conn, type, text, opus_data): - if not conn.read_config_from_api: + if not conn.read_config_from_api or conn.need_bind: return """将TTS数据加入上报队列 @@ -108,7 +108,7 @@ def enqueue_tts_report(conn, type, text, opus_data): # 使用连接对象的队列,传入文本和二进制数据而非文件路径 conn.tts_report_queue.put((type, text, opus_data)) - logger.bind(tag=TAG).info( + logger.bind(tag=TAG).debug( f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} " ) except Exception as e: diff --git a/main/xiaozhi-server/core/providers/tts/base.py b/main/xiaozhi-server/core/providers/tts/base.py index e668459c..4cf37edb 100644 --- a/main/xiaozhi-server/core/providers/tts/base.py +++ b/main/xiaozhi-server/core/providers/tts/base.py @@ -1,11 +1,9 @@ import asyncio from config.logger import setup_logging import os -import numpy as np -import opuslib_next -from pydub import AudioSegment from abc import ABC, abstractmethod from core.utils.tts import MarkdownCleaner +from core.utils.util import audio_to_opus_data TAG = __name__ logger = setup_logging() @@ -29,7 +27,9 @@ class TTSProviderBase(ABC): try: asyncio.run(self.text_to_speak(text, tmp_file)) except Exception as e: - logger.bind(tag=TAG).warning(f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}") + logger.bind(tag=TAG).warning( + f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}" + ) # 未执行成功,删除文件 if os.path.exists(tmp_file): os.remove(tmp_file) @@ -54,47 +54,4 @@ class TTSProviderBase(ABC): pass def audio_to_opus_data(self, audio_file_path): - """音频文件转换为Opus编码""" - # 获取文件后缀名 - file_type = os.path.splitext(audio_file_path)[1] - if file_type: - file_type = file_type.lstrip(".") - # 读取音频文件,-nostdin 参数:不要从标准输入读取数据,否则FFmpeg会阻塞 - audio = AudioSegment.from_file( - audio_file_path, format=file_type, parameters=["-nostdin"] - ) - - # 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配) - audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2) - - # 音频时长(秒) - duration = len(audio) / 1000.0 - - # 获取原始PCM数据(16位小端) - raw_data = audio.raw_data - - # 初始化Opus编码器 - encoder = opuslib_next.Encoder(16000, 1, opuslib_next.APPLICATION_AUDIO) - - # 编码参数 - frame_duration = 60 # 60ms per frame - frame_size = int(16000 * frame_duration / 1000) # 960 samples/frame - - opus_datas = [] - # 按帧处理所有音频数据(包括最后一帧可能补零) - for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample - # 获取当前帧的二进制数据 - chunk = raw_data[i : i + frame_size * 2] - - # 如果最后一帧不足,补零 - if len(chunk) < frame_size * 2: - chunk += b"\x00" * (frame_size * 2 - len(chunk)) - - # 转换为numpy数组处理 - np_frame = np.frombuffer(chunk, dtype=np.int16) - - # 编码Opus数据 - opus_data = encoder.encode(np_frame.tobytes(), frame_size) - opus_datas.append(opus_data) - - return opus_datas, duration + return audio_to_opus_data(audio_file_path) diff --git a/main/xiaozhi-server/core/utils/util.py b/main/xiaozhi-server/core/utils/util.py index a1956cee..6e5b1028 100644 --- a/main/xiaozhi-server/core/utils/util.py +++ b/main/xiaozhi-server/core/utils/util.py @@ -2,35 +2,40 @@ import json import socket import subprocess import re +import os +import numpy as np import requests +import opuslib_next +from pydub import AudioSegment from typing import Dict, Any from core.utils import tts, llm, intent, memory, vad, asr TAG = __name__ emoji_map = { - 'neutral': '😶', - 'happy': '🙂', - 'laughing': '😆', - 'funny': '😂', - 'sad': '😔', - 'angry': '😠', - 'crying': '😭', - 'loving': '😍', - 'embarrassed': '😳', - 'surprised': '😲', - 'shocked': '😱', - 'thinking': '🤔', - 'winking': '😉', - 'cool': '😎', - 'relaxed': '😌', - 'delicious': '🤤', - 'kissy': '😘', - 'confident': '😏', - 'sleepy': '😴', - 'silly': '😜', - 'confused': '🙄' + "neutral": "😶", + "happy": "🙂", + "laughing": "😆", + "funny": "😂", + "sad": "😔", + "angry": "😠", + "crying": "😭", + "loving": "😍", + "embarrassed": "😳", + "surprised": "😲", + "shocked": "😱", + "thinking": "🤔", + "winking": "😉", + "cool": "😎", + "relaxed": "😌", + "delicious": "🤤", + "kissy": "😘", + "confident": "😏", + "sleepy": "😴", + "silly": "😜", + "confused": "🙄", } + def get_local_ip(): try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -117,9 +122,9 @@ def is_punctuation_or_emoji(char): "、", # 中文顿号 "“", "”", - "\"", # 中文双引号 + 英文引号 + '"', # 中文双引号 + 英文引号 ":", - ":", # 中文冒号 + 英文冒号 + ":", # 中文冒号 + 英文冒号 } if char.isspace() or char in punctuation_set: return True @@ -353,12 +358,13 @@ def initialize_modules( return modules + def analyze_emotion(text): """ 分析文本情感并返回对应的emoji名称(支持中英文) """ if not text or not isinstance(text, str): - return 'neutral' + return "neutral" original_text = text text = text.lower().strip() @@ -369,84 +375,444 @@ def analyze_emotion(text): return emotion # 标点符号分析 - has_exclamation = '!' in original_text or '!' in original_text - has_question = '?' in original_text or '?' in original_text - has_ellipsis = '...' in original_text or '…' in original_text + has_exclamation = "!" in original_text or "!" in original_text + has_question = "?" in original_text or "?" in original_text + has_ellipsis = "..." in original_text or "…" in original_text # 定义情感关键词映射(中英文扩展版) emotion_keywords = { - 'happy': ['开心', '高兴', '快乐', '愉快', '幸福', '满意', '棒', '好', '不错', '完美', '棒极了', '太好了', - '好呀', '好的', 'happy', 'joy', 'great', 'good', 'nice', 'awesome', 'fantastic', 'wonderful'], - 'laughing': ['哈哈', '哈哈哈', '呵呵', '嘿嘿', '嘻嘻', '笑死', '太好笑了', '笑死我了', 'lol', 'lmao', 'haha', - 'hahaha', 'hehe', 'rofl', 'funny', 'laugh'], - 'funny': ['搞笑', '滑稽', '逗', '幽默', '笑点', '段子', '笑话', '太逗了', 'hilarious', 'joke', 'comedy'], - 'sad': ['伤心', '难过', '悲哀', '悲伤', '忧郁', '郁闷', '沮丧', '失望', '想哭', '难受', '不开心', '唉', '呜呜', - 'sad', 'upset', 'unhappy', 'depressed', 'sorrow', 'gloomy'], - 'angry': ['生气', '愤怒', '气死', '讨厌', '烦人', '可恶', '烦死了', '恼火', '暴躁', '火大', '愤怒', '气炸了', - 'angry', 'mad', 'annoyed', 'furious', 'pissed', 'hate'], - 'crying': ['哭泣', '泪流', '大哭', '伤心欲绝', '泪目', '流泪', '哭死', '哭晕', '想哭', '泪崩', - 'cry', 'crying', 'tears', 'sob', 'weep'], - 'loving': ['爱你', '喜欢', '爱', '亲爱的', '宝贝', '么么哒', '抱抱', '想你', '思念', '最爱', '亲亲', '喜欢你', - 'love', 'like', 'adore', 'darling', 'sweetie', 'honey', 'miss you', 'heart'], - 'embarrassed': ['尴尬', '不好意思', '害羞', '脸红', '难为情', '社死', '丢脸', '出丑', - 'embarrassed', 'awkward', 'shy', 'blush'], - 'surprised': ['惊讶', '吃惊', '天啊', '哇塞', '哇', '居然', '竟然', '没想到', '出乎意料', - 'surprise', 'wow', 'omg', 'oh my god', 'amazing', 'unbelievable'], - 'shocked': ['震惊', '吓到', '惊呆了', '不敢相信', '震撼', '吓死', '恐怖', '害怕', '吓人', - 'shocked', 'shocking', 'scared', 'frightened', 'terrified', 'horror'], - 'thinking': ['思考', '考虑', '想一下', '琢磨', '沉思', '冥想', '想', '思考中', '在想', - 'think', 'thinking', 'consider', 'ponder', 'meditate'], - 'winking': ['调皮', '眨眼', '你懂的', '坏笑', '邪恶', '奸笑', '使眼色', - 'wink', 'teasing', 'naughty', 'mischievous'], - 'cool': ['酷', '帅', '厉害', '棒极了', '真棒', '牛逼', '强', '优秀', '杰出', '出色', '完美', - 'cool', 'awesome', 'amazing', 'great', 'impressive', 'perfect'], - 'relaxed': ['放松', '舒服', '惬意', '悠闲', '轻松', '舒适', '安逸', '自在', - 'relax', 'relaxed', 'comfortable', 'cozy', 'chill', 'peaceful'], - 'delicious': ['好吃', '美味', '香', '馋', '可口', '香甜', '大餐', '大快朵颐', '流口水', '垂涎', - 'delicious', 'yummy', 'tasty', 'yum', 'appetizing', 'mouthwatering'], - 'kissy': ['亲亲', '么么', '吻', 'mua', 'muah', '亲一下', '飞吻', - 'kiss', 'xoxo', 'hug', 'muah', 'smooch'], - 'confident': ['自信', '肯定', '确定', '毫无疑问', '当然', '必须的', '毫无疑问', '确信', '坚信', - 'confident', 'sure', 'certain', 'definitely', 'positive'], - 'sleepy': ['困', '睡觉', '晚安', '想睡', '好累', '疲惫', '疲倦', '困了', '想休息', '睡意', - 'sleep', 'sleepy', 'tired', 'exhausted', 'bedtime', 'good night'], - 'silly': ['傻', '笨', '呆', '憨', '蠢', '二', '憨憨', '傻乎乎', '呆萌', - 'silly', 'stupid', 'dumb', 'foolish', 'goofy', 'ridiculous'], - 'confused': ['疑惑', '不明白', '不懂', '困惑', '疑问', '为什么', '怎么回事', '啥意思', '不清楚', - 'confused', 'puzzled', 'doubt', 'question', 'what', 'why', 'how'] + "happy": [ + "开心", + "高兴", + "快乐", + "愉快", + "幸福", + "满意", + "棒", + "好", + "不错", + "完美", + "棒极了", + "太好了", + "好呀", + "好的", + "happy", + "joy", + "great", + "good", + "nice", + "awesome", + "fantastic", + "wonderful", + ], + "laughing": [ + "哈哈", + "哈哈哈", + "呵呵", + "嘿嘿", + "嘻嘻", + "笑死", + "太好笑了", + "笑死我了", + "lol", + "lmao", + "haha", + "hahaha", + "hehe", + "rofl", + "funny", + "laugh", + ], + "funny": [ + "搞笑", + "滑稽", + "逗", + "幽默", + "笑点", + "段子", + "笑话", + "太逗了", + "hilarious", + "joke", + "comedy", + ], + "sad": [ + "伤心", + "难过", + "悲哀", + "悲伤", + "忧郁", + "郁闷", + "沮丧", + "失望", + "想哭", + "难受", + "不开心", + "唉", + "呜呜", + "sad", + "upset", + "unhappy", + "depressed", + "sorrow", + "gloomy", + ], + "angry": [ + "生气", + "愤怒", + "气死", + "讨厌", + "烦人", + "可恶", + "烦死了", + "恼火", + "暴躁", + "火大", + "愤怒", + "气炸了", + "angry", + "mad", + "annoyed", + "furious", + "pissed", + "hate", + ], + "crying": [ + "哭泣", + "泪流", + "大哭", + "伤心欲绝", + "泪目", + "流泪", + "哭死", + "哭晕", + "想哭", + "泪崩", + "cry", + "crying", + "tears", + "sob", + "weep", + ], + "loving": [ + "爱你", + "喜欢", + "爱", + "亲爱的", + "宝贝", + "么么哒", + "抱抱", + "想你", + "思念", + "最爱", + "亲亲", + "喜欢你", + "love", + "like", + "adore", + "darling", + "sweetie", + "honey", + "miss you", + "heart", + ], + "embarrassed": [ + "尴尬", + "不好意思", + "害羞", + "脸红", + "难为情", + "社死", + "丢脸", + "出丑", + "embarrassed", + "awkward", + "shy", + "blush", + ], + "surprised": [ + "惊讶", + "吃惊", + "天啊", + "哇塞", + "哇", + "居然", + "竟然", + "没想到", + "出乎意料", + "surprise", + "wow", + "omg", + "oh my god", + "amazing", + "unbelievable", + ], + "shocked": [ + "震惊", + "吓到", + "惊呆了", + "不敢相信", + "震撼", + "吓死", + "恐怖", + "害怕", + "吓人", + "shocked", + "shocking", + "scared", + "frightened", + "terrified", + "horror", + ], + "thinking": [ + "思考", + "考虑", + "想一下", + "琢磨", + "沉思", + "冥想", + "想", + "思考中", + "在想", + "think", + "thinking", + "consider", + "ponder", + "meditate", + ], + "winking": [ + "调皮", + "眨眼", + "你懂的", + "坏笑", + "邪恶", + "奸笑", + "使眼色", + "wink", + "teasing", + "naughty", + "mischievous", + ], + "cool": [ + "酷", + "帅", + "厉害", + "棒极了", + "真棒", + "牛逼", + "强", + "优秀", + "杰出", + "出色", + "完美", + "cool", + "awesome", + "amazing", + "great", + "impressive", + "perfect", + ], + "relaxed": [ + "放松", + "舒服", + "惬意", + "悠闲", + "轻松", + "舒适", + "安逸", + "自在", + "relax", + "relaxed", + "comfortable", + "cozy", + "chill", + "peaceful", + ], + "delicious": [ + "好吃", + "美味", + "香", + "馋", + "可口", + "香甜", + "大餐", + "大快朵颐", + "流口水", + "垂涎", + "delicious", + "yummy", + "tasty", + "yum", + "appetizing", + "mouthwatering", + ], + "kissy": [ + "亲亲", + "么么", + "吻", + "mua", + "muah", + "亲一下", + "飞吻", + "kiss", + "xoxo", + "hug", + "muah", + "smooch", + ], + "confident": [ + "自信", + "肯定", + "确定", + "毫无疑问", + "当然", + "必须的", + "毫无疑问", + "确信", + "坚信", + "confident", + "sure", + "certain", + "definitely", + "positive", + ], + "sleepy": [ + "困", + "睡觉", + "晚安", + "想睡", + "好累", + "疲惫", + "疲倦", + "困了", + "想休息", + "睡意", + "sleep", + "sleepy", + "tired", + "exhausted", + "bedtime", + "good night", + ], + "silly": [ + "傻", + "笨", + "呆", + "憨", + "蠢", + "二", + "憨憨", + "傻乎乎", + "呆萌", + "silly", + "stupid", + "dumb", + "foolish", + "goofy", + "ridiculous", + ], + "confused": [ + "疑惑", + "不明白", + "不懂", + "困惑", + "疑问", + "为什么", + "怎么回事", + "啥意思", + "不清楚", + "confused", + "puzzled", + "doubt", + "question", + "what", + "why", + "how", + ], } # 特殊句型判断(中英文) # 赞美他人 - if any(phrase in text for phrase in - ['你真', '你好', '您真', '你真棒', '你好厉害', '你太强了', '你真好', '你真聪明', - 'you are', 'you\'re', 'you look', 'you seem', 'so smart', 'so kind']): - return 'loving' + if any( + phrase in text + for phrase in [ + "你真", + "你好", + "您真", + "你真棒", + "你好厉害", + "你太强了", + "你真好", + "你真聪明", + "you are", + "you're", + "you look", + "you seem", + "so smart", + "so kind", + ] + ): + return "loving" # 自我赞美 - if any(phrase in text for phrase in ['我真', '我最', '我太棒了', '我厉害', '我聪明', '我优秀', - 'i am', 'i\'m', 'i feel', 'so good', 'so happy']): - return 'cool' + if any( + phrase in text + for phrase in [ + "我真", + "我最", + "我太棒了", + "我厉害", + "我聪明", + "我优秀", + "i am", + "i'm", + "i feel", + "so good", + "so happy", + ] + ): + return "cool" # 晚安/睡觉相关 - if any(phrase in text for phrase in ['睡觉', '晚安', '睡了', '好梦', '休息了', '去睡了', - 'sleep', 'good night', 'bedtime', 'go to bed']): - return 'sleepy' + if any( + phrase in text + for phrase in [ + "睡觉", + "晚安", + "睡了", + "好梦", + "休息了", + "去睡了", + "sleep", + "good night", + "bedtime", + "go to bed", + ] + ): + return "sleepy" # 疑问句 if has_question and not has_exclamation: - return 'thinking' + return "thinking" # 强烈情感(感叹号) if has_exclamation and not has_question: # 检查是否是积极内容 - positive_words = emotion_keywords['happy'] + emotion_keywords['laughing'] + emotion_keywords['cool'] + positive_words = ( + emotion_keywords["happy"] + + emotion_keywords["laughing"] + + emotion_keywords["cool"] + ) if any(word in text for word in positive_words): - return 'laughing' + return "laughing" # 检查是否是消极内容 - negative_words = emotion_keywords['angry'] + emotion_keywords['sad'] + emotion_keywords['crying'] + negative_words = ( + emotion_keywords["angry"] + + emotion_keywords["sad"] + + emotion_keywords["crying"] + ) if any(word in text for word in negative_words): - return 'angry' - return 'surprised' + return "angry" + return "surprised" # 省略号(表示犹豫或思考) if has_ellipsis: - return 'thinking' + return "thinking" # 关键词匹配(带权重) emotion_scores = {emotion: 0 for emotion in emoji_map.keys()} @@ -466,18 +832,33 @@ def analyze_emotion(text): # 根据分数选择最可能的情感 max_score = max(emotion_scores.values()) if max_score == 0: - return 'happy' # 默认 + return "happy" # 默认 # 可能有多个情感同分,根据上下文选择最合适的 top_emotions = [e for e, s in emotion_scores.items() if s == max_score] # 如果多个情感同分,使用以下优先级 priority_order = [ - 'laughing', 'crying', 'angry', 'surprised', 'shocked', # 强烈情感优先 - 'loving', 'happy', 'funny', 'cool', # 积极情感 - 'sad', 'embarrassed', 'confused', # 消极情感 - 'thinking', 'winking', 'relaxed', # 中性情感 - 'delicious', 'kissy', 'confident', 'sleepy', 'silly' # 特殊场景 + "laughing", + "crying", + "angry", + "surprised", + "shocked", # 强烈情感优先 + "loving", + "happy", + "funny", + "cool", # 积极情感 + "sad", + "embarrassed", + "confused", # 消极情感 + "thinking", + "winking", + "relaxed", # 中性情感 + "delicious", + "kissy", + "confident", + "sleepy", + "silly", # 特殊场景 ] for emotion in priority_order: @@ -485,3 +866,50 @@ def analyze_emotion(text): return emotion return top_emotions[0] # 如果都不在优先级列表里,返回第一个 + + +def audio_to_opus_data(audio_file_path): + """音频文件转换为Opus编码""" + # 获取文件后缀名 + file_type = os.path.splitext(audio_file_path)[1] + if file_type: + file_type = file_type.lstrip(".") + # 读取音频文件,-nostdin 参数:不要从标准输入读取数据,否则FFmpeg会阻塞 + audio = AudioSegment.from_file( + audio_file_path, format=file_type, parameters=["-nostdin"] + ) + + # 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配) + audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2) + + # 音频时长(秒) + duration = len(audio) / 1000.0 + + # 获取原始PCM数据(16位小端) + raw_data = audio.raw_data + + # 初始化Opus编码器 + encoder = opuslib_next.Encoder(16000, 1, opuslib_next.APPLICATION_AUDIO) + + # 编码参数 + frame_duration = 60 # 60ms per frame + frame_size = int(16000 * frame_duration / 1000) # 960 samples/frame + + opus_datas = [] + # 按帧处理所有音频数据(包括最后一帧可能补零) + for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample + # 获取当前帧的二进制数据 + chunk = raw_data[i : i + frame_size * 2] + + # 如果最后一帧不足,补零 + if len(chunk) < frame_size * 2: + chunk += b"\x00" * (frame_size * 2 - len(chunk)) + + # 转换为numpy数组处理 + np_frame = np.frombuffer(chunk, dtype=np.int16) + + # 编码Opus数据 + opus_data = encoder.encode(np_frame.tobytes(), frame_size) + opus_datas.append(opus_data) + + return opus_datas, duration From 05331f001a134ec8e8f50121288e0a669d22bfcc Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Tue, 6 May 2025 15:15:27 +0800 Subject: [PATCH 14/26] =?UTF-8?q?update:=20tts=E8=B6=85=E6=97=B6=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E7=9A=84=E6=96=87=E6=9C=AC=E7=B4=A2=E5=BC=95=E6=B7=B7?= =?UTF-8?q?=E4=B9=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/connection.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index f08a61ff..1fe63912 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -565,7 +565,7 @@ class ConnectionHandler: future = self.executor.submit( self.speak_and_play, segment_text, text_index ) - self.tts_queue.put(future) + self.tts_queue.put((future, text_index)) processed_chars += len(segment_text_raw) # 更新已处理字符位置 # 处理最后剩余的文本 @@ -695,7 +695,7 @@ class ConnectionHandler: future = self.executor.submit( self.speak_and_play, segment_text, text_index ) - self.tts_queue.put(future) + self.tts_queue.put((future, text_index)) # 更新已处理字符位置 processed_chars += len(segment_text_raw) @@ -859,7 +859,10 @@ class ConnectionHandler: text = None try: try: - future = self.tts_queue.get(timeout=1) + item = self.tts_queue.get(timeout=1) + if item is None: + continue + future, text_index = item # 解包获取 Future 和 text_index except queue.Empty: if self.stop_event.is_set(): break @@ -867,11 +870,11 @@ class ConnectionHandler: if future is None: continue text = None - opus_datas, text_index, tts_file = [], 0, None + opus_datas, tts_file = [], None try: self.logger.bind(tag=TAG).debug("正在处理TTS任务...") tts_timeout = int(self.config.get("tts_timeout", 10)) - tts_file, text, text_index = future.result(timeout=tts_timeout) + tts_file, text, _ = future.result(timeout=tts_timeout) if text is None or len(text) <= 0: self.logger.bind(tag=TAG).error( f"TTS出错:{text_index}: tts text is empty" From 12c957d48b3684bea2c9a8f1501870d7951d7b99 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 6 May 2025 17:14:58 +0800 Subject: [PATCH 15/26] =?UTF-8?q?update:=E4=BF=AE=E5=A4=8D=E6=99=BA?= =?UTF-8?q?=E6=8E=A7=E5=8F=B0=E6=A8=A1=E5=BC=8F=E4=B8=8B=EF=BC=8C=E6=89=80?= =?UTF-8?q?=E9=80=89=E6=A8=A1=E5=9D=97=E7=9A=84=E6=97=A5=E5=BF=97=E5=90=8D?= =?UTF-8?q?=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config/logger.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/main/xiaozhi-server/config/logger.py b/main/xiaozhi-server/config/logger.py index 396260f4..03768989 100644 --- a/main/xiaozhi-server/config/logger.py +++ b/main/xiaozhi-server/config/logger.py @@ -8,10 +8,16 @@ SERVER_VERSION = "0.3.14" def get_module_abbreviation(module_name, module_dict): - """获取模块名称的缩写,如果为空则返回00""" - return ( - module_dict.get(module_name, "")[:2] if module_dict.get(module_name) else "00" - ) + """获取模块名称的缩写,如果为空则返回00 + 如果名称中包含下划线,则返回下划线后面的前两个字符 + """ + module_value = module_dict.get(module_name, "") + if not module_value: + return "00" + if "_" in module_value: + parts = module_value.split("_") + return parts[-1][:2] if parts[-1] else "00" + return module_value[:2] def build_module_string(selected_module): From b699886953e26cb07f7b709366b0ea0429e7ea4b Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Tue, 6 May 2025 17:30:39 +0800 Subject: [PATCH 16/26] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E9=85=8D=E7=BD=AE=E6=95=8F=E6=84=9F=E5=AF=86=E9=92=A5?= =?UTF-8?q?=E7=9A=84=E6=98=BE=E7=A4=BA=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/ParamsManagement.vue | 32 ++++++++++++++++--- main/xiaozhi-server/core/connection.py | 1 + 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/main/manager-web/src/views/ParamsManagement.vue b/main/manager-web/src/views/ParamsManagement.vue index 3301bbb5..afb0dc1d 100644 --- a/main/manager-web/src/views/ParamsManagement.vue +++ b/main/manager-web/src/views/ParamsManagement.vue @@ -25,8 +25,19 @@ - + + + @@ -45,6 +45,25 @@ export default { data() { return { switchValue: false } }, + computed: { + formattedLastConnectedTime() { + if (!this.device.lastConnectedAt) return '暂未对话'; + + const lastTime = new Date(this.device.lastConnectedAt); + const now = new Date(); + const diffMinutes = Math.floor((now - lastTime) / (1000 * 60)); + + if (diffMinutes < 60) { + return `${diffMinutes}分钟前`; + } else if (diffMinutes < 24 * 60) { + const hours = Math.floor(diffMinutes / 60); + const minutes = diffMinutes % 60; + return `${hours}小时${minutes > 0 ? minutes + '分钟' : ''}前`; + } else { + return this.device.lastConnectedAt; + } + } + }, methods: { handleDelete() { this.$emit('delete', this.device.agentId) From e6d63a811ea1bca9319fd154ea07b803291e1f08 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Wed, 7 May 2025 23:21:47 +0800 Subject: [PATCH 24/26] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=98=BE=E7=A4=BA=20(#1139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/xiaozhi/common/constant/Constant.java | 2 +- main/manager-web/src/components/DeviceItem.vue | 4 +++- main/xiaozhi-server/config/logger.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index 779fea83..a9b08d5c 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -177,5 +177,5 @@ public interface Constant { /** * 版本号 */ - public static final String VERSION = "0.3.14"; + public static final String VERSION = "0.4.1"; } \ No newline at end of file diff --git a/main/manager-web/src/components/DeviceItem.vue b/main/manager-web/src/components/DeviceItem.vue index c8acfde2..60f54b9d 100644 --- a/main/manager-web/src/components/DeviceItem.vue +++ b/main/manager-web/src/components/DeviceItem.vue @@ -53,7 +53,9 @@ export default { const now = new Date(); const diffMinutes = Math.floor((now - lastTime) / (1000 * 60)); - if (diffMinutes < 60) { + if (diffMinutes <= 1) { + return '刚刚'; + } else if (diffMinutes < 60) { return `${diffMinutes}分钟前`; } else if (diffMinutes < 24 * 60) { const hours = Math.floor(diffMinutes / 60); diff --git a/main/xiaozhi-server/config/logger.py b/main/xiaozhi-server/config/logger.py index 03768989..ce04c31c 100644 --- a/main/xiaozhi-server/config/logger.py +++ b/main/xiaozhi-server/config/logger.py @@ -4,7 +4,7 @@ from loguru import logger from config.config_loader import load_config from config.settings import check_config_file -SERVER_VERSION = "0.3.14" +SERVER_VERSION = "0.4.1" def get_module_abbreviation(module_name, module_dict): From 919c2ffd46640278c334c2effe44160e70c869a8 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Wed, 7 May 2025 23:37:38 +0800 Subject: [PATCH 25/26] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E6=98=BE=E7=A4=BA=20(#1143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update:优化时间显示 * update:优化时间显示 --- .../service/biz/impl/AgentChatHistoryBizServiceImpl.java | 2 +- .../modules/device/service/impl/DeviceServiceImpl.java | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) 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 8b405e49..21e645de 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 @@ -82,7 +82,7 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic // 3. 保存数据 agentChatHistoryService.save(entity); // 4. 更新设备最后对话时间 - redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date(), 60 * 2); + redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date()); return Boolean.TRUE; } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java index 59e329fa..515aea7f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/device/service/impl/DeviceServiceImpl.java @@ -1,7 +1,12 @@ package xiaozhi.modules.device.service.impl; import java.time.Instant; -import java.util.*; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -262,7 +267,7 @@ public class DeviceServiceImpl extends BaseServiceImpl } Date maxDate = deviceDao.getAllLastConnectedAtByAgentId(agentId); if (maxDate != null) { - redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), maxDate, 60 * 2); + redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), maxDate); } return maxDate; } From b246d9e5676fd45b7ca42af278b8bb85a6e9920e Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Thu, 8 May 2025 09:35:46 +0800 Subject: [PATCH 26/26] =?UTF-8?q?fix:selected=5Fmodule=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E4=B8=BA=E7=A9=BA=E7=9A=84bug=20(#1144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update:优化时间显示 * update:优化时间显示 * fix:selected_module可能为空的bug --- main/xiaozhi-server/core/utils/util.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/main/xiaozhi-server/core/utils/util.py b/main/xiaozhi-server/core/utils/util.py index b75a24da..2ed67b19 100644 --- a/main/xiaozhi-server/core/utils/util.py +++ b/main/xiaozhi-server/core/utils/util.py @@ -910,7 +910,10 @@ def audio_to_opus_data(audio_file_path): def check_vad_update(before_config, new_config): - if new_config["selected_module"].get("VAD") is None: + if ( + new_config.get("selected_module") is None + or new_config["selected_module"].get("VAD") is None + ): return False update_vad = False current_vad_module = before_config["selected_module"]["VAD"] @@ -931,7 +934,10 @@ def check_vad_update(before_config, new_config): def check_asr_update(before_config, new_config): - if new_config["selected_module"].get("ASR") is None: + if ( + new_config.get("selected_module") is None + or new_config["selected_module"].get("ASR") is None + ): return False update_asr = False current_asr_module = before_config["selected_module"]["ASR"]