feat: add multi-device concurrency, server lifecycle CLI, and unified execution stream
- Implement DevicePool and DeviceExecutionLock concurrency modes (per-device FIFO vs global serialization) - Extend ArtemisClient SDK and CLI with explicit device targeting and concurrency strategy configuration - Add server lifecycle management (artemis restart, stop, status) with cross-platform PID discovery - Update MCP server tools to support multi-device dispatch, trace tagging, and per-device execution - Enhance Showcase UI with step-by-step visual replay, playback speed controls, and device serial badges - Add comprehensive unit test coverage and edge-case verification scripts
This commit is contained in:
1 parent
a36a66ce6d
commit
c2c90a6378
84 files changed
+5612
-645
No files matched your search
@@ -334,12 +334,12 @@ PRESET_TASK_CATALOG: list[TaskPreset] = [
|
||||
required_packages=["com.google.android.apps.photos"],
|
||||
priority=82,
|
||||
),
|
||||
# 9. WeChat (微信)
|
||||
# 9. WeChat
|
||||
TaskPreset(
|
||||
id="wechat_browse",
|
||||
title="Check WeChat Messages",
|
||||
description="Open WeChat and view top recent chat conversations",
|
||||
goal="Open WeChat (微信) and view the top recent chat messages.",
|
||||
goal="Open WeChat and view the top recent chat messages.",
|
||||
profile="flash",
|
||||
category="flash",
|
||||
tag="WeChat",
|
||||
@@ -351,7 +351,7 @@ PRESET_TASK_CATALOG: list[TaskPreset] = [
|
||||
id="pro_wechat_to_calendar",
|
||||
title="WeChat Notice to Calendar",
|
||||
description="Extract meeting notice from WeChat chat and add to Calendar",
|
||||
goal="Open WeChat (微信), locate the latest meeting announcement or event message in the top chat, extract the time and topic, then open Calendar and schedule an event.",
|
||||
goal="Open WeChat, locate the latest meeting announcement or event message in the top chat, extract the time and topic, then open Calendar and schedule an event.",
|
||||
profile="pro",
|
||||
category="cross_app",
|
||||
tag="WeChat + Calendar",
|
||||
@@ -367,12 +367,12 @@ PRESET_TASK_CATALOG: list[TaskPreset] = [
|
||||
required_packages=["com.tencent.mm"],
|
||||
priority=93,
|
||||
),
|
||||
# 10. Xiaohongshu (小红书)
|
||||
# 10. Xiaohongshu
|
||||
TaskPreset(
|
||||
id="xhs_coffee_guide",
|
||||
title="RED Cafe Guide Search",
|
||||
description="Search trending specialty cafe reviews on Xiaohongshu",
|
||||
goal="Open Xiaohongshu (小红书), search for top-rated specialty coffee shops, and view the top post.",
|
||||
goal="Open Xiaohongshu, search for top-rated specialty coffee shops, and view the top post.",
|
||||
profile="flash",
|
||||
category="flash",
|
||||
tag="Xiaohongshu",
|
||||
@@ -384,12 +384,12 @@ PRESET_TASK_CATALOG: list[TaskPreset] = [
|
||||
required_packages=["com.xingin.xhs"],
|
||||
priority=87,
|
||||
),
|
||||
# 11. Meituan / Dianping (美团 / 大众点评)
|
||||
# 11. Meituan / Dianping
|
||||
TaskPreset(
|
||||
id="meituan_ramen_search",
|
||||
title="Meituan Food Search",
|
||||
description="Search top-rated Ramen nearby on Meituan or Dianping",
|
||||
goal="Open Meituan (美团) or Dianping (大众点评), search for top-rated Ramen nearby, and view top restaurant rating.",
|
||||
goal="Open Meituan or Dianping, search for top-rated Ramen nearby, and view top restaurant rating.",
|
||||
profile="flash",
|
||||
category="flash",
|
||||
tag="Meituan",
|
||||
@@ -401,12 +401,12 @@ PRESET_TASK_CATALOG: list[TaskPreset] = [
|
||||
required_packages=["com.sankuai.meituan", "com.dianping.v1"],
|
||||
priority=86,
|
||||
),
|
||||
# 12. Bilibili (哔哩哔哩)
|
||||
# 12. Bilibili
|
||||
TaskPreset(
|
||||
id="bilibili_stream",
|
||||
title="Bilibili Tech Video",
|
||||
description="Search and play an AI Agent tutorial video on Bilibili",
|
||||
goal='Open Bilibili (哔哩哔哩), search for "AI Agent 架构实战", and play the top matching video.',
|
||||
goal='Open Bilibili, search for "AI Agent Architecture", and play the top matching video.',
|
||||
profile="flash",
|
||||
category="flash",
|
||||
tag="Bilibili",
|
||||
|
||||
@@ -241,6 +241,11 @@ class TaskQueueService:
|
||||
async def queue_worker(cls):
|
||||
"""Persistent, self-healing background worker loop processing tasks in FIFO order."""
|
||||
print("[QueueWorker] Background worker loop initialized and running.")
|
||||
try:
|
||||
DeviceExecutionLock.cleanup_stale_locks()
|
||||
except Exception as exc:
|
||||
print(f"[QueueWorker] Initial stale lock cleanup notice: {exc}")
|
||||
|
||||
while True:
|
||||
task_item = None
|
||||
sess_id = None
|
||||
@@ -289,6 +294,7 @@ class TaskQueueService:
|
||||
"session_id": sess_id,
|
||||
"initial_goal": goal,
|
||||
"profile": profile,
|
||||
"device_serial": task_item.get("device_serial"),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -322,6 +328,8 @@ class TaskQueueService:
|
||||
"--test-name",
|
||||
test_name,
|
||||
]
|
||||
if sess_id:
|
||||
cmd.extend(["--session-id", str(sess_id)])
|
||||
if expected_output:
|
||||
cmd.extend(["--output-description", str(expected_output)])
|
||||
if enable_outputter is not None:
|
||||
@@ -330,9 +338,13 @@ class TaskQueueService:
|
||||
cmd.extend(["--locked-app", str(locked_app)])
|
||||
if app_path:
|
||||
cmd.extend(["--app-path", str(app_path)])
|
||||
device_serial = task_item.get("device_serial")
|
||||
if device_serial:
|
||||
cmd.extend(["--device-serial", str(device_serial)])
|
||||
env["ADB_DEVICE_SERIAL"] = str(device_serial)
|
||||
|
||||
print(
|
||||
f"[QueueWorker] Starting task [{sess_id}]: '{goal}' (profile: {profile}, outputter: {bool(expected_output or enable_outputter)})"
|
||||
f"[QueueWorker] Starting task [{sess_id}]: '{goal}' (profile: {profile}, device: {device_serial or 'auto'}, outputter: {bool(expected_output or enable_outputter)})"
|
||||
)
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
@@ -349,6 +361,7 @@ class TaskQueueService:
|
||||
str(queue_ticket),
|
||||
proc.pid,
|
||||
description=f"frontend task: {goal[:120]}",
|
||||
device_id=device_serial or "pending",
|
||||
session_id=str(sess_id) if sess_id else None,
|
||||
ingress="frontend",
|
||||
)
|
||||
@@ -453,6 +466,7 @@ class TaskQueueService:
|
||||
enable_outputter: bool | None = None,
|
||||
locked_app_package: str | None = None,
|
||||
app_path: str | None = None,
|
||||
device_serial: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Enqueues one or more goals and wakes up the background worker."""
|
||||
cls.ensure_worker_running()
|
||||
@@ -461,8 +475,18 @@ class TaskQueueService:
|
||||
now = time.time()
|
||||
for i, goal in enumerate(goals):
|
||||
sess_id = str(uuid.uuid4())
|
||||
assigned_serial = device_serial
|
||||
if not assigned_serial:
|
||||
try:
|
||||
from artemis.runtime import device_pool
|
||||
|
||||
assigned_serial = device_pool.select_device()
|
||||
except Exception:
|
||||
assigned_serial = None
|
||||
|
||||
queue_ticket = DeviceExecutionLock.reserve(
|
||||
description=f"frontend task: {goal[:120]}",
|
||||
device_id=assigned_serial or "pending",
|
||||
session_id=sess_id,
|
||||
ingress="frontend",
|
||||
)
|
||||
@@ -474,6 +498,7 @@ class TaskQueueService:
|
||||
"enable_outputter": enable_outputter,
|
||||
"locked_app_package": locked_app_package,
|
||||
"app_path": app_path,
|
||||
"device_serial": assigned_serial,
|
||||
"status": "pending",
|
||||
"queue_ticket": queue_ticket,
|
||||
"created_at": now + i * 0.001,
|
||||
|
||||
Reference in new issue
Block a user