function cleanText(value) { return String(value || '') .replace(/\s+/g, ' ') .replace(/\u200b/g, '') .trim(); } function pageText() { const body = document.body; return cleanText(body && (body.innerText || body.textContent || '')); } function isLoginBlocked(url, text) { return /\/signin\b|\/signup\b/.test(url) || /登录|注册|验证码|安全验证|验证后继续|请先登录/.test(text); } function isVisible(node) { if (!node) { return false; } const rect = typeof node.getBoundingClientRect === 'function' ? node.getBoundingClientRect() : null; return !rect || rect.width > 0 || rect.height > 0; } function attrText(node, name) { if (!node || typeof node.getAttribute !== 'function') { return ''; } return cleanText(node.getAttribute(name) || ''); } function looksLikeTitleInput(node) { const signals = [ attrText(node, 'placeholder'), attrText(node, 'data-placeholder'), attrText(node, 'aria-label'), ].filter(Boolean); return signals.some((value) => value.includes('标题')); } function dispatchTextInput(node) { node.dispatchEvent(new Event('input', { bubbles: true, composed: true })); node.dispatchEvent(new Event('change', { bubbles: true, composed: true })); } function dispatchRichInput(node, inputType, data) { try { node.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, composed: true, inputType, data, })); } catch (_) {} try { node.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true, inputType, data, })); } catch (_) { node.dispatchEvent(new Event('input', { bubbles: true, composed: true })); } node.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, composed: true, key: 'Enter' })); node.dispatchEvent(new Event('change', { bubbles: true, composed: true })); node.dispatchEvent(new Event('blur', { bubbles: true, composed: true })); } function fillInput(node, value) { node.focus(); if ('value' in node) { // React overrides the value setter on input/textarea to track changes. // Calling .value = x directly bypasses React's tracking, so the // component's onChange never fires and React state stays stale. // Using the native prototype setter makes React detect the update. var proto = node instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; var nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value'); var set = (nativeSetter && nativeSetter.set) ? function(v) { nativeSetter.set.call(node, v); } : function(v) { node.value = v; }; set(''); dispatchTextInput(node); set(value); dispatchTextInput(node); return; } node.textContent = value; dispatchTextInput(node); } function selectEditableContents(node) { const selection = window.getSelection && window.getSelection(); if (!selection) { return null; } const range = document.createRange(); range.selectNodeContents(node); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); return selection; } function fillEditable(node, value) { const normalized = String(value || '').replace(/\r\n/g, '\n'); node.focus(); // Strategy 1: Simulate a clipboard paste event. // Rich-text frameworks (Draft.js, ProseMirror, Slate) have explicit paste // handlers that parse pasted content and update their internal content // model. This keeps word counts, undo stacks, and toolbar / publish-button // states in sync — something direct DOM writes and execCommand cannot // guarantee. var pasteWorked = false; try { selectEditableContents(node); try { document.execCommand('selectAll', false, null); } catch (_) {} try { document.execCommand('delete', false, null); } catch (_) {} var htmlLines = normalized.split('\n').map(function(line) { var escaped = (line || '').replace(/&/g, '&').replace(//g, '>'); return '

' + (escaped || '
') + '

'; }).join(''); var dt = new DataTransfer(); dt.setData('text/plain', normalized); dt.setData('text/html', htmlLines); node.dispatchEvent(new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: dt, })); var afterPaste = cleanText(node.innerText || node.textContent || ''); pasteWorked = !!afterPaste && afterPaste !== '请输入正文' && afterPaste !== cleanText(attrText(node, 'placeholder')); } catch (_) {} if (pasteWorked) { return; } // Strategy 2: document.execCommand('insertText') selectEditableContents(node); let inserted = false; try { document.execCommand('selectAll', false, null); } catch (_) {} try { document.execCommand('delete', false, null); } catch (_) {} try { inserted = document.execCommand('insertText', false, normalized); } catch (_) { inserted = false; } if (!inserted) { // Strategy 3: Direct DOM write (visual only — editor state may not update) node.innerHTML = ''; const lines = normalized.split(/\n/); lines.forEach((line, index) => { if (index > 0) { node.appendChild(document.createElement('p')); } const container = index > 0 ? node.lastChild : node; const textNode = document.createTextNode(line || ''); if (container === node) { node.appendChild(textNode); } else { container.appendChild(textNode); } if (!line && container !== node) { container.appendChild(document.createElement('br')); } }); } dispatchRichInput(node, 'insertText', normalized); const actualText = cleanText(node.innerText || node.textContent || ''); if (!actualText || actualText === cleanText(attrText(node, 'placeholder')) || actualText === '请输入正文') { node.innerHTML = ''; normalized.split(/\n/).forEach((line) => { const paragraph = document.createElement('p'); if (line) { paragraph.textContent = line; } else { paragraph.appendChild(document.createElement('br')); } node.appendChild(paragraph); }); dispatchRichInput(node, 'insertParagraph', normalized); } } function findVisible(selectors) { for (const selector of selectors) { const nodes = Array.from(document.querySelectorAll(selector)); const match = nodes.find((node) => { return isVisible(node); }); if (match) { return match; } } return null; } function findBodyEditor(titleInput) { for (const selector of [ "div[contenteditable='true'][role='textbox']", "div.public-DraftEditor-content[contenteditable='true']", "[role='textbox'][contenteditable='true']", "[contenteditable='true'][data-placeholder]", "div[contenteditable='true']", ]) { const nodes = Array.from(document.querySelectorAll(selector)); const match = nodes.find((node) => isVisible(node) && node !== titleInput && !looksLikeTitleInput(node)); if (match) { return match; } } return null; } function isDisabledButton(node) { if (!node) { return true; } if (node.disabled) { return true; } const ariaDisabled = attrText(node, 'aria-disabled'); if (ariaDisabled === 'true') { return true; } const className = typeof node.className === 'string' ? node.className : ''; return /disabled|is-disabled/.test(className); } function findButtonByText(fragment) { const candidates = Array.from(document.querySelectorAll("button, [role='button'], a")); const wanted = cleanText(fragment); return candidates.find((node) => { const text = cleanText(node.textContent); if (!text || !text.includes(wanted)) { return false; } return isVisible(node); }) || null; } function findPreferredButtonByText(fragment) { const candidates = Array.from(document.querySelectorAll("button, [role='button'], a")) .filter((node) => isVisible(node)); const wanted = cleanText(fragment); const exactMatch = candidates.find((node) => cleanText(node.textContent) === wanted); if (exactMatch) { return exactMatch; } return candidates.find((node) => cleanText(node.textContent).includes(wanted)) || null; } function nodeText(node) { if (!node) { return ''; } if ('value' in node && typeof node.value === 'string') { return cleanText(node.value); } return cleanText(node.innerText || node.textContent || ''); } function collectDraftState(titleInput, bodyEditor) { const titleText = nodeText(titleInput); const bodyText = nodeText(bodyEditor); const normalizedBodyText = bodyText === '请输入正文' ? '' : bodyText; return { titleText, bodyText: normalizedBodyText, ready: !!titleText && !!normalizedBodyText, }; } const currentUrl = location.href; const text = pageText(); if (isLoginBlocked(currentUrl, text)) { return { status: 'login_required', current_url: currentUrl, }; } const titleInput = findVisible([ "textarea[placeholder*='标题']", "input[placeholder*='标题']", "textarea[data-placeholder*='标题']", "input[data-placeholder*='标题']", "[role='textbox'][aria-label*='标题']", "[contenteditable='true'][aria-label*='标题']", "[contenteditable='true'][data-placeholder*='标题']", ]); const bodyEditor = findBodyEditor(titleInput); if (!titleInput || !bodyEditor) { return { status: 'editor_not_ready', current_url: currentUrl, }; } const liveInputMode = String(args.input_mode || '').toLowerCase() === 'live_input'; if (liveInputMode) { var draftState = collectDraftState(titleInput, bodyEditor); const publishMode = String(args.publish_mode || '').toLowerCase() === 'true'; // If keyboard simulation hasn't populated the fields yet (fire-and-forget // may still be in progress or may have missed), fall back to direct DOM fill. if (!draftState.titleText && args.title) { fillInput(titleInput, String(args.title || '')); } if (!draftState.bodyText && args.body) { fillEditable(bodyEditor, String(args.body || '')); } draftState = collectDraftState(titleInput, bodyEditor); if (!draftState.ready) { return { status: 'editor_not_ready', current_url: currentUrl, title: draftState.titleText || cleanText(args.title), body_text: draftState.bodyText, }; } if (!publishMode) { return { status: 'draft_ready', current_url: currentUrl, title: draftState.titleText || cleanText(args.title), body_text: draftState.bodyText, }; } const publishButton = findPreferredButtonByText('发布'); if (!publishButton || isDisabledButton(publishButton)) { return { status: 'publish_button_missing', current_url: currentUrl, title: draftState.titleText || cleanText(args.title), publish_button_disabled: !!publishButton && isDisabledButton(publishButton), body_text: draftState.bodyText, }; } publishButton.click(); const confirmButton = findPreferredButtonByText('确认发布'); if (!confirmButton || isDisabledButton(confirmButton)) { return { status: 'publish_clicked', current_url: currentUrl, title: draftState.titleText || cleanText(args.title), body_text: draftState.bodyText, }; } confirmButton.click(); return { status: 'publish_submitted', current_url: currentUrl, title: draftState.titleText || cleanText(args.title), body_text: draftState.bodyText, }; } fillInput(titleInput, String(args.title || '')); fillEditable(bodyEditor, String(args.body || '')); const bodyTextAfterFill = cleanText(bodyEditor.innerText || bodyEditor.textContent || ''); if (!bodyTextAfterFill || bodyTextAfterFill === '请输入正文') { return { status: 'editor_not_ready', current_url: currentUrl, title: cleanText(args.title), body_text: bodyTextAfterFill, }; } const publishMode = String(args.publish_mode || '').toLowerCase() === 'true'; if (!publishMode) { return { status: 'draft_ready', current_url: currentUrl, title: cleanText(args.title), }; } const publishButton = findButtonByText('发布'); if (!publishButton || isDisabledButton(publishButton)) { return { status: 'publish_button_missing', current_url: currentUrl, title: cleanText(args.title), publish_button_disabled: !!publishButton && isDisabledButton(publishButton), body_text: bodyTextAfterFill, }; } publishButton.click(); const confirmButton = findButtonByText('确认发布'); if (!confirmButton || isDisabledButton(confirmButton)) { return { status: 'publish_clicked', current_url: currentUrl, title: cleanText(args.title), }; } confirmButton.click(); return { status: 'publish_submitted', current_url: currentUrl, title: cleanText(args.title), };