feat: add plugin runtime and cross-device task orchestration

This commit is contained in:
chenmingxuan committed 2026-10-14 20:21:44 +08:00
1 parent 3ee4a5f7a8
commit 6663233ab8
82 files changed
+4147 -124

No files matched your search

+58 -4
View File
@@ -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):