Add PR A CLI browser command parity with MCP (#4789)
This commit is contained in:
@@ -3,17 +3,26 @@ from __future__ import annotations
|
||||
import os
|
||||
from contextvars import ContextVar
|
||||
|
||||
import structlog
|
||||
|
||||
from skyvern.client import SkyvernEnvironment
|
||||
from skyvern.config import settings
|
||||
from skyvern.library.skyvern import Skyvern
|
||||
|
||||
_skyvern_instance: ContextVar[Skyvern | None] = ContextVar("skyvern_instance", default=None)
|
||||
_global_skyvern_instance: Skyvern | None = None
|
||||
LOG = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def get_skyvern() -> Skyvern:
|
||||
"""Get or create a Skyvern client instance."""
|
||||
global _global_skyvern_instance
|
||||
|
||||
instance = _skyvern_instance.get()
|
||||
if instance is None:
|
||||
instance = _global_skyvern_instance
|
||||
if instance is not None:
|
||||
_skyvern_instance.set(instance)
|
||||
return instance
|
||||
|
||||
api_key = settings.SKYVERN_API_KEY or os.environ.get("SKYVERN_API_KEY")
|
||||
@@ -28,5 +37,28 @@ def get_skyvern() -> Skyvern:
|
||||
else:
|
||||
instance = Skyvern.local()
|
||||
|
||||
_global_skyvern_instance = instance
|
||||
_skyvern_instance.set(instance)
|
||||
return instance
|
||||
|
||||
|
||||
async def close_skyvern() -> None:
|
||||
"""Close active Skyvern client(s) and release Playwright resources."""
|
||||
global _global_skyvern_instance
|
||||
|
||||
instances: list[Skyvern] = []
|
||||
seen: set[int] = set()
|
||||
for candidate in (_skyvern_instance.get(), _global_skyvern_instance):
|
||||
if candidate is None or id(candidate) in seen:
|
||||
continue
|
||||
seen.add(id(candidate))
|
||||
instances.append(candidate)
|
||||
|
||||
for instance in instances:
|
||||
try:
|
||||
await instance.aclose()
|
||||
except Exception:
|
||||
LOG.warning("Failed to close Skyvern client", exc_info=True)
|
||||
|
||||
_skyvern_instance.set(None)
|
||||
_global_skyvern_instance = None
|
||||
|
||||
@@ -5,9 +5,13 @@ from contextvars import ContextVar
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, AsyncIterator
|
||||
|
||||
import structlog
|
||||
|
||||
from .client import get_skyvern
|
||||
from .result import BrowserContext, ErrorCode, make_error
|
||||
|
||||
LOG = structlog.get_logger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from skyvern.library.skyvern_browser import SkyvernBrowser
|
||||
from skyvern.library.skyvern_browser_page import SkyvernBrowserPage
|
||||
@@ -23,20 +27,46 @@ class SessionState:
|
||||
|
||||
|
||||
_current_session: ContextVar[SessionState | None] = ContextVar("mcp_session", default=None)
|
||||
_global_session: SessionState | None = None
|
||||
|
||||
|
||||
def get_current_session() -> SessionState:
|
||||
global _global_session
|
||||
|
||||
state = _current_session.get()
|
||||
if state is None:
|
||||
state = SessionState()
|
||||
if _global_session is None:
|
||||
_global_session = SessionState()
|
||||
state = _global_session
|
||||
_current_session.set(state)
|
||||
return state
|
||||
|
||||
|
||||
def set_current_session(state: SessionState) -> None:
|
||||
global _global_session
|
||||
_global_session = state
|
||||
_current_session.set(state)
|
||||
|
||||
|
||||
def _matches_current(
|
||||
current: SessionState,
|
||||
*,
|
||||
session_id: str | None = None,
|
||||
cdp_url: str | None = None,
|
||||
local: bool = False,
|
||||
) -> bool:
|
||||
if current.browser is None or current.context is None:
|
||||
return False
|
||||
|
||||
if session_id:
|
||||
return current.context.mode == "cloud_session" and current.context.session_id == session_id
|
||||
if cdp_url:
|
||||
return current.context.mode == "cdp" and current.context.cdp_url == cdp_url
|
||||
if local:
|
||||
return current.context.mode == "local"
|
||||
return False
|
||||
|
||||
|
||||
async def resolve_browser(
|
||||
session_id: str | None = None,
|
||||
cdp_url: str | None = None,
|
||||
@@ -54,6 +84,11 @@ async def resolve_browser(
|
||||
skyvern = get_skyvern()
|
||||
current = get_current_session()
|
||||
|
||||
if _matches_current(current, session_id=session_id, cdp_url=cdp_url, local=local):
|
||||
# _matches_current() guarantees both are non-None
|
||||
assert current.browser is not None and current.context is not None
|
||||
return current.browser, current.context
|
||||
|
||||
browser: SkyvernBrowser | None = None
|
||||
try:
|
||||
if session_id:
|
||||
@@ -94,6 +129,31 @@ async def resolve_browser(
|
||||
raise BrowserNotAvailableError()
|
||||
|
||||
|
||||
async def close_current_session() -> None:
|
||||
"""Close the active browser session (if any) and clear local session state."""
|
||||
from .session_ops import do_session_close
|
||||
|
||||
current = get_current_session()
|
||||
try:
|
||||
if current.context and current.context.mode == "cloud_session" and current.context.session_id:
|
||||
try:
|
||||
skyvern = get_skyvern()
|
||||
await do_session_close(skyvern, current.context.session_id)
|
||||
# Prevent SkyvernBrowser.close() from making a redundant API call
|
||||
if current.browser is not None:
|
||||
current.browser._browser_session_id = None
|
||||
except Exception:
|
||||
LOG.warning(
|
||||
"Best-effort cloud session close failed",
|
||||
session_id=current.context.session_id,
|
||||
exc_info=True,
|
||||
)
|
||||
if current.browser is not None:
|
||||
await current.browser.close()
|
||||
finally:
|
||||
set_current_session(SessionState())
|
||||
|
||||
|
||||
async def get_page(
|
||||
session_id: str | None = None,
|
||||
cdp_url: str | None = None,
|
||||
|
||||
Reference in New Issue
Block a user