diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index b7dab24..0000000 --- a/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -config.yaml -config.yaml.bak -.idea/ -.git/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index cc99b48..7813b76 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,19 @@ go.work.sum .idea config.yaml.bak +# Java +*.class +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar +hs_err_pid* +target/ +.idea/ +*.iml +*.iws +*.ipr +.DS_Store + diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index be7251a..0000000 --- a/Dockerfile +++ /dev/null @@ -1,41 +0,0 @@ -FROM golang:1.22-alpine AS builder - -WORKDIR /app - -# 复制go.mod和go.sum文件 -COPY go.mod go.sum ./ - -# 下载依赖 -RUN go mod download - -# 复制源代码 -COPY . . - -# 构建应用 -RUN CGO_ENABLED=0 GOOS=linux go build -o /app/ai-code-review - -# 使用轻量级的基础镜像 -FROM alpine:latest - -WORKDIR /app - -# 从构建阶段复制二进制文件 -COPY --from=builder /app/ai-code-review /app/ - -# 复制静态文件目录 -COPY --from=builder /app/static /app/static - -# 创建默认配置文件到临时位置 -RUN echo 'port: 53321\nadmin_token: "token"\n\nauto_disable:\n enabled: true\n max_failures: 3\n reset_after: 30\n\nais: []\n\ngit: []' > /app/config.default.yaml - -# 创建日志目录并设置权限 -RUN mkdir -p /app/logs && chmod 755 /app/logs - -# 暴露端口 -EXPOSE 53321 - -# 设置卷挂载点 -VOLUME ["/app/config.yaml", "/app/logs"] - -# 运行应用,如果配置文件不存在则使用默认配置 -CMD ["sh", "-c", "if [ ! -f /app/config.yaml ]; then cp /app/config.default.yaml /app/config.yaml; fi && /app/ai-code-review"] \ No newline at end of file diff --git a/README.md b/README.md index d49bd72..2fc8e3b 100644 --- a/README.md +++ b/README.md @@ -1,80 +1,152 @@ -# ai-code-review +# AI 代码审查服务 -一个基于AI的代码审查工具,支持多种Git平台和AI模型。 +这是一个基于 AI 的代码审查服务,支持多种 Git 平台和 AI 服务。 ## 功能特点 -- 支持多个Git平台(GitLab、Gitee、Gogs等) -- 支持多种AI模型(OpenAI、Ollama等) -- 支持AI服务负载均衡 -- 提供Web界面进行配置管理 -- 支持自动禁用异常的AI服务 +- 支持多种 Git 平台:GitLab、Gitea +- 支持多种 AI 服务:OpenAI、Ollama +- 自动代码审查 +- 详细的审查报告 +- 可配置的审查规则 +- 支持自定义提示词 ## 快速开始 -1. 下载并编译项目: +### 环境要求 + +- JDK 17+ +- Maven 3.8+ +- Git 平台(GitLab/Gitea) +- AI 服务(OpenAI/Ollama) + +### 配置 + +1. 复制配置文件模板: + ```bash -git clone https://github.com/your-username/ai-code-review.git -cd ai-code-review -go build +cp src/main/resources/application.yml.template src/main/resources/application.yml ``` -2. 配置config.yaml: +2. 编辑配置文件: + ```yaml -port: 53321 -admin_token: "your-admin-token" # 管理页面访问令牌 -ais: - - name: "your-ai" - type: "ollama" # 或 "openai" - api_key: "" - url: "http://localhost:11434" # AI服务地址 - model: "your-model" # 使用的模型名称 - temperature: 0 - stream: false - priority: 0 # 添加优先级配置 - system_msg: | - 你是一个代码审查员,你的职责是识别提交代码中的错误、性能问题和需要优化的地方。 - 你还负责提供建设性的反馈,并建议最佳实践来提高代码的整体质量。 +server: + port: 53321 + +logging: + level: + com.codereview: DEBUG + +admin: + token: your-admin-token + +ai: + services: + - type: openai + enabled: true + api-key: your-openai-api-key + model: gpt-3.5-turbo + temperature: 0.7 + system-msg: 你是一个代码审查助手 + - type: ollama + enabled: false + url: http://localhost:11434 + model: codellama + temperature: 0.7 + system-msg: 你是一个代码审查助手 - 在审查代码时: - - 审查代码变更(差异)并提供反馈 - - 仔细检查是否真的存在错误或需要优化的空间,突出显示它们 - - 不要突出显示小问题和细节 - - 如果有多个评论,请使用要点符号 - - 你不需要解释代码的功能 - - 请使用中文给出反馈 - - 如果你认为不需要优化或修改,请只回复 666 - weight: 1 - enabled: true - auto_disable: true # 使用自己的自动禁用配置 - max_failures: 5 # 覆盖全局配置 - reset_after: 60 # 覆盖全局配置 git: - - name: "your-git" - type: "gitlab" # 或 "gitee", "gogs" - token: "your-git-token" - webhook_secret: "your-webhook-secret" - api_base: "your-git-api-base" - signature_header: X-Gitlab-Token - event_header: X-Gitlab-Event + platforms: + - name: gitlab + type: gitlab + api-base: https://gitlab.com/api/v4 + token: your-gitlab-token + webhook-secret: your-webhook-secret + - name: gitea + type: gitea + api-base: https://gitea.example.com/api/v1 + token: your-gitea-token + webhook-secret: your-webhook-secret ``` -3. 运行服务: +### 构建 + ```bash -./ai-code-review +mvn clean package ``` -4. 访问管理界面: -- 打开浏览器访问 `http://localhost:53321` -- 使用配置文件中的admin_token登录 -- 在Web界面中管理AI和Git平台配置 -5. 配置Git平台: -- 在你的Git平台中添加Webhook -- Webhook URL设置为: `http://your-server:53321/api/webhook/git-name` -- 设置Webhook密钥与配置文件中的webhook_secret一致 +### 运行 -## 注意事项 +```bash +java -jar target/ai-code-review.jar +``` -- 请确保AI服务和Git平台的API地址可以正常访问 -- 妥善保管各类密钥和Token -- 建议在生产环境中使用HTTPS \ No newline at end of file +## 使用指南 + +### 配置 Git 平台 Webhook + +1. GitLab: + - 进入项目设置 -> Webhooks + - URL: `http://your-server:53321/webhook/gitlab` + - Secret Token: 配置文件中设置的 webhook-secret + - 触发事件: Push events + +2. Gitea: + - 进入仓库设置 -> Webhooks + - URL: `http://your-server:53321/webhook/gitea` + - Secret: 配置文件中设置的 webhook-secret + - 触发事件: Push events + +### 访问管理界面 + +- URL: `http://your-server:53321/admin` +- 使用配置文件中设置的 admin-token 进行认证 + +## 开发指南 + +### 项目结构 + +``` +src/main/java/com/codereview/ +├── config/ # 配置类 +├── controller/ # 控制器 +├── service/ # 服务接口 +├── service/impl/ # 服务实现 +└── util/ # 工具类 +``` + +### 添加新的 Git 平台支持 + +1. 创建新的 WebhookEvent 实现类 +2. 在 WebhookController 中添加处理逻辑 +3. 更新配置文件结构 + +### 添加新的 AI 服务支持 + +1. 创建新的 AIService 实现类 +2. 在配置文件中添加服务配置 +3. 更新 AIServiceFactory + +## 常见问题 + +1. Webhook 验证失败 + - 检查 webhook-secret 配置 + - 确认请求头中的签名 + +2. AI 服务调用失败 + - 检查 API 密钥配置 + - 确认服务是否可用 + - 查看日志获取详细信息 + +## 贡献指南 + +1. Fork 项目 +2. 创建特性分支 +3. 提交更改 +4. 推送到分支 +5. 创建 Pull Request + +## 许可证 + +MIT License \ No newline at end of file diff --git a/config/config.go b/config/config.go deleted file mode 100644 index c13881b..0000000 --- a/config/config.go +++ /dev/null @@ -1,360 +0,0 @@ -package config - -import ( - "fmt" - "log" - "math/rand" - "os" - "sort" - "sync" - - "github.com/spf13/viper" - "gopkg.in/yaml.v3" -) - -var ( - cfg *Config - once sync.Once - mu sync.RWMutex - - aiBalancer *AIBalancer - balancerMu sync.RWMutex -) - -type Config struct { - Port int `mapstructure:"port"` - AdminToken string `mapstructure:"admin_token"` // 管理令牌 - AIs []AIConfig `mapstructure:"ais"` - Git []GitConfig `mapstructure:"git"` - AutoDisableConfig `mapstructure:"auto_disable"` // 全局自动禁用配置 -} - -// AutoDisableConfig 自动禁用配置 -type AutoDisableConfig struct { - Enabled bool `mapstructure:"enabled"` // 是否启用自动禁用 - MaxFailures int `mapstructure:"max_failures"` // 最大失败次数 - ResetAfter int `mapstructure:"reset_after"` // 重置时间(分钟) -} - -type AIConfig struct { - Name string `mapstructure:"name"` - Type string `mapstructure:"type"` // "ollama" 或 "openai" - APIKey string `mapstructure:"api_key"` - APIBase string `mapstructure:"url"` - Model string `mapstructure:"model"` - SystemMsg string `mapstructure:"system_msg"` // 系统提示词 - Temperature float64 `mapstructure:"temperature"` // 温度 - Stream bool `mapstructure:"stream"` // 是否使用流式响应 - Weight int `mapstructure:"weight"` - Priority int `mapstructure:"priority"` // 优先级,数字越大优先级越高 - Enabled bool `mapstructure:"enabled"` // 是否启用 - AutoDisable bool `mapstructure:"auto_disable"` // 是否启用自动禁用(覆盖全局配置) - MaxFailures *int `mapstructure:"max_failures"` // 最大失败次数(覆盖全局配置) - ResetAfter *int `mapstructure:"reset_after"` // 重置时间(覆盖全局配置) -} - -type GitConfig struct { - Name string `mapstructure:"name"` // 平台名称 - Type string `mapstructure:"type"` // 平台类型 - Token string `mapstructure:"token"` // 访问令牌 - Username string `mapstructure:"username"` // 用户名(用于基本认证) - Password string `mapstructure:"password"` // 密码(用于基本认证) - SudoUser string `mapstructure:"sudo_user"` // sudo 用户 - TOTP string `mapstructure:"totp"` // TOTP 令牌 - Secret string `mapstructure:"webhook_secret"` // webhook 密钥 - APIBase string `mapstructure:"api_base"` // API 基础 URL - SignatureHeader string `mapstructure:"signature_header"` // webhook 签名的 header 名称 - EventHeader string `mapstructure:"event_header"` // webhook 事件类型的 header 名称 -} - -// 添加一个新的结构体用于负载均衡 -type AIBalancer struct { - ais []AIConfig - current int - mu sync.Mutex -} - -func init() { - // 设置默认值 - viper.SetDefault("port", 53321) - viper.SetDefault("admin_token", "token") - viper.SetDefault("ais", []interface{}{}) - viper.SetDefault("git", []interface{}{}) - viper.SetDefault("auto_disable", map[string]interface{}{ - "enabled": true, - "max_failures": 3, - "reset_after": 30, - }) -} - -func Load() (*Config, error) { - var err error - once.Do(func() { - viper.SetConfigName("config") - viper.SetConfigType("yaml") - viper.AddConfigPath(".") - - if err = viper.ReadInConfig(); err != nil { - return - } - - cfg = &Config{} - if err = viper.Unmarshal(cfg); err != nil { - return - } - - // 添加日志输出以便调试 - log.Printf("已加载配置: %+v", cfg) - log.Printf("使用的配置文件: %s", viper.ConfigFileUsed()) - }) - - if err != nil { - return nil, fmt.Errorf("加载配置失败: %w", err) - } - - // 确保配置不为空 - if cfg == nil { - return nil, fmt.Errorf("配置加载失败: 配置为空") - } - - return cfg, nil -} - -func GetConfig() *Config { - if cfg == nil { - panic("配置未初始化") - } - return cfg -} - -// Save 保存配置到文件 -func Save(newConfig *Config) error { - mu.Lock() - defer mu.Unlock() - - // 创建备份 - backupFile := "config.yaml.bak" - if _, err := os.Stat("config.yaml"); err == nil { - if err := os.Rename("config.yaml", backupFile); err != nil { - return fmt.Errorf("backup config failed: %w", err) - } - } - // 使用自定义的 YAML 编码器 - file, err := os.Create("config.yaml") - if err != nil { - return fmt.Errorf("创建配置文件失败: %w", err) - } - yamlEncoder := yaml.NewEncoder(file) - yamlEncoder.SetIndent(2) // 设置缩进 - - // 将配置转换为 map 并保存 - configMap := newConfig.ToMap() - if err := yamlEncoder.Encode(configMap); err != nil { - // 如果保存失败,恢复备份 - if _, err := os.Stat(backupFile); err == nil { - err := os.Rename(backupFile, "config.yaml") - if err != nil { - return err - } - } - return fmt.Errorf("save config failed: %w", err) - } - - // 更新内存中的配置 - cfg = newConfig - - return nil -} - -// ToMap 将配置转换为 map -func (c *Config) ToMap() map[string]interface{} { - ais := make([]map[string]interface{}, len(c.AIs)) - for i, ai := range c.AIs { - ais[i] = map[string]interface{}{ - "name": ai.Name, - "type": ai.Type, - "api_key": ai.APIKey, - "url": ai.APIBase, - "model": ai.Model, - "system_msg": ai.SystemMsg, - "temperature": ai.Temperature, - "stream": ai.Stream, - "weight": ai.Weight, - "priority": ai.Priority, - "enabled": ai.Enabled, - "auto_disable": ai.AutoDisable, - "max_failures": ai.MaxFailures, - "reset_after": ai.ResetAfter, - } - } - - // 修改 Git 平台配置的转换逻辑 - platforms := make([]map[string]interface{}, len(c.Git)) - for i, platform := range c.Git { - platforms[i] = map[string]interface{}{ - "name": platform.Name, - "type": platform.Type, - "token": platform.Token, - "username": platform.Username, - "password": platform.Password, - "sudo_user": platform.SudoUser, - "totp": platform.TOTP, - "webhook_secret": platform.Secret, - "api_base": platform.APIBase, - "signature_header": platform.SignatureHeader, - "event_header": platform.EventHeader, - } - } - - return map[string]interface{}{ - "port": c.Port, - "admin_token": c.AdminToken, - "ais": ais, - "git": platforms, - "auto_disable": map[string]interface{}{ - "enabled": c.AutoDisableConfig.Enabled, - "max_failures": c.AutoDisableConfig.MaxFailures, - "reset_after": c.AutoDisableConfig.ResetAfter, - }, - } -} - -// ToMapHtml 将配置转换为 map 页面展示 -func (c *Config) ToMapHtml() map[string]interface{} { - ais := make([]map[string]interface{}, len(c.AIs)) - for i, ai := range c.AIs { - ais[i] = map[string]interface{}{ - "name": ai.Name, - "type": ai.Type, - "api_key": ai.APIKey, - "url": ai.APIBase, - "model": ai.Model, - "system_msg": ai.SystemMsg, - "temperature": ai.Temperature, - "stream": ai.Stream, - "weight": ai.Weight, - "priority": ai.Priority, - "enabled": ai.Enabled, - "auto_disable": ai.AutoDisable, - "max_failures": ai.MaxFailures, - "reset_after": ai.ResetAfter, - } - } - - // 修改 Git 平台配置的转换逻辑 - platforms := make([]map[string]interface{}, len(c.Git)) - for i, platform := range c.Git { - platforms[i] = map[string]interface{}{ - "name": platform.Name, - "type": platform.Type, - "token": platform.Token, - "username": platform.Username, - "webhook_secret": platform.Secret, - "api_base": platform.APIBase, - "signature_header": platform.SignatureHeader, - "event_header": platform.EventHeader, - } - } - - return map[string]interface{}{ - "ais": ais, - "git": platforms, - "auto_disable": map[string]interface{}{ - "enabled": c.AutoDisableConfig.Enabled, - "max_failures": c.AutoDisableConfig.MaxFailures, - "reset_after": c.AutoDisableConfig.ResetAfter, - }, - } -} - -func LoadConfig(configFile string) error { - log.Printf("加载配置文件: file=%s", configFile) - - viper.SetConfigFile(configFile) - if err := viper.ReadInConfig(); err != nil { - log.Printf("读取配置文件失败: file=%s, error=%v", configFile, err) - return fmt.Errorf("读取配置文件失败: %w", err) - } - - mu.Lock() - defer mu.Unlock() - - if err := viper.Unmarshal(&cfg); err != nil { - log.Printf("解析配置文件失败: file=%s, error=%v", configFile, err) - return fmt.Errorf("解析配置文件失败: %w", err) - } - - log.Printf("配置文件加载成功: file=%s", configFile) - return nil -} - -// 添加新的方法 -func NewAIBalancer(ais []AIConfig) *AIBalancer { - // 过滤出已启用的 AI - enabledAIs := make([]AIConfig, 0) - for _, ai := range ais { - if ai.Enabled { - enabledAIs = append(enabledAIs, ai) - } - } - - // 按优先级排序(从高到低) - sort.Slice(enabledAIs, func(i, j int) bool { - return enabledAIs[i].Priority > enabledAIs[j].Priority - }) - - return &AIBalancer{ - ais: enabledAIs, - current: 0, - } -} - -// 获取下一个可用的 AI 配置 -func (b *AIBalancer) Next() (*AIConfig, error) { - b.mu.Lock() - defer b.mu.Unlock() - - if len(b.ais) == 0 { - return nil, fmt.Errorf("没有可用的 AI 配置") - } - - // 获取最高优先级 - highestPriority := b.ais[0].Priority - - // 收集具有最高优先级的 AI - highPriorityAIs := make([]AIConfig, 0) - totalWeight := 0 - for _, ai := range b.ais { - if ai.Priority == highestPriority { - highPriorityAIs = append(highPriorityAIs, ai) - totalWeight += ai.Weight - } - } - - // 在最高优先级的 AI 中按权重选择 - target := rand.Intn(totalWeight) - currentWeight := 0 - for i, ai := range highPriorityAIs { - currentWeight += ai.Weight - if currentWeight > target { - return &highPriorityAIs[i], nil - } - } - - // 如果没有选中任何一个(不应该发生),返回第一个最高优先级的 AI - return &highPriorityAIs[0], nil -} - -// ResetAIBalancer 重置 AI 负载均衡器 -func ResetAIBalancer(ais []AIConfig) { - balancerMu.Lock() - defer balancerMu.Unlock() - aiBalancer = NewAIBalancer(ais) -} - -// GetAIBalancer 获取当前的 AI 负载均衡器 -func GetAIBalancer() *AIBalancer { - balancerMu.RLock() - defer balancerMu.RUnlock() - return aiBalancer -} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 606bcf7..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: '3.8' - -services: - ai-code-review: -# image: ai-code-review:lasted - build: - context: . - dockerfile: Dockerfile - container_name: ai-code-review - ports: - - "53321:53321" -# volumes: -# # 如果需要使用自定义配置文件,取消下面的注释并修改路径 -# - ./config.yaml:/app/config.yaml - restart: unless-stopped - environment: - - TZ=Asia/Shanghai \ No newline at end of file diff --git a/go.mod b/go.mod deleted file mode 100644 index d2389fc..0000000 --- a/go.mod +++ /dev/null @@ -1,55 +0,0 @@ -module code-review - -go 1.22.0 - -toolchain go1.23.6 - -require ( - github.com/gin-gonic/gin v1.10.0 - github.com/spf13/viper v1.19.0 -) - -require ( - github.com/bytedance/sonic v1.12.8 // indirect - github.com/bytedance/sonic/loader v0.2.3 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/cloudwego/base64x v0.1.5 // indirect - github.com/cloudwego/iasm v0.2.0 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/gin-contrib/sse v1.0.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.24.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/magiconair/properties v1.8.9 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.12.0 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/spf13/pflag v1.0.6 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.12 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/arch v0.14.0 // indirect - golang.org/x/crypto v0.33.0 // indirect - golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index b9411c3..0000000 --- a/go.sum +++ /dev/null @@ -1,166 +0,0 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= -github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= -github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs= -github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= -github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= -github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= -github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= -github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= -github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= -github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= -github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= -github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= -github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= -github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= -github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= -github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/leodido/go-urn v1.3.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= -github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= -github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= -github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= -github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= -github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= -golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/handlers/config.go b/handlers/config.go deleted file mode 100644 index 825ac7f..0000000 --- a/handlers/config.go +++ /dev/null @@ -1,160 +0,0 @@ -package handlers - -import ( - "net/http" - - "code-review/config" - "code-review/utils" - - "log" - - "strings" - - "github.com/gin-gonic/gin" -) - -type ConfigHandler struct { - cfg *config.Config -} - -func NewConfigHandler(cfg *config.Config) *ConfigHandler { - return &ConfigHandler{ - cfg: cfg, - } -} - -// GetConfig 获取当前配置 -func (h *ConfigHandler) GetConfig(c *gin.Context) { - // 使用 ToMap 方法转换配置 - configMap := h.cfg.ToMapHtml() - c.JSON(http.StatusOK, configMap) -} - -// UpdateConfig 更新配置 -func (h *ConfigHandler) UpdateConfig(c *gin.Context) { - // 首先解析为 map - var configMap map[string]interface{} - if err := c.ShouldBindJSON(&configMap); err != nil { - log.Printf("解析配置 JSON 失败: %v", err) - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - log.Printf("开始更新配置") - - // 使用当前配置作为基础 - newConfig := *h.cfg - - // 处理自动禁用配置 - if autoDisable, ok := configMap["auto_disable"].(map[string]interface{}); ok { - log.Printf("更新自动禁用配置") - if enabled, exists := autoDisable["enabled"].(bool); exists { - newConfig.AutoDisableConfig.Enabled = enabled - log.Printf("自动禁用功能状态设置为: %v", enabled) - } - if maxFailures, ok := autoDisable["max_failures"].(float64); ok { - newConfig.AutoDisableConfig.MaxFailures = int(maxFailures) - } - if resetAfter, ok := autoDisable["reset_after"].(float64); ok { - newConfig.AutoDisableConfig.ResetAfter = int(resetAfter) - } - } - - // 处理 AI 配置 - newConfig.AIs = make([]config.AIConfig, 0) - for key, value := range configMap { - if strings.HasPrefix(key, "ais[") { - if aiMap, ok := value.(map[string]interface{}); ok { - aiConfig := config.AIConfig{ - Name: utils.GetString(aiMap, "name"), - Type: utils.GetString(aiMap, "type"), - APIKey: utils.GetString(aiMap, "api_key"), - APIBase: utils.GetString(aiMap, "url"), - Model: utils.GetString(aiMap, "model"), - SystemMsg: utils.GetString(aiMap, "system_msg"), - Temperature: utils.GetFloat64(aiMap, "temperature"), - Stream: utils.GetBool(aiMap, "stream"), - Weight: utils.GetInt(aiMap, "weight"), - Priority: utils.GetInt(aiMap, "priority"), - Enabled: utils.GetBool(aiMap, "enabled"), - AutoDisable: utils.GetBool(aiMap, "auto_disable"), - MaxFailures: utils.GetIntPtr(aiMap, "max_failures"), - ResetAfter: utils.GetIntPtr(aiMap, "reset_after"), - } - log.Printf("添加 AI 配置: %s, 类型: %s", aiConfig.Name, aiConfig.Type) - newConfig.AIs = append(newConfig.AIs, aiConfig) - } - } - } - - // 处理 Git 平台配置 - newConfig.Git = make([]config.GitConfig, 0) - for key, value := range configMap { - if strings.HasPrefix(key, "git[") { - if p, ok := value.(map[string]interface{}); ok { - platformType := utils.GetString(p, "type") - platformConfig := config.GitConfig{ - Name: utils.GetString(p, "name"), - Type: platformType, - Token: utils.GetString(p, "token"), - Secret: utils.GetString(p, "webhook_secret"), - APIBase: utils.GetString(p, "api_base"), - } - - // 根据平台类型设置对应的 header - switch platformType { - case "gitlab": - platformConfig.SignatureHeader = "X-Gitlab-Token" - platformConfig.EventHeader = "X-Gitlab-Event" - case "gogs": - platformConfig.SignatureHeader = "X-Gogs-Signature" - platformConfig.EventHeader = "X-Gogs-Event" - case "github": - platformConfig.SignatureHeader = "X-Hub-Signature" - platformConfig.EventHeader = "X-GitHub-Event" - case "gitee": - platformConfig.SignatureHeader = "X-Gitee-Token" - platformConfig.EventHeader = "X-Gitee-Event" - default: - // 如果前端传入了自定义的 header,则使用前端传入的值 - platformConfig.SignatureHeader = utils.GetString(p, "signature_header") - platformConfig.EventHeader = utils.GetString(p, "event_header") - } - - log.Printf("添加 Git 平台配置: %s, 类型: %s, SignatureHeader: %s, EventHeader: %s", - platformConfig.Name, platformConfig.Type, - platformConfig.SignatureHeader, platformConfig.EventHeader) - newConfig.Git = append(newConfig.Git, platformConfig) - } - } - } - - if err := config.Save(&newConfig); err != nil { - log.Printf("保存配置失败: %v", err) - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - // 更新运行时配置 - h.cfg = &newConfig - - // 重新初始化 AI 负载均衡器 - config.ResetAIBalancer(newConfig.AIs) - - log.Printf("配置更新成功") - c.JSON(http.StatusOK, gin.H{"message": "配置更新成功"}) -} - -func AuthRequired() gin.HandlerFunc { - return func(c *gin.Context) { - token := c.GetHeader("X-Admin-Token") - cfg := config.GetConfig() - - if token == "" || token != cfg.AdminToken { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "未授权访问"}) - return - } - - c.Next() - } -} diff --git a/handlers/webhook.go b/handlers/webhook.go deleted file mode 100644 index 44754a9..0000000 --- a/handlers/webhook.go +++ /dev/null @@ -1,161 +0,0 @@ -package handlers - -import ( - "bytes" - "code-review/config" - "code-review/services" - "code-review/services/platforms" - "crypto/hmac" - "crypto/sha256" - "encoding/hex" - "fmt" - "io/ioutil" - "log" - - "github.com/gin-gonic/gin" -) - -type WebhookHandler struct { - reviewService *services.ReviewService -} - -func NewWebhookHandler(reviewService *services.ReviewService) *WebhookHandler { - return &WebhookHandler{ - reviewService: reviewService, - } -} - -func (h *WebhookHandler) HandleWebhook(c *gin.Context) { - platform := c.Param("platform") - log.Printf("收到 webhook 请求: platform=%s", platform) - - // 验证 webhook 签名 - if !h.verifySignature(c, platform) { - log.Printf("webhook 签名验证失败: platform=%s", platform) - c.JSON(400, gin.H{"error": "无效的签名"}) - return - } - - // 解析请求体 - event, err := h.parseWebhookEvent(c, platform) - if err != nil { - log.Printf("解析 webhook 事件失败: platform=%s, error=%v", platform, err) - c.JSON(400, gin.H{"error": err.Error()}) - return - } - - // 启动代码审查 - if err := h.reviewService.Review(event); err != nil { - log.Printf("代码审查失败: platform=%s, error=%v", platform, err) - c.JSON(500, gin.H{"error": err.Error()}) - return - } - - log.Printf("代码审查完成: platform=%s", platform) - c.JSON(200, gin.H{"message": "success"}) -} - -func (h *WebhookHandler) verifySignature(c *gin.Context, platform string) bool { - body, err := c.GetRawData() - if err != nil { - return false - } - c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) - - cfg := config.GetConfig() - // 查找对应的平台配置 - var gitConfig config.GitConfig - found := false - for _, p := range cfg.Git { - if p.Name == platform { - gitConfig = p - found = true - break - } - } - if !found { - return false - } - - // 获取签名 - signature := c.GetHeader(gitConfig.SignatureHeader) - if signature == "" { - return false - } - - mac := hmac.New(sha256.New, []byte(gitConfig.Secret)) - mac.Write(body) - expectedMAC := hex.EncodeToString(mac.Sum(nil)) - - return hmac.Equal([]byte(signature), []byte(expectedMAC)) || hmac.Equal([]byte(signature), []byte(gitConfig.Secret)) -} - -func (h *WebhookHandler) parseWebhookEvent(c *gin.Context, platform string) (services.WebhookEvent, error) { - cfg := config.GetConfig() - // 查找对应的平台配置 - var platformConfig config.GitConfig - found := false - for _, p := range cfg.Git { - if p.Name == platform { - platformConfig = p - found = true - break - } - } - if !found { - return nil, fmt.Errorf("不支持的平台: %s", platform) - } - - // 获取事件类型 - eventType := c.GetHeader(platformConfig.EventHeader) - if eventType == "" { - return nil, fmt.Errorf("未找到事件类型 header: %s", platformConfig.EventHeader) - } - - // 创建认证配置 - auth := &platforms.AuthConfig{ - Token: platformConfig.Token, - Username: platformConfig.Username, - Password: platformConfig.Password, - SudoUser: platformConfig.SudoUser, - TOTP: platformConfig.TOTP, - UseBasicAuth: platformConfig.Username != "" && platformConfig.Password != "", - UseSudoHeader: platformConfig.SudoUser != "", - UseSudoParam: platformConfig.SudoUser != "", - UseTOTPHeader: platformConfig.TOTP != "", - } - - var event services.WebhookEvent - switch platformConfig.Type { - case "gitea": - event = platforms.NewGiteaEvent( - platformConfig.APIBase, - auth, - eventType, - ) - // case "gitee": - // event = platforms.NewGiteeEvent( - // platformConfig.APIBase, - // auth, - // ) - case "gitlab": - event = platforms.NewGitlabEvent( - platformConfig.APIBase, - auth, - eventType, - ) - default: - return nil, fmt.Errorf("不支持的平台: %s", platform) - } - - // 只处理 push 和 pull_request 事件 - //if eventType != "push" && eventType != "pull_request" { - // return nil, fmt.Errorf("不支持的事件类型: %s", eventType) - //} - - if err := c.ShouldBindJSON(event); err != nil { - return nil, err - } - - return event, nil -} diff --git a/main.go b/main.go deleted file mode 100644 index 3b75329..0000000 --- a/main.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "code-review/config" - "code-review/handlers" - "code-review/services" - "code-review/services/ai" - "code-review/utils" - "fmt" - "log" - - "github.com/gin-gonic/gin" -) - -func main() { - // 初始化日志系统 - if err := utils.InitLogger(); err != nil { - log.Fatalf("初始化日志系统失败: %v", err) - } - defer utils.CloseLogger() - - if err := config.LoadConfig("config.yaml"); err != nil { - log.Fatalf("加载配置失败: %v", err) - } - cfg := config.GetConfig() - - // 检查是否有可用的 AI 配置 - if len(cfg.AIs) == 0 { - log.Fatalf("未找到有效的 AI 配置") - } - - // 初始化 AI 负载均衡器 - config.ResetAIBalancer(cfg.AIs) - - // 获取一个 AI 配置 - aiConfig, err := config.GetAIBalancer().Next() - if err != nil { - log.Fatalf("获取 AI 配置失败: %v", err) - } - - // 创建 AI 客户端和审查器 - client := ai.NewClient( - aiConfig.APIBase, - aiConfig.APIKey, - aiConfig.Model, - aiConfig.Type, - aiConfig.Temperature, - ) - reviewer := ai.NewAI(aiConfig.Model, aiConfig.SystemMsg, client) - - // 创建服务和处理器 - reviewService := services.NewReviewService(reviewer) - webhookHandler := handlers.NewWebhookHandler(reviewService) - configHandler := handlers.NewConfigHandler(cfg) - - // 初始化路由 - r := gin.Default() - r.Static("/static", "./static") - r.StaticFile("/", "./static/index.html") - - api := r.Group("/api") - configAPI := api.Group("/config") - configAPI.Use(handlers.AuthRequired()) - { - configAPI.GET("", configHandler.GetConfig) - configAPI.POST("", configHandler.UpdateConfig) - } - api.POST("/webhook/:platform", webhookHandler.HandleWebhook) - - log.Printf("服务启动在 :%d", cfg.Port) - if err := r.Run(fmt.Sprintf(":%d", cfg.Port)); err != nil { - log.Fatalf("服务器启动失败: %v", err) - } -} diff --git a/services/ai/ai.go b/services/ai/ai.go deleted file mode 100644 index 09ff055..0000000 --- a/services/ai/ai.go +++ /dev/null @@ -1,84 +0,0 @@ -package ai - -import ( - "code-review/services" - "code-review/services/types" - "fmt" - "log" - "strings" -) - -// AI 实现了 CodeReviewer 和 AIPool 接口 -type AI struct { - model string - systemMsg string - client AIClient -} - -// NewAI 创建新的 AI 实例 -func NewAI(model, systemMsg string, client AIClient) *AI { - return &AI{ - model: model, - systemMsg: systemMsg, - client: client, - } -} - -// GetAI 实现 AIPool 接口 -func (a *AI) GetAI() services.CodeReviewer { - return a -} - -// Review 实现 CodeReviewer 接口 -func (a *AI) Review(changes *types.CodeChanges) (*types.ReviewResult, error) { - log.Printf("AI 开始审查代码: model=%s", a.model) - - // 构建审查请求 - prompt := a.buildPrompt(changes) - - // 调用 AI API - response, err := a.client.Chat(a.systemMsg, prompt) - if err != nil { - log.Printf("AI API 调用失败: model=%s, error=%v", a.model, err) - return nil, fmt.Errorf("AI API 调用失败: %w", err) - } - - // 解析审查结果 - result, err := a.parseResponse(response) - if err != nil { - log.Printf("解析 AI 响应失败: model=%s, error=%v", a.model, err) - return nil, fmt.Errorf("解析 AI 响应失败: %w", err) - } - - log.Printf("AI 代码审查完成: model=%s", a.model) - return result, nil -} - -// buildPrompt 构建代码审查的提示词 -func (a *AI) buildPrompt(changes *types.CodeChanges) string { - var prompt strings.Builder - - prompt.WriteString("请审查以下代码变更:\n\n") - - for _, file := range changes.Files { - prompt.WriteString(fmt.Sprintf("文件: %s\n", file.Path)) - prompt.WriteString("diff:\n") - prompt.WriteString(file.Content) - prompt.WriteString("\n---\n\n") - } - - return prompt.String() -} - -// parseResponse 解析 AI 的响应 -func (a *AI) parseResponse(response string) (*types.ReviewResult, error) { - return &types.ReviewResult{ - Comments: []types.Comment{ - { - Path: "全局", - Content: response, - }, - }, - Summary: "代码审查完成", - }, nil -} diff --git a/services/ai/client.go b/services/ai/client.go deleted file mode 100644 index cf6b99b..0000000 --- a/services/ai/client.go +++ /dev/null @@ -1,185 +0,0 @@ -package ai - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "regexp" - "strings" -) - -// AIClient 定义了与 AI 服务交互的接口 -type AIClient interface { - Chat(systemMsg, prompt string) (string, error) -} - -// Client 实现了 AIClient 接口 -type Client struct { - apiBase string - apiKey string - model string - aiType string - stream bool - temperature float64 - client *http.Client -} - -// NewClient 创建新的 AI 客户端 -func NewClient(apiBase, apiKey, model, aiType string, temperature float64) *Client { - return &Client{ - apiBase: apiBase, - apiKey: apiKey, - model: model, - aiType: aiType, - temperature: temperature, - client: &http.Client{}, - } -} - -// Chat 发送聊天请求到 AI 服务 -func (c *Client) Chat(systemMsg, prompt string) (string, error) { - // 根据配置的类型判断使用哪个服务 - var response string - var err error - - log.Printf("AImodel=%s, prompt=%s", c.model, prompt) - if c.aiType == "ollama" { - response, err = c.ollamaChat(systemMsg, prompt) - } else { - response, err = c.openAIChat(systemMsg, prompt) - } - - if err != nil { - log.Printf("AI 聊天请求失败: error=%v", err) - return "", fmt.Errorf("AI 聊天请求失败: %w", err) - } - - return response, nil -} - -// ollamaChat 发送请求到 Ollama API -func (c *Client) ollamaChat(systemMsg, prompt string) (string, error) { - // 组合系统提示词和用户提示词 - fullPrompt := fmt.Sprintf("%s\n\n%s", systemMsg, prompt) - - reqBody := map[string]interface{}{ - "model": c.model, - "prompt": fullPrompt, - "stream": c.stream, - "temperature": c.temperature, - } - - jsonData, err := json.Marshal(reqBody) - if err != nil { - return "", fmt.Errorf("序列化请求失败: %w", err) - } - - req, err := http.NewRequest("POST", c.apiBase+"/api/generate", bytes.NewBuffer(jsonData)) - if err != nil { - return "", fmt.Errorf("创建请求失败: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - - resp, err := c.client.Do(req) - if err != nil { - return "", fmt.Errorf("发送请求失败: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("API 请求失败: status=%d, body=%s", resp.StatusCode, string(body)) - } - - var result struct { - Response string `json:"response"` - Done bool `json:"done"` - } - - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - body, _ := io.ReadAll(resp.Body) - log.Printf("响应内容: %s", string(body)) - return "", fmt.Errorf("解析响应失败: %w", err) - } - - if !result.Done { - return "", fmt.Errorf("AI 响应未完成") - } - - pattern := "(?s)(.*?)" - - reg := regexp.MustCompile(pattern) - matches := reg.ReplaceAllString(result.Response, "") - - return strings.TrimSpace(matches), nil -} - -// openAIChat 发送请求到 OpenAI API -func (c *Client) openAIChat(systemMsg, prompt string) (string, error) { - reqBody := map[string]interface{}{ - "model": c.model, - "messages": []map[string]string{ - { - "role": "system", - "content": systemMsg, - }, - { - "role": "user", - "content": prompt, - }, - }, - "stream": false, - "temperature": c.temperature, - } - - jsonData, err := json.Marshal(reqBody) - if err != nil { - return "", fmt.Errorf("序列化请求失败: %w", err) - } - - req, err := http.NewRequest("POST", c.apiBase+"/v1/chat/completions", bytes.NewBuffer(jsonData)) - if err != nil { - return "", fmt.Errorf("创建请求失败: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+c.apiKey) - - resp, err := c.client.Do(req) - if err != nil { - return "", fmt.Errorf("发送请求失败: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("API 请求失败: status=%d, body=%s", resp.StatusCode, string(body)) - } - - var result struct { - Choices []struct { - Message struct { - Content string `json:"content"` - } `json:"message"` - } `json:"choices"` - } - - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return "", fmt.Errorf("解析响应失败: %w", err) - } - - if len(result.Choices) == 0 { - return "", fmt.Errorf("AI 响应为空") - } - - pattern := "(?s)(.*?)" - - reg := regexp.MustCompile(pattern) - matches := reg.ReplaceAllString(result.Choices[0].Message.Content, "") - - return strings.TrimSpace(matches), nil -} diff --git a/services/interfaces.go b/services/interfaces.go deleted file mode 100644 index 5b566b6..0000000 --- a/services/interfaces.go +++ /dev/null @@ -1,96 +0,0 @@ -package services - -import ( - "code-review/services/types" - "fmt" - "log" -) - -// CodeReviewer 定义代码审查的接口 -type CodeReviewer interface { - Review(changes *types.CodeChanges) (*types.ReviewResult, error) -} - -// WebhookEvent 定义 webhook 事件的接口 -type WebhookEvent interface { - ExtractChanges() (*types.CodeChanges, error) - PostComments(result *types.ReviewResult) error - GetPlatform() string -} - -// AIPool 定义 AI 池接口 -type AIPool interface { - GetAI() CodeReviewer -} - -// ReviewService 代码审查服务 -type ReviewService struct { - aiPool AIPool -} - -// NewReviewService 创建代码审查服务 -func NewReviewService(aiPool AIPool) *ReviewService { - return &ReviewService{ - aiPool: aiPool, - } -} - -// GetCodeChanges 从 webhook 事件中提取代码变更 -func (s *ReviewService) GetCodeChanges(event WebhookEvent) (*types.CodeChanges, error) { - return event.ExtractChanges() -} - -// ReviewCode 使用配置的审查器进行代码审查 -func (s *ReviewService) ReviewCode(changes *types.CodeChanges) (*types.ReviewResult, error) { - if s.aiPool == nil { - return nil, fmt.Errorf("未配置 AI 池") - } - ai := s.aiPool.GetAI() - if ai == nil { - log.Printf("没有可用的 AI") - return nil, fmt.Errorf("没有可用的 AI") - } - return ai.Review(changes) -} - -// PostReviewComments 将审查结果发送到代码托管平台 -func (s *ReviewService) PostReviewComments(event WebhookEvent, result *types.ReviewResult) error { - return event.PostComments(result) -} - -// Review 执行代码审查流程 -func (s *ReviewService) Review(event WebhookEvent) error { - log.Printf("开始代码审查: platform=%s", event.GetPlatform()) - - changes, err := event.ExtractChanges() - if err != nil { - log.Printf("提取代码变更失败: error=%v", err) - return fmt.Errorf("提取代码变更失败: %w", err) - } - - // 如果没有变更或者跳过审查,直接返回 - if changes == nil || len(changes.Files) == 0 { - log.Printf("没有需要审查的代码变更") - return nil - } - - ai := s.aiPool.GetAI() - if ai == nil { - log.Printf("没有可用的 AI") - return fmt.Errorf("没有可用的 AI") - } - - result, err := ai.Review(changes) - if err != nil { - log.Printf("AI 代码审查失败: error=%v", err) - return fmt.Errorf("AI 代码审查失败: %w", err) - } - - if err := event.PostComments(result); err != nil { - log.Printf("发送审查结果失败: error=%v", err) - return fmt.Errorf("发送审查结果失败: %w", err) - } - - log.Printf("代码审查完成") - return nil -} diff --git a/services/platforms/client.go b/services/platforms/client.go deleted file mode 100644 index b086282..0000000 --- a/services/platforms/client.go +++ /dev/null @@ -1,232 +0,0 @@ -package platforms - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "log" - "net/http" -) - -type AuthConfig struct { - Token string - Username string - Password string - SudoUser string - TOTP string - UseBasicAuth bool - UseSudoHeader bool - UseSudoParam bool - UseTOTPHeader bool -} - -type httpClient struct { - url string - auth *AuthConfig - client *http.Client -} - -func newHTTPClient(baseURL string, authConfig *AuthConfig) *httpClient { - return &httpClient{ - url: baseURL, - auth: authConfig, - client: &http.Client{}, - } -} - -func (c *httpClient) setAuthHeaders(req *http.Request) { - if c.auth == nil { - return - } - - // 设置 Token 认证 - if c.auth.Token != "" { - req.Header.Set("Authorization", "token "+c.auth.Token) - } -} - -func (c *httpClient) get(path string, result interface{}) error { - url := fmt.Sprintf("%s%s", c.url, path) - log.Printf("发送 GET 请求: url=%s", url) - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Printf("创建 GET 请求失败: url=%s, error=%v", url, err) - return fmt.Errorf("创建请求失败: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - c.setAuthHeaders(req) - - resp, err := c.client.Do(req) - if err != nil { - log.Printf("发送 GET 请求失败: url=%s, error=%v", url, err) - return fmt.Errorf("发送请求失败: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - log.Printf("GET 请求返回错误状态码: url=%s, status=%d, response=%s", url, resp.StatusCode, string(body)) - return fmt.Errorf("请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body)) - } - - if err := json.NewDecoder(resp.Body).Decode(result); err != nil { - log.Printf("解析 GET 响应失败: url=%s, error=%v", url, err) - return fmt.Errorf("解析响应失败: %w", err) - } - - log.Printf("GET 请求成功: url=%s", url) - return nil -} - -func (c *httpClient) post(path string, data interface{}) error { - url := fmt.Sprintf("%s%s", c.url, path) - log.Printf("发送 POST 请求: url=%s", url) - - jsonData, err := json.Marshal(data) - if err != nil { - log.Printf("序列化 POST 数据失败: url=%s, error=%v", url, err) - return fmt.Errorf("序列化请求数据失败: %w", err) - } - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) - if err != nil { - log.Printf("创建 POST 请求失败: url=%s, error=%v", url, err) - return fmt.Errorf("创建请求失败: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - c.setAuthHeaders(req) - - resp, err := c.client.Do(req) - if err != nil { - log.Printf("发送 POST 请求失败: url=%s, error=%v", url, err) - return fmt.Errorf("发送请求失败: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - body, _ := io.ReadAll(resp.Body) - log.Printf("POST 请求返回错误状态码: url=%s, status=%d, response=%s", url, resp.StatusCode, string(body)) - return fmt.Errorf("请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body)) - } - - log.Printf("POST 请求成功: url=%s", url) - return nil -} - -func (c *httpClient) getWithHeaders(path string, result interface{}, headers map[string]string) error { - url := fmt.Sprintf("%s%s", c.url, path) - log.Printf("发送 GET 请求: url=%s", url) - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Printf("创建 GET 请求失败: url=%s, error=%v", url, err) - return fmt.Errorf("创建请求失败: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - for key, value := range headers { - req.Header.Set(key, value) - } - - c.setAuthHeaders(req) - - resp, err := c.client.Do(req) - if err != nil { - log.Printf("发送 GET 请求失败: url=%s, error=%v", url, err) - return fmt.Errorf("发送请求失败: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - log.Printf("GET 请求返回错误状态码: url=%s, status=%d, response=%s", url, resp.StatusCode, string(body)) - return fmt.Errorf("请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body)) - } - - if err := json.NewDecoder(resp.Body).Decode(result); err != nil { - log.Printf("解析 GET 响应失败: url=%s, error=%v", url, err) - return fmt.Errorf("解析响应失败: %w", err) - } - - log.Printf("GET 请求成功: url=%s", url) - return nil -} - -func (c *httpClient) postWithHeaders(path string, data interface{}, headers map[string]string) error { - url := fmt.Sprintf("%s%s", c.url, path) - log.Printf("发送 POST 请求: url=%s", url) - - jsonData, err := json.Marshal(data) - if err != nil { - log.Printf("序列化 POST 数据失败: url=%s, error=%v", url, err) - return fmt.Errorf("序列化请求数据失败: %w", err) - } - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) - if err != nil { - log.Printf("创建 POST 请求失败: url=%s, error=%v", url, err) - return fmt.Errorf("创建请求失败: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - for key, value := range headers { - req.Header.Set(key, value) - } - - c.setAuthHeaders(req) - - resp, err := c.client.Do(req) - if err != nil { - log.Printf("发送 POST 请求失败: url=%s, error=%v", url, err) - return fmt.Errorf("发送请求失败: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - body, _ := io.ReadAll(resp.Body) - log.Printf("POST 请求返回错误状态码: url=%s, status=%d, response=%s", url, resp.StatusCode, string(body)) - return fmt.Errorf("请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body)) - } - - log.Printf("POST 请求成功: url=%s", url) - return nil -} - -func (c *httpClient) getRaw(path string) (string, error) { - url := fmt.Sprintf("%s%s", c.url, path) - log.Printf("发送 GET 请求: url=%s", url) - - req, err := http.NewRequest("GET", url, nil) - if err != nil { - log.Printf("创建 GET 请求失败: url=%s, error=%v", url, err) - return "", fmt.Errorf("创建请求失败: %w", err) - } - - c.setAuthHeaders(req) - - resp, err := c.client.Do(req) - if err != nil { - log.Printf("发送 GET 请求失败: url=%s, error=%v", url, err) - return "", fmt.Errorf("发送请求失败: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - log.Printf("GET 请求返回错误状态码: url=%s, status=%d, response=%s", url, resp.StatusCode, string(body)) - return "", fmt.Errorf("请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body)) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Printf("读取响应失败: url=%s, error=%v", url, err) - return "", fmt.Errorf("读取响应失败: %w", err) - } - - log.Printf("GET 请求成功: url=%s", url) - return string(body), nil -} diff --git a/services/platforms/gitea.go b/services/platforms/gitea.go deleted file mode 100644 index b61e777..0000000 --- a/services/platforms/gitea.go +++ /dev/null @@ -1,332 +0,0 @@ -package platforms - -import ( - "code-review/services/types" - "code-review/utils" - "fmt" - "log" - "strings" -) - -type GiteaEvent struct { - apiBase string - auth *AuthConfig - Event string - client *httpClient - - Secret string `json:"secret"` - Ref string `json:"ref"` - Before string `json:"before"` - After string `json:"after"` - CompareURL string `json:"compare_url"` - Commits []struct { - ID string `json:"sha"` - Message string `json:"commit.message"` - URL string `json:"url"` - HTMLURL string `json:"html_url"` - Created string `json:"created"` - Author struct { - Active bool `json:"active"` - AvatarURL string `json:"avatar_url"` - Created string `json:"created"` - Description string `json:"description"` - Email string `json:"email"` - FollowersCount int `json:"followers_count"` - FollowingCount int `json:"following_count"` - FullName string `json:"full_name"` - HTMLURL string `json:"html_url"` - ID int `json:"id"` - IsAdmin bool `json:"is_admin"` - Language string `json:"language"` - LastLogin string `json:"last_login"` - Location string `json:"location"` - Login string `json:"login"` - LoginName string `json:"login_name"` - ProhibitLogin bool `json:"prohibit_login"` - Restricted bool `json:"restricted"` - SourceID int `json:"source_id"` - Visibility string `json:"visibility"` - Website string `json:"website"` - } `json:"author"` - Committer struct { - Active bool `json:"active"` - AvatarURL string `json:"avatar_url"` - Created string `json:"created"` - Description string `json:"description"` - Email string `json:"email"` - FollowersCount int `json:"followers_count"` - FollowingCount int `json:"following_count"` - FullName string `json:"full_name"` - HTMLURL string `json:"html_url"` - ID int `json:"id"` - IsAdmin bool `json:"is_admin"` - Language string `json:"language"` - LastLogin string `json:"last_login"` - Location string `json:"location"` - Login string `json:"login"` - LoginName string `json:"login_name"` - ProhibitLogin bool `json:"prohibit_login"` - Restricted bool `json:"restricted"` - SourceID int `json:"source_id"` - Visibility string `json:"visibility"` - Website string `json:"website"` - } `json:"committer"` - Commit struct { - Author struct { - Date string `json:"date"` - Email string `json:"email"` - Name string `json:"name"` - } `json:"author"` - Committer struct { - Date string `json:"date"` - Email string `json:"email"` - Name string `json:"name"` - } `json:"committer"` - Message string `json:"message"` - Tree struct { - Created string `json:"created"` - SHA string `json:"sha"` - URL string `json:"url"` - } `json:"tree"` - URL string `json:"url"` - Verification struct { - Payload string `json:"payload"` - Reason string `json:"reason"` - Signature string `json:"signature"` - Signer struct { - Email string `json:"email"` - Name string `json:"name"` - Username string `json:"username"` - } `json:"signer"` - Verified bool `json:"verified"` - } `json:"verification"` - } `json:"commit"` - Files []struct { - Filename string `json:"filename"` - Status string `json:"status"` - } `json:"files"` - Parents []struct { - Created string `json:"created"` - SHA string `json:"sha"` - URL string `json:"url"` - } `json:"parents"` - Stats struct { - Additions int `json:"additions"` - Deletions int `json:"deletions"` - Total int `json:"total"` - } `json:"stats"` - } `json:"commits"` - Repository struct { - ID int `json:"id"` - Name string `json:"name"` - FullName string `json:"full_name"` - Description string `json:"description"` - Private bool `json:"private"` - Fork bool `json:"fork"` - HTMLURL string `json:"html_url"` - SSHURL string `json:"ssh_url"` - CloneURL string `json:"clone_url"` - Website string `json:"website"` - StarsCount int `json:"stars_count"` - ForksCount int `json:"forks_count"` - WatchersCount int `json:"watchers_count"` - OpenIssuesCount int `json:"open_issues_count"` - DefaultBranch string `json:"default_branch"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - Owner struct { - ID int `json:"id"` - Login string `json:"login"` - FullName string `json:"full_name"` - Email string `json:"email"` - AvatarURL string `json:"avatar_url"` - Username string `json:"username"` - } `json:"owner"` - } `json:"repository"` - Pusher struct { - ID int `json:"id"` - Login string `json:"login"` - FullName string `json:"full_name"` - Email string `json:"email"` - AvatarURL string `json:"avatar_url"` - Username string `json:"username"` - } `json:"pusher"` - Sender struct { - ID int `json:"id"` - Login string `json:"login"` - FullName string `json:"full_name"` - Email string `json:"email"` - AvatarURL string `json:"avatar_url"` - Username string `json:"username"` - } `json:"sender"` -} - -// NewGiteaEvent 创建 Gitea 事件 -func NewGiteaEvent(apiBase string, auth *AuthConfig, event string) *GiteaEvent { - return &GiteaEvent{ - apiBase: apiBase, - auth: auth, - Event: event, - } -} - -// 定义 Gitea commit 响应的结构 -type giteaCommitResponse struct { - Diff string `json:"diff"` -} - -func (e *GiteaEvent) ExtractChanges() (*types.CodeChanges, error) { - // 检查是否有提交记录 - if len(e.Commits) == 0 { - log.Printf("没有提交记录,跳过代码审查") - return nil, nil - } - - if e.client == nil { - e.client = newHTTPClient(e.apiBase, e.auth) - log.Printf("初始化 HTTP 客户端: url=%s", e.apiBase) - } - - changes := &types.CodeChanges{ - Repository: e.Repository.FullName, - Branch: e.Ref, - CommitID: e.After, - Files: make([]types.FileChange, 0), - } - - for _, commit := range e.Commits { - // 检查提交信息是否包含跳过标记 - if strings.Contains(commit.Message, "[skip codereview]") { - log.Printf("提交包含跳过标记,跳过所有文件审查: commit=%s", commit.ID) - continue - } - - // 检查是否是合并提交 - if strings.HasPrefix(commit.Message, "Merge remote-tracking branch") || - strings.HasPrefix(commit.Message, "Merge branch") { - log.Printf("跳过合并提交的文件审查: commit=%s", commit.ID) - continue - } - - // 获取 diff 内容 - apiPath := fmt.Sprintf("/api/v1/repos/%s/%s/git/commits/%s.diff", e.Repository.Owner.Login, e.Repository.Name, e.After) - diffContent, err := e.client.getRaw(apiPath) - if err != nil { - log.Printf("获取提交详情失败: commit=%s, error=%v", commit.ID, err) - continue - } - - // 解析 diff 内容 - diffLines := strings.Split(diffContent, "\n") - var currentFile *types.FileChange - var currentContent strings.Builder - - for _, line := range diffLines { - if strings.HasPrefix(line, "diff --git") { - // 保存前一个文件的内容 - if currentFile != nil { - currentFile.Content = currentContent.String() + "```\n" - changes.Files = append(changes.Files, *currentFile) - } - - // 解析文件名 - parts := strings.Split(line, " ") - if len(parts) >= 3 { - filePath := strings.TrimPrefix(parts[2], "b/") - if shouldSkipFile(filePath) { - log.Printf("跳过文件审查: file=%s", filePath) - currentFile = nil - continue - } - - currentFile = &types.FileChange{ - Path: filePath, - Type: utils.ParseFileType("modified"), - } - currentContent.Reset() - currentContent.WriteString(fmt.Sprintf("### 变更说明\n")) - currentContent.WriteString(fmt.Sprintf("提交信息: %s\n\n", commit.Message)) - currentContent.WriteString("### 变更内容\n") - currentContent.WriteString("```diff\n") - } - } else if currentFile != nil { - currentContent.WriteString(line + "\n") - } - } - - // 保存最后一个文件的内容 - if currentFile != nil { - currentFile.Content = currentContent.String() + "```\n" - changes.Files = append(changes.Files, *currentFile) - } - } - - return changes, nil -} - -func (e *GiteaEvent) PostComments(result *types.ReviewResult) error { - if e.client == nil { - e.client = newHTTPClient(e.apiBase, e.auth) - } - - // 获取所有提交作者并去重 - assignees := make(map[string]struct{}) - for _, commit := range e.Commits { - if commit.Author.Login != "" { - assignees[commit.Author.Login] = struct{}{} - } - } - - // 如果没有提交作者,则使用推送者 - if len(assignees) == 0 { - assignees[e.Pusher.Login] = struct{}{} - } - - // 将 map 转换为 slice - assigneeList := make([]string, 0, len(assignees)) - for assignee := range assignees { - assigneeList = append(assigneeList, assignee) - } - - // 创建 issue - path := fmt.Sprintf("/api/v1/repos/%s/%s/issues", e.Repository.Owner.Login, e.Repository.Name) - issueData := map[string]interface{}{ - "title": fmt.Sprintf("AI 代码审查 - %s", e.After[:7]), - "body": utils.FormatReviewResult(result), - "assignee": assigneeList[0], // 使用第一个作者作为主要受理人 - "assignees": assigneeList, - } - - if err := e.client.post(path, issueData); err != nil { - log.Printf("创建 issue 失败: path=%s, error=%v", path, err) - return fmt.Errorf("创建 issue 失败: %w", err) - } - - log.Printf("成功创建 issue: path=%s, commitID=%s, assignees=%v", path, e.After[:7], assigneeList) - return nil -} - -func (e *GiteaEvent) GetPlatform() string { - return "gitea" -} - -// 判断是否应该跳过文件审查 -func shouldSkipFile(filename string) bool { - // 跳过特定文件类型 - skipExtensions := []string{".md", ".txt", ".json", ".yaml", ".yml", ".lock"} - for _, ext := range skipExtensions { - if strings.HasSuffix(filename, ext) { - return true - } - } - - // 跳过特定目录 - skipDirs := []string{"node_modules/", "dist/", "build/", "vendor/"} - for _, dir := range skipDirs { - if strings.Contains(filename, dir) { - return true - } - } - - return false -} diff --git a/services/platforms/gitee.go b/services/platforms/gitee.go deleted file mode 100644 index bb0644b..0000000 --- a/services/platforms/gitee.go +++ /dev/null @@ -1,165 +0,0 @@ -package platforms - -import ( - "code-review/services/types" - "code-review/utils" - "fmt" - "log" - "strings" -) - -// GiteeEvent Gitee 平台的 webhook 事件 -type GiteeEvent struct { - apiBase string - auth *AuthConfig - Event string - client *httpClient - Action string `json:"action"` - ActionDesc string `json:"action_desc"` - Hook struct { - Password string `json:"password"` // webhook 密码 - } `json:"hook"` - Password string `json:"password"` // 兼容旧版本 - Project struct { - ID int `json:"id"` - Name string `json:"name"` - Path string `json:"path"` - FullName string `json:"full_name"` - WebURL string `json:"web_url"` - Description string `json:"description"` - } `json:"project"` - PullRequest struct { - ID int `json:"id"` - Number int `json:"number"` - State string `json:"state"` - Title string `json:"title"` - Body string `json:"body"` - SourceBranch string `json:"source_branch"` - TargetBranch string `json:"target_branch"` - Commits []struct { - ID string `json:"id"` - Message string `json:"message"` - } `json:"commits"` - Changes []struct { - Path string `json:"path"` - Content string `json:"content"` - Type string `json:"type"` // added, modified, deleted, renamed - OldPath string `json:"old_path,omitempty"` - } `json:"changes"` - } `json:"pull_request"` -} - -func NewGiteeEvent(baseURL string, auth *AuthConfig) *GiteeEvent { - return &GiteeEvent{ - apiBase: baseURL, - auth: auth, - client: newHTTPClient(baseURL, auth), - } -} - -func (e *GiteeEvent) ExtractChanges() (*types.CodeChanges, error) { - changes := &types.CodeChanges{ - Repository: e.Project.FullName, - Branch: e.PullRequest.SourceBranch, - CommitID: e.PullRequest.Commits[len(e.PullRequest.Commits)-1].ID, - Files: make([]types.FileChange, 0, len(e.PullRequest.Changes)), - PullRequest: &types.PullRequest{ - ID: e.PullRequest.ID, - Title: e.PullRequest.Title, - Description: e.PullRequest.Body, - SourceBranch: e.PullRequest.SourceBranch, - TargetBranch: e.PullRequest.TargetBranch, - }, - } - - // 过滤合并提交 - validCommits := make([]struct { - ID string `json:"id"` - Message string `json:"message"` - }, 0) - - for _, commit := range e.PullRequest.Commits { - if strings.HasPrefix(commit.Message, "Merge remote-tracking branch") || - strings.HasPrefix(commit.Message, "Merge branch") { - log.Printf("跳过合并提交: commit=%s", commit.ID) - continue - } - validCommits = append(validCommits, commit) - } - - if len(validCommits) == 0 { - log.Printf("没有有效的提交记录(所有提交都是合并提交),跳过代码审查") - return nil, nil - } - - // 更新最后的提交ID - if len(validCommits) > 0 { - changes.CommitID = validCommits[len(validCommits)-1].ID - } - - for _, change := range e.PullRequest.Changes { - fileChange := types.FileChange{ - Path: change.Path, - Content: change.Content, - OldPath: change.OldPath, - } - - fileChange.Type = utils.ParseFileType(change.Type) - - changes.Files = append(changes.Files, fileChange) - } - - return changes, nil -} - -func (e *GiteeEvent) PostComments(result *types.ReviewResult) error { - if e.client == nil { - e.client = newHTTPClient(e.apiBase, e.auth) - } - - for _, comment := range result.Comments { - body := map[string]interface{}{ - "access_token": e.auth.Token, - "body": fmt.Sprintf("**Code Review Comment**\n\nFile: %s\nLine: %d\nSeverity: %s\n\n%s", - comment.Path, - comment.Line, - comment.Severity, - comment.Content, - ), - "position_line": comment.Line, - "path": comment.Path, - } - - path := fmt.Sprintf("/v5/repos/%s/pulls/%d/comments", - e.Project.FullName, - e.PullRequest.Number, - ) - - if err := e.client.post(path, body); err != nil { - return fmt.Errorf("post comment failed: %w", err) - } - } - - // 发送总结评论 - if result.Summary != "" { - body := map[string]interface{}{ - "access_token": e.auth.Token, - "body": fmt.Sprintf("**Code Review Summary**\n\n%s", result.Summary), - } - - path := fmt.Sprintf("/v5/repos/%s/pulls/%d/comments", - e.Project.FullName, - e.PullRequest.Number, - ) - - if err := e.client.post(path, body); err != nil { - return fmt.Errorf("post summary failed: %w", err) - } - } - - return nil -} - -func (e *GiteeEvent) GetPlatform() string { - return "gitee" -} diff --git a/services/platforms/gitlab.go b/services/platforms/gitlab.go deleted file mode 100644 index 9abd34a..0000000 --- a/services/platforms/gitlab.go +++ /dev/null @@ -1,165 +0,0 @@ -package platforms - -import ( - "code-review/services/types" - "code-review/utils" - "fmt" - "log" - "strings" -) - -// GitlabEvent Gitlab 平台的 webhook 事件 -type GitlabEvent struct { - apiBase string - auth *AuthConfig - Event string - client *httpClient - - ObjectKind string `json:"object_kind"` // "push", "merge_request" 等 - Before string `json:"before"` - After string `json:"after"` - Ref string `json:"ref"` - Project struct { - ID int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - WebURL string `json:"web_url"` - PathWithNamespace string `json:"path_with_namespace"` - } `json:"project"` - Commits []struct { - ID string `json:"id"` - Message string `json:"message"` - Title string `json:"title"` - } `json:"commits"` -} - -// NewGitlabEvent 创建 GitLab 事件实例 -func NewGitlabEvent(baseURL string, auth *AuthConfig, event string) *GitlabEvent { - return &GitlabEvent{ - apiBase: baseURL, - auth: auth, - Event: event, - } -} - -// 定义 GitLab diff 响应的结构 -type gitlabDiff struct { - Diff string `json:"diff"` - NewPath string `json:"new_path"` - OldPath string `json:"old_path"` - AMode string `json:"a_mode"` - BMode string `json:"b_mode"` - NewFile bool `json:"new_file"` - RenamedFile bool `json:"renamed_file"` - DeletedFile bool `json:"deleted_file"` -} - -func (e *GitlabEvent) ExtractChanges() (*types.CodeChanges, error) { - // 检查是否有提交记录 - if len(e.Commits) == 0 { - log.Printf("没有提交记录,跳过代码审查") - return nil, nil - } - - // 检查是否跳过代码审查 - validCommits := make([]struct { - ID string `json:"id"` - Message string `json:"message"` - Title string `json:"title"` - }, 0) - - for _, commit := range e.Commits { - if strings.Contains(commit.Message, "[skip codereview]") { - log.Printf("跳过代码审查: commit=%s", commit.ID) - return nil, nil - } - // 过滤合并提交 - if strings.HasPrefix(commit.Message, "Merge remote-tracking branch") || - strings.HasPrefix(commit.Message, "Merge branch") { - log.Printf("跳过合并提交: commit=%s", commit.ID) - continue - } - validCommits = append(validCommits, commit) - } - - if len(validCommits) == 0 { - log.Printf("没有有效的提交记录(所有提交都是合并提交),跳过代码审查") - return nil, nil - } - - if e.client == nil { - e.client = newHTTPClient(e.apiBase, e.auth) - log.Printf("初始化 HTTP 客户端: url=%s", e.apiBase) - } - - changes := &types.CodeChanges{ - Repository: e.Project.PathWithNamespace, - Branch: e.Ref, - CommitID: e.After, - Files: make([]types.FileChange, 0), - } - - for _, commit := range validCommits { - // 移除 URL 中的 token - apiPath := fmt.Sprintf("/api/v4/projects/%d/repository/commits/%s/diff", - e.Project.ID, commit.ID) - - headers := map[string]string{ - "PRIVATE-TOKEN": e.auth.Token, - } - - var diffs []gitlabDiff - if err := e.client.getWithHeaders(apiPath, &diffs, headers); err != nil { - log.Printf("获取提交详情失败: commit=%s, error=%v", commit.ID, err) - continue - } - - for _, diff := range diffs { - status := "modified" - if diff.NewFile { - status = "added" - } else if diff.DeletedFile { - status = "deleted" - } else if diff.RenamedFile { - status = "renamed" - } - - changes.Files = append(changes.Files, types.FileChange{ - Path: diff.NewPath, - Content: diff.Diff, - Type: utils.ParseFileType(status), - }) - } - } - - return changes, nil -} - -func (e *GitlabEvent) PostComments(result *types.ReviewResult) error { - if e.client == nil { - e.client = newHTTPClient(e.apiBase, e.auth) - } - - // 创建 issue - path := fmt.Sprintf("/api/v4/projects/%d/issues", e.Project.ID) - issueData := map[string]interface{}{ - "title": fmt.Sprintf("AI 代码审查 - %s", e.After[:7]), - "description": utils.FormatReviewResult(result), - } - - headers := map[string]string{ - "PRIVATE-TOKEN": e.auth.Token, - } - - if err := e.client.postWithHeaders(path, issueData, headers); err != nil { - log.Printf("创建 issue 失败: path=%s, error=%v", path, err) - return fmt.Errorf("创建 issue 失败: %w", err) - } - - log.Printf("成功创建 issue: path=%s, commitID=%s", path, e.After[:7]) - return nil -} - -func (e *GitlabEvent) GetPlatform() string { - return "gitlab" -} diff --git a/services/types/review.go b/services/types/review.go deleted file mode 100644 index e4909f4..0000000 --- a/services/types/review.go +++ /dev/null @@ -1,24 +0,0 @@ -package types - -// ReviewResult 代表代码审查结果 -type ReviewResult struct { - Comments []Comment - Summary string -} - -// Comment 代表代码审查评论 -type Comment struct { - Path string - Line int - Content string - Severity Severity -} - -// Severity 代表问题严重程度 -type Severity string - -const ( - Error Severity = "error" - Warning Severity = "warning" - Info Severity = "info" -) diff --git a/services/types/webhook.go b/services/types/webhook.go deleted file mode 100644 index c92747f..0000000 --- a/services/types/webhook.go +++ /dev/null @@ -1,37 +0,0 @@ -package types - -// CodeChanges 代表代码变更信息 -type CodeChanges struct { - Repository string - Branch string - CommitID string - Files []FileChange - PullRequest *PullRequest -} - -// FileChange 代表单个文件的变更 -type FileChange struct { - Path string - Content string - OldPath string // 用于重命名场景 - Type ChangeType -} - -// ChangeType 代表变更类型 -type ChangeType string - -const ( - Added ChangeType = "added" - Modified ChangeType = "modified" - Deleted ChangeType = "deleted" - Renamed ChangeType = "renamed" -) - -// PullRequest 代表 PR/MR 信息 -type PullRequest struct { - ID int - Title string - Description string - SourceBranch string - TargetBranch string -} diff --git a/src/main/java/com/codereview/enums/ChatMessageRole.java b/src/main/java/com/codereview/enums/ChatMessageRole.java new file mode 100644 index 0000000..09fdab6 --- /dev/null +++ b/src/main/java/com/codereview/enums/ChatMessageRole.java @@ -0,0 +1,24 @@ +package com.codereview.enums; + +import lombok.Getter; + +/** + * @author Hua + * @since 2025/4/30 11:02 + */ +@Getter +public enum ChatMessageRole { + SYSTEM("system", "系统提示词"), + USER("user", "用户"), + + + + ; + + private final String role; + private final String desc; + ChatMessageRole(String role, String desc) { + this.role = role; + this.desc = desc; + } +} diff --git a/static/index.html b/static/index.html deleted file mode 100644 index c2d7f4f..0000000 --- a/static/index.html +++ /dev/null @@ -1,680 +0,0 @@ - - - - Code Review 配置 - - - - - - - -
-
-
-
- - -
- -
-
- - -
- - - - - - - \ No newline at end of file diff --git a/utils/logger.go b/utils/logger.go deleted file mode 100644 index 378b750..0000000 --- a/utils/logger.go +++ /dev/null @@ -1,47 +0,0 @@ -package utils - -import ( - "fmt" - "io" - "log" - "os" - "path/filepath" - "time" -) - -var ( - // 日志文件句柄 - logFile *os.File - // 日志目录 - logDir = "logs" -) - -// InitLogger 初始化日志系统 -func InitLogger() error { - // 创建日志目录 - if err := os.MkdirAll(logDir, 0755); err != nil { - return fmt.Errorf("创建日志目录失败: %w", err) - } - - // 生成日志文件名 - logFileName := filepath.Join(logDir, time.Now().Format("2006-01-02")+".log") - - // 打开日志文件 - file, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return fmt.Errorf("打开日志文件失败: %w", err) - } - - // 设置日志输出 - log.SetOutput(io.MultiWriter(os.Stdout, file)) - logFile = file - - return nil -} - -// CloseLogger 关闭日志文件 -func CloseLogger() { - if logFile != nil { - logFile.Close() - } -} diff --git a/utils/utils.go b/utils/utils.go deleted file mode 100644 index 05d6a8f..0000000 --- a/utils/utils.go +++ /dev/null @@ -1,105 +0,0 @@ -package utils - -import ( - "code-review/services/types" - "fmt" - "strings" -) - -// GetString 从 map 中安全获取字符串值 -func GetString(m map[string]interface{}, key string) string { - if v, ok := m[key].(string); ok { - return v - } - return "" -} - -// GetFloat64 从 map 中安全获取 float64 值 -func GetFloat64(m map[string]interface{}, key string) float64 { - if v, ok := m[key].(float64); ok { - return v - } - return 0 -} - -// GetBool 从 map 中安全获取布尔值 -func GetBool(m map[string]interface{}, key string) bool { - if v, ok := m[key].(bool); ok { - return v - } - return false -} - -// GetInt 从 map 中安全获取整数值 -func GetInt(m map[string]interface{}, key string) int { - if v, ok := m[key].(float64); ok { - return int(v) - } - return 0 -} - -// GetIntPtr 从 map 中安全获取整数指针值 -func GetIntPtr(m map[string]interface{}, key string) *int { - if v, ok := m[key].(float64); ok { - intVal := int(v) - return &intVal - } - return nil -} - -// GetIntOk 从 map 中获取 int 值,并返回是否存在 -func GetIntOk(m map[string]interface{}, key string) (int, bool) { - if val, ok := m[key]; ok { - switch v := val.(type) { - case float64: - return int(v), true - case int: - return v, true - case int64: - return int(v), true - } - } - return 0, false -} - -// FormatReviewResult 将审查结果格式化为 Markdown 格式 -func FormatReviewResult(result *types.ReviewResult) string { - var body strings.Builder - - // 添加摘要 - if result.Summary != "" { - body.WriteString("## 审查摘要\n\n") - body.WriteString(result.Summary) - body.WriteString("\n\n") - } - - // 添加详细评论 - if len(result.Comments) > 0 { - body.WriteString("## 详细评论\n\n") - for _, comment := range result.Comments { - if comment.Path != "全局" { - body.WriteString(fmt.Sprintf("### %s\n\n", comment.Path)) - } - body.WriteString(comment.Content) - body.WriteString("\n\n") - } - } - - return body.String() -} - -// ParseFileType 将文件变更类型字符串转换为 ChangeType -func ParseFileType(t string) types.ChangeType { - switch t { - case "add", "added": - return types.Added - case "modify", "modified": - return types.Modified - case "delete", "deleted": - return types.Deleted - case "renamed": - return types.Renamed - default: - return types.Modified - } -}