Fix agent snapshot history issues

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