Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

1157 lines
42 KiB
JavaScript

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';
}