Skip to content

feat: update C API bindings to MaaFramework v5.10.5#27

Merged
moomiji merged 15 commits into
mainfrom
copilot/fix-cs-binding-issues
Jul 8, 2026
Merged

feat: update C API bindings to MaaFramework v5.10.5#27
moomiji merged 15 commits into
mainfrom
copilot/fix-cs-binding-issues

Conversation

Copilot AI commented Apr 13, 2026

Copy link
Copy Markdown
Contributor

Audit of MaaFramework/include headers against the C# binding, filling all gaps.

New controller types

  • MaaMacOSController — wraps MaaMacOSControllerCreate(window_id, screencap_method, input_method)
  • MaaAndroidNativeController — wraps MaaAndroidNativeControllerCreate(config_json)
  • MaaReplayController — wraps MaaReplayControllerCreate(recording_path)
  • MaaRecordController — wraps MaaRecordControllerCreate(inner, recording_path)

Updated MaaDbgController

MaaDbgControllerCreate signature changed upstream from 4 params (readPath, writePath, type, config) to 1 param (readPath). Updated P/Invoke and wrapper accordingly.

New controller action

  • MaaControllerPostRelativeMove(ctrl, dx, dy) exposed as IMaaController.RelativeMove(int dx, int dy)

New tasker query

  • MaaTaskerGetWaitFreezesDetail exposed as IMaaTasker.GetWaitFreezesDetail(...) with the same two-pass size-query pattern used by GetTaskDetail

macOS toolkit

  • P/Invoke for MaaToolkitMacOSCheckPermission, MaaToolkitMacOSRequestPermission, MaaToolkitMacOSRevealPermissionSettings
  • IMaaToolkitMacOS interface + implementation on MaaToolkit.MacOS

New enums & type aliases

  • MacOSScreencapMethod, MacOSInputMethod, MacOSPermission enums
  • MaaWfId, MaaMacOSScreencapMethod, MaaMacOSInputMethod, MaaMacOSPermission global using aliases

New MaaMsg entries

  • MaaMsg.Node.WaitFreezes.{Starting,Prefix,Succeeded,Failed}

Usage example

// macOS controller
using var ctrl = new MaaMacOSController(
    windowId: 0,
    screencapMethod: MacOSScreencapMethod.ScreenCaptureKit,
    inputMethod: MacOSInputMethod.GlobalEvent);

// Record + replay
using var record = new MaaRecordController(innerCtrl, "recording.jsonl");
using var replay = new MaaReplayController("recording.jsonl");

// Relative move
ctrl.RelativeMove(dx: 10, dy: -5);

// macOS permissions
bool granted = tasker.Toolkit.MacOS.CheckPermission(MacOSPermission.ScreenCapture);

Summary by Sourcery

使 C# MaaFramework 绑定与最新的 C API 对齐,补充缺失的 controller/toolkit 能力以及 wait-freeze 诊断功能。

New Features:

  • 通过新的 C# 包装类和 interop 绑定,暴露 macOS、Android 原生、record 和 replay controllers。
  • 添加 IMaaController.RelativeMove 以支持相对指针移动,并将其接入原生及自定义 controllers。
  • IMaaTasker 和通知扩展中暴露 MaaTaskerGetWaitFreezesDetail,以提供详细的 wait-freeze 诊断信息。
  • 通过 IMaaToolkit.MacOS 以及对应的 MaaToolkitMacOS P/Invoke 绑定,引入 macOS toolkit 权限辅助工具。
  • 添加 macOS 特定的 screencap、input 与权限枚举,以及 MaaWfId 别名和 Node.WaitFreezesMaaMsg 条目。

Enhancements:

  • 更新 MaaDbgController 及相关 interop,使其匹配简化后的 MaaDbgControllerCreate 签名,并改进 XML 文档/注释。
  • 扩展 controller 选项与 marshalling 辅助方法,以支持鼠标锁定跟随模式、截图缩放方法以及由后台管理的按键域。
  • 收紧通知与详情模型(ID 改为 Int64,确保焦点字段非空,引入新的枚举解析),并在 tasker interop 中启用可空注解。

Tests:

  • 扩展 controller 选项和 shell 相关的单元测试,以覆盖新的选项和参数名称,并为 RelativeMove/Shell 添加自定义 controller 测试。
Original summary in English

Summary by Sourcery

Align C# MaaFramework bindings with the latest C API by adding missing controller/toolkit capabilities and wait-freeze diagnostics.

New Features:

  • Expose macOS, Android native, record, and replay controllers through new C# wrapper classes and interop bindings.
  • Add IMaaController.RelativeMove to support relative pointer movement and wire it through native and custom controllers.
  • Expose MaaTaskerGetWaitFreezesDetail in IMaaTasker and notification extensions to provide detailed wait-freeze diagnostics.
  • Introduce macOS toolkit permission helpers via IMaaToolkit.MacOS and corresponding MaaToolkitMacOS P/Invoke bindings.
  • Add macOS-specific screencap, input, and permission enums plus MaaWfId aliases and Node.WaitFreezes MaaMsg entries.

Enhancements:

  • Update MaaDbgController and related interop to match the simplified MaaDbgControllerCreate signature and improve XML documentation/comments.
  • Extend controller options and marshalling helpers to support mouse-lock follow mode, screenshot resize method, and background-managed key domains.
  • Tighten notification and detail models (IDs as Int64, non-null focus fields, new enum parsing) and enable nullable annotations in tasker interop.

Tests:

  • Expand controller option and shell-related unit tests to cover new options and parameter names, and add custom controller tests for RelativeMove/Shell.
Original summary in English

Summary by Sourcery

使 C# MaaFramework 绑定与最新的 C API 对齐,补充缺失的 controller/toolkit 能力以及 wait-freeze 诊断功能。

New Features:

  • 通过新的 C# 包装类和 interop 绑定,暴露 macOS、Android 原生、record 和 replay controllers。
  • 添加 IMaaController.RelativeMove 以支持相对指针移动,并将其接入原生及自定义 controllers。
  • IMaaTasker 和通知扩展中暴露 MaaTaskerGetWaitFreezesDetail,以提供详细的 wait-freeze 诊断信息。
  • 通过 IMaaToolkit.MacOS 以及对应的 MaaToolkitMacOS P/Invoke 绑定,引入 macOS toolkit 权限辅助工具。
  • 添加 macOS 特定的 screencap、input 与权限枚举,以及 MaaWfId 别名和 Node.WaitFreezesMaaMsg 条目。

Enhancements:

  • 更新 MaaDbgController 及相关 interop,使其匹配简化后的 MaaDbgControllerCreate 签名,并改进 XML 文档/注释。
  • 扩展 controller 选项与 marshalling 辅助方法,以支持鼠标锁定跟随模式、截图缩放方法以及由后台管理的按键域。
  • 收紧通知与详情模型(ID 改为 Int64,确保焦点字段非空,引入新的枚举解析),并在 tasker interop 中启用可空注解。

Tests:

  • 扩展 controller 选项和 shell 相关的单元测试,以覆盖新的选项和参数名称,并为 RelativeMove/Shell 添加自定义 controller 测试。
Original summary in English

Summary by Sourcery

Align C# MaaFramework bindings with the latest C API by adding missing controller/toolkit capabilities and wait-freeze diagnostics.

New Features:

  • Expose macOS, Android native, record, and replay controllers through new C# wrapper classes and interop bindings.
  • Add IMaaController.RelativeMove to support relative pointer movement and wire it through native and custom controllers.
  • Expose MaaTaskerGetWaitFreezesDetail in IMaaTasker and notification extensions to provide detailed wait-freeze diagnostics.
  • Introduce macOS toolkit permission helpers via IMaaToolkit.MacOS and corresponding MaaToolkitMacOS P/Invoke bindings.
  • Add macOS-specific screencap, input, and permission enums plus MaaWfId aliases and Node.WaitFreezes MaaMsg entries.

Enhancements:

  • Update MaaDbgController and related interop to match the simplified MaaDbgControllerCreate signature and improve XML documentation/comments.
  • Extend controller options and marshalling helpers to support mouse-lock follow mode, screenshot resize method, and background-managed key domains.
  • Tighten notification and detail models (IDs as Int64, non-null focus fields, new enum parsing) and enable nullable annotations in tasker interop.

Tests:

  • Expand controller option and shell-related unit tests to cover new options and parameter names, and add custom controller tests for RelativeMove/Shell.
Original summary in English

Summary by Sourcery

使 C# MaaFramework 绑定与最新的 C API 对齐,补充缺失的 controller/toolkit 能力以及 wait-freeze 诊断功能。

New Features:

  • 通过新的 C# 包装类和 interop 绑定,暴露 macOS、Android 原生、record 和 replay controllers。
  • 添加 IMaaController.RelativeMove 以支持相对指针移动,并将其接入原生及自定义 controllers。
  • IMaaTasker 和通知扩展中暴露 MaaTaskerGetWaitFreezesDetail,以提供详细的 wait-freeze 诊断信息。
  • 通过 IMaaToolkit.MacOS 以及对应的 MaaToolkitMacOS P/Invoke 绑定,引入 macOS toolkit 权限辅助工具。
  • 添加 macOS 特定的 screencap、input 与权限枚举,以及 MaaWfId 别名和 Node.WaitFreezesMaaMsg 条目。

Enhancements:

  • 更新 MaaDbgController 及相关 interop,使其匹配简化后的 MaaDbgControllerCreate 签名,并改进 XML 文档/注释。
  • 扩展 controller 选项与 marshalling 辅助方法,以支持鼠标锁定跟随模式、截图缩放方法以及由后台管理的按键域。
  • 收紧通知与详情模型(ID 改为 Int64,确保焦点字段非空,引入新的枚举解析),并在 tasker interop 中启用可空注解。

Tests:

  • 扩展 controller 选项和 shell 相关的单元测试,以覆盖新的选项和参数名称,并为 RelativeMove/Shell 添加自定义 controller 测试。
Original summary in English

Summary by Sourcery

Align C# MaaFramework bindings with the latest C API by adding missing controller/toolkit capabilities and wait-freeze diagnostics.

New Features:

  • Expose macOS, Android native, record, and replay controllers through new C# wrapper classes and interop bindings.
  • Add IMaaController.RelativeMove to support relative pointer movement and wire it through native and custom controllers.
  • Expose MaaTaskerGetWaitFreezesDetail in IMaaTasker and notification extensions to provide detailed wait-freeze diagnostics.
  • Introduce macOS toolkit permission helpers via IMaaToolkit.MacOS and corresponding MaaToolkitMacOS P/Invoke bindings.
  • Add macOS-specific screencap, input, and permission enums plus MaaWfId aliases and Node.WaitFreezes MaaMsg entries.

Enhancements:

  • Update MaaDbgController and related interop to match the simplified MaaDbgControllerCreate signature and improve XML documentation/comments.
  • Extend controller options and marshalling helpers to support mouse-lock follow mode, screenshot resize method, and background-managed key domains.
  • Tighten notification and detail models (IDs as Int64, non-null focus fields, new enum parsing) and enable nullable annotations in tasker interop.

Tests:

  • Expand controller option and shell-related unit tests to cover new options and parameter names, and add custom controller tests for RelativeMove/Shell.
Original summary in English

Summary by Sourcery

使 C# MaaFramework 绑定与最新的 C API 对齐,补充缺失的 controller/toolkit 能力以及 wait-freeze 诊断功能。

New Features:

  • 通过新的 C# 包装类和 interop 绑定,暴露 macOS、Android 原生、record 和 replay controllers。
  • 添加 IMaaController.RelativeMove 以支持相对指针移动,并将其接入原生及自定义 controllers。
  • IMaaTasker 和通知扩展中暴露 MaaTaskerGetWaitFreezesDetail,以提供详细的 wait-freeze 诊断信息。
  • 通过 IMaaToolkit.MacOS 以及对应的 MaaToolkitMacOS P/Invoke 绑定,引入 macOS toolkit 权限辅助工具。
  • 添加 macOS 特定的 screencap、input 与权限枚举,以及 MaaWfId 别名和 Node.WaitFreezesMaaMsg 条目。

Enhancements:

  • 更新 MaaDbgController 及相关 interop,使其匹配简化后的 MaaDbgControllerCreate 签名,并改进 XML 文档/注释。
  • 扩展 controller 选项与 marshalling 辅助方法,以支持鼠标锁定跟随模式、截图缩放方法以及由后台管理的按键域。
  • 收紧通知与详情模型(ID 改为 Int64,确保焦点字段非空,引入新的枚举解析),并在 tasker interop 中启用可空注解。

Tests:

  • 扩展 controller 选项和 shell 相关的单元测试,以覆盖新的选项和参数名称,并为 RelativeMove/Shell 添加自定义 controller 测试。
Original summary in English

Summary by Sourcery

Align C# MaaFramework bindings with the latest C API by adding missing controller/toolkit capabilities and wait-freeze diagnostics.

New Features:

  • Expose macOS, Android native, record, and replay controllers through new C# wrapper classes and interop bindings.
  • Add IMaaController.RelativeMove to support relative pointer movement and wire it through native and custom controllers.
  • Expose MaaTaskerGetWaitFreezesDetail in IMaaTasker and notification extensions to provide detailed wait-freeze diagnostics.
  • Introduce macOS toolkit permission helpers via IMaaToolkit.MacOS and corresponding MaaToolkitMacOS P/Invoke bindings.
  • Add macOS-specific screencap, input, and permission enums plus MaaWfId aliases and Node.WaitFreezes MaaMsg entries.

Enhancements:

  • Update MaaDbgController and related interop to match the simplified MaaDbgControllerCreate signature and improve XML documentation/comments.
  • Extend controller options and marshalling helpers to support mouse-lock follow mode, screenshot resize method, and background-managed key domains.
  • Tighten notification and detail models (IDs as Int64, non-null focus fields, new enum parsing) and enable nullable annotations in tasker interop.

Tests:

  • Expand controller option and shell-related unit tests to cover new options and parameter names, and add custom controller tests for RelativeMove/Shell.

新功能:

  • 通过新的 C# 包装类和互操作绑定,暴露调试、macOS、Android 原生、录制和回放控制器。
  • IMaaController 中添加 RelativeMove 控制器动作,以相对偏移量移动指针。
  • IMaaTasker 中引入 GetWaitFreezesDetail,通过原生 MaaTaskerGetWaitFreezesDetail API 获取等待冻结(wait-freeze)诊断信息。
  • 通过 IMaaToolkit.MacOS 和原生 MaaToolkitMacOS P/Invoke 绑定,暴露 macOS 工具包权限辅助方法。
  • 添加 macOS 特定的屏幕捕获方式、输入方式和权限相关枚举,并在托管绑定中增加对 MaaWfId 的支持。
  • 扩展 MaaMsg,增加 Node.WaitFreezes 消息常量,用于等待冻结生命周期事件。

增强:

  • 更新 MaaDbgController 以匹配简化后的原生 MaaDbgControllerCreate 函数签名,并精简其托管包装实现。
Original summary in English

Summary by Sourcery

使 C# MaaFramework 绑定与最新的 C API 对齐,补充缺失的 controller/toolkit 能力以及 wait-freeze 诊断功能。

New Features:

  • 通过新的 C# 包装类和 interop 绑定,暴露 macOS、Android 原生、record 和 replay controllers。
  • 添加 IMaaController.RelativeMove 以支持相对指针移动,并将其接入原生及自定义 controllers。
  • IMaaTasker 和通知扩展中暴露 MaaTaskerGetWaitFreezesDetail,以提供详细的 wait-freeze 诊断信息。
  • 通过 IMaaToolkit.MacOS 以及对应的 MaaToolkitMacOS P/Invoke 绑定,引入 macOS toolkit 权限辅助工具。
  • 添加 macOS 特定的 screencap、input 与权限枚举,以及 MaaWfId 别名和 Node.WaitFreezesMaaMsg 条目。

Enhancements:

  • 更新 MaaDbgController 及相关 interop,使其匹配简化后的 MaaDbgControllerCreate 签名,并改进 XML 文档/注释。
  • 扩展 controller 选项与 marshalling 辅助方法,以支持鼠标锁定跟随模式、截图缩放方法以及由后台管理的按键域。
  • 收紧通知与详情模型(ID 改为 Int64,确保焦点字段非空,引入新的枚举解析),并在 tasker interop 中启用可空注解。

Tests:

  • 扩展 controller 选项和 shell 相关的单元测试,以覆盖新的选项和参数名称,并为 RelativeMove/Shell 添加自定义 controller 测试。
Original summary in English

Summary by Sourcery

Align C# MaaFramework bindings with the latest C API by adding missing controller/toolkit capabilities and wait-freeze diagnostics.

New Features:

  • Expose macOS, Android native, record, and replay controllers through new C# wrapper classes and interop bindings.
  • Add IMaaController.RelativeMove to support relative pointer movement and wire it through native and custom controllers.
  • Expose MaaTaskerGetWaitFreezesDetail in IMaaTasker and notification extensions to provide detailed wait-freeze diagnostics.
  • Introduce macOS toolkit permission helpers via IMaaToolkit.MacOS and corresponding MaaToolkitMacOS P/Invoke bindings.
  • Add macOS-specific screencap, input, and permission enums plus MaaWfId aliases and Node.WaitFreezes MaaMsg entries.

Enhancements:

  • Update MaaDbgController and related interop to match the simplified MaaDbgControllerCreate signature and improve XML documentation/comments.
  • Extend controller options and marshalling helpers to support mouse-lock follow mode, screenshot resize method, and background-managed key domains.
  • Tighten notification and detail models (IDs as Int64, non-null focus fields, new enum parsing) and enable nullable annotations in tasker interop.

Tests:

  • Expand controller option and shell-related unit tests to cover new options and parameter names, and add custom controller tests for RelativeMove/Shell.
Original summary in English

Summary by Sourcery

使 C# MaaFramework 绑定与最新的 C API 对齐,补充缺失的 controller/toolkit 能力以及 wait-freeze 诊断功能。

New Features:

  • 通过新的 C# 包装类和 interop 绑定,暴露 macOS、Android 原生、record 和 replay controllers。
  • 添加 IMaaController.RelativeMove 以支持相对指针移动,并将其接入原生及自定义 controllers。
  • IMaaTasker 和通知扩展中暴露 MaaTaskerGetWaitFreezesDetail,以提供详细的 wait-freeze 诊断信息。
  • 通过 IMaaToolkit.MacOS 以及对应的 MaaToolkitMacOS P/Invoke 绑定,引入 macOS toolkit 权限辅助工具。
  • 添加 macOS 特定的 screencap、input 与权限枚举,以及 MaaWfId 别名和 Node.WaitFreezesMaaMsg 条目。

Enhancements:

  • 更新 MaaDbgController 及相关 interop,使其匹配简化后的 MaaDbgControllerCreate 签名,并改进 XML 文档/注释。
  • 扩展 controller 选项与 marshalling 辅助方法,以支持鼠标锁定跟随模式、截图缩放方法以及由后台管理的按键域。
  • 收紧通知与详情模型(ID 改为 Int64,确保焦点字段非空,引入新的枚举解析),并在 tasker interop 中启用可空注解。

