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

@@ -28,6 +28,8 @@ import (
"ragflow/internal/storage"
"ragflow/internal/utility"
"sort"
"strings"
"sync"
"time"
"go.uber.org/zap"
@@ -233,6 +235,209 @@ func (s *FileCommitService) CreateCommit(ctx context.Context, folderID, authorID
return commit, nil
}
// PageEditCommitInput carries the data needed to record a single wiki/skill
// page edit as an audit commit.
type PageEditCommitInput struct {
DatasetID string // knowledgebase scope, stored on the commit for isolation
DocID string // ES doc id of the page content
Slug string
PageType string
Title string
AuthorID string
OldContent string
NewContent string
}
// wikiFileID derives the stable file key used to scope page-edit commits to a
// specific knowledgebase and page, so identical slugs in different
// knowledgebases never share a commit parent or history.
func wikiFileID(datasetID, pageType, slug string) string {
return datasetID + "/" + pageType + "/" + slug
}
// RecordPageEdit records a wiki/skill page edit as an audit commit with a
// git-style parent chain (each edit points at the previous commit for the same
// page). The new content_after is referenced in ES by doc_id; a unified diff of
// old vs new content is stored on the commit item. The commit is scoped to the
// dataset via FolderID and a derived page file key so page histories never cross
// knowledgebase boundaries.
//
// This path is independent of the workspace File tree (it does not require a
// File record or a tree_state snapshot).
func (s *FileCommitService) RecordPageEdit(ctx context.Context, in PageEditCommitInput) (*entity.FileCommit, error) {
// Parent chain: previous commit that touched the same page file key.
fileID := wikiFileID(in.DatasetID, in.PageType, in.Slug)
commitID := utility.GenerateUUID()
diffText := unifiedDiff(in.OldContent, in.NewContent)
contentAfterStorage := "es"
contentAfterLocation := in.DocID
slugKwd := in.Slug
pageTypeKwd := in.PageType
item := &entity.FileCommitItem{
ID: utility.GenerateUUID(),
CommitID: commitID,
FileID: fileID,
Operation: "modify",
Diff: &diffText,
ContentAfterStorage: &contentAfterStorage,
ContentAfterLocation: &contentAfterLocation,
SlugKwd: &slugKwd,
PageTypeKwd: &pageTypeKwd,
}
var commit *entity.FileCommit
// Serialize parent selection with insertion in process so two concurrent
// edits on the same page cannot both read the same parent and fork the
// chain. This is backend-agnostic (works identically on MySQL and SQLite)
// and cheaper than row-level DB locks.
mu := pageCommitLock(fileID)
mu.Lock()
defer mu.Unlock()
// Read the parent inside the lock, on the shared connection, so it always
// reflects the previously committed edit for this page.
parentID, perr := s.commitItemDAO.GetLatestCommitIDByFileID(ctx, dao.DB, fileID)
if perr != nil {
return nil, fmt.Errorf("failed to resolve page commit parent: %w", perr)
}
commit = &entity.FileCommit{
ID: commitID,
FolderID: in.DatasetID,
Message: in.Title,
AuthorID: in.AuthorID,
Title: &in.Title,
FileCount: 1,
}
if parentID != "" {
commit.ParentID = &parentID
}
if err := dao.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if cerr := tx.Create(commit).Error; cerr != nil {
return fmt.Errorf("failed to create page commit: %w", cerr)
}
if ierr := tx.Create(item).Error; ierr != nil {
return fmt.Errorf("failed to create page commit item: %w", ierr)
}
return nil
}); err != nil {
return nil, err
}
return commit, nil
}
// pageCommitLocks is a per-page-file-key mutex registry that serializes
// RecordPageEdit calls for the same page. Entries are retained for the process
// lifetime (bounded by the number of distinct pages edited).
var pageCommitLocks sync.Map // fileID -> *sync.Mutex
func pageCommitLock(fileID string) *sync.Mutex {
v, _ := pageCommitLocks.LoadOrStore(fileID, &sync.Mutex{})
return v.(*sync.Mutex)
}
// ListPageCommits lists audit commits for a specific wiki/skill page.
func (s *FileCommitService) ListPageCommits(ctx context.Context, datasetID, pageType, slug string, page, pageSize int) ([]*entity.FileCommit, int64, error) {
items, err := s.commitItemDAO.ListByFileID(ctx, dao.DB, wikiFileID(datasetID, pageType, slug))
if err != nil {
return nil, 0, err
}
commitIDs := make([]string, 0, len(items))
for _, it := range items {
commitIDs = append(commitIDs, it.CommitID)
}
if len(commitIDs) == 0 {
return []*entity.FileCommit{}, 0, nil
}
commits, total, err := s.commitDAO.ListByIDs(ctx, dao.DB, commitIDs, page, pageSize)
if err != nil {
return nil, 0, err
}
return commits, total, nil
}
// unifiedDiff produces a simple line-based unified diff between two texts.
func unifiedDiff(oldText, newText string) string {
oldLines := strings.Split(oldText, "\n")
newLines := strings.Split(newText, "\n")
const maxCtx = 3
type hunkLine struct {
prefix string
text string
}
var hunks []hunkLine
// Longest common subsequence over lines, then render the diff.
cur := make([][]int, len(oldLines)+1)
for i := range cur {
cur[i] = make([]int, len(newLines)+1)
}
for i := len(oldLines) - 1; i >= 0; i-- {
for j := len(newLines) - 1; j >= 0; j-- {
if oldLines[i] == newLines[j] {
cur[i][j] = cur[i+1][j+1] + 1
} else if cur[i+1][j] >= cur[i][j+1] {
cur[i][j] = cur[i+1][j]
} else {
cur[i][j] = cur[i][j+1]
}
}
}
i, j := 0, 0
for i < len(oldLines) && j < len(newLines) {
if oldLines[i] == newLines[j] {
i++
j++
} else if cur[i+1][j] >= cur[i][j+1] {
hunks = append(hunks, hunkLine{"-", oldLines[i]})
i++
} else {
hunks = append(hunks, hunkLine{"+", newLines[j]})
j++
}
}
for ; i < len(oldLines); i++ {
hunks = append(hunks, hunkLine{"-", oldLines[i]})
}
for ; j < len(newLines); j++ {
hunks = append(hunks, hunkLine{"+", newLines[j]})
}
if len(hunks) == 0 {
return ""
}
if len(hunks) > maxCtx*2 {
var b strings.Builder
for k := 0; k < maxCtx; k++ {
b.WriteString(hunks[k].prefix)
b.WriteString(hunks[k].text)
b.WriteString("\n")
}
b.WriteString("... (" + fmt.Sprintf("%d", len(hunks)-maxCtx*2) + " lines omitted) ...\n")
for k := len(hunks) - maxCtx; k < len(hunks); k++ {
b.WriteString(hunks[k].prefix)
b.WriteString(hunks[k].text)
b.WriteString("\n")
}
return b.String()
}
var b strings.Builder
for _, h := range hunks {
b.WriteString(h.prefix)
b.WriteString(h.text)
b.WriteString("\n")
}
return b.String()
}
// ListCommits lists commits for a workspace folder with pagination
func (s *FileCommitService) ListCommits(ctx context.Context, folderID string, page, pageSize int, orderBy string, desc bool) ([]*entity.FileCommit, int64, error) {
return s.commitDAO.ListByFolderID(ctx, dao.DB, folderID, page, pageSize, orderBy, desc)