2026-04-09 10:01:23 +08:00
|
|
|
package services
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"time"
|
|
|
|
|
|
2026-05-31 18:43:48 +08:00
|
|
|
"agent-desk/internal/ai/rag"
|
|
|
|
|
"agent-desk/internal/models"
|
|
|
|
|
"agent-desk/internal/pkg/dto"
|
|
|
|
|
"agent-desk/internal/pkg/dto/request"
|
|
|
|
|
"agent-desk/internal/pkg/enums"
|
|
|
|
|
"agent-desk/internal/pkg/errorsx"
|
|
|
|
|
"agent-desk/internal/pkg/utils"
|
|
|
|
|
"agent-desk/internal/repositories"
|
|
|
|
|
|
|
|
|
|
"agent-desk/internal/pkg/httpx/params"
|
|
|
|
|
|
2026-04-09 10:01:23 +08:00
|
|
|
"github.com/mlogclub/simple/common/strs"
|
|
|
|
|
"github.com/mlogclub/simple/sqls"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var KnowledgeDocumentService = newKnowledgeDocumentService()
|
|
|
|
|
|
|
|
|
|
func newKnowledgeDocumentService() *knowledgeDocumentService {
|
|
|
|
|
return &knowledgeDocumentService{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type knowledgeDocumentService struct {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) Get(id int64) *models.KnowledgeDocument {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.Get(sqls.DB(), id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) Take(where ...interface{}) *models.KnowledgeDocument {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.Take(sqls.DB(), where...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) Find(cnd *sqls.Cnd) []models.KnowledgeDocument {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.Find(sqls.DB(), cnd)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) FindOne(cnd *sqls.Cnd) *models.KnowledgeDocument {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.FindOne(sqls.DB(), cnd)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) FindPageByParams(params *params.QueryParams) (list []models.KnowledgeDocument, paging *sqls.Paging) {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.FindPageByParams(sqls.DB(), params)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) FindPageByCnd(cnd *sqls.Cnd) (list []models.KnowledgeDocument, paging *sqls.Paging) {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.FindPageByCnd(sqls.DB(), cnd)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 13:43:22 +08:00
|
|
|
func (s *knowledgeDocumentService) FindPageListByCnd(cnd *sqls.Cnd) (list []models.KnowledgeDocument, paging *sqls.Paging) {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.FindPageListByCnd(sqls.DB(), cnd)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 10:01:23 +08:00
|
|
|
func (s *knowledgeDocumentService) Count(cnd *sqls.Cnd) int64 {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.Count(sqls.DB(), cnd)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) Create(t *models.KnowledgeDocument) error {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.Create(sqls.DB(), t)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) Update(t *models.KnowledgeDocument) error {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.Update(sqls.DB(), t)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) Updates(id int64, columns map[string]interface{}) error {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.Updates(sqls.DB(), id, columns)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) UpdateColumn(id int64, name string, value interface{}) error {
|
|
|
|
|
return repositories.KnowledgeDocumentRepository.UpdateColumn(sqls.DB(), id, name, value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) Delete(id int64) {
|
|
|
|
|
repositories.KnowledgeDocumentRepository.Delete(sqls.DB(), id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) CreateKnowledgeDocument(req request.CreateKnowledgeDocumentRequest, operator *dto.AuthPrincipal) (*models.KnowledgeDocument, error) {
|
|
|
|
|
if operator == nil {
|
2026-06-02 20:51:13 +08:00
|
|
|
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
kb := KnowledgeBaseService.Get(req.KnowledgeBaseID)
|
|
|
|
|
if kb == nil {
|
2026-06-02 20:51:13 +08:00
|
|
|
return nil, errorsx.InvalidParamI18n("error.e0283")
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
if kb.KnowledgeType == string(enums.KnowledgeBaseTypeFAQ) {
|
2026-06-02 20:51:13 +08:00
|
|
|
return nil, errorsx.InvalidParamI18n("error.e0026")
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
2026-06-02 15:37:50 +08:00
|
|
|
if _, err := KnowledgeDirectoryService.RequireUsableDirectory(req.KnowledgeBaseID, req.DirectoryID); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-04-09 10:01:23 +08:00
|
|
|
item, err := s.buildKnowledgeDocumentModel(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
item.Status = enums.StatusOk
|
|
|
|
|
item.IndexStatus = enums.KnowledgeDocumentIndexStatusPending
|
|
|
|
|
item.IndexError = ""
|
|
|
|
|
item.IndexedAt = nil
|
|
|
|
|
item.AuditFields = utils.BuildAuditFields(operator)
|
|
|
|
|
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
|
|
|
|
if err := ctx.Tx.Create(item).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if err := rag.Index.IndexDocumentByID(context.Background(), item.ID); err != nil {
|
|
|
|
|
slog.Error("failed to index created knowledge document", "document_id", item.ID, "error", err)
|
|
|
|
|
}
|
|
|
|
|
item = s.Get(item.ID)
|
|
|
|
|
return item, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) UpdateKnowledgeDocument(req request.UpdateKnowledgeDocumentRequest, operator *dto.AuthPrincipal) error {
|
|
|
|
|
if operator == nil {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.UnauthorizedI18n("error.auth.expired")
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
current := s.Get(req.ID)
|
|
|
|
|
if current == nil {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.InvalidParamI18n("error.e0218")
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
kb := KnowledgeBaseService.Get(req.KnowledgeBaseID)
|
|
|
|
|
if kb == nil {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.InvalidParamI18n("error.e0283")
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
if kb.KnowledgeType == string(enums.KnowledgeBaseTypeFAQ) {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.InvalidParamI18n("error.e0026")
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
2026-06-02 15:37:50 +08:00
|
|
|
if _, err := KnowledgeDirectoryService.RequireUsableDirectory(req.KnowledgeBaseID, req.DirectoryID); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-04-09 10:01:23 +08:00
|
|
|
item, err := s.buildKnowledgeDocumentModel(req.CreateKnowledgeDocumentRequest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
oldKnowledgeBaseID := current.KnowledgeBaseID
|
|
|
|
|
if err := repositories.KnowledgeDocumentRepository.Updates(sqls.DB(), req.ID, map[string]any{
|
|
|
|
|
"knowledge_base_id": item.KnowledgeBaseID,
|
2026-06-02 15:37:50 +08:00
|
|
|
"directory_id": item.DirectoryID,
|
2026-04-09 10:01:23 +08:00
|
|
|
"title": item.Title,
|
|
|
|
|
"content_type": item.ContentType,
|
|
|
|
|
"content_hash": item.ContentHash,
|
|
|
|
|
"content": item.Content,
|
|
|
|
|
"index_status": enums.KnowledgeDocumentIndexStatusPending,
|
|
|
|
|
"indexed_at": nil,
|
|
|
|
|
"index_error": "",
|
|
|
|
|
"update_user_id": operator.UserID,
|
|
|
|
|
"update_user_name": operator.Username,
|
|
|
|
|
"updated_at": time.Now(),
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if oldKnowledgeBaseID != item.KnowledgeBaseID {
|
2026-06-02 11:25:30 +08:00
|
|
|
if err := rag.Index.RemoveDocumentIndex(context.Background(), req.ID); err != nil {
|
2026-04-09 10:01:23 +08:00
|
|
|
slog.Error("failed to remove old document index after knowledge base change", "document_id", req.ID, "knowledge_base_id", oldKnowledgeBaseID, "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := rag.Index.IndexDocumentByID(context.Background(), req.ID); err != nil {
|
|
|
|
|
slog.Error("failed to reindex updated knowledge document", "document_id", req.ID, "error", err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) DeleteKnowledgeDocument(id int64) error {
|
2026-06-02 11:33:24 +08:00
|
|
|
if err := repositories.KnowledgeDocumentRepository.Updates(sqls.DB(), id, map[string]any{
|
|
|
|
|
"status": enums.StatusDeleted,
|
|
|
|
|
"updated_at": time.Now(),
|
2026-04-09 10:01:23 +08:00
|
|
|
}); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-06-02 11:33:24 +08:00
|
|
|
return rag.Index.RemoveDocumentIndex(context.Background(), id)
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-02 16:13:08 +08:00
|
|
|
func (s *knowledgeDocumentService) BatchMoveKnowledgeDocuments(req request.BatchMoveKnowledgeDocumentRequest, operator *dto.AuthPrincipal) error {
|
|
|
|
|
if operator == nil {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.UnauthorizedI18n("error.auth.expired")
|
2026-06-02 16:13:08 +08:00
|
|
|
}
|
|
|
|
|
ids := uniquePositiveIDs(req.IDs)
|
|
|
|
|
if len(ids) == 0 {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.InvalidParamI18n("error.e0333")
|
2026-06-02 16:13:08 +08:00
|
|
|
}
|
|
|
|
|
kb := KnowledgeBaseService.Get(req.KnowledgeBaseID)
|
|
|
|
|
if kb == nil {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.InvalidParamI18n("error.e0283")
|
2026-06-02 16:13:08 +08:00
|
|
|
}
|
|
|
|
|
if kb.KnowledgeType == string(enums.KnowledgeBaseTypeFAQ) {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.InvalidParamI18n("error.e0026")
|
2026-06-02 16:13:08 +08:00
|
|
|
}
|
|
|
|
|
if _, err := KnowledgeDirectoryService.RequireUsableDirectory(req.KnowledgeBaseID, req.DirectoryID); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
for _, id := range ids {
|
|
|
|
|
current := s.Get(id)
|
|
|
|
|
if current == nil || current.Status == enums.StatusDeleted {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.InvalidParamI18n("error.e0218")
|
2026-06-02 16:13:08 +08:00
|
|
|
}
|
|
|
|
|
if current.KnowledgeBaseID != req.KnowledgeBaseID {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.InvalidParamI18n("error.e0139")
|
2026-06-02 16:13:08 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
now := time.Now()
|
|
|
|
|
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
|
|
|
|
for _, id := range ids {
|
|
|
|
|
if err := repositories.KnowledgeDocumentRepository.Updates(ctx.Tx, id, map[string]any{
|
|
|
|
|
"directory_id": req.DirectoryID,
|
|
|
|
|
"index_status": enums.KnowledgeDocumentIndexStatusPending,
|
|
|
|
|
"indexed_at": nil,
|
|
|
|
|
"index_error": "",
|
|
|
|
|
"update_user_id": operator.UserID,
|
|
|
|
|
"update_user_name": operator.Username,
|
|
|
|
|
"updated_at": now,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
for _, id := range ids {
|
|
|
|
|
if err := rag.Index.IndexDocumentByID(context.Background(), id); err != nil {
|
|
|
|
|
slog.Error("failed to reindex moved knowledge document", "document_id", id, "error", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *knowledgeDocumentService) BatchDeleteKnowledgeDocuments(req request.BatchDeleteKnowledgeDocumentRequest) error {
|
|
|
|
|
ids := uniquePositiveIDs(req.IDs)
|
|
|
|
|
if len(ids) == 0 {
|
2026-06-02 20:51:13 +08:00
|
|
|
return errorsx.InvalidParamI18n("error.e0331")
|
2026-06-02 16:13:08 +08:00
|
|
|
}
|
|
|
|
|
for _, id := range ids {
|
|
|
|
|
if err := s.DeleteKnowledgeDocument(id); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 10:01:23 +08:00
|
|
|
func (s *knowledgeDocumentService) buildKnowledgeDocumentModel(req request.CreateKnowledgeDocumentRequest) (*models.KnowledgeDocument, error) {
|
|
|
|
|
if strs.IsBlank(string(req.ContentType)) {
|
|
|
|
|
req.ContentType = enums.KnowledgeDocumentContentTypeHTML
|
|
|
|
|
}
|
|
|
|
|
if req.ContentType != enums.KnowledgeDocumentContentTypeHTML && req.ContentType != enums.KnowledgeDocumentContentTypeMarkdown {
|
2026-06-02 20:51:13 +08:00
|
|
|
return nil, errorsx.InvalidParamI18n("error.e0129")
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
plainText := rag.ExtractPlainText(req.Content, req.ContentType)
|
|
|
|
|
item := &models.KnowledgeDocument{
|
|
|
|
|
KnowledgeBaseID: req.KnowledgeBaseID,
|
2026-06-02 15:37:50 +08:00
|
|
|
DirectoryID: req.DirectoryID,
|
2026-04-09 10:01:23 +08:00
|
|
|
Title: req.Title,
|
|
|
|
|
ContentType: req.ContentType,
|
|
|
|
|
Content: req.Content,
|
|
|
|
|
}
|
|
|
|
|
if plainText != "" {
|
|
|
|
|
hash := sha256.Sum256([]byte(plainText))
|
|
|
|
|
item.ContentHash = hex.EncodeToString(hash[:])
|
|
|
|
|
}
|
|
|
|
|
return item, nil
|
|
|
|
|
}
|
2026-06-02 16:13:08 +08:00
|
|
|
|
|
|
|
|
func uniquePositiveIDs(ids []int64) []int64 {
|
|
|
|
|
result := make([]int64, 0, len(ids))
|
|
|
|
|
seen := make(map[int64]struct{}, len(ids))
|
|
|
|
|
for _, id := range ids {
|
|
|
|
|
if id <= 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, ok := seen[id]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[id] = struct{}{}
|
|
|
|
|
result = append(result, id)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|