import { CommandExecutionError } from '@jackwener/opencli/errors'; export const DOUBAO_DOMAIN = 'www.doubao.com'; export const DOUBAO_CHAT_URL = 'https://www.doubao.com/chat'; export const DOUBAO_NEW_CHAT_URL = 'https://www.doubao.com/chat/new-thread/create-by-msg'; const DOUBAO_COMPOSER_SELECTORS = [ 'textarea[data-testid="chat_input_input"]', '[data-testid="chat_input"] textarea', '.chat-input textarea', '.chat-input [contenteditable="true"]', '.chat-editor textarea', '.chat-editor [contenteditable="true"]', 'textarea[placeholder*="发消息"]', 'textarea[placeholder*="Message"]', '[contenteditable="true"][placeholder*="发消息"]', '[contenteditable="true"][placeholder*="Message"]', '[contenteditable="true"][aria-label*="发消息"]', '[contenteditable="true"][aria-label*="Message"]', 'textarea', '[contenteditable="true"]', ]; function buildDoubaoComposerLocatorScript() { return ` const isVisible = (el) => { if (!(el instanceof HTMLElement)) return false; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') return false; const rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }; const composerSelectors = ${JSON.stringify(DOUBAO_COMPOSER_SELECTORS)}; const findComposer = () => { for (const selector of composerSelectors) { const node = Array.from(document.querySelectorAll(selector)).find(isVisible); if (node) return node; } return null; }; `; } function getTranscriptLinesScript() { return ` (() => { const clean = (value) => (value || '') .replace(/\\u00a0/g, ' ') .replace(/\\n{3,}/g, '\\n\\n') .trim(); const root = document.body.cloneNode(true); const removableSelectors = [ '[data-testid="flow_chat_sidebar"]', '[data-testid="chat_input"]', '[data-testid="flow_chat_guidance_page"]', ]; for (const selector of removableSelectors) { root.querySelectorAll(selector).forEach((node) => node.remove()); } root.querySelectorAll('script, style, noscript').forEach((node) => node.remove()); const stopLines = new Set([ '豆包', '新对话', '内容由豆包 AI 生成,请仔细甄别', 'AI 创作', '云盘', '更多', '历史对话', '手机版对话', '快速', '超能模式', 'Beta', 'PPT 生成', '图像生成', '帮我写作', '请仔细甄别', '下载电脑版', ]); const noisyPatterns = [ /^window\\._SSR_DATA/, /^window\\._ROUTER_DATA/, /^\{"namedChunks"/, /^在此处拖放文件/, /^文件数量:/, /^文件类型:/, ]; const transcriptText = clean(root.innerText || root.textContent || '') .replace(/新对话/g, '\\n') .replace(/内容由豆包 AI 生成,请仔细甄别/g, '\\n') .replace(/在此处拖放文件/g, '\\n') .replace(/文件数量:[^\\n]*/g, '') .replace(/文件类型:[^\\n]*/g, ''); return clean(transcriptText) .split('\\n') .map((line) => clean(line)) .filter((line) => line && line.length <= 400 && !stopLines.has(line) && !noisyPatterns.some((pattern) => pattern.test(line))); })() `; } function getStateScript() { return ` (() => { const routerData = window._ROUTER_DATA?.loaderData?.chat_layout; const placeholderNode = document.querySelector( 'textarea[data-testid="chat_input_input"], textarea[placeholder], [contenteditable="true"][placeholder], [aria-label*="发消息"], [aria-label*="Message"]' ); return { url: window.location.href, title: document.title || '', isLogin: typeof routerData?.userSetting?.data?.is_login === 'boolean' ? routerData.userSetting.data.is_login : null, accountDescription: routerData?.accountInfo?.data?.description || '', placeholder: placeholderNode?.getAttribute('placeholder') || placeholderNode?.getAttribute('aria-label') || '', }; })() `; } function getTurnsScript() { return ` (() => { const clean = (value) => (value || '') .replace(/\\u00a0/g, ' ') .replace(/\\n{3,}/g, '\\n\\n') .trim(); const isVisible = (el) => { if (!(el instanceof HTMLElement)) return false; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') return false; const rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }; const getRole = (root) => { if ( root.matches('[data-testid="send_message"], [class*="send-message"]') || root.querySelector('[data-testid="send_message"], [class*="send-message"]') || root.matches('[class*="bg-g-send-msg-bubble"]') || root.querySelector('[class*="bg-g-send-msg-bubble"]') || root.querySelector('[data-foundation-type="send-message-action-bar"]') ) { return 'User'; } if ( root.matches('[data-testid="receive_message"], [data-testid*="receive_message"], [class*="receive-message"]') || root.querySelector('[data-testid="receive_message"], [data-testid*="receive_message"], [class*="receive-message"]') || root.matches('[class*="bg-g-receive-msg-bubble"]') || root.querySelector('[class*="bg-g-receive-msg-bubble"]') || root.querySelector('[data-foundation-type="receive-message-action-bar"]') ) { return 'Assistant'; } // 2026-05 Doubao DOM refactor: no more receive-message / bg-g-receive-msg-bubble // markers on assistant turns. Wrappers are now [class*="inner-item-"] / // [class*="top-item-"] and the only reliable assistant signal is the // .flow-markdown-body / .md-box-root content container WITHOUT any // send-bubble marker. if ( (root.matches('[class*="inner-item-"], [class*="top-item-"]') || root.closest('[class*="inner-item-"], [class*="top-item-"]')) && (root.matches('.flow-markdown-body, .md-box-root, [class*="md-box-root"]') || root.querySelector('.flow-markdown-body, .md-box-root, [class*="md-box-root"]')) && !root.matches('[class*="bg-g-send-msg-bubble"]') && !root.querySelector('[class*="bg-g-send-msg-bubble"]') ) { return 'Assistant'; } return ''; }; const messageTextSelectors = [ '[data-testid="message_text_content"]', '[data-testid="message_content"]', '[data-testid*="message_text"]', '[data-testid*="message_content"]', '[class*="message-text"]', '[class*="message-content"]', '[class*="bg-g-send-msg-bubble"]', '[class*="bg-g-receive-msg-bubble"]', '.flow-markdown-body', '.md-box-root', '[class*="md-box-root"]', '[class*="bubble"]', ]; const messageImageSelector = messageTextSelectors.map((s) => s + ' img').join(', '); const extractTextChunks = (root) => { const chunks = []; const seen = new Set(); for (const selector of messageTextSelectors) { const nodes = Array.from(root.querySelectorAll(selector)) .filter((el) => isVisible(el)) .map((el) => clean(el.innerText || el.textContent || '')) .filter(Boolean); for (const nodeText of nodes) { if (seen.has(nodeText)) continue; seen.add(nodeText); chunks.push(nodeText); } if (chunks.length > 0) break; } return chunks; }; const extractImageLines = (root) => Array.from(root.querySelectorAll(messageImageSelector)) .filter((el) => el instanceof HTMLImageElement && isVisible(el)) .map((el) => { const width = el.naturalWidth || el.width || 0; const height = el.naturalHeight || el.height || 0; if (width > 0 && height > 0 && width <= 48 && height <= 48) return ''; const url = clean(el.currentSrc || el.src || ''); return /^https?:\\/\\//i.test(url) ? 'Image: ' + url : ''; }) .filter((line, index, items) => Boolean(line) && items.indexOf(line) === index); const extractText = (root) => { const chunks = extractTextChunks(root); const text = chunks.length > 0 ? clean(chunks.join('\\n')) : clean(root.innerText || root.textContent || ''); const imageLines = extractImageLines(root); if (imageLines.length === 0) return text; return text ? text + '\\n' + imageLines.join('\\n') : imageLines.join('\\n'); }; const itemSelectors = [ // 2026-05 Doubao DOM refactor wrappers (prepended; outer ones win via // ancestor-keep dedup below). '[class*="inner-item-"]', '[class*="top-item-"]', '[class*="item-kDun2N"]', '[data-testid="union_message"]', '[data-testid="message-block-container"]', '[data-message-id]', '[class*="bg-g-send-msg-bubble"]', '[class*="bg-g-receive-msg-bubble"]', ]; const messageLists = Array.from(document.querySelectorAll('[class*="message-list-"], .container-PvPoAn, .scroll-view-OEiNXD, [data-testid="message-list"]')) .filter((el) => isVisible(el)); if (messageLists.length === 0) return []; const allRoots = []; const seen = new Set(); for (const messageList of messageLists) { for (const sel of itemSelectors) { messageList.querySelectorAll(sel).forEach((el) => { if (!seen.has(el)) { seen.add(el); allRoots.push(el); } }); } } const roots = allRoots .filter((el) => isVisible(el) && !el.closest('script, style, noscript')) .filter((el, index, items) => !items.some((other, otherIndex) => otherIndex !== index && other.contains(el))); const turns = roots .map((el) => { const role = getRole(el); const text = extractText(el); return { el, role, text }; }) .filter((item) => (item.role === 'User' || item.role === 'Assistant') && item.text); turns.sort((a, b) => { if (a.el === b.el) return 0; const pos = a.el.compareDocumentPosition(b.el); return pos & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1; }); const deduped = []; const dedupedSeen = new Set(); for (const turn of turns) { const key = turn.role + '::' + turn.text; if (dedupedSeen.has(key)) continue; dedupedSeen.add(key); deduped.push({ Role: turn.role, Text: turn.text }); } if (deduped.length > 0) return deduped; return []; })() `; } function prepareDoubaoComposerScript() { return ` (() => { ${buildDoubaoComposerLocatorScript()} const composer = findComposer(); if ( !(composer instanceof HTMLTextAreaElement) && !(composer instanceof HTMLInputElement) && !(composer instanceof HTMLElement) ) { return { ok: false, reason: 'Could not find Doubao input element' }; } try { composer.focus(); if (composer instanceof HTMLTextAreaElement || composer instanceof HTMLInputElement) { const length = composer.value.length; composer.setSelectionRange(0, length); } else { const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(composer); selection?.removeAllRanges(); selection?.addRange(range); } } catch (error) { return { ok: false, reason: error instanceof Error ? error.message : String(error), }; } return { ok: true }; })() `; } function composerStateScript() { return ` (() => { ${buildDoubaoComposerLocatorScript()} const composer = findComposer(); if (composer instanceof HTMLTextAreaElement || composer instanceof HTMLInputElement) { return { hasText: !!composer.value.trim(), text: composer.value }; } if (composer instanceof HTMLElement) { const text = (composer.innerText || '').trim() || (composer.textContent || '').trim(); return { hasText: !!text, text, }; } return { hasText: false, text: '' }; })() `; } function syncComposerAfterNativeTypeScript() { return ` (() => { ${buildDoubaoComposerLocatorScript()} const composer = findComposer(); if (composer instanceof HTMLTextAreaElement || composer instanceof HTMLInputElement) { const value = composer.value; composer.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: value, inputType: 'insertText' })); composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: value, inputType: 'insertText' })); composer.dispatchEvent(new Event('change', { bubbles: true })); return { hasText: !!value.trim(), text: value }; } if (composer instanceof HTMLElement) { const text = (composer.innerText || '').trim() || (composer.textContent || '').trim(); composer.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: text, inputType: 'insertText' })); composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: text, inputType: 'insertText' })); composer.dispatchEvent(new Event('change', { bubbles: true })); return { hasText: !!text, text }; } return { hasText: false, text: '' }; })() `; } function fillComposerScript(text) { return ` ((inputText) => { ${buildDoubaoComposerLocatorScript()} const composer = findComposer(); if (!composer) throw new Error('Could not find Doubao input element'); composer.focus(); if (composer instanceof HTMLTextAreaElement || composer instanceof HTMLInputElement) { const proto = composer instanceof HTMLTextAreaElement ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set; setter?.call(composer, inputText); composer.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: inputText, inputType: 'insertText' })); composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: inputText, inputType: 'insertText' })); composer.dispatchEvent(new Event('change', { bubbles: true })); return { hasText: !!composer.value.trim(), mode: 'text-input', text: composer.value }; } if (composer instanceof HTMLElement) { composer.textContent = ''; const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(composer); range.collapse(false); selection?.removeAllRanges(); selection?.addRange(range); document.execCommand('insertText', false, inputText); composer.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: inputText, inputType: 'insertText' })); composer.dispatchEvent(new InputEvent('input', { bubbles: true, data: inputText, inputType: 'insertText' })); composer.dispatchEvent(new Event('change', { bubbles: true })); return { hasText: !!((composer.innerText || '').trim() || (composer.textContent || '').trim()), mode: 'contenteditable', text: (composer.innerText || '').trim() || (composer.textContent || '').trim(), }; } throw new Error('Unsupported Doubao input element'); })(${JSON.stringify(text)}) `; } function detectDoubaoVerificationScript() { return ` (() => { const isVisible = (el) => { if (!(el instanceof HTMLElement)) return false; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') return false; const rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }; const challengeSelectors = [ 'iframe[src*="captcha"]', 'iframe[src*="verify"]', 'input[placeholder*="验证码"]', 'input[aria-label*="验证码"]', ]; const selectorMatch = challengeSelectors.find((selector) => { return Array.from(document.querySelectorAll(selector)).some((node) => isVisible(node)); }); if (selectorMatch) { return { detected: true, reason: selectorMatch }; } const phrasePattern = /人机验证|完成安全验证|异常访问|滑动验证|拖动滑块/i; const candidateRoots = Array.from( document.querySelectorAll('[role="dialog"], [aria-modal="true"], .semi-modal, .modal') ); const match = candidateRoots.find((node) => { if (!(node instanceof HTMLElement)) return false; if (!isVisible(node)) return false; const text = (node.innerText || node.textContent || '').trim(); if (!text || text.length > 400) return false; return phrasePattern.test(text); }); return { detected: !!match, reason: match ? ((match.innerText || match.textContent || '').trim().slice(0, 80) || 'challenge-ui') : '', }; })() `; } function clickSendButtonScript() { return ` (() => { ${buildDoubaoComposerLocatorScript()} const directSendButton = document.querySelector('button#flow-end-msg-send'); if (directSendButton instanceof HTMLElement && isVisible(directSendButton)) { const disabled = directSendButton.getAttribute('disabled') !== null || directSendButton.getAttribute('aria-disabled') === 'true'; if (!disabled) { directSendButton.click(); return true; } } const composer = findComposer(); if (!(composer instanceof HTMLElement)) return false; const composerRect = composer.getBoundingClientRect(); const rootCandidates = [ composer.closest('form'), composer.closest('[role="form"]'), composer.closest('[data-testid="chat_input"]'), composer.closest('.chat-input'), composer.parentElement, composer.parentElement?.parentElement, ].filter(Boolean); const seen = new Set(); const buttons = []; for (const root of rootCandidates) { root.querySelectorAll('button, [role="button"]').forEach((node) => { if (!(node instanceof HTMLElement)) return; if (seen.has(node)) return; seen.add(node); buttons.push(node); }); } const submitPattern = /send|发送|提交|发消息/i; const excludedPattern = /新对话|new chat|快速|视频生成|深入研究|图像生成|帮我写作|音乐生成|更多|上传|upload|麦克风|microphone|模式|mode|工具|tools|设置|settings|云盘|history|历史/i; let bestButton = null; let bestScore = -Infinity; for (const button of buttons) { if (!isVisible(button)) continue; const disabled = button.getAttribute('disabled') !== null || button.getAttribute('aria-disabled') === 'true'; if (disabled) continue; const text = (button.innerText || button.textContent || '').trim(); const aria = (button.getAttribute('aria-label') || '').trim(); const title = (button.getAttribute('title') || '').trim(); const className = String(button.className || ''); const haystack = [text, aria, title].join(' ').trim(); if (excludedPattern.test(haystack)) continue; const rect = button.getBoundingClientRect(); const dx = rect.left - composerRect.right; const dy = Math.abs((rect.top + rect.height / 2) - (composerRect.top + composerRect.height / 2)); const distancePenalty = Math.abs(dx) + dy; const isSubmitLike = submitPattern.test(haystack) || button.getAttribute('type') === 'submit' || className.includes('bg-dbx-text-highlight') || className.includes('bg-dbx-fill-highlight') || className.includes('text-dbx-text-static-white-primary'); if (!isSubmitLike) continue; if (dx < -80 || dx > 280) continue; if (dy > 140) continue; let score = -distancePenalty; if (submitPattern.test(haystack)) score += 5000; if (button.getAttribute('type') === 'submit') score += 1200; if (button.closest('.chat-input-button')) score += 1200; if (className.includes('bg-dbx-text-highlight')) score += 600; if (className.includes('bg-dbx-fill-highlight')) score += 600; if (className.includes('text-dbx-text-static-white-primary')) score += 400; if (dx >= -40 && dx <= 240) score += 120; if (rect.left >= composerRect.left - 40) score += 40; if (score > bestScore) { bestScore = score; bestButton = button; } } if (bestButton && bestScore >= 200) { bestButton.click(); return true; } return false; })() `; } function clickNewChatScript() { return ` (() => { const isVisible = (el) => { if (!(el instanceof HTMLElement)) return false; const style = window.getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') return false; const rect = el.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; }; const labels = ['新对话', 'New Chat', '创建新对话']; const buttons = Array.from(document.querySelectorAll('button, a, [role="button"]')); for (const button of buttons) { if (!isVisible(button)) continue; const text = (button.innerText || button.textContent || '').trim(); const aria = (button.getAttribute('aria-label') || '').trim(); const title = (button.getAttribute('title') || '').trim(); const haystacks = [text, aria, title]; if (haystacks.some((value) => labels.some((label) => value.includes(label)))) { button.click(); return text || aria || title || 'new-chat'; } } return ''; })() `; } function normalizeDoubaoTabs(rawTabs) { return rawTabs .map((tab, index) => { const record = (tab || {}); return { index: typeof record.index === 'number' ? record.index : index, url: typeof record.url === 'string' ? record.url : '', title: typeof record.title === 'string' ? record.title : '', active: record.active === true, }; }) .filter((tab) => tab.url.includes('doubao.com/chat')); } async function selectPreferredDoubaoTab(page) { const rawTabs = await page.tabs().catch(() => []); if (!Array.isArray(rawTabs) || rawTabs.length === 0) return false; const tabs = normalizeDoubaoTabs(rawTabs); if (tabs.length === 0) return false; const preferred = [...tabs].sort((left, right) => { const score = (tab) => { let value = tab.index; if (/https:\/\/www\.doubao\.com\/chat\/[A-Za-z0-9_-]+/.test(tab.url)) value += 1000; else if (tab.url.startsWith(DOUBAO_CHAT_URL)) value += 100; if (tab.active) value += 25; return value; }; return score(right) - score(left); })[0]; if (!preferred) return false; await page.selectTab(preferred.index); await page.wait(0.8); return true; } export async function ensureDoubaoChatPage(page) { let currentUrl = await page.evaluate('window.location.href').catch(() => ''); if (typeof currentUrl === 'string' && currentUrl.includes('doubao.com/chat')) { await page.wait(1); return; } const reusedTab = await selectPreferredDoubaoTab(page); if (reusedTab) { currentUrl = await page.evaluate('window.location.href').catch(() => ''); if (typeof currentUrl === 'string' && currentUrl.includes('doubao.com/chat')) { await page.wait(1); return; } } await page.goto(DOUBAO_CHAT_URL, { waitUntil: 'load', settleMs: 2500 }); await page.wait(1.5); } export async function getDoubaoPageState(page) { await ensureDoubaoChatPage(page); return await page.evaluate(getStateScript()); } export async function getDoubaoTurns(page) { await ensureDoubaoChatPage(page); const turns = await page.evaluate(getTurnsScript()); if (turns.length > 0) return turns; const lines = await page.evaluate(getTranscriptLinesScript()); return lines.map((line) => ({ Role: 'System', Text: line })); } export async function getDoubaoVisibleTurns(page) { await ensureDoubaoChatPage(page); return await page.evaluate(getTurnsScript()); } export async function getDoubaoTranscriptLines(page) { await ensureDoubaoChatPage(page); return await page.evaluate(getTranscriptLinesScript()); } export async function sendDoubaoMessage(page, text) { await ensureDoubaoChatPage(page); const normalizeComposerText = (value) => value.replace(/\r\n/g, '\n').trim(); const expectedText = normalizeComposerText(text); const prepared = await page.evaluate(prepareDoubaoComposerScript()); if (!prepared?.ok) { throw new CommandExecutionError(prepared?.reason || 'Could not find Doubao input element'); } let hasText = false; if (page.nativeType) { try { await page.nativeType(text); await page.wait(0.2); await page.evaluate(syncComposerAfterNativeTypeScript()); const nativeState = await page.evaluate(composerStateScript()); hasText = !!nativeState?.hasText && normalizeComposerText(nativeState?.text || '') === expectedText; } catch { } } if (!hasText) { const fallbackState = await page.evaluate(fillComposerScript(text)); hasText = !!fallbackState?.hasText && normalizeComposerText(fallbackState?.text || '') === expectedText; } if (!hasText) { throw new CommandExecutionError('Failed to insert text into Doubao composer'); } let submittedBy = 'enter'; const clicked = await page.evaluate(clickSendButtonScript()); if (clicked) { submittedBy = 'button'; } else if (page.nativeKeyPress) { try { await page.nativeKeyPress('Enter'); } catch { await page.pressKey('Enter'); } } else { await page.pressKey('Enter'); } await page.wait(0.8); const verification = await page.evaluate(detectDoubaoVerificationScript()); if (verification?.detected) { throw new CommandExecutionError('Doubao blocked the request with a verification challenge', verification.reason ? `Detected challenge signal: ${verification.reason}` : 'Please complete the challenge in the browser and try again.'); } return submittedBy; } export async function waitForDoubaoResponse(page, beforeLines, beforeTurns, promptText, timeoutSeconds) { const beforeTurnSet = new Set(beforeTurns .filter((turn) => turn.Role === 'Assistant') .map((turn) => `${turn.Role}::${turn.Text}`)); const sanitizeCandidate = (value) => value .replace(promptText, '') .replace(/内容由豆包 AI 生成/g, '') .replace(/在此处拖放文件/g, '') .replace(/文件数量:.*$/g, '') .replace(/\{"namedChunks".*$/g, '') .replace(/window\\._SSR_DATA.*$/g, '') .trim(); const getCandidate = async () => { const verification = await page.evaluate(detectDoubaoVerificationScript()); if (verification?.detected) { throw new CommandExecutionError('Doubao blocked the request with a verification challenge', verification.reason ? `Detected challenge signal: ${verification.reason}` : 'Please complete the challenge in the browser and try again.'); } const turns = await getDoubaoVisibleTurns(page); const assistantCandidate = [...turns] .reverse() .find((turn) => turn.Role === 'Assistant' && !beforeTurnSet.has(`${turn.Role}::${turn.Text}`)); const visibleCandidate = assistantCandidate ? sanitizeCandidate(assistantCandidate.Text) : ''; if (visibleCandidate) return visibleCandidate; const lines = await getDoubaoTranscriptLines(page); const additions = collectDoubaoTranscriptAdditions(beforeLines, lines, promptText, sanitizeCandidate) .split('\n') .filter(Boolean); const shortCandidate = additions.find((line) => line.length <= 120); return shortCandidate || additions[additions.length - 1] || ''; }; const pollIntervalSeconds = 2; const maxPolls = Math.max(1, Math.ceil(timeoutSeconds / pollIntervalSeconds)); let lastCandidate = ''; let stableCount = 0; for (let index = 0; index < maxPolls; index += 1) { await page.wait(index === 0 ? 1.5 : pollIntervalSeconds); const candidate = await getCandidate(); if (!candidate) continue; if (candidate === lastCandidate) { stableCount += 1; } else { lastCandidate = candidate; stableCount = 1; } if (stableCount >= 2 || index === maxPolls - 1) { return candidate; } } return lastCandidate; } export function isLikelyDoubaoUiNoise(value) { const text = value.replace(/\s+/g, ''); if (!text) return false; const exactNoise = new Set([ '快速视频生成深入研究图像生成帮我写作音乐生成更多', ]); return exactNoise.has(text); } function isAlwaysTranscriptUiNoise(value) { const text = value.replace(/\s+/g, ''); if (!text) return false; const exactNoise = new Set([ 'AI创作云盘更多历史对话', ]); return exactNoise.has(text); } function isLikelyTranscriptUiNoise(rawValue, sanitizedValue, promptText) { const normalizeWhitespace = (value) => value.replace(/\s+/g, ' ').trim(); const normalizedRaw = normalizeWhitespace(rawValue); const normalizedPrompt = normalizeWhitespace(promptText); if (!normalizedPrompt || !normalizedRaw.startsWith(normalizedPrompt)) return false; const remainder = normalizedRaw.slice(normalizedPrompt.length).trim(); if (!remainder) return true; return isLikelyDoubaoUiNoise(remainder) || isLikelyDoubaoUiNoise(sanitizedValue); } export function collectDoubaoTranscriptAdditions(beforeLines, currentLines, promptText, sanitize = (value) => value.trim()) { const normalizedBefore = new Set(beforeLines.map((line) => sanitize(line)).filter(Boolean)); return currentLines .filter((line) => !beforeLines.includes(line)) .map((line) => ({ raw: line, sanitized: sanitize(line) })) .filter(({ raw, sanitized }) => sanitized && sanitized !== promptText && !normalizedBefore.has(sanitized) && !isAlwaysTranscriptUiNoise(sanitized) && !isLikelyTranscriptUiNoise(raw, sanitized, promptText)) .map(({ sanitized }) => sanitized) .join('\n'); } function getConversationListScript() { return ` (() => { const sidebar = document.querySelector('[data-testid="flow_chat_sidebar"]'); if (!sidebar) return []; const items = Array.from( sidebar.querySelectorAll('a[data-testid="chat_list_thread_item"]') ); return items .map(a => { const href = a.getAttribute('href') || ''; const match = href.match(/\\/chat\\/(\\d{10,})/); if (!match) return null; const id = match[1]; const textContent = (a.textContent || a.innerText || '').trim(); const title = textContent .replace(/\\s+/g, ' ') .substring(0, 200); return { id, title, href }; }) .filter(Boolean); })() `; } export async function getDoubaoConversationList(page) { await ensureDoubaoChatPage(page); const raw = await page.evaluate(getConversationListScript()); if (!Array.isArray(raw)) return []; return raw.map((item) => ({ Id: item.id, Title: item.title, Url: `${DOUBAO_CHAT_URL}/${item.id}`, })); } export function parseDoubaoConversationId(input) { const match = input.match(/(\d{10,})/); return match ? match[1] : input; } function getConversationDetailScript() { return ` (() => { const clean = (v) => (v || '').replace(/\\u00a0/g, ' ').replace(/\\n{3,}/g, '\\n\\n').trim(); const messageList = document.querySelector('[data-testid="message-list"]'); if (!messageList) return { messages: [], meeting: null }; const meetingCard = messageList.querySelector('[data-testid="meeting-minutes-card"]'); let meeting = null; if (meetingCard) { const raw = clean(meetingCard.textContent || ''); const match = raw.match(/^(.+?)(?:会议时间:|\\s*$)(.*)/); meeting = { title: match ? match[1].trim() : raw, time: match && match[2] ? match[2].trim() : '', }; } const unions = Array.from(messageList.querySelectorAll('[data-testid="union_message"]')); const messages = unions.map(u => { const isSend = !!u.querySelector('[data-testid="send_message"]'); const isReceive = !!u.querySelector('[data-testid="receive_message"]'); const textEl = u.querySelector('[data-testid="message_text_content"]'); const text = textEl ? clean(textEl.innerText || textEl.textContent || '') : ''; return { role: isSend ? 'User' : isReceive ? 'Assistant' : 'System', text, hasMeetingCard: !!u.querySelector('[data-testid="meeting-minutes-card"]'), }; }).filter(m => m.text); return { messages, meeting }; })() `; } export async function navigateToConversation(page, conversationId) { const url = `${DOUBAO_CHAT_URL}/${conversationId}`; const currentUrl = await page.evaluate('window.location.href').catch(() => ''); if (typeof currentUrl === 'string' && currentUrl.includes(`/chat/${conversationId}`)) { await page.wait(1); return; } await page.goto(url, { waitUntil: 'load', settleMs: 3000 }); await page.wait(2); } export async function getConversationDetail(page, conversationId) { await navigateToConversation(page, conversationId); const raw = await page.evaluate(getConversationDetailScript()); const messages = (raw.messages || []).map((m) => ({ Role: m.role, Text: m.text, HasMeetingCard: m.hasMeetingCard, })); return { messages, meeting: raw.meeting }; } // --------------------------------------------------------------------------- // Meeting minutes panel helpers // --------------------------------------------------------------------------- function clickMeetingCardScript() { return ` (() => { const card = document.querySelector('[data-testid="meeting-minutes-card"]'); if (!card) return false; card.click(); return true; })() `; } function readMeetingSummaryScript() { return ` (() => { const panel = document.querySelector('[data-testid="canvas_panel_container"]'); if (!panel) return { error: 'no panel' }; const summary = panel.querySelector('[data-testid="meeting-summary-todos"]'); const summaryText = summary ? (summary.innerText || summary.textContent || '').trim() : ''; return { summary: summaryText }; })() `; } function clickTextNotesTabScript() { return ` (() => { const panel = document.querySelector('[data-testid="canvas_panel_container"]'); if (!panel) return false; const tabs = panel.querySelectorAll('[role="tab"], .semi-tabs-tab'); for (const tab of tabs) { if ((tab.textContent || '').trim().includes('文字')) { tab.click(); return true; } } return false; })() `; } function readTextNotesScript() { return ` (() => { const panel = document.querySelector('[data-testid="canvas_panel_container"]'); if (!panel) return ''; const textNotes = panel.querySelector('[data-testid="meeting-text-notes"]'); if (!textNotes) return ''; return (textNotes.innerText || textNotes.textContent || '').trim(); })() `; } function normalizeTranscriptLines(text) { return text .split('\n') .map(line => line.trim()) .filter(Boolean); } function containsLineSequence(haystack, needle) { if (needle.length === 0) return true; if (needle.length > haystack.length) return false; for (let start = 0; start <= haystack.length - needle.length; start += 1) { let matched = true; for (let offset = 0; offset < needle.length; offset += 1) { if (haystack[start + offset] !== needle[offset]) { matched = false; break; } } if (matched) return true; } return false; } export function mergeTranscriptSnapshots(existing, incoming) { const currentLines = normalizeTranscriptLines(existing); const nextLines = normalizeTranscriptLines(incoming); if (nextLines.length === 0) return currentLines.join('\n'); if (currentLines.length === 0) return nextLines.join('\n'); if (containsLineSequence(currentLines, nextLines)) return currentLines.join('\n'); const maxOverlap = Math.min(currentLines.length, nextLines.length); for (let overlap = maxOverlap; overlap > 0; overlap -= 1) { let matched = true; for (let index = 0; index < overlap; index += 1) { if (currentLines[currentLines.length - overlap + index] !== nextLines[index]) { matched = false; break; } } if (matched) { return [...currentLines, ...nextLines.slice(overlap)].join('\n'); } } return [...currentLines, ...nextLines].join('\n'); } function clickChapterTabScript() { return ` (() => { const panel = document.querySelector('[data-testid="canvas_panel_container"]'); if (!panel) return false; const tabs = panel.querySelectorAll('[role="tab"], .semi-tabs-tab'); for (const tab of tabs) { if ((tab.textContent || '').trim().includes('章节')) { tab.click(); return true; } } return false; })() `; } function readChapterScript() { return ` (() => { const panel = document.querySelector('[data-testid="canvas_panel_container"]'); if (!panel) return ''; const chapter = panel.querySelector('[data-testid="meeting-ai-chapter"]'); if (!chapter) return ''; return (chapter.innerText || chapter.textContent || '').trim(); })() `; } function triggerTranscriptDownloadScript() { return ` (() => { const panel = document.querySelector('[data-testid="canvas_panel_container"]'); if (!panel) return { error: 'no panel' }; const downloadIcon = panel.querySelector('[class*="DownloadMeetingAudio"] span[role="img"]'); if (!downloadIcon) return { error: 'no download icon' }; downloadIcon.click(); return { clicked: 'icon' }; })() `; } function clickTranscriptDownloadBtnScript() { return ` (() => { const btn = document.querySelector('[data-testid="minutes-download-text-btn"]'); if (!btn) return { error: 'no download text btn' }; btn.click(); return { clicked: 'transcript' }; })() `; } export async function openMeetingPanel(page, conversationId) { await navigateToConversation(page, conversationId); const clicked = await page.evaluate(clickMeetingCardScript()); if (!clicked) return false; await page.wait(2); return true; } export async function getMeetingSummary(page) { const result = await page.evaluate(readMeetingSummaryScript()); return result.summary || ''; } export async function getMeetingChapters(page) { await page.evaluate(clickChapterTabScript()); await page.wait(1.5); return await page.evaluate(readChapterScript()); } function scrollTextNotesPanelScript() { return ` (() => { const panel = document.querySelector('[data-testid="canvas_panel_container"]'); if (!panel) return 0; const textNotes = panel.querySelector('[data-testid="meeting-text-notes"]'); if (!textNotes) return 0; const scrollable = textNotes.closest('[class*="overflow"]') || textNotes.parentElement || textNotes; const maxScroll = scrollable.scrollHeight - scrollable.clientHeight; if (maxScroll > 0) { scrollable.scrollTop = scrollable.scrollHeight; } return maxScroll; })() `; } export async function getMeetingTranscript(page) { await page.evaluate(clickTextNotesTabScript()); await page.wait(2); let merged = ''; let stableRounds = 0; for (let i = 0; i < 10; i++) { await page.evaluate(scrollTextNotesPanelScript()); await page.wait(1); const snapshot = await page.evaluate(readTextNotesScript()); const nextMerged = mergeTranscriptSnapshots(merged, snapshot); if (nextMerged === merged && snapshot.length > 0) { stableRounds += 1; if (stableRounds >= 2) break; } else { stableRounds = 0; merged = nextMerged; } } return merged; } export async function triggerTranscriptDownload(page) { const iconResult = await page.evaluate(triggerTranscriptDownloadScript()); if (iconResult.error) return false; await page.wait(1); const btnResult = await page.evaluate(clickTranscriptDownloadBtnScript()); return !btnResult.error; } export const __test__ = { clickSendButtonScript, composerStateScript, detectDoubaoVerificationScript, getTurnsScript, getTranscriptLinesScript, }; export async function startNewDoubaoChat(page) { await ensureDoubaoChatPage(page); const clickedLabel = await page.evaluate(clickNewChatScript()); if (clickedLabel) { await page.wait(1.5); return clickedLabel; } await page.goto(DOUBAO_NEW_CHAT_URL, { waitUntil: 'load', settleMs: 2000 }); await page.wait(1.5); return 'navigate'; }