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>
432 lines
13 KiB
JavaScript
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, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
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),
|
|
};
|