RAGFlow go API server (#13240)

# RAGFlow Go Implementation Plan 🚀

This repository tracks the progress of porting RAGFlow to Go. We'll
implement core features and provide performance comparisons between
Python and Go versions.

## Implementation Checklist

- [x] User Management APIs
- [x] Dataset Management Operations
- [x] Retrieval Test
- [x] Chat Management Operations
- [x] Infinity Go SDK

---------

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
Co-authored-by: Yingfeng Zhang <yingfeng.zhang@gmail.com>
This commit is contained in:
Jin Hai
2026-03-04 19:17:16 +08:00
committed by GitHub
parent 2508c46c8f
commit 70e9743ef1
257 changed files with 80490 additions and 6 deletions

314
internal/handler/chat.go Normal file
View File

@ -0,0 +1,314 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// ChatHandler chat handler
type ChatHandler struct {
chatService *service.ChatService
userService *service.UserService
}
// NewChatHandler create chat handler
func NewChatHandler(chatService *service.ChatService, userService *service.UserService) *ChatHandler {
return &ChatHandler{
chatService: chatService,
userService: userService,
}
}
// ListChats list chats
// @Summary List Chats
// @Description Get list of chats (dialogs) for the current user
// @Tags chat
// @Accept json
// @Produce json
// @Success 200 {object} service.ListChatsResponse
// @Router /v1/dialog/list [get]
func (h *ChatHandler) ListChats(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// List chats - default to valid status "1" (same as Python StatusEnum.VALID.value)
result, err := h.chatService.ListChats(userID, "1")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": result,
"message": "success",
})
}
// ListChatsNext list chats with advanced filtering and pagination
// @Summary List Chats Next
// @Description Get list of chats with filtering, pagination and sorting (equivalent to list_dialogs_next)
// @Tags chat
// @Accept json
// @Produce json
// @Param keywords query string false "search keywords"
// @Param page query int false "page number"
// @Param page_size query int false "items per page"
// @Param orderby query string false "order by field (default: create_time)"
// @Param desc query bool false "descending order (default: true)"
// @Param request body service.ListChatsNextRequest true "filter options including owner_ids"
// @Success 200 {object} service.ListChatsNextResponse
// @Router /v1/dialog/next [post]
func (h *ChatHandler) ListChatsNext(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// Parse query parameters
keywords := c.Query("keywords")
page := 0
if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
pageSize := 0
if pageSizeStr := c.Query("page_size"); pageSizeStr != "" {
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 {
pageSize = ps
}
}
orderby := c.DefaultQuery("orderby", "create_time")
desc := true
if descStr := c.Query("desc"); descStr != "" {
desc = descStr != "false"
}
// Parse request body for owner_ids
var req service.ListChatsNextRequest
if c.Request.ContentLength > 0 {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
}
// List chats with advanced filtering
result, err := h.chatService.ListChatsNext(userID, keywords, page, pageSize, orderby, desc, req.OwnerIDs)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": result,
"message": "success",
})
}
// SetDialog create or update a dialog
// @Summary Set Dialog
// @Description Create or update a dialog (chat). If dialog_id is provided, updates existing dialog; otherwise creates new one.
// @Tags chat
// @Accept json
// @Produce json
// @Param request body service.SetDialogRequest true "dialog configuration"
// @Success 200 {object} service.SetDialogResponse
// @Router /v1/dialog/set [post]
func (h *ChatHandler) SetDialog(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// Parse request body
var req service.SetDialogRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
// Validate required field: prompt_config
if req.PromptConfig == nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "prompt_config is required",
})
return
}
// Call service to set dialog
result, err := h.chatService.SetDialog(userID, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": result,
"message": "success",
})
}
// RemoveDialogsRequest remove dialogs request
type RemoveDialogsRequest struct {
DialogIDs []string `json:"dialog_ids" binding:"required"`
}
// RemoveChats remove/delete dialogs (soft delete by setting status to invalid)
// @Summary Remove Dialogs
// @Description Remove dialogs by setting their status to invalid. Only the owner of the dialog can perform this operation.
// @Tags chat
// @Accept json
// @Produce json
// @Param request body RemoveDialogsRequest true "dialog IDs to remove"
// @Success 200 {object} map[string]interface{}
// @Router /v1/dialog/rm [post]
func (h *ChatHandler) RemoveChats(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// Parse request body
var req RemoveDialogsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
// Call service to remove dialogs
if err := h.chatService.RemoveChats(userID, req.DialogIDs); err != nil {
// Check if it's an authorization error
if err.Error() == "only owner of chat authorized for this operation" {
c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"data": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": true,
"message": "success",
})
}

View File

