feat(go): port knowledge compilation template/group + wiki artifact/nav/skill REST APIs from Python (#17656)

This commit is contained in:
Zhichang Yu
2026-07-31 22:57:43 +08:00
committed by GitHub
parent d67d14e4c6
commit b811dce1b0
18 changed files with 3428 additions and 54 deletions

View File

@@ -0,0 +1,78 @@
//
// 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 (
"ragflow/internal/common"
"ragflow/internal/service"
"github.com/gin-gonic/gin"
)
// CompilationTemplateHandler serves the knowledge-compilation template REST
// APIs. It mirrors the Python compilation_template_api blueprint.
type CompilationTemplateHandler struct {
compilationTemplateService *service.CompilationTemplateService
}
// NewCompilationTemplateHandler creates a CompilationTemplateHandler.
func NewCompilationTemplateHandler(svc *service.CompilationTemplateService) *CompilationTemplateHandler {
return &CompilationTemplateHandler{compilationTemplateService: svc}
}
// ListBuiltins returns the built-in template palette used by the frontend
// "Add template group" panel.
//
// @Summary List built-in compilation templates
// @Tags compilation template
// @Security ApiKeyAuth
// @Success 200 {object} map[string]interface{}
// @Router /v1/compilation-templates/builtins [get]
func (h *CompilationTemplateHandler) ListBuiltins(c *gin.Context) {
user, code, msg := GetUser(c)
if code != common.CodeSuccess {
common.ErrorWithCode(c, code, msg)
return
}
templates, err := h.compilationTemplateService.ListBuiltins(c.Request.Context(), user.ID)
if err != nil {
common.ResponseWithCodeData(c, common.CodeExceptionError, nil, err.Error())
return
}
common.SuccessWithData(c, templates, "success")
}
// ListWikiPresets returns the wiki page-structure presets loaded from YAML.
//
// @Summary List wiki presets
// @Tags compilation template
// @Security ApiKeyAuth
// @Success 200 {object} map[string]interface{}
// @Router /v1/compilation-templates/wiki-presets [get]
func (h *CompilationTemplateHandler) ListWikiPresets(c *gin.Context) {
_, code, msg := GetUser(c)
if code != common.CodeSuccess {
common.ErrorWithCode(c, code, msg)
return
}
presets, err := h.compilationTemplateService.LoadWikiPresets()
if err != nil {
common.ResponseWithCodeData(c, common.CodeExceptionError, nil, err.Error())
return
}
common.SuccessWithData(c, presets, "success")
}

View File

