| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 | package llmimport (	"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 filteringfunc 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_idfunc 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 sessionfunc 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 sessionfunc 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_idfunc 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 sessionfunc 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 contextfunc 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",	})}
 |