10 小时以前 8f3bf7050e65fdbe55eaad74fde307c57dab960e
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java
@@ -8,22 +8,23 @@
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.qianfan.QianFanChatOptions;
import cn.hutool.extra.spring.SpringUtil;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.google.genai.GoogleGenAiChatOptions;
import org.springframework.ai.ollama.api.OllamaChatOptions;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import org.springframework.core.env.Environment;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.API_CONFIG_PLACEHOLDER_NOT_RESOLVED;
/**
 * Spring AI 工具类
@@ -36,13 +37,42 @@
    public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID";
    /**
     * 解析 DB 等动态配置里的 Spring 占位符,例如 ${OPENAI_API_KEY}
     *
     * @param value 待解析的配置值
     * @return 解析后的配置值
     */
    public static String resolveSpringPlaceholders(String value) {
        if (StrUtil.isBlank(value) || !StrUtil.contains(value, "${")) {
            return value;
        }
        try {
            return SpringUtil.getBean(Environment.class).resolveRequiredPlaceholders(value);
        } catch (IllegalArgumentException ex) {
            throw exception(API_CONFIG_PLACEHOLDER_NOT_RESOLVED, value);
        }
    }
    /**
     * 校验 API Key,避免集成测试使用默认占位值发起调用。
     *
     * @param apiKey API Key
     */
    public static void validateApiKey(String apiKey) {
        if (StrUtil.isBlank(apiKey) || "sk-xxxx".equals(apiKey)) {
            throw new IllegalStateException("apiKey 不能为空");
        }
    }
    /**
     * 通义千问支持多模态的模型
     *
     * @see <a href="https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-market/all?providers=qwen&capabilities=VU">模型广场</a>
     * @see <a href="https://help.aliyun.com/zh/model-studio/error-code#error-url">必须开启 withMultiModel 参数</a>
     */
    public static final Set<String> TONG_YI_MULTI_MODELS = SetUtils.asSet(
            // qwen3.5 / 3.6 系列(统一多模态主干)
            // qwen3.5 / 3.6 / 3.7 系列(统一多模态主干)
            "qwen3.7-max", "qwen3.7-plus", "qwen3.7-flash",
            "qwen3.6-plus", "qwen3.6-flash",
            "qwen3.5-plus", "qwen3.5-flash",
            // qwen-vl 视觉理解
@@ -72,32 +102,29 @@
                        .enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
                        .multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型
                        .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
            case YI_YAN:
                return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
            case DEEP_SEEK:
            case DOU_BAO: // 复用 DeepSeek 客户端
            case HUN_YUAN: // 复用 DeepSeek 客户端
            case SILICON_FLOW: // 复用 DeepSeek 客户端
            case YI_YAN: // 复用 DeepSeek 客户端
            case ZHI_PU: // 复用 DeepSeek 客户端
            case XING_HUO: // 复用 DeepSeek 客户端
            case MINI_MAX: // 复用 DeepSeek 客户端
            case MOONSHOT: // 复用 DeepSeek 客户端
            case BAI_CHUAN: // 复用 DeepSeek 客户端
            case STEP_FUN: // 复用 DeepSeek 客户端
                return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
                        .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
            case ZHI_PU:
                return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
                        .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
            case MINI_MAX:
                return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
                        .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
            case MOONSHOT:
                return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
                        .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
            case OPENAI:
            case GEMINI: // 复用 OpenAI 客户端
            case BAI_CHUAN: // 复用 OpenAI 客户端
            case GROK: // 复用 OpenAI 客户端
                return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
                        .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
            case GEMINI:
                return GoogleGenAiChatOptions.builder().model(model).temperature(temperature).maxOutputTokens(maxTokens)
                        .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
            case AZURE_OPENAI:
                return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens)
                return OpenAiChatOptions.builder().model(model).deploymentName(model).azure(true)
                        .temperature(temperature).maxTokens(maxTokens)
                        .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
            case ANTHROPIC:
                return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
@@ -159,4 +186,4 @@
        return MapUtil.getStr(output.getMetadata(), "reasoningContent");
    }
}
}