@@ -0,0 +1,233 @@
//
// 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 (
"strconv"
"ragflow/internal/common"
"ragflow/internal/service"
"github.com/gin-gonic/gin"
)
// CompilationTemplateGroupHandler serves the knowledge-compilation template
// group REST APIs. It mirrors the Python compilation_template_group_api blueprint.
type CompilationTemplateGroupHandler struct {
compilationTemplateGroupService *service.CompilationTemplateGroupService
}
// NewCompilationTemplateGroupHandler creates a CompilationTemplateGroupHandler.
func NewCompilationTemplateGroupHandler(svc *service.CompilationTemplateGroupService) *CompilationTemplateGroupHandler {
return &CompilationTemplateGroupHandler{compilationTemplateGroupService: svc}
}
// List returns the tenant's compilation template groups with their nested
// templates and pagination.
//
// @Summary List compilation template groups
// @Tags compilation template group
// @Security ApiKeyAuth
// @Param keywords query string false "name keyword filter"
// @Param scope query string false "scope filter (file|dataset)"
// @Param page query int false "page number" default(1)
// @Param page_size query int false "items per page" default(30)
// @Param orderby query string false "order field" default(create_time)
// @Param desc query bool false "descending" default(true)
// @Success 200 {object} map[string]interface{}
// @Router /v1/compilation-template-groups [get]
func (h *CompilationTemplateGroupHandler) List(c *gin.Context) {
user, code, msg := GetUser(c)
if code != common.CodeSuccess {
common.ErrorWithCode(c, code, msg)
return
}
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
}
keywords := c.Query("keywords")
scope := c.Query("scope")
orderby := c.DefaultQuery("orderby", "create_time")
desc := c.DefaultQuery("desc", "true") != "false"
groups, err := h.compilationTemplateGroupService.ListSaved(c.Request.Context(), user.ID, keywords, scope, orderby, desc)
if err != nil {
common.ResponseWithCodeData(c, common.CodeExceptionError, nil, err.Error())
return
}
total := len(groups)
if page > 0 && pageSize > 0 {
start := (page - 1) * pageSize
if start >= total {
groups = []*service.GroupListItem{}
} else {
end := start + pageSize
if end > total {
end = total
}
groups = groups[start:end]
}
}
common.SuccessWithData(c, gin.H{"groups": groups, "total": total}, "success")
}
// Get returns a single compilation template group.
//
// @Summary Get compilation template group
// @Tags compilation template group
// @Security ApiKeyAuth
// @Param group_id path string true "group id"
// @Success 200 {object} map[string]interface{}
// @Router /v1/compilation-template-groups/{group_id} [get]
func (h *CompilationTemplateGroupHandler) Get(c *gin.Context) {
user, code, msg := GetUser(c)
if code != common.CodeSuccess {
common.ErrorWithCode(c, code, msg)
return
}
id := c.Param("group_id")
group, err := h.compilationTemplateGroupService.GetSaved(c.Request.Context(), user.ID, id)
if err != nil {
common.ResponseWithCodeData(c, common.CodeExceptionError, nil, err.Error())
return
}
if group == nil {
common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Cannot find compilation template group "+id+".")
return
}
common.SuccessWithData(c, group, "success")
}
// Save creates a new compilation template group with its child templates.
//
// @Summary Save compilation template group
// @Tags compilation template group
// @Security ApiKeyAuth
// @Param request body service.GroupRequest true "group payload"
// @Success 200 {object} map[string]interface{}
// @Router /v1/compilation-template-groups [post]
func (h *CompilationTemplateGroupHandler) Save(c *gin.Context) {
user, code, msg := GetUser(c)
if code != common.CodeSuccess {
common.ErrorWithCode(c, code, msg)
return
}
var req service.GroupRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ResponseWithCodeData(c, common.CodeArgumentError, nil, "Invalid request: "+err.Error())
return
}
if err := service.ValidateGroupRequest(&req); err != nil {
common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
return
}
nameExists, err := h.compilationTemplateGroupService.NameExists(c.Request.Context(), user.ID, req.Name)
if err != nil {
common.ResponseWithCodeData(c, common.CodeExceptionError, nil, err.Error())
return
}
if nameExists {
common.ResponseWithCodeData(c, common.CodeDataError, nil, "Duplicated compilation template group name.")
return
}
group, err := h.compilationTemplateGroupService.CreateGroup(c.Request.Context(), user.ID, &req)
if err != nil {
common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
return
}
common.SuccessWithData(c, group, "success")
}
// Update applies a partial update to a compilation template group.
//
// @Summary Update compilation template group
// @Tags compilation template group
// @Security ApiKeyAuth
// @Param group_id path string true "group id"
// @Param request body service.GroupRequest true "group payload"
// @Success 200 {object} map[string]interface{}
// @Router /v1/compilation-template-groups/{group_id} [put]
func (h *CompilationTemplateGroupHandler) Update(c *gin.Context) {
user, code, msg := GetUser(c)
if code != common.CodeSuccess {
common.ErrorWithCode(c, code, msg)
return
}
id := c.Param("group_id")
var req service.GroupRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ResponseWithCodeData(c, common.CodeArgumentError, nil, "Invalid request: "+err.Error())
return
}
if err := service.ValidateGroupRequest(&req); err != nil {
common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
return
}
if req.Name != "" {
nameExists, nerr := h.compilationTemplateGroupService.NameExists(c.Request.Context(), user.ID, req.Name, id)
if nerr != nil {
common.ResponseWithCodeData(c, common.CodeExceptionError, nil, nerr.Error())
return
}
if nameExists {
common.ResponseWithCodeData(c, common.CodeDataError, nil, "Duplicated compilation template group name.")
return
}
}
group, err := h.compilationTemplateGroupService.UpdateGroup(c.Request.Context(), user.ID, id, &req)
if err != nil {
common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
return
}
if group == nil {
common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Cannot find compilation template group "+id+".")
return
}
common.SuccessWithData(c, group, "success")
}
// Delete soft-deletes a compilation template group and its children.
//
// @Summary Delete compilation template group
// @Tags compilation template group
// @Security ApiKeyAuth
// @Param group_id path string true "group id"
// @Success 200 {object} map[string]interface{}
// @Router /v1/compilation-template-groups/{group_id} [delete]
func (h *CompilationTemplateGroupHandler) Delete(c *gin.Context) {
user, code, msg := GetUser(c)
if code != common.CodeSuccess {
common.ErrorWithCode(c, code, msg)
return
}
id := c.Param("group_id")
ok, err := h.compilationTemplateGroupService.DeleteGroup(c.Request.Context(), user.ID, id)
if err != nil {
common.ResponseWithCodeData(c, common.CodeExceptionError, nil, err.Error())
return
}
if !ok {
common.ResponseWithCodeData(c, common.CodeNotFound, nil, "Cannot find compilation template group "+id+".")
return
}
common.SuccessWithData(c, true, "success")
}

View File

@@ -0,0 +1,475 @@
//
// 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")
}

View File

@@ -38,6 +38,7 @@ type fileCommitService interface {
GetCommitTree(ctx context.Context, commitID string) (map[string]interface{}, error)
GetCommitFileContent(ctx context.Context, folderID, commitID, fileID string) ([]byte, error)
GetFileVersionHistory(ctx context.Context, fileID string) ([]entity.VersionEntry, error)
ListPageCommits(ctx context.Context, datasetID, pageType, slug string, page, pageSize int) ([]*entity.FileCommit, int64, error)
}
// FileCommitHandler file commit handler
@@ -217,6 +218,49 @@ func (h *FileCommitHandler) ListCommits(c *gin.Context) {
}
ctx := c.Request.Context()
// Python's list_commits supports ?slug=<page_slug> to filter audit commits
// for a specific wiki/skill page (written by record_page_edit). These page
// commits are scoped to the dataset and page file key, so route them through
// the page-commit path instead of the folder-based ListCommits. This only
// applies to the /datasets/{dataset_id}/commits route.
if slug := c.Query("slug"); slug != "" {
datasetID := c.Param("dataset_id")
if datasetID == "" {
common.ErrorWithCode(c, common.CodeArgumentError, "slug requires a dataset scope")
return
}
pageType := c.Query("page_type")
commits, total, err := h.commitService.ListPageCommits(ctx, datasetID, pageType, slug, page, pageSize)
if err != nil {
jsonInternalError(c, err)
return
}
var commitList []entity.CommitResponse
for _, commit := range commits {
var ct int64
if commit.CreateTime != nil {
ct = *commit.CreateTime
}
commitList = append(commitList, entity.CommitResponse{
ID: commit.ID,
FolderID: commit.FolderID,
ParentID: commit.ParentID,
Message: commit.Message,
AuthorID: commit.AuthorID,
FileCount: commit.FileCount,
CreateTime: &ct,
})
}
common.SuccessWithData(c, gin.H{
"total": total,
"page": page,
"page_size": pageSize,
"commits": commitList,
}, common.CodeSuccess.Message())
return
}
commits, total, err := h.commitService.ListCommits(ctx, folderID, page, pageSize, orderBy, desc)
if err != nil {
jsonInternalError(c, err)

View File

@@ -41,6 +41,7 @@ type mockFileCommitSvc struct {
getCommitTreeFn func(ctx context.Context, commitID string) (map[string]interface{}, error)
getCommitFileContentFn func(ctx context.Context, folderID, commitID, fileID string) ([]byte, error)
getFileVersionHistoryFn func(ctx context.Context, fileID string) ([]entity.VersionEntry, error)
listPageCommitsFn func(ctx context.Context, datasetID, pageType, slug string, page, pageSize int) ([]*entity.FileCommit, int64, error)
}
func (m *mockFileCommitSvc) CreateCommit(ctx context.Context, folderID, authorID, message string, changes []entity.FileChange) (*entity.FileCommit, error) {
@@ -128,6 +129,13 @@ func (m *mockFileCommitSvc) GetFileVersionHistory(ctx context.Context, fileID st
}, nil
}
func (m *mockFileCommitSvc) ListPageCommits(ctx context.Context, datasetID, pageType, slug string, page, pageSize int) ([]*entity.FileCommit, int64, error) {
if m.listPageCommitsFn != nil {
return m.listPageCommitsFn(ctx, datasetID, pageType, slug, page, pageSize)
}
return []*entity.FileCommit{}, 0, nil
}
func setupFileCommitTest(userID string) (*gin.Engine, *mockFileCommitSvc) {
mock := &mockFileCommitSvc{}
h := &FileCommitHandler{commitService: mock}

View File

@@ -112,7 +112,7 @@ func (h *ProviderHandler) AddProvider(c *gin.Context) {
}
func (h *ProviderHandler) DeleteProvider(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
@@ -131,7 +131,7 @@ func (h *ProviderHandler) DeleteProvider(c *gin.Context) {
}
func (h *ProviderHandler) ShowProvider(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
@@ -146,7 +146,7 @@ func (h *ProviderHandler) ShowProvider(c *gin.Context) {
}
func (h *ProviderHandler) ListModels(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
@@ -221,7 +221,7 @@ func (h *ProviderHandler) ListModels(c *gin.Context) {
}
func (h *ProviderHandler) ShowModel(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
@@ -249,7 +249,7 @@ type CreateProviderInstanceRequest struct {
}
func (h *ProviderHandler) CreateProviderInstance(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
@@ -287,7 +287,7 @@ func (h *ProviderHandler) CreateProviderInstance(c *gin.Context) {
}
func (h *ProviderHandler) ListProviderInstances(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
@@ -306,13 +306,13 @@ func (h *ProviderHandler) ListProviderInstances(c *gin.Context) {
}
func (h *ProviderHandler) ShowProviderInstance(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceIDOrName := c.Param("instance_name")
instanceIDOrName := c.Param("instance_id_or_name")
if instanceIDOrName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
@@ -331,13 +331,13 @@ func (h *ProviderHandler) ShowProviderInstance(c *gin.Context) {
}
func (h *ProviderHandler) ShowInstanceBalance(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_name")
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
@@ -357,7 +357,7 @@ func (h *ProviderHandler) ShowInstanceBalance(c *gin.Context) {
}
func (h *ProviderHandler) CheckConnection(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
@@ -382,13 +382,13 @@ func (h *ProviderHandler) CheckConnection(c *gin.Context) {
func (h *ProviderHandler) CheckInstanceConnection(c *gin.Context) {
ctx := c.Request.Context()
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_name")
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
@@ -417,13 +417,13 @@ func (h *ProviderHandler) CheckInstanceConnection(c *gin.Context) {
func (h *ProviderHandler) ListTasks(c *gin.Context) {
ctx := c.Request.Context()
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_name")
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
@@ -443,13 +443,13 @@ func (h *ProviderHandler) ListTasks(c *gin.Context) {
func (h *ProviderHandler) ShowTask(c *gin.Context) {
ctx := c.Request.Context()
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_name")
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
@@ -484,13 +484,13 @@ type AlterProviderInstanceRequest struct {
func (h *ProviderHandler) AlterProviderInstance(c *gin.Context) {
ctx := c.Request.Context()
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_name")
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
@@ -527,7 +527,7 @@ type DropProviderInstanceRequest struct {
}
func (h *ProviderHandler) DropProviderInstance(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
@@ -552,12 +552,12 @@ func (h *ProviderHandler) DropProviderInstance(c *gin.Context) {
func (h *ProviderHandler) ListInstanceModels(c *gin.Context) {
ctx := c.Request.Context()
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_name")
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
@@ -600,13 +600,13 @@ type AlterModelRequest struct {
}
func (h *ProviderHandler) AlterModel(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_name")
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return
@@ -691,8 +691,8 @@ func (h *ProviderHandler) AddModel(c *gin.Context) {
return
}
req.ProviderName = c.Param("provider_name")
req.InstanceName = c.Param("instance_name")
req.ProviderName = c.Param("provider_id_or_name")
req.InstanceName = c.Param("instance_id_or_name")
if req.ProviderName == "" || req.InstanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, common.CodeBadRequest, nil, "provider_name and instance_name are required")
@@ -726,12 +726,12 @@ type DropInstanceModelRequest struct {
}
func (h *ProviderHandler) DropInstanceModels(c *gin.Context) {
providerName := c.Param("provider_name")
providerName := c.Param("provider_id_or_name")
if providerName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Provider name is required")
return
}
instanceName := c.Param("instance_name")
instanceName := c.Param("instance_id_or_name")
if instanceName == "" {
common.ResponseWithHttpCodeData(c, http.StatusBadRequest, 400, nil, "Instance name is required")
return

View File

@@ -88,8 +88,8 @@ func TestProviderHandlerAlterModelRejectsMissingModelSelector(t *testing.T) {
ctx, recorder := newProviderHandlerRequest(
t,
map[string]interface{}{"status": "active"},
gin.Param{Key: "provider_name", Value: "OpenAI"},
gin.Param{Key: "instance_name", Value: "default"},
gin.Param{Key: "provider_id_or_name", Value: "OpenAI"},
gin.Param{Key: "instance_id_or_name", Value: "default"},
)
NewProviderHandler(nil, service.NewModelProviderService()).AlterModel(ctx)
@@ -107,8 +107,8 @@ func TestProviderHandlerAlterModelRejectsInvalidStatus(t *testing.T) {
ctx, recorder := newProviderHandlerRequest(
t,
map[string]interface{}{"status": "disabled"},
gin.Param{Key: "provider_name", Value: "OpenAI"},
gin.Param{Key: "instance_name", Value: "default"},
gin.Param{Key: "provider_id_or_name", Value: "OpenAI"},
gin.Param{Key: "instance_id_or_name", Value: "default"},
gin.Param{Key: "model_name", Value: "gpt-test"},
)
@@ -131,8 +131,8 @@ func TestProviderHandlerAlterModelUpdatesStatus(t *testing.T) {
ctx, recorder := newProviderHandlerRequest(
t,
map[string]interface{}{"status": "inactive"},
gin.Param{Key: "provider_name", Value: "OpenAI"},
gin.Param{Key: "instance_name", Value: "default"},
gin.Param{Key: "provider_id_or_name", Value: "OpenAI"},
gin.Param{Key: "instance_id_or_name", Value: "default"},
gin.Param{Key: "model_name", Value: "gpt-test"},
)