using System.Runtime.InteropServices; namespace AgentWorker.WindowsRunner; // 输入类 P/Invoke 独立成类:NativeWindows 保持对面"只读编译边界"原样不动。 // 本层没有进程启动接口;所有输入只作用于经策略核验后的前台窗口。 // 坐标一律物理屏幕像素——进程入口(ActuateService.EnsureDpiAwareness)声明 // Per-Monitor V2 DPI 感知,与 RPA 侧 PowerShell 驱动每脚本声明的 // SetProcessDpiAwarenessContext(-4) 语义一致,保证 SetCursorPos 全程物理像素。 // 抢前台五招兜底从 RPA 驱动 activateWindowScript 逐行移植: // Windows 前台锁定策略会拒绝普通 SetForegroundWindow(应用点击"运行"后自身是前台), // 五招顺序与睡眠时间是踩坑调出来的实战参数,一行语义都不允许改动。 internal static class NativeInput { private const uint InputMouse = 0; private const uint MouseEventfMove = 0x0001; private const uint MouseEventfLeftDown = 0x0002; private const uint MouseEventfLeftUp = 0x0004; private const uint MouseEventfRightDown = 0x0008; private const uint MouseEventfRightUp = 0x0010; private const uint MouseEventfWheel = 0x0800; internal const int SwRestore = 9; internal const int SwMinimize = 6; internal const int SwMaximize = 3; private const uint WmClose = 0x0010; private const byte VkMenu = 0x12; // ALT private const uint KeyeventfKeyup = 0x0002; [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SetCursorPos(int x, int y); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool GetCursorPos(out Point point); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool SetForegroundWindow(nint handle); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool ShowWindow(nint handle, int command); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool IsIconic(nint handle); [DllImport("user32.dll")] internal static extern void keybd_event(byte virtualKey, byte scanCode, uint flags, nint extraInfo); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool AttachThreadInput(uint currentThread, uint targetThread, bool attach); [DllImport("user32.dll")] private static extern void SwitchToThisWindow(nint handle, bool altTab); [DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId(); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetProcessDpiAwarenessContext(nint value); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetWindowPos(nint handle, nint after, int x, int y, int width, int height, uint flags); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool PostMessage(nint handle, uint message, nint wParam, nint lParam); [DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint count, Input[] inputs, int size); [StructLayout(LayoutKind.Sequential)] internal struct Point { public int X; public int Y; } [StructLayout(LayoutKind.Sequential)] private struct Input { public uint Type; public InputUnion Union; } [StructLayout(LayoutKind.Explicit)] private struct InputUnion { [FieldOffset(0)] public MouseInput Mouse; } [StructLayout(LayoutKind.Sequential)] private struct MouseInput { public int Dx; public int Dy; public uint MouseData; public uint Flags; public uint Time; public nint ExtraInfo; } // DPI 感知声明是进程级一次性设置;失败(如已被宿主先行声明)不影响后续调用。 internal static void EnsureDpiAwareness() { try { _ = SetProcessDpiAwarenessContext(new nint(-4)); } catch { /* 旧系统无此 API 时按物理像素继续;对面 CoordinateSpace 以 GetDpiForWindow 实测为准。 */ } } // SendInput 批量提交鼠标标志组合;单次失败按 input_failed 处理由上层转稳定错误码。 internal static void SendMouse(uint flags, uint data) { var input = new Input { Type = InputMouse, Union = new InputUnion { Mouse = new MouseInput { Dx = 0, Dy = 0, MouseData = data, Flags = flags } } }; if (SendInput(1, [input], Marshal.SizeOf()) != 1) throw new RunnerException("input_failed"); } internal static void SendWheel(int amount) => SendMouse(MouseEventfWheel, unchecked((uint)amount)); internal static void Click(bool right, int repeats, int settleMs) { var down = right ? MouseEventfRightDown : MouseEventfLeftDown; var up = right ? MouseEventfRightUp : MouseEventfLeftUp; for (var i = 0; i < repeats; i++) { SendMouse(down, 0); SendMouse(up, 0); if (settleMs > 0) Thread.Sleep(settleMs); } } internal static Point CursorPosition() { if (!GetCursorPos(out var point)) throw new RunnerException("input_failed"); return point; } // 窗口拖拽时序:按下→hold→移动→release→抬起,与 RPA 驱动逐段一致。 internal static void DragSequence(bool right, int fromX, int fromY, int toX, int toY, int holdMs, int releaseDelayMs) { var down = right ? MouseEventfRightDown : MouseEventfLeftDown; var up = right ? MouseEventfRightUp : MouseEventfLeftUp; if (!SetCursorPos(fromX, fromY)) throw new RunnerException("input_failed"); SendMouse(down, 0); if (holdMs > 0) Thread.Sleep(holdMs); if (!SetCursorPos(toX, toY)) throw new RunnerException("input_failed"); if (releaseDelayMs > 0) Thread.Sleep(releaseDelayMs); SendMouse(up, 0); } // 抢前台五招兜底(RPA 驱动 activateWindowScript 的逐行移植): // ①最小化则恢复 +150ms ②SwitchToThisWindow +150ms ③ALT 借位 + SetForegroundWindow +200ms // ④AttachThreadInput 挂到前台线程 + SetForegroundWindow +200ms ⑤最小化再恢复 +450ms // 五招全失败抛 focus_failed——对应 PS 版 RPA_ERROR '无法把窗口带到前台'。 internal static void FocusChain(nint handle) { if (IsIconic(handle)) { _ = ShowWindow(handle, SwRestore); Thread.Sleep(150); } if (NativeWindows.GetForegroundWindow() != handle) { SwitchToThisWindow(handle, true); Thread.Sleep(150); } if (NativeWindows.GetForegroundWindow() != handle) { keybd_event(VkMenu, 0, 0, nint.Zero); _ = SetForegroundWindow(handle); keybd_event(VkMenu, 0, KeyeventfKeyup, nint.Zero); Thread.Sleep(200); } if (NativeWindows.GetForegroundWindow() != handle) { // 返回值即前台窗口的线程号;PID 出参不需要。 var foregroundThread = NativeWindows.GetWindowThreadProcessId( NativeWindows.GetForegroundWindow(), out _); var currentThread = GetCurrentThreadId(); var attached = AttachThreadInput(currentThread, foregroundThread, true); try { _ = SetForegroundWindow(handle); } finally { if (attached) _ = AttachThreadInput(currentThread, foregroundThread, false); } Thread.Sleep(200); } if (NativeWindows.GetForegroundWindow() != handle) { _ = ShowWindow(handle, SwMinimize); Thread.Sleep(150); _ = ShowWindow(handle, SwRestore); Thread.Sleep(300); } if (NativeWindows.GetForegroundWindow() != handle) throw new RunnerException("focus_failed"); } internal static void MoveWindowBounds(nint handle, int x, int y, int width, int height) => _ = SetWindowPos(handle, nint.Zero, x, y, width, height, 4); // flags=4 SWP_NOZORDER,与 RPA 驱动一致 internal static void CloseWindow(nint handle) => _ = PostMessage(handle, WmClose, nint.Zero, nint.Zero); }