From 630f53341d91f6fa9748e9c306a2a7ca913f017d Mon Sep 17 00:00:00 2001 From: WenhuaXia Date: Sat, 30 May 2026 22:03:46 +0800 Subject: [PATCH] fix(doubao): update selectors for frontend changes (2026-05) Doubao frontend removed all data-testid attributes (chat_input_input, receive_message, message_text_content) and changed message list container class from item-kD... to hash classes. Changes: - read: use [data-plugin-identifier*="block_type:10000"] for messages, distinguish user/assistant via justify-end class - send: use textarea.semi-input-textarea for input, walk up DOM tree to find SVG send button - ask: same input/button fixes + poll response via block_type:10000 --- adapters/doubao/ask.yaml | 49 ++++++++++++++++++++++----------------- adapters/doubao/read.yaml | 41 ++++++++++++++++++-------------- adapters/doubao/send.yaml | 36 ++++++++++++++++++---------- 3 files changed, 76 insertions(+), 50 deletions(-) diff --git a/adapters/doubao/ask.yaml b/adapters/doubao/ask.yaml index ff6cc7a..e51ed40 100644 --- a/adapters/doubao/ask.yaml +++ b/adapters/doubao/ask.yaml @@ -7,7 +7,6 @@ domain: www.doubao.com strategy: cookie browser: true timeoutSeconds: 180 - args: text: positional: true @@ -18,9 +17,7 @@ args: type: int default: 60 description: Max seconds to wait - columns: [Role, Text] - pipeline: - navigate: https://www.doubao.com/chat - wait: 2 @@ -29,10 +26,10 @@ pipeline: const text = ${{ args.text | json }}; const timeout = ${{ args.timeout }} || 60; - // Fill composer and submit + // Find input - updated for Doubao frontend changes (2026-05) const candidates = [ - 'textarea[data-testid="chat_input_input"]', - 'textarea[placeholder*="发消息"]', + 'textarea.semi-input-textarea', + 'textarea[placeholder*="发"]', 'textarea', ]; let composer = null; @@ -41,7 +38,6 @@ pipeline: if (node) { composer = node; break; } } if (!composer) return [{ Role: 'System', Text: 'Could not find Doubao input element' }]; - composer.focus(); const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set; if (setter) setter.call(composer, text); @@ -50,16 +46,25 @@ pipeline: composer.dispatchEvent(new Event('change', { bubbles: true })); await new Promise(r => setTimeout(r, 500)); - // Click send or press Enter - const root = document.querySelector('[data-testid="chat_input"]') || document.body; - const buttons = Array.from(root.querySelectorAll('button, [role="button"]')); - const lastBtn = buttons[buttons.length - 1]; - if (lastBtn) lastBtn.click(); - else composer.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true })); - - await new Promise(r => setTimeout(r, 1500)); + // Click send button - updated: walk up from textarea to find SVG button + let container = composer.parentElement; + let depth = 0; + while (container && depth < 6) { + const btns = Array.from(container.querySelectorAll('button')).filter(b => { + const svg = b.querySelector('svg'); + return svg && !b.disabled && b.offsetParent !== null; + }); + if (btns.length > 0) { + const rightmost = btns.reduce((a, b) => a.offsetLeft > b.offsetLeft ? a : b); + rightmost.click(); + break; + } + container = container.parentElement; + depth++; + } - // Poll for response + // Poll for response using updated selectors (2026-05) + await new Promise(r => setTimeout(r, 3000)); const pollInterval = 2; const maxPolls = Math.ceil(timeout / pollInterval); let lastCandidate = ''; @@ -67,12 +72,14 @@ pipeline: for (let i = 0; i < maxPolls; i++) { await new Promise(r => setTimeout(r, pollInterval * 1000)); - const msgs = document.querySelectorAll('[data-testid="receive_message"] [data-testid="message_text_content"]'); - if (msgs.length === 0) continue; - const lastMsg = msgs[msgs.length - 1]; - const candidate = (lastMsg.innerText || '').trim(); + const blocks = Array.from(document.querySelectorAll('[data-plugin-identifier*="block_type:10000"]')); + const assistantBlocks = blocks.filter(b => !b.className?.toString().includes('justify-end')); + if (assistantBlocks.length === 0) continue; + const lastBlock = assistantBlocks[assistantBlocks.length - 1]; + const candidate = (lastBlock.innerText || '').trim(); if (!candidate) continue; - if (candidate === lastCandidate) { stableCount++; } else { lastCandidate = candidate; stableCount = 1; } + if (candidate === lastCandidate) { stableCount++; } + else { lastCandidate = candidate; stableCount = 1; } if (stableCount >= 2) break; } diff --git a/adapters/doubao/read.yaml b/adapters/doubao/read.yaml index e953c9d..e0c2161 100644 --- a/adapters/doubao/read.yaml +++ b/adapters/doubao/read.yaml @@ -10,28 +10,35 @@ browser: true columns: [Role, Text] pipeline: - - navigate: https://www.doubao.com/chat - - wait: 2 + - wait: 1 - evaluate: | - (function() { + (async () => { const clean = (value) => (value || '').replace(/\u00a0/g, ' ').replace(/\n{3,}/g, '\n\n').trim(); - const messageList = document.querySelector('[data-testid="message-list"]'); - if (!messageList) return [{ Role: 'System', Text: 'No visible Doubao messages were found.' }]; - const roots = Array.from(messageList.querySelectorAll('[data-testid="union_message"]')); - if (roots.length === 0) return [{ Role: 'System', Text: 'No visible Doubao messages were found.' }]; + // If on homepage, click first chat item to enter a conversation + if (!window.location.href.match(/\/chat\/\d+/)) { + const chatItems = document.querySelectorAll('[class*="chat-item"]'); + if (chatItems.length > 0) { + chatItems[0].click(); + await new Promise(r => setTimeout(r, 3000)); + } + } + + // Find all message blocks (block_type:10000 = user/assistant messages) + const blocks = Array.from(document.querySelectorAll('[data-plugin-identifier*="block_type:10000"]')); + + if (blocks.length === 0) return [{ Role: 'System', Text: 'No messages found.' }]; const turns = []; - for (const el of roots) { - let role = ''; - if (el.querySelector('[data-testid="send_message"]')) role = 'User'; - else if (el.querySelector('[data-testid="receive_message"]')) role = 'Assistant'; - if (!role) continue; - const textEl = el.querySelector('[data-testid="message_text_content"]'); - const text = textEl ? clean(textEl.innerText) : clean(el.innerText); - if (text) turns.push({ Role: role, Text: text }); + for (const block of blocks) { + const text = clean(block.innerText); + if (!text || text.length < 2) continue; + + // User messages are right-aligned (justify-end), assistant messages are not + const cls = block.className?.toString() || ''; + const role = cls.includes('justify-end') ? 'User' : 'Assistant'; + turns.push({ Role: role, Text: text }); } - if (turns.length === 0) return [{ Role: 'System', Text: 'No visible Doubao messages were found.' }]; - return turns; + return turns.length > 0 ? turns : [{ Role: 'System', Text: 'No readable messages.' }]; })() diff --git a/adapters/doubao/send.yaml b/adapters/doubao/send.yaml index b13d9a9..eaa43ea 100644 --- a/adapters/doubao/send.yaml +++ b/adapters/doubao/send.yaml @@ -6,25 +6,23 @@ description: Send a message to Doubao web chat domain: www.doubao.com strategy: cookie browser: true - args: text: positional: true type: string required: true description: Message to send - columns: [Status, SubmittedBy, InjectedText] - pipeline: - - navigate: https://www.doubao.com/chat - - wait: 2 + - wait: 1 - evaluate: | (async () => { const text = ${{ args.text | json }}; + + // Find input textarea - updated for Doubao frontend changes (2026-05) const candidates = [ - 'textarea[data-testid="chat_input_input"]', - 'textarea[placeholder*="发消息"]', + 'textarea.semi-input-textarea', + 'textarea[placeholder*="发"]', 'textarea', ]; let composer = null; @@ -33,7 +31,6 @@ pipeline: if (node) { composer = node; break; } } if (!composer) return [{ Status: 'Error', SubmittedBy: 'none', InjectedText: 'Could not find Doubao input' }]; - composer.focus(); const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set; if (setter) setter.call(composer, text); @@ -42,9 +39,24 @@ pipeline: composer.dispatchEvent(new Event('change', { bubbles: true })); await new Promise(r => setTimeout(r, 500)); - const root = document.querySelector('[data-testid="chat_input"]') || document.body; - const buttons = Array.from(root.querySelectorAll('button, [role="button"]')); - const lastBtn = buttons[buttons.length - 1]; - if (lastBtn) { lastBtn.click(); return [{ Status: 'Success', SubmittedBy: 'button', InjectedText: text }]; } + // Find send button: walk up from textarea, find buttons with SVG icon + let container = composer.parentElement; + let depth = 0; + while (container && depth < 6) { + const btns = Array.from(container.querySelectorAll('button')).filter(b => { + const svg = b.querySelector('svg'); + return svg && !b.disabled && b.offsetParent !== null; + }); + if (btns.length > 0) { + const rightmost = btns.reduce((a, b) => a.offsetLeft > b.offsetLeft ? a : b); + rightmost.click(); + return [{ Status: 'Success', SubmittedBy: 'button', InjectedText: text }]; + } + container = container.parentElement; + depth++; + } + + // Fallback: press Enter + composer.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true })); return [{ Status: 'Success', SubmittedBy: 'enter', InjectedText: text }]; })()