Compare commits

..
14 Commits
Author SHA1 Message Date
wonder 7cb2f7fe62 🔄Update: 更新AI模型版本 2025-10-15 14:59:20 +08:00
wonder afdd50c857 🔄Update: 消息卡片新版本 2025-10-15 14:58:37 +08:00
wonder ed4000b834 🐛Fix: 修复已删除文件逻辑 2025-10-15 14:55:10 +08:00
wonder 90af1d199d 🐛Fix: 排除部分文件读取异常 2025-10-13 19:49:57 +08:00
wonder ea42aa89d9 🐛Fix: 增加 AI 返回错误时的特判 2025-10-13 19:44:21 +08:00
wonder 2fca4f38df 📝Docs: 更新 README.md 2025-10-13 19:39:49 +08:00
wonder 233b7f4c36 🐛Fix: 返回用户友好格式 2025-10-13 19:28:45 +08:00
wonder 023f0b9590 ✨Feat: 支持 AI 总结代码修改内容 2025-10-13 19:20:48 +08:00
wonder c4acef5887 🔄Update: 设置系统提示词 2025-10-13 19:04:05 +08:00
wonder fe8dde8c73 🔄Update: 引入 AI 服务 2025-10-13 18:43:42 +08:00
wonder ff2494dcf2 ✨Feat: 获取文件内容 2025-10-13 15:36:03 +08:00
wonder 7866db7cbb 🔧Chore: 增加日志 2025-10-13 15:07:26 +08:00
wonder ef2bd4ec21 🐛Fix: 修复缺失的响应头 2025-10-13 15:05:07 +08:00
wonder ce1d5dd7fe ✨Feat: 获取文件修改 2025-10-13 15:02:00 +08:00
12 changed files with 235 additions and 4 deletions
+4
View File
@@ -2,6 +2,10 @@
🤖 基于飞书 Webhook 实现仓库代码更新 | AI 摘要
![img.png](img.png)
![lark.jpg](lark.jpg)
> [飞书开放平台 - 服务端API - 事件概述](https://open.feishu.cn/document/server-docs/event-subscription-guide/overview)
```mermaid
+14
View File
@@ -28,6 +28,20 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-http -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>5.8.27</version>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-json -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
<version>5.8.27</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,19 @@
package cn.hezhaohui.webhook.constant;
public class AIConstant {
public static final String AI_URL = "https://api.siliconflow.cn/v1/chat/completions";
public static final String AI_TOKEN = "sk-htzofywujywqlpwdddcrwxuotefavvqnibrrhvrbdnbiilkw";
public static final String AI_MODEL = "Qwen/Qwen3-Coder-30B-A3B-Instruct";
public static final String AI_SYSTEM_PROMPT = "【身份】你是一位代码总结专家,你擅长总结用户给出的代码并且做出有见解的回答。" +
"【要求】你需要根据用户给出的 commit messages 以及 代码文件的变化,总结出代码的改动内容,并给出有见解的回答。回答结果需要简洁精炼专业,默认情况下不分点描述。对于简单的技术内容,通过概括性的语言描述即可,避免口语化回答。你不需要输出带有 MarkDown 格式的文本,因为用户的终端不支持该选项。在适宜的地方需要使用换行。" +
"【输入格式】" +
"[Commit Messages]: ...\n" +
"[Added Files]: ...\n" +
"[Removed Files]: ...\n" +
"[Modified Files]: ...\n" +
"【输出格式】" +
"[总结]: ...";
public static final String DEFAULT_ERROR_RESPONSE = "*变化文件数量过多,请在仓库中查看";
}
@@ -23,7 +23,8 @@ public class HookController {
@PostMapping
public ResponseEntity<String> handlePush(@RequestBody GiteaPushEvent event) {
log.info(event.toString());
log.info("\n============================\n");
hookService.handlePush(event);
return ResponseEntity.ok().build();
return ResponseEntity.ok("Ciallo~(∠・ω< )⌒★");
}
}
@@ -6,7 +6,6 @@ import java.util.List;
@Data
public class GiteaPushEvent {
private String secret;
private String ref;
private String before;
private String after;
@@ -35,6 +34,9 @@ public class GiteaPushEvent {
private User author;
private User committer;
private String timestamp;
private List<String> added;
private List<String> removed;
private List<String> modified;
}
@Data
@@ -0,0 +1,35 @@
package cn.hezhaohui.webhook.service;
import cn.hezhaohui.webhook.constant.AIConstant;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class AIService {
public String getAIResponse(String prompt) {
Map<String, Object> body = new HashMap<>();
body.put("model", AIConstant.AI_MODEL);
body.put("messages", List.of(Map.of("role", "system", "content", AIConstant.AI_SYSTEM_PROMPT), Map.of("role", "user", "content", prompt)));
String jsonBody = JSONUtil.toJsonStr(body);
HttpResponse response = HttpRequest.post(AIConstant.AI_URL)
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + AIConstant.AI_TOKEN)
.body(jsonBody)
.execute();
if (response.getStatus() != 200) {
return AIConstant.DEFAULT_ERROR_RESPONSE;
}
JSONObject jsonResponse = JSONUtil.parseObj(response.body());
return jsonResponse.getJSONArray("choices").getJSONObject(0).getJSONObject("message").getStr("content");
}
}
@@ -0,0 +1,50 @@
package cn.hezhaohui.webhook.service;
import cn.hutool.http.HttpUtil;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
@Service
public class FileService {
private static final Set<String> UNSUPPORTED_FILE_TYPES = Set.of(
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".ico", // 图片
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", // 文档
".zip", ".rar", ".7z", ".tar", ".gz", // 压缩文件
".exe", ".dll", ".so", ".class", // 可执行文件
".jar", ".war", ".ear", // Java归档
".gitignore", ".gitattributes", ".gitmodules" // Git配置文件
);
// repo: http://47.121.181.112:3000/wonder/lark-webhook-sender
// prefix: /raw/branch/main/
// raw: gitea-webhook/src/main/java/cn/hezhaohui/webhook/controller/HookController.java
// parse: http://47.121.181.112:3000/wonder/lark-webhook-sender/raw/branch/main/gitea-webhook/src/main/java/cn/hezhaohui/webhook/controller/HookController.java
public String getRawUrl(String repo, String raw) {
return repo +"/raw/branch/main/" +raw;
}
public String getRawContents(String repo, String raw) {
String url = getRawUrl(repo, raw);
String fileName = raw.substring(raw.lastIndexOf("/")+1);
// 检查文件拓展名
String fileExtension = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
if (UNSUPPORTED_FILE_TYPES.contains(fileExtension)) {
return "[FileName]" + fileName + "\n[FileContent]*不支持显示的文件类型,请忽略\n";
}
String fileContent = HttpUtil.get(url);
return "[FileName]" + fileName + "\n" + "[FileContent]" + fileContent + "\n";
}
public String getAllFiles(String repo, List<String> files) {
StringBuilder sb = new StringBuilder();
for (String file : files) {
sb.append(getRawContents(repo, file));
}
return sb.toString();
}
}
@@ -2,18 +2,76 @@ package cn.hezhaohui.webhook.service;
import cn.hezhaohui.lark.entity.CardData;
import cn.hezhaohui.lark.util.Lark;
import cn.hezhaohui.webhook.constant.AIConstant;
import cn.hezhaohui.webhook.entity.GiteaPushEvent;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Stream;
@Service
@Slf4j
public class HookService {
@Resource
private AIService aiService;
@Resource
private FileService fileService;
public void handlePush(GiteaPushEvent event) {
List<String> commitMsgList = event.getCommits().stream()
.map(GiteaPushEvent.Commit::getMessage)
.toList();
List<String> addedFiles = event.getCommits().stream()
.flatMap(commit -> commit.getAdded().stream())
.distinct()
.toList();
List<String> removedFiles = event.getCommits().stream()
.flatMap(commit -> commit.getRemoved().stream())
.distinct()
.toList();
List<String> modifiedFiles = event.getCommits().stream()
.flatMap(commit -> commit.getModified().stream())
.distinct()
.toList();
if (addedFiles.isEmpty() && removedFiles.isEmpty() && modifiedFiles.isEmpty()) {
return;
}
String ai = AIConstant.DEFAULT_ERROR_RESPONSE;
if (addedFiles.size() + removedFiles.size() + modifiedFiles.size() <= 20) {
String addedFilesContent = fileService.getAllFiles(event.getRepository().getHtml_url(), addedFiles);
// 对于已被删除的文件,访问不到对应文件的url,只需要返回文件名即可
String removedFilesContent = removedFiles.toString();
String modifiedFilesContent = fileService.getAllFiles(event.getRepository().getHtml_url(), modifiedFiles);
String allFilesContentString = "[Added Files]: \n" +
addedFilesContent +
"\n[Removed Files]: \n" +
removedFilesContent +
"\n[Modified Files]: \n" +
modifiedFilesContent;
String input = "[Commit Messages]: \n" +
String.join("\n", commitMsgList) +
"\n" + allFilesContentString;
ai = aiService.getAIResponse(input);
}
log.info("commitMsgList: {}\n", commitMsgList);
log.info("addedFiles: {}\n", addedFiles);
log.info("removedFiles: {}\n", removedFiles);
log.info("modifiedFiles: {}\n", modifiedFiles);
CardData cardData = CardData.builder()
.title("[Gitea] 仓库状态变化")
.repo(event.getRepository().getName())
@@ -21,7 +79,7 @@ public class HookService {
.msg(String.join("\n", commitMsgList))
.tag("PUSH")
.subtitle("Ciallo~(∠・ω< )⌒★")
.ai("待开发")
.ai(ai)
.build();
try {
@@ -0,0 +1,48 @@
package cn.hezhaohui.webhook.service;
import junit.framework.TestCase;
public class AIServiceTest extends TestCase {
public void testGetAIResponse() {
AIService aiService = new AIService();
String response = aiService.getAIResponse("[Commit Messages]: Chore: 增加日志\n" +
" Fix: 修复缺失的响应头\n" +
" ✨Feat: 获取文件修改" +
"[Added Files]:" +
"[FileName] AIService.java\n" +
"package cn.hezhaohui.webhook.service;\n" +
"\n" +
"import cn.hezhaohui.webhook.constant.AIConstant;\n" +
"import cn.hutool.http.HttpRequest;\n" +
"import cn.hutool.http.HttpResponse;\n" +
"import cn.hutool.http.HttpUtil;\n" +
"import cn.hutool.json.JSONUtil;\n" +
"import org.springframework.stereotype.Service;\n" +
"\n" +
"import java.util.HashMap;\n" +
"import java.util.List;\n" +
"import java.util.Map;\n" +
"\n" +
"@Service\n" +
"public class AIService {\n" +
" public String getAIResponse(String prompt) {\n" +
" Map<String, Object> body = new HashMap<>();\n" +
" body.put(\"model\", AIConstant.AI_MODEL);\n" +
" body.put(\"messages\", List.of(Map.of(\"role\", \"system\", \"content\", AIConstant.AI_SYSTEM_PROMPT), Map.of(\"role\", \"user\", \"content\", prompt)));\n" +
"\n" +
" String jsonBody = JSONUtil.toJsonStr(body);\n" +
"\n" +
" HttpResponse response = HttpRequest.post(AIConstant.AI_URL)\n" +
" .header(\"Content-Type\", \"application/json\")\n" +
" .header(\"Authorization\", \"Bearer \" + AIConstant.AI_TOKEN)\n" +
" .body(jsonBody)\n" +
" .execute();\n" +
" return \"AI response: \" + response.body();\n" +
" }\n" +
"}\n" +
"[Removed Files]:" +
"[Modified Files]:");
System.out.println(response);
assertNotNull(response);
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

@@ -4,5 +4,5 @@ public class AppConstant {
public static final String APP_ID = "cli_a860966d8eb69013";
public static final String APP_SECRET = "srCSYZQDaglgSkLSs5MGrfEsCW4fCQE2";
public static final String TEMPATE_ID = "AAqxBOlIDzI20";
public static final String TEMPLATE_VERSION = "1.0.1";
public static final String TEMPLATE_VERSION = "1.0.2";
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB