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
@@ -258,7 +258,7 @@ function resolveCitationTitle(citation: MonitoringQuestionDetailCitation): strin
}
function isLinkedInlineCitationPlatform(platformId: string | null | undefined): boolean {
return platformId === "qwen" || platformId === "yuanbao";
return platformId === "yuanbao";
}
function isKimiInlineCitationPlatform(platformId: string | null | undefined): boolean {
@@ -269,6 +269,10 @@ function isDeepSeekInlineCitationPlatform(platformId: string | null | undefined)
return platformId === "deepseek";
}
function supportsContentCitationDisplay(platformId: string | null | undefined): boolean {
return platformId !== "qwen";
}
function createQwenSourceGroupPattern(): RegExp {
return /\[\[source_group_web_(\d+)\]\]/g;
}
@@ -880,6 +884,10 @@ function analyzeInlineContentCitations(
});
}
if (!isLinkedInlineCitationPlatform(platform?.ai_platform_id)) {
return [];
}
const sourceGroupIndexes = collectInlineCitationIndexes(answerText);
if (!sourceGroupIndexes.length) {
return [];
@@ -1160,7 +1168,13 @@ const contentCitationColumns = computed(() => [
<strong>{{ citation.site_name }}</strong>
<span class="tracking-question-source-card__divider" v-if="resolveCitationTitle(citation)">·</span>
<span class="tracking-question-source-card__title" :title="resolveCitationTitle(citation)">{{ resolveCitationTitle(citation) }}</span>
<a-tag v-if="citation.article_id" color="blue" class="tracking-question-source-card__badge">{{ t("tracking.contentCitationBadge") }}</a-tag>
<a-tag
v-if="citation.article_id && supportsContentCitationDisplay(activePlatform?.ai_platform_id)"
color="blue"
class="tracking-question-source-card__badge"
>
{{ t("tracking.contentCitationBadge") }}
</a-tag>
</div>
<div class="tracking-question-source-card__link">
{{ citation.cited_url }}
@@ -1199,7 +1213,10 @@ const contentCitationColumns = computed(() => [
<template v-if="column.key === 'sourcePlatform'">
<div class="cell-primary-stack">
<strong class="table-text" :title="record.site_name">{{ record.site_name }}</strong>
<small v-if="record.content_citation_count > 0" class="meta-subtext">
<small
v-if="record.content_citation_count > 0 && supportsContentCitationDisplay(record.ai_platform_id)"
class="meta-subtext"
>
{{ t("tracking.contentCitationMeta", { count: record.content_citation_count }) }}
</small>
</div>
@@ -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