fix(harvest): robust Download-row finder + two-step hover + Copy Log button + 300-line log
Reported symptom: harvest opens the More menu then moves on without ever hovering Download. Cause: the size flyout's text can glue onto the Download row (textContent "Download1K Original size2K Upscaled..."), so the old /^download\b/ match — which needs a word boundary right after "download" — returned null and the whole size step was skipped. Fix: findDownloadRow() matches "download" anywhere and takes the SHORTEST visible candidate (the row itself, not a wrapper that swallowed the flyout). Stage-3 hover now moves in two steps (approach from the left, then settle on centre) to fire a real mouseenter that unfurls the submenu, and re-queries. Added per-stage logging (Download row text+coords, chosen size option) so a copied log shows exactly where any future miss happens. Popup gains a Copy button (whole activity log to clipboard) and log retention is raised 100 -> 300 so a full harvest pass is capturable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4aa7e19825
commit
607b5ec2b5
42
content.js
42
content.js
@ -573,6 +573,19 @@ function findMenuItem(re) {
|
||||
return leaf.length ? (leaf[0].closest('[role="menuitem"], li, button') || leaf[0]) : null;
|
||||
}
|
||||
|
||||
// The "Download" menu ROW. NOTE: its size flyout may live inside/next to it so a naive
|
||||
// textContent reads "Download1K Original size2K Upscaled…" — then /^download\b/ fails
|
||||
// (no word boundary after 'download'), which silently skipped the whole step. Match
|
||||
// "download" anywhere and take the SHORTEST visible candidate (the row, not a wrapper).
|
||||
function findDownloadRow() {
|
||||
const cands = [...document.querySelectorAll('[role="menuitem"], [role="menu"] button, button, [role="button"], div, span, li')]
|
||||
.filter(visible)
|
||||
.filter(e => /download/i.test(e.textContent || '') && (e.textContent || '').trim().length < 60);
|
||||
if (!cands.length) return null;
|
||||
cands.sort((a, b) => (a.textContent || '').length - (b.textContent || '').length);
|
||||
return cands[0].closest('[role="menuitem"], button, [role="button"], li') || cands[0];
|
||||
}
|
||||
|
||||
// The FREE "original size" download option. Images: "1K Original size". Videos:
|
||||
// "720p Original Size". Both contain "original size" — that's the safe key.
|
||||
// SAFETY: never return an "Upscaled" / "…credits" row. The 4K video upscale costs
|
||||
@ -636,25 +649,28 @@ async function executeHarvest(task) {
|
||||
|
||||
// Stage 2/4: click ⋮ ("More"), wait for the menu with Download in it
|
||||
await bg({ type: 'clickAt', ...center(dots) });
|
||||
const dl = await waitFor(() => findMenuItem(/^\s*download\b/i), 3000, 300);
|
||||
if (!dl) { missed++; missedThisPass.add(href); log('info', `harvest: ⋮ clicked but no Download item — ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; }
|
||||
const dl = await waitFor(findDownloadRow, 3000, 300);
|
||||
if (!dl) { missed++; missedThisPass.add(href); log('info', `harvest: ⋮ clicked but no Download row found — ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; }
|
||||
const dc = center(dl);
|
||||
log('info', `harvest: Download row "${(dl.textContent || '').trim().slice(0, 18)}" @ ${Math.round(dc.x)},${Math.round(dc.y)} — hovering for the size flyout…`);
|
||||
|
||||
// Stage 3/4: HOVER Download — the size flyout unfurls beside it (side depends on
|
||||
// where the tile sits; irrelevant, coords are re-queried fresh). Jiggle the
|
||||
// pointer a few px and retry — one mouseMoved sometimes isn't enough for the
|
||||
// hover intent to register. We pick "original size" (1K image / 720p video) and
|
||||
// NEVER the paid upscales — findSizeOption enforces that.
|
||||
// Stage 3/4: HOVER Download so the size flyout unfurls beside it. A single CDP jump
|
||||
// can miss the hover-intent, so move in TWO steps (approach from the left, then
|
||||
// settle on centre) to fire a real mouseenter, and re-query. We pick "original
|
||||
// size" (1K image / 720p video) and NEVER the paid upscales (findSizeOption).
|
||||
let oneK = null;
|
||||
for (let j = 0; j < 3 && !oneK; j++) {
|
||||
for (let j = 0; j < 4 && !oneK; j++) {
|
||||
const c = center(dl);
|
||||
await bg({ type: 'hoverAt', x: c.x - 4 + j * 4, y: c.y });
|
||||
oneK = await waitFor(findSizeOption, 1600, 250);
|
||||
await bg({ type: 'hoverAt', x: c.x - 28, y: c.y }); await sleep(130);
|
||||
await bg({ type: 'hoverAt', x: c.x, y: c.y });
|
||||
oneK = await waitFor(findSizeOption, 1400, 200);
|
||||
}
|
||||
if (!oneK) { // last resort: some builds expand on click
|
||||
if (!oneK) { // last resort: some builds expand the submenu on click
|
||||
await bg({ type: 'clickAt', ...center(dl) });
|
||||
oneK = await waitFor(findSizeOption, 1600, 250);
|
||||
}
|
||||
if (!oneK) { missed++; missedThisPass.add(href); log('info', 'harvest: no "original size" option after hover + click-expand.'); await bg({ type: 'pressEscape' }); await sleep(400); continue; }
|
||||
if (!oneK) { missed++; missedThisPass.add(href); log('info', `harvest: Download hovered but size flyout never appeared (no "original size"). ${gridCensus()}`); await bg({ type: 'pressEscape' }); await sleep(400); continue; }
|
||||
log('info', `harvest: size option "${(oneK.textContent || '').trim().slice(0, 22)}" — downloading.`);
|
||||
|
||||
// Stage 4/4: walk the pointer INTO the flyout (keeps it open), then click 1K,
|
||||
// then VERIFY a download event actually fired before counting it.
|
||||
@ -737,7 +753,7 @@ async function handleRateLimit(task) {
|
||||
function log(level, text) {
|
||||
console.log(`[Flow][${level}] ${text}`);
|
||||
chrome.storage.local.get({ logs: [] }, (r) => {
|
||||
chrome.storage.local.set({ logs: [...r.logs, { level, text, timestamp: Date.now() }].slice(-100) });
|
||||
chrome.storage.local.set({ logs: [...r.logs, { level, text, timestamp: Date.now() }].slice(-300) });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
<div class="console-section">
|
||||
<div class="console-header">
|
||||
<span>Activity Log</span>
|
||||
<button id="clear-logs" class="btn-text">Clear</button>
|
||||
<span><button id="copy-logs" class="btn-text">Copy</button> <button id="clear-logs" class="btn-text">Clear</button></span>
|
||||
</div>
|
||||
<div class="console-body" id="log-console">
|
||||
<div class="log-entry system-log">[System] Extension loaded. Click Start to begin.</div>
|
||||
|
||||
19
popup.js
19
popup.js
@ -129,6 +129,23 @@ clearLogsBtn.addEventListener('click', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Copy Logs — grab the whole activity log to the clipboard for pasting into chat.
|
||||
const copyLogsBtn = document.getElementById('copy-logs');
|
||||
copyLogsBtn.addEventListener('click', () => {
|
||||
chrome.storage.local.get({ logs: [] }, (res) => {
|
||||
const text = (res.logs || []).map(l =>
|
||||
`[${new Date(l.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })}] ${l.text}`
|
||||
).join('\n');
|
||||
const done = () => { copyLogsBtn.textContent = 'Copied!'; setTimeout(() => { copyLogsBtn.textContent = 'Copy'; }, 1200); };
|
||||
navigator.clipboard.writeText(text).then(done).catch(() => {
|
||||
const ta = document.createElement('textarea'); ta.value = text;
|
||||
document.body.appendChild(ta); ta.select();
|
||||
try { document.execCommand('copy'); } catch (e) {}
|
||||
ta.remove(); done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle Run Loop
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
chrome.storage.local.get('isRunning', (res) => {
|
||||
@ -152,7 +169,7 @@ function addSystemLog(text) {
|
||||
text: text,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
const updated = [...res.logs, newLog].slice(-100); // limit to 100 logs
|
||||
const updated = [...res.logs, newLog].slice(-300); // keep enough for a harvest debug
|
||||
chrome.storage.local.set({ logs: updated }, () => {
|
||||
appendLogToUI(newLog);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user