Initial commit: CarrotAssistant backend (Go + Gin + SQLite, OpenAI-compatible agent runtime)

This commit is contained in:
2026-07-06 16:10:55 +08:00
commit 5f84739b18
40 changed files with 4781 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
package server
import (
"errors"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"code.littlelan.cn/CarrotAssistant/backend/internal/security"
"code.littlelan.cn/CarrotAssistant/backend/internal/store"
)
type modelConfigReq struct {
Name string `json:"name" binding:"required"`
BaseURL string `json:"base_url" binding:"required"`
APIKey string `json:"api_key"`
DefaultModel string `json:"default_model" binding:"required"`
SupportsTools *bool `json:"supports_tools"`
MaxTokens int `json:"max_tokens"`
}
// GET /admin/model-configs
func (d Deps) handleListModels(c *gin.Context) {
var list []store.ModelConfig
d.DB.Order("id DESC").Find(&list)
c.JSON(http.StatusOK, list)
}
// POST /admin/model-configs
func (d Deps) handleCreateModel(c *gin.Context) {
var req modelConfigReq
if !bind(c, &req) {
return
}
req.BaseURL = strings.TrimRight(strings.TrimSpace(req.BaseURL), "/")
enc, err := d.Crypto.Encrypt(req.APIKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加密失败"})
return
}
m := store.ModelConfig{
Name: req.Name,
BaseURL: req.BaseURL,
APIKeyEncrypted: enc,
APIKeyPreview: security.GenerateAPIKeyPreview(req.APIKey),
DefaultModel: req.DefaultModel,
SupportsTools: req.SupportsTools == nil || *req.SupportsTools,
MaxTokens: req.MaxTokens,
}
if err := d.DB.Create(&m).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
c.JSON(http.StatusConflict, gin.H{"error": "名称已存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建失败"})
return
}
d.audit(c, nil, nil, "model_config.create", map[string]any{"id": m.ID, "name": m.Name})
c.JSON(http.StatusOK, m)
}
// PUT /admin/model-configs/:id — api_key is optional on update; an empty
// value keeps the existing key, a non-empty value replaces it.
func (d Deps) handleUpdateModel(c *gin.Context) {
id := c.Param("id")
var m store.ModelConfig
if err := d.DB.First(&m, "id = ?", id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "未找到"})
return
}
var req modelConfigReq
if !bind(c, &req) {
return
}
req.BaseURL = strings.TrimRight(strings.TrimSpace(req.BaseURL), "/")
m.Name = req.Name
m.BaseURL = req.BaseURL
m.DefaultModel = req.DefaultModel
m.MaxTokens = req.MaxTokens
if req.SupportsTools != nil {
m.SupportsTools = *req.SupportsTools
}
if strings.TrimSpace(req.APIKey) != "" {
enc, err := d.Crypto.Encrypt(req.APIKey)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "加密失败"})
return
}
m.APIKeyEncrypted = enc
m.APIKeyPreview = security.GenerateAPIKeyPreview(req.APIKey)
}
if err := d.DB.Save(&m).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存失败"})
return
}
d.audit(c, nil, nil, "model_config.update", map[string]any{"id": m.ID})
c.JSON(http.StatusOK, m)
}
// DELETE /admin/model-configs/:id
func (d Deps) handleDeleteModel(c *gin.Context) {
id := c.Param("id")
res := d.DB.Delete(&store.ModelConfig{}, "id = ?", id)
if res.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
return
}
if res.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "未找到"})
return
}
d.audit(c, nil, nil, "model_config.delete", map[string]any{"id": id})
c.JSON(http.StatusOK, gin.H{"ok": true})
}