111 lines
4.6 KiB
Python
111 lines
4.6 KiB
Python
"""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",
|
|
"deletion_preview",
|
|
"delete",
|
|
"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,
|
|
user_confirmed: bool = False,
|
|
) -> Any:
|
|
"""Explore ONE complex test with user guidance and produce a reusable case.
|
|
|
|
deletion_preview lists affected conversation runs; delete requires user_confirmed=true
|
|
after the user requests deletion. Independent validation runs are preserved.
|
|
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-terra/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/delete, 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 {"deletion_preview", "delete"}:
|
|
path = f"{base}/{_id(exploration_id)}"
|
|
if action == "deletion_preview":
|
|
return await _request("GET", path + "/deletion-preview")
|
|
if not user_confirmed:
|
|
raise ValueError("删除探索需要用户明确要求;先用 deletion_preview 核对影响范围")
|
|
return await _request("DELETE", path)
|
|
|
|
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"}
|
|
)
|