feat: add guided exploration and share user-started MCP backend

This commit is contained in:
chenmingxuan committed 2026-10-16 18:32:46 +08:00
1 parent f86db1ced6
commit ee256ad7ce
144 files changed
+8625 -2075

No files matched your search

+2
View File
@@ -19,6 +19,7 @@ from mcp_server.tools.diagnose import mobile_diagnose
from mcp_server.tools.inspect_trace import mobile_inspect_trace
from mcp_server.tools.task_manager import mobile_manage_task
from mcp_server.tools.task_runner import mobile_run_task
from mcp_server.tools.exploration import mobile_exploration
__all__ = [
"mobile_run_task",
@@ -32,6 +33,7 @@ __all__ = [
"mobile_batch",
"mobile_search_history",
"mobile_export_report",
"mobile_exploration",
]
from mcp_server.tools.automation import (
+16 -2
View File
@@ -143,7 +143,7 @@ async def mobile_workspace(include_devices: bool = True) -> dict:
@mcp.tool()
async def mobile_config(
section: Literal["model", "capabilities", "reports"] = "model",
section: Literal["model", "capabilities", "reports", "exploration"] = "model",
changes: dict[str, Any] | None = None,
persist_defaults: bool = False,
) -> dict:
@@ -163,6 +163,8 @@ async def mobile_config(
worker_cleanup_timeout_seconds (30), adb_auto_connect (true),
adb_discovery_interval_seconds (10), adb_discovery_timeout_seconds (5).
reports: formats=['docx','xlsx'] (at least one).
exploration: model (default gpt-6.1-sol), reasoning_effort (high), timeout_seconds
(1800 active seconds), demonstration_timeout_seconds (900), demonstration_warning_seconds (60).
API keys are never accepted or returned: use the Web UI or .env for credentials.
Saving settings does not change an already running task. Flash/Pro are execution
profiles, not model names. Model retries do not repeat device test cases.
@@ -171,6 +173,16 @@ async def mobile_config(
"model": ("/api/system/model-settings", MODEL_FIELDS),
"capabilities": ("/api/system/capability-settings", CAPABILITY_FIELDS),
"reports": ("/api/automation/preferences", {"formats"}),
"exploration": (
"/api/explorations/settings",
{
"model",
"reasoning_effort",
"timeout_seconds",
"demonstration_timeout_seconds",
"demonstration_warning_seconds",
},
),
}[section]
if changes is not None:
if not persist_defaults:
@@ -182,7 +194,9 @@ async def mobile_config(
return current
payload = {key: value for key, value in current.items() if key in allowed}
payload.update(changes)
return await _request("PUT" if section == "reports" else "POST", route, json=payload)
return await _request(
"PUT" if section in {"reports", "exploration"} else "POST", route, json=payload
)
@mcp.tool()
+97
View File
@@ -0,0 +1,97 @@
"""Guided exploration through the same HTTP service used by the workspace."""
from typing import Any, Literal
from mcp_server.base import mcp
from mcp_server.tools.automation import _id, _request
@mcp.tool()
async def mobile_exploration(
action: Literal[
"list",
"get",
"create",
"start",
"message",
"pause",
"continue",
"stop",
"demonstrate",
"demo_finish",
"demo_cancel",
"demo_extend",
"case",
"save_case",
"generate",
"validate",
"batch",
],
exploration_id: str | None = None,
request_id: str | None = None,
payload: dict[str, Any] | None = None,
demonstration_id: str | None = None,
after: int = 0,
version: int | None = None,
) -> Any:
"""Explore ONE complex test with user guidance and produce a reusable case.
Read-only list/get/case never execute tasks. create stores {goal,config?,task_config?};
start/continue explicitly enqueue or resume Pro exploration. Independent defaults:
gpt-6.1-sol/high, 1800 active seconds; timeout pauses, never fails by itself.
message payload={text}; answer pending questions or guide without discarding history.
demonstrate payload={source:'android'|'computer'|'both'} pauses at a safe point and
records while retaining the device/plugin lease. Wait for recording before the user
operates. demo_finish analyzes the SAVED clip; demo_extend payload={seconds:900};
demo_cancel preserves evidence. Default 900s, warning 60s before expiry, auto-pause.
save_case payload={expected_version,case:{name,goal,preconditions?,evidence?}};
generate payload={source_run_id,expected_version} starts evidence-based synthesis,
poll get.generations for completion. validate payload={version,profile,task_config?}
explicitly runs the saved version in Flash/Pro; batch additionally accepts name and
creates a DRAFT batch, never starts it. Exploration pass and rerun pass are separate.
Use stable request_id for every mutation except pause/stop, reusing it on transport
retries. No API keys; configure secrets in the Web UI. mobile_config(section='exploration')
controls non-secret defaults. After errors call mobile_diagnose before resubmitting.
"""
base = "/api/explorations"
if action in {"list", "get", "case"}:
if payload or request_id:
raise ValueError("查询不接受写入内容")
if action == "list":
return await _request("GET", base)
path = f"{base}/{_id(exploration_id)}"
if action == "get":
if after < 0:
raise ValueError("after 不能小于零")
return await _request("GET", path, params={"after": after})
return await _request(
"GET", path + "/case", params={"version": version} if version is not None else {}
)
if action not in {"pause", "stop"} and (not request_id or not request_id.strip()):
raise ValueError("写入探索需提供稳定的 request_id")
body = dict(payload or {})
if "request_id" in body:
raise ValueError("request_id 请使用单独参数")
if request_id:
body["request_id"] = request_id
if action == "create":
return await _request("POST", base, json=body, params={"source": "mcp"})
suffix = {
"start": "start",
"message": "messages",
"pause": "pause",
"continue": "continue",
"stop": "stop",
"demonstrate": "demonstrations",
"save_case": "case",
"generate": "generate",
"validate": "validate",
"batch": "batch",
}.get(action)
if action.startswith("demo_"):
suffix = f"demonstrations/{_id(demonstration_id)}/{action.removeprefix('demo_')}"
if suffix is None:
raise ValueError("不支持的探索操作")
return await _request(
"POST", f"{base}/{_id(exploration_id)}/{suffix}", json=body, params={"source": "mcp"}
)
+2 -1
View File
@@ -40,6 +40,7 @@ from artemis.runtime.task_limits import ExecutionBudget, task_timeout
from artemis.runtime.task_control import read_state
from artemis.runtime.task_result import save_failure_result, read_worker_outcome
from artemis.config.settings import settings
from artemis.runtime.daemon_client import local_daemon_url
# Seconds the spawned runner gets to finish its imports and open its log files
# before the spawn is declared dead. Normal startup creates stdout.log within a
@@ -393,7 +394,7 @@ def mobile_run_task(
return {"trace_id": trace_id, **rejection}
# 3. Dispatch via unified Artemis Daemon scheduler if available (unless standalone forced)
if os.environ.get("ARTEMIS_STANDALONE") != "1":
if local_daemon_url() or os.environ.get("ARTEMIS_STANDALONE") != "1":
try:
is_running, base_url = ensure_daemon_running(timeout=2.0, wait_ready=True)
if is_running: