fix(input): force Genshin foreground via AttachThreadInput (auto-scan no longer aborts)
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>
This commit is contained in:
@@ -1,5 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { detailFingerprint, fingerprintDataUrl, isRepeatedProcessedPageFingerprint, screenFingerprint } from "./autoScanLoop";
|
||||
import { runAutoScanLoop } from "./autoScanLoop";
|
||||
import type { AutoScanLoopDependencies } from "./autoScanLoop";
|
||||
import type { CaptureResult } from "../types/global";
|
||||
|
||||
type ScanTestCapture = CaptureResult & { inventoryGrid: NonNullable<CaptureResult["inventoryGrid"]> };
|
||||
|
||||
const sampleGrid = {
|
||||
centers: [{ x: 80, y: 90, row: 0, col: 0 }],
|
||||
rows: 1,
|
||||
cols: 1,
|
||||
confidence: 86,
|
||||
source: "detected" as const,
|
||||
};
|
||||
|
||||
const sampleParse = {
|
||||
name: "A Tiara of Torrents",
|
||||
slot: "Flower of Life",
|
||||
level: 16,
|
||||
mainStat: "HP",
|
||||
mainValue: "10.7%",
|
||||
substats: ["ATK+29", "DEF+10"],
|
||||
setName: "Tenacity of the Millelith",
|
||||
equipped: "Traveler",
|
||||
confidence: 84,
|
||||
notes: [],
|
||||
fields: {
|
||||
name: { value: "A Tiara of Torrents", confidence: 84, source: "ocr" as const },
|
||||
slot: { value: "Flower of Life", confidence: 84, source: "ocr" as const },
|
||||
level: { value: "16", confidence: 84, source: "ocr" as const },
|
||||
mainStat: { value: "HP", confidence: 84, source: "ocr" as const },
|
||||
mainValue: { value: "10.7%", confidence: 84, source: "ocr" as const },
|
||||
setName: { value: "Tenacity of the Millelith", confidence: 84, source: "ocr" as const },
|
||||
equipped: { value: "Traveler", confidence: 84, source: "ocr" as const },
|
||||
substats: { value: "ATK+29, DEF+10", confidence: 84, source: "ocr" as const },
|
||||
},
|
||||
};
|
||||
|
||||
function capture(overrides: Partial<ScanTestCapture> = {}): ScanTestCapture {
|
||||
return {
|
||||
id: "source",
|
||||
name: "screen-capture",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
dataUrl: "data:image/png;base64,AA",
|
||||
capturedAt: "2026-01-01T00:00:00.000Z",
|
||||
captureTarget: "desktop-source",
|
||||
ocr: [],
|
||||
detailDataUrl: "data:image/png;base64,DETAIL-AAA",
|
||||
inventoryDataUrl: "data:image/png;base64,GRID-AAA",
|
||||
inventoryGrid: sampleGrid,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("autoScanLoop fingerprints", () => {
|
||||
it("distinguishes captures that share the same prefix but differ later", () => {
|
||||
@@ -47,4 +100,49 @@ describe("autoScanLoop fingerprints", () => {
|
||||
expect(isRepeatedProcessedPageFingerprint("abc", seen, 2)).toBe(true);
|
||||
expect(isRepeatedProcessedPageFingerprint("", seen, 3)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not block before first click when start capture is from the primary screen", async () => {
|
||||
const startCapture = capture({
|
||||
captureTarget: "primary-screen",
|
||||
detailDataUrl: `data:image/png;base64,${"D".repeat(600)}`,
|
||||
});
|
||||
|
||||
let clicked = 0;
|
||||
const deps: AutoScanLoopDependencies = {
|
||||
api: {
|
||||
clickScreen: async () => {
|
||||
clicked += 1;
|
||||
return {
|
||||
ok: true,
|
||||
x: 0,
|
||||
y: 0,
|
||||
cursorX: 0,
|
||||
cursorY: 0,
|
||||
clicked: true,
|
||||
moved: true,
|
||||
focused: true,
|
||||
};
|
||||
},
|
||||
scrollScreen: async () => ({ ok: true, notchesSent: 0 }),
|
||||
getAutomationGuard: async () => ({ ok: true, escapePressed: false, enterPressed: false, f9Pressed: false }),
|
||||
},
|
||||
captureSelectedSource: async () => capture({ detailDataUrl: `data:image/png;base64,${"E".repeat(600)}` }),
|
||||
captureFastSelectedSource: async () => startCapture,
|
||||
parseArtifact: () => sampleParse,
|
||||
persistParsedArtifact: async () => false,
|
||||
saveReviewSample: async () => ({ ok: true }),
|
||||
getAutoReviewReason: () => "",
|
||||
shouldFlagArtifactForReview: () => false,
|
||||
appendAutomationLog: () => undefined,
|
||||
appendClickDiagnostics: () => undefined,
|
||||
setReviewStatus: () => undefined,
|
||||
setAutoScanStats: () => undefined,
|
||||
shouldStop: () => false,
|
||||
};
|
||||
|
||||
const result = await runAutoScanLoop(deps, { scanLimit: 1, skipRows: 0, detectedInventoryCount: null });
|
||||
expect(result.blockedReason).toBe("");
|
||||
expect(result.status).toBe("done");
|
||||
expect(clicked).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user