XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEWorkflow

【WinUI】XFE基于windows的工作流

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Nodes;
using FlaUI.Core.AutomationElements;
using FlaUI.Core.Definitions;
using FlaUI.UIA3;
using XFEExtension.NetCore.InputSimulator;
using XFEWorkflow.Abstractions;

namespace XFEWorkflow.Plugins.Desktop;

public sealed class DesktopPlugin : IWorkflowPlugin
{
    public static PluginManifest Manifest
    {
        get
        {
            using var stream = typeof(DesktopPlugin).Assembly.GetManifestResourceStream("XFEWorkflow.Plugins.Desktop.plugin.json")!;
            return JsonSerializer.Deserialize<PluginManifest>(stream, WorkflowJson.Options)!;
        }
    }
    public void Register(IPluginRegistry registry)
    {
        foreach (var descriptor in Manifest.Capabilities) registry.RegisterAction(descriptor, () => new DesktopAction(descriptor.Id));
    }
}
public sealed class DesktopAction(string id) : IWorkflowAction
{
    public async ValueTask<JsonObject> ExecuteAsync(JsonObject input, IWorkflowContext context, CancellationToken ct)
    {
        await context.InvokeHostAsync("desktop.acquire", [], ct);
        if (id is "xfe.desktop.text" or "xfe.desktop.keys" or "xfe.desktop.sequence")
        {
            var parameters = WorkflowJson.Copy(input); parameters["operation"] = id["xfe.desktop.".Length..];
            return await context.InvokeHostAsync("desktop.input", parameters, ct);
        }
        using var automation = new UIA3Automation();
        ct.ThrowIfCancellationRequested();
        if (id == "xfe.desktop.pick")
        {
            await Task.Delay(Math.Max(0, input.Integer("delaySeconds", 3)) * 1000, ct);
            GetCursorPos(out var point);
            return Describe(automation.FromPoint(new Point(point.X, point.Y)));
        }
        var root = automation.GetDesktop();
        var processId = input.Integer("processId"); var windowName = input.String("window");
        var window = processId > 0
            ? root.FindAllChildren(c => c.ByProcessId(processId)).FirstOrDefault(x => x.ControlType == ControlType.Window && (windowName.Length == 0 || x.Name == windowName))
            : windowName.Length > 0 ? root.FindFirstChild(c => c.ByName(windowName)) : null;
        if (window is null) throw new InvalidOperationException("未找到目标窗口");
        if (id == "xfe.desktop.activate") { window.AsWindow().Focus(); return Describe(window); }
        AutomationElement? element = window;
        if (input.String("automationId") is { Length: > 0 } automationId) element = window.FindFirstDescendant(c => c.ByAutomationId(automationId));
        else if (input.String("name") is { Length: > 0 } name) element = window.FindFirstDescendant(c => c.ByName(name));
        if (element is null) throw new InvalidOperationException("未找到目标控件");
        if (id == "xfe.desktop.read")
        {
            var result = Describe(element);
            if (element.Patterns.Value.IsSupported) result["text"] = element.Patterns.Value.Pattern.Value.Value;
            else if (element.Patterns.Text.IsSupported) result["text"] = element.Patterns.Text.Pattern.DocumentRange.GetText(-1);
            else result["text"] = element.Name;
            return result;
        }
        window.AsWindow().Focus(); element.Focus();
        if (id == "xfe.desktop.fill")
        {
            if (element.Patterns.Value.IsSupported) element.Patterns.Value.Pattern.SetValue(input.String("text"));
            else await context.InvokeHostAsync("desktop.input", new() { ["operation"] = "text", ["text"] = input.String("text") }, ct);
            return Describe(element);
        }
        if (id == "xfe.desktop.click")
        {
            if (element.Patterns.Invoke.IsSupported) element.Patterns.Invoke.Pattern.Invoke();
            else
            {
                var bounds = element.BoundingRectangle;
                await context.InvokeHostAsync("desktop.input", new() { ["operation"] = "click", ["x"] = (int)(bounds.Left + bounds.Width / 2), ["y"] = (int)(bounds.Top + bounds.Height / 2) }, ct);
            }
            return Describe(element);
        }
        throw new NotSupportedException(id);
    }
    private static JsonObject Describe(AutomationElement element) => new()
    {
        ["name"] = element.Name, ["automationId"] = element.AutomationId, ["processId"] = element.Properties.ProcessId.ValueOrDefault,
        ["controlType"] = element.ControlType.ToString(),
        ["x"] = element.BoundingRectangle.X, ["y"] = element.BoundingRectangle.Y,
        ["width"] = element.BoundingRectangle.Width, ["height"] = element.BoundingRectangle.Height
    };
    [DllImport("user32.dll")] private static extern bool GetCursorPos(out NativePoint point);
    [StructLayout(LayoutKind.Sequential)] private struct NativePoint { public int X; public int Y; }
}

