Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions src/MaaDebugger/maafw/__init__.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
import re
import io
from pathlib import Path
from typing import Callable, List, Optional, Tuple, Union

from asyncify import asyncify
from PIL import Image
from maa.controller import AdbController, Win32Controller
from maa.controller import AdbController, Win32Controller, CustomController
from maa.context import Context, ContextEventSink
from maa.tasker import Tasker, RecognitionDetail
from maa.resource import Resource, ResourceEventSink
from maa.toolkit import Toolkit, AdbDevice, DesktopWindow
from maa.agent_client import AgentClient
from maa.library import Library
from maa.event_sink import NotificationType
import numpy as np

from ..utils import cvmat_to_image
from ..utils.img_tools import cvmat_to_image, rgb_to_bgr


class MyCustomController(CustomController):
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

[nitpick] The MyCustomController class name doesn't follow the naming convention of the codebase. Since it's implementing a custom controller for static images, a more descriptive name like StaticImageController or UploadedImageController would better convey its purpose and be more consistent with framework naming patterns.

Suggested change
class MyCustomController(CustomController):
class StaticImageController(CustomController):

Copilot uses AI. Check for mistakes.
def __init__(self, img_bytes: bytes):
super().__init__()

img = Image.open(io.BytesIO(img_bytes))
self.ndarray = rgb_to_bgr(np.array(img))

def connect(self) -> bool:
return True

def request_uuid(self) -> str:
return "0"
Comment on lines +31 to +32
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

[nitpick] The request_uuid method returns a hardcoded "0" string. While this might be acceptable for a static image controller, consider documenting why this value is sufficient or using a more descriptive constant to clarify that UUIDs are not meaningful for static images.

Copilot uses AI. Check for mistakes.

def screencap(self) -> np.ndarray:
return self.ndarray


class MaaFW:

