diff --git a/docs/3.8-远程控制协议.md b/docs/3.8-远程控制协议.md index 34af693363..47a16b5549 100644 --- a/docs/3.8-远程控制协议.md +++ b/docs/3.8-远程控制协议.md @@ -33,6 +33,7 @@ MAA 会以 1 秒的间隔持续轮询这个端点,尝试获取他要执行的 { "tasks": // 需要让MAA执行的Task的列表,目前可以支持的类型如示例中所示,如果不存在tasks则视为连接无效。 [ + // 顺序执行的任务:下面这些任务会按照下发的顺序排队执行 { "id": "b353c469-b902-4357-bd8f-d133199eea31", //任务的唯一id,字符串类型,在汇报任务时会使用 "type": "CaptureImage", //截图任务,会截取一张当前模拟器的截图,并以Base64字符串的形式放在汇报任务的payload里。如果你需要下发这种类型的任务,请务必注意你的端点可接受的最大请求大小,因为截图会有数十MB,会超过一般网关的默认大小限制。 @@ -54,6 +55,19 @@ MAA 会以 1 秒的间隔持续轮询这个端点,尝试获取他要执行的 "type": "Settings-ConnectionAddress", //修改配置项的任务,等同于执行ConfigurationHelper.SetValue("ConnectionAddress", params); 为了安全起见,不是每个配置都可以修改,能修改的配置详见下述。 "params": "value" //要修改的值 }, + // 立即执行任务:下面这些任务可以在顺序执行任务运行中执行,并且MAA保证下面的任何一个任务都会尽快返回结果,通常用于对远程控制功能本身的控制。 + { + "id": "b353c469-b902-4357-bd8f-d133199eea31", //任务的唯一id,字符串类型,在汇报任务时会使用 + "type": "CaptureImageNow", //立刻截图任务,和上面的截图任务是基本一样的,唯一的区别是这个任务会立刻被运行,而不会等待其他任务。 + }, + { + "id": "b353c469-b902-4357-bd8f-d133199eea31", //任务的唯一id,字符串类型,在汇报任务时会使用 + "type": "StopTask", //"结束当前任务"任务,将会尝试结束当前运行的任务。如果任务列表还有其他任务会继续开始执行下一个。该任务不会等待并确认当前任务已停止才会返回,因此请使用心跳任务来确认停止命令是否已生效。 + }, + { + "id": "b353c469-b902-4357-bd8f-d133199eea31", //任务的唯一id,字符串类型,在汇报任务时会使用 + "type": "HeartBeat", //心跳任务,该任务会立即返回,并且将当前“顺序执行的任务”队列中正在执行的任务的Id作为Payload返回,如果当前没有任务执行,返回空字符串。 + }, ], ... // 如果你的这个端点还有其他用途,你可以自行添加可选的返回值,但是MAA只会读取tasks } @@ -64,8 +78,9 @@ MAA 会以 1 秒的间隔持续轮询这个端点,尝试获取他要执行的 ::: note - LinkStart-[TaskName] 型的任务 type 的可选值为 LinkStart-Base,LinkStart-WakeUp,LinkStart-Combat,LinkStart-Recruiting,LinkStart-Mall,LinkStart-Mission,LinkStart-AutoRoguelike,LinkStart-ReclamationAlgorithm -- Settings-[SettingsName] 型的任务的 type 的可选值为 Settings-ConnectionAddress +- Settings-[SettingsName] 型的任务的 type 的可选值为 Settings-ConnectionAddress, Settings-Stage1 - Settings 系列任务仍然是要按顺序执行的,并不会在收到任务的时候立刻执行,而是排在上一个任务的后面 +- 多个立即执行的任务也会按下发顺序执行,只不过这些任务的执行速度都很快,通常来说,并不需要关注他们的顺序。 ::: ## 汇报任务端点 diff --git a/src/MaaWpfGui/Services/RemoteControl/RemoteControlService.cs b/src/MaaWpfGui/Services/RemoteControl/RemoteControlService.cs index df66b78c3b..67156541c9 100644 --- a/src/MaaWpfGui/Services/RemoteControl/RemoteControlService.cs +++ b/src/MaaWpfGui/Services/RemoteControl/RemoteControlService.cs @@ -26,11 +26,15 @@ namespace MaaWpfGui.Services.RemoteControl public class RemoteControlService { private readonly Task _pollJobTask = Task.CompletedTask; - private readonly Task _executeJobTask = Task.CompletedTask; - private readonly List _executedTaskIds = new List(); - private readonly ConcurrentQueue _taskQueue = new ConcurrentQueue(); + private readonly List _enqueueTaskIds = new List(); + private readonly ConcurrentQueue _sequentialTaskQueue = new ConcurrentQueue(); + private readonly ConcurrentQueue _instantTaskQueue = new ConcurrentQueue(); + private readonly Task _executeSequentialJobTask = Task.CompletedTask; + private readonly Task _executeInstantJobTask = Task.CompletedTask; private readonly RunningState _runningState; + private string _currentSequentialTaskId = string.Empty; + public RemoteControlService() { _pollJobTask = _pollJobTask.ContinueWith(async _ => @@ -51,14 +55,32 @@ namespace MaaWpfGui.Services.RemoteControl // ReSharper disable once FunctionNeverReturns }); - _executeJobTask = _executeJobTask.ContinueWith(async _ => + _executeSequentialJobTask = _executeSequentialJobTask.ContinueWith(async _ => { while (true) { await Task.Delay(1000); try { - await ExecuteJobLoop(); + await ExecuteSequentialJobLoop(); + } + catch (Exception ex) + { + Log.Logger.Error(ex, "RemoteControl service raises unknown error."); + } + } + + // ReSharper disable once FunctionNeverReturns + }); + + _executeInstantJobTask = _executeInstantJobTask.ContinueWith(async _ => + { + while (true) + { + await Task.Delay(1000); + try + { + await ExecuteInstantJobLoop(); } catch (Exception ex) { @@ -193,6 +215,8 @@ namespace MaaWpfGui.Services.RemoteControl #endregion + #region JobLoops + private async Task PollJobTaskLoop() { var endpoint = Instances.SettingsViewModel.RemoteControlGetTaskEndpointUri; @@ -226,20 +250,44 @@ namespace MaaWpfGui.Services.RemoteControl // It is a valid task var id = task.GetValue("id")?.Value(); - if (_executedTaskIds.Contains(id)) + if (_enqueueTaskIds.Contains(id)) { continue; } - _executedTaskIds.Add(id); - _taskQueue.Enqueue(task); + _enqueueTaskIds.Add(id); + + switch (type) + { + case "LinkStart": + case "LinkStart-Base": + case "LinkStart-WakeUp": + case "LinkStart-Combat": + case "LinkStart-Recruiting": + case "LinkStart-Mall": + case "LinkStart-Mission": + case "LinkStart-AutoRoguelike": + case "LinkStart-ReclamationAlgorithm": + case "Toolbox-GachaOnce": + case "Toolbox-GachaTenTimes": + case "CaptureImage": + case "Settings-ConnectAddress": + case "Settings-Stage1": + _sequentialTaskQueue.Enqueue(task); + break; + case "CaptureImageNow": + case "HeartBeat": + case "StopTask": + _instantTaskQueue.Enqueue(task); + break; + } } } } - private async Task ExecuteJobLoop() + private async Task ExecuteSequentialJobLoop() { - if (_taskQueue.TryDequeue(out var task)) + if (_sequentialTaskQueue.TryDequeue(out var task)) { var type = task.GetValue("type")?.Value(); var id = task.GetValue("id")?.Value(); @@ -247,6 +295,8 @@ namespace MaaWpfGui.Services.RemoteControl var payload = string.Empty; var status = "SUCCESS"; + _currentSequentialTaskId = id; + switch (type) { case "LinkStart": @@ -343,8 +393,106 @@ namespace MaaWpfGui.Services.RemoteControl { Instances.SettingsViewModel.ConnectAddress = data; }); - break; + case "Settings-Stage1": + Application.Current.Dispatcher.Invoke(() => + { + Instances.TaskQueueViewModel.Stage1 = data; + }); + break; + + // ReSharper disable once RedundantEmptySwitchSection + default: + // 未知的Type统一直接发给MAACore + // No! 未知的任务一概不处理 + break; + } + + var endpoint = Instances.SettingsViewModel.RemoteControlReportStatusUri; + if (!string.IsNullOrWhiteSpace(endpoint) && endpoint.ToLower().StartsWith("https://")) + { + var uid = Instances.SettingsViewModel.RemoteControlUserIdentity; + var did = Instances.SettingsViewModel.RemoteControlDeviceIdentity; + var response = await Instances.HttpService.PostAsJsonAsync(new Uri(endpoint), new + { + user = uid, + device = did, + status, + task = id, + payload, + }); + if (response == null) + { + Log.Logger.Error("RemoteControlService report task failed."); + } + } + + _currentSequentialTaskId = string.Empty; + } + } + + private async Task ExecuteInstantJobLoop() + { + if (_instantTaskQueue.TryDequeue(out var task)) + { + var type = task.GetValue("type")?.Value(); + var id = task.GetValue("id")?.Value(); + var data = task.GetValue("params")?.Value(); + var payload = string.Empty; + var status = "SUCCESS"; + + switch (type) + { + case "HeartBeat": + { + payload = _currentSequentialTaskId; + break; + } + + case "StopTask": + { + await Task.Run(() => + { + if (!Instances.AsstProxy.AsstStop()) + { + // 无法确定当前的界面,找不到借用的UI位置,因此只能Log + Log.Logger.Error("Failed to stop Asst."); + } + }); + + // 无需等待,甩出任务即可返回,远端应该用心跳来确认界面卡死和取消是否成功。 + break; + } + + case "CaptureImageNow": + { + string errMsg = string.Empty; + bool connected = await Task.Run(() => Instances.AsstProxy.AsstConnect(ref errMsg)); + if (connected) + { + var image = Instances.AsstProxy.AsstGetImage(); + if (image == null) + { + status = "FAILED"; + break; + } + + byte[] bytes; + using (MemoryStream stream = new MemoryStream()) + { + PngBitmapEncoder encoder = new PngBitmapEncoder(); + encoder.Frames.Add(BitmapFrame.Create(image)); + encoder.Save(stream); + bytes = stream.ToArray(); + } + + payload = Convert.ToBase64String(bytes); + break; + } + + status = "FAILED"; + break; + } // ReSharper disable once RedundantEmptySwitchSection default: @@ -374,6 +522,8 @@ namespace MaaWpfGui.Services.RemoteControl } } + #endregion + /// /// 根据"一键长草"功能进行修改的方法。 /// diff --git a/src/MaaWpfGui/Views/UserControl/RemoteControlUserControl.xaml b/src/MaaWpfGui/Views/UserControl/RemoteControlUserControl.xaml index ca7ae45294..bc4b7aff2e 100644 --- a/src/MaaWpfGui/Views/UserControl/RemoteControlUserControl.xaml +++ b/src/MaaWpfGui/Views/UserControl/RemoteControlUserControl.xaml @@ -33,6 +33,7 @@ Grid.Column="0" Grid.ColumnSpan="3" Margin="10" + HorizontalAlignment="Center" Block.TextAlignment="Center" Text="{DynamicResource RemoteControlTooltips}" TextWrapping="Wrap" />