191 lines
5.5 KiB
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 会话操作失败,请稍后重试")
|
|
}
|
|
}
|