- ADB client (connect, tap, swipe, screencap, app control) - Vision module (in-memory screenshot + OpenCV template matching) - Action loader (auto-discovers .py files in actions/) - Engine (screenshot → detect → act loop with YAML playbook) - 12 built-in actions: tap, click, wait, swipe, find, find_and_click, wait_until, click_seq, keyevent, app, log - Example custom action template - Debug screenshot tool for template creation - Detailed plan.md with 20 actions across 9 phases - Sample playbook with 10 steps
80 lines
2.6 KiB
Python
80 lines
2.6 KiB
Python
"""
|
|
Action Loader — dynamically discovers and loads action modules
|
|
from the actions/ directory. Each .py file is a self-contained action.
|
|
"""
|
|
import importlib.util
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
logger = logging.getLogger("sos.actions")
|
|
|
|
|
|
class ActionRegistry:
|
|
"""Holds all registered actions by name."""
|
|
|
|
def __init__(self):
|
|
self._actions = {}
|
|
|
|
def register(self, action):
|
|
"""Register an action instance."""
|
|
self._actions[action.name] = action
|
|
|
|
def get(self, name: str):
|
|
"""Get an action by name, or None."""
|
|
return self._actions.get(name)
|
|
|
|
def list(self):
|
|
"""Return all registered action names."""
|
|
return list(self._actions.keys())
|
|
|
|
def run(self, name: str, ctx: dict, params: dict):
|
|
"""Run an action by name. Raises KeyError if not found."""
|
|
action = self.get(name)
|
|
if action is None:
|
|
raise KeyError(f"Unknown action: '{name}' (available: {self.list()})")
|
|
return action.run(ctx, params)
|
|
|
|
|
|
def load_actions(actions_dir: str) -> ActionRegistry:
|
|
"""
|
|
Scan actions_dir for .py files (except __init__.py) and load each.
|
|
Each module must export a class named 'Action' with:
|
|
- name: str (the playbook-visible name)
|
|
- def run(self, ctx: dict, params: dict) -> any
|
|
Returns an ActionRegistry with all loaded actions.
|
|
"""
|
|
registry = ActionRegistry()
|
|
if not os.path.isdir(actions_dir):
|
|
logger.warning("Actions directory not found: %s", actions_dir)
|
|
return registry
|
|
|
|
sys.path.insert(0, os.path.dirname(actions_dir))
|
|
|
|
for fname in sorted(os.listdir(actions_dir)):
|
|
if not fname.endswith(".py") or fname == "__init__.py":
|
|
continue
|
|
mod_path = os.path.join(actions_dir, fname)
|
|
mod_name = f"sos_actions_{fname[:-3]}"
|
|
|
|
spec = importlib.util.spec_from_file_location(mod_name, mod_path)
|
|
if spec is None or spec.loader is None:
|
|
logger.warning("Cannot load action module: %s", fname)
|
|
continue
|
|
|
|
try:
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
action_class = getattr(mod, "Action", None)
|
|
if action_class is None:
|
|
logger.warning("Action %s has no 'Action' class — skipping", fname)
|
|
continue
|
|
instance = action_class()
|
|
registry.register(instance)
|
|
logger.info("Loaded action: %s (%s)", instance.name, fname)
|
|
except Exception as e:
|
|
logger.warning("Failed to load action %s: %s", fname, e)
|
|
|
|
sys.path.pop(0)
|
|
return registry
|