JIRA 工时自动填报 Agent
xsun_workflow_agent
项目简介
这是一个基于 AI 的 JIRA 工时自动填报系统,能够根据用户的 Git 提交记录自动分析并填写 JIRA 工作日志。该系统通过集成 LangChain4j 框架,利用大语言模型的能力,智能地将 Git 提交内容与 JIRA 问题进行匹配,并自动生成符合要求的工作日志。
功能特性
- 自动获取用户当天的 Git 提交记录
- 查询用户在 JIRA 上未完成的任务
- 智能匹配 Git 提交与 JIRA 问题
- 自动计算所需填写的工时
- 自动生成工作日志并提交至 JIRA 系统
技术架构
本项目采用以下技术栈:
- Java 21
- Spring Boot 3.2.3
- LangChain4j 0.27.1
- JIRA REST Java Client 7.0.1
- Eclipse JGit 6.8.0
项目结构
src/main/java/com/xsun/jira/jira_workflor_agent/ ├── JiraWorkflorAgentApplication.java // 应用入口类 ├── config/ │ ├── AgentConfig.java // AI Agent 配置类 │ └── GitConfig.java // Git 配置类 ├── controller/ │ └── WorklogController.java // 工作日志控制器 ├── model/dto/ │ ├── GitCommit.java // Git 提交数据传输对象 │ ├── JiraIssue.java // JIRA 问题数据传输对象 │ └── WorklogEntry.java // 工作日志条目数据传输对象 ├── service/ │ ├── WorklogAgent.java // 工作日志 AI Agent 接口 │ └── WorklogService.java // 工作日志服务类 └── tools/ ├── DateTimeTools.java // 日期时间工具类 ├── GitTools.java // Git 操作工具类 └── JiraTools.java // JIRA 操作工具类核心代码展示
主应用类
类名:JiraWorkflorAgentApplication.java
packagecom.xsun.jira.jira_workflor_agent;importcom.xsun.jira.jira_workflor_agent.service.WorklogService;importlombok.RequiredArgsConstructor;importlombok.extern.slf4j.Slf4j;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.scheduling.annotation.EnableScheduling;importorg.springframework.scheduling.annotation.Scheduled;@Slf4j@SpringBootApplication@EnableScheduling@RequiredArgsConstructorpublicclassJiraWorkflorAgentApplicationimplementsCommandLineRunner{publicstaticvoidmain(String[]args){SpringApplication.run(JiraWorkflorAgentApplication.class,args);}@Overridepublicvoidrun(String...args)throwsException{log.info("工作日志Agent启动成功");}}AI Agent 配置
类名:AgentConfig.java
packagecom.xsun.jira.jira_workflor_agent.config;importcom.xsun.jira.jira_workflor_agent.service.WorklogAgent;importcom.xsun.jira.jira_workflor_agent.tools.DateTimeTools;importcom.xsun.jira.jira_workflor_agent.tools.GitTools;importcom.xsun.jira.jira_workflor_agent.tools.JiraTools;importdev.langchain4j.memory.chat.MessageWindowChatMemory;importdev.langchain4j.model.chat.ChatLanguageModel;importdev.langchain4j.model.openai.OpenAiChatModel;importdev.langchain4j.service.AiServices;importlombok.RequiredArgsConstructor;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importjava.time.Duration;/** * @program: AgentConfig.java * @description: * @author: sunmouren * @create: 2025-12-14 **/@Configuration@RequiredArgsConstructorpublicclassAgentConfig{@Value("${openai.api-key}")privateStringopenaiApiKey;@Value("${openai.model}")privateStringopenaiModel;@Value("${openai.base_url}")privateStringbaseUrl;privatefinalJiraToolsjiraTools;privatefinalGitToolsgitTools;privatefinalDateTimeToolsdateTimeTools;@BeanpublicChatLanguageModelchatLanguageModel(){returnOpenAiChatModel.builder().apiKey(openaiApiKey).modelName(openaiModel).baseUrl(baseUrl).temperature(0.5).timeout(Duration.ofSeconds(60)).build();}@BeanpublicWorklogAgentworklogAgent(ChatLanguageModelchatLanguageModel){returnAiServices.builder(WorklogAgent.class).chatLanguageModel(chatLanguageModel).chatMemory(MessageWindowChatMemory.withMaxMessages(10)).tools(jiraTools,gitTools,dateTimeTools).build();}}工作日志 AI Agent 接口
类名:WorklogAgent.java
packagecom.xsun.jira.jira_workflor_agent.service;importdev.langchain4j.service.SystemMessage;importdev.langchain4j.service.UserMessage;publicinterfaceWorklogAgent{@SystemMessage(""" 你是一个工作日志助手。你的任务是帮助用户自动记录JIRA工作时间。 工作流程: 1. 获取当前日期,判断今天是星期几 2. 根据星期判断需要工作的时间(周一/三/五:8小时,周二/四:10小时) 3. 获取用户今天已经记录的JIRA工作时间 4. 如果工作时间不足,需要: - 获取用户今天的Git提交记录 - 获取用户未完成的JIRA问题列表 - 使用这些信息匹配并补充工作时间(git提交记录最匹配的jira问题,使用余弦相似度) 5. 确认后记录工作时间 注意: - 时间以0.5小时为最小单位 - 最终的总工作时间必须等于要求的时间 - 工作说明要基于Git提交的comment总结 - 工作说明不需要明写是根据git总结的,只需要返回具体的总结信息即可 - 如果不需要记录,只需要返回已经填满,不缺时间 - 选择一个你觉得最好的方案进行填写jira,不必找我二次确认,直接填写 """)Stringchat(@UserMessageStringmessage);}Git 操作工具
类名:GitTools.java
packagecom.xsun.jira.jira_workflor_agent.tools;importcom.xsun.jira.jira_workflor_agent.config.GitConfig;importcom.xsun.jira.jira_workflor_agent.model.dto.GitCommit;importdev.langchain4j.agent.tool.Tool;importlombok.extern.slf4j.Slf4j;importorg.eclipse.jgit.api.Git;importorg.eclipse.jgit.lib.PersonIdent;importorg.eclipse.jgit.revwalk.RevCommit;importorg.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.stereotype.Component;importjava.io.File;importjava.time.LocalDate;importjava.time.LocalDateTime;importjava.time.ZoneId;importjava.util.ArrayList;importjava.util.List;/** * @program: GitTools.java * @description: * @author: sunmouren * @create: 2025-12-14 **/@Slf4j@ComponentpublicclassGitTools{@Value("${git.username}")privateStringgitUsername;@AutowiredprivateGitConfiggitConfig;@Tool("从Git仓库获取指定用户今天的提交记录")publicList<GitCommit>getTodayCommitsByUser(){log.info("正在获取用户 {} 今天的Git提交记录...",gitUsername);List<GitCommit>commits=newArrayList<>();LocalDatetoday=LocalDate.now();for(GitConfig.Repositoryrepo:gitConfig.getRepositories()){try{FiletempDir=File.createTempFile("git-","-repo");tempDir.delete();tempDir.mkdirs();log.info("克隆仓库: {}",repo.getUrl());Gitgit=Git.cloneRepository().setURI(repo.getUrl()).setDirectory(tempDir).setCredentialsProvider(newUsernamePasswordCredentialsProvider(gitUsername,repo.getToken())).setBranch(repo.getBranch()).call();Iterable<RevCommit>logs=git.log().call();for(RevCommitcommit:logs){PersonIdentauthor=commit.getAuthorIdent();LocalDateTimecommitTime=LocalDateTime.ofInstant(author.getWhen().toInstant(),ZoneId.systemDefault());// 只获取今天的提交if(commitTime.toLocalDate().equals(today)&&author.getName().equals(gitUsername)){GitCommitgitCommit=newGitCommit();gitCommit.setCommitId(commit.getName());gitCommit.setAuthor(author.getName());gitCommit.setMessage(commit.getFullMessage());gitCommit.setCommitTime(commitTime);gitCommit.setRepository(repo.getUrl());commits.add(gitCommit);}}git.close();deleteDirectory(tempDir);}catch(Exceptione){log.error("获取仓库 {} 的提交记录失败",repo.getUrl(),e);}}log.info("共找到 {} 条今天的提交记录",commits.size());returncommits;}privatevoiddeleteDirectory(Filedirectory){File[]files=directory.listFiles();if(files!=null){for(Filefile:files){if(file.isDirectory()){deleteDirectory(file);}else{file.delete();}}}directory.delete();}}JIRA 操作工具
类名:JiraTools.java
packagecom.xsun.jira.jira_workflor_agent.tools;importai.djl.util.JsonUtils;importcom.atlassian.jira.rest.client.api.JiraRestClient;importcom.atlassian.jira.rest.client.api.JiraRestClientFactory;importcom.atlassian.jira.rest.client.api.domain.SearchResult;importcom.atlassian.jira.rest.client.api.domain.input.WorklogInput;importcom.atlassian.jira.rest.client.api.domain.input.WorklogInputBuilder;importcom.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory;importcom.google.gson.JsonNull;importcom.xsun.jira.jira_workflor_agent.model.dto.JiraIssue;importcom.xsun.jira.jira_workflor_agent.model.dto.WorklogEntry;importdev.langchain4j.agent.tool.Tool;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.stereotype.Component;importjava.net.URI;importjava.time.LocalDate;importjava.time.LocalDateTime;importjava.time.ZoneId;importjava.util.ArrayList;importjava.util.List;importjava.util.stream.StreamSupport;/** * @program: JiraTools.java * @description: * @author: sunmouren * @create: 2025-12-14 **/@Slf4j@ComponentpublicclassJiraTools{@Value("${jira.url}")privateStringjiraUrl;@Value("${jira.username}")privateStringusername;@Value("${jira.api-token}")privateStringapiToken;privateJiraRestClientjiraClient;privateJiraRestClientgetClient(){if(jiraClient==null){JiraRestClientFactoryfactory=newAsynchronousJiraRestClientFactory();URIjiraServerUri=URI.create(jiraUrl);jiraClient=factory.createWithBasicHttpAuthentication(jiraServerUri,username,apiToken);}returnjiraClient;}@Tool("从JIRA获取我未完成的问题列表")publicList<JiraIssue>getMyUnfinishedIssues(){log.info("正在获取未完成的JIRA问题...");List<JiraIssue>issues=newArrayList<>();try{Stringjql=String.format("assignee = currentUser() AND resolution = Unresolved ");varsearchResult=getClient().getSearchClient().searchJql(jql).claim();searchResult.getIssues().forEach(issue->{JiraIssuejiraIssue=newJiraIssue();jiraIssue.setKey(issue.getKey());jiraIssue.setSummary(issue.getSummary());jiraIssue.setDescription(issue.getDescription()!=null?issue.getDescription():"");jiraIssue.setStatus(issue.getStatus().getName());jiraIssue.setCreated(LocalDateTime.ofInstant(issue.getCreationDate().toDate().toInstant(),ZoneId.systemDefault()));jiraIssue.setUpdated(LocalDateTime.ofInstant(issue.getUpdateDate().toDate().toInstant(),ZoneId.systemDefault()));issues.add(jiraIssue);});log.info("找到 {} 个未完成的问题",issues.size());}catch(Exceptione){log.error("获取JIRA问题失败",e);}returnissues;}@Tool("从JIRA获取我今天的工作记录")publicList<WorklogEntry>getMyTodayWorklogs(){log.info("正在获取今天的工作记录...");List<WorklogEntry>worklogs=newArrayList<>();try{LocalDatetoday=LocalDate.now();Stringjql=String.format("worklogAuthor = currentUser() AND worklogDate = '%s'",today.toString());SearchResultsearchResult=getClient().getSearchClient().searchJql(jql).claim();log.info("找到 {} 个问题",searchResult.getTotal());searchResult.getIssues().forEach(issue->{varissueWorklogs=getClient().getIssueClient().getIssue(issue.getKey()).claim().getWorklogs();StreamSupport.stream(issueWorklogs.spliterator(),false).filter(wl->{LocalDateworklogDate=LocalDateTime.ofInstant(wl.getUpdateDate().toDate().toInstant(),ZoneId.systemDefault()).toLocalDate();returnworklogDate.equals(today);}).forEach(wl->{WorklogEntryentry=newWorklogEntry();entry.setIssueKey(issue.getKey());entry.setTimeSpentHours(wl.getMinutesSpent()/60.0);entry.setComment(wl.getComment());entry.setStarted(LocalDateTime.ofInstant(wl.getStartDate().toDate().toInstant(),ZoneId.systemDefault()));worklogs.add(entry);});});log.info("找到 {} 条今天的工作记录",worklogs.size());}catch(Exceptione){log.error("获取工作记录失败",e);}returnworklogs;}@Tool("记录JIRA工作时间,参数:issueKey-问题编号, timeSpentHours-工作时长(小时), comment-工作说明")publicbooleanlogWork(StringissueKey,doubletimeSpentHours,Stringcomment){log.info("正在记录工作时间: {} - {}小时 - {}",issueKey,timeSpentHours,comment);try{intminutes=(int)(timeSpentHours*60);WorklogInputBuilderworklogInputBuilder=newWorklogInputBuilder(URI.create(jiraUrl+"/rest/api/2/issue/"+issueKey+"/worklog"));WorklogInputbuild=worklogInputBuilder.setMinutesSpent(minutes).setComment(comment).build();getClient().getIssueClient().addWorklog(URI.create(jiraUrl+"/rest/api/2/issue/"+issueKey+"/worklog"),build).claim();log.info("工作时间记录成功");returntrue;}catch(Exceptione){log.error("记录工作时间失败",e);returnfalse;}}}数据传输对象
类名:GitCommit.java
packagecom.xsun.jira.jira_workflor_agent.model.dto;importlombok.Getter;importlombok.Setter;importlombok.experimental.Accessors;importjava.time.LocalDateTime;/** * @program: GitCommit.java * @description: * @author: sunmouren * @create: 2025-12-14 **/@Getter@Setter@Accessors(chain=true)publicclassGitCommit{privateStringcommitId;privateStringauthor;privateStringmessage;privateLocalDateTimecommitTime;privateStringrepository;privateintfilesChanged;privateintadditions;privateintdeletions;}类名:JiraIssue.java
packagecom.xsun.jira.jira_workflor_agent.model.dto;importlombok.Getter;importlombok.Setter;importlombok.experimental.Accessors;importjava.time.LocalDateTime;/** * @program: JiraIssue.java * @description: * @author: sunmouren * @create: 2025-12-14 **/@Getter@Setter@Accessors(chain=true)publicclassJiraIssue{privateStringkey;privateStringsummary;privateStringdescription;privateStringstatus;privateLocalDateTimecreated;privateLocalDateTimeupdated;}类名:WorklogEntry.java
packagecom.xsun.jira.jira_workflor_agent.model.dto;importlombok.Getter;importlombok.Setter;importlombok.experimental.Accessors;importjava.time.LocalDateTime;/** * @program: WorklogEntry.java * @description: * @author: sunmouren * @create: 2025-12-14 **/@Getter@Setter@Accessors(chain=true)publicclassWorklogEntry{privateStringissueKey;privatedoubletimeSpentHours;privateStringcomment;privateLocalDateTimestarted;/** * 相似度分数 */privatedoublesimilarityScore;}配置文件
配置文件路径:application.yml
jira:url:http://jira***# 用户名username:xxx# 密码api-token:xxxgit:repositories:# 仓库地址-url:https://gitlab.xsun.com/***# 密码token:xxx# 统计分支branch:xxx# 账号username:xxx# 相关ai的key等openai:api-key:sk-YOUR-API-KEYmodel:qwen-plusbase_url:https://dashscope.aliyuncs.com/compatible-mode/v1# 每天需要填写时长work-hours:monday:8.0tuesday:10.0wednesday:8.0thursday:10.0friday:8.0minimum-unit:0.5# 最小时间单位(小时)使用方法
- 配置 JIRA 和 Git 相关信息到 application.yml 文件中
- 启动应用程序
- 访问
/api/worklog/chat?message=请帮我填写今日工时接口触发工时填报流程
工作原理
- 当用户请求填写工时时,系统首先判断今天是星期几,确定应填写的工时数(周一/三/五为8小时,周二/四为10小时)
- 检查用户今天已填写的工时总数
- 如果还需要补填工时,则:
- 获取用户今天的所有 Git 提交记录
- 获取用户所有的未完成 JIRA 任务
- 利用 AI 模型对 Git 提交内容和 JIRA 任务描述进行语义匹配
- 自动分配剩余工时到最相关的 JIRA 任务上
- 自动生成合适的工作说明并提交到 JIRA
依赖管理
类名:pom.xml(部分)
<!-- LangChain4j Core --><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j</artifactId><version>${langchain4j.version}</version></dependency><!-- LangChain4j OpenAI --><dependency><groupId>dev.langchain4j</groupId><artifactId>langchain4j-open-ai</artifactId><version>${langchain4j.version}</version></dependency><!-- JIRA REST Client --><dependency><groupId>com.atlassian.jira</groupId><artifactId>jira-rest-java-client-core</artifactId><version>7.0.1</version></dependency><!-- JGit for Git operations --><dependency><groupId>org.eclipse.jgit</groupId><artifactId>org.eclipse.jgit</artifactId><version>6.8.0.202311291450-r</version></dependency>