package service import ( "context" "crypto/sha256" "fmt" "sort" "strings" "github.com/Tencent/WeKnora/internal/types" ) // Reuse the same access-checked scope resolver as retrieval. Descriptions are // persisted document summaries, not a claim that every raw chunk was read. func (s *sessionService) KnowledgeReadingOverview(ctx context.Context, kbs, files []string, scopes []types.TagScope) (*types.KnowledgeReadingOverview, error) { tenant, ok := types.TenantIDFromContext(ctx) if !ok { return nil, fmt.Errorf("workspace ID not found in context") } targets, err := s.buildSearchTargets(ctx, tenant, kbs, files, scopes) if err != nil { return nil, err } result := &types.KnowledgeReadingOverview{Entries: []types.KnowledgeReadingEntry{}} scopeParts := []string{} seen := map[string]bool{} for _, target := range targets { ids, tags := append([]string(nil), target.KnowledgeIDs...), append([]string(nil), target.TagIDs...) sort.Strings(ids) sort.Strings(tags) scopeParts = append(scopeParts, fmt.Sprintf("%s|%s|%v|%v", target.KnowledgeBaseID, target.Type, ids, tags)) base, checkErr := s.knowledgeBaseService.GetKnowledgeBaseByID(ctx, target.KnowledgeBaseID) if checkErr != nil { return nil, checkErr } if base == nil { return nil, fmt.Errorf("知识库不存在或无权使用") } listCtx := ctx if base.TenantID != tenant { if s.kbShareService == nil { return nil, fmt.Errorf("知识库不存在或无权使用") } allowed, shareErr := s.kbShareService.HasTenantKBPermission(ctx, base.ID, tenant, types.TenantRoleFromContext(ctx), types.OrgRoleViewer) if shareErr != nil || !allowed { return nil, fmt.Errorf("知识库不存在或无权使用") } listCtx = context.WithValue(ctx, types.TenantIDContextKey, base.TenantID) } var rows []*types.Knowledge if target.Type == types.SearchTargetTypeKnowledge { rows, err = s.knowledgeService.GetKnowledgeBatchWithSharedAccess(ctx, tenant, target.KnowledgeIDs) } else { rows, err = s.knowledgeService.ListKnowledgeByKnowledgeBaseID(listCtx, target.KnowledgeBaseID) } if err != nil { return nil, err } for _, row := range rows { if row == nil || row.KnowledgeBaseID != target.KnowledgeBaseID || seen[row.ID] || row.EnableStatus == "disabled" || row.ParseStatus != types.ParseStatusCompleted { continue } seen[row.ID] = true title := row.Title if title == "" { title = row.FileName } summary := "" // FAQ tags select chunks within one document; its whole-document // summary could expose content outside the chosen category. if row.SummaryStatus == types.SummaryStatusCompleted && len(target.TagIDs) == 0 { summary = strings.TrimSpace(row.Description) } entry := types.KnowledgeReadingEntry{ID: row.ID, KnowledgeBaseID: row.KnowledgeBaseID, Title: title, Folder: row.FolderPath, Summary: summary} entry.Fingerprint = fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%s|%s|%s", row.ID, row.FileHash, row.UpdatedAt.UTC().Format("2006-01-02T15:04:05.999999999Z"), title, summary)))) result.Entries = append(result.Entries, entry) result.DocumentCount++ if summary != "" { result.SummaryCount++ } } } sort.Slice(result.Entries, func(i,j int) bool { return result.Entries[i].ID < result.Entries[j].ID }) hash := sha256.New() sort.Strings(scopeParts) for _, part := range scopeParts { fmt.Fprintln(hash, part) } for _, entry := range result.Entries { fmt.Fprintln(hash, entry.Fingerprint) } result.Fingerprint = fmt.Sprintf("%x", hash.Sum(nil)) return result, nil }