Scope knowledge and image libraries by brand
This commit is contained in:
@@ -353,6 +353,7 @@ func (s *ArticleImitationService) executeGeneration(ctx context.Context, job art
|
||||
knowledgeContext, resolveErr := s.knowledge.ResolveContext(
|
||||
ctx,
|
||||
job.TenantID,
|
||||
int64(extractInt(job.InputParams, "brand_id")),
|
||||
knowledgeGroupIDs,
|
||||
buildImitationKnowledgeQuery(job.InputParams),
|
||||
)
|
||||
|
||||
@@ -492,6 +492,7 @@ func (s *ArticleService) loadArticleDetail(ctx context.Context, tenantID, brandI
|
||||
ctx,
|
||||
s.pool,
|
||||
tenantID,
|
||||
brandID,
|
||||
markdownValue,
|
||||
item.CoverImageAssetID,
|
||||
)
|
||||
@@ -628,6 +629,7 @@ func (s *ArticleService) Update(ctx context.Context, id int64, req UpdateArticle
|
||||
ctx,
|
||||
tx,
|
||||
actor.TenantID,
|
||||
brandID,
|
||||
markdown,
|
||||
coverImageAssetID,
|
||||
)
|
||||
@@ -1037,12 +1039,13 @@ func (s *ArticleService) resolveArticleImageMetadata(
|
||||
ctx context.Context,
|
||||
q imageAssetLookupQueryer,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
markdown string,
|
||||
coverImageAssetID *int64,
|
||||
) (string, []int64, *int64, error) {
|
||||
referencedImageAssetIDs := extractReferencedImageAssetIDsFromMarkdown(markdown)
|
||||
imageAssetIDsToValidate := collectImageAssetIDsForValidation(referencedImageAssetIDs, coverImageAssetID)
|
||||
activeImageAssetIDs, err := s.loadActiveImageAssetIDSet(ctx, q, tenantID, imageAssetIDsToValidate)
|
||||
activeImageAssetIDs, err := s.loadActiveImageAssetIDSet(ctx, q, tenantID, brandID, imageAssetIDsToValidate)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
@@ -1064,6 +1067,7 @@ func (s *ArticleService) loadActiveImageAssetIDSet(
|
||||
ctx context.Context,
|
||||
q imageAssetLookupQueryer,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
imageAssetIDs []int64,
|
||||
) (map[int64]struct{}, error) {
|
||||
if len(imageAssetIDs) == 0 {
|
||||
@@ -1073,11 +1077,12 @@ func (s *ArticleService) loadActiveImageAssetIDSet(
|
||||
rows, err := q.Query(ctx, `
|
||||
SELECT id
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND id = ANY($2::BIGINT[])
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`, tenantID, imageAssetIDs)
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND id = ANY($3::BIGINT[])
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
`, tenantID, brandID, imageAssetIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50012, "update_failed", "failed to validate image assets")
|
||||
}
|
||||
|
||||
@@ -809,6 +809,7 @@ func (s *EnterpriseSiteService) PublishArticle(ctx context.Context, connectionID
|
||||
contentHTML, coverURL := s.prepareEnterpriseSitePublishAssets(
|
||||
ctx,
|
||||
actor.TenantID,
|
||||
brandID,
|
||||
contentHTML,
|
||||
coverURLInput,
|
||||
coverImageAssetID,
|
||||
@@ -1865,20 +1866,20 @@ func enterpriseSitePublishCoverInput(article enterpriseSitePublishArticle, req P
|
||||
return coverURL, coverImageAssetID
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSitePublishAssets(ctx context.Context, tenantID int64, contentHTML, coverURL string, coverImageAssetID *int64) (string, string) {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSitePublishAssets(ctx context.Context, tenantID int64, brandID int64, contentHTML, coverURL string, coverImageAssetID *int64) (string, string) {
|
||||
base := s.enterpriseSitePublicBaseURL()
|
||||
preparedHTML := s.prepareEnterpriseSiteImageURLs(ctx, tenantID, contentHTML, base)
|
||||
preparedCover := s.prepareEnterpriseSiteCoverURL(ctx, tenantID, coverURL, coverImageAssetID, base)
|
||||
preparedHTML := s.prepareEnterpriseSiteImageURLs(ctx, tenantID, brandID, contentHTML, base)
|
||||
preparedCover := s.prepareEnterpriseSiteCoverURL(ctx, tenantID, brandID, coverURL, coverImageAssetID, base)
|
||||
return preparedHTML, preparedCover
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteCoverURL(ctx context.Context, tenantID int64, coverURL string, coverImageAssetID *int64, baseURL string) string {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteCoverURL(ctx context.Context, tenantID int64, brandID int64, coverURL string, coverImageAssetID *int64, baseURL string) string {
|
||||
if coverImageAssetID != nil && *coverImageAssetID > 0 {
|
||||
if dataURI := s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, *coverImageAssetID); dataURI != "" {
|
||||
if dataURI := s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, brandID, *coverImageAssetID); dataURI != "" {
|
||||
return dataURI
|
||||
}
|
||||
}
|
||||
return s.prepareEnterpriseSiteAssetURL(ctx, tenantID, coverURL, baseURL)
|
||||
return s.prepareEnterpriseSiteAssetURL(ctx, tenantID, brandID, coverURL, baseURL)
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) enterpriseSitePublicBaseURL() string {
|
||||
@@ -1918,7 +1919,7 @@ func normalizeEnterpriseSiteAssetBaseURL(value string) string {
|
||||
return strings.TrimRight(parsed.String(), "/")
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLs(ctx context.Context, tenantID int64, fragment string, baseURL string) string {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLs(ctx context.Context, tenantID int64, brandID int64, fragment string, baseURL string) string {
|
||||
if strings.TrimSpace(fragment) == "" || !strings.Contains(strings.ToLower(fragment), "<img") {
|
||||
return fragment
|
||||
}
|
||||
@@ -1929,37 +1930,37 @@ func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLs(ctx context.Conte
|
||||
}
|
||||
nodes, err := html.ParseFragment(strings.NewReader(fragment), contextNode)
|
||||
if err != nil {
|
||||
return s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, fragment, baseURL)
|
||||
return s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, brandID, fragment, baseURL)
|
||||
}
|
||||
for _, node := range nodes {
|
||||
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, node, baseURL)
|
||||
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, brandID, node, baseURL)
|
||||
}
|
||||
nodes = enterpriseSitePruneEmptyImageWrapperNodes(nodes)
|
||||
return enterpriseSiteRenderHTMLNodes(nodes, s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, fragment, baseURL))
|
||||
return enterpriseSiteRenderHTMLNodes(nodes, s.prepareEnterpriseSiteImageURLsFallback(ctx, tenantID, brandID, fragment, baseURL))
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageNodeURLs(ctx context.Context, tenantID int64, node *html.Node, baseURL string) {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageNodeURLs(ctx context.Context, tenantID int64, brandID int64, node *html.Node, baseURL string) {
|
||||
if node == nil {
|
||||
return
|
||||
}
|
||||
if node.Type == html.ElementNode && strings.EqualFold(node.Data, "img") {
|
||||
if !s.prepareEnterpriseSiteImageAttrs(ctx, tenantID, node, baseURL) {
|
||||
if !s.prepareEnterpriseSiteImageAttrs(ctx, tenantID, brandID, node, baseURL) {
|
||||
enterpriseSiteRemoveHTMLNode(node)
|
||||
return
|
||||
}
|
||||
}
|
||||
for child := node.FirstChild; child != nil; {
|
||||
next := child.NextSibling
|
||||
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, child, baseURL)
|
||||
s.prepareEnterpriseSiteImageNodeURLs(ctx, tenantID, brandID, child, baseURL)
|
||||
child = next
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageAttrs(ctx context.Context, tenantID int64, node *html.Node, baseURL string) bool {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageAttrs(ctx context.Context, tenantID int64, brandID int64, node *html.Node, baseURL string) bool {
|
||||
if node == nil {
|
||||
return true
|
||||
}
|
||||
dataURI := s.enterpriseSiteDataURIFromImageNode(ctx, tenantID, node)
|
||||
dataURI := s.enterpriseSiteDataURIFromImageNode(ctx, tenantID, brandID, node)
|
||||
hasImageURL := false
|
||||
for idx := range node.Attr {
|
||||
key := strings.ToLower(strings.TrimSpace(node.Attr[idx].Key))
|
||||
@@ -1971,7 +1972,7 @@ func (s *EnterpriseSiteService) prepareEnterpriseSiteImageAttrs(ctx context.Cont
|
||||
node.Attr[idx].Val = dataURI
|
||||
continue
|
||||
}
|
||||
node.Attr[idx].Val = s.prepareEnterpriseSiteAssetURL(ctx, tenantID, node.Attr[idx].Val, baseURL)
|
||||
node.Attr[idx].Val = s.prepareEnterpriseSiteAssetURL(ctx, tenantID, brandID, node.Attr[idx].Val, baseURL)
|
||||
if strings.TrimSpace(node.Attr[idx].Val) == "" {
|
||||
return false
|
||||
}
|
||||
@@ -2046,12 +2047,12 @@ func enterpriseSitePruneEmptyImageWrapperNodes(nodes []*html.Node) []*html.Node
|
||||
return pruned
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteAssetURL(ctx context.Context, tenantID int64, value string, baseURL string) string {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteAssetURL(ctx context.Context, tenantID int64, brandID int64, value string, baseURL string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || strings.HasPrefix(strings.ToLower(value), "data:") {
|
||||
return value
|
||||
}
|
||||
if dataURI := s.enterpriseSiteDataURIFromAssetURL(ctx, tenantID, value); dataURI != "" {
|
||||
if dataURI := s.enterpriseSiteDataURIFromAssetURL(ctx, tenantID, brandID, value); dataURI != "" {
|
||||
return dataURI
|
||||
}
|
||||
if enterpriseSiteLooksLikeSaaSAssetURL(value) {
|
||||
@@ -2060,34 +2061,34 @@ func (s *EnterpriseSiteService) prepareEnterpriseSiteAssetURL(ctx context.Contex
|
||||
return enterpriseSiteAbsoluteAssetURL(value, baseURL)
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLsFallback(ctx context.Context, tenantID int64, fragment string, baseURL string) string {
|
||||
func (s *EnterpriseSiteService) prepareEnterpriseSiteImageURLsFallback(ctx context.Context, tenantID int64, brandID int64, fragment string, baseURL string) string {
|
||||
return enterpriseSiteImageAttrPattern.ReplaceAllStringFunc(fragment, func(match string) string {
|
||||
parts := enterpriseSiteImageAttrPattern.FindStringSubmatch(match)
|
||||
if len(parts) != 4 {
|
||||
return match
|
||||
}
|
||||
return parts[1] + s.prepareEnterpriseSiteAssetURL(ctx, tenantID, parts[2], baseURL) + parts[3]
|
||||
return parts[1] + s.prepareEnterpriseSiteAssetURL(ctx, tenantID, brandID, parts[2], baseURL) + parts[3]
|
||||
})
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromAssetURL(ctx context.Context, tenantID int64, value string) string {
|
||||
objectKey, ok := s.enterpriseSiteObjectKeyFromAssetURL(ctx, tenantID, value)
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromAssetURL(ctx context.Context, tenantID int64, brandID int64, value string) string {
|
||||
objectKey, ok := s.enterpriseSiteObjectKeyFromAssetURL(ctx, tenantID, brandID, value)
|
||||
if !ok || s == nil || s.storage == nil {
|
||||
return ""
|
||||
}
|
||||
return s.enterpriseSiteDataURIFromObjectKey(ctx, objectKey)
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageNode(ctx context.Context, tenantID int64, node *html.Node) string {
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageNode(ctx context.Context, tenantID int64, brandID int64, node *html.Node) string {
|
||||
assetID := enterpriseSiteImageNodeAssetID(node)
|
||||
if assetID <= 0 {
|
||||
return ""
|
||||
}
|
||||
return s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, assetID)
|
||||
return s.enterpriseSiteDataURIFromImageAssetID(ctx, tenantID, brandID, assetID)
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageAssetID(ctx context.Context, tenantID, assetID int64) string {
|
||||
objectKey, ok := s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, assetID)
|
||||
func (s *EnterpriseSiteService) enterpriseSiteDataURIFromImageAssetID(ctx context.Context, tenantID, brandID, assetID int64) string {
|
||||
objectKey, ok := s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, brandID, assetID)
|
||||
if !ok || s == nil || s.storage == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -2143,27 +2144,54 @@ func enterpriseSiteImageNodeAssetID(node *html.Node) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) enterpriseSiteObjectKeyFromAssetURL(ctx context.Context, tenantID int64, value string) (string, bool) {
|
||||
func (s *EnterpriseSiteService) enterpriseSiteObjectKeyFromAssetURL(ctx context.Context, tenantID int64, brandID int64, value string) (string, bool) {
|
||||
if objectKey, ok := enterpriseSiteObjectKeyFromPublicAssetURL(value, tenantID, s.enterpriseSiteAssetTokenSecret()); ok {
|
||||
if !s.enterpriseSiteImageObjectKeyBelongsToBrand(ctx, tenantID, brandID, objectKey) {
|
||||
return "", false
|
||||
}
|
||||
return objectKey, true
|
||||
}
|
||||
assetID, ok := enterpriseSiteAssetIDFromURL(value)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, assetID)
|
||||
return s.loadEnterpriseSiteImageAssetObjectKey(ctx, tenantID, brandID, assetID)
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) loadEnterpriseSiteImageAssetObjectKey(ctx context.Context, tenantID, assetID int64) (string, bool) {
|
||||
if s == nil || s.pool == nil || tenantID <= 0 || assetID <= 0 {
|
||||
func (s *EnterpriseSiteService) enterpriseSiteImageObjectKeyBelongsToBrand(ctx context.Context, tenantID, brandID int64, objectKey string) bool {
|
||||
if s == nil || s.pool == nil || !enterpriseSiteObjectKeyIsImageLibraryAsset(objectKey, tenantID) {
|
||||
return true
|
||||
}
|
||||
if tenantID <= 0 || brandID <= 0 || strings.TrimSpace(objectKey) == "" {
|
||||
return false
|
||||
}
|
||||
var exists bool
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND object_key = $3
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
)
|
||||
`, tenantID, brandID, objectKey).Scan(&exists); err != nil {
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
func (s *EnterpriseSiteService) loadEnterpriseSiteImageAssetObjectKey(ctx context.Context, tenantID, brandID, assetID int64) (string, bool) {
|
||||
if s == nil || s.pool == nil || tenantID <= 0 || brandID <= 0 || assetID <= 0 {
|
||||
return "", false
|
||||
}
|
||||
var objectKey string
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT object_key
|
||||
FROM image_assets
|
||||
WHERE id = $1 AND tenant_id = $2 AND status = 'active' AND deleted_at IS NULL
|
||||
`, assetID, tenantID).Scan(&objectKey); err != nil {
|
||||
SELECT object_key
|
||||
FROM image_assets
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND status = 'active' AND deleted_at IS NULL
|
||||
`, assetID, tenantID, brandID).Scan(&objectKey); err != nil {
|
||||
return "", false
|
||||
}
|
||||
if !enterpriseSiteObjectKeyAllowedForTenant(objectKey, tenantID) {
|
||||
@@ -2264,6 +2292,14 @@ func enterpriseSiteObjectKeyAllowedForTenant(objectKey string, tenantID int64) b
|
||||
strings.HasPrefix(objectKey, tenantPrefix+"articles/")
|
||||
}
|
||||
|
||||
func enterpriseSiteObjectKeyIsImageLibraryAsset(objectKey string, tenantID int64) bool {
|
||||
if tenantID <= 0 {
|
||||
return false
|
||||
}
|
||||
tenantPrefix := "tenants/" + strconv.FormatInt(tenantID, 10) + "/"
|
||||
return strings.HasPrefix(strings.TrimSpace(objectKey), tenantPrefix+"images/")
|
||||
}
|
||||
|
||||
func enterpriseSiteAbsoluteImageURLs(fragment string, baseURL string) string {
|
||||
if strings.TrimSpace(fragment) == "" || !strings.Contains(strings.ToLower(fragment), "<img") {
|
||||
return fragment
|
||||
|
||||
@@ -245,6 +245,7 @@ func TestPrepareEnterpriseSiteImageURLsEmbedsPublicAssetImages(t *testing.T) {
|
||||
got := svc.prepareEnterpriseSiteImageURLs(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
`<p><img src="/api/public/assets/`+token+`" data-src="/api/public/assets/`+token+`" /></p>`,
|
||||
"http://127.0.0.1:8080",
|
||||
)
|
||||
@@ -271,6 +272,7 @@ func TestPrepareEnterpriseSiteImageURLsEmbedsPublicAssetImagesWithStaleSignature
|
||||
got := svc.prepareEnterpriseSiteImageURLs(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
`<p><img src="http://localhost:5178/api/public/assets/`+token+`" /></p>`,
|
||||
"http://127.0.0.1:8080",
|
||||
)
|
||||
@@ -297,6 +299,7 @@ func TestPrepareEnterpriseSiteImageURLsStripsSaaSImageAttrs(t *testing.T) {
|
||||
got := svc.prepareEnterpriseSiteImageURLs(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
`<p><img src="/api/public/assets/`+token+`" alt="" data-asset-id="35" asset-id="35" /></p>`,
|
||||
"http://127.0.0.1:8080",
|
||||
)
|
||||
@@ -320,6 +323,7 @@ func TestPrepareEnterpriseSiteImageURLsDropsUnavailableSaaSAssetImage(t *testing
|
||||
got := svc.prepareEnterpriseSiteImageURLs(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
`<p>前文</p><p class="article-editor-image article-editor-image--center" align="center"><img src="http://localhost:5178/api/public/assets/`+token+`" alt="" /></p><p>后文</p>`,
|
||||
"http://127.0.0.1:8080",
|
||||
)
|
||||
@@ -346,6 +350,7 @@ func TestPrepareEnterpriseSiteImageURLsEmbedsIncompletePublicAssetPaths(t *testi
|
||||
got := svc.prepareEnterpriseSiteImageURLs(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
`<p><img src="api/public/assets/`+token+`?format=png" /></p>`,
|
||||
"http://127.0.0.1:8080",
|
||||
)
|
||||
@@ -373,7 +378,7 @@ func TestMarkdownRenderedEnterpriseSiteImageEmbedsIncompletePublicAssetPath(t *t
|
||||
t.Fatalf("render markdown: %v", err)
|
||||
}
|
||||
|
||||
got := svc.prepareEnterpriseSiteImageURLs(context.Background(), 7, html, "http://127.0.0.1:8080")
|
||||
got := svc.prepareEnterpriseSiteImageURLs(context.Background(), 7, 11, html, "http://127.0.0.1:8080")
|
||||
|
||||
if storage.gotKey != objectKey {
|
||||
t.Fatalf("object key = %q, want %q", storage.gotKey, objectKey)
|
||||
@@ -398,6 +403,7 @@ func TestPrepareEnterpriseSitePublishAssetsEmbedsIncompleteCoverAssetPath(t *tes
|
||||
got := svc.prepareEnterpriseSiteCoverURL(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
"api/public/assets/"+token+"?format=png",
|
||||
nil,
|
||||
"http://127.0.0.1:8080",
|
||||
@@ -423,6 +429,7 @@ func TestPrepareEnterpriseSiteCoverURLDropsUnavailableSaaSAssetURL(t *testing.T)
|
||||
got := svc.prepareEnterpriseSiteCoverURL(
|
||||
context.Background(),
|
||||
7,
|
||||
11,
|
||||
"http://localhost:5178/api/public/assets/"+token,
|
||||
nil,
|
||||
"http://127.0.0.1:8080",
|
||||
|
||||
@@ -376,6 +376,7 @@ func (s *PromptRuleGenerationService) ProcessQueuedTask(ctx context.Context, tas
|
||||
knowledgeContext, err := s.knowledge.ResolveContext(
|
||||
ctx,
|
||||
task.TenantID,
|
||||
int64(extractInt(inputParams, "brand_id")),
|
||||
knowledgeGroupIDs,
|
||||
buildPromptRuleKnowledgeQuery(rule, inputParams),
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/messaging/rabbitmq"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/middleware"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/objectstorage"
|
||||
@@ -48,9 +49,13 @@ type ImageReferenceArticleBrief struct {
|
||||
|
||||
func (s *ImageFolderService) List(ctx context.Context) ([]ImageFolderItem, error) {
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := repository.NewImageFolderRepository(s.pool)
|
||||
|
||||
folders, err := repo.List(ctx, tenantID)
|
||||
folders, err := repo.List(ctx, tenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -77,11 +82,15 @@ func (s *ImageFolderService) Create(ctx context.Context, name string) (int64, er
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
repo := repository.NewImageFolderRepository(s.pool)
|
||||
|
||||
id, err := repo.Create(ctx, tenantID, name)
|
||||
id, err := repo.Create(ctx, tenantID, brandID, name)
|
||||
if err != nil {
|
||||
if isConstraintError(err, "uq_image_folders_tenant_name") {
|
||||
if isConstraintError(err, "uq_image_folders_tenant_name") || isConstraintError(err, "uq_image_folders_tenant_brand_name") {
|
||||
return 0, response.ErrConflict(40901, "folder_name_exists", "a folder with this name already exists")
|
||||
}
|
||||
return 0, err
|
||||
@@ -96,13 +105,17 @@ func (s *ImageFolderService) Update(ctx context.Context, id int64, name string)
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo := repository.NewImageFolderRepository(s.pool)
|
||||
if _, err := repo.Get(ctx, tenantID, id); err != nil {
|
||||
if _, err := repo.Get(ctx, tenantID, brandID, id); err != nil {
|
||||
return mapImageFolderNotFound(err)
|
||||
}
|
||||
|
||||
if err := repo.Update(ctx, tenantID, id, name); err != nil {
|
||||
if isConstraintError(err, "uq_image_folders_tenant_name") {
|
||||
if err := repo.Update(ctx, tenantID, brandID, id, name); err != nil {
|
||||
if isConstraintError(err, "uq_image_folders_tenant_name") || isConstraintError(err, "uq_image_folders_tenant_brand_name") {
|
||||
return response.ErrConflict(40901, "folder_name_exists", "a folder with this name already exists")
|
||||
}
|
||||
return err
|
||||
@@ -112,13 +125,17 @@ func (s *ImageFolderService) Update(ctx context.Context, id int64, name string)
|
||||
|
||||
func (s *ImageFolderService) DeletePreview(ctx context.Context, folderID int64) (*FolderDeletePreview, error) {
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folderRepo := repository.NewImageFolderRepository(s.pool)
|
||||
|
||||
if _, err := folderRepo.Get(ctx, tenantID, folderID); err != nil {
|
||||
if _, err := folderRepo.Get(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return nil, mapImageFolderNotFound(err)
|
||||
}
|
||||
|
||||
imageCount, err := folderRepo.CountActiveImages(ctx, tenantID, folderID)
|
||||
imageCount, err := folderRepo.CountActiveImages(ctx, tenantID, brandID, folderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -131,19 +148,23 @@ func (s *ImageFolderService) DeletePreview(ctx context.Context, folderID int64)
|
||||
|
||||
func (s *ImageFolderService) Delete(ctx context.Context, folderID int64, force bool) error {
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
folderRepo := repository.NewImageFolderRepository(s.pool)
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
|
||||
if _, err := folderRepo.Get(ctx, tenantID, folderID); err != nil {
|
||||
if _, err := folderRepo.Get(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return mapImageFolderNotFound(err)
|
||||
}
|
||||
|
||||
imageCount, err := folderRepo.CountActiveImages(ctx, tenantID, folderID)
|
||||
imageCount, err := folderRepo.CountActiveImages(ctx, tenantID, brandID, folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if imageCount == 0 {
|
||||
return folderRepo.Delete(ctx, tenantID, folderID)
|
||||
return folderRepo.Delete(ctx, tenantID, brandID, folderID)
|
||||
}
|
||||
|
||||
if !force {
|
||||
@@ -159,7 +180,7 @@ func (s *ImageFolderService) Delete(ctx context.Context, folderID int64, force b
|
||||
}
|
||||
}
|
||||
|
||||
images, err := imageRepo.ListActiveByFolder(ctx, tenantID, folderID)
|
||||
images, err := imageRepo.ListActiveByFolder(ctx, tenantID, brandID, folderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -174,10 +195,10 @@ func (s *ImageFolderService) Delete(ctx context.Context, folderID int64, force b
|
||||
|
||||
imageRepoTx := repository.NewImageRepository(tx)
|
||||
folderRepoTx := repository.NewImageFolderRepository(tx)
|
||||
if err := imageRepoTx.MarkImagesInFolderPendingDelete(ctx, tenantID, folderID); err != nil {
|
||||
if err := imageRepoTx.MarkImagesInFolderPendingDelete(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return response.ErrInternal(50048, "folder_image_delete_prepare_failed", "failed to prepare folder images for deletion")
|
||||
}
|
||||
if err := folderRepoTx.Delete(ctx, tenantID, folderID); err != nil {
|
||||
if err := folderRepoTx.Delete(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
@@ -185,7 +206,8 @@ func (s *ImageFolderService) Delete(ctx context.Context, folderID int64, force b
|
||||
}
|
||||
|
||||
for _, image := range images {
|
||||
if err := s.imageSvc.finalizeDeleteNowOrEnqueue(ctx, tenantID, image.ID); err != nil {
|
||||
deleteCtx := auth.WithCurrentBrandID(ctx, brandID)
|
||||
if err := s.imageSvc.finalizeDeleteNowOrEnqueue(deleteCtx, tenantID, image.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,9 +158,13 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actor := auth.MustActor(ctx)
|
||||
if folderID != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, *folderID); err != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, brandID, *folderID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -181,7 +185,7 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
|
||||
sizeBytes := int64(len(webpBytes))
|
||||
contentHash := hashImageContent(webpBytes)
|
||||
existingAsset, err := s.findActiveImageAssetByContentHash(ctx, tenantID, contentHash)
|
||||
existingAsset, err := s.findActiveImageAssetByContentHash(ctx, tenantID, brandID, contentHash)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50012, "image_asset_lookup_failed", "failed to check existing image asset")
|
||||
}
|
||||
@@ -219,9 +223,9 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
return nil, response.ErrInternal(50015, "image_quota_reserve_failed", "failed to reserve image storage quota")
|
||||
}
|
||||
|
||||
assetID, err := s.insertPendingImageAsset(ctx, tx, tenantID, folderID, objectKey, displayName, sizeBytes, contentHash, actor.UserID)
|
||||
assetID, err := s.insertPendingImageAsset(ctx, tx, tenantID, brandID, folderID, objectKey, displayName, sizeBytes, contentHash, actor.UserID)
|
||||
if err != nil {
|
||||
if isConstraintError(err, "fk_image_assets_folder") {
|
||||
if isConstraintError(err, "fk_image_assets_folder") || isConstraintError(err, "fk_image_assets_folder_brand") {
|
||||
return nil, response.ErrNotFound(40402, "image_folder_not_found", "folder not found")
|
||||
}
|
||||
return nil, response.ErrInternal(50016, "image_asset_create_failed", "failed to create image asset")
|
||||
@@ -241,7 +245,7 @@ func (s *ImageService) Upload(ctx context.Context, fileName string, content []by
|
||||
}
|
||||
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, assetID, "active"); err != nil {
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, brandID, assetID, "active"); err != nil {
|
||||
s.logError("failed to mark image active after upload", zap.Int64("tenant_id", tenantID), zap.Int64("asset_id", assetID), zap.Error(err))
|
||||
if queueErr := s.enqueueFinalizeUploadTask(tenantID, assetID); queueErr == nil {
|
||||
return &ImageUploadResult{
|
||||
@@ -282,9 +286,14 @@ func (s *ImageService) List(ctx context.Context, folderID *int64, query *string,
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo := repository.NewImageRepository(s.pool)
|
||||
assets, total, err := repo.List(ctx, repository.ImageAssetListParams{
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
FolderID: folderID,
|
||||
SearchQuery: searchQuery,
|
||||
PageSize: pageSize,
|
||||
@@ -323,9 +332,13 @@ func (s *ImageService) Update(ctx context.Context, id int64, req ImageUpdateRequ
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
repo := repository.NewImageRepository(s.pool)
|
||||
|
||||
asset, err := repo.Get(ctx, tenantID, id)
|
||||
asset, err := repo.Get(ctx, tenantID, brandID, id)
|
||||
if err != nil {
|
||||
return mapImageNotFound(err)
|
||||
}
|
||||
@@ -341,7 +354,7 @@ func (s *ImageService) Update(ctx context.Context, id int64, req ImageUpdateRequ
|
||||
if len(name) > 255 {
|
||||
return response.ErrBadRequest(40002, "invalid_name", "name must be at most 255 characters")
|
||||
}
|
||||
if err := repo.UpdateName(ctx, tenantID, id, name); err != nil {
|
||||
if err := repo.UpdateName(ctx, tenantID, brandID, id, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -352,12 +365,12 @@ func (s *ImageService) Update(ctx context.Context, id int64, req ImageUpdateRequ
|
||||
return err
|
||||
}
|
||||
if folderID != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, *folderID); err != nil {
|
||||
if err := s.ensureFolderExists(ctx, tenantID, brandID, *folderID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := repo.UpdateFolder(ctx, tenantID, id, folderID); err != nil {
|
||||
if isConstraintError(err, "fk_image_assets_folder") {
|
||||
if err := repo.UpdateFolder(ctx, tenantID, brandID, id, folderID); err != nil {
|
||||
if isConstraintError(err, "fk_image_assets_folder") || isConstraintError(err, "fk_image_assets_folder_brand") {
|
||||
return response.ErrNotFound(40402, "image_folder_not_found", "folder not found")
|
||||
}
|
||||
return err
|
||||
@@ -374,7 +387,7 @@ type existingImageAsset struct {
|
||||
SizeBytes int64
|
||||
}
|
||||
|
||||
func (s *ImageService) findActiveImageAssetByContentHash(ctx context.Context, tenantID int64, contentHash string) (*existingImageAsset, error) {
|
||||
func (s *ImageService) findActiveImageAssetByContentHash(ctx context.Context, tenantID, brandID int64, contentHash string) (*existingImageAsset, error) {
|
||||
if strings.TrimSpace(contentHash) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -384,12 +397,13 @@ func (s *ImageService) findActiveImageAssetByContentHash(ctx context.Context, te
|
||||
SELECT id, object_key, name, size_bytes
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND content_hash = $2
|
||||
AND brand_id = $2
|
||||
AND content_hash = $3
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`, tenantID, contentHash).Scan(&asset.ID, &asset.ObjectKey, &asset.Name, &asset.SizeBytes)
|
||||
`, tenantID, brandID, contentHash).Scan(&asset.ID, &asset.ObjectKey, &asset.Name, &asset.SizeBytes)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -404,6 +418,7 @@ func (s *ImageService) insertPendingImageAsset(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
folderID *int64,
|
||||
objectKey string,
|
||||
name string,
|
||||
@@ -412,10 +427,10 @@ func (s *ImageService) insertPendingImageAsset(
|
||||
createdBy int64,
|
||||
) (int64, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO image_assets (tenant_id, folder_id, object_key, name, size_bytes, status, content_hash, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, 'pending_upload', $6, $7)
|
||||
INSERT INTO image_assets (tenant_id, brand_id, folder_id, object_key, name, size_bytes, status, content_hash, created_by)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'pending_upload', $7, $8)
|
||||
RETURNING id
|
||||
`, tenantID, nullableImageFolderValue(folderID), objectKey, name, sizeBytes, contentHash, createdBy)
|
||||
`, tenantID, brandID, nullableImageFolderValue(folderID), objectKey, name, sizeBytes, contentHash, createdBy)
|
||||
|
||||
var assetID int64
|
||||
if err := row.Scan(&assetID); err != nil {
|
||||
@@ -426,10 +441,14 @@ RETURNING id
|
||||
|
||||
func (s *ImageService) GetReferences(ctx context.Context, imageID int64) (*ImageReferenceDetail, error) {
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
refRepo := repository.NewImageReferenceRepository(s.pool)
|
||||
|
||||
asset, err := imageRepo.Get(ctx, tenantID, imageID)
|
||||
asset, err := imageRepo.Get(ctx, tenantID, brandID, imageID)
|
||||
if err != nil {
|
||||
return nil, mapImageNotFound(err)
|
||||
}
|
||||
@@ -494,10 +513,14 @@ func (s *ImageService) Delete(ctx context.Context, id int64, force bool) error {
|
||||
}
|
||||
|
||||
tenantID := middleware.TenantIDFromCtx(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
refRepo := repository.NewImageReferenceRepository(s.pool)
|
||||
|
||||
asset, err := imageRepo.Get(ctx, tenantID, id)
|
||||
asset, err := imageRepo.Get(ctx, tenantID, brandID, id)
|
||||
if err != nil {
|
||||
return mapImageNotFound(err)
|
||||
}
|
||||
@@ -515,7 +538,7 @@ func (s *ImageService) Delete(ctx context.Context, id int64, force bool) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, id, "pending_delete"); err != nil {
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, brandID, id, "pending_delete"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -556,7 +579,13 @@ func (s *ImageService) rollbackUpload(ctx context.Context, tenantID, assetID, si
|
||||
imageRepo := repository.NewImageRepository(s.pool)
|
||||
usageRepo := repository.NewImageUsageRepository(s.pool)
|
||||
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, assetID, "upload_failed"); err != nil {
|
||||
brandID, err := s.lookupImageAssetBrandID(ctx, tenantID, assetID)
|
||||
if err != nil {
|
||||
s.logError("failed to resolve image brand after upload rollback", zap.Int64("tenant_id", tenantID), zap.Int64("asset_id", assetID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := imageRepo.UpdateStatus(ctx, tenantID, brandID, assetID, "upload_failed"); err != nil {
|
||||
s.logError("failed to mark image upload as failed", zap.Int64("tenant_id", tenantID), zap.Int64("asset_id", assetID), zap.Error(err))
|
||||
}
|
||||
if err := usageRepo.Release(ctx, tenantID, sizeBytes); err != nil {
|
||||
@@ -605,11 +634,11 @@ func (s *ImageService) ProcessFinalizeUpload(ctx context.Context, tenantID, asse
|
||||
|
||||
imageRepoTx := repository.NewImageRepository(tx)
|
||||
if exists {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, assetID, "active"); err != nil {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, lockedAsset.BrandID, assetID, "active"); err != nil {
|
||||
return response.ErrInternal(50033, "image_upload_finalize_failed", "failed to activate uploaded image")
|
||||
}
|
||||
} else {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, assetID, "upload_failed"); err != nil {
|
||||
if err := imageRepoTx.UpdateStatus(ctx, tenantID, lockedAsset.BrandID, assetID, "upload_failed"); err != nil {
|
||||
return response.ErrInternal(50034, "image_upload_finalize_failed", "failed to mark image upload as failed")
|
||||
}
|
||||
usageRepoTx := repository.NewImageUsageRepository(tx)
|
||||
@@ -680,7 +709,7 @@ WHERE tenant_id = $1 AND image_asset_id = $2
|
||||
|
||||
imageRepoTx := repository.NewImageRepository(tx)
|
||||
usageRepoTx := repository.NewImageUsageRepository(tx)
|
||||
if err := imageRepoTx.MarkDeleted(ctx, tenantID, assetID); err != nil {
|
||||
if err := imageRepoTx.MarkDeleted(ctx, tenantID, lockedAsset.BrandID, assetID); err != nil {
|
||||
return response.ErrInternal(50044, "image_delete_finalize_failed", "failed to finalize image deletion")
|
||||
}
|
||||
if err := usageRepoTx.Release(ctx, tenantID, lockedAsset.SizeBytes); err != nil {
|
||||
@@ -725,10 +754,15 @@ func (s *ImageService) enqueueFinalizeDeleteTask(tenantID, assetID int64) error
|
||||
}
|
||||
|
||||
func (s *ImageService) loadImageAsset(ctx context.Context, tenantID, assetID int64) (*repository.ImageAsset, error) {
|
||||
return repository.NewImageRepository(s.pool).Get(ctx, tenantID, assetID)
|
||||
brandID, err := s.lookupImageAssetBrandID(ctx, tenantID, assetID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return repository.NewImageRepository(s.pool).Get(ctx, tenantID, brandID, assetID)
|
||||
}
|
||||
|
||||
type lockedImageAsset struct {
|
||||
BrandID int64
|
||||
ObjectKey string
|
||||
SizeBytes int64
|
||||
Status string
|
||||
@@ -739,11 +773,11 @@ func (s *ImageService) lockImageAsset(ctx context.Context, tx pgx.Tx, tenantID,
|
||||
var asset lockedImageAsset
|
||||
var deletedAt *time.Time
|
||||
err := tx.QueryRow(ctx, `
|
||||
SELECT object_key, size_bytes, status, deleted_at
|
||||
SELECT brand_id, object_key, size_bytes, status, deleted_at
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1 AND id = $2
|
||||
FOR UPDATE
|
||||
`, tenantID, assetID).Scan(&asset.ObjectKey, &asset.SizeBytes, &asset.Status, &deletedAt)
|
||||
`, tenantID, assetID).Scan(&asset.BrandID, &asset.ObjectKey, &asset.SizeBytes, &asset.Status, &deletedAt)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, false, nil
|
||||
@@ -754,14 +788,26 @@ FOR UPDATE
|
||||
return &asset, true, nil
|
||||
}
|
||||
|
||||
func (s *ImageService) ensureFolderExists(ctx context.Context, tenantID, folderID int64) error {
|
||||
func (s *ImageService) ensureFolderExists(ctx context.Context, tenantID, brandID, folderID int64) error {
|
||||
folderRepo := repository.NewImageFolderRepository(s.pool)
|
||||
if _, err := folderRepo.Get(ctx, tenantID, folderID); err != nil {
|
||||
if _, err := folderRepo.Get(ctx, tenantID, brandID, folderID); err != nil {
|
||||
return mapImageFolderNotFound(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ImageService) lookupImageAssetBrandID(ctx context.Context, tenantID, assetID int64) (int64, error) {
|
||||
var brandID int64
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT brand_id
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1 AND id = $2
|
||||
`, tenantID, assetID).Scan(&brandID); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return brandID, nil
|
||||
}
|
||||
|
||||
func (s *ImageService) lookupImageQuotaBytes(ctx context.Context, tenantID int64) (int64, error) {
|
||||
var quotaBytes int64
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
|
||||
@@ -61,6 +61,7 @@ type knowledgeParseJob struct {
|
||||
type knowledgeItemRecord struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
BrandID int64
|
||||
GroupID int64
|
||||
GroupName string
|
||||
Name string
|
||||
@@ -207,12 +208,16 @@ func (s *KnowledgeService) runtimeConfig() knowledgeRuntimeConfig {
|
||||
|
||||
func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, name, parent_id, sort_order, created_at
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1 AND deleted_at IS NULL
|
||||
WHERE tenant_id = $1 AND brand_id = $2 AND deleted_at IS NULL
|
||||
ORDER BY sort_order, created_at, id
|
||||
`, actor.TenantID)
|
||||
`, actor.TenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50050, "knowledge_groups_query_failed", "failed to list knowledge groups")
|
||||
}
|
||||
@@ -240,6 +245,7 @@ func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResp
|
||||
SELECT ki.group_id, COUNT(DISTINCT ki.id)::bigint
|
||||
FROM knowledge_items ki
|
||||
WHERE ki.tenant_id = $1
|
||||
AND ki.brand_id = $2
|
||||
AND ki.deleted_at IS NULL
|
||||
AND ki.status = 'completed'
|
||||
AND EXISTS (
|
||||
@@ -251,7 +257,7 @@ func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResp
|
||||
AND kcm.status = 'active'
|
||||
)
|
||||
GROUP BY ki.group_id
|
||||
`, actor.TenantID)
|
||||
`, actor.TenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50050, "knowledge_group_counts_query_failed", "failed to list knowledge group counts")
|
||||
}
|
||||
@@ -310,6 +316,10 @@ func (s *KnowledgeService) ListGroups(ctx context.Context) ([]KnowledgeGroupResp
|
||||
|
||||
func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRequest) (*KnowledgeGroupResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
@@ -318,7 +328,7 @@ func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRe
|
||||
|
||||
var parentID *int64
|
||||
if req.ParentID != nil && *req.ParentID > 0 {
|
||||
if err := s.ensureRootGroup(ctx, actor.TenantID, *req.ParentID); err != nil {
|
||||
if err := s.ensureRootGroup(ctx, actor.TenantID, brandID, *req.ParentID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parentID = req.ParentID
|
||||
@@ -333,11 +343,11 @@ func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRe
|
||||
groupID int64
|
||||
createdAt time.Time
|
||||
)
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
INSERT INTO knowledge_groups (tenant_id, name, parent_id, sort_order)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
INSERT INTO knowledge_groups (tenant_id, brand_id, name, parent_id, sort_order)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, created_at
|
||||
`, actor.TenantID, name, parentID, sortOrder).Scan(&groupID, &createdAt)
|
||||
`, actor.TenantID, brandID, name, parentID, sortOrder).Scan(&groupID, &createdAt)
|
||||
if err != nil {
|
||||
return nil, response.ErrConflict(40950, "knowledge_group_create_failed", "knowledge group already exists or cannot be created")
|
||||
}
|
||||
@@ -355,6 +365,10 @@ func (s *KnowledgeService) CreateGroup(ctx context.Context, req KnowledgeGroupRe
|
||||
|
||||
func (s *KnowledgeService) UpdateGroup(ctx context.Context, groupID int64, req KnowledgeGroupRequest) error {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if name == "" {
|
||||
@@ -369,8 +383,8 @@ func (s *KnowledgeService) UpdateGroup(ctx context.Context, groupID int64, req K
|
||||
tag, err := s.pool.Exec(ctx, `
|
||||
UPDATE knowledge_groups
|
||||
SET name = $1, sort_order = $2, updated_at = NOW()
|
||||
WHERE id = $3 AND tenant_id = $4 AND deleted_at IS NULL
|
||||
`, name, sortOrder, groupID, actor.TenantID)
|
||||
WHERE id = $3 AND tenant_id = $4 AND brand_id = $5 AND deleted_at IS NULL
|
||||
`, name, sortOrder, groupID, actor.TenantID, brandID)
|
||||
if err != nil {
|
||||
return response.ErrConflict(40950, "knowledge_group_update_failed", "knowledge group already exists or cannot be updated")
|
||||
}
|
||||
@@ -382,6 +396,10 @@ func (s *KnowledgeService) UpdateGroup(ctx context.Context, groupID int64, req K
|
||||
|
||||
func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
@@ -389,7 +407,7 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
groupIDs, err := s.listGroupSubtreeIDs(ctx, tx, actor.TenantID, groupID)
|
||||
groupIDs, err := s.listGroupSubtreeIDs(ctx, tx, actor.TenantID, brandID, groupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -397,7 +415,7 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
|
||||
return response.ErrNotFound(40451, "knowledge_group_not_found", "knowledge group not found")
|
||||
}
|
||||
|
||||
itemIDs, err := s.listActiveKnowledgeItemIDs(ctx, tx, actor.TenantID, groupIDs)
|
||||
itemIDs, err := s.listActiveKnowledgeItemIDs(ctx, tx, actor.TenantID, brandID, groupIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -407,9 +425,10 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
|
||||
UPDATE knowledge_items
|
||||
SET deleted_at = NOW(), updated_at = NOW(), status = 'deleted'
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND deleted_at IS NULL
|
||||
AND id = ANY($2::bigint[])
|
||||
`, actor.TenantID, itemIDs); err != nil {
|
||||
AND id = ANY($3::bigint[])
|
||||
`, actor.TenantID, brandID, itemIDs); err != nil {
|
||||
return response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to delete knowledge items")
|
||||
}
|
||||
|
||||
@@ -448,9 +467,10 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
|
||||
UPDATE knowledge_groups
|
||||
SET deleted_at = NOW(), updated_at = NOW()
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND deleted_at IS NULL
|
||||
AND id = ANY($2::bigint[])
|
||||
`, actor.TenantID, groupIDs); err != nil {
|
||||
AND id = ANY($3::bigint[])
|
||||
`, actor.TenantID, brandID, groupIDs); err != nil {
|
||||
return response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to delete knowledge groups")
|
||||
}
|
||||
|
||||
@@ -475,8 +495,12 @@ func (s *KnowledgeService) DeleteGroup(ctx context.Context, groupID int64) error
|
||||
|
||||
func (s *KnowledgeService) ListItems(ctx context.Context, groupID *int64) ([]KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if groupID != nil && *groupID > 0 {
|
||||
if err := s.ensureGroupAccessible(ctx, actor.TenantID, *groupID); err != nil {
|
||||
if err := s.ensureGroupAccessible(ctx, actor.TenantID, brandID, *groupID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -485,23 +509,24 @@ func (s *KnowledgeService) ListItems(ctx context.Context, groupID *int64) ([]Kno
|
||||
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.status,
|
||||
ki.size_bytes, ki.error_message, ki.created_at, ki.updated_at
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
|
||||
WHERE ki.tenant_id = $1
|
||||
AND ki.brand_id = $2
|
||||
AND ki.deleted_at IS NULL
|
||||
`
|
||||
args := []any{actor.TenantID}
|
||||
args := []any{actor.TenantID, brandID}
|
||||
if groupID != nil && *groupID > 0 {
|
||||
query += `
|
||||
AND ki.group_id IN (
|
||||
WITH RECURSIVE selected_groups AS (
|
||||
SELECT id
|
||||
FROM knowledge_groups
|
||||
WHERE id = $2 AND tenant_id = $1 AND deleted_at IS NULL
|
||||
WHERE id = $3 AND tenant_id = $1 AND brand_id = $2 AND deleted_at IS NULL
|
||||
UNION ALL
|
||||
SELECT child.id
|
||||
FROM knowledge_groups child
|
||||
JOIN selected_groups parent ON child.parent_id = parent.id
|
||||
WHERE child.tenant_id = $1 AND child.deleted_at IS NULL
|
||||
WHERE child.tenant_id = $1 AND child.brand_id = $2 AND child.deleted_at IS NULL
|
||||
)
|
||||
SELECT id FROM selected_groups
|
||||
)
|
||||
@@ -551,6 +576,10 @@ func (s *KnowledgeService) ListItems(ctx context.Context, groupID *int64) ([]Kno
|
||||
|
||||
func (s *KnowledgeService) GetItemDetail(ctx context.Context, itemID int64) (*KnowledgeItemDetailResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
item KnowledgeItemDetailResponse
|
||||
@@ -560,13 +589,13 @@ func (s *KnowledgeService) GetItemDetail(ctx context.Context, itemID int64) (*Kn
|
||||
createdAt, updatedAt time.Time
|
||||
)
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.status,
|
||||
ki.size_bytes, ki.error_message, ki.markdown_content, ki.created_at, ki.updated_at
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
|
||||
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.deleted_at IS NULL
|
||||
`, itemID, actor.TenantID).Scan(
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
|
||||
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.brand_id = $3 AND ki.deleted_at IS NULL
|
||||
`, itemID, actor.TenantID, brandID).Scan(
|
||||
&item.ID,
|
||||
&item.GroupID,
|
||||
&item.GroupName,
|
||||
@@ -597,6 +626,10 @@ func (s *KnowledgeService) GetItemDetail(ctx context.Context, itemID int64) (*Kn
|
||||
|
||||
func (s *KnowledgeService) CreateTextItem(ctx context.Context, req KnowledgeTextItemRequest) (*KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.validateIngestionDependencies("text"); err != nil {
|
||||
return nil, err
|
||||
@@ -612,7 +645,7 @@ func (s *KnowledgeService) CreateTextItem(ctx context.Context, req KnowledgeText
|
||||
return nil, response.ErrBadRequest(40055, "knowledge_text_name_too_long", "knowledge text name must be at most 20 characters")
|
||||
}
|
||||
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, knowledgeIngestInput{
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, brandID, knowledgeIngestInput{
|
||||
GroupID: req.GroupID,
|
||||
Name: name,
|
||||
SourceType: "text",
|
||||
@@ -623,6 +656,10 @@ func (s *KnowledgeService) CreateTextItem(ctx context.Context, req KnowledgeText
|
||||
|
||||
func (s *KnowledgeService) CreateURLItem(ctx context.Context, req KnowledgeURLItemRequest) (*KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.validateIngestionDependencies("website"); err != nil {
|
||||
return nil, err
|
||||
@@ -633,7 +670,7 @@ func (s *KnowledgeService) CreateURLItem(ctx context.Context, req KnowledgeURLIt
|
||||
return nil, response.ErrBadRequest(40052, "knowledge_url_invalid", "knowledge url is invalid")
|
||||
}
|
||||
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, knowledgeIngestInput{
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, brandID, knowledgeIngestInput{
|
||||
GroupID: req.GroupID,
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
SourceType: "website",
|
||||
@@ -649,6 +686,10 @@ func (s *KnowledgeService) CreateFileItem(
|
||||
content []byte,
|
||||
) (*KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.validateIngestionDependencies("document"); err != nil {
|
||||
return nil, err
|
||||
@@ -664,7 +705,7 @@ func (s *KnowledgeService) CreateFileItem(
|
||||
return nil, response.ErrServiceUnavailable(50357, "knowledge_storage_unavailable", err.Error())
|
||||
}
|
||||
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, knowledgeIngestInput{
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, brandID, knowledgeIngestInput{
|
||||
GroupID: groupID,
|
||||
Name: displayName,
|
||||
SourceType: "document",
|
||||
@@ -675,6 +716,10 @@ func (s *KnowledgeService) CreateFileItem(
|
||||
|
||||
func (s *KnowledgeService) DeleteItem(ctx context.Context, itemID int64) error {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
@@ -685,8 +730,8 @@ func (s *KnowledgeService) DeleteItem(ctx context.Context, itemID int64) error {
|
||||
tag, err := tx.Exec(ctx, `
|
||||
UPDATE knowledge_items
|
||||
SET deleted_at = NOW(), updated_at = NOW(), status = 'deleted'
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`, itemID, actor.TenantID)
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
`, itemID, actor.TenantID, brandID)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50054, "knowledge_item_delete_failed", "failed to delete knowledge item")
|
||||
}
|
||||
@@ -734,14 +779,18 @@ func (s *KnowledgeService) DeleteItem(ctx context.Context, itemID int64) error {
|
||||
|
||||
func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sourceType string
|
||||
var status string
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
err = s.pool.QueryRow(ctx, `
|
||||
SELECT source_type, status
|
||||
FROM knowledge_items
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`, itemID, actor.TenantID).Scan(&sourceType, &status)
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
`, itemID, actor.TenantID, brandID).Scan(&sourceType, &status)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, response.ErrNotFound(40450, "knowledge_item_not_found", "knowledge item not found")
|
||||
@@ -793,9 +842,10 @@ func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*Knowle
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND brand_id = $3
|
||||
AND deleted_at IS NULL
|
||||
AND status = 'failed'
|
||||
`, itemID, actor.TenantID)
|
||||
`, itemID, actor.TenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50055, "knowledge_item_retry_failed", "failed to reset knowledge item")
|
||||
}
|
||||
@@ -816,7 +866,7 @@ func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*Knowle
|
||||
return nil, response.ErrServiceUnavailable(50356, "knowledge_queue_unavailable", "knowledge parse queue is busy, please retry")
|
||||
}
|
||||
|
||||
items, err := s.listKnowledgeItemsByIDs(ctx, actor.TenantID, []int64{itemID})
|
||||
items, err := s.listKnowledgeItemsByIDs(ctx, actor.TenantID, brandID, []int64{itemID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -829,6 +879,7 @@ func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*Knowle
|
||||
func (s *KnowledgeService) ResolveContext(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
groupIDs []int64,
|
||||
query string,
|
||||
) (*KnowledgeContext, error) {
|
||||
@@ -871,7 +922,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
return nil, response.ErrServiceUnavailable(50352, "qdrant_unavailable", err.Error())
|
||||
}
|
||||
|
||||
searchGroupIDs, groupNames, err := s.resolveKnowledgeSearchScope(ctx, tenantID, requestedGroupIDs)
|
||||
searchGroupIDs, groupNames, err := s.resolveKnowledgeSearchScope(ctx, tenantID, brandID, requestedGroupIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -883,7 +934,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
GroupIDs: searchGroupIDs,
|
||||
GroupNames: groupNames,
|
||||
Snippets: []KnowledgeSnippet{},
|
||||
PreciseFacts: s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, nil),
|
||||
PreciseFacts: s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, nil),
|
||||
}
|
||||
|
||||
vectors, err := s.provider.Embed(ctx, queries)
|
||||
@@ -903,6 +954,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
points, err := s.store.Search(ctx, retrieval.SearchRequest{
|
||||
Vector: vector,
|
||||
TenantID: tenantID,
|
||||
BrandID: brandID,
|
||||
GroupIDs: searchGroupIDs,
|
||||
Limit: runtimeCfg.RecallLimit,
|
||||
})
|
||||
@@ -922,7 +974,7 @@ func (s *KnowledgeService) ResolveContext(
|
||||
return baseContext, nil
|
||||
}
|
||||
|
||||
candidates, err = s.filterActiveKnowledgeSnippets(ctx, tenantID, candidates)
|
||||
candidates, err = s.filterActiveKnowledgeSnippets(ctx, tenantID, brandID, candidates)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -939,17 +991,17 @@ func (s *KnowledgeService) ResolveContext(
|
||||
reranked, err := s.provider.Rerank(ctx, rerankQuery, documents, minInt(runtimeCfg.RerankTopN, len(documents)))
|
||||
if err != nil {
|
||||
selected := selectKnowledgeCandidateSnippets(candidates, runtimeCfg.RerankTopN)
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, selected)
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, brandID, selected)
|
||||
baseContext.Snippets = selected
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, selected)
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, selected)
|
||||
s.logKnowledgeResolve(baseContext, tenantID, rerankQuery, "rerank_failed_fallback", queryRewriteStage, queryRewriteModel, candidates, selected)
|
||||
return baseContext, nil
|
||||
}
|
||||
|
||||
selected := selectKnowledgeRerankedSnippets(candidates, reranked, runtimeCfg.RerankTopN)
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, selected)
|
||||
selected = s.enrichKnowledgePromptSnippets(ctx, tenantID, brandID, selected)
|
||||
baseContext.Snippets = selected
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, searchGroupIDs, selected)
|
||||
baseContext.PreciseFacts = s.collectKnowledgeContextPreciseFacts(ctx, tenantID, brandID, searchGroupIDs, selected)
|
||||
s.logKnowledgeResolve(baseContext, tenantID, rerankQuery, "resolved", queryRewriteStage, queryRewriteModel, candidates, selected)
|
||||
return baseContext, nil
|
||||
}
|
||||
@@ -1341,6 +1393,7 @@ type knowledgePromptItemRecord struct {
|
||||
func (s *KnowledgeService) enrichKnowledgePromptSnippets(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
snippets []KnowledgeSnippet,
|
||||
) []KnowledgeSnippet {
|
||||
if len(snippets) == 0 {
|
||||
@@ -1358,7 +1411,7 @@ func (s *KnowledgeService) enrichKnowledgePromptSnippets(
|
||||
return snippets
|
||||
}
|
||||
|
||||
items, err := s.loadKnowledgePromptItems(ctx, tenantID, itemIDs)
|
||||
items, err := s.loadKnowledgePromptItems(ctx, tenantID, brandID, itemIDs)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Warn("load knowledge prompt items failed", zap.Error(err))
|
||||
@@ -1395,6 +1448,7 @@ func (s *KnowledgeService) enrichKnowledgePromptSnippets(
|
||||
func (s *KnowledgeService) loadKnowledgePromptItems(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
itemIDs []int64,
|
||||
) ([]knowledgePromptItemRecord, error) {
|
||||
if len(itemIDs) == 0 {
|
||||
@@ -1404,12 +1458,13 @@ func (s *KnowledgeService) loadKnowledgePromptItems(
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.content_text, ki.markdown_content
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
|
||||
WHERE ki.tenant_id = $1
|
||||
AND ki.id = ANY($2::bigint[])
|
||||
AND ki.brand_id = $2
|
||||
AND ki.id = ANY($3::bigint[])
|
||||
AND ki.status = 'completed'
|
||||
AND ki.deleted_at IS NULL
|
||||
`, tenantID, itemIDs)
|
||||
`, tenantID, brandID, itemIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to load knowledge prompt items")
|
||||
}
|
||||
@@ -1438,6 +1493,7 @@ func (s *KnowledgeService) loadKnowledgePromptItems(
|
||||
func (s *KnowledgeService) collectKnowledgeContextPreciseFacts(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
groupIDs []int64,
|
||||
snippets []KnowledgeSnippet,
|
||||
) []string {
|
||||
@@ -1454,7 +1510,7 @@ func (s *KnowledgeService) collectKnowledgeContextPreciseFacts(
|
||||
}
|
||||
matchedItemIDs = normalizeInt64IDs(matchedItemIDs)
|
||||
|
||||
items, err := s.loadKnowledgePromptItemsByGroupIDs(ctx, tenantID, groupIDs, matchedItemIDs, 24)
|
||||
items, err := s.loadKnowledgePromptItemsByGroupIDs(ctx, tenantID, brandID, groupIDs, matchedItemIDs, 24)
|
||||
if err != nil {
|
||||
if s.logger != nil {
|
||||
s.logger.Warn("collect knowledge context precise facts failed", zap.Error(err))
|
||||
@@ -1506,6 +1562,7 @@ func (s *KnowledgeService) collectKnowledgeContextPreciseFacts(
|
||||
func (s *KnowledgeService) loadKnowledgePromptItemsByGroupIDs(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
groupIDs []int64,
|
||||
prioritizedItemIDs []int64,
|
||||
limit int,
|
||||
@@ -1524,31 +1581,34 @@ func (s *KnowledgeService) loadKnowledgePromptItemsByGroupIDs(
|
||||
SELECT id
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1
|
||||
AND id = ANY($2::bigint[])
|
||||
AND brand_id = $2
|
||||
AND id = ANY($3::bigint[])
|
||||
AND deleted_at IS NULL
|
||||
UNION ALL
|
||||
SELECT child.id
|
||||
FROM knowledge_groups child
|
||||
JOIN selected_groups parent ON child.parent_id = parent.id
|
||||
WHERE child.tenant_id = $1
|
||||
AND child.brand_id = $2
|
||||
AND child.deleted_at IS NULL
|
||||
)
|
||||
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.content_text, ki.markdown_content
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.deleted_at IS NULL
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id AND kg.deleted_at IS NULL
|
||||
WHERE ki.tenant_id = $1
|
||||
AND ki.brand_id = $2
|
||||
AND ki.status = 'completed'
|
||||
AND ki.deleted_at IS NULL
|
||||
AND ki.group_id IN (SELECT id FROM selected_groups)
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN ki.id = ANY($3::bigint[]) THEN 0
|
||||
WHEN ki.id = ANY($4::bigint[]) THEN 0
|
||||
ELSE 1
|
||||
END,
|
||||
ki.updated_at DESC,
|
||||
ki.id DESC
|
||||
LIMIT $4
|
||||
`, tenantID, groupIDs, prioritizedItemIDs, limit)
|
||||
LIMIT $5
|
||||
`, tenantID, brandID, groupIDs, prioritizedItemIDs, limit)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to load knowledge prompt items by groups")
|
||||
}
|
||||
@@ -1859,9 +1919,10 @@ func (s *KnowledgeService) validateIngestionDependencies(sourceType string) erro
|
||||
func (s *KnowledgeService) queueKnowledgeItem(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
input knowledgeIngestInput,
|
||||
) (*KnowledgeItemResponse, error) {
|
||||
resolvedGroupID, err := s.resolveKnowledgeStorageGroupID(ctx, tenantID, input.GroupID)
|
||||
resolvedGroupID, err := s.resolveKnowledgeStorageGroupID(ctx, tenantID, brandID, input.GroupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1880,7 +1941,7 @@ func (s *KnowledgeService) queueKnowledgeItem(
|
||||
input.ContentText = &contentText
|
||||
}
|
||||
|
||||
itemID, parseTaskID, err := s.createPendingKnowledgeItem(ctx, tenantID, input)
|
||||
itemID, parseTaskID, err := s.createPendingKnowledgeItem(ctx, tenantID, brandID, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1894,7 +1955,7 @@ func (s *KnowledgeService) queueKnowledgeItem(
|
||||
return nil, response.ErrServiceUnavailable(50356, "knowledge_queue_unavailable", "knowledge parse queue is busy, please retry")
|
||||
}
|
||||
|
||||
items, err := s.listKnowledgeItemsByIDs(ctx, tenantID, []int64{itemID})
|
||||
items, err := s.listKnowledgeItemsByIDs(ctx, tenantID, brandID, []int64{itemID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2064,6 +2125,7 @@ func (s *KnowledgeService) executeParseJob(ctx context.Context, job knowledgePar
|
||||
Vector: embeddings[index],
|
||||
Payload: map[string]any{
|
||||
"tenant_id": job.TenantID,
|
||||
"brand_id": item.BrandID,
|
||||
"group_id": item.GroupID,
|
||||
"group_name": item.GroupName,
|
||||
"item_id": item.ID,
|
||||
@@ -2105,14 +2167,15 @@ func (s *KnowledgeService) loadKnowledgeItemForParse(
|
||||
var contentText *string
|
||||
var markdownContent *string
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT ki.id, ki.tenant_id, ki.group_id, kg.name, ki.name, ki.source_type,
|
||||
SELECT ki.id, ki.tenant_id, ki.brand_id, ki.group_id, kg.name, ki.name, ki.source_type,
|
||||
ki.source_uri, ki.storage_key, ki.content_text, ki.markdown_content, ki.item_version
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id
|
||||
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.deleted_at IS NULL
|
||||
`, itemID, tenantID).Scan(
|
||||
&record.ID,
|
||||
&record.TenantID,
|
||||
&record.BrandID,
|
||||
&record.GroupID,
|
||||
&record.GroupName,
|
||||
&record.Name,
|
||||
@@ -2166,6 +2229,7 @@ type knowledgeChunkMetaInput struct {
|
||||
func (s *KnowledgeService) createPendingKnowledgeItem(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
input knowledgeIngestInput,
|
||||
) (int64, int64, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
@@ -2177,12 +2241,12 @@ func (s *KnowledgeService) createPendingKnowledgeItem(
|
||||
var itemID int64
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO knowledge_items (
|
||||
tenant_id, group_id, source_type, name, source_uri, storage_key,
|
||||
tenant_id, brand_id, group_id, source_type, name, source_uri, storage_key,
|
||||
content_text, markdown_content, status, size_bytes, item_version
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, 'pending', $8, 1)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NULL, 'pending', $9, 1)
|
||||
RETURNING id
|
||||
`, tenantID, input.GroupID, input.SourceType, input.Name, input.SourceURI, input.StorageKey, input.ContentText, input.SizeBytes).Scan(&itemID)
|
||||
`, tenantID, brandID, input.GroupID, input.SourceType, input.Name, input.SourceURI, input.StorageKey, input.ContentText, input.SizeBytes).Scan(&itemID)
|
||||
if err != nil {
|
||||
return 0, 0, response.ErrConflict(40951, "knowledge_item_create_failed", "failed to create knowledge item")
|
||||
}
|
||||
@@ -2434,15 +2498,15 @@ func buildKnowledgeSearchText(input knowledgeSearchTextInput) string {
|
||||
return strings.Join(metadata, "\n")
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID, groupID int64) error {
|
||||
func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID, brandID, groupID int64) error {
|
||||
var exists bool
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM knowledge_groups
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
)
|
||||
`, groupID, tenantID).Scan(&exists)
|
||||
`, groupID, tenantID, brandID).Scan(&exists)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
|
||||
}
|
||||
@@ -2452,8 +2516,8 @@ func (s *KnowledgeService) ensureGroupAccessible(ctx context.Context, tenantID,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, groupID int64) error {
|
||||
if err := s.ensureGroupAccessible(ctx, tenantID, groupID); err != nil {
|
||||
func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, brandID, groupID int64) error {
|
||||
if err := s.ensureGroupAccessible(ctx, tenantID, brandID, groupID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2464,10 +2528,11 @@ func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, groupI
|
||||
FROM knowledge_groups
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND brand_id = $3
|
||||
AND deleted_at IS NULL
|
||||
AND parent_id IS NULL
|
||||
)
|
||||
`, groupID, tenantID).Scan(&isRoot)
|
||||
`, groupID, tenantID, brandID).Scan(&isRoot)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
|
||||
}
|
||||
@@ -2477,8 +2542,8 @@ func (s *KnowledgeService) ensureRootGroup(ctx context.Context, tenantID, groupI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, groupID int64) error {
|
||||
if err := s.ensureGroupAccessible(ctx, tenantID, groupID); err != nil {
|
||||
func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, brandID, groupID int64) error {
|
||||
if err := s.ensureGroupAccessible(ctx, tenantID, brandID, groupID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2490,13 +2555,15 @@ func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, gr
|
||||
JOIN knowledge_groups parent
|
||||
ON parent.id = child.parent_id
|
||||
AND parent.tenant_id = child.tenant_id
|
||||
AND parent.brand_id = child.brand_id
|
||||
AND parent.deleted_at IS NULL
|
||||
WHERE child.id = $1
|
||||
AND child.tenant_id = $2
|
||||
AND child.brand_id = $3
|
||||
AND child.deleted_at IS NULL
|
||||
AND parent.parent_id IS NULL
|
||||
)
|
||||
`, groupID, tenantID).Scan(&storable)
|
||||
`, groupID, tenantID, brandID).Scan(&storable)
|
||||
if err != nil {
|
||||
return response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
|
||||
}
|
||||
@@ -2506,14 +2573,14 @@ func (s *KnowledgeService) ensureStorableGroup(ctx context.Context, tenantID, gr
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) resolveKnowledgeStorageGroupID(ctx context.Context, tenantID, groupID int64) (int64, error) {
|
||||
func (s *KnowledgeService) resolveKnowledgeStorageGroupID(ctx context.Context, tenantID, brandID, groupID int64) (int64, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return 0, response.ErrInternal(50055, "knowledge_group_resolve_failed", "failed to begin knowledge group transaction")
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
resolvedGroupID, err := s.resolveKnowledgeStorageGroupIDTx(ctx, tx, tenantID, groupID)
|
||||
resolvedGroupID, err := s.resolveKnowledgeStorageGroupIDTx(ctx, tx, tenantID, brandID, groupID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -2527,15 +2594,15 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupID(ctx context.Context, t
|
||||
func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
tenantID, groupID int64,
|
||||
tenantID, brandID, groupID int64,
|
||||
) (int64, error) {
|
||||
var parentID *int64
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT parent_id
|
||||
FROM knowledge_groups
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
FOR UPDATE
|
||||
`, groupID, tenantID).Scan(&parentID); err != nil {
|
||||
`, groupID, tenantID, brandID).Scan(&parentID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, response.ErrNotFound(40451, "knowledge_group_not_found", "knowledge group not found")
|
||||
}
|
||||
@@ -2547,8 +2614,8 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT parent_id
|
||||
FROM knowledge_groups
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`, *parentID, tenantID).Scan(&grandParentID); err != nil {
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
`, *parentID, tenantID, brandID).Scan(&grandParentID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, response.ErrNotFound(40451, "knowledge_group_not_found", "knowledge group not found")
|
||||
}
|
||||
@@ -2565,22 +2632,23 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
|
||||
SELECT id
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1
|
||||
AND parent_id = $2
|
||||
AND name = $3
|
||||
AND brand_id = $2
|
||||
AND parent_id = $3
|
||||
AND name = $4
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
`, tenantID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err == nil {
|
||||
`, tenantID, brandID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err == nil {
|
||||
return childGroupID, nil
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge group")
|
||||
}
|
||||
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO knowledge_groups (tenant_id, name, parent_id, sort_order)
|
||||
VALUES ($1, $2, $3, 0)
|
||||
INSERT INTO knowledge_groups (tenant_id, brand_id, name, parent_id, sort_order)
|
||||
VALUES ($1, $2, $3, $4, 0)
|
||||
RETURNING id
|
||||
`, tenantID, defaultKnowledgeChildGroupName, groupID).Scan(&childGroupID); err == nil {
|
||||
`, tenantID, brandID, defaultKnowledgeChildGroupName, groupID).Scan(&childGroupID); err == nil {
|
||||
return childGroupID, nil
|
||||
}
|
||||
|
||||
@@ -2588,12 +2656,13 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
|
||||
SELECT id
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1
|
||||
AND parent_id = $2
|
||||
AND name = $3
|
||||
AND brand_id = $2
|
||||
AND parent_id = $3
|
||||
AND name = $4
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
`, tenantID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err != nil {
|
||||
`, tenantID, brandID, groupID, defaultKnowledgeChildGroupName).Scan(&childGroupID); err != nil {
|
||||
return 0, response.ErrConflict(40950, "knowledge_group_create_failed", "failed to create default knowledge group")
|
||||
}
|
||||
return childGroupID, nil
|
||||
@@ -2602,22 +2671,22 @@ func (s *KnowledgeService) resolveKnowledgeStorageGroupIDTx(
|
||||
func (s *KnowledgeService) listGroupSubtreeIDs(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
tenantID, groupID int64,
|
||||
tenantID, brandID, groupID int64,
|
||||
) ([]int64, error) {
|
||||
rows, err := tx.Query(ctx, `
|
||||
WITH RECURSIVE selected_groups AS (
|
||||
SELECT id
|
||||
FROM knowledge_groups
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND deleted_at IS NULL
|
||||
UNION ALL
|
||||
SELECT child.id
|
||||
FROM knowledge_groups child
|
||||
JOIN selected_groups parent ON child.parent_id = parent.id
|
||||
WHERE child.tenant_id = $2 AND child.deleted_at IS NULL
|
||||
WHERE child.tenant_id = $2 AND child.brand_id = $3 AND child.deleted_at IS NULL
|
||||
)
|
||||
SELECT id
|
||||
FROM selected_groups
|
||||
`, groupID, tenantID)
|
||||
`, groupID, tenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to query knowledge groups")
|
||||
}
|
||||
@@ -2638,6 +2707,7 @@ func (s *KnowledgeService) listActiveKnowledgeItemIDs(
|
||||
ctx context.Context,
|
||||
tx pgx.Tx,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
groupIDs []int64,
|
||||
) ([]int64, error) {
|
||||
if len(groupIDs) == 0 {
|
||||
@@ -2648,9 +2718,10 @@ func (s *KnowledgeService) listActiveKnowledgeItemIDs(
|
||||
SELECT id
|
||||
FROM knowledge_items
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND deleted_at IS NULL
|
||||
AND group_id = ANY($2::bigint[])
|
||||
`, tenantID, groupIDs)
|
||||
AND group_id = ANY($3::bigint[])
|
||||
`, tenantID, brandID, groupIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50054, "knowledge_group_delete_failed", "failed to query knowledge items")
|
||||
}
|
||||
@@ -2714,6 +2785,7 @@ func (s *KnowledgeService) listKnowledgePointIDs(ctx context.Context, tenantID,
|
||||
func (s *KnowledgeService) filterActiveKnowledgeSnippets(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
candidates []KnowledgeSnippet,
|
||||
) ([]KnowledgeSnippet, error) {
|
||||
if len(candidates) == 0 {
|
||||
@@ -2735,10 +2807,11 @@ func (s *KnowledgeService) filterActiveKnowledgeSnippets(
|
||||
SELECT id
|
||||
FROM knowledge_items
|
||||
WHERE tenant_id = $1
|
||||
AND id = ANY($2::bigint[])
|
||||
AND brand_id = $2
|
||||
AND id = ANY($3::bigint[])
|
||||
AND status = 'completed'
|
||||
AND deleted_at IS NULL
|
||||
`, tenantID, itemIDs)
|
||||
`, tenantID, brandID, itemIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to filter deleted knowledge items")
|
||||
}
|
||||
@@ -2852,14 +2925,15 @@ func (s *KnowledgeService) loadKnowledgeItemForCleanup(
|
||||
var contentText *string
|
||||
var markdownContent *string
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT ki.id, ki.tenant_id, ki.group_id, kg.name, ki.name, ki.source_type,
|
||||
SELECT ki.id, ki.tenant_id, ki.brand_id, ki.group_id, kg.name, ki.name, ki.source_type,
|
||||
ki.source_uri, ki.storage_key, ki.content_text, ki.markdown_content, ki.item_version
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id
|
||||
WHERE ki.id = $1 AND ki.tenant_id = $2 AND ki.deleted_at IS NOT NULL
|
||||
`, itemID, tenantID).Scan(
|
||||
&record.ID,
|
||||
&record.TenantID,
|
||||
&record.BrandID,
|
||||
&record.GroupID,
|
||||
&record.GroupName,
|
||||
&record.Name,
|
||||
@@ -2925,7 +2999,7 @@ func (s *KnowledgeService) clearKnowledgeCleanupError(ctx context.Context, tenan
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID int64, itemIDs []int64) ([]KnowledgeItemResponse, error) {
|
||||
func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID, brandID int64, itemIDs []int64) ([]KnowledgeItemResponse, error) {
|
||||
if len(itemIDs) == 0 {
|
||||
return []KnowledgeItemResponse{}, nil
|
||||
}
|
||||
@@ -2933,10 +3007,10 @@ func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID
|
||||
SELECT ki.id, ki.group_id, kg.name, ki.name, ki.source_type, ki.source_uri, ki.status,
|
||||
ki.size_bytes, ki.error_message, ki.created_at, ki.updated_at
|
||||
FROM knowledge_items ki
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id
|
||||
WHERE ki.tenant_id = $1 AND ki.id = ANY($2::bigint[]) AND ki.deleted_at IS NULL
|
||||
JOIN knowledge_groups kg ON kg.id = ki.group_id AND kg.brand_id = ki.brand_id
|
||||
WHERE ki.tenant_id = $1 AND ki.brand_id = $2 AND ki.id = ANY($3::bigint[]) AND ki.deleted_at IS NULL
|
||||
ORDER BY ki.created_at DESC, ki.id DESC
|
||||
`, tenantID, itemIDs)
|
||||
`, tenantID, brandID, itemIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to list knowledge items")
|
||||
}
|
||||
@@ -2974,7 +3048,7 @@ func (s *KnowledgeService) listKnowledgeItemsByIDs(ctx context.Context, tenantID
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) resolveKnowledgeSearchScope(ctx context.Context, tenantID int64, groupIDs []int64) ([]int64, []string, error) {
|
||||
func (s *KnowledgeService) resolveKnowledgeSearchScope(ctx context.Context, tenantID, brandID int64, groupIDs []int64) ([]int64, []string, error) {
|
||||
groupIDs = normalizeInt64IDs(groupIDs)
|
||||
if len(groupIDs) == 0 {
|
||||
return []int64{}, []string{}, nil
|
||||
@@ -2985,19 +3059,21 @@ func (s *KnowledgeService) resolveKnowledgeSearchScope(ctx context.Context, tena
|
||||
SELECT id, name, parent_id, sort_order, created_at
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1
|
||||
AND id = ANY($2::bigint[])
|
||||
AND brand_id = $2
|
||||
AND id = ANY($3::bigint[])
|
||||
AND deleted_at IS NULL
|
||||
UNION
|
||||
SELECT child.id, child.name, child.parent_id, child.sort_order, child.created_at
|
||||
FROM knowledge_groups child
|
||||
JOIN selected_groups parent ON child.parent_id = parent.id
|
||||
WHERE child.tenant_id = $1
|
||||
AND child.brand_id = $2
|
||||
AND child.deleted_at IS NULL
|
||||
)
|
||||
SELECT id, name
|
||||
FROM selected_groups
|
||||
ORDER BY sort_order, created_at, id
|
||||
`, tenantID, groupIDs)
|
||||
`, tenantID, brandID, groupIDs)
|
||||
if err != nil {
|
||||
return nil, nil, response.ErrInternal(50050, "knowledge_group_query_failed", "failed to query knowledge groups")
|
||||
}
|
||||
|
||||
@@ -32,15 +32,15 @@ type mediaSupplyArticleImageUpload struct {
|
||||
Content []byte
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) prepareMeijiequanArticleContent(ctx context.Context, tenantID int64, content string) (string, error) {
|
||||
func (s *MediaSupplyService) prepareMeijiequanArticleContent(ctx context.Context, tenantID int64, brandID int64, content string) (string, error) {
|
||||
html := meijiequanArticleHTML(content)
|
||||
if strings.TrimSpace(html) == "" {
|
||||
return "", nil
|
||||
}
|
||||
return s.uploadMeijiequanArticleImages(ctx, tenantID, html)
|
||||
return s.uploadMeijiequanArticleImages(ctx, tenantID, brandID, html)
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context, tenantID int64, fragment string) (string, error) {
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context, tenantID int64, brandID int64, fragment string) (string, error) {
|
||||
if !strings.Contains(strings.ToLower(fragment), "<img") {
|
||||
return fragment, nil
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context,
|
||||
|
||||
cache := make(map[string]string)
|
||||
for _, node := range nodes {
|
||||
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, node, cache); err != nil {
|
||||
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, brandID, node, cache); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
@@ -70,31 +70,31 @@ func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context,
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImageNode(ctx context.Context, tenantID int64, node *nethtml.Node, cache map[string]string) error {
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImageNode(ctx context.Context, tenantID int64, brandID int64, node *nethtml.Node, cache map[string]string) error {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if node.Type == nethtml.ElementNode && strings.EqualFold(node.Data, "img") {
|
||||
if err := s.uploadMeijiequanArticleImageAttrs(ctx, tenantID, node, cache); err != nil {
|
||||
if err := s.uploadMeijiequanArticleImageAttrs(ctx, tenantID, brandID, node, cache); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, child, cache); err != nil {
|
||||
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, brandID, child, cache); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImageAttrs(ctx context.Context, tenantID int64, node *nethtml.Node, cache map[string]string) error {
|
||||
func (s *MediaSupplyService) uploadMeijiequanArticleImageAttrs(ctx context.Context, tenantID int64, brandID int64, node *nethtml.Node, cache map[string]string) error {
|
||||
src := mediaSupplyHTMLAttrValue(node.Attr, "src")
|
||||
originalSrc := mediaSupplyHTMLAttrValue(node.Attr, "_src")
|
||||
dataSrc := mediaSupplyHTMLAttrValue(node.Attr, "data-src")
|
||||
assetID := mediaSupplyHTMLInt64AttrValue(node.Attr, "data-asset-id")
|
||||
currentURL := firstNonEmptyText(src, originalSrc, dataSrc)
|
||||
|
||||
upload, resolved, err := s.resolveMeijiequanArticleImageUpload(ctx, tenantID, currentURL, assetID)
|
||||
upload, resolved, err := s.resolveMeijiequanArticleImageUpload(ctx, tenantID, brandID, currentURL, assetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,8 +158,8 @@ func (s *MediaSupplyService) uploadMeijiequanArticleImageAttrs(ctx context.Conte
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) resolveMeijiequanArticleImageUpload(ctx context.Context, tenantID int64, imageURL string, assetID int64) (mediaSupplyArticleImageUpload, bool, error) {
|
||||
objectKey, internalAssetURL, ok := s.mediaSupplyObjectKeyFromPublicAssetURL(imageURL, tenantID)
|
||||
func (s *MediaSupplyService) resolveMeijiequanArticleImageUpload(ctx context.Context, tenantID int64, brandID int64, imageURL string, assetID int64) (mediaSupplyArticleImageUpload, bool, error) {
|
||||
objectKey, internalAssetURL, ok := s.mediaSupplyObjectKeyFromPublicAssetURL(ctx, imageURL, tenantID, brandID)
|
||||
if internalAssetURL {
|
||||
if !ok {
|
||||
if assetID <= 0 {
|
||||
@@ -171,7 +171,7 @@ func (s *MediaSupplyService) resolveMeijiequanArticleImageUpload(ctx context.Con
|
||||
}
|
||||
}
|
||||
if assetID > 0 {
|
||||
objectKey, err := s.loadMediaSupplyImageAssetObjectKey(ctx, tenantID, assetID)
|
||||
objectKey, err := s.loadMediaSupplyImageAssetObjectKey(ctx, tenantID, brandID, assetID)
|
||||
if err != nil {
|
||||
return mediaSupplyArticleImageUpload{}, false, err
|
||||
}
|
||||
@@ -259,7 +259,7 @@ func mediaSupplyUploadFilename(objectKey, sourceURL, ext string) string {
|
||||
return name
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) mediaSupplyObjectKeyFromPublicAssetURL(raw string, tenantID int64) (string, bool, bool) {
|
||||
func (s *MediaSupplyService) mediaSupplyObjectKeyFromPublicAssetURL(ctx context.Context, raw string, tenantID int64, brandID int64) (string, bool, bool) {
|
||||
token, ok := mediaSupplyPublicAssetTokenFromURL(raw)
|
||||
if !ok {
|
||||
return "", false, false
|
||||
@@ -268,6 +268,9 @@ func (s *MediaSupplyService) mediaSupplyObjectKeyFromPublicAssetURL(raw string,
|
||||
if !valid || !mediaSupplyObjectKeyAllowedForTenant(objectKey, tenantID) {
|
||||
return "", true, false
|
||||
}
|
||||
if !s.mediaSupplyImageObjectKeyBelongsToBrand(ctx, tenantID, brandID, objectKey) {
|
||||
return "", true, false
|
||||
}
|
||||
return objectKey, true, true
|
||||
}
|
||||
|
||||
@@ -317,16 +320,48 @@ func mediaSupplyObjectKeyAllowedForTenant(objectKey string, tenantID int64) bool
|
||||
strings.HasPrefix(objectKey, tenantPrefix+"articles/")
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) loadMediaSupplyImageAssetObjectKey(ctx context.Context, tenantID, assetID int64) (string, error) {
|
||||
func mediaSupplyObjectKeyIsImageLibraryAsset(objectKey string, tenantID int64) bool {
|
||||
if tenantID <= 0 {
|
||||
return false
|
||||
}
|
||||
tenantPrefix := "tenants/" + strconv.FormatInt(tenantID, 10) + "/"
|
||||
return strings.HasPrefix(strings.TrimSpace(objectKey), tenantPrefix+"images/")
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) mediaSupplyImageObjectKeyBelongsToBrand(ctx context.Context, tenantID, brandID int64, objectKey string) bool {
|
||||
if s == nil || s.pool == nil || !mediaSupplyObjectKeyIsImageLibraryAsset(objectKey, tenantID) {
|
||||
return true
|
||||
}
|
||||
if tenantID <= 0 || brandID <= 0 || strings.TrimSpace(objectKey) == "" {
|
||||
return false
|
||||
}
|
||||
var exists bool
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM image_assets
|
||||
WHERE tenant_id = $1
|
||||
AND brand_id = $2
|
||||
AND object_key = $3
|
||||
AND status = 'active'
|
||||
AND deleted_at IS NULL
|
||||
)
|
||||
`, tenantID, brandID, objectKey).Scan(&exists); err != nil {
|
||||
return false
|
||||
}
|
||||
return exists
|
||||
}
|
||||
|
||||
func (s *MediaSupplyService) loadMediaSupplyImageAssetObjectKey(ctx context.Context, tenantID, brandID, assetID int64) (string, error) {
|
||||
if s == nil || s.pool == nil {
|
||||
return "", errMediaSupplyArticleImageUpload
|
||||
}
|
||||
var objectKey string
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT object_key
|
||||
FROM image_assets
|
||||
WHERE id = $1 AND tenant_id = $2 AND status = 'active' AND deleted_at IS NULL
|
||||
`, assetID, tenantID).Scan(&objectKey); err != nil {
|
||||
SELECT object_key
|
||||
FROM image_assets
|
||||
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND status = 'active' AND deleted_at IS NULL
|
||||
`, assetID, tenantID, brandID).Scan(&objectKey); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", errMediaSupplyArticleImageUpload
|
||||
}
|
||||
|
||||
@@ -534,7 +534,7 @@ func TestPrepareMeijiequanArticleContentUploadsSaaSImages(t *testing.T) {
|
||||
}).WithObjectStorage(storage)
|
||||
content := `<p class="article-editor-image article-editor-image--center" align="center"><img src="/api/public/assets/` + token + `" alt="" data-asset-id="5" /></p>`
|
||||
|
||||
got, err := svc.prepareMeijiequanArticleContent(context.Background(), 4, content)
|
||||
got, err := svc.prepareMeijiequanArticleContent(context.Background(), 4, 11, content)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare content: %v", err)
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ type mediaSupplySyncJob struct {
|
||||
type mediaSupplySubmitOrder struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
BrandID int64
|
||||
UserID int64
|
||||
ModelID int
|
||||
Title string
|
||||
@@ -172,7 +173,7 @@ func (w *MediaSupplyWorker) processOneOrder(ctx context.Context) error {
|
||||
if err := w.refreshOrderCostsBeforeSubmit(ctx, order, items); err != nil {
|
||||
return w.failOrder(ctx, order, err)
|
||||
}
|
||||
content, err := w.service.prepareMeijiequanArticleContent(ctx, order.TenantID, order.ContentSnapshot)
|
||||
content, err := w.service.prepareMeijiequanArticleContent(ctx, order.TenantID, order.BrandID, order.ContentSnapshot)
|
||||
if err != nil {
|
||||
return w.failOrder(ctx, order, err)
|
||||
}
|
||||
@@ -1143,7 +1144,7 @@ func (w *MediaSupplyWorker) claimSubmitOrder(ctx context.Context) (*mediaSupplyS
|
||||
var order mediaSupplySubmitOrder
|
||||
var requestPayload []byte
|
||||
if err := tx.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, user_id, model_id, title, content_snapshot, remark, order_brand, attempt_count, request_payload_json
|
||||
SELECT id, tenant_id, brand_id, user_id, model_id, title, content_snapshot, remark, order_brand, attempt_count, request_payload_json
|
||||
FROM media_supply_orders
|
||||
WHERE supplier = $1 AND status = $2 AND next_attempt_at <= NOW() AND deleted_at IS NULL
|
||||
ORDER BY queued_at ASC, id ASC
|
||||
@@ -1152,6 +1153,7 @@ func (w *MediaSupplyWorker) claimSubmitOrder(ctx context.Context) (*mediaSupplyS
|
||||
`, mediaSupplySupplierMeijiequan, mediaSupplyOrderStatusQueued).Scan(
|
||||
&order.ID,
|
||||
&order.TenantID,
|
||||
&order.BrandID,
|
||||
&order.UserID,
|
||||
&order.ModelID,
|
||||
&order.Title,
|
||||
|
||||
@@ -355,6 +355,7 @@ func (s *PromptRuleGenerationService) enqueuePromptRuleGeneration(ctx context.Co
|
||||
knowledgeContext, resolveErr := s.knowledge.ResolveContext(
|
||||
ctx,
|
||||
input.TenantID,
|
||||
*input.BrandID,
|
||||
rule.KnowledgeGroupIDs,
|
||||
buildPromptRuleKnowledgeQuery(rule, inputParams),
|
||||
)
|
||||
|
||||
@@ -254,7 +254,7 @@ func (s *PromptRuleService) Create(ctx context.Context, req PromptRuleRequest) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
knowledgeGroups, err := s.validatePromptRuleKnowledgeGroups(ctx, actor.TenantID, req.KnowledgeGroupIDs)
|
||||
knowledgeGroups, err := s.validatePromptRuleKnowledgeGroups(ctx, actor.TenantID, brandID, req.KnowledgeGroupIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -325,7 +325,7 @@ func (s *PromptRuleService) Update(ctx context.Context, id int64, req PromptRule
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
knowledgeGroups, err := s.validatePromptRuleKnowledgeGroups(ctx, actor.TenantID, req.KnowledgeGroupIDs)
|
||||
knowledgeGroups, err := s.validatePromptRuleKnowledgeGroups(ctx, actor.TenantID, brandID, req.KnowledgeGroupIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -535,7 +535,7 @@ func (s *PromptRuleService) loadPromptRules(ctx context.Context, tenantID, brand
|
||||
item.KnowledgeGroups = []PromptRuleKnowledgeGroup{}
|
||||
items = append(items, item)
|
||||
}
|
||||
groupMap, err := s.loadPromptRuleKnowledgeGroupMap(ctx, tenantID, collectPromptRuleIDs(items))
|
||||
groupMap, err := s.loadPromptRuleKnowledgeGroupMap(ctx, tenantID, brandID, collectPromptRuleIDs(items))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -572,7 +572,7 @@ func (s *PromptRuleService) loadPromptRuleDetail(ctx context.Context, tenantID,
|
||||
item.UpdatedAt = fmt.Sprintf("%v", updatedAt)
|
||||
item.ArticleCount = articleCount
|
||||
|
||||
groupMap, err := s.loadPromptRuleKnowledgeGroupMap(ctx, tenantID, []int64{ruleID})
|
||||
groupMap, err := s.loadPromptRuleKnowledgeGroupMap(ctx, tenantID, brandID, []int64{ruleID})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -630,6 +630,7 @@ func (s *PromptRuleService) validatePromptRuleGroup(ctx context.Context, tenantI
|
||||
func (s *PromptRuleService) validatePromptRuleKnowledgeGroups(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
groupIDs []int64,
|
||||
) ([]PromptRuleKnowledgeGroup, error) {
|
||||
groupIDs = normalizeInt64IDs(groupIDs)
|
||||
@@ -640,9 +641,9 @@ func (s *PromptRuleService) validatePromptRuleKnowledgeGroups(
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, name
|
||||
FROM knowledge_groups
|
||||
WHERE tenant_id = $1 AND id = ANY($2::bigint[]) AND deleted_at IS NULL
|
||||
WHERE tenant_id = $1 AND brand_id = $2 AND id = ANY($3::bigint[]) AND deleted_at IS NULL
|
||||
ORDER BY sort_order, created_at, id
|
||||
`, tenantID, groupIDs)
|
||||
`, tenantID, brandID, groupIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50010, "knowledge_group_query_failed", "failed to query knowledge groups")
|
||||
}
|
||||
@@ -669,6 +670,7 @@ func (s *PromptRuleService) validatePromptRuleKnowledgeGroups(
|
||||
func (s *PromptRuleService) loadPromptRuleKnowledgeGroupMap(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
brandID int64,
|
||||
ruleIDs []int64,
|
||||
) (map[int64][]PromptRuleKnowledgeGroup, error) {
|
||||
result := make(map[int64][]PromptRuleKnowledgeGroup, len(ruleIDs))
|
||||
@@ -683,9 +685,10 @@ func (s *PromptRuleService) loadPromptRuleKnowledgeGroupMap(
|
||||
JOIN knowledge_groups kg ON kg.id = prkg.knowledge_group_id
|
||||
WHERE prkg.prompt_rule_id = ANY($1::bigint[])
|
||||
AND kg.tenant_id = $2
|
||||
AND kg.brand_id = $3
|
||||
AND kg.deleted_at IS NULL
|
||||
ORDER BY kg.sort_order, kg.created_at, kg.id
|
||||
`, ruleIDs, tenantID)
|
||||
`, ruleIDs, tenantID, brandID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50010, "knowledge_group_query_failed", "failed to query prompt rule knowledge groups")
|
||||
}
|
||||
|
||||
@@ -1169,7 +1169,11 @@ func (s *ScheduleTaskService) normalizeAutoPublishConfig(ctx context.Context, ac
|
||||
randomScope := ScheduleCoverRandomScopeAll
|
||||
var randomFolderID *int64
|
||||
if coverMode == ScheduleCoverModeRandom {
|
||||
randomScope, randomFolderID, err = s.normalizeScheduleCoverRandomSource(ctx, actor.TenantID, req.CoverRandomScope, req.CoverRandomFolderID)
|
||||
brandID, err := requireCurrentBrandID(ctx)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
randomScope, randomFolderID, err = s.normalizeScheduleCoverRandomSource(ctx, actor.TenantID, brandID, req.CoverRandomScope, req.CoverRandomFolderID)
|
||||
if err != nil {
|
||||
return config, err
|
||||
}
|
||||
@@ -1218,7 +1222,7 @@ func normalizeScheduleCoverMode(value string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScheduleTaskService) normalizeScheduleCoverRandomSource(ctx context.Context, tenantID int64, scope string, folderID *int64) (string, *int64, error) {
|
||||
func (s *ScheduleTaskService) normalizeScheduleCoverRandomSource(ctx context.Context, tenantID, brandID int64, scope string, folderID *int64) (string, *int64, error) {
|
||||
normalizedScope := strings.TrimSpace(scope)
|
||||
if normalizedScope == "" {
|
||||
normalizedScope = ScheduleCoverRandomScopeAll
|
||||
@@ -1230,7 +1234,7 @@ func (s *ScheduleTaskService) normalizeScheduleCoverRandomSource(ctx context.Con
|
||||
if folderID == nil || *folderID <= 0 {
|
||||
return "", nil, response.ErrBadRequest(40077, "cover_random_folder_required", "请选择随机封面图片文件夹")
|
||||
}
|
||||
if err := s.ensureImageFolderExists(ctx, tenantID, *folderID); err != nil {
|
||||
if err := s.ensureImageFolderExists(ctx, tenantID, brandID, *folderID); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return ScheduleCoverRandomScopeFolder, folderID, nil
|
||||
@@ -1239,7 +1243,7 @@ func (s *ScheduleTaskService) normalizeScheduleCoverRandomSource(ctx context.Con
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScheduleTaskService) ensureImageFolderExists(ctx context.Context, tenantID, folderID int64) error {
|
||||
func (s *ScheduleTaskService) ensureImageFolderExists(ctx context.Context, tenantID, brandID, folderID int64) error {
|
||||
var exists bool
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (
|
||||
@@ -1247,9 +1251,9 @@ func (s *ScheduleTaskService) ensureImageFolderExists(ctx context.Context, tenan
|
||||
FROM image_folders
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND deleted_at IS NULL
|
||||
AND brand_id = $3
|
||||
)
|
||||
`, folderID, tenantID).Scan(&exists); err != nil {
|
||||
`, folderID, tenantID, brandID).Scan(&exists); err != nil {
|
||||
return response.ErrInternal(50010, "query_failed", "failed to validate image folder")
|
||||
}
|
||||
if !exists {
|
||||
|
||||
@@ -490,6 +490,7 @@ func (s *TemplateService) executeGeneration(ctx context.Context, job generationJ
|
||||
knowledgeContext, resolveErr := s.knowledge.ResolveContext(
|
||||
ctx,
|
||||
job.TenantID,
|
||||
int64(extractInt(job.Params, "brand_id")),
|
||||
knowledgeGroupIDs,
|
||||
buildGenerationKnowledgeQuery(job.TemplateKey, job.TemplateName, job.Params),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user