Files
skill-lib/skills/zhihu-write/scripts/fill_article_draft.js
木炎 e4283f04cc feat: refresh Zhihu dashboard and draft autofill
Refresh the Zhihu hotlist screen presentation and update the article draft autofill flow to match the latest interaction changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 21:54:29 +08:00

432 lines
13 KiB
JavaScript

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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return '<p>' + (escaped || '<br>') + '</p>';
}).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),
};