feat: library harvest pass + error-baseline fix
- countErrMsgs baseline: lingering 'Something went wrong' chat history no longer fails every task (the 20:33-21:01 massacre — gens succeeded, all marked failed) - Try-again only clicked when a NEW error appears (old buttons re-ran old prompts) - kind:'harvest' task = automated John-ritual per tile: trusted CDP hover -> ⋮ -> Download -> 1K; background routes downloads to flowrinse/harvest/; seen-list in storage makes it resumable; paced + capped at 500/pass - queued harvest_all_001 to recover tonight's generated-but-not-downloaded batch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
78b3ac85b0
commit
d3281bc251
@ -27,6 +27,14 @@ async function ensure(tabId) {
|
||||
}
|
||||
|
||||
const ENTER = { key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 };
|
||||
const ESC = { key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27 };
|
||||
|
||||
// While a harvest pass runs, route every browser download into flowrinse/harvest/.
|
||||
let harvestRouting = false;
|
||||
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
|
||||
if (!harvestRouting) return;
|
||||
suggest({ filename: `flowrinse/harvest/${(item.filename || 'asset').split(/[\\/]/).pop()}`, conflictAction: 'uniquify' });
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
const tabId = sender.tab && sender.tab.id;
|
||||
@ -48,6 +56,25 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
{ url: msg.url, filename: msg.filename, conflictAction: 'uniquify', saveAs: false },
|
||||
(id) => chrome.runtime.lastError ? rej(chrome.runtime.lastError) : res(id)));
|
||||
sendResponse({ ok: dlId != null, id: dlId });
|
||||
} else if (msg.type === 'hoverAt') {
|
||||
await ensure(tabId);
|
||||
await cmd(tabId, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x: msg.x, y: msg.y });
|
||||
sendResponse({ ok: true });
|
||||
} else if (msg.type === 'clickAt') {
|
||||
// Trusted click at viewport coords — Flow's menus ignore synthetic .click().
|
||||
await ensure(tabId);
|
||||
await cmd(tabId, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x: msg.x, y: msg.y });
|
||||
await cmd(tabId, 'Input.dispatchMouseEvent', { type: 'mousePressed', x: msg.x, y: msg.y, button: 'left', clickCount: 1 });
|
||||
await cmd(tabId, 'Input.dispatchMouseEvent', { type: 'mouseReleased', x: msg.x, y: msg.y, button: 'left', clickCount: 1 });
|
||||
sendResponse({ ok: true });
|
||||
} else if (msg.type === 'pressEscape') {
|
||||
await ensure(tabId);
|
||||
await cmd(tabId, 'Input.dispatchKeyEvent', { type: 'keyDown', ...ESC });
|
||||
await cmd(tabId, 'Input.dispatchKeyEvent', { type: 'keyUp', ...ESC });
|
||||
sendResponse({ ok: true });
|
||||
} else if (msg.type === 'harvestRouting') {
|
||||
harvestRouting = !!msg.on;
|
||||
sendResponse({ ok: true });
|
||||
} else if (msg.type === 'hardReload') {
|
||||
// Cache-bypass reload — the by-hand fix for a wedged Flow session.
|
||||
if (attached.has(tabId)) { chrome.debugger.detach({ tabId }, () => {}); attached.delete(tabId); }
|
||||
|
||||
94
content.js
94
content.js
@ -209,11 +209,13 @@ function snapshotAssets() {
|
||||
return urls;
|
||||
}
|
||||
|
||||
// Detect Flow's own failure banner so we can skip fast instead of waiting the full timeout.
|
||||
function flowError() {
|
||||
return [...document.querySelectorAll('*')].some(e => visible(e) &&
|
||||
/something went wrong|please try again|couldn'?t generate|failed to generate|generation failed/i.test(e.textContent || '') &&
|
||||
(e.textContent || '').length < 90);
|
||||
// Flow's failure chatter — same lingering problem as the done-messages: old errors
|
||||
// stay visible in the chat history forever. COUNT matches; only a NEW one (count
|
||||
// above the pre-submit baseline) means THIS task errored.
|
||||
function countErrMsgs() {
|
||||
const m = (document.body.innerText || '').match(
|
||||
/something went wrong|please try again|couldn'?t generate|failed to generate|generation failed/gi);
|
||||
return m ? m.length : 0;
|
||||
}
|
||||
|
||||
// The account-level abuse/rate flag ("unusual activity" / "Help Center"). Persistent —
|
||||
@ -255,14 +257,15 @@ function findTryAgain() {
|
||||
// errors constantly under load, so instead of failing we AUTO-CLICK "Try again"
|
||||
// (what works by hand) up to maxRetries, with a cooldown so a lingering banner
|
||||
// doesn't burn all the retries at once. Returns 'asset' | 'error' | null(timeout).
|
||||
async function waitForResultOrError(baseCount, maxMs, maxRetries = 3) {
|
||||
async function waitForResultOrError(baseCount, maxMs, errBase = 0, maxRetries = 3) {
|
||||
const t0 = Date.now();
|
||||
let retries = 0, cooldownUntil = 0;
|
||||
while (Date.now() - t0 < maxMs) {
|
||||
if (snapshotAssets().size > baseCount) return 'asset';
|
||||
if (Date.now() - t0 > 6000 && persistentError()) return 'ratelimit'; // abuse flag: back off, don't retry
|
||||
const now = Date.now();
|
||||
if (now > cooldownUntil) {
|
||||
const newError = countErrMsgs() > errBase; // lingering old banners don't count
|
||||
if (now > cooldownUntil && newError) {
|
||||
const btn = findTryAgain();
|
||||
if (btn) {
|
||||
if (retries >= maxRetries) return 'error'; // gave it maxRetries shots
|
||||
@ -273,7 +276,7 @@ async function waitForResultOrError(baseCount, maxMs, maxRetries = 3) {
|
||||
await sleep(4000);
|
||||
continue;
|
||||
}
|
||||
if (now - t0 > 8000 && flowError()) return 'error'; // errored, no retry offered
|
||||
if (now - t0 > 8000) return 'error'; // errored, no retry offered
|
||||
}
|
||||
await sleep(2500);
|
||||
}
|
||||
@ -377,6 +380,7 @@ async function requeueWithReload(task, why) {
|
||||
}
|
||||
|
||||
async function executeTask(task) {
|
||||
if (task.kind === 'harvest') return executeHarvest(task); // library download pass, no generation
|
||||
generationInProgress = true;
|
||||
chrome.storage.local.set({ status: 'working' });
|
||||
try {
|
||||
@ -385,6 +389,7 @@ async function executeTask(task) {
|
||||
|
||||
const before = snapshotAssets();
|
||||
const doneBase = countDoneMsgs(); // old "finished" chatter lingers — only a NEW one counts
|
||||
const errBase = countErrMsgs(); // ditto for old error banners
|
||||
|
||||
// Soft-detect first: if the chat already echoes this prompt, the PREVIOUS attempt's
|
||||
// submit registered before its reload — re-typing would double-generate. Skip to waiting.
|
||||
@ -423,7 +428,7 @@ async function executeTask(task) {
|
||||
if (snapshotAssets().size > before.size) { signal = 'asset in grid'; break; }
|
||||
if (countDoneMsgs() > doneBase) { signal = 'NEW agent completion message'; break; }
|
||||
if (Date.now() - t0 > 6000 && persistentError()) { await handleRateLimit(task); return; }
|
||||
if (Date.now() - t0 > 8000 && (findTryAgain() || flowError()))
|
||||
if (Date.now() - t0 > 8000 && countErrMsgs() > errBase)
|
||||
return await requeueWithReload(task, 'Flow errored during video generation');
|
||||
await sleep(2500);
|
||||
}
|
||||
@ -440,7 +445,7 @@ async function executeTask(task) {
|
||||
}
|
||||
|
||||
// Images: the grid updates live — wait in-page for a new asset, error, or timeout.
|
||||
const outcome = await waitForResultOrError(before.size, timeoutMs);
|
||||
const outcome = await waitForResultOrError(before.size, timeoutMs, errBase);
|
||||
if (outcome === 'ratelimit') { await handleRateLimit(task); return; } // long back-off, don't hammer
|
||||
if (outcome === 'error') return await requeueWithReload(task, '"Something went wrong" persisted through Try-again');
|
||||
if (outcome !== 'asset') {
|
||||
@ -518,6 +523,75 @@ async function runPostReloadHarvest(ph) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- library harvest pass (queue a task with kind:"harvest" to trigger) ---------
|
||||
// John's by-hand download: hover the tile → ⋮ → Download → 1K (original, instant).
|
||||
// Reproduced with trusted CDP hover/clicks; background routes the downloads into
|
||||
// Downloads/flowrinse/harvest/. Seen-list in storage → resumable, no double-dips.
|
||||
|
||||
const center = (el) => { const r = el.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; };
|
||||
|
||||
function findTileMenuButton(tile) {
|
||||
// controls overlay lives on the tile's card ancestor; ⋮ is labelled more/options,
|
||||
// or failing that the last icon button (screenshot order: ♡ ↻ ⋮)
|
||||
let card = tile;
|
||||
for (let i = 0; i < 3 && card.parentElement; i++) card = card.parentElement;
|
||||
const btns = [...card.querySelectorAll('button, [role="button"]')].filter(visible);
|
||||
return btns.find(b => /more|option|menu/i.test((b.getAttribute('aria-label') || '') + (b.getAttribute('title') || '')))
|
||||
|| btns[btns.length - 1] || null;
|
||||
}
|
||||
|
||||
function findMenuItem(re) {
|
||||
return [...document.querySelectorAll('[role="menu"] [role="menuitem"], [role="menuitem"], [role="menu"] button, [role="dialog"] button')]
|
||||
.filter(visible).find(e => re.test((e.textContent || '').trim()));
|
||||
}
|
||||
|
||||
async function executeHarvest(task) {
|
||||
generationInProgress = true;
|
||||
let got = 0, missed = 0, skipped = 0;
|
||||
try {
|
||||
await waitForAgentReady(30000);
|
||||
const stored = await new Promise(r => chrome.storage.local.get({ harvested_urls: [] }, d => r(d.harvested_urls)));
|
||||
const seen = new Set(stored);
|
||||
await bg({ type: 'harvestRouting', on: true });
|
||||
window.scrollTo(0, 0); await sleep(800);
|
||||
let idle = 0;
|
||||
while (idle < 3 && got + missed < 500) {
|
||||
const tile = [...document.querySelectorAll('a[href*="/edit/"]')]
|
||||
.find(t => visible(t) && !seen.has(t.href.split('#')[0]));
|
||||
if (!tile) { // nothing new in view — page down (grid may virtualize)
|
||||
window.scrollBy(0, innerHeight * 0.8); await sleep(1000); idle++; continue;
|
||||
}
|
||||
idle = 0;
|
||||
const href = tile.href.split('#')[0];
|
||||
tile.scrollIntoView({ block: 'center' }); await sleep(600);
|
||||
await bg({ type: 'hoverAt', ...center(tile) }); await sleep(600);
|
||||
const dots = findTileMenuButton(tile);
|
||||
if (!dots) { missed++; seen.add(href); log('info', `harvest: no ⋮ on a tile — ${gridCensus()}`); continue; }
|
||||
await bg({ type: 'clickAt', ...center(dots) }); await sleep(800);
|
||||
const dl = findMenuItem(/^\s*download\s*$/i);
|
||||
if (!dl) { missed++; seen.add(href); await bg({ type: 'pressEscape' }); await sleep(400); continue; }
|
||||
await bg({ type: 'clickAt', ...center(dl) }); await sleep(800);
|
||||
const oneK = findMenuItem(/1k|original/i);
|
||||
if (!oneK) { missed++; seen.add(href); await bg({ type: 'pressEscape' }); await sleep(400); continue; }
|
||||
await bg({ type: 'clickAt', ...center(oneK) });
|
||||
got++; seen.add(href);
|
||||
chrome.storage.local.set({ harvested_urls: [...seen].slice(-3000) });
|
||||
if (got % 10 === 0) log('info', `harvest: ${got} downloaded so far…`);
|
||||
await sleep(1800 + Math.random() * 1500); // pace — don't machine-gun the account
|
||||
}
|
||||
log('success', `Harvest pass done: ${got} downloaded, ${missed} missed, ${skipped} skipped → Downloads/flowrinse/harvest/`);
|
||||
await reportCompletion(task.id, 'success', null, [`downloaded:${got}`, `missed:${missed}`]);
|
||||
} catch (err) {
|
||||
log('error', `Harvest pass failed after ${got}: ${err.message}`);
|
||||
await reportCompletion(task.id, 'failed', err.message, [`downloaded:${got}`]);
|
||||
} finally {
|
||||
await bg({ type: 'harvestRouting', on: false });
|
||||
window.scrollTo(0, 0);
|
||||
generationInProgress = false;
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
async function reportCompletion(id, status, error, assets) {
|
||||
try {
|
||||
await fetch(`${settings.serverUrl}/task-completed`, {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user