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 5e2435e9..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 = "获取用户智能体列表") @@ -127,7 +121,7 @@ public class AgentController { } AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO(); agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory()); - agentService.updateAgentById(device.getAgentId(), agentUpdateDTO); + agentService.updateAgentById(device.getAgentId(), agentUpdateDTO, false); return new Result<>(); } @@ -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<>(); } @@ -332,8 +315,11 @@ public class AgentController { public Result saveAgentTags(@PathVariable String id, @RequestBody Map params) { List tagIds = (List) params.get("tagIds"); List tagNames = (List) params.get("tagNames"); - agentTagService.saveAgentTags(id, tagIds, tagNames); + AgentUpdateDTO dto = new AgentUpdateDTO(); + dto.setTagIds(tagIds); + dto.setTagNames(tagNames); + agentService.updateAgentById(id, dto); return new Result().ok(null); } -} \ No newline at end of file +} 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 new file mode 100644 index 00000000..255e606d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentSnapshotController.java @@ -0,0 +1,75 @@ +package xiaozhi.modules.agent.controller; + +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.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +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; +import xiaozhi.modules.security.user.SecurityUser; + +@Tag(name = "智能体快照") +@AllArgsConstructor +@RestController +@RequestMapping("/agent/{agentId}/snapshots") +public class AgentSnapshotController { + private final AgentSnapshotService agentSnapshotService; + private final AgentService agentService; + + @GetMapping + @Operation(summary = "获取智能体快照列表") + @RequiresPermissions("sys:role:normal") + public Result> page( + @PathVariable String agentId, + @ParameterObject AgentSnapshotPageDTO params) { + checkPermission(agentId); + return new Result>().ok(agentSnapshotService.page(agentId, params)); + } + + @GetMapping("/{snapshotId}") + @Operation(summary = "获取智能体快照详情") + @RequiresPermissions("sys:role:normal") + public Result getSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) { + checkPermission(agentId); + return new Result().ok(agentSnapshotService.getSnapshot(agentId, snapshotId)); + } + + @PostMapping("/{snapshotId}/restore") + @Operation(summary = "恢复智能体快照") + @RequiresPermissions("sys:role:normal") + public Result restore(@PathVariable String agentId, @PathVariable String snapshotId) { + checkPermission(agentId); + agentSnapshotService.restoreSnapshot(agentId, snapshotId); + 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())) { + throw new RenException("没有权限访问该智能体快照"); + } + } +} 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 new file mode 100644 index 00000000..ee17f15f --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentSnapshotDao.java @@ -0,0 +1,20 @@ +package xiaozhi.modules.agent.dao; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import xiaozhi.common.dao.BaseDao; +import xiaozhi.modules.agent.entity.AgentSnapshotEntity; + +@Mapper +public interface AgentSnapshotDao extends BaseDao { + Integer selectMaxVersionNo(@Param("agentId") String agentId); + + AgentSnapshotEntity selectLatestSnapshot(@Param("agentId") String agentId); + + AgentSnapshotEntity selectNextSnapshot(@Param("agentId") String agentId, @Param("versionNo") Integer versionNo); + + int insertWithNextVersion(@Param("snapshot") AgentSnapshotEntity snapshot); + + int deleteOlderThanKeepLimit(@Param("agentId") String agentId, @Param("keepLimit") int keepLimit); +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentSnapshotDataDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentSnapshotDataDTO.java new file mode 100644 index 00000000..328c73cf --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentSnapshotDataDTO.java @@ -0,0 +1,40 @@ +package xiaozhi.modules.agent.dto; + +import java.io.Serializable; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "智能体快照数据") +public class AgentSnapshotDataDTO implements Serializable { + private static final long serialVersionUID = 1L; + + private String agentCode; + private String agentName; + private String asrModelId; + private String vadModelId; + private String llmModelId; + private String slmModelId; + private String vllmModelId; + private String ttsModelId; + private String ttsVoiceId; + private String ttsLanguage; + private Integer ttsVolume; + private Integer ttsRate; + private Integer ttsPitch; + private String memModelId; + private String intentModelId; + private Integer chatHistoryConf; + private String systemPrompt; + private String summaryMemory; + private String langCode; + private String language; + private Integer sort; + private List functions; + private List contextProviders; + private List correctWordFileIds; + private List tagNames; + private List tags; +} 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/dto/AgentSnapshotTagDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentSnapshotTagDTO.java new file mode 100644 index 00000000..6e93dd0e --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentSnapshotTagDTO.java @@ -0,0 +1,16 @@ +package xiaozhi.modules.agent.dto; + +import java.io.Serializable; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "智能体快照标签") +public class AgentSnapshotTagDTO implements Serializable { + private static final long serialVersionUID = 1L; + + private String id; + private String tagName; + private Integer sort; +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java index f2053ac5..7c939630 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java @@ -4,9 +4,12 @@ import java.io.Serializable; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; +import java.util.Map; +import com.fasterxml.jackson.core.type.TypeReference; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import xiaozhi.common.utils.JsonUtils; /** * 智能体更新DTO @@ -91,15 +94,50 @@ public class AgentUpdateDTO implements Serializable { @Schema(description = "替换词文件ID列表", nullable = true) private List correctWordFileIds; + @Schema(description = "标签名称列表", nullable = true) + private List tagNames; + + @Schema(description = "标签ID列表", nullable = true) + private List tagIds; + @Data @Schema(description = "插件函数信息") public static class FunctionInfo implements Serializable { + private static final TypeReference> PARAM_INFO_TYPE = new TypeReference<>() { + }; + @Schema(description = "插件ID", example = "plugin_01") private String pluginId; @Schema(description = "函数参数信息", nullable = true) - private HashMap paramInfo; + private HashMap paramInfo = new HashMap<>(); + + public void setParamInfo(Object paramInfo) { + this.paramInfo = normalizeParamInfo(paramInfo); + } + + private static HashMap normalizeParamInfo(Object paramInfo) { + if (paramInfo == null) { + return new HashMap<>(); + } + if (paramInfo instanceof String value) { + if (value.trim().isEmpty()) { + return new HashMap<>(); + } + return JsonUtils.parseObject(value, PARAM_INFO_TYPE); + } + if (paramInfo instanceof Map value) { + HashMap normalized = new HashMap<>(); + value.forEach((key, val) -> { + if (key != null) { + normalized.put(String.valueOf(key), val); + } + }); + return normalized; + } + return JsonUtils.parseObject(JsonUtils.toJsonString(paramInfo), PARAM_INFO_TYPE); + } private static final long serialVersionUID = 1L; } -} \ No newline at end of file +} 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 new file mode 100644 index 00000000..0d99fd36 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentSnapshotEntity.java @@ -0,0 +1,50 @@ +package xiaozhi.modules.agent.entity; + +import java.util.Date; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@TableName("ai_agent_snapshot") +@Schema(description = "智能体配置快照") +public class AgentSnapshotEntity { + + @TableId(type = IdType.ASSIGN_UUID) + @Schema(description = "快照ID") + private String id; + + @Schema(description = "智能体ID") + private String agentId; + + @Schema(description = "所属用户ID") + private Long userId; + + @Schema(description = "版本号") + private Integer versionNo; + + @Schema(description = "快照数据JSON") + private String snapshotData; + + @Schema(description = "变更字段JSON") + private String changedFields; + + @Schema(description = "快照来源") + private String source; + + @Schema(description = "恢复来源快照ID") + private String restoreFromSnapshotId; + + @Schema(description = "恢复来源版本号") + private Integer restoreFromVersionNo; + + @Schema(description = "创建者") + private Long creator; + + @Schema(description = "创建时间") + private Date createdAt; +} 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 22e8a2e0..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); + /** * 获取用户智能体列表 * @@ -93,6 +100,15 @@ public interface AgentService extends BaseService { */ void updateAgentById(String agentId, AgentUpdateDTO dto); + /** + * 更新智能体 + * + * @param agentId 智能体ID + * @param dto 更新智能体所需的信息 + * @param createSnapshot 是否创建配置快照 + */ + void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot); + /** * 创建智能体 * 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 new file mode 100644 index 00000000..f88c47ea --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentSnapshotService.java @@ -0,0 +1,23 @@ +package xiaozhi.modules.agent.service; + +import xiaozhi.common.page.PageData; +import xiaozhi.common.service.BaseService; +import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO; +import xiaozhi.modules.agent.entity.AgentSnapshotEntity; +import xiaozhi.modules.agent.vo.AgentSnapshotVO; + +public interface AgentSnapshotService extends BaseService { + void createSnapshot(String agentId, String source); + + 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/AgentChatSummaryServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatSummaryServiceImpl.java index a3b78b18..09cbf062 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatSummaryServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentChatSummaryServiceImpl.java @@ -118,7 +118,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService { { setSummaryMemory(summaryDTO.getSummary()); } - }); + }, false); log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId); } else { log.info("生成总结失败: {}", summaryDTO.getErrorMessage()); @@ -518,4 +518,4 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService { return null; } } -} \ No newline at end of file +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java index 981a2bcf..3d6da990 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; @@ -43,9 +42,10 @@ import xiaozhi.modules.agent.entity.AgentTagEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentContextProviderService; -import xiaozhi.modules.agent.service.AgentPluginMappingService; -import xiaozhi.modules.agent.service.AgentService; -import xiaozhi.modules.agent.service.AgentTagService; +import xiaozhi.modules.agent.service.AgentPluginMappingService; +import xiaozhi.modules.agent.service.AgentService; +import xiaozhi.modules.agent.service.AgentSnapshotService; +import xiaozhi.modules.agent.service.AgentTagService; import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.agent.vo.AgentInfoVO; import xiaozhi.modules.correctword.service.CorrectWordFileService; @@ -73,9 +73,10 @@ public class AgentServiceImpl extends BaseServiceImpl imp private final AgentChatHistoryService agentChatHistoryService; private final AgentTemplateService agentTemplateService; private final ModelProviderService modelProviderService; - private final AgentContextProviderService agentContextProviderService; - private final AgentTagService agentTagService; - private final CorrectWordFileService correctWordFileService; + private final AgentContextProviderService agentContextProviderService; + private final AgentTagService agentTagService; + private final CorrectWordFileService correctWordFileService; + private final AgentSnapshotService agentSnapshotService; @Override public PageData adminAgentList(Map params) { @@ -106,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 @@ -134,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"); @@ -272,20 +294,31 @@ public class AgentServiceImpl extends BaseServiceImpl imp return userId.equals(agent.getUserId()); } - // 根据id更新智能体信息 - @Override - @Transactional(rollbackFor = Exception.class) - public void updateAgentById(String agentId, AgentUpdateDTO dto) { - // 先查询现有实体 - AgentEntity existingEntity = this.getAgentById(agentId); - if (existingEntity == null) { - throw new RenException(ErrorCode.AGENT_NOT_FOUND); - } - - // 只更新提供的非空字段 - if (dto.getAgentName() != null) { - existingEntity.setAgentName(dto.getAgentName()); - } + // 根据id更新智能体信息 + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAgentById(String agentId, AgentUpdateDTO dto) { + updateAgentById(agentId, dto, true); + } + + // 根据id更新智能体信息 + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot) { + if (agentDao.selectByIdForUpdate(agentId) == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + + // 锁定后查询现有实体和关联配置 + AgentEntity existingEntity = this.getAgentById(agentId); + if (createSnapshot && agentSnapshotService.getCurrentVersionNo(agentId) == 0) { + agentSnapshotService.createSnapshot(agentId, "initial"); + } + + // 只更新提供的非空字段 + if (dto.getAgentName() != null) { + existingEntity.setAgentName(dto.getAgentName()); + } if (dto.getAgentCode() != null) { existingEntity.setAgentCode(dto.getAgentCode()); } @@ -426,16 +459,24 @@ public class AgentServiceImpl extends BaseServiceImpl imp } // 更新替换词文件关联 - if (dto.getCorrectWordFileIds() != null) { - correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds()); - } - - boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId()); - if (!b) { - throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH); - } - this.updateById(existingEntity); - } + if (dto.getCorrectWordFileIds() != null) { + correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds()); + } + + // 更新智能体标签 + if (dto.getTagNames() != null || dto.getTagIds() != null) { + agentTagService.saveAgentTags(agentId, dto.getTagIds(), dto.getTagNames()); + } + + boolean b = validateLLMIntentParams(existingEntity.getLlmModelId(), existingEntity.getIntentModelId()); + if (!b) { + throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH); + } + this.updateById(existingEntity); + if (createSnapshot) { + agentSnapshotService.createSnapshot(agentId, "config"); + } + } /** * 验证大语言模型和意图识别的参数是否符合匹配 @@ -576,4 +617,4 @@ public class AgentServiceImpl extends BaseServiceImpl imp } } -} \ No newline at end of file +} 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 new file mode 100644 index 00000000..3919b917 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentSnapshotServiceImpl.java @@ -0,0 +1,759 @@ +package xiaozhi.modules.agent.service.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +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.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; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; +import xiaozhi.common.exception.RenException; +import xiaozhi.common.page.PageData; +import xiaozhi.common.service.impl.BaseServiceImpl; +import xiaozhi.common.utils.JsonUtils; +import xiaozhi.modules.agent.dao.AgentDao; +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; +import xiaozhi.modules.agent.entity.AgentEntity; +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; +import xiaozhi.modules.agent.service.AgentSnapshotService; +import xiaozhi.modules.agent.service.AgentTagService; +import xiaozhi.modules.agent.vo.AgentInfoVO; +import xiaozhi.modules.agent.vo.AgentSnapshotVO; +import xiaozhi.modules.correctword.service.CorrectWordFileService; +import xiaozhi.modules.model.entity.ModelConfigEntity; +import xiaozhi.modules.model.service.ModelConfigService; +import xiaozhi.modules.security.user.SecurityUser; + +@Service +@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; + private final AgentTagDao agentTagDao; + private final AgentTagRelationDao agentTagRelationDao; + private final AgentPluginMappingService agentPluginMappingService; + private final AgentContextProviderService agentContextProviderService; + private final AgentTagService agentTagService; + private final AgentChatHistoryService agentChatHistoryService; + private final ModelConfigService modelConfigService; + private final CorrectWordFileService correctWordFileService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createSnapshot(String agentId, String source) { + lockAgent(agentId); + AgentInfoVO agent = getAgentInfo(agentId); + AgentSnapshotDataDTO snapshotData = buildSnapshotData(agent); + AgentSnapshotEntity previousSnapshot = agentSnapshotDao.selectLatestSnapshot(agentId); + List changedFields; + if (previousSnapshot == null) { + changedFields = List.of("initial"); + } else { + AgentSnapshotDataDTO previousData = JsonUtils.parseObject(previousSnapshot.getSnapshotData(), + AgentSnapshotDataDTO.class); + changedFields = getChangedFields(previousData, snapshotData); + } + if (changedFields.isEmpty()) { + return; + } + + insertSnapshot(agentId, agent.getUserId(), source, snapshotData, changedFields); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public PageData page(String agentId, AgentSnapshotPageDTO params) { + 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) + .le(pageParams.getMaxVersionNo() != null, "version_no", pageParams.getMaxVersionNo())); + List list = result.getRecords().stream() + .map(entity -> toVO(entity, false)) + .toList(); + return new PageData<>(list, result.getTotal()); + } + + @Override + public AgentSnapshotVO getSnapshot(String agentId, String snapshotId) { + AgentSnapshotEntity entity = getSnapshotEntity(agentId, snapshotId); + return toVO(entity, true); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void restoreSnapshot(String agentId, String snapshotId) { + AgentEntity agent = agentDao.selectByIdForUpdate(agentId); + if (agent == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + + AgentSnapshotEntity snapshot = getSnapshotEntity(agentId, snapshotId); + AgentSnapshotDataDTO data = JsonUtils.parseObject(snapshot.getSnapshotData(), AgentSnapshotDataDTO.class); + if (data == null) { + throw new RenException("快照数据为空,无法恢复"); + } + + AgentInfoVO currentAgent = getAgentInfo(agentId); + AgentSnapshotDataDTO currentData = buildSnapshotData(currentAgent); + AgentSnapshotDataDTO restoreData = preserveCurrentSensitiveValues(data, currentData); + List changedFields = getChangedFields(currentData, restoreData); + if (changedFields.isEmpty()) { + return; + } + + applyAgentFields(agent, restoreData); + validateRestoreParams(agent); + applyMemoryPolicy(agent); + agent.setUpdater(SecurityUser.getUserId()); + agent.setUpdatedAt(new Date()); + agentDao.updateById(agent); + + restoreFunctions(agentId, restoreData.getFunctions()); + restoreContextProviders(agentId, restoreData.getContextProviders()); + correctWordFileService.saveAgentCorrectWords(agentId, nullToEmpty(restoreData.getCorrectWordFileIds())); + restoreTags(agentId, restoreData); + insertSnapshot(agentId, currentAgent.getUserId(), "restore", restoreData, changedFields, + snapshot.getId(), snapshot.getVersionNo()); + } + + @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; + } + + @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.setId(UUID.randomUUID().toString().replace("-", "")); + entity.setAgentId(agentId); + entity.setUserId(userId); + 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()); + int inserted = agentSnapshotDao.insertWithNextVersion(entity); + if (inserted != 1) { + throw new RenException("快照版本号生成失败"); + } + pruneSnapshots(agentId); + } + + private void lockAgent(String agentId) { + if (agentDao.selectByIdForUpdate(agentId) == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + } + + private void pruneSnapshots(String agentId) { + agentSnapshotDao.deleteOlderThanKeepLimit(agentId, MAX_SNAPSHOTS_PER_AGENT); + } + + private AgentSnapshotEntity getSnapshotEntity(String agentId, String snapshotId) { + AgentSnapshotEntity entity = selectById(snapshotId); + if (entity == null || !Objects.equals(agentId, entity.getAgentId())) { + throw new RenException("快照不存在"); + } + return entity; + } + + private AgentInfoVO getAgentInfo(String agentId) { + AgentInfoVO agent = agentDao.selectAgentInfoById(agentId); + if (agent == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + return agent; + } + + private AgentSnapshotDataDTO buildSnapshotData(String agentId) { + return buildSnapshotData(getAgentInfo(agentId)); + } + + private AgentSnapshotDataDTO buildSnapshotData(AgentInfoVO agent) { + AgentSnapshotDataDTO data = new AgentSnapshotDataDTO(); + data.setAgentCode(agent.getAgentCode()); + data.setAgentName(agent.getAgentName()); + data.setAsrModelId(agent.getAsrModelId()); + data.setVadModelId(agent.getVadModelId()); + data.setLlmModelId(agent.getLlmModelId()); + data.setSlmModelId(agent.getSlmModelId()); + data.setVllmModelId(agent.getVllmModelId()); + data.setTtsModelId(agent.getTtsModelId()); + data.setTtsVoiceId(agent.getTtsVoiceId()); + data.setTtsLanguage(agent.getTtsLanguage()); + data.setTtsVolume(agent.getTtsVolume()); + data.setTtsRate(agent.getTtsRate()); + data.setTtsPitch(agent.getTtsPitch()); + data.setMemModelId(agent.getMemModelId()); + data.setIntentModelId(agent.getIntentModelId()); + data.setChatHistoryConf(agent.getChatHistoryConf()); + data.setSystemPrompt(agent.getSystemPrompt()); + data.setSummaryMemory(agent.getSummaryMemory()); + data.setLangCode(agent.getLangCode()); + data.setLanguage(agent.getLanguage()); + data.setSort(agent.getSort()); + data.setFunctions(toFunctionInfo(agent.getFunctions())); + 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; + } + + private List getSnapshotTags(String agentId) { + List tags = agentTagDao.selectByAgentId(agentId); + if (tags == null || tags.isEmpty()) { + return Collections.emptyList(); + } + List snapshotTags = new ArrayList<>(); + for (int i = 0; i < tags.size(); i++) { + AgentTagEntity tag = tags.get(i); + AgentSnapshotTagDTO snapshotTag = new AgentSnapshotTagDTO(); + snapshotTag.setId(tag.getId()); + snapshotTag.setTagName(tag.getTagName()); + snapshotTag.setSort(i); + snapshotTags.add(snapshotTag); + } + return snapshotTags; + } + + private List toFunctionInfo(List mappings) { + if (mappings == null || mappings.isEmpty()) { + return Collections.emptyList(); + } + return mappings.stream().map(mapping -> { + AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo(); + info.setPluginId(mapping.getPluginId()); + info.setParamInfo(parseParamInfo(mapping.getParamInfo())); + return info; + }).toList(); + } + + private HashMap parseParamInfo(String paramInfo) { + if (StringUtils.isBlank(paramInfo)) { + return new HashMap<>(); + } + return JsonUtils.parseObject(paramInfo, PARAM_INFO_TYPE); + } + + private List getContextProviders(String agentId) { + AgentContextProviderEntity contextEntity = agentContextProviderService.getByAgentId(agentId); + if (contextEntity == null || contextEntity.getContextProviders() == null) { + return Collections.emptyList(); + } + return contextEntity.getContextProviders(); + } + + private List getChangedFields(AgentSnapshotDataDTO current, AgentUpdateDTO pendingUpdate) { + if (pendingUpdate == null) { + return List.of("restore"); + } + + List fields = new ArrayList<>(); + 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 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) { + String keyText = String.valueOf(key); + result.put(keyText, isSensitiveKey(keyText) ? SECRET_PLACEHOLDER : normalizeValue(value)); + } + }); + return result; + } + + private void applyAgentFields(AgentEntity agent, AgentSnapshotDataDTO data) { + for (AgentSnapshotField field : AgentSnapshotField.values()) { + field.applyTo(agent, data); + } + } + + private void validateRestoreParams(AgentEntity agent) { + if (agent == null || StringUtils.isBlank(agent.getLlmModelId())) { + return; + } + + ModelConfigEntity llmModelData = modelConfigService.selectById(agent.getLlmModelId()); + if (llmModelData == null || llmModelData.getConfigJson() == null) { + throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH); + } + Object typeValue = llmModelData.getConfigJson().get("type"); + String type = typeValue == null ? "" : typeValue.toString(); + if ("openai".equals(type) || "ollama".equals(type)) { + return; + } + if ("Intent_function_call".equals(agent.getIntentModelId())) { + throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH); + } + } + + private void applyMemoryPolicy(AgentEntity agent) { + if (agent == null || StringUtils.isBlank(agent.getMemModelId())) { + return; + } + if (Constant.MEMORY_NO_MEM.equals(agent.getMemModelId())) { + agentChatHistoryService.deleteByAgentId(agent.getId(), true, true); + agent.setSummaryMemory(""); + } else if (Constant.MEMORY_MEM_REPORT_ONLY.equals(agent.getMemModelId())) { + agent.setSummaryMemory(""); + } + } + + private void restoreFunctions(String agentId, List functions) { + agentPluginMappingService.deleteByAgentId(agentId); + if (functions == null || functions.isEmpty()) { + return; + } + List mappings = functions.stream().map(info -> { + AgentPluginMapping mapping = new AgentPluginMapping(); + mapping.setAgentId(agentId); + mapping.setPluginId(info.getPluginId()); + mapping.setParamInfo(JsonUtils.toJsonString(info.getParamInfo())); + return mapping; + }).toList(); + agentPluginMappingService.saveBatch(mappings); + } + + private void restoreContextProviders(String agentId, + List contextProviders) { + AgentContextProviderEntity entity = new AgentContextProviderEntity(); + entity.setAgentId(agentId); + entity.setContextProviders(nullToEmpty(contextProviders)); + entity.setUpdater(SecurityUser.getUserId()); + entity.setUpdatedAt(new Date()); + agentContextProviderService.saveOrUpdateByAgentId(entity); + } + + private void restoreTags(String agentId, AgentSnapshotDataDTO data) { + List tags = data.getTags(); + if (tags == null || tags.isEmpty()) { + agentTagService.saveAgentTags(agentId, null, nullToEmpty(data.getTagNames())); + return; + } + + agentTagRelationDao.deleteByAgentId(agentId); + List relations = new ArrayList<>(); + Date now = new Date(); + for (int i = 0; i < tags.size(); i++) { + AgentSnapshotTagDTO snapshotTag = tags.get(i); + String tagId = restoreTag(snapshotTag, now); + if (StringUtils.isBlank(tagId)) { + continue; + } + AgentTagRelationEntity relation = new AgentTagRelationEntity(); + relation.setId(UUID.randomUUID().toString().replace("-", "")); + relation.setAgentId(agentId); + relation.setTagId(tagId); + relation.setSort(i); + relation.setCreator(SecurityUser.getUserId()); + relation.setCreatedAt(now); + relation.setUpdater(SecurityUser.getUserId()); + relation.setUpdatedAt(now); + relations.add(relation); + } + if (!relations.isEmpty()) { + agentTagRelationDao.batchInsertRelation(relations); + } + } + + private String restoreTag(AgentSnapshotTagDTO snapshotTag, Date now) { + if (snapshotTag == null || StringUtils.isBlank(snapshotTag.getTagName())) { + return null; + } + + AgentTagEntity tag = null; + if (StringUtils.isNotBlank(snapshotTag.getId())) { + tag = agentTagDao.selectById(snapshotTag.getId()); + if (tag != null && Objects.equals(tag.getDeleted(), 1)) { + tag = null; + } + } + if (tag == null) { + tag = agentTagDao.selectOne(new QueryWrapper() + .eq("tag_name", snapshotTag.getTagName()) + .eq("deleted", 0) + .last("LIMIT 1")); + } + if (tag != null) { + return tag.getId(); + } + + AgentTagEntity newTag = new AgentTagEntity(); + newTag.setId(UUID.randomUUID().toString().replace("-", "")); + newTag.setTagName(snapshotTag.getTagName()); + newTag.setSort(snapshotTag.getSort() == null ? 0 : snapshotTag.getSort()); + newTag.setDeleted(0); + newTag.setCreator(SecurityUser.getUserId()); + newTag.setCreatedAt(now); + newTag.setUpdater(SecurityUser.getUserId()); + newTag.setUpdatedAt(now); + agentTagDao.insert(newTag); + return newTag.getId(); + } + + private AgentSnapshotVO toVO(AgentSnapshotEntity entity, boolean includeData) { + AgentSnapshotVO vo = new AgentSnapshotVO(); + vo.setId(entity.getId()); + vo.setAgentId(entity.getAgentId()); + vo.setUserId(entity.getUserId()); + vo.setVersionNo(entity.getVersionNo()); + vo.setChangedFields(parseChangedFields(entity.getChangedFields())); + vo.setFieldOrder(AgentSnapshotField.names()); + vo.setSource(entity.getSource()); + vo.setRestoreFromSnapshotId(entity.getRestoreFromSnapshotId()); + vo.setRestoreFromVersionNo(entity.getRestoreFromVersionNo()); + vo.setCreator(entity.getCreator()); + vo.setCreatedAt(entity.getCreatedAt()); + if (includeData) { + vo.setSnapshotData(redactSnapshotData(JsonUtils.parseObject(entity.getSnapshotData(), + AgentSnapshotDataDTO.class))); + vo.setAfterSnapshotData(redactSnapshotData(getAfterSnapshotData(entity))); + } + return vo; + } + + private AgentSnapshotDataDTO getAfterSnapshotData(AgentSnapshotEntity entity) { + AgentSnapshotEntity nextSnapshot = agentSnapshotDao.selectNextSnapshot(entity.getAgentId(), entity.getVersionNo()); + if (nextSnapshot != null) { + return JsonUtils.parseObject(nextSnapshot.getSnapshotData(), AgentSnapshotDataDTO.class); + } + return null; + } + + private List parseChangedFields(String changedFields) { + if (StringUtils.isBlank(changedFields)) { + return Collections.emptyList(); + } + return JsonUtils.parseObject(changedFields, STRING_LIST_TYPE); + } + + private AgentSnapshotDataDTO redactSnapshotData(AgentSnapshotDataDTO data) { + if (data == null) { + return null; + } + AgentSnapshotDataDTO copy = JsonUtils.parseObject(JsonUtils.toJsonString(data), AgentSnapshotDataDTO.class); + if (copy.getFunctions() != null) { + copy.getFunctions().forEach(function -> { + 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 658ed6f2..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 @@ -1,7 +1,5 @@ package xiaozhi.modules.agent.vo; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -20,7 +18,6 @@ import java.util.List; public class AgentInfoVO extends AgentEntity { @Schema(description = "插件列表Id") - @TableField(typeHandler = JacksonTypeHandler.class) private List functions; @Schema(description = "上下文源配置") @@ -28,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 new file mode 100644 index 00000000..69547e58 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentSnapshotVO.java @@ -0,0 +1,30 @@ +package xiaozhi.modules.agent.vo; + +import java.util.Date; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO; + +@Data +@Schema(description = "智能体配置快照") +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; + private AgentSnapshotDataDTO afterSnapshotData; +} diff --git a/main/manager-api/src/main/resources/db/changelog/202607071530.sql b/main/manager-api/src/main/resources/db/changelog/202607071530.sql new file mode 100644 index 00000000..be224228 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202607071530.sql @@ -0,0 +1,20 @@ +-- liquibase formatted sql + +-- changeset tykechen:202607071530 +CREATE TABLE IF NOT EXISTS `ai_agent_snapshot` ( + `id` VARCHAR(32) NOT NULL COMMENT '快照ID', + `agent_id` VARCHAR(32) NOT NULL COMMENT '智能体ID', + `user_id` BIGINT DEFAULT NULL COMMENT '所属用户ID', + `version_no` INT UNSIGNED NOT NULL COMMENT '版本号', + `snapshot_data` JSON NOT NULL DEFAULT (JSON_OBJECT()) COMMENT '快照数据', + `changed_fields` JSON DEFAULT NULL COMMENT '变更字段', + `source` VARCHAR(32) DEFAULT 'config' COMMENT '快照来源', + `restore_from_snapshot_id` VARCHAR(32) DEFAULT NULL COMMENT '恢复来源快照ID', + `restore_from_version_no` INT UNSIGNED DEFAULT NULL COMMENT '恢复来源版本号', + `creator` BIGINT DEFAULT NULL COMMENT '创建者', + `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_agent_version` (`agent_id`, `version_no`), + INDEX `idx_agent_created_at` (`agent_id`, `created_at`), + INDEX `idx_snapshot_user_created_at` (`user_id`, `created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置快照表'; 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 a1902bf4..27ed05a3 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 @@ -697,3 +697,10 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202607011405.sql + - changeSet: + id: 202607071530 + author: tykechen + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202607071530.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 c250afe3..c779a5e6 100644 --- a/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml +++ b/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml @@ -25,9 +25,6 @@ - - @@ -38,6 +35,13 @@ + + + + + + - \ No newline at end of file + + + + diff --git a/main/manager-api/src/main/resources/mapper/agent/AgentSnapshotDao.xml b/main/manager-api/src/main/resources/mapper/agent/AgentSnapshotDao.xml new file mode 100644 index 00000000..ea5650c2 --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/agent/AgentSnapshotDao.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + INSERT INTO ai_agent_snapshot ( + id, + agent_id, + user_id, + version_no, + snapshot_data, + changed_fields, + source, + restore_from_snapshot_id, + restore_from_version_no, + creator, + created_at + ) + SELECT #{snapshot.id}, + #{snapshot.agentId}, + #{snapshot.userId}, + COALESCE(MAX(version_no), 0) + 1, + #{snapshot.snapshotData}, + #{snapshot.changedFields}, + #{snapshot.source}, + #{snapshot.restoreFromSnapshotId}, + #{snapshot.restoreFromVersionNo}, + #{snapshot.creator}, + #{snapshot.createdAt} + FROM ai_agent_snapshot + WHERE agent_id = #{snapshot.agentId} + + + + 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..e3aad1b1 --- /dev/null +++ b/main/manager-api/src/test/java/xiaozhi/modules/agent/service/impl/AgentSnapshotServiceImplTest.java @@ -0,0 +1,450 @@ +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.anyInt; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +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.mockito.InOrder; +import org.springframework.test.util.ReflectionTestUtils; +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.service.AgentSnapshotService; +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 firstAgentSaveKeepsInitialBaselineBeforePersistingNewState() { + AgentDao agentDao = mock(AgentDao.class); + AgentContextProviderService contextProviderService = mock(AgentContextProviderService.class); + CorrectWordFileService correctWordFileService = mock(CorrectWordFileService.class); + AgentSnapshotService snapshotService = mock(AgentSnapshotService.class); + AgentServiceImpl service = new AgentServiceImpl(agentDao, null, null, null, null, null, null, null, + null, null, contextProviderService, null, correctWordFileService, snapshotService); + ReflectionTestUtils.setField(service, "baseDao", agentDao); + + String agentId = "agent-id"; + AgentEntity lockedAgent = new AgentEntity(); + lockedAgent.setId(agentId); + AgentInfoVO currentAgent = new AgentInfoVO(); + currentAgent.setId(agentId); + currentAgent.setAgentName("old-name"); + AgentUpdateDTO update = new AgentUpdateDTO(); + update.setAgentName("new-name"); + + when(agentDao.selectByIdForUpdate(agentId)).thenReturn(lockedAgent); + when(agentDao.selectAgentInfoById(agentId)).thenReturn(currentAgent); + when(snapshotService.getCurrentVersionNo(agentId)).thenReturn(0); + when(contextProviderService.getByAgentId(agentId)).thenReturn(null); + when(correctWordFileService.getAgentCorrectWordFileIds(agentId)).thenReturn(List.of()); + when(agentDao.updateById(any())).thenReturn(1); + + service.updateAgentById(agentId, update); + + InOrder inOrder = inOrder(agentDao, snapshotService); + inOrder.verify(snapshotService).createSnapshot(agentId, "initial"); + inOrder.verify(agentDao).updateById(argThat(agent -> "new-name".equals(agent.getAgentName()))); + inOrder.verify(snapshotService).createSnapshot(agentId, "config"); + } + + @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 snapshotInsertAllocatesVersionInSingleSqlStatement() throws Exception { + String xml = normalizeWhitespace(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")); + + assertTrue(dao.contains("insertWithNextVersion")); + assertTrue(xml.contains("")); + assertTrue(xml.contains("INSERT INTO ai_agent_snapshot")); + assertTrue(xml.contains("COALESCE(MAX(version_no), 0) + 1")); + assertFalse(xml.contains("LAST_INSERT_ID")); + assertFalse(xml.contains("ai_agent_snapshot_sequence")); + } + + @Test + void snapshotMigrationIsMergedIntoSingleChangeLog() throws Exception { + String sql = 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")); + + assertTrue(sql.contains("restore_from_snapshot_id")); + assertTrue(sql.contains("restore_from_version_no")); + assertTrue(sql.contains("idx_snapshot_user_created_at")); + assertTrue(sql.contains("DEFAULT (JSON_OBJECT())")); + assertTrue(master.contains("202607071530.sql")); + assertFalse(master.contains("202607081150.sql")); + assertFalse(master.contains("202607081230.sql")); + } + + @Test + void selectNextSnapshotLoadsRestoreTraceColumns() throws Exception { + String xml = normalizeWhitespace(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 pageDoesNotCreateInitialSnapshotWhenAgentHasNoHistory() { + AgentSnapshotDao snapshotDao = mock(AgentSnapshotDao.class); + AgentDao agentDao = mock(AgentDao.class); + AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(snapshotDao, agentDao, null, null, null, + null, null, null, null, null); + + when(snapshotDao.selectPage(any(), any())).thenReturn(new Page(1, 10)); + + service.page("agent-id", new AgentSnapshotPageDTO()); + + verify(agentDao, never()).selectByIdForUpdate(any()); + verify(snapshotDao, never()).selectMaxVersionNo(any()); + verify(snapshotDao, never()).insertWithNextVersion(any()); + verify(snapshotDao, never()).deleteOlderThanKeepLimit(any(), anyInt()); + } + + @Test + void getCurrentVersionNoReturnsPersistedMaxVersion() { + AgentSnapshotDao snapshotDao = mock(AgentSnapshotDao.class); + AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(snapshotDao, null, null, null, null, + null, null, null, null, null); + + when(snapshotDao.selectMaxVersionNo("agent-id")).thenReturn(7); + + assertEquals(7, service.getCurrentVersionNo("agent-id")); + } + + @Test + void createSnapshotStoresCurrentStateAsInitialVersionWhenHistoryIsEmpty() { + 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(agentDao.selectByIdForUpdate("agent-id")).thenReturn(lockedAgent); + when(agentDao.selectAgentInfoById("agent-id")).thenReturn(agentInfo); + when(snapshotDao.selectLatestSnapshot("agent-id")).thenReturn(null); + when(snapshotDao.insertWithNextVersion(any())).thenReturn(1); + when(correctWordFileService.getAgentCorrectWordFileIds("agent-id")).thenReturn(List.of()); + when(contextProviderService.getByAgentId("agent-id")).thenReturn(null); + when(tagDao.selectByAgentId("agent-id")).thenReturn(List.of()); + + service.createSnapshot("agent-id", "config"); + + verify(snapshotDao).insertWithNextVersion(argThat(snapshot -> "config".equals(snapshot.getSource()) + && snapshot.getVersionNo() == null + && snapshot.getChangedFields().contains("initial"))); + 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; + } + + private String normalizeWhitespace(String value) { + return value.replaceAll("\\s+", " ").trim(); + } +} 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 d79f8da4..3c9522db 100644 --- a/main/manager-web/src/apis/module/agent.js +++ b/main/manager-web/src/apis/module/agent.js @@ -81,6 +81,67 @@ export default { }); }).send(); }, + // 获取智能体配置快照列表 + getAgentSnapshots(agentId, params, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/${agentId}/snapshots`) + .method('GET') + .data(params) + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.getAgentSnapshots(agentId, params, callback); + }); + }).send(); + }, + // 获取智能体配置快照详情 + getAgentSnapshot(agentId, snapshotId, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/${agentId}/snapshots/${snapshotId}`) + .method('GET') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.getAgentSnapshot(agentId, snapshotId, callback); + }); + }).send(); + }, + // 恢复智能体配置快照 + restoreAgentSnapshot(agentId, snapshotId, callback) { + RequestService.sendRequest() + .url(`${getServiceUrl()}/agent/${agentId}/snapshots/${snapshotId}/restore`) + .method('POST') + .success((res) => { + RequestService.clearRequestTime(); + callback(res); + }) + .networkFail(() => { + RequestService.reAjaxFun(() => { + this.restoreAgentSnapshot(agentId, snapshotId, callback); + }); + }).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 new file mode 100644 index 00000000..af3e6e91 --- /dev/null +++ b/main/manager-web/src/components/AgentSnapshotDialog.vue @@ -0,0 +1,2334 @@ + + + + + diff --git a/main/manager-web/src/i18n/de.js b/main/manager-web/src/i18n/de.js index d1a42bb9..eeb2ae4b 100644 --- a/main/manager-web/src/i18n/de.js +++ b/main/manager-web/src/i18n/de.js @@ -806,12 +806,128 @@ 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.secretRedacted': 'Geheimer Schlüssel maskiert', + 'agentSnapshot.configValue': 'Konfigurationswert', + '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)', @@ -1336,10 +1452,6 @@ export default { 'knowledgeFileUpload.viewSlices': 'Segmente anzeigen', 'knowledgeFileUpload.delete': 'Löschen', 'knowledgeFileUpload.itemsPerPage': 'Einträge/Seite', - 'knowledgeFileUpload.firstPage': 'Erste Seite', - 'knowledgeFileUpload.prevPage': 'Vorherige Seite', - 'knowledgeFileUpload.nextPage': 'Nächste Seite', - 'knowledgeFileUpload.totalRecords': 'Insgesamt {total} Datensätze', 'knowledgeFileUpload.uploadDocument': 'Dokument hochladen', 'knowledgeFileUpload.documentNamePlaceholder': 'Bitte Dokumentnamen eingeben', 'knowledgeFileUpload.file': 'Datei', @@ -1378,10 +1490,7 @@ export default { 'knowledgeFileUpload.sliceCount': 'Segmentanzahl', 'knowledgeFileUpload.add': 'Hinzufügen', 'knowledgeFileUpload.retrievalTest': 'Abruftest', - 'knowledgeFileUpload.testQuestion': 'Testfrage', - 'knowledgeFileUpload.testQuestionPlaceholder': 'Bitte geben Sie die zu testende Frage ein', 'knowledgeFileUpload.executeTest': 'Test ausführen', - 'knowledgeFileUpload.testResult': 'Testergebnis:', 'knowledgeFileUpload.selectedFiles': 'Ausgewählte Dateien', 'knowledgeFileUpload.totalSlices': 'Insgesamt {total} Datensätze', 'knowledgeFileUpload.slice': 'Segment', @@ -1457,7 +1566,6 @@ export default { 'addressBookManagement.name': 'Adressbuchname', 'addressBookManagement.description': 'Adressbuchbeschreibung', 'addressBookManagement.contactCount': 'Kontaktanzahl', - 'addressBookManagement.status': 'Aktiviert', 'addressBookManagement.createdAt': 'Erstellt am', 'addressBookManagement.operation': 'Aktion', 'addressBookManagement.add': 'Hinzufügen', @@ -1478,9 +1586,7 @@ export default { 'addressBookManagement.operationCancelled': 'Löschvorgang abgebrochen', 'addressBookManagement.updateSuccess': 'Aktualisierung erfolgreich', 'addressBookManagement.addSuccess': 'Erfolgreich hinzugefügt', - 'addressBookManagement.updateFailed': 'Aktualisierung fehlgeschlagen', 'addressBookManagement.addFailed': 'Hinzufügen fehlgeschlagen', - 'addressBookManagement.selectAll': 'Alle auswählen', 'addressBookManagement.cancelSelectAll': 'Alle abwählen', 'addressBookManagement.deviceName': 'Gerätename', 'addressBookManagement.deviceType': 'Gerätetyp', @@ -1495,9 +1601,7 @@ export default { 'addressBookManagement.addressBookDescPlaceholder': 'Bitte Adressbuchbeschreibung eingeben', 'addressBookManagement.remarksPlaceholder': 'Bitte Bemerkungen eingeben', 'addressBookManagement.selectDevice': 'Bitte ein Gerät auswählen, um Adressbuchdetails anzuzeigen', - 'addressBookManagement.save': 'Speichern', 'addressBookManagement.saveSuccess': 'Speichern erfolgreich', - 'addressBookManagement.saveFailed': 'Speichern fehlgeschlagen', 'addressBookManagement.deviceNotSelected': 'Bitte zuerst ein Gerät auswählen', 'addressBookManagement.addDeviceTip': 'Gerät hinzufügen Funktion ist in Entwicklung', 'addressBookManagement.selectAgentFirst': 'Bitte zuerst einen Agent auswählen', @@ -1554,4 +1658,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 0248a653..8e258c60 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -806,6 +806,9 @@ export default { 'roleConfig.addTag': 'Add New Tag', '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', @@ -862,6 +865,119 @@ export default { 'roleConfig.audioPlayFailed': 'Audio playback failed', 'roleConfig.cannotPlayAudio': 'Cannot play audio', 'roleConfig.audioPlayError': 'Error occurred during audio playback', + 'agentSnapshot.title': 'Version History', + 'agentSnapshot.empty': 'No versions yet', + 'agentSnapshot.version': 'Version', + 'agentSnapshot.createdAt': 'Saved At', + 'agentSnapshot.source': 'Source', + 'agentSnapshot.changedFields': 'Changes', + 'agentSnapshot.actions': 'Actions', + 'agentSnapshot.view': 'View', + 'agentSnapshot.restore': 'Restore', + 'agentSnapshot.delete': 'Delete', + 'agentSnapshot.detailTitle': 'Change Details', + 'agentSnapshot.restorePreviewTitle': 'Restore Preview', + 'agentSnapshot.confirmRestore': 'Confirm Restore', + 'agentSnapshot.before': 'Before', + 'agentSnapshot.after': 'After', + 'agentSnapshot.emptyValue': 'None', + 'agentSnapshot.secretRedacted': 'Secret redacted', + 'agentSnapshot.configValue': 'Config Value', + 'agentSnapshot.noChangedContent': 'No changes to display', + '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', + 'agentSnapshot.field.vadModelId': 'Voice Detect', + 'agentSnapshot.field.llmModelId': 'Main Language Model', + 'agentSnapshot.field.slmModelId': 'Small Language Model', + 'agentSnapshot.field.vllmModelId': 'Vision Model', + 'agentSnapshot.field.ttsModelId': 'Text-to-Speech', + 'agentSnapshot.field.ttsVoiceId': 'Voice', + 'agentSnapshot.field.ttsLanguage': 'Language', + 'agentSnapshot.field.ttsVolume': 'Volume', + 'agentSnapshot.field.ttsRate': 'Speed', + 'agentSnapshot.field.ttsPitch': 'Pitch', + 'agentSnapshot.field.memModelId': 'Memory Model', + 'agentSnapshot.field.intentModelId': 'Intent Recognition', + 'agentSnapshot.field.chatHistoryConf': 'Chat History', + 'agentSnapshot.field.systemPrompt': 'Introduction', + 'agentSnapshot.field.summaryMemory': 'Memory', + 'agentSnapshot.field.langCode': 'Language Code', + 'agentSnapshot.field.language': 'Interaction Language', + 'agentSnapshot.field.sort': 'Sort', + 'agentSnapshot.field.functions': 'Functions', + 'agentSnapshot.field.contextProviders': 'Context Providers', + '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', @@ -1336,10 +1452,6 @@ export default { 'knowledgeFileUpload.viewSlices': 'View Slices', 'knowledgeFileUpload.delete': 'Delete', 'knowledgeFileUpload.itemsPerPage': 'items/page', - 'knowledgeFileUpload.firstPage': 'First Page', - 'knowledgeFileUpload.prevPage': 'Previous Page', - 'knowledgeFileUpload.nextPage': 'Next Page', - 'knowledgeFileUpload.totalRecords': 'Total {total} records', 'knowledgeFileUpload.uploadDocument': 'Upload Document', 'knowledgeFileUpload.documentNamePlaceholder': 'Please enter document name', 'knowledgeFileUpload.file': 'File', @@ -1378,10 +1490,7 @@ export default { 'knowledgeFileUpload.sliceCount': 'Slice Count', 'knowledgeFileUpload.add': 'Add', 'knowledgeFileUpload.retrievalTest': 'Retrieval Test', - 'knowledgeFileUpload.testQuestion': 'Test Question', - 'knowledgeFileUpload.testQuestionPlaceholder': 'Please enter the question to test', 'knowledgeFileUpload.executeTest': 'Execute Test', - 'knowledgeFileUpload.testResult': 'Test Result:', 'knowledgeFileUpload.selectedFiles': 'Selected Files', 'knowledgeFileUpload.totalSlices': 'Total {total} records', 'knowledgeFileUpload.slice': 'Slice', @@ -1457,7 +1566,6 @@ export default { 'addressBookManagement.name': 'Address Book Name', 'addressBookManagement.description': 'Address Book Description', 'addressBookManagement.contactCount': 'Contact Count', - 'addressBookManagement.status': 'Enabled', 'addressBookManagement.createdAt': 'Created Time', 'addressBookManagement.operation': 'Operation', 'addressBookManagement.add': 'Add', @@ -1478,9 +1586,7 @@ export default { 'addressBookManagement.operationCancelled': 'Delete operation cancelled', 'addressBookManagement.updateSuccess': 'Update successful', 'addressBookManagement.addSuccess': 'Add successful', - 'addressBookManagement.updateFailed': 'Update failed', 'addressBookManagement.addFailed': 'Add failed', - 'addressBookManagement.selectAll': 'Select All', 'addressBookManagement.cancelSelectAll': 'Deselect All', 'addressBookManagement.deviceName': 'Device Name', 'addressBookManagement.deviceType': 'Device Type', @@ -1495,9 +1601,7 @@ export default { 'addressBookManagement.addressBookDescPlaceholder': 'Please enter address book description', 'addressBookManagement.remarksPlaceholder': 'Please enter remarks', 'addressBookManagement.selectDevice': 'Please select a device to view address book details', - 'addressBookManagement.save': 'Save', 'addressBookManagement.saveSuccess': 'Save successful', - 'addressBookManagement.saveFailed': 'Save failed', 'addressBookManagement.deviceNotSelected': 'Please select a device first', 'addressBookManagement.addDeviceTip': 'Add device feature is under development', 'addressBookManagement.selectAgentFirst': 'Please select an agent first', @@ -1554,4 +1658,4 @@ export default { // Header navigation 'header.addressBook': 'Address Book', -} \ No newline at end of file +} diff --git a/main/manager-web/src/i18n/pt_BR.js b/main/manager-web/src/i18n/pt_BR.js index 90e9fc87..a53ba976 100644 --- a/main/manager-web/src/i18n/pt_BR.js +++ b/main/manager-web/src/i18n/pt_BR.js @@ -806,12 +806,128 @@ 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.secretRedacted': 'Chave secreta mascarada', + 'agentSnapshot.configValue': 'Valor da configuração', + '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', @@ -1336,10 +1452,6 @@ export default { 'knowledgeFileUpload.viewSlices': 'Ver Fatias', 'knowledgeFileUpload.delete': 'Excluir', 'knowledgeFileUpload.itemsPerPage': 'itens/página', - 'knowledgeFileUpload.firstPage': 'Primeira Página', - 'knowledgeFileUpload.prevPage': 'Página Anterior', - 'knowledgeFileUpload.nextPage': 'Próxima Página', - 'knowledgeFileUpload.totalRecords': 'Total de {total} registros', 'knowledgeFileUpload.uploadDocument': 'Enviar Documento', 'knowledgeFileUpload.documentNamePlaceholder': 'Por favor, insira o nome do documento', 'knowledgeFileUpload.file': 'Arquivo', @@ -1378,10 +1490,7 @@ export default { 'knowledgeFileUpload.sliceCount': 'Quantidade de Fatias', 'knowledgeFileUpload.add': 'Adicionar', 'knowledgeFileUpload.retrievalTest': 'Teste de Recuperação', - 'knowledgeFileUpload.testQuestion': 'Pergunta de Teste', - 'knowledgeFileUpload.testQuestionPlaceholder': 'Por favor, insira a pergunta para testar', 'knowledgeFileUpload.executeTest': 'Executar Teste', - 'knowledgeFileUpload.testResult': 'Resultado do Teste:', 'knowledgeFileUpload.selectedFiles': 'Arquivos Selecionados', 'knowledgeFileUpload.totalSlices': 'Total de {total} registros', 'knowledgeFileUpload.slice': 'Fatia', @@ -1457,7 +1566,6 @@ export default { 'addressBookManagement.name': 'Nome da Lista de Contatos', 'addressBookManagement.description': 'Descrição da Lista de Contatos', 'addressBookManagement.contactCount': 'Número de Contatos', - 'addressBookManagement.status': 'Ativo', 'addressBookManagement.createdAt': 'Data de Criação', 'addressBookManagement.operation': 'Operação', 'addressBookManagement.add': 'Adicionar', @@ -1478,9 +1586,7 @@ export default { 'addressBookManagement.operationCancelled': 'Operação de exclusão cancelada', 'addressBookManagement.updateSuccess': 'Atualização bem-sucedida', 'addressBookManagement.addSuccess': 'Adição bem-sucedida', - 'addressBookManagement.updateFailed': 'Falha na atualização', 'addressBookManagement.addFailed': 'Falha ao adicionar', - 'addressBookManagement.selectAll': 'Selecionar Todos', 'addressBookManagement.cancelSelectAll': 'Desmarcar Todos', 'addressBookManagement.deviceName': 'Nome do Dispositivo', 'addressBookManagement.deviceType': 'Tipo do Dispositivo', @@ -1495,9 +1601,7 @@ export default { 'addressBookManagement.addressBookDescPlaceholder': 'Digite a descrição da lista de contatos', 'addressBookManagement.remarksPlaceholder': 'Digite as observações', 'addressBookManagement.selectDevice': 'Selecione um dispositivo para ver os detalhes da lista de contatos', - 'addressBookManagement.save': 'Salvar', 'addressBookManagement.saveSuccess': 'Salvo com sucesso', - 'addressBookManagement.saveFailed': 'Falha ao salvar', 'addressBookManagement.deviceNotSelected': 'Selecione um dispositivo primeiro', 'addressBookManagement.addDeviceTip': 'Recurso de adicionar dispositivo em desenvolvimento', 'addressBookManagement.selectAgentFirst': 'Selecione um agente primeiro', @@ -1554,4 +1658,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..a2565f9f 100644 --- a/main/manager-web/src/i18n/vi.js +++ b/main/manager-web/src/i18n/vi.js @@ -806,12 +806,128 @@ 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.secretRedacted': 'Khóa bí mật đã được ẩn', + 'agentSnapshot.configValue': 'Giá trị cấu hình', + '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', @@ -1336,10 +1452,6 @@ export default { 'knowledgeFileUpload.viewSlices': 'Xem các phần', 'knowledgeFileUpload.delete': 'Xóa', 'knowledgeFileUpload.itemsPerPage': 'mục/trang', - 'knowledgeFileUpload.firstPage': 'Trang đầu', - 'knowledgeFileUpload.prevPage': 'Trang trước', - 'knowledgeFileUpload.nextPage': 'Trang sau', - 'knowledgeFileUpload.totalRecords': 'Tổng cộng {total} bản ghi', 'knowledgeFileUpload.uploadDocument': 'Tải lên tài liệu', 'knowledgeFileUpload.documentNamePlaceholder': 'Vui lòng nhập tên tài liệu', 'knowledgeFileUpload.file': 'Tệp', @@ -1378,10 +1490,7 @@ export default { 'knowledgeFileUpload.sliceCount': 'Số lượng phần', 'knowledgeFileUpload.add': 'Thêm', 'knowledgeFileUpload.retrievalTest': 'Kiểm tra truy xuất', - 'knowledgeFileUpload.testQuestion': 'Câu hỏi kiểm tra', - 'knowledgeFileUpload.testQuestionPlaceholder': 'Vui lòng nhập câu hỏi để kiểm tra', 'knowledgeFileUpload.executeTest': 'Thực thi kiểm tra', - 'knowledgeFileUpload.testResult': 'Kết quả kiểm tra:', 'knowledgeFileUpload.selectedFiles': 'Tệp đã chọn', 'knowledgeFileUpload.totalSlices': 'Tổng cộng {total} bản ghi', 'knowledgeFileUpload.slice': 'Phần', @@ -1457,7 +1566,6 @@ export default { 'addressBookManagement.name': 'Tên danh bạ', 'addressBookManagement.description': 'Mô tả danh bạ', 'addressBookManagement.contactCount': 'Số liên hệ', - 'addressBookManagement.status': 'Kích hoạt', 'addressBookManagement.createdAt': 'Thời gian tạo', 'addressBookManagement.operation': 'Thao tác', 'addressBookManagement.add': 'Thêm', @@ -1478,9 +1586,7 @@ export default { 'addressBookManagement.operationCancelled': 'Đã hủy thao tác xóa', 'addressBookManagement.updateSuccess': 'Cập nhật thành công', 'addressBookManagement.addSuccess': 'Thêm thành công', - 'addressBookManagement.updateFailed': 'Cập nhật thất bại', 'addressBookManagement.addFailed': 'Thêm thất bại', - 'addressBookManagement.selectAll': 'Chọn tất cả', 'addressBookManagement.cancelSelectAll': 'Bỏ chọn tất cả', 'addressBookManagement.deviceName': 'Tên thiết bị', 'addressBookManagement.deviceType': 'Loại thiết bị', @@ -1495,9 +1601,7 @@ export default { 'addressBookManagement.addressBookDescPlaceholder': 'Vui lòng nhập mô tả danh bạ', 'addressBookManagement.remarksPlaceholder': 'Vui lòng nhập ghi chú', 'addressBookManagement.selectDevice': 'Vui lòng chọn thiết bị để xem chi tiết danh bạ', - 'addressBookManagement.save': 'Lưu', 'addressBookManagement.saveSuccess': 'Lưu thành công', - 'addressBookManagement.saveFailed': 'Lưu thất bại', 'addressBookManagement.deviceNotSelected': 'Vui lòng chọn thiết bị trước', 'addressBookManagement.addDeviceTip': 'Tính năng thêm thiết bị đang phát triển', 'addressBookManagement.selectAgentFirst': 'Vui lòng chọn tác tử trước', @@ -1554,4 +1658,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 badd75f3..3a583cea 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -275,7 +275,7 @@ export default { 'voicePrint.closeOperation': '操作已关闭', 'voicePrint.loading': '拼命加载中', - // 手動添加設備對話框相關 + // 手动添加设备对话框相关 'manualAddDeviceDialog.title': '手动添加设备', 'manualAddDeviceDialog.deviceType': '设备型号', 'manualAddDeviceDialog.deviceTypePlaceholder': '请选择设备型号', @@ -806,6 +806,9 @@ export default { 'roleConfig.addTag': '添加新标签', 'roleConfig.restartNotice': '保存配置后,需要重启设备,新的配置才会生效。', 'roleConfig.saveConfig': '保存配置', + 'roleConfig.snapshotHistory': '历史版本', + 'roleConfig.currentVersion': '当前版本 {version}', + 'roleConfig.functionName': '功能名称', 'roleConfig.reset': '重置', 'roleConfig.agentName': '助手昵称', 'roleConfig.roleTemplate': '角色模版', @@ -815,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': '记忆', @@ -862,6 +865,119 @@ export default { 'roleConfig.audioPlayFailed': '音频播放失败', 'roleConfig.cannotPlayAudio': '无法播放音频', 'roleConfig.audioPlayError': '播放音频过程出错', + '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.secretRedacted': '密钥已脱敏', + 'agentSnapshot.configValue': '配置值', + '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男声-云夏', // 表单字段 Tooltip 提示说明 'roleConfig.tooltip.agentName': '设置智能体的名称,用于标识和识别您的AI助手', @@ -874,7 +990,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能否记住之前的对话内容,实现长期记忆功能', @@ -1336,10 +1452,6 @@ export default { 'knowledgeFileUpload.viewSlices': '查看切片', 'knowledgeFileUpload.delete': '删除', 'knowledgeFileUpload.itemsPerPage': '条/页', - 'knowledgeFileUpload.firstPage': '首页', - 'knowledgeFileUpload.prevPage': '上一页', - 'knowledgeFileUpload.nextPage': '下一页', - 'knowledgeFileUpload.totalRecords': '共{total}条记录', 'knowledgeFileUpload.uploadDocument': '上传文档', 'knowledgeFileUpload.documentNamePlaceholder': '请输入文档名称', 'knowledgeFileUpload.file': '文件', @@ -1378,10 +1490,7 @@ export default { 'knowledgeFileUpload.sliceCount': '切片数量', 'knowledgeFileUpload.add': '新增', 'knowledgeFileUpload.retrievalTest': '召回测试', - 'knowledgeFileUpload.testQuestion': '测试问题', - 'knowledgeFileUpload.testQuestionPlaceholder': '请输入要测试的问题', 'knowledgeFileUpload.executeTest': '执行测试', - 'knowledgeFileUpload.testResult': '测试结果:', 'knowledgeFileUpload.selectedFiles': '已选择文件', 'knowledgeFileUpload.totalSlices': '共{total}条记录', 'knowledgeFileUpload.slice': '切片', @@ -1401,6 +1510,8 @@ export default { 'knowledgeFileUpload.testQuestionRequired': '请输入测试问题', 'knowledgeBaseDialog.descriptionRequired': '请输入知识库描述', 'knowledgeFileUpload.parsing': '正在解析中...', + + // 知识库管理页面文本 'knowledgeBaseManagement.loading': '加载中...', 'knowledgeBaseManagement.noData': '暂无数据', 'knowledgeBaseManagement.switchKnowledgeBase': '切换知识库', @@ -1455,7 +1566,6 @@ export default { 'addressBookManagement.name': '通讯录名称', 'addressBookManagement.description': '通讯录描述', 'addressBookManagement.contactCount': '联系人数量', - 'addressBookManagement.status': '启用', 'addressBookManagement.createdAt': '创建时间', 'addressBookManagement.operation': '操作', 'addressBookManagement.add': '新增', @@ -1476,9 +1586,7 @@ export default { 'addressBookManagement.operationCancelled': '已取消删除操作', 'addressBookManagement.updateSuccess': '修改成功', 'addressBookManagement.addSuccess': '新增成功', - 'addressBookManagement.updateFailed': '更新失败', 'addressBookManagement.addFailed': '新增失败', - 'addressBookManagement.selectAll': '全选', 'addressBookManagement.cancelSelectAll': '取消全选', 'addressBookManagement.deviceName': '设备名称', 'addressBookManagement.deviceType': '设备类型', @@ -1495,7 +1603,6 @@ export default { 'addressBookManagement.selectDevice': '请选择一个设备查看通讯录详情', 'addressBookManagement.save': '保存权限', 'addressBookManagement.saveSuccess': '保存成功', - 'addressBookManagement.saveFailed': '保存失败', 'addressBookManagement.deviceNotSelected': '请先选择一个设备', 'addressBookManagement.addDeviceTip': '新增设备功能开发中', 'addressBookManagement.selectAgentFirst': '请先选择智能体', @@ -1551,4 +1658,4 @@ export default { 'addressBookManagement.monthsAgo': '{months}个月前', 'addressBookManagement.yearsAgo': '{years}年前', -} \ No newline at end of file +} diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index cc89ba2f..d2c1f8e5 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -806,12 +806,128 @@ 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.secretRedacted': '密鑰已脫敏', + 'agentSnapshot.configValue': '配置值', + '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)', @@ -1336,10 +1452,6 @@ export default { 'knowledgeFileUpload.viewSlices': '查看切片', 'knowledgeFileUpload.delete': '刪除', 'knowledgeFileUpload.itemsPerPage': '條/頁', - 'knowledgeFileUpload.firstPage': '首頁', - 'knowledgeFileUpload.prevPage': '上一頁', - 'knowledgeFileUpload.nextPage': '下一頁', - 'knowledgeFileUpload.totalRecords': '共{total}條記錄', 'knowledgeFileUpload.uploadDocument': '上傳文檔', 'knowledgeFileUpload.documentNamePlaceholder': '請輸入文檔名稱', 'knowledgeFileUpload.file': '文件', @@ -1378,10 +1490,7 @@ export default { 'knowledgeFileUpload.sliceCount': '切片數量', 'knowledgeFileUpload.add': '新增', 'knowledgeFileUpload.retrievalTest': '召回測試', - 'knowledgeFileUpload.testQuestion': '測試問題', - 'knowledgeFileUpload.testQuestionPlaceholder': '請輸入要測試的問題', 'knowledgeFileUpload.executeTest': '執行測試', - 'knowledgeFileUpload.testResult': '測試結果:', 'knowledgeFileUpload.selectedFiles': '已選擇文件', 'knowledgeFileUpload.totalSlices': '共{total}條記錄', 'knowledgeFileUpload.slice': '切片', @@ -1456,7 +1565,6 @@ export default { 'addressBookManagement.name': '通訊錄名稱', 'addressBookManagement.description': '通訊錄描述', 'addressBookManagement.contactCount': '聯繫人數量', - 'addressBookManagement.status': '啟用', 'addressBookManagement.createdAt': '創建時間', 'addressBookManagement.operation': '操作', 'addressBookManagement.add': '新增', @@ -1477,9 +1585,7 @@ export default { 'addressBookManagement.operationCancelled': '已取消刪除操作', 'addressBookManagement.updateSuccess': '修改成功', 'addressBookManagement.addSuccess': '新增成功', - 'addressBookManagement.updateFailed': '更新失敗', 'addressBookManagement.addFailed': '新增失敗', - 'addressBookManagement.selectAll': '全選', 'addressBookManagement.cancelSelectAll': '取消全選', 'addressBookManagement.selectAgent': '請選擇智能體', 'addressBookManagement.deviceName': '設備名稱', @@ -1497,7 +1603,6 @@ export default { 'addressBookManagement.selectDevice': '請選擇一個設備查看通訊錄詳情', 'addressBookManagement.save': '保存權限', 'addressBookManagement.saveSuccess': '儲存成功', - 'addressBookManagement.saveFailed': '儲存失敗', 'addressBookManagement.deviceNotSelected': '請先選擇一個設備', 'addressBookManagement.addDeviceTip': '新增設備功能開發中', 'addressBookManagement.selectAgentFirst': '請先選擇智能體', @@ -1553,4 +1658,4 @@ export default { 'addressBookManagement.monthsAgo': '{months}個月前', 'addressBookManagement.yearsAgo': '{years}年前', -} \ No newline at end of file +} diff --git a/main/manager-web/src/utils/date.js b/main/manager-web/src/utils/date.js index c6210bc0..84b0a927 100755 --- a/main/manager-web/src/utils/date.js +++ b/main/manager-web/src/utils/date.js @@ -39,6 +39,7 @@ function formatDateTool(date, fmt) { const o = { 'M+': date.getMonth() + 1, 'd+': date.getDate(), + 'H+': date.getHours(), 'h+': date.getHours(), 'm+': date.getMinutes(), 's+': date.getSeconds() diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index bc1fa900..d6dcf4bb 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -16,6 +16,9 @@ {{ form.agentName }} + + {{ $t("roleConfig.currentVersion", { version: currentVersionNo }) }} +
{{ $t("roleConfig.restartNotice") }}
+ + {{ $t("roleConfig.snapshotHistory") }} + {{ $t("roleConfig.saveConfig") }} @@ -313,7 +319,7 @@ placement="top" >
-
功能名称: {{ func.name }}
+
{{ $t("roleConfig.functionName") }}: {{ func.name }}
{{ getFunctionDisplayChar(func.name) }} @@ -457,6 +463,13 @@ :checked-replacement-word-ids="checkedReplacementWordIds" @save="handleTtsSettingsSave" /> + @@ -470,6 +483,7 @@ import RequestService from "@/apis/httpRequest"; import FunctionDialog from "@/components/FunctionDialog.vue"; import ContextProviderDialog from "@/components/ContextProviderDialog.vue"; import TtsAdvancedSettings from "@/components/TtsAdvancedSettings.vue"; +import AgentSnapshotDialog from "@/components/AgentSnapshotDialog.vue"; import HeaderBar from "@/components/HeaderBar.vue"; import i18n from "@/i18n"; import featureManager from "@/utils/featureManager"; @@ -477,11 +491,12 @@ import VersionFooter from "@/components/VersionFooter.vue"; export default { name: "RoleConfigPage", - components: { HeaderBar, FunctionDialog, ContextProviderDialog, TtsAdvancedSettings, VersionFooter }, + components: { HeaderBar, FunctionDialog, ContextProviderDialog, TtsAdvancedSettings, AgentSnapshotDialog, VersionFooter }, data() { return { showContextProviderDialog: false, showTtsAdvancedDialog: false, + showSnapshotDialog: false, ttsSettings: { volume: 0, speed: 0, @@ -529,6 +544,7 @@ export default { voiceOptions: [], voiceDetails: {}, // 保存完整的音色信息 showFunctionDialog: false, + currentVersionNo: null, currentFunctions: [], currentContextProviders: [], allFunctions: [], @@ -546,6 +562,7 @@ export default { asr: false, // 语音识别功能状态 }, dynamicTags: [], + originalTagNames: [], inputVisible: false, inputValue: '', checkedReplacementWordIds: [] @@ -555,14 +572,26 @@ export default { goToHome() { this.$router.push("/home"); }, - async saveConfig() { - try { - await this.handleSaveAgentTags(this.$route.query.agentId); - } catch (error) { - console.error('保存标签失败:', error); - return; + normalizeFunctionParams(params, fallback = {}) { + if (params === null || params === undefined || params === '') { + return { ...fallback }; } - + if (typeof params === 'string') { + try { + const parsed = JSON.parse(params); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? parsed + : { ...fallback }; + } catch (error) { + return { ...fallback }; + } + } + if (typeof params === 'object' && !Array.isArray(params)) { + return { ...params }; + } + return { ...fallback }; + }, + async saveConfig() { const configData = { agentCode: this.form.agentCode, agentName: this.form.agentName, @@ -585,12 +614,17 @@ export default { functions: this.currentFunctions.map((item) => { return { pluginId: item.id, - paramInfo: item.params, + paramInfo: this.normalizeFunctionParams(item.params), }; }), contextProviders: this.currentContextProviders, correctWordFileIds: this.checkedReplacementWordIds, }; + 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) { @@ -602,12 +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, - }); + 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"), @@ -617,6 +659,26 @@ export default { }); }, + handleSnapshotRestored() { + const agentId = this.$route.query.agentId; + if (agentId) { + this.fetchAgentConfig(agentId); + this.getAgentTags(agentId); + this.fetchCurrentVersion(agentId); + } + }, + fetchCurrentVersion(agentId) { + if (!agentId) { + this.currentVersionNo = null; + return; + } + + 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"), { confirmButtonText: i18n.t("button.ok"), @@ -754,7 +816,7 @@ export default { id: mapping.pluginId, name: meta.name, // 后端如果还有 paramInfo 字段就用 mapping.paramInfo,否则用 meta.params 默认值 - params: mapping.paramInfo || { ...meta.params }, + params: this.normalizeFunctionParams(mapping.paramInfo, meta.params), fieldsMeta: meta.fieldsMeta, // 保留以便对话框渲染 tooltip }; }); @@ -806,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")); } }); } @@ -1329,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; @@ -1339,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); @@ -1382,6 +1451,7 @@ export default { this.fetchAgentConfig(agentId); this.getAgentTags(agentId); this.fetchAllFunctions(); + this.fetchCurrentVersion(agentId); } this.fetchModelOptions(); this.fetchTemplates(); @@ -1507,6 +1577,18 @@ export default { flex-shrink: 0; } +.current-version-tag { + flex-shrink: 0; + padding: 3px 9px; + border: 1px solid #dfe7ff; + border-radius: 999px; + background: #f4f7ff; + color: #5778ff; + font-size: 12px; + font-weight: 500; + line-height: 1.5; +} + .more-tag { cursor: pointer; flex-shrink: 0; @@ -1785,6 +1867,16 @@ export default { font-size: 14px; } +.header-actions .history-btn { + background: #ffffff; + color: #4d5b7c; + border: 1px solid #d8dce8; + border-radius: 18px; + padding: 8px 16px; + height: 32px; + font-size: 14px; +} + .header-actions .reset-btn { background: #e6ebff; color: #5778ff; diff --git a/main/xiaozhi-server/config/assets/wakeup_words/ed76d459636c2481aec828516c1b4f54.wav b/main/xiaozhi-server/config/assets/wakeup_words/ed76d459636c2481aec828516c1b4f54.wav deleted file mode 100644 index 1e7246be..00000000 Binary files a/main/xiaozhi-server/config/assets/wakeup_words/ed76d459636c2481aec828516c1b4f54.wav and /dev/null 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")