feat: add guided exploration and share user-started MCP backend
This commit is contained in:
1 parent
f86db1ced6
commit
ee256ad7ce
144 files changed
+8625
-2075
No files matched your search
@@ -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 (
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"}
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user