mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-02 22:07:31 +08:00
feat(go): port knowledge compilation template/group + wiki artifact/nav/skill REST APIs from Python (#17656)
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user