Merge pull request #1564 from xinnan-tech/agent-plugin

Agent plugin
This commit is contained in:
欣南科技
2025-06-13 09:48:38 +08:00
committed by GitHub
54 changed files with 1469 additions and 699 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>();
}
}
@@ -1,6 +1,5 @@
package xiaozhi.modules.agent.controller; package xiaozhi.modules.agent.controller;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -33,8 +32,8 @@ import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.user.UserDetail; import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils;
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO; import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO; import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
import xiaozhi.modules.agent.dto.AgentCreateDTO; import xiaozhi.modules.agent.dto.AgentCreateDTO;
@@ -45,8 +44,10 @@ import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentChatAudioService; import xiaozhi.modules.agent.service.AgentChatAudioService;
import xiaozhi.modules.agent.service.AgentChatHistoryService; import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.entity.DeviceEntity; import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
@@ -61,6 +62,7 @@ public class AgentController {
private final DeviceService deviceService; private final DeviceService deviceService;
private final AgentChatHistoryService agentChatHistoryService; private final AgentChatHistoryService agentChatHistoryService;
private final AgentChatAudioService agentChatAudioService; private final AgentChatAudioService agentChatAudioService;
private final AgentPluginMappingService agentPluginMappingService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
@GetMapping("/list") @GetMapping("/list")
@@ -88,46 +90,17 @@ public class AgentController {
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "获取智能体详情") @Operation(summary = "获取智能体详情")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<AgentEntity> getAgentById(@PathVariable("id") String id) { public Result<AgentInfoVO> getAgentById(@PathVariable("id") String id) {
AgentEntity agent = agentService.getAgentById(id); AgentInfoVO agent = agentService.getAgentById(id);
return new Result<AgentEntity>().ok(agent); return ResultUtils.success(agent);
} }
@PostMapping @PostMapping
@Operation(summary = "创建智能体") @Operation(summary = "创建智能体")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<String> save(@RequestBody @Valid AgentCreateDTO dto) { public Result<String> save(@RequestBody @Valid AgentCreateDTO dto) {
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class); String agentId = agentService.createAgent(dto);
return new Result<String>().ok(agentId);
// 获取默认模板
AgentTemplateEntity template = agentTemplateService.getDefaultTemplate();
if (template != null) {
// 设置模板中的默认值
entity.setAsrModelId(template.getAsrModelId());
entity.setVadModelId(template.getVadModelId());
entity.setLlmModelId(template.getLlmModelId());
entity.setVllmModelId(template.getVllmModelId());
entity.setTtsModelId(template.getTtsModelId());
entity.setTtsVoiceId(template.getTtsVoiceId());
entity.setMemModelId(template.getMemModelId());
entity.setIntentModelId(template.getIntentModelId());
entity.setSystemPrompt(template.getSystemPrompt());
entity.setSummaryMemory(template.getSummaryMemory());
entity.setChatHistoryConf(template.getChatHistoryConf());
entity.setLangCode(template.getLangCode());
entity.setLanguage(template.getLanguage());
}
// 设置用户ID和创建者信息
UserDetail user = SecurityUser.getUser();
entity.setUserId(user.getId());
entity.setCreator(user.getId());
entity.setCreatedAt(new Date());
// ID、智能体编码和排序会在Service层自动生成
agentService.insert(entity);
return new Result<String>().ok(entity.getId());
} }
@PutMapping("/saveMemory/{macAddress}") @PutMapping("/saveMemory/{macAddress}")
@@ -139,88 +112,15 @@ public class AgentController {
} }
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO(); AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory()); agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
return updateAgentById(device.getAgentId(), agentUpdateDTO); agentService.updateAgentById(device.getAgentId(), agentUpdateDTO);
return new Result<>();
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "更新智能体") @Operation(summary = "更新智能体")
@RequiresPermissions("sys:role:normal") @RequiresPermissions("sys:role:normal")
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) { public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
return updateAgentById(id, dto); agentService.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);
return new Result<>(); return new Result<>();
} }
@@ -232,6 +132,8 @@ public class AgentController {
deviceService.deleteByAgentId(id); deviceService.deleteByAgentId(id);
// 删除关联的聊天记录 // 删除关联的聊天记录
agentChatHistoryService.deleteByAgentId(id, true, true); agentChatHistoryService.deleteByAgentId(id, true, true);
// 删除关联的插件
agentPluginMappingService.deleteByAgentId(id);
// 再删除智能体 // 再删除智能体
agentService.deleteById(id); agentService.deleteById(id);
return new Result<>(); return new Result<>();
@@ -6,6 +6,7 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import xiaozhi.common.dao.BaseDao; import xiaozhi.common.dao.BaseDao;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.vo.AgentInfoVO;
@Mapper @Mapper
public interface AgentDao extends BaseDao<AgentEntity> { public interface AgentDao extends BaseDao<AgentEntity> {
@@ -28,4 +29,11 @@ public interface AgentDao extends BaseDao<AgentEntity> {
" WHERE d.mac_address = #{macAddress} " + " WHERE d.mac_address = #{macAddress} " +
" ORDER BY d.id DESC LIMIT 1") " ORDER BY d.id DESC LIMIT 1")
AgentEntity getDefaultAgentByMacAddress(@Param("macAddress") String macAddress); 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,8 @@
package xiaozhi.modules.agent.dto; package xiaozhi.modules.agent.dto;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -15,19 +17,19 @@ import lombok.Data;
public class AgentUpdateDTO implements Serializable { public class AgentUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@Schema(description = "智能体编码", example = "AGT_1234567890", required = false) @Schema(description = "智能体编码", example = "AGT_1234567890", nullable = true)
private String agentCode; private String agentCode;
@Schema(description = "智能体名称", example = "客服助手", required = false) @Schema(description = "智能体名称", example = "客服助手", nullable = true)
private String agentName; private String agentName;
@Schema(description = "语音识别模型标识", example = "asr_model_02", required = false) @Schema(description = "语音识别模型标识", example = "asr_model_02", nullable = true)
private String asrModelId; private String asrModelId;
@Schema(description = "语音活动检测标识", example = "vad_model_02", required = false) @Schema(description = "语音活动检测标识", example = "vad_model_02", nullable = true)
private String vadModelId; private String vadModelId;
@Schema(description = "大语言模型标识", example = "llm_model_02", required = false) @Schema(description = "大语言模型标识", example = "llm_model_02", nullable = true)
private String llmModelId; private String llmModelId;
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false) @Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
@@ -36,31 +38,46 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "语音合成模型标识", example = "tts_model_02", required = false) @Schema(description = "语音合成模型标识", example = "tts_model_02", required = false)
private String ttsModelId; private String ttsModelId;
@Schema(description = "音色标识", example = "voice_02", required = false) @Schema(description = "音色标识", example = "voice_02", nullable = true)
private String ttsVoiceId; private String ttsVoiceId;
@Schema(description = "记忆模型标识", example = "mem_model_02", required = false) @Schema(description = "记忆模型标识", example = "mem_model_02", nullable = true)
private String memModelId; private String memModelId;
@Schema(description = "意图模型标识", example = "intent_model_02", required = false) @Schema(description = "意图模型标识", example = "intent_model_02", nullable = true)
private String intentModelId; private String intentModelId;
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false) @Schema(description = "插件函数信息", nullable = true)
private List<FunctionInfo> functions;
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", nullable = true)
private String systemPrompt; private String systemPrompt;
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" + @Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n"
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false) + "根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", nullable = true)
private String summaryMemory; private String summaryMemory;
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", required = false) @Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", nullable = true)
private Integer chatHistoryConf; private Integer chatHistoryConf;
@Schema(description = "语言编码", example = "zh_CN", required = false) @Schema(description = "语言编码", example = "zh_CN", nullable = true)
private String langCode; private String langCode;
@Schema(description = "交互语种", example = "中文", required = false) @Schema(description = "交互语种", example = "中文", nullable = true)
private String language; private String language;
@Schema(description = "排序", example = "1", required = false) @Schema(description = "排序", example = "1", nullable = true)
private Integer sort; 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,54 @@
package xiaozhi.modules.agent.entity;
import java.io.Serializable;
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 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,29 @@
package xiaozhi.modules.agent.service;
import java.util.List;
import com.baomidou.mybatisplus.extension.service.IService;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
/**
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service
* @createDate 2025-05-25 22:33:17
*/
public interface AgentPluginMappingService extends IService<AgentPluginMapping> {
/**
* 根据智能体id获取插件参数
*
* @param agentId
* @return
*/
List<AgentPluginMapping> agentPluginParamsByAgentId(String agentId);
/**
* 根据智能体id删除插件参数
*
* @param agentId
*/
void deleteByAgentId(String agentId);
}
@@ -5,8 +5,11 @@ import java.util.Map;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.service.BaseService; import xiaozhi.common.service.BaseService;
import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.vo.AgentInfoVO;
/** /**
* 智能体表处理service * 智能体表处理service
@@ -30,7 +33,7 @@ public interface AgentService extends BaseService<AgentEntity> {
* @param id 智能体ID * @param id 智能体ID
* @return 智能体实体 * @return 智能体实体
*/ */
AgentEntity getAgentById(String id); AgentInfoVO getAgentById(String id);
/** /**
* 插入智能体 * 插入智能体
@@ -79,4 +82,20 @@ public interface AgentService extends BaseService<AgentEntity> {
* @return 是否有权限 * @return 是否有权限
*/ */
boolean checkAgentPermission(String agentId, Long userId); boolean checkAgentPermission(String agentId, Long userId);
/**
* 更新智能体
*
* @param agentId 智能体ID
* @param dto 更新智能体所需的信息
*/
void updateAgentById(String agentId, AgentUpdateDTO dto);
/**
* 创建智能体
*
* @param dto 创建智能体所需的信息
* @return 创建的智能体ID
*/
String createAgent(AgentCreateDTO dto);
} }
@@ -0,0 +1,37 @@
package xiaozhi.modules.agent.service.impl;
import java.util.List;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import xiaozhi.modules.agent.dao.AgentPluginMappingMapper;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
/**
* @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);
}
@Override
public void deleteByAgentId(String agentId) {
UpdateWrapper<AgentPluginMapping> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("agent_id", agentId);
agentPluginMappingMapper.delete(updateWrapper);
}
}
@@ -1,12 +1,17 @@
package xiaozhi.modules.agent.service.impl; package xiaozhi.modules.agent.service.impl;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; 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.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
@@ -14,16 +19,30 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant; import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.service.impl.BaseServiceImpl;
import xiaozhi.common.user.UserDetail;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.dao.AgentDao; import xiaozhi.modules.agent.dao.AgentDao;
import xiaozhi.modules.agent.dto.AgentCreateDTO;
import xiaozhi.modules.agent.dto.AgentDTO; import xiaozhi.modules.agent.dto.AgentDTO;
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.service.ModelConfigService; import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.model.service.ModelProviderService;
import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.security.user.SecurityUser;
import xiaozhi.modules.sys.enums.SuperAdminEnum; import xiaozhi.modules.sys.enums.SuperAdminEnum;
import xiaozhi.modules.timbre.service.TimbreService; import xiaozhi.modules.timbre.service.TimbreService;
@@ -36,6 +55,10 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
private final ModelConfigService modelConfigService; private final ModelConfigService modelConfigService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final DeviceService deviceService; private final DeviceService deviceService;
private final AgentPluginMappingService agentPluginMappingService;
private final AgentChatHistoryService agentChatHistoryService;
private final AgentTemplateService agentTemplateService;
private final ModelProviderService modelProviderService;
@Override @Override
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) { public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
@@ -46,15 +69,20 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
} }
@Override @Override
public AgentEntity getAgentById(String id) { public AgentInfoVO getAgentById(String id) {
AgentEntity agent = agentDao.selectById(id); AgentInfoVO agent = agentDao.selectAgentInfoById(id);
if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode()); if (agent == null) {
} else if (agent != null && agent.getMemModelId() != null throw new RenException("智能体不存在");
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
&& agent.getChatHistoryConf() == null) {
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
} }
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; return agent;
} }
@@ -167,4 +195,198 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
// 检查是否是智能体的所有者 // 检查是否是智能体的所有者
return userId.equals(agent.getUserId()); 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
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);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String createAgent(AgentCreateDTO dto) {
// 转换为实体
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
// 获取默认模板
AgentTemplateEntity template = agentTemplateService.getDefaultTemplate();
if (template != null) {
// 设置模板中的默认值
entity.setAsrModelId(template.getAsrModelId());
entity.setVadModelId(template.getVadModelId());
entity.setLlmModelId(template.getLlmModelId());
entity.setVllmModelId(template.getVllmModelId());
entity.setTtsModelId(template.getTtsModelId());
entity.setTtsVoiceId(template.getTtsVoiceId());
entity.setMemModelId(template.getMemModelId());
entity.setIntentModelId(template.getIntentModelId());
entity.setSystemPrompt(template.getSystemPrompt());
entity.setSummaryMemory(template.getSummaryMemory());
entity.setChatHistoryConf(template.getChatHistoryConf());
entity.setLangCode(template.getLangCode());
entity.setLanguage(template.getLanguage());
}
// 设置用户ID和创建者信息
UserDetail user = SecurityUser.getUser();
entity.setUserId(user.getId());
entity.setCreator(user.getId());
entity.setCreatedAt(new Date());
// 保存智能体
insert(entity);
// 设置默认插件
List<AgentPluginMapping> toInsert = new ArrayList<>();
// 播放音乐、查天气、查新闻
String[] pluginIds = new String[] { "SYSTEM_PLUGIN_MUSIC", "SYSTEM_PLUGIN_WEATHER",
"SYSTEM_PLUGIN_NEWS_NEWSNOW" };
for (String pluginId : pluginIds) {
ModelProviderDTO provider = modelProviderService.getById(pluginId);
if (provider == null) {
continue;
}
AgentPluginMapping mapping = new AgentPluginMapping();
mapping.setPluginId(pluginId);
Map<String, Object> paramInfo = new HashMap<>();
List<Map<String, Object>> fields = JsonUtils.parseObject(provider.getFields(), List.class);
if (fields != null) {
for (Map<String, Object> field : fields) {
paramInfo.put((String) field.get("key"), field.get("default"));
}
}
mapping.setParamInfo(JsonUtils.toJsonString(paramInfo));
mapping.setAgentId(entity.getId());
toInsert.add(mapping);
}
// 保存默认插件
agentPluginMappingService.saveBatch(toInsert);
return entity.getId();
}
} }
@@ -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; private final ConfigService configService;
@PostMapping("server-base") @PostMapping("server-base")
@Operation(summary = "获取配置") @Operation(summary = "服务端获取配置接口")
public Result<Object> getConfig() { public Result<Object> getConfig() {
Object config = configService.getConfig(true); Object config = configService.getConfig(true);
return new Result<Object>().ok(config); return new Result<Object>().ok(config);
@@ -1,9 +1,6 @@
package xiaozhi.modules.config.service.impl; package xiaozhi.modules.config.service.impl;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -16,7 +13,9 @@ import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.utils.JsonUtils; import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.entity.AgentEntity; import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentPluginMapping;
import xiaozhi.modules.agent.entity.AgentTemplateEntity; import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService; import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.config.service.ConfigService; import xiaozhi.modules.config.service.ConfigService;
@@ -39,6 +38,7 @@ public class ConfigServiceImpl implements ConfigService {
private final AgentTemplateService agentTemplateService; private final AgentTemplateService agentTemplateService;
private final RedisUtils redisUtils; private final RedisUtils redisUtils;
private final TimbreService timbreService; private final TimbreService timbreService;
private final AgentPluginMappingService agentPluginMappingService;
@Override @Override
public Object getConfig(Boolean isCache) { public Object getConfig(Boolean isCache) {
@@ -132,6 +132,19 @@ public class ConfigServiceImpl implements ConfigService {
agent.setAsrModelId(null); 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( buildModuleConfig(
agent.getAgentName(), agent.getAgentName(),
@@ -284,6 +297,7 @@ public class ConfigServiceImpl implements ConfigService {
map.put("functions", functions); map.put("functions", functions);
} }
} }
System.out.println("map: " + map);
} }
if ("Memory".equals(modelTypes[i])) { if ("Memory".equals(modelTypes[i])) {
Map<String, Object> map = (Map<String, Object>) model.getConfigJson(); 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 lombok.AllArgsConstructor;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.Result; import xiaozhi.common.utils.Result;
import xiaozhi.common.utils.ResultUtils;
import xiaozhi.common.validator.group.UpdateGroup; import xiaozhi.common.validator.group.UpdateGroup;
import xiaozhi.modules.model.dto.ModelProviderDTO; import xiaozhi.modules.model.dto.ModelProviderDTO;
import xiaozhi.modules.model.service.ModelProviderService; import xiaozhi.modules.model.service.ModelProviderService;
@@ -65,4 +66,10 @@ public class ModelProviderController {
return new Result<>(); 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; package xiaozhi.modules.model.service;
import java.util.Collection;
import java.util.List; import java.util.List;
import xiaozhi.common.page.PageData; import xiaozhi.common.page.PageData;
@@ -7,7 +8,11 @@ import xiaozhi.modules.model.dto.ModelProviderDTO;
public interface ModelProviderService { public interface ModelProviderService {
// List<String> getModelNames(String modelType, String modelName); List<ModelProviderDTO> getPluginList();
ModelProviderDTO getById(String id);
List<ModelProviderDTO> getPluginListByIds(Collection<String> ids);
List<ModelProviderDTO> getListByModelType(String modelType); List<ModelProviderDTO> getListByModelType(String modelType);
@@ -1,5 +1,6 @@
package xiaozhi.modules.model.service.impl; package xiaozhi.modules.model.service.impl;
import java.util.Collection;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -8,6 +9,7 @@ import java.util.Map;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
@@ -32,6 +34,29 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
private final ModelProviderDao 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 ModelProviderDTO getById(String id) {
ModelProviderEntity entity = modelProviderDao.selectById(id);
return ConvertUtils.sourceToTarget(entity, 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 @Override
public List<ModelProviderDTO> getListByModelType(String modelType) { public List<ModelProviderDTO> getListByModelType(String modelType) {
@@ -2,36 +2,29 @@ package xiaozhi.modules.sys.enums;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import xiaozhi.common.exception.RenException;
/** /**
* 服务端动作枚举 * 服务端动作枚举
*/ */
public enum ServerActionEnum public enum ServerActionEnum {
{
RESTART("restart"), RESTART("restart"),
UPDATE_CONFIG("update_config"); UPDATE_CONFIG("update_config");
private final String value; private final String value;
ServerActionEnum(String value) ServerActionEnum(String value) {
{
this.value = value; this.value = value;
} }
@JsonValue @JsonValue
public String getValue() public String getValue() {
{
return value; return value;
} }
@JsonCreator @JsonCreator
public static ServerActionEnum fromValue(String value) public static ServerActionEnum fromValue(String value) {
{ for (ServerActionEnum action : ServerActionEnum.values()) {
for (ServerActionEnum action : ServerActionEnum.values()) if (action.value.equalsIgnoreCase(value)) {
{
if (action.value.equalsIgnoreCase(value))
{
return action; return action;
} }
} }
@@ -1,16 +1,16 @@
package xiaozhi.modules.sys.enums; package xiaozhi.modules.sys.enums;
import org.apache.commons.lang3.StringUtils;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
/** /**
* 服务端调用响应枚举 * 服务端调用响应枚举
*/ */
public enum ServerActionResponseEnum public enum ServerActionResponseEnum {
{
SUCCESS("success"), FAIL("fail"); SUCCESS("success"), FAIL("fail");
private final String value; private final String value;
ServerActionResponseEnum(String value) { ServerActionResponseEnum(String value) {
@@ -18,8 +18,7 @@ public enum ServerActionResponseEnum
} }
@JsonValue @JsonValue
public String getValue() public String getValue() {
{
return value; return value;
} }
@@ -1,13 +1,5 @@
package xiaozhi.modules.sys.utils; package xiaozhi.modules.sys.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.web.socket.*;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import xiaozhi.common.utils.DateUtils;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
@@ -15,30 +7,51 @@ import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.*; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.springframework.util.StopWatch;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHttpHeaders;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import xiaozhi.common.utils.DateUtils;
/** /**
* WebSocketClientResource:支持 try-with-resources 模式 * WebSocketClientResource:支持 try-with-resources 模式
*/ */
@Slf4j @Slf4j
public class WebSocketClientManager implements Closeable public class WebSocketClientManager implements Closeable {
{
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
// 全局回调线程池 // 全局回调线程池
private static final ExecutorService CALLBACK_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() { private static final ExecutorService CALLBACK_EXECUTOR = Executors
private final AtomicInteger cnt = new AtomicInteger(); .newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
private final AtomicInteger cnt = new AtomicInteger();
public Thread newThread(Runnable r) public Thread newThread(Runnable r) {
{ Thread t = new Thread(r, "ws-callback-" + cnt.getAndIncrement());
Thread t = new Thread(r, "ws-callback-" + cnt.getAndIncrement()); t.setDaemon(true);
t.setDaemon(true); return t;
return t; }
} });
});
private volatile WebSocketSession session; private volatile WebSocketSession session;
private final BlockingQueue<String> textMessageQueue; private final BlockingQueue<String> textMessageQueue;
@@ -51,18 +64,10 @@ public class WebSocketClientManager implements Closeable
private volatile Consumer<byte[]> onBinary; private volatile Consumer<byte[]> onBinary;
private volatile Consumer<Throwable> onError; private volatile Consumer<Throwable> onError;
private final String uri;
private final WebSocketHttpHeaders headers;
private final long connectTimeout;
private final TimeUnit connectUnit;
private final int queueCapacity; private final int queueCapacity;
// 私有构造,仅由 Builder 调用 // 私有构造,仅由 Builder 调用
private WebSocketClientManager(Builder b) { private WebSocketClientManager(Builder b) {
this.uri = b.uri;
this.headers = b.headers != null ? b.headers : new WebSocketHttpHeaders();
this.connectTimeout = b.connectTimeout;
this.connectUnit = b.connectUnit;
this.maxSessionDuration = b.maxSessionDuration; this.maxSessionDuration = b.maxSessionDuration;
this.maxSessionDurationUnit = b.maxSessionDurationUnit; this.maxSessionDurationUnit = b.maxSessionDurationUnit;
this.queueCapacity = b.queueCapacity; this.queueCapacity = b.queueCapacity;
@@ -71,13 +76,14 @@ public class WebSocketClientManager implements Closeable
this.errorFuture = new CompletableFuture<>(); this.errorFuture = new CompletableFuture<>();
} }
public static WebSocketClientManager build(Builder b) throws InterruptedException, ExecutionException, TimeoutException, IOException { public static WebSocketClientManager build(Builder b)
throws InterruptedException, ExecutionException, TimeoutException, IOException {
WebSocketClientManager ws = new WebSocketClientManager(b); WebSocketClientManager ws = new WebSocketClientManager(b);
StandardWebSocketClient client = new StandardWebSocketClient(); StandardWebSocketClient client = new StandardWebSocketClient();
CompletableFuture<WebSocketSession> future = client.execute(ws.new InternalHandler(b.uri), b.headers, URI.create(b.uri)); CompletableFuture<WebSocketSession> future = client.execute(ws.new InternalHandler(b.uri), b.headers,
URI.create(b.uri));
WebSocketSession sess = future.get(b.connectTimeout, b.connectUnit); WebSocketSession sess = future.get(b.connectTimeout, b.connectUnit);
if (sess == null || !sess.isOpen()) if (sess == null || !sess.isOpen()) {
{
throw new IOException("握手失败或会话未打开"); throw new IOException("握手失败或会话未打开");
} }
ws.session = sess; ws.session = sess;
@@ -100,13 +106,10 @@ public class WebSocketClientManager implements Closeable
session.sendMessage(new TextMessage(json)); session.sendMessage(new TextMessage(json));
} }
private <T> List<T> listenerCustom( private <T> List<T> listenerCustom(
BlockingQueue<T> queue, BlockingQueue<T> queue,
Predicate<T> predicate) Predicate<T> predicate)
throws InterruptedException, TimeoutException, ExecutionException throws InterruptedException, TimeoutException, ExecutionException {
{
List<T> collected = new ArrayList<>(); List<T> collected = new ArrayList<>();
long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration); long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration);
@@ -136,17 +139,16 @@ public class WebSocketClientManager implements Closeable
/** /**
* 同步接收多条消息,直到 predicate 为 true 或超时抛异常; * 同步接收多条消息,直到 predicate 为 true 或超时抛异常;
*
* @return 返回监听期间的所有消息列表 * @return 返回监听期间的所有消息列表
*/ */
public List<String> listener(Predicate<String> predicate) public List<String> listener(Predicate<String> predicate)
throws InterruptedException, TimeoutException, ExecutionException throws InterruptedException, TimeoutException, ExecutionException {
{
return listenerCustom(textMessageQueue, predicate); return listenerCustom(textMessageQueue, predicate);
} }
public List<byte[]> listenerBinary(Predicate<byte[]> predicate) public List<byte[]> listenerBinary(Predicate<byte[]> predicate)
throws InterruptedException, TimeoutException, ExecutionException throws InterruptedException, TimeoutException, ExecutionException {
{
return listenerCustom(binaryMessageQueue, predicate); return listenerCustom(binaryMessageQueue, predicate);
} }
@@ -183,8 +185,8 @@ public class WebSocketClientManager implements Closeable
if (session != null && session.isOpen()) { if (session != null && session.isOpen()) {
session.close(CloseStatus.NORMAL); session.close(CloseStatus.NORMAL);
} }
} catch (IOException ignored) {
} }
catch (IOException ignored) {}
textMessageQueue.clear(); textMessageQueue.clear();
binaryMessageQueue.clear(); binaryMessageQueue.clear();
errorFuture.completeExceptionally(new IOException("WebSocket 已关闭")); errorFuture.completeExceptionally(new IOException("WebSocket 已关闭"));
@@ -207,7 +209,8 @@ public class WebSocketClientManager implements Closeable
// 保存会话 // 保存会话
WebSocketClientManager.this.session = session; WebSocketClientManager.this.session = session;
this.stopWatch.start(); this.stopWatch.start();
log.info("ws连接成功, 目标URI: {}, 连接时间: {}", targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN)); log.info("ws连接成功, 目标URI: {}, 连接时间: {}", targetUri,
DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN));
} }
/** /**
@@ -264,18 +267,19 @@ public class WebSocketClientManager implements Closeable
stopWatch.stop(); stopWatch.stop();
} }
log.info("ws连接关闭, 目标URI: {}, 关闭时间: {}, 连接总时长: {}s", log.info("ws连接关闭, 目标URI: {}, 关闭时间: {}, 连接总时长: {}s",
targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN), DateUtils.millsToSecond(stopWatch.getTotalTimeMillis())); targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN),
DateUtils.millsToSecond(stopWatch.getTotalTimeMillis()));
} }
} }
public static class Builder { public static class Builder {
private String uri; // 目标 WS URI private String uri; // 目标 WS URI
private long connectTimeout = 3; // 请求连接等待时间 private long connectTimeout = 3; // 请求连接等待时间
private TimeUnit connectUnit = TimeUnit.SECONDS; // 请求连接等待时间单位 private TimeUnit connectUnit = TimeUnit.SECONDS; // 请求连接等待时间单位
private long maxSessionDuration = 5; // 最大连线时间,默认5秒 private long maxSessionDuration = 5; // 最大连线时间,默认5秒
private TimeUnit maxSessionDurationUnit = TimeUnit.SECONDS; // 最大连线时间单位 private TimeUnit maxSessionDurationUnit = TimeUnit.SECONDS; // 最大连线时间单位
private int queueCapacity = 100; // 消息队列容量 private int queueCapacity = 100; // 消息队列容量
private WebSocketHttpHeaders headers; // 请求头 private WebSocketHttpHeaders headers; // 请求头
/** /**
* 目标 WS URI * 目标 WS URI
@@ -307,7 +311,8 @@ public class WebSocketClientManager implements Closeable
return this; return this;
} }
public WebSocketClientManager build() throws InterruptedException, ExecutionException, TimeoutException, IOException { public WebSocketClientManager build()
throws InterruptedException, ExecutionException, TimeoutException, IOException {
return WebSocketClientManager.build(this); return WebSocketClientManager.build(this);
} }
@@ -1,6 +0,0 @@
-- 更新intent_llmM供应器
update `ai_model_provider` set fields = '[{"key":"llm","label":"LLM模型","type":"string"},{"key":"functions","label":"函数列表","type":"dict","dict_name":"functions"}]' where id = 'SYSTEM_Intent_intent_llm';
-- 更新ChatGLMLLM的意图识别配置
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\", \"functions\": \"get_weather;get_news_from_newsnow;play_music\"}' where id = 'Intent_intent_llm';
-- 更新函数调用意图识别配置
UPDATE `ai_model_config` SET config_json = REPLACE(config_json, ';get_news;', ';get_news_from_newsnow;') WHERE id = 'Intent_function_call';
@@ -0,0 +1,189 @@
-- ===============================
-- 一、在ai_model_provider中插入plugin 记录
-- ===============================
START TRANSACTION;
-- intent_llm和function_call不设置函数列表
update `ai_model_provider` set fields = '[{"key":"llm","label":"LLM模型","type":"string"}]' where id = 'SYSTEM_Intent_intent_llm';
update `ai_model_provider` set fields = '[]' where id = 'SYSTEM_Intent_function_call';
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\"}' where id = 'Intent_intent_llm';
UPDATE `ai_model_config` SET config_json = '{\"type\": \"function_call\"}' WHERE id = 'Intent_function_call';
delete from ai_model_provider where model_type = 'Plugin';
-- 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());
-- 6. 本地播放音乐
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(),
20, 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_CHINANEWS',
'Plugin',
'get_news_from_chinanews',
'中新网新闻',
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', 'society_rss_url',
'type', 'string',
'label', '社会新闻 RSS 地址',
'default',
'https://www.chinanews.com.cn/rss/society.xml'
),
JSON_OBJECT(
'key', 'world_rss_url',
'type', 'string',
'label', '国际新闻 RSS 地址',
'default',
'https://www.chinanews.com.cn/rss/world.xml'
),
JSON_OBJECT(
'key', 'finance_rss_url',
'type', 'string',
'label', '财经新闻 RSS 地址',
'default',
'https://www.chinanews.com.cn/rss/finance.xml'
)
),
30, 0, NOW(), 0, NOW());
-- 3. 新闻订阅
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_NEWS_NEWSNOW',
'Plugin',
'get_news_from_newsnow',
'newsnow新闻聚合',
JSON_ARRAY(
JSON_OBJECT(
'key', 'url',
'type', 'string',
'label', '接口地址',
'default',
'https://newsnow.busiyi.world/api/s?id='
)
),
40, 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_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')
)
),
50, 0, NOW(), 0, NOW());
-- 5. 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(),
60, 0, NOW(), 0, NOW());
-- 5. HomeAssistant 音乐播放
INSERT INTO ai_model_provider (id, model_type, provider_code, name, fields,
sort, creator, create_date, updater, update_date)
VALUES ('SYSTEM_PLUGIN_HA_PLAY_MUSIC',
'Plugin',
'hass_play_music',
'HomeAssistant音乐播放',
JSON_ARRAY(),
70, 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;
@@ -107,13 +107,6 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202505081146.sql path: classpath:db/changelog/202505081146.sql
- changeSet:
id: 202505091409
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505091409.sql
- changeSet: - changeSet:
id: 202505091555 id: 202505091555
author: whosmyqueen author: whosmyqueen
@@ -170,6 +163,13 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202505271414.sql path: classpath:db/changelog/202505271414.sql
- changeSet:
id: 202505292203
author: CAIXYPROMISE
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505292203.sql
- changeSet: - changeSet:
id: 202506010920 id: 202506010920
author: hrz author: hrz
@@ -5,4 +5,72 @@
<select id="getDeviceCountByAgentId" resultType="java.lang.Integer"> <select id="getDeviceCountByAgentId" resultType="java.lang.Integer">
SELECT COUNT(*) FROM ai_device WHERE agent_id = #{agentId} SELECT COUNT(*) FROM ai_device WHERE agent_id = #{agentId}
</select> </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.vllm_model_id AS vllmModelId,
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> </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() }).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()
}
} }
@@ -1,5 +1,5 @@
<template> <template>
<el-dialog :title="title" :visible.sync="dialogVisible" @close="handleClose" @open="handleOpen"> <el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose" @open="handleOpen">
<el-form ref="form" :model="form" :rules="rules" label-width="100px"> <el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="固件名称" prop="firmwareName"> <el-form-item label="固件名称" prop="firmwareName">
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input> <el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
@@ -1,5 +1,5 @@
<template> <template>
<el-drawer :visible.sync="dialogVisible" direction="rtl" size="50%" :wrapperClosable="false" :withHeader="false"> <el-drawer :visible.sync="dialogVisible" direction="rtl" size="80%" :wrapperClosable="false" :withHeader="false">
<!-- 自定义标题区域 --> <!-- 自定义标题区域 -->
<div class="custom-header"> <div class="custom-header">
<div class="header-left"> <div class="header-left">
@@ -16,15 +16,18 @@
<el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button> <el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button>
</div> </div>
<div class="function-list"> <div class="function-list">
<div v-for="func in unselected" :key="func.name" class="function-item"> <div v-if="unselected.length">
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox> <div v-for="func in unselected" :key="func.name" class="function-item">
<div class="func-tag" @click="handleFunctionClick(func)"> <el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)"
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div> @click.native.stop></el-checkbox>
<span>{{ func.name }}</span> <div class="func-tag" @click="handleFunctionClick(func)">
<div class="color-dot" :style="{ backgroundColor: getFunctionColor(func.name) }"></div>
<span>{{ func.name }}</span>
</div>
</div> </div>
<el-tooltip class="item" effect="dark" :content="func.description || '暂无功能描述'" placement="top"> </div>
<img src="@/assets/home/info.png" alt="" class="info-icon"> <div v-else style="display: flex; justify-content: center; align-items: center;">
</el-tooltip> <el-empty description="没有更多的插件了" />
</div> </div>
</div> </div>
</div> </div>
@@ -36,26 +39,62 @@
<el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button> <el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button>
</div> </div>
<div class="function-list"> <div class="function-list">
<div v-for="func in selectedList" :key="func.name" class="function-item"> <div v-if="selectedList.length > 0">
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox> <div v-for="func in selectedList" :key="func.name" class="function-item">
<div class="func-tag" @click="handleFunctionClick(func)"> <el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)"
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div> @click.native.stop></el-checkbox>
<span>{{ func.name }}</span> <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> </div>
<div v-else style="display: flex; justify-content: center; align-items: center;">
<el-empty description="请选择插件功能" />
</div>
</div> </div>
</div> </div>
<!-- 右侧参数配置 --> <!-- 右侧参数配置 -->
<div class="params-column"> <div class="params-column">
<h4 v-if="currentFunction" class="column-title">参数配置 - {{ currentFunction.name }}</h4> <h4 v-if="currentFunction" class="column-title">参数配置 - {{ currentFunction.name }}</h4>
<div v-if="currentFunction" class="params-container"> <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 :model="currentFunction" class="param-form">
<el-form-item v-for="(value, key) in currentFunction.params" :key="key" :label="key" class="param-item"> <!-- 遍历 fieldsMeta而不是 params keys -->
<el-input v-model="currentFunction.params[key]" size="mini" class="param-input" @change="(val) => handleParamChange(currentFunction, key, val)"/> <div v-if="currentFunction.fieldsMeta.length == 0">
</el-form-item> <el-empty :description="currentFunction.name + ' 无需配置参数'" />
</el-form> </div>
</div> <el-form-item v-for="field in currentFunction.fieldsMeta" :key="field.key" :label="field.label"
class="param-item" :class="{ 'textarea-field': field.type === 'array' || field.type === 'json' }">
<template #label>
<span style="font-size: 16px; margin-right: 6px;">{{ field.label }}</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" v-model="currentFunction.params[field.key]"
@change="val => handleParamChange(currentFunction, field.key, val)" />
<!-- 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 v-else class="empty-tip">请选择已配置的功能进行参数设置</div>
</div> </div>
</div> </div>
@@ -74,24 +113,19 @@ export default {
functions: { functions: {
type: Array, type: Array,
default: () => [] default: () => []
},
allFunctions: {
type: Array,
default: () => []
} }
}, },
data() { data() {
return { return {
textCache: {},
dialogVisible: this.value, dialogVisible: this.value,
selectedNames: [], selectedNames: [],
currentFunction: null, currentFunction: null,
modifiedFunctions: {}, 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: [ functionColorMap: [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#FF6B6B', '#4ECDC4', '#45B7D1',
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E' '#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
@@ -111,10 +145,37 @@ export default {
} }
}, },
watch: { watch: {
value(newVal) { currentFunction(newFn) {
this.dialogVisible = newVal; if (!newFn) return;
if (newVal) { // 对每个字段,如果是 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.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; this.currentFunction = this.selectedList[0] || null;
} }
}, },
@@ -123,14 +184,32 @@ export default {
} }
}, },
methods: { 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) { handleFunctionClick(func) {
if (this.selectedNames.includes(func.name)) { if (this.selectedNames.includes(func.name)) {
this.loading = true; const tempFunc = this.tempFunctions[func.name];
setTimeout(() => { this.currentFunction = tempFunc ? tempFunc : func;
const tempFunc = this.tempFunctions[func.name];
this.currentFunction = tempFunc ? tempFunc : JSON.parse(JSON.stringify(func));
this.loading = false;
}, 300);
} }
}, },
handleParamChange(func, key, value) { handleParamChange(func, key, value) {
@@ -185,23 +264,31 @@ export default {
const selected = this.selectedList.map(f => { const selected = this.selectedList.map(f => {
const modified = this.modifiedFunctions[f.name]; const modified = this.modifiedFunctions[f.name];
return modified || f; return {
}).map(f => ({ id: f.id,
...f, name: f.name,
params: JSON.parse(JSON.stringify(f.params)) params: modified
})); ? { ...modified.params }
: { ...f.params }
}
});
this.$emit('update-functions', selected); this.$emit('update-functions', selected);
this.dialogVisible = false; this.dialogVisible = false;
this.$message.success('配置保存成功');
// 通知父组件对话框已关闭且已保存 // 通知父组件对话框已关闭且已保存
this.$emit('dialog-closed', true); this.$emit('dialog-closed', true);
}, },
getFunctionColor(name) { getFunctionColor(name) {
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0); 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> </script>
@@ -209,7 +296,7 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
.function-manager { .function-manager {
display: grid; 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; gap: 12px;
height: calc(70vh - 60px); height: calc(70vh - 60px);
} }
@@ -248,6 +335,7 @@ export default {
overflow-y: auto; overflow-y: auto;
border-right: 1px solid #EBEEF5; border-right: 1px solid #EBEEF5;
scrollbar-width: none; scrollbar-width: none;
overflow-x: hidden;
} }
.function-column::-webkit-scrollbar { .function-column::-webkit-scrollbar {
@@ -257,7 +345,7 @@ export default {
.function-list { .function-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 8px;
} }
.function-item { .function-item {
@@ -317,9 +405,31 @@ export default {
} }
.param-form { .param-form {
.param-item {
font-size: 16px;
&.textarea-field {
::v-deep .el-form-item__content {
margin-left: 0 !important;
display: block;
width: 100%;
}
::v-deep .el-form-item__label {
display: block;
width: 100% !important;
margin-bottom: 8px;
}
}
}
.param-input {
width: 100%;
}
::v-deep .el-form-item { ::v-deep .el-form-item {
display: flex; display: flex;
align-items: center; flex-direction: column;
margin-bottom: 12px; margin-bottom: 12px;
.el-form-item__label { .el-form-item__label {
@@ -356,9 +466,6 @@ export default {
text-align: center; text-align: center;
} }
.param-input {
width: 100%;
}
.drawer-footer { .drawer-footer {
position: absolute; position: absolute;
@@ -1,5 +1,5 @@
<template> <template>
<el-dialog :visible.sync="dialogVisible" width="57%" center custom-class="custom-dialog" :show-close="false" <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="57%" center custom-class="custom-dialog" :show-close="false"
class="center-dialog" > class="center-dialog" >
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;"> <div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;"> <div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
@@ -1,5 +1,5 @@
<template> <template>
<el-dialog :visible="visible" @update:visible="handleVisibleChange" width="57%" center custom-class="custom-dialog" <el-dialog :visible="visible" :close-on-click-modal="false" @update:visible="handleVisibleChange" width="57%" center custom-class="custom-dialog"
:show-close="false" class="center-dialog"> :show-close="false" class="center-dialog">
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;"> <div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
@@ -18,7 +18,7 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="供应器编码" prop="providerCode" style="flex: 1;"> <el-form-item label="编码" prop="providerCode" style="flex: 1;">
<el-input v-model="form.providerCode" placeholder="请输入供应器编码" class="custom-input-bg"></el-input> <el-input v-model="form.providerCode" placeholder="请输入供应器编码" class="custom-input-bg"></el-input>
</el-form-item> </el-form-item>
</div> </div>
@@ -87,6 +87,7 @@
<el-option label="数字" value="number"></el-option> <el-option label="数字" value="number"></el-option>
<el-option label="布尔值" value="boolean"></el-option> <el-option label="布尔值" value="boolean"></el-option>
<el-option label="字典" value="dict"></el-option> <el-option label="字典" value="dict"></el-option>
<el-option label="分号分割的列表" value="array"></el-option>
</el-select> </el-select>
</template> </template>
<template v-else> <template v-else>
@@ -97,10 +98,10 @@
<el-table-column label="默认值"> <el-table-column label="默认值">
<template slot-scope="scope"> <template slot-scope="scope">
<template v-if="scope.row.editing"> <template v-if="scope.row.editing">
<el-input v-model="scope.row.default_value" placeholder="请输入默认值"></el-input> <el-input v-model="scope.row.default" placeholder="请输入默认值"></el-input>
</template> </template>
<template v-else> <template v-else>
{{ scope.row.default_value }} {{ scope.row.default }}
</template> </template>
</template> </template>
</el-table-column> </el-table-column>
@@ -161,7 +162,8 @@ export default {
'string': '字符串', 'string': '字符串',
'number': '数字', 'number': '数字',
'boolean': '布尔值', 'boolean': '布尔值',
'dict': '字典' 'dict': '字典',
'array': '分号分割的列表'
}; };
return typeMap[type]; return typeMap[type];
}, },
@@ -220,7 +222,7 @@ export default {
key: '', key: '',
label: '', label: '',
type: 'string', type: 'string',
default_value: '', default: '',
selected: false, selected: false,
editing: true editing: true
}); });
@@ -143,9 +143,11 @@ export default {
{ value: "ASR", label: "语音识别" }, { value: "ASR", label: "语音识别" },
{ value: "TTS", label: "语音合成" }, { value: "TTS", label: "语音合成" },
{ value: "LLM", label: "大语言模型" }, { value: "LLM", label: "大语言模型" },
{ value: "VLLM", label: "视觉大语言模型" },
{ value: "Intent", label: "意图识别" }, { value: "Intent", label: "意图识别" },
{ value: "Memory", label: "记忆模块" }, { value: "Memory", label: "记忆模块" },
{ value: "VAD", label: "语音活动检测" } { value: "VAD", label: "语音活动检测" },
{ value: "Plugin", label: "插件工具" }
], ],
currentPage: 1, currentPage: 1,
loading: false, loading: false,
+73 -40
View File
@@ -97,19 +97,12 @@
popper-class="custom-tooltip"> popper-class="custom-tooltip">
<div slot="content"> <div slot="content">
<div><strong>功能名称:</strong> {{ func.name }}</div> <div><strong>功能名称:</strong> {{ func.name }}</div>
<div v-if="Object.keys(func.params).length > 0">
<strong>参数配置:</strong>
<div v-for="(value, key) in func.params" :key="key">
{{ key }}: {{ value }}
</div>
</div>
<div v-else>无参数配置</div>
</div> </div>
<div class="icon-dot" :style="{ backgroundColor: getFunctionColor(func.name) }"> <div class="icon-dot" :style="{ backgroundColor: getFunctionColor(func.name) }">
{{ func.name.charAt(0) }} {{ func.name.charAt(0) }}
</div> </div>
</el-tooltip> </el-tooltip>
<el-button class="edit-function-btn" @click="showFunctionDialog = true" <el-button class="edit-function-btn" @click="openFunctionDialog"
:class="{ 'active-btn': showFunctionDialog }"> :class="{ 'active-btn': showFunctionDialog }">
编辑功能 编辑功能
</el-button> </el-button>
@@ -138,7 +131,7 @@
</div> </div>
</div> </div>
<function-dialog v-model="showFunctionDialog" :functions="currentFunctions" <function-dialog v-model="showFunctionDialog" :functions="currentFunctions" :all-functions="allFunctions"
@update-functions="handleUpdateFunctions" @dialog-closed="handleDialogClosed" /> @update-functions="handleUpdateFunctions" @dialog-closed="handleDialogClosed" />
</div> </div>
</template> </template>
@@ -192,12 +185,8 @@ export default {
'#FF6B6B', '#4ECDC4', '#45B7D1', '#FF6B6B', '#4ECDC4', '#45B7D1',
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E' '#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
], ],
allFunctions: [ allFunctions: [],
{ name: '天气', params: {} }, originalFunctions: [],
{ name: '新闻', params: {} },
{ name: '工具', params: {} },
{ name: '退出', params: {} }
],
} }
}, },
methods: { methods: {
@@ -222,7 +211,12 @@ export default {
langCode: this.form.langCode, langCode: this.form.langCode,
language: this.form.language, language: this.form.language,
sort: this.form.sort, 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) { if (data.code === 0) {
@@ -269,7 +263,8 @@ export default {
message: '配置已重置', message: '配置已重置',
showClose: true showClose: true
}) })
}).catch(() => { }); }).catch(() => {
});
}, },
fetchTemplates() { fetchTemplates() {
Api.agent.getAgentTemplate(({ data }) => { Api.agent.getAgentTemplate(({ data }) => {
@@ -335,7 +330,33 @@ export default {
intentModelId: data.data.intentModelId 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 { } else {
this.$message.error(data.msg || '获取配置失败'); this.$message.error(data.msg || '获取配置失败');
} }
@@ -373,17 +394,15 @@ export default {
}, },
getFunctionColor(name) { getFunctionColor(name) {
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0); 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) { showFunctionIcons(type) {
// TODO 暂时不放出来 return type === 'Intent' &&
return false; this.form.model.intentModelId !== 'Intent_nointent';
// return type === 'Intent' &&
// this.form.model.intentModelId !== 'Intent_nointent';
}, },
handleModelChange(type, value) { handleModelChange(type, value) {
if (type === 'Intent' && value !== 'Intent_nointent') { if (type === 'Intent' && value !== 'Intent_nointent') {
this.fetchFunctionList(); this.fetchAllFunctions();
} }
if (type === 'Memory' && value === 'Memory_nomem') { if (type === 'Memory' && value === 'Memory_nomem') {
this.form.chatHistoryConf = 0; this.form.chatHistoryConf = 0;
@@ -392,28 +411,44 @@ export default {
this.form.chatHistoryConf = 2; this.form.chatHistoryConf = 2;
} }
}, },
fetchFunctionList() { fetchAllFunctions() {
// 使用假数据代替API调用 return new Promise((resolve, reject) => {
return new Promise(resolve => { Api.model.getPluginFunctionList(null, ({ data }) => {
setTimeout(() => { if (data.code === 0) {
this.currentFunctions = [ this.allFunctions = data.data.map(item => {
{ name: '天气', params: { city: '北京' } }, const meta = JSON.parse(item.fields || '[]');
{ name: '新闻', params: { type: '科技' } } const params = meta.reduce((m, f) => {
]; m[f.key] = f.default;
resolve(); return m;
}, 500); }, {});
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) { handleUpdateFunctions(selected) {
this.currentFunctions = selected; this.currentFunctions = selected;
console.log('保存的功能列表:', selected);
this.$message.success('功能配置已保存');
}, },
handleDialogClosed(saved) { handleDialogClosed(saved) {
if (!saved) { if (!saved) {
// 如果未保存,恢复原始功能列表
this.currentFunctions = JSON.parse(JSON.stringify(this.originalFunctions)); this.currentFunctions = JSON.parse(JSON.stringify(this.originalFunctions));
} else {
this.originalFunctions = JSON.parse(JSON.stringify(this.currentFunctions));
} }
this.showFunctionDialog = false;
}, },
updateChatHistoryConf() { updateChatHistoryConf() {
if (this.form.model.memModelId === 'Memory_nomem') { if (this.form.model.memModelId === 'Memory_nomem') {
@@ -446,9 +481,7 @@ export default {
const agentId = this.$route.query.agentId; const agentId = this.$route.query.agentId;
if (agentId) { if (agentId) {
this.fetchAgentConfig(agentId); this.fetchAgentConfig(agentId);
this.fetchFunctionList().then(() => { this.fetchAllFunctions();
this.originalFunctions = JSON.parse(JSON.stringify(this.currentFunctions));
});
} }
this.fetchModelOptions(); this.fetchModelOptions();
this.fetchTemplates(); this.fetchTemplates();
+3 -4
View File
@@ -117,10 +117,9 @@ plugins:
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/ # 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
get_news_from_chinanews: get_news_from_chinanews:
default_rss_url: "https://www.chinanews.com.cn/rss/society.xml" default_rss_url: "https://www.chinanews.com.cn/rss/society.xml"
category_urls: society_rss_url: "https://www.chinanews.com.cn/rss/society.xml"
society: "https://www.chinanews.com.cn/rss/society.xml" world_rss_url: "https://www.chinanews.com.cn/rss/world.xml"
world: "https://www.chinanews.com.cn/rss/world.xml" finance_rss_url: "https://www.chinanews.com.cn/rss/finance.xml"
finance: "https://www.chinanews.com.cn/rss/finance.xml"
get_news_from_newsnow: {"url": "https://newsnow.busiyi.world/api/s?id="} get_news_from_newsnow: {"url": "https://newsnow.busiyi.world/api/s?id="}
home_assistant: home_assistant:
devices: devices:
+11 -3
View File
@@ -453,9 +453,17 @@ class ConnectionHandler:
if private_config.get("Intent", None) is not None: if private_config.get("Intent", None) is not None:
init_intent = True init_intent = True
self.config["Intent"] = private_config["Intent"] self.config["Intent"] = private_config["Intent"]
self.config["selected_module"]["Intent"] = private_config[ model_intent = private_config.get("selected_module", {}).get("Intent", {})
"selected_module" self.config["selected_module"]["Intent"] = model_intent
]["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
self.config["Intent"][self.config["selected_module"]["Intent"]][
"functions"
] = plugin_from_server.keys()
if private_config.get("prompt", None) is not None: if private_config.get("prompt", None) is not None:
self.config["prompt"] = private_config["prompt"] self.config["prompt"] = private_config["prompt"]
if private_config.get("summaryMemory", None) is not None: if private_config.get("summaryMemory", None) is not None:
@@ -61,7 +61,6 @@ class FunctionHandler:
self.function_registry.register_function("plugin_loader") self.function_registry.register_function("plugin_loader")
self.function_registry.register_function("get_time") self.function_registry.register_function("get_time")
self.function_registry.register_function("get_lunar") self.function_registry.register_function("get_lunar")
# self.function_registry.register_function("handle_speaker_volume_or_screen_brightness")
def register_config_functions(self): def register_config_functions(self):
"""注册配置中的函数,可以不同客户端使用不同的配置""" """注册配置中的函数,可以不同客户端使用不同的配置"""
@@ -23,7 +23,9 @@ class LLMProvider(LLMProviderBase):
self.bot_id = str(config.get("bot_id")) self.bot_id = str(config.get("bot_id"))
self.user_id = str(config.get("user_id")) self.user_id = str(config.get("user_id"))
self.session_conversation_map = {} # 存储session_id和conversation_id的映射 self.session_conversation_map = {} # 存储session_id和conversation_id的映射
check_model_key("CozeLLM", self.personal_access_token) model_key_msg = check_model_key("CozeLLM", self.personal_access_token)
if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
coze_api_token = self.personal_access_token coze_api_token = self.personal_access_token
@@ -15,7 +15,9 @@ class LLMProvider(LLMProviderBase):
self.mode = config.get("mode", "chat-messages") self.mode = config.get("mode", "chat-messages")
self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip("/") self.base_url = config.get("base_url", "https://api.dify.ai/v1").rstrip("/")
self.session_conversation_map = {} # 存储session_id和conversation_id的映射 self.session_conversation_map = {} # 存储session_id和conversation_id的映射
check_model_key("DifyLLM", self.api_key) model_key_msg = check_model_key("DifyLLM", self.api_key)
if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
try: try:
@@ -14,7 +14,9 @@ class LLMProvider(LLMProviderBase):
self.base_url = config.get("base_url") self.base_url = config.get("base_url")
self.detail = config.get("detail", False) self.detail = config.get("detail", False)
self.variables = config.get("variables", {}) self.variables = config.get("variables", {})
check_model_key("FastGPTLLM", self.api_key) model_key_msg = check_model_key("FastGPTLLM", self.api_key)
if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
try: try:
@@ -73,8 +73,9 @@ class LLMProvider(LLMProviderBase):
http_proxy = cfg.get("http_proxy") http_proxy = cfg.get("http_proxy")
https_proxy = cfg.get("https_proxy") https_proxy = cfg.get("https_proxy")
if not check_model_key("LLM", self.api_key): model_key_msg = check_model_key("LLM", self.api_key)
raise ValueError("无效的Gemini API Key,请检查是否配置正确") if model_key_msg:
log.bind(tag=TAG).error(model_key_msg)
if http_proxy or https_proxy: if http_proxy or https_proxy:
log.bind(tag=TAG).info( log.bind(tag=TAG).info(
@@ -21,20 +21,27 @@ class LLMProvider(LLMProviderBase):
"max_tokens": (500, int), "max_tokens": (500, int),
"temperature": (0.7, lambda x: round(float(x), 1)), "temperature": (0.7, lambda x: round(float(x), 1)),
"top_p": (1.0, lambda x: round(float(x), 1)), "top_p": (1.0, lambda x: round(float(x), 1)),
"frequency_penalty": (0, lambda x: round(float(x), 1)) "frequency_penalty": (0, lambda x: round(float(x), 1)),
} }
for param, (default, converter) in param_defaults.items(): for param, (default, converter) in param_defaults.items():
value = config.get(param) value = config.get(param)
try: try:
setattr(self, param, converter(value) if value not in (None, "") else default) setattr(
self,
param,
converter(value) if value not in (None, "") else default,
)
except (ValueError, TypeError): except (ValueError, TypeError):
setattr(self, param, default) setattr(self, param, default)
logger.debug( logger.debug(
f"意图识别参数初始化: {self.temperature}, {self.max_tokens}, {self.top_p}, {self.frequency_penalty}") f"意图识别参数初始化: {self.temperature}, {self.max_tokens}, {self.top_p}, {self.frequency_penalty}"
)
check_model_key("LLM", self.api_key) model_key_msg = check_model_key("LLM", self.api_key)
if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url) self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
def response(self, session_id, dialogue, **kwargs): def response(self, session_id, dialogue, **kwargs):
@@ -46,7 +53,9 @@ class LLMProvider(LLMProviderBase):
max_tokens=kwargs.get("max_tokens", self.max_tokens), max_tokens=kwargs.get("max_tokens", self.max_tokens),
temperature=kwargs.get("temperature", self.temperature), temperature=kwargs.get("temperature", self.temperature),
top_p=kwargs.get("top_p", self.top_p), top_p=kwargs.get("top_p", self.top_p),
frequency_penalty=kwargs.get("frequency_penalty", self.frequency_penalty), frequency_penalty=kwargs.get(
"frequency_penalty", self.frequency_penalty
),
) )
is_active = True is_active = True
@@ -84,10 +93,12 @@ class LLMProvider(LLMProviderBase):
for chunk in stream: for chunk in stream:
# 检查是否存在有效的choice且content不为空 # 检查是否存在有效的choice且content不为空
if getattr(chunk, "choices", None): if getattr(chunk, "choices", None):
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls yield chunk.choices[0].delta.content, chunk.choices[
0
].delta.tool_calls
# 存在 CompletionUsage 消息时,生成 Token 消耗 log # 存在 CompletionUsage 消息时,生成 Token 消耗 log
elif isinstance(getattr(chunk, 'usage', None), CompletionUsage): elif isinstance(getattr(chunk, "usage", None), CompletionUsage):
usage_info = getattr(chunk, 'usage', None) usage_info = getattr(chunk, "usage", None)
logger.bind(tag=TAG).info( logger.bind(tag=TAG).info(
f"Token 消耗:输入 {getattr(usage_info, 'prompt_tokens', '未知')}" f"Token 消耗:输入 {getattr(usage_info, 'prompt_tokens', '未知')}"
f"输出 {getattr(usage_info, 'completion_tokens', '未知')}" f"输出 {getattr(usage_info, 'completion_tokens', '未知')}"
@@ -12,10 +12,6 @@ class MemoryProviderBase(ABC):
def set_llm(self, llm): def set_llm(self, llm):
self.llm = llm self.llm = llm
# 获取模型名称和类型信息
model_name = getattr(llm, "model_name", str(llm.__class__.__name__))
# 记录更详细的日志
logger.bind(tag=TAG).info(f"记忆总结设置LLM: {model_name}")
@abstractmethod @abstractmethod
async def save_memory(self, msgs): async def save_memory(self, msgs):
@@ -12,12 +12,14 @@ class MemoryProvider(MemoryProviderBase):
super().__init__(config) super().__init__(config)
self.api_key = config.get("api_key", "") self.api_key = config.get("api_key", "")
self.api_version = config.get("api_version", "v1.1") self.api_version = config.get("api_version", "v1.1")
have_key = check_model_key("Mem0ai", self.api_key) model_key_msg = check_model_key("Mem0ai", self.api_key)
if not have_key: if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
self.use_mem0 = False self.use_mem0 = False
return return
else: else:
self.use_mem0 = True self.use_mem0 = True
try: try:
self.client = MemoryClient(api_key=self.api_key) self.client = MemoryClient(api_key=self.api_key)
logger.bind(tag=TAG).info("成功连接到 Mem0ai 服务") logger.bind(tag=TAG).info("成功连接到 Mem0ai 服务")
@@ -37,7 +37,9 @@ class TTSProvider(TTSProviderBase):
self.api_url = config.get("api_url") self.api_url = config.get("api_url")
self.authorization = config.get("authorization") self.authorization = config.get("authorization")
self.header = {"Authorization": f"{self.authorization}{self.access_token}"} self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
check_model_key("TTS", self.access_token) model_key_msg = check_model_key("TTS", self.access_token)
if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
request_json = { request_json = {
@@ -90,8 +90,9 @@ class TTSProvider(TTSProviderBase):
self.format = config.get("response_format", "wav") self.format = config.get("response_format", "wav")
self.audio_file_type = config.get("response_format", "wav") self.audio_file_type = config.get("response_format", "wav")
self.api_key = config.get("api_key", "YOUR_API_KEY") self.api_key = config.get("api_key", "YOUR_API_KEY")
have_key = check_model_key("FishSpeech TTS", self.api_key) model_key_msg = check_model_key("FishSpeech TTS", self.api_key)
if not have_key: if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
return return
self.normalize = str(config.get("normalize", True)).lower() in ( self.normalize = str(config.get("normalize", True)).lower() in (
"true", "true",
@@ -157,7 +157,9 @@ class TTSProvider(TTSProviderBase):
self.opus_encoder = opus_encoder_utils.OpusEncoderUtils( self.opus_encoder = opus_encoder_utils.OpusEncoderUtils(
sample_rate=16000, channels=1, frame_size_ms=60 sample_rate=16000, channels=1, frame_size_ms=60
) )
check_model_key("TTS", self.access_token) model_key_msg = check_model_key("TTS", self.access_token)
if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
async def open_audio_channels(self, conn): async def open_audio_channels(self, conn):
try: try:
@@ -166,7 +166,9 @@ class TTSProvider(TTSProviderBase):
) as resp: ) as resp:
if resp.status != 200: if resp.status != 200:
logger.error(f"TTS请求失败: {resp.status}, {await resp.text()}") logger.bind(tag=TAG).error(
f"TTS请求失败: {resp.status}, {await resp.text()}"
)
self.tts_audio_queue.put((SentenceType.LAST, [], None)) self.tts_audio_queue.put((SentenceType.LAST, [], None))
return return
@@ -229,7 +231,7 @@ class TTSProvider(TTSProviderBase):
self._process_before_stop_play_files() self._process_before_stop_play_files()
except Exception as e: except Exception as e:
logger.error(f"TTS请求异常: {e}") logger.bind(tag=TAG).error(f"TTS请求异常: {e}")
self.tts_audio_queue.put((SentenceType.LAST, [], None)) self.tts_audio_queue.put((SentenceType.LAST, [], None))
def to_tts(self, text: str) -> list: def to_tts(self, text: str) -> list:
@@ -263,7 +265,7 @@ class TTSProvider(TTSProviderBase):
self.api_url, params=params, headers=headers, timeout=5 self.api_url, params=params, headers=headers, timeout=5
) as response: ) as response:
if response.status_code != 200: if response.status_code != 200:
logger.error( logger.bind(tag=TAG).error(
f"TTS请求失败: {response.status_code}, {response.text}" f"TTS请求失败: {response.status_code}, {response.text}"
) )
return [] return []
@@ -299,5 +301,5 @@ class TTSProvider(TTSProviderBase):
return opus_datas return opus_datas
except Exception as e: except Exception as e:
logger.error(f"TTS请求异常: {e}") logger.bind(tag=TAG).error(f"TTS请求异常: {e}")
return [] return []
@@ -25,7 +25,9 @@ class TTSProvider(TTSProviderBase):
self.speed = float(speed) if speed else 1.0 self.speed = float(speed) if speed else 1.0
self.output_file = config.get("output_dir", "tmp/") self.output_file = config.get("output_dir", "tmp/")
check_model_key("TTS", self.api_key) model_key_msg = check_model_key("TTS", self.api_key)
if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
async def text_to_speak(self, text, output_file): async def text_to_speak(self, text, output_file):
headers = { headers = {
@@ -34,7 +34,9 @@ class VLLMProvider(VLLMProviderBase):
except (ValueError, TypeError): except (ValueError, TypeError):
setattr(self, param, default) setattr(self, param, default)
check_model_key("VLLM", self.api_key) model_key_msg = check_model_key("VLLM", self.api_key)
if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url) self.client = openai.OpenAI(api_key=self.api_key, base_url=self.base_url)
def response(self, question, base64_image): def response(self, question, base64_image):
+7 -7
View File
@@ -186,10 +186,8 @@ def remove_punctuation_and_length(text):
def check_model_key(modelType, modelKey): def check_model_key(modelType, modelKey):
if "" in modelKey: if "" in modelKey:
raise ValueError( return f"配置错误: {modelType} 的 API key 未设置,当前值为: {modelKey}"
"你还没配置" + modelType + "的密钥,请检查一下所使用的LLM是否配置了密钥" return None
)
return True
def parse_string_to_list(value, separator=";"): def parse_string_to_list(value, separator=";"):
@@ -785,7 +783,9 @@ def audio_bytes_to_data(audio_bytes, file_type, is_opus=True):
return p3.decode_opus_from_bytes(audio_bytes) return p3.decode_opus_from_bytes(audio_bytes)
else: else:
# 其他格式用pydub # 其他格式用pydub
audio = AudioSegment.from_file(BytesIO(audio_bytes), format=file_type, parameters=["-nostdin"]) audio = AudioSegment.from_file(
BytesIO(audio_bytes), format=file_type, parameters=["-nostdin"]
)
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2) audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
duration = len(audio) / 1000.0 duration = len(audio) / 1000.0
raw_data = audio.raw_data raw_data = audio.raw_data
@@ -838,11 +838,11 @@ def opus_datas_to_wav_bytes(opus_datas, sample_rate=16000, channels=1):
pcm = decoder.decode(opus_frame, frame_size) pcm = decoder.decode(opus_frame, frame_size)
pcm_datas.append(pcm) pcm_datas.append(pcm)
pcm_bytes = b''.join(pcm_datas) pcm_bytes = b"".join(pcm_datas)
# 写入wav字节流 # 写入wav字节流
wav_buffer = BytesIO() wav_buffer = BytesIO()
with wave.open(wav_buffer, 'wb') as wf: with wave.open(wav_buffer, "wb") as wf:
wf.setnchannels(channels) wf.setnchannels(channels)
wf.setsampwidth(2) # 16bit wf.setsampwidth(2) # 16bit
wf.setframerate(sample_rate) wf.setframerate(sample_rate)
@@ -120,16 +120,16 @@ def map_category(category_text):
# 类别映射字典,目前支持社会、国际、财经新闻,如需更多类型,参见配置文件 # 类别映射字典,目前支持社会、国际、财经新闻,如需更多类型,参见配置文件
category_map = { category_map = {
# 社会新闻 # 社会新闻
"社会": "society", "社会": "society_rss_url",
"社会新闻": "society", "社会新闻": "society_rss_url",
# 国际新闻 # 国际新闻
"国际": "world", "国际": "world_rss_url",
"国际新闻": "world", "国际新闻": "world_rss_url",
# 财经新闻 # 财经新闻
"财经": "finance", "财经": "finance_rss_url",
"财经新闻": "finance", "财经新闻": "finance_rss_url",
"金融": "finance", "金融": "finance_rss_url",
"经济": "finance", "经济": "finance_rss_url",
} }
# 转换为小写并去除空格 # 转换为小写并去除空格
@@ -205,8 +205,8 @@ def get_news_from_chinanews(
# 如果提供了类别,尝试从配置中获取对应的URL # 如果提供了类别,尝试从配置中获取对应的URL
rss_url = default_rss_url rss_url = default_rss_url
if mapped_category and mapped_category in rss_config.get("category_urls", {}): if mapped_category and mapped_category in rss_config:
rss_url = rss_config["category_urls"][mapped_category] rss_url = rss_config[mapped_category]
logger.bind(tag=TAG).info( logger.bind(tag=TAG).info(
f"获取新闻: 原始类别={category}, 映射类别={mapped_category}, URL={rss_url}" f"获取新闻: 原始类别={category}, 映射类别={mapped_category}, URL={rss_url}"
@@ -1,157 +0,0 @@
from config.logger import setup_logging
from plugins_func.register import register_function, ToolType, ActionResponse, Action
from core.handle.iotHandle import get_iot_status, send_iot_conn
import asyncio
TAG = __name__
logger = setup_logging()
async def _get_device_status(conn, device_name, device_type, property_name):
"""获取设备状态"""
status = await get_iot_status(conn, device_type, property_name)
if status is None:
raise Exception(f"你的设备不支持{device_name}控制")
return status
async def _set_device_property(
conn,
device_name,
device_type,
method_name,
property_name,
new_value=None,
action=None,
step=10,
):
"""设置设备属性"""
current_value = await _get_device_status(
conn, device_name, device_type, property_name
)
if action == "raise":
current_value += step
elif action == "lower":
current_value -= step
elif action == "set":
if new_value is None:
raise Exception(f"缺少{property_name}参数")
current_value = new_value
# 限制属性范围在0到100之间
current_value = max(0, min(100, current_value))
await send_iot_conn(conn, device_type, method_name, {property_name: current_value})
return current_value
def _handle_device_action(conn, func, success_message, error_message, *args, **kwargs):
"""处理设备操作的通用函数"""
future = asyncio.run_coroutine_threadsafe(func(conn, *args, **kwargs), conn.loop)
try:
result = future.result()
logger.bind(tag=TAG).info(f"{success_message}: {result}")
response = f"{success_message}{result}"
return ActionResponse(action=Action.RESPONSE, result=result, response=response)
except Exception as e:
logger.bind(tag=TAG).error(f"{error_message}: {e}")
response = f"{error_message}: {e}"
return ActionResponse(action=Action.RESPONSE, result=None, response=response)
# 设备控制
handle_device_function_desc = {
"type": "function",
"function": {
"name": "handle_speaker_volume_or_screen_brightness",
"description": (
"用户想要获取或者设置设备的音量/亮度大小,或者用户觉得声音/亮度过高或过低,或者用户想提高或降低音量/亮度。\n"
"**严格限制**:仅当用户明确操作 **Speaker(音量)或Screen(亮度)** 时才能调用此函数!\n"
"对于其他设备(如AC、Battery、Switch等),请不要调用此函数,而是继续正常的对话。\n\n"
"示例:\n"
"- 用户说『现在亮度多少』 → 调用函数:device_type: Screen, action: get\n"
"- 用户说『设置音量为50』 → 调用函数:device_type: Speaker, action: set, value: 50\n"
"- 用户说『亮度太高了』 → 调用函数:device_type: Screen, action: lower\n"
"- 用户说『调大音量』 → 调用函数:device_type: Speaker, action: raise\n\n"
"**拒绝调用示例**(应继续对话而非调用本函数):\n"
"- 用户说『空调调低一度』 → 不调用(设备类型为AC)\n"
"- 用户说『开关灯』 → 不调用(设备类型为Switch)\n"
"- 用户说『电量多少』 → 不调用(设备类型为Battery)\n"
),
"parameters": {
"type": "object",
"properties": {
"device_type": {
"type": "string",
"description": "设备类型,**严格限定为Speaker(音量)或Screen(亮度)**,其他设备类型禁止调用此函数",
"enum": ["Speaker", "Screen"],
},
"action": {
"type": "string",
"description": "动作名称,可选值:get(获取),set(设置),raise(提高),lower(降低)",
},
"value": {
"type": "integer",
"description": "值大小,可选值:0-100之间的整数",
},
},
"required": ["device_type", "action"],
},
},
}
@register_function(
"handle_speaker_volume_or_screen_brightness",
handle_device_function_desc,
ToolType.IOT_CTL,
)
def handle_speaker_volume_or_screen_brightness(
conn, device_type: str, action: str, value: int = None
):
# 检查value是否为中文值
if (
value is not None
and isinstance(value, str)
and any("\u4e00" <= char <= "\u9fff" for char in str(value))
):
raise Exception(
f"请直接告诉我要将{'音量' if device_type=='Speaker' else '亮度'}调整成多少"
)
if device_type == "Speaker":
method_name, property_name, device_name = "SetVolume", "volume", "音量"
elif device_type == "Screen":
method_name, property_name, device_name = "SetBrightness", "brightness", "亮度"
else:
raise Exception(f"未识别的设备类型: {device_type}")
if action not in ["get", "set", "raise", "lower"]:
raise Exception(f"未识别的动作名称: {action}")
if action == "get":
# get
return _handle_device_action(
conn,
_get_device_status,
f"当前{device_name}",
f"获取{device_name}失败",
device_name=device_name,
device_type=device_type,
property_name=property_name,
)
else:
# set, raise, lower
return _handle_device_action(
conn,
_set_device_property,
f"{device_name}已调整到",
f"{device_name}调整失败",
device_name=device_name,
device_type=device_type,
method_name=method_name,
property_name=property_name,
new_value=value,
action=action,
)
@@ -17,57 +17,78 @@ hass_get_state_function_desc = {
"properties": { "properties": {
"entity_id": { "entity_id": {
"type": "string", "type": "string",
"description": "需要操作的设备id,homeassistant里的entity_id" "description": "需要操作的设备id,homeassistant里的entity_id",
} }
}, },
"required": ["entity_id"] "required": ["entity_id"],
} },
} },
} }
@register_function("hass_get_state", hass_get_state_function_desc, ToolType.SYSTEM_CTL) @register_function("hass_get_state", hass_get_state_function_desc, ToolType.SYSTEM_CTL)
def hass_get_state(conn, entity_id=''): def hass_get_state(conn, entity_id=""):
try: try:
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
handle_hass_get_state(conn, entity_id), handle_hass_get_state(conn, entity_id), conn.loop
conn.loop
) )
ha_response = future.result() ha_response = future.result()
return ActionResponse( Action.REQLLM, ha_response , None ) return ActionResponse(Action.REQLLM, ha_response, None)
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}") logger.bind(tag=TAG).error(f"处理设置属性意图错误: {e}")
async def handle_hass_get_state(conn, entity_id): async def handle_hass_get_state(conn, entity_id):
HASS_CACHE = initialize_hass_handler(conn) ha_config = initialize_hass_handler(conn)
api_key = HASS_CACHE['api_key'] api_key = ha_config.get("api_key")
base_url = HASS_CACHE['base_url'] base_url = ha_config.get("base_url")
url = f"{base_url}/api/states/{entity_id}" url = f"{base_url}/api/states/{entity_id}"
headers = { headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
response = requests.get(url, headers=headers) response = requests.get(url, headers=headers)
if response.status_code == 200: if response.status_code == 200:
responsetext = '设备状态:' + response.json()['state'] + ' ' responsetext = "设备状态:" + response.json()["state"] + " "
logger.bind(tag=TAG).info(f"api返回内容: {response.json()}") logger.bind(tag=TAG).info(f"api返回内容: {response.json()}")
if 'media_title' in response.json()['attributes']: if "media_title" in response.json()["attributes"]:
responsetext = responsetext+ '正在播放的是:'+str(response.json()['attributes']['media_title'])+' ' responsetext = (
if 'volume_level' in response.json()['attributes']: responsetext
responsetext = responsetext+ '音量是:'+str(response.json()['attributes']['volume_level'])+' ' + "正在播放的是:"
if 'color_temp_kelvin' in response.json()['attributes']: + str(response.json()["attributes"]["media_title"])
responsetext = responsetext+ '色温是:'+str(response.json()['attributes']['color_temp_kelvin'])+' ' + " "
if 'rgb_color' in response.json()['attributes']: )
responsetext = responsetext+ 'rgb颜色是:'+str(response.json()['attributes']['rgb_color'])+' ' if "volume_level" in response.json()["attributes"]:
if 'brightness' in response.json()['attributes']: responsetext = (
responsetext = responsetext+ '亮度是:'+str(response.json()['attributes']['brightness'])+' ' responsetext
+ "音量是:"
+ str(response.json()["attributes"]["volume_level"])
+ " "
)
if "color_temp_kelvin" in response.json()["attributes"]:
responsetext = (
responsetext
+ "色温是:"
+ str(response.json()["attributes"]["color_temp_kelvin"])
+ " "
)
if "rgb_color" in response.json()["attributes"]:
responsetext = (
responsetext
+ "rgb颜色是:"
+ str(response.json()["attributes"]["rgb_color"])
+ " "
)
if "brightness" in response.json()["attributes"]:
responsetext = (
responsetext
+ "亮度是:"
+ str(response.json()["attributes"]["brightness"])
+ " "
)
logger.bind(tag=TAG).info(f"查询返回内容: {responsetext}") logger.bind(tag=TAG).info(f"查询返回内容: {responsetext}")
return responsetext return responsetext
#return response.json()['attributes'] # return response.json()['attributes']
#response.attributes # response.attributes
else: else:
return f"切换失败,错误码: {response.status_code}" return f"切换失败,错误码: {response.status_code}"
@@ -4,40 +4,49 @@ from core.utils.util import check_model_key
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
HASS_CACHE = {}
def append_devices_to_prompt(conn): def append_devices_to_prompt(conn):
if conn.intent_type == "function_call": if conn.intent_type == "function_call":
funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get( funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
"functions", [] "functions", []
) )
config_source = (
"home_assistant"
if conn.config["plugins"].get("home_assistant")
else "hass_get_state"
)
if "hass_get_state" in funcs or "hass_set_state" in funcs: if "hass_get_state" in funcs or "hass_set_state" in funcs:
prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n" prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
devices = conn.config["plugins"]["home_assistant"].get("devices", []) deviceStr = conn.config["plugins"].get(config_source, {}).get("devices", "")
if len(devices) == 0: conn.prompt += prompt + deviceStr + "\n"
return
for device in devices:
prompt += device + "\n"
conn.prompt += prompt
# 更新提示词 # 更新提示词
conn.dialogue.update_system_message(conn.prompt) conn.dialogue.update_system_message(conn.prompt)
def initialize_hass_handler(conn): def initialize_hass_handler(conn):
global HASS_CACHE ha_config = {}
if HASS_CACHE == {}: if not conn.load_function_plugin:
if conn.load_function_plugin: return ha_config
funcs = conn.config["Intent"][conn.config["selected_module"]["Intent"]].get(
"functions", []
)
if "hass_get_state" in funcs or "hass_set_state" in funcs:
HASS_CACHE["base_url"] = conn.config["plugins"]["home_assistant"].get(
"base_url"
)
HASS_CACHE["api_key"] = conn.config["plugins"]["home_assistant"].get(
"api_key"
)
check_model_key("home_assistant", HASS_CACHE["api_key"]) # 确定配置来源
return HASS_CACHE config_source = (
"home_assistant"
if conn.config["plugins"].get("home_assistant")
else "hass_get_state"
)
if not conn.config["plugins"].get(config_source):
return ha_config
# 统一获取配置
plugin_config = conn.config["plugins"][config_source]
ha_config["base_url"] = plugin_config.get("base_url")
ha_config["api_key"] = plugin_config.get("api_key")
# 统一检查API密钥
model_key_msg = check_model_key("home_assistant", ha_config.get("api_key"))
if model_key_msg:
logger.bind(tag=TAG).error(model_key_msg)
return ha_config
@@ -17,46 +17,43 @@ hass_play_music_function_desc = {
"properties": { "properties": {
"media_content_id": { "media_content_id": {
"type": "string", "type": "string",
"description": "可以是音乐或有声书的专辑名称、歌曲名、演唱者,如果未指定就填random" "description": "可以是音乐或有声书的专辑名称、歌曲名、演唱者,如果未指定就填random",
}, },
"entity_id": { "entity_id": {
"type": "string", "type": "string",
"description": "需要操作的音箱的设备id,homeassistant里的entity_id,media_player开头" "description": "需要操作的音箱的设备id,homeassistant里的entity_id,media_player开头",
} },
}, },
"required": ["media_content_id", "entity_id"] "required": ["media_content_id", "entity_id"],
} },
} },
} }
@register_function('hass_play_music', hass_play_music_function_desc, ToolType.SYSTEM_CTL) @register_function(
def hass_play_music(conn, entity_id='', media_content_id='random'): "hass_play_music", hass_play_music_function_desc, ToolType.SYSTEM_CTL
)
def hass_play_music(conn, entity_id="", media_content_id="random"):
try: try:
# 执行音乐播放命令 # 执行音乐播放命令
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
handle_hass_play_music(conn, entity_id, media_content_id), handle_hass_play_music(conn, entity_id, media_content_id), conn.loop
conn.loop
) )
ha_response = future.result() ha_response = future.result()
return ActionResponse(action=Action.RESPONSE, result="退出意图已处理", response=ha_response) return ActionResponse(
action=Action.RESPONSE, result="退出意图已处理", response=ha_response
)
except Exception as e: except Exception as e:
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}") logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
async def handle_hass_play_music(conn, entity_id, media_content_id): async def handle_hass_play_music(conn, entity_id, media_content_id):
HASS_CACHE = initialize_hass_handler(conn) ha_config = initialize_hass_handler(conn)
api_key = HASS_CACHE['api_key'] api_key = ha_config.get("api_key")
base_url = HASS_CACHE['base_url'] base_url = ha_config.get("base_url")
url = f"{base_url}/api/services/music_assistant/play_media" url = f"{base_url}/api/services/music_assistant/play_media"
headers = { headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
"Authorization": f"Bearer {api_key}", data = {"entity_id": entity_id, "media_id": media_content_id}
"Content-Type": "application/json"
}
data = {
"entity_id": entity_id,
"media_id": media_content_id
}
response = requests.post(url, headers=headers, json=data) response = requests.post(url, headers=headers, json=data)
if response.status_code == 200: if response.status_code == 200:
return f"正在播放{media_content_id}的音乐" return f"正在播放{media_content_id}的音乐"
@@ -20,40 +20,39 @@ hass_set_state_function_desc = {
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"description": "需要操作的动作,打开设备:turn_on,关闭设备:turn_off,增加亮度:brightness_up,降低亮度:brightness_down,设置亮度:brightness_value,增加音量:volume_up,降低音量:volume_down,设置音量:volume_set,设置色温:set_kelvin,设置颜色:set_color,设备暂停:pause,设备继续:continue,静音/取消静音:volume_mute" "description": "需要操作的动作,打开设备:turn_on,关闭设备:turn_off,增加亮度:brightness_up,降低亮度:brightness_down,设置亮度:brightness_value,增加音量:volume_up,降低音量:volume_down,设置音量:volume_set,设置色温:set_kelvin,设置颜色:set_color,设备暂停:pause,设备继续:continue,静音/取消静音:volume_mute",
}, },
"input": { "input": {
"type": "integer", "type": "integer",
"description": "只有在设置音量,设置亮度时候才需要,有效值为1-100,对应音量和亮度的1%-100%" "description": "只有在设置音量,设置亮度时候才需要,有效值为1-100,对应音量和亮度的1%-100%",
}, },
"is_muted": { "is_muted": {
"type": "string", "type": "string",
"description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false" "description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false",
},
"rgb_color": {
"type": "list",
"description": "只有在设置颜色时需要,这里填目标颜色的rgb值",
}, },
"rgb_color":{
"type":"list",
"description": "只有在设置颜色时需要,这里填目标颜色的rgb值"
}
}, },
"required": ["type"] "required": ["type"],
}, },
"entity_id": { "entity_id": {
"type": "string", "type": "string",
"description": "需要操作的设备id,homeassistant里的entity_id" "description": "需要操作的设备id,homeassistant里的entity_id",
} },
}, },
"required": ["state", "entity_id"] "required": ["state", "entity_id"],
} },
} },
} }
@register_function('hass_set_state', hass_set_state_function_desc, ToolType.SYSTEM_CTL) @register_function("hass_set_state", hass_set_state_function_desc, ToolType.SYSTEM_CTL)
def hass_set_state(conn, entity_id='', state={}): def hass_set_state(conn, entity_id="", state={}):
try: try:
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
handle_hass_set_state(conn, entity_id, state), handle_hass_set_state(conn, entity_id, state), conn.loop
conn.loop
) )
ha_response = future.result() ha_response = future.result()
return ActionResponse(Action.REQLLM, ha_response, None) return ActionResponse(Action.REQLLM, ha_response, None)
@@ -62,21 +61,21 @@ def hass_set_state(conn, entity_id='', state={}):
async def handle_hass_set_state(conn, entity_id, state): async def handle_hass_set_state(conn, entity_id, state):
HASS_CACHE = initialize_hass_handler(conn) ha_config = initialize_hass_handler(conn)
api_key = HASS_CACHE['api_key'] api_key = ha_config.get("api_key")
base_url = HASS_CACHE['base_url'] base_url = ha_config.get("base_url")
''' """
state = { "type":"brightness_up","input":"80","is_muted":"true"} state = { "type":"brightness_up","input":"80","is_muted":"true"}
''' """
domains = entity_id.split(".") domains = entity_id.split(".")
if len(domains) > 1: if len(domains) > 1:
domain = domains[0] domain = domains[0]
else: else:
return "执行失败,错误的设备id" return "执行失败,错误的设备id"
action = '' action = ""
arg = '' arg = ""
value = '' value = ""
if state['type'] == 'turn_on': if state["type"] == "turn_on":
description = "设备已打开" description = "设备已打开"
if domain == "cover": if domain == "cover":
action = "open_cover" action = "open_cover"
@@ -84,91 +83,87 @@ async def handle_hass_set_state(conn, entity_id, state):
action = "start" action = "start"
else: else:
action = "turn_on" action = "turn_on"
elif state['type'] == 'turn_off': elif state["type"] == "turn_off":
description = "设备已关闭" description = "设备已关闭"
if domain == 'cover': if domain == "cover":
action = "close_cover" action = "close_cover"
elif domain == 'vacuum': elif domain == "vacuum":
action = "stop" action = "stop"
else: else:
action = "turn_off" action = "turn_off"
elif state['type'] == 'brightness_up': elif state["type"] == "brightness_up":
description = "灯光已调亮" description = "灯光已调亮"
action = 'turn_on' action = "turn_on"
arg = 'brightness_step_pct' arg = "brightness_step_pct"
value = 10 value = 10
elif state['type'] == 'brightness_down': elif state["type"] == "brightness_down":
description = "灯光已调暗" description = "灯光已调暗"
action = 'turn_on' action = "turn_on"
arg = 'brightness_step_pct' arg = "brightness_step_pct"
value = -10 value = -10
elif state['type'] == 'brightness_value': elif state["type"] == "brightness_value":
description = f"亮度已调整到{state['input']}" description = f"亮度已调整到{state['input']}"
action = 'turn_on' action = "turn_on"
arg = 'brightness_pct' arg = "brightness_pct"
value = state['input'] value = state["input"]
elif state['type'] == 'set_color': elif state["type"] == "set_color":
description = f"颜色已调整到{state['rgb_color']}" description = f"颜色已调整到{state['rgb_color']}"
action = 'turn_on' action = "turn_on"
arg = 'rgb_color' arg = "rgb_color"
value = state['rgb_color'] value = state["rgb_color"]
elif state['type'] == 'set_kelvin': elif state["type"] == "set_kelvin":
description = f"色温已调整到{state['input']}K" description = f"色温已调整到{state['input']}K"
action = 'turn_on' action = "turn_on"
arg = 'kelvin' arg = "kelvin"
value = state['input'] value = state["input"]
elif state['type'] == 'volume_up': elif state["type"] == "volume_up":
description = "音量已调大" description = "音量已调大"
action = state['type'] action = state["type"]
elif state['type'] == 'volume_down': elif state["type"] == "volume_down":
description = "音量已调小" description = "音量已调小"
action = state['type'] action = state["type"]
elif state['type'] == 'volume_set': elif state["type"] == "volume_set":
description = f"音量已调整到{state['input']}" description = f"音量已调整到{state['input']}"
action = state['type'] action = state["type"]
arg = 'volume_level' arg = "volume_level"
value = state['input'] value = state["input"]
if state['input'] >= 1: if state["input"] >= 1:
value = state['input']/100 value = state["input"] / 100
elif state['type'] == 'volume_mute': elif state["type"] == "volume_mute":
description = f"设备已静音" description = f"设备已静音"
action = state['type'] action = state["type"]
arg = 'is_volume_muted' arg = "is_volume_muted"
value = state['is_muted'] value = state["is_muted"]
elif state['type'] == 'pause': elif state["type"] == "pause":
description = f"设备已暂停" description = f"设备已暂停"
action = state['type'] action = state["type"]
if domain == 'media_player': if domain == "media_player":
action = 'media_pause' action = "media_pause"
if domain == 'cover': if domain == "cover":
action = 'stop_cover' action = "stop_cover"
if domain == 'vacuum': if domain == "vacuum":
action = 'pause' action = "pause"
elif state['type'] == 'continue': elif state["type"] == "continue":
description = f"设备已继续" description = f"设备已继续"
if domain == 'media_player': if domain == "media_player":
action = 'media_play' action = "media_play"
if domain == 'vacuum': if domain == "vacuum":
action = 'start' action = "start"
else: else:
return f"{domain} {state.type}功能尚未支持" return f"{domain} {state.type}功能尚未支持"
if arg == '': if arg == "":
data = { data = {
"entity_id": entity_id, "entity_id": entity_id,
} }
else: else:
data = { data = {"entity_id": entity_id, arg: value}
"entity_id": entity_id,
arg: value
}
url = f"{base_url}/api/services/{domain}/{action}" url = f"{base_url}/api/services/{domain}/{action}"
headers = { headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
response = requests.post(url, headers=headers, json=data) response = requests.post(url, headers=headers, json=data)
logger.bind(tag=TAG).info(f"设置状态:{description},url:{url},return_code:{response.status_code}") logger.bind(tag=TAG).info(
f"设置状态:{description},url:{url},return_code:{response.status_code}"
)
if response.status_code == 200: if response.status_code == 200:
return description return description
else: else: