Improve IK-style artifact scanner pipeline
This commit is contained in:
+315
-58
@@ -1,4 +1,4 @@
|
||||
import type { BooleanResult, CaptureResult, ClickResult, AutomationGuard, ScrollResult } from "../types/global";
|
||||
import type { BooleanResult, CaptureOptions, CaptureResult, ClickResult, AutomationGuard, ScrollResult } from "../types/global";
|
||||
import type { ParsedArtifactCandidate } from "./artifactOcrParser";
|
||||
import { sessionSignature } from "./artifactStore";
|
||||
import { buildGridModel, buildInventoryPagePlan, type GridTarget } from "./automationPlanner";
|
||||
@@ -6,7 +6,8 @@ import { classifyAutoScanCapture, shouldAbortAfterConsecutiveMisses } from "./au
|
||||
import { waitForCardReady } from "./cardReadyGate";
|
||||
import { captureRejectionReason, captureSourceRejectionReason, shouldPersistParsedArtifact } from "./scannerCaptureQuality";
|
||||
import type { AutoScanStats, ScanSummary } from "./scannerSession";
|
||||
import { clampSkipRows, emptyAutoScanStats, resolveScanTargetCount } from "./scannerSession";
|
||||
import { addCaptureTiming, addCardReadyTiming, addScrollReadyTiming, clampSkipRows, emptyAutoScanStats, resolveScanTargetCount, updateScanTiming } from "./scannerSession";
|
||||
import { validateAutoScanEntryPreflight } from "./autoScanEntry";
|
||||
|
||||
// Simplified to match Inventory Kamera's proven approach (see docs/DECISIONS.md
|
||||
// ADR-007): one click per tile, a fixed settle delay, one retry if the detail
|
||||
@@ -22,8 +23,8 @@ type AutoScanApi = {
|
||||
|
||||
export type AutoScanLoopDependencies = {
|
||||
api: AutoScanApi;
|
||||
captureSelectedSource: (delayMs?: number, focusGenshin?: boolean) => Promise<CaptureResult | null>;
|
||||
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean) => Promise<CaptureResult | null>;
|
||||
captureSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
|
||||
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
|
||||
parseArtifact: (capture: CaptureResult | null) => ParsedArtifactCandidate | null;
|
||||
persistParsedArtifact: (
|
||||
capture: CaptureResult | null,
|
||||
@@ -49,6 +50,9 @@ export type AutoScanLoopOptions = {
|
||||
scanLimit: number;
|
||||
skipRows: number;
|
||||
detectedInventoryCount?: number | null;
|
||||
processInitialSelection?: boolean;
|
||||
skipInitialGridTarget?: boolean;
|
||||
ocrEngine?: CaptureOptions["ocrEngine"];
|
||||
};
|
||||
|
||||
export type AutoScanLoopResult = {
|
||||
@@ -62,9 +66,14 @@ export type AutoScanLoopResult = {
|
||||
|
||||
// Card-ready gating replaces a fixed settle delay: poll the detail fingerprint
|
||||
// until it has changed and stabilized (or the budget is spent). See cardReadyGate.
|
||||
const CARD_READY_MAX_MS = 900;
|
||||
const CARD_READY_POLL_MS = 90;
|
||||
const CARD_READY_MAX_MS = 420;
|
||||
const CARD_READY_POLL_MS = 60;
|
||||
const CARD_READY_STABLE_SAMPLES = 2;
|
||||
const CARD_READY_ACCEPT_CHANGED_AFTER_MS = 200;
|
||||
const SCROLL_READY_MAX_MS = 760;
|
||||
const SCROLL_READY_POLL_MS = 80;
|
||||
const SCROLL_READY_STABLE_SAMPLES = 2;
|
||||
const SCROLL_READY_ACCEPT_CHANGED_AFTER_MS = 100;
|
||||
const MISS_ABORT_THRESHOLD = 3;
|
||||
const UNREADABLE_ABORT_THRESHOLD = 5;
|
||||
|
||||
@@ -89,28 +98,66 @@ export async function runAutoScanLoop(
|
||||
} = deps;
|
||||
|
||||
const stats: AutoScanStats = { ...emptyAutoScanStats };
|
||||
const startedAt = Date.now();
|
||||
const maxTargets = resolveScanTargetCount(options.scanLimit, options.detectedInventoryCount);
|
||||
const rowsToSkip = clampSkipRows(options.skipRows);
|
||||
const seen = new Set<string>();
|
||||
const seenDetailFingerprints = new Set<string>();
|
||||
const seenPageFingerprints = new Set<string>();
|
||||
let page = 0;
|
||||
let blockedReason = "";
|
||||
let aborted = false;
|
||||
let consecutiveMisses = 0;
|
||||
let rowsQueued = 0;
|
||||
const primaryScreenStartWarning =
|
||||
"Start-Capture ist vom Primary-Screen, kein spezifischer Genshin-Client-Marker vorhanden - Auto-Scan wird mit Vorsicht fortgesetzt.";
|
||||
|
||||
function updateStats() {
|
||||
let writeQueue: Promise<void> = Promise.resolve();
|
||||
function updateStats(preserveActiveScanMs = false) {
|
||||
updateScanTiming(stats, startedAt, Date.now(), { preserveActiveScanMs });
|
||||
setAutoScanStats({ ...stats });
|
||||
}
|
||||
|
||||
async function saveAutomaticReviewSample(capture: CaptureResult | null, parsed: ParsedArtifactCandidate | null, reason: string) {
|
||||
const saved = await saveReviewSample(capture, parsed, reason);
|
||||
if (saved?.ok) {
|
||||
stats.review++;
|
||||
updateStats();
|
||||
}
|
||||
function enqueueWrite(label: string, task: () => Promise<void>) {
|
||||
writeQueue = writeQueue
|
||||
.catch(() => undefined)
|
||||
.then(async () => {
|
||||
try {
|
||||
await task();
|
||||
} catch (error) {
|
||||
appendAutomationLog(`write failed ${label}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function flushWrites() {
|
||||
await writeQueue.catch(() => undefined);
|
||||
updateStats(true);
|
||||
}
|
||||
|
||||
async function finish(result: AutoScanLoopResult) {
|
||||
const flushStartedAt = Date.now();
|
||||
stats.activeScanMs = Math.max(0, flushStartedAt - startedAt);
|
||||
await flushWrites();
|
||||
stats.writeFlushMs += Math.max(0, Date.now() - flushStartedAt);
|
||||
updateStats(true);
|
||||
return { ...result, stats: { ...stats } };
|
||||
}
|
||||
|
||||
function saveAutomaticReviewSample(capture: CaptureResult | null, parsed: ParsedArtifactCandidate | null, reason: string) {
|
||||
enqueueWrite(`review:${reason}`, async () => {
|
||||
const saved = await saveReviewSample(capture, parsed, reason);
|
||||
if (saved?.ok) {
|
||||
stats.review++;
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function persistArtifactLater(capture: CaptureResult | null, parsed: ParsedArtifactCandidate, source: string, needsReview: boolean) {
|
||||
enqueueWrite(`persist:${source}:${parsed.name}`, async () => {
|
||||
if (await persistParsedArtifact(capture, parsed, source, needsReview)) {
|
||||
stats.stored++;
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function checkGuard() {
|
||||
@@ -154,41 +201,183 @@ export async function runAutoScanLoop(
|
||||
return clickResult;
|
||||
}
|
||||
|
||||
let currentCapture = await captureSelectedSource(0, true);
|
||||
const isPrimaryCapture = currentCapture?.captureTarget === "primary-screen";
|
||||
const initialCaptureRejection = isPrimaryCapture ? "" : captureSourceRejectionReason(currentCapture);
|
||||
function reportedClickDeliveryFailure(result: ClickResult) {
|
||||
return result.moved === false || result.clicked === false;
|
||||
}
|
||||
|
||||
function clickDeliveryFailureReason(target: GridTarget, clickResult: ClickResult) {
|
||||
if (clickResult.inputBlocked) {
|
||||
return "Windows blockiert die Eingabe (UIPI). Starte die App als Administrator (Scanner Diagnose > 'App als Administrator neu starten').";
|
||||
}
|
||||
if (clickResult.isElevated === false) {
|
||||
return `Klick kam nicht an (Ziel ${target.x},${target.y}). Starte die App als Administrator und versuche es erneut.`;
|
||||
}
|
||||
const cursor = `${clickResult.cursorX ?? "?"},${clickResult.cursorY ?? "?"}`;
|
||||
return `Cursor kam nicht am Klick-Ziel ${target.x},${target.y} an (Cursor ${cursor}). Genshin im Vordergrund lassen und erneut versuchen.`;
|
||||
}
|
||||
|
||||
let currentCapture = await captureFastSelectedSource(0, false, {
|
||||
omitFullFrame: true,
|
||||
omitDetailPreview: true,
|
||||
omitInventoryPreview: true,
|
||||
omitCrops: true,
|
||||
omitCropImages: true,
|
||||
omitLockState: true,
|
||||
});
|
||||
const initialSurfaceRejection = validateAutoScanEntryPreflight(currentCapture);
|
||||
const initialCaptureRejection = initialSurfaceRejection.ok ? captureSourceRejectionReason(currentCapture) : initialSurfaceRejection.reason;
|
||||
let gridModel = buildGridModel(currentCapture?.inventoryGrid);
|
||||
|
||||
if (initialCaptureRejection || !gridModel || gridModel.targets.length === 0) {
|
||||
const reason = initialCaptureRejection || "Kein verlaessliches Kachel-Grid erkannt. Artifact-Inventar sichtbar lassen und Smart Capture einmal ausfuehren.";
|
||||
setReviewStatus(reason);
|
||||
return { status: "blocked", stats, blockedReason: reason, pageCount: 0, gridLabel: reason, targetCount: maxTargets };
|
||||
}
|
||||
|
||||
if (isPrimaryCapture) {
|
||||
appendAutomationLog(primaryScreenStartWarning);
|
||||
return finish({ status: "blocked", stats, blockedReason: reason, pageCount: 0, gridLabel: reason, targetCount: maxTargets });
|
||||
}
|
||||
|
||||
let lastDetailSignature = "";
|
||||
const initialParsed = parseArtifact(currentCapture);
|
||||
if (initialParsed) lastDetailSignature = sessionSignature(initialParsed);
|
||||
let lastDetailViewFingerprint = detailFingerprint(currentCapture);
|
||||
if (lastDetailViewFingerprint) seenDetailFingerprints.add(lastDetailViewFingerprint);
|
||||
|
||||
const shouldSkipInitialGridTarget = Boolean(options.processInitialSelection && options.skipInitialGridTarget);
|
||||
let initialProcessedOffset = 0;
|
||||
let initialSelectionDuplicateSkipped = false;
|
||||
if (options.processInitialSelection) {
|
||||
const initialCapture = await captureSelectedSource(0, false, {
|
||||
ocrMode: "artifact",
|
||||
ocrProfile: "fast",
|
||||
...(options.ocrEngine ? { ocrEngine: options.ocrEngine } : {}),
|
||||
omitFullFrame: true,
|
||||
omitInventoryPreview: true,
|
||||
omitCropImages: true,
|
||||
omitEquippedOcr: true,
|
||||
skipOcrUnlessArtifactDetail: true,
|
||||
});
|
||||
const initialSurfaceRejection = validateAutoScanEntryPreflight(initialCapture);
|
||||
if (!initialSurfaceRejection.ok) {
|
||||
return finish({
|
||||
status: "blocked",
|
||||
stats,
|
||||
blockedReason: initialSurfaceRejection.reason,
|
||||
pageCount: 0,
|
||||
gridLabel: initialSurfaceRejection.reason,
|
||||
targetCount: maxTargets,
|
||||
});
|
||||
}
|
||||
if (!initialCapture) {
|
||||
return finish({
|
||||
status: "blocked",
|
||||
stats,
|
||||
blockedReason: "Keine Capture-Daten fuer die initiale Artifact-Auswahl.",
|
||||
pageCount: 0,
|
||||
gridLabel: "Keine Capture-Daten fuer die initiale Artifact-Auswahl.",
|
||||
targetCount: maxTargets,
|
||||
});
|
||||
}
|
||||
|
||||
const parsed = parseArtifact(initialCapture);
|
||||
const rejection = captureRejectionReason(initialCapture, parsed);
|
||||
if (rejection || !parsed) {
|
||||
saveAutomaticReviewSample(initialCapture, parsed, `automatic:initial-selection-rejected`);
|
||||
stats.verified++;
|
||||
stats.misses++;
|
||||
addCaptureTiming(stats, initialCapture.timings);
|
||||
initialProcessedOffset = shouldSkipInitialGridTarget ? 1 : 0;
|
||||
lastDetailViewFingerprint = detailFingerprint(initialCapture);
|
||||
updateStats();
|
||||
appendAutomationLog(`initial selection review: ${rejection || "kein Artifact lesbar"}; scan continues with next tile`);
|
||||
} else {
|
||||
stats.verified++;
|
||||
stats.parsed++;
|
||||
addCaptureTiming(stats, initialCapture.timings);
|
||||
initialProcessedOffset = shouldSkipInitialGridTarget ? 1 : 0;
|
||||
const signature = sessionSignature(parsed);
|
||||
seen.add(signature);
|
||||
lastDetailSignature = signature;
|
||||
lastDetailViewFingerprint = detailFingerprint(initialCapture);
|
||||
const reason = getAutoReviewReason(initialCapture, parsed);
|
||||
const needsReview = reason ? true : shouldFlagArtifactForReview(parsed);
|
||||
if (reason) saveAutomaticReviewSample(initialCapture, parsed, `automatic:${reason}:initial-selection`);
|
||||
persistArtifactLater(initialCapture, parsed, "auto-scan-initial", needsReview);
|
||||
updateStats();
|
||||
appendAutomationLog(`initial selection parsed: ${parsed.name}`);
|
||||
if (stats.parsed >= maxTargets) {
|
||||
return finish({
|
||||
status: "done",
|
||||
stats,
|
||||
blockedReason: "",
|
||||
pageCount: 1,
|
||||
gridLabel: `Initial ausgewaehltes Artifact verarbeitet, Ziel ${maxTargets} Artifacts`,
|
||||
targetCount: maxTargets,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function awaitCardReady() {
|
||||
return waitForCardReady(
|
||||
const startedAt = Date.now();
|
||||
const result = await waitForCardReady(
|
||||
{
|
||||
sampleFingerprint: async () => detailFingerprint(await captureFastSelectedSource(0, true)),
|
||||
sampleFingerprint: async () => detailFingerprint(await captureFastSelectedSource(0, false, {
|
||||
omitFullFrame: true,
|
||||
omitDetailPreview: true,
|
||||
omitInventoryPreview: true,
|
||||
omitCrops: true,
|
||||
omitCropImages: true,
|
||||
omitLockState: true,
|
||||
})),
|
||||
wait,
|
||||
now: () => Date.now(),
|
||||
checkAbort: checkGuard,
|
||||
},
|
||||
lastDetailViewFingerprint,
|
||||
{ minStableSamples: CARD_READY_STABLE_SAMPLES, maxWaitMs: CARD_READY_MAX_MS, pollIntervalMs: CARD_READY_POLL_MS },
|
||||
{
|
||||
minStableSamples: CARD_READY_STABLE_SAMPLES,
|
||||
maxWaitMs: CARD_READY_MAX_MS,
|
||||
pollIntervalMs: CARD_READY_POLL_MS,
|
||||
acceptChangedAfterMs: CARD_READY_ACCEPT_CHANGED_AFTER_MS,
|
||||
},
|
||||
);
|
||||
addCardReadyTiming(stats, Date.now() - startedAt);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function awaitInventoryPageReady(previousFingerprint: string): Promise<{
|
||||
ready: Awaited<ReturnType<typeof waitForCardReady>>;
|
||||
capture: CaptureResult | null;
|
||||
}> {
|
||||
let latestCapture: CaptureResult | null = null;
|
||||
const startedAt = Date.now();
|
||||
const ready = await waitForCardReady(
|
||||
{
|
||||
sampleFingerprint: async () => {
|
||||
latestCapture = await captureFastSelectedSource(0, false, {
|
||||
omitFullFrame: true,
|
||||
omitDetailPreview: true,
|
||||
omitInventoryPreview: true,
|
||||
omitCrops: true,
|
||||
omitCropImages: true,
|
||||
omitLockState: true,
|
||||
});
|
||||
return screenFingerprint(latestCapture);
|
||||
},
|
||||
wait,
|
||||
now: () => Date.now(),
|
||||
checkAbort: checkGuard,
|
||||
},
|
||||
previousFingerprint,
|
||||
{
|
||||
minStableSamples: SCROLL_READY_STABLE_SAMPLES,
|
||||
maxWaitMs: SCROLL_READY_MAX_MS,
|
||||
pollIntervalMs: SCROLL_READY_POLL_MS,
|
||||
acceptChangedAfterMs: SCROLL_READY_ACCEPT_CHANGED_AFTER_MS,
|
||||
},
|
||||
);
|
||||
addScrollReadyTiming(stats, Date.now() - startedAt);
|
||||
return { ready, capture: latestCapture };
|
||||
}
|
||||
|
||||
try {
|
||||
while (!blockedReason && !shouldStop() && stats.clicked < maxTargets) {
|
||||
while (!blockedReason && !shouldStop() && stats.parsed < maxTargets) {
|
||||
page++;
|
||||
stats.pages = page;
|
||||
updateStats();
|
||||
@@ -207,21 +396,23 @@ export async function runAutoScanLoop(
|
||||
cols: gridModel.cols,
|
||||
rows: Math.max(1, gridModel.rows - pageSkipRows),
|
||||
totalTargetCount: maxTargets,
|
||||
processedTargets: stats.clicked,
|
||||
processedTargets: stats.clicked + initialProcessedOffset,
|
||||
rowsQueued,
|
||||
});
|
||||
const targets = pagePlan.pageTargets;
|
||||
const targets = shouldSkipInitialGridTarget && page === 1
|
||||
? baseTargets.slice(initialProcessedOffset)
|
||||
: pagePlan.pageTargets;
|
||||
|
||||
if (targets.length === 0) {
|
||||
blockedReason = `Keine Klick-Ziele nach dem Skippen von ${pageSkipRows} Zeile(n) auf Seite ${page}.`;
|
||||
break;
|
||||
}
|
||||
|
||||
setReviewStatus(`Automatischer Scan Seite ${page}: ${gridModel.cols} x ${gridModel.rows} Raster (${gridModel.source}, ${gridModel.confidence}%), ${targets.length} Klick-Ziele, ${stats.clicked}/${maxTargets} geklickt.`);
|
||||
setReviewStatus(`Automatischer Scan Seite ${page}: ${gridModel.cols} x ${gridModel.rows} Raster (${gridModel.source}, ${gridModel.confidence}%), ${targets.length} Klick-Ziele, ${stats.parsed}/${maxTargets} gelesen.`);
|
||||
let newArtifactsOnPage = 0;
|
||||
|
||||
for (const target of targets) {
|
||||
if (shouldStop() || stats.clicked >= maxTargets) break;
|
||||
if (shouldStop() || stats.parsed >= maxTargets) break;
|
||||
|
||||
const guardReason = await checkGuard();
|
||||
if (guardReason) {
|
||||
@@ -238,16 +429,13 @@ export async function runAutoScanLoop(
|
||||
break;
|
||||
}
|
||||
|
||||
if (clickResult.moved === false || clickResult.clicked === false) {
|
||||
// A structural failure (cursor could not be placed, or SendInput
|
||||
// was rejected outright) means clicks are not reaching Genshin at
|
||||
// all - almost always an elevation mismatch. Abort immediately
|
||||
// instead of clicking blindly through the rest of the inventory.
|
||||
blockedReason = clickResult.inputBlocked
|
||||
? "Windows blockiert die Eingabe (UIPI). Starte die App als Administrator (Scanner Diagnose > 'App als Administrator neu starten')."
|
||||
: `Klick kam nicht an (Ziel ${target.x},${target.y}). Starte die App als Administrator und versuche es erneut.`;
|
||||
if (clickResult.inputBlocked) {
|
||||
blockedReason = clickDeliveryFailureReason(target, clickResult);
|
||||
break;
|
||||
}
|
||||
if (reportedClickDeliveryFailure(clickResult)) {
|
||||
appendAutomationLog(`warn r${target.row} c${target.col}: helper reported cursor/click miss; verifying detail change`);
|
||||
}
|
||||
|
||||
let ready = await awaitCardReady();
|
||||
if (ready.abortReason) {
|
||||
@@ -258,6 +446,14 @@ export async function runAutoScanLoop(
|
||||
let changedDetail = ready.changed;
|
||||
|
||||
if (!changedDetail) {
|
||||
if (options.processInitialSelection && !initialSelectionDuplicateSkipped && !reportedClickDeliveryFailure(clickResult)) {
|
||||
initialSelectionDuplicateSkipped = true;
|
||||
consecutiveMisses = 0;
|
||||
stats.duplicates++;
|
||||
updateStats();
|
||||
appendAutomationLog(`duplicate selected tile r${target.row} c${target.col}: already processed initial detail`);
|
||||
continue;
|
||||
}
|
||||
appendAutomationLog(`retry r${target.row} c${target.col}: Detailansicht unveraendert`);
|
||||
clickResult = await clickTarget(target, "retry");
|
||||
stopReason = inputStopReason(clickResult);
|
||||
@@ -266,6 +462,13 @@ export async function runAutoScanLoop(
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
if (clickResult.inputBlocked) {
|
||||
blockedReason = clickDeliveryFailureReason(target, clickResult);
|
||||
break;
|
||||
}
|
||||
if (reportedClickDeliveryFailure(clickResult)) {
|
||||
appendAutomationLog(`warn r${target.row} c${target.col}: retry helper reported cursor/click miss; verifying detail change`);
|
||||
}
|
||||
ready = await awaitCardReady();
|
||||
if (ready.abortReason) {
|
||||
blockedReason = ready.abortReason;
|
||||
@@ -276,6 +479,18 @@ export async function runAutoScanLoop(
|
||||
}
|
||||
|
||||
if (!changedDetail) {
|
||||
if (reportedClickDeliveryFailure(clickResult)) {
|
||||
blockedReason = clickDeliveryFailureReason(target, clickResult);
|
||||
break;
|
||||
}
|
||||
if (options.processInitialSelection && !initialSelectionDuplicateSkipped) {
|
||||
initialSelectionDuplicateSkipped = true;
|
||||
consecutiveMisses = 0;
|
||||
stats.duplicates++;
|
||||
updateStats();
|
||||
appendAutomationLog(`duplicate selected tile r${target.row} c${target.col}: already processed initial detail`);
|
||||
continue;
|
||||
}
|
||||
stats.misses++;
|
||||
consecutiveMisses++;
|
||||
updateStats();
|
||||
@@ -289,8 +504,36 @@ export async function runAutoScanLoop(
|
||||
|
||||
stats.verified++;
|
||||
|
||||
const capture = await captureSelectedSource(0, true);
|
||||
if (ready.fingerprint) {
|
||||
if (seenDetailFingerprints.has(ready.fingerprint)) {
|
||||
consecutiveMisses = 0;
|
||||
stats.duplicates++;
|
||||
lastDetailViewFingerprint = ready.fingerprint;
|
||||
updateStats();
|
||||
appendAutomationLog(`duplicate visual r${target.row} c${target.col}: OCR uebersprungen`);
|
||||
continue;
|
||||
}
|
||||
seenDetailFingerprints.add(ready.fingerprint);
|
||||
}
|
||||
|
||||
const capture = await captureSelectedSource(0, false, {
|
||||
ocrMode: "artifact",
|
||||
ocrProfile: "fast",
|
||||
...(options.ocrEngine ? { ocrEngine: options.ocrEngine } : {}),
|
||||
omitFullFrame: true,
|
||||
omitInventoryPreview: true,
|
||||
omitCropImages: true,
|
||||
omitEquippedOcr: true,
|
||||
skipOcrUnlessArtifactDetail: true,
|
||||
});
|
||||
const captureSurfaceRejection = validateAutoScanEntryPreflight(capture);
|
||||
if (!captureSurfaceRejection.ok) {
|
||||
blockedReason = captureSurfaceRejection.reason;
|
||||
appendAutomationLog(`blocked r${target.row} c${target.col}: ${blockedReason}`);
|
||||
break;
|
||||
}
|
||||
if (capture?.ocrTimedOut) {
|
||||
addCaptureTiming(stats, capture.timings);
|
||||
stats.misses++;
|
||||
consecutiveMisses++;
|
||||
lastDetailViewFingerprint = detailFingerprint(capture);
|
||||
@@ -305,9 +548,10 @@ export async function runAutoScanLoop(
|
||||
|
||||
const parsed = parseArtifact(capture);
|
||||
const rejection = captureRejectionReason(capture, parsed);
|
||||
addCaptureTiming(stats, capture?.timings);
|
||||
|
||||
if (rejection) {
|
||||
await saveAutomaticReviewSample(capture, parsed, `automatic:capture-rejected:p${page}:r${target.row}c${target.col}`);
|
||||
saveAutomaticReviewSample(capture, parsed, `automatic:capture-rejected:p${page}:r${target.row}c${target.col}`);
|
||||
if (parsed && shouldPersistParsedArtifact(parsed, true)) {
|
||||
consecutiveMisses = 0;
|
||||
const signature = sessionSignature(parsed);
|
||||
@@ -316,7 +560,7 @@ export async function runAutoScanLoop(
|
||||
lastDetailViewFingerprint = detailFingerprint(capture);
|
||||
seen.add(signature);
|
||||
newArtifactsOnPage++;
|
||||
if (await persistParsedArtifact(capture, parsed, "auto-scan-review", true)) stats.stored++;
|
||||
persistArtifactLater(capture, parsed, "auto-scan-review", true);
|
||||
updateStats();
|
||||
continue;
|
||||
}
|
||||
@@ -375,9 +619,9 @@ export async function runAutoScanLoop(
|
||||
const reason = getAutoReviewReason(capture, parsed);
|
||||
const needsReview = reason ? true : shouldFlagArtifactForReview(parsed);
|
||||
if (reason) {
|
||||
await saveAutomaticReviewSample(capture, parsed, `automatic:${reason}:p${page}:r${target.row}c${target.col}`);
|
||||
saveAutomaticReviewSample(capture, parsed, `automatic:${reason}:p${page}:r${target.row}c${target.col}`);
|
||||
}
|
||||
if (await persistParsedArtifact(capture, parsed, "auto-scan", needsReview)) stats.stored++;
|
||||
persistArtifactLater(capture, parsed, "auto-scan", needsReview);
|
||||
updateStats();
|
||||
}
|
||||
|
||||
@@ -386,12 +630,12 @@ export async function runAutoScanLoop(
|
||||
cols: gridModel.cols,
|
||||
rows: Math.max(1, gridModel.rows - pageSkipRows),
|
||||
totalTargetCount: maxTargets,
|
||||
processedTargets: stats.clicked,
|
||||
processedTargets: stats.clicked + initialProcessedOffset,
|
||||
rowsQueued,
|
||||
});
|
||||
rowsQueued = endOfPagePlan.rowsQueuedAfterPage;
|
||||
|
||||
if (aborted || stats.clicked >= maxTargets || shouldStop() || blockedReason) break;
|
||||
if (aborted || stats.parsed >= maxTargets || shouldStop() || blockedReason) break;
|
||||
|
||||
if (newArtifactsOnPage === 0 && page > 1) {
|
||||
blockedReason = `Seite ${page} hat keine neuen Artifacts geliefert; gestoppt, um nicht dieselbe Seite zu loopen.`;
|
||||
@@ -425,16 +669,26 @@ export async function runAutoScanLoop(
|
||||
}
|
||||
}
|
||||
|
||||
const scrollWaitStop = await waitDuringScan(760);
|
||||
if (scrollWaitStop) {
|
||||
blockedReason = scrollWaitStop;
|
||||
const beforeScrollFingerprint = currentPageFingerprint || screenFingerprint(currentCapture);
|
||||
const scrollReady = await awaitInventoryPageReady(beforeScrollFingerprint);
|
||||
if (scrollReady.ready.abortReason) {
|
||||
blockedReason = scrollReady.ready.abortReason;
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
const beforeScrollFingerprint = currentPageFingerprint || screenFingerprint(currentCapture);
|
||||
currentCapture = await captureFastSelectedSource(0, true);
|
||||
const afterScrollFingerprint = screenFingerprint(currentCapture);
|
||||
const scrolledCapture = scrollReady.capture;
|
||||
if (!scrolledCapture) {
|
||||
blockedReason = "Keine Capture-Daten nach dem Scrollen.";
|
||||
break;
|
||||
}
|
||||
currentCapture = scrolledCapture;
|
||||
const scrolledSurfaceRejection = validateAutoScanEntryPreflight(scrolledCapture);
|
||||
if (!scrolledSurfaceRejection.ok) {
|
||||
blockedReason = scrolledSurfaceRejection.reason;
|
||||
break;
|
||||
}
|
||||
const afterScrollFingerprint = screenFingerprint(scrolledCapture);
|
||||
|
||||
if (beforeScrollFingerprint && afterScrollFingerprint && beforeScrollFingerprint === afterScrollFingerprint) {
|
||||
blockedReason = "Scrollen hat die sichtbare Inventarseite nicht veraendert.";
|
||||
@@ -446,7 +700,7 @@ export async function runAutoScanLoop(
|
||||
break;
|
||||
}
|
||||
|
||||
const refreshedModel = buildGridModel(currentCapture?.inventoryGrid);
|
||||
const refreshedModel = buildGridModel(scrolledCapture.inventoryGrid);
|
||||
if (!refreshedModel) {
|
||||
blockedReason = "Kachel-Grid nach dem Scrollen verloren.";
|
||||
break;
|
||||
@@ -461,24 +715,27 @@ export async function runAutoScanLoop(
|
||||
}
|
||||
|
||||
const status: ScanSummary["status"] = aborted || shouldStop() ? "stopped" : blockedReason ? "blocked" : "done";
|
||||
return {
|
||||
updateScanTiming(stats, startedAt);
|
||||
return finish({
|
||||
status,
|
||||
stats,
|
||||
blockedReason,
|
||||
pageCount: page,
|
||||
gridLabel: blockedReason || `${page} Seite(n) verarbeitet, Ziel ${maxTargets} Artifacts, ${rowsToSkip} Zeile(n) auf der ersten Seite uebersprungen`,
|
||||
targetCount: maxTargets,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function detailFingerprint(capture: CaptureResult | null) {
|
||||
if (!capture) return "";
|
||||
if (capture.detailFingerprint) return capture.detailFingerprint;
|
||||
if (capture.detailDataUrl) return fingerprintDataUrl(capture.detailDataUrl);
|
||||
if (capture.dataUrl) return fingerprintDataUrl(capture.dataUrl);
|
||||
return "";
|
||||
}
|
||||
|
||||
export function screenFingerprint(capture: CaptureResult | null) {
|
||||
if (capture?.inventoryFingerprint) return capture.inventoryFingerprint;
|
||||
if (capture?.inventoryDataUrl) return fingerprintDataUrl(capture.inventoryDataUrl);
|
||||
if (capture?.dataUrl) return fingerprintDataUrl(capture.dataUrl);
|
||||
return "";
|
||||
|
||||
Reference in New Issue
Block a user