- 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>
97 lines
4.7 KiB
JavaScript
97 lines
4.7 KiB
JavaScript
// Background service worker.
|
|
// * Trusted input via the Chrome DevTools Protocol — Flow's editor ignores synthetic
|
|
// DOM events (isTrusted=false); CDP Input.* events are trusted so it registers them.
|
|
// * Asset download — fetches the harvested media URL with the page's session cookies
|
|
// (host_permission covers labs.google) and hands the bytes to the local server. Runs
|
|
// in the worker, so it's not blocked by the page's CSP.
|
|
|
|
const attached = new Set();
|
|
|
|
function attach(tabId) {
|
|
return new Promise((res, rej) =>
|
|
chrome.debugger.attach({ tabId }, '1.3', () =>
|
|
chrome.runtime.lastError ? rej(chrome.runtime.lastError) : res()));
|
|
}
|
|
function cmd(tabId, method, params) {
|
|
return new Promise((res, rej) =>
|
|
chrome.debugger.sendCommand({ tabId }, method, params || {}, (r) =>
|
|
chrome.runtime.lastError ? rej(chrome.runtime.lastError) : res(r)));
|
|
}
|
|
async function ensure(tabId) {
|
|
if (attached.has(tabId)) return;
|
|
try { await attach(tabId); attached.add(tabId); }
|
|
catch (e) {
|
|
if (/already attached/i.test(String(e.message || e))) attached.add(tabId);
|
|
else throw e;
|
|
}
|
|
}
|
|
|
|
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;
|
|
(async () => {
|
|
try {
|
|
if (msg.type === 'insertText') {
|
|
await ensure(tabId);
|
|
await cmd(tabId, 'Input.insertText', { text: msg.text });
|
|
sendResponse({ ok: true });
|
|
} else if (msg.type === 'pressEnter') {
|
|
await ensure(tabId);
|
|
await cmd(tabId, 'Input.dispatchKeyEvent', { type: 'keyDown', ...ENTER });
|
|
await cmd(tabId, 'Input.dispatchKeyEvent', { type: 'keyUp', ...ENTER });
|
|
sendResponse({ ok: true });
|
|
} else if (msg.type === 'download') {
|
|
// Browser-native download: uses the page's session cookies, follows the media
|
|
// redirect, no CSP/CORS/base64. Lands under the browser's Downloads dir.
|
|
const dlId = await new Promise((res, rej) => chrome.downloads.download(
|
|
{ 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); }
|
|
chrome.tabs.reload(tabId, { bypassCache: true });
|
|
sendResponse({ ok: true });
|
|
} else if (msg.type === 'detach') {
|
|
if (attached.has(tabId)) { chrome.debugger.detach({ tabId }, () => {}); attached.delete(tabId); }
|
|
sendResponse({ ok: true });
|
|
} else {
|
|
sendResponse({ ok: false, error: 'unknown message' });
|
|
}
|
|
} catch (e) {
|
|
sendResponse({ ok: false, error: String(e.message || e) });
|
|
}
|
|
})();
|
|
return true; // async response
|
|
});
|
|
|
|
chrome.debugger.onDetach.addListener((src) => { if (src.tabId) attached.delete(src.tabId); });
|