Tests:

  • 扩展 controller 选项和 shell 相关的单元测试,以覆盖新的选项和参数名称,并为 RelativeMove/Shell 添加自定义 controller 测试。
Original summary in English

Summary by Sourcery

Align C# MaaFramework bindings with the latest C API by adding missing controller/toolkit capabilities and wait-freeze diagnostics.

New Features:

  • Expose macOS, Android native, record, and replay controllers through new C# wrapper classes and interop bindings.
  • Add IMaaController.RelativeMove to support relative pointer movement and wire it through native and custom controllers.
  • Expose MaaTaskerGetWaitFreezesDetail in IMaaTasker and notification extensions to provide detailed wait-freeze diagnostics.
  • Introduce macOS toolkit permission helpers via IMaaToolkit.MacOS and corresponding MaaToolkitMacOS P/Invoke bindings.
  • Add macOS-specific screencap, input, and permission enums plus MaaWfId aliases and Node.WaitFreezes MaaMsg entries.

Enhancements:

  • Update MaaDbgController and related interop to match the simplified MaaDbgControllerCreate signature and improve XML documentation/comments.
  • Extend controller options and marshalling helpers to support mouse-lock follow mode, screenshot resize method, and background-managed key domains.
  • Tighten notification and detail models (IDs as Int64, non-null focus fields, new enum parsing) and enable nullable annotations in tasker interop.

Tests:

  • Expand controller option and shell-related unit tests to cover new options and parameter names, and add custom controller tests for RelativeMove/Shell.
Original summary in English

Summary by Sourcery

使 C# MaaFramework 绑定与最新的 C API 对齐,补充缺失的 controller/toolkit 能力以及 wait-freeze 诊断功能。

New Features:

  • 通过新的 C# 包装类和 interop 绑定,暴露 macOS、Android 原生、record 和 replay controllers。
  • 添加 IMaaController.RelativeMove 以支持相对指针移动,并将其接入原生及自定义 controllers。
  • IMaaTasker 和通知扩展中暴露 MaaTaskerGetWaitFreezesDetail,以提供详细的 wait-freeze 诊断信息。
  • 通过 IMaaToolkit.MacOS 以及对应的 MaaToolkitMacOS P/Invoke 绑定,引入 macOS toolkit 权限辅助工具。
  • 添加 macOS 特定的 screencap、input 与权限枚举,以及 MaaWfId 别名和 Node.WaitFreezesMaaMsg 条目。

Enhancements:

  • 更新 MaaDbgController 及相关 interop,使其匹配简化后的 MaaDbgControllerCreate 签名,并改进 XML 文档/注释。
  • 扩展 controller 选项与 marshalling 辅助方法,以支持鼠标锁定跟随模式、截图缩放方法以及由后台管理的按键域。
  • 收紧通知与详情模型(ID 改为 Int64,确保焦点字段非空,引入新的枚举解析),并在 tasker interop 中启用可空注解。

Tests:

  • 扩展 controller 选项和 shell 相关的单元测试,以覆盖新的选项和参数名称,并为 RelativeMove/Shell 添加自定义 controller 测试。
Original summary in English

Summary by Sourcery

Align C# MaaFramework bindings with the latest C API by adding missing controller/toolkit capabilities and wait-freeze diagnostics.

New Features:

  • Expose macOS, Android native, record, and replay controllers through new C# wrapper classes and interop bindings.
  • Add IMaaController.RelativeMove to support relative pointer movement and wire it through native and custom controllers.
  • Expose MaaTaskerGetWaitFreezesDetail in IMaaTasker and notification extensions to provide detailed wait-freeze diagnostics.
  • Introduce macOS toolkit permission helpers via IMaaToolkit.MacOS and corresponding MaaToolkitMacOS P/Invoke bindings.
  • Add macOS-specific screencap, input, and permission enums plus MaaWfId aliases and Node.WaitFreezes MaaMsg entries.

Enhancements:

  • Update MaaDbgController and related interop to match the simplified MaaDbgControllerCreate signature and improve XML documentation/comments.
  • Extend controller options and marshalling helpers to support mouse-lock follow mode, screenshot resize method, and background-managed key domains.
  • Tighten notification and detail models (IDs as Int64, non-null focus fields, new enum parsing) and enable nullable annotations in tasker interop.

Tests:

  • Expand controller option and shell-related unit tests to cover new options and parameter names, and add custom controller tests for RelativeMove/Shell.
Original summary in English

Summary by Sourcery

使 C# MaaFramework 绑定与最新的 C API 对齐,补充缺失的 controller/toolkit 能力以及 wait-freeze 诊断功能。

New Features:

  • 通过新的 C# 包装类和 interop 绑定,暴露 macOS、Android 原生、record 和 replay controllers。
  • 添加 IMaaController.RelativeMove 以支持相对指针移动,并将其接入原生及自定义 controllers。
  • IMaaTasker 和通知扩展中暴露 MaaTaskerGetWaitFreezesDetail,以提供详细的 wait-freeze 诊断信息。
  • 通过 IMaaToolkit.MacOS 以及对应的 MaaToolkitMacOS P/Invoke 绑定,引入 macOS toolkit 权限辅助工具。
  • 添加 macOS 特定的 screencap、input 与权限枚举,以及 MaaWfId 别名和 Node.WaitFreezesMaaMsg 条目。

Enhancements:

  • 更新 MaaDbgController 及相关 interop,使其匹配简化后的 MaaDbgControllerCreate 签名,并改进 XML 文档/注释。
  • 扩展 controller 选项与 marshalling 辅助方法,以支持鼠标锁定跟随模式、截图缩放方法以及由后台管理的按键域。
  • 收紧通知与详情模型(ID 改为 Int64,确保焦点字段非空,引入新的枚举解析),并在 tasker interop 中启用可空注解。

Tests:

  • 扩展 controller 选项和 shell 相关的单元测试,以覆盖新的选项和参数名称,并为 RelativeMove/Shell 添加自定义 controller 测试。
Original summary in English

Summary by Sourcery

Align C# MaaFramework bindings with the latest C API by adding missing controller/toolkit capabilities and wait-freeze diagnostics.

New Features:

  • Expose macOS, Android native, record, and replay controllers through new C# wrapper classes and interop bindings.
  • Add IMaaController.RelativeMove to support relative pointer movement and wire it through native and custom controllers.
  • Expose MaaTaskerGetWaitFreezesDetail in IMaaTasker and notification extensions to provide detailed wait-freeze diagnostics.
  • Introduce macOS toolkit permission helpers via IMaaToolkit.MacOS and corresponding MaaToolkitMacOS P/Invoke bindings.
  • Add macOS-specific screencap, input, and permission enums plus MaaWfId aliases and Node.WaitFreezes MaaMsg entries.

Enhancements:

  • Update MaaDbgController and related interop to match the simplified MaaDbgControllerCreate signature and improve XML documentation/comments.
  • Extend controller options and marshalling helpers to support mouse-lock follow mode, screenshot resize method, and background-managed key domains.
  • Tighten notification and detail models (IDs as Int64, non-null focus fields, new enum parsing) and enable nullable annotations in tasker interop.

Tests:

  • Expand controller option and shell-related unit tests to cover new options and parameter names, and add custom controller tests for RelativeMove/Shell.

- Add MaaMacOSControllerCreate, MaaAndroidNativeControllerCreate,
  MaaReplayControllerCreate, MaaRecordControllerCreate P/Invoke
- Add MaaControllerPostRelativeMove P/Invoke
- Add MaaTaskerGetWaitFreezesDetail P/Invoke
- Add MaaToolkitMacOS P/Invoke (CheckPermission, RequestPermission,
  RevealPermissionSettings)
- Update MaaDbgControllerCreate to new 1-param signature
- Add MaaMacOSController, MaaAndroidNativeController,
  MaaReplayController, MaaRecordController wrapper classes
- Update MaaDbgController wrapper for new API
- Add RelativeMove to IMaaController/MaaController
- Add GetWaitFreezesDetail to IMaaTasker/MaaTasker
- Add MacOSScreencapMethod, MacOSInputMethod, MacOSPermission enums
- Add MaaWfId type alias
- Add IMaaToolkitMacOS interface and implementation
- Add Node.WaitFreezes messages to MaaMsg

Agent-Logs-Url: https://github.com/MaaXYZ/MaaFramework.Binding.CSharp/sessions/201d767e-cec0-4888-b09f-8b7a74837c9a

Co-authored-by: MistEO <18511905+MistEO@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the C# binding layer to close gaps with the upstream MaaFramework C API, adding new controller/toolkit bindings and extending the controller/tasker interfaces accordingly.

Changes:

  • Added new controller wrappers (macOS, Android native, record/replay) and updated MaaDbgControllerCreate to the new 1-parameter signature.
  • Exposed new controller action RelativeMove(dx, dy) and added tasker query GetWaitFreezesDetail(...) using the existing two-pass size query pattern.
  • Added macOS toolkit permission APIs (CheckPermission, RequestPermission, RevealPermissionSettings), enums/type aliases, and new MaaMsg entries for WaitFreezes notifications.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/MaaFramework.Binding/MaaMsg.cs Adds MaaMsg.Node.WaitFreezes.* message constants.
src/MaaFramework.Binding/MaaDef.cs Introduces MaaWfId type alias for wait-freezes IDs.
src/MaaFramework.Binding/IMaaToolkit.cs Extends toolkit API with MacOS property and new IMaaToolkitMacOS interface.
src/MaaFramework.Binding/IMaaTasker.cs Adds GetWaitFreezesDetail to the tasker interface.
src/MaaFramework.Binding/IMaaController.cs Adds RelativeMove(dx, dy) to the controller interface.
src/MaaFramework.Binding/Enums/Controllers/MacOSScreencapMethod.cs Adds macOS screencap method enum for the new controller.
src/MaaFramework.Binding/Enums/Controllers/MacOSPermission.cs Adds macOS permission enum for toolkit permission APIs.
src/MaaFramework.Binding/Enums/Controllers/MacOSInputMethod.cs Adds macOS input method flags enum for the new controller.
src/MaaFramework.Binding.Native/MaaToolkit.cs Implements IMaaToolkit.MacOS via MacOSClass calling new toolkit P/Invokes.
src/MaaFramework.Binding.Native/MaaTasker.cs Implements GetWaitFreezesDetail wrapper over the new P/Invoke.
src/MaaFramework.Binding.Native/MaaController/MaaReplayController.cs New replay controller wrapper over MaaReplayControllerCreate.
src/MaaFramework.Binding.Native/MaaController/MaaRecordController.cs New record controller wrapper over MaaRecordControllerCreate.
src/MaaFramework.Binding.Native/MaaController/MaaMacOSController.cs New macOS controller wrapper over MaaMacOSControllerCreate.
src/MaaFramework.Binding.Native/MaaController/MaaDbgController.cs Updates managed wrapper to match new MaaDbgControllerCreate(readPath) signature.
src/MaaFramework.Binding.Native/MaaController/MaaAndroidNativeController.cs New Android native controller wrapper over MaaAndroidNativeControllerCreate.
src/MaaFramework.Binding.Native/MaaController.cs Implements IMaaController.RelativeMove via MaaControllerPostRelativeMove.
src/MaaFramework.Binding.Native/Interop/Toolkit/MacOS/MaaToolkitMacOS.cs Adds MaaToolkit macOS permission P/Invokes.
src/MaaFramework.Binding.Native/Interop/Toolkit/MaaToolkitDef.cs Adds MaaMacOSPermission interop alias.
src/MaaFramework.Binding.Native/Interop/Framework/MaaDef.cs Adds macOS controller interop aliases and MaaWfId.
src/MaaFramework.Binding.Native/Interop/Framework/Instance/MaaTasker.cs Adds MaaTaskerGetWaitFreezesDetail P/Invoke signature.
src/MaaFramework.Binding.Native/Interop/Framework/Instance/MaaController.cs Adds new controller factory P/Invokes and MaaControllerPostRelativeMove; updates dbg factory signature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/MaaFramework.Binding/IMaaToolkit.cs
Comment thread src/MaaFramework.Binding/IMaaController.cs
Comment thread src/MaaFramework.Binding/IMaaTasker.cs Outdated
@moomiji moomiji marked this pull request as ready for review July 8, 2026 08:24

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 5 个问题,并给出了一些高层次的反馈:

  • 在 MaaOptionExtensions 中,SetOption_ScreenshotResizeMethod 目前接受的是一个 bool,但却将其转发给 ControllerOption.ScreenshotResizeMethod,而后者在文档和使用上都是 int(cv::InterpolationFlags);为避免运行时的 NotSupportedException 以及与原生 API 类型不匹配,扩展方法应该接受 int(或强类型枚举)。
  • NativeLibrary.GetLibraryPaths 的修改在调用 Path.GetDirectoryName 时去掉了空值宽恕运算符,但后面在 basePaths 中仍然无条件使用结果;建议对可能为 null 的 assemblyDirectory 做保护,以避免在程序集没有目录时 Path.Combine 抛出 ArgumentNullException。
给 AI 代理的提示
请根据本次代码审查中的评论进行修改:

## 总体评论
- 在 MaaOptionExtensions 中,SetOption_ScreenshotResizeMethod 目前接受的是一个 bool,但却将其转发给 ControllerOption.ScreenshotResizeMethod,而后者在文档和使用上都是 int(cv::InterpolationFlags);为避免运行时的 NotSupportedException 以及与原生 API 类型不匹配,扩展方法应该接受 int(或强类型枚举)。
- NativeLibrary.GetLibraryPaths 的修改在调用 Path.GetDirectoryName 时去掉了空值宽恕运算符,但后面在 basePaths 中仍然无条件使用结果;建议对可能为 null 的 assemblyDirectory 做保护,以避免在程序集没有目录时 Path.Combine 抛出 ArgumentNullException。

## 逐条评论

### Comment 1
<location path="src/MaaFramework.Binding/Extensions/MaaOptionExtensions.cs" line_range="35-36" />
<code_context>
+
+    /// <inheritdoc cref="ControllerOption.ScreenshotResizeMethod"/>
+    /// <inheritdoc cref="IMaaOption{T}.SetOption{T}"/>
+    public static bool SetOption_ScreenshotResizeMethod(this IMaaOption<ControllerOption>? opt, bool value)
+        => opt?.SetOption(ControllerOption.ScreenshotResizeMethod, value) ?? throw new ArgumentNullException(nameof(opt));
+
+    /// <inheritdoc cref="ControllerOption.BackgroundManagedKeys"/>
</code_context>
<issue_to_address>
**issue (bug_risk):** ScreenshotResizeMethod 选项扩展使用了错误的值类型

`ControllerOption.ScreenshotResizeMethod` 被定义为使用 `int`(与 `cv::InterpolationFlags` 对应),但当前这个扩展方法的参数类型是 `bool`。这要么会在运行时抛出异常,要么会把选项设置为错误的值。请将参数更新为 `int`,并将其直接传给 `SetOption`,以与文档中的类型保持一致。
</issue_to_address>

### Comment 2
<location path="src/MaaFramework.Binding.Native/Interop/NativeLibrary.cs" line_range="94" />
<code_context>
         // to AppContext.BaseDirectory which is the directory containing the single-file executable.
         var assemblyDirectory = string.IsNullOrEmpty(s_assembly.Location)
             ? AppContext.BaseDirectory
-            : Path.GetDirectoryName(s_assembly.Location)!;
+            : Path.GetDirectoryName(s_assembly.Location);

         string?[] basePaths =
</code_context>
<issue_to_address>
**issue (bug_risk):** 当 Path.GetDirectoryName 返回 null 时 assemblyDirectory 可能为 null

去掉空值宽恕运算符之后,如果 `Path.GetDirectoryName(s_assembly.Location)` 返回 null,`assemblyDirectory` 现在就可能为 null。由于之后在 `Path.Combine` 中会使用 `assemblyDirectory`,这可能会抛出 `ArgumentNullException`。请显式处理为 null 的情况(例如:回退到 `AppContext.BaseDirectory`,或在 `basePaths` 中跳过 null 条目),以避免潜在的运行时失败。
</issue_to_address>

