feat(ai): add local expert library

This commit is contained in:
2026-07-22 17:09:54 +08:00
parent b28a5d635c
commit 42dd0dce47
22 changed files with 3432 additions and 60 deletions

View File

@@ -15,18 +15,38 @@ import (
// 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"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
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"`
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 会话接口。
@@ -39,6 +59,8 @@ func NewHandler(service *SessionService) *Handler {
}
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)
}
@@ -70,7 +92,7 @@ func (h *Handler) create(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
return
}
session, err := h.service.Create(userID, projectIdentity, input.Title, input.Context)
session, err := h.service.CreateWithExpert(userID, projectIdentity, input.Title, input.Context, input.ExpertID)
if err != nil {
writeAIError(c, err)
return
@@ -78,6 +100,35 @@ func (h *Handler) create(c *gin.Context) {
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 {
@@ -92,7 +143,7 @@ func aiRequestContext(c *gin.Context) (uint, string, bool) {
}
func sessionDTO(session models.SenlinAgentAISession) SessionDTO {
return SessionDTO{
dto := SessionDTO{
ID: session.Identity,
ProjectID: session.ProjectIdentity,
Title: session.Title,
@@ -101,10 +152,29 @@ func sessionDTO(session models.SenlinAgentAISession) SessionDTO {
CreatedAt: session.CreatedAt.UTC(),
UpdatedAt: session.UpdatedAt.UTC(),
}
if session.Expert != nil {
dto.Expert = expertSummaryPointer(*session.Expert)
}
return dto
}
func expertSummaryPointer(expert models.SenlinAgentAIExpert) *ExpertSummaryDTO {
dto := expertSummaryDTO(expert)
return &dto
}
func expertSummaryDTO(expert models.SenlinAgentAIExpert) 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):