grasp-sdk 0.2.0a1__tar.gz → 0.2.0b2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of grasp-sdk might be problematic. Click here for more details.
- {grasp_sdk-0.2.0a1/grasp_sdk.egg-info → grasp_sdk-0.2.0b2}/PKG-INFO +1 -1
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/example_async_context.py +1 -1
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/example_grasp_usage.py +1 -1
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/grasp_terminal.py +3 -2
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/test_grasp_classes.py +1 -2
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/test_removed_methods.py +1 -1
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/test_terminal_updates.py +2 -2
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/__init__.py +1 -3
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/grasp/__init__.py +0 -2
- grasp_sdk-0.2.0b2/grasp_sdk/grasp/browser.py +162 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/grasp/index.py +11 -6
- grasp_sdk-0.2.0b2/grasp_sdk/grasp/session.py +146 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/models/__init__.py +10 -5
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/services/browser.py +5 -3
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/services/filesystem.py +30 -1
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/services/sandbox.py +14 -7
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/services/terminal.py +5 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/utils/__init__.py +1 -2
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/utils/config.py +2 -32
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2/grasp_sdk.egg-info}/PKG-INFO +1 -1
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/SOURCES.txt +0 -6
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/pyproject.toml +1 -1
- grasp_sdk-0.2.0a1/examples/test_get_replay_screenshots.py +0 -90
- grasp_sdk-0.2.0a1/examples/test_shutdown_deprecation.py +0 -62
- grasp_sdk-0.2.0a1/grasp_sdk/grasp/browser.py +0 -69
- grasp_sdk-0.2.0a1/grasp_sdk/grasp/session.py +0 -108
- grasp_sdk-0.2.0a1/grasp_sdk/grasp/terminal.py +0 -32
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/MANIFEST.in +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/README.md +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/build_and_publish.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/example_binary_file_support.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/example_readfile_usage.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/grasp_usage.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/test_async_context.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/test_python_script.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/grasp/server.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/grasp/utils.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/services/__init__.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/utils/auth.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/utils/logger.py +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/dependency_links.txt +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/entry_points.txt +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/not-zip-safe +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/requires.txt +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/top_level.txt +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/py.typed +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/requirements.txt +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/setup.cfg +0 -0
- {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/setup.py +0 -0
|
@@ -36,7 +36,8 @@ async def main():
|
|
|
36
36
|
# 'debug': True,
|
|
37
37
|
},
|
|
38
38
|
'keepAliveMS': 10000,
|
|
39
|
-
'
|
|
39
|
+
# 'logLevel': 'error',
|
|
40
|
+
#'timeout': 3600000, # 容器最长运行1小时(最大值可以为一天 86400000)
|
|
40
41
|
})
|
|
41
42
|
|
|
42
43
|
connection = session.browser.connection
|
|
@@ -44,7 +45,7 @@ async def main():
|
|
|
44
45
|
|
|
45
46
|
# Create terminal connection
|
|
46
47
|
print("📡 Creating terminal connection...")
|
|
47
|
-
terminal = session.terminal
|
|
48
|
+
terminal = session.terminal
|
|
48
49
|
|
|
49
50
|
print("✅ Terminal connected!")
|
|
50
51
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""测试 Grasp 类的功能"""
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
|
-
from grasp_sdk import Grasp, GraspSession, GraspBrowser
|
|
5
|
+
from grasp_sdk import Grasp, GraspSession, GraspBrowser
|
|
6
6
|
|
|
7
7
|
def test_grasp_classes():
|
|
8
8
|
"""测试所有 Grasp 类的基本功能"""
|
|
@@ -25,7 +25,6 @@ def test_grasp_classes():
|
|
|
25
25
|
print(f" ✓ Grasp 类可用: {Grasp is not None}")
|
|
26
26
|
print(f" ✓ GraspSession 类可用: {GraspSession is not None}")
|
|
27
27
|
print(f" ✓ GraspBrowser 类可用: {GraspBrowser is not None}")
|
|
28
|
-
print(f" ✓ GraspTerminal 类可用: {GraspTerminal is not None}")
|
|
29
28
|
|
|
30
29
|
# 测试方法可用性
|
|
31
30
|
print("\n3. 测试方法可用性:")
|
|
@@ -17,7 +17,7 @@ def test_removed_methods():
|
|
|
17
17
|
print("=" * 30)
|
|
18
18
|
|
|
19
19
|
try:
|
|
20
|
-
from grasp_sdk import Grasp, GraspSession, GraspBrowser
|
|
20
|
+
from grasp_sdk import Grasp, GraspSession, GraspBrowser
|
|
21
21
|
print("✅ 核心类导入成功")
|
|
22
22
|
except ImportError as e:
|
|
23
23
|
print(f"❌ 核心类导入失败: {e}")
|
|
@@ -111,7 +111,7 @@ async def test_terminal_service():
|
|
|
111
111
|
mock_sandbox.run_command.return_value = mock_emitter
|
|
112
112
|
|
|
113
113
|
# Test run_command
|
|
114
|
-
result = await terminal.run_command('echo "Hello World"', {'
|
|
114
|
+
result = await terminal.run_command('echo "Hello World"', {'timeout_ms': 5000})
|
|
115
115
|
print("✓ run_command executed successfully")
|
|
116
116
|
|
|
117
117
|
# Verify the command was called with correct options
|
|
@@ -152,7 +152,7 @@ async def test_command_options_compatibility():
|
|
|
152
152
|
# Test that ICommandOptions supports 'inBackground' parameter
|
|
153
153
|
options: ICommandOptions = {
|
|
154
154
|
'inBackground': True,
|
|
155
|
-
'
|
|
155
|
+
'timeout_ms': 5000,
|
|
156
156
|
'cwd': '/tmp',
|
|
157
157
|
'nohup': False
|
|
158
158
|
}
|
|
@@ -16,7 +16,6 @@ from .utils.logger import get_logger
|
|
|
16
16
|
from .grasp import (
|
|
17
17
|
GraspServer,
|
|
18
18
|
GraspBrowser,
|
|
19
|
-
GraspTerminal,
|
|
20
19
|
GraspSession,
|
|
21
20
|
Grasp,
|
|
22
21
|
_servers,
|
|
@@ -26,7 +25,7 @@ from .grasp import (
|
|
|
26
25
|
# Import CDPConnection for type annotation
|
|
27
26
|
from .services.browser import CDPConnection
|
|
28
27
|
|
|
29
|
-
__version__ = "0.2.
|
|
28
|
+
__version__ = "0.2.0b2"
|
|
30
29
|
__author__ = "Grasp Team"
|
|
31
30
|
__email__ = "team@grasp.dev"
|
|
32
31
|
|
|
@@ -177,7 +176,6 @@ default = {
|
|
|
177
176
|
'Grasp': Grasp,
|
|
178
177
|
'GraspSession': GraspSession,
|
|
179
178
|
'GraspBrowser': GraspBrowser,
|
|
180
|
-
'GraspTerminal': GraspTerminal,
|
|
181
179
|
'launch_browser': launch_browser,
|
|
182
180
|
'shutdown': shutdown,
|
|
183
181
|
}
|
|
@@ -4,7 +4,6 @@ from typing import Dict
|
|
|
4
4
|
|
|
5
5
|
from .server import GraspServer
|
|
6
6
|
from .browser import GraspBrowser
|
|
7
|
-
from .terminal import GraspTerminal
|
|
8
7
|
from .session import GraspSession
|
|
9
8
|
from .index import Grasp
|
|
10
9
|
from .utils import (
|
|
@@ -18,7 +17,6 @@ _servers: Dict[str, GraspServer] = _servers
|
|
|
18
17
|
__all__ = [
|
|
19
18
|
'GraspServer',
|
|
20
19
|
'GraspBrowser',
|
|
21
|
-
'GraspTerminal',
|
|
22
20
|
'GraspSession',
|
|
23
21
|
'Grasp',
|
|
24
22
|
'shutdown',
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""GraspBrowser class for browser interface."""
|
|
2
|
+
|
|
3
|
+
import aiohttp
|
|
4
|
+
import os
|
|
5
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from ..services.browser import CDPConnection
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .session import GraspSession
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GraspBrowser:
|
|
14
|
+
"""Browser interface for Grasp session."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, connection: CDPConnection, session: Optional['GraspSession'] = None):
|
|
17
|
+
"""Initialize GraspBrowser.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
connection: CDP connection instance
|
|
21
|
+
session: GraspSession instance for accessing terminal and files services
|
|
22
|
+
"""
|
|
23
|
+
self.connection = connection
|
|
24
|
+
self.session = session
|
|
25
|
+
|
|
26
|
+
def get_host(self) -> str:
|
|
27
|
+
"""Get browser host.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Host for browser connection
|
|
31
|
+
"""
|
|
32
|
+
from urllib.parse import urlparse
|
|
33
|
+
return urlparse(self.connection.http_url).netloc
|
|
34
|
+
|
|
35
|
+
def get_endpoint(self) -> str:
|
|
36
|
+
"""Get browser WebSocket endpoint URL.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
WebSocket URL for browser connection
|
|
40
|
+
"""
|
|
41
|
+
return self.connection.ws_url
|
|
42
|
+
|
|
43
|
+
async def get_current_page_target_info(self) -> Optional[Dict[str, Any]]:
|
|
44
|
+
"""Get current page target information.
|
|
45
|
+
|
|
46
|
+
Warning:
|
|
47
|
+
This method is experimental and may change or be removed in future versions.
|
|
48
|
+
Use with caution in production environments.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dictionary containing target info and last screenshot, or None if failed
|
|
52
|
+
"""
|
|
53
|
+
host = self.connection.http_url
|
|
54
|
+
api = f"{host}/api/page/info"
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
async with aiohttp.ClientSession() as session:
|
|
58
|
+
async with session.get(api) as response:
|
|
59
|
+
if not response.ok:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
data = await response.json()
|
|
63
|
+
page_info = data.get('pageInfo', {})
|
|
64
|
+
last_screenshot = page_info.get('lastScreenshot')
|
|
65
|
+
session_id = page_info.get('sessionId')
|
|
66
|
+
targets = page_info.get('targets', {})
|
|
67
|
+
target_id = page_info.get('targetId')
|
|
68
|
+
page_loaded = page_info.get('pageLoaded')
|
|
69
|
+
|
|
70
|
+
current_target = {}
|
|
71
|
+
for target in targets.values():
|
|
72
|
+
if target.get('targetId') == target_id:
|
|
73
|
+
current_target = target
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
'targetId': target_id,
|
|
78
|
+
'sessionId': session_id,
|
|
79
|
+
'pageLoaded': page_loaded,
|
|
80
|
+
**current_target,
|
|
81
|
+
'lastScreenshot': last_screenshot,
|
|
82
|
+
'targets': targets,
|
|
83
|
+
}
|
|
84
|
+
except Exception:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
async def download_replay_video(self, local_path: str) -> None:
|
|
88
|
+
"""Download replay video from screenshots.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
local_path: Local path to save the video file
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
RuntimeError: If session is not available or video generation fails
|
|
95
|
+
"""
|
|
96
|
+
if not self.session:
|
|
97
|
+
raise RuntimeError("Session is required for download_replay_video")
|
|
98
|
+
|
|
99
|
+
# Create terminal instance
|
|
100
|
+
terminal = self.session.terminal
|
|
101
|
+
|
|
102
|
+
# Generate file list for ffmpeg
|
|
103
|
+
command1 = await terminal.run_command(
|
|
104
|
+
"cd /home/user/downloads/grasp-screenshots && ls -1 | grep -v '^filelist.txt$' | sort | awk '{print \"file '\''\" $0 \"'\''\"}'> filelist.txt"
|
|
105
|
+
)
|
|
106
|
+
await command1.end()
|
|
107
|
+
|
|
108
|
+
# Generate video using ffmpeg
|
|
109
|
+
command2 = await terminal.run_command(
|
|
110
|
+
"cd /home/user/downloads/grasp-screenshots && ffmpeg -r 25 -f concat -safe 0 -i filelist.txt -vsync vfr -pix_fmt yuv420p output.mp4"
|
|
111
|
+
)
|
|
112
|
+
await command2.end()
|
|
113
|
+
|
|
114
|
+
# Determine final local path
|
|
115
|
+
if not local_path.endswith('.mp4'):
|
|
116
|
+
local_path = os.path.join(local_path, 'output.mp4')
|
|
117
|
+
|
|
118
|
+
# Download the generated video file
|
|
119
|
+
await self.session.files.download_file(
|
|
120
|
+
'/home/user/downloads/grasp-screenshots/output.mp4',
|
|
121
|
+
local_path
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
async def get_liveview_streaming_url(self) -> Optional[str]:
|
|
125
|
+
"""Get liveview streaming URL.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Liveview streaming URL or None if not ready
|
|
129
|
+
"""
|
|
130
|
+
host = self.connection.http_url
|
|
131
|
+
api = f"{host}/api/session/liveview/stream"
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
async with aiohttp.ClientSession() as session:
|
|
135
|
+
async with session.get(api) as response:
|
|
136
|
+
if not response.ok:
|
|
137
|
+
return None
|
|
138
|
+
return api
|
|
139
|
+
except Exception:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
async def get_liveview_page_url(self) -> Optional[str]:
|
|
143
|
+
"""Get liveview page URL.
|
|
144
|
+
|
|
145
|
+
Warning:
|
|
146
|
+
This method is experimental and may change or be removed in future versions.
|
|
147
|
+
Use with caution in production environments.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Liveview page URL or None if not ready
|
|
151
|
+
"""
|
|
152
|
+
host = self.connection.http_url
|
|
153
|
+
api = f"{host}/api/session/liveview/preview"
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
async with aiohttp.ClientSession() as session:
|
|
157
|
+
async with session.get(api) as response:
|
|
158
|
+
if not response.ok:
|
|
159
|
+
return None
|
|
160
|
+
return api
|
|
161
|
+
except Exception:
|
|
162
|
+
return None
|
|
@@ -5,6 +5,7 @@ from typing import Dict, Optional, Any
|
|
|
5
5
|
|
|
6
6
|
from .server import GraspServer
|
|
7
7
|
from .session import GraspSession
|
|
8
|
+
from ..utils.config import get_config
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class Grasp:
|
|
@@ -32,12 +33,13 @@ class Grasp:
|
|
|
32
33
|
Returns:
|
|
33
34
|
GraspSession instance
|
|
34
35
|
"""
|
|
36
|
+
config = get_config()
|
|
35
37
|
browser_options = options.get('browser', {})
|
|
36
|
-
browser_options['key'] = self.key
|
|
37
|
-
browser_options['keepAliveMS'] = options.get('keepAliveMS',
|
|
38
|
-
browser_options['timeout'] = options.get('timeout',
|
|
39
|
-
browser_options['debug'] = options.get('debug',
|
|
40
|
-
browser_options['logLevel'] = options.get('logLevel', '
|
|
38
|
+
browser_options['key'] = self.key or config['sandbox']['key']
|
|
39
|
+
browser_options['keepAliveMS'] = options.get('keepAliveMS', config['sandbox']['keepAliveMs'])
|
|
40
|
+
browser_options['timeout'] = options.get('timeout', config['sandbox']['timeout'])
|
|
41
|
+
browser_options['debug'] = options.get('debug', config['sandbox']['debug'])
|
|
42
|
+
browser_options['logLevel'] = options.get('logLevel', config['logger']['level'])
|
|
41
43
|
|
|
42
44
|
server = GraspServer(browser_options)
|
|
43
45
|
cdp_connection = await server.create_browser_task()
|
|
@@ -116,7 +118,10 @@ class Grasp:
|
|
|
116
118
|
|
|
117
119
|
if not cdp_connection:
|
|
118
120
|
# Create new server with no timeout for existing sessions
|
|
119
|
-
server = GraspServer({
|
|
121
|
+
server = GraspServer({
|
|
122
|
+
'timeout': 0,
|
|
123
|
+
'key': self.key,
|
|
124
|
+
})
|
|
120
125
|
cdp_connection = await server.connect_browser_task(session_id)
|
|
121
126
|
|
|
122
127
|
return GraspSession(cdp_connection)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""GraspSession class for session management."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import websockets
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from .browser import GraspBrowser
|
|
7
|
+
from ..services.terminal import TerminalService
|
|
8
|
+
from ..services.browser import CDPConnection
|
|
9
|
+
from ..services.filesystem import FileSystemService
|
|
10
|
+
from ..utils.logger import get_logger
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GraspSession:
|
|
14
|
+
"""Main session interface providing access to browser, terminal, and files."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, connection: CDPConnection):
|
|
17
|
+
"""Initialize GraspSession.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
connection: CDP connection instance
|
|
21
|
+
"""
|
|
22
|
+
self.connection = connection
|
|
23
|
+
self.browser = GraspBrowser(connection, self)
|
|
24
|
+
|
|
25
|
+
# Create files service
|
|
26
|
+
connection_id = connection.id
|
|
27
|
+
try:
|
|
28
|
+
from .utils import _servers
|
|
29
|
+
server = _servers[connection_id]
|
|
30
|
+
if server.sandbox is None:
|
|
31
|
+
raise RuntimeError('Sandbox is not available for file system service')
|
|
32
|
+
self.files = FileSystemService(server.sandbox, connection)
|
|
33
|
+
self.terminal = TerminalService(server.sandbox, connection)
|
|
34
|
+
except (ImportError, KeyError) as e:
|
|
35
|
+
# In test environment or when server is not available, create a mock files service
|
|
36
|
+
self.logger.debug(f"Warning: Files service not available: {e}")
|
|
37
|
+
raise e
|
|
38
|
+
|
|
39
|
+
# WebSocket connection for keep-alive
|
|
40
|
+
self._ws = None
|
|
41
|
+
self._is_closed = False
|
|
42
|
+
|
|
43
|
+
self.logger = get_logger().child('GraspSession')
|
|
44
|
+
|
|
45
|
+
# Initialize WebSocket connection and start keep-alive
|
|
46
|
+
# self.logger.debug('create session...')
|
|
47
|
+
asyncio.create_task(self._initialize_websocket())
|
|
48
|
+
|
|
49
|
+
async def _initialize_websocket(self) -> None:
|
|
50
|
+
"""Initialize WebSocket connection and start keep-alive."""
|
|
51
|
+
try:
|
|
52
|
+
self._ws = await websockets.connect(self.connection.ws_url)
|
|
53
|
+
if not self._is_closed:
|
|
54
|
+
await self._keep_alive()
|
|
55
|
+
else:
|
|
56
|
+
await self._ws.close()
|
|
57
|
+
except Exception as e:
|
|
58
|
+
self.logger.debug(f"Failed to initialize WebSocket connection: {e}")
|
|
59
|
+
|
|
60
|
+
async def _keep_alive(self) -> None:
|
|
61
|
+
"""Keep WebSocket connection alive with periodic ping."""
|
|
62
|
+
ws = self._ws
|
|
63
|
+
await asyncio.sleep(10) # 10 seconds interval, same as TypeScript version
|
|
64
|
+
if ws and ws.close_code is None:
|
|
65
|
+
try:
|
|
66
|
+
# Send ping and wait for pong response
|
|
67
|
+
pong_waiter = ws.ping()
|
|
68
|
+
await pong_waiter
|
|
69
|
+
# Recursively call keep_alive after receiving pong
|
|
70
|
+
await self._keep_alive()
|
|
71
|
+
except Exception as e:
|
|
72
|
+
self.logger.debug(f"Keep-alive ping failed: {e}")
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def id(self) -> str:
|
|
76
|
+
"""Get session ID.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Session ID
|
|
80
|
+
"""
|
|
81
|
+
return self.connection.id
|
|
82
|
+
|
|
83
|
+
def is_running(self) -> bool:
|
|
84
|
+
"""Check if the session is currently running.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
True if the session is running, False otherwise
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
from .utils import _servers
|
|
91
|
+
from ..models import SandboxStatus
|
|
92
|
+
server = _servers[self.id]
|
|
93
|
+
if server.sandbox is None:
|
|
94
|
+
return False
|
|
95
|
+
status = server.sandbox.get_status()
|
|
96
|
+
return status == SandboxStatus.RUNNING
|
|
97
|
+
except (ImportError, KeyError) as e:
|
|
98
|
+
self.logger.error(f"Failed to check running status: {e}")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def get_host(self, port: int) -> Optional[str]:
|
|
102
|
+
"""Get the external host address for a specific port.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
port: Port number to get host for
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
External host address or None if sandbox not available
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
from .utils import _servers
|
|
112
|
+
server = _servers[self.id]
|
|
113
|
+
if server.sandbox is None:
|
|
114
|
+
self.logger.warn('Cannot get host: sandbox not available')
|
|
115
|
+
return None
|
|
116
|
+
return server.sandbox.get_sandbox_host(port)
|
|
117
|
+
except (ImportError, KeyError) as e:
|
|
118
|
+
self.logger.error(f"Failed to get host: {e}")
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
async def close(self) -> None:
|
|
122
|
+
"""Close the session and cleanup resources."""
|
|
123
|
+
# Set closed flag to prevent new keep-alive cycles
|
|
124
|
+
self._is_closed = True
|
|
125
|
+
|
|
126
|
+
# Close WebSocket connection with timeout
|
|
127
|
+
if self._ws and self._ws.close_code is None:
|
|
128
|
+
try:
|
|
129
|
+
# Add timeout to prevent hanging on close
|
|
130
|
+
await asyncio.wait_for(self._ws.close(), timeout=5.0)
|
|
131
|
+
self.logger.debug('✅ WebSocket closed successfully')
|
|
132
|
+
except asyncio.TimeoutError:
|
|
133
|
+
self.logger.debug('⚠️ WebSocket close timeout, forcing termination')
|
|
134
|
+
# Force close if timeout
|
|
135
|
+
if hasattr(self._ws, 'transport') and self._ws.transport:
|
|
136
|
+
self._ws.transport.close()
|
|
137
|
+
except Exception as e:
|
|
138
|
+
self.logger.error(f"Failed to close WebSocket connection: {e}")
|
|
139
|
+
|
|
140
|
+
# Cleanup server resources
|
|
141
|
+
try:
|
|
142
|
+
from .utils import _servers
|
|
143
|
+
await _servers[self.id].cleanup()
|
|
144
|
+
except (ImportError, KeyError) as e:
|
|
145
|
+
# In test environment or when server is not available, skip cleanup
|
|
146
|
+
self.logger.error(f"Warning: Server cleanup not available: {e}")
|
|
@@ -23,6 +23,7 @@ class ISandboxConfig(TypedDict):
|
|
|
23
23
|
timeout: int # Required: Default timeout in milliseconds
|
|
24
24
|
workspace: NotRequired[str] # Optional: Grasp workspace ID
|
|
25
25
|
debug: NotRequired[bool] # Optional: Enable debug mode for detailed logging
|
|
26
|
+
keepAliveMs: NotRequired[int] # Optional: Keep-alive time in milliseconds
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class IBrowserConfig(TypedDict):
|
|
@@ -35,19 +36,23 @@ class IBrowserConfig(TypedDict):
|
|
|
35
36
|
|
|
36
37
|
class ICommandOptions(TypedDict):
|
|
37
38
|
"""Command execution options interface."""
|
|
38
|
-
inBackground: NotRequired[bool] # Whether to run command in background
|
|
39
|
-
|
|
39
|
+
inBackground: NotRequired[bool] # Whether to run command in background (corresponds to 'background' in TypeScript)
|
|
40
|
+
timeout_ms: NotRequired[int] # Timeout in milliseconds (corresponds to 'timeoutMs' in TypeScript)
|
|
40
41
|
cwd: NotRequired[str] # Working directory
|
|
41
|
-
|
|
42
|
+
user: NotRequired[str] # User to run the command as (default: 'user')
|
|
43
|
+
envs: NotRequired[Dict[str, str]] # Environment variables for the command
|
|
44
|
+
nohup: NotRequired[bool] # Whether to use nohup for command execution (Grasp-specific extension)
|
|
45
|
+
# Note: onStdout and onStderr callbacks are handled differently in Python implementation
|
|
42
46
|
|
|
43
47
|
|
|
44
48
|
class IScriptOptions(TypedDict):
|
|
45
49
|
"""Script execution options interface."""
|
|
46
50
|
type: str # Required: Script type: 'cjs' for CommonJS, 'esm' for ES Modules, 'py' for Python
|
|
47
51
|
cwd: NotRequired[str] # Working directory
|
|
48
|
-
|
|
52
|
+
timeout_ms: NotRequired[int] # Timeout in milliseconds
|
|
49
53
|
background: NotRequired[bool] # Run in background
|
|
50
|
-
|
|
54
|
+
user: NotRequired[str] # User to run the script as (default: 'user')
|
|
55
|
+
nohup: NotRequired[bool] # Use nohup for background execution (Grasp-specific extension)
|
|
51
56
|
envs: NotRequired[Dict[str, str]] # The environment variables
|
|
52
57
|
preCommand: NotRequired[str] # Pre command
|
|
53
58
|
|
|
@@ -106,6 +106,7 @@ class BrowserService:
|
|
|
106
106
|
'HEADLESS': str(self.config['headless']).lower(),
|
|
107
107
|
'NODE_ENV': 'production',
|
|
108
108
|
# 'SANDBOX_ID': self.sandbox_service.id,
|
|
109
|
+
'PLAYWRIGHT_BROWSERS_PATH': '0',
|
|
109
110
|
'WORKSPACE': self.sandbox_service.workspace,
|
|
110
111
|
'BROWSER_TYPE': browser_type,
|
|
111
112
|
**self.config['envs']
|
|
@@ -185,7 +186,7 @@ class BrowserService:
|
|
|
185
186
|
'type': 'esm',
|
|
186
187
|
'background': True,
|
|
187
188
|
'nohup': not self.sandbox_service.is_debug,
|
|
188
|
-
'
|
|
189
|
+
'timeout_ms': 0,
|
|
189
190
|
'preCommand': ''
|
|
190
191
|
}
|
|
191
192
|
|
|
@@ -376,8 +377,9 @@ class BrowserService:
|
|
|
376
377
|
self._health_check_task = None
|
|
377
378
|
|
|
378
379
|
# Kill the browser process
|
|
379
|
-
|
|
380
|
-
|
|
380
|
+
if self.browser_process is not None:
|
|
381
|
+
if hasattr(self.browser_process, 'kill'):
|
|
382
|
+
await self.browser_process.kill()
|
|
381
383
|
|
|
382
384
|
self.browser_process = None
|
|
383
385
|
self.cdp_connection = None
|
|
@@ -91,4 +91,33 @@ class FileSystemService:
|
|
|
91
91
|
File content as string or bytes depending on encoding
|
|
92
92
|
"""
|
|
93
93
|
read_result = await self.sandbox.read_file_from_sandbox(remote_path, options)
|
|
94
|
-
return read_result
|
|
94
|
+
return read_result
|
|
95
|
+
|
|
96
|
+
async def sync_downloads_directory(
|
|
97
|
+
self,
|
|
98
|
+
local_path: str,
|
|
99
|
+
remote_path: Optional[str] = None
|
|
100
|
+
) -> str:
|
|
101
|
+
"""Synchronize downloads directory from sandbox to local filesystem.
|
|
102
|
+
|
|
103
|
+
This method is experimental and may change or be removed in future versions.
|
|
104
|
+
Use with caution in production environments.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
local_path: Local directory path to sync to
|
|
108
|
+
remote_path: Remote directory path to sync from (defaults to local_path)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Local sync directory path
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
RuntimeError: If sandbox is not running or sync fails
|
|
115
|
+
"""
|
|
116
|
+
if remote_path is None:
|
|
117
|
+
remote_path = local_path
|
|
118
|
+
|
|
119
|
+
sync_result = await self.sandbox.sync_downloads_directory(
|
|
120
|
+
dist=local_path,
|
|
121
|
+
src=remote_path
|
|
122
|
+
)
|
|
123
|
+
return sync_result
|
|
@@ -42,6 +42,8 @@ class CommandEventEmitter:
|
|
|
42
42
|
def set_handle(self, handle: Any) -> None:
|
|
43
43
|
"""Set the command handle (used internally)."""
|
|
44
44
|
self.handle = handle
|
|
45
|
+
if self.is_killed:
|
|
46
|
+
self.handle.kill()
|
|
45
47
|
|
|
46
48
|
def on(self, event: str, callback: Callable) -> None:
|
|
47
49
|
"""Register event listener."""
|
|
@@ -98,9 +100,9 @@ class CommandEventEmitter:
|
|
|
98
100
|
|
|
99
101
|
async def kill(self) -> None:
|
|
100
102
|
"""Kill the running command."""
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
self.is_killed = True
|
|
104
|
+
if self.handle:
|
|
105
|
+
await self.handle.kill()
|
|
104
106
|
|
|
105
107
|
def get_handle(self) -> Optional[Any]:
|
|
106
108
|
"""Get the original command handle."""
|
|
@@ -284,10 +286,11 @@ class SandboxService:
|
|
|
284
286
|
options = {}
|
|
285
287
|
|
|
286
288
|
cwd = options.get('cwd', self.DEFAULT_WORKING_DIRECTORY)
|
|
287
|
-
timeout_ms = options.get('
|
|
289
|
+
timeout_ms = options.get('timeout_ms', 0)
|
|
288
290
|
use_nohup = options.get('nohup', False)
|
|
289
291
|
in_background = options.get('inBackground', False)
|
|
290
292
|
envs = options.get('envs', {})
|
|
293
|
+
user = options.get('user', 'user')
|
|
291
294
|
|
|
292
295
|
# print(f'command: {command}')
|
|
293
296
|
# print(f'🚀 options {options}')
|
|
@@ -316,9 +319,10 @@ class SandboxService:
|
|
|
316
319
|
result = await self.sandbox.commands.run(
|
|
317
320
|
final_command,
|
|
318
321
|
cwd=cwd,
|
|
319
|
-
timeout=timeout_ms,
|
|
322
|
+
timeout=timeout_ms // 1000,
|
|
320
323
|
envs=envs,
|
|
321
324
|
background=in_background,
|
|
325
|
+
user=user,
|
|
322
326
|
)
|
|
323
327
|
else:
|
|
324
328
|
raise RuntimeError('Sandbox commands interface not available')
|
|
@@ -344,6 +348,7 @@ class SandboxService:
|
|
|
344
348
|
timeout=timeout_ms // 1000,
|
|
345
349
|
envs=envs,
|
|
346
350
|
background=in_background,
|
|
351
|
+
user=user,
|
|
347
352
|
)
|
|
348
353
|
return None
|
|
349
354
|
|
|
@@ -363,6 +368,7 @@ class SandboxService:
|
|
|
363
368
|
timeout=timeout_ms // 1000,
|
|
364
369
|
background=True,
|
|
365
370
|
envs=envs,
|
|
371
|
+
user=user,
|
|
366
372
|
on_stdout=lambda data: event_emitter.emit('stdout', data),
|
|
367
373
|
on_stderr=lambda data: event_emitter.emit('stderr', data)
|
|
368
374
|
)
|
|
@@ -467,10 +473,11 @@ class SandboxService:
|
|
|
467
473
|
# Prepare command options
|
|
468
474
|
cmd_options: Optional[Any] = {
|
|
469
475
|
'cwd': options.get('cwd'),
|
|
470
|
-
'
|
|
476
|
+
'timeout_ms': options.get('timeout_ms', 0),
|
|
471
477
|
'inBackground': options.get('background', False),
|
|
472
478
|
'nohup': options.get('nohup', False),
|
|
473
479
|
'envs': envs,
|
|
480
|
+
'user': options.get('user', 'user'),
|
|
474
481
|
}
|
|
475
482
|
|
|
476
483
|
# Execute the script
|
|
@@ -858,7 +865,7 @@ class SandboxService:
|
|
|
858
865
|
raise ValueError(error_msg)
|
|
859
866
|
|
|
860
867
|
remote_path = src
|
|
861
|
-
local_path =
|
|
868
|
+
local_path = dist
|
|
862
869
|
|
|
863
870
|
try:
|
|
864
871
|
self.logger.debug('🗂️ Starting recursive directory sync', {
|
|
@@ -138,6 +138,11 @@ class Commands:
|
|
|
138
138
|
# Force background execution for streaming
|
|
139
139
|
bg_options: ICommandOptions = {**options, 'inBackground': True, 'nohup': False}
|
|
140
140
|
|
|
141
|
+
# Set browser endpoint in environment variables
|
|
142
|
+
if 'envs' not in bg_options:
|
|
143
|
+
bg_options['envs'] = {}
|
|
144
|
+
bg_options['envs']['BROWSER_ENDPOINT'] = self.connection.ws_url
|
|
145
|
+
|
|
141
146
|
emitter: Optional[CommandEventEmitter] = await self.sandbox.run_command(
|
|
142
147
|
command, bg_options
|
|
143
148
|
)
|
|
@@ -4,7 +4,7 @@ This package contains utility functions and classes for configuration,
|
|
|
4
4
|
logging, authentication, and other common functionality."""
|
|
5
5
|
|
|
6
6
|
# Import all utility modules
|
|
7
|
-
from .config import get_config, get_sandbox_config,
|
|
7
|
+
from .config import get_config, get_sandbox_config, DEFAULT_CONFIG
|
|
8
8
|
from .logger import Logger, init_logger, get_logger, debug, info, warn, error
|
|
9
9
|
from .auth import verify, login, AuthError, KeyVerificationError, AuthManager
|
|
10
10
|
|
|
@@ -12,7 +12,6 @@ __all__ = [
|
|
|
12
12
|
# Configuration
|
|
13
13
|
'get_config',
|
|
14
14
|
'get_sandbox_config',
|
|
15
|
-
'get_browser_config',
|
|
16
15
|
'DEFAULT_CONFIG',
|
|
17
16
|
# Logging
|
|
18
17
|
'Logger',
|
|
@@ -25,9 +25,10 @@ def get_config() -> Dict[str, Any]:
|
|
|
25
25
|
'key': os.getenv('GRASP_KEY', ''),
|
|
26
26
|
'timeout': int(os.getenv('GRASP_SERVICE_TIMEOUT', '900000')),
|
|
27
27
|
'debug': os.getenv('GRASP_DEBUG', 'false').lower() == 'true',
|
|
28
|
+
'keepAliveMs': int(os.getenv('GRASP_KEEP_ALIVE_MS', '10000')),
|
|
28
29
|
},
|
|
29
30
|
'logger': {
|
|
30
|
-
'level': os.getenv('GRASP_LOG_LEVEL',
|
|
31
|
+
'level': os.getenv('GRASP_LOG_LEVEL', None),
|
|
31
32
|
'console': True,
|
|
32
33
|
'file': os.getenv('GRASP_LOG_FILE'),
|
|
33
34
|
},
|
|
@@ -54,40 +55,9 @@ def get_sandbox_config() -> ISandboxConfig:
|
|
|
54
55
|
|
|
55
56
|
return sandbox_config
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
def get_browser_config() -> IBrowserConfig:
|
|
59
|
-
"""Gets browser-specific configuration.
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
IBrowserConfig: Browser configuration object.
|
|
63
|
-
"""
|
|
64
|
-
return IBrowserConfig(
|
|
65
|
-
args=[
|
|
66
|
-
'--no-sandbox',
|
|
67
|
-
'--disable-setuid-sandbox',
|
|
68
|
-
'--disable-dev-shm-usage',
|
|
69
|
-
'--disable-gpu',
|
|
70
|
-
'--remote-debugging-port=9222',
|
|
71
|
-
'--remote-debugging-address=0.0.0.0',
|
|
72
|
-
],
|
|
73
|
-
headless=os.getenv('GRASP_HEADLESS', 'true').lower() == 'true',
|
|
74
|
-
launchTimeout=int(os.getenv('GRASP_LAUNCH_TIMEOUT', '30000')),
|
|
75
|
-
envs={
|
|
76
|
-
'PLAYWRIGHT_BROWSERS_PATH': '0',
|
|
77
|
-
'DISPLAY': ':99',
|
|
78
|
-
},
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
58
|
# Default configuration constants
|
|
83
59
|
DEFAULT_CONFIG = {
|
|
84
|
-
'PLAYWRIGHT_BROWSERS_PATH': '0',
|
|
85
60
|
'WORKING_DIRECTORY': '/home/user',
|
|
86
|
-
'SCREENSHOT_PATH': '/home/user',
|
|
87
|
-
'DEFAULT_VIEWPORT': {
|
|
88
|
-
'width': 1280,
|
|
89
|
-
'height': 720,
|
|
90
|
-
},
|
|
91
61
|
}
|
|
92
62
|
|
|
93
63
|
|
|
@@ -15,11 +15,9 @@ setup.py
|
|
|
15
15
|
./examples/grasp_terminal.py
|
|
16
16
|
./examples/grasp_usage.py
|
|
17
17
|
./examples/test_async_context.py
|
|
18
|
-
./examples/test_get_replay_screenshots.py
|
|
19
18
|
./examples/test_grasp_classes.py
|
|
20
19
|
./examples/test_python_script.py
|
|
21
20
|
./examples/test_removed_methods.py
|
|
22
|
-
./examples/test_shutdown_deprecation.py
|
|
23
21
|
./examples/test_terminal_updates.py
|
|
24
22
|
./grasp_sdk/__init__.py
|
|
25
23
|
./grasp_sdk/grasp/__init__.py
|
|
@@ -27,7 +25,6 @@ setup.py
|
|
|
27
25
|
./grasp_sdk/grasp/index.py
|
|
28
26
|
./grasp_sdk/grasp/server.py
|
|
29
27
|
./grasp_sdk/grasp/session.py
|
|
30
|
-
./grasp_sdk/grasp/terminal.py
|
|
31
28
|
./grasp_sdk/grasp/utils.py
|
|
32
29
|
./grasp_sdk/models/__init__.py
|
|
33
30
|
./grasp_sdk/services/__init__.py
|
|
@@ -46,11 +43,9 @@ examples/example_readfile_usage.py
|
|
|
46
43
|
examples/grasp_terminal.py
|
|
47
44
|
examples/grasp_usage.py
|
|
48
45
|
examples/test_async_context.py
|
|
49
|
-
examples/test_get_replay_screenshots.py
|
|
50
46
|
examples/test_grasp_classes.py
|
|
51
47
|
examples/test_python_script.py
|
|
52
48
|
examples/test_removed_methods.py
|
|
53
|
-
examples/test_shutdown_deprecation.py
|
|
54
49
|
examples/test_terminal_updates.py
|
|
55
50
|
grasp_sdk/__init__.py
|
|
56
51
|
grasp_sdk.egg-info/PKG-INFO
|
|
@@ -65,7 +60,6 @@ grasp_sdk/grasp/browser.py
|
|
|
65
60
|
grasp_sdk/grasp/index.py
|
|
66
61
|
grasp_sdk/grasp/server.py
|
|
67
62
|
grasp_sdk/grasp/session.py
|
|
68
|
-
grasp_sdk/grasp/terminal.py
|
|
69
63
|
grasp_sdk/grasp/utils.py
|
|
70
64
|
grasp_sdk/models/__init__.py
|
|
71
65
|
grasp_sdk/services/__init__.py
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""测试 get_replay_screenshots 功能的示例脚本
|
|
3
|
-
|
|
4
|
-
这个脚本演示如何使用 get_replay_screenshots 函数获取截图目录路径。
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import asyncio
|
|
8
|
-
import os
|
|
9
|
-
from grasp_sdk import launch_browser, get_replay_screenshots
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
async def test_get_replay_screenshots():
|
|
13
|
-
"""测试 get_replay_screenshots 功能"""
|
|
14
|
-
print("🚀 启动浏览器服务...")
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
# 启动浏览器并获取连接信息
|
|
18
|
-
connection = await launch_browser({
|
|
19
|
-
'headless': True,
|
|
20
|
-
'type': 'chromium'
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
print(f"✅ 浏览器启动成功!")
|
|
24
|
-
print(f"连接 ID: {connection.id}")
|
|
25
|
-
print(f"WebSocket URL: {connection.ws_url or connection.ws_url}")
|
|
26
|
-
print(f"HTTP URL: {connection.http_url}")
|
|
27
|
-
|
|
28
|
-
# 获取截图目录路径
|
|
29
|
-
print("\n📸 获取截图目录路径...")
|
|
30
|
-
screenshots_dir = await get_replay_screenshots(connection)
|
|
31
|
-
|
|
32
|
-
print(f"✅ 截图目录路径: {screenshots_dir}")
|
|
33
|
-
|
|
34
|
-
# 检查目录是否存在
|
|
35
|
-
if os.path.exists(screenshots_dir):
|
|
36
|
-
print(f"📁 目录已存在")
|
|
37
|
-
|
|
38
|
-
# 列出目录中的文件
|
|
39
|
-
files = os.listdir(screenshots_dir)
|
|
40
|
-
if files:
|
|
41
|
-
print(f"📋 目录中有 {len(files)} 个文件:")
|
|
42
|
-
for file in files[:5]: # 只显示前5个文件
|
|
43
|
-
print(f" - {file}")
|
|
44
|
-
if len(files) > 5:
|
|
45
|
-
print(f" ... 还有 {len(files) - 5} 个文件")
|
|
46
|
-
else:
|
|
47
|
-
print("📂 目录为空")
|
|
48
|
-
else:
|
|
49
|
-
print(f"⚠️ 目录不存在,将在首次使用时创建")
|
|
50
|
-
|
|
51
|
-
# 测试错误处理
|
|
52
|
-
print("\n🧪 测试错误处理...")
|
|
53
|
-
|
|
54
|
-
# 测试缺少 id 字段的情况
|
|
55
|
-
try:
|
|
56
|
-
invalid_connection = {
|
|
57
|
-
'ws_url': connection.ws_url,
|
|
58
|
-
'http_url': connection.http_url
|
|
59
|
-
# 缺少 'id' 字段
|
|
60
|
-
}
|
|
61
|
-
await get_replay_screenshots(invalid_connection)
|
|
62
|
-
except ValueError as e:
|
|
63
|
-
print(f"✅ 正确捕获 ValueError: {e}")
|
|
64
|
-
|
|
65
|
-
# 测试无效 ID 的情况
|
|
66
|
-
try:
|
|
67
|
-
invalid_connection = {
|
|
68
|
-
'id': 'invalid-id-123',
|
|
69
|
-
'ws_url': connection.ws_url,
|
|
70
|
-
'http_url': connection.http_url
|
|
71
|
-
}
|
|
72
|
-
await get_replay_screenshots(invalid_connection)
|
|
73
|
-
except KeyError as e:
|
|
74
|
-
print(f"✅ 正确捕获 KeyError: {e}")
|
|
75
|
-
|
|
76
|
-
print("\n🎉 所有测试完成!")
|
|
77
|
-
|
|
78
|
-
except Exception as e:
|
|
79
|
-
print(f"❌ 测试失败: {e}")
|
|
80
|
-
raise
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if __name__ == "__main__":
|
|
84
|
-
# 检查环境变量
|
|
85
|
-
if not os.getenv('GRASP_KEY'):
|
|
86
|
-
print("⚠️ 警告: 未设置 GRASP_KEY 环境变量")
|
|
87
|
-
print("请设置: export GRASP_KEY=your_api_key")
|
|
88
|
-
|
|
89
|
-
# 运行测试
|
|
90
|
-
asyncio.run(test_get_replay_screenshots())
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
测试 shutdown 方法的废弃警告
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import warnings
|
|
7
|
-
import sys
|
|
8
|
-
import os
|
|
9
|
-
|
|
10
|
-
# 添加项目路径
|
|
11
|
-
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
12
|
-
|
|
13
|
-
from grasp_sdk import shutdown
|
|
14
|
-
|
|
15
|
-
def test_shutdown_deprecation():
|
|
16
|
-
"""测试 shutdown 方法是否正确发出废弃警告"""
|
|
17
|
-
print("🧪 测试 shutdown 方法的废弃警告...")
|
|
18
|
-
|
|
19
|
-
# 设置警告过滤器显示所有警告
|
|
20
|
-
warnings.filterwarnings("always")
|
|
21
|
-
|
|
22
|
-
# 捕获警告
|
|
23
|
-
with warnings.catch_warnings(record=True) as w:
|
|
24
|
-
warnings.simplefilter("always")
|
|
25
|
-
|
|
26
|
-
print("📞 调用 shutdown(None)...")
|
|
27
|
-
|
|
28
|
-
# 调用废弃的 shutdown 方法
|
|
29
|
-
try:
|
|
30
|
-
shutdown(None)
|
|
31
|
-
print("✅ shutdown 方法调用完成")
|
|
32
|
-
except SystemExit:
|
|
33
|
-
print("⚠️ shutdown 方法触发了系统退出")
|
|
34
|
-
except Exception as e:
|
|
35
|
-
print(f"⚠️ shutdown 方法调用出现异常: {e}")
|
|
36
|
-
|
|
37
|
-
# 检查是否有废弃警告
|
|
38
|
-
print(f"\n📊 捕获到 {len(w)} 个警告")
|
|
39
|
-
deprecation_warnings = [warning for warning in w if issubclass(warning.category, DeprecationWarning)]
|
|
40
|
-
|
|
41
|
-
if deprecation_warnings:
|
|
42
|
-
print(f"✅ 检测到 {len(deprecation_warnings)} 个废弃警告:")
|
|
43
|
-
for warning in deprecation_warnings:
|
|
44
|
-
print(f" - {warning.message}")
|
|
45
|
-
print(f" 文件: {warning.filename}:{warning.lineno}")
|
|
46
|
-
else:
|
|
47
|
-
print("❌ 未检测到废弃警告")
|
|
48
|
-
if w:
|
|
49
|
-
print("其他警告:")
|
|
50
|
-
for warning in w:
|
|
51
|
-
print(f" - {warning.category.__name__}: {warning.message}")
|
|
52
|
-
|
|
53
|
-
print("\n📋 废弃方法迁移指南:")
|
|
54
|
-
print(" 旧方式: shutdown(connection)")
|
|
55
|
-
print(" 新方式: await session.close()")
|
|
56
|
-
print("\n💡 建议:")
|
|
57
|
-
print(" - 使用 grasp.launch() 获取 session")
|
|
58
|
-
print(" - 使用 await session.close() 关闭会话")
|
|
59
|
-
print(" - 避免直接使用 shutdown() 方法")
|
|
60
|
-
|
|
61
|
-
if __name__ == '__main__':
|
|
62
|
-
test_shutdown_deprecation()
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
"""GraspBrowser class for browser interface."""
|
|
2
|
-
|
|
3
|
-
import aiohttp
|
|
4
|
-
from typing import Optional, Dict, Any
|
|
5
|
-
|
|
6
|
-
from ..services.browser import CDPConnection
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class GraspBrowser:
|
|
10
|
-
"""Browser interface for Grasp session."""
|
|
11
|
-
|
|
12
|
-
def __init__(self, connection: CDPConnection):
|
|
13
|
-
"""Initialize GraspBrowser.
|
|
14
|
-
|
|
15
|
-
Args:
|
|
16
|
-
connection: CDP connection instance
|
|
17
|
-
"""
|
|
18
|
-
self.connection = connection
|
|
19
|
-
|
|
20
|
-
def get_endpoint(self) -> str:
|
|
21
|
-
"""Get browser WebSocket endpoint URL.
|
|
22
|
-
|
|
23
|
-
Returns:
|
|
24
|
-
WebSocket URL for browser connection
|
|
25
|
-
"""
|
|
26
|
-
return self.connection.ws_url
|
|
27
|
-
|
|
28
|
-
async def get_current_page_target_info(self) -> Optional[Dict[str, Any]]:
|
|
29
|
-
"""Get current page target information.
|
|
30
|
-
|
|
31
|
-
Warning:
|
|
32
|
-
This method is experimental and may change or be removed in future versions.
|
|
33
|
-
Use with caution in production environments.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
Dictionary containing target info and last screenshot, or None if failed
|
|
37
|
-
"""
|
|
38
|
-
host = self.connection.http_url
|
|
39
|
-
api = f"{host}/api/page/info"
|
|
40
|
-
|
|
41
|
-
try:
|
|
42
|
-
async with aiohttp.ClientSession() as session:
|
|
43
|
-
async with session.get(api) as response:
|
|
44
|
-
if not response.ok:
|
|
45
|
-
return None
|
|
46
|
-
|
|
47
|
-
data = await response.json()
|
|
48
|
-
page_info = data.get('pageInfo', {})
|
|
49
|
-
last_screenshot = page_info.get('lastScreenshot')
|
|
50
|
-
session_id = page_info.get('sessionId')
|
|
51
|
-
targets = page_info.get('targets', {})
|
|
52
|
-
|
|
53
|
-
if session_id and session_id in targets:
|
|
54
|
-
target_info = targets[session_id].copy()
|
|
55
|
-
target_info['lastScreenshot'] = last_screenshot
|
|
56
|
-
return target_info
|
|
57
|
-
|
|
58
|
-
return None
|
|
59
|
-
except Exception:
|
|
60
|
-
return None
|
|
61
|
-
|
|
62
|
-
def get_liveview_url(self) -> Optional[str]:
|
|
63
|
-
"""Get liveview URL (TODO: implementation pending).
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
Liveview URL or None
|
|
67
|
-
"""
|
|
68
|
-
# TODO: Implement liveview URL generation
|
|
69
|
-
return None
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
"""GraspSession class for session management."""
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
import websockets
|
|
5
|
-
from .browser import GraspBrowser
|
|
6
|
-
from .terminal import GraspTerminal
|
|
7
|
-
from ..services.browser import CDPConnection
|
|
8
|
-
from ..services.filesystem import FileSystemService
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class GraspSession:
|
|
12
|
-
"""Main session interface providing access to browser, terminal, and files."""
|
|
13
|
-
|
|
14
|
-
def __init__(self, connection: CDPConnection):
|
|
15
|
-
"""Initialize GraspSession.
|
|
16
|
-
|
|
17
|
-
Args:
|
|
18
|
-
connection: CDP connection instance
|
|
19
|
-
"""
|
|
20
|
-
self.connection = connection
|
|
21
|
-
self.browser = GraspBrowser(connection)
|
|
22
|
-
self.terminal = GraspTerminal(connection)
|
|
23
|
-
|
|
24
|
-
# Create files service
|
|
25
|
-
connection_id = connection.id
|
|
26
|
-
try:
|
|
27
|
-
from . import _servers
|
|
28
|
-
server = _servers[connection_id]
|
|
29
|
-
if server.sandbox is None:
|
|
30
|
-
raise RuntimeError('Sandbox is not available for file system service')
|
|
31
|
-
self.files = FileSystemService(server.sandbox, connection)
|
|
32
|
-
except (ImportError, KeyError) as e:
|
|
33
|
-
# In test environment or when server is not available, create a mock files service
|
|
34
|
-
print(f"Warning: Files service not available: {e}")
|
|
35
|
-
raise e
|
|
36
|
-
|
|
37
|
-
# WebSocket connection for keep-alive
|
|
38
|
-
self._ws = None
|
|
39
|
-
self._keep_alive_task = None
|
|
40
|
-
|
|
41
|
-
# Initialize WebSocket connection and start keep-alive
|
|
42
|
-
print('create session...')
|
|
43
|
-
asyncio.create_task(self._initialize_websocket())
|
|
44
|
-
|
|
45
|
-
async def _initialize_websocket(self) -> None:
|
|
46
|
-
"""Initialize WebSocket connection and start keep-alive."""
|
|
47
|
-
try:
|
|
48
|
-
self._ws = await websockets.connect(self.connection.ws_url)
|
|
49
|
-
await self._keep_alive()
|
|
50
|
-
except Exception as e:
|
|
51
|
-
print(f"Failed to initialize WebSocket connection: {e}")
|
|
52
|
-
|
|
53
|
-
async def _keep_alive(self) -> None:
|
|
54
|
-
"""Keep WebSocket connection alive with periodic ping."""
|
|
55
|
-
if self._ws is None:
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
async def ping_loop():
|
|
59
|
-
while self._ws and self._ws.close_code is None:
|
|
60
|
-
try:
|
|
61
|
-
await self._ws.ping()
|
|
62
|
-
await asyncio.sleep(10) # 10 seconds interval, same as TypeScript version
|
|
63
|
-
except Exception as e:
|
|
64
|
-
print(f"Keep-alive ping failed: {e}")
|
|
65
|
-
break
|
|
66
|
-
|
|
67
|
-
self._keep_alive_task = asyncio.create_task(ping_loop())
|
|
68
|
-
|
|
69
|
-
@property
|
|
70
|
-
def id(self) -> str:
|
|
71
|
-
"""Get session ID.
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
Session ID
|
|
75
|
-
"""
|
|
76
|
-
return self.connection.id
|
|
77
|
-
|
|
78
|
-
async def close(self) -> None:
|
|
79
|
-
"""Close the session and cleanup resources."""
|
|
80
|
-
# Stop keep-alive task
|
|
81
|
-
if self._keep_alive_task:
|
|
82
|
-
self._keep_alive_task.cancel()
|
|
83
|
-
try:
|
|
84
|
-
await self._keep_alive_task
|
|
85
|
-
except asyncio.CancelledError:
|
|
86
|
-
pass
|
|
87
|
-
|
|
88
|
-
# Close WebSocket connection with timeout
|
|
89
|
-
if self._ws and self._ws.close_code is None:
|
|
90
|
-
try:
|
|
91
|
-
# Add timeout to prevent hanging on close
|
|
92
|
-
await asyncio.wait_for(self._ws.close(), timeout=5.0)
|
|
93
|
-
print('✅ WebSocket closed successfully')
|
|
94
|
-
except asyncio.TimeoutError:
|
|
95
|
-
print('⚠️ WebSocket close timeout, forcing termination')
|
|
96
|
-
# Force close if timeout
|
|
97
|
-
if hasattr(self._ws, 'transport') and self._ws.transport:
|
|
98
|
-
self._ws.transport.close()
|
|
99
|
-
except Exception as e:
|
|
100
|
-
print(f"Failed to close WebSocket connection: {e}")
|
|
101
|
-
|
|
102
|
-
# Cleanup server resources
|
|
103
|
-
try:
|
|
104
|
-
from . import _servers
|
|
105
|
-
await _servers[self.id].cleanup()
|
|
106
|
-
except (ImportError, KeyError) as e:
|
|
107
|
-
# In test environment or when server is not available, skip cleanup
|
|
108
|
-
print(f"Warning: Server cleanup not available: {e}")
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"""GraspTerminal class for terminal interface."""
|
|
2
|
-
|
|
3
|
-
from ..services.browser import CDPConnection
|
|
4
|
-
from ..services.terminal import TerminalService
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class GraspTerminal:
|
|
8
|
-
"""Terminal interface for Grasp session."""
|
|
9
|
-
|
|
10
|
-
def __init__(self, connection: CDPConnection):
|
|
11
|
-
"""Initialize GraspTerminal.
|
|
12
|
-
|
|
13
|
-
Args:
|
|
14
|
-
connection: CDP connection instance
|
|
15
|
-
"""
|
|
16
|
-
self.connection = connection
|
|
17
|
-
|
|
18
|
-
def create(self) -> TerminalService:
|
|
19
|
-
"""Create a new terminal service instance.
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
TerminalService instance
|
|
23
|
-
|
|
24
|
-
Raises:
|
|
25
|
-
RuntimeError: If sandbox is not available
|
|
26
|
-
"""
|
|
27
|
-
connection_id = self.connection.id
|
|
28
|
-
from . import _servers
|
|
29
|
-
server = _servers[connection_id]
|
|
30
|
-
if server.sandbox is None:
|
|
31
|
-
raise RuntimeError('Sandbox is not available')
|
|
32
|
-
return TerminalService(server.sandbox, self.connection)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|