feat(deepseek): normalize model labels and unwrap redirect URLs

- Normalize provider_model so UI shows DeepSeek/DeepSeek-R1 instead of raw toggle text (联网搜索 etc.)
- Unwrap DeepSeek redirect URLs (chat.deepseek.com/api/redirect?url=...) to canonical sources
- Expand reasoning sections and separate reasoning search pages from answer-body citations
- Improve webpages-trigger click heuristics and auxiliary link filtering in answer rendering

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 23:37:16 +08:00
parent bc2e23cfcb
commit 4f3c39f5f5
3 changed files with 940 additions and 123 deletions
@@ -220,6 +220,39 @@ function statusLabel(status: string): string {
return t(`tracking.status.${status}`);
}
function resolveDeepSeekProviderModelLabel(value: string | null | undefined): string {
const normalized = String(value ?? "").trim();
if (!normalized || normalized === "--") {
return "DeepSeek";
}
const compact = normalized.replace(/\s+/g, "").toLowerCase();
if (/r1|reasoner|deepthink|深度思考/.test(compact)) {
return "DeepSeek";
}
const versionMatch =
normalized.match(/deepseek[-_\s]*(r1|v\d+(?:\.\d+)?)/i)
?? normalized.match(/\b(r1|v\d+(?:\.\d+)?)\b/i);
if (versionMatch?.[1]) {
return `DeepSeek-${versionMatch[1].toUpperCase()}`;
}
if (/search|web|联网|搜索/.test(compact)) {
return "DeepSeek";
}
return normalized;
}
function resolveAnswerProviderModel(platform: MonitoringQuestionDetailPlatform | null): string {
if (isDeepSeekInlineCitationPlatform(platform?.ai_platform_id)) {
return resolveDeepSeekProviderModelLabel(platform?.provider_model);
}
return String(platform?.provider_model ?? "").trim() || "--";
}
function resolveCitationTitle(citation: MonitoringQuestionDetailCitation): string {
return citation.cited_title || citation.article_title || citation.cited_url;
}
@@ -374,6 +407,78 @@ function buildCitationSourceLookup(platform: MonitoringQuestionDetailPlatform |
return lookup;
}
function buildDeepSeekAnswerLinkSourceLookup(
platform: MonitoringQuestionDetailPlatform | null,
): Map<string, { title: string | null; siteName: string | null }> {
const lookup = new Map<string, { title: string | null; siteName: string | null }>();
for (const item of platform?.inline_citations ?? []) {
const normalizedURL = normalizeInlineCitationUrl(item.normalized_url ?? item.url);
if (!normalizedURL || lookup.has(normalizedURL)) {
continue;
}
lookup.set(normalizedURL, {
title: item.title?.trim() || null,
siteName: item.site_name?.trim() || null,
});
}
for (const citation of platform?.citations ?? []) {
const normalizedURL = normalizeInlineCitationUrl(citation.cited_url);
if (!normalizedURL || lookup.has(normalizedURL)) {
continue;
}
lookup.set(normalizedURL, {
title: resolveCitationTitle(citation),
siteName: citation.site_name?.trim() || null,
});
}
return lookup;
}
function isHiddenDeepSeekAnswerNode(element: Element): boolean {
let current: Element | null = element;
for (let depth = 0; current && depth < 8; depth += 1) {
if (current.hasAttribute("hidden") || current.getAttribute("aria-hidden") === "true") {
return true;
}
const style = current.getAttribute("style") ?? "";
if (/(display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0)/i.test(style)) {
return true;
}
current = current.parentElement;
}
return false;
}
function isInsideDeepSeekAuxiliaryLinkContainer(anchor: HTMLAnchorElement): boolean {
let current = anchor.parentElement;
for (let depth = 0; current && depth < 8; depth += 1) {
const descriptor = `${current.className || ""} ${current.getAttribute("data-testid") || ""} ${current.getAttribute("role") || ""}`;
if (/(search-view-card|search-result|result-card|source-card|reference-card|citation-card|popover|tooltip|dropdown|floating|hover-card|preview-card)/i.test(descriptor)) {
return true;
}
current = current.parentElement;
}
return false;
}
function shouldCountDeepSeekAnswerLink(anchor: HTMLAnchorElement): boolean {
if (isHiddenDeepSeekAnswerNode(anchor) || isInsideDeepSeekAuxiliaryLinkContainer(anchor)) {
return false;
}
const text = String(anchor.textContent ?? "").trim();
if (normalizeDeepSeekCitationLabel(text)) {
return true;
}
if (shouldStripDeepSeekOrphanAnchor(anchor)) {
return false;
}
return /[A-Za-z0-9\u3400-\u9fff]/.test(text);
}
function normalizeDeepSeekCitationLabel(value: string | null | undefined): string | null {
const normalized = String(value ?? "").trim().replace(/\s+/g, " ");
if (!normalized) {
@@ -416,13 +521,16 @@ function pushUniqueDeepSeekCitationLabel(labels: string[], label: string): void
function shouldStripDeepSeekOrphanAnchor(anchor: HTMLAnchorElement): boolean {
const text = String(anchor.textContent ?? "").trim().replace(/\s+/g, "");
if (/^(link|source|citation|reference|open|原文|链接|来源|引用)$/i.test(text)) {
return true;
}
if (/[A-Za-z0-9\u3400-\u9fff]/.test(text)) {
return false;
}
if (anchor.querySelector("img, svg")) {
return true;
}
return text.length === 0 || /^[\[\](){}<>#.,:;!?+\-_/\\|~`'"“”‘’·•::,。!?;、🔗]+$/.test(text);
return text.length === 0 || /^[\[\](){}<>#.,:;!?+\-_/\\|~`'"“”‘’·•::,。!?;、🔗↗↘↙↖↪⤴]+$/.test(text);
}
function collectDeepSeekCitationReferenceLabels(
@@ -489,6 +597,17 @@ function resolveCitationIndexDisplayLabels(
return labels;
}
function shouldShowCitationSourceListIndex(
citationIndex: number,
platform: MonitoringQuestionDetailPlatform | null,
): boolean {
if (!isDeepSeekInlineCitationPlatform(platform?.ai_platform_id)) {
return true;
}
return resolveCitationIndexDisplayLabels(citationIndex, platform).length === 0;
}
function countHanCharacters(value: string): number {
return value.match(/[\u3400-\u9fff]/g)?.length ?? 0;
}
@@ -575,6 +694,8 @@ function renderDeepSeekAnswerContent(
if (!source && !citationSource) {
if (shouldStripDeepSeekOrphanAnchor(anchor)) {
anchor.remove();
} else {
anchor.replaceWith(document.createTextNode(anchor.textContent ?? ""));
}
continue;
}
@@ -657,28 +778,26 @@ function collectDeepSeekInlineCitationOccurrences(
return [];
}
const inlineCitationLookup = buildInlineCitationSourceLookup(platform);
if (!inlineCitationLookup.size) {
return [];
}
const sourceLookup = buildDeepSeekAnswerLinkSourceLookup(platform);
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)) {
if (!normalizedURL || !shouldCountDeepSeekAnswerLink(anchor)) {
continue;
}
const source = inlineCitationLookup.get(normalizedURL) ?? null;
const source = sourceLookup.get(normalizedURL) ?? null;
const existing = counts.get(normalizedURL);
if (existing) {
// One row per URL, but every visible answer occurrence contributes to the count.
existing.citationCount += 1;
continue;
}
counts.set(normalizedURL, {
label: source?.title?.trim() || null,
siteName: source?.site_name?.trim() || null,
label: source?.title || anchor.getAttribute("title")?.trim() || null,
siteName: source?.siteName || null,
citationCount: 1,
});
}
@@ -996,7 +1115,7 @@ const contentCitationColumns = computed(() => [
<div class="tracking-question-answer__meta">
<span>{{ activePlatform?.sampled_at ? formatDateTime(activePlatform.sampled_at) : "--" }}</span>
<span>{{ activePlatform?.provider_model ?? "--" }}</span>
<span>{{ resolveAnswerProviderModel(activePlatform) }}</span>
</div>
</div>
</section>
@@ -1031,6 +1150,7 @@ const contentCitationColumns = computed(() => [
{{ label }}
</span>
<span
v-if="shouldShowCitationSourceListIndex(index, activePlatform)"
class="tracking-question-source-card__index"
:class="{ 'tracking-question-source-card__index--secondary': resolveCitationIndexDisplayLabels(index, activePlatform).length > 0 }"
>
@@ -97,6 +97,40 @@ describe("deepseek adapter helpers", () => {
expect(classified.searchResults).toHaveLength(1);
});
it("keeps reasoning search pages as citation sources separate from answer content citations", () => {
const classified = classifyDeepseekSources({
inlineLinks: [
{
url: "https://example.com/body-citation",
title: "Answer body citation",
text: "[15]",
siteName: "Body Site",
kind: "inline",
},
],
sourcePanelLinks: [],
searchPanelLinks: [
{
url: "https://example.com/reasoning-search",
title: "Reasoning searched page",
text: "浏览页面",
siteName: "Search Site",
kind: "search",
},
],
});
expect(classified.inlineCitationCandidates.map((item) => item.url)).toEqual([
"https://example.com/body-citation",
]);
expect(classified.citations.map((item) => item.url)).toEqual([
"https://example.com/reasoning-search",
]);
expect(classified.searchResults.map((item) => item.url)).toEqual([
"https://example.com/reasoning-search",
]);
});
it("retains all revealed DeepSeek search results instead of truncating after 32 items", () => {
const searchPanelLinks = Array.from({ length: 78 }, (_, index) => ({
url: `https://example.com/search-${index + 1}`,
@@ -173,6 +207,22 @@ describe("deepseek adapter helpers", () => {
}));
});
it("unwraps DeepSeek redirect URLs before storing source items", () => {
const item = buildSourceItem({
url: "https://chat.deepseek.com/api/redirect?url=https%3A%2F%2Fexample.com%2Fsource%23ref",
title: "Redirected source",
text: null,
siteName: null,
kind: "search",
});
expect(item).toEqual(expect.objectContaining({
url: "https://example.com/source",
normalized_url: "https://example.com/source",
host: "example.com",
}));
});
it("does not use numeric citation markers as source titles", () => {
const item = buildSourceItem({
url: "https://example.com/report#cite-1",
@@ -217,6 +267,45 @@ describe("deepseek adapter helpers", () => {
expect(isDeepSeekWebpagesTriggerText("搜索到 39 个网页")).toBe(false);
});
it("normalizes DeepSeek model labels without mistaking search toggles for models", () => {
const snapshot = {
url: "https://chat.deepseek.com/",
title: "DeepSeek",
loginRequired: false,
loginReason: null,
challengeRequired: false,
challengeReason: null,
busy: false,
busySignals: [],
composerValue: null,
sendDisabled: false,
answer: "Final answer",
answerText: "Final answer",
reasoning: null,
signature: "sig-1",
currentModelLabel: "深度思考 (R1)",
providerRequestID: null,
requestID: null,
inlineLinks: [],
sourcePanelLinks: [],
searchPanelLinks: [],
};
const sources = {
citations: [],
inlineCitationCandidates: [],
searchResults: [],
};
const r1Payload = buildDeepSeekPayload(snapshot, sources, "Final answer");
const searchPayload = buildDeepSeekPayload({
...snapshot,
currentModelLabel: "联网搜索",
}, sources, "Final answer");
expect(r1Payload.provider_model).toBe("DeepSeek-R1");
expect(searchPayload.provider_model).toBeNull();
});
it("builds json-safe payloads with null placeholders instead of undefined", () => {
const payload = buildDeepSeekPayload({
url: "https://chat.deepseek.com/",
File diff suppressed because it is too large Load Diff