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:
@@ -258,7 +258,7 @@ function resolveCitationTitle(citation: MonitoringQuestionDetailCitation): strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isLinkedInlineCitationPlatform(platformId: string | null | undefined): boolean {
|
function isLinkedInlineCitationPlatform(platformId: string | null | undefined): boolean {
|
||||||
return platformId === "qwen" || platformId === "yuanbao";
|
return platformId === "yuanbao";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKimiInlineCitationPlatform(platformId: string | null | undefined): boolean {
|
function isKimiInlineCitationPlatform(platformId: string | null | undefined): boolean {
|
||||||
@@ -269,6 +269,10 @@ function isDeepSeekInlineCitationPlatform(platformId: string | null | undefined)
|
|||||||
return platformId === "deepseek";
|
return platformId === "deepseek";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function supportsContentCitationDisplay(platformId: string | null | undefined): boolean {
|
||||||
|
return platformId !== "qwen";
|
||||||
|
}
|
||||||
|
|
||||||
function createQwenSourceGroupPattern(): RegExp {
|
function createQwenSourceGroupPattern(): RegExp {
|
||||||
return /\[\[source_group_web_(\d+)\]\]/g;
|
return /\[\[source_group_web_(\d+)\]\]/g;
|
||||||
}
|
}
|
||||||
@@ -880,6 +884,10 @@ function analyzeInlineContentCitations(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isLinkedInlineCitationPlatform(platform?.ai_platform_id)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const sourceGroupIndexes = collectInlineCitationIndexes(answerText);
|
const sourceGroupIndexes = collectInlineCitationIndexes(answerText);
|
||||||
if (!sourceGroupIndexes.length) {
|
if (!sourceGroupIndexes.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -1160,7 +1168,13 @@ const contentCitationColumns = computed(() => [
|
|||||||
<strong>{{ citation.site_name }}</strong>
|
<strong>{{ citation.site_name }}</strong>
|
||||||
<span class="tracking-question-source-card__divider" v-if="resolveCitationTitle(citation)">·</span>
|
<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>
|
<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>
|
||||||
<div class="tracking-question-source-card__link">
|
<div class="tracking-question-source-card__link">
|
||||||
{{ citation.cited_url }}
|
{{ citation.cited_url }}
|
||||||
@@ -1199,7 +1213,10 @@ const contentCitationColumns = computed(() => [
|
|||||||
<template v-if="column.key === 'sourcePlatform'">
|
<template v-if="column.key === 'sourcePlatform'">
|
||||||
<div class="cell-primary-stack">
|
<div class="cell-primary-stack">
|
||||||
<strong class="table-text" :title="record.site_name">{{ record.site_name }}</strong>
|
<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 }) }}
|
{{ t("tracking.contentCitationMeta", { count: record.content_citation_count }) }}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2072,7 +2072,7 @@ func (s *MonitoringCallbackService) processTaskResult(ctx context.Context, task
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if runStatus == "succeeded" {
|
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 {
|
if buildErr != nil {
|
||||||
return nil, buildErr
|
return nil, buildErr
|
||||||
}
|
}
|
||||||
@@ -2385,37 +2385,42 @@ func shouldUseMonitoringLLMParse(req MonitoringTaskResultRequest) bool {
|
|||||||
return !hasCompleteMonitoringParsePayload(req)
|
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 {
|
if len(inputs) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedURLs := make([]string, 0, len(inputs))
|
resolveContentCitations := monitoringPlatformResolvesContentCitations(platformID)
|
||||||
seenURLs := make(map[string]struct{}, len(inputs))
|
aliasMap := map[string]monitoringAliasResolution{}
|
||||||
for _, item := range inputs {
|
if resolveContentCitations {
|
||||||
key := strings.TrimSpace(monitoringStringValue(item.NormalizedURL))
|
normalizedURLs := make([]string, 0, len(inputs))
|
||||||
if key == "" {
|
seenURLs := make(map[string]struct{}, len(inputs))
|
||||||
key = normalizeCitationURL(item.URL)
|
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)
|
var err error
|
||||||
if err != nil {
|
aliasMap, err = s.loadAliasResolutions(ctx, tx, tenantID, normalizedURLs)
|
||||||
return nil, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]monitoringCitationFact, 0, len(inputs))
|
result := make([]monitoringCitationFact, 0, len(inputs))
|
||||||
seenFacts := make(map[string]struct{}, len(inputs))
|
seenFacts := make(map[string]struct{}, len(inputs))
|
||||||
for _, input := range inputs {
|
for _, input := range inputs {
|
||||||
fact, ok := buildMonitoringCitationFact(input, aliasMap)
|
fact, ok := buildMonitoringCitationFact(input, aliasMap, resolveContentCitations)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -2552,11 +2557,11 @@ func (s *MonitoringCallbackService) loadExistingRunSummary(ctx context.Context,
|
|||||||
if err := s.monitoringPool.QueryRow(ctx, `
|
if err := s.monitoringPool.QueryRow(ctx, `
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::int AS citation_source_count,
|
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
|
FROM monitoring_citation_facts
|
||||||
WHERE tenant_id = $1
|
WHERE tenant_id = $1
|
||||||
AND run_id = $2
|
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")
|
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 {
|
func newMonitoringTaskResultEnvelope(task *monitoringCollectTask, installationID string, receivedAt time.Time, req MonitoringTaskResultRequest) monitoringTaskResultEnvelope {
|
||||||
return monitoringTaskResultEnvelope{
|
return monitoringTaskResultEnvelope{
|
||||||
Version: "v1",
|
Version: "v1",
|
||||||
@@ -2741,7 +2755,7 @@ func extractMonitoringSourceItems(value interface{}) []MonitoringSourceItem {
|
|||||||
return result
|
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)
|
rawURL := strings.TrimSpace(input.URL)
|
||||||
normalizedURL := strings.TrimSpace(monitoringStringValue(input.NormalizedURL))
|
normalizedURL := strings.TrimSpace(monitoringStringValue(input.NormalizedURL))
|
||||||
if normalizedURL == "" {
|
if normalizedURL == "" {
|
||||||
@@ -2791,14 +2805,18 @@ func buildMonitoringCitationFact(input MonitoringSourceItem, aliasMap map[string
|
|||||||
siteKey = registrableDomain
|
siteKey = registrableDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
articleID := cloneInt64(input.ArticleID)
|
var articleID *int64
|
||||||
publishRecordID := cloneInt64(input.PublishRecordID)
|
var publishRecordID *int64
|
||||||
if alias, ok := aliasMap[normalizedURL]; ok {
|
if resolveContentCitations {
|
||||||
if articleID == nil {
|
articleID = cloneInt64(input.ArticleID)
|
||||||
articleID = cloneInt64(alias.ArticleID)
|
publishRecordID = cloneInt64(input.PublishRecordID)
|
||||||
}
|
if alias, ok := aliasMap[normalizedURL]; ok {
|
||||||
if publishRecordID == nil {
|
if articleID == nil {
|
||||||
publishRecordID = cloneInt64(alias.PublishRecordID)
|
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) {
|
func TestBuildMonitoringRawPayloadDeepSeekIncludesSearchResultsInCitationInputs(t *testing.T) {
|
||||||
searchTitle := "搜索网页结果"
|
searchTitle := "搜索网页结果"
|
||||||
|
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ func (s *MonitoringCallbackService) loadMonitoringBrandDailyAggregate(ctx contex
|
|||||||
WHERE cf.tenant_id = $1
|
WHERE cf.tenant_id = $1
|
||||||
AND cf.brand_id = $2
|
AND cf.brand_id = $2
|
||||||
AND cf.article_id IS NOT NULL
|
AND cf.article_id IS NOT NULL
|
||||||
|
AND r.ai_platform_id <> 'qwen'
|
||||||
AND r.collector_type = $3
|
AND r.collector_type = $3
|
||||||
AND r.business_date = $4::date
|
AND r.business_date = $4::date
|
||||||
AND r.status = 'succeeded'
|
AND r.status = 'succeeded'
|
||||||
|
|||||||
@@ -2438,8 +2438,8 @@ func (s *MonitoringService) loadCitationRanking(
|
|||||||
citation_counts AS (
|
citation_counts AS (
|
||||||
SELECT
|
SELECT
|
||||||
r.ai_platform_id,
|
r.ai_platform_id,
|
||||||
COUNT(DISTINCT cf.run_id) FILTER (WHERE cf.article_id IS NOT NULL) AS cited_answer_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) AS cited_article_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
|
FROM question_monitor_runs r
|
||||||
JOIN monitoring_citation_facts cf
|
JOIN monitoring_citation_facts cf
|
||||||
ON cf.tenant_id = r.tenant_id AND cf.run_id = r.id
|
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
|
WHERE cf.tenant_id = $1
|
||||||
AND cf.brand_id = $2
|
AND cf.brand_id = $2
|
||||||
AND cf.article_id IS NOT NULL
|
AND cf.article_id IS NOT NULL
|
||||||
|
AND r.ai_platform_id <> 'qwen'
|
||||||
AND r.collector_type = $3
|
AND r.collector_type = $3
|
||||||
AND r.status = 'succeeded'
|
AND r.status = 'succeeded'
|
||||||
AND r.business_date BETWEEN $4::date AND $5::date
|
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.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,
|
COALESCE(tm.registrable_domain, gm.registrable_domain, cf.site_key, cf.registrable_domain) AS site_domain,
|
||||||
COUNT(cf.id) AS citation_count,
|
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
|
FROM question_monitor_runs r
|
||||||
JOIN monitoring_citation_facts cf
|
JOIN monitoring_citation_facts cf
|
||||||
ON cf.tenant_id = r.tenant_id AND cf.run_id = r.id
|
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.status = 'succeeded'
|
||||||
AND r.business_date BETWEEN $6::date AND $7::date
|
AND r.business_date BETWEEN $6::date AND $7::date
|
||||||
AND cf.article_id IS NOT NULL
|
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))
|
AND ($8::text[] IS NULL OR r.ai_platform_id = ANY($8))
|
||||||
GROUP BY cf.article_id, r.ai_platform_id
|
GROUP BY cf.article_id, r.ai_platform_id
|
||||||
ORDER BY citation_count DESC, cf.article_id ASC
|
ORDER BY citation_count DESC, cf.article_id ASC
|
||||||
|
|||||||
Reference in New Issue
Block a user