fix: preserve monitor unknown and adapt doubao references
Desktop Client Build / Resolve Build Metadata (push) Successful in 32s
Backend CI / Backend (push) Successful in 15m37s
Desktop Client Build / Build Desktop Client (push) Successful in 22m53s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 30s
Desktop Client Build / Resolve Build Metadata (push) Successful in 32s
Backend CI / Backend (push) Successful in 15m37s
Desktop Client Build / Build Desktop Client (push) Successful in 22m53s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 30s
This commit is contained in:
@@ -5,7 +5,9 @@ import { __doubaoTestUtils } from './doubao'
|
|||||||
const {
|
const {
|
||||||
buildSourceItem,
|
buildSourceItem,
|
||||||
dedupeSourceItems,
|
dedupeSourceItems,
|
||||||
|
doubaoModeKindFromLabelText,
|
||||||
extractQuestionText,
|
extractQuestionText,
|
||||||
|
isDoubaoRecoverablePageError,
|
||||||
isNonCitationAssetDomain,
|
isNonCitationAssetDomain,
|
||||||
isNonCitationAssetUrl,
|
isNonCitationAssetUrl,
|
||||||
parseDoubaoStreamBody,
|
parseDoubaoStreamBody,
|
||||||
@@ -20,6 +22,19 @@ describe('doubao adapter helpers', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('treats query timeouts as recoverable page failures', () => {
|
||||||
|
expect(isDoubaoRecoverablePageError('doubao_query_timeout')).toBe(true)
|
||||||
|
expect(isDoubaoRecoverablePageError('doubao_page_navigation_interrupted')).toBe(true)
|
||||||
|
expect(isDoubaoRecoverablePageError('doubao_login_expired')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('maps the new expert/deep-thinking mode label to expert mode', () => {
|
||||||
|
expect(doubaoModeKindFromLabelText('专家')).toBe('expert')
|
||||||
|
expect(doubaoModeKindFromLabelText('专家 深度思考 研究级智能模型')).toBe('expert')
|
||||||
|
expect(doubaoModeKindFromLabelText('思考')).toBe('thinking')
|
||||||
|
expect(doubaoModeKindFromLabelText('快速')).toBe('fast')
|
||||||
|
})
|
||||||
|
|
||||||
it('filters byteimg and bytednsdoc asset CDN links from source items', () => {
|
it('filters byteimg and bytednsdoc asset CDN links from source items', () => {
|
||||||
expect(isNonCitationAssetDomain('p3-spider-image-sign.byteimg.com')).toBe(true)
|
expect(isNonCitationAssetDomain('p3-spider-image-sign.byteimg.com')).toBe(true)
|
||||||
expect(isNonCitationAssetDomain('lf3-static.bytednsdoc.com')).toBe(true)
|
expect(isNonCitationAssetDomain('lf3-static.bytednsdoc.com')).toBe(true)
|
||||||
@@ -99,6 +114,22 @@ describe('doubao adapter helpers', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('extracts source URLs embedded in Doubao thinking-panel text', () => {
|
||||||
|
const item = buildSourceItem({
|
||||||
|
url: '1. 合肥全屋定制榜单 https://www.doubao.com/link?target=https%3A%2F%2Fexample.com%2Fsource%3Ffrom%3Dthink#card',
|
||||||
|
title: '思考链路里的来源',
|
||||||
|
site_name: '示例站点',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(item).toMatchObject({
|
||||||
|
url: 'https://example.com/source?from=think',
|
||||||
|
normalized_url: 'https://example.com/source?from=think',
|
||||||
|
host: 'example.com',
|
||||||
|
title: '思考链路里的来源',
|
||||||
|
site_name: '示例站点',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('parses Doubao answer deltas and source cards from the completion SSE stream', () => {
|
it('parses Doubao answer deltas and source cards from the completion SSE stream', () => {
|
||||||
const sourceChunkPayload = {
|
const sourceChunkPayload = {
|
||||||
patch_op: [
|
patch_op: [
|
||||||
|
|||||||
@@ -491,13 +491,27 @@ function looksLikeSourceUrl(value: string | null): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/^https?:\/\//i.test(normalized) || /^\/\//.test(normalized)) {
|
if (/https?:\/\//i.test(normalized) || /^\/\//.test(normalized)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?:[/?#].*)?$/i.test(normalized)
|
return /^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?:[/?#].*)?$/i.test(normalized)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractEmbeddedHttpUrl(value: string | null): string | null {
|
||||||
|
const input = normalizeText(value)
|
||||||
|
if (!input) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = input.match(/https?:\/\/[^\s"'<>\\]+/i)?.[0] ?? null
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return match.replace(/[),,。;;、]+$/u, '')
|
||||||
|
}
|
||||||
|
|
||||||
function extractDoubaoEmbeddedSourceUrl(url: URL, depth = 0): string | null {
|
function extractDoubaoEmbeddedSourceUrl(url: URL, depth = 0): string | null {
|
||||||
if (depth > 3) {
|
if (depth > 3) {
|
||||||
return null
|
return null
|
||||||
@@ -550,7 +564,8 @@ function normalizeSourceUrl(value: string | null, depth = 0): string | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const candidates = [input]
|
const embeddedURL = extractEmbeddedHttpUrl(input)
|
||||||
|
const candidates = embeddedURL && embeddedURL !== input ? [embeddedURL, input] : [input]
|
||||||
try {
|
try {
|
||||||
const decoded = decodeURIComponent(input)
|
const decoded = decodeURIComponent(input)
|
||||||
if (decoded && decoded !== input) {
|
if (decoded && decoded !== input) {
|
||||||
@@ -1313,12 +1328,7 @@ async function ensurePageOnDoubaoChat(page: PlaywrightPage, signal: AbortSignal)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentURL = safePageURL(page)
|
const currentURL = safePageURL(page)
|
||||||
if (!currentURL.startsWith('https://www.doubao.com/')) {
|
if (currentURL !== DOUBAO_BOOTSTRAP_URL) {
|
||||||
await page.goto(DOUBAO_BOOTSTRAP_URL, {
|
|
||||||
waitUntil: 'domcontentloaded',
|
|
||||||
timeout: DOUBAO_PAGE_READY_TIMEOUT_MS,
|
|
||||||
})
|
|
||||||
} else if (!currentURL.startsWith('https://www.doubao.com/chat')) {
|
|
||||||
await page.goto(DOUBAO_BOOTSTRAP_URL, {
|
await page.goto(DOUBAO_BOOTSTRAP_URL, {
|
||||||
waitUntil: 'domcontentloaded',
|
waitUntil: 'domcontentloaded',
|
||||||
timeout: DOUBAO_PAGE_READY_TIMEOUT_MS,
|
timeout: DOUBAO_PAGE_READY_TIMEOUT_MS,
|
||||||
@@ -1337,6 +1347,26 @@ async function ensurePageOnDoubaoChat(page: PlaywrightPage, signal: AbortSignal)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resetDoubaoPageAfterRecoverableFailure(
|
||||||
|
page: PlaywrightPage,
|
||||||
|
error: string,
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!isDoubaoRecoverablePageError(error) || signal.aborted) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto(DOUBAO_BOOTSTRAP_URL, {
|
||||||
|
waitUntil: 'domcontentloaded',
|
||||||
|
timeout: DOUBAO_PAGE_READY_TIMEOUT_MS,
|
||||||
|
})
|
||||||
|
await page.waitForLoadState('networkidle', { timeout: 3_000 }).catch(() => undefined)
|
||||||
|
} catch {
|
||||||
|
// Best effort: timeout recovery must not mask the original unknown result.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function attachDoubaoResponseCapture(page: PlaywrightPage): {
|
function attachDoubaoResponseCapture(page: PlaywrightPage): {
|
||||||
captures: DoubaoCaptureRecord[]
|
captures: DoubaoCaptureRecord[]
|
||||||
flush: (timeoutMs: number) => Promise<void>
|
flush: (timeoutMs: number) => Promise<void>
|
||||||
@@ -1483,7 +1513,16 @@ function doubaoModeProviderModel(mode: DoubaoAnswerModeKind | null): string | nu
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDoubaoModeLabel(value: string | null): DoubaoAnswerModeKind | null {
|
function normalizeDoubaoModeLabel(value: string | null): DoubaoAnswerModeKind | null {
|
||||||
switch (normalizeText(value)) {
|
return doubaoModeKindFromLabelText(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDoubaoThinkingMode(mode: DoubaoAnswerModeKind | null): boolean {
|
||||||
|
return mode === 'thinking' || mode === 'expert'
|
||||||
|
}
|
||||||
|
|
||||||
|
function doubaoModeKindFromLabelText(value: string | null): DoubaoAnswerModeKind | null {
|
||||||
|
const label = normalizeText(value)
|
||||||
|
switch (label) {
|
||||||
case '快速':
|
case '快速':
|
||||||
return 'fast'
|
return 'fast'
|
||||||
case '思考':
|
case '思考':
|
||||||
@@ -1491,6 +1530,9 @@ function normalizeDoubaoModeLabel(value: string | null): DoubaoAnswerModeKind |
|
|||||||
case '专家':
|
case '专家':
|
||||||
return 'expert'
|
return 'expert'
|
||||||
default:
|
default:
|
||||||
|
if (label?.includes('深度思考') || label?.includes('研究级智能模型')) {
|
||||||
|
return 'expert'
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1505,7 +1547,7 @@ function resolveProviderModel(pageResult: DoubaoPageQueryResult): string {
|
|||||||
return label
|
return label
|
||||||
}
|
}
|
||||||
if (pageResult.thinkingModeApplied) {
|
if (pageResult.thinkingModeApplied) {
|
||||||
return 'doubao-web-thinking'
|
return pageResult.modeLabel === '专家' ? 'doubao-web-expert' : 'doubao-web-thinking'
|
||||||
}
|
}
|
||||||
return pageResult.deepThinkEnabled ? 'doubao-web-thinking' : 'doubao-web'
|
return pageResult.deepThinkEnabled ? 'doubao-web-thinking' : 'doubao-web'
|
||||||
}
|
}
|
||||||
@@ -1807,13 +1849,22 @@ const doubaoQueryInPage = async (
|
|||||||
if (label === ANSWER_MODE_LABELS.expert) {
|
if (label === ANSWER_MODE_LABELS.expert) {
|
||||||
return 'expert'
|
return 'expert'
|
||||||
}
|
}
|
||||||
|
if (label.includes('专家')) {
|
||||||
|
return 'expert'
|
||||||
|
}
|
||||||
|
if (label.includes('思考') || label.includes('深度思考')) {
|
||||||
|
return 'thinking'
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isThinkingAnswerMode = (mode: AnswerModeKind | null): boolean =>
|
||||||
|
mode === 'thinking' || mode === 'expert'
|
||||||
|
|
||||||
const classifyAnswerModeText = (value: unknown): AnswerModeKind | null => {
|
const classifyAnswerModeText = (value: unknown): AnswerModeKind | null => {
|
||||||
const multiline = normalizeMultiline(typeof value === 'string' ? value : null)
|
const multiline = normalizeMultiline(typeof value === 'string' ? value : null)
|
||||||
const normalized = multiline ?? normalize(value)
|
const normalized = multiline ?? normalize(value)
|
||||||
if (!normalized || /深度思考|正在思考|思考中/.test(normalized)) {
|
if (!normalized || /正在思考|思考中/.test(normalized)) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1845,7 +1896,7 @@ const doubaoQueryInPage = async (
|
|||||||
if (compact.startsWith(label)) {
|
if (compact.startsWith(label)) {
|
||||||
return answerModeKindFromLabel(label)
|
return answerModeKindFromLabel(label)
|
||||||
}
|
}
|
||||||
return null
|
return answerModeKindFromLabel(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
const readElementAnswerMode = (element: Element | null | undefined): AnswerModeSnapshot => {
|
const readElementAnswerMode = (element: Element | null | undefined): AnswerModeSnapshot => {
|
||||||
@@ -2049,7 +2100,7 @@ const doubaoQueryInPage = async (
|
|||||||
): Promise<AnswerModeSnapshot & { applied: boolean | null }> => {
|
): Promise<AnswerModeSnapshot & { applied: boolean | null }> => {
|
||||||
const initialTrigger = findAnswerModeTrigger(shell, composer)
|
const initialTrigger = findAnswerModeTrigger(shell, composer)
|
||||||
const initialMode = readElementAnswerMode(initialTrigger)
|
const initialMode = readElementAnswerMode(initialTrigger)
|
||||||
if (initialMode.kind === 'thinking') {
|
if (isThinkingAnswerMode(initialMode.kind)) {
|
||||||
return { ...initialMode, applied: true }
|
return { ...initialMode, applied: true }
|
||||||
}
|
}
|
||||||
if (!initialTrigger) {
|
if (!initialTrigger) {
|
||||||
@@ -2059,12 +2110,14 @@ const doubaoQueryInPage = async (
|
|||||||
clickModeElement(initialTrigger)
|
clickModeElement(initialTrigger)
|
||||||
await wait(260)
|
await wait(260)
|
||||||
|
|
||||||
const thinkingOption = findAnswerModeOption('thinking', initialTrigger)
|
const thinkingOption =
|
||||||
|
findAnswerModeOption('expert', initialTrigger) ??
|
||||||
|
findAnswerModeOption('thinking', initialTrigger)
|
||||||
if (!thinkingOption) {
|
if (!thinkingOption) {
|
||||||
pressEscape()
|
pressEscape()
|
||||||
await wait(120)
|
await wait(120)
|
||||||
const current = readCurrentAnswerMode(shell, composer)
|
const current = readCurrentAnswerMode(shell, composer)
|
||||||
return { ...current, applied: current.kind === 'thinking' ? true : null }
|
return { ...current, applied: isThinkingAnswerMode(current.kind) ? true : null }
|
||||||
}
|
}
|
||||||
|
|
||||||
clickModeElement(thinkingOption)
|
clickModeElement(thinkingOption)
|
||||||
@@ -2073,7 +2126,7 @@ const doubaoQueryInPage = async (
|
|||||||
const current = readCurrentAnswerMode(shell, composer)
|
const current = readCurrentAnswerMode(shell, composer)
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
applied: current.kind === 'thinking' ? true : null,
|
applied: isThinkingAnswerMode(current.kind) ? true : null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2442,25 +2495,76 @@ const doubaoQueryInPage = async (
|
|||||||
}
|
}
|
||||||
const links: DoubaoDomLink[] = []
|
const links: DoubaoDomLink[] = []
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
for (const node of Array.from(scope.querySelectorAll('a[href]'))) {
|
const pushLink = (
|
||||||
if (!(node instanceof HTMLAnchorElement) || !isVisible(node)) {
|
hrefCandidate: string | null | undefined,
|
||||||
continue
|
element: HTMLElement,
|
||||||
}
|
fallbackText: string | null,
|
||||||
const href = normalizeHref(node.getAttribute('href') ?? node.href)
|
): void => {
|
||||||
|
const href = normalizeHref(hrefCandidate ?? null)
|
||||||
if (!href || !isExternalHref(href) || seen.has(href)) {
|
if (!href || !isExternalHref(href) || seen.has(href)) {
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
seen.add(href)
|
seen.add(href)
|
||||||
const visibleText = normalize(node.innerText) ?? normalize(node.textContent)
|
const visibleText = normalize(element.innerText) ?? normalize(element.textContent) ?? fallbackText
|
||||||
links.push({
|
links.push({
|
||||||
url: href,
|
url: href,
|
||||||
title: normalize(node.title) ?? normalize(node.getAttribute('aria-label')) ?? visibleText,
|
title:
|
||||||
|
normalize(element.getAttribute('title')) ??
|
||||||
|
normalize(element.getAttribute('aria-label')) ??
|
||||||
|
visibleText,
|
||||||
text: visibleText,
|
text: visibleText,
|
||||||
siteName:
|
siteName:
|
||||||
normalize(node.getAttribute('data-site-name')) ??
|
normalize(element.getAttribute('data-site-name')) ??
|
||||||
normalize(node.dataset.siteName ?? null),
|
normalize(element.dataset.siteName ?? null) ??
|
||||||
|
normalize(element.getAttribute('data-source-name')) ??
|
||||||
|
normalize(element.dataset.sourceName ?? null),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selector = [
|
||||||
|
'a[href]',
|
||||||
|
'[data-url]',
|
||||||
|
'[data-href]',
|
||||||
|
'[data-link]',
|
||||||
|
'[data-target-url]',
|
||||||
|
'[data-targetUrl]',
|
||||||
|
'[data-source-url]',
|
||||||
|
'[data-sourceUrl]',
|
||||||
|
'[data-jump-url]',
|
||||||
|
'[data-jumpUrl]',
|
||||||
|
'[aria-label*="http"]',
|
||||||
|
'[title*="http"]',
|
||||||
|
].join(',')
|
||||||
|
|
||||||
|
for (const node of Array.from(scope.querySelectorAll(selector))) {
|
||||||
|
if (!(node instanceof HTMLElement) || !isVisible(node)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
node.getAttribute('href'),
|
||||||
|
node.getAttribute('data-url'),
|
||||||
|
node.getAttribute('data-href'),
|
||||||
|
node.getAttribute('data-link'),
|
||||||
|
node.getAttribute('data-target-url'),
|
||||||
|
node.getAttribute('data-targetUrl'),
|
||||||
|
node.getAttribute('data-source-url'),
|
||||||
|
node.getAttribute('data-sourceUrl'),
|
||||||
|
node.getAttribute('data-jump-url'),
|
||||||
|
node.getAttribute('data-jumpUrl'),
|
||||||
|
node.getAttribute('aria-label'),
|
||||||
|
node.getAttribute('title'),
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
pushLink(candidate, node, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = normalizeMultiline(scope.innerText)
|
||||||
|
for (const match of text?.matchAll(/https?:\/\/[^\s"'<>\\]+/gi) ?? []) {
|
||||||
|
pushLink(match[0], scope, null)
|
||||||
|
}
|
||||||
return links
|
return links
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2535,6 +2639,21 @@ const doubaoQueryInPage = async (
|
|||||||
|
|
||||||
const normalizedQuestion = normalizeMultiline(questionText)
|
const normalizedQuestion = normalizeMultiline(questionText)
|
||||||
|
|
||||||
|
const isReferenceHeaderLine = (line: string): boolean =>
|
||||||
|
/搜索\s*\d+\s*个关键词|参考\s*\d+\s*篇资料|查看\d+篇资料|参考资料|资料来源/.test(line)
|
||||||
|
|
||||||
|
const isReferenceQueryLine = (line: string): boolean =>
|
||||||
|
/[“"「].+[”"」]/.test(line) ||
|
||||||
|
/、/.test(line) ||
|
||||||
|
/关键词|搜索词|查询词/.test(line)
|
||||||
|
|
||||||
|
const isReferenceItemLine = (line: string): boolean =>
|
||||||
|
/^\d+[.、]\s*\S+/.test(line) &&
|
||||||
|
(/https?:\/\//i.test(line) ||
|
||||||
|
/新闻|新浪|搜狐|网易|腾讯|百度|知乎|什么值得买|列举网|装修网|家居|资料|来源|推荐|排名|榜|厂家|品牌|公司|工厂|门店/.test(
|
||||||
|
line,
|
||||||
|
))
|
||||||
|
|
||||||
// Tokens that only appear on Doubao's empty-chat surface (prompt templates, nav, feature chips).
|
// Tokens that only appear on Doubao's empty-chat surface (prompt templates, nav, feature chips).
|
||||||
const EMPTY_CHAT_TOKENS = [
|
const EMPTY_CHAT_TOKENS = [
|
||||||
'快速',
|
'快速',
|
||||||
@@ -2580,7 +2699,30 @@ const doubaoQueryInPage = async (
|
|||||||
.split(/\n+/)
|
.split(/\n+/)
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
let inReferenceBlock = false
|
||||||
|
let referenceIntroBudget = 0
|
||||||
const kept = lines.filter((line) => {
|
const kept = lines.filter((line) => {
|
||||||
|
if (/^-{4,}$/.test(line.replace(/\s+/g, ''))) {
|
||||||
|
inReferenceBlock = false
|
||||||
|
referenceIntroBudget = 0
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (isReferenceHeaderLine(line)) {
|
||||||
|
inReferenceBlock = true
|
||||||
|
referenceIntroBudget = 1
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (inReferenceBlock) {
|
||||||
|
if (referenceIntroBudget > 0 && isReferenceQueryLine(line)) {
|
||||||
|
referenceIntroBudget -= 1
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
referenceIntroBudget = 0
|
||||||
|
if (isReferenceItemLine(line)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
inReferenceBlock = false
|
||||||
|
}
|
||||||
if (EMPTY_CHAT_TOKENS.includes(line)) {
|
if (EMPTY_CHAT_TOKENS.includes(line)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -2632,11 +2774,31 @@ const doubaoQueryInPage = async (
|
|||||||
return best?.element ?? null
|
return best?.element ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const collectReferencePanelLinks = (): DoubaoDomLink[] => {
|
||||||
|
const groups: DoubaoDomLink[][] = []
|
||||||
|
const nodes = Array.from(document.querySelectorAll('article, section, div, main, li'))
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (!(node instanceof HTMLElement) || !isVisible(node)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const text = normalizeMultiline(node.innerText)
|
||||||
|
if (!text || !isReferenceHeaderLine(text)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const links = collectLinks(node)
|
||||||
|
if (links.length > 0) {
|
||||||
|
groups.push(links)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mergeLinks(...groups)
|
||||||
|
}
|
||||||
|
|
||||||
const snapshotConversation = (composerTop: number | null): ConversationSnapshot => {
|
const snapshotConversation = (composerTop: number | null): ConversationSnapshot => {
|
||||||
expandReferenceControls()
|
expandReferenceControls()
|
||||||
|
|
||||||
const questionAnchor = findQuestionAnchor()
|
const questionAnchor = findQuestionAnchor()
|
||||||
const anchorRect = questionAnchor?.getBoundingClientRect() ?? null
|
const anchorRect = questionAnchor?.getBoundingClientRect() ?? null
|
||||||
|
const referencePanelLinks = collectReferencePanelLinks()
|
||||||
|
|
||||||
const candidates: Array<{
|
const candidates: Array<{
|
||||||
element: HTMLElement
|
element: HTMLElement
|
||||||
@@ -2708,6 +2870,9 @@ const doubaoQueryInPage = async (
|
|||||||
if (questionAnchor && rect.top > (anchorRect?.bottom ?? 0)) {
|
if (questionAnchor && rect.top > (anchorRect?.bottom ?? 0)) {
|
||||||
score += 1_000
|
score += 1_000
|
||||||
}
|
}
|
||||||
|
if (rawText && isReferenceHeaderLine(rawText)) {
|
||||||
|
score -= 1_200
|
||||||
|
}
|
||||||
|
|
||||||
if (score <= 0) {
|
if (score <= 0) {
|
||||||
continue
|
continue
|
||||||
@@ -2801,6 +2966,7 @@ const doubaoQueryInPage = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scopedReferenceLinks = mergeLinks(
|
const scopedReferenceLinks = mergeLinks(
|
||||||
|
referencePanelLinks,
|
||||||
...orderedCandidates
|
...orderedCandidates
|
||||||
.filter((candidate) => {
|
.filter((candidate) => {
|
||||||
if (!candidate.text) {
|
if (!candidate.text) {
|
||||||
@@ -2856,7 +3022,7 @@ const doubaoQueryInPage = async (
|
|||||||
title: normalize(document.title),
|
title: normalize(document.title),
|
||||||
modelLabel: resolveModelLabel(),
|
modelLabel: resolveModelLabel(),
|
||||||
modeLabel,
|
modeLabel,
|
||||||
thinkingModeApplied: modeKind === 'thinking' ? true : (modeSnapshot.applied ?? null),
|
thinkingModeApplied: isThinkingAnswerMode(modeKind) ? true : (modeSnapshot.applied ?? null),
|
||||||
deepThinkEnabled: toggleState(deepThinkButton),
|
deepThinkEnabled: toggleState(deepThinkButton),
|
||||||
domAnswer: snapshot.answerText,
|
domAnswer: snapshot.answerText,
|
||||||
domReasoning: snapshot.reasoningText,
|
domReasoning: snapshot.reasoningText,
|
||||||
@@ -2973,7 +3139,9 @@ const doubaoQueryInPage = async (
|
|||||||
answerMode = {
|
answerMode = {
|
||||||
kind: currentMode.kind ?? answerMode.kind,
|
kind: currentMode.kind ?? answerMode.kind,
|
||||||
label: currentMode.label ?? answerMode.label,
|
label: currentMode.label ?? answerMode.label,
|
||||||
applied: (currentMode.kind ?? answerMode.kind) === 'thinking' ? true : answerMode.applied,
|
applied: isThinkingAnswerMode(currentMode.kind ?? answerMode.kind)
|
||||||
|
? true
|
||||||
|
: answerMode.applied,
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -3077,6 +3245,10 @@ export const doubaoAdapter: MonitorAdapter = {
|
|||||||
const hasRecoveredContent = hasRecoveredAnswer || searchResults.length > 0
|
const hasRecoveredContent = hasRecoveredAnswer || searchResults.length > 0
|
||||||
const retryableStreamError = isDoubaoRetryableErrorMessage(streamSummary.error_message)
|
const retryableStreamError = isDoubaoRetryableErrorMessage(streamSummary.error_message)
|
||||||
|
|
||||||
|
if (pageResult.ok === false && pageRecoverableError && !hasRecoveredAnswer) {
|
||||||
|
await resetDoubaoPageAfterRecoverableFailure(page, pageResult.error, context.signal)
|
||||||
|
}
|
||||||
|
|
||||||
if (pageResult.ok === false) {
|
if (pageResult.ok === false) {
|
||||||
const failed = pageResult
|
const failed = pageResult
|
||||||
const detail = normalizeText(failed.detail) ?? 'unknown'
|
const detail = normalizeText(failed.detail) ?? 'unknown'
|
||||||
@@ -3255,9 +3427,11 @@ export const doubaoAdapter: MonitorAdapter = {
|
|||||||
export const __doubaoTestUtils = {
|
export const __doubaoTestUtils = {
|
||||||
buildSourceItem,
|
buildSourceItem,
|
||||||
dedupeSourceItems,
|
dedupeSourceItems,
|
||||||
|
doubaoModeKindFromLabelText,
|
||||||
extractQuestionText,
|
extractQuestionText,
|
||||||
isNonCitationAssetDomain,
|
isNonCitationAssetDomain,
|
||||||
isNonCitationAssetUrl,
|
isNonCitationAssetUrl,
|
||||||
|
isDoubaoRecoverablePageError,
|
||||||
parseDoubaoStreamBody,
|
parseDoubaoStreamBody,
|
||||||
resolveDoubaoSubmissionAnswer,
|
resolveDoubaoSubmissionAnswer,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -947,7 +947,7 @@ func (s *DesktopTaskService) Complete(ctx context.Context, client *repository.De
|
|||||||
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
|
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
finalStatus := normalizeDesktopTaskTerminalStatus(req.Status)
|
finalStatus := normalizeDesktopTaskCompletionStatus(req.Status)
|
||||||
resultJSON, err := marshalOptionalJSON(req.Payload)
|
resultJSON, err := marshalOptionalJSON(req.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, response.ErrBadRequest(40086, "invalid_desktop_task_payload", "payload must be serializable")
|
return nil, response.ErrBadRequest(40086, "invalid_desktop_task_payload", "payload must be serializable")
|
||||||
@@ -1520,7 +1520,7 @@ func scanDesktopTaskRows(rows pgx.Rows) ([]DesktopTaskView, error) {
|
|||||||
Platform: platform,
|
Platform: platform,
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
Payload: json.RawMessage(payload),
|
Payload: json.RawMessage(payload),
|
||||||
Status: normalizeDesktopTaskTerminalStatus(status),
|
Status: normalizeDesktopTaskViewStatus(kind, status),
|
||||||
PublishJobStatus: publishJobStatus,
|
PublishJobStatus: publishJobStatus,
|
||||||
ComplianceBlockedRecordID: complianceBlockedRecordID,
|
ComplianceBlockedRecordID: complianceBlockedRecordID,
|
||||||
ComplianceBlockedAt: complianceBlockedAt,
|
ComplianceBlockedAt: complianceBlockedAt,
|
||||||
@@ -1598,7 +1598,7 @@ func buildDesktopTaskView(task *repository.DesktopTask) DesktopTaskView {
|
|||||||
Platform: task.Platform,
|
Platform: task.Platform,
|
||||||
Kind: task.Kind,
|
Kind: task.Kind,
|
||||||
Payload: json.RawMessage(task.Payload),
|
Payload: json.RawMessage(task.Payload),
|
||||||
Status: normalizeDesktopTaskTerminalStatus(task.Status),
|
Status: normalizeDesktopTaskViewStatus(task.Kind, task.Status),
|
||||||
DedupKey: task.DedupKey,
|
DedupKey: task.DedupKey,
|
||||||
ActiveAttemptID: activeAttemptID,
|
ActiveAttemptID: activeAttemptID,
|
||||||
LeaseExpiresAt: task.LeaseExpiresAt,
|
LeaseExpiresAt: task.LeaseExpiresAt,
|
||||||
@@ -1637,6 +1637,22 @@ func normalizeDesktopTaskTerminalStatus(status string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeDesktopTaskCompletionStatus(status string) string {
|
||||||
|
switch strings.TrimSpace(status) {
|
||||||
|
case "succeeded", "failed", "unknown":
|
||||||
|
return strings.TrimSpace(status)
|
||||||
|
default:
|
||||||
|
return strings.TrimSpace(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDesktopTaskViewStatus(kind string, status string) string {
|
||||||
|
if strings.TrimSpace(kind) == "publish" {
|
||||||
|
return normalizeDesktopTaskTerminalStatus(status)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(status)
|
||||||
|
}
|
||||||
|
|
||||||
type desktopTaskRecoveryMode string
|
type desktopTaskRecoveryMode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -138,6 +138,25 @@ func TestResolvePublishRecoveryOutcome(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNormalizeDesktopTaskCompletionStatusPreservesUnknown(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if got := normalizeDesktopTaskCompletionStatus("unknown"); got != "unknown" {
|
||||||
|
t.Fatalf("normalizeDesktopTaskCompletionStatus(unknown) = %q, want unknown", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeDesktopTaskViewStatusOnlyMapsPublishUnknownToFailed(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if got := normalizeDesktopTaskViewStatus("monitor", "unknown"); got != "unknown" {
|
||||||
|
t.Fatalf("monitor unknown view status = %q, want unknown", got)
|
||||||
|
}
|
||||||
|
if got := normalizeDesktopTaskViewStatus("publish", "unknown"); got != "failed" {
|
||||||
|
t.Fatalf("publish unknown view status = %q, want failed", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsComplianceInvalidArticleVersionError(t *testing.T) {
|
func TestIsComplianceInvalidArticleVersionError(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user