ai 代码审核

This commit is contained in:
caesar
2025-01-15 13:14:21 +08:00
commit 36cf1eddb2
16 changed files with 893 additions and 0 deletions

21
.editorconfig Normal file
View File

@@ -0,0 +1,21 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
ij_any_block_comment_at_first_column = false
ij_any_line_comment_at_first_column = false
ij_any_line_comment_add_space_on_reformat = true
ij_any_line_comment_add_space = true
ij_any_block_comment_add_space = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.{js,py,html}]
charset = utf-8

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/.idea/
/node_modules/

169
README.md Normal file
View File

@@ -0,0 +1,169 @@
# 基于 Gitea 和 Ollama (open-webui) 的代码合并自动检查
如果您需要对合并的代码进行审核,但又希望避免将代码发送给第三方,或者您的网络环境处于离线状态(无法连接到第三方平台),那么本项目将是一个理想的选择。
该项目结合了 Gitea 和 Ollama (open-webui),能够自动审核合并代码,并将结果通过评论的形式推送到相应的合并请求中,供开发人员或审核人员参考使用。
# Automatic Code Merge Checks Based on Gitea and Ollama (open-webui)
If you need to review your merged code but prefer not to send it to a third party, or if your network environment is offline (unable to connect to third-party platforms), then this project is an ideal choice.
This project integrates Gitea and Ollama (open-webui) to automatically review merged code and push the results as comments to the corresponding merge requests, allowing developers or reviewers to reference them.
- [如何使用 How to use](#如何使用-How-to-use)
- [输入参数](#输入参数)
- [Input Parameters](#input-parameters)
# 如何使用 How to use
使用方式和普通的 github actions 没什么区别(gitea actions 基本兼容 github actions)。
The usage is similar to regular GitHub Actions (Gitea Actions are mostly compatible with GitHub Actions).
需要设置一个 ollama host如果使用的是 open-webui 建议加上授权token。
You need to set up an Ollama host, and if you are using open-webui, it is recommended to include an authorization token.
```yaml
name: ai-reviews
on:
pull_request:
types: [opened, synchronize]
jobs:
review:
name: Review PR
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Review code
uses: kekxv/AiReviewPR@v0.0.1
with:
model: 'gemma2:2b'
host: ${{ vars.OLLAMA_HOST }}
ai_token: ${{ secrets.AI_TOKEN }}
```
效果如下:
result
![actions.run.png](assets/actions.run.png)
![review.comments.1.png](assets/review.comments.1.png)
![review.comments.2.png](assets/review.comments.2.png)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=kekxv/AiReviewPR&type=Date)](https://star-history.com/#kekxv/AiReviewPR&Date)
## 输入参数
1. **repository**:
- **描述**: 要审查的代码库名称,格式为 `owner/repository`,例如 `actions/checkout`
- **默认值**: `${{ github.repository }}`
2. **BASE_REF**:
- **描述**: GitHub 事件中 Pull Request 的基线引用。
- **默认值**: `${{ github.event.pull_request.base.ref }}`
3. **PULL_REQUEST_NUMBER**:
- **描述**: GitHub 事件中 Pull Request 的编号。
- **默认值**: `${{ github.event.pull_request.number }}`
4. **CHINESE**:
- **描述**: 是否使用中文进行审查。
- **默认值**: `"true"`
5. **token**:
- **描述**: 用于获取存储库的个人访问令牌PAT。建议使用最小权限的服务帐户。
- **默认值**: `${{ github.token }}`
6. **model**:
- **描述**: 要用于代码审查的 AI 模型。
- **必需**: 是
- **默认值**: `'gemma2:2b'`
7. **host**:
- **描述**: Ollama 主机地址。
- **必需**: 是
- **默认值**: `'http://127.0.0.1:11434'`
8. **reviewers_prompt**:
- **描述**: Ollama 的系统提示。定义了代码审查的参数和预期格式。
- **必需**: 否
- **默认值**: 一段详细的说明文本,指导审查生成过程。
9. **ai_token**:
- **描述**: AI 访问令牌。
- **必需**: 否
- **默认值**: `" "`
10. **include_files**:
- **描述**: 要包含审查的文件列表,以逗号分隔。
- **必需**: 否
- **默认值**: `" "`(默认为空,不限制)
11. **exclude_files**:
- **描述**: 要排除审查的文件列表,以逗号分隔。
- **必需**: 否
- **默认值**: `" "`(默认为空,不传递文件)
### Input Parameters
1. **repository**:
- **Description**: The name of the repository to review, formatted as `owner/repository`, for example, `actions/checkout`.
- **Default Value**: `${{ github.repository }}`
2. **BASE_REF**:
- **Description**: The base reference of the Pull Request in the GitHub event.
- **Default Value**: `${{ github.event.pull_request.base.ref }}`
3. **PULL_REQUEST_NUMBER**:
- **Description**: The number of the Pull Request in the GitHub event.
- **Default Value**: `${{ github.event.pull_request.number }}`
4. **CHINESE**:
- **Description**: Whether to use Chinese for the review.
- **Default Value**: `"true"`
5. **token**:
- **Description**: A Personal Access Token (PAT) used to access the repository. It is recommended to use a service account with the least necessary permissions.
- **Default Value**: `${{ github.token }}`
6. **model**:
- **Description**: The AI model to use for the code review.
- **Required**: Yes
- **Default Value**: `'gemma2:2b'`
7. **host**:
- **Description**: The Ollama host address.
- **Required**: Yes
- **Default Value**: `'http://127.0.0.1:11434'`
8. **reviewers_prompt**:
- **Description**: Ollama's system prompt. It defines the parameters and expected format for the code review.
- **Required**: No
- **Default Value**: A detailed description text guiding the review generation process.
9. **ai_token**:
- **Description**: The AI access token.
- **Required**: No
- **Default Value**: `" "`
10. **include_files**:
- **Description**: A comma-separated list of files to include in the review.
- **Required**: No
- **Default Value**: `" "` (empty by default, no restrictions)
11. **exclude_files**:
- **Description**: A comma-separated list of files to exclude from the review.
- **Required**: No
- **Default Value**: `" "` (empty by default, no files passed)

76
action.yml Normal file
View File

@@ -0,0 +1,76 @@
name: 'AI Code Reviewer (AiReviewPR)'
description: 'Perform code review using openai API'
author: kekxv
inputs:
repository:
description: 'Repository name with owner. For example, actions/checkout'
default: ${{ github.repository }}
BASE_REF:
description: 'github event pull_request base ref'
default: ${{ github.event.pull_request.base.ref }}
PULL_REQUEST_NUMBER:
description: 'github event pull_request number'
default: ${{ github.event.pull_request.number }}
CHINESE:
description: 'use chines'
default: "true"
token:
description: >
Personal access token (PAT) used to fetch the repository. The PAT is configured
with the local git config, which enables your scripts to run authenticated git
commands. The post-job step removes the PAT.
We recommend using a service account with the least permissions necessary.
Also when generating a new PAT, select the least scopes necessary.
[Learn more about creating and using encrypted secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets)
default: ${{ github.token }}
model:
description: 'AI model to use for code review'
required: true
default: 'gemma2:2b'
host:
description: 'ollama host'
required: true
default: 'http://127.0.0.1:11434'
reviewers_prompt:
description: 'ollama system prompt'
required: false
default: >
You are an expert developer, your task is to review a set of pull requests.
You are given a list of filenames and their partial contents, but note that you might not have the full context of the code.
Only review lines of code which have been changed (added or removed) in the pull request. The code looks similar to the output of a git diff command. Lines which have been removed are prefixed with a minus (-) and lines which have been added are prefixed with a plus (+). Other lines are added to provide context but should be ignored in the review.
Begin your review by evaluating the changed code using a risk score similar to a LOGAF score but measured from 1 to 5, where 1 is the lowest risk to the code base if the code is merged and 5 is the highest risk which would likely break something or be unsafe.
In your feedback, focus on highlighting potential bugs, improving readability if it is a problem, making code cleaner, and maximising the performance of the programming language. Flag any API keys or secrets present in the code in plain text immediately as highest risk. Rate the changes based on SOLID principles if applicable.
Do not comment on breaking functions down into smaller, more manageable functions unless it is a huge problem. Also be aware that there will be libraries and techniques used which you are not familiar with, so do not comment on those unless you are confident that there is a problem.
Use markdown formatting for the feedback details. Also do not include the filename or risk level in the feedback details.
Ensure the feedback details are brief, concise, accurate. If there are multiple similar issues, only comment on the most critical.
Include brief example code snippets in the feedback details for your suggested changes when you're confident your suggestions are improvements. Use the same programming language as the file under review.
If there are multiple improvements you suggest in the feedback details, use an ordered list to indicate the priority of the changes.
Please respond without using "```markdown"
ai_token:
description: 'ai token'
required: false
default: " "
include_files:
description: 'Comma-separated list of files to include review'
required: false
default: " " # 默认为空,表示不限制
exclude_files:
description: 'Comma-separated list of files to exclude review'
required: false
default: " " # 默认为空,表示不传递文件
runs:
using: node20
main: 'dist/index.js'

BIN
assets/actions.run.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

114
dist/index.js vendored Normal file
View File

@@ -0,0 +1,114 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const node_child_process_1 = require("node:child_process");
const utils_1 = require("./utils");
const prompt_1 = require("./prompt");
const useChinese = (process.env.INPUT_CHINESE || "true").toLowerCase() != "false"; // use chinese
const include_files = (0, utils_1.split_message)(process.env.INPUT_INCLUDE_FILES || "");
const exclude_files = (0, utils_1.split_message)(process.env.INPUT_EXCLUDE_FILES || "");
const system_prompt = (0, prompt_1.take_system_prompt)(useChinese);
// 获取输入参数
const url = process.env.INPUT_HOST; // INPUT_HOST 是从 action.yml 中定义的输入
if (!url) {
console.error('HOST input is required.');
process.exit(1); // 退出程序,返回错误代码
}
const model = process.env.INPUT_MODEL; // INPUT_HOST 是从 action.yml 中定义的输入
if (!model) {
console.error('model input is required.');
process.exit(1); // 退出程序,返回错误代码
}
async function pushComments(message) {
if (!process.env.INPUT_PULL_REQUEST_NUMBER) {
console.log(message);
return;
}
return await (0, utils_1.post)({
url: `${process.env.GITHUB_API_URL}/repos/${process.env.INPUT_REPOSITORY}/issues/${process.env.INPUT_PULL_REQUEST_NUMBER}/comments`,
body: { body: message },
header: { 'Authorization': `token ${process.env.INPUT_TOKEN}` }
});
}
async function aiGenerate({ host, token, prompt, model, system }) {
const data = JSON.stringify({
prompt: prompt,
model: model,
stream: false,
system: system || system_prompt,
options: {
tfs_z: 1.5,
top_k: 30,
top_p: 0.8,
temperature: 0.7,
num_ctx: 10240,
}
});
return await (0, utils_1.post)({
url: `${host}/api/generate`,
body: data,
header: { 'Authorization': token ? `Bearer ${token}` : "", }
});
}
async function aiCheckDiffContext() {
const BASE_REF = process.env.INPUT_BASE_REF;
try {
(0, node_child_process_1.execSync)(`git fetch origin ${BASE_REF}`, { encoding: 'utf-8' });
// exec git diff get diff files
const diffOutput = (0, node_child_process_1.execSync)(`git diff --name-only origin/${BASE_REF}...HEAD`, { encoding: 'utf-8' });
let files = diffOutput.trim().split("\n");
for (let key in files) {
if (!files[key])
continue;
if (include_files.length > 0) {
if (!(0, utils_1.doesAnyPatternMatch)(include_files, files[key])) {
console.log("exclude(include):", files[key]);
continue;
}
}
if (exclude_files.length > 0) {
if ((0, utils_1.doesAnyPatternMatch)(exclude_files, files[key])) {
console.log("exclude(exclude):", files[key]);
continue;
}
}
const fileDiffOutput = (0, node_child_process_1.execSync)(`git diff origin/${BASE_REF}...HEAD -- "${files[key]}"`, { encoding: 'utf-8' });
// ai generate
try {
let response = await aiGenerate({
host: url,
token: process.env.INPUT_AI_TOKEN,
prompt: fileDiffOutput,
model: model,
system: process.env.INPUT_REVIEW_PROMPT
});
if (response.detail) { // noinspection ExceptionCaughtLocallyJS
throw response.detail;
}
if (!response.response) { // noinspection ExceptionCaughtLocallyJS
throw "ollama error";
}
let Review = useChinese ? "审核结果" : "Review";
let comments = `# ${Review} \r\n${process.env.GITHUB_SERVER_URL}/${process.env.INPUT_REPOSITORY}/src/commit/${process.env.GITHUB_SHA}/${files[key]} \r\n\r\n\r\n${response.response}`;
let resp = await pushComments(comments);
if (!resp.id) {
// noinspection ExceptionCaughtLocallyJS
throw new Error(useChinese ? "提交issue评论失败" : "push comment error");
}
console.log(useChinese ? "提交issue评论成功" : "push comment success: ", resp.id);
}
catch (e) {
console.error("aiGenerate:", e);
}
}
}
catch (error) {
console.error('Error executing git diff:', error);
process.exit(1); // error exit
}
}
aiCheckDiffContext()
.then(_ => console.log(useChinese ? "检查结束" : "review finish"))
.catch(e => {
console.error(useChinese ? "检查失败:" : "review error", e);
process.exit(1);
});

30
dist/prompt.js vendored Normal file
View File

@@ -0,0 +1,30 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.take_system_prompt = void 0;
function take_system_prompt(useChinese) {
const chinese_prompt = useChinese ? "You must respond only in Chinese to all inquiries. Please provide clear and accurate answers in Chinese language." : "";
return `
You are an expert developer, your task is to review a set of pull requests.
You are given a list of filenames and their partial contents, but note that you might not have the full context of the code.
Only review lines of code which have been changed (added or removed) in the pull request. The code looks similar to the output of a git diff command. Lines which have been removed are prefixed with a minus (-) and lines which have been added are prefixed with a plus (+). Other lines are added to provide context but should be ignored in the review.
Begin your review by evaluating the changed code using a risk score similar to a LOGAF score but measured from 1 to 5, where 1 is the lowest risk to the code base if the code is merged and 5 is the highest risk which would likely break something or be unsafe.
In your feedback, focus on highlighting potential bugs, improving readability if it is a problem, making code cleaner, and maximising the performance of the programming language. Flag any API keys or secrets present in the code in plain text immediately as highest risk. Rate the changes based on SOLID principles if applicable.
Do not comment on breaking functions down into smaller, more manageable functions unless it is a huge problem. Also be aware that there will be libraries and techniques used which you are not familiar with, so do not comment on those unless you are confident that there is a problem.
Use markdown formatting for the feedback details. Also do not include the filename or risk level in the feedback details.
Ensure the feedback details are brief, concise, accurate. If there are multiple similar issues, only comment on the most critical.
Include brief example code snippets in the feedback details for your suggested changes when you're confident your suggestions are improvements. Use the same programming language as the file under review.
If there are multiple improvements you suggest in the feedback details, use an ordered list to indicate the priority of the changes.
${chinese_prompt}
Please respond without using "\`\`\`markdown"
`;
}
exports.take_system_prompt = take_system_prompt;

79
dist/utils.js vendored Normal file
View File

@@ -0,0 +1,79 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.post = exports.doesAnyPatternMatch = exports.split_message = void 0;
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
function split_message(files) {
console.log("files debug:", files);
files = files.trim();
if (!files) {
let t = files.split("\n");
if (t.length > 0)
return t.map(str => str.trim());
return files.split(",").map(str => str.trim());
}
return [];
}
exports.split_message = split_message;
function doesAnyPatternMatch(patterns, str) {
// 遍历正则表达式数组
return patterns.some(pattern => {
// 创建正则表达式对象,匹配模式
const regex = new RegExp(pattern);
// 测试字符串是否与正则表达式匹配
return regex.test(str);
});
}
exports.doesAnyPatternMatch = doesAnyPatternMatch;
/**
* post data
* @param url url
* @param body post data
* @param header post header
* @param json is json res
*/
async function post({ url, body, header, json }) {
return new Promise((resolve, reject) => {
json = typeof json === "boolean" ? json : true;
const data = typeof body === "string" ? body : JSON.stringify(body);
let url_ = new URL(url);
header = header || {};
header['Content-Type'] = header['Content-Type'] || 'application/json';
header['Content-Length'] = Buffer.byteLength(data);
const options = {
hostname: url_.hostname,
path: url_.pathname + (url_.search || ''),
method: 'POST',
headers: header
};
// noinspection DuplicatedCode
const req = (url_.protocol === "http" ? http_1.default : https_1.default).request(options, (res) => {
let responseBody = '';
res.on('data', (chunk) => {
responseBody += chunk;
});
res.on('end', () => {
try {
if (json) {
resolve(JSON.parse(responseBody));
}
else {
resolve(responseBody);
}
}
catch (error) {
reject(new Error('Failed to parse JSON response'));
}
});
});
req.on('error', (error) => {
reject(new Error(`Request failed: ${error.message}`));
});
req.write(data);
req.end();
});
}
exports.post = post;

137
package-lock.json generated Normal file
View File

@@ -0,0 +1,137 @@
{
"name": "hello-world-action",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hello-world-action",
"version": "1.0.0",
"devDependencies": {
"@types/node": "^22.10.6",
"typescript": "^4.5.4"
}
},
"node_modules/@types/node": {
"version": "22.10.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz",
"integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
}
}
}