// The desktop host owns the input controller so killing a Runner can still release every held key.
public sealed class DesktopInputService : IDisposable
{
    private InputController? controller;
    private string owner = "";
    private readonly SemaphoreSlim gate = new(1);
    private CancellationTokenSource? emergency;
    public string Backend { get; set; } = "standard";
    public async ValueTask<JsonObject> ExecuteAsync(string runId, JsonObject input, CancellationToken ct)
    {
        await gate.WaitAsync(ct);
        try
        {
            if (owner != runId)
            {
                controller?.Dispose(); emergency?.Dispose();
                emergency = new(); owner = runId;
                controller = new InputController(Backend == "hid" ? new DriverBackend() : new WindowsInputBackend());
            }
            using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, emergency!.Token);
            var token = linked.Token;
            async Task Step(JsonObject step)
            {
                token.ThrowIfCancellationRequested();
                switch (step.String("operation"))
                {
                    case "text": await controller!.TypeTextAsync(step.String("text"), step.Integer("delay"), token); break;
                    case "keys":
                        var keys = step.String("keys").Split('+', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
                            .Select(x => Enum.Parse<ScanCode>(x, true)).ToArray();
                        await controller!.PressCombinationAsync(keys, Math.Max(0, step.Integer("holdTime", 30)), token); break;
                    case "down": controller!.KeyDown(Enum.Parse<ScanCode>(step.String("key"), true)); break;
                    case "up": controller!.KeyUp(Enum.Parse<ScanCode>(step.String("key"), true)); break;
                    case "move": controller!.MouseMove(step.Integer("x"), step.Integer("y")); break;
                    case "locate": controller!.LocateTo(step.Integer("x"), step.Integer("y")); break;
                    case "click":
                        controller!.LocateTo(step.Integer("x"), step.Integer("y"));
                        var button = ParseButton(step);
                        if (step.Integer("clicks", 1) == 2) await controller.MouseDoubleClickAsync(button, 80, token);
                        else await controller.MouseClickAsync(button, 30, token);
                        break;
                    case "drag":
                        await controller!.MouseDragAsync(new Point(step.Integer("fromX"), step.Integer("fromY")), new Point(step.Integer("toX"), step.Integer("toY")), Math.Clamp(step.Integer("durationMilliseconds", 500), 0, 60000), ParseButton(step), token);
                        break;
                    case "scroll":
                        if (step.Boolean("horizontal")) controller!.MouseHorizontalWheelRoll(step.Integer("delta", 120));
                        else controller!.MouseWheelRoll(step.Integer("delta", 120));
                        break;
                    case "delay": await Task.Delay(Math.Clamp(step.Integer("milliseconds"), 0, 3_600_000), token); break;
                    default: throw new NotSupportedException("未知输入步骤");
                }
            }
            try
            {
                if (input.String("operation") == "sequence")
                    foreach (var step in input["steps"]?.AsArray() ?? []) await Step(step!.AsObject());
                else await Step(input);
                return new() { ["backend"] = controller!.BackendName };
            }
            finally { controller?.ReleaseAll(); }
        }
        finally { gate.Release(); }
    }
    public void EmergencyStop() { emergency?.Cancel(); controller?.ReleaseAll(); }
    private static MouseButton ParseButton(JsonObject step) => step.String("button", "left") switch
    { "left" => MouseButton.Left, "right" => MouseButton.Right, "middle" => MouseButton.Middle, _ => throw new ArgumentException("鼠标按键无效") };
    public async ValueTask ReleaseAsync(string runId)
    {
        if (runId != owner) return;
        emergency?.Cancel();
        await gate.WaitAsync();
        try { controller?.Dispose(); controller = null; owner = ""; emergency?.Dispose(); emergency = null; }
        finally { gate.Release(); }
    }
    public void Dispose() { EmergencyStop(); controller?.Dispose(); emergency?.Dispose(); }
}

public sealed class WindowsInputBackend : IInputBackend
{
    public string Name => "Windows SendInput";
    public bool SupportsUnicodeText => true;
    public void SendKey(ScanCode key, bool isKeyUp)
    {
        var code = (ushort)key;
        Send(new() { Type = 1, Data = new() { Keyboard = new() { Scan = (ushort)(code & 0xff), Flags = 8u | (isKeyUp ? 2u : 0u) | ((code & 0xff00) == 0xe000 ? 1u : 0u) } } });
    }
    public void SendUnicode(char codeUnit, bool isKeyUp) =>
        Send(new() { Type = 1, Data = new() { Keyboard = new() { Scan = codeUnit, Flags = 4u | (isKeyUp ? 2u : 0u) } } });
    public void MoveMouse(int x, int y, bool absolute, bool virtualDesktop) =>
        Send(new() { Data = new() { Mouse = new() { X = x, Y = y, Flags = 1u | (absolute ? 0x8000u : 0u) | (virtualDesktop ? 0x4000u : 0u) } } });
    public void SendMouseButton(MouseButton button, bool isKeyUp)
    {
        var (down, up, data) = button switch
        {
            MouseButton.Left => (2u, 4u, 0u), MouseButton.Right => (8u, 16u, 0u), MouseButton.Middle => (32u, 64u, 0u),
            MouseButton.XButton1 => (128u, 256u, 1u), MouseButton.XButton2 => (128u, 256u, 2u), _ => throw new ArgumentOutOfRangeException(nameof(button))
        };
        Send(new() { Data = new() { Mouse = new() { Flags = isKeyUp ? up : down, MouseData = data } } });
    }
    public void ScrollMouse(int delta, bool horizontal) => Send(new() { Data = new() { Mouse = new() { Flags = horizontal ? 0x1000u : 0x800u, MouseData = unchecked((uint)delta) } } });
    public void Dispose() { }
    private static void Send(Input input)
    {
        if (SendInput(1, [input], Marshal.SizeOf<Input>()) != 1) throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error(), "Windows 输入被拒绝;请检查目标进程权限");
    }
    [DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint count, Input[] inputs, int size);
    [StructLayout(LayoutKind.Sequential)] private struct Input { public uint Type; public InputUnion Data; }
    [StructLayout(LayoutKind.Explicit)] private struct InputUnion
    { [FieldOffset(0)] public MouseInput Mouse; [FieldOffset(0)] public KeyboardInput Keyboard; }
    [StructLayout(LayoutKind.Sequential)] private struct MouseInput { public int X, Y; public uint MouseData, Flags, Time; public nuint ExtraInfo; }
    [StructLayout(LayoutKind.Sequential)] private struct KeyboardInput { public ushort VirtualKey, Scan; public uint Flags, Time; public nuint ExtraInfo; }
}