Reliability improvements: click retry, resume selection, answer loop, browser recovery

- Easy Apply click: click found element directly, retry with force if modal doesn't open
- Resume: select first radio if none checked, fall back to file upload
- AI answers: inject stored answers into formFiller on needs_answer retry
- Answers persistence: reload answers.json before each job for Telegram replies
- Browser recovery: detect dead page, create fresh browser session
- Multiple dialogs: findApplyModal() tags the right dialog among cookie banners etc.
- Select matching: case-insensitive fuzzy match with substring fallback
- dismissModal: scope Discard scan to dialog elements only
- Label dedup: normalize whitespace, fix odd-length edge case
- no_modal status: explicit handleResult case
- Per-job timeout: 10 minutes (was 3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 11:30:09 -08:00
parent 7f8cc3658e
commit 14cf9a12c1
4 changed files with 145 additions and 17 deletions

View File

@@ -135,7 +135,26 @@ async function main() {
break;
}
// Reload answers.json before each job — picks up Telegram replies between jobs
try {
const freshAnswers = existsSync(answersPath) ? loadConfig(answersPath) : [];
formFiller.answers = freshAnswers;
} catch { /* keep existing answers on read error */ }
try {
// If this job previously returned needs_answer and has an AI or user-provided answer,
// inject it into formFiller so the question gets answered on retry
if (job.status === 'needs_answer' && job.pending_question && job.ai_suggested_answer) {
const questionLabel = job.pending_question.label || job.pending_question;
const answer = job.ai_suggested_answer;
// Only inject if not already in answers (avoid duplicates across retries)
const alreadyHas = formFiller.answers.some(a => a.pattern === questionLabel);
if (!alreadyHas) {
formFiller.answers.push({ pattern: questionLabel, answer });
console.log(` Injecting AI answer for "${questionLabel}": "${String(answer).slice(0, 50)}"`);
}
}
// Per-job timeout — prevents a single hung browser from blocking the run
const result = await Promise.race([
applyToJob(browser.page, job, formFiller),
@@ -145,6 +164,24 @@ async function main() {
} catch (e) {
console.error(` ❌ Error: ${e.message}`);
if (e.stack) console.error(` Stack: ${e.stack.split('\n').slice(1, 3).join(' | ').trim()}`);
// Browser crash recovery — check if page is still usable
const pageAlive = await browser.page.evaluate(() => true).catch(() => false);
if (!pageAlive) {
console.log(` Browser session dead — creating new browser`);
await browser.browser?.close().catch(() => {});
try {
const newBrowser = platform === 'external'
? await createBrowser(settings, null)
: await createBrowser(settings, platform);
browser = newBrowser;
console.log(` ✅ New browser session created`);
} catch (browserErr) {
console.error(` ❌ Could not recover browser: ${browserErr.message}`);
break; // can't continue without a browser
}
}
const retries = (job.retry_count || 0) + 1;
if (retries <= maxRetries) {
updateJobStatus(job.id, 'new', { retry_count: retries });
@@ -241,6 +278,7 @@ async function handleResult(job, result, results, settings, profile, apiKey) {
break;
}
case 'no_modal':
case 'skipped_no_apply':
case 'skipped_easy_apply_unsupported':
console.log(` ⏭️ Skipped — ${status}`);