Browse Source

fix(model-config): AI 大模型配置管理全面排查修复(20 项)

后端修复:
- 修复 updateConfig 激活逻辑竞态条件:appType 变更时只停用目标类型的活跃配置
- 修复 getConfigDetail 直接修改 MyBatis 缓存实体的安全隐患,改为防御性拷贝
- 修复 persistExtraConfig 异常被静默吞掉的问题,改为抛出异常触发事务回滚
- 修复 deleteConfig 缺失 @Transactional 注解
- 修复 updatePriority 未校验配置存在性
- 修复 updateConfig 空 apiKey(trim 后)会覆盖已有值的问题
- createConfig 增加 provider 字段非空校验
- 导入接口增加 100 条数据量限制,导出接口移除 maskApiKey 参数强制脱敏
- ChatModelFactory.testConnection 使用专用线程池替代 ForkJoinPool.commonPool
- EmbeddingModelFactory RetryTemplate 显式设置最大重试次数
- ModelConfigLoader 增加 RERANK 类型启动校验
- RERANK 健康状态从误报 ONLINE 改为 UNKNOWN
- listConfigs 从原生 SQL 重构为 MyBatis Plus LambdaQueryWrapper

前端修复:
- 新建模式切换提供商时始终重置 model_name 为新提供商默认模型
- 编辑模式切换提供商时清理不属于新提供商的无效 model_name
- extraConfig 始终发送(含空对象),确保后端能清空已废弃的高级参数
- 关闭导入弹窗时重置预览和结果状态
- 健康看板支持 UNKNOWN 状态(🟡 未检测)展示
dev-mcp
wanghanlin 4 weeks ago
parent
commit
43449bc59a
  1. 7
      src/main/java/com/wok/supportbot/config/ChatModelFactory.java
  2. 2
      src/main/java/com/wok/supportbot/config/EmbeddingModelFactory.java
  3. 1
      src/main/java/com/wok/supportbot/config/ModelConfigLoader.java
  4. 11
      src/main/java/com/wok/supportbot/config/ModelHealthService.java
  5. 19
      src/main/java/com/wok/supportbot/controller/AiModelConfigController.java
  6. 105
      src/main/java/com/wok/supportbot/service/AiModelConfigService.java
  7. 44
      src/main/resources/static/components/ModelConfigManager.js
  8. 6
      src/main/resources/static/js/api.js

7
src/main/java/com/wok/supportbot/config/ChatModelFactory.java

