mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-21 22:53:56 +08:00
Merge remote-tracking branch 'origin/main' into fix-agent-idor
# Conflicts: # main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java # main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentService.java # main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
<liquibase-core.version>4.20.0</liquibase-core.version>
|
||||
<aliyun-sms-version>4.1.0</aliyun-sms-version>
|
||||
<okio-version>3.4.0</okio-version>
|
||||
<skipTests>true</skipTests>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -275,7 +276,7 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skipTests>true</skipTests>
|
||||
<skipTests>${skipTests}</skipTests>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -342,7 +342,10 @@ public class AgentController {
|
||||
requireAgentPermission(id);
|
||||
List<String> tagIds = (List<String>) params.get("tagIds");
|
||||
List<String> tagNames = (List<String>) params.get("tagNames");
|
||||
agentTagService.saveAgentTags(id, tagIds, tagNames);
|
||||
AgentUpdateDTO dto = new AgentUpdateDTO();
|
||||
dto.setTagIds(tagIds);
|
||||
dto.setTagNames(tagNames);
|
||||
agentService.updateAgentById(id, dto);
|
||||
return new Result<Void>().ok(null);
|
||||
}
|
||||
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package xiaozhi.modules.agent.controller;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Tag(name = "智能体快照")
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/agent/{agentId}/snapshots")
|
||||
public class AgentSnapshotController {
|
||||
private final AgentSnapshotService agentSnapshotService;
|
||||
private final AgentService agentService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取智能体快照列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<PageData<AgentSnapshotVO>> page(
|
||||
@PathVariable String agentId,
|
||||
@ParameterObject AgentSnapshotPageDTO params) {
|
||||
checkPermission(agentId);
|
||||
return new Result<PageData<AgentSnapshotVO>>().ok(agentSnapshotService.page(agentId, params));
|
||||
}
|
||||
|
||||
@GetMapping("/{snapshotId}")
|
||||
@Operation(summary = "获取智能体快照详情")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<AgentSnapshotVO> getSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
|
||||
checkPermission(agentId);
|
||||
return new Result<AgentSnapshotVO>().ok(agentSnapshotService.getSnapshot(agentId, snapshotId));
|
||||
}
|
||||
|
||||
@PostMapping("/{snapshotId}/restore")
|
||||
@Operation(summary = "恢复智能体快照")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> restore(@PathVariable String agentId, @PathVariable String snapshotId) {
|
||||
checkPermission(agentId);
|
||||
agentSnapshotService.restoreSnapshot(agentId, snapshotId);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{snapshotId}")
|
||||
@Operation(summary = "删除智能体历史快照")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> deleteSnapshot(@PathVariable String agentId, @PathVariable String snapshotId) {
|
||||
checkPermission(agentId);
|
||||
agentSnapshotService.deleteSnapshot(agentId, snapshotId);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
private void checkPermission(String agentId) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
if (user == null || !agentService.checkAgentPermission(agentId, user.getId())) {
|
||||
throw new RenException("没有权限访问该智能体快照");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,4 +36,11 @@ public interface AgentDao extends BaseDao<AgentEntity> {
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId);
|
||||
|
||||
/**
|
||||
* 锁定智能体主记录,用于串行化同一智能体的配置写入
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
AgentEntity selectByIdForUpdate(@Param("agentId") String agentId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
|
||||
|
||||
@Mapper
|
||||
public interface AgentSnapshotDao extends BaseDao<AgentSnapshotEntity> {
|
||||
Integer selectMaxVersionNo(@Param("agentId") String agentId);
|
||||
|
||||
AgentSnapshotEntity selectLatestSnapshot(@Param("agentId") String agentId);
|
||||
|
||||
AgentSnapshotEntity selectNextSnapshot(@Param("agentId") String agentId, @Param("versionNo") Integer versionNo);
|
||||
|
||||
int insertWithNextVersion(@Param("snapshot") AgentSnapshotEntity snapshot);
|
||||
|
||||
int deleteOlderThanKeepLimit(@Param("agentId") String agentId, @Param("keepLimit") int keepLimit);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体快照数据")
|
||||
public class AgentSnapshotDataDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String agentCode;
|
||||
private String agentName;
|
||||
private String asrModelId;
|
||||
private String vadModelId;
|
||||
private String llmModelId;
|
||||
private String slmModelId;
|
||||
private String vllmModelId;
|
||||
private String ttsModelId;
|
||||
private String ttsVoiceId;
|
||||
private String ttsLanguage;
|
||||
private Integer ttsVolume;
|
||||
private Integer ttsRate;
|
||||
private Integer ttsPitch;
|
||||
private String memModelId;
|
||||
private String intentModelId;
|
||||
private Integer chatHistoryConf;
|
||||
private String systemPrompt;
|
||||
private String summaryMemory;
|
||||
private String langCode;
|
||||
private String language;
|
||||
private Integer sort;
|
||||
private List<AgentUpdateDTO.FunctionInfo> functions;
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
private List<String> correctWordFileIds;
|
||||
private List<String> tagNames;
|
||||
private List<AgentSnapshotTagDTO> tags;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体快照标签")
|
||||
public class AgentSnapshotTagDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String id;
|
||||
private String tagName;
|
||||
private Integer sort;
|
||||
}
|
||||
@@ -4,9 +4,12 @@ import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* 智能体更新DTO
|
||||
@@ -91,14 +94,49 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "替换词文件ID列表", nullable = true)
|
||||
private List<String> correctWordFileIds;
|
||||
|
||||
@Schema(description = "标签名称列表", nullable = true)
|
||||
private List<String> tagNames;
|
||||
|
||||
@Schema(description = "标签ID列表", nullable = true)
|
||||
private List<String> tagIds;
|
||||
|
||||
@Data
|
||||
@Schema(description = "插件函数信息")
|
||||
public static class FunctionInfo implements Serializable {
|
||||
private static final TypeReference<HashMap<String, Object>> PARAM_INFO_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
@Schema(description = "插件ID", example = "plugin_01")
|
||||
private String pluginId;
|
||||
|
||||
@Schema(description = "函数参数信息", nullable = true)
|
||||
private HashMap<String, Object> paramInfo;
|
||||
private HashMap<String, Object> paramInfo = new HashMap<>();
|
||||
|
||||
public void setParamInfo(Object paramInfo) {
|
||||
this.paramInfo = normalizeParamInfo(paramInfo);
|
||||
}
|
||||
|
||||
private static HashMap<String, Object> normalizeParamInfo(Object paramInfo) {
|
||||
if (paramInfo == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
if (paramInfo instanceof String value) {
|
||||
if (value.trim().isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return JsonUtils.parseObject(value, PARAM_INFO_TYPE);
|
||||
}
|
||||
if (paramInfo instanceof Map<?, ?> value) {
|
||||
HashMap<String, Object> normalized = new HashMap<>();
|
||||
value.forEach((key, val) -> {
|
||||
if (key != null) {
|
||||
normalized.put(String.valueOf(key), val);
|
||||
}
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
return JsonUtils.parseObject(JsonUtils.toJsonString(paramInfo), PARAM_INFO_TYPE);
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
||||
|
||||
+2
-2
@@ -7,11 +7,11 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
import xiaozhi.modules.agent.typehandler.ContextProviderListTypeHandler;
|
||||
|
||||
@Data
|
||||
@TableName(value = "ai_agent_context_provider", autoResultMap = true)
|
||||
@@ -26,7 +26,7 @@ public class AgentContextProviderEntity {
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@TableField(typeHandler = ContextProviderListTypeHandler.class)
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ai_agent_snapshot")
|
||||
@Schema(description = "智能体配置快照")
|
||||
public class AgentSnapshotEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "快照ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "智能体ID")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "所属用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "版本号")
|
||||
private Integer versionNo;
|
||||
|
||||
@Schema(description = "快照数据JSON")
|
||||
private String snapshotData;
|
||||
|
||||
@Schema(description = "变更字段JSON")
|
||||
private String changedFields;
|
||||
|
||||
@Schema(description = "快照来源")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "恢复来源快照ID")
|
||||
private String restoreFromSnapshotId;
|
||||
|
||||
@Schema(description = "恢复来源版本号")
|
||||
private Integer restoreFromVersionNo;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
}
|
||||
@@ -60,6 +60,13 @@ public interface AgentService extends BaseService<AgentEntity> {
|
||||
*/
|
||||
void deleteAgentByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 删除智能体及其关联数据
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAgent(String agentId);
|
||||
|
||||
/**
|
||||
* 获取用户智能体列表
|
||||
*
|
||||
@@ -129,6 +136,15 @@ public interface AgentService extends BaseService<AgentEntity> {
|
||||
*/
|
||||
void deleteAgentById(String agentId, Long userId);
|
||||
|
||||
/**
|
||||
* 更新智能体
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param dto 更新智能体所需的信息
|
||||
* @param createSnapshot 是否创建配置快照
|
||||
*/
|
||||
void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot);
|
||||
|
||||
/**
|
||||
* 创建智能体
|
||||
*
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
|
||||
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
|
||||
|
||||
public interface AgentSnapshotService extends BaseService<AgentSnapshotEntity> {
|
||||
void createSnapshot(String agentId, String source);
|
||||
|
||||
PageData<AgentSnapshotVO> page(String agentId, AgentSnapshotPageDTO params);
|
||||
|
||||
AgentSnapshotVO getSnapshot(String agentId, String snapshotId);
|
||||
|
||||
void restoreSnapshot(String agentId, String snapshotId);
|
||||
|
||||
void deleteSnapshot(String agentId, String snapshotId);
|
||||
|
||||
Integer getCurrentVersionNo(String agentId);
|
||||
|
||||
void deleteByAgentId(String agentId);
|
||||
}
|
||||
+1
-1
@@ -118,7 +118,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
{
|
||||
setSummaryMemory(summaryDTO.getSummary());
|
||||
}
|
||||
});
|
||||
}, false);
|
||||
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
|
||||
} else {
|
||||
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
|
||||
|
||||
+63
-26
@@ -15,7 +15,6 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -46,6 +45,7 @@ import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
@@ -77,6 +77,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentTagService agentTagService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
private final AgentSnapshotService agentSnapshotService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
@@ -111,6 +112,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
// 查询替换词文件ID列表
|
||||
List<String> correctWordFileIds = correctWordFileService.getAgentCorrectWordFileIds(id);
|
||||
agent.setCorrectWordFileIds(correctWordFileIds);
|
||||
agent.setCurrentVersionNo(agentSnapshotService.getCurrentVersionNo(id));
|
||||
|
||||
// 无需额外查询插件列表,已通过SQL查询出来
|
||||
return agent;
|
||||
@@ -196,10 +198,30 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteAgentByUserId(Long userId) {
|
||||
UpdateWrapper<AgentEntity> wrapper = new UpdateWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
baseDao.delete(wrapper);
|
||||
List<AgentEntity> agents = baseDao.selectList(new QueryWrapper<AgentEntity>()
|
||||
.select("id")
|
||||
.eq("user_id", userId));
|
||||
for (AgentEntity agent : agents) {
|
||||
deleteAgent(agent.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteAgent(String agentId) {
|
||||
if (agentDao.selectByIdForUpdate(agentId) == null) {
|
||||
return;
|
||||
}
|
||||
deviceService.deleteByAgentId(agentId);
|
||||
agentChatHistoryService.deleteByAgentId(agentId, true, true);
|
||||
agentPluginMappingService.deleteByAgentId(agentId);
|
||||
agentContextProviderService.deleteByAgentId(agentId);
|
||||
correctWordFileService.deleteMappingsByAgentId(agentId);
|
||||
agentTagService.deleteAgentTags(agentId);
|
||||
agentSnapshotService.deleteByAgentId(agentId);
|
||||
deleteById(agentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -323,20 +345,39 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
|
||||
AgentEntity existingEntity = getAgentEntityOrThrow(agentId);
|
||||
requireCurrentUserPermissionIfPresent(existingEntity);
|
||||
updateAgentEntity(agentId, dto, existingEntity);
|
||||
updateAgentById(agentId, dto, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId) {
|
||||
AgentEntity existingEntity = getAgentEntityOrThrow(agentId);
|
||||
requireAgentPermission(existingEntity, userId);
|
||||
updateAgentEntity(agentId, dto, existingEntity);
|
||||
updateAgentById(agentId, dto, userId, true);
|
||||
}
|
||||
|
||||
// 根据id更新智能体信息
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAgentById(String agentId, AgentUpdateDTO dto, boolean createSnapshot) {
|
||||
updateAgentById(agentId, dto, null, createSnapshot);
|
||||
}
|
||||
|
||||
private void updateAgentById(String agentId, AgentUpdateDTO dto, Long userId, boolean createSnapshot) {
|
||||
AgentEntity lockedAgent = agentDao.selectByIdForUpdate(agentId);
|
||||
if (lockedAgent == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
if (userId == null) {
|
||||
requireCurrentUserPermissionIfPresent(lockedAgent);
|
||||
} else {
|
||||
requireAgentPermission(lockedAgent, userId);
|
||||
}
|
||||
|
||||
// 锁定后查询现有实体和关联配置
|
||||
AgentEntity existingEntity = this.getAgentById(agentId);
|
||||
if (createSnapshot && agentSnapshotService.getCurrentVersionNo(agentId) == 0) {
|
||||
agentSnapshotService.createSnapshot(agentId, "initial");
|
||||
}
|
||||
|
||||
private void updateAgentEntity(String agentId, AgentUpdateDTO dto, AgentEntity existingEntity) {
|
||||
// 只更新提供的非空字段
|
||||
if (dto.getAgentName() != null) {
|
||||
existingEntity.setAgentName(dto.getAgentName());
|
||||
@@ -485,11 +526,19 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
correctWordFileService.saveAgentCorrectWords(agentId, dto.getCorrectWordFileIds());
|
||||
}
|
||||
|
||||
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
||||
// 更新智能体标签
|
||||
if (dto.getTagNames() != null || dto.getTagIds() != null) {
|
||||
agentTagService.saveAgentTags(agentId, dto.getTagIds(), dto.getTagNames());
|
||||
}
|
||||
|
||||
boolean b = validateLLMIntentParams(existingEntity.getLlmModelId(), existingEntity.getIntentModelId());
|
||||
if (!b) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
}
|
||||
this.updateById(existingEntity);
|
||||
if (createSnapshot) {
|
||||
agentSnapshotService.createSnapshot(agentId, "config");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -504,7 +553,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
|
||||
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
|
||||
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
|
||||
updateAgentById(device.getAgentId(), agentUpdateDTO, userId);
|
||||
updateAgentById(device.getAgentId(), agentUpdateDTO, userId, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -512,19 +561,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
public void deleteAgentById(String agentId, Long userId) {
|
||||
AgentEntity agent = getAgentEntityOrThrow(agentId);
|
||||
requireAgentPermission(agent, userId);
|
||||
|
||||
// 先删除关联的设备
|
||||
deviceService.deleteByAgentId(agentId);
|
||||
// 删除关联的聊天记录
|
||||
agentChatHistoryService.deleteByAgentId(agentId, true, true);
|
||||
// 删除关联的插件
|
||||
agentPluginMappingService.deleteByAgentId(agentId);
|
||||
// 删除关联的上下文源配置
|
||||
agentContextProviderService.deleteByAgentId(agentId);
|
||||
// 删除关联的替换词文件关联记录
|
||||
correctWordFileService.deleteMappingsByAgentId(agentId);
|
||||
// 再删除智能体
|
||||
baseDao.deleteById(agentId);
|
||||
deleteAgent(agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+759
@@ -0,0 +1,759 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dao.AgentSnapshotDao;
|
||||
import xiaozhi.modules.agent.dao.AgentTagDao;
|
||||
import xiaozhi.modules.agent.dao.AgentTagRelationDao;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotTagDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTagRelationEntity;
|
||||
import xiaozhi.modules.agent.Enums.AgentSnapshotField;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
import xiaozhi.modules.agent.service.AgentTagService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class AgentSnapshotServiceImpl extends BaseServiceImpl<AgentSnapshotDao, AgentSnapshotEntity>
|
||||
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<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 AgentDao agentDao;
|
||||
private final AgentTagDao agentTagDao;
|
||||
private final AgentTagRelationDao agentTagRelationDao;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentTagService agentTagService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final CorrectWordFileService correctWordFileService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void createSnapshot(String agentId, String source) {
|
||||
lockAgent(agentId);
|
||||
AgentInfoVO agent = getAgentInfo(agentId);
|
||||
AgentSnapshotDataDTO snapshotData = buildSnapshotData(agent);
|
||||
AgentSnapshotEntity previousSnapshot = agentSnapshotDao.selectLatestSnapshot(agentId);
|
||||
List<String> changedFields;
|
||||
if (previousSnapshot == null) {
|
||||
changedFields = List.of("initial");
|
||||
} else {
|
||||
AgentSnapshotDataDTO previousData = JsonUtils.parseObject(previousSnapshot.getSnapshotData(),
|
||||
AgentSnapshotDataDTO.class);
|
||||
changedFields = getChangedFields(previousData, snapshotData);
|
||||
}
|
||||
if (changedFields.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
insertSnapshot(agentId, agent.getUserId(), source, snapshotData, changedFields);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PageData<AgentSnapshotVO> page(String agentId, AgentSnapshotPageDTO params) {
|
||||
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,
|
||||
new QueryWrapper<AgentSnapshotEntity>()
|
||||
.eq("agent_id", agentId)
|
||||
.le(pageParams.getMaxVersionNo() != null, "version_no", pageParams.getMaxVersionNo()));
|
||||
List<AgentSnapshotVO> list = result.getRecords().stream()
|
||||
.map(entity -> toVO(entity, false))
|
||||
.toList();
|
||||
return new PageData<>(list, result.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AgentSnapshotVO getSnapshot(String agentId, String snapshotId) {
|
||||
AgentSnapshotEntity entity = getSnapshotEntity(agentId, snapshotId);
|
||||
return toVO(entity, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void restoreSnapshot(String agentId, String snapshotId) {
|
||||
AgentEntity agent = agentDao.selectByIdForUpdate(agentId);
|
||||
if (agent == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
AgentSnapshotEntity snapshot = getSnapshotEntity(agentId, snapshotId);
|
||||
AgentSnapshotDataDTO data = JsonUtils.parseObject(snapshot.getSnapshotData(), AgentSnapshotDataDTO.class);
|
||||
if (data == null) {
|
||||
throw new RenException("快照数据为空,无法恢复");
|
||||
}
|
||||
|
||||
AgentInfoVO currentAgent = getAgentInfo(agentId);
|
||||
AgentSnapshotDataDTO currentData = buildSnapshotData(currentAgent);
|
||||
AgentSnapshotDataDTO restoreData = preserveCurrentSensitiveValues(data, currentData);
|
||||
List<String> changedFields = getChangedFields(currentData, restoreData);
|
||||
if (changedFields.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
applyAgentFields(agent, restoreData);
|
||||
validateRestoreParams(agent);
|
||||
applyMemoryPolicy(agent);
|
||||
agent.setUpdater(SecurityUser.getUserId());
|
||||
agent.setUpdatedAt(new Date());
|
||||
agentDao.updateById(agent);
|
||||
|
||||
restoreFunctions(agentId, restoreData.getFunctions());
|
||||
restoreContextProviders(agentId, restoreData.getContextProviders());
|
||||
correctWordFileService.saveAgentCorrectWords(agentId, nullToEmpty(restoreData.getCorrectWordFileIds()));
|
||||
restoreTags(agentId, restoreData);
|
||||
insertSnapshot(agentId, currentAgent.getUserId(), "restore", restoreData, changedFields,
|
||||
snapshot.getId(), snapshot.getVersionNo());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteSnapshot(String agentId, String snapshotId) {
|
||||
AgentSnapshotEntity entity = getSnapshotEntity(agentId, snapshotId);
|
||||
Integer maxVersionNo = agentSnapshotDao.selectMaxVersionNo(agentId);
|
||||
if (Objects.equals(entity.getVersionNo(), maxVersionNo)) {
|
||||
throw new RenException("最新历史版本不能删除");
|
||||
}
|
||||
deleteById(snapshotId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getCurrentVersionNo(String agentId) {
|
||||
Integer maxVersionNo = agentSnapshotDao.selectMaxVersionNo(agentId);
|
||||
return maxVersionNo == null ? 0 : maxVersionNo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteByAgentId(String agentId) {
|
||||
agentSnapshotDao.delete(new QueryWrapper<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.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
entity.setAgentId(agentId);
|
||||
entity.setUserId(userId);
|
||||
entity.setSnapshotData(JsonUtils.toJsonString(redactSnapshotData(snapshotData)));
|
||||
entity.setChangedFields(JsonUtils.toJsonString(changedFields));
|
||||
entity.setSource(StringUtils.defaultIfBlank(source, "config"));
|
||||
entity.setRestoreFromSnapshotId(restoreFromSnapshotId);
|
||||
entity.setRestoreFromVersionNo(restoreFromVersionNo);
|
||||
entity.setCreator(SecurityUser.getUserId());
|
||||
entity.setCreatedAt(new Date());
|
||||
int inserted = agentSnapshotDao.insertWithNextVersion(entity);
|
||||
if (inserted != 1) {
|
||||
throw new RenException("快照版本号生成失败");
|
||||
}
|
||||
pruneSnapshots(agentId);
|
||||
}
|
||||
|
||||
private void lockAgent(String agentId) {
|
||||
if (agentDao.selectByIdForUpdate(agentId) == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
private void pruneSnapshots(String agentId) {
|
||||
agentSnapshotDao.deleteOlderThanKeepLimit(agentId, MAX_SNAPSHOTS_PER_AGENT);
|
||||
}
|
||||
|
||||
private AgentSnapshotEntity getSnapshotEntity(String agentId, String snapshotId) {
|
||||
AgentSnapshotEntity entity = selectById(snapshotId);
|
||||
if (entity == null || !Objects.equals(agentId, entity.getAgentId())) {
|
||||
throw new RenException("快照不存在");
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private AgentInfoVO getAgentInfo(String agentId) {
|
||||
AgentInfoVO agent = agentDao.selectAgentInfoById(agentId);
|
||||
if (agent == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO buildSnapshotData(String agentId) {
|
||||
return buildSnapshotData(getAgentInfo(agentId));
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO buildSnapshotData(AgentInfoVO agent) {
|
||||
AgentSnapshotDataDTO data = new AgentSnapshotDataDTO();
|
||||
data.setAgentCode(agent.getAgentCode());
|
||||
data.setAgentName(agent.getAgentName());
|
||||
data.setAsrModelId(agent.getAsrModelId());
|
||||
data.setVadModelId(agent.getVadModelId());
|
||||
data.setLlmModelId(agent.getLlmModelId());
|
||||
data.setSlmModelId(agent.getSlmModelId());
|
||||
data.setVllmModelId(agent.getVllmModelId());
|
||||
data.setTtsModelId(agent.getTtsModelId());
|
||||
data.setTtsVoiceId(agent.getTtsVoiceId());
|
||||
data.setTtsLanguage(agent.getTtsLanguage());
|
||||
data.setTtsVolume(agent.getTtsVolume());
|
||||
data.setTtsRate(agent.getTtsRate());
|
||||
data.setTtsPitch(agent.getTtsPitch());
|
||||
data.setMemModelId(agent.getMemModelId());
|
||||
data.setIntentModelId(agent.getIntentModelId());
|
||||
data.setChatHistoryConf(agent.getChatHistoryConf());
|
||||
data.setSystemPrompt(agent.getSystemPrompt());
|
||||
data.setSummaryMemory(agent.getSummaryMemory());
|
||||
data.setLangCode(agent.getLangCode());
|
||||
data.setLanguage(agent.getLanguage());
|
||||
data.setSort(agent.getSort());
|
||||
data.setFunctions(toFunctionInfo(agent.getFunctions()));
|
||||
data.setContextProviders(getContextProviders(agent.getId()));
|
||||
data.setCorrectWordFileIds(nullToEmpty(correctWordFileService.getAgentCorrectWordFileIds(agent.getId())));
|
||||
List<AgentSnapshotTagDTO> tags = getSnapshotTags(agent.getId());
|
||||
data.setTags(tags);
|
||||
data.setTagNames(tags.stream().map(AgentSnapshotTagDTO::getTagName).toList());
|
||||
return data;
|
||||
}
|
||||
|
||||
private List<AgentSnapshotTagDTO> getSnapshotTags(String agentId) {
|
||||
List<AgentTagEntity> tags = agentTagDao.selectByAgentId(agentId);
|
||||
if (tags == null || tags.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<AgentSnapshotTagDTO> snapshotTags = new ArrayList<>();
|
||||
for (int i = 0; i < tags.size(); i++) {
|
||||
AgentTagEntity tag = tags.get(i);
|
||||
AgentSnapshotTagDTO snapshotTag = new AgentSnapshotTagDTO();
|
||||
snapshotTag.setId(tag.getId());
|
||||
snapshotTag.setTagName(tag.getTagName());
|
||||
snapshotTag.setSort(i);
|
||||
snapshotTags.add(snapshotTag);
|
||||
}
|
||||
return snapshotTags;
|
||||
}
|
||||
|
||||
private List<AgentUpdateDTO.FunctionInfo> toFunctionInfo(List<AgentPluginMapping> mappings) {
|
||||
if (mappings == null || mappings.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return mappings.stream().map(mapping -> {
|
||||
AgentUpdateDTO.FunctionInfo info = new AgentUpdateDTO.FunctionInfo();
|
||||
info.setPluginId(mapping.getPluginId());
|
||||
info.setParamInfo(parseParamInfo(mapping.getParamInfo()));
|
||||
return info;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
private HashMap<String, Object> parseParamInfo(String paramInfo) {
|
||||
if (StringUtils.isBlank(paramInfo)) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return JsonUtils.parseObject(paramInfo, PARAM_INFO_TYPE);
|
||||
}
|
||||
|
||||
private List<xiaozhi.modules.agent.dto.ContextProviderDTO> getContextProviders(String agentId) {
|
||||
AgentContextProviderEntity contextEntity = agentContextProviderService.getByAgentId(agentId);
|
||||
if (contextEntity == null || contextEntity.getContextProviders() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return contextEntity.getContextProviders();
|
||||
}
|
||||
|
||||
private List<String> getChangedFields(AgentSnapshotDataDTO current, AgentUpdateDTO pendingUpdate) {
|
||||
if (pendingUpdate == null) {
|
||||
return List.of("restore");
|
||||
}
|
||||
|
||||
List<String> fields = new ArrayList<>();
|
||||
for (AgentSnapshotField field : AgentSnapshotField.values()) {
|
||||
Object nextValue = field.updateValue(pendingUpdate);
|
||||
if (nextValue != null && isChanged(field, field.snapshotValue(current), nextValue)) {
|
||||
fields.add(field.getFieldName());
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
private List<String> getChangedFields(AgentSnapshotDataDTO current, AgentSnapshotDataDTO next) {
|
||||
if (next == null) {
|
||||
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) {
|
||||
String keyText = String.valueOf(key);
|
||||
result.put(keyText, isSensitiveKey(keyText) ? SECRET_PLACEHOLDER : normalizeValue(value));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private void applyAgentFields(AgentEntity agent, AgentSnapshotDataDTO data) {
|
||||
for (AgentSnapshotField field : AgentSnapshotField.values()) {
|
||||
field.applyTo(agent, data);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRestoreParams(AgentEntity agent) {
|
||||
if (agent == null || StringUtils.isBlank(agent.getLlmModelId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModelConfigEntity llmModelData = modelConfigService.selectById(agent.getLlmModelId());
|
||||
if (llmModelData == null || llmModelData.getConfigJson() == null) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
}
|
||||
Object typeValue = llmModelData.getConfigJson().get("type");
|
||||
String type = typeValue == null ? "" : typeValue.toString();
|
||||
if ("openai".equals(type) || "ollama".equals(type)) {
|
||||
return;
|
||||
}
|
||||
if ("Intent_function_call".equals(agent.getIntentModelId())) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMemoryPolicy(AgentEntity agent) {
|
||||
if (agent == null || StringUtils.isBlank(agent.getMemModelId())) {
|
||||
return;
|
||||
}
|
||||
if (Constant.MEMORY_NO_MEM.equals(agent.getMemModelId())) {
|
||||
agentChatHistoryService.deleteByAgentId(agent.getId(), true, true);
|
||||
agent.setSummaryMemory("");
|
||||
} else if (Constant.MEMORY_MEM_REPORT_ONLY.equals(agent.getMemModelId())) {
|
||||
agent.setSummaryMemory("");
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreFunctions(String agentId, List<AgentUpdateDTO.FunctionInfo> functions) {
|
||||
agentPluginMappingService.deleteByAgentId(agentId);
|
||||
if (functions == null || functions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<AgentPluginMapping> mappings = functions.stream().map(info -> {
|
||||
AgentPluginMapping mapping = new AgentPluginMapping();
|
||||
mapping.setAgentId(agentId);
|
||||
mapping.setPluginId(info.getPluginId());
|
||||
mapping.setParamInfo(JsonUtils.toJsonString(info.getParamInfo()));
|
||||
return mapping;
|
||||
}).toList();
|
||||
agentPluginMappingService.saveBatch(mappings);
|
||||
}
|
||||
|
||||
private void restoreContextProviders(String agentId,
|
||||
List<xiaozhi.modules.agent.dto.ContextProviderDTO> contextProviders) {
|
||||
AgentContextProviderEntity entity = new AgentContextProviderEntity();
|
||||
entity.setAgentId(agentId);
|
||||
entity.setContextProviders(nullToEmpty(contextProviders));
|
||||
entity.setUpdater(SecurityUser.getUserId());
|
||||
entity.setUpdatedAt(new Date());
|
||||
agentContextProviderService.saveOrUpdateByAgentId(entity);
|
||||
}
|
||||
|
||||
private void restoreTags(String agentId, AgentSnapshotDataDTO data) {
|
||||
List<AgentSnapshotTagDTO> tags = data.getTags();
|
||||
if (tags == null || tags.isEmpty()) {
|
||||
agentTagService.saveAgentTags(agentId, null, nullToEmpty(data.getTagNames()));
|
||||
return;
|
||||
}
|
||||
|
||||
agentTagRelationDao.deleteByAgentId(agentId);
|
||||
List<AgentTagRelationEntity> relations = new ArrayList<>();
|
||||
Date now = new Date();
|
||||
for (int i = 0; i < tags.size(); i++) {
|
||||
AgentSnapshotTagDTO snapshotTag = tags.get(i);
|
||||
String tagId = restoreTag(snapshotTag, now);
|
||||
if (StringUtils.isBlank(tagId)) {
|
||||
continue;
|
||||
}
|
||||
AgentTagRelationEntity relation = new AgentTagRelationEntity();
|
||||
relation.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
relation.setAgentId(agentId);
|
||||
relation.setTagId(tagId);
|
||||
relation.setSort(i);
|
||||
relation.setCreator(SecurityUser.getUserId());
|
||||
relation.setCreatedAt(now);
|
||||
relation.setUpdater(SecurityUser.getUserId());
|
||||
relation.setUpdatedAt(now);
|
||||
relations.add(relation);
|
||||
}
|
||||
if (!relations.isEmpty()) {
|
||||
agentTagRelationDao.batchInsertRelation(relations);
|
||||
}
|
||||
}
|
||||
|
||||
private String restoreTag(AgentSnapshotTagDTO snapshotTag, Date now) {
|
||||
if (snapshotTag == null || StringUtils.isBlank(snapshotTag.getTagName())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AgentTagEntity tag = null;
|
||||
if (StringUtils.isNotBlank(snapshotTag.getId())) {
|
||||
tag = agentTagDao.selectById(snapshotTag.getId());
|
||||
if (tag != null && Objects.equals(tag.getDeleted(), 1)) {
|
||||
tag = null;
|
||||
}
|
||||
}
|
||||
if (tag == null) {
|
||||
tag = agentTagDao.selectOne(new QueryWrapper<AgentTagEntity>()
|
||||
.eq("tag_name", snapshotTag.getTagName())
|
||||
.eq("deleted", 0)
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
if (tag != null) {
|
||||
return tag.getId();
|
||||
}
|
||||
|
||||
AgentTagEntity newTag = new AgentTagEntity();
|
||||
newTag.setId(UUID.randomUUID().toString().replace("-", ""));
|
||||
newTag.setTagName(snapshotTag.getTagName());
|
||||
newTag.setSort(snapshotTag.getSort() == null ? 0 : snapshotTag.getSort());
|
||||
newTag.setDeleted(0);
|
||||
newTag.setCreator(SecurityUser.getUserId());
|
||||
newTag.setCreatedAt(now);
|
||||
newTag.setUpdater(SecurityUser.getUserId());
|
||||
newTag.setUpdatedAt(now);
|
||||
agentTagDao.insert(newTag);
|
||||
return newTag.getId();
|
||||
}
|
||||
|
||||
private AgentSnapshotVO toVO(AgentSnapshotEntity entity, boolean includeData) {
|
||||
AgentSnapshotVO vo = new AgentSnapshotVO();
|
||||
vo.setId(entity.getId());
|
||||
vo.setAgentId(entity.getAgentId());
|
||||
vo.setUserId(entity.getUserId());
|
||||
vo.setVersionNo(entity.getVersionNo());
|
||||
vo.setChangedFields(parseChangedFields(entity.getChangedFields()));
|
||||
vo.setFieldOrder(AgentSnapshotField.names());
|
||||
vo.setSource(entity.getSource());
|
||||
vo.setRestoreFromSnapshotId(entity.getRestoreFromSnapshotId());
|
||||
vo.setRestoreFromVersionNo(entity.getRestoreFromVersionNo());
|
||||
vo.setCreator(entity.getCreator());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
if (includeData) {
|
||||
vo.setSnapshotData(redactSnapshotData(JsonUtils.parseObject(entity.getSnapshotData(),
|
||||
AgentSnapshotDataDTO.class)));
|
||||
vo.setAfterSnapshotData(redactSnapshotData(getAfterSnapshotData(entity)));
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO getAfterSnapshotData(AgentSnapshotEntity entity) {
|
||||
AgentSnapshotEntity nextSnapshot = agentSnapshotDao.selectNextSnapshot(entity.getAgentId(), entity.getVersionNo());
|
||||
if (nextSnapshot != null) {
|
||||
return JsonUtils.parseObject(nextSnapshot.getSnapshotData(), AgentSnapshotDataDTO.class);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> parseChangedFields(String changedFields) {
|
||||
if (StringUtils.isBlank(changedFields)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return JsonUtils.parseObject(changedFields, STRING_LIST_TYPE);
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO redactSnapshotData(AgentSnapshotDataDTO data) {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
AgentSnapshotDataDTO copy = JsonUtils.parseObject(JsonUtils.toJsonString(data), AgentSnapshotDataDTO.class);
|
||||
if (copy.getFunctions() != null) {
|
||||
copy.getFunctions().forEach(function -> {
|
||||
if (function != null) {
|
||||
function.setParamInfo(redactSensitiveMap(function.getParamInfo()));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (copy.getContextProviders() != null) {
|
||||
copy.getContextProviders().forEach(provider -> {
|
||||
if (provider != null) {
|
||||
provider.setHeaders(redactSensitiveMap(provider.getHeaders()));
|
||||
}
|
||||
});
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
private AgentSnapshotDataDTO preserveCurrentSensitiveValues(AgentSnapshotDataDTO target, AgentSnapshotDataDTO current) {
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
AgentSnapshotDataDTO copy = JsonUtils.parseObject(JsonUtils.toJsonString(target), AgentSnapshotDataDTO.class);
|
||||
Map<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) {
|
||||
return list == null ? Collections.emptyList() : list;
|
||||
}
|
||||
}
|
||||
+31
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package xiaozhi.modules.agent.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -20,7 +18,6 @@ import java.util.List;
|
||||
public class AgentInfoVO extends AgentEntity
|
||||
{
|
||||
@Schema(description = "插件列表Id")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<AgentPluginMapping> functions;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
@@ -28,4 +25,7 @@ public class AgentInfoVO extends AgentEntity
|
||||
|
||||
@Schema(description = "替换词文件ID列表")
|
||||
private List<String> correctWordFileIds;
|
||||
|
||||
@Schema(description = "当前配置版本号")
|
||||
private Integer currentVersionNo;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package xiaozhi.modules.agent.vo;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
|
||||
|
||||
@Data
|
||||
@Schema(description = "智能体配置快照")
|
||||
public class AgentSnapshotVO {
|
||||
private String id;
|
||||
private String agentId;
|
||||
@Schema(description = "所属用户ID,表示该快照归属的智能体所有者")
|
||||
private Long userId;
|
||||
private Integer versionNo;
|
||||
private List<String> changedFields;
|
||||
private List<String> fieldOrder;
|
||||
private String source;
|
||||
@Schema(description = "恢复来源快照ID,仅恢复前自动备份版本有值")
|
||||
private String restoreFromSnapshotId;
|
||||
@Schema(description = "恢复来源版本号,仅恢复前自动备份版本有值")
|
||||
private Integer restoreFromVersionNo;
|
||||
@Schema(description = "创建者,表示触发本次快照写入的操作人")
|
||||
private Long creator;
|
||||
private Date createdAt;
|
||||
private AgentSnapshotDataDTO snapshotData;
|
||||
private AgentSnapshotDataDTO afterSnapshotData;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
-- liquibase formatted sql
|
||||
|
||||
-- changeset tykechen:202607071530
|
||||
CREATE TABLE IF NOT EXISTS `ai_agent_snapshot` (
|
||||
`id` VARCHAR(32) NOT NULL COMMENT '快照ID',
|
||||
`agent_id` VARCHAR(32) NOT NULL COMMENT '智能体ID',
|
||||
`user_id` BIGINT DEFAULT NULL COMMENT '所属用户ID',
|
||||
`version_no` INT UNSIGNED NOT NULL COMMENT '版本号',
|
||||
`snapshot_data` JSON NOT NULL DEFAULT (JSON_OBJECT()) COMMENT '快照数据',
|
||||
`changed_fields` JSON DEFAULT NULL COMMENT '变更字段',
|
||||
`source` VARCHAR(32) DEFAULT 'config' COMMENT '快照来源',
|
||||
`restore_from_snapshot_id` VARCHAR(32) DEFAULT NULL COMMENT '恢复来源快照ID',
|
||||
`restore_from_version_no` INT UNSIGNED DEFAULT NULL COMMENT '恢复来源版本号',
|
||||
`creator` BIGINT DEFAULT NULL COMMENT '创建者',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_agent_version` (`agent_id`, `version_no`),
|
||||
INDEX `idx_agent_created_at` (`agent_id`, `created_at`),
|
||||
INDEX `idx_snapshot_user_created_at` (`user_id`, `created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体配置快照表';
|
||||
@@ -697,3 +697,10 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202607011405.sql
|
||||
- changeSet:
|
||||
id: 202607071530
|
||||
author: tykechen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202607071530.sql
|
||||
|
||||
@@ -25,9 +25,6 @@
|
||||
<result column="memModelId" property="memModelId"/>
|
||||
<result column="intentModelId" property="intentModelId"/>
|
||||
|
||||
<result column="functions" property="functions"
|
||||
typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
|
||||
<result column="chatHistoryConf" property="chatHistoryConf"/>
|
||||
<result column="systemPrompt" property="systemPrompt"/>
|
||||
<result column="summaryMemory" property="summaryMemory"/>
|
||||
@@ -38,6 +35,13 @@
|
||||
<result column="createdAt" property="createdAt"/>
|
||||
<result column="updater" property="updater"/>
|
||||
<result column="updatedAt" property="updatedAt"/>
|
||||
<collection property="functions"
|
||||
ofType="xiaozhi.modules.agent.entity.AgentPluginMapping">
|
||||
<id column="functionId" property="id"/>
|
||||
<result column="functionAgentId" property="agentId"/>
|
||||
<result column="pluginId" property="pluginId"/>
|
||||
<result column="paramInfo" property="paramInfo"/>
|
||||
</collection>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectAgentInfoById" resultMap="AgentInfoMap">
|
||||
@@ -58,19 +62,6 @@
|
||||
a.tts_pitch AS ttsPitch,
|
||||
a.mem_model_id AS memModelId,
|
||||
a.intent_model_id AS intentModelId,
|
||||
COALESCE(
|
||||
(SELECT JSON_ARRAYAGG(
|
||||
JSON_OBJECT(
|
||||
'id', m.id,
|
||||
'agentId', m.agent_id,
|
||||
'pluginId', m.plugin_id,
|
||||
'paramInfo', m.param_info
|
||||
)
|
||||
)
|
||||
FROM ai_agent_plugin_mapping m
|
||||
WHERE m.agent_id = a.id),
|
||||
JSON_ARRAY()
|
||||
) AS functions,
|
||||
a.chat_history_conf AS chatHistoryConf,
|
||||
a.system_prompt AS systemPrompt,
|
||||
a.summary_memory AS summaryMemory,
|
||||
@@ -80,8 +71,22 @@
|
||||
a.creator,
|
||||
a.created_at AS createdAt,
|
||||
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
|
||||
LEFT JOIN ai_agent_plugin_mapping f ON f.agent_id = a.id
|
||||
WHERE a.id = #{agentId}
|
||||
ORDER BY f.id ASC
|
||||
</select>
|
||||
|
||||
<select id="selectByIdForUpdate" resultType="xiaozhi.modules.agent.entity.AgentEntity">
|
||||
SELECT *
|
||||
FROM ai_agent
|
||||
WHERE id = #{agentId}
|
||||
FOR UPDATE
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="xiaozhi.modules.agent.dao.AgentSnapshotDao">
|
||||
|
||||
<select id="selectMaxVersionNo" resultType="java.lang.Integer">
|
||||
SELECT COALESCE(MAX(version_no), 0)
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
</select>
|
||||
|
||||
<select id="selectLatestSnapshot" resultType="xiaozhi.modules.agent.entity.AgentSnapshotEntity">
|
||||
SELECT id,
|
||||
agent_id AS agentId,
|
||||
user_id AS userId,
|
||||
version_no AS versionNo,
|
||||
snapshot_data AS snapshotData,
|
||||
changed_fields AS changedFields,
|
||||
source,
|
||||
restore_from_snapshot_id AS restoreFromSnapshotId,
|
||||
restore_from_version_no AS restoreFromVersionNo,
|
||||
creator,
|
||||
created_at AS createdAt
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
ORDER BY version_no DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectNextSnapshot" resultType="xiaozhi.modules.agent.entity.AgentSnapshotEntity">
|
||||
SELECT id,
|
||||
agent_id AS agentId,
|
||||
user_id AS userId,
|
||||
version_no AS versionNo,
|
||||
snapshot_data AS snapshotData,
|
||||
changed_fields AS changedFields,
|
||||
source,
|
||||
restore_from_snapshot_id AS restoreFromSnapshotId,
|
||||
restore_from_version_no AS restoreFromVersionNo,
|
||||
creator,
|
||||
created_at AS createdAt
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{agentId}
|
||||
AND version_no > #{versionNo}
|
||||
ORDER BY version_no ASC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<insert id="insertWithNextVersion">
|
||||
INSERT INTO ai_agent_snapshot (
|
||||
id,
|
||||
agent_id,
|
||||
user_id,
|
||||
version_no,
|
||||
snapshot_data,
|
||||
changed_fields,
|
||||
source,
|
||||
restore_from_snapshot_id,
|
||||
restore_from_version_no,
|
||||
creator,
|
||||
created_at
|
||||
)
|
||||
SELECT #{snapshot.id},
|
||||
#{snapshot.agentId},
|
||||
#{snapshot.userId},
|
||||
COALESCE(MAX(version_no), 0) + 1,
|
||||
#{snapshot.snapshotData},
|
||||
#{snapshot.changedFields},
|
||||
#{snapshot.source},
|
||||
#{snapshot.restoreFromSnapshotId},
|
||||
#{snapshot.restoreFromVersionNo},
|
||||
#{snapshot.creator},
|
||||
#{snapshot.createdAt}
|
||||
FROM ai_agent_snapshot
|
||||
WHERE agent_id = #{snapshot.agentId}
|
||||
</insert>
|
||||
|
||||
<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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InOrder;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.Enums.AgentSnapshotField;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dao.AgentSnapshotDao;
|
||||
import xiaozhi.modules.agent.dao.AgentTagDao;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotDataDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotPageDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentSnapshotTagDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentSnapshotEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTagEntity;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentSnapshotService;
|
||||
import xiaozhi.modules.agent.vo.AgentSnapshotVO;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.correctword.service.CorrectWordFileService;
|
||||
|
||||
class AgentSnapshotServiceImplTest {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void normalizeSortedJsonListTreatsDtoAndMapShapesEqually() throws Exception {
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null,
|
||||
null, null);
|
||||
Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("normalizeSortedJsonList", List.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
ContextProviderDTO dto = new ContextProviderDTO();
|
||||
dto.setUrl("https://example.com/context");
|
||||
dto.setHeaders(Map.of("Authorization", "Bearer token"));
|
||||
|
||||
Map<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 firstAgentSaveKeepsInitialBaselineBeforePersistingNewState() {
|
||||
AgentDao agentDao = mock(AgentDao.class);
|
||||
AgentContextProviderService contextProviderService = mock(AgentContextProviderService.class);
|
||||
CorrectWordFileService correctWordFileService = mock(CorrectWordFileService.class);
|
||||
AgentSnapshotService snapshotService = mock(AgentSnapshotService.class);
|
||||
AgentServiceImpl service = new AgentServiceImpl(agentDao, null, null, null, null, null, null, null,
|
||||
null, null, contextProviderService, null, correctWordFileService, snapshotService);
|
||||
ReflectionTestUtils.setField(service, "baseDao", agentDao);
|
||||
|
||||
String agentId = "agent-id";
|
||||
AgentEntity lockedAgent = new AgentEntity();
|
||||
lockedAgent.setId(agentId);
|
||||
AgentInfoVO currentAgent = new AgentInfoVO();
|
||||
currentAgent.setId(agentId);
|
||||
currentAgent.setAgentName("old-name");
|
||||
AgentUpdateDTO update = new AgentUpdateDTO();
|
||||
update.setAgentName("new-name");
|
||||
|
||||
when(agentDao.selectByIdForUpdate(agentId)).thenReturn(lockedAgent);
|
||||
when(agentDao.selectAgentInfoById(agentId)).thenReturn(currentAgent);
|
||||
when(snapshotService.getCurrentVersionNo(agentId)).thenReturn(0);
|
||||
when(contextProviderService.getByAgentId(agentId)).thenReturn(null);
|
||||
when(correctWordFileService.getAgentCorrectWordFileIds(agentId)).thenReturn(List.of());
|
||||
when(agentDao.updateById(any())).thenReturn(1);
|
||||
|
||||
service.updateAgentById(agentId, update);
|
||||
|
||||
InOrder inOrder = inOrder(agentDao, snapshotService);
|
||||
inOrder.verify(snapshotService).createSnapshot(agentId, "initial");
|
||||
inOrder.verify(agentDao).updateById(argThat(agent -> "new-name".equals(agent.getAgentName())));
|
||||
inOrder.verify(snapshotService).createSnapshot(agentId, "config");
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentSnapshotFieldAppliesRestorableAgentFields() {
|
||||
AgentSnapshotDataDTO data = new AgentSnapshotDataDTO();
|
||||
data.setAgentName("restored-name");
|
||||
data.setLlmModelId("llm-new");
|
||||
data.setSystemPrompt("restored prompt");
|
||||
data.setSort(7);
|
||||
|
||||
AgentEntity agent = new AgentEntity();
|
||||
for (AgentSnapshotField field : AgentSnapshotField.values()) {
|
||||
field.applyTo(agent, data);
|
||||
}
|
||||
|
||||
assertEquals("restored-name", agent.getAgentName());
|
||||
assertEquals("llm-new", agent.getLlmModelId());
|
||||
assertEquals("restored prompt", agent.getSystemPrompt());
|
||||
assertEquals(7, agent.getSort());
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreSnapshotRollsBackForAnyException() throws Exception {
|
||||
Method method = AgentSnapshotServiceImpl.class.getMethod("restoreSnapshot", String.class, String.class);
|
||||
|
||||
Transactional transactional = method.getAnnotation(Transactional.class);
|
||||
|
||||
assertNotNull(transactional);
|
||||
assertTrue(List.of(transactional.rollbackFor()).contains(Exception.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteSnapshotRollsBackForAnyException() throws Exception {
|
||||
Method method = AgentSnapshotServiceImpl.class.getMethod("deleteSnapshot", String.class, String.class);
|
||||
|
||||
Transactional transactional = method.getAnnotation(Transactional.class);
|
||||
|
||||
assertNotNull(transactional);
|
||||
assertTrue(List.of(transactional.rollbackFor()).contains(Exception.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void snapshotInsertAllocatesVersionInSingleSqlStatement() throws Exception {
|
||||
String xml = normalizeWhitespace(Files.readString(Path.of("src/main/resources/mapper/agent/AgentSnapshotDao.xml")));
|
||||
String dao = Files.readString(Path.of("src/main/java/xiaozhi/modules/agent/dao/AgentSnapshotDao.java"));
|
||||
|
||||
assertTrue(dao.contains("insertWithNextVersion"));
|
||||
assertTrue(xml.contains("<insert id=\"insertWithNextVersion\">"));
|
||||
assertTrue(xml.contains("INSERT INTO ai_agent_snapshot"));
|
||||
assertTrue(xml.contains("COALESCE(MAX(version_no), 0) + 1"));
|
||||
assertFalse(xml.contains("LAST_INSERT_ID"));
|
||||
assertFalse(xml.contains("ai_agent_snapshot_sequence"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void snapshotMigrationIsMergedIntoSingleChangeLog() throws Exception {
|
||||
String sql = Files.readString(Path.of("src/main/resources/db/changelog/202607071530.sql"));
|
||||
String master = Files.readString(Path.of("src/main/resources/db/changelog/db.changelog-master.yaml"));
|
||||
|
||||
assertTrue(sql.contains("restore_from_snapshot_id"));
|
||||
assertTrue(sql.contains("restore_from_version_no"));
|
||||
assertTrue(sql.contains("idx_snapshot_user_created_at"));
|
||||
assertTrue(sql.contains("DEFAULT (JSON_OBJECT())"));
|
||||
assertTrue(master.contains("202607071530.sql"));
|
||||
assertFalse(master.contains("202607081150.sql"));
|
||||
assertFalse(master.contains("202607081230.sql"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectNextSnapshotLoadsRestoreTraceColumns() throws Exception {
|
||||
String xml = normalizeWhitespace(Files.readString(Path.of("src/main/resources/mapper/agent/AgentSnapshotDao.xml")));
|
||||
|
||||
assertTrue(xml.contains("restore_from_snapshot_id AS restoreFromSnapshotId"));
|
||||
assertTrue(xml.contains("restore_from_version_no AS restoreFromVersionNo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retentionPruneSqlKeepsLatestVersions() throws Exception {
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/agent/AgentSnapshotDao.xml"));
|
||||
|
||||
assertTrue(xml.contains("deleteOlderThanKeepLimit"));
|
||||
assertTrue(xml.contains("ORDER BY version_no DESC"));
|
||||
assertTrue(xml.contains("LIMIT #{keepLimit}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void agentInfoMapperLoadsFunctionsWithJoinInsteadOfNestedSelect() throws Exception {
|
||||
String xml = Files.readString(Path.of("src/main/resources/mapper/agent/AgentDao.xml"));
|
||||
|
||||
assertTrue(xml.contains("LEFT JOIN ai_agent_plugin_mapping f ON f.agent_id = a.id"));
|
||||
assertFalse(xml.contains("selectAgentFunctionsByAgentId"));
|
||||
assertFalse(xml.contains("select=\"selectAgentFunctions"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toVOIncludesRestoreTraceFields() throws Exception {
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, null, null, null, null, null, null,
|
||||
null, null);
|
||||
Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("toVO",
|
||||
AgentSnapshotEntity.class, boolean.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
AgentSnapshotEntity entity = new AgentSnapshotEntity();
|
||||
entity.setId("snapshot-id");
|
||||
entity.setAgentId("agent-id");
|
||||
entity.setVersionNo(3);
|
||||
entity.setChangedFields("[]");
|
||||
entity.setSource("restore");
|
||||
entity.setRestoreFromSnapshotId("target-id");
|
||||
entity.setRestoreFromVersionNo(1);
|
||||
|
||||
AgentSnapshotVO vo = (AgentSnapshotVO) method.invoke(service, entity, false);
|
||||
|
||||
assertEquals("target-id", vo.getRestoreFromSnapshotId());
|
||||
assertEquals(1, vo.getRestoreFromVersionNo());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageDoesNotCreateInitialSnapshotWhenAgentHasNoHistory() {
|
||||
AgentSnapshotDao snapshotDao = mock(AgentSnapshotDao.class);
|
||||
AgentDao agentDao = mock(AgentDao.class);
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(snapshotDao, agentDao, null, null, null,
|
||||
null, null, null, null, null);
|
||||
|
||||
when(snapshotDao.selectPage(any(), any())).thenReturn(new Page<AgentSnapshotEntity>(1, 10));
|
||||
|
||||
service.page("agent-id", new AgentSnapshotPageDTO());
|
||||
|
||||
verify(agentDao, never()).selectByIdForUpdate(any());
|
||||
verify(snapshotDao, never()).selectMaxVersionNo(any());
|
||||
verify(snapshotDao, never()).insertWithNextVersion(any());
|
||||
verify(snapshotDao, never()).deleteOlderThanKeepLimit(any(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCurrentVersionNoReturnsPersistedMaxVersion() {
|
||||
AgentSnapshotDao snapshotDao = mock(AgentSnapshotDao.class);
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(snapshotDao, null, null, null, null,
|
||||
null, null, null, null, null);
|
||||
|
||||
when(snapshotDao.selectMaxVersionNo("agent-id")).thenReturn(7);
|
||||
|
||||
assertEquals(7, service.getCurrentVersionNo("agent-id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSnapshotStoresCurrentStateAsInitialVersionWhenHistoryIsEmpty() {
|
||||
AgentSnapshotDao snapshotDao = mock(AgentSnapshotDao.class);
|
||||
AgentDao agentDao = mock(AgentDao.class);
|
||||
AgentTagDao tagDao = mock(AgentTagDao.class);
|
||||
AgentContextProviderService contextProviderService = mock(AgentContextProviderService.class);
|
||||
CorrectWordFileService correctWordFileService = mock(CorrectWordFileService.class);
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(snapshotDao, agentDao, tagDao, null, null,
|
||||
contextProviderService, null, null, null, correctWordFileService);
|
||||
|
||||
AgentEntity lockedAgent = new AgentEntity();
|
||||
lockedAgent.setId("agent-id");
|
||||
AgentInfoVO agentInfo = new AgentInfoVO();
|
||||
agentInfo.setId("agent-id");
|
||||
agentInfo.setUserId(7L);
|
||||
agentInfo.setAgentName("agent");
|
||||
when(agentDao.selectByIdForUpdate("agent-id")).thenReturn(lockedAgent);
|
||||
when(agentDao.selectAgentInfoById("agent-id")).thenReturn(agentInfo);
|
||||
when(snapshotDao.selectLatestSnapshot("agent-id")).thenReturn(null);
|
||||
when(snapshotDao.insertWithNextVersion(any())).thenReturn(1);
|
||||
when(correctWordFileService.getAgentCorrectWordFileIds("agent-id")).thenReturn(List.of());
|
||||
when(contextProviderService.getByAgentId("agent-id")).thenReturn(null);
|
||||
when(tagDao.selectByAgentId("agent-id")).thenReturn(List.of());
|
||||
|
||||
service.createSnapshot("agent-id", "config");
|
||||
|
||||
verify(snapshotDao).insertWithNextVersion(argThat(snapshot -> "config".equals(snapshot.getSource())
|
||||
&& snapshot.getVersionNo() == null
|
||||
&& snapshot.getChangedFields().contains("initial")));
|
||||
verify(snapshotDao).deleteOlderThanKeepLimit("agent-id", 100);
|
||||
}
|
||||
|
||||
@Test
|
||||
void restoreTagDoesNotReviveSoftDeletedSnapshotTagId() throws Exception {
|
||||
AgentTagDao tagDao = mock(AgentTagDao.class);
|
||||
AgentSnapshotServiceImpl service = new AgentSnapshotServiceImpl(null, null, tagDao, null, null, null, null, null,
|
||||
null, null);
|
||||
Method method = AgentSnapshotServiceImpl.class.getDeclaredMethod("restoreTag",
|
||||
AgentSnapshotTagDTO.class, Date.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
AgentTagEntity deletedTag = new AgentTagEntity();
|
||||
deletedTag.setId("deleted-tag-id");
|
||||
deletedTag.setTagName("archived");
|
||||
deletedTag.setDeleted(1);
|
||||
when(tagDao.selectById("deleted-tag-id")).thenReturn(deletedTag);
|
||||
when(tagDao.selectOne(any())).thenReturn(null);
|
||||
AtomicReference<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;
|
||||
}
|
||||
|
||||
private String normalizeWhitespace(String value) {
|
||||
return value.replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
}
|
||||
+42
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,28 @@ function handleInputConfirm() {
|
||||
// 是否禁用历史记忆输入框
|
||||
const isMemoryDisabled = computed(() => formData.value.memModelId !== 'Memory_mem_local_short')
|
||||
|
||||
function normalizeFunctionParams(paramInfo: any) {
|
||||
if (!paramInfo)
|
||||
return {}
|
||||
if (typeof paramInfo === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(paramInfo)
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||
}
|
||||
catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
return typeof paramInfo === 'object' && !Array.isArray(paramInfo) ? { ...paramInfo } : {}
|
||||
}
|
||||
|
||||
function normalizeAgentFunctions(functions: any[] = []) {
|
||||
return functions.map(item => ({
|
||||
...item,
|
||||
paramInfo: normalizeFunctionParams(item.paramInfo),
|
||||
}))
|
||||
}
|
||||
|
||||
// 打开上下文源编辑弹窗
|
||||
function openContextProviderDialog() {
|
||||
uni.navigateTo({
|
||||
@@ -191,11 +213,12 @@ async function loadAgentDetail() {
|
||||
loading.value = true
|
||||
tempSummaryMemory.value = ''
|
||||
const detail = await getAgentDetail(agentId.value)
|
||||
formData.value = { ...detail }
|
||||
const normalizedFunctions = normalizeAgentFunctions(detail.functions || [])
|
||||
formData.value = { ...detail, functions: normalizedFunctions }
|
||||
|
||||
// 更新插件store
|
||||
pluginStore.setCurrentAgentId(agentId.value)
|
||||
pluginStore.setCurrentFunctions(detail.functions || [])
|
||||
pluginStore.setCurrentFunctions(normalizedFunctions)
|
||||
|
||||
// 更新语速音调
|
||||
speedPitchStore.updateSpeedPitch({
|
||||
@@ -626,6 +649,7 @@ async function saveAgent() {
|
||||
...speedPitchStore.speedPitch,
|
||||
ttsLanguage: formData.value.language,
|
||||
contextProviders: providerStore.providers,
|
||||
functions: normalizeAgentFunctions(formData.value.functions || []),
|
||||
}
|
||||
await updateAgent(agentId.value, saveData)
|
||||
loadAgentDetail()
|
||||
@@ -663,7 +687,7 @@ function handleTools() {
|
||||
|
||||
// 确保store中有最新数据
|
||||
pluginStore.setCurrentAgentId(agentId.value)
|
||||
pluginStore.setCurrentFunctions(formData.value.functions || [])
|
||||
pluginStore.setCurrentFunctions(normalizeAgentFunctions(formData.value.functions || []))
|
||||
pluginStore.setAllFunctions(allFunctions.value)
|
||||
|
||||
uni.navigateTo({
|
||||
@@ -688,7 +712,7 @@ async function handleUpdateAgentTags() {
|
||||
|
||||
// 监听store中的插件配置变化
|
||||
watch(() => pluginStore.currentFunctions, (newFunctions) => {
|
||||
formData.value.functions = newFunctions
|
||||
formData.value.functions = normalizeAgentFunctions(newFunctions || [])
|
||||
}, { deep: true })
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -41,17 +41,32 @@ const tempParams = ref<Record<string, any>>({})
|
||||
const arrayTextCache = 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() {
|
||||
selectedList.value = functions.value.map((mapping) => {
|
||||
const meta = allFunctions.value.find(f => f.id === mapping.pluginId)
|
||||
if (!meta) {
|
||||
return { id: mapping.pluginId, name: mapping.pluginId, params: {} }
|
||||
return { id: mapping.pluginId, name: mapping.pluginId, params: normalizeFunctionParams(mapping.paramInfo) }
|
||||
}
|
||||
|
||||
return {
|
||||
id: mapping.pluginId,
|
||||
name: meta.name,
|
||||
params: mapping.paramInfo || { ...meta.params },
|
||||
params: normalizeFunctionParams(mapping.paramInfo, meta.params),
|
||||
fieldsMeta: meta.fieldsMeta,
|
||||
}
|
||||
})
|
||||
@@ -91,7 +106,7 @@ function selectFunction(func: any) {
|
||||
selectedList.value = [...selectedList.value, {
|
||||
id: func.id,
|
||||
name: func.name,
|
||||
params: { ...func.params },
|
||||
params: normalizeFunctionParams(func.params),
|
||||
fieldsMeta: func.fieldsMeta,
|
||||
}]
|
||||
|
||||
@@ -118,7 +133,7 @@ function editFunction(func: any) {
|
||||
currentFunction.value = func
|
||||
|
||||
// 直接使用当前函数的参数
|
||||
tempParams.value = { ...func.params }
|
||||
tempParams.value = normalizeFunctionParams(func.params)
|
||||
|
||||
// 初始化文本缓存
|
||||
if (func.fieldsMeta) {
|
||||
@@ -252,7 +267,7 @@ function getFieldRemark(field: any) {
|
||||
watch(() => selectedList.value, (newSelectedList) => {
|
||||
const finalFunctions = newSelectedList.map(f => ({
|
||||
pluginId: f.id,
|
||||
paramInfo: f.params,
|
||||
paramInfo: normalizeFunctionParams(f.params),
|
||||
}))
|
||||
pluginStore.updateFunctions(finalFunctions)
|
||||
})
|
||||
|
||||
Generated
+10
@@ -11096,6 +11096,16 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
|
||||
@@ -81,6 +81,67 @@ export default {
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取智能体配置快照列表
|
||||
getAgentSnapshots(agentId, params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${agentId}/snapshots`)
|
||||
.method('GET')
|
||||
.data(params)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentSnapshots(agentId, params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取智能体配置快照详情
|
||||
getAgentSnapshot(agentId, snapshotId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${agentId}/snapshots/${snapshotId}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentSnapshot(agentId, snapshotId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 恢复智能体配置快照
|
||||
restoreAgentSnapshot(agentId, snapshotId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${agentId}/snapshots/${snapshotId}/restore`)
|
||||
.method('POST')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.restoreAgentSnapshot(agentId, snapshotId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 删除智能体配置快照
|
||||
deleteAgentSnapshot(agentId, snapshotId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${agentId}/snapshots/${snapshotId}`)
|
||||
.method('DELETE')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteAgentSnapshot(agentId, snapshotId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 新增方法:获取智能体模板
|
||||
getAgentTemplate(callback) { // 移除templateName参数
|
||||
RequestService.sendRequest()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+116
-12
@@ -806,12 +806,128 @@ export default {
|
||||
'roleConfig.addTag': 'Neues Label hinzufügen',
|
||||
'roleConfig.restartNotice': 'Neustart nach Konfigurationsspeichern zur Aktivierung.',
|
||||
'roleConfig.saveConfig': 'Konfiguration speichern',
|
||||
'roleConfig.snapshotHistory': 'Versionsverlauf',
|
||||
'roleConfig.currentVersion': 'Aktuelle Version {version}',
|
||||
'roleConfig.functionName': 'Funktionsname',
|
||||
'roleConfig.reset': 'Zurücksetzen',
|
||||
'roleConfig.agentName': 'Spitzname',
|
||||
'roleConfig.roleTemplate': 'Vorlage',
|
||||
'roleConfig.roleIntroduction': 'Einführung',
|
||||
'roleConfig.languageCode': 'Sprachcode',
|
||||
'roleConfig.interactionLanguage': 'Interaktionssprache',
|
||||
'agentSnapshot.title': 'Versionsverlauf',
|
||||
'agentSnapshot.empty': 'Noch keine Versionen',
|
||||
'agentSnapshot.version': 'Version',
|
||||
'agentSnapshot.createdAt': 'Gespeichert am',
|
||||
'agentSnapshot.source': 'Quelle',
|
||||
'agentSnapshot.changedFields': 'Änderungen',
|
||||
'agentSnapshot.actions': 'Aktionen',
|
||||
'agentSnapshot.view': 'Anzeigen',
|
||||
'agentSnapshot.restore': 'Wiederherstellen',
|
||||
'agentSnapshot.delete': 'Löschen',
|
||||
'agentSnapshot.detailTitle': 'Änderungsdetails',
|
||||
'agentSnapshot.restorePreviewTitle': 'Wiederherstellungsvorschau',
|
||||
'agentSnapshot.confirmRestore': 'Wiederherstellung bestätigen',
|
||||
'agentSnapshot.before': 'Vorher',
|
||||
'agentSnapshot.after': 'Nachher',
|
||||
'agentSnapshot.emptyValue': 'Keine',
|
||||
'agentSnapshot.secretRedacted': 'Geheimer Schlüssel maskiert',
|
||||
'agentSnapshot.configValue': 'Konfigurationswert',
|
||||
'agentSnapshot.noChangedContent': 'Keine anzeigbaren Änderungen',
|
||||
'agentSnapshot.noFunctionChange': 'Keine Funktionsänderungen',
|
||||
'agentSnapshot.beforeChange': 'Vor der Änderung',
|
||||
'agentSnapshot.afterChange': 'Nach der Änderung',
|
||||
'agentSnapshot.beforeRestore': 'Vor der Wiederherstellung',
|
||||
'agentSnapshot.afterRestore': 'Nach der Wiederherstellung',
|
||||
'agentSnapshot.restoreMemoryWarning': 'Beim Wiederherstellen einer Version ohne Speicher wird der Chatverlauf dieses Agenten gelöscht. Fahren Sie nur fort, wenn Sie dieses Risiko bestätigt haben.',
|
||||
'agentSnapshot.restoreConfirm': 'Version {version} wiederherstellen? Die aktuelle Konfiguration wird zuerst als neue Version gespeichert.',
|
||||
'agentSnapshot.restoreSuccess': 'Version wurde wiederhergestellt',
|
||||
'agentSnapshot.restoreFailed': 'Version konnte nicht wiederhergestellt werden',
|
||||
'agentSnapshot.restoreFailedRollback': 'Version konnte nicht wiederhergestellt werden. Die Transaktion wurde zurueckgerollt und die aktuelle Konfiguration wurde nicht geaendert',
|
||||
'agentSnapshot.deleteConfirm': 'Version {version} löschen? Dies kann nicht rückgängig gemacht werden.',
|
||||
'agentSnapshot.deleteSuccess': 'Verlaufsversion gelöscht',
|
||||
'agentSnapshot.deleteFailed': 'Verlaufsversion konnte nicht gelöscht werden',
|
||||
'agentSnapshot.fetchFailed': 'Versionsverlauf konnte nicht abgerufen werden',
|
||||
'agentSnapshot.detailFailed': 'Snapshot-Details konnten nicht abgerufen werden',
|
||||
'agentSnapshot.currentVersion': 'Aktuelle Version',
|
||||
'agentSnapshot.source.config': 'Konfiguration gespeichert',
|
||||
'agentSnapshot.source.current': 'Aktuelle Konfiguration',
|
||||
'agentSnapshot.source.restore': 'Vor der Wiederherstellung',
|
||||
'agentSnapshot.source.initial': 'Initialversion',
|
||||
'agentSnapshot.field.restore': 'Wiederherstellung',
|
||||
'agentSnapshot.field.initial': 'Initialer Snapshot',
|
||||
'agentSnapshot.field.agentCode': 'Agent-Code',
|
||||
'agentSnapshot.field.agentName': 'Spitzname',
|
||||
'agentSnapshot.field.asrModelId': 'Spracherkennung',
|
||||
'agentSnapshot.field.vadModelId': 'Sprachaktivitätserkennung',
|
||||
'agentSnapshot.field.llmModelId': 'Hauptsprachmodell',
|
||||
'agentSnapshot.field.slmModelId': 'Kleines Sprachmodell',
|
||||
'agentSnapshot.field.vllmModelId': 'Vision-Modell',
|
||||
'agentSnapshot.field.ttsModelId': 'Text-zu-Sprache',
|
||||
'agentSnapshot.field.ttsVoiceId': 'Stimme',
|
||||
'agentSnapshot.field.ttsLanguage': 'Sprache',
|
||||
'agentSnapshot.field.ttsVolume': 'Lautstärke',
|
||||
'agentSnapshot.field.ttsRate': 'Geschwindigkeit',
|
||||
'agentSnapshot.field.ttsPitch': 'Tonhöhe',
|
||||
'agentSnapshot.field.memModelId': 'Speichermodell',
|
||||
'agentSnapshot.field.intentModelId': 'Absichtserkennung',
|
||||
'agentSnapshot.field.chatHistoryConf': 'Chatverlauf',
|
||||
'agentSnapshot.field.systemPrompt': 'Einführung',
|
||||
'agentSnapshot.field.summaryMemory': 'Speicher',
|
||||
'agentSnapshot.field.langCode': 'Sprachcode',
|
||||
'agentSnapshot.field.language': 'Interaktionssprache',
|
||||
'agentSnapshot.field.sort': 'Sortierung',
|
||||
'agentSnapshot.field.functions': 'Funktionen',
|
||||
'agentSnapshot.field.contextProviders': 'Kontextquellen',
|
||||
'agentSnapshot.field.correctWordFileIds': 'Ersatzwörter',
|
||||
'agentSnapshot.field.tagNames': 'Labels',
|
||||
'agentSnapshot.field.tagIds': 'Label-IDs',
|
||||
'agentSnapshot.function.added': 'Aktiviert',
|
||||
'agentSnapshot.function.removed': 'Deaktiviert',
|
||||
'agentSnapshot.function.updated': 'Parameter geändert',
|
||||
'agentSnapshot.function.enabled': 'Aktiviert',
|
||||
'agentSnapshot.function.disabled': 'Deaktiviert',
|
||||
'agentSnapshot.function.disabledNote': 'Diese Funktion ist deaktiviert',
|
||||
'agentSnapshot.function.noParams': 'Keine Parameter erforderlich',
|
||||
'agentSnapshot.chatHistoryConf.none': 'Chatverlauf nicht aufzeichnen',
|
||||
'agentSnapshot.chatHistoryConf.text': 'Text melden',
|
||||
'agentSnapshot.chatHistoryConf.textVoice': 'Text und Sprache melden',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': 'Wetter',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Server-Musikplayer',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'China News',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'NewsNow-Aggregation',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant-Statusabfrage',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant-Statusaktualisierung',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant-Musikplayer',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Websuche',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Geräteanruf',
|
||||
'agentSnapshot.pluginField.url': 'Endpunkt-URL',
|
||||
'agentSnapshot.pluginField.news_sources': 'Nachrichtenquellen',
|
||||
'agentSnapshot.pluginField.default_rss_url': 'Standard-RSS-URL',
|
||||
'agentSnapshot.pluginField.society_rss_url': 'RSS-URL für Gesellschaft',
|
||||
'agentSnapshot.pluginField.world_rss_url': 'RSS-URL für Welt',
|
||||
'agentSnapshot.pluginField.finance_rss_url': 'RSS-URL für Finanzen',
|
||||
'agentSnapshot.pluginField.api_key': 'API-Schlüssel',
|
||||
'agentSnapshot.pluginField.default_location': 'Standardstadt',
|
||||
'agentSnapshot.pluginField.api_host': 'Entwickler-API-Host',
|
||||
'agentSnapshot.pluginField.base_url': 'Server-URL',
|
||||
'agentSnapshot.pluginField.devices': 'Geräte',
|
||||
'agentSnapshot.pluginField.provider': 'Suchanbieter',
|
||||
'agentSnapshot.pluginField.description': 'Werkzeugbeschreibung',
|
||||
'agentSnapshot.pluginField.max_results': 'Ergebnisanzahl',
|
||||
'agentSnapshot.model.Memory_nomem': 'Kein Speicher',
|
||||
'agentSnapshot.model.Memory_mem_local_short': 'Lokaler Kurzzeitspeicher',
|
||||
'agentSnapshot.model.Memory_mem0ai': 'Mem0AI-Speicher',
|
||||
'agentSnapshot.model.Memory_mem_report_only': 'Nur melden',
|
||||
'agentSnapshot.model.Intent_nointent': 'Keine Absichtserkennung',
|
||||
'agentSnapshot.model.Intent_intent_llm': 'Externe LLM-Absichtserkennung',
|
||||
'agentSnapshot.model.Intent_function_call': 'LLM-Funktionsaufruf',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS weiblich - Xiaoxiao',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS männlich - Yunyang',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS weiblich - Xiaoyi',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS männlich - Yunjian',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS männlich - Yunxi',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS männlich - Yunxia',
|
||||
'roleConfig.vad': 'VAD',
|
||||
'roleConfig.asr': 'ASR',
|
||||
'roleConfig.llm': 'Hauptsprachmodell (LLM)',
|
||||
@@ -1336,10 +1452,6 @@ export default {
|
||||
'knowledgeFileUpload.viewSlices': 'Segmente anzeigen',
|
||||
'knowledgeFileUpload.delete': 'Löschen',
|
||||
'knowledgeFileUpload.itemsPerPage': 'Einträge/Seite',
|
||||
'knowledgeFileUpload.firstPage': 'Erste Seite',
|
||||
'knowledgeFileUpload.prevPage': 'Vorherige Seite',
|
||||
'knowledgeFileUpload.nextPage': 'Nächste Seite',
|
||||
'knowledgeFileUpload.totalRecords': 'Insgesamt {total} Datensätze',
|
||||
'knowledgeFileUpload.uploadDocument': 'Dokument hochladen',
|
||||
'knowledgeFileUpload.documentNamePlaceholder': 'Bitte Dokumentnamen eingeben',
|
||||
'knowledgeFileUpload.file': 'Datei',
|
||||
@@ -1378,10 +1490,7 @@ export default {
|
||||
'knowledgeFileUpload.sliceCount': 'Segmentanzahl',
|
||||
'knowledgeFileUpload.add': 'Hinzufügen',
|
||||
'knowledgeFileUpload.retrievalTest': 'Abruftest',
|
||||
'knowledgeFileUpload.testQuestion': 'Testfrage',
|
||||
'knowledgeFileUpload.testQuestionPlaceholder': 'Bitte geben Sie die zu testende Frage ein',
|
||||
'knowledgeFileUpload.executeTest': 'Test ausführen',
|
||||
'knowledgeFileUpload.testResult': 'Testergebnis:',
|
||||
'knowledgeFileUpload.selectedFiles': 'Ausgewählte Dateien',
|
||||
'knowledgeFileUpload.totalSlices': 'Insgesamt {total} Datensätze',
|
||||
'knowledgeFileUpload.slice': 'Segment',
|
||||
@@ -1457,7 +1566,6 @@ export default {
|
||||
'addressBookManagement.name': 'Adressbuchname',
|
||||
'addressBookManagement.description': 'Adressbuchbeschreibung',
|
||||
'addressBookManagement.contactCount': 'Kontaktanzahl',
|
||||
'addressBookManagement.status': 'Aktiviert',
|
||||
'addressBookManagement.createdAt': 'Erstellt am',
|
||||
'addressBookManagement.operation': 'Aktion',
|
||||
'addressBookManagement.add': 'Hinzufügen',
|
||||
@@ -1478,9 +1586,7 @@ export default {
|
||||
'addressBookManagement.operationCancelled': 'Löschvorgang abgebrochen',
|
||||
'addressBookManagement.updateSuccess': 'Aktualisierung erfolgreich',
|
||||
'addressBookManagement.addSuccess': 'Erfolgreich hinzugefügt',
|
||||
'addressBookManagement.updateFailed': 'Aktualisierung fehlgeschlagen',
|
||||
'addressBookManagement.addFailed': 'Hinzufügen fehlgeschlagen',
|
||||
'addressBookManagement.selectAll': 'Alle auswählen',
|
||||
'addressBookManagement.cancelSelectAll': 'Alle abwählen',
|
||||
'addressBookManagement.deviceName': 'Gerätename',
|
||||
'addressBookManagement.deviceType': 'Gerätetyp',
|
||||
@@ -1495,9 +1601,7 @@ export default {
|
||||
'addressBookManagement.addressBookDescPlaceholder': 'Bitte Adressbuchbeschreibung eingeben',
|
||||
'addressBookManagement.remarksPlaceholder': 'Bitte Bemerkungen eingeben',
|
||||
'addressBookManagement.selectDevice': 'Bitte ein Gerät auswählen, um Adressbuchdetails anzuzeigen',
|
||||
'addressBookManagement.save': 'Speichern',
|
||||
'addressBookManagement.saveSuccess': 'Speichern erfolgreich',
|
||||
'addressBookManagement.saveFailed': 'Speichern fehlgeschlagen',
|
||||
'addressBookManagement.deviceNotSelected': 'Bitte zuerst ein Gerät auswählen',
|
||||
'addressBookManagement.addDeviceTip': 'Gerät hinzufügen Funktion ist in Entwicklung',
|
||||
'addressBookManagement.selectAgentFirst': 'Bitte zuerst einen Agent auswählen',
|
||||
|
||||
+116
-12
@@ -806,6 +806,9 @@ export default {
|
||||
'roleConfig.addTag': 'Add New Tag',
|
||||
'roleConfig.restartNotice': 'After saving the configuration, you need to restart the device for the new configuration to take effect.',
|
||||
'roleConfig.saveConfig': 'Save Configuration',
|
||||
'roleConfig.snapshotHistory': 'History',
|
||||
'roleConfig.currentVersion': 'Current Version {version}',
|
||||
'roleConfig.functionName': 'Function Name',
|
||||
'roleConfig.reset': 'Reset',
|
||||
'roleConfig.agentName': 'Nickname',
|
||||
'roleConfig.roleTemplate': 'Template',
|
||||
@@ -862,6 +865,119 @@ export default {
|
||||
'roleConfig.audioPlayFailed': 'Audio playback failed',
|
||||
'roleConfig.cannotPlayAudio': 'Cannot play audio',
|
||||
'roleConfig.audioPlayError': 'Error occurred during audio playback',
|
||||
'agentSnapshot.title': 'Version History',
|
||||
'agentSnapshot.empty': 'No versions yet',
|
||||
'agentSnapshot.version': 'Version',
|
||||
'agentSnapshot.createdAt': 'Saved At',
|
||||
'agentSnapshot.source': 'Source',
|
||||
'agentSnapshot.changedFields': 'Changes',
|
||||
'agentSnapshot.actions': 'Actions',
|
||||
'agentSnapshot.view': 'View',
|
||||
'agentSnapshot.restore': 'Restore',
|
||||
'agentSnapshot.delete': 'Delete',
|
||||
'agentSnapshot.detailTitle': 'Change Details',
|
||||
'agentSnapshot.restorePreviewTitle': 'Restore Preview',
|
||||
'agentSnapshot.confirmRestore': 'Confirm Restore',
|
||||
'agentSnapshot.before': 'Before',
|
||||
'agentSnapshot.after': 'After',
|
||||
'agentSnapshot.emptyValue': 'None',
|
||||
'agentSnapshot.secretRedacted': 'Secret redacted',
|
||||
'agentSnapshot.configValue': 'Config Value',
|
||||
'agentSnapshot.noChangedContent': 'No changes to display',
|
||||
'agentSnapshot.noFunctionChange': 'No function changes',
|
||||
'agentSnapshot.beforeChange': 'Before Change',
|
||||
'agentSnapshot.afterChange': 'After Change',
|
||||
'agentSnapshot.beforeRestore': 'Before Restore',
|
||||
'agentSnapshot.afterRestore': 'After Restore',
|
||||
'agentSnapshot.restoreMemoryWarning': 'Restoring to a no-memory version will clear this agent\'s chat history. Continue only after confirming this risk.',
|
||||
'agentSnapshot.restoreConfirm': 'Restore to version {version}? The current configuration will be saved as a new version first.',
|
||||
'agentSnapshot.restoreSuccess': 'Version restored',
|
||||
'agentSnapshot.restoreFailed': 'Failed to restore version',
|
||||
'agentSnapshot.restoreFailedRollback': 'Failed to restore version. The transaction was rolled back and the current configuration was not changed',
|
||||
'agentSnapshot.deleteConfirm': 'Delete version {version}? This cannot be undone.',
|
||||
'agentSnapshot.deleteSuccess': 'History version deleted',
|
||||
'agentSnapshot.deleteFailed': 'Failed to delete history version',
|
||||
'agentSnapshot.fetchFailed': 'Failed to fetch versions',
|
||||
'agentSnapshot.detailFailed': 'Failed to fetch snapshot details',
|
||||
'agentSnapshot.currentVersion': 'Current Version',
|
||||
'agentSnapshot.source.config': 'Config Save',
|
||||
'agentSnapshot.source.current': 'Current Config',
|
||||
'agentSnapshot.source.restore': 'Before Restore',
|
||||
'agentSnapshot.source.initial': 'Initial Version',
|
||||
'agentSnapshot.field.restore': 'Restore',
|
||||
'agentSnapshot.field.initial': 'Initial Snapshot',
|
||||
'agentSnapshot.field.agentCode': 'Agent Code',
|
||||
'agentSnapshot.field.agentName': 'Nickname',
|
||||
'agentSnapshot.field.asrModelId': 'Speech Recognition',
|
||||
'agentSnapshot.field.vadModelId': 'Voice Detect',
|
||||
'agentSnapshot.field.llmModelId': 'Main Language Model',
|
||||
'agentSnapshot.field.slmModelId': 'Small Language Model',
|
||||
'agentSnapshot.field.vllmModelId': 'Vision Model',
|
||||
'agentSnapshot.field.ttsModelId': 'Text-to-Speech',
|
||||
'agentSnapshot.field.ttsVoiceId': 'Voice',
|
||||
'agentSnapshot.field.ttsLanguage': 'Language',
|
||||
'agentSnapshot.field.ttsVolume': 'Volume',
|
||||
'agentSnapshot.field.ttsRate': 'Speed',
|
||||
'agentSnapshot.field.ttsPitch': 'Pitch',
|
||||
'agentSnapshot.field.memModelId': 'Memory Model',
|
||||
'agentSnapshot.field.intentModelId': 'Intent Recognition',
|
||||
'agentSnapshot.field.chatHistoryConf': 'Chat History',
|
||||
'agentSnapshot.field.systemPrompt': 'Introduction',
|
||||
'agentSnapshot.field.summaryMemory': 'Memory',
|
||||
'agentSnapshot.field.langCode': 'Language Code',
|
||||
'agentSnapshot.field.language': 'Interaction Language',
|
||||
'agentSnapshot.field.sort': 'Sort',
|
||||
'agentSnapshot.field.functions': 'Functions',
|
||||
'agentSnapshot.field.contextProviders': 'Context Providers',
|
||||
'agentSnapshot.field.correctWordFileIds': 'Replacement Words',
|
||||
'agentSnapshot.field.tagNames': 'Tags',
|
||||
'agentSnapshot.field.tagIds': 'Tag IDs',
|
||||
'agentSnapshot.function.added': 'Enabled',
|
||||
'agentSnapshot.function.removed': 'Disabled',
|
||||
'agentSnapshot.function.updated': 'Parameters changed',
|
||||
'agentSnapshot.function.enabled': 'Enabled',
|
||||
'agentSnapshot.function.disabled': 'Disabled',
|
||||
'agentSnapshot.function.disabledNote': 'This function is disabled',
|
||||
'agentSnapshot.function.noParams': 'No parameters required',
|
||||
'agentSnapshot.chatHistoryConf.none': 'Do not record chat history',
|
||||
'agentSnapshot.chatHistoryConf.text': 'Report text',
|
||||
'agentSnapshot.chatHistoryConf.textVoice': 'Report text and voice',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': 'Weather',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Server music player',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'China News',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'NewsNow aggregation',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant state query',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant state update',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant music player',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Web search',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Device-to-device call',
|
||||
'agentSnapshot.pluginField.url': 'Endpoint URL',
|
||||
'agentSnapshot.pluginField.news_sources': 'News sources',
|
||||
'agentSnapshot.pluginField.default_rss_url': 'Default RSS URL',
|
||||
'agentSnapshot.pluginField.society_rss_url': 'Society RSS URL',
|
||||
'agentSnapshot.pluginField.world_rss_url': 'World RSS URL',
|
||||
'agentSnapshot.pluginField.finance_rss_url': 'Finance RSS URL',
|
||||
'agentSnapshot.pluginField.api_key': 'API key',
|
||||
'agentSnapshot.pluginField.default_location': 'Default city',
|
||||
'agentSnapshot.pluginField.api_host': 'Developer API host',
|
||||
'agentSnapshot.pluginField.base_url': 'Server URL',
|
||||
'agentSnapshot.pluginField.devices': 'Devices',
|
||||
'agentSnapshot.pluginField.provider': 'Search provider',
|
||||
'agentSnapshot.pluginField.description': 'Tool description',
|
||||
'agentSnapshot.pluginField.max_results': 'Result count',
|
||||
'agentSnapshot.model.Memory_nomem': 'No memory',
|
||||
'agentSnapshot.model.Memory_mem_local_short': 'Local short memory',
|
||||
'agentSnapshot.model.Memory_mem0ai': 'Mem0AI memory',
|
||||
'agentSnapshot.model.Memory_mem_report_only': 'Report only',
|
||||
'agentSnapshot.model.Intent_nointent': 'No intent recognition',
|
||||
'agentSnapshot.model.Intent_intent_llm': 'External LLM intent recognition',
|
||||
'agentSnapshot.model.Intent_function_call': 'LLM function calling',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS female - Xiaoxiao',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS male - Yunyang',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS female - Xiaoyi',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS male - Yunjian',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS male - Yunxi',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS male - Yunxia',
|
||||
|
||||
// Form field Tooltip descriptions
|
||||
'roleConfig.tooltip.agentName': 'Set the name of your AI agent for identification and recognition',
|
||||
@@ -1336,10 +1452,6 @@ export default {
|
||||
'knowledgeFileUpload.viewSlices': 'View Slices',
|
||||
'knowledgeFileUpload.delete': 'Delete',
|
||||
'knowledgeFileUpload.itemsPerPage': 'items/page',
|
||||
'knowledgeFileUpload.firstPage': 'First Page',
|
||||
'knowledgeFileUpload.prevPage': 'Previous Page',
|
||||
'knowledgeFileUpload.nextPage': 'Next Page',
|
||||
'knowledgeFileUpload.totalRecords': 'Total {total} records',
|
||||
'knowledgeFileUpload.uploadDocument': 'Upload Document',
|
||||
'knowledgeFileUpload.documentNamePlaceholder': 'Please enter document name',
|
||||
'knowledgeFileUpload.file': 'File',
|
||||
@@ -1378,10 +1490,7 @@ export default {
|
||||
'knowledgeFileUpload.sliceCount': 'Slice Count',
|
||||
'knowledgeFileUpload.add': 'Add',
|
||||
'knowledgeFileUpload.retrievalTest': 'Retrieval Test',
|
||||
'knowledgeFileUpload.testQuestion': 'Test Question',
|
||||
'knowledgeFileUpload.testQuestionPlaceholder': 'Please enter the question to test',
|
||||
'knowledgeFileUpload.executeTest': 'Execute Test',
|
||||
'knowledgeFileUpload.testResult': 'Test Result:',
|
||||
'knowledgeFileUpload.selectedFiles': 'Selected Files',
|
||||
'knowledgeFileUpload.totalSlices': 'Total {total} records',
|
||||
'knowledgeFileUpload.slice': 'Slice',
|
||||
@@ -1457,7 +1566,6 @@ export default {
|
||||
'addressBookManagement.name': 'Address Book Name',
|
||||
'addressBookManagement.description': 'Address Book Description',
|
||||
'addressBookManagement.contactCount': 'Contact Count',
|
||||
'addressBookManagement.status': 'Enabled',
|
||||
'addressBookManagement.createdAt': 'Created Time',
|
||||
'addressBookManagement.operation': 'Operation',
|
||||
'addressBookManagement.add': 'Add',
|
||||
@@ -1478,9 +1586,7 @@ export default {
|
||||
'addressBookManagement.operationCancelled': 'Delete operation cancelled',
|
||||
'addressBookManagement.updateSuccess': 'Update successful',
|
||||
'addressBookManagement.addSuccess': 'Add successful',
|
||||
'addressBookManagement.updateFailed': 'Update failed',
|
||||
'addressBookManagement.addFailed': 'Add failed',
|
||||
'addressBookManagement.selectAll': 'Select All',
|
||||
'addressBookManagement.cancelSelectAll': 'Deselect All',
|
||||
'addressBookManagement.deviceName': 'Device Name',
|
||||
'addressBookManagement.deviceType': 'Device Type',
|
||||
@@ -1495,9 +1601,7 @@ export default {
|
||||
'addressBookManagement.addressBookDescPlaceholder': 'Please enter address book description',
|
||||
'addressBookManagement.remarksPlaceholder': 'Please enter remarks',
|
||||
'addressBookManagement.selectDevice': 'Please select a device to view address book details',
|
||||
'addressBookManagement.save': 'Save',
|
||||
'addressBookManagement.saveSuccess': 'Save successful',
|
||||
'addressBookManagement.saveFailed': 'Save failed',
|
||||
'addressBookManagement.deviceNotSelected': 'Please select a device first',
|
||||
'addressBookManagement.addDeviceTip': 'Add device feature is under development',
|
||||
'addressBookManagement.selectAgentFirst': 'Please select an agent first',
|
||||
|
||||
@@ -806,12 +806,128 @@ export default {
|
||||
'roleConfig.addTag': 'Adicionar Novo Rótulo',
|
||||
'roleConfig.restartNotice': 'Reinicie após salvar a configuração para ativação.',
|
||||
'roleConfig.saveConfig': 'Salvar Configuração',
|
||||
'roleConfig.snapshotHistory': 'Histórico de Versões',
|
||||
'roleConfig.currentVersion': 'Versão Atual {version}',
|
||||
'roleConfig.functionName': 'Nome da Função',
|
||||
'roleConfig.reset': 'Redefinir',
|
||||
'roleConfig.agentName': 'Apelido',
|
||||
'roleConfig.roleTemplate': 'Modelo',
|
||||
'roleConfig.roleIntroduction': 'Introdução',
|
||||
'roleConfig.languageCode': 'Código do Idioma',
|
||||
'roleConfig.interactionLanguage': 'Idioma de Interação',
|
||||
'agentSnapshot.title': 'Histórico de Versões',
|
||||
'agentSnapshot.empty': 'Ainda sem versões',
|
||||
'agentSnapshot.version': 'Versão',
|
||||
'agentSnapshot.createdAt': 'Salvo em',
|
||||
'agentSnapshot.source': 'Origem',
|
||||
'agentSnapshot.changedFields': 'Alterações',
|
||||
'agentSnapshot.actions': 'Ações',
|
||||
'agentSnapshot.view': 'Ver',
|
||||
'agentSnapshot.restore': 'Restaurar',
|
||||
'agentSnapshot.delete': 'Excluir',
|
||||
'agentSnapshot.detailTitle': 'Detalhes da Alteração',
|
||||
'agentSnapshot.restorePreviewTitle': 'Prévia da Restauração',
|
||||
'agentSnapshot.confirmRestore': 'Confirmar Restauração',
|
||||
'agentSnapshot.before': 'Antes',
|
||||
'agentSnapshot.after': 'Depois',
|
||||
'agentSnapshot.emptyValue': 'Nenhum',
|
||||
'agentSnapshot.secretRedacted': 'Chave secreta mascarada',
|
||||
'agentSnapshot.configValue': 'Valor da configuração',
|
||||
'agentSnapshot.noChangedContent': 'Nenhuma alteração para exibir',
|
||||
'agentSnapshot.noFunctionChange': 'Sem alterações de função',
|
||||
'agentSnapshot.beforeChange': 'Antes da Alteração',
|
||||
'agentSnapshot.afterChange': 'Depois da Alteração',
|
||||
'agentSnapshot.beforeRestore': 'Antes da Restauração',
|
||||
'agentSnapshot.afterRestore': 'Depois da Restauração',
|
||||
'agentSnapshot.restoreMemoryWarning': 'Restaurar para uma versão sem memória apagará o histórico de conversa deste agente. Continue somente após confirmar esse risco.',
|
||||
'agentSnapshot.restoreConfirm': 'Restaurar para a versão {version}? A configuração atual será salva primeiro como uma nova versão.',
|
||||
'agentSnapshot.restoreSuccess': 'Versão restaurada',
|
||||
'agentSnapshot.restoreFailed': 'Falha ao restaurar versão',
|
||||
'agentSnapshot.restoreFailedRollback': 'Falha ao restaurar versão. A transação foi revertida e a configuração atual não foi alterada',
|
||||
'agentSnapshot.deleteConfirm': 'Excluir a versão {version}? Esta ação não pode ser desfeita.',
|
||||
'agentSnapshot.deleteSuccess': 'Versão do histórico excluída',
|
||||
'agentSnapshot.deleteFailed': 'Falha ao excluir a versão do histórico',
|
||||
'agentSnapshot.fetchFailed': 'Falha ao buscar versões',
|
||||
'agentSnapshot.detailFailed': 'Falha ao buscar detalhes do snapshot',
|
||||
'agentSnapshot.currentVersion': 'Versão Atual',
|
||||
'agentSnapshot.source.config': 'Configuração Salva',
|
||||
'agentSnapshot.source.current': 'Configuração Atual',
|
||||
'agentSnapshot.source.restore': 'Antes da Restauração',
|
||||
'agentSnapshot.source.initial': 'Versão inicial',
|
||||
'agentSnapshot.field.restore': 'Restauração',
|
||||
'agentSnapshot.field.initial': 'Snapshot inicial',
|
||||
'agentSnapshot.field.agentCode': 'Código do Agente',
|
||||
'agentSnapshot.field.agentName': 'Apelido',
|
||||
'agentSnapshot.field.asrModelId': 'Reconhecimento de Fala',
|
||||
'agentSnapshot.field.vadModelId': 'Detecção de Atividade de Voz',
|
||||
'agentSnapshot.field.llmModelId': 'Modelo de Linguagem Principal',
|
||||
'agentSnapshot.field.slmModelId': 'Modelo de Linguagem Pequeno',
|
||||
'agentSnapshot.field.vllmModelId': 'Modelo de Visão',
|
||||
'agentSnapshot.field.ttsModelId': 'Texto para Fala',
|
||||
'agentSnapshot.field.ttsVoiceId': 'Voz',
|
||||
'agentSnapshot.field.ttsLanguage': 'Idioma',
|
||||
'agentSnapshot.field.ttsVolume': 'Volume',
|
||||
'agentSnapshot.field.ttsRate': 'Velocidade',
|
||||
'agentSnapshot.field.ttsPitch': 'Tom',
|
||||
'agentSnapshot.field.memModelId': 'Modelo de Memória',
|
||||
'agentSnapshot.field.intentModelId': 'Reconhecimento de Intenção',
|
||||
'agentSnapshot.field.chatHistoryConf': 'Histórico de Conversa',
|
||||
'agentSnapshot.field.systemPrompt': 'Introdução',
|
||||
'agentSnapshot.field.summaryMemory': 'Memória',
|
||||
'agentSnapshot.field.langCode': 'Código do Idioma',
|
||||
'agentSnapshot.field.language': 'Idioma de Interação',
|
||||
'agentSnapshot.field.sort': 'Ordenação',
|
||||
'agentSnapshot.field.functions': 'Funções',
|
||||
'agentSnapshot.field.contextProviders': 'Provedores de Contexto',
|
||||
'agentSnapshot.field.correctWordFileIds': 'Palavras de Substituição',
|
||||
'agentSnapshot.field.tagNames': 'Rótulos',
|
||||
'agentSnapshot.field.tagIds': 'IDs dos Rótulos',
|
||||
'agentSnapshot.function.added': 'Ativado',
|
||||
'agentSnapshot.function.removed': 'Desativado',
|
||||
'agentSnapshot.function.updated': 'Parâmetros alterados',
|
||||
'agentSnapshot.function.enabled': 'Ativado',
|
||||
'agentSnapshot.function.disabled': 'Desativado',
|
||||
'agentSnapshot.function.disabledNote': 'Esta função está desativada',
|
||||
'agentSnapshot.function.noParams': 'Nenhum parâmetro necessário',
|
||||
'agentSnapshot.chatHistoryConf.none': 'Não registrar histórico de conversa',
|
||||
'agentSnapshot.chatHistoryConf.text': 'Enviar texto',
|
||||
'agentSnapshot.chatHistoryConf.textVoice': 'Enviar texto e voz',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': 'Clima',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Reprodutor de música do servidor',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'China News',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'Agregação NewsNow',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'Consulta de estado do HomeAssistant',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'Atualização de estado do HomeAssistant',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'Reprodutor de música do HomeAssistant',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Pesquisa na web',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Chamada entre dispositivos',
|
||||
'agentSnapshot.pluginField.url': 'URL do endpoint',
|
||||
'agentSnapshot.pluginField.news_sources': 'Fontes de notícias',
|
||||
'agentSnapshot.pluginField.default_rss_url': 'URL RSS padrão',
|
||||
'agentSnapshot.pluginField.society_rss_url': 'URL RSS de sociedade',
|
||||
'agentSnapshot.pluginField.world_rss_url': 'URL RSS mundial',
|
||||
'agentSnapshot.pluginField.finance_rss_url': 'URL RSS financeiro',
|
||||
'agentSnapshot.pluginField.api_key': 'Chave de API',
|
||||
'agentSnapshot.pluginField.default_location': 'Cidade padrão',
|
||||
'agentSnapshot.pluginField.api_host': 'Host da API de desenvolvedor',
|
||||
'agentSnapshot.pluginField.base_url': 'URL do servidor',
|
||||
'agentSnapshot.pluginField.devices': 'Dispositivos',
|
||||
'agentSnapshot.pluginField.provider': 'Provedor de pesquisa',
|
||||
'agentSnapshot.pluginField.description': 'Descrição da ferramenta',
|
||||
'agentSnapshot.pluginField.max_results': 'Quantidade de resultados',
|
||||
'agentSnapshot.model.Memory_nomem': 'Sem memória',
|
||||
'agentSnapshot.model.Memory_mem_local_short': 'Memória curta local',
|
||||
'agentSnapshot.model.Memory_mem0ai': 'Memória Mem0AI',
|
||||
'agentSnapshot.model.Memory_mem_report_only': 'Somente relatório',
|
||||
'agentSnapshot.model.Intent_nointent': 'Sem reconhecimento de intenção',
|
||||
'agentSnapshot.model.Intent_intent_llm': 'Reconhecimento de intenção por LLM externo',
|
||||
'agentSnapshot.model.Intent_function_call': 'Chamada de função por LLM',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS feminino - Xiaoxiao',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS masculino - Yunyang',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS feminino - Xiaoyi',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS masculino - Yunjian',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS masculino - Yunxi',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS masculino - Yunxia',
|
||||
'roleConfig.vad': 'Detecção de Voz',
|
||||
'roleConfig.asr': 'Reconhecimento de Fala',
|
||||
'roleConfig.llm': 'Modelo de Linguagem Principal',
|
||||
@@ -1336,10 +1452,6 @@ export default {
|
||||
'knowledgeFileUpload.viewSlices': 'Ver Fatias',
|
||||
'knowledgeFileUpload.delete': 'Excluir',
|
||||
'knowledgeFileUpload.itemsPerPage': 'itens/página',
|
||||
'knowledgeFileUpload.firstPage': 'Primeira Página',
|
||||
'knowledgeFileUpload.prevPage': 'Página Anterior',
|
||||
'knowledgeFileUpload.nextPage': 'Próxima Página',
|
||||
'knowledgeFileUpload.totalRecords': 'Total de {total} registros',
|
||||
'knowledgeFileUpload.uploadDocument': 'Enviar Documento',
|
||||
'knowledgeFileUpload.documentNamePlaceholder': 'Por favor, insira o nome do documento',
|
||||
'knowledgeFileUpload.file': 'Arquivo',
|
||||
@@ -1378,10 +1490,7 @@ export default {
|
||||
'knowledgeFileUpload.sliceCount': 'Quantidade de Fatias',
|
||||
'knowledgeFileUpload.add': 'Adicionar',
|
||||
'knowledgeFileUpload.retrievalTest': 'Teste de Recuperação',
|
||||
'knowledgeFileUpload.testQuestion': 'Pergunta de Teste',
|
||||
'knowledgeFileUpload.testQuestionPlaceholder': 'Por favor, insira a pergunta para testar',
|
||||
'knowledgeFileUpload.executeTest': 'Executar Teste',
|
||||
'knowledgeFileUpload.testResult': 'Resultado do Teste:',
|
||||
'knowledgeFileUpload.selectedFiles': 'Arquivos Selecionados',
|
||||
'knowledgeFileUpload.totalSlices': 'Total de {total} registros',
|
||||
'knowledgeFileUpload.slice': 'Fatia',
|
||||
@@ -1457,7 +1566,6 @@ export default {
|
||||
'addressBookManagement.name': 'Nome da Lista de Contatos',
|
||||
'addressBookManagement.description': 'Descrição da Lista de Contatos',
|
||||
'addressBookManagement.contactCount': 'Número de Contatos',
|
||||
'addressBookManagement.status': 'Ativo',
|
||||
'addressBookManagement.createdAt': 'Data de Criação',
|
||||
'addressBookManagement.operation': 'Operação',
|
||||
'addressBookManagement.add': 'Adicionar',
|
||||
@@ -1478,9 +1586,7 @@ export default {
|
||||
'addressBookManagement.operationCancelled': 'Operação de exclusão cancelada',
|
||||
'addressBookManagement.updateSuccess': 'Atualização bem-sucedida',
|
||||
'addressBookManagement.addSuccess': 'Adição bem-sucedida',
|
||||
'addressBookManagement.updateFailed': 'Falha na atualização',
|
||||
'addressBookManagement.addFailed': 'Falha ao adicionar',
|
||||
'addressBookManagement.selectAll': 'Selecionar Todos',
|
||||
'addressBookManagement.cancelSelectAll': 'Desmarcar Todos',
|
||||
'addressBookManagement.deviceName': 'Nome do Dispositivo',
|
||||
'addressBookManagement.deviceType': 'Tipo do Dispositivo',
|
||||
@@ -1495,9 +1601,7 @@ export default {
|
||||
'addressBookManagement.addressBookDescPlaceholder': 'Digite a descrição da lista de contatos',
|
||||
'addressBookManagement.remarksPlaceholder': 'Digite as observações',
|
||||
'addressBookManagement.selectDevice': 'Selecione um dispositivo para ver os detalhes da lista de contatos',
|
||||
'addressBookManagement.save': 'Salvar',
|
||||
'addressBookManagement.saveSuccess': 'Salvo com sucesso',
|
||||
'addressBookManagement.saveFailed': 'Falha ao salvar',
|
||||
'addressBookManagement.deviceNotSelected': 'Selecione um dispositivo primeiro',
|
||||
'addressBookManagement.addDeviceTip': 'Recurso de adicionar dispositivo em desenvolvimento',
|
||||
'addressBookManagement.selectAgentFirst': 'Selecione um agente primeiro',
|
||||
|
||||
+116
-12
@@ -806,12 +806,128 @@ export default {
|
||||
'roleConfig.addTag': 'Thêm mới nhãn',
|
||||
'roleConfig.restartNotice': 'Sau khi lưu cấu hình, bạn cần khởi động lại thiết bị để cấu hình mới có hiệu lực.',
|
||||
'roleConfig.saveConfig': 'Lưu cấu hình',
|
||||
'roleConfig.snapshotHistory': 'Lịch sử phiên bản',
|
||||
'roleConfig.currentVersion': 'Phiên bản hiện tại {version}',
|
||||
'roleConfig.functionName': 'Tên chức năng',
|
||||
'roleConfig.reset': 'Đặt lại',
|
||||
'roleConfig.agentName': 'Biệt danh',
|
||||
'roleConfig.roleTemplate': 'Mẫu',
|
||||
'roleConfig.roleIntroduction': 'Giới thiệu',
|
||||
'roleConfig.languageCode': 'Mã ngôn ngữ',
|
||||
'roleConfig.interactionLanguage': 'Ngôn ngữ tương tác',
|
||||
'agentSnapshot.title': 'Lịch sử phiên bản',
|
||||
'agentSnapshot.empty': 'Chưa có phiên bản',
|
||||
'agentSnapshot.version': 'Phiên bản',
|
||||
'agentSnapshot.createdAt': 'Lưu lúc',
|
||||
'agentSnapshot.source': 'Nguồn',
|
||||
'agentSnapshot.changedFields': 'Thay đổi',
|
||||
'agentSnapshot.actions': 'Thao tác',
|
||||
'agentSnapshot.view': 'Xem',
|
||||
'agentSnapshot.restore': 'Khôi phục',
|
||||
'agentSnapshot.delete': 'Xóa',
|
||||
'agentSnapshot.detailTitle': 'Chi tiết thay đổi',
|
||||
'agentSnapshot.restorePreviewTitle': 'Xem trước khôi phục',
|
||||
'agentSnapshot.confirmRestore': 'Xác nhận khôi phục',
|
||||
'agentSnapshot.before': 'Trước',
|
||||
'agentSnapshot.after': 'Sau',
|
||||
'agentSnapshot.emptyValue': 'Không có',
|
||||
'agentSnapshot.secretRedacted': 'Khóa bí mật đã được ẩn',
|
||||
'agentSnapshot.configValue': 'Giá trị cấu hình',
|
||||
'agentSnapshot.noChangedContent': 'Không có thay đổi để hiển thị',
|
||||
'agentSnapshot.noFunctionChange': 'Không có thay đổi chức năng',
|
||||
'agentSnapshot.beforeChange': 'Trước thay đổi',
|
||||
'agentSnapshot.afterChange': 'Sau thay đổi',
|
||||
'agentSnapshot.beforeRestore': 'Trước khi khôi phục',
|
||||
'agentSnapshot.afterRestore': 'Sau khi khôi phục',
|
||||
'agentSnapshot.restoreMemoryWarning': 'Khôi phục về phiên bản không có bộ nhớ sẽ xóa lịch sử trò chuyện của tác nhân này. Chỉ tiếp tục sau khi xác nhận rủi ro này.',
|
||||
'agentSnapshot.restoreConfirm': 'Khôi phục về phiên bản {version}? Cấu hình hiện tại sẽ được lưu thành phiên bản mới trước.',
|
||||
'agentSnapshot.restoreSuccess': 'Đã khôi phục phiên bản',
|
||||
'agentSnapshot.restoreFailed': 'Khôi phục phiên bản thất bại',
|
||||
'agentSnapshot.restoreFailedRollback': 'Khôi phục phiên bản thất bại. Giao dịch đã được hoàn tác và cấu hình hiện tại chưa thay đổi',
|
||||
'agentSnapshot.deleteConfirm': 'Xóa phiên bản {version}? Không thể hoàn tác thao tác này.',
|
||||
'agentSnapshot.deleteSuccess': 'Đã xóa phiên bản lịch sử',
|
||||
'agentSnapshot.deleteFailed': 'Xóa phiên bản lịch sử thất bại',
|
||||
'agentSnapshot.fetchFailed': 'Không thể tải lịch sử phiên bản',
|
||||
'agentSnapshot.detailFailed': 'Không thể tải chi tiết snapshot',
|
||||
'agentSnapshot.currentVersion': 'Phiên bản hiện tại',
|
||||
'agentSnapshot.source.config': 'Lưu cấu hình',
|
||||
'agentSnapshot.source.current': 'Cấu hình hiện tại',
|
||||
'agentSnapshot.source.restore': 'Trước khi khôi phục',
|
||||
'agentSnapshot.source.initial': 'Phiên bản ban đầu',
|
||||
'agentSnapshot.field.restore': 'Khôi phục',
|
||||
'agentSnapshot.field.initial': 'Snapshot ban đầu',
|
||||
'agentSnapshot.field.agentCode': 'Mã tác nhân',
|
||||
'agentSnapshot.field.agentName': 'Biệt danh',
|
||||
'agentSnapshot.field.asrModelId': 'Nhận dạng giọng nói',
|
||||
'agentSnapshot.field.vadModelId': 'Phát hiện hoạt động giọng nói',
|
||||
'agentSnapshot.field.llmModelId': 'Mô hình ngôn ngữ chính',
|
||||
'agentSnapshot.field.slmModelId': 'Mô hình ngôn ngữ nhỏ',
|
||||
'agentSnapshot.field.vllmModelId': 'Mô hình thị giác',
|
||||
'agentSnapshot.field.ttsModelId': 'Văn bản thành giọng nói',
|
||||
'agentSnapshot.field.ttsVoiceId': 'Giọng nói',
|
||||
'agentSnapshot.field.ttsLanguage': 'Ngôn ngữ',
|
||||
'agentSnapshot.field.ttsVolume': 'Âm lượng',
|
||||
'agentSnapshot.field.ttsRate': 'Tốc độ',
|
||||
'agentSnapshot.field.ttsPitch': 'Cao độ',
|
||||
'agentSnapshot.field.memModelId': 'Mô hình bộ nhớ',
|
||||
'agentSnapshot.field.intentModelId': 'Nhận dạng ý định',
|
||||
'agentSnapshot.field.chatHistoryConf': 'Lịch sử trò chuyện',
|
||||
'agentSnapshot.field.systemPrompt': 'Giới thiệu',
|
||||
'agentSnapshot.field.summaryMemory': 'Bộ nhớ',
|
||||
'agentSnapshot.field.langCode': 'Mã ngôn ngữ',
|
||||
'agentSnapshot.field.language': 'Ngôn ngữ tương tác',
|
||||
'agentSnapshot.field.sort': 'Sắp xếp',
|
||||
'agentSnapshot.field.functions': 'Chức năng',
|
||||
'agentSnapshot.field.contextProviders': 'Nguồn ngữ cảnh',
|
||||
'agentSnapshot.field.correctWordFileIds': 'Từ thay thế',
|
||||
'agentSnapshot.field.tagNames': 'Nhãn',
|
||||
'agentSnapshot.field.tagIds': 'ID nhãn',
|
||||
'agentSnapshot.function.added': 'Đã bật',
|
||||
'agentSnapshot.function.removed': 'Đã tắt',
|
||||
'agentSnapshot.function.updated': 'Tham số đã thay đổi',
|
||||
'agentSnapshot.function.enabled': 'Đã bật',
|
||||
'agentSnapshot.function.disabled': 'Đã tắt',
|
||||
'agentSnapshot.function.disabledNote': 'Chức năng này đang tắt',
|
||||
'agentSnapshot.function.noParams': 'Không cần cấu hình tham số',
|
||||
'agentSnapshot.chatHistoryConf.none': 'Không ghi lịch sử trò chuyện',
|
||||
'agentSnapshot.chatHistoryConf.text': 'Báo cáo văn bản',
|
||||
'agentSnapshot.chatHistoryConf.textVoice': 'Báo cáo văn bản và giọng nói',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': 'Thời tiết',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': 'Trình phát nhạc máy chủ',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': 'Tin tức China News',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'Tổng hợp NewsNow',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'Truy vấn trạng thái HomeAssistant',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'Cập nhật trạng thái HomeAssistant',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'Trình phát nhạc HomeAssistant',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': 'Tìm kiếm web',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': 'Gọi thiết bị',
|
||||
'agentSnapshot.pluginField.url': 'URL điểm cuối',
|
||||
'agentSnapshot.pluginField.news_sources': 'Nguồn tin',
|
||||
'agentSnapshot.pluginField.default_rss_url': 'URL RSS mặc định',
|
||||
'agentSnapshot.pluginField.society_rss_url': 'URL RSS xã hội',
|
||||
'agentSnapshot.pluginField.world_rss_url': 'URL RSS thế giới',
|
||||
'agentSnapshot.pluginField.finance_rss_url': 'URL RSS tài chính',
|
||||
'agentSnapshot.pluginField.api_key': 'Khóa API',
|
||||
'agentSnapshot.pluginField.default_location': 'Thành phố mặc định',
|
||||
'agentSnapshot.pluginField.api_host': 'Máy chủ API nhà phát triển',
|
||||
'agentSnapshot.pluginField.base_url': 'URL máy chủ',
|
||||
'agentSnapshot.pluginField.devices': 'Thiết bị',
|
||||
'agentSnapshot.pluginField.provider': 'Nhà cung cấp tìm kiếm',
|
||||
'agentSnapshot.pluginField.description': 'Mô tả công cụ',
|
||||
'agentSnapshot.pluginField.max_results': 'Số lượng kết quả',
|
||||
'agentSnapshot.model.Memory_nomem': 'Không có bộ nhớ',
|
||||
'agentSnapshot.model.Memory_mem_local_short': 'Bộ nhớ ngắn cục bộ',
|
||||
'agentSnapshot.model.Memory_mem0ai': 'Bộ nhớ Mem0AI',
|
||||
'agentSnapshot.model.Memory_mem_report_only': 'Chỉ báo cáo bộ nhớ',
|
||||
'agentSnapshot.model.Intent_nointent': 'Không nhận dạng ý định',
|
||||
'agentSnapshot.model.Intent_intent_llm': 'Nhận dạng ý định bằng LLM bên ngoài',
|
||||
'agentSnapshot.model.Intent_function_call': 'LLM gọi hàm',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS nữ - Xiaoxiao',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS nam - Yunyang',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS nữ - Xiaoyi',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS nam - Yunjian',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS nam - Yunxi',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS nam - Yunxia',
|
||||
'roleConfig.vad': 'Phát hiện giọng nói',
|
||||
'roleConfig.asr': 'Nhận dạng giọng nói',
|
||||
'roleConfig.llm': 'Mô hình ngôn ngữ chính',
|
||||
@@ -1336,10 +1452,6 @@ export default {
|
||||
'knowledgeFileUpload.viewSlices': 'Xem các phần',
|
||||
'knowledgeFileUpload.delete': 'Xóa',
|
||||
'knowledgeFileUpload.itemsPerPage': 'mục/trang',
|
||||
'knowledgeFileUpload.firstPage': 'Trang đầu',
|
||||
'knowledgeFileUpload.prevPage': 'Trang trước',
|
||||
'knowledgeFileUpload.nextPage': 'Trang sau',
|
||||
'knowledgeFileUpload.totalRecords': 'Tổng cộng {total} bản ghi',
|
||||
'knowledgeFileUpload.uploadDocument': 'Tải lên tài liệu',
|
||||
'knowledgeFileUpload.documentNamePlaceholder': 'Vui lòng nhập tên tài liệu',
|
||||
'knowledgeFileUpload.file': 'Tệp',
|
||||
@@ -1378,10 +1490,7 @@ export default {
|
||||
'knowledgeFileUpload.sliceCount': 'Số lượng phần',
|
||||
'knowledgeFileUpload.add': 'Thêm',
|
||||
'knowledgeFileUpload.retrievalTest': 'Kiểm tra truy xuất',
|
||||
'knowledgeFileUpload.testQuestion': 'Câu hỏi kiểm tra',
|
||||
'knowledgeFileUpload.testQuestionPlaceholder': 'Vui lòng nhập câu hỏi để kiểm tra',
|
||||
'knowledgeFileUpload.executeTest': 'Thực thi kiểm tra',
|
||||
'knowledgeFileUpload.testResult': 'Kết quả kiểm tra:',
|
||||
'knowledgeFileUpload.selectedFiles': 'Tệp đã chọn',
|
||||
'knowledgeFileUpload.totalSlices': 'Tổng cộng {total} bản ghi',
|
||||
'knowledgeFileUpload.slice': 'Phần',
|
||||
@@ -1457,7 +1566,6 @@ export default {
|
||||
'addressBookManagement.name': 'Tên danh bạ',
|
||||
'addressBookManagement.description': 'Mô tả danh bạ',
|
||||
'addressBookManagement.contactCount': 'Số liên hệ',
|
||||
'addressBookManagement.status': 'Kích hoạt',
|
||||
'addressBookManagement.createdAt': 'Thời gian tạo',
|
||||
'addressBookManagement.operation': 'Thao tác',
|
||||
'addressBookManagement.add': 'Thêm',
|
||||
@@ -1478,9 +1586,7 @@ export default {
|
||||
'addressBookManagement.operationCancelled': 'Đã hủy thao tác xóa',
|
||||
'addressBookManagement.updateSuccess': 'Cập nhật thành công',
|
||||
'addressBookManagement.addSuccess': 'Thêm thành công',
|
||||
'addressBookManagement.updateFailed': 'Cập nhật thất bại',
|
||||
'addressBookManagement.addFailed': 'Thêm thất bại',
|
||||
'addressBookManagement.selectAll': 'Chọn tất cả',
|
||||
'addressBookManagement.cancelSelectAll': 'Bỏ chọn tất cả',
|
||||
'addressBookManagement.deviceName': 'Tên thiết bị',
|
||||
'addressBookManagement.deviceType': 'Loại thiết bị',
|
||||
@@ -1495,9 +1601,7 @@ export default {
|
||||
'addressBookManagement.addressBookDescPlaceholder': 'Vui lòng nhập mô tả danh bạ',
|
||||
'addressBookManagement.remarksPlaceholder': 'Vui lòng nhập ghi chú',
|
||||
'addressBookManagement.selectDevice': 'Vui lòng chọn thiết bị để xem chi tiết danh bạ',
|
||||
'addressBookManagement.save': 'Lưu',
|
||||
'addressBookManagement.saveSuccess': 'Lưu thành công',
|
||||
'addressBookManagement.saveFailed': 'Lưu thất bại',
|
||||
'addressBookManagement.deviceNotSelected': 'Vui lòng chọn thiết bị trước',
|
||||
'addressBookManagement.addDeviceTip': 'Tính năng thêm thiết bị đang phát triển',
|
||||
'addressBookManagement.selectAgentFirst': 'Vui lòng chọn tác tử trước',
|
||||
|
||||
@@ -275,7 +275,7 @@ export default {
|
||||
'voicePrint.closeOperation': '操作已关闭',
|
||||
'voicePrint.loading': '拼命加载中',
|
||||
|
||||
// 手動添加設備對話框相關
|
||||
// 手动添加设备对话框相关
|
||||
'manualAddDeviceDialog.title': '手动添加设备',
|
||||
'manualAddDeviceDialog.deviceType': '设备型号',
|
||||
'manualAddDeviceDialog.deviceTypePlaceholder': '请选择设备型号',
|
||||
@@ -806,6 +806,9 @@ export default {
|
||||
'roleConfig.addTag': '添加新标签',
|
||||
'roleConfig.restartNotice': '保存配置后,需要重启设备,新的配置才会生效。',
|
||||
'roleConfig.saveConfig': '保存配置',
|
||||
'roleConfig.snapshotHistory': '历史版本',
|
||||
'roleConfig.currentVersion': '当前版本 {version}',
|
||||
'roleConfig.functionName': '功能名称',
|
||||
'roleConfig.reset': '重置',
|
||||
'roleConfig.agentName': '助手昵称',
|
||||
'roleConfig.roleTemplate': '角色模版',
|
||||
@@ -815,7 +818,7 @@ export default {
|
||||
'roleConfig.vad': '语音活动检测(VAD)',
|
||||
'roleConfig.asr': '语音识别(ASR)',
|
||||
'roleConfig.llm': '主语言模型(LLM)',
|
||||
"roleConfig.slm": "小参数模型(SLM)",
|
||||
'roleConfig.slm': '小参数模型(SLM)',
|
||||
'roleConfig.vllm': '视觉大模型(VLLM)',
|
||||
'roleConfig.intent': '意图识别(Intent)',
|
||||
'roleConfig.memoryHis': '记忆',
|
||||
@@ -862,6 +865,119 @@ export default {
|
||||
'roleConfig.audioPlayFailed': '音频播放失败',
|
||||
'roleConfig.cannotPlayAudio': '无法播放音频',
|
||||
'roleConfig.audioPlayError': '播放音频过程出错',
|
||||
'agentSnapshot.title': '历史版本',
|
||||
'agentSnapshot.empty': '暂无历史版本',
|
||||
'agentSnapshot.version': '版本',
|
||||
'agentSnapshot.createdAt': '保存时间',
|
||||
'agentSnapshot.source': '来源',
|
||||
'agentSnapshot.changedFields': '变更内容',
|
||||
'agentSnapshot.actions': '操作',
|
||||
'agentSnapshot.view': '查看',
|
||||
'agentSnapshot.restore': '恢复',
|
||||
'agentSnapshot.delete': '删除',
|
||||
'agentSnapshot.detailTitle': '变更详情',
|
||||
'agentSnapshot.restorePreviewTitle': '恢复确认',
|
||||
'agentSnapshot.confirmRestore': '确认恢复',
|
||||
'agentSnapshot.before': '变化前',
|
||||
'agentSnapshot.after': '变化后',
|
||||
'agentSnapshot.emptyValue': '无',
|
||||
'agentSnapshot.secretRedacted': '密钥已脱敏',
|
||||
'agentSnapshot.configValue': '配置值',
|
||||
'agentSnapshot.noChangedContent': '暂无可展示的变更内容',
|
||||
'agentSnapshot.noFunctionChange': '功能配置无变化',
|
||||
'agentSnapshot.beforeChange': '变化前',
|
||||
'agentSnapshot.afterChange': '变化后',
|
||||
'agentSnapshot.beforeRestore': '恢复前',
|
||||
'agentSnapshot.afterRestore': '恢复后',
|
||||
'agentSnapshot.restoreMemoryWarning': '恢复到无记忆版本会清空该智能体的聊天记录,请确认后再继续。',
|
||||
'agentSnapshot.restoreConfirm': '确定恢复到版本 {version} 吗?当前配置会先自动保存为新的历史版本。',
|
||||
'agentSnapshot.restoreSuccess': '已恢复历史版本',
|
||||
'agentSnapshot.restoreFailed': '恢复历史版本失败',
|
||||
'agentSnapshot.restoreFailedRollback': '恢复历史版本失败,事务已回滚,当前配置未改变',
|
||||
'agentSnapshot.deleteConfirm': '确定删除版本 {version} 吗?删除后不可恢复。',
|
||||
'agentSnapshot.deleteSuccess': '已删除历史版本',
|
||||
'agentSnapshot.deleteFailed': '删除历史版本失败',
|
||||
'agentSnapshot.fetchFailed': '获取历史版本失败',
|
||||
'agentSnapshot.detailFailed': '获取快照详情失败',
|
||||
'agentSnapshot.currentVersion': '当前版本',
|
||||
'agentSnapshot.source.config': '配置保存',
|
||||
'agentSnapshot.source.current': '当前配置',
|
||||
'agentSnapshot.source.restore': '恢复前',
|
||||
'agentSnapshot.source.initial': '初始版本',
|
||||
'agentSnapshot.field.restore': '恢复操作',
|
||||
'agentSnapshot.field.initial': '初始快照',
|
||||
'agentSnapshot.field.agentCode': '助手编码',
|
||||
'agentSnapshot.field.agentName': '助手昵称',
|
||||
'agentSnapshot.field.asrModelId': '语音识别',
|
||||
'agentSnapshot.field.vadModelId': '语音活动检测',
|
||||
'agentSnapshot.field.llmModelId': '主语言模型',
|
||||
'agentSnapshot.field.slmModelId': '小参数模型',
|
||||
'agentSnapshot.field.vllmModelId': '视觉大模型',
|
||||
'agentSnapshot.field.ttsModelId': '语音合成',
|
||||
'agentSnapshot.field.ttsVoiceId': '声音音色',
|
||||
'agentSnapshot.field.ttsLanguage': '对话语言',
|
||||
'agentSnapshot.field.ttsVolume': '音量',
|
||||
'agentSnapshot.field.ttsRate': '语速',
|
||||
'agentSnapshot.field.ttsPitch': '音调',
|
||||
'agentSnapshot.field.memModelId': '记忆模式',
|
||||
'agentSnapshot.field.intentModelId': '意图识别',
|
||||
'agentSnapshot.field.chatHistoryConf': '聊天记录配置',
|
||||
'agentSnapshot.field.systemPrompt': '角色介绍',
|
||||
'agentSnapshot.field.summaryMemory': '记忆',
|
||||
'agentSnapshot.field.langCode': '语言编码',
|
||||
'agentSnapshot.field.language': '交互语种',
|
||||
'agentSnapshot.field.sort': '排序',
|
||||
'agentSnapshot.field.functions': '功能插件',
|
||||
'agentSnapshot.field.contextProviders': '上下文源',
|
||||
'agentSnapshot.field.correctWordFileIds': '替换词',
|
||||
'agentSnapshot.field.tagNames': '标签',
|
||||
'agentSnapshot.field.tagIds': '标签ID',
|
||||
'agentSnapshot.function.added': '已开启',
|
||||
'agentSnapshot.function.removed': '已关闭',
|
||||
'agentSnapshot.function.updated': '参数变更',
|
||||
'agentSnapshot.function.enabled': '已开启',
|
||||
'agentSnapshot.function.disabled': '未开启',
|
||||
'agentSnapshot.function.disabledNote': '未开启此功能',
|
||||
'agentSnapshot.function.noParams': '无需配置参数',
|
||||
'agentSnapshot.chatHistoryConf.none': '不记录聊天记录',
|
||||
'agentSnapshot.chatHistoryConf.text': '上报文字',
|
||||
'agentSnapshot.chatHistoryConf.textVoice': '上报文字+语音',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': '天气查询',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': '服务器音乐播放',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': '中新网新闻',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'newsnow新闻聚合',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant设备状态查询',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant设备状态修改',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant音乐播放',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': '联网搜索',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': '设备呼叫设备',
|
||||
'agentSnapshot.pluginField.url': '接口地址',
|
||||
'agentSnapshot.pluginField.news_sources': '新闻源配置',
|
||||
'agentSnapshot.pluginField.default_rss_url': '默认 RSS 源',
|
||||
'agentSnapshot.pluginField.society_rss_url': '社会新闻 RSS 地址',
|
||||
'agentSnapshot.pluginField.world_rss_url': '国际新闻 RSS 地址',
|
||||
'agentSnapshot.pluginField.finance_rss_url': '财经新闻 RSS 地址',
|
||||
'agentSnapshot.pluginField.api_key': 'API 密钥',
|
||||
'agentSnapshot.pluginField.default_location': '默认查询城市',
|
||||
'agentSnapshot.pluginField.api_host': '开发者 API Host',
|
||||
'agentSnapshot.pluginField.base_url': '服务器地址',
|
||||
'agentSnapshot.pluginField.devices': '设备列表',
|
||||
'agentSnapshot.pluginField.provider': '搜索源',
|
||||
'agentSnapshot.pluginField.description': '工具描述',
|
||||
'agentSnapshot.pluginField.max_results': '返回数量',
|
||||
'agentSnapshot.model.Memory_nomem': '无记忆',
|
||||
'agentSnapshot.model.Memory_mem_local_short': '本地短记忆',
|
||||
'agentSnapshot.model.Memory_mem0ai': 'Mem0AI记忆',
|
||||
'agentSnapshot.model.Memory_mem_report_only': '仅上报记忆',
|
||||
'agentSnapshot.model.Intent_nointent': '无意图识别',
|
||||
'agentSnapshot.model.Intent_intent_llm': '外挂的大模型意图识别',
|
||||
'agentSnapshot.model.Intent_function_call': '大模型自主函数调用',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS女声-晓晓',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS男声-云扬',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS女声-晓伊',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS男声-云健',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS男声-云希',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS男声-云夏',
|
||||
|
||||
// 表单字段 Tooltip 提示说明
|
||||
'roleConfig.tooltip.agentName': '设置智能体的名称,用于标识和识别您的AI助手',
|
||||
@@ -874,7 +990,7 @@ export default {
|
||||
'roleConfig.tooltip.vad': '语音活动检测(Voice Activity Detection):检测用户何时开始或结束说话,用于判断对话的开始和结束,实现打断功能',
|
||||
'roleConfig.tooltip.asr': '自动语音识别(Automatic Speech Recognition):将用户的语音转换为文字,是人机对话的第一步,支持多语言识别',
|
||||
'roleConfig.tooltip.llm': '主语言模型(Large Language Model):AI助手的"大脑",负责理解用户意图、生成回答和执行各种任务',
|
||||
"roleConfig.tooltip.slm": '小参数模型(Small Language Model):用于智能体唤醒,生成记忆总结标题',
|
||||
'roleConfig.tooltip.slm': '小参数模型(Small Language Model):用于智能体唤醒,生成记忆总结标题',
|
||||
'roleConfig.tooltip.vllm': '视觉大语言模型(Visual LLM):处理图像和视频理解,使AI助手能够分析和描述摄像头捕获的画面内容',
|
||||
'roleConfig.tooltip.intent': '意图识别(Intent Detection):分析用户语音或文本,判断用户的真实意图,如查询、聊天、控制设备等',
|
||||
'roleConfig.tooltip.memory': '记忆模型(Memory Model):管理对话历史的存储和摘要,决定AI能否记住之前的对话内容,实现长期记忆功能',
|
||||
@@ -1336,10 +1452,6 @@ export default {
|
||||
'knowledgeFileUpload.viewSlices': '查看切片',
|
||||
'knowledgeFileUpload.delete': '删除',
|
||||
'knowledgeFileUpload.itemsPerPage': '条/页',
|
||||
'knowledgeFileUpload.firstPage': '首页',
|
||||
'knowledgeFileUpload.prevPage': '上一页',
|
||||
'knowledgeFileUpload.nextPage': '下一页',
|
||||
'knowledgeFileUpload.totalRecords': '共{total}条记录',
|
||||
'knowledgeFileUpload.uploadDocument': '上传文档',
|
||||
'knowledgeFileUpload.documentNamePlaceholder': '请输入文档名称',
|
||||
'knowledgeFileUpload.file': '文件',
|
||||
@@ -1378,10 +1490,7 @@ export default {
|
||||
'knowledgeFileUpload.sliceCount': '切片数量',
|
||||
'knowledgeFileUpload.add': '新增',
|
||||
'knowledgeFileUpload.retrievalTest': '召回测试',
|
||||
'knowledgeFileUpload.testQuestion': '测试问题',
|
||||
'knowledgeFileUpload.testQuestionPlaceholder': '请输入要测试的问题',
|
||||
'knowledgeFileUpload.executeTest': '执行测试',
|
||||
'knowledgeFileUpload.testResult': '测试结果:',
|
||||
'knowledgeFileUpload.selectedFiles': '已选择文件',
|
||||
'knowledgeFileUpload.totalSlices': '共{total}条记录',
|
||||
'knowledgeFileUpload.slice': '切片',
|
||||
@@ -1401,6 +1510,8 @@ export default {
|
||||
'knowledgeFileUpload.testQuestionRequired': '请输入测试问题',
|
||||
'knowledgeBaseDialog.descriptionRequired': '请输入知识库描述',
|
||||
'knowledgeFileUpload.parsing': '正在解析中...',
|
||||
|
||||
// 知识库管理页面文本
|
||||
'knowledgeBaseManagement.loading': '加载中...',
|
||||
'knowledgeBaseManagement.noData': '暂无数据',
|
||||
'knowledgeBaseManagement.switchKnowledgeBase': '切换知识库',
|
||||
@@ -1455,7 +1566,6 @@ export default {
|
||||
'addressBookManagement.name': '通讯录名称',
|
||||
'addressBookManagement.description': '通讯录描述',
|
||||
'addressBookManagement.contactCount': '联系人数量',
|
||||
'addressBookManagement.status': '启用',
|
||||
'addressBookManagement.createdAt': '创建时间',
|
||||
'addressBookManagement.operation': '操作',
|
||||
'addressBookManagement.add': '新增',
|
||||
@@ -1476,9 +1586,7 @@ export default {
|
||||
'addressBookManagement.operationCancelled': '已取消删除操作',
|
||||
'addressBookManagement.updateSuccess': '修改成功',
|
||||
'addressBookManagement.addSuccess': '新增成功',
|
||||
'addressBookManagement.updateFailed': '更新失败',
|
||||
'addressBookManagement.addFailed': '新增失败',
|
||||
'addressBookManagement.selectAll': '全选',
|
||||
'addressBookManagement.cancelSelectAll': '取消全选',
|
||||
'addressBookManagement.deviceName': '设备名称',
|
||||
'addressBookManagement.deviceType': '设备类型',
|
||||
@@ -1495,7 +1603,6 @@ export default {
|
||||
'addressBookManagement.selectDevice': '请选择一个设备查看通讯录详情',
|
||||
'addressBookManagement.save': '保存权限',
|
||||
'addressBookManagement.saveSuccess': '保存成功',
|
||||
'addressBookManagement.saveFailed': '保存失败',
|
||||
'addressBookManagement.deviceNotSelected': '请先选择一个设备',
|
||||
'addressBookManagement.addDeviceTip': '新增设备功能开发中',
|
||||
'addressBookManagement.selectAgentFirst': '请先选择智能体',
|
||||
|
||||
@@ -806,12 +806,128 @@ export default {
|
||||
'roleConfig.addTag': '添加新標籤',
|
||||
'roleConfig.restartNotice': '保存配置後,需要重啟設備,新的配置才會生效。',
|
||||
'roleConfig.saveConfig': '保存配置',
|
||||
'roleConfig.snapshotHistory': '歷史版本',
|
||||
'roleConfig.currentVersion': '目前版本 {version}',
|
||||
'roleConfig.functionName': '功能名稱',
|
||||
'roleConfig.reset': '重置',
|
||||
'roleConfig.agentName': '助手暱稱',
|
||||
'roleConfig.roleTemplate': '角色模版',
|
||||
'roleConfig.roleIntroduction': '角色介紹',
|
||||
'roleConfig.languageCode': '語言編碼',
|
||||
'roleConfig.interactionLanguage': '交互語種',
|
||||
'agentSnapshot.title': '歷史版本',
|
||||
'agentSnapshot.empty': '暫無歷史版本',
|
||||
'agentSnapshot.version': '版本',
|
||||
'agentSnapshot.createdAt': '保存時間',
|
||||
'agentSnapshot.source': '來源',
|
||||
'agentSnapshot.changedFields': '變更內容',
|
||||
'agentSnapshot.actions': '操作',
|
||||
'agentSnapshot.view': '查看',
|
||||
'agentSnapshot.restore': '恢復',
|
||||
'agentSnapshot.delete': '刪除',
|
||||
'agentSnapshot.detailTitle': '變更詳情',
|
||||
'agentSnapshot.restorePreviewTitle': '恢復確認',
|
||||
'agentSnapshot.confirmRestore': '確認恢復',
|
||||
'agentSnapshot.before': '之前',
|
||||
'agentSnapshot.after': '之後',
|
||||
'agentSnapshot.emptyValue': '無',
|
||||
'agentSnapshot.secretRedacted': '密鑰已脫敏',
|
||||
'agentSnapshot.configValue': '配置值',
|
||||
'agentSnapshot.noChangedContent': '暫無可展示的變更內容',
|
||||
'agentSnapshot.noFunctionChange': '功能配置無變化',
|
||||
'agentSnapshot.beforeChange': '變化前',
|
||||
'agentSnapshot.afterChange': '變化後',
|
||||
'agentSnapshot.beforeRestore': '恢復前',
|
||||
'agentSnapshot.afterRestore': '恢復後',
|
||||
'agentSnapshot.restoreMemoryWarning': '恢復到無記憶版本會清空該智能體的聊天記錄,請確認後再繼續。',
|
||||
'agentSnapshot.restoreConfirm': '確定恢復到版本 {version} 嗎?目前配置會先自動保存為新的歷史版本。',
|
||||
'agentSnapshot.restoreSuccess': '已恢復歷史版本',
|
||||
'agentSnapshot.restoreFailed': '恢復歷史版本失敗',
|
||||
'agentSnapshot.restoreFailedRollback': '恢復歷史版本失敗,交易已回滾,目前配置未改變',
|
||||
'agentSnapshot.deleteConfirm': '確定刪除版本 {version} 嗎?刪除後無法復原。',
|
||||
'agentSnapshot.deleteSuccess': '已刪除歷史版本',
|
||||
'agentSnapshot.deleteFailed': '刪除歷史版本失敗',
|
||||
'agentSnapshot.fetchFailed': '獲取歷史版本失敗',
|
||||
'agentSnapshot.detailFailed': '獲取快照詳情失敗',
|
||||
'agentSnapshot.currentVersion': '目前版本',
|
||||
'agentSnapshot.source.config': '配置保存',
|
||||
'agentSnapshot.source.current': '目前配置',
|
||||
'agentSnapshot.source.restore': '恢復前',
|
||||
'agentSnapshot.source.initial': '初始版本',
|
||||
'agentSnapshot.field.restore': '恢復操作',
|
||||
'agentSnapshot.field.initial': '初始快照',
|
||||
'agentSnapshot.field.agentCode': '助手編碼',
|
||||
'agentSnapshot.field.agentName': '助手暱稱',
|
||||
'agentSnapshot.field.asrModelId': '語音識別',
|
||||
'agentSnapshot.field.vadModelId': '語音活動檢測',
|
||||
'agentSnapshot.field.llmModelId': '主語言模型',
|
||||
'agentSnapshot.field.slmModelId': '小參數模型',
|
||||
'agentSnapshot.field.vllmModelId': '視覺大模型',
|
||||
'agentSnapshot.field.ttsModelId': '語音合成',
|
||||
'agentSnapshot.field.ttsVoiceId': '聲音音色',
|
||||
'agentSnapshot.field.ttsLanguage': '對話語言',
|
||||
'agentSnapshot.field.ttsVolume': '音量',
|
||||
'agentSnapshot.field.ttsRate': '語速',
|
||||
'agentSnapshot.field.ttsPitch': '音調',
|
||||
'agentSnapshot.field.memModelId': '記憶模式',
|
||||
'agentSnapshot.field.intentModelId': '意圖識別',
|
||||
'agentSnapshot.field.chatHistoryConf': '聊天記錄配置',
|
||||
'agentSnapshot.field.systemPrompt': '角色介紹',
|
||||
'agentSnapshot.field.summaryMemory': '記憶',
|
||||
'agentSnapshot.field.langCode': '語言編碼',
|
||||
'agentSnapshot.field.language': '交互語種',
|
||||
'agentSnapshot.field.sort': '排序',
|
||||
'agentSnapshot.field.functions': '功能插件',
|
||||
'agentSnapshot.field.contextProviders': '上下文源',
|
||||
'agentSnapshot.field.correctWordFileIds': '替換詞',
|
||||
'agentSnapshot.field.tagNames': '標籤',
|
||||
'agentSnapshot.field.tagIds': '標籤ID',
|
||||
'agentSnapshot.function.added': '已開啟',
|
||||
'agentSnapshot.function.removed': '已關閉',
|
||||
'agentSnapshot.function.updated': '參數變更',
|
||||
'agentSnapshot.function.enabled': '已開啟',
|
||||
'agentSnapshot.function.disabled': '未開啟',
|
||||
'agentSnapshot.function.disabledNote': '未開啟此功能',
|
||||
'agentSnapshot.function.noParams': '無需配置參數',
|
||||
'agentSnapshot.chatHistoryConf.none': '不記錄聊天記錄',
|
||||
'agentSnapshot.chatHistoryConf.text': '上報文字',
|
||||
'agentSnapshot.chatHistoryConf.textVoice': '上報文字+語音',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEATHER': '天氣查詢',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_MUSIC': '伺服器音樂播放',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_CHINANEWS': '中新網新聞',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_NEWS_NEWSNOW': 'newsnow新聞聚合',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_GET_STATE': 'HomeAssistant設備狀態查詢',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_SET_STATE': 'HomeAssistant設備狀態修改',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_HA_PLAY_MUSIC': 'HomeAssistant音樂播放',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_WEB_SEARCH': '聯網搜尋',
|
||||
'agentSnapshot.plugin.SYSTEM_PLUGIN_CALL_DEVICE': '設備呼叫設備',
|
||||
'agentSnapshot.pluginField.url': '接口地址',
|
||||
'agentSnapshot.pluginField.news_sources': '新聞源配置',
|
||||
'agentSnapshot.pluginField.default_rss_url': '預設 RSS 源',
|
||||
'agentSnapshot.pluginField.society_rss_url': '社會新聞 RSS 地址',
|
||||
'agentSnapshot.pluginField.world_rss_url': '國際新聞 RSS 地址',
|
||||
'agentSnapshot.pluginField.finance_rss_url': '財經新聞 RSS 地址',
|
||||
'agentSnapshot.pluginField.api_key': 'API 密鑰',
|
||||
'agentSnapshot.pluginField.default_location': '預設查詢城市',
|
||||
'agentSnapshot.pluginField.api_host': '開發者 API Host',
|
||||
'agentSnapshot.pluginField.base_url': '伺服器地址',
|
||||
'agentSnapshot.pluginField.devices': '設備列表',
|
||||
'agentSnapshot.pluginField.provider': '搜尋源',
|
||||
'agentSnapshot.pluginField.description': '工具描述',
|
||||
'agentSnapshot.pluginField.max_results': '返回數量',
|
||||
'agentSnapshot.model.Memory_nomem': '無記憶',
|
||||
'agentSnapshot.model.Memory_mem_local_short': '本地短記憶',
|
||||
'agentSnapshot.model.Memory_mem0ai': 'Mem0AI記憶',
|
||||
'agentSnapshot.model.Memory_mem_report_only': '僅上報記憶',
|
||||
'agentSnapshot.model.Intent_nointent': '無意圖識別',
|
||||
'agentSnapshot.model.Intent_intent_llm': '外掛的大模型意圖識別',
|
||||
'agentSnapshot.model.Intent_function_call': '大模型自主函式呼叫',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0001': 'EdgeTTS女聲-曉曉',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0002': 'EdgeTTS男聲-雲揚',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0003': 'EdgeTTS女聲-曉伊',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0004': 'EdgeTTS男聲-雲健',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0005': 'EdgeTTS男聲-雲希',
|
||||
'agentSnapshot.voice.TTS_EdgeTTS0006': 'EdgeTTS男聲-雲夏',
|
||||
'roleConfig.vad': '語音活動檢測(VAD)',
|
||||
'roleConfig.asr': '語音識別(ASR)',
|
||||
'roleConfig.llm': '主語言模型(LLM)',
|
||||
@@ -1336,10 +1452,6 @@ export default {
|
||||
'knowledgeFileUpload.viewSlices': '查看切片',
|
||||
'knowledgeFileUpload.delete': '刪除',
|
||||
'knowledgeFileUpload.itemsPerPage': '條/頁',
|
||||
'knowledgeFileUpload.firstPage': '首頁',
|
||||
'knowledgeFileUpload.prevPage': '上一頁',
|
||||
'knowledgeFileUpload.nextPage': '下一頁',
|
||||
'knowledgeFileUpload.totalRecords': '共{total}條記錄',
|
||||
'knowledgeFileUpload.uploadDocument': '上傳文檔',
|
||||
'knowledgeFileUpload.documentNamePlaceholder': '請輸入文檔名稱',
|
||||
'knowledgeFileUpload.file': '文件',
|
||||
@@ -1378,10 +1490,7 @@ export default {
|
||||
'knowledgeFileUpload.sliceCount': '切片數量',
|
||||
'knowledgeFileUpload.add': '新增',
|
||||
'knowledgeFileUpload.retrievalTest': '召回測試',
|
||||
'knowledgeFileUpload.testQuestion': '測試問題',
|
||||
'knowledgeFileUpload.testQuestionPlaceholder': '請輸入要測試的問題',
|
||||
'knowledgeFileUpload.executeTest': '執行測試',
|
||||
'knowledgeFileUpload.testResult': '測試結果:',
|
||||
'knowledgeFileUpload.selectedFiles': '已選擇文件',
|
||||
'knowledgeFileUpload.totalSlices': '共{total}條記錄',
|
||||
'knowledgeFileUpload.slice': '切片',
|
||||
@@ -1456,7 +1565,6 @@ export default {
|
||||
'addressBookManagement.name': '通訊錄名稱',
|
||||
'addressBookManagement.description': '通訊錄描述',
|
||||
'addressBookManagement.contactCount': '聯繫人數量',
|
||||
'addressBookManagement.status': '啟用',
|
||||
'addressBookManagement.createdAt': '創建時間',
|
||||
'addressBookManagement.operation': '操作',
|
||||
'addressBookManagement.add': '新增',
|
||||
@@ -1477,9 +1585,7 @@ export default {
|
||||
'addressBookManagement.operationCancelled': '已取消刪除操作',
|
||||
'addressBookManagement.updateSuccess': '修改成功',
|
||||
'addressBookManagement.addSuccess': '新增成功',
|
||||
'addressBookManagement.updateFailed': '更新失敗',
|
||||
'addressBookManagement.addFailed': '新增失敗',
|
||||
'addressBookManagement.selectAll': '全選',
|
||||
'addressBookManagement.cancelSelectAll': '取消全選',
|
||||
'addressBookManagement.selectAgent': '請選擇智能體',
|
||||
'addressBookManagement.deviceName': '設備名稱',
|
||||
@@ -1497,7 +1603,6 @@ export default {
|
||||
'addressBookManagement.selectDevice': '請選擇一個設備查看通訊錄詳情',
|
||||
'addressBookManagement.save': '保存權限',
|
||||
'addressBookManagement.saveSuccess': '儲存成功',
|
||||
'addressBookManagement.saveFailed': '儲存失敗',
|
||||
'addressBookManagement.deviceNotSelected': '請先選擇一個設備',
|
||||
'addressBookManagement.addDeviceTip': '新增設備功能開發中',
|
||||
'addressBookManagement.selectAgentFirst': '請先選擇智能體',
|
||||
|
||||
@@ -39,6 +39,7 @@ function formatDateTool(date, fmt) {
|
||||
const o = {
|
||||
'M+': date.getMonth() + 1,
|
||||
'd+': date.getDate(),
|
||||
'H+': date.getHours(),
|
||||
'h+': date.getHours(),
|
||||
'm+': date.getMinutes(),
|
||||
's+': date.getSeconds()
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="" />
|
||||
</div>
|
||||
<span class="header-title">{{ form.agentName }}</span>
|
||||
<span v-if="currentVersionNo" class="current-version-tag">
|
||||
{{ $t("roleConfig.currentVersion", { version: currentVersionNo }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="header-tags">
|
||||
<el-tag
|
||||
@@ -45,6 +48,9 @@
|
||||
<img loading="lazy" src="@/assets/home/info.png" alt="" />
|
||||
<span>{{ $t("roleConfig.restartNotice") }}</span>
|
||||
</div>
|
||||
<el-button class="history-btn" @click="showSnapshotDialog = true">
|
||||
{{ $t("roleConfig.snapshotHistory") }}
|
||||
</el-button>
|
||||
<el-button type="primary" class="save-btn" @click="saveConfig">
|
||||
{{ $t("roleConfig.saveConfig") }}
|
||||
</el-button>
|
||||
@@ -313,7 +319,7 @@
|
||||
placement="top"
|
||||
>
|
||||
<div slot="content">
|
||||
<div><strong>功能名称:</strong> {{ func.name }}</div>
|
||||
<div><strong>{{ $t("roleConfig.functionName") }}:</strong> {{ func.name }}</div>
|
||||
</div>
|
||||
<div class="icon-dot">
|
||||
{{ getFunctionDisplayChar(func.name) }}
|
||||
@@ -457,6 +463,13 @@
|
||||
:checked-replacement-word-ids="checkedReplacementWordIds"
|
||||
@save="handleTtsSettingsSave"
|
||||
/>
|
||||
<agent-snapshot-dialog
|
||||
v-if="$route.query.agentId"
|
||||
:visible.sync="showSnapshotDialog"
|
||||
:agent-id="$route.query.agentId"
|
||||
:current-version-no="currentVersionNo"
|
||||
@restored="handleSnapshotRestored"
|
||||
/>
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
@@ -470,6 +483,7 @@ import RequestService from "@/apis/httpRequest";
|
||||
import FunctionDialog from "@/components/FunctionDialog.vue";
|
||||
import ContextProviderDialog from "@/components/ContextProviderDialog.vue";
|
||||
import TtsAdvancedSettings from "@/components/TtsAdvancedSettings.vue";
|
||||
import AgentSnapshotDialog from "@/components/AgentSnapshotDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import i18n from "@/i18n";
|
||||
import featureManager from "@/utils/featureManager";
|
||||
@@ -477,11 +491,12 @@ import VersionFooter from "@/components/VersionFooter.vue";
|
||||
|
||||
export default {
|
||||
name: "RoleConfigPage",
|
||||
components: { HeaderBar, FunctionDialog, ContextProviderDialog, TtsAdvancedSettings, VersionFooter },
|
||||
components: { HeaderBar, FunctionDialog, ContextProviderDialog, TtsAdvancedSettings, AgentSnapshotDialog, VersionFooter },
|
||||
data() {
|
||||
return {
|
||||
showContextProviderDialog: false,
|
||||
showTtsAdvancedDialog: false,
|
||||
showSnapshotDialog: false,
|
||||
ttsSettings: {
|
||||
volume: 0,
|
||||
speed: 0,
|
||||
@@ -529,6 +544,7 @@ export default {
|
||||
voiceOptions: [],
|
||||
voiceDetails: {}, // 保存完整的音色信息
|
||||
showFunctionDialog: false,
|
||||
currentVersionNo: null,
|
||||
currentFunctions: [],
|
||||
currentContextProviders: [],
|
||||
allFunctions: [],
|
||||
@@ -546,6 +562,7 @@ export default {
|
||||
asr: false, // 语音识别功能状态
|
||||
},
|
||||
dynamicTags: [],
|
||||
originalTagNames: [],
|
||||
inputVisible: false,
|
||||
inputValue: '',
|
||||
checkedReplacementWordIds: []
|
||||
@@ -555,14 +572,26 @@ export default {
|
||||
goToHome() {
|
||||
this.$router.push("/home");
|
||||
},
|
||||
async saveConfig() {
|
||||
try {
|
||||
await this.handleSaveAgentTags(this.$route.query.agentId);
|
||||
} catch (error) {
|
||||
console.error('保存标签失败:', error);
|
||||
return;
|
||||
normalizeFunctionParams(params, fallback = {}) {
|
||||
if (params === null || params === undefined || params === '') {
|
||||
return { ...fallback };
|
||||
}
|
||||
|
||||
if (typeof params === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(params);
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
||||
? parsed
|
||||
: { ...fallback };
|
||||
} catch (error) {
|
||||
return { ...fallback };
|
||||
}
|
||||
}
|
||||
if (typeof params === 'object' && !Array.isArray(params)) {
|
||||
return { ...params };
|
||||
}
|
||||
return { ...fallback };
|
||||
},
|
||||
async saveConfig() {
|
||||
const configData = {
|
||||
agentCode: this.form.agentCode,
|
||||
agentName: this.form.agentName,
|
||||
@@ -585,12 +614,17 @@ export default {
|
||||
functions: this.currentFunctions.map((item) => {
|
||||
return {
|
||||
pluginId: item.id,
|
||||
paramInfo: item.params,
|
||||
paramInfo: this.normalizeFunctionParams(item.params),
|
||||
};
|
||||
}),
|
||||
contextProviders: this.currentContextProviders,
|
||||
correctWordFileIds: this.checkedReplacementWordIds,
|
||||
};
|
||||
const tagNames = this.dynamicTags.map(tag => tag.tagName);
|
||||
const tagsChanged = !this.isSameStringList(tagNames, this.originalTagNames);
|
||||
if (tagsChanged) {
|
||||
configData.tagNames = tagNames;
|
||||
}
|
||||
|
||||
// 只在用户设置了TTS参数时才传递(不为null/undefined)
|
||||
if (this.form.ttsVolume !== null && this.form.ttsVolume !== undefined) {
|
||||
@@ -602,12 +636,20 @@ export default {
|
||||
if (this.form.ttsPitch !== null && this.form.ttsPitch !== undefined) {
|
||||
configData.ttsPitch = this.form.ttsPitch;
|
||||
}
|
||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
||||
const agentId = this.$route.query.agentId;
|
||||
Api.agent.updateAgentConfig(agentId, configData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
const afterSave = () => {
|
||||
if (tagsChanged) {
|
||||
this.originalTagNames = [...tagNames];
|
||||
}
|
||||
this.$message.success({
|
||||
message: i18n.t("roleConfig.saveSuccess"),
|
||||
showClose: true,
|
||||
});
|
||||
this.fetchCurrentVersion(agentId);
|
||||
};
|
||||
afterSave();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || i18n.t("roleConfig.saveFailed"),
|
||||
@@ -617,6 +659,26 @@ export default {
|
||||
});
|
||||
|
||||
},
|
||||
handleSnapshotRestored() {
|
||||
const agentId = this.$route.query.agentId;
|
||||
if (agentId) {
|
||||
this.fetchAgentConfig(agentId);
|
||||
this.getAgentTags(agentId);
|
||||
this.fetchCurrentVersion(agentId);
|
||||
}
|
||||
},
|
||||
fetchCurrentVersion(agentId) {
|
||||
if (!agentId) {
|
||||
this.currentVersionNo = null;
|
||||
return;
|
||||
}
|
||||
|
||||
Api.agent.getDeviceConfig(agentId, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.currentVersionNo = data.data?.currentVersionNo || null;
|
||||
}
|
||||
});
|
||||
},
|
||||
resetConfig() {
|
||||
this.$confirm(i18n.t("roleConfig.confirmReset"), i18n.t("message.info"), {
|
||||
confirmButtonText: i18n.t("button.ok"),
|
||||
@@ -754,7 +816,7 @@ export default {
|
||||
id: mapping.pluginId,
|
||||
name: meta.name,
|
||||
// 后端如果还有 paramInfo 字段就用 mapping.paramInfo,否则用 meta.params 默认值
|
||||
params: mapping.paramInfo || { ...meta.params },
|
||||
params: this.normalizeFunctionParams(mapping.paramInfo, meta.params),
|
||||
fieldsMeta: meta.fieldsMeta, // 保留以便对话框渲染 tooltip
|
||||
};
|
||||
});
|
||||
@@ -806,7 +868,7 @@ export default {
|
||||
});
|
||||
this.$set(this.modelOptions, model.type, LLMdata);
|
||||
} else {
|
||||
this.$message.error(data.msg || "获取LLM模型列表失败");
|
||||
this.$message.error(data.msg || i18n.t("roleConfig.fetchModelsFailed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1329,7 +1391,7 @@ export default {
|
||||
handleInputConfirm() {
|
||||
let inputValue = this.inputValue;
|
||||
if (inputValue) {
|
||||
const tag = { id: new Date().getTime(), tagName: inputValue };
|
||||
const tag = { id: `tmp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, tagName: inputValue };
|
||||
this.dynamicTags.push(tag);
|
||||
}
|
||||
this.inputVisible = false;
|
||||
@@ -1339,14 +1401,21 @@ export default {
|
||||
Api.agent.getAgentTags(agentId, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.dynamicTags = data.data || [];
|
||||
this.originalTagNames = this.dynamicTags.map(tag => tag.tagName);
|
||||
}
|
||||
});
|
||||
},
|
||||
handleSaveAgentTags(agentId) {
|
||||
isSameStringList(left, right) {
|
||||
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
|
||||
return false;
|
||||
}
|
||||
return left.every((value, index) => value === right[index]);
|
||||
},
|
||||
handleSaveAgentTags(agentId, tagNames = this.dynamicTags.map(tag => tag.tagName)) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tagNames = this.dynamicTags.map(tag => tag.tagName);
|
||||
Api.agent.saveAgentTags(agentId, { tagNames }, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.originalTagNames = [...tagNames];
|
||||
resolve();
|
||||
} else {
|
||||
reject(data.msg);
|
||||
@@ -1382,6 +1451,7 @@ export default {
|
||||
this.fetchAgentConfig(agentId);
|
||||
this.getAgentTags(agentId);
|
||||
this.fetchAllFunctions();
|
||||
this.fetchCurrentVersion(agentId);
|
||||
}
|
||||
this.fetchModelOptions();
|
||||
this.fetchTemplates();
|
||||
@@ -1507,6 +1577,18 @@ export default {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.current-version-tag {
|
||||
flex-shrink: 0;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid #dfe7ff;
|
||||
border-radius: 999px;
|
||||
background: #f4f7ff;
|
||||
color: #5778ff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.more-tag {
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
@@ -1785,6 +1867,16 @@ export default {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-actions .history-btn {
|
||||
background: #ffffff;
|
||||
color: #4d5b7c;
|
||||
border: 1px solid #d8dce8;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-actions .reset-btn {
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
|
||||
@@ -45,7 +45,7 @@ async def monitor_stdin():
|
||||
|
||||
async def main():
|
||||
check_ffmpeg_installed()
|
||||
config = load_config()
|
||||
config = await load_config()
|
||||
|
||||
# auth_key优先级:配置文件server.auth_key > manager-api.secret > 自动生成
|
||||
# auth_key用于jwt认证,比如视觉分析接口的jwt认证、ota接口的token生成与websocket认证
|
||||
|
||||
Binary file not shown.
@@ -7,7 +7,6 @@ from config.manage_api_client import (
|
||||
get_server_config,
|
||||
get_agent_models,
|
||||
get_correct_words,
|
||||
lookup_address_book,
|
||||
DeviceNotFoundException,
|
||||
DeviceBindException,
|
||||
)
|
||||
@@ -24,7 +23,7 @@ def read_config(config_path):
|
||||
return config
|
||||
|
||||
|
||||
def load_config():
|
||||
async def load_config():
|
||||
"""加载配置文件"""
|
||||
from core.utils.cache.manager import cache_manager, CacheType
|
||||
|
||||
@@ -41,16 +40,7 @@ def load_config():
|
||||
custom_config = read_config(custom_config_path)
|
||||
|
||||
if custom_config.get("manager-api", {}).get("url"):
|
||||
import asyncio
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
# 如果已经在事件循环中,使用异步版本
|
||||
config = asyncio.run_coroutine_threadsafe(
|
||||
get_config_from_api_async(custom_config), loop
|
||||
).result()
|
||||
except RuntimeError:
|
||||
# 如果不在事件循环中(启动时),创建新的事件循环
|
||||
config = asyncio.run(get_config_from_api_async(custom_config))
|
||||
config = await get_config_from_api_async(custom_config)
|
||||
else:
|
||||
# 合并配置
|
||||
config = merge_configs(default_config, custom_config)
|
||||
@@ -139,7 +129,7 @@ def ensure_directories(config):
|
||||
selected_provider = selected_modules.get(module_type)
|
||||
if not selected_provider:
|
||||
continue
|
||||
if config.get(module) is None:
|
||||
if config.get(module_type) is None:
|
||||
continue
|
||||
if config.get(selected_provider) is None:
|
||||
continue
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import sys
|
||||
import asyncio
|
||||
from loguru import logger
|
||||
from config.config_loader import load_config
|
||||
from config.settings import check_config_file
|
||||
from datetime import datetime
|
||||
from core.utils.cache.manager import cache_manager, CacheType
|
||||
|
||||
SERVER_VERSION = "0.9.5"
|
||||
_logger_initialized = False
|
||||
@@ -45,10 +46,15 @@ def formatter(record):
|
||||
return record["message"]
|
||||
|
||||
|
||||
def setup_logging():
|
||||
check_config_file()
|
||||
def setup_logging(config=None):
|
||||
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
|
||||
config = load_config()
|
||||
if config is None:
|
||||
check_config_file()
|
||||
# 先查缓存,避免在 async 上下文中重复 await load_config
|
||||
config = cache_manager.get(CacheType.CONFIG, "main_config")
|
||||
if config is None:
|
||||
# 缓存也没有(理论上不该发生),才走 asyncio.run
|
||||
config = asyncio.run(load_config())
|
||||
log_config = config["log"]
|
||||
global _logger_initialized
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ class ManageApiClient:
|
||||
},
|
||||
timeout=cls.config.get("timeout", 30),
|
||||
limits=limits, # 使用限制
|
||||
trust_env=False,
|
||||
)
|
||||
return cls._async_clients[loop_id]
|
||||
except RuntimeError:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import asyncio
|
||||
from config.config_loader import read_config, get_project_dir, load_config
|
||||
|
||||
|
||||
@@ -20,7 +21,7 @@ def check_config_file():
|
||||
)
|
||||
|
||||
# 检查是否从API读取配置
|
||||
config = load_config()
|
||||
config = asyncio.run(load_config())
|
||||
if config.get("read_config_from_api", False):
|
||||
print("从API读取配置")
|
||||
old_config_origin = read_config(custom_config_file)
|
||||
|
||||
@@ -11,6 +11,8 @@ import threading
|
||||
import traceback
|
||||
import subprocess
|
||||
import websockets
|
||||
import opuslib_next
|
||||
import numpy as np
|
||||
|
||||
from core.utils.util import (
|
||||
extract_json_from_string,
|
||||
@@ -114,6 +116,7 @@ class ConnectionHandler:
|
||||
self.client_abort = False
|
||||
self.client_is_speaking = False
|
||||
self.client_listen_mode = "auto"
|
||||
self.client_aec = False # 是否启用了服务端AEC
|
||||
|
||||
# 线程任务相关
|
||||
self.loop = None # 在 handle_connection 中获取运行中的事件循环
|
||||
@@ -153,7 +156,7 @@ class ConnectionHandler:
|
||||
# asr相关变量
|
||||
# 因为实际部署时可能会用到公共的本地ASR,不能把变量暴露给公共ASR
|
||||
# 所以涉及到ASR的变量,需要在这里定义,属于connection的私有变量
|
||||
self.asr_audio = []
|
||||
self.asr_audio = [] # 存储PCM帧列表,供VAD和ASR共享
|
||||
self.asr_audio_queue = queue.Queue()
|
||||
self.current_speaker = None # 存储当前说话人
|
||||
|
||||
@@ -231,6 +234,9 @@ class ConnectionHandler:
|
||||
# 启动超时检查任务
|
||||
self.timeout_task = asyncio.create_task(self._check_timeout())
|
||||
|
||||
# 启动AEC缓存清理任务
|
||||
self._aec_cache_cleanup_task = asyncio.create_task(self._check_aec_cache_expiry())
|
||||
|
||||
self.welcome_msg = self.config["xiaozhi"]
|
||||
self.welcome_msg["session_id"] = self.session_id
|
||||
|
||||
@@ -366,12 +372,14 @@ class ConnectionHandler:
|
||||
if handled:
|
||||
return
|
||||
|
||||
# 不需要头部处理或没有头部时,直接处理原始消息
|
||||
self.asr_audio_queue.put(message)
|
||||
# 入口处直接解码PCM,避免VAD和ASR重复解码
|
||||
pcm_frame = self._decode_opus_packet(message)
|
||||
if pcm_frame:
|
||||
self.asr_audio_queue.put(pcm_frame)
|
||||
|
||||
async def _process_mqtt_audio_message(self, message):
|
||||
"""
|
||||
处理来自MQTT网关的音频消息,解析16字节头部并提取音频数据
|
||||
处理来自MQTT网关的音频消息,解析16字节头部并提取音频数据,在入队前进行AEC处理
|
||||
|
||||
Args:
|
||||
message: 包含头部的音频消息
|
||||
@@ -380,21 +388,20 @@ class ConnectionHandler:
|
||||
bool: 是否成功处理了消息
|
||||
"""
|
||||
try:
|
||||
# 提取头部信息
|
||||
# 解析timestamp
|
||||
timestamp = int.from_bytes(message[8:12], "big")
|
||||
audio_length = int.from_bytes(message[12:16], "big")
|
||||
|
||||
# 提取音频数据
|
||||
if audio_length > 0 and len(message) >= 16 + audio_length:
|
||||
# 有指定长度,提取精确的音频数据
|
||||
audio_data = message[16 : 16 + audio_length]
|
||||
# 基于时间戳进行排序处理
|
||||
self._process_websocket_audio(audio_data, timestamp)
|
||||
return True
|
||||
elif len(message) > 16:
|
||||
# 没有指定长度或长度无效,去掉头部后处理剩余数据
|
||||
audio_data = message[16:]
|
||||
self.asr_audio_queue.put(audio_data)
|
||||
# 入口直接解码PCM
|
||||
pcm_frame = self._decode_opus_packet(audio_data)
|
||||
if not pcm_frame:
|
||||
return True
|
||||
|
||||
# AEC处理:如果timestamp>0且启用了AEC
|
||||
if timestamp > 0 and self.client_aec:
|
||||
pcm_frame = self._apply_aec(timestamp, pcm_frame)
|
||||
|
||||
self.asr_audio_queue.put(pcm_frame)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"解析WebSocket音频包失败: {e}")
|
||||
@@ -402,36 +409,171 @@ class ConnectionHandler:
|
||||
# 处理失败,返回False表示需要继续处理
|
||||
return False
|
||||
|
||||
def _process_websocket_audio(self, audio_data, timestamp):
|
||||
"""处理WebSocket格式的音频包"""
|
||||
# 初始化时间戳序列管理
|
||||
if not hasattr(self, "audio_timestamp_buffer"):
|
||||
self.audio_timestamp_buffer = {}
|
||||
self.last_processed_timestamp = 0
|
||||
self.max_timestamp_buffer_size = 20
|
||||
def _apply_aec(self, timestamp: int, pcm_frame: bytes) -> bytes:
|
||||
"""应用AEC处理 - 综合算法:互相关延迟估计 + Wiener滤波 + 频谱减法"""
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
# 如果时间戳是递增的,直接处理
|
||||
if timestamp >= self.last_processed_timestamp:
|
||||
self.asr_audio_queue.put(audio_data)
|
||||
self.last_processed_timestamp = timestamp
|
||||
if not pcm_frame or len(pcm_frame) == 0:
|
||||
return pcm_frame
|
||||
|
||||
# 处理缓冲区中的后续包
|
||||
processed_any = True
|
||||
while processed_any:
|
||||
processed_any = False
|
||||
for ts in sorted(self.audio_timestamp_buffer.keys()):
|
||||
if ts > self.last_processed_timestamp:
|
||||
buffered_audio = self.audio_timestamp_buffer.pop(ts)
|
||||
self.asr_audio_queue.put(buffered_audio)
|
||||
self.last_processed_timestamp = ts
|
||||
processed_any = True
|
||||
break
|
||||
if not hasattr(self, "aec_audio_cache") or not self.aec_audio_cache:
|
||||
return pcm_frame
|
||||
|
||||
mic_audio = np.frombuffer(pcm_frame, dtype=np.int16).astype(np.float32)
|
||||
mic_rms = np.sqrt(np.mean(mic_audio ** 2))
|
||||
|
||||
if mic_rms < 100:
|
||||
return pcm_frame
|
||||
|
||||
# 初始化
|
||||
if not hasattr(self, "_aec_filter_state"):
|
||||
self._aec_filter_state = np.zeros(512) # 滤波器状态
|
||||
self._aec_delay_ms = 200
|
||||
self._aec_last_delay = 200
|
||||
self._aec_coherence_history = []
|
||||
|
||||
sorted_timestamps = sorted(self.aec_audio_cache.keys())
|
||||
if len(sorted_timestamps) < 2:
|
||||
return pcm_frame
|
||||
|
||||
# ========== 匹配参考帧(对数功率谱匹配) ==========
|
||||
n = len(mic_audio)
|
||||
sorted_timestamps = sorted(self.aec_audio_cache.keys())
|
||||
|
||||
# 找最接近的timestamp作为起点
|
||||
closest_idx = min(range(len(sorted_timestamps)), key=lambda i: abs(sorted_timestamps[i] - timestamp))
|
||||
|
||||
# 对数功率谱相关性计算
|
||||
def log_powerSpectrum_corr(audio1, audio2):
|
||||
window = np.hanning(len(audio1))
|
||||
fft1 = np.fft.rfft(audio1 * window)
|
||||
fft2 = np.fft.rfft(audio2 * window)
|
||||
psd1 = np.abs(fft1) ** 2
|
||||
psd2 = np.abs(fft2) ** 2
|
||||
log1 = 10 * np.log10(psd1 + 1e-8)
|
||||
log2 = 10 * np.log10(psd2 + 1e-8)
|
||||
P_xy = np.dot(log1, log2)
|
||||
P_xx = np.dot(log1, log1)
|
||||
P_yy = np.dot(log2, log2)
|
||||
return abs(P_xy) / (np.sqrt(P_xx) * np.sqrt(P_yy) + 1e-8)
|
||||
|
||||
# 用对数功率谱匹配找最佳帧:前后各找2帧
|
||||
best_corr = -1
|
||||
best_ref_idx = closest_idx
|
||||
|
||||
for offset in range(-2, 3): # T-2, T-1, T, T+1, T+2
|
||||
test_idx = closest_idx + offset
|
||||
if test_idx < 0 or test_idx >= len(sorted_timestamps):
|
||||
continue
|
||||
test_ts = sorted_timestamps[test_idx]
|
||||
test_ref = np.frombuffer(self.aec_audio_cache[test_ts], dtype=np.int16).astype(np.float32)
|
||||
test_ref_rms = np.sqrt(np.mean(test_ref ** 2))
|
||||
if test_ref_rms < 50:
|
||||
continue
|
||||
|
||||
# 对数功率谱相关性
|
||||
corr = log_powerSpectrum_corr(mic_audio, test_ref)
|
||||
|
||||
if corr > best_corr:
|
||||
best_corr = corr
|
||||
best_ref_idx = test_idx
|
||||
|
||||
best_ts = sorted_timestamps[best_ref_idx]
|
||||
|
||||
best_ref = np.frombuffer(self.aec_audio_cache[best_ts], dtype=np.int16).astype(np.float32)
|
||||
ref_rms = np.sqrt(np.mean(best_ref ** 2))
|
||||
|
||||
if ref_rms < 50:
|
||||
return pcm_frame
|
||||
|
||||
# 对齐参考信号(直接截取相同长度)
|
||||
aligned_ref = best_ref[:n]
|
||||
if len(aligned_ref) < n:
|
||||
aligned_ref = np.pad(aligned_ref, (0, n - len(aligned_ref)))
|
||||
|
||||
# ========== 频域 AEC 处理(谱减法) ==========
|
||||
# 时域信号经过声学路径后相位失真,导致时域相关性低且P_xy正负不定
|
||||
# 频域幅度谱不受相位影响,对数功率谱相关性稳定在0.97+
|
||||
# 公式:result_mag = max(|mic_fft| - |ref_fft| * scale * coef, 0)
|
||||
|
||||
window = np.hanning(n)
|
||||
mic_fft = np.fft.rfft(mic_audio * window)
|
||||
ref_fft = np.fft.rfft(aligned_ref * window)
|
||||
|
||||
mic_mag = np.abs(mic_fft)
|
||||
ref_mag = np.abs(ref_fft)
|
||||
mic_phase = np.angle(mic_fft)
|
||||
|
||||
# 频域计算回声比例 scale
|
||||
scale = np.sum(mic_mag * ref_mag) / (np.dot(ref_mag, ref_mag) + 1e-8)
|
||||
|
||||
# 使用对数功率谱相关性作为coh
|
||||
coh = best_corr
|
||||
ref_power = ref_rms
|
||||
|
||||
# 自适应系数:根据scale和coh动态调整
|
||||
# scale大(回声强)-> coef大;coh高(匹配准)-> coef大
|
||||
raw_coef = 1.0 + scale * 3 + (coh - 0.97) * 30
|
||||
coef = max(0.5, min(3.0, raw_coef))
|
||||
|
||||
# 谱减法(过减 + 半波整流)
|
||||
if ref_power < 50:
|
||||
output = mic_audio
|
||||
coef = 0
|
||||
gain = 0
|
||||
else:
|
||||
# 乱序包,暂存
|
||||
if len(self.audio_timestamp_buffer) < self.max_timestamp_buffer_size:
|
||||
self.audio_timestamp_buffer[timestamp] = audio_data
|
||||
else:
|
||||
self.asr_audio_queue.put(audio_data)
|
||||
echo_mag = ref_mag * scale * coef
|
||||
# 过减:使用较大系数抑制回声
|
||||
over_subtract = 1.5
|
||||
result_mag = np.maximum(mic_mag - echo_mag * over_subtract, mic_mag * 0.1)
|
||||
|
||||
# 保留相位重建信号
|
||||
result_fft = result_mag * np.exp(1j * mic_phase)
|
||||
output = np.fft.irfft(result_fft, n)
|
||||
gain = scale * coef
|
||||
|
||||
# 高置信度是纯回声时,再压一下确保VAD检测不到
|
||||
if coh >= 0.97 and ref_power > 500:
|
||||
output = output * 0.3
|
||||
|
||||
# 后处理:限幅
|
||||
output = np.clip(output, -32768, 32767)
|
||||
|
||||
# 转换为bytes
|
||||
result = output.astype(np.int16).tobytes()
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).warning(f"[AEC] 处理失败: {e}")
|
||||
return pcm_frame
|
||||
|
||||
def _decode_opus_packet(self, opus_packet: bytes) -> bytes:
|
||||
"""
|
||||
解码Opus数据包为PCM数据
|
||||
|
||||
Args:
|
||||
opus_packet: Opus编码的音频数据
|
||||
|
||||
Returns:
|
||||
bytes: 解码后的PCM数据,失败返回None
|
||||
"""
|
||||
try:
|
||||
if not opus_packet or len(opus_packet) == 0:
|
||||
return None
|
||||
|
||||
self._init_connection_state(self)
|
||||
pcm_frame = self._connection_opus_decoder.decode(opus_packet, 960)
|
||||
return pcm_frame
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).debug(f"Opus解码失败: {e}")
|
||||
return None
|
||||
|
||||
def _init_connection_state(self, conn):
|
||||
"""为连接初始化独立的Opus解码器"""
|
||||
if not hasattr(conn, "_connection_opus_decoder"):
|
||||
conn._connection_opus_decoder = opuslib_next.Decoder(16000, 1)
|
||||
|
||||
async def handle_restart(self, message):
|
||||
"""处理服务器重启请求"""
|
||||
@@ -1403,6 +1545,13 @@ class ConnectionHandler:
|
||||
):
|
||||
self.vad.release_conn_resources(self)
|
||||
|
||||
# 清理opus解码器
|
||||
if hasattr(self, "_connection_opus_decoder"):
|
||||
try:
|
||||
delattr(self, "_connection_opus_decoder")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 清理音频缓冲区
|
||||
if hasattr(self, "audio_buffer"):
|
||||
self.audio_buffer.clear()
|
||||
@@ -1416,6 +1565,20 @@ class ConnectionHandler:
|
||||
pass
|
||||
self.timeout_task = None
|
||||
|
||||
# 取消AEC缓存清理任务
|
||||
if hasattr(self, "_aec_cache_cleanup_task") and self._aec_cache_cleanup_task and not self._aec_cache_cleanup_task.done():
|
||||
self._aec_cache_cleanup_task.cancel()
|
||||
try:
|
||||
await self._aec_cache_cleanup_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
self._aec_cache_cleanup_task = None
|
||||
|
||||
# 清理AEC缓存
|
||||
if hasattr(self, "aec_audio_cache"):
|
||||
self.aec_audio_cache.clear()
|
||||
self.aec_audio_cache_time.clear()
|
||||
|
||||
# 清理工具处理器资源
|
||||
if hasattr(self, "func_handler") and self.func_handler:
|
||||
try:
|
||||
@@ -1579,6 +1742,26 @@ class ConnectionHandler:
|
||||
finally:
|
||||
self.logger.bind(tag=TAG).info("超时检查任务已退出")
|
||||
|
||||
async def _check_aec_cache_expiry(self):
|
||||
"""定期清理过期的AEC缓存"""
|
||||
try:
|
||||
while not self.stop_event.is_set():
|
||||
if hasattr(self, "aec_audio_cache") and self.aec_audio_cache:
|
||||
current_time = time.time()
|
||||
expired_keys = [
|
||||
ts for ts, cache_time in list(self.aec_audio_cache_time.items())
|
||||
if current_time - cache_time > 120 # 2分钟过期
|
||||
]
|
||||
for ts in expired_keys:
|
||||
self.aec_audio_cache.pop(ts, None)
|
||||
self.aec_audio_cache_time.pop(ts, None)
|
||||
if expired_keys:
|
||||
self.logger.bind(tag=TAG).debug(f"[AEC] 清理过期缓存 {len(expired_keys)} 条")
|
||||
# 每30秒检查一次
|
||||
await asyncio.sleep(30)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"AEC缓存清理任务出错: {e}")
|
||||
|
||||
@staticmethod
|
||||
def _extract_direct_answer_response(arguments_str):
|
||||
"""从 direct_answer 的参数中提取 response 值。
|
||||
|
||||
@@ -56,6 +56,9 @@ async def handleHelloMessage(conn: "ConnectionHandler", msg_json):
|
||||
conn.mcp_client = MCPClient()
|
||||
# 发送初始化
|
||||
asyncio.create_task(send_mcp_initialize_message(conn))
|
||||
if features.get("aec"):
|
||||
conn.logger.bind(tag=TAG).debug("客户端启用了服务端AEC")
|
||||
conn.client_aec = True
|
||||
|
||||
await conn.websocket.send(json.dumps(conn.welcome_msg))
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ from core.handle.sendAudioHandle import send_stt_message, SentenceType
|
||||
TAG = __name__
|
||||
|
||||
|
||||
async def handleAudioMessage(conn: "ConnectionHandler", audio):
|
||||
async def handleAudioMessage(conn: "ConnectionHandler", pcm_frame):
|
||||
# 当前片段是否有人说话
|
||||
have_voice = conn.vad.is_vad(conn, audio)
|
||||
have_voice = conn.vad.is_vad(conn, pcm_frame)
|
||||
# 如果设备刚刚被唤醒,短暂忽略VAD检测
|
||||
if hasattr(conn, "just_woken_up") and conn.just_woken_up:
|
||||
have_voice = False
|
||||
@@ -24,10 +24,14 @@ async def handleAudioMessage(conn: "ConnectionHandler", audio):
|
||||
if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done():
|
||||
conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn))
|
||||
return
|
||||
# 服务端AEC功能需要实时触发打断
|
||||
if conn.client_aec and have_voice:
|
||||
if conn.client_is_speaking and conn.client_listen_mode != "manual":
|
||||
await handleAbortMessage(conn)
|
||||
# 设备长时间空闲检测,用于say goodbye
|
||||
await no_voice_close_connect(conn, have_voice)
|
||||
# 接收音频
|
||||
await conn.asr.receive_audio(conn, audio, have_voice)
|
||||
await conn.asr.receive_audio(conn, pcm_frame, have_voice)
|
||||
|
||||
|
||||
async def resume_vad_detection(conn: "ConnectionHandler"):
|
||||
|
||||
@@ -50,33 +50,27 @@ async def report(conn: "ConnectionHandler", type, text, opus_data, report_time):
|
||||
conn.logger.bind(tag=TAG).error(f"聊天记录上报失败: {e}")
|
||||
|
||||
|
||||
def opus_to_wav(conn: "ConnectionHandler", opus_data):
|
||||
"""将Opus数据转换为WAV格式的字节流
|
||||
def opus_to_wav(conn: "ConnectionHandler", pcm_data):
|
||||
"""将PCM数据转换为WAV格式的字节流
|
||||
|
||||
Args:
|
||||
output_dir: 输出目录(保留参数以保持接口兼容)
|
||||
opus_data: opus音频数据
|
||||
pcm_data: PCM音频数据(可能是列表或bytes)
|
||||
|
||||
Returns:
|
||||
bytes: WAV格式的音频数据
|
||||
"""
|
||||
decoder = None
|
||||
try:
|
||||
decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道
|
||||
pcm_data = []
|
||||
# 处理可能是列表或bytes的PCM数据
|
||||
if isinstance(pcm_data, list):
|
||||
pcm_data_bytes = b"".join(pcm_data)
|
||||
else:
|
||||
pcm_data_bytes = pcm_data
|
||||
|
||||
for opus_packet in opus_data:
|
||||
try:
|
||||
pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms
|
||||
pcm_data.append(pcm_frame)
|
||||
except opuslib_next.OpusError as e:
|
||||
conn.logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True)
|
||||
|
||||
if not pcm_data:
|
||||
if not pcm_data_bytes:
|
||||
raise ValueError("没有有效的PCM数据")
|
||||
|
||||
# 创建WAV文件头
|
||||
pcm_data_bytes = b"".join(pcm_data)
|
||||
num_samples = len(pcm_data_bytes) // 2 # 16-bit samples
|
||||
|
||||
# WAV文件头
|
||||
@@ -97,12 +91,9 @@ def opus_to_wav(conn: "ConnectionHandler", opus_data):
|
||||
|
||||
# 返回完整的WAV数据
|
||||
return bytes(wav_header) + pcm_data_bytes
|
||||
finally:
|
||||
if decoder is not None:
|
||||
try:
|
||||
del decoder
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).debug(f"释放decoder资源时出错: {e}")
|
||||
conn.logger.bind(tag=TAG).error(f"PCM转WAV失败: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def enqueue_tts_report(conn: "ConnectionHandler", text, opus_data):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import time
|
||||
import asyncio
|
||||
import opuslib_next
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -81,13 +82,24 @@ async def _send_to_mqtt_gateway(
|
||||
conn: "ConnectionHandler", opus_packet, timestamp, sequence
|
||||
):
|
||||
"""
|
||||
发送带16字节头部的opus数据包给mqtt_gateway
|
||||
发送带16字节头部的opus数据包给mqtt_gateway,同时缓存音频用于AEC处理
|
||||
Args:
|
||||
conn: 连接对象
|
||||
opus_packet: opus数据包
|
||||
timestamp: 时间戳
|
||||
sequence: 序列号
|
||||
"""
|
||||
# 如果启用了服务端AEC,缓存PCM数据用于后续AEC处理
|
||||
if conn.client_aec and timestamp > 0:
|
||||
if not hasattr(conn, "aec_audio_cache"):
|
||||
conn.aec_audio_cache = {}
|
||||
conn.aec_audio_cache_time = {}
|
||||
conn._send_opus_decoder = opuslib_next.Decoder(16000, 1)
|
||||
# 解码opus为PCM后缓存
|
||||
pcm_data = conn._send_opus_decoder.decode(bytes(opus_packet), 960)
|
||||
conn.aec_audio_cache[timestamp] = bytes(pcm_data)
|
||||
conn.aec_audio_cache_time[timestamp] = time.time()
|
||||
|
||||
# 为opus数据包添加16字节头部
|
||||
header = bytearray(16)
|
||||
header[0] = 1 # type
|
||||
|
||||
@@ -213,7 +213,7 @@ class ASRProvider(ASRProviderBase):
|
||||
return None
|
||||
|
||||
async def speech_to_text(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
|
||||
self, opus_data: List[bytes], session_id: str, artifacts=None
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
if self._is_token_expired():
|
||||
|
||||
@@ -7,7 +7,6 @@ import hashlib
|
||||
import asyncio
|
||||
import requests
|
||||
import websockets
|
||||
import opuslib_next
|
||||
from urllib import parse
|
||||
from datetime import datetime
|
||||
from config.logger import setup_logging
|
||||
@@ -74,7 +73,6 @@ class ASRProvider(ASRProviderBase):
|
||||
self.interface_type = InterfaceType.STREAM
|
||||
self.config = config
|
||||
self.text = ""
|
||||
self.decoder = opuslib_next.Decoder(16000, 1)
|
||||
self.asr_ws = None
|
||||
self.forward_task = None
|
||||
self.is_processing = False
|
||||
@@ -129,9 +127,9 @@ class ASRProvider(ASRProviderBase):
|
||||
async def open_audio_channels(self, conn):
|
||||
await super().open_audio_channels(conn)
|
||||
|
||||
async def receive_audio(self, conn, audio, audio_have_voice):
|
||||
async def receive_audio(self, conn, pcm_frame, audio_have_voice):
|
||||
# 先调用父类方法处理基础逻辑
|
||||
await super().receive_audio(conn, audio, audio_have_voice)
|
||||
await super().receive_audio(conn, pcm_frame, audio_have_voice)
|
||||
|
||||
# 只在有声音且没有连接时建立连接(排除正在停止的情况)
|
||||
if audio_have_voice and not self.is_processing and not self.asr_ws:
|
||||
@@ -144,7 +142,6 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
if self.asr_ws and self.is_processing and self.server_ready:
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(audio, 960)
|
||||
await self.asr_ws.send(pcm_frame)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(f"发送音频失败: {str(e)}")
|
||||
@@ -232,10 +229,9 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 发送缓存音频
|
||||
if conn.asr_audio:
|
||||
for cached_audio in conn.asr_audio[-10:]:
|
||||
for cached_pcm in conn.asr_audio[-10:]:
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(cached_audio, 960)
|
||||
await self.asr_ws.send(pcm_frame)
|
||||
await self.asr_ws.send(cached_pcm)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(f"发送缓存音频失败: {e}")
|
||||
break
|
||||
@@ -328,7 +324,7 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
logger.bind(tag=TAG).debug("ASR会话清理完成")
|
||||
|
||||
async def speech_to_text(self, opus_data, session_id, audio_format, artifacts=None):
|
||||
async def speech_to_text(self, opus_data, session_id, artifacts=None):
|
||||
"""获取识别结果"""
|
||||
result = self.text
|
||||
self.text = ""
|
||||
@@ -337,10 +333,3 @@ class ASRProvider(ASRProviderBase):
|
||||
async def close(self):
|
||||
"""关闭资源"""
|
||||
await self._cleanup()
|
||||
if hasattr(self, 'decoder') and self.decoder is not None:
|
||||
try:
|
||||
del self.decoder
|
||||
self.decoder = None
|
||||
logger.bind(tag=TAG).debug("Aliyun decoder resources released")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).debug(f"释放Aliyun decoder资源时出错: {e}")
|
||||
|
||||
@@ -2,7 +2,6 @@ import json
|
||||
import uuid
|
||||
import asyncio
|
||||
import websockets
|
||||
import opuslib_next
|
||||
from typing import List, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -22,7 +21,6 @@ class ASRProvider(ASRProviderBase):
|
||||
self.interface_type = InterfaceType.STREAM
|
||||
self.config = config
|
||||
self.text = ""
|
||||
self.decoder = opuslib_next.Decoder(16000, 1)
|
||||
self.asr_ws = None
|
||||
self.forward_task = None
|
||||
self.is_processing = False
|
||||
@@ -55,9 +53,9 @@ class ASRProvider(ASRProviderBase):
|
||||
async def open_audio_channels(self, conn):
|
||||
await super().open_audio_channels(conn)
|
||||
|
||||
async def receive_audio(self, conn, audio, audio_have_voice):
|
||||
async def receive_audio(self, conn, pcm_frame, audio_have_voice):
|
||||
# 先调用父类方法处理基础逻辑
|
||||
await super().receive_audio(conn, audio, audio_have_voice)
|
||||
await super().receive_audio(conn, pcm_frame, audio_have_voice)
|
||||
|
||||
# 只在有声音且没有连接时建立连接
|
||||
if audio_have_voice and not self.is_processing and not self.asr_ws:
|
||||
@@ -71,8 +69,6 @@ class ASRProvider(ASRProviderBase):
|
||||
# 发送音频数据
|
||||
if self.asr_ws and self.is_processing and self.server_ready:
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(audio, 960)
|
||||
# 直接发送PCM音频数据(二进制)
|
||||
await self.asr_ws.send(pcm_frame)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(f"发送音频失败: {str(e)}")
|
||||
@@ -179,10 +175,9 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 发送缓存音频
|
||||
if conn.asr_audio:
|
||||
for cached_audio in conn.asr_audio[-10:]:
|
||||
for cached_pcm in conn.asr_audio[-10:]:
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(cached_audio, 960)
|
||||
await self.asr_ws.send(pcm_frame)
|
||||
await self.asr_ws.send(cached_pcm)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(f"发送缓存音频失败: {e}")
|
||||
break
|
||||
@@ -312,7 +307,7 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
logger.bind(tag=TAG).debug("ASR会话清理完成")
|
||||
|
||||
async def speech_to_text(self, opus_data, session_id, audio_format, artifacts=None):
|
||||
async def speech_to_text(self, opus_data, session_id, artifacts=None):
|
||||
"""获取识别结果"""
|
||||
result = self.text
|
||||
self.text = ""
|
||||
@@ -321,10 +316,3 @@ class ASRProvider(ASRProviderBase):
|
||||
async def close(self):
|
||||
"""关闭资源"""
|
||||
await self._cleanup()
|
||||
if hasattr(self, 'decoder') and self.decoder is not None:
|
||||
try:
|
||||
del self.decoder
|
||||
self.decoder = None
|
||||
logger.bind(tag=TAG).debug("Aliyun BL decoder resources released")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).debug(f"释放Aliyun BL decoder资源时出错: {e}")
|
||||
@@ -30,7 +30,7 @@ class ASRProvider(ASRProviderBase):
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
|
||||
async def speech_to_text(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
|
||||
self, opus_data: List[bytes], session_id: str, artifacts=None
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
if not opus_data:
|
||||
|
||||
@@ -10,7 +10,6 @@ import asyncio
|
||||
import tempfile
|
||||
import traceback
|
||||
import threading
|
||||
import opuslib_next
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from config.logger import setup_logging
|
||||
@@ -59,13 +58,13 @@ class ASRProviderBase(ABC):
|
||||
continue
|
||||
|
||||
# 接收音频
|
||||
async def receive_audio(self, conn: "ConnectionHandler", audio, audio_have_voice):
|
||||
async def receive_audio(self, conn: "ConnectionHandler", pcm_frame, audio_have_voice):
|
||||
if conn.client_listen_mode == "manual":
|
||||
# 手动模式:缓存音频用于ASR识别
|
||||
conn.asr_audio.append(audio)
|
||||
conn.asr_audio.append(pcm_frame)
|
||||
else:
|
||||
# 自动/实时模式:使用VAD检测
|
||||
conn.asr_audio.append(audio)
|
||||
conn.asr_audio.append(pcm_frame)
|
||||
|
||||
# 如果没有语音,且之前也没有声音,缓存部分音频
|
||||
if not audio_have_voice and not conn.client_have_voice:
|
||||
@@ -74,24 +73,21 @@ class ASRProviderBase(ABC):
|
||||
|
||||
# 自动模式下通过VAD检测到语音停止时触发识别
|
||||
if conn.asr.interface_type != InterfaceType.STREAM and conn.client_voice_stop:
|
||||
asr_audio_task = conn.asr_audio.copy()
|
||||
# 直接使用asr_audio中的PCM数据
|
||||
pcm_bytes = b"".join(conn.asr_audio)
|
||||
# 检查是否有足够的音频数据(每帧1920字节,15帧约28800字节)
|
||||
if len(pcm_bytes) > 1920 * 15:
|
||||
await self.handle_voice_stop(conn, [pcm_bytes])
|
||||
conn.reset_audio_states()
|
||||
|
||||
if len(asr_audio_task) > 15:
|
||||
await self.handle_voice_stop(conn, asr_audio_task)
|
||||
|
||||
# 处理语音停止
|
||||
async def handle_voice_stop(self, conn: "ConnectionHandler", asr_audio_task: List[bytes]):
|
||||
"""并行处理ASR和声纹识别"""
|
||||
try:
|
||||
total_start_time = time.monotonic()
|
||||
|
||||
# 准备音频数据
|
||||
if conn.audio_format == "pcm":
|
||||
# 数据已经是PCM直接使用
|
||||
pcm_data = asr_audio_task
|
||||
else:
|
||||
pcm_data = self.decode_opus(asr_audio_task)
|
||||
|
||||
combined_pcm_data = b"".join(pcm_data)
|
||||
|
||||
# 预先准备WAV数据
|
||||
@@ -101,7 +97,7 @@ class ASRProviderBase(ABC):
|
||||
|
||||
# 定义ASR任务
|
||||
asr_task = self.speech_to_text_wrapper(
|
||||
asr_audio_task, conn.session_id, conn.audio_format
|
||||
asr_audio_task, conn.session_id
|
||||
)
|
||||
|
||||
if conn.voiceprint_provider and wav_data:
|
||||
@@ -270,15 +266,11 @@ class ASRProviderBase(ABC):
|
||||
return file_path
|
||||
|
||||
async def speech_to_text_wrapper(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus"
|
||||
self, pcm_data: List[bytes], session_id: str
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
file_path = None
|
||||
temp_path = None
|
||||
try:
|
||||
if audio_format == "pcm":
|
||||
pcm_data = opus_data
|
||||
else:
|
||||
pcm_data = self.decode_opus(opus_data)
|
||||
combined_pcm_data = b"".join(pcm_data)
|
||||
|
||||
free_space = shutil.disk_usage(self.output_dir).free
|
||||
@@ -304,7 +296,7 @@ class ASRProviderBase(ABC):
|
||||
)
|
||||
|
||||
text, _ = await self.speech_to_text(
|
||||
opus_data, session_id, audio_format, artifacts
|
||||
pcm_data, session_id, artifacts
|
||||
)
|
||||
return text, file_path
|
||||
except OSError as e:
|
||||
@@ -332,50 +324,13 @@ class ASRProviderBase(ABC):
|
||||
self,
|
||||
opus_data: List[bytes],
|
||||
session_id: str,
|
||||
audio_format="opus",
|
||||
artifacts: Optional[AudioArtifacts] = None,
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本
|
||||
|
||||
:param opus_data: 输入的Opus音频数据
|
||||
:param session_id: 会话ID
|
||||
:param audio_format: 音频格式,默认"opus"
|
||||
:param artifacts: 音频工件,包含PCM数据、文件路径等
|
||||
:return: 识别结果文本和文件路径(如果有)
|
||||
"""
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def decode_opus(opus_data: List[bytes]) -> List[bytes]:
|
||||
"""将Opus音频数据解码为PCM数据"""
|
||||
decoder = None
|
||||
try:
|
||||
decoder = opuslib_next.Decoder(16000, 1)
|
||||
pcm_data = []
|
||||
buffer_size = 960 # 每次处理960个采样点 (60ms at 16kHz)
|
||||
|
||||
for i, opus_packet in enumerate(opus_data):
|
||||
try:
|
||||
if not opus_packet or len(opus_packet) == 0:
|
||||
continue
|
||||
|
||||
pcm_frame = decoder.decode(opus_packet, buffer_size)
|
||||
if pcm_frame and len(pcm_frame) > 0:
|
||||
pcm_data.append(pcm_frame)
|
||||
|
||||
except opuslib_next.OpusError as e:
|
||||
logger.bind(tag=TAG).warning(f"Opus解码错误,跳过数据包 {i}: {e}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"音频处理错误,数据包 {i}: {e}")
|
||||
|
||||
return pcm_data
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"音频解码过程发生错误: {e}")
|
||||
return []
|
||||
finally:
|
||||
if decoder is not None:
|
||||
try:
|
||||
del decoder
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).debug(f"释放decoder资源时出错: {e}")
|
||||
|
||||
@@ -232,7 +232,7 @@ class ASRProvider(ASRProviderBase):
|
||||
yield data[offset:data_len], True
|
||||
|
||||
async def speech_to_text(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
|
||||
self, opus_data: List[bytes], session_id: str, artifacts=None
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import gzip
|
||||
import uuid
|
||||
import asyncio
|
||||
import websockets
|
||||
import opuslib_next
|
||||
from core.providers.asr.base import ASRProviderBase
|
||||
from config.logger import setup_logging
|
||||
from core.providers.asr.dto.dto import InterfaceType
|
||||
@@ -22,7 +21,6 @@ class ASRProvider(ASRProviderBase):
|
||||
self.interface_type = InterfaceType.STREAM
|
||||
self.config = config
|
||||
self.text = ""
|
||||
self.decoder = opuslib_next.Decoder(16000, 1)
|
||||
self.asr_ws = None
|
||||
self.forward_task = None
|
||||
self.is_processing = False # 添加处理状态标志
|
||||
@@ -68,9 +66,9 @@ class ASRProvider(ASRProviderBase):
|
||||
async def open_audio_channels(self, conn):
|
||||
await super().open_audio_channels(conn)
|
||||
|
||||
async def receive_audio(self, conn: "ConnectionHandler", audio, audio_have_voice):
|
||||
async def receive_audio(self, conn: "ConnectionHandler", pcm_frame, audio_have_voice):
|
||||
# 先调用父类方法处理基础逻辑
|
||||
await super().receive_audio(conn, audio, audio_have_voice)
|
||||
await super().receive_audio(conn, pcm_frame, audio_have_voice)
|
||||
|
||||
# 如果本次有声音,且之前没有建立连接
|
||||
if audio_have_voice and self.asr_ws is None and not self.is_processing:
|
||||
@@ -123,10 +121,9 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 发送缓存的音频数据
|
||||
if conn.asr_audio and len(conn.asr_audio) > 0:
|
||||
for cached_audio in conn.asr_audio[-10:]:
|
||||
for cached_pcm in conn.asr_audio[-10:]:
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(cached_audio, 960)
|
||||
payload = gzip.compress(pcm_frame)
|
||||
payload = gzip.compress(cached_pcm)
|
||||
audio_request = bytearray(
|
||||
self.generate_audio_default_header()
|
||||
)
|
||||
@@ -151,7 +148,6 @@ class ASRProvider(ASRProviderBase):
|
||||
# 发送当前音频数据
|
||||
if self.asr_ws and self.is_processing and not self._is_stopping:
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(audio, 960)
|
||||
payload = gzip.compress(pcm_frame)
|
||||
audio_request = bytearray(self.generate_audio_default_header())
|
||||
audio_request.extend(len(payload).to_bytes(4, "big"))
|
||||
@@ -414,7 +410,7 @@ class ASRProvider(ASRProviderBase):
|
||||
logger.bind(tag=TAG).error(f"原始响应数据: {res.hex()}")
|
||||
raise
|
||||
|
||||
async def speech_to_text(self, opus_data, session_id, audio_format, artifacts=None):
|
||||
async def speech_to_text(self, opus_data, session_id, artifacts=None):
|
||||
result = self.text
|
||||
self.text = "" # 清空text
|
||||
return result, None
|
||||
@@ -432,12 +428,3 @@ class ASRProvider(ASRProviderBase):
|
||||
pass
|
||||
self.forward_task = None
|
||||
self.is_processing = False
|
||||
|
||||
# 显式释放decoder资源
|
||||
if hasattr(self, "decoder") and self.decoder is not None:
|
||||
try:
|
||||
del self.decoder
|
||||
self.decoder = None
|
||||
logger.bind(tag=TAG).debug("Doubao decoder resources released")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).debug(f"释放Doubao decoder资源时出错: {e}")
|
||||
|
||||
@@ -43,7 +43,11 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 内存检测,要求大于2G
|
||||
min_mem_bytes = 2 * 1024 * 1024 * 1024
|
||||
try:
|
||||
total_mem = psutil.virtual_memory().total
|
||||
except RuntimeError as e:
|
||||
logger.bind(tag=TAG).warning(f"获取系统内存信息失败,跳过FunASR内存检测: {e}")
|
||||
else:
|
||||
if total_mem < min_mem_bytes:
|
||||
logger.bind(tag=TAG).error(f"可用内存不足2G,当前仅有 {total_mem / (1024*1024):.2f} MB,可能无法启动FunASR")
|
||||
|
||||
@@ -65,7 +69,7 @@ class ASRProvider(ASRProviderBase):
|
||||
)
|
||||
|
||||
async def speech_to_text(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
|
||||
self, opus_data: List[bytes], session_id: str, artifacts=None
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""语音转文本主处理逻辑"""
|
||||
retry_count = 0
|
||||
|
||||
@@ -101,7 +101,7 @@ class ASRProvider(ASRProviderBase):
|
||||
logger.bind(tag=TAG).debug(f"Sent end message: {end_message}")
|
||||
|
||||
async def speech_to_text(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
|
||||
self, opus_data: List[bytes], session_id: str, artifacts=None
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""
|
||||
Convert speech data to text using FunASR.
|
||||
|
||||
@@ -24,7 +24,7 @@ class ASRProvider(ASRProviderBase):
|
||||
def requires_file(self) -> bool:
|
||||
return True
|
||||
|
||||
async def speech_to_text(self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None) -> Tuple[Optional[str], Optional[str]]:
|
||||
async def speech_to_text(self, opus_data: List[bytes], session_id: str, artifacts=None) -> Tuple[Optional[str], Optional[str]]:
|
||||
file_path = None
|
||||
try:
|
||||
if artifacts is None:
|
||||
|
||||
@@ -41,7 +41,7 @@ class ASRProvider(ASRProviderBase):
|
||||
return True
|
||||
|
||||
async def speech_to_text(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
|
||||
self, opus_data: List[bytes], session_id: str, artifacts=None
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
temp_file_path = None
|
||||
|
||||
@@ -124,7 +124,7 @@ class ASRProvider(ASRProviderBase):
|
||||
return True
|
||||
|
||||
async def speech_to_text(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
|
||||
self, opus_data: List[bytes], session_id: str, artifacts=None
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""语音转文本主处理逻辑"""
|
||||
file_path = None
|
||||
|
||||
@@ -32,7 +32,7 @@ class ASRProvider(ASRProviderBase):
|
||||
os.makedirs(self.output_dir, exist_ok=True)
|
||||
|
||||
async def speech_to_text(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
|
||||
self, opus_data: List[bytes], session_id: str, artifacts=None
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
if not opus_data:
|
||||
|
||||
@@ -44,7 +44,7 @@ class ASRProvider(ASRProviderBase):
|
||||
raise
|
||||
|
||||
async def speech_to_text(
|
||||
self, opus_data: List[bytes], session_id: str, audio_format="opus", artifacts=None
|
||||
self, opus_data: List[bytes], session_id: str, artifacts=None
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,6 @@ import base64
|
||||
import hashlib
|
||||
import asyncio
|
||||
import websockets
|
||||
import opuslib_next
|
||||
import gc
|
||||
from time import mktime
|
||||
from datetime import datetime
|
||||
@@ -33,7 +32,6 @@ class ASRProvider(ASRProviderBase):
|
||||
self.interface_type = InterfaceType.STREAM
|
||||
self.config = config
|
||||
self.text = ""
|
||||
self.decoder = opuslib_next.Decoder(16000, 1)
|
||||
self.asr_ws = None
|
||||
self.forward_task = None
|
||||
self.is_processing = False
|
||||
@@ -100,9 +98,9 @@ class ASRProvider(ASRProviderBase):
|
||||
async def open_audio_channels(self, conn: "ConnectionHandler"):
|
||||
await super().open_audio_channels(conn)
|
||||
|
||||
async def receive_audio(self, conn: "ConnectionHandler", audio, audio_have_voice):
|
||||
async def receive_audio(self, conn: "ConnectionHandler", pcm_frame, audio_have_voice):
|
||||
# 先调用父类方法处理基础逻辑
|
||||
await super().receive_audio(conn, audio, audio_have_voice)
|
||||
await super().receive_audio(conn, pcm_frame, audio_have_voice)
|
||||
|
||||
# 如果本次有声音,且之前没有建立连接
|
||||
if audio_have_voice and self.asr_ws is None and not self.is_processing:
|
||||
@@ -116,7 +114,6 @@ class ASRProvider(ASRProviderBase):
|
||||
# 发送当前音频数据
|
||||
if self.asr_ws and self.is_processing and self.server_ready:
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(audio, 960)
|
||||
await self._send_audio_frame(pcm_frame, STATUS_CONTINUE_FRAME)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(f"发送音频数据时发生错误: {e}")
|
||||
@@ -148,19 +145,15 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 发送首帧音频
|
||||
if conn.asr_audio and len(conn.asr_audio) > 0:
|
||||
first_audio = conn.asr_audio[-1] if conn.asr_audio else b""
|
||||
pcm_frame = (
|
||||
self.decoder.decode(first_audio, 960) if first_audio else b""
|
||||
)
|
||||
await self._send_audio_frame(pcm_frame, STATUS_FIRST_FRAME)
|
||||
first_pcm = conn.asr_audio[-1] if conn.asr_audio else b""
|
||||
await self._send_audio_frame(first_pcm, STATUS_FIRST_FRAME)
|
||||
self.server_ready = True
|
||||
logger.bind(tag=TAG).info("已发送首帧,开始识别")
|
||||
|
||||
# 发送缓存的音频数据
|
||||
for cached_audio in conn.asr_audio[-10:]:
|
||||
for cached_pcm in conn.asr_audio[-10:]:
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(cached_audio, 960)
|
||||
await self._send_audio_frame(pcm_frame, STATUS_CONTINUE_FRAME)
|
||||
await self._send_audio_frame(cached_pcm, STATUS_CONTINUE_FRAME)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).info(f"发送缓存音频数据时发生错误: {e}")
|
||||
break
|
||||
@@ -322,7 +315,7 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
logger.bind(tag=TAG).debug("ASR会话清理完成")
|
||||
|
||||
async def speech_to_text(self, opus_data, session_id, audio_format, artifacts=None):
|
||||
async def speech_to_text(self, opus_data, session_id, artifacts=None):
|
||||
"""获取识别结果"""
|
||||
result = self.text
|
||||
self.text = ""
|
||||
@@ -342,12 +335,3 @@ class ASRProvider(ASRProviderBase):
|
||||
self.forward_task = None
|
||||
self.is_processing = False
|
||||
|
||||
# 显式释放decoder资源
|
||||
if hasattr(self, "decoder") and self.decoder is not None:
|
||||
try:
|
||||
del self.decoder
|
||||
self.decoder = None
|
||||
logger.bind(tag=TAG).debug("Xunfei decoder resources released")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).debug(f"释放Xunfei decoder资源时出错: {e}")
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
import asyncio
|
||||
import traceback
|
||||
|
||||
from ..base import MemoryProviderBase, logger
|
||||
@@ -59,7 +60,9 @@ class MemoryProvider(MemoryProviderBase):
|
||||
|
||||
messages.append({"role": message.role, "content": content})
|
||||
|
||||
result = self.client.add(messages, user_id=self.role_id)
|
||||
result = await asyncio.to_thread(
|
||||
self.client.add, messages, user_id=self.role_id
|
||||
)
|
||||
logger.bind(tag=TAG).debug(f"Save memory result: {result}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"保存记忆失败: {str(e)}")
|
||||
@@ -84,7 +87,9 @@ class MemoryProvider(MemoryProviderBase):
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
pass
|
||||
|
||||
results = self.client.search(search_query, filters=filters)
|
||||
results = await asyncio.to_thread(
|
||||
self.client.search, search_query, filters=filters
|
||||
)
|
||||
if not results or "results" not in results:
|
||||
return ""
|
||||
|
||||
|
||||
@@ -186,13 +186,19 @@ class MemoryProvider(MemoryProviderBase):
|
||||
messages.append({"role": message.role, "content": content})
|
||||
|
||||
# Add memory using PowerMem SDK
|
||||
result = self.memory_client.add(
|
||||
if self.enable_user_profile:
|
||||
# UserMemory uses sync add
|
||||
result = await asyncio.to_thread(
|
||||
self.memory_client.add,
|
||||
messages=messages,
|
||||
user_id=self.role_id
|
||||
)
|
||||
else:
|
||||
# AsyncMemory uses async add
|
||||
result = await self.memory_client.add(
|
||||
messages=messages,
|
||||
user_id=self.role_id
|
||||
)
|
||||
# Handle both sync and async returns
|
||||
if asyncio.iscoroutine(result):
|
||||
result = await result
|
||||
|
||||
logger.bind(tag=TAG).debug(f"Save memory result: {result}")
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""服务端插件工具执行器"""
|
||||
|
||||
import asyncio
|
||||
from typing import Dict, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -41,6 +42,10 @@ class ServerPluginExecutor(ToolExecutor):
|
||||
# 默认不传conn参数
|
||||
result = func_item.func(**arguments)
|
||||
|
||||
# 兼容 async def 工具函数
|
||||
if asyncio.iscoroutine(result):
|
||||
result = await result
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import time
|
||||
import os
|
||||
import numpy as np
|
||||
import opuslib_next
|
||||
import onnxruntime
|
||||
from config.logger import setup_logging
|
||||
from core.providers.vad.base import VADProviderBase
|
||||
@@ -39,8 +38,6 @@ class VADProvider(VADProviderBase):
|
||||
|
||||
def _init_connection_state(self, conn):
|
||||
"""为连接初始化独立的 VAD 状态"""
|
||||
if not hasattr(conn, "_vad_opus_decoder"):
|
||||
conn._vad_opus_decoder = opuslib_next.Decoder(16000, 1)
|
||||
if not hasattr(conn, "_vad_state"):
|
||||
conn._vad_state = np.zeros((2, 1, 128), dtype=np.float32)
|
||||
if not hasattr(conn, "_vad_context"):
|
||||
@@ -48,14 +45,14 @@ class VADProvider(VADProviderBase):
|
||||
|
||||
def release_conn_resources(self, conn):
|
||||
"""释放连接的 VAD 资源(连接关闭时调用)"""
|
||||
for attr in ("_vad_opus_decoder", "_vad_state", "_vad_context"):
|
||||
for attr in ("_vad_state", "_vad_context"):
|
||||
if hasattr(conn, attr):
|
||||
try:
|
||||
delattr(conn, attr)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def is_vad(self, conn, opus_packet):
|
||||
def is_vad(self, conn, pcm_frame):
|
||||
# 手动模式:直接返回True,不进行实时VAD检测,所有音频都缓存
|
||||
if conn.client_listen_mode == "manual":
|
||||
return True
|
||||
@@ -63,7 +60,7 @@ class VADProvider(VADProviderBase):
|
||||
try:
|
||||
self._init_connection_state(conn)
|
||||
|
||||
pcm_frame = conn._vad_opus_decoder.decode(opus_packet, 960)
|
||||
# pcm_frame已经是处理后的PCM数据
|
||||
conn.client_audio_buffer.extend(pcm_frame)
|
||||
|
||||
client_have_voice = False
|
||||
@@ -115,7 +112,5 @@ class VADProvider(VADProviderBase):
|
||||
conn.vad_last_voice_time = time.time() * 1000
|
||||
|
||||
return client_have_voice
|
||||
except opuslib_next.OpusError as e:
|
||||
logger.bind(tag=TAG).info(f"解码错误: {e}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error processing audio packet: {e}")
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
import threading
|
||||
from typing import Dict, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -169,12 +171,33 @@ class PromptManager:
|
||||
if cached_weather is not None:
|
||||
return cached_weather
|
||||
|
||||
# 缓存未命中,调用get_weather函数获取
|
||||
# 缓存未命中,调用 async get_weather 函数
|
||||
# Windows ProactorEventLoop 不支持 run_coroutine_threadsafe().result()
|
||||
# 因此用 call_soon_threadsafe 提交任务 + threading.Event 等待结果
|
||||
# 注意:Event.wait() 只阻塞当前线程池线程,不阻塞主事件循环
|
||||
from plugins_func.functions.get_weather import get_weather
|
||||
from plugins_func.register import ActionResponse
|
||||
|
||||
# 调用get_weather函数
|
||||
result = get_weather(conn, location=location, lang="zh_CN")
|
||||
result_holder = []
|
||||
exception_holder = []
|
||||
|
||||
async def _call():
|
||||
try:
|
||||
result_holder.append(
|
||||
await get_weather(conn, location=location, lang="zh_CN")
|
||||
)
|
||||
except Exception as e:
|
||||
exception_holder.append(e)
|
||||
finally:
|
||||
event.set()
|
||||
|
||||
event = threading.Event()
|
||||
conn.loop.call_soon_threadsafe(lambda: asyncio.ensure_future(_call()))
|
||||
if not event.wait(timeout=10):
|
||||
raise TimeoutError("获取天气信息超时")
|
||||
if exception_holder:
|
||||
raise exception_holder[0]
|
||||
result = result_holder[0]
|
||||
if isinstance(result, ActionResponse):
|
||||
weather_report = result.result
|
||||
self.cache_manager.set(self.CacheType.WEATHER, location, weather_report)
|
||||
|
||||
@@ -42,7 +42,7 @@ TAG = __name__
|
||||
class WebSocketServer:
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self.logger = setup_logging()
|
||||
self.logger = setup_logging(config)
|
||||
self.config_lock = asyncio.Lock()
|
||||
modules = initialize_modules(
|
||||
self.logger,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""呼叫设备工具"""
|
||||
import requests
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.connection import ConnectionHandler
|
||||
@@ -35,8 +34,9 @@ call_device_function_desc = {
|
||||
}
|
||||
|
||||
|
||||
def _request_api(url: str, params: dict, headers: dict) -> requests.Response:
|
||||
return requests.get(url, params=params, headers=headers, timeout=10)
|
||||
async def _request_api(url: str, params: dict, headers: dict):
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=3.0)) as client:
|
||||
return await client.get(url, params=params, headers=headers)
|
||||
|
||||
|
||||
def _failed_reply(msg: str) -> ActionResponse:
|
||||
@@ -49,7 +49,7 @@ def _is_answering(conn: "ConnectionHandler") -> bool:
|
||||
|
||||
|
||||
@register_function("call_device", call_device_function_desc, ToolType.SYSTEM_CTL)
|
||||
def call_device(conn: "ConnectionHandler", nickname: str):
|
||||
async def call_device(conn: "ConnectionHandler", nickname: str):
|
||||
caller_mac = conn.headers.get("device-id")
|
||||
if not caller_mac:
|
||||
return _failed_reply("无法获取本机MAC地址")
|
||||
@@ -71,13 +71,13 @@ def call_device(conn: "ConnectionHandler", nickname: str):
|
||||
|
||||
# 查询通讯录并发起呼叫
|
||||
try:
|
||||
resp = _request_api(
|
||||
resp = await _request_api(
|
||||
f"{api_url}/device/address-book/call",
|
||||
params=params,
|
||||
headers=headers,
|
||||
)
|
||||
result = resp.json()
|
||||
except requests.RequestException as e:
|
||||
except httpx.HTTPError as e:
|
||||
logger.bind(tag=TAG).error(f"呼叫请求失败: {e}")
|
||||
return _failed_reply("呼叫失败,请稍后再试")
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import random
|
||||
import requests
|
||||
import httpx
|
||||
import xml.etree.ElementTree as ET
|
||||
from bs4 import BeautifulSoup
|
||||
from config.logger import setup_logging
|
||||
@@ -44,11 +44,11 @@ GET_NEWS_FROM_CHINANEWS_FUNCTION_DESC = {
|
||||
}
|
||||
|
||||
|
||||
def fetch_news_from_rss(rss_url):
|
||||
async def fetch_news_from_rss(rss_url):
|
||||
"""从RSS源获取新闻列表"""
|
||||
try:
|
||||
response = requests.get(rss_url)
|
||||
response.raise_for_status()
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, connect=3.0)) as client:
|
||||
response = await client.get(rss_url)
|
||||
|
||||
# 解析XML
|
||||
root = ET.fromstring(response.content)
|
||||
@@ -86,11 +86,11 @@ def fetch_news_from_rss(rss_url):
|
||||
return []
|
||||
|
||||
|
||||
def fetch_news_detail(url):
|
||||
async def fetch_news_detail(url):
|
||||
"""获取新闻详情页内容并总结"""
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=3.0)) as client:
|
||||
response = await client.get(url)
|
||||
|
||||
soup = BeautifulSoup(response.content, "html.parser")
|
||||
|
||||
@@ -148,7 +148,7 @@ def map_category(category_text):
|
||||
GET_NEWS_FROM_CHINANEWS_FUNCTION_DESC,
|
||||
ToolType.SYSTEM_CTL,
|
||||
)
|
||||
def get_news_from_chinanews(
|
||||
async def get_news_from_chinanews(
|
||||
conn: "ConnectionHandler",
|
||||
category: str = None,
|
||||
detail: bool = False,
|
||||
@@ -180,7 +180,7 @@ def get_news_from_chinanews(
|
||||
logger.bind(tag=TAG).debug(f"获取新闻详情: {title}, URL={link}")
|
||||
|
||||
# 获取新闻详情
|
||||
detail_content = fetch_news_detail(link)
|
||||
detail_content = await fetch_news_detail(link)
|
||||
|
||||
if not detail_content or detail_content == "无法获取详细内容":
|
||||
return ActionResponse(
|
||||
@@ -220,7 +220,7 @@ def get_news_from_chinanews(
|
||||
)
|
||||
|
||||
# 获取新闻列表
|
||||
news_items = fetch_news_from_rss(rss_url)
|
||||
news_items = await fetch_news_from_rss(rss_url)
|
||||
|
||||
if not news_items:
|
||||
return ActionResponse(
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import random
|
||||
import requests
|
||||
import json
|
||||
import httpx
|
||||
from markitdown import MarkItDown
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from markitdown import MarkItDown
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -110,7 +109,7 @@ GET_NEWS_FROM_NEWSNOW_FUNCTION_DESC = {
|
||||
}
|
||||
|
||||
|
||||
def fetch_news_from_api(conn: "ConnectionHandler", source="thepaper"):
|
||||
async def fetch_news_from_api(conn: "ConnectionHandler", source="thepaper"):
|
||||
"""从API获取新闻列表"""
|
||||
try:
|
||||
api_url = f"https://newsnow.busiyi.world/api/s?id={source}"
|
||||
@@ -120,8 +119,8 @@ def fetch_news_from_api(conn: "ConnectionHandler", source="thepaper"):
|
||||
api_url = news_config["url"] + source
|
||||
|
||||
headers = {"User-Agent": "Mozilla/5.0"}
|
||||
response = requests.get(api_url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=3.0)) as client:
|
||||
response = await client.get(api_url, headers=headers)
|
||||
|
||||
data = response.json()
|
||||
|
||||
@@ -136,12 +135,12 @@ def fetch_news_from_api(conn: "ConnectionHandler", source="thepaper"):
|
||||
return []
|
||||
|
||||
|
||||
def fetch_news_detail(url):
|
||||
async def fetch_news_detail(url):
|
||||
"""获取新闻详情页内容并使用MarkItDown清理HTML"""
|
||||
try:
|
||||
headers = {"User-Agent": "Mozilla/5.0"}
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=3.0)) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
|
||||
# 使用MarkItDown清理HTML内容
|
||||
md = MarkItDown(enable_plugins=False)
|
||||
@@ -166,7 +165,7 @@ def fetch_news_detail(url):
|
||||
GET_NEWS_FROM_NEWSNOW_FUNCTION_DESC,
|
||||
ToolType.SYSTEM_CTL,
|
||||
)
|
||||
def get_news_from_newsnow(
|
||||
async def get_news_from_newsnow(
|
||||
conn: "ConnectionHandler",
|
||||
source: str = "澎湃新闻",
|
||||
detail: bool = False,
|
||||
@@ -206,7 +205,7 @@ def get_news_from_newsnow(
|
||||
)
|
||||
|
||||
# 获取新闻详情
|
||||
detail_content = fetch_news_detail(url)
|
||||
detail_content = await fetch_news_detail(url)
|
||||
|
||||
if not detail_content or detail_content == "无法获取详细内容":
|
||||
return ActionResponse(
|
||||
@@ -248,7 +247,7 @@ def get_news_from_newsnow(
|
||||
logger.bind(tag=TAG).info(f"获取新闻: 新闻源={source}({english_source_id})")
|
||||
|
||||
# 获取新闻列表
|
||||
news_items = fetch_news_from_api(conn, english_source_id)
|
||||
news_items = await fetch_news_from_api(conn, english_source_id)
|
||||
|
||||
if not news_items:
|
||||
return ActionResponse(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import requests
|
||||
import httpx
|
||||
from bs4 import BeautifulSoup
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
@@ -111,20 +111,23 @@ WEATHER_CODE_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def fetch_city_info(location, api_key, api_host):
|
||||
async def fetch_city_info(location, api_key, api_host):
|
||||
url = f"https://{api_host}/geo/v2/city/lookup?key={api_key}&location={location}&lang=zh"
|
||||
response = requests.get(url, headers=HEADERS).json()
|
||||
if response.get("error") is not None:
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, connect=3.0)) as client:
|
||||
response = await client.get(url, headers=HEADERS)
|
||||
data = response.json()
|
||||
if data.get("error") is not None:
|
||||
logger.bind(tag=TAG).error(
|
||||
f"获取天气失败,原因:{response.get('error', {}).get('detail')}"
|
||||
f"获取天气失败,原因:{data.get('error', {}).get('detail')}"
|
||||
)
|
||||
return None
|
||||
return response.get("location", [])[0] if response.get("location") else None
|
||||
return data.get("location", [])[0] if data.get("location") else None
|
||||
|
||||
|
||||
def fetch_weather_page(url):
|
||||
response = requests.get(url, headers=HEADERS)
|
||||
return BeautifulSoup(response.text, "html.parser") if response.ok else None
|
||||
async def fetch_weather_page(url):
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=3.0)) as client:
|
||||
response = await client.get(url, headers=HEADERS)
|
||||
return BeautifulSoup(response.text, "html.parser") if response.status_code == 200 else None
|
||||
|
||||
|
||||
def parse_weather_info(soup):
|
||||
@@ -159,7 +162,7 @@ def parse_weather_info(soup):
|
||||
|
||||
|
||||
@register_function("get_weather", GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
|
||||
def get_weather(conn: "ConnectionHandler", location: str = None, lang: str = "zh_CN"):
|
||||
async def get_weather(conn: "ConnectionHandler", location: str = None, lang: str = "zh_CN"):
|
||||
from core.utils.cache.manager import cache_manager, CacheType
|
||||
|
||||
weather_config = conn.config.get("plugins", {}).get("get_weather", {})
|
||||
@@ -195,12 +198,12 @@ def get_weather(conn: "ConnectionHandler", location: str = None, lang: str = "zh
|
||||
return ActionResponse(Action.REQLLM, cached_weather_report, None)
|
||||
|
||||
# 缓存未命中,获取实时天气数据
|
||||
city_info = fetch_city_info(location, api_key, api_host)
|
||||
city_info = await fetch_city_info(location, api_key, api_host)
|
||||
if not city_info:
|
||||
return ActionResponse(
|
||||
Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None
|
||||
)
|
||||
soup = fetch_weather_page(city_info["fxLink"])
|
||||
soup = await fetch_weather_page(city_info["fxLink"])
|
||||
if not soup:
|
||||
return ActionResponse(Action.REQLLM, None, "请求失败")
|
||||
city_name, current_abstract, current_basic, temps_list = parse_weather_info(soup)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||
import httpx
|
||||
from config.logger import setup_logging
|
||||
import asyncio
|
||||
import requests
|
||||
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -31,11 +30,11 @@ hass_get_state_function_desc = {
|
||||
|
||||
|
||||
@register_function("hass_get_state", hass_get_state_function_desc, ToolType.SYSTEM_CTL)
|
||||
def hass_get_state(conn: "ConnectionHandler", entity_id=""):
|
||||
async def hass_get_state(conn: "ConnectionHandler", entity_id=""):
|
||||
try:
|
||||
ha_response = handle_hass_get_state(conn, entity_id)
|
||||
ha_response = await handle_hass_get_state(conn, entity_id)
|
||||
return ActionResponse(Action.REQLLM, ha_response, None)
|
||||
except asyncio.TimeoutError:
|
||||
except httpx.TimeoutException:
|
||||
logger.bind(tag=TAG).error("获取Home Assistant状态超时")
|
||||
return ActionResponse(Action.ERROR, "请求超时", None)
|
||||
except Exception as e:
|
||||
@@ -44,13 +43,16 @@ def hass_get_state(conn: "ConnectionHandler", entity_id=""):
|
||||
return ActionResponse(Action.ERROR, error_msg, None)
|
||||
|
||||
|
||||
def handle_hass_get_state(conn: "ConnectionHandler", entity_id):
|
||||
async def handle_hass_get_state(conn: "ConnectionHandler", entity_id):
|
||||
ha_config = initialize_hass_handler(conn)
|
||||
api_key = ha_config.get("api_key")
|
||||
base_url = ha_config.get("base_url")
|
||||
url = f"{base_url}/api/states/{entity_id}"
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
response = requests.get(url, headers=headers, timeout=5)
|
||||
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, connect=3.0)) as client:
|
||||
response = await client.get(url, headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
responsetext = "设备状态:" + response.json()["state"] + " "
|
||||
logger.bind(tag=TAG).info(f"api返回内容: {response.json()}")
|
||||
@@ -92,8 +94,5 @@ def handle_hass_get_state(conn: "ConnectionHandler", entity_id):
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"查询返回内容: {responsetext}")
|
||||
return responsetext
|
||||
# return response.json()['attributes']
|
||||
# response.attributes
|
||||
|
||||
else:
|
||||
return f"切换失败,错误码: {response.status_code}"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||
import httpx
|
||||
from config.logger import setup_logging
|
||||
import asyncio
|
||||
import requests
|
||||
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -37,18 +36,17 @@ hass_play_music_function_desc = {
|
||||
@register_function(
|
||||
"hass_play_music", hass_play_music_function_desc, ToolType.SYSTEM_CTL
|
||||
)
|
||||
def hass_play_music(conn: "ConnectionHandler", entity_id="", media_content_id="random"):
|
||||
async def hass_play_music(conn: "ConnectionHandler", entity_id="", media_content_id="random"):
|
||||
try:
|
||||
# 执行音乐播放命令
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
handle_hass_play_music(conn, entity_id, media_content_id), conn.loop
|
||||
)
|
||||
ha_response = future.result()
|
||||
result = await handle_hass_play_music(conn, entity_id, media_content_id)
|
||||
return ActionResponse(
|
||||
action=Action.RESPONSE, result="退出意图已处理", response=ha_response
|
||||
action=Action.RECORD, result="指令已接收", response=result
|
||||
)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
|
||||
return ActionResponse(
|
||||
action=Action.RESPONSE, result=str(e), response="播放音乐时出错了"
|
||||
)
|
||||
|
||||
|
||||
async def handle_hass_play_music(
|
||||
@@ -60,7 +58,10 @@ async def handle_hass_play_music(
|
||||
url = f"{base_url}/api/services/music_assistant/play_media"
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
data = {"entity_id": entity_id, "media_id": media_content_id}
|
||||
response = requests.post(url, headers=headers, json=data)
|
||||
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0, connect=3.0)) as client:
|
||||
response = await client.post(url, headers=headers, json=data)
|
||||
|
||||
if response.status_code == 200:
|
||||
return f"正在播放{media_content_id}的音乐"
|
||||
else:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||
import httpx
|
||||
from config.logger import setup_logging
|
||||
import asyncio
|
||||
import requests
|
||||
from plugins_func.functions.hass_init import initialize_hass_handler
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -54,13 +53,13 @@ hass_set_state_function_desc = {
|
||||
|
||||
|
||||
@register_function("hass_set_state", hass_set_state_function_desc, ToolType.SYSTEM_CTL)
|
||||
def hass_set_state(conn: "ConnectionHandler", entity_id="", state=None):
|
||||
async def hass_set_state(conn: "ConnectionHandler", entity_id="", state=None):
|
||||
if state is None:
|
||||
state = {}
|
||||
try:
|
||||
ha_response = handle_hass_set_state(conn, entity_id, state)
|
||||
ha_response = await handle_hass_set_state(conn, entity_id, state)
|
||||
return ActionResponse(Action.REQLLM, ha_response, None)
|
||||
except asyncio.TimeoutError:
|
||||
except httpx.TimeoutException:
|
||||
logger.bind(tag=TAG).error("设置Home Assistant状态超时")
|
||||
return ActionResponse(Action.ERROR, "请求超时", None)
|
||||
except Exception as e:
|
||||
@@ -69,7 +68,7 @@ def hass_set_state(conn: "ConnectionHandler", entity_id="", state=None):
|
||||
return ActionResponse(Action.ERROR, error_msg, None)
|
||||
|
||||
|
||||
def handle_hass_set_state(conn: "ConnectionHandler", entity_id, state):
|
||||
async def handle_hass_set_state(conn: "ConnectionHandler", entity_id, state):
|
||||
ha_config = initialize_hass_handler(conn)
|
||||
api_key = ha_config.get("api_key")
|
||||
base_url = ha_config.get("base_url")
|
||||
@@ -169,7 +168,10 @@ def handle_hass_set_state(conn: "ConnectionHandler", entity_id, state):
|
||||
data = {"entity_id": entity_id, arg: value}
|
||||
url = f"{base_url}/api/services/{domain}/{action}"
|
||||
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
||||
response = requests.post(url, headers=headers, json=data, timeout=5) # 设置5秒超时
|
||||
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, connect=3.0)) as client:
|
||||
response = await client.post(url, headers=headers, json=data)
|
||||
|
||||
logger.bind(tag=TAG).info(
|
||||
f"设置状态:{description},url:{url},return_code:{response.status_code}"
|
||||
)
|
||||
|
||||
@@ -5,10 +5,8 @@ import random
|
||||
import difflib
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from core.utils.dialogue import Message
|
||||
from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType, ContentType
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -38,34 +36,12 @@ play_music_function_desc = {
|
||||
|
||||
|
||||
@register_function("play_music", play_music_function_desc, ToolType.SYSTEM_CTL)
|
||||
def play_music(conn: "ConnectionHandler", song_name: str):
|
||||
async def play_music(conn: "ConnectionHandler", song_name: str):
|
||||
try:
|
||||
music_intent = (
|
||||
f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐"
|
||||
)
|
||||
|
||||
# 检查事件循环状态
|
||||
if not conn.loop.is_running():
|
||||
conn.logger.bind(tag=TAG).error("事件循环未运行,无法提交任务")
|
||||
return ActionResponse(
|
||||
action=Action.RESPONSE, result="系统繁忙", response="请稍后再试"
|
||||
)
|
||||
|
||||
# 提交异步任务
|
||||
task = conn.loop.create_task(
|
||||
handle_music_command(conn, music_intent) # 封装异步逻辑
|
||||
)
|
||||
|
||||
# 非阻塞回调处理
|
||||
def handle_done(f):
|
||||
try:
|
||||
f.result() # 可在此处理成功逻辑
|
||||
conn.logger.bind(tag=TAG).info("播放完成")
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"播放失败: {e}")
|
||||
|
||||
task.add_done_callback(handle_done)
|
||||
|
||||
await handle_music_command(conn, music_intent)
|
||||
return ActionResponse(
|
||||
action=Action.RECORD, result="指令已接收", response="正在为您播放音乐"
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import requests
|
||||
import sys
|
||||
import json
|
||||
import httpx
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -28,7 +28,7 @@ SEARCH_FROM_RAGFLOW_FUNCTION_DESC = {
|
||||
@register_function(
|
||||
"search_from_ragflow", SEARCH_FROM_RAGFLOW_FUNCTION_DESC, ToolType.SYSTEM_CTL
|
||||
)
|
||||
def search_from_ragflow(conn: "ConnectionHandler", question=None):
|
||||
async def search_from_ragflow(conn: "ConnectionHandler", question=None):
|
||||
# 确保字符串参数正确处理编码
|
||||
if question and isinstance(question, str):
|
||||
# 确保问题参数是UTF-8编码的字符串
|
||||
@@ -49,13 +49,8 @@ def search_from_ragflow(conn: "ConnectionHandler", question=None):
|
||||
|
||||
try:
|
||||
# 使用ensure_ascii=False确保JSON序列化时正确处理中文
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
verify=False,
|
||||
)
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(5.0, connect=3.0), verify=False) as client:
|
||||
response = await client.post(url, json=payload, headers=headers)
|
||||
|
||||
# 显式设置响应的编码为utf-8
|
||||
response.encoding = "utf-8"
|
||||
@@ -64,7 +59,6 @@ def search_from_ragflow(conn: "ConnectionHandler", question=None):
|
||||
|
||||
# 先获取文本内容,然后手动处理JSON解码
|
||||
response_text = response.text
|
||||
import json
|
||||
|
||||
result = json.loads(response_text)
|
||||
|
||||
@@ -110,36 +104,16 @@ def search_from_ragflow(conn: "ConnectionHandler", question=None):
|
||||
context_text = "根据知识库查询结果,没有相关信息。"
|
||||
return ActionResponse(Action.REQLLM, context_text, None)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
# 网络请求异常
|
||||
error_type = type(e).__name__
|
||||
logger.bind(tag=TAG).error(
|
||||
f"RAGflow网络请求失败,异常类型:{error_type},详情:{str(e)}"
|
||||
)
|
||||
|
||||
# 根据异常类型提供更详细的错误信息和解决方案
|
||||
if isinstance(e, requests.exceptions.ConnectTimeout):
|
||||
error_response = "RAG接口连接超时(5秒)"
|
||||
error_response += "\n可能原因:RAGflow服务未启动或网络连接问题"
|
||||
error_response += "\n解决方案:请检查RAGflow服务状态和网络连接"
|
||||
|
||||
elif isinstance(e, requests.exceptions.ConnectionError):
|
||||
error_response = "无法连接到RAG接口"
|
||||
error_response += "\n可能原因:RAGflow服务地址错误或服务未运行"
|
||||
error_response += "\n解决方案:请检查RAGflow服务地址配置和服务状态"
|
||||
|
||||
elif isinstance(e, requests.exceptions.Timeout):
|
||||
except httpx.TimeoutException as e:
|
||||
error_response = "RAG接口请求超时"
|
||||
error_response += "\n可能原因:RAGflow服务响应缓慢或网络延迟"
|
||||
error_response += "\n解决方案:请稍后重试或检查RAGflow服务性能"
|
||||
return ActionResponse(Action.RESPONSE, None, error_response)
|
||||
|
||||
elif isinstance(e, requests.exceptions.HTTPError):
|
||||
# 处理HTTP错误状态码
|
||||
except httpx.HTTPStatusError as e:
|
||||
if hasattr(e.response, "status_code"):
|
||||
status_code = e.response.status_code
|
||||
error_response = f"RAG接口HTTP错误(状态码:{status_code})"
|
||||
|
||||
# 尝试获取响应内容中的错误信息
|
||||
try:
|
||||
error_detail = e.response.json().get("error", {}).get("message", "")
|
||||
if error_detail:
|
||||
@@ -148,10 +122,12 @@ def search_from_ragflow(conn: "ConnectionHandler", question=None):
|
||||
pass
|
||||
else:
|
||||
error_response = f"RAG接口HTTP异常:{str(e)}"
|
||||
return ActionResponse(Action.RESPONSE, None, error_response)
|
||||
|
||||
else:
|
||||
error_response = f"RAG接口网络异常({error_type}):{str(e)}"
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
error_response = "无法连接到RAG接口"
|
||||
error_response += "\n可能原因:RAGflow服务地址错误或服务未运行"
|
||||
error_response += "\n解决方案:请检查RAGflow服务地址配置和服务状态"
|
||||
return ActionResponse(Action.RESPONSE, None, error_response)
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import requests
|
||||
import httpx
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import (
|
||||
register_function,
|
||||
@@ -37,7 +37,7 @@ WEB_SEARCH_FUNCTION_DESC = {
|
||||
}
|
||||
|
||||
|
||||
def _search_metaso(api_key: str, query: str, max_results: int) -> str:
|
||||
async def _search_metaso(api_key: str, query: str, max_results: int) -> str:
|
||||
"""调用秘塔搜索API"""
|
||||
url = "https://metaso.cn/api/v1/search"
|
||||
headers = {
|
||||
@@ -54,8 +54,8 @@ def _search_metaso(api_key: str, query: str, max_results: int) -> str:
|
||||
"conciseSnippet": False,
|
||||
}
|
||||
logger.bind(tag=TAG).debug(f"秘塔搜索请求 | URL: {url} | payload: {payload}")
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=15)
|
||||
response.raise_for_status()
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(15.0, connect=3.0)) as client:
|
||||
response = await client.post(url, json=payload, headers=headers)
|
||||
data = response.json()
|
||||
logger.bind(tag=TAG).debug(f"秘塔搜索响应 | status: {response.status_code}")
|
||||
|
||||
@@ -77,7 +77,7 @@ def _search_metaso(api_key: str, query: str, max_results: int) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _search_tavily(api_key: str, query: str, max_results: int) -> str:
|
||||
async def _search_tavily(api_key: str, query: str, max_results: int) -> str:
|
||||
"""调用Tavily搜索API"""
|
||||
url = "https://api.tavily.com/search"
|
||||
headers = {
|
||||
@@ -91,8 +91,8 @@ def _search_tavily(api_key: str, query: str, max_results: int) -> str:
|
||||
"include_answer": "advanced",
|
||||
}
|
||||
logger.bind(tag=TAG).debug(f"Tavily搜索请求 | URL: {url} | payload: {payload}")
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=15)
|
||||
response.raise_for_status()
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(15.0, connect=3.0)) as client:
|
||||
response = await client.post(url, json=payload, headers=headers)
|
||||
data = response.json()
|
||||
logger.bind(tag=TAG).debug(f"Tavily搜索响应 | status: {response.status_code} | data: {data}")
|
||||
|
||||
@@ -113,7 +113,7 @@ def _search_tavily(api_key: str, query: str, max_results: int) -> str:
|
||||
|
||||
|
||||
@register_function("web_search", WEB_SEARCH_FUNCTION_DESC, ToolType.SYSTEM_CTL)
|
||||
def web_search(conn: "ConnectionHandler", query: str = None):
|
||||
async def web_search(conn: "ConnectionHandler", query: str = None):
|
||||
logger.bind(tag=TAG).info(f"web_search 被调用 | query={query}")
|
||||
if not query:
|
||||
return ActionResponse(Action.REQLLM, "请提供搜索关键词。", None)
|
||||
@@ -131,24 +131,22 @@ def web_search(conn: "ConnectionHandler", query: str = None):
|
||||
None,
|
||||
)
|
||||
|
||||
try:
|
||||
if provider == "metaso":
|
||||
search_fn = lambda: _search_metaso(api_key, query, max_results)
|
||||
result_text = await _search_metaso(api_key, query, max_results)
|
||||
elif provider == "tavily":
|
||||
search_fn = lambda: _search_tavily(api_key, query, max_results)
|
||||
result_text = await _search_tavily(api_key, query, max_results)
|
||||
else:
|
||||
return ActionResponse(
|
||||
Action.REQLLM,
|
||||
f"联网搜索功能未配置或配置的搜索源无效(当前:{provider}),请检查配置。",
|
||||
None,
|
||||
)
|
||||
|
||||
try:
|
||||
result_text = search_fn()
|
||||
logger.bind(tag=TAG).info(f"搜索结果组装完成:\n{result_text}")
|
||||
except requests.exceptions.Timeout:
|
||||
except httpx.TimeoutException:
|
||||
logger.bind(tag=TAG).error("联网搜索请求超时")
|
||||
result_text = "联网搜索请求超时,请稍后重试。"
|
||||
except requests.exceptions.RequestException as e:
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.bind(tag=TAG).error(f"联网搜索请求失败: {e}")
|
||||
result_text = "联网搜索请求失败,请稍后重试。"
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user