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

XFEWorkflow

【WinUI】XFE基于windows的工作流

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEWorkflow.Abstractions;
using XFEWorkflow.Plugins.WeCom.Native.Core;

namespace XFEWorkflow.Plugins.WeCom;

public sealed class WeComPlugin : IWorkflowPlugin
{
    public PluginManifest Manifest
    {
        get
        {
            using var source = typeof(WeComPlugin).Assembly.GetManifestResourceStream("XFEWorkflow.Plugins.WeCom.plugin.json")!;
            return JsonSerializer.Deserialize<PluginManifest>(source, WorkflowJson.Options)!;
        }
    }
    public void Register(IPluginRegistry registry)
    {
        foreach (var capability in Manifest.Capabilities)
            if (capability.IsTrigger) registry.RegisterTrigger(capability, () => new WeComMessageTrigger());
            else registry.RegisterAction(capability, () => new WeComAction(capability.Id));
    }
}

public sealed class WeComMessageTrigger : IWorkflowTrigger
{
    public async Task RunAsync(JsonObject parameters, IWorkflowContext context, Func<JsonObject, ValueTask> emit, CancellationToken ct)
    {
        var receive = new JsonObject { ["_deliveryMode"] = "all", ["_workflowId"] = parameters["_workflowId"]?.DeepClone(), ["_priority"] = parameters["_priority"]?.DeepClone() };
        var subscription = (await context.InvokeHostAsync("wecom.subscribe", receive, ct)).String("subscription");
        try
        {
            while (true)
            {
                var message = await context.InvokeHostAsync("wecom.next", new() { ["subscription"] = subscription }, ct);
                await emit(message);
                // The desktop processes trigger events before reading this request. This
                // acknowledgement therefore happens after the triggered workflow finishes.
                await context.InvokeHostAsync("wecom.ack", new() { ["subscription"] = subscription, ["dispatchId"] = message.String("dispatchId") }, ct);
            }
        }
        finally
        {
            try { await context.InvokeHostAsync("wecom.unsubscribe", new() { ["subscription"] = subscription }, CancellationToken.None).AsTask().WaitAsync(TimeSpan.FromSeconds(2)); }
            catch (Exception) { /* The desktop also releases subscriptions when this Runner exits. */ }
        }
    }
}

public sealed class WeComAction(string capabilityId) : IWorkflowAction
{
    private static readonly HttpClient http = new(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(5) }) { Timeout = Timeout.InfiniteTimeSpan };
    public async ValueTask<JsonObject> ExecuteAsync(JsonObject input, IWorkflowContext context, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        if (capabilityId == "xfe.wecom.send") return await SendWebhookAsync(input, context, ct);
        if (capabilityId.StartsWith("xfe.wecom.filter.", StringComparison.Ordinal)) return Filter(capabilityId, input);
        var operation = capabilityId["xfe.wecom.".Length..];
        var parameters = WorkflowJson.Copy(input);
        if (operation is "quoteReply" or "dryRun")
        { parameters[operation == "quoteReply" ? "quoteOriginal" : "dryRun"] = true; operation = "reply"; }
        var output = await context.InvokeHostAsync("wecom." + operation, parameters, ct);
        if (operation is "reply" or "selfTest") await context.LogAsync("企业微信 · " + output.Text(), ct);
        // A result object is available on every native action, alongside its named returns.
        output["result"] = output.DeepClone(); return output;
    }
    public static JsonObject Filter(string id, JsonObject input)
    {
        var message = input["message"] as JsonObject ?? throw new ArgumentException("请连接企业微信触发节点的 message 消息引用输出");
        bool matched;
        switch (id)
        {
            case "xfe.wecom.filter.group":
                var groupId = input.String("groupId"); var groupName = input.String("groupName");
                if (groupId.Length == 0 && groupName.Length == 0) throw new ArgumentException("请填写群 ID 或群名称");
                matched = (groupId.Length == 0 || message.String("conversationId") == groupId) && (groupName.Length == 0 || TextMatching.SameDisplayName(message.String("groupName"), groupName)); break;
            case "xfe.wecom.filter.member":
                var members = MemberIdFilter.Resolve(input.String("memberIds"), "");
                if (members.Any(x => !MemberIdFilter.IsNativeId(x))) throw new ArgumentException("请填写完整数字成员 ID");
                matched = members.Contains(message.String("senderId"), StringComparer.Ordinal); break;
            case "xfe.wecom.filter.kind":
                var kind = input.String("messageKind", "MergedForward");
                if (kind is not ("Any" or "Text" or "MergedForward")) throw new ArgumentException("未知消息类型");
                matched = kind == "Any" || message.String("kind") == kind; break;
            case "xfe.wecom.filter.keyword":
                if (!message.Boolean("hasContent")) throw new NotSupportedException("当前原生接口只提供消息元数据,没有正文,不能执行关键词筛选");
                var keywords = input.String("keywords").Split(['\r', '\n', ',', ',', ';', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(TextMatching.Normalize).Where(x => x.Length > 0).ToArray();
                if (keywords.Length == 0) throw new ArgumentException("请填写至少一个关键词");
                var content = TextMatching.Normalize(message.String("content"));
                matched = input.Boolean("matchAll") ? keywords.All(x => content.Contains(x, StringComparison.OrdinalIgnoreCase)) : keywords.Any(x => content.Contains(x, StringComparison.OrdinalIgnoreCase)); break;
            default: throw new NotSupportedException(id);
        }
        return new() { ["matched"] = matched, ["branch"] = matched ? "match" : "noMatch", ["message"] = message.DeepClone() };
    }
    private static async Task<JsonObject> SendWebhookAsync(JsonObject input, IWorkflowContext context, CancellationToken ct)
    {
        var webhook = input.String("webhook"); if (input.String("credential") is { Length: > 0 } reference) webhook = await context.GetSecretAsync(reference, ct);
        if (!Uri.TryCreate(webhook, UriKind.Absolute, out var uri) || uri.Scheme is not ("http" or "https")) throw new ArgumentException("请输入 HTTP/HTTPS 群机器人地址或凭据引用");
        var format = input.String("format", "text"); if (format is not ("text" or "markdown")) throw new ArgumentException("机器人只支持 text 和 markdown");
        var payload = new JsonObject { ["content"] = input["content"].Text() };
        if (format == "text" && input["mentionedList"] is JsonArray mentions) payload["mentioned_list"] = mentions.DeepClone();
        using var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = new StringContent(new JsonObject { ["msgtype"] = format, [format] = payload }.ToJsonString(), Encoding.UTF8, "application/json") };
        using var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct); response.EnsureSuccessStatusCode();
        if (response.Content.Headers.ContentLength > 1024 * 1024) throw new IOException("机器人响应超过 1 MiB");
        await using var stream = await response.Content.ReadAsStreamAsync(ct); using var buffer = new MemoryStream(); var bytes = new byte[8192];
        int read; while ((read = await stream.ReadAsync(bytes, ct)) > 0) { if (buffer.Length + read > 1024 * 1024) throw new IOException("机器人响应超过 1 MiB"); await buffer.WriteAsync(bytes.AsMemory(0, read), ct); }
        var result = JsonNode.Parse(buffer.ToArray())?.AsObject() ?? throw new IOException("机器人返回了空响应");
        if (result.Integer("errcode") != 0) throw new InvalidOperationException("企业微信拒绝通知:" + result.String("errmsg"));
        return new() { ["response"] = result };
    }
}