@ -22,6 +22,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -237,6 +239,9 @@ public class ChatModelFactory {
/** 连接测试超时时间(秒) */ /** 连接测试超时时间(秒) */
private static final int TEST_CONNECTION_TIMEOUT_SECONDS = 10; private static final int TEST_CONNECTION_TIMEOUT_SECONDS = 10;
/** 连接测试专用线程池,避免占用 ForkJoinPool.commonPool() */
private final ExecutorService testConnectionExecutor = Executors.newFixedThreadPool(3);
/** /**
* 测试指定配置的连接可用性 * 测试指定配置的连接可用性
* 创建临时 ChatModel不走缓存发送一条简单 prompt 验证 API Key 和模型是否可用 * 创建临时 ChatModel不走缓存发送一条简单 prompt 验证 API Key 和模型是否可用
@ -270,7 +275,7 @@ public class ChatModelFactory {
result.put("errorMessage", e.getMessage()); result.put("errorMessage", e.getMessage());
} }
return result; return result;
});
}, testConnectionExecutor);
return future.get(TEST_CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS); return future.get(TEST_CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} catch (java.util.concurrent.TimeoutException e) { } catch (java.util.concurrent.TimeoutException e) {
Map<String, Object> result = new LinkedHashMap<>(); Map<String, Object> result = new LinkedHashMap<>();

2
src/main/java/com/wok/supportbot/config/EmbeddingModelFactory.java

@ -205,6 +205,8 @@ public class EmbeddingModelFactory {
*/ */
private RetryTemplate createRetryTemplate() { private RetryTemplate createRetryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate(); RetryTemplate retryTemplate = new RetryTemplate();
// 显式设置最大重试次数避免依赖隐式默认值
retryTemplate.setRetryPolicy(new org.springframework.retry.policy.SimpleRetryPolicy(3));
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy(); ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000); backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMultiplier(2); backOffPolicy.setMultiplier(2);

1
src/main/java/com/wok/supportbot/config/ModelConfigLoader.java

@ -30,6 +30,7 @@ public class ModelConfigLoader implements ApplicationListener<ApplicationReadyEv
checkAppTypeConfig("CHAT"); checkAppTypeConfig("CHAT");
checkAppTypeConfig("EMBEDDING"); checkAppTypeConfig("EMBEDDING");
checkAppTypeConfig("RAG_REWRITE"); checkAppTypeConfig("RAG_REWRITE");
checkAppTypeConfig("RERANK");
log.info("========== AI 模型配置校验完成 =========="); log.info("========== AI 模型配置校验完成 ==========");
} catch (Exception e) { } catch (Exception e) {
log.warn("模型配置校验异常(不影响启动): {}", e.getMessage()); log.warn("模型配置校验异常(不影响启动): {}", e.getMessage());

11
src/main/java/com/wok/supportbot/config/ModelHealthService.java

@ -125,7 +125,8 @@ public class ModelHealthService {
return embeddingModelFactory.testConnection(config); return embeddingModelFactory.testConnection(config);
} else if ("RERANK".equals(appType)) { } else if ("RERANK".equals(appType)) {
// RERANK 暂无通用测试方式标记为 UNKNOWN // RERANK 暂无通用测试方式标记为 UNKNOWN
return Map.of("success", true, "latencyMs", 0);
return Map.of("success", false, "status", "UNKNOWN",
"errorMessage", "RERANK 类型暂不支持自动探活");
} else { } else {
// CHAT / RAG_REWRITE // CHAT / RAG_REWRITE
return chatModelFactory.testConnection(config); return chatModelFactory.testConnection(config);
@ -140,7 +141,13 @@ public class ModelHealthService {
*/ */
private void updateHealthStatus(Long configId, Map<String, Object> result) { private void updateHealthStatus(Long configId, Map<String, Object> result) {
HealthStatus status = new HealthStatus(); HealthStatus status = new HealthStatus();
// 支持显式 UNKNOWN 状态 RERANK 类型暂不支持探活
Object explicitStatus = result.get("status");
if ("UNKNOWN".equals(explicitStatus)) {
status.setStatus("UNKNOWN");
} else {
status.setStatus(Boolean.TRUE.equals(result.get("success")) ? "ONLINE" : "OFFLINE"); status.setStatus(Boolean.TRUE.equals(result.get("success")) ? "ONLINE" : "OFFLINE");
}
Object latency = result.get("latencyMs"); Object latency = result.get("latencyMs");
status.setLatencyMs(latency instanceof Number ? ((Number) latency).longValue() : null); status.setLatencyMs(latency instanceof Number ? ((Number) latency).longValue() : null);
status.setLastCheckTime(new Date()); status.setLastCheckTime(new Date());
@ -204,9 +211,11 @@ public class ModelHealthService {
// 添加汇总统计 // 添加汇总统计
long online = healthCache.values().stream().filter(s -> "ONLINE".equals(s.getStatus())).count(); long online = healthCache.values().stream().filter(s -> "ONLINE".equals(s.getStatus())).count();
long offline = healthCache.values().stream().filter(s -> "OFFLINE".equals(s.getStatus())).count(); long offline = healthCache.values().stream().filter(s -> "OFFLINE".equals(s.getStatus())).count();
long unknown = healthCache.values().stream().filter(s -> "UNKNOWN".equals(s.getStatus())).count();
Map<String, Object> summary = new LinkedHashMap<>(); Map<String, Object> summary = new LinkedHashMap<>();
summary.put("online", online); summary.put("online", online);
summary.put("offline", offline); summary.put("offline", offline);
summary.put("unknown", unknown);
summary.put("total", (long) healthCache.size()); summary.put("total", (long) healthCache.size());
result.put("_summary", summary); result.put("_summary", summary);

19
src/main/java/com/wok/supportbot/controller/AiModelConfigController.java

@ -173,6 +173,12 @@ public class AiModelConfigController {
"message", "API Key 不能为空" "message", "API Key 不能为空"
)); ));
} }
if (config.getProvider() == null || config.getProvider().trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "提供商不能为空"
));
}
AiModelConfig created = aiModelConfigService.createConfig(config); AiModelConfig created = aiModelConfigService.createConfig(config);
refreshCache(); refreshCache();
@ -435,16 +441,13 @@ public class AiModelConfigController {
// ==================== F7: 配置导入/导出 ==================== // ==================== F7: 配置导入/导出 ====================
/** /**
* 导出所有配置为 JSON
*
* @param maskApiKey 是否脱敏 API Key默认 true
* 导出所有配置为 JSONAPI Key 始终脱敏
*/ */
@GetMapping("/model-config/export") @GetMapping("/model-config/export")
@PreAuthorize("hasRole('admin')") @PreAuthorize("hasRole('admin')")
public ResponseEntity<Map<String, Object>> exportConfigs(
@RequestParam(defaultValue = "true") boolean maskApiKey) {
public ResponseEntity<Map<String, Object>> exportConfigs() {
try { try {
List<AiModelConfig> configs = aiModelConfigService.exportConfigs(maskApiKey);
List<AiModelConfig> configs = aiModelConfigService.exportConfigs(true);
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"success", true, "success", true,
"data", configs, "data", configs,
@ -471,6 +474,10 @@ public class AiModelConfigController {
return ResponseEntity.badRequest().body(Map.of( return ResponseEntity.badRequest().body(Map.of(
"success", false, "message", "导入数据不能为空")); "success", false, "message", "导入数据不能为空"));
} }
if (rawConfigs.size() > 100) {
return ResponseEntity.badRequest().body(Map.of(
"success", false, "message", "单次导入不能超过 100 条配置"));
}
String onConflict = body.getOrDefault("onConflict", "skip").toString(); String onConflict = body.getOrDefault("onConflict", "skip").toString();

105
src/main/java/com/wok/supportbot/service/AiModelConfigService.java

@ -42,39 +42,45 @@ public class AiModelConfigService {
* @return 分页结果 * @return 分页结果
*/ */
public Map<String, Object> listConfigs(String appType, int page, int size) { public Map<String, Object> listConfigs(String appType, int page, int size) {
// 构建查询条件
StringBuilder whereClause = new StringBuilder("WHERE is_delete = false ");
List<Object> params = new ArrayList<>();
// 构建查询条件共用基础条件
LambdaQueryWrapper<AiModelConfig> baseCondition = new LambdaQueryWrapper<>();
if (appType != null && !appType.isEmpty()) { if (appType != null && !appType.isEmpty()) {
whereClause.append(" AND app_type = ? ");
params.add(appType);
baseCondition.eq(AiModelConfig::getAppType, appType);
} }
// 查询总数
String countSql = "SELECT COUNT(*) FROM ai_model_config " + whereClause;
Long total = jdbcTemplate.queryForObject(countSql, Long.class, params.toArray());
// 查询总数不含排序和分页
Long total = aiModelConfigMapper.selectCount(baseCondition);
if (total == null) total = 0L; if (total == null) total = 0L;
// 查询列表 priority 降序create_time 降序
String listSql = "SELECT * FROM ai_model_config " + whereClause +
" ORDER BY priority DESC, create_time DESC LIMIT ? OFFSET ?";
List<Object> queryParams = new ArrayList<>(params);
queryParams.add(size);
queryParams.add((page - 1) * size);
List<Map<String, Object>> records = jdbcTemplate.queryForList(listSql, queryParams.toArray());
// 查询列表带排序和分页
LambdaQueryWrapper<AiModelConfig> listWrapper = new LambdaQueryWrapper<>();
if (appType != null && !appType.isEmpty()) {
listWrapper.eq(AiModelConfig::getAppType, appType);
}
listWrapper.orderByDesc(AiModelConfig::getPriority)
.orderByDesc(AiModelConfig::getCreateTime);
listWrapper.last("LIMIT " + size + " OFFSET " + ((page - 1) * size));
List<AiModelConfig> records = aiModelConfigMapper.selectList(listWrapper);
// 脱敏 API Key 并格式化结果
// 脱敏 API Key
List<Map<String, Object>> formattedRecords = new ArrayList<>(); List<Map<String, Object>> formattedRecords = new ArrayList<>();
for (Map<String, Object> record : records) {
Map<String, Object> formatted = new LinkedHashMap<>(record);
// API Key 脱敏
String apiKey = (String) record.get("api_key");
if (apiKey != null) {
formatted.put("api_key", maskApiKey(apiKey));
}
for (AiModelConfig record : records) {
Map<String, Object> formatted = new LinkedHashMap<>();
formatted.put("id", record.getId());
formatted.put("name", record.getName());
formatted.put("app_type", record.getAppType());
formatted.put("provider", record.getProvider());
formatted.put("model_name", record.getModelName());
formatted.put("temperature", record.getTemperature());
formatted.put("max_tokens", record.getMaxTokens());
formatted.put("base_url", record.getBaseUrl());
formatted.put("api_key", maskApiKey(record.getApiKey()));
formatted.put("extra_config", record.getExtraConfig());
formatted.put("is_active", record.getIsActive());
formatted.put("priority", record.getPriority());
formatted.put("description", record.getDescription());
formatted.put("create_time", record.getCreateTime());
formatted.put("update_time", record.getUpdateTime());
formattedRecords.add(formatted); formattedRecords.add(formatted);
} }
@ -98,9 +104,27 @@ public class AiModelConfigService {
public AiModelConfig getConfigDetail(Long id) { public AiModelConfig getConfigDetail(Long id) {
AiModelConfig config = aiModelConfigMapper.selectById(id); AiModelConfig config = aiModelConfigMapper.selectById(id);
if (config != null) { if (config != null) {
config.setApiKey(maskApiKey(config.getApiKey()));
// 防御性拷贝不直接修改 MyBatis 一级缓存中的实体对象避免污染后续查询
AiModelConfig masked = AiModelConfig.builder()
.id(config.getId())
.name(config.getName())
.appType(config.getAppType())
.provider(config.getProvider())
.apiKey(maskApiKey(config.getApiKey()))
.modelName(config.getModelName())
.temperature(config.getTemperature())
.maxTokens(config.getMaxTokens())
.baseUrl(config.getBaseUrl())
.extraConfig(config.getExtraConfig())
.isActive(config.getIsActive())
.priority(config.getPriority())
.description(config.getDescription())
.createTime(config.getCreateTime())
.updateTime(config.getUpdateTime())
.build();
return masked;
} }
return config;
return null;
} }
// ==================== 获取活跃配置 ==================== // ==================== 获取活跃配置 ====================
@ -197,22 +221,21 @@ public class AiModelConfigService {
.build(); .build();
validateModelTypeMatch(merged); validateModelTypeMatch(merged);
// 如果要激活此配置先禁用同类型的其他配置
// 如果要激活此配置先禁用目标类型的其他配置
// 目标类型 = appType如有修改或原 appType
if (Boolean.TRUE.equals(config.getIsActive())) { if (Boolean.TRUE.equals(config.getIsActive())) {
deactivateByAppType(existing.getAppType());
}
// 如果 app_type 被修改且新配置为活跃需要禁用新类型的其他配置
if (config.getAppType() != null && !config.getAppType().equals(existing.getAppType())
&& Boolean.TRUE.equals(config.getIsActive())) {
deactivateByAppType(config.getAppType());
String targetAppType = config.getAppType() != null ? config.getAppType() : existing.getAppType();
deactivateByAppType(targetAppType);
} }
// 设置 ID 确保更新正确 // 设置 ID 确保更新正确
config.setId(id); config.setId(id);
// 防御trim API Key 前后空白
// 防御trim API Key 前后空白空字符串置为 null 避免覆盖已有值
if (config.getApiKey() != null) { if (config.getApiKey() != null) {
config.setApiKey(config.getApiKey().trim()); config.setApiKey(config.getApiKey().trim());
if (config.getApiKey().isEmpty()) {
config.setApiKey(null);
}
} }
aiModelConfigMapper.updateById(config); aiModelConfigMapper.updateById(config);
// JSONB 字段 MyBatis Plus typeHandler 可能不触发 JdbcTemplate 显式写入保证可靠 // JSONB 字段 MyBatis Plus typeHandler 可能不触发 JdbcTemplate 显式写入保证可靠
@ -278,6 +301,7 @@ public class AiModelConfigService {
* *
* @param id 配置ID * @param id 配置ID
*/ */
@Transactional(rollbackFor = Exception.class)
public void deleteConfig(Long id) { public void deleteConfig(Long id) {
AiModelConfig config = aiModelConfigMapper.selectById(id); AiModelConfig config = aiModelConfigMapper.selectById(id);
if (config == null) { if (config == null) {
@ -362,7 +386,8 @@ public class AiModelConfigService {
json, id); json, id);
log.debug("持久化 extraConfig: id={}, extraConfig={}", id, json); log.debug("持久化 extraConfig: id={}, extraConfig={}", id, json);
} catch (Exception e) { } catch (Exception e) {
log.warn("持久化 extraConfig 失败: id={}, error={}", id, e.getMessage());
log.error("持久化 extraConfig 失败: id={}, error={}", id, e.getMessage());
throw new RuntimeException("持久化扩展配置失败: " + e.getMessage(), e);
} }
} }
@ -450,6 +475,10 @@ public class AiModelConfigService {
* @param priority 新优先级值 * @param priority 新优先级值
*/ */
public void updatePriority(Long id, Integer priority) { public void updatePriority(Long id, Integer priority) {
AiModelConfig config = aiModelConfigMapper.selectById(id);
if (config == null) {
throw new RuntimeException("配置不存在");
}
LambdaUpdateWrapper<AiModelConfig> updateWrapper = new LambdaUpdateWrapper<>(); LambdaUpdateWrapper<AiModelConfig> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(AiModelConfig::getId, id) updateWrapper.eq(AiModelConfig::getId, id)
.set(AiModelConfig::getPriority, priority); .set(AiModelConfig::getPriority, priority);

44
src/main/resources/static/components/ModelConfigManager.js

@ -148,6 +148,10 @@ export default {
<div style="font-size:22px;font-weight:700;color:#dc2626;">{{ healthSummary.offline }}</div> <div style="font-size:22px;font-weight:700;color:#dc2626;">{{ healthSummary.offline }}</div>
<div style="font-size:11px;color:#991b1b;">🔴 离线</div> <div style="font-size:11px;color:#991b1b;">🔴 离线</div>
</div> </div>
<div v-if="healthSummary.unknown" style="padding:10px 16px;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;min-width:100px;text-align:center;">
<div style="font-size:22px;font-weight:700;color:#d97706;">{{ healthSummary.unknown }}</div>
<div style="font-size:11px;color:#92400e;">🟡 未检测</div>
</div>
<div style="padding:10px 16px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;min-width:100px;text-align:center;"> <div style="padding:10px 16px;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;min-width:100px;text-align:center;">
<div style="font-size:22px;font-weight:700;color:#374151;">{{ healthSummary.total }}</div> <div style="font-size:22px;font-weight:700;color:#374151;">{{ healthSummary.total }}</div>
<div style="font-size:11px;color:#6b7280;">📊 总计</div> <div style="font-size:11px;color:#6b7280;">📊 总计</div>
@ -186,7 +190,7 @@ export default {
<!-- F4: 健康状态指示 --> <!-- F4: 健康状态指示 -->
<span v-if="healthMap[item.id]" :title="getHealthTooltip(item.id)" <span v-if="healthMap[item.id]" :title="getHealthTooltip(item.id)"
:style="{ color: healthMap[item.id].status === 'ONLINE' ? '#16a34a' : '#dc2626' }"> :style="{ color: healthMap[item.id].status === 'ONLINE' ? '#16a34a' : '#dc2626' }">
{{ healthMap[item.id].status === 'ONLINE' ? '🟢' : '🔴' }}
{{ healthMap[item.id].status === 'ONLINE' ? '🟢' : healthMap[item.id].status === 'UNKNOWN' ? '🟡' : '🔴' }}
<span v-if="healthMap[item.id].latencyMs" style="font-size:10px;">{{ healthMap[item.id].latencyMs }}ms</span> <span v-if="healthMap[item.id].latencyMs" style="font-size:10px;">{{ healthMap[item.id].latencyMs }}ms</span>
</span> </span>
<span v-else style="color:#d1d5db;" title="未检测">🟡</span> <span v-else style="color:#d1d5db;" title="未检测">🟡</span>
@ -245,8 +249,8 @@ export default {
<!-- 健康指示器 --> <!-- 健康指示器 -->
<span v-if="c.is_active && healthMap[c.id]" <span v-if="c.is_active && healthMap[c.id]"
:title="getHealthTooltip(c.id)" :title="getHealthTooltip(c.id)"
:style="{ cursor: 'help', color: healthMap[c.id].status === 'ONLINE' ? '#16a34a' : '#dc2626' }">
{{ healthMap[c.id].status === 'ONLINE' ? '●' : '●' }}
:style="{ cursor: 'help', color: healthMap[c.id].status === 'ONLINE' ? '#16a34a' : healthMap[c.id].status === 'UNKNOWN' ? '#d97706' : '#dc2626' }">
{{ healthMap[c.id].status === 'ONLINE' ? '●' : healthMap[c.id].status === 'UNKNOWN' ? '●' : '●' }}
<span v-if="healthMap[c.id].latencyMs" style="font-size:10px;">{{ healthMap[c.id].latencyMs }}ms</span> <span v-if="healthMap[c.id].latencyMs" style="font-size:10px;">{{ healthMap[c.id].latencyMs }}ms</span>
</span> </span>
<span v-else-if="c.is_active" style="color:#d1d5db;cursor:help;" title="未检测"></span> <span v-else-if="c.is_active" style="color:#d1d5db;cursor:help;" title="未检测"></span>
@ -467,7 +471,7 @@ export default {
<!-- F7: 导入弹窗 --> <!-- F7: 导入弹窗 -->
<div class="modal-overlay" :class="{ active: showImportDialog }"> <div class="modal-overlay" :class="{ active: showImportDialog }">
<div class="modal-box" style="max-width:600px;"> <div class="modal-box" style="max-width:600px;">
<button class="modal-close" @click="showImportDialog = false">×</button>
<button class="modal-close" @click="closeImportDialog">×</button>
<h2>📥 导入模型配置</h2> <h2>📥 导入模型配置</h2>
<div style="margin:16px 0;"> <div style="margin:16px 0;">
<label style="font-size:13px;font-weight:600;display:block;margin-bottom:8px;">选择 JSON 文件</label> <label style="font-size:13px;font-weight:600;display:block;margin-bottom:8px;">选择 JSON 文件</label>
@ -499,7 +503,7 @@ export default {
</div> </div>
</div> </div>
<div style="display:flex;gap:10px;margin-top:20px;justify-content:flex-end;"> <div style="display:flex;gap:10px;margin-top:20px;justify-content:flex-end;">
<button class="btn btn-outline" @click="showImportDialog = false">关闭</button>
<button class="btn btn-outline" @click="closeImportDialog">关闭</button>
<button class="btn btn-primary" @click="doImport" :disabled="!importPreview">📥 确认导入</button> <button class="btn btn-primary" @click="doImport" :disabled="!importPreview">📥 确认导入</button>
</div> </div>
</div> </div>
@ -593,8 +597,18 @@ export default {
const models = isEmbedding ? (defaults.embeddingModels || []) : (defaults.chatModels || []) const models = isEmbedding ? (defaults.embeddingModels || []) : (defaults.chatModels || [])
const defaultModel = isEmbedding ? (defaults.defaultEmbeddingModel || '') : (defaults.defaultChatModel || '') const defaultModel = isEmbedding ? (defaults.defaultEmbeddingModel || '') : (defaults.defaultChatModel || '')
if (editModal.value.mode === 'add' && !editModal.value.form.model_name) {
editModal.value.form.model_name = defaultModel
// 新建模式:始终重置 model_name 为新提供商的默认模型
if (editModal.value.mode === 'add') {
editModal.value.form.model_name = defaultModel || ''
} else {
// 编辑模式:如果当前 model_name 不在新提供商的模型列表中,重置为默认模型
const currentModel = editModal.value.form.model_name
if (currentModel && currentModel !== '__custom__' && models.length > 0) {
const found = models.find(m => m.name === currentModel)
if (!found) {
editModal.value.form.model_name = defaultModel || ''
}
}
} }
// F5: 使用对象格式的模型列表 // F5: 使用对象格式的模型列表
@ -671,7 +685,7 @@ export default {
function getHealthTooltip(configId) { function getHealthTooltip(configId) {
const h = healthMap.value[configId] const h = healthMap.value[configId]
if (!h) return '未检测' if (!h) return '未检测'
let tip = h.status === 'ONLINE' ? '在线' : '离线'
let tip = h.status === 'ONLINE' ? '在线' : h.status === 'UNKNOWN' ? '未支持自动检测' : '离线'
if (h.latencyMs) tip += ` · 延迟 ${h.latencyMs}ms` if (h.latencyMs) tip += ` · 延迟 ${h.latencyMs}ms`
if (h.lastCheckTime) tip += ` · 最后检测 ${new Date(h.lastCheckTime).toLocaleTimeString()}` if (h.lastCheckTime) tip += ` · 最后检测 ${new Date(h.lastCheckTime).toLocaleTimeString()}`
if (h.errorMessage) tip += ` · ${h.errorMessage}` if (h.errorMessage) tip += ` · ${h.errorMessage}`
@ -793,7 +807,7 @@ export default {
isActive: form.is_active, isActive: form.is_active,
description: form.description description: form.description
} }
// 构建 extraConfig
// 构建 extraConfig(始终发送,以便后端清空已废弃的高级参数)
const extraConfig = {} const extraConfig = {}
if (form.app_type === 'EMBEDDING') { if (form.app_type === 'EMBEDDING') {
extraConfig.dimensions = form.embeddingDimensions || 1024 extraConfig.dimensions = form.embeddingDimensions || 1024
@ -805,9 +819,7 @@ export default {
if (form.presencePenalty != null) extraConfig.presencePenalty = form.presencePenalty if (form.presencePenalty != null) extraConfig.presencePenalty = form.presencePenalty
if (form.stopSequences && form.stopSequences.trim()) extraConfig.stopSequences = form.stopSequences.trim() if (form.stopSequences && form.stopSequences.trim()) extraConfig.stopSequences = form.stopSequences.trim()
if (Object.keys(extraConfig).length > 0) {
data.extraConfig = extraConfig data.extraConfig = extraConfig
}
return data return data
} }
@ -973,7 +985,7 @@ export default {
async function exportConfigs() { async function exportConfigs() {
try { try {
const json = await api.exportModelConfigs(true)
const json = await api.exportModelConfigs()
if (json.success) { if (json.success) {
const blob = new Blob([JSON.stringify(json.data, null, 2)], { type: 'application/json' }) const blob = new Blob([JSON.stringify(json.data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
@ -1029,6 +1041,12 @@ export default {
} }
} }
function closeImportDialog() {
showImportDialog.value = false
importPreview.value = null
importResult.value = null
}
// ==================== 工具函数 ==================== // ==================== 工具函数 ====================
function getAppTypeLabel(appType) { function getAppTypeLabel(appType) {
@ -1076,7 +1094,7 @@ export default {
fallbackChains, onDragStart, onDrop, deactivateConfig, fallbackChains, onDragStart, onDrop, deactivateConfig,
// F7 // F7
showImportDialog, importPreview, importConflict, importResult, showImportDialog, importPreview, importConflict, importResult,
onImportFileSelect, doImport, exportConfigs,
onImportFileSelect, doImport, closeImportDialog, exportConfigs,
// 基础 // 基础
load, openAddModal, openEditModal, closeEditModal, saveConfig, activate, remove, load, openAddModal, openEditModal, closeEditModal, saveConfig, activate, remove,
onProviderChange, getAppTypeLabel, getAppTypeBadgeClass, getProviderLabel, formatDate onProviderChange, getAppTypeLabel, getAppTypeBadgeClass, getProviderLabel, formatDate

6
src/main/resources/static/js/api.js

@ -627,10 +627,10 @@ export function deactivateModelConfig(id) {
// ==================== F7: 配置导入/导出 ==================== // ==================== F7: 配置导入/导出 ====================
/** /**
* 导出所有模型配置
* 导出所有模型配置API Key 始终脱敏
*/ */
export function exportModelConfigs(maskApiKey = true) {
return getJSON(`/model-config/export?maskApiKey=${maskApiKey}`)
export function exportModelConfigs() {
return getJSON('/model-config/export')
} }
/** /**

Loading…
Cancel
Save