Merge remote-tracking branch 'origin/main'

This commit is contained in:
辉鸭蛋
2026-03-14 15:56:15 +08:00
11 changed files with 222 additions and 158 deletions

View File

@@ -7,6 +7,7 @@ on:
jobs:
mirrorchyan_release_note:
if: github.repository_owner == 'babalae'
runs-on: macos-latest
steps:

View File

@@ -7,6 +7,7 @@ on:
jobs:
mirrorchyan:
if: github.repository_owner == 'babalae'
runs-on: windows-latest
steps:
- name: 📥 Download release

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyName>BetterGI</AssemblyName>
<Version>0.58.0</Version>
<Version>0.58.1-alpha.1</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>

View File

@@ -56,6 +56,12 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
/// </summary>
public event EventHandler? AutoUpdateStateChanged;
/// <summary>
/// 命令行启动时并行执行的自动更新 Task。
/// StartGameTask 结束后会 await 此 Task确保更新完成后再执行任务。
/// </summary>
public Task? CommandLineAutoUpdateTask { get; set; }
// 仓储位置
public static readonly string ReposPath = Global.Absolute("Repos");
@@ -280,13 +286,26 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
return (0, 0);
}
// 展开所有订阅路径,直接全部更新
// 展开所有订阅路径
var expandedPaths = ExpandTopLevelPaths(subscribedPaths, repoPath);
// 过滤掉仓库中已不存在的路径(幽灵订阅),避免删除用户文件后检出空内容
var validPaths = FilterExistingPaths(expandedPaths, repoPath);
// 清理订阅文件中的幽灵项:直接对原始订阅路径做过滤
if (validPaths.Count < expandedPaths.Count)
{
var cleaned = FilterExistingPaths(subscribedPaths, repoPath);
if (cleaned.Count < subscribedPaths.Count)
{
SetSubscribedPathsForCurrentRepo(cleaned);
}
}
int successCount = 0;
int failCount = 0;
foreach (var path in expandedPaths)
foreach (var path in validPaths)
{
try
{
@@ -412,6 +431,43 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
return result;
}
/// <summary>
/// 过滤掉仓库中已不存在的路径,防止幽灵订阅导致误删用户文件。
/// </summary>
private List<string> FilterExistingPaths(List<string> paths, string repoPath)
{
bool isGitRepo = IsGitRepository(repoPath);
if (isGitRepo)
{
using var repo = new Repository(repoPath);
if (repo.Head.Tip == null) return paths;
var repoTree = GetRepoSubdirectoryTree(repo);
return paths.Where(path =>
{
var parts = path.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
var currentTree = repoTree;
foreach (var part in parts)
{
var entry = currentTree[part];
if (entry == null) return false;
if (entry.TargetType == TreeEntryTargetType.Tree)
currentTree = (Tree)entry.Target;
}
return true;
}).ToList();
}
else
{
return paths.Where(path =>
{
var fullPath = Path.Combine(repoPath, path);
return Directory.Exists(fullPath) || File.Exists(fullPath);
}).ToList();
}
}
/// <summary>
/// 静默更新中央仓库(用于自动更新订阅脚本前同步最新仓库内容)。
/// 注意:此方法设计为在 _repoWriteLock 持有期间调用,
@@ -2414,73 +2470,8 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
if (oldPaths.Count == 0)
return;
// 默认归入当前仓库
var repoFolderName = GetCurrentRepoFolderName();
// 如果存在多个仓库,尝试按 repo.json 分配路径
if (Directory.Exists(ReposPath))
{
var repoDirs = Directory.GetDirectories(ReposPath)
.Where(d => !Path.GetFileName(d).Equals("Temp", StringComparison.OrdinalIgnoreCase))
.ToList();
if (repoDirs.Count > 1)
{
var repoPathSets = new Dictionary<string, HashSet<string>>();
foreach (var repoDir in repoDirs)
{
var repoJsonFile = Directory.GetFiles(repoDir, "repo.json", SearchOption.AllDirectories).FirstOrDefault();
if (string.IsNullOrEmpty(repoJsonFile)) continue;
try
{
var json = File.ReadAllText(repoJsonFile);
var jsonObj = JObject.Parse(json);
if (jsonObj["indexes"] is JArray indexes)
{
var pathSet = new HashSet<string>();
CollectAllPathsFromIndexes(indexes, "", pathSet);
repoPathSets[Path.GetFileName(repoDir)] = pathSet;
}
}
catch { /* ignore */ }
}
if (repoPathSets.Count > 1)
{
// 按仓库聚合后批量写入
var repoSubscriptions = new Dictionary<string, List<string>>();
foreach (var path in oldPaths)
{
var targetRepo = repoFolderName; // 默认归入当前仓库
foreach (var (repoName, pathSet) in repoPathSets)
{
if (pathSet.Contains(path))
{
targetRepo = repoName;
break;
}
}
if (!repoSubscriptions.ContainsKey(targetRepo))
repoSubscriptions[targetRepo] = new List<string>();
repoSubscriptions[targetRepo].Add(path);
}
foreach (var (repoName, paths) in repoSubscriptions)
{
WriteSubscriptionFile(GetSubscriptionFilePath(repoName), paths);
}
// 清空配置属性,框架自动保存
scriptConfig.SubscribedScriptPaths = new List<string>();
_logger.LogInformation("已完成订阅路径迁移到独立文件(多仓库分配)");
return;
}
}
}
// 单仓库:直接写入
WriteSubscriptionFile(GetSubscriptionFilePath(repoFolderName), new List<string>(oldPaths));
// 全部归入当前仓库,幽灵路径由后续 UpdateAllSubscribedScriptsCore 统一清理
WriteSubscriptionFile(GetSubscriptionFilePath(GetCurrentRepoFolderName()), [.. oldPaths]);
// 清空配置属性,框架自动保存
scriptConfig.SubscribedScriptPaths = new List<string>();
@@ -2492,30 +2483,6 @@ public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
}
}
/// <summary>
/// 递归收集 indexes 中所有路径(用于迁移时匹配)
/// </summary>
private static void CollectAllPathsFromIndexes(JArray nodes, string currentPath, HashSet<string> result)
{
foreach (var node in nodes)
{
if (node is JObject nodeObj)
{
var name = nodeObj["name"]?.ToString();
if (!string.IsNullOrEmpty(name))
{
var fullPath = string.IsNullOrEmpty(currentPath) ? name : $"{currentPath}/{name}";
result.Add(fullPath);
if (nodeObj["children"] is JArray children)
{
CollectAllPathsFromIndexes(children, fullPath, result);
}
}
}
}
}
// 更新订阅脚本路径列表,移除无效路径(仅处理当前仓库的订阅)
public void UpdateSubscribedScriptPaths()
{

View File

@@ -419,13 +419,6 @@ public class AutoFightTask : ISoloTask
}
#endregion
#region check动作触发战斗结束检测
if (command.Method == Method.Check)
{
fightEndFlag = await CheckFightFinish(delayTime, detectDelayTime);
}
#endregion
command.Execute(combatScenes, lastCommand);
//统计战斗人次
if (i == combatCommands.Count - 1 || command.Name != combatCommands[i + 1].Name)
@@ -433,6 +426,13 @@ public class AutoFightTask : ISoloTask
countFight++;
}
#region check动作触发战斗结束检测
if (command.Method == Method.Check)
{
fightEndFlag = await CheckFightFinish(delayTime, detectDelayTime);
}
#endregion
lastFightName = command.Name;
if (!fightEndFlag && _taskParam is { FightFinishDetectEnabled: true })
{

View File

@@ -0,0 +1,96 @@
using System;
using System.Linq;
namespace BetterGenshinImpact.Helpers;
/// <summary>
/// 命令行参数统一解析,启动时解析一次,各处查询解析结果。
/// </summary>
public class CommandLineOptions
{
private static CommandLineOptions? _instance;
public static CommandLineOptions Instance => _instance ??= Parse(Environment.GetCommandLineArgs());
public CommandLineAction Action { get; }
/// <summary>
/// startOneDragon 时可选的配置名称(第 3 个参数)
/// </summary>
public string? OneDragonConfigName { get; }
/// <summary>
/// --startGroups / --TaskProgress 时传入的组名列表(第 3 个参数起)
/// </summary>
public string[] GroupNames { get; } = [];
/// <summary>
/// 是否有命令行任务参数startOneDragon / --startGroups / --TaskProgress / start
/// </summary>
public bool HasTaskArgs => Action != CommandLineAction.None;
/// <summary>
/// 是否是需要 StartGameTask 自行处理游戏启动的命令
/// (一条龙、配置组、任务进度由各自流程中的 StartGameTask 启动游戏)
/// </summary>
public bool ShouldDeferGameStart => Action is CommandLineAction.StartOneDragon
or CommandLineAction.StartGroups
or CommandLineAction.TaskProgress;
private CommandLineOptions(CommandLineAction action, string? oneDragonConfigName = null, string[]? groupNames = null)
{
Action = action;
OneDragonConfigName = oneDragonConfigName;
GroupNames = groupNames ?? [];
}
internal static CommandLineOptions Parse(string[] args)
{
if (args.Length <= 1)
return new CommandLineOptions(CommandLineAction.None);
var arg1 = args[1].Trim();
var extra = args.Skip(2).Select(x => x.Trim()).ToArray();
if (arg1.Contains("startOneDragon", StringComparison.OrdinalIgnoreCase))
{
return new CommandLineOptions(CommandLineAction.StartOneDragon,
oneDragonConfigName: extra.Length > 0 ? extra[0] : null);
}
if (arg1.Equals("--startGroups", StringComparison.OrdinalIgnoreCase))
{
return new CommandLineOptions(CommandLineAction.StartGroups, groupNames: extra);
}
if (arg1.Equals("--TaskProgress", StringComparison.OrdinalIgnoreCase))
{
return new CommandLineOptions(CommandLineAction.TaskProgress, groupNames: extra);
}
if (arg1.Contains("start", StringComparison.OrdinalIgnoreCase))
{
return new CommandLineOptions(CommandLineAction.Start);
}
return new CommandLineOptions(CommandLineAction.None);
}
}
public enum CommandLineAction
{
/// <summary>双击启动,无命令行参数</summary>
None,
/// <summary>纯 "start" — 仅启动截图器</summary>
Start,
/// <summary>startOneDragon — 启动一条龙</summary>
StartOneDragon,
/// <summary>--startGroups — 启动调度组</summary>
StartGroups,
/// <summary>--TaskProgress — 启动任务进度</summary>
TaskProgress,
}

View File

@@ -9,8 +9,7 @@ using System.Threading.Tasks;
using System.Windows;
using BetterGenshinImpact.Core.Script;
using BetterGenshinImpact.GameTask;
using BetterGenshinImpact.GameTask.Common;
using Microsoft.Extensions.Logging;
using BetterGenshinImpact.Helpers;
using Wpf.Ui;
namespace BetterGenshinImpact.Service;
@@ -49,66 +48,56 @@ public class ApplicationHostService(IServiceProvider serviceProvider) : IHostedS
{
_navigationWindow = (serviceProvider.GetService(typeof(INavigationWindow)) as INavigationWindow)!;
_navigationWindow!.ShowWindow();
//
var args = Environment.GetCommandLineArgs();
if (args.Length > 1)
var cmdOptions = CommandLineOptions.Instance;
if (cmdOptions.HasTaskArgs)
{
//无论如何,先跳到主页,否则在通过参数的任务在执行完之前,不会加载快捷键
_ = _navigationWindow.Navigate(typeof(HomePage));
// 命令行启动时,先等待自动更新订阅脚本完成,再运行配置组/一条龙
// (正常双击启动在 MainWindowViewModel.OnLoaded 中以 fire-and-forget 方式调用)
// 命令行启动时,并行更新订阅脚本(不阻塞游戏启动和导航)
// StartGameTask 会在游戏进入主界面后等待此 Task 完成,再开始执行任务
var scriptConfig = TaskContext.Instance().Config.ScriptConfig;
if (scriptConfig.AutoUpdateBeforeCommandLineRun)
{
await Task.Run(() => ScriptRepoUpdater.Instance.AutoUpdateSubscribedScripts());
ScriptRepoUpdater.Instance.CommandLineAutoUpdateTask =
Task.Run(() => ScriptRepoUpdater.Instance.AutoUpdateSubscribedScripts());
}
if (args[1].Contains("startOneDragon", StringComparison.InvariantCultureIgnoreCase))
switch (cmdOptions.Action)
{
case CommandLineAction.StartOneDragon:
// 通过命令行参数启动「一条龙」 => 跳转到一条龙配置页。
_ = _navigationWindow.Navigate(typeof(OneDragonFlowPage));
// 后续代码在 OneDragonFlowViewModel / OnLoaded 中。
}
else if (args[1].Trim().Equals("--startGroups", StringComparison.InvariantCultureIgnoreCase))
{
// 通过命令行参数启动「调度组」 => 跳转到调度器配置页。
_ = _navigationWindow.Navigate(typeof(ScriptControlPage));
if (args.Length > 2)
{
// 获取调度组
var names = args.Skip(2).ToArray().Select(x => x.Trim()).ToArray();
// 启动调度器
var scheduler = App.GetService<ScriptControlViewModel>();
scheduler?.OnStartMultiScriptGroupWithNamesAsync(names);
}
}else if (args[1].Trim().Equals("--TaskProgress", StringComparison.InvariantCultureIgnoreCase))
{
break;
case CommandLineAction.StartGroups:
// 通过命令行参数启动「调度组」 => 跳转到调度器配置页。
_ = _navigationWindow.Navigate(typeof(ScriptControlPage));
if (args.Length > 1)
if (cmdOptions.GroupNames.Length > 0)
{
// 获取调度组
var names = args.Skip(2).ToArray().Select(x => x.Trim()).ToArray();
// 启动调度器
var scheduler = App.GetService<ScriptControlViewModel>();
scheduler?.OnStartMultiScriptTaskProgressAsync(names);
scheduler?.OnStartMultiScriptGroupWithNamesAsync(cmdOptions.GroupNames);
}
}
else if (args[1].Contains("start"))
break;
case CommandLineAction.TaskProgress:
// 通过命令行参数启动「任务进度」 => 跳转到调度器配置页。
_ = _navigationWindow.Navigate(typeof(ScriptControlPage));
if (cmdOptions.GroupNames.Length > 0)
{
var scheduler = App.GetService<ScriptControlViewModel>();
scheduler?.OnStartMultiScriptTaskProgressAsync(cmdOptions.GroupNames);
}
break;
case CommandLineAction.Start:
// 通过命令行参数打开「启动页开关」 => 跳转到主页。
_ = _navigationWindow.Navigate(typeof(HomePage));
// 后续代码在 HomePageViewModel / OnLoaded 中。
}
else
{
// 其它命令行参数 => 跳转到主页。
_ = _navigationWindow.Navigate(typeof(HomePage));
break;
}
}
else

View File

@@ -621,5 +621,13 @@ public partial class ScriptService : IScriptService
});
}
}
// 等待命令行启动时并行执行的自动更新完成(如果有)
var pendingUpdate = ScriptRepoUpdater.Instance.CommandLineAutoUpdateTask;
if (pendingUpdate != null)
{
await pendingUpdate;
ScriptRepoUpdater.Instance.CommandLineAutoUpdateTask = null;
}
}
}

