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}`);
|
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 {
|
function resolveCitationTitle(citation: MonitoringQuestionDetailCitation): string {
|
||||||
return citation.cited_title || citation.article_title || citation.cited_url;
|
return citation.cited_title || citation.article_title || citation.cited_url;
|
||||||
}
|
}
|
||||||
@@ -374,6 +407,78 @@ function buildCitationSourceLookup(platform: MonitoringQuestionDetailPlatform |
|
|||||||
return lookup;
|
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 {
|
function normalizeDeepSeekCitationLabel(value: string | null | undefined): string | null {
|
||||||
const normalized = String(value ?? "").trim().replace(/\s+/g, " ");
|
const normalized = String(value ?? "").trim().replace(/\s+/g, " ");
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
@@ -416,13 +521,16 @@ function pushUniqueDeepSeekCitationLabel(labels: string[], label: string): void
|
|||||||
|
|
||||||
function shouldStripDeepSeekOrphanAnchor(anchor: HTMLAnchorElement): boolean {
|
function shouldStripDeepSeekOrphanAnchor(anchor: HTMLAnchorElement): boolean {
|
||||||
const text = String(anchor.textContent ?? "").trim().replace(/\s+/g, "");
|
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)) {
|
if (/[A-Za-z0-9\u3400-\u9fff]/.test(text)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (anchor.querySelector("img, svg")) {
|
if (anchor.querySelector("img, svg")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return text.length === 0 || /^[\[\](){}<>#.,:;!?+\-_/\\|~`'"“”‘’·•::,。!?;、🔗]+$/.test(text);
|
return text.length === 0 || /^[\[\](){}<>#.,:;!?+\-_/\\|~`'"“”‘’·•::,。!?;、🔗↗↘↙↖↪⤴]+$/.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectDeepSeekCitationReferenceLabels(
|
function collectDeepSeekCitationReferenceLabels(
|
||||||
@@ -489,6 +597,17 @@ function resolveCitationIndexDisplayLabels(
|
|||||||
return labels;
|
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 {
|
function countHanCharacters(value: string): number {
|
||||||
return value.match(/[\u3400-\u9fff]/g)?.length ?? 0;
|
return value.match(/[\u3400-\u9fff]/g)?.length ?? 0;
|
||||||
}
|
}
|
||||||
@@ -575,6 +694,8 @@ function renderDeepSeekAnswerContent(
|
|||||||
if (!source && !citationSource) {
|
if (!source && !citationSource) {
|
||||||
if (shouldStripDeepSeekOrphanAnchor(anchor)) {
|
if (shouldStripDeepSeekOrphanAnchor(anchor)) {
|
||||||
anchor.remove();
|
anchor.remove();
|
||||||
|
} else {
|
||||||
|
anchor.replaceWith(document.createTextNode(anchor.textContent ?? ""));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -657,28 +778,26 @@ function collectDeepSeekInlineCitationOccurrences(
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const inlineCitationLookup = buildInlineCitationSourceLookup(platform);
|
const sourceLookup = buildDeepSeekAnswerLinkSourceLookup(platform);
|
||||||
if (!inlineCitationLookup.size) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const counts = new Map<string, { label: string | null; siteName: string | null; citationCount: number }>();
|
const counts = new Map<string, { label: string | null; siteName: string | null; citationCount: number }>();
|
||||||
for (const anchor of Array.from(root.querySelectorAll<HTMLAnchorElement>("a[href]"))) {
|
for (const anchor of Array.from(root.querySelectorAll<HTMLAnchorElement>("a[href]"))) {
|
||||||
const normalizedURL = normalizeInlineCitationUrl(anchor.getAttribute("href") || anchor.href || "");
|
const normalizedURL = normalizeInlineCitationUrl(anchor.getAttribute("href") || anchor.href || "");
|
||||||
if (!normalizedURL || !inlineCitationLookup.has(normalizedURL)) {
|
if (!normalizedURL || !shouldCountDeepSeekAnswerLink(anchor)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = inlineCitationLookup.get(normalizedURL) ?? null;
|
const source = sourceLookup.get(normalizedURL) ?? null;
|
||||||
const existing = counts.get(normalizedURL);
|
const existing = counts.get(normalizedURL);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
// One row per URL, but every visible answer occurrence contributes to the count.
|
||||||
existing.citationCount += 1;
|
existing.citationCount += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
counts.set(normalizedURL, {
|
counts.set(normalizedURL, {
|
||||||
label: source?.title?.trim() || null,
|
label: source?.title || anchor.getAttribute("title")?.trim() || null,
|
||||||
siteName: source?.site_name?.trim() || null,
|
siteName: source?.siteName || null,
|
||||||
citationCount: 1,
|
citationCount: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -996,7 +1115,7 @@ const contentCitationColumns = computed(() => [
|
|||||||
|
|
||||||
<div class="tracking-question-answer__meta">
|
<div class="tracking-question-answer__meta">
|
||||||
<span>{{ activePlatform?.sampled_at ? formatDateTime(activePlatform.sampled_at) : "--" }}</span>
|
<span>{{ activePlatform?.sampled_at ? formatDateTime(activePlatform.sampled_at) : "--" }}</span>
|
||||||
<span>{{ activePlatform?.provider_model ?? "--" }}</span>
|
<span>{{ resolveAnswerProviderModel(activePlatform) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -1031,6 +1150,7 @@ const contentCitationColumns = computed(() => [
|
|||||||
{{ label }}
|
{{ label }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
|
v-if="shouldShowCitationSourceListIndex(index, activePlatform)"
|
||||||
class="tracking-question-source-card__index"
|
class="tracking-question-source-card__index"
|
||||||
:class="{ 'tracking-question-source-card__index--secondary': resolveCitationIndexDisplayLabels(index, activePlatform).length > 0 }"
|
: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);
|
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", () => {
|
it("retains all revealed DeepSeek search results instead of truncating after 32 items", () => {
|
||||||
const searchPanelLinks = Array.from({ length: 78 }, (_, index) => ({
|
const searchPanelLinks = Array.from({ length: 78 }, (_, index) => ({
|
||||||
url: `https://example.com/search-${index + 1}`,
|
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", () => {
|
it("does not use numeric citation markers as source titles", () => {
|
||||||
const item = buildSourceItem({
|
const item = buildSourceItem({
|
||||||
url: "https://example.com/report#cite-1",
|
url: "https://example.com/report#cite-1",
|
||||||
@@ -217,6 +267,45 @@ describe("deepseek adapter helpers", () => {
|
|||||||
expect(isDeepSeekWebpagesTriggerText("搜索到 39 个网页")).toBe(false);
|
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", () => {
|
it("builds json-safe payloads with null placeholders instead of undefined", () => {
|
||||||
const payload = buildDeepSeekPayload({
|
const payload = buildDeepSeekPayload({
|
||||||
url: "https://chat.deepseek.com/",
|
url: "https://chat.deepseek.com/",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user