/ clis / doubao-app / utils.js
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  }