Files
geo/apps/desktop-client/src/main/adapters/deepseek.ts
T
root abacc1f421 fix(desktop-client): stop false manual-verification states and auto-revive probes after sleep/offline
Challenge detection previously regex-matched the entire page innerText, so
chat transcripts or console copy containing words like 验证码/安全验证/verify
flagged healthy accounts as challenge_required. Detection is now scoped to
real evidence only (visible captcha vendor widgets, short modal copy, sparse
interstitial pages) via a shared challenge-signals module used by the generic
AI probe, doubao/kimi/qwen auth rules, and the deepseek runtime snapshot.
Platform API error codes (e.g. baijiahao_challenge_required) keep working.

Account health also could not recover after lid-close sleep or a network
drop: offline probes misread cached console pages as expired, and expired
records null out nextProbeAt so they were never re-probed. Probing is now
skipped while offline (network_error backoff instead), and powerMonitor
resume/unlock plus an offline-to-online transition watch schedule jittered
recovery probes for all tracked accounts, including expired ones. Challenge
rechecks wait 8s so transient widgets during page load no longer stick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:06:51 +08:00

2969 lines
93 KiB
TypeScript

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<DeepSeekWaitResult, { ok: false }> {
return !result.ok
}
function isRecord(value: unknown): value is Record<string, unknown> {
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 | null | undefined>
}): 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<DeepSeekRawSourceLink | MonitoringSourceItem>,
): MonitoringSourceItem[] {
const keyed = new Map<string, MonitoringSourceItem>()
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<string, DeepSeekRawSourceLink>()
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<DeepSeekRawSourceLink | MonitoringSourceItem>
sourcePanelLinks: Array<DeepSeekRawSourceLink | MonitoringSourceItem>
searchPanelLinks: Array<DeepSeekRawSourceLink | MonitoringSourceItem>
}): 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, unknown>): 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<string, JsonValue> = {},
): Record<string, JsonValue> {
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<string, JsonValue> = {}
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<string, JsonValue> {
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<string, JsonValue> {
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<void> {
if (signal?.aborted) {
throw new Error('adapter_aborted')
}
await new Promise<void>((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<void> {
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<PlaywrightPage['locator']>
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<PlaywrightPage['locator']>,
): Promise<boolean> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<boolean> {
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<boolean> {
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<DeepSeekRawSourceLink[]> {
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<void>((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<EvalSourceLink, 'title' | 'text' | 'siteName'> => ({
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<HTMLElement>()
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<string>()
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<HTMLAnchorElement>('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<DeepSeekPageSnapshot> {
const draft = (await page.evaluate((input) => {
type EvalRecord = Record<string, unknown>
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<HTMLAnchorElement>('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<EvalSourceLink, 'title' | 'text' | 'siteName'> => ({
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<string>,
): 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<string>()
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<object>()
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<HTMLElement>()
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<string>()
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<string>()
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<string>()
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<string>()
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<DeepSeekWaitResult> {
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,
}