一、项目架构图
这个组件是基于 GitHub Actions + OpenAI(ChatGLM) + Git/GitHub + 公众号模板消息实现,然后串联出从代码提交获取通知,Git 检出分支变化,在使用 OpenAI 进行代码和写入日志,再发送消息通知完成整个链路。
1、项目概述
- 技术栈:GitHub Actions、OpenAI(ChatGLM)、Git/GitHub、公众号模板消息。
- 目标:实现从代码提交到通知、Git检出分支变化、使用OpenAI进行代码评审并写入日志,最后通过微信公众号发送消息通知的完整链路。
2、项目流程
- 代码提交:开发者在GitHub上提交代码变更。
- Git检出:GitHub Actions触发,检出最新的代码变更。
- 代码评审:利用OpenAI(ChatGLM)进行代码评审,识别潜在的代码问题或改进建议。
- 写入日志:将评审结果写入到指定的日志库中,以便后续跟踪和查询。
- 消息通知:通过微信公众号发送模板消息,通知相关人员代码评审的结果和变更详情。
3、项目价值
AI代码评审组件项目具有以下价值:
- 提高代码质量:通过引入AI进行代码评审,能够发现人工评审可能遗漏的问题,从而提高代码的整体质量。
- 提升评审效率:自动化评审流程可以大大缩短评审时间,使开发者能够更快地获得反馈并进行修改。
- 增强协作能力:通过微信公众号发送评审结果通知,可以方便地实现团队成员之间的信息共享和协作。
- 降低人力成本:减少了对人工评审的依赖,降低了人力成本,特别是在大型项目或团队中效果更为显著。
- 促进技术创新:该项目结合了多种先进技术(如GitHub Actions、OpenAI等),为程序员提供了学习和实践新技术的机会,有助于推动技术创新。
4、注意事项
- 为了使用该项目,开发者需要配置GitHub Actions、申请OpenAI的API密钥、创建并配置微信公众号。
二、配置说明
1、Github Actions
官网:了解 GitHub Actions - GitHub 文档
GitHub Actions 是一种持续集成和持续交付 (CI/CD) 平台,可用于自动执行生成、测试和部署管道。 您可以创建工作流程来构建和测试存储库的每个拉取请求,或将合并的拉取请求部署到生产环境。
GitHub Actions 不仅仅是 DevOps,还允许您在存储库中发生其他事件时运行工作流程。 例如,您可以运行工作流程,以便在有人在您的存储库中创建新问题时自动添加相应的标签。
GitHub 提供 Linux、Windows 和 macOS 虚拟机来运行工作流程,或者您可以在自己的数据中心或云基础架构中托管自己的自托管运行器。
主要特点和优势
- 集成在 GitHub 中:GitHub Actions 紧密集成在 GitHub 平台中,使得代码仓库、问题和拉取请求等资源的管理更加统一和便捷。
- 灵活的自定义能力:开发者可以定义自己的工作流,并使用 Docker 容器作为执行环境,这提供了极大的灵活性来运行几乎任何工具或服务。
- 丰富的预定义动作(Actions):GitHub Actions 市场提供了大量的预定义动作,涵盖了从代码检查、测试到部署的各个方面,大大简化了配置过程。
- 可视化工作流:GitHub Actions 提供了一个直观的可视化界面,用于查看工作流的执行状态、日志和结果,便于调试和监控。
- 强大的事件触发机制:GitHub Actions 支持多种事件触发工作流,包括但不限于代码推送、拉取请求、评论、标签创建等,满足不同场景下的自动化需求。
基本概念
- 工作流(Workflows):定义了一组自动化任务的集合,这些任务在特定事件发生时执行。
- 事件(Events):触发工作流执行的触发器,如代码推送、拉取请求等。
- 作业(Jobs):工作流中的一个执行单元,可以包含多个步骤(steps)。
- 步骤(Steps):作业中的一个执行指令,可以是一个 shell 命令、一个脚本或一个预定义的动作(Action)。
- 动作(Actions):可重用的代码包,用于在 GitHub Actions 中执行特定任务。
使用场景
- 自动化测试:在代码被推送到仓库时自动运行单元测试、集成测试等,确保代码质量。
- 代码检查:使用静态代码分析工具检查代码中的潜在问题,如代码风格不一致、潜在的bug等。
- 自动化部署:将代码自动部署到生产环境或测试环境,提高部署效率。
- 自动化通知:在构建失败、代码合并到主分支等事件发生时,通过邮件、Slack等方式通知相关人员。
入门指南
- 创建工作流文件:在仓库的 .github/workflows 目录下创建一个 YAML 文件来定义工作流。
- 配置工作流:在 YAML 文件中配置工作流的事件、作业、步骤和动作等。
- 提交和推送:将工作流文件提交到仓库,并推送到 GitHub。
- 查看执行结果:在 GitHub 仓库的 Actions 选项卡中查看工作流的执行状态和结果。
GitHub Actions术语
workflow(工作流程):一个 .yml 文件对应一个 workflow,也就是一次持续集成。一个 GitHub 仓库可以包含多个 workflow,只要是在 .github/workflow 目录下的 .yml 文件都会被 GitHub 执行。
job(任务):一个 workflow 由一个或多个 job 构成,每个 job 代表一个持续集成任务。
step(步骤):每个 job 由多个 step 构成,一步步完成。
action(动作):每个 step 可以依次执行一个或多个命令(action)。
on:一个 workflow 的触发条件,决定了当前的 workflow 在什么时候被执行。
maven.yml部分代码
name: Build and Run OpenAiCodeReview By Main Maven Jar
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository //检出代码仓库
uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
distribution: 'adopt'
java-version: '11'
- name: Build with Maven
run: mvn clean install //安装maven
GitHub Actions 配置
依次配置Key和值,值都是自己注册的。
2、智谱
官网:智谱AI开放平台
3、微信公众平台
官网:微信公众平台 (qq.com)
进去扫码注册appID,appsecret以及测试公众号,模板消息接口模板ID。
三、代码分析
1、Git层
public String diff() throws IOException, InterruptedException {
// git log -1 --pretty=format:%H 命令的作用是仅显示最近一次提交的完整哈希值
//ProcessBuilder 类用于创建操作系统进程
ProcessBuilder logProcessBuilder = new ProcessBuilder("git", "log", "-1", "--pretty=format:%H");
logProcessBuilder.directory(new File("."));
Process logProcess = logProcessBuilder.start();
BufferedReader logReader = new BufferedReader(new InputStreamReader(logProcess.getInputStream()));
String latestCommitHash = logReader.readLine();
logReader.close();
logProcess.waitFor();
ProcessBuilder diffProcessBuilder = new ProcessBuilder("git", "diff", latestCommitHash + "^", latestCommitHash);
diffProcessBuilder.directory(new File("."));
Process diffProcess = diffProcessBuilder.start();
StringBuilder diffCode = new StringBuilder();
BufferedReader diffReader = new BufferedReader(new InputStreamReader(diffProcess.getInputStream()));
String line;
while ((line = diffReader.readLine()) != null) {
diffCode.append(line).append("\n");
}
diffReader.close();
int exitCode = diffProcess.waitFor();
if (exitCode != 0) {
throw new RuntimeException("Failed to get diff, exit code:" + exitCode);
}
return diffCode.toString();
}
这段代码主要是用于从Git仓库中获取最近一次提交的哈希值,并基于该哈希值计算于前一次提交的差异。
public String commitAndPush(String recommend) throws Exception {//recommend是评审的日志信息
//从远程仓库克隆一个新的副本到本地目录
Git git = Git.cloneRepository()
.setURI(githubReviewLogUri + ".git")
.setDirectory(new File("repo"))
.setCredentialsProvider(new UsernamePasswordCredentialsProvider(githubToken, ""))
.call();
// 创建分支
String dateFolderName = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
File dateFolder = new File("repo/" + dateFolderName);
if (!dateFolder.exists()) {
dateFolder.mkdirs();
}
// 创建文件并写入内容
String fileName = project + "-" + branch + "-" + author + System.currentTimeMillis() + "-" + RandomStringUtils.randomNumeric(4) + ".md";
File newFile = new File(dateFolder, fileName);
try (FileWriter writer = new FileWriter(newFile)) {
writer.write(recommend);
}
// 提交内容
git.add().addFilepattern(dateFolderName + "/" + fileName).call();
git.commit().setMessage("add code review new file" + fileName).call();
git.push().setCredentialsProvider(new UsernamePasswordCredentialsProvider(githubToken, "")).call();
logger.info("openai-code-review git commit and push done! {}", fileName);
return githubReviewLogUri + "/blob/master/" + dateFolderName + "/" + fileName;
}
这段代码是提交操作的。默认推送到master分支下。
2、Openai层
public ChatCompletionSyncResponseDTO completions(ChatCompletionRequestDTO requestDTO) throws Exception {
// 获取令牌,通过调用getToken方法,传入apiKeySecret,来获取一个访问令牌(token),这个令牌将被用于后续HTTP请求的授权。
String token = BearerTokenUtils.getToken(apiKeySecret);
// apiHost : API服务的URL
URL url = new URL(apiHost);
// 通过url.openConnection()方法获取到这个URL的HttpURLConnection对象,并强制转换为HttpURLConnection,以便使用HTTP相关的设置和方法。
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 设置请求方法为POST。
connection.setRequestMethod("POST");
// 通过Authorization头设置令牌,格式为"Bearer " + token,这是OAuth 2.0中使用Bearer令牌的常见方式
connection.setRequestProperty("Authorization", "Bearer " + token);
// 设置Content-Type为application/json,表示发送的内容是JSON格式。
connection.setRequestProperty("Content-Type", "application/json");
// 设置User-Agent头,它用于告诉服务器请求的客户端类型或版本。
connection.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
connection.setDoOutput(true);
try (OutputStream os = connection.getOutputStream()) {
byte[] input = JSON.toJSONString(requestDTO).getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String inputLine;
StringBuilder content = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
content.append(inputLine);
}
// 读取完响应后,关闭BufferedReader和HTTP连接,以释放系统资源。
in.close();
connection.disconnect();
// 使用JSON.parseObject(content.toString(), ChatCompletionSyncResponseDTO.class)将响应内容(现在是一个JSON字符串)解析为ChatCompletionSyncResponseDTO对象。
// 这个对象现在包含了API响应的数据。
return JSON.parseObject(content.toString(), ChatCompletionSyncResponseDTO.class);
}
这段代码是一个完整的HTTP POST请求发送和响应处理过程,用于与远程API服务进行交互,发送聊天补全请求并接收响应。
3、WeiXin层
public void sendTemplateMessage(String logUrl, Map<String, Map<String, String>> data) throws Exception {
// 通过调用WXAccessTokenUtils.getAccessToken(appid, secret)方法
// 使用应用的appid和secret来获取微信API的访问令牌(accessToken)。这个令牌用于后续请求的身份验证。
String accessToken = WXAccessTokenUtils.getAccessToken(appid, secret);
// 构建模板消息对象
TemplateMessageDTO templateMessageDTO = new TemplateMessageDTO(touser, template_id);
templateMessageDTO.setUrl(logUrl);
templateMessageDTO.setData(data);
// 构建HTTP请求
URL url = new URL(String.format("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s", accessToken));
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json; utf-8");
conn.setRequestProperty("Accept", "application/json");
conn.setDoOutput(true);
// 发送请求体,将TemplateMessageDTO对象转换为JSON字符串,并写入请求的输出流中。
// 通过获取OutputStream,将JSON字符串转换为字节数组,并写入输出流来实现。
try (OutputStream os = conn.getOutputStream()) {
byte[] input = JSON.toJSONString(templateMessageDTO).getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
//读取响应使用Scanner类从HTTP连接的输入流中读取响应。这里指定了字符集为UTF-8,并设置了一个分隔符(\A),
//它在整个输入序列的开头匹配,确保Scanner能一次性读取整个响应内容。
//将读取到的响应内容记录到日志中。
try (Scanner scanner = new Scanner(conn.getInputStream(), StandardCharsets.UTF_8.name())) {
String response = scanner.useDelimiter("\\A").next();
logger.info("openai-code-review weixin template message! {}", response);
}
}
这个方法用于向微信用户发送模板消息。它接受两个参数:
logUrl
(消息链接,用户点击模板消息时跳转的URL)和
data
(模板消息中需要替换的数据,格式为
Map<String, Map<String, String>>
,其中外层Map的键对应模板的key,内层Map的键和值分别对应字段名和字段值)。
4、service层
protected void pushMessage(String logUrl) throws Exception {
// 初始化数据容器 创建一个 HashMap 实例 data,用于存储模板消息中需要替换的数据
Map<String, Map<String, String>> data = new HashMap<>();
// 填充数据
TemplateMessageDTO.put(data, TemplateMessageDTO.TemplateKey.REPO_NAME, gitCommand.getProject());
TemplateMessageDTO.put(data, TemplateMessageDTO.TemplateKey.BRANCH_NAME, gitCommand.getBranch());
TemplateMessageDTO.put(data, TemplateMessageDTO.TemplateKey.COMMIT_AUTHOR, gitCommand.getAuthor());
TemplateMessageDTO.put(data, TemplateMessageDTO.TemplateKey.COMMIT_MESSAGE, gitCommand.getMessage());
// 发送模板消息
//调用 weiXin对象sendTemplateMessage方法,将logUrl和填充好的data Map作为参数传递。
// 这个方法负责将模板消息发送到微信服务器,并处理响应。
weiXin.sendTemplateMessage(logUrl, data);
}
这个方法用于构建一个包含Git提交信息的模板消息数据,并通过微信服务发送这个模板消息。它接收一个logUrl字符串作为参数,该字符串是用户点击模板消息时应该跳转的链接。
四、对接方式
Token
对于相关秘钥信息以如下方式来处理
对于对接的方式,采用了Token方式
// 过期时间;默认30分钟
private static final long expireMillis = 30 * 60 * 1000L;
// 缓存服务
public static Cache<String, String> cache = CacheBuilder.newBuilder()
.expireAfterWrite(expireMillis - (60 * 1000L), TimeUnit.MILLISECONDS)
.build();
public static String getToken(String apiKeySecret) {
String[] split = apiKeySecret.split("\\.");
return getToken(split[0], split[1]); // 解析密钥格式:AAAA.BBBB
}
public static String getToken(String apiKey, String apiSecret) {
// 缓存Token
String token = cache.getIfPresent(apiKey);
if (null != token) return token;
// 创建Token
Algorithm algorithm = Algorithm.HMAC256(apiSecret.getBytes(StandardCharsets.UTF_8));
Map<String, Object> payload = new HashMap<>();
payload.put("api_key", apiKey);
payload.put("exp", System.currentTimeMillis() + expireMillis);
payload.put("timestamp", Calendar.getInstance().getTimeInMillis());
Map<String, Object> headerClaims = new HashMap<>();
headerClaims.put("alg", "HS256"); // JWT封装为Token
headerClaims.put("sign_type", "SIGN");
token = JWT.create().withPayload(payload).withHeader(headerClaims).sign(algorithm);
cache.put(apiKey, token);
return token;
}
然后定义了公共的请求参数和返回的响应结果,这里的这个接口非常通用和标准的AI大模型对接的基本通用的结构,解析到响应结果后,提交代码到Github仓库就实现了。
public class ChatCompletionSyncResponseDTO {
private List<Choice> choices;
public static class Choice {
private Message message;
public Message getMessage() {
return message;
}
public void setMessage(Message message) {
this.message = message;
}
}
public static class Message {
private String role;
private String content;
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
public List<Choice> getChoices() {
return choices;
}
public void setChoices(List<Choice> choices) {
this.choices = choices;
}
}
五、实现效果
当修改代码后,并上传到github的master分支后,通过Actions来运行看效果。具体如下:
同时在openai-code-review-log和微信端可以看到推送的消息。
六、项目地址
waafffj/openai-code-review (github.com)
七、项目总结
首先感谢小傅哥的视频讲解,通过学习这个组件项目,学到了一整套的设计方案分析和相应的技术问题处理手段,以及GitHub Actions 的使用机制、OpenAI ChatGLM 对接使用、微信公众号的模板配置和API对接使用、通过代码完成 Git 命令使用,检出代码分支等。同时也对项目开发有了初步认识。
对这个项目最深的印象就是配置脚本文件以及Token对接,前面还了解到JWT技术和RBAC权限模型。期待下次跟进学习!
版权归原作者 Cherish~~ 所有, 如有侵权,请联系我们删除。