diff --git a/solon-ai-in-quarkus/README.md b/solon-ai-in-quarkus/README.md index 7c068a1d3115742687b3acd763d1600a247a2d75..7df556827022e6e7c0c0e7d28b6a06533beccecd 100644 --- a/solon-ai-in-quarkus/README.md +++ b/solon-ai-in-quarkus/README.md @@ -1 +1,7 @@ -希望有人能提交 pr 完善此示例 \ No newline at end of file +#### 第一步 启动服务 +```maven +# -Dquarkus.analytics.disabled=true 的目的是为了启动加速,避免老是提问你是否要参与问卷调查一类的 +mvn quarkus:dev -Dquarkus.analytics.disabled=true +``` +或者右侧maven有一个启动插件,通过它启动也可以 +![img.png](img.png) \ No newline at end of file diff --git a/solon-ai-in-quarkus/img.png b/solon-ai-in-quarkus/img.png new file mode 100644 index 0000000000000000000000000000000000000000..cae79b8aebbb8f45a20cf85d59c4e7aa1966fbc7 Binary files /dev/null and b/solon-ai-in-quarkus/img.png differ diff --git a/solon-ai-in-quarkus/pom.xml b/solon-ai-in-quarkus/pom.xml index f56a38a0b3d784421f859d3eb927d9b8907cd9a8..45c64cd7622b5f9c22461af1d4bab767f9b016e4 100644 --- a/solon-ai-in-quarkus/pom.xml +++ b/solon-ai-in-quarkus/pom.xml @@ -12,9 +12,24 @@ 17 + 3.28.3 3.6.0 + + 3.14.0 + + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 3.28.3 + true + 3.5.4 + + 3.6.0 + + 5.8.26 @@ -82,43 +97,100 @@ ${project.name} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - org.apache.maven.plugins + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + native-image-agent + + + + + maven-compiler-plugin - 3.11.0 + ${compiler-plugin.version} - -parameters - ${java.version} - ${java.version} - UTF-8 + true - - org.apache.maven.plugins - maven-assembly-plugin + maven-surefire-plugin + ${surefire-plugin.version} - ${project.name} - false - - jar-with-dependencies - - - - webapp.HelloApp - - + + org.jboss.logmanager.LogManager + ${maven.home} + + + + maven-failsafe-plugin + ${surefire-plugin.version} - make-assembly - package - single + integration-test + verify + + + ${project.build.directory}/${project.build.finalName}-runner + + org.jboss.logmanager.LogManager + ${maven.home} + + @@ -145,4 +217,23 @@ + + + + + + native + + + native + + + + false + false + true + + + + \ No newline at end of file diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/DynamicRoutingFilter.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/DynamicRoutingFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..75ed001e37dca1dd9319fdbe92950eaf7852825e --- /dev/null +++ b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/DynamicRoutingFilter.java @@ -0,0 +1,131 @@ +package webapp.mcpserver; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.ManagedContext; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import jakarta.inject.Inject; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.PreMatching; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; +import org.noear.solon.web.vertx.VxWebHandler; + +import java.io.IOException; +import java.lang.reflect.Method; + +@Provider +@PreMatching // 必须使用这个标记,不然不存在的路径无法执行,直接会被拦截 +public class DynamicRoutingFilter implements ContainerRequestFilter { + + + @Inject + RoutingContext routingContext; + + @Inject + Router router; + + @Inject + VxWebHandler handler; + + + @Inject + HttpServerRequest request; + + @Override + public void filter(ContainerRequestContext ctx) throws IOException { +// +// System.out.println(request.params()); +// System.out.println(request.headers()); +// +// System.out.println(request.body()); + + + String realPath = ctx.getUriInfo().getPath(); +// String method = ctx.getMethod(); + String patternPath = "/api/hello/:name"; + +// router.routeWithRegex("/mcp/.*").handler(req -> { +// // 获取上下文 +// ManagedContext requestContext = Arc.container().requestContext(); +// // 激活上下文 +// requestContext.activate(); +// handler.handle(req.request()); +// }); + +// router.route(patternPath).handler(rc -> { +// // 这个的目的是激活 router 对象,当第二次进入的时候,就可以获取到实际的动态 name 了,算是一个bug,所以还是需要直接用 request 直接获取参数即可 +// rc.next(); +// }); + +// if (realPath.contains("mcp")){ +// // 获取请求上下文 +// ManagedContext requestContext = Arc.container().requestContext(); +// // 激活上下文 +// requestContext.activate(); +// handler.handle(request); +// } + + // 如果符合规则的话,则会触发实现 + if(PathMatcher.isMatch(patternPath,realPath)){ + String target = "org.noear.quarkus.path.HelloNamePath#hello"; + // 解析 target e.g. "com.example.Hello#hello" + String[] parts = target.split("#"); + String className = parts[0]; + String methodName = parts[1]; + + try { + Class cls = Class.forName(className, false, Thread.currentThread().getContextClassLoader()); + + Object bean = null; + try { + InstanceHandle handle = Arc.container().instance(cls); + if (handle != null && handle.isAvailable()) { + bean = handle.get(); + } + } catch (Exception ignored) {} + + // 优先尝试接收 ContainerRequestContext + try { + // 这个就是对应的参数对象方法获取了 + Method m = cls.getMethod(methodName, RoutingContext.class); + // 这个就能触发内部方法,并且quarkus的相应注入对象,就能获取到, 其他拦截前的响应都会失效,但是通过 ctx.abortWith 即可触发 quarkus 自带的一系列后置响应拦截实现 + Object ret = m.invoke(bean, routingContext); + // 如果方法自己写响应可以返回 null 或 void —— 你需要决定如何判断 + if (ret instanceof Response) { + ctx.abortWith((Response) ret); + } else if (ret instanceof String) { + ctx.abortWith(Response.ok(ret).build()); + } else { + ctx.abortWith(Response.noContent().build()); + } + return; + } catch (NoSuchMethodException ex) { + // 尝试无参方法 + Method m = cls.getMethod(methodName); + Object ret = m.invoke(bean); + if (ret instanceof Response) { + ctx.abortWith((Response) ret); + } else if (ret instanceof String) { + ctx.abortWith(Response.ok(ret).build()); + } else { + ctx.abortWith(Response.noContent().build()); + } + return; + } + } catch (Throwable t) { + // 出错返回 500 + ctx.abortWith(Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("dynamic route invoke error: " + t.getMessage()).build()); + } + } + + + + + + } +} diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java index 3cf6cb1bf77b4e106430baf2347c8d94b4255de2..bf0b9380e45071db6ffb46bb244448345cdec730 100644 --- a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java +++ b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/McpServerConfig.java @@ -1,13 +1,25 @@ package webapp.mcpserver; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ManagedContext; +import io.quarkus.runtime.Startup; import io.vertx.core.AbstractVerticle; import io.vertx.ext.web.Router; +import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.PreMatching; +import jakarta.ws.rs.ext.Provider; import org.noear.solon.Solon; import org.noear.solon.ai.chat.tool.MethodToolProvider; +import org.noear.solon.ai.embedding.EmbeddingModel; import org.noear.solon.ai.mcp.McpChannel; import org.noear.solon.ai.mcp.server.IMcpServerEndpoint; import org.noear.solon.ai.mcp.server.McpServerEndpointProvider; @@ -15,31 +27,55 @@ import org.noear.solon.ai.mcp.server.annotation.McpServerEndpoint; import org.noear.solon.ai.mcp.server.prompt.MethodPromptProvider; import org.noear.solon.ai.mcp.server.resource.MethodResourceProvider; import org.noear.solon.web.vertx.VxWebHandler; + +import java.util.ArrayList; import java.util.List; +import webapp.llm._Constants; import webapp.mcpserver.tool.McpServerTool2; /** * 这个类独立一个目录,可以让 Solon 扫描范围最小化 * */ +@Startup @ApplicationScoped -public class McpServerConfig extends AbstractVerticle { +public class McpServerConfig extends AbstractVerticle { + @Inject Router router; @Inject - List serverEndpoints; + @Any + Instance serverEndpoints; + + + @Inject + VxWebHandler handler; - private final VxWebHandler handler; + @Produces + @ApplicationScoped + public VxWebHandler handler() { + System.out.println("=== VxWebHandler ==="); + return new VxWebHandler(); + } public McpServerConfig() { - this.handler = new VxWebHandler(); + // this.handler = new VxWebHandler(); } + @PostConstruct @Override public void start() { + System.out.println("McpServerConfig.start"); router.routeWithRegex("/mcp/.*").handler(req -> { + System.out.println("=== mcp ==="); + // 获取上下文 +// ManagedContext requestContext = Arc.container().requestContext(); +// // 激活上下文 +// requestContext.activate(); handler.handle(req.request()); + System.out.println("=== mcp end ==="); +// req.next(); }); Solon.start(McpServerConfig.class, new String[]{"--cfg=mcpserver.yml"}, app->{ @@ -76,28 +112,34 @@ public class McpServerConfig extends AbstractVerticle { } } - //Spring 组件转为端点 + // quarkus 组件转为端点 protected void quarkusCom2Endpoint() { //提取实现容器里 IMcpServerEndpoint 接口的 bean ,并注册为服务端点 - for (IMcpServerEndpoint serverEndpoint : serverEndpoints) { - Class serverEndpointClz = serverEndpoint.getClass(); - McpServerEndpoint anno = serverEndpointClz.getAnnotation(McpServerEndpoint.class); + if (serverEndpoints!=null){ - if (anno == null) { - continue; - } + for (IMcpServerEndpoint serverEndpoint : serverEndpoints) { + + Class serverEndpointClz = serverEndpoint.getClass(); + System.out.println("serverEndpoints "+serverEndpointClz.getSimpleName()); + McpServerEndpoint anno = serverEndpointClz.getAnnotation(McpServerEndpoint.class); + + if (anno == null) { + continue; + } - McpServerEndpointProvider serverEndpointProvider = McpServerEndpointProvider.builder() - .from(serverEndpointClz, anno) - .build(); + McpServerEndpointProvider serverEndpointProvider = McpServerEndpointProvider.builder() + .from(serverEndpointClz, anno) + .build(); - serverEndpointProvider.addTool(new MethodToolProvider(serverEndpointClz, serverEndpoint)); - serverEndpointProvider.addResource(new MethodResourceProvider(serverEndpointClz, serverEndpoint)); - serverEndpointProvider.addPrompt(new MethodPromptProvider(serverEndpointClz, serverEndpoint)); + serverEndpointProvider.addTool(new MethodToolProvider(serverEndpointClz, serverEndpoint)); + serverEndpointProvider.addResource(new MethodResourceProvider(serverEndpointClz, serverEndpoint)); + serverEndpointProvider.addPrompt(new MethodPromptProvider(serverEndpointClz, serverEndpoint)); - serverEndpointProvider.postStart(); + serverEndpointProvider.postStart(); - //可以再把 serverEndpointProvider 手动转入 SpringBoot 容器 + //可以再把 serverEndpointProvider 手动转入 SpringBoot 容器 + } } + } } diff --git a/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/PathMatcher.java b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/PathMatcher.java new file mode 100644 index 0000000000000000000000000000000000000000..7720a21664d44fd4007f545709870ae314892516 --- /dev/null +++ b/solon-ai-in-quarkus/src/main/java/webapp/mcpserver/PathMatcher.java @@ -0,0 +1,46 @@ +package webapp.mcpserver; + +public class PathMatcher { + public static boolean isMatch(String patternPath, String realPath) { + // 分割路径为片段(过滤空字符串) + String[] patternSegments = splitPath(patternPath); + String[] realSegments = splitPath(realPath); + + // 片段数量不同,直接不匹配 + if (patternSegments.length != realSegments.length) { + return false; + } + + // 逐个对比片段 + for (int i = 0; i < patternSegments.length; i++) { + String patternSeg = patternSegments[i]; + String realSeg = realSegments[i]; + + // 动态参数片段(:xxx)可匹配任意非空片段 + if (patternSeg.startsWith(":")) { + // 确保动态参数对应的值非空(根据业务需求可调整) + if (realSeg.isEmpty()) { + return false; + } + } else { + // 静态片段必须完全相等 + if (!patternSeg.equals(realSeg)) { + return false; + } + } + } + + return true; + } + + // 分割路径为片段,过滤空字符串(处理连续/或首尾/的情况) + private static String[] splitPath(String path) { + return path.split("/"); + } + + public static void main(String[] args) { + String path = "/api/hello/:name"; + String realPath = "/api/hello/MrYang"; + System.out.println(isMatch(path, realPath)); // 输出:true + } +}