Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions internal/dao/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,16 @@ func (dao *ChatDAO) QueryByTenantIDAndID(tenantID string, chatID string, status
err := DB.Where("tenant_id = ? AND id = ? AND status = ?", tenantID, chatID, status).Find(&chats).Error
return chats, err
}

// NameConflictExists returns true when a *different* chat in the same tenant already uses the given name.
// excludeID may be empty (create path) or the ID of the chat being updated (patch path).
func (dao *ChatDAO) NameConflictExists(tenantID, name, excludeID, status string) (bool, error) {
query := DB.Model(&entity.Chat{}).
Where("tenant_id = ? AND LOWER(name) = LOWER(?) AND status = ?", tenantID, name, status)
if excludeID != "" {
query = query.Where("id <> ?", excludeID)
}
var count int64
err := query.Count(&count).Error
return count > 0, err
}
156 changes: 156 additions & 0 deletions internal/handler/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,159 @@ func (h *ChatHandler) GetChat(c *gin.Context) {
"message": "success",
})
}

// CreateChat creates a new chat dialog.
// @Summary Create Chat
// @Description Create a new chat dialog for the current user.
// @Tags chat
// @Accept json
// @Produce json
// @Param request body service.CreateChatRequest true "chat configuration"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/chats [post]
func (h *ChatHandler) CreateChat(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}

var req service.CreateChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
return
}

data, err := h.chatService.CreateChat(user.ID, &req)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
return
}

c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": data, "message": "success"})
}

// PatchChat partially updates an existing chat dialog (owner only).
// @Summary Patch Chat
// @Description Partially update a chat dialog. Prompt config and LLM settings are merged with existing values.
// @Tags chat
// @Accept json
// @Produce json
// @Param chat_id path string true "Chat ID"
// @Param request body service.PatchChatRequest true "fields to update"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/chats/{chat_id} [patch]
func (h *ChatHandler) PatchChat(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}

chatID := c.Param("chat_id")
if chatID == "" {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": nil, "message": "chat_id is required"})
return
}

var req service.PatchChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
return
}

data, err := h.chatService.PatchChat(user.ID, chatID, &req)
if err != nil {
msg := err.Error()
if msg == "No authorization." {
c.JSON(http.StatusOK, gin.H{"code": common.CodeAuthenticationError, "data": false, "message": msg})
return
}
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": msg})
return
}

c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": data, "message": "success"})
}

// DeleteChatByID soft-deletes a single chat (status → "0"). Owner only.
// @Summary Delete Chat
// @Description Soft-delete a chat dialog by ID.
// @Tags chat
// @Produce json
// @Param chat_id path string true "Chat ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/chats/{chat_id} [delete]
func (h *ChatHandler) DeleteChatByID(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}

chatID := c.Param("chat_id")
if chatID == "" {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": nil, "message": "chat_id is required"})
return
}

if err := h.chatService.DeleteChatByID(user.ID, chatID); err != nil {
msg := err.Error()
if msg == "No authorization." {
c.JSON(http.StatusOK, gin.H{"code": common.CodeAuthenticationError, "data": false, "message": msg})
return
}
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": msg})
return
}

c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": true, "message": "success"})
}

// CreateChatSession creates a new conversation session for a chat. Owner only.
// @Summary Create Chat Session
// @Description Create a new conversation session for the given chat.
// @Tags chat
// @Accept json
// @Produce json
// @Param chat_id path string true "Chat ID"
// @Param request body object false "session name"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/chats/{chat_id}/sessions [post]
func (h *ChatHandler) CreateChatSession(c *gin.Context) {
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}

chatID := c.Param("chat_id")
if chatID == "" {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": nil, "message": "chat_id is required"})
return
}

var body struct {
Name string `json:"name"`
}
// body is optional; ignore bind error and fall back to default name
_ = c.ShouldBindJSON(&body)

name := body.Name
if name == "" {
name = "New session"
}
Comment on lines +512 to +518
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For github.com/gin-gonic/gin v1.9.1, what error does Context.ShouldBindJSON return for (1) an empty request body and (2) malformed JSON?

💡 Result:

