package com.vincent.rsf.server.ai.service; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.vincent.rsf.server.ai.constant.AiSceneCode; import com.vincent.rsf.server.system.entity.AiModelRoute; import com.vincent.rsf.server.system.service.AiModelRouteService; import lombok.Data; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @Service public class AiModelRouteRuntimeService { @Resource private AiRuntimeConfigService aiRuntimeConfigService; @Resource private AiModelRouteService aiModelRouteService; @Resource private com.vincent.rsf.server.ai.config.AiProperties aiProperties; /** * 为一次聊天/诊断请求解析候选模型列表。 * 顺序是:会话首选模型 -> 当前场景可用路由 -> 默认模型。 */ public List resolveCandidates(Long tenantId, String sceneCode, String preferredModelCode) { List output = new ArrayList<>(); Set seen = new LinkedHashSet<>(); if (preferredModelCode != null && !preferredModelCode.trim().isEmpty()) { RouteCandidate preferredCandidate = toCandidate(null, preferredModelCode, seen); if (preferredCandidate != null) { output.add(preferredCandidate); } } String routeCode = resolveRouteCode(sceneCode); for (AiModelRoute route : aiModelRouteService.listAvailableRoutes(tenantId, routeCode)) { RouteCandidate candidate = toCandidate(route, route.getModelCode(), seen); if (candidate != null) { output.add(candidate); } } if (output.isEmpty()) { RouteCandidate defaultCandidate = toCandidate(null, aiRuntimeConfigService.resolveDefaultModelCode(), seen); if (defaultCandidate != null) { output.add(defaultCandidate); } } return output; } /** * 标记某条模型路由本次调用成功,并清空失败计数和冷却状态。 */ public void markSuccess(Long routeId) { if (routeId == null) { return; } aiModelRouteService.update(new LambdaUpdateWrapper() .setSql("success_count = IFNULL(success_count, 0) + 1") .set(AiModelRoute::getFailCount, 0) .set(AiModelRoute::getCooldownUntil, null) .eq(AiModelRoute::getId, routeId)); } /** * 标记某条模型路由本次调用失败。 * 连续失败达到阈值后会进入冷却期,避免短时间内持续命中故障模型。 */ public void markFailure(Long routeId) { if (routeId == null) { return; } AiModelRoute route = aiModelRouteService.getById(routeId); if (route == null) { return; } int nextFailCount = (route.getFailCount() == null ? 0 : route.getFailCount()) + 1; Date cooldownUntil = route.getCooldownUntil(); if (nextFailCount >= aiProperties.getRouteFailThreshold()) { cooldownUntil = new Date(System.currentTimeMillis() + aiProperties.getRouteCooldownMinutes() * 60_000L); } route.setFailCount(nextFailCount); route.setCooldownUntil(cooldownUntil); aiModelRouteService.updateById(route); } /** * 手动重置一条路由的成功/失败统计和冷却时间。 */ public void resetRoute(Long routeId) { if (routeId == null) { return; } aiModelRouteService.update(new LambdaUpdateWrapper() .set(AiModelRoute::getFailCount, 0) .set(AiModelRoute::getSuccessCount, 0) .set(AiModelRoute::getCooldownUntil, null) .eq(AiModelRoute::getId, routeId)); } /** * 将聊天场景统一映射成路由组编码。 */ private String resolveRouteCode(String sceneCode) { if (AiSceneCode.SYSTEM_DIAGNOSE.equals(sceneCode)) { return AiSceneCode.SYSTEM_DIAGNOSE; } return AiSceneCode.GENERAL_CHAT; } /** * 基于模型编码和路由记录组装运行时候选项,并负责去重与可用性校验。 */ private RouteCandidate toCandidate(AiModelRoute route, String modelCode, Set seen) { if (modelCode == null || modelCode.trim().isEmpty() || seen.contains(modelCode)) { return null; } AiRuntimeConfigService.ModelRuntimeConfig runtimeConfig = aiRuntimeConfigService.resolveModel(modelCode); if (runtimeConfig == null || !Boolean.TRUE.equals(runtimeConfig.getEnabled())) { return null; } seen.add(modelCode); RouteCandidate candidate = new RouteCandidate(); candidate.setRouteId(route == null ? null : route.getId()); candidate.setRouteCode(route == null ? null : route.getRouteCode()); candidate.setAttemptModelCode(runtimeConfig.getCode()); candidate.setRuntimeConfig(runtimeConfig); return candidate; } @Data public static class RouteCandidate { private Long routeId; private String routeCode; private String attemptModelCode; private AiRuntimeConfigService.ModelRuntimeConfig runtimeConfig; } }