diff --git a/clis/doubao/utils.js b/clis/doubao/utils.js index fa58d0168..119f25f4c 100644 --- a/clis/doubao/utils.js +++ b/clis/doubao/utils.js @@ -798,24 +798,32 @@ export function collectDoubaoTranscriptAdditions(beforeLines, currentLines, prom function getConversationListScript() { return ` (() => { - const sidebar = document.querySelector('[data-testid="flow_chat_sidebar"]'); + const sidebar = document.querySelector('[data-testid="flow_chat_sidebar"], #flow_chat_sidebar'); if (!sidebar) return []; + const clean = (value) => (value || '') + .replace(/\\u00a0/g, ' ') + .replace(/\\s+/g, ' ') + .trim(); + const items = Array.from( - sidebar.querySelectorAll('a[data-testid="chat_list_thread_item"]') + sidebar.querySelectorAll('a[data-testid="chat_list_thread_item"], a[id^="conversation_"], a[href^="/chat/"]') ); + const seen = new Set(); return items .map(a => { const href = a.getAttribute('href') || ''; - const match = href.match(/\\/chat\\/(\\d{10,})/); + const idAttr = a.getAttribute('id') || ''; + const match = href.match(/\\/chat\\/(\\d{10,})/) || idAttr.match(/^conversation_(\\d{10,})$/); if (!match) return null; const id = match[1]; - const textContent = (a.textContent || a.innerText || '').trim(); - const title = textContent - .replace(/\\s+/g, ' ') + if (seen.has(id)) return null; + seen.add(id); + const titleNode = a.querySelector('[title], span') || a; + const title = clean(titleNode.getAttribute?.('title') || titleNode.textContent || a.textContent) .substring(0, 200); - return { id, title, href }; + return { id, title: title || id, href }; }) .filter(Boolean); })() @@ -839,9 +847,20 @@ export function parseDoubaoConversationId(input) { function getConversationDetailScript() { return ` (() => { - const clean = (v) => (v || '').replace(/\\u00a0/g, ' ').replace(/\\n{3,}/g, '\\n\\n').trim(); + const clean = (v) => (v || '') + .replace(/\\u00a0/g, ' ') + .replace(/\\n{3,}/g, '\\n\\n') + .trim(); - const messageList = document.querySelector('[data-testid="message-list"]'); + 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 messageList = document.querySelector('[data-testid="message-list"], [class*="message-list"], [aria-label="doc_editor"]'); if (!messageList) return { messages: [], meeting: null }; const meetingCard = messageList.querySelector('[data-testid="meeting-minutes-card"]'); @@ -855,20 +874,59 @@ function getConversationDetailScript() { }; } - 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 itemSelectors = [ + '[data-testid="union_message"]', + '[data-testid="message-block-container"]', + '[data-message-id]', + '[class^="item-"]', + '[class*=" item-"]', + '[class*="bg-g-send-msg-bubble"]', + '[class*="bg-g-receive-msg-bubble"]', + ]; + const allRoots = []; + const rootSeen = new Set(); + for (const selector of itemSelectors) { + messageList.querySelectorAll(selector).forEach((el) => { + if (!rootSeen.has(el)) { + rootSeen.add(el); + allRoots.push(el); + } + }); + } + const itemRoots = allRoots + .filter((root) => isVisible(root) && !root.closest('script, style, noscript')) + .filter((root, index, items) => !items.some((other, otherIndex) => otherIndex !== index && other.contains(root))); + + const findTextEl = (root, selector) => { + if (root.matches(selector)) return root; + return root.querySelector(selector); + }; + + const messages = itemRoots.map((root) => { + const sendTextEl = findTextEl(root, '[data-testid="send_message"] [data-testid="message_text_content"], [data-testid="send_message"], [class*="bg-g-send-msg-bubble"]'); + const receiveTextEl = findTextEl(root, '[data-testid="receive_message"] [data-testid="message_text_content"], [data-testid="receive_message"], .flow-markdown-body, [class*="bg-g-receive-msg-bubble"]'); + const hasMeetingCard = !!root.querySelector('[data-testid="meeting-minutes-card"]'); + const isSend = !!sendTextEl; + const isReceive = !isSend && !!receiveTextEl; + const textEl = isSend ? sendTextEl : receiveTextEl; const text = textEl ? clean(textEl.innerText || textEl.textContent || '') : ''; return { role: isSend ? 'User' : isReceive ? 'Assistant' : 'System', text, - hasMeetingCard: !!u.querySelector('[data-testid="meeting-minutes-card"]'), + hasMeetingCard, }; }).filter(m => m.text); - return { messages, meeting }; + const deduped = []; + const dedupedSeen = new Set(); + for (const message of messages) { + const key = message.role + '::' + message.text; + if (dedupedSeen.has(key)) continue; + dedupedSeen.add(key); + deduped.push(message); + } + + return { messages: deduped, meeting }; })() `; } @@ -1116,6 +1174,8 @@ export const __test__ = { clickSendButtonScript, composerStateScript, detectDoubaoVerificationScript, + getConversationDetailScript, + getConversationListScript, getTurnsScript, getTranscriptLinesScript, }; diff --git a/clis/doubao/utils.test.js b/clis/doubao/utils.test.js index 2543a4bde..7cfd52fa0 100644 --- a/clis/doubao/utils.test.js +++ b/clis/doubao/utils.test.js @@ -145,6 +145,31 @@ describe('doubao send strategy', () => { }); }); describe('doubao receive strategy', () => { + it('supports current Doubao sidebar conversation anchors', () => { + const listScript = __test__.getConversationListScript(); + expect(listScript).toContain('#flow_chat_sidebar'); + expect(listScript).toContain('a[id^="conversation_"]'); + expect(listScript).toContain('a[href^="/chat/"]'); + expect(listScript).toContain('idAttr.match'); + expect(listScript).toContain('seen.has(id)'); + }); + + it('supports current Doubao detail message containers', () => { + const detailScript = __test__.getConversationDetailScript(); + expect(detailScript).toContain('[class*="message-list"]'); + expect(detailScript).toContain('[aria-label="doc_editor"]'); + expect(detailScript).toContain('[class^="item-"]'); + expect(detailScript).toContain('[class*="bg-g-send-msg-bubble"]'); + expect(detailScript).toContain('[class*="bg-g-receive-msg-bubble"]'); + expect(detailScript).toContain('[data-message-id]'); + expect(detailScript).toContain('.flow-markdown-body'); + expect(detailScript).toContain('root.matches(selector)'); + expect(detailScript).toContain("replace(/\\n{3,}/g, '\\n\\n')"); + expect(detailScript).toContain('!items.some((other, otherIndex)'); + expect(detailScript).toContain('dedupedSeen.has(key)'); + expect(detailScript).not.toContain('.filter(Boolean)'); + }); + it('keeps both the new skin selectors and the older structural fallbacks in the turns script', () => { const turnsScript = __test__.getTurnsScript(); expect(turnsScript).toContain('[class*="message-list-S2Fv2S"]');