resource: Optional[Resource]
controller: Union[AdbController, Win32Controller, None]
controller: Union[AdbController, Win32Controller, CustomController, None]
tasker: Optional[Tasker]
agent: Optional[AgentClient]
context_event_sink: Optional[ContextEventSink]
Expand Down Expand Up @@ -89,6 +108,12 @@ def connect_win32hwnd(

return True, None

Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The connect_custom_controller method is missing the @asyncify decorator that is present in other similar connection methods (connect_adb and connect_win32hwnd). This inconsistency could lead to issues if the method is called using await like the other connection methods in the UI code.

Suggested change
@asyncify

Copilot uses AI. Check for mistakes.
def connect_custom_controller(self, img_bytes) -> Tuple[bool, Optional[str]]:
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The parameter name img_bytes lacks type annotation. Other connection methods in this class consistently use type annotations for their parameters. Add : bytes type annotation for consistency.

Suggested change
def connect_custom_controller(self, img_bytes) -> Tuple[bool, Optional[str]]:
def connect_custom_controller(self, img_bytes: bytes) -> Tuple[bool, Optional[str]]:

Copilot uses AI. Check for mistakes.
self.controller = MyCustomController(img_bytes)
self.controller.post_connection().wait()

Comment on lines +113 to +114
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The connect_custom_controller method doesn't check the connection result like connect_adb and connect_win32hwnd do. Those methods verify post_connection().wait().succeeded and return failure status if unsuccessful. This method should similarly validate the connection and handle failure cases.

Suggested change
self.controller.post_connection().wait()
connected = self.controller.post_connection().wait().succeeded
if not connected:
return False, "Failed to connect custom controller"

Copilot uses AI. Check for mistakes.
return True, None

@asyncify
def load_resource(self, dir: List[Path]) -> Tuple[bool, Optional[str]]:
if not self.resource:
Expand Down Expand Up @@ -149,7 +174,20 @@ def run_task(
if not AgentClient().register_sink(self.resource, self.controller, self.tasker):
return False, "Failed to register Agent sink."

return self.tasker.post_task(entry, pipeline_override).wait().succeeded, None
if isinstance(self.controller, CustomController):
# disable action
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

[nitpick] The comment "disable action" is too brief. Consider expanding it to explain why actions are disabled for custom controllers (e.g., "Disable actions for custom controllers as they use static images and cannot perform clicks or other interactive operations").

Suggested change
# disable action
# Disable actions for custom controllers as they use static images
# and cannot perform clicks or other interactive operations.

Copilot uses AI. Check for mistakes.
pipeline_override.update(
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

This expression mutates a default value.

Copilot uses AI. Check for mistakes.
{entry: {"action": {"type": "DoNothing"}, "next": []}}
)
return (
self.tasker.post_task(entry, pipeline_override).wait().succeeded,
None,
)
else:
return (
self.tasker.post_task(entry, pipeline_override).wait().succeeded,
None,
)
Comment on lines +182 to +190
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

There's code duplication in the if-else branches. Both branches return identical values (self.tasker.post_task(entry, pipeline_override).wait().succeeded, None). The logic can be simplified by updating pipeline_override conditionally before a single post_task call:

if isinstance(self.controller, CustomController):
    # disable action
    pipeline_override.update(
        {entry: {"action": {"type": "DoNothing"}, "next": []}}
    )
return (
    self.tasker.post_task(entry, pipeline_override).wait().succeeded,
    None,
)
Suggested change
return (
self.tasker.post_task(entry, pipeline_override).wait().succeeded,
None,
)
else:
return (
self.tasker.post_task(entry, pipeline_override).wait().succeeded,
None,
)
return (
self.tasker.post_task(entry, pipeline_override).wait().succeeded,
None,
)

Copilot uses AI. Check for mistakes.

@asyncify
def stop_task(self) -> None:
Expand All @@ -176,6 +214,9 @@ def click(self, x, y) -> bool:
if not self.controller:
return False

if isinstance(self.controller, CustomController):
return False

return self.controller.post_click(x, y).wait().succeeded

@asyncify
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ def cvmat_to_image(cvmat: ndarray) -> Image.Image:
pil = Image.fromarray(cvmat)
b, g, r = pil.split()
return Image.merge("RGB", (r, g, b))


def rgb_to_bgr(arr: ndarray) -> ndarray:
"""RGB -> BGR 转换"""
if arr.ndim == 3 and arr.shape[2] >= 3:
return arr[:, :, ::-1].copy()
else:
return arr
23 changes: 23 additions & 0 deletions src/MaaDebugger/webpage/index_page/master_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def connect_control():
with ui.tabs() as tabs:
adb = ui.tab("Adb")
win32 = ui.tab("Win32")
custom = ui.tab("Custom")

tab_panels = ui.tab_panels(tabs, value="Adb").bind_value(STORAGE, "controller_type")
with tab_panels:
Expand All @@ -49,6 +50,10 @@ def connect_control():
with ui.row(align_items="center").classes("w-full"):
connect_win32_control()

with ui.tab_panel(custom):
with ui.row(align_items="center").classes("w-full"):
connect_custom_control()

os_type = system.get_os_type()
if os_type != system.OSTypeEnum.Windows:
win32.disable()
Expand Down Expand Up @@ -301,6 +306,24 @@ def on_change_hwnd_select(value: Optional[str]):
hwnd_input.value = value


def connect_custom_control():
def on_upload(e):
GlobalStatus.ctrl_connecting = Status.RUNNING
try:
maafw.connect_custom_controller(e.content.read())
except Exception as e:
GlobalStatus.ctrl_connecting = Status.FAILED
ui.notify(
f"Failed to load image. {e}", position="bottom-right", type="negative"
Comment on lines +314 to +317
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

Variable name e shadows the upload event parameter from the outer scope. The exception should be given a different name to avoid confusion and improve code clarity. Consider using ex or err for the exception variable.

Suggested change
except Exception as e:
GlobalStatus.ctrl_connecting = Status.FAILED
ui.notify(
f"Failed to load image. {e}", position="bottom-right", type="negative"
except Exception as ex:
GlobalStatus.ctrl_connecting = Status.FAILED
ui.notify(
f"Failed to load image. {ex}", position="bottom-right", type="negative"

Copilot uses AI. Check for mistakes.
)
Comment on lines +316 to +318
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The error message is too generic. When image loading fails, users would benefit from more specific information about what went wrong (e.g., "unsupported image format", "corrupted image file", "image too large"). Consider catching specific exceptions (like PIL.UnidentifiedImageError) and providing tailored error messages.

Copilot uses AI. Check for mistakes.
return

GlobalStatus.ctrl_connecting = Status.SUCCEEDED

StatusIndicator(GlobalStatus, "ctrl_connecting")
ui.upload(auto_upload=True, on_upload=lambda e: on_upload(e))
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

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

The ui.upload component should specify accepted file types to prevent users from uploading non-image files. Consider adding the accept parameter with image MIME types, e.g., ui.upload(auto_upload=True, on_upload=lambda e: on_upload(e), accept="image/*").

Suggested change
ui.upload(auto_upload=True, on_upload=lambda e: on_upload(e))
ui.upload(auto_upload=True, on_upload=lambda e: on_upload(e), accept="image/*")

Copilot uses AI. Check for mistakes.


def screenshot_control():
with (
ui.row()
Expand Down
2 changes: 1 addition & 1 deletion src/MaaDebugger/webpage/reco_page/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from nicegui import ui

from ...utils import cvmat_to_image
from ...utils.img_tools import cvmat_to_image
from ...maafw import maafw, RecognitionDetail


Expand Down