feat(monitoring): attribute SaaS citations by registrable domain
Persist host/registrable_domain/site_key on monitoring article URL aliases, populate them on desktop-publish completion, and use them in the citation pipeline to count how often SaaS-published content appears as a citation source. Expose a /citation-summary endpoint with a 7/30-day window switch and surface the new source-share metrics in the tracking dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *DesktopTaskService) syncMonitoringArticleAlias(ctx context.Context, input *monitoringArticleAliasInput) error {
|
||||
if s == nil || s.monitoringPool == nil || input == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
originalURL, confidence := monitoringAliasOriginalURL(input)
|
||||
if originalURL == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
normalizedURL := normalizeCitationURL(originalURL)
|
||||
if normalizedURL == "" {
|
||||
return nil
|
||||
}
|
||||
host, registrableDomain, _, _ := deriveCitationURLParts(originalURL, normalizedURL)
|
||||
if registrableDomain == "" {
|
||||
registrableDomain = host
|
||||
}
|
||||
siteKey := host
|
||||
if siteKey == "" {
|
||||
siteKey = registrableDomain
|
||||
}
|
||||
lastPathSegment := monitoringAliasLastPathSegment(normalizedURL)
|
||||
|
||||
_, err := s.monitoringPool.Exec(ctx, `
|
||||
INSERT INTO monitoring_article_url_aliases (
|
||||
tenant_id, article_id, publish_record_id, platform_id, publish_platform_name_snapshot,
|
||||
external_article_id, article_title_snapshot, original_url, normalized_url,
|
||||
host, registrable_domain, site_key, last_path_segment, confidence
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8, $9,
|
||||
$10, $11, $12, $13, $14
|
||||
)
|
||||
ON CONFLICT (tenant_id, normalized_url)
|
||||
DO UPDATE SET
|
||||
article_id = EXCLUDED.article_id,
|
||||
publish_record_id = EXCLUDED.publish_record_id,
|
||||
platform_id = EXCLUDED.platform_id,
|
||||
publish_platform_name_snapshot = EXCLUDED.publish_platform_name_snapshot,
|
||||
external_article_id = EXCLUDED.external_article_id,
|
||||
article_title_snapshot = EXCLUDED.article_title_snapshot,
|
||||
original_url = EXCLUDED.original_url,
|
||||
host = EXCLUDED.host,
|
||||
registrable_domain = EXCLUDED.registrable_domain,
|
||||
site_key = EXCLUDED.site_key,
|
||||
last_path_segment = EXCLUDED.last_path_segment,
|
||||
confidence = EXCLUDED.confidence,
|
||||
updated_at = NOW()
|
||||
`,
|
||||
input.TenantID,
|
||||
input.ArticleID,
|
||||
input.PublishRecordID,
|
||||
strings.TrimSpace(input.PlatformID),
|
||||
strings.TrimSpace(input.PlatformName),
|
||||
normalizeStringPointer(input.ExternalArticleID),
|
||||
strings.TrimSpace(input.ArticleTitle),
|
||||
originalURL,
|
||||
normalizedURL,
|
||||
host,
|
||||
registrableDomain,
|
||||
siteKey,
|
||||
nullableString(lastPathSegment),
|
||||
confidence,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func monitoringAliasOriginalURL(input *monitoringArticleAliasInput) (string, string) {
|
||||
if input == nil {
|
||||
return "", ""
|
||||
}
|
||||
if strings.TrimSpace(input.PlatformID) == "baijiahao" {
|
||||
if articleID := strings.TrimSpace(monitoringStringValue(input.ExternalArticleID)); articleID != "" {
|
||||
return baijiahaoPublishedArticleURL(articleID), "high"
|
||||
}
|
||||
}
|
||||
if value := strings.TrimSpace(monitoringStringValue(input.ExternalArticleURL)); value != "" {
|
||||
return value, "high"
|
||||
}
|
||||
if value := strings.TrimSpace(monitoringStringValue(input.ExternalManageURL)); value != "" {
|
||||
return value, "medium"
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func monitoringAliasLastPathSegment(rawURL string) *string {
|
||||
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
segments := strings.Split(strings.Trim(parsed.Path, "/"), "/")
|
||||
for index := len(segments) - 1; index >= 0; index-- {
|
||||
if segment := strings.TrimSpace(segments[index]); segment != "" {
|
||||
return &segment
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user