### Comment 3
<location path="src/MaaFramework.Binding.Extensions/Notification/NotificationDetail.cs" line_range="120-121" />
<code_context>
+    [JsonIgnore] public TimeSpan Timeout => TimeSpan.FromMilliseconds(MillisecondsTimeout);
+}
+
+/// <inheritdoc cref="MaaMsg.Node.Action"/>
+public record NodeWaitFreezesDetail(
+    [property: JsonPropertyName("task_id")] long TaskId,
+    [property: JsonPropertyName("wf_id")] long WaitFreezesId,
</code_context>
<issue_to_address>
**suggestion:** NodeWaitFreezesDetail 从 Action 继承文档而不是从 WaitFreezes 继承

`NodeWaitFreezesDetail` 上的 cref 指向的是 `MaaMsg.Node.Action`,但该类型的字段实际上对应的是 `Node.WaitFreezes`。请将 cref 更新为 `MaaMsg.Node.WaitFreezes`,以便生成的文档能与实际的消息分组相匹配。

```suggestion
    /// <inheritdoc cref="MaaMsg.Node.WaitFreezes"/>
    public record NodeWaitFreezesDetail(
```
</issue_to_address>

### Comment 4
<location path="src/MaaFramework.Binding.UnitTests/Test_Custom.cs" line_range="305-308" />
<code_context>
         public bool Scroll(int dx, int dy)
             => c.Scroll(dx, dy).Wait().IsSucceeded();
+
+        public bool RelativeMove(int dx, int dy)
+            => c.RelativeMove(dx, dy).Wait().IsSucceeded();
+
+        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
+            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded() && c.GetShellOutput(out _);
+
</code_context>
<issue_to_address>
**suggestion (testing):** 为自定义控制器的 RelativeMove 和 Shell 回调(包括 shell 输出)增加直接覆盖的测试

适配器现在会把 RelativeMove 和 Shell 转发到 IMaaCustomController,但目前还没有通过 MaaCustomController 调用这些方法并断言行为的测试。请添加测试,用来:
- 通过自定义控制器调用 RelativeMove,并断言底层 IMaaController 的任务完成;
- 使用一个简单命令(例如 "echo hello")调用 Shell,断言任务成功,并验证 GetShellOutput(通过 buffer)返回预期的非空内容。
这样可以验证新委托的行为,并提前暴露任何封送或缓冲区处理方面的问题。

推荐实现示例:

```csharp
        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded() && c.GetShellOutput(out _);

        public bool Inactive()
                => c.Inactive().Wait().IsSucceeded();
        public bool GetInfo(IMaaStringBuffer buffer)

```

```csharp
}

[TestFixture]
public class CustomControllerRelativeMoveAndShellTests
{
    [Test]
    public void RelativeMove_CallsUnderlyingControllerAndCompletes()
    {
        // Arrange: 创建一个自定义控制器,与记录/模拟的 IMaaController 绑定
        // 注意:使用当前文件中已有的辅助方法或模式来构造 MaaCustomController。
        var recordingController = new RecordingMaaController();
        var customController = new MaaCustomController(recordingController);

        const int dx = 5;
        const int dy = -3;

        // Act
        var succeeded = customController.RelativeMove(dx, dy);

        // Assert
        Assert.IsTrue(succeeded, "当底层任务成功时,RelativeMove 应该返回成功。");
        Assert.IsTrue(recordingController.RelativeMoveCalled, "应调用底层 IMaaController.RelativeMove。");
        Assert.AreEqual(dx, recordingController.LastRelativeMoveDx);
        Assert.AreEqual(dy, recordingController.LastRelativeMoveDy);
        Assert.IsTrue(recordingController.LastRelativeMoveJobCompleted, "记录的 RelativeMove 任务应被标记为已完成。");
    }

    [Test]
    public void Shell_ExecutesCommandAndWritesOutputToBuffer()
    {
        // Arrange
        var recordingController = new RecordingMaaController();
        var customController = new MaaCustomController(recordingController);

        var buffer = new TestStringBuffer();
        const string command = "echo hello";
        const long timeoutMs = 5_000;

        // Act
        var succeeded = customController.Shell(command, timeoutMs, buffer);

        // Assert: 适配器返回成功,并通过 GetShellOutput 填充缓冲区
        Assert.IsTrue(succeeded, "当底层任务成功时,Shell 应该返回成功。");
        Assert.IsTrue(recordingController.ShellCalled, "应调用底层 IMaaController.Shell。");
        Assert.AreEqual(command, recordingController.LastShellCommand);
        Assert.AreEqual(timeoutMs, recordingController.LastShellTimeoutMs);

        var output = buffer.ToString();
        Assert.IsFalse(string.IsNullOrEmpty(output), "Shell 输出缓冲区应包含非空内容。");
    }

    /// <summary>
    /// 用于验证从 MaaCustomController 到 IMaaController 委托的最小记录实现。
    /// 请根据现有测试辅助类/约定实现 IMaaController 的其余成员。
    /// </summary>
    private sealed class RecordingMaaController : IMaaController
    {
        public bool RelativeMoveCalled { get; private set; }
        public int LastRelativeMoveDx { get; private set; }
        public int LastRelativeMoveDy { get; private set; }
        public bool LastRelativeMoveJobCompleted { get; private set; }

        public bool ShellCalled { get; private set; }
        public string LastShellCommand { get; private set; } = string.Empty;
        public long LastShellTimeoutMs { get; private set; }
        public string ShellOutput { get; private set; } = "stub-output";

        // RelativeMove/Shell 的精确返回类型取决于具体的 IMaaController;
        // 这里假设使用 c.Method(...).Wait().IsSucceeded() 这一模式。
        public IJob RelativeMove(int dx, int dy)
        {
            RelativeMoveCalled = true;
            LastRelativeMoveDx = dx;
            LastRelativeMoveDy = dy;

            var job = new RecordingJob();
            job.CompleteSuccessfully();
            LastRelativeMoveJobCompleted = true;
            return job;
        }

        public IJob Shell(string cmd, long millisecondsTimeout)
        {
            ShellCalled = true;
            LastShellCommand = cmd;
            LastShellTimeoutMs = millisecondsTimeout;

            // 对于测试,预先填充一段简单输出,供之后的 GetShellOutput 使用。
            ShellOutput = $"command: {cmd}";

            var job = new RecordingJob();
            job.CompleteSuccessfully();
            return job;
        }

        public bool GetShellOutput(out string output)
        {
            output = ShellOutput;
            return true;
        }

        // 实现或桩实现编译器要求的其他 IMaaController 成员。
        // 对于不会在这些测试中运行的成员,可以抛出 NotImplementedException,
        // 或委托给该文件中其他测试已使用的辅助实现。
    }

    /// <summary>
    /// 满足 c.Method(...).Wait().IsSucceeded() 模式所需的最小任务抽象。
    /// 请与测试集中 IMaaController 实际使用的任务/结果类型保持一致。
    /// </summary>
    private sealed class RecordingJob : IJob
    {
        private bool succeeded;

        public void CompleteSuccessfully()
        {
            succeeded = true;
        }

        public IJobResult Wait()
        {
            return new RecordingJobResult(succeeded);
        }
    }

    private sealed class RecordingJobResult : IJobResult
    {
        private readonly bool succeeded;

        public RecordingJobResult(bool succeeded)
        {
            this.succeeded = succeeded;
        }

        public bool IsSucceeded()
        {
            return succeeded;
        }
    }

    /// <summary>
    /// 用于在测试中捕获 shell 输出的简单缓冲区实现。
    /// 如果已经存在 IMaaStringBuffer 的实现,请改用已有实现。
    /// </summary>
    private sealed class TestStringBuffer : IMaaStringBuffer
    {
        private readonly StringBuilder sb = new();

        public void Append(string value)
        {
            sb.Append(value);
        }

        public override string ToString()
        {
            return sb.ToString();
        }
    }
}

```

1. 上述测试示例假定使用 NUnit(`[TestFixture]``[Test]``Assert.*`)以及占位抽象 `IMaaController``IJob``IJobResult``IMaaStringBuffer`。请根据你当前使用的单元测试框架(例如 xUnit 的 `[Fact]`、MSTest 的 `[TestMethod]`)调整特性和断言,并优先复用现有的辅助类,而不是重新定义 `RecordingMaaController``RecordingJob``RecordingJobResult``TestStringBuffer`,如果这些在 `Test_Custom.cs` 中已经存在的话。
2. 实现 `RecordingMaaController` 以完全满足你实际的 `IMaaController` 接口。对于不会在这些测试中被调用的成员,可以遵循本文件已有模式(例如委托给其他假对象或抛出 NotImplementedException),只要不会在这些测试里运行即可。
3.`c.RelativeMove(...).Wait().IsSucceeded()``c.Shell(...).Wait().IsSucceeded()` 使用的任务/结果接口与绑定中的真实类型对齐(将示例中的 `IJob`/`IJobResult` 替换为你的具体任务/结果类型或已有测试替身)。
4.`TestStringBuffer` 与真实的 `IMaaStringBuffer` 契约对齐:如果测试中已经有对应实现,请移除 `TestStringBuffer` 并改用现有类,同时确保 Shell 测试是通过与测试集中其他位置相同的 API 读取缓冲区内容。
</issue_to_address>

### Comment 5
<location path="src/MaaFramework.Binding.UnitTests/Test_Common.cs" line_range="98" />
<code_context>
     internal static NotificationHandler<NodeNextListDetail, IMaaContext> OnNodeNextList = (type, detail, context) => Assert.IsNotNull(context);
     internal static NotificationHandler<NodeRecognitionDetail, IMaaContext> OnNodeRecognition = (type, detail, context) => Assert.IsNotNull(context);
     internal static NotificationHandler<NodeActionDetail, IMaaContext> OnNodeAction = (type, detail, context) => Assert.IsNotNull(context);
+    internal static NotificationHandler<NodeWaitFreezesDetail, IMaaContext> OnNodeWaitFreezes = (type, detail, context) => Assert.IsNotNull(context);
 }
</code_context>
<issue_to_address>
**suggestion (testing):** 为 Node.WaitFreezes 消息和 NodeWaitFreezesDetail 反序列化添加端到端通知测试

目前这个测试只检查 context 非空。为了完整覆盖新类型/注册表/线路,请添加一个端到端测试:向 NotificationHandlerRegistry.Node.WaitFreezes(Starting/Succeeded/Failed)注入构造的 MaaMsg.Node.WaitFreezes.* JSON,并断言处理函数接收到的 NodeWaitFreezesDetail 具有期望的 TaskId/WfId/Phase/Roi/Param 值,同时 TimeSpan 属性(Param.Time、Param.Timeout、Elapsed)能正确反映来源中的毫秒值。

推荐实现示例:

```csharp
    internal static NotificationHandler<NodeNextListDetail, IMaaContext> OnNodeNextList = (type, detail, context) => Assert.IsNotNull(context);
    internal static NotificationHandler<NodeRecognitionDetail, IMaaContext> OnNodeRecognition = (type, detail, context) => Assert.IsNotNull(context);
    internal static NotificationHandler<NodeActionDetail, IMaaContext> OnNodeAction = (type, detail, context) => Assert.IsNotNull(context);

    // 用于 Node.WaitFreezes 端到端测试的捕获值
    internal static string? LastNodeWaitFreezesType;
    internal static NodeWaitFreezesDetail? LastNodeWaitFreezesDetail;
    internal static IMaaContext? LastNodeWaitFreezesContext;

    internal static NotificationHandler<NodeWaitFreezesDetail, IMaaContext> OnNodeWaitFreezes = (type, detail, context) =>
    {
        // 保留已有的安全断言
        Assert.IsNotNull(context);

        // 捕获反序列化后的通知,用于端到端测试
        LastNodeWaitFreezesType = type;
        LastNodeWaitFreezesDetail = detail;
        LastNodeWaitFreezesContext = context;
    }
}

/// <summary>
/// MaaMsg.Node.WaitFreezes 通知和 NodeWaitFreezesDetail 反序列化的端到端测试。
/// </summary>
[TestClass]
public sealed class NodeWaitFreezesNotificationTests
{
    private sealed class TestContext : IMaaContext
    {
        // 为测试实现所需的 IMaaContext 成员,或替换为你现有的测试上下文类型
    }

    private static string BuildWaitFreezesJson(string type)
    {
        // 构造 MaaMsg.Node.WaitFreezes.* 的 JSON 负载;根据实际 schema 调整属性名
        return $@"
{{
  ""Type"": ""{type}"",
  ""Detail"": {{
    ""TaskId"": 42,
    ""WfId"": ""wf-123"",
    ""Phase"": ""SomePhase"",
    ""Roi"": {{
      ""X"": 10,
      ""Y"": 20,
      ""W"": 30,
      ""H"": 40
    }},
    ""Param"": {{
      ""Time"": 1500,
      ""Timeout"": 3000
    }},
    ""Elapsed"": 750
  }}
}}";
    }

    private static void DispatchWaitFreezes(string type)
    {
        var json = BuildWaitFreezesJson(type);
        var context = new TestContext();

        // 清空之前捕获的值
        Test_Common.LastNodeWaitFreezesType = null;
        Test_Common.LastNodeWaitFreezesDetail = null;
        Test_Common.LastNodeWaitFreezesContext = null;

        // 注册处理函数并通过注册表分发 JSON;根据你的实际 API 做出调整
        NotificationHandlerRegistry.Node.WaitFreezes.Register(Test_Common.OnNodeWaitFreezes);

        NotificationHandlerRegistry.Node.WaitFreezes.Handle(json, context);
    }

    [TestMethod]
    public void NodeWaitFreezes_Starting_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Starting");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Starting", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(10, detail.Roi.X);
        Assert.AreEqual(20, detail.Roi.Y);
        Assert.AreEqual(30, detail.Roi.W);
        Assert.AreEqual(40, detail.Roi.H);

        // Param 中的毫秒值应映射到 TimeSpan
        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);

        // Elapsed 的毫秒值也应映射到 TimeSpan
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }

    [TestMethod]
    public void NodeWaitFreezes_Succeeded_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Succeeded");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Succeeded", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }

    [TestMethod]
    public void NodeWaitFreezes_Failed_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Failed");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Failed", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }
}

```

1. `BuildWaitFreezesJson` 中构造的 JSON 假定消息形状为 `{ "Type": "...", "Detail": { ... } }`,且属性名(`TaskId``WfId``Phase``Roi``Param``Elapsed`)与 `NodeWaitFreezesDetail` 一致。如果你的实际消息 schema 不同,请相应调整 JSON 的构造。
2. `NotificationHandlerRegistry.Node.WaitFreezes.Register(...)``.Handle(json, context)` 假定 NotificationHandlerRegistry 提供这样的 API。如果你实际的 API 使用了不同的方法名(例如 `Dispatch``Invoke``OnMessage`,或接受强类型消息而不是原始 JSON 的重载),请在 `DispatchWaitFreezes` 中做对应修改。
3. `TestContext` 是 IMaaContext 的最小占位实现。请替换为你现有的测试上下文类型(或实现必要成员),以与当前测试基础设施更好地集成。
4. 如果你的测试框架不是 MSTest(例如 xUnit 或 NUnit),请将 `[TestClass]` / `[TestMethod]` 以及 `Assert.*` 替换为对应框架的等价用法,或者把这些测试合并到你现有的测试类结构中。
</issue_to_address>

Sourcery 对开源项目免费使用——如果你觉得我们的审查有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的审查。
Original comment in English

Hey - I've found 5 issues, and left some high level feedback:

  • In MaaOptionExtensions, SetOption_ScreenshotResizeMethod currently takes a bool but forwards it to ControllerOption.ScreenshotResizeMethod, which is documented and consumed as an int (cv::InterpolationFlags); the extension should accept an int (or a strongly typed enum) to avoid runtime NotSupportedException and mismatch with the native API.
  • The change in NativeLibrary.GetLibraryPaths removes the null-forgiving operator when calling Path.GetDirectoryName, but still unconditionally uses the result in basePaths; consider guarding against a null assemblyDirectory to avoid a potential ArgumentNullException from Path.Combine when the assembly has no directory.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In MaaOptionExtensions, SetOption_ScreenshotResizeMethod currently takes a bool but forwards it to ControllerOption.ScreenshotResizeMethod, which is documented and consumed as an int (cv::InterpolationFlags); the extension should accept an int (or a strongly typed enum) to avoid runtime NotSupportedException and mismatch with the native API.
- The change in NativeLibrary.GetLibraryPaths removes the null-forgiving operator when calling Path.GetDirectoryName, but still unconditionally uses the result in basePaths; consider guarding against a null assemblyDirectory to avoid a potential ArgumentNullException from Path.Combine when the assembly has no directory.

## Individual Comments

### Comment 1
<location path="src/MaaFramework.Binding/Extensions/MaaOptionExtensions.cs" line_range="35-36" />
<code_context>
+
+    /// <inheritdoc cref="ControllerOption.ScreenshotResizeMethod"/>
+    /// <inheritdoc cref="IMaaOption{T}.SetOption{T}"/>
+    public static bool SetOption_ScreenshotResizeMethod(this IMaaOption<ControllerOption>? opt, bool value)
+        => opt?.SetOption(ControllerOption.ScreenshotResizeMethod, value) ?? throw new ArgumentNullException(nameof(opt));
+
+    /// <inheritdoc cref="ControllerOption.BackgroundManagedKeys"/>
</code_context>
<issue_to_address>
**issue (bug_risk):** ScreenshotResizeMethod option extension uses wrong value type

`ControllerOption.ScreenshotResizeMethod` is defined to use an `int` (matching `cv::InterpolationFlags`), but this extension method takes a `bool`. This will either throw at runtime or set the option incorrectly. Update the parameter to `int` and pass it directly to `SetOption` to align with the documented type.
</issue_to_address>

### Comment 2
<location path="src/MaaFramework.Binding.Native/Interop/NativeLibrary.cs" line_range="94" />
<code_context>
         // to AppContext.BaseDirectory which is the directory containing the single-file executable.
         var assemblyDirectory = string.IsNullOrEmpty(s_assembly.Location)
             ? AppContext.BaseDirectory
-            : Path.GetDirectoryName(s_assembly.Location)!;
+            : Path.GetDirectoryName(s_assembly.Location);

         string?[] basePaths =
</code_context>
<issue_to_address>
**issue (bug_risk):** Potential null assemblyDirectory when Path.GetDirectoryName returns null

Without the null-forgiving operator, `assemblyDirectory` can now be null if `Path.GetDirectoryName(s_assembly.Location)` returns null. Since `assemblyDirectory` is then used in `Path.Combine`, this can throw `ArgumentNullException`. Please either handle the null case explicitly (e.g., fallback to `AppContext.BaseDirectory` or skip null entries in `basePaths`) instead of introducing a possible runtime failure.
</issue_to_address>

