package llm import ( "net/http" "path/filepath" "github.com/0xJacky/Nginx-UI/internal/helper" "github.com/0xJacky/Nginx-UI/internal/llm" "github.com/0xJacky/Nginx-UI/internal/nginx" "github.com/0xJacky/Nginx-UI/model" "github.com/0xJacky/Nginx-UI/query" "github.com/gin-gonic/gin" "github.com/sashabaranov/go-openai" "github.com/uozi-tech/cosy" "github.com/uozi-tech/cosy/logger" ) const TerminalAssistantPath = "__terminal_assistant__" // GetLLMSessions returns LLM sessions with optional filtering func GetLLMSessions(c *gin.Context) { g := query.LLMSession query := g.Order(g.UpdatedAt.Desc()) // Filter by type if provided if assistantType := c.Query("type"); assistantType != "" { if assistantType == "terminal" { // For terminal type, filter by terminal assistant path query = query.Where(g.Path.Eq(TerminalAssistantPath)) } else if assistantType == "nginx" { // For nginx type, exclude terminal assistant path query = query.Where(g.Path.Neq(TerminalAssistantPath)) } } else if path := c.Query("path"); path != "" { // Filter by path if provided (legacy support) // Skip path validation for terminal assistant if path != TerminalAssistantPath && !helper.IsUnderDirectory(path, nginx.GetConfPath()) { c.JSON(http.StatusForbidden, gin.H{ "message": "path is not under the nginx conf path", }) return } query = query.Where(g.Path.Eq(path)) } sessions, err := query.Find() if err != nil { cosy.ErrHandler(c, err) return } c.JSON(http.StatusOK, sessions) } // GetLLMSession returns a single session by session_id func GetLLMSession(c *gin.Context) { sessionID := c.Param("session_id") g := query.LLMSession session, err := g.Where(g.SessionID.Eq(sessionID)).First() if err != nil { cosy.ErrHandler(c, err) return } c.JSON(http.StatusOK, session) } // CreateLLMSession creates a new LLM session func CreateLLMSession(c *gin.Context) { var json struct { Title string `json:"title" binding:"required"` Path string `json:"path"` Type string `json:"type"` } if !cosy.BindAndValid(c, &json) { return } // Determine path based on type var sessionPath string if json.Type == "terminal" { sessionPath = TerminalAssistantPath } else { sessionPath = json.Path // Validate path for non-terminal types if sessionPath != "" && !helper.IsUnderDirectory(sessionPath, nginx.GetConfPath()) { c.JSON(http.StatusForbidden, gin.H{ "message": "path is not under the nginx conf path", }) return } } session := &model.LLMSession{ Title: json.Title, Path: sessionPath, Messages: []openai.ChatCompletionMessage{}, MessageCount: 0, IsActive: true, } g := query.LLMSession // When creating a new active session, deactivate all other sessions with the same path if session.IsActive && sessionPath != "" { _, err := g.Where(g.Path.Eq(sessionPath)).UpdateSimple(g.IsActive.Value(false)) if err != nil { logger.Error("Failed to deactivate other sessions:", err) // Continue anyway, this is not critical } } err := g.Create(session) if err != nil { logger.Error(err) cosy.ErrHandler(c, err) return } c.JSON(http.StatusOK, session) } // UpdateLLMSession updates an existing session func UpdateLLMSession(c *gin.Context) { sessionID := c.Param("session_id") var json struct { Title string `json:"title,omitempty"` Messages []openai.ChatCompletionMessage `json:"messages,omitempty"` IsActive *bool `json:"is_active,omitempty"` } if !cosy.BindAndValid(c, &json) { return } g := query.LLMSession session, err := g.Where(g.SessionID.Eq(sessionID)).First() if err != nil { logger.Error(err) cosy.ErrHandler(c, err) return } // Update fields if json.Title != "" { session.Title = json.Title } if json.Messages != nil { session.Messages = json.Messages session.MessageCount = len(json.Messages) } if json.IsActive != nil && *json.IsActive { session.IsActive = true // Deactivate all other sessions with the same path _, err = g.Where( g.Path.Eq(session.Path), g.SessionID.Neq(sessionID), ).UpdateSimple(g.IsActive.Value(false)) if err != nil { logger.Error("Failed to deactivate other sessions:", err) // Continue anyway, this is not critical } } else if json.IsActive != nil { session.IsActive = *json.IsActive } // Save the updated session err = g.Save(session) if err != nil { logger.Error(err) cosy.ErrHandler(c, err) return } c.JSON(http.StatusOK, session) } // DeleteLLMSession deletes a session by session_id func DeleteLLMSession(c *gin.Context) { sessionID := c.Param("session_id") g := query.LLMSession result, err := g.Where(g.SessionID.Eq(sessionID)).Delete() if err != nil { logger.Error(err) cosy.ErrHandler(c, err) return } if result.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{ "message": "Session not found", }) return } c.JSON(http.StatusOK, gin.H{ "message": "Session deleted successfully", }) } // DuplicateLLMSession duplicates an existing session func DuplicateLLMSession(c *gin.Context) { sessionID := c.Param("session_id") g := query.LLMSession originalSession, err := g.Where(g.SessionID.Eq(sessionID)).First() if err != nil { logger.Error(err) cosy.ErrHandler(c, err) return } // Create a new session with the same content newSession := &model.LLMSession{ Title: originalSession.Title + " (Copy)", Path: originalSession.Path, Messages: originalSession.Messages, MessageCount: originalSession.MessageCount, } err = g.Create(newSession) if err != nil { logger.Error(err) cosy.ErrHandler(c, err) return } c.JSON(http.StatusOK, newSession) } // GetLLMSessionByPath - 兼容性端点,基于路径获取或创建会话 func GetLLMSessionByPath(c *gin.Context) { path := c.Query("path") // Skip path validation for terminal assistant if path != TerminalAssistantPath && !helper.IsUnderDirectory(path, nginx.GetConfPath()) { c.JSON(http.StatusForbidden, gin.H{ "message": "path is not under the nginx conf path", }) return } g := query.LLMSession // 查找基于该路径的会话 session, err := g.Where(g.Path.Eq(path)).First() if err != nil { // 如果没找到,创建一个新的会话 title := "Chat for " + filepath.Base(path) session = &model.LLMSession{ Title: title, Path: path, Messages: []openai.ChatCompletionMessage{}, MessageCount: 0, IsActive: true, } // Deactivate all other sessions with the same path before creating if path != "" { _, deactivateErr := g.Where(g.Path.Eq(path)).UpdateSimple(g.IsActive.Value(false)) if deactivateErr != nil { logger.Error("Failed to deactivate other sessions:", deactivateErr) } } err = g.Create(session) if err != nil { logger.Error(err) cosy.ErrHandler(c, err) return } } // 返回兼容格式 response := struct { Name string `json:"name"` Content []openai.ChatCompletionMessage `json:"content"` }{ Name: session.Path, Content: session.Messages, } c.JSON(http.StatusOK, response) } // CreateOrUpdateLLMSessionByPath - 兼容性端点,基于路径创建或更新会话 func CreateOrUpdateLLMSessionByPath(c *gin.Context) { var json struct { FileName string `json:"file_name"` Messages []openai.ChatCompletionMessage `json:"messages"` } if !cosy.BindAndValid(c, &json) { return } // Skip path validation for terminal assistant if json.FileName != TerminalAssistantPath && !helper.IsUnderDirectory(json.FileName, nginx.GetConfPath()) { c.JSON(http.StatusForbidden, gin.H{ "message": "path is not under the nginx conf path", }) return } g := query.LLMSession // 查找或创建基于该路径的会话 session, err := g.Where(g.Path.Eq(json.FileName)).First() if err != nil { // 创建新会话 title := "Chat for " + filepath.Base(json.FileName) session = &model.LLMSession{ Title: title, Path: json.FileName, Messages: json.Messages, MessageCount: len(json.Messages), IsActive: true, } // Deactivate all other sessions with the same path before creating if json.FileName != "" { _, deactivateErr := g.Where(g.Path.Eq(json.FileName)).UpdateSimple(g.IsActive.Value(false)) if deactivateErr != nil { logger.Error("Failed to deactivate other sessions:", deactivateErr) } } err = g.Create(session) if err != nil { logger.Error(err) cosy.ErrHandler(c, err) return } } else { // 更新现有会话 session.Messages = json.Messages session.MessageCount = len(json.Messages) err = g.Save(session) if err != nil { logger.Error(err) cosy.ErrHandler(c, err) return } } c.JSON(http.StatusOK, gin.H{ "message": "ok", }) } // GenerateSessionTitle generates a title for a session based on its context func GenerateSessionTitle(c *gin.Context) { sessionID := c.Param("session_id") g := query.LLMSession session, err := g.Where(g.SessionID.Eq(sessionID)).First() if err != nil { cosy.ErrHandler(c, err) return } // Generate title based on session messages title, err := llm.GenerateSessionTitle(session.Messages) if err != nil { logger.Error("Failed to generate session title:", err) cosy.ErrHandler(c, err) return } // Update the session with the new title session.Title = title err = g.Save(session) if err != nil { logger.Error("Failed to save session with new title:", err) cosy.ErrHandler(c, err) return } c.JSON(http.StatusOK, gin.H{ "title": title, "message": "Title generated successfully", }) }