feat: add plugin runtime and cross-device task orchestration
This commit is contained in:
1 parent
3ee4a5f7a8
commit
6663233ab8
82 files changed
+4147
-124
No files matched your search
@@ -33,6 +33,8 @@ import signal
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
import typer
|
||||
from pydantic import TypeAdapter
|
||||
from artemis.plugins.models import ExecutionTarget, PluginSelection
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -59,6 +61,8 @@ async def execute_task(
|
||||
explorer_flash_mode: str | None = None,
|
||||
explorer_pro_mode: str | None = None,
|
||||
verification_level: str | None = None,
|
||||
execution_target: str | None = None,
|
||||
plugins: list[PluginSelection] | None = None,
|
||||
) -> None:
|
||||
"""Executes a single mobile automation task end-to-end.
|
||||
|
||||
@@ -138,7 +142,12 @@ async def execute_task(
|
||||
target_serial = (
|
||||
device_serial or settings.ADB_DEVICE_SERIAL or os.environ.get("ADB_DEVICE_SERIAL")
|
||||
)
|
||||
if not target_serial:
|
||||
execution_target = TypeAdapter(ExecutionTarget).validate_python(
|
||||
execution_target or os.getenv("ARTEMIS_EXECUTION_TARGET", "android")
|
||||
)
|
||||
if execution_target == "computer":
|
||||
target_serial = None
|
||||
if not target_serial and execution_target != "computer":
|
||||
try:
|
||||
from artemis.runtime import device_pool
|
||||
|
||||
@@ -158,6 +167,7 @@ async def execute_task(
|
||||
try:
|
||||
agent = Agent(config=config.build(), session_id=effective_sid)
|
||||
await agent.init(
|
||||
execution_target=execution_target,
|
||||
retry_count=int(os.getenv("ARTEMIS_HEALTH_RETRIES", 5)),
|
||||
retry_wait_seconds=int(os.getenv("ARTEMIS_HEALTH_DELAY", 2)),
|
||||
)
|
||||
@@ -179,7 +189,14 @@ async def execute_task(
|
||||
if llm_result_path:
|
||||
task.with_llm_output_saving(path=llm_result_path)
|
||||
|
||||
await agent.run_task(request=task.build())
|
||||
request = task.build()
|
||||
request.execution_target = execution_target
|
||||
plugin_choices = os.getenv("ARTEMIS_TASK_PLUGINS")
|
||||
if plugins is not None:
|
||||
request.plugins = plugins
|
||||
elif plugin_choices is not None:
|
||||
request.plugins = TypeAdapter(list[PluginSelection]).validate_json(plugin_choices)
|
||||
await agent.run_task(request=request)
|
||||
finally:
|
||||
if agent is not None:
|
||||
await agent.clean()
|
||||
@@ -330,6 +347,17 @@ def run_command(
|
||||
help="Target specific Android device by serial number (e.g. emulator-5554).",
|
||||
),
|
||||
] = None,
|
||||
execution_target: Annotated[
|
||||
str | None,
|
||||
typer.Option("--target", help="执行目标:android、computer 或 mixed。电脑任务无需 ADB。 "),
|
||||
] = None,
|
||||
plugins_json: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--plugins-json",
|
||||
help='插件配置引用 JSON,如 [{"id":"open-computer-use","profile":"default"}];不能提供密钥。',
|
||||
),
|
||||
] = None,
|
||||
session_id: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
@@ -350,6 +378,25 @@ def run_command(
|
||||
check_ffmpeg_available()
|
||||
|
||||
console = Console()
|
||||
try:
|
||||
target = TypeAdapter(ExecutionTarget).validate_python(
|
||||
execution_target or os.getenv("ARTEMIS_EXECUTION_TARGET", "android")
|
||||
)
|
||||
choices_json = (
|
||||
plugins_json if plugins_json is not None else os.getenv("ARTEMIS_TASK_PLUGINS")
|
||||
)
|
||||
choices = (
|
||||
TypeAdapter(list[PluginSelection]).validate_json(choices_json)
|
||||
if choices_json is not None
|
||||
else None
|
||||
)
|
||||
except ValueError:
|
||||
raise typer.BadParameter(
|
||||
"执行目标或插件配置引用 JSON 无效;只能引用已保存的插件配置。"
|
||||
) from None
|
||||
overrides = {"execution_target": target}
|
||||
if choices is not None:
|
||||
overrides["plugins"] = [p.model_dump() for p in choices]
|
||||
|
||||
is_worker = (
|
||||
os.environ.get("ARTEMIS_TASK_WORKER") == "1"
|
||||
@@ -386,6 +433,7 @@ def run_command(
|
||||
session_id=target_sid,
|
||||
ingress="cli",
|
||||
base_url=base_url,
|
||||
runtime_overrides=overrides,
|
||||
)
|
||||
if resp and resp.get("tasks"):
|
||||
console.print(
|
||||
@@ -440,9 +488,12 @@ def run_command(
|
||||
f"[yellow]Daemon routing notice: {exc}. Falling back to local execution...[/yellow]"
|
||||
)
|
||||
|
||||
if not is_worker and not is_standalone and (target != "android" or choices):
|
||||
raise typer.BadParameter("插件任务需要统一队列服务,请检查服务后重试;未自动重复提交。")
|
||||
|
||||
adb_client = None
|
||||
try:
|
||||
if which("adb"):
|
||||
if target != "computer" and which("adb"):
|
||||
adb_client = AdbClient(
|
||||
host=settings.ADB_HOST or "localhost",
|
||||
port=settings.ADB_PORT or 5037,
|
||||
@@ -451,7 +502,8 @@ def run_command(
|
||||
# Optional cosmetic device-status display; run continues without it.
|
||||
logger.debug(f"Could not create ADB client for device status display: {exc}")
|
||||
|
||||
display_device_status(console, adb_client=adb_client)
|
||||
if target != "computer":
|
||||
display_device_status(console, adb_client=adb_client)
|
||||
|
||||
cancelled = False
|
||||
original_sigterm = None
|
||||
@@ -488,6 +540,8 @@ def run_command(
|
||||
explorer_flash_mode=explorer_flash_mode,
|
||||
explorer_pro_mode=explorer_pro_mode,
|
||||
verification_level=verification_level,
|
||||
execution_target=target,
|
||||
plugins=choices,
|
||||
)
|
||||
)
|
||||
except (KeyboardInterrupt, asyncio.CancelledError):
|
||||
|
||||
Reference in new issue
Block a user