mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-26 09:03:54 +08:00
resolve merge conflict
This commit is contained in:
@@ -199,6 +199,12 @@
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>${knife4j.version}</version>
|
||||
</dependency>
|
||||
<!-- BouncyCastle SM2加密 -->
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>1.78</version>
|
||||
</dependency>
|
||||
<!-- springdoc -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
|
||||
@@ -86,11 +86,26 @@ public interface Constant {
|
||||
*/
|
||||
String SERVER_SECRET = "server.secret";
|
||||
|
||||
/**
|
||||
* SM2公钥
|
||||
*/
|
||||
String SM2_PUBLIC_KEY = "server.public_key";
|
||||
|
||||
/**
|
||||
* SM2私钥
|
||||
*/
|
||||
String SM2_PRIVATE_KEY = "server.private_key";
|
||||
|
||||
/**
|
||||
* websocket地址
|
||||
*/
|
||||
String SERVER_WEBSOCKET = "server.websocket";
|
||||
|
||||
/**
|
||||
* mqtt gateway 配置
|
||||
*/
|
||||
String SERVER_MQTT_GATEWAY = "server.mqtt_gateway";
|
||||
|
||||
/**
|
||||
* ota地址
|
||||
*/
|
||||
@@ -121,11 +136,31 @@ public interface Constant {
|
||||
*/
|
||||
String SERVER_VOICE_PRINT = "server.voice_print";
|
||||
|
||||
/**
|
||||
* mqtt密钥
|
||||
*/
|
||||
String SERVER_MQTT_SECRET = "server.mqtt_signature_key";
|
||||
|
||||
/**
|
||||
* WebSocket认证开关
|
||||
*/
|
||||
String SERVER_AUTH_ENABLED = "server.auth.enabled";
|
||||
|
||||
/**
|
||||
* 无记忆
|
||||
*/
|
||||
String MEMORY_NO_MEM = "Memory_nomem";
|
||||
|
||||
/**
|
||||
* 火山引擎双声道语音克隆
|
||||
*/
|
||||
String VOICE_CLONE_HUOSHAN_DOUBLE_STREAM = "huoshan_double_stream";
|
||||
|
||||
/**
|
||||
* RAG配置类型
|
||||
*/
|
||||
String RAG_CONFIG_TYPE = "RAG";
|
||||
|
||||
enum SysBaseParam {
|
||||
/**
|
||||
* ICP备案号
|
||||
@@ -151,6 +186,38 @@ public interface Constant {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 训练状态
|
||||
*/
|
||||
enum TrainStatus {
|
||||
/**
|
||||
* 未训练
|
||||
*/
|
||||
NOT_TRAINED(0),
|
||||
/**
|
||||
* 训练中
|
||||
*/
|
||||
TRAINING(1),
|
||||
/**
|
||||
* 已训练
|
||||
*/
|
||||
TRAINED(2),
|
||||
/**
|
||||
* 训练失败
|
||||
*/
|
||||
TRAIN_FAILED(3);
|
||||
|
||||
private final int code;
|
||||
|
||||
TrainStatus(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统短信
|
||||
*/
|
||||
@@ -237,7 +304,7 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.7.7";
|
||||
public static final String VERSION = "0.8.10";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
|
||||
@@ -55,4 +55,189 @@ public interface ErrorCode {
|
||||
|
||||
int OTA_DEVICE_NOT_FOUND = 10041;
|
||||
int OTA_DEVICE_NEED_BIND = 10042;
|
||||
|
||||
// 新增错误编码
|
||||
int DELETE_DATA_FAILED = 10043;
|
||||
int USER_NOT_LOGIN = 10044;
|
||||
int WEB_SOCKET_CONNECT_FAILED = 10045;
|
||||
int VOICE_PRINT_SAVE_ERROR = 10046;
|
||||
int TODAY_SMS_LIMIT_REACHED = 10047;
|
||||
int OLD_PASSWORD_ERROR = 10048;
|
||||
int INVALID_LLM_TYPE = 10049;
|
||||
int TOKEN_GENERATE_ERROR = 10050;
|
||||
int RESOURCE_NOT_FOUND = 10051;
|
||||
|
||||
// 新增错误编码
|
||||
int DEFAULT_AGENT_NOT_FOUND = 10052;
|
||||
int AGENT_NOT_FOUND = 10053;
|
||||
int VOICEPRINT_API_NOT_CONFIGURED = 10054;
|
||||
int SMS_SEND_FAILED = 10055;
|
||||
int SMS_CONNECTION_FAILED = 10056;
|
||||
int AGENT_VOICEPRINT_CREATE_FAILED = 10057;
|
||||
int AGENT_VOICEPRINT_UPDATE_FAILED = 10058;
|
||||
int AGENT_VOICEPRINT_DELETE_FAILED = 10059;
|
||||
int SMS_SEND_TOO_FREQUENTLY = 10060;
|
||||
int ACTIVATION_CODE_EMPTY = 10061;
|
||||
int ACTIVATION_CODE_ERROR = 10062;
|
||||
int DEVICE_ALREADY_ACTIVATED = 10063;
|
||||
// 默认模型删除错误
|
||||
int DEFAULT_MODEL_DELETE_ERROR = 10064;
|
||||
// 登录相关错误码
|
||||
int ADD_DATA_FAILED = 10065; // 新增数据失败
|
||||
int UPDATE_DATA_FAILED = 10066; // 修改数据失败
|
||||
int SMS_CAPTCHA_ERROR = 10067; // 短信验证码错误
|
||||
int MOBILE_REGISTER_DISABLED = 10068; // 未开启手机注册
|
||||
int USERNAME_NOT_PHONE = 10069; // 用户名不是手机号码
|
||||
int PHONE_ALREADY_REGISTERED = 10070; // 手机号码已注册
|
||||
int PHONE_NOT_REGISTERED = 10071; // 手机号码未注册
|
||||
int USER_REGISTER_DISABLED = 10072; // 不允许用户注册
|
||||
int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能
|
||||
int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确
|
||||
int SMS_CODE_ERROR = 10075; // 手机验证码错误
|
||||
|
||||
// 字典类型相关错误码
|
||||
int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在
|
||||
int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复
|
||||
|
||||
// 资源处理相关错误码
|
||||
int RESOURCE_READ_ERROR = 10078; // 读取资源失败
|
||||
|
||||
// 智能体相关错误码
|
||||
int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配
|
||||
|
||||
// 声纹相关错误码
|
||||
int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册
|
||||
int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误
|
||||
int VOICEPRINT_UPDATE_NOT_ALLOWED = 10082; // 声纹修改不允许,声音已注册
|
||||
int VOICEPRINT_UPDATE_ADMIN_ERROR = 10083; // 修改声纹错误,请联系管理员
|
||||
int VOICEPRINT_API_URI_ERROR = 10084; // 声纹接口地址错误
|
||||
int VOICEPRINT_AUDIO_NOT_BELONG_AGENT = 10085; // 音频数据不属于智能体
|
||||
int VOICEPRINT_AUDIO_EMPTY = 10086; // 音频数据为空
|
||||
int VOICEPRINT_REGISTER_REQUEST_ERROR = 10087; // 声纹保存请求失败
|
||||
int VOICEPRINT_REGISTER_PROCESS_ERROR = 10088; // 声纹保存处理失败
|
||||
int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败
|
||||
int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败
|
||||
int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败
|
||||
|
||||
int LLM_NOT_EXIST = 10092; // 设置的LLM不存在
|
||||
int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除
|
||||
int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除
|
||||
|
||||
// 服务端管理相关错误码
|
||||
int INVALID_SERVER_ACTION = 10095; // 无效服务端操作
|
||||
int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址
|
||||
int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在
|
||||
|
||||
// 参数验证相关错误码
|
||||
int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空
|
||||
int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1
|
||||
int WEBSOCKET_URL_FORMAT_ERROR = 10100; // WebSocket地址格式不正确
|
||||
int WEBSOCKET_CONNECTION_FAILED = 10101; // WebSocket连接测试失败
|
||||
int OTA_URL_EMPTY = 10102; // OTA地址不能为空
|
||||
int OTA_URL_LOCALHOST = 10103; // OTA地址不能使用localhost或127.0.0.1
|
||||
int OTA_URL_PROTOCOL_ERROR = 10104; // OTA地址必须以http或https开头
|
||||
int OTA_URL_FORMAT_ERROR = 10105; // OTA地址必须以/ota/结尾
|
||||
int OTA_INTERFACE_ACCESS_FAILED = 10106; // OTA接口访问失败
|
||||
int OTA_INTERFACE_FORMAT_ERROR = 10107; // OTA接口返回内容格式不正确
|
||||
int OTA_INTERFACE_VALIDATION_FAILED = 10108; // OTA接口验证失败
|
||||
int MCP_URL_EMPTY = 10109; // MCP地址不能为空
|
||||
int MCP_URL_LOCALHOST = 10110; // MCP地址不能使用localhost或127.0.0.1
|
||||
int MCP_URL_INVALID = 10111; // 不是正确的MCP地址
|
||||
int MCP_INTERFACE_ACCESS_FAILED = 10112; // MCP接口访问失败
|
||||
int MCP_INTERFACE_FORMAT_ERROR = 10113; // MCP接口返回内容格式不正确
|
||||
int MCP_INTERFACE_VALIDATION_FAILED = 10114; // MCP接口验证失败
|
||||
int VOICEPRINT_URL_EMPTY = 10115; // 声纹接口地址不能为空
|
||||
int VOICEPRINT_URL_LOCALHOST = 10116; // 声纹接口地址不能使用localhost或127.0.0.1
|
||||
int VOICEPRINT_URL_INVALID = 10117; // 不是正确的声纹接口地址
|
||||
int VOICEPRINT_URL_PROTOCOL_ERROR = 10118; // 声纹接口地址必须以http或https开头
|
||||
int VOICEPRINT_INTERFACE_ACCESS_FAILED = 10119; // 声纹接口访问失败
|
||||
int VOICEPRINT_INTERFACE_FORMAT_ERROR = 10120; // 声纹接口返回内容格式不正确
|
||||
int VOICEPRINT_INTERFACE_VALIDATION_FAILED = 10121; // 声纹接口验证失败
|
||||
int MQTT_SECRET_EMPTY = 10122; // mqtt密钥不能为空
|
||||
int MQTT_SECRET_LENGTH_INSECURE = 10123; // mqtt密钥长度不安全
|
||||
int MQTT_SECRET_CHARACTER_INSECURE = 10124; // mqtt密钥必须同时包含大小写字母
|
||||
int MQTT_SECRET_WEAK_PASSWORD = 10125; // mqtt密钥包含弱密码
|
||||
int DICT_LABEL_DUPLICATE = 10128; // 字典标签重复
|
||||
int SM2_KEY_NOT_CONFIGURED = 10129; // SM2密钥未配置
|
||||
int SM2_DECRYPT_ERROR = 10130; // SM2解密失败
|
||||
int MODEL_TYPE_PROVIDE_CODE_NOT_NULL = 10131; // modelType和provideCode不能为空
|
||||
|
||||
// 聊天记录相关错误码
|
||||
int CHAT_HISTORY_NO_PERMISSION = 10132; // 没有权限查看该智能体的聊天记录
|
||||
int CHAT_HISTORY_SESSION_ID_NOT_NULL = 10133; // 会话ID不能为空
|
||||
int CHAT_HISTORY_AGENT_ID_NOT_NULL = 10134; // 智能体ID不能为空
|
||||
int CHAT_HISTORY_DOWNLOAD_FAILED = 10135; // 聊天记录下载失败
|
||||
int DOWNLOAD_LINK_EXPIRED = 10136; // 下载链接已过期或无效
|
||||
int DOWNLOAD_LINK_INVALID = 10137; // 下载链接无效
|
||||
int CHAT_ROLE_USER = 10138; // 用户角色
|
||||
int CHAT_ROLE_AGENT = 10139; // 智能体角色
|
||||
|
||||
// 声音克隆相关错误码
|
||||
int VOICE_CLONE_AUDIO_EMPTY = 10140; // 音频文件不能为空
|
||||
int VOICE_CLONE_NOT_AUDIO_FILE = 10141; // 只支持音频文件
|
||||
int VOICE_CLONE_AUDIO_TOO_LARGE = 10142; // 音频文件大小不能超过10MB
|
||||
int VOICE_CLONE_UPLOAD_FAILED = 10143; // 上传失败
|
||||
int VOICE_CLONE_RECORD_NOT_EXIST = 10144; // 声音克隆记录不存在
|
||||
int VOICE_RESOURCE_INFO_EMPTY = 10145; // 音色资源信息不能为空
|
||||
int VOICE_RESOURCE_PLATFORM_NAME_EMPTY = 10146; // 平台名称不能为空
|
||||
int VOICE_RESOURCE_ID_EMPTY = 10147; // 音色ID不能为空
|
||||
int VOICE_RESOURCE_ACCOUNT_EMPTY = 10148; // 归属账号不能为空
|
||||
int VOICE_RESOURCE_DELETE_ID_EMPTY = 10149; // 删除的音色资源ID不能为空
|
||||
int VOICE_RESOURCE_NO_PERMISSION = 10150; // 您没有权限操作该记录
|
||||
int VOICE_CLONE_AUDIO_NOT_UPLOADED = 10151; // 请先上传音频文件
|
||||
int VOICE_CLONE_MODEL_CONFIG_NOT_FOUND = 10152; // 模型配置未找到
|
||||
int VOICE_CLONE_MODEL_TYPE_NOT_FOUND = 10153; // 模型类型未找到
|
||||
int VOICE_CLONE_TRAINING_FAILED = 10154; // 训练失败
|
||||
int VOICE_CLONE_HUOSHAN_CONFIG_MISSING = 10155; // 火山引擎缺少配置
|
||||
int VOICE_CLONE_RESPONSE_FORMAT_ERROR = 10156; // 响应格式错误
|
||||
int VOICE_CLONE_REQUEST_FAILED = 10157; // 请求失败
|
||||
int VOICE_CLONE_PREFIX = 10158; // 复刻音色前缀
|
||||
int VOICE_ID_ALREADY_EXISTS = 10159; // 音色ID已存在
|
||||
int VOICE_CLONE_HUOSHAN_VOICE_ID_ERROR = 10160; // 火山引擎音色ID格式错误
|
||||
|
||||
// 设备相关错误码
|
||||
int MAC_ADDRESS_ALREADY_EXISTS = 10161; // Mac地址已存在
|
||||
// 模型相关错误码
|
||||
int MODEL_PROVIDER_NOT_EXIST = 10162; // 供应器不存在
|
||||
|
||||
// 知识库相关错误码
|
||||
int Knowledge_Base_RECORD_NOT_EXISTS = 10163; // 知识库记录不存在
|
||||
int RAG_CONFIG_NOT_FOUND = 10164; // RAG配置未找到
|
||||
int RAG_CONFIG_TYPE_ERROR = 10165; // RAG配置类型错误
|
||||
int RAG_DEFAULT_CONFIG_NOT_FOUND = 10166; // 默认RAG配置未找到
|
||||
int RAG_API_ERROR = 10167; // RAG调用失败
|
||||
int UPLOAD_FILE_ERROR = 10168; // 上传文件失败
|
||||
int NO_PERMISSION = 10169; // 没有权限
|
||||
int KNOWLEDGE_BASE_NAME_EXISTS = 10170; // 同名知识库已存在
|
||||
int RAG_API_ERROR_URL_NULL = 10171; // RAG配置中base_url为空,请完善配置
|
||||
int RAG_API_ERROR_API_KEY_NULL = 10172; // RAG配置中api_key为空,请完善配置
|
||||
int RAG_API_ERROR_API_KEY_INVALID = 10173; // RAG配置中api_key包含占位符,请替换为实际的API密钥
|
||||
int RAG_API_ERROR_URL_INVALID = 10174; // RAG配置中base_url格式不正确,请检查协议是否正确
|
||||
int RAG_DATASET_ID_NOT_NULL = 10176; // RAG配置中dataset_id不能为空
|
||||
int RAG_MODEL_ID_NOT_NULL = 10177; // RAG配置中model_id不能为空
|
||||
int RAG_DATASET_ID_AND_MODEL_ID_NOT_NULL = 10178; // RAG配置中dataset_id和model_id不能为空
|
||||
int RAG_FILE_NAME_NOT_NULL = 10179; // 文件名称不能为空
|
||||
int RAG_FILE_CONTENT_EMPTY = 10180; // 文件内容不能为空
|
||||
|
||||
// 设备相关错误码(补充)
|
||||
int MCA_NOT_NULL = 10175; // mac地址不能为空
|
||||
|
||||
// 音色克隆(补充)
|
||||
int VOICE_CLONE_NAME_NOT_NULL = 10181; // 音色克隆名称不能为空
|
||||
int VOICE_CLONE_AUDIO_NOT_FOUND = 10182; // 音色克隆音频不存在
|
||||
|
||||
// 智能体模板相关错误码(补充)
|
||||
int AGENT_TEMPLATE_NOT_FOUND = 10183; // 默认智能体未找到
|
||||
|
||||
// 知识库适配器相关错误码
|
||||
int RAG_ADAPTER_TYPE_NOT_SUPPORTED = 10184; // 不支持的适配器类型
|
||||
int RAG_CONFIG_VALIDATION_FAILED = 10185; // RAG配置验证失败
|
||||
int RAG_ADAPTER_CREATION_FAILED = 10186; // 适配器创建失败
|
||||
int RAG_ADAPTER_INIT_FAILED = 10187; // 适配器初始化失败
|
||||
int RAG_ADAPTER_CONNECTION_FAILED = 10188; // 适配器连接测试失败
|
||||
int RAG_ADAPTER_OPERATION_FAILED = 10189; // 适配器操作失败
|
||||
int RAG_ADAPTER_NOT_FOUND = 10190; // 适配器未找到
|
||||
int RAG_ADAPTER_CACHE_ERROR = 10191; // 适配器缓存错误
|
||||
int RAG_ADAPTER_TYPE_NOT_FOUND = 10192; // 适配器类型未找到
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.utils.MessageUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
|
||||
/**
|
||||
@@ -62,7 +63,7 @@ public class RenExceptionHandler {
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
public Result<Void> handleNoResourceFoundException(NoResourceFoundException ex) {
|
||||
log.warn("Resource not found: {}", ex.getMessage());
|
||||
return new Result<Void>().error(404, "资源不存在");
|
||||
return new Result<Void>().error(404, MessageUtils.getMessage(ErrorCode.RESOURCE_NOT_FOUND));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@@ -76,7 +77,7 @@ public class RenExceptionHandler {
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse("请求参数错误!");
|
||||
.orElse(MessageUtils.getMessage(ErrorCode.PARAM_VALUE_NULL));
|
||||
|
||||
return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg);
|
||||
}
|
||||
|
||||
@@ -32,13 +32,23 @@ public class FieldMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
// 创建者
|
||||
strictInsertFill(metaObject, CREATOR, Long.class, user.getId());
|
||||
// 创建时间
|
||||
strictInsertFill(metaObject, CREATE_DATE, Date.class, date);
|
||||
// 创建时间 - 支持createDate和createdAt两种字段名
|
||||
if (metaObject.hasSetter(CREATE_DATE)) {
|
||||
strictInsertFill(metaObject, CREATE_DATE, Date.class, date);
|
||||
}
|
||||
if (metaObject.hasSetter("createdAt")) {
|
||||
strictInsertFill(metaObject, "createdAt", Date.class, date);
|
||||
}
|
||||
|
||||
// 更新者
|
||||
strictInsertFill(metaObject, UPDATER, Long.class, user.getId());
|
||||
// 更新时间
|
||||
strictInsertFill(metaObject, UPDATE_DATE, Date.class, date);
|
||||
// 更新时间 - 支持updateDate和updatedAt两种字段名
|
||||
if (metaObject.hasSetter(UPDATE_DATE)) {
|
||||
strictInsertFill(metaObject, UPDATE_DATE, Date.class, date);
|
||||
}
|
||||
if (metaObject.hasSetter("updatedAt")) {
|
||||
strictInsertFill(metaObject, "updatedAt", Date.class, date);
|
||||
}
|
||||
|
||||
// 数据标识
|
||||
strictInsertFill(metaObject, DATA_OPERATION, String.class, Constant.DataOperation.INSERT.getValue());
|
||||
@@ -46,10 +56,17 @@ public class FieldMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
Date date = new Date();
|
||||
|
||||
// 更新者
|
||||
strictUpdateFill(metaObject, UPDATER, Long.class, SecurityUser.getUserId());
|
||||
// 更新时间
|
||||
strictUpdateFill(metaObject, UPDATE_DATE, Date.class, new Date());
|
||||
// 更新时间 - 支持updateDate和updatedAt两种字段名
|
||||
if (metaObject.hasSetter(UPDATE_DATE)) {
|
||||
strictUpdateFill(metaObject, UPDATE_DATE, Date.class, date);
|
||||
}
|
||||
if (metaObject.hasSetter("updatedAt")) {
|
||||
strictUpdateFill(metaObject, "updatedAt", Date.class, date);
|
||||
}
|
||||
|
||||
// 数据标识
|
||||
strictInsertFill(metaObject, DATA_OPERATION, String.class, Constant.DataOperation.UPDATE.getValue());
|
||||
|
||||
@@ -139,4 +139,31 @@ public class RedisKeys {
|
||||
return "sms:Validate:Code:" + phone + ":today_count";
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天记录UUID映射的Key
|
||||
*/
|
||||
public static String getChatHistoryKey(String uuid) {
|
||||
return "agent:chat:history:" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音色克隆音频ID的缓存key
|
||||
*/
|
||||
public static String getVoiceCloneAudioIdKey(String uuid) {
|
||||
return "voiceClone:audio:id:" + uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库缓存key
|
||||
*/
|
||||
public static String getKnowledgeBaseCacheKey(String datasetId) {
|
||||
return "knowledge:base:" + datasetId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取临时注册设备标记key
|
||||
*/
|
||||
public static String getTmpRegisterMacKey(String deviceId) {
|
||||
return "tmp_register_mac:" + deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ import org.springframework.context.i18n.LocaleContextHolder;
|
||||
public class MessageUtils {
|
||||
private static MessageSource messageSource;
|
||||
|
||||
static {
|
||||
messageSource = (MessageSource) SpringContextUtils.getBean("messageSource");
|
||||
}
|
||||
|
||||
public static String getMessage(int code) {
|
||||
return getMessage(code, new String[0]);
|
||||
}
|
||||
|
||||
public static String getMessage(int code, String... params) {
|
||||
if (messageSource == null) {
|
||||
// 延迟初始化,确保Spring上下文已完全初始化
|
||||
messageSource = (MessageSource) SpringContextUtils.getBean("messageSource");
|
||||
}
|
||||
return messageSource.getMessage(code + "", params, LocaleContextHolder.getLocale());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@@ -37,7 +38,7 @@ public class ResourcesUtils {
|
||||
}
|
||||
} catch (IOException e){
|
||||
log.error("方法:loadString()读取资源失败--{}",e.getMessage());
|
||||
throw new RenException("读取资源失败");
|
||||
throw new RenException(ErrorCode.RESOURCE_READ_ERROR);
|
||||
}
|
||||
return luaScriptBuilder.toString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import org.bouncycastle.asn1.gm.GMNamedCurves;
|
||||
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||
import org.bouncycastle.crypto.engines.SM2Engine;
|
||||
import org.bouncycastle.crypto.params.ECDomainParameters;
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.bouncycastle.crypto.params.ParametersWithRandom;
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.math.ec.ECPoint;
|
||||
import org.bouncycastle.util.encoders.Hex;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.*;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SM2加密工具类(采用十六进制格式,与chancheng-archive-service项目保持一致)
|
||||
*/
|
||||
public class SM2Utils {
|
||||
|
||||
/**
|
||||
* 公钥常量
|
||||
*/
|
||||
public static final String KEY_PUBLIC_KEY = "publicKey";
|
||||
/**
|
||||
* 私钥返回值常量
|
||||
*/
|
||||
public static final String KEY_PRIVATE_KEY = "privateKey";
|
||||
|
||||
static {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
/**
|
||||
* SM2加密算法
|
||||
*
|
||||
* @param publicKey 十六进制公钥
|
||||
* @param data 明文数据
|
||||
* @return 十六进制密文
|
||||
*/
|
||||
public static String encrypt(String publicKey, String data) {
|
||||
try {
|
||||
// 获取一条SM2曲线参数
|
||||
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
|
||||
// 构造ECC算法参数,曲线方程、椭圆曲线G点、大整数N
|
||||
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
|
||||
//提取公钥点
|
||||
ECPoint pukPoint = sm2ECParameters.getCurve().decodePoint(Hex.decode(publicKey));
|
||||
// 公钥前面的02或者03表示是压缩公钥,04表示未压缩公钥, 04的时候,可以去掉前面的04
|
||||
ECPublicKeyParameters publicKeyParameters = new ECPublicKeyParameters(pukPoint, domainParameters);
|
||||
|
||||
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
|
||||
// 设置sm2为加密模式
|
||||
sm2Engine.init(true, new ParametersWithRandom(publicKeyParameters, new SecureRandom()));
|
||||
|
||||
byte[] in = data.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] arrayOfBytes = sm2Engine.processBlock(in, 0, in.length);
|
||||
return Hex.toHexString(arrayOfBytes);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("SM2加密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SM2解密算法
|
||||
*
|
||||
* @param privateKey 十六进制私钥
|
||||
* @param cipherData 十六进制密文数据
|
||||
* @return 明文
|
||||
*/
|
||||
public static String decrypt(String privateKey, String cipherData) {
|
||||
try {
|
||||
// 使用BC库加解密时密文以04开头,传入的密文前面没有04则补上
|
||||
if (!cipherData.startsWith("04")) {
|
||||
cipherData = "04" + cipherData;
|
||||
}
|
||||
byte[] cipherDataByte = Hex.decode(cipherData);
|
||||
BigInteger privateKeyD = new BigInteger(privateKey, 16);
|
||||
//获取一条SM2曲线参数
|
||||
X9ECParameters sm2ECParameters = GMNamedCurves.getByName("sm2p256v1");
|
||||
//构造domain参数
|
||||
ECDomainParameters domainParameters = new ECDomainParameters(sm2ECParameters.getCurve(), sm2ECParameters.getG(), sm2ECParameters.getN());
|
||||
ECPrivateKeyParameters privateKeyParameters = new ECPrivateKeyParameters(privateKeyD, domainParameters);
|
||||
|
||||
SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
|
||||
// 设置sm2为解密模式
|
||||
sm2Engine.init(false, privateKeyParameters);
|
||||
|
||||
byte[] arrayOfBytes = sm2Engine.processBlock(cipherDataByte, 0, cipherDataByte.length);
|
||||
return new String(arrayOfBytes, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("SM2解密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成密钥对
|
||||
*/
|
||||
public static Map<String, String> createKey() {
|
||||
try {
|
||||
ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");
|
||||
// 获取一个椭圆曲线类型的密钥对生成器
|
||||
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());
|
||||
// 使用SM2参数初始化生成器
|
||||
kpg.initialize(sm2Spec);
|
||||
// 获取密钥对
|
||||
KeyPair keyPair = kpg.generateKeyPair();
|
||||
PublicKey publicKey = keyPair.getPublic();
|
||||
BCECPublicKey p = (BCECPublicKey) publicKey;
|
||||
PrivateKey privateKey = keyPair.getPrivate();
|
||||
BCECPrivateKey s = (BCECPrivateKey) privateKey;
|
||||
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put(KEY_PUBLIC_KEY, Hex.toHexString(p.getQ().getEncoded(false)));
|
||||
result.put(KEY_PRIVATE_KEY, Hex.toHexString(s.getD().toByteArray()));
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("生成SM2密钥对失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
|
||||
/**
|
||||
* 敏感数据处理工具类
|
||||
*/
|
||||
public class SensitiveDataUtils {
|
||||
|
||||
// 敏感字段列表
|
||||
private static final Set<String> SENSITIVE_FIELDS = new HashSet<>(Arrays.asList(
|
||||
"api_key", "personal_access_token", "access_token", "token",
|
||||
"secret", "access_key_secret", "secret_key"));
|
||||
|
||||
/**
|
||||
* 检查字段是否为敏感字段
|
||||
*/
|
||||
public static boolean isSensitiveField(String fieldName) {
|
||||
return StringUtils.isNotBlank(fieldName) && SENSITIVE_FIELDS.contains(fieldName.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏字符串中间部分
|
||||
*/
|
||||
public static String maskMiddle(String value) {
|
||||
if (StringUtils.isBlank(value) || value.length() == 1) {
|
||||
return value;
|
||||
}
|
||||
|
||||
int length = value.length();
|
||||
if (length <= 8) {
|
||||
// 短字符串保留前2后2
|
||||
return value.substring(0, 2) + "****" + value.substring(length - 2);
|
||||
} else {
|
||||
// 长字符串保留前4后4
|
||||
int maskLength = length - 8;
|
||||
StringBuilder maskBuilder = new StringBuilder();
|
||||
for (int i = 0; i < maskLength; i++) {
|
||||
maskBuilder.append('*');
|
||||
}
|
||||
return value.substring(0, 4) + maskBuilder.toString() + value.substring(length - 4);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否是被掩码处理过的值
|
||||
*/
|
||||
public static boolean isMaskedValue(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return false;
|
||||
}
|
||||
// 掩码值至少包含4个连续的*
|
||||
return value.contains("****");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理JSONObject中的敏感字段
|
||||
*/
|
||||
public static JSONObject maskSensitiveFields(JSONObject jsonObject) {
|
||||
if (jsonObject == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
|
||||
for (String key : jsonObject.keySet()) {
|
||||
Object value = jsonObject.get(key);
|
||||
|
||||
if (SENSITIVE_FIELDS.contains(key.toLowerCase()) && value instanceof String) {
|
||||
result.put(key, maskMiddle((String) value));
|
||||
} else if (value instanceof JSONObject) {
|
||||
result.put(key, maskSensitiveFields((JSONObject) value));
|
||||
} else {
|
||||
result.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个JSONObject的敏感字段是否相同
|
||||
* 特别针对api_key等敏感字段进行单独比较
|
||||
*/
|
||||
public static boolean isSensitiveDataEqual(JSONObject original, JSONObject updated) {
|
||||
if (original == null && updated == null) {
|
||||
return true;
|
||||
}
|
||||
if (original == null || updated == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 提取并比较特定敏感字段
|
||||
return compareSpecificSensitiveFields(original, updated, "api_key") &&
|
||||
compareSpecificSensitiveFields(original, updated, "personal_access_token") &&
|
||||
compareSpecificSensitiveFields(original, updated, "access_token") &&
|
||||
compareSpecificSensitiveFields(original, updated, "token") &&
|
||||
compareSpecificSensitiveFields(original, updated, "secret") &&
|
||||
compareSpecificSensitiveFields(original, updated, "access_key_secret") &&
|
||||
compareSpecificSensitiveFields(original, updated, "secret_key");
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较两个JSON对象中特定敏感字段是否相同
|
||||
* 遍历整个JSON对象树,查找并比较指定敏感字段
|
||||
*/
|
||||
private static boolean compareSpecificSensitiveFields(JSONObject original, JSONObject updated, String fieldName) {
|
||||
// 提取原始对象中的指定敏感字段
|
||||
Map<String, String> originalFields = new HashMap<>();
|
||||
extractSpecificSensitiveField(original, originalFields, fieldName, "");
|
||||
|
||||
// 提取更新对象中的指定敏感字段
|
||||
Map<String, String> updatedFields = new HashMap<>();
|
||||
extractSpecificSensitiveField(updated, updatedFields, fieldName, "");
|
||||
|
||||
// 如果字段数量不同,说明有增删
|
||||
if (originalFields.size() != updatedFields.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 比较每个字段的值
|
||||
for (Map.Entry<String, String> entry : originalFields.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String originalValue = entry.getValue();
|
||||
String updatedValue = updatedFields.get(key);
|
||||
|
||||
if (updatedValue == null || !updatedValue.equals(originalValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归提取JSON对象中指定名称的敏感字段
|
||||
*/
|
||||
private static void extractSpecificSensitiveField(JSONObject jsonObject, Map<String, String> fieldsMap,
|
||||
String targetFieldName, String parentPath) {
|
||||
if (jsonObject == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String key : jsonObject.keySet()) {
|
||||
String fullPath = parentPath.isEmpty() ? key : parentPath + "." + key;
|
||||
Object value = jsonObject.get(key);
|
||||
|
||||
if (value instanceof JSONObject) {
|
||||
// 递归处理嵌套JSON对象
|
||||
extractSpecificSensitiveField((JSONObject) value, fieldsMap, targetFieldName, fullPath);
|
||||
} else if (value instanceof String && key.equalsIgnoreCase(targetFieldName)) {
|
||||
// 找到目标敏感字段,保存其路径和值
|
||||
fieldsMap.put(fullPath, (String) value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.modules.security.service.CaptchaService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* SM2解密和验证码验证工具类
|
||||
* 封装了重复的SM2解密、验证码提取和验证逻辑
|
||||
*/
|
||||
public class Sm2DecryptUtil {
|
||||
|
||||
/**
|
||||
* 验证码长度
|
||||
*/
|
||||
private static final int CAPTCHA_LENGTH = 5;
|
||||
|
||||
/**
|
||||
* 解密SM2加密内容,提取验证码并验证
|
||||
* @param encryptedPassword SM2加密的密码字符串
|
||||
* @param captchaId 验证码ID
|
||||
* @param captchaService 验证码服务
|
||||
* @param sysParamsService 系统参数服务
|
||||
* @return 解密后的实际密码
|
||||
*/
|
||||
public static String decryptAndValidateCaptcha(String encryptedPassword, String captchaId,
|
||||
CaptchaService captchaService, SysParamsService sysParamsService) {
|
||||
// 获取SM2私钥
|
||||
String privateKeyStr = sysParamsService.getValue(Constant.SM2_PRIVATE_KEY, true);
|
||||
if (StringUtils.isBlank(privateKeyStr)) {
|
||||
throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
// 使用SM2私钥解密密码
|
||||
String decryptedContent;
|
||||
try {
|
||||
decryptedContent = SM2Utils.decrypt(privateKeyStr, encryptedPassword);
|
||||
} catch (Exception e) {
|
||||
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
|
||||
}
|
||||
|
||||
// 分离验证码和密码:前5位是验证码,后面是密码
|
||||
if (decryptedContent.length() > CAPTCHA_LENGTH) {
|
||||
String embeddedCaptcha = decryptedContent.substring(0, CAPTCHA_LENGTH);
|
||||
String actualPassword = decryptedContent.substring(CAPTCHA_LENGTH);
|
||||
|
||||
// 验证嵌入的验证码是否正确
|
||||
boolean embeddedCaptchaValid = captchaService.validate(captchaId, embeddedCaptcha, true);
|
||||
if (!embeddedCaptchaValid) {
|
||||
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
|
||||
}
|
||||
|
||||
return actualPassword;
|
||||
} else {
|
||||
throw new RenException(ErrorCode.SM2_DECRYPT_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
+210
@@ -1,5 +1,19 @@
|
||||
package xiaozhi.modules.agent.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -7,11 +21,26 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.utils.MessageUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryReportDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Tag(name = "智能体聊天历史管理")
|
||||
@RequiredArgsConstructor
|
||||
@@ -19,6 +48,9 @@ import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||
@RequestMapping("/agent/chat-history")
|
||||
public class AgentChatHistoryController {
|
||||
private final AgentChatHistoryBizService agentChatHistoryBizService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentService agentService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
/**
|
||||
* 小智服务聊天上报请求
|
||||
@@ -33,4 +65,182 @@ public class AgentChatHistoryController {
|
||||
Boolean result = agentChatHistoryBizService.report(request);
|
||||
return new Result<Boolean>().ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天记录下载链接
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param sessionId 会话ID
|
||||
* @return UUID作为下载标识
|
||||
*/
|
||||
@Operation(summary = "获取聊天记录下载链接")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
@PostMapping("/getDownloadUrl/{agentId}/{sessionId}")
|
||||
public Result<String> getDownloadUrl(@PathVariable("agentId") String agentId,
|
||||
@PathVariable("sessionId") String sessionId) {
|
||||
// 获取当前用户
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
// 检查权限
|
||||
if (!agentService.checkAgentPermission(agentId, user.getId())) {
|
||||
throw new RenException(ErrorCode.CHAT_HISTORY_NO_PERMISSION);
|
||||
}
|
||||
|
||||
// 生成UUID
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
// 存储agentId和sessionId到Redis,格式为agentId:sessionId
|
||||
redisUtils.set(RedisKeys.getChatHistoryKey(uuid), agentId + ":" + sessionId);
|
||||
|
||||
return new Result<String>().ok(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载本会话聊天记录
|
||||
*
|
||||
* @param uuid 下载标识
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@Operation(summary = "下载本会话聊天记录")
|
||||
@GetMapping("/download/{uuid}/current")
|
||||
public void downloadCurrentSession(@PathVariable("uuid") String uuid,
|
||||
HttpServletResponse response) {
|
||||
// 从Redis获取agentId和sessionId
|
||||
String agentSessionInfo = (String) redisUtils.get(RedisKeys.getChatHistoryKey(uuid));
|
||||
if (StringUtils.isBlank(agentSessionInfo)) {
|
||||
throw new RenException(ErrorCode.DOWNLOAD_LINK_EXPIRED);
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析agentId和sessionId
|
||||
String[] parts = agentSessionInfo.split(":");
|
||||
if (parts.length != 2) {
|
||||
throw new RenException(ErrorCode.DOWNLOAD_LINK_INVALID);
|
||||
}
|
||||
String agentId = parts[0];
|
||||
String sessionId = parts[1];
|
||||
|
||||
// 执行下载
|
||||
downloadChatHistory(agentId, List.of(sessionId), response);
|
||||
} finally {
|
||||
// 下载完成后删除UUID,防止盗刷
|
||||
redisUtils.delete(RedisKeys.getChatHistoryKey(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载本会话及前20条会话聊天记录
|
||||
*
|
||||
* @param uuid 下载标识
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
@Operation(summary = "下载本会话及前20条会话聊天记录")
|
||||
@GetMapping("/download/{uuid}/previous")
|
||||
public void downloadCurrentSessionWithPrevious(@PathVariable("uuid") String uuid,
|
||||
HttpServletResponse response) {
|
||||
// 从Redis获取agentId和sessionId
|
||||
String agentSessionInfo = (String) redisUtils.get(RedisKeys.getChatHistoryKey(uuid));
|
||||
if (StringUtils.isBlank(agentSessionInfo)) {
|
||||
throw new RenException(ErrorCode.DOWNLOAD_LINK_EXPIRED);
|
||||
}
|
||||
|
||||
try {
|
||||
// 解析agentId和sessionId
|
||||
String[] parts = agentSessionInfo.split(":");
|
||||
if (parts.length != 2) {
|
||||
throw new RenException(ErrorCode.DOWNLOAD_LINK_INVALID);
|
||||
}
|
||||
String agentId = parts[0];
|
||||
String sessionId = parts[1];
|
||||
|
||||
// 获取所有会话列表
|
||||
Map<String, Object> params = Map.of(
|
||||
"agentId", agentId,
|
||||
Constant.PAGE, 1,
|
||||
Constant.LIMIT, 1000 // 获取足够多的会话
|
||||
);
|
||||
PageData<AgentChatSessionDTO> sessionPage = agentChatHistoryService.getSessionListByAgentId(params);
|
||||
List<AgentChatSessionDTO> allSessions = sessionPage.getList();
|
||||
|
||||
// 查找当前会话在列表中的位置
|
||||
int currentIndex = -1;
|
||||
for (int i = 0; i < allSessions.size(); i++) {
|
||||
if (allSessions.get(i).getSessionId().equals(sessionId)) {
|
||||
currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了当前会话,收集当前会话及前20条会话ID
|
||||
List<String> sessionIdsToDownload = new ArrayList<>();
|
||||
if (currentIndex != -1) {
|
||||
// 从当前会话开始,向后(数组后面)取最多20条会话(包括当前会话)
|
||||
int endIndex = Math.min(allSessions.size() - 1, currentIndex + 20); // 确保不越界
|
||||
for (int i = currentIndex; i <= endIndex; i++) {
|
||||
sessionIdsToDownload.add(allSessions.get(i).getSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到当前会话,至少下载当前会话
|
||||
if (sessionIdsToDownload.isEmpty()) {
|
||||
sessionIdsToDownload.add(sessionId);
|
||||
}
|
||||
downloadChatHistory(agentId, sessionIdsToDownload, response);
|
||||
} finally {
|
||||
// 下载完成后删除UUID,防止盗刷
|
||||
redisUtils.delete(RedisKeys.getChatHistoryKey(uuid));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载指定会话的聊天记录
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @param sessionIds 会话ID列表
|
||||
* @param response HTTP响应
|
||||
*/
|
||||
private void downloadChatHistory(String agentId, List<String> sessionIds, HttpServletResponse response) {
|
||||
try {
|
||||
// 设置响应头
|
||||
response.setContentType("text/plain;charset=UTF-8");
|
||||
String fileName = URLEncoder.encode("history.txt", StandardCharsets.UTF_8.toString());
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
|
||||
|
||||
// 获取聊天记录并写入响应流
|
||||
try (OutputStream out = response.getOutputStream()) {
|
||||
// 为每个会话生成聊天记录
|
||||
for (String sessionId : sessionIds) {
|
||||
// 获取该会话的所有聊天记录
|
||||
List<AgentChatHistoryDTO> chatHistoryList = agentChatHistoryService
|
||||
.getChatHistoryBySessionId(agentId, sessionId);
|
||||
|
||||
// 从聊天记录中获取第一条消息的创建时间作为会话时间
|
||||
if (!chatHistoryList.isEmpty()) {
|
||||
Date firstMessageTime = chatHistoryList.get(0).getCreatedAt();
|
||||
String sessionTimeStr = DateUtils.format(firstMessageTime, DateUtils.DATE_TIME_PATTERN);
|
||||
out.write((sessionTimeStr + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
for (AgentChatHistoryDTO message : chatHistoryList) {
|
||||
String role = message.getChatType() == 1 ? MessageUtils.getMessage(ErrorCode.CHAT_ROLE_USER)
|
||||
: MessageUtils.getMessage(ErrorCode.CHAT_ROLE_AGENT);
|
||||
String direction = message.getChatType() == 1 ? ">>" : "<<";
|
||||
Date messageTime = message.getCreatedAt();
|
||||
String messageTimeStr = DateUtils.format(messageTime, DateUtils.DATE_TIME_PATTERN);
|
||||
String content = message.getContent();
|
||||
|
||||
String line = "[" + role + "]-[" + messageTimeStr + "]" + direction + ":" + content + "\n";
|
||||
out.write(line.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
// 会话之间添加空行分隔
|
||||
if (sessionIds.indexOf(sessionId) < sessionIds.size() - 1) {
|
||||
out.write("\n".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
out.flush();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
@@ -64,6 +66,8 @@ public class AgentController {
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentChatSummaryService agentChatSummaryService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@GetMapping("/list")
|
||||
@@ -117,6 +121,27 @@ public class AgentController {
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PostMapping("/chat-summary/{sessionId}/save")
|
||||
@Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)")
|
||||
public Result<Void> generateAndSaveChatSummary(@PathVariable String sessionId) {
|
||||
try {
|
||||
// 异步执行总结生成任务,立即返回成功响应
|
||||
new Thread(() -> {
|
||||
try {
|
||||
agentChatSummaryService.generateAndSaveChatSummary(sessionId);
|
||||
System.out.println("异步执行会话 " + sessionId + " 的聊天记录总结完成");
|
||||
} catch (Exception e) {
|
||||
System.err.println("异步执行会话 " + sessionId + " 的聊天记录总结失败: " + e.getMessage());
|
||||
}
|
||||
}).start();
|
||||
|
||||
// 立即返回成功响应,不等待总结生成完成
|
||||
return new Result<Void>().ok(null);
|
||||
} catch (Exception e) {
|
||||
return new Result<Void>().error("启动异步总结生成任务失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
@@ -135,6 +160,8 @@ public class AgentController {
|
||||
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||
// 删除关联的插件
|
||||
agentPluginMappingService.deleteByAgentId(id);
|
||||
// 删除关联的上下文源配置
|
||||
agentContextProviderService.deleteByAgentId(id);
|
||||
// 再删除智能体
|
||||
agentService.deleteById(id);
|
||||
return new Result<>();
|
||||
@@ -182,6 +209,7 @@ public class AgentController {
|
||||
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
||||
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/chat-history/user")
|
||||
@Operation(summary = "获取智能体聊天记录(用户)")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package xiaozhi.modules.agent.controller;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.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 com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.utils.ResultUtils;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.agent.vo.AgentTemplateVO;
|
||||
|
||||
@Tag(name = "智能体模板管理")
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/agent/template")
|
||||
public class AgentTemplateController {
|
||||
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获取模板分页列表")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||
@Parameter(name = "agentName", description = "模板名称,模糊查询")
|
||||
})
|
||||
public Result<PageData<AgentTemplateVO>> getAgentTemplatesPage(
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
|
||||
// 创建分页对象
|
||||
int page = Integer.parseInt(params.getOrDefault(Constant.PAGE, "1").toString());
|
||||
int limit = Integer.parseInt(params.getOrDefault(Constant.LIMIT, "10").toString());
|
||||
Page<AgentTemplateEntity> pageInfo = new Page<>(page, limit);
|
||||
|
||||
// 创建查询条件
|
||||
QueryWrapper<AgentTemplateEntity> wrapper = new QueryWrapper<>();
|
||||
String agentName = (String) params.get("agentName");
|
||||
if (agentName != null && !agentName.isEmpty()) {
|
||||
wrapper.like("agent_name", agentName);
|
||||
}
|
||||
wrapper.orderByAsc("sort");
|
||||
|
||||
// 执行分页查询
|
||||
IPage<AgentTemplateEntity> pageResult = agentTemplateService.page(pageInfo, wrapper);
|
||||
|
||||
// 使用ConvertUtils转换为VO列表
|
||||
List<AgentTemplateVO> voList = ConvertUtils.sourceToTarget(pageResult.getRecords(), AgentTemplateVO.class);
|
||||
|
||||
// 修复:使用构造函数创建PageData对象,而不是无参构造+setter
|
||||
PageData<AgentTemplateVO> pageData = new PageData<>(voList, pageResult.getTotal());
|
||||
|
||||
return new Result<PageData<AgentTemplateVO>>().ok(pageData);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "获取模板详情")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<AgentTemplateVO> getAgentTemplateById(@PathVariable("id") String id) {
|
||||
AgentTemplateEntity template = agentTemplateService.getById(id);
|
||||
if (template == null) {
|
||||
return ResultUtils.error("模板不存在");
|
||||
}
|
||||
|
||||
// 使用ConvertUtils转换为VO
|
||||
AgentTemplateVO vo = ConvertUtils.sourceToTarget(template, AgentTemplateVO.class);
|
||||
|
||||
return ResultUtils.success(vo);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建模板")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<AgentTemplateEntity> createAgentTemplate(@Valid @RequestBody AgentTemplateEntity template) {
|
||||
// 设置排序值为下一个可用的序号
|
||||
template.setSort(agentTemplateService.getNextAvailableSort());
|
||||
|
||||
boolean saved = agentTemplateService.save(template);
|
||||
if (saved) {
|
||||
return ResultUtils.success(template);
|
||||
} else {
|
||||
return ResultUtils.error("创建模板失败");
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "更新模板")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<AgentTemplateEntity> updateAgentTemplate(@Valid @RequestBody AgentTemplateEntity template) {
|
||||
boolean updated = agentTemplateService.updateById(template);
|
||||
if (updated) {
|
||||
return ResultUtils.success(template);
|
||||
} else {
|
||||
return ResultUtils.error("更新模板失败");
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "删除模板")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<String> deleteAgentTemplate(@PathVariable("id") String id) {
|
||||
// 先查询要删除的模板信息,获取其排序值
|
||||
AgentTemplateEntity template = agentTemplateService.getById(id);
|
||||
if (template == null) {
|
||||
return ResultUtils.error("模板不存在");
|
||||
}
|
||||
|
||||
Integer deletedSort = template.getSort();
|
||||
|
||||
// 执行删除操作
|
||||
boolean deleted = agentTemplateService.removeById(id);
|
||||
if (deleted) {
|
||||
// 删除成功后,重新排序剩余模板
|
||||
agentTemplateService.reorderTemplatesAfterDelete(deletedSort);
|
||||
return ResultUtils.success("删除模板成功");
|
||||
} else {
|
||||
return ResultUtils.error("删除模板失败");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 添加新的批量删除方法,使用不同的URL
|
||||
@PostMapping("/batch-remove")
|
||||
@Operation(summary = "批量删除模板")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<String> batchRemoveAgentTemplates(@RequestBody List<String> ids) {
|
||||
boolean deleted = agentTemplateService.removeByIds(ids);
|
||||
if (deleted) {
|
||||
return ResultUtils.success("批量删除成功");
|
||||
} else {
|
||||
return ResultUtils.error("批量删除模板失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentVoicePrintSaveDTO;
|
||||
@@ -42,7 +43,7 @@ public class AgentVoicePrintController {
|
||||
if (b) {
|
||||
return new Result<>();
|
||||
}
|
||||
return new Result<Void>().error("智能体的声纹创建失败");
|
||||
return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_CREATE_FAILED);
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@@ -54,7 +55,7 @@ public class AgentVoicePrintController {
|
||||
if (b) {
|
||||
return new Result<>();
|
||||
}
|
||||
return new Result<Void>().error("智能体的对应声纹更新失败");
|
||||
return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_UPDATE_FAILED);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@@ -67,7 +68,7 @@ public class AgentVoicePrintController {
|
||||
if (delete) {
|
||||
return new Result<>();
|
||||
}
|
||||
return new Result<Void>().error("智能体的对应声纹删除失败");
|
||||
return new Result<Void>().error(ErrorCode.AGENT_VOICEPRINT_DELETE_FAILED);
|
||||
}
|
||||
|
||||
@GetMapping("/list/{id}")
|
||||
@@ -76,7 +77,7 @@ public class AgentVoicePrintController {
|
||||
public Result<List<AgentVoicePrintVO>> list(@PathVariable String id) {
|
||||
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
|
||||
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
|
||||
throw new RenException("声纹接口未配置,请先在参数配置中配置声纹接口地址(server.voice_print)");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_API_NOT_CONFIGURED);
|
||||
}
|
||||
Long userId = SecurityUser.getUserId();
|
||||
List<AgentVoicePrintVO> list = agentVoicePrintService.list(userId, id);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
|
||||
@Mapper
|
||||
public interface AgentContextProviderDao extends BaseDao<AgentContextProviderEntity> {
|
||||
}
|
||||
+18
-6
@@ -1,6 +1,9 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
@@ -15,12 +18,6 @@ import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity> {
|
||||
/**
|
||||
* 根据智能体ID删除音频
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAudioByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除聊天历史记录
|
||||
@@ -35,4 +32,19 @@ public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAudioIdByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 根据智能体ID获取所有音频ID列表
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @return 音频ID列表
|
||||
*/
|
||||
List<String> getAudioIdsByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 批量删除音频
|
||||
*
|
||||
* @param audioIds 音频ID列表
|
||||
*/
|
||||
void deleteAudioByIds(@Param("audioIds") List<String> audioIds);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录总结DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "智能体聊天记录总结对象")
|
||||
public class AgentChatSummaryDTO {
|
||||
|
||||
@Schema(description = "会话ID")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "智能体ID")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "总结内容")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "总结状态")
|
||||
private boolean success;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
public AgentChatSummaryDTO() {
|
||||
this.success = true;
|
||||
}
|
||||
|
||||
public AgentChatSummaryDTO(String sessionId, String agentId, String summary) {
|
||||
this.sessionId = sessionId;
|
||||
this.agentId = agentId;
|
||||
this.summary = summary;
|
||||
this.success = true;
|
||||
}
|
||||
|
||||
public AgentChatSummaryDTO(String sessionId, String errorMessage) {
|
||||
this.sessionId = sessionId;
|
||||
this.errorMessage = errorMessage;
|
||||
this.success = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,6 +69,9 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "排序", example = "1", nullable = true)
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "上下文源配置", nullable = true)
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Data
|
||||
@Schema(description = "插件函数信息")
|
||||
public static class FunctionInfo implements Serializable {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "上下文源配置DTO")
|
||||
public class ContextProviderDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "URL地址")
|
||||
private String url;
|
||||
|
||||
@Schema(description = "请求头")
|
||||
private Map<String, Object> headers;
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
|
||||
@Data
|
||||
@TableName(value = "ai_agent_context_provider", autoResultMap = true)
|
||||
@Schema(description = "智能体上下文源配置")
|
||||
public class AgentContextProviderEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "智能体ID")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录总结服务接口
|
||||
*/
|
||||
public interface AgentChatSummaryService {
|
||||
|
||||
/**
|
||||
* 根据会话ID生成聊天记录总结并保存到智能体记忆
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 保存结果
|
||||
*/
|
||||
boolean generateAndSaveChatSummary(String sessionId);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
|
||||
public interface AgentContextProviderService extends BaseService<AgentContextProviderEntity> {
|
||||
/**
|
||||
* 根据智能体ID获取上下文源配置
|
||||
* @param agentId 智能体ID
|
||||
* @return 上下文源配置实体
|
||||
*/
|
||||
AgentContextProviderEntity getByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 保存或更新上下文源配置
|
||||
* @param entity 实体
|
||||
*/
|
||||
void saveOrUpdateByAgentId(AgentContextProviderEntity entity);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除上下文源配置
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteByAgentId(String agentId);
|
||||
}
|
||||
+14
@@ -25,4 +25,18 @@ public interface AgentTemplateService extends IService<AgentTemplateEntity> {
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
void updateDefaultTemplateModelId(String modelType, String modelId);
|
||||
|
||||
/**
|
||||
* 删除模板后重新排序剩余模板
|
||||
*
|
||||
* @param deletedSort 被删除模板的排序值
|
||||
*/
|
||||
void reorderTemplatesAfterDelete(Integer deletedSort);
|
||||
|
||||
/**
|
||||
* 获取下一个可用的排序序号(寻找最小的未使用序号)
|
||||
*
|
||||
* @return 下一个可用的排序序号
|
||||
*/
|
||||
Integer getNextAvailableSort();
|
||||
}
|
||||
|
||||
+6
-2
@@ -17,6 +17,7 @@ import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
@@ -36,6 +37,7 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
||||
private final AgentService agentService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final AgentChatSummaryService agentChatSummaryService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@@ -50,7 +52,8 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
||||
public Boolean report(AgentChatHistoryReportDTO report) {
|
||||
String macAddress = report.getMacAddress();
|
||||
Byte chatType = report.getChatType();
|
||||
Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime() * 1000 : System.currentTimeMillis();
|
||||
Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime() * 1000
|
||||
: System.currentTimeMillis();
|
||||
log.info("小智设备聊天上报请求: macAddress={}, type={} reportTime={}", macAddress, chatType, reportTimeMillis);
|
||||
|
||||
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||
@@ -105,7 +108,8 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
||||
/**
|
||||
* 组装上报数据
|
||||
*/
|
||||
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId, Long reportTime) {
|
||||
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId,
|
||||
Long reportTime) {
|
||||
// 构建聊天记录实体
|
||||
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||
.macAddress(macAddress)
|
||||
|
||||
+11
-2
@@ -84,7 +84,16 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
||||
if (deleteAudio) {
|
||||
baseMapper.deleteAudioByAgentId(agentId);
|
||||
// 分批删除音频,避免超时
|
||||
List<String> audioIds = baseMapper.getAudioIdsByAgentId(agentId);
|
||||
if (audioIds != null && !audioIds.isEmpty()) {
|
||||
int batchSize = 1000; // 每批删除1000条
|
||||
for (int i = 0; i < audioIds.size(); i += batchSize) {
|
||||
int end = Math.min(i + batchSize, audioIds.size());
|
||||
List<String> batch = audioIds.subList(i, end);
|
||||
baseMapper.deleteAudioByIds(batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (deleteAudio && !deleteText) {
|
||||
baseMapper.deleteAudioIdByAgentId(agentId);
|
||||
@@ -107,7 +116,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
// 添加此行,确保查询结果按照创建时间降序排列
|
||||
// 使用id的原因:数据形式,id越大的创建时间就越晚,所以使用id的结果和创建时间降序排列结果一样
|
||||
// id作为降序排列的优势,性能高,有主键索引,不用在排序的时候重新进行排除扫描比较
|
||||
.orderByDesc(AgentChatHistoryEntity::getId);
|
||||
.orderByDesc(AgentChatHistoryEntity::getId);
|
||||
|
||||
// 构建分页查询,查询前50页数据
|
||||
Page<AgentChatHistoryEntity> pageParam = new Page<>(0, 50);
|
||||
|
||||
+423
@@ -0,0 +1,423 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSummaryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.llm.service.LLMService;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录总结服务实现类
|
||||
* 实现Python端mem_local_short.py中的总结逻辑
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AgentChatSummaryServiceImpl.class);
|
||||
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentService agentService;
|
||||
private final DeviceService deviceService;
|
||||
private final LLMService llmService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
|
||||
// 总结规则常量
|
||||
private static final int MAX_SUMMARY_LENGTH = 1800; // 最大总结长度
|
||||
private static final Pattern JSON_PATTERN = Pattern.compile("\\{.*?\\}", Pattern.DOTALL);
|
||||
private static final Pattern DEVICE_CONTROL_PATTERN = Pattern.compile("设备控制|设备操作|控制设备|设备状态",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern WEATHER_PATTERN = Pattern.compile("天气|温度|湿度|降雨|气象", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile("日期|时间|星期|月份|年份", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private AgentChatSummaryDTO generateChatSummary(String sessionId) {
|
||||
try {
|
||||
System.out.println("开始生成会话 " + sessionId + " 的聊天记录总结");
|
||||
|
||||
// 1. 根据sessionId获取聊天记录
|
||||
List<AgentChatHistoryDTO> chatHistory = getChatHistoryBySessionId(sessionId);
|
||||
if (chatHistory == null || chatHistory.isEmpty()) {
|
||||
return new AgentChatSummaryDTO(sessionId, "未找到该会话的聊天记录");
|
||||
}
|
||||
|
||||
// 2. 获取智能体信息
|
||||
String agentId = getAgentIdFromSession(sessionId, chatHistory);
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return new AgentChatSummaryDTO(sessionId, "无法获取智能体信息");
|
||||
}
|
||||
|
||||
// 3. 提取关键对话内容
|
||||
List<String> meaningfulMessages = extractMeaningfulMessages(chatHistory);
|
||||
if (meaningfulMessages.isEmpty()) {
|
||||
return new AgentChatSummaryDTO(sessionId, "没有有效的对话内容可总结");
|
||||
}
|
||||
|
||||
// 4. 生成总结(generateSummaryFromMessages方法已包含长度限制逻辑)
|
||||
String summary = generateSummaryFromMessages(meaningfulMessages, agentId);
|
||||
|
||||
System.out.println("成功生成会话 " + sessionId + " 的聊天记录总结,长度: " + summary.length() + " 字符");
|
||||
return new AgentChatSummaryDTO(sessionId, agentId, summary);
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("生成会话 " + sessionId + " 的聊天记录总结时发生错误: " + e.getMessage());
|
||||
return new AgentChatSummaryDTO(sessionId, "生成总结时发生错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean generateAndSaveChatSummary(String sessionId) {
|
||||
try {
|
||||
// 1. 生成总结
|
||||
AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId);
|
||||
if (!summaryDTO.isSuccess()) {
|
||||
System.err.println("生成总结失败: " + summaryDTO.getErrorMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 获取设备信息(通过会话关联的设备)
|
||||
DeviceEntity device = getDeviceBySessionId(sessionId);
|
||||
if (device == null) {
|
||||
System.err.println("未找到与会话 " + sessionId + " 关联的设备");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 更新智能体记忆
|
||||
AgentMemoryDTO memoryDTO = new AgentMemoryDTO();
|
||||
memoryDTO.setSummaryMemory(summaryDTO.getSummary());
|
||||
|
||||
// 调用现有接口更新记忆
|
||||
agentService.updateAgentById(device.getAgentId(),
|
||||
new AgentUpdateDTO() {
|
||||
{
|
||||
setSummaryMemory(summaryDTO.getSummary());
|
||||
}
|
||||
});
|
||||
|
||||
System.out.println("成功保存会话 " + sessionId + " 的聊天记录总结到智能体 " + device.getAgentId());
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("保存会话 " + sessionId + " 的聊天记录总结时发生错误: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取聊天记录
|
||||
*/
|
||||
private List<AgentChatHistoryDTO> getChatHistoryBySessionId(String sessionId) {
|
||||
try {
|
||||
// 这里需要根据sessionId获取聊天记录
|
||||
// 由于现有接口需要agentId,我们需要先找到关联的agentId
|
||||
String agentId = findAgentIdBySessionId(sessionId);
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return null;
|
||||
}
|
||||
return agentChatHistoryService.getChatHistoryBySessionId(agentId, sessionId);
|
||||
} catch (Exception e) {
|
||||
System.err.println("获取会话 " + sessionId + " 的聊天记录失败: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID查找关联的智能体ID
|
||||
*/
|
||||
private String findAgentIdBySessionId(String sessionId) {
|
||||
try {
|
||||
// 查询该会话的第一条记录获取agentId
|
||||
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.select("agent_id")
|
||||
.eq("session_id", sessionId)
|
||||
.last("LIMIT 1");
|
||||
|
||||
AgentChatHistoryEntity entity = agentChatHistoryService.getOne(wrapper);
|
||||
return entity != null ? entity.getAgentId() : null;
|
||||
} catch (Exception e) {
|
||||
System.err.println("根据会话ID " + sessionId + " 查找智能体ID失败: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从会话中获取智能体ID
|
||||
*/
|
||||
private String getAgentIdFromSession(String sessionId, List<AgentChatHistoryDTO> chatHistory) {
|
||||
// 直接从数据库查询智能体ID
|
||||
return findAgentIdBySessionId(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取有意义的对话内容(只提取用户消息,排除AI回复)
|
||||
*/
|
||||
private List<String> extractMeaningfulMessages(List<AgentChatHistoryDTO> chatHistory) {
|
||||
List<String> meaningfulMessages = new ArrayList<>();
|
||||
|
||||
for (AgentChatHistoryDTO message : chatHistory) {
|
||||
// 只处理用户消息(chatType = 1)
|
||||
if (message.getChatType() != null && message.getChatType() == 1) {
|
||||
String content = extractContentFromMessage(message);
|
||||
if (isMeaningfulMessage(content)) {
|
||||
meaningfulMessages.add(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return meaningfulMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息中提取内容(处理JSON格式)
|
||||
*/
|
||||
private String extractContentFromMessage(AgentChatHistoryDTO message) {
|
||||
String content = message.getContent();
|
||||
if (StringUtils.isBlank(content)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 处理JSON格式内容(与前端ChatHistoryDialog.vue逻辑一致)
|
||||
Matcher matcher = JSON_PATTERN.matcher(content);
|
||||
if (matcher.find()) {
|
||||
String jsonContent = matcher.group();
|
||||
// 简化处理:提取JSON中的文本内容
|
||||
return extractTextFromJson(jsonContent);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JSON中提取文本内容
|
||||
*/
|
||||
private String extractTextFromJson(String jsonContent) {
|
||||
// 简化处理:提取"content"字段的值
|
||||
Pattern contentPattern = Pattern.compile("\"content\"\s*:\s*\"([^\"]*)\"");
|
||||
Matcher matcher = contentPattern.matcher(jsonContent);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return jsonContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为有意义的消息
|
||||
*/
|
||||
private boolean isMeaningfulMessage(String content) {
|
||||
if (StringUtils.isBlank(content)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 排除设备控制信息
|
||||
if (DEVICE_CONTROL_PATTERN.matcher(content).find()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 排除日期天气等无关内容
|
||||
if (WEATHER_PATTERN.matcher(content).find() || DATE_PATTERN.matcher(content).find()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 排除过短的消息
|
||||
return content.length() >= 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息生成总结
|
||||
*/
|
||||
private String generateSummaryFromMessages(List<String> messages, String agentId) {
|
||||
if (messages.isEmpty()) {
|
||||
return "本次对话内容较少,没有需要总结的重要信息。";
|
||||
}
|
||||
|
||||
// 构建完整的对话内容
|
||||
StringBuilder conversation = new StringBuilder();
|
||||
for (int i = 0; i < messages.size(); i++) {
|
||||
conversation.append("消息").append(i + 1).append(": ").append(messages.get(i)).append("\n");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前智能体的历史记忆
|
||||
String historyMemory = getCurrentAgentMemory(agentId);
|
||||
|
||||
// 调用LLM服务进行智能总结,传递agentId以获取正确的模型配置
|
||||
String summary = callJavaLLMForSummaryWithHistory(conversation.toString(), historyMemory, agentId);
|
||||
|
||||
// 应用总结规则:限制最大长度
|
||||
if (summary.length() > MAX_SUMMARY_LENGTH) {
|
||||
summary = summary.substring(0, MAX_SUMMARY_LENGTH) + "...";
|
||||
}
|
||||
|
||||
return summary;
|
||||
} catch (Exception e) {
|
||||
System.err.println("调用Java端LLM服务失败: " + e.getMessage());
|
||||
throw new RuntimeException("LLM服务不可用,无法生成聊天总结");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前智能体的历史记忆
|
||||
*/
|
||||
private String getCurrentAgentMemory(String agentId) {
|
||||
try {
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取智能体信息
|
||||
AgentInfoVO agentInfo = agentService.getAgentById(agentId);
|
||||
if (agentInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回智能体的当前总结记忆
|
||||
return agentInfo.getSummaryMemory();
|
||||
} catch (Exception e) {
|
||||
System.err.println("获取智能体历史记忆失败,agentId: " + agentId + ", 错误: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Java端LLM服务进行智能总结(支持历史记忆合并)
|
||||
*/
|
||||
private String callJavaLLMForSummaryWithHistory(String conversation, String historyMemory, String agentId) {
|
||||
try {
|
||||
// 获取智能体配置,从中提取记忆总结的模型ID
|
||||
String modelId = getMemorySummaryModelId(agentId);
|
||||
|
||||
if (StringUtils.isBlank(modelId)) {
|
||||
System.out.println("未找到记忆总结的LLM模型配置,使用默认LLM服务");
|
||||
return llmService.generateSummaryWithHistory(conversation, historyMemory, null, null);
|
||||
}
|
||||
|
||||
// 使用指定的模型ID调用LLM服务(支持历史记忆合并)
|
||||
String summary = llmService.generateSummaryWithHistory(conversation, historyMemory, null, modelId);
|
||||
|
||||
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Java端LLM服务返回异常: " + summary);
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("调用Java端LLM服务异常,agentId: " + agentId + ", 错误: " + e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Java端LLM服务进行智能总结
|
||||
*/
|
||||
private String callJavaLLMForSummary(String conversation, String agentId) {
|
||||
try {
|
||||
// 获取智能体配置,从中提取记忆总结的模型ID
|
||||
String modelId = getMemorySummaryModelId(agentId);
|
||||
|
||||
if (StringUtils.isBlank(modelId)) {
|
||||
System.out.println("未找到记忆总结的LLM模型配置,使用默认LLM服务");
|
||||
return llmService.generateSummary(conversation);
|
||||
}
|
||||
|
||||
// 使用指定的模型ID调用LLM服务
|
||||
String summary = llmService.generateSummaryWithModel(conversation, modelId);
|
||||
|
||||
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Java端LLM服务返回异常: " + summary);
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("调用Java端LLM服务异常,agentId: " + agentId + ", 错误: " + e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取记忆总结的LLM模型ID
|
||||
*/
|
||||
private String getMemorySummaryModelId(String agentId) {
|
||||
try {
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取智能体信息
|
||||
AgentInfoVO agentInfo = agentService.getAgentById(agentId);
|
||||
if (agentInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取智能体的记忆模型ID
|
||||
String memModelId = agentInfo.getMemModelId();
|
||||
if (StringUtils.isBlank(memModelId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取记忆模型配置
|
||||
ModelConfigEntity memModelConfig = modelConfigService.getModelByIdFromCache(memModelId);
|
||||
if (memModelConfig == null || memModelConfig.getConfigJson() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从记忆模型配置中提取对应的LLM模型ID
|
||||
Map<String, Object> configMap = memModelConfig.getConfigJson();
|
||||
String llmModelId = (String) configMap.get("llm");
|
||||
|
||||
if (StringUtils.isBlank(llmModelId)) {
|
||||
// 如果记忆模型没有配置独立的LLM,则使用智能体的默认LLM模型
|
||||
return agentInfo.getLlmModelId();
|
||||
}
|
||||
|
||||
return llmModelId;
|
||||
} catch (Exception e) {
|
||||
System.err.println("获取记忆总结LLM模型ID失败,agentId: " + agentId + ", 错误: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取设备信息
|
||||
*/
|
||||
private DeviceEntity getDeviceBySessionId(String sessionId) {
|
||||
try {
|
||||
// 查询该会话的第一条记录获取macAddress
|
||||
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.select("mac_address")
|
||||
.eq("session_id", sessionId)
|
||||
.last("LIMIT 1");
|
||||
|
||||
AgentChatHistoryEntity entity = agentChatHistoryService.getOne(wrapper);
|
||||
if (entity != null && StringUtils.isNotBlank(entity.getMacAddress())) {
|
||||
return deviceService.getDeviceByMacAddress(entity.getMacAddress());
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
System.err.println("根据会话ID " + sessionId + " 查找设备信息失败: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.modules.agent.dao.AgentContextProviderDao;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
|
||||
@Service
|
||||
public class AgentContextProviderServiceImpl extends BaseServiceImpl<AgentContextProviderDao, AgentContextProviderEntity> implements AgentContextProviderService {
|
||||
|
||||
@Override
|
||||
public AgentContextProviderEntity getByAgentId(String agentId) {
|
||||
return baseDao.selectOne(new QueryWrapper<AgentContextProviderEntity>().eq("agent_id", agentId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveOrUpdateByAgentId(AgentContextProviderEntity entity) {
|
||||
AgentContextProviderEntity exist = getByAgentId(entity.getAgentId());
|
||||
if (exist != null) {
|
||||
entity.setId(exist.getId());
|
||||
updateById(entity);
|
||||
} else {
|
||||
insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByAgentId(String agentId) {
|
||||
baseDao.delete(new QueryWrapper<AgentContextProviderEntity>().eq("agent_id", agentId));
|
||||
}
|
||||
}
|
||||
+71
-1
@@ -1,16 +1,26 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentPluginMappingMapper;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.knowledge.entity.KnowledgeBaseEntity;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
|
||||
/**
|
||||
* @description 针对表【ai_agent_plugin_mapping(Agent与插件的唯一映射表)】的数据库操作Service实现
|
||||
@@ -18,13 +28,73 @@ import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AgentPluginMappingServiceImpl extends ServiceImpl<AgentPluginMappingMapper, AgentPluginMapping>
|
||||
implements AgentPluginMappingService {
|
||||
private final AgentPluginMappingMapper agentPluginMappingMapper;
|
||||
private final KnowledgeBaseService knowledgeBaseService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
|
||||
@Override
|
||||
public List<AgentPluginMapping> agentPluginParamsByAgentId(String agentId) {
|
||||
return agentPluginMappingMapper.selectPluginsByAgentId(agentId);
|
||||
List<AgentPluginMapping> list = agentPluginMappingMapper.selectPluginsByAgentId(agentId);
|
||||
Map<String, List<KnowledgeBaseEntity>> knowledgeBaseMap = new HashMap<>();
|
||||
Map<String, ModelConfigEntity> modelConfigMap = new HashMap<>();
|
||||
for (int i = list.size() - 1; i >= 0; i--) {
|
||||
AgentPluginMapping mapping = list.get(i);
|
||||
if (StringUtils.isBlank(mapping.getProviderCode())) {
|
||||
// 查询知识库插件参数
|
||||
KnowledgeBaseEntity knowledgeBaseEntity = knowledgeBaseService.selectById(mapping.getPluginId());
|
||||
if (knowledgeBaseEntity == null) {
|
||||
list.remove(i);
|
||||
continue;
|
||||
}
|
||||
ModelConfigEntity modelConfigEntity = modelConfigService
|
||||
.getModelByIdFromCache(knowledgeBaseEntity.getRagModelId());
|
||||
if (modelConfigEntity == null) {
|
||||
list.remove(i);
|
||||
continue;
|
||||
}
|
||||
List<KnowledgeBaseEntity> knowledgeBaseList = knowledgeBaseMap.get(modelConfigEntity.getModelCode());
|
||||
if (knowledgeBaseList == null) {
|
||||
knowledgeBaseList = new ArrayList<>();
|
||||
}
|
||||
modelConfigMap.put(modelConfigEntity.getModelCode(), modelConfigEntity);
|
||||
knowledgeBaseList.add(knowledgeBaseEntity);
|
||||
knowledgeBaseMap.put(modelConfigEntity.getModelCode(), knowledgeBaseList);
|
||||
list.remove(i);
|
||||
}
|
||||
}
|
||||
if (knowledgeBaseMap.size() > 0) {
|
||||
for (String pluginCode : knowledgeBaseMap.keySet()) {
|
||||
List<KnowledgeBaseEntity> knowledgeBaseList = knowledgeBaseMap.get(pluginCode);
|
||||
if (knowledgeBaseList == null || knowledgeBaseList.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AgentPluginMapping agentPluginMapping = new AgentPluginMapping();
|
||||
agentPluginMapping.setAgentId(agentId);
|
||||
agentPluginMapping.setPluginId(pluginCode);
|
||||
agentPluginMapping.setProviderCode("search_from_" + pluginCode);
|
||||
agentPluginMapping.setId(Long.valueOf(list.size() + 1));
|
||||
|
||||
Map<String, Object> paramInfo = new HashMap<>(4);
|
||||
ModelConfigEntity modelConfigEntity = modelConfigMap.get(pluginCode);
|
||||
paramInfo.put("base_url", modelConfigEntity.getConfigJson().getStr("base_url"));
|
||||
paramInfo.put("api_key", modelConfigEntity.getConfigJson().getStr("api_key"));
|
||||
paramInfo.put("dataset_ids",
|
||||
knowledgeBaseList.stream().map(KnowledgeBaseEntity::getDatasetId).toList());
|
||||
|
||||
String description = "如果用户询问与【"
|
||||
+ String.join(",", knowledgeBaseList.stream().map(KnowledgeBaseEntity::getName).toList())
|
||||
+ "】涵盖的主体范围相关内容时应调用本方法,用于查询:" + String.join(",",
|
||||
knowledgeBaseList.stream().map(KnowledgeBaseEntity::getDescription).toList());
|
||||
paramInfo.put("description", description);
|
||||
agentPluginMapping.setParamInfo(JsonUtils.toJsonString(paramInfo));
|
||||
list.add(agentPluginMapping);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+56
-4
@@ -19,6 +19,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
@@ -31,16 +32,19 @@ import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
@@ -60,6 +64,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
@@ -74,7 +79,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
AgentInfoVO agent = agentDao.selectAgentInfoById(id);
|
||||
|
||||
if (agent == null) {
|
||||
throw new RenException("智能体不存在");
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
@@ -83,6 +88,13 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
// 查询上下文源配置
|
||||
AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(id);
|
||||
if (contextProviderEntity != null) {
|
||||
agent.setContextProviders(contextProviderEntity.getContextProviders());
|
||||
}
|
||||
|
||||
// 无需额外查询插件列表,已通过SQL查询出来
|
||||
return agent;
|
||||
}
|
||||
@@ -182,6 +194,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
|
||||
@Override
|
||||
public boolean checkAgentPermission(String agentId, Long userId) {
|
||||
if (SecurityUser.getUser() == null || SecurityUser.getUser().getId() == null) {
|
||||
return false;
|
||||
}
|
||||
// 获取智能体信息
|
||||
AgentEntity agent = getAgentById(agentId);
|
||||
if (agent == null) {
|
||||
@@ -204,7 +219,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
// 先查询现有实体
|
||||
AgentEntity existingEntity = this.getAgentById(agentId);
|
||||
if (existingEntity == null) {
|
||||
throw new RuntimeException("智能体不存在");
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 只更新提供的非空字段
|
||||
@@ -326,9 +341,17 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
|
||||
}
|
||||
|
||||
// 更新上下文源配置
|
||||
if (dto.getContextProviders() != null) {
|
||||
AgentContextProviderEntity contextEntity = new AgentContextProviderEntity();
|
||||
contextEntity.setAgentId(agentId);
|
||||
contextEntity.setContextProviders(dto.getContextProviders());
|
||||
agentContextProviderService.saveOrUpdateByAgentId(contextEntity);
|
||||
}
|
||||
|
||||
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
||||
if (!b) {
|
||||
throw new RenException("LLM大模型和Intent意图识别,选择参数不匹配");
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
}
|
||||
this.updateById(existingEntity);
|
||||
}
|
||||
@@ -369,12 +392,41 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
entity.setLlmModelId(template.getLlmModelId());
|
||||
entity.setVllmModelId(template.getVllmModelId());
|
||||
entity.setTtsModelId(template.getTtsModelId());
|
||||
|
||||
if (template.getTtsVoiceId() == null && template.getTtsModelId() != null) {
|
||||
ModelConfigEntity ttsModel = modelConfigService.selectById(template.getTtsModelId());
|
||||
if (ttsModel != null && ttsModel.getConfigJson() != null) {
|
||||
Map<String, Object> config = ttsModel.getConfigJson();
|
||||
String voice = (String) config.get("voice");
|
||||
if (StringUtils.isBlank(voice)) {
|
||||
voice = (String) config.get("speaker");
|
||||
}
|
||||
VoiceDTO timbre = timbreModelService.getByVoiceCode(template.getTtsModelId(), voice);
|
||||
if (timbre != null) {
|
||||
template.setTtsVoiceId(timbre.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entity.setTtsVoiceId(template.getTtsVoiceId());
|
||||
entity.setMemModelId(template.getMemModelId());
|
||||
entity.setIntentModelId(template.getIntentModelId());
|
||||
entity.setSystemPrompt(template.getSystemPrompt());
|
||||
entity.setSummaryMemory(template.getSummaryMemory());
|
||||
entity.setChatHistoryConf(template.getChatHistoryConf());
|
||||
|
||||
// 根据记忆模型类型设置默认的chatHistoryConf值
|
||||
if (template.getMemModelId() != null) {
|
||||
if (template.getMemModelId().equals("Memory_nomem")) {
|
||||
// 无记忆功能的模型,默认不记录聊天记录
|
||||
entity.setChatHistoryConf(0);
|
||||
} else {
|
||||
// 有记忆功能的模型,默认记录文本和语音
|
||||
entity.setChatHistoryConf(2);
|
||||
}
|
||||
} else {
|
||||
entity.setChatHistoryConf(template.getChatHistoryConf());
|
||||
}
|
||||
|
||||
entity.setLangCode(template.getLangCode());
|
||||
entity.setLanguage(template.getLanguage());
|
||||
}
|
||||
|
||||
+53
@@ -10,6 +10,11 @@ import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
/**
|
||||
* @author chenerlei
|
||||
* @description 针对表【ai_agent_template(智能体配置模板表)】的数据库操作Service实现
|
||||
@@ -40,6 +45,10 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
|
||||
@Override
|
||||
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
||||
modelType = modelType.toUpperCase();
|
||||
// 如果是rag模型,不需要更新
|
||||
if (modelType.equals("RAG")) {
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateWrapper<AgentTemplateEntity> wrapper = new UpdateWrapper<>();
|
||||
switch (modelType) {
|
||||
@@ -69,4 +78,48 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
|
||||
wrapper.ge("sort", 0);
|
||||
update(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reorderTemplatesAfterDelete(Integer deletedSort) {
|
||||
if (deletedSort == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询所有排序值大于被删除模板的记录
|
||||
UpdateWrapper<AgentTemplateEntity> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.gt("sort", deletedSort)
|
||||
.setSql("sort = sort - 1");
|
||||
|
||||
// 执行批量更新,将这些记录的排序值减1
|
||||
this.update(updateWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getNextAvailableSort() {
|
||||
// 查询所有已存在的排序值并按升序排序
|
||||
List<Integer> sortValues = baseMapper.selectList(new QueryWrapper<AgentTemplateEntity>())
|
||||
.stream()
|
||||
.map(AgentTemplateEntity::getSort)
|
||||
.filter(Objects::nonNull)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 如果没有排序值,返回1
|
||||
if (sortValues.isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 寻找最小的未使用序号
|
||||
int expectedSort = 1;
|
||||
for (Integer sort : sortValues) {
|
||||
if (sort > expectedSort) {
|
||||
// 找到空缺的序号
|
||||
return expectedSort;
|
||||
}
|
||||
expectedSort = sort + 1;
|
||||
}
|
||||
|
||||
// 如果没有空缺,返回最大序号+1
|
||||
return expectedSort;
|
||||
}
|
||||
}
|
||||
|
||||
+14
-13
@@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
@@ -78,7 +79,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
||||
// 根据识别出的声纹ID查询对应的用户信息
|
||||
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
|
||||
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
|
||||
throw new RenException("此声音声纹对应的人(" + existingUserName + ")已经注册,请选择其他声音注册");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_ALREADY_REGISTERED, existingUserName);
|
||||
}
|
||||
AgentVoicePrintEntity entity = ConvertUtils.sourceToTarget(dto, AgentVoicePrintEntity.class);
|
||||
// 开启事务
|
||||
@@ -100,7 +101,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
||||
} catch (Exception e) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
log.error("保存声纹错误原因:{}", e.getMessage());
|
||||
throw new RenException("保存声纹错误,请联系管理员");
|
||||
throw new RenException(ErrorCode.VOICE_PRINT_SAVE_ERROR);
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -123,7 +124,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
||||
} catch (Exception e) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
log.error("删除声纹存在错误原因:{}", e.getMessage());
|
||||
throw new RenException("删除声纹出现了错误");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_DELETE_ERROR);
|
||||
}
|
||||
}));
|
||||
// 数据库声纹数据删除成功才继续执行删除声纹服务的数据
|
||||
@@ -179,7 +180,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
||||
// 根据识别出的声纹ID查询对应的用户信息
|
||||
AgentVoicePrintEntity existingVoicePrint = baseMapper.selectById(response.getSpeakerId());
|
||||
String existingUserName = existingVoicePrint != null ? existingVoicePrint.getSourceName() : "未知用户";
|
||||
throw new RenException("此次修改不允许,此声音已经注册为声纹了(" + existingUserName + ")");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_UPDATE_NOT_ALLOWED, existingUserName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -208,7 +209,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
||||
} catch (Exception e) {
|
||||
status.setRollbackOnly(); // 标记事务回滚
|
||||
log.error("修改声纹错误原因:{}", e.getMessage());
|
||||
throw new RenException("修改声纹错误,请联系管理员");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_UPDATE_ADMIN_ERROR);
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -225,7 +226,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
||||
return new URI(voicePrint);
|
||||
} catch (URISyntaxException e) {
|
||||
log.error("路径格式不正确路径:{},\n错误信息:{}", voicePrint, e.getMessage());
|
||||
throw new RuntimeException("声纹接口的地址存在错误,请进入参数管理修改声纹接口地址");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_API_URI_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,13 +271,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
||||
// 判断这个音频是否属于当前智能体
|
||||
boolean b = agentChatHistoryService.isAudioOwnedByAgent(audioId, agentId);
|
||||
if (!b) {
|
||||
throw new RenException("音频数据不属于这个智能体");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_AUDIO_NOT_BELONG_AGENT);
|
||||
}
|
||||
// 获取到音频数据
|
||||
byte[] audio = agentChatAudioService.getAudio(audioId);
|
||||
// 如果音频数据为空的直接报错不进行下去
|
||||
if (audio == null || audio.length == 0) {
|
||||
throw new RenException("音频数据是空的请检查上传数据");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_AUDIO_EMPTY);
|
||||
}
|
||||
// 将字节数组包装为资源,返回
|
||||
return new ByteArrayResource(audio) {
|
||||
@@ -314,13 +315,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
||||
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.error("声纹注册失败,请求路径:{}", requestUrl);
|
||||
throw new RenException("声纹保存失败,请求不成功");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_REGISTER_REQUEST_ERROR);
|
||||
}
|
||||
// 检查响应内容
|
||||
String responseBody = response.getBody();
|
||||
if (responseBody == null || !responseBody.contains("true")) {
|
||||
log.error("声纹注册失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
|
||||
throw new RenException("声纹保存失败,请求处理失败");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_REGISTER_PROCESS_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,13 +345,13 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
||||
String.class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.error("声纹注销失败,请求路径:{}", requestUrl);
|
||||
throw new RenException("声纹注销失败,请求不成功");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_UNREGISTER_REQUEST_ERROR);
|
||||
}
|
||||
// 检查响应内容
|
||||
String responseBody = response.getBody();
|
||||
if (responseBody == null || !responseBody.contains("true")) {
|
||||
log.error("声纹注销失败,请求处理失败内容:{}", responseBody == null ? "空内容" : responseBody);
|
||||
throw new RenException("声纹注销失败,请求处理失败");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_UNREGISTER_PROCESS_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +399,7 @@ public class AgentVoicePrintServiceImpl extends ServiceImpl<AgentVoicePrintDao,
|
||||
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
log.error("声纹识别请求失败,请求路径:{}", requestUrl);
|
||||
throw new RenException("声纹识别失败,请求不成功");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_IDENTIFY_REQUEST_ERROR);
|
||||
}
|
||||
// 检查响应内容
|
||||
String responseBody = response.getBody();
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
|
||||
@@ -21,4 +22,7 @@ public class AgentInfoVO extends AgentEntity
|
||||
@Schema(description = "插件列表Id")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<AgentPluginMapping> functions;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
}
|
||||
|
||||
+65
-8
@@ -20,10 +20,12 @@ import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
@@ -38,6 +40,8 @@ import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@@ -51,6 +55,8 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
private final TimbreService timbreService;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final VoiceCloneService cloneVoiceService;
|
||||
private final AgentVoicePrintDao agentVoicePrintDao;
|
||||
|
||||
@Override
|
||||
@@ -70,7 +76,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
// 查询默认智能体
|
||||
AgentTemplateEntity agent = agentTemplateService.getDefaultTemplate();
|
||||
if (agent == null) {
|
||||
throw new RenException("默认智能体未找到");
|
||||
throw new RenException(ErrorCode.AGENT_TEMPLATE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 构建模块配置
|
||||
@@ -88,6 +94,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
result,
|
||||
isCache);
|
||||
|
||||
@@ -99,6 +106,15 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule) {
|
||||
// 检查是否为管理控制台请求
|
||||
String redisKey = RedisKeys.getTmpRegisterMacKey(macAddress);
|
||||
Object isAdminRequest = redisUtils.get(redisKey);
|
||||
|
||||
if (isAdminRequest != null && "true".equals(isAdminRequest)) {
|
||||
// 管理控制台请求,返回getConfig的结果
|
||||
redisUtils.delete(redisKey); // 使用后清理
|
||||
return (Map<String, Object>) getConfig(true);
|
||||
}
|
||||
// 根据MAC地址查找设备
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||
if (device == null) {
|
||||
@@ -107,13 +123,13 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
if (StringUtils.isNotBlank(cachedCode)) {
|
||||
throw new RenException(ErrorCode.OTA_DEVICE_NEED_BIND, cachedCode);
|
||||
}
|
||||
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device");
|
||||
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 获取智能体信息
|
||||
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
||||
if (agent == null) {
|
||||
throw new RenException("智能体未找到");
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
}
|
||||
// 获取音色信息
|
||||
String voice = null;
|
||||
@@ -124,6 +140,11 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
voice = timbre.getTtsVoice();
|
||||
referenceAudio = timbre.getReferenceAudio();
|
||||
referenceText = timbre.getReferenceText();
|
||||
} else {
|
||||
VoiceCloneEntity voice_print = cloneVoiceService.selectById(agent.getTtsVoiceId());
|
||||
if (voice_print != null) {
|
||||
voice = voice_print.getVoiceId();
|
||||
}
|
||||
}
|
||||
// 构建返回数据
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
@@ -169,6 +190,13 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/");
|
||||
result.put("mcp_endpoint", mcpEndpoint);
|
||||
}
|
||||
|
||||
// 获取上下文源配置
|
||||
AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(agent.getId());
|
||||
if (contextProviderEntity != null && contextProviderEntity.getContextProviders() != null && !contextProviderEntity.getContextProviders().isEmpty()) {
|
||||
result.put("context_providers", contextProviderEntity.getContextProviders());
|
||||
}
|
||||
|
||||
// 获取声纹信息
|
||||
buildVoiceprintConfig(agent.getId(), result);
|
||||
|
||||
@@ -187,6 +215,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
agent.getTtsModelId(),
|
||||
agent.getMemModelId(),
|
||||
agent.getIntentModelId(),
|
||||
null,
|
||||
result,
|
||||
true);
|
||||
|
||||
@@ -299,6 +328,20 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
voiceprintConfig.put("url", voiceprintUrl);
|
||||
voiceprintConfig.put("speakers", speakers);
|
||||
|
||||
// 获取声纹识别相似度阈值,默认0.4
|
||||
String thresholdStr = sysParamsService.getValue("server.voiceprint_similarity_threshold", true);
|
||||
if (StringUtils.isNotBlank(thresholdStr) && !"null".equals(thresholdStr)) {
|
||||
try {
|
||||
double threshold = Double.parseDouble(thresholdStr);
|
||||
voiceprintConfig.put("similarity_threshold", threshold);
|
||||
} catch (NumberFormatException e) {
|
||||
// 如果解析失败,使用默认值0.4
|
||||
voiceprintConfig.put("similarity_threshold", 0.4);
|
||||
}
|
||||
} else {
|
||||
voiceprintConfig.put("similarity_threshold", 0.4);
|
||||
}
|
||||
|
||||
result.put("voiceprint", voiceprintConfig);
|
||||
} catch (Exception e) {
|
||||
// 声纹配置获取失败时不影响其他功能
|
||||
@@ -349,12 +392,14 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
String ttsModelId,
|
||||
String memModelId,
|
||||
String intentModelId,
|
||||
String ragModelId,
|
||||
Map<String, Object> result,
|
||||
boolean isCache) {
|
||||
Map<String, String> selectedModule = new HashMap<>();
|
||||
|
||||
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM" };
|
||||
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId };
|
||||
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM", "VLLM", "RAG" };
|
||||
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId, vllmModelId,
|
||||
ragModelId };
|
||||
String intentLLMModelId = null;
|
||||
String memLocalShortLLMModelId = null;
|
||||
|
||||
@@ -362,7 +407,8 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
if (modelIds[i] == null) {
|
||||
continue;
|
||||
}
|
||||
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache);
|
||||
// 关键:第三个参数传false,确保获取原始密钥
|
||||
ModelConfigEntity model = modelConfigService.getModelByIdFromCache(modelIds[i]);
|
||||
if (model == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -377,6 +423,15 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
|
||||
if (referenceText != null)
|
||||
((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText);
|
||||
|
||||
// 火山引擎声音克隆需要替换resource_id
|
||||
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
|
||||
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(map.get("type"))) {
|
||||
// 如果voice是”S_“开头的,使用seed-icl-1.0
|
||||
if (voice != null && voice.startsWith("S_")) {
|
||||
map.put("resource_id", "seed-icl-1.0");
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果是Intent类型,且type=intent_llm,则给他添加附加模型
|
||||
if ("Intent".equals(modelTypes[i])) {
|
||||
@@ -410,14 +465,16 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
if ("LLM".equals(modelTypes[i])) {
|
||||
if (StringUtils.isNotBlank(intentLLMModelId)) {
|
||||
if (!typeConfig.containsKey(intentLLMModelId)) {
|
||||
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache);
|
||||
// 修改这里:添加isMaskSensitive=false参数
|
||||
ModelConfigEntity intentLLM = modelConfigService.getModelByIdFromCache(intentLLMModelId);
|
||||
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(memLocalShortLLMModelId)) {
|
||||
if (!typeConfig.containsKey(memLocalShortLLMModelId)) {
|
||||
// 修改这里:添加isMaskSensitive=false参数
|
||||
ModelConfigEntity memLocalShortLLM = modelConfigService
|
||||
.getModelById(memLocalShortLLMModelId, isCache);
|
||||
.getModelByIdFromCache(memLocalShortLLMModelId);
|
||||
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
|
||||
}
|
||||
}
|
||||
|
||||
+108
-7
@@ -5,6 +5,10 @@ import java.util.List;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -12,32 +16,45 @@ 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.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Tag(name = "设备管理")
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/device")
|
||||
public class DeviceController {
|
||||
private final DeviceService deviceService;
|
||||
|
||||
private final RedisUtils redisUtils;
|
||||
private final SysParamsService sysParamsService;
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService,
|
||||
RestTemplate restTemplate, ObjectMapper objectMapper) {
|
||||
this.deviceService = deviceService;
|
||||
this.redisUtils = redisUtils;
|
||||
this.sysParamsService = sysParamsService;
|
||||
this.restTemplate = restTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@PostMapping("/bind/{agentId}/{deviceCode}")
|
||||
@Operation(summary = "绑定设备")
|
||||
@@ -52,13 +69,15 @@ public class DeviceController {
|
||||
public Result<String> registerDevice(@RequestBody DeviceRegisterDTO deviceRegisterDTO) {
|
||||
String macAddress = deviceRegisterDTO.getMacAddress();
|
||||
if (StringUtils.isBlank(macAddress)) {
|
||||
return new Result<String>().error(ErrorCode.NOT_NULL, "mac地址不能为空");
|
||||
return new Result<String>().error(ErrorCode.MCA_NOT_NULL);
|
||||
}
|
||||
// 生成六位验证码
|
||||
String code = String.valueOf(Math.random()).substring(2, 8);
|
||||
String key = RedisKeys.getDeviceCaptchaKey(code);
|
||||
String code;
|
||||
String key;
|
||||
String existsMac = null;
|
||||
do {
|
||||
code = String.valueOf(Math.random()).substring(2, 8);
|
||||
key = RedisKeys.getDeviceCaptchaKey(code);
|
||||
existsMac = (String) redisUtils.get(key);
|
||||
} while (StringUtils.isNotBlank(existsMac));
|
||||
|
||||
@@ -75,6 +94,88 @@ public class DeviceController {
|
||||
return new Result<List<DeviceEntity>>().ok(devices);
|
||||
}
|
||||
|
||||
@PostMapping("/bind/{agentId}")
|
||||
@Operation(summary = "设备在线接口")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
|
||||
try {
|
||||
// 从系统参数中获取MQTT网关地址
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
// 获取当前用户的设备列表
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
|
||||
|
||||
// 构建deviceIds数组
|
||||
java.util.List<String> deviceIds = new java.util.ArrayList<>();
|
||||
for (DeviceEntity device : devices) {
|
||||
String macAddress = device.getMacAddress() != null ? device.getMacAddress() : "unknown";
|
||||
String groupId = device.getBoard() != null ? device.getBoard() : "GID_default";
|
||||
|
||||
// 替换冒号为下划线
|
||||
groupId = groupId.replace(":", "_");
|
||||
macAddress = macAddress.replace(":", "_");
|
||||
|
||||
// 构建mqtt客户端ID格式:groupId@@@macAddress@@@macAddress
|
||||
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
|
||||
deviceIds.add(mqttClientId);
|
||||
}
|
||||
|
||||
// 构建完整的URL
|
||||
String url = "http://" + mqttGatewayUrl + "/api/devices/status";
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
// 生成Bearer令牌
|
||||
String token = generateBearerToken();
|
||||
if (token == null) {
|
||||
return new Result<String>().error("令牌生成失败");
|
||||
}
|
||||
headers.set("Authorization", "Bearer " + token);
|
||||
|
||||
// 构建请求体JSON
|
||||
String jsonBody = "{\"clientIds\":" + objectMapper.writeValueAsString(deviceIds) + "}";
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(jsonBody, headers);
|
||||
|
||||
// 发送POST请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
|
||||
|
||||
// 返回响应
|
||||
return new Result<String>().ok(response.getBody());
|
||||
} catch (Exception e) {
|
||||
return new Result<String>().error("转发请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String generateBearerToken() {
|
||||
try {
|
||||
// 获取当前日期,格式为yyyy-MM-dd
|
||||
String dateStr = java.time.LocalDate.now()
|
||||
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||
|
||||
// 获取MQTT签名密钥
|
||||
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
|
||||
if (StringUtils.isBlank(signatureKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 将日期字符串与MQTT_SIGNATURE_KEY连接
|
||||
String tokenContent = dateStr + signatureKey;
|
||||
|
||||
// 对连接后的字符串进行SHA256哈希计算
|
||||
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
|
||||
|
||||
return token;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/unbind")
|
||||
@Operation(summary = "解绑设备")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
|
||||
@@ -77,6 +77,10 @@ public class OTAController {
|
||||
@GetMapping
|
||||
@Hidden
|
||||
public ResponseEntity<String> getOTA() {
|
||||
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, false);
|
||||
if (StringUtils.isBlank(mqttUdpConfig)) {
|
||||
return ResponseEntity.ok("OTA接口不正常,缺少mqtt_gateway地址,请登录智控台,在参数管理找到【server.mqtt_gateway】配置");
|
||||
}
|
||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
|
||||
@@ -19,10 +19,13 @@ public class DeviceReportRespDTO {
|
||||
|
||||
@Schema(description = "固件版本信息")
|
||||
private Firmware firmware;
|
||||
|
||||
|
||||
@Schema(description = "WebSocket配置")
|
||||
private Websocket websocket;
|
||||
|
||||
@Schema(description = "MQTT Gateway配置")
|
||||
private MQTT mqtt;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Firmware {
|
||||
@@ -63,11 +66,30 @@ public class DeviceReportRespDTO {
|
||||
@Schema(description = "时区偏移量,单位为分钟")
|
||||
private Integer timezone_offset;
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Websocket {
|
||||
@Schema(description = "WebSocket服务器地址")
|
||||
private String url;
|
||||
@Schema(description = "WebSocket 认证 token")
|
||||
private String token;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class MQTT {
|
||||
@Schema(description = "MQTT 配置网址")
|
||||
private String endpoint;
|
||||
@Schema(description = "MQTT 客户端唯一标识符")
|
||||
private String client_id;
|
||||
@Schema(description = "MQTT 认证用户名")
|
||||
private String username;
|
||||
@Schema(description = "MQTT 认证密码")
|
||||
private String password;
|
||||
@Schema(description = "ESP32 发布消息的主题")
|
||||
private String publish_topic;
|
||||
@Schema(description = "ESP32 订阅的主题")
|
||||
private String subscribe_topic;
|
||||
}
|
||||
}
|
||||
@@ -98,4 +98,14 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
*/
|
||||
void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion);
|
||||
|
||||
/**
|
||||
* 生成WebSocket认证token
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
* @param username 用户名(通常为deviceId)
|
||||
* @return 认证token字符串
|
||||
* @throws Exception 生成token时的异常
|
||||
*/
|
||||
String generateWebSocketToken(String clientId, String username) throws Exception;
|
||||
|
||||
}
|
||||
+169
-8
@@ -1,6 +1,10 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -8,6 +12,9 @@ import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.aop.framework.AopContext;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
@@ -24,6 +31,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
@@ -33,6 +41,7 @@ import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.modules.device.dao.DeviceDao;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
@@ -44,7 +53,6 @@ import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -78,27 +86,27 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
@Override
|
||||
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||
if (StringUtils.isBlank(activationCode)) {
|
||||
throw new RenException("激活码不能为空");
|
||||
throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY);
|
||||
}
|
||||
String deviceKey = "ota:activation:code:" + activationCode;
|
||||
Object cacheDeviceId = redisUtils.get(deviceKey);
|
||||
if (cacheDeviceId == null) {
|
||||
throw new RenException("激活码错误");
|
||||
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||
}
|
||||
String deviceId = (String) cacheDeviceId;
|
||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
||||
if (cacheMap == null) {
|
||||
throw new RenException("激活码错误");
|
||||
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||
}
|
||||
String cachedCode = (String) cacheMap.get("activation_code");
|
||||
if (!activationCode.equals(cachedCode)) {
|
||||
throw new RenException("激活码错误");
|
||||
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||
}
|
||||
// 检查设备有没有被激活
|
||||
if (selectById(deviceId) != null) {
|
||||
throw new RenException("设备已激活");
|
||||
throw new RenException(ErrorCode.DEVICE_ALREADY_ACTIVATED);
|
||||
}
|
||||
|
||||
String macAddress = (String) cacheMap.get("mac_address");
|
||||
@@ -106,7 +114,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
String appVersion = (String) cacheMap.get("app_version");
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
if (user.getId() == null) {
|
||||
throw new RenException("用户未登录");
|
||||
throw new RenException(ErrorCode.USER_NOT_LOGIN);
|
||||
}
|
||||
|
||||
Date currentTime = new Date();
|
||||
@@ -128,6 +136,10 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
// 清理redis缓存
|
||||
redisUtils.delete(cacheDeviceKey);
|
||||
redisUtils.delete(deviceKey);
|
||||
|
||||
// 添加:清除智能体设备数量缓存
|
||||
redisUtils.delete(RedisKeys.getAgentDeviceCountById(agentId));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -159,6 +171,22 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
|
||||
// 检查是否启用认证并生成token
|
||||
String authEnabled = sysParamsService.getValue(Constant.SERVER_AUTH_ENABLED, true);
|
||||
if ("true".equalsIgnoreCase(authEnabled)) {
|
||||
try {
|
||||
// 生成token
|
||||
String token = generateWebSocketToken(clientId, macAddress);
|
||||
websocket.setToken(token);
|
||||
} catch (Exception e) {
|
||||
log.error("生成WebSocket token失败: {}", e.getMessage());
|
||||
websocket.setToken("");
|
||||
}
|
||||
} else {
|
||||
websocket.setToken("");
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||
@@ -176,6 +204,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
response.setWebsocket(websocket);
|
||||
|
||||
// 添加MQTT UDP配置
|
||||
// 从系统参数获取MQTT Gateway地址,仅在配置有效时使用
|
||||
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, true);
|
||||
if (mqttUdpConfig != null && !mqttUdpConfig.equals("null") && !mqttUdpConfig.isEmpty()) {
|
||||
try {
|
||||
String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard()
|
||||
: "GID_default";
|
||||
DeviceReportRespDTO.MQTT mqtt = buildMqttConfig(macAddress, groupId);
|
||||
if (mqtt != null) {
|
||||
mqtt.setEndpoint(mqttUdpConfig);
|
||||
response.setMqtt(mqtt);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("生成MQTT配置失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceById != null) {
|
||||
// 如果设备存在,则异步更新上次连接时间和版本信息
|
||||
String appVersion = deviceReport.getApplication() != null ? deviceReport.getApplication().getVersion()
|
||||
@@ -202,6 +247,16 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
@Override
|
||||
public void unbindDevice(Long userId, String deviceId) {
|
||||
// 先查询设备信息,获取agentId
|
||||
DeviceEntity device = baseDao.selectById(deviceId);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isNotBlank(device.getAgentId())) {
|
||||
// 清除智能体设备数量缓存
|
||||
redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId()));
|
||||
}
|
||||
|
||||
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
wrapper.eq("id", deviceId);
|
||||
@@ -419,7 +474,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
wrapper.eq("mac_address", dto.getMacAddress());
|
||||
DeviceEntity exist = baseDao.selectOne(wrapper);
|
||||
if (exist != null) {
|
||||
throw new RenException("该Mac地址已存在");
|
||||
throw new RenException(ErrorCode.MAC_ADDRESS_ALREADY_EXISTS);
|
||||
}
|
||||
Date now = new Date();
|
||||
DeviceEntity entity = new DeviceEntity();
|
||||
@@ -436,5 +491,111 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
entity.setUpdater(userId);
|
||||
entity.setAutoUpdate(1);
|
||||
baseDao.insert(entity);
|
||||
|
||||
// 添加:清除智能体设备数量缓存
|
||||
redisUtils.delete(RedisKeys.getAgentDeviceCountById(dto.getAgentId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MQTT密码签名
|
||||
*
|
||||
* @param content 签名内容 (clientId + '|' + username)
|
||||
* @param secretKey 密钥
|
||||
* @return Base64编码的HMAC-SHA256签名
|
||||
*/
|
||||
private String generatePasswordSignature(String content, String secretKey) throws Exception {
|
||||
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||
hmac.init(keySpec);
|
||||
byte[] signature = hmac.doFinal(content.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成WebSocket认证token 遵循Python端AuthManager的实现逻辑:token = signature.timestamp
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
* @param username 用户名 (通常为deviceId/macAddress)
|
||||
* @return 认证token字符串
|
||||
*/
|
||||
public String generateWebSocketToken(String clientId, String username)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
// 从系统参数获取密钥
|
||||
String secretKey = sysParamsService.getValue(Constant.SERVER_SECRET, false);
|
||||
if (StringUtils.isBlank(secretKey)) {
|
||||
throw new IllegalStateException("WebSocket认证密钥未配置(server.secret)");
|
||||
}
|
||||
|
||||
// 获取当前时间戳(秒)
|
||||
long timestamp = System.currentTimeMillis() / 1000;
|
||||
|
||||
// 构建签名内容: clientId|username|timestamp
|
||||
String content = String.format("%s|%s|%d", clientId, username, timestamp);
|
||||
|
||||
// 生成HMAC-SHA256签名
|
||||
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||
hmac.init(keySpec);
|
||||
byte[] signature = hmac.doFinal(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Base64 URL-safe编码签名(去除填充符=)
|
||||
String signatureBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(signature);
|
||||
|
||||
// 返回格式: signature.timestamp
|
||||
return String.format("%s.%d", signatureBase64, timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建MQTT配置信息
|
||||
*
|
||||
* @param macAddress MAC地址
|
||||
* @param groupId 分组ID
|
||||
* @return MQTT配置对象
|
||||
*/
|
||||
private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String groupId)
|
||||
throws Exception {
|
||||
// 从环境变量或系统参数获取签名密钥
|
||||
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
|
||||
if (StringUtils.isBlank(signatureKey)) {
|
||||
log.warn("缺少MQTT_SIGNATURE_KEY,跳过MQTT配置生成");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建客户端ID格式:groupId@@@macAddress@@@uuid
|
||||
String groupIdSafeStr = groupId.replace(":", "_");
|
||||
String deviceIdSafeStr = macAddress.replace(":", "_");
|
||||
String mqttClientId = String.format("%s@@@%s@@@%s", groupIdSafeStr, deviceIdSafeStr, deviceIdSafeStr);
|
||||
|
||||
// 构建用户数据(包含IP等信息)
|
||||
Map<String, String> userData = new HashMap<>();
|
||||
// 尝试获取客户端IP
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
|
||||
.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
String clientIp = request.getRemoteAddr();
|
||||
userData.put("ip", clientIp);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
userData.put("ip", "unknown");
|
||||
}
|
||||
|
||||
// 将用户数据编码为Base64 JSON
|
||||
String userDataJson = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(userData);
|
||||
String username = Base64.getEncoder().encodeToString(userDataJson.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// 生成密码签名
|
||||
String password = generatePasswordSignature(mqttClientId + "|" + username, signatureKey);
|
||||
|
||||
// 构建MQTT配置
|
||||
DeviceReportRespDTO.MQTT mqtt = new DeviceReportRespDTO.MQTT();
|
||||
mqtt.setClient_id(mqttClientId);
|
||||
mqtt.setUsername(username);
|
||||
mqtt.setPassword(password);
|
||||
mqtt.setPublish_topic("device-server");
|
||||
mqtt.setSubscribe_topic("devices/p2p/" + deviceIdSafeStr);
|
||||
|
||||
return mqtt;
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package xiaozhi.modules.knowledge.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import xiaozhi.modules.knowledge.rag.KnowledgeBaseAdapterFactory;
|
||||
|
||||
/**
|
||||
* 知识库配置类
|
||||
* 配置知识库相关的Bean
|
||||
*/
|
||||
@Configuration
|
||||
public class KnowledgeBaseConfig {
|
||||
|
||||
/**
|
||||
* 提供KnowledgeBaseAdapterFactory的Bean实例
|
||||
* @return KnowledgeBaseAdapterFactory实例
|
||||
*/
|
||||
@Bean
|
||||
public KnowledgeBaseAdapterFactory knowledgeBaseAdapterFactory() {
|
||||
return new KnowledgeBaseAdapterFactory();
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package xiaozhi.modules.knowledge.controller;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.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.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/datasets")
|
||||
@Tag(name = "知识库管理")
|
||||
public class KnowledgeBaseController {
|
||||
|
||||
private final KnowledgeBaseService knowledgeBaseService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询知识库列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<PageData<KnowledgeBaseDTO>> getPageList(
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer page_size) {
|
||||
// 获取当前登录用户ID
|
||||
Long currentUserId = SecurityUser.getUserId();
|
||||
|
||||
KnowledgeBaseDTO knowledgeBaseDTO = new KnowledgeBaseDTO();
|
||||
knowledgeBaseDTO.setName(name);
|
||||
knowledgeBaseDTO.setCreator(currentUserId); // 设置创建者ID,用于权限过滤
|
||||
|
||||
PageData<KnowledgeBaseDTO> pageData = knowledgeBaseService.getPageList(knowledgeBaseDTO, page, page_size);
|
||||
return new Result<PageData<KnowledgeBaseDTO>>().ok(pageData);
|
||||
}
|
||||
|
||||
@GetMapping("/{dataset_id}")
|
||||
@Operation(summary = "根据知识库ID获取知识库详情")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<KnowledgeBaseDTO> getByDatasetId(@PathVariable("dataset_id") String datasetId) {
|
||||
// 获取当前登录用户ID
|
||||
Long currentUserId = SecurityUser.getUserId();
|
||||
|
||||
KnowledgeBaseDTO knowledgeBaseDTO = knowledgeBaseService.getByDatasetId(datasetId);
|
||||
|
||||
// 检查权限:用户只能查看自己创建的知识库
|
||||
if (knowledgeBaseDTO.getCreator() == null || !knowledgeBaseDTO.getCreator().equals(currentUserId)) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
|
||||
return new Result<KnowledgeBaseDTO>().ok(knowledgeBaseDTO);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "创建知识库")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<KnowledgeBaseDTO> save(@RequestBody @Validated KnowledgeBaseDTO knowledgeBaseDTO) {
|
||||
KnowledgeBaseDTO resp = knowledgeBaseService.save(knowledgeBaseDTO);
|
||||
return new Result<KnowledgeBaseDTO>().ok(resp);
|
||||
}
|
||||
|
||||
@PutMapping("/{dataset_id}")
|
||||
@Operation(summary = "更新知识库")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<KnowledgeBaseDTO> update(@PathVariable("dataset_id") String datasetId,
|
||||
@RequestBody @Validated KnowledgeBaseDTO knowledgeBaseDTO) {
|
||||
// 获取当前登录用户ID
|
||||
Long currentUserId = SecurityUser.getUserId();
|
||||
|
||||
// 先获取现有知识库信息以检查权限
|
||||
KnowledgeBaseDTO existingKnowledgeBase = knowledgeBaseService.getByDatasetId(datasetId);
|
||||
|
||||
// 检查权限:用户只能更新自己创建的知识库
|
||||
if (existingKnowledgeBase.getCreator() == null || !existingKnowledgeBase.getCreator().equals(currentUserId)) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
|
||||
knowledgeBaseDTO.setDatasetId(datasetId);
|
||||
KnowledgeBaseDTO resp = knowledgeBaseService.update(knowledgeBaseDTO);
|
||||
return new Result<KnowledgeBaseDTO>().ok(resp);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{dataset_id}")
|
||||
@Operation(summary = "删除单个知识库")
|
||||
@Parameter(name = "dataset_id", description = "知识库ID", required = true)
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> delete(@PathVariable("dataset_id") String datasetId) {
|
||||
// 获取当前登录用户ID
|
||||
Long currentUserId = SecurityUser.getUserId();
|
||||
|
||||
// 先获取现有知识库信息以检查权限
|
||||
KnowledgeBaseDTO existingKnowledgeBase = knowledgeBaseService.getByDatasetId(datasetId);
|
||||
|
||||
// 检查权限:用户只能删除自己创建的知识库
|
||||
if (existingKnowledgeBase.getCreator() == null || !existingKnowledgeBase.getCreator().equals(currentUserId)) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
|
||||
knowledgeBaseService.deleteByDatasetId(datasetId);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@DeleteMapping("/batch")
|
||||
@Operation(summary = "批量删除知识库")
|
||||
@Parameter(name = "ids", description = "知识库ID列表,用逗号分隔", required = true)
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> deleteBatch(@RequestParam("ids") String ids) {
|
||||
if (StringUtils.isBlank(ids)) {
|
||||
throw new RenException(ErrorCode.PARAMS_GET_ERROR);
|
||||
}
|
||||
|
||||
// 获取当前登录用户ID
|
||||
Long currentUserId = SecurityUser.getUserId();
|
||||
String[] idArray = ids.split(",");
|
||||
for (String datasetId : idArray) {
|
||||
if (StringUtils.isNotBlank(datasetId)) {
|
||||
// 先获取现有知识库信息以检查权限
|
||||
KnowledgeBaseDTO existingKnowledgeBase = knowledgeBaseService.getByDatasetId(datasetId.trim());
|
||||
|
||||
// 检查权限:用户只能删除自己创建的知识库
|
||||
if (existingKnowledgeBase.getCreator() == null
|
||||
|| !existingKnowledgeBase.getCreator().equals(currentUserId)) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
|
||||
knowledgeBaseService.deleteByDatasetId(datasetId.trim());
|
||||
}
|
||||
}
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@GetMapping("/rag-models")
|
||||
@Operation(summary = "获取RAG模型列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<Map<String, Object>>> getRAGModels() {
|
||||
List<Map<String, Object>> result = knowledgeBaseService.getRAGModels();
|
||||
return new Result<List<Map<String, Object>>>().ok(result);
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
package xiaozhi.modules.knowledge.controller;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
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.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeFilesService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/datasets/{dataset_id}")
|
||||
@Tag(name = "知识库文档管理")
|
||||
public class KnowledgeFilesController {
|
||||
|
||||
private final KnowledgeFilesService knowledgeFilesService;
|
||||
private final KnowledgeBaseService knowledgeBaseService;
|
||||
|
||||
/**
|
||||
* 验证当前用户是否有权限操作指定知识库
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
*/
|
||||
private void validateKnowledgeBasePermission(String datasetId) {
|
||||
// 获取当前登录用户ID
|
||||
Long currentUserId = SecurityUser.getUserId();
|
||||
|
||||
// 获取知识库信息
|
||||
KnowledgeBaseDTO knowledgeBase = knowledgeBaseService.getByDatasetId(datasetId);
|
||||
|
||||
// 检查权限:用户只能操作自己创建的知识库
|
||||
if (knowledgeBase.getCreator() == null || !knowledgeBase.getCreator().equals(currentUserId)) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/documents")
|
||||
@Operation(summary = "分页查询文档列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<PageData<KnowledgeFilesDTO>> getPageList(
|
||||
@PathVariable("dataset_id") String datasetId,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer page_size) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
// 如果指定了状态参数,使用状态查询接口
|
||||
if (status != null) {
|
||||
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageListByStatus(datasetId, status, page, page_size);
|
||||
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
||||
}
|
||||
|
||||
// 否则使用通用查询接口
|
||||
KnowledgeFilesDTO knowledgeFilesDTO = new KnowledgeFilesDTO();
|
||||
knowledgeFilesDTO.setDatasetId(datasetId);
|
||||
knowledgeFilesDTO.setName(name);
|
||||
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageList(knowledgeFilesDTO, page, page_size);
|
||||
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
||||
}
|
||||
|
||||
@GetMapping("/documents/status/{status}")
|
||||
@Operation(summary = "根据状态分页查询文档列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<PageData<KnowledgeFilesDTO>> getPageListByStatus(
|
||||
@PathVariable("dataset_id") String datasetId,
|
||||
@PathVariable("status") Integer status,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer page_size) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageListByStatus(datasetId, status, page, page_size);
|
||||
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
||||
}
|
||||
|
||||
@PostMapping("/documents")
|
||||
@Operation(summary = "上传文档到知识库")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<KnowledgeFilesDTO> uploadDocument(
|
||||
@PathVariable("dataset_id") String datasetId,
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(required = false) String name,
|
||||
@RequestParam(required = false) String chunkMethod,
|
||||
@RequestParam(required = false) String metaFields,
|
||||
@RequestParam(required = false) String parserConfig) {
|
||||
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
KnowledgeFilesDTO resp = knowledgeFilesService.uploadDocument(datasetId, file, name,
|
||||
metaFields != null ? parseJsonMap(metaFields) : null,
|
||||
chunkMethod,
|
||||
parserConfig != null ? parseJsonMap(parserConfig) : null);
|
||||
return new Result<KnowledgeFilesDTO>().ok(resp);
|
||||
}
|
||||
|
||||
@DeleteMapping("/documents/{document_id}")
|
||||
@Operation(summary = "删除单个文档")
|
||||
@Parameter(name = "document_id", description = "文档ID", required = true)
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> delete(@PathVariable("dataset_id") String datasetId,
|
||||
@PathVariable("document_id") String documentId) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
knowledgeFilesService.deleteByDocumentId(documentId, datasetId);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PostMapping("/chunks")
|
||||
@Operation(summary = "解析文档(切块)")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> parseDocuments(@PathVariable("dataset_id") String datasetId,
|
||||
@RequestBody Map<String, List<String>> requestBody) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
List<String> documentIds = requestBody.get("document_ids");
|
||||
if (documentIds == null || documentIds.isEmpty()) {
|
||||
return new Result<Void>().error("document_ids参数不能为空");
|
||||
}
|
||||
|
||||
boolean success = knowledgeFilesService.parseDocuments(datasetId, documentIds);
|
||||
if (success) {
|
||||
return new Result<Void>();
|
||||
} else {
|
||||
return new Result<Void>().error("文档解析失败,文档可能正在处理中");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/documents/{document_id}/chunks")
|
||||
@Operation(summary = "列出指定文档的切片")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Map<String, Object>> listChunks(@PathVariable("dataset_id") String datasetId,
|
||||
@PathVariable("document_id") String documentId,
|
||||
@RequestParam(required = false) String keywords,
|
||||
@RequestParam(required = false, defaultValue = "1") Integer page,
|
||||
@RequestParam(required = false, defaultValue = "1024") Integer page_size,
|
||||
@RequestParam(required = false) String id) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
Map<String, Object> result = knowledgeFilesService.listChunks(datasetId, documentId, keywords, page, page_size,
|
||||
id);
|
||||
return new Result<Map<String, Object>>().ok(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 召回测试
|
||||
*/
|
||||
@PostMapping("/retrieval-test")
|
||||
@Operation(summary = "召回测试")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Map<String, Object>> retrievalTest(@PathVariable("dataset_id") String datasetId,
|
||||
@RequestBody Map<String, Object> params) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
try {
|
||||
// 提取参数
|
||||
String question = (String) params.get("question");
|
||||
if (question == null || question.trim().isEmpty()) {
|
||||
return new Result<Map<String, Object>>().error("问题不能为空");
|
||||
}
|
||||
|
||||
List<String> datasetIds = (List<String>) params.get("dataset_ids");
|
||||
List<String> documentIds = (List<String>) params.get("document_ids");
|
||||
Integer page = (Integer) params.get("page");
|
||||
Integer pageSize = (Integer) params.get("page_size");
|
||||
Float similarityThreshold = (Float) params.get("similarity_threshold");
|
||||
Float vectorSimilarityWeight = (Float) params.get("vector_similarity_weight");
|
||||
Integer topK = (Integer) params.get("top_k");
|
||||
String rerankId = (String) params.get("rerank_id");
|
||||
Boolean keyword = (Boolean) params.get("keyword");
|
||||
Boolean highlight = (Boolean) params.get("highlight");
|
||||
List<String> crossLanguages = (List<String>) params.get("cross_languages");
|
||||
Map<String, Object> metadataCondition = (Map<String, Object>) params.get("metadata_condition");
|
||||
|
||||
// 如果未指定数据集ID,使用当前数据集
|
||||
if (datasetIds == null || datasetIds.isEmpty()) {
|
||||
datasetIds = java.util.Arrays.asList(datasetId);
|
||||
}
|
||||
|
||||
Map<String, Object> result = knowledgeFilesService.retrievalTest(
|
||||
question, datasetIds, documentIds, page, pageSize, similarityThreshold,
|
||||
vectorSimilarityWeight, topK, rerankId, keyword, highlight, crossLanguages, metadataCondition);
|
||||
|
||||
return new Result<Map<String, Object>>().ok(result);
|
||||
} catch (Exception e) {
|
||||
return new Result<Map<String, Object>>().error("召回测试失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 解析JSON字符串为Map对象
|
||||
*/
|
||||
private Map<String, Object> parseJsonMap(String jsonString) {
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
return objectMapper.readValue(jsonString, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("解析JSON字符串失败: " + jsonString, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package xiaozhi.modules.knowledge.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.knowledge.entity.KnowledgeBaseEntity;
|
||||
|
||||
/**
|
||||
* 知识库知识库
|
||||
*/
|
||||
@Mapper
|
||||
public interface KnowledgeBaseDao extends BaseDao<KnowledgeBaseEntity> {
|
||||
|
||||
/**
|
||||
* 根据知识库ID删除相关的插件映射记录
|
||||
*
|
||||
* @param knowledgeBaseId 知识库ID
|
||||
*/
|
||||
void deletePluginMappingByKnowledgeBaseId(@Param("knowledgeBaseId") String knowledgeBaseId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package xiaozhi.modules.knowledge.dto;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "知识库知识库")
|
||||
public class KnowledgeBaseDTO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "唯一标识")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "知识库ID")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "RAG模型配置ID")
|
||||
private String ragModelId;
|
||||
|
||||
@Schema(description = "知识库名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "知识库描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "状态(0:禁用 1:启用)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
|
||||
@Schema(description = "文档数量")
|
||||
private Integer documentCount;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package xiaozhi.modules.knowledge.dto;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "知识库文档")
|
||||
public class KnowledgeFilesDTO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Schema(description = "唯一标识")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "文档ID")
|
||||
private String documentId;
|
||||
|
||||
@Schema(description = "知识库ID")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "文档名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "文档类型")
|
||||
private String fileType;
|
||||
|
||||
@Schema(description = "文件大小(字节)")
|
||||
private Long fileSize;
|
||||
|
||||
@Schema(description = "文件路径")
|
||||
private String filePath;
|
||||
|
||||
@Schema(description = "元数据字段")
|
||||
private Map<String, Object> metaFields;
|
||||
|
||||
@Schema(description = "分块方法")
|
||||
private String chunkMethod;
|
||||
|
||||
@Schema(description = "解析器配置")
|
||||
private Map<String, Object> parserConfig;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "文档解析状态")
|
||||
private String run;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
|
||||
// 文档解析状态常量定义
|
||||
private static final Integer STATUS_UNSTART = 0;
|
||||
private static final Integer STATUS_RUNNING = 1;
|
||||
private static final Integer STATUS_CANCEL = 2;
|
||||
private static final Integer STATUS_DONE = 3;
|
||||
private static final Integer STATUS_FAIL = 4;
|
||||
|
||||
/**
|
||||
* 获取文档解析状态码(基于run字段转换)
|
||||
*/
|
||||
public Integer getParseStatusCode() {
|
||||
if (run == null) {
|
||||
return STATUS_UNSTART;
|
||||
}
|
||||
|
||||
// RAGFlow根据run字段的值直接映射到对应的状态码
|
||||
switch (run.toUpperCase()) {
|
||||
case "RUNNING":
|
||||
return STATUS_RUNNING;
|
||||
case "CANCEL":
|
||||
return STATUS_CANCEL;
|
||||
case "DONE":
|
||||
return STATUS_DONE;
|
||||
case "FAIL":
|
||||
return STATUS_FAIL;
|
||||
case "UNSTART":
|
||||
default:
|
||||
return STATUS_UNSTART;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package xiaozhi.modules.knowledge.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName(value = "ai_rag_dataset", autoResultMap = true)
|
||||
@Schema(description = "知识库知识库表")
|
||||
public class KnowledgeBaseEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "唯一标识")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "知识库ID")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "RAG模型配置ID")
|
||||
private String ragModelId;
|
||||
|
||||
@Schema(description = "知识库名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "知识库描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "状态(0:禁用 1:启用)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@TableField(fill = FieldFill.UPDATE)
|
||||
private Date updatedAt;
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package xiaozhi.modules.knowledge.rag;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
|
||||
|
||||
/**
|
||||
* 知识库API适配器抽象基类
|
||||
* 定义通用的知识库操作接口,支持多种后端API实现
|
||||
*/
|
||||
public abstract class KnowledgeBaseAdapter {
|
||||
|
||||
/**
|
||||
* 获取适配器类型标识
|
||||
*
|
||||
* @return 适配器类型(如:ragflow, milvus, pinecone等)
|
||||
*/
|
||||
public abstract String getAdapterType();
|
||||
|
||||
/**
|
||||
* 初始化适配器配置
|
||||
*
|
||||
* @param config 配置参数
|
||||
*/
|
||||
public abstract void initialize(Map<String, Object> config);
|
||||
|
||||
/**
|
||||
* 验证配置是否有效
|
||||
*
|
||||
* @param config 配置参数
|
||||
* @return 验证结果
|
||||
*/
|
||||
public abstract boolean validateConfig(Map<String, Object> config);
|
||||
|
||||
/**
|
||||
* 分页查询文档列表
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param queryParams 查询参数
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @return 分页数据
|
||||
*/
|
||||
public abstract PageData<KnowledgeFilesDTO> getDocumentList(String datasetId,
|
||||
Map<String, Object> queryParams,
|
||||
Integer page,
|
||||
Integer limit);
|
||||
|
||||
/**
|
||||
* 根据文档ID获取文档详情
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @return 文档详情
|
||||
*/
|
||||
public abstract KnowledgeFilesDTO getDocumentById(String datasetId, String documentId);
|
||||
|
||||
/**
|
||||
* 上传文档到知识库
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param file 上传的文件
|
||||
* @param name 文档名称
|
||||
* @param metaFields 元数据字段
|
||||
* @param chunkMethod 分块方法
|
||||
* @param parserConfig 解析器配置
|
||||
* @return 上传的文档信息
|
||||
*/
|
||||
public abstract KnowledgeFilesDTO uploadDocument(String datasetId,
|
||||
MultipartFile file,
|
||||
String name,
|
||||
Map<String, Object> metaFields,
|
||||
String chunkMethod,
|
||||
Map<String, Object> parserConfig);
|
||||
|
||||
/**
|
||||
* 根据状态分页查询文档列表
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param status 文档解析状态
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @return 分页数据
|
||||
*/
|
||||
public abstract PageData<KnowledgeFilesDTO> getDocumentListByStatus(String datasetId,
|
||||
Integer status,
|
||||
Integer page,
|
||||
Integer limit);
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentId 文档ID
|
||||
*/
|
||||
public abstract void deleteDocument(String datasetId, String documentId);
|
||||
|
||||
/**
|
||||
* 解析文档(切块)
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentIds 文档ID列表
|
||||
* @return 解析结果
|
||||
*/
|
||||
public abstract boolean parseDocuments(String datasetId, List<String> documentIds);
|
||||
|
||||
/**
|
||||
* 列出指定文档的切片
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentId 文档ID
|
||||
* @param keywords 关键词过滤
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param chunkId 切片ID
|
||||
* @return 切片列表信息
|
||||
*/
|
||||
public abstract Map<String, Object> listChunks(String datasetId,
|
||||
String documentId,
|
||||
String keywords,
|
||||
Integer page,
|
||||
Integer pageSize,
|
||||
String chunkId);
|
||||
|
||||
/**
|
||||
* 召回测试 - 从知识库中检索相关切片
|
||||
*
|
||||
* @param question 用户查询
|
||||
* @param datasetIds 数据集ID列表
|
||||
* @param documentIds 文档ID列表
|
||||
* @param retrievalParams 检索参数
|
||||
* @return 召回测试结果
|
||||
*/
|
||||
public abstract Map<String, Object> retrievalTest(String question,
|
||||
List<String> datasetIds,
|
||||
List<String> documentIds,
|
||||
Map<String, Object> retrievalParams);
|
||||
|
||||
/**
|
||||
* 测试连接
|
||||
*
|
||||
* @return 连接测试结果
|
||||
*/
|
||||
public abstract boolean testConnection();
|
||||
|
||||
/**
|
||||
* 获取适配器状态信息
|
||||
*
|
||||
* @return 状态信息
|
||||
*/
|
||||
public abstract Map<String, Object> getStatus();
|
||||
|
||||
/**
|
||||
* 获取支持的配置参数
|
||||
*
|
||||
* @return 配置参数说明
|
||||
*/
|
||||
public abstract Map<String, Object> getSupportedConfig();
|
||||
|
||||
/**
|
||||
* 获取默认配置
|
||||
*
|
||||
* @return 默认配置
|
||||
*/
|
||||
public abstract Map<String, Object> getDefaultConfig();
|
||||
|
||||
/**
|
||||
* 创建数据集
|
||||
*
|
||||
* @param createParams 创建参数
|
||||
* @return 数据集ID
|
||||
*/
|
||||
public abstract String createDataset(Map<String, Object> createParams);
|
||||
|
||||
/**
|
||||
* 更新数据集
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
* @param updateParams 更新参数
|
||||
*/
|
||||
public abstract void updateDataset(String datasetId, Map<String, Object> updateParams);
|
||||
|
||||
/**
|
||||
* 删除数据集
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
*/
|
||||
public abstract void deleteDataset(String datasetId);
|
||||
|
||||
/**
|
||||
* 获取数据集的文档数量
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
* @return 文档数量
|
||||
*/
|
||||
public abstract Integer getDocumentCount(String datasetId);
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package xiaozhi.modules.knowledge.rag;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
|
||||
/**
|
||||
* 知识库适配器工厂类
|
||||
* 负责创建和管理不同类型的知识库API适配器
|
||||
*/
|
||||
@Slf4j
|
||||
public class KnowledgeBaseAdapterFactory {
|
||||
|
||||
// 注册的适配器类型映射
|
||||
private static final Map<String, Class<? extends KnowledgeBaseAdapter>> adapterRegistry = new HashMap<>();
|
||||
|
||||
// 适配器实例缓存
|
||||
private static final Map<String, KnowledgeBaseAdapter> adapterCache = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
// 注册内置适配器类型
|
||||
registerAdapter("ragflow", xiaozhi.modules.knowledge.rag.impl.RAGFlowAdapter.class);
|
||||
// 可以在这里注册更多适配器类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册新的适配器类型
|
||||
*
|
||||
* @param adapterType 适配器类型标识
|
||||
* @param adapterClass 适配器类
|
||||
*/
|
||||
public static void registerAdapter(String adapterType, Class<? extends KnowledgeBaseAdapter> adapterClass) {
|
||||
if (adapterRegistry.containsKey(adapterType)) {
|
||||
log.warn("适配器类型 '{}' 已存在,将被覆盖", adapterType);
|
||||
}
|
||||
adapterRegistry.put(adapterType, adapterClass);
|
||||
log.info("注册适配器类型: {} -> {}", adapterType, adapterClass.getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器实例
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
* @param config 配置参数
|
||||
* @return 适配器实例
|
||||
*/
|
||||
public static KnowledgeBaseAdapter getAdapter(String adapterType, Map<String, Object> config) {
|
||||
String cacheKey = buildCacheKey(adapterType, config);
|
||||
|
||||
// 检查缓存中是否已存在实例
|
||||
if (adapterCache.containsKey(cacheKey)) {
|
||||
log.debug("从缓存获取适配器实例: {}", cacheKey);
|
||||
return adapterCache.get(cacheKey);
|
||||
}
|
||||
|
||||
// 创建新的适配器实例
|
||||
KnowledgeBaseAdapter adapter = createAdapter(adapterType, config);
|
||||
|
||||
// 缓存适配器实例
|
||||
adapterCache.put(cacheKey, adapter);
|
||||
log.info("创建并缓存适配器实例: {}", cacheKey);
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器实例(无配置)
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
* @return 适配器实例
|
||||
*/
|
||||
public static KnowledgeBaseAdapter getAdapter(String adapterType) {
|
||||
return getAdapter(adapterType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的适配器类型
|
||||
*
|
||||
* @return 适配器类型集合
|
||||
*/
|
||||
public static Set<String> getRegisteredAdapterTypes() {
|
||||
return adapterRegistry.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查适配器类型是否已注册
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
* @return 是否已注册
|
||||
*/
|
||||
public static boolean isAdapterTypeRegistered(String adapterType) {
|
||||
return adapterRegistry.containsKey(adapterType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除适配器缓存
|
||||
*/
|
||||
public static void clearCache() {
|
||||
int cacheSize = adapterCache.size();
|
||||
adapterCache.clear();
|
||||
log.info("清除适配器缓存,共清除 {} 个实例", cacheSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除特定适配器类型的缓存
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
*/
|
||||
public static void removeCacheByType(String adapterType) {
|
||||
int removedCount = 0;
|
||||
for (String cacheKey : adapterCache.keySet()) {
|
||||
if (cacheKey.startsWith(adapterType + "@")) {
|
||||
adapterCache.remove(cacheKey);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
log.info("移除适配器类型 '{}' 的缓存,共移除 {} 个实例", adapterType, removedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器工厂状态信息
|
||||
*
|
||||
* @return 状态信息
|
||||
*/
|
||||
public static Map<String, Object> getFactoryStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
status.put("registeredAdapterTypes", adapterRegistry.keySet());
|
||||
status.put("cachedAdapterCount", adapterCache.size());
|
||||
status.put("cacheKeys", adapterCache.keySet());
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建适配器实例
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
* @param config 配置参数
|
||||
* @return 适配器实例
|
||||
*/
|
||||
private static KnowledgeBaseAdapter createAdapter(String adapterType, Map<String, Object> config) {
|
||||
if (!adapterRegistry.containsKey(adapterType)) {
|
||||
throw new RenException(ErrorCode.RAG_ADAPTER_TYPE_NOT_SUPPORTED,
|
||||
"不支持的适配器类型: " + adapterType);
|
||||
}
|
||||
|
||||
try {
|
||||
Class<? extends KnowledgeBaseAdapter> adapterClass = adapterRegistry.get(adapterType);
|
||||
KnowledgeBaseAdapter adapter = adapterClass.getDeclaredConstructor().newInstance();
|
||||
|
||||
// 初始化适配器
|
||||
if (config != null) {
|
||||
adapter.initialize(config);
|
||||
|
||||
// 验证配置
|
||||
if (!adapter.validateConfig(config)) {
|
||||
throw new RenException(ErrorCode.RAG_CONFIG_VALIDATION_FAILED,
|
||||
"适配器配置验证失败: " + adapterType);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("成功创建适配器实例: {}", adapterType);
|
||||
return adapter;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("创建适配器实例失败: {}", adapterType, e);
|
||||
throw new RenException(ErrorCode.RAG_ADAPTER_CREATION_FAILED,
|
||||
"创建适配器失败: " + adapterType + ", 错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建缓存键
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
* @param config 配置参数
|
||||
* @return 缓存键
|
||||
*/
|
||||
private static String buildCacheKey(String adapterType, Map<String, Object> config) {
|
||||
if (config == null || config.isEmpty()) {
|
||||
return adapterType + "@default";
|
||||
}
|
||||
|
||||
// 基于配置参数生成缓存键
|
||||
StringBuilder keyBuilder = new StringBuilder(adapterType + "@");
|
||||
|
||||
// 使用配置的哈希值作为缓存键的一部分
|
||||
int configHash = config.hashCode();
|
||||
keyBuilder.append(configHash);
|
||||
|
||||
return keyBuilder.toString();
|
||||
}
|
||||
}
|
||||
+1155
File diff suppressed because it is too large
Load Diff
+87
@@ -0,0 +1,87 @@
|
||||
package xiaozhi.modules.knowledge.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||
import xiaozhi.modules.knowledge.entity.KnowledgeBaseEntity;
|
||||
|
||||
/**
|
||||
* 知识库知识库服务接口
|
||||
*/
|
||||
public interface KnowledgeBaseService extends BaseService<KnowledgeBaseEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询知识库列表
|
||||
*
|
||||
* @param knowledgeBaseDTO 查询条件
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @return 分页数据
|
||||
*/
|
||||
PageData<KnowledgeBaseDTO> getPageList(KnowledgeBaseDTO knowledgeBaseDTO, Integer page, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据ID获取知识库详情
|
||||
*
|
||||
* @param id 知识库ID
|
||||
* @return 知识库详情
|
||||
*/
|
||||
KnowledgeBaseDTO getById(String id);
|
||||
|
||||
/**
|
||||
* 新增知识库
|
||||
*
|
||||
* @param knowledgeBaseDTO 知识库信息
|
||||
* @return 新增的知识库
|
||||
*/
|
||||
KnowledgeBaseDTO save(KnowledgeBaseDTO knowledgeBaseDTO);
|
||||
|
||||
/**
|
||||
* 更新知识库
|
||||
*
|
||||
* @param knowledgeBaseDTO 知识库信息
|
||||
* @return 更新的知识库
|
||||
*/
|
||||
KnowledgeBaseDTO update(KnowledgeBaseDTO knowledgeBaseDTO);
|
||||
|
||||
/**
|
||||
* 根据知识库ID查询知识库
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @return 知识库详情
|
||||
*/
|
||||
KnowledgeBaseDTO getByDatasetId(String datasetId);
|
||||
|
||||
/**
|
||||
* 根据知识库ID删除知识库
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
*/
|
||||
void deleteByDatasetId(String datasetId);
|
||||
|
||||
/**
|
||||
* 获取RAG配置信息
|
||||
*
|
||||
* @param ragModelId RAG模型配置ID
|
||||
* @return RAG配置信息
|
||||
*/
|
||||
Map<String, Object> getRAGConfig(String ragModelId);
|
||||
|
||||
/**
|
||||
* 根据知识库ID获取对应的RAG配置
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @return RAG配置
|
||||
*/
|
||||
Map<String, Object> getRAGConfigByDatasetId(String datasetId);
|
||||
|
||||
/**
|
||||
* 获取RAG模型列表
|
||||
*
|
||||
* @return RAG模型列表
|
||||
*/
|
||||
List<Map<String, Object>> getRAGModels();
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package xiaozhi.modules.knowledge.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
|
||||
|
||||
/**
|
||||
* 知识库文档服务接口
|
||||
*/
|
||||
public interface KnowledgeFilesService {
|
||||
|
||||
/**
|
||||
* 分页查询文档列表
|
||||
*
|
||||
* @param knowledgeFilesDTO 查询条件
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @return 分页数据
|
||||
*/
|
||||
PageData<KnowledgeFilesDTO> getPageList(KnowledgeFilesDTO knowledgeFilesDTO, Integer page, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据文档ID和知识库ID获取文档详情
|
||||
*
|
||||
* @param documentId 文档ID
|
||||
* @param datasetId 知识库ID
|
||||
* @return 文档详情
|
||||
*/
|
||||
KnowledgeFilesDTO getByDocumentId(String documentId, String datasetId);
|
||||
|
||||
/**
|
||||
* 上传文档到知识库
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param file 上传的文件
|
||||
* @param name 文档名称
|
||||
* @param metaFields 元数据字段
|
||||
* @param chunkMethod 分块方法
|
||||
* @param parserConfig 解析器配置
|
||||
* @return 上传的文档信息
|
||||
*/
|
||||
KnowledgeFilesDTO uploadDocument(String datasetId, MultipartFile file, String name,
|
||||
Map<String, Object> metaFields, String chunkMethod,
|
||||
Map<String, Object> parserConfig);
|
||||
|
||||
/**
|
||||
* 根据状态分页查询文档列表
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param status 文档解析状态(0-未开始,1-进行中,2-已取消,3-已完成,4-失败)
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @return 分页数据
|
||||
*/
|
||||
PageData<KnowledgeFilesDTO> getPageListByStatus(String datasetId, Integer status, Integer page, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据文档ID和知识库ID删除文档
|
||||
*
|
||||
* @param documentId 文档ID
|
||||
* @param datasetId 知识库ID
|
||||
*/
|
||||
void deleteByDocumentId(String documentId, String datasetId);
|
||||
|
||||
/**
|
||||
* 获取RAG配置信息
|
||||
*
|
||||
* @param ragModelId RAG模型配置ID
|
||||
* @return RAG配置信息
|
||||
*/
|
||||
Map<String, Object> getRAGConfig(String ragModelId);
|
||||
|
||||
/**
|
||||
* 解析文档(切块)
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentIds 文档ID列表
|
||||
* @return 解析结果
|
||||
*/
|
||||
boolean parseDocuments(String datasetId, List<String> documentIds);
|
||||
|
||||
/**
|
||||
* 列出指定文档的切片
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentId 文档ID
|
||||
* @param keywords 关键词过滤
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param chunkId 切片ID
|
||||
* @return 切片列表信息
|
||||
*/
|
||||
Map<String, Object> listChunks(String datasetId, String documentId, String keywords,
|
||||
Integer page, Integer pageSize, String chunkId);
|
||||
|
||||
/**
|
||||
* 召回测试 - 从指定数据集或文档中检索相关切片
|
||||
*
|
||||
* @param question 用户查询或查询关键词
|
||||
* @param datasetIds 数据集ID列表
|
||||
* @param documentIds 文档ID列表
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param similarityThreshold 最小相似度阈值
|
||||
* @param vectorSimilarityWeight 向量相似度权重
|
||||
* @param topK 参与向量余弦计算的切片数量
|
||||
* @param rerankId 重排模型ID
|
||||
* @param keyword 是否启用关键词匹配
|
||||
* @param highlight 是否启用高亮显示
|
||||
* @param crossLanguages 跨语言翻译列表
|
||||
* @param metadataCondition 元数据过滤条件
|
||||
* @return 召回测试结果
|
||||
*/
|
||||
Map<String, Object> retrievalTest(String question, List<String> datasetIds, List<String> documentIds,
|
||||
Integer page, Integer pageSize, Float similarityThreshold,
|
||||
Float vectorSimilarityWeight, Integer topK, String rerankId,
|
||||
Boolean keyword, Boolean highlight, List<String> crossLanguages,
|
||||
Map<String, Object> metadataCondition);
|
||||
}
|
||||
+644
@@ -0,0 +1,644 @@
|
||||
package xiaozhi.modules.knowledge.service.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.MessageUtils;
|
||||
import xiaozhi.modules.knowledge.dao.KnowledgeBaseDao;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||
import xiaozhi.modules.knowledge.entity.KnowledgeBaseEntity;
|
||||
import xiaozhi.modules.knowledge.rag.KnowledgeBaseAdapter;
|
||||
import xiaozhi.modules.knowledge.rag.KnowledgeBaseAdapterFactory;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
||||
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao, KnowledgeBaseEntity>
|
||||
implements KnowledgeBaseService {
|
||||
|
||||
private final KnowledgeBaseDao knowledgeBaseDao;
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final ModelConfigDao modelConfigDao;
|
||||
private final RedisUtils redisUtils;
|
||||
private RestTemplate restTemplate = new RestTemplate();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public KnowledgeBaseEntity selectById(Serializable datasetId) {
|
||||
if (datasetId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 先从Redis获取缓存
|
||||
String key = RedisKeys.getKnowledgeBaseCacheKey(datasetId.toString());
|
||||
KnowledgeBaseEntity cachedEntity = (KnowledgeBaseEntity) redisUtils.get(key);
|
||||
if (cachedEntity != null) {
|
||||
return cachedEntity;
|
||||
}
|
||||
|
||||
// 如果缓存中没有,则从数据库获取
|
||||
KnowledgeBaseEntity entity = knowledgeBaseDao.selectById(datasetId);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 存入Redis缓存
|
||||
redisUtils.set(key, entity);
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageData<KnowledgeBaseDTO> getPageList(KnowledgeBaseDTO knowledgeBaseDTO, Integer page, Integer limit) {
|
||||
long curPage = page;
|
||||
long pageSize = limit;
|
||||
Page<KnowledgeBaseEntity> pageInfo = new Page<>(curPage, pageSize);
|
||||
|
||||
QueryWrapper<KnowledgeBaseEntity> queryWrapper = new QueryWrapper<>();
|
||||
|
||||
// 添加查询条件
|
||||
if (knowledgeBaseDTO != null) {
|
||||
queryWrapper.like(StringUtils.isNotBlank(knowledgeBaseDTO.getName()), "name", knowledgeBaseDTO.getName());
|
||||
queryWrapper.eq(knowledgeBaseDTO.getStatus() != null, "status", knowledgeBaseDTO.getStatus());
|
||||
queryWrapper.eq("creator", knowledgeBaseDTO.getCreator());
|
||||
}
|
||||
|
||||
// 添加排序规则:按创建时间降序
|
||||
queryWrapper.orderByDesc("created_at");
|
||||
|
||||
IPage<KnowledgeBaseEntity> knowledgeBaseEntityIPage = knowledgeBaseDao.selectPage(pageInfo, queryWrapper);
|
||||
|
||||
// 获取分页数据
|
||||
PageData<KnowledgeBaseDTO> pageData = getPageData(knowledgeBaseEntityIPage, KnowledgeBaseDTO.class);
|
||||
|
||||
// 为每个知识库获取文档数量
|
||||
if (pageData != null && pageData.getList() != null) {
|
||||
for (KnowledgeBaseDTO knowledgeBase : pageData.getList()) {
|
||||
try {
|
||||
Integer documentCount = getDocumentCountFromRAG(knowledgeBase.getDatasetId(),
|
||||
knowledgeBase.getRagModelId());
|
||||
knowledgeBase.setDocumentCount(documentCount);
|
||||
} catch (Exception e) {
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName() + " - 获取知识库文档数量失败";
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.warn("知识库 {} {}", knowledgeBase.getDatasetId(), errorMessage);
|
||||
knowledgeBase.setDocumentCount(0); // 设置默认值
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pageData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KnowledgeBaseDTO getById(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
throw new RenException(ErrorCode.IDENTIFIER_NOT_NULL);
|
||||
}
|
||||
|
||||
KnowledgeBaseEntity entity = knowledgeBaseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
throw new RenException(ErrorCode.Knowledge_Base_RECORD_NOT_EXISTS);
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, KnowledgeBaseDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KnowledgeBaseDTO save(KnowledgeBaseDTO knowledgeBaseDTO) {
|
||||
if (knowledgeBaseDTO == null) {
|
||||
throw new RenException(ErrorCode.PARAMS_GET_ERROR);
|
||||
}
|
||||
|
||||
// 检查是否存在同名知识库
|
||||
checkDuplicateKnowledgeBaseName(knowledgeBaseDTO, null);
|
||||
|
||||
String datasetId = null;
|
||||
// 调用RAG API创建数据集
|
||||
try {
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(knowledgeBaseDTO.getRagModelId());
|
||||
datasetId = createDatasetInRAG(
|
||||
knowledgeBaseDTO.getName(),
|
||||
knowledgeBaseDTO.getDescription(),
|
||||
ragConfig);
|
||||
} catch (Exception e) {
|
||||
// 如果RAG API调用失败,直接抛出异常
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 验证数据集ID是否已存在
|
||||
KnowledgeBaseEntity existingEntity = knowledgeBaseDao.selectOne(
|
||||
new QueryWrapper<KnowledgeBaseEntity>().eq("dataset_id", datasetId));
|
||||
if (existingEntity != null) {
|
||||
// 如果datasetId已存在,删除RAG中的数据集并抛出异常
|
||||
try {
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(knowledgeBaseDTO.getRagModelId());
|
||||
deleteDatasetInRAG(datasetId, ragConfig);
|
||||
} catch (Exception deleteException) {
|
||||
// 提供更详细的错误信息,包括异常类型和消息
|
||||
String errorMessage = "删除重复datasetId的RAG数据集失败: " + deleteException.getClass().getSimpleName();
|
||||
if (deleteException.getMessage() != null) {
|
||||
errorMessage += " - " + deleteException.getMessage();
|
||||
}
|
||||
log.warn(errorMessage, deleteException);
|
||||
}
|
||||
throw new RenException(ErrorCode.DB_RECORD_EXISTS);
|
||||
}
|
||||
|
||||
// 创建本地实体并保存
|
||||
KnowledgeBaseEntity entity = ConvertUtils.sourceToTarget(knowledgeBaseDTO, KnowledgeBaseEntity.class);
|
||||
entity.setDatasetId(datasetId);
|
||||
knowledgeBaseDao.insert(entity);
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, KnowledgeBaseDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KnowledgeBaseDTO update(KnowledgeBaseDTO knowledgeBaseDTO) {
|
||||
if (knowledgeBaseDTO == null || StringUtils.isBlank(knowledgeBaseDTO.getId())) {
|
||||
throw new RenException(ErrorCode.IDENTIFIER_NOT_NULL);
|
||||
}
|
||||
|
||||
// 检查记录是否存在
|
||||
KnowledgeBaseEntity existingEntity = knowledgeBaseDao.selectById(knowledgeBaseDTO.getId());
|
||||
if (existingEntity == null) {
|
||||
throw new RenException(ErrorCode.Knowledge_Base_RECORD_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 检查是否存在同名知识库(排除当前记录)
|
||||
checkDuplicateKnowledgeBaseName(knowledgeBaseDTO, knowledgeBaseDTO.getId());
|
||||
|
||||
// 验证数据集ID是否与其他记录冲突
|
||||
if (StringUtils.isNotBlank(knowledgeBaseDTO.getDatasetId())) {
|
||||
KnowledgeBaseEntity conflictEntity = knowledgeBaseDao.selectOne(
|
||||
new QueryWrapper<KnowledgeBaseEntity>()
|
||||
.eq("dataset_id", knowledgeBaseDTO.getDatasetId())
|
||||
.ne("id", knowledgeBaseDTO.getId()));
|
||||
if (conflictEntity != null) {
|
||||
throw new RenException(ErrorCode.DB_RECORD_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
boolean needRagValidation = StringUtils.isNotBlank(knowledgeBaseDTO.getDatasetId())
|
||||
&& StringUtils.isNotBlank(knowledgeBaseDTO.getRagModelId());
|
||||
|
||||
if (needRagValidation) {
|
||||
try {
|
||||
// 先校验RAG配置
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(knowledgeBaseDTO.getRagModelId());
|
||||
|
||||
// 调用RAG API更新数据集
|
||||
updateDatasetInRAG(
|
||||
knowledgeBaseDTO.getDatasetId(),
|
||||
knowledgeBaseDTO.getName(),
|
||||
knowledgeBaseDTO.getDescription(),
|
||||
ragConfig);
|
||||
|
||||
log.info("RAG API更新成功,datasetId: {}", knowledgeBaseDTO.getDatasetId());
|
||||
} catch (Exception e) {
|
||||
// 提供更详细的错误信息,包括异常类型和消息
|
||||
String errorMessage = "更新RAG数据集失败: " + e.getClass().getSimpleName();
|
||||
if (e.getMessage() != null) {
|
||||
errorMessage += " - " + e.getMessage();
|
||||
}
|
||||
log.error(errorMessage, e);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
log.warn("datasetId或ragModelId为空,跳过RAG更新");
|
||||
}
|
||||
|
||||
KnowledgeBaseEntity entity = ConvertUtils.sourceToTarget(knowledgeBaseDTO, KnowledgeBaseEntity.class);
|
||||
knowledgeBaseDao.updateById(entity);
|
||||
|
||||
// 删除缓存
|
||||
if (entity.getDatasetId() != null) {
|
||||
redisUtils.delete(RedisKeys.getKnowledgeBaseCacheKey(entity.getId()));
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, KnowledgeBaseDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public KnowledgeBaseDTO getByDatasetId(String datasetId) {
|
||||
if (StringUtils.isBlank(datasetId)) {
|
||||
throw new RenException(ErrorCode.PARAMS_GET_ERROR);
|
||||
}
|
||||
|
||||
KnowledgeBaseEntity entity = knowledgeBaseDao.selectOne(
|
||||
new QueryWrapper<KnowledgeBaseEntity>().eq("dataset_id", datasetId));
|
||||
|
||||
if (entity == null) {
|
||||
throw new RenException(ErrorCode.Knowledge_Base_RECORD_NOT_EXISTS);
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, KnowledgeBaseDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByDatasetId(String datasetId) {
|
||||
if (StringUtils.isBlank(datasetId)) {
|
||||
throw new RenException(ErrorCode.PARAMS_GET_ERROR);
|
||||
}
|
||||
|
||||
log.info("=== 开始通过datasetId删除操作 ===");
|
||||
log.info("删除datasetId: {}", datasetId);
|
||||
|
||||
KnowledgeBaseEntity entity = knowledgeBaseDao.selectOne(
|
||||
new QueryWrapper<KnowledgeBaseEntity>().eq("dataset_id", datasetId));
|
||||
|
||||
if (entity == null) {
|
||||
log.warn("记录不存在,datasetId: {}", datasetId);
|
||||
throw new RenException(ErrorCode.Knowledge_Base_RECORD_NOT_EXISTS);
|
||||
}
|
||||
redisUtils.delete(RedisKeys.getKnowledgeBaseCacheKey(entity.getId()));
|
||||
|
||||
log.info("找到记录: ID={}, datasetId={}, ragModelId={}",
|
||||
entity.getId(), entity.getDatasetId(), entity.getRagModelId());
|
||||
|
||||
// 先调用RAG API删除数据集
|
||||
boolean apiDeleteSuccess = false;
|
||||
if (StringUtils.isNotBlank(entity.getDatasetId()) && StringUtils.isNotBlank(entity.getRagModelId())) {
|
||||
try {
|
||||
log.info("开始调用RAG API删除数据集");
|
||||
// 在删除前进行RAG配置校验
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(entity.getRagModelId());
|
||||
deleteDatasetInRAG(entity.getDatasetId(), ragConfig);
|
||||
log.info("RAG API删除调用完成");
|
||||
apiDeleteSuccess = true;
|
||||
} catch (Exception e) {
|
||||
// 提供更详细的错误信息,包括异常类型和消息
|
||||
String errorMessage = "删除RAG数据集失败: " + e.getClass().getSimpleName();
|
||||
if (e.getMessage() != null) {
|
||||
errorMessage += " - " + e.getMessage();
|
||||
}
|
||||
log.error(errorMessage, e);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
log.warn("datasetId或ragModelId为空,跳过RAG删除");
|
||||
apiDeleteSuccess = true; // 没有RAG数据集,视为成功
|
||||
}
|
||||
|
||||
// API删除成功后再删除本地记录
|
||||
if (apiDeleteSuccess) {
|
||||
log.info("开始删除ai_agent_plugin_mapping表中与知识库ID '{}' 相关的映射记录", entity.getId());
|
||||
|
||||
// 先删除相关的插件映射记录
|
||||
knowledgeBaseDao.deletePluginMappingByKnowledgeBaseId(entity.getId());
|
||||
log.info("插件映射记录删除完成");
|
||||
|
||||
int deleteCount = knowledgeBaseDao.deleteById(entity.getId());
|
||||
log.info("本地数据库删除结果: {}", deleteCount > 0 ? "成功" : "失败");
|
||||
}
|
||||
|
||||
log.info("=== 通过datasetId删除操作结束 ===");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getRAGConfig(String ragModelId) {
|
||||
if (StringUtils.isBlank(ragModelId)) {
|
||||
throw new RenException(ErrorCode.PARAMS_GET_ERROR);
|
||||
}
|
||||
|
||||
// 从缓存获取模型配置
|
||||
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(ragModelId);
|
||||
if (modelConfig == null || modelConfig.getConfigJson() == null) {
|
||||
throw new RenException(ErrorCode.RAG_CONFIG_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 验证是否为RAG类型配置
|
||||
if (!Constant.RAG_CONFIG_TYPE.equals(modelConfig.getModelType().toUpperCase())) {
|
||||
throw new RenException(ErrorCode.RAG_CONFIG_TYPE_ERROR);
|
||||
}
|
||||
|
||||
Map<String, Object> config = modelConfig.getConfigJson();
|
||||
|
||||
// 验证必要的配置参数
|
||||
validateRagConfig(config);
|
||||
|
||||
// 返回配置信息
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getRAGConfigByDatasetId(String datasetId) {
|
||||
if (StringUtils.isBlank(datasetId)) {
|
||||
throw new RenException(ErrorCode.RAG_DATASET_ID_NOT_NULL);
|
||||
}
|
||||
|
||||
// 根据datasetId查询知识库信息
|
||||
KnowledgeBaseDTO knowledgeBase = getByDatasetId(datasetId);
|
||||
if (knowledgeBase == null) {
|
||||
log.warn("未找到datasetId为{}的知识库", datasetId);
|
||||
throw new RenException(ErrorCode.Knowledge_Base_RECORD_NOT_EXISTS);
|
||||
}
|
||||
|
||||
// 如果知识库指定了ragModelId,使用该配置
|
||||
String ragModelId = knowledgeBase.getRagModelId();
|
||||
if (StringUtils.isBlank(ragModelId)) {
|
||||
log.warn("知识库datasetId为{}未配置ragModelId", datasetId);
|
||||
throw new RenException(ErrorCode.RAG_CONFIG_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 获取并返回RAG配置
|
||||
return getRAGConfig(ragModelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getRAGModels() {
|
||||
// 查询RAG类型的模型配置
|
||||
QueryWrapper<ModelConfigEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("model_type", Constant.RAG_CONFIG_TYPE)
|
||||
.eq("is_enabled", 1)
|
||||
.orderByDesc("is_default")
|
||||
.orderByDesc("create_date");
|
||||
|
||||
List<ModelConfigEntity> modelConfigs = modelConfigDao.selectList(queryWrapper);
|
||||
|
||||
List<Map<String, Object>> modelList = new ArrayList<>();
|
||||
for (ModelConfigEntity modelConfig : modelConfigs) {
|
||||
Map<String, Object> modelInfo = new HashMap<>();
|
||||
modelInfo.put("id", modelConfig.getId());
|
||||
modelInfo.put("modelName", modelConfig.getModelName());
|
||||
modelList.add(modelInfo);
|
||||
}
|
||||
return modelList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证RAG配置中是否包含必要的参数
|
||||
*/
|
||||
private void validateRagConfig(Map<String, Object> config) {
|
||||
if (config == null) {
|
||||
throw new RenException(ErrorCode.RAG_CONFIG_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 从配置中提取必要的参数
|
||||
String baseUrl = (String) config.get("base_url");
|
||||
String apiKey = (String) config.get("api_key");
|
||||
|
||||
// 验证base_url是否存在且非空
|
||||
if (StringUtils.isBlank(baseUrl)) {
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR_URL_NULL);
|
||||
}
|
||||
|
||||
// 验证api_key是否存在且非空
|
||||
if (StringUtils.isBlank(apiKey)) {
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR_API_KEY_NULL);
|
||||
}
|
||||
|
||||
// 检查api_key是否包含占位符
|
||||
if (apiKey.contains("你")) {
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR_API_KEY_INVALID);
|
||||
}
|
||||
|
||||
// 验证base_url格式
|
||||
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR_URL_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从RAG配置中提取适配器类型
|
||||
*
|
||||
* @param config RAG配置
|
||||
* @return 适配器类型
|
||||
*/
|
||||
private String extractAdapterType(Map<String, Object> config) {
|
||||
if (config == null) {
|
||||
throw new RenException(ErrorCode.RAG_CONFIG_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 从配置中提取适配器类型
|
||||
String adapterType = (String) config.get("type");
|
||||
|
||||
// 验证适配器类型是否存在且非空
|
||||
if (StringUtils.isBlank(adapterType)) {
|
||||
throw new RenException(ErrorCode.RAG_ADAPTER_TYPE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 验证适配器类型是否已注册
|
||||
if (!KnowledgeBaseAdapterFactory.isAdapterTypeRegistered(adapterType)) {
|
||||
throw new RenException(ErrorCode.RAG_ADAPTER_TYPE_NOT_SUPPORTED,
|
||||
"不支持的适配器类型: " + adapterType);
|
||||
}
|
||||
|
||||
return adapterType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用适配器创建数据集
|
||||
*/
|
||||
private String createDatasetInRAG(String name, String description, Map<String, Object> ragConfig) {
|
||||
log.info("开始使用适配器创建数据集, name: {}", name);
|
||||
|
||||
try {
|
||||
// 从RAG配置中提取适配器类型
|
||||
String adapterType = extractAdapterType(ragConfig);
|
||||
|
||||
// 使用适配器工厂获取适配器实例
|
||||
KnowledgeBaseAdapter adapter = KnowledgeBaseAdapterFactory.getAdapter(adapterType, ragConfig);
|
||||
|
||||
// 构建数据集创建参数
|
||||
Map<String, Object> createParams = new HashMap<>();
|
||||
String username = SecurityUser.getUser().getUsername();
|
||||
createParams.put("name", username + "_" + name);
|
||||
if (StringUtils.isNotBlank(description)) {
|
||||
createParams.put("description", description);
|
||||
}
|
||||
|
||||
// 调用适配器的创建数据集方法
|
||||
String datasetId = adapter.createDataset(createParams);
|
||||
|
||||
log.info("数据集创建成功,datasetId: {}", datasetId);
|
||||
return datasetId;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 直接传递底层适配器的详细错误信息
|
||||
log.error("创建数据集失败", e);
|
||||
if (e instanceof RenException) {
|
||||
throw (RenException) e;
|
||||
}
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用适配器更新数据集
|
||||
*/
|
||||
private void updateDatasetInRAG(String datasetId, String name, String description,
|
||||
Map<String, Object> ragConfig) {
|
||||
log.info("开始使用适配器更新数据集,datasetId: {}, name: {}", datasetId, name);
|
||||
|
||||
try {
|
||||
// 从RAG配置中提取适配器类型
|
||||
String adapterType = extractAdapterType(ragConfig);
|
||||
|
||||
// 使用适配器工厂获取适配器实例
|
||||
KnowledgeBaseAdapter adapter = KnowledgeBaseAdapterFactory.getAdapter(adapterType, ragConfig);
|
||||
|
||||
// 构建数据集更新参数
|
||||
Map<String, Object> updateParams = new HashMap<>();
|
||||
String username = SecurityUser.getUser().getUsername();
|
||||
updateParams.put("name", username + "_" + name);
|
||||
if (StringUtils.isNotBlank(description)) {
|
||||
updateParams.put("description", description);
|
||||
}
|
||||
|
||||
// 调用适配器的更新数据集方法
|
||||
adapter.updateDataset(datasetId, updateParams);
|
||||
|
||||
log.info("数据集更新成功,datasetId: {}", datasetId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 直接传递底层适配器的详细错误信息
|
||||
log.error("更新数据集失败", e);
|
||||
if (e instanceof RenException) {
|
||||
throw (RenException) e;
|
||||
}
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用适配器删除数据集
|
||||
*/
|
||||
private void deleteDatasetInRAG(String datasetId, Map<String, Object> ragConfig) {
|
||||
log.info("开始使用适配器删除数据集,datasetId: {}", datasetId);
|
||||
|
||||
try {
|
||||
// 从RAG配置中提取适配器类型
|
||||
String adapterType = extractAdapterType(ragConfig);
|
||||
|
||||
// 使用适配器工厂获取适配器实例
|
||||
KnowledgeBaseAdapter adapter = KnowledgeBaseAdapterFactory.getAdapter(adapterType, ragConfig);
|
||||
|
||||
// 调用适配器的删除数据集方法
|
||||
adapter.deleteDataset(datasetId);
|
||||
|
||||
log.info("数据集删除成功,datasetId: {}", datasetId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 直接传递底层适配器的详细错误信息
|
||||
log.error("删除数据集失败", e);
|
||||
if (e instanceof RenException) {
|
||||
throw (RenException) e;
|
||||
}
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取RAG配置并验证
|
||||
*/
|
||||
private Map<String, Object> getValidatedRAGConfig(String ragModelId) {
|
||||
if (StringUtils.isBlank(ragModelId)) {
|
||||
throw new RenException(ErrorCode.RAG_MODEL_ID_NOT_NULL);
|
||||
}
|
||||
|
||||
Map<String, Object> ragConfig = getRAGConfig(ragModelId);
|
||||
|
||||
// 验证RAG配置参数
|
||||
validateRagConfig(ragConfig);
|
||||
|
||||
return ragConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在同名知识库
|
||||
*
|
||||
* @param knowledgeBaseDTO 知识库DTO
|
||||
* @param excludeId 排除的ID(更新时使用)
|
||||
*/
|
||||
private void checkDuplicateKnowledgeBaseName(KnowledgeBaseDTO knowledgeBaseDTO, String excludeId) {
|
||||
if (StringUtils.isNotBlank(knowledgeBaseDTO.getName())) {
|
||||
Long currentUserId = SecurityUser.getUserId();
|
||||
QueryWrapper<KnowledgeBaseEntity> queryWrapper = new QueryWrapper<KnowledgeBaseEntity>()
|
||||
.eq("name", knowledgeBaseDTO.getName())
|
||||
.eq("creator", currentUserId);
|
||||
|
||||
// 如果提供了排除ID,则排除该记录
|
||||
if (StringUtils.isNotBlank(excludeId)) {
|
||||
queryWrapper.ne("id", excludeId);
|
||||
}
|
||||
|
||||
long count = knowledgeBaseDao.selectCount(queryWrapper);
|
||||
if (count > 0) {
|
||||
throw new RenException(ErrorCode.KNOWLEDGE_BASE_NAME_EXISTS,
|
||||
MessageUtils.getMessage(ErrorCode.KNOWLEDGE_BASE_NAME_EXISTS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从适配器获取知识库的文档数量
|
||||
*/
|
||||
private Integer getDocumentCountFromRAG(String datasetId, String ragModelId) {
|
||||
if (StringUtils.isBlank(datasetId) || StringUtils.isBlank(ragModelId)) {
|
||||
log.warn("datasetId或ragModelId为空,无法获取文档数量");
|
||||
return 0;
|
||||
}
|
||||
|
||||
log.info("开始获取知识库 {} 的文档数量", datasetId);
|
||||
|
||||
try {
|
||||
// 获取RAG配置
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(ragModelId);
|
||||
|
||||
// 从RAG配置中提取适配器类型
|
||||
String adapterType = extractAdapterType(ragConfig);
|
||||
|
||||
// 使用适配器工厂获取适配器实例
|
||||
KnowledgeBaseAdapter adapter = KnowledgeBaseAdapterFactory.getAdapter(adapterType, ragConfig);
|
||||
|
||||
// 调用适配器的获取文档数量方法
|
||||
Integer documentCount = adapter.getDocumentCount(datasetId);
|
||||
|
||||
log.info("获取知识库 {} 的文档数量成功: {}", datasetId, documentCount);
|
||||
return documentCount;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName() + " - 获取知识库文档数量失败";
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.error(errorMessage, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1367
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
package xiaozhi.modules.llm.service;
|
||||
|
||||
/**
|
||||
* LLM服务接口
|
||||
* 支持多种大模型调用
|
||||
*/
|
||||
public interface LLMService {
|
||||
|
||||
/**
|
||||
* 生成聊天记录总结
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @param promptTemplate 提示词模板
|
||||
* @return 总结结果
|
||||
*/
|
||||
String generateSummary(String conversation, String promptTemplate);
|
||||
|
||||
/**
|
||||
* 生成聊天记录总结(使用默认提示词)
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @return 总结结果
|
||||
*/
|
||||
String generateSummary(String conversation);
|
||||
|
||||
/**
|
||||
* 生成聊天记录总结(指定模型ID)
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @param modelId 模型ID
|
||||
* @return 总结结果
|
||||
*/
|
||||
String generateSummaryWithModel(String conversation, String modelId);
|
||||
|
||||
/**
|
||||
* 生成聊天记录总结(指定模型ID和提示词模板)
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @param promptTemplate 提示词模板
|
||||
* @param modelId 模型ID
|
||||
* @return 总结结果
|
||||
*/
|
||||
String generateSummary(String conversation, String promptTemplate, String modelId);
|
||||
|
||||
/**
|
||||
* 生成聊天记录总结(包含历史记忆合并)
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @param historyMemory 历史记忆
|
||||
* @param promptTemplate 提示词模板
|
||||
* @param modelId 模型ID
|
||||
* @return 总结结果
|
||||
*/
|
||||
String generateSummaryWithHistory(String conversation, String historyMemory, String promptTemplate, String modelId);
|
||||
|
||||
/**
|
||||
* 检查服务是否可用
|
||||
*
|
||||
* @return 是否可用
|
||||
*/
|
||||
boolean isAvailable();
|
||||
|
||||
/**
|
||||
* 检查指定模型的服务是否可用
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
* @return 是否可用
|
||||
*/
|
||||
boolean isAvailable(String modelId);
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
package xiaozhi.modules.llm.service.impl;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.modules.llm.service.LLMService;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
|
||||
/**
|
||||
* OpenAI风格API的LLM服务实现
|
||||
* 支持阿里云、DeepSeek、ChatGLM等兼容OpenAI API的模型
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
|
||||
@Autowired
|
||||
private ModelConfigService modelConfigService;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
private static final String DEFAULT_SUMMARY_PROMPT = "你是一个经验丰富的记忆总结者,擅长将对话内容进行总结摘要,遵循以下规则:\n1、总结用户的重要信息,以便在未来的对话中提供更个性化的服务\n2、不要重复总结,不要遗忘之前记忆,除非原来的记忆超过了1800字,否则不要遗忘、不要压缩用户的历史记忆\n3、用户操控的设备音量、播放音乐、天气、退出、不想对话等和用户本身无关的内容,这些信息不需要加入到总结中\n4、聊天内容中的今天的日期时间、今天的天气情况与用户事件无关的数据,这些信息如果当成记忆存储会影响后续对话,这些信息不需要加入到总结中\n5、不要把设备操控的成果结果和失败结果加入到总结中,也不要把用户的一些废话加入到总结中\n6、不要为了总结而总结,如果用户的聊天没有意义,请返回原来的历史记录也是可以的\n7、只需要返回总结摘要,严格控制在1800字内\n8、不要包含代码、xml,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容\n9、如果提供了历史记忆,请将新对话内容与历史记忆进行智能合并,保留有价值的历史信息,同时添加新的重要信息\n\n历史记忆:\n{history_memory}\n\n新对话内容:\n{conversation}";
|
||||
|
||||
@Override
|
||||
public String generateSummary(String conversation) {
|
||||
return generateSummary(conversation, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummaryWithModel(String conversation, String modelId) {
|
||||
return generateSummary(conversation, null, modelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummary(String conversation, String promptTemplate, String modelId) {
|
||||
if (!isAvailable()) {
|
||||
log.warn("LLM服务不可用,无法生成总结");
|
||||
return "LLM服务不可用,无法生成总结";
|
||||
}
|
||||
|
||||
try {
|
||||
// 从智控台获取LLM模型配置
|
||||
ModelConfigEntity llmConfig;
|
||||
if (modelId != null && !modelId.trim().isEmpty()) {
|
||||
// 通过具体模型ID获取配置
|
||||
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||
} else {
|
||||
// 保持向后兼容,使用默认配置
|
||||
llmConfig = getDefaultLLMConfig();
|
||||
}
|
||||
|
||||
if (llmConfig == null || llmConfig.getConfigJson() == null) {
|
||||
log.error("未找到可用的LLM模型配置,modelId: {}", modelId);
|
||||
return "未找到可用的LLM模型配置";
|
||||
}
|
||||
|
||||
JSONObject configJson = llmConfig.getConfigJson();
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String model = configJson.getStr("model_name");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
Double temperature = configJson.getDouble("temperature");
|
||||
Integer maxTokens = configJson.getInt("max_tokens");
|
||||
|
||||
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
||||
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
||||
return "LLM配置不完整,无法生成总结";
|
||||
}
|
||||
|
||||
// 构建提示词
|
||||
String prompt = (promptTemplate != null ? promptTemplate : DEFAULT_SUMMARY_PROMPT).replace("{conversation}",
|
||||
conversation);
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("role", "user");
|
||||
message.put("content", prompt);
|
||||
messages[0] = message;
|
||||
|
||||
requestBody.put("messages", messages);
|
||||
requestBody.put("temperature", temperature != null ? temperature : 0.7);
|
||||
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
|
||||
|
||||
// 发送HTTP请求
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 构建完整的API URL
|
||||
String apiUrl = baseUrl;
|
||||
if (!apiUrl.endsWith("/chat/completions")) {
|
||||
if (!apiUrl.endsWith("/")) {
|
||||
apiUrl += "/";
|
||||
}
|
||||
apiUrl += "chat/completions";
|
||||
}
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
apiUrl, HttpMethod.POST, entity, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
JSONObject responseJson = JSONUtil.parseObj(response.getBody());
|
||||
JSONArray choices = responseJson.getJSONArray("choices");
|
||||
if (choices != null && choices.size() > 0) {
|
||||
JSONObject choice = choices.getJSONObject(0);
|
||||
JSONObject messageObj = choice.getJSONObject("message");
|
||||
return messageObj.getStr("content");
|
||||
}
|
||||
} else {
|
||||
log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("调用LLM服务生成总结时发生异常,modelId: {}", modelId, e);
|
||||
}
|
||||
|
||||
return "生成总结失败,请稍后重试";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummary(String conversation, String promptTemplate) {
|
||||
return generateSummary(conversation, promptTemplate, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummaryWithHistory(String conversation, String historyMemory, String promptTemplate,
|
||||
String modelId) {
|
||||
if (!isAvailable()) {
|
||||
log.warn("LLM服务不可用,无法生成总结");
|
||||
return "LLM服务不可用,无法生成总结";
|
||||
}
|
||||
|
||||
try {
|
||||
// 从智控台获取LLM模型配置
|
||||
ModelConfigEntity llmConfig;
|
||||
if (modelId != null && !modelId.trim().isEmpty()) {
|
||||
// 通过具体模型ID获取配置
|
||||
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||
} else {
|
||||
// 保持向后兼容,使用默认配置
|
||||
llmConfig = getDefaultLLMConfig();
|
||||
}
|
||||
|
||||
if (llmConfig == null || llmConfig.getConfigJson() == null) {
|
||||
log.error("未找到可用的LLM模型配置,modelId: {}", modelId);
|
||||
return "未找到可用的LLM模型配置";
|
||||
}
|
||||
|
||||
JSONObject configJson = llmConfig.getConfigJson();
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String model = configJson.getStr("model_name");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
|
||||
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
||||
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
||||
return "LLM配置不完整,无法生成总结";
|
||||
}
|
||||
|
||||
// 构建提示词,包含历史记忆
|
||||
String prompt = (promptTemplate != null ? promptTemplate : DEFAULT_SUMMARY_PROMPT)
|
||||
.replace("{history_memory}", historyMemory != null ? historyMemory : "无历史记忆")
|
||||
.replace("{conversation}", conversation);
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("role", "user");
|
||||
message.put("content", prompt);
|
||||
messages[0] = message;
|
||||
|
||||
requestBody.put("messages", messages);
|
||||
requestBody.put("temperature", 0.2);
|
||||
requestBody.put("max_tokens", 2000);
|
||||
|
||||
// 发送HTTP请求
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 构建完整的API URL
|
||||
String apiUrl = baseUrl;
|
||||
if (!apiUrl.endsWith("/chat/completions")) {
|
||||
if (!apiUrl.endsWith("/")) {
|
||||
apiUrl += "/";
|
||||
}
|
||||
apiUrl += "chat/completions";
|
||||
}
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
apiUrl, HttpMethod.POST, entity, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
JSONObject responseJson = JSONUtil.parseObj(response.getBody());
|
||||
JSONArray choices = responseJson.getJSONArray("choices");
|
||||
if (choices != null && choices.size() > 0) {
|
||||
JSONObject choice = choices.getJSONObject(0);
|
||||
JSONObject messageObj = choice.getJSONObject("message");
|
||||
return messageObj.getStr("content");
|
||||
}
|
||||
} else {
|
||||
log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("调用LLM服务生成总结时发生异常,modelId: {}", modelId, e);
|
||||
}
|
||||
|
||||
return "生成总结失败,请稍后重试";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
try {
|
||||
ModelConfigEntity defaultLLMConfig = getDefaultLLMConfig();
|
||||
if (defaultLLMConfig == null || defaultLLMConfig.getConfigJson() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONObject configJson = defaultLLMConfig.getConfigJson();
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
|
||||
return baseUrl != null && !baseUrl.trim().isEmpty() &&
|
||||
apiKey != null && !apiKey.trim().isEmpty();
|
||||
} catch (Exception e) {
|
||||
log.error("检查LLM服务可用性时发生异常:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable(String modelId) {
|
||||
try {
|
||||
if (modelId == null || modelId.trim().isEmpty()) {
|
||||
return isAvailable();
|
||||
}
|
||||
|
||||
// 通过具体模型ID获取配置
|
||||
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||
if (modelConfig == null || modelConfig.getConfigJson() == null) {
|
||||
log.warn("未找到指定的LLM模型配置,modelId: {}", modelId);
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONObject configJson = modelConfig.getConfigJson();
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
|
||||
return baseUrl != null && !baseUrl.trim().isEmpty() &&
|
||||
apiKey != null && !apiKey.trim().isEmpty();
|
||||
} catch (Exception e) {
|
||||
log.error("检查LLM服务可用性时发生异常,modelId: {}", modelId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从智控台获取默认的LLM模型配置
|
||||
*/
|
||||
private ModelConfigEntity getDefaultLLMConfig() {
|
||||
try {
|
||||
// 获取所有启用的LLM模型配置
|
||||
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
|
||||
if (llmConfigs == null || llmConfigs.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 优先返回默认配置,如果没有默认配置则返回第一个启用的配置
|
||||
for (ModelConfigEntity config : llmConfigs) {
|
||||
if (config.getIsDefault() != null && config.getIsDefault() == 1) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
return llmConfigs.get(0);
|
||||
} catch (Exception e) {
|
||||
log.error("获取LLM模型配置时发生异常:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -21,7 +21,12 @@ import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.config.service.ConfigService;
|
||||
import xiaozhi.modules.model.dto.*;
|
||||
import xiaozhi.modules.model.dto.LlmModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
@@ -124,6 +129,8 @@ public class ModelController {
|
||||
if (entity == null) {
|
||||
return new Result<Void>().error("模型配置不存在");
|
||||
}
|
||||
// 不更新ConfigJson字段
|
||||
entity.setConfigJson(null);
|
||||
entity.setIsEnabled(status);
|
||||
modelConfigService.updateById(entity);
|
||||
return new Result<Void>();
|
||||
@@ -141,6 +148,8 @@ public class ModelController {
|
||||
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
||||
entity.setIsEnabled(1);
|
||||
entity.setIsDefault(1);
|
||||
// 不更新ConfigJson字段
|
||||
entity.setConfigJson(null);
|
||||
modelConfigService.updateById(entity);
|
||||
|
||||
// 更新模板表中对应的模型ID
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.model.dao;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
@@ -15,4 +16,9 @@ public interface ModelConfigDao extends BaseDao<ModelConfigEntity> {
|
||||
* get model_code list
|
||||
*/
|
||||
List<String> getModelCodeList(@Param("modelType") String modelType, @Param("modelName") String modelName);
|
||||
|
||||
/**
|
||||
* 获取符合条件的TTS平台列表(id和modelName)
|
||||
*/
|
||||
List<Map<String, Object>> getTtsPlatformList();
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ public class ModelConfigBodyDTO {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// @Schema(description = "模型类型(Memory/ASR/VAD/LLM/TTS)")
|
||||
// private String modelType;
|
||||
//
|
||||
@Schema(description = "模型ID,未填写将自动生成")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "模型编码(如AliLLM、DoubaoTTS)")
|
||||
private String modelCode;
|
||||
|
||||
|
||||
@@ -19,4 +19,15 @@ public class VoiceDTO implements Serializable {
|
||||
|
||||
@Schema(description = "音色名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "音频播放地址")
|
||||
private String voiceDemo;
|
||||
|
||||
// 添加双参数构造函数,保持向后兼容
|
||||
public VoiceDTO(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.voiceDemo = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,9 +3,7 @@ package xiaozhi.modules.model.entity;
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
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;
|
||||
|
||||
@@ -18,7 +16,6 @@ import lombok.Data;
|
||||
@Schema(description = "模型配置表")
|
||||
public class ModelConfigEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
|
||||
|
||||
+20
-5
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.model.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
@@ -35,17 +36,31 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
||||
/**
|
||||
* 根据ID获取模型配置
|
||||
*
|
||||
* @param id 模型ID
|
||||
* @param isCache 是否缓存
|
||||
* @param id 模型ID
|
||||
* @return 模型配置实体
|
||||
*/
|
||||
ModelConfigEntity getModelById(String id, boolean isCache);
|
||||
ModelConfigEntity getModelByIdFromCache(String id);
|
||||
|
||||
/**
|
||||
* 设置默认模型
|
||||
*
|
||||
*
|
||||
* @param modelType 模型类型
|
||||
* @param isDefault 是否默认
|
||||
* @param isDefault 是否默认(1:是,0:否)
|
||||
*/
|
||||
void setDefaultModel(String modelType, int isDefault);
|
||||
|
||||
/**
|
||||
* 获取符合条件的TTS平台列表
|
||||
*
|
||||
* @return TTS平台列表(id和modelName)
|
||||
*/
|
||||
List<Map<String, Object>> getTtsPlatformList();
|
||||
|
||||
/**
|
||||
* 根据模型类型获取所有启用的模型配置
|
||||
*
|
||||
* @param modelType 模型类型(如:LLM, TTS, ASR等)
|
||||
* @return 启用的模型配置列表
|
||||
*/
|
||||
List<ModelConfigEntity> getEnabledModelsByType(String modelType);
|
||||
}
|
||||
|
||||
+370
-102
@@ -1,5 +1,6 @@
|
||||
package xiaozhi.modules.model.service.impl;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -10,17 +11,23 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.SensitiveDataUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||
@@ -62,7 +69,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
.eq("is_enabled", 1)
|
||||
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
||||
.select("id", "model_name", "config_json"));
|
||||
// 处理获取到的内容
|
||||
|
||||
return entities.stream().map(item -> {
|
||||
LlmModelBasicInfoDTO dto = new LlmModelBasicInfoDTO();
|
||||
dto.setId(item.getId());
|
||||
@@ -75,90 +82,377 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
|
||||
@Override
|
||||
public PageData<ModelConfigDTO> getPageList(String modelType, String modelName, String page, String limit) {
|
||||
Map<String, Object> params = new HashMap<String, Object>();
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put(Constant.PAGE, page);
|
||||
params.put(Constant.LIMIT, limit);
|
||||
|
||||
long curPage = Long.parseLong(page);
|
||||
long pageSize = Long.parseLong(limit);
|
||||
Page<ModelConfigEntity> pageInfo = new Page<>(curPage, pageSize);
|
||||
|
||||
// 添加排序规则:先按is_enabled降序,再按sort升序
|
||||
pageInfo.addOrder(OrderItem.desc("is_enabled"));
|
||||
pageInfo.addOrder(OrderItem.asc("sort"));
|
||||
|
||||
IPage<ModelConfigEntity> modelConfigEntityIPage = modelConfigDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
pageInfo,
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", modelType)
|
||||
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%"));
|
||||
|
||||
return getPageData(modelConfigEntityIPage, ModelConfigDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelConfigDTO add(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||
// 先验证有没有供应器
|
||||
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
|
||||
throw new RenException("modelType和provideCode不能为空");
|
||||
}
|
||||
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||
if (CollectionUtil.isEmpty(providerList)) {
|
||||
throw new RenException("供应器不存在");
|
||||
}
|
||||
public ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||
// 1. 参数验证
|
||||
validateEditParameters(modelType, provideCode, id, modelConfigBodyDTO);
|
||||
|
||||
// 再保存供应器提供的模型
|
||||
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
||||
modelConfigEntity.setModelType(modelType);
|
||||
modelConfigEntity.setIsDefault(0);
|
||||
modelConfigDao.insert(modelConfigEntity);
|
||||
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
||||
// 2. 验证模型提供者
|
||||
validateModelProvider(modelType, provideCode);
|
||||
|
||||
// 3. 获取原始配置(不经过敏感数据处理)
|
||||
ModelConfigEntity originalEntity = getOriginalConfigFromDb(id);
|
||||
|
||||
// 4. 验证LLM配置
|
||||
validateLlmConfiguration(modelConfigBodyDTO);
|
||||
|
||||
// 5. 准备更新实体并处理敏感数据
|
||||
ModelConfigEntity modelConfigEntity = prepareUpdateEntity(modelConfigBodyDTO, originalEntity, modelType, id);
|
||||
|
||||
// 6. 执行数据库更新
|
||||
modelConfigDao.updateById(modelConfigEntity);
|
||||
|
||||
// 7. 清除缓存
|
||||
clearModelCache(id);
|
||||
|
||||
// 8. 返回处理后的数据(包含敏感数据掩码)
|
||||
return buildResponseDTO(modelConfigEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||
// 先验证有没有供应器
|
||||
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
|
||||
throw new RenException("modelType和provideCode不能为空");
|
||||
}
|
||||
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||
if (CollectionUtil.isEmpty(providerList)) {
|
||||
throw new RenException("供应器不存在");
|
||||
}
|
||||
if (modelConfigBodyDTO.getConfigJson().containsKey("llm")) {
|
||||
String llm = modelConfigBodyDTO.getConfigJson().get("llm").toString();
|
||||
ModelConfigEntity modelConfigEntity = modelConfigDao.selectOne(new LambdaQueryWrapper<ModelConfigEntity>()
|
||||
.eq(ModelConfigEntity::getId, llm));
|
||||
String selectModelType = (modelConfigEntity == null || modelConfigEntity.getModelType() == null) ? null
|
||||
: modelConfigEntity.getModelType().toUpperCase();
|
||||
if (modelConfigEntity == null || !"LLM".equals(selectModelType)) {
|
||||
throw new RenException("设置的LLM不存在");
|
||||
}
|
||||
String type = modelConfigEntity.getConfigJson().get("type").toString();
|
||||
// 如果查询大语言模型是openai或者ollama,意图识别选参数都可以
|
||||
if (!"openai".equals(type) && !"ollama".equals(type)) {
|
||||
throw new RenException("设置的LLM不是openai和ollama");
|
||||
}
|
||||
}
|
||||
public ModelConfigDTO add(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||
validateAddParameters(modelType, provideCode, modelConfigBodyDTO);
|
||||
|
||||
// 再更新供应器提供的模型
|
||||
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
||||
modelConfigEntity.setId(id);
|
||||
modelConfigEntity.setModelType(modelType);
|
||||
modelConfigDao.updateById(modelConfigEntity);
|
||||
// 清除缓存
|
||||
redisUtils.delete(RedisKeys.getModelConfigById(modelConfigEntity.getId()));
|
||||
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
|
||||
validateModelProvider(modelType, provideCode);
|
||||
|
||||
ModelConfigEntity modelConfigEntity = prepareAddEntity(modelConfigBodyDTO, modelType);
|
||||
|
||||
modelConfigDao.insert(modelConfigEntity);
|
||||
|
||||
return buildResponseDTO(modelConfigEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
// 查看是否是默认
|
||||
if (StringUtils.isBlank(id)) {
|
||||
throw new RenException(ErrorCode.IDENTIFIER_NOT_NULL);
|
||||
}
|
||||
|
||||
ModelConfigEntity modelConfig = modelConfigDao.selectById(id);
|
||||
if (modelConfig != null && modelConfig.getIsDefault() == 1) {
|
||||
throw new RenException("该模型为默认模型,请先设置其他模型为默认模型");
|
||||
throw new RenException(ErrorCode.DEFAULT_MODEL_DELETE_ERROR);
|
||||
}
|
||||
// 验证是否有引用
|
||||
|
||||
checkAgentReference(id);
|
||||
checkIntentConfigReference(id);
|
||||
|
||||
modelConfigDao.deleteById(id);
|
||||
|
||||
clearModelCache(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelNameById(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String cacheKey = RedisKeys.getModelNameById(id);
|
||||
String cachedName = (String) redisUtils.get(cacheKey);
|
||||
if (StringUtils.isNotBlank(cachedName)) {
|
||||
return cachedName;
|
||||
}
|
||||
|
||||
ModelConfigEntity entity = modelConfigDao.selectById(id);
|
||||
if (entity != null) {
|
||||
String modelName = entity.getModelName();
|
||||
if (StringUtils.isNotBlank(modelName)) {
|
||||
redisUtils.set(cacheKey, modelName);
|
||||
}
|
||||
return modelName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelConfigEntity selectById(Serializable id) {
|
||||
ModelConfigEntity entity = super.selectById(id);
|
||||
if (entity != null && entity.getConfigJson() != null) {
|
||||
entity.setConfigJson(maskSensitiveFields(entity.getConfigJson()));
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected <D> PageData<D> getPageData(IPage<?> page, Class<D> target) {
|
||||
List<?> records = page.getRecords();
|
||||
if (records != null && !records.isEmpty()) {
|
||||
for (Object record : records) {
|
||||
if (record instanceof ModelConfigEntity) {
|
||||
ModelConfigEntity entity = (ModelConfigEntity) record;
|
||||
if (entity.getConfigJson() != null) {
|
||||
entity.setConfigJson(maskSensitiveFields(entity.getConfigJson()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.getPageData(page, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelConfigEntity getModelByIdFromCache(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
String cacheKey = RedisKeys.getModelConfigById(id);
|
||||
ModelConfigEntity entity = (ModelConfigEntity) redisUtils.get(cacheKey);
|
||||
if (entity == null) {
|
||||
entity = modelConfigDao.selectById(id);
|
||||
if (entity != null) {
|
||||
redisUtils.set(cacheKey, entity);
|
||||
}
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证编辑参数
|
||||
*/
|
||||
private void validateEditParameters(String modelType, String provideCode, String id,
|
||||
ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
|
||||
throw new RenException(ErrorCode.MODEL_TYPE_PROVIDE_CODE_NOT_NULL);
|
||||
}
|
||||
if (StringUtils.isBlank(id)) {
|
||||
throw new RenException(ErrorCode.IDENTIFIER_NOT_NULL);
|
||||
}
|
||||
if (modelConfigBodyDTO == null) {
|
||||
throw new RenException(ErrorCode.PARAMS_GET_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证添加参数
|
||||
*/
|
||||
private void validateAddParameters(String modelType, String provideCode, ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
|
||||
throw new RenException(ErrorCode.MODEL_TYPE_PROVIDE_CODE_NOT_NULL);
|
||||
}
|
||||
if (modelConfigBodyDTO == null) {
|
||||
throw new RenException(ErrorCode.PARAMS_GET_ERROR);
|
||||
}
|
||||
if (StringUtils.isBlank(modelConfigBodyDTO.getId())) {
|
||||
// 参照 MP @TableId AutoUUID 策略使用
|
||||
// com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator(UUID.replace("-",""))
|
||||
// 进行分配默认模型ID
|
||||
modelConfigBodyDTO.setId(DefaultIdentifierGenerator.getInstance().nextUUID(ModelConfigEntity.class));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认模型
|
||||
*/
|
||||
@Override
|
||||
public void setDefaultModel(String modelType, int isDefault) {
|
||||
// 参数验证
|
||||
if (StringUtils.isBlank(modelType)) {
|
||||
throw new RenException(ErrorCode.MODEL_TYPE_PROVIDE_CODE_NOT_NULL);
|
||||
}
|
||||
|
||||
ModelConfigEntity entity = new ModelConfigEntity();
|
||||
entity.setIsDefault(isDefault);
|
||||
modelConfigDao.update(entity, new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", modelType));
|
||||
|
||||
// 清除相关缓存
|
||||
clearModelCacheByType(modelType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证模型提供者
|
||||
*/
|
||||
private void validateModelProvider(String modelType, String provideCode) {
|
||||
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||
if (CollectionUtil.isEmpty(providerList)) {
|
||||
throw new RenException(ErrorCode.MODEL_PROVIDER_NOT_EXIST);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库获取原始配置(不经过敏感数据处理)
|
||||
*/
|
||||
private ModelConfigEntity getOriginalConfigFromDb(String id) {
|
||||
ModelConfigEntity originalEntity = modelConfigDao.selectById(id);
|
||||
if (originalEntity == null) {
|
||||
throw new RenException(ErrorCode.RESOURCE_NOT_FOUND);
|
||||
}
|
||||
return originalEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证LLM配置
|
||||
*/
|
||||
private void validateLlmConfiguration(ModelConfigBodyDTO modelConfigBodyDTO) {
|
||||
if (modelConfigBodyDTO.getConfigJson() != null && modelConfigBodyDTO.getConfigJson().containsKey("llm")) {
|
||||
String llm = modelConfigBodyDTO.getConfigJson().get("llm").toString();
|
||||
ModelConfigEntity modelConfigEntity = modelConfigDao.selectOne(new LambdaQueryWrapper<ModelConfigEntity>()
|
||||
.eq(ModelConfigEntity::getId, llm));
|
||||
|
||||
if (modelConfigEntity == null) {
|
||||
throw new RenException(ErrorCode.LLM_NOT_EXIST);
|
||||
}
|
||||
|
||||
String modelType = modelConfigEntity.getModelType();
|
||||
if (modelType == null || !"LLM".equals(modelType.toUpperCase())) {
|
||||
throw new RenException(ErrorCode.LLM_NOT_EXIST);
|
||||
}
|
||||
|
||||
// 验证LLM类型
|
||||
JSONObject configJson = modelConfigEntity.getConfigJson();
|
||||
if (configJson != null && configJson.containsKey("type")) {
|
||||
String type = configJson.get("type").toString();
|
||||
if (!"openai".equals(type) && !"ollama".equals(type)) {
|
||||
throw new RenException(ErrorCode.INVALID_LLM_TYPE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备更新实体,处理敏感数据
|
||||
*/
|
||||
private ModelConfigEntity prepareUpdateEntity(ModelConfigBodyDTO modelConfigBodyDTO,
|
||||
ModelConfigEntity originalEntity,
|
||||
String modelType,
|
||||
String id) {
|
||||
// 1. 复制原始实体,保留所有原始数据(包括敏感信息)
|
||||
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(originalEntity, ModelConfigEntity.class);
|
||||
modelConfigEntity.setId(id);
|
||||
modelConfigEntity.setModelType(modelType);
|
||||
|
||||
// 2. 只更新非敏感字段
|
||||
modelConfigEntity.setModelName(modelConfigBodyDTO.getModelName());
|
||||
modelConfigEntity.setSort(modelConfigBodyDTO.getSort());
|
||||
modelConfigEntity.setIsEnabled(modelConfigBodyDTO.getIsEnabled());
|
||||
// 3. 处理配置JSON,仅更新非敏感字段和明确修改的敏感字段
|
||||
if (modelConfigBodyDTO.getConfigJson() != null && originalEntity.getConfigJson() != null) {
|
||||
JSONObject originalJson = originalEntity.getConfigJson();
|
||||
JSONObject updatedJson = new JSONObject(originalJson); // 基于原始JSON进行修改
|
||||
|
||||
// 遍历更新的JSON,只更新非敏感字段或确实被修改的敏感字段
|
||||
for (String key : modelConfigBodyDTO.getConfigJson().keySet()) {
|
||||
Object value = modelConfigBodyDTO.getConfigJson().get(key);
|
||||
|
||||
// 如果是敏感字段,需要确认是否真的被修改(前端传入的可能是掩码后的值)
|
||||
if (SensitiveDataUtils.isSensitiveField(key)) {
|
||||
|
||||
if (value instanceof String && !SensitiveDataUtils.isMaskedValue((String) value)) {
|
||||
updatedJson.put(key, value);
|
||||
}
|
||||
} else if (value instanceof JSONObject) {
|
||||
// 递归处理嵌套JSON
|
||||
mergeJson(updatedJson, key, (JSONObject) value);
|
||||
} else {
|
||||
// 非敏感字段直接更新
|
||||
updatedJson.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
modelConfigEntity.setConfigJson(updatedJson);
|
||||
}
|
||||
|
||||
return modelConfigEntity;
|
||||
}
|
||||
|
||||
// 辅助方法:判断值是否是掩码格式
|
||||
private boolean isMaskedValue(String value) {
|
||||
if (value == null)
|
||||
return false;
|
||||
// 简单判断是否包含掩码的特征(***)
|
||||
return value.contains("***");
|
||||
}
|
||||
|
||||
// 辅助方法:递归合并JSON,保留原始敏感字段
|
||||
private void mergeJson(JSONObject original, String key, JSONObject updated) {
|
||||
if (!original.containsKey(key)) {
|
||||
original.put(key, new JSONObject());
|
||||
}
|
||||
JSONObject originalChild = original.getJSONObject(key);
|
||||
|
||||
for (String childKey : updated.keySet()) {
|
||||
Object childValue = updated.get(childKey);
|
||||
if (childValue instanceof JSONObject) {
|
||||
mergeJson(originalChild, childKey, (JSONObject) childValue);
|
||||
} else {
|
||||
if (!SensitiveDataUtils.isSensitiveField(childKey) ||
|
||||
(childValue instanceof String && !isMaskedValue((String) childValue))) {
|
||||
originalChild.put(childKey, childValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备新增实体
|
||||
*/
|
||||
private ModelConfigEntity prepareAddEntity(ModelConfigBodyDTO modelConfigBodyDTO, String modelType) {
|
||||
ModelConfigEntity modelConfigEntity = ConvertUtils.sourceToTarget(modelConfigBodyDTO, ModelConfigEntity.class);
|
||||
modelConfigEntity.setModelType(modelType);
|
||||
modelConfigEntity.setIsDefault(0);
|
||||
return modelConfigEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建返回的DTO,处理敏感数据
|
||||
*/
|
||||
private ModelConfigDTO buildResponseDTO(ModelConfigEntity entity) {
|
||||
ModelConfigDTO dto = ConvertUtils.sourceToTarget(entity, ModelConfigDTO.class);
|
||||
if (dto.getConfigJson() != null) {
|
||||
dto.setConfigJson(maskSensitiveFields(dto.getConfigJson()));
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理敏感字段
|
||||
*/
|
||||
private JSONObject maskSensitiveFields(JSONObject configJson) {
|
||||
return SensitiveDataUtils.maskSensitiveFields(configJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除模型缓存
|
||||
*/
|
||||
private void clearModelCache(String id) {
|
||||
redisUtils.delete(RedisKeys.getModelConfigById(id));
|
||||
redisUtils.delete(RedisKeys.getModelNameById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按模型类型清除缓存
|
||||
*/
|
||||
private void clearModelCacheByType(String modelType) {
|
||||
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||
new QueryWrapper<ModelConfigEntity>().eq("model_type", modelType));
|
||||
for (ModelConfigEntity entity : entities) {
|
||||
clearModelCache(entity.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查智能体配置是否有引用
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
private void checkAgentReference(String modelId) {
|
||||
List<AgentEntity> agents = agentDao.selectList(
|
||||
@@ -180,14 +474,12 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
String agentNames = agents.stream()
|
||||
.map(AgentEntity::getAgentName)
|
||||
.collect(Collectors.joining("、"));
|
||||
throw new RenException(String.format("该模型配置已被智能体[%s]引用,无法删除", agentNames));
|
||||
throw new RenException(ErrorCode.MODEL_REFERENCED_BY_AGENT, agentNames);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查意图识别配置是否有引用
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
private void checkIntentConfigReference(String modelId) {
|
||||
ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId);
|
||||
@@ -198,58 +490,34 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
.eq("model_type", "Intent")
|
||||
.like("config_json", "%" + modelId + "%"));
|
||||
if (!intentConfigs.isEmpty()) {
|
||||
throw new RenException("该LLM模型已被意图识别配置引用,无法删除");
|
||||
throw new RenException(ErrorCode.LLM_REFERENCED_BY_INTENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取符合条件的TTS平台列表
|
||||
*/
|
||||
@Override
|
||||
public String getModelNameById(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
public List<Map<String, Object>> getTtsPlatformList() {
|
||||
return modelConfigDao.getTtsPlatformList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型类型获取所有启用的模型配置
|
||||
*/
|
||||
@Override
|
||||
public List<ModelConfigEntity> getEnabledModelsByType(String modelType) {
|
||||
if (StringUtils.isBlank(modelType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String cachedName = (String) redisUtils.get(RedisKeys.getModelNameById(id));
|
||||
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", modelType)
|
||||
.eq("is_enabled", 1)
|
||||
.orderByAsc("sort"));
|
||||
|
||||
if (StringUtils.isNotBlank(cachedName)) {
|
||||
return cachedName;
|
||||
}
|
||||
|
||||
ModelConfigEntity entity = modelConfigDao.selectById(id);
|
||||
if (entity != null) {
|
||||
String modelName = entity.getModelName();
|
||||
if (StringUtils.isNotBlank(modelName)) {
|
||||
redisUtils.set(RedisKeys.getModelNameById(id), modelName);
|
||||
}
|
||||
return modelName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelConfigEntity getModelById(String id, boolean isCache) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
if (isCache) {
|
||||
ModelConfigEntity cachedConfig = (ModelConfigEntity) redisUtils.get(RedisKeys.getModelConfigById(id));
|
||||
if (cachedConfig != null) {
|
||||
return ConvertUtils.sourceToTarget(cachedConfig, ModelConfigEntity.class);
|
||||
}
|
||||
}
|
||||
ModelConfigEntity entity = modelConfigDao.selectById(id);
|
||||
if (entity != null) {
|
||||
redisUtils.set(RedisKeys.getModelConfigById(id), entity);
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDefaultModel(String modelType, int isDefault) {
|
||||
ModelConfigEntity entity = new ModelConfigEntity();
|
||||
entity.setIsDefault(isDefault);
|
||||
modelConfigDao.update(entity, new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", modelType));
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
|
||||
+38
-5
@@ -16,11 +16,14 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.knowledge.dao.KnowledgeBaseDao;
|
||||
import xiaozhi.modules.knowledge.entity.KnowledgeBaseEntity;
|
||||
import xiaozhi.modules.model.dao.ModelProviderDao;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||
@@ -33,13 +36,43 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
implements ModelProviderService {
|
||||
|
||||
private final ModelProviderDao modelProviderDao;
|
||||
private final KnowledgeBaseDao knowledgeBaseDao;
|
||||
|
||||
@Override
|
||||
public List<ModelProviderDTO> getPluginList() {
|
||||
// 1. 获取插件列表
|
||||
LambdaQueryWrapper<ModelProviderEntity> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(ModelProviderEntity::getModelType, "Plugin");
|
||||
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
|
||||
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
|
||||
List<ModelProviderDTO> resultList = ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
|
||||
|
||||
// 2. 获取当前用户的知识库列表并追加到结果中
|
||||
UserDetail userDetail = SecurityUser.getUser();
|
||||
if (userDetail != null && userDetail.getId() != null) {
|
||||
// 查询当前用户的知识库
|
||||
LambdaQueryWrapper<KnowledgeBaseEntity> kbQueryWrapper = new LambdaQueryWrapper<>();
|
||||
kbQueryWrapper.eq(KnowledgeBaseEntity::getCreator, userDetail.getId());
|
||||
kbQueryWrapper.eq(KnowledgeBaseEntity::getStatus, 1); // 只获取启用状态的知识库
|
||||
List<KnowledgeBaseEntity> knowledgeBases = knowledgeBaseDao.selectList(kbQueryWrapper);
|
||||
|
||||
// 将知识库转换为ModelProviderDTO格式并添加到结果列表
|
||||
for (KnowledgeBaseEntity kb : knowledgeBases) {
|
||||
ModelProviderDTO dto = new ModelProviderDTO();
|
||||
dto.setId(kb.getId());
|
||||
dto.setModelType("Rag");
|
||||
dto.setName("[知识库]" + kb.getName());
|
||||
dto.setProviderCode("ragflow"); // 假设所有RAG都使用ragflow
|
||||
dto.setFields("[]");
|
||||
dto.setSort(0);
|
||||
dto.setCreateDate(kb.getCreatedAt());
|
||||
dto.setUpdateDate(kb.getUpdatedAt());
|
||||
dto.setCreator(0L);
|
||||
dto.setUpdater(0L);
|
||||
resultList.add(dto);
|
||||
}
|
||||
}
|
||||
|
||||
return resultList;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -109,7 +142,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
modelProviderDTO.setFields(modelProviderDTO.getFields());
|
||||
ModelProviderEntity entity = ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class);
|
||||
if (modelProviderDao.insert(entity) == 0) {
|
||||
throw new RenException("新增数据失败");
|
||||
throw new RenException(ErrorCode.ADD_DATA_FAILED);
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||
@@ -122,7 +155,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
modelProviderDTO.setUpdateDate(new Date());
|
||||
if (modelProviderDao
|
||||
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
|
||||
throw new RenException("修改数据失败");
|
||||
throw new RenException(ErrorCode.UPDATE_DATA_FAILED);
|
||||
}
|
||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||
}
|
||||
@@ -130,14 +163,14 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
@Override
|
||||
public void delete(String id) {
|
||||
if (modelProviderDao.deleteById(id) == 0) {
|
||||
throw new RenException("删除数据失败");
|
||||
throw new RenException(ErrorCode.DELETE_DATA_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(List<String> ids) {
|
||||
if (modelProviderDao.deleteBatchIds(ids) == 0) {
|
||||
throw new RenException("删除数据失败");
|
||||
throw new RenException(ErrorCode.DELETE_DATA_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,8 +88,10 @@ public class ShiroConfig {
|
||||
// 将config路径使用server服务过滤器
|
||||
filterMap.put("/config/**", "server");
|
||||
filterMap.put("/agent/chat-history/report", "server");
|
||||
filterMap.put("/agent/saveMemory/**", "server");
|
||||
filterMap.put("/agent/chat-history/download/**", "anon");
|
||||
filterMap.put("/agent/chat-summary/**", "server");
|
||||
filterMap.put("/agent/play/**", "anon");
|
||||
filterMap.put("/voiceClone/play/**", "anon");
|
||||
filterMap.put("/**", "oauth2");
|
||||
shiroFilter.setFilterChainDefinitionMap(filterMap);
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@ package xiaozhi.modules.security.config;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
@@ -12,8 +15,10 @@ import org.springframework.http.converter.ResourceHttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -96,4 +101,56 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
return converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 国际化配置 - 根据请求头中的Accept-Language设置语言环境
|
||||
*/
|
||||
@Bean
|
||||
public LocaleResolver localeResolver() {
|
||||
return new AcceptHeaderLocaleResolver() {
|
||||
@Override
|
||||
public Locale resolveLocale(HttpServletRequest request) {
|
||||
String acceptLanguage = request.getHeader("Accept-Language");
|
||||
if (acceptLanguage == null || acceptLanguage.isEmpty()) {
|
||||
return Locale.getDefault();
|
||||
}
|
||||
|
||||
// 解析Accept-Language请求头中的首选语言
|
||||
String[] languages = acceptLanguage.split(",");
|
||||
if (languages.length > 0) {
|
||||
// 提取第一个语言代码,去除可能的质量值(q=...)
|
||||
String[] parts = languages[0].split(";" + "\\s*");
|
||||
String primaryLanguage = parts[0].trim();
|
||||
|
||||
// 根据前端发送的语言代码直接创建Locale对象
|
||||
if (primaryLanguage.equals("zh-CN")) {
|
||||
return Locale.SIMPLIFIED_CHINESE;
|
||||
} else if (primaryLanguage.equals("zh-TW")) {
|
||||
return Locale.TRADITIONAL_CHINESE;
|
||||
} else if (primaryLanguage.equals("en-US")) {
|
||||
return Locale.US;
|
||||
} else if (primaryLanguage.equals("de-DE")) {
|
||||
return Locale.GERMANY;
|
||||
} else if (primaryLanguage.equals("vi-VN")) {
|
||||
return Locale.forLanguageTag("vi-VN");
|
||||
} else if (primaryLanguage.startsWith("zh")) {
|
||||
// 对于其他中文变体,默认使用简体中文
|
||||
return Locale.SIMPLIFIED_CHINESE;
|
||||
} else if (primaryLanguage.startsWith("en")) {
|
||||
// 对于其他英文变体,默认使用美式英语
|
||||
return Locale.US;
|
||||
} else if (primaryLanguage.startsWith("de")) {
|
||||
// 对于其他德语变体,默认使用德语
|
||||
return Locale.GERMANY;
|
||||
} else if (primaryLanguage.startsWith("vi")) {
|
||||
// 对于其他越南语变体,默认使用越南语
|
||||
return Locale.forLanguageTag("vi-VN");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有匹配的语言,使用默认语言
|
||||
return Locale.getDefault();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
+57
-24
@@ -6,6 +6,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
@@ -17,12 +18,15 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.TokenDTO;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.utils.Sm2DecryptUtil;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.security.dto.LoginDTO;
|
||||
@@ -42,6 +46,7 @@ import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
/**
|
||||
* 登录控制层
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@@ -66,14 +71,15 @@ public class LoginController {
|
||||
@Operation(summary = "短信验证码")
|
||||
public Result<Void> smsVerification(@RequestBody SmsVerificationDTO dto) {
|
||||
// 验证图形验证码
|
||||
boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), true);
|
||||
boolean validate = captchaService.validate(dto.getCaptchaId(), dto.getCaptcha(), false);
|
||||
if (!validate) {
|
||||
throw new RenException("图形验证码错误");
|
||||
throw new RenException(ErrorCode.SMS_CAPTCHA_ERROR);
|
||||
}
|
||||
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
if (!isMobileRegister) {
|
||||
throw new RenException("没有开启手机注册,没法使用短信验证码功能");
|
||||
throw new RenException(ErrorCode.MOBILE_REGISTER_DISABLED);
|
||||
}
|
||||
// 发送短信验证码
|
||||
captchaService.sendSMSValidateCode(dto.getPhone());
|
||||
@@ -83,20 +89,23 @@ public class LoginController {
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "登录")
|
||||
public Result<TokenDTO> login(@RequestBody LoginDTO login) {
|
||||
// 验证是否正确输入验证码
|
||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
|
||||
if (!validate) {
|
||||
throw new RenException("图形验证码错误,请重新获取");
|
||||
}
|
||||
String password = login.getPassword();
|
||||
|
||||
// 使用工具类解密并验证验证码
|
||||
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
|
||||
password, login.getCaptchaId(), captchaService, sysParamsService);
|
||||
|
||||
login.setPassword(actualPassword);
|
||||
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
// 判断用户是否存在
|
||||
if (userDTO == null) {
|
||||
throw new RenException("请检测用户和密码是否输入错误");
|
||||
throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR);
|
||||
}
|
||||
// 判断密码是否正确,不一样则进入if
|
||||
if (!PasswordUtils.matches(login.getPassword(), userDTO.getPassword())) {
|
||||
throw new RenException("请检测用户和密码是否输入错误");
|
||||
throw new RenException(ErrorCode.ACCOUNT_PASSWORD_ERROR);
|
||||
}
|
||||
return sysUserTokenService.createToken(userDTO.getId());
|
||||
}
|
||||
@@ -105,8 +114,17 @@ public class LoginController {
|
||||
@Operation(summary = "注册")
|
||||
public Result<Void> register(@RequestBody LoginDTO login) {
|
||||
if (!sysUserService.getAllowUserRegister()) {
|
||||
throw new RenException("当前不允许普通用户注册");
|
||||
throw new RenException(ErrorCode.USER_REGISTER_DISABLED);
|
||||
}
|
||||
|
||||
String password = login.getPassword();
|
||||
|
||||
// 使用工具类解密并验证验证码
|
||||
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
|
||||
password, login.getCaptchaId(), captchaService, sysParamsService);
|
||||
|
||||
login.setPassword(actualPassword);
|
||||
|
||||
// 是否开启手机注册
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
@@ -115,25 +133,19 @@ public class LoginController {
|
||||
// 验证用户是否是手机号码
|
||||
boolean validPhone = ValidatorUtils.isValidPhone(login.getUsername());
|
||||
if (!validPhone) {
|
||||
throw new RenException("用户名不是手机号码,请重新输入");
|
||||
throw new RenException(ErrorCode.USERNAME_NOT_PHONE);
|
||||
}
|
||||
// 验证短信验证码是否正常
|
||||
validate = captchaService.validateSMSValidateCode(login.getUsername(), login.getMobileCaptcha(), false);
|
||||
if (!validate) {
|
||||
throw new RenException("手机验证码错误,请重新获取");
|
||||
}
|
||||
} else {
|
||||
// 验证是否正确输入验证码
|
||||
validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha(), true);
|
||||
if (!validate) {
|
||||
throw new RenException("图形验证码错误,请重新获取");
|
||||
throw new RenException(ErrorCode.SMS_CODE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
if (userDTO != null) {
|
||||
throw new RenException("此手机号码已经注册过");
|
||||
throw new RenException(ErrorCode.PHONE_ALREADY_REGISTERED);
|
||||
}
|
||||
userDTO = new SysUserDTO();
|
||||
userDTO.setUsername(login.getUsername());
|
||||
@@ -168,28 +180,36 @@ public class LoginController {
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
if (!isMobileRegister) {
|
||||
throw new RenException("没有开启手机注册,没法使用找回密码功能");
|
||||
throw new RenException(ErrorCode.RETRIEVE_PASSWORD_DISABLED);
|
||||
}
|
||||
// 判断非空
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
// 验证用户是否是手机号码
|
||||
boolean validPhone = ValidatorUtils.isValidPhone(dto.getPhone());
|
||||
if (!validPhone) {
|
||||
throw new RenException("输入的手机号码格式不正确");
|
||||
throw new RenException(ErrorCode.PHONE_FORMAT_ERROR);
|
||||
}
|
||||
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(dto.getPhone());
|
||||
if (userDTO == null) {
|
||||
throw new RenException("输入的手机号码未注册");
|
||||
throw new RenException(ErrorCode.PHONE_NOT_REGISTERED);
|
||||
}
|
||||
// 验证短信验证码是否正常
|
||||
boolean validate = captchaService.validateSMSValidateCode(dto.getPhone(), dto.getCode(), false);
|
||||
// 判断是否通过验证
|
||||
if (!validate) {
|
||||
throw new RenException("输入的手机验证码错误");
|
||||
throw new RenException(ErrorCode.SMS_CODE_ERROR);
|
||||
}
|
||||
|
||||
String password = dto.getPassword();
|
||||
|
||||
// 使用工具类解密并验证验证码
|
||||
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
|
||||
password, dto.getCaptchaId(), captchaService, sysParamsService);
|
||||
|
||||
dto.setPassword(actualPassword);
|
||||
|
||||
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
|
||||
return new Result<>();
|
||||
}
|
||||
@@ -209,6 +229,19 @@ public class LoginController {
|
||||
config.put("beianGaNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_GA_NUM.getValue(), true));
|
||||
config.put("name", sysParamsService.getValue(Constant.SysBaseParam.SERVER_NAME.getValue(), true));
|
||||
|
||||
// SM2公钥
|
||||
String publicKey = sysParamsService.getValue(Constant.SM2_PUBLIC_KEY, true);
|
||||
if (StringUtils.isBlank(publicKey)) {
|
||||
throw new RenException(ErrorCode.SM2_KEY_NOT_CONFIGURED);
|
||||
}
|
||||
config.put("sm2PublicKey", publicKey);
|
||||
|
||||
// 获取system-web.menu参数配置
|
||||
String menuConfig = sysParamsService.getValue("system-web.menu", true);
|
||||
if (StringUtils.isNotBlank(menuConfig)) {
|
||||
config.put("systemWebMenu", JsonUtils.parseObject(menuConfig, Object.class));
|
||||
}
|
||||
|
||||
return new Result<Map<String, Object>>().ok(config);
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,6 @@ public class LoginDTO implements Serializable {
|
||||
@NotBlank(message = "{sysuser.password.require}")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "验证码")
|
||||
@NotBlank(message = "{sysuser.captcha.require}")
|
||||
private String captcha;
|
||||
|
||||
@Schema(description = "手机验证码")
|
||||
private String mobileCaptcha;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.utils.HttpContextUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.MessageUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
|
||||
/**
|
||||
@@ -82,8 +83,8 @@ public class Oauth2Filter extends AuthenticatingFilter {
|
||||
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
|
||||
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
|
||||
try {
|
||||
Throwable throwable = e.getCause() == null ? e : e.getCause();
|
||||
Result<Void> r = new Result<Void>().error(ErrorCode.UNAUTHORIZED, throwable.getMessage());
|
||||
// 使用国际化消息替代直接使用异常消息
|
||||
Result<Void> r = new Result<Void>().error(ErrorCode.UNAUTHORIZED);
|
||||
|
||||
String json = JsonUtils.toJsonString(r);
|
||||
httpResponse.getWriter().print(json);
|
||||
|
||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.security.oauth2;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.UUID;
|
||||
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
|
||||
/**
|
||||
@@ -38,7 +39,7 @@ public class TokenGenerator {
|
||||
byte[] messageDigest = algorithm.digest();
|
||||
return toHexString(messageDigest);
|
||||
} catch (Exception e) {
|
||||
throw new RenException("token invalid", e);
|
||||
throw new RenException(ErrorCode.TOKEN_GENERATE_ERROR, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -16,6 +16,7 @@ import com.wf.captcha.base.Captcha;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
@@ -88,7 +89,7 @@ public class CaptchaServiceImpl implements CaptchaService {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long timeDiff = currentTime - lastSendTimeLong;
|
||||
if (timeDiff < 60000) {
|
||||
throw new RenException("发送太频繁,请" + (60000 - timeDiff) / 1000 + "秒后再试");
|
||||
throw new RenException(ErrorCode.SMS_SEND_TOO_FREQUENTLY, String.valueOf((60000 - timeDiff) / 1000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +109,7 @@ public class CaptchaServiceImpl implements CaptchaService {
|
||||
}
|
||||
|
||||
if (todayCount >= maxSendCount) {
|
||||
throw new RenException("今日发送次数已达上限");
|
||||
throw new RenException(ErrorCode.TODAY_SMS_LIMIT_REACHED);
|
||||
}
|
||||
|
||||
String key = RedisKeys.getSMSValidateCodeKey(phone);
|
||||
|
||||
+3
-2
@@ -9,6 +9,7 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
@@ -45,7 +46,7 @@ public class ALiYunSmsService implements SmsService {
|
||||
redisUtils.delete(todayCountKey);
|
||||
// 错误 message
|
||||
log.error(e.getMessage());
|
||||
throw new RenException("短信发送失败");
|
||||
throw new RenException(ErrorCode.SMS_SEND_FAILED);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -70,7 +71,7 @@ public class ALiYunSmsService implements SmsService {
|
||||
}catch (Exception e){
|
||||
// 错误 message
|
||||
log.error(e.getMessage());
|
||||
throw new RenException("短信连接建立失败");
|
||||
throw new RenException(ErrorCode.SMS_CONNECTION_FAILED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-6
@@ -22,6 +22,7 @@ import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.annotation.LogOperation;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.sys.dto.EmitSeverActionDTO;
|
||||
@@ -30,6 +31,8 @@ import xiaozhi.modules.sys.dto.ServerActionResponseDTO;
|
||||
import xiaozhi.modules.sys.enums.ServerActionEnum;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.utils.WebSocketClientManager;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
|
||||
/**
|
||||
* 服务端管理控制器
|
||||
@@ -40,6 +43,8 @@ import xiaozhi.modules.sys.utils.WebSocketClientManager;
|
||||
@AllArgsConstructor
|
||||
public class ServerSideManageController {
|
||||
private final SysParamsService sysParamsService;
|
||||
private final DeviceService deviceService;
|
||||
private final RedisUtils redisUtils;
|
||||
private static final ObjectMapper objectMapper;
|
||||
static {
|
||||
objectMapper = new ObjectMapper();
|
||||
@@ -64,17 +69,17 @@ public class ServerSideManageController {
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Boolean> emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) {
|
||||
if (emitSeverActionDTO.getAction() == null) {
|
||||
throw new RenException("无效服务端操作");
|
||||
throw new RenException(ErrorCode.INVALID_SERVER_ACTION);
|
||||
}
|
||||
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsText)) {
|
||||
throw new RenException("未配置服务端WebSocket地址");
|
||||
throw new RenException(ErrorCode.SERVER_WEBSOCKET_NOT_CONFIGURED);
|
||||
}
|
||||
String targetWs = emitSeverActionDTO.getTargetWs();
|
||||
String[] wsList = wsText.split(";");
|
||||
// 找到需要发起的
|
||||
if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) {
|
||||
throw new RenException("目标WebSocket地址不存在");
|
||||
throw new RenException(ErrorCode.TARGET_WEBSOCKET_NOT_EXIST);
|
||||
}
|
||||
return new Result<Boolean>().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction()));
|
||||
}
|
||||
@@ -84,9 +89,22 @@ public class ServerSideManageController {
|
||||
return false;
|
||||
}
|
||||
String serverSK = sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
||||
|
||||
String deviceId = UUID.randomUUID().toString();
|
||||
String clientId = UUID.randomUUID().toString();
|
||||
|
||||
String redisKey = xiaozhi.common.redis.RedisKeys.getTmpRegisterMacKey(deviceId);
|
||||
redisUtils.set(redisKey, "true", 300); // 5分钟有效期
|
||||
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
headers.add("device-id", UUID.randomUUID().toString());
|
||||
headers.add("client-id", UUID.randomUUID().toString());
|
||||
headers.add("device-id", deviceId);
|
||||
headers.add("client-id", clientId);
|
||||
try {
|
||||
String token = deviceService.generateWebSocketToken(clientId, deviceId);
|
||||
headers.add("authorization", "Bearer " + token);
|
||||
} catch (Exception e) {
|
||||
throw new RenException(ErrorCode.WEB_SOCKET_CONNECT_FAILED);
|
||||
}
|
||||
|
||||
try (WebSocketClientManager client = new WebSocketClientManager.Builder()
|
||||
.connectTimeout(3, TimeUnit.SECONDS)
|
||||
@@ -114,7 +132,7 @@ public class ServerSideManageController {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
// 捕获全部错误,由全局异常处理器返回
|
||||
throw new RenException("WebSocket连接失败或连接超时");
|
||||
throw new RenException(ErrorCode.WEB_SOCKET_CONNECT_FAILED);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
+54
-25
@@ -25,6 +25,7 @@ import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.annotation.LogOperation;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
@@ -107,9 +108,12 @@ public class SysParamsController {
|
||||
// 验证MCP地址
|
||||
validateMcpUrl(dto.getParamCode(), dto.getParamValue());
|
||||
|
||||
//
|
||||
// 验证声纹地址
|
||||
validateVoicePrint(dto.getParamCode(), dto.getParamValue());
|
||||
|
||||
// 校验mqtt密钥长度
|
||||
validateMqttSecretLength(dto.getParamCode(), dto.getParamValue());
|
||||
|
||||
sysParamsService.update(dto);
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
@@ -127,23 +131,23 @@ public class SysParamsController {
|
||||
}
|
||||
String[] wsUrls = urls.split("\\;");
|
||||
if (wsUrls.length == 0) {
|
||||
throw new RenException("WebSocket地址列表不能为空");
|
||||
throw new RenException(ErrorCode.WEBSOCKET_URLS_EMPTY);
|
||||
}
|
||||
for (String url : wsUrls) {
|
||||
if (StringUtils.isNotBlank(url)) {
|
||||
// 检查是否包含localhost或127.0.0.1
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException("WebSocket地址不能使用localhost或127.0.0.1");
|
||||
throw new RenException(ErrorCode.WEBSOCKET_URL_LOCALHOST);
|
||||
}
|
||||
|
||||
// 验证WebSocket地址格式
|
||||
if (!WebSocketValidator.validateUrlFormat(url)) {
|
||||
throw new RenException("WebSocket地址格式不正确: " + url);
|
||||
throw new RenException(ErrorCode.WEBSOCKET_URL_FORMAT_ERROR);
|
||||
}
|
||||
|
||||
// 测试WebSocket连接
|
||||
if (!WebSocketValidator.testConnection(url)) {
|
||||
throw new RenException("WebSocket连接测试失败: " + url);
|
||||
throw new RenException(ErrorCode.WEBSOCKET_CONNECTION_FAILED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,35 +174,35 @@ public class SysParamsController {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||
throw new RenException("OTA地址不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否包含localhost或127.0.0.1
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException("OTA地址不能使用localhost或127.0.0.1");
|
||||
throw new RenException(ErrorCode.OTA_URL_LOCALHOST);
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
if (!url.toLowerCase().startsWith("http")) {
|
||||
throw new RenException("OTA地址必须以http或https开头");
|
||||
throw new RenException(ErrorCode.OTA_URL_PROTOCOL_ERROR);
|
||||
}
|
||||
if (!url.endsWith("/ota/")) {
|
||||
throw new RenException("OTA地址必须以/ota/结尾");
|
||||
throw new RenException(ErrorCode.OTA_URL_FORMAT_ERROR);
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送GET请求
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
throw new RenException("OTA接口访问失败,状态码:" + response.getStatusCode());
|
||||
throw new RenException(ErrorCode.OTA_INTERFACE_ACCESS_FAILED);
|
||||
}
|
||||
// 检查响应内容是否包含OTA相关信息
|
||||
String body = response.getBody();
|
||||
if (body == null || !body.contains("OTA")) {
|
||||
throw new RenException("OTA接口返回内容格式不正确,可能不是一个真实的OTA接口");
|
||||
throw new RenException(ErrorCode.OTA_INTERFACE_FORMAT_ERROR);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RenException("OTA接口验证失败:" + e.getMessage());
|
||||
throw new RenException(ErrorCode.OTA_INTERFACE_VALIDATION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,61 +211,86 @@ public class SysParamsController {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||
throw new RenException("MCP地址不能为空");
|
||||
return;
|
||||
}
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException("MCP地址不能使用localhost或127.0.0.1");
|
||||
throw new RenException(ErrorCode.MCP_URL_LOCALHOST);
|
||||
}
|
||||
if (!url.toLowerCase().contains("key")) {
|
||||
throw new RenException("不是正确的MCP地址");
|
||||
throw new RenException(ErrorCode.MCP_URL_INVALID);
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送GET请求
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
throw new RenException("MCP接口访问失败,状态码:" + response.getStatusCode());
|
||||
throw new RenException(ErrorCode.MCP_INTERFACE_ACCESS_FAILED);
|
||||
}
|
||||
// 检查响应内容是否包含mcp相关信息
|
||||
String body = response.getBody();
|
||||
if (body == null || !body.contains("success")) {
|
||||
throw new RenException("MCP接口返回内容格式不正确,可能不是一个真实的MCP接口");
|
||||
throw new RenException(ErrorCode.MCP_INTERFACE_FORMAT_ERROR);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RenException("MCP接口验证失败:" + e.getMessage());
|
||||
throw new RenException(ErrorCode.MCP_INTERFACE_VALIDATION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证声纹接口地址是否正常
|
||||
private void validateVoicePrint(String paramCode, String url) {
|
||||
if (!paramCode.equals(Constant.SERVER_VOICE_PRINT)) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||
throw new RenException("声纹接口地址不能为空");
|
||||
return;
|
||||
}
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException("声纹接口地址不能使用localhost或127.0.0.1");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_URL_LOCALHOST);
|
||||
}
|
||||
if (!url.toLowerCase().contains("key")) {
|
||||
throw new RenException("不是正确的声纹接口地址");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_URL_INVALID);
|
||||
}
|
||||
// 验证URL格式
|
||||
if (!url.toLowerCase().startsWith("http")) {
|
||||
throw new RenException("声纹接口地址必须以http或https开头");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_URL_PROTOCOL_ERROR);
|
||||
}
|
||||
try {
|
||||
// 发送GET请求
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
|
||||
if (response.getStatusCode() != HttpStatus.OK) {
|
||||
throw new RenException("声纹接口访问失败,状态码:" + response.getStatusCode());
|
||||
throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_ACCESS_FAILED);
|
||||
}
|
||||
// 检查响应内容
|
||||
String body = response.getBody();
|
||||
if (body == null || !body.contains("healthy")) {
|
||||
throw new RenException("声纹接口返回内容格式不正确,可能不是一个真实的MCP接口");
|
||||
throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_FORMAT_ERROR);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RenException("声纹接口验证失败:" + e.getMessage());
|
||||
throw new RenException(ErrorCode.VOICEPRINT_INTERFACE_VALIDATION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
// 校验mqtt密钥长度和复杂度
|
||||
private void validateMqttSecretLength(String paramCode, String secret) {
|
||||
if (!paramCode.equals(Constant.SERVER_MQTT_SECRET)) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(secret) || secret.equals("null")) {
|
||||
throw new RenException(ErrorCode.MQTT_SECRET_EMPTY);
|
||||
}
|
||||
if (secret.length() < 8) {
|
||||
throw new RenException(ErrorCode.MQTT_SECRET_LENGTH_INSECURE);
|
||||
}
|
||||
// 检查是否同时包含大小写字母
|
||||
if (!secret.matches(".*[a-z].*") || !secret.matches(".*[A-Z].*")) {
|
||||
throw new RenException(ErrorCode.MQTT_SECRET_CHARACTER_INSECURE);
|
||||
}
|
||||
// 不允许包含弱密码
|
||||
String[] weakPasswords = { "test", "1234", "admin", "password", "qwerty", "xiaozhi" };
|
||||
for (String weakPassword : weakPasswords) {
|
||||
if (secret.toLowerCase().contains(weakPassword)) {
|
||||
throw new RenException(ErrorCode.MQTT_SECRET_WEAK_PASSWORD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ public class RetrievePasswordDTO implements Serializable {
|
||||
@NotBlank(message = "{sysuser.password.require}")
|
||||
private String password;
|
||||
|
||||
@Schema(description = "图形验证码ID")
|
||||
@NotBlank(message = "{sysuser.uuid.require}")
|
||||
private String captchaId;
|
||||
|
||||
|
||||
|
||||
}
|
||||
+2
-1
@@ -17,6 +17,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
@@ -153,7 +154,7 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
||||
}
|
||||
boolean exists = baseDao.exists(queryWrapper);
|
||||
if (exists) {
|
||||
throw new RenException("字典标签重复");
|
||||
throw new RenException(ErrorCode.DICT_LABEL_DUPLICATE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -18,6 +18,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
@@ -66,7 +67,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
||||
public SysDictTypeVO get(Long id) {
|
||||
SysDictTypeEntity entity = baseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
throw new RenException("字典类型不存在");
|
||||
throw new RenException(ErrorCode.DICT_TYPE_NOT_EXIST);
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(entity, SysDictTypeVO.class);
|
||||
@@ -147,7 +148,7 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl<SysDictTypeDao, SysD
|
||||
}
|
||||
boolean exists = baseDao.exists(queryWrapper);
|
||||
if (exists) {
|
||||
throw new RenException("字典类型编码重复");
|
||||
throw new RenException(ErrorCode.DICT_TYPE_DUPLICATE);
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
@@ -21,6 +21,7 @@ import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.SM2Utils;
|
||||
import xiaozhi.modules.sys.dao.SysParamsDao;
|
||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||
import xiaozhi.modules.sys.entity.SysParamsEntity;
|
||||
@@ -206,6 +207,31 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
||||
String newSecret = UUID.randomUUID().toString();
|
||||
updateValueByCode(Constant.SERVER_SECRET, newSecret);
|
||||
}
|
||||
|
||||
// 初始化SM2密钥对
|
||||
initSM2KeyPair();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化SM2密钥对
|
||||
*/
|
||||
private void initSM2KeyPair() {
|
||||
// 获取SM2公钥
|
||||
String publicKey = getValue(Constant.SM2_PUBLIC_KEY, false);
|
||||
// 获取SM2私钥
|
||||
String privateKey = getValue(Constant.SM2_PRIVATE_KEY, false);
|
||||
|
||||
// 如果公钥或私钥为空,则生成新的密钥对
|
||||
if (StringUtils.isBlank(publicKey) || StringUtils.isBlank(privateKey) ||
|
||||
"null".equals(publicKey) || "null".equals(privateKey)) {
|
||||
Map<String, String> keyPair = SM2Utils.createKey();
|
||||
String newPublicKey = keyPair.get(SM2Utils.KEY_PUBLIC_KEY);
|
||||
String newPrivateKey = keyPair.get(SM2Utils.KEY_PRIVATE_KEY);
|
||||
|
||||
// 更新数据库中的密钥对
|
||||
updateValueByCode(Constant.SM2_PUBLIC_KEY, newPublicKey);
|
||||
updateValueByCode(Constant.SM2_PRIVATE_KEY, newPrivateKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+24
-3
@@ -115,7 +115,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
|
||||
// 判断旧密码是否正确
|
||||
if (!PasswordUtils.matches(passwordDTO.getPassword(), sysUserEntity.getPassword())) {
|
||||
throw new RenException("旧密码输入错误");
|
||||
throw new RenException(ErrorCode.OLD_PASSWORD_ERROR);
|
||||
}
|
||||
|
||||
// 新密码强度
|
||||
@@ -197,10 +197,31 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
*/
|
||||
private String generatePassword() {
|
||||
StringBuilder password = new StringBuilder();
|
||||
for (int i = 0; i < 12; i++) {
|
||||
|
||||
// 确保包含至少一个数字
|
||||
password.append("0123456789".charAt(random.nextInt(10)));
|
||||
// 确保包含至少一个小写字母
|
||||
password.append("abcdefghijklmnopqrstuvwxyz".charAt(random.nextInt(26)));
|
||||
// 确保包含至少一个大写字母
|
||||
password.append("ABCDEFGHIJKLMNOPQRSTUVWXYZ".charAt(random.nextInt(26)));
|
||||
// 确保包含至少一个特殊符号
|
||||
password.append("!@#$%^&*()".charAt(random.nextInt(10)));
|
||||
|
||||
// 生成剩余的8个字符
|
||||
for (int i = 4; i < 12; i++) {
|
||||
password.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
|
||||
}
|
||||
return password.toString();
|
||||
|
||||
// 打乱密码中字符的顺序
|
||||
char[] passwordChars = password.toString().toCharArray();
|
||||
for (int i = 0; i < passwordChars.length; i++) {
|
||||
int randomIndex = random.nextInt(passwordChars.length);
|
||||
char temp = passwordChars[i];
|
||||
passwordChars[i] = passwordChars[randomIndex];
|
||||
passwordChars[randomIndex] = temp;
|
||||
}
|
||||
|
||||
return new String(passwordChars);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -64,4 +64,13 @@ public interface TimbreService extends BaseService<TimbreEntity> {
|
||||
* @return 音色名称
|
||||
*/
|
||||
String getTimbreNameById(String id);
|
||||
|
||||
/**
|
||||
* 根据音色编码获取音色信息
|
||||
*
|
||||
* @param ttsModelId 音色模型ID
|
||||
* @param voiceCode 音色编码
|
||||
* @return 音色信息
|
||||
*/
|
||||
VoiceDTO getByVoiceCode(String ttsModelId, String voiceCode);
|
||||
}
|
||||
+61
-3
@@ -1,9 +1,11 @@
|
||||
package xiaozhi.modules.timbre.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -15,18 +17,23 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.MessageUtils;
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.timbre.dao.TimbreDao;
|
||||
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
|
||||
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
|
||||
import xiaozhi.modules.timbre.entity.TimbreEntity;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||
import xiaozhi.modules.voiceclone.dao.VoiceCloneDao;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
|
||||
/**
|
||||
* 音色的业务层的实现
|
||||
@@ -39,6 +46,7 @@ import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
|
||||
|
||||
private final TimbreDao timbreDao;
|
||||
private final VoiceCloneDao voiceCloneDao;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
@@ -121,11 +129,36 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
queryWrapper.like("name", voiceName);
|
||||
}
|
||||
List<TimbreEntity> timbreEntities = timbreDao.selectList(queryWrapper);
|
||||
if (CollectionUtil.isEmpty(timbreEntities)) {
|
||||
return null;
|
||||
if (timbreEntities == null) {
|
||||
timbreEntities = new ArrayList<>();
|
||||
}
|
||||
List<VoiceDTO> voiceDTOs = timbreEntities.stream()
|
||||
.map(entity -> {
|
||||
VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName());
|
||||
dto.setVoiceDemo(entity.getVoiceDemo());
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 获取当前登录用户ID
|
||||
Long currentUserId = SecurityUser.getUser().getId();
|
||||
if (currentUserId != null) {
|
||||
// 查询用户的所有克隆音色记录
|
||||
List<VoiceDTO> cloneEntities = voiceCloneDao.getTrainSuccess(ttsModelId, currentUserId);
|
||||
for (VoiceDTO entity : cloneEntities) {
|
||||
// 只添加训练成功的克隆音色,且模型ID匹配
|
||||
VoiceDTO voiceDTO = new VoiceDTO();
|
||||
voiceDTO.setId(entity.getId());
|
||||
voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName());
|
||||
// 保留从数据库查询到的voiceDemo字段
|
||||
voiceDTO.setVoiceDemo(entity.getVoiceDemo());
|
||||
redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(),
|
||||
RedisUtils.NOT_EXPIRE);
|
||||
voiceDTOs.add(0, voiceDTO);
|
||||
}
|
||||
}
|
||||
|
||||
return ConvertUtils.sourceToTarget(timbreEntities, VoiceDTO.class);
|
||||
return CollectionUtil.isEmpty(voiceDTOs) ? null : voiceDTOs;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,8 +187,33 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
redisUtils.set(RedisKeys.getTimbreNameById(id), name);
|
||||
}
|
||||
return name;
|
||||
} else {
|
||||
VoiceCloneEntity cloneEntity = voiceCloneDao.selectById(id);
|
||||
if (cloneEntity != null) {
|
||||
String name = MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + cloneEntity.getName();
|
||||
redisUtils.set(RedisKeys.getTimbreNameById(id), name);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoiceDTO getByVoiceCode(String ttsModelId, String voiceCode) {
|
||||
if (StringUtils.isBlank(voiceCode)) {
|
||||
return null;
|
||||
}
|
||||
QueryWrapper<TimbreEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("tts_model_id", ttsModelId);
|
||||
queryWrapper.eq("tts_voice", voiceCode);
|
||||
List<TimbreEntity> list = timbreDao.selectList(queryWrapper);
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
TimbreEntity entity = list.get(0);
|
||||
VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName());
|
||||
dto.setVoiceDemo(entity.getVoiceDemo());
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
package xiaozhi.modules.voiceclone.controller;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||
|
||||
@Tag(name = "音色资源管理", description = "音色资源开通相关接口")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/voiceClone")
|
||||
public class VoiceCloneController {
|
||||
|
||||
private final VoiceCloneService voiceCloneService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询音色资源")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
|
||||
})
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<PageData<VoiceCloneResponseDTO>> page(
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
ValidatorUtils.validateEntity(params);
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
params.put("userId", user.getId().toString());
|
||||
PageData<VoiceCloneResponseDTO> page = voiceCloneService.pageWithNames(params);
|
||||
return new Result<PageData<VoiceCloneResponseDTO>>().ok(page);
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
@Operation(summary = "上传音频进行声音克隆")
|
||||
@Parameters({
|
||||
@Parameter(name = "id", description = "声音克隆记录ID", required = true),
|
||||
@Parameter(name = "voiceFile", description = "音频文件", required = true)
|
||||
})
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> uploadVoice(
|
||||
@RequestParam("id") String id,
|
||||
@RequestParam("voiceFile") MultipartFile voiceFile) {
|
||||
try {
|
||||
// 验证文件
|
||||
if (voiceFile == null || voiceFile.isEmpty()) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_EMPTY);
|
||||
}
|
||||
|
||||
// 验证文件类型
|
||||
String contentType = voiceFile.getContentType();
|
||||
if (contentType == null || !contentType.startsWith("audio/")) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_NOT_AUDIO_FILE);
|
||||
}
|
||||
|
||||
// 验证文件大小 (最大10MB)
|
||||
if (voiceFile.getSize() > 10 * 1024 * 1024) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_TOO_LARGE);
|
||||
}
|
||||
// 检查权限
|
||||
checkPermission(id);
|
||||
// 调用服务层处理
|
||||
voiceCloneService.uploadVoice(id, voiceFile);
|
||||
|
||||
return new Result<String>();
|
||||
} catch (Exception e) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_UPLOAD_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/updateName")
|
||||
@Operation(summary = "更新声音克隆名称")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> updateName(@RequestBody Map<String, String> params) {
|
||||
try {
|
||||
String id = params.get("id");
|
||||
String name = params.get("name");
|
||||
|
||||
if (id == null || id.isEmpty()) {
|
||||
return new Result<String>().error(ErrorCode.IDENTIFIER_NOT_NULL);
|
||||
}
|
||||
if (name == null || name.isEmpty()) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_NAME_NOT_NULL);
|
||||
}
|
||||
// 检查权限
|
||||
checkPermission(id);
|
||||
|
||||
voiceCloneService.updateName(id, name);
|
||||
redisUtils.delete(RedisKeys.getTimbreNameById(id));
|
||||
return new Result<String>();
|
||||
} catch (Exception e) {
|
||||
return new Result<String>().error(ErrorCode.UPDATE_DATA_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/audio/{id}")
|
||||
@Operation(summary = "获取音频下载ID")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> getAudioId(@PathVariable("id") String id) {
|
||||
// 检查权限
|
||||
checkPermission(id);
|
||||
byte[] audioData = voiceCloneService.getVoiceData(id);
|
||||
if (audioData == null) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_NOT_FOUND);
|
||||
}
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
redisUtils.set(RedisKeys.getVoiceCloneAudioIdKey(uuid), id);
|
||||
return new Result<String>().ok(uuid);
|
||||
}
|
||||
|
||||
@GetMapping("/play/{uuid}")
|
||||
@Operation(summary = "播放音频")
|
||||
public void playVoice(@PathVariable("uuid") String uuid, HttpServletResponse response) {
|
||||
try {
|
||||
String id = (String) redisUtils.get(RedisKeys.getVoiceCloneAudioIdKey(uuid));
|
||||
redisUtils.delete(RedisKeys.getVoiceCloneAudioIdKey(uuid));
|
||||
if (StringUtils.isBlank(id)) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
// 获取音频数据
|
||||
byte[] voiceData = voiceCloneService.getVoiceData(id);
|
||||
|
||||
if (voiceData == null || voiceData.length == 0) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
response.setContentType("audio/wav");
|
||||
response.setContentLength(voiceData.length);
|
||||
response.setHeader("Content-Disposition", "inline; filename=voice.wav");
|
||||
|
||||
// 写入音频数据
|
||||
response.getOutputStream().write(voiceData);
|
||||
response.getOutputStream().flush();
|
||||
} catch (Exception e) {
|
||||
log.error("播放音频失败", e);
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/cloneAudio")
|
||||
@Operation(summary = "复刻音频")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> cloneAudio(@RequestBody Map<String, String> params) {
|
||||
String cloneId = params.get("cloneId");
|
||||
checkPermission(cloneId);
|
||||
// 调用服务层进行语音克隆训练
|
||||
voiceCloneService.cloneAudio(cloneId);
|
||||
return new Result<String>();
|
||||
}
|
||||
|
||||
private void checkPermission(String id) {
|
||||
VoiceCloneEntity voiceClone = voiceCloneService.selectById(id);
|
||||
if (voiceClone == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
|
||||
}
|
||||
if (!voiceClone.getUserId().equals(SecurityUser.getUser().getId())) {
|
||||
throw new RenException(ErrorCode.VOICE_RESOURCE_NO_PERMISSION);
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package xiaozhi.modules.voiceclone.controller;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.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.Parameters;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
|
||||
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||
|
||||
@Tag(name = "音色资源管理", description = "音色资源开通相关接口")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/voiceResource")
|
||||
public class VoiceResourceController {
|
||||
|
||||
private final VoiceCloneService voiceCloneService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询音色资源")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
|
||||
})
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<VoiceCloneResponseDTO>> page(
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
ValidatorUtils.validateEntity(params);
|
||||
PageData<VoiceCloneResponseDTO> page = voiceCloneService.pageWithNames(params);
|
||||
return new Result<PageData<VoiceCloneResponseDTO>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("{id}")
|
||||
@Operation(summary = "获取音色资源详情")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<VoiceCloneResponseDTO> get(@PathVariable("id") String id) {
|
||||
VoiceCloneResponseDTO data = voiceCloneService.getByIdWithNames(id);
|
||||
return new Result<VoiceCloneResponseDTO>().ok(data);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "新增音色资源")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> save(@RequestBody VoiceCloneDTO dto) {
|
||||
if (dto == null) {
|
||||
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_INFO_EMPTY);
|
||||
}
|
||||
if (dto.getModelId() == null || dto.getModelId().isEmpty()) {
|
||||
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_PLATFORM_NAME_EMPTY);
|
||||
}
|
||||
if (dto.getVoiceIds() == null || dto.getVoiceIds().isEmpty()) {
|
||||
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_ID_EMPTY);
|
||||
}
|
||||
if (dto.getUserId() == null) {
|
||||
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_ACCOUNT_EMPTY);
|
||||
}
|
||||
try {
|
||||
voiceCloneService.save(dto);
|
||||
return new Result<Void>();
|
||||
} catch (xiaozhi.common.exception.RenException e) {
|
||||
return new Result<Void>().error(e.getCode(), e.getMsg());
|
||||
} catch (RuntimeException e) {
|
||||
return new Result<Void>().error(ErrorCode.ADD_DATA_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "删除音色资源")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@PathVariable("id") String[] ids) {
|
||||
if (ids == null || ids.length == 0) {
|
||||
return new Result<Void>().error(ErrorCode.VOICE_RESOURCE_DELETE_ID_EMPTY);
|
||||
}
|
||||
voiceCloneService.delete(ids);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@GetMapping("/user/{userId}")
|
||||
@Operation(summary = "根据用户ID获取音色资源列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<VoiceCloneResponseDTO>> getByUserId(@PathVariable("userId") Long userId) {
|
||||
List<VoiceCloneResponseDTO> list = voiceCloneService.getByUserIdWithNames(userId);
|
||||
return new Result<List<VoiceCloneResponseDTO>>().ok(list);
|
||||
}
|
||||
|
||||
@GetMapping("/ttsPlatforms")
|
||||
@Operation(summary = "获取TTS平台列表")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<List<Map<String, Object>>> getTtsPlatformList() {
|
||||
List<Map<String, Object>> list = modelConfigService.getTtsPlatformList();
|
||||
return new Result<List<Map<String, Object>>>().ok(list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package xiaozhi.modules.voiceclone.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
|
||||
/**
|
||||
* 声音克隆
|
||||
*/
|
||||
@Mapper
|
||||
public interface VoiceCloneDao extends BaseMapper<VoiceCloneEntity> {
|
||||
/**
|
||||
* 获取用户训练成功的音色列表
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
* @param userId 用户ID
|
||||
* @return 训练成功的音色列表
|
||||
*/
|
||||
List<VoiceDTO> getTrainSuccess(String modelId, Long userId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package xiaozhi.modules.voiceclone.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "声音克隆DTO")
|
||||
public class VoiceCloneDTO {
|
||||
|
||||
@Schema(description = "模型ID")
|
||||
private String modelId;
|
||||
|
||||
@Schema(description = "音色ID列表")
|
||||
private List<String> voiceIds;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Long userId;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package xiaozhi.modules.voiceclone.dto;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 声音克隆响应DTO
|
||||
* 用于向前端展示声音克隆信息,包含模型名称和用户名称
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "声音克隆响应DTO")
|
||||
public class VoiceCloneResponseDTO {
|
||||
|
||||
@Schema(description = "唯一标识")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "声音名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "模型id")
|
||||
private String modelId;
|
||||
|
||||
@Schema(description = "模型名称")
|
||||
private String modelName;
|
||||
|
||||
@Schema(description = "声音id")
|
||||
private String voiceId;
|
||||
|
||||
@Schema(description = "用户ID(关联用户表)")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户名称")
|
||||
private String userName;
|
||||
|
||||
@Schema(description = "训练状态:0待训练 1训练中 2训练成功 3训练失败")
|
||||
private Integer trainStatus;
|
||||
|
||||
@Schema(description = "训练错误原因")
|
||||
private String trainError;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createDate;
|
||||
|
||||
@Schema(description = "是否有音频数据")
|
||||
private Boolean hasVoice;
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package xiaozhi.modules.voiceclone.entity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@TableName("ai_voice_clone")
|
||||
@Schema(description = "声音克隆")
|
||||
public class VoiceCloneEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "唯一标识")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "声音名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "模型id")
|
||||
private String modelId;
|
||||
|
||||
@Schema(description = "声音id")
|
||||
private String voiceId;
|
||||
|
||||
@Schema(description = "用户 ID(关联用户表)")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "声音")
|
||||
private byte[] voice;
|
||||
|
||||
@Schema(description = "训练状态:0待训练 1训练中 2训练成功 3训练失败")
|
||||
private Integer trainStatus;
|
||||
|
||||
@Schema(description = "训练错误原因")
|
||||
private String trainError;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createDate;
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package xiaozhi.modules.voiceclone.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
|
||||
/**
|
||||
* 声音克隆管理
|
||||
*/
|
||||
public interface VoiceCloneService extends BaseService<VoiceCloneEntity> {
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*/
|
||||
PageData<VoiceCloneEntity> page(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 保存声音克隆
|
||||
*/
|
||||
void save(VoiceCloneDTO dto);
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*/
|
||||
void delete(String[] ids);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询声音克隆列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 声音克隆列表
|
||||
*/
|
||||
List<VoiceCloneEntity> getByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询带模型名称和用户名称的声音克隆列表
|
||||
*/
|
||||
PageData<VoiceCloneResponseDTO> pageWithNames(Map<String, Object> params);
|
||||
|
||||
/**
|
||||
* 根据ID查询带模型名称和用户名称的声音克隆信息
|
||||
*/
|
||||
VoiceCloneResponseDTO getByIdWithNames(String id);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询带模型名称的声音克隆列表
|
||||
*/
|
||||
List<VoiceCloneResponseDTO> getByUserIdWithNames(Long userId);
|
||||
|
||||
/**
|
||||
* 上传音频文件
|
||||
*/
|
||||
void uploadVoice(String id, MultipartFile voiceFile) throws Exception;
|
||||
|
||||
/**
|
||||
* 更新声音克隆名称
|
||||
*/
|
||||
void updateName(String id, String name);
|
||||
|
||||
/**
|
||||
* 获取音频数据
|
||||
*/
|
||||
byte[] getVoiceData(String id);
|
||||
|
||||
/**
|
||||
* 克隆音频,调用火山引擎进行语音复刻训练
|
||||
*
|
||||
* @param cloneId 语音克隆记录ID
|
||||
*/
|
||||
void cloneAudio(String cloneId);
|
||||
}
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
package xiaozhi.modules.voiceclone.service.impl;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
import xiaozhi.modules.voiceclone.dao.VoiceCloneDao;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO;
|
||||
import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity;
|
||||
import xiaozhi.modules.voiceclone.service.VoiceCloneService;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceCloneEntity>
|
||||
implements VoiceCloneService {
|
||||
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final SysUserService sysUserService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public PageData<VoiceCloneEntity> page(Map<String, Object> params) {
|
||||
IPage<VoiceCloneEntity> page = baseDao.selectPage(
|
||||
getPage(params, "create_date", true),
|
||||
getWrapper(params));
|
||||
|
||||
return new PageData<>(page.getRecords(), page.getTotal());
|
||||
}
|
||||
|
||||
private QueryWrapper<VoiceCloneEntity> getWrapper(Map<String, Object> params) {
|
||||
String name = (String) params.get("name");
|
||||
String userId = (String) params.get("userId");
|
||||
|
||||
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq(StringUtils.isNotBlank(userId), "user_id", userId);
|
||||
if (StringUtils.isNotBlank(name)) {
|
||||
wrapper.and(w -> w.like("name", name)
|
||||
.or().eq("voice_id", name));
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void save(VoiceCloneDTO dto) {
|
||||
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(dto.getModelId());
|
||||
if (modelConfig == null || modelConfig.getConfigJson() == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_CONFIG_NOT_FOUND);
|
||||
}
|
||||
Map<String, Object> config = modelConfig.getConfigJson();
|
||||
String type = (String) config.get("type");
|
||||
if (StringUtils.isBlank(type)) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_TYPE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 检查Voice ID是否已经被使用
|
||||
for (String voiceId : dto.getVoiceIds()) {
|
||||
if (StringUtils.isBlank(voiceId)) {
|
||||
continue;
|
||||
}
|
||||
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(type)) {
|
||||
if (voiceId.indexOf("S_") == -1) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_HUOSHAN_VOICE_ID_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("voice_id", voiceId);
|
||||
wrapper.eq("model_id", dto.getModelId());
|
||||
Long count = baseDao.selectCount(wrapper);
|
||||
if (count > 0) {
|
||||
throw new RenException(ErrorCode.VOICE_ID_ALREADY_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历选择的音色ID,为每个音色ID创建一条记录
|
||||
int index = 0;
|
||||
String namePrefix = DateUtils.format(new java.util.Date(), "MMddHHmm");
|
||||
for (String voiceId : dto.getVoiceIds()) {
|
||||
index++;
|
||||
VoiceCloneEntity entity = new VoiceCloneEntity();
|
||||
entity.setModelId(dto.getModelId());
|
||||
entity.setVoiceId(voiceId);
|
||||
entity.setName(namePrefix + "_" + index);
|
||||
entity.setUserId(dto.getUserId());
|
||||
entity.setTrainStatus(0); // 默认训练中
|
||||
|
||||
baseDao.insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String[] ids) {
|
||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VoiceCloneEntity> getByUserId(Long userId) {
|
||||
QueryWrapper<VoiceCloneEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
wrapper.orderByDesc("created_at");
|
||||
return baseDao.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageData<VoiceCloneResponseDTO> pageWithNames(Map<String, Object> params) {
|
||||
// 先查询分页数据
|
||||
IPage<VoiceCloneEntity> page = baseDao.selectPage(
|
||||
getPage(params, "create_date", true),
|
||||
getWrapper(params));
|
||||
|
||||
// 将实体列表转换为DTO列表
|
||||
List<VoiceCloneResponseDTO> dtoList = convertToResponseDTOList(page.getRecords());
|
||||
|
||||
return new PageData<>(dtoList, page.getTotal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public VoiceCloneResponseDTO getByIdWithNames(String id) {
|
||||
VoiceCloneEntity entity = baseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
VoiceCloneResponseDTO dto = ConvertUtils.sourceToTarget(entity, VoiceCloneResponseDTO.class);
|
||||
|
||||
// 设置模型名称
|
||||
if (StringUtils.isNotBlank(entity.getModelId())) {
|
||||
dto.setModelName(modelConfigService.getModelNameById(entity.getModelId()));
|
||||
}
|
||||
|
||||
// 设置用户名称
|
||||
if (entity.getUserId() != null) {
|
||||
dto.setUserName(sysUserService.getByUserId(entity.getUserId()).getUsername());
|
||||
}
|
||||
|
||||
// 确保trainStatus字段被正确设置,前端需要这个字段来判断是否为克隆音频
|
||||
dto.setTrainStatus(entity.getTrainStatus());
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<VoiceCloneResponseDTO> getByUserIdWithNames(Long userId) {
|
||||
List<VoiceCloneEntity> entityList = getByUserId(userId);
|
||||
return convertToResponseDTOList(entityList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将VoiceCloneEntity列表转换为VoiceCloneResponseDTO列表
|
||||
*/
|
||||
private List<VoiceCloneResponseDTO> convertToResponseDTOList(List<VoiceCloneEntity> entityList) {
|
||||
if (entityList == null || entityList.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
List<VoiceCloneResponseDTO> dtoList = new ArrayList<>(entityList.size());
|
||||
|
||||
// 转换每个实体为DTO
|
||||
for (VoiceCloneEntity entity : entityList) {
|
||||
VoiceCloneResponseDTO dto = ConvertUtils.sourceToTarget(entity, VoiceCloneResponseDTO.class);
|
||||
|
||||
// 设置模型名称
|
||||
if (StringUtils.isNotBlank(entity.getModelId())) {
|
||||
dto.setModelName(modelConfigService.getModelNameById(entity.getModelId()));
|
||||
}
|
||||
|
||||
// 设置用户名称
|
||||
if (entity.getUserId() != null) {
|
||||
dto.setUserName(sysUserService.getByUserId(entity.getUserId()).getUsername());
|
||||
}
|
||||
|
||||
// 确保trainStatus字段被正确设置,前端需要这个字段来判断是否为克隆音频
|
||||
dto.setTrainStatus(entity.getTrainStatus());
|
||||
|
||||
// 设置是否有音频数据
|
||||
dto.setHasVoice(entity.getVoice() != null);
|
||||
|
||||
dtoList.add(dto);
|
||||
}
|
||||
|
||||
return dtoList;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void uploadVoice(String id, MultipartFile voiceFile) throws Exception {
|
||||
// 查询声音克隆记录
|
||||
VoiceCloneEntity entity = baseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
|
||||
}
|
||||
|
||||
// 读取音频文件并转为字节数组
|
||||
byte[] voiceData = voiceFile.getBytes();
|
||||
|
||||
// 更新voice字段
|
||||
entity.setVoice(voiceData);
|
||||
// 更新训练状态为待训练
|
||||
entity.setTrainStatus(0);
|
||||
|
||||
// 保存到数据库
|
||||
baseDao.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateName(String id, String name) {
|
||||
// 查询声音克隆记录
|
||||
VoiceCloneEntity entity = baseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
|
||||
}
|
||||
|
||||
// 更新名称
|
||||
entity.setName(name);
|
||||
baseDao.updateById(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getVoiceData(String id) {
|
||||
VoiceCloneEntity entity = baseDao.selectById(id);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
return entity.getVoice();
|
||||
}
|
||||
|
||||
@Override
|
||||
// @Transactional(rollbackFor = Exception.class)
|
||||
public void cloneAudio(String cloneId) {
|
||||
VoiceCloneEntity entity = baseDao.selectById(cloneId);
|
||||
if (entity == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
|
||||
}
|
||||
if (entity.getVoice() == null || entity.getVoice().length == 0) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_AUDIO_NOT_UPLOADED);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(entity.getModelId());
|
||||
if (modelConfig == null || modelConfig.getConfigJson() == null) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_CONFIG_NOT_FOUND);
|
||||
}
|
||||
Map<String, Object> config = modelConfig.getConfigJson();
|
||||
String type = (String) config.get("type");
|
||||
if (StringUtils.isBlank(type)) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_MODEL_TYPE_NOT_FOUND);
|
||||
}
|
||||
if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(type)) {
|
||||
huoshanClone(config, entity);
|
||||
}
|
||||
} catch (RenException re) {
|
||||
entity.setTrainStatus(3);
|
||||
entity.setTrainError(re.getMsg());
|
||||
baseDao.updateById(entity);
|
||||
throw re;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
entity.setTrainStatus(3);
|
||||
entity.setTrainError(e.getMessage());
|
||||
baseDao.updateById(entity);
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_TRAINING_FAILED, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用火山引擎进行语音复刻训练
|
||||
*
|
||||
* @param config 模型配置
|
||||
* @param entity 语音克隆记录实体
|
||||
* @throws Exception
|
||||
*/
|
||||
private void huoshanClone(Map<String, Object> config, VoiceCloneEntity entity) throws Exception {
|
||||
String appid = (String) config.get("appid");
|
||||
String accessToken = (String) config.get("access_token");
|
||||
|
||||
if (StringUtils.isAnyBlank(appid, accessToken)) {
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_HUOSHAN_CONFIG_MISSING);
|
||||
}
|
||||
|
||||
String audioBase64 = Base64.getEncoder().encodeToString(entity.getVoice());
|
||||
Map<String, Object> reqBody = new HashMap<>();
|
||||
reqBody.put("appid", appid);
|
||||
List<Map<String, String>> audios = new ArrayList<>();
|
||||
Map<String, String> audioMap = new HashMap<>();
|
||||
audioMap.put("audio_bytes", audioBase64);
|
||||
audioMap.put("audio_format", "wav");
|
||||
audios.add(audioMap);
|
||||
reqBody.put("audios", audios);
|
||||
reqBody.put("source", 2);
|
||||
reqBody.put("language", 0);
|
||||
reqBody.put("model_type", 1);
|
||||
reqBody.put("speaker_id", entity.getVoiceId());
|
||||
|
||||
String apiUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/audio/upload";
|
||||
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(apiUrl))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", "Bearer;" + accessToken)
|
||||
.header("Resource-Id", "seed-icl-1.0")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(reqBody)))
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
System.out.println(">>> HTTP status = " + response.statusCode());
|
||||
System.out.println(">>> response body = " + response.body());
|
||||
|
||||
Map<String, Object> rsp = objectMapper.readValue(response.body(),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
// 获取BaseResp对象
|
||||
Map<String, Object> baseResp = objectMapper.convertValue(rsp.get("BaseResp"),
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
if (baseResp != null) {
|
||||
Integer statusCode = objectMapper.convertValue(baseResp.get("StatusCode"), Integer.class);
|
||||
String statusMessage = objectMapper.convertValue(baseResp.getOrDefault("StatusMessage", ""),
|
||||
String.class);
|
||||
|
||||
// 获取speaker_id
|
||||
String speakerId = objectMapper.convertValue(rsp.get("speaker_id"), String.class);
|
||||
|
||||
// StatusCode == 0 表示成功
|
||||
if (statusCode != null && statusCode == 0 && StringUtils.isNotBlank(speakerId)) {
|
||||
entity.setTrainStatus(2);
|
||||
entity.setVoiceId(speakerId);
|
||||
entity.setTrainError("");
|
||||
baseDao.updateById(entity);
|
||||
} else {
|
||||
// 失败时使用StatusMessage作为错误信息
|
||||
String errorMsg = StringUtils.isNotBlank(statusMessage) ? statusMessage : "训练失败";
|
||||
throw new RenException(errorMsg);
|
||||
}
|
||||
} else {
|
||||
String errorMsg = objectMapper.convertValue(rsp.get("message"),
|
||||
new TypeReference<String>() {
|
||||
});
|
||||
if (StringUtils.isNotBlank(errorMsg)) {
|
||||
throw new RenException(errorMsg);
|
||||
}
|
||||
throw new RenException(ErrorCode.VOICE_CLONE_RESPONSE_FORMAT_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
-- VOSK ASR模型供应器
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_ASR_VoskASR';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_ASR_VoskASR', 'ASR', 'vosk', 'VOSK离线语音识别', '[{"key": "model_path", "type": "string", "label": "模型路径"}, {"key": "output_dir", "type": "string", "label": "输出目录"}]', 11, 1, NOW(), 1, NOW());
|
||||
|
||||
-- VOSK ASR模型配置
|
||||
delete from `ai_model_config` where id = 'ASR_VoskASR';
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_VoskASR', 'ASR', 'VoskASR', 'VOSK离线语音识别', 0, 1, '{\"type\": \"vosk\", \"model_path\": \"\", \"output_dir\": \"tmp/\"}', NULL, NULL, 11, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新VOSK ASR配置说明
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://alphacephei.com/vosk/',
|
||||
`remark` = 'VOSK ASR配置说明:
|
||||
1. VOSK是一个离线语音识别库,支持多种语言
|
||||
2. 需要先下载模型文件:https://alphacephei.com/vosk/models
|
||||
3. 中文模型推荐使用vosk-model-small-cn-0.22或vosk-model-cn-0.22
|
||||
4. 完全离线运行,无需网络连接
|
||||
5. 输出文件保存在tmp/目录
|
||||
使用步骤:
|
||||
1. 访问 https://alphacephei.com/vosk/models 下载中文模型
|
||||
2. 解压模型文件到项目目录下的models/vosk/文件夹
|
||||
3. 在配置中指定正确的模型路径
|
||||
4. 注意:VOSK中文模型输出不带标点符号,词与词之间会有空格
|
||||
' WHERE `id` = 'ASR_VoskASR';
|
||||
@@ -0,0 +1,45 @@
|
||||
-- 添加 MinimaxHTTPStream 流式 TTS 供应器
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_TTS_MinimaxStreamTTS';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_TTS_MinimaxStreamTTS', 'TTS', 'minimax_httpstream', 'Minimax流式语音合成', '[{"key":"group_id","label":"组ID","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"model","label":"模型","type":"string"},{"key":"voice_id","label":"音色ID","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"voice_setting","label":"音色设置","type":"dict","dict_name":"voice_setting"},{"key":"pronunciation_dict","label":"发音字典","type":"dict","dict_name":"pronunciation_dict"},{"key":"audio_setting","label":"音频设置","type":"dict","dict_name":"audio_setting"},{"key":"timber_weights","label":"音色权重","type":"string"}]', 18, 1, NOW(), 1, NOW());
|
||||
|
||||
-- 添加Minimax流式TTS模型配置
|
||||
delete from `ai_model_config` where id = 'TTS_MinimaxStreamTTS';
|
||||
INSERT INTO `ai_model_config` VALUES ('TTS_MinimaxStreamTTS', 'TTS', 'MinimaxStreamTTS', 'Minimax流式语音合成', 0, 1, '{"type": "minimax_httpstream", "group_id": "", "api_key": "", "model": "speech-01-turbo", "voice_id": "female-shaonv", "output_dir": "tmp/", "voice_setting": {"speed": 1, "vol": 1, "pitch": 0, "emotion": "happy"}, "pronunciation_dict": {"tone": ["处理/(chu3)(li3)", "危险/dangerous"]}, "audio_setting": {"sample_rate": 24000, "bitrate": 128000, "format": "pcm", "channel": 1}}', NULL, NULL, 21, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新Minimax流式TTS配置说明
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://platform.minimaxi.com/',
|
||||
`remark` = 'Minimax流式TTS配置说明:
|
||||
1. 需要先申请Minimax API Key
|
||||
2. 需要填写Group ID
|
||||
3. 支持多种音色设置和音频参数调整
|
||||
4. 支持实时流式合成,具有较低的延迟
|
||||
5. 支持自定义发音字典和音色权重
|
||||
6. 隐藏参数配置:声音设定(voice_setting)、发音字典(pronunciation_dict)、音色权重(timber_weights)
|
||||
- 语速(speed): 范围[0.5,2],默认1.0,取值越大语速越快
|
||||
- 音量(vol): 范围(0,10],默认1.0,取值越大音量越高
|
||||
- 音调(pitch): 范围[-12,12],默认0,取值需为整数
|
||||
- 情绪(emotion): 控制合成语音的情绪,支持7种值:["happy", "sad", "angry", "fearful", "disgusted", "surprised", "calm"],该参数仅对 speech-2.5-hd-preview、speech-2.5-turbo-preview、speech-02-hd、speech-02-turbo、speech-01-turbo、speech-01-hd 生效
|
||||
- timbre_weights与voice_id二选一必填
|
||||
- voice_id(请求的音色id,须和weight参数同步填写)
|
||||
- weight(权重,最多支持4种音色混合。范围[1,100])
|
||||
' WHERE `id` = 'TTS_MinimaxStreamTTS';
|
||||
|
||||
-- 添加Minimax流式TTS音色
|
||||
delete from `ai_tts_voice` where tts_model_id = 'TTS_MinimaxStreamTTS';
|
||||
|
||||
-- 默认音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0001', 'TTS_MinimaxStreamTTS', '少女音', 'female-shaonv', '中文', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0002', 'TTS_MinimaxStreamTTS', '成熟女声', 'female-chengshu', '中文', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0003', 'TTS_MinimaxStreamTTS', '霸道少爷', 'badao_shaoye', '中文', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0004', 'TTS_MinimaxStreamTTS', '病娇弟弟', 'bingjiao_didi', '中文', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0005', 'TTS_MinimaxStreamTTS', '纯真学弟', 'chunzhen_xuedi', '中文', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0006', 'TTS_MinimaxStreamTTS', '冷淡学长', 'lengdan_xiongzhang', '中文', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0007', 'TTS_MinimaxStreamTTS', '甜美小玲', 'tianxin_xiaoling', '中文', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0008', 'TTS_MinimaxStreamTTS', '俏皮萌妹', 'qiaopi_mengmei', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0009', 'TTS_MinimaxStreamTTS', '妩媚御姐', 'wumei_yujie', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0010', 'TTS_MinimaxStreamTTS', '嗲嗲学妹', 'diadia_xuemei', '中文', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0011', 'TTS_MinimaxStreamTTS', '淡雅学姐', 'danya_xuejie', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0012', 'TTS_MinimaxStreamTTS', 'Santa Claus', 'Santa_Claus', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0013', 'TTS_MinimaxStreamTTS', 'Grinch', 'Grinch', '中文', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
|
||||
@@ -0,0 +1,8 @@
|
||||
delete from `sys_params` where param_code = 'server.mqtt_gateway';
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (116, 'server.mqtt_gateway', 'null', 'string', 1, 'mqtt gateway 配置');
|
||||
|
||||
delete from `sys_params` where param_code = 'server.mqtt_signature_key';
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (117, 'server.mqtt_signature_key', 'null', 'string', 1, 'mqtt 密钥 配置');
|
||||
|
||||
delete from `sys_params` where param_code = 'server.udp_gateway';
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (118, 'server.udp_gateway', 'null', 'string', 1, 'udp gateway 配置');
|
||||
@@ -0,0 +1,4 @@
|
||||
-- 添加声纹识别相似度阈值参数配置
|
||||
delete from `sys_params` where id = 115;
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
|
||||
VALUES (115, 'server.voiceprint_similarity_threshold', '0.4', 'string', 1, '声纹识别相似度阈值,范围0.0-1.0,默认0.4,数值越高越严格');
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 删除非流式MiniMax TTS配置,保留流式版本
|
||||
|
||||
-- 删除旧的非流式MiniMax TTS模型配置
|
||||
DELETE FROM `ai_model_config` WHERE `id` = 'TTS_MinimaxTTS';
|
||||
|
||||
-- 删除旧的非流式MiniMax TTS供应器配置
|
||||
DELETE FROM `ai_model_provider` WHERE `id` = 'SYSTEM_TTS_minimax';
|
||||
|
||||
-- 删除旧的非流式MiniMax TTS音色配置
|
||||
DELETE FROM `ai_tts_voice` WHERE `tts_model_id` = 'TTS_MinimaxTTS';
|
||||
@@ -0,0 +1,16 @@
|
||||
-- 添加通义千问Qwen3-ASR-Flash语音识别服务配置
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_ASR_Qwen3Flash';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_ASR_Qwen3Flash', 'ASR', 'qwen3_asr_flash', 'Qwen3-ASR-Flash语音识别', '[{"key":"api_key","label":"API密钥","type":"password"},{"key":"base_url","label":"服务地址","type":"string"},{"key":"model_name","label":"模型名称","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 17, 1, NOW(), 1, NOW());
|
||||
|
||||
delete from `ai_model_config` where id = 'ASR_Qwen3Flash';
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_Qwen3Flash', 'ASR', 'Qwen3-ASR-Flash', '通义千问语音识别服务', 0, 1, '{"type": "qwen3_asr_flash", "api_key": "", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "model_name": "qwen3-asr-flash", "output_dir": "tmp/", "enable_lid": true, "enable_itn": true}', 'https://help.aliyun.com/zh/bailian/', '支持多语言识别、歌唱识别、噪声拒识功能', 20, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新Qwen3-ASR-Flash模型配置的说明文档
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1&tab=doc#/doc/?type=model&url=2979031',
|
||||
`remark` = '通义千问Qwen3-ASR-Flash配置说明:
|
||||
1. 登录阿里云百炼平台https://bailian.console.aliyun.com/
|
||||
2. 创建API-KEY https://bailian.console.aliyun.com/#/api-key
|
||||
3.Qwen3-ASR-Flash基于通义千问多模态基座,支持多语言识别、歌唱识别、噪声拒识等功能
|
||||
' WHERE `id` = 'ASR_Qwen3Flash';
|
||||
@@ -0,0 +1,3 @@
|
||||
delete from `sys_params` where param_code = 'server.mqtt_manager_api';
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark)
|
||||
VALUES (119, 'server.mqtt_manager_api', 'null', 'string', 1, 'MQTT网关管理API的地址');
|
||||
@@ -0,0 +1,54 @@
|
||||
-- 添加阿里百炼流式TTS 供应器
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_TTS_AliBLStreamTTS';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_TTS_AliBLStreamTTS', 'TTS', 'alibl_stream', '阿里百炼流式语音合成', '[{"key":"api_key","label":"API密钥","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"model","label":"模型","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"sample_rate","label":"采样率","type":"number"},{"key": "volume", "type": "number", "label": "音量"},{"key": "rate", "type": "number", "label": "语速"},{"key": "pitch", "type": "number", "label": "音调"}]', 19, 1, NOW(), 1, NOW());
|
||||
|
||||
-- 添加阿里百炼流式TTS模型配置
|
||||
delete from `ai_model_config` where id = 'TTS_AliBLStreamTTS';
|
||||
INSERT INTO `ai_model_config` VALUES ('TTS_AliBLStreamTTS', 'TTS', 'AliBLStreamTTS', '阿里百炼流式语音合成', 0, 1, '{\"type\": \"alibl_stream\", \"appkey\": \"\", \"output_dir\": \"tmp/\", \"model\": \"cosyvoice-v2\", \"voice\": \"longcheng_v2\", \"format\": \"pcm\", \"sample_rate\": 24000, \"volume\": 50, \"rate\": 1, \"pitch\": 1}', NULL, NULL, 22, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新阿里百炼流式TTS配置说明
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://bailian.console.aliyun.com/?apiKey=1#/api-key',
|
||||
`remark` = '阿里百炼流式TTS说明:
|
||||
1. 访问 https://bailian.console.aliyun.com/?apiKey=1#/api-key 创建项目并获取appkey
|
||||
2. 支持实时流式合成,具有较低的延迟
|
||||
3. 支持多种音色设置和音频参数调整
|
||||
4. 支持CosyVoice-V3大模型音色,价格实惠(0.4元/万字符)
|
||||
5. 支持实时调节音量、语速、音调等参数
|
||||
6. 如果需要使用CosyVoice-V3模型和一些限制类型的音色,需要联系阿里百炼客服申请
|
||||
' WHERE `id` = 'TTS_AliBLStreamTTS';
|
||||
|
||||
-- 添加阿里百炼流式TTS音色
|
||||
delete from `ai_tts_voice` where tts_model_id = 'TTS_AliBLStreamTTS';
|
||||
|
||||
-- 语音助手
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0001', 'TTS_AliBLStreamTTS', '龙小淳-知性积极女', 'longxiaochun_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0002', 'TTS_AliBLStreamTTS', '龙小夏-沉稳权威女', 'longxiaoxia_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 直播带货
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0003', 'TTS_AliBLStreamTTS', '龙安燃-活泼质感女', 'longanran', '中文及中英文混合', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0004', 'TTS_AliBLStreamTTS', '龙安宣-经典直播女', 'longanxuan', '中文及中英文混合', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 社交陪伴
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0005', 'TTS_AliBLStreamTTS', '龙寒-温暖痴情男', 'longhan_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0006', 'TTS_AliBLStreamTTS', '龙颜-温暖春风女', 'longyan_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0007', 'TTS_AliBLStreamTTS', '龙菲菲-甜美矫情女', 'longfeifei_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 方言
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0008', 'TTS_AliBLStreamTTS', '龙老铁-东北直率男', 'longlaotie_v2', '中文(东北)及中英文混合', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0009', 'TTS_AliBLStreamTTS', '龙嘉怡-知性粤语女', 'longjiayi_v2', '中文(粤语)及中英文混合', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 童声
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0010', 'TTS_AliBLStreamTTS', '龙杰力豆-阳光顽皮男', 'longjielidou_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0011', 'TTS_AliBLStreamTTS', '龙铃-稚气呆板女', 'longling_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 诗歌朗诵
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0012', 'TTS_AliBLStreamTTS', '李白-古代诗仙男', 'libai_v2', '中文及中英文混合', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 出海营销
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0013', 'TTS_AliBLStreamTTS', 'loongeva-知性英文女', 'loongeva_v2', '英式英文', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0014', 'TTS_AliBLStreamTTS', 'loongbrian-沉稳英文男', 'loongbrian_v2', '英式英文', NULL, NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0015', 'TTS_AliBLStreamTTS', 'loongkyong-韩语女', 'loongkyong_v2', '韩语', NULL, NULL, NULL, NULL, 15, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0016', 'TTS_AliBLStreamTTS', 'loongtomoka-日语女', 'loongtomoka_v2', '日语', NULL, NULL, NULL, NULL, 16, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliBLStreamTTS_0017', 'TTS_AliBLStreamTTS', 'loongtomoya-日语男', 'loongtomoya_v2', '日语', NULL, NULL, NULL, NULL, 17, NULL, NULL, NULL, NULL);
|
||||
@@ -0,0 +1,41 @@
|
||||
-- 添加讯飞流式TTS供应器
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_TTS_XunFeiStreamTTS';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_TTS_XunFeiStreamTTS', 'TTS', 'xunfei_stream', '讯飞流式语音合成', '[{"key":"app_id","label":"APP_ID","type":"string"},{"key":"api_secret","label":"API_Secret","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"sample_rate","label":"采样率","type":"number"},{"key": "volume", "type": "number", "label": "音量"},{"key": "speed", "type": "number", "label": "语速"},{"key": "pitch", "type": "number", "label": "音调"},{"key": "oral_level", "type": "number", "label": "口语化等级"},{"key": "spark_assist", "type": "number", "label": "是否口语化"},{"key": "stop_split", "type": "number", "label": "服务端拆句"},{"key": "remain", "type": "number", "label": "保留书面语"}]', 20, 1, NOW(), 1, NOW());
|
||||
|
||||
-- 添加讯飞流式TTS模型配置
|
||||
delete from `ai_model_config` where id = 'TTS_XunFeiStreamTTS';
|
||||
INSERT INTO `ai_model_config` VALUES ('TTS_XunFeiStreamTTS', 'TTS', 'XunFeiStreamTTS', '讯飞流式语音合成', 0, 1, '{\"type\": \"xunfei_stream\", \"app_id\": \"\", \"api_secret\": \"\", \"api_key\": \"\", \"output_dir\": \"tmp/\", \"voice\": \"x5_lingxiaoxuan_flow\", \"format\": \"raw\", \"sample_rate\": 24000, \"volume\": 50, \"speed\": 50, \"pitch\": 50, \"oral_level\": \"mid\", \"spark_assist\": 1, \"stop_split\": 0, \"remain\": 0}', NULL, NULL, 23, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新讯飞流式TTS配置说明
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://console.xfyun.cn/app/myapp',
|
||||
`remark` = '讯飞流式TTS说明:
|
||||
1. 登录讯飞语音技术平台 https://console.xfyun.cn/app/myapp 创建相关应用
|
||||
2. 选择需要的服务获取api相关配置 https://console.xfyun.cn/services/uts
|
||||
3. 为需要使用的应用(APPID)购买相关服务 例如:超拟人合成 https://console.xfyun.cn/services/uts
|
||||
5. 支持实时双流式通信,具有较低的延迟
|
||||
6. 支持口语化设置和音频参数调整 注意:V5音色不支持相关口语化配置
|
||||
7. 支持实时调节音量、语速、音调等参数
|
||||
' WHERE `id` = 'TTS_XunFeiStreamTTS';
|
||||
|
||||
-- 添加讯飞流式TTS音色
|
||||
delete from `ai_tts_voice` where tts_model_id = 'TTS_XunFeiStreamTTS';
|
||||
|
||||
-- 基础角色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0001', 'TTS_XunFeiStreamTTS', '聆小璇', 'x5_lingxiaoxuan_flow', '中文', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0002', 'TTS_XunFeiStreamTTS', '聆飞逸', 'x5_lingfeiyi_flow', '中文', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0003', 'TTS_XunFeiStreamTTS', '聆小玥', 'x5_lingxiaoyue_flow', '中文', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0004', 'TTS_XunFeiStreamTTS', '聆玉昭', 'x5_lingyuzhao_flow', '中文', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0005', 'TTS_XunFeiStreamTTS', '聆玉言', 'x5_lingyuyan_flow', '中文', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 需要添加对应的角色音色
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0006', 'TTS_XunFeiStreamTTS', '聆飞哲', 'x4_lingfeizhe_oral', '中文', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0007', 'TTS_XunFeiStreamTTS', '聆小璃', 'x4_lingxiaoli_oral', '中文', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0008', 'TTS_XunFeiStreamTTS', '聆小糖', 'x5_lingxiaotang_flow', '中文', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0009', 'TTS_XunFeiStreamTTS', '聆小琪', 'x4_lingxiaoqi_oral', '中文', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0010', 'TTS_XunFeiStreamTTS', '聆佑佑-童年女声', 'x4_lingyouyou_oral', '中文', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0011', 'TTS_XunFeiStreamTTS', '子津', 'x4_zijin_oral', '天津话', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0012', 'TTS_XunFeiStreamTTS', '子阳', 'x4_ziyang_oral', '东北话', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0013', 'TTS_XunFeiStreamTTS', 'Grant', 'x5_EnUs_Grant_flow', '英文', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_XunFeiStreamTTS_0014', 'TTS_XunFeiStreamTTS', 'Lila', 'x5_EnUs_Lila_flow', '英文', NULL, NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
|
||||
@@ -0,0 +1,26 @@
|
||||
-- 添加讯飞流式语音识别服务配置
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_ASR_XunfeiStream';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_ASR_XunfeiStream', 'ASR', 'xunfei_stream', '讯飞流式语音识别', '[{"key":"app_id","label":"应用ID","type":"string"},{"key":"api_key","label":"API_KEY","type":"password"},{"key":"api_secret","label":"API_SECRET","type":"password"},{"key":"domain","label":"识别领域","type":"string"},{"key":"language","label":"识别语言","type":"string"},{"key":"accent","label":"方言","type":"string"},{"key":"dwa","label":"动态修正","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 18, 1, NOW(), 1, NOW());
|
||||
|
||||
delete from `ai_model_config` where id = 'ASR_XunfeiStream';
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_XunfeiStream', 'ASR', '讯飞流式语音识别', '讯飞流式语音识别服务', 0, 1, '{"type": "xunfei_stream", "app_id": "", "api_key": "", "api_secret": "", "domain": "slm", "language": "zh_cn", "accent": "mandarin", "dwa": "wpgs", "output_dir": "tmp/"}', 'https://www.xfyun.cn/doc/spark/spark_zh_iat.html', '支持实时流式语音识别,适用于中文普通话及多种方言识别', 21, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新讯飞流式语音识别模型配置的说明文档
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://www.xfyun.cn/doc/spark/spark_zh_iat.html',
|
||||
`remark` = '讯飞流式语音识别配置说明:
|
||||
1. 登录讯飞开放平台 https://www.xfyun.cn/
|
||||
2. 创建语音识别应用获取APPID、APISecret、APIKey
|
||||
3. 参数说明:
|
||||
- app_id: 应用ID,在讯飞开放平台创建应用后获得
|
||||
- api_key: API密钥,用于接口鉴权
|
||||
- api_secret: API密钥,用于生成签名
|
||||
- domain: 识别领域,默认slm(智能化语音转写)
|
||||
- language: 识别语言,默认zh_cn(中文)
|
||||
- accent: 方言类型,默认mandarin(普通话),支持cantonese(粤语)等
|
||||
- dwa: 动态修正,默认wpgs(开启动态修正)
|
||||
- output_dir: 音频文件输出目录,默认tmp/
|
||||
4. 支持实时流式识别,适用于实时语音交互场景
|
||||
5. 支持多种方言和语言识别
|
||||
' WHERE `id` = 'ASR_XunfeiStream';
|
||||
@@ -0,0 +1,19 @@
|
||||
delete from `ai_model_config` where id = 'LLM_XunfeiSparkLLM';
|
||||
INSERT INTO `ai_model_config` VALUES ('LLM_XunfeiSparkLLM', 'LLM', '讯飞星火认知大模型', '讯飞星火认知大模型', 0, 1, '{"type": "openai", "model_name": "generalv3.5", "base_url": "https://spark-api-open.xf-yun.com/v1", "api_password": "你的api_password", "temperature": 0.5, "max_tokens": 2048, "top_p": 1.0, "frequency_penalty": 0.0}', 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html', '讯飞星火认知大模型,支持多轮对话、文本生成等功能', 14, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新讯飞星火认知大模型配置的说明文档
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://www.xfyun.cn/doc/spark/HTTP%E8%B0%83%E7%94%A8%E6%96%87%E6%A1%A3.html',
|
||||
`remark` = '讯飞星火认知大模型配置说明:
|
||||
1. 登录讯飞开放平台 https://www.xfyun.cn/,每一个模型对应每一个api_password,更改模型时需要查看对应模型的api_password
|
||||
2. 创建星火认知大模型应用获取API Password
|
||||
3. 参数说明:
|
||||
- api_password: API Password,在讯飞开放平台创建应用后获得
|
||||
- model_name: 模型名称,支持generalv3.5、generalv3等版本
|
||||
- base_url: API地址,默认https://spark-api-open.xf-yun.com/v1
|
||||
- temperature: 温度参数,控制生成随机性,范围0-1,默认0.5
|
||||
- max_tokens: 最大输出token数,默认2048
|
||||
- top_p: 核心采样参数,控制词汇多样性,默认1.0
|
||||
- frequency_penalty: 频率惩罚,降低重复内容,默认0.0
|
||||
4. 每一个模型对应每一个api_password,更改模型时需要查看对应模型的api_password。
|
||||
' WHERE `id` = 'LLM_XunfeiSparkLLM';
|
||||
@@ -0,0 +1,7 @@
|
||||
-- 添加SM2国密算法密钥参数
|
||||
-- 用于服务器端SM2加密解密功能
|
||||
|
||||
-- 添加SM2密钥参数
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES
|
||||
(120, 'server.public_key', '', 'string', 1, '服务器SM2公钥'),
|
||||
(121, 'server.private_key', '', 'string', 1, '服务器SM2私钥');
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user