From b465a1fd781066bddf96913d73fd2e2c1e51107d Mon Sep 17 00:00:00 2001 From: Robin Shen Date: Sun, 9 Nov 2025 11:50:01 +0800 Subject: [PATCH] feat: Natural language query for issues/builds/pull requests via AI (OD-2600) --- .gitignore | 1 + pom.xml | 6 + server-core/pom.xml | 4 + .../java/io/onedev/server/CoreModule.java | 18 +- .../resource => ai}/McpHelperResource.java | 249 +-------------- .../onedev/server/ai/QueryDescriptions.java | 300 ++++++++++++++++++ .../server/data/DefaultDataService.java | 29 +- .../server/data/migration/DataMigrator.java | 4 + .../java/io/onedev/server/model/Setting.java | 2 +- .../server/model/support/AIModelSetting.java | 135 ++++++++ .../support/administration/AISetting.java | 41 +++ .../onedev/server/service/SettingService.java | 7 +- .../service/impl/DefaultSettingService.java | 14 +- .../java/io/onedev/server/util/ZoneIdChanged | 0 .../web/behavior/BuildQueryBehavior.java | 49 ++- .../web/behavior/IssueQueryBehavior.java | 56 +++- .../behavior/PullRequestQueryBehavior.java | 71 ++++- .../inputassist/InputAssistBehavior.java | 159 +++++++--- .../NaturalLanguageTranslator.java | 41 +++ .../web/behavior/inputassist/input-assist.js | 83 ++++- .../server/web/mapper/BaseUrlMapper.java | 2 + .../page/admin/aisetting/AISettingPage.html | 10 + .../page/admin/aisetting/AISettingPage.java | 58 ++++ .../io/onedev/server/web/page/base/base.js | 2 +- .../server/web/page/layout/LayoutPage.java | 8 +- .../onedev/server/web/page/test/TestPage.java | 1 + .../web/translation/Translation_de.java | 4 + .../web/translation/Translation_es.java | 4 + .../web/translation/Translation_fr.java | 4 + .../web/translation/Translation_it.java | 4 + .../web/translation/Translation_ja.java | 4 + .../web/translation/Translation_ko.java | 4 + .../web/translation/Translation_pt.java | 4 + .../web/translation/Translation_zh.java | 4 + 34 files changed, 1014 insertions(+), 368 deletions(-) rename server-core/src/main/java/io/onedev/server/{rest/resource => ai}/McpHelperResource.java (81%) create mode 100644 server-core/src/main/java/io/onedev/server/ai/QueryDescriptions.java create mode 100644 server-core/src/main/java/io/onedev/server/model/support/AIModelSetting.java create mode 100644 server-core/src/main/java/io/onedev/server/model/support/administration/AISetting.java delete mode 100644 server-core/src/main/java/io/onedev/server/util/ZoneIdChanged create mode 100644 server-core/src/main/java/io/onedev/server/web/behavior/inputassist/NaturalLanguageTranslator.java create mode 100644 server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/AISettingPage.html create mode 100644 server-core/src/main/java/io/onedev/server/web/page/admin/aisetting/AISettingPage.java diff --git a/.gitignore b/.gitignore index 1885cccd64..1af13a9b9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ server-product/docker/build/ server-product/docker/onedev-*/ **/target/ +**/bin/ **/.classpath **/.gitignore **/.settings diff --git a/pom.xml b/pom.xml index 6746367289..d9d0872c2f 100644 --- a/pom.xml +++ b/pom.xml @@ -627,6 +627,11 @@ fastexcel 0.15.7 + + dev.langchain4j + langchain4j-open-ai + ${langchain4j.version} + @@ -663,5 +668,6 @@ 3.1.0 2.15.0 1.28.3 + 1.8.0 diff --git a/server-core/pom.xml b/server-core/pom.xml index f4dfe35097..d8a3d81678 100644 --- a/server-core/pom.xml +++ b/server-core/pom.xml @@ -394,6 +394,10 @@ kotlin-stdlib-jdk7 ${kotlin.version} + + dev.langchain4j + langchain4j-open-ai + 1.9.23 diff --git a/server-core/src/main/java/io/onedev/server/CoreModule.java b/server-core/src/main/java/io/onedev/server/CoreModule.java index 1b7313561c..8071e254f8 100644 --- a/server-core/src/main/java/io/onedev/server/CoreModule.java +++ b/server-core/src/main/java/io/onedev/server/CoreModule.java @@ -75,6 +75,7 @@ import io.onedev.commons.utils.ExceptionUtils; import io.onedev.commons.utils.StringUtils; import io.onedev.k8shelper.KubernetesHelper; import io.onedev.k8shelper.OsInfo; +import io.onedev.server.ai.McpHelperResource; import io.onedev.server.annotation.Shallow; import io.onedev.server.attachment.AttachmentService; import io.onedev.server.attachment.DefaultAttachmentService; @@ -144,11 +145,11 @@ import io.onedev.server.persistence.HibernateInterceptor; import io.onedev.server.persistence.IdService; import io.onedev.server.persistence.PersistListener; import io.onedev.server.persistence.PrefixedNamingStrategy; -import io.onedev.server.persistence.SessionFactoryService; import io.onedev.server.persistence.SessionFactoryProvider; +import io.onedev.server.persistence.SessionFactoryService; import io.onedev.server.persistence.SessionInterceptor; -import io.onedev.server.persistence.SessionService; import io.onedev.server.persistence.SessionProvider; +import io.onedev.server.persistence.SessionService; import io.onedev.server.persistence.TransactionInterceptor; import io.onedev.server.persistence.TransactionService; import io.onedev.server.persistence.annotation.Sessional; @@ -160,7 +161,6 @@ import io.onedev.server.rest.DefaultServletContainer; import io.onedev.server.rest.JerseyConfigurator; import io.onedev.server.rest.ResourceConfigProvider; import io.onedev.server.rest.WebApplicationExceptionHandler; -import io.onedev.server.rest.resource.McpHelperResource; import io.onedev.server.rest.resource.ProjectResource; import io.onedev.server.search.code.CodeIndexService; import io.onedev.server.search.code.CodeSearchService; @@ -197,10 +197,10 @@ import io.onedev.server.service.BuildMetricService; import io.onedev.server.service.BuildParamService; import io.onedev.server.service.BuildQueryPersonalizationService; import io.onedev.server.service.BuildService; -import io.onedev.server.service.CodeCommentService; import io.onedev.server.service.CodeCommentMentionService; import io.onedev.server.service.CodeCommentQueryPersonalizationService; import io.onedev.server.service.CodeCommentReplyService; +import io.onedev.server.service.CodeCommentService; import io.onedev.server.service.CodeCommentStatusChangeService; import io.onedev.server.service.CodeCommentTouchService; import io.onedev.server.service.CommitQueryPersonalizationService; @@ -215,9 +215,9 @@ import io.onedev.server.service.GroupAuthorizationService; import io.onedev.server.service.GroupService; import io.onedev.server.service.IssueAuthorizationService; import io.onedev.server.service.IssueChangeService; -import io.onedev.server.service.IssueCommentService; import io.onedev.server.service.IssueCommentReactionService; import io.onedev.server.service.IssueCommentRevisionService; +import io.onedev.server.service.IssueCommentService; import io.onedev.server.service.IssueDescriptionRevisionService; import io.onedev.server.service.IssueFieldService; import io.onedev.server.service.IssueLinkService; @@ -248,9 +248,9 @@ import io.onedev.server.service.ProjectLastEventDateService; import io.onedev.server.service.ProjectService; import io.onedev.server.service.PullRequestAssignmentService; import io.onedev.server.service.PullRequestChangeService; -import io.onedev.server.service.PullRequestCommentService; import io.onedev.server.service.PullRequestCommentReactionService; import io.onedev.server.service.PullRequestCommentRevisionService; +import io.onedev.server.service.PullRequestCommentService; import io.onedev.server.service.PullRequestDescriptionRevisionService; import io.onedev.server.service.PullRequestLabelService; import io.onedev.server.service.PullRequestMentionService; @@ -285,10 +285,10 @@ import io.onedev.server.service.impl.DefaultBuildMetricService; import io.onedev.server.service.impl.DefaultBuildParamService; import io.onedev.server.service.impl.DefaultBuildQueryPersonalizationService; import io.onedev.server.service.impl.DefaultBuildService; -import io.onedev.server.service.impl.DefaultCodeCommentService; import io.onedev.server.service.impl.DefaultCodeCommentMentionService; import io.onedev.server.service.impl.DefaultCodeCommentQueryPersonalizationService; import io.onedev.server.service.impl.DefaultCodeCommentReplyService; +import io.onedev.server.service.impl.DefaultCodeCommentService; import io.onedev.server.service.impl.DefaultCodeCommentStatusChangeService; import io.onedev.server.service.impl.DefaultCodeCommentTouchService; import io.onedev.server.service.impl.DefaultCommitQueryPersonalizationService; @@ -303,9 +303,9 @@ import io.onedev.server.service.impl.DefaultGroupAuthorizationService; import io.onedev.server.service.impl.DefaultGroupService; import io.onedev.server.service.impl.DefaultIssueAuthorizationService; import io.onedev.server.service.impl.DefaultIssueChangeService; -import io.onedev.server.service.impl.DefaultIssueCommentService; import io.onedev.server.service.impl.DefaultIssueCommentReactionService; import io.onedev.server.service.impl.DefaultIssueCommentRevisionService; +import io.onedev.server.service.impl.DefaultIssueCommentService; import io.onedev.server.service.impl.DefaultIssueDescriptionRevisionService; import io.onedev.server.service.impl.DefaultIssueFieldService; import io.onedev.server.service.impl.DefaultIssueLinkService; @@ -336,9 +336,9 @@ import io.onedev.server.service.impl.DefaultProjectLastEventDateService; import io.onedev.server.service.impl.DefaultProjectService; import io.onedev.server.service.impl.DefaultPullRequestAssignmentService; import io.onedev.server.service.impl.DefaultPullRequestChangeService; -import io.onedev.server.service.impl.DefaultPullRequestCommentService; import io.onedev.server.service.impl.DefaultPullRequestCommentReactionService; import io.onedev.server.service.impl.DefaultPullRequestCommentRevisionService; +import io.onedev.server.service.impl.DefaultPullRequestCommentService; import io.onedev.server.service.impl.DefaultPullRequestDescriptionRevisionService; import io.onedev.server.service.impl.DefaultPullRequestLabelService; import io.onedev.server.service.impl.DefaultPullRequestMentionService; diff --git a/server-core/src/main/java/io/onedev/server/rest/resource/McpHelperResource.java b/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java similarity index 81% rename from server-core/src/main/java/io/onedev/server/rest/resource/McpHelperResource.java rename to server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java index f399e94d9e..91064ae14f 100644 --- a/server-core/src/main/java/io/onedev/server/rest/resource/McpHelperResource.java +++ b/server-core/src/main/java/io/onedev/server/ai/McpHelperResource.java @@ -1,6 +1,8 @@ -package io.onedev.server.rest.resource; +package io.onedev.server.ai; +import static io.onedev.server.ai.QueryDescriptions.getBuildQueryDescription; import static javax.ws.rs.core.Response.Status.NOT_ACCEPTABLE; +import static org.unbescape.html.HtmlEscape.escapeHtml5; import java.io.Serializable; import java.nio.charset.StandardCharsets; @@ -16,7 +18,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.jspecify.annotations.Nullable; import javax.inject.Inject; import javax.inject.Singleton; import javax.persistence.EntityNotFoundException; @@ -37,7 +38,7 @@ import javax.ws.rs.core.Response; import org.apache.shiro.authz.UnauthenticatedException; import org.apache.shiro.authz.UnauthorizedException; import org.eclipse.jgit.lib.ObjectId; -import org.unbescape.html.HtmlEscape; +import org.jspecify.annotations.Nullable; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -165,8 +166,6 @@ public class McpHelperResource { private final BuildService buildService; - private final BuildParamService buildParamService; - private final JobService jobService; private final GitService gitService; @@ -215,243 +214,11 @@ public class McpHelperResource { this.urlService = urlService; this.pullRequestCommentService = pullRequestCommentService; this.buildService = buildService; - this.buildParamService = buildParamService; this.jobService = jobService; this.validator = validator; this.serverConfig = serverConfig; } - private String getIssueQueryStringDescription() { - var stateNames = new StringBuilder(); - for (var state: settingService.getIssueSetting().getStateSpecs()) { - stateNames.append(" - "); - stateNames.append(state.getName()); - if (state.getDescription() != null) { - stateNames.append(": ").append(state.getDescription().replace("\n", " ")); - } - stateNames.append("\n"); - } - var fieldCriterias = new StringBuilder(); - for (var field: settingService.getIssueSetting().getFieldSpecs()) { - if (field instanceof ChoiceField) { - var choiceField = (ChoiceField) field; - fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \"" - + field.getName() + "\" is \"<" + field.getName().toLowerCase() - + " value>\" (quotes are required), where <" + field.getName().toLowerCase() - + " value> is one of below:\n"); - for (var choice : choiceField.getPossibleValues()) - fieldCriterias.append(" - " + choice).append("\n"); - } else if (field instanceof UserChoiceField) { - fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \"" - + field.getName() + "\" is \"\" (quotes are required)\n"); - fieldCriterias.append( - "- " + field.getName().toLowerCase() + " criteria for current user in form of: \"" - + field.getName() + "\" is me (quotes are required)\n"); - } else if (field instanceof GroupChoiceField) { - fieldCriterias.append("- " + field.getName().toLowerCase() + " criteria in form of: \"" - + field.getName() + "\" is \"\" (quotes are required)\n"); - } else if (field instanceof BooleanField) { - fieldCriterias.append("- " + field.getName().toLowerCase() + " is true criteria in form of: \"" - + field.getName() + "\" is \"true\" (quotes are required)\n"); - fieldCriterias.append("- " + field.getName().toLowerCase() + " is false criteria in form of: \"" - + field.getName() + "\" is \"false\" (quotes are required)\n"); - } else if (field instanceof DateField) { - fieldCriterias.append("- " + field.getName().toLowerCase() - + " is before certain date criteria in form of: \"" + field.getName() - + "\" is before \"\" (quotes are required), where is of format YYYY-MM-DD\n"); - fieldCriterias.append("- " + field.getName().toLowerCase() - + " is after certain date criteria in form of: \"" + field.getName() - + "\" is after \"\" (quotes are required), where is of format YYYY-MM-DD\n"); - } else if (field instanceof DateTimeField) { - fieldCriterias.append("- " + field.getName().toLowerCase() - + " is before certain date time criteria in form of: \"" + field.getName() - + "\" is before \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n"); - fieldCriterias.append("- " + field.getName().toLowerCase() - + " is after certain date time criteria in form of: \"" + field.getName() - + "\" is after \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n"); - } else if (field instanceof IntegerField) { - fieldCriterias.append("- " + field.getName().toLowerCase() - + " is equal to certain integer criteria in form of: \"" + field.getName() - + "\" is \"\" (quotes are required), where is an integer\n"); - fieldCriterias.append("- " + field.getName().toLowerCase() - + " is greater than certain integer criteria in form of: \"" + field.getName() - + "\" is greater than \"\" (quotes are required), where is an integer\n"); - fieldCriterias.append("- " + field.getName().toLowerCase() - + " is less than certain integer criteria in form of: \"" + field.getName() - + "\" is less than \"\" (quotes are required), where is an integer\n"); - } - fieldCriterias.append("- " + field.getName().toLowerCase() + " is not set criteria in form of: \"" - + field.getName() + "\" is empty (quotes are required)\n"); - } - var linkCriterias = new StringBuilder(); - for (var linkSpec: linkSpecService.query()) { - linkCriterias.append("- criteria to list issues with any " + linkSpec.getName().toLowerCase() - + " issues matching certain criteria in form of: any \"" + linkSpec.getName() - + "\" matching(another criteria) (quotes are required)\n"); - linkCriterias.append("- criteria to list issues with all " + linkSpec.getName().toLowerCase() - + " issues matching certain criteria in form of: all \"" + linkSpec.getName() - + "\" matching(another criteria) (quotes are required)\n"); - linkCriterias.append("- criteria to list issues with some " + linkSpec.getName().toLowerCase() - + " issues in form of: has any \"" + linkSpec.getName() + "\" (quotes are required)\n"); - if (linkSpec.getOpposite() != null) { - linkCriterias.append("- criteria to list issues with any " - + linkSpec.getOpposite().getName().toLowerCase() - + " issues matching certain criteria in form of: any \"" + linkSpec.getOpposite().getName() - + "\" matching(another criteria) (quotes are required)\n"); - linkCriterias.append("- criteria to list issues with all " - + linkSpec.getOpposite().getName().toLowerCase() - + " issues matching certain criteria in form of: all \"" + linkSpec.getOpposite().getName() - + "\" matching(another criteria) (quotes are required)\n"); - linkCriterias.append("- criteria to list issues with some " + linkSpec.getOpposite().getName().toLowerCase() - + " issues in form of: has any \"" + linkSpec.getOpposite().getName() + "\" (quotes are required)\n"); - } - } - var orderFields = new StringBuilder(); - for (var field: Issue.SORT_FIELDS.keySet()) { - orderFields.append("- ").append(field).append("\n"); - } - - var description = - "A query string is one of below criteria:\n" + - "- issue with specified number in form of: \"Number\" is \"#\", or in form of: \"Number\" is \"-\" (quotes are required)\n" + - "- criteria to check if title/description/comment contains specified text in form of: ~~\n" + - "- state criteria in form of: \"State\" is \"\" (quotes are required), where is one of below:\n" + - stateNames + - fieldCriterias + - linkCriterias + - "- submitted by specified user criteria in form of: submitted by \"\" (quotes are required)\n" + - "- submitted by current user criteria in form of: submitted by me (quotes are required)\n" + - "- submitted before certain date criteria in form of: \"Submit Date\" is until \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n" + - "- submitted after certain date criteria in form of: \"Submit Date\" is since \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n" + - "- updated before certain date criteria in form of: \"Last Activity Date\" is until \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n" + - "- updated after certain date criteria in form of: \"Last Activity Date\" is since \"\" (quotes are required), where is of format YYYY-MM-DD HH:mm\n" + - "- confidential criteria in form of: confidential\n" + - "- iteration criteria in form of: \"Iteration\" is \"\" (quotes are required)\n" + - "- and criteria in form of and \n" + - "- or criteria in form of or . Note that \"and criteria\" takes precedence over \"or criteria\", use braces to group \"or criteria\" like \"(criteria1 or criteria2) and criteria3\" if you want to override precedence\n" + - "- not criteria in form of not()\n" + - "\n" + - "And can optionally add order clause at end of query string in form of: order by \"\" ,\"\" ,... (quotes are required), where is one of below:\n" + - orderFields + - "\n" + - "Leave empty to list all accessible issues"; - - return HtmlEscape.escapeHtml5(description); - } - - private String getPullRequestQueryStringDescription() { - var orderFields = new StringBuilder(); - for (var field : PullRequest.SORT_FIELDS.keySet()) { - orderFields.append("- ").append(field).append("\n"); - } - - var labelNames = labelSpecService.query().stream().map(LabelSpec::getName).collect(Collectors.joining(", ")); - var mergeStrategyNames = Arrays.stream(MergeStrategy.values()).map(MergeStrategy::name).collect(Collectors.joining(", ")); - - var description = - "A query string is one of below criteria:\n" + - "- pull request with specified number in form of: \"Number\" is \"#\", or in form of: \"Number\" is \"-\" (quotes are required)\n" + - "- criteria to check if title/description/comment contains specified text in form of: ~~\n" + - "- open criteria in form of: open\n" + - "- merged criteria in form of: merged\n" + - "- discarded criteria in form of: discarded\n" + - "- source branch criteria in form of: \"Source Branch\" is \"\" (quotes are required)\n" + - "- target branch criteria in form of: \"Target Branch\" is \"\" (quotes are required)\n" + - "- merge strategy criteria in form of: \"Merge Strategy\" is \"\" (quotes are required), where is one of: " + mergeStrategyNames + "\n" + - "- label criteria in form of: \"Label\" is \"