From 04c10ef225e068d5879512c66bf5444767134286 Mon Sep 17 00:00:00 2001 From: DaGou12138 <991623169@qq.com> Date: Wed, 8 Jul 2026 14:12:16 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix=EF=BC=9A=201.=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E8=AF=8D=E4=B8=8E=E8=AF=B4=E8=AF=9D=E4=BA=BA?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=EF=BC=8C=E5=87=8F=E5=B0=91=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E6=AF=8F=E6=AC=A1=E5=9B=9E=E7=AD=94=E9=83=BD=E5=B8=A6=E7=9D=80?= =?UTF-8?q?=E8=AF=B4=E8=AF=9D=E4=BA=BA=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/agent-base-prompt.txt | 2 +- main/xiaozhi-server/core/connection.py | 4 ++-- main/xiaozhi-server/core/utils/dialogue.py | 9 +++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/main/xiaozhi-server/agent-base-prompt.txt b/main/xiaozhi-server/agent-base-prompt.txt index 7b54792c..51bd34a0 100644 --- a/main/xiaozhi-server/agent-base-prompt.txt +++ b/main/xiaozhi-server/agent-base-prompt.txt @@ -65,7 +65,7 @@ Calling unnecessarily is also wrong: User asks ”明天天气” → you call g For the input format `{"speaker":"...", "content":"..."}` (speaker = speaker name, content = text): -1. [Identity known] When `speaker` is a specific name, identity has been recognized. On the first turn you must address them naturally and adjust your response style based on their history. +1. [Identity known] When `speaker` is a specific name, identity has been recognized — remember it and stay consistent about who they are. The only restriction is about OPENING a reply: do not start every reply by greeting or addressing them by name. You may greet/address them by name naturally ONLY ONCE (the first time you see their name); after that, get straight to the point. This does NOT forbid mentioning their name — if the user asks who they are, whether you know them (e.g. "你知道我是谁吗", "还记得我吗"), or otherwise references their identity, you MUST answer truthfully using their recognized name. Always adjust your response style based on their history. 2. [Identity unknown] When `speaker` is `未知说话人`, the system failed to identify the voice. You must NEVER mention the data inside the `speakers_info` tag to the user. Judge from context whether the speaker is the owner or the owner's friend, and keep the conversation natural. diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 8772180c..7dfd2145 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -983,7 +983,7 @@ class ConnectionHandler: llm_responses = self.llm.response_with_functions( self.session_id, self.dialogue.get_llm_dialogue_with_memory( - memory_str, self.config.get("voiceprint", {}) + memory_str, self.config.get("voiceprint", {}), self.current_speaker ), functions=functions, ) @@ -991,7 +991,7 @@ class ConnectionHandler: llm_responses = self.llm.response( self.session_id, self.dialogue.get_llm_dialogue_with_memory( - memory_str, self.config.get("voiceprint", {}) + memory_str, self.config.get("voiceprint", {}), self.current_speaker ), ) except Exception as e: diff --git a/main/xiaozhi-server/core/utils/dialogue.py b/main/xiaozhi-server/core/utils/dialogue.py index 4039f01e..cb0a20f2 100644 --- a/main/xiaozhi-server/core/utils/dialogue.py +++ b/main/xiaozhi-server/core/utils/dialogue.py @@ -92,7 +92,8 @@ class Dialogue: return result def get_llm_dialogue_with_memory( - self, memory_str: str = None, voiceprint_config: dict = None + self, memory_str: str = None, voiceprint_config: dict = None, + current_speaker: str = None, ) -> List[Dict[str, str]]: # 构建对话 dialogue = [] @@ -145,8 +146,12 @@ class Dialogue: # 追加说话人信息 try: speakers = voiceprint_config.get("speakers", []) - if speakers: + current_speaker_name = (current_speaker or "").strip() + if speakers or (current_speaker_name and current_speaker_name != "未知说话人"): dynamic_part += "\n" + # 当前说话人置于块首,确保弱模型也能稳定获取身份 + if current_speaker_name and current_speaker_name != "未知说话人": + dynamic_part += f"\n当前说话人:{current_speaker_name}" for speaker_str in speakers: try: parts = speaker_str.split(",", 2) From aae2ef21523a17818fe12f7aaff955869c5a89b8 Mon Sep 17 00:00:00 2001 From: Tyke Chen <190473011+chentyke@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:52:47 +0800 Subject: [PATCH 2/5] fix: prevent agent IDOR access --- .../agent/controller/AgentController.java | 76 +++++----- .../service/AgentChatHistoryService.java | 16 +++ .../modules/agent/service/AgentService.java | 36 +++++ .../impl/AgentChatHistoryServiceImpl.java | 26 ++++ .../agent/service/impl/AgentServiceImpl.java | 136 +++++++++++++++--- 5 files changed, 234 insertions(+), 56 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java index 5e2435e9..429db8ee 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java @@ -28,6 +28,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.AllArgsConstructor; import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; +import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; @@ -48,15 +50,10 @@ import xiaozhi.modules.agent.service.AgentTagService; import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentChatSummaryService; -import xiaozhi.modules.agent.service.AgentContextProviderService; -import xiaozhi.modules.agent.service.AgentPluginMappingService; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentTemplateService; -import xiaozhi.modules.correctword.service.CorrectWordFileService; import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO; import xiaozhi.modules.agent.vo.AgentInfoVO; -import xiaozhi.modules.device.entity.DeviceEntity; -import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.security.user.SecurityUser; @Tag(name = "智能体管理") @@ -64,17 +61,39 @@ import xiaozhi.modules.security.user.SecurityUser; @RestController @RequestMapping("/agent") public class AgentController { + private static final long AUDIO_PLAY_TOKEN_EXPIRE_SECONDS = 300L; + private final AgentService agentService; private final AgentTemplateService agentTemplateService; - private final DeviceService deviceService; private final AgentChatHistoryService agentChatHistoryService; private final AgentChatAudioService agentChatAudioService; - private final AgentPluginMappingService agentPluginMappingService; - private final AgentContextProviderService agentContextProviderService; private final AgentChatSummaryService agentChatSummaryService; private final RedisUtils redisUtils; private final AgentTagService agentTagService; - private final CorrectWordFileService correctWordFileService; + + private void requireAgentPermission(String agentId) { + if (!agentService.checkAgentPermission(agentId, SecurityUser.getUserId())) { + throw new RenException(ErrorCode.NO_PERMISSION); + } + } + + private String requireSessionAgent(String sessionId) { + String agentId = agentChatHistoryService.getAgentIdBySessionId(sessionId); + if (StringUtils.isBlank(agentId)) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + agentService.getAgentById(agentId); + return agentId; + } + + private String requireAudioPermission(String audioId) { + String agentId = agentChatHistoryService.getAgentIdByAudioId(audioId); + if (StringUtils.isBlank(agentId)) { + throw new RenException(ErrorCode.NO_PERMISSION); + } + requireAgentPermission(agentId); + return agentId; + } @GetMapping("/list") @Operation(summary = "获取用户智能体列表") @@ -106,7 +125,7 @@ public class AgentController { @Operation(summary = "获取智能体详情") @RequiresPermissions("sys:role:normal") public Result getAgentById(@PathVariable("id") String id) { - AgentInfoVO agent = agentService.getAgentById(id); + AgentInfoVO agent = agentService.getAgentById(id, SecurityUser.getUserId()); return ResultUtils.success(agent); } @@ -120,20 +139,16 @@ public class AgentController { @PutMapping("/saveMemory/{macAddress}") @Operation(summary = "根据设备id更新智能体") + @RequiresPermissions("sys:role:normal") public Result updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) { - DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress); - if (device == null) { - return new Result<>(); - } - AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO(); - agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory()); - agentService.updateAgentById(device.getAgentId(), agentUpdateDTO); - return new Result<>(); + agentService.updateAgentMemoryByDeviceMacAddress(macAddress, dto, SecurityUser.getUserId()); + return new Result().ok(null); } @PostMapping("/chat-summary/{sessionId}/save") @Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)") public Result generateAndSaveChatSummary(@PathVariable String sessionId) { + requireSessionAgent(sessionId); try { // 异步执行总结生成任务,立即返回成功响应 new Thread(() -> { @@ -155,6 +170,7 @@ public class AgentController { @PostMapping("/chat-title/{sessionId}/generate") @Operation(summary = "根据会话ID生成聊天标题") public Result generateAndSaveChatTitle(@PathVariable String sessionId) { + requireSessionAgent(sessionId); agentChatSummaryService.generateAndSaveChatTitle(sessionId); return new Result().ok(null); } @@ -163,7 +179,7 @@ public class AgentController { @Operation(summary = "更新智能体") @RequiresPermissions("sys:role:normal") public Result update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) { - agentService.updateAgentById(id, dto); + agentService.updateAgentById(id, dto, SecurityUser.getUserId()); return new Result<>(); } @@ -171,18 +187,7 @@ public class AgentController { @Operation(summary = "删除智能体") @RequiresPermissions("sys:role:normal") public Result delete(@PathVariable String id) { - // 先删除关联的设备 - deviceService.deleteByAgentId(id); - // 删除关联的聊天记录 - agentChatHistoryService.deleteByAgentId(id, true, true); - // 删除关联的插件 - agentPluginMappingService.deleteByAgentId(id); - // 删除关联的上下文源配置 - agentContextProviderService.deleteByAgentId(id); - // 删除关联的替换词文件关联记录 - correctWordFileService.deleteMappingsByAgentId(id); - // 再删除智能体 - agentService.deleteById(id); + agentService.deleteAgentById(id, SecurityUser.getUserId()); return new Result<>(); } @@ -205,6 +210,7 @@ public class AgentController { public Result> getAgentSessions( @PathVariable("id") String id, @Parameter(hidden = true) @RequestParam Map params) { + requireAgentPermission(id); params.put("agentId", id); PageData page = agentChatHistoryService.getSessionListByAgentId(params); return new Result>().ok(page); @@ -252,6 +258,7 @@ public class AgentController { @RequiresPermissions("sys:role:normal") public Result getContentByAudioId( @PathVariable("id") String id) { + requireAudioPermission(id); // 查询聊天记录 String data = agentChatHistoryService.getContentByAudioId(id); return new Result().ok(data); @@ -261,12 +268,13 @@ public class AgentController { @Operation(summary = "获取音频下载ID") @RequiresPermissions("sys:role:normal") public Result getAudioId(@PathVariable("audioId") String audioId) { + requireAudioPermission(audioId); byte[] audioData = agentChatAudioService.getAudio(audioId); if (audioData == null) { return new Result().error("音频不存在"); } String uuid = UUID.randomUUID().toString(); - redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId); + redisUtils.set(RedisKeys.getAgentAudioIdKey(uuid), audioId, AUDIO_PLAY_TOKEN_EXPIRE_SECONDS); return new Result().ok(uuid); } @@ -322,6 +330,7 @@ public class AgentController { @Operation(summary = "获取智能体的标签") @RequiresPermissions("sys:role:normal") public Result> getAgentTags(@PathVariable String id) { + requireAgentPermission(id); List tags = agentTagService.getTagsByAgentId(id); return new Result>().ok(tags); } @@ -330,10 +339,11 @@ public class AgentController { @Operation(summary = "保存智能体的标签") @RequiresPermissions("sys:role:normal") public Result saveAgentTags(@PathVariable String id, @RequestBody Map params) { + requireAgentPermission(id); List tagIds = (List) params.get("tagIds"); List tagNames = (List) params.get("tagNames"); agentTagService.saveAgentTags(id, tagIds, tagNames); return new Result().ok(null); } -} \ No newline at end of file +} 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 index 459b5a5a..48fc0539 100644 --- 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 @@ -37,6 +37,14 @@ public interface AgentChatHistoryService extends IService getChatHistoryBySessionId(String agentId, String sessionId); + /** + * 根据会话ID获取智能体ID + * + * @param sessionId 会话ID + * @return 智能体ID + */ + String getAgentIdBySessionId(String sessionId); + /** * 根据智能体ID删除聊天记录 * @@ -62,6 +70,14 @@ public interface AgentChatHistoryService extends IService { */ AgentInfoVO getAgentById(String id); + /** + * 根据ID获取当前用户有权访问的智能体 + * + * @param id 智能体ID + * @param userId 当前用户ID + * @return 智能体实体 + */ + AgentInfoVO getAgentById(String id, Long userId); + /** * 插入智能体 * @@ -93,6 +103,32 @@ public interface AgentService extends BaseService { */ void updateAgentById(String agentId, AgentUpdateDTO dto); + /** + * 更新当前用户有权访问的智能体 + * + * @param agentId 智能体ID + * @param dto 更新智能体所需的信息 + * @param userId 当前用户ID + */ + void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId); + + /** + * 根据设备MAC地址更新当前用户有权访问的智能体记忆 + * + * @param macAddress 设备MAC地址 + * @param dto 智能体记忆 + * @param userId 当前用户ID + */ + void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId); + + /** + * 删除当前用户有权访问的智能体 + * + * @param agentId 智能体ID + * @param userId 当前用户ID + */ + void deleteAgentById(String agentId, Long userId); + /** * 创建智能体 * 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 index 9cfece89..d2e8e905 100644 --- 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 @@ -88,6 +88,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl() + .select(AgentChatHistoryEntity::getAgentId) + .eq(AgentChatHistoryEntity::getSessionId, sessionId) + .last("LIMIT 1")); + return entity == null ? null : entity.getAgentId(); + } + @Override @Transactional(rollbackFor = Exception.class) public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) { @@ -176,6 +189,19 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl() + .select(AgentChatHistoryEntity::getAgentId) + .eq(AgentChatHistoryEntity::getAudioId, audioId) + .last("LIMIT 1")); + return entity == null ? null : entity.getAgentId(); + } + @Override public boolean isAudioOwnedByAgent(String audioId, String agentId) { // 查询是否有指定音频id和智能体id的数据,如果有且只有一条说明此数据属性此智能体 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 981a2bcf..cdf7cd0a 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 @@ -34,6 +34,7 @@ import xiaozhi.modules.agent.dao.AgentDao; import xiaozhi.modules.agent.dao.AgentTagDao; import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentDTO; +import xiaozhi.modules.agent.dto.AgentMemoryDTO; import xiaozhi.modules.agent.dto.AgentTagDTO; import xiaozhi.modules.agent.dto.AgentUpdateDTO; import xiaozhi.modules.agent.entity.AgentContextProviderEntity; @@ -92,6 +93,7 @@ public class AgentServiceImpl extends BaseServiceImpl imp if (agent == null) { throw new RenException(ErrorCode.AGENT_NOT_FOUND); } + requireCurrentUserPermissionIfPresent(agent); if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) { agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode()); @@ -114,6 +116,65 @@ public class AgentServiceImpl extends BaseServiceImpl imp return agent; } + @Override + public AgentInfoVO getAgentById(String id, Long userId) { + AgentInfoVO agent = getAgentById(id); + requireAgentPermission(agent, userId); + return agent; + } + + private AgentEntity getAgentEntityOrThrow(String agentId) { + AgentEntity agent = agentDao.selectById(agentId); + if (agent == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + return agent; + } + + private boolean isCurrentUserSuperAdmin() { + UserDetail user = SecurityUser.getUser(); + return user != null && Integer.valueOf(SuperAdminEnum.YES.value()).equals(user.getSuperAdmin()); + } + + private void requireCurrentUserPermissionIfPresent(AgentEntity agent) { + Long userId = SecurityUser.getUserId(); + if (userId != null) { + requireAgentPermission(agent, userId); + } + } + + private boolean hasAgentPermission(AgentEntity agent, Long userId) { + if (agent == null) { + return false; + } + if (isCurrentUserSuperAdmin()) { + return true; + } + return userId != null && userId.equals(agent.getUserId()); + } + + private void requireAgentPermission(AgentEntity agent, Long userId) { + if (!hasAgentPermission(agent, userId)) { + throw new RenException(ErrorCode.NO_PERMISSION); + } + } + + private boolean hasDevicePermission(DeviceEntity device, Long userId) { + if (device == null) { + return false; + } + if (isCurrentUserSuperAdmin()) { + return true; + } + return userId != null && userId.equals(device.getUserId()); + } + + private void requireDevicePermission(DeviceEntity device, Long userId) { + if (!hasDevicePermission(device, userId)) { + throw new RenException(ErrorCode.NO_PERMISSION); + } + } + @Override public boolean insert(AgentEntity entity) { // 如果ID为空,自动生成一个UUID作为ID @@ -254,34 +315,28 @@ public class AgentServiceImpl extends BaseServiceImpl imp @Override public boolean checkAgentPermission(String agentId, Long userId) { - if (SecurityUser.getUser() == null || SecurityUser.getUser().getId() == null) { - return false; - } - // 获取智能体信息 - AgentEntity agent = getAgentById(agentId); - if (agent == null) { - return false; - } - - // 如果是超级管理员,直接返回true - if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.YES.value()) { - return true; - } - - // 检查是否是智能体的所有者 - return userId.equals(agent.getUserId()); + AgentEntity agent = agentDao.selectById(agentId); + return hasAgentPermission(agent, userId); } // 根据id更新智能体信息 @Override @Transactional(rollbackFor = Exception.class) public void updateAgentById(String agentId, AgentUpdateDTO dto) { - // 先查询现有实体 - AgentEntity existingEntity = this.getAgentById(agentId); - if (existingEntity == null) { - throw new RenException(ErrorCode.AGENT_NOT_FOUND); - } - + AgentEntity existingEntity = getAgentEntityOrThrow(agentId); + requireCurrentUserPermissionIfPresent(existingEntity); + updateAgentEntity(agentId, dto, existingEntity); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId) { + AgentEntity existingEntity = getAgentEntityOrThrow(agentId); + requireAgentPermission(existingEntity, userId); + updateAgentEntity(agentId, dto, existingEntity); + } + + private void updateAgentEntity(String agentId, AgentUpdateDTO dto, AgentEntity existingEntity) { // 只更新提供的非空字段 if (dto.getAgentName() != null) { existingEntity.setAgentName(dto.getAgentName()); @@ -437,6 +492,41 @@ public class AgentServiceImpl extends BaseServiceImpl imp this.updateById(existingEntity); } + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAgentMemoryByDeviceMacAddress(String macAddress, AgentMemoryDTO dto, Long userId) { + DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress); + if (device == null || StringUtils.isBlank(device.getAgentId()) || dto == null) { + return; + } + + requireDevicePermission(device, userId); + + AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO(); + agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory()); + updateAgentById(device.getAgentId(), agentUpdateDTO, userId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAgentById(String agentId, Long userId) { + AgentEntity agent = getAgentEntityOrThrow(agentId); + requireAgentPermission(agent, userId); + + // 先删除关联的设备 + deviceService.deleteByAgentId(agentId); + // 删除关联的聊天记录 + agentChatHistoryService.deleteByAgentId(agentId, true, true); + // 删除关联的插件 + agentPluginMappingService.deleteByAgentId(agentId); + // 删除关联的上下文源配置 + agentContextProviderService.deleteByAgentId(agentId); + // 删除关联的替换词文件关联记录 + correctWordFileService.deleteMappingsByAgentId(agentId); + // 再删除智能体 + baseDao.deleteById(agentId); + } + /** * 验证大语言模型和意图识别的参数是否符合匹配 * @@ -576,4 +666,4 @@ public class AgentServiceImpl extends BaseServiceImpl imp } } -} \ No newline at end of file +} From 3b6e8f0e4bfff2f842725296b7e752d82944b4f5 Mon Sep 17 00:00:00 2001 From: DaGou12138 <991623169@qq.com> Date: Wed, 8 Jul 2026 16:14:10 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix=EF=BC=9A=201.=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=A3=B0=E7=BA=B9=E7=A7=B0=E5=91=BC=E4=BA=BA=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D=E5=92=8C=E6=B3=A8=E5=85=A5=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E5=87=8F=E5=B0=91=E6=AC=A1=E6=AC=A1=E9=83=BD=E7=A7=B0=E5=91=BC?= =?UTF-8?q?=E7=9A=84=E9=A2=91=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/agent-base-prompt.txt | 2 +- main/xiaozhi-server/core/connection.py | 14 ++++++++++++-- .../core/handle/receiveAudioHandle.py | 13 ++++++++----- main/xiaozhi-server/core/utils/dialogue.py | 10 +++++----- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/main/xiaozhi-server/agent-base-prompt.txt b/main/xiaozhi-server/agent-base-prompt.txt index 51bd34a0..7c6f84b7 100644 --- a/main/xiaozhi-server/agent-base-prompt.txt +++ b/main/xiaozhi-server/agent-base-prompt.txt @@ -65,7 +65,7 @@ Calling unnecessarily is also wrong: User asks ”明天天气” → you call g For the input format `{"speaker":"...", "content":"..."}` (speaker = speaker name, content = text): -1. [Identity known] When `speaker` is a specific name, identity has been recognized — remember it and stay consistent about who they are. The only restriction is about OPENING a reply: do not start every reply by greeting or addressing them by name. You may greet/address them by name naturally ONLY ONCE (the first time you see their name); after that, get straight to the point. This does NOT forbid mentioning their name — if the user asks who they are, whether you know them (e.g. "你知道我是谁吗", "还记得我吗"), or otherwise references their identity, you MUST answer truthfully using their recognized name. Always adjust your response style based on their history. +1. [Identity known] When `speaker` is a specific name, identity has been recognized — remember who they are. Do not start every reply by addressing them by name; get straight to the content (a natural greeting by name once, at the very start of a conversation, is fine). If they ask about their identity (e.g. "你知道我是谁吗", "还记得我吗"), answer truthfully using their recognized name. 2. [Identity unknown] When `speaker` is `未知说话人`, the system failed to identify the voice. You must NEVER mention the data inside the `speakers_info` tag to the user. Judge from context whether the speaker is the owner or the owner's friend, and keep the conversation natural. diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 7dfd2145..6e1d21d9 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -156,6 +156,8 @@ class ConnectionHandler: self.asr_audio = [] self.asr_audio_queue = queue.Queue() self.current_speaker = None # 存储当前说话人 + self.introduced_speakers = set() # 已"首次引入"的说话人,控制只在首轮带名字 + self.system_introduced_speakers = set() # 已在 system 注入过身份的说话人,控制 system 身份只首轮出现 # llm相关变量 self.dialogue = Dialogue() @@ -978,12 +980,20 @@ class ConnectionHandler: ) memory_str = future.result() + # 仅在该说话人首次出现时把身份注入 system,之后靠对话历史首轮保留, + # 避免每轮在 system 重复出现名字诱导模型反复称呼 + speaker_for_system = None + cs = (self.current_speaker or "").strip() + if cs and cs != "未知说话人" and cs not in self.system_introduced_speakers: + self.system_introduced_speakers.add(cs) + speaker_for_system = cs + if self.intent_type == "function_call" and functions is not None: # 使用支持functions的streaming接口 llm_responses = self.llm.response_with_functions( self.session_id, self.dialogue.get_llm_dialogue_with_memory( - memory_str, self.config.get("voiceprint", {}), self.current_speaker + memory_str, self.config.get("voiceprint", {}), speaker_for_system ), functions=functions, ) @@ -991,7 +1001,7 @@ class ConnectionHandler: llm_responses = self.llm.response( self.session_id, self.dialogue.get_llm_dialogue_with_memory( - memory_str, self.config.get("voiceprint", {}), self.current_speaker + memory_str, self.config.get("voiceprint", {}), speaker_for_system ), ) except Exception as e: diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py index ae4673a0..5628c937 100644 --- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py +++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py @@ -39,7 +39,6 @@ async def resume_vad_detection(conn: "ConnectionHandler"): async def startToChat(conn: "ConnectionHandler", text): # 检查输入是否是JSON格式(包含说话人信息) speaker_name = None - language_tag = None actual_text = text try: @@ -48,12 +47,16 @@ async def startToChat(conn: "ConnectionHandler", text): data = json.loads(text) if "speaker" in data and "content" in data: speaker_name = data["speaker"] - language_tag = data["language"] - actual_text = data["content"] + actual_content = data["content"] conn.logger.bind(tag=TAG).info(f"解析到说话人信息: {speaker_name}") - # 直接使用JSON格式的文本,不解析 - actual_text = text + # 仅在该说话人首次出现时保留 {"speaker":...} JSON,让模型自然称呼一次; + # 后续轮降为纯文本,避免每轮重复出现名字诱导模型反复称呼 + if speaker_name not in conn.introduced_speakers: + conn.introduced_speakers.add(speaker_name) + actual_text = text + else: + actual_text = actual_content except (json.JSONDecodeError, KeyError): # 如果解析失败,继续使用原始文本 pass diff --git a/main/xiaozhi-server/core/utils/dialogue.py b/main/xiaozhi-server/core/utils/dialogue.py index cb0a20f2..80b57158 100644 --- a/main/xiaozhi-server/core/utils/dialogue.py +++ b/main/xiaozhi-server/core/utils/dialogue.py @@ -145,13 +145,13 @@ class Dialogue: # 追加说话人信息 try: - speakers = voiceprint_config.get("speakers", []) current_speaker_name = (current_speaker or "").strip() - if speakers or (current_speaker_name and current_speaker_name != "未知说话人"): + # 仅在本轮注入了有效身份时才输出 speakers_info,避免列表里的名字每轮 + # 重复出现诱导模型反复称呼;后续轮不再注入身份,靠对话历史首轮保留 + if current_speaker_name and current_speaker_name != "未知说话人": + speakers = voiceprint_config.get("speakers", []) dynamic_part += "\n" - # 当前说话人置于块首,确保弱模型也能稳定获取身份 - if current_speaker_name and current_speaker_name != "未知说话人": - dynamic_part += f"\n当前说话人:{current_speaker_name}" + dynamic_part += f"\n当前说话人:{current_speaker_name}" for speaker_str in speakers: try: parts = speaker_str.split(",", 2) From 14e57a1addc2ad9cd9fb9a3d1e1ba152236ce3ca Mon Sep 17 00:00:00 2001 From: wengzh <1337326764@qq.com> Date: Fri, 10 Jul 2026 10:06:07 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(powermem):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E9=85=8D=E7=BD=AE=E5=90=AF=E7=94=A8=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=9A=84=E7=B1=BB=E5=9E=8B=E5=88=A4=E6=96=AD=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将配置的启用值统一转为小写字符串后判断是否为true,避免非布尔类型配置导致的判断失效 --- .../core/providers/memory/powermem/powermem.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py index 72d6526f..6c581058 100644 --- a/main/xiaozhi-server/core/providers/memory/powermem/powermem.py +++ b/main/xiaozhi-server/core/providers/memory/powermem/powermem.py @@ -43,11 +43,8 @@ class MemoryProvider(MemoryProviderBase): self.memory_client = None self.enable_user_profile = False self.last_profile_content = "" # Cache for user profile from UserMemory - try: - # Check if user profile mode is enabled - self.enable_user_profile = config.get("enable_user_profile", False) - + self.enable_user_profile = str(config.get("enable_user_profile", False)).lower() == 'true' # Get configuration parameters database_provider = config.get("database_provider", "sqlite") llm_provider = config.get("llm_provider", "qwen") From 2aa6c5ee4926b48a999226760b61c0654c4a5096 Mon Sep 17 00:00:00 2001 From: DaGou12138 <991623169@qq.com> Date: Fri, 10 Jul 2026 15:05:02 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix=EF=BC=9A=201.=E4=BC=98=E5=8C=96context?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E6=96=B9=E5=BC=8F=EF=BC=8C=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E4=B8=BA=E4=B8=80=E4=B8=AAsystem=EF=BC=8C=E8=A7=A3=E5=86=B3qwe?= =?UTF-8?q?n=E6=9C=AC=E5=9C=B0=E9=83=A8=E7=BD=B2=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E5=A4=9Asystem=E5=BC=82=E5=B8=B8400=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/utils/dialogue.py | 49 ++++++++-------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/main/xiaozhi-server/core/utils/dialogue.py b/main/xiaozhi-server/core/utils/dialogue.py index 80b57158..ccc1e2c2 100644 --- a/main/xiaozhi-server/core/utils/dialogue.py +++ b/main/xiaozhi-server/core/utils/dialogue.py @@ -104,42 +104,19 @@ class Dialogue: ) if system_message: - # 以 为分界点,拆分静态 system prompt 和动态上下文 - # 静态部分(规则、身份等)保持不变,可命中前缀缓存 - # 动态部分(时间、天气、记忆等)作为第二条 system 消息,保持 system 权威性 full_prompt = system_message.content - context_match = re.search(r"", full_prompt) - if context_match: - static_part = full_prompt[:context_match.start()] - dynamic_part = full_prompt[context_match.start():] - else: - static_part = full_prompt - dynamic_part = "" - # 第一段:静态 system prompt(前缀缓存可命中) - dialogue.append({"role": "system", "content": static_part}) - - # 第二段:few-shot 示例(会话内不变,也是缓存前缀的一部分) - non_system_messages = [m for m in self.dialogue if m.role != "system"] - fewshot_messages = [m for m in non_system_messages if m.is_temporary] - complete_fewshot = self._ensure_tool_calls_complete(fewshot_messages) - for m in complete_fewshot: - self.getMessages(m, dialogue) - - # 第三段:动态上下文 system prompt(时间、记忆、说话人等) - # 保持 system 角色以确保模型权威性,不降级为 user - if system_message and dynamic_part: # 替换时间占位符 - dynamic_part = dynamic_part.replace( + full_prompt = full_prompt.replace( "{{current_time}}", datetime.now().strftime("%H:%M") ) # 填充记忆 if memory_str is not None: - dynamic_part = re.sub( + full_prompt = re.sub( r".*?", f"\n{memory_str}\n", - dynamic_part, + full_prompt, flags=re.DOTALL, ) @@ -150,8 +127,8 @@ class Dialogue: # 重复出现诱导模型反复称呼;后续轮不再注入身份,靠对话历史首轮保留 if current_speaker_name and current_speaker_name != "未知说话人": speakers = voiceprint_config.get("speakers", []) - dynamic_part += "\n" - dynamic_part += f"\n当前说话人:{current_speaker_name}" + speakers_info = "\n" + speakers_info += f"\n当前说话人:{current_speaker_name}" for speaker_str in speakers: try: parts = speaker_str.split(",", 2) @@ -160,16 +137,24 @@ class Dialogue: description = ( parts[2].strip() if len(parts) >= 3 else "" ) - dynamic_part += f"\n- {name}:{description}" + speakers_info += f"\n- {name}:{description}" except: pass - dynamic_part += "\n" + speakers_info += "\n" + full_prompt += speakers_info except: pass - dialogue.append({"role": "system", "content": dynamic_part}) + dialogue.append({"role": "system", "content": full_prompt}) - # 第四段:实际对话历史(不含 few-shot) + # 第二段:few-shot 示例(会话内不变) + non_system_messages = [m for m in self.dialogue if m.role != "system"] + fewshot_messages = [m for m in non_system_messages if m.is_temporary] + complete_fewshot = self._ensure_tool_calls_complete(fewshot_messages) + for m in complete_fewshot: + self.getMessages(m, dialogue) + + # 第三段:实际对话历史(不含 few-shot) actual_messages = [m for m in non_system_messages if not m.is_temporary] complete_actual = self._ensure_tool_calls_complete(actual_messages) for m in complete_actual: