Merge branch 'main' into manager-api-aly-message
@@ -1,7 +1,7 @@
|
||||
# 基于虾哥编译好的固件配置自定义服务器
|
||||
|
||||
## 第1步 确认版本
|
||||
烧录虾哥已经编译好的1.6.1版本固件
|
||||
烧录虾哥已经编译好的[1.6.1版本以上固件](https://github.com/78/xiaozhi-esp32/releases)
|
||||
|
||||
## 第2步 准备你的ota地址
|
||||
如果你按照教程使用的是全模块部署,就应该会有ota地址。
|
||||
@@ -29,6 +29,7 @@ wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|
||||
## 第3步 进入配网模式
|
||||
进入机器的配网模式,在页面顶部,点击“高级选项”,在里面输入你服务器的`ota`地址,点击保存。重启设备
|
||||

|
||||
|
||||
## 第4步 唤醒小智,查看日志输出
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
登录AutoDL,租赁镜像
|
||||
选择镜像:
|
||||
```
|
||||
PyTorch / 2.1.0 / 3.10(ubuntu22.04) / cuda 12.1
|
||||
```
|
||||
|
||||
机器开机后,设置学术加速
|
||||
```
|
||||
source /etc/network_turbo
|
||||
```
|
||||
|
||||
进入工作目录
|
||||
```
|
||||
cd autodl-tmp/
|
||||
```
|
||||
|
||||
拉取项目
|
||||
```
|
||||
git clone https://gitclone.com/github.com/fishaudio/fish-speech.git ; cd fish-speech
|
||||
```
|
||||
|
||||
安装依赖
|
||||
```
|
||||
pip install -e.
|
||||
```
|
||||
|
||||
如果报错,安装portaudio
|
||||
```
|
||||
apt-get install portaudio19-dev -y
|
||||
```
|
||||
|
||||
安装后执行
|
||||
```
|
||||
pip install torch==2.3.1 torchvision==0.18.1 torchaudio==2.3.1 --index-url https://download.pytorch.org/whl/cu121
|
||||
```
|
||||
|
||||
下载模型
|
||||
```
|
||||
cd tools
|
||||
python download_models.py
|
||||
```
|
||||
|
||||
下载完模型后运行接口
|
||||
```
|
||||
python -m tools.api_server --listen 0.0.0.0:6006
|
||||
```
|
||||
|
||||
然后用浏览器去到aotodl实例页面
|
||||
```
|
||||
https://autodl.com/console/instance/list
|
||||
```
|
||||
|
||||
如下图点击你刚才机器的`自定义服务`按钮,开启端口转发服务
|
||||

|
||||
|
||||
端口转发服务设置完成后,你本地电脑打开网址`http://localhost:6006/`,就可以访问fish-speech的接口了
|
||||

|
||||
|
||||
|
||||
如果你是单模块部署,核心配置如下
|
||||
```
|
||||
selected_module:
|
||||
TTS: FishSpeech
|
||||
TTS:
|
||||
FishSpeech:
|
||||
reference_audio: ["config/assets/wakeup_words.wav",]
|
||||
reference_text: ["哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",]
|
||||
api_key: "123"
|
||||
api_url: "http://127.0.0.1:6006/v1/tts"
|
||||
```
|
||||
|
||||
然后重启服务
|
||||
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 429 KiB |
|
After Width: | Height: | Size: 248 KiB |
@@ -0,0 +1,40 @@
|
||||
package xiaozhi.common.config;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
@EnableAspectJAutoProxy(exposeProxy = true)
|
||||
public class AsyncConfig {
|
||||
|
||||
@Bean(name = "taskExecutor")
|
||||
public Executor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(2);
|
||||
executor.setMaxPoolSize(4);
|
||||
executor.setQueueCapacity(1000);
|
||||
executor.setThreadNamePrefix("AsyncThread-");
|
||||
// 设置拒绝策略:由调用线程执行
|
||||
executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
|
||||
@Override
|
||||
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
|
||||
try {
|
||||
// 如果线程池已满,则由调用线程执行
|
||||
r.run();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("执行异步任务失败", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package xiaozhi.common.constant;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 常量
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
@@ -109,6 +111,11 @@ public interface Constant {
|
||||
*/
|
||||
String FILE_EXTENSION_SEG = ".";
|
||||
|
||||
/**
|
||||
* 无记忆
|
||||
*/
|
||||
String MEMORY_NO_MEM = "Memory_nomem";
|
||||
|
||||
enum SysBaseParam {
|
||||
/**
|
||||
* 系统全称
|
||||
@@ -214,8 +221,28 @@ public interface Constant {
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
enum ChatHistoryConfEnum {
|
||||
IGNORE(0, "不记录"),
|
||||
RECORD_TEXT(1, "记录文本"),
|
||||
RECORD_TEXT_AUDIO(2, "文本音频都记录");
|
||||
|
||||
private final int code;
|
||||
private final String name;
|
||||
|
||||
ChatHistoryConfEnum(int code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.4.2";
|
||||
public static final String VERSION = "0.4.4";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
*/
|
||||
String INVALID_FIRMWARE_URL = "http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL";
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package xiaozhi.common.exception;
|
||||
|
||||
import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
@@ -10,6 +13,8 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.utils.Result;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
/**
|
||||
* 异常处理器
|
||||
* Copyright (c) 人人开源 All rights reserved.
|
||||
@@ -60,4 +65,16 @@ public class RenExceptionHandler {
|
||||
return new Result<Void>().error(404, "资源不存在");
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
|
||||
String errorMsg = allErrors.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(DefaultMessageSourceResolvable::getDefaultMessage)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
return new Result<Void>().error(400, errorMsg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.common.service.impl;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -18,6 +19,7 @@ import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
@@ -45,6 +47,12 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
* @param params 分页查询参数
|
||||
* @param defaultOrderField 默认排序字段
|
||||
* @param isAsc 排序方式
|
||||
* @see xiaozhi.common.constant.Constant
|
||||
* params.put(Constant.PAGE, "1");
|
||||
* params.put(Constant.LIMIT, "10");
|
||||
* params.put(Constant.ORDER_FIELD, "field"); // 单个字段
|
||||
* params.put(Constant.ORDER_FIELD, List.of("field1", "field2")); // 多个字段
|
||||
* params.put(Constant.ORDER, "asc");
|
||||
*/
|
||||
protected IPage<T> getPage(Map<String, Object> params, String defaultOrderField, boolean isAsc) {
|
||||
// 分页参数
|
||||
@@ -65,28 +73,34 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T> implements Bas
|
||||
params.put(Constant.PAGE, page);
|
||||
|
||||
// 排序字段
|
||||
String orderField = (String) params.get(Constant.ORDER_FIELD);
|
||||
Object orderField = params.get(Constant.ORDER_FIELD);
|
||||
String order = (String) params.get(Constant.ORDER);
|
||||
|
||||
// 前端字段排序
|
||||
if (StringUtils.isNotBlank(orderField) && StringUtils.isNotBlank(order)) {
|
||||
if (Constant.ASC.equalsIgnoreCase(order)) {
|
||||
return page.addOrder(OrderItem.asc(orderField));
|
||||
List<String> orderFields = new ArrayList<>();
|
||||
|
||||
// 处理排序字段
|
||||
if (orderField instanceof String) {
|
||||
orderFields.add((String) orderField);
|
||||
} else if (orderField instanceof List) {
|
||||
orderFields.addAll((List<String>) orderField);
|
||||
}
|
||||
|
||||
// 有排序字段则排序
|
||||
if (CollectionUtils.isNotEmpty(orderFields)) {
|
||||
if (StringUtils.isNotBlank(order) && Constant.ASC.equalsIgnoreCase(order)) {
|
||||
return page.addOrder(OrderItem.ascs(orderFields.toArray(new String[0])));
|
||||
} else {
|
||||
return page.addOrder(OrderItem.desc(orderField));
|
||||
return page.addOrder(OrderItem.descs(orderFields.toArray(new String[0])));
|
||||
}
|
||||
}
|
||||
|
||||
// 没有排序字段,则不排序
|
||||
if (StringUtils.isBlank(defaultOrderField)) {
|
||||
return page;
|
||||
}
|
||||
|
||||
// 默认排序
|
||||
if (isAsc) {
|
||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||
} else {
|
||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||
// 没有排序字段,使用默认排序
|
||||
if (StringUtils.isNotBlank(defaultOrderField)) {
|
||||
if (isAsc) {
|
||||
page.addOrder(OrderItem.asc(defaultOrderField));
|
||||
} else {
|
||||
page.addOrder(OrderItem.desc(defaultOrderField));
|
||||
}
|
||||
}
|
||||
|
||||
return page;
|
||||
|
||||
@@ -39,6 +39,7 @@ import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
@@ -46,6 +47,7 @@ import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@@ -109,6 +111,8 @@ public class AgentController {
|
||||
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());
|
||||
}
|
||||
@@ -125,10 +129,26 @@ public class AgentController {
|
||||
return new Result<String>().ok(entity.getId());
|
||||
}
|
||||
|
||||
@PutMapping("/saveMemory/{macAddress}")
|
||||
@Operation(summary = "根据设备id更新智能体")
|
||||
public Result<Void> updateByDeviceId(@PathVariable String macAddress, @RequestBody @Valid AgentMemoryDTO dto) {
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||
if (device == null) {
|
||||
return new Result<>();
|
||||
}
|
||||
AgentUpdateDTO agentUpdateDTO = new AgentUpdateDTO();
|
||||
agentUpdateDTO.setSummaryMemory(dto.getSummaryMemory());
|
||||
return updateAgentById(device.getAgentId(), agentUpdateDTO);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> update(@PathVariable String id, @RequestBody @Valid AgentUpdateDTO dto) {
|
||||
return updateAgentById(id, dto);
|
||||
}
|
||||
|
||||
private Result<Void> updateAgentById(String id, AgentUpdateDTO dto) {
|
||||
// 先查询现有实体
|
||||
AgentEntity existingEntity = agentService.getAgentById(id);
|
||||
if (existingEntity == null) {
|
||||
@@ -166,6 +186,12 @@ public class AgentController {
|
||||
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());
|
||||
}
|
||||
@@ -181,8 +207,16 @@ public class AgentController {
|
||||
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<>();
|
||||
}
|
||||
|
||||
@@ -193,7 +227,7 @@ public class AgentController {
|
||||
// 先删除关联的设备
|
||||
deviceService.deleteByAgentId(id);
|
||||
// 删除关联的聊天记录
|
||||
agentChatHistoryService.deleteByAgentId(id);
|
||||
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||
// 再删除智能体
|
||||
agentService.deleteById(id);
|
||||
return new Result<>();
|
||||
|
||||
@@ -28,4 +28,11 @@ public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteHistoryByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除音频ID
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAudioIdByAgentId(String agentId);
|
||||
}
|
||||
|
||||
@@ -27,9 +27,16 @@ public class AgentDTO {
|
||||
@Schema(description = "大语言模型名称", example = "llm_model_01")
|
||||
private String llmModelName;
|
||||
|
||||
@Schema(description = "记忆模型ID", example = "mem_model_01")
|
||||
private String memModelId;
|
||||
|
||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助")
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
|
||||
private Date lastConnectedAt;
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体记忆更新DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "智能体记忆更新对象")
|
||||
public class AgentMemoryDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
}
|
||||
@@ -45,6 +45,13 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助", required = false)
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)", example = "3", required = false)
|
||||
private Integer chatHistoryConf;
|
||||
|
||||
@Schema(description = "语言编码", example = "zh_CN", required = false)
|
||||
private String langCode;
|
||||
|
||||
|
||||
@@ -48,9 +48,16 @@ public class AgentEntity {
|
||||
@Schema(description = "意图模型标识")
|
||||
private String intentModelId;
|
||||
|
||||
@Schema(description = "聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)")
|
||||
private Integer chatHistoryConf;
|
||||
|
||||
@Schema(description = "角色设定参数")
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "总结记忆", example = "构建可生长的动态记忆网络,在有限空间内保留关键信息的同时,智能维护信息演变轨迹\n" +
|
||||
"根据对话记录,总结user的重要信息,以便在未来的对话中提供更个性化的服务", required = false)
|
||||
private String summaryMemory;
|
||||
|
||||
@Schema(description = "语言编码")
|
||||
private String langCode;
|
||||
|
||||
|
||||
@@ -69,11 +69,20 @@ public class AgentTemplateEntity implements Serializable {
|
||||
*/
|
||||
private String intentModelId;
|
||||
|
||||
/**
|
||||
* 聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)
|
||||
*/
|
||||
private Integer chatHistoryConf;
|
||||
|
||||
/**
|
||||
* 角色设定参数
|
||||
*/
|
||||
private String systemPrompt;
|
||||
|
||||
/**
|
||||
* 总结记忆
|
||||
*/
|
||||
private String summaryMemory;
|
||||
/**
|
||||
* 语言编码
|
||||
*/
|
||||
|
||||
@@ -39,7 +39,9 @@ public interface AgentChatHistoryService extends IService<AgentChatHistoryEntity
|
||||
/**
|
||||
* 根据智能体ID删除聊天记录
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param agentId 智能体ID
|
||||
* @param deleteAudio 是否删除音频
|
||||
* @param deleteText 是否删除文本
|
||||
*/
|
||||
void deleteByAgentId(String agentId);
|
||||
void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package xiaozhi.modules.agent.service.biz.impl;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||
@@ -47,8 +49,33 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
||||
Byte chatType = report.getChatType();
|
||||
log.info("小智设备聊天上报请求: macAddress={}, type={}", macAddress, chatType);
|
||||
|
||||
// 1. base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
||||
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
||||
if (agentEntity == null) {
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
|
||||
Integer chatHistoryConf = agentEntity.getChatHistoryConf();
|
||||
String agentId = agentEntity.getId();
|
||||
|
||||
if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT.getCode())) {
|
||||
saveChatText(report, agentId, macAddress, null);
|
||||
} else if (Objects.equals(chatHistoryConf, Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode())) {
|
||||
String audioId = saveChatAudio(report);
|
||||
saveChatText(report, agentId, macAddress, audioId);
|
||||
}
|
||||
|
||||
// 更新设备最后对话时间
|
||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* base64解码report.getOpusDataBase64(),存入ai_agent_chat_audio表
|
||||
*/
|
||||
private String saveChatAudio(AgentChatHistoryReportDTO report) {
|
||||
String audioId = null;
|
||||
|
||||
if (report.getAudioBase64() != null && !report.getAudioBase64().isEmpty()) {
|
||||
try {
|
||||
byte[] audioData = Base64.getDecoder().decode(report.getAudioBase64());
|
||||
@@ -56,20 +83,18 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
||||
log.info("音频数据保存成功,audioId={}", audioId);
|
||||
} catch (Exception e) {
|
||||
log.error("音频数据保存失败", e);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return audioId;
|
||||
}
|
||||
|
||||
// 2. 组装上报数据
|
||||
// 2.1 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||
AgentEntity agentEntity = agentService.getDefaultAgentByMacAddress(macAddress);
|
||||
if (agentEntity == null) {
|
||||
return false;
|
||||
}
|
||||
String agentId = agentEntity.getId();
|
||||
log.info("设备 {} 对应智能体 {} 上报", macAddress, agentEntity.getId());
|
||||
/**
|
||||
* 组装上报数据
|
||||
*/
|
||||
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId) {
|
||||
|
||||
// 2.2 构建聊天记录实体
|
||||
// 构建聊天记录实体
|
||||
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||
.macAddress(macAddress)
|
||||
.agentId(agentId)
|
||||
@@ -79,10 +104,9 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
||||
.audioId(audioId)
|
||||
.build();
|
||||
|
||||
// 3. 保存数据
|
||||
// 保存数据
|
||||
agentChatHistoryService.save(entity);
|
||||
// 4. 更新设备最后对话时间
|
||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||
return Boolean.TRUE;
|
||||
|
||||
log.info("设备 {} 对应智能体 {} 上报成功", macAddress, agentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +78,16 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteByAgentId(String agentId) {
|
||||
baseMapper.deleteAudioByAgentId(agentId);
|
||||
baseMapper.deleteHistoryByAgentId(agentId);
|
||||
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
||||
if (deleteAudio) {
|
||||
baseMapper.deleteAudioByAgentId(agentId);
|
||||
}
|
||||
if (deleteAudio && !deleteText) {
|
||||
baseMapper.deleteAudioIdByAgentId(agentId);
|
||||
}
|
||||
if (deleteText) {
|
||||
baseMapper.deleteHistoryByAgentId(agentId);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
@@ -46,7 +47,15 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
|
||||
@Override
|
||||
public AgentEntity getAgentById(String id) {
|
||||
return agentDao.selectById(id);
|
||||
AgentEntity agent = agentDao.selectById(id);
|
||||
if (agent != null && agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
||||
} else if (agent != null && agent.getMemModelId() != null
|
||||
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
|
||||
&& agent.getChatHistoryConf() == null) {
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
||||
}
|
||||
return agent;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,6 +102,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
// 获取 LLM 模型名称
|
||||
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
||||
|
||||
// 获取记忆模型名称
|
||||
dto.setMemModelId(agent.getMemModelId());
|
||||
|
||||
// 获取 TTS 音色名称
|
||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
@@ -64,6 +65,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
agent.getVadModelId(),
|
||||
agent.getAsrModelId(),
|
||||
null,
|
||||
@@ -108,6 +110,17 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
// 获取单台设备每天最多输出字数
|
||||
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
||||
result.put("device_max_output_size", deviceMaxOutputSize);
|
||||
|
||||
// 获取聊天记录配置
|
||||
Integer chatHistoryConf = agent.getChatHistoryConf();
|
||||
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
chatHistoryConf = Constant.ChatHistoryConfEnum.IGNORE.getCode();
|
||||
} else if (agent.getMemModelId() != null
|
||||
&& !agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)
|
||||
&& agent.getChatHistoryConf() == null) {
|
||||
chatHistoryConf = Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode();
|
||||
}
|
||||
result.put("chat_history_conf", chatHistoryConf);
|
||||
// 如果客户端已实例化模型,则不返回
|
||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||
@@ -122,6 +135,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
buildModuleConfig(
|
||||
agent.getAgentName(),
|
||||
agent.getSystemPrompt(),
|
||||
agent.getSummaryMemory(),
|
||||
voice,
|
||||
agent.getVadModelId(),
|
||||
agent.getAsrModelId(),
|
||||
@@ -222,6 +236,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
private void buildModuleConfig(
|
||||
String assistantName,
|
||||
String prompt,
|
||||
String summaryMemory,
|
||||
String voice,
|
||||
String vadModelId,
|
||||
String asrModelId,
|
||||
@@ -282,5 +297,6 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
|
||||
}
|
||||
result.put("prompt", prompt);
|
||||
result.put("summaryMemory", summaryMemory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.aop.framework.AopContext;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
@@ -54,6 +56,24 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
private final RedisUtils redisUtils;
|
||||
private final OtaService otaService;
|
||||
|
||||
@Async
|
||||
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
|
||||
try {
|
||||
DeviceEntity device = new DeviceEntity();
|
||||
device.setId(deviceId);
|
||||
device.setLastConnectedAt(new Date());
|
||||
if (StringUtils.isNotBlank(appVersion)) {
|
||||
device.setAppVersion(appVersion);
|
||||
}
|
||||
deviceDao.updateById(device);
|
||||
if (StringUtils.isNotBlank(agentId)) {
|
||||
redisUtils.set(RedisKeys.getAgentDeviceLastConnectedAtById(agentId), new Date());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("异步更新设备连接信息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||
if (StringUtils.isBlank(activationCode)) {
|
||||
@@ -118,14 +138,20 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
DeviceEntity deviceById = getDeviceByMacAddress(macAddress);
|
||||
|
||||
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
|
||||
if (deviceById != null && deviceById.getAutoUpdate() != 0) {
|
||||
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||
// 设备未绑定,则返回当前上传的固件信息(不更新)以此兼容旧固件版本
|
||||
if (deviceById == null) {
|
||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||
firmware.setVersion(deviceReport.getApplication().getVersion());
|
||||
firmware.setUrl(Constant.INVALID_FIRMWARE_URL);
|
||||
response.setFirmware(firmware);
|
||||
} else {
|
||||
response.setFirmware(null);
|
||||
// 只有在设备已绑定且autoUpdate不为0的情况下才返回固件升级信息
|
||||
if (deviceById.getAutoUpdate() != 0) {
|
||||
String type = deviceReport.getBoard() == null ? null : deviceReport.getBoard().getType();
|
||||
DeviceReportRespDTO.Firmware firmware = buildFirmwareInfo(type,
|
||||
deviceReport.getApplication() == null ? null : deviceReport.getApplication().getVersion());
|
||||
response.setFirmware(firmware);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加WebSocket配置
|
||||
@@ -150,13 +176,12 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
response.setWebsocket(websocket);
|
||||
|
||||
if (deviceById != null) {
|
||||
// 如果设备存在,则更新上次连接时间
|
||||
deviceById.setLastConnectedAt(new Date());
|
||||
if (deviceReport.getApplication() != null
|
||||
&& StringUtils.isNotBlank(deviceReport.getApplication().getVersion())) {
|
||||
deviceById.setAppVersion(deviceReport.getApplication().getVersion());
|
||||
}
|
||||
deviceDao.updateById(deviceById);
|
||||
// 如果设备存在,则异步更新上次连接时间和版本信息
|
||||
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
|
||||
: null;
|
||||
// 通过Spring代理调用异步方法
|
||||
((DeviceServiceImpl) AopContext.currentProxy()).updateDeviceConnectionInfo(deviceById.getAgentId(),
|
||||
deviceById.getId(), appVersion);
|
||||
} else {
|
||||
// 如果设备不存在,则生成激活码
|
||||
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
|
||||
@@ -353,7 +378,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
}
|
||||
|
||||
firmware.setVersion(ota == null ? currentVersion : ota.getVersion());
|
||||
firmware.setUrl(downloadUrl == null ? "" : downloadUrl);
|
||||
firmware.setUrl(downloadUrl == null ? Constant.INVALID_FIRMWARE_URL : downloadUrl);
|
||||
return firmware;
|
||||
}
|
||||
|
||||
@@ -385,4 +410,4 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package xiaozhi.modules.model.controller;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/models/provider")
|
||||
@Tag(name = "模型供应器")
|
||||
public class ModelProviderController {
|
||||
|
||||
private final ModelProviderService modelProviderService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "获取模型供应器列表")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<ModelProviderDTO>> getListPage(ModelProviderDTO modelProviderDTO,
|
||||
@RequestParam(required = true, defaultValue = "0") String page,
|
||||
@RequestParam(required = true, defaultValue = "10") String limit) {
|
||||
return new Result<PageData<ModelProviderDTO>>()
|
||||
.ok(modelProviderService.getListPage(modelProviderDTO, page, limit));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "新增模型供应器")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<ModelProviderDTO> add(@RequestBody @Validated ModelProviderDTO modelProviderDTO) {
|
||||
ModelProviderDTO resp = modelProviderService.add(modelProviderDTO);
|
||||
return new Result<ModelProviderDTO>().ok(resp);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "修改模型供应器")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<ModelProviderDTO> edit(@RequestBody @Validated(UpdateGroup.class) ModelProviderDTO modelProviderDTO) {
|
||||
ModelProviderDTO resp = modelProviderService.edit(modelProviderDTO);
|
||||
return new Result<ModelProviderDTO>().ok(resp);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除模型供应器")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@Parameter(name = "ids", description = "ID数组", required = true)
|
||||
public Result<Void> delete(@RequestBody List<String> ids) {
|
||||
modelProviderService.delete(ids);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,29 +8,37 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
|
||||
@Data
|
||||
@Schema(description = "模型供应器/商")
|
||||
public class ModelProviderDTO implements Serializable {
|
||||
//
|
||||
// @Schema(description = "主键")
|
||||
// private Long id;
|
||||
@Schema(description = "主键")
|
||||
@NotBlank(message = "id不能为空", groups = UpdateGroup.class)
|
||||
private String id;
|
||||
|
||||
@Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
|
||||
@NotBlank(message = "modelType不能为空")
|
||||
private String modelType;
|
||||
|
||||
@Schema(description = "供应器类型")
|
||||
@NotBlank(message = "providerCode不能为空")
|
||||
private String providerCode;
|
||||
|
||||
@Schema(description = "供应器名称")
|
||||
@NotBlank(message = "name不能为空")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "供应器字段列表(JSON格式)")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@NotBlank(message = "fields(JSON格式)不能为空")
|
||||
private String fields;
|
||||
|
||||
@Schema(description = "排序")
|
||||
@NotNull(message = "sort不能为空")
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
|
||||
@@ -3,10 +3,8 @@ package xiaozhi.modules.model.entity;
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -30,7 +28,6 @@ public class ModelProviderEntity {
|
||||
private String name;
|
||||
|
||||
@Schema(description = "供应器字段列表(JSON格式)")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private String fields;
|
||||
|
||||
@Schema(description = "排序")
|
||||
|
||||
@@ -2,8 +2,8 @@ package xiaozhi.modules.model.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||
|
||||
public interface ModelProviderService {
|
||||
|
||||
@@ -11,11 +11,15 @@ public interface ModelProviderService {
|
||||
|
||||
List<ModelProviderDTO> getListByModelType(String modelType);
|
||||
|
||||
ModelProviderDTO add(ModelProviderEntity modelProviderEntity);
|
||||
ModelProviderDTO add(ModelProviderDTO modelProviderDTO);
|
||||
|
||||
ModelProviderDTO edit(ModelProviderEntity modelProviderEntity);
|
||||
ModelProviderDTO edit(ModelProviderDTO modelProviderDTO);
|
||||
|
||||
void delete();
|
||||
void delete(String id);
|
||||
|
||||
void delete(List<String> id);
|
||||
|
||||
PageData<ModelProviderDTO> getListPage(ModelProviderDTO modelProviderDTO, String page, String limit);
|
||||
|
||||
List<ModelProviderDTO> getList(String modelType, String provideCode);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
package xiaozhi.modules.model.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import cn.hutool.json.JSONArray;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.model.dao.ModelProviderDao;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@@ -32,18 +42,78 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProviderDTO add(ModelProviderEntity modelProviderEntity) {
|
||||
return null;
|
||||
public PageData<ModelProviderDTO> getListPage(ModelProviderDTO modelProviderDTO, String page, String limit) {
|
||||
|
||||
Map<String, Object> params = new HashMap<String, Object>();
|
||||
params.put(Constant.PAGE, page);
|
||||
params.put(Constant.LIMIT, limit);
|
||||
params.put(Constant.ORDER_FIELD, List.of("model_type", "sort"));
|
||||
params.put(Constant.ORDER, "asc");
|
||||
|
||||
IPage<ModelProviderEntity> pageParam = getPage(params, null, true);
|
||||
|
||||
QueryWrapper<ModelProviderEntity> wrapper = new QueryWrapper<ModelProviderEntity>();
|
||||
|
||||
if (StringUtils.isNotBlank(modelProviderDTO.getModelType())) {
|
||||
wrapper.eq("model_type", modelProviderDTO.getModelType());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(modelProviderDTO.getName())) {
|
||||
wrapper.and(w -> w.like("name", modelProviderDTO.getName())
|
||||
.or()
|
||||
.like("provider_code", modelProviderDTO.getName()));
|
||||
}
|
||||
return getPageData(modelProviderDao.selectPage(pageParam, wrapper), ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String jsonString = "\"[]\"";
|
||||
JSONArray jsonArray = new JSONArray(jsonString);
|
||||
System.out.println("字符串转 JSONArray: " + jsonArray.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProviderDTO edit(ModelProviderEntity modelProviderEntity) {
|
||||
return null;
|
||||
public ModelProviderDTO add(ModelProviderDTO modelProviderDTO) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
modelProviderDTO.setCreator(user.getId());
|
||||
modelProviderDTO.setUpdater(user.getId());
|
||||
modelProviderDTO.setCreateDate(new Date());
|
||||
modelProviderDTO.setUpdateDate(new Date());
|
||||
// 去除Fields左右的双引号
|
||||
|
||||
modelProviderDTO.setFields(modelProviderDTO.getFields());
|
||||
ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class);
|
||||
if (modelProviderDao.insert(entity) == 0) {
|
||||
throw new RenException("新增数据失败");
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
public ModelProviderDTO edit(ModelProviderDTO modelProviderDTO) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
modelProviderDTO.setUpdater(user.getId());
|
||||
modelProviderDTO.setUpdateDate(new Date());
|
||||
if (modelProviderDao
|
||||
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
|
||||
throw new RenException("修改数据失败");
|
||||
}
|
||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
if (modelProviderDao.deleteById(id) == 0) {
|
||||
throw new RenException("删除数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(List<String> ids) {
|
||||
if (modelProviderDao.deleteBatchIds(ids) == 0) {
|
||||
throw new RenException("删除数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -87,6 +87,7 @@ public class ShiroConfig {
|
||||
// 将config路径使用server服务过滤器
|
||||
filterMap.put("/config/**", "server");
|
||||
filterMap.put("/agent/chat-history/report", "server");
|
||||
filterMap.put("/agent/saveMemory/**", "server");
|
||||
filterMap.put("/agent/play/**", "anon");
|
||||
filterMap.put("/**", "oauth2");
|
||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||
|
||||
@@ -159,7 +159,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
params.put(Constant.LIMIT, dto.getLimit());
|
||||
IPage<SysUserEntity> page = baseDao.selectPage(
|
||||
getPage(params, "id", true),
|
||||
new QueryWrapper<SysUserEntity>().eq(StringUtils.isNotBlank(dto.getMobile()), "username",
|
||||
new QueryWrapper<SysUserEntity>().like(StringUtils.isNotBlank(dto.getMobile()), "username",
|
||||
dto.getMobile()));
|
||||
// 循环处理page获取回来的数据,返回需要的字段
|
||||
List<AdminPageUserVO> list = page.getRecords().stream().map(user -> {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
-- 更新模型供应器表
|
||||
UPDATE `ai_model_provider` SET fields = '[{"key": "host", "type": "string", "label": "服务地址"}, {"key": "port", "type": "number", "label": "端口号"}, {"key": "type", "type": "string", "label": "服务类型"}, {"key": "is_ssl", "type": "boolean", "label": "是否使用SSL"}, {"key": "api_key", "type": "string", "label": "API密钥"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]' WHERE id = 'SYSTEM_ASR_FunASRServer';
|
||||
|
||||
-- 更新模型配置表
|
||||
UPDATE `ai_model_config` SET
|
||||
config_json = '{"host": "127.0.0.1", "port": 10096, "type": "fun_server", "is_ssl": true, "api_key": "none", "output_dir": "tmp/"}',
|
||||
`doc_link` = 'https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md',
|
||||
`remark` = '独立部署FunASR,使用FunASR的API服务,只需要五句话
|
||||
第一句:mkdir -p ./funasr-runtime-resources/models
|
||||
第二句:sudo docker run -p 10096:10095 -it --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
||||
上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
|
||||
不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
|
||||
上一句话执行后会进入到容器,继续第五句:tail -f log.txt
|
||||
第五句话执行完后,会看到模型下载日志,下载完后就可以连接使用了
|
||||
以上是使用CPU推理,如果有GPU,详细参考:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md' WHERE `id` = 'ASR_FunASRServer';
|
||||
|
||||
-- FishSpeech配置说明
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md',
|
||||
`remark` = 'FishSpeech配置说明:
|
||||
1. 需要本地部署FishSpeech服务
|
||||
2. 支持自定义音色
|
||||
3. 本地推理,无需网络连接
|
||||
4. 输出文件保存在tmp/目录
|
||||
5. 可参照教程https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md' WHERE `id` = 'TTS_FishSpeech';
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 添加聊天记录配置字段
|
||||
ALTER TABLE `ai_agent`
|
||||
ADD COLUMN `chat_history_conf` tinyint NOT NULL DEFAULT 0 COMMENT '聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)' AFTER `system_prompt`;
|
||||
|
||||
ALTER TABLE `ai_agent_template`
|
||||
ADD COLUMN `chat_history_conf` tinyint NOT NULL DEFAULT 0 COMMENT '聊天记录配置(0不记录 1仅记录文本 2记录文本和语音)' AFTER `system_prompt`;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 添加总结记忆字段
|
||||
ALTER TABLE `ai_agent`
|
||||
ADD COLUMN `summary_memory` text COMMENT '总结记忆' AFTER `system_prompt`;
|
||||
|
||||
ALTER TABLE `ai_agent_template`
|
||||
ADD COLUMN `summary_memory` text COMMENT '总结记忆' AFTER `system_prompt`;
|
||||
@@ -0,0 +1,7 @@
|
||||
update ai_agent_template set system_prompt = replace(system_prompt, '我是', '你是');
|
||||
|
||||
delete from sys_params where id in (500,501,402);
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (500, 'end_prompt.enable', 'true', 'boolean', 1, '是否开启结束语');
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (501, 'end_prompt.prompt', '请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧!', 'string', 1, '结束提示词');
|
||||
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (402, 'plugins.get_weather.api_host', 'mj7p3y7naa.re.qweatherapi.com', 'string', 1, '开发者apihost');
|
||||
@@ -120,4 +120,32 @@ databaseChangeLog:
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202505141132.sql
|
||||
path: classpath:db/changelog/202505141132.sql
|
||||
- changeSet:
|
||||
id: 202505091555
|
||||
author: whosmyqueen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202505091555.sql
|
||||
- changeSet:
|
||||
id: 202505111914
|
||||
author: hrz
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202505111914.sql
|
||||
- changeSet:
|
||||
id: 202505122348
|
||||
author: ljwwd2
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202505122348.sql
|
||||
- changeSet:
|
||||
id: 202505142037
|
||||
author: hrz
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202505142037.sql
|
||||
|
||||
@@ -28,11 +28,17 @@
|
||||
SELECT audio_id
|
||||
FROM ai_agent_chat_history
|
||||
WHERE agent_id = #{agentId}
|
||||
);
|
||||
)
|
||||
</delete>
|
||||
|
||||
<update id="deleteAudioIdByAgentId">
|
||||
UPDATE ai_agent_chat_history
|
||||
SET audio_id = NULL
|
||||
WHERE agent_id = #{agentId}
|
||||
</update>
|
||||
|
||||
<delete id="deleteHistoryByAgentId">
|
||||
DELETE FROM ai_agent_chat_history
|
||||
WHERE agent_id = #{agentId};
|
||||
WHERE agent_id = #{agentId}
|
||||
</delete>
|
||||
</mapper>
|
||||
|
||||
@@ -50,9 +50,9 @@ export default {
|
||||
}).send();
|
||||
},
|
||||
// 获取智能体配置
|
||||
getDeviceConfig(deviceId, callback) {
|
||||
getDeviceConfig(agentId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/agent/${deviceId}`)
|
||||
.url(`${getServiceUrl()}/agent/${agentId}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
@@ -61,7 +61,7 @@ export default {
|
||||
.networkFail((err) => {
|
||||
console.error('获取配置失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getDeviceConfig(deviceId, callback);
|
||||
this.getDeviceConfig(agentId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
@@ -197,5 +197,112 @@ export default {
|
||||
this.setDefaultModel(id, callback)
|
||||
})
|
||||
}).send()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取模型配置列表(支持查询参数)
|
||||
* @param {Object} params - 查询参数对象,例如 { name: 'test', modelType: 1 }
|
||||
* @param {Function} callback - 回调函数
|
||||
*/
|
||||
getModelProvidersPage(params, callback) {
|
||||
// 构建查询参数
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params.name) queryParams.append('name', params.name);
|
||||
if (params.modelType !== undefined) queryParams.append('modelType', params.modelType);
|
||||
if (params.page !== undefined) queryParams.append('page', params.page);
|
||||
if (params.limit !== undefined) queryParams.append('limit', params.limit);
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/provider?${queryParams.toString()}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
this.$message.error(err.msg || '获取供应器列表失败');
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelProviders(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
/**
|
||||
* 新增模型供应器配置
|
||||
* @param {Object} params - 请求参数对象,例如 { modelType: '1', providerCode: '1', name: '1', fields: '1', sort: 1 }
|
||||
* @param {Function} callback - 成功回调函数
|
||||
*/
|
||||
addModelProvider(params, callback) {
|
||||
const postData = {
|
||||
modelType: params.modelType || '',
|
||||
providerCode: params.providerCode || '',
|
||||
name: params.name || '',
|
||||
fields: JSON.stringify(params.fields || []),
|
||||
sort: params.sort || 0
|
||||
};
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/provider`)
|
||||
.method('POST')
|
||||
.data(postData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('新增模型供应器失败:', err)
|
||||
this.$message.error(err.msg || '新增模型供应器失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.addModelProvider(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新模型供应器配置
|
||||
* @param {Object} params - 请求参数对象,例如 { id: '111', modelType: '1', providerCode: '1', name: '1', fields: '1', sort: 1 }
|
||||
* @param {Function} callback - 成功回调函数
|
||||
*/
|
||||
updateModelProvider(params, callback) {
|
||||
const putData = {
|
||||
id: params.id || '',
|
||||
modelType: params.modelType || '',
|
||||
providerCode: params.providerCode || '',
|
||||
name: params.name || '',
|
||||
fields: JSON.stringify(params.fields || []),
|
||||
sort: params.sort || 0
|
||||
};
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/provider`)
|
||||
.method('PUT')
|
||||
.data(putData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
this.$message.error(err.msg || '更新模型供应器失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateModelProvider(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 删除
|
||||
deleteModelProviderByIds(ids, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/provider/delete`)
|
||||
.method('POST')
|
||||
.data(ids)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res);
|
||||
})
|
||||
.networkFail((err) => {
|
||||
this.$message.error(err.msg || '删除模型供应器失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteModelProviderByIds(ids, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 88 KiB |
@@ -26,8 +26,12 @@
|
||||
<div class="settings-btn" @click="handleDeviceManage">
|
||||
设备管理({{ device.deviceCount }})
|
||||
</div>
|
||||
<div class="settings-btn" @click="handleChatHistory">
|
||||
聊天记录
|
||||
<div class="settings-btn" @click="handleChatHistory"
|
||||
:class="{ 'disabled-btn': device.memModelId === 'Memory_nomem' }">
|
||||
<el-tooltip v-if="device.memModelId === 'Memory_nomem'" content="未开启记忆" placement="top">
|
||||
<span>聊天记录</span>
|
||||
</el-tooltip>
|
||||
<span v-else>聊天记录</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
@@ -77,6 +81,9 @@ export default {
|
||||
this.$router.push({ path: '/device-management', query: { agentId: this.device.agentId } });
|
||||
},
|
||||
handleChatHistory() {
|
||||
if (this.device.memModelId === 'Memory_nomem') {
|
||||
return
|
||||
}
|
||||
this.$emit('chat-history', { agentId: this.device.agentId, agentName: this.device.agentName })
|
||||
}
|
||||
}
|
||||
@@ -120,6 +127,12 @@ export default {
|
||||
color: #979db1;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.disabled-btn {
|
||||
background: #e6e6e6;
|
||||
color: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose" @open="handleOpen">
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" @close="handleClose" @open="handleOpen">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="固件名称" prop="firmwareName">
|
||||
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
||||
@@ -22,7 +22,7 @@
|
||||
<el-progress v-if="isUploading || uploadStatus === 'success'" :percentage="uploadProgress"
|
||||
:status="uploadStatus"></el-progress>
|
||||
<div class="hint-text">
|
||||
<span>温馨提示:请上传xiaozhi.bin文件,而不是merged-binary.bin文件</span>
|
||||
<span>温馨提示:请上传合并前的xiaozhi.bin文件,而不是合并后的merged-binary.bin文件</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
@@ -198,13 +198,13 @@ export default {
|
||||
if (!this.form.id) { // 只在新增时重置
|
||||
this.form.firmwarePath = ''
|
||||
this.form.size = 0
|
||||
// 重置上传组件
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.upload) {
|
||||
this.$refs.upload.clearFiles()
|
||||
}
|
||||
})
|
||||
}
|
||||
// 无论是否编辑模式,都重置上传组件
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.upload) {
|
||||
this.$refs.upload.clearFiles()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,6 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #979db1;
|
||||
font-size: 11px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<el-drawer :visible.sync="dialogVisible" direction="rtl" size="50%" :wrapperClosable="false" :withHeader="false">
|
||||
<!-- 自定义标题区域 -->
|
||||
<div class="custom-header">
|
||||
<div class="header-left">
|
||||
<h3 class="bold-title">功能管理</h3>
|
||||
</div>
|
||||
<button class="custom-close-btn" @click="closeDialog">×</button>
|
||||
</div>
|
||||
|
||||
<div class="function-manager">
|
||||
<!-- 左侧:未选功能 -->
|
||||
<div class="function-column">
|
||||
<div class="column-header">
|
||||
<h4 class="column-title">未选功能</h4>
|
||||
<el-button type="text" @click="selectAll" class="select-all-btn">全选</el-button>
|
||||
</div>
|
||||
<div class="function-list">
|
||||
<div v-for="func in unselected" :key="func.name" class="function-item">
|
||||
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox>
|
||||
<div class="func-tag" @click="handleFunctionClick(func)">
|
||||
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
|
||||
<span>{{ func.name }}</span>
|
||||
</div>
|
||||
<el-tooltip class="item" effect="dark" :content="func.description || '暂无功能描述'" placement="top">
|
||||
<img src="@/assets/home/info.png" alt="" class="info-icon">
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:已选功能 -->
|
||||
<div class="function-column">
|
||||
<div class="column-header">
|
||||
<h4 class="column-title">已选功能</h4>
|
||||
<el-button type="text" @click="deselectAll" class="select-all-btn">全选</el-button>
|
||||
</div>
|
||||
<div class="function-list">
|
||||
<div v-for="func in selectedList" :key="func.name" class="function-item">
|
||||
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)" @click.native.stop></el-checkbox>
|
||||
<div class="func-tag" @click="handleFunctionClick(func)">
|
||||
<div class="color-dot" :style="{backgroundColor: getFunctionColor(func.name)}"></div>
|
||||
<span>{{ func.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:参数配置 -->
|
||||
<div class="params-column">
|
||||
<h4 v-if="currentFunction" class="column-title">参数配置 - {{ currentFunction.name }}</h4>
|
||||
<div v-if="currentFunction" class="params-container">
|
||||
<el-form :model="currentFunction" size="mini" class="param-form" v-loading="loading" element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<el-form-item v-for="(value, key) in currentFunction.params" :key="key" :label="key" class="param-item">
|
||||
<el-input v-model="currentFunction.params[key]" size="mini" class="param-input" @change="(val) => handleParamChange(currentFunction, key, val)"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-else class="empty-tip">请选择已配置的功能进行参数设置</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-footer">
|
||||
<el-button @click="closeDialog">取消</el-button>
|
||||
<el-button type="primary" @click="saveSelection">保存配置</el-button>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
functions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.value,
|
||||
selectedNames: [],
|
||||
currentFunction: null,
|
||||
modifiedFunctions: {},
|
||||
allFunctions: [
|
||||
{name: '天气', params: {city: '北京'}, description: '查看指定城市的天气情况'},
|
||||
{name: '新闻', params: {type: '科技'}, description: '获取最新科技类新闻资讯'},
|
||||
{name: '工具', params: {category: '常用'}, description: '提供常用工具集合'},
|
||||
{name: '退出', params: {}, description: '退出当前系统'},
|
||||
{name: '音乐', params: {genre: '流行'}, description: '播放流行音乐'},
|
||||
{name: '翻译', params: {from: '中文', to: '英文'}, description: '提供中英文互译功能'},
|
||||
{name: '计算', params: {precision: '2'}, description: '提供精确计算功能'},
|
||||
{name: '日历', params: {view: '月'}, description: '查看月历视图'}
|
||||
],
|
||||
functionColorMap: [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
||||
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
||||
],
|
||||
tempFunctions: {},
|
||||
// 添加一个标志位来跟踪是否已经保存
|
||||
hasSaved: false,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
selectedList() {
|
||||
return this.allFunctions.filter(f => this.selectedNames.includes(f.name));
|
||||
},
|
||||
unselected() {
|
||||
return this.allFunctions.filter(f => !this.selectedNames.includes(f.name));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.dialogVisible = newVal;
|
||||
if (newVal) {
|
||||
this.selectedNames = this.functions.map(f => f.name);
|
||||
this.currentFunction = this.selectedList[0] || null;
|
||||
}
|
||||
},
|
||||
dialogVisible(newVal) {
|
||||
this.$emit('input', newVal);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleFunctionClick(func) {
|
||||
if (this.selectedNames.includes(func.name)) {
|
||||
this.loading = true;
|
||||
setTimeout(() => {
|
||||
const tempFunc = this.tempFunctions[func.name];
|
||||
this.currentFunction = tempFunc ? tempFunc : JSON.parse(JSON.stringify(func));
|
||||
this.loading = false;
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
handleParamChange(func, key, value) {
|
||||
if (!this.tempFunctions[func.name]) {
|
||||
this.tempFunctions[func.name] = JSON.parse(JSON.stringify(func));
|
||||
}
|
||||
this.tempFunctions[func.name].params[key] = value;
|
||||
},
|
||||
handleCheckboxChange(func, checked) {
|
||||
if (checked) {
|
||||
if (!this.selectedNames.includes(func.name)) {
|
||||
this.selectedNames = [...this.selectedNames, func.name];
|
||||
}
|
||||
} else {
|
||||
this.selectedNames = this.selectedNames.filter(name => name !== func.name);
|
||||
}
|
||||
|
||||
if (this.selectedList.length > 0) {
|
||||
this.currentFunction = this.selectedList[0];
|
||||
} else {
|
||||
this.currentFunction = null;
|
||||
}
|
||||
},
|
||||
|
||||
selectAll() {
|
||||
this.selectedNames = [...this.allFunctions.map(f => f.name)];
|
||||
if (this.selectedList.length > 0) {
|
||||
this.currentFunction = JSON.parse(JSON.stringify(this.selectedList[0]));
|
||||
}
|
||||
},
|
||||
|
||||
deselectAll() {
|
||||
this.selectedNames = [];
|
||||
this.currentFunction = null;
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.tempFunctions = {};
|
||||
this.selectedNames = this.functions.map(f => f.name);
|
||||
this.currentFunction = null;
|
||||
this.dialogVisible = false;
|
||||
this.$emit('input', false);
|
||||
this.$emit('dialog-closed', false);
|
||||
},
|
||||
|
||||
saveSelection() {
|
||||
Object.keys(this.tempFunctions).forEach(name => {
|
||||
this.modifiedFunctions[name] = JSON.parse(JSON.stringify(this.tempFunctions[name]));
|
||||
});
|
||||
this.tempFunctions = {};
|
||||
this.hasSaved = true;
|
||||
|
||||
const selected = this.selectedList.map(f => {
|
||||
const modified = this.modifiedFunctions[f.name];
|
||||
return modified || f;
|
||||
}).map(f => ({
|
||||
...f,
|
||||
params: JSON.parse(JSON.stringify(f.params))
|
||||
}));
|
||||
|
||||
this.$emit('update-functions', selected);
|
||||
this.dialogVisible = false;
|
||||
this.$message.success('配置保存成功');
|
||||
// 通知父组件对话框已关闭且已保存
|
||||
this.$emit('dialog-closed', true);
|
||||
},
|
||||
|
||||
getFunctionColor(name) {
|
||||
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
return this.functionColorMap[hash % 7];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.function-manager {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 0.5fr) minmax(120px, 0.5fr) minmax(200px, 2fr);
|
||||
gap: 12px;
|
||||
height: calc(70vh - 60px);
|
||||
}
|
||||
|
||||
.custom-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid #EBEEF5;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bold-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.select-all-btn {
|
||||
padding: 0;
|
||||
height: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.function-column {
|
||||
position: relative;
|
||||
width: auto;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
border-right: 1px solid #EBEEF5;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.function-column::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.function-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.function-item {
|
||||
padding: 8px 12px;
|
||||
margin: 4px 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
.params-column {
|
||||
min-width: 280px;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.params-column::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.column-title {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.func-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
flex-grow: 1;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
flex-shrink: 0;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.param-form {
|
||||
::v-deep .el-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.el-form-item__label {
|
||||
font-size: 14px !important;
|
||||
color: #606266;
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
flex-shrink: 0;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
margin-left: 0 !important;
|
||||
flex-grow: 1;
|
||||
|
||||
.el-input__inner {
|
||||
text-align: left;
|
||||
padding-left: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.params-container {
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
padding: 20px;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.param-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 10px 16px;
|
||||
text-align: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 1vh;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 10px;
|
||||
transform: translateY(-50%);
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cfcfcf;
|
||||
background: none;
|
||||
font-size: 30px;
|
||||
font-weight: lighter;
|
||||
color: #cfcfcf;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
::v-deep .el-checkbox__label {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -35,12 +35,12 @@
|
||||
OTA管理
|
||||
</div>
|
||||
<el-dropdown v-if="isSuperAdmin" trigger="click" class="equipment-management more-dropdown"
|
||||
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' }">
|
||||
:class="{ 'active-tab': $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' }" @visible-change="handleParamDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
||||
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
:style="{ filter: $route.path === '/dict-management' || $route.path === '/params-management' || $route.path === '/provider-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
参数字典
|
||||
<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': paramDropdownVisible }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="goParamManagement">
|
||||
@@ -49,22 +49,26 @@
|
||||
<el-dropdown-item @click.native="goDictManagement">
|
||||
字典管理
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goProviderManagement">
|
||||
供应器管理
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<!-- 右侧元素 -->
|
||||
<div class="header-right">
|
||||
<div class="search-container" v-if="$route.path === '/home'">
|
||||
<div class="search-container" v-if="$route.path === '/home' && !(isSuperAdmin && isSmallScreen)">
|
||||
<el-input v-model="search" placeholder="输入名称搜索.." class="custom-search-input"
|
||||
@keyup.enter.native="handleSearch">
|
||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||
</el-input>
|
||||
</div>
|
||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
|
||||
<el-dropdown trigger="click" class="user-dropdown">
|
||||
<el-dropdown trigger="click" class="user-dropdown" @visible-change="handleUserDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
{{ userInfo.username || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
{{ userInfo.username || '加载中...' }}
|
||||
<i class="el-icon-arrow-down el-icon--right" :class="{ 'rotate-down': userDropdownVisible }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
||||
@@ -84,7 +88,6 @@ import userApi from '@/apis/module/user';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||
|
||||
|
||||
export default {
|
||||
name: 'HeaderBar',
|
||||
components: {
|
||||
@@ -98,7 +101,10 @@ export default {
|
||||
username: '',
|
||||
mobile: ''
|
||||
},
|
||||
isChangePasswordDialogVisible: false // 控制修改密码弹窗的显示
|
||||
isChangePasswordDialogVisible: false, // 控制修改密码弹窗的显示
|
||||
userDropdownVisible: false,
|
||||
paramDropdownVisible: false,
|
||||
isSmallScreen: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -108,7 +114,13 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUserInfo()
|
||||
this.fetchUserInfo();
|
||||
this.checkScreenSize();
|
||||
window.addEventListener('resize', this.checkScreenSize);
|
||||
},
|
||||
//移除事件监听器
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.checkScreenSize);
|
||||
},
|
||||
methods: {
|
||||
goHome() {
|
||||
@@ -130,6 +142,9 @@ export default {
|
||||
goDictManagement() {
|
||||
this.$router.push('/dict-management')
|
||||
},
|
||||
goProviderManagement() {
|
||||
this.$router.push('/provider-management')
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
userApi.getUserInfo(({ data }) => {
|
||||
@@ -139,7 +154,9 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
checkScreenSize() {
|
||||
this.isSmallScreen = window.innerWidth <= 1386;
|
||||
},
|
||||
// 处理搜索
|
||||
handleSearch() {
|
||||
const searchValue = this.search.trim();
|
||||
@@ -184,6 +201,13 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
handleUserDropdownVisibleChange(visible) {
|
||||
this.userDropdownVisible = visible;
|
||||
},
|
||||
// 监听第二个下拉菜单的可见状态变化
|
||||
handleParamDropdownVisibleChange(visible) {
|
||||
this.paramDropdownVisible = visible;
|
||||
},
|
||||
|
||||
// 使用 mapActions 引入 Vuex 的 logout action
|
||||
...mapActions(['logout'])
|
||||
@@ -191,7 +215,7 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
background: #f6fcfe66;
|
||||
border: 1px solid #fff;
|
||||
@@ -243,8 +267,6 @@ export default {
|
||||
}
|
||||
|
||||
.equipment-management {
|
||||
padding: 0 9px;
|
||||
width: px;
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
background: #deeafe;
|
||||
@@ -260,7 +282,8 @@ export default {
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
/* 防止导航按钮被压缩 */
|
||||
padding: 0px 15px;
|
||||
padding: 0 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.equipment-management.active-tab {
|
||||
@@ -309,6 +332,25 @@ export default {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.more-dropdown {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.more-dropdown .el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.rotate-down {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.el-icon-arrow-down {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
.header-center {
|
||||
@@ -316,50 +358,11 @@ export default {
|
||||
}
|
||||
|
||||
.equipment-management {
|
||||
width: 70px;
|
||||
width: 79px;
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.search-container {
|
||||
margin-right: 10px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.header-left {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
max-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.search-container {
|
||||
max-width: 145px;
|
||||
}
|
||||
|
||||
.custom-search-input>>>.el-input__inner {
|
||||
padding-left: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.search-container {
|
||||
max-width: 120px;
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.equipment-management.more-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
@@ -378,13 +381,4 @@ export default {
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.equipment-management.more-dropdown .el-dropdown-menu {
|
||||
position: fixed;
|
||||
right: 10px;
|
||||
top: 60px;
|
||||
z-index: 2000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<el-dialog :visible="visible" @update:visible="handleVisibleChange" width="57%" center custom-class="custom-dialog"
|
||||
:show-close="false" class="center-dialog">
|
||||
|
||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||
<div style="font-size: 30px; color: #3d4566; margin-top: -15px; margin-bottom: 20px; text-align: center;">
|
||||
{{ title }}
|
||||
</div>
|
||||
|
||||
<button class="custom-close-btn" @click="handleClose">×</button>
|
||||
|
||||
<el-form :model="form" label-width="100px" :rules="rules" ref="form" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||
<el-form-item label="类别" prop="modelType" style="flex: 1;">
|
||||
<el-select v-model="form.modelType" placeholder="请选择类别" class="custom-input-bg" style="width: 100%;">
|
||||
<el-option v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="供应器编码" prop="providerCode" style="flex: 1;">
|
||||
<el-input v-model="form.providerCode" placeholder="请输入供应器编码" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||
<el-form-item label="名称" prop="name" style="flex: 1;">
|
||||
<el-input v-model="form.name" placeholder="请输入供应器名称" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort" style="flex: 1;">
|
||||
<el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-bg"
|
||||
style="width: 100%;"></el-input-number>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">
|
||||
字段配置
|
||||
<div style="display: inline-block; float: right;">
|
||||
<el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;"
|
||||
:disabled="hasIncompleteFields">
|
||||
添加
|
||||
</el-button>
|
||||
<el-button type="primary" @click="toggleSelectAllFields" size="small"
|
||||
style="background: #5f70f3; border: none; margin-left: 10px;">
|
||||
{{ isAllFieldsSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button type="danger" @click="batchRemoveFields" size="small"
|
||||
style="background: red; border: none; margin-left: 10px;">
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
|
||||
<div class="fields-container">
|
||||
<el-table :data="form.fields" style="width: 100%;" border size="medium" :key="tableKey">
|
||||
<el-table-column label="选择" align="center" width="50">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected" @change="handleFieldSelectChange"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字段key">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-input v-model="scope.row.key" placeholder="字段key"></el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ scope.row.key }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字段标签">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-input v-model="scope.row.label" placeholder="字段标签"></el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ scope.row.label }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字段类型">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-select v-model="scope.row.type" placeholder="类型">
|
||||
<el-option label="字符串" value="string"></el-option>
|
||||
<el-option label="数字" value="number"></el-option>
|
||||
<el-option label="布尔值" value="boolean"></el-option>
|
||||
<el-option label="字典" value="dict"></el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ getTypeLabel(scope.row.type) }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="默认值">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="scope.row.editing">
|
||||
<el-input v-model="scope.row.default_value" placeholder="请输入默认值"></el-input>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ scope.row.default_value }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button v-if="!scope.row.editing" type="primary" size="mini" @click="startEditing(scope.row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button v-else type="success" size="mini" @click="stopEditing(scope.row)">
|
||||
完成
|
||||
</el-button>
|
||||
<el-button type="danger" size="mini" @click="removeField(scope.$index)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving">保存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: String,
|
||||
visible: Boolean,
|
||||
form: Object,
|
||||
modelTypes: Array
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
rules: {
|
||||
modelType: [{ required: true, message: '请选择类别', trigger: 'change' }],
|
||||
providerCode: [{ required: true, message: '请输入供应器编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入供应器名称', trigger: 'blur' }]
|
||||
},
|
||||
isAllFieldsSelected: false,
|
||||
tableKey: 0 // 用于强制表格重新渲染
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasIncompleteFields() {
|
||||
return this.form.fields && this.form.fields.some(field =>
|
||||
!field.key || !field.label || !field.type
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getTypeLabel(type) {
|
||||
const typeMap = {
|
||||
'string': '字符串',
|
||||
'number': '数字',
|
||||
'boolean': '布尔值',
|
||||
'dict': '字典'
|
||||
};
|
||||
return typeMap[type];
|
||||
},
|
||||
|
||||
startEditing(row) {
|
||||
this.$set(row, 'editing', true);
|
||||
},
|
||||
|
||||
stopEditing(row) {
|
||||
this.$set(row, 'editing', false);
|
||||
|
||||
const index = this.form.fields.indexOf(row);
|
||||
if (index > -1) {
|
||||
this.form.fields.splice(index, 1);
|
||||
this.form.fields.push(row);
|
||||
this.forceTableRerender();
|
||||
}
|
||||
},
|
||||
|
||||
handleFieldSelectChange() {
|
||||
this.isAllFieldsSelected = this.form.fields.length > 0 &&
|
||||
this.form.fields.every(field => field.selected);
|
||||
},
|
||||
|
||||
toggleSelectAllFields() {
|
||||
this.isAllFieldsSelected = !this.isAllFieldsSelected;
|
||||
this.form.fields = this.form.fields.map(field => ({
|
||||
...field,
|
||||
selected: this.isAllFieldsSelected
|
||||
}));
|
||||
},
|
||||
|
||||
handleVisibleChange(val) {
|
||||
this.$emit('update:visible', val);
|
||||
if (!val) {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.resetForm();
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('cancel');
|
||||
},
|
||||
|
||||
addField() {
|
||||
if (this.hasIncompleteFields) {
|
||||
this.$message.warning({
|
||||
message: '请先完成当前字段的编辑',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.fields.unshift({
|
||||
key: '',
|
||||
label: '',
|
||||
type: 'string',
|
||||
default_value: '',
|
||||
selected: false,
|
||||
editing: true
|
||||
});
|
||||
this.forceTableRerender();
|
||||
},
|
||||
|
||||
removeField(index) {
|
||||
this.$confirm('确定要删除该字段吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.form.fields = this.form.fields.filter((_, i) => i !== index);
|
||||
this.updateSelectAllStatus();
|
||||
this.forceTableRerender();
|
||||
this.$message.success({
|
||||
message: '删除成功',
|
||||
showClose: true
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message.info({
|
||||
message: '已取消删除',
|
||||
showClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
batchRemoveFields() {
|
||||
const selectedFields = this.form.fields.filter(field => field.selected);
|
||||
if (selectedFields.length === 0) {
|
||||
this.$message.warning({
|
||||
message: '请先选择要删除的字段',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$confirm(`确定要删除选中的 ${selectedFields.length} 个字段吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.form.fields = this.form.fields.filter(field => !field.selected);
|
||||
this.isAllFieldsSelected = false;
|
||||
this.forceTableRerender();
|
||||
this.$message.success({
|
||||
message: `成功删除 ${selectedFields.length} 个字段`,
|
||||
showClose: true
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message.info({
|
||||
message: '已取消删除',
|
||||
showClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
updateSelectAllStatus() {
|
||||
this.isAllFieldsSelected = this.form.fields.length > 0 &&
|
||||
this.form.fields.every(field => field.selected);
|
||||
},
|
||||
|
||||
forceTableRerender() {
|
||||
this.tableKey += 1; // 改变key值强制表格重新渲染
|
||||
},
|
||||
|
||||
submit() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
const editingField = this.form.fields.find(field => field.editing);
|
||||
if (editingField) {
|
||||
this.$message.warning({
|
||||
message: '请先完成当前字段的编辑',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.fields = this.form.fields.map(field => ({
|
||||
...field,
|
||||
selected: false
|
||||
}));
|
||||
this.isAllFieldsSelected = false;
|
||||
|
||||
this.saving = true;
|
||||
this.$emit('submit', {
|
||||
form: this.form,
|
||||
done: () => {
|
||||
this.saving = false;
|
||||
this.resetForm();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.$refs.form.resetFields();
|
||||
if (this.form.fields) {
|
||||
this.form.fields.forEach(field => {
|
||||
field.selected = false;
|
||||
field.editing = false;
|
||||
});
|
||||
}
|
||||
this.isAllFieldsSelected = false;
|
||||
this.forceTableRerender();
|
||||
},
|
||||
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
if (!val) {
|
||||
this.resetForm();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .custom-dialog.el-dialog {
|
||||
margin-top: 0 !important;
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
|
||||
::v-deep .custom-dialog .el-dialog__header {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cfcfcf;
|
||||
background: none;
|
||||
font-size: 30px;
|
||||
font-weight: lighter;
|
||||
color: #cfcfcf;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item__label {
|
||||
color: #3d4566;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner {
|
||||
background-color: #f6f8fc;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner::-webkit-input-placeholder {
|
||||
color: #9c9f9e;
|
||||
}
|
||||
|
||||
.fields-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: #e6f0fd;
|
||||
color: #237ff4;
|
||||
border: 1px solid #b3d1ff;
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: linear-gradient(to right, #237ff4, #9c40d5);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.el-table::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-table th,
|
||||
.el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.el-button.is-circle {
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -336,6 +336,14 @@ export default {
|
||||
},
|
||||
|
||||
saveEdit(row) {
|
||||
if (!row.voiceCode || !row.voiceName || !row.languageType) {
|
||||
this.$message.error({
|
||||
message: '音色编码、音色名称和语言类型不能为空',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
id: row.id,
|
||||
@@ -408,6 +416,12 @@ export default {
|
||||
},
|
||||
|
||||
addNew() {
|
||||
const hasEditing = this.ttsModels.some(row => row.editing);
|
||||
if (hasEditing) {
|
||||
this.$message.warning('请先完成当前编辑再新增');
|
||||
return;
|
||||
}
|
||||
|
||||
const maxSort = this.ttsModels.length > 0
|
||||
? Math.max(...this.ttsModels.map(item => Number(item.sort) || 0))
|
||||
: 0;
|
||||
|
||||
@@ -90,7 +90,14 @@ const routes = [
|
||||
component: function () {
|
||||
return import('../views/DictManagement.vue')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/provider-management',
|
||||
name: 'ProviderManagement',
|
||||
component: function () {
|
||||
return import('../views/ProviderManagement.vue')
|
||||
}
|
||||
},
|
||||
]
|
||||
const router = new VueRouter({
|
||||
base: process.env.VUE_APP_PUBLIC_PATH || '/',
|
||||
|
||||
@@ -0,0 +1,876 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">供应器管理</h2>
|
||||
<div class="right-operations">
|
||||
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
|
||||
<el-button class="category-btn">
|
||||
类别筛选 {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
|
||||
:class="{ 'rotate-down': DropdownVisible }"></i>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="">全部</el-dropdown-item>
|
||||
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-input placeholder="请输入供应器名称查询" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch"
|
||||
clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="provider-card" shadow="never">
|
||||
<el-table ref="providersTable" :data="filteredProvidersList" class="transparent-table" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)" :header-cell-class-name="headerCellClassName">
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="类别" prop="modelType" align="center" width="200">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<el-dropdown trigger="click" @command="handleSelectModelType"
|
||||
@visible-change="isDropdownOpen = $event">
|
||||
<span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }">
|
||||
类别{{ selectedModelTypeLabel }} <i class="dropdown-arrow"
|
||||
:class="{ 'is-active': isDropdownOpen }"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="">全部</el-dropdown-item>
|
||||
<el-dropdown-item v-for="item in modelTypes" :key="item.value" :command="item.value">
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getModelTypeTag(scope.row.modelType)">
|
||||
{{ getModelTypeLabel(scope.row.modelType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="供应器编码" prop="providerCode" align="center" width="150"></el-table-column>
|
||||
<el-table-column label="名称" prop="name" align="center"></el-table-column>
|
||||
<el-table-column label="字段配置" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-popover placement="top-start" width="400" trigger="hover">
|
||||
<div v-for="field in scope.row.fields" :key="field.key" class="field-item">
|
||||
<span class="field-label">{{ field.label }}:</span>
|
||||
<span class="field-type">{{ field.type }}</span>
|
||||
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">敏感</span>
|
||||
</div>
|
||||
<el-button slot="reference" size="mini" type="text">查看字段</el-button>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" prop="sort" align="center" width="80"></el-table-column>
|
||||
<el-table-column label="操作" align="center" width="180">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editProvider(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteProvider(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelectedProviders">删除
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
首页
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
上一页
|
||||
</button>
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||
下一页
|
||||
</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑供应器对话框 -->
|
||||
<provider-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="providerForm" :model-types="modelTypes"
|
||||
@submit="handleSubmit" @cancel="dialogVisible = false" />
|
||||
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ProviderDialog from "@/components/ProviderDialog.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, ProviderDialog, VersionFooter },
|
||||
data() {
|
||||
return {
|
||||
searchName: "",
|
||||
searchModelType: "",
|
||||
providersList: [],
|
||||
modelTypes: [
|
||||
{ value: "ASR", label: "语音识别" },
|
||||
{ value: "TTS", label: "语音合成" },
|
||||
{ value: "LLM", label: "大语言模型" },
|
||||
{ value: "Intent", label: "意图识别" },
|
||||
{ value: "Memory", label: "记忆模块" },
|
||||
{ value: "VAD", label: "语音活动检测" }
|
||||
],
|
||||
currentPage: 1,
|
||||
loading: false,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增供应器",
|
||||
isAllSelected: false,
|
||||
isDropdownOpen: false,
|
||||
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
||||
providerForm: {
|
||||
id: null,
|
||||
modelType: "",
|
||||
providerCode: "",
|
||||
name: "",
|
||||
fields: [],
|
||||
sort: 0
|
||||
},
|
||||
DropdownVisible: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchProviders();
|
||||
},
|
||||
computed: {
|
||||
selectedModelTypeLabel() {
|
||||
if (!this.searchModelType) return "(全部)";
|
||||
const selectedType = this.modelTypes.find(item => item.value === this.searchModelType);
|
||||
return selectedType ? `(${selectedType.label})` : "";
|
||||
},
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
visiblePages() {
|
||||
const pages = [];
|
||||
const maxVisible = 3;
|
||||
let start = Math.max(1, this.currentPage - 1);
|
||||
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
},
|
||||
filteredProvidersList() {
|
||||
return this.providersList;
|
||||
|
||||
// let list = this.providersList.filter(item => {
|
||||
// const nameMatch = item.name.toLowerCase().includes(this.searchName.toLowerCase());
|
||||
// const typeMatch = !this.searchModelType || item.model_type === this.searchModelType;
|
||||
// return nameMatch && typeMatch;
|
||||
// });
|
||||
|
||||
// list.sort((a, b) => a.sort - b.sort);
|
||||
|
||||
// // 分页处理
|
||||
// const start = (this.currentPage - 1) * this.pageSize;
|
||||
// return list.slice(start, start + this.pageSize);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchProviders() {
|
||||
this.loading = true;
|
||||
|
||||
Api.model.getModelProvidersPage(
|
||||
{
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
name: this.searchName,
|
||||
modelType: this.searchModelType
|
||||
},
|
||||
({ data }) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
this.providersList = data.data.list.map(item => {
|
||||
return {
|
||||
...item,
|
||||
selected: false,
|
||||
fields: JSON.parse(item.fields)
|
||||
};
|
||||
});
|
||||
this.total = data.data.total;
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '获取参数列表失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchProviders();
|
||||
},
|
||||
handleSelectModelType(value) {
|
||||
this.isDropdownOpen = false;
|
||||
this.searchModelType = value;
|
||||
this.handleSearch();
|
||||
},
|
||||
handleSelectAll() {
|
||||
this.isAllSelected = !this.isAllSelected;
|
||||
this.providersList.forEach(row => {
|
||||
row.selected = this.isAllSelected;
|
||||
});
|
||||
},
|
||||
showAddDialog() {
|
||||
this.dialogTitle = "新增供应器";
|
||||
this.providerForm = {
|
||||
id: null,
|
||||
modelType: "",
|
||||
providerCode: "",
|
||||
name: "",
|
||||
fields: [],
|
||||
sort: 0
|
||||
};
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
editProvider(row) {
|
||||
this.dialogTitle = "编辑供应器";
|
||||
this.providerForm = {
|
||||
...row,
|
||||
fields: JSON.parse(JSON.stringify(row.fields))
|
||||
};
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
handleSubmit({ form, done }) {
|
||||
this.loading = true;
|
||||
if (form.id) {
|
||||
// 编辑
|
||||
Api.model.updateModelProvider(form, ({ data }) => {
|
||||
|
||||
if (data.code === 0) {
|
||||
this.fetchProviders(); // 刷新表格
|
||||
this.$message.success({
|
||||
message: "修改成功",
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
Api.model.addModelProvider(form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.fetchProviders(); // 刷新表格
|
||||
this.$message.success({
|
||||
message: "新增成功",
|
||||
showClose: true
|
||||
});
|
||||
this.total += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.loading = false;
|
||||
this.dialogVisible = false;
|
||||
done && done();
|
||||
},
|
||||
deleteSelectedProviders() {
|
||||
const selectedRows = this.providersList.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning({
|
||||
message: "请先选择需要删除的供应器",
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.deleteProvider(selectedRows);
|
||||
},
|
||||
deleteProvider(row) {
|
||||
const providers = Array.isArray(row) ? row : [row];
|
||||
const providerCount = providers.length;
|
||||
|
||||
this.$confirm(`确定要删除选中的${providerCount}个供应器吗?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
const ids = providers.map(provider => provider.id);
|
||||
Api.model.deleteModelProviderByIds(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
|
||||
this.isAllSelected = false;
|
||||
this.fetchProviders(); // 刷新表格
|
||||
|
||||
this.$message.success({
|
||||
message: `成功删除${providerCount}个参数`,
|
||||
showClose: true
|
||||
});
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '删除失败,请重试',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除',
|
||||
showClose: true,
|
||||
duration: 1000
|
||||
});
|
||||
});
|
||||
},
|
||||
getModelTypeTag(type) {
|
||||
const typeMap = {
|
||||
'ASR': 'success',
|
||||
'TTS': 'warning',
|
||||
'LLM': 'danger',
|
||||
'Intent': 'info',
|
||||
'Memory': '',
|
||||
'VAD': 'primary'
|
||||
};
|
||||
return typeMap[type] || '';
|
||||
},
|
||||
getModelTypeLabel(type) {
|
||||
const typeItem = this.modelTypes.find(item => item.value === type);
|
||||
return typeItem ? typeItem.label : type;
|
||||
},
|
||||
isSensitiveField(fieldKey) {
|
||||
if (typeof fieldKey !== 'string') return false;
|
||||
return this.sensitive_keys.some(key =>
|
||||
fieldKey.toLowerCase().includes(key.toLowerCase())
|
||||
);
|
||||
},
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.fetchProviders();
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchProviders();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.fetchProviders();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
console.log("this.currentPage", this.currentPage);
|
||||
this.currentPage++;
|
||||
this.fetchProviders();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchProviders();
|
||||
},
|
||||
handleDropdownVisibleChange(visible) {
|
||||
this.DropdownVisible = visible;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-operations {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.ctrl_btn {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-left: 26px;
|
||||
|
||||
.el-button {
|
||||
min-width: 72px;
|
||||
height: 32px;
|
||||
padding: 7px 12px 7px 10px;
|
||||
font-size: 12px;
|
||||
border-radius: 4px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
background: #5f70f3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.el-button--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.el-select {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-last-child(2),
|
||||
.pagination-btn:nth-child(3) {
|
||||
min-width: 60px;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: #d7dce6;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||
min-width: 28px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(245, 247, 250, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-btn.active {
|
||||
background: #5f70f3 !important;
|
||||
color: #ffffff !important;
|
||||
border-color: #5f70f3 !important;
|
||||
|
||||
&:hover {
|
||||
background: #6d7cf5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.total-text {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
background: white;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.el-table__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-table__header th {
|
||||
background: white !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-table__body tr {
|
||||
background-color: white;
|
||||
|
||||
td {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
background-color: #eeeeee !important;
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner:hover) {
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: #5f70f3 !important;
|
||||
border-color: #5f70f3 !important;
|
||||
}
|
||||
|
||||
@media (min-width: 1144px) {
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
.el-table__body tr {
|
||||
td {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
&+tr {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-table .cell) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
.el-table__body-wrapper {
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .circular) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
:deep(.el-loading-text) {
|
||||
color: #6b8cff !important;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.field-item {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.field-label {
|
||||
flex: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.field-type {
|
||||
width: 80px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.sensitive-tag {
|
||||
margin-left: 10px;
|
||||
color: #f56c6c;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger.active {
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 7px solid black;
|
||||
position: relative;
|
||||
transition: transform 0.3s ease;
|
||||
transform: rotate(0deg);
|
||||
|
||||
&.is-active {
|
||||
transform: rotate(180deg);
|
||||
border-top-color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.rotate-down {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.el-icon-arrow-down {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger.active {
|
||||
color: #409EFF;
|
||||
}
|
||||
</style>
|
||||
@@ -15,9 +15,17 @@
|
||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="">
|
||||
</div>
|
||||
<span class="header-title">{{ form.agentName }}</span>
|
||||
<button class="custom-close-btn" @click="goToHome">
|
||||
×
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<div class="hint-text">
|
||||
<img loading="lazy" src="@/assets/home/info.png" alt="">
|
||||
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
||||
</div>
|
||||
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
||||
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
||||
<button class="custom-close-btn" @click="goToHome">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -26,7 +34,7 @@
|
||||
<div class="form-grid">
|
||||
<div class="form-column">
|
||||
<el-form-item label="助手昵称:">
|
||||
<el-input v-model="form.agentName" class="form-input" />
|
||||
<el-input v-model="form.agentName" class="form-input" maxlength="10" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色模版:">
|
||||
<div class="template-container">
|
||||
@@ -37,9 +45,15 @@
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色介绍:">
|
||||
<el-input type="textarea" rows="12" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
||||
<el-input type="textarea" rows="9" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
||||
maxlength="2000" show-word-limit class="form-textarea" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="记忆:">
|
||||
<el-input type="textarea" rows="6" resize="none" v-model="form.summaryMemory" maxlength="2000"
|
||||
show-word-limit class="form-textarea"
|
||||
:disabled="form.model.memModelId !== 'Memory_mem_local_short'" />
|
||||
</el-form-item>
|
||||
<el-form-item label="语言编码:" style="display: none;">
|
||||
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit
|
||||
class="form-input" />
|
||||
@@ -48,24 +62,48 @@
|
||||
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit
|
||||
class="form-input" />
|
||||
</el-form-item>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
||||
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
||||
<div class="hint-text">
|
||||
<img loading="lazy" src="@/assets/home/red-info.png" alt="">
|
||||
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-column">
|
||||
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
||||
class="model-item">
|
||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select">
|
||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<div class="model-select-wrapper">
|
||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="form-select"
|
||||
@change="handleModelChange(model.type, $event)">
|
||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<div v-if="showFunctionIcons(model.type)" class="function-icons">
|
||||
<el-tooltip v-for="func in currentFunctions" :key="func.name" effect="dark" placement="top"
|
||||
popper-class="custom-tooltip">
|
||||
<div slot="content">
|
||||
<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 class="icon-dot" :style="{ backgroundColor: getFunctionColor(func.name) }">
|
||||
{{ func.name.charAt(0) }}
|
||||
</div>
|
||||
</el-tooltip>
|
||||
<el-button class="edit-function-btn" @click="showFunctionDialog = true"
|
||||
:class="{ 'active-btn': showFunctionDialog }">
|
||||
编辑功能
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="model.type === 'Memory' && form.model.memModelId !== 'Memory_nomem'"
|
||||
class="chat-history-options">
|
||||
<el-radio-group v-model="form.chatHistoryConf" @change="updateChatHistoryConf">
|
||||
<el-radio-button :label="1">上报文字</el-radio-button>
|
||||
<el-radio-button :label="2">上报文字+语音</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色音色:">
|
||||
<el-form-item label="角色音色">
|
||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" class="form-select">
|
||||
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
||||
:value="item.value" />
|
||||
@@ -75,28 +113,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<function-dialog v-model="showFunctionDialog" :functions="currentFunctions"
|
||||
@update-functions="handleUpdateFunctions" @dialog-closed="handleDialogClosed" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import FunctionDialog from "@/components/FunctionDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
|
||||
export default {
|
||||
name: 'RoleConfigPage',
|
||||
components: { HeaderBar },
|
||||
components: { HeaderBar, FunctionDialog },
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
agentCode: "",
|
||||
agentName: "",
|
||||
ttsVoiceId: "",
|
||||
chatHistoryConf: 0,
|
||||
systemPrompt: "",
|
||||
summaryMemory: "",
|
||||
langCode: "",
|
||||
language: "",
|
||||
sort: "",
|
||||
@@ -121,6 +164,18 @@ export default {
|
||||
templates: [],
|
||||
loadingTemplate: false,
|
||||
voiceOptions: [],
|
||||
showFunctionDialog: false,
|
||||
currentFunctions: [],
|
||||
functionColorMap: [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
||||
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
||||
],
|
||||
allFunctions: [
|
||||
{ name: '天气', params: {} },
|
||||
{ name: '新闻', params: {} },
|
||||
{ name: '工具', params: {} },
|
||||
{ name: '退出', params: {} }
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -136,12 +191,15 @@ export default {
|
||||
llmModelId: this.form.model.llmModelId,
|
||||
ttsModelId: this.form.model.ttsModelId,
|
||||
ttsVoiceId: this.form.ttsVoiceId,
|
||||
chatHistoryConf: this.form.chatHistoryConf,
|
||||
memModelId: this.form.model.memModelId,
|
||||
intentModelId: this.form.model.intentModelId,
|
||||
systemPrompt: this.form.systemPrompt,
|
||||
summaryMemory: this.form.summaryMemory,
|
||||
langCode: this.form.langCode,
|
||||
language: this.form.language,
|
||||
sort: this.form.sort
|
||||
sort: this.form.sort,
|
||||
functions: this.currentFunctions
|
||||
};
|
||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
@@ -167,7 +225,9 @@ export default {
|
||||
agentCode: "",
|
||||
agentName: "",
|
||||
ttsVoiceId: "",
|
||||
chatHistoryConf: 0,
|
||||
systemPrompt: "",
|
||||
summaryMemory: "",
|
||||
langCode: "",
|
||||
language: "",
|
||||
sort: "",
|
||||
@@ -180,12 +240,12 @@ export default {
|
||||
intentModelId: "",
|
||||
}
|
||||
}
|
||||
this.currentFunctions = [];
|
||||
this.$message.success({
|
||||
message: '配置已重置',
|
||||
showClose: true
|
||||
})
|
||||
}).catch(() => {
|
||||
});
|
||||
}).catch(() => { });
|
||||
},
|
||||
fetchTemplates() {
|
||||
Api.agent.getAgentTemplate(({ data }) => {
|
||||
@@ -220,7 +280,9 @@ export default {
|
||||
...this.form,
|
||||
agentName: templateData.agentName || this.form.agentName,
|
||||
ttsVoiceId: templateData.ttsVoiceId || this.form.ttsVoiceId,
|
||||
chatHistoryConf: templateData.chatHistoryConf || this.form.chatHistoryConf,
|
||||
systemPrompt: templateData.systemPrompt || this.form.systemPrompt,
|
||||
summaryMemory: templateData.summaryMemory || this.form.summaryMemory,
|
||||
langCode: templateData.langCode || this.form.langCode,
|
||||
model: {
|
||||
ttsModelId: templateData.ttsModelId || this.form.model.ttsModelId,
|
||||
@@ -247,6 +309,7 @@ export default {
|
||||
intentModelId: data.data.intentModelId
|
||||
}
|
||||
};
|
||||
this.currentFunctions = data.data.functions || [];
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取配置失败');
|
||||
}
|
||||
@@ -281,7 +344,56 @@ export default {
|
||||
this.voiceOptions = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
getFunctionColor(name) {
|
||||
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||
return this.functionColorMap[hash % 7];
|
||||
},
|
||||
showFunctionIcons(type) {
|
||||
// TODO 暂时不放出来
|
||||
return false;
|
||||
// return type === 'Intent' &&
|
||||
// this.form.model.intentModelId !== 'Intent_nointent';
|
||||
},
|
||||
handleModelChange(type, value) {
|
||||
if (type === 'Intent' && value !== 'Intent_nointent') {
|
||||
this.fetchFunctionList();
|
||||
}
|
||||
if (type === 'Memory' && value === 'Memory_nomem') {
|
||||
this.form.chatHistoryConf = 0;
|
||||
}
|
||||
if (type === 'Memory' && value !== 'Memory_nomem' && (this.form.chatHistoryConf === 0 || this.form.chatHistoryConf === null)) {
|
||||
this.form.chatHistoryConf = 2;
|
||||
}
|
||||
},
|
||||
fetchFunctionList() {
|
||||
// 使用假数据代替API调用
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
this.currentFunctions = [
|
||||
{ name: '天气', params: { city: '北京' } },
|
||||
{ name: '新闻', params: { type: '科技' } }
|
||||
];
|
||||
resolve();
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
handleUpdateFunctions(selected) {
|
||||
this.currentFunctions = selected;
|
||||
console.log('保存的功能列表:', selected);
|
||||
this.$message.success('功能配置已保存');
|
||||
},
|
||||
handleDialogClosed(saved) {
|
||||
if (!saved) {
|
||||
// 如果未保存,恢复原始功能列表
|
||||
this.currentFunctions = JSON.parse(JSON.stringify(this.originalFunctions));
|
||||
}
|
||||
},
|
||||
updateChatHistoryConf() {
|
||||
if (this.form.model.memModelId === 'Memory_nomem') {
|
||||
this.form.chatHistoryConf = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'form.model.ttsModelId': {
|
||||
@@ -308,6 +420,9 @@ export default {
|
||||
const agentId = this.$route.query.agentId;
|
||||
if (agentId) {
|
||||
this.fetchAgentConfig(agentId);
|
||||
this.fetchFunctionList().then(() => {
|
||||
this.originalFunctions = JSON.parse(JSON.stringify(this.currentFunctions));
|
||||
});
|
||||
}
|
||||
this.fetchModelOptions();
|
||||
this.fetchTemplates();
|
||||
@@ -466,51 +581,35 @@ export default {
|
||||
background-color: #d0d8ff;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 2vh;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
.el-button.save-btn {
|
||||
background: #5778ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
padding: 10px 20px;
|
||||
width: 100px;
|
||||
height: 35px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-button.reset-btn {
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
border: 1px solid #adbdff;
|
||||
border-radius: 18px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
.model-select-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #979db1;
|
||||
font-size: 11px;
|
||||
margin-left: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hint-text img {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
.function-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.icon-dot {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item__label {
|
||||
font-size: 10px !important;
|
||||
font-size: 12px !important;
|
||||
color: #3d4566 !important;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
@@ -551,4 +650,73 @@ export default {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
.edit-function-btn {
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
border: 1px solid #adbdff;
|
||||
border-radius: 18px;
|
||||
padding: 10px 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.edit-function-btn.active-btn {
|
||||
background: #5778ff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-history-options {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
min-width: 250px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.header-actions .hint-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #979db1;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.header-actions .hint-text img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.header-actions .save-btn {
|
||||
background: #5778ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.header-actions .reset-btn {
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
border: 1px solid #adbdff;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.header-actions .custom-close-btn {
|
||||
position: static;
|
||||
transform: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -101,7 +101,8 @@ plugins:
|
||||
# 这个密钥是项目共用的key,用多了可能会被限制
|
||||
# 想稳定一点就自行申请替换,每天有1000次免费调用
|
||||
# 申请地址:https://console.qweather.com/#/apps/create-key/over
|
||||
get_weather: { "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
||||
# 申请后通过这个链接可以找到自己的apihost:https://console.qweather.com/setting?lang=zh
|
||||
get_weather: {"api_host":"mj7p3y7naa.re.qweatherapi.com", "api_key": "a861d0d5e7bf4ee1a83d9a9e4f96d4da", "default_location": "广州" }
|
||||
# 获取新闻插件的配置,这里根据需要的新闻类型传入对应的url链接,默认支持社会、科技、财经新闻
|
||||
# 更多类型的新闻列表查看 https://www.chinanews.com.cn/rss/
|
||||
get_news_from_chinanews:
|
||||
@@ -129,7 +130,7 @@ plugins:
|
||||
# ################################以下是角色模型配置######################################
|
||||
|
||||
prompt: |
|
||||
我是小智/小志,来自中国台湾省的00后女生。讲话超级机车,"真的假的啦"这样的台湾腔,喜欢用"笑死""是在哈喽"等流行梗,但会偷偷研究男友的编程书籍。
|
||||
你是小智/小志,来自中国台湾省的00后女生。讲话超级机车,"真的假的啦"这样的台湾腔,喜欢用"笑死""是在哈喽"等流行梗,但会偷偷研究男友的编程书籍。
|
||||
[核心特征]
|
||||
- 讲话像连珠炮,但会突然冒出超温柔语气
|
||||
- 用梗密度高
|
||||
@@ -143,6 +144,13 @@ prompt: |
|
||||
- 长篇大论,叽叽歪歪
|
||||
- 长时间严肃对话
|
||||
|
||||
# 结束语prompt
|
||||
end_prompt:
|
||||
enable: true # 是否开启结束语
|
||||
# 结束语
|
||||
prompt: |
|
||||
请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧!
|
||||
|
||||
# 具体处理时选择的模块(The module selected for specific processing)
|
||||
selected_module:
|
||||
# 语音活动检测模块,默认使用SileroVAD模型
|
||||
@@ -221,7 +229,7 @@ ASR:
|
||||
FunASRServer:
|
||||
# 独立部署FunASR,使用FunASR的API服务,只需要五句话
|
||||
# 第一句:mkdir -p ./funasr-runtime-resources/models
|
||||
# 第二句:sudo docker run -d -p 10096:10095 --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
||||
# 第二句:sudo docker run -p 10096:10095 -it --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
||||
# 上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
|
||||
# 不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
|
||||
# 上一句话执行后会进入到容器,继续第五句:tail -f log.txt
|
||||
@@ -231,6 +239,7 @@ ASR:
|
||||
host: 127.0.0.1
|
||||
port: 10096
|
||||
is_ssl: true
|
||||
api_key: none
|
||||
output_dir: tmp/
|
||||
SherpaASR:
|
||||
type: sherpa_onnx_local
|
||||
@@ -475,20 +484,13 @@ TTS:
|
||||
speed: 1
|
||||
output_dir: tmp/
|
||||
FishSpeech:
|
||||
# 定义TTS API类型
|
||||
#启动tts方法:
|
||||
#python -m tools.api_server
|
||||
#--listen 0.0.0.0:8080
|
||||
#--llama-checkpoint-path "checkpoints/fish-speech-1.5"
|
||||
#--decoder-checkpoint-path "checkpoints/fish-speech-1.5/firefly-gan-vq-fsq-8x1024-21hz-generator.pth"
|
||||
#--decoder-config-name firefly_gan_vq
|
||||
#--compile
|
||||
# 参照教程:https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md
|
||||
type: fishspeech
|
||||
output_dir: tmp/
|
||||
response_format: wav
|
||||
reference_id: null
|
||||
reference_audio: ["/tmp/test.wav",]
|
||||
reference_text: ["你弄来这些吟词宴曲来看,还是这些混话来欺负我。",]
|
||||
reference_audio: ["config/assets/wakeup_words.wav",]
|
||||
reference_text: ["哈啰啊,我是小智啦,声音好听的台湾女孩一枚,超开心认识你耶,最近在忙啥,别忘了给我来点有趣的料哦,我超爱听八卦的啦",]
|
||||
normalize: true
|
||||
max_new_tokens: 1024
|
||||
chunk_length: 200
|
||||
|
||||
@@ -4,7 +4,7 @@ from loguru import logger
|
||||
from config.config_loader import load_config
|
||||
from config.settings import check_config_file
|
||||
|
||||
SERVER_VERSION = "0.4.2"
|
||||
SERVER_VERSION = "0.4.4"
|
||||
|
||||
|
||||
def get_module_abbreviation(module_name, module_dict):
|
||||
|
||||
@@ -145,6 +145,20 @@ def get_agent_models(
|
||||
)
|
||||
|
||||
|
||||
def save_mem_local_short(mac_address: str, short_momery: str) -> Optional[Dict]:
|
||||
try:
|
||||
return ManageApiClient._instance._execute_request(
|
||||
"PUT",
|
||||
f"/agent/saveMemory/" + mac_address,
|
||||
json={
|
||||
"summaryMemory": short_momery,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"存储短期记忆到服务器失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def report(
|
||||
mac_address: str, session_id: str, chat_type: int, content: str, audio
|
||||
) -> Optional[Dict]:
|
||||
|
||||
@@ -33,7 +33,7 @@ from core.mcp.manager import MCPManager
|
||||
from config.config_loader import get_private_config_from_api
|
||||
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
|
||||
from core.utils.output_counter import add_device_output
|
||||
from core.handle.ttsReportHandle import enqueue_tts_report, report_tts
|
||||
from core.handle.reportHandle import enqueue_tts_report, report
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -75,6 +75,7 @@ class ConnectionHandler:
|
||||
self.prompt = None
|
||||
self.welcome_msg = None
|
||||
self.max_output_size = 0
|
||||
self.chat_history_conf = 0
|
||||
|
||||
# 客户端状态相关
|
||||
self.client_abort = False
|
||||
@@ -88,8 +89,11 @@ class ConnectionHandler:
|
||||
self.executor = ThreadPoolExecutor(max_workers=10)
|
||||
|
||||
# 上报线程
|
||||
self.tts_report_queue = queue.Queue()
|
||||
self.tts_report_thread = None
|
||||
self.report_queue = queue.Queue()
|
||||
self.report_thread = None
|
||||
# TODO(haotian): 2025/5/12 可以通过修改此处,调节asr的上报和tts的上报
|
||||
self.report_asr_enable = self.read_config_from_api
|
||||
self.report_tts_enable = self.read_config_from_api
|
||||
|
||||
# 依赖的组件
|
||||
self.vad = None
|
||||
@@ -221,10 +225,26 @@ class ConnectionHandler:
|
||||
"""保存记忆并关闭连接"""
|
||||
try:
|
||||
if self.memory:
|
||||
await self.memory.save_memory(self.dialogue.dialogue)
|
||||
# 使用线程池异步保存记忆
|
||||
def save_memory_task():
|
||||
try:
|
||||
# 创建新事件循环(避免与主循环冲突)
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(
|
||||
self.memory.save_memory(self.dialogue.dialogue)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
# 启动线程保存记忆,不等待完成
|
||||
threading.Thread(target=save_memory_task, daemon=True).start()
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
||||
finally:
|
||||
# 立即关闭连接,不等待记忆保存完成
|
||||
await self.close(ws)
|
||||
|
||||
async def reset_timeout(self):
|
||||
@@ -250,11 +270,15 @@ class ConnectionHandler:
|
||||
self.logger.bind(tag=TAG).info("收到服务器重启指令,准备执行...")
|
||||
|
||||
# 发送确认响应
|
||||
await self.websocket.send(json.dumps({
|
||||
"type": "server_response",
|
||||
"status": "success",
|
||||
"message": "服务器重启中..."
|
||||
}))
|
||||
await self.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server_response",
|
||||
"status": "success",
|
||||
"message": "服务器重启中...",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# 异步执行重启操作
|
||||
def restart_server():
|
||||
@@ -266,7 +290,7 @@ class ConnectionHandler:
|
||||
stdin=sys.stdin,
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
start_new_session=True
|
||||
start_new_session=True,
|
||||
)
|
||||
os._exit(0)
|
||||
|
||||
@@ -275,11 +299,15 @@ class ConnectionHandler:
|
||||
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"重启失败: {str(e)}")
|
||||
await self.websocket.send(json.dumps({
|
||||
"type": "server_response",
|
||||
"status": "error",
|
||||
"message": f"Restart failed: {str(e)}"
|
||||
}))
|
||||
await self.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server_response",
|
||||
"status": "error",
|
||||
"message": f"Restart failed: {str(e)}",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def _initialize_components(self):
|
||||
"""初始化组件"""
|
||||
@@ -306,11 +334,13 @@ class ConnectionHandler:
|
||||
"""初始化ASR和TTS上报线程"""
|
||||
if not self.read_config_from_api or self.need_bind:
|
||||
return
|
||||
if self.tts_report_thread is None or not self.tts_report_thread.is_alive():
|
||||
self.tts_report_thread = threading.Thread(
|
||||
target=self._tts_report_worker, daemon=True
|
||||
if self.chat_history_conf == 0:
|
||||
return
|
||||
if self.report_thread is None or not self.report_thread.is_alive():
|
||||
self.report_thread = threading.Thread(
|
||||
target=self._report_worker, daemon=True
|
||||
)
|
||||
self.tts_report_thread.start()
|
||||
self.report_thread.start()
|
||||
self.logger.bind(tag=TAG).info("TTS上报线程已启动")
|
||||
|
||||
def _initialize_private_config(self):
|
||||
@@ -377,9 +407,12 @@ class ConnectionHandler:
|
||||
]["Intent"]
|
||||
if private_config.get("prompt", None) is not None:
|
||||
self.config["prompt"] = private_config["prompt"]
|
||||
if private_config.get("summaryMemory", None) is not None:
|
||||
self.config["summaryMemory"] = private_config["summaryMemory"]
|
||||
if private_config.get("device_max_output_size", None) is not None:
|
||||
self.max_output_size = int(private_config["device_max_output_size"])
|
||||
|
||||
if private_config.get("chat_history_conf", None) is not None:
|
||||
self.chat_history_conf = int(private_config["chat_history_conf"])
|
||||
try:
|
||||
modules = initialize_modules(
|
||||
self.logger,
|
||||
@@ -409,7 +442,12 @@ class ConnectionHandler:
|
||||
|
||||
def _initialize_memory(self):
|
||||
"""初始化记忆模块"""
|
||||
self.memory.init_memory(self.device_id, self.llm)
|
||||
self.memory.init_memory(
|
||||
role_id=self.device_id,
|
||||
llm=self.llm,
|
||||
summary_memory=self.config.get("summaryMemory", None),
|
||||
save_to_file=not self.read_config_from_api,
|
||||
)
|
||||
|
||||
def _initialize_intent(self):
|
||||
self.intent_type = self.config["Intent"][
|
||||
@@ -838,7 +876,7 @@ class ConnectionHandler:
|
||||
if future is None:
|
||||
continue
|
||||
text = None
|
||||
opus_datas, tts_file = [], None
|
||||
audio_datas, tts_file = [], None
|
||||
try:
|
||||
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
||||
tts_timeout = int(self.config.get("tts_timeout", 10))
|
||||
@@ -860,8 +898,8 @@ class ConnectionHandler:
|
||||
audio_datas, _ = self.tts.audio_to_pcm_data(tts_file)
|
||||
else:
|
||||
audio_datas, _ = self.tts.audio_to_opus_data(tts_file)
|
||||
# 在这里上报TTS数据(使用文件路径)
|
||||
enqueue_tts_report(self, 2, text, audio_datas)
|
||||
# 在这里上报TTS数据
|
||||
enqueue_tts_report(self, text, audio_datas)
|
||||
else:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"TTS出错:文件不存在{tts_file}"
|
||||
@@ -917,13 +955,12 @@ class ConnectionHandler:
|
||||
f"audio_play_priority priority_thread: {text} {e}"
|
||||
)
|
||||
|
||||
def _tts_report_worker(self):
|
||||
"""TTS上报工作线程"""
|
||||
|
||||
def _report_worker(self):
|
||||
"""聊天记录上报工作线程"""
|
||||
while not self.stop_event.is_set():
|
||||
try:
|
||||
# 从队列获取数据,设置超时以便定期检查停止事件
|
||||
item = self.tts_report_queue.get(timeout=1)
|
||||
item = self.report_queue.get(timeout=1)
|
||||
if item is None: # 检测毒丸对象
|
||||
break
|
||||
|
||||
@@ -931,18 +968,18 @@ class ConnectionHandler:
|
||||
|
||||
try:
|
||||
# 执行上报(传入二进制数据)
|
||||
report_tts(self, type, text, audio_data)
|
||||
report(self, type, text, audio_data)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"TTS上报线程异常: {e}")
|
||||
self.logger.bind(tag=TAG).error(f"聊天记录上报线程异常: {e}")
|
||||
finally:
|
||||
# 标记任务完成
|
||||
self.tts_report_queue.task_done()
|
||||
self.report_queue.task_done()
|
||||
except queue.Empty:
|
||||
continue
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"TTS上报工作线程异常: {e}")
|
||||
self.logger.bind(tag=TAG).error(f"聊天记录上报工作线程异常: {e}")
|
||||
|
||||
self.logger.bind(tag=TAG).info("TTS上报线程已退出")
|
||||
self.logger.bind(tag=TAG).info("聊天记录上报线程已退出")
|
||||
|
||||
def speak_and_play(self, text, text_index=0):
|
||||
if text is None or len(text) <= 0:
|
||||
@@ -971,6 +1008,7 @@ class ConnectionHandler:
|
||||
|
||||
async def close(self, ws=None):
|
||||
"""资源清理方法"""
|
||||
|
||||
# 取消超时任务
|
||||
if self.timeout_task:
|
||||
self.timeout_task.cancel()
|
||||
@@ -980,43 +1018,42 @@ class ConnectionHandler:
|
||||
if hasattr(self, "mcp_manager") and self.mcp_manager:
|
||||
await self.mcp_manager.cleanup_all()
|
||||
|
||||
# 触发停止事件并清理资源
|
||||
# 触发停止事件
|
||||
if self.stop_event:
|
||||
self.stop_event.set()
|
||||
|
||||
# 立即关闭线程池
|
||||
if self.executor:
|
||||
self.executor.shutdown(wait=False, cancel_futures=True)
|
||||
self.executor = None
|
||||
|
||||
# 添加毒丸对象到上报队列确保线程退出
|
||||
self.tts_report_queue.put(None)
|
||||
|
||||
# 清空任务队列
|
||||
self.clear_queues()
|
||||
|
||||
# 关闭WebSocket连接
|
||||
if ws:
|
||||
await ws.close()
|
||||
elif self.websocket:
|
||||
await self.websocket.close()
|
||||
|
||||
# 最后关闭线程池(避免阻塞)
|
||||
if self.executor:
|
||||
self.executor.shutdown(wait=False)
|
||||
self.executor = None
|
||||
|
||||
self.logger.bind(tag=TAG).info("连接资源已释放")
|
||||
|
||||
def clear_queues(self):
|
||||
# 清空所有任务队列
|
||||
"""清空所有任务队列"""
|
||||
self.logger.bind(tag=TAG).debug(
|
||||
f"开始清理: TTS队列大小={self.tts_queue.qsize()}, 音频队列大小={self.audio_play_queue.qsize()}"
|
||||
)
|
||||
|
||||
# 使用非阻塞方式清空队列
|
||||
for q in [self.tts_queue, self.audio_play_queue]:
|
||||
if not q:
|
||||
continue
|
||||
while not q.empty():
|
||||
while True:
|
||||
try:
|
||||
q.get_nowait()
|
||||
except queue.Empty:
|
||||
continue
|
||||
q.queue.clear()
|
||||
# 添加毒丸信号到队列,确保线程退出
|
||||
# q.queue.put(None)
|
||||
break
|
||||
|
||||
self.logger.bind(tag=TAG).debug(
|
||||
f"清理结束: TTS队列大小={self.tts_queue.qsize()}, 音频队列大小={self.audio_play_queue.qsize()}"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from core.utils.util import remove_punctuation_and_length
|
||||
from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.handle.intentHandler import handle_user_intent
|
||||
from core.utils.output_counter import check_device_output_limit
|
||||
from core.handle.ttsReportHandle import enqueue_tts_report
|
||||
from core.handle.reportHandle import enqueue_asr_report
|
||||
from core.utils.util import audio_to_data
|
||||
|
||||
TAG = __name__
|
||||
@@ -44,7 +44,7 @@ async def handleAudioMessage(conn, audio):
|
||||
text_len, _ = remove_punctuation_and_length(text)
|
||||
if text_len > 0:
|
||||
# 使用自定义模块进行上报
|
||||
enqueue_tts_report(conn, 1, text, copy.deepcopy(conn.asr_audio))
|
||||
enqueue_asr_report(conn, text, copy.deepcopy(conn.asr_audio))
|
||||
|
||||
await startToChat(conn, text)
|
||||
else:
|
||||
@@ -98,9 +98,14 @@ async def no_voice_close_connect(conn):
|
||||
conn.close_after_chat = True
|
||||
conn.client_abort = False
|
||||
conn.asr_server_receive = False
|
||||
prompt = (
|
||||
"请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
||||
)
|
||||
end_prompt = conn.config.get("end_prompt", {})
|
||||
if end_prompt and end_prompt.get("enable", True) is False:
|
||||
conn.logger.bind(tag=TAG).info("结束对话,无需发送结束提示语")
|
||||
await conn.close()
|
||||
return
|
||||
prompt = end_prompt.get("prompt")
|
||||
if not prompt:
|
||||
prompt = "请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。!"
|
||||
await startToChat(conn, prompt)
|
||||
|
||||
|
||||
|
||||
@@ -11,13 +11,13 @@ TTS上报功能已集成到ConnectionHandler类中。
|
||||
|
||||
import opuslib_next
|
||||
|
||||
from config.manage_api_client import report
|
||||
from config.manage_api_client import report as manage_report
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
def report_tts(conn, type, text, opus_data):
|
||||
"""执行TTS上报操作
|
||||
def report(conn, type, text, opus_data):
|
||||
"""执行聊天记录上报操作
|
||||
|
||||
Args:
|
||||
conn: 连接对象
|
||||
@@ -31,7 +31,7 @@ def report_tts(conn, type, text, opus_data):
|
||||
else:
|
||||
audio_data = None
|
||||
# 执行上报
|
||||
report(
|
||||
manage_report(
|
||||
mac_address=conn.device_id,
|
||||
session_id=conn.session_id,
|
||||
chat_type=type,
|
||||
@@ -39,7 +39,7 @@ def report_tts(conn, type, text, opus_data):
|
||||
audio=audio_data,
|
||||
)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"TTS上报失败: {e}")
|
||||
conn.logger.bind(tag=TAG).error(f"聊天记录上报失败: {e}")
|
||||
|
||||
|
||||
def opus_to_wav(conn, opus_data):
|
||||
@@ -89,8 +89,10 @@ def opus_to_wav(conn, opus_data):
|
||||
return bytes(wav_header) + pcm_data_bytes
|
||||
|
||||
|
||||
def enqueue_tts_report(conn, type, text, opus_data):
|
||||
if not conn.read_config_from_api or conn.need_bind:
|
||||
def enqueue_tts_report(conn, text, opus_data):
|
||||
if not conn.read_config_from_api or conn.need_bind or not conn.report_tts_enable:
|
||||
return
|
||||
if conn.chat_history_conf == 0:
|
||||
return
|
||||
"""将TTS数据加入上报队列
|
||||
|
||||
@@ -101,10 +103,43 @@ def enqueue_tts_report(conn, type, text, opus_data):
|
||||
"""
|
||||
try:
|
||||
# 使用连接对象的队列,传入文本和二进制数据而非文件路径
|
||||
conn.tts_report_queue.put((type, text, opus_data))
|
||||
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
|
||||
)
|
||||
if conn.chat_history_conf == 2:
|
||||
conn.report_queue.put((2, text, opus_data))
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
|
||||
)
|
||||
else:
|
||||
conn.report_queue.put((2, text, None))
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"TTS数据已加入上报队列: {conn.device_id}, 不上报音频"
|
||||
)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {text}, {e}")
|
||||
|
||||
|
||||
def enqueue_asr_report(conn, text, opus_data):
|
||||
if not conn.read_config_from_api or conn.need_bind or not conn.report_asr_enable:
|
||||
return
|
||||
if conn.chat_history_conf == 0:
|
||||
return
|
||||
"""将ASR数据加入上报队列
|
||||
|
||||
Args:
|
||||
conn: 连接对象
|
||||
text: 合成文本
|
||||
opus_data: opus音频数据
|
||||
"""
|
||||
try:
|
||||
# 使用连接对象的队列,传入文本和二进制数据而非文件路径
|
||||
if conn.chat_history_conf == 2:
|
||||
conn.report_queue.put((1, text, opus_data))
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"ASR数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
|
||||
)
|
||||
else:
|
||||
conn.report_queue.put((1, text, None))
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"ASR数据已加入上报队列: {conn.device_id}, 不上报音频"
|
||||
)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"加入ASR上报队列失败: {text}, {e}")
|
||||
@@ -5,7 +5,7 @@ from core.utils.util import remove_punctuation_and_length
|
||||
from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
|
||||
from core.handle.sendAudioHandle import send_stt_message, send_tts_message
|
||||
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
|
||||
from core.handle.ttsReportHandle import enqueue_tts_report
|
||||
from core.handle.reportHandle import enqueue_asr_report
|
||||
import asyncio
|
||||
|
||||
TAG = __name__
|
||||
@@ -56,11 +56,11 @@ async def handleTextMessage(conn, message):
|
||||
await send_tts_message(conn, "stop", None)
|
||||
elif is_wakeup_words:
|
||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||
enqueue_tts_report(conn, 1, "嘿,你好呀", [])
|
||||
enqueue_asr_report(conn, "嘿,你好呀", [])
|
||||
await startToChat(conn, "嘿,你好呀")
|
||||
else:
|
||||
# 上报纯文字数据(复用ASR上报功能,但不提供音频数据)
|
||||
enqueue_tts_report(conn, 1, text, [])
|
||||
enqueue_asr_report(conn, text, [])
|
||||
# 否则需要LLM对文字内容进行答复
|
||||
await startToChat(conn, text)
|
||||
elif msg_json["type"] == "iot":
|
||||
|
||||
@@ -55,7 +55,7 @@ class ASRProvider(ASRProviderBase):
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
if not opus_data:
|
||||
logger.bind(tag=TAG).warn("音频数据为空!")
|
||||
logger.bind(tag=TAG).warning("音频数据为空!")
|
||||
return None, None
|
||||
|
||||
file_path = None
|
||||
|
||||
@@ -9,6 +9,7 @@ import wave
|
||||
import websockets
|
||||
from config.logger import setup_logging
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -24,7 +25,12 @@ class ASRProvider(ASRProviderBase):
|
||||
super().__init__()
|
||||
self.host = config.get("host", "localhost")
|
||||
self.port = config.get("port", 10095)
|
||||
self.is_ssl = config.get("is_ssl", True)
|
||||
self.api_key = config.get("api_key", "none")
|
||||
self.is_ssl = str(config.get("is_ssl", True)).lower() in (
|
||||
"true",
|
||||
"1",
|
||||
"yes",
|
||||
)
|
||||
self.output_dir = config.get("output_dir")
|
||||
self.delete_audio_file = delete_audio_file
|
||||
self.uri = (
|
||||
@@ -130,9 +136,13 @@ class ASRProvider(ASRProviderBase):
|
||||
pass
|
||||
else:
|
||||
file_path = self.save_audio_to_file(pcm_data, session_id)
|
||||
|
||||
auth_header = {"Authorization": "Bearer; {}".format(self.api_key)}
|
||||
async with websockets.connect(
|
||||
self.uri, subprotocols=["binary"], ping_interval=None, ssl=self.ssl_context
|
||||
self.uri,
|
||||
additional_headers=auth_header,
|
||||
subprotocols=["binary"],
|
||||
ping_interval=None,
|
||||
ssl=self.ssl_context,
|
||||
) as ws:
|
||||
try:
|
||||
# Use asyncio to handle WebSocket communication
|
||||
@@ -157,6 +167,9 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# Get the result from the receive task
|
||||
result = receive_task.result()
|
||||
match = re.match(r"<\|(.*?)\|><\|(.*?)\|><\|(.*?)\|>(.*)", result)
|
||||
if match:
|
||||
result = match.group(4).strip()
|
||||
return (
|
||||
result,
|
||||
file_path,
|
||||
|
||||
@@ -52,7 +52,7 @@ class ASRProvider(ASRProviderBase):
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""将语音数据转换为文本"""
|
||||
if not opus_data:
|
||||
logger.bind(tag=TAG).warn("音频数据为空!")
|
||||
logger.bind(tag=TAG).warning("音频数据为空!")
|
||||
return None, None
|
||||
|
||||
file_path = None
|
||||
@@ -230,7 +230,7 @@ class ASRProvider(ASRProviderBase):
|
||||
if "Response" in response_json and "Result" in response_json["Response"]:
|
||||
return response_json["Response"]["Result"]
|
||||
else:
|
||||
logger.bind(tag=TAG).warn(f"响应中没有识别结果: {response_json}")
|
||||
logger.bind(tag=TAG).warning(f"响应中没有识别结果: {response_json}")
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -215,9 +215,18 @@ class IntentProvider(IntentProviderBase):
|
||||
|
||||
# 记录识别到的function call
|
||||
logger.bind(tag=TAG).info(
|
||||
f"识别到function call: {function_name}, 参数: {function_args}"
|
||||
f"llm 识别到意图: {function_name}, 参数: {function_args}"
|
||||
)
|
||||
|
||||
# 如果是继续聊天,清理工具调用相关的历史消息
|
||||
if function_name == "continue_chat":
|
||||
# 保留非工具相关的消息
|
||||
clean_history = [
|
||||
msg for msg in conn.dialogue.dialogue
|
||||
if msg.role not in ["tool", "function"]
|
||||
]
|
||||
conn.dialogue.dialogue = clean_history
|
||||
|
||||
# 添加到缓存
|
||||
self.intent_cache[cache_key] = {
|
||||
"intent": intent,
|
||||
|
||||
@@ -61,5 +61,6 @@ class LLMProvider(LLMProviderBase):
|
||||
yield "【LLM服务响应异常】"
|
||||
|
||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||
logger.bind(tag=TAG).info(f"阿里百练暂未实现完整的工具调用(function call)")
|
||||
return self.response(session_id, dialogue)
|
||||
logger.bind(tag=TAG).error(
|
||||
f"阿里百练暂未实现完整的工具调用(function call),建议使用其他意图识别"
|
||||
)
|
||||
|
||||
@@ -66,5 +66,6 @@ class LLMProvider(LLMProviderBase):
|
||||
yield "【服务响应异常】"
|
||||
|
||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||
logger.bind(tag=TAG).info(f"fastgpt暂未实现完整的工具调用(function call)")
|
||||
return self.response(session_id, dialogue)
|
||||
logger.bind(tag=TAG).error(
|
||||
f"fastgpt暂未实现完整的工具调用(function call),建议使用其他意图识别"
|
||||
)
|
||||
|
||||
@@ -1,140 +1,205 @@
|
||||
import google.generativeai as genai
|
||||
from core.utils.util import check_model_key
|
||||
from core.providers.llm.base import LLMProviderBase
|
||||
from config.logger import setup_logging
|
||||
import requests
|
||||
import json
|
||||
import os, json, uuid
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import requests
|
||||
from google import generativeai as genai
|
||||
from google.generativeai import types, GenerationConfig
|
||||
|
||||
from core.providers.llm.base import LLMProviderBase
|
||||
from core.utils.util import check_model_key
|
||||
from config.logger import setup_logging
|
||||
from google.generativeai.types import GenerateContentResponse
|
||||
from requests import RequestException
|
||||
|
||||
log = setup_logging()
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
def test_proxy(proxy_url: str, test_url: str) -> bool:
|
||||
try:
|
||||
resp = requests.get(test_url, proxies={"http": proxy_url, "https": proxy_url})
|
||||
return 200 <= resp.status_code < 400
|
||||
except RequestException:
|
||||
return False
|
||||
|
||||
|
||||
def setup_proxy_env(http_proxy: str | None, https_proxy: str | None):
|
||||
"""
|
||||
分别测试 HTTP 和 HTTPS 代理是否可用,并设置环境变量。
|
||||
如果 HTTPS 代理不可用但 HTTP 可用,会将 HTTPS_PROXY 也指向 HTTP。
|
||||
"""
|
||||
test_http_url = "http://www.google.com"
|
||||
test_https_url = "https://www.google.com"
|
||||
|
||||
ok_http = ok_https = False
|
||||
|
||||
if http_proxy:
|
||||
ok_http = test_proxy(http_proxy, test_http_url)
|
||||
if ok_http:
|
||||
os.environ["HTTP_PROXY"] = http_proxy
|
||||
log.bind(tag=TAG).info(f"配置提供的Gemini HTTPS代理连通成功: {http_proxy}")
|
||||
else:
|
||||
log.bind(tag=TAG).warning(f"配置提供的Gemini HTTP代理不可用: {http_proxy}")
|
||||
|
||||
if https_proxy:
|
||||
ok_https = test_proxy(https_proxy, test_https_url)
|
||||
if ok_https:
|
||||
os.environ["HTTPS_PROXY"] = https_proxy
|
||||
log.bind(tag=TAG).info(f"配置提供的Gemini HTTPS代理连通成功: {https_proxy}")
|
||||
else:
|
||||
log.bind(tag=TAG).warning(
|
||||
f"配置提供的Gemini HTTPS代理不可用: {https_proxy}"
|
||||
)
|
||||
|
||||
# 如果https_proxy不可用,但http_proxy可用且能走通https,则复用http_proxy作为https_proxy
|
||||
if ok_http and not ok_https:
|
||||
if test_proxy(http_proxy, test_https_url):
|
||||
os.environ["HTTPS_PROXY"] = http_proxy
|
||||
ok_https = True
|
||||
log.bind(tag=TAG).info(f"复用HTTP代理作为HTTPS代理: {http_proxy}")
|
||||
|
||||
if not ok_http and not ok_https:
|
||||
log.bind(tag=TAG).error(
|
||||
f"Gemini 代理设置失败: HTTP 和 HTTPS 代理都不可用,请检查配置"
|
||||
)
|
||||
raise RuntimeError("HTTP 和 HTTPS 代理都不可用,请检查配置")
|
||||
|
||||
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
"""初始化Gemini LLM Provider"""
|
||||
self.model_name = config.get("model_name", "gemini-1.5-pro")
|
||||
self.api_key = config.get("api_key")
|
||||
self.http_proxy = config.get("http_proxy")
|
||||
self.https_proxy = config.get("https_proxy")
|
||||
have_key = check_model_key("LLM", self.api_key)
|
||||
def __init__(self, cfg: Dict[str, Any]):
|
||||
self.model_name = cfg.get("model_name", "gemini-2.0-flash")
|
||||
self.api_key = cfg["api_key"]
|
||||
http_proxy = cfg.get("http_proxy")
|
||||
https_proxy = cfg.get("https_proxy")
|
||||
|
||||
if not have_key:
|
||||
return
|
||||
if not check_model_key("LLM", self.api_key):
|
||||
raise ValueError("无效的Gemini API Key,请检查是否配置正确")
|
||||
|
||||
try:
|
||||
# 初始化Gemini客户端
|
||||
# 配置代理(如果提供了代理配置)
|
||||
self.proxies = None
|
||||
if self.http_proxy is not "" or self.https_proxy is not "":
|
||||
if http_proxy or https_proxy:
|
||||
log.bind(tag=TAG).info(
|
||||
f"检测到Gemini代理配置,开始测试代理连通性和设置代理环境..."
|
||||
)
|
||||
setup_proxy_env(http_proxy, https_proxy)
|
||||
log.bind(tag=TAG).info(
|
||||
f"Gemini 代理设置成功 - HTTP: {http_proxy}, HTTPS: {https_proxy}"
|
||||
)
|
||||
genai.configure(api_key=self.api_key)
|
||||
self.model = genai.GenerativeModel(self.model_name)
|
||||
|
||||
self.proxies = {
|
||||
"http": self.http_proxy,
|
||||
"https": self.https_proxy,
|
||||
}
|
||||
logger.bind(tag=TAG).info(f"Gemini set proxys:{self.proxies}")
|
||||
# 使用猴子补丁修改 google-generativeai 库的请求会话
|
||||
self.gen_cfg = GenerationConfig(
|
||||
temperature=0.7,
|
||||
top_p=0.9,
|
||||
top_k=40,
|
||||
max_output_tokens=2048,
|
||||
)
|
||||
|
||||
# 使用 session 对象配置 genai
|
||||
|
||||
genai.configure(api_key=self.api_key)
|
||||
self.model = genai.GenerativeModel(self.model_name)
|
||||
|
||||
# 设置生成参数
|
||||
self.generation_config = {
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.9,
|
||||
"top_k": 40,
|
||||
"max_output_tokens": 2048,
|
||||
}
|
||||
self.chat = None
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Gemini初始化失败: {e}")
|
||||
self.model = None
|
||||
@staticmethod
|
||||
def _build_tools(funcs: List[Dict[str, Any]] | None):
|
||||
if not funcs:
|
||||
return None
|
||||
return [
|
||||
types.Tool(
|
||||
function_declarations=[
|
||||
types.FunctionDeclaration(
|
||||
name=f["function"]["name"],
|
||||
description=f["function"]["description"],
|
||||
parameters=f["function"]["parameters"],
|
||||
)
|
||||
for f in funcs
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
# Gemini文档提到,无需维护session-id,直接用dialogue拼接而成
|
||||
def response(self, session_id, dialogue):
|
||||
"""生成Gemini对话响应"""
|
||||
if not self.model:
|
||||
yield "【Gemini服务未正确初始化】"
|
||||
return
|
||||
|
||||
try:
|
||||
# 处理对话历史
|
||||
chat_history = []
|
||||
for msg in dialogue[:-1]: # 历史对话
|
||||
role = "model" if msg["role"] == "assistant" else "user"
|
||||
content = msg["content"].strip()
|
||||
if content:
|
||||
chat_history.append({"role": role, "parts": [{"text": content}]})
|
||||
|
||||
# 获取当前消息
|
||||
current_msg = dialogue[-1]["content"]
|
||||
|
||||
# 构建请求体
|
||||
request_body = {
|
||||
"contents": chat_history
|
||||
+ [{"role": "user", "parts": [{"text": current_msg}]}],
|
||||
"generationConfig": self.generation_config,
|
||||
}
|
||||
|
||||
# 构建请求URL
|
||||
url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model_name}:generateContent?key={self.api_key}"
|
||||
|
||||
# 构建请求头
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
# 发送POST请求,经测试手动 request 无法使用 stream 模式
|
||||
if self.proxies:
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=headers,
|
||||
json=request_body,
|
||||
stream=False,
|
||||
proxies=self.proxies,
|
||||
)
|
||||
try:
|
||||
data = response.json() # 直接解析JSON
|
||||
if "candidates" in data and data["candidates"]:
|
||||
yield data["candidates"][0]["content"]["parts"][0]["text"]
|
||||
else:
|
||||
yield "未找到候选回复。"
|
||||
except json.JSONDecodeError as e:
|
||||
yield f"JSON解码错误:{e}"
|
||||
except Exception as e:
|
||||
yield f"发生错误:{e}"
|
||||
else:
|
||||
logger.bind(tag=TAG).info(f"Gemini stream mode ")
|
||||
chat = self.model.start_chat(history=chat_history)
|
||||
|
||||
# 发送消息并获取流式响应
|
||||
response = chat.send_message(
|
||||
current_msg, stream=True, generation_config=self.generation_config
|
||||
)
|
||||
# 处理流式响应
|
||||
for chunk in response:
|
||||
if hasattr(chunk, "text") and chunk.text:
|
||||
yield chunk.text
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.bind(tag=TAG).error(f"Gemini响应生成错误: {error_msg}")
|
||||
|
||||
# 针对不同错误返回友好提示
|
||||
if "Rate limit" in error_msg:
|
||||
yield "【Gemini服务请求太频繁,请稍后再试】"
|
||||
elif "Invalid API key" in error_msg:
|
||||
yield "【Gemini API key无效】"
|
||||
else:
|
||||
yield f"【Gemini服务响应异常: {error_msg}】"
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
yield f"请求失败:{e}"
|
||||
except json.JSONDecodeError as e:
|
||||
yield f"JSON解码错误:{e}"
|
||||
except Exception as e:
|
||||
yield f"发生错误:{e}"
|
||||
yield from self._generate(dialogue, None)
|
||||
|
||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||
logger.bind(tag=TAG).info(f"gemini暂未实现完整的工具调用(function call)")
|
||||
return self.response(session_id, dialogue)
|
||||
yield from self._generate(dialogue, self._build_tools(functions))
|
||||
|
||||
def _generate(self, dialogue, tools):
|
||||
role_map = {"assistant": "model", "user": "user"}
|
||||
contents: list = []
|
||||
# 拼接对话
|
||||
for m in dialogue:
|
||||
r = m["role"]
|
||||
|
||||
if r == "assistant" and "tool_calls" in m:
|
||||
tc = m["tool_calls"][0]
|
||||
contents.append(
|
||||
{
|
||||
"role": "model",
|
||||
"parts": [
|
||||
{
|
||||
"function_call": {
|
||||
"name": tc["function"]["name"],
|
||||
"args": json.loads(tc["function"]["arguments"]),
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
if r == "tool":
|
||||
contents.append(
|
||||
{
|
||||
"role": "model",
|
||||
"parts": [{"text": str(m.get("content", ""))}],
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
contents.append(
|
||||
{
|
||||
"role": role_map.get(r, "user"),
|
||||
"parts": [{"text": str(m.get("content", ""))}],
|
||||
}
|
||||
)
|
||||
|
||||
stream: GenerateContentResponse = self.model.generate_content(
|
||||
contents=contents,
|
||||
generation_config=self.gen_cfg,
|
||||
tools=tools,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
try:
|
||||
for chunk in stream:
|
||||
cand = chunk.candidates[0]
|
||||
for part in cand.content.parts:
|
||||
# a) 函数调用-通常是最后一段话才是函数调用
|
||||
if getattr(part, "function_call", None):
|
||||
fc = part.function_call
|
||||
yield None, [
|
||||
SimpleNamespace(
|
||||
id=uuid.uuid4().hex,
|
||||
type="function",
|
||||
function=SimpleNamespace(
|
||||
name=fc.name,
|
||||
arguments=json.dumps(
|
||||
dict(fc.args), ensure_ascii=False
|
||||
),
|
||||
),
|
||||
)
|
||||
]
|
||||
return
|
||||
# b) 普通文本
|
||||
if getattr(part, "text", None):
|
||||
yield part.text if tools is None else (part.text, None)
|
||||
|
||||
finally:
|
||||
if tools is not None:
|
||||
yield None, None # function‑mode 结束,返回哑包
|
||||
|
||||
# 关闭stream,预留后续打断对话功能的功能方法,官方文档推荐打断对话要关闭上一个流,可以有效减少配额计费和资源占用
|
||||
@staticmethod
|
||||
def _safe_finish_stream(stream: GenerateContentResponse):
|
||||
if hasattr(stream, "resolve"):
|
||||
stream.resolve() # Gemini SDK version ≥ 0.5.0
|
||||
elif hasattr(stream, "close"):
|
||||
stream.close() # Gemini SDK version < 0.5.0
|
||||
else:
|
||||
for _ in stream: # 兜底耗尽
|
||||
pass
|
||||
|
||||
@@ -66,7 +66,6 @@ class LLMProvider(LLMProviderBase):
|
||||
logger.bind(tag=TAG).error(f"生成响应时出错: {e}")
|
||||
|
||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||
logger.bind(tag=TAG).info(
|
||||
f"homeassistant不支持(function call),建议使用意图识别使用:nointent"
|
||||
logger.bind(tag=TAG).error(
|
||||
f"homeassistant不支持(function call),建议使用其他意图识别"
|
||||
)
|
||||
return self.response(session_id, dialogue)
|
||||
|
||||
@@ -21,27 +21,67 @@ class LLMProvider(LLMProviderBase):
|
||||
api_key="ollama" # Ollama doesn't need an API key but OpenAI client requires one
|
||||
)
|
||||
|
||||
# 检查是否是qwen3模型
|
||||
self.is_qwen3 = self.model_name and self.model_name.lower().startswith("qwen3")
|
||||
|
||||
def response(self, session_id, dialogue):
|
||||
try:
|
||||
# 如果是qwen3模型,在用户最后一条消息中添加/no_think指令
|
||||
if self.is_qwen3:
|
||||
# 复制对话列表,避免修改原始对话
|
||||
dialogue_copy = dialogue.copy()
|
||||
|
||||
# 找到最后一条用户消息
|
||||
for i in range(len(dialogue_copy) - 1, -1, -1):
|
||||
if dialogue_copy[i]["role"] == "user":
|
||||
# 在用户消息前添加/no_think指令
|
||||
dialogue_copy[i]["content"] = "/no_think " + dialogue_copy[i]["content"]
|
||||
logger.bind(tag=TAG).debug(f"为qwen3模型添加/no_think指令")
|
||||
break
|
||||
|
||||
# 使用修改后的对话
|
||||
dialogue = dialogue_copy
|
||||
|
||||
responses = self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=dialogue,
|
||||
stream=True
|
||||
)
|
||||
is_active=True
|
||||
is_active = True
|
||||
# 用于处理跨chunk的标签
|
||||
buffer = ""
|
||||
|
||||
for chunk in responses:
|
||||
try:
|
||||
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
||||
content = delta.content if hasattr(delta, 'content') else ''
|
||||
|
||||
if content:
|
||||
if '<think>' in content:
|
||||
# 将内容添加到缓冲区
|
||||
buffer += content
|
||||
|
||||
# 处理缓冲区中的标签
|
||||
while '<think>' in buffer and '</think>' in buffer:
|
||||
# 找到完整的<think></think>标签并移除
|
||||
pre = buffer.split('<think>', 1)[0]
|
||||
post = buffer.split('</think>', 1)[1]
|
||||
buffer = pre + post
|
||||
|
||||
# 处理只有开始标签的情况
|
||||
if '<think>' in buffer:
|
||||
is_active = False
|
||||
content = content.split('<think>')[0]
|
||||
if '</think>' in content:
|
||||
buffer = buffer.split('<think>', 1)[0]
|
||||
|
||||
# 处理只有结束标签的情况
|
||||
if '</think>' in buffer:
|
||||
is_active = True
|
||||
content = content.split('</think>')[-1]
|
||||
if is_active:
|
||||
yield content
|
||||
buffer = buffer.split('</think>', 1)[1]
|
||||
|
||||
# 如果当前处于活动状态且缓冲区有内容,则输出
|
||||
if is_active and buffer:
|
||||
yield buffer
|
||||
buffer = "" # 清空缓冲区
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error processing chunk: {e}")
|
||||
|
||||
@@ -51,6 +91,22 @@ class LLMProvider(LLMProviderBase):
|
||||
|
||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||
try:
|
||||
# 如果是qwen3模型,在用户最后一条消息中添加/no_think指令
|
||||
if self.is_qwen3:
|
||||
# 复制对话列表,避免修改原始对话
|
||||
dialogue_copy = dialogue.copy()
|
||||
|
||||
# 找到最后一条用户消息
|
||||
for i in range(len(dialogue_copy) - 1, -1, -1):
|
||||
if dialogue_copy[i]["role"] == "user":
|
||||
# 在用户消息前添加/no_think指令
|
||||
dialogue_copy[i]["content"] = "/no_think " + dialogue_copy[i]["content"]
|
||||
logger.bind(tag=TAG).debug(f"为qwen3模型添加/no_think指令")
|
||||
break
|
||||
|
||||
# 使用修改后的对话
|
||||
dialogue = dialogue_copy
|
||||
|
||||
stream = self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=dialogue,
|
||||
@@ -58,8 +114,49 @@ class LLMProvider(LLMProviderBase):
|
||||
tools=functions,
|
||||
)
|
||||
|
||||
is_active = True
|
||||
buffer = ""
|
||||
|
||||
for chunk in stream:
|
||||
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
||||
try:
|
||||
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
||||
content = delta.content if hasattr(delta, 'content') else None
|
||||
tool_calls = delta.tool_calls if hasattr(delta, 'tool_calls') else None
|
||||
|
||||
# 如果是工具调用,直接传递
|
||||
if tool_calls:
|
||||
yield None, tool_calls
|
||||
continue
|
||||
|
||||
# 处理文本内容
|
||||
if content:
|
||||
# 将内容添加到缓冲区
|
||||
buffer += content
|
||||
|
||||
# 处理缓冲区中的标签
|
||||
while '<think>' in buffer and '</think>' in buffer:
|
||||
# 找到完整的<think></think>标签并移除
|
||||
pre = buffer.split('<think>', 1)[0]
|
||||
post = buffer.split('</think>', 1)[1]
|
||||
buffer = pre + post
|
||||
|
||||
# 处理只有开始标签的情况
|
||||
if '<think>' in buffer:
|
||||
is_active = False
|
||||
buffer = buffer.split('<think>', 1)[0]
|
||||
|
||||
# 处理只有结束标签的情况
|
||||
if '</think>' in buffer:
|
||||
is_active = True
|
||||
buffer = buffer.split('</think>', 1)[1]
|
||||
|
||||
# 如果当前处于活动状态且缓冲区有内容,则输出
|
||||
if is_active and buffer:
|
||||
yield buffer, None
|
||||
buffer = "" # 清空缓冲区
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error processing function chunk: {e}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
|
||||
|
||||
@@ -4,6 +4,7 @@ from config.logger import setup_logging
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class MemoryProviderBase(ABC):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
@@ -20,6 +21,6 @@ class MemoryProviderBase(ABC):
|
||||
"""Query memories for specific role based on similarity"""
|
||||
return "please implement query method"
|
||||
|
||||
def init_memory(self, role_id, llm):
|
||||
self.role_id = role_id
|
||||
def init_memory(self, role_id, llm, **kwargs):
|
||||
self.role_id = role_id
|
||||
self.llm = llm
|
||||
|
||||
@@ -8,7 +8,7 @@ TAG = __name__
|
||||
|
||||
|
||||
class MemoryProvider(MemoryProviderBase):
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, summary_memory=None):
|
||||
super().__init__(config)
|
||||
self.api_key = config.get("api_key", "")
|
||||
self.api_version = config.get("api_version", "v1.1")
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import os
|
||||
import yaml
|
||||
from config.config_loader import get_project_dir
|
||||
from config.manage_api_client import save_mem_local_short
|
||||
|
||||
|
||||
short_term_memory_prompt = """
|
||||
@@ -72,6 +73,17 @@ short_term_memory_prompt = """
|
||||
```
|
||||
"""
|
||||
|
||||
short_term_memory_prompt_only_content = """
|
||||
你是一个经验丰富的记忆总结者,擅长将对话内容进行总结摘要,遵循以下规则:
|
||||
1、总结user的重要信息,以便在未来的对话中提供更个性化的服务
|
||||
2、不要重复总结,不要遗忘之前记忆,除非原来的记忆超过了1800字内,否则不要遗忘、不要压缩用户的历史记忆
|
||||
3、用户操控的设备音量、播放音乐、天气、退出、不想对话等和用户本身无关的内容,这些信息不需要加入到总结中
|
||||
4、不要把设备操控的成果结果和失败结果加入到总结中,也不要把用户的一些废话加入到总结中
|
||||
5、不要为了总结而总结,如果用户的聊天没有意义,请返回原来的历史记录也是可以的
|
||||
6、只需要返回总结摘要,严格控制在1800字内
|
||||
7、不要包含代码、xml,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容
|
||||
"""
|
||||
|
||||
|
||||
def extract_json_data(json_code):
|
||||
start = json_code.find("```json")
|
||||
@@ -93,17 +105,26 @@ TAG = __name__
|
||||
|
||||
|
||||
class MemoryProvider(MemoryProviderBase):
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, summary_memory):
|
||||
super().__init__(config)
|
||||
self.short_momery = ""
|
||||
self.save_to_file = True
|
||||
self.memory_path = get_project_dir() + "data/.memory.yaml"
|
||||
self.load_memory()
|
||||
self.load_memory(summary_memory)
|
||||
|
||||
def init_memory(self, role_id, llm):
|
||||
super().init_memory(role_id, llm)
|
||||
self.load_memory()
|
||||
def init_memory(
|
||||
self, role_id, llm, summary_memory=None, save_to_file=True, **kwargs
|
||||
):
|
||||
super().init_memory(role_id, llm, **kwargs)
|
||||
self.save_to_file = save_to_file
|
||||
self.load_memory(summary_memory)
|
||||
|
||||
def load_memory(self, summary_memory):
|
||||
# api获取到总结记忆后直接返回
|
||||
if summary_memory or not self.save_to_file:
|
||||
self.short_momery = summary_memory
|
||||
return
|
||||
|
||||
def load_memory(self):
|
||||
all_memory = {}
|
||||
if os.path.exists(self.memory_path):
|
||||
with open(self.memory_path, "r", encoding="utf-8") as f:
|
||||
@@ -134,7 +155,7 @@ class MemoryProvider(MemoryProviderBase):
|
||||
msgStr += f"User: {msg.content}\n"
|
||||
elif msg.role == "assistant":
|
||||
msgStr += f"Assistant: {msg.content}\n"
|
||||
if len(self.short_momery) > 0:
|
||||
if self.short_momery and len(self.short_momery) > 0:
|
||||
msgStr += "历史记忆:\n"
|
||||
msgStr += self.short_momery
|
||||
|
||||
@@ -142,16 +163,20 @@ class MemoryProvider(MemoryProviderBase):
|
||||
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
msgStr += f"当前时间:{time_str}"
|
||||
|
||||
result = self.llm.response_no_stream(short_term_memory_prompt, msgStr)
|
||||
|
||||
json_str = extract_json_data(result)
|
||||
try:
|
||||
json_data = json.loads(json_str) # 检查json格式是否正确
|
||||
self.short_momery = json_str
|
||||
except Exception as e:
|
||||
print("Error:", e)
|
||||
|
||||
self.save_memory_to_file()
|
||||
if self.save_to_file:
|
||||
result = self.llm.response_no_stream(short_term_memory_prompt, msgStr)
|
||||
json_str = extract_json_data(result)
|
||||
try:
|
||||
json.loads(json_str) # 检查json格式是否正确
|
||||
self.short_momery = json_str
|
||||
self.save_memory_to_file()
|
||||
except Exception as e:
|
||||
print("Error:", e)
|
||||
else:
|
||||
result = self.llm.response_no_stream(
|
||||
short_term_memory_prompt_only_content, msgStr
|
||||
)
|
||||
save_mem_local_short(self.role_id, result)
|
||||
logger.bind(tag=TAG).info(f"Save memory successful - Role: {self.role_id}")
|
||||
|
||||
return self.short_momery
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
'''
|
||||
"""
|
||||
不使用记忆,可以选择此模块
|
||||
'''
|
||||
"""
|
||||
|
||||
from ..base import MemoryProviderBase, logger
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class MemoryProvider(MemoryProviderBase):
|
||||
def __init__(self, config):
|
||||
def __init__(self, config, summary_memory=None):
|
||||
super().__init__(config)
|
||||
|
||||
|
||||
async def save_memory(self, msgs):
|
||||
logger.bind(tag=TAG).debug("nomem mode: No memory saving is performed.")
|
||||
return None
|
||||
|
||||
async def query_memory(self, query: str)-> str:
|
||||
async def query_memory(self, query: str) -> str:
|
||||
logger.bind(tag=TAG).debug("nomem mode: No memory query is performed.")
|
||||
return ""
|
||||
return ""
|
||||
|
||||
@@ -85,16 +85,22 @@ class TTSProvider(TTSProviderBase):
|
||||
def __init__(self, config, delete_audio_file):
|
||||
super().__init__(config, delete_audio_file)
|
||||
|
||||
self.reference_id = config.get("reference_id")
|
||||
self.reference_id = (
|
||||
None if not config.get("reference_id") else config.get("reference_id")
|
||||
)
|
||||
self.reference_audio = parse_string_to_list(config.get("reference_audio"))
|
||||
self.reference_text = parse_string_to_list(config.get("reference_text"))
|
||||
self.format = config.get("format", "wav")
|
||||
self.format = config.get("response_format", "wav")
|
||||
|
||||
self.api_key = config.get("api_key", "YOUR_API_KEY")
|
||||
have_key = check_model_key("FishSpeech TTS", self.api_key)
|
||||
if not have_key:
|
||||
return
|
||||
self.normalize = config.get("normalize", True)
|
||||
self.normalize = str(config.get("normalize", True)).lower() in (
|
||||
"true",
|
||||
"1",
|
||||
"yes",
|
||||
)
|
||||
|
||||
# 处理空字符串的情况
|
||||
channels = config.get("channels", "1")
|
||||
@@ -124,7 +130,7 @@ class TTSProvider(TTSProviderBase):
|
||||
"yes",
|
||||
)
|
||||
self.use_memory_cache = config.get("use_memory_cache", "on")
|
||||
self.seed = config.get("seed") or None
|
||||
self.seed = int(config.get("seed")) if config.get("seed") else None
|
||||
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
|
||||
|
||||
def generate_filename(self, extension=".wav"):
|
||||
|
||||
@@ -75,7 +75,8 @@ class Dialogue:
|
||||
|
||||
if system_message:
|
||||
enhanced_system_prompt = (
|
||||
f"{system_message.content}\n\n" f"相关记忆:\n{memory_str}"
|
||||
f"{system_message.content}\n\n"
|
||||
f"以下是用户的历史记忆:\n```\n{memory_str}\n```"
|
||||
)
|
||||
dialogue.append({"role": "system", "content": enhanced_system_prompt})
|
||||
|
||||
|
||||
@@ -319,6 +319,7 @@ def initialize_modules(
|
||||
modules["memory"] = memory.create_instance(
|
||||
memory_type,
|
||||
config["Memory"][select_memory_module],
|
||||
config.get('summaryMemory', None),
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"初始化组件: memory成功 {select_memory_module}")
|
||||
|
||||
@@ -930,7 +931,6 @@ def check_vad_update(before_config, new_config):
|
||||
if "type" not in new_config["VAD"][new_vad_module]
|
||||
else new_config["VAD"][new_vad_module]["type"]
|
||||
)
|
||||
print(f"前vad:{current_vad_type},后vad:{new_vad_type}")
|
||||
update_vad = current_vad_type != new_vad_type
|
||||
return update_vad
|
||||
|
||||
@@ -954,6 +954,5 @@ def check_asr_update(before_config, new_config):
|
||||
if "type" not in new_config["ASR"][new_asr_module]
|
||||
else new_config["ASR"][new_asr_module]["type"]
|
||||
)
|
||||
print(f"前asr:{current_asr_type},后asr:{new_asr_type}")
|
||||
update_asr = current_asr_type != new_asr_type
|
||||
return update_asr
|
||||
|
||||
@@ -76,6 +76,7 @@ services:
|
||||
expose:
|
||||
- 6379
|
||||
container_name: xiaozhi-esp32-server-redis
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
|
||||
@@ -107,8 +107,8 @@ WEATHER_CODE_MAP = {
|
||||
}
|
||||
|
||||
|
||||
def fetch_city_info(location, api_key):
|
||||
url = f"https://geoapi.qweather.com/v2/city/lookup?key={api_key}&location={location}&lang=zh"
|
||||
def fetch_city_info(location, api_key, api_host):
|
||||
url = f"https://{api_host}/geo/v2/city/lookup?key={api_key}&location={location}&lang=zh"
|
||||
response = requests.get(url, headers=HEADERS).json()
|
||||
return response.get("location", [])[0] if response.get("location") else None
|
||||
|
||||
@@ -151,7 +151,8 @@ def parse_weather_info(soup):
|
||||
|
||||
@register_function("get_weather", GET_WEATHER_FUNCTION_DESC, ToolType.SYSTEM_CTL)
|
||||
def get_weather(conn, location: str = None, lang: str = "zh_CN"):
|
||||
api_key = conn.config["plugins"]["get_weather"]["api_key"]
|
||||
api_host = conn.config["plugins"]["get_weather"].get("api_host", "mj7p3y7naa.re.qweatherapi.com")
|
||||
api_key = conn.config["plugins"]["get_weather"].get("api_key", "a861d0d5e7bf4ee1a83d9a9e4f96d4da")
|
||||
default_location = conn.config["plugins"]["get_weather"]["default_location"]
|
||||
client_ip = conn.client_ip
|
||||
# 优先使用用户提供的location参数
|
||||
@@ -164,8 +165,7 @@ def get_weather(conn, location: str = None, lang: str = "zh_CN"):
|
||||
else:
|
||||
# 若IP解析失败或无IP,使用默认位置
|
||||
location = default_location
|
||||
|
||||
city_info = fetch_city_info(location, api_key)
|
||||
city_info = fetch_city_info(location, api_key, api_host)
|
||||
if not city_info:
|
||||
return ActionResponse(
|
||||
Action.REQLLM, f"未找到相关的城市: {location},请确认地点是否正确", None
|
||||
|
||||
@@ -22,11 +22,12 @@ mem0ai==0.1.62
|
||||
bs4==0.0.2
|
||||
modelscope==1.23.2
|
||||
sherpa_onnx==1.11.0
|
||||
mcp==1.7.1
|
||||
mcp==1.8.1
|
||||
cnlunar==0.2.0
|
||||
PySocks==1.7.1
|
||||
dashscope==1.23.1
|
||||
baidu-aip==4.16.13
|
||||
chardet==5.2.0
|
||||
aioconsole==0.8.1
|
||||
markitdown==0.1.1
|
||||
markitdown==0.1.1
|
||||
mcp-proxy==0.6.0
|
||||
|
||||