diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index 88f7144336..877ba77453 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.util.AiUtils; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; @@ -238,7 +239,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // 2. 构建 ChatOptions 对象 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), - conversation.getTemperature(), conversation.getMaxTokens()); + conversation.getTemperature(), conversation.getMaxTokens(), + SetUtils.asSet("directory_list", "weather_query")); return new Prompt(chatMessages, chatOptions); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java deleted file mode 100644 index 4315e549ae..0000000000 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java +++ /dev/null @@ -1,95 +0,0 @@ -package cn.iocoder.yudao.module.ai.service.tool; - -import cn.hutool.core.date.LocalDateTimeUtil; -import cn.hutool.core.io.FileUtil; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.StrUtil; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.springframework.ai.tool.annotation.Tool; -import org.springframework.ai.tool.annotation.ToolParam; -import org.springframework.stereotype.Component; - -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; - -/** - * 目录内容列表工具:列出指定目录的内容 - * - * @author 芋道源码 - */ -@Component -public class ListDirTool { - - /** - * 列出指定目录的内容 - * - * @param relativePath 要列出内容的目录路径,相对于工作区根目录 - * @return 目录内容列表 - */ - @Tool(name = "listDir", description = "列出指定目录的内容") - public Response listDir(@ToolParam(description = "要列出内容的目录路径,相对于工作区根目录") String relativePath) { - // 校验目录存在 - String path = StrUtil.blankToDefault(relativePath, "."); - Path dirPath = Paths.get(path); - if (!FileUtil.exist(dirPath.toString()) || !FileUtil.isDirectory(dirPath.toString())) { - return new Response(Collections.emptyList()); - } - // 列出目录内容 - File[] files = dirPath.toFile().listFiles(); - if (ArrayUtil.isEmpty(files)) { - return new Response(Collections.emptyList()); - } - return new Response(convertList(Arrays.asList(files), file -> new Response.File() - .setDirectory(file.isDirectory()).setName(file.getName()) - .setSize(file.isFile() ? FileUtil.readableFileSize(file.length()) : null) - .setLastModified( - LocalDateTimeUtil.format(LocalDateTimeUtil.of(file.lastModified()), NORM_DATETIME_PATTERN)))); - } - - @Data - @AllArgsConstructor - @NoArgsConstructor - public static class Response { - - /** - * 目录内容列表 - */ - private List files; - - @Data - public static class File { - - /** - * 是否为目录 - */ - private Boolean directory; - - /** - * 名称 - */ - private String name; - - /** - * 大小,仅对文件有效 - */ - private String size; - - /** - * 最后修改时间 - */ - private String lastModified; - - } - - } - -} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/DirectoryListToolFunction.java similarity index 74% rename from yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java rename to yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/DirectoryListToolFunction.java index 3eafff4b2c..0ec86724ec 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/function/DirectoryListToolFunction.java @@ -1,10 +1,11 @@ -package cn.iocoder.yudao.module.ai.service.tool; +package cn.iocoder.yudao.module.ai.service.tool.function; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; import lombok.AllArgsConstructor; import lombok.Data; @@ -12,8 +13,6 @@ import lombok.NoArgsConstructor; import org.springframework.stereotype.Component; import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -23,21 +22,22 @@ import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; /** - * 目录内容列表工具:列出指定目录的内容 + * 工具:列出指定目录的文件列表 * * @author 芋道源码 */ -@Component("listDir") -public class ListDirToolB implements Function { +@Component("directory_list") +public class DirectoryListToolFunction implements Function { @Data - @JsonClassDescription("列出指定目录的内容") + @JsonClassDescription("列出指定目录的文件列表") public static class Request { /** - * 要列出内容的目录路径 + * 目录路径 */ - @JsonPropertyDescription("要列出内容的目录路径,例如说:/Users/yunai") + @JsonProperty(required = true, value = "path") + @JsonPropertyDescription("目录路径,例如说:/Users/yunai") private String path; } @@ -48,7 +48,7 @@ public class ListDirToolB implements Function files; @@ -82,13 +82,12 @@ public class ListDirToolB implements Function { + + private static final String[] WEATHER_CONDITIONS = { "晴朗", "多云", "阴天", "小雨", "大雨", "雷雨", "小雪", "大雪" }; + + @Data + @JsonClassDescription("查询指定城市的天气信息") + public static class Request { + + /** + * 城市名称 + */ + @JsonProperty(required = true, value = "city") + @JsonPropertyDescription("城市名称,例如:北京、上海、广州") + private String city; + + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Response { + + /** + * 城市名称 + */ + private String city; + + /** + * 天气信息 + */ + private WeatherInfo weatherInfo; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class WeatherInfo { + + /** + * 温度(摄氏度) + */ + private Integer temperature; + + /** + * 天气状况 + */ + private String condition; + + /** + * 湿度百分比 + */ + private Integer humidity; + + /** + * 风速(km/h) + */ + private Integer windSpeed; + + /** + * 查询时间 + */ + private String queryTime; + + } + + } + + @Override + public Response apply(Request request) { + // 检查城市名称是否为空 + if (StrUtil.isBlank(request.getCity())) { + return new Response("未知城市", null); + } + + // 获取天气数据 + String city = request.getCity(); + Response.WeatherInfo weatherInfo = generateMockWeatherInfo(); + return new Response(city, weatherInfo); + } + + /** + * 生成模拟的天气数据 + * 在实际应用中,应替换为真实 API 调用 + */ + private Response.WeatherInfo generateMockWeatherInfo() { + int temperature = RandomUtil.randomInt(-5, 30); + int humidity = RandomUtil.randomInt(1, 100); + int windSpeed = RandomUtil.randomInt(1, 30); + String condition = RandomUtil.randomEle(WEATHER_CONDITIONS); + return new Response.WeatherInfo(temperature, condition, humidity, windSpeed, + LocalDateTimeUtil.format(LocalDateTime.now(), NORM_DATETIME_PATTERN)); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java new file mode 100644 index 0000000000..ba65093ecc --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.module.ai.service.tool; \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java index 720d933087..ef3314a48b 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.framework.ai.config; import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; @@ -17,6 +18,7 @@ import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStore import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties; import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.TokenCountBatchingStrategy; +import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; @@ -70,6 +72,7 @@ public class YudaoAiAutoConfiguration { .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) + .toolCallingManager(getToolCallingManager()) .build(); return new DeepSeekChatModel(openAiChatModel); } @@ -96,6 +99,7 @@ public class YudaoAiAutoConfiguration { .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) + .toolCallingManager(getToolCallingManager()) .build(); return new DouBaoChatModel(openAiChatModel); } @@ -122,6 +126,7 @@ public class YudaoAiAutoConfiguration { .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) + .toolCallingManager(getToolCallingManager()) .build(); return new SiliconFlowChatModel(openAiChatModel); } @@ -155,6 +160,7 @@ public class YudaoAiAutoConfiguration { .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) + .toolCallingManager(getToolCallingManager()) .build(); return new HunYuanChatModel(openAiChatModel); } @@ -181,6 +187,7 @@ public class YudaoAiAutoConfiguration { .maxTokens(properties.getMaxTokens()) .topP(properties.getTopP()) .build()) + .toolCallingManager(getToolCallingManager()) .build(); return new XingHuoChatModel(openAiChatModel); } @@ -210,4 +217,8 @@ public class YudaoAiAutoConfiguration { return new TokenCountBatchingStrategy(); } + private static ToolCallingManager getToolCallingManager() { + return SpringUtil.getBean(ToolCallingManager.class); + } + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java index 8565294b0c..356715be26 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -23,6 +23,7 @@ import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; +import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; @@ -58,11 +59,14 @@ import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.image.ImageModel; import org.springframework.ai.minimax.MiniMaxChatModel; +import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.minimax.MiniMaxEmbeddingModel; import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; import org.springframework.ai.minimax.api.MiniMaxApi; +import org.springframework.ai.model.function.FunctionCallbackResolver; import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.moonshot.MoonshotChatModel; +import org.springframework.ai.moonshot.MoonshotChatOptions; import org.springframework.ai.moonshot.api.MoonshotApi; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaEmbeddingModel; @@ -90,10 +94,7 @@ import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservat import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention; import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; import org.springframework.ai.vectorstore.redis.RedisVectorStore; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingModel; -import org.springframework.ai.zhipuai.ZhiPuAiEmbeddingOptions; -import org.springframework.ai.zhipuai.ZhiPuAiImageModel; +import org.springframework.ai.zhipuai.*; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; import org.springframework.beans.BeansException; @@ -110,6 +111,7 @@ import java.util.Timer; import java.util.TimerTask; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static org.springframework.ai.retry.RetryUtils.DEFAULT_RETRY_TEMPLATE; /** * AI Model 模型工厂的实现类 @@ -308,7 +310,9 @@ public class AiModelFactoryImpl implements AiModelFactory { */ private static DashScopeChatModel buildTongYiChatModel(String key) { DashScopeApi dashScopeApi = new DashScopeApi(key); - return new DashScopeChatModel(dashScopeApi); + DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL) + .withTemperature(0.7).build(); + return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); } /** @@ -385,7 +389,8 @@ public class AiModelFactoryImpl implements AiModelFactory { private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) : new ZhiPuAiApi(url, apiKey); - return new ZhiPuAiChatModel(zhiPuAiApi); + ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); + return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); } /** @@ -403,7 +408,8 @@ public class AiModelFactoryImpl implements AiModelFactory { private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey) : new MiniMaxApi(url, apiKey); - return new MiniMaxChatModel(miniMaxApi); + MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); + return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); } /** @@ -412,7 +418,8 @@ public class AiModelFactoryImpl implements AiModelFactory { private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey) : new MoonshotApi(url, apiKey); - return new MoonshotChatModel(moonshotApi); + MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build(); + return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); } /** @@ -449,7 +456,7 @@ public class AiModelFactoryImpl implements AiModelFactory { // 获取 AzureOpenAiChatProperties 对象 AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class); return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, - null, null, null); + getToolCallingManager(), null, null); } /** @@ -704,4 +711,8 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(ToolCallingManager.class); } + private static FunctionCallbackResolver getFunctionCallbackResolver() { + return SpringUtil.getBean(FunctionCallbackResolver.class); + } + } diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index b15fb2e54d..becc54ee43 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -13,6 +13,8 @@ import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.qianfan.QianFanChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; +import java.util.Set; + /** * Spring AI 工具类 * @@ -21,22 +23,27 @@ import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; public class AiUtils { public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) { + return buildChatOptions(platform, model, temperature, maxTokens, null); + } + + public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens, + Set toolNames) { // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: - // TODO functions - return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens).build(); + return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) + .withFunctions(toolNames).build(); case YI_YAN: return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: - // TODO functions - return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); + return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .functions(toolNames).build(); case MINI_MAX: - // TODO functions - return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); + return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .functions(toolNames).build(); case MOONSHOT: - // TODO functions - return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); + return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) + .functions(toolNames).build(); case OPENAI: case DEEP_SEEK: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端 @@ -44,17 +51,14 @@ public class AiUtils { case XING_HUO: // 复用 OpenAI 客户端 case SILICON_FLOW: // 复用 OpenAI 客户端 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) -// .toolNames() TODO - .toolNames("listDir") - .build(); + .toolNames(toolNames).build(); case AZURE_OPENAI: // TODO 芋艿:貌似没 model 字段???! - // TODO 芋艿:.toolNames() TODO - return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build(); + return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens) + .toolNames(toolNames).build(); case OLLAMA: - // TODO 芋艿:.toolNames() TODO return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) - .toolNames("listDir").build(); + .toolNames(toolNames).build(); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); }