diff --git a/internal/cli/user_command.go b/internal/cli/user_command.go index 5651064006..7b5e0e40d7 100644 --- a/internal/cli/user_command.go +++ b/internal/cli/user_command.go @@ -2623,12 +2623,7 @@ func (c *CLI) DevUpdateChunkCommand(commandCount int, cmd *Command) (ResponseIf, return nil, fmt.Errorf("invalid JSON body: %w", err) } - // Add IDs to payload - payload["dataset_id"] = datasetID - payload["document_id"] = docID - payload["chunk_id"] = chunkID - - resp, err := httpClient.Request(commandCount, "POST", "/chunk/update", httpClient.AuthKind(), nil, payload) + resp, err := httpClient.Request(commandCount, "PATCH", utils.APIPath("/datasets", datasetID, "documents", docID, "chunks", chunkID), httpClient.AuthKind(), nil, payload) if err != nil { return nil, fmt.Errorf("failed to update chunk: %w", err) } diff --git a/internal/cli/user_command_test.go b/internal/cli/user_command_test.go new file mode 100644 index 0000000000..79dbc851f9 --- /dev/null +++ b/internal/cli/user_command_test.go @@ -0,0 +1,79 @@ +// +// 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 cli + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" +) + +func TestDevUpdateChunkCommandUsesChunkResource(t *testing.T) { + updated := make(chan struct{}, 1) + client := newTestHTTPClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.URL.Path { + case "/api/v1/datasets": + if r.Method != http.MethodGet { + t.Errorf("dataset lookup method = %s, want GET", r.Method) + } + _, _ = fmt.Fprint(w, `{"code":0,"data":[{"id":"dataset-1","name":"test"}]}`) + case "/api/v1/datasets/dataset-1/documents/doc-1/chunks/chunk-1": + if r.Method != http.MethodPatch { + t.Errorf("update method = %s, want PATCH", r.Method) + } + var payload map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + t.Errorf("decode update: %v", err) + } + if payload["content"] != "updated" || payload["available"] != false { + t.Errorf("unexpected update payload: %#v", payload) + } + updated <- struct{}{} + _, _ = fmt.Fprint(w, `{"code":0,"message":"success"}`) + default: + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + http.NotFound(w, r) + } + })) + client.useAPIKey = true + c := &CLI{ + Config: &CommandLineConfig{ + CLIMode: APIMode, + APIClientConfig: APIModeConfig{CurrentAPIServer: "test"}, + }, + APIServerClientMap: map[string]*HTTPClient{"test": client}, + } + result, err := c.DevUpdateChunkCommand(1, &Command{Params: map[string]interface{}{ + "dataset_name": "test", + "doc_id": "doc-1", + "chunk_id": "chunk-1", + "json_body": `{"content":"updated","available":false}`, + }}) + if err != nil { + t.Fatal(err) + } + select { + case <-updated: + default: + t.Fatal("chunk update was not sent") + } + if response, ok := result.(*SimpleResponse); !ok || response.Code != 0 { + t.Fatalf("unexpected response: %#v", result) + } +} diff --git a/internal/handler/chunk.go b/internal/handler/chunk.go index d148fbfc57..e6ec5a2e36 100644 --- a/internal/handler/chunk.go +++ b/internal/handler/chunk.go @@ -361,50 +361,6 @@ func (h *ChunkHandler) StopParsing(c *gin.Context) { common.SuccessWithData(c, data, message) } -// List retrieves chunks for a document. -// @Summary List Chunks -// @Description Retrieve paginated chunks for a document with optional filtering. -// @Tags chunks -// @Param request body service.ListChunksRequest true "List chunks parameters" -// @Success 200 {object} map[string]interface{} -// @Router /api/v1/chunk/list [post] -func (h *ChunkHandler) List(c *gin.Context) { - user, errorCode, errorMessage := GetUser(c) - if errorCode != common.CodeSuccess { - common.ErrorWithCode(c, errorCode, errorMessage) - return - } - - // Bind JSON request - var req service.ListChunksRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, 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 - } - - ctx := c.Request.Context() - resp, err := h.chunkService.List(ctx, &req, user.ID) - if err != nil { - if respondPermissionErrorIf(c, err, false) { - return - } - common.ResponseWithHttpCodeData(c, http.StatusInternalServerError, 500, nil, err.Error()) - return - } - - common.SuccessWithData(c, resp, "success") -} - // SwitchChunks enable or disable a chunk func (h *ChunkHandler) SwitchChunks(c *gin.Context) { user, errorCode, errorMessage := GetUser(c) @@ -539,7 +495,7 @@ func optionalBodyString(rawBody map[string]interface{}, field, typeMessage strin // @Tags chunks // @Param request body service.UpdateChunkRequest true "update chunk" // @Success 200 {object} map[string]interface{} -// @Router /api/v1/chunk/update [post] +// @Router /api/v1/datasets/{dataset_id}/documents/{document_id}/chunks/{chunk_id} [patch] func (h *ChunkHandler) UpdateChunk(c *gin.Context) { user, errorCode, errorMessage := GetUser(c) if errorCode != common.CodeSuccess { @@ -547,7 +503,7 @@ func (h *ChunkHandler) UpdateChunk(c *gin.Context) { return } - // Validate allowed update fields and get IDs from body + // Decode the update fields. body, err := io.ReadAll(c.Request.Body) var rawBody map[string]interface{} if err == nil { diff --git a/internal/handler/document.go b/internal/handler/document.go index abcf08dc05..ae8355d3d4 100644 --- a/internal/handler/document.go +++ b/internal/handler/document.go @@ -66,9 +66,6 @@ type documentServiceIface interface { GetDocumentImageForDocument(ctx context.Context, userID, docID, imageID string) ([]byte, error) GetDocumentThumbnail(ctx context.Context, userID, docID string) ([]byte, error) GetMetadataSummary(ctx context.Context, kbID string, docIDs []string) (map[string]interface{}, error) - SetDocumentMetadata(ctx context.Context, docID string, meta map[string]interface{}) error - DeleteDocumentMetadata(ctx context.Context, docID string, keys []string) error - DeleteDocumentAllMetadata(ctx context.Context, docID string) error GetDocumentMetadataByID(ctx context.Context, docID string) (map[string]interface{}, error) GetDocumentArtifact(ctx context.Context, filename, userID string) (*document.ArtifactResponse, error) GetDocumentPreview(ctx context.Context, userID, docID string) (*document.DocumentPreview, error) @@ -1311,137 +1308,6 @@ func stringValue(value *string) string { return *value } -// MetadataSummary handles the metadata summary request -func (h *DocumentHandler) MetadataSummary(c *gin.Context) { - user, errorCode, errorMessage := GetUser(c) - if errorCode != common.CodeSuccess { - common.ErrorWithCode(c, errorCode, errorMessage) - return - } - - var requestBody struct { - KBID string `json:"kb_id" binding:"required"` - DocIDs []string `json:"doc_ids"` - } - - if err := c.ShouldBindJSON(&requestBody); err != nil { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, "kb_id is required") - return - } - - kbID := requestBody.KBID - if kbID == "" { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, "kb_id is required") - return - } - - ctx := c.Request.Context() - if err := h.datasetService.CheckAccess(ctx, permission.Subject{UserID: user.ID}, kbID, permission.OperationRead); err != nil { - respondHTTPPermissionError(c, err, false) - return - } - - summary, err := h.documentService.GetMetadataSummary(ctx, kbID, requestBody.DocIDs) - if err != nil { - common.ResponseWithHttpCodeData(c, http.StatusInternalServerError, 1, nil, "Failed to get metadata summary: "+err.Error()) - return - } - - common.SuccessWithData(c, gin.H{"summary": summary}, "success") -} - -// SetMetaRequest represents the request for setting document metadata -type SetMetaRequest struct { - DocID string `json:"doc_id" binding:"required"` - Meta string `json:"meta" binding:"required"` -} - -// SetMeta handles the set metadata request for a document -// @Summary Set Document Metadata -// @Description Set metadata for a specific document -// @Tags documents -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param request body SetMetaRequest true "metadata info" -// @Success 200 {object} map[string]interface{} -// @Router /v1/document/set_meta [post] -func (h *DocumentHandler) SetMeta(c *gin.Context) { - user, errorCode, errorMessage := GetUser(c) - if errorCode != common.CodeSuccess { - common.ErrorWithCode(c, errorCode, errorMessage) - return - } - - var req SetMetaRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, err.Error()) - return - } - - if req.DocID == "" { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, "doc_id is required") - return - } - - // Parse meta JSON string - var meta map[string]interface{} - if err := json.Unmarshal([]byte(req.Meta), &meta); err != nil { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, "Json syntax error: "+err.Error()) - return - } - - if meta == nil { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, "meta is required") - return - } - - // Validate meta values - must be str, int, float, or list of those - for k, v := range meta { - switch val := v.(type) { - case string, int, float64: - // Valid - case []interface{}: - for _, item := range val { - if _, ok := item.(string); !ok { - if _, ok = item.(float64); !ok { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, fmt.Sprintf("Unsupported type in list for key %s: %T", k, item)) - return - } - } - } - default: - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, fmt.Sprintf("Unsupported type for key %s: %T", k, v)) - return - } - } - - // Authorization: user must be able to access the document's dataset. - ctx := c.Request.Context() - doc, err := h.documentService.GetDocumentByID(ctx, req.DocID) - if err != nil { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, "document not found") - return - } - if err := h.datasetService.CheckAccess(ctx, permission.Subject{UserID: user.ID}, doc.KbID, permission.OperationUpdate); err != nil { - respondPermissionError(c, err, false) - return - } - - err = h.documentService.SetDocumentMetadata(ctx, req.DocID, meta) - if err != nil { - errMsg := err.Error() - if strings.Contains(errMsg, "no such document") || strings.Contains(errMsg, "document not found") { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, errMsg) - } else { - common.ResponseWithHttpCodeData(c, http.StatusInternalServerError, 1, nil, "Failed to set metadata: "+errMsg) - } - return - } - - common.SuccessWithData(c, true, "success") -} - // Ingest handles document ingestion // @Summary Ingest Document // @Description Ingest a document for processing @@ -1477,93 +1343,6 @@ func (h *DocumentHandler) Ingest(c *gin.Context) { common.SuccessWithData(c, true, "success") } -// DeleteMetaRequest represents the request for deleting document metadata -type DeleteMetaRequest struct { - DocID string `json:"doc_id" binding:"required"` - Keys string `json:"keys"` // optional - if provided, deletes specific keys; otherwise deletes entire document metadata -} - -// DeleteMeta handles the delete metadata request for a document -// If Keys is provided, deletes specific metadata keys; otherwise deletes entire document metadata -// @Summary Delete Document Metadata -// @Description Delete metadata keys or entire document metadata for a specific document -// @Tags documents -// @Accept json -// @Produce json -// @Security ApiKeyAuth -// @Param request body DeleteMetaRequest true "metadata keys to delete or empty to delete all" -// @Success 200 {object} map[string]interface{} -// @Router /api/v1/document/delete_meta [post] -func (h *DocumentHandler) DeleteMeta(c *gin.Context) { - user, errorCode, errorMessage := GetUser(c) - if errorCode != common.CodeSuccess { - common.ErrorWithCode(c, errorCode, errorMessage) - return - } - - var req DeleteMetaRequest - if err := c.ShouldBindJSON(&req); err != nil { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, err.Error()) - return - } - - if req.DocID == "" { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, "doc_id is required") - return - } - - ctx := c.Request.Context() - doc, err := h.documentService.GetDocumentByID(ctx, req.DocID) - if err != nil { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, "document not found") - return - } - if err := h.datasetService.CheckAccess(ctx, permission.Subject{UserID: user.ID}, doc.KbID, permission.OperationUpdate); err != nil { - respondPermissionError(c, err, false) - return - } - - // If Keys is provided, parse and delete specific keys; otherwise delete entire document metadata - if req.Keys != "" { - // Parse keys JSON string - expected to be a list of key names to delete - var keys []string - if err = json.Unmarshal([]byte(req.Keys), &keys); err != nil { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, "Json syntax error: "+err.Error()) - return - } - - if keys == nil || len(keys) == 0 { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, "keys list is required") - return - } - - err = h.documentService.DeleteDocumentMetadata(ctx, req.DocID, keys) - if err != nil { - errMsg := err.Error() - if strings.Contains(errMsg, "no such document") || strings.Contains(errMsg, "document not found") { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, errMsg) - } else { - common.ResponseWithHttpCodeData(c, http.StatusInternalServerError, 1, nil, "Failed to delete metadata: "+errMsg) - } - return - } - } else { - // Delete entire document metadata - err = h.documentService.DeleteDocumentAllMetadata(ctx, req.DocID) - if err != nil { - errMsg := err.Error() - if strings.Contains(errMsg, "no such document") || strings.Contains(errMsg, "document not found") { - common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 1, nil, errMsg) - } else { - common.ResponseWithHttpCodeData(c, http.StatusInternalServerError, 1, nil, "Failed to delete metadata: "+errMsg) - } - return - } - } - - common.SuccessWithData(c, true, "success") -} - type ListIngestionsRequest struct { DatasetID *string `json:"dataset_id"` } diff --git a/internal/handler/document_test.go b/internal/handler/document_test.go index 3eef8ce150..4d5a66c95d 100644 --- a/internal/handler/document_test.go +++ b/internal/handler/document_test.go @@ -146,9 +146,6 @@ type fakeDocumentService struct { metadataErr error metadataKBID string metadataDocIDs []string - setMetaCalled bool - setMetaDocID string - setMetaValue map[string]interface{} uploadLocalData []map[string]interface{} uploadLocalErrs []string uploadLocalKB *entity.Knowledgebase @@ -336,18 +333,6 @@ func (f *fakeDocumentService) HasActiveIngestionTasks(ctx context.Context, datas return f.hasActiveTasks, f.hasActiveTasksErr } -func (f *fakeDocumentService) SetDocumentMetadata(ctx context.Context, docID string, meta map[string]interface{}) error { - f.setMetaCalled = true - f.setMetaDocID = docID - f.setMetaValue = meta - return nil -} -func (f *fakeDocumentService) DeleteDocumentMetadata(ctx context.Context, docID string, keys []string) error { - return nil -} -func (f *fakeDocumentService) DeleteDocumentAllMetadata(ctx context.Context, docID string) error { - return nil -} func (f *fakeDocumentService) GetDocumentMetadataByID(ctx context.Context, docID string) (map[string]interface{}, error) { return nil, nil } @@ -457,17 +442,18 @@ func TestDocumentReadHandlersForbidden(t *testing.T) { body string params gin.Params handler gin.HandlerFunc + status int }{ - {"document detail", http.MethodGet, "/api/v1/documents/doc-1", "", gin.Params{{Key: "id", Value: "doc-1"}}, h.GetDocumentByID}, - {"metadata summary", http.MethodPost, "/api/v1/document/metadata/summary", `{"kb_id":"kb-owner","doc_ids":["doc-1"]}`, nil, h.MetadataSummary}, + {"document detail", http.MethodGet, "/api/v1/documents/doc-1", "", gin.Params{{Key: "id", Value: "doc-1"}}, h.GetDocumentByID, http.StatusForbidden}, + {"metadata summary", http.MethodGet, "/api/v1/datasets/kb-owner/metadata/summary?doc_ids=doc-1", "", gin.Params{{Key: "dataset_id", Value: "kb-owner"}}, h.MetadataSummaryByDataset, http.StatusOK}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c, w := setupGinContextWithUser(tt.method, tt.path, tt.body) c.Params = tt.params tt.handler(c) - if w.Code != http.StatusForbidden { - t.Fatalf("status = %d, want 403: %s", w.Code, w.Body.String()) + if w.Code != tt.status { + t.Fatalf("status = %d, want %d: %s", w.Code, tt.status, w.Body.String()) } var resp struct { Code common.ErrorCode `json:"code"` @@ -487,73 +473,6 @@ func TestDocumentReadHandlersForbidden(t *testing.T) { } } -func TestSetMetaHandler_NotAccessible(t *testing.T) { - setupDocumentPermissionDB(t, false) - - fake := &fakeDocumentService{ - doc: &document.DocumentResponse{ID: "doc-1", KbID: "kb-owner"}, - } - h := &DocumentHandler{ - documentService: fake, - datasetService: dataset.NewDatasetService(), - } - - c, w := setupGinContextWithUser("POST", "/api/v1/document/set_meta", `{"doc_id":"doc-1","meta":"{\"poc\":\"blocked\"}"}`) - h.SetMeta(c) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - var resp map[string]interface{} - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { - t.Fatal(err) - } - if resp["code"] != float64(common.CodeForbidden) { - t.Fatalf("expected auth error, got %v", resp) - } - if resp["message"] != "Permission denied" { - t.Fatalf("unexpected message: %v", resp["message"]) - } - if fake.setMetaCalled { - t.Fatal("SetDocumentMetadata should not be called without dataset access") - } -} - -func TestSetMetaHandler_Accessible(t *testing.T) { - setupDocumentPermissionDB(t, true) - - fake := &fakeDocumentService{ - doc: &document.DocumentResponse{ID: "doc-1", KbID: "kb-owner"}, - } - h := &DocumentHandler{ - documentService: fake, - datasetService: dataset.NewDatasetService(), - } - - c, w := setupGinContextWithUser("POST", "/api/v1/document/set_meta", `{"doc_id":"doc-1","meta":"{\"category\":\"tech\",\"year\":2026}"}`) - h.SetMeta(c) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String()) - } - var resp map[string]interface{} - if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { - t.Fatal(err) - } - if resp["code"] != float64(common.CodeSuccess) || resp["data"] != true { - t.Fatalf("unexpected response: %v", resp) - } - if !fake.setMetaCalled { - t.Fatal("SetDocumentMetadata should be called with dataset access") - } - if fake.setMetaDocID != "doc-1" { - t.Fatalf("set meta doc id = %q, want doc-1", fake.setMetaDocID) - } - if fake.setMetaValue["category"] != "tech" || fake.setMetaValue["year"] != float64(2026) { - t.Fatalf("unexpected meta: %#v", fake.setMetaValue) - } -} - func TestDeleteDocumentHandler_NotAccessible(t *testing.T) { setupDocumentPermissionDB(t, false) diff --git a/internal/handler/file.go b/internal/handler/file.go index 84670f2bdb..b711992e6b 100644 --- a/internal/handler/file.go +++ b/internal/handler/file.go @@ -133,33 +133,6 @@ func (h *FileHandler) ListFiles(c *gin.Context) { common.SuccessWithData(c, result, "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) { - user, errorCode, errorMessage := GetUser(c) - if errorCode != common.CodeSuccess { - common.ErrorWithCode(c, errorCode, errorMessage) - return - } - userID := user.ID - - ctx := c.Request.Context() - // Get root folder - rootFolder, err := h.fileService.GetRootFolder(ctx, userID) - if err != nil { - respondFileServiceError(c, err) - return - } - - common.SuccessWithData(c, gin.H{"root_folder": rootFolder}, common.CodeSuccess.Message()) -} - // GetParentFolder gets parent folder of a file // @Summary Get Parent Folder // @Description Get parent folder of a file by file ID @@ -168,7 +141,7 @@ func (h *FileHandler) GetRootFolder(c *gin.Context) { // @Produce json // @Param file_id query string true "file ID" // @Success 200 {object} map[string]interface{} -// @Router /v1/file/parent_folder [get] +// @Router /api/v1/files/{id}/parent [get] func (h *FileHandler) GetParentFolder(c *gin.Context) { user, errorCode, errorMessage := GetUser(c) if errorCode != common.CodeSuccess { @@ -195,41 +168,6 @@ func (h *FileHandler) GetParentFolder(c *gin.Context) { common.SuccessWithData(c, gin.H{"parent_folder": parentFolder}, common.CodeSuccess.Message()) } -// 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) { - user, errorCode, errorMessage := GetUser(c) - if errorCode != common.CodeSuccess { - common.ErrorWithCode(c, errorCode, errorMessage) - return - } - userID := user.ID - - // Get file_id from query - fileID := c.Query("file_id") - if fileID == "" { - common.ResponseWithCodeData(c, common.CodeBadRequest, nil, "file_id is required") - return - } - - ctx := c.Request.Context() - // Get all parent folders with permission check - parentFolders, err := h.fileService.GetAllParentFolders(ctx, userID, fileID) - if err != nil { - respondFileServiceError(c, err) - return - } - - common.SuccessWithData(c, gin.H{"parent_folders": parentFolders}, common.CodeSuccess.Message()) -} - // GetFileAncestors gets all ancestor folders of a file (matches Python /files//ancestors) // @Summary Get File Ancestors // @Description Get all ancestor folders in path from file to root diff --git a/internal/handler/tenant.go b/internal/handler/tenant.go index 5767127d5f..296846733d 100644 --- a/internal/handler/tenant.go +++ b/internal/handler/tenant.go @@ -160,7 +160,7 @@ func (h *TenantHandler) TenantInfo(c *gin.Context) { // @Tags tenants // @Security ApiKeyAuth // @Success 200 {object} map[string]interface{} -// @Router /v1/tenant/list [get] +// @Router /api/v1/tenants [get] func (h *TenantHandler) TenantList(c *gin.Context) { user, errorCode, errorMessage := GetUser(c) if errorCode != common.CodeSuccess { diff --git a/internal/router/router.go b/internal/router/router.go index 3f38e8529d..23bb9a2f3e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -281,7 +281,6 @@ func (r *Router) Setup(engine *gin.Engine) { // Tenant routes (per-tenant resources) tenant := v1.Group("/tenant") { - tenant.GET("/list", r.tenantHandler.TenantList) tenant.POST("/chunk_store", r.tenantHandler.CreateChunkStore) // Internal API only for GO tenant.DELETE("/chunk_store", r.tenantHandler.DeleteChunkStore) // Internal API only for GO tenant.POST("/metadata_store", r.tenantHandler.CreateMetadataStore) // Internal API only for GO @@ -420,7 +419,6 @@ func (r *Router) Setup(engine *gin.Engine) { searches.GET("/:search_id", r.searchHandler.GetSearch) searches.PUT("/:search_id", r.searchHandler.UpdateSearch) searches.DELETE("/:search_id", r.searchHandler.DeleteSearch) - searches.POST("/:search_id/completion", r.searchHandler.Completion) searches.POST("/:search_id/completions", r.searchHandler.Completion) } @@ -441,14 +439,6 @@ func (r *Router) Setup(engine *gin.Engine) { workspaceFiles.GET("/:file_id/versions", r.fileCommitHandler.GetFileVersionHistory) } - // File routes - file := authorized.Group("/v1/file") - { - file.GET("/root_folder", r.fileHandler.GetRootFolder) - file.GET("/parent_folder", r.fileHandler.GetParentFolder) - file.GET("/all_parent_folder", r.fileHandler.GetAllParentFolders) - } - // File commit routes — /folders/ takes folder_id directly commitFolders := v1.Group("/folders") { @@ -556,13 +546,18 @@ func (r *Router) Setup(engine *gin.Engine) { provider.PATCH("/:provider_id_or_name/instances/:instance_id_or_name/models/*model_name", r.providerHandler.AlterModel) provider.POST("/:provider_id_or_name/instances/:instance_id_or_name/models", r.providerHandler.AddModel) provider.DELETE("/:provider_id_or_name/instances/:instance_id_or_name/models", r.providerHandler.DropInstanceModels) - v1.POST("/chat/to_model", r.providerHandler.ChatToModel) - v1.POST("/embeddings", r.providerHandler.EmbedText) - v1.POST("/rerank", r.providerHandler.RerankDocument) - v1.POST("/audio/transcriptions", r.providerHandler.TranscribeAudio) - v1.POST("/audio/speech", r.providerHandler.AudioSpeech) - v1.POST("/file/ocr", r.providerHandler.OCRFile) - v1.POST("/file/parse", r.providerHandler.ParseFile) + } + + // command is for CLI + command := v1 + { + command.POST("/chat/to_model", r.providerHandler.ChatToModel) + command.POST("/embeddings", r.providerHandler.EmbedText) + command.POST("/rerank", r.providerHandler.RerankDocument) + command.POST("/audio/transcriptions", r.providerHandler.TranscribeAudio) + command.POST("/audio/speech", r.providerHandler.AudioSpeech) + command.POST("/file/ocr", r.providerHandler.OCRFile) + command.POST("/file/parse", r.providerHandler.ParseFile) } model := v1.Group("/models") @@ -692,22 +687,6 @@ func (r *Router) Setup(engine *gin.Engine) { system.PUT("/concurrency", r.systemHandler.SetConcurrency) } - // Document routes - doc := v1.Group("/document") - { - doc.POST("/list", r.documentHandler.ListDocuments) - doc.POST("/metadata/summary", r.documentHandler.MetadataSummary) - doc.POST("/set_meta", r.documentHandler.SetMeta) - doc.POST("/delete_meta", r.documentHandler.DeleteMeta) // Internal API only for GO - } - - // Chunk routes - chunk := v1.Group("/chunk") - { - chunk.POST("/list", r.chunkHandler.List) - chunk.POST("/update", r.chunkHandler.UpdateChunk) // Internal API only for GO - } - // Chat Channel chatChannel := v1.Group("/chat-channels") { diff --git a/internal/router/router_test.go b/internal/router/router_test.go index 891ccfec2c..7ed8b4ded4 100644 --- a/internal/router/router_test.go +++ b/internal/router/router_test.go @@ -7,12 +7,46 @@ import ( "testing" "github.com/gin-gonic/gin" + "go.uber.org/zap" "ragflow/internal/common" "ragflow/internal/handler" "ragflow/internal/service" ) +func TestRouterSetupRemovesActionRoutes(t *testing.T) { + gin.SetMode(gin.TestMode) + originalLogger := common.Logger + common.Logger = zap.NewNop() + t.Cleanup(func() { common.Logger = originalLogger }) + + engine := gin.New() + r := &Router{authHandler: handler.NewAuthHandler()} + r.Setup(engine) + + tests := []struct { + method string + path string + }{ + {http.MethodGet, "/api/v1/tenant/list"}, + {http.MethodPost, "/api/v1/document/list"}, + {http.MethodPost, "/api/v1/document/metadata/summary"}, + {http.MethodPost, "/api/v1/document/delete_meta"}, + {http.MethodPost, "/api/v1/document/set_meta"}, + {http.MethodPost, "/api/v1/chunk/list"}, + {http.MethodPost, "/api/v1/chunk/update"}, + } + for _, tt := range tests { + t.Run(tt.method+" "+tt.path, func(t *testing.T) { + resp := httptest.NewRecorder() + engine.ServeHTTP(resp, httptest.NewRequest(tt.method, tt.path, nil)) + if resp.Code != http.StatusNotFound { + t.Fatalf("status=%d body=%s; want 404", resp.Code, resp.Body.String()) + } + }) + } +} + func TestConnectorRoutesDoNotConflictWithOAuthCallbacks(t *testing.T) { gin.SetMode(gin.TestMode) diff --git a/test/testcases/restful_api/test_searches.py b/test/testcases/restful_api/test_searches.py index b0993b3a7f..f81aec8bb9 100644 --- a/test/testcases/restful_api/test_searches.py +++ b/test/testcases/restful_api/test_searches.py @@ -107,7 +107,7 @@ def test_search_completion_requires_question(rest_client, search_resource): search_id = search_resource expected_message = "question is required" - completion_res = rest_client.post(f"/searches/{search_id}/completion", json={}) + completion_res = rest_client.post(f"/searches/{search_id}/completions", json={}) assert completion_res.status_code == 200 completion_payload = completion_res.json() assert completion_payload["code"] == 101, completion_payload @@ -123,7 +123,7 @@ def test_search_completion_requires_question(rest_client, search_resource): @pytest.mark.p2 def test_search_completion_requires_kb_ids(rest_client, search_resource): search_id = search_resource - for path in ("completion", "completions"): + for path in ["completions"]: res = rest_client.post( f"/searches/{search_id}/{path}", json={"question": "what is coriander?"}, @@ -140,7 +140,7 @@ def test_search_completion_sse_shape_when_kb_ids_provided(rest_client, search_re # Even with kb_ids provided, runtime may return an error event in-stream, but # contract remains SSE with JSON data lines and terminal boolean event. res = rest_client.post( - f"/searches/{search_id}/completion", + f"/searches/{search_id}/completions", json={"question": "what is coriander?", "kb_ids": ["nonexistent_dataset"]}, timeout=60, ) diff --git a/web/src/hooks/document-query-keys.ts b/web/src/hooks/document-query-keys.ts index 0c4b2245ad..823d7f4241 100644 --- a/web/src/hooks/document-query-keys.ts +++ b/web/src/hooks/document-query-keys.ts @@ -22,7 +22,6 @@ export const enum DocumentApiAction { RemoveDocument = 'removeDocument', SaveDocumentName = 'saveDocumentName', SetDocumentParser = 'setDocumentParser', - SetDocumentMeta = 'setDocumentMeta', FetchDocumentFilter = 'fetchDocumentFilter', CreateDocument = 'createDocument', FetchDocumentThumbnails = 'fetchDocumentThumbnails', diff --git a/web/src/hooks/use-document-request.ts b/web/src/hooks/use-document-request.ts index 1d43031e2e..62ec25974e 100644 --- a/web/src/hooks/use-document-request.ts +++ b/web/src/hooks/use-document-request.ts @@ -29,10 +29,7 @@ import { IClaimsResponse, IStructureGraphResponse, } from '@/interfaces/database/document-structure'; -import { - IChangeParserConfigRequestBody, - IDocumentMetaRequestBody, -} from '@/interfaces/request/document'; +import { IChangeParserConfigRequestBody } from '@/interfaces/request/document'; import i18n from '@/locales/config'; import { EMPTY_METADATA_FIELD } from '@/pages/dataset/dataset/use-select-filters'; import { @@ -702,39 +699,6 @@ export const useSetDocumentPipelineParser = () => { return { setDocumentPipelineParser: mutateAsync, data, loading }; }; -export const useSetDocumentMeta = () => { - const queryClient = useQueryClient(); - - const { - data, - isPending: loading, - mutateAsync, - } = useMutation({ - mutationKey: [DocumentApiAction.SetDocumentMeta], - mutationFn: async (params: IDocumentMetaRequestBody) => { - try { - const { data } = await kbService.setMeta({ - meta: params.meta, - doc_id: params.documentId, - }); - - if (data?.code === 0) { - queryClient.invalidateQueries({ - queryKey: DocumentKeys.all(), - }); - - message.success(i18n.t('message.modified')); - } - return data?.code; - } catch (error) { - message.error('error:' + error); - } - }, - }); - - return { setDocumentMeta: mutateAsync, data, loading }; -}; - export const useCreateDocument = () => { const { id } = useParams(); const { setPaginationParams, page } = useSetPaginationParams(); diff --git a/web/src/interfaces/request/document.ts b/web/src/interfaces/request/document.ts index 464edd91e6..803db4aa25 100644 --- a/web/src/interfaces/request/document.ts +++ b/web/src/interfaces/request/document.ts @@ -44,8 +44,3 @@ export interface IChangeParserRequestBody { doc_id?: string; parser_config: IChangeParserConfigRequestBody; } - -export interface IDocumentMetaRequestBody { - documentId: string; - meta: string; // json format string -} diff --git a/web/src/pages/dataset/components/metedata/hooks/use-manage-modal.ts b/web/src/pages/dataset/components/metedata/hooks/use-manage-modal.ts index b03a0c9b32..50ce8d64af 100644 --- a/web/src/pages/dataset/components/metedata/hooks/use-manage-modal.ts +++ b/web/src/pages/dataset/components/metedata/hooks/use-manage-modal.ts @@ -307,8 +307,6 @@ export const useManageMetaDataModal = ( resetOperations, } = useMetadataOperations(); - // const { setDocumentMeta } = useSetDocumentMeta(); - useEffect(() => { if (fetchTypeList.includes(type)) { if (data) { diff --git a/web/src/pages/dataset/dataset/set-meta-dialog.tsx b/web/src/pages/dataset/dataset/set-meta-dialog.tsx deleted file mode 100644 index ff5aeabc27..0000000000 --- a/web/src/pages/dataset/dataset/set-meta-dialog.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { TagRenameId } from '@/constants/knowledge'; -import { IModalProps } from '@/interfaces/common'; -import { useTranslation } from 'react-i18next'; - -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; - -import { ButtonLoading } from '@/components/ui/button'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { IDocumentInfo } from '@/interfaces/database/document'; -import Editor, { loader } from '@monaco-editor/react'; -import DOMPurify from 'dompurify'; -import { useEffect } from 'react'; - -loader.config({ paths: { vs: '/vs' } }); - -export function SetMetaDialog({ - hideModal, - onOk, - loading, - initialMetaData, -}: IModalProps & { initialMetaData?: IDocumentInfo['meta_fields'] }) { - const { t } = useTranslation(); - - const FormSchema = z.object({ - meta: z - .string() - .min(1, { - message: t('knowledgeDetails.pleaseInputJson'), - }) - .trim() - .refine( - (value) => { - try { - JSON.parse(value); - return true; - } catch (error) { - return false; - } - }, - { message: t('knowledgeDetails.pleaseInputJson') }, - ), - }); - - const form = useForm>({ - resolver: zodResolver(FormSchema), - defaultValues: {}, - }); - - async function onSubmit(data: z.infer) { - const ret = await onOk?.(data.meta); - if (ret) { - hideModal?.(); - } - } - - useEffect(() => { - form.setValue('meta', JSON.stringify(initialMetaData, null, 4)); - }, [form, initialMetaData]); - - return ( - - - - {t('knowledgeDetails.setMetaData')} - -
- - ( - - - } - > - {t('knowledgeDetails.metaData')} - - - - - - - )} - /> - - - - - {t('common.save')} - - -
-
- ); -} diff --git a/web/src/pages/dataset/dataset/use-save-meta.ts b/web/src/pages/dataset/dataset/use-save-meta.ts deleted file mode 100644 index c16eb17558..0000000000 --- a/web/src/pages/dataset/dataset/use-save-meta.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useSetModalState } from '@/hooks/common-hooks'; -import { useSetDocumentMeta } from '@/hooks/use-document-request'; -import { IDocumentInfo } from '@/interfaces/database/document'; -import { useCallback, useState } from 'react'; - -export const useSaveMeta = () => { - const { setDocumentMeta, loading } = useSetDocumentMeta(); - const [record, setRecord] = useState({} as IDocumentInfo); - - const { - visible: setMetaVisible, - hideModal: hideSetMetaModal, - showModal: showSetMetaModal, - } = useSetModalState(); - - const onSetMetaModalOk = useCallback( - async (meta: string) => { - const ret = await setDocumentMeta({ - documentId: record?.id, - meta, - }); - if (ret === 0) { - hideSetMetaModal(); - } - }, - [setDocumentMeta, record?.id, hideSetMetaModal], - ); - - const handleShowSetMetaModal = useCallback( - (row: IDocumentInfo) => { - setRecord(row); - showSetMetaModal(); - }, - [showSetMetaModal], - ); - - return { - setMetaLoading: loading, - onSetMetaModalOk, - setMetaVisible, - hideSetMetaModal, - showSetMetaModal: handleShowSetMetaModal, - metaRecord: record, - }; -}; - -export type UseSaveMetaShowType = Pick< - ReturnType, - 'showSetMetaModal' ->; diff --git a/web/src/services/knowledge-service.ts b/web/src/services/knowledge-service.ts index dec23324fd..4ae9a6025f 100644 --- a/web/src/services/knowledge-service.ts +++ b/web/src/services/knowledge-service.ts @@ -37,7 +37,6 @@ const { documentIngest, listTagByKnowledgeIds, listPipelines, - setMeta, getMeta, getMetaKeys, retrievalTestShare, @@ -64,10 +63,6 @@ const methods = { url: documentThumbnails, method: 'get', }, - setMeta: { - url: setMeta, - method: 'post', - }, listTagByKnowledgeIds: { url: listTagByKnowledgeIds, method: 'get', diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index af2253ce26..e233e00d04 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -289,7 +289,6 @@ export default { webCrawl: (datasetId: string) => `${restAPIv1}/datasets/${datasetId}/documents?type=web`, documentInfoUpload: `${restAPIv1}/documents/upload`, - setMeta: `${webAPI}/document/set_meta`, getDatasetFilter: (datasetId: string) => `${restAPIv1}/datasets/${datasetId}/documents?type=filter`,