utils.js
1 /** 2 * Shared constants and helpers for Doubao desktop app (Electron + CDP). 3 * 4 * Requires: Doubao launched with --remote-debugging-port=9226 5 */ 6 /** Selectors discovered via data-testid attributes */ 7 export const SEL = { 8 INPUT: '[data-testid="chat_input_input"]', 9 SEND_BTN: '[data-testid="chat_input_send_button"]', 10 MESSAGE: '[data-testid="message_content"]', 11 MESSAGE_TEXT: '[data-testid="message_text_content"]', 12 INDICATOR: '[data-testid="indicator"]', 13 NEW_CHAT: '[data-testid="new_chat_button"]', 14 NEW_CHAT_SIDEBAR: '[data-testid="app-open-newChat"]', 15 }; 16 /** 17 * Inject text into the Doubao chat textarea via React-compatible value setter. 18 * Returns an evaluate script string. 19 */ 20 export function injectTextScript(text) { 21 return `(function(t) { 22 const textarea = document.querySelector('${SEL.INPUT}'); 23 if (!textarea) return { ok: false, error: 'No textarea found' }; 24 textarea.focus(); 25 const setter = Object.getOwnPropertyDescriptor( 26 window.HTMLTextAreaElement.prototype, 'value' 27 )?.set; 28 if (setter) setter.call(textarea, t); 29 else textarea.value = t; 30 textarea.dispatchEvent(new Event('input', { bubbles: true })); 31 textarea.dispatchEvent(new Event('change', { bubbles: true })); 32 return { ok: true }; 33 })(${JSON.stringify(text)})`; 34 } 35 /** 36 * Click the send button. Returns an evaluate script string. 37 */ 38 export function clickSendScript() { 39 return `(function() { 40 const btn = document.querySelector('${SEL.SEND_BTN}'); 41 if (!btn) return false; 42 btn.click(); 43 return true; 44 })()`; 45 } 46 /** 47 * Read all chat messages from the DOM. Returns an evaluate script string. 48 */ 49 export function readMessagesScript() { 50 return `(function() { 51 const results = []; 52 const containers = document.querySelectorAll('${SEL.MESSAGE}'); 53 for (const container of containers) { 54 const textEl = container.querySelector('${SEL.MESSAGE_TEXT}'); 55 if (!textEl) continue; 56 // Skip streaming messages 57 if (textEl.querySelector('${SEL.INDICATOR}') || 58 textEl.getAttribute('data-show-indicator') === 'true') continue; 59 const isUser = container.classList.contains('justify-end'); 60 let text = ''; 61 const children = textEl.querySelectorAll('div[dir]'); 62 if (children.length > 0) { 63 text = Array.from(children).map(c => c.innerText || c.textContent || '').join(''); 64 } else { 65 text = textEl.innerText?.trim() || textEl.textContent?.trim() || ''; 66 } 67 if (!text) continue; 68 results.push({ role: isUser ? 'User' : 'Assistant', text: text.substring(0, 2000) }); 69 } 70 return results; 71 })()`; 72 } 73 /** 74 * Click the new-chat button. Returns an evaluate script string. 75 */ 76 export function clickNewChatScript() { 77 return `(function() { 78 let btn = document.querySelector('${SEL.NEW_CHAT}'); 79 if (btn) { btn.click(); return true; } 80 btn = document.querySelector('${SEL.NEW_CHAT_SIDEBAR}'); 81 if (btn) { btn.click(); return true; } 82 return false; 83 })()`; 84 } 85 /** 86 * Poll for a new assistant response after sending. 87 * Returns evaluate script that checks message count vs baseline. 88 */ 89 export function pollResponseScript(beforeCount) { 90 return `(function(prevCount) { 91 const msgs = document.querySelectorAll('${SEL.MESSAGE}'); 92 if (msgs.length <= prevCount) return { phase: 'waiting', text: null }; 93 const lastMsg = msgs[msgs.length - 1]; 94 if (lastMsg.classList.contains('justify-end')) return { phase: 'waiting', text: null }; 95 const textEl = lastMsg.querySelector('${SEL.MESSAGE_TEXT}'); 96 if (!textEl) return { phase: 'waiting', text: null }; 97 if (textEl.querySelector('${SEL.INDICATOR}') || 98 textEl.getAttribute('data-show-indicator') === 'true') { 99 return { phase: 'streaming', text: null }; 100 } 101 let text = ''; 102 const children = textEl.querySelectorAll('div[dir]'); 103 if (children.length > 0) { 104 text = Array.from(children).map(c => c.innerText || c.textContent || '').join(''); 105 } else { 106 text = textEl.innerText?.trim() || textEl.textContent?.trim() || ''; 107 } 108 return { phase: 'done', text }; 109 })(${beforeCount})`; 110 }