### Comment 3
<location path="src/MaaFramework.Binding.Extensions/Notification/NotificationDetail.cs" line_range="120-121" />
<code_context>
+    [JsonIgnore] public TimeSpan Timeout => TimeSpan.FromMilliseconds(MillisecondsTimeout);
+}
+
+/// <inheritdoc cref="MaaMsg.Node.Action"/>
+public record NodeWaitFreezesDetail(
+    [property: JsonPropertyName("task_id")] long TaskId,
+    [property: JsonPropertyName("wf_id")] long WaitFreezesId,
</code_context>
<issue_to_address>
**suggestion:** NodeWaitFreezesDetail inherits docs from Action instead of WaitFreezes

The cref on `NodeWaitFreezesDetail` points to `MaaMsg.Node.Action`, but this type’s fields correspond to `Node.WaitFreezes`. Please update the cref to `MaaMsg.Node.WaitFreezes` so the generated documentation matches the actual message group.

```suggestion
    /// <inheritdoc cref="MaaMsg.Node.WaitFreezes"/>
    public record NodeWaitFreezesDetail(
```
</issue_to_address>

### Comment 4
<location path="src/MaaFramework.Binding.UnitTests/Test_Custom.cs" line_range="305-308" />
<code_context>
         public bool Scroll(int dx, int dy)
             => c.Scroll(dx, dy).Wait().IsSucceeded();
+
+        public bool RelativeMove(int dx, int dy)
+            => c.RelativeMove(dx, dy).Wait().IsSucceeded();
+
+        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
+            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded() && c.GetShellOutput(out _);
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage that directly exercises custom controller RelativeMove and Shell callbacks, including shell output.

The adapter now forwards RelativeMove and Shell to IMaaCustomController, but there’s no test that calls these through MaaCustomController and asserts behavior. Please add tests that:
- Call RelativeMove via the custom controller and assert the underlying IMaaController job completes.
- Call Shell with a simple command (e.g., "echo hello"), assert the job succeeds, and verify GetShellOutput (via the buffer) returns the expected non-empty content.
This will validate the new delegates and surface any marshalling/buffer-handling issues.

Suggested implementation:

```csharp
        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded() && c.GetShellOutput(out _);

        public bool Inactive()
                => c.Inactive().Wait().IsSucceeded();
        public bool GetInfo(IMaaStringBuffer buffer)

```

```csharp
}

[TestFixture]
public class CustomControllerRelativeMoveAndShellTests
{
    [Test]
    public void RelativeMove_CallsUnderlyingControllerAndCompletes()
    {
        // Arrange: create a custom controller wired to a recording/mocked IMaaController
        // NOTE: use the same helper or pattern already used in this file to build MaaCustomController.
        var recordingController = new RecordingMaaController();
        var customController = new MaaCustomController(recordingController);

        const int dx = 5;
        const int dy = -3;

        // Act
        var succeeded = customController.RelativeMove(dx, dy);

        // Assert
        Assert.IsTrue(succeeded, "RelativeMove should report success when the underlying job succeeds.");
        Assert.IsTrue(recordingController.RelativeMoveCalled, "Underlying IMaaController.RelativeMove should be invoked.");
        Assert.AreEqual(dx, recordingController.LastRelativeMoveDx);
        Assert.AreEqual(dy, recordingController.LastRelativeMoveDy);
        Assert.IsTrue(recordingController.LastRelativeMoveJobCompleted, "The recorded RelativeMove job should be marked as completed.");
    }

    [Test]
    public void Shell_ExecutesCommandAndWritesOutputToBuffer()
    {
        // Arrange
        var recordingController = new RecordingMaaController();
        var customController = new MaaCustomController(recordingController);

        var buffer = new TestStringBuffer();
        const string command = "echo hello";
        const long timeoutMs = 5_000;

        // Act
        var succeeded = customController.Shell(command, timeoutMs, buffer);

        // Assert: the adapter returns success and GetShellOutput is used to populate the buffer
        Assert.IsTrue(succeeded, "Shell should report success when the underlying job succeeds.");
        Assert.IsTrue(recordingController.ShellCalled, "Underlying IMaaController.Shell should be invoked.");
        Assert.AreEqual(command, recordingController.LastShellCommand);
        Assert.AreEqual(timeoutMs, recordingController.LastShellTimeoutMs);

        var output = buffer.ToString();
        Assert.IsFalse(string.IsNullOrEmpty(output), "Shell output buffer should contain non-empty content.");
    }

    /// <summary>
    /// Minimal recording implementation of IMaaController used to validate delegation from MaaCustomController.
    /// Implement the rest of IMaaController members according to the existing test helpers/conventions.
    /// </summary>
    private sealed class RecordingMaaController : IMaaController
    {
        public bool RelativeMoveCalled { get; private set; }
        public int LastRelativeMoveDx { get; private set; }
        public int LastRelativeMoveDy { get; private set; }
        public bool LastRelativeMoveJobCompleted { get; private set; }

        public bool ShellCalled { get; private set; }
        public string LastShellCommand { get; private set; } = string.Empty;
        public long LastShellTimeoutMs { get; private set; }
        public string ShellOutput { get; private set; } = "stub-output";

        // The exact return types for RelativeMove/Shell depend on the concrete IMaaController;
        // here we assume the pattern c.Method(...).Wait().IsSucceeded().
        public IJob RelativeMove(int dx, int dy)
        {
            RelativeMoveCalled = true;
            LastRelativeMoveDx = dx;
            LastRelativeMoveDy = dy;

            var job = new RecordingJob();
            job.CompleteSuccessfully();
            LastRelativeMoveJobCompleted = true;
            return job;
        }

        public IJob Shell(string cmd, long millisecondsTimeout)
        {
            ShellCalled = true;
            LastShellCommand = cmd;
            LastShellTimeoutMs = millisecondsTimeout;

            // For the test, pre-populate a simple output that GetShellOutput will later expose.
            ShellOutput = $"command: {cmd}";

            var job = new RecordingJob();
            job.CompleteSuccessfully();
            return job;
        }

        public bool GetShellOutput(out string output)
        {
            output = ShellOutput;
            return true;
        }

        // Implement or stub any additional IMaaController members required by the compiler.
        // For members not used in these tests, you can throw NotImplementedException or
        // delegate to existing helpers used in other tests in this file.
    }

    /// <summary>
    /// Minimal job abstraction to satisfy the c.Method(...).Wait().IsSucceeded() pattern.
    /// Align this with the actual job/result types used by IMaaController in your test suite.
    /// </summary>
    private sealed class RecordingJob : IJob
    {
        private bool succeeded;

        public void CompleteSuccessfully()
        {
            succeeded = true;
        }

        public IJobResult Wait()
        {
            return new RecordingJobResult(succeeded);
        }
    }

    private sealed class RecordingJobResult : IJobResult
    {
        private readonly bool succeeded;

        public RecordingJobResult(bool succeeded)
        {
            this.succeeded = succeeded;
        }

        public bool IsSucceeded()
        {
            return succeeded;
        }
    }

    /// <summary>
    /// Simple buffer implementation used to capture shell output in tests.
    /// Replace this with the existing IMaaStringBuffer implementation if one already exists.
    /// </summary>
    private sealed class TestStringBuffer : IMaaStringBuffer
    {
        private readonly StringBuilder sb = new();

        public void Append(string value)
        {
            sb.Append(value);
        }

        public override string ToString()
        {
            return sb.ToString();
        }
    }
}

```

1. The above tests assume NUnit (`[TestFixture]`, `[Test]`, `Assert.*`) and placeholder abstractions `IMaaController`, `IJob`, `IJobResult`, and `IMaaStringBuffer`. Adjust the attributes and assertions to match your existing unit test framework (e.g., xUnit `[Fact]`, MSTest `[TestMethod]`) and reuse existing helper classes instead of `RecordingMaaController`, `RecordingJob`, `RecordingJobResult`, and `TestStringBuffer` if those already exist in `Test_Custom.cs`.
2. Implement `RecordingMaaController` to fully satisfy your actual `IMaaController` interface. For any members not exercised by these tests, you can follow the pattern already used in the file (e.g., delegating to other fakes or throwing `NotImplementedException`) so long as they don't run during these tests.
3. Align the job/result interfaces used by `c.RelativeMove(...).Wait().IsSucceeded()` and `c.Shell(...).Wait().IsSucceeded()` with the real types in your binding (replace `IJob`/`IJobResult` and their implementations with your concrete job/result types or existing test doubles).
4. Wire `TestStringBuffer` to the real `IMaaStringBuffer` contract: if an implementation already exists in your tests, remove `TestStringBuffer` and use that instead, ensuring that `Shell` tests verify the buffer content by reading through the same API used elsewhere in the suite.
</issue_to_address>

### Comment 5
<location path="src/MaaFramework.Binding.UnitTests/Test_Common.cs" line_range="98" />
<code_context>
     internal static NotificationHandler<NodeNextListDetail, IMaaContext> OnNodeNextList = (type, detail, context) => Assert.IsNotNull(context);
     internal static NotificationHandler<NodeRecognitionDetail, IMaaContext> OnNodeRecognition = (type, detail, context) => Assert.IsNotNull(context);
     internal static NotificationHandler<NodeActionDetail, IMaaContext> OnNodeAction = (type, detail, context) => Assert.IsNotNull(context);
+    internal static NotificationHandler<NodeWaitFreezesDetail, IMaaContext> OnNodeWaitFreezes = (type, detail, context) => Assert.IsNotNull(context);
 }
</code_context>
<issue_to_address>
**suggestion (testing):** Add end-to-end notification tests for Node.WaitFreezes messages and NodeWaitFreezesDetail deserialization.

Right now this test only checks that the context is non-null. To fully cover the new type/registry/wiring, please add an end-to-end test that feeds a synthetic MaaMsg.Node.WaitFreezes.* JSON into NotificationHandlerRegistry.Node.WaitFreezes (for Starting/Succeeded/Failed) and asserts that the handler receives a NodeWaitFreezesDetail with the expected TaskId/WfId/Phase/Roi/Param values, and that the TimeSpan properties (Param.Time, Param.Timeout, Elapsed) correctly reflect the source millisecond values.

Suggested implementation:

```csharp
    internal static NotificationHandler<NodeNextListDetail, IMaaContext> OnNodeNextList = (type, detail, context) => Assert.IsNotNull(context);
    internal static NotificationHandler<NodeRecognitionDetail, IMaaContext> OnNodeRecognition = (type, detail, context) => Assert.IsNotNull(context);
    internal static NotificationHandler<NodeActionDetail, IMaaContext> OnNodeAction = (type, detail, context) => Assert.IsNotNull(context);

    // Captured values for Node.WaitFreezes end-to-end tests
    internal static string? LastNodeWaitFreezesType;
    internal static NodeWaitFreezesDetail? LastNodeWaitFreezesDetail;
    internal static IMaaContext? LastNodeWaitFreezesContext;

    internal static NotificationHandler<NodeWaitFreezesDetail, IMaaContext> OnNodeWaitFreezes = (type, detail, context) =>
    {
        // Keep existing safety assertion
        Assert.IsNotNull(context);

        // Capture the deserialized notification for end-to-end tests
        LastNodeWaitFreezesType = type;
        LastNodeWaitFreezesDetail = detail;
        LastNodeWaitFreezesContext = context;
    }
}

/// <summary>
/// End-to-end tests for MaaMsg.Node.WaitFreezes notifications and NodeWaitFreezesDetail deserialization.
/// </summary>
[TestClass]
public sealed class NodeWaitFreezesNotificationTests
{
    private sealed class TestContext : IMaaContext
    {
        // Implement required IMaaContext members for tests or adapt to your existing test context type
    }

    private static string BuildWaitFreezesJson(string type)
    {
        // Synthetic MaaMsg.Node.WaitFreezes.* JSON payload; adjust property names to match the actual schema
        return $@"
{{
  ""Type"": ""{type}"",
  ""Detail"": {{
    ""TaskId"": 42,
    ""WfId"": ""wf-123"",
    ""Phase"": ""SomePhase"",
    ""Roi"": {{
      ""X"": 10,
      ""Y"": 20,
      ""W"": 30,
      ""H"": 40
    }},
    ""Param"": {{
      ""Time"": 1500,
      ""Timeout"": 3000
    }},
    ""Elapsed"": 750
  }}
}}";
    }

    private static void DispatchWaitFreezes(string type)
    {
        var json = BuildWaitFreezesJson(type);
        var context = new TestContext();

        // Clear any previously captured values
        Test_Common.LastNodeWaitFreezesType = null;
        Test_Common.LastNodeWaitFreezesDetail = null;
        Test_Common.LastNodeWaitFreezesContext = null;

        // Register handler and dispatch JSON through the registry; adapt to your actual registry API
        NotificationHandlerRegistry.Node.WaitFreezes.Register(Test_Common.OnNodeWaitFreezes);

        NotificationHandlerRegistry.Node.WaitFreezes.Handle(json, context);
    }

    [TestMethod]
    public void NodeWaitFreezes_Starting_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Starting");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Starting", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(10, detail.Roi.X);
        Assert.AreEqual(20, detail.Roi.Y);
        Assert.AreEqual(30, detail.Roi.W);
        Assert.AreEqual(40, detail.Roi.H);

        // Param millisecond values should map to TimeSpan
        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);

        // Elapsed millisecond value should map to TimeSpan
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }

    [TestMethod]
    public void NodeWaitFreezes_Succeeded_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Succeeded");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Succeeded", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }

    [TestMethod]
    public void NodeWaitFreezes_Failed_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Failed");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Failed", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }
}

```

1. The synthetic JSON in `BuildWaitFreezesJson` assumes a `{ "Type": "...", "Detail": { ... } }` shape and property names (`TaskId`, `WfId`, `Phase`, `Roi`, `Param`, `Elapsed`) consistent with `NodeWaitFreezesDetail`. If your actual message schema differs, adjust the JSON construction accordingly.
2. The registry calls `NotificationHandlerRegistry.Node.WaitFreezes.Register(...)` and `.Handle(json, context)` assume a certain API on `NotificationHandlerRegistry`. If your actual API uses a different method name (e.g., `Dispatch`, `Invoke`, `OnMessage`, or an overload taking a strongly-typed message instead of raw JSON), update `DispatchWaitFreezes` to call the correct methods.
3. `TestContext` is a minimal placeholder implementation of `IMaaContext`. Replace it with your existing test context type (or implement the required members) to integrate cleanly with the rest of your test infrastructure.
4. If your test framework is not MSTest (e.g., xUnit or NUnit), replace `[TestClass]` / `[TestMethod]` and `Assert.*` calls with the equivalents from your framework or move these tests into the existing test class structure you already use.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/MaaFramework.Binding/Extensions/MaaOptionExtensions.cs Outdated
Comment thread src/MaaFramework.Binding.Native/Interop/NativeLibrary.cs
Comment thread src/MaaFramework.Binding.Extensions/Notification/NotificationDetail.cs Outdated
Comment on lines +305 to +308
public bool RelativeMove(int dx, int dy)
=> c.RelativeMove(dx, dy).Wait().IsSucceeded();

public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 为自定义控制器的 RelativeMove 和 Shell 回调(包括 shell 输出)增加直接覆盖的测试

适配器现在会把 RelativeMove 和 Shell 转发到 IMaaCustomController,但目前还没有通过 MaaCustomController 调用这些方法并断言行为的测试。请添加测试,用来:

  • 通过自定义控制器调用 RelativeMove,并断言底层 IMaaController 的任务完成;
  • 使用一个简单命令(例如 "echo hello")调用 Shell,断言任务成功,并验证 GetShellOutput(通过 buffer)返回预期的非空内容。
    这样可以验证新委托的行为,并提前暴露任何封送或缓冲区处理方面的问题。

推荐实现示例:

        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded() && c.GetShellOutput(out _);

        public bool Inactive()
                => c.Inactive().Wait().IsSucceeded();
        public bool GetInfo(IMaaStringBuffer buffer)
}

[TestFixture]
public class CustomControllerRelativeMoveAndShellTests
{
    [Test]
    public void RelativeMove_CallsUnderlyingControllerAndCompletes()
    {
        // Arrange: 创建一个自定义控制器,与记录/模拟的 IMaaController 绑定
        // 注意:使用当前文件中已有的辅助方法或模式来构造 MaaCustomController。
        var recordingController = new RecordingMaaController();
        var customController = new MaaCustomController(recordingController);

        const int dx = 5;
        const int dy = -3;

        // Act
        var succeeded = customController.RelativeMove(dx, dy);

        // Assert
        Assert.IsTrue(succeeded, "当底层任务成功时,RelativeMove 应该返回成功。");
        Assert.IsTrue(recordingController.RelativeMoveCalled, "应调用底层 IMaaController.RelativeMove。");
        Assert.AreEqual(dx, recordingController.LastRelativeMoveDx);
        Assert.AreEqual(dy, recordingController.LastRelativeMoveDy);
        Assert.IsTrue(recordingController.LastRelativeMoveJobCompleted, "记录的 RelativeMove 任务应被标记为已完成。");
    }

    [Test]
    public void Shell_ExecutesCommandAndWritesOutputToBuffer()
    {
        // Arrange
        var recordingController = new RecordingMaaController();
        var customController = new MaaCustomController(recordingController);

        var buffer = new TestStringBuffer();
        const string command = "echo hello";
        const long timeoutMs = 5_000;

        // Act
        var succeeded = customController.Shell(command, timeoutMs, buffer);

        // Assert: 适配器返回成功,并通过 GetShellOutput 填充缓冲区
        Assert.IsTrue(succeeded, "当底层任务成功时,Shell 应该返回成功。");
        Assert.IsTrue(recordingController.ShellCalled, "应调用底层 IMaaController.Shell。");
        Assert.AreEqual(command, recordingController.LastShellCommand);
        Assert.AreEqual(timeoutMs, recordingController.LastShellTimeoutMs);

        var output = buffer.ToString();
        Assert.IsFalse(string.IsNullOrEmpty(output), "Shell 输出缓冲区应包含非空内容。");
    }

    /// <summary>
    /// 用于验证从 MaaCustomController 到 IMaaController 委托的最小记录实现。
    /// 请根据现有测试辅助类/约定实现 IMaaController 的其余成员。
    /// </summary>
    private sealed class RecordingMaaController : IMaaController
    {
        public bool RelativeMoveCalled { get; private set; }
        public int LastRelativeMoveDx { get; private set; }
        public int LastRelativeMoveDy { get; private set; }
        public bool LastRelativeMoveJobCompleted { get; private set; }

        public bool ShellCalled { get; private set; }
        public string LastShellCommand { get; private set; } = string.Empty;
        public long LastShellTimeoutMs { get; private set; }
        public string ShellOutput { get; private set; } = "stub-output";

        // RelativeMove/Shell 的精确返回类型取决于具体的 IMaaController;
        // 这里假设使用 c.Method(...).Wait().IsSucceeded() 这一模式。
        public IJob RelativeMove(int dx, int dy)
        {
            RelativeMoveCalled = true;
            LastRelativeMoveDx = dx;
            LastRelativeMoveDy = dy;

            var job = new RecordingJob();
            job.CompleteSuccessfully();
            LastRelativeMoveJobCompleted = true;
            return job;
        }

        public IJob Shell(string cmd, long millisecondsTimeout)
        {
            ShellCalled = true;
            LastShellCommand = cmd;
            LastShellTimeoutMs = millisecondsTimeout;

            // 对于测试,预先填充一段简单输出,供之后的 GetShellOutput 使用。
            ShellOutput = $"command: {cmd}";

            var job = new RecordingJob();
            job.CompleteSuccessfully();
            return job;
        }

        public bool GetShellOutput(out string output)
        {
            output = ShellOutput;
            return true;
        }

        // 实现或桩实现编译器要求的其他 IMaaController 成员。
        // 对于不会在这些测试中运行的成员,可以抛出 NotImplementedException,
        // 或委托给该文件中其他测试已使用的辅助实现。
    }

    /// <summary>
    /// 满足 c.Method(...).Wait().IsSucceeded() 模式所需的最小任务抽象。
    /// 请与测试集中 IMaaController 实际使用的任务/结果类型保持一致。
    /// </summary>
    private sealed class RecordingJob : IJob
    {
        private bool succeeded;

        public void CompleteSuccessfully()
        {
            succeeded = true;
        }

        public IJobResult Wait()
        {
            return new RecordingJobResult(succeeded);
        }
    }

    private sealed class RecordingJobResult : IJobResult
    {
        private readonly bool succeeded;

        public RecordingJobResult(bool succeeded)
        {
            this.succeeded = succeeded;
        }

        public bool IsSucceeded()
        {
            return succeeded;
        }
    }

    /// <summary>
    /// 用于在测试中捕获 shell 输出的简单缓冲区实现。
    /// 如果已经存在 IMaaStringBuffer 的实现,请改用已有实现。
    /// </summary>
    private sealed class TestStringBuffer : IMaaStringBuffer
    {
        private readonly StringBuilder sb = new();

        public void Append(string value)
        {
            sb.Append(value);
        }

        public override string ToString()
        {
            return sb.ToString();
        }
    }
}
  1. 上述测试示例假定使用 NUnit([TestFixture][Test]Assert.*)以及占位抽象 IMaaControllerIJobIJobResultIMaaStringBuffer。请根据你当前使用的单元测试框架(例如 xUnit 的 [Fact]、MSTest 的 [TestMethod])调整特性和断言,并优先复用现有的辅助类,而不是重新定义 RecordingMaaControllerRecordingJobRecordingJobResultTestStringBuffer,如果这些在 Test_Custom.cs 中已经存在的话。
  2. 实现 RecordingMaaController 以完全满足你实际的 IMaaController 接口。对于不会在这些测试中被调用的成员,可以遵循本文件已有模式(例如委托给其他假对象或抛出 NotImplementedException),只要不会在这些测试里运行即可。
  3. c.RelativeMove(...).Wait().IsSucceeded()c.Shell(...).Wait().IsSucceeded() 使用的任务/结果接口与绑定中的真实类型对齐(将示例中的 IJob/IJobResult 替换为你的具体任务/结果类型或已有测试替身)。
  4. TestStringBuffer 与真实的 IMaaStringBuffer 契约对齐:如果测试中已经有对应实现,请移除 TestStringBuffer 并改用现有类,同时确保 Shell 测试是通过与测试集中其他位置相同的 API 读取缓冲区内容。
Original comment in English

suggestion (testing): Add coverage that directly exercises custom controller RelativeMove and Shell callbacks, including shell output.

