213 lines
8.3 KiB
C#
213 lines
8.3 KiB
C#
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)]
|
||
private 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<Input>()) != 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);
|
||
}
|