@ -0,0 +1,377 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"fmt"
"io"
"net/http"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// ChatSessionHandler chat session (conversation) handler
type ChatSessionHandler struct {
chatSessionService *service.ChatSessionService
userService *service.UserService
}
// NewChatSessionHandler create chat session handler
func NewChatSessionHandler(chatSessionService *service.ChatSessionService, userService *service.UserService) *ChatSessionHandler {
return &ChatSessionHandler{
chatSessionService: chatSessionService,
userService: userService,
}
}
// SetChatSession create or update a chat session
// @Summary Set chat session
// @Description Create or update a chat session. If is_new is true, creates new chat session; otherwise updates existing one.
// @Tags chat_session
// @Accept json
// @Produce json
// @Param request body service.SetChatSessionRequest true "chat session configuration"
// @Success 200 {object} service.SetChatSessionResponse
// @Router /v1/conversation/set [post]
func (h *ChatSessionHandler) SetChatSession(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// Parse request body
var req service.SetChatSessionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
// Call service to set chat session
result, err := h.chatSessionService.SetChatSession(userID, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": result,
"message": "success",
})
}
// RemoveChatSessionsRequest remove chat sessions request
type RemoveChatSessionsRequest struct {
ConversationIDs []string `json:"conversation_ids" binding:"required"`
}
// RemoveChatSessions remove/delete chat sessions
// @Summary Remove Chat Sessions
// @Description Remove chat sessions by their IDs. Only the owner of the chat session can perform this operation.
// @Tags chat_session
// @Accept json
// @Produce json
// @Param request body RemoveChatSessionsRequest true "chat session IDs to remove"
// @Success 200 {object} map[string]interface{}
// @Router /v1/conversation/rm [post]
func (h *ChatSessionHandler) RemoveChatSessions(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// Parse request body
var req RemoveChatSessionsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
// Call service to remove chat sessions
if err := h.chatSessionService.RemoveChatSessions(userID, req.ConversationIDs); err != nil {
// Check if it's an authorization error
if err.Error() == "Only owner of chat session authorized for this operation" {
c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"data": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": true,
"message": "success",
})
}
// ListChatSessions list chat sessions for a dialog
// @Summary List Chat Sessions
// @Description Get list of chat sessions for a specific dialog
// @Tags chat_session
// @Accept json
// @Produce json
// @Param dialog_id query string true "dialog ID"
// @Success 200 {object} service.ListChatSessionsResponse
// @Router /v1/conversation/list [get]
func (h *ChatSessionHandler) ListChatSessions(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// Get dialog_id from query parameter
dialogID := c.Query("dialog_id")
if dialogID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "dialog_id is required",
})
return
}
// Call service to list chat sessions
result, err := h.chatSessionService.ListChatSessions(userID, dialogID)
if err != nil {
// Check if it's an authorization error
if err.Error() == "Only owner of dialog authorized for this operation" {
c.JSON(http.StatusForbidden, gin.H{
"code": 403,
"data": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": result.Sessions,
"message": "success",
})
}
// CompletionRequest completion request
type CompletionRequest struct {
ConversationID string `json:"conversation_id" binding:"required"`
Messages []map[string]interface{} `json:"messages" binding:"required"`
LLMID string `json:"llm_id,omitempty"`
Stream bool `json:"stream,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
TopP float64 `json:"top_p,omitempty"`
FrequencyPenalty float64 `json:"frequency_penalty,omitempty"`
PresencePenalty float64 `json:"presence_penalty,omitempty"`
MaxTokens int `json:"max_tokens,omitempty"`
}
// Completion chat completion
// @Summary Chat Completion
// @Description Send messages to the chat model and get a response. Supports streaming and non-streaming modes.
// @Tags chat_session
// @Accept json
// @Produce json
// @Param request body CompletionRequest true "completion request"
// @Success 200 {object} map[string]interface{}
// @Router /v1/conversation/completion [post]
func (h *ChatSessionHandler) Completion(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// Parse request body
var req CompletionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
// Build chat model config
chatModelConfig := make(map[string]interface{})
if req.Temperature != 0 {
chatModelConfig["temperature"] = req.Temperature
}
if req.TopP != 0 {
chatModelConfig["top_p"] = req.TopP
}
if req.FrequencyPenalty != 0 {
chatModelConfig["frequency_penalty"] = req.FrequencyPenalty
}
if req.PresencePenalty != 0 {
chatModelConfig["presence_penalty"] = req.PresencePenalty
}
if req.MaxTokens != 0 {
chatModelConfig["max_tokens"] = req.MaxTokens
}
// Process messages - filter out system messages and initial assistant messages
var processedMessages []map[string]interface{}
for i, m := range req.Messages {
role, _ := m["role"].(string)
if role == "system" {
continue
}
if role == "assistant" && len(processedMessages) == 0 {
continue
}
processedMessages = append(processedMessages, m)
_ = i
}
// Get last message ID if present
var messageID string
if len(processedMessages) > 0 {
if id, ok := processedMessages[len(processedMessages)-1]["id"].(string); ok {
messageID = id
}
}
// Call service
if req.Stream {
// Streaming response
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
// Create a channel for streaming data
streamChan := make(chan string)
go func() {
defer close(streamChan)
err := h.chatSessionService.CompletionStream(userID, req.ConversationID, processedMessages, req.LLMID, chatModelConfig, messageID, streamChan)
if err != nil {
streamChan <- fmt.Sprintf("data: %s\n\n", err.Error())
}
}()
// Stream data to client
c.Stream(func(w io.Writer) bool {
data, ok := <-streamChan
if !ok {
return false
}
c.Writer.Write([]byte(data))
return true
})
} else {
// Non-streaming response
result, err := h.chatSessionService.Completion(userID, req.ConversationID, processedMessages, req.LLMID, chatModelConfig, messageID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": result,
"message": "",
})
}
}

180
internal/handler/chunk.go Normal file
View File

@ -0,0 +1,180 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// ChunkHandler chunk handler
type ChunkHandler struct {
chunkService *service.ChunkService
userService *service.UserService
}
// NewChunkHandler create chunk handler
func NewChunkHandler(chunkService *service.ChunkService, userService *service.UserService) *ChunkHandler {
return &ChunkHandler{
chunkService: chunkService,
userService: userService,
}
}
// RetrievalTest performs retrieval test for chunks
// @Summary Retrieval Test
// @Description Test retrieval of chunks based on question and knowledge base
// @Tags chunks
// @Accept json
// @Produce json
// @Param request body service.RetrievalTestRequest true "retrieval test parameters"
// @Success 200 {object} map[string]interface{}
// @Router /v1/chunk/retrieval_test [post]
func (h *ChunkHandler) RetrievalTest(c *gin.Context) {
// Extract access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
// Bind JSON request
var req service.RetrievalTestRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
// Set default values for optional parameters
if req.Page == nil {
defaultPage := 1
req.Page = &defaultPage
}
if req.Size == nil {
defaultSize := 30
req.Size = &defaultSize
}
if req.TopK == nil {
defaultTopK := 1024
req.TopK = &defaultTopK
}
if req.UseKG == nil {
defaultUseKG := false
req.UseKG = &defaultUseKG
}
// Validate required fields
if req.Question == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "question is required",
})
return
}
if req.KbID == nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "kb_id is required",
})
return
}
// Validate kb_id type: string or []string
switch v := req.KbID.(type) {
case string:
if v == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "kb_id cannot be empty string",
})
return
}
case []interface{}:
// Convert to []string
var kbIDs []string
for _, item := range v {
if str, ok := item.(string); ok && str != "" {
kbIDs = append(kbIDs, str)
} else {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "kb_id array must contain non-empty strings",
})
return
}
}
if len(kbIDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "kb_id array cannot be empty",
})
return
}
// Convert back to interface{} for service
req.KbID = kbIDs
case []string:
// Already correct type
if len(v) == 0 {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "kb_id array cannot be empty",
})
return
}
default:
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "kb_id must be string or array of strings",
})
return
}
// Call service with user ID for permission checks
resp, err := h.chunkService.RetrievalTest(&req, user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": resp,
"message": "success",
})
}

View File

@ -0,0 +1,86 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// ConnectorHandler connector handler
type ConnectorHandler struct {
connectorService *service.ConnectorService
userService *service.UserService
}
// NewConnectorHandler create connector handler
func NewConnectorHandler(connectorService *service.ConnectorService, userService *service.UserService) *ConnectorHandler {
return &ConnectorHandler{
connectorService: connectorService,
userService: userService,
}
}
// ListConnectors list connectors
// @Summary List Connectors
// @Description Get list of connectors for the current user (equivalent to Python's list_connector)
// @Tags connector
// @Accept json
// @Produce json
// @Success 200 {object} service.ListConnectorsResponse
// @Router /connector/list [get]
func (h *ConnectorHandler) ListConnectors(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// List connectors
result, err := h.connectorService.ListConnectors(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": result.Connectors,
"message": "success",
})
}

View File

@ -0,0 +1,258 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// DocumentHandler document handler
type DocumentHandler struct {
documentService *service.DocumentService
}
// NewDocumentHandler create document handler
func NewDocumentHandler(documentService *service.DocumentService) *DocumentHandler {
return &DocumentHandler{
documentService: documentService,
}
}
// CreateDocument create document
// @Summary Create Document
// @Description Create new document
// @Tags documents
// @Accept json
// @Produce json
// @Param request body service.CreateDocumentRequest true "document info"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/documents [post]
func (h *DocumentHandler) CreateDocument(c *gin.Context) {
var req service.CreateDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
document, err := h.documentService.CreateDocument(&req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "created successfully",
"data": document,
})
}
// GetDocumentByID get document by ID
// @Summary Get Document Info
// @Description Get document details by ID
// @Tags documents
// @Accept json
// @Produce json
// @Param id path int true "document ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/documents/{id} [get]
func (h *DocumentHandler) GetDocumentByID(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid document id",
})
return
}
document, err := h.documentService.GetDocumentByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "document not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": document,
})
}
// UpdateDocument update document
// @Summary Update Document
// @Description Update document info
// @Tags documents
// @Accept json
// @Produce json
// @Param id path int true "document ID"
// @Param request body service.UpdateDocumentRequest true "update info"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/documents/{id} [put]
func (h *DocumentHandler) UpdateDocument(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid document id",
})
return
}
var req service.UpdateDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
if err := h.documentService.UpdateDocument(id, &req); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "updated successfully",
})
}
// DeleteDocument delete document
// @Summary Delete Document
// @Description Delete specified document
// @Tags documents
// @Accept json
// @Produce json
// @Param id path int true "document ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/documents/{id} [delete]
func (h *DocumentHandler) DeleteDocument(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid document id",
})
return
}
if err := h.documentService.DeleteDocument(id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "deleted successfully",
})
}
// ListDocuments document list
// @Summary Document List
// @Description Get paginated document list
// @Tags documents
// @Accept json
// @Produce json
// @Param page query int false "page number" default(1)
// @Param page_size query int false "items per page" default(10)
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/documents [get]
func (h *DocumentHandler) ListDocuments(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
documents, total, err := h.documentService.ListDocuments(page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to get documents",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"items": documents,
"total": total,
"page": page,
"page_size": pageSize,
},
})
}
// GetDocumentsByAuthorID get documents by author ID
// @Summary Get Author Documents
// @Description Get paginated document list by author ID
// @Tags documents
// @Accept json
// @Produce json
// @Param author_id path int true "author ID"
// @Param page query int false "page number" default(1)
// @Param page_size query int false "items per page" default(10)
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/authors/{author_id}/documents [get]
func (h *DocumentHandler) GetDocumentsByAuthorID(c *gin.Context) {
authorIDStr := c.Param("author_id")
authorID, err := strconv.Atoi(authorIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid author id",
})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
documents, total, err := h.documentService.GetDocumentsByAuthorID(authorID, page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to get documents",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"items": documents,
"total": total,
"page": page,
"page_size": pageSize,
},
})
}

46
internal/handler/error.go Normal file
View File

@ -0,0 +1,46 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"ragflow/internal/logger"
)
// HandleNoRoute handles requests to undefined routes
func HandleNoRoute(c *gin.Context) {
// Log the request details on server side
logger.Logger.Warn("The requested URL was not found",
zap.String("method", c.Request.Method),
zap.String("path", c.Request.URL.Path),
zap.String("query", c.Request.URL.RawQuery),
zap.String("remote_addr", c.ClientIP()),
zap.String("user_agent", c.Request.UserAgent()),
)
// Return JSON error response
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "Not Found: " + c.Request.URL.Path,
"data": nil,
"error": "Not Found",
})
}

283
internal/handler/file.go Normal file
View File

@ -0,0 +1,283 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// FileHandler file handler
type FileHandler struct {
fileService *service.FileService
userService *service.UserService
}
// NewFileHandler create file handler
func NewFileHandler(fileService *service.FileService, userService *service.UserService) *FileHandler {
return &FileHandler{
fileService: fileService,
userService: userService,
}
}
// ListFiles list files
// @Summary List Files
// @Description Get list of files for the current user with filtering, pagination and sorting
// @Tags file
// @Accept json
// @Produce json
// @Param parent_id query string false "parent folder ID"
// @Param keywords query string false "search keywords"
// @Param page query int false "page number (default: 1)"
// @Param page_size query int false "items per page (default: 15)"
// @Param orderby query string false "order by field (default: create_time)"
// @Param desc query bool false "descending order (default: true)"
// @Success 200 {object} service.ListFilesResponse
// @Router /v1/file/list [get]
func (h *FileHandler) ListFiles(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// Parse query parameters
parentID := c.Query("parent_id")
keywords := c.Query("keywords")
// Parse page (default: 1)
page := 1
if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
// Parse page_size (default: 15)
pageSize := 15
if pageSizeStr := c.Query("page_size"); pageSizeStr != "" {
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 {
pageSize = ps
}
}
// Parse orderby (default: create_time)
orderby := c.DefaultQuery("orderby", "create_time")
// Parse desc (default: true)
desc := true
if descStr := c.Query("desc"); descStr != "" {
desc = descStr != "false"
}
// List files
result, err := h.fileService.ListFiles(userID, parentID, page, pageSize, orderby, desc, keywords)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": result,
"message": "success",
})
}
// GetRootFolder gets root folder for current user
// @Summary Get Root Folder
// @Description Get or create root folder for the current user
// @Tags file
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /v1/file/root_folder [get]
func (h *FileHandler) GetRootFolder(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// Get root folder
rootFolder, err := h.fileService.GetRootFolder(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{"root_folder": rootFolder},
"message": "success",
})
}
// GetParentFolder gets parent folder of a file
// @Summary Get Parent Folder
// @Description Get parent folder of a file by file ID
// @Tags file
// @Accept json
// @Produce json
// @Param file_id query string true "file ID"
// @Success 200 {object} map[string]interface{}
// @Router /v1/file/parent_folder [get]
func (h *FileHandler) GetParentFolder(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token (for validation)
_, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
// Get file_id from query
fileID := c.Query("file_id")
if fileID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "file_id is required",
})
return
}
// Get parent folder
parentFolder, err := h.fileService.GetParentFolder(fileID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{"parent_folder": parentFolder},
"message": "success",
})
}
// GetAllParentFolders gets all parent folders in path
// @Summary Get All Parent Folders
// @Description Get all parent folders in path from file to root
// @Tags file
// @Accept json
// @Produce json
// @Param file_id query string true "file ID"
// @Success 200 {object} map[string]interface{}
// @Router /v1/file/all_parent_folder [get]
func (h *FileHandler) GetAllParentFolders(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token (for validation)
_, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
// Get file_id from query
fileID := c.Query("file_id")
if fileID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "file_id is required",
})
return
}
// Get all parent folders
parentFolders, err := h.fileService.GetAllParentFolders(fileID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{"parent_folders": parentFolders},
"message": "success",
})
}

158
internal/handler/kb.go Normal file
View File

@ -0,0 +1,158 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// KnowledgebaseHandler knowledge base handler
type KnowledgebaseHandler struct {
kbService *service.KnowledgebaseService
userService *service.UserService
}
// NewKnowledgebaseHandler create knowledge base handler
func NewKnowledgebaseHandler(kbService *service.KnowledgebaseService, userService *service.UserService) *KnowledgebaseHandler {
return &KnowledgebaseHandler{
kbService: kbService,
userService: userService,
}
}
// ListKbs list knowledge bases
// @Summary List Knowledge Bases
// @Description Get list of knowledge bases with filtering and pagination
// @Tags knowledgebase
// @Accept json
// @Produce json
// @Param keywords query string false "search keywords"
// @Param page query int false "page number"
// @Param page_size query int false "items per page"
// @Param parser_id query string false "parser ID filter"
// @Param orderby query string false "order by field"
// @Param desc query bool false "descending order"
// @Param request body service.ListKbsRequest true "filter options"
// @Success 200 {object} service.ListKbsResponse
// @Router /v1/kb/list [post]
func (h *KnowledgebaseHandler) ListKbs(c *gin.Context) {
// Parse request body - allow empty body
var req service.ListKbsRequest
if c.Request.ContentLength > 0 {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
}
// Extract parameters from query or request body with defaults
keywords := ""
if req.Keywords != nil {
keywords = *req.Keywords
} else if queryKeywords := c.Query("keywords"); queryKeywords != "" {
keywords = queryKeywords
}
page := 0
if req.Page != nil {
page = *req.Page
} else if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
pageSize := 0
if req.PageSize != nil {
pageSize = *req.PageSize
} else if pageSizeStr := c.Query("page_size"); pageSizeStr != "" {
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 {
pageSize = ps
}
}
parserID := ""
if req.ParserID != nil {
parserID = *req.ParserID
} else if queryParserID := c.Query("parser_id"); queryParserID != "" {
parserID = queryParserID
}
orderby := "update_time"
if req.Orderby != nil {
orderby = *req.Orderby
} else if queryOrderby := c.Query("orderby"); queryOrderby != "" {
orderby = queryOrderby
}
desc := true
if req.Desc != nil {
desc = *req.Desc
} else if descStr := c.Query("desc"); descStr != "" {
desc = descStr == "true"
}
var ownerIDs []string
if req.OwnerIDs != nil {
ownerIDs = *req.OwnerIDs
}
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// List knowledge bases
result, err := h.kbService.ListKbs(keywords, page, pageSize, parserID, orderby, desc, ownerIDs, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": result,
"message": "success",
})
}

247
internal/handler/llm.go Normal file
View File

@ -0,0 +1,247 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"ragflow/internal/dao"
"ragflow/internal/service"
)
// FactoryResponse represents a model provider factory
type FactoryResponse struct {
Name string `json:"name"`
Logo string `json:"logo"`
Tags string `json:"tags"`
Status string `json:"status"`
Rank string `json:"rank"`
ModelTypes []string `json:"model_types"`
}
// LLMHandler LLM handler
type LLMHandler struct {
llmService *service.LLMService
userService *service.UserService
}
// NewLLMHandler create LLM handler
func NewLLMHandler(llmService *service.LLMService, userService *service.UserService) *LLMHandler {
return &LLMHandler{
llmService: llmService,
userService: userService,
}
}
// GetMyLLMs get my LLMs
// @Summary Get My LLMs
// @Description Get LLM list for current tenant
// @Tags llm
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param include_details query string false "Include detailed fields" default(false)
// @Success 200 {object} map[string]interface{}
// @Router /v1/llm/my_llms [get]
func (h *LLMHandler) GetMyLLMs(c *gin.Context) {
// Extract token from request
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid access token",
})
return
}
// Get tenant ID from user
tenantID := user.ID
if tenantID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "User has no tenant ID",
})
return
}
// Parse include_details query parameter
includeDetailsStr := c.DefaultQuery("include_details", "false")
includeDetails := includeDetailsStr == "true"
// Get LLMs for tenant
llms, err := h.llmService.GetMyLLMs(tenantID, includeDetails)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get LLMs",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": llms,
})
}
// Factories get model provider factories
// @Summary Get Model Provider Factories
// @Description Get list of model provider factories
// @Tags llm
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {array} FactoryResponse
// @Router /v1/llm/factories [get]
func (h *LLMHandler) Factories(c *gin.Context) {
// Extract token from request
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by token
_, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid access token",
})
return
}
// Get model providers
dao := dao.NewModelProviderDAO()
providers := dao.GetAllProviders()
// Filter out unwanted providers
filtered := make([]FactoryResponse, 0)
excluded := map[string]bool{
"Youdao": true,
"FastEmbed": true,
"BAAI": true,
"Builtin": true,
}
for _, provider := range providers {
if excluded[provider.Name] {
continue
}
// Collect unique model types from LLMs
modelTypes := make(map[string]bool)
for _, llm := range provider.LLMs {
modelTypes[llm.ModelType] = true
}
// Convert to slice
modelTypeSlice := make([]string, 0, len(modelTypes))
for mt := range modelTypes {
modelTypeSlice = append(modelTypeSlice, mt)
}
// If no model types found, use defaults
if len(modelTypeSlice) == 0 {
modelTypeSlice = []string{"chat", "embedding", "rerank", "image2text", "speech2text", "tts", "ocr"}
}
filtered = append(filtered, FactoryResponse{
Name: provider.Name,
Logo: provider.Logo,
Tags: provider.Tags,
Status: provider.Status,
Rank: provider.Rank,
ModelTypes: modelTypeSlice,
})
}
c.JSON(http.StatusOK, gin.H{
"data": filtered,
})
}
// ListApp lists LLMs grouped by factory
// @Summary List LLMs
// @Description Get list of LLMs grouped by factory with availability info
// @Tags llm
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param model_type query string false "Filter by model type"
// @Success 200 {object} map[string][]service.LLMListItem
// @Router /v1/llm/list [get]
func (h *LLMHandler) ListApp(c *gin.Context) {
// Extract token from request
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
// Get tenant ID from user
tenantID := user.ID
if tenantID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "User has no tenant ID",
})
return
}
// Parse model_type query parameter
modelType := c.Query("model_type")
// Get LLM list
llms, err := h.llmService.ListLLMs(tenantID, modelType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": llms,
"message": "success",
})
}

129
internal/handler/search.go Normal file
View File

@ -0,0 +1,129 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// SearchHandler search handler
type SearchHandler struct {
searchService *service.SearchService
userService *service.UserService
}
// NewSearchHandler create search handler
func NewSearchHandler(searchService *service.SearchService, userService *service.UserService) *SearchHandler {
return &SearchHandler{
searchService: searchService,
userService: userService,
}
}
// ListSearchApps list search apps
// @Summary List Search Apps
// @Description Get list of search apps for the current user with filtering, pagination and sorting
// @Tags search
// @Accept json
// @Produce json
// @Param keywords query string false "search keywords"
// @Param page query int false "page number"
// @Param page_size query int false "items per page"
// @Param orderby query string false "order by field (default: create_time)"
// @Param desc query bool false "descending order (default: true)"
// @Param request body service.ListSearchAppsRequest true "filter options including owner_ids"
// @Success 200 {object} service.ListSearchAppsResponse
// @Router /v1/search/list [post]
func (h *SearchHandler) ListSearchApps(c *gin.Context) {
// Get access token from Authorization header
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by access token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
userID := user.ID
// Parse query parameters
keywords := c.Query("keywords")
page := 0
if pageStr := c.Query("page"); pageStr != "" {
if p, err := strconv.Atoi(pageStr); err == nil && p > 0 {
page = p
}
}
pageSize := 0
if pageSizeStr := c.Query("page_size"); pageSizeStr != "" {
if ps, err := strconv.Atoi(pageSizeStr); err == nil && ps > 0 {
pageSize = ps
}
}
orderby := c.DefaultQuery("orderby", "create_time")
desc := true
if descStr := c.Query("desc"); descStr != "" {
desc = descStr != "false"
}
// Parse request body for owner_ids
var req service.ListSearchAppsRequest
if c.Request.ContentLength > 0 {
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
}
// List search apps with filtering
result, err := h.searchService.ListSearchApps(userID, keywords, page, pageSize, orderby, desc, req.OwnerIDs)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": result,
"message": "success",
})
}

125
internal/handler/system.go Normal file
View File

@ -0,0 +1,125 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"ragflow/internal/server"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// SystemHandler system handler
type SystemHandler struct {
systemService *service.SystemService
}
// NewSystemHandler create system handler
func NewSystemHandler(systemService *service.SystemService) *SystemHandler {
return &SystemHandler{
systemService: systemService,
}
}
// Ping health check endpoint
// @Summary Ping
// @Description Simple ping endpoint
// @Tags system
// @Produce plain
// @Success 200 {string} string "pong"
// @Router /v1/system/ping [get]
func (h *SystemHandler) Ping(c *gin.Context) {
c.String(http.StatusOK, "pong")
}
// GetConfig get system configuration
// @Summary Get System Configuration
// @Description Get system configuration including register enabled status
// @Tags system
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /v1/system/config [get]
func (h *SystemHandler) GetConfig(c *gin.Context) {
config, err := h.systemService.GetConfig()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to get system configuration",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": config,
})
}
// GetConfigs get all system configurations
// @Summary Get All System Configurations
// @Description Get all system configurations from globalConfig
// @Tags system
// @Accept json
// @Produce json
// @Success 200 {object} config.Config
// @Router /v1/system/configs [get]
func (h *SystemHandler) GetConfigs(c *gin.Context) {
cfg := server.GetConfig()
if cfg == nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Configuration not initialized",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": cfg,
})
}
// GetVersion get RAGFlow version
// @Summary Get RAGFlow Version
// @Description Get the current version of the application
// @Tags system
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} map[string]interface{}
// @Router /v1/system/version [get]
func (h *SystemHandler) GetVersion(c *gin.Context) {
version, err := h.systemService.GetVersion()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to get version",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": version.Version,
})
}

135
internal/handler/tenant.go Normal file
View File

@ -0,0 +1,135 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// TenantHandler tenant handler
type TenantHandler struct {
tenantService *service.TenantService
userService *service.UserService
}
// NewTenantHandler create tenant handler
func NewTenantHandler(tenantService *service.TenantService, userService *service.UserService) *TenantHandler {
return &TenantHandler{
tenantService: tenantService,
userService: userService,
}
}
// TenantInfo get tenant information
// @Summary Get Tenant Information
// @Description Get current user's tenant information (owner tenant)
// @Tags tenants
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} map[string]interface{}
// @Router /v1/user/tenant_info [get]
func (h *TenantHandler) TenantInfo(c *gin.Context) {
// Extract token from request
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid access token",
})
return
}
// Get tenant info
tenantInfo, err := h.tenantService.GetTenantInfo(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to get tenant information",
})
return
}
if tenantInfo == nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Tenant not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": tenantInfo,
})
}
// TenantList get tenant list for current user
// @Summary Get Tenant List
// @Description Get all tenants that the current user belongs to
// @Tags tenants
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} map[string]interface{}
// @Router /v1/tenant/list [get]
func (h *TenantHandler) TenantList(c *gin.Context) {
// Extract token from request
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
// Get tenant list
tenantList, err := h.tenantService.GetTenantList(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to get tenant list",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": tenantList,
})
}

456
internal/handler/user.go Normal file
View File

@ -0,0 +1,456 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package handler
import (
"net/http"
"ragflow/internal/server"
"ragflow/internal/utility"
"strconv"
"github.com/gin-gonic/gin"
"ragflow/internal/service"
)
// UserHandler user handler
type UserHandler struct {
userService *service.UserService
}
// NewUserHandler create user handler
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{
userService: userService,
}
}
// Register user registration
// @Summary User Registration
// @Description Create new user
// @Tags users
// @Accept json
// @Produce json
// @Param request body service.RegisterRequest true "registration info"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/users/register [post]
func (h *UserHandler) Register(c *gin.Context) {
var req service.RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
user, err := h.userService.Register(&req)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "registration successful",
"data": gin.H{
"id": user.ID,
"nickname": user.Nickname,
"email": user.Email,
},
})
}
// Login user login
// @Summary User Login
// @Description User login verification
// @Tags users
// @Accept json
// @Produce json
// @Param request body service.LoginRequest true "login info"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/users/login [post]
func (h *UserHandler) Login(c *gin.Context) {
var req service.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
user, err := h.userService.Login(&req)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": err.Error(),
})
return
}
// Set Authorization header with access_token
if user.AccessToken != nil {
c.Header("Authorization", *user.AccessToken)
}
// Set CORS headers
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "*")
c.Header("Access-Control-Allow-Headers", "*")
c.Header("Access-Control-Expose-Headers", "Authorization")
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "Welcome back!",
"data": user,
})
}
// LoginByEmail user login by email
// @Summary User Login by Email
// @Description User login verification using email
// @Tags users
// @Accept json
// @Produce json
// @Param request body service.EmailLoginRequest true "login info with email"
// @Success 200 {object} map[string]interface{}
// @Router /v1/user/login [post]
func (h *UserHandler) LoginByEmail(c *gin.Context) {
var req service.EmailLoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": err.Error(),
})
return
}
user, err := h.userService.LoginByEmail(&req)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": err.Error(),
})
return
}
variables := server.GetVariables()
secretKey := variables.SecretKey
authToken, err := utility.DumpAccessToken(*user.AccessToken, secretKey)
// Set Authorization header with access_token
if user.AccessToken != nil {
c.Header("Authorization", authToken)
}
// Set CORS headers
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "*")
c.Header("Access-Control-Allow-Headers", "*")
c.Header("Access-Control-Expose-Headers", "Authorization")
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "Welcome back!",
"data": user,
})
}
// GetUserByID get user by ID
// @Summary Get User Info
// @Description Get user details by ID
// @Tags users
// @Accept json
// @Produce json
// @Param id path int true "user ID"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/users/{id} [get]
func (h *UserHandler) GetUserByID(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid user id",
})
return
}
user, err := h.userService.GetUserByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "user not found",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": user,
})
}
// ListUsers user list
// @Summary User List
// @Description Get paginated user list
// @Tags users
// @Accept json
// @Produce json
// @Param page query int false "page number" default(1)
// @Param page_size query int false "items per page" default(10)
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/users [get]
func (h *UserHandler) ListUsers(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 10
}
users, total, err := h.userService.ListUsers(page, pageSize)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "failed to get users",
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"items": users,
"total": total,
"page": page,
"page_size": pageSize,
},
})
}
// Logout user logout
// @Summary User Logout
// @Description Logout user and invalidate access token
// @Tags users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} map[string]interface{}
// @Router /v1/user/logout [post]
func (h *UserHandler) Logout(c *gin.Context) {
// Extract token from request
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid access token",
})
return
}
// Logout user
if err := h.userService.Logout(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": true,
"message": "success",
})
}
// Info get user profile information
// @Summary Get User Profile
// @Description Get current user's profile information
// @Tags users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Success 200 {object} map[string]interface{}
// @Router /v1/user/info [get]
func (h *UserHandler) Info(c *gin.Context) {
// Extract token from request
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"error": "Invalid access token",
})
return
}
// Get user profile
profile := h.userService.GetUserProfile(user)
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": profile,
})
}
// Setting update user settings
// @Summary Update User Settings
// @Description Update current user's settings
// @Tags users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param request body service.UpdateSettingsRequest true "user settings"
// @Success 200 {object} map[string]interface{}
// @Router /v1/user/setting [post]
func (h *UserHandler) Setting(c *gin.Context) {
// Extract token from request
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid access token",
})
return
}
// Parse request
var req service.UpdateSettingsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// Update user settings
if err := h.userService.UpdateUserSettings(user, &req); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "settings updated successfully",
})
}
// ChangePassword change user password
// @Summary Change User Password
// @Description Change current user's password
// @Tags users
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param request body service.ChangePasswordRequest true "password change info"
// @Success 200 {object} map[string]interface{}
// @Router /v1/user/setting/password [post]
func (h *UserHandler) ChangePassword(c *gin.Context) {
// Extract token from request
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Missing Authorization header",
})
return
}
// Get user by token
user, err := h.userService.GetUserByToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid access token",
})
return
}
// Parse request
var req service.ChangePasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// Change password
if err := h.userService.ChangePassword(user, &req); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "password changed successfully",
})
}
// GetLoginChannels get all supported authentication channels
// @Summary Get Login Channels
// @Description Get all supported OAuth authentication channels
// @Tags users
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /v1/user/login/channels [get]
func (h *UserHandler) GetLoginChannels(c *gin.Context) {
channels, err := h.userService.GetLoginChannels()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Load channels failure, error: " + err.Error(),
"data": []interface{}{},
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": channels,
})
}