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..d41004fc 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 @@ -127,7 +127,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<>(); } @@ -332,8 +332,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..fff4d81f --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentSnapshotController.java @@ -0,0 +1,66 @@ +package xiaozhi.modules.agent.controller; + +import java.util.Map; + +import org.apache.shiro.authz.annotation.RequiresPermissions; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +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.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, + @RequestParam Map 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<>(); + } + + 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/AgentSnapshotDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentSnapshotDao.java new file mode 100644 index 00000000..9c6c54f7 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentSnapshotDao.java @@ -0,0 +1,14 @@ +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 selectNextSnapshot(@Param("agentId") String agentId, @Param("versionNo") Integer versionNo); +} 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/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/AgentSnapshotEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentSnapshotEntity.java new file mode 100644 index 00000000..2783bcc7 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentSnapshotEntity.java @@ -0,0 +1,44 @@ +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 = "创建者") + 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..c985ae59 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 @@ -93,6 +93,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..511e420a --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentSnapshotService.java @@ -0,0 +1,19 @@ +package xiaozhi.modules.agent.service; + +import java.util.Map; + +import xiaozhi.common.page.PageData; +import xiaozhi.common.service.BaseService; +import xiaozhi.modules.agent.dto.AgentUpdateDTO; +import xiaozhi.modules.agent.entity.AgentSnapshotEntity; +import xiaozhi.modules.agent.vo.AgentSnapshotVO; + +public interface AgentSnapshotService extends BaseService { + void createSnapshot(String agentId, String source, AgentUpdateDTO pendingUpdate); + + PageData page(String agentId, Map params); + + AgentSnapshotVO getSnapshot(String agentId, String snapshotId); + + void restoreSnapshot(String agentId, String snapshotId); +} 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..02d7a76c 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 @@ -43,9 +43,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 +74,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) { @@ -272,17 +274,28 @@ 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); - } - - // 只更新提供的非空字段 + // 根据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) { + // 先查询现有实体 + AgentEntity existingEntity = this.getAgentById(agentId); + if (existingEntity == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + + if (createSnapshot) { + agentSnapshotService.createSnapshot(agentId, "config", dto); + } + + // 只更新提供的非空字段 if (dto.getAgentName() != null) { existingEntity.setAgentName(dto.getAgentName()); } @@ -426,11 +439,16 @@ public class AgentServiceImpl extends BaseServiceImpl imp } // 更新替换词文件关联 - if (dto.getCorrectWordFileIds() != null) { - correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds()); - } - - boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId()); + 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(dto.getLlmModelId(), dto.getIntentModelId()); if (!b) { throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH); } @@ -576,4 +594,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..8cbf7cb5 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentSnapshotServiceImpl.java @@ -0,0 +1,475 @@ +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.UUID; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.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.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.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 TypeReference> STRING_LIST_TYPE = new TypeReference<>() { + }; + private static final TypeReference> PARAM_INFO_TYPE = new TypeReference<>() { + }; + + 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, AgentUpdateDTO pendingUpdate) { + AgentSnapshotDataDTO snapshotData = buildSnapshotData(agentId); + List changedFields = getChangedFields(snapshotData, pendingUpdate); + if (pendingUpdate != null && changedFields.isEmpty()) { + return; + } + + AgentInfoVO agent = agentDao.selectAgentInfoById(agentId); + if (agent == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + + AgentSnapshotEntity entity = new AgentSnapshotEntity(); + entity.setAgentId(agentId); + entity.setUserId(agent.getUserId()); + entity.setSnapshotData(JsonUtils.toJsonString(snapshotData)); + entity.setChangedFields(JsonUtils.toJsonString(changedFields)); + entity.setSource(StringUtils.defaultIfBlank(source, "config")); + entity.setCreator(SecurityUser.getUserId()); + entity.setCreatedAt(new Date()); + insertSnapshotWithRetry(entity); + } + + @Override + public PageData page(String agentId, Map params) { + IPage page = getPage(params, "created_at", false); + IPage result = agentSnapshotDao.selectPage(page, + new QueryWrapper().eq("agent_id", agentId)); + 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) { + AgentSnapshotEntity snapshot = getSnapshotEntity(agentId, snapshotId); + AgentSnapshotDataDTO data = JsonUtils.parseObject(snapshot.getSnapshotData(), AgentSnapshotDataDTO.class); + if (data == null) { + throw new RenException("快照数据为空,无法恢复"); + } + + createSnapshot(agentId, "restore", null); + + AgentEntity agent = agentDao.selectById(agentId); + if (agent == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + applyAgentFields(agent, data); + validateRestoreParams(agent); + applyMemoryPolicy(agent); + agent.setUpdater(SecurityUser.getUserId()); + agent.setUpdatedAt(new Date()); + agentDao.updateById(agent); + + restoreFunctions(agentId, data.getFunctions()); + restoreContextProviders(agentId, data.getContextProviders()); + correctWordFileService.saveAgentCorrectWords(agentId, nullToEmpty(data.getCorrectWordFileIds())); + restoreTags(agentId, data); + } + + private void insertSnapshotWithRetry(AgentSnapshotEntity entity) { + for (int attempt = 0; attempt < 3; attempt++) { + Integer maxVersionNo = agentSnapshotDao.selectMaxVersionNo(entity.getAgentId()); + entity.setVersionNo((maxVersionNo == null ? 0 : maxVersionNo) + 1); + try { + agentSnapshotDao.insert(entity); + return; + } catch (DuplicateKeyException e) { + if (attempt == 2) { + throw e; + } + } + } + } + + 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 AgentSnapshotDataDTO buildSnapshotData(String agentId) { + AgentInfoVO agent = agentDao.selectAgentInfoById(agentId); + if (agent == null) { + throw new RenException(ErrorCode.AGENT_NOT_FOUND); + } + + 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(agentId)); + data.setCorrectWordFileIds(nullToEmpty(correctWordFileService.getAgentCorrectWordFileIds(agentId))); + List tags = getSnapshotTags(agentId); + 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<>(); + compare(fields, "agentCode", current.getAgentCode(), pendingUpdate.getAgentCode()); + compare(fields, "agentName", current.getAgentName(), pendingUpdate.getAgentName()); + compare(fields, "asrModelId", current.getAsrModelId(), pendingUpdate.getAsrModelId()); + compare(fields, "vadModelId", current.getVadModelId(), pendingUpdate.getVadModelId()); + compare(fields, "llmModelId", current.getLlmModelId(), pendingUpdate.getLlmModelId()); + compare(fields, "slmModelId", current.getSlmModelId(), pendingUpdate.getSlmModelId()); + compare(fields, "vllmModelId", current.getVllmModelId(), pendingUpdate.getVllmModelId()); + compare(fields, "ttsModelId", current.getTtsModelId(), pendingUpdate.getTtsModelId()); + compare(fields, "ttsVoiceId", current.getTtsVoiceId(), pendingUpdate.getTtsVoiceId()); + compare(fields, "ttsLanguage", current.getTtsLanguage(), pendingUpdate.getTtsLanguage()); + compare(fields, "ttsVolume", current.getTtsVolume(), pendingUpdate.getTtsVolume()); + compare(fields, "ttsRate", current.getTtsRate(), pendingUpdate.getTtsRate()); + compare(fields, "ttsPitch", current.getTtsPitch(), pendingUpdate.getTtsPitch()); + compare(fields, "memModelId", current.getMemModelId(), pendingUpdate.getMemModelId()); + compare(fields, "intentModelId", current.getIntentModelId(), pendingUpdate.getIntentModelId()); + compare(fields, "chatHistoryConf", current.getChatHistoryConf(), pendingUpdate.getChatHistoryConf()); + compare(fields, "systemPrompt", current.getSystemPrompt(), pendingUpdate.getSystemPrompt()); + compare(fields, "summaryMemory", current.getSummaryMemory(), pendingUpdate.getSummaryMemory()); + compare(fields, "langCode", current.getLangCode(), pendingUpdate.getLangCode()); + compare(fields, "language", current.getLanguage(), pendingUpdate.getLanguage()); + compare(fields, "sort", current.getSort(), pendingUpdate.getSort()); + compare(fields, "functions", current.getFunctions(), pendingUpdate.getFunctions()); + compare(fields, "contextProviders", current.getContextProviders(), pendingUpdate.getContextProviders()); + compare(fields, "correctWordFileIds", current.getCorrectWordFileIds(), pendingUpdate.getCorrectWordFileIds()); + compare(fields, "tagNames", current.getTagNames(), pendingUpdate.getTagNames()); + compare(fields, "tagIds", current.getTags().stream().map(AgentSnapshotTagDTO::getId).toList(), + pendingUpdate.getTagIds()); + return fields; + } + + private void compare(List fields, String name, T current, T next) { + if (next != null && !Objects.equals(current, next)) { + fields.add(name); + } + } + + private void applyAgentFields(AgentEntity agent, AgentSnapshotDataDTO data) { + agent.setAgentCode(data.getAgentCode()); + agent.setAgentName(data.getAgentName()); + agent.setAsrModelId(data.getAsrModelId()); + agent.setVadModelId(data.getVadModelId()); + agent.setLlmModelId(data.getLlmModelId()); + agent.setSlmModelId(data.getSlmModelId()); + agent.setVllmModelId(data.getVllmModelId()); + agent.setTtsModelId(data.getTtsModelId()); + agent.setTtsVoiceId(data.getTtsVoiceId()); + agent.setTtsLanguage(data.getTtsLanguage()); + agent.setTtsVolume(data.getTtsVolume()); + agent.setTtsRate(data.getTtsRate()); + agent.setTtsPitch(data.getTtsPitch()); + agent.setMemModelId(data.getMemModelId()); + agent.setIntentModelId(data.getIntentModelId()); + agent.setChatHistoryConf(data.getChatHistoryConf()); + agent.setSystemPrompt(data.getSystemPrompt()); + agent.setSummaryMemory(data.getSummaryMemory()); + agent.setLangCode(data.getLangCode()); + agent.setLanguage(data.getLanguage()); + agent.setSort(data.getSort()); + } + + 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) { + tag = agentTagDao.selectOne(new QueryWrapper() + .eq("tag_name", snapshotTag.getTagName()) + .last("LIMIT 1")); + } + if (tag != null) { + if (Objects.equals(tag.getDeleted(), 1)) { + tag.setDeleted(0); + tag.setUpdater(SecurityUser.getUserId()); + tag.setUpdatedAt(now); + agentTagDao.updateById(tag); + } + return tag.getId(); + } + + AgentTagEntity newTag = new AgentTagEntity(); + if (StringUtils.isNotBlank(snapshotTag.getId())) { + newTag.setId(snapshotTag.getId()); + } + newTag.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.setSource(entity.getSource()); + vo.setCreator(entity.getCreator()); + vo.setCreatedAt(entity.getCreatedAt()); + if (includeData) { + vo.setSnapshotData(JsonUtils.parseObject(entity.getSnapshotData(), AgentSnapshotDataDTO.class)); + vo.setAfterSnapshotData(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 buildSnapshotData(entity.getAgentId()); + } + + private List parseChangedFields(String changedFields) { + if (StringUtils.isBlank(changedFields)) { + return Collections.emptyList(); + } + return JsonUtils.parseObject(changedFields, STRING_LIST_TYPE); + } + + private List nullToEmpty(List list) { + return list == null ? Collections.emptyList() : list; + } +} 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..e5998327 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 = "上下文源配置") 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..2caf3486 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentSnapshotVO.java @@ -0,0 +1,23 @@ +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; + private Long userId; + private Integer versionNo; + private List changedFields; + private String source; + 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..6d3460a6 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202607071530.sql @@ -0,0 +1,17 @@ +-- liquibase formatted sql + +-- changeset xiaozhi: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 COMMENT '快照数据', + `changed_fields` JSON DEFAULT NULL COMMENT '变更字段', + `source` VARCHAR(32) DEFAULT 'config' 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`) +) 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..e81ea5c1 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: Codex + 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..6fb8fb02 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,10 @@ + - \ 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..38609ac4 --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/agent/AgentSnapshotDao.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/main/manager-web/src/apis/module/agent.js b/main/manager-web/src/apis/module/agent.js index d79f8da4..2a45f197 100644 --- a/main/manager-web/src/apis/module/agent.js +++ b/main/manager-web/src/apis/module/agent.js @@ -81,6 +81,52 @@ 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(); + }, // 新增方法:获取智能体模板 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..5efc20d3 --- /dev/null +++ b/main/manager-web/src/components/AgentSnapshotDialog.vue @@ -0,0 +1,1972 @@ + + + + + diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 0248a653..9c62c087 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -806,6 +806,7 @@ 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.reset': 'Reset', 'roleConfig.agentName': 'Nickname', 'roleConfig.roleTemplate': 'Template', @@ -863,6 +864,59 @@ export default { '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.detailTitle': 'Change Details', + 'agentSnapshot.restorePreviewTitle': 'Restore Preview', + 'agentSnapshot.confirmRestore': 'Confirm Restore', + 'agentSnapshot.before': 'Before', + 'agentSnapshot.after': 'After', + 'agentSnapshot.emptyValue': 'None', + 'agentSnapshot.noChangedContent': 'No changes to display', + 'agentSnapshot.restoreConfirm': 'Restore to version #{version}? The current configuration will be saved as a new version first.', + 'agentSnapshot.restoreSuccess': 'Version restored', + 'agentSnapshot.restoreFailed': 'Failed to restore 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.field.restore': 'Restore', + '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', + // Form field Tooltip descriptions 'roleConfig.tooltip.agentName': 'Set the name of your AI agent for identification and recognition', 'roleConfig.tooltip.roleTemplate': 'Choose from preset role templates to quickly configure your agent\'s basic settings', @@ -1554,4 +1608,4 @@ export default { // Header navigation 'header.addressBook': 'Address Book', -} \ 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..71c28d3e 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -806,6 +806,7 @@ export default { 'roleConfig.addTag': '添加新标签', 'roleConfig.restartNotice': '保存配置后,需要重启设备,新的配置才会生效。', 'roleConfig.saveConfig': '保存配置', + 'roleConfig.snapshotHistory': '历史版本', 'roleConfig.reset': '重置', 'roleConfig.agentName': '助手昵称', 'roleConfig.roleTemplate': '角色模版', @@ -863,6 +864,59 @@ export default { 'roleConfig.cannotPlayAudio': '无法播放音频', 'roleConfig.audioPlayError': '播放音频过程出错', + 'agentSnapshot.title': '历史版本', + 'agentSnapshot.empty': '暂无历史版本', + 'agentSnapshot.version': '版本', + 'agentSnapshot.createdAt': '保存时间', + 'agentSnapshot.source': '来源', + 'agentSnapshot.changedFields': '变更内容', + 'agentSnapshot.actions': '操作', + 'agentSnapshot.view': '查看', + 'agentSnapshot.restore': '恢复', + 'agentSnapshot.detailTitle': '变更详情', + 'agentSnapshot.restorePreviewTitle': '恢复确认', + 'agentSnapshot.confirmRestore': '确认恢复', + 'agentSnapshot.before': '变化前', + 'agentSnapshot.after': '变化后', + 'agentSnapshot.emptyValue': '无', + 'agentSnapshot.noChangedContent': '暂无可展示的变更内容', + 'agentSnapshot.restoreConfirm': '确定恢复到版本 #{version} 吗?当前配置会先自动保存为新的历史版本。', + 'agentSnapshot.restoreSuccess': '已恢复历史版本', + 'agentSnapshot.restoreFailed': '恢复历史版本失败', + 'agentSnapshot.fetchFailed': '获取历史版本失败', + 'agentSnapshot.detailFailed': '获取快照详情失败', + 'agentSnapshot.currentVersion': '当前版本', + 'agentSnapshot.source.config': '配置保存', + 'agentSnapshot.source.current': '当前配置', + 'agentSnapshot.source.restore': '恢复前', + 'agentSnapshot.field.restore': '恢复操作', + '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', + // 表单字段 Tooltip 提示说明 'roleConfig.tooltip.agentName': '设置智能体的名称,用于标识和识别您的AI助手', 'roleConfig.tooltip.roleTemplate': '从预设的角色模板中选择,快速配置智能体的基础设定', @@ -1551,4 +1605,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..a0075317 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -16,6 +16,9 @@ {{ form.agentName }} + + 当前版本 #{{ currentVersionNo }} +
{{ $t("roleConfig.restartNotice") }}
+ + {{ $t("roleConfig.snapshotHistory") }} + {{ $t("roleConfig.saveConfig") }} @@ -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: [], @@ -555,14 +571,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,11 +613,12 @@ 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, + tagNames: this.dynamicTags.map(tag => tag.tagName), }; // 只在用户设置了TTS参数时才传递(不为null/undefined) @@ -608,6 +637,7 @@ export default { message: i18n.t("roleConfig.saveSuccess"), showClose: true, }); + this.fetchCurrentVersion(this.$route.query.agentId); } else { this.$message.error({ message: data.msg || i18n.t("roleConfig.saveFailed"), @@ -617,6 +647,32 @@ 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.getAgentSnapshots( + agentId, + { page: "1", limit: "1" }, + ({ data }) => { + if (data.code === 0) { + const latest = data.data?.list?.[0]; + const latestVersionNo = Number(latest?.versionNo) || 0; + this.currentVersionNo = latestVersionNo + 1; + } + } + ); + }, resetConfig() { this.$confirm(i18n.t("roleConfig.confirmReset"), i18n.t("message.info"), { confirmButtonText: i18n.t("button.ok"), @@ -754,7 +810,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 }; }); @@ -1382,6 +1438,7 @@ export default { this.fetchAgentConfig(agentId); this.getAgentTags(agentId); this.fetchAllFunctions(); + this.fetchCurrentVersion(agentId); } this.fetchModelOptions(); this.fetchTemplates(); @@ -1507,6 +1564,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 +1854,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;