From 15f4005e1649950ec949af37f2d76bb4729c0127 Mon Sep 17 00:00:00 2001 From: orcasjh Date: Fri, 14 Aug 2026 20:54:27 +0000 Subject: [PATCH] =?UTF-8?q?feat(ai):=20=E6=96=B0=E5=A2=9E=20OrcaRouter=20A?= =?UTF-8?q?I=20=E4=BB=BB=E5=8A=A1=20Handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 AI 执行器示例中新增命名 OrcaRouter 集成,镜像现有 ollamaJobHandler 接法: - 新增 OrcaRouterChatModel(复用 Spring AI OpenAiChatModel,base-url 指向 OrcaRouter 网关 https://api.orcarouter.ai) - 新增 OrcaRouterConfig 配置 Bean(spring.ai.orcarouter.base-url / api-key / model) - AIXxlJob 新增 orcaRouterJobHandler 任务,支持自定义 prompt、input、model - application.properties 新增 orcarouter 配置示例 - XXL-JOB官方文档.md 新增 orcaRouterJobHandler 说明与配置示例 - 新增 OrcaRouterLiveTest 本地验证测试 Co-Authored-By: orcasjh Signed-off-by: orcasjh --- doc/XXL-JOB官方文档.md | 16 ++- .../executor/config/OrcaRouterChatModel.java | 41 +++++++ .../job/executor/config/OrcaRouterConfig.java | 62 ++++++++++ .../xxl/job/executor/jobhandler/AIXxlJob.java | 108 ++++++++++++++++++ .../src/main/resources/application.properties | 4 + .../test/orcarouter/OrcaRouterLiveTest.java | 48 ++++++++ 6 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/config/OrcaRouterChatModel.java create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/config/OrcaRouterConfig.java create mode 100644 xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/test/java/com/xxl/job/executor/test/orcarouter/OrcaRouterLiveTest.java diff --git a/doc/XXL-JOB官方文档.md b/doc/XXL-JOB官方文档.md index f61ad33d..6d79d3d0 100644 --- a/doc/XXL-JOB官方文档.md +++ b/doc/XXL-JOB官方文档.md @@ -1280,7 +1280,7 @@ public void demoJobHandler() throws Exception { - d、commandJobHandler:通用命令行任务Handler;业务方只需要提供命令行即可,命令及参数之间通过空格隔开;如任务参数 "ls la" 或 "pwd" 将会执行命令并输出数据; #### 原生内置Bean模式任务(AI执行器) -为方便用户参考与快速使用,提供 “AI执行器” 并内置多个Bean模式 AI任务Handler,与spring-ai、ollama、dify等集成打通,支持快速开发AI类任务,如下: +为方便用户参考与快速使用,提供 “AI执行器” 并内置多个Bean模式 AI任务Handler,与spring-ai、ollama、OrcaRouter、dify等集成打通,支持快速开发AI类任务,如下: **AI执行器说明:** - AppName:xxl-job-executor-sample-ai @@ -1317,6 +1317,15 @@ public void demoJobHandler() throws Exception { } ``` +- d、orcaRouterJobHandler: [OrcaRouter](https://www.orcarouter.ai)任务(OpenAI兼容的LLM路由网关),支持自定义prompt、input、model等输入信息。示例任务入参如下: +``` +{ + "input": "{输入信息,必填信息}", + "prompt": "{模型prompt,可选信息}", + "model": "{模型,如orcarouter/auto,可选信息}" +} +``` + - 依赖1:参考 [Ollama本地化部署大模型](https://www.xuxueli.com/blog/?blog=./notebook/13-AI/%E4%BD%BF%E7%94%A8Ollama%E6%9C%AC%E5%9C%B0%E5%8C%96%E9%83%A8%E7%BD%B2DeepSeek.md) ,执行器示例部署“qwen2.5:1.5b”模型,也可自定选择其他模型版本。 - 依赖2:参考 [使用DeepSeek与Dify搭建AI助手](https://www.xuxueli.com/blog/?blog=./notebook/13-AI/%E4%BD%BF%E7%94%A8DeepSeek%E4%B8%8EDify%E6%90%AD%E5%BB%BAAI%E5%8A%A9%E6%89%8B.md),执行器示例新建Dify DifyWork应用,并在开始节点添加“input”参数,可结合实际情况调整。 - 依赖3:启动示例 “AI执行器” 相关配置文件说明如下: @@ -1333,6 +1342,11 @@ spring.ai.ollama.chat.options.temperature=0.8 dify.base-url=http://localhost/v1 // dify api-key;选择相关 workflow 应用并进入 “访问API” 页面,右上角 “API 密钥” 入口获取 api-key。 dify.api-key={自行获取并修改} + +// orcarouter 配置;OrcaRouter 是 OpenAI 兼容的 LLM 路由网关,官网 https://www.orcarouter.ai +spring.ai.orcarouter.base-url=https://api.orcarouter.ai/v1 +spring.ai.orcarouter.api-key={自行获取并修改} +spring.ai.orcarouter.model=orcarouter/auto ``` ### 3.3 GLUE模式(Java) diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/config/OrcaRouterChatModel.java b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/config/OrcaRouterChatModel.java new file mode 100644 index 00000000..33c136dc --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/config/OrcaRouterChatModel.java @@ -0,0 +1,41 @@ +package com.xxl.job.executor.config; + +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import reactor.core.publisher.Flux; + +/** + * OrcaRouter ChatModel + * + * OrcaRouter 是 OpenAI 兼容的 LLM 路由网关,ChatModel 底层复用 Spring AI 的 OpenAiChatModel, + * 将 base-url 指向 OrcaRouter 网关地址(https://api.orcarouter.ai)即可接入。 + * + * @author orcasjh + */ +public class OrcaRouterChatModel implements ChatModel { + + private final OpenAiChatModel delegate; + + public OrcaRouterChatModel(OpenAiChatModel delegate) { + this.delegate = delegate; + } + + @Override + public ChatResponse call(Prompt prompt) { + return delegate.call(prompt); + } + + @Override + public Flux stream(Prompt prompt) { + return delegate.stream(prompt); + } + + @Override + public ChatOptions getOptions() { + return delegate.getOptions(); + } + +} diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/config/OrcaRouterConfig.java b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/config/OrcaRouterConfig.java new file mode 100644 index 00000000..d26ff004 --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/config/OrcaRouterConfig.java @@ -0,0 +1,62 @@ +package com.xxl.job.executor.config; + +import com.openai.client.OpenAIClient; +import com.openai.client.OpenAIClientAsync; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.setup.OpenAiSetup; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.util.List; + +/** + * OrcaRouter 配置 + * + * OrcaRouter(https://www.orcarouter.ai)是 OpenAI 兼容的 LLM 路由网关, + * 通过 Spring AI 的 OpenAiChatModel 接入,只需把 base-url 指向 OrcaRouter 网关地址。 + * + * 配置示例(application.properties): + *
+ *     spring.ai.orcarouter.base-url=https://api.orcarouter.ai/v1
+ *     spring.ai.orcarouter.api-key={自行获取并修改}
+ *     spring.ai.orcarouter.model=orcarouter/auto
+ * 
+ * + * @author orcasjh + */ +@Configuration +public class OrcaRouterConfig { + + @Bean + public OrcaRouterChatModel orcaRouterChatModel( + @Value("${spring.ai.orcarouter.base-url:https://api.orcarouter.ai/v1}") String baseUrl, + @Value("${spring.ai.orcarouter.api-key:}") String apiKey, + @Value("${spring.ai.orcarouter.model:orcarouter/auto}") String model) { + + // 构建 OrcaRouter OpenAI 兼容客户端(同步 + 异步) + ObservationRegistry observationRegistry = ObservationRegistry.create(); + MeterRegistry meterRegistry = new SimpleMeterRegistry(); + + OpenAIClient client = OpenAiSetup.setupSyncClient( + baseUrl, apiKey, null, null, null, null, + false, false, model, Duration.ofSeconds(30), 2, + null, null, observationRegistry, meterRegistry, List.of()); + OpenAIClientAsync clientAsync = OpenAiSetup.setupAsyncClient( + baseUrl, apiKey, null, null, null, null, + false, false, model, Duration.ofSeconds(30), 2, + null, null, observationRegistry, meterRegistry, List.of()); + + OpenAiChatModel inner = OpenAiChatModel.builder() + .openAiClient(client) + .openAiClientAsync(clientAsync) + .build(); + + return new OrcaRouterChatModel(inner); + } + +} diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/jobhandler/AIXxlJob.java b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/jobhandler/AIXxlJob.java index 34b0a16a..89448fbc 100644 --- a/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/jobhandler/AIXxlJob.java +++ b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/java/com/xxl/job/executor/jobhandler/AIXxlJob.java @@ -2,6 +2,7 @@ package com.xxl.job.executor.jobhandler; import com.xxl.job.core.context.XxlJobHelper; import com.xxl.job.core.handler.annotation.XxlJob; +import com.xxl.job.executor.config.OrcaRouterChatModel; import com.xxl.tool.json.GsonTool; import io.github.imfangs.dify.client.DifyClientFactory; import io.github.imfangs.dify.client.DifyWorkflowClient; @@ -17,6 +18,7 @@ import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.api.OllamaChatOptions; import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.stereotype.Component; import java.util.HashMap; @@ -36,6 +38,8 @@ public class AIXxlJob { private OllamaChatModel ollamaChatModel; @Resource private OpenAiChatModel openAiChatModel; + @Resource + private OrcaRouterChatModel orcaRouterChatModel; /** * 1、ollama Chat任务 @@ -345,4 +349,108 @@ public class AIXxlJob { } + // --------------------------------- orcarouter --------------------------------- + + /** + * 4、OrcaRouter 任务 + * + * OrcaRouter 是 OpenAI 兼容的 LLM 路由网关(https://www.orcarouter.ai), + * 通过 spring-ai 的 OpenAI 兼容路径接入,base-url 指向 https://api.orcarouter.ai。 + * + * 参数示例:格式见 OrcaRouterParam + *
+     *      {
+     *          "input": "{输入信息,必填信息}",
+     *          "prompt": "{模型prompt,可选信息}",
+     *          "model": "{模型,可选,默认 orcarouter/auto}"
+     *      }
+     *  
+ */ + @XxlJob("orcaRouterJobHandler") + public void orcaRouterJobHandler() { + + // param + String param = XxlJobHelper.getJobParam(); + if (param == null || param.trim().isEmpty()) { + XxlJobHelper.log("param is empty."); + + XxlJobHelper.handleFail(); + return; + } + + // orcarouter param + OrcaRouterParam orcaRouterParam = null; + try { + orcaRouterParam = GsonTool.fromJson(param, OrcaRouterParam.class); + if (orcaRouterParam.getPrompt()==null || orcaRouterParam.getPrompt().isBlank()) { + orcaRouterParam.setPrompt("你是一个研发工程师,擅长解决技术类问题。"); + } + if (orcaRouterParam.getInput() == null || orcaRouterParam.getInput().isBlank()) { + XxlJobHelper.log("input is empty."); + + XxlJobHelper.handleFail(); + return; + } + if (orcaRouterParam.getModel()==null || orcaRouterParam.getModel().isBlank()) { + orcaRouterParam.setModel("orcarouter/auto"); + } + } catch (Exception e) { + XxlJobHelper.log(new RuntimeException("OrcaRouterParam parse error", e)); + XxlJobHelper.handleFail(); + return; + } + + // input + XxlJobHelper.log("

【Input】: " + orcaRouterParam.getInput()+ "

"); + + // build chat-client + ChatClient orcaRouterChatClient = ChatClient + .builder(orcaRouterChatModel) + .defaultAdvisors(MessageChatMemoryAdvisor.builder(MessageWindowChatMemory.builder().build()).build()) + .defaultAdvisors(SimpleLoggerAdvisor.builder().build()) + .defaultOptions(OpenAiChatOptions.builder().model(orcaRouterParam.getModel())) + .build(); + + // call orcarouter + String response = orcaRouterChatClient + .prompt(orcaRouterParam.getPrompt()) + .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, "conversationId-default")) + .user(orcaRouterParam.getInput()) + .call() + .content(); + + XxlJobHelper.log("

【Output】: " + response + "

"); + } + + private static class OrcaRouterParam { + private String input; + private String prompt; + private String model; + + public String getInput() { + return input; + } + + public void setInput(String input) { + this.input = input; + } + + public String getPrompt() { + return prompt; + } + + public void setPrompt(String prompt) { + this.prompt = prompt; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + } + + } diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties index 3cddb4a7..7ee3897c 100644 --- a/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties +++ b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/main/resources/application.properties @@ -46,4 +46,8 @@ spring.ai.ollama.base-url=http://localhost:11434 ### spring-ai: openai (for openclaw) spring.ai.openai.base-url=http://127.0.0.1:18789 spring.ai.openai.api-key=xxxxxx +### spring-ai: orcarouter (OpenAI 兼容 LLM 路由网关,官网 https://www.orcarouter.ai) +spring.ai.orcarouter.base-url=https://api.orcarouter.ai/v1 +spring.ai.orcarouter.api-key={自行获取并修改} +spring.ai.orcarouter.model=orcarouter/auto diff --git a/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/test/java/com/xxl/job/executor/test/orcarouter/OrcaRouterLiveTest.java b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/test/java/com/xxl/job/executor/test/orcarouter/OrcaRouterLiveTest.java new file mode 100644 index 00000000..f7b86942 --- /dev/null +++ b/xxl-job-executor-samples/xxl-job-executor-sample-springboot-ai/src/test/java/com/xxl/job/executor/test/orcarouter/OrcaRouterLiveTest.java @@ -0,0 +1,48 @@ +package com.xxl.job.executor.test.orcarouter; + +import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; +import com.xxl.job.executor.config.OrcaRouterChatModel; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +@SpringBootTest +public class OrcaRouterLiveTest { + private static final Logger logger = LoggerFactory.getLogger(OrcaRouterLiveTest.class); + + // ignore + @MockitoBean + private XxlJobSpringExecutor xxlJobSpringExecutor; + + @Resource + private OrcaRouterChatModel orcaRouterChatModel; + + @Test + public void chatTest() { + + String prompt = "你是一个研发工程师,擅长解决技术类问题。"; + String input = "请简单介绍一下你自己"; + + // build chat-client + ChatClient orcaRouterChatClient = ChatClient + .builder(orcaRouterChatModel) + .defaultOptions(OpenAiChatOptions.builder().model("orcarouter/auto")) + .build(); + + // call orcarouter + String response = orcaRouterChatClient + .prompt(prompt) + .user(input) + .call() + .content(); + + logger.info("input: {}", input); + logger.info("response: {}", response); + } + +}