diff --git a/BetterGenshinImpact/Core/Script/Group/ScriptGroupConfig.cs b/BetterGenshinImpact/Core/Script/Group/ScriptGroupConfig.cs
index 5cc98bd2..2141cd5c 100644
--- a/BetterGenshinImpact/Core/Script/Group/ScriptGroupConfig.cs
+++ b/BetterGenshinImpact/Core/Script/Group/ScriptGroupConfig.cs
@@ -1,6 +1,6 @@
-using CommunityToolkit.Mvvm.ComponentModel;
+using BetterGenshinImpact.Core.Config;
+using CommunityToolkit.Mvvm.ComponentModel;
using System;
-using BetterGenshinImpact.Core.Config;
namespace BetterGenshinImpact.Core.Script.Group;
@@ -9,4 +9,10 @@ public partial class ScriptGroupConfig : ObservableObject
{
[ObservableProperty]
private PathingPartyConfig _pathingConfig = new();
+
+ ///
+ /// Shell 执行配置
+ ///
+ [ObservableProperty]
+ private ShellConfig _shellConfig = new();
}
diff --git a/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs b/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs
index 727ba343..ba3efe70 100644
--- a/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs
+++ b/BetterGenshinImpact/Core/Script/Group/ScriptGroupProject.cs
@@ -4,6 +4,7 @@ using BetterGenshinImpact.Core.Script.Project;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.GameTask.AutoPathing;
using BetterGenshinImpact.GameTask.AutoPathing.Model;
+using BetterGenshinImpact.GameTask.Shell;
using BetterGenshinImpact.ViewModel.Pages;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
@@ -12,7 +13,6 @@ using System.Dynamic;
using System.IO;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
-using BetterGenshinImpact.GameTask.Shell;
namespace BetterGenshinImpact.Core.Script.Group;
@@ -63,7 +63,6 @@ public partial class ScriptGroupProject : ObservableObject
[JsonIgnore]
public ScriptProject? Project { get; set; }
-
public ExpandoObject? JsScriptSettingsObject { get; set; }
///
@@ -71,6 +70,7 @@ public partial class ScriptGroupProject : ObservableObject
///
[JsonIgnore]
public ScriptGroup? GroupInfo { get; set; }
+
///
/// 下一个从此执行标志
///
@@ -132,6 +132,7 @@ public partial class ScriptGroupProject : ObservableObject
{
throw new Exception("FolderName 为空");
}
+
Project = new ScriptProject(FolderName);
}
@@ -143,15 +144,16 @@ public partial class ScriptGroupProject : ObservableObject
{
throw new Exception("JS脚本未初始化");
}
+
JsScriptSettingsObject ??= new ExpandoObject();
-
+
var pathingPartyConfig = GroupInfo?.Config.PathingConfig;
- if (!(pathingPartyConfig is {Enabled:true,JsScriptUseEnabled:true}))
+ if (!(pathingPartyConfig is { Enabled: true, JsScriptUseEnabled: true }))
{
pathingPartyConfig = null;
}
- await Project.ExecuteAsync(JsScriptSettingsObject,pathingPartyConfig);
+ await Project.ExecuteAsync(JsScriptSettingsObject, pathingPartyConfig);
}
else if (Type == "KeyMouse")
{
@@ -173,8 +175,9 @@ public partial class ScriptGroupProject : ObservableObject
}
else if (Type == "Shell")
{
- var task = ShellExecutor.BuildFromShellName(Name);
- await task.Execute();
+ var shellConfig = GroupInfo?.Config.ShellConfig ?? new ShellConfig();
+ var task = new ShellTask(ShellTaskParam.BuildFromConfig(Name, shellConfig));
+ await task.Start(CancellationContext.Instance.Cts.Token);
}
}
diff --git a/BetterGenshinImpact/GameTask/Shell/ShellConfig.cs b/BetterGenshinImpact/GameTask/Shell/ShellConfig.cs
new file mode 100644
index 00000000..b7f413af
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/Shell/ShellConfig.cs
@@ -0,0 +1,23 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using System;
+
+namespace BetterGenshinImpact.Core.Config;
+
+///
+/// shell执行配置
+///
+[Serializable]
+public partial class ShellConfig : ObservableObject
+{
+ // 禁用Shell任务
+ [ObservableProperty] private bool _disable;
+
+ // 最长等待命令返回的时间,单位秒,<=0不等待,直接返回。
+ [ObservableProperty] private int _timeout = 60;
+
+ // 隐藏命令执行窗口
+ [ObservableProperty] private bool _noWindow = true;
+
+ // 向log打印命令执行输出
+ [ObservableProperty] private bool _output = true;
+}
diff --git a/BetterGenshinImpact/GameTask/Shell/ShellExecutor.cs b/BetterGenshinImpact/GameTask/Shell/ShellExecutor.cs
deleted file mode 100644
index d66868da..00000000
--- a/BetterGenshinImpact/GameTask/Shell/ShellExecutor.cs
+++ /dev/null
@@ -1,121 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Threading;
-using System.Threading.Tasks;
-using BetterGenshinImpact.GameTask.Common;
-using Microsoft.Extensions.Logging;
-
-namespace BetterGenshinImpact.GameTask.Shell;
-
-[Serializable]
-public class ShellExecutor
-{
- private string command = string.Empty;
- private int maxWaitSeconds = 60;
- private bool noWindow = true;
- private bool output = true;
-
- public static readonly JsonSerializerOptions JsonOptions = new()
- {
- NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
- Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
- AllowTrailingCommas = true,
- ReadCommentHandling = JsonCommentHandling.Skip,
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
- };
-
-
- public async Task Execute(CancellationToken ct = default)
- {
- if (string.IsNullOrEmpty(command))
- {
- TaskControl.Logger.LogWarning("无法执行Shell: Shell为空");
- return;
- }
-
- var cmd = new Process();
- cmd.StartInfo.FileName = "cmd.exe";
- cmd.StartInfo.Arguments = "/k @echo off";
- cmd.StartInfo.RedirectStandardInput = true;
- cmd.StartInfo.RedirectStandardOutput = true;
- cmd.StartInfo.CreateNoWindow = noWindow;
- cmd.StartInfo.UseShellExecute = false;
- if (ct.IsCancellationRequested)
- {
- TaskControl.Logger.LogError("shell {Shell} 被取消", command);
- }
-
- TaskControl.Logger.LogInformation("执行shell:{Shell},超时时间为 {Wait} 秒", command, maxWaitSeconds);
- var timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(maxWaitSeconds));
- var mixedToken = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutSignal.Token).Token;
-
- cmd.Start();
- var outputShell = "";
- var outputText = "";
- var cmdCanceled = false;
- try
- {
- await cmd.StandardInput.WriteLineAsync(command.AsMemory(), mixedToken);
- await cmd.StandardInput.FlushAsync(mixedToken);
- cmd.StandardInput.Close();
- await cmd.WaitForExitAsync(mixedToken);
- if (output)
- {
- outputShell = await cmd.StandardOutput.ReadLineAsync(mixedToken) ?? "";
- outputText = await cmd.StandardOutput.ReadToEndAsync(mixedToken);
- }
- }
- catch (OperationCanceledException)
- {
- cmdCanceled = true;
- }
-
- if (!cmd.HasExited || cmdCanceled)
- {
- cmd.Kill();
- if (ct.IsCancellationRequested)
- {
- TaskControl.Logger.LogError("shell {Shell} 被取消", command);
- }
- else if (timeoutSignal.IsCancellationRequested)
- {
- TaskControl.Logger.LogWarning("shell {Shell} 超时", command);
- }
- else
- {
- TaskControl.Logger.LogWarning("shell {Shell} 出现异常输出,可能未能成功执行。", command);
- }
- }
-
- if (output)
- {
- TaskControl.Logger.LogInformation("shell {End} 运行结束,输出:{Output}", outputShell, outputText);
- }
- else
- {
- TaskControl.Logger.LogInformation("shell {End} 运行结束", command);
- }
-
- SystemControl.ActivateWindow();
- }
-
- public static ShellExecutor BuildFromShellName(string name)
- {
- var obj = new ShellExecutor
- {
- command = name
- };
- return obj;
- }
-
- public static ShellExecutor BuildFromJson(string json)
- {
- // 留给以后玩
- var task = JsonSerializer.Deserialize(json, JsonOptions) ??
- throw new Exception("Failed to deserialize ShellExecutorTask");
- return task;
- }
-}
\ No newline at end of file
diff --git a/BetterGenshinImpact/GameTask/Shell/ShellTask.cs b/BetterGenshinImpact/GameTask/Shell/ShellTask.cs
new file mode 100644
index 00000000..66fdd38b
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/Shell/ShellTask.cs
@@ -0,0 +1,146 @@
+using BetterGenshinImpact.GameTask.Common;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BetterGenshinImpact.GameTask.Shell;
+
+public class ShellTask(ShellTaskParam param) : ISoloTask
+{
+ public string Name => "Shell";
+
+ public Task Start(CancellationToken ct = default)
+ {
+ return Execute(ct);
+ }
+
+ private async Task Execute(CancellationToken ct)
+ {
+ if (param.Disable)
+ {
+ TaskControl.Logger.LogWarning("无法执行Shell: Shell任务被禁用");
+ return;
+ }
+
+ if (string.IsNullOrEmpty(param.Command))
+ {
+ TaskControl.Logger.LogWarning("无法执行Shell: Shell为空");
+ return;
+ }
+
+ if (ct.IsCancellationRequested)
+ {
+ TaskControl.Logger.LogError("shell {Shell} 被取消", param.Command);
+ }
+
+ TaskControl.Logger.LogInformation("执行shell:{Shell},超时时间为 {Wait} 秒", param.Command, param.TimeoutSeconds);
+
+ var mixedToken = ct;
+ var waitForExit = true;
+ CancellationTokenSource? timeoutSignal = null;
+ if (param.TimeoutSeconds > 0)
+ {
+ timeoutSignal = new CancellationTokenSource(TimeSpan.FromSeconds(param.TimeoutSeconds));
+ // 超时取消或任务被取消
+ mixedToken = CancellationTokenSource.CreateLinkedTokenSource(ct, timeoutSignal.Token).Token;
+ }
+ else
+ {
+ // timeout小于0不等待,不获取输出,仅仅启动shell即返回
+ waitForExit = false;
+ }
+
+ ShellExecutionRecord result;
+ try
+ {
+ result = await StartAndInject(param, waitForExit, mixedToken);
+ }
+ catch (OperationCanceledException)
+ {
+ if (timeoutSignal is { IsCancellationRequested: true })
+ {
+ TaskControl.Logger.LogError("shell {Shell} 执行超时", param.Command);
+ }
+
+ TaskControl.Logger.LogError("shell {Shell} 被取消", param.Command);
+ return;
+ }
+
+ if (result.End)
+ {
+ if (param.Output && result.HasOutput)
+ {
+ TaskControl.Logger.LogInformation("shell {End} 运行结束,输出:{Output}", result.Shell, result.Output);
+ return;
+ }
+
+ TaskControl.Logger.LogInformation("shell {End} 运行结束", param.Command);
+ }
+
+ SystemControl.ActivateWindow();
+ }
+
+ ///
+ /// 启动cmd并注入要执行的命令
+ ///
+ /// Task参数
+ /// 是否等待到执行结束
+ /// CancellationToken
+ private static async Task StartAndInject(ShellTaskParam param, bool waitForExit,
+ CancellationToken ct)
+ {
+ using var cmd = new Process();
+ cmd.StartInfo = BuildStartInfo(param);
+ cmd.Start();
+ await cmd.StandardInput.WriteLineAsync(param.Command.AsMemory(), ct);
+ await cmd.StandardInput.FlushAsync(ct);
+ cmd.StandardInput.Close();
+ if (!waitForExit)
+ {
+ return new ShellExecutionRecord(false, "", "");
+ }
+
+ var outputShell = "";
+ var outputText = "";
+ await cmd.WaitForExitAsync(ct);
+ if (param.Output)
+ {
+ outputShell = await cmd.StandardOutput.ReadLineAsync(ct) ?? "";
+ outputText = await cmd.StandardOutput.ReadToEndAsync(ct);
+ }
+
+ if (cmd.HasExited)
+ {
+ return new ShellExecutionRecord(true, outputShell, outputText);
+ }
+
+ cmd.Kill();
+ return new ShellExecutionRecord(false, outputShell, outputText);
+ }
+
+ private static ProcessStartInfo BuildStartInfo(ShellTaskParam param)
+ {
+ return new ProcessStartInfo
+ {
+ FileName = "cmd.exe",
+ Arguments = "/k @echo off",
+ RedirectStandardInput = true,
+ RedirectStandardOutput = true,
+ CreateNoWindow = param.NoWindow,
+ UseShellExecute = false
+ };
+ }
+
+ ///
+ /// Shell执行记录
+ ///
+ /// 是否是结束后的记录
+ /// 执行输出的Shell
+ /// Shell输出的内容
+ private record ShellExecutionRecord(bool End, string Shell, string Output)
+ {
+ public bool HasOutput => !string.IsNullOrEmpty(Output) || !string.IsNullOrEmpty(Shell);
+ }
+}
diff --git a/BetterGenshinImpact/GameTask/Shell/ShellTaskParam.cs b/BetterGenshinImpact/GameTask/Shell/ShellTaskParam.cs
new file mode 100644
index 00000000..1fbd129f
--- /dev/null
+++ b/BetterGenshinImpact/GameTask/Shell/ShellTaskParam.cs
@@ -0,0 +1,27 @@
+using BetterGenshinImpact.Core.Config;
+using BetterGenshinImpact.GameTask.Model;
+
+namespace BetterGenshinImpact.GameTask.Shell;
+
+public class ShellTaskParam : BaseTaskParam
+{
+ private ShellTaskParam(string command, int configTimeoutSeconds, bool configNoWindow, bool configOutput, bool configDisable)
+ {
+ Command = command;
+ TimeoutSeconds = configTimeoutSeconds;
+ NoWindow = configNoWindow;
+ Output = configOutput;
+ Disable = configDisable;
+ }
+
+ public readonly bool Disable;
+ public readonly string Command;
+ public readonly int TimeoutSeconds;
+ public readonly bool NoWindow;
+ public readonly bool Output;
+
+ public static ShellTaskParam BuildFromConfig(string command, ShellConfig config)
+ {
+ return new ShellTaskParam(command, config.Timeout, config.NoWindow, config.Output, config.Disable);
+ }
+}