Files
ragflow/internal/handler/dataset_artifact.go

476 lines
16 KiB
Go

//
// 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.
//
// 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"
"ragflow/internal/common"
"ragflow/internal/dao"
"ragflow/internal/entity"
"ragflow/internal/service"
dataset "ragflow/internal/service/dataset"
"ragflow/internal/service/file"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// maxArtifactPageSize caps the page_size query parameter for artifact list
// endpoints so a client cannot force an unbounded document-engine search.
const maxArtifactPageSize = 100
// DatasetArtifactHandler exposes the knowledge-compilation artifact REST APIs
// backed by DatasetArtifactService.
type DatasetArtifactHandler struct {
svc *service.DatasetArtifactService
datasetSvc *dataset.DatasetService
fileCommitSvc *file.FileCommitService
}
// NewDatasetArtifactHandler creates a DatasetArtifactHandler.
func NewDatasetArtifactHandler(svc *service.DatasetArtifactService, datasetSvc *dataset.DatasetService, fileCommitSvc *file.FileCommitService) *DatasetArtifactHandler {
return &DatasetArtifactHandler{svc: svc, datasetSvc: datasetSvc, fileCommitSvc: fileCommitSvc}
}
// datasetOwner resolves the dataset and returns its tenant id. It also enforces
// dataset access permission for the requesting user. When the request is
// unauthenticated, the dataset is missing, or the user is not allowed to access
// it, an error response is written and (nil, "", "") is returned so callers can
// abort without sending a second response.
func (h *DatasetArtifactHandler) datasetOwner(c *gin.Context, datasetID string) (*entity.User, string, string) {
user, code, msg := GetUser(c)
if code != common.CodeSuccess {
common.ErrorWithCode(c, code, msg)
return nil, "", ""
}
kb, err := h.datasetSvc.GetKnowledgebaseByID(c.Request.Context(), datasetID)
if err != nil {
if dao.IsNotFoundErr(err) {
common.ErrorWithCode(c, common.CodeNotFound, "dataset not found")
} else {
common.ErrorWithCode(c, common.CodeServerError, "failed to resolve dataset")
}
return nil, "", ""
}
if kb == nil {
common.ErrorWithCode(c, common.CodeNotFound, "dataset not found")
return nil, "", ""
}
if !h.datasetSvc.Accessible(c.Request.Context(), datasetID, user.ID) {
common.ErrorWithCode(c, common.CodeForbidden, "no permission to access this dataset")
return nil, "", ""
}
return user, kb.TenantID, msg
}
// HEAD /artifacts — any wiki artifact present?
func (h *DatasetArtifactHandler) AnyArtifact(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
has, err := h.svc.HasWiki(c.Request.Context(), tenantID, c.Param("dataset_id"))
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
if has {
c.Status(http.StatusOK)
} else {
c.Status(http.StatusNotFound)
}
}
// GET /artifacts — list wiki pages.
func (h *DatasetArtifactHandler) ListArtifacts(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
datasetID := c.Param("dataset_id")
pageType := c.Query("page_type")
topic := c.Query("topic")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "30"))
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 30
}
if pageSize > maxArtifactPageSize {
pageSize = maxArtifactPageSize
}
items, total, err := h.svc.ListWikiPages(c.Request.Context(), tenantID, datasetID, pageType, topic, page, pageSize)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"total": total, "pages": items}, "success")
}
// PUT /artifacts/<page_type>/<slug> — edit a wiki page.
func (h *DatasetArtifactHandler) UpdateArtifact(c *gin.Context) {
user, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
datasetID := c.Param("dataset_id")
pageType := c.Param("page_type")
slug := c.Param("slug")
var req struct {
ContentMd string `json:"content_md"`
Title string `json:"title"`
Outlinks []string `json:"outlinks"`
}
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorWithCode(c, common.CodeArgumentError, err.Error())
return
}
// Read the current page before mutating so an audit commit can record an
// accurate old-to-new diff. Only record when the prior state was read
// successfully; a failed read must not produce a diff against an unknown
// old value.
var oldContent string
canRecordAudit := false
if req.ContentMd != "" {
before, gerr := h.svc.GetWikiPage(c.Request.Context(), tenantID, datasetID, pageType, slug)
if gerr != nil {
common.Warn("failed to read wiki page before recording edit commit", zap.Error(gerr))
} else if before != nil {
oldContent = before.ContentMd
canRecordAudit = true
}
}
detail, err := h.svc.UpdateWikiPage(c.Request.Context(), tenantID, datasetID, pageType, slug, req.ContentMd, req.Title, req.Outlinks)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
if detail == nil {
common.ErrorWithCode(c, common.CodeNotFound, "page not found")
return
}
// Record a wiki-page edit audit commit (git-style parent chain) when the
// content actually changed. Best-effort: a commit write failure must not
// roll back the page edit already persisted above.
if canRecordAudit && req.ContentMd != oldContent && h.fileCommitSvc != nil {
title := req.Title
if title == "" {
title = detail.Title
}
if _, cerr := h.fileCommitSvc.RecordPageEdit(c.Request.Context(), file.PageEditCommitInput{
DatasetID: datasetID,
DocID: pageType + "/" + slug,
Slug: slug,
PageType: pageType,
Title: title,
AuthorID: user.ID,
OldContent: oldContent,
NewContent: req.ContentMd,
}); cerr != nil {
common.Warn("failed to record wiki page edit commit", zap.Error(cerr))
}
}
common.SuccessWithData(c, detail, "success")
}
// GET /artifacts/<page_type>/<slug> — single wiki page.
func (h *DatasetArtifactHandler) GetArtifact(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
datasetID := c.Param("dataset_id")
pageType := c.Param("page_type")
slug := c.Param("slug")
detail, err := h.svc.GetWikiPage(c.Request.Context(), tenantID, datasetID, pageType, slug)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
if detail == nil {
common.ErrorWithCode(c, common.CodeNotFound, "page not found")
return
}
common.SuccessWithData(c, detail, "success")
}
// DELETE /artifacts — clear all wiki artifacts.
func (h *DatasetArtifactHandler) DeleteArtifacts(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
deleted, err := h.svc.ClearWiki(c.Request.Context(), tenantID, c.Param("dataset_id"))
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, deleted, "success")
}
// GET /artifacts/topics — list wiki topics.
func (h *DatasetArtifactHandler) ListArtifactTopics(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
datasetID := c.Param("dataset_id")
items, total, err := h.svc.ListWikiTopics(c.Request.Context(), tenantID, datasetID)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"total": total, "topics": items}, "success")
}
// GET /artifacts/alteration — wiki alteration summary.
func (h *DatasetArtifactHandler) GetArtifactAlteration(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
datasetID := c.Param("dataset_id")
alt, err := h.svc.GetWikiAlteration(c.Request.Context(), tenantID, datasetID)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, alt, "success")
}
// GET /artifacts/graph — wiki entity/relation graph.
func (h *DatasetArtifactHandler) GetArtifactGraph(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
datasetID := c.Param("dataset_id")
graph, err := h.svc.GetWikiGraph(c.Request.Context(), tenantID, datasetID)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, graph, "success")
}
// GET /artifacts/structure — list compiled structures of a dataset.
func (h *DatasetArtifactHandler) ListStructures(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
datasetID := c.Param("dataset_id")
structureKind := c.Query("structure_kind")
structureIndexType := c.Query("structure_index_type")
items, total, err := h.svc.ListStructures(c.Request.Context(), tenantID, datasetID, structureKind, structureIndexType)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"total": total, "structures": items}, "success")
}
// DELETE /artifacts/structure — delete compiled structures of a dataset.
func (h *DatasetArtifactHandler) DeleteStructures(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
datasetID := c.Param("dataset_id")
structureKind := c.Query("structure_kind")
structureIndexType := c.Query("structure_index_type")
n, err := h.svc.DeleteStructures(c.Request.Context(), tenantID, datasetID, structureKind, structureIndexType)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"deleted": n}, "success")
}
// HEAD /skills — any skill artifact present?
func (h *DatasetArtifactHandler) AnySkill(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
has, err := h.svc.HasSkill(c.Request.Context(), tenantID, c.Param("dataset_id"))
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
if has {
c.Status(http.StatusOK)
} else {
c.Status(http.StatusNotFound)
}
}
// GET /navigation — list navigation clusters.
func (h *DatasetArtifactHandler) ListNavigation(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
items, total, err := h.svc.ListNavClusters(c.Request.Context(), tenantID, c.Param("dataset_id"))
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"total": total, "nav": items}, "success")
}
// DELETE /navigation — delete all navigation clusters.
func (h *DatasetArtifactHandler) DeleteNavigation(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
n, err := h.svc.DeleteNav(c.Request.Context(), tenantID, c.Param("dataset_id"))
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"deleted": n}, "success")
}
// DELETE /navigation/<name> — delete a single navigation cluster.
func (h *DatasetArtifactHandler) DeleteNavigationNode(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
n, err := h.svc.DeleteNavNode(c.Request.Context(), tenantID, c.Param("dataset_id"), c.Param("name"))
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"deleted": n}, "success")
}
// GET /navigation/<name>/children — list children of a navigation cluster.
func (h *DatasetArtifactHandler) ListNavigationChildren(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
items, total, err := h.svc.ListNavChildren(c.Request.Context(), tenantID, c.Param("dataset_id"), c.Param("name"))
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"total": total, "children": items}, "success")
}
// GET /skills — skill tree.
func (h *DatasetArtifactHandler) GetSkillTree(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
kwd := c.Query("kwd")
items, total, err := h.svc.GetSkillTree(c.Request.Context(), tenantID, c.Param("dataset_id"), kwd)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"total": total, "tree": items}, "success")
}
// DELETE /skills — delete all skills.
func (h *DatasetArtifactHandler) DeleteSkills(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
n, err := h.svc.DeleteSkills(c.Request.Context(), tenantID, c.Param("dataset_id"), "")
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"deleted": n}, "success")
}
// GET /skills/<skill_kwd> — single skill page.
func (h *DatasetArtifactHandler) GetSkillPage(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
detail, err := h.svc.GetSkillPage(c.Request.Context(), tenantID, c.Param("dataset_id"), c.Param("skill_kwd"))
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
if detail == nil {
common.ErrorWithCode(c, common.CodeNotFound, "skill not found")
return
}
common.SuccessWithData(c, detail, "success")
}
// DELETE /skills/<skill_kwd> — delete a single skill.
func (h *DatasetArtifactHandler) DeleteSkill(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
n, err := h.svc.DeleteSkills(c.Request.Context(), tenantID, c.Param("dataset_id"), c.Param("skill_kwd"))
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"deleted": n}, "success")
}
// GET /documents/<document_id>/structure/graph — document structure graph.
func (h *DatasetArtifactHandler) GetDocumentGraph(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
datasetID := c.Param("dataset_id")
documentID := c.Param("document_id")
graphType := c.Query("graph_type")
items, total, err := h.svc.GetDocumentGraph(c.Request.Context(), tenantID, datasetID, documentID, graphType)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"total": total, "graph": items}, "success")
}
// DELETE /documents/<document_id>/structure/graph — delete document structure graph.
func (h *DatasetArtifactHandler) DeleteDocumentGraph(c *gin.Context) {
_, tenantID, _ := h.datasetOwner(c, c.Param("dataset_id"))
if tenantID == "" {
return
}
datasetID := c.Param("dataset_id")
documentID := c.Param("document_id")
n, err := h.svc.DeleteDocumentGraph(c.Request.Context(), tenantID, datasetID, documentID)
if err != nil {
common.ErrorWithCode(c, common.CodeDataError, err.Error())
return
}
common.SuccessWithData(c, gin.H{"deleted": n}, "success")
}