Files
agent/backend/internal/logic/ai/handlers.go

191 lines
5.5 KiB
Go

package ai
import (
"errors"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"senlinai-agent/backend/internal/httpx"
"senlinai-agent/backend/internal/logic/auth"
"senlinai-agent/backend/internal/models"
)
// SessionDTO 是普通项目 AI 会话的公开契约,不携带数据库主键或自动创建对象的 ID。
type SessionDTO struct {
ID string `json:"id"`
ProjectID string `json:"projectId"`
Title string `json:"title"`
Context string `json:"context"`
Status string `json:"status"`
Expert *ExpertSummaryDTO `json:"expert"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type createSessionRequest struct {
Title string `json:"title"`
Context string `json:"context"`
ExpertID string `json:"expertId"`
}
type ExpertSummaryDTO struct {
ID string `json:"id"`
Slug string `json:"slug"`
Category string `json:"category"`
CategoryName string `json:"categoryName"`
Name string `json:"name"`
Description string `json:"description"`
Emoji string `json:"emoji"`
Color string `json:"color"`
}
type ExpertDetailDTO struct {
ExpertSummaryDTO
SystemPrompt string `json:"systemPrompt"`
Source string `json:"source"`
SourceLicense string `json:"sourceLicense"`
}
// Handler 注册受认证、受项目所有权保护的 AI 会话接口。
type Handler struct {
service *SessionService
}
func NewHandler(service *SessionService) *Handler {
return &Handler{service: service}
}
func (h *Handler) Register(router gin.IRouter) {
router.GET("/ai-experts", h.listExperts)
router.GET("/ai-experts/:id", h.getExpert)
router.GET("/projects/:id/ai-sessions", h.list)
router.POST("/projects/:id/ai-sessions", h.create)
}
func (h *Handler) list(c *gin.Context) {
userID, projectIdentity, ok := aiRequestContext(c)
if !ok {
return
}
sessions, err := h.service.List(userID, projectIdentity)
if err != nil {
writeAIError(c, err)
return
}
items := make([]SessionDTO, 0, len(sessions))
for _, session := range sessions {
items = append(items, sessionDTO(session))
}
c.JSON(http.StatusOK, items)
}
func (h *Handler) create(c *gin.Context) {
userID, projectIdentity, ok := aiRequestContext(c)
if !ok {
return
}
var input createSessionRequest
if err := c.ShouldBindJSON(&input); err != nil {
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
return
}
session, err := h.service.CreateWithExpert(userID, projectIdentity, input.Title, input.Context, input.ExpertID)
if err != nil {
writeAIError(c, err)
return
}
c.JSON(http.StatusCreated, sessionDTO(*session))
}
func (h *Handler) listExperts(c *gin.Context) {
experts, err := h.service.ListExperts(ExpertFilter{Category: c.Query("category"), Query: c.Query("q")})
if err != nil {
writeAIError(c, err)
return
}
items := make([]ExpertSummaryDTO, 0, len(experts))
for _, expert := range experts {
items = append(items, expertSummaryDTO(expert))
}
c.JSON(http.StatusOK, items)
}
func (h *Handler) getExpert(c *gin.Context) {
identity, ok := httpx.IdentityParam(c, "id")
if !ok {
return
}
expert, err := h.service.GetExpert(identity)
if err != nil {
writeAIError(c, err)
return
}
c.JSON(http.StatusOK, ExpertDetailDTO{
ExpertSummaryDTO: expertSummaryDTO(*expert),
SystemPrompt: expert.SystemPrompt, Source: expert.Source, SourceLicense: expert.SourceLicense,
})
}
func aiRequestContext(c *gin.Context) (uint, string, bool) {
userID, ok := auth.CurrentUserID(c)
if !ok {
httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效")
return 0, "", false
}
projectIdentity, ok := httpx.IdentityParam(c, "id")
if !ok {
return 0, "", false
}
return userID, projectIdentity, true
}
func sessionDTO(session models.SaAISession) SessionDTO {
dto := SessionDTO{
ID: session.Identity,
ProjectID: session.ProjectIdentity,
Title: session.Title,
Context: session.Context,
Status: aiSessionStatus(session),
CreatedAt: session.CreatedAt.UTC(),
UpdatedAt: session.UpdatedAt.UTC(),
}
if session.Expert != nil {
dto.Expert = expertSummaryPointer(*session.Expert)
}
return dto
}
func expertSummaryPointer(expert models.SaAIExpertItem) *ExpertSummaryDTO {
dto := expertSummaryDTO(expert)
return &dto
}
func expertSummaryDTO(expert models.SaAIExpertItem) ExpertSummaryDTO {
return ExpertSummaryDTO{
ID: expert.Identity, Slug: expert.Slug, Category: expert.Category,
CategoryName: expert.CategoryName, Name: expert.Name, Description: expert.Description,
Emoji: expert.Emoji, Color: expert.Color,
}
}
func writeAIError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrExpertNotFound):
httpx.Error(c, http.StatusNotFound, "expert_not_found", "专家不存在或已停用")
case errors.Is(err, gorm.ErrRecordNotFound):
httpx.Error(c, http.StatusNotFound, "not_found", "项目不存在或无权访问")
case errors.Is(err, ErrInvalidSession):
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请输入 AI 会话标题")
case errors.Is(err, ErrAIRateLimited):
httpx.Error(c, http.StatusTooManyRequests, "ai_rate_limited", "AI 请求过于频繁,请稍后重试")
case errors.Is(err, ErrAIKeyMissing):
httpx.Error(c, http.StatusServiceUnavailable, "ai_key_missing", "尚未配置可用的 AI 密钥")
default:
log.Printf("ai session request failed: %v", err)
httpx.Error(c, http.StatusInternalServerError, "internal_error", "AI 会话操作失败,请稍后重试")
}
}