browser-assistant 2.8.4 → 2.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extension/automation-policy.js +19 -0
- package/extension/content.js +101 -7
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
- package/server/main.py +1 -1
|
@@ -379,6 +379,25 @@
|
|
|
379
379
|
const unique = [...new Set(candidates)];
|
|
380
380
|
return unique.length === 1 ? unique[0] : null;
|
|
381
381
|
},
|
|
382
|
+
getPracticeSkip(doc, question) {
|
|
383
|
+
const anchor = question.form || question.container;
|
|
384
|
+
if (!anchor)
|
|
385
|
+
return null;
|
|
386
|
+
const skips = [...doc.querySelectorAll('button,[role="button"]')].filter(button => button.isConnected && button.getClientRects().length && !button.disabled
|
|
387
|
+
&& button.getAttribute('aria-disabled') !== 'true' && !button.closest('#assistant-root')
|
|
388
|
+
&& /^skip(?: question)?$/i.test((button.innerText || button.textContent || button.getAttribute('aria-label') || '').trim()));
|
|
389
|
+
if (skips.length !== 1)
|
|
390
|
+
return null;
|
|
391
|
+
for (let scope = anchor; scope && scope !== doc.body; scope = scope.parentElement) {
|
|
392
|
+
if (!scope.contains(skips[0]))
|
|
393
|
+
continue;
|
|
394
|
+
const submits = [...scope.querySelectorAll('button,[role="button"],input[type="submit"]')].filter(button => button.getClientRects().length && !button.closest('#assistant-root')
|
|
395
|
+
&& /^submit(?: answer)?$/i.test((button.innerText || button.textContent || button.value || button.getAttribute('aria-label') || '').trim()));
|
|
396
|
+
if (submits.length === 1)
|
|
397
|
+
return skips[0];
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
},
|
|
382
401
|
getBlockingReason(doc) {
|
|
383
402
|
const expired = [...doc.querySelectorAll('p,span,div,h1,h2,h3,h4')].some(node => node.getClientRects().length && !node.closest('#assistant-root') && !node.children.length
|
|
384
403
|
&& /^time['’]s up!\s*you cannot attempt this assessment now\.?$/i.test((node.innerText || node.textContent || '').trim()));
|
package/extension/content.js
CHANGED
|
@@ -415,6 +415,7 @@
|
|
|
415
415
|
const currentQuestion = automation.batch?.title || (automation.running && automation.codingQuestionText)
|
|
416
416
|
|| (authorized ? getAuthorizedQuestion(context)?.text : '');
|
|
417
417
|
const pageName = authorized ? context.target.id : window.location.hostname;
|
|
418
|
+
const deferred = automation.deferredQuestions?.size ?? 0;
|
|
418
419
|
if (status) {
|
|
419
420
|
status.textContent = authorized
|
|
420
421
|
? `Build ${extensionVersion} · ${context.target.id}: ${automation.state} · Completed: ${automation.completed}${automation.error ? ` · ${automation.error}` : ''}`
|
|
@@ -431,14 +432,15 @@
|
|
|
431
432
|
if (question)
|
|
432
433
|
question.textContent = currentQuestion || '—';
|
|
433
434
|
if (progress)
|
|
434
|
-
progress.textContent = `${automation.completed} / —`;
|
|
435
|
+
progress.textContent = deferred ? `${automation.completed} completed · ${deferred} deferred` : `${automation.completed} / —`;
|
|
435
436
|
if (latest) {
|
|
437
|
+
const reviewing = ['WAITING_FOR_REVIEW', 'DEFERRING_QUESTION', 'REVIEW_NEEDED'].includes(automation.state);
|
|
436
438
|
latest.textContent = automation.error
|
|
437
|
-
? `⚠ ${automation.error}`
|
|
439
|
+
? reviewing ? automation.error : `⚠ ${automation.error}`
|
|
438
440
|
: authorized
|
|
439
441
|
? `Current status: ${automation.state.replace(/_/g, ' ').toLowerCase()}.`
|
|
440
442
|
: 'Automation is available only on an authorized QA page.';
|
|
441
|
-
latest.classList.toggle('has-error', Boolean(automation.error));
|
|
443
|
+
latest.classList.toggle('has-error', Boolean(automation.error) && !reviewing);
|
|
442
444
|
}
|
|
443
445
|
if (start)
|
|
444
446
|
start.disabled = !authorized || automation.running;
|
|
@@ -477,6 +479,8 @@
|
|
|
477
479
|
automation.paused = false;
|
|
478
480
|
automation.pendingNavigation = false;
|
|
479
481
|
automation.state = reason;
|
|
482
|
+
if (reason === 'REVIEW_NEEDED')
|
|
483
|
+
automation.error = 'Review deferred questions on the portal.';
|
|
480
484
|
updateAutomationUI();
|
|
481
485
|
}
|
|
482
486
|
async function waitWhilePaused(runId) {
|
|
@@ -1178,6 +1182,73 @@
|
|
|
1178
1182
|
stopAutomation('STOPPED');
|
|
1179
1183
|
}
|
|
1180
1184
|
}
|
|
1185
|
+
async function continueAfterUnverifiedQuestion(question, runId, reason, allowSkip = true) {
|
|
1186
|
+
if (!(await waitWhilePaused(runId)))
|
|
1187
|
+
return false;
|
|
1188
|
+
const initial = getAutomationContext();
|
|
1189
|
+
const adapter = getAutomationAdapter(initial);
|
|
1190
|
+
if (!adapter || initial.target.id !== automation.targetId) {
|
|
1191
|
+
stopAutomation('UNAUTHORIZED');
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
const fresh = getAuthorizedQuestion(initial);
|
|
1195
|
+
const sameQuestion = fresh?.signature === question.signature && hasSameQuestionOptions(question, fresh);
|
|
1196
|
+
const practice = initial.target.id === 'ccbp-learning-practice'
|
|
1197
|
+
&& /^\/(?:default-question|primitive-coding(?:-review)?)\/practice\//.test(location.pathname);
|
|
1198
|
+
const hasDraft = fresh?.input?.value?.trim() || fresh?.options.some(option => option.isSelected?.() || option.input?.checked);
|
|
1199
|
+
if (allowSkip && practice && sameQuestion && !hasDraft && question.type !== 'ordering'
|
|
1200
|
+
&& !adapter.isSaving?.(document) && !adapter.getSubmittedAdvance?.(document)
|
|
1201
|
+
&& automationPolicy.canExecute('click', initial) && automationPolicy.canExecute('validate', initial)) {
|
|
1202
|
+
const skip = adapter.getPracticeSkip?.(document, fresh);
|
|
1203
|
+
if (skip) {
|
|
1204
|
+
automation.state = 'DEFERRING_QUESTION';
|
|
1205
|
+
automation.error = reason;
|
|
1206
|
+
automation.pendingNavigation = true;
|
|
1207
|
+
updateAutomationUI();
|
|
1208
|
+
skip.click();
|
|
1209
|
+
const transition = await waitForPracticeTransition(question.signature, runId);
|
|
1210
|
+
if (!automation.running || automation.runId !== runId)
|
|
1211
|
+
return false;
|
|
1212
|
+
automation.pendingNavigation = false;
|
|
1213
|
+
if (transition === 'next' || transition === 'complete') {
|
|
1214
|
+
automation.deferredQuestions.add(question.signature);
|
|
1215
|
+
automation.error = '';
|
|
1216
|
+
return true;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
automation.state = 'WAITING_FOR_REVIEW';
|
|
1221
|
+
automation.error = `${reason} Use the page controls to review and move to the next question. Automation will continue here.`;
|
|
1222
|
+
updateAutomationUI();
|
|
1223
|
+
while (await waitWhilePaused(runId)) {
|
|
1224
|
+
if (automation.state !== 'WAITING_FOR_REVIEW') {
|
|
1225
|
+
automation.state = 'WAITING_FOR_REVIEW';
|
|
1226
|
+
updateAutomationUI();
|
|
1227
|
+
}
|
|
1228
|
+
const context = getAutomationContext();
|
|
1229
|
+
const currentAdapter = getAutomationAdapter(context);
|
|
1230
|
+
if (context.mode !== 'FULL' || context.target.id !== initial.target.id || currentAdapter?.id !== adapter.id) {
|
|
1231
|
+
stopAutomation('UNAUTHORIZED');
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
const blocked = currentAdapter.getBlockingReason?.(document);
|
|
1235
|
+
if (blocked) {
|
|
1236
|
+
automation.error = blocked;
|
|
1237
|
+
stopAutomation('PAUSED');
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
if (!currentAdapter.isSaving?.(document)) {
|
|
1241
|
+
const current = getAuthorizedQuestion(context);
|
|
1242
|
+
if (currentAdapter.getSubmittedAdvance?.(document) || currentAdapter.isComplete(document)
|
|
1243
|
+
|| (current && current.signature !== question.signature && !hasSameQuestionOptions(question, current))) {
|
|
1244
|
+
automation.error = '';
|
|
1245
|
+
return true;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1249
|
+
}
|
|
1250
|
+
return false;
|
|
1251
|
+
}
|
|
1181
1252
|
async function startPracticeAutomation() {
|
|
1182
1253
|
if (window.BrowserAssistantJournal?.matches()) {
|
|
1183
1254
|
await window.BrowserAssistantJournal.start();
|
|
@@ -1230,6 +1301,7 @@
|
|
|
1230
1301
|
automation.paused = false;
|
|
1231
1302
|
automation.error = '';
|
|
1232
1303
|
automation.completed = 0;
|
|
1304
|
+
automation.deferredQuestions = new Set();
|
|
1233
1305
|
automation.targetId = initialContext.target.id;
|
|
1234
1306
|
automation.pendingNavigation = false;
|
|
1235
1307
|
automation.currentSignature = null;
|
|
@@ -1286,10 +1358,12 @@
|
|
|
1286
1358
|
const transition = await waitForPracticeTransition(previousSignature, runId);
|
|
1287
1359
|
if (!automation.running || automation.runId !== runId)
|
|
1288
1360
|
break;
|
|
1289
|
-
if (transition === 'next' || transition === 'complete')
|
|
1361
|
+
if (transition === 'next' || transition === 'complete') {
|
|
1290
1362
|
automation.completed += 1;
|
|
1363
|
+
automation.deferredQuestions.delete(previousSignature);
|
|
1364
|
+
}
|
|
1291
1365
|
if (transition === 'complete') {
|
|
1292
|
-
stopAutomation('COMPLETED');
|
|
1366
|
+
stopAutomation(automation.deferredQuestions.size ? 'REVIEW_NEEDED' : 'COMPLETED');
|
|
1293
1367
|
break;
|
|
1294
1368
|
}
|
|
1295
1369
|
if (transition !== 'next') {
|
|
@@ -1300,6 +1374,10 @@
|
|
|
1300
1374
|
continue;
|
|
1301
1375
|
}
|
|
1302
1376
|
if (currentAdapter.isComplete(document)) {
|
|
1377
|
+
if (automation.deferredQuestions.size) {
|
|
1378
|
+
stopAutomation('REVIEW_NEEDED');
|
|
1379
|
+
break;
|
|
1380
|
+
}
|
|
1303
1381
|
const nextPractice = currentAdapter.getNextPractice?.(document);
|
|
1304
1382
|
if (!nextPractice) {
|
|
1305
1383
|
stopAutomation('COMPLETED');
|
|
@@ -1341,6 +1419,12 @@
|
|
|
1341
1419
|
await runCodingAutomation(question, runId);
|
|
1342
1420
|
break;
|
|
1343
1421
|
}
|
|
1422
|
+
if (automation.deferredQuestions.has(question.signature)) {
|
|
1423
|
+
automation.currentSignature = question.signature;
|
|
1424
|
+
if (await continueAfterUnverifiedQuestion(question, runId, 'This question was deferred earlier.', false))
|
|
1425
|
+
continue;
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1344
1428
|
if (automation.processed.has(question.signature)) {
|
|
1345
1429
|
automation.error = 'Repeated question detected.';
|
|
1346
1430
|
stopAutomation('PAUSED');
|
|
@@ -1403,7 +1487,12 @@
|
|
|
1403
1487
|
if (!automation.running || automation.runId !== runId)
|
|
1404
1488
|
break;
|
|
1405
1489
|
automation.error = error.message || 'Local AI server unavailable.';
|
|
1406
|
-
|
|
1490
|
+
if (['answer_unconfirmed', 'vision_unconfirmed'].includes(error.code)) {
|
|
1491
|
+
if (await continueAfterUnverifiedQuestion(question, runId, automation.error))
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
else
|
|
1495
|
+
stopAutomation('ERROR');
|
|
1407
1496
|
break;
|
|
1408
1497
|
}
|
|
1409
1498
|
if (!automation.running || automation.runId !== runId || !response)
|
|
@@ -1474,6 +1563,11 @@
|
|
|
1474
1563
|
// transient code-block markup.
|
|
1475
1564
|
if (!answerIndexes.length || !current || !hasSameQuestionOptions(question, current)) {
|
|
1476
1565
|
automation.error = !answerIndexes.length ? 'The verified answer did not match the displayed options exactly.' : !current ? 'Question controls disappeared while solving.' : 'Question text or options changed while solving. Please retry.';
|
|
1566
|
+
if (!answerIndexes.length && current?.signature === question.signature && hasSameQuestionOptions(question, current)) {
|
|
1567
|
+
if (await continueAfterUnverifiedQuestion(question, runId, automation.error))
|
|
1568
|
+
continue;
|
|
1569
|
+
break;
|
|
1570
|
+
}
|
|
1477
1571
|
stopAutomation('PAUSED');
|
|
1478
1572
|
break;
|
|
1479
1573
|
}
|
|
@@ -1590,7 +1684,7 @@
|
|
|
1590
1684
|
}
|
|
1591
1685
|
if (transition === 'complete') {
|
|
1592
1686
|
automation.completed += 1;
|
|
1593
|
-
stopAutomation('COMPLETED');
|
|
1687
|
+
stopAutomation(automation.deferredQuestions.size ? 'REVIEW_NEEDED' : 'COMPLETED');
|
|
1594
1688
|
break;
|
|
1595
1689
|
}
|
|
1596
1690
|
if (transition !== 'next') {
|
package/extension/manifest.json
CHANGED
package/package.json
CHANGED
package/server/main.py
CHANGED
|
@@ -190,7 +190,7 @@ async def lifespan(_app: FastAPI):
|
|
|
190
190
|
client.close()
|
|
191
191
|
|
|
192
192
|
|
|
193
|
-
app = FastAPI(title="Browser Assistant API", version="2.8.
|
|
193
|
+
app = FastAPI(title="Browser Assistant API", version="2.8.5", lifespan=lifespan)
|
|
194
194
|
install_local_guards(app)
|
|
195
195
|
|
|
196
196
|
@app.exception_handler(HTTPException)
|