The adapter now forwards RelativeMove and Shell to IMaaCustomController, but there’s no test that calls these through MaaCustomController and asserts behavior. Please add tests that:

  • Call RelativeMove via the custom controller and assert the underlying IMaaController job completes.
  • Call Shell with a simple command (e.g., "echo hello"), assert the job succeeds, and verify GetShellOutput (via the buffer) returns the expected non-empty content.
    This will validate the new delegates and surface any marshalling/buffer-handling issues.

Suggested implementation:

        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded() && c.GetShellOutput(out _);

        public bool Inactive()
                => c.Inactive().Wait().IsSucceeded();
        public bool GetInfo(IMaaStringBuffer buffer)
}

[TestFixture]
public class CustomControllerRelativeMoveAndShellTests
{
    [Test]
    public void RelativeMove_CallsUnderlyingControllerAndCompletes()
    {
        // Arrange: create a custom controller wired to a recording/mocked IMaaController
        // NOTE: use the same helper or pattern already used in this file to build MaaCustomController.
        var recordingController = new RecordingMaaController();
        var customController = new MaaCustomController(recordingController);

        const int dx = 5;
        const int dy = -3;

        // Act
        var succeeded = customController.RelativeMove(dx, dy);

        // Assert
        Assert.IsTrue(succeeded, "RelativeMove should report success when the underlying job succeeds.");
        Assert.IsTrue(recordingController.RelativeMoveCalled, "Underlying IMaaController.RelativeMove should be invoked.");
        Assert.AreEqual(dx, recordingController.LastRelativeMoveDx);
        Assert.AreEqual(dy, recordingController.LastRelativeMoveDy);
        Assert.IsTrue(recordingController.LastRelativeMoveJobCompleted, "The recorded RelativeMove job should be marked as completed.");
    }

    [Test]
    public void Shell_ExecutesCommandAndWritesOutputToBuffer()
    {
        // Arrange
        var recordingController = new RecordingMaaController();
        var customController = new MaaCustomController(recordingController);

        var buffer = new TestStringBuffer();
        const string command = "echo hello";
        const long timeoutMs = 5_000;

        // Act
        var succeeded = customController.Shell(command, timeoutMs, buffer);

        // Assert: the adapter returns success and GetShellOutput is used to populate the buffer
        Assert.IsTrue(succeeded, "Shell should report success when the underlying job succeeds.");
        Assert.IsTrue(recordingController.ShellCalled, "Underlying IMaaController.Shell should be invoked.");
        Assert.AreEqual(command, recordingController.LastShellCommand);
        Assert.AreEqual(timeoutMs, recordingController.LastShellTimeoutMs);

        var output = buffer.ToString();
        Assert.IsFalse(string.IsNullOrEmpty(output), "Shell output buffer should contain non-empty content.");
    }

    /// <summary>
    /// Minimal recording implementation of IMaaController used to validate delegation from MaaCustomController.
    /// Implement the rest of IMaaController members according to the existing test helpers/conventions.
    /// </summary>
    private sealed class RecordingMaaController : IMaaController
    {
        public bool RelativeMoveCalled { get; private set; }
        public int LastRelativeMoveDx { get; private set; }
        public int LastRelativeMoveDy { get; private set; }
        public bool LastRelativeMoveJobCompleted { get; private set; }

        public bool ShellCalled { get; private set; }
        public string LastShellCommand { get; private set; } = string.Empty;
        public long LastShellTimeoutMs { get; private set; }
        public string ShellOutput { get; private set; } = "stub-output";

        // The exact return types for RelativeMove/Shell depend on the concrete IMaaController;
        // here we assume the pattern c.Method(...).Wait().IsSucceeded().
        public IJob RelativeMove(int dx, int dy)
        {
            RelativeMoveCalled = true;
            LastRelativeMoveDx = dx;
            LastRelativeMoveDy = dy;

            var job = new RecordingJob();
            job.CompleteSuccessfully();
            LastRelativeMoveJobCompleted = true;
            return job;
        }

        public IJob Shell(string cmd, long millisecondsTimeout)
        {
            ShellCalled = true;
            LastShellCommand = cmd;
            LastShellTimeoutMs = millisecondsTimeout;

            // For the test, pre-populate a simple output that GetShellOutput will later expose.
            ShellOutput = $"command: {cmd}";

            var job = new RecordingJob();
            job.CompleteSuccessfully();
            return job;
        }

        public bool GetShellOutput(out string output)
        {
            output = ShellOutput;
            return true;
        }

        // Implement or stub any additional IMaaController members required by the compiler.
        // For members not used in these tests, you can throw NotImplementedException or
        // delegate to existing helpers used in other tests in this file.
    }

    /// <summary>
    /// Minimal job abstraction to satisfy the c.Method(...).Wait().IsSucceeded() pattern.
    /// Align this with the actual job/result types used by IMaaController in your test suite.
    /// </summary>
    private sealed class RecordingJob : IJob
    {
        private bool succeeded;

        public void CompleteSuccessfully()
        {
            succeeded = true;
        }

        public IJobResult Wait()
        {
            return new RecordingJobResult(succeeded);
        }
    }

    private sealed class RecordingJobResult : IJobResult
    {
        private readonly bool succeeded;

        public RecordingJobResult(bool succeeded)
        {
            this.succeeded = succeeded;
        }

        public bool IsSucceeded()
        {
            return succeeded;
        }
    }

    /// <summary>
    /// Simple buffer implementation used to capture shell output in tests.
    /// Replace this with the existing IMaaStringBuffer implementation if one already exists.
    /// </summary>
    private sealed class TestStringBuffer : IMaaStringBuffer
    {
        private readonly StringBuilder sb = new();

        public void Append(string value)
        {
            sb.Append(value);
        }

        public override string ToString()
        {
            return sb.ToString();
        }
    }
}
  1. The above tests assume NUnit ([TestFixture], [Test], Assert.*) and placeholder abstractions IMaaController, IJob, IJobResult, and IMaaStringBuffer. Adjust the attributes and assertions to match your existing unit test framework (e.g., xUnit [Fact], MSTest [TestMethod]) and reuse existing helper classes instead of RecordingMaaController, RecordingJob, RecordingJobResult, and TestStringBuffer if those already exist in Test_Custom.cs.
  2. Implement RecordingMaaController to fully satisfy your actual IMaaController interface. For any members not exercised by these tests, you can follow the pattern already used in the file (e.g., delegating to other fakes or throwing NotImplementedException) so long as they don't run during these tests.
  3. Align the job/result interfaces used by c.RelativeMove(...).Wait().IsSucceeded() and c.Shell(...).Wait().IsSucceeded() with the real types in your binding (replace IJob/IJobResult and their implementations with your concrete job/result types or existing test doubles).
  4. Wire TestStringBuffer to the real IMaaStringBuffer contract: if an implementation already exists in your tests, remove TestStringBuffer and use that instead, ensuring that Shell tests verify the buffer content by reading through the same API used elsewhere in the suite.

internal static NotificationHandler<NodeNextListDetail, IMaaContext> OnNodeNextList = (type, detail, context) => Assert.IsNotNull(context);
internal static NotificationHandler<NodeRecognitionDetail, IMaaContext> OnNodeRecognition = (type, detail, context) => Assert.IsNotNull(context);
internal static NotificationHandler<NodeActionDetail, IMaaContext> OnNodeAction = (type, detail, context) => Assert.IsNotNull(context);
internal static NotificationHandler<NodeWaitFreezesDetail, IMaaContext> OnNodeWaitFreezes = (type, detail, context) => Assert.IsNotNull(context);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 为 Node.WaitFreezes 消息和 NodeWaitFreezesDetail 反序列化添加端到端通知测试

目前这个测试只检查 context 非空。为了完整覆盖新类型/注册表/线路,请添加一个端到端测试:向 NotificationHandlerRegistry.Node.WaitFreezes(Starting/Succeeded/Failed)注入构造的 MaaMsg.Node.WaitFreezes.* JSON,并断言处理函数接收到的 NodeWaitFreezesDetail 具有期望的 TaskId/WfId/Phase/Roi/Param 值,同时 TimeSpan 属性(Param.Time、Param.Timeout、Elapsed)能正确反映来源中的毫秒值。

推荐实现示例:

    internal static NotificationHandler<NodeNextListDetail, IMaaContext> OnNodeNextList = (type, detail, context) => Assert.IsNotNull(context);
    internal static NotificationHandler<NodeRecognitionDetail, IMaaContext> OnNodeRecognition = (type, detail, context) => Assert.IsNotNull(context);
    internal static NotificationHandler<NodeActionDetail, IMaaContext> OnNodeAction = (type, detail, context) => Assert.IsNotNull(context);

    // 用于 Node.WaitFreezes 端到端测试的捕获值
    internal static string? LastNodeWaitFreezesType;
    internal static NodeWaitFreezesDetail? LastNodeWaitFreezesDetail;
    internal static IMaaContext? LastNodeWaitFreezesContext;

    internal static NotificationHandler<NodeWaitFreezesDetail, IMaaContext> OnNodeWaitFreezes = (type, detail, context) =>
    {
        // 保留已有的安全断言
        Assert.IsNotNull(context);

        // 捕获反序列化后的通知,用于端到端测试
        LastNodeWaitFreezesType = type;
        LastNodeWaitFreezesDetail = detail;
        LastNodeWaitFreezesContext = context;
    }
}

/// <summary>
/// MaaMsg.Node.WaitFreezes 通知和 NodeWaitFreezesDetail 反序列化的端到端测试。
/// </summary>
[TestClass]
public sealed class NodeWaitFreezesNotificationTests
{
    private sealed class TestContext : IMaaContext
    {
        // 为测试实现所需的 IMaaContext 成员,或替换为你现有的测试上下文类型
    }

    private static string BuildWaitFreezesJson(string type)
    {
        // 构造 MaaMsg.Node.WaitFreezes.* 的 JSON 负载;根据实际 schema 调整属性名
        return $@"
{{
  ""Type"": ""{type}"",
  ""Detail"": {{
    ""TaskId"": 42,
    ""WfId"": ""wf-123"",
    ""Phase"": ""SomePhase"",
    ""Roi"": {{
      ""X"": 10,
      ""Y"": 20,
      ""W"": 30,
      ""H"": 40
    }},
    ""Param"": {{
      ""Time"": 1500,
      ""Timeout"": 3000
    }},
    ""Elapsed"": 750
  }}
}}";
    }

    private static void DispatchWaitFreezes(string type)
    {
        var json = BuildWaitFreezesJson(type);
        var context = new TestContext();

        // 清空之前捕获的值
        Test_Common.LastNodeWaitFreezesType = null;
        Test_Common.LastNodeWaitFreezesDetail = null;
        Test_Common.LastNodeWaitFreezesContext = null;

        // 注册处理函数并通过注册表分发 JSON;根据你的实际 API 做出调整
        NotificationHandlerRegistry.Node.WaitFreezes.Register(Test_Common.OnNodeWaitFreezes);

        NotificationHandlerRegistry.Node.WaitFreezes.Handle(json, context);
    }

    [TestMethod]
    public void NodeWaitFreezes_Starting_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Starting");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Starting", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(10, detail.Roi.X);
        Assert.AreEqual(20, detail.Roi.Y);
        Assert.AreEqual(30, detail.Roi.W);
        Assert.AreEqual(40, detail.Roi.H);

        // Param 中的毫秒值应映射到 TimeSpan
        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);

        // Elapsed 的毫秒值也应映射到 TimeSpan
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }

    [TestMethod]
    public void NodeWaitFreezes_Succeeded_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Succeeded");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Succeeded", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }

    [TestMethod]
    public void NodeWaitFreezes_Failed_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Failed");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Failed", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }
}
  1. BuildWaitFreezesJson 中构造的 JSON 假定消息形状为 { "Type": "...", "Detail": { ... } },且属性名(TaskIdWfIdPhaseRoiParamElapsed)与 NodeWaitFreezesDetail 一致。如果你的实际消息 schema 不同,请相应调整 JSON 的构造。
  2. NotificationHandlerRegistry.Node.WaitFreezes.Register(...).Handle(json, context) 假定 NotificationHandlerRegistry 提供这样的 API。如果你实际的 API 使用了不同的方法名(例如 DispatchInvokeOnMessage,或接受强类型消息而不是原始 JSON 的重载),请在 DispatchWaitFreezes 中做对应修改。
  3. TestContext 是 IMaaContext 的最小占位实现。请替换为你现有的测试上下文类型(或实现必要成员),以与当前测试基础设施更好地集成。
  4. 如果你的测试框架不是 MSTest(例如 xUnit 或 NUnit),请将 [TestClass] / [TestMethod] 以及 Assert.* 替换为对应框架的等价用法,或者把这些测试合并到你现有的测试类结构中。
Original comment in English

suggestion (testing): Add end-to-end notification tests for Node.WaitFreezes messages and NodeWaitFreezesDetail deserialization.

Right now this test only checks that the context is non-null. To fully cover the new type/registry/wiring, please add an end-to-end test that feeds a synthetic MaaMsg.Node.WaitFreezes.* JSON into NotificationHandlerRegistry.Node.WaitFreezes (for Starting/Succeeded/Failed) and asserts that the handler receives a NodeWaitFreezesDetail with the expected TaskId/WfId/Phase/Roi/Param values, and that the TimeSpan properties (Param.Time, Param.Timeout, Elapsed) correctly reflect the source millisecond values.

Suggested implementation:

    internal static NotificationHandler<NodeNextListDetail, IMaaContext> OnNodeNextList = (type, detail, context) => Assert.IsNotNull(context);
    internal static NotificationHandler<NodeRecognitionDetail, IMaaContext> OnNodeRecognition = (type, detail, context) => Assert.IsNotNull(context);
    internal static NotificationHandler<NodeActionDetail, IMaaContext> OnNodeAction = (type, detail, context) => Assert.IsNotNull(context);

    // Captured values for Node.WaitFreezes end-to-end tests
    internal static string? LastNodeWaitFreezesType;
    internal static NodeWaitFreezesDetail? LastNodeWaitFreezesDetail;
    internal static IMaaContext? LastNodeWaitFreezesContext;

    internal static NotificationHandler<NodeWaitFreezesDetail, IMaaContext> OnNodeWaitFreezes = (type, detail, context) =>
    {
        // Keep existing safety assertion
        Assert.IsNotNull(context);

        // Capture the deserialized notification for end-to-end tests
        LastNodeWaitFreezesType = type;
        LastNodeWaitFreezesDetail = detail;
        LastNodeWaitFreezesContext = context;
    }
}

/// <summary>
/// End-to-end tests for MaaMsg.Node.WaitFreezes notifications and NodeWaitFreezesDetail deserialization.
/// </summary>
[TestClass]
public sealed class NodeWaitFreezesNotificationTests
{
    private sealed class TestContext : IMaaContext
    {
        // Implement required IMaaContext members for tests or adapt to your existing test context type
    }

    private static string BuildWaitFreezesJson(string type)
    {
        // Synthetic MaaMsg.Node.WaitFreezes.* JSON payload; adjust property names to match the actual schema
        return $@"
{{
  ""Type"": ""{type}"",
  ""Detail"": {{
    ""TaskId"": 42,
    ""WfId"": ""wf-123"",
    ""Phase"": ""SomePhase"",
    ""Roi"": {{
      ""X"": 10,
      ""Y"": 20,
      ""W"": 30,
      ""H"": 40
    }},
    ""Param"": {{
      ""Time"": 1500,
      ""Timeout"": 3000
    }},
    ""Elapsed"": 750
  }}
}}";
    }

    private static void DispatchWaitFreezes(string type)
    {
        var json = BuildWaitFreezesJson(type);
        var context = new TestContext();

        // Clear any previously captured values
        Test_Common.LastNodeWaitFreezesType = null;
        Test_Common.LastNodeWaitFreezesDetail = null;
        Test_Common.LastNodeWaitFreezesContext = null;

        // Register handler and dispatch JSON through the registry; adapt to your actual registry API
        NotificationHandlerRegistry.Node.WaitFreezes.Register(Test_Common.OnNodeWaitFreezes);

        NotificationHandlerRegistry.Node.WaitFreezes.Handle(json, context);
    }

    [TestMethod]
    public void NodeWaitFreezes_Starting_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Starting");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Starting", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(10, detail.Roi.X);
        Assert.AreEqual(20, detail.Roi.Y);
        Assert.AreEqual(30, detail.Roi.W);
        Assert.AreEqual(40, detail.Roi.H);

        // Param millisecond values should map to TimeSpan
        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);

        // Elapsed millisecond value should map to TimeSpan
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }

    [TestMethod]
    public void NodeWaitFreezes_Succeeded_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Succeeded");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Succeeded", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }

    [TestMethod]
    public void NodeWaitFreezes_Failed_DeserializesDetailAndTimeSpans()
    {
        DispatchWaitFreezes("MaaMsg.Node.WaitFreezes.Failed");

        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesContext);
        Assert.AreEqual("MaaMsg.Node.WaitFreezes.Failed", Test_Common.LastNodeWaitFreezesType);
        Assert.IsNotNull(Test_Common.LastNodeWaitFreezesDetail);

        var detail = Test_Common.LastNodeWaitFreezesDetail!;

        Assert.AreEqual(42, detail.TaskId);
        Assert.AreEqual("wf-123", detail.WfId);
        Assert.AreEqual("SomePhase", detail.Phase);

        Assert.AreEqual(TimeSpan.FromMilliseconds(1500), detail.Param.Time);
        Assert.AreEqual(TimeSpan.FromMilliseconds(3000), detail.Param.Timeout);
        Assert.AreEqual(TimeSpan.FromMilliseconds(750), detail.Elapsed);
    }
}
  1. The synthetic JSON in BuildWaitFreezesJson assumes a { "Type": "...", "Detail": { ... } } shape and property names (TaskId, WfId, Phase, Roi, Param, Elapsed) consistent with NodeWaitFreezesDetail. If your actual message schema differs, adjust the JSON construction accordingly.
  2. The registry calls NotificationHandlerRegistry.Node.WaitFreezes.Register(...) and .Handle(json, context) assume a certain API on NotificationHandlerRegistry. If your actual API uses a different method name (e.g., Dispatch, Invoke, OnMessage, or an overload taking a strongly-typed message instead of raw JSON), update DispatchWaitFreezes to call the correct methods.
  3. TestContext is a minimal placeholder implementation of IMaaContext. Replace it with your existing test context type (or implement the required members) to integrate cleanly with the rest of your test infrastructure.
  4. If your test framework is not MSTest (e.g., xUnit or NUnit), replace [TestClass] / [TestMethod] and Assert.* calls with the equivalents from your framework or move these tests into the existing test class structure you already use.

@moomiji moomiji force-pushed the copilot/fix-cs-binding-issues branch from 5fb8129 to 3705114 Compare July 8, 2026 08:43
@moomiji moomiji requested a review from Copilot July 8, 2026 08:43

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 4 个问题,并给出了一些整体性的反馈:

  • MaaOptionExtensions.SetOption_ScreenshotResizeMethod 中,参数类型是 bool,但 ControllerOption.ScreenshotResizeMethod 期望的是 int(并且在 SetOption<T> 中按 int 进行封送),这会导致运行时失败;这个扩展方法应该接受 int(例如插值枚举值)而不是 bool
  • NotificationDetailContext 中,[JsonSerializable(typeof(NodeAttr))] 条目被移除了,但 NodeNextListDetail 仍然使用 IReadOnlyList<NodeAttr>;在使用源生成的 System.Text.Json 时,这个类型可能不再被包含在生成的上下文中,所以请再次确认是否仍需要显式注册 NodeAttr 以避免反序列化问题。
  • NodeWaitFreezesDetail 的 XML 文档目前使用的是 <inheritdoc cref="MaaMsg.Node.Action"/> 而不是 MaaMsg.Node.WaitFreezes,这可能会让使用者感到困惑,应当更新为引用正确的消息组。
