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>
This commit is contained in:
木炎
2026-04-06 21:54:29 +08:00
parent 51913555ad
commit e4283f04cc
2 changed files with 1396 additions and 2157 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -44,12 +44,48 @@ function dispatchTextInput(node) {
node.dispatchEvent(new Event('change', { 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) { function fillInput(node, value) {
node.focus(); node.focus();
if ('value' in node) { if ('value' in node) {
node.value = ''; // 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); dispatchTextInput(node);
node.value = value; set(value);
dispatchTextInput(node); dispatchTextInput(node);
return; return;
} }
@@ -58,17 +94,111 @@ function fillInput(node, value) {
dispatchTextInput(node); 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) { function fillEditable(node, value) {
const normalized = String(value || '').replace(/\r\n/g, '\n');
node.focus(); 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 = ''; node.innerHTML = '';
const lines = String(value || '').split(/\n/); const lines = normalized.split(/\n/);
lines.forEach((line, index) => { lines.forEach((line, index) => {
if (index > 0) { if (index > 0) {
node.appendChild(document.createElement('br')); 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'));
} }
node.appendChild(document.createTextNode(line));
}); });
dispatchTextInput(node); }
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) { function findVisible(selectors) {
@@ -101,6 +231,21 @@ function findBodyEditor(titleInput) {
return null; 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) { function findButtonByText(fragment) {
const candidates = Array.from(document.querySelectorAll("button, [role='button'], a")); const candidates = Array.from(document.querySelectorAll("button, [role='button'], a"));
const wanted = cleanText(fragment); const wanted = cleanText(fragment);
@@ -113,6 +258,38 @@ function findButtonByText(fragment) {
}) || null; }) || 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 currentUrl = location.href;
const text = pageText(); const text = pageText();
if (isLoginBlocked(currentUrl, text)) { if (isLoginBlocked(currentUrl, text)) {
@@ -140,9 +317,82 @@ if (!titleInput || !bodyEditor) {
}; };
} }
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 || '')); fillInput(titleInput, String(args.title || ''));
fillEditable(bodyEditor, String(args.body || '')); 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'; const publishMode = String(args.publish_mode || '').toLowerCase() === 'true';
if (!publishMode) { if (!publishMode) {
return { return {
@@ -153,17 +403,19 @@ if (!publishMode) {
} }
const publishButton = findButtonByText('发布'); const publishButton = findButtonByText('发布');
if (!publishButton) { if (!publishButton || isDisabledButton(publishButton)) {
return { return {
status: 'publish_button_missing', status: 'publish_button_missing',
current_url: currentUrl, current_url: currentUrl,
title: cleanText(args.title), title: cleanText(args.title),
publish_button_disabled: !!publishButton && isDisabledButton(publishButton),
body_text: bodyTextAfterFill,
}; };
} }
publishButton.click(); publishButton.click();
const confirmButton = findButtonByText('确认发布'); const confirmButton = findButtonByText('确认发布');
if (!confirmButton) { if (!confirmButton || isDisabledButton(confirmButton)) {
return { return {
status: 'publish_clicked', status: 'publish_clicked',
current_url: currentUrl, current_url: currentUrl,