grasp-sdk 0.1.6__tar.gz → 0.1.7__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.1.6/grasp_sdk.egg-info → grasp_sdk-0.1.7}/PKG-INFO +1 -1
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/__init__.py +2 -7
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/models/__init__.py +0 -2
- grasp_sdk-0.1.7/grasp_sdk/sandbox/http-proxy.mjs +322 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/services/browser.py +14 -14
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/services/sandbox.py +3 -3
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/utils/config.py +0 -4
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7/grasp_sdk.egg-info}/PKG-INFO +1 -1
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk.egg-info/SOURCES.txt +1 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/pyproject.toml +1 -1
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/MANIFEST.in +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/README.md +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/build_and_publish.py +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/example_usage.py +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/sandbox/chrome-stable.mjs +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/sandbox/chromium.mjs +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/services/__init__.py +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/utils/__init__.py +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/utils/auth.py +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk/utils/logger.py +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk.egg-info/dependency_links.txt +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk.egg-info/entry_points.txt +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk.egg-info/not-zip-safe +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk.egg-info/requires.txt +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/grasp_sdk.egg-info/top_level.txt +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/py.typed +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/requirements.txt +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/setup.cfg +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/setup.py +0 -0
- {grasp_sdk-0.1.6 → grasp_sdk-0.1.7}/test_install.py +0 -0
|
@@ -24,7 +24,7 @@ from .models import (
|
|
|
24
24
|
SandboxStatus,
|
|
25
25
|
)
|
|
26
26
|
|
|
27
|
-
__version__ = "0.1.
|
|
27
|
+
__version__ = "0.1.7"
|
|
28
28
|
__author__ = "Grasp Team"
|
|
29
29
|
__email__ = "team@grasp.dev"
|
|
30
30
|
|
|
@@ -76,9 +76,7 @@ class GraspServer:
|
|
|
76
76
|
|
|
77
77
|
self.browser_service: Optional[BrowserService] = None
|
|
78
78
|
|
|
79
|
-
self.logger.info(
|
|
80
|
-
f'GraspE2B initialized (templateId: {config["sandbox"]["templateId"]})'
|
|
81
|
-
)
|
|
79
|
+
self.logger.info('GraspE2B initialized')
|
|
82
80
|
|
|
83
81
|
async def __aenter__(self):
|
|
84
82
|
connection = await self.create_browser_task()
|
|
@@ -147,7 +145,6 @@ class GraspServer:
|
|
|
147
145
|
|
|
148
146
|
# Create base browser config
|
|
149
147
|
browser_config: IBrowserConfig = {
|
|
150
|
-
'cdpPort': 9222,
|
|
151
148
|
'headless': True,
|
|
152
149
|
'launchTimeout': 30000,
|
|
153
150
|
'args': [
|
|
@@ -158,8 +155,6 @@ class GraspServer:
|
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
# Apply user config overrides with type safety
|
|
161
|
-
if 'cdpPort' in config:
|
|
162
|
-
browser_config['cdpPort'] = config['cdpPort']
|
|
163
158
|
if 'headless' in config:
|
|
164
159
|
browser_config['headless'] = config['headless']
|
|
165
160
|
if 'launchTimeout' in config:
|
|
@@ -20,7 +20,6 @@ class SandboxStatus(Enum):
|
|
|
20
20
|
class ISandboxConfig(TypedDict):
|
|
21
21
|
"""Sandbox configuration interface."""
|
|
22
22
|
key: str # Required: Grasp API key
|
|
23
|
-
templateId: str # Required: Sandbox template ID
|
|
24
23
|
timeout: int # Required: Default timeout in milliseconds
|
|
25
24
|
workspace: NotRequired[str] # Optional: Grasp workspace ID
|
|
26
25
|
debug: NotRequired[bool] # Optional: Enable debug mode for detailed logging
|
|
@@ -28,7 +27,6 @@ class ISandboxConfig(TypedDict):
|
|
|
28
27
|
|
|
29
28
|
class IBrowserConfig(TypedDict):
|
|
30
29
|
"""Browser service configuration interface."""
|
|
31
|
-
cdpPort: int # Required: Port for CDP server (default: 9222)
|
|
32
30
|
args: List[str] # Required: Chromium launch arguments
|
|
33
31
|
headless: bool # Required: Headless mode (default: true)
|
|
34
32
|
launchTimeout: int # Required: Timeout for browser launch (default: 30000ms)
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import httpProxy from 'http-proxy';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
|
|
4
|
+
import { Logtail } from '@logtail/node';
|
|
5
|
+
import * as Sentry from "@sentry/node";
|
|
6
|
+
|
|
7
|
+
const logtail = new Logtail(process.env.BS_SOURCE_TOKEN, {
|
|
8
|
+
endpoint: `https://${process.env.BS_INGESTING_HOST}`,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
Sentry.init({
|
|
12
|
+
dsn: process.env.SENTRY_DSN,
|
|
13
|
+
|
|
14
|
+
// Setting this option to true will send default PII data to Sentry.
|
|
15
|
+
// For example, automatic IP address collection on events
|
|
16
|
+
sendDefaultPii: true,
|
|
17
|
+
_experiments: {
|
|
18
|
+
enableLogs: true, // 启用日志功能
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const logger = {
|
|
23
|
+
info: async (message, context) => {
|
|
24
|
+
Sentry.logger.info(message, context);
|
|
25
|
+
return logtail.info(message, context);
|
|
26
|
+
},
|
|
27
|
+
warn: async (message, context) => {
|
|
28
|
+
Sentry.logger.warn(message, context);
|
|
29
|
+
return logtail.warn(message, context);
|
|
30
|
+
},
|
|
31
|
+
error: async (message, context) => {
|
|
32
|
+
Sentry.logger.error(message, context);
|
|
33
|
+
return logtail.error(message, context);
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseWebSocketFrame(buffer) {
|
|
38
|
+
if (buffer.length < 2) {
|
|
39
|
+
throw new Error('Incomplete WebSocket frame.');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const firstByte = buffer.readUInt8(0);
|
|
43
|
+
const fin = (firstByte & 0x80) !== 0;
|
|
44
|
+
const opcode = firstByte & 0x0f;
|
|
45
|
+
|
|
46
|
+
// 仅处理文本帧(opcode 为 0x1)
|
|
47
|
+
if (opcode !== 0x1) {
|
|
48
|
+
throw new Error(`Unsupported opcode: ${opcode}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const secondByte = buffer.readUInt8(1);
|
|
52
|
+
const isMasked = (secondByte & 0x80) !== 0;
|
|
53
|
+
let payloadLength = secondByte & 0x7f;
|
|
54
|
+
let offset = 2;
|
|
55
|
+
|
|
56
|
+
if (payloadLength === 126) {
|
|
57
|
+
if (buffer.length < offset + 2) {
|
|
58
|
+
throw new Error('Incomplete extended payload length.');
|
|
59
|
+
}
|
|
60
|
+
payloadLength = buffer.readUInt16BE(offset);
|
|
61
|
+
offset += 2;
|
|
62
|
+
} else if (payloadLength === 127) {
|
|
63
|
+
if (buffer.length < offset + 8) {
|
|
64
|
+
throw new Error('Incomplete extended payload length.');
|
|
65
|
+
}
|
|
66
|
+
// 注意:JavaScript 无法精确表示超过 2^53 的整数
|
|
67
|
+
const highBits = buffer.readUInt32BE(offset);
|
|
68
|
+
const lowBits = buffer.readUInt32BE(offset + 4);
|
|
69
|
+
payloadLength = highBits * 2 ** 32 + lowBits;
|
|
70
|
+
offset += 8;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let maskingKey;
|
|
74
|
+
if (isMasked) {
|
|
75
|
+
if (buffer.length < offset + 4) {
|
|
76
|
+
throw new Error('Incomplete masking key.');
|
|
77
|
+
}
|
|
78
|
+
maskingKey = buffer.slice(offset, offset + 4);
|
|
79
|
+
offset += 4;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (buffer.length < offset + payloadLength) {
|
|
83
|
+
throw new Error('Incomplete payload data.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const payloadData = buffer.slice(offset, offset + payloadLength);
|
|
87
|
+
|
|
88
|
+
if (isMasked) {
|
|
89
|
+
for (let i = 0; i < payloadLength; i++) {
|
|
90
|
+
payloadData[i] ^= maskingKey[i % 4];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return payloadData.toString('utf8');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sandboxId = process.env.SANDBOX_ID;
|
|
98
|
+
const cdpPort = Number(process.env.CDP_PORT);
|
|
99
|
+
const headless = process.env.HEADLESS !== 'false';
|
|
100
|
+
const enableAdblock = process.env.ADBLOCK !== 'false';
|
|
101
|
+
const timeoutMS = process.env.SANBOX_TIMEOUT;
|
|
102
|
+
const workspace = process.env.WORKSPACE;
|
|
103
|
+
const keepAliveMS = Number(process.env.KEEP_ALIVE_MS) || 0;
|
|
104
|
+
const args = [];
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// 创建代理服务:从 ${this.config.cdpPort! + 1} 转发到 127.0.0.1:${this.config.cdpPort!}
|
|
108
|
+
const proxy = httpProxy.createProxyServer({
|
|
109
|
+
target: `http://127.0.0.1:${cdpPort}`,
|
|
110
|
+
ws: true, // 支持 WebSocket
|
|
111
|
+
changeOrigin: true
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const clients = new Set();
|
|
115
|
+
|
|
116
|
+
// 监听 WebSocket 事件
|
|
117
|
+
proxy.on('open', () => {
|
|
118
|
+
console.log('🔌 CDP WebSocket connection established');
|
|
119
|
+
const wsId = Date.now();
|
|
120
|
+
logger.info('CDP WebSocket connection established', { sandboxId, wsId });
|
|
121
|
+
Sentry.addBreadcrumb({
|
|
122
|
+
category: 'websocket',
|
|
123
|
+
message: 'CDP connection established',
|
|
124
|
+
level: 'info',
|
|
125
|
+
data: { sandboxId }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
clients.add(wsId);
|
|
129
|
+
proxy.once('close', async (req, socket, head) => {
|
|
130
|
+
console.log('🔒 CDP WebSocket connection closed');
|
|
131
|
+
await logger.info('CDP WebSocket connection closed', { sandboxId });
|
|
132
|
+
Sentry.addBreadcrumb({
|
|
133
|
+
category: 'websocket',
|
|
134
|
+
message: 'CDP connection closed',
|
|
135
|
+
level: 'info',
|
|
136
|
+
data: { sandboxId }
|
|
137
|
+
});
|
|
138
|
+
clients.delete(wsId);
|
|
139
|
+
|
|
140
|
+
if(clients.size <= 0) {
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
if(clients.size <= 0) {
|
|
143
|
+
console.log('❌ Force closed...', wsId);
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
}, keepAliveMS);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
proxy.on('proxyReqWs', (proxyReq, req, socket, options, head) => {
|
|
152
|
+
console.log('📡 New CDP WebSocket connection request:', req.url);
|
|
153
|
+
logger.info('New CDP WebSocket connection request', { url: req.url, sandboxId });
|
|
154
|
+
Sentry.addBreadcrumb({
|
|
155
|
+
category: 'websocket',
|
|
156
|
+
message: 'New CDP connection request',
|
|
157
|
+
level: 'info',
|
|
158
|
+
data: { url: req.url, sandboxId }
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
proxy.on('error', (err, req, res) => {
|
|
163
|
+
console.error('❌ CDP WebSocket proxy error:', err);
|
|
164
|
+
logger.error('CDP WebSocket proxy error', { error: err.message, url: req?.url, sandboxId });
|
|
165
|
+
Sentry.captureException(err, {
|
|
166
|
+
tags: { type: 'websocket_proxy_error', sandboxId },
|
|
167
|
+
extra: { url: req?.url }
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const server = http.createServer(async (req, res) => {
|
|
172
|
+
if (req.url === '/health') {
|
|
173
|
+
res.end('ok');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (req.url === '/json/version' || req.url === '/json/version/') {
|
|
177
|
+
try {
|
|
178
|
+
// 向本地 CDP 发请求,获取原始 JSON
|
|
179
|
+
const jsonRes = await fetch(`http://127.0.0.1:${cdpPort}/json/version`);
|
|
180
|
+
const data = await jsonRes.json();
|
|
181
|
+
// 替换掉本地的 WebSocket 地址为代理暴露地址
|
|
182
|
+
data.webSocketDebuggerUrl = data.webSocketDebuggerUrl.replace(
|
|
183
|
+
`ws://127.0.0.1:${cdpPort}`,
|
|
184
|
+
`wss://${req.headers.host}`
|
|
185
|
+
);
|
|
186
|
+
await logger.info('CDP version info requested', { url: req.url, response: data, sandboxId });
|
|
187
|
+
Sentry.addBreadcrumb({
|
|
188
|
+
category: 'http',
|
|
189
|
+
message: 'CDP version info requested',
|
|
190
|
+
level: 'info',
|
|
191
|
+
data: { url: req.url, response: data, sandboxId }
|
|
192
|
+
});
|
|
193
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
194
|
+
res.end(JSON.stringify(data));
|
|
195
|
+
} catch(ex) {
|
|
196
|
+
console.error('Failed to fetch CDP version:', ex.message);
|
|
197
|
+
await logger.error('Failed to fetch CDP version', { error: ex.message, sandboxId });
|
|
198
|
+
Sentry.captureException(ex, {
|
|
199
|
+
tags: { type: 'cdp_version_error', sandboxId }
|
|
200
|
+
});
|
|
201
|
+
res.writeHead(500);
|
|
202
|
+
res.end('Internal Server Error');
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
proxy.web(req, res, {}, async (err) => {
|
|
206
|
+
console.error('Proxy error:', err);
|
|
207
|
+
await logger.error('HTTP proxy error', { error: err.message, url: req.url, sandboxId });
|
|
208
|
+
Sentry.captureException(err, {
|
|
209
|
+
tags: { type: 'proxy_error', sandboxId },
|
|
210
|
+
extra: { url: req.url }
|
|
211
|
+
});
|
|
212
|
+
res.writeHead(502);
|
|
213
|
+
res.end('Bad gateway');
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
server.on('upgrade', (req, socket, head) => {
|
|
219
|
+
// 监听 WebSocket 数据
|
|
220
|
+
let _buffers = [];
|
|
221
|
+
socket.on('data', (data) => {
|
|
222
|
+
let message = '';
|
|
223
|
+
try {
|
|
224
|
+
_buffers.push(data);
|
|
225
|
+
// console.log(`💬 ${_buffers.length}`);
|
|
226
|
+
message = parseWebSocketFrame(Buffer.concat(_buffers)); // 复制data不能破坏原始数据
|
|
227
|
+
_buffers.length = 0;
|
|
228
|
+
if (message.startsWith('{')){ // 只解析 JSON 消息
|
|
229
|
+
const parsed = JSON.parse(message);
|
|
230
|
+
console.log('📨 CDP WebSocket message:', parsed);
|
|
231
|
+
logger.info('CDP WebSocket message received', {
|
|
232
|
+
data: parsed,
|
|
233
|
+
sandboxId: process.env.SANDBOX_ID,
|
|
234
|
+
});
|
|
235
|
+
Sentry.addBreadcrumb({
|
|
236
|
+
category: 'websocket',
|
|
237
|
+
message: 'CDP message received',
|
|
238
|
+
level: 'debug',
|
|
239
|
+
data: { ...parsed, sandboxId }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
const msg = err.message;
|
|
244
|
+
if(!msg.includes('Incomplete')) {
|
|
245
|
+
// 记录解析错误
|
|
246
|
+
console.warn('⚠️ Failed to parse CDP WebSocket message:', err.message, _buffers.length);
|
|
247
|
+
_buffers.length = 0;
|
|
248
|
+
Sentry.captureException(err, {
|
|
249
|
+
tags: { type: 'websocket_error', sandboxId }
|
|
250
|
+
});
|
|
251
|
+
logger.warn('Failed to parse CDP WebSocket message', {
|
|
252
|
+
error: err.message,
|
|
253
|
+
data: message,
|
|
254
|
+
sandboxId: process.env.SANDBOX_ID
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
socket.on('error', (err) => {
|
|
261
|
+
console.error('❌ CDP WebSocket error:', err);
|
|
262
|
+
logger.error('CDP WebSocket error', { error: err.message, sandboxId });
|
|
263
|
+
Sentry.captureException(err, {
|
|
264
|
+
tags: { type: 'websocket_error', sandboxId }
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
proxy.ws(req, socket, head);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
server.listen(cdpPort + 1, '0.0.0.0', () => {
|
|
272
|
+
console.log(`🎯 Proxy server listening on http://0.0.0.0:${cdpPort + 1} → http://127.0.0.1:${cdpPort}`);
|
|
273
|
+
logger.info('Proxy server started', {
|
|
274
|
+
port: cdpPort + 1,
|
|
275
|
+
target: cdpPort,
|
|
276
|
+
sandboxId,
|
|
277
|
+
settings: {
|
|
278
|
+
type: 'chromium',
|
|
279
|
+
args,
|
|
280
|
+
headless,
|
|
281
|
+
enableAdblock,
|
|
282
|
+
timeoutMS,
|
|
283
|
+
workspace,
|
|
284
|
+
sandboxId
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
Sentry.addBreadcrumb({
|
|
288
|
+
category: 'server',
|
|
289
|
+
message: 'Proxy server started',
|
|
290
|
+
level: 'info',
|
|
291
|
+
data: {
|
|
292
|
+
port: cdpPort + 1,
|
|
293
|
+
target: cdpPort,
|
|
294
|
+
sandboxId,
|
|
295
|
+
settings: {
|
|
296
|
+
type: 'chromium',
|
|
297
|
+
args,
|
|
298
|
+
headless,
|
|
299
|
+
enableAdblock,
|
|
300
|
+
timeoutMS,
|
|
301
|
+
workspace,
|
|
302
|
+
sandboxId
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
} catch (ex) {
|
|
308
|
+
console.error('Failed to launch Chromium:', ex);
|
|
309
|
+
logger.error('Failed to launch Chrome', {
|
|
310
|
+
error: ex.message,
|
|
311
|
+
args,
|
|
312
|
+
headless,
|
|
313
|
+
cdpPort,
|
|
314
|
+
enableAdblock,
|
|
315
|
+
sandboxId,
|
|
316
|
+
});
|
|
317
|
+
Sentry.captureException(ex, {
|
|
318
|
+
tags: { type: 'launch_error', sandboxId },
|
|
319
|
+
extra: { args, headless, cdpPort, enableAdblock }
|
|
320
|
+
});
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
@@ -63,7 +63,6 @@ class BrowserService:
|
|
|
63
63
|
|
|
64
64
|
# Set default browser config
|
|
65
65
|
default_config: IBrowserConfig = {
|
|
66
|
-
'cdpPort': 9222,
|
|
67
66
|
'headless': True,
|
|
68
67
|
'launchTimeout': 30000,
|
|
69
68
|
'args': [],
|
|
@@ -99,7 +98,7 @@ class BrowserService:
|
|
|
99
98
|
Promise that resolves when sandbox is ready
|
|
100
99
|
"""
|
|
101
100
|
self.logger.info('Initializing Browser service')
|
|
102
|
-
await self.sandbox_service.create_sandbox()
|
|
101
|
+
await self.sandbox_service.create_sandbox('grasp-run-template')
|
|
103
102
|
self.logger.info('Grasp sandbox initialized successfully')
|
|
104
103
|
|
|
105
104
|
async def launch_browser(
|
|
@@ -122,8 +121,8 @@ class BrowserService:
|
|
|
122
121
|
|
|
123
122
|
try:
|
|
124
123
|
self.logger.info(
|
|
125
|
-
f'Launching Chromium browser with CDP (port:
|
|
126
|
-
|
|
124
|
+
f'Launching Chromium browser with CDP (port: 9222, headless: {self.config["headless"]})')
|
|
125
|
+
|
|
127
126
|
|
|
128
127
|
# Check if adblock is enabled and adjust browser type
|
|
129
128
|
if (
|
|
@@ -136,7 +135,7 @@ class BrowserService:
|
|
|
136
135
|
browser_type = 'chrome-stable'
|
|
137
136
|
|
|
138
137
|
# Read the Playwright script
|
|
139
|
-
script_path = Path(__file__).parent.parent / 'sandbox' /
|
|
138
|
+
script_path = Path(__file__).parent.parent / 'sandbox' / 'http-proxy.mjs'
|
|
140
139
|
|
|
141
140
|
try:
|
|
142
141
|
with open(script_path, 'r', encoding='utf-8') as f:
|
|
@@ -146,7 +145,7 @@ class BrowserService:
|
|
|
146
145
|
|
|
147
146
|
# Prepare environment variables
|
|
148
147
|
envs = {
|
|
149
|
-
'CDP_PORT':
|
|
148
|
+
'CDP_PORT': '9222',
|
|
150
149
|
'BROWSER_ARGS': json.dumps(self.config['args']),
|
|
151
150
|
'LAUNCH_TIMEOUT': str(self.config['launchTimeout']),
|
|
152
151
|
'SANDBOX_TIMEOUT': str(self.sandbox_service.timeout),
|
|
@@ -187,7 +186,7 @@ class BrowserService:
|
|
|
187
186
|
self.cdp_connection = result
|
|
188
187
|
|
|
189
188
|
self.logger.info(
|
|
190
|
-
f'Chromium browser launched successfully (cdpPort:
|
|
189
|
+
f'Chromium browser launched successfully (cdpPort: 9222, wsUrl: {self.cdp_connection.ws_url})'
|
|
191
190
|
)
|
|
192
191
|
|
|
193
192
|
# Start health check if not in debug mode
|
|
@@ -238,7 +237,7 @@ class BrowserService:
|
|
|
238
237
|
Raises:
|
|
239
238
|
RuntimeError: If CDP server fails to become ready within timeout
|
|
240
239
|
"""
|
|
241
|
-
delay_ms =
|
|
240
|
+
delay_ms = 100
|
|
242
241
|
max_attempts = self.config['launchTimeout'] // delay_ms
|
|
243
242
|
|
|
244
243
|
for attempt in range(1, max_attempts + 1):
|
|
@@ -246,6 +245,10 @@ class BrowserService:
|
|
|
246
245
|
self.logger.debug(
|
|
247
246
|
f'Checking CDP availability (attempt {attempt}/{max_attempts})'
|
|
248
247
|
)
|
|
248
|
+
|
|
249
|
+
host = self.sandbox_service.get_sandbox_host(
|
|
250
|
+
9223
|
|
251
|
+
)
|
|
249
252
|
|
|
250
253
|
# Check if CDP endpoint is responding
|
|
251
254
|
options: ICommandOptions = {
|
|
@@ -253,7 +256,7 @@ class BrowserService:
|
|
|
253
256
|
'inBackground': False
|
|
254
257
|
}
|
|
255
258
|
result = await self.sandbox_service.run_command(
|
|
256
|
-
f'curl -s
|
|
259
|
+
f'curl -s https://{host}/json/version',
|
|
257
260
|
options,
|
|
258
261
|
True,
|
|
259
262
|
)
|
|
@@ -264,15 +267,12 @@ class BrowserService:
|
|
|
264
267
|
):
|
|
265
268
|
stdout_content = getattr(result, 'stdout', '')
|
|
266
269
|
metadata = json.loads(stdout_content)
|
|
267
|
-
host = self.sandbox_service.get_sandbox_host(
|
|
268
|
-
self.config['cdpPort'] + 1
|
|
269
|
-
)
|
|
270
270
|
|
|
271
271
|
# Update URLs for external access
|
|
272
272
|
ws_url = metadata['webSocketDebuggerUrl'].replace(
|
|
273
273
|
'ws://', 'wss://'
|
|
274
274
|
).replace(
|
|
275
|
-
f'localhost:
|
|
275
|
+
f'localhost:9222', host
|
|
276
276
|
)
|
|
277
277
|
|
|
278
278
|
http_url = f'https://{host}'
|
|
@@ -280,7 +280,7 @@ class BrowserService:
|
|
|
280
280
|
connection = CDPConnection(
|
|
281
281
|
ws_url=ws_url,
|
|
282
282
|
http_url=http_url,
|
|
283
|
-
port=
|
|
283
|
+
port=9222
|
|
284
284
|
)
|
|
285
285
|
|
|
286
286
|
self.logger.info(f'CDP server is ready (metadata: {metadata})')
|
|
@@ -123,7 +123,7 @@ class SandboxService:
|
|
|
123
123
|
"""Get timeout value."""
|
|
124
124
|
return self.config['timeout']
|
|
125
125
|
|
|
126
|
-
async def create_sandbox(self) -> None:
|
|
126
|
+
async def create_sandbox(self, template_id: str) -> None:
|
|
127
127
|
"""
|
|
128
128
|
Creates and starts a new sandbox.
|
|
129
129
|
|
|
@@ -143,11 +143,11 @@ class SandboxService:
|
|
|
143
143
|
|
|
144
144
|
# Create sandbox
|
|
145
145
|
self.logger.info('Creating Grasp sandbox', {
|
|
146
|
-
'templateId':
|
|
146
|
+
'templateId': template_id,
|
|
147
147
|
})
|
|
148
148
|
# Use Sandbox constructor directly (e2b SDK 1.5.3+)
|
|
149
149
|
self.sandbox = await AsyncSandbox.create(
|
|
150
|
-
template=
|
|
150
|
+
template=template_id,
|
|
151
151
|
api_key=api_key,
|
|
152
152
|
timeout=self.config['timeout'] // 1000 # Convert ms to seconds
|
|
153
153
|
)
|
|
@@ -23,7 +23,6 @@ def get_config() -> Dict[str, Any]:
|
|
|
23
23
|
return {
|
|
24
24
|
'sandbox': {
|
|
25
25
|
'key': os.getenv('GRASP_KEY', ''),
|
|
26
|
-
'templateId': 'playwright-pnpm-template',
|
|
27
26
|
'timeout': int(os.getenv('GRASP_SERVICE_TIMEOUT', '900000')),
|
|
28
27
|
'debug': os.getenv('GRASP_DEBUG', 'false').lower() == 'true',
|
|
29
28
|
},
|
|
@@ -44,7 +43,6 @@ def get_sandbox_config() -> ISandboxConfig:
|
|
|
44
43
|
config = get_config()
|
|
45
44
|
sandbox_config = ISandboxConfig(
|
|
46
45
|
key=config['sandbox']['key'],
|
|
47
|
-
templateId=config['sandbox']['templateId'],
|
|
48
46
|
timeout=config['sandbox']['timeout'],
|
|
49
47
|
debug=config['sandbox'].get('debug', False),
|
|
50
48
|
)
|
|
@@ -64,7 +62,6 @@ def get_browser_config() -> IBrowserConfig:
|
|
|
64
62
|
IBrowserConfig: Browser configuration object.
|
|
65
63
|
"""
|
|
66
64
|
return IBrowserConfig(
|
|
67
|
-
cdpPort=int(os.getenv('GRASP_CDP_PORT', '9222')),
|
|
68
65
|
args=[
|
|
69
66
|
'--no-sandbox',
|
|
70
67
|
'--disable-setuid-sandbox',
|
|
@@ -102,7 +99,6 @@ ENV_VARS = {
|
|
|
102
99
|
'GRASP_DEBUG': 'GRASP_DEBUG',
|
|
103
100
|
'GRASP_LOG_LEVEL': 'GRASP_LOG_LEVEL',
|
|
104
101
|
'GRASP_LOG_FILE': 'GRASP_LOG_FILE',
|
|
105
|
-
'GRASP_CDP_PORT': 'GRASP_CDP_PORT',
|
|
106
102
|
'GRASP_HEADLESS': 'GRASP_HEADLESS',
|
|
107
103
|
'GRASP_LAUNCH_TIMEOUT': 'GRASP_LAUNCH_TIMEOUT',
|
|
108
104
|
}
|
|
@@ -32,6 +32,7 @@ grasp_sdk.egg-info/top_level.txt
|
|
|
32
32
|
grasp_sdk/models/__init__.py
|
|
33
33
|
grasp_sdk/sandbox/chrome-stable.mjs
|
|
34
34
|
grasp_sdk/sandbox/chromium.mjs
|
|
35
|
+
grasp_sdk/sandbox/http-proxy.mjs
|
|
35
36
|
grasp_sdk/services/__init__.py
|
|
36
37
|
grasp_sdk/services/browser.py
|
|
37
38
|
grasp_sdk/services/sandbox.py
|
|
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
|