feat(monitoring): exclude qwen from content citation resolution

Skip article alias resolution and content-citation aggregates when the
platform is qwen, and hide the content-citation badge/meta in the
tracking detail view, since qwen's source URLs are not reliable enough
to map to brand articles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 16:48:28 +08:00
parent 19527cdf42
commit 4a7427a7f1
5 changed files with 114 additions and 38 deletions
@@ -2072,7 +2072,7 @@ func (s *MonitoringCallbackService) processTaskResult(ctx context.Context, task
return nil, err
}
if runStatus == "succeeded" {
facts, buildErr := s.buildCitationFacts(ctx, tx, task.TenantID, sourceInputs)
facts, buildErr := s.buildCitationFacts(ctx, tx, task.TenantID, task.AIPlatformID, sourceInputs)
if buildErr != nil {
return nil, buildErr
}
@@ -2385,37 +2385,42 @@ func shouldUseMonitoringLLMParse(req MonitoringTaskResultRequest) bool {
return !hasCompleteMonitoringParsePayload(req)
}
func (s *MonitoringCallbackService) buildCitationFacts(ctx context.Context, tx pgx.Tx, tenantID int64, inputs []MonitoringSourceItem) ([]monitoringCitationFact, error) {
func (s *MonitoringCallbackService) buildCitationFacts(ctx context.Context, tx pgx.Tx, tenantID int64, platformID string, inputs []MonitoringSourceItem) ([]monitoringCitationFact, error) {
if len(inputs) == 0 {
return nil, nil
}
normalizedURLs := make([]string, 0, len(inputs))
seenURLs := make(map[string]struct{}, len(inputs))
for _, item := range inputs {
key := strings.TrimSpace(monitoringStringValue(item.NormalizedURL))
if key == "" {
key = normalizeCitationURL(item.URL)
resolveContentCitations := monitoringPlatformResolvesContentCitations(platformID)
aliasMap := map[string]monitoringAliasResolution{}
if resolveContentCitations {
normalizedURLs := make([]string, 0, len(inputs))
seenURLs := make(map[string]struct{}, len(inputs))
for _, item := range inputs {
key := strings.TrimSpace(monitoringStringValue(item.NormalizedURL))
if key == "" {
key = normalizeCitationURL(item.URL)
}
if key == "" {
continue
}
if _, ok := seenURLs[key]; ok {
continue
}
seenURLs[key] = struct{}{}
normalizedURLs = append(normalizedURLs, key)
}
if key == "" {
continue
}
if _, ok := seenURLs[key]; ok {
continue
}
seenURLs[key] = struct{}{}
normalizedURLs = append(normalizedURLs, key)
}
aliasMap, err := s.loadAliasResolutions(ctx, tx, tenantID, normalizedURLs)
if err != nil {
return nil, err
var err error
aliasMap, err = s.loadAliasResolutions(ctx, tx, tenantID, normalizedURLs)
if err != nil {
return nil, err
}
}
result := make([]monitoringCitationFact, 0, len(inputs))
seenFacts := make(map[string]struct{}, len(inputs))
for _, input := range inputs {
fact, ok := buildMonitoringCitationFact(input, aliasMap)
fact, ok := buildMonitoringCitationFact(input, aliasMap, resolveContentCitations)
if !ok {
continue
}
@@ -2552,11 +2557,11 @@ func (s *MonitoringCallbackService) loadExistingRunSummary(ctx context.Context,
if err := s.monitoringPool.QueryRow(ctx, `
SELECT
COUNT(*)::int AS citation_source_count,
COUNT(*) FILTER (WHERE article_id IS NOT NULL)::int AS content_citation_count
COUNT(*) FILTER (WHERE article_id IS NOT NULL AND $3::text <> 'qwen')::int AS content_citation_count
FROM monitoring_citation_facts
WHERE tenant_id = $1
AND run_id = $2
`, task.TenantID, runID.Int64).Scan(&citationSourceCount, &contentCitationCount); err != nil {
`, task.TenantID, runID.Int64, normalizeMonitoringPlatformID(task.AIPlatformID)).Scan(&citationSourceCount, &contentCitationCount); err != nil {
return nil, 0, 0, response.ErrInternal(50041, "citation_lookup_failed", "failed to inspect existing citation facts")
}
@@ -2683,6 +2688,15 @@ func monitoringPlatformUsesDedicatedCitationPanel(platformID string) bool {
}
}
func monitoringPlatformResolvesContentCitations(platformID string) bool {
switch normalizeMonitoringPlatformID(platformID) {
case "qwen":
return false
default:
return true
}
}
func newMonitoringTaskResultEnvelope(task *monitoringCollectTask, installationID string, receivedAt time.Time, req MonitoringTaskResultRequest) monitoringTaskResultEnvelope {
return monitoringTaskResultEnvelope{
Version: "v1",
@@ -2741,7 +2755,7 @@ func extractMonitoringSourceItems(value interface{}) []MonitoringSourceItem {
return result
}
func buildMonitoringCitationFact(input MonitoringSourceItem, aliasMap map[string]monitoringAliasResolution) (monitoringCitationFact, bool) {
func buildMonitoringCitationFact(input MonitoringSourceItem, aliasMap map[string]monitoringAliasResolution, resolveContentCitations bool) (monitoringCitationFact, bool) {
rawURL := strings.TrimSpace(input.URL)
normalizedURL := strings.TrimSpace(monitoringStringValue(input.NormalizedURL))
if normalizedURL == "" {
@@ -2791,14 +2805,18 @@ func buildMonitoringCitationFact(input MonitoringSourceItem, aliasMap map[string
siteKey = registrableDomain
}
articleID := cloneInt64(input.ArticleID)
publishRecordID := cloneInt64(input.PublishRecordID)
if alias, ok := aliasMap[normalizedURL]; ok {
if articleID == nil {
articleID = cloneInt64(alias.ArticleID)
}
if publishRecordID == nil {
publishRecordID = cloneInt64(alias.PublishRecordID)
var articleID *int64
var publishRecordID *int64
if resolveContentCitations {
articleID = cloneInt64(input.ArticleID)
publishRecordID = cloneInt64(input.PublishRecordID)
if alias, ok := aliasMap[normalizedURL]; ok {
if articleID == nil {
articleID = cloneInt64(alias.ArticleID)
}
if publishRecordID == nil {
publishRecordID = cloneInt64(alias.PublishRecordID)
}
}
}
@@ -346,6 +346,44 @@ func TestBuildMonitoringRawPayloadQwenIncludesSearchResultsInCitationInputs(t *t
}
}
func TestBuildMonitoringCitationFactCanDisableContentResolution(t *testing.T) {
aliasArticleID := int64(42)
aliasPublishRecordID := int64(84)
normalizedURL := "https://example.com/article"
input := MonitoringSourceItem{
URL: "https://example.com/article?utm_source=test#section",
NormalizedURL: &normalizedURL,
}
aliasMap := map[string]monitoringAliasResolution{
normalizedURL: {
ArticleID: &aliasArticleID,
PublishRecordID: &aliasPublishRecordID,
},
}
resolvedFact, ok := buildMonitoringCitationFact(input, aliasMap, true)
if !ok {
t.Fatal("buildMonitoringCitationFact() with content resolution returned !ok")
}
if resolvedFact.ArticleID == nil || *resolvedFact.ArticleID != aliasArticleID {
t.Fatalf("resolved ArticleID = %v, want %d", resolvedFact.ArticleID, aliasArticleID)
}
if resolvedFact.PublishRecordID == nil || *resolvedFact.PublishRecordID != aliasPublishRecordID {
t.Fatalf("resolved PublishRecordID = %v, want %d", resolvedFact.PublishRecordID, aliasPublishRecordID)
}
sourceOnlyFact, ok := buildMonitoringCitationFact(input, aliasMap, false)
if !ok {
t.Fatal("buildMonitoringCitationFact() without content resolution returned !ok")
}
if sourceOnlyFact.ArticleID != nil {
t.Fatalf("source-only ArticleID = %v, want nil", sourceOnlyFact.ArticleID)
}
if sourceOnlyFact.PublishRecordID != nil {
t.Fatalf("source-only PublishRecordID = %v, want nil", sourceOnlyFact.PublishRecordID)
}
}
func TestBuildMonitoringRawPayloadDeepSeekIncludesSearchResultsInCitationInputs(t *testing.T) {
searchTitle := "搜索网页结果"
@@ -214,6 +214,7 @@ func (s *MonitoringCallbackService) loadMonitoringBrandDailyAggregate(ctx contex
WHERE cf.tenant_id = $1
AND cf.brand_id = $2
AND cf.article_id IS NOT NULL
AND r.ai_platform_id <> 'qwen'
AND r.collector_type = $3
AND r.business_date = $4::date
AND r.status = 'succeeded'
@@ -2438,8 +2438,8 @@ func (s *MonitoringService) loadCitationRanking(
citation_counts AS (
SELECT
r.ai_platform_id,
COUNT(DISTINCT cf.run_id) FILTER (WHERE cf.article_id IS NOT NULL) AS cited_answer_count,
COUNT(DISTINCT cf.article_id) FILTER (WHERE cf.article_id IS NOT NULL) AS cited_article_count
COUNT(DISTINCT cf.run_id) FILTER (WHERE cf.article_id IS NOT NULL AND r.ai_platform_id <> 'qwen') AS cited_answer_count,
COUNT(DISTINCT cf.article_id) FILTER (WHERE cf.article_id IS NOT NULL AND r.ai_platform_id <> 'qwen') AS cited_article_count
FROM question_monitor_runs r
JOIN monitoring_citation_facts cf
ON cf.tenant_id = r.tenant_id AND cf.run_id = r.id
@@ -2562,6 +2562,7 @@ func (s *MonitoringService) loadCitedArticles(
WHERE cf.tenant_id = $1
AND cf.brand_id = $2
AND cf.article_id IS NOT NULL
AND r.ai_platform_id <> 'qwen'
AND r.collector_type = $3
AND r.status = 'succeeded'
AND r.business_date BETWEEN $4::date AND $5::date
@@ -2878,7 +2879,7 @@ func (s *MonitoringService) loadQuestionCitationAnalysis(ctx context.Context, te
COALESCE(tm.site_key, gm.site_key, cf.site_key) AS site_key,
COALESCE(tm.registrable_domain, gm.registrable_domain, cf.site_key, cf.registrable_domain) AS site_domain,
COUNT(cf.id) AS citation_count,
COUNT(*) FILTER (WHERE cf.article_id IS NOT NULL) AS content_citation_count
COUNT(*) FILTER (WHERE cf.article_id IS NOT NULL AND r.ai_platform_id <> 'qwen') AS content_citation_count
FROM question_monitor_runs r
JOIN monitoring_citation_facts cf
ON cf.tenant_id = r.tenant_id AND cf.run_id = r.id
@@ -3058,6 +3059,7 @@ func (s *MonitoringService) loadQuestionContentCitations(ctx context.Context, te
AND r.status = 'succeeded'
AND r.business_date BETWEEN $6::date AND $7::date
AND cf.article_id IS NOT NULL
AND r.ai_platform_id <> 'qwen'
AND ($8::text[] IS NULL OR r.ai_platform_id = ANY($8))
GROUP BY cf.article_id, r.ai_platform_id
ORDER BY citation_count DESC, cf.article_id ASC