14
package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "hello-world-action",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "tsc"
},
"devDependencies": {
"@types/node": "^22.10.6",
"typescript": "^4.5.4"
},
"dependencies": {
}
}

117
src/index.ts Normal file
View File

@@ -0,0 +1,117 @@
import {execSync} from "node:child_process";
import {doesAnyPatternMatch, post, split_message} from "./utils";
import {take_system_prompt} from "./prompt";
const useChinese = (process.env.INPUT_CHINESE || "true").toLowerCase() != "false"; // use chinese
const include_files = split_message(process.env.INPUT_INCLUDE_FILES || "");
const exclude_files = split_message(process.env.INPUT_EXCLUDE_FILES || "");
const system_prompt = take_system_prompt(useChinese);
// 获取输入参数
const url = process.env.INPUT_HOST; // INPUT_HOST 是从 action.yml 中定义的输入
if (!url) {
console.error('HOST input is required.');
process.exit(1); // 退出程序,返回错误代码
}
const model = process.env.INPUT_MODEL; // INPUT_HOST 是从 action.yml 中定义的输入
if (!model) {
console.error('model input is required.');
process.exit(1); // 退出程序,返回错误代码
}
async function pushComments(message: string): Promise<any> {
if (!process.env.INPUT_PULL_REQUEST_NUMBER) {
console.log(message);
return;
}
return await post({
url: `${process.env.GITHUB_API_URL}/repos/${process.env.INPUT_REPOSITORY}/issues/${process.env.INPUT_PULL_REQUEST_NUMBER}/comments`,
body: {body: message},
header: {'Authorization': `token ${process.env.INPUT_TOKEN}`}
})
}
async function aiGenerate({host, token, prompt, model, system}: any): Promise<any> {
const data = JSON.stringify({
prompt: prompt,
model: model,
stream: false,
system: system || system_prompt,
options: {
tfs_z: 1.5,
top_k: 30,
top_p: 0.8,
temperature: 0.7,
num_ctx: 10240,
}
});
return await post({
url: `${host}/api/generate`,
body: data,
header: {'Authorization': token ? `Bearer ${token}` : "",}
})
}
async function aiCheckDiffContext() {
const BASE_REF = process.env.INPUT_BASE_REF
try {
execSync(`git fetch origin ${BASE_REF}`, {encoding: 'utf-8'});
// exec git diff get diff files
const diffOutput = execSync(`git diff --name-only origin/${BASE_REF}...HEAD`, {encoding: 'utf-8'});
let files = diffOutput.trim().split("\n");
for (let key in files) {
if (!files[key]) continue;
if (include_files.length > 0) {
if (!doesAnyPatternMatch(include_files, files[key])) {
console.log("exclude(include):", files[key])
continue;
}
}
if (exclude_files.length > 0) {
if (doesAnyPatternMatch(exclude_files, files[key])) {
console.log("exclude(exclude):", files[key])
continue;
}
}
const fileDiffOutput = execSync(`git diff origin/${BASE_REF}...HEAD -- "${files[key]}"`, {encoding: 'utf-8'});
// ai generate
try {
let response = await aiGenerate({
host: url,
token: process.env.INPUT_AI_TOKEN,
prompt: fileDiffOutput,
model: model,
system: process.env.INPUT_REVIEW_PROMPT
})
if (response.detail) { // noinspection ExceptionCaughtLocallyJS
throw response.detail;
}
if (!response.response) { // noinspection ExceptionCaughtLocallyJS
throw "ollama error";
}
let Review = useChinese ? "审核结果" : "Review";
let comments = `# ${Review} \r\n${process.env.GITHUB_SERVER_URL}/${process.env.INPUT_REPOSITORY}/src/commit/${process.env.GITHUB_SHA}/${files[key]} \r\n\r\n\r\n${response.response}`
let resp = await pushComments(comments);
if (!resp.id) {
// noinspection ExceptionCaughtLocallyJS
throw new Error(useChinese ? "提交issue评论失败" : "push comment error")
}
console.log(useChinese ? "提交issue评论成功" : "push comment success: ", resp.id)
} catch (e) {
console.error("aiGenerate:", e)
}
}
} catch (error) {
console.error('Error executing git diff:', error);
process.exit(1); // error exit
}
}
aiCheckDiffContext()
.then(_ => console.log(useChinese ? "检查结束" : "review finish"))
.catch(e => {
console.error(useChinese ? "检查失败:" : "review error", e);
process.exit(1);
});

