c8ae0dd7bf
Auto-scan aborted immediately with "Genshin konnte nicht in den Vordergrund geholt werden". Root cause: the focus call runs in the background input/capture helper process, and Windows' foreground lock silently refuses SetForegroundWindow from a process that is neither foreground nor the last input source. When the user clicks "Auto-Scan starten" the Electron window is foreground, so the helper's plain SetForegroundWindow is dropped and focus stays false. Fix (both the C# sidecar and the PowerShell fallback): before SetForegroundWindow, attach our thread's input queue to the target (and current-foreground) window thread with AttachThreadInput and clear SPI_..FOREGROUNDLOCKTIMEOUT, then restore. This is the same technique Inventory Kamera and other reliable automators use; it is what our helper was missing after the old ALT-tap workaround was removed on the wrong assumption that equal integrity level is sufficient (that only covers UIPI input injection, not foreground changes). Sidecar recompiled + republished; electron build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
315 lines
12 KiB
TypeScript
315 lines
12 KiB
TypeScript
import { automationBlockReason, requiresAdminForAutomation } from "../../../lib/automationPlanner";
|
|
import { captureRejectionReason } from "../../../lib/scannerCaptureQuality";
|
|
import { runAutoScanLoop } from "../../../lib/autoScanLoop";
|
|
import { clampScanLimit, emptyAutoScanStats, resolveScanTargetCount, type AutoScanStats, type ScanSummary } from "../../../lib/scannerSession";
|
|
import { getAutoReviewReason, wait } from "../../../lib/scanReviewUtils";
|
|
import type { AutomationRepositoryPort, RuntimeRepositoryPort } from "../../../infrastructure/repositories/rendererBridgeRepositories";
|
|
import type {
|
|
AutomationGuard,
|
|
BooleanResult,
|
|
CaptureOptions,
|
|
CaptureResult,
|
|
ClickResult,
|
|
FocusGenshinResult,
|
|
RuntimeInfo,
|
|
ScrollResult,
|
|
} from "../../../types/global";
|
|
import type { ParsedArtifactCandidate } from "../../../lib/artifactOcrParser";
|
|
import type { MutableRefObject } from "react";
|
|
import type { Dispatch, SetStateAction } from "react";
|
|
|
|
export interface ScanActionContext {
|
|
autoScanRunning: boolean;
|
|
setAutoScanRunning: Dispatch<SetStateAction<boolean>>;
|
|
isScanning: boolean;
|
|
stopVisibleScanRef: MutableRefObject<boolean>;
|
|
selectedSourceId: string;
|
|
bridgeReady: boolean;
|
|
automationRepo?: AutomationRepositoryPort;
|
|
runtimeRepo?: RuntimeRepositoryPort;
|
|
runtimeInfo?: RuntimeInfo | null;
|
|
scanLimit: number;
|
|
skipRows: number;
|
|
detectedInventoryCount: number;
|
|
setScanSummary: Dispatch<SetStateAction<ScanSummary | null>>;
|
|
setAutoScanStats: Dispatch<SetStateAction<AutoScanStats>>;
|
|
setReviewStatus: Dispatch<SetStateAction<string>>;
|
|
appendAutomationLog: (line: string) => void;
|
|
appendClickDiagnostics: (result: ClickResult, prefix?: string) => void;
|
|
captureSelectedSource: (delayMs?: number, focusGenshin?: boolean, options?: CaptureOptions) => Promise<CaptureResult | null>;
|
|
captureFastSelectedSource: (delayMs?: number, focusGenshin?: boolean) => Promise<CaptureResult | null>;
|
|
parseArtifact: (capture: CaptureResult | null) => ParsedArtifactCandidate | null;
|
|
persistParsedArtifact: (capture: CaptureResult | null, parsed: ParsedArtifactCandidate, source: string, needsReview: boolean) => Promise<boolean>;
|
|
saveReviewSample: (capture: CaptureResult | null, parsed: ParsedArtifactCandidate | null, reason?: string) => Promise<BooleanResult | null>;
|
|
shouldFlagArtifactForReview: (parsed: ParsedArtifactCandidate | null) => boolean;
|
|
focusDashboard: () => Promise<void>;
|
|
}
|
|
|
|
function buildScanSignature(parsed: ParsedArtifactCandidate) {
|
|
return `${parsed.name}-${parsed.slot}-${parsed.mainStat}-${parsed.mainValue}-${parsed.level ?? 0}`;
|
|
}
|
|
|
|
export async function runAutoReviewScan(context: ScanActionContext): Promise<void> {
|
|
const {
|
|
autoScanRunning,
|
|
selectedSourceId,
|
|
stopVisibleScanRef,
|
|
setAutoScanRunning,
|
|
setReviewStatus,
|
|
setScanSummary,
|
|
setAutoScanStats,
|
|
appendAutomationLog,
|
|
scanLimit,
|
|
detectedInventoryCount,
|
|
captureSelectedSource,
|
|
parseArtifact,
|
|
persistParsedArtifact,
|
|
saveReviewSample,
|
|
shouldFlagArtifactForReview,
|
|
focusDashboard,
|
|
} = context;
|
|
|
|
if (autoScanRunning || !selectedSourceId) return;
|
|
|
|
setAutoScanRunning(true);
|
|
stopVisibleScanRef.current = false;
|
|
setScanSummary(null);
|
|
setAutoScanStats(emptyAutoScanStats);
|
|
setReviewStatus("Manueller Scan laeuft. Klicke in Genshin auf ein anderes Artifact; nur neue Artifacts werden verarbeitet.");
|
|
|
|
const seen = new Set<string>();
|
|
const stats: AutoScanStats = { ...emptyAutoScanStats, pages: 1 };
|
|
let idleTicks = 0;
|
|
const maxArtifacts = resolveScanTargetCount(scanLimit, detectedInventoryCount);
|
|
const maxIdleTicks = 90;
|
|
|
|
while (!stopVisibleScanRef.current && stats.parsed < maxArtifacts && idleTicks < maxIdleTicks) {
|
|
const capture = await captureSelectedSource(0, true);
|
|
const rejection = captureRejectionReason(capture, parseArtifact(capture));
|
|
const parsed = parseArtifact(capture);
|
|
|
|
if (!capture || !parsed || rejection) {
|
|
if (capture && rejection) {
|
|
await saveReviewSample(capture, parsed, `manual:capture-rejected`);
|
|
stats.review++;
|
|
setAutoScanStats({ ...stats });
|
|
}
|
|
idleTicks++;
|
|
setReviewStatus(`Manueller Scan wartet auf ein lesbares Artifact... (${stats.parsed}/${maxArtifacts})${rejection ? ` ${rejection}` : ""}`);
|
|
await wait(700);
|
|
continue;
|
|
}
|
|
|
|
const signature = buildScanSignature(parsed);
|
|
if (seen.has(signature)) {
|
|
idleTicks++;
|
|
setReviewStatus(`Manueller Scan wartet auf ein neues Artifact... (${stats.parsed}/${maxArtifacts})`);
|
|
await wait(700);
|
|
continue;
|
|
}
|
|
|
|
seen.add(signature);
|
|
idleTicks = 0;
|
|
stats.attempted++;
|
|
stats.verified++;
|
|
stats.parsed++;
|
|
|
|
const reason = getAutoReviewReason(capture, parsed);
|
|
const needsReview = shouldFlagArtifactForReview(parsed);
|
|
if (reason) {
|
|
await saveReviewSample(capture, parsed, `manual:${reason}`);
|
|
stats.review++;
|
|
}
|
|
if (await persistParsedArtifact(capture, parsed, "manual-scan", needsReview)) {
|
|
stats.stored++;
|
|
}
|
|
setAutoScanStats({ ...stats });
|
|
setReviewStatus(`Manueller Scan: neues Artifact erkannt (${stats.parsed}/${maxArtifacts}). Klicke das naechste Artifact an oder druecke Stop.`);
|
|
await wait(700);
|
|
}
|
|
|
|
setAutoScanRunning(false);
|
|
const status: ScanSummary["status"] = stopVisibleScanRef.current ? "stopped" : "done";
|
|
const idleSuffix = idleTicks >= maxIdleTicks ? " Keine neuen Artifacts erkannt; manueller Scan beendet." : "";
|
|
await focusDashboard();
|
|
setReviewStatus(
|
|
`Manueller Scan ${status}. ${stats.verified} neue Ansichten verifiziert, ${stats.parsed} Artifacts gelesen, ${stats.stored} in der Datenbank gespeichert, ${stats.review} Review-Samples.${idleSuffix}`,
|
|
);
|
|
setScanSummary({
|
|
mode: "Manueller Scan",
|
|
status,
|
|
...stats,
|
|
targetCount: maxArtifacts,
|
|
gridLabel: "Nur neue, vom User angeklickte Artifacts wurden verarbeitet.",
|
|
});
|
|
|
|
appendAutomationLog(`manual scan finished: ${stats.parsed} parsed, ${stats.stored} stored, ${stats.review} review`);
|
|
}
|
|
|
|
export async function runVisibleGridScan(context: ScanActionContext): Promise<void> {
|
|
const {
|
|
autoScanRunning,
|
|
bridgeReady,
|
|
selectedSourceId,
|
|
runtimeInfo,
|
|
automationRepo,
|
|
runtimeRepo,
|
|
stopVisibleScanRef,
|
|
setAutoScanRunning,
|
|
setScanSummary,
|
|
setAutoScanStats,
|
|
setReviewStatus,
|
|
appendAutomationLog,
|
|
appendClickDiagnostics,
|
|
captureSelectedSource,
|
|
captureFastSelectedSource,
|
|
parseArtifact,
|
|
persistParsedArtifact,
|
|
saveReviewSample,
|
|
shouldFlagArtifactForReview,
|
|
scanLimit,
|
|
skipRows,
|
|
detectedInventoryCount,
|
|
focusDashboard,
|
|
} = context;
|
|
|
|
if (autoScanRunning || !bridgeReady || !selectedSourceId || !automationRepo?.focusGenshinForScanStart || !automationRepo?.focusGenshin || !automationRepo?.clickScreen || !automationRepo?.scrollScreen) return;
|
|
|
|
const requiresAdminForAutoScan = requiresAdminForAutomation(runtimeInfo);
|
|
if (requiresAdminForAutoScan) {
|
|
setReviewStatus(
|
|
"App laeuft nicht als Administrator. Bitte die App schliessen und als Administrator neu starten - Auto-Scan braucht Administrator-Rechte, damit Windows die simulierten Eingaben an Genshin nicht blockiert.",
|
|
);
|
|
setScanSummary({
|
|
mode: "Automatischer Scan",
|
|
status: "blocked",
|
|
...emptyAutoScanStats,
|
|
targetCount: resolveScanTargetCount(scanLimit, detectedInventoryCount),
|
|
gridLabel: "App laeuft nicht als Administrator. Neustart als Administrator noetig.",
|
|
});
|
|
return;
|
|
}
|
|
|
|
setAutoScanRunning(true);
|
|
stopVisibleScanRef.current = false;
|
|
setScanSummary(null);
|
|
setAutoScanStats(emptyAutoScanStats);
|
|
setReviewStatus("Automatischer Scan startet. ESC gedrueckt halten oder Stop klicken beendet den Scan sofort.");
|
|
|
|
const freshRuntime = await runtimeRepo?.getRuntimeInfo().catch(() => null);
|
|
const adminBlockReason = automationBlockReason(freshRuntime);
|
|
if (adminBlockReason) {
|
|
setAutoScanRunning(false);
|
|
setReviewStatus(adminBlockReason);
|
|
appendAutomationLog("blocked: App laeuft nicht als Administrator, keine In-Game-Klicks ausgefuehrt");
|
|
setScanSummary({
|
|
mode: "Automatischer Scan",
|
|
status: "blocked",
|
|
...emptyAutoScanStats,
|
|
targetCount: resolveScanTargetCount(scanLimit, detectedInventoryCount),
|
|
gridLabel: adminBlockReason,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (freshRuntime) {
|
|
const required = freshRuntime.genshinFound ? `found:${freshRuntime.targetProcess || "genshin"}` : "not-found";
|
|
appendAutomationLog(`runtime ping: elevated=${freshRuntime.isElevated} ${required}`);
|
|
}
|
|
|
|
const focusGenshinForScanStart = automationRepo.focusGenshinForScanStart ?? automationRepo.focusGenshin;
|
|
setReviewStatus("Genshin wird in den Vordergrund geholt...");
|
|
let focusResult: FocusGenshinResult | null = null;
|
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
const current = await focusGenshinForScanStart().catch(() => null);
|
|
if (!current) {
|
|
appendAutomationLog(`focus attempt ${attempt}/3: exception`);
|
|
} else {
|
|
focusResult = current;
|
|
appendAutomationLog(
|
|
`focus attempt ${attempt}/3: ${current.focused ? "ok" : "failed"} found:${current.genshinFound ? "yes" : "no"} setForeground:${current.setForegroundResult ?? "n/a"} target:${current.targetProcess || "?"} fg:${current.foregroundProcess || "?"}`,
|
|
);
|
|
if (current.focused) break;
|
|
if (!current.genshinFound) break;
|
|
}
|
|
if (attempt < 3) await wait(300);
|
|
}
|
|
|
|
if (!focusResult?.focused) {
|
|
setAutoScanRunning(false);
|
|
const reason = !focusResult?.genshinFound
|
|
? "Genshin-Prozess wurde nicht gefunden. Bitte pruefen, ob Genshin laeuft, und Auto-Scan erneut starten."
|
|
: "Genshin konnte nicht in den Vordergrund geholt werden. Bitte Genshin manuell anklicken/fokussieren und Auto-Scan erneut starten.";
|
|
setReviewStatus(reason);
|
|
setScanSummary({
|
|
mode: "Automatischer Scan",
|
|
status: "blocked",
|
|
...emptyAutoScanStats,
|
|
targetCount: resolveScanTargetCount(scanLimit, detectedInventoryCount),
|
|
gridLabel: reason,
|
|
});
|
|
return;
|
|
}
|
|
|
|
setReviewStatus("Genshin ist im Vordergrund. Automatischer Scan startet. ESC gedrueckt halten oder Stop klicken beendet den Scan sofort.");
|
|
|
|
const result = await runAutoScanLoop(
|
|
{
|
|
api: {
|
|
clickScreen: (x: number, y: number) => {
|
|
if (!automationRepo?.clickScreen) {
|
|
return Promise.resolve<ClickResult>({
|
|
ok: false,
|
|
x: 0,
|
|
y: 0,
|
|
clicked: false,
|
|
moved: false,
|
|
focused: false,
|
|
inputBlocked: false,
|
|
});
|
|
}
|
|
return automationRepo.clickScreen(x, y);
|
|
},
|
|
scrollScreen: (notches: number, anchorX?: number, anchorY?: number) => {
|
|
if (!automationRepo?.scrollScreen) {
|
|
return Promise.resolve<ScrollResult>({ ok: false, notchesSent: 0, inputBlocked: false });
|
|
}
|
|
return automationRepo.scrollScreen(notches, anchorX, anchorY);
|
|
},
|
|
getAutomationGuard: () =>
|
|
automationRepo?.getAutomationGuard?.() ??
|
|
Promise.resolve<AutomationGuard>({ ok: false, escapePressed: false, enterPressed: false, f9Pressed: false }),
|
|
},
|
|
captureSelectedSource: (delayMs = 0, focusGenshin = false) => captureSelectedSource(delayMs, focusGenshin),
|
|
captureFastSelectedSource,
|
|
parseArtifact,
|
|
persistParsedArtifact,
|
|
saveReviewSample,
|
|
getAutoReviewReason,
|
|
shouldFlagArtifactForReview,
|
|
appendAutomationLog,
|
|
appendClickDiagnostics,
|
|
setReviewStatus,
|
|
setAutoScanStats,
|
|
shouldStop: () => stopVisibleScanRef.current,
|
|
},
|
|
{
|
|
scanLimit,
|
|
skipRows,
|
|
detectedInventoryCount,
|
|
},
|
|
);
|
|
|
|
setAutoScanRunning(false);
|
|
if (result.blockedReason) appendAutomationLog(`stop: ${result.blockedReason}`);
|
|
await focusDashboard();
|
|
setReviewStatus(`Automatischer Scan ${result.status === "stopped" ? "gestoppt" : result.status === "blocked" ? "blockiert" : "fertig"}. ${result.stats.clicked} Klicks, ${result.stats.attempted} Positionen bearbeitet, ${result.stats.verified} Ansichten verifiziert, ${result.stats.parsed} gelesen, ${result.stats.stored} in der Datenbank, ${result.stats.review} Review-Samples, ${result.stats.duplicates} Duplikate, ${result.stats.misses} Misses.${result.blockedReason ? ` ${result.blockedReason}` : ""}`);
|
|
setScanSummary({
|
|
mode: "Automatischer Scan",
|
|
status: result.status,
|
|
...result.stats,
|
|
targetCount: result.targetCount,
|
|
gridLabel: result.gridLabel,
|
|
});
|
|
}
|