In gin-gonic/gin v1.9.1, the Context.ShouldBindJSON method utilizes the standard library's encoding/json package to decode the request body [1][2]. Consequently, it returns the following errors for the specified scenarios: (1) Empty Request Body: When the request body is empty, ShouldBindJSON returns an io.EOF error [3][4]. This occurs because the underlying json.Unmarshal process attempts to read from the request body stream, which is empty [4]. (2) Malformed JSON: When the JSON is malformed (e.g., missing brackets, trailing commas, or invalid syntax), ShouldBindJSON returns a *json.SyntaxError [1][2]. In some cases involving incomplete or unparseable payloads, it may also return an io.EOF or an "unexpected EOF" error [5]. It is important to note that because ShouldBindJSON reads the request body stream, subsequent attempts to read or bind the body will result in an io.EOF error [4][1][6]. If you need to bind the body multiple times, you should use Context.ShouldBindBodyWithJSON instead, which caches the body in the context [3][6].

Citations:


Reject malformed JSON payloads instead of silently defaulting the session name.

ShouldBindJSON in gin v1.9.1 returns io.EOF for an empty body, but malformed JSON returns *json.SyntaxError (and in some cases an unexpected-EOF-style error). Since the code currently discards the bind error, malformed payloads also fall through and create a session with the default "New session".

💡 Suggested change
 import (
+	"errors"
+	"io"
 	"net/http"
 	"ragflow/internal/common"
 	"strconv"
@@
 	var body struct {
 		Name string `json:"name"`
 	}
-	// body is optional; ignore bind error and fall back to default name
-	_ = c.ShouldBindJSON(&body)
+	// body is optional; malformed JSON should still fail fast
+	if err := c.ShouldBindJSON(&body); err != nil && !errors.Is(err, io.EOF) {
+		c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
+		return
+	}
 
 	name := body.Name
 	if name == "" {
 		name = "New session"
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// body is optional; ignore bind error and fall back to default name
_ = c.ShouldBindJSON(&body)
name := body.Name
if name == "" {
name = "New session"
}
// body is optional; malformed JSON should still fail fast
if err := c.ShouldBindJSON(&body); err != nil && !errors.Is(err, io.EOF) {
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": err.Error()})
return
}
name := body.Name
if name == "" {
name = "New session"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/handler/chat.go` around lines 512 - 518, Do not ignore the binding
error from c.ShouldBindJSON: capture the returned error when binding into body
and reject malformed JSON (e.g., *json.SyntaxError or unexpected EOF) with a 400
response instead of silently defaulting name; only treat an empty body (io.EOF /
no content) as acceptable and fall back to the default session name. In
practice, change the current anonymous bind call to check err :=
c.ShouldBindJSON(&body), return c.JSON/AbortWithStatusJSON(400, ...) for JSON
syntax/unmarshal errors (use errors.As or check error types) and only allow the
empty-body case to continue to the name handling where name := body.Name and
default to "New session".


data, err := h.chatService.CreateChatSession(user.ID, chatID, name)
if err != nil {
msg := err.Error()
if msg == "No authorization." {
c.JSON(http.StatusOK, gin.H{"code": common.CodeAuthenticationError, "data": false, "message": msg})
return
}
c.JSON(http.StatusOK, gin.H{"code": common.CodeDataError, "data": false, "message": msg})
return
}

c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": data, "message": "success"})
}
4 changes: 4 additions & 0 deletions internal/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,12 @@ func (r *Router) Setup(engine *gin.Engine) {
chats := v1.Group("/chats")
{
chats.GET("", r.chatHandler.ListChats)
chats.POST("", r.chatHandler.CreateChat)
chats.GET("/:chat_id", r.chatHandler.GetChat)
chats.PATCH("/:chat_id", r.chatHandler.PatchChat)
chats.DELETE("/:chat_id", r.chatHandler.DeleteChatByID)
chats.GET("/:chat_id/sessions", r.chatSessionHandler.ListChatSessions)
chats.POST("/:chat_id/sessions", r.chatHandler.CreateChatSession)
}

// Searchbot routes
Expand Down
Loading