Files
actionbook__actionbook/packages/dify-plugin/tools/browser_stop_session.py
sen 2db938d519 feat(dify-plugin): v0.1.1 HTTP API migration and serverless hardening (#162)
* chore: align dify plugin manifest and privacy policy

* [packages/dify-plugin]refactor: replace hyperbrowser SDK with direct HTTP API

Remove hyperbrowser Python SDK dependency and use requests to call
Hyperbrowser REST API directly. This eliminates an extra dependency
that may not install cleanly in Dify Cloud's serverless runtime.

- providers/hyperbrowser.py: HTTP POST/PUT instead of SDK client
- pyproject.toml: drop hyperbrowser>=0.1.0 dependency
- requirements.txt: add explicit requirements file
- tests/test_providers.py: mock requests instead of SDK classes

* [packages/dify-plugin]feat: add HTML misroute guard and simplify tool error handling

Add utils/http_guard.py to detect when API responses return HTML
instead of expected text/JSON — a common symptom of ACTIONBOOK_API_URL
pointing to a non-Actionbook endpoint (e.g. Dify app API).

- Integrate HTML guard into search_actions, get_action_by_area_id, provider
- Simplify verbose error branches into concise messages
- Remove debug-level logging from hot paths
- Update all related tests with Content-Type headers and new assertions

* [packages/dify-plugin]refactor: lazy-load browser deps for serverless compatibility

Defer importing playwright and connection_pool until first browser
tool invocation. This allows search_actions and get_action_by_area_id
to work even when playwright binaries are unavailable (e.g. Dify
Cloud serverless runtime).

- tools/__init__.py: lazy __getattr__ for browser tool classes
- browser_create_session.py: thread-safe lazy pool import
- browser_operator.py: lazy import with stub exception fallbacks

* [packages/dify-plugin]fix: review fixes — dead code, lazy import, session recovery

- Remove unreachable except BaseException blocks in search_actions
  and get_action_by_area_id (dead code: only re-raised what it caught)
- Apply lazy connection_pool import in browser_stop_session, matching
  browser_create_session and browser_operator pattern for serverless
- Preserve provider_name/api_key on session recovery reconnect to
  prevent remote session leaks when browser_stop_session runs later
- Update tests accordingly

* [root]chore: use changeset for dify-plugin version bump

Revert manual 0.1.1 bump back to 0.1.0 and add a changeset file
so CI handles versioning through the standard two-phase flow.
Also extend sync-versions.js to run `uv lock` after updating
pyproject.toml to keep uv.lock in sync automatically.
2026-02-25 22:04:23 +08:00

97 lines
3.2 KiB
Python

"""Browser Stop Session Tool — release a managed cloud browser session."""
import logging
import threading
from collections.abc import Generator
from typing import Any
from dify_plugin import Tool
from dify_plugin.entities.tool import ToolInvokeMessage
from providers import get_provider
# Lazy import — avoid loading connection_pool at module level.
# connection_pool spawns a background cleanup thread on import,
# which may fail in Dify Cloud's serverless runtime.
pool = None # Module-level alias set by _ensure_pool(); patchable by tests.
_pool = None
_pool_lock = threading.Lock()
def _ensure_pool():
"""Thread-safe lazy import of connection pool."""
global pool, _pool
if _pool is not None:
return
with _pool_lock:
if _pool is not None:
return
from utils.connection_pool import pool as _p
_pool = _p
pool = _p
logger = logging.getLogger(__name__)
class BrowserStopSessionTool(Tool):
"""Stop a cloud browser session and persist profile state."""
def _invoke(self, tool_parameters: dict[str, Any]) -> Generator[ToolInvokeMessage, None, None]:
if pool is None:
try:
_ensure_pool()
except Exception as e:
yield self.create_text_message(
f"Error: Browser pool unavailable: {type(e).__name__}: {e}\n"
"This environment may not support browser tools."
)
return
session_id = (tool_parameters.get("session_id") or "").strip()
if not session_id:
yield self.create_text_message("Error: 'session_id' is required.")
return
# Look up provider/api_key cached when the session was created
session_info = pool.get_session_info(session_id)
if session_info is None:
yield self.create_text_message(
f"Error: No active session found for session_id '{session_id}'.\n"
"Was browser_create_session called first in this plugin process?"
)
return
provider_name, api_key = session_info
try:
provider = get_provider(provider_name, api_key)
provider.stop_session(session_id)
except NotImplementedError as e:
yield self.create_text_message(f"Error: Provider not yet implemented. {e}")
return
except ValueError as e:
yield self.create_text_message(f"Error: {e}")
return
except Exception as e:
logger.error("Failed to stop browser session.")
yield self.create_text_message(
f"Error: Failed to stop browser session.\n"
f"Reason: {type(e).__name__}: {e}\n"
f"Verify your API key is valid and the provider service is reachable."
)
return
try:
pool.disconnect(session_id)
except Exception:
logger.warning("Failed to disconnect local session after remote stop.")
yield self.create_text_message(
f"Session stopped.\n"
f"Provider: {provider_name}\n"
f"Session ID: {session_id}\n\n"
"Profile state has been persisted (if a profile_id was used)."
)