diff --git a/main/manager-api/pom.xml b/main/manager-api/pom.xml index b3798571..5981f4b2 100644 --- a/main/manager-api/pom.xml +++ b/main/manager-api/pom.xml @@ -32,6 +32,7 @@ 4.20.0 4.1.0 3.4.0 + true @@ -275,7 +276,7 @@ org.apache.maven.plugins maven-surefire-plugin - true + ${skipTests} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/Enums/AgentSnapshotField.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/Enums/AgentSnapshotField.java new file mode 100644 index 00000000..166b76fe --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/Enums/AgentSnapshotField.java @@ -0,0 +1,103 @@ +package xiaozhi.modules.agent.Enums; + +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import lombok.Getter; +import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO; +import xiaozhi.modules.agent.dto.AgentUpdateDTO; +import xiaozhi.modules.agent.entity.AgentEntity; + +@Getter +public enum AgentSnapshotField { + AGENT_CODE("agentCode", AgentSnapshotDataDTO::getAgentCode, AgentUpdateDTO::getAgentCode, + (agent, data) -> agent.setAgentCode(data.getAgentCode())), + AGENT_NAME("agentName", AgentSnapshotDataDTO::getAgentName, AgentUpdateDTO::getAgentName, + (agent, data) -> agent.setAgentName(data.getAgentName())), + ASR_MODEL_ID("asrModelId", AgentSnapshotDataDTO::getAsrModelId, AgentUpdateDTO::getAsrModelId, + (agent, data) -> agent.setAsrModelId(data.getAsrModelId())), + VAD_MODEL_ID("vadModelId", AgentSnapshotDataDTO::getVadModelId, AgentUpdateDTO::getVadModelId, + (agent, data) -> agent.setVadModelId(data.getVadModelId())), + LLM_MODEL_ID("llmModelId", AgentSnapshotDataDTO::getLlmModelId, AgentUpdateDTO::getLlmModelId, + (agent, data) -> agent.setLlmModelId(data.getLlmModelId())), + SLM_MODEL_ID("slmModelId", AgentSnapshotDataDTO::getSlmModelId, AgentUpdateDTO::getSlmModelId, + (agent, data) -> agent.setSlmModelId(data.getSlmModelId())), + VLLM_MODEL_ID("vllmModelId", AgentSnapshotDataDTO::getVllmModelId, AgentUpdateDTO::getVllmModelId, + (agent, data) -> agent.setVllmModelId(data.getVllmModelId())), + TTS_MODEL_ID("ttsModelId", AgentSnapshotDataDTO::getTtsModelId, AgentUpdateDTO::getTtsModelId, + (agent, data) -> agent.setTtsModelId(data.getTtsModelId())), + TTS_VOICE_ID("ttsVoiceId", AgentSnapshotDataDTO::getTtsVoiceId, AgentUpdateDTO::getTtsVoiceId, + (agent, data) -> agent.setTtsVoiceId(data.getTtsVoiceId())), + TTS_LANGUAGE("ttsLanguage", AgentSnapshotDataDTO::getTtsLanguage, AgentUpdateDTO::getTtsLanguage, + (agent, data) -> agent.setTtsLanguage(data.getTtsLanguage())), + TTS_VOLUME("ttsVolume", AgentSnapshotDataDTO::getTtsVolume, AgentUpdateDTO::getTtsVolume, + (agent, data) -> agent.setTtsVolume(data.getTtsVolume())), + TTS_RATE("ttsRate", AgentSnapshotDataDTO::getTtsRate, AgentUpdateDTO::getTtsRate, + (agent, data) -> agent.setTtsRate(data.getTtsRate())), + TTS_PITCH("ttsPitch", AgentSnapshotDataDTO::getTtsPitch, AgentUpdateDTO::getTtsPitch, + (agent, data) -> agent.setTtsPitch(data.getTtsPitch())), + MEM_MODEL_ID("memModelId", AgentSnapshotDataDTO::getMemModelId, AgentUpdateDTO::getMemModelId, + (agent, data) -> agent.setMemModelId(data.getMemModelId())), + INTENT_MODEL_ID("intentModelId", AgentSnapshotDataDTO::getIntentModelId, AgentUpdateDTO::getIntentModelId, + (agent, data) -> agent.setIntentModelId(data.getIntentModelId())), + CHAT_HISTORY_CONF("chatHistoryConf", AgentSnapshotDataDTO::getChatHistoryConf, AgentUpdateDTO::getChatHistoryConf, + (agent, data) -> agent.setChatHistoryConf(data.getChatHistoryConf())), + SYSTEM_PROMPT("systemPrompt", AgentSnapshotDataDTO::getSystemPrompt, AgentUpdateDTO::getSystemPrompt, + (agent, data) -> agent.setSystemPrompt(data.getSystemPrompt())), + SUMMARY_MEMORY("summaryMemory", AgentSnapshotDataDTO::getSummaryMemory, AgentUpdateDTO::getSummaryMemory, + (agent, data) -> agent.setSummaryMemory(data.getSummaryMemory())), + LANG_CODE("langCode", AgentSnapshotDataDTO::getLangCode, AgentUpdateDTO::getLangCode, + (agent, data) -> agent.setLangCode(data.getLangCode())), + LANGUAGE("language", AgentSnapshotDataDTO::getLanguage, AgentUpdateDTO::getLanguage, + (agent, data) -> agent.setLanguage(data.getLanguage())), + SORT("sort", AgentSnapshotDataDTO::getSort, AgentUpdateDTO::getSort, + (agent, data) -> agent.setSort(data.getSort())), + FUNCTIONS("functions", AgentSnapshotDataDTO::getFunctions, AgentUpdateDTO::getFunctions, null), + CONTEXT_PROVIDERS("contextProviders", AgentSnapshotDataDTO::getContextProviders, + AgentUpdateDTO::getContextProviders, null), + CORRECT_WORD_FILE_IDS("correctWordFileIds", AgentSnapshotDataDTO::getCorrectWordFileIds, + AgentUpdateDTO::getCorrectWordFileIds, null), + TAG_NAMES("tagNames", AgentSnapshotDataDTO::getTagNames, AgentUpdateDTO::getTagNames, null); + + private final String fieldName; + private final Function snapshotGetter; + private final Function updateGetter; + private final BiConsumer restoreApplier; + + AgentSnapshotField(String fieldName, Function snapshotGetter, + Function updateGetter, + BiConsumer restoreApplier) { + this.fieldName = fieldName; + this.snapshotGetter = snapshotGetter; + this.updateGetter = updateGetter; + this.restoreApplier = restoreApplier; + } + + public static List names() { + return Arrays.stream(values()).map(AgentSnapshotField::getFieldName).toList(); + } + + public static String canonicalName(String fieldName) { + return "tags".equals(fieldName) ? TAG_NAMES.getFieldName() : fieldName; + } + + public Object snapshotValue(AgentSnapshotDataDTO data) { + return data == null ? null : snapshotGetter.apply(data); + } + + public Object updateValue(AgentUpdateDTO data) { + return data == null ? null : updateGetter.apply(data); + } + + public boolean isRestorableAgentField() { + return restoreApplier != null; + } + + public void applyTo(AgentEntity agent, AgentSnapshotDataDTO data) { + if (restoreApplier != null) { + restoreApplier.accept(agent, data); + } + } +} 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 d41004fc..eb56be23 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 @@ -48,11 +48,8 @@ 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; @@ -69,12 +66,9 @@ public class AgentController { 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; @GetMapping("/list") @Operation(summary = "获取用户智能体列表") @@ -171,18 +165,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.deleteAgent(id); return new Result<>(); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentSnapshotController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentSnapshotController.java index fff4d81f..255e606d 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentSnapshotController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentSnapshotController.java @@ -1,13 +1,12 @@ package xiaozhi.modules.agent.controller; -import java.util.Map; - import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; @@ -17,6 +16,7 @@ import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; import xiaozhi.common.user.UserDetail; import xiaozhi.common.utils.Result; +import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO; import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentSnapshotService; import xiaozhi.modules.agent.vo.AgentSnapshotVO; @@ -35,7 +35,7 @@ public class AgentSnapshotController { @RequiresPermissions("sys:role:normal") public Result> page( @PathVariable String agentId, - @RequestParam Map params) { + @ParameterObject AgentSnapshotPageDTO params) { checkPermission(agentId); return new Result>().ok(agentSnapshotService.page(agentId, params)); } @@ -57,6 +57,15 @@ public class AgentSnapshotController { return new Result<>(); } + @DeleteMapping("/{snapshotId}") + @Operation(summary = "删除智能体历史快照") + @RequiresPermissions("sys:role:normal") + public Result deleteSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) { + checkPermission(agentId); + agentSnapshotService.deleteSnapshot(agentId, snapshotId); + return new Result<>(); + } + private void checkPermission(String agentId) { UserDetail user = SecurityUser.getUser(); if (user == null || !agentService.checkAgentPermission(agentId, user.getId())) { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java index 03e569cd..4def2b67 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentDao.java @@ -36,4 +36,11 @@ public interface AgentDao extends BaseDao { * @param agentId 智能体ID */ AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId); + + /** + * 锁定智能体主记录,用于串行化同一智能体的配置写入 + * + * @param agentId 智能体ID + */ + AgentEntity selectByIdForUpdate(@Param("agentId") String agentId); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentSnapshotDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentSnapshotDao.java index 9c6c54f7..955c13dd 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentSnapshotDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentSnapshotDao.java @@ -11,4 +11,6 @@ public interface AgentSnapshotDao extends BaseDao { Integer selectMaxVersionNo(@Param("agentId") String agentId); AgentSnapshotEntity selectNextSnapshot(@Param("agentId") String agentId, @Param("versionNo") Integer versionNo); + + int deleteOlderThanKeepLimit(@Param("agentId") String agentId, @Param("keepLimit") int keepLimit); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentSnapshotPageDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentSnapshotPageDTO.java new file mode 100644 index 00000000..3f3f5d65 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentSnapshotPageDTO.java @@ -0,0 +1,28 @@ +package xiaozhi.modules.agent.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "智能体快照分页查询参数") +public class AgentSnapshotPageDTO { + @Schema(description = "当前页码,从1开始", example = "1") + private Integer page = 1; + + @Schema(description = "每页数量", example = "10") + private Integer limit = 10; + + @Schema(description = "版本锚点,只查询小于等于该版本号的历史快照", example = "20") + private Integer maxVersionNo; + + public int pageOrDefault() { + return page == null || page < 1 ? 1 : page; + } + + public int limitOrDefault() { + if (limit == null || limit < 1) { + return 10; + } + return limit; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentContextProviderEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentContextProviderEntity.java index 937556ef..4269b70f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentContextProviderEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentContextProviderEntity.java @@ -7,11 +7,11 @@ import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import xiaozhi.modules.agent.dto.ContextProviderDTO; +import xiaozhi.modules.agent.typehandler.ContextProviderListTypeHandler; @Data @TableName(value = "ai_agent_context_provider", autoResultMap = true) @@ -26,7 +26,7 @@ public class AgentContextProviderEntity { private String agentId; @Schema(description = "上下文源配置") - @TableField(typeHandler = JacksonTypeHandler.class) + @TableField(typeHandler = ContextProviderListTypeHandler.class) private List contextProviders; @Schema(description = "创建者") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentSnapshotEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentSnapshotEntity.java index 2783bcc7..0d99fd36 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentSnapshotEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentSnapshotEntity.java @@ -36,6 +36,12 @@ public class AgentSnapshotEntity { @Schema(description = "快照来源") private String source; + @Schema(description = "恢复来源快照ID") + private String restoreFromSnapshotId; + + @Schema(description = "恢复来源版本号") + private Integer restoreFromVersionNo; + @Schema(description = "创建者") private Long creator; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java index c985ae59..709f36ad 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java @@ -50,6 +50,13 @@ public interface AgentService extends BaseService { */ void deleteAgentByUserId(Long userId); + /** + * 删除智能体及其关联数据 + * + * @param agentId 智能体ID + */ + void deleteAgent(String agentId); + /** * 获取用户智能体列表 * diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentSnapshotService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentSnapshotService.java index 511e420a..fe1d8e6e 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentSnapshotService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentSnapshotService.java @@ -1,9 +1,8 @@ package xiaozhi.modules.agent.service; -import java.util.Map; - import xiaozhi.common.page.PageData; import xiaozhi.common.service.BaseService; +import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO; import xiaozhi.modules.agent.dto.AgentUpdateDTO; import xiaozhi.modules.agent.entity.AgentSnapshotEntity; import xiaozhi.modules.agent.vo.AgentSnapshotVO; @@ -11,9 +10,15 @@ import xiaozhi.modules.agent.vo.AgentSnapshotVO; public interface AgentSnapshotService extends BaseService { void createSnapshot(String agentId, String source, AgentUpdateDTO pendingUpdate); - PageData page(String agentId, Map params); + PageData page(String agentId, AgentSnapshotPageDTO params); AgentSnapshotVO getSnapshot(String agentId, String snapshotId); void restoreSnapshot(String agentId, String snapshotId); + + void deleteSnapshot(String agentId, String snapshotId); + + Integer getCurrentVersionNo(String agentId); + + void deleteByAgentId(String agentId); } 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 02d7a76c..75382ea7 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 @@ -15,7 +15,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.AllArgsConstructor; @@ -108,12 +107,13 @@ public class AgentServiceImpl extends BaseServiceImpl imp agent.setContextProviders(contextProviderEntity.getContextProviders()); } - // 查询替换词文件ID列表 - List correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id); - agent.setCorrectWordFileIds(correctWordFileIds); - - // 无需额外查询插件列表,已通过SQL查询出来 - return agent; + // 查询替换词文件ID列表 + List correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id); + agent.setCorrectWordFileIds(correctWordFileIds); + agent.setCurrentVersionNo(agentSnapshotService.getCurrentVersionNo(id)); + + // 无需额外查询插件列表,已通过SQL查询出来 + return agent; } @Override @@ -136,15 +136,35 @@ public class AgentServiceImpl extends BaseServiceImpl imp return super.insert(entity); } - @Override - public void deleteAgentByUserId(Long userId) { - UpdateWrapper wrapper = new UpdateWrapper<>(); - wrapper.eq("user_id", userId); - baseDao.delete(wrapper); - } - - @Override - public List getUserAgents(Long userId, String keyword, String searchType) { + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAgentByUserId(Long userId) { + List agents = baseDao.selectList(new QueryWrapper() + .select("id") + .eq("user_id", userId)); + for (AgentEntity agent : agents) { + deleteAgent(agent.getId()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAgent(String agentId) { + if (agentDao.selectByIdForUpdate(agentId) == null) { + return; + } + deviceService.deleteByAgentId(agentId); + agentChatHistoryService.deleteByAgentId(agentId, true, true); + agentPluginMappingService.deleteByAgentId(agentId); + agentContextProviderService.deleteByAgentId(agentId); + correctWordFileService.deleteMappingsByAgentId(agentId); + agentTagService.deleteAgentTags(agentId); + agentSnapshotService.deleteByAgentId(agentId); + deleteById(agentId); + } + + @Override + public List getUserAgents(Long userId, String keyword, String searchType) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("user_id", userId).orderByDesc("created_at"); @@ -285,12 +305,13 @@ public class AgentServiceImpl extends BaseServiceImpl imp @Override @Transactional(rollbackFor = Exception.class) public void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot) { - // 先查询现有实体 - AgentEntity existingEntity = this.getAgentById(agentId); - if (existingEntity == null) { + if (agentDao.selectByIdForUpdate(agentId) == null) { throw new RenException(ErrorCode.AGENT_NOT_FOUND); } + // 锁定后查询现有实体和关联配置 + AgentEntity existingEntity = this.getAgentById(agentId); + if (createSnapshot) { agentSnapshotService.createSnapshot(agentId, "config", dto); } @@ -448,7 +469,7 @@ public class AgentServiceImpl extends BaseServiceImpl imp agentTagService.saveAgentTags(agentId, dto.getTagIds(), dto.getTagNames()); } - boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId()); + boolean b = validateLLMIntentParams(existingEntity.getLlmModelId(), existingEntity.getIntentModelId()); if (!b) { throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentSnapshotServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentSnapshotServiceImpl.java index 8cbf7cb5..28c9320a 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentSnapshotServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentSnapshotServiceImpl.java @@ -7,15 +7,19 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.TreeMap; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; -import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.fasterxml.jackson.core.type.TypeReference; import lombok.AllArgsConstructor; @@ -30,6 +34,7 @@ import xiaozhi.modules.agent.dao.AgentSnapshotDao; import xiaozhi.modules.agent.dao.AgentTagDao; import xiaozhi.modules.agent.dao.AgentTagRelationDao; import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO; +import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO; import xiaozhi.modules.agent.dto.AgentSnapshotTagDTO; import xiaozhi.modules.agent.dto.AgentUpdateDTO; import xiaozhi.modules.agent.entity.AgentContextProviderEntity; @@ -38,6 +43,7 @@ import xiaozhi.modules.agent.entity.AgentPluginMapping; import xiaozhi.modules.agent.entity.AgentSnapshotEntity; import xiaozhi.modules.agent.entity.AgentTagEntity; import xiaozhi.modules.agent.entity.AgentTagRelationEntity; +import xiaozhi.modules.agent.Enums.AgentSnapshotField; import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentContextProviderService; import xiaozhi.modules.agent.service.AgentPluginMappingService; @@ -54,10 +60,14 @@ import xiaozhi.modules.security.user.SecurityUser; @AllArgsConstructor public class AgentSnapshotServiceImpl extends BaseServiceImpl implements AgentSnapshotService { + private static final int MAX_SNAPSHOTS_PER_AGENT = 100; private static final TypeReference> STRING_LIST_TYPE = new TypeReference<>() { }; private static final TypeReference> PARAM_INFO_TYPE = new TypeReference<>() { }; + private static final TypeReference> OBJECT_MAP_TYPE = new TypeReference<>() { + }; + private static final String SECRET_PLACEHOLDER = "__SNAPSHOT_SECRET_REDACTED__"; private final AgentSnapshotDao agentSnapshotDao; private final AgentDao agentDao; @@ -73,33 +83,27 @@ public class AgentSnapshotServiceImpl extends BaseServiceImpl changedFields = getChangedFields(snapshotData, pendingUpdate); if (pendingUpdate != null && changedFields.isEmpty()) { return; } - AgentInfoVO agent = agentDao.selectAgentInfoById(agentId); - if (agent == null) { - throw new RenException(ErrorCode.AGENT_NOT_FOUND); - } - - AgentSnapshotEntity entity = new AgentSnapshotEntity(); - entity.setAgentId(agentId); - entity.setUserId(agent.getUserId()); - entity.setSnapshotData(JsonUtils.toJsonString(snapshotData)); - entity.setChangedFields(JsonUtils.toJsonString(changedFields)); - entity.setSource(StringUtils.defaultIfBlank(source, "config")); - entity.setCreator(SecurityUser.getUserId()); - entity.setCreatedAt(new Date()); - insertSnapshotWithRetry(entity); + insertSnapshot(agentId, agent.getUserId(), source, snapshotData, changedFields); } @Override - public PageData page(String agentId, Map params) { - IPage page = getPage(params, "created_at", false); + @Transactional(rollbackFor = Exception.class) + public PageData page(String agentId, AgentSnapshotPageDTO params) { + ensureInitialSnapshot(agentId); + AgentSnapshotPageDTO pageParams = params == null ? new AgentSnapshotPageDTO() : params; + Page page = new Page<>(pageParams.pageOrDefault(), pageParams.limitOrDefault()); + page.addOrder(OrderItem.desc("version_no")); IPage result = agentSnapshotDao.selectPage(page, - new QueryWrapper().eq("agent_id", agentId)); + new QueryWrapper() + .eq("agent_id", agentId) + .le(pageParams.getMaxVersionNo() != null, "version_no", pageParams.getMaxVersionNo())); List list = result.getRecords().stream() .map(entity -> toVO(entity, false)) .toList(); @@ -115,44 +119,109 @@ public class AgentSnapshotServiceImpl extends BaseServiceImpl changedFields = getChangedFields(currentData, restoreData); + insertSnapshot(agentId, currentAgent.getUserId(), "restore", currentData, changedFields, + snapshot.getId(), snapshot.getVersionNo()); - AgentEntity agent = agentDao.selectById(agentId); - if (agent == null) { - throw new RenException(ErrorCode.AGENT_NOT_FOUND); - } - applyAgentFields(agent, data); + applyAgentFields(agent, restoreData); validateRestoreParams(agent); applyMemoryPolicy(agent); agent.setUpdater(SecurityUser.getUserId()); agent.setUpdatedAt(new Date()); agentDao.updateById(agent); - restoreFunctions(agentId, data.getFunctions()); - restoreContextProviders(agentId, data.getContextProviders()); - correctWordFileService.saveAgentCorrectWords(agentId, nullToEmpty(data.getCorrectWordFileIds())); - restoreTags(agentId, data); + restoreFunctions(agentId, restoreData.getFunctions()); + restoreContextProviders(agentId, restoreData.getContextProviders()); + correctWordFileService.saveAgentCorrectWords(agentId, nullToEmpty(restoreData.getCorrectWordFileIds())); + restoreTags(agentId, restoreData); } - private void insertSnapshotWithRetry(AgentSnapshotEntity entity) { - for (int attempt = 0; attempt < 3; attempt++) { - Integer maxVersionNo = agentSnapshotDao.selectMaxVersionNo(entity.getAgentId()); - entity.setVersionNo((maxVersionNo == null ? 0 : maxVersionNo) + 1); - try { - agentSnapshotDao.insert(entity); - return; - } catch (DuplicateKeyException e) { - if (attempt == 2) { - throw e; - } - } + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteSnapshot(String agentId, String snapshotId) { + AgentSnapshotEntity entity = getSnapshotEntity(agentId, snapshotId); + Integer maxVersionNo = agentSnapshotDao.selectMaxVersionNo(agentId); + if (Objects.equals(entity.getVersionNo(), maxVersionNo)) { + throw new RenException("最新历史版本不能删除"); } + deleteById(snapshotId); + } + + @Override + public Integer getCurrentVersionNo(String agentId) { + Integer maxVersionNo = agentSnapshotDao.selectMaxVersionNo(agentId); + return (maxVersionNo == null ? 0 : maxVersionNo) + 1; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteByAgentId(String agentId) { + agentSnapshotDao.delete(new QueryWrapper().eq("agent_id", agentId)); + } + + private void insertSnapshot(String agentId, Long userId, String source, AgentSnapshotDataDTO snapshotData, + List changedFields) { + insertSnapshot(agentId, userId, source, snapshotData, changedFields, null, null); + } + + private void insertSnapshot(String agentId, Long userId, String source, AgentSnapshotDataDTO snapshotData, + List changedFields, String restoreFromSnapshotId, Integer restoreFromVersionNo) { + AgentSnapshotEntity entity = new AgentSnapshotEntity(); + entity.setAgentId(agentId); + entity.setUserId(userId); + entity.setVersionNo(allocateVersionNo(agentId)); + entity.setSnapshotData(JsonUtils.toJsonString(redactSnapshotData(snapshotData))); + entity.setChangedFields(JsonUtils.toJsonString(changedFields)); + entity.setSource(StringUtils.defaultIfBlank(source, "config")); + entity.setRestoreFromSnapshotId(restoreFromSnapshotId); + entity.setRestoreFromVersionNo(restoreFromVersionNo); + entity.setCreator(SecurityUser.getUserId()); + entity.setCreatedAt(new Date()); + agentSnapshotDao.insert(entity); + pruneSnapshots(agentId); + } + + private void ensureInitialSnapshot(String agentId) { + Integer maxVersionNo = agentSnapshotDao.selectMaxVersionNo(agentId); + if (maxVersionNo != null && maxVersionNo > 0) { + return; + } + AgentEntity agent = agentDao.selectByIdForUpdate(agentId); + if (agent == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + maxVersionNo = agentSnapshotDao.selectMaxVersionNo(agentId); + if (maxVersionNo != null && maxVersionNo > 0) { + return; + } + AgentInfoVO agentInfo = getAgentInfo(agentId); + insertSnapshot(agentId, agentInfo.getUserId(), "initial", buildSnapshotData(agentInfo), List.of("initial")); + } + + private void pruneSnapshots(String agentId) { + agentSnapshotDao.deleteOlderThanKeepLimit(agentId, MAX_SNAPSHOTS_PER_AGENT); + } + + private Integer allocateVersionNo(String agentId) { + Integer maxVersionNo = agentSnapshotDao.selectMaxVersionNo(agentId); + if (maxVersionNo == null || maxVersionNo < 0) { + throw new RenException("快照版本号生成失败"); + } + return maxVersionNo + 1; } private AgentSnapshotEntity getSnapshotEntity(String agentId, String snapshotId) { @@ -163,12 +232,19 @@ public class AgentSnapshotServiceImpl extends BaseServiceImpl tags = getSnapshotTags(agentId); + data.setContextProviders(getContextProviders(agent.getId())); + data.setCorrectWordFileIds(nullToEmpty(correctWordFileService.getAgentCorrectWordFileIds(agent.getId()))); + List tags = getSnapshotTags(agent.getId()); data.setTags(tags); data.setTagNames(tags.stream().map(AgentSnapshotTagDTO::getTagName).toList()); return data; @@ -250,64 +326,124 @@ public class AgentSnapshotServiceImpl extends BaseServiceImpl fields = new ArrayList<>(); - compare(fields, "agentCode", current.getAgentCode(), pendingUpdate.getAgentCode()); - compare(fields, "agentName", current.getAgentName(), pendingUpdate.getAgentName()); - compare(fields, "asrModelId", current.getAsrModelId(), pendingUpdate.getAsrModelId()); - compare(fields, "vadModelId", current.getVadModelId(), pendingUpdate.getVadModelId()); - compare(fields, "llmModelId", current.getLlmModelId(), pendingUpdate.getLlmModelId()); - compare(fields, "slmModelId", current.getSlmModelId(), pendingUpdate.getSlmModelId()); - compare(fields, "vllmModelId", current.getVllmModelId(), pendingUpdate.getVllmModelId()); - compare(fields, "ttsModelId", current.getTtsModelId(), pendingUpdate.getTtsModelId()); - compare(fields, "ttsVoiceId", current.getTtsVoiceId(), pendingUpdate.getTtsVoiceId()); - compare(fields, "ttsLanguage", current.getTtsLanguage(), pendingUpdate.getTtsLanguage()); - compare(fields, "ttsVolume", current.getTtsVolume(), pendingUpdate.getTtsVolume()); - compare(fields, "ttsRate", current.getTtsRate(), pendingUpdate.getTtsRate()); - compare(fields, "ttsPitch", current.getTtsPitch(), pendingUpdate.getTtsPitch()); - compare(fields, "memModelId", current.getMemModelId(), pendingUpdate.getMemModelId()); - compare(fields, "intentModelId", current.getIntentModelId(), pendingUpdate.getIntentModelId()); - compare(fields, "chatHistoryConf", current.getChatHistoryConf(), pendingUpdate.getChatHistoryConf()); - compare(fields, "systemPrompt", current.getSystemPrompt(), pendingUpdate.getSystemPrompt()); - compare(fields, "summaryMemory", current.getSummaryMemory(), pendingUpdate.getSummaryMemory()); - compare(fields, "langCode", current.getLangCode(), pendingUpdate.getLangCode()); - compare(fields, "language", current.getLanguage(), pendingUpdate.getLanguage()); - compare(fields, "sort", current.getSort(), pendingUpdate.getSort()); - compare(fields, "functions", current.getFunctions(), pendingUpdate.getFunctions()); - compare(fields, "contextProviders", current.getContextProviders(), pendingUpdate.getContextProviders()); - compare(fields, "correctWordFileIds", current.getCorrectWordFileIds(), pendingUpdate.getCorrectWordFileIds()); - compare(fields, "tagNames", current.getTagNames(), pendingUpdate.getTagNames()); - compare(fields, "tagIds", current.getTags().stream().map(AgentSnapshotTagDTO::getId).toList(), - pendingUpdate.getTagIds()); + for (AgentSnapshotField field : AgentSnapshotField.values()) { + Object nextValue = field.updateValue(pendingUpdate); + if (nextValue != null && isChanged(field, field.snapshotValue(current), nextValue)) { + fields.add(field.getFieldName()); + } + } return fields; } - private void compare(List fields, String name, T current, T next) { - if (next != null && !Objects.equals(current, next)) { - fields.add(name); + private List getChangedFields(AgentSnapshotDataDTO current, AgentSnapshotDataDTO next) { + if (next == null) { + return List.of("restore"); } + if (current == null) { + current = new AgentSnapshotDataDTO(); + } + + List fields = new ArrayList<>(); + for (AgentSnapshotField field : AgentSnapshotField.values()) { + if (isChanged(field, field.snapshotValue(current), field.snapshotValue(next))) { + fields.add(field.getFieldName()); + } + } + return fields; + } + + private boolean isChanged(AgentSnapshotField field, Object current, Object next) { + return !Objects.equals(normalizeForCompare(field, current), normalizeForCompare(field, next)); + } + + @SuppressWarnings("unchecked") + private Object normalizeForCompare(AgentSnapshotField field, Object value) { + return switch (field) { + case FUNCTIONS -> normalizeFunctions((List) value); + case CONTEXT_PROVIDERS -> normalizeSortedJsonList((List) value); + case CORRECT_WORD_FILE_IDS -> normalizeStringList((List) value); + case TAG_NAMES -> normalizeStringList((List) value); + default -> value; + }; + } + + private Map normalizeFunctions(List functions) { + Map result = new TreeMap<>(); + if (functions == null) { + return result; + } + for (AgentUpdateDTO.FunctionInfo function : functions) { + if (function == null || StringUtils.isBlank(function.getPluginId())) { + continue; + } + result.put(function.getPluginId(), normalizeMap(function.getParamInfo())); + } + return result; + } + + private List normalizeStringList(List values) { + if (values == null) { + return Collections.emptyList(); + } + return values.stream() + .filter(StringUtils::isNotBlank) + .sorted() + .toList(); + } + + private List normalizeSortedJsonList(List values) { + if (values == null) { + return Collections.emptyList(); + } + return values.stream() + .filter(Objects::nonNull) + .map(value -> JsonUtils.toJsonString(normalizeValue(value))) + .sorted() + .toList(); + } + + private Object normalizeValue(Object value) { + if (value == null) { + return null; + } + if (value instanceof Map map) { + return normalizeMap(map); + } + if (value instanceof List list) { + return list.stream().map(this::normalizeValue).toList(); + } + if (isScalarValue(value)) { + return value; + } + return normalizeMap(JsonUtils.parseObject(JsonUtils.toJsonString(value), OBJECT_MAP_TYPE)); + } + + private boolean isScalarValue(Object value) { + return value instanceof CharSequence + || value instanceof Number + || value instanceof Boolean + || value instanceof Character + || value instanceof Enum + || value instanceof Date; + } + + private Map normalizeMap(Map map) { + Map result = new TreeMap<>(); + if (map == null) { + return result; + } + map.forEach((key, value) -> { + if (key != null) { + result.put(String.valueOf(key), normalizeValue(value)); + } + }); + return result; } private void applyAgentFields(AgentEntity agent, AgentSnapshotDataDTO data) { - agent.setAgentCode(data.getAgentCode()); - agent.setAgentName(data.getAgentName()); - agent.setAsrModelId(data.getAsrModelId()); - agent.setVadModelId(data.getVadModelId()); - agent.setLlmModelId(data.getLlmModelId()); - agent.setSlmModelId(data.getSlmModelId()); - agent.setVllmModelId(data.getVllmModelId()); - agent.setTtsModelId(data.getTtsModelId()); - agent.setTtsVoiceId(data.getTtsVoiceId()); - agent.setTtsLanguage(data.getTtsLanguage()); - agent.setTtsVolume(data.getTtsVolume()); - agent.setTtsRate(data.getTtsRate()); - agent.setTtsPitch(data.getTtsPitch()); - agent.setMemModelId(data.getMemModelId()); - agent.setIntentModelId(data.getIntentModelId()); - agent.setChatHistoryConf(data.getChatHistoryConf()); - agent.setSystemPrompt(data.getSystemPrompt()); - agent.setSummaryMemory(data.getSummaryMemory()); - agent.setLangCode(data.getLangCode()); - agent.setLanguage(data.getLanguage()); - agent.setSort(data.getSort()); + for (AgentSnapshotField field : AgentSnapshotField.values()) { + field.applyTo(agent, data); + } } private void validateRestoreParams(AgentEntity agent) { @@ -406,26 +542,22 @@ public class AgentSnapshotServiceImpl extends BaseServiceImpl() .eq("tag_name", snapshotTag.getTagName()) + .eq("deleted", 0) .last("LIMIT 1")); } if (tag != null) { - if (Objects.equals(tag.getDeleted(), 1)) { - tag.setDeleted(0); - tag.setUpdater(SecurityUser.getUserId()); - tag.setUpdatedAt(now); - agentTagDao.updateById(tag); - } return tag.getId(); } AgentTagEntity newTag = new AgentTagEntity(); - if (StringUtils.isNotBlank(snapshotTag.getId())) { - newTag.setId(snapshotTag.getId()); - } + newTag.setId(UUID.randomUUID().toString().replace("-", "")); newTag.setTagName(snapshotTag.getTagName()); newTag.setSort(snapshotTag.getSort() == null ? 0 : snapshotTag.getSort()); newTag.setDeleted(0); @@ -444,12 +576,16 @@ public class AgentSnapshotServiceImpl extends BaseServiceImpl parseChangedFields(String changedFields) { @@ -469,6 +605,158 @@ public class AgentSnapshotServiceImpl extends BaseServiceImpl { + if (function != null) { + function.setParamInfo(redactSensitiveMap(function.getParamInfo())); + } + }); + } + if (copy.getContextProviders() != null) { + copy.getContextProviders().forEach(provider -> { + if (provider != null) { + provider.setHeaders(redactSensitiveMap(provider.getHeaders())); + } + }); + } + return copy; + } + + private AgentSnapshotDataDTO preserveCurrentSensitiveValues(AgentSnapshotDataDTO target, AgentSnapshotDataDTO current) { + if (target == null) { + return null; + } + AgentSnapshotDataDTO copy = JsonUtils.parseObject(JsonUtils.toJsonString(target), AgentSnapshotDataDTO.class); + Map currentFunctions = nullToEmpty(current == null ? null : current.getFunctions()) + .stream() + .filter(function -> function != null && StringUtils.isNotBlank(function.getPluginId())) + .collect(Collectors.toMap(AgentUpdateDTO.FunctionInfo::getPluginId, Function.identity(), + (left, right) -> left)); + if (copy.getFunctions() != null) { + copy.getFunctions().forEach(function -> { + if (function == null) { + return; + } + AgentUpdateDTO.FunctionInfo currentFunction = currentFunctions.get(function.getPluginId()); + function.setParamInfo(preserveCurrentSensitiveMap(function.getParamInfo(), + currentFunction == null ? null : currentFunction.getParamInfo())); + }); + } + + Map currentProviders = nullToEmpty( + current == null ? null : current.getContextProviders()) + .stream() + .filter(provider -> provider != null && StringUtils.isNotBlank(provider.getUrl())) + .collect(Collectors.toMap(xiaozhi.modules.agent.dto.ContextProviderDTO::getUrl, Function.identity(), + (left, right) -> left)); + if (copy.getContextProviders() != null) { + copy.getContextProviders().forEach(provider -> { + if (provider == null) { + return; + } + xiaozhi.modules.agent.dto.ContextProviderDTO currentProvider = currentProviders.get(provider.getUrl()); + provider.setHeaders(preserveCurrentSensitiveMap(provider.getHeaders(), + currentProvider == null ? null : currentProvider.getHeaders())); + }); + } + return copy; + } + + private HashMap redactSensitiveMap(Map map) { + HashMap result = new HashMap<>(); + if (map == null) { + return result; + } + map.forEach((key, value) -> { + if (key != null) { + String keyText = String.valueOf(key); + result.put(keyText, isSensitiveKey(keyText) ? SECRET_PLACEHOLDER : redactSensitiveValue(value)); + } + }); + return result; + } + + private Object redactSensitiveValue(Object value) { + if (value instanceof Map map) { + return redactSensitiveMap(map); + } + if (value instanceof List list) { + return list.stream().map(this::redactSensitiveValue).toList(); + } + return value; + } + + private HashMap preserveCurrentSensitiveMap(Map target, Map current) { + HashMap result = new HashMap<>(); + if (target == null) { + return result; + } + target.forEach((key, value) -> { + if (key == null) { + return; + } + String keyText = String.valueOf(key); + Object currentValue = getMapValue(current, keyText); + if (isSensitiveKey(keyText)) { + result.put(keyText, currentValue == null || SECRET_PLACEHOLDER.equals(currentValue) ? "" : currentValue); + } else { + result.put(keyText, preserveCurrentSensitiveValue(value, currentValue)); + } + }); + return result; + } + + private Object preserveCurrentSensitiveValue(Object target, Object current) { + if (target instanceof Map targetMap) { + return preserveCurrentSensitiveMap(targetMap, current instanceof Map currentMap ? currentMap : null); + } + if (target instanceof List targetList) { + List currentList = current instanceof List list ? list : Collections.emptyList(); + List result = new ArrayList<>(); + for (int i = 0; i < targetList.size(); i++) { + Object currentItem = i < currentList.size() ? currentList.get(i) : null; + result.add(preserveCurrentSensitiveValue(targetList.get(i), currentItem)); + } + return result; + } + return target; + } + + private Object getMapValue(Map map, String key) { + if (map == null) { + return null; + } + if (map.containsKey(key)) { + return map.get(key); + } + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() != null && Objects.equals(key, String.valueOf(entry.getKey()))) { + return entry.getValue(); + } + } + return null; + } + + private boolean isSensitiveKey(String key) { + String normalized = StringUtils.defaultString(key).toLowerCase().replaceAll("[^a-z0-9]", ""); + return normalized.equals("authorization") + || normalized.equals("token") + || normalized.endsWith("token") + || normalized.contains("apikey") + || normalized.contains("appkey") + || normalized.contains("accesskey") + || normalized.contains("privatekey") + || normalized.contains("password") + || normalized.contains("passwd") + || normalized.contains("secret") + || normalized.contains("credential"); + } + private List nullToEmpty(List list) { return list == null ? Collections.emptyList() : list; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/typehandler/ContextProviderListTypeHandler.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/typehandler/ContextProviderListTypeHandler.java new file mode 100644 index 00000000..f0762e35 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/typehandler/ContextProviderListTypeHandler.java @@ -0,0 +1,31 @@ +package xiaozhi.modules.agent.typehandler; + +import java.util.Collections; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; + +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import com.fasterxml.jackson.core.type.TypeReference; + +import xiaozhi.common.utils.JsonUtils; +import xiaozhi.modules.agent.dto.ContextProviderDTO; + +public class ContextProviderListTypeHandler extends AbstractJsonTypeHandler> { + private static final TypeReference> CONTEXT_PROVIDER_LIST_TYPE = new TypeReference<>() { + }; + + @Override + protected List parse(String json) { + if (StringUtils.isBlank(json)) { + return Collections.emptyList(); + } + List providers = JsonUtils.parseObject(json, CONTEXT_PROVIDER_LIST_TYPE); + return providers == null ? Collections.emptyList() : providers; + } + + @Override + protected String toJson(List obj) { + return JsonUtils.toJsonString(obj == null ? Collections.emptyList() : obj); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java index e5998327..ab1ca3fc 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java @@ -25,4 +25,7 @@ public class AgentInfoVO extends AgentEntity @Schema(description = "替换词文件ID列表") private List correctWordFileIds; + + @Schema(description = "当前配置版本号") + private Integer currentVersionNo; } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentSnapshotVO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentSnapshotVO.java index 2caf3486..69547e58 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentSnapshotVO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentSnapshotVO.java @@ -12,10 +12,17 @@ import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO; public class AgentSnapshotVO { private String id; private String agentId; + @Schema(description = "所属用户ID,表示该快照归属的智能体所有者") private Long userId; private Integer versionNo; private List changedFields; + private List fieldOrder; private String source; + @Schema(description = "恢复来源快照ID,仅恢复前自动备份版本有值") + private String restoreFromSnapshotId; + @Schema(description = "恢复来源版本号,仅恢复前自动备份版本有值") + private Integer restoreFromVersionNo; + @Schema(description = "创建者,表示触发本次快照写入的操作人") private Long creator; private Date createdAt; private AgentSnapshotDataDTO snapshotData; diff --git a/main/manager-api/src/main/resources/db/changelog/202607081150.sql b/main/manager-api/src/main/resources/db/changelog/202607081150.sql new file mode 100644 index 00000000..b8496eb1 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202607081150.sql @@ -0,0 +1,7 @@ +-- liquibase formatted sql + +-- changeset codex:202607081150 +ALTER TABLE `ai_agent_snapshot` + ADD COLUMN `restore_from_snapshot_id` VARCHAR(32) DEFAULT NULL COMMENT '恢复来源快照ID' AFTER `source`, + ADD COLUMN `restore_from_version_no` INT UNSIGNED DEFAULT NULL COMMENT '恢复来源版本号' AFTER `restore_from_snapshot_id`, + ADD INDEX `idx_snapshot_user_created_at` (`user_id`, `created_at`); diff --git a/main/manager-api/src/main/resources/db/changelog/202607081230.sql b/main/manager-api/src/main/resources/db/changelog/202607081230.sql new file mode 100644 index 00000000..b62bb584 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202607081230.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql + +-- changeset codex:202607081230 +ALTER TABLE `ai_agent_snapshot` + ALTER COLUMN `snapshot_data` SET DEFAULT (JSON_OBJECT()); diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index e81ea5c1..4b8c323d 100644 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -704,3 +704,17 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202607071530.sql + - changeSet: + id: 202607081150 + author: Codex + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202607081150.sql + - changeSet: + id: 202607081230 + author: Codex + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202607081230.sql diff --git a/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml b/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml index 6fb8fb02..c779a5e6 100644 --- a/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml +++ b/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml @@ -36,9 +36,12 @@ + ofType="xiaozhi.modules.agent.entity.AgentPluginMapping"> + + + + + - + SELECT * + FROM ai_agent + WHERE id = #{agentId} + FOR UPDATE + diff --git a/main/manager-api/src/main/resources/mapper/agent/AgentSnapshotDao.xml b/main/manager-api/src/main/resources/mapper/agent/AgentSnapshotDao.xml index 38609ac4..7926cac7 100644 --- a/main/manager-api/src/main/resources/mapper/agent/AgentSnapshotDao.xml +++ b/main/manager-api/src/main/resources/mapper/agent/AgentSnapshotDao.xml @@ -16,6 +16,8 @@ snapshot_data AS snapshotData, changed_fields AS changedFields, source, + restore_from_snapshot_id AS restoreFromSnapshotId, + restore_from_version_no AS restoreFromVersionNo, creator, created_at AS createdAt FROM ai_agent_snapshot @@ -25,4 +27,19 @@ LIMIT 1 + + DELETE FROM ai_agent_snapshot + WHERE agent_id = #{agentId} + AND id NOT IN ( + SELECT id + FROM ( + SELECT id + FROM ai_agent_snapshot + WHERE agent_id = #{agentId} + ORDER BY version_no DESC + LIMIT #{keepLimit} + ) retained_snapshots + ) + + diff --git a/main/manager-api/src/test/java/xiaozhi/modules/agent/dto/AgentUpdateDTOTest.java b/main/manager-api/src/test/java/xiaozhi/modules/agent/dto/AgentUpdateDTOTest.java new file mode 100644 index 00000000..8ffe01c4 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/modules/agent/dto/AgentUpdateDTOTest.java @@ -0,0 +1,44 @@ +package xiaozhi.modules.agent.dto; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +class AgentUpdateDTOTest { + + @Test + void functionInfoAcceptsJsonStringParamInfo() { + AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo(); + + info.setParamInfo("{\"api_key\":\"abc\",\"max_results\":5}"); + + assertEquals("abc", info.getParamInfo().get("api_key")); + assertEquals(5, info.getParamInfo().get("max_results")); + } + + @Test + void functionInfoNormalizesMapKeysToStrings() { + AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo(); + Map params = new LinkedHashMap<>(); + params.put("api_key", "abc"); + params.put(42, true); + + info.setParamInfo(params); + + assertEquals("abc", info.getParamInfo().get("api_key")); + assertEquals(true, info.getParamInfo().get("42")); + } + + @Test + void functionInfoFallsBackToEmptyMapForBlankParamInfo() { + AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo(); + + info.setParamInfo(" "); + + assertTrue(info.getParamInfo().isEmpty()); + } +} diff --git a/main/manager-api/src/test/java/xiaozhi/modules/agent/service/impl/AgentSnapshotServiceImplTest.java b/main/manager-api/src/test/java/xiaozhi/modules/agent/service/impl/AgentSnapshotServiceImplTest.java new file mode 100644 index 00000000..2648145d --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/modules/agent/service/impl/AgentSnapshotServiceImplTest.java @@ -0,0 +1,380 @@ +package xiaozhi.modules.agent.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; +import org.springframework.transaction.annotation.Transactional; + +import xiaozhi.common.utils.JsonUtils; +import xiaozhi.modules.agent.Enums.AgentSnapshotField; +import xiaozhi.modules.agent.dao.AgentDao; +import xiaozhi.modules.agent.dao.AgentSnapshotDao; +import xiaozhi.modules.agent.dao.AgentTagDao; +import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO; +import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO; +import xiaozhi.modules.agent.dto.AgentSnapshotTagDTO; +import xiaozhi.modules.agent.dto.AgentUpdateDTO; +import xiaozhi.modules.agent.dto.ContextProviderDTO; +import xiaozhi.modules.agent.entity.AgentEntity; +import xiaozhi.modules.agent.entity.AgentSnapshotEntity; +import xiaozhi.modules.agent.entity.AgentTagEntity; +import xiaozhi.modules.agent.service.AgentContextProviderService; +import xiaozhi.modules.agent.vo.AgentSnapshotVO; +import xiaozhi.modules.agent.vo.AgentInfoVO; +import xiaozhi.modules.correctword.service.CorrectWordFileService; + +class AgentSnapshotServiceImplTest { + + @Test + @SuppressWarnings("unchecked") + void normalizeSortedJsonListTreatsDtoAndMapShapesEqually() throws Exception { + AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null, + null, null); + Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("normalizeSortedJsonList", List.class); + method.setAccessible(true); + + ContextProviderDTO dto = new ContextProviderDTO(); + dto.setUrl("https://example.com/context"); + dto.setHeaders(Map.of("Authorization", "Bearer token")); + + Map map = new LinkedHashMap<>(); + map.put("headers", Map.of("Authorization", "Bearer token")); + map.put("url", "https://example.com/context"); + + assertEquals(method.invoke(service, List.of(dto)), method.invoke(service, List.of(map))); + } + + @Test + void redactSnapshotDataMasksSensitiveFunctionAndHeaderValues() throws Exception { + AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null, + null, null); + Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("redactSnapshotData", + AgentSnapshotDataDTO.class); + method.setAccessible(true); + + AgentSnapshotDataDTO data = new AgentSnapshotDataDTO(); + AgentUpdateDTO.FunctionInfo function = new AgentUpdateDTO.FunctionInfo(); + function.setPluginId("rag"); + function.setParamInfo(Map.of("api_key", "secret-key", "max_tokens", 32)); + ContextProviderDTO provider = new ContextProviderDTO(); + provider.setUrl("https://example.com/context"); + provider.setHeaders(Map.of("Authorization", "Bearer secret-token", "Accept", "application/json")); + data.setFunctions(List.of(function)); + data.setContextProviders(List.of(provider)); + + AgentSnapshotDataDTO redacted = (AgentSnapshotDataDTO) method.invoke(service, data); + String json = JsonUtils.toJsonString(redacted); + + assertFalse(json.contains("secret-key")); + assertFalse(json.contains("secret-token")); + assertTrue(json.contains("__SNAPSHOT_SECRET_REDACTED__")); + assertTrue(json.contains("max_tokens")); + assertTrue(json.contains("application/json")); + } + + @Test + void preserveCurrentSensitiveValuesKeepsCurrentSecretsWhenRestoringSnapshot() throws Exception { + AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null, + null, null); + Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("preserveCurrentSensitiveValues", + AgentSnapshotDataDTO.class, AgentSnapshotDataDTO.class); + method.setAccessible(true); + + AgentSnapshotDataDTO snapshot = buildSnapshot("old-key", "old-token", "old-value"); + AgentSnapshotDataDTO current = buildSnapshot("current-key", "current-token", "current-value"); + + AgentSnapshotDataDTO restored = (AgentSnapshotDataDTO) method.invoke(service, snapshot, current); + + assertEquals("current-key", restored.getFunctions().get(0).getParamInfo().get("api_key")); + assertEquals("old-value", restored.getFunctions().get(0).getParamInfo().get("description")); + assertEquals("current-token", restored.getContextProviders().get(0).getHeaders().get("Authorization")); + } + + @Test + @SuppressWarnings("unchecked") + void getChangedFieldsFollowsCentralFieldOrder() throws Exception { + AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null, + null, null); + Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("getChangedFields", + AgentSnapshotDataDTO.class, AgentUpdateDTO.class); + method.setAccessible(true); + + AgentSnapshotDataDTO current = new AgentSnapshotDataDTO(); + current.setAgentName("old-name"); + current.setCorrectWordFileIds(List.of("b", "a")); + + AgentUpdateDTO pendingUpdate = new AgentUpdateDTO(); + pendingUpdate.setAgentName("new-name"); + pendingUpdate.setCorrectWordFileIds(List.of("a", "b")); + AgentUpdateDTO.FunctionInfo function = new AgentUpdateDTO.FunctionInfo(); + function.setPluginId("rag"); + function.setParamInfo(Map.of("api_key", "secret-key")); + pendingUpdate.setFunctions(List.of(function)); + + List changedFields = (List) method.invoke(service, current, pendingUpdate); + + assertEquals(List.of(AgentSnapshotField.AGENT_NAME.getFieldName(), + AgentSnapshotField.FUNCTIONS.getFieldName()), changedFields); + } + + @Test + @SuppressWarnings("unchecked") + void tagIdsAreNotExposedAsIndependentSnapshotFields() throws Exception { + AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null, + null, null); + Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("getChangedFields", + AgentSnapshotDataDTO.class, AgentUpdateDTO.class); + method.setAccessible(true); + + AgentSnapshotDataDTO current = new AgentSnapshotDataDTO(); + current.setTagNames(List.of("alpha", "beta")); + + AgentUpdateDTO pendingUpdate = new AgentUpdateDTO(); + pendingUpdate.setTagNames(List.of("beta", "alpha")); + pendingUpdate.setTagIds(List.of("new-id")); + + List changedFields = (List) method.invoke(service, current, pendingUpdate); + + assertFalse(AgentSnapshotField.names().contains("tagIds")); + assertFalse(changedFields.contains("tagIds")); + assertTrue(changedFields.isEmpty()); + + pendingUpdate.setTagNames(List.of("alpha", "gamma")); + changedFields = (List) method.invoke(service, current, pendingUpdate); + + assertEquals(List.of(AgentSnapshotField.TAG_NAMES.getFieldName()), changedFields); + } + + @Test + void tagOnlySavePathCreatesSnapshot() throws Exception { + String controller = Files.readString(Path.of("src/main/java/xiaozhi/modules/agent/controller/AgentController.java")); + String roleConfig = Files.readString(Path.of("../manager-web/src/views/roleConfig.vue")); + + assertTrue(controller.contains("agentService.updateAgentById(id, dto);")); + assertFalse(controller.contains("agentService.updateAgentById(id, dto, false);")); + assertTrue(roleConfig.contains("configData.tagNames = tagNames;")); + assertFalse(roleConfig.contains("this.handleSaveAgentTags(agentId, tagNames)")); + } + + @Test + void agentSnapshotFieldAppliesRestorableAgentFields() { + AgentSnapshotDataDTO data = new AgentSnapshotDataDTO(); + data.setAgentName("restored-name"); + data.setLlmModelId("llm-new"); + data.setSystemPrompt("restored prompt"); + data.setSort(7); + + AgentEntity agent = new AgentEntity(); + for (AgentSnapshotField field : AgentSnapshotField.values()) { + field.applyTo(agent, data); + } + + assertEquals("restored-name", agent.getAgentName()); + assertEquals("llm-new", agent.getLlmModelId()); + assertEquals("restored prompt", agent.getSystemPrompt()); + assertEquals(7, agent.getSort()); + } + + @Test + void restoreSnapshotRollsBackForAnyException() throws Exception { + Method method = AgentSnapshotServiceImpl.class.getMethod("restoreSnapshot", String.class, String.class); + + Transactional transactional = method.getAnnotation(Transactional.class); + + assertNotNull(transactional); + assertTrue(List.of(transactional.rollbackFor()).contains(Exception.class)); + } + + @Test + void deleteSnapshotRollsBackForAnyException() throws Exception { + Method method = AgentSnapshotServiceImpl.class.getMethod("deleteSnapshot", String.class, String.class); + + Transactional transactional = method.getAnnotation(Transactional.class); + + assertNotNull(transactional); + assertTrue(List.of(transactional.rollbackFor()).contains(Exception.class)); + } + + @Test + void versionAllocationDoesNotDependOnSequenceTable() throws Exception { + String xml = Files.readString(Path.of("src/main/resources/mapper/agent/AgentSnapshotDao.xml")); + String dao = Files.readString(Path.of("src/main/java/xiaozhi/modules/agent/dao/AgentSnapshotDao.java")); + String createMigration = Files.readString(Path.of("src/main/resources/db/changelog/202607071530.sql")); + String master = Files.readString(Path.of("src/main/resources/db/changelog/db.changelog-master.yaml")); + + assertFalse(xml.contains("ai_agent_snapshot_sequence")); + assertFalse(xml.contains("LAST_INSERT_ID")); + assertFalse(dao.contains("allocateVersionNo")); + assertFalse(dao.contains("selectLastInsertId")); + assertFalse(createMigration.contains("ai_agent_snapshot_sequence")); + assertFalse(master.contains("202607081430.sql")); + } + + @Test + void snapshotMigrationAddsRestoreTraceAndUserIndex() throws Exception { + String sql = Files.readString(Path.of("src/main/resources/db/changelog/202607081150.sql")); + String defaultSql = Files.readString(Path.of("src/main/resources/db/changelog/202607081230.sql")); + String master = Files.readString(Path.of("src/main/resources/db/changelog/db.changelog-master.yaml")); + + assertTrue(sql.contains("restore_from_snapshot_id")); + assertTrue(sql.contains("restore_from_version_no")); + assertTrue(sql.contains("idx_snapshot_user_created_at")); + assertTrue(master.contains("202607081150.sql")); + assertTrue(defaultSql.contains("DEFAULT (JSON_OBJECT())")); + assertTrue(master.contains("202607081230.sql")); + } + + @Test + void selectNextSnapshotLoadsRestoreTraceColumns() throws Exception { + String xml = Files.readString(Path.of("src/main/resources/mapper/agent/AgentSnapshotDao.xml")); + + assertTrue(xml.contains("restore_from_snapshot_id AS restoreFromSnapshotId")); + assertTrue(xml.contains("restore_from_version_no AS restoreFromVersionNo")); + } + + @Test + void retentionPruneSqlKeepsLatestVersions() throws Exception { + String xml = Files.readString(Path.of("src/main/resources/mapper/agent/AgentSnapshotDao.xml")); + + assertTrue(xml.contains("deleteOlderThanKeepLimit")); + assertTrue(xml.contains("ORDER BY version_no DESC")); + assertTrue(xml.contains("LIMIT #{keepLimit}")); + } + + @Test + void agentInfoMapperLoadsFunctionsWithJoinInsteadOfNestedSelect() throws Exception { + String xml = Files.readString(Path.of("src/main/resources/mapper/agent/AgentDao.xml")); + + assertTrue(xml.contains("LEFT JOIN ai_agent_plugin_mapping f ON f.agent_id = a.id")); + assertFalse(xml.contains("selectAgentFunctionsByAgentId")); + assertFalse(xml.contains("select=\"selectAgentFunctions")); + } + + @Test + void toVOIncludesRestoreTraceFields() throws Exception { + AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null, + null, null); + Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("toVO", + AgentSnapshotEntity.class, boolean.class); + method.setAccessible(true); + + AgentSnapshotEntity entity = new AgentSnapshotEntity(); + entity.setId("snapshot-id"); + entity.setAgentId("agent-id"); + entity.setVersionNo(3); + entity.setChangedFields("[]"); + entity.setSource("restore"); + entity.setRestoreFromSnapshotId("target-id"); + entity.setRestoreFromVersionNo(1); + + AgentSnapshotVO vo = (AgentSnapshotVO) method.invoke(service, entity, false); + + assertEquals("target-id", vo.getRestoreFromSnapshotId()); + assertEquals(1, vo.getRestoreFromVersionNo()); + } + + @Test + void pageCreatesInitialSnapshotWhenAgentHasNoHistory() { + AgentSnapshotDao snapshotDao = mock(AgentSnapshotDao.class); + AgentDao agentDao = mock(AgentDao.class); + AgentTagDao tagDao = mock(AgentTagDao.class); + AgentContextProviderService contextProviderService = mock(AgentContextProviderService.class); + CorrectWordFileService correctWordFileService = mock(CorrectWordFileService.class); + AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(snapshotDao, agentDao, tagDao, null, null, + contextProviderService, null, null, null, correctWordFileService); + + AgentEntity lockedAgent = new AgentEntity(); + lockedAgent.setId("agent-id"); + AgentInfoVO agentInfo = new AgentInfoVO(); + agentInfo.setId("agent-id"); + agentInfo.setUserId(7L); + agentInfo.setAgentName("agent"); + when(snapshotDao.selectMaxVersionNo("agent-id")).thenReturn(0, 0, 0); + when(agentDao.selectByIdForUpdate("agent-id")).thenReturn(lockedAgent); + when(agentDao.selectAgentInfoById("agent-id")).thenReturn(agentInfo); + when(correctWordFileService.getAgentCorrectWordFileIds("agent-id")).thenReturn(List.of()); + when(tagDao.selectByAgentId("agent-id")).thenReturn(List.of()); + when(snapshotDao.selectPage(any(), any())).thenReturn(new Page(1, 10)); + + service.page("agent-id", new AgentSnapshotPageDTO()); + + verify(snapshotDao, times(3)).selectMaxVersionNo("agent-id"); + verify(snapshotDao).insert(argThat(snapshot -> Integer.valueOf(1).equals(snapshot.getVersionNo()))); + verify(snapshotDao).deleteOlderThanKeepLimit("agent-id", 100); + } + + @Test + void restoreTagDoesNotReviveSoftDeletedSnapshotTagId() throws Exception { + AgentTagDao tagDao = mock(AgentTagDao.class); + AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, tagDao, null, null, null, null, null, + null, null); + Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("restoreTag", + AgentSnapshotTagDTO.class, Date.class); + method.setAccessible(true); + + AgentTagEntity deletedTag = new AgentTagEntity(); + deletedTag.setId("deleted-tag-id"); + deletedTag.setTagName("archived"); + deletedTag.setDeleted(1); + when(tagDao.selectById("deleted-tag-id")).thenReturn(deletedTag); + when(tagDao.selectOne(any())).thenReturn(null); + AtomicReference insertedTag = new AtomicReference<>(); + when(tagDao.insert(any())).thenAnswer(invocation -> { + insertedTag.set(invocation.getArgument(0)); + return 1; + }); + + AgentSnapshotTagDTO snapshotTag = new AgentSnapshotTagDTO(); + snapshotTag.setId("deleted-tag-id"); + snapshotTag.setTagName("archived"); + + String restoredTagId = (String) method.invoke(service, snapshotTag, new Date()); + + verify(tagDao, never()).updateById(any()); + assertNotEquals("deleted-tag-id", restoredTagId); + assertNotNull(insertedTag.get()); + assertEquals(restoredTagId, insertedTag.get().getId()); + assertEquals(0, insertedTag.get().getDeleted()); + } + + private AgentSnapshotDataDTO buildSnapshot(String apiKey, String authToken, String description) { + AgentSnapshotDataDTO data = new AgentSnapshotDataDTO(); + + AgentUpdateDTO.FunctionInfo function = new AgentUpdateDTO.FunctionInfo(); + function.setPluginId("rag"); + HashMap params = new HashMap<>(); + params.put("api_key", apiKey); + params.put("description", description); + function.setParamInfo(params); + + ContextProviderDTO provider = new ContextProviderDTO(); + provider.setUrl("https://example.com/context"); + provider.setHeaders(Map.of("Authorization", authToken)); + + data.setFunctions(List.of(function)); + data.setContextProviders(List.of(provider)); + return data; + } +} diff --git a/main/manager-api/src/test/java/xiaozhi/modules/agent/typehandler/ContextProviderListTypeHandlerTest.java b/main/manager-api/src/test/java/xiaozhi/modules/agent/typehandler/ContextProviderListTypeHandlerTest.java new file mode 100644 index 00000000..e1d0e6c4 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/modules/agent/typehandler/ContextProviderListTypeHandlerTest.java @@ -0,0 +1,42 @@ +package xiaozhi.modules.agent.typehandler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.apache.ibatis.type.TypeHandler; +import org.apache.ibatis.type.TypeHandlerRegistry; +import org.junit.jupiter.api.Test; + +import xiaozhi.modules.agent.dto.ContextProviderDTO; + +class ContextProviderListTypeHandlerTest { + + private final ContextProviderListTypeHandler handler = new ContextProviderListTypeHandler(); + + @Test + void parseKeepsContextProviderDtoElementType() { + List providers = handler + .parse("[{\"url\":\"https://example.com/context\",\"headers\":{\"Authorization\":\"Bearer token\"}}]"); + + assertEquals(1, providers.size()); + assertInstanceOf(ContextProviderDTO.class, providers.get(0)); + assertEquals("https://example.com/context", providers.get(0).getUrl()); + assertEquals("Bearer token", providers.get(0).getHeaders().get("Authorization")); + } + + @Test + void parseBlankJsonAsEmptyList() { + assertTrue(handler.parse(" ").isEmpty()); + } + + @Test + void myBatisCanInstantiateHandlerForListField() { + TypeHandler typeHandler = new TypeHandlerRegistry().getInstance(List.class, + ContextProviderListTypeHandler.class); + + assertInstanceOf(ContextProviderListTypeHandler.class, typeHandler); + } +} diff --git a/main/manager-mobile/src/pages/agent/edit.vue b/main/manager-mobile/src/pages/agent/edit.vue index 964ad0b2..e33c0211 100644 --- a/main/manager-mobile/src/pages/agent/edit.vue +++ b/main/manager-mobile/src/pages/agent/edit.vue @@ -169,6 +169,28 @@ function handleInputConfirm() { // 是否禁用历史记忆输入框 const isMemoryDisabled = computed(() => formData.value.memModelId !== 'Memory_mem_local_short') +function normalizeFunctionParams(paramInfo: any) { + if (!paramInfo) + return {} + if (typeof paramInfo === 'string') { + try { + const parsed = JSON.parse(paramInfo) + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + } + catch { + return {} + } + } + return typeof paramInfo === 'object' && !Array.isArray(paramInfo) ? { ...paramInfo } : {} +} + +function normalizeAgentFunctions(functions: any[] = []) { + return functions.map(item => ({ + ...item, + paramInfo: normalizeFunctionParams(item.paramInfo), + })) +} + // 打开上下文源编辑弹窗 function openContextProviderDialog() { uni.navigateTo({ @@ -191,11 +213,12 @@ async function loadAgentDetail() { loading.value = true tempSummaryMemory.value = '' const detail = await getAgentDetail(agentId.value) - formData.value = { ...detail } + const normalizedFunctions = normalizeAgentFunctions(detail.functions || []) + formData.value = { ...detail, functions: normalizedFunctions } // 更新插件store pluginStore.setCurrentAgentId(agentId.value) - pluginStore.setCurrentFunctions(detail.functions || []) + pluginStore.setCurrentFunctions(normalizedFunctions) // 更新语速音调 speedPitchStore.updateSpeedPitch({ @@ -626,6 +649,7 @@ async function saveAgent() { ...speedPitchStore.speedPitch, ttsLanguage: formData.value.language, contextProviders: providerStore.providers, + functions: normalizeAgentFunctions(formData.value.functions || []), } await updateAgent(agentId.value, saveData) loadAgentDetail() @@ -663,7 +687,7 @@ function handleTools() { // 确保store中有最新数据 pluginStore.setCurrentAgentId(agentId.value) - pluginStore.setCurrentFunctions(formData.value.functions || []) + pluginStore.setCurrentFunctions(normalizeAgentFunctions(formData.value.functions || [])) pluginStore.setAllFunctions(allFunctions.value) uni.navigateTo({ @@ -688,7 +712,7 @@ async function handleUpdateAgentTags() { // 监听store中的插件配置变化 watch(() => pluginStore.currentFunctions, (newFunctions) => { - formData.value.functions = newFunctions + formData.value.functions = normalizeAgentFunctions(newFunctions || []) }, { deep: true }) onMounted(async () => { diff --git a/main/manager-mobile/src/pages/agent/tools.vue b/main/manager-mobile/src/pages/agent/tools.vue index fb47deae..6d835a30 100644 --- a/main/manager-mobile/src/pages/agent/tools.vue +++ b/main/manager-mobile/src/pages/agent/tools.vue @@ -41,17 +41,32 @@ const tempParams = ref>({}) const arrayTextCache = ref>({}) const jsonTextCache = ref>({}) +function normalizeFunctionParams(paramInfo: any, fallback: Record = {}) { + if (!paramInfo) + return { ...fallback } + if (typeof paramInfo === 'string') { + try { + const parsed = JSON.parse(paramInfo) + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : { ...fallback } + } + catch { + return { ...fallback } + } + } + return typeof paramInfo === 'object' && !Array.isArray(paramInfo) ? { ...fallback, ...paramInfo } : { ...fallback } +} + async function mergeFunctions() { selectedList.value = functions.value.map((mapping) => { const meta = allFunctions.value.find(f => f.id === mapping.pluginId) if (!meta) { - return { id: mapping.pluginId, name: mapping.pluginId, params: {} } + return { id: mapping.pluginId, name: mapping.pluginId, params: normalizeFunctionParams(mapping.paramInfo) } } return { id: mapping.pluginId, name: meta.name, - params: mapping.paramInfo || { ...meta.params }, + params: normalizeFunctionParams(mapping.paramInfo, meta.params), fieldsMeta: meta.fieldsMeta, } }) @@ -91,7 +106,7 @@ function selectFunction(func: any) { selectedList.value = [...selectedList.value, { id: func.id, name: func.name, - params: { ...func.params }, + params: normalizeFunctionParams(func.params), fieldsMeta: func.fieldsMeta, }] @@ -118,7 +133,7 @@ function editFunction(func: any) { currentFunction.value = func // 直接使用当前函数的参数 - tempParams.value = { ...func.params } + tempParams.value = normalizeFunctionParams(func.params) // 初始化文本缓存 if (func.fieldsMeta) { @@ -252,7 +267,7 @@ function getFieldRemark(field: any) { watch(() => selectedList.value, (newSelectedList) => { const finalFunctions = newSelectedList.map(f => ({ pluginId: f.id, - paramInfo: f.params, + paramInfo: normalizeFunctionParams(f.params), })) pluginStore.updateFunctions(finalFunctions) }) diff --git a/main/manager-web/package-lock.json b/main/manager-web/package-lock.json index acc4aec2..44829664 100644 --- a/main/manager-web/package-lock.json +++ b/main/manager-web/package-lock.json @@ -11096,6 +11096,16 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/main/manager-web/src/apis/module/agent.js b/main/manager-web/src/apis/module/agent.js index 2a45f197..3c9522db 100644 --- a/main/manager-web/src/apis/module/agent.js +++ b/main/manager-web/src/apis/module/agent.js @@ -127,6 +127,21 @@ export default { }); }).send(); }, + // 删除智能体配置快照 + deleteAgentSnapshot(agentId, snapshotId, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/${agentId}/snapshots/${snapshotId}`) + .method('DELETE') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.deleteAgentSnapshot(agentId, snapshotId, callback); + }); + }).send(); + }, // 新增方法:获取智能体模板 getAgentTemplate(callback) { // 移除templateName参数 RequestService.sendRequest() diff --git a/main/manager-web/src/components/AgentSnapshotDialog.vue b/main/manager-web/src/components/AgentSnapshotDialog.vue index 5efc20d3..9b09a00a 100644 --- a/main/manager-web/src/components/AgentSnapshotDialog.vue +++ b/main/manager-web/src/components/AgentSnapshotDialog.vue @@ -5,7 +5,7 @@ :visible="visible" width="760px" class="agent-snapshot-dialog" - @open="fetchSnapshots" + @open="open" @close="close" > — - + @@ -185,7 +195,7 @@ -
功能配置无变化
+
{{ $t('agentSnapshot.noFunctionChange') }}
@@ -306,7 +316,7 @@
-
功能配置无变化
+
{{ $t('agentSnapshot.noFunctionChange') }}
@@ -339,6 +349,22 @@ :description="$t('agentSnapshot.noChangedContent')" :image-size="80" /> + +
@@ -361,33 +387,33 @@ import Api from "@/apis/api"; import { formatDate } from "@/utils/date"; -const FALLBACK_PLUGIN_NAMES = { - SYSTEM_PLUGIN_WEATHER: "天气查询", - SYSTEM_PLUGIN_MUSIC: "服务器音乐播放", - SYSTEM_PLUGIN_NEWS_CHINANEWS: "中新网新闻", - SYSTEM_PLUGIN_NEWS_NEWSNOW: "newsnow新闻聚合", - SYSTEM_PLUGIN_HA_GET_STATE: "HomeAssistant设备状态查询", - SYSTEM_PLUGIN_HA_SET_STATE: "HomeAssistant设备状态修改", - SYSTEM_PLUGIN_HA_PLAY_MUSIC: "HomeAssistant音乐播放", - SYSTEM_PLUGIN_WEB_SEARCH: "联网搜索", - SYSTEM_PLUGIN_CALL_DEVICE: "设备呼叫设备" +const FALLBACK_PLUGIN_NAME_KEYS = { + SYSTEM_PLUGIN_WEATHER: "agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER", + SYSTEM_PLUGIN_MUSIC: "agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC", + SYSTEM_PLUGIN_NEWS_CHINANEWS: "agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS", + SYSTEM_PLUGIN_NEWS_NEWSNOW: "agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW", + SYSTEM_PLUGIN_HA_GET_STATE: "agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE", + SYSTEM_PLUGIN_HA_SET_STATE: "agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE", + SYSTEM_PLUGIN_HA_PLAY_MUSIC: "agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC", + SYSTEM_PLUGIN_WEB_SEARCH: "agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH", + SYSTEM_PLUGIN_CALL_DEVICE: "agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE" }; -const FALLBACK_FIELD_LABELS = { - url: "接口地址", - news_sources: "新闻源配置", - default_rss_url: "默认 RSS 源", - society_rss_url: "社会新闻 RSS 地址", - world_rss_url: "国际新闻 RSS 地址", - finance_rss_url: "财经新闻 RSS 地址", - api_key: "API 密钥", - default_location: "默认查询城市", - api_host: "开发者 API Host", - base_url: "服务器地址", - devices: "设备列表", - provider: "搜索源", - description: "工具描述", - max_results: "返回数量" +const FALLBACK_FIELD_LABEL_KEYS = { + url: "agentSnapshot.pluginField.url", + news_sources: "agentSnapshot.pluginField.news_sources", + default_rss_url: "agentSnapshot.pluginField.default_rss_url", + society_rss_url: "agentSnapshot.pluginField.society_rss_url", + world_rss_url: "agentSnapshot.pluginField.world_rss_url", + finance_rss_url: "agentSnapshot.pluginField.finance_rss_url", + api_key: "agentSnapshot.pluginField.api_key", + default_location: "agentSnapshot.pluginField.default_location", + api_host: "agentSnapshot.pluginField.api_host", + base_url: "agentSnapshot.pluginField.base_url", + devices: "agentSnapshot.pluginField.devices", + provider: "agentSnapshot.pluginField.provider", + description: "agentSnapshot.pluginField.description", + max_results: "agentSnapshot.pluginField.max_results" }; const MODEL_FIELD_TYPES = { @@ -403,29 +429,29 @@ const MODEL_FIELD_TYPES = { const MODEL_TYPES = ["ASR", "VAD", "LLM", "SLM", "VLLM", "TTS", "Memory", "Intent"]; -const FALLBACK_MODEL_NAMES = { - Memory_nomem: "无记忆", - Memory_mem_local_short: "本地短记忆", - Memory_mem0ai: "Mem0AI记忆", - Memory_mem_report_only: "仅上报记忆", - Intent_nointent: "无意图识别", - Intent_intent_llm: "外挂的大模型意图识别", - Intent_function_call: "大模型自主函数调用" +const FALLBACK_MODEL_NAME_KEYS = { + Memory_nomem: "agentSnapshot.model.Memory_nomem", + Memory_mem_local_short: "agentSnapshot.model.Memory_mem_local_short", + Memory_mem0ai: "agentSnapshot.model.Memory_mem0ai", + Memory_mem_report_only: "agentSnapshot.model.Memory_mem_report_only", + Intent_nointent: "agentSnapshot.model.Intent_nointent", + Intent_intent_llm: "agentSnapshot.model.Intent_intent_llm", + Intent_function_call: "agentSnapshot.model.Intent_function_call" }; -const FALLBACK_VOICE_NAMES = { - TTS_EdgeTTS0001: "EdgeTTS女声-晓晓", - TTS_EdgeTTS0002: "EdgeTTS男声-云扬", - TTS_EdgeTTS0003: "EdgeTTS女声-晓伊", - TTS_EdgeTTS0004: "EdgeTTS男声-云健", - TTS_EdgeTTS0005: "EdgeTTS男声-云希", - TTS_EdgeTTS0006: "EdgeTTS男声-云夏" +const FALLBACK_VOICE_NAME_KEYS = { + TTS_EdgeTTS0001: "agentSnapshot.voice.TTS_EdgeTTS0001", + TTS_EdgeTTS0002: "agentSnapshot.voice.TTS_EdgeTTS0002", + TTS_EdgeTTS0003: "agentSnapshot.voice.TTS_EdgeTTS0003", + TTS_EdgeTTS0004: "agentSnapshot.voice.TTS_EdgeTTS0004", + TTS_EdgeTTS0005: "agentSnapshot.voice.TTS_EdgeTTS0005", + TTS_EdgeTTS0006: "agentSnapshot.voice.TTS_EdgeTTS0006" }; -const CHAT_HISTORY_CONF_LABELS = { - 0: "不记录聊天记录", - 1: "上报文字", - 2: "上报文字+语音" +const CHAT_HISTORY_CONF_LABEL_KEYS = { + 0: "agentSnapshot.chatHistoryConf.none", + 1: "agentSnapshot.chatHistoryConf.text", + 2: "agentSnapshot.chatHistoryConf.textVoice" }; export default { @@ -456,16 +482,22 @@ export default { currentSnapshot: null, restorePreviewSnapshot: null, restorePreviewRow: null, + deletingSnapshotId: null, pluginMetadata: {}, pluginMetadataLoaded: false, pluginMetadataLoading: null, modelNameMap: {}, modelMetadataLoaded: false, modelMetadataLoading: null, - voiceNameMap: { ...FALLBACK_VOICE_NAMES }, + voiceNameMap: {}, voiceMetadataLoading: {}, loadedVoiceModelIds: {}, snapshotFetchSeq: 0, + detailFetchSeq: 0, + restorePreviewFetchSeq: 0, + restoreActionSeq: 0, + deleteActionSeq: 0, + historyAnchorVersionNo: null, historyTotal: 0, page: 1, limit: 10, @@ -503,10 +535,11 @@ export default { const beforeData = this.currentSnapshot.snapshotData || {}; const afterData = this.currentSnapshot.afterSnapshotData || {}; return this.buildDiffs(beforeData, afterData, this.currentSnapshot.changedFields || [], { - beforeLabel: "变化前", - afterLabel: "变化后", + beforeLabel: this.$t("agentSnapshot.beforeChange"), + afterLabel: this.$t("agentSnapshot.afterChange"), beforeVersionNo: this.currentSnapshot.beforeVersionNo, - afterVersionNo: this.currentSnapshot.afterVersionNo || this.currentSnapshot.versionNo + afterVersionNo: this.currentSnapshot.afterVersionNo || this.currentSnapshot.versionNo, + fieldOrder: this.currentSnapshot.fieldOrder }); }, restorePreviewDiffs() { @@ -519,17 +552,35 @@ export default { this.restorePreviewSnapshot.afterSnapshotData || {}, [], { - beforeLabel: "恢复前", - afterLabel: "恢复后", + beforeLabel: this.$t("agentSnapshot.beforeRestore"), + afterLabel: this.$t("agentSnapshot.afterRestore"), beforeVersionNo: this.restorePreviewSnapshot.beforeVersionNo || this.resolveCurrentVersionNo(), - afterVersionNo: this.restorePreviewSnapshot.afterVersionNo || this.restorePreviewSnapshot.versionNo + afterVersionNo: this.restorePreviewSnapshot.afterVersionNo || this.restorePreviewSnapshot.versionNo, + fieldOrder: this.restorePreviewSnapshot.fieldOrder } ); + }, + restoreWillClearChatHistory() { + return this.restorePreviewSnapshot?.afterSnapshotData?.memModelId === "Memory_nomem"; + }, + restoreTargetVersion() { + return this.restorePreviewSnapshot?.versionNo || this.restorePreviewRow?.versionNo || ""; } }, + watch: { + currentVersionNo(newValue, oldValue) { + if (this.visible && newValue !== oldValue) { + this.historyAnchorVersionNo = null; + this.fetchSnapshots({ clearTable: false }); + } + } + }, + beforeDestroy() { + this.cancelPendingSnapshotRequests(); + }, methods: { buildDiffs(beforeData, afterData, changedFields, options = {}) { - const fields = this.resolveDiffFields(beforeData, afterData, changedFields); + const fields = this.resolveDiffFields(beforeData, afterData, changedFields, options.fieldOrder); return fields.map((field) => { const beforeValue = this.getFieldValue(beforeData, field); const afterValue = this.getFieldValue(afterData, field); @@ -552,10 +603,27 @@ export default { }); }, close() { - this.snapshotFetchSeq += 1; - this.loading = false; + this.cancelPendingSnapshotRequests(); + this.historyAnchorVersionNo = null; this.$emit("update:visible", false); }, + open() { + this.historyAnchorVersionNo = null; + this.page = 1; + this.fetchSnapshots(); + }, + cancelPendingSnapshotRequests() { + this.snapshotFetchSeq += 1; + this.detailFetchSeq += 1; + this.restorePreviewFetchSeq += 1; + this.restoreActionSeq += 1; + this.deleteActionSeq += 1; + this.loading = false; + this.detailLoading = false; + this.restorePreviewLoading = false; + this.restoring = false; + this.deletingSnapshotId = null; + }, snapshotRowKey(row) { if (row?.isCurrentVersion) { return "current-version"; @@ -566,11 +634,14 @@ export default { return row?.isCurrentVersion ? "current-version-row" : ""; }, canViewSnapshot(row) { - return Number(row?.versionNo) > 1; + return !!row && (row.isCurrentVersion || row.previousSnapshotId || row.previousVersionNo); }, canRestoreSnapshot(row) { return !!row && !row.isCurrentVersion; }, + canDeleteSnapshot(row) { + return !!row && !row.isCurrentVersion && !row.isLatestSnapshot; + }, fetchSnapshots(options = {}) { if (!this.agentId) { this.snapshots = []; @@ -582,7 +653,7 @@ export default { const requestSeq = ++this.snapshotFetchSeq; const displayStart = (this.page - 1) * this.limit; const displayEnd = displayStart + this.limit; - const historyFetchLimit = Math.max(displayEnd, this.limit, 1); + const historyFetchLimit = Math.max(displayEnd + 1, this.limit, 1); this.ensurePluginMetadata(); this.ensureModelMetadata(); this.loading = true; @@ -591,24 +662,32 @@ export default { } Api.agent.getAgentSnapshots( this.agentId, - { page: "1", limit: String(historyFetchLimit) }, + this.snapshotQueryParams({ page: "1", limit: String(historyFetchLimit) }), ({ data }) => { if (requestSeq !== this.snapshotFetchSeq) { return; } this.loading = false; if (data.code === 0) { - const historyRows = data.data?.list || []; + const historyRows = this.decorateSnapshotRows(data.data?.list || []); + if (!this.historyAnchorVersionNo && historyRows[0]?.versionNo) { + this.historyAnchorVersionNo = Number(historyRows[0].versionNo); + } + const currentRow = this.buildCurrentVersionRow(historyRows[0]); + const historyOffset = currentRow ? 1 : 0; this.historyTotal = data.data?.total || 0; - this.total = this.historyTotal + 1; + this.total = this.historyTotal + historyOffset; if (this.page === 1) { + const firstPageRows = historyRows.slice(0, currentRow ? this.limit - 1 : this.limit); this.snapshots = [ - this.buildCurrentVersionRow(historyRows[0]), - ...this.applyPreviousChangedFields(historyRows.slice(0, this.limit - 1), historyRows) + ...(currentRow ? [currentRow] : []), + ...this.applyPreviousChangedFields(firstPageRows, historyRows) ]; } else { + const historyStart = Math.max(displayStart - historyOffset, 0); + const historyEnd = Math.max(displayEnd - historyOffset, 0); this.snapshots = this.applyPreviousChangedFields( - historyRows.slice(displayStart - 1, displayEnd - 1), + historyRows.slice(historyStart, historyEnd), historyRows ); } @@ -629,18 +708,27 @@ export default { this.detailVisible = true; this.detailLoading = true; this.currentSnapshot = null; + const requestSeq = ++this.detailFetchSeq; this.ensurePluginMetadata(); this.ensureModelMetadata(); this.buildVersionDiffSnapshot(row) .then((snapshot) => this.ensureDisplayMetadata(snapshot).then(() => snapshot)) .then((snapshot) => { + if (requestSeq !== this.detailFetchSeq) { + return; + } this.currentSnapshot = snapshot; }) .catch(() => { + if (requestSeq !== this.detailFetchSeq) { + return; + } this.$message.error(this.$t("agentSnapshot.detailFailed")); }) .finally(() => { - this.detailLoading = false; + if (requestSeq === this.detailFetchSeq) { + this.detailLoading = false; + } }); }, restoreSnapshot(row) { @@ -651,6 +739,7 @@ export default { this.restorePreviewLoading = true; this.restorePreviewRow = row; this.restorePreviewSnapshot = null; + const requestSeq = ++this.restorePreviewFetchSeq; this.ensurePluginMetadata(); this.ensureModelMetadata(); @@ -658,41 +747,97 @@ export default { this.fetchSnapshotDetail(row.id), this.fetchCurrentAgentData() ]).then(([targetSnapshot, currentData]) => { + if (requestSeq !== this.restorePreviewFetchSeq) { + return Promise.resolve(); + } const previewSnapshot = { ...targetSnapshot, beforeSnapshotData: currentData, afterSnapshotData: targetSnapshot.snapshotData || {}, beforeVersionNo: this.resolveCurrentVersionNo(), - afterVersionNo: targetSnapshot.versionNo + afterVersionNo: targetSnapshot.versionNo, + fieldOrder: targetSnapshot.fieldOrder || [] }; return this.ensureDisplayMetadata(previewSnapshot).then(() => { + if (requestSeq !== this.restorePreviewFetchSeq) { + return; + } this.restorePreviewSnapshot = previewSnapshot; }); }).catch(() => { + if (requestSeq !== this.restorePreviewFetchSeq) { + return; + } this.restorePreviewVisible = false; this.$message.error(this.$t("agentSnapshot.detailFailed")); }).finally(() => { - this.restorePreviewLoading = false; + if (requestSeq === this.restorePreviewFetchSeq) { + this.restorePreviewLoading = false; + } }); }, + decorateSnapshotRows(rows) { + return (rows || []).map((row, index) => ({ + ...row, + isLatestSnapshot: index === 0 + })); + }, confirmRestoreSnapshot() { if (!this.restorePreviewRow) { return; } this.restoring = true; + const requestSeq = ++this.restoreActionSeq; Api.agent.restoreAgentSnapshot(this.agentId, this.restorePreviewRow.id, ({ data }) => { + if (requestSeq !== this.restoreActionSeq) { + return; + } this.restoring = false; if (data.code === 0) { this.$message.success(this.$t("agentSnapshot.restoreSuccess")); this.restorePreviewVisible = false; this.detailVisible = false; this.$emit("restored"); + this.historyAnchorVersionNo = null; this.fetchSnapshots(); } else { - this.$message.error(data.msg || this.$t("agentSnapshot.restoreFailed")); + this.$message.error(this.restoreFailedMessage(data)); } }); }, + deleteSnapshot(row) { + if (!this.canDeleteSnapshot(row)) { + return; + } + this.$confirm( + this.$t("agentSnapshot.deleteConfirm", { version: row.versionNo }), + this.$t("common.warning"), + { + confirmButtonText: this.$t("common.confirm"), + cancelButtonText: this.$t("common.cancel"), + type: "warning" + } + ).then(() => { + const requestSeq = ++this.deleteActionSeq; + this.deletingSnapshotId = row.id; + Api.agent.deleteAgentSnapshot(this.agentId, row.id, ({ data }) => { + if (requestSeq !== this.deleteActionSeq) { + return; + } + this.deletingSnapshotId = null; + if (data.code === 0) { + this.$message.success(this.$t("agentSnapshot.deleteSuccess")); + const savedRowsOnPage = this.snapshots.filter((item) => !item.isCurrentVersion); + if (savedRowsOnPage.length <= 1 && this.page > 1) { + this.page -= 1; + } + this.fetchSnapshots(); + } else { + this.$message.error(data.msg || this.$t("agentSnapshot.deleteFailed")); + } + }); + }).catch(() => {}); + }, hasAfterSnapshotData(snapshot) { return !!( snapshot && @@ -701,8 +846,9 @@ export default { ); }, resolveCurrentVersionNo() { - if (this.currentVersionNo) { - return this.currentVersionNo; + const propVersionNo = Number(this.currentVersionNo); + if (Number.isFinite(propVersionNo) && propVersionNo > 0) { + return propVersionNo; } const currentRow = this.snapshots.find((item) => item.isCurrentVersion); @@ -710,12 +856,7 @@ export default { return currentRow.versionNo; } - const maxVersionNo = Math.max( - ...this.snapshots - .filter((item) => !item.isCurrentVersion) - .map((item) => Number(item.versionNo) || 0) - ); - return maxVersionNo > 0 ? maxVersionNo + 1 : 1; + return null; }, formatPaneTitle(label, versionNo) { return versionNo ? `${label} #${versionNo}` : label; @@ -745,7 +886,7 @@ export default { }, fetchSnapshotRows(params) { return new Promise((resolve, reject) => { - Api.agent.getAgentSnapshots(this.agentId, params, ({ data }) => { + Api.agent.getAgentSnapshots(this.agentId, this.snapshotQueryParams(params), ({ data }) => { if (data.code === 0) { resolve(data.data || { list: [], total: 0 }); } else { @@ -754,6 +895,15 @@ export default { }); }); }, + snapshotQueryParams(params = {}) { + if (!this.historyAnchorVersionNo) { + return params; + } + return { + ...params, + maxVersionNo: String(this.historyAnchorVersionNo) + }; + }, applyPreviousChangedFields(rows, sourceRows) { return rows.map((row) => { const versionNo = Number(row.versionNo); @@ -763,27 +913,38 @@ export default { changedFields: [] }; } - const previousRow = sourceRows.find((item) => Number(item.versionNo) === versionNo - 1); + const previousRow = this.findPreviousSnapshotRow(row, sourceRows); return { ...row, + previousSnapshotId: previousRow?.id || null, + previousVersionNo: previousRow?.versionNo || null, changedFields: previousRow?.changedFields || [] }; }); }, + findPreviousSnapshotRow(row, sourceRows) { + const versionNo = Number(row?.versionNo); + if (!Number.isFinite(versionNo)) { + return null; + } + return (sourceRows || []).find((item) => { + return !item.isCurrentVersion && Number(item.versionNo) < versionNo; + }) || null; + }, buildCurrentVersionRow(latestSnapshot) { const latestVersionNo = Number(latestSnapshot?.versionNo) || 0; const propVersionNo = Number(this.currentVersionNo); - const inferredVersionNo = latestVersionNo + 1 || 1; - const currentVersionNo = Number.isFinite(propVersionNo) && propVersionNo > latestVersionNo - ? propVersionNo - : inferredVersionNo; + if (!Number.isFinite(propVersionNo) || propVersionNo <= latestVersionNo) { + return null; + } return { id: "__current__", agentId: this.agentId, - versionNo: currentVersionNo, + versionNo: propVersionNo, source: "current", createdAt: latestSnapshot?.createdAt || null, changedFields: latestSnapshot?.changedFields || [], + fieldOrder: latestSnapshot?.fieldOrder || [], isCurrentVersion: true, previousVersionNo: latestVersionNo || null, previousSnapshotId: latestSnapshot?.id || null @@ -797,9 +958,8 @@ export default { }, buildCurrentVersionDiffSnapshot(row) { const currentVersionNo = Number(row.versionNo); - const previousVersionNo = currentVersionNo - 1; return Promise.all([ - this.fetchSnapshotDetailByVersion(previousVersionNo), + this.fetchPreviousSnapshotDetail(row), this.fetchCurrentAgentData() ]).then(([previousSnapshot, currentData]) => ({ ...row, @@ -807,26 +967,41 @@ export default { changedFields: previousSnapshot.changedFields || [], snapshotData: previousSnapshot.snapshotData || {}, afterSnapshotData: currentData, - beforeVersionNo: previousVersionNo, - afterVersionNo: currentVersionNo + beforeVersionNo: previousSnapshot.versionNo, + afterVersionNo: currentVersionNo, + fieldOrder: previousSnapshot.fieldOrder || row.fieldOrder || [] })); }, buildSavedVersionDiffSnapshot(row) { const versionNo = Number(row.versionNo); - const previousVersionNo = versionNo - 1; return Promise.all([ this.fetchSnapshotDetail(row.id), - this.fetchSnapshotDetailByVersion(previousVersionNo) + this.fetchPreviousSnapshotDetail(row) ]).then(([selectedSnapshot, previousSnapshot]) => ({ ...selectedSnapshot, versionNo, changedFields: previousSnapshot.changedFields || [], snapshotData: previousSnapshot.snapshotData || {}, afterSnapshotData: selectedSnapshot.snapshotData || {}, - beforeVersionNo: previousVersionNo, - afterVersionNo: versionNo + beforeVersionNo: previousSnapshot.versionNo, + afterVersionNo: versionNo, + fieldOrder: selectedSnapshot.fieldOrder || previousSnapshot.fieldOrder || row.fieldOrder || [] })); }, + fetchPreviousSnapshotDetail(row) { + if (row?.previousSnapshotId) { + return this.fetchSnapshotDetail(row.previousSnapshotId); + } + if (row?.previousVersionNo) { + return this.fetchSnapshotDetailByVersion(row.previousVersionNo); + } + return this.fetchSnapshotRowBeforeVersion(row?.versionNo).then((previousRow) => { + if (!previousRow?.id) { + return Promise.reject(new Error("previous snapshot not found")); + } + return this.fetchSnapshotDetail(previousRow.id); + }); + }, fetchSnapshotDetailByVersion(versionNo) { return this.fetchSnapshotRowByVersion(versionNo).then((row) => { if (!row?.id) { @@ -867,6 +1042,17 @@ export default { }); }); }, + fetchSnapshotRowBeforeVersion(versionNo) { + const cached = this.findPreviousSnapshotRow({ versionNo }, this.snapshots); + if (cached) { + return Promise.resolve(cached); + } + + return this.fetchSnapshotRows({ + page: "1", + limit: String(Math.max(this.historyTotal, this.limit, 1)) + }).then((data) => this.findPreviousSnapshotRow({ versionNo }, data.list || [])); + }, ensurePluginMetadata() { if (this.pluginMetadataLoaded) { return Promise.resolve(); @@ -927,7 +1113,7 @@ export default { this.modelMetadataLoading = Promise.all(fetchers) .then((groups) => { - const metadata = { ...FALLBACK_MODEL_NAMES }; + const metadata = {}; groups.flat().forEach((item) => { if (item.id) { metadata[item.id] = item.name || item.id; @@ -1166,13 +1352,13 @@ export default { const name = this.functionDisplayName(pluginId); const statusMap = { added: { - badge: "已开启" + badge: this.$t("agentSnapshot.function.added") }, removed: { - badge: "已关闭" + badge: this.$t("agentSnapshot.function.removed") }, updated: { - badge: "参数变更" + badge: this.$t("agentSnapshot.function.updated") } }; return { @@ -1188,18 +1374,18 @@ export default { if (!func) { return { active: false, - status: "未开启", + status: this.$t("agentSnapshot.function.disabled"), params: [], - note: "未开启此功能" + note: this.$t("agentSnapshot.function.disabledNote") }; } const params = this.functionParamRows(pluginId, func.params, changedParamKeys); return { active: true, - status: "已开启", + status: this.$t("agentSnapshot.function.enabled"), params, - note: params.length ? "" : "无需配置参数" + note: params.length ? "" : this.$t("agentSnapshot.function.noParams") }; }, changedFunctionParamKeys(pluginId, beforeParams, afterParams) { @@ -1235,11 +1421,12 @@ export default { }, functionDisplayName(pluginId) { const meta = this.resolveFunctionMeta(pluginId); - return meta?.name || FALLBACK_PLUGIN_NAMES[pluginId] || pluginId || this.$t("agentSnapshot.emptyValue"); + return meta?.name || this.translateKey(FALLBACK_PLUGIN_NAME_KEYS[pluginId], pluginId) + || pluginId || this.$t("agentSnapshot.emptyValue"); }, functionParamLabel(pluginId, key) { const field = this.functionFields(pluginId).find((item) => item.key === key); - return field?.label || FALLBACK_FIELD_LABELS[key] || key; + return field?.label || this.translateKey(FALLBACK_FIELD_LABEL_KEYS[key], key) || key; }, functionFields(pluginId) { const meta = this.resolveFunctionMeta(pluginId); @@ -1273,7 +1460,7 @@ export default { const lines = String(value).replace(/\r\n/g, "\n").split("\n"); const html = []; let paragraph = []; - let listType = null; + const listStack = []; const closeParagraph = () => { if (paragraph.length) { @@ -1281,59 +1468,89 @@ export default { paragraph = []; } }; - const closeList = () => { - if (listType) { - html.push(``); - listType = null; + const closeOneList = () => { + const list = listStack.pop(); + if (!list) { + return; + } + if (list.liOpen) { + html.push(""); + } + html.push(``); + }; + const closeLists = () => { + while (listStack.length) { + closeOneList(); } }; - const openList = (type) => { - if (listType !== type) { - closeList(); - html.push(`<${type}>`); - listType = type; + const openList = (type, indent) => { + html.push(`<${type}>`); + listStack.push({ type, indent, liOpen: false }); + }; + const alignList = (type, indent) => { + while (listStack.length && indent < listStack[listStack.length - 1].indent) { + closeOneList(); } + + let current = listStack[listStack.length - 1]; + if (current && indent === current.indent && type !== current.type) { + closeOneList(); + } + + current = listStack[listStack.length - 1]; + if (!current || indent > current.indent || indent !== current.indent) { + openList(type, indent); + } + }; + const addListItem = (type, indent, text) => { + closeParagraph(); + alignList(type, indent); + const current = listStack[listStack.length - 1]; + if (current.liOpen) { + html.push(""); + } + html.push(`
  • ${this.renderInlineMarkdown(text)}`); + current.liOpen = true; }; lines.forEach((rawLine) => { - const line = rawLine.trim(); + const normalizedLine = rawLine.replace(/\t/g, " "); + const line = normalizedLine.trim(); if (!line) { closeParagraph(); - closeList(); + closeLists(); return; } const heading = line.match(/^(#{1,6})\s+(.+)$/); if (heading) { closeParagraph(); - closeList(); + closeLists(); const level = Math.min(heading[1].length, 4); html.push(`${this.renderInlineMarkdown(heading[2])}`); return; } - const unordered = line.match(/^[-*+]\s+(.+)$/); + const indent = normalizedLine.match(/^ */)[0].length; + const listContent = normalizedLine.slice(indent); + const unordered = listContent.match(/^[-*+]\s+(.+)$/); if (unordered) { - closeParagraph(); - openList("ul"); - html.push(`
  • ${this.renderInlineMarkdown(unordered[1])}
  • `); + addListItem("ul", indent, unordered[1]); return; } - const ordered = line.match(/^\d+\.\s+(.+)$/); + const ordered = listContent.match(/^\d+\.\s+(.+)$/); if (ordered) { - closeParagraph(); - openList("ol"); - html.push(`
  • ${this.renderInlineMarkdown(ordered[1])}
  • `); + addListItem("ol", indent, ordered[1]); return; } - closeList(); + closeLists(); paragraph.push(line); }); closeParagraph(); - closeList(); + closeLists(); return html.join(""); }, renderInlineMarkdown(value) { @@ -1351,13 +1568,19 @@ export default { .replace(/"/g, """) .replace(/'/g, "'"); }, - resolveDiffFields(beforeData, afterData, changedFields) { - const directFields = changedFields + restoreFailedMessage(data) { + return data?.msg + ? `${this.$t("agentSnapshot.restoreFailedRollback")}: ${data.msg}` + : this.$t("agentSnapshot.restoreFailedRollback"); + }, + resolveDiffFields(beforeData, afterData, changedFields = [], fieldOrder = []) { + const safeChangedFields = Array.isArray(changedFields) ? changedFields : []; + const directFields = safeChangedFields .filter((field) => field && field !== "restore") .map((field) => this.canonicalField(field)); - const candidates = directFields.length > 0 && !changedFields.includes("restore") + const candidates = directFields.length > 0 && !safeChangedFields.includes("restore") ? directFields - : this.snapshotFieldOrder(); + : this.snapshotFieldOrder(fieldOrder, beforeData, afterData); return Array.from(new Set(candidates)).filter((field) => { return !this.isSameValue( @@ -1366,38 +1589,19 @@ export default { ); }); }, - snapshotFieldOrder() { - return [ - "agentCode", - "agentName", - "asrModelId", - "vadModelId", - "llmModelId", - "slmModelId", - "vllmModelId", - "ttsModelId", - "ttsVoiceId", - "ttsLanguage", - "ttsVolume", - "ttsRate", - "ttsPitch", - "memModelId", - "intentModelId", - "chatHistoryConf", - "systemPrompt", - "summaryMemory", - "langCode", - "language", - "sort", - "functions", - "contextProviders", - "correctWordFileIds", - "tagNames", - "tagIds" - ]; + snapshotFieldOrder(fieldOrder = [], beforeData = {}, afterData = {}) { + const backendOrder = Array.isArray(fieldOrder) ? fieldOrder : []; + if (backendOrder.length) { + return backendOrder.map((field) => this.canonicalField(field)); + } + const dataFields = [ + ...Object.keys(beforeData || {}), + ...Object.keys(afterData || {}) + ].map((field) => this.canonicalField(field)); + return Array.from(new Set(dataFields)).filter((field) => field !== "tags"); }, canonicalField(field) { - return field === "tags" ? "tagNames" : field; + return field === "tags" || field === "tagIds" ? "tagNames" : field; }, getFieldValue(data, field) { if (!data) { @@ -1434,7 +1638,8 @@ export default { return this.voiceDisplayName(data.ttsModelId, value); } if (field === "chatHistoryConf") { - return CHAT_HISTORY_CONF_LABELS[value] || CHAT_HISTORY_CONF_LABELS[Number(value)] || this.formatValue(value); + return this.translateKey(CHAT_HISTORY_CONF_LABEL_KEYS[value] || CHAT_HISTORY_CONF_LABEL_KEYS[Number(value)]) + || this.formatValue(value); } return this.formatValue(value); }, @@ -1442,13 +1647,14 @@ export default { if (value === null || value === undefined || value === "") { return this.$t("agentSnapshot.emptyValue"); } - return this.modelNameMap[value] || FALLBACK_MODEL_NAMES[value] || String(value); + return this.modelNameMap[value] || this.translateKey(FALLBACK_MODEL_NAME_KEYS[value], String(value)) || String(value); }, voiceDisplayName(modelId, value) { if (value === null || value === undefined || value === "") { return this.$t("agentSnapshot.emptyValue"); } - return this.voiceNameMap[`${modelId}:${value}`] || this.voiceNameMap[value] || String(value); + return this.voiceNameMap[`${modelId}:${value}`] || this.voiceNameMap[value] + || this.translateKey(FALLBACK_VOICE_NAME_KEYS[value], String(value)) || String(value); }, formatValue(value) { if (value === null || value === undefined || value === "") { @@ -1481,6 +1687,9 @@ export default { fieldLabel(field) { const key = `agentSnapshot.field.${field}`; return this.$te && this.$te(key) ? this.$t(key) : field; + }, + translateKey(key, fallback = "") { + return key && this.$te && this.$te(key) ? this.$t(key) : fallback; } } }; @@ -1540,6 +1749,10 @@ export default { background: #fbfffa; } +.snapshot-delete-button { + color: #f56c6c; +} + .field-tags { display: flex; flex-wrap: wrap; @@ -1564,6 +1777,10 @@ export default { @include scrollbar-style; } +.restore-risk-alert { + margin-top: 14px; +} + .detail-summary { display: flex; flex-wrap: wrap; diff --git a/main/manager-web/src/i18n/de.js b/main/manager-web/src/i18n/de.js index d1a42bb9..414a3283 100644 --- a/main/manager-web/src/i18n/de.js +++ b/main/manager-web/src/i18n/de.js @@ -806,12 +806,126 @@ export default { 'roleConfig.addTag': 'Neues Label hinzufügen', 'roleConfig.restartNotice': 'Neustart nach Konfigurationsspeichern zur Aktivierung.', 'roleConfig.saveConfig': 'Konfiguration speichern', + 'roleConfig.snapshotHistory': 'Versionsverlauf', + 'roleConfig.currentVersion': 'Aktuelle Version {version}', + 'roleConfig.functionName': 'Funktionsname', 'roleConfig.reset': 'Zurücksetzen', 'roleConfig.agentName': 'Spitzname', 'roleConfig.roleTemplate': 'Vorlage', 'roleConfig.roleIntroduction': 'Einführung', 'roleConfig.languageCode': 'Sprachcode', 'roleConfig.interactionLanguage': 'Interaktionssprache', + 'agentSnapshot.title': 'Versionsverlauf', + 'agentSnapshot.empty': 'Noch keine Versionen', + 'agentSnapshot.version': 'Version', + 'agentSnapshot.createdAt': 'Gespeichert am', + 'agentSnapshot.source': 'Quelle', + 'agentSnapshot.changedFields': 'Änderungen', + 'agentSnapshot.actions': 'Aktionen', + 'agentSnapshot.view': 'Anzeigen', + 'agentSnapshot.restore': 'Wiederherstellen', + 'agentSnapshot.delete': 'Löschen', + 'agentSnapshot.detailTitle': 'Änderungsdetails', + 'agentSnapshot.restorePreviewTitle': 'Wiederherstellungsvorschau', + 'agentSnapshot.confirmRestore': 'Wiederherstellung bestätigen', + 'agentSnapshot.before': 'Vorher', + 'agentSnapshot.after': 'Nachher', + 'agentSnapshot.emptyValue': 'Keine', + 'agentSnapshot.noChangedContent': 'Keine anzeigbaren Änderungen', + 'agentSnapshot.noFunctionChange': 'Keine Funktionsänderungen', + 'agentSnapshot.beforeChange': 'Vor der Änderung', + 'agentSnapshot.afterChange': 'Nach der Änderung', + 'agentSnapshot.beforeRestore': 'Vor der Wiederherstellung', + 'agentSnapshot.afterRestore': 'Nach der Wiederherstellung', + 'agentSnapshot.restoreMemoryWarning': 'Beim Wiederherstellen einer Version ohne Speicher wird der Chatverlauf dieses Agenten gelöscht. Fahren Sie nur fort, wenn Sie dieses Risiko bestätigt haben.', + 'agentSnapshot.restoreConfirm': 'Version {version} wiederherstellen? Die aktuelle Konfiguration wird zuerst als neue Version gespeichert.', + 'agentSnapshot.restoreSuccess': 'Version wurde wiederhergestellt', + 'agentSnapshot.restoreFailed': 'Version konnte nicht wiederhergestellt werden', + 'agentSnapshot.restoreFailedRollback': 'Version konnte nicht wiederhergestellt werden. Die Transaktion wurde zurueckgerollt und die aktuelle Konfiguration wurde nicht geaendert', + 'agentSnapshot.deleteConfirm': 'Version {version} löschen? Dies kann nicht rückgängig gemacht werden.', + 'agentSnapshot.deleteSuccess': 'Verlaufsversion gelöscht', + 'agentSnapshot.deleteFailed': 'Verlaufsversion konnte nicht gelöscht werden', + 'agentSnapshot.fetchFailed': 'Versionsverlauf konnte nicht abgerufen werden', + 'agentSnapshot.detailFailed': 'Snapshot-Details konnten nicht abgerufen werden', + 'agentSnapshot.currentVersion': 'Aktuelle Version', + 'agentSnapshot.source.config': 'Konfiguration gespeichert', + 'agentSnapshot.source.current': 'Aktuelle Konfiguration', + 'agentSnapshot.source.restore': 'Vor der Wiederherstellung', + 'agentSnapshot.source.initial': 'Initialversion', + 'agentSnapshot.field.restore': 'Wiederherstellung', + 'agentSnapshot.field.initial': 'Initialer Snapshot', + 'agentSnapshot.field.agentCode': 'Agent-Code', + 'agentSnapshot.field.agentName': 'Spitzname', + 'agentSnapshot.field.asrModelId': 'Spracherkennung', + 'agentSnapshot.field.vadModelId': 'Sprachaktivitätserkennung', + 'agentSnapshot.field.llmModelId': 'Hauptsprachmodell', + 'agentSnapshot.field.slmModelId': 'Kleines Sprachmodell', + 'agentSnapshot.field.vllmModelId': 'Vision-Modell', + 'agentSnapshot.field.ttsModelId': 'Text-zu-Sprache', + 'agentSnapshot.field.ttsVoiceId': 'Stimme', + 'agentSnapshot.field.ttsLanguage': 'Sprache', + 'agentSnapshot.field.ttsVolume': 'Lautstärke', + 'agentSnapshot.field.ttsRate': 'Geschwindigkeit', + 'agentSnapshot.field.ttsPitch': 'Tonhöhe', + 'agentSnapshot.field.memModelId': 'Speichermodell', + 'agentSnapshot.field.intentModelId': 'Absichtserkennung', + 'agentSnapshot.field.chatHistoryConf': 'Chatverlauf', + 'agentSnapshot.field.systemPrompt': 'Einführung', + 'agentSnapshot.field.summaryMemory': 'Speicher', + 'agentSnapshot.field.langCode': 'Sprachcode', + 'agentSnapshot.field.language': 'Interaktionssprache', + 'agentSnapshot.field.sort': 'Sortierung', + 'agentSnapshot.field.functions': 'Funktionen', + 'agentSnapshot.field.contextProviders': 'Kontextquellen', + 'agentSnapshot.field.correctWordFileIds': 'Ersatzwörter', + 'agentSnapshot.field.tagNames': 'Labels', + 'agentSnapshot.field.tagIds': 'Label-IDs', + 'agentSnapshot.function.added': 'Aktiviert', + 'agentSnapshot.function.removed': 'Deaktiviert', + 'agentSnapshot.function.updated': 'Parameter geändert', + 'agentSnapshot.function.enabled': 'Aktiviert', + 'agentSnapshot.function.disabled': 'Deaktiviert', + 'agentSnapshot.function.disabledNote': 'Diese Funktion ist deaktiviert', + 'agentSnapshot.function.noParams': 'Keine Parameter erforderlich', + 'agentSnapshot.chatHistoryConf.none': 'Chatverlauf nicht aufzeichnen', + 'agentSnapshot.chatHistoryConf.text': 'Text melden', + 'agentSnapshot.chatHistoryConf.textVoice': 'Text und Sprache melden', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': 'Wetter', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Server-Musikplayer', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'China News', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'NewsNow-Aggregation', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant-Statusabfrage', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant-Statusaktualisierung', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant-Musikplayer', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Websuche', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Geräteanruf', + 'agentSnapshot.pluginField.url': 'Endpunkt-URL', + 'agentSnapshot.pluginField.news_sources': 'Nachrichtenquellen', + 'agentSnapshot.pluginField.default_rss_url': 'Standard-RSS-URL', + 'agentSnapshot.pluginField.society_rss_url': 'RSS-URL für Gesellschaft', + 'agentSnapshot.pluginField.world_rss_url': 'RSS-URL für Welt', + 'agentSnapshot.pluginField.finance_rss_url': 'RSS-URL für Finanzen', + 'agentSnapshot.pluginField.api_key': 'API-Schlüssel', + 'agentSnapshot.pluginField.default_location': 'Standardstadt', + 'agentSnapshot.pluginField.api_host': 'Entwickler-API-Host', + 'agentSnapshot.pluginField.base_url': 'Server-URL', + 'agentSnapshot.pluginField.devices': 'Geräte', + 'agentSnapshot.pluginField.provider': 'Suchanbieter', + 'agentSnapshot.pluginField.description': 'Werkzeugbeschreibung', + 'agentSnapshot.pluginField.max_results': 'Ergebnisanzahl', + 'agentSnapshot.model.Memory_nomem': 'Kein Speicher', + 'agentSnapshot.model.Memory_mem_local_short': 'Lokaler Kurzzeitspeicher', + 'agentSnapshot.model.Memory_mem0ai': 'Mem0AI-Speicher', + 'agentSnapshot.model.Memory_mem_report_only': 'Nur melden', + 'agentSnapshot.model.Intent_nointent': 'Keine Absichtserkennung', + 'agentSnapshot.model.Intent_intent_llm': 'Externe LLM-Absichtserkennung', + 'agentSnapshot.model.Intent_function_call': 'LLM-Funktionsaufruf', + 'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS weiblich - Xiaoxiao', + 'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS männlich - Yunyang', + 'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS weiblich - Xiaoyi', + 'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS männlich - Yunjian', + 'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS männlich - Yunxi', + 'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS männlich - Yunxia', 'roleConfig.vad': 'VAD', 'roleConfig.asr': 'ASR', 'roleConfig.llm': 'Hauptsprachmodell (LLM)', @@ -1554,4 +1668,4 @@ export default { // Header navigation 'header.addressBook': 'Adressbuch', -} \ No newline at end of file +} diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 9c62c087..9f339ffb 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -807,6 +807,8 @@ export default { 'roleConfig.restartNotice': 'After saving the configuration, you need to restart the device for the new configuration to take effect.', 'roleConfig.saveConfig': 'Save Configuration', 'roleConfig.snapshotHistory': 'History', + 'roleConfig.currentVersion': 'Current Version {version}', + 'roleConfig.functionName': 'Function Name', 'roleConfig.reset': 'Reset', 'roleConfig.agentName': 'Nickname', 'roleConfig.roleTemplate': 'Template', @@ -873,6 +875,7 @@ export default { 'agentSnapshot.actions': 'Actions', 'agentSnapshot.view': 'View', 'agentSnapshot.restore': 'Restore', + 'agentSnapshot.delete': 'Delete', 'agentSnapshot.detailTitle': 'Change Details', 'agentSnapshot.restorePreviewTitle': 'Restore Preview', 'agentSnapshot.confirmRestore': 'Confirm Restore', @@ -880,16 +883,28 @@ export default { 'agentSnapshot.after': 'After', 'agentSnapshot.emptyValue': 'None', 'agentSnapshot.noChangedContent': 'No changes to display', - 'agentSnapshot.restoreConfirm': 'Restore to version #{version}? The current configuration will be saved as a new version first.', + 'agentSnapshot.noFunctionChange': 'No function changes', + 'agentSnapshot.beforeChange': 'Before Change', + 'agentSnapshot.afterChange': 'After Change', + 'agentSnapshot.beforeRestore': 'Before Restore', + 'agentSnapshot.afterRestore': 'After Restore', + 'agentSnapshot.restoreMemoryWarning': 'Restoring to a no-memory version will clear this agent\'s chat history. Continue only after confirming this risk.', + 'agentSnapshot.restoreConfirm': 'Restore to version {version}? The current configuration will be saved as a new version first.', 'agentSnapshot.restoreSuccess': 'Version restored', 'agentSnapshot.restoreFailed': 'Failed to restore version', + 'agentSnapshot.restoreFailedRollback': 'Failed to restore version. The transaction was rolled back and the current configuration was not changed', + 'agentSnapshot.deleteConfirm': 'Delete version {version}? This cannot be undone.', + 'agentSnapshot.deleteSuccess': 'History version deleted', + 'agentSnapshot.deleteFailed': 'Failed to delete history version', 'agentSnapshot.fetchFailed': 'Failed to fetch versions', 'agentSnapshot.detailFailed': 'Failed to fetch snapshot details', 'agentSnapshot.currentVersion': 'Current Version', 'agentSnapshot.source.config': 'Config Save', 'agentSnapshot.source.current': 'Current Config', 'agentSnapshot.source.restore': 'Before Restore', + 'agentSnapshot.source.initial': 'Initial Version', 'agentSnapshot.field.restore': 'Restore', + 'agentSnapshot.field.initial': 'Initial Snapshot', 'agentSnapshot.field.agentCode': 'Agent Code', 'agentSnapshot.field.agentName': 'Nickname', 'agentSnapshot.field.asrModelId': 'Speech Recognition', @@ -916,6 +931,52 @@ export default { 'agentSnapshot.field.correctWordFileIds': 'Replacement Words', 'agentSnapshot.field.tagNames': 'Tags', 'agentSnapshot.field.tagIds': 'Tag IDs', + 'agentSnapshot.function.added': 'Enabled', + 'agentSnapshot.function.removed': 'Disabled', + 'agentSnapshot.function.updated': 'Parameters changed', + 'agentSnapshot.function.enabled': 'Enabled', + 'agentSnapshot.function.disabled': 'Disabled', + 'agentSnapshot.function.disabledNote': 'This function is disabled', + 'agentSnapshot.function.noParams': 'No parameters required', + 'agentSnapshot.chatHistoryConf.none': 'Do not record chat history', + 'agentSnapshot.chatHistoryConf.text': 'Report text', + 'agentSnapshot.chatHistoryConf.textVoice': 'Report text and voice', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': 'Weather', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Server music player', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'China News', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'NewsNow aggregation', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant state query', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant state update', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant music player', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Web search', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Device-to-device call', + 'agentSnapshot.pluginField.url': 'Endpoint URL', + 'agentSnapshot.pluginField.news_sources': 'News sources', + 'agentSnapshot.pluginField.default_rss_url': 'Default RSS URL', + 'agentSnapshot.pluginField.society_rss_url': 'Society RSS URL', + 'agentSnapshot.pluginField.world_rss_url': 'World RSS URL', + 'agentSnapshot.pluginField.finance_rss_url': 'Finance RSS URL', + 'agentSnapshot.pluginField.api_key': 'API key', + 'agentSnapshot.pluginField.default_location': 'Default city', + 'agentSnapshot.pluginField.api_host': 'Developer API host', + 'agentSnapshot.pluginField.base_url': 'Server URL', + 'agentSnapshot.pluginField.devices': 'Devices', + 'agentSnapshot.pluginField.provider': 'Search provider', + 'agentSnapshot.pluginField.description': 'Tool description', + 'agentSnapshot.pluginField.max_results': 'Result count', + 'agentSnapshot.model.Memory_nomem': 'No memory', + 'agentSnapshot.model.Memory_mem_local_short': 'Local short memory', + 'agentSnapshot.model.Memory_mem0ai': 'Mem0AI memory', + 'agentSnapshot.model.Memory_mem_report_only': 'Report only', + 'agentSnapshot.model.Intent_nointent': 'No intent recognition', + 'agentSnapshot.model.Intent_intent_llm': 'External LLM intent recognition', + 'agentSnapshot.model.Intent_function_call': 'LLM function calling', + 'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS female - Xiaoxiao', + 'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS male - Yunyang', + 'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS female - Xiaoyi', + 'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS male - Yunjian', + 'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS male - Yunxi', + 'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS male - Yunxia', // Form field Tooltip descriptions 'roleConfig.tooltip.agentName': 'Set the name of your AI agent for identification and recognition', diff --git a/main/manager-web/src/i18n/pt_BR.js b/main/manager-web/src/i18n/pt_BR.js index 90e9fc87..8db61171 100644 --- a/main/manager-web/src/i18n/pt_BR.js +++ b/main/manager-web/src/i18n/pt_BR.js @@ -806,12 +806,126 @@ export default { 'roleConfig.addTag': 'Adicionar Novo Rótulo', 'roleConfig.restartNotice': 'Reinicie após salvar a configuração para ativação.', 'roleConfig.saveConfig': 'Salvar Configuração', + 'roleConfig.snapshotHistory': 'Histórico de Versões', + 'roleConfig.currentVersion': 'Versão Atual {version}', + 'roleConfig.functionName': 'Nome da Função', 'roleConfig.reset': 'Redefinir', 'roleConfig.agentName': 'Apelido', 'roleConfig.roleTemplate': 'Modelo', 'roleConfig.roleIntroduction': 'Introdução', 'roleConfig.languageCode': 'Código do Idioma', 'roleConfig.interactionLanguage': 'Idioma de Interação', + 'agentSnapshot.title': 'Histórico de Versões', + 'agentSnapshot.empty': 'Ainda sem versões', + 'agentSnapshot.version': 'Versão', + 'agentSnapshot.createdAt': 'Salvo em', + 'agentSnapshot.source': 'Origem', + 'agentSnapshot.changedFields': 'Alterações', + 'agentSnapshot.actions': 'Ações', + 'agentSnapshot.view': 'Ver', + 'agentSnapshot.restore': 'Restaurar', + 'agentSnapshot.delete': 'Excluir', + 'agentSnapshot.detailTitle': 'Detalhes da Alteração', + 'agentSnapshot.restorePreviewTitle': 'Prévia da Restauração', + 'agentSnapshot.confirmRestore': 'Confirmar Restauração', + 'agentSnapshot.before': 'Antes', + 'agentSnapshot.after': 'Depois', + 'agentSnapshot.emptyValue': 'Nenhum', + 'agentSnapshot.noChangedContent': 'Nenhuma alteração para exibir', + 'agentSnapshot.noFunctionChange': 'Sem alterações de função', + 'agentSnapshot.beforeChange': 'Antes da Alteração', + 'agentSnapshot.afterChange': 'Depois da Alteração', + 'agentSnapshot.beforeRestore': 'Antes da Restauração', + 'agentSnapshot.afterRestore': 'Depois da Restauração', + 'agentSnapshot.restoreMemoryWarning': 'Restaurar para uma versão sem memória apagará o histórico de conversa deste agente. Continue somente após confirmar esse risco.', + 'agentSnapshot.restoreConfirm': 'Restaurar para a versão {version}? A configuração atual será salva primeiro como uma nova versão.', + 'agentSnapshot.restoreSuccess': 'Versão restaurada', + 'agentSnapshot.restoreFailed': 'Falha ao restaurar versão', + 'agentSnapshot.restoreFailedRollback': 'Falha ao restaurar versão. A transação foi revertida e a configuração atual não foi alterada', + 'agentSnapshot.deleteConfirm': 'Excluir a versão {version}? Esta ação não pode ser desfeita.', + 'agentSnapshot.deleteSuccess': 'Versão do histórico excluída', + 'agentSnapshot.deleteFailed': 'Falha ao excluir a versão do histórico', + 'agentSnapshot.fetchFailed': 'Falha ao buscar versões', + 'agentSnapshot.detailFailed': 'Falha ao buscar detalhes do snapshot', + 'agentSnapshot.currentVersion': 'Versão Atual', + 'agentSnapshot.source.config': 'Configuração Salva', + 'agentSnapshot.source.current': 'Configuração Atual', + 'agentSnapshot.source.restore': 'Antes da Restauração', + 'agentSnapshot.source.initial': 'Versão inicial', + 'agentSnapshot.field.restore': 'Restauração', + 'agentSnapshot.field.initial': 'Snapshot inicial', + 'agentSnapshot.field.agentCode': 'Código do Agente', + 'agentSnapshot.field.agentName': 'Apelido', + 'agentSnapshot.field.asrModelId': 'Reconhecimento de Fala', + 'agentSnapshot.field.vadModelId': 'Detecção de Atividade de Voz', + 'agentSnapshot.field.llmModelId': 'Modelo de Linguagem Principal', + 'agentSnapshot.field.slmModelId': 'Modelo de Linguagem Pequeno', + 'agentSnapshot.field.vllmModelId': 'Modelo de Visão', + 'agentSnapshot.field.ttsModelId': 'Texto para Fala', + 'agentSnapshot.field.ttsVoiceId': 'Voz', + 'agentSnapshot.field.ttsLanguage': 'Idioma', + 'agentSnapshot.field.ttsVolume': 'Volume', + 'agentSnapshot.field.ttsRate': 'Velocidade', + 'agentSnapshot.field.ttsPitch': 'Tom', + 'agentSnapshot.field.memModelId': 'Modelo de Memória', + 'agentSnapshot.field.intentModelId': 'Reconhecimento de Intenção', + 'agentSnapshot.field.chatHistoryConf': 'Histórico de Conversa', + 'agentSnapshot.field.systemPrompt': 'Introdução', + 'agentSnapshot.field.summaryMemory': 'Memória', + 'agentSnapshot.field.langCode': 'Código do Idioma', + 'agentSnapshot.field.language': 'Idioma de Interação', + 'agentSnapshot.field.sort': 'Ordenação', + 'agentSnapshot.field.functions': 'Funções', + 'agentSnapshot.field.contextProviders': 'Provedores de Contexto', + 'agentSnapshot.field.correctWordFileIds': 'Palavras de Substituição', + 'agentSnapshot.field.tagNames': 'Rótulos', + 'agentSnapshot.field.tagIds': 'IDs dos Rótulos', + 'agentSnapshot.function.added': 'Ativado', + 'agentSnapshot.function.removed': 'Desativado', + 'agentSnapshot.function.updated': 'Parâmetros alterados', + 'agentSnapshot.function.enabled': 'Ativado', + 'agentSnapshot.function.disabled': 'Desativado', + 'agentSnapshot.function.disabledNote': 'Esta função está desativada', + 'agentSnapshot.function.noParams': 'Nenhum parâmetro necessário', + 'agentSnapshot.chatHistoryConf.none': 'Não registrar histórico de conversa', + 'agentSnapshot.chatHistoryConf.text': 'Enviar texto', + 'agentSnapshot.chatHistoryConf.textVoice': 'Enviar texto e voz', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': 'Clima', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Reprodutor de música do servidor', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'China News', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'Agregação NewsNow', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'Consulta de estado do HomeAssistant', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'Atualização de estado do HomeAssistant', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'Reprodutor de música do HomeAssistant', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Pesquisa na web', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Chamada entre dispositivos', + 'agentSnapshot.pluginField.url': 'URL do endpoint', + 'agentSnapshot.pluginField.news_sources': 'Fontes de notícias', + 'agentSnapshot.pluginField.default_rss_url': 'URL RSS padrão', + 'agentSnapshot.pluginField.society_rss_url': 'URL RSS de sociedade', + 'agentSnapshot.pluginField.world_rss_url': 'URL RSS mundial', + 'agentSnapshot.pluginField.finance_rss_url': 'URL RSS financeiro', + 'agentSnapshot.pluginField.api_key': 'Chave de API', + 'agentSnapshot.pluginField.default_location': 'Cidade padrão', + 'agentSnapshot.pluginField.api_host': 'Host da API de desenvolvedor', + 'agentSnapshot.pluginField.base_url': 'URL do servidor', + 'agentSnapshot.pluginField.devices': 'Dispositivos', + 'agentSnapshot.pluginField.provider': 'Provedor de pesquisa', + 'agentSnapshot.pluginField.description': 'Descrição da ferramenta', + 'agentSnapshot.pluginField.max_results': 'Quantidade de resultados', + 'agentSnapshot.model.Memory_nomem': 'Sem memória', + 'agentSnapshot.model.Memory_mem_local_short': 'Memória curta local', + 'agentSnapshot.model.Memory_mem0ai': 'Memória Mem0AI', + 'agentSnapshot.model.Memory_mem_report_only': 'Somente relatório', + 'agentSnapshot.model.Intent_nointent': 'Sem reconhecimento de intenção', + 'agentSnapshot.model.Intent_intent_llm': 'Reconhecimento de intenção por LLM externo', + 'agentSnapshot.model.Intent_function_call': 'Chamada de função por LLM', + 'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS feminino - Xiaoxiao', + 'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS masculino - Yunyang', + 'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS feminino - Xiaoyi', + 'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS masculino - Yunjian', + 'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS masculino - Yunxi', + 'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS masculino - Yunxia', 'roleConfig.vad': 'Detecção de Voz', 'roleConfig.asr': 'Reconhecimento de Fala', 'roleConfig.llm': 'Modelo de Linguagem Principal', @@ -1554,4 +1668,4 @@ export default { // Header navigation 'header.addressBook': 'Lista de Contatos', -} \ No newline at end of file +} diff --git a/main/manager-web/src/i18n/vi.js b/main/manager-web/src/i18n/vi.js index b58023aa..f78a0c64 100644 --- a/main/manager-web/src/i18n/vi.js +++ b/main/manager-web/src/i18n/vi.js @@ -806,12 +806,126 @@ export default { 'roleConfig.addTag': 'Thêm mới nhãn', 'roleConfig.restartNotice': 'Sau khi lưu cấu hình, bạn cần khởi động lại thiết bị để cấu hình mới có hiệu lực.', 'roleConfig.saveConfig': 'Lưu cấu hình', + 'roleConfig.snapshotHistory': 'Lịch sử phiên bản', + 'roleConfig.currentVersion': 'Phiên bản hiện tại {version}', + 'roleConfig.functionName': 'Tên chức năng', 'roleConfig.reset': 'Đặt lại', 'roleConfig.agentName': 'Biệt danh', 'roleConfig.roleTemplate': 'Mẫu', 'roleConfig.roleIntroduction': 'Giới thiệu', 'roleConfig.languageCode': 'Mã ngôn ngữ', 'roleConfig.interactionLanguage': 'Ngôn ngữ tương tác', + 'agentSnapshot.title': 'Lịch sử phiên bản', + 'agentSnapshot.empty': 'Chưa có phiên bản', + 'agentSnapshot.version': 'Phiên bản', + 'agentSnapshot.createdAt': 'Lưu lúc', + 'agentSnapshot.source': 'Nguồn', + 'agentSnapshot.changedFields': 'Thay đổi', + 'agentSnapshot.actions': 'Thao tác', + 'agentSnapshot.view': 'Xem', + 'agentSnapshot.restore': 'Khôi phục', + 'agentSnapshot.delete': 'Xóa', + 'agentSnapshot.detailTitle': 'Chi tiết thay đổi', + 'agentSnapshot.restorePreviewTitle': 'Xem trước khôi phục', + 'agentSnapshot.confirmRestore': 'Xác nhận khôi phục', + 'agentSnapshot.before': 'Trước', + 'agentSnapshot.after': 'Sau', + 'agentSnapshot.emptyValue': 'Không có', + 'agentSnapshot.noChangedContent': 'Không có thay đổi để hiển thị', + 'agentSnapshot.noFunctionChange': 'Không có thay đổi chức năng', + 'agentSnapshot.beforeChange': 'Trước thay đổi', + 'agentSnapshot.afterChange': 'Sau thay đổi', + 'agentSnapshot.beforeRestore': 'Trước khi khôi phục', + 'agentSnapshot.afterRestore': 'Sau khi khôi phục', + 'agentSnapshot.restoreMemoryWarning': 'Khôi phục về phiên bản không có bộ nhớ sẽ xóa lịch sử trò chuyện của tác nhân này. Chỉ tiếp tục sau khi xác nhận rủi ro này.', + 'agentSnapshot.restoreConfirm': 'Khôi phục về phiên bản {version}? Cấu hình hiện tại sẽ được lưu thành phiên bản mới trước.', + 'agentSnapshot.restoreSuccess': 'Đã khôi phục phiên bản', + 'agentSnapshot.restoreFailed': 'Khôi phục phiên bản thất bại', + 'agentSnapshot.restoreFailedRollback': 'Khôi phục phiên bản thất bại. Giao dịch đã được hoàn tác và cấu hình hiện tại chưa thay đổi', + 'agentSnapshot.deleteConfirm': 'Xóa phiên bản {version}? Không thể hoàn tác thao tác này.', + 'agentSnapshot.deleteSuccess': 'Đã xóa phiên bản lịch sử', + 'agentSnapshot.deleteFailed': 'Xóa phiên bản lịch sử thất bại', + 'agentSnapshot.fetchFailed': 'Không thể tải lịch sử phiên bản', + 'agentSnapshot.detailFailed': 'Không thể tải chi tiết snapshot', + 'agentSnapshot.currentVersion': 'Phiên bản hiện tại', + 'agentSnapshot.source.config': 'Lưu cấu hình', + 'agentSnapshot.source.current': 'Cấu hình hiện tại', + 'agentSnapshot.source.restore': 'Trước khi khôi phục', + 'agentSnapshot.source.initial': 'Phiên bản ban đầu', + 'agentSnapshot.field.restore': 'Khôi phục', + 'agentSnapshot.field.initial': 'Snapshot ban đầu', + 'agentSnapshot.field.agentCode': 'Mã tác nhân', + 'agentSnapshot.field.agentName': 'Biệt danh', + 'agentSnapshot.field.asrModelId': 'Nhận dạng giọng nói', + 'agentSnapshot.field.vadModelId': 'Phát hiện hoạt động giọng nói', + 'agentSnapshot.field.llmModelId': 'Mô hình ngôn ngữ chính', + 'agentSnapshot.field.slmModelId': 'Mô hình ngôn ngữ nhỏ', + 'agentSnapshot.field.vllmModelId': 'Mô hình thị giác', + 'agentSnapshot.field.ttsModelId': 'Văn bản thành giọng nói', + 'agentSnapshot.field.ttsVoiceId': 'Giọng nói', + 'agentSnapshot.field.ttsLanguage': 'Ngôn ngữ', + 'agentSnapshot.field.ttsVolume': 'Âm lượng', + 'agentSnapshot.field.ttsRate': 'Tốc độ', + 'agentSnapshot.field.ttsPitch': 'Cao độ', + 'agentSnapshot.field.memModelId': 'Mô hình bộ nhớ', + 'agentSnapshot.field.intentModelId': 'Nhận dạng ý định', + 'agentSnapshot.field.chatHistoryConf': 'Lịch sử trò chuyện', + 'agentSnapshot.field.systemPrompt': 'Giới thiệu', + 'agentSnapshot.field.summaryMemory': 'Bộ nhớ', + 'agentSnapshot.field.langCode': 'Mã ngôn ngữ', + 'agentSnapshot.field.language': 'Ngôn ngữ tương tác', + 'agentSnapshot.field.sort': 'Sắp xếp', + 'agentSnapshot.field.functions': 'Chức năng', + 'agentSnapshot.field.contextProviders': 'Nguồn ngữ cảnh', + 'agentSnapshot.field.correctWordFileIds': 'Từ thay thế', + 'agentSnapshot.field.tagNames': 'Nhãn', + 'agentSnapshot.field.tagIds': 'ID nhãn', + 'agentSnapshot.function.added': 'Đã bật', + 'agentSnapshot.function.removed': 'Đã tắt', + 'agentSnapshot.function.updated': 'Tham số đã thay đổi', + 'agentSnapshot.function.enabled': 'Đã bật', + 'agentSnapshot.function.disabled': 'Đã tắt', + 'agentSnapshot.function.disabledNote': 'Chức năng này đang tắt', + 'agentSnapshot.function.noParams': 'Không cần cấu hình tham số', + 'agentSnapshot.chatHistoryConf.none': 'Không ghi lịch sử trò chuyện', + 'agentSnapshot.chatHistoryConf.text': 'Báo cáo văn bản', + 'agentSnapshot.chatHistoryConf.textVoice': 'Báo cáo văn bản và giọng nói', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': 'Thời tiết', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Trình phát nhạc máy chủ', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'Tin tức China News', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'Tổng hợp NewsNow', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'Truy vấn trạng thái HomeAssistant', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'Cập nhật trạng thái HomeAssistant', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'Trình phát nhạc HomeAssistant', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Tìm kiếm web', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Gọi thiết bị', + 'agentSnapshot.pluginField.url': 'URL điểm cuối', + 'agentSnapshot.pluginField.news_sources': 'Nguồn tin', + 'agentSnapshot.pluginField.default_rss_url': 'URL RSS mặc định', + 'agentSnapshot.pluginField.society_rss_url': 'URL RSS xã hội', + 'agentSnapshot.pluginField.world_rss_url': 'URL RSS thế giới', + 'agentSnapshot.pluginField.finance_rss_url': 'URL RSS tài chính', + 'agentSnapshot.pluginField.api_key': 'Khóa API', + 'agentSnapshot.pluginField.default_location': 'Thành phố mặc định', + 'agentSnapshot.pluginField.api_host': 'Máy chủ API nhà phát triển', + 'agentSnapshot.pluginField.base_url': 'URL máy chủ', + 'agentSnapshot.pluginField.devices': 'Thiết bị', + 'agentSnapshot.pluginField.provider': 'Nhà cung cấp tìm kiếm', + 'agentSnapshot.pluginField.description': 'Mô tả công cụ', + 'agentSnapshot.pluginField.max_results': 'Số lượng kết quả', + 'agentSnapshot.model.Memory_nomem': 'Không có bộ nhớ', + 'agentSnapshot.model.Memory_mem_local_short': 'Bộ nhớ ngắn cục bộ', + 'agentSnapshot.model.Memory_mem0ai': 'Bộ nhớ Mem0AI', + 'agentSnapshot.model.Memory_mem_report_only': 'Chỉ báo cáo bộ nhớ', + 'agentSnapshot.model.Intent_nointent': 'Không nhận dạng ý định', + 'agentSnapshot.model.Intent_intent_llm': 'Nhận dạng ý định bằng LLM bên ngoài', + 'agentSnapshot.model.Intent_function_call': 'LLM gọi hàm', + 'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS nữ - Xiaoxiao', + 'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS nam - Yunyang', + 'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS nữ - Xiaoyi', + 'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS nam - Yunjian', + 'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS nam - Yunxi', + 'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS nam - Yunxia', 'roleConfig.vad': 'Phát hiện giọng nói', 'roleConfig.asr': 'Nhận dạng giọng nói', 'roleConfig.llm': 'Mô hình ngôn ngữ chính', @@ -1554,4 +1668,4 @@ export default { // Header navigation 'header.addressBook': 'Danh bạ', -} \ No newline at end of file +} diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 71c28d3e..285db53c 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -807,6 +807,8 @@ export default { 'roleConfig.restartNotice': '保存配置后,需要重启设备,新的配置才会生效。', 'roleConfig.saveConfig': '保存配置', 'roleConfig.snapshotHistory': '历史版本', + 'roleConfig.currentVersion': '当前版本 {version}', + 'roleConfig.functionName': '功能名称', 'roleConfig.reset': '重置', 'roleConfig.agentName': '助手昵称', 'roleConfig.roleTemplate': '角色模版', @@ -816,7 +818,7 @@ export default { 'roleConfig.vad': '语音活动检测(VAD)', 'roleConfig.asr': '语音识别(ASR)', 'roleConfig.llm': '主语言模型(LLM)', - "roleConfig.slm": "小参数模型(SLM)", + 'roleConfig.slm': '小参数模型(SLM)', 'roleConfig.vllm': '视觉大模型(VLLM)', 'roleConfig.intent': '意图识别(Intent)', 'roleConfig.memoryHis': '记忆', @@ -873,6 +875,7 @@ export default { 'agentSnapshot.actions': '操作', 'agentSnapshot.view': '查看', 'agentSnapshot.restore': '恢复', + 'agentSnapshot.delete': '删除', 'agentSnapshot.detailTitle': '变更详情', 'agentSnapshot.restorePreviewTitle': '恢复确认', 'agentSnapshot.confirmRestore': '确认恢复', @@ -880,16 +883,28 @@ export default { 'agentSnapshot.after': '变化后', 'agentSnapshot.emptyValue': '无', 'agentSnapshot.noChangedContent': '暂无可展示的变更内容', - 'agentSnapshot.restoreConfirm': '确定恢复到版本 #{version} 吗?当前配置会先自动保存为新的历史版本。', + 'agentSnapshot.noFunctionChange': '功能配置无变化', + 'agentSnapshot.beforeChange': '变化前', + 'agentSnapshot.afterChange': '变化后', + 'agentSnapshot.beforeRestore': '恢复前', + 'agentSnapshot.afterRestore': '恢复后', + 'agentSnapshot.restoreMemoryWarning': '恢复到无记忆版本会清空该智能体的聊天记录,请确认后再继续。', + 'agentSnapshot.restoreConfirm': '确定恢复到版本 {version} 吗?当前配置会先自动保存为新的历史版本。', 'agentSnapshot.restoreSuccess': '已恢复历史版本', 'agentSnapshot.restoreFailed': '恢复历史版本失败', + 'agentSnapshot.restoreFailedRollback': '恢复历史版本失败,事务已回滚,当前配置未改变', + 'agentSnapshot.deleteConfirm': '确定删除版本 {version} 吗?删除后不可恢复。', + 'agentSnapshot.deleteSuccess': '已删除历史版本', + 'agentSnapshot.deleteFailed': '删除历史版本失败', 'agentSnapshot.fetchFailed': '获取历史版本失败', 'agentSnapshot.detailFailed': '获取快照详情失败', 'agentSnapshot.currentVersion': '当前版本', 'agentSnapshot.source.config': '配置保存', 'agentSnapshot.source.current': '当前配置', 'agentSnapshot.source.restore': '恢复前', + 'agentSnapshot.source.initial': '初始版本', 'agentSnapshot.field.restore': '恢复操作', + 'agentSnapshot.field.initial': '初始快照', 'agentSnapshot.field.agentCode': '助手编码', 'agentSnapshot.field.agentName': '助手昵称', 'agentSnapshot.field.asrModelId': '语音识别', @@ -916,6 +931,52 @@ export default { 'agentSnapshot.field.correctWordFileIds': '替换词', 'agentSnapshot.field.tagNames': '标签', 'agentSnapshot.field.tagIds': '标签ID', + 'agentSnapshot.function.added': '已开启', + 'agentSnapshot.function.removed': '已关闭', + 'agentSnapshot.function.updated': '参数变更', + 'agentSnapshot.function.enabled': '已开启', + 'agentSnapshot.function.disabled': '未开启', + 'agentSnapshot.function.disabledNote': '未开启此功能', + 'agentSnapshot.function.noParams': '无需配置参数', + 'agentSnapshot.chatHistoryConf.none': '不记录聊天记录', + 'agentSnapshot.chatHistoryConf.text': '上报文字', + 'agentSnapshot.chatHistoryConf.textVoice': '上报文字+语音', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': '天气查询', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': '服务器音乐播放', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': '中新网新闻', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'newsnow新闻聚合', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant设备状态查询', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant设备状态修改', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant音乐播放', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': '联网搜索', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': '设备呼叫设备', + 'agentSnapshot.pluginField.url': '接口地址', + 'agentSnapshot.pluginField.news_sources': '新闻源配置', + 'agentSnapshot.pluginField.default_rss_url': '默认 RSS 源', + 'agentSnapshot.pluginField.society_rss_url': '社会新闻 RSS 地址', + 'agentSnapshot.pluginField.world_rss_url': '国际新闻 RSS 地址', + 'agentSnapshot.pluginField.finance_rss_url': '财经新闻 RSS 地址', + 'agentSnapshot.pluginField.api_key': 'API 密钥', + 'agentSnapshot.pluginField.default_location': '默认查询城市', + 'agentSnapshot.pluginField.api_host': '开发者 API Host', + 'agentSnapshot.pluginField.base_url': '服务器地址', + 'agentSnapshot.pluginField.devices': '设备列表', + 'agentSnapshot.pluginField.provider': '搜索源', + 'agentSnapshot.pluginField.description': '工具描述', + 'agentSnapshot.pluginField.max_results': '返回数量', + 'agentSnapshot.model.Memory_nomem': '无记忆', + 'agentSnapshot.model.Memory_mem_local_short': '本地短记忆', + 'agentSnapshot.model.Memory_mem0ai': 'Mem0AI记忆', + 'agentSnapshot.model.Memory_mem_report_only': '仅上报记忆', + 'agentSnapshot.model.Intent_nointent': '无意图识别', + 'agentSnapshot.model.Intent_intent_llm': '外挂的大模型意图识别', + 'agentSnapshot.model.Intent_function_call': '大模型自主函数调用', + 'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS女声-晓晓', + 'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS男声-云扬', + 'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS女声-晓伊', + 'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS男声-云健', + 'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS男声-云希', + 'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS男声-云夏', // 表单字段 Tooltip 提示说明 'roleConfig.tooltip.agentName': '设置智能体的名称,用于标识和识别您的AI助手', @@ -928,7 +989,7 @@ export default { 'roleConfig.tooltip.vad': '语音活动检测(Voice Activity Detection):检测用户何时开始或结束说话,用于判断对话的开始和结束,实现打断功能', 'roleConfig.tooltip.asr': '自动语音识别(Automatic Speech Recognition):将用户的语音转换为文字,是人机对话的第一步,支持多语言识别', 'roleConfig.tooltip.llm': '主语言模型(Large Language Model):AI助手的"大脑",负责理解用户意图、生成回答和执行各种任务', - "roleConfig.tooltip.slm": '小参数模型(Small Language Model):用于智能体唤醒,生成记忆总结标题', + 'roleConfig.tooltip.slm': '小参数模型(Small Language Model):用于智能体唤醒,生成记忆总结标题', 'roleConfig.tooltip.vllm': '视觉大语言模型(Visual LLM):处理图像和视频理解,使AI助手能够分析和描述摄像头捕获的画面内容', 'roleConfig.tooltip.intent': '意图识别(Intent Detection):分析用户语音或文本,判断用户的真实意图,如查询、聊天、控制设备等', 'roleConfig.tooltip.memory': '记忆模型(Memory Model):管理对话历史的存储和摘要,决定AI能否记住之前的对话内容,实现长期记忆功能', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index cc89ba2f..56b41f86 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -806,12 +806,126 @@ export default { 'roleConfig.addTag': '添加新標籤', 'roleConfig.restartNotice': '保存配置後,需要重啟設備,新的配置才會生效。', 'roleConfig.saveConfig': '保存配置', + 'roleConfig.snapshotHistory': '歷史版本', + 'roleConfig.currentVersion': '目前版本 {version}', + 'roleConfig.functionName': '功能名稱', 'roleConfig.reset': '重置', 'roleConfig.agentName': '助手暱稱', 'roleConfig.roleTemplate': '角色模版', 'roleConfig.roleIntroduction': '角色介紹', 'roleConfig.languageCode': '語言編碼', 'roleConfig.interactionLanguage': '交互語種', + 'agentSnapshot.title': '歷史版本', + 'agentSnapshot.empty': '暫無歷史版本', + 'agentSnapshot.version': '版本', + 'agentSnapshot.createdAt': '保存時間', + 'agentSnapshot.source': '來源', + 'agentSnapshot.changedFields': '變更內容', + 'agentSnapshot.actions': '操作', + 'agentSnapshot.view': '查看', + 'agentSnapshot.restore': '恢復', + 'agentSnapshot.delete': '刪除', + 'agentSnapshot.detailTitle': '變更詳情', + 'agentSnapshot.restorePreviewTitle': '恢復確認', + 'agentSnapshot.confirmRestore': '確認恢復', + 'agentSnapshot.before': '之前', + 'agentSnapshot.after': '之後', + 'agentSnapshot.emptyValue': '無', + 'agentSnapshot.noChangedContent': '暫無可展示的變更內容', + 'agentSnapshot.noFunctionChange': '功能配置無變化', + 'agentSnapshot.beforeChange': '變化前', + 'agentSnapshot.afterChange': '變化後', + 'agentSnapshot.beforeRestore': '恢復前', + 'agentSnapshot.afterRestore': '恢復後', + 'agentSnapshot.restoreMemoryWarning': '恢復到無記憶版本會清空該智能體的聊天記錄,請確認後再繼續。', + 'agentSnapshot.restoreConfirm': '確定恢復到版本 {version} 嗎?目前配置會先自動保存為新的歷史版本。', + 'agentSnapshot.restoreSuccess': '已恢復歷史版本', + 'agentSnapshot.restoreFailed': '恢復歷史版本失敗', + 'agentSnapshot.restoreFailedRollback': '恢復歷史版本失敗,交易已回滾,目前配置未改變', + 'agentSnapshot.deleteConfirm': '確定刪除版本 {version} 嗎?刪除後無法復原。', + 'agentSnapshot.deleteSuccess': '已刪除歷史版本', + 'agentSnapshot.deleteFailed': '刪除歷史版本失敗', + 'agentSnapshot.fetchFailed': '獲取歷史版本失敗', + 'agentSnapshot.detailFailed': '獲取快照詳情失敗', + 'agentSnapshot.currentVersion': '目前版本', + 'agentSnapshot.source.config': '配置保存', + 'agentSnapshot.source.current': '目前配置', + 'agentSnapshot.source.restore': '恢復前', + 'agentSnapshot.source.initial': '初始版本', + 'agentSnapshot.field.restore': '恢復操作', + 'agentSnapshot.field.initial': '初始快照', + 'agentSnapshot.field.agentCode': '助手編碼', + 'agentSnapshot.field.agentName': '助手暱稱', + 'agentSnapshot.field.asrModelId': '語音識別', + 'agentSnapshot.field.vadModelId': '語音活動檢測', + 'agentSnapshot.field.llmModelId': '主語言模型', + 'agentSnapshot.field.slmModelId': '小參數模型', + 'agentSnapshot.field.vllmModelId': '視覺大模型', + 'agentSnapshot.field.ttsModelId': '語音合成', + 'agentSnapshot.field.ttsVoiceId': '聲音音色', + 'agentSnapshot.field.ttsLanguage': '對話語言', + 'agentSnapshot.field.ttsVolume': '音量', + 'agentSnapshot.field.ttsRate': '語速', + 'agentSnapshot.field.ttsPitch': '音調', + 'agentSnapshot.field.memModelId': '記憶模式', + 'agentSnapshot.field.intentModelId': '意圖識別', + 'agentSnapshot.field.chatHistoryConf': '聊天記錄配置', + 'agentSnapshot.field.systemPrompt': '角色介紹', + 'agentSnapshot.field.summaryMemory': '記憶', + 'agentSnapshot.field.langCode': '語言編碼', + 'agentSnapshot.field.language': '交互語種', + 'agentSnapshot.field.sort': '排序', + 'agentSnapshot.field.functions': '功能插件', + 'agentSnapshot.field.contextProviders': '上下文源', + 'agentSnapshot.field.correctWordFileIds': '替換詞', + 'agentSnapshot.field.tagNames': '標籤', + 'agentSnapshot.field.tagIds': '標籤ID', + 'agentSnapshot.function.added': '已開啟', + 'agentSnapshot.function.removed': '已關閉', + 'agentSnapshot.function.updated': '參數變更', + 'agentSnapshot.function.enabled': '已開啟', + 'agentSnapshot.function.disabled': '未開啟', + 'agentSnapshot.function.disabledNote': '未開啟此功能', + 'agentSnapshot.function.noParams': '無需配置參數', + 'agentSnapshot.chatHistoryConf.none': '不記錄聊天記錄', + 'agentSnapshot.chatHistoryConf.text': '上報文字', + 'agentSnapshot.chatHistoryConf.textVoice': '上報文字+語音', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': '天氣查詢', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': '伺服器音樂播放', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': '中新網新聞', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'newsnow新聞聚合', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant設備狀態查詢', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant設備狀態修改', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant音樂播放', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': '聯網搜尋', + 'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': '設備呼叫設備', + 'agentSnapshot.pluginField.url': '接口地址', + 'agentSnapshot.pluginField.news_sources': '新聞源配置', + 'agentSnapshot.pluginField.default_rss_url': '預設 RSS 源', + 'agentSnapshot.pluginField.society_rss_url': '社會新聞 RSS 地址', + 'agentSnapshot.pluginField.world_rss_url': '國際新聞 RSS 地址', + 'agentSnapshot.pluginField.finance_rss_url': '財經新聞 RSS 地址', + 'agentSnapshot.pluginField.api_key': 'API 密鑰', + 'agentSnapshot.pluginField.default_location': '預設查詢城市', + 'agentSnapshot.pluginField.api_host': '開發者 API Host', + 'agentSnapshot.pluginField.base_url': '伺服器地址', + 'agentSnapshot.pluginField.devices': '設備列表', + 'agentSnapshot.pluginField.provider': '搜尋源', + 'agentSnapshot.pluginField.description': '工具描述', + 'agentSnapshot.pluginField.max_results': '返回數量', + 'agentSnapshot.model.Memory_nomem': '無記憶', + 'agentSnapshot.model.Memory_mem_local_short': '本地短記憶', + 'agentSnapshot.model.Memory_mem0ai': 'Mem0AI記憶', + 'agentSnapshot.model.Memory_mem_report_only': '僅上報記憶', + 'agentSnapshot.model.Intent_nointent': '無意圖識別', + 'agentSnapshot.model.Intent_intent_llm': '外掛的大模型意圖識別', + 'agentSnapshot.model.Intent_function_call': '大模型自主函式呼叫', + 'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS女聲-曉曉', + 'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS男聲-雲揚', + 'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS女聲-曉伊', + 'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS男聲-雲健', + 'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS男聲-雲希', + 'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS男聲-雲夏', 'roleConfig.vad': '語音活動檢測(VAD)', 'roleConfig.asr': '語音識別(ASR)', 'roleConfig.llm': '主語言模型(LLM)', @@ -1553,4 +1667,4 @@ export default { 'addressBookManagement.monthsAgo': '{months}個月前', 'addressBookManagement.yearsAgo': '{years}年前', -} \ No newline at end of file +} diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index a0075317..d6dcf4bb 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -17,7 +17,7 @@
    {{ form.agentName }} - 当前版本 #{{ currentVersionNo }} + {{ $t("roleConfig.currentVersion", { version: currentVersionNo }) }}
    @@ -319,7 +319,7 @@ placement="top" >
    -
    功能名称: {{ func.name }}
    +
    {{ $t("roleConfig.functionName") }}: {{ func.name }}
    {{ getFunctionDisplayChar(func.name) }} @@ -562,6 +562,7 @@ export default { asr: false, // 语音识别功能状态 }, dynamicTags: [], + originalTagNames: [], inputVisible: false, inputValue: '', checkedReplacementWordIds: [] @@ -618,8 +619,12 @@ export default { }), contextProviders: this.currentContextProviders, correctWordFileIds: this.checkedReplacementWordIds, - tagNames: this.dynamicTags.map(tag => tag.tagName), }; + const tagNames = this.dynamicTags.map(tag => tag.tagName); + const tagsChanged = !this.isSameStringList(tagNames, this.originalTagNames); + if (tagsChanged) { + configData.tagNames = tagNames; + } // 只在用户设置了TTS参数时才传递(不为null/undefined) if (this.form.ttsVolume !== null && this.form.ttsVolume !== undefined) { @@ -631,13 +636,20 @@ export default { if (this.form.ttsPitch !== null && this.form.ttsPitch !== undefined) { configData.ttsPitch = this.form.ttsPitch; } - Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => { + const agentId = this.$route.query.agentId; + Api.agent.updateAgentConfig(agentId, configData, ({ data }) => { if (data.code === 0) { - this.$message.success({ - message: i18n.t("roleConfig.saveSuccess"), - showClose: true, - }); - this.fetchCurrentVersion(this.$route.query.agentId); + const afterSave = () => { + if (tagsChanged) { + this.originalTagNames = [...tagNames]; + } + this.$message.success({ + message: i18n.t("roleConfig.saveSuccess"), + showClose: true, + }); + this.fetchCurrentVersion(agentId); + }; + afterSave(); } else { this.$message.error({ message: data.msg || i18n.t("roleConfig.saveFailed"), @@ -661,17 +673,11 @@ export default { return; } - Api.agent.getAgentSnapshots( - agentId, - { page: "1", limit: "1" }, - ({ data }) => { - if (data.code === 0) { - const latest = data.data?.list?.[0]; - const latestVersionNo = Number(latest?.versionNo) || 0; - this.currentVersionNo = latestVersionNo + 1; - } + Api.agent.getDeviceConfig(agentId, ({ data }) => { + if (data.code === 0) { + this.currentVersionNo = data.data?.currentVersionNo || null; } - ); + }); }, resetConfig() { this.$confirm(i18n.t("roleConfig.confirmReset"), i18n.t("message.info"), { @@ -862,7 +868,7 @@ export default { }); this.$set(this.modelOptions, model.type, LLMdata); } else { - this.$message.error(data.msg || "获取LLM模型列表失败"); + this.$message.error(data.msg || i18n.t("roleConfig.fetchModelsFailed")); } }); } @@ -1385,7 +1391,7 @@ export default { handleInputConfirm() { let inputValue = this.inputValue; if (inputValue) { - const tag = { id: new Date().getTime(), tagName: inputValue }; + const tag = { id: `tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, tagName: inputValue }; this.dynamicTags.push(tag); } this.inputVisible = false; @@ -1395,14 +1401,21 @@ export default { Api.agent.getAgentTags(agentId, ({ data }) => { if (data.code === 0) { this.dynamicTags = data.data || []; + this.originalTagNames = this.dynamicTags.map(tag => tag.tagName); } }); }, - handleSaveAgentTags(agentId) { + isSameStringList(left, right) { + if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) { + return false; + } + return left.every((value, index) => value === right[index]); + }, + handleSaveAgentTags(agentId, tagNames = this.dynamicTags.map(tag => tag.tagName)) { return new Promise((resolve, reject) => { - const tagNames = this.dynamicTags.map(tag => tag.tagName); Api.agent.saveAgentTags(agentId, { tagNames }, ({ data }) => { if (data.code === 0) { + this.originalTagNames = [...tagNames]; resolve(); } else { reject(data.msg); diff --git a/main/xiaozhi-server/config/assets/wakeup_words/ed76d459636c2481aec828516c1b4f54.wav b/main/xiaozhi-server/config/assets/wakeup_words/ed76d459636c2481aec828516c1b4f54.wav index 1e7246be..0dd48cb0 100644 Binary files a/main/xiaozhi-server/config/assets/wakeup_words/ed76d459636c2481aec828516c1b4f54.wav and b/main/xiaozhi-server/config/assets/wakeup_words/ed76d459636c2481aec828516c1b4f54.wav differ diff --git a/main/xiaozhi-server/config/manage_api_client.py b/main/xiaozhi-server/config/manage_api_client.py index 7cc4c361..8eb2da20 100644 --- a/main/xiaozhi-server/config/manage_api_client.py +++ b/main/xiaozhi-server/config/manage_api_client.py @@ -73,6 +73,7 @@ class ManageApiClient: }, timeout=cls.config.get("timeout", 30), limits=limits, # 使用限制 + trust_env=False, ) return cls._async_clients[loop_id] except RuntimeError: diff --git a/main/xiaozhi-server/core/providers/asr/fun_local.py b/main/xiaozhi-server/core/providers/asr/fun_local.py index fa428710..ef11ae02 100644 --- a/main/xiaozhi-server/core/providers/asr/fun_local.py +++ b/main/xiaozhi-server/core/providers/asr/fun_local.py @@ -43,9 +43,13 @@ class ASRProvider(ASRProviderBase): # 内存检测,要求大于2G min_mem_bytes = 2 * 1024 * 1024 * 1024 - total_mem = psutil.virtual_memory().total - if total_mem < min_mem_bytes: - logger.bind(tag=TAG).error(f"可用内存不足2G,当前仅有 {total_mem / (1024*1024):.2f} MB,可能无法启动FunASR") + try: + total_mem = psutil.virtual_memory().total + except RuntimeError as e: + logger.bind(tag=TAG).warning(f"获取系统内存信息失败,跳过FunASR内存检测: {e}") + else: + if total_mem < min_mem_bytes: + logger.bind(tag=TAG).error(f"可用内存不足2G,当前仅有 {total_mem / (1024*1024):.2f} MB,可能无法启动FunASR") self.interface_type = InterfaceType.LOCAL self.model_dir = config.get("model_dir")