refactor: Unify /url route and file URL handling
This commit is contained in:
+13
-48
@@ -188,51 +188,15 @@ func (c *Client) CreateKnowledgeFromFile(ctx context.Context,
|
||||
return &response.Data, nil
|
||||
}
|
||||
|
||||
// CreateKnowledgeFromURL creates a knowledge entry from a web URL
|
||||
func (c *Client) CreateKnowledgeFromURL(
|
||||
ctx context.Context,
|
||||
knowledgeBaseID string,
|
||||
url string,
|
||||
enableMultimodel *bool,
|
||||
title string,
|
||||
) (*Knowledge, error) {
|
||||
path := fmt.Sprintf("/api/v1/knowledge-bases/%s/knowledge/url", knowledgeBaseID)
|
||||
|
||||
reqBody := struct {
|
||||
URL string `json:"url"`
|
||||
EnableMultimodel *bool `json:"enable_multimodel"`
|
||||
Title string `json:"title"`
|
||||
}{
|
||||
URL: url,
|
||||
EnableMultimodel: enableMultimodel,
|
||||
Title: title,
|
||||
}
|
||||
|
||||
resp, err := c.doRequest(ctx, http.MethodPost, path, reqBody, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response KnowledgeResponse
|
||||
if resp.StatusCode == http.StatusConflict {
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
return &response.Data, ErrDuplicateURL
|
||||
} else if err := parseResponse(resp, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response.Data, nil
|
||||
}
|
||||
|
||||
// CreateKnowledgeFromFileURLRequest contains the parameters for creating a knowledge entry from a remote file URL
|
||||
type CreateKnowledgeFromFileURLRequest struct {
|
||||
// URL is the remote file URL to download (must be http/https, required)
|
||||
// CreateKnowledgeFromURLRequest contains the parameters for creating a knowledge entry from a URL.
|
||||
// When FileName or FileType is provided (or the URL path has a known file extension such as .pdf/.docx/.doc/.txt/.md),
|
||||
// the server automatically switches to file-download mode instead of web-page crawling.
|
||||
type CreateKnowledgeFromURLRequest struct {
|
||||
// URL is the target URL (required)
|
||||
URL string `json:"url"`
|
||||
// FileName is the optional file name; if empty, the server resolves it from Content-Disposition or URL path
|
||||
// FileName is the optional file name; used to hint file-download mode when URL has no extension
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
// FileType is the optional file type (e.g. "pdf"); if empty, the server resolves it from the file name
|
||||
// FileType is the optional file type (e.g. "pdf"); used to hint file-download mode
|
||||
FileType string `json:"file_type,omitempty"`
|
||||
// EnableMultimodel is the optional flag to enable multimodal processing
|
||||
EnableMultimodel *bool `json:"enable_multimodel,omitempty"`
|
||||
@@ -242,14 +206,15 @@ type CreateKnowledgeFromFileURLRequest struct {
|
||||
TagID string `json:"tag_id,omitempty"`
|
||||
}
|
||||
|
||||
// CreateKnowledgeFromFileURL creates a knowledge entry from a remote file URL.
|
||||
// The server downloads the file (.txt/.md/.pdf/.docx/.doc) and parses it asynchronously.
|
||||
func (c *Client) CreateKnowledgeFromFileURL(
|
||||
// CreateKnowledgeFromURL creates a knowledge entry from a URL.
|
||||
// When req.FileName or req.FileType is provided (or the URL path has a known file extension),
|
||||
// the server automatically switches to file-download mode instead of web-page crawling.
|
||||
func (c *Client) CreateKnowledgeFromURL(
|
||||
ctx context.Context,
|
||||
knowledgeBaseID string,
|
||||
req CreateKnowledgeFromFileURLRequest,
|
||||
req CreateKnowledgeFromURLRequest,
|
||||
) (*Knowledge, error) {
|
||||
path := fmt.Sprintf("/api/v1/knowledge-bases/%s/knowledge/file-url", knowledgeBaseID)
|
||||
path := fmt.Sprintf("/api/v1/knowledge-bases/%s/knowledge/url", knowledgeBaseID)
|
||||
|
||||
reqBody := req
|
||||
|
||||
|
||||
@@ -367,11 +367,32 @@ func (s *knowledgeService) CreateKnowledgeFromFile(ctx context.Context,
|
||||
|
||||
// CreateKnowledgeFromURL creates a knowledge entry from a URL source
|
||||
// tagID is optional - when provided, the knowledge will be assigned to the specified tag/category.
|
||||
// isFileURL reports whether the given URL should be treated as a direct file download.
|
||||
// Priority: URL path has a known file extension first, then fall back to user-provided fileName/fileType hints.
|
||||
func isFileURL(rawURL, fileName, fileType string) bool {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err == nil {
|
||||
ext := strings.ToLower(strings.TrimPrefix(path.Ext(u.Path), "."))
|
||||
if ext != "" && allowedFileURLExtensions[ext] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Fall back to user-provided hints
|
||||
return fileName != "" || fileType != ""
|
||||
}
|
||||
|
||||
func (s *knowledgeService) CreateKnowledgeFromURL(ctx context.Context,
|
||||
kbID string, url string, enableMultimodel *bool, title string, tagID string,
|
||||
kbID string, rawURL string, fileName string, fileType string, enableMultimodel *bool, title string, tagID string,
|
||||
) (*types.Knowledge, error) {
|
||||
logger.Info(ctx, "Start creating knowledge from URL")
|
||||
logger.Infof(ctx, "Knowledge base ID: %s, URL: %s", kbID, url)
|
||||
logger.Infof(ctx, "Knowledge base ID: %s, URL: %s", kbID, rawURL)
|
||||
|
||||
// Route to file_url logic when the URL points to a downloadable file
|
||||
if isFileURL(rawURL, fileName, fileType) {
|
||||
return s.createKnowledgeFromFileURL(ctx, kbID, rawURL, fileName, fileType, enableMultimodel, title, tagID)
|
||||
}
|
||||
|
||||
url := rawURL
|
||||
|
||||
// Get knowledge base configuration
|
||||
logger.Info(ctx, "Getting knowledge base configuration")
|
||||
@@ -540,10 +561,9 @@ func extractFileNameFromContentDisposition(header string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// CreateKnowledgeFromFileURL creates a knowledge entry by downloading a file from a remote URL.
|
||||
// Sync phase: URL format + SSRF validation only (no HEAD request).
|
||||
// Async phase: download file to temp dir, pass binary to docreader.
|
||||
func (s *knowledgeService) CreateKnowledgeFromFileURL(
|
||||
// createKnowledgeFromFileURL is the internal implementation for file URL knowledge creation.
|
||||
// Called by CreateKnowledgeFromURL when the URL is detected as a direct file download.
|
||||
func (s *knowledgeService) createKnowledgeFromFileURL(
|
||||
ctx context.Context,
|
||||
kbID string,
|
||||
fileURL string,
|
||||
|
||||
@@ -366,12 +366,12 @@ func (h *KnowledgeHandler) CreateKnowledgeFromFile(c *gin.Context) {
|
||||
|
||||
// CreateKnowledgeFromURL godoc
|
||||
// @Summary 从URL创建知识
|
||||
// @Description 从指定URL抓取内容并创建知识条目
|
||||
// @Description 从指定URL抓取内容并创建知识条目。当提供 file_name/file_type 或 URL 路径含已知文件扩展名时,自动切换为文件下载模式
|
||||
// @Tags 知识管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param request body object{url=string,enable_multimodel=bool,title=string,tag_id=string} true "URL请求"
|
||||
// @Param request body object{url=string,file_name=string,file_type=string,enable_multimodel=bool,title=string,tag_id=string} true "URL请求"
|
||||
// @Success 201 {object} map[string]interface{} "创建的知识"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Failure 409 {object} map[string]interface{} "URL重复"
|
||||
@@ -399,6 +399,8 @@ func (h *KnowledgeHandler) CreateKnowledgeFromURL(c *gin.Context) {
|
||||
// Parse URL from request body
|
||||
var req struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
FileName string `json:"file_name"`
|
||||
FileType string `json:"file_type"`
|
||||
EnableMultimodel *bool `json:"enable_multimodel"`
|
||||
Title string `json:"title"`
|
||||
TagID string `json:"tag_id"`
|
||||
@@ -409,98 +411,24 @@ func (h *KnowledgeHandler) CreateKnowledgeFromURL(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Received URL request: %s", secutils.SanitizeForLog(req.URL))
|
||||
logger.Infof(
|
||||
ctx,
|
||||
logger.Infof(ctx, "Received URL request: %s, file_name: %s, file_type: %s",
|
||||
secutils.SanitizeForLog(req.URL),
|
||||
secutils.SanitizeForLog(req.FileName),
|
||||
secutils.SanitizeForLog(req.FileType),
|
||||
)
|
||||
logger.Infof(ctx,
|
||||
"Creating knowledge from URL, knowledge base ID: %s, URL: %s",
|
||||
secutils.SanitizeForLog(kbID),
|
||||
secutils.SanitizeForLog(req.URL),
|
||||
)
|
||||
|
||||
// Create knowledge entry from the URL
|
||||
knowledge, err := h.kgService.CreateKnowledgeFromURL(ctx, kbID, req.URL, req.EnableMultimodel, req.Title, req.TagID)
|
||||
knowledge, err := h.kgService.CreateKnowledgeFromURL(ctx, kbID, req.URL, req.FileName, req.FileType, req.EnableMultimodel, req.Title, req.TagID)
|
||||
// Check for duplicate knowledge error
|
||||
if err != nil {
|
||||
if h.handleDuplicateKnowledgeError(c, err, knowledge, "url") {
|
||||
return
|
||||
}
|
||||
logger.ErrorWithFields(ctx, err, nil)
|
||||
c.Error(errors.NewInternalServerError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(
|
||||
ctx,
|
||||
"Knowledge created successfully from URL, ID: %s, title: %s",
|
||||
secutils.SanitizeForLog(knowledge.ID),
|
||||
secutils.SanitizeForLog(knowledge.Title),
|
||||
)
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"success": true,
|
||||
"data": knowledge,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateKnowledgeFromFileURL godoc
|
||||
// @Summary 从文件资源链接创建知识
|
||||
// @Description 提供远程文件链接(.txt/.md/.pdf/.docx/.doc),后台下载并解析创建知识条目
|
||||
// @Tags 知识管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "知识库ID"
|
||||
// @Param request body object{url=string,file_name=string,file_type=string,enable_multimodel=bool,title=string,tag_id=string} true "文件URL请求"
|
||||
// @Success 201 {object} map[string]interface{} "创建的知识"
|
||||
// @Failure 400 {object} errors.AppError "请求参数错误"
|
||||
// @Failure 409 {object} map[string]interface{} "文件重复"
|
||||
// @Security Bearer
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /knowledge-bases/{id}/knowledge/file-url [post]
|
||||
func (h *KnowledgeHandler) CreateKnowledgeFromFileURL(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
logger.Info(ctx, "Start creating knowledge from file URL")
|
||||
|
||||
// Validate access to the knowledge base (only owner or admin/editor can create)
|
||||
_, kbID, effectiveTenantID, permission, err := h.validateKnowledgeBaseAccess(c)
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, types.TenantIDContextKey, effectiveTenantID)
|
||||
|
||||
// Check write permission
|
||||
if permission != types.OrgRoleAdmin && permission != types.OrgRoleEditor {
|
||||
c.Error(errors.NewForbiddenError("No permission to create knowledge"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
FileName string `json:"file_name"`
|
||||
FileType string `json:"file_type"`
|
||||
EnableMultimodel *bool `json:"enable_multimodel"`
|
||||
Title string `json:"title"`
|
||||
TagID string `json:"tag_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logger.Error(ctx, "Failed to parse file URL request", err)
|
||||
c.Error(errors.NewBadRequestError(err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Received file URL request: %s, file_name: %s, file_type: %s",
|
||||
secutils.SanitizeForLog(req.URL),
|
||||
secutils.SanitizeForLog(req.FileName),
|
||||
secutils.SanitizeForLog(req.FileType),
|
||||
)
|
||||
|
||||
knowledge, err := h.kgService.CreateKnowledgeFromFileURL(
|
||||
ctx, kbID, req.URL, req.FileName, req.FileType, req.EnableMultimodel, req.Title, req.TagID,
|
||||
)
|
||||
if err != nil {
|
||||
if h.handleDuplicateKnowledgeError(c, err, knowledge, "file_url") {
|
||||
return
|
||||
}
|
||||
if appErr, ok := errors.IsAppError(err); ok {
|
||||
c.Error(appErr)
|
||||
return
|
||||
@@ -510,7 +438,9 @@ func (h *KnowledgeHandler) CreateKnowledgeFromFileURL(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof(ctx, "Knowledge created successfully from file URL, ID: %s, title: %s",
|
||||
logger.Infof(
|
||||
ctx,
|
||||
"Knowledge created successfully from URL, ID: %s, title: %s",
|
||||
secutils.SanitizeForLog(knowledge.ID),
|
||||
secutils.SanitizeForLog(knowledge.Title),
|
||||
)
|
||||
|
||||
@@ -148,10 +148,8 @@ func RegisterKnowledgeRoutes(r *gin.RouterGroup, handler *handler.KnowledgeHandl
|
||||
{
|
||||
// 从文件创建知识
|
||||
kb.POST("/file", handler.CreateKnowledgeFromFile)
|
||||
// 从URL创建知识
|
||||
// 从URL创建知识(支持网页URL和文件URL,传 file_name/file_type 或 URL 含已知扩展名时自动切换为文件下载模式)
|
||||
kb.POST("/url", handler.CreateKnowledgeFromURL)
|
||||
// 从文件资源链接创建知识
|
||||
kb.POST("/file-url", handler.CreateKnowledgeFromFileURL)
|
||||
// 手工 Markdown 录入
|
||||
kb.POST("/manual", handler.CreateManualKnowledge)
|
||||
// 获取知识库下的知识列表
|
||||
|
||||
@@ -23,22 +23,13 @@ type KnowledgeService interface {
|
||||
tagID string,
|
||||
) (*types.Knowledge, error)
|
||||
// CreateKnowledgeFromURL creates knowledge from a URL.
|
||||
// When fileName or fileType is provided (or the URL path has a known file extension),
|
||||
// the URL is treated as a direct file download instead of a web page crawl.
|
||||
// tagID is optional - when provided, the knowledge will be assigned to the specified tag/category.
|
||||
CreateKnowledgeFromURL(
|
||||
ctx context.Context,
|
||||
kbID string,
|
||||
url string,
|
||||
enableMultimodel *bool,
|
||||
title string,
|
||||
tagID string,
|
||||
) (*types.Knowledge, error)
|
||||
// CreateKnowledgeFromFileURL creates knowledge by downloading a file from a remote URL.
|
||||
// fileName and fileType are optional - when not provided, they will be inferred from the URL or Content-Disposition header.
|
||||
// tagID is optional - when provided, the knowledge will be assigned to the specified tag/category.
|
||||
CreateKnowledgeFromFileURL(
|
||||
ctx context.Context,
|
||||
kbID string,
|
||||
fileURL string,
|
||||
fileName string,
|
||||
fileType string,
|
||||
enableMultimodel *bool,
|
||||
|
||||
Reference in New Issue
Block a user