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:
@@ -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
Reference in New Issue
Block a user