mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
@@ -0,0 +1,164 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 敏感数据处理工具类
|
||||
*/
|
||||
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)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-8
@@ -70,7 +70,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
// 查询默认智能体
|
||||
AgentTemplateEntity agent = agentTemplateService.getDefaultTemplate();
|
||||
if (agent == null) {
|
||||
throw new RenException(ErrorCode.DEFAULT_AGENT_NOT_FOUND);
|
||||
throw new RenException("默认智能体未找到");
|
||||
}
|
||||
|
||||
// 构建模块配置
|
||||
@@ -113,7 +113,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
// 获取智能体信息
|
||||
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
||||
if (agent == null) {
|
||||
throw new RenException(ErrorCode.AGENT_NOT_FOUND);
|
||||
throw new RenException("智能体未找到");
|
||||
}
|
||||
// 获取音色信息
|
||||
String voice = null;
|
||||
@@ -350,7 +350,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
* @param result 结果Map
|
||||
*/
|
||||
private void buildModuleConfig(
|
||||
String assistantName,
|
||||
String assistantName,
|
||||
String prompt,
|
||||
String summaryMemory,
|
||||
String voice,
|
||||
@@ -366,17 +366,18 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
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 intentLLMModelId = null;
|
||||
String memLocalShortLLMModelId = null;
|
||||
|
||||
|
||||
for (int i = 0; i < modelIds.length; i++) {
|
||||
if (modelIds[i] == null) {
|
||||
continue;
|
||||
}
|
||||
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache);
|
||||
// 关键:第三个参数传false,确保获取原始密钥
|
||||
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache, false);
|
||||
if (model == null) {
|
||||
continue;
|
||||
}
|
||||
@@ -424,14 +425,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.getModelById(intentLLMModelId, isCache, false);
|
||||
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
|
||||
}
|
||||
}
|
||||
if (StringUtils.isNotBlank(memLocalShortLLMModelId)) {
|
||||
if (!typeConfig.containsKey(memLocalShortLLMModelId)) {
|
||||
// 修改这里:添加isMaskSensitive=false参数
|
||||
ModelConfigEntity memLocalShortLLM = modelConfigService
|
||||
.getModelById(memLocalShortLLMModelId, isCache);
|
||||
.getModelById(memLocalShortLLMModelId, isCache, false);
|
||||
typeConfig.put(memLocalShortLLM.getId(), memLocalShortLLM.getConfigJson());
|
||||
}
|
||||
}
|
||||
|
||||
+15
-5
@@ -35,17 +35,27 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
||||
/**
|
||||
* 根据ID获取模型配置
|
||||
*
|
||||
* @param id 模型ID
|
||||
* @param isCache 是否缓存
|
||||
* @param id 模型ID
|
||||
* @param isCache 是否缓存
|
||||
* @param isMaskSensitive 是否掩码敏感信息
|
||||
* @return 模型配置实体
|
||||
*/
|
||||
ModelConfigEntity getModelById(String id, boolean isCache, boolean isMaskSensitive);
|
||||
|
||||
/**
|
||||
* 根据ID获取模型配置(默认掩码敏感信息)
|
||||
*
|
||||
* @param id 模型ID
|
||||
* @param isCache 是否缓存
|
||||
* @return 模型配置实体
|
||||
*/
|
||||
ModelConfigEntity getModelById(String id, boolean isCache);
|
||||
|
||||
|
||||
/**
|
||||
* 设置默认模型
|
||||
*
|
||||
* @param modelType 模型类型
|
||||
* @param isDefault 是否默认
|
||||
* @param modelType 模型类型
|
||||
* @param isDefault 是否默认(1:是,0:否)
|
||||
*/
|
||||
void setDefaultModel(String modelType, int isDefault);
|
||||
}
|
||||
|
||||
+362
-113
@@ -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;
|
||||
@@ -11,10 +12,11 @@ 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.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
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;
|
||||
@@ -24,6 +26,7 @@ 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;
|
||||
@@ -65,7 +68,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());
|
||||
@@ -78,102 +81,400 @@ 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);
|
||||
|
||||
// 不再使用默认的getPage方法,而是直接创建Page对象并自定义排序
|
||||
|
||||
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(
|
||||
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(ErrorCode.MODEL_TYPE_PROVIDE_CODE_NOT_NULL);
|
||||
}
|
||||
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||
if (CollectionUtil.isEmpty(providerList)) {
|
||||
throw new RenException(ErrorCode.MODEL_PROVIDER_NOT_EXIST);
|
||||
}
|
||||
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(ErrorCode.MODEL_TYPE_PROVIDE_CODE_NOT_NULL);
|
||||
}
|
||||
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||
if (CollectionUtil.isEmpty(providerList)) {
|
||||
throw new RenException(ErrorCode.MODEL_PROVIDER_NOT_EXIST);
|
||||
}
|
||||
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(ErrorCode.LLM_NOT_EXIST);
|
||||
}
|
||||
String type = modelConfigEntity.getConfigJson().get("type").toString();
|
||||
// 如果查询大语言模型是openai或者ollama,意图识别选参数都可以
|
||||
if (!"openai".equals(type) && !"ollama".equals(type)) {
|
||||
throw new RenException(ErrorCode.INVALID_LLM_TYPE);
|
||||
}
|
||||
}
|
||||
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(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 getModelById(String id, boolean isCache) {
|
||||
return getModelById(id, isCache, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelConfigEntity getModelById(String id, boolean isCache, boolean isMaskSensitive) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ModelConfigEntity entity = null;
|
||||
String cacheKey = RedisKeys.getModelConfigById(id);
|
||||
|
||||
if (isCache) {
|
||||
// 从缓存获取
|
||||
entity = (ModelConfigEntity) redisUtils.get(cacheKey);
|
||||
if (entity != null) {
|
||||
// 修改:根据isMaskSensitive参数重新处理敏感信息
|
||||
if (!isMaskSensitive && entity.getConfigJson() != null) {
|
||||
// 如果需要获取原始配置,但缓存的是掩码后的配置,则从数据库重新获取
|
||||
ModelConfigEntity originalEntity = modelConfigDao.selectById(id);
|
||||
if (originalEntity != null) {
|
||||
entity.setConfigJson(originalEntity.getConfigJson());
|
||||
}
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
// 从数据库获取数据
|
||||
entity = modelConfigDao.selectById(id);
|
||||
if (entity != null) {
|
||||
// 根据isMaskSensitive参数决定是否掩码敏感信息
|
||||
if (isMaskSensitive && entity.getConfigJson() != null) {
|
||||
entity.setConfigJson(maskSensitiveFields(entity.getConfigJson()));
|
||||
}
|
||||
|
||||
if (isCache) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认模型
|
||||
*/
|
||||
@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(
|
||||
@@ -201,8 +502,6 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
|
||||
/**
|
||||
* 检查意图识别配置是否有引用
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
private void checkIntentConfigReference(String modelId) {
|
||||
ModelConfigEntity modelConfig = modelConfigDao.selectById(modelId);
|
||||
@@ -217,54 +516,4 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelNameById(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String cachedName = (String) redisUtils.get(RedisKeys.getModelNameById(id));
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
custom-class="custom-dialog" :show-close="false" class="center-dialog">
|
||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||
{{ $t('modelConfigDialog.addModel') }}
|
||||
</div>
|
||||
{{ $t('modelConfigDialog.addModel') }}
|
||||
</div>
|
||||
|
||||
<button class="custom-close-btn" @click="handleClose">
|
||||
×
|
||||
@@ -29,37 +29,43 @@
|
||||
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
|
||||
<el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')" class="custom-input-bg"></el-input>
|
||||
<el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="modelCode" style="flex: 1;">
|
||||
<el-input v-model="formData.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')" class="custom-input-bg"></el-input>
|
||||
<el-input v-model="formData.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
|
||||
<el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')" class="custom-select custom-input-bg"
|
||||
style="width: 100%;" @focus="loadProviders" filterable>
|
||||
<el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')"
|
||||
class="custom-select custom-input-bg" style="width: 100%;" @focus="loadProviders" filterable>
|
||||
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sortOrder" style="flex: 1;">
|
||||
<el-input v-model="formData.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')" class="custom-input-bg"></el-input>
|
||||
<el-input v-model="formData.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
|
||||
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docLink" style="margin-bottom: 27px;">
|
||||
<el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')" class="custom-input-bg"></el-input>
|
||||
<el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" :placeholder="$t('modelConfigDialog.enterRemark')" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3"
|
||||
:placeholder="$t('modelConfigDialog.enterRemark')" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">{{ $t('modelConfigDialog.callInfo') }}</div>
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">{{
|
||||
$t('modelConfigDialog.callInfo') }}</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
|
||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
||||
@@ -77,12 +83,7 @@
|
||||
</div>
|
||||
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="confirm"
|
||||
class="save-btn"
|
||||
:loading="saving"
|
||||
:disabled="saving">
|
||||
<el-button type="primary" @click="confirm" class="save-btn" :loading="saving" :disabled="saving">
|
||||
{{ $t('modelConfigDialog.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -159,7 +160,7 @@ export default {
|
||||
label: f.label,
|
||||
prop: f.key,
|
||||
type: f.type === 'password' ? 'password' : 'text',
|
||||
placeholder: `请输入${f.label}`
|
||||
placeholder: `请输入${f.key}`
|
||||
}))
|
||||
}))
|
||||
this.providersLoaded = true
|
||||
|
||||
@@ -1,98 +1,132 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="57%" center custom-class="custom-dialog" :show-close="false"
|
||||
class="center-dialog" >
|
||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||
{{ modelData.duplicateMode ? $t('modelConfigDialog.duplicateModel') : $t('modelConfigDialog.editModel') }}
|
||||
<el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="57%" center custom-class="custom-dialog"
|
||||
:show-close="false" class="center-dialog">
|
||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px">
|
||||
<div style="
|
||||
font-size: 30px;
|
||||
color: #3d4566;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
">
|
||||
{{
|
||||
modelData.duplicateMode
|
||||
? $t("modelConfigDialog.duplicateModel")
|
||||
: $t("modelConfigDialog.editModel")
|
||||
}}
|
||||
</div>
|
||||
|
||||
<button class="custom-close-btn" @click="dialogVisible = false">
|
||||
×
|
||||
</button>
|
||||
<button class="custom-close-btn" @click="dialogVisible = false">×</button>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">{{ $t('modelConfigDialog.modelInfo') }}</div>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.enable') }}</span>
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
">
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566">
|
||||
{{ $t("modelConfigDialog.modelInfo") }}
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 20px">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span style="margin-right: 8px">{{ $t("modelConfigDialog.enable") }}</span>
|
||||
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
<div style="display: none; align-items: center;">
|
||||
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.setDefault') }}</span>
|
||||
<div style="display: none; align-items: center">
|
||||
<span style="margin-right: 8px">{{
|
||||
$t("modelConfigDialog.setDefault")
|
||||
}}</span>
|
||||
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px"></div>
|
||||
|
||||
<el-form :model="form" ref="form" label-width="auto" label-position="left" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="name" style="flex: 1;">
|
||||
<el-input v-model="form.modelName" :placeholder="$t('modelConfigDialog.enterModelName')" class="custom-input-bg"></el-input>
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0">
|
||||
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="name" style="flex: 1">
|
||||
<el-input v-model="form.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="code" style="flex: 1;">
|
||||
<el-input v-model="form.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="code" style="flex: 1">
|
||||
<el-input v-model="form.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
|
||||
<el-select v-model="form.configJson.type" :placeholder="$t('modelConfigDialog.selectSupplier')" class="custom-select custom-input-bg"
|
||||
style="width: 100%;" @focus="loadProviders" filterable>
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0">
|
||||
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1">
|
||||
<el-select v-model="form.configJson.type" :placeholder="$t('modelConfigDialog.selectSupplier')"
|
||||
class="custom-select custom-input-bg" style="width: 100%" @focus="loadProviders" filterable>
|
||||
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sort" style="flex: 1;">
|
||||
<el-input v-model.number="form.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sort" style="flex: 1">
|
||||
<el-input v-model.number="form.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docUrl" style="margin-bottom: 27px;">
|
||||
<el-input v-model="form.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docUrl" style="margin-bottom: 27px">
|
||||
<el-input v-model="form.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="$t('modelConfigDialog.enterRemark')" :autosize="{ minRows: 3, maxRows: 5 }"
|
||||
class="custom-input-bg"></el-input>
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="$t('modelConfigDialog.enterRemark')"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">{{ $t('modelConfigDialog.callInfo') }}</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px">
|
||||
{{ $t("modelConfigDialog.callInfo") }}
|
||||
</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px"></div>
|
||||
|
||||
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
||||
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0">
|
||||
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||
style="flex: 1;">
|
||||
style="flex: 1">
|
||||
<template v-if="field.type === 'json-textarea'">
|
||||
<el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3" :placeholder="$t('modelConfigDialog.enterJsonExample')"
|
||||
class="custom-input-bg" @change="(val) => handleJsonChange(field.prop, val)"></el-input>
|
||||
<el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3"
|
||||
:placeholder="$t('modelConfigDialog.enterJsonExample')" class="custom-input-bg"
|
||||
@change="(val) => handleJsonChange(field.prop, val)" @focus="
|
||||
isSensitiveField(field.prop)
|
||||
? handleJsonInputFocus(field.prop, fieldJsonMap[field.prop])
|
||||
: undefined
|
||||
" @blur="
|
||||
isSensitiveField(field.prop)
|
||||
? handleJsonInputBlur(field.prop)
|
||||
: undefined
|
||||
"></el-input>
|
||||
</template>
|
||||
|
||||
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
||||
class="custom-input-bg" :show-password="field.type === 'password'"></el-input>
|
||||
class="custom-input-bg" :show-password="field.type === 'password'" @focus="
|
||||
isSensitiveField(field.prop)
|
||||
? handleInputFocus(field.prop, form.configJson[field.prop])
|
||||
: undefined
|
||||
" @blur="
|
||||
isSensitiveField(field.prop) ? handleInputBlur(field.prop) : undefined
|
||||
"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
class="save-btn"
|
||||
:loading="saving"
|
||||
:disabled="saving">
|
||||
{{ $t('modelConfigDialog.save') }}
|
||||
<div style="display: flex; justify-content: center">
|
||||
<el-button type="primary" @click="handleSave" class="save-btn" :loading="saving" :disabled="saving">
|
||||
{{ $t("modelConfigDialog.save") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import Api from "@/apis/api";
|
||||
|
||||
export default {
|
||||
name: "ModelEditDialog",
|
||||
@@ -101,9 +135,9 @@ export default {
|
||||
modelData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
validator: value => typeof value === 'object' && !Array.isArray(value)
|
||||
validator: (value) => typeof value === "object" && !Array.isArray(value),
|
||||
},
|
||||
modelType: { type: String, required: true }
|
||||
modelType: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -116,6 +150,16 @@ export default {
|
||||
pendingModelData: null,
|
||||
dynamicCallInfoFields: [],
|
||||
fieldJsonMap: {}, // 用于存储JSON字段的字符串形式
|
||||
sensitive_keys: [
|
||||
"api_key",
|
||||
"personal_access_token",
|
||||
"access_token",
|
||||
"token",
|
||||
"secret",
|
||||
"access_key_secret",
|
||||
"secret_key",
|
||||
],
|
||||
originalValues: {}, // 存储原始值,用于失焦时恢复
|
||||
form: {
|
||||
id: "",
|
||||
modelType: "",
|
||||
@@ -126,8 +170,8 @@ export default {
|
||||
docLink: "",
|
||||
remark: "",
|
||||
sort: 0,
|
||||
configJson: {}
|
||||
}
|
||||
configJson: {},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -146,7 +190,7 @@ export default {
|
||||
this.loadProviders();
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
this.$emit("update:visible", val);
|
||||
if (!val) {
|
||||
this.resetForm();
|
||||
} else if (val && this.modelData.id) {
|
||||
@@ -159,11 +203,11 @@ export default {
|
||||
this.loadProviders();
|
||||
}
|
||||
},
|
||||
'form.configJson.type'(newVal) {
|
||||
"form.configJson.type"(newVal) {
|
||||
if (newVal && this.providersLoaded) {
|
||||
this.loadProviderFields(newVal);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resetForm() {
|
||||
@@ -177,7 +221,7 @@ export default {
|
||||
docLink: "",
|
||||
remark: "",
|
||||
sort: 0,
|
||||
configJson: {}
|
||||
configJson: {},
|
||||
};
|
||||
this.fieldJsonMap = {};
|
||||
},
|
||||
@@ -189,11 +233,23 @@ export default {
|
||||
if (this.modelData.id) {
|
||||
Api.model.getModelConfig(this.modelData.id, ({ data }) => {
|
||||
if (data.code === 0 && data.data) {
|
||||
const model = data.data;
|
||||
let model = data.data;
|
||||
|
||||
if (this.modelData.duplicateMode) {
|
||||
model.modelName = this.modelData.modelName + this.$t('modelConfigDialog.copySuffix');
|
||||
model.modelCode = this.modelData.modelCode + this.$t('modelConfigDialog.copySuffix');
|
||||
model.modelName =
|
||||
this.modelData.modelName + this.$t("modelConfigDialog.copySuffix");
|
||||
model.modelCode =
|
||||
this.modelData.modelCode + this.$t("modelConfigDialog.copySuffix");
|
||||
|
||||
// 处理敏感字段
|
||||
if (model.configJson) {
|
||||
Object.keys(model.configJson).forEach((key) => {
|
||||
if (this.isSensitiveField(key) && model.configJson[key]) {
|
||||
const sensitiveName = this.getSensitiveFieldName(key);
|
||||
model.configJson[key] = `你的${sensitiveName}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this.pendingProviderType = model.configJson.type;
|
||||
this.pendingModelData = model;
|
||||
@@ -211,7 +267,7 @@ export default {
|
||||
this.saving = true; // 开始保存加载
|
||||
|
||||
// 处理所有JSON字段
|
||||
Object.keys(this.fieldJsonMap).forEach(key => {
|
||||
Object.keys(this.fieldJsonMap).forEach((key) => {
|
||||
const parsed = this.validateJson(this.fieldJsonMap[key]);
|
||||
if (parsed !== null) {
|
||||
this.form.configJson[key] = parsed;
|
||||
@@ -227,7 +283,7 @@ export default {
|
||||
docLink: this.form.docLink,
|
||||
remark: this.form.remark,
|
||||
sort: this.form.sort || 0,
|
||||
configJson: { ...this.form.configJson }
|
||||
configJson: { ...this.form.configJson },
|
||||
};
|
||||
|
||||
this.$emit("save", {
|
||||
@@ -235,7 +291,7 @@ export default {
|
||||
formData,
|
||||
done: () => {
|
||||
this.saving = false; // 保存完成后回调
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 如果父组件不处理done回调,3秒后自动关闭加载状态
|
||||
@@ -247,9 +303,9 @@ export default {
|
||||
if (this.providersLoaded) return;
|
||||
|
||||
Api.model.getModelProviders(this.modelType, (data) => {
|
||||
this.providers = data.map(item => ({
|
||||
this.providers = data.map((item) => ({
|
||||
label: item.name,
|
||||
value: String(item.providerCode)
|
||||
value: String(item.providerCode),
|
||||
}));
|
||||
this.providersLoaded = true;
|
||||
this.allProvidersData = data;
|
||||
@@ -261,13 +317,20 @@ export default {
|
||||
},
|
||||
loadProviderFields(providerCode) {
|
||||
if (this.allProvidersData) {
|
||||
const provider = this.allProvidersData.find(p => p.providerCode === providerCode);
|
||||
const provider = this.allProvidersData.find(
|
||||
(p) => p.providerCode === providerCode
|
||||
);
|
||||
if (provider) {
|
||||
this.dynamicCallInfoFields = JSON.parse(provider.fields || '[]').map(f => ({
|
||||
this.dynamicCallInfoFields = JSON.parse(provider.fields || "[]").map((f) => ({
|
||||
label: f.label,
|
||||
prop: f.key,
|
||||
type: f.type === 'dict' ? 'json-textarea' : (f.type === 'password' ? 'password' : 'text'),
|
||||
placeholder: `请输入${f.label}`
|
||||
type:
|
||||
f.type === "dict"
|
||||
? "json-textarea"
|
||||
: f.type === "password"
|
||||
? "password"
|
||||
: "text",
|
||||
placeholder: `请输入${f.key}`,
|
||||
}));
|
||||
|
||||
if (this.pendingModelData && this.pendingProviderType === providerCode) {
|
||||
@@ -280,13 +343,17 @@ export default {
|
||||
},
|
||||
processModelData(model) {
|
||||
let configJson = model.configJson || {};
|
||||
this.dynamicCallInfoFields.forEach(field => {
|
||||
this.dynamicCallInfoFields.forEach((field) => {
|
||||
if (!configJson.hasOwnProperty(field.prop)) {
|
||||
configJson[field.prop] = '';
|
||||
} else if (field.type === 'json-textarea') {
|
||||
this.$set(this.fieldJsonMap, field.prop, this.formatJson(configJson[field.prop]));
|
||||
configJson[field.prop] = "";
|
||||
} else if (field.type === "json-textarea") {
|
||||
this.$set(
|
||||
this.fieldJsonMap,
|
||||
field.prop,
|
||||
this.formatJson(configJson[field.prop])
|
||||
);
|
||||
configJson[field.prop] = this.ensureObject(configJson[field.prop]);
|
||||
} else if (typeof configJson[field.prop] !== 'string') {
|
||||
} else if (typeof configJson[field.prop] !== "string") {
|
||||
configJson[field.prop] = String(configJson[field.prop]);
|
||||
}
|
||||
});
|
||||
@@ -301,7 +368,7 @@ export default {
|
||||
docLink: model.docLink,
|
||||
remark: model.remark,
|
||||
sort: Number(model.sort) || 0,
|
||||
configJson: { ...configJson }
|
||||
configJson: { ...configJson },
|
||||
};
|
||||
},
|
||||
handleJsonChange(field, value) {
|
||||
@@ -313,18 +380,18 @@ export default {
|
||||
validateJson(value) {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
this.$message.error({
|
||||
message: '必须输入字典格式(如 {"key":"value"}),保存则使用原数据',
|
||||
showClose: true
|
||||
showClose: true,
|
||||
});
|
||||
return null;
|
||||
} catch (e) {
|
||||
this.$message.error({
|
||||
message: 'JSON格式错误(如 {"key":"value"}),保存则使用原数据',
|
||||
showClose: true
|
||||
showClose: true,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
@@ -333,13 +400,82 @@ export default {
|
||||
try {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
} catch {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
},
|
||||
ensureObject(value) {
|
||||
return typeof value === 'object' ? value : {};
|
||||
}
|
||||
}
|
||||
return typeof value === "object" ? value : {};
|
||||
},
|
||||
|
||||
// 检测字段是否为敏感字段
|
||||
isSensitiveField(fieldName) {
|
||||
// 将字段名转换为小写进行比较
|
||||
const lowerFieldName = fieldName.toLowerCase();
|
||||
// 精确匹配keyMap中定义的7个敏感词
|
||||
return this.sensitive_keys.includes(lowerFieldName);
|
||||
},
|
||||
|
||||
// 获取敏感字段对应的中文名称
|
||||
getSensitiveFieldName(fieldName) {
|
||||
const keyMap = {
|
||||
api_key: "API密钥",
|
||||
personal_access_token: "个人访问令牌",
|
||||
access_token: "访问令牌",
|
||||
token: "令牌",
|
||||
secret: "密钥",
|
||||
access_key_secret: "访问密钥",
|
||||
secret_key: "密钥",
|
||||
};
|
||||
|
||||
for (const [key, value] of Object.entries(keyMap)) {
|
||||
if (fieldName.toLowerCase().includes(key)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "敏感信息";
|
||||
},
|
||||
|
||||
// 处理input聚焦事件
|
||||
handleInputFocus(field, value) {
|
||||
// 如果值包含星号,清空显示
|
||||
if (value && value.includes("*")) {
|
||||
// 存储原始值,用于失焦时恢复
|
||||
this.$set(this.originalValues, field, this.form.configJson[field]);
|
||||
this.$set(this.form.configJson, field, "");
|
||||
}
|
||||
},
|
||||
|
||||
// 处理input失焦事件
|
||||
handleInputBlur(field) {
|
||||
// 检查是否为敏感字段
|
||||
if (this.isSensitiveField(field)) {
|
||||
// 如果值为空,恢复掩码值
|
||||
if (!this.form.configJson[field] || this.form.configJson[field].trim() === "") {
|
||||
// 如果有原始值,则恢复原始值;否则设置为掩码提示
|
||||
if (this.originalValues[field]) {
|
||||
this.$set(this.form.configJson, field, this.originalValues[field]);
|
||||
} else {
|
||||
const sensitiveName = this.getSensitiveFieldName(field);
|
||||
this.$set(this.form.configJson, field, `你的${sensitiveName}`);
|
||||
}
|
||||
// 清除临时存储的原始值
|
||||
this.$delete(this.originalValues, field);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 处理JSON字段的聚焦事件
|
||||
handleJsonInputFocus(field, value) {
|
||||
if (value && value.includes("*")) {
|
||||
this.$set(this.fieldJsonMap, field, "");
|
||||
}
|
||||
},
|
||||
|
||||
// 处理JSON字段的失焦事件
|
||||
handleJsonInputBlur(field) {
|
||||
// JSON字段不做特殊处理,因为它们通常不包含简单的敏感信息
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -385,8 +521,8 @@ export default {
|
||||
}
|
||||
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.custom-select .el-input__suffix {
|
||||
@@ -475,7 +611,7 @@ export default {
|
||||
background-color: white;
|
||||
top: 3px;
|
||||
left: 4px;
|
||||
transition: all .3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.custom-switch.is-checked .el-switch__core {
|
||||
@@ -508,4 +644,4 @@ export default {
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user