├── src ├── main │ ├── resources │ │ ├── prompt │ │ │ ├── group-aggregated.txt │ │ │ ├── search-result.txt │ │ │ ├── content-result.txt │ │ │ ├── multi-result.txt │ │ │ ├── remote-unavailable.txt │ │ │ └── local-not-found.txt │ │ └── application.yml │ └── java │ │ └── top │ │ └── codestyle │ │ └── mcp │ │ ├── McpServerApplication.java │ │ ├── model │ │ ├── meta │ │ │ ├── LocalMetaInfo.java │ │ │ ├── LocalMetaVariable.java │ │ │ └── LocalMetaConfig.java │ │ ├── sdk │ │ │ ├── MetaVariable.java │ │ │ ├── MetaInfo.java │ │ │ └── RemoteMetaConfig.java │ │ └── tree │ │ │ ├── TreeNode.java │ │ │ └── Node.java │ │ ├── config │ │ ├── CodestyleMCPToolsConfig.java │ │ └── RepositoryConfig.java │ │ ├── util │ │ ├── PromptUtils.java │ │ ├── MetaInfoConvertUtil.java │ │ └── SDKUtils.java │ │ └── service │ │ ├── CodestyleService.java │ │ ├── PromptService.java │ │ ├── TemplateService.java │ │ └── LuceneIndexService.java └── test │ └── java │ └── top │ └── codestyle │ └── mcp │ └── service │ └── CodestyleServiceTest.java ├── img ├── image.png ├── image-1.png ├── image-2.png ├── image-3.png └── image-4.png ├── lombok.config ├── .gitignore ├── LICENSE ├── pom.xml └── README.md /src/main/resources/prompt/group-aggregated.txt: -------------------------------------------------------------------------------- 1 | 找到 %{s} 命名空间下的 %{s} 个模板组: 2 | %{s} -------------------------------------------------------------------------------- /img/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itxaiohanglover/mcp-codestyle-server/HEAD/img/image.png -------------------------------------------------------------------------------- /img/image-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itxaiohanglover/mcp-codestyle-server/HEAD/img/image-1.png -------------------------------------------------------------------------------- /img/image-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itxaiohanglover/mcp-codestyle-server/HEAD/img/image-2.png -------------------------------------------------------------------------------- /img/image-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itxaiohanglover/mcp-codestyle-server/HEAD/img/image-3.png -------------------------------------------------------------------------------- /img/image-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itxaiohanglover/mcp-codestyle-server/HEAD/img/image-4.png -------------------------------------------------------------------------------- /src/main/resources/prompt/search-result.txt: -------------------------------------------------------------------------------- 1 | 找到模板组: %{s} 2 | 3 | 目录树: 4 | ``` 5 | %{s} 6 | ``` 7 | 模板组介绍: 8 | %{s} -------------------------------------------------------------------------------- /src/main/resources/prompt/content-result.txt: -------------------------------------------------------------------------------- 1 | #文件名:%{s} 2 | #文件变量: 3 | ``` 4 | %{s} 5 | ``` 6 | #文件内容: 7 | ``` 8 | %{s} 9 | ``` -------------------------------------------------------------------------------- /src/main/resources/prompt/multi-result.txt: -------------------------------------------------------------------------------- 1 | 找到 %{s} 个匹配 "%{s}" 的模板: 2 | 3 | %{s} 4 | 5 | 请使用 "groupId/artifactId" 格式明确指定,例如:"%{s}" -------------------------------------------------------------------------------- /src/main/resources/prompt/remote-unavailable.txt: -------------------------------------------------------------------------------- 1 | 远程仓库不可访问,无法获取模板"%{s}"的信息。 2 | 3 | 建议尝试以下模板提示词: 4 | 【后端】CRUD, controller, service, mapper, entity 5 | 【前端】CRUD, index, form, modal 6 | 【通用】bankend, frontend 7 | 8 | 请检查模板提示词是否正确,或联系管理员 -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | config.stopBubbling=true 2 | lombok.toString.callSuper=CALL 3 | lombok.equalsAndHashCode.callSuper=CALL 4 | clear lombok.val.flagUsage 5 | lombok.val.flagUsage=ERROR 6 | clear lombok.accessors.flagUsage 7 | lombok.accessors.flagUsage=ERROR -------------------------------------------------------------------------------- /src/main/resources/prompt/local-not-found.txt: -------------------------------------------------------------------------------- 1 | 本地仓库%{s}未找到匹配的模板"%{s}"。 2 | 3 | 建议尝试以下模板提示词: 4 | 【后端】CRUD, controller, service, mapper, entity 5 | 【前端】CRUD, index, form, modal 6 | 【通用】增删改查, 代码生成 7 | 8 | 如需从远程获取模板,请设置 9 | repository.remote-search-enabled=true -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/McpServerApplication.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * MCP代码模板服务器应用程序 8 | * 提供代码模板搜索、下载、管理等MCP工具服务 9 | * 10 | * @author 小航love666, Kanttha, movclantian 11 | * @since 2025-09-03 12 | */ 13 | @SpringBootApplication 14 | public class McpServerApplication { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(McpServerApplication.class, args); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/model/meta/LocalMetaInfo.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.model.meta; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.NoArgsConstructor; 6 | import top.codestyle.mcp.model.sdk.MetaInfo; 7 | 8 | /** 9 | * 本地缓存的 Meta 信息结构 10 | * 11 | * @author 小航love666, Kanttha, movclantian 12 | * @since 2025-09-29 13 | */ 14 | @Data 15 | @NoArgsConstructor 16 | @EqualsAndHashCode(callSuper=false) 17 | public class LocalMetaInfo extends MetaInfo { 18 | 19 | /** 20 | * 模板文件内容 21 | */ 22 | private String templateContent; 23 | 24 | } -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/model/sdk/MetaVariable.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.model.sdk; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | /** 8 | * 元变量 9 | * 10 | * @author 小航love666, movclantian 11 | * @since 2025-09-29 12 | */ 13 | @Data 14 | @NoArgsConstructor 15 | @JsonIgnoreProperties(ignoreUnknown = true) 16 | public class MetaVariable { 17 | /** 18 | * 变量名 19 | */ 20 | public String variableName; 21 | 22 | /** 23 | * 变量类型 24 | */ 25 | public String variableType; 26 | 27 | /** 28 | * 变量注释说明 29 | */ 30 | public String variableComment; 31 | 32 | /** 33 | * 变量示例值 34 | */ 35 | private String example; 36 | } -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/model/meta/LocalMetaVariable.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.model.meta; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * 本地缓存的元变量结构 10 | * 11 | * @author 小航love666, movclantian 12 | * @since 2025-09-29 13 | */ 14 | @Data 15 | @NoArgsConstructor 16 | @JsonIgnoreProperties(ignoreUnknown = true) 17 | public class LocalMetaVariable { 18 | /** 19 | * 变量名 20 | */ 21 | private String variableName; 22 | 23 | /** 24 | * 变量类型 25 | */ 26 | private String variableType; 27 | 28 | /** 29 | * 变量注释说明 30 | */ 31 | private String variableComment; 32 | 33 | /** 34 | * 变量示例值 35 | */ 36 | private String example; 37 | } -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/config/CodestyleMCPToolsConfig.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.config; 2 | 3 | import org.springframework.ai.tool.ToolCallbackProvider; 4 | import org.springframework.ai.tool.method.MethodToolCallbackProvider; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import top.codestyle.mcp.service.CodestyleService; 8 | 9 | /** 10 | * 管理MCP工具的注册 配置类 11 | * 12 | * @author ChonghaoGao 13 | * @date 2025/12/13 22:44) 14 | */ 15 | @Configuration 16 | public class CodestyleMCPToolsConfig { 17 | 18 | @Bean 19 | public ToolCallbackProvider codestyleTools(CodestyleService codestyleService){ 20 | return MethodToolCallbackProvider.builder().toolObjects(codestyleService).build(); 21 | } 22 | 23 | //可以拓展新的工具 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8090 3 | spring: 4 | application: 5 | name: mcp-codestyle-server 6 | main: # 不启动web服务,并且关闭web启动时候的横幅 7 | web-application-type: none 8 | banner-mode: off 9 | ai: 10 | mcp: 11 | server: 12 | name: mcp-codestyle-server # 服务器名称 13 | version: 1.0.0 # 服务器版本 14 | type: SYNC # 服务器类型: SYNC(同步) 或 ASYNC(异步) 15 | stdio: true # 启用stdio模式 16 | # 仓库配置 17 | repository: 18 | # 本地基础路径,可通过JVM参数 -Dcache.base-path=自定义路径 覆盖 19 | # 示例: java -Dcache.base-path=/custom/path -jar app.jar 20 | local-path: /var/cache/codestyle # 默认本地路径 21 | remote-path: http://localhost:8080 # 远程仓库地址 22 | # 仓库目录配置(可选,不配置则自动使用 local-path/codestyle-cache) 23 | dir: /var/cache/codestyle/codestyle-cache 24 | # 是否启用远程检索(默认false,使用本地Lucene检索) 25 | remote-search-enabled: true 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ../../java_projects/mcp-codestyle-server/target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | target/ 6 | # 本地配置文件 7 | !**/src/main/resources/config/application-local.yml 8 | 9 | ### STS ### 10 | .apt_generated 11 | .classpath 12 | .factorypath 13 | .project 14 | .settings 15 | .springBeans 16 | .sts4-cache 17 | 18 | ### IntelliJ IDEA ### 19 | .idea 20 | !.idea/icon.png 21 | *.iws 22 | *.iml 23 | *.ipr 24 | 25 | ### NetBeans ### 26 | /nbproject/private/ 27 | /nbbuild/ 28 | /dist/ 29 | /nbdist/ 30 | /.nb-gradle/ 31 | build/ 32 | !**/src/main/**/build/ 33 | !**/src/test/**/build/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | # Maven ignore 39 | .flattened-pom.xml 40 | 41 | ### Temp ### 42 | *.log 43 | *.log.gz 44 | *.logs 45 | *.cache 46 | *.diff 47 | *.patch 48 | *.tmp 49 | 50 | # Ignore .github directory 51 | .github/ 52 | 53 | # Ignore CHANGELOG.md file 54 | CHANGELOG.md 55 | -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/model/tree/TreeNode.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.model.tree; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.TreeMap; 11 | 12 | /** 13 | * 树节点 14 | * 15 | * @author 小航love666, movclantian 16 | * @since 2025-09-29 17 | */ 18 | @Data 19 | @NoArgsConstructor 20 | @JsonIgnoreProperties(ignoreUnknown = true) 21 | public class TreeNode { 22 | /** 23 | * 节点名称 24 | */ 25 | String name; 26 | 27 | /** 28 | * 子节点映射,使用TreeMap保证字典序 29 | */ 30 | Map children = new TreeMap<>(); 31 | 32 | /** 33 | * 当前节点下的文件列表 34 | */ 35 | List files = new ArrayList<>(); 36 | 37 | public TreeNode(String name) { 38 | this.name = name; 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/model/tree/Node.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.model.tree; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import top.codestyle.mcp.model.sdk.MetaVariable; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * 节点信息 12 | * 13 | * @author 小航love666, movclantian 14 | * @since 2025-09-29 15 | */ 16 | @Data 17 | @NoArgsConstructor 18 | @JsonIgnoreProperties(ignoreUnknown = true) 19 | public class Node { 20 | /** 21 | * 父节点路径 22 | */ 23 | public String parent_path; 24 | 25 | /** 26 | * 当前节点路径 27 | */ 28 | public String path; 29 | 30 | /** 31 | * 节点类型: 0-目录, 1-文件 32 | */ 33 | public int type; 34 | 35 | /** 36 | * 节点名称 37 | */ 38 | public String name; 39 | 40 | /** 41 | * 输入变量列表 42 | */ 43 | public List inputVarivales; 44 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 baidu-maps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/model/sdk/MetaInfo.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.model.sdk; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * 元信息 10 | * 11 | * @author 小航love666, Kanttha, movclantian 12 | * @since 2025-09-29 13 | */ 14 | @Data 15 | @NoArgsConstructor 16 | public class MetaInfo { 17 | /** 18 | * 模板ID 19 | */ 20 | private Long id; 21 | 22 | /** 23 | * 组织名(如: artboy) 24 | */ 25 | private String groupId; 26 | 27 | /** 28 | * 模板组名(如: CRUD) 29 | */ 30 | private String artifactId; 31 | 32 | /** 33 | * 模板描述(如: 控制层) 34 | */ 35 | private String description; 36 | 37 | /** 38 | * 文件SHA256哈希值 39 | */ 40 | private String sha256; 41 | 42 | /** 43 | * 版本号(如: 1.0.0) 44 | */ 45 | private String version; 46 | 47 | /** 48 | * 模板文件名(如: Controller.java.ftl) 49 | */ 50 | private String filename; 51 | 52 | /** 53 | * 模板文件所在目录路径(如: /src/main/java/com/air/controller) 54 | */ 55 | private String filePath; 56 | 57 | /** 58 | * 模板文件完整路径(如: /src/main/java/com/air/controller/Controller.java.ftl) 59 | */ 60 | private String path; 61 | 62 | /** 63 | * 模板输入变量列表 64 | */ 65 | private List inputVariables; 66 | } -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/model/meta/LocalMetaConfig.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.model.meta; 2 | 3 | import lombok.Data; 4 | import top.codestyle.mcp.model.sdk.MetaVariable; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * 本地缓存的 meta.json 配置结构 10 | * 支持多版本的 configs 数组 11 | * 12 | * @author movclantian 13 | * @since 2025-11-22 14 | */ 15 | @Data 16 | public class LocalMetaConfig { 17 | /** 18 | * 组织名(用户名) 19 | */ 20 | private String groupId; 21 | 22 | /** 23 | * 模板组名 24 | */ 25 | private String artifactId; 26 | 27 | /** 28 | * 多版本配置列表 29 | */ 30 | private List configs; 31 | 32 | @Data 33 | public static class Config { 34 | /** 35 | * 版本号(如 "1.0") 36 | */ 37 | private String version; 38 | 39 | /** 40 | * 该版本的文件列表 41 | */ 42 | private List files; 43 | } 44 | 45 | @Data 46 | public static class FileInfo { 47 | /** 48 | * 文件路径(如 "src/main/java/com/air/controller") 49 | */ 50 | private String filePath; 51 | 52 | /** 53 | * 文件说明 54 | */ 55 | private String description; 56 | 57 | /** 58 | * 文件名(如 "Controller.java.ftl") 59 | */ 60 | private String filename; 61 | 62 | /** 63 | * 输入变量列表 64 | */ 65 | private List inputVariables; 66 | 67 | /** 68 | * 文件SHA256哈希值 69 | */ 70 | private String sha256; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/model/sdk/RemoteMetaConfig.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.model.sdk; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * 远程 API 返回的 meta.json 配置结构 9 | * config 字段为单个对象 10 | * 11 | * @author movclantian 12 | * @since 2025-11-22 13 | */ 14 | @Data 15 | public class RemoteMetaConfig { 16 | /** 17 | * 组织名(用户名) 18 | */ 19 | private String groupId; 20 | 21 | /** 22 | * 模板组名 23 | */ 24 | private String artifactId; 25 | 26 | /** 27 | * 模板组总体描述 28 | */ 29 | private String description; 30 | 31 | /** 32 | * 单个版本配置对象 33 | */ 34 | private Config config; 35 | 36 | @Data 37 | public static class Config { 38 | /** 39 | * 版本号(如 "1.0") 40 | */ 41 | private String version; 42 | 43 | /** 44 | * 该版本的文件列表 45 | */ 46 | private List files; 47 | } 48 | 49 | @Data 50 | public static class FileInfo { 51 | /** 52 | * 文件路径(如 "src/main/java/com/air/controller") 53 | */ 54 | private String filePath; 55 | 56 | /** 57 | * 文件说明 58 | */ 59 | private String description; 60 | 61 | /** 62 | * 文件名(如 "Controller.java.ftl") 63 | */ 64 | private String filename; 65 | 66 | /** 67 | * 输入变量列表 68 | */ 69 | private List inputVariables; 70 | 71 | /** 72 | * 文件SHA256哈希值 73 | */ 74 | private String sha256; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/top/codestyle/mcp/service/CodestyleServiceTest.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.service; 2 | 3 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import io.modelcontextprotocol.client.McpClient; 6 | import io.modelcontextprotocol.client.transport.ServerParameters; 7 | import io.modelcontextprotocol.client.transport.StdioClientTransport; 8 | import io.modelcontextprotocol.spec.McpSchema; 9 | 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | 12 | import java.util.Map; 13 | 14 | /** 15 | * 使用stdio传输,MCP服务器由客户端自动启动 16 | * 但你需要先构建服务器jar: 17 | * 18 | *
19 |  * ./mvnw clean install -DskipTests
20 |  * 
21 | */ 22 | @SpringBootTest 23 | class CodestyleServiceTest { 24 | 25 | public static void main(String[] args) { 26 | 27 | String Root_Path = "C:/Users/movcl/Desktop/mcp-codestyle-server"; 28 | 29 | var stdioParams = ServerParameters.builder("java") 30 | .args("-jar", 31 | "-Dspring.ai.mcp.server.stdio=true", 32 | "-Dspring.main.web-application-type=none", 33 | "-Dlogging.pattern.console=", 34 | "-Dfile.encoding=UTF-8", 35 | Root_Path + "/target/mcp-codestyle-server-0.0.1.jar") 36 | .build(); 37 | 38 | // var jsonMapper = new JacksonMcpJsonMapper(); 升级版不需要构建专用的JsonMapper 39 | var transport = new StdioClientTransport(stdioParams, new ObjectMapper()); 40 | var client = McpClient.sync(transport).build(); 41 | 42 | client.initialize(); 43 | 44 | // 列出并展示可用的工具 45 | McpSchema.ListToolsResult toolsList = client.listTools(); 46 | System.err.println("可用工具 = " + toolsList); 47 | 48 | // 获取模板目录树 49 | McpSchema.CallToolResult codestyle = client.callTool( 50 | new McpSchema.CallToolRequest("codestyleSearch", 51 | Map.of("templateKeyword", "CRUD"))); 52 | System.err.println("代码模板目录树: " + codestyle); 53 | 54 | //获取具体模板内容 55 | McpSchema.CallToolResult codestyleContent = client.callTool( 56 | new McpSchema.CallToolRequest("getTemplateByPath", 57 | Map.of("templatePath", "backend/CRUD/1.0.1/src/main/java/com/air/controller/Controller.ftl"))); 58 | System.err.println("代码模板内容: " + codestyleContent); 59 | 60 | client.closeGracefully(); 61 | } 62 | } -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/config/RepositoryConfig.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import top.codestyle.mcp.util.SDKUtils; 8 | 9 | import java.io.File; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | 14 | /** 15 | * 仓库配置类 16 | * 管理仓库路径和缓存目录配置 17 | * 18 | * @author 小航love666, Kanttha, movclantian chonghaoGao 19 | * @since 2025-09-29 20 | */ 21 | @Configuration 22 | public class RepositoryConfig { 23 | 24 | /** 25 | * 本地基础路径,默认使用系统临时目录 26 | * 可通过JVM参数 -Dcache.base-path=自定义路径 覆盖 27 | */ 28 | @Value("${cache.base-path:${repository.local-path:${java.io.tmpdir}}}") 29 | private String localPath; 30 | 31 | /** 32 | * 远程仓库地址 33 | */ 34 | @Value("${repository.remote-path}") 35 | private String remotePath; 36 | 37 | /** 38 | * 仓库目录路径 39 | * 默认在基础路径下创建codestyle-cache目录 40 | */ 41 | @Value("${repository.dir:}") 42 | private String repositoryDir; 43 | 44 | /** 45 | * 是否启用远程检索 46 | * 默认false,使用本地Lucene检索 47 | */ 48 | @Value("${repository.remote-search-enabled:false}") 49 | private boolean remoteSearchEnabled; 50 | 51 | /** 52 | * 获取本地基础路径 53 | */ 54 | public String getLocalPath() { 55 | return localPath; 56 | } 57 | 58 | /** 59 | * 获取远程仓库地址 60 | */ 61 | public String getRemotePath() { 62 | return remotePath; 63 | } 64 | 65 | /** 66 | * 获取仓库目录路径 67 | */ 68 | public String getRepositoryDir() { 69 | // 如果未配置repository.dir,则使用localPath + codestyle-cache 70 | if (repositoryDir == null || repositoryDir.isEmpty()) { 71 | return localPath + File.separator + "codestyle-cache"; 72 | } 73 | return repositoryDir; 74 | } 75 | 76 | /** 77 | * 是否启用远程检索 78 | */ 79 | public boolean isRemoteSearchEnabled() { 80 | return remoteSearchEnabled; 81 | } 82 | 83 | /** 84 | * 创建仓库目录Bean 85 | * 确保仓库目录存在,创建失败时自动降级到系统临时目录 86 | * 87 | * @return 仓库目录路径 88 | */ 89 | @Bean 90 | public Path repositoryDirectory() { 91 | try { 92 | String normalizedRepoDir = SDKUtils.normalizePath(getRepositoryDir()); 93 | Path repoPath = Paths.get(normalizedRepoDir); 94 | 95 | if (!Files.exists(repoPath)) { 96 | Files.createDirectories(repoPath); 97 | } 98 | return repoPath; 99 | } catch (Exception e) { 100 | String fallbackTempDir = System.getProperty("java.io.tmpdir") + File.separator + "codestyle-cache"; 101 | Path fallbackPath = Paths.get(fallbackTempDir); 102 | try { 103 | if (!Files.exists(fallbackPath)) { 104 | Files.createDirectories(fallbackPath); 105 | } 106 | return fallbackPath; 107 | } catch (Exception ex) { 108 | throw new RuntimeException("无法创建仓库目录", ex); 109 | } 110 | } 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 3.4.12 11 | 12 | 13 | top.codestyle.mcp 14 | mcp-codestyle-server 15 | 1.0.2 16 | mcp-codestyle-server 17 | mcp-codestyle-server 18 | 19 | 2.17.0 20 | 21 | 17 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-test 27 | test 28 | 29 | 30 | org.apache.lucene 31 | lucene-core 32 | 9.12.3 33 | 34 | 35 | org.apache.lucene 36 | lucene-queryparser 37 | 9.12.3 38 | 39 | 40 | org.apache.lucene 41 | lucene-analysis-smartcn 42 | 9.12.3 43 | 44 | 45 | org.springframework.ai 46 | spring-ai-starter-mcp-server-webmvc 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-web 51 | 52 | 53 | cn.hutool 54 | hutool-all 55 | 5.8.42 56 | 57 | 58 | com.fasterxml.jackson.core 59 | jackson-core 60 | ${jackson.version} 61 | 62 | 63 | com.fasterxml.jackson.core 64 | jackson-databind 65 | ${jackson.version} 66 | 67 | 68 | com.fasterxml.jackson.core 69 | jackson-annotations 70 | ${jackson.version} 71 | 72 | 73 | org.projectlombok 74 | lombok 75 | true 76 | 77 | 78 | com.googlecode.concurrentlinkedhashmap 79 | concurrentlinkedhashmap-lru 80 | 1.4.2 81 | 82 | 83 | 84 | 85 | 86 | org.springframework.ai 87 | spring-ai-bom 88 | 1.0.0-M7 89 | pom 90 | import 91 | 92 | 93 | 94 | 95 | 96 | 97 | org.springframework.boot 98 | spring-boot-maven-plugin 99 | 100 | 101 | 102 | repackage 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/util/PromptUtils.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.util; 2 | 3 | import top.codestyle.mcp.model.meta.LocalMetaInfo; 4 | import top.codestyle.mcp.model.sdk.MetaInfo; 5 | import top.codestyle.mcp.model.sdk.MetaVariable; 6 | import top.codestyle.mcp.model.tree.TreeNode; 7 | 8 | import java.util.HashMap; 9 | import java.util.LinkedHashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | /** 14 | * 提示词构建工具类 15 | * 提供目录树构建、变量格式化等功能 16 | * 17 | * @author 小航love666, Kanttha, movclantian 18 | * @since 2025-09-29 19 | */ 20 | public class PromptUtils { 21 | 22 | /** 23 | * 根据模板信息列表构建目录树 24 | * 25 | * @param list 模板元信息列表 26 | * @return 目录树根节点 27 | */ 28 | public static TreeNode buildTree(List list) { 29 | TreeNode root = new TreeNode(""); 30 | Map cache = new HashMap<>(); 31 | cache.put("", root); 32 | 33 | for (MetaInfo t : list) { 34 | // 构建完整路径(格式: groupId/artifactId/version/filePath) 35 | String fullPath = t.getGroupId() + "/" + t.getArtifactId() + "/" + 36 | t.getVersion() + t.getFilePath(); 37 | 38 | // 确保完整路径目录链已存在 39 | mkdir(fullPath, cache); 40 | 41 | // 挂载文件到目录节点 42 | if (t.getFilename() != null && !t.getFilename().endsWith("/")) { 43 | TreeNode dir = cache.get(fullPath); 44 | if (dir != null) { 45 | String fullFilePath = fullPath + "/" + t.getFilename(); 46 | dir.getFiles().add(fullFilePath); 47 | } 48 | } 49 | } 50 | return root; 51 | } 52 | 53 | /** 54 | * 递归创建目录节点链 55 | * 56 | * @param path 完整路径 57 | * @param cache 节点缓存(路径->节点映射) 58 | */ 59 | private static void mkdir(String path, Map cache) { 60 | if (cache.containsKey(path)) 61 | return; 62 | 63 | String[] parts = path.split("/"); 64 | StringBuilder sb = new StringBuilder(); 65 | TreeNode parent = cache.get(""); 66 | 67 | for (String p : parts) { 68 | if (p.isEmpty()) 69 | continue; 70 | if (sb.length() > 0) 71 | sb.append("/"); 72 | sb.append(p); 73 | String curPath = sb.toString(); 74 | TreeNode node = cache.get(curPath); 75 | if (node == null) { 76 | node = new TreeNode(p); 77 | parent.getChildren().put(p, node); 78 | cache.put(curPath, node); 79 | } 80 | parent = node; 81 | } 82 | } 83 | 84 | /** 85 | * 递归构建目录树的字符串表示 86 | * 87 | * @param node 当前节点 88 | * @param indent 当前缩进 89 | * @return 格式化的目录树字符串 90 | */ 91 | public static String buildTreeStr(TreeNode node, String indent) { 92 | StringBuilder sb = new StringBuilder(); 93 | if (!node.getName().isEmpty()) { 94 | sb.append(indent).append(node.getName()).append("/\n"); 95 | } 96 | node.getChildren().values().forEach(c -> sb.append(buildTreeStr(c, indent + " "))); 97 | node.getFiles().forEach(f -> { 98 | String fileName = f.substring(f.lastIndexOf("/") + 1); 99 | sb.append(indent).append("└── ").append(fileName).append('\n'); 100 | }); 101 | return sb.toString(); 102 | } 103 | 104 | /** 105 | * 构建变量信息的字符串表示 106 | * 107 | * @param vars 变量名->描述映射 108 | * @return 格式化的变量列表字符串 109 | */ 110 | public static String buildVarString(Map vars) { 111 | StringBuilder sb = new StringBuilder(); 112 | vars.forEach((k, v) -> sb.append("- ").append(k).append(": ").append(v).append('\n')); 113 | return sb.toString(); 114 | } 115 | 116 | /** 117 | * 构建模板文件字符串(包含变量和模板内容) 118 | * 119 | * @param loadTemplateFile 模板文件列表 120 | * @return 格式化的模板字符串 121 | */ 122 | public static String buildTemplatesStr(List loadTemplateFile) { 123 | // 构建模板内容字符串 124 | StringBuilder detailTemplates = new StringBuilder(); 125 | for (LocalMetaInfo metaInfo : loadTemplateFile) { 126 | detailTemplates.append("```\n") 127 | .append(metaInfo.getTemplateContent() != null ? metaInfo.getTemplateContent() : "") 128 | .append("\n```\n"); 129 | } 130 | String detailTemplatesStr = detailTemplates.toString().trim(); 131 | 132 | // 加载并构建模板变量列表 133 | Map vars = new LinkedHashMap<>(); 134 | for (MetaInfo n : loadTemplateFile) { 135 | if (n.getInputVariables() == null) 136 | continue; 137 | for (MetaVariable v : n.getInputVariables()) { 138 | String desc = String.format("%s[%s]", v.getVariableComment(), v.getVariableType()); 139 | vars.putIfAbsent(v.getVariableName(), desc); 140 | } 141 | } 142 | String variables = buildVarString(vars).trim(); 143 | return variables + detailTemplatesStr; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/service/CodestyleService.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.ai.tool.annotation.Tool; 5 | import org.springframework.ai.tool.annotation.ToolParam; 6 | import org.springframework.stereotype.Service; 7 | import top.codestyle.mcp.config.RepositoryConfig; 8 | import top.codestyle.mcp.model.meta.LocalMetaInfo; 9 | import top.codestyle.mcp.model.sdk.MetaInfo; 10 | import top.codestyle.mcp.model.sdk.RemoteMetaConfig; 11 | import top.codestyle.mcp.model.tree.TreeNode; 12 | import top.codestyle.mcp.util.PromptUtils; 13 | 14 | import java.io.IOException; 15 | import java.util.LinkedHashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * 代码模板搜索和内容获取服务 21 | * 22 | * @author 小航love666, Kanttha, movclantian 23 | * @since 2025-12-03 24 | */ 25 | @Service 26 | @RequiredArgsConstructor 27 | public class CodestyleService { 28 | 29 | private final TemplateService templateService; 30 | private final PromptService promptService; 31 | private final LuceneIndexService luceneIndexService; 32 | private final RepositoryConfig repositoryConfig; 33 | 34 | /** 35 | * 搜索代码模板 36 | *

根据模板提示词搜索模板信息,返回目录树和模板组介绍。 37 | * 支持本地Lucene检索和远程检索两种模式。 38 | * 39 | * @param templateKeyword 模板提示词,支持关键词或 groupId/artifactId 格式,如: CRUD, backend, frontend, continew/DatabaseConfig 40 | * @return 模板目录树和描述信息字符串 41 | */ 42 | @Tool(name = "codestyleSearch", description = """ 43 | 根据模板提示词搜索代码模板库,返回匹配的模板目录树和模板组介绍。 44 | 支持以下搜索格式: 45 | 1. 关键词搜索:CRUD, frontend, backend 等 46 | 2. 精确搜索:groupId/artifactId 格式 47 | """) 48 | public String codestyleSearch( 49 | @ToolParam(description = "模板提示词,如: CRUD, bankend, frontend等") String templateKeyword) { 50 | try { 51 | // 远程检索模式 52 | if (templateService.isRemoteSearchEnabled()) { 53 | RemoteMetaConfig remoteConfig = templateService.fetchRemoteMetaConfig(templateKeyword); 54 | 55 | if (remoteConfig == null) { 56 | return promptService.buildRemoteUnavailable(templateKeyword); 57 | } 58 | 59 | templateService.smartDownloadTemplate(remoteConfig); 60 | 61 | String groupId = remoteConfig.getGroupId(); 62 | String artifactId = remoteConfig.getArtifactId(); 63 | String description = remoteConfig.getDescription(); 64 | 65 | List metaInfos = templateService.searchLocalRepository(groupId, artifactId); 66 | if (metaInfos.isEmpty()) { 67 | return "本地仓库模板文件不完整,请检查模板目录"; 68 | } 69 | 70 | TreeNode treeNode = PromptUtils.buildTree(metaInfos); 71 | String treeStr = PromptUtils.buildTreeStr(treeNode, "").trim(); 72 | return promptService.buildSearchResult(artifactId, treeStr, description); 73 | } 74 | 75 | // 本地Lucene全文检索模式 76 | List searchResults = luceneIndexService.fetchLocalMetaConfig(templateKeyword); 77 | 78 | if (searchResults.isEmpty()) { 79 | return promptService.buildLocalNotFound(repositoryConfig.getRepositoryDir(), templateKeyword); 80 | } 81 | 82 | // 检查是否为同一groupId的多个模板(命名空间搜索) 83 | if (templateService.isGroupIdSearch(searchResults)) { 84 | return templateService.buildGroupAggregatedResult(templateKeyword, searchResults); 85 | } 86 | 87 | // 处理多个不同模板的情况(让AI选择) 88 | if (searchResults.size() > 1) { 89 | return templateService.buildMultiResultResponse(templateKeyword, searchResults); 90 | } 91 | 92 | // 单模板结果 93 | LuceneIndexService.SearchResult searchResult = searchResults.get(0); 94 | List metaInfos = templateService.searchLocalRepository( 95 | searchResult.groupId(), searchResult.artifactId()); 96 | 97 | if (metaInfos.isEmpty()) { 98 | return "本地仓库模板文件不完整,请检查模板目录"; 99 | } 100 | 101 | TreeNode treeNode = PromptUtils.buildTree(metaInfos); 102 | String treeStr = PromptUtils.buildTreeStr(treeNode, "").trim(); 103 | return promptService.buildSearchResult(searchResult.artifactId(), treeStr, searchResult.description()); 104 | } catch (Exception e) { 105 | return "模板搜索失败: " + e.getMessage(); 106 | } 107 | } 108 | 109 | /** 110 | * 获取模板文件内容 111 | *

根据完整的模板文件路径获取详细内容,包括变量说明和模板代码 112 | * 113 | * @param templatePath 完整模板文件路径(包含版本号和.ftl扩展名),如: backend/CRUD/1.0.0/src/main/java/com/air/controller/Controller.ftl 114 | * @return 模板文件的详细信息字符串(包含变量说明和模板内容) 115 | * @throws IOException 文件读取异常 116 | */ 117 | @Tool(name = "getTemplateByPath", description = "传入模板文件路径,获取模板文件的详细内容(包括变量说明和模板代码)") 118 | public String getTemplateByPath( 119 | @ToolParam(description = "模板文件路径,如:backend/CRUD/1.0.0/src/main/java/com/air/controller/Controller.ftl") String templatePath) 120 | throws IOException { 121 | 122 | // 使用精确路径搜索模板 123 | LocalMetaInfo matchedTemplate = templateService.searchByPath(templatePath); 124 | 125 | // 校验搜索结果 126 | if (matchedTemplate == null) { 127 | return String.format("未找到路径为 '%s' 的模板文件,请检查路径是否正确。", templatePath); 128 | } 129 | 130 | // 构建变量信息 131 | Map vars = new LinkedHashMap<>(); 132 | if (matchedTemplate.getInputVariables() != null && !matchedTemplate.getInputVariables().isEmpty()) { 133 | for (var variable : matchedTemplate.getInputVariables()) { 134 | String desc = String.format("%s(示例:%s)[%s]", 135 | variable.getVariableComment(), 136 | variable.getExample(), 137 | variable.getVariableType()); 138 | vars.put(variable.getVariableName(), desc); 139 | } 140 | } 141 | 142 | // 使用PromptUtils格式化变量信息 143 | String varInfo = vars.isEmpty() ? "无变量" : PromptUtils.buildVarString(vars).trim(); 144 | 145 | // 使用PromptService模板构建最终输出 146 | return promptService.buildPrompt( 147 | templatePath, 148 | varInfo, 149 | matchedTemplate.getTemplateContent() != null ? matchedTemplate.getTemplateContent() : ""); 150 | } 151 | } -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/util/MetaInfoConvertUtil.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.util; 2 | 3 | import cn.hutool.core.collection.CollUtil; 4 | import cn.hutool.core.io.FileUtil; 5 | import cn.hutool.json.JSONUtil; 6 | import top.codestyle.mcp.model.meta.LocalMetaInfo; 7 | import top.codestyle.mcp.model.meta.LocalMetaVariable; 8 | import top.codestyle.mcp.model.sdk.MetaInfo; 9 | import top.codestyle.mcp.model.sdk.MetaVariable; 10 | import top.codestyle.mcp.model.meta.LocalMetaConfig; 11 | import top.codestyle.mcp.model.sdk.RemoteMetaConfig; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | /** 19 | * 元信息转换工具类 20 | * 提供MetaInfo、LocalMetaConfig、RemoteMetaConfig之间的转换 21 | * 22 | * @author Kanttha, movclantian 23 | * @since 2025-10-17 24 | */ 25 | public class MetaInfoConvertUtil { 26 | 27 | /** 28 | * 转换MetaInfo为LocalMetaInfo 29 | * 30 | * @param source 源MetaInfo对象 31 | * @return 转换后的LocalMetaInfo对象,source为null时返回null 32 | */ 33 | public static LocalMetaInfo convert(MetaInfo source) { 34 | if (source == null) { 35 | return null; 36 | } 37 | LocalMetaInfo target = new LocalMetaInfo(); 38 | 39 | // 复制基础字段 40 | target.setId(source.getId()); 41 | target.setVersion(source.getVersion()); 42 | target.setGroupId(source.getGroupId()); 43 | target.setArtifactId(source.getArtifactId()); 44 | target.setFilePath(source.getFilePath()); 45 | target.setDescription(source.getDescription()); 46 | target.setFilename(source.getFilename()); 47 | target.setSha256(source.getSha256()); 48 | 49 | // 复制变量列表 50 | List vars = source.getInputVariables(); 51 | if (CollUtil.isNotEmpty(vars)) { 52 | target.setInputVariables(vars); 53 | } 54 | return target; 55 | } 56 | 57 | /** 58 | * 解析meta.json文件为模板元信息列表(预留扩展) 59 | * 60 | * @param metaFile meta.json文件 61 | * @return 模板元信息列表 62 | * @throws IOException 文件读取异常 63 | */ 64 | public static List parseMetaJson(File metaFile) throws IOException { 65 | List result = new ArrayList<>(); 66 | 67 | LocalMetaConfig localConfig = JSONUtil.toBean(FileUtil.readUtf8String(metaFile), LocalMetaConfig.class); 68 | 69 | String groupId = localConfig.getGroupId(); 70 | String artifactId = localConfig.getArtifactId(); 71 | 72 | if (localConfig.getConfigs() != null) { 73 | for (LocalMetaConfig.Config config : localConfig.getConfigs()) { 74 | String version = config.getVersion(); 75 | 76 | if (config.getFiles() != null) { 77 | for (LocalMetaConfig.FileInfo fileInfo : config.getFiles()) { 78 | MetaInfo metaInfo = new MetaInfo(); 79 | metaInfo.setGroupId(groupId); 80 | metaInfo.setArtifactId(artifactId); 81 | metaInfo.setVersion(version); 82 | metaInfo.setFilename(fileInfo.getFilename()); 83 | metaInfo.setFilePath(fileInfo.getFilePath()); 84 | metaInfo.setDescription(fileInfo.getDescription()); 85 | metaInfo.setSha256(fileInfo.getSha256()); 86 | metaInfo.setInputVariables(fileInfo.getInputVariables()); 87 | 88 | String fullPath = groupId + File.separator + artifactId + File.separator + version + 89 | fileInfo.getFilePath() + File.separator + fileInfo.getFilename(); 90 | metaInfo.setPath(fullPath); 91 | 92 | result.add(metaInfo); 93 | } 94 | } 95 | } 96 | } 97 | 98 | return result; 99 | } 100 | 101 | /** 102 | * 仅解析 meta.json 中“最新版本”为模板元信息列表 103 | * 104 | * @param metaFile meta.json 文件 105 | * @return 仅包含最新版本文件的模板元信息列表 106 | * @throws IOException 文件读取异常 107 | */ 108 | public static List parseMetaJsonLatestOnly(File metaFile) throws IOException { 109 | List result = new ArrayList<>(); 110 | 111 | LocalMetaConfig localConfig = JSONUtil.toBean(FileUtil.readUtf8String(metaFile), LocalMetaConfig.class); 112 | 113 | String groupId = localConfig.getGroupId(); 114 | String artifactId = localConfig.getArtifactId(); 115 | 116 | if (CollUtil.isEmpty(localConfig.getConfigs())) { 117 | return result; 118 | } 119 | 120 | LocalMetaConfig.Config latest = localConfig.getConfigs().get(localConfig.getConfigs().size() - 1); 121 | if (latest == null) { 122 | return result; 123 | } 124 | 125 | String version = latest.getVersion(); 126 | if (latest.getFiles() != null) { 127 | for (LocalMetaConfig.FileInfo fileInfo : latest.getFiles()) { 128 | MetaInfo metaInfo = new MetaInfo(); 129 | metaInfo.setGroupId(groupId); 130 | metaInfo.setArtifactId(artifactId); 131 | metaInfo.setVersion(version); 132 | metaInfo.setFilename(fileInfo.getFilename()); 133 | metaInfo.setFilePath(fileInfo.getFilePath()); 134 | metaInfo.setDescription(fileInfo.getDescription()); 135 | metaInfo.setSha256(fileInfo.getSha256()); 136 | metaInfo.setInputVariables(fileInfo.getInputVariables()); 137 | 138 | String fullPath = groupId + File.separator + artifactId + File.separator + version + 139 | fileInfo.getFilePath() + File.separator + fileInfo.getFilename(); 140 | metaInfo.setPath(fullPath); 141 | 142 | result.add(metaInfo); 143 | } 144 | } 145 | 146 | return result; 147 | } 148 | 149 | /** 150 | * 转换远程配置为本地配置 151 | * 152 | * @param remoteConfig 远程配置 153 | * @return 本地配置 154 | */ 155 | public static LocalMetaConfig.Config convertRemoteToLocalConfig(RemoteMetaConfig remoteConfig) { 156 | LocalMetaConfig.Config newConfig = new LocalMetaConfig.Config(); 157 | newConfig.setVersion(remoteConfig.getConfig().getVersion()); 158 | 159 | List localFiles = new ArrayList<>(); 160 | if (remoteConfig.getConfig().getFiles() != null) { 161 | for (RemoteMetaConfig.FileInfo remoteFile : remoteConfig.getConfig().getFiles()) { 162 | LocalMetaConfig.FileInfo localFile = new LocalMetaConfig.FileInfo(); 163 | localFile.setFilePath(remoteFile.getFilePath()); 164 | localFile.setFilename(remoteFile.getFilename()); 165 | localFile.setDescription(remoteFile.getDescription()); 166 | localFile.setSha256(remoteFile.getSha256()); 167 | localFile.setInputVariables(remoteFile.getInputVariables()); 168 | localFiles.add(localFile); 169 | } 170 | } 171 | newConfig.setFiles(localFiles); 172 | return newConfig; 173 | } 174 | 175 | /** 176 | * 转换MetaVariable为LocalMetaVariable(预留扩展) 177 | * 178 | * @param src 源MetaVariable对象 179 | * @return 转换后的LocalMetaVariable对象 180 | */ 181 | private static LocalMetaVariable convertVariable(MetaVariable src) { 182 | if (src == null) { 183 | return null; 184 | } 185 | return new LocalMetaVariable(); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/service/PromptService.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.core.io.Resource; 5 | import org.springframework.core.io.ResourceLoader; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.io.IOException; 9 | import java.nio.charset.StandardCharsets; 10 | import java.util.Objects; 11 | import java.util.regex.Matcher; 12 | 13 | /** 14 | * 提示词模板加载服务 15 | * 使用懒加载模式从classpath读取提示词模板 16 | * 17 | * @author 小航love666, Kanttha, movclantian 18 | * @since 2025-09-29 19 | */ 20 | @Service 21 | public class PromptService { 22 | 23 | private static final String CONTENT_RESULT_TEMPLATE_PATH = "classpath:prompt/content-result.txt"; 24 | private static final String SEARCH_RESULT_TEMPLATE_PATH = "classpath:prompt/search-result.txt"; 25 | private static final String REMOTE_UNAVAILABLE_TEMPLATE_PATH = "classpath:prompt/remote-unavailable.txt"; 26 | private static final String LOCAL_NOT_FOUND_TEMPLATE_PATH = "classpath:prompt/local-not-found.txt"; 27 | private static final String MULTI_RESULT_TEMPLATE_PATH = "classpath:prompt/multi-result.txt"; 28 | private static final String GROUP_AGGREGATED_TEMPLATE_PATH = "classpath:prompt/group-aggregated.txt"; 29 | 30 | @Autowired 31 | private ResourceLoader resourceLoader; 32 | 33 | private volatile String contentResultTemplate; 34 | private volatile String searchResultTemplate; 35 | private volatile String remoteUnavailableTemplate; 36 | private volatile String localNotFoundTemplate; 37 | private volatile String multiResultTemplate; 38 | private volatile String groupAggregatedTemplate; 39 | 40 | /** 41 | * 线程安全懒加载模板内容模板 42 | * 43 | * @return 模板内容字符串 44 | */ 45 | private String getContentResultTemplate() { 46 | if (contentResultTemplate == null) { 47 | synchronized (this) { 48 | if (contentResultTemplate == null) { 49 | contentResultTemplate = loadTemplate(CONTENT_RESULT_TEMPLATE_PATH); 50 | } 51 | } 52 | } 53 | return contentResultTemplate; 54 | } 55 | 56 | /** 57 | * 线程安全懒加载搜索结果模板 58 | * 59 | * @return 搜索结果模板字符串 60 | */ 61 | private String getSearchResultTemplate() { 62 | if (searchResultTemplate == null) { 63 | synchronized (this) { 64 | if (searchResultTemplate == null) { 65 | searchResultTemplate = loadTemplate(SEARCH_RESULT_TEMPLATE_PATH); 66 | } 67 | } 68 | } 69 | return searchResultTemplate; 70 | } 71 | 72 | /** 73 | * 线程安全懒加载远程不可用模板 74 | * 75 | * @return 远程不可用模板字符串 76 | */ 77 | private String getRemoteUnavailableTemplate() { 78 | if (remoteUnavailableTemplate == null) { 79 | synchronized (this) { 80 | if (remoteUnavailableTemplate == null) { 81 | remoteUnavailableTemplate = loadTemplate(REMOTE_UNAVAILABLE_TEMPLATE_PATH); 82 | } 83 | } 84 | } 85 | return remoteUnavailableTemplate; 86 | } 87 | 88 | /** 89 | * 线程安全懒加载本地未找到模板 90 | * 91 | * @return 本地未找到模板字符串 92 | */ 93 | private String getLocalNotFoundTemplate() { 94 | if (localNotFoundTemplate == null) { 95 | synchronized (this) { 96 | if (localNotFoundTemplate == null) { 97 | localNotFoundTemplate = loadTemplate(LOCAL_NOT_FOUND_TEMPLATE_PATH); 98 | } 99 | } 100 | } 101 | return localNotFoundTemplate; 102 | } 103 | 104 | /** 105 | * 线程安全懒加载多结果模板 106 | * 107 | * @return 多结果模板字符串 108 | */ 109 | private String getMultiResultTemplate() { 110 | if (multiResultTemplate == null) { 111 | synchronized (this) { 112 | if (multiResultTemplate == null) { 113 | multiResultTemplate = loadTemplate(MULTI_RESULT_TEMPLATE_PATH); 114 | } 115 | } 116 | } 117 | return multiResultTemplate; 118 | } 119 | 120 | /** 121 | * 线程安全懒加载分组聚合模板 122 | * 123 | * @return 分组聚合模板字符串 124 | */ 125 | private String getGroupAggregatedTemplate() { 126 | if (groupAggregatedTemplate == null) { 127 | synchronized (this) { 128 | if (groupAggregatedTemplate == null) { 129 | groupAggregatedTemplate = loadTemplate(GROUP_AGGREGATED_TEMPLATE_PATH); 130 | } 131 | } 132 | } 133 | return groupAggregatedTemplate; 134 | } 135 | 136 | /** 137 | * 从classpath加载模板文件 138 | * 139 | * @param templatePath 模板文件路径 140 | * @return 模板内容字符串 141 | * @throws IllegalStateException 文件不存在或加载失败 142 | */ 143 | private String loadTemplate(String templatePath) { 144 | try { 145 | Resource resource = resourceLoader.getResource(templatePath); 146 | if (!resource.exists()) { 147 | throw new IllegalStateException("classpath 下找不到 " + templatePath); 148 | } 149 | String content = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); 150 | return content.strip(); 151 | } catch (IOException e) { 152 | throw new IllegalStateException("加载 " + templatePath + " 失败", e); 153 | } 154 | } 155 | 156 | /** 157 | * 构建内容提示词(按顺序替换模板中的%{s}占位符) 158 | * 159 | * @param params 可变参数,依次对应模板中的%{s} 160 | * @return 替换后的提示词字符串 161 | * @throws IllegalArgumentException 参数数量与占位符数量不匹配 162 | */ 163 | public String buildPrompt(String... params) { 164 | return buildFromTemplate(getContentResultTemplate(), params); 165 | } 166 | 167 | /** 168 | * 构建搜索结果(按顺序替换模板中的%{s}占位符) 169 | * 170 | * @param params 可变参数,依次对应模板中的%{s} 171 | * @return 替换后的搜索结果字符串 172 | * @throws IllegalArgumentException 参数数量与占位符数量不匹配 173 | */ 174 | public String buildSearchResult(String... params) { 175 | return buildFromTemplate(getSearchResultTemplate(), params); 176 | } 177 | 178 | /** 179 | * 构建远程不可用消息 180 | * 181 | * @param templateKeyword 模板关键词 182 | * @return 格式化后的消息 183 | */ 184 | public String buildRemoteUnavailable(String templateKeyword) { 185 | return buildFromTemplate(getRemoteUnavailableTemplate(), templateKeyword); 186 | } 187 | 188 | /** 189 | * 构建本地未找到消息 190 | * 191 | * @param repositoryPath 仓库路径 192 | * @param templateKeyword 模板关键词 193 | * @return 格式化后的消息 194 | */ 195 | public String buildLocalNotFound(String repositoryPath, String templateKeyword) { 196 | return buildFromTemplate(getLocalNotFoundTemplate(), repositoryPath, templateKeyword); 197 | } 198 | 199 | /** 200 | * 构建多结果响应 201 | * 202 | * @param count 结果数量 203 | * @param keyword 搜索关键词 204 | * @param resultList 结果列表字符串 205 | * @param exampleArtifact 示例artifactId 206 | * @return 格式化后的消息 207 | */ 208 | public String buildMultiResult(String count, String keyword, String resultList, String exampleArtifact) { 209 | return buildFromTemplate(getMultiResultTemplate(), count, keyword, resultList, exampleArtifact); 210 | } 211 | 212 | /** 213 | * 构建分组聚合结果 214 | * 215 | * @param groupId 组ID 216 | * @param count 模板组数量 217 | * @param artifactList 模板组列表 218 | * @return 格式化后的描述 219 | */ 220 | public String buildGroupAggregated(String groupId, String count, String artifactList) { 221 | return buildFromTemplate(getGroupAggregatedTemplate(), groupId, count, artifactList); 222 | } 223 | 224 | /** 225 | * 从模板构建内容 226 | * 227 | * @param template 模板内容 228 | * @param params 可变参数,依次对应模板中的%{s} 229 | * @return 替换后的字符串 230 | * @throws IllegalArgumentException 参数数量与占位符数量不匹配 231 | */ 232 | private String buildFromTemplate(String template, String... params) { 233 | Objects.requireNonNull(params, "params must not be null"); 234 | 235 | int placeholderCount = countPlaceholder(template); 236 | if (placeholderCount != params.length) { 237 | throw new IllegalArgumentException( 238 | "模板需要 " + placeholderCount + " 个参数,实际传入 " + params.length); 239 | } 240 | 241 | String result = template; 242 | for (String p : params) { 243 | // 使用 Matcher.quoteReplacement 来转义特殊字符,避免 $ 和 \ 被当作正则表达式的反向引用 244 | String replacement = Matcher.quoteReplacement(p == null ? "" : p); 245 | result = result.replaceFirst("%\\{s}", replacement); 246 | } 247 | return result; 248 | } 249 | 250 | /** 251 | * 统计模板中%{s}占位符的数量 252 | * 253 | * @param template 模板字符串 254 | * @return 占位符数量 255 | */ 256 | private static int countPlaceholder(String template) { 257 | int count = 0, idx = 0; 258 | while ((idx = template.indexOf("%{s}", idx)) != -1) { 259 | count++; 260 | idx += 4; 261 | } 262 | return count; 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/service/TemplateService.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Lazy; 5 | import org.springframework.stereotype.Service; 6 | import top.codestyle.mcp.config.RepositoryConfig; 7 | import top.codestyle.mcp.model.meta.LocalMetaInfo; 8 | import top.codestyle.mcp.model.sdk.MetaInfo; 9 | import top.codestyle.mcp.model.sdk.RemoteMetaConfig; 10 | import top.codestyle.mcp.model.tree.TreeNode; 11 | import top.codestyle.mcp.util.MetaInfoConvertUtil; 12 | import top.codestyle.mcp.util.PromptUtils; 13 | import top.codestyle.mcp.util.SDKUtils; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.nio.charset.StandardCharsets; 18 | import java.nio.file.Files; 19 | import java.nio.file.Path; 20 | import java.nio.file.Paths; 21 | import java.util.ArrayList; 22 | import java.util.HashSet; 23 | import java.util.List; 24 | 25 | /** 26 | * 模板服务 27 | * 提供模板搜索、远程配置获取和智能下载功能 28 | * 29 | * @author 小航love666, Kanttha, movclantian 30 | * @since 2025-09-29 31 | */ 32 | @Service 33 | @RequiredArgsConstructor 34 | public class TemplateService { 35 | 36 | private final RepositoryConfig repositoryConfig; 37 | 38 | @Lazy 39 | private final LuceneIndexService luceneIndexService; 40 | 41 | @Lazy 42 | private final PromptService promptService; 43 | 44 | /** 45 | * 根据groupId和artifactId搜索指定模板组 46 | * 47 | * @param groupId 组ID 48 | * @param artifactId 项目ID 49 | * @return 匹配的模板元信息列表 50 | */ 51 | public List searchLocalRepository(String groupId, String artifactId) { 52 | String localRepoPath = repositoryConfig.getRepositoryDir(); 53 | return SDKUtils.searchLocalRepository(groupId, artifactId, localRepoPath); 54 | } 55 | 56 | /** 57 | * 根据精确路径搜索模板 58 | * 本地未找到时尝试从远程下载 59 | * 60 | * @param exactPath 精确路径,格式: groupId/artifactId/version/filePath/filename 61 | * @return 模板元信息,未找到返回null 62 | * @throws IOException 文件读取异常 63 | */ 64 | public LocalMetaInfo searchByPath(String exactPath) throws IOException { 65 | String localRepoPath = repositoryConfig.getRepositoryDir(); 66 | 67 | // 从本地仓库中查找模板 68 | MetaInfo localResult = SDKUtils.searchByPath(exactPath, localRepoPath); 69 | if (localResult != null) { 70 | LocalMetaInfo result = MetaInfoConvertUtil.convert(localResult); 71 | result.setTemplateContent(readTemplateContent(localResult)); 72 | return result; 73 | } 74 | 75 | // 本地未找到,尝试智能下载 76 | try { 77 | // 解析路径获取artifactId(格式: groupId/artifactId/version/filePath/filename) 78 | String[] parts = exactPath.split("/"); 79 | if (parts.length >= 2) { 80 | String artifactId = parts[1]; 81 | 82 | // 获取远程配置 83 | RemoteMetaConfig remoteConfig = fetchRemoteMetaConfig(artifactId); 84 | if (remoteConfig == null) { 85 | return null; 86 | } 87 | 88 | // 触发智能下载 89 | boolean downloadSuccess = smartDownloadTemplate(remoteConfig); 90 | 91 | // 下载成功后重新搜索 92 | if (downloadSuccess) { 93 | localResult = SDKUtils.searchByPath(exactPath, localRepoPath); 94 | if (localResult != null) { 95 | LocalMetaInfo result = MetaInfoConvertUtil.convert(localResult); 96 | result.setTemplateContent(readTemplateContent(localResult)); 97 | return result; 98 | } 99 | } 100 | } 101 | } catch (Exception ignored) { 102 | } 103 | 104 | return null; 105 | } 106 | 107 | /** 108 | * 智能下载或更新模板 109 | *

根据SHA256哈希值判断是否需要更新,下载成功后自动更新Lucene索引 110 | * 111 | * @param remoteConfig 远程模板配置 112 | * @return true-下载成功,false-下载失败 113 | */ 114 | public boolean smartDownloadTemplate(RemoteMetaConfig remoteConfig) { 115 | String localRepoPath = repositoryConfig.getRepositoryDir(); 116 | String remoteBaseUrl = repositoryConfig.getRemotePath(); 117 | boolean success = SDKUtils.smartDownloadTemplate(localRepoPath, remoteBaseUrl, remoteConfig); 118 | 119 | // 下载成功后更新Lucene索引 120 | if (success) { 121 | try { 122 | String groupId = remoteConfig.getGroupId(); 123 | String artifactId = remoteConfig.getArtifactId(); 124 | String description = remoteConfig.getDescription(); 125 | String metaPath = localRepoPath + File.separator + groupId + File.separator + 126 | artifactId + File.separator + "meta.json"; 127 | 128 | // 提取路径关键词 129 | String pathKeywords = extractPathKeywordsFromRemoteConfig(remoteConfig); 130 | 131 | luceneIndexService.updateIndex(groupId, artifactId, description, pathKeywords, metaPath); 132 | } catch (Exception ignored) { 133 | } 134 | } 135 | return success; 136 | } 137 | 138 | /** 139 | * 从远程配置提取路径关键词 140 | * 141 | * @param remoteConfig 远程配置 142 | * @return 路径关键词 143 | */ 144 | private String extractPathKeywordsFromRemoteConfig(RemoteMetaConfig remoteConfig) { 145 | HashSet keywords = new HashSet<>(); 146 | if (remoteConfig.getConfig() != null && remoteConfig.getConfig().getFiles() != null) { 147 | for (var file : remoteConfig.getConfig().getFiles()) { 148 | String path = file.getFilePath(); 149 | if (path != null && !path.isEmpty()) { 150 | String[] segments = path.split("[/\\\\]"); 151 | for (String seg : segments) { 152 | if (!seg.isEmpty() && !seg.equals(".")) { 153 | keywords.add(seg); 154 | } 155 | } 156 | } 157 | } 158 | } 159 | return String.join(" ", keywords); 160 | } 161 | 162 | /** 163 | * 从远程仓库获取元配置 164 | * 165 | * @param templateKeyword 模板关键词 166 | * @return 远程模板配置 167 | */ 168 | public RemoteMetaConfig fetchRemoteMetaConfig(String templateKeyword) { 169 | String remoteBaseUrl = repositoryConfig.getRemotePath(); 170 | return SDKUtils.fetchRemoteMetaConfig(remoteBaseUrl, templateKeyword); 171 | } 172 | 173 | /** 174 | * 读取模板文件内容 175 | *

从本地缓存目录读取模板文件的完整内容 176 | * 177 | * @param info 模板元信息 178 | * @return 模板文件内容字符串 179 | * @throws IOException 文件不存在或读取失败 180 | */ 181 | private String readTemplateContent(MetaInfo info) throws IOException { 182 | String localCachePath = repositoryConfig.getRepositoryDir(); 183 | 184 | // 拼装模板文件绝对路径(本地缓存根目录 + groupId + artifactId + version + filePath + filename) 185 | Path templatePath = Paths.get(localCachePath, 186 | info.getGroupId(), 187 | info.getArtifactId(), 188 | info.getVersion(), 189 | info.getFilePath(), 190 | info.getFilename()) 191 | .toAbsolutePath() 192 | .normalize(); 193 | 194 | // 校验文件是否存在 195 | if (!Files.exists(templatePath)) { 196 | throw new IOException("模板文件不存在: " + templatePath); 197 | } 198 | 199 | // 读取文件内容(一次性读入,文件通常几十KB以内,性能足够) 200 | return Files.readString(templatePath, StandardCharsets.UTF_8); 201 | } 202 | 203 | /** 204 | * 是否启用远程检索 205 | * 206 | * @return true-远程检索, false-本地Lucene检索 207 | */ 208 | public boolean isRemoteSearchEnabled() { 209 | return repositoryConfig.isRemoteSearchEnabled(); 210 | } 211 | 212 | /** 213 | * 判断是否为同一groupId的搜索(命名空间搜索) 214 | * 215 | * @param results 搜索结果 216 | * @return true表示所有结果属于同一groupId 217 | */ 218 | public boolean isGroupIdSearch(List results) { 219 | if (results.size() <= 1) return false; 220 | String firstGroupId = results.get(0).groupId(); 221 | return results.stream().allMatch(r -> r.groupId().equals(firstGroupId)); 222 | } 223 | 224 | /** 225 | * 构建按groupId聚合的结果 226 | *

展示该命名空间下所有模板的目录树和聚合描述 227 | * 228 | * @param keyword 搜索关键词 229 | * @param results 同一groupId的所有模板搜索结果 230 | * @return 聚合后的目录树字符串 231 | */ 232 | public String buildGroupAggregatedResult(String keyword, List results) { 233 | String groupId = results.get(0).groupId(); 234 | List allMetaInfos = new ArrayList<>(); 235 | 236 | for (LuceneIndexService.SearchResult result : results) { 237 | List metaInfos = searchLocalRepository(result.groupId(), result.artifactId()); 238 | allMetaInfos.addAll(metaInfos); 239 | } 240 | 241 | if (allMetaInfos.isEmpty()) { 242 | return "本地仓库模板文件不完整,请检查模板目录"; 243 | } 244 | 245 | TreeNode treeNode = PromptUtils.buildTree(allMetaInfos); 246 | String treeStr = PromptUtils.buildTreeStr(treeNode, "").trim(); 247 | 248 | StringBuilder artifactList = new StringBuilder(); 249 | for (LuceneIndexService.SearchResult r : results) { 250 | artifactList.append(" - ").append(r.artifactId()).append("\n"); 251 | } 252 | 253 | String description = promptService.buildGroupAggregated( 254 | groupId, 255 | String.valueOf(results.size()), 256 | artifactList.toString()); 257 | 258 | return promptService.buildSearchResult(groupId, treeStr, description); 259 | } 260 | 261 | /** 262 | * 构建多结果响应 263 | *

当搜索匹配多个不同的模板时,返回格式化的模板列表 264 | * 265 | * @param keyword 搜索关键词 266 | * @param results 搜索结果列表 267 | * @return 格式化的多结果响应字符串 268 | */ 269 | public String buildMultiResultResponse(String keyword, List results) { 270 | StringBuilder resultList = new StringBuilder(); 271 | for (int i = 0; i < results.size(); i++) { 272 | LuceneIndexService.SearchResult result = results.get(i); 273 | resultList.append(String.format("%d. %s/%s - %s\n", 274 | i + 1, 275 | result.groupId(), 276 | result.artifactId(), 277 | result.description() != null && !result.description().isEmpty() 278 | ? result.description().split("\n")[0] 279 | : result.artifactId())); 280 | } 281 | 282 | LuceneIndexService.SearchResult first = results.get(0); 283 | return promptService.buildMultiResult( 284 | String.valueOf(results.size()), 285 | keyword, 286 | resultList.toString(), 287 | first.groupId() + "/" + first.artifactId()); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/service/LuceneIndexService.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.service; 2 | 3 | import cn.hutool.core.io.FileUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import cn.hutool.json.JSONUtil; 6 | import jakarta.annotation.PostConstruct; 7 | import jakarta.annotation.PreDestroy; 8 | import lombok.RequiredArgsConstructor; 9 | import org.apache.lucene.analysis.Analyzer; 10 | import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; 11 | import org.apache.lucene.document.*; 12 | import org.apache.lucene.index.*; 13 | import org.apache.lucene.queryparser.classic.QueryParser; 14 | import org.apache.lucene.search.*; 15 | import org.apache.lucene.store.Directory; 16 | import org.apache.lucene.store.FSDirectory; 17 | import org.springframework.stereotype.Service; 18 | import top.codestyle.mcp.config.RepositoryConfig; 19 | import top.codestyle.mcp.model.meta.LocalMetaConfig; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.nio.file.*; 24 | import java.util.ArrayList; 25 | import java.util.Collections; 26 | import java.util.HashSet; 27 | import java.util.List; 28 | import java.util.concurrent.locks.ReentrantReadWriteLock; 29 | 30 | /** 31 | * Lucene本地索引服务 - 模板索引和检索 32 | * 33 | * @author movclantian 34 | * @since 2025-12-02 35 | */ 36 | @Service 37 | @RequiredArgsConstructor 38 | public class LuceneIndexService { 39 | 40 | private static final String INDEX_DIR = "lucene-index", 41 | F_GID = "groupId", 42 | F_AID = "artifactId", 43 | F_DESC = "description", 44 | F_PATH = "metaPath", 45 | F_PATH_KEYWORDS = "pathKeywords", 46 | F_CONTENT = "content"; 47 | 48 | private final RepositoryConfig repositoryConfig; 49 | private final ReentrantReadWriteLock indexLock = new ReentrantReadWriteLock(); 50 | private Directory directory; 51 | private Analyzer analyzer; 52 | private volatile long lastIndexBuildTime = 0; 53 | private volatile int lastMetaFileCount = 0; 54 | private volatile long lastCheckTime = 0; 55 | private static final long CHECK_INTERVAL_MS = 5000; // 检查间隔5秒 56 | 57 | /** 58 | * 初始化Lucene索引服务 59 | *

创建索引目录,初始化中文分词器,并重建索引 60 | * 61 | * @throws IOException 索引目录创建失败 62 | */ 63 | @PostConstruct 64 | public void init() throws IOException { 65 | var indexPath = Paths.get(repositoryConfig.getRepositoryDir(), INDEX_DIR); 66 | FileUtil.mkdir(indexPath.toFile()); 67 | directory = FSDirectory.open(indexPath); 68 | analyzer = new SmartChineseAnalyzer(); 69 | rebuildIndex(); 70 | } 71 | 72 | /** 73 | * 销毁Lucene索引服务 74 | *

关闭索引目录资源 75 | * 76 | * @throws IOException 关闭失败 77 | */ 78 | @PreDestroy 79 | public void destroy() throws IOException { 80 | if (directory != null) { 81 | directory.close(); 82 | } 83 | } 84 | 85 | /** 86 | * 重建索引 87 | *

扫描本地仓库所有meta.json文件并建立索引 88 | * 89 | * @throws IOException 索引写入失败 90 | */ 91 | public void rebuildIndex() throws IOException { 92 | indexLock.writeLock().lock(); 93 | try { 94 | var config = new IndexWriterConfig(analyzer).setOpenMode(IndexWriterConfig.OpenMode.CREATE); 95 | try (var writer = new IndexWriter(directory, config)) { 96 | scanAndIndexTemplates(writer, repositoryConfig.getRepositoryDir()); 97 | } 98 | lastIndexBuildTime = System.currentTimeMillis(); 99 | lastMetaFileCount = countMetaFiles(new File(repositoryConfig.getRepositoryDir())); 100 | } finally { 101 | indexLock.writeLock().unlock(); 102 | } 103 | } 104 | 105 | /** 106 | * 扫描并索引模板 107 | * 108 | * @param writer 索引写入器 109 | * @param basePath 基础路径 110 | * @throws IOException 扫描失败 111 | */ 112 | private void scanAndIndexTemplates(IndexWriter writer, String basePath) throws IOException { 113 | var baseDir = new File(basePath); 114 | if (!baseDir.isDirectory()) 115 | return; 116 | var groupDirs = baseDir.listFiles(File::isDirectory); 117 | if (groupDirs == null) 118 | return; 119 | 120 | for (var groupDir : groupDirs) { 121 | if (INDEX_DIR.equals(groupDir.getName())) 122 | continue; 123 | var artifactDirs = groupDir.listFiles(File::isDirectory); 124 | if (artifactDirs == null) 125 | continue; 126 | for (var artifactDir : artifactDirs) { 127 | var metaFile = new File(artifactDir, "meta.json"); 128 | if (metaFile.exists()) 129 | indexTemplate(writer, metaFile); 130 | } 131 | } 132 | } 133 | 134 | /** 135 | * 索引单个模板 136 | * 137 | * @param writer 索引写入器 138 | * @param metaFile meta.json文件 139 | */ 140 | private void indexTemplate(IndexWriter writer, File metaFile) { 141 | try { 142 | var meta = JSONUtil.toBean(FileUtil.readUtf8String(metaFile), LocalMetaConfig.class); 143 | var desc = readDescription(metaFile.getParentFile(), meta); 144 | var pathKeywords = extractPathKeywords(meta); 145 | writer.addDocument(createDoc(meta.getGroupId(), meta.getArtifactId(), desc, pathKeywords, metaFile.getAbsolutePath())); 146 | } catch (Exception ignored) { 147 | // 单个模板索引失败不影响其他模板 148 | } 149 | } 150 | 151 | /** 152 | * 提取路径关键词 153 | * 从meta.json中提取所有文件路径的目录名作为关键词 154 | * 155 | * @param meta 元配置 156 | * @return 路径关键词字符串 157 | */ 158 | private String extractPathKeywords(LocalMetaConfig meta) { 159 | HashSet keywords = new HashSet<>(); 160 | if (meta.getConfigs() != null) { 161 | for (var config : meta.getConfigs()) { 162 | if (config.getFiles() != null) { 163 | for (var file : config.getFiles()) { 164 | String path = file.getFilePath(); 165 | if (path != null && !path.isEmpty()) { 166 | // 分割路径: /bankend/src/main → [bankend, src, main] 167 | String[] segments = path.split("[/\\\\]"); 168 | for (String seg : segments) { 169 | if (!seg.isEmpty() && !seg.equals(".")) { 170 | keywords.add(seg); 171 | } 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | return String.join(" ", keywords); 179 | } 180 | 181 | /** 182 | * 从README.md读取描述信息 183 | * 184 | * @param artifactDir 模板目录 185 | * @param meta 元配置信息 186 | * @return 描述内容 187 | */ 188 | private String readDescription(File artifactDir, LocalMetaConfig meta) { 189 | var configs = meta.getConfigs(); 190 | if (configs == null || configs.isEmpty()) 191 | return meta.getArtifactId(); 192 | var readme = new File(artifactDir, configs.get(configs.size() - 1).getVersion() + File.separator + "README.md"); 193 | return readme.exists() ? FileUtil.readUtf8String(readme) : meta.getArtifactId(); 194 | } 195 | 196 | /** 197 | * 创建Lucene文档 198 | * 199 | * @param groupId 组ID 200 | * @param artifactId 项目ID 201 | * @param desc 模板描述 202 | * @param pathKeywords 路径关键词 203 | * @param metaPath meta.json路径 204 | * @return Lucene文档 205 | */ 206 | private Document createDoc(String groupId, String artifactId, String desc, String pathKeywords, String metaPath) { 207 | var doc = new Document(); 208 | doc.add(new StringField(F_GID, groupId, Field.Store.YES)); 209 | doc.add(new StringField(F_AID, artifactId, Field.Store.YES)); 210 | doc.add(new StringField(F_PATH, metaPath, Field.Store.YES)); 211 | doc.add(new TextField(F_DESC, StrUtil.nullToEmpty(desc), Field.Store.YES)); 212 | doc.add(new TextField(F_PATH_KEYWORDS, StrUtil.nullToEmpty(pathKeywords), Field.Store.NO)); 213 | doc.add(new TextField(F_CONTENT, String.join(" ", groupId, artifactId, StrUtil.nullToEmpty(desc), StrUtil.nullToEmpty(pathKeywords)), 214 | Field.Store.NO)); 215 | return doc; 216 | } 217 | 218 | /** 219 | * 更新单个模板的索引 220 | * 221 | * @param groupId 组ID 222 | * @param artifactId 项目ID 223 | * @param desc 模板描述 224 | * @param pathKeywords 路径关键词 225 | * @param metaPath meta.json路径 226 | */ 227 | public void updateIndex(String groupId, String artifactId, String desc, String pathKeywords, String metaPath) { 228 | indexLock.writeLock().lock(); 229 | try { 230 | var config = new IndexWriterConfig(analyzer).setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); 231 | try (var writer = new IndexWriter(directory, config)) { 232 | writer.deleteDocuments(new Term(F_PATH, metaPath)); 233 | writer.addDocument(createDoc(groupId, artifactId, desc, pathKeywords, metaPath)); 234 | } 235 | } catch (IOException ignored) { 236 | // 索引更新失败不影响主流程 237 | } finally { 238 | indexLock.writeLock().unlock(); 239 | } 240 | } 241 | 242 | /** 243 | * 本地检索模板 - 全文搜索 244 | *

在所有字段(groupId、artifactId、description、pathKeywords)中搜索,返回评分最高的结果。 245 | * 支持两种搜索模式: 246 | *

    247 | *
  • 关键词搜索:搜索所有字段
  • 248 | *
  • 精确搜索:使用 groupId/artifactId 格式精确匹配
  • 249 | *
250 | * 自动检测仓库更新并重建索引。 251 | * 252 | * @param keyword 搜索关键词或 groupId/artifactId 格式 253 | * @return 匹配的模板列表(按相关度排序) 254 | */ 255 | public List fetchLocalMetaConfig(String keyword) { 256 | // 自动检测并重建索引(如果仓库有更新) 257 | autoRebuildIndexIfNeeded(); 258 | 259 | indexLock.readLock().lock(); 260 | try { 261 | if (!DirectoryReader.indexExists(directory)) { 262 | indexLock.readLock().unlock(); 263 | rebuildIndex(); 264 | indexLock.readLock().lock(); 265 | } 266 | try (var reader = DirectoryReader.open(directory)) { 267 | var searcher = new IndexSearcher(reader); 268 | 269 | Query query; 270 | // 检测 "groupId/artifactId" 格式 271 | if (keyword.contains("/")) { 272 | String[] parts = keyword.split("/", 2); 273 | if (parts.length == 2) { 274 | // 精确匹配 groupId 和 artifactId 275 | var builder = new BooleanQuery.Builder(); 276 | builder.add(new TermQuery(new Term(F_GID, parts[0])), BooleanClause.Occur.MUST); 277 | builder.add(new TermQuery(new Term(F_AID, parts[1])), BooleanClause.Occur.MUST); 278 | query = builder.build(); 279 | } else { 280 | // 格式错误,降级为全文搜索 281 | var parser = new QueryParser(F_CONTENT, analyzer); 282 | parser.setDefaultOperator(QueryParser.Operator.OR); 283 | query = parser.parse(QueryParser.escape(keyword)); 284 | } 285 | } else { 286 | // 全文搜索 287 | var parser = new QueryParser(F_CONTENT, analyzer); 288 | parser.setDefaultOperator(QueryParser.Operator.OR); 289 | String queryStr = keyword.matches(".*[+\\-&|!(){}\\[\\]^\"~*?:\\\\/].*") 290 | ? QueryParser.escape(keyword) 291 | : keyword; 292 | query = parser.parse(queryStr); 293 | } 294 | 295 | var topDocs = searcher.search(query, Integer.MAX_VALUE); 296 | var results = new ArrayList(); 297 | 298 | for (var scoreDoc : topDocs.scoreDocs) { 299 | var doc = reader.storedFields().document(scoreDoc.doc); 300 | results.add(new SearchResult( 301 | doc.get(F_GID), 302 | doc.get(F_AID), 303 | doc.get(F_DESC), 304 | doc.get(F_PATH) 305 | )); 306 | } 307 | return results; 308 | } 309 | } catch (Exception ignored) { 310 | // 检索失败返回空列表 311 | } finally { 312 | indexLock.readLock().unlock(); 313 | } 314 | return Collections.emptyList(); 315 | } 316 | 317 | /** 318 | * 自动检测仓库更新并重建索引 319 | *

为避免频繁检查造成性能损失,最多每5秒检查一次。 320 | * 检查文件修改时间和文件数量,满足以下任一条件则重建索引: 321 | *

    322 | *
  • 有 meta.json 文件修改时间晚于上次索引构建时间(新增/修改)
  • 323 | *
  • meta.json 文件数量发生变化(删除/新增)
  • 324 | *
325 | */ 326 | private void autoRebuildIndexIfNeeded() { 327 | try { 328 | long now = System.currentTimeMillis(); 329 | 330 | // 距离上次检查不足5秒,跳过检查 331 | if (now - lastCheckTime < CHECK_INTERVAL_MS) { 332 | return; 333 | } 334 | 335 | if (lastIndexBuildTime == 0) { 336 | return; // 首次构建,跳过检查 337 | } 338 | 339 | File baseDir = new File(repositoryConfig.getRepositoryDir()); 340 | if (!baseDir.isDirectory()) { 341 | return; 342 | } 343 | 344 | // 更新检查时间 345 | lastCheckTime = now; 346 | 347 | // 检查文件数量变化 348 | int currentCount = countMetaFiles(baseDir); 349 | if (currentCount != lastMetaFileCount) { 350 | rebuildIndex(); 351 | return; 352 | } 353 | 354 | // 检查文件修改时间 355 | if (hasNewerMetaFiles(baseDir, lastIndexBuildTime)) { 356 | rebuildIndex(); 357 | } 358 | } catch (Exception ignored) { 359 | // 自动重建失败不影响主流程 360 | } 361 | } 362 | 363 | /** 364 | * 递归检查是否有比指定时间更新的meta.json文件 365 | * 366 | * @param dir 目录 367 | * @param timestamp 时间戳 368 | * @return true表示有更新的文件 369 | */ 370 | private boolean hasNewerMetaFiles(File dir, long timestamp) { 371 | File[] files = dir.listFiles(); 372 | if (files == null) { 373 | return false; 374 | } 375 | 376 | for (File file : files) { 377 | if (file.isDirectory()) { 378 | // 跳过lucene索引目录 379 | if (INDEX_DIR.equals(file.getName())) { 380 | continue; 381 | } 382 | // 递归检查子目录 383 | if (hasNewerMetaFiles(file, timestamp)) { 384 | return true; 385 | } 386 | } else if ("meta.json".equals(file.getName())) { 387 | // 检查meta.json修改时间 388 | if (file.lastModified() > timestamp) { 389 | return true; 390 | } 391 | } 392 | } 393 | return false; 394 | } 395 | 396 | /** 397 | * 递归统计meta.json文件数量 398 | * 399 | * @param dir 目录 400 | * @return meta.json文件数量 401 | */ 402 | private int countMetaFiles(File dir) { 403 | File[] files = dir.listFiles(); 404 | if (files == null) { 405 | return 0; 406 | } 407 | 408 | int count = 0; 409 | for (File file : files) { 410 | if (file.isDirectory()) { 411 | // 跳过lucene索引目录 412 | if (INDEX_DIR.equals(file.getName())) { 413 | continue; 414 | } 415 | // 递归统计子目录 416 | count += countMetaFiles(file); 417 | } else if ("meta.json".equals(file.getName())) { 418 | count++; 419 | } 420 | } 421 | return count; 422 | } 423 | 424 | /** 425 | * 检索结果记录 426 | * 427 | * @param groupId 组ID 428 | * @param artifactId 项目ID 429 | * @param description 模板描述 430 | * @param metaPath meta.json路径 431 | */ 432 | public record SearchResult(String groupId, String artifactId, String description, String metaPath) { 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /src/main/java/top/codestyle/mcp/util/SDKUtils.java: -------------------------------------------------------------------------------- 1 | package top.codestyle.mcp.util; 2 | 3 | import cn.hutool.core.collection.CollUtil; 4 | import cn.hutool.core.io.FileUtil; 5 | import cn.hutool.core.io.IoUtil; 6 | import cn.hutool.core.util.StrUtil; 7 | import cn.hutool.core.util.ZipUtil; 8 | import cn.hutool.http.HttpRequest; 9 | import cn.hutool.http.HttpResponse; 10 | import cn.hutool.json.JSONUtil; 11 | import top.codestyle.mcp.model.meta.LocalMetaConfig; 12 | import top.codestyle.mcp.model.sdk.MetaInfo; 13 | import top.codestyle.mcp.model.sdk.RemoteMetaConfig; 14 | 15 | import java.io.*; 16 | import java.util.*; 17 | import java.util.regex.Pattern; 18 | 19 | /** 20 | * 模板仓库SDK工具类 21 | * 提供模板搜索、远程下载、本地管理等功能 22 | * 23 | * @author 小航love666, Kanttha, movclantian 24 | * @since 2025-09-29 25 | */ 26 | public class SDKUtils { 27 | 28 | /** 29 | * 根据groupId和artifactId搜索指定模板组 30 | * 31 | * @param groupId 组ID 32 | * @param artifactId 项目ID 33 | * @param templateBasePath 模板基础路径 34 | * @return 匹配的模板元信息列表 35 | */ 36 | public static List searchLocalRepository(String groupId, String artifactId, String templateBasePath) { 37 | List result = new ArrayList<>(); 38 | try { 39 | templateBasePath = normalizePath(templateBasePath); 40 | File metaFile = new File(templateBasePath + File.separator + groupId + File.separator + artifactId 41 | + File.separator + "meta.json"); 42 | if (!metaFile.exists()) { 43 | return result; 44 | } 45 | List metaInfoList = MetaInfoConvertUtil.parseMetaJsonLatestOnly(metaFile); 46 | for (MetaInfo metaInfo : metaInfoList) { 47 | if (isTemplateFileExists(templateBasePath, metaInfo)) { 48 | result.add(metaInfo); 49 | } 50 | } 51 | } catch (Exception e) { 52 | // 搜索失败,返回空结果 53 | } 54 | return result; 55 | } 56 | 57 | /** 58 | * 根据精确路径搜索模板 59 | * 60 | * @param exactPath 精确路径,格式: groupId/artifactId/version/filePath/filename 61 | * @param templateBasePath 模板基础路径 62 | * @return 匹配的模板元信息,未找到返回null 63 | */ 64 | public static MetaInfo searchByPath(String exactPath, String templateBasePath) { 65 | try { 66 | // 规范化路径 67 | templateBasePath = normalizePath(templateBasePath); 68 | String normalizedExactPath = normalizePath(exactPath); 69 | 70 | // 从路径解析 groupId 和 artifactId,直接定位 meta.json 71 | // 路径格式: groupId/artifactId/version/filePath/filename 72 | String[] parts = normalizedExactPath.split(Pattern.quote(File.separator)); 73 | if (parts.length < 3) { 74 | return null; 75 | } 76 | String groupId = parts[0]; 77 | String artifactId = parts[1]; 78 | 79 | // 直接定位 meta.json 文件 80 | File metaFile = new File(templateBasePath + File.separator + groupId + File.separator + artifactId 81 | + File.separator + "meta.json"); 82 | if (!metaFile.exists()) { 83 | return null; 84 | } 85 | 86 | // 在匹配的 meta.json 中查找模板 87 | List metaInfoList = MetaInfoConvertUtil.parseMetaJsonLatestOnly(metaFile); 88 | for (MetaInfo metaInfo : metaInfoList) { 89 | String fullPath = metaInfo.getGroupId() + File.separator + metaInfo.getArtifactId() + File.separator + 90 | metaInfo.getVersion() + metaInfo.getFilePath() + File.separator + metaInfo.getFilename(); 91 | if (normalizePath(fullPath).equals(normalizedExactPath)) { 92 | return isTemplateFileExists(templateBasePath, metaInfo) ? metaInfo : null; 93 | } 94 | } 95 | } catch (Exception e) { 96 | // 搜索失败 97 | } 98 | return null; 99 | } 100 | 101 | /** 102 | * 从远程仓库获取元配置 103 | * 104 | * @param remoteBaseUrl 远程仓库基础URL 105 | * @param query 模板关键词,如: RuoYi, CRUD 106 | * @return 远程模板配置,失败返回null 107 | */ 108 | public static RemoteMetaConfig fetchRemoteMetaConfig(String remoteBaseUrl, String query) { 109 | try { 110 | String responseBody = HttpRequest.get(remoteBaseUrl + "/api/mcp/search") 111 | .form("query", query) 112 | .timeout(30000) 113 | .header("User-Agent", "MCP-CodeStyle-Server/1.0") 114 | .execute() 115 | .body(); 116 | 117 | return JSONUtil.toBean(responseBody, RemoteMetaConfig.class); 118 | 119 | } catch (Exception e) { 120 | return null; 121 | } 122 | } 123 | 124 | /** 125 | * 智能下载或更新模板 126 | * 通过SHA256哈希值判断是否需要更新,下载ZIP并解压到本地仓库,更新meta.json 127 | * 128 | * @param localRepoPath 本地仓库路径 129 | * @param remoteBaseUrl 远程仓库基础URL 130 | * @param remoteConfig 远程模板配置 131 | * @return 是否成功 132 | */ 133 | public static boolean smartDownloadTemplate(String localRepoPath, String remoteBaseUrl, 134 | RemoteMetaConfig remoteConfig) { 135 | try { 136 | String groupId = remoteConfig.getGroupId(); 137 | String artifactId = remoteConfig.getArtifactId(); 138 | 139 | boolean needsUpdate = false; 140 | try { 141 | String localMetaPath = localRepoPath + File.separator + 142 | groupId + File.separator + 143 | artifactId + File.separator + 144 | "meta.json"; 145 | File localMetaFile = new File(localMetaPath); 146 | 147 | if (!localMetaFile.exists()) { 148 | needsUpdate = true; 149 | } else { 150 | needsUpdate = checkIfNeedsUpdate(localMetaFile, remoteConfig, localRepoPath, groupId, artifactId); 151 | } 152 | } catch (Exception e) { 153 | needsUpdate = true; 154 | } 155 | 156 | if (needsUpdate) { 157 | return downloadAndExtractTemplate(localRepoPath, remoteBaseUrl, groupId, artifactId, remoteConfig); 158 | } else { 159 | return true; 160 | } 161 | 162 | } catch (Exception e) { 163 | return false; 164 | } 165 | } 166 | 167 | /** 168 | * 验证模板文件是否存在 169 | * 170 | * @param templateBasePath 模板基础路径 171 | * @param metaInfo 模板元信息 172 | * @return 文件是否存在 173 | */ 174 | private static boolean isTemplateFileExists(String templateBasePath, MetaInfo metaInfo) { 175 | String normalizedFilePath = StrUtil.removePrefix(normalizePath(metaInfo.getFilePath()), File.separator); 176 | String versionPath = metaInfo.getVersion(); 177 | 178 | String actualFilePath = templateBasePath + File.separator + 179 | metaInfo.getGroupId() + File.separator + 180 | metaInfo.getArtifactId() + File.separator + 181 | versionPath + File.separator + 182 | normalizedFilePath + File.separator + 183 | metaInfo.getFilename(); 184 | return new File(actualFilePath).exists(); 185 | } 186 | 187 | /** 188 | * 检查是否需要更新模板 189 | * 190 | * @param localMetaFile 本地meta.json文件 191 | * @param remoteConfig 远程配置 192 | * @param localRepoPath 本地仓库路径 193 | * @param groupId 组ID 194 | * @param artifactId 项目ID 195 | * @return 是否需要更新 196 | */ 197 | private static boolean checkIfNeedsUpdate(File localMetaFile, RemoteMetaConfig remoteConfig, 198 | String localRepoPath, String groupId, String artifactId) { 199 | try { 200 | LocalMetaConfig localConfig = JSONUtil.toBean(FileUtil.readUtf8String(localMetaFile), 201 | LocalMetaConfig.class); 202 | String remoteVersion = remoteConfig.getConfig().getVersion(); 203 | 204 | LocalMetaConfig.Config matchedConfig = findMatchedConfig(localConfig, remoteVersion); 205 | if (matchedConfig == null) { 206 | return true; 207 | } 208 | 209 | List remoteFiles = remoteConfig.getConfig().getFiles(); 210 | if (CollUtil.isEmpty(remoteFiles)) { 211 | return false; 212 | } 213 | 214 | List localFiles = matchedConfig.getFiles(); 215 | String versionPath = remoteVersion; 216 | 217 | for (RemoteMetaConfig.FileInfo remoteFile : remoteFiles) { 218 | String normalizedFilePath = normalizePath(remoteFile.getFilePath()); 219 | if (normalizedFilePath.startsWith(File.separator)) { 220 | normalizedFilePath = normalizedFilePath.substring(1); 221 | } 222 | 223 | String actualFilePath = localRepoPath + File.separator + groupId + File.separator + 224 | artifactId + File.separator + versionPath + File.separator + normalizedFilePath 225 | + File.separator + remoteFile.getFilename(); 226 | 227 | if (!new File(actualFilePath).exists()) { 228 | return true; 229 | } 230 | 231 | if (isFileShaChanged(localFiles, remoteFile.getFilename(), remoteFile.getFilePath(), 232 | StrUtil.emptyToDefault(remoteFile.getSha256(), ""))) { 233 | return true; 234 | } 235 | } 236 | return false; 237 | } catch (Exception e) { 238 | return true; 239 | } 240 | } 241 | 242 | /** 243 | * 查找匹配的版本配置 244 | * 245 | * @param localConfig 本地配置 246 | * @param version 版本号 247 | * @return 匹配的配置,未找到返回null 248 | */ 249 | private static LocalMetaConfig.Config findMatchedConfig(LocalMetaConfig localConfig, String version) { 250 | if (localConfig.getConfigs() != null) { 251 | for (LocalMetaConfig.Config config : localConfig.getConfigs()) { 252 | if (config.getVersion().equals(version)) { 253 | return config; 254 | } 255 | } 256 | } 257 | return null; 258 | } 259 | 260 | /** 261 | * 检查文件SHA256是否变化 262 | * 263 | * @param localFiles 本地文件列表 264 | * @param filename 文件名 265 | * @param filePath 文件路径 266 | * @param remoteSha 远程SHA256 267 | * @return SHA是否变化 268 | */ 269 | private static boolean isFileShaChanged(List localFiles, 270 | String filename, String filePath, String remoteSha) { 271 | if (localFiles == null) { 272 | return true; 273 | } 274 | 275 | for (LocalMetaConfig.FileInfo localFile : localFiles) { 276 | if (localFile.getFilename().equals(filename) && localFile.getFilePath().equals(filePath)) { 277 | String localSha = StrUtil.emptyToDefault(localFile.getSha256(), ""); 278 | return !localSha.equals(remoteSha); 279 | } 280 | } 281 | return true; 282 | } 283 | 284 | /** 285 | * 下载并解压模板 286 | * 287 | * @param localRepoPath 本地仓库路径 288 | * @param remoteBaseUrl 远程基础URL 289 | * @param groupId 组ID 290 | * @param artifactId 项目ID 291 | * @param remoteConfig 远程配置 292 | * @return 是否成功 293 | */ 294 | private static boolean downloadAndExtractTemplate(String localRepoPath, String remoteBaseUrl, 295 | String groupId, String artifactId, 296 | RemoteMetaConfig remoteConfig) { 297 | String templatePath = File.separator + groupId + File.separator + artifactId; 298 | String templateDir = localRepoPath + File.separator + groupId + File.separator + artifactId; 299 | File localMetaFile = new File(templateDir, "meta.json"); 300 | String backupContent = null; 301 | File zipFile = null; 302 | 303 | try { 304 | // 备份现有meta.json内容,用于后续版本追加 305 | if (localMetaFile.exists()) { 306 | backupContent = FileUtil.readUtf8String(localMetaFile); 307 | } 308 | 309 | HttpResponse response = HttpRequest.get(remoteBaseUrl + "/api/file/load") 310 | .form("paths", templatePath) 311 | .timeout(60000) 312 | .header("User-Agent", "MCP-CodeStyle-Server/1.0") 313 | .execute(); 314 | 315 | if (!response.isOk()) { 316 | return false; 317 | } 318 | 319 | zipFile = FileUtil.createTempFile("template-", ".zip", true); 320 | 321 | IoUtil.copy(response.bodyStream(), FileUtil.getOutputStream(zipFile)); 322 | 323 | // 解压到仓库根目录 324 | if (extractZipFile(zipFile, localRepoPath, templateDir)) { 325 | updateLocalMetaJson(localRepoPath, groupId, artifactId, remoteConfig, backupContent); 326 | // 将远程的description写入README.md(缓存到本地) 327 | saveDescriptionToReadme(templateDir, remoteConfig); 328 | return true; 329 | } 330 | return false; 331 | } catch (Exception e) { 332 | return false; 333 | } finally { 334 | FileUtil.del(zipFile); 335 | } 336 | } 337 | 338 | /** 339 | * 解压ZIP文件 340 | * 341 | * @param zipFile ZIP文件 342 | * @param targetPath 目标路径 343 | * @param templateDir 当前模板目录 344 | * @return 是否成功 345 | */ 346 | private static boolean extractZipFile(File zipFile, String targetPath, String templateDir) { 347 | try { 348 | File targetDir = FileUtil.mkdir(targetPath); 349 | ZipUtil.unzip(zipFile, targetDir); 350 | // 仅删除当前模板目录下的meta.json,避免影响其他模板 351 | File metaFile = new File(templateDir, "meta.json"); 352 | if (metaFile.exists()) { 353 | FileUtil.del(metaFile); 354 | } 355 | return true; 356 | } catch (Exception e) { 357 | return false; 358 | } 359 | } 360 | 361 | /** 362 | * 更新本地meta.json文件 363 | * 364 | * @param localRepoPath 本地仓库路径 365 | * @param groupId 组ID 366 | * @param artifactId 项目ID 367 | * @param remoteConfig 远程配置 368 | * @param backupContent 备份的meta.json内容(用于版本追加) 369 | */ 370 | private static void updateLocalMetaJson(String localRepoPath, String groupId, 371 | String artifactId, RemoteMetaConfig remoteConfig, String backupContent) { 372 | 373 | String newVersion = remoteConfig.getConfig().getVersion(); 374 | 375 | String localMetaPath = localRepoPath + File.separator + groupId + File.separator + 376 | artifactId + File.separator + "meta.json"; 377 | 378 | File localMetaFile = new File(localMetaPath); 379 | 380 | LocalMetaConfig localConfig; 381 | 382 | // 优先使用备份内容,确保版本追加正确 383 | if (StrUtil.isNotBlank(backupContent)) { 384 | localConfig = JSONUtil.toBean(backupContent, LocalMetaConfig.class); 385 | } else if (FileUtil.exist(localMetaFile)) { 386 | localConfig = JSONUtil.toBean(FileUtil.readUtf8String(localMetaFile), LocalMetaConfig.class); 387 | } else { 388 | localConfig = new LocalMetaConfig(); 389 | localConfig.setGroupId(groupId); 390 | localConfig.setArtifactId(artifactId); 391 | localConfig.setConfigs(new ArrayList<>()); 392 | } 393 | 394 | List configs = localConfig.getConfigs(); 395 | if (CollUtil.isEmpty(configs)) { 396 | configs = new ArrayList<>(); 397 | localConfig.setConfigs(configs); 398 | } 399 | 400 | configs.removeIf(config -> config.getVersion().equals(newVersion)); 401 | 402 | LocalMetaConfig.Config newConfig = MetaInfoConvertUtil.convertRemoteToLocalConfig(remoteConfig); 403 | configs.add(newConfig); 404 | 405 | FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(localConfig), localMetaFile); 406 | } 407 | 408 | /** 409 | * 将远程description保存到本地README.md 410 | * 缓存路径: groupId/artifactId/version/README.md 411 | * 412 | * @param templateDir 模板目录 (groupId/artifactId) 413 | * @param remoteConfig 远程配置 414 | */ 415 | private static void saveDescriptionToReadme(String templateDir, RemoteMetaConfig remoteConfig) { 416 | String description = remoteConfig.getDescription(); 417 | if (StrUtil.isBlank(description)) { 418 | return; 419 | } 420 | 421 | String version = remoteConfig.getConfig().getVersion(); 422 | String readmePath = templateDir + File.separator + version + File.separator + "README.md"; 423 | File readmeFile = new File(readmePath); 424 | 425 | // 确保版本目录存在 426 | FileUtil.mkdir(readmeFile.getParentFile()); 427 | FileUtil.writeUtf8String(description, readmeFile); 428 | } 429 | 430 | /** 431 | * 规范化路径字符串 432 | * 统一路径分隔符并移除连续分隔符,确保跨平台兼容性 433 | * 434 | * @param path 原始路径字符串 435 | * @return 规范化后的路径字符串 436 | */ 437 | public static String normalizePath(String path) { 438 | if (StrUtil.isEmpty(path)) { 439 | return path; 440 | } 441 | 442 | // 统一使用系统分隔符 443 | String normalizedPath = path.replace('/', File.separatorChar).replace('\\', File.separatorChar); 444 | 445 | // 使用 StrUtil 移除连续的分隔符 446 | String doubleSep = File.separator + File.separator; 447 | while (StrUtil.contains(normalizedPath, doubleSep)) { 448 | normalizedPath = normalizedPath.replace(doubleSep, File.separator); 449 | } 450 | 451 | return normalizedPath; 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codestyle Server MCP【码蜂】 2 | 3 | Codestyle Server MCP 是一个基于 Spring AI 实现的 Model Context Protocol (MCP) 服务器,为 IDE 和 AI 代理提供代码模板搜索和检索工具。该服务从本地缓存查找模板,并在缺失时自动从远程仓库下载元数据和文件进行修复。 4 | 5 | ## 核心特性 6 | 7 | - **原生 MCP 工具**:`CodestyleService` 通过 `spring-ai-starter-mcp-server` 注册 `codestyleSearch` 和 `getTemplateByPath` 工具,STDIO 客户端(Cherry Studio、Cursor 等)可直接调用 8 | - **Lucene 本地全文检索**:集成 Apache Lucene ,支持中文分词(SmartChineseAnalyzer),离线环境下也能高效检索模板 9 | - **双模式检索**:支持本地 Lucene 检索(默认)和远程 API 检索两种模式,通过配置一键切换 10 | - **增量更新机制**:通过 SHA256 哈希值比对判断模板是否需要更新,避免重复下载 11 | - **自修复模板缓存**:本地未找到模板时自动触发远程下载,支持按需获取 12 | - **精确路径定位**:从请求路径解析 `groupId/artifactId`,直接定位 `meta.json` 13 | - **提示词模板化**:`PromptService` 使用 `content-result.txt` 和 `search-result.txt` 渲染响应,确保变量和文件内容遵循统一布局 14 | - **多版本共存**:采用 `groupId/artifactId/version/` Maven 风格目录结构,支持同一模板的多版本管理 15 | 16 | ## 技术栈 17 | 18 | - Java 17, Maven 3.9+ 19 | - Spring Boot 3.4.3 20 | - Spring AI MCP Server 1.1.0 21 | - Apache Lucene 9.12.3(本地全文检索引擎,支持中文分词) 22 | - Hutool 5.8.42(HTTP、文件、JSON、ZIP 工具) 23 | 24 | ## 架构设计 25 | 26 | ### 分层架构 27 | 28 | ``` 29 | ┌───────────────────────────────────────────────────────────────────┐ 30 | │ MCP 客户端 (Cursor/Cherry Studio) │ 31 | └───────────────────────────────────────────────────────────────────┘ 32 | │ 33 | │ STDIO 34 | ▼ 35 | ┌───────────────────────────────────────────────────────────────────┐ 36 | │ CodestyleService (@McpTool) │ 37 | │ ├── codestyleSearch(keyword) → 目录树 + 模板组介绍 │ 38 | │ └── getTemplateByPath(path) → 变量说明 + 模板内容 │ 39 | └───────────────────────────────────────────────────────────────────┘ 40 | │ 41 | ┌───────────────┴───────────────┐ 42 | │ │ 43 | ▼ ▼ 44 | ┌───────────────────────────────┐ ┌───────────────────────────────┐ 45 | │ LuceneIndexService │ │ TemplateService │ 46 | │ ├── rebuildIndex() │ │ ├── searchLocalRepository() │ 47 | │ ├── updateIndex() │ │ ├── searchByPath() │ 48 | │ └── fetchLocalMetaConfig() │ │ ├── fetchRemoteMetaConfig() │ 49 | └───────────────────────────────┘ │ └── smartDownloadTemplate() │ 50 | │ └───────────────────────────────┘ 51 | │ │ 52 | ▼ ▼ 53 | ┌───────────────────────────────┐ ┌───────────────────────────────┐ 54 | │ Lucene 索引 │ │ SDKUtils (核心工具层) │ 55 | │ (lucene-index/) │ │ ├── searchLocalRepository() │ 56 | │ ├── 中文分词 (SmartCN) │ │ ├── searchByPath() │ 57 | │ └── 全文检索 │ │ ├── fetchRemoteMetaConfig() │ 58 | └───────────────────────────────┘ │ ├── smartDownloadTemplate() │ 59 | │ └── downloadAndExtract...() │ 60 | └───────────────────────────────┘ 61 | │ 62 | ▼ 63 | ┌───────────────────────────────────┐ 64 | │ 本地缓存 (codestyle-cache/) │ 65 | │ └── {groupId}/{artifactId}/ │ 66 | │ ├── meta.json │ 67 | │ └── {version}/... │ 68 | └───────────────────────────────────┘ 69 | ``` 70 | 71 | ## 项目结构 72 | 73 | ```text 74 | mcp-codestyle-server 75 | ├── pom.xml 76 | ├── src/main 77 | │ ├── java/top/codestyle/mcp 78 | │ │ ├── McpServerApplication.java # 应用入口 79 | │ │ ├── config 80 | │ │ │ └── RepositoryConfig.java # 仓库路径配置(支持远程检索开关) 81 | │ │ ├── model 82 | │ │ │ ├── meta/ # 本地缓存模型 83 | │ │ │ │ ├── LocalMetaConfig.java # 本地 meta.json 结构(多版本) 84 | │ │ │ │ ├── LocalMetaInfo.java # 本地模板元信息 85 | │ │ │ │ └── LocalMetaVariable.java # 本地变量(预留扩展) 86 | │ │ │ ├── sdk/ # SDK 模型 87 | │ │ │ │ ├── MetaInfo.java # 通用模板元信息 88 | │ │ │ │ ├── MetaVariable.java # 模板变量 89 | │ │ │ │ └── RemoteMetaConfig.java # 远程 API 响应结构(单版本) 90 | │ │ │ └── tree/ # 目录树模型 91 | │ │ │ ├── Node.java # 节点接口 92 | │ │ │ └── TreeNode.java # 树节点实现 93 | │ │ ├── service 94 | │ │ │ ├── CodestyleService.java # MCP 工具实现(@McpTool) 95 | │ │ │ ├── LuceneIndexService.java # Lucene 本地索引服务(全文检索) 96 | │ │ │ ├── TemplateService.java # 模板业务编排 97 | │ │ │ └── PromptService.java # 提示词模板加载(懒加载) 98 | │ │ └── util 99 | │ │ ├── SDKUtils.java # 核心工具(搜索/下载/SHA256) 100 | │ │ ├── MetaInfoConvertUtil.java # 元信息转换 101 | │ │ └── PromptUtils.java # 目录树和变量格式化 102 | │ └── resources 103 | │ ├── application.yml # 配置文件 104 | │ ├── content-result.txt # 模板内容提示词模板 105 | │ └── search-result.txt # 搜索结果提示词模板 106 | └── examples/ # 示例模板 107 | └── continew/ # ContiNew 框架模板组 108 | ├── CRUD/ # 增删改查模板 109 | │ ├── meta.json # 模板元数据 110 | │ └── 1.0.0/ # 版本目录 111 | │ ├── README.md # 模板说明文档 112 | │ ├── backend/ # 后端模板 113 | │ └── frontend/ # 前端模板 114 | └── Logo/ # Logo 模板组 115 | └── 1.0.0/ 116 | ``` 117 | 118 | ## 配置说明 119 | 120 | 核心配置位于 `src/main/resources/application.yml`,可通过 JVM 系统属性覆盖。 121 | 122 | ```yaml 123 | spring: 124 | application: 125 | name: mcp-codestyle-server 126 | main: 127 | web-application-type: none # 关闭Web服务器 128 | banner-mode: off # 关闭启动横幅 129 | ai: 130 | mcp: 131 | server: 132 | name: mcp-codestyle-server # MCP服务器名称 133 | version: 1.0.0 # 版本号 134 | type: SYNC # 服务器类型: SYNC(同步) 或 ASYNC(异步) 135 | stdio: true # 启用STDIO模式 136 | annotation-scanner: 137 | enabled: true # 启用注解扫描 138 | 139 | repository: 140 | local-path: /var/cache/codestyle # 本地基础路径 141 | remote-path: http://localhost # 远程仓库地址(需配置) 142 | dir: # 可选,不配置则使用local-path/codestyle-cache 143 | remote-search-enabled: false # 是否启用远程检索(默认false,使用本地Lucene检索) 144 | ``` 145 | 146 | ### 配置项说明: 147 | 148 | - `repository.local-path`:本地缓存基础目录(可通过 `-Dcache.base-path` 覆盖) 149 | - `repository.dir`:具体缓存文件夹,默认为 `/codestyle-cache` 150 | - `repository.remote-path`:远程仓库基础 URL(仅在启用远程检索时需要配置) 151 | - `repository.remote-search-enabled`:**检索模式开关** 152 | - `false`(默认):使用本地 Lucene 全文检索,无需网络连接 153 | - `true`:使用远程 API 检索,需配置远程仓库地址 154 | 155 | ### 远程服务接口: 156 | 157 | 远程服务提供以下两个接口: 158 | 159 | 1. **获取模板元数据** 160 | 161 | ``` 162 | GET {remoteBaseUrl}/api/mcp/search?templateKeyword=CRUD 163 | ``` 164 | 165 | 返回 `RemoteMetaConfig` JSON 格式: 166 | 167 | ```json 168 | { 169 | "groupId": "backend", 170 | "artifactId": "CRUD", 171 | "description": "完整的CRUD操作模板", 172 | "config": { 173 | "version": "1.0.0", 174 | "files": [ 175 | { 176 | "filePath": "/src/main/java/com/air/controller", 177 | "filename": "Controller.ftl", 178 | "description": "控制器模板", 179 | "sha256": "abc123.", 180 | "inputVariables": [ 181 | { 182 | "variableName": "className", 183 | "variableType": "String", 184 | "variableComment": "类名", 185 | "example": "UserController" 186 | } 187 | ] 188 | } 189 | ] 190 | } 191 | } 192 | ``` 193 | 194 | 2. **下载模板 ZIP** 195 | ``` 196 | GET {remoteBaseUrl}/api/file/load?paths=/groupId/artifactId 197 | ``` 198 | 返回包含模板文件的 ZIP 压缩包,解压后目录结构应为: 199 | ``` 200 | src/ 201 | main/ 202 | java/ 203 | com/ 204 | air/ 205 | controller/ 206 | Controller.ftl 207 | ``` 208 | 209 | ## 快速开始 210 | 211 | ### 1. 前置条件 212 | 213 | - JDK 17+ 214 | - Maven 3.9+(或使用项目自带的 `mvnw` / `mvnw.cmd`) 215 | 216 | ### 2. 克隆并构建 217 | 218 | ```bash 219 | git clone https://github.com/itxaiohanglover/mcp-codestyle-server.git 220 | cd mcp-codestyle-server 221 | ./mvnw clean package -DskipTests 222 | ``` 223 | 224 | ### 3. 配置远程仓库地址 225 | 226 | 编辑 `src/main/resources/application.yml`,将 `repository.remote-path` 修改为实际的远程服务器地址: 227 | 228 | ```yaml 229 | repository: 230 | remote-path: http://your-server.com # 替换为实际地址 231 | ``` 232 | 233 | ### 4. 运行 MCP 服务器 234 | 235 | ```bash 236 | # Windows 237 | java ^ 238 | -Dspring.ai.mcp.server.stdio=true ^ 239 | -Dspring.main.web-application-type=none ^ 240 | -Dlogging.pattern.console= ^ 241 | -Dcache.base-path=C:/mcp-cache ^ 242 | -Dfile.encoding=UTF-8 ^ 243 | -Drepository.remote-path=http://your-server.com ^ 244 | -jar target/mcp-codestyle-server-1.0.2.jar 245 | 246 | # Linux/macOS 247 | java \ 248 | -Dspring.ai.mcp.server.stdio=true \ 249 | -Dspring.main.web-application-type=none \ 250 | -Dlogging.pattern.console= \ 251 | -Dcache.base-path=/mcp-cache \ 252 | -Dfile.encoding=UTF-8 \ 253 | -Drepository.remote-path=http://your-server.com \ 254 | -jar target/mcp-codestyle-server-1.0.2.jar 255 | ``` 256 | 257 | ### 5. 配置 MCP 客户端 258 | 259 | #### Cherry Studio 配置示例 260 | 261 | 在设置 -> MCP Servers 中添加: 262 | 263 | ```json 264 | { 265 | "mcpServers": { 266 | "codestyleServer": { 267 | "command": "java", 268 | "args": [ 269 | "-Dspring.ai.mcp.server.stdio=true", 270 | "-Dspring.main.web-application-type=none", 271 | "-Dlogging.pattern.console=", 272 | "-Dfile.encoding=UTF-8" 273 | "-jar", 274 | "C:/path/to/mcp-codestyle-server/target/mcp-codestyle-server-0.0.1.jar" 275 | ], 276 | "env": {} 277 | } 278 | } 279 | } 280 | ``` 281 | 282 | ![打开Cherry Studio](../../java_projects/mcp-codestyle-server/img/image.png) 283 | 注意实际 jar 路径和参数配置一致 284 | ![添加Json导入MCP](../../java_projects/mcp-codestyle-server/img/image-1.png) 285 | 添加成功 286 | ![成功添加](../../java_projects/mcp-codestyle-server/img/image-2.png) 287 | 通过配置按钮可以查看到两个工具已注册 288 | ![查看已注册工具](../../java_projects/mcp-codestyle-server/img/image-3.png) 289 | 290 | #### Cursor 配置示例 291 | 292 | 在 `~/.cursor/mcp_settings.json` 中添加: 293 | 294 | ```json 295 | { 296 | "mcpServers": { 297 | "codestyleServer": { 298 | "command": "java", 299 | "args": [ 300 | "-Dspring.ai.mcp.server.stdio=true", 301 | "-Dspring.main.web-application-type=none", 302 | "-Dlogging.pattern.console=", 303 | "-Dfile.encoding=UTF-8", 304 | "-jar", 305 | "/path/to/mcp-codestyle-server/target/mcp-codestyle-server-0.0.1.jar" 306 | ] 307 | } 308 | } 309 | } 310 | ``` 311 | 312 | ![在cursor中添加MCP](../../java_projects/mcp-codestyle-server/img/image-4.png) 313 | 启用服务器后,在聊天界面即可调用工具。 314 | 315 | ## MCP 工具 316 | 317 | ### 1. codestyleSearch - 搜索模板目录树 318 | 319 | **参数:** 320 | 321 | - `templateKeyword` (String): 模板关键词 322 | - 示例:`CRUD`、`backend`、`frontend` 323 | 324 | **响应示例:** 325 | 326 | ``` 327 | 找到模板组: CRUD 328 | 329 | 目录树: 330 | backend/ 331 | CRUD/ 332 | 1.0.0/ 333 | src/ 334 | main/ 335 | java/ 336 | com/ 337 | air/ 338 | controller/ 339 | └── Controller.ftl 340 | service/ 341 | └── Service.ftl 342 | 343 | 模板组介绍: 344 | 完整的CRUD操作模板,包含控制器、服务层、数据访问层等 345 | ``` 346 | 347 | **执行流程:** 348 | 349 | ``` 350 | # 本地Lucene检索模式(默认) 351 | 1. luceneIndexService.fetchLocalMetaConfig(keyword) → 本地全文检索 352 | 2. searchLocalRepository(groupId, artifactId) → 从本地 meta.json 读取文件列表 353 | 3. PromptUtils.buildTree(metaInfos) → 构建目录树结构 354 | 4. promptService.buildSearchResult() → 格式化输出 355 | 356 | # 远程检索模式(remote-search-enabled=true) 357 | 1. fetchRemoteMetaConfig(keyword) → 调用远程 API 获取模板配置 358 | 2. smartDownloadTemplate(config) → SHA256 比对,按需下载 359 | 3. searchLocalRepository(groupId, artifactId) → 从本地 meta.json 读取文件列表 360 | 4. PromptUtils.buildTree(metaInfos) → 构建目录树结构 361 | 5. promptService.buildSearchResult() → 格式化输出 362 | ``` 363 | 364 | ### 2. getTemplateByPath - 获取模板详细内容 365 | 366 | **参数:** 367 | 368 | - `templatePath` (String): 完整模板路径 369 | - 格式:`groupId/artifactId/version/filePath/filename` 370 | - 示例:`backend/CRUD/1.0.0/src/main/java/com/air/controller/Controller.ftl` 371 | 372 | **响应示例:** 373 | 374 | ``` 375 | #文件名:backend/CRUD/1.0.0/src/main/java/com/air/controller/Controller.ftl 376 | #文件变量: 377 | - className: 类名(示例:UserController)[String] 378 | - packageName: 包名(示例:com.air.controller)[String] 379 | #文件内容: 380 | package ${packageName}; 381 | 382 | public class ${className} { 383 | // CRUD方法 384 | } 385 | ``` 386 | 387 | **执行流程:** 388 | 389 | ``` 390 | 1. searchByPath(path) → 从路径解析 groupId/artifactId,直接定位 meta.json 391 | 2. 如未找到 → fetchRemoteMetaConfig() + smartDownloadTemplate() → 自动修复 392 | 3. readTemplateContent() → 读取模板文件内容 393 | 4. promptService.buildPrompt() → 格式化输出(变量 + 内容) 394 | ``` 395 | 396 | ## 模板仓库结构 397 | 398 | ### 本地缓存目录结构 399 | 400 | ``` 401 | codestyle-cache/ 402 | ├── lucene-index/ # Lucene 全文索引目录 403 | │ ├── _1.cfe 404 | │ ├── _1.cfs 405 | │ ├── _1.si 406 | │ └── segments_2 407 | └── continew/ # groupId 408 | └── CRUD/ # artifactId 409 | ├── meta.json # 元数据配置 410 | ├── preview.png # 预览图(可选) 411 | └── 1.0.0/ # version 412 | ├── README.md # 模板说明文档 413 | ├── backend/ # 后端模板 414 | │ ├── sql/ 415 | │ └── src/ 416 | └── frontend/ # 前端模板 417 | └── src/ 418 | ``` 419 | 420 | ### meta.json 格式(本地) 421 | 422 | ```json 423 | { 424 | "groupId": "backend", 425 | "artifactId": "CRUD", 426 | "configs": [ 427 | { 428 | "version": "1.0.0", 429 | "files": [ 430 | { 431 | "filePath": "/src/main/java/com/air/controller", 432 | "filename": "Controller.ftl", 433 | "description": "控制器模板", 434 | "sha256": "abc123...", 435 | "inputVariables": [ 436 | { 437 | "variableName": "className", 438 | "variableType": "String", 439 | "variableComment": "类名", 440 | "example": "UserController" 441 | } 442 | ] 443 | } 444 | ] 445 | } 446 | ] 447 | } 448 | ``` 449 | 450 | **特点:** 451 | 452 | - 支持多版本共存(`configs` 数组) 453 | - SHA256 校验保证文件完整性 454 | - 精确的文件路径和变量描述 455 | 456 | ## 提示词模板 457 | 458 | ### content-result.txt(模板内容) 459 | 460 | ``` 461 | #文件名:%{s} 462 | #文件变量: 463 | %{s} 464 | #文件内容: 465 | %{s} 466 | ``` 467 | 468 | - 3 个占位符:文件名、变量列表、模板内容 469 | 470 | ### search-result.txt(搜索结果) 471 | 472 | ``` 473 | 找到模板组: %{s} 474 | 475 | 目录树: 476 | %{s} 477 | 模板组介绍: 478 | %{s} 479 | ``` 480 | 481 | - 3 个占位符:groupId/artifactId、目录树、描述 482 | 483 | 可编辑这些文件以适配不同 MCP 客户端的响应风格。 484 | 485 | ## 核心逻辑详解 486 | 487 | ### Lucene 本地索引机制 488 | 489 | v1.0.2 版本引入了 Apache Lucene 全文检索引擎,实现离线模板检索能力: 490 | 491 | ``` 492 | ┌─────────────────────────────────────────────────────────────┐ 493 | │ LuceneIndexService │ 494 | ├─────────────────────────────────────────────────────────────┤ 495 | │ 初始化流程 (@PostConstruct) │ 496 | │ 1. 创建 lucene-index 目录 │ 497 | │ 2. 初始化 SmartChineseAnalyzer (中文分词器) │ 498 | │ 3. 扫描本地仓库所有 meta.json │ 499 | │ 4. 为每个模板建立索引文档 │ 500 | └─────────────────────────────────────────────────────────────┘ 501 | │ 502 | ▼ 503 | ┌─────────────────────────────────────────────────────────────┐ 504 | │ 索引文档结构 │ 505 | │ ├── groupId (StringField) → 精确匹配 │ 506 | │ ├── artifactId (StringField) → 精确匹配 │ 507 | │ ├── metaPath (StringField) → meta.json 路径 │ 508 | │ ├── description (TextField) → 全文检索 (README.md内容) │ 509 | │ └── content (TextField) → 组合检索字段 │ 510 | └─────────────────────────────────────────────────────────────┘ 511 | ``` 512 | 513 | **索引特性:** 514 | 515 | | 特性 | 说明 | 516 | | -------- | --------------------------------------------- | 517 | | 中文分词 | 使用 SmartChineseAnalyzer,支持中文关键词检索 | 518 | | 自动重建 | 启动时自动扫描并重建索引 | 519 | | 增量更新 | 下载新模板后自动更新对应索引 | 520 | | 离线检索 | 无需网络连接,本地即可完成模板搜索 | 521 | 522 | ### 模板更新机制 523 | 524 | ``` 525 | 用户搜索模板 (codestyleSearch) 526 | │ 527 | ▼ 528 | ┌───────────────────────┐ 529 | │ 1. 获取远程配置 │ fetchRemoteMetaConfig(keyword) 530 | │ (远程API返回) │ → 返回 groupId, artifactId, version, 文件列表+SHA256 531 | └───────────────────────┘ 532 | │ 533 | ▼ 534 | ┌───────────────────────┐ 535 | │ 2. 智能下载判断 │ smartDownloadTemplate(remoteConfig) 536 | └───────────────────────┘ 537 | │ 538 | ▼ 539 | 本地 meta.json 存在? 540 | │ 541 | ┌────┴────┐ 542 | │ 否 │ 是 543 | ▼ ▼ 544 | 需要下载 ┌─────────────────────┐ 545 | │ 3. 检查是否需要更新 │ checkIfNeedsUpdate() 546 | └─────────────────────┘ 547 | │ 548 | ┌───────────┼───────────┐ 549 | ▼ ▼ ▼ 550 | 版本不存在? 文件不存在? SHA256变化? 551 | │ │ │ 552 | └─────┬─────┴───────────┘ 553 | ▼ 554 | 需要更新? 555 | ┌────┴────┐ 556 | │ 否 │ 是 557 | ▼ ▼ 558 | 跳过 ┌─────────────────────┐ 559 | │ 4. 下载并解压 │ downloadAndExtractTemplate() 560 | │ - 备份本地meta │ 561 | │ - 请求远程ZIP │ 562 | │ - 解压到本地仓库 │ 563 | │ - 合并更新meta │ 564 | └─────────────────────┘ 565 | ``` 566 | 567 | ### 更新触发条件 568 | 569 | | 条件 | 触发更新 | 570 | | --------------------- | ----------- | 571 | | 本地 meta.json 不存在 | ✅ | 572 | | 远程版本在本地不存在 | ✅ | 573 | | 远程文件在本地不存在 | ✅ | 574 | | 文件 SHA256 不一致 | ✅ | 575 | | 所有文件 SHA256 一致 | ❌ 跳过下载 | 576 | 577 | ### 版本合并策略 578 | 579 | 下载新版本时会**保留本地已有版本**: 580 | 581 | ``` 582 | 场景:本地已有 v0.9.0,远程推送了 v1.0.0 583 | 584 | 1. 备份本地 meta.json(包含 v0.9.0 配置) 585 | 2. 下载并解压远程 ZIP 586 | 3. 删除远程带来的 meta.json(只含单版本) 587 | 4. 从备份恢复,追加 v1.0.0 配置 588 | 5. 最终 meta.json 包含:v0.9.0 + v1.0.0 589 | ``` 590 | 591 | ## 开发与测试 592 | 593 | ### 运行集成测试 594 | 595 | ```bash 596 | mvn test 597 | ``` 598 | 599 | 或运行 `CodestyleServiceTest.main()` 方法: 600 | 601 | ```java 602 | // 通过 STDIO 启动 JAR 并调用 MCP 工具 603 | CodestyleServiceTest.main(new String[]{}); 604 | ``` 605 | 606 | ### 扩展新模板 607 | 608 | 1. 在远程仓库添加新的模板 ZIP 和对应的 JSON 配置 609 | 2. 确保 `meta.json` 中的 `sha256` 与实际文件哈希一致 610 | 3. 运行测试验证端到端流程 611 | 612 | ### 调试技巧 613 | 614 | - 查看日志:移除 `-Dlogging.pattern.console=` 参数 615 | - 检查缓存:查看 `repository.dir` 配置的目录 616 | - 验证远程接口:使用 `curl` 或 Postman 测试远程 API 617 | 618 | ## 常见问题 619 | 620 | ### Q: 如何清理本地缓存? 621 | 622 | A: 删除 `repository.dir` 配置的目录,下次运行时会自动重新下载。 623 | 624 | ### Q: 如何支持多用户? 625 | 626 | A: 为每个用户配置不同的 `cache.base-path`,或在 `groupId` 前添加用户标识。 627 | 628 | ### Q: 远程仓库不可用时如何处理? 629 | 630 | A: 系统会继续使用本地缓存,并返回友好的错误提示。 631 | 632 | ### Q: 为什么下载后本地 meta.json 和远程不一样? 633 | 634 | A: 本地 meta.json 使用 `configs` 数组支持**多版本共存**,而远程返回的是单版本的 `config` 对象。下载时会自动转换并合并。 635 | 636 | ### Q: SHA256 校验失败怎么办? 637 | 638 | A: 删除对应模板目录(如 `codestyle-cache/continew/CRUD/`),系统会重新下载完整模板。 639 | 640 | ### Q: 如何切换检索模式? 641 | 642 | A: 在 `application.yml` 中设置 `repository.remote-search-enabled`: 643 | 644 | - `false`(默认):本地 Lucene 检索,启动快、无需网络 645 | - `true`:远程 API 检索,始终获取最新模板信息 646 | 647 | ### Q: Lucene 索引损坏怎么办? 648 | 649 | A: 删除 `codestyle-cache/lucene-index/` 目录,重启服务后会自动重建索引。 650 | 651 | ## 许可证 652 | 653 | 基于 [MIT License](LICENSE) 发布。 654 | 655 | ## 作者 656 | 657 | - artboy (itxaiohanglover) 658 | - Kanttha 659 | - movclantian 660 | 661 | ## 更新日志 662 | 663 | - **v1.0** (2025-12-03) 664 | - 集成 Apache Lucene 全文检索引擎 665 | - 支持 SmartChineseAnalyzer 中文分词 666 | - 实现离线模板检索,无需网络连接 667 | - 启动时自动扫描本地仓库并建立索引 668 | - 默认使用本地 Lucene 检索(false) 669 | - 可切换为远程 API 检索模式(true) 670 | - 拉取远程模板后自动更新 Lucene 索引 671 | - 保证本地索引与模板文件同步 672 | - 初始版本 673 | - 支持 `codestyleSearch` 和 `getTemplateByPath` 工具 674 | - Maven 风格目录结构 675 | - SHA256 增量更新机制 676 | - 自动修复机制(本地缺失时触发下载) 677 | - 多版本共存支持 678 | - 精确路径定位优化 679 | --------------------------------------------------------------------------------