Merge pull request #1404 from CaixyPromise/main

feat: 智控台智能体级插件/工具调用改造-#1358
This commit is contained in:
hrz
2025-06-04 23:52:54 +08:00
committed by GitHub
24 changed files with 1058 additions and 231 deletions
@@ -0,0 +1,31 @@
package xiaozhi.common.utils;
/**
* 返回响应体工具类
*/
public class ResultUtils
{
public static <T> Result<T> success(T data) {
return new Result<T>().ok(data);
}
public static <T> Result<T> error() {
return new Result<T>().error();
}
public static <T> Result<T> error(String msg) {
return new Result<T>().error(msg);
}
public static <T> Result<T> error(int errorCode, String msg) {
return new Result<T>().error(errorCode, msg);
}
public static <T> Result<T> error(int errorCode) {
return new Result<T>().error(errorCode);
}
public static <T> Result<T> empty() {
return new Result<T>();
}
}
@@ -4,6 +4,7 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
@@ -34,7 +35,9 @@ import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
import xiaozhi.modules.agent.dto.AgentCreateDTO;
@@ -42,13 +45,14 @@ import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentChatAudioService;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.service.*;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.service.ModelProviderService;
import xiaozhi.modules.security.user.SecurityUser;
@Tag(name = "智能体管理")
@@ -88,9 +92,9 @@ public class AgentController {
@GetMapping("/{id}")
@Operation(summary = "获取智能体详情")
@RequiresPermissions("sys:role:normal")
public Result<AgentEntity> getAgentById(@PathVariable("id") String id) {
AgentEntity agent = agentService.getAgentById(id);
return new Result<AgentEntity>().ok(agent);
public Result<AgentInfoVO> getAgentById(@PathVariable("id") String id) {
AgentInfoVO agent = agentService.getAgentById(id);
return ResultUtils.success(agent);
}
@PostMapping
@@ -138,91 +142,19 @@ public class AgentController {
}
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
return updateAgentById(device.getAgentId(), agentUpdateDTO);
agentService.updateAgentById(device.getAgentId(), agentUpdateDTO);
return new Result<>();
}
@PutMapping("/{id}")
@Operation(summary = "更新智能体")
@RequiresPermissions("sys:role:normal")
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
return updateAgentById(id, dto);
}
private Result<Void> updateAgentById(String id, AgentUpdateDTO dto) {
// 先查询现有实体
AgentEntity existingEntity = agentService.getAgentById(id);
if (existingEntity == null) {
return new Result<Void>().error("智能体不存在");
}
// 只更新提供的非空字段
if (dto.getAgentName() != null) {
existingEntity.setAgentName(dto.getAgentName());
}
if (dto.getAgentCode() != null) {
existingEntity.setAgentCode(dto.getAgentCode());
}
if (dto.getAsrModelId() != null) {
existingEntity.setAsrModelId(dto.getAsrModelId());
}
if (dto.getVadModelId() != null) {
existingEntity.setVadModelId(dto.getVadModelId());
}
if (dto.getLlmModelId() != null) {
existingEntity.setLlmModelId(dto.getLlmModelId());
}
if (dto.getVllmModelId() != null) {
existingEntity.setVllmModelId(dto.getVllmModelId());
}
if (dto.getTtsModelId() != null) {
existingEntity.setTtsModelId(dto.getTtsModelId());
}
if (dto.getTtsVoiceId() != null) {
existingEntity.setTtsVoiceId(dto.getTtsVoiceId());
}
if (dto.getMemModelId() != null) {
existingEntity.setMemModelId(dto.getMemModelId());
}
if (dto.getIntentModelId() != null) {
existingEntity.setIntentModelId(dto.getIntentModelId());
}
if (dto.getSystemPrompt() != null) {
existingEntity.setSystemPrompt(dto.getSystemPrompt());
}
if (dto.getSummaryMemory() != null) {
existingEntity.setSummaryMemory(dto.getSummaryMemory());
}
if (dto.getChatHistoryConf() != null) {
existingEntity.setChatHistoryConf(dto.getChatHistoryConf());
}
if (dto.getLangCode() != null) {
existingEntity.setLangCode(dto.getLangCode());
}
if (dto.getLanguage() != null) {
existingEntity.setLanguage(dto.getLanguage());
}
if (dto.getSort() != null) {
existingEntity.setSort(dto.getSort());
}
// 设置更新者信息
UserDetail user = SecurityUser.getUser();
existingEntity.setUpdater(user.getId());
existingEntity.setUpdatedAt(new Date());
// 更新记忆策略
if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
// 删除所有记录
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
existingEntity.setSummaryMemory("");
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
// 删除音频数据
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
}
agentService.updateById(existingEntity);
agentService.updateAgentById(id, dto);
return new Result<>();
}
@DeleteMapping("/{id}")
@Operation(summary = "删除智能体")
@RequiresPermissions("sys:role:normal")
@@ -6,6 +6,7 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.vo.AgentInfoVO;
@Mapper
public interface AgentDao extends BaseDao<AgentEntity> {
@@ -28,4 +29,11 @@ public interface AgentDao extends BaseDao<AgentEntity> {
" WHERE d.mac_address = #{macAddress} " +
" ORDER BY d.id DESC LIMIT 1")
AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress);
/**
* 根据id查询agent信息,包括插件信息
*
* @param agentId 智能体ID
*/
AgentInfoVO selectAgentInfoById(@Param("agentId") String agentId);
}
@@ -0,0 +1,22 @@
package xiaozhi.modules.agent.dao;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
/**
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Mapper
* @createDate 2025-05-25 22:33:17
* @Entity xiaozhi.modules.agent.entity.AgentPluginMapping
*/
@Mapper
public interface AgentPluginMappingMapper extends BaseMapper<AgentPluginMapping> {
List<AgentPluginMapping> selectPluginsByAgentId(@Param("agentId") String agentId);
}
@@ -1,6 +1,9 @@
package xiaozhi.modules.agent.dto;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -15,19 +18,19 @@ import lombok.Data;
public class AgentUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "智能体编码", example = "AGT_1234567890", required = false)
@Schema(description = "智能体编码", example = "AGT_1234567890", nullable = true)
private String agentCode;
@Schema(description = "智能体名称", example = "客服助手", required = false)
@Schema(description = "智能体名称", example = "客服助手", nullable = true)
private String agentName;
@Schema(description = "语音识别模型标识", example = "asr_model_02", required = false)
@Schema(description = "语音识别模型标识", example = "asr_model_02", nullable = true)
private String asrModelId;
@Schema(description = "语音活动检测标识", example = "vad_model_02", required = false)
@Schema(description = "语音活动检测标识", example = "vad_model_02", nullable = true)
private String vadModelId;
@Schema(description = "大语言模型标识", example = "llm_model_02", required = false)
@Schema(description = "大语言模型标识", example = "llm_model_02", nullable = true)
private String llmModelId;
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
@@ -36,31 +39,45 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "语音合成模型标识", example = "tts_model_02", required = false)
private String ttsModelId;
@Schema(description = "音色标识", example = "voice_02", required = false)
@Schema(description = "音色标识", example = "voice_02", nullable = true)
private String ttsVoiceId;
@Schema(description = "记忆模型标识", example = "mem_model_02", required = false)
@Schema(description = "记忆模型标识", example = "mem_model_02", nullable = true)
private String memModelId;
@Schema(description = "意图模型标识", example = "intent_model_02", required = false)
@Schema(description = "意图模型标识", example = "intent_model_02", nullable = true)
private String intentModelId;
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
@Schema(description = "插件函数信息", nullable = true)
private List<FunctionInfo> functions;
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", nullable = true)
private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" + "根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", nullable = true)
private String summaryMemory;
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", required = false)
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", nullable = true)
private Integer chatHistoryConf;
@Schema(description = "语言编码", example = "zh_CN", required = false)
@Schema(description = "语言编码", example = "zh_CN", nullable = true)
private String langCode;
@Schema(description = "交互语种", example = "中文", required = false)
@Schema(description = "交互语种", example = "中文", nullable = true)
private String language;
@Schema(description = "排序", example = "1", required = false)
@Schema(description = "排序", example = "1", nullable = true)
private Integer sort;
@Data
@Schema(description = "插件函数信息")
public static class FunctionInfo implements Serializable {
@Schema(description = "插件ID", example = "plugin_01")
private String pluginId;
@Schema(description = "函数参数信息", nullable = true)
private HashMap<String, Object> paramInfo;
private static final long serialVersionUID = 1L;
}
}
@@ -0,0 +1,53 @@
package xiaozhi.modules.agent.entity;
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 java.io.Serializable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* Agent与插件的唯一映射表
* @TableName ai_agent_plugin_mapping
*/
@Data
@TableName(value ="ai_agent_plugin_mapping")
@Schema(description = "Agent与插件的唯一映射表")
public class AgentPluginMapping implements Serializable {
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "映射信息主键ID")
private Long id;
/**
* 智能体ID
*/
@Schema(description = "智能体ID")
private String agentId;
/**
* 插件ID
*/
@Schema(description = "插件ID")
private String pluginId;
/**
* 插件参数(Json)格式
*/
@Schema(description = "插件参数(Json)格式")
private String paramInfo;
// 冗余字段,用于方便在根据id查询插件时,对照查出插件的Provider_code,详见dao层xml文件
@TableField(exist = false)
@Schema(description = "插件provider_code, 对应表ai_model_provider")
private String providerCode;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
@@ -0,0 +1,15 @@
package xiaozhi.modules.agent.service;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service
* @createDate 2025-05-25 22:33:17
*/
public interface AgentPluginMappingService extends IService<AgentPluginMapping> {
List<AgentPluginMapping> agentPluginParamsByAgentId(String agentId);
}
@@ -6,7 +6,9 @@ import java.util.Map;
import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService;
import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.vo.AgentInfoVO;
/**
* 智能体表处理service
@@ -30,7 +32,7 @@ public interface AgentService extends BaseService<AgentEntity> {
* @param id 智能体ID
* @return 智能体实体
*/
AgentEntity getAgentById(String id);
AgentInfoVO getAgentById(String id);
/**
* 插入智能体
@@ -79,4 +81,6 @@ public interface AgentService extends BaseService<AgentEntity> {
* @return 是否有权限
*/
boolean checkAgentPermission(String agentId, Long userId);
void updateAgentById(String agentId, AgentUpdateDTO dto);
}
@@ -0,0 +1,32 @@
package xiaozhi.modules.agent.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.dao.AgentPluginMappingMapper;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service实现
* @createDate 2025-05-25 22:33:17
*/
@Service
@RequiredArgsConstructor
public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappingMapper, AgentPluginMapping>
implements AgentPluginMappingService{
private final AgentPluginMappingMapper agentPluginMappingMapper;
@Override
public List<AgentPluginMapping> agentPluginParamsByAgentId(String agentId) {
return agentPluginMappingMapper.selectPluginsByAgentId(agentId);
}
}
@@ -1,10 +1,11 @@
package xiaozhi.modules.agent.service.impl;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -13,17 +14,29 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService;
import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.enums.SuperAdminEnum;
import xiaozhi.modules.timbre.service.TimbreService;
@@ -36,6 +49,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
private final ModelConfigService modelConfigService;
private final RedisUtils redisUtils;
private final DeviceService deviceService;
private final AgentPluginMappingService agentPluginMappingService;
private final ModelProviderService modelProviderService;
private final AgentChatHistoryService agentChatHistoryService;
@Override
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
@@ -46,15 +63,20 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
}
@Override
public AgentEntity getAgentById(String id) {
AgentEntity agent = agentDao.selectById(id);
if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
} else if (agent != null && agent.getMemModelId() != null
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
&& agent.getChatHistoryConf() == null) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
public AgentInfoVO getAgentById(String id) {
AgentInfoVO agent = agentDao.selectAgentInfoById(id);
if (agent == null) {
throw new RenException("智能体不存在");
}
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
if (agent.getChatHistoryConf() == null) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
}
}
// 无需额外查询插件列表,已通过SQL查询出来
return agent;
}
@@ -167,4 +189,137 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 检查是否是智能体的所有者
return userId.equals(agent.getUserId());
}
// 根据id更新智能体信息
@Override
@Transactional(rollbackFor = Exception.class)
public void updateAgentById(String agentId, AgentUpdateDTO dto) {
// 先查询现有实体
AgentEntity existingEntity = this.getAgentById(agentId);
if (existingEntity == null) {
throw new RuntimeException("智能体不存在");
}
// 只更新提供的非空字段
if (dto.getAgentName() != null) {
existingEntity.setAgentName(dto.getAgentName());
}
if (dto.getAgentCode() != null) {
existingEntity.setAgentCode(dto.getAgentCode());
}
if (dto.getAsrModelId() != null) {
existingEntity.setAsrModelId(dto.getAsrModelId());
}
if (dto.getVadModelId() != null) {
existingEntity.setVadModelId(dto.getVadModelId());
}
if (dto.getLlmModelId() != null) {
existingEntity.setLlmModelId(dto.getLlmModelId());
}
if (dto.getVllmModelId() != null) {
existingEntity.setVllmModelId(dto.getVllmModelId());
}
if (dto.getTtsModelId() != null) {
existingEntity.setTtsModelId(dto.getTtsModelId());
}
if (dto.getTtsVoiceId() != null) {
existingEntity.setTtsVoiceId(dto.getTtsVoiceId());
}
if (dto.getMemModelId() != null) {
existingEntity.setMemModelId(dto.getMemModelId());
}
if (dto.getIntentModelId() != null) {
existingEntity.setIntentModelId(dto.getIntentModelId());
}
if (dto.getSystemPrompt() != null) {
existingEntity.setSystemPrompt(dto.getSystemPrompt());
}
if (dto.getSummaryMemory() != null) {
existingEntity.setSummaryMemory(dto.getSummaryMemory());
}
if (dto.getChatHistoryConf() != null) {
existingEntity.setChatHistoryConf(dto.getChatHistoryConf());
}
if (dto.getLangCode() != null) {
existingEntity.setLangCode(dto.getLangCode());
}
if (dto.getLanguage() != null) {
existingEntity.setLanguage(dto.getLanguage());
}
if (dto.getSort() != null) {
existingEntity.setSort(dto.getSort());
}
// 更新函数插件信息
List<AgentUpdateDTO.FunctionInfo> functions = dto.getFunctions();
if (functions != null) {
// 1. 收集本次提交的 pluginId
// TODO: 提交的参数信息里,这里是允许为空,后续可以扩展required字段限制避免为空
List<String> newPluginIds = functions.stream()
.map(AgentUpdateDTO.FunctionInfo::getPluginId)
.toList();
// 2. 查询当前agent现有的所有映射
List<AgentPluginMapping> existing = agentPluginMappingService.list(
new QueryWrapper<AgentPluginMapping>()
.eq("agent_id", agentId)
);
Map<String, AgentPluginMapping> existMap = existing.stream()
.collect(Collectors.toMap(AgentPluginMapping::getPluginId, Function.identity()));
// 3. 构造所有要 保存或更新 的实体
List<AgentPluginMapping> allToPersist = functions.stream().map(info -> {
AgentPluginMapping m = new AgentPluginMapping();
m.setAgentId(agentId);
m.setPluginId(info.getPluginId());
m.setParamInfo(JsonUtils.toJsonString(info.getParamInfo()));
AgentPluginMapping old = existMap.get(info.getPluginId());
if (old != null) {
// 已存在,设置id表示更新
m.setId(old.getId());
}
return m;
}).toList();
// 4. 拆分:已有ID的走更新,无ID的走插入
List<AgentPluginMapping> toUpdate = allToPersist.stream()
.filter(m -> m.getId() != null)
.toList();
List<AgentPluginMapping> toInsert = allToPersist.stream()
.filter(m -> m.getId() == null)
.toList();
if (!toUpdate.isEmpty()) {
agentPluginMappingService.updateBatchById(toUpdate);
}
if (!toInsert.isEmpty()) {
agentPluginMappingService.saveBatch(toInsert);
}
// 5. 删除本次不在提交列表里的插件映射
List<Long> toDelete = existing.stream()
.filter(old -> !newPluginIds.contains(old.getPluginId()))
.map(AgentPluginMapping::getId)
.toList();
if (!toDelete.isEmpty()) {
agentPluginMappingService.removeBatchByIds(toDelete);
}
}
// 设置更新者信息
UserDetail user = SecurityUser.getUser();
existingEntity.setUpdater(user.getId());
existingEntity.setUpdatedAt(new Date());
// 更新记忆策略
if (existingEntity.getMemModelId() == null || existingEntity.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
// 删除所有记录
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, true);
existingEntity.setSummaryMemory("");
} else if (existingEntity.getChatHistoryConf() != null && existingEntity.getChatHistoryConf() == 1) {
// 删除音频数据
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
}
this.updateById(existingEntity);
}
}
@@ -0,0 +1,24 @@
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;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import java.util.List;
/**
* Agent信息返回体VO
* 这里直接extend了Agent实体类AgentEntity,后续需要规范返回字段可以copy字段出来
*/
@EqualsAndHashCode(callSuper = true)
@Data
public class AgentInfoVO extends AgentEntity
{
@Schema(description = "插件列表Id")
@TableField(typeHandler = JacksonTypeHandler.class)
private List<AgentPluginMapping> functions;
}
@@ -27,7 +27,7 @@ public class ConfigController {
private final ConfigService configService;
@PostMapping("server-base")
@Operation(summary = "获取配置")
@Operation(summary = "服务端获取配置接口")
public Result<Object> getConfig() {
Object config = configService.getConfig(true);
return new Result<Object>().ok(config);
@@ -1,9 +1,6 @@
package xiaozhi.modules.config.service.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -16,7 +13,9 @@ import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.config.service.ConfigService;
@@ -39,6 +38,7 @@ public class ConfigServiceImpl implements ConfigService {
private final AgentTemplateService agentTemplateService;
private final RedisUtils redisUtils;
private final TimbreService timbreService;
private final AgentPluginMappingService agentPluginMappingService;
@Override
public Object getConfig(Boolean isCache) {
@@ -132,6 +132,19 @@ public class ConfigServiceImpl implements ConfigService {
agent.setAsrModelId(null);
}
// 添加函数调用参数信息
if (!Objects.equals(agent.getIntentModelId(), "Intent_nointent")) {
String agentId = agent.getId();
List<AgentPluginMapping> pluginMappings = agentPluginMappingService.agentPluginParamsByAgentId(agentId);
if (pluginMappings != null && !pluginMappings.isEmpty()) {
Map<String, Object> pluginParams = new HashMap<>();
for (AgentPluginMapping pluginMapping : pluginMappings) {
pluginParams.put(pluginMapping.getProviderCode(), pluginMapping.getParamInfo());
}
result.put("plugins", pluginParams);
}
}
// 构建模块配置
buildModuleConfig(
agent.getAgentName(),
@@ -284,6 +297,7 @@ public class ConfigServiceImpl implements ConfigService {
map.put("functions", functions);
}
}
System.out.println("map: " + map);
}
if ("Memory".equals(modelTypes[i])) {
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
@@ -18,6 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils;
import xiaozhi.common.validator.group.UpdateGroup;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.service.ModelProviderService;
@@ -65,4 +66,10 @@ public class ModelProviderController {
return new Result<>();
}
@GetMapping("/plugin/names")
@Tag(name = "获取插件名称列表")
public Result<List<ModelProviderDTO>> getPluginNameList() {
return ResultUtils.success(modelProviderService.getPluginList());
}
}
@@ -1,5 +1,6 @@
package xiaozhi.modules.model.service;
import java.util.Collection;
import java.util.List;
import xiaozhi.common.page.PageData;
@@ -9,6 +10,10 @@ public interface ModelProviderService {
// List<String> getModelNames(String modelType, String modelName);
List<ModelProviderDTO> getPluginList();
List<ModelProviderDTO> getPluginListByIds(Collection<String> ids);
List<ModelProviderDTO> getListByModelType(String modelType);
ModelProviderDTO add(ModelProviderDTO modelProviderDTO);
@@ -1,10 +1,8 @@
package xiaozhi.modules.model.service.impl;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
@@ -32,6 +30,24 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
private final ModelProviderDao modelProviderDao;
@Override
public List<ModelProviderDTO> getPluginList() {
LambdaQueryWrapper<ModelProviderEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ModelProviderEntity::getModelType, "Plugin");
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
}
@Override
public List<ModelProviderDTO> getPluginListByIds(Collection<String> ids) {
LambdaQueryWrapper<ModelProviderEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(ModelProviderEntity::getId, ids);
queryWrapper.eq(ModelProviderEntity::getModelType, "Plugin");
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
}
@Override
public List<ModelProviderDTO> getListByModelType(String modelType) {
@@ -0,0 +1,181 @@
-- ===============================
-- 一、在ai_model_provider中插入plugin 记录
-- ===============================
START TRANSACTION;
-- 1. 天气查询
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_WEATHER',
'Plugin',
'get_weather',
'天气查询',
JSON_ARRAY(
JSON_OBJECT(
'key', 'api_key',
'type', 'string',
'label', '天气插件 API 密钥',
'default', (SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_weather.api_key')
),
JSON_OBJECT(
'key', 'default_location',
'type', 'string',
'label', '默认查询城市',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_weather.default_location')
),
JSON_OBJECT(
'key', 'api_host',
'type', 'string',
'label', '开发者 API Host',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_weather.api_host')
)
),
10, 0, NOW(), 0, NOW());
-- 2. 新闻订阅
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_NEWS',
'Plugin',
'get_news_from_newsnow',
'新闻订阅',
JSON_ARRAY(
JSON_OBJECT(
'key', 'default_rss_url',
'type', 'string',
'label', '默认 RSS 源',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_news.default_rss_url')
),
JSON_OBJECT(
'key', 'category_urls',
'type', 'json',
'label', '分类 RSS 地址映射',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.get_news.category_urls')
)
),
20, 0, NOW(), 0, NOW());
-- 3. HomeAssistant 状态查询
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_HA_GET_STATE',
'Plugin',
'hass_get_state',
'HomeAssistant 状态查询',
JSON_ARRAY(
JSON_OBJECT(
'key', 'base_url',
'type', 'string',
'label', 'HA 服务器地址',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.base_url')
),
JSON_OBJECT(
'key', 'api_key',
'type', 'string',
'label', 'HA API 访问令牌',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.api_key')
),
JSON_OBJECT(
'key', 'devices',
'type', 'array',
'label', '设备列表(名称,实体ID;…)',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.devices')
)
),
30, 0, NOW(), 0, NOW());
-- 4. HomeAssistant 状态写入
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_HA_SET_STATE',
'Plugin',
'hass_set_state',
'HomeAssistant 状态写入',
JSON_ARRAY(
JSON_OBJECT(
'key', 'base_url',
'type', 'string',
'label', 'HA 服务器地址',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.base_url')
),
JSON_OBJECT(
'key', 'api_key',
'type', 'string',
'label', 'HA API 访问令牌',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.api_key')
),
JSON_OBJECT(
'key', 'devices',
'type', 'array',
'label', '设备列表(名称,实体ID;…)',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.home_assistant.devices')
)
),
40, 0, NOW(), 0, NOW());
-- 5. 本地播放
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_MUSIC',
'Plugin',
'play_music',
'本地播放',
JSON_ARRAY(
JSON_OBJECT(
'key', 'music_dir',
'type', 'string',
'label', '音乐文件根目录',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.play_music.music_dir')
),
JSON_OBJECT(
'key', 'music_ext',
'type', 'array',
'label', '支持的文件后缀',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.play_music.music_ext')
),
JSON_OBJECT(
'key', 'refresh_time',
'type', 'number',
'label', '列表刷新间隔(秒)',
'default',
(SELECT param_value FROM sys_params WHERE param_code = 'plugins.play_music.refresh_time')
)
),
50, 0, NOW(), 0, NOW());
-- ===============================
-- 二、删除sys_params中旧的plugins.*参数
-- ===============================
DELETE
FROM sys_params
WHERE param_code LIKE 'plugins.%';
-- ===============================
-- 三、添加智能体插件id字段
-- ===============================
CREATE TABLE IF NOT EXISTS ai_agent_plugin_mapping
(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
agent_id VARCHAR(32) NOT NULL COMMENT '智能体ID',
plugin_id VARCHAR(32) NOT NULL COMMENT '插件ID',
param_info JSON NOT NULL COMMENT '参数信息',
UNIQUE KEY uk_agent_provider (agent_id, plugin_id)
) COMMENT 'Agent与插件的唯一映射表';
COMMIT;
@@ -170,6 +170,13 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505271414.sql
- changeSet:
id: 202505292203
author: CAIXYPROMISE
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505292203.sql
- changeSet:
id: 202506010920
author: hrz
@@ -190,4 +197,4 @@ databaseChangeLog:
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202506032232.sql
path: classpath:db/changelog/202506032232.sql
@@ -5,4 +5,71 @@
<select id="getDeviceCountByAgentId" resultType="java.lang.Integer">
SELECT COUNT(*) FROM ai_device WHERE agent_id = #{agentId}
</select>
<resultMap id="AgentInfoMap" type="xiaozhi.modules.agent.vo.AgentInfoVO">
<id column="id" property="id"/>
<result column="userId" property="userId"/>
<result column="agentCode" property="agentCode"/>
<result column="agentName" property="agentName"/>
<result column="asrModelId" property="asrModelId"/>
<result column="vadModelId" property="vadModelId"/>
<result column="llmModelId" property="llmModelId"/>
<result column="ttsModelId" property="ttsModelId"/>
<result column="ttsVoiceId" property="ttsVoiceId"/>
<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"/>
<result column="langCode" property="langCode"/>
<result column="language" property="language"/>
<result column="sort" property="sort"/>
<result column="creator" property="creator"/>
<result column="createdAt" property="createdAt"/>
<result column="updater" property="updater"/>
<result column="updatedAt" property="updatedAt"/>
</resultMap>
<select id="selectAgentInfoById" resultMap="AgentInfoMap">
SELECT a.id,
a.user_id AS userId,
a.agent_code AS agentCode,
a.agent_name AS agentName,
a.asr_model_id AS asrModelId,
a.vad_model_id AS vadModelId,
a.llm_model_id AS llmModelId,
a.tts_model_id AS ttsModelId,
a.tts_voice_id AS ttsVoiceId,
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,
a.lang_code AS langCode,
a.language AS language,
a.sort,
a.creator,
a.created_at AS createdAt,
a.updater,
a.updated_at AS updatedAt
FROM ai_agent a
WHERE a.id = #{agentId}
</select>
</mapper>
@@ -0,0 +1,48 @@
<?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.AgentPluginMappingMapper">
<resultMap id="BaseResultMap" type="xiaozhi.modules.agent.entity.AgentPluginMapping">
<id property="id" column="id" jdbcType="BIGINT"/>
<result property="agentId" column="agent_id" jdbcType="VARCHAR"/>
<result property="pluginId" column="plugin_id" jdbcType="VARCHAR"/>
<result property="paramInfo" column="param_info" jdbcType="VARCHAR"/>
</resultMap>
<!-- 用于映射根据agentId查询完整插件信息 -->
<resultMap id="AgentPluginWithCodeMap" type="xiaozhi.modules.agent.entity.AgentPluginMapping">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="agentId" property="agentId" jdbcType="VARCHAR"/>
<result column="pluginId" property="pluginId" jdbcType="VARCHAR"/>
<result column="paramInfo" property="paramInfo" jdbcType="VARCHAR"/>
<result column="providerCode" property="providerCode" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
id,agent_id,plugin_id
</sql>
<select id="selectPluginsByAgentId" resultMap="AgentPluginWithCodeMap">
SELECT m.id AS id,
m.agent_id AS agentId,
m.plugin_id AS pluginId,
m.param_info AS paramInfo,
(
SELECT
p.provider_code
FROM
ai_model_provider p
WHERE
p.id = m.plugin_id
LIMIT
1
) AS providerCode
FROM ai_agent_plugin_mapping m
WHERE m.agent_id = #{agentId}
</select>
</mapper>
+16
View File
@@ -305,4 +305,20 @@ export default {
})
}).send()
},
// 获取插件列表
getPluginFunctionList(params, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/provider/plugin/names`)
.method('GET')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.networkFail((err) => {
this.$message.error(err.msg || '获取插件列表失败')
RequestService.reAjaxFun(() => {
this.getPluginFunctionList(params, callback)
})
}).send()
}
}
@@ -16,15 +16,21 @@
<el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button>
</div>
<div class="function-list">
<div v-for="func in unselected" :key="func.name" class="function-item">
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox>
<div class="func-tag" @click="handleFunctionClick(func)">
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
<span>{{ func.name }}</span>
<div v-if="unselected.length">
<div v-for="func in unselected" :key="func.name" class="function-item">
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)"
@click.native.stop></el-checkbox>
<div class="func-tag" @click="handleFunctionClick(func)">
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
<span>{{ func.name }}</span>
</div>
<el-tooltip class="item" effect="dark" :content="func.description || '暂无功能描述'" placement="top">
<img src="@/assets/home/info.png" alt="" class="info-icon">
</el-tooltip>
</div>
<el-tooltip class="item" effect="dark" :content="func.description || '暂无功能描述'" placement="top">
<img src="@/assets/home/info.png" alt="" class="info-icon">
</el-tooltip>
</div>
<div v-else style="display: flex; justify-content: center; align-items: center;">
<el-empty description="没有更多的插件了"/>
</div>
</div>
</div>
@@ -36,26 +42,83 @@
<el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button>
</div>
<div class="function-list">
<div v-for="func in selectedList" :key="func.name" class="function-item">
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox>
<div class="func-tag" @click="handleFunctionClick(func)">
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
<span>{{ func.name }}</span>
<div v-if="selectedList.length > 0">
<div v-for="func in selectedList" :key="func.name" class="function-item">
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)"
@click.native.stop></el-checkbox>
<div class="func-tag" @click="handleFunctionClick(func)">
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
<span>{{ func.name }}</span>
</div>
</div>
</div>
<div v-else style="display: flex; justify-content: center; align-items: center;">
<el-empty description="请选择插件功能"/>
</div>
</div>
</div>
<!-- 右侧参数配置 -->
<div class="params-column">
<h4 v-if="currentFunction" class="column-title">参数配置 - {{ currentFunction.name }}</h4>
<div v-if="currentFunction" class="params-container">
<el-form :model="currentFunction" size="mini" class="param-form" v-loading="loading" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
<el-form-item v-for="(value, key) in currentFunction.params" :key="key" :label="key" class="param-item">
<el-input v-model="currentFunction.params[key]" size="mini" class="param-input" @change="(val) => handleParamChange(currentFunction, key, val)"/>
</el-form-item>
</el-form>
</div>
<div v-if="currentFunction" class="params-container">
<el-form :model="currentFunction" size="mini" class="param-form">
<!-- 遍历 fieldsMeta而不是 params keys -->
<el-form-item
v-for="field in currentFunction.fieldsMeta"
:key="field.key"
:label="field.label"
class="param-item"
>
<template #label>
<span style="font-size: 16px; margin-right: 6px;">{{ field.key }}</span>
<el-tooltip effect="dark" :content="fieldRemark(field)" placement="top">
<img src="@/assets/home/info.png" alt="" class="info-icon">
</el-tooltip>
</template>
<!-- ARRAY -->
<el-input
v-if="field.type === 'array'"
type="textarea"
:rows="4"
placeholder="每行一个,回车分隔"
v-model="textCache[field.key]"
@blur="flushArray(field.key)"
/>
<!-- JSON -->
<el-input
v-else-if="field.type === 'json'"
type="textarea"
:rows="6"
placeholder="请输入合法的 JSON"
v-model="textCache[field.key]"
@blur="flushJson(field)"
/>
<!-- number -->
<el-input-number
v-else-if="field.type === 'number'"
:value="currentFunction.params[field.key]"
@change="val => handleParamChange(currentFunction, field.key, val)"
/>
<!-- boolean -->
<el-switch
v-else-if="field.type === 'boolean' || field.type === 'bool'"
:value="currentFunction.params[field.key]"
@change="val => handleParamChange(currentFunction, field.key, val)"
/>
<!-- string or fallback -->
<el-input
v-else
v-model="currentFunction.params[field.key]"
@change="val => handleParamChange(currentFunction, field.key, val)"
/>
</el-form-item>
</el-form>
</div>
<div v-else class="empty-tip">请选择已配置的功能进行参数设置</div>
</div>
</div>
@@ -74,24 +137,19 @@ export default {
functions: {
type: Array,
default: () => []
},
allFunctions: {
type: Array,
default: () => []
}
},
data() {
return {
textCache: {},
dialogVisible: this.value,
selectedNames: [],
currentFunction: null,
modifiedFunctions: {},
allFunctions: [
{name: '天气', params: {city: '北京'}, description: '查看指定城市的天气情况'},
{name: '新闻', params: {type: '科技'}, description: '获取最新科技类新闻资讯'},
{name: '工具', params: {category: '常用'}, description: '提供常用工具集合'},
{name: '退出', params: {}, description: '退出当前系统'},
{name: '音乐', params: {genre: '流行'}, description: '播放流行音乐'},
{name: '翻译', params: {from: '中文', to: '英文'}, description: '提供中英文互译功能'},
{name: '计算', params: {precision: '2'}, description: '提供精确计算功能'},
{name: '日历', params: {view: '月'}, description: '查看月历视图'}
],
functionColorMap: [
'#FF6B6B', '#4ECDC4', '#45B7D1',
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
@@ -111,10 +169,37 @@ export default {
}
},
watch: {
value(newVal) {
this.dialogVisible = newVal;
if (newVal) {
currentFunction(newFn) {
if (!newFn) return;
// 对每个字段,如果是 array 或 json,就在 textCache 里生成初始字符串
newFn.fieldsMeta.forEach(f => {
const v = newFn.params[f.key];
if (f.type === 'array') {
this.$set(this.textCache, f.key, Array.isArray(v) ? v.join('\n') : '');
}
else if (f.type === 'json') {
try {
this.$set(this.textCache, f.key, JSON.stringify(v ?? {}, null, 2));
} catch {
this.$set(this.textCache, f.key, '');
}
}
});
},
value(v) {
this.dialogVisible = v;
if (v) {
// 对话框打开时,初始化选中态
this.selectedNames = this.functions.map(f => f.name);
// 把后端传来的 this.functions(带 paramsmerge 到 allFunctions 上
this.functions.forEach(saved => {
const idx = this.allFunctions.findIndex(f => f.name === saved.name);
if (idx >= 0) {
// 保留用户之前在 saved.params 上的改动
this.allFunctions[idx].params = {...saved.params};
}
});
// 右侧默认指向第一个
this.currentFunction = this.selectedList[0] || null;
}
},
@@ -123,14 +208,32 @@ export default {
}
},
methods: {
flushArray(key) {
const text = this.textCache[key] || '';
const arr = text
.split('\n')
.map(s => s.trim())
.filter(Boolean);
this.handleParamChange(this.currentFunction, key, arr);
},
flushJson(field) {
const key = field.key;
if (!key) {
return;
}
const text = this.textCache[key] || '';
try {
const obj = JSON.parse(text);
this.handleParamChange(this.currentFunction, key, obj);
} catch {
this.$message.error(`${this.currentFunction.name}${key}字段格式错误:JSON格式有误`);
}
},
handleFunctionClick(func) {
if (this.selectedNames.includes(func.name)) {
this.loading = true;
setTimeout(() => {
const tempFunc = this.tempFunctions[func.name];
this.currentFunction = tempFunc ? tempFunc : JSON.parse(JSON.stringify(func));
this.loading = false;
}, 300);
const tempFunc = this.tempFunctions[func.name];
this.currentFunction = tempFunc ? tempFunc : func;
}
},
handleParamChange(func, key, value) {
@@ -185,11 +288,14 @@ export default {
const selected = this.selectedList.map(f => {
const modified = this.modifiedFunctions[f.name];
return modified || f;
}).map(f => ({
...f,
params: JSON.parse(JSON.stringify(f.params))
}));
return {
id: f.id,
name: f.name,
params: modified
? {...modified.params}
: {...f.params}
}
});
this.$emit('update-functions', selected);
this.dialogVisible = false;
@@ -197,11 +303,17 @@ export default {
// 通知父组件对话框已关闭且已保存
this.$emit('dialog-closed', true);
},
getFunctionColor(name) {
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
return this.functionColorMap[hash % 7];
}
return this.functionColorMap[hash % this.functionColorMap.length];
},
fieldRemark(field) {
let description = (field && field.label) ? field.label : '';
if (field.default) {
description += `(默认值:${field.default}`;
}
return description;
},
}
}
</script>
@@ -209,7 +321,7 @@ export default {
<style lang="scss" scoped>
.function-manager {
display: grid;
grid-template-columns: minmax(120px, 0.5fr) minmax(120px, 0.5fr) minmax(200px, 2fr);
grid-template-columns: max-content max-content 1fr;
gap: 12px;
height: calc(70vh - 60px);
}
@@ -248,6 +360,7 @@ export default {
overflow-y: auto;
border-right: 1px solid #EBEEF5;
scrollbar-width: none;
overflow-x: hidden;
}
.function-column::-webkit-scrollbar {
@@ -257,7 +370,7 @@ export default {
.function-list {
display: flex;
flex-direction: column;
gap: 4px;
gap: 8px;
}
.function-item {
@@ -317,6 +430,14 @@ export default {
}
.param-form {
.param-item {
font-size: 16px;
}
.param-input {
width: 100%;
}
::v-deep .el-form-item {
display: flex;
align-items: center;
@@ -356,9 +477,6 @@ export default {
text-align: center;
}
.param-input {
width: 100%;
}
.drawer-footer {
position: absolute;
+106 -56
View File
@@ -1,6 +1,6 @@
<template>
<div class="welcome">
<HeaderBar />
<HeaderBar/>
<div class="operation-bar">
<h2 class="page-title">角色配置</h2>
@@ -34,33 +34,36 @@
<div class="form-grid">
<div class="form-column">
<el-form-item label="助手昵称:">
<el-input v-model="form.agentName" class="form-input" maxlength="10" />
<el-input v-model="form.agentName" class="form-input" maxlength="10"/>
</el-form-item>
<el-form-item label="角色模版:">
<div class="template-container">
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
{{ template.agentName }}
</div>
</div>
</el-form-item>
<el-form-item label="角色介绍:">
<el-input type="textarea" rows="9" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
maxlength="2000" show-word-limit class="form-textarea" />
<el-input type="textarea" rows="9" resize="none" placeholder="请输入内容"
v-model="form.systemPrompt"
maxlength="2000" show-word-limit class="form-textarea"/>
</el-form-item>
<el-form-item label="记忆:">
<el-input type="textarea" rows="6" resize="none" v-model="form.summaryMemory" maxlength="2000"
show-word-limit class="form-textarea"
:disabled="form.model.memModelId !== 'Memory_mem_local_short'" />
show-word-limit class="form-textarea"
:disabled="form.model.memModelId !== 'Memory_mem_local_short'"/>
</el-form-item>
<el-form-item label="语言编码:" style="display: none;">
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit
class="form-input" />
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10"
show-word-limit
class="form-input"/>
</el-form-item>
<el-form-item label="交互语种:" style="display: none;">
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit
class="form-input" />
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10"
show-word-limit
class="form-input"/>
</el-form-item>
</div>
<div class="form-column">
@@ -88,13 +91,13 @@
class="model-item">
<div class="model-select-wrapper">
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select"
@change="handleModelChange(model.type, $event)">
@change="handleModelChange(model.type, $event)">
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value"/>
</el-select>
<div v-if="showFunctionIcons(model.type)" class="function-icons">
<el-tooltip v-for="func in currentFunctions" :key="func.name" effect="dark" placement="top"
popper-class="custom-tooltip">
popper-class="custom-tooltip">
<div slot="content">
<div><strong>功能名称:</strong> {{ func.name }}</div>
<div v-if="Object.keys(func.params).length > 0">
@@ -109,13 +112,15 @@
{{ func.name.charAt(0) }}
</div>
</el-tooltip>
<el-button class="edit-function-btn" @click="showFunctionDialog = true"
:class="{ 'active-btn': showFunctionDialog }">
<el-button
class="edit-function-btn"
@click="openFunctionDialog"
:class="{ 'active-btn': showFunctionDialog }">
编辑功能
</el-button>
</div>
<div v-if="model.type === 'Memory' && form.model.memModelId !== 'Memory_nomem'"
class="chat-history-options">
class="chat-history-options">
<el-radio-group v-model="form.chatHistoryConf" @change="updateChatHistoryConf">
<el-radio-button :label="1">上报文字</el-radio-button>
<el-radio-button :label="2">上报文字+语音</el-radio-button>
@@ -126,7 +131,7 @@
<el-form-item label="角色音色">
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
:value="item.value" />
:value="item.value"/>
</el-select>
</el-form-item>
</div>
@@ -138,8 +143,12 @@
</div>
</div>
<function-dialog v-model="showFunctionDialog" :functions="currentFunctions"
@update-functions="handleUpdateFunctions" @dialog-closed="handleDialogClosed" />
<function-dialog
v-model="showFunctionDialog"
:functions="currentFunctions"
:all-functions="allFunctions"
@update-functions="handleUpdateFunctions"
@dialog-closed="handleDialogClosed"/>
</div>
</template>
@@ -150,7 +159,7 @@ import HeaderBar from "@/components/HeaderBar.vue";
export default {
name: 'RoleConfigPage',
components: { HeaderBar, FunctionDialog },
components: {HeaderBar, FunctionDialog},
data() {
return {
form: {
@@ -192,12 +201,8 @@ export default {
'#FF6B6B', '#4ECDC4', '#45B7D1',
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
],
allFunctions: [
{ name: '天气', params: {} },
{ name: '新闻', params: {} },
{ name: '工具', params: {} },
{ name: '退出', params: {} }
],
allFunctions: [],
originalFunctions: [],
}
},
methods: {
@@ -222,9 +227,14 @@ export default {
langCode: this.form.langCode,
language: this.form.language,
sort: this.form.sort,
functions: this.currentFunctions
functions: this.currentFunctions.map(item => {
return ({
pluginId: item.id,
paramInfo: item.params
})
})
};
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
if (data.code === 0) {
this.$message.success({
message: '配置保存成功',
@@ -269,10 +279,11 @@ export default {
message: '配置已重置',
showClose: true
})
}).catch(() => { });
}).catch(() => {
});
},
fetchTemplates() {
Api.agent.getAgentTemplate(({ data }) => {
Api.agent.getAgentTemplate(({data}) => {
if (data.code === 0) {
this.templates = data.data;
} else {
@@ -320,7 +331,7 @@ export default {
};
},
fetchAgentConfig(agentId) {
Api.agent.getDeviceConfig(agentId, ({ data }) => {
Api.agent.getDeviceConfig(agentId, ({data}) => {
if (data.code === 0) {
this.form = {
...this.form,
@@ -335,7 +346,33 @@ export default {
intentModelId: data.data.intentModelId
}
};
this.currentFunctions = data.data.functions || [];
// 后端只给了最小映射:[{ id, agentId, pluginId }, ...]
const savedMappings = data.data.functions || [];
// 先保证 allFunctions 已经加载(如果没有,则先 fetchAllFunctions
const ensureFuncs = this.allFunctions.length
? Promise.resolve()
: this.fetchAllFunctions();
ensureFuncs.then(() => {
// 合并:按照 pluginId(id 字段)把全量元数据信息补齐
this.currentFunctions = savedMappings.map(mapping => {
const meta = this.allFunctions.find(f => f.id === mapping.pluginId);
if (!meta) {
// 插件定义没找到,退化处理
return { id: mapping.pluginId, name: mapping.pluginId, params: {} };
}
return {
id: mapping.pluginId,
name: meta.name,
// 后端如果还有 paramInfo 字段就用 mapping.paramInfo,否则用 meta.params 默认值
params: mapping.paramInfo || { ...meta.params },
fieldsMeta: meta.fieldsMeta // 保留以便对话框渲染 tooltip
};
});
// 备份原始,以备取消时恢复
this.originalFunctions = JSON.parse(JSON.stringify(this.currentFunctions));
});
} else {
this.$message.error(data.msg || '获取配置失败');
}
@@ -343,7 +380,7 @@ export default {
},
fetchModelOptions() {
this.models.forEach(model => {
Api.model.getModelNames(model.type, '', ({ data }) => {
Api.model.getModelNames(model.type, '', ({data}) => {
if (data.code === 0) {
this.$set(this.modelOptions, model.type, data.data.map(item => ({
value: item.id,
@@ -360,7 +397,7 @@ export default {
this.voiceOptions = [];
return;
}
Api.model.getModelVoices(modelId, '', ({ data }) => {
Api.model.getModelVoices(modelId, '', ({data}) => {
if (data.code === 0 && data.data) {
this.voiceOptions = data.data.map(voice => ({
value: voice.id,
@@ -373,17 +410,15 @@ export default {
},
getFunctionColor(name) {
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
return this.functionColorMap[hash % 7];
return this.functionColorMap[hash % this.functionColorMap.length];
},
showFunctionIcons(type) {
// TODO 暂时不放出来
return false;
// return type === 'Intent' &&
// this.form.model.intentModelId !== 'Intent_nointent';
return type === 'Intent' &&
this.form.model.intentModelId !== 'Intent_nointent';
},
handleModelChange(type, value) {
if (type === 'Intent' && value !== 'Intent_nointent') {
this.fetchFunctionList();
this.fetchAllFunctions();
}
if (type === 'Memory' && value === 'Memory_nomem') {
this.form.chatHistoryConf = 0;
@@ -392,28 +427,45 @@ export default {
this.form.chatHistoryConf = 2;
}
},
fetchFunctionList() {
// 使用假数据代替API调用
return new Promise(resolve => {
setTimeout(() => {
this.currentFunctions = [
{ name: '天气', params: { city: '北京' } },
{ name: '新闻', params: { type: '科技' } }
];
resolve();
}, 500);
fetchAllFunctions() {
return new Promise((resolve, reject) => {
Api.model.getPluginFunctionList(null, ({ data }) => {
if (data.code === 0) {
this.allFunctions = data.data.map(item => {
const meta = JSON.parse(item.fields || '[]');
const params = meta.reduce((m, f) => {
m[f.key] = f.default;
return m;
}, {});
return { ...item, fieldsMeta: meta, params };
});
resolve();
} else {
this.$message.error(data.msg || '获取插件列表失败');
reject();
}
});
});
},
openFunctionDialog() {
// 显示编辑对话框时,确保 allFunctions 已经加载
if (this.allFunctions.length === 0) {
this.fetchAllFunctions().then(() => this.showFunctionDialog=true);
} else {
this.showFunctionDialog = true;
}
},
handleUpdateFunctions(selected) {
this.currentFunctions = selected;
console.log('保存的功能列表:', selected);
this.$message.success('功能配置已保存');
},
handleDialogClosed(saved) {
if (!saved) {
// 如果未保存,恢复原始功能列表
this.currentFunctions = JSON.parse(JSON.stringify(this.originalFunctions));
} else {
this.originalFunctions = JSON.parse(JSON.stringify(this.currentFunctions));
}
this.showFunctionDialog = false;
},
updateChatHistoryConf() {
if (this.form.model.memModelId === 'Memory_nomem') {
@@ -446,9 +498,7 @@ export default {
const agentId = this.$route.query.agentId;
if (agentId) {
this.fetchAgentConfig(agentId);
this.fetchFunctionList().then(() => {
this.originalFunctions = JSON.parse(JSON.stringify(this.currentFunctions));
});
this.fetchAllFunctions();
}
this.fetchModelOptions();
this.fetchTemplates();
+8 -3
View File
@@ -445,9 +445,14 @@ class ConnectionHandler:
if private_config.get("Intent", None) is not None:
init_intent = True
self.config["Intent"] = private_config["Intent"]
self.config["selected_module"]["Intent"] = private_config[
"selected_module"
]["Intent"]
model_intent = private_config.get('selected_module', {}).get('Intent', {})
self.config["selected_module"]["Intent"] = model_intent
# 加载插件配置
if model_intent != 'Intent_nointent':
plugin_from_server = private_config.get("plugins", {})
for plugin, config_str in plugin_from_server.items():
plugin_from_server[plugin] = json.loads(config_str)
self.config['plugins'] = plugin_from_server
if private_config.get("prompt", None) is not None:
self.config["prompt"] = private_config["prompt"]
if private_config.get("summaryMemory", None) is not None: