diff --git a/apps/admin-web/src/views/TrackingQuestionDetailView.vue b/apps/admin-web/src/views/TrackingQuestionDetailView.vue
index 69f9d58..c9e5a85 100644
--- a/apps/admin-web/src/views/TrackingQuestionDetailView.vue
+++ b/apps/admin-web/src/views/TrackingQuestionDetailView.vue
@@ -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(() => [
{{ citation.site_name }}
·
{{ resolveCitationTitle(citation) }}
- {{ t("tracking.contentCitationBadge") }}
+
+ {{ t("tracking.contentCitationBadge") }}
+
{{ citation.cited_url }}
@@ -1199,7 +1213,10 @@ const contentCitationColumns = computed(() => [
{{ record.site_name }}
-
+
{{ t("tracking.contentCitationMeta", { count: record.content_citation_count }) }}
diff --git a/server/internal/tenant/app/monitoring_callback_service.go b/server/internal/tenant/app/monitoring_callback_service.go
index bd419c3..59b6f02 100644
--- a/server/internal/tenant/app/monitoring_callback_service.go
+++ b/server/internal/tenant/app/monitoring_callback_service.go
@@ -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)
+ }
}
}
diff --git a/server/internal/tenant/app/monitoring_callback_service_test.go b/server/internal/tenant/app/monitoring_callback_service_test.go
index dc133e8..df7ea08 100644
--- a/server/internal/tenant/app/monitoring_callback_service_test.go
+++ b/server/internal/tenant/app/monitoring_callback_service_test.go
@@ -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 := "搜索网页结果"
diff --git a/server/internal/tenant/app/monitoring_projection.go b/server/internal/tenant/app/monitoring_projection.go
index 342d2e3..cbb0e09 100644
--- a/server/internal/tenant/app/monitoring_projection.go
+++ b/server/internal/tenant/app/monitoring_projection.go
@@ -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'
diff --git a/server/internal/tenant/app/monitoring_service.go b/server/internal/tenant/app/monitoring_service.go
index fd59624..f8e1621 100644
--- a/server/internal/tenant/app/monitoring_service.go
+++ b/server/internal/tenant/app/monitoring_service.go
@@ -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