26
src/prompt.ts Normal file
View File

@@ -0,0 +1,26 @@
export function take_system_prompt(useChinese: boolean) {
const chinese_prompt = useChinese ? "You must respond only in Chinese to all inquiries. Please provide clear and accurate answers in Chinese language." : "";
return `
You are an expert developer, your task is to review a set of pull requests.
You are given a list of filenames and their partial contents, but note that you might not have the full context of the code.
Only review lines of code which have been changed (added or removed) in the pull request. The code looks similar to the output of a git diff command. Lines which have been removed are prefixed with a minus (-) and lines which have been added are prefixed with a plus (+). Other lines are added to provide context but should be ignored in the review.
Begin your review by evaluating the changed code using a risk score similar to a LOGAF score but measured from 1 to 5, where 1 is the lowest risk to the code base if the code is merged and 5 is the highest risk which would likely break something or be unsafe.
In your feedback, focus on highlighting potential bugs, improving readability if it is a problem, making code cleaner, and maximising the performance of the programming language. Flag any API keys or secrets present in the code in plain text immediately as highest risk. Rate the changes based on SOLID principles if applicable.
Do not comment on breaking functions down into smaller, more manageable functions unless it is a huge problem. Also be aware that there will be libraries and techniques used which you are not familiar with, so do not comment on those unless you are confident that there is a problem.
Use markdown formatting for the feedback details. Also do not include the filename or risk level in the feedback details.
Ensure the feedback details are brief, concise, accurate. If there are multiple similar issues, only comment on the most critical.
Include brief example code snippets in the feedback details for your suggested changes when you're confident your suggestions are improvements. Use the same programming language as the file under review.
If there are multiple improvements you suggest in the feedback details, use an ordered list to indicate the priority of the changes.
${chinese_prompt}
Please respond without using "\`\`\`markdown"
`;
}

75
src/utils.ts Normal file
View File

@@ -0,0 +1,75 @@
import http from "http";
import https from "https";
export function split_message(files: string) {
console.log("files debug:",files)
files = files.trim()
if (!files) {
let t = files.split("\n");
if (t.length > 0) return t.map(str => str.trim());
return files.split(",").map(str => str.trim())
}
return []
}
export function doesAnyPatternMatch(patterns: Array<string>, str: string) {
// 遍历正则表达式数组
return patterns.some(pattern => {
// 创建正则表达式对象,匹配模式
const regex = new RegExp(pattern);
// 测试字符串是否与正则表达式匹配
return regex.test(str);
});
}
/**
* post data
* @param url url
* @param body post data
* @param header post header
* @param json is json res
*/
export async function post({url, body, header, json}: any): Promise<string> {
return new Promise((resolve, reject) => {
json = typeof json === "boolean" ? json : true;
const data = typeof body === "string" ? body : JSON.stringify(body);
let url_ = new URL(url);
header = header || {};
header['Content-Type'] = header['Content-Type'] || 'application/json';
header['Content-Length'] = Buffer.byteLength(data)
const options = {
hostname: url_.hostname, // 确保去掉协议部分
path: url_.pathname + (url_.search || ''),
method: 'POST',
headers: header
};
// noinspection DuplicatedCode
const req = (url_.protocol === "http" ? http : https).request(options, (res) => {
let responseBody = '';
res.on('data', (chunk) => {
responseBody += chunk;
});
res.on('end', () => {
try {
if (json) {
resolve(JSON.parse(responseBody));
} else {
resolve(responseBody);
}
} catch (error) {
reject(new Error('Failed to parse JSON response'));
}
});
});
req.on('error', (error) => {
reject(new Error(`Request failed: ${error.message}`));
});
req.write(data);
req.end();
});
}

33
tsconfig.json Normal file
View File

@@ -0,0 +1,33 @@
{
"moduleResolution": "node",
"compilerOptions": {
"typeRoots": [
"node_modules/@types"
],
"types": [
"node"
],
"target": "ES2020",
// 编译目标
"module": "CommonJS",
"moduleResolution": "node",
"outDir": "./dist",
// 输出目录
"strict": true,
// 启用所有严格类型检查选项
"esModuleInterop": true,
// 允许默认导入非模块
"skipLibCheck": true,
// 跳过库文件检查
"forceConsistentCasingInFileNames": true
// 强制文件名的大小写一致性
},
"include": [
"src/**/*"
// 包含 src 文件夹中的所有文件
],
"exclude": [
"**/*.spec.ts"
// 排除测试文件
]
}