feat(monitoring): add DeepSeek monitoring end-to-end
- desktop: new DeepSeek page adapter and monitor registry - desktop: extract generic AI auth helpers into shared module and skip binding when challenge signals are present - admin-web: render DeepSeek HTML answers with inline citation anchors and surface reference-number badges on source cards - server: fall back to inline_links/source_panel_links and read text/siteName fields when extracting DeepSeek citations Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -138,10 +138,19 @@ type TrackingAnalyzedContentCitation = {
|
||||
citedURL: string | null;
|
||||
};
|
||||
|
||||
type TrackingCitationSourceLookupItem = {
|
||||
sourceGroupIndex: number;
|
||||
citation: MonitoringQuestionDetailCitation;
|
||||
};
|
||||
|
||||
const renderedAnswerContent = computed<string | null>(() => {
|
||||
return formatAnswerContent(activePlatform.value);
|
||||
});
|
||||
|
||||
const activeDeepSeekCitationReferenceLabels = computed<string[][]>(() => {
|
||||
return collectDeepSeekCitationReferenceLabels(activePlatform.value);
|
||||
});
|
||||
|
||||
const activeInlineContentCitations = computed<TrackingAnalyzedContentCitation[]>(() => {
|
||||
return analyzeInlineContentCitations(activePlatform.value);
|
||||
});
|
||||
@@ -208,6 +217,10 @@ function isKimiInlineCitationPlatform(platformId: string | null | undefined): bo
|
||||
return platformId === "kimi";
|
||||
}
|
||||
|
||||
function isDeepSeekInlineCitationPlatform(platformId: string | null | undefined): boolean {
|
||||
return platformId === "deepseek";
|
||||
}
|
||||
|
||||
function createQwenSourceGroupPattern(): RegExp {
|
||||
return /\[\[source_group_web_(\d+)\]\]/g;
|
||||
}
|
||||
@@ -228,6 +241,21 @@ function createMarkdownLinkPattern(): RegExp {
|
||||
return /\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)/g;
|
||||
}
|
||||
|
||||
function looksLikeHtmlAnswer(value: string | null | undefined): boolean {
|
||||
const normalized = String(value ?? "").trim();
|
||||
return /<\/?[a-z][^>]*>/i.test(normalized);
|
||||
}
|
||||
|
||||
function parseHtmlFragment(content: string): HTMLElement | null {
|
||||
if (!looksLikeHtmlAnswer(content)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const document = parser.parseFromString(`<div>${content}</div>`, "text/html");
|
||||
return document.body.firstElementChild instanceof HTMLElement ? document.body.firstElementChild : null;
|
||||
}
|
||||
|
||||
function collectInlineCitationIndexes(answerText: string): number[] {
|
||||
const matches = [
|
||||
...answerText.matchAll(createQwenSourceGroupPattern()),
|
||||
@@ -316,6 +344,136 @@ function buildInlineCitationSourceLookup(platform: MonitoringQuestionDetailPlatf
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function buildCitationSourceLookup(platform: MonitoringQuestionDetailPlatform | null): Map<string, TrackingCitationSourceLookupItem> {
|
||||
const lookup = new Map<string, TrackingCitationSourceLookupItem>();
|
||||
for (const [index, citation] of (platform?.citations ?? []).entries()) {
|
||||
const normalizedURL = normalizeInlineCitationUrl(citation.cited_url);
|
||||
if (!normalizedURL || lookup.has(normalizedURL)) {
|
||||
continue;
|
||||
}
|
||||
lookup.set(normalizedURL, {
|
||||
sourceGroupIndex: index + 1,
|
||||
citation,
|
||||
});
|
||||
}
|
||||
return lookup;
|
||||
}
|
||||
|
||||
function normalizeDeepSeekCitationLabel(value: string | null | undefined): string | null {
|
||||
const normalized = String(value ?? "").trim().replace(/\s+/g, " ");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const compact = normalized.replace(/\s+/g, "");
|
||||
const numericCandidate = compact.match(/^\[?(-?\d+)\]?$/)?.[1]
|
||||
?? compact.match(/^[((【](-?\d+)[】))]$/)?.[1]
|
||||
?? compact.match(/^#?(-?\d+)$/)?.[1]
|
||||
?? null;
|
||||
if (numericCandidate) {
|
||||
return `[${numericCandidate.replace(/^-/, "")}]`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getOrCreateDeepSeekFallbackCitationLabel(
|
||||
normalizedURL: string,
|
||||
fallbackLabels: Map<string, string>,
|
||||
state: { nextIndex: number },
|
||||
): string {
|
||||
const existing = fallbackLabels.get(normalizedURL);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const nextLabel = `[${state.nextIndex}]`;
|
||||
fallbackLabels.set(normalizedURL, nextLabel);
|
||||
state.nextIndex += 1;
|
||||
return nextLabel;
|
||||
}
|
||||
|
||||
function pushUniqueDeepSeekCitationLabel(labels: string[], label: string): void {
|
||||
if (!labels.includes(label)) {
|
||||
labels.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
function shouldStripDeepSeekOrphanAnchor(anchor: HTMLAnchorElement): boolean {
|
||||
const text = String(anchor.textContent ?? "").trim().replace(/\s+/g, "");
|
||||
if (/[A-Za-z0-9\u3400-\u9fff]/.test(text)) {
|
||||
return false;
|
||||
}
|
||||
if (anchor.querySelector("img, svg")) {
|
||||
return true;
|
||||
}
|
||||
return text.length === 0 || /^[\[\](){}<>#.,:;!?+\-_/\\|~`'"“”‘’·•::,。!?;、🔗]+$/.test(text);
|
||||
}
|
||||
|
||||
function collectDeepSeekCitationReferenceLabels(
|
||||
platform: MonitoringQuestionDetailPlatform | null,
|
||||
): string[][] {
|
||||
if (!isDeepSeekInlineCitationPlatform(platform?.ai_platform_id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rawAnswer = String(platform?.answer_text ?? "").trim();
|
||||
const root = rawAnswer ? parseHtmlFragment(rawAnswer) : null;
|
||||
if (!root) {
|
||||
return (platform?.citations ?? []).map(() => []);
|
||||
}
|
||||
|
||||
const citationSourceLookup = buildCitationSourceLookup(platform);
|
||||
const labelsByUrl = new Map<string, string[]>();
|
||||
const fallbackLabels = new Map<string, string>();
|
||||
const fallbackState = { nextIndex: 1 };
|
||||
|
||||
for (const anchor of Array.from(root.querySelectorAll<HTMLAnchorElement>("a[href]"))) {
|
||||
const normalizedURL = normalizeInlineCitationUrl(anchor.getAttribute("href") || anchor.href || "");
|
||||
if (!normalizedURL || !citationSourceLookup.has(normalizedURL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const label = normalizeDeepSeekCitationLabel(anchor.textContent)
|
||||
?? getOrCreateDeepSeekFallbackCitationLabel(normalizedURL, fallbackLabels, fallbackState);
|
||||
const labels = labelsByUrl.get(normalizedURL) ?? [];
|
||||
pushUniqueDeepSeekCitationLabel(labels, label);
|
||||
labelsByUrl.set(normalizedURL, labels);
|
||||
}
|
||||
|
||||
return (platform?.citations ?? []).map((citation) => {
|
||||
const normalizedURL = normalizeInlineCitationUrl(citation.cited_url);
|
||||
return normalizedURL ? (labelsByUrl.get(normalizedURL) ?? []) : [];
|
||||
});
|
||||
}
|
||||
|
||||
function resolveCitationReferenceLabels(
|
||||
citationIndex: number,
|
||||
platform: MonitoringQuestionDetailPlatform | null,
|
||||
): string[] {
|
||||
if (!isDeepSeekInlineCitationPlatform(platform?.ai_platform_id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return activeDeepSeekCitationReferenceLabels.value[citationIndex] ?? [];
|
||||
}
|
||||
|
||||
function resolveCitationIndexDisplayLabels(
|
||||
citationIndex: number,
|
||||
platform: MonitoringQuestionDetailPlatform | null,
|
||||
): string[] {
|
||||
const labels = resolveCitationReferenceLabels(citationIndex, platform);
|
||||
if (!labels.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const fallbackLabel = `[${citationIndex + 1}]`;
|
||||
if (labels.length === 1 && labels[0] === fallbackLabel) {
|
||||
return [];
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
function countHanCharacters(value: string): number {
|
||||
return value.match(/[\u3400-\u9fff]/g)?.length ?? 0;
|
||||
}
|
||||
@@ -339,6 +497,10 @@ function sanitizeAnswerBody(answerText: string | null | undefined): string | nul
|
||||
return null;
|
||||
}
|
||||
|
||||
if (looksLikeHtmlAnswer(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const lines = normalized.split(/\r?\n+/).map((line) => line.trim()).filter(Boolean);
|
||||
const keptLines = lines.filter((line) => !looksLikeNonAnswerNoise(line));
|
||||
const sanitized = (keptLines.length ? keptLines.join("\n") : normalized)
|
||||
@@ -370,8 +532,80 @@ function buildInlineCitationMarker(
|
||||
return `<button type="button" class="${className}" data-citation-index="${sourceGroupIndex}">[${sourceGroupIndex}]</button>`;
|
||||
}
|
||||
|
||||
function renderDeepSeekAnswerContent(
|
||||
answerText: string,
|
||||
platform: MonitoringQuestionDetailPlatform | null,
|
||||
): string {
|
||||
const root = parseHtmlFragment(answerText);
|
||||
if (!root) {
|
||||
return answerText;
|
||||
}
|
||||
|
||||
const inlineCitationLookup = buildInlineCitationSourceLookup(platform);
|
||||
const citationSourceLookup = buildCitationSourceLookup(platform);
|
||||
const fallbackLabels = new Map<string, string>();
|
||||
const fallbackState = { nextIndex: 1 };
|
||||
|
||||
for (const anchor of Array.from(root.querySelectorAll<HTMLAnchorElement>("a[href], a"))) {
|
||||
const rawHref = anchor.hasAttribute("href")
|
||||
? (anchor.getAttribute("href") || anchor.href || "")
|
||||
: "";
|
||||
const normalizedURL = normalizeInlineCitationUrl(rawHref);
|
||||
if (!normalizedURL && !shouldStripDeepSeekOrphanAnchor(anchor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const source = inlineCitationLookup.get(normalizedURL) ?? null;
|
||||
const citationSource = citationSourceLookup.get(normalizedURL) ?? null;
|
||||
if (!source && !citationSource) {
|
||||
if (shouldStripDeepSeekOrphanAnchor(anchor)) {
|
||||
anchor.remove();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
anchor.setAttribute("href", normalizedURL);
|
||||
anchor.setAttribute("target", "_blank");
|
||||
anchor.setAttribute("rel", "noreferrer");
|
||||
anchor.setAttribute("class", "tracking-inline-citation tracking-inline-citation--content");
|
||||
if (citationSource) {
|
||||
anchor.setAttribute("data-citation-index", String(citationSource.sourceGroupIndex));
|
||||
} else {
|
||||
anchor.removeAttribute("data-citation-index");
|
||||
}
|
||||
|
||||
const title = citationSource
|
||||
? resolveCitationTitle(citationSource.citation)
|
||||
: (source?.title?.trim() || "");
|
||||
if (title) {
|
||||
anchor.setAttribute("title", title);
|
||||
} else {
|
||||
anchor.removeAttribute("title");
|
||||
}
|
||||
|
||||
const displayLabel = normalizeDeepSeekCitationLabel(anchor.textContent)
|
||||
?? getOrCreateDeepSeekFallbackCitationLabel(normalizedURL, fallbackLabels, fallbackState);
|
||||
anchor.textContent = displayLabel;
|
||||
}
|
||||
|
||||
return root.innerHTML.trim() || answerText;
|
||||
}
|
||||
|
||||
function formatAnswerContent(platform: MonitoringQuestionDetailPlatform | null): string | null {
|
||||
const sanitized = sanitizeAnswerBody(platform?.answer_text);
|
||||
const rawAnswer = String(platform?.answer_text ?? "").trim();
|
||||
if (!rawAnswer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isDeepSeekInlineCitationPlatform(platform?.ai_platform_id) && looksLikeHtmlAnswer(rawAnswer)) {
|
||||
return renderDeepSeekAnswerContent(rawAnswer, platform);
|
||||
}
|
||||
|
||||
if (looksLikeHtmlAnswer(rawAnswer)) {
|
||||
return rawAnswer;
|
||||
}
|
||||
|
||||
const sanitized = sanitizeAnswerBody(rawAnswer);
|
||||
if (!sanitized) {
|
||||
return null;
|
||||
}
|
||||
@@ -394,6 +628,54 @@ function formatAnswerContent(platform: MonitoringQuestionDetailPlatform | null):
|
||||
.trim();
|
||||
}
|
||||
|
||||
function collectDeepSeekInlineCitationOccurrences(
|
||||
answerText: string,
|
||||
platform: MonitoringQuestionDetailPlatform | null,
|
||||
): Array<{
|
||||
url: string;
|
||||
label: string | null;
|
||||
siteName: string | null;
|
||||
citationCount: number;
|
||||
}> {
|
||||
const root = parseHtmlFragment(answerText);
|
||||
if (!root) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const inlineCitationLookup = buildInlineCitationSourceLookup(platform);
|
||||
if (!inlineCitationLookup.size) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const counts = new Map<string, { label: string | null; siteName: string | null; citationCount: number }>();
|
||||
for (const anchor of Array.from(root.querySelectorAll<HTMLAnchorElement>("a[href]"))) {
|
||||
const normalizedURL = normalizeInlineCitationUrl(anchor.getAttribute("href") || anchor.href || "");
|
||||
if (!normalizedURL || !inlineCitationLookup.has(normalizedURL)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const source = inlineCitationLookup.get(normalizedURL) ?? null;
|
||||
const existing = counts.get(normalizedURL);
|
||||
if (existing) {
|
||||
existing.citationCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
counts.set(normalizedURL, {
|
||||
label: source?.title?.trim() || null,
|
||||
siteName: source?.site_name?.trim() || null,
|
||||
citationCount: 1,
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(counts.entries()).map(([url, value]) => ({
|
||||
url,
|
||||
label: value.label,
|
||||
siteName: value.siteName,
|
||||
citationCount: value.citationCount,
|
||||
}));
|
||||
}
|
||||
|
||||
function analyzeInlineContentCitations(
|
||||
platform: MonitoringQuestionDetailPlatform | null,
|
||||
): TrackingAnalyzedContentCitation[] {
|
||||
@@ -402,6 +684,34 @@ function analyzeInlineContentCitations(
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isDeepSeekInlineCitationPlatform(platform?.ai_platform_id)) {
|
||||
const occurrences = collectDeepSeekInlineCitationOccurrences(answerText, platform);
|
||||
if (!occurrences.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const totalCitationCount = occurrences.reduce((sum, item) => sum + item.citationCount, 0);
|
||||
if (!totalCitationCount) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return occurrences
|
||||
.sort((left, right) => {
|
||||
if (right.citationCount !== left.citationCount) {
|
||||
return right.citationCount - left.citationCount;
|
||||
}
|
||||
return left.url.localeCompare(right.url, "zh-CN");
|
||||
})
|
||||
.map((item) => ({
|
||||
key: `deepseek-${item.url}`,
|
||||
contentName: item.label || item.url,
|
||||
channelName: item.siteName || deriveCitationChannelName(item.url),
|
||||
citationCount: item.citationCount,
|
||||
citationRate: item.citationCount / totalCitationCount,
|
||||
citedURL: item.url,
|
||||
}));
|
||||
}
|
||||
|
||||
if (isKimiInlineCitationPlatform(platform?.ai_platform_id)) {
|
||||
const occurrences = collectKimiInlineCitationOccurrences(answerText);
|
||||
if (!occurrences.length) {
|
||||
@@ -682,7 +992,21 @@ const contentCitationColumns = computed(() => [
|
||||
:data-citation-index="index + 1"
|
||||
>
|
||||
<div class="tracking-question-source-card__headline">
|
||||
<span class="tracking-question-source-card__index">[{{ index + 1 }}]</span>
|
||||
<div class="tracking-question-source-card__indexes">
|
||||
<span
|
||||
v-for="label in resolveCitationIndexDisplayLabels(index, activePlatform)"
|
||||
:key="`${citation.cited_url}-${label}`"
|
||||
class="tracking-question-source-card__reference-badge"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
<span
|
||||
class="tracking-question-source-card__index"
|
||||
:class="{ 'tracking-question-source-card__index--secondary': resolveCitationIndexDisplayLabels(index, activePlatform).length > 0 }"
|
||||
>
|
||||
[{{ index + 1 }}]
|
||||
</span>
|
||||
</div>
|
||||
<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>
|
||||
@@ -1076,6 +1400,13 @@ const contentCitationColumns = computed(() => [
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tracking-question-source-card__indexes {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tracking-question-source-card__index {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1091,6 +1422,29 @@ const contentCitationColumns = computed(() => [
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tracking-question-source-card__index--secondary {
|
||||
background: #f8fafc;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.tracking-question-source-card__reference-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 34px;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #bfdbfe;
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tracking-question-source-card__headline strong {
|
||||
display: block;
|
||||
color: #2563eb;
|
||||
@@ -1151,6 +1505,7 @@ const contentCitationColumns = computed(() => [
|
||||
line-height: 1;
|
||||
vertical-align: baseline;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.18s ease;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user