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:
2026-04-23 22:54:56 +08:00
parent f6889ecdee
commit f6e553dcc3
12 changed files with 2971 additions and 65 deletions
@@ -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;
}