Skip to main content

python-action-guide

MaaNTE Python 自定义动作(CustomAction)编写指南。覆盖 agent/custom/action/ 下的 Python 代码的架构、注册、命名、maafocus 用户消息、日志、Controller API、Pipeline 集成、错误处理等编码规范和模式参考。在编写、修改或审查 Python 自定义动作,或需要了解 agent 项目结构与 MaaFramework Python 绑定集成方式时使用。

Source facts

Repository
1bananachicken/MaaNTE
Last source activity
May 30, 2026 at 09:00
Detected SKILL.md language
Chinese
Stars
2,837
Forks
209

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
python-action-guide
description
MaaNTE Python 自定义动作(CustomAction)编写指南。覆盖 agent/custom/action/ 下的 Python 代码的架构、注册、命名、maafocus 用户消息、日志、Controller API、Pipeline 集成、错误处理等编码规范和模式参考。在编写、修改或审查 Python 自定义动作,或需要了解 agent 项目结构与 MaaFramework Python 绑定集成方式时使用。
# MaaNTE Python CustomAction 编写指南 ## 架构定位 Python CustomAction 处理 Pipeline JSON 无法覆盖的复杂逻辑(图像算法、状态机、音频检测、MIDI 播放等)。**流程控制由 Pipeline 负责,Python 只处理难点。** 一句话:**Pipeline 管流程,Python 管难点。** 没有必要的 Python 逻辑会大大增加代码复杂度,造成下一位开发者开发调试极其困难。 所有坐标与图像以 **720p (1280×720)** 为基准。 ## 目录结构 ``` agent/ ├── main.py # 入口:venv、依赖安装、AgentServer 启动 ├── custom/ │ ├── __init__.py # from .action import * │ └── action/ │ ├── __init__.py # 所有 CustomAction 的 import 与 __all__ 注册 │ ├── Common/ # 通用工具(click、alt_click、logger、utils) │ │ ├── __init__.py │ │ ├── click.py │ │ ├── alt_click.py │ │ ├── logger.py │ │ └── utils.py # get_image、click_rect、match_template_in_region │ ├── AutoFish/ # 钓鱼业务(按功能聚合) │ ├── Tetris/ # 俄罗斯方块业务 │ │ ├── feats/ # 功能实现 │ │ └── utils/ # 工具类(棋盘、场景检测) │ ├── rhythm/ # 节奏任务 │ │ ├── feats/ │ │ └── utils/ │ ├── SoundTrigger/ # 音频触发业务 │ ├── Movement/ # 移动控制 │ ├── auto_piano/ # 自动钢琴 │ ├── pinkpaw/ # 粉爪大劫案 │ ├── DatasetCollection/ # 数据集采集 │ ├── auto_make_coffee.py # 单文件简单动作 │ ├── auto_tetris.py # 俄罗斯方块入口 │ ├── auto_f_scroll.py # 自动滚屏 │ ├── furniture_claim.py # 家具收取 │ ├── realtime_task.py # 实时任务调度 │ ├── withdraw_money_choose_item.py # 补货取钱物品选择 │ └── __init__.py # 所有 CustomAction 的导入与注册 └── utils/ ├── __init__.py # 子模块聚合导出 ├── logger.py # 日志系统(loguru/logging) ├── pienv.py # PI 环境变量 ├── maafocus.py # 用户可见消息(Pipeline focus 协议) ├── i18n.py # 多语言/翻译 ├── screen.py # 屏幕缩放/分辨率 ├── time.py # 时间工具 └── win32_process.py # Win32 窗口查找 ``` ## 注册机制 ### 装饰器注册 每个 CustomAction 类使用 `@AgentServer.custom_action("name")` 注册,名称必须与 Pipeline JSON 中 `custom_action` 的值一致: ```python from maa.agent.agent_server import AgentServer from maa.custom_action import CustomAction from maa.context import Context @AgentServer.custom_action("auto_make_coffee") class AutoMakeCoffee(CustomAction): def run( self, context: Context, argv: CustomAction.RunArg ) -> CustomAction.RunResult: ... ``` Pipeline JSON 中对应: ```jsonc { "action": { "type": "Custom", "param": { "custom_action": "auto_make_coffee", "custom_action_param": { "count": 10 } } } } ``` ### `__init__.py` 注册 所有 CustomAction 必须在 `agent/custom/action/__init__.py` 中导入并加入 `__all__`: ```python from .auto_make_coffee import * from .Tetris import * __all__ = [ "AutoMakeCoffee", "AutoTetris", "TetrisResetContext", "TetrisCheckVitalityAction", ] ``` 遗漏导入 = 动作不生效。 ## 命名 - **类名**:PascalCase,语义化。如 `AutoMakeCoffee`、`TetrisGamePlayer`、`SoundDodgeAction`。 - **注册名**:snake_case,与 Pipeline JSON 一致。如 `"auto_make_coffee"`、`"auto_tetris"`。 - **文件名**:snake_case。单文件简单动作直接用 `.py` 文件;复杂模块用目录 + `__init__.py`。 - **内部函数/变量**:snake_case,模块内部使用的加前导下划线 `_`。 ## Common 内置动作 以下 Custom Action 位于 `agent/custom/action/Common/`,可在 Pipeline 中直接使用,无需额外编写 Python 代码。 ### click_override 自定义点击。通过 `custom_action_param` 指定目标 rect,或使用当前识别结果的 box。 - 注册名:`click_override` - 参数 `custom_action_param`:`{ "target": [x, y, w, h] }` - 若未提供 `custom_action_param`,则使用 `argv.box`(识别结果 box) ```jsonc { "action": { "type": "Custom", "param": { "custom_action": "click_override", "custom_action_param": { "target": [100, 200, 50, 50] } } } } ``` ### alt_click Alt + 点击。先按下 Alt 键,再点击识别结果 box 位置,最后松开 Alt。 - 注册名:`alt_click` - 无需额外参数,点击位置由识别结果的 `box` 决定 ```jsonc { "recognition": { "type": "TemplateMatch", "param": { "template": "xxx.png" } }, "action": { "type": "Custom", "param": { "custom_action": "alt_click" } } } ``` ## Common 工具函数 `agent/custom/action/Common/utils.py` 提供常用辅助函数: | 函数 | 说明 | |------|------| | `get_image(controller)` | 截图,返回 numpy array | | `click_rect(controller, rect, delay)` | 点击指定 rect 的中心 | | `match_template_in_region(img, region, template, min_similarity, green_mask)` | 在区域内做模板匹配,返回 `(hit, score, x, y)` | ```python from Common.utils import get_image, click_rect, match_template_in_region img = get_image(controller) hit, score, x, y = match_template_in_region(img, [0, 0, 1280, 720], template, 0.8) if hit: click_rect(controller, [x, y, 50, 50]) ``` ## CustomAction 模板 ### 简单动作 ```python import json from maa.agent.agent_server import AgentServer from maa.custom_action import CustomAction from maa.context import Context from utils.logger import logger from utils.maafocus import PrintT @AgentServer.custom_action("my_action") class MyAction(CustomAction): def run( self, context: Context, argv: CustomAction.RunArg ) -> CustomAction.RunResult: PrintT(context, "my_action.started") params = {} if argv.custom_action_param: try: params = json.loads(argv.custom_action_param) except (json.JSONDecodeError, TypeError): pass controller = context.tasker.controller # 截图 controller.post_screencap().wait() img = controller.cached_image # 检查停止信号 if context.tasker.stopping: return CustomAction.RunResult(success=False) # 业务逻辑 ... logger.debug("识别结果: score=%.2f", score) return CustomAction.RunResult(success=True) ``` ### 带状态管理的动作 对于需要跨多次调用保持状态的场景(如连打计数),使用模块级全局变量,并通过独立的 reset action 重置: ```python _round_count = 0 _task_config = {} @AgentServer.custom_action("my_reset") class MyResetContext(CustomAction): def run(self, context, argv) -> CustomAction.RunResult: global _round_count, _task_config _round_count = 0 _task_config = {} return CustomAction.RunResult(success=True) @AgentServer.custom_action("my_action") class MyAction(CustomAction): def run(self, context, argv) -> CustomAction.RunResult: global _round_count _round_count += 1 ... ``` ## Controller API 通过 `context.tasker.controller` 访问,常用操作: ```python controller = context.tasker.controller # 截图 controller.post_screencap().wait() img = controller.cached_image # numpy array (BGR or BGRA) # 点击(绝对坐标,720p 基准) controller.post_touch_down(x, y).wait() time.sleep(0.005) # 短暂按下 controller.post_touch_up().wait() # 按键(虚拟键码) controller.post_key_down(0x46).wait() # F 键 time.sleep(0.1) controller.post_key_up(0x46).wait() # 组合键:先 Alt 再点 controller.post_key_down(0x12).wait() # VK_MENU (Alt) # ... 执行点击 ... controller.post_key_up(0x12).wait() ``` 常用虚拟键码: | 键 | 码 | |----|----| | Esc | 27 | | F | 70 | | E | 69 | | Q | 81 | | D | 0x44 | | F | 0x46 | | J | 0x4A | | K | 0x4B | ## Pipeline 集成 ### context.run_recognition 调用已定义的 Pipeline 识别节点: ```python # 基本调用 result = context.run_recognition("FurnitureOcrRec", image) # 带参数覆盖(动态 expected 文本、自定义 roi 等) result = context.run_recognition( "FurnitureOcrRec", image, pipeline_override={ "FurnitureOcrRec": { "recognition": {"param": {"expected": "仓鼠球"}} } }, ) if result and result.box: x, y, w, h = result.box.x, result.box.y, result.box.w, result.box.h ``` ### RecognitionDetail 结构 `context.run_recognition()` 返回 `RecognitionDetail`(`maa.define.RecognitionDetail`),结构如下: ```python @dataclass class RecognitionDetail: reco_id: int name: str algorithm: Union[AlgorithmEnum, str] hit: bool # 是否有命中 box: Optional[Rect] # 最佳命中的 box all_results: List[RecognitionResult] # 全部原始结果 filtered_results: List[RecognitionResult] # 过滤后的结果(常用) best_result: Optional[RecognitionResult] # 最佳结果 raw_detail: Dict raw_image: numpy.ndarray draw_images: List[numpy.ndarray] ``` 关键方法/属性: ```python result = context.run_recognition("MyNode", image) # 是否存在命中 if result and result.hit: # 遍历所有命中(正确的多结果方式,不是 .all) for r in result.filtered_results: # r.box — Rect(x, y, w, h) # 根据算法类型,r 还有不同属性: # OCRResult: r.text (str), r.score (float) # ColorMatchResult: r.count (int) # TemplateMatchResult: r.score (float) # result.box — 最佳命中的 Rect(快捷方式) # result.box.x, result.box.y, result.box.w, result.box.h 都是 int ``` ### 多结果模式 **点击所有命中 rect(如 ColorMatch connected 组件、OCR 多匹配):** ```python def _filtered_boxes(result): """返回 filtered_results 中所有命中的 Rect 列表""" if result is None or not result.hit: return [] return [r.box for r in result.filtered_results if r.box is not None] result = context.run_recognition("SomeColorMatch", image) for box in _filtered_boxes(result): cx, cy = box.x + box.w // 2, box.y + box.h // 2 controller.post_touch_down(cx, cy).wait() time.sleep(0.001) controller.post_touch_up().wait() ``` **遍历 OCR 多结果并提取文本:** ```python result = context.run_recognition("SomeOCRNode", image) for r in result.filtered_results if result else []: text = getattr(r, "text", "") # OCRResult 有 .text,其他类型无 box = r.box # Rect # 处理 text 和 box ... ``` **常见错误:** - ~~`result.all`~~ → 不存在,应该用 `result.filtered_results` - ~~`box.text`~~ → `Rect` 没有 `.text`,应该用 `getattr(r, "text", "")` 在结果对象上取 - ~~`ColorMatchResult.box.text`~~ → ColorMatchResult 只有 `.box`(Rect)和 `.count`(int),无 `.text` **`box` 类型注意**:MaaFramework Python 绑定返回的 `.box` 实际是 `list [x,y,w,h]` 而非 `Rect` 对象(虽然类型标注为 `Rect`),转换时需兼容: ```python def _box_to_rect(box): if isinstance(box, (list, tuple)): return list(box) return [box.x, box.y, box.w, box.h] ``` **真实 `raw_detail` 结构参考(ColorMatch):** ```json { "reco_id": 400000008, "algorithm": "ColorMatch", "box": [256, 384, 19, 18], "detail": { "all": [ {"box": [256, 384, 19, 18], "count": 185}, {"box": [256, 531, 19, 17], "count": 182} ], "best": {"box": [256, 384, 19, 18], "count": 185}, "filtered": [ {"box": [256, 384, 19, 18], "count": 185}, {"box": [256, 531, 19, 17], "count": 182} ] } } ``` - `result.filtered_results[i].box` → `list [x,y,w,h]` - `result.filtered_results[i].count` → `int`(ColorMatch)/ `result.filtered_results[i].text` → `str`(OCR) - `result.raw_detail` 包含完整 JSON-serializable dict,可作为兜底访问 ### context.run_recognition_direct 在 Python 中直接调用 MaaFramework 识别算法(无需 Pipeline 节点定义): ```python from maa.pipeline import JRecognitionType, JOCR detail = context.run_recognition_direct( JRecognitionType.OCR, JOCR(roi=[x, y, w, h]), frame
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub