init
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@ -25,3 +25,5 @@ go.work.sum
|
|||||||
# env file
|
# env file
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
.idea
|
||||||
|
|
||||||
|
57
config.yaml
Normal file
57
config.yaml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
port: 53321
|
||||||
|
admin_token: "token" # 管理页面访问令牌
|
||||||
|
|
||||||
|
auto_disable:
|
||||||
|
enabled: true
|
||||||
|
max_failures: 3
|
||||||
|
reset_after: 30 # 分钟
|
||||||
|
|
||||||
|
ais:
|
||||||
|
- name: "ollama"
|
||||||
|
api_key: ""
|
||||||
|
type: "ollama"
|
||||||
|
# url: "http://192.168.0.106:11434"
|
||||||
|
url: "http://localhost:11434"
|
||||||
|
model: "deepseek-r1:1.5b"
|
||||||
|
temperature: 0
|
||||||
|
stream: false
|
||||||
|
priority: 0 # 添加优先级配置
|
||||||
|
system_msg: |
|
||||||
|
你是一个代码审查员,你的职责是识别提交代码中的错误、性能问题和需要优化的地方。
|
||||||
|
你还负责提供建设性的反馈,并建议最佳实践来提高代码的整体质量。
|
||||||
|
|
||||||
|
在审查代码时:
|
||||||
|
- 审查代码变更(差异)并提供反馈
|
||||||
|
- 仔细检查是否真的存在错误或需要优化的空间,突出显示它们
|
||||||
|
- 不要突出显示小问题和细节
|
||||||
|
- 如果有多个评论,请使用要点符号
|
||||||
|
- 你不需要解释代码的功能
|
||||||
|
- 请使用中文给出反馈
|
||||||
|
- 如果你认为不需要优化或修改,请只回复 666
|
||||||
|
weight: 1
|
||||||
|
enabled: true
|
||||||
|
auto_disable: true # 使用自己的自动禁用配置
|
||||||
|
max_failures: 5 # 覆盖全局配置
|
||||||
|
reset_after: 60 # 覆盖全局配置
|
||||||
|
|
||||||
|
git:
|
||||||
|
# gitee:
|
||||||
|
# token: your-gitee-token
|
||||||
|
# webhook_secret: your-webhook-secret
|
||||||
|
# api_base: https://gitee.com/api/v5
|
||||||
|
#
|
||||||
|
- name: gitlab1
|
||||||
|
type: gitlab
|
||||||
|
token: glpat-XxSURo3JGaxmbkbf-EU6
|
||||||
|
webhook_secret: code
|
||||||
|
api_base: http://0.0.0.0:30080
|
||||||
|
signature_header: X-Gitlab-Token
|
||||||
|
event_header: X-Gitlab-Event
|
||||||
|
|
||||||
|
- name: gogs
|
||||||
|
type: gogs
|
||||||
|
token: 44315b70cb4f23ce1e35b24db666c253c09b4ca2
|
||||||
|
webhook_secret: code
|
||||||
|
api_base: http://0.0.0.0:13000
|
||||||
|
signature_header: X-Gogs-Signature
|
||||||
|
event_header: X-Gogs-Event
|
56
config.yaml.bak
Normal file
56
config.yaml.bak
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
port: 53321
|
||||||
|
admin_token: "token" # 管理页面访问令牌
|
||||||
|
|
||||||
|
auto_disable:
|
||||||
|
enabled: true
|
||||||
|
max_failures: 3
|
||||||
|
reset_after: 30 # 分钟
|
||||||
|
|
||||||
|
ais:
|
||||||
|
- name: "ollama"
|
||||||
|
api_key: ""
|
||||||
|
type: "ollama"
|
||||||
|
url: "http://localhost:11434"
|
||||||
|
model: "deepseek-coder:1.3b"
|
||||||
|
temperature: 0
|
||||||
|
stream: false
|
||||||
|
priority: 0 # 添加优先级配置
|
||||||
|
system_msg: |
|
||||||
|
你是一个代码审查员,你的职责是识别提交代码中的错误、性能问题和需要优化的地方。
|
||||||
|
你还负责提供建设性的反馈,并建议最佳实践来提高代码的整体质量。
|
||||||
|
|
||||||
|
在审查代码时:
|
||||||
|
- 审查代码变更(差异)并提供反馈
|
||||||
|
- 仔细检查是否真的存在错误或需要优化的空间,突出显示它们
|
||||||
|
- 不要突出显示小问题和细节
|
||||||
|
- 如果有多个评论,请使用要点符号
|
||||||
|
- 你不需要解释代码的功能
|
||||||
|
- 请使用中文给出反馈
|
||||||
|
- 如果你认为不需要优化或修改,请只回复 666
|
||||||
|
weight: 1
|
||||||
|
enabled: true
|
||||||
|
auto_disable: true # 使用自己的自动禁用配置
|
||||||
|
max_failures: 5 # 覆盖全局配置
|
||||||
|
reset_after: 60 # 覆盖全局配置
|
||||||
|
|
||||||
|
git:
|
||||||
|
# gitee:
|
||||||
|
# token: your-gitee-token
|
||||||
|
# webhook_secret: your-webhook-secret
|
||||||
|
# api_base: https://gitee.com/api/v5
|
||||||
|
#
|
||||||
|
- name: gitlab
|
||||||
|
type: gitlab
|
||||||
|
token: glpat-XxSURo3JGaxmbkbf-EU6
|
||||||
|
webhook_secret: code
|
||||||
|
api_base: http://0.0.0.0:30080
|
||||||
|
signature_header: X-Gitlab-Token
|
||||||
|
event_header: X-Gitlab-Event
|
||||||
|
|
||||||
|
- name: gogs
|
||||||
|
type: gogs
|
||||||
|
token: 44315b70cb4f23ce1e35b24db666c253c09b4ca2
|
||||||
|
webhook_secret: code
|
||||||
|
api_base: http://0.0.0.0:13000
|
||||||
|
signature_header: X-Gogs-Signature
|
||||||
|
event_header: X-Gogs-Event
|
338
config/config.go
Normal file
338
config/config.go
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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"` // 访问令牌
|
||||||
|
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 {
|
||||||
|
os.Rename(backupFile, "config.yaml")
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
"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,
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加获取 AIBalancer 的方法
|
||||||
|
func GetAIBalancer() *AIBalancer {
|
||||||
|
return aiBalancer
|
||||||
|
}
|
55
go.mod
Normal file
55
go.mod
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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
|
||||||
|
)
|
166
go.sum
Normal file
166
go.sum
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
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=
|
155
handlers/config.go
Normal file
155
handlers/config.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("配置更新成功")
|
||||||
|
h.cfg = &newConfig
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
154
handlers/webhook.go
Normal file
154
handlers/webhook.go
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var event services.WebhookEvent
|
||||||
|
switch platformConfig.Type {
|
||||||
|
case "gogs":
|
||||||
|
event = platforms.NewGogsEvent(
|
||||||
|
platformConfig.APIBase,
|
||||||
|
platformConfig.Token,
|
||||||
|
eventType,
|
||||||
|
)
|
||||||
|
case "gitea":
|
||||||
|
event = platforms.NewGiteaEvent(
|
||||||
|
platformConfig.APIBase,
|
||||||
|
platformConfig.Token,
|
||||||
|
eventType,
|
||||||
|
)
|
||||||
|
//case "gitee":
|
||||||
|
// event = &platforms.GiteeEvent{
|
||||||
|
// ApiBase: platformConfig.APIBase,
|
||||||
|
// Token: platformConfig.Token,
|
||||||
|
// }
|
||||||
|
case "gitlab":
|
||||||
|
event = platforms.NewGitlabEvent(
|
||||||
|
platformConfig.APIBase,
|
||||||
|
platformConfig.Token,
|
||||||
|
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
|
||||||
|
}
|
66
main.go
Normal file
66
main.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code-review/config"
|
||||||
|
"code-review/handlers"
|
||||||
|
"code-review/services"
|
||||||
|
"code-review/services/ai"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
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 负载均衡器
|
||||||
|
aiBalancer := config.NewAIBalancer(cfg.AIs)
|
||||||
|
|
||||||
|
// 获取一个 AI 配置
|
||||||
|
aiConfig, err := aiBalancer.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)
|
||||||
|
}
|
||||||
|
}
|
84
services/ai/ai.go
Normal file
84
services/ai/ai.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
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
|
||||||
|
}
|
184
services/ai/client.go
Normal file
184
services/ai/client.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
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)<think>(.*?)</think>"
|
||||||
|
|
||||||
|
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)<think>(.*?)</think>"
|
||||||
|
|
||||||
|
reg := regexp.MustCompile(pattern)
|
||||||
|
matches := reg.ReplaceAllString(result.Choices[0].Message.Content, "")
|
||||||
|
|
||||||
|
return strings.TrimSpace(matches), nil
|
||||||
|
}
|
96
services/interfaces.go
Normal file
96
services/interfaces.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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
|
||||||
|
}
|
174
services/platforms/client.go
Normal file
174
services/platforms/client.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package platforms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type httpClient struct {
|
||||||
|
url string
|
||||||
|
token string
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPClient(baseURL, token string) *httpClient {
|
||||||
|
return &httpClient{
|
||||||
|
url: baseURL,
|
||||||
|
token: token,
|
||||||
|
client: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
if c.token != "" {
|
||||||
|
req.Header.Set("Authorization", "token "+c.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
if c.token != "" {
|
||||||
|
req.Header.Set("Authorization", "token "+c.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
189
services/platforms/gitea.go
Normal file
189
services/platforms/gitea.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
package platforms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code-review/services/types"
|
||||||
|
"code-review/utils"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GiteaEvent struct {
|
||||||
|
apiBase string
|
||||||
|
token string
|
||||||
|
Event string
|
||||||
|
client *httpClient
|
||||||
|
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
Before string `json:"before"`
|
||||||
|
After string `json:"after"`
|
||||||
|
CompareURL string `json:"compare_url"`
|
||||||
|
Commits []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Author Author `json:"author"`
|
||||||
|
Committer Author `json:"committer"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
} `json:"commits"`
|
||||||
|
Repository struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
Private bool `json:"private"`
|
||||||
|
} `json:"repository"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGiteaEvent 创建 Gitea 事件
|
||||||
|
func NewGiteaEvent(apiBase, token string, event string) *GiteaEvent {
|
||||||
|
return &GiteaEvent{
|
||||||
|
apiBase: apiBase,
|
||||||
|
token: token,
|
||||||
|
Event: event,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GiteaEvent) ExtractChanges() (*types.CodeChanges, error) {
|
||||||
|
// 检查是否有提交记录
|
||||||
|
if len(e.Commits) == 0 {
|
||||||
|
log.Printf("没有提交记录,跳过代码审查")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否跳过代码审查
|
||||||
|
for _, commit := range e.Commits {
|
||||||
|
if strings.Contains(commit.Message, "[skip codereview]") {
|
||||||
|
log.Printf("跳过代码审查: commit=%s", commit.ID)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.client == nil {
|
||||||
|
e.client = newHTTPClient(e.apiBase, e.token)
|
||||||
|
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 {
|
||||||
|
// 直接获取 diff 内容
|
||||||
|
apiPath := fmt.Sprintf("/api/v1/repos/%s/git/commits/%s.diff?access_token=%s", e.Repository.FullName, commit.ID, e.token)
|
||||||
|
var diffContent string
|
||||||
|
|
||||||
|
if err := e.client.get(apiPath, &diffContent); err != nil {
|
||||||
|
log.Printf("获取提交详情失败: commit=%s, error=%v", commit.ID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除首尾空白
|
||||||
|
diffContent = strings.TrimSpace(diffContent)
|
||||||
|
if diffContent == "" {
|
||||||
|
log.Printf("提交没有变更内容: commit=%s", commit.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 diff 内容
|
||||||
|
diffBlocks := strings.Split(diffContent, "diff --git ")
|
||||||
|
// 去除空块
|
||||||
|
for _, block := range diffBlocks {
|
||||||
|
if strings.TrimSpace(block) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 'diff --git ' 前缀
|
||||||
|
block = strings.TrimPrefix(block, "diff --git ")
|
||||||
|
|
||||||
|
// 解析文件名和变更类型
|
||||||
|
lines := strings.Split(block, "\n")
|
||||||
|
if len(lines) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件名
|
||||||
|
filename := ""
|
||||||
|
status := "modified"
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "--- a/") {
|
||||||
|
filename = strings.TrimPrefix(line, "--- a/")
|
||||||
|
break
|
||||||
|
} else if strings.HasPrefix(line, "+++ b/") {
|
||||||
|
filename = strings.TrimPrefix(line, "+++ b/")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到文件名,跳过
|
||||||
|
if filename == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定变更类型
|
||||||
|
if strings.Contains(block, "new file mode") {
|
||||||
|
status = "added"
|
||||||
|
} else if strings.Contains(block, "deleted file mode") {
|
||||||
|
status = "deleted"
|
||||||
|
} else if strings.Contains(block, "rename from") {
|
||||||
|
status = "renamed"
|
||||||
|
}
|
||||||
|
|
||||||
|
var content strings.Builder
|
||||||
|
content.WriteString(fmt.Sprintf("### 变更说明\n"))
|
||||||
|
content.WriteString(fmt.Sprintf("提交信息: %s\n\n", commit.Message))
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case "added":
|
||||||
|
content.WriteString(fmt.Sprintf("新增文件: %s\n\n", filename))
|
||||||
|
case "modified":
|
||||||
|
content.WriteString(fmt.Sprintf("修改文件: %s\n\n", filename))
|
||||||
|
case "deleted":
|
||||||
|
content.WriteString(fmt.Sprintf("删除文件: %s\n\n", filename))
|
||||||
|
case "renamed":
|
||||||
|
content.WriteString(fmt.Sprintf("重命名文件: %s\n\n", filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString("### 变更内容\n")
|
||||||
|
content.WriteString("```diff\n")
|
||||||
|
content.WriteString(block)
|
||||||
|
content.WriteString("\n```\n")
|
||||||
|
|
||||||
|
changes.Files = append(changes.Files, types.FileChange{
|
||||||
|
Path: filename,
|
||||||
|
Content: content.String(),
|
||||||
|
Type: parseFileType(status),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GiteaEvent) PostComments(result *types.ReviewResult) error {
|
||||||
|
if e.client == nil {
|
||||||
|
e.client = newHTTPClient(e.apiBase, e.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 issue
|
||||||
|
path := fmt.Sprintf("/api/v1/repos/%s/issues", e.Repository.FullName)
|
||||||
|
issueData := map[string]interface{}{
|
||||||
|
"title": fmt.Sprintf("AI 代码审查 - %s", e.After[:7]),
|
||||||
|
"body": utils.FormatReviewResult(result),
|
||||||
|
}
|
||||||
|
|
||||||
|
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", path, e.After[:7])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GiteaEvent) GetPlatform() string {
|
||||||
|
return "gitea"
|
||||||
|
}
|
141
services/platforms/gitee.go
Normal file
141
services/platforms/gitee.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package platforms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code-review/services/types"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GiteeEvent Gitee 平台的 webhook 事件
|
||||||
|
type GiteeEvent struct {
|
||||||
|
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, token string) *GiteeEvent {
|
||||||
|
return &GiteeEvent{
|
||||||
|
client: newHTTPClient(baseURL, token),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, change := range e.PullRequest.Changes {
|
||||||
|
fileChange := types.FileChange{
|
||||||
|
Path: change.Path,
|
||||||
|
Content: change.Content,
|
||||||
|
OldPath: change.OldPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch change.Type {
|
||||||
|
case "added":
|
||||||
|
fileChange.Type = types.Added
|
||||||
|
case "modified":
|
||||||
|
fileChange.Type = types.Modified
|
||||||
|
case "deleted":
|
||||||
|
fileChange.Type = types.Deleted
|
||||||
|
case "renamed":
|
||||||
|
fileChange.Type = types.Renamed
|
||||||
|
}
|
||||||
|
|
||||||
|
changes.Files = append(changes.Files, fileChange)
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GiteeEvent) PostComments(result *types.ReviewResult) error {
|
||||||
|
if e.client == nil {
|
||||||
|
return fmt.Errorf("client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, comment := range result.Comments {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"access_token": e.client.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.client.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"
|
||||||
|
}
|
147
services/platforms/gitlab.go
Normal file
147
services/platforms/gitlab.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package platforms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code-review/services/types"
|
||||||
|
"code-review/utils"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GitlabEvent Gitlab 平台的 webhook 事件
|
||||||
|
type GitlabEvent struct {
|
||||||
|
apiBase string
|
||||||
|
token string
|
||||||
|
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, token string, event string) *GitlabEvent {
|
||||||
|
return &GitlabEvent{
|
||||||
|
apiBase: baseURL,
|
||||||
|
token: token,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否跳过代码审查
|
||||||
|
for _, commit := range e.Commits {
|
||||||
|
if strings.Contains(commit.Message, "[skip codereview]") {
|
||||||
|
log.Printf("跳过代码审查: commit=%s", commit.ID)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.client == nil {
|
||||||
|
e.client = newHTTPClient(e.apiBase, e.token)
|
||||||
|
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 e.Commits {
|
||||||
|
// 移除 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.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: parseFileType(status),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GitlabEvent) PostComments(result *types.ReviewResult) error {
|
||||||
|
if e.client == nil {
|
||||||
|
e.client = newHTTPClient(e.apiBase, e.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 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.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"
|
||||||
|
}
|
245
services/platforms/gogs.go
Normal file
245
services/platforms/gogs.go
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
package platforms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code-review/services/types"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GogsEvent Gogs 平台的 webhook 事件
|
||||||
|
type GogsEvent struct {
|
||||||
|
apiBase string
|
||||||
|
token string
|
||||||
|
Event string // 事件类型
|
||||||
|
client *httpClient
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
Before string `json:"before"`
|
||||||
|
After string `json:"after"`
|
||||||
|
CompareURL string `json:"compare_url"`
|
||||||
|
Commits []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Author Author `json:"author"`
|
||||||
|
Committer Author `json:"committer"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
} `json:"commits"`
|
||||||
|
CommitDetail struct {
|
||||||
|
Files []struct {
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Additions int `json:"additions"`
|
||||||
|
Deletions int `json:"deletions"`
|
||||||
|
Patch string `json:"patch"`
|
||||||
|
} `json:"files"`
|
||||||
|
}
|
||||||
|
Repository struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Owner Author `json:"owner"`
|
||||||
|
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"`
|
||||||
|
} `json:"repository"`
|
||||||
|
Pusher Author `json:"pusher"`
|
||||||
|
Sender Author `json:"sender"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Author 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGogsEvent 创建 Gogs 事件
|
||||||
|
func NewGogsEvent(apiBase, token string, event string) *GogsEvent {
|
||||||
|
return &GogsEvent{
|
||||||
|
apiBase: apiBase,
|
||||||
|
token: token,
|
||||||
|
Event: event,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GogsEvent) ExtractChanges() (*types.CodeChanges, error) {
|
||||||
|
// 检查是否有提交记录
|
||||||
|
if len(e.Commits) == 0 {
|
||||||
|
log.Printf("没有提交记录,跳过代码审查")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否跳过代码审查
|
||||||
|
for _, commit := range e.Commits {
|
||||||
|
if strings.Contains(commit.Message, "[skip codereview]") {
|
||||||
|
log.Printf("跳过代码审查: commit=%s", commit.ID)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.client == nil {
|
||||||
|
e.client = newHTTPClient(e.apiBase, e.token)
|
||||||
|
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 {
|
||||||
|
// 直接获取 diff 内容
|
||||||
|
apiPath := fmt.Sprintf("/api/v1/repos/%s/git/commits/%s.diff?access_token=%s", e.Repository.FullName, commit.ID, e.token)
|
||||||
|
var diffContent string
|
||||||
|
|
||||||
|
if err := e.client.get(apiPath, &diffContent); err != nil {
|
||||||
|
log.Printf("获取提交详情失败: commit=%s, error=%v", commit.ID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 去除首尾空白
|
||||||
|
diffContent = strings.TrimSpace(diffContent)
|
||||||
|
if diffContent == "" {
|
||||||
|
log.Printf("提交没有变更内容: commit=%s", commit.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 diff 内容
|
||||||
|
diffBlocks := strings.Split(diffContent, "diff --git ")
|
||||||
|
// 去除空块
|
||||||
|
for _, block := range diffBlocks {
|
||||||
|
if strings.TrimSpace(block) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 'diff --git ' 前缀
|
||||||
|
block = strings.TrimPrefix(block, "diff --git ")
|
||||||
|
|
||||||
|
// 解析文件名和变更类型
|
||||||
|
lines := strings.Split(block, "\n")
|
||||||
|
if len(lines) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件名
|
||||||
|
filename := ""
|
||||||
|
status := "modified"
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "--- a/") {
|
||||||
|
filename = strings.TrimPrefix(line, "--- a/")
|
||||||
|
break
|
||||||
|
} else if strings.HasPrefix(line, "+++ b/") {
|
||||||
|
filename = strings.TrimPrefix(line, "+++ b/")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到文件名,跳过
|
||||||
|
if filename == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定变更类型
|
||||||
|
if strings.Contains(block, "new file mode") {
|
||||||
|
status = "added"
|
||||||
|
} else if strings.Contains(block, "deleted file mode") {
|
||||||
|
status = "deleted"
|
||||||
|
} else if strings.Contains(block, "rename from") {
|
||||||
|
status = "renamed"
|
||||||
|
}
|
||||||
|
|
||||||
|
var content strings.Builder
|
||||||
|
content.WriteString(fmt.Sprintf("### 变更说明\n"))
|
||||||
|
content.WriteString(fmt.Sprintf("提交信息: %s\n\n", commit.Message))
|
||||||
|
|
||||||
|
switch status {
|
||||||
|
case "added":
|
||||||
|
content.WriteString(fmt.Sprintf("新增文件: %s\n\n", filename))
|
||||||
|
case "modified":
|
||||||
|
content.WriteString(fmt.Sprintf("修改文件: %s\n\n", filename))
|
||||||
|
case "deleted":
|
||||||
|
content.WriteString(fmt.Sprintf("删除文件: %s\n\n", filename))
|
||||||
|
case "renamed":
|
||||||
|
content.WriteString(fmt.Sprintf("重命名文件: %s\n\n", filename))
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString("### 变更内容\n")
|
||||||
|
content.WriteString("```diff\n")
|
||||||
|
content.WriteString(block)
|
||||||
|
content.WriteString("\n```\n")
|
||||||
|
|
||||||
|
changes.Files = append(changes.Files, types.FileChange{
|
||||||
|
Path: filename,
|
||||||
|
Content: content.String(),
|
||||||
|
Type: parseFileType(status),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GogsEvent) PostComments(result *types.ReviewResult) error {
|
||||||
|
if e.client == nil {
|
||||||
|
e.client = newHTTPClient(e.apiBase, e.token)
|
||||||
|
log.Printf("初始化 HTTP 客户端: url=%s", e.apiBase)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 issue 评论
|
||||||
|
issueBody := fmt.Sprintf("**代码审查报告**\n\n提交: %s\n\n", e.After)
|
||||||
|
for _, comment := range result.Comments {
|
||||||
|
issueBody += fmt.Sprintf("### 文件: %s\n\n%s\n\n", comment.Path, comment.Content)
|
||||||
|
}
|
||||||
|
if result.Summary != "" {
|
||||||
|
issueBody += fmt.Sprintf("\n### 总结\n\n%s", result.Summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 issue
|
||||||
|
createIssuePath := fmt.Sprintf("/api/v1/repos/%s/issues", e.Repository.FullName)
|
||||||
|
issueData := map[string]interface{}{
|
||||||
|
"title": fmt.Sprintf("代码审查: %s", e.After[:7]),
|
||||||
|
"body": issueBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.client.post(createIssuePath, issueData); err != nil {
|
||||||
|
log.Printf("创建 issue 失败: path=%s, error=%v", createIssuePath, err)
|
||||||
|
return fmt.Errorf("创建 issue 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("成功创建 issue: path=%s, commitID=%s", createIssuePath, e.After[:7])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GogsEvent) GetPlatform() string {
|
||||||
|
return "gogs"
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFileType(t string) types.ChangeType {
|
||||||
|
switch t {
|
||||||
|
case "add":
|
||||||
|
return types.Added
|
||||||
|
case "modify":
|
||||||
|
return types.Modified
|
||||||
|
case "delete":
|
||||||
|
return types.Deleted
|
||||||
|
default:
|
||||||
|
return types.Modified
|
||||||
|
}
|
||||||
|
}
|
24
services/types/review.go
Normal file
24
services/types/review.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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"
|
||||||
|
)
|
37
services/types/webhook.go
Normal file
37
services/types/webhook.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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
|
||||||
|
}
|
680
static/index.html
Normal file
680
static/index.html
Normal file
@ -0,0 +1,680 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Code Review 配置</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="color-scheme" content="light dark">
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
.container { max-width: 800px; margin-top: 2rem; }
|
||||||
|
.form-group { margin-bottom: 1rem; }
|
||||||
|
.password-field {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.toggle-password {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
/* 主题切换按钮样式 */
|
||||||
|
.theme-switcher {
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.theme-switcher .dropdown-menu {
|
||||||
|
min-width: 8rem;
|
||||||
|
}
|
||||||
|
.theme-switcher .dropdown-item i {
|
||||||
|
width: 1rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.theme-switcher .dropdown-item.active {
|
||||||
|
background-color: var(--bs-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题下的一些自定义样式 */
|
||||||
|
[data-bs-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
background-color: #1a1d20;
|
||||||
|
color: #e9ecef;
|
||||||
|
}
|
||||||
|
[data-bs-theme="dark"] .card {
|
||||||
|
background-color: #2b3035;
|
||||||
|
border-color: #495057;
|
||||||
|
}
|
||||||
|
[data-bs-theme="dark"] .form-control {
|
||||||
|
background-color: #1a1d20;
|
||||||
|
border-color: #495057;
|
||||||
|
color: #e9ecef;
|
||||||
|
}
|
||||||
|
[data-bs-theme="dark"] .form-control:focus {
|
||||||
|
background-color: #1a1d20;
|
||||||
|
border-color: #86b7fe;
|
||||||
|
color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题下的其他元素样式 */
|
||||||
|
[data-bs-theme="dark"] .card-header {
|
||||||
|
background-color: #343a40;
|
||||||
|
border-bottom-color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] .btn-outline-secondary {
|
||||||
|
color: #e9ecef;
|
||||||
|
border-color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] .btn-outline-secondary:hover {
|
||||||
|
background-color: #495057;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] .dropdown-menu {
|
||||||
|
background-color: #2b3035;
|
||||||
|
border-color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] .dropdown-item {
|
||||||
|
color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] .dropdown-item:hover {
|
||||||
|
background-color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 亮色主题样式 */
|
||||||
|
[data-bs-theme="light"] {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body data-bs-theme="light">
|
||||||
|
<div class="theme-switcher dropdown">
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="bi bi-sun-fill"></i>
|
||||||
|
<span class="d-none d-md-inline ms-1">主题</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
<li><button class="dropdown-item" data-theme="light"><i class="bi bi-sun-fill"></i>亮色</button></li>
|
||||||
|
<li><button class="dropdown-item" data-theme="dark"><i class="bi bi-moon-fill"></i>暗色</button></li>
|
||||||
|
<li><button class="dropdown-item" data-theme="auto"><i class="bi bi-circle-half"></i>自动</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div id="loginForm" class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>管理令牌</label>
|
||||||
|
<input type="password" class="form-control" id="adminToken">
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="verifyToken()">验证</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="configSection" style="display: none;">
|
||||||
|
<h2>Code Review 配置</h2>
|
||||||
|
<form id="configForm">
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
AI 配置
|
||||||
|
<button type="button" class="btn btn-sm btn-primary float-end" onclick="addAIService()">添加 AI 服务</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body" id="aiServices">
|
||||||
|
<!-- AI 服务配置将动态添加到这里 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">自动禁用配置</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input type="checkbox" class="form-check-input" name="auto_disable.enabled" id="autoDisableEnabled">
|
||||||
|
<label class="form-check-label" for="autoDisableEnabled">启用自动禁用</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>最大失败次数</label>
|
||||||
|
<input type="number" class="form-control" name="auto_disable.max_failures" min="1" value="3">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>重置时间(分钟)</label>
|
||||||
|
<input type="number" class="form-control" name="auto_disable.reset_after" min="1" value="30">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
Git 平台配置
|
||||||
|
<button type="button" class="btn btn-sm btn-primary float-end" onclick="addGitPlatform()">添加平台</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="gitPlatforms">
|
||||||
|
<!-- Git 平台配置将动态添加到这里 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">保存配置</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 添加 Bootstrap JS
|
||||||
|
document.write('<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"><\/script>');
|
||||||
|
|
||||||
|
let adminToken = '';
|
||||||
|
|
||||||
|
// 验证令牌
|
||||||
|
async function verifyToken() {
|
||||||
|
const token = document.getElementById('adminToken').value;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config', {
|
||||||
|
headers: {
|
||||||
|
'X-Admin-Token': token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('令牌验证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证成功,保存令牌并显示配置界面
|
||||||
|
adminToken = token;
|
||||||
|
document.getElementById('loginForm').style.display = 'none';
|
||||||
|
document.getElementById('configSection').style.display = 'block';
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
loadConfig();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config', {
|
||||||
|
headers: {
|
||||||
|
'X-Admin-Token': adminToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const config = await response.json();
|
||||||
|
|
||||||
|
// 填充自动禁用配置
|
||||||
|
document.querySelector('[name="auto_disable.enabled"]').checked = config.auto_disable.enabled;
|
||||||
|
document.querySelector('[name="auto_disable.max_failures"]').value = config.auto_disable.max_failures;
|
||||||
|
document.querySelector('[name="auto_disable.reset_after"]').value = config.auto_disable.reset_after;
|
||||||
|
|
||||||
|
// 填充 AI 服务配置
|
||||||
|
if (config.ais && config.ais.length > 0) {
|
||||||
|
const aiServices = document.getElementById('aiServices');
|
||||||
|
aiServices.innerHTML = ''; // 清空现有内容
|
||||||
|
config.ais.forEach((ai, index) => {
|
||||||
|
aiServices.insertAdjacentHTML('beforeend', createAIServiceHTML(index, ai));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addAIService(); // 添加一个默认的 AI 服务配置
|
||||||
|
}
|
||||||
|
|
||||||
|
// 填充 Git 配置
|
||||||
|
const gitPlatforms = document.getElementById('gitPlatforms');
|
||||||
|
gitPlatforms.innerHTML = ''; // 清空现有内容
|
||||||
|
|
||||||
|
console.log('Git config:', config.git); // 调试日志
|
||||||
|
|
||||||
|
// 检查并处理 Git 平台配置
|
||||||
|
if (config.git && config.git.length > 0) {
|
||||||
|
config.git.forEach((platform, index) => {
|
||||||
|
gitPlatforms.insertAdjacentHTML('beforeend', createGitPlatformHTML(index, platform));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addGitPlatform(); // 添加一个默认的平台配置
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加调试日志
|
||||||
|
console.log('Loaded config:', config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载配置失败:', error);
|
||||||
|
alert('加载配置失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认系统提示词
|
||||||
|
const defaultSystemMsg = `你是一个代码审查员,你的职责是识别提交代码中的错误、性能问题和需要优化的地方。
|
||||||
|
你还负责提供建设性的反馈,并建议最佳实践来提高代码的整体质量。
|
||||||
|
|
||||||
|
在审查代码时:
|
||||||
|
- 审查代码变更(差异)并提供反馈
|
||||||
|
- 仔细检查是否真的存在错误或需要优化的空间,突出显示它们
|
||||||
|
- 不要突出显示小问题和细节
|
||||||
|
- 如果有多个评论,请使用要点符号
|
||||||
|
- 你不需要解释代码的功能
|
||||||
|
- 请使用中文给出反馈
|
||||||
|
- 如果你认为不需要优化或修改,请只回复 666`;
|
||||||
|
|
||||||
|
function createAIServiceHTML(index, ai = {}) {
|
||||||
|
return `
|
||||||
|
<div class="ai-service mb-4" data-index="${index}">
|
||||||
|
<h5 class="d-flex justify-content-between">
|
||||||
|
AI 服务 #${index + 1}
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" onclick="removeAIService(${index})">删除</button>
|
||||||
|
</h5>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input type="checkbox" class="form-check-input" name="ais[${index}].enabled" ${ai.enabled ? 'checked' : ''}>
|
||||||
|
<label class="form-check-label">启用服务</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>名称</label>
|
||||||
|
<input type="text" class="form-control" name="ais[${index}].name"
|
||||||
|
value="${ai.name || ''}" required
|
||||||
|
title="请输入 AI 服务名称">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>类型</label>
|
||||||
|
<select class="form-control" name="ais[${index}].type"
|
||||||
|
onchange="toggleAPIKeyField(${index})" required
|
||||||
|
title="请选择 AI 服务类型">
|
||||||
|
<option value="openai" ${ai.type === 'openai' ? 'selected' : ''}>OpenAI</option>
|
||||||
|
<option value="ollama" ${ai.type === 'ollama' ? 'selected' : ''}>Ollama</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>API URL</label>
|
||||||
|
<input type="url" class="form-control" name="ais[${index}].url"
|
||||||
|
value="${ai.url || ''}" required
|
||||||
|
title="请输入有效的 API URL"
|
||||||
|
pattern="https?://.+">
|
||||||
|
</div>
|
||||||
|
<div class="api-key-field" style="display: ${ai.type === 'ollama' ? 'none' : 'block'}">
|
||||||
|
${createPasswordField(`ais[${index}].api_key`, ai.api_key, 'API Key')}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>模型</label>
|
||||||
|
<input type="text" class="form-control" name="ais[${index}].model"
|
||||||
|
value="${ai.model || ''}" required
|
||||||
|
title="请输入模型名称">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Temperature</label>
|
||||||
|
<input type="number" class="form-control" name="ais[${index}].temperature"
|
||||||
|
value="${ai.temperature ?? 0}" min="0" max="2" step="0.1" required>
|
||||||
|
<small class="form-text text-muted">控制输出的随机性(0-2),0 表示最确定的输出</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input type="checkbox" class="form-check-input" name="ais[${index}].stream" ${ai.stream ? 'checked' : ''}>
|
||||||
|
<label class="form-check-label">启用流式响应</label>
|
||||||
|
<small class="form-text text-muted d-block">启用后可以逐步获取 AI 的响应</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>系统提示词</label>
|
||||||
|
<textarea class="form-control" name="ais[${index}].system_msg" rows="3"
|
||||||
|
placeholder="设置与 AI 对话的系统提示词,用于定义 AI 的角色和行为"
|
||||||
|
>${ai.system_msg || defaultSystemMsg}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>权重</label>
|
||||||
|
<input type="number" class="form-control" name="ais[${index}].weight" value="${ai.weight || 1}" min="1" required>
|
||||||
|
<small class="form-text text-muted">在相同优先级的 AI 中,按权重比例分配请求</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>优先级</label>
|
||||||
|
<input type="number" class="form-control" name="ais[${index}].priority" value="${ai.priority || 0}" min="0" required>
|
||||||
|
<small class="form-text text-muted">数字越大优先级越高,优先级相同的 AI 将按权重负载均衡</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input type="checkbox" class="form-check-input" name="ais[${index}].auto_disable" ${ai.auto_disable ? 'checked' : ''}>
|
||||||
|
<label class="form-check-label">使用自定义自动禁用配置</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>最大失败次数(可选)</label>
|
||||||
|
<input type="number" class="form-control" name="ais[${index}].max_failures" min="1" value="${ai.max_failures || ''}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>重置时间(分钟,可选)</label>
|
||||||
|
<input type="number" class="form-control" name="ais[${index}].reset_after" min="1" value="${ai.reset_after || ''}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAIService() {
|
||||||
|
const aiServices = document.getElementById('aiServices');
|
||||||
|
const index = aiServices.children.length;
|
||||||
|
aiServices.insertAdjacentHTML('beforeend', createAIServiceHTML(index, {
|
||||||
|
system_msg: defaultSystemMsg,
|
||||||
|
temperature: 0,
|
||||||
|
stream: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAIService(index) {
|
||||||
|
const service = document.querySelector(`.ai-service[data-index="${index}"]`);
|
||||||
|
if (service) {
|
||||||
|
service.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加检查平台名称唯一性的函数
|
||||||
|
function isGitPlatformNameUnique(name, currentIndex) {
|
||||||
|
const platforms = document.querySelectorAll('.git-platform');
|
||||||
|
for (let i = 0; i < platforms.length; i++) {
|
||||||
|
if (i === currentIndex) continue;
|
||||||
|
const nameInput = platforms[i].querySelector('input[name$="].name"]');
|
||||||
|
if (nameInput && nameInput.value.trim() === name.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改 createGitPlatformHTML 函数,添加 oninput 事件处理
|
||||||
|
function createGitPlatformHTML(index, platform = {}) {
|
||||||
|
// 添加调试日志
|
||||||
|
console.log('Creating Git platform HTML with:', platform);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="git-platform mb-4" data-index="${index}">
|
||||||
|
<h5 class="d-flex justify-content-between">
|
||||||
|
<div class="form-group" style="flex: 1; margin-right: 1rem;">
|
||||||
|
<input type="text" class="form-control" name="git[${index}].name"
|
||||||
|
value="${platform.name || ''}" placeholder="平台名称" required
|
||||||
|
title="请输入 Git 平台名称"
|
||||||
|
oninput="validateGitPlatformName(this, ${index})"
|
||||||
|
data-original-value="${platform.name || ''}">
|
||||||
|
<div class="invalid-feedback">平台名称已存在,请使用其他名称</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" onclick="removeGitPlatform(${index})">删除</button>
|
||||||
|
</h5>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平台类型</label>
|
||||||
|
<select class="form-control" name="git[${index}].type" required
|
||||||
|
title="请选择 Git 平台类型">
|
||||||
|
<option value="gitlab" ${platform.type === 'gitlab' ? 'selected' : ''}>GitLab</option>
|
||||||
|
<option value="gitee" ${platform.type === 'gitee' ? 'selected' : ''}>Gitee</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
${createPasswordField(`git[${index}].token`, platform.token, 'Token', '请输入平台访问令牌')}
|
||||||
|
${createPasswordField(`git[${index}].webhook_secret`, platform.webhook_secret, 'Webhook Secret', '请输入 Webhook 密钥')}
|
||||||
|
<div class="form-group">
|
||||||
|
<label>API Base URL</label>
|
||||||
|
<input type="url" class="form-control" name="git[${index}].api_base"
|
||||||
|
value="${platform.api_base || ''}" required
|
||||||
|
title="请输入有效的 API Base URL"
|
||||||
|
pattern="https?://.+">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加验证平台名称的函数
|
||||||
|
function validateGitPlatformName(input, index) {
|
||||||
|
const name = input.value.trim();
|
||||||
|
const originalValue = input.dataset.originalValue;
|
||||||
|
|
||||||
|
// 如果是编辑现有平台且名称未改变,则不需要验证
|
||||||
|
if (name === originalValue) {
|
||||||
|
input.setCustomValidity('');
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isGitPlatformNameUnique(name, index)) {
|
||||||
|
input.setCustomValidity('平台名称必须唯一');
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
input.setCustomValidity('');
|
||||||
|
input.classList.remove('is-invalid');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addGitPlatform() {
|
||||||
|
const gitPlatforms = document.getElementById('gitPlatforms');
|
||||||
|
const index = gitPlatforms.children.length;
|
||||||
|
gitPlatforms.insertAdjacentHTML('beforeend', createGitPlatformHTML(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeGitPlatform(index) {
|
||||||
|
const platform = document.querySelector(`.git-platform[data-index="${index}"]`);
|
||||||
|
if (platform) {
|
||||||
|
platform.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改表单提交处理,添加额外的验证
|
||||||
|
document.getElementById('configForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 验证所有 Git 平台名称的唯一性
|
||||||
|
const gitPlatformNames = new Set();
|
||||||
|
const platforms = document.querySelectorAll('.git-platform input[name$="].name"]');
|
||||||
|
let hasNameConflict = false;
|
||||||
|
|
||||||
|
platforms.forEach((input, index) => {
|
||||||
|
const name = input.value.trim();
|
||||||
|
if (gitPlatformNames.has(name)) {
|
||||||
|
hasNameConflict = true;
|
||||||
|
input.setCustomValidity('平台名称必须唯一');
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
} else {
|
||||||
|
gitPlatformNames.add(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasNameConflict) {
|
||||||
|
e.target.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 HTML5 原生表单验证
|
||||||
|
const form = e.target;
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
form.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const formEntries = Array.from(formData.entries()).reduce((acc, [key, value]) => {
|
||||||
|
// 处理复选框和数字类型的值
|
||||||
|
if (key.endsWith('.enabled') || key.endsWith('.auto_disable') || key.endsWith('.stream')) {
|
||||||
|
acc[key] = value === 'on';
|
||||||
|
} else if (key.includes('.temperature')) {
|
||||||
|
acc[key] = parseFloat(value);
|
||||||
|
} else if (key.includes('.weight') || key.includes('.max_failures') ||
|
||||||
|
key.includes('.reset_after') || key === 'port') {
|
||||||
|
acc[key] = value ? parseInt(value) : null;
|
||||||
|
} else {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const config = unflattenObject(formEntries);
|
||||||
|
|
||||||
|
// 转换 Git 配置格式
|
||||||
|
if (config.git && Array.isArray(config.git)) {
|
||||||
|
config.git = {
|
||||||
|
platforms: config.git.map(platform => ({
|
||||||
|
name: platform.name,
|
||||||
|
type: platform.type,
|
||||||
|
token: platform.token,
|
||||||
|
webhook_secret: platform.webhook_secret,
|
||||||
|
api_base: platform.api_base,
|
||||||
|
signature_header: platform.type === 'gitlab' ? 'X-Gitlab-Token' : 'X-Gitee-Token',
|
||||||
|
event_header: platform.type === 'gitlab' ? 'X-Gitlab-Event' : 'X-Gitee-Event'
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 AI 配置中的空值
|
||||||
|
if (config.ais) {
|
||||||
|
config.ais = config.ais.map(ai => {
|
||||||
|
// 如果 max_failures 和 reset_after 为空,则设置为 null
|
||||||
|
if (!ai.max_failures) ai.max_failures = null;
|
||||||
|
if (!ai.reset_after) ai.reset_after = null;
|
||||||
|
return ai;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Admin-Token': adminToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.message || '更新配置失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('配置已更新');
|
||||||
|
// 重新加载配置以显示最新状态
|
||||||
|
loadConfig();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.message);
|
||||||
|
console.error('保存配置失败:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 辅助函数:展平对象
|
||||||
|
function flattenObject(obj, prefix = '') {
|
||||||
|
return Object.keys(obj).reduce((acc, k) => {
|
||||||
|
const pre = prefix.length ? prefix + '.' : '';
|
||||||
|
if (typeof obj[k] === 'object' && obj[k] !== null) {
|
||||||
|
Object.assign(acc, flattenObject(obj[k], pre + k));
|
||||||
|
} else {
|
||||||
|
acc[pre + k] = obj[k];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:还原对象
|
||||||
|
function unflattenObject(obj) {
|
||||||
|
const result = {};
|
||||||
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
|
const parts = key.split('.');
|
||||||
|
let current = result;
|
||||||
|
while (parts.length > 1) {
|
||||||
|
const part = parts.shift();
|
||||||
|
current = current[part] = current[part] || {};
|
||||||
|
}
|
||||||
|
current[parts[0]] = value;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加切换密码显示的函数
|
||||||
|
function togglePassword(button) {
|
||||||
|
const input = button.previousElementSibling;
|
||||||
|
const type = input.getAttribute('type');
|
||||||
|
input.setAttribute('type', type === 'password' ? 'text' : 'password');
|
||||||
|
button.innerHTML = type === 'password' ?
|
||||||
|
'<i class="bi bi-eye-slash"></i>' :
|
||||||
|
'<i class="bi bi-eye"></i>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建带切换按钮的密码输入框
|
||||||
|
function createPasswordField(name, value, label, title = '') {
|
||||||
|
return `
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${label}</label>
|
||||||
|
<div class="password-field">
|
||||||
|
<input type="password" class="form-control"
|
||||||
|
name="${name}"
|
||||||
|
value="${value || ''}"
|
||||||
|
${name.includes('api_key') ? '' : 'required'}
|
||||||
|
title="${title || `请输入${label}`}">
|
||||||
|
<button type="button" class="toggle-password" onclick="togglePassword(this)">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加切换 API Key 字段显示的函数
|
||||||
|
function toggleAPIKeyField(index) {
|
||||||
|
const aiService = document.querySelector(`.ai-service[data-index="${index}"]`);
|
||||||
|
const typeSelect = aiService.querySelector('select[name^="ais"][name$=".type"]');
|
||||||
|
const apiKeyField = aiService.querySelector('.api-key-field');
|
||||||
|
const apiKeyInput = apiKeyField.querySelector('input');
|
||||||
|
|
||||||
|
if (typeSelect.value === 'ollama') {
|
||||||
|
apiKeyField.style.display = 'none';
|
||||||
|
if (apiKeyInput) {
|
||||||
|
apiKeyInput.value = '';
|
||||||
|
apiKeyInput.removeAttribute('required');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apiKeyField.style.display = 'block';
|
||||||
|
if (apiKeyInput) {
|
||||||
|
apiKeyInput.setAttribute('required', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主题切换功能
|
||||||
|
function setTheme(theme) {
|
||||||
|
const body = document.body;
|
||||||
|
const button = document.querySelector('.theme-switcher .dropdown-toggle i');
|
||||||
|
|
||||||
|
// 更新下拉菜单选中状态
|
||||||
|
document.querySelectorAll('.theme-switcher .dropdown-item').forEach(item => {
|
||||||
|
item.classList.toggle('active', item.dataset.theme === theme);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (theme === 'auto') {
|
||||||
|
// 检测系统主题
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
body.setAttribute('data-bs-theme', prefersDark ? 'dark' : 'light');
|
||||||
|
button.className = 'bi bi-circle-half';
|
||||||
|
} else {
|
||||||
|
body.setAttribute('data-bs-theme', theme);
|
||||||
|
button.className = theme === 'light' ? 'bi bi-sun-fill' : 'bi bi-moon-fill';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存主题设置
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化主题
|
||||||
|
function initTheme() {
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||||
|
setTheme(savedTheme);
|
||||||
|
|
||||||
|
// 监听系统主题变化
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||||
|
if (localStorage.getItem('theme') === 'auto') {
|
||||||
|
document.body.setAttribute('data-bs-theme', e.matches ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加主题切换事件监听
|
||||||
|
document.querySelectorAll('.theme-switcher .dropdown-item').forEach(item => {
|
||||||
|
item.addEventListener('click', () => setTheme(item.dataset.theme));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时初始化主题
|
||||||
|
document.addEventListener('DOMContentLoaded', initTheme);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 在 body 末尾添加 Bootstrap Icons CDN -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
|
||||||
|
</body>
|
||||||
|
</html>
|
89
utils/utils.go
Normal file
89
utils/utils.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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()
|
||||||
|
}
|
Reference in New Issue
Block a user