View File

@@ -250,7 +250,7 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel
// 预热OCR
await OcrPreheating();
if (Environment.GetCommandLineArgs().Length > 1)
if (CommandLineOptions.Instance.HasTaskArgs)
{
return;
}

View File

@@ -133,8 +133,9 @@ public partial class HomePageViewModel : ViewModel
_autoRun = false;
var args = Environment.GetCommandLineArgs();
if (args.Length > 1 && args[1].Contains("start"))
// 只对纯 "start" 参数自动启动截图器
// startOneDragon、--startGroups 等由各自流程中的 StartGameTask 处理
if (CommandLineOptions.Instance.Action == CommandLineAction.Start)
{
_ = OnStartTriggerAsync();
}

View File

@@ -545,15 +545,16 @@ public partial class OneDragonFlowViewModel : ViewModel
}
_autoRun = false;
//
var args = Environment.GetCommandLineArgs();
if (args.Length > 1 && args[1].Contains("startOneDragon"))
var cmdOptions = CommandLineOptions.Instance;
if (cmdOptions.Action == CommandLineAction.StartOneDragon)
{
// 通过命令行参数启动一条龙。
if (args.Length > 2)
if (cmdOptions.OneDragonConfigName != null)
{
// 从命令行参数中提取一条龙配置名称。
_logger.LogInformation($"参数指定的一条龙配置:{args[2]}");
var argsOneDragonConfig = ConfigList.FirstOrDefault(x => x.Name == args[2], null);
_logger.LogInformation($"参数指定的一条龙配置:{cmdOptions.OneDragonConfigName}");
var argsOneDragonConfig = ConfigList.FirstOrDefault(x =>
string.Equals(x.Name, cmdOptions.OneDragonConfigName, StringComparison.Ordinal));
if (argsOneDragonConfig != null)
{
// 设定配置,配置下拉框会选定。