给 AI Agent 的提示
请解决本次代码评审中的所有评论:

## 整体评论
-`MaaOptionExtensions.SetOption_ScreenshotResizeMethod` 中,参数类型是 `bool`,但 `ControllerOption.ScreenshotResizeMethod` 期望的是 `int`(并且在 `SetOption<T>` 中按 `int` 进行封送),这会导致运行时失败;这个扩展方法应该接受 `int`(例如插值枚举值)而不是 `bool`-`NotificationDetailContext` 中,`[JsonSerializable(typeof(NodeAttr))]` 条目被移除了,但 `NodeNextListDetail` 仍然使用 `IReadOnlyList<NodeAttr>`;在使用源生成的 `System.Text.Json` 时,这个类型可能不再被包含在生成的上下文中,所以请再次确认是否仍需要显式注册 `NodeAttr` 以避免反序列化问题。
- `NodeWaitFreezesDetail` 的 XML 文档目前使用的是 `<inheritdoc cref="MaaMsg.Node.Action"/>` 而不是 `MaaMsg.Node.WaitFreezes`,这可能会让使用者感到困惑,应当更新为引用正确的消息组。

## 单条评论

### 评论 1
<location path="src/MaaFramework.Binding/Extensions/MaaOptionExtensions.cs" line_range="35-36" />
<code_context>
+
+    /// <inheritdoc cref="ControllerOption.ScreenshotResizeMethod"/>
+    /// <inheritdoc cref="IMaaOption{T}.SetOption{T}"/>
+    public static bool SetOption_ScreenshotResizeMethod(this IMaaOption<ControllerOption>? opt, bool value)
+        => opt?.SetOption(ControllerOption.ScreenshotResizeMethod, value) ?? throw new ArgumentNullException(nameof(opt));
+
+    /// <inheritdoc cref="ControllerOption.BackgroundManagedKeys"/>
</code_context>
<issue_to_address>
**issue (bug_risk):** ScreenshotResizeMethod 选项扩展使用了 bool 而不是 int

`ControllerOption.ScreenshotResizeMethod` 被定义为 `int`(与 `cv::InterpolationFlags` 对应),但这个扩展方法接受的是 `bool`,并将其传给 `SetOption`,这会走进 `MaaController.SetOption``int` 分支并在运行时抛出 `NotSupportedException`。

这里应该接受 `int`(或一个枚举),再传给 `SetOption(ControllerOption.ScreenshotResizeMethod, value)`;否则该 API 会产生误导且无法正常使用。
</issue_to_address>

### 评论 2
<location path="src/MaaFramework.Binding.Native/Interop/NativeLibrary.cs" line_range="94" />
<code_context>
         // to AppContext.BaseDirectory which is the directory containing the single-file executable.
         var assemblyDirectory = string.IsNullOrEmpty(s_assembly.Location)
             ? AppContext.BaseDirectory
-            : Path.GetDirectoryName(s_assembly.Location)!;
+            : Path.GetDirectoryName(s_assembly.Location);

         string?[] basePaths =
</code_context>
<issue_to_address>
**issue (bug_risk):** Path.GetDirectoryName 可能返回 null 导致 assemblyDirectory 为空

去掉 `!` 之后,当 `s_assembly.Location` 没有目录部分时,`assemblyDirectory` 可能为 null,将其传递给 `Path.Combine` 会抛异常。请在 `Path.GetDirectoryName` 返回 null 时回退到 `AppContext.BaseDirectory`,或者以其他方式处理该 null 情况,以保证解析过程健壮。
</issue_to_address>

### 评论 3
<location path="src/MaaFramework.Binding.UnitTests/Test_IMaaController.cs" line_range="147-149" />
<code_context>
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetLongSide, 1280)]
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetShortSide, 720)]
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotUseRawSize, false)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, false)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotResizeMethod, 0)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[] { 0x57, 0x41, 0x53, 0x44 })]
     public void Interface_SetOption(MaaTypes type, IMaaController maaController, ControllerOption opt, object arg)
     {
</code_context>
<issue_to_address>
**suggestion (testing):** 为 IMaaController 上新的 RelativeMove 控制器动作添加测试覆盖

新的 RelativeMove API 已在 IMaaController 上暴露,并通过 MaaControllerPostRelativeMove 进行路由,但目前没有覆盖它的单元测试。请添加一个类似现有 touch/move/key 测试的用例(例如 `Interface_RelativeMove`),该用例应当:
- 使用不同的 dx/dy(包括负数和 0)调用 `maaController.RelativeMove(dx, dy)`- 对于支持该功能的控制器(例如 Win32/ADB),断言 MaaJob 能成功完成;
- 验证对不支持 RelativeMove 的控制器来说预期的失败行为。
这样可以确保新的 P/Invoke 和托管层接线是正确的,并防止在不同控制器类型间出现回归。

推荐实现:

```csharp
    [TestMethod]
    [MaaData(MaaTypes.Win32, nameof(Data), 0, 0, true)]
    [MaaData(MaaTypes.Win32, nameof(Data), 10, -5, true)]
    [MaaData(MaaTypes.Adb, nameof(Data), -15, 20, true)]
    [MaaData(MaaTypes.None, nameof(Data), 5, 5, false)]
    public void Interface_RelativeMove(MaaTypes type, IMaaController maaController, int dx, int dy, bool expectSuccess)
    {
        Assert.IsNotNull(maaController);

        // Act
        var job = maaController.RelativeMove(dx, dy);

        Assert.IsNotNull(job);

        // Assert
        if (expectSuccess)
        {
            // Controllers that support RelativeMove should complete the job successfully
            Assert.IsTrue(job.IsCompleted, "Expected RelativeMove job to complete for a supported controller.");
            Assert.IsTrue(job.IsSuccess, "Expected RelativeMove job to succeed for a supported controller.");
        }
        else
        {
            // Controllers that do not support RelativeMove should not succeed
            Assert.IsTrue(job.IsCompleted, "Expected RelativeMove job to complete even for unsupported controllers.");
            Assert.IsFalse(job.IsSuccess, "Expected RelativeMove job to fail for an unsupported controller.");
        }
    }

    [TestMethod]
    [MaaData(MaaTypes.All, nameof(Data), "echo hello", 20000)]
    public void Interface_Shell_ShellOutput(MaaTypes type, IMaaController maaController, string cmd, long millisecondsTimeout)
    {
        Assert.IsNotNull(maaController);

```

1. 根据你测试集中实际使用的枚举成员,调整 `MaaTypes` 的取值(`Win32`、`Adb`、`None`):
   - 支持 `RelativeMove` 的控制器(例如 Win32ADB),
   - 不支持 `RelativeMove` 的控制器。
2. 将对作业结果的断言与现有的作业抽象保持一致:
   - 如果你的作业类型没有 `IsCompleted` / `IsSuccess`,请改用你在其他测试里使用的实际状态/结果 API(例如 `Wait`、`Status == MaaJobStatus.Succeeded` 等)。
3. 如果其他控制器测试(touch/move/key)使用了共享的帮助方法来断言作业完成情况,建议重构 `Interface_RelativeMove` 的方法体,使其复用该帮助方法以保持一致性。
4. 如果 `MaaData` 需要一个超时参数来等待作业完成,请在 `Interface_RelativeMove` 中添加一个 `long millisecondsTimeout` 参数,并相应扩展 `MaaData` 的参数列表,参考 `Interface_Shell_ShellOutput` 处理超时的方式。
</issue_to_address>

### 评论 4
<location path="src/MaaFramework.Binding.UnitTests/Test_Custom.cs" line_range="305-308" />
<code_context>
         public bool Scroll(int dx, int dy)
             => c.Scroll(dx, dy).Wait().IsSucceeded();
+
+        public bool RelativeMove(int dx, int dy)
+            => c.RelativeMove(dx, dy).Wait().IsSucceeded();
+
+        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
+            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded() && c.GetShellOutput(out _);
+
</code_context>
<issue_to_address>
**suggestion (testing):** 为自定义控制器实现中的新 RelativeMoveShell 方法添加测试

这些新的适配方法(RelativeMoveShell)目前缺少单元测试来在自定义控制器上调用它们并验证:
- 当底层 MaaController 作业成功时,RelativeMove 返回 true- Shell 会向缓冲区写入输出,并且仅在 Shell 作业和 GetShellOutput 都成功时才返回 true。
请添加与现有 click/key/touch 测试类似的用例,以验证新的委托和非托管函数指针接线是否正确。

推荐实现:

```csharp
        public bool RelativeMove(int dx, int dy)
            => c.RelativeMove(dx, dy).Wait().IsSucceeded();

        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded()
               && c.GetShellOutput(out buffer);

        public bool Inactive()
                => c.Inactive().Wait().IsSucceeded();
        public bool GetInfo(IMaaStringBuffer buffer)

```

为了完整实现你的测试需求,你还需要在 `src/MaaFramework.Binding.UnitTests/Test_Custom.cs` 中、现有 click/key/touch 测试附近添加新的单元测试。由于此处无法看到该文件的那部分内容,你需要将下面这些模式整合到现有测试类中(复用现有的命名约定和初始化辅助类):

1. **RelativeMove 测试**

   添加一个测试用例,用于:
   - 使用与现有 clickscroll 测试相同的固定装置/模式创建并初始化待测自定义控制器;
   - 配置底层 `MaaController`(或它的 mock/fake),使其在调用 `RelativeMove(dx, dy)` 时返回一个成功的作业结果;
   - 调用适配器的 `RelativeMove(dx, dy)` 方法;
   - 断言返回值为 `true`,并且期望的非托管函数指针 / 委托以正确的 `dx`/`dy` 被调用(断言方式应与现有 scroll/touch 测试保持一致)。

   示例结构(请根据文件中实际类型和固定装置进行调整):

   ```csharp
   [Fact]
   public void RelativeMove_ReturnsTrue_WhenUnderlyingJobSucceeds()
   {
       // Arrange
       using var fixture = CreateCustomControllerFixture(); // 你已有的辅助方法
       var controller = fixture.Controller;                 // 自定义适配实例

       fixture.MaaControllerMock
              .Setup(c => c.RelativeMove(10, -5))
              .Returns(SuccessfulJob());

       // Act
       var result = controller.RelativeMove(10, -5);

       // Assert
       Assert.True(result);
       fixture.MaaControllerMock.Verify(c => c.RelativeMove(10, -5), Times.Once);
   }
   ```

2. **Shell 成功场景测试**

   添加一个测试用例,用于:
   - 设置 `MaaController.Shell(cmd, timeout)` 返回成功作业;
   - 设置 `MaaController.GetShellOutput(out buffer)`:
     - 返回 `true`;
     - 使用已知输出(如 `"hello"`)填充一个 `IMaaStringBuffer`;
   - 调用适配器的 `Shell(cmd, timeout, buffer)` 方法;
   - 断言:
     - 方法返回 `true`;
     - 传入的 `IMaaStringBuffer` 包含预期的输出字符串;
     - `Shell` 和 `GetShellOutput` 都恰好被调用一次。

   示例结构:

   ```csharp
   [Fact]
   public void Shell_ReturnsTrueAndWritesOutput_WhenShellAndGetOutputSucceed()
   {
       // Arrange
       using var fixture = CreateCustomControllerFixture();
       var controller = fixture.Controller;
       var buffer = new TestStringBuffer(); // 复用你已有的 IMaaStringBuffer 测试实现

       const string cmd = "echo hello";
       const long timeout = 1000;
       const string expectedOutput = "hello";

       fixture.MaaControllerMock
              .Setup(c => c.Shell(cmd, timeout))
              .Returns(SuccessfulJob());

       fixture.MaaControllerMock
              .Setup(c => c.GetShellOutput(out It.Ref<IMaaStringBuffer>.IsAny))
              .Callback(new GetShellOutputCallback((out IMaaStringBuffer b) =>
              {
                  b = buffer;
                  b.Set(expectedOutput); // 或根据你的 buffer API 调整
              }))
              .Returns(true);

       // Act
       var result = controller.Shell(cmd, timeout, buffer);

       // Assert
       Assert.True(result);
       Assert.Equal(expectedOutput, buffer.ToString()); // 根据你的 buffer API 调整

       fixture.MaaControllerMock.Verify(c => c.Shell(cmd, timeout), Times.Once);
       fixture.MaaControllerMock.Verify(c => c.GetShellOutput(out It.Ref<IMaaStringBuffer>.IsAny), Times.Once);
   }
   ```

3. **Shell 失败行为测试(可选但推荐)**

   为了匹配你描述的行为(Shell 只有在两个操作都成功时才返回 true),请再添加一些测试来验证:
   - 当 `Shell(cmd, timeout)` 失败但 `GetShellOutput` 本可以成功时,`Shell` 方法返回 `false`;
   - 当 `Shell(cmd, timeout)` 成功但 `GetShellOutput` 返回 `false` 时,`Shell` 方法也返回 `false`。

   这些测试应使用与上述相同的固定装置/Mock 模式,并同时断言返回值以及具体的调用情况。

请将这些测试模式整合进你现有的测试套件中,复用 `Test_Custom.cs` 中已经存在的固定装置、Mock 和辅助类型,使其在命名和结构上与现有的 click/key/touch 测试保持一致。
</issue_to_address>

Sourcery 对开源项目免费使用——如果你喜欢这些评审,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续评审。
Original comment in English

Hey - I've found 4 issues, and left some high level feedback:

  • In MaaOptionExtensions.SetOption_ScreenshotResizeMethod the parameter is typed as bool but ControllerOption.ScreenshotResizeMethod expects an int (and is marshalled as such in SetOption<T>), which will cause runtime failures; this extension should accept an int (e.g., interpolation enum value) instead of bool.
  • In NotificationDetailContext the [JsonSerializable(typeof(NodeAttr))] entry was removed while NodeNextListDetail still uses IReadOnlyList<NodeAttr>; with source-generated System.Text.Json this type may no longer be included in the generated context, so please double-check whether NodeAttr still needs to be explicitly registered to avoid deserialization issues.
  • The XML docs for NodeWaitFreezesDetail currently use <inheritdoc cref="MaaMsg.Node.Action"/> instead of MaaMsg.Node.WaitFreezes, which can be confusing for consumers and should be updated to reference the correct message group.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `MaaOptionExtensions.SetOption_ScreenshotResizeMethod` the parameter is typed as `bool` but `ControllerOption.ScreenshotResizeMethod` expects an `int` (and is marshalled as such in `SetOption<T>`), which will cause runtime failures; this extension should accept an `int` (e.g., interpolation enum value) instead of `bool`.
- In `NotificationDetailContext` the `[JsonSerializable(typeof(NodeAttr))]` entry was removed while `NodeNextListDetail` still uses `IReadOnlyList<NodeAttr>`; with source-generated `System.Text.Json` this type may no longer be included in the generated context, so please double-check whether `NodeAttr` still needs to be explicitly registered to avoid deserialization issues.
- The XML docs for `NodeWaitFreezesDetail` currently use `<inheritdoc cref="MaaMsg.Node.Action"/>` instead of `MaaMsg.Node.WaitFreezes`, which can be confusing for consumers and should be updated to reference the correct message group.

## Individual Comments

### Comment 1
<location path="src/MaaFramework.Binding/Extensions/MaaOptionExtensions.cs" line_range="35-36" />
<code_context>
+
+    /// <inheritdoc cref="ControllerOption.ScreenshotResizeMethod"/>
+    /// <inheritdoc cref="IMaaOption{T}.SetOption{T}"/>
+    public static bool SetOption_ScreenshotResizeMethod(this IMaaOption<ControllerOption>? opt, bool value)
+        => opt?.SetOption(ControllerOption.ScreenshotResizeMethod, value) ?? throw new ArgumentNullException(nameof(opt));
+
+    /// <inheritdoc cref="ControllerOption.BackgroundManagedKeys"/>
</code_context>
<issue_to_address>
**issue (bug_risk):** ScreenshotResizeMethod option extension uses bool instead of int

`ControllerOption.ScreenshotResizeMethod` is defined as an `int` (matching `cv::InterpolationFlags`), but this extension takes a `bool` and passes it to `SetOption`, which will hit the `int` branch in `MaaController.SetOption` and throw `NotSupportedException` at runtime.

This should accept an `int` (or an enum) and pass it to `SetOption(ControllerOption.ScreenshotResizeMethod, value)`; otherwise the API is misleading and unusable.
</issue_to_address>

### Comment 2
<location path="src/MaaFramework.Binding.Native/Interop/NativeLibrary.cs" line_range="94" />
<code_context>
         // to AppContext.BaseDirectory which is the directory containing the single-file executable.
         var assemblyDirectory = string.IsNullOrEmpty(s_assembly.Location)
             ? AppContext.BaseDirectory
-            : Path.GetDirectoryName(s_assembly.Location)!;
+            : Path.GetDirectoryName(s_assembly.Location);

         string?[] basePaths =
</code_context>
<issue_to_address>
**issue (bug_risk):** Potential null assemblyDirectory from Path.GetDirectoryName

Without the `!`, `assemblyDirectory` may be null when `s_assembly.Location` has no directory component, and passing that into `Path.Combine` will throw. Please either fall back to `AppContext.BaseDirectory` when `Path.GetDirectoryName` returns null, or otherwise handle the null case to keep resolution robust.
</issue_to_address>

### Comment 3
<location path="src/MaaFramework.Binding.UnitTests/Test_IMaaController.cs" line_range="147-149" />
<code_context>
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetLongSide, 1280)]
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetShortSide, 720)]
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotUseRawSize, false)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, false)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotResizeMethod, 0)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[] { 0x57, 0x41, 0x53, 0x44 })]
     public void Interface_SetOption(MaaTypes type, IMaaController maaController, ControllerOption opt, object arg)
     {
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for the new RelativeMove controller action on IMaaController.

The new RelativeMove API is exposed on IMaaController and routed via MaaControllerPostRelativeMove, but there’s no unit test covering it. Please add a test similar to the existing touch/move/key ones (e.g. `Interface_RelativeMove`) that:
- calls `maaController.RelativeMove(dx, dy)` with various dx/dy values (including negative and zero),
- asserts the MaaJob completes successfully for controllers that support it (e.g. Win32/ADB), and
- verifies the expected failure behavior for controllers that don’t support RelativeMove.
This ensures the new P/Invoke and managed wiring are correct and guards against regressions across controller types.

Suggested implementation:

```csharp
    [TestMethod]
    [MaaData(MaaTypes.Win32, nameof(Data), 0, 0, true)]
    [MaaData(MaaTypes.Win32, nameof(Data), 10, -5, true)]
    [MaaData(MaaTypes.Adb, nameof(Data), -15, 20, true)]
    [MaaData(MaaTypes.None, nameof(Data), 5, 5, false)]
    public void Interface_RelativeMove(MaaTypes type, IMaaController maaController, int dx, int dy, bool expectSuccess)
    {
        Assert.IsNotNull(maaController);

        // Act
        var job = maaController.RelativeMove(dx, dy);

        Assert.IsNotNull(job);

        // Assert
        if (expectSuccess)
        {
            // Controllers that support RelativeMove should complete the job successfully
            Assert.IsTrue(job.IsCompleted, "Expected RelativeMove job to complete for a supported controller.");
            Assert.IsTrue(job.IsSuccess, "Expected RelativeMove job to succeed for a supported controller.");
        }
        else
        {
            // Controllers that do not support RelativeMove should not succeed
            Assert.IsTrue(job.IsCompleted, "Expected RelativeMove job to complete even for unsupported controllers.");
            Assert.IsFalse(job.IsSuccess, "Expected RelativeMove job to fail for an unsupported controller.");
        }
    }

    [TestMethod]
    [MaaData(MaaTypes.All, nameof(Data), "echo hello", 20000)]
    public void Interface_Shell_ShellOutput(MaaTypes type, IMaaController maaController, string cmd, long millisecondsTimeout)
    {
        Assert.IsNotNull(maaController);

```

1. Adjust the `MaaTypes` values (`Win32`, `Adb`, `None`) to match the actual enum members used in your test suite for:
   - Controllers that support `RelativeMove` (e.g., Win32, ADB),
   - Controllers that do not support `RelativeMove`.
2. Align the assertions on the job result with your existing job abstraction:
   - If your job type does not expose `IsCompleted` / `IsSuccess`, replace them with the actual status/Result APIs you use in other tests (e.g. `Wait`, `Status == MaaJobStatus.Succeeded`, etc.).
3. If other controller tests (touch/move/key) use a shared helper to assert job completion, refactor the body of `Interface_RelativeMove` to use that helper for consistency.
4. If `MaaData` requires a timeout parameter for job completion, add a `long millisecondsTimeout` parameter to `Interface_RelativeMove` and extend the `MaaData` arguments accordingly, mirroring how `Interface_Shell_ShellOutput` handles timeouts.
</issue_to_address>

### Comment 4
<location path="src/MaaFramework.Binding.UnitTests/Test_Custom.cs" line_range="305-308" />
<code_context>
         public bool Scroll(int dx, int dy)
             => c.Scroll(dx, dy).Wait().IsSucceeded();
+
+        public bool RelativeMove(int dx, int dy)
+            => c.RelativeMove(dx, dy).Wait().IsSucceeded();
+
+        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
+            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded() && c.GetShellOutput(out _);
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests that exercise the new RelativeMove and Shell methods on the custom controller implementation.

These new adapter methods (RelativeMove and Shell) currently lack unit tests that call them on the custom controller and verify that:
- RelativeMove returns true when the underlying MaaController job succeeds.
- Shell writes output to the buffer and returns true only when both the Shell job and GetShellOutput succeed.
Please add tests similar to the existing click/key/touch tests to validate the new delegates and unmanaged function pointer wiring.

Suggested implementation:

```csharp
        public bool RelativeMove(int dx, int dy)
            => c.RelativeMove(dx, dy).Wait().IsSucceeded();

        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded()
               && c.GetShellOutput(out buffer);

        public bool Inactive()
                => c.Inactive().Wait().IsSucceeded();
        public bool GetInfo(IMaaStringBuffer buffer)

```

To fully implement your test request, you should also add new unit tests in `src/MaaFramework.Binding.UnitTests/Test_Custom.cs` alongside the existing click/key/touch tests. Because that part of the file is not visible here, you'll need to integrate the following patterns into your existing test class (reusing its naming conventions and setup helpers):

1. **RelativeMove test**

   Add a test that:
   - Creates/initializes the custom controller under test (using the same fixture/pattern as the existing click or scroll tests).
   - Configures the underlying `MaaController` (or its mock/fake) to have `RelativeMove(dx, dy)` return a successful job result.
   - Calls the adapters `RelativeMove(dx, dy)` method.
   - Asserts that the return value is `true` and that the expected unmanaged function pointer / delegate has been invoked with the correct `dx`/`dy` (following the same assertion strategy as your existing scroll/touch tests).

   Example structure (adapt to actual type names and fixtures already in the file):

   ```csharp
   [Fact]
   public void RelativeMove_ReturnsTrue_WhenUnderlyingJobSucceeds()
   {
       // Arrange
       using var fixture = CreateCustomControllerFixture(); // whatever helper you already use
       var controller = fixture.Controller;                 // your custom adapter instance

       fixture.MaaControllerMock
              .Setup(c => c.RelativeMove(10, -5))
              .Returns(SuccessfulJob());

       // Act
       var result = controller.RelativeMove(10, -5);

       // Assert
       Assert.True(result);
       fixture.MaaControllerMock.Verify(c => c.RelativeMove(10, -5), Times.Once);
   }
   ```

2. **Shell success test**

   Add a test that:
   - Sets up `MaaController.Shell(cmd, timeout)` to return a successful job.
   - Sets up `MaaController.GetShellOutput(out buffer)` to:
     - Return `true`.
     - Populate an `IMaaStringBuffer` with known output (e.g., `"hello"`).
   - Calls the adapters `Shell(cmd, timeout, buffer)` method.
   - Asserts:
     - The method returns `true`.
     - The `IMaaStringBuffer` passed in contains the expected output string.
     - Both `Shell` and `GetShellOutput` were invoked exactly once.

   Example structure:

   ```csharp
   [Fact]
   public void Shell_ReturnsTrueAndWritesOutput_WhenShellAndGetOutputSucceed()
   {
       // Arrange
       using var fixture = CreateCustomControllerFixture();
       var controller = fixture.Controller;
       var buffer = new TestStringBuffer(); // reuse your existing IMaaStringBuffer test implementation

       const string cmd = "echo hello";
       const long timeout = 1000;
       const string expectedOutput = "hello";

       fixture.MaaControllerMock
              .Setup(c => c.Shell(cmd, timeout))
              .Returns(SuccessfulJob());

       fixture.MaaControllerMock
              .Setup(c => c.GetShellOutput(out It.Ref<IMaaStringBuffer>.IsAny))
              .Callback(new GetShellOutputCallback((out IMaaStringBuffer b) =>
              {
                  b = buffer;
                  b.Set(expectedOutput); // or whatever API your buffer exposes
              }))
              .Returns(true);

       // Act
       var result = controller.Shell(cmd, timeout, buffer);

       // Assert
       Assert.True(result);
       Assert.Equal(expectedOutput, buffer.ToString()); // adapt to your buffer API

       fixture.MaaControllerMock.Verify(c => c.Shell(cmd, timeout), Times.Once);
       fixture.MaaControllerMock.Verify(c => c.GetShellOutput(out It.Ref<IMaaStringBuffer>.IsAny), Times.Once);
   }
   ```

3. **Shell failure behavior tests (optional but recommended)**

   To mirror the behavior you described (Shell returns true only when *both* operations succeed), add tests that:
   - Verify `Shell` returns `false` when `Shell(cmd, timeout)` fails but `GetShellOutput` would succeed.
   - Verify `Shell` returns `false` when `Shell(cmd, timeout)` succeeds but `GetShellOutput` returns `false`.

   These tests should use the same fixture/mocking pattern as above and assert both the return value and which calls were made.

Please integrate these test patterns into your existing test suite, reusing the actual fixture, mock, and helper types already present in `Test_Custom.cs` so they follow the same conventions as the existing click/key/touch tests.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/MaaFramework.Binding/Extensions/MaaOptionExtensions.cs Outdated
Comment thread src/MaaFramework.Binding.Native/Interop/NativeLibrary.cs
Comment on lines +147 to +149
[MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, false)]
[MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotResizeMethod, 0)]
[MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[] { 0x57, 0x41, 0x53, 0x44 })]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 为 IMaaController 上新的 RelativeMove 控制器动作添加测试覆盖

新的 RelativeMove API 已在 IMaaController 上暴露,并通过 MaaControllerPostRelativeMove 进行路由,但目前没有覆盖它的单元测试。请添加一个类似现有 touch/move/key 测试的用例(例如 Interface_RelativeMove),该用例应当:

  • 使用不同的 dx/dy(包括负数和 0)调用 maaController.RelativeMove(dx, dy)
  • 对于支持该功能的控制器(例如 Win32/ADB),断言 MaaJob 能成功完成;
  • 验证对不支持 RelativeMove 的控制器来说预期的失败行为。
    这样可以确保新的 P/Invoke 和托管层接线是正确的,并防止在不同控制器类型间出现回归。

推荐实现:

    [TestMethod]
    [MaaData(MaaTypes.Win32, nameof(Data), 0, 0, true)]
    [MaaData(MaaTypes.Win32, nameof(Data), 10, -5, true)]
    [MaaData(MaaTypes.Adb, nameof(Data), -15, 20, true)]
    [MaaData(MaaTypes.None, nameof(Data), 5, 5, false)]
    public void Interface_RelativeMove(MaaTypes type, IMaaController maaController, int dx, int dy, bool expectSuccess)
    {
        Assert.IsNotNull(maaController);

        // Act
        var job = maaController.RelativeMove(dx, dy);

        Assert.IsNotNull(job);

        // Assert
        if (expectSuccess)
        {
            // Controllers that support RelativeMove should complete the job successfully
            Assert.IsTrue(job.IsCompleted, "Expected RelativeMove job to complete for a supported controller.");
            Assert.IsTrue(job.IsSuccess, "Expected RelativeMove job to succeed for a supported controller.");
        }
        else
        {
            // Controllers that do not support RelativeMove should not succeed
            Assert.IsTrue(job.IsCompleted, "Expected RelativeMove job to complete even for unsupported controllers.");
            Assert.IsFalse(job.IsSuccess, "Expected RelativeMove job to fail for an unsupported controller.");
        }
    }

    [TestMethod]
    [MaaData(MaaTypes.All, nameof(Data), "echo hello", 20000)]
    public void Interface_Shell_ShellOutput(MaaTypes type, IMaaController maaController, string cmd, long millisecondsTimeout)
    {
        Assert.IsNotNull(maaController);
  1. 根据你测试集中实际使用的枚举成员,调整 MaaTypes 的取值(Win32AdbNone):
    • 支持 RelativeMove 的控制器(例如 Win32、ADB),
    • 不支持 RelativeMove 的控制器。
  2. 将对作业结果的断言与现有的作业抽象保持一致:
    • 如果你的作业类型没有 IsCompleted / IsSuccess,请改用你在其他测试里使用的实际状态/结果 API(例如 WaitStatus == MaaJobStatus.Succeeded 等)。
  3. 如果其他控制器测试(touch/move/key)使用了共享的帮助方法来断言作业完成情况,建议重构 Interface_RelativeMove 的方法体,使其复用该帮助方法以保持一致性。
  4. 如果 MaaData 需要一个超时参数来等待作业完成,请在 Interface_RelativeMove 中添加一个 long millisecondsTimeout 参数,并相应扩展 MaaData 的参数列表,参考 Interface_Shell_ShellOutput 处理超时的方式。
Original comment in English

suggestion (testing): Add coverage for the new RelativeMove controller action on IMaaController.

The new RelativeMove API is exposed on IMaaController and routed via MaaControllerPostRelativeMove, but there’s no unit test covering it. Please add a test similar to the existing touch/move/key ones (e.g. Interface_RelativeMove) that:

  • calls maaController.RelativeMove(dx, dy) with various dx/dy values (including negative and zero),
  • asserts the MaaJob completes successfully for controllers that support it (e.g. Win32/ADB), and
  • verifies the expected failure behavior for controllers that don’t support RelativeMove.
    This ensures the new P/Invoke and managed wiring are correct and guards against regressions across controller types.

Suggested implementation:

    [TestMethod]
    [MaaData(MaaTypes.Win32, nameof(Data), 0, 0, true)]
    [MaaData(MaaTypes.Win32, nameof(Data), 10, -5, true)]
    [MaaData(MaaTypes.Adb, nameof(Data), -15, 20, true)]
    [MaaData(MaaTypes.None, nameof(Data), 5, 5, false)]
    public void Interface_RelativeMove(MaaTypes type, IMaaController maaController, int dx, int dy, bool expectSuccess)
    {
        Assert.IsNotNull(maaController);

        // Act
        var job = maaController.RelativeMove(dx, dy);

        Assert.IsNotNull(job);

        // Assert
        if (expectSuccess)
        {
            // Controllers that support RelativeMove should complete the job successfully
            Assert.IsTrue(job.IsCompleted, "Expected RelativeMove job to complete for a supported controller.");
            Assert.IsTrue(job.IsSuccess, "Expected RelativeMove job to succeed for a supported controller.");
        }
        else
        {
            // Controllers that do not support RelativeMove should not succeed
            Assert.IsTrue(job.IsCompleted, "Expected RelativeMove job to complete even for unsupported controllers.");
            Assert.IsFalse(job.IsSuccess, "Expected RelativeMove job to fail for an unsupported controller.");
        }
    }

    [TestMethod]
    [MaaData(MaaTypes.All, nameof(Data), "echo hello", 20000)]
    public void Interface_Shell_ShellOutput(MaaTypes type, IMaaController maaController, string cmd, long millisecondsTimeout)
    {
        Assert.IsNotNull(maaController);
  1. Adjust the MaaTypes values (Win32, Adb, None) to match the actual enum members used in your test suite for:
    • Controllers that support RelativeMove (e.g., Win32, ADB),
    • Controllers that do not support RelativeMove.
  2. Align the assertions on the job result with your existing job abstraction:
    • If your job type does not expose IsCompleted / IsSuccess, replace them with the actual status/Result APIs you use in other tests (e.g. Wait, Status == MaaJobStatus.Succeeded, etc.).
  3. If other controller tests (touch/move/key) use a shared helper to assert job completion, refactor the body of Interface_RelativeMove to use that helper for consistency.
  4. If MaaData requires a timeout parameter for job completion, add a long millisecondsTimeout parameter to Interface_RelativeMove and extend the MaaData arguments accordingly, mirroring how Interface_Shell_ShellOutput handles timeouts.

Comment on lines +305 to +308
public bool RelativeMove(int dx, int dy)
=> c.RelativeMove(dx, dy).Wait().IsSucceeded();

public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 为自定义控制器实现中的新 RelativeMove 和 Shell 方法添加测试

这些新的适配方法(RelativeMove 和 Shell)目前缺少单元测试来在自定义控制器上调用它们并验证:

  • 当底层 MaaController 作业成功时,RelativeMove 返回 true;
  • Shell 会向缓冲区写入输出,并且仅在 Shell 作业和 GetShellOutput 都成功时才返回 true。
    请添加与现有 click/key/touch 测试类似的用例,以验证新的委托和非托管函数指针接线是否正确。

推荐实现:

        public bool RelativeMove(int dx, int dy)
            => c.RelativeMove(dx, dy).Wait().IsSucceeded();

        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded()
               && c.GetShellOutput(out buffer);

        public bool Inactive()
                => c.Inactive().Wait().IsSucceeded();
        public bool GetInfo(IMaaStringBuffer buffer)

为了完整实现你的测试需求,你还需要在 src/MaaFramework.Binding.UnitTests/Test_Custom.cs 中、现有 click/key/touch 测试附近添加新的单元测试。由于此处无法看到该文件的那部分内容,你需要将下面这些模式整合到现有测试类中(复用现有的命名约定和初始化辅助类):

  1. RelativeMove 测试

    添加一个测试用例,用于:

    • 使用与现有 click 或 scroll 测试相同的固定装置/模式创建并初始化待测自定义控制器;
    • 配置底层 MaaController(或它的 mock/fake),使其在调用 RelativeMove(dx, dy) 时返回一个成功的作业结果;
    • 调用适配器的 RelativeMove(dx, dy) 方法;
    • 断言返回值为 true,并且期望的非托管函数指针 / 委托以正确的 dx/dy 被调用(断言方式应与现有 scroll/touch 测试保持一致)。

    示例结构(请根据文件中实际类型和固定装置进行调整):

    [Fact]
    public void RelativeMove_ReturnsTrue_WhenUnderlyingJobSucceeds()
    {
        // Arrange
        using var fixture = CreateCustomControllerFixture(); // 你已有的辅助方法
        var controller = fixture.Controller;                 // 自定义适配实例
    
        fixture.MaaControllerMock
               .Setup(c => c.RelativeMove(10, -5))
               .Returns(SuccessfulJob());
    
        // Act
        var result = controller.RelativeMove(10, -5);
    
        // Assert
        Assert.True(result);
        fixture.MaaControllerMock.Verify(c => c.RelativeMove(10, -5), Times.Once);
    }
  2. Shell 成功场景测试

    添加一个测试用例,用于:

    • 设置 MaaController.Shell(cmd, timeout) 返回成功作业;
    • 设置 MaaController.GetShellOutput(out buffer)
      • 返回 true
      • 使用已知输出(如 "hello")填充一个 IMaaStringBuffer
    • 调用适配器的 Shell(cmd, timeout, buffer) 方法;
    • 断言:
      • 方法返回 true
      • 传入的 IMaaStringBuffer 包含预期的输出字符串;
      • ShellGetShellOutput 都恰好被调用一次。

    示例结构:

    [Fact]
    public void Shell_ReturnsTrueAndWritesOutput_WhenShellAndGetOutputSucceed()
    {
        // Arrange
        using var fixture = CreateCustomControllerFixture();
        var controller = fixture.Controller;
        var buffer = new TestStringBuffer(); // 复用你已有的 IMaaStringBuffer 测试实现
    
        const string cmd = "echo hello";
        const long timeout = 1000;
        const string expectedOutput = "hello";
    
        fixture.MaaControllerMock
               .Setup(c => c.Shell(cmd, timeout))
               .Returns(SuccessfulJob());
    
        fixture.MaaControllerMock
               .Setup(c => c.GetShellOutput(out It.Ref<IMaaStringBuffer>.IsAny))
               .Callback(new GetShellOutputCallback((out IMaaStringBuffer b) =>
               {
                   b = buffer;
                   b.Set(expectedOutput); // 或根据你的 buffer API 调整
               }))
               .Returns(true);
    
        // Act
        var result = controller.Shell(cmd, timeout, buffer);
    
        // Assert
        Assert.True(result);
        Assert.Equal(expectedOutput, buffer.ToString()); // 根据你的 buffer API 调整
    
        fixture.MaaControllerMock.Verify(c => c.Shell(cmd, timeout), Times.Once);
        fixture.MaaControllerMock.Verify(c => c.GetShellOutput(out It.Ref<IMaaStringBuffer>.IsAny), Times.Once);
    }
  3. Shell 失败行为测试(可选但推荐)

    为了匹配你描述的行为(Shell 只有在两个操作都成功时才返回 true),请再添加一些测试来验证:

    • Shell(cmd, timeout) 失败但 GetShellOutput 本可以成功时,Shell 方法返回 false
    • Shell(cmd, timeout) 成功但 GetShellOutput 返回 false 时,Shell 方法也返回 false

    这些测试应使用与上述相同的固定装置/Mock 模式,并同时断言返回值以及具体的调用情况。

请将这些测试模式整合进你现有的测试套件中,复用 Test_Custom.cs 中已经存在的固定装置、Mock 和辅助类型,使其在命名和结构上与现有的 click/key/touch 测试保持一致。

Original comment in English

suggestion (testing): Add tests that exercise the new RelativeMove and Shell methods on the custom controller implementation.

These new adapter methods (RelativeMove and Shell) currently lack unit tests that call them on the custom controller and verify that:

  • RelativeMove returns true when the underlying MaaController job succeeds.
  • Shell writes output to the buffer and returns true only when both the Shell job and GetShellOutput succeed.
    Please add tests similar to the existing click/key/touch tests to validate the new delegates and unmanaged function pointer wiring.

Suggested implementation:

        public bool RelativeMove(int dx, int dy)
            => c.RelativeMove(dx, dy).Wait().IsSucceeded();

        public bool Shell(string cmd, long millisecondsTimeout, IMaaStringBuffer buffer)
            => c.Shell(cmd, millisecondsTimeout).Wait().IsSucceeded()
               && c.GetShellOutput(out buffer);

        public bool Inactive()
                => c.Inactive().Wait().IsSucceeded();
        public bool GetInfo(IMaaStringBuffer buffer)

To fully implement your test request, you should also add new unit tests in src/MaaFramework.Binding.UnitTests/Test_Custom.cs alongside the existing click/key/touch tests. Because that part of the file is not visible here, you'll need to integrate the following patterns into your existing test class (reusing its naming conventions and setup helpers):

  1. RelativeMove test

    Add a test that:

    • Creates/initializes the custom controller under test (using the same fixture/pattern as the existing click or scroll tests).
    • Configures the underlying MaaController (or its mock/fake) to have RelativeMove(dx, dy) return a successful job result.
    • Calls the adapter’s RelativeMove(dx, dy) method.
    • Asserts that the return value is true and that the expected unmanaged function pointer / delegate has been invoked with the correct dx/dy (following the same assertion strategy as your existing scroll/touch tests).

    Example structure (adapt to actual type names and fixtures already in the file):

    [Fact]
    public void RelativeMove_ReturnsTrue_WhenUnderlyingJobSucceeds()
    {
        // Arrange
        using var fixture = CreateCustomControllerFixture(); // whatever helper you already use
        var controller = fixture.Controller;                 // your custom adapter instance
    
        fixture.MaaControllerMock
               .Setup(c => c.RelativeMove(10, -5))
               .Returns(SuccessfulJob());
    
        // Act
        var result = controller.RelativeMove(10, -5);
    
        // Assert
        Assert.True(result);
        fixture.MaaControllerMock.Verify(c => c.RelativeMove(10, -5), Times.Once);
    }
  2. Shell success test

    Add a test that:

    • Sets up MaaController.Shell(cmd, timeout) to return a successful job.
    • Sets up MaaController.GetShellOutput(out buffer) to:
      • Return true.
      • Populate an IMaaStringBuffer with known output (e.g., "hello").
    • Calls the adapter’s Shell(cmd, timeout, buffer) method.
    • Asserts:
      • The method returns true.
      • The IMaaStringBuffer passed in contains the expected output string.
      • Both Shell and GetShellOutput were invoked exactly once.

    Example structure:

    [Fact]
    public void Shell_ReturnsTrueAndWritesOutput_WhenShellAndGetOutputSucceed()
    {
        // Arrange
        using var fixture = CreateCustomControllerFixture();
        var controller = fixture.Controller;
        var buffer = new TestStringBuffer(); // reuse your existing IMaaStringBuffer test implementation
    
        const string cmd = "echo hello";
        const long timeout = 1000;
        const string expectedOutput = "hello";
    
        fixture.MaaControllerMock
               .Setup(c => c.Shell(cmd, timeout))
               .Returns(SuccessfulJob());
    
        fixture.MaaControllerMock
               .Setup(c => c.GetShellOutput(out It.Ref<IMaaStringBuffer>.IsAny))
               .Callback(new GetShellOutputCallback((out IMaaStringBuffer b) =>
               {
                   b = buffer;
                   b.Set(expectedOutput); // or whatever API your buffer exposes
               }))
               .Returns(true);
    
        // Act
        var result = controller.Shell(cmd, timeout, buffer);
    
        // Assert
        Assert.True(result);
        Assert.Equal(expectedOutput, buffer.ToString()); // adapt to your buffer API
    
        fixture.MaaControllerMock.Verify(c => c.Shell(cmd, timeout), Times.Once);
        fixture.MaaControllerMock.Verify(c => c.GetShellOutput(out It.Ref<IMaaStringBuffer>.IsAny), Times.Once);
    }
  3. Shell failure behavior tests (optional but recommended)

    To mirror the behavior you described (Shell returns true only when both operations succeed), add tests that:

    • Verify Shell returns false when Shell(cmd, timeout) fails but GetShellOutput would succeed.
    • Verify Shell returns false when Shell(cmd, timeout) succeeds but GetShellOutput returns false.

    These tests should use the same fixture/mocking pattern as above and assert both the return value and which calls were made.

Please integrate these test patterns into your existing test suite, reusing the actual fixture, mock, and helper types already present in Test_Custom.cs so they follow the same conventions as the existing click/key/touch tests.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 46 out of 46 changed files in this pull request and generated 5 comments.

Comment thread src/MaaFramework.Binding/MaaMsg.cs Outdated
Comment thread src/MaaFramework.Binding/MaaMsg.cs Outdated
Comment thread src/MaaFramework.Binding/MaaMsg.cs Outdated
Comment thread src/MaaFramework.Binding/MaaMsg.cs Outdated

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题,并给出了一些高层次的反馈:

  • 在 MaaRecordController 中,围绕调试字段使用没有指定特定警告 ID 的 #pragma warning disable/restore 会屏蔽该区域内的所有编译器警告;为安全起见,建议只针对具体的警告(例如 CS0169),这样就不会意外隐藏无关的问题。
  • MaaMarshaller.ConvertToMaaOptionValue(IEnumerable<int>) 中,建议将硬编码的 4 字节宽度替换为 sizeof(int),并对小端字节序的假设进行文档化/集中管理,这样一来,如果整数大小或平台字节序发生变化,编组逻辑仍能保持正确且自解释。
供 AI Agents 使用的提示
Please address the comments from this code review:

## Overall Comments
- 在 MaaRecordController 中,围绕调试字段使用没有指定特定警告 ID 的 `#pragma warning disable`/`restore` 会屏蔽该区域内的所有编译器警告;为安全起见,建议只针对具体的警告(例如 CS0169),这样就不会意外隐藏无关的问题。
-`MaaMarshaller.ConvertToMaaOptionValue(IEnumerable<int>)` 中,建议将硬编码的 `4` 字节宽度替换为 `sizeof(int)`,并对小端字节序的假设进行文档化/集中管理,这样一来,如果整数大小或平台字节序发生变化,编组逻辑仍能保持正确且自解释。

## Individual Comments

### Comment 1
<location path="src/MaaFramework.Binding.UnitTests/Test_IMaaController.cs" line_range="147-149" />
<code_context>
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetLongSide, 1280)]
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetShortSide, 720)]
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotUseRawSize, false)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, false)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotResizeMethod, 0)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[] { 0x57, 0x41, 0x53, 0x44 })]
     public void Interface_SetOption(MaaTypes type, IMaaController maaController, ControllerOption opt, object arg)
     {
</code_context>
<issue_to_address>
**suggestion (testing):** 为 BackgroundManagedKeys 和 MouseLockFollow 控制器选项增加更多边界情况的测试覆盖。

当前测试只覆盖了一个非空的 BackgroundManagedKeys 数组以及 MouseLockFollow = false 的情况。请同时增加:

- 一个空的 BackgroundManagedKeys 数组,用于验证零长度的处理。
- 一个更大的 BackgroundManagedKeys 数组(例如 16 个以上条目),以触发 IEnumerable<int> 路径,包括 MaaMarshaller 中的 TryGetNonEnumeratedCount 和 Unsafe.WriteUnaligned。
- 一个 MouseLockFollow = true 的用例,以覆盖两个布尔取值。
- 可选地,增加一个测试,在为非 Win32 控制器设置 BackgroundManagedKeys 时,检查 SetOption 的返回值,以记录并保护不受支持控制器的行为。

建议实现方式:

```csharp
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetLongSide, 1280)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetShortSide, 720)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotUseRawSize, false)]
    // MouseLockFollow: cover both false and true
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, false)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, true)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotResizeMethod, 0)]
    // BackgroundManagedKeys: single non-empty array (existing), empty array, and a larger array to exercise IEnumerable<int> marshalling
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[] { 0x57, 0x41, 0x53, 0x44 })]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[0])]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[]
    {
        0x30, 0x31, 0x32, 0x33,
        0x34, 0x35, 0x36, 0x37,
        0x38, 0x39, 0x41, 0x42,
        0x43, 0x44, 0x45, 0x46
    })]
    public void Interface_SetOption(MaaTypes type, IMaaController maaController, ControllerOption opt, object arg)
    {
        Assert.IsNotNull(maaController);

```

为了完整实现你关于非 Win32 控制器在设置 BackgroundManagedKeys 时检查 SetOption 返回值的可选建议,你还需要:
1. 找到 `Interface_SetOption` 的方法体,并添加一个断言,在 `opt == ControllerOption.BackgroundManagedKeys` 且 `type` 为某个非 Win32 的 `MaaTypes` 值(例如 `MaaTypes.Android` 或其他,取决于你的枚举)时,显式检查返回值。
2. 添加一个 `MaaData` 行,目标为该非 Win32 的 `MaaTypes` 值,并提供一个具有代表性的 `BackgroundManagedKeys` 数组以及期望的返回值,遵循该测试文件中用于验证 `SetOption` 结果的既有模式。
具体使用的枚举值和断言形式需要根据文件其余部分来确定,因此你需要将这些部分适配到现有的测试基础设施中。
</issue_to_address>

Sourcery 对开源项目免费使用——如果你喜欢我们的评审,请考虑分享它们 ✨
帮我变得更有用!请对每条评论点 👍 或 👎,我会根据这些反馈改进后续评审。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • In MaaRecordController, the #pragma warning disable/restore without a specific warning ID around the debug fields will suppress all compiler warnings in that region; it would be safer to target the explicit warning (e.g., CS0169) so you don't accidentally hide unrelated issues.
  • In MaaMarshaller.ConvertToMaaOptionValue(IEnumerable<int>), consider replacing the hardcoded 4 byte width with sizeof(int) and documenting/centralizing the little‑endian assumption so the marshalling logic remains correct and self-explanatory if the integer size or platform endianness ever change.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In MaaRecordController, the `#pragma warning disable`/`restore` without a specific warning ID around the debug fields will suppress all compiler warnings in that region; it would be safer to target the explicit warning (e.g., CS0169) so you don't accidentally hide unrelated issues.
- In `MaaMarshaller.ConvertToMaaOptionValue(IEnumerable<int>)`, consider replacing the hardcoded `4` byte width with `sizeof(int)` and documenting/centralizing the little‑endian assumption so the marshalling logic remains correct and self-explanatory if the integer size or platform endianness ever change.

## Individual Comments

### Comment 1
<location path="src/MaaFramework.Binding.UnitTests/Test_IMaaController.cs" line_range="147-149" />
<code_context>
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetLongSide, 1280)]
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetShortSide, 720)]
     [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotUseRawSize, false)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, false)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotResizeMethod, 0)]
+    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[] { 0x57, 0x41, 0x53, 0x44 })]
     public void Interface_SetOption(MaaTypes type, IMaaController maaController, ControllerOption opt, object arg)
     {
</code_context>
<issue_to_address>
**suggestion (testing):** Add more edge-case coverage for BackgroundManagedKeys and MouseLockFollow controller options.

Current tests only cover one non-empty BackgroundManagedKeys array and MouseLockFollow = false. Please also add:

- An empty BackgroundManagedKeys array to validate zero-length handling.
- A larger BackgroundManagedKeys array (e.g., 16+ entries) to exercise the IEnumerable<int> path, including TryGetNonEnumeratedCount and Unsafe.WriteUnaligned in MaaMarshaller.
- A MouseLockFollow = true case so both boolean values are covered.
- Optionally, a test checking SetOption’s return value for non-Win32 controllers when setting BackgroundManagedKeys, to document and guard behavior for unsupported controllers.

Suggested implementation:

```csharp
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetLongSide, 1280)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetShortSide, 720)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotUseRawSize, false)]
    // MouseLockFollow: cover both false and true
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, false)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, true)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotResizeMethod, 0)]
    // BackgroundManagedKeys: single non-empty array (existing), empty array, and a larger array to exercise IEnumerable<int> marshalling
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[] { 0x57, 0x41, 0x53, 0x44 })]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[0])]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[]
    {
        0x30, 0x31, 0x32, 0x33,
        0x34, 0x35, 0x36, 0x37,
        0x38, 0x39, 0x41, 0x42,
        0x43, 0x44, 0x45, 0x46
    })]
    public void Interface_SetOption(MaaTypes type, IMaaController maaController, ControllerOption opt, object arg)
    {
        Assert.IsNotNull(maaController);

```

To fully implement your optional suggestion about SetOptions return value for non-Win32 controllers, you will also want to:
1. Locate the body of `Interface_SetOption` and add an assertion that explicitly checks the return value when `opt == ControllerOption.BackgroundManagedKeys` and `type` is a non-Win32 `MaaTypes` value (e.g., `MaaTypes.Android` or similar, depending on your enum).
2. Add a `MaaData` row targeting that non-Win32 `MaaTypes` value with a representative `BackgroundManagedKeys` array and expected return value, following whatever pattern the rest of the test uses for verifying `SetOption` results.
The exact enum values and assertion shape depend on the rest of the file, so youll need to adapt those parts to your existing test infrastructure.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +147 to +149
[MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, false)]
[MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotResizeMethod, 0)]
[MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[] { 0x57, 0x41, 0x53, 0x44 })]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 为 BackgroundManagedKeys 和 MouseLockFollow 控制器选项增加更多边界情况的测试覆盖。

当前测试只覆盖了一个非空的 BackgroundManagedKeys 数组以及 MouseLockFollow = false 的情况。请同时增加:

  • 一个空的 BackgroundManagedKeys 数组,用于验证零长度的处理。
  • 一个更大的 BackgroundManagedKeys 数组(例如 16 个以上条目),以触发 IEnumerable 路径,包括 MaaMarshaller 中的 TryGetNonEnumeratedCount 和 Unsafe.WriteUnaligned。
  • 一个 MouseLockFollow = true 的用例,以覆盖两个布尔取值。
  • 可选地,增加一个测试,在为非 Win32 控制器设置 BackgroundManagedKeys 时,检查 SetOption 的返回值,以记录并保护不受支持控制器的行为。

建议实现方式:

    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetLongSide, 1280)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetShortSide, 720)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotUseRawSize, false)]
    // MouseLockFollow: cover both false and true
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, false)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, true)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotResizeMethod, 0)]
    // BackgroundManagedKeys: single non-empty array (existing), empty array, and a larger array to exercise IEnumerable<int> marshalling
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[] { 0x57, 0x41, 0x53, 0x44 })]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[0])]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[]
    {
        0x30, 0x31, 0x32, 0x33,
        0x34, 0x35, 0x36, 0x37,
        0x38, 0x39, 0x41, 0x42,
        0x43, 0x44, 0x45, 0x46
    })]
    public void Interface_SetOption(MaaTypes type, IMaaController maaController, ControllerOption opt, object arg)
    {
        Assert.IsNotNull(maaController);

为了完整实现你关于非 Win32 控制器在设置 BackgroundManagedKeys 时检查 SetOption 返回值的可选建议,你还需要:

  1. 找到 Interface_SetOption 的方法体,并添加一个断言,在 opt == ControllerOption.BackgroundManagedKeystype 为某个非 Win32 的 MaaTypes 值(例如 MaaTypes.Android 或其他,取决于你的枚举)时,显式检查返回值。
  2. 添加一个 MaaData 行,目标为该非 Win32 的 MaaTypes 值,并提供一个具有代表性的 BackgroundManagedKeys 数组以及期望的返回值,遵循该测试文件中用于验证 SetOption 结果的既有模式。
    具体使用的枚举值和断言形式需要根据文件其余部分来确定,因此你需要将这些部分适配到现有的测试基础设施中。
Original comment in English

suggestion (testing): Add more edge-case coverage for BackgroundManagedKeys and MouseLockFollow controller options.

Current tests only cover one non-empty BackgroundManagedKeys array and MouseLockFollow = false. Please also add:

  • An empty BackgroundManagedKeys array to validate zero-length handling.
  • A larger BackgroundManagedKeys array (e.g., 16+ entries) to exercise the IEnumerable path, including TryGetNonEnumeratedCount and Unsafe.WriteUnaligned in MaaMarshaller.
  • A MouseLockFollow = true case so both boolean values are covered.
  • Optionally, a test checking SetOption’s return value for non-Win32 controllers when setting BackgroundManagedKeys, to document and guard behavior for unsupported controllers.

Suggested implementation:

    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetLongSide, 1280)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotTargetShortSide, 720)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotUseRawSize, false)]
    // MouseLockFollow: cover both false and true
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, false)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.MouseLockFollow, true)]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.ScreenshotResizeMethod, 0)]
    // BackgroundManagedKeys: single non-empty array (existing), empty array, and a larger array to exercise IEnumerable<int> marshalling
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[] { 0x57, 0x41, 0x53, 0x44 })]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[0])]
    [MaaData(MaaTypes.All, nameof(Data), ControllerOption.BackgroundManagedKeys, new int[]
    {
        0x30, 0x31, 0x32, 0x33,
        0x34, 0x35, 0x36, 0x37,
        0x38, 0x39, 0x41, 0x42,
        0x43, 0x44, 0x45, 0x46
    })]
    public void Interface_SetOption(MaaTypes type, IMaaController maaController, ControllerOption opt, object arg)
    {
        Assert.IsNotNull(maaController);

To fully implement your optional suggestion about SetOption’s return value for non-Win32 controllers, you will also want to:

  1. Locate the body of Interface_SetOption and add an assertion that explicitly checks the return value when opt == ControllerOption.BackgroundManagedKeys and type is a non-Win32 MaaTypes value (e.g., MaaTypes.Android or similar, depending on your enum).
  2. Add a MaaData row targeting that non-Win32 MaaTypes value with a representative BackgroundManagedKeys array and expected return value, following whatever pattern the rest of the test uses for verifying SetOption results.
    The exact enum values and assertion shape depend on the rest of the file, so you’ll need to adapt those parts to your existing test infrastructure.

@moomiji moomiji force-pushed the copilot/fix-cs-binding-issues branch from 3705114 to d5096ca Compare July 8, 2026 08:55
@moomiji moomiji merged commit d9f6f00 into main Jul 8, 2026
4 of 5 checks passed
@moomiji moomiji changed the title Add missing C# bindings to match MaaFramework C API feat: update C API bindings to MaaFramework v5.10.5 Jul 8, 2026
@moomiji moomiji deleted the copilot/fix-cs-binding-issues branch July 8, 2026 09:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants