feat: 项目更名并调整接口

This commit is contained in:
jeffusion
2025-04-14 20:55:56 +08:00
parent ad8aaa0615
commit b8e5c5eb41
9 changed files with 355 additions and 155 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules/
dist/
.env
kubernetes.yaml

39
Makefile Normal file
View File

@@ -0,0 +1,39 @@
NAME = ai-code-review
REGISTRY = docker-hosted.nexus.satfabric.com
SHA1 = $(shell git rev-parse HEAD)
REVISION = $(shell git rev-list --count HEAD)
LABELS = --label SHA1=${SHA1} --label REVISION=${REVISION}
VERSION = $(shell ./auto-ver.sh).${REVISION}
OS ?= linux
ARCH ?= amd64
.PHONY: build
build:
npm run build
.PHONY: container.build
container.build:
make build
docker buildx build --platform=${OS}/${ARCH} -t ${REGISTRY}/${NAME}:v${VERSION} ${LABELS} -f Dockerfile .
.PHONY: container.push
container.push:
make build
docker buildx build --platform=${OS}/${ARCH} -t ${REGISTRY}/${NAME}:v${VERSION} ${LABELS} -f Dockerfile . --push
.PHONY: k8s.yaml
k8s.yaml:
cp ./kubernetes.yaml.template ./kubernetes.yaml
sed -i.bak 's@<%= IMAGE_FROM %>@registry.kuiper.com/${NAME}:v${VERSION}@g' ./kubernetes.yaml
sed -i.bak 's@<%= APP_NAME %>@${NAME}@g' ./kubernetes.yaml
rm -f ./kubernetes.yaml.bak
.PHONY: help
help:
@echo 'Usage: make [target]'
@echo 'Available targets:'
@printf " %-25s %s\n" "container.<build|push>" "本地构建或构建加推送容器镜像若不执行TAG参数则自动生成VERSION字段"
@printf " %-25s %s\n" "k8s.yaml" "生成kubernetes.yaml文件"
@printf " %-25s %s\n" "help" "显示此帮助信息"

View File

@@ -1,6 +1,6 @@
# AI Code Review for Gitea
# Gitea Assistant
基于Bun和TypeScript的Gitea代码审查助手自动为Pull Request和单个提交提供AI驱动的代码审查。
Gitea功能增强助手基于Bun和TypeScript开发,提供AI驱动的代码审查等增强功能
## 功能特点
@@ -68,21 +68,15 @@
2. 在Gitea仓库中配置Webhook
在Gitea仓库设置中添加两个Webhook:
在Gitea仓库设置中添加Webhook:
**Pull Request审查webhook**:
- URL: `http://your-server:3000/webhook/gitea/pull_request`
**统一Webhook端点**:
- URL: `http://your-server:3000/webhook/gitea`
- 内容类型: `application/json`
- 秘钥: 设置为与`WEBHOOK_SECRET`环境变量相同的值
- 触发事件: 选择"Pull Request"
- 触发事件: 选择"Pull Request"和"Status"事件
**提交状态审查webhook**:
- URL: `http://your-server:3000/webhook/gitea/status`
- 内容类型: `application/json`
- 秘钥: 设置为与`WEBHOOK_SECRET`环境变量相同的值
- 触发事件: 选择"Status"
> 注意: 老端点 `/webhook/gitea` 仍然支持Pull Request审查但仅作向后兼容使用。
> 注意: 系统使用统一的webhook端点处理所有事件类型包括Pull Request和Commit Status事件。
### Webhook签名验证
@@ -92,6 +86,7 @@
2. 在Gitea的Webhook配置中使用相同的字符串作为"秘钥"
3. 每次请求时,系统会验证请求头中的`X-Gitea-Signature`
4. 如果签名验证失败,请求会被拒绝处理
5. 在开发环境下(`NODE_ENV=development`),如果没有提供签名,系统会跳过验证
验证方法使用SHA-256哈希算法在处理高负载的情况下这能防止恶意请求并保证请求来源的真实性。
@@ -139,6 +134,6 @@ MIT
- `${file.path}` - 当前文件路径
- `${fileContent}` - 文件的完整内容
- `${file.changes.map(c => `${c.lineNumber}: ${c.content} (${c.type === 'add' ? '新增' : '上下文'})`).join('\n')}` - 变更行的上下文
- ```${file.changes.map(c => `${c.lineNumber}: ${c.content} (${c.type === 'add' ? '新增' : '上下文'})`).join('\n')}``` - 变更行的上下文
请确保你的自定义提示返回正确的格式特别是对于行评论必须返回有效的JSON数组。

46
auto-ver.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/bin/sh
# 生成基于日期的版本号函数
generate_date_version() {
date +"%Y.%-m.%-d"
}
# 由于可能允许在detached状态下执行脚本因此我们允许外部指定HEAD的REF
HEAD_REF_NAME=$1
if [ -z "$HEAD_REF_NAME" ]; then
HEAD_REF_NAME=$(git rev-parse --abbrev-ref HEAD)
fi
if [ "$HEAD_REF_NAME" = "main" ]; then
# 对于main分支使用日期格式的版本号
generate_date_version
exit 0
fi
TAG_RECENT=$(git describe --tags --abbrev=0 --match "v*" 2>/dev/null)
if [ -z "$TAG_RECENT" ]; then
# 无法获取到符合semver格式的tag时使用日期格式的版本号
generate_date_version
exit 0
fi
TAG_COMMIT=$(git log $TAG_RECENT --oneline -1 | awk '{print $1}')
TAG_VERSION=$(echo $TAG_RECENT | tr -d "v")
sub_version_by_type()
{
type=$1
field=3
if [ "$type" = "major" ]; then
field=1
elif [ "$type" = "minor" ]; then
field=2
else
field=3
fi
echo $TAG_VERSION | awk -F '.' -v field=$field '{print $field}'
}
MAJOR=$(sub_version_by_type major)
MINOR=$(sub_version_by_type minor)
PATCH=$(sub_version_by_type patch)
REVISION_TO_HEAD=$(git rev-list --count $TAG_COMMIT...HEAD)
echo "$MAJOR.$MINOR.$(($PATCH+$REVISION_TO_HEAD))"

View File

@@ -1,12 +1,12 @@
version: '3.8'
services:
ai-code-review:
gitea-assistant:
build:
context: .
dockerfile: Dockerfile
image: registry.kuiper.com/ai-code-review:latest
container_name: ai-code-review
image: registry.kuiper.com/gitea-assistant:latest
container_name: gitea-assistant
restart: unless-stopped
ports:
- "3000:3000"

97
kubernetes.yaml.template Normal file
View File

@@ -0,0 +1,97 @@
# ConfigMap 用于存储非敏感配置
apiVersion: v1
kind: ConfigMap
metadata:
name: gitea-assistant-config
data:
GITEA_API_URL: "http://gitea.kuiper.com/api/v1"
OPENAI_BASE_URL: "https://aihubmix.com/v1"
OPENAI_MODEL: "gpt-4o-mini"
PORT: "3000"
---
# Secret 用于存储敏感信息
apiVersion: v1
kind: Secret
metadata:
name: gitea-assistant-secrets
type: Opaque
data:
# base64 编码的敏感数据
GITEA_ACCESS_TOKEN: "{{GITEA_ACCESS_TOKEN}}"
OPENAI_API_KEY: "{{OPENAI_API_KEY}}"
WEBHOOK_SECRET: "{{WEBHOOK_SECRET}}"
---
# Deployment 定义应用程序部署
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea-assistant
labels:
app: gitea-assistant
spec:
replicas: 1
selector:
matchLabels:
app: gitea-assistant
template:
metadata:
labels:
app: gitea-assistant
spec:
containers:
- name: gitea-assistant
image: registry.kuiper.com/gitea-assistant:{{VERSION}}
imagePullPolicy: Always
ports:
- containerPort: 3000
name: http
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "100m"
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 5
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
securityContext:
runAsUser: 1001
runAsGroup: 1001
allowPrivilegeEscalation: false
envFrom:
- configMapRef:
name: gitea-assistant-config
- secretRef:
name: gitea-assistant-secrets
---
# Service 暴露应用程序
apiVersion: v1
kind: Service
metadata:
name: gitea-assistant
labels:
app: gitea-assistant
spec:
selector:
app: gitea-assistant
ports:
- port: 3000
targetPort: 3000
nodePort: 30300
name: http
type: NodePort

View File

@@ -1,7 +1,7 @@
{
"name": "gitea-ai-reviewer",
"name": "gitea-assistant",
"version": "1.0.0",
"description": "AI-driven code review for Gitea",
"description": "Gitea功能增强助手包含AI代码审核功能",
"engines": {
"bun": ">=1.2.5"
},

View File

@@ -8,6 +8,13 @@ import { logger } from '../utils/logger';
// 判断是否为开发环境
const isDev = process.env.NODE_ENV === 'development' || !process.env.NODE_ENV;
// Gitea webhook事件类型
enum GiteaEventType {
PullRequest = 'pull_request',
Status = 'status',
Unknown = 'unknown'
}
/**
* 验证Webhook请求签名
*/
@@ -48,9 +55,9 @@ function verifyWebhookSignature(body: string, signature: string): boolean {
}
/**
* 处理Pull Request事件
* 统一处理Gitea Webhook事件
*/
export async function handlePullRequestEvent(c: Context): Promise<Response> {
export async function handleGiteaWebhook(c: Context): Promise<Response> {
try {
// 验证Webhook签名
const signature = c.req.header('X-Gitea-Signature') || '';
@@ -64,137 +71,160 @@ export async function handlePullRequestEvent(c: Context): Promise<Response> {
// 解析请求体
const body = JSON.parse(rawBody);
// 仅处理PR打开或更新事件
if (
body.action !== 'opened' &&
body.action !== 'reopened' &&
body.action !== 'synchronize' &&
body.action !== 'edited'
) {
return c.json({ status: 'ignored', message: '无需处理的事件类型' }, 200);
// 确定事件类型
const eventType = determineEventType(c, body);
logger.info(`收到Gitea Webhook事件: ${eventType}`);
// 根据事件类型路由到相应的处理逻辑
switch (eventType) {
case GiteaEventType.PullRequest:
return await handlePullRequestEvent(c, body);
case GiteaEventType.Status:
return await handleCommitStatusEvent(c, body);
default:
logger.warn(`未支持的Webhook事件类型: ${eventType}`);
return c.json({ status: 'ignored', message: '未支持的Webhook事件类型' }, 200);
}
// 从事件中提取必要信息
const {
pull_request: pullRequest,
repository: repo
} = body;
if (!pullRequest || !repo) {
return c.json({ error: '无效的Webhook数据' }, 400);
}
const prNumber = pullRequest.number;
const owner = repo.owner.login;
const repoName = repo.name;
logger.info(`收到PR事件`, { owner, repo: repoName, prNumber, action: body.action });
// 开始异步审查流程
reviewPullRequest(owner, repoName, prNumber).catch(error => {
logger.error(`审查PR ${owner}/${repoName}#${prNumber} 失败:`, error);
});
// 立即返回以不阻塞Webhook
return c.json({ status: 'accepted', message: '代码审查请求已接受' }, 202);
} catch (error) {
logger.error('处理Webhook事件失败:', error);
return c.json({ error: '处理Webhook事件失败' }, 500);
logger.error('处理Gitea Webhook事件失败:', error);
return c.json({ error: '处理Gitea Webhook事件失败' }, 500);
}
}
/**
* 确定Gitea Webhook事件类型
*/
function determineEventType(c: Context, body: any): GiteaEventType {
// 优先从请求头获取事件类型
const eventHeader = c.req.header('X-Gitea-Event');
if (eventHeader) {
if (eventHeader === 'pull_request') return GiteaEventType.PullRequest;
if (eventHeader === 'status') return GiteaEventType.Status;
}
// 如果没有事件头,尝试从请求体判断
if (body.pull_request) return GiteaEventType.PullRequest;
if (body.state && (body.sha || body.commit)) return GiteaEventType.Status;
// 无法确定事件类型
return GiteaEventType.Unknown;
}
/**
* 处理Pull Request事件
*/
async function handlePullRequestEvent(c: Context, body: any): Promise<Response> {
// 仅处理PR打开或更新事件
if (
body.action !== 'opened' &&
body.action !== 'reopened' &&
body.action !== 'synchronize' &&
body.action !== 'edited'
) {
return c.json({ status: 'ignored', message: '无需处理的事件类型' }, 200);
}
// 从事件中提取必要信息
const {
pull_request: pullRequest,
repository: repo
} = body;
if (!pullRequest || !repo) {
return c.json({ error: '无效的Webhook数据' }, 400);
}
const prNumber = pullRequest.number;
const owner = repo.owner.login;
const repoName = repo.name;
logger.info(`收到PR事件`, { owner, repo: repoName, prNumber, action: body.action });
// 开始异步审查流程
reviewPullRequest(owner, repoName, prNumber).catch(error => {
logger.error(`审查PR ${owner}/${repoName}#${prNumber} 失败:`, error);
});
// 立即返回以不阻塞Webhook
return c.json({ status: 'accepted', message: '代码审查请求已接受' }, 202);
}
/**
* 处理提交状态更新事件
*/
export async function handleCommitStatusEvent(c: Context): Promise<Response> {
try {
// 验证Webhook签名
const signature = c.req.header('X-Gitea-Signature') || '';
const rawBody = await c.req.text();
async function handleCommitStatusEvent(c: Context, body: any): Promise<Response> {
// 记录收到的数据,方便调试
logger.debug('收到提交状态webhook数据', {
state: body.state,
sha: body.sha,
commit_id: body.commit?.id,
context: body.context,
repo: body.repository?.full_name
});
if (!verifyWebhookSignature(rawBody, signature)) {
logger.error('Webhook签名验证失败');
return c.json({ error: 'Webhook签名验证失败' }, 401);
}
const body = JSON.parse(rawBody);
// 记录收到的数据,方便调试
logger.debug('收到提交状态webhook数据', {
state: body.state,
sha: body.sha,
commit_id: body.commit?.id,
context: body.context,
repo: body.repository?.full_name
});
// 验证请求体中是否包含必要信息
if (!body.commit || !body.repository || !body.state) {
logger.error('无效的Webhook数据', { body: JSON.stringify(body).substring(0, 500) });
return c.json({ error: '无效的Webhook数据' }, 400);
}
// 只处理成功状态的提交
if (body.state !== 'success') {
return c.json({ status: 'ignored', message: `忽略非成功状态的提交: ${body.state}` }, 200);
}
// 获取关键信息
const commitSha = body.sha || body.commit.id; // 兼容不同版本的Gitea
const owner = body.repository.owner.login;
const repoName = body.repository.name;
// 检查提交是否与PR相关
let relatedPR: PullRequestDetails | null = null;
try {
relatedPR = await giteaService.getRelatedPullRequest(owner, repoName, commitSha);
if (!relatedPR) {
logger.info(`提交 ${commitSha} 不与任何PR关联跳过审查`);
return c.json({ status: 'ignored', message: '提交不与任何PR关联' }, 200);
}
logger.info(`提交 ${commitSha} 关联到PR #${relatedPR.number}`);
} catch (error) {
logger.warn(`检查提交 ${commitSha} 是否与PR关联时出错`, error);
// 继续处理因为有可能API临时错误但提交仍需审查
}
// 提取commit信息
const commitInfo = {
sha: commitSha,
message: body.commit.message || '',
added: body.commit.added || [],
removed: body.commit.removed || [],
modified: body.commit.modified || []
};
logger.info(`收到提交状态更新事件`, {
owner,
repo: repoName,
commitSha,
state: body.state,
relatedPR: relatedPR?.number || 'unknown',
added: commitInfo.added.length,
modified: commitInfo.modified.length,
removed: commitInfo.removed.length
});
// 如果没有文件变更信息,则忽略
if (commitInfo.added.length === 0 && commitInfo.modified.length === 0 && commitInfo.removed.length === 0) {
logger.warn('提交没有文件变更信息,忽略审查', { commitSha });
return c.json({ status: 'ignored', message: '提交没有文件变更信息' }, 200);
}
// 开始异步审查流程传入关联的PR信息
reviewCommit(owner, repoName, commitSha, commitInfo, relatedPR).catch(error => {
logger.error(`审查提交 ${owner}/${repoName}@${commitSha} 失败:`, error);
});
// 立即返回以不阻塞Webhook
return c.json({ status: 'accepted', message: '提交代码审查请求已接受' }, 202);
} catch (error) {
logger.error('处理提交状态Webhook事件失败:', error);
return c.json({ error: '处理提交状态Webhook事件失败' }, 500);
// 验证请求体中是否包含必要信息
if (!body.commit || !body.repository || !body.state) {
logger.error('无效的Webhook数据', { body: JSON.stringify(body).substring(0, 500) });
return c.json({ error: '无效的Webhook数据' }, 400);
}
// 只处理成功状态的提交
if (body.state !== 'success') {
return c.json({ status: 'ignored', message: `忽略非成功状态的提交: ${body.state}` }, 200);
}
// 获取关键信息
const commitSha = body.sha || body.commit.id; // 兼容不同版本的Gitea
const owner = body.repository.owner.login;
const repoName = body.repository.name;
// 检查提交是否与PR相关
let relatedPR: PullRequestDetails | null = null;
try {
relatedPR = await giteaService.getRelatedPullRequest(owner, repoName, commitSha);
if (!relatedPR) {
logger.info(`提交 ${commitSha} 不与任何PR关联跳过审查`);
return c.json({ status: 'ignored', message: '提交不与任何PR关联' }, 200);
}
logger.info(`提交 ${commitSha} 关联到PR #${relatedPR.number}`);
} catch (error) {
logger.warn(`检查提交 ${commitSha} 是否与PR关联时出错`, error);
// 继续处理因为有可能API临时错误但提交仍需审查
}
// 提取commit信息
const commitInfo = {
sha: commitSha,
message: body.commit.message || '',
added: body.commit.added || [],
removed: body.commit.removed || [],
modified: body.commit.modified || []
};
logger.info(`收到提交状态更新事件`, {
owner,
repo: repoName,
commitSha,
state: body.state,
relatedPR: relatedPR?.number || 'unknown',
added: commitInfo.added.length,
modified: commitInfo.modified.length,
removed: commitInfo.removed.length
});
// 如果没有文件变更信息,则忽略
if (commitInfo.added.length === 0 && commitInfo.modified.length === 0 && commitInfo.removed.length === 0) {
logger.warn('提交没有文件变更信息,忽略审查', { commitSha });
return c.json({ status: 'ignored', message: '提交没有文件变更信息' }, 200);
}
// 开始异步审查流程传入关联的PR信息
reviewCommit(owner, repoName, commitSha, commitInfo, relatedPR).catch(error => {
logger.error(`审查提交 ${owner}/${repoName}@${commitSha} 失败:`, error);
});
// 立即返回以不阻塞Webhook
return c.json({ status: 'accepted', message: '提交代码审查请求已接受' }, 202);
}
/**

View File

@@ -1,5 +1,5 @@
import { Hono } from 'hono';
import { handlePullRequestEvent, handleCommitStatusEvent } from './controllers/review';
import { handleGiteaWebhook } from './controllers/review';
import config from './config';
// 创建Hono应用实例
@@ -12,13 +12,11 @@ app.get('/', (c) => {
return c.json({
status: 'ok',
message: 'AI Code Review 服务运行中',
version: '1.1.0',
version: '2.0.0',
webhookSecurityEnabled: webhookSecretConfigured,
configuration: {
webhookEndpoints: {
pullRequest: '/webhook/gitea/pull_request',
commitStatus: '/webhook/gitea/status',
legacy: '/webhook/gitea (仅支持Pull Request事件)'
unified: '/webhook/gitea (支持Pull Request和Commit Status事件)'
},
signature: webhookSecretConfigured
? '签名验证已启用 (使用X-Gitea-Signature头)'
@@ -27,14 +25,8 @@ app.get('/', (c) => {
});
});
// Gitea webhook路由 - 处理PR事件
app.post('/webhook/gitea/pull_request', handlePullRequestEvent);
// Gitea webhook路由 - 处理提交状态更新事件
app.post('/webhook/gitea/status', handleCommitStatusEvent);
// 向后兼容的路由(将保留一段时间)
app.post('/webhook/gitea', handlePullRequestEvent);
// 统一的Gitea webhook路由 - 处理所有事件类型
app.post('/webhook/gitea', handleGiteaWebhook);
// 启动服务器
const port = config.app.port;