import type { JsonValue, MonitoringSourceItem } from '@geo/shared-types' import type { Page as PlaywrightPage } from 'playwright-core' import type { MonitorAdapter } from './base' import { normalizeText } from './common' const DEEPSEEK_BOOTSTRAP_URL = 'https://chat.deepseek.com/' const DEEPSEEK_PAGE_READY_TIMEOUT_MS = 20_000 const DEEPSEEK_QUERY_TIMEOUT_MS = 120_000 const DEEPSEEK_QUERY_POLL_INTERVAL_MS = 1_200 const DEEPSEEK_STABLE_POLLS_REQUIRED = 4 const DEEPSEEK_MAX_SEARCH_RESULT_LINKS = 96 const DEEPSEEK_MAX_CITATION_LINKS = 96 const DEEPSEEK_MAX_INLINE_CITATION_LINKS = 32 const DEEPSEEK_SEARCH_RESULTS_BUTTON_PATTERN = /view search results|search results|搜索结果|查看搜索结果/i const DEEPSEEK_SEARCH_RESULTS_HEADING_PATTERN = /^(?:搜索结果|search results)$/i const DEEPSEEK_WEBPAGES_TRIGGER_ZH_PATTERN = /^(?:已阅读\s*)?\d+\s*个网页$/i const DEEPSEEK_WEBPAGES_TRIGGER_EN_PATTERN = /^(?:(?:read|viewed)\s*)?\d+\s*(?:web\s*pages?|webpages?)$/i const DEEPSEEK_REASONING_SEARCH_TRIGGER_ZH_PATTERN = /^搜索到\s*\d+\s*个网页$/i const DEEPSEEK_REASONING_SEARCH_TRIGGER_EN_PATTERN = /^(?:found|searched)\s*\d+\s*(?:web\s*pages?|webpages?)$/i const DEEPSEEK_BUSY_TEXT_PATTERN = /(searching|thinking|generating|please wait until the message is generated|搜索中|思考中|生成中|停止生成|stop generating)/i const DEEPSEEK_BUSY_DESCRIPTOR_PATTERN = /(searching|thinking|generating|stop generating|loading|spinner|搜索中|思考中|生成中|停止生成)/i const DEEPSEEK_BUSY_PLACEHOLDER_PATTERN = /(site_logo_loading|site_logo_fallback)/i const DEEPSEEK_TOGGLE_SELECTED_PATTERN = /(ds-toggle-button--selected|(^|[\s_-])(active|selected|checked|current|on)([\s_-]|$))/i const DEEPSEEK_DEEP_THINK_PATTERN = /深度思考|deep\s*think|reasoner/i type DeepSeekRawSourceLink = { url: string title: string | null text: string | null siteName: string | null kind: 'inline' | 'source' | 'search' } type DeepSeekSourceInput = DeepSeekRawSourceLink | MonitoringSourceItem type DeepSeekLinkKind = DeepSeekRawSourceLink['kind'] type DeepSeekWindowState = Window & typeof globalThis & { __NEXT_DATA__?: unknown __INITIAL_STATE__?: unknown __APP_STATE__?: unknown __NUXT__?: unknown __STORE__?: { getState?: () => unknown } } type DeepSeekPageSnapshot = { url: string title: string | null loginRequired: boolean loginReason: string | null challengeRequired: boolean challengeReason: string | null busy: boolean busySignals: string[] composerValue: string | null sendDisabled: boolean | null answer: string | null answerText: string | null reasoning: string | null signature: string | null currentModelLabel: string | null providerRequestID: string | null requestID: string | null inlineLinks: DeepSeekRawSourceLink[] sourcePanelLinks: DeepSeekRawSourceLink[] searchPanelLinks: DeepSeekRawSourceLink[] } type DeepSeekPageSnapshotDraft = Omit< DeepSeekPageSnapshot, 'busy' | 'busySignals' | 'signature' > & { bodyText: string | null busyDescriptors: string[] signatureParts: string[] } type DeepSeekClassifiedSources = { inlineCitationCandidates: MonitoringSourceItem[] citations: MonitoringSourceItem[] searchResults: MonitoringSourceItem[] } type DeepSeekWaitResult = | { ok: true finalSnapshot: DeepSeekPageSnapshot stablePollCount: number elapsedMs: number } | { ok: false error: string detail: string | null finalSnapshot: DeepSeekPageSnapshot stablePollCount: number elapsedMs: number } function isDeepSeekWaitFailure( result: DeepSeekWaitResult, ): result is Extract { return !result.ok } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } function normalizeOptionalString(value: unknown): string | null { if (typeof value !== 'string') { return null } const trimmed = value.trim() return trimmed ? trimmed : null } function normalizeDeepSeekProviderModelLabel(value: unknown): string | null { const normalized = normalizeOptionalString(value) if (!normalized) { return null } const compact = normalized.replace(/\s+/g, '').toLowerCase() if (/^(search|websearch|联网搜索|搜索|网页搜索)$/.test(compact)) { return null } 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 (/deepseek/i.test(normalized)) { return normalized } return null } function normalizeUrl(value: unknown, depth = 0): string | null { if (depth > 3) { return null } const input = normalizeText(value) if (!input) { return null } const candidates = [input] try { const decoded = decodeURIComponent(input) if (decoded && decoded !== input) { candidates.push(decoded) } } catch { // Keep the original candidate when the value is not URI-encoded. } for (const candidate of candidates) { try { const url = new URL(candidate) const embeddedURL = extractDeepSeekEmbeddedURL(url, depth + 1) if (embeddedURL) { return embeddedURL } if (!/^https?:$/i.test(url.protocol)) { continue } url.hash = '' return url.toString() } catch { const match = candidate.match(/https?:\/\/[^\s"'<>\\)]+/i)?.[0] if (match && match !== candidate) { const normalized = normalizeUrl(match, depth + 1) if (normalized) { return normalized } } } } return null } function extractDeepSeekEmbeddedURL(url: URL, depth = 0): string | null { if (depth > 3) { return null } const host = url.hostname.toLowerCase() const likelyRedirectURL = /(^|\.)deepseek\.com$/i.test(host) || /(redirect|jump|out|target|link|source)/i.test(url.pathname) if (!likelyRedirectURL) { return null } const candidateKeys = [ 'url', 'target', 'target_url', 'u', 'redirect', 'redirect_url', 'link', 'source', 'source_url', ] for (const key of candidateKeys) { const value = url.searchParams.get(key) if (!value) { continue } const normalized = normalizeUrl(value, depth + 1) if (normalized) { return normalized } } return null } function normalizeSourceTitle(value: unknown): string | null { const title = normalizeText(value) if (!title) { return null } const markerCandidate = title .replace(/\s+/g, '') .replace(/^[\[({(【]+/, '') .replace(/[\])})】]+$/, '') if (/^[-–—]?\d+$/.test(markerCandidate)) { return null } return title } function getSourceNormalizedUrl(source: DeepSeekSourceInput): string | null | undefined { return 'normalized_url' in source ? source.normalized_url : null } function getSourceText(source: DeepSeekSourceInput): string | null | undefined { return 'text' in source ? source.text : null } function getSourceSiteName(source: DeepSeekSourceInput): string | null | undefined { if ('site_name' in source) { return source.site_name } if ('siteName' in source) { return source.siteName } return null } function mergeSourceItemMetadata( item: MonitoringSourceItem, metadata: MonitoringSourceItem | null | undefined, ): MonitoringSourceItem { if (!metadata) { return item } return { ...item, title: item.title ?? metadata.title ?? null, site_name: (item.site_name && item.site_name !== item.host ? item.site_name : null) ?? metadata.site_name ?? item.site_name ?? null, host: item.host ?? metadata.host ?? null, } } function enrichSourceItemsWithMetadata( items: MonitoringSourceItem[], metadataItems: MonitoringSourceItem[], ): MonitoringSourceItem[] { const metadataByUrl = new Map( metadataItems .map((item) => [item.normalized_url ?? item.url, item] as const) .filter(([key]) => Boolean(key)), ) return items.map((item) => mergeSourceItemMetadata(item, metadataByUrl.get(item.normalized_url ?? item.url)), ) } function detectDeepSeekBusySignals(input: { bodyText: string | null | undefined descriptors: Array }): string[] { const busySignals: string[] = [] if (input.bodyText && DEEPSEEK_BUSY_TEXT_PATTERN.test(input.bodyText)) { busySignals.push('body_text') } for (const descriptor of input.descriptors) { const normalized = normalizeText(descriptor) if (!normalized || DEEPSEEK_BUSY_PLACEHOLDER_PATTERN.test(normalized)) { continue } if (!DEEPSEEK_BUSY_DESCRIPTOR_PATTERN.test(normalized)) { continue } busySignals.push(normalized) if (busySignals.length >= 6) { break } } return busySignals } function isDeepSeekToggleSelected(value: string | null | undefined): boolean { const normalized = normalizeText(value) if (!normalized) { return false } return DEEPSEEK_TOGGLE_SELECTED_PATTERN.test(normalized) } function isDeepSeekWebpagesTriggerText(value: string | null | undefined): boolean { const normalized = normalizeText(value) if (!normalized) { return false } return ( DEEPSEEK_WEBPAGES_TRIGGER_ZH_PATTERN.test(normalized) || DEEPSEEK_WEBPAGES_TRIGGER_EN_PATTERN.test(normalized) ) } function isDeepSeekReasoningSearchTriggerText(value: string | null | undefined): boolean { const normalized = normalizeText(value) if (!normalized) { return false } return ( DEEPSEEK_REASONING_SEARCH_TRIGGER_ZH_PATTERN.test(normalized) || DEEPSEEK_REASONING_SEARCH_TRIGGER_EN_PATTERN.test(normalized) ) } function buildSourceItem(source: DeepSeekSourceInput): MonitoringSourceItem | null { const url = normalizeUrl(getSourceNormalizedUrl(source) ?? source.url) if (!url) { return null } let host: string | null = null try { host = new URL(url).hostname || null } catch { host = null } const title = normalizeSourceTitle(source.title) ?? normalizeSourceTitle(getSourceText(source)) const siteName = normalizeText(getSourceSiteName(source) ?? host) return { url, title, site_name: siteName, normalized_url: url, host, } } function dedupeSourceItems( items: Array, ): MonitoringSourceItem[] { const keyed = new Map() for (const item of items) { const built = buildSourceItem(item) if (!built) { continue } const key = built.normalized_url ?? built.url if (!key) { continue } const existing = keyed.get(key) if (!existing) { keyed.set(key, built) continue } keyed.set(key, { ...existing, title: existing.title ?? built.title ?? null, site_name: existing.site_name ?? built.site_name ?? null, host: existing.host ?? built.host ?? null, }) } return Array.from(keyed.values()) } function mergeDeepSeekRawSourceLinks( ...groups: DeepSeekRawSourceLink[][] ): DeepSeekRawSourceLink[] { const keyed = new Map() for (const group of groups) { for (const item of group) { const url = normalizeUrl(item.url) if (!url) { continue } const key = `${item.kind}:${url}` const existing = keyed.get(key) const normalizedItem: DeepSeekRawSourceLink = { ...item, url, } if (!existing) { keyed.set(key, normalizedItem) continue } keyed.set(key, { ...existing, title: existing.title ?? normalizedItem.title ?? null, text: existing.text ?? normalizedItem.text ?? null, siteName: existing.siteName ?? normalizedItem.siteName ?? null, }) } } return Array.from(keyed.values()) } function classifyDeepseekSources(input: { inlineLinks: Array sourcePanelLinks: Array searchPanelLinks: Array }): DeepSeekClassifiedSources { const searchResultsWithMetadata = dedupeSourceItems(input.searchPanelLinks) const inlineCitationCandidates = enrichSourceItemsWithMetadata( dedupeSourceItems([...input.inlineLinks, ...input.sourcePanelLinks]), searchResultsWithMetadata, ).slice(0, DEEPSEEK_MAX_INLINE_CITATION_LINKS) const citations = searchResultsWithMetadata.slice(0, DEEPSEEK_MAX_CITATION_LINKS) const searchResults = searchResultsWithMetadata.slice(0, DEEPSEEK_MAX_SEARCH_RESULT_LINKS) return { inlineCitationCandidates, citations, searchResults, } } function resolveDeepSeekCitationPanelLinks( snapshotSearchPanelLinks: DeepSeekRawSourceLink[], revealedSearchPanelLinks: DeepSeekRawSourceLink[], ): DeepSeekRawSourceLink[] { if (revealedSearchPanelLinks.length > 0) { return mergeDeepSeekRawSourceLinks(revealedSearchPanelLinks) } return mergeDeepSeekRawSourceLinks(snapshotSearchPanelLinks) } function extractQuestionText(payload: Record): string { const candidates = [ payload.question_text, payload.questionText, payload.query, payload.question, payload.prompt, payload.content, payload.title, ] for (const candidate of candidates) { const text = normalizeOptionalString(candidate) if (text) { return text } } throw new Error('deepseek_question_text_missing') } function buildAdapterError( code: string, message: string, extras: Record = {}, ): Record { return { code, message, ...extras, } } function toJsonValue(value: unknown, depth = 0): JsonValue { if (value == null || depth > 12) { return null } if (typeof value === 'string' || typeof value === 'boolean') { return value } if (typeof value === 'number') { return Number.isFinite(value) ? value : null } if (Array.isArray(value)) { return value.map((item) => toJsonValue(item, depth + 1)) } if (!isRecord(value)) { return null } const result: Record = {} for (const [key, item] of Object.entries(value)) { result[key] = toJsonValue(item, depth + 1) } return result } function toJsonSources(items: MonitoringSourceItem[]): JsonValue[] { return items.map((item) => ({ url: item.url, title: item.title ?? null, site_name: item.site_name ?? null, site_key: item.site_key ?? null, normalized_url: item.normalized_url ?? null, host: item.host ?? null, registrable_domain: item.registrable_domain ?? null, subdomain: item.subdomain ?? null, suffix: item.suffix ?? null, article_id: item.article_id ?? null, publish_record_id: item.publish_record_id ?? null, resolution_status: item.resolution_status ?? null, resolution_confidence: item.resolution_confidence ?? null, })) } function buildDeepSeekRawResponse( snapshot: DeepSeekPageSnapshot, classifiedSources: DeepSeekClassifiedSources, answer: string | null, ): Record { const providerModel = normalizeDeepSeekProviderModelLabel(snapshot.currentModelLabel) return { platform: 'deepseek', page_url: snapshot.url, page_title: snapshot.title ?? null, provider_model: providerModel, provider_request_id: snapshot.providerRequestID ?? null, request_id: snapshot.requestID ?? snapshot.providerRequestID ?? null, answer, answer_text: snapshot.answerText ?? null, reasoning: snapshot.reasoning ?? null, busy: snapshot.busy, busy_signals: snapshot.busySignals, send_disabled: snapshot.sendDisabled, signature: snapshot.signature ?? null, citation_count: classifiedSources.citations.length, inline_citation_candidate_count: classifiedSources.inlineCitationCandidates.length, search_result_count: classifiedSources.searchResults.length, inline_citation_candidates: toJsonSources(classifiedSources.inlineCitationCandidates), citations: toJsonSources(classifiedSources.citations), search_results: toJsonSources(classifiedSources.searchResults), inline_links: toJsonValue(snapshot.inlineLinks), source_panel_links: toJsonValue(snapshot.sourcePanelLinks), search_panel_links: toJsonValue(snapshot.searchPanelLinks), } } function buildDeepSeekPayload( snapshot: DeepSeekPageSnapshot, classifiedSources: DeepSeekClassifiedSources, answer: string | null, ): Record { const providerModel = normalizeDeepSeekProviderModelLabel(snapshot.currentModelLabel) return { platform: 'deepseek', provider_model: providerModel, provider_request_id: snapshot.providerRequestID ?? null, request_id: snapshot.requestID ?? snapshot.providerRequestID ?? null, answer, citations: toJsonSources(classifiedSources.citations), search_results: toJsonSources(classifiedSources.searchResults), raw_response_json: buildDeepSeekRawResponse(snapshot, classifiedSources, answer), } } function answerScore(snapshot: DeepSeekPageSnapshot): number { const answerLength = snapshot.answerText?.length ?? snapshot.answer?.length ?? 0 const linkCount = snapshot.inlineLinks.length + snapshot.sourcePanelLinks.length + snapshot.searchPanelLinks.length return answerLength * 8 + linkCount * 13 + (snapshot.reasoning?.length ?? 0) } function isObservationComplete(snapshot: { answer: string | null busy: boolean loginRequired: boolean challengeRequired: boolean signature: string | null }): boolean { return Boolean( snapshot.answer && snapshot.signature && !snapshot.busy && !snapshot.loginRequired && !snapshot.challengeRequired, ) } function safePageURL(page: PlaywrightPage): string { try { return page.url() } catch { return '' } } async function sleep(ms: number, signal?: AbortSignal): Promise { if (signal?.aborted) { throw new Error('adapter_aborted') } await new Promise((resolve, reject) => { const timer = setTimeout(() => { signal?.removeEventListener('abort', onAbort) resolve() }, ms) const onAbort = () => { clearTimeout(timer) reject(new Error('adapter_aborted')) } signal?.addEventListener('abort', onAbort, { once: true }) }) } async function ensurePageOnDeepSeekChat(page: PlaywrightPage, signal: AbortSignal): Promise { if (signal.aborted) { throw new Error('adapter_aborted') } const currentURL = safePageURL(page) if (!currentURL || !currentURL.startsWith('https://chat.deepseek.com/')) { await page.goto(DEEPSEEK_BOOTSTRAP_URL, { waitUntil: 'domcontentloaded', timeout: DEEPSEEK_PAGE_READY_TIMEOUT_MS, }) } await page .waitForLoadState('domcontentloaded', { timeout: DEEPSEEK_PAGE_READY_TIMEOUT_MS, }) .catch(() => undefined) await page .waitForLoadState('networkidle', { timeout: 3_000, }) .catch(() => undefined) if (signal.aborted) { throw new Error('adapter_aborted') } } async function resolveDeepSeekEditor(page: PlaywrightPage): Promise<{ locator: ReturnType mode: 'textarea' | 'contenteditable' }> { const candidates: Array<{ selector: string; mode: 'textarea' | 'contenteditable' }> = [ { selector: 'textarea.ds-textarea', mode: 'textarea' }, { selector: '.ds-textarea textarea', mode: 'textarea' }, { selector: "textarea[placeholder*='DeepSeek']", mode: 'textarea' }, { selector: "textarea[placeholder*='消息']", mode: 'textarea' }, { selector: 'textarea', mode: 'textarea' }, { selector: "[role='textbox'][contenteditable='true']", mode: 'contenteditable' }, { selector: "[contenteditable='true'][data-lexical-editor='true']", mode: 'contenteditable' }, { selector: "div[contenteditable='true']", mode: 'contenteditable' }, ] for (const candidate of candidates) { const locator = page.locator(candidate.selector).first() const count = await locator.count().catch(() => 0) if (!count) { continue } const visible = await locator.isVisible().catch(() => false) if (!visible) { continue } return { locator, mode: candidate.mode, } } throw new Error('deepseek_editor_not_found') } async function tryClickDeepSeekSendButton( _page: PlaywrightPage, editor: ReturnType, ): Promise { return await editor .evaluate((node) => { const isVisible = (value: Element | null): value is HTMLElement => { if (!(value instanceof HTMLElement)) { return false } const style = window.getComputedStyle(value) if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false } const rect = value.getBoundingClientRect() return rect.width > 0 && rect.height > 0 } const root = node.closest( "form, [class*='textarea'], [class*='input'], [class*='editor'], [class*='compose']", ) ?? node.parentElement ?? document.body const buttons = Array.from(root.querySelectorAll("button, [role='button']")) for (const button of buttons.reverse()) { if (!isVisible(button)) { continue } const htmlButton = button as HTMLButtonElement if ('disabled' in htmlButton && htmlButton.disabled) { continue } htmlButton.click() return true } return false }) .catch(() => false) } async function ensureDeepSeekToggleEnabled( page: PlaywrightPage, labelPattern: RegExp, signal: AbortSignal, ): Promise { if (signal.aborted) { throw new Error('adapter_aborted') } const clicked = await page .evaluate( ({ source, flags }) => { const pattern = new RegExp(source, flags) const normalize = (value: unknown): string | null => { if (typeof value !== 'string') { return null } const trimmed = value.trim().replace(/\s+/g, ' ') return trimmed || null } const isVisible = (node: Element | null): node is HTMLElement => { if (!(node instanceof HTMLElement)) { return false } const style = window.getComputedStyle(node) if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false } const rect = node.getBoundingClientRect() return rect.width > 0 && rect.height > 0 } const isSelected = (element: HTMLElement | null): boolean => { if (!element) { return false } const attrs = [ element.getAttribute('aria-pressed'), element.getAttribute('aria-checked'), element.getAttribute('data-state'), element.getAttribute('data-selected'), element.getAttribute('data-active'), ] .map((value) => normalize(value)?.toLowerCase() ?? null) .filter((value): value is string => value !== null) if ( attrs.some((value) => ['true', 'checked', 'selected', 'active', 'on'].includes(value)) ) { return true } if (attrs.some((value) => ['false', 'unchecked', 'off'].includes(value))) { return false } const className = normalize(String(element.className || '')) ?? '' return /(ds-toggle-button--selected|(^|[\s_-])(active|selected|checked|current|on)([\s_-]|$))/i.test( className, ) } const candidates = Array.from( document.querySelectorAll( "button, [role='button'], [class*='toggle-button'], [class*='toggle']", ), ).filter(isVisible) const target = candidates.find((node) => { const text = normalize( node.innerText || node.textContent || node.getAttribute('aria-label') || '', ) return Boolean(text && pattern.test(text)) }) if (!target || isSelected(target)) { return false } target.click() return true }, { source: labelPattern.source, flags: labelPattern.flags, }, ) .catch(() => false) if (clicked) { await page.waitForTimeout(180) } } async function submitDeepSeekQuestion( page: PlaywrightPage, questionText: string, signal: AbortSignal, ): Promise { if (signal.aborted) { throw new Error('adapter_aborted') } const { locator, mode } = await resolveDeepSeekEditor(page) await ensureDeepSeekToggleEnabled(page, DEEPSEEK_DEEP_THINK_PATTERN, signal).catch( () => undefined, ) await locator.click({ timeout: 5_000 }) if (mode === 'textarea') { await locator.fill(questionText) } else { await page.keyboard.press('Meta+A').catch(() => undefined) await page.keyboard.press('Control+A').catch(() => undefined) await page.keyboard.press('Backspace').catch(() => undefined) await page.keyboard.insertText(questionText) } await page.keyboard.press('Enter').catch(() => undefined) await sleep(300, signal) const snapshot = await readDeepSeekPageSnapshot(page, questionText).catch(() => null) const composerStillFilled = normalizeText(snapshot?.composerValue) if (composerStillFilled && composerStillFilled.includes(questionText)) { const clicked = await tryClickDeepSeekSendButton(page, locator) if (!clicked) { throw new Error('deepseek_send_button_unavailable') } } } async function maybeRevealDeepSeekSources(page: PlaywrightPage): Promise { if (await hasDeepSeekSearchResultsPanel(page)) { return } const webpagesTrigger = await page .evaluate(() => { const normalize = (value: unknown): string | null => { if (typeof value !== 'string') { return null } const trimmed = value.trim().replace(/\s+/g, ' ') return trimmed || null } const isVisible = (node: Element | null): boolean => { if (!(node instanceof HTMLElement)) { return false } const style = window.getComputedStyle(node) if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false } const rect = node.getBoundingClientRect() return rect.width > 0 && rect.height > 0 } const isTriggerText = (value: unknown): boolean => { const text = normalize(value) if (!text) { return false } return ( /^(?:已阅读\s*)?\d+\s*个网页$/i.test(text) || /^(?:(?:read|viewed)\s*)?\d+\s*(?:web\s*pages?|webpages?)$/i.test(text) ) } const isClickable = (node: HTMLElement): boolean => { const tagName = node.tagName.toLowerCase() const role = normalize(node.getAttribute('role')) const style = window.getComputedStyle(node) return ( tagName === 'button' || tagName === 'a' || role === 'button' || node.tabIndex >= 0 || style.cursor === 'pointer' || typeof (node as { onclick?: unknown }).onclick === 'function' ) } const findClickTarget = (node: HTMLElement): HTMLElement | null => { let fallback: HTMLElement | null = null let current: HTMLElement | null = node for (let depth = 0; current && depth < 9; depth += 1) { if (!isVisible(current)) { current = current.parentElement continue } const currentText = normalize( current.textContent || current.getAttribute('aria-label') || '', ) if (!fallback && currentText && isTriggerText(currentText)) { fallback = current } if (isClickable(current)) { return current } current = current.parentElement } return fallback } let bestTarget: HTMLElement | null = null let bestText: string | null = null let bestScore = -1 for (const node of Array.from( document.querySelectorAll("span, div, button, a, [role='button']"), ).slice(0, 1200)) { if (!(node instanceof HTMLElement) || !isVisible(node)) { continue } const text = normalize(node.textContent || node.getAttribute('aria-label') || '') if (!isTriggerText(text)) { continue } const target = findClickTarget(node) if (!(target instanceof HTMLElement)) { continue } const rect = target.getBoundingClientRect() let score = 0 if (isClickable(target)) { score += 32 } if (normalize(target.textContent || target.getAttribute('aria-label') || '') === text) { score += 12 } if (rect.top >= window.innerHeight * 0.35) { score += 20 } if (rect.width <= 320) { score += 8 } if (rect.left >= window.innerWidth * 0.2) { score += 4 } const className = String(target.className || '') if (/web|page|source|search|reference|ds-/i.test(className)) { score += 4 } if (score > bestScore) { bestScore = score bestTarget = target bestText = text } } if (!bestTarget) { return null } bestTarget.scrollIntoView({ block: 'center', inline: 'center' }) const rect = bestTarget.getBoundingClientRect() return { text: bestText, x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, width: rect.width, height: rect.height, tagName: bestTarget.tagName, className: String(bestTarget.className || ''), } }) .catch(() => false) if (webpagesTrigger && typeof webpagesTrigger === 'object') { await page.mouse.click(webpagesTrigger.x, webpagesTrigger.y).catch(() => undefined) await page.waitForTimeout(500) if (await waitForDeepSeekSearchResultsPanel(page, 5_000)) { return } await page .evaluate(() => { const normalize = (value: unknown): string | null => { if (typeof value !== 'string') { return null } const trimmed = value.trim().replace(/\s+/g, ' ') return trimmed || null } const isVisible = (node: Element | null): boolean => { if (!(node instanceof HTMLElement)) { return false } const style = window.getComputedStyle(node) if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false } const rect = node.getBoundingClientRect() return rect.width > 0 && rect.height > 0 } const isTriggerText = (value: unknown): boolean => { const text = normalize(value) return Boolean( text && (/^(?:已阅读\s*)?\d+\s*个网页$/i.test(text) || /^(?:(?:read|viewed)\s*)?\d+\s*(?:web\s*pages?|webpages?)$/i.test(text)), ) } const isClickable = (node: HTMLElement): boolean => { const tagName = node.tagName.toLowerCase() const role = normalize(node.getAttribute('role')) const style = window.getComputedStyle(node) return ( tagName === 'button' || tagName === 'a' || role === 'button' || node.tabIndex >= 0 || style.cursor === 'pointer' || typeof (node as { onclick?: unknown }).onclick === 'function' ) } const dispatchClick = (target: HTMLElement): void => { target.dispatchEvent( new PointerEvent('pointerdown', { bubbles: true, cancelable: true, pointerId: 1 }), ) target.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })) target.dispatchEvent( new PointerEvent('pointerup', { bubbles: true, cancelable: true, pointerId: 1 }), ) target.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })) target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) } for (const node of Array.from( document.querySelectorAll("span, div, button, a, [role='button']"), ).slice(0, 1200)) { if (!(node instanceof HTMLElement) || !isVisible(node)) { continue } const text = normalize(node.textContent || node.getAttribute('aria-label') || '') if (!isTriggerText(text)) { continue } let target: HTMLElement | null = node let fallback: HTMLElement | null = node for (let depth = 0; target && depth < 9; depth += 1) { if (isVisible(target) && isClickable(target)) { dispatchClick(target) return } if (isVisible(target)) { fallback = target } target = target.parentElement } if (fallback) { dispatchClick(fallback) return } } }) .catch(() => undefined) await page.waitForTimeout(500) if (await waitForDeepSeekSearchResultsPanel(page)) { return } } const searchResultsButton = page .getByRole('button', { name: DEEPSEEK_SEARCH_RESULTS_BUTTON_PATTERN, }) .first() if (await searchResultsButton.isVisible().catch(() => false)) { await searchResultsButton.click().catch(() => undefined) await page.waitForTimeout(300) await waitForDeepSeekSearchResultsPanel(page).catch(() => undefined) } } async function maybeRevealDeepSeekReasoningSearchSources(page: PlaywrightPage): Promise { if (await hasDeepSeekSearchResultsPanel(page)) { return } const reasoningSearchTrigger = await page .evaluate(() => { const normalize = (value: unknown): string | null => { if (typeof value !== 'string') { return null } const trimmed = value.trim().replace(/\s+/g, ' ') return trimmed || null } const isVisible = (node: Element | null): node is HTMLElement => { if (!(node instanceof HTMLElement)) { return false } const style = window.getComputedStyle(node) if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false } const rect = node.getBoundingClientRect() return rect.width > 0 && rect.height > 0 } const isTriggerText = (value: unknown): boolean => { const text = normalize(value) return Boolean( text && (/^搜索到\s*\d+\s*个网页$/i.test(text) || /^(?:found|searched)\s*\d+\s*(?:web\s*pages?|webpages?)$/i.test(text)), ) } const isReasoningNode = (node: HTMLElement): boolean => { if ( node.closest(".ds-think-content, [class*='think-content'], [class*='reasoning-content']") ) { return true } let current: HTMLElement | null = node for (let depth = 0; current && depth < 8; depth += 1) { const text = normalize(current.textContent || '') const className = String(current.className || '') if ( /think|reason|analysis|思考/i.test(className) || Boolean(text && /已思考|思考过程|搜索到\s*\d+\s*个网页/.test(text)) ) { return true } current = current.parentElement } return false } const isClickable = (node: HTMLElement): boolean => { const tagName = node.tagName.toLowerCase() const role = normalize(node.getAttribute('role')) const style = window.getComputedStyle(node) return ( tagName === 'button' || tagName === 'a' || role === 'button' || node.tabIndex >= 0 || style.cursor === 'pointer' || typeof (node as { onclick?: unknown }).onclick === 'function' ) } const findClickTarget = (node: HTMLElement): HTMLElement | null => { let fallback: HTMLElement | null = null const nextParent = (element: HTMLElement): HTMLElement | null => element.parentNode instanceof HTMLElement ? element.parentNode : null let current: HTMLElement | null = node for (let depth = 0; current !== null && depth < 10; depth += 1) { const element: HTMLElement = current if (!isVisible(element)) { current = nextParent(element) continue } const currentText = normalize( element.textContent || element.getAttribute('aria-label') || '', ) if (!fallback && currentText && isTriggerText(currentText)) { fallback = element } if (isClickable(element)) { return element } current = nextParent(element) } return fallback } let bestTarget: HTMLElement | null = null let bestScore = -1 for (const node of Array.from( document.querySelectorAll("span, div, button, a, [role='button']"), ).slice(0, 1600)) { if (!(node instanceof HTMLElement) || !isVisible(node)) { continue } const text = normalize(node.textContent || node.getAttribute('aria-label') || '') if (!isTriggerText(text) || !isReasoningNode(node)) { continue } const target = findClickTarget(node) if (!(target instanceof HTMLElement)) { continue } const rect = target.getBoundingClientRect() let score = 0 if (isClickable(target)) { score += 32 } if (rect.left >= window.innerWidth * 0.18 && rect.left <= window.innerWidth * 0.75) { score += 18 } if (rect.top >= window.innerHeight * 0.2) { score += 12 } if (rect.width <= 360) { score += 8 } if (target.closest(".ds-think-content, [class*='think-content']")) { score += 16 } if (score > bestScore) { bestScore = score bestTarget = target } } if (!bestTarget) { return null } bestTarget.scrollIntoView({ block: 'center', inline: 'center' }) const rect = bestTarget.getBoundingClientRect() return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, } }) .catch(() => null) if (!reasoningSearchTrigger || typeof reasoningSearchTrigger !== 'object') { return } await page.mouse.click(reasoningSearchTrigger.x, reasoningSearchTrigger.y).catch(() => undefined) await page.waitForTimeout(500).catch(() => undefined) if (await waitForDeepSeekSearchResultsPanel(page, 5_000)) { return } await page .evaluate(() => { const normalize = (value: unknown): string | null => { if (typeof value !== 'string') { return null } const trimmed = value.trim().replace(/\s+/g, ' ') return trimmed || null } const isVisible = (node: Element | null): node is HTMLElement => { if (!(node instanceof HTMLElement)) { return false } const style = window.getComputedStyle(node) if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false } const rect = node.getBoundingClientRect() return rect.width > 0 && rect.height > 0 } const isTriggerText = (value: unknown): boolean => { const text = normalize(value) return Boolean(text && /^搜索到\s*\d+\s*个网页$/i.test(text)) } const isClickable = (node: HTMLElement): boolean => { const tagName = node.tagName.toLowerCase() const role = normalize(node.getAttribute('role')) const style = window.getComputedStyle(node) return ( tagName === 'button' || tagName === 'a' || role === 'button' || node.tabIndex >= 0 || style.cursor === 'pointer' || typeof (node as { onclick?: unknown }).onclick === 'function' ) } const dispatchClick = (target: HTMLElement): void => { target.dispatchEvent( new PointerEvent('pointerdown', { bubbles: true, cancelable: true, pointerId: 1 }), ) target.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })) target.dispatchEvent( new PointerEvent('pointerup', { bubbles: true, cancelable: true, pointerId: 1 }), ) target.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })) target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) } for (const node of Array.from( document.querySelectorAll("span, div, button, a, [role='button']"), ).slice(0, 1600)) { if (!(node instanceof HTMLElement) || !isVisible(node)) { continue } const text = normalize(node.textContent || node.getAttribute('aria-label') || '') if (!isTriggerText(text)) { continue } let target: HTMLElement | null = node let fallback: HTMLElement | null = node for (let depth = 0; target && depth < 10; depth += 1) { if (isVisible(target) && isClickable(target)) { dispatchClick(target) return } if (isVisible(target)) { fallback = target } target = target.parentElement } if (fallback) { dispatchClick(fallback) return } } }) .catch(() => undefined) await page.waitForTimeout(500).catch(() => undefined) await waitForDeepSeekSearchResultsPanel(page).catch(() => undefined) } async function maybeExpandDeepSeekReasoning(page: PlaywrightPage): Promise { await page .evaluate(() => { const normalize = (value: unknown): string | null => { if (typeof value !== 'string') { return null } const trimmed = value.trim().replace(/\s+/g, ' ') return trimmed || null } const isVisible = (node: Element | null): node is HTMLElement => { if (!(node instanceof HTMLElement)) { return false } const style = window.getComputedStyle(node) if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false } const rect = node.getBoundingClientRect() return rect.width > 0 && rect.height > 0 } const isCollapsed = (node: HTMLElement): boolean => { const expanded = normalize(node.getAttribute('aria-expanded')) if (expanded === 'true') { return false } if (expanded === 'false') { return true } const className = String(node.className || '') return /(collapsed|folded|close|closed)/i.test(className) } const hasVisibleReasoningContent = (node: HTMLElement): boolean => { const root = node.closest("[data-message-id], [class*='message'], article, li, section") ?? node.parentElement if (!(root instanceof HTMLElement)) { return false } return Array.from( root.querySelectorAll( ".ds-think-content, [class*='think-content'], [class*='reasoning-content']", ), ).some( (candidate) => isVisible(candidate) && Boolean(normalize(candidate.textContent || '')), ) } for (const node of Array.from( document.querySelectorAll("button, [role='button'], [class*='think']"), )) { if (!(node instanceof HTMLElement) || !isVisible(node)) { continue } const text = normalize(node.textContent || node.getAttribute('aria-label') || '') if (!text || !/(已思考|思考过程|思维链|thought|thinking|reasoning)/i.test(text)) { continue } if (!isCollapsed(node) && hasVisibleReasoningContent(node)) { continue } node.click() break } }) .catch(() => undefined) await page.waitForTimeout(180).catch(() => undefined) } async function hasDeepSeekSearchResultsPanel(page: PlaywrightPage): Promise { const heading = page .getByRole('heading', { name: DEEPSEEK_SEARCH_RESULTS_HEADING_PATTERN, }) .first() if (await heading.isVisible().catch(() => false)) { return true } const cardTitle = page .locator("[class*='search-view-card__title'], .search-view-card__title") .first() return await cardTitle.isVisible().catch(() => false) } async function waitForDeepSeekSearchResultsPanel( page: PlaywrightPage, timeoutMs = 3_000, ): Promise { const deadline = Date.now() + timeoutMs while (Date.now() < deadline) { if (await hasDeepSeekSearchResultsPanel(page)) { return true } await page.waitForTimeout(150) } return await hasDeepSeekSearchResultsPanel(page) } async function collectDeepSeekSearchResultsPanelLinks( page: PlaywrightPage, ): Promise { return await page .evaluate(async () => { type EvalSourceLink = { url: string title: string | null text: string | null siteName: string | null kind: 'search' } const sleep = (ms: number) => new Promise((resolve) => { window.setTimeout(resolve, ms) }) const normalize = (value: unknown): string | null => { if (typeof value !== 'string') { return null } const trimmed = value.trim().replace(/\s+/g, ' ') return trimmed || null } const isVisible = (node: Element | null): node is HTMLElement => { if (!(node instanceof HTMLElement)) { return false } const style = window.getComputedStyle(node) if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false } const rect = node.getBoundingClientRect() return rect.width > 0 && rect.height > 0 } const normalizeUrl = (value: unknown, depth = 0): string | null => { if (depth > 3) { return null } const text = normalize(value) if (!text) { return null } const candidates = [text] try { const decoded = decodeURIComponent(text) if (decoded && decoded !== text) { candidates.push(decoded) } } catch { // Keep the original candidate when the value is not URI-encoded. } const unwrapEmbeddedURL = (url: URL, depthValue: number): string | null => { if (depthValue > 3) { return null } const likelyRedirectURL = url.hostname === window.location.hostname || /(^|\.)deepseek\.com$/i.test(url.hostname) || /(redirect|jump|out|target|link|source)/i.test(url.pathname) if (!likelyRedirectURL) { return null } for (const key of [ 'url', 'target', 'target_url', 'u', 'redirect', 'redirect_url', 'link', 'source', 'source_url', ]) { const paramValue = url.searchParams.get(key) if (!paramValue) { continue } const normalized = normalizeUrl(paramValue, depthValue + 1) if (normalized) { return normalized } } return null } for (const candidate of candidates) { try { const url = new URL(candidate, window.location.href) const embeddedURL = unwrapEmbeddedURL(url, depth + 1) if (embeddedURL) { return embeddedURL } if (!/^https?:$/i.test(url.protocol)) { continue } url.hash = '' return url.toString() } catch { const match = candidate.match(/https?:\/\/[^\s"'<>\\)]+/i)?.[0] if (match && match !== candidate) { const normalized = normalizeUrl(match, depth + 1) if (normalized) { return normalized } } } } return null } const isExternalUrl = (value: unknown): boolean => { const href = normalizeUrl(value) if (!href) { return false } try { return new URL(href).hostname !== window.location.hostname } catch { return false } } const queryText = (root: ParentNode, selectors: string): string | null => { for (const node of Array.from(root.querySelectorAll(selectors))) { const text = normalize(node.textContent || '') if (text) { return text } } return null } const findURLInAttributeValue = (value: unknown): string | null => { const normalized = normalizeUrl(value) if (normalized && isExternalUrl(normalized)) { return normalized } const text = normalize(value) if (!text) { return null } const match = text.match(/https?:\/\/[^\s"'<>\\)]+/i)?.[0] ?? null return match ? normalizeUrl(match) : null } const findURLInElement = (root: Element): string | null => { const nodes = [ root, ...Array.from( root.querySelectorAll( '[href], [data-url], [data-href], [data-link], [data-source-url], [data-target-url], [title], [aria-label]', ), ).slice(0, 40), ] for (const node of nodes) { for (const attribute of Array.from(node.attributes)) { const url = findURLInAttributeValue(attribute.value) if (url) { return url } } } return null } const extractElementMetadata = ( element: Element, ): Pick => ({ title: normalize(element.getAttribute('title')) || queryText( element, "[class*='search-view-card__title'], [class*='source-card__title'], [class*='reference-card__title'], [class*='result-title'], [class*='card__title'], h1, h2, h3, h4", ) || normalize(element instanceof HTMLAnchorElement ? element.textContent || '' : '') || null, text: queryText( element, "[class*='search-view-card__snippet'], [class*='source-card__snippet'], [class*='reference-card__snippet'], [class*='result-snippet'], [class*='card__snippet'], [class*='snippet'], [class*='summary'], p", ) || normalize(element.textContent || '') || null, siteName: normalize(element.getAttribute('data-site-name')) || normalize(element.getAttribute('data-domain')) || normalize(element.getAttribute('data-host')) || queryText( element, "[class*='site'], [class*='domain'], [class*='host'], [class*='origin'], [class*='source-card__source']", ) || normalize(element.getAttribute('aria-label')) || null, }) const findResultRoot = (node: Element): Element => { let current: Element | null = node for (let depth = 0; current && depth < 5; depth += 1) { const className = String((current as HTMLElement).className || '') if ( /(search|result|source|reference).*card|card.*(search|result|source|reference)/i.test( className, ) ) { return current } current = current.parentElement } return node } const findSearchResultsPanel = (): HTMLElement | null => { const headingPattern = /^(?:搜索结果|search results)$/i const seen = new Set() const candidates: Array<{ node: HTMLElement; score: number }> = [] const addCandidate = (node: Element | null, baseScore: number): void => { let current: HTMLElement | null = node instanceof HTMLElement ? node : null for (let depth = 0; current && depth < 7; depth += 1) { if (seen.has(current) || !isVisible(current)) { current = current.parentElement continue } const rect = current.getBoundingClientRect() if (rect.width < 220 || rect.height < 220 || rect.left < window.innerWidth * 0.45) { current = current.parentElement continue } const style = window.getComputedStyle(current) const headingText = queryText(current, "[role='heading'], h1, h2, h3, h4") const hasHeading = Boolean(headingText && headingPattern.test(headingText)) const hasSearchCards = Boolean( current.querySelector("[class*='search-view-card__title'], .search-view-card__title"), ) if (!hasHeading && !hasSearchCards) { current = current.parentElement continue } let score = baseScore if (hasHeading) { score += 24 } if (hasSearchCards) { score += 24 } if ( ['auto', 'scroll', 'overlay'].includes(style.overflowY) && current.scrollHeight > current.clientHeight + 80 ) { score += 18 } if (rect.left >= window.innerWidth * 0.55) { score += 18 } if (rect.width <= 540) { score += 8 } if (rect.height >= 360) { score += 8 } score += Math.min(current.querySelectorAll('a[href]').length, 12) seen.add(current) candidates.push({ node: current, score }) current = current.parentElement } } for (const heading of Array.from( document.querySelectorAll("[role='heading'], h1, h2, h3, h4"), )) { const text = normalize(heading.textContent || '') if (text && headingPattern.test(text)) { addCandidate(heading, 40) } } for (const titleNode of Array.from( document.querySelectorAll("[class*='search-view-card__title'], .search-view-card__title"), )) { addCandidate(titleNode, 30) } return candidates.sort((left, right) => right.score - left.score).at(0)?.node ?? null } const searchPanel = findSearchResultsPanel() if (!(searchPanel instanceof HTMLElement)) { return [] } const originalScrollTop = searchPanel.scrollTop const seen = new Set() const results: EvalSourceLink[] = [] const pushResult = (url: string | null, metadataRoot: Element): void => { if (!url || !isExternalUrl(url) || seen.has(url)) { return } const metadata = extractElementMetadata(metadataRoot) seen.add(url) results.push({ url, title: metadata.title, text: metadata.text, siteName: metadata.siteName, kind: 'search', }) } const collect = () => { for (const anchor of Array.from( searchPanel.querySelectorAll('a[href]'), )) { const href = normalizeUrl(anchor.getAttribute('href') || anchor.href || '') if (!href) { continue } pushResult(href, findResultRoot(anchor)) } const candidates = Array.from( searchPanel.querySelectorAll( "[class*='search-view-card'], [class*='search-result'], [class*='result-card'], [class*='source-card'], [class*='reference-card'], [data-url], [data-href], [data-link], [data-source-url], [data-target-url]", ), ) for (const candidate of candidates) { if (!(candidate instanceof HTMLElement) || !isVisible(candidate)) { continue } pushResult(findURLInElement(candidate), findResultRoot(candidate)) } } collect() let previousScrollTop = -1 let stableScrollCount = 0 for (let attempt = 0; attempt < 48; attempt += 1) { const nextTop = Math.min( searchPanel.scrollTop + Math.max(searchPanel.clientHeight - 96, 420), Math.max(searchPanel.scrollHeight - searchPanel.clientHeight, 0), ) searchPanel.scrollTop = nextTop await sleep(140) collect() if (searchPanel.scrollTop === previousScrollTop) { stableScrollCount += 1 } else { previousScrollTop = searchPanel.scrollTop stableScrollCount = 0 } if ( searchPanel.scrollTop + searchPanel.clientHeight >= searchPanel.scrollHeight - 2 && stableScrollCount >= 1 ) { break } if (stableScrollCount >= 2) { break } } searchPanel.scrollTop = originalScrollTop return results }) .catch(() => []) } async function readDeepSeekPageSnapshot( page: PlaywrightPage, questionText?: string, ): Promise { const draft = (await page.evaluate((input) => { type EvalRecord = Record type EvalSourceLink = { url: string title: string | null text: string | null siteName: string | null kind: DeepSeekLinkKind } const globalWindow = window as DeepSeekWindowState const questionText = typeof input === 'string' ? input.trim().replace(/\s+/g, ' ') : '' const normalize = (value: unknown): string | null => { if (typeof value !== 'string') { return null } const trimmed = value.trim().replace(/\s+/g, ' ') return trimmed || null } const normalizeComparable = (value: unknown): string => (normalize(value) || '').toLowerCase() const normalizeModelLabel = (value: unknown): string | null => { const text = normalize(value) if (!text) { return null } const compact = text.replace(/\s+/g, '').toLowerCase() if (/^(search|websearch|联网搜索|搜索|网页搜索)$/.test(compact)) { return null } if (/r1|reasoner|deepthink|深度思考/.test(compact)) { return 'DeepSeek' } const versionMatch = text.match(/deepseek[-_\s]*(r1|v\d+(?:\.\d+)?)/i) ?? text.match(/\b(r1|v\d+(?:\.\d+)?)\b/i) if (versionMatch?.[1]) { return `DeepSeek-${versionMatch[1].toUpperCase()}` } if (/deepseek/i.test(text) && text.length <= 60) { return text } return null } const isVisible = (node: Element | null): node is HTMLElement => { if (!(node instanceof HTMLElement)) { return false } const style = window.getComputedStyle(node) if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { return false } const rect = node.getBoundingClientRect() return rect.width > 0 && rect.height > 0 } const normalizeUrl = (value: unknown, depth = 0): string | null => { if (depth > 3) { return null } const text = normalize(value) if (!text) { return null } const candidates = [text] try { const decoded = decodeURIComponent(text) if (decoded && decoded !== text) { candidates.push(decoded) } } catch { // Keep the original candidate when the value is not URI-encoded. } const unwrapEmbeddedURL = (url: URL, depthValue: number): string | null => { if (depthValue > 3) { return null } const likelyRedirectURL = url.hostname === window.location.hostname || /(^|\.)deepseek\.com$/i.test(url.hostname) || /(redirect|jump|out|target|link|source)/i.test(url.pathname) if (!likelyRedirectURL) { return null } for (const key of [ 'url', 'target', 'target_url', 'u', 'redirect', 'redirect_url', 'link', 'source', 'source_url', ]) { const paramValue = url.searchParams.get(key) if (!paramValue) { continue } const normalized = normalizeUrl(paramValue, depthValue + 1) if (normalized) { return normalized } } return null } for (const candidate of candidates) { try { const url = new URL(candidate, window.location.href) const embeddedURL = unwrapEmbeddedURL(url, depth + 1) if (embeddedURL) { return embeddedURL } if (!/^https?:$/i.test(url.protocol)) { continue } url.hash = '' return url.toString() } catch { const match = candidate.match(/https?:\/\/[^\s"'<>\\)]+/i)?.[0] if (match && match !== candidate) { const normalized = normalizeUrl(match, depth + 1) if (normalized) { return normalized } } } } return null } const isExternalUrl = (value: unknown): boolean => { const href = normalizeUrl(value) if (!href) { return false } try { return new URL(href).hostname !== window.location.hostname } catch { return false } } const linkKey = (url: string, kind: DeepSeekLinkKind): string => `${kind}:${url}` const queryText = (root: ParentNode, selectors: string): string | null => { for (const node of Array.from(root.querySelectorAll(selectors))) { const text = normalize(node.textContent || '') if (text) { return text } } return null } const renderStructuredHtml = (root: Element | null): string | null => { if (!(root instanceof Element)) { return null } const clone = root.cloneNode(true) if (!(clone instanceof HTMLElement)) { return null } for (const anchor of Array.from(clone.querySelectorAll('a[href]'))) { const href = normalizeUrl(anchor.getAttribute('href') || anchor.href || '') if (!href || !isExternalUrl(href)) { continue } anchor.setAttribute('href', href) anchor.setAttribute('target', '_blank') anchor.setAttribute('rel', 'noreferrer') } const html = clone.innerHTML.trim() return html || null } const findURLInAttributeValue = (value: unknown): string | null => { const normalized = normalizeUrl(value) if (normalized && isExternalUrl(normalized)) { return normalized } const text = normalize(value) if (!text) { return null } const match = text.match(/https?:\/\/[^\s"'<>\\)]+/i)?.[0] ?? null return match ? normalizeUrl(match) : null } const findURLInElement = (root: Element): string | null => { const nodes = [ root, ...Array.from( root.querySelectorAll( '[href], [data-url], [data-href], [data-link], [data-source-url], [data-target-url], [title], [aria-label]', ), ).slice(0, 40), ] for (const node of nodes) { for (const attribute of Array.from(node.attributes)) { const url = findURLInAttributeValue(attribute.value) if (url) { return url } } } return null } const extractElementMetadata = ( element: Element, ): Pick => ({ title: normalize(element.getAttribute('title')) || queryText( element, "[class*='search-view-card__title'], [class*='source-card__title'], [class*='reference-card__title'], [class*='result-title'], [class*='card__title'], h1, h2, h3, h4", ) || normalize(element instanceof HTMLAnchorElement ? element.textContent || '' : '') || null, text: queryText( element, "[class*='search-view-card__snippet'], [class*='source-card__snippet'], [class*='reference-card__snippet'], [class*='result-snippet'], [class*='card__snippet'], [class*='snippet'], [class*='summary'], p", ) || normalize(element.textContent || '') || null, siteName: normalize(element.getAttribute('data-site-name')) || normalize(element.getAttribute('data-domain')) || normalize(element.getAttribute('data-host')) || queryText( element, "[class*='site'], [class*='domain'], [class*='host'], [class*='origin'], [class*='source-card__source']", ) || normalize(element.getAttribute('aria-label')) || null, }) const findResultRoot = (node: Element): Element => { let current: Element | null = node for (let depth = 0; current && depth < 5; depth += 1) { const className = String((current as HTMLElement).className || '') if ( /(search|result|source|reference).*card|card.*(search|result|source|reference)/i.test( className, ) ) { return current } current = current.parentElement } return node } const addLinksFromRoot = ( root: Element | null, kind: DeepSeekLinkKind, bucket: EvalSourceLink[], seen: Set, ): void => { if (!(root instanceof Element)) { return } const anchors: HTMLAnchorElement[] = root instanceof HTMLAnchorElement && root.matches('a[href]') ? [root] : Array.from(root.querySelectorAll('a[href]')) for (const anchor of anchors) { if (!isVisible(anchor)) { continue } if ( kind !== 'search' && anchor.closest( ".ds-think-content, [class*='think-content'], [class*='reasoning-content']", ) ) { continue } const href = normalizeUrl(anchor.getAttribute('href') || anchor.href || '') if (!href || !isExternalUrl(href)) { continue } const key = linkKey(href, kind) if (seen.has(key)) { continue } const metadata = extractElementMetadata(kind === 'search' ? findResultRoot(anchor) : anchor) seen.add(key) bucket.push({ url: href, title: metadata.title, text: metadata.text, siteName: metadata.siteName, kind, }) } if (kind !== 'search') { return } const candidates = Array.from( root.querySelectorAll( "[class*='search-view-card'], [class*='search-result'], [class*='result-card'], [class*='source-card'], [class*='reference-card'], [data-url], [data-href], [data-link], [data-source-url], [data-target-url]", ), ) for (const candidate of candidates) { if (!(candidate instanceof HTMLElement) || !isVisible(candidate)) { continue } if ( kind !== 'search' && candidate.closest( ".ds-think-content, [class*='think-content'], [class*='reasoning-content']", ) ) { continue } const href = findURLInElement(candidate) if (!href || !isExternalUrl(href)) { continue } const key = linkKey(href, kind) if (seen.has(key)) { continue } const metadata = extractElementMetadata(findResultRoot(candidate)) seen.add(key) bucket.push({ url: href, title: metadata.title, text: metadata.text, siteName: metadata.siteName, kind, }) } } const collectWindowStateLinks = (): EvalSourceLink[] => { const results: EvalSourceLink[] = [] const seen = new Set() const roots: unknown[] = [] const candidates = [ globalWindow.__NEXT_DATA__, globalWindow.__INITIAL_STATE__, globalWindow.__APP_STATE__, globalWindow.__NUXT__, typeof globalWindow.__STORE__?.getState === 'function' ? globalWindow.__STORE__.getState() : null, ] for (const candidate of candidates) { if (candidate && typeof candidate === 'object') { roots.push(candidate) } } const queue: Array<{ value: unknown; path: string }> = roots.map((root) => ({ value: root, path: '', })) const visited = new WeakSet() while (queue.length && results.length < 96) { const current = queue.shift() if (!current) { continue } const { value, path } = current if (!value || typeof value !== 'object') { continue } if (visited.has(value)) { continue } visited.add(value) if (Array.isArray(value)) { for (const item of value.slice(0, 32)) { queue.push({ value: item, path }) } continue } const record = value as EvalRecord const rawUrl = record.url ?? record.href ?? record.link ?? record.uri ?? record.webUrl ?? record.web_url ?? record.sourceUrl ?? record.source_url ?? record.targetUrl ?? record.target_url ?? record.pcUrl ?? record.pc_url ?? record.mobileUrl ?? record.mobile_url const url = normalizeUrl(rawUrl) if (url && isExternalUrl(url)) { const kind: DeepSeekLinkKind = /search|result|reference|citation|source|web|page|card/i.test(path) ? 'search' : 'source' const key = linkKey(url, kind) if (!seen.has(key)) { seen.add(key) results.push({ url, title: normalize( record.title ?? record.name ?? record.label ?? record.summaryTitle ?? record.summary_title, ), text: normalize( record.text ?? record.snippet ?? record.description ?? record.summary ?? record.content, ), siteName: normalize( record.site_name ?? record.siteName ?? record.source ?? record.domain ?? record.host, ), kind, }) } } for (const [key, item] of Object.entries(record).slice(0, 64)) { if (item && typeof item === 'object') { queue.push({ value: item, path: path ? `${path}.${key}` : key, }) } } } return results } const findSearchResultsPanel = (): HTMLElement | null => { const headingPattern = /^(?:搜索结果|search results)$/i const seen = new Set() const candidates: Array<{ node: HTMLElement; score: number }> = [] const addCandidate = (node: Element | null, baseScore: number): void => { let current: HTMLElement | null = node instanceof HTMLElement ? node : null for (let depth = 0; current && depth < 7; depth += 1) { if (seen.has(current) || !isVisible(current)) { current = current.parentElement continue } const rect = current.getBoundingClientRect() if (rect.width < 220 || rect.height < 220 || rect.left < window.innerWidth * 0.45) { current = current.parentElement continue } const style = window.getComputedStyle(current) const headingText = queryText(current, "[role='heading'], h1, h2, h3, h4") const hasHeading = Boolean(headingText && headingPattern.test(headingText)) const hasSearchCards = Boolean( current.querySelector("[class*='search-view-card__title'], .search-view-card__title"), ) if (!hasHeading && !hasSearchCards) { current = current.parentElement continue } let score = baseScore if (hasHeading) { score += 24 } if (hasSearchCards) { score += 24 } if ( ['auto', 'scroll', 'overlay'].includes(style.overflowY) && current.scrollHeight > current.clientHeight + 80 ) { score += 18 } if (rect.left >= window.innerWidth * 0.55) { score += 18 } if (rect.width <= 540) { score += 8 } if (rect.height >= 360) { score += 8 } score += Math.min(current.querySelectorAll('a[href]').length, 12) seen.add(current) candidates.push({ node: current, score }) current = current.parentElement } } for (const heading of Array.from( document.querySelectorAll("[role='heading'], h1, h2, h3, h4"), )) { const text = normalize(heading.textContent || '') if (text && headingPattern.test(text)) { addCandidate(heading, 40) } } for (const titleNode of Array.from( document.querySelectorAll("[class*='search-view-card__title'], .search-view-card__title"), )) { addCandidate(titleNode, 30) } return candidates.sort((left, right) => right.score - left.score).at(0)?.node ?? null } const bodyText = normalize(document.body?.innerText || '') // 登录/验证判定不能直接对整页 innerText 匹配:聊天问题与回答里出现 // "登录/验证码/安全验证" 这类词会把正常会话误判成需要人工处理。 // 只认三类证据:sign_in 路由、可见弹层内的短文案、稀疏拦截页。 const loginPhrasePattern = /(log in|send code|scan with wechat to login|phone number|continue with deepseek|和 deepseek 继续聊|登录|发送验证码|手机号|验证码)/i const challengePhrasePattern = /(人机验证|安全验证|请完成验证|完成下方验证|拖动滑块|滑动验证|向右滑动|安全检测|访问异常|环境异常|verification required|security check|unusual traffic|not a robot|请输入(?:图形)?验证码)/i const findVisibleSurfaceText = ( selectors: string, pattern: RegExp, maxTextLength: number, ): boolean => { for (const node of Array.from(document.querySelectorAll(selectors)).slice(0, 50)) { if (!isVisible(node)) { continue } const text = normalize(node.innerText || node.textContent) if (text && text.length <= maxTextLength && pattern.test(text)) { return true } } return false } const hasChallengeVendorWidget = (() => { const vendorSelectors = [ '[class*="geetest_"]', '[id^="nc_"][id$="_wrapper"]', '.nc-container', '[class*="nocaptcha"]', '[class*="yidun_modal"]', '[class*="yidun_popup"]', '[class*="vaptcha"]', '[class*="tcaptcha"]', '#tcaptcha_iframe', '[class*="captcha_verify"]', '[class*="secsdk-captcha"]', 'iframe[src*="captcha"]', 'iframe[src*="geetest"]', 'iframe[src*="turnstile"]', 'iframe[src*="hcaptcha"]', 'iframe[src*="recaptcha"]', 'iframe[title*="验证"]', ].join(',') for (const node of Array.from(document.querySelectorAll(vendorSelectors)).slice(0, 50)) { if (isVisible(node)) { return true } } return false })() const loginRequired = window.location.pathname.includes('/sign_in') || findVisibleSurfaceText( '[role="dialog"], [aria-modal="true"], [class*="modal"], [class*="login"], [class*="sign"], [class*="auth"]', loginPhrasePattern, 300, ) || Boolean(bodyText && bodyText.length <= 600 && loginPhrasePattern.test(bodyText)) const challengeRequired = hasChallengeVendorWidget || findVisibleSurfaceText( '[role="dialog"], [aria-modal="true"], [class*="captcha"], [id*="captcha"], [class*="verify"], [class*="Verify"]', challengePhrasePattern, 240, ) || Boolean(bodyText && bodyText.length <= 400 && challengePhrasePattern.test(bodyText)) const busyDescriptors: string[] = [] for (const node of Array.from( document.querySelectorAll( "button, [role='button'], [class*='loading'], [class*='spinner'], [class*='think']", ), ).slice(0, 60)) { if (!isVisible(node)) { continue } const text = normalize(node.textContent || node.getAttribute('aria-label') || '') const descriptor = `${node.className || ''} ${text || ''}` if (descriptor) { busyDescriptors.push(descriptor) } if (busyDescriptors.length >= 24) { break } } const editor = document.querySelector('textarea.ds-textarea') || document.querySelector('.ds-textarea textarea') || document.querySelector('textarea') || document.querySelector("[role='textbox'][contenteditable='true']") || document.querySelector("[contenteditable='true']") const composerValue = editor instanceof HTMLTextAreaElement || editor instanceof HTMLInputElement ? normalize(editor.value) : normalize(editor?.textContent || '') const sendButton = Array.from(document.querySelectorAll("button, [role='button']")).find((node) => { if (!isVisible(node)) { return false } const text = normalize(node.textContent || node.getAttribute('aria-label') || '') if (text && /(send|发送|提交)/i.test(text)) { return true } return /sendButton/i.test(String(node.className || '')) }) || null const sendDisabled = sendButton && 'disabled' in sendButton ? Boolean(sendButton.disabled) : null const markdownNodes = Array.from(document.querySelectorAll('.ds-markdown')) .filter((node) => isVisible(node)) .map((node) => { const text = normalize(node.textContent || '') const comparableText = normalizeComparable(text) const html = renderStructuredHtml(node) const rect = node.getBoundingClientRect() const inThink = Boolean(node.closest('.ds-think-content')) const container = node.closest("[data-message-id], [class*='message'], article, li, section") || node.parentElement const links: EvalSourceLink[] = [] const seen = new Set() addLinksFromRoot(node, 'inline', links, seen) const containerLinks: EvalSourceLink[] = [] if (container) { addLinksFromRoot(container, 'source', containerLinks, seen) } return { node, html, text, comparableText, top: Math.round(rect.top), inThink, links, containerLinks, container, } }) const reasoningRoots = Array.from( document.querySelectorAll( ".ds-think-content, [class*='think-content'], [class*='reasoning-content']", ), ) .filter((node) => isVisible(node)) .map((node) => { const text = normalize(node.textContent || '') const links: EvalSourceLink[] = [] const seen = new Set() addLinksFromRoot(node, 'search', links, seen) const rect = node.getBoundingClientRect() return { node, text, links, top: Math.round(rect.top), } }) .filter((candidate) => candidate.text || candidate.links.length) const answerCandidates = markdownNodes.filter((candidate) => { if (candidate.inThink) { return false } if (!candidate.text && !candidate.links.length && !candidate.containerLinks.length) { return false } if (questionText && candidate.comparableText === questionText.toLowerCase()) { return false } return true }) const answerCandidate = answerCandidates.at(-1) || null const thinkCandidates = markdownNodes.filter((candidate) => candidate.inThink && candidate.text) const thinkCandidate = thinkCandidates.at(-1) || null const reasoningRoot = reasoningRoots.at(-1) || null const inlineLinks = answerCandidate?.links ?? [] const sourcePanelLinks = answerCandidate?.containerLinks ?? [] const searchSeen = new Set() const searchPanelLinks: EvalSourceLink[] = [] const addSearchLink = (item: EvalSourceLink): void => { const key = linkKey(item.url, 'search') if (searchSeen.has(key)) { return } searchSeen.add(key) searchPanelLinks.push({ ...item, kind: 'search', }) } const reasoningSearchLinks: EvalSourceLink[] = [] const reasoningSearchSeen = new Set() if (reasoningRoot?.node) { addLinksFromRoot(reasoningRoot.node, 'search', reasoningSearchLinks, reasoningSearchSeen) } if (thinkCandidate?.node) { addLinksFromRoot(thinkCandidate.node, 'search', reasoningSearchLinks, reasoningSearchSeen) } if (thinkCandidate?.container) { addLinksFromRoot( thinkCandidate.container, 'search', reasoningSearchLinks, reasoningSearchSeen, ) } for (const item of reasoningSearchLinks) { addSearchLink(item) } const searchResultsPanel = findSearchResultsPanel() if (searchResultsPanel) { addLinksFromRoot(searchResultsPanel, 'search', searchPanelLinks, searchSeen) } for (const item of collectWindowStateLinks()) { if (item.kind === 'search') { if (!searchResultsPanel && searchPanelLinks.length === 0) { continue } addSearchLink(item) continue } const key = linkKey(item.url, 'source') if (!searchSeen.has(key)) { searchSeen.add(key) sourcePanelLinks.push(item) } } const modelCandidates = Array.from( document.querySelectorAll("button, [role='button'], [class*='model']"), ).flatMap((node) => { if (!isVisible(node)) { return [] } const text = normalize(node.textContent || node.getAttribute('aria-label') || '') const label = normalizeModelLabel(text) if (!label) { return [] } const className = String((node as HTMLElement).className || '') const selected = node.getAttribute('aria-pressed') === 'true' || node.getAttribute('aria-selected') === 'true' || node.getAttribute('data-state') === 'on' || /selected|active|checked|current|on/.test(className) let score = selected ? 30 : 0 if (/r1|reasoner|deepthink|深度思考/i.test(text || '')) { score += 12 } if (/deepseek/i.test(text || '')) { score += 8 } if ( String((node as HTMLElement).className || '') .toLowerCase() .includes('model') ) { score += 6 } return [{ label, score }] }) const currentModelLabel = modelCandidates.sort((left, right) => right.score - left.score).at(0)?.label ?? null const providerRequestID = normalize(document.documentElement.getAttribute('data-request-id')) || normalize(document.body?.getAttribute('data-request-id')) || null const requestID = normalize(document.documentElement.getAttribute('data-message-id')) || normalize(document.body?.getAttribute('data-message-id')) || providerRequestID || null const signatureParts = [ normalize(answerCandidate?.text), normalize(thinkCandidate?.text ?? reasoningRoot?.text), ...inlineLinks.map((item) => item.url), ...sourcePanelLinks.map((item) => item.url), ...searchPanelLinks.map((item) => item.url), ].filter(Boolean) return { url: window.location.href, title: normalize(document.title), loginRequired, loginReason: loginRequired ? normalize(bodyText) || 'login_required' : null, challengeRequired, challengeReason: challengeRequired ? normalize(bodyText) || 'challenge_required' : null, bodyText, busyDescriptors, composerValue, sendDisabled, answer: answerCandidate?.html ?? answerCandidate?.text ?? null, answerText: answerCandidate?.text ?? null, reasoning: thinkCandidate?.text ?? reasoningRoot?.text ?? null, signatureParts, currentModelLabel, providerRequestID, requestID, inlineLinks, sourcePanelLinks, searchPanelLinks, } }, questionText ?? '')) as DeepSeekPageSnapshotDraft const busySignals = detectDeepSeekBusySignals({ bodyText: draft.bodyText, descriptors: draft.busyDescriptors, }) const { bodyText: _bodyText, busyDescriptors: _busyDescriptors, signatureParts, ...rest } = draft return { ...rest, busy: busySignals.length > 0, busySignals, signature: [...signatureParts, String(busySignals.length)].join('|') || null, } } function selectBetterSnapshot( current: DeepSeekPageSnapshot, candidate: DeepSeekPageSnapshot, ): DeepSeekPageSnapshot { if (answerScore(candidate) > answerScore(current)) { return candidate } if (isObservationComplete(candidate) && !isObservationComplete(current)) { return candidate } return current } async function waitForDeepSeekAnswer( page: PlaywrightPage, questionText: string, signal: AbortSignal, ): Promise { const startedAt = Date.now() let stablePollCount = 0 let previousSignature: string | null = null let bestSnapshot = await readDeepSeekPageSnapshot(page, questionText) while (Date.now() - startedAt < DEEPSEEK_QUERY_TIMEOUT_MS) { if (signal.aborted) { throw new Error('adapter_aborted') } const snapshot = await readDeepSeekPageSnapshot(page, questionText) bestSnapshot = selectBetterSnapshot(bestSnapshot, snapshot) if (snapshot.loginRequired) { return { ok: false, error: 'deepseek_login_required', detail: snapshot.loginReason ?? 'login_required', finalSnapshot: bestSnapshot, stablePollCount, elapsedMs: Date.now() - startedAt, } } if (snapshot.challengeRequired && !snapshot.answer) { return { ok: false, error: 'deepseek_challenge_required', detail: snapshot.challengeReason ?? 'challenge_required', finalSnapshot: bestSnapshot, stablePollCount, elapsedMs: Date.now() - startedAt, } } if (isObservationComplete(snapshot)) { if (snapshot.signature && snapshot.signature === previousSignature) { stablePollCount += 1 } else { stablePollCount = 1 } previousSignature = snapshot.signature if (stablePollCount >= DEEPSEEK_STABLE_POLLS_REQUIRED) { return { ok: true, finalSnapshot: snapshot, stablePollCount, elapsedMs: Date.now() - startedAt, } } } else { stablePollCount = 0 previousSignature = snapshot.signature } await sleep(DEEPSEEK_QUERY_POLL_INTERVAL_MS, signal) } return { ok: false, error: 'deepseek_query_timeout', detail: 'timed out while waiting for DeepSeek answer', finalSnapshot: bestSnapshot, stablePollCount, elapsedMs: Date.now() - startedAt, } } export const deepseekAdapter: MonitorAdapter = { provider: 'deepseek', executionMode: 'playwright', async query(context, payload) { if (!context.playwright?.page) { return { status: 'failed', summary: 'DeepSeek 监测缺少 Playwright 页面上下文。', error: buildAdapterError('deepseek_playwright_required', 'deepseek_playwright_required'), } } let questionText: string try { questionText = extractQuestionText(payload) } catch (error) { return { status: 'failed', summary: 'DeepSeek 监测缺少问题文本,无法执行。', error: buildAdapterError( 'deepseek_question_text_missing', error instanceof Error ? error.message : 'deepseek_question_text_missing', ), } } const page = context.playwright.page context.reportProgress('deepseek.page_ready') await ensurePageOnDeepSeekChat(page, context.signal) const initialSnapshot = await readDeepSeekPageSnapshot(page, questionText) if (initialSnapshot.loginRequired) { return { status: 'failed', summary: 'DeepSeek 账号未登录或登录态已失效,请先在 desktop client 中重新授权。', error: buildAdapterError( 'deepseek_login_required', initialSnapshot.loginReason ?? 'login_required', { page_url: initialSnapshot.url, }, ), } } if (initialSnapshot.challengeRequired) { return { status: 'failed', summary: 'DeepSeek 当前需要人工验证,相关监控任务已暂停。', error: buildAdapterError( 'deepseek_challenge_required', initialSnapshot.challengeReason ?? 'challenge_required', { page_url: initialSnapshot.url, }, ), } } context.reportProgress('deepseek.query') try { await submitDeepSeekQuestion(page, questionText, context.signal) } catch (error) { const message = error instanceof Error ? error.message : String(error) return { status: 'failed', summary: `DeepSeek 提问失败:${message}`, error: buildAdapterError('deepseek_submit_failed', message, { page_url: safePageURL(page), }), } } context.reportProgress('deepseek.wait_answer') const waitResult = await waitForDeepSeekAnswer(page, questionText, context.signal) context.reportProgress('deepseek.reveal_sources') await maybeExpandDeepSeekReasoning(page).catch(() => undefined) await maybeRevealDeepSeekSources(page).catch(() => undefined) let revealedSearchPanelLinks = await collectDeepSeekSearchResultsPanelLinks(page).catch( () => [], ) if (revealedSearchPanelLinks.length === 0) { await maybeRevealDeepSeekReasoningSearchSources(page).catch(() => undefined) revealedSearchPanelLinks = await collectDeepSeekSearchResultsPanelLinks(page).catch(() => []) } const finalSnapshot = await readDeepSeekPageSnapshot(page, questionText).catch( () => waitResult.finalSnapshot, ) const bestSnapshot = selectBetterSnapshot(waitResult.finalSnapshot, finalSnapshot) const citationPanelLinks = resolveDeepSeekCitationPanelLinks( bestSnapshot.searchPanelLinks, revealedSearchPanelLinks, ) const mergedSnapshot: DeepSeekPageSnapshot = { ...bestSnapshot, searchPanelLinks: citationPanelLinks, } const classifiedSources = classifyDeepseekSources({ inlineLinks: mergedSnapshot.inlineLinks, sourcePanelLinks: mergedSnapshot.sourcePanelLinks, searchPanelLinks: mergedSnapshot.searchPanelLinks, }) if (isDeepSeekWaitFailure(waitResult)) { const detail = waitResult.detail ?? waitResult.error if (waitResult.error === 'deepseek_login_required') { return { status: 'failed', summary: 'DeepSeek 账号未登录或登录态已失效,请先在 desktop client 中重新授权。', error: buildAdapterError('deepseek_login_required', detail, { page_url: mergedSnapshot.url, }), } } if (waitResult.error === 'deepseek_challenge_required') { return { status: 'failed', summary: 'DeepSeek 当前需要人工验证,相关监控任务已暂停。', error: buildAdapterError('deepseek_challenge_required', detail, { page_url: mergedSnapshot.url, }), } } return { status: 'unknown', summary: 'DeepSeek 回答等待超时,已回写 unknown 等待后续对账。', error: buildAdapterError(waitResult.error, detail, { page_url: mergedSnapshot.url, }), payload: buildDeepSeekPayload(mergedSnapshot, classifiedSources, mergedSnapshot.answer), } } const answer = normalizeText(mergedSnapshot.answer) if (!answer) { return { status: 'unknown', summary: 'DeepSeek 未返回可用答案,已回写 unknown 等待后续对账。', error: buildAdapterError('deepseek_empty_answer', 'deepseek returned no final answer'), payload: buildDeepSeekPayload(mergedSnapshot, classifiedSources, mergedSnapshot.answer), } } return { status: 'succeeded', summary: 'DeepSeek 监控任务执行成功。', payload: buildDeepSeekPayload(mergedSnapshot, classifiedSources, answer), } }, } export const __deepseekTestUtils = { extractQuestionText, buildSourceItem, buildDeepSeekPayload, detectDeepSeekBusySignals, dedupeSourceItems, classifyDeepseekSources, mergeDeepSeekRawSourceLinks, resolveDeepSeekCitationPanelLinks, isObservationComplete, isDeepSeekToggleSelected, isDeepSeekWebpagesTriggerText, isDeepSeekReasoningSearchTriggerText, }