vibesurf 0.1.26__py3-none-any.whl → 0.1.28__py3-none-any.whl
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 vibesurf might be problematic. Click here for more details.
- vibe_surf/_version.py +2 -2
- vibe_surf/agents/vibe_surf_agent.py +4 -5
- vibe_surf/browser/agent_browser_session.py +26 -0
- vibe_surf/tools/browser_use_tools.py +168 -1
- vibe_surf/tools/vibesurf_tools.py +425 -3
- vibe_surf/tools/views.py +75 -0
- vibe_surf/tools/website_api/__init__.py +0 -0
- vibe_surf/tools/website_api/douyin/__init__.py +0 -0
- vibe_surf/tools/website_api/douyin/client.py +845 -0
- vibe_surf/tools/website_api/douyin/helpers.py +239 -0
- vibe_surf/tools/website_api/weibo/__init__.py +0 -0
- vibe_surf/tools/website_api/weibo/client.py +846 -0
- vibe_surf/tools/website_api/weibo/helpers.py +997 -0
- vibe_surf/tools/website_api/xhs/__init__.py +0 -0
- vibe_surf/tools/website_api/xhs/client.py +807 -0
- vibe_surf/tools/website_api/xhs/helpers.py +301 -0
- vibe_surf/tools/website_api/youtube/__init__.py +32 -0
- vibe_surf/tools/website_api/youtube/client.py +1179 -0
- vibe_surf/tools/website_api/youtube/helpers.py +420 -0
- {vibesurf-0.1.26.dist-info → vibesurf-0.1.28.dist-info}/METADATA +26 -5
- {vibesurf-0.1.26.dist-info → vibesurf-0.1.28.dist-info}/RECORD +25 -12
- vibesurf-0.1.28.dist-info/licenses/LICENSE +22 -0
- vibesurf-0.1.26.dist-info/licenses/LICENSE +0 -201
- {vibesurf-0.1.26.dist-info → vibesurf-0.1.28.dist-info}/WHEEL +0 -0
- {vibesurf-0.1.26.dist-info → vibesurf-0.1.28.dist-info}/entry_points.txt +0 -0
- {vibesurf-0.1.26.dist-info → vibesurf-0.1.28.dist-info}/top_level.txt +0 -0
vibe_surf/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.28'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 28)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -479,17 +479,16 @@ async def _vibesurf_agent_node_impl(state: VibeSurfState) -> VibeSurfState:
|
|
|
479
479
|
llm=vibesurf_agent.llm,
|
|
480
480
|
file_system=vibesurf_agent.file_system,
|
|
481
481
|
)
|
|
482
|
-
if action_name
|
|
482
|
+
if action_name.startswith("skill_"):
|
|
483
483
|
state.current_step = "END"
|
|
484
484
|
# Format final response
|
|
485
|
-
final_response = f"{result.extracted_content}"
|
|
486
|
-
await log_agent_activity(state, agent_name, "result", final_response)
|
|
485
|
+
final_response = f"{result.extracted_content}" or f"{result.error}"
|
|
487
486
|
state.final_response = final_response
|
|
488
487
|
logger.debug(final_response)
|
|
489
488
|
state.is_complete = True
|
|
490
|
-
|
|
489
|
+
else:
|
|
490
|
+
state.current_step = "vibesurf_agent"
|
|
491
491
|
|
|
492
|
-
state.current_step = "vibesurf_agent"
|
|
493
492
|
if result.extracted_content:
|
|
494
493
|
vibesurf_agent.message_history.append(
|
|
495
494
|
UserMessage(content=f'Action result:\n{result.extracted_content}'))
|
|
@@ -384,6 +384,32 @@ class AgentBrowserSession(BrowserSession):
|
|
|
384
384
|
)
|
|
385
385
|
]
|
|
386
386
|
|
|
387
|
+
def model_post_init(self, __context) -> None:
|
|
388
|
+
"""Register event handlers after model initialization."""
|
|
389
|
+
# Check if handlers are already registered to prevent duplicates
|
|
390
|
+
|
|
391
|
+
from browser_use.browser.watchdog_base import BaseWatchdog
|
|
392
|
+
|
|
393
|
+
start_handlers = self.event_bus.handlers.get('BrowserStartEvent', [])
|
|
394
|
+
start_handler_names = [getattr(h, '__name__', str(h)) for h in start_handlers]
|
|
395
|
+
|
|
396
|
+
if any('on_BrowserStartEvent' in name for name in start_handler_names):
|
|
397
|
+
raise RuntimeError(
|
|
398
|
+
'[BrowserSession] Duplicate handler registration attempted! '
|
|
399
|
+
'on_BrowserStartEvent is already registered. '
|
|
400
|
+
'This likely means BrowserSession was initialized multiple times with the same EventBus.'
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
BaseWatchdog.attach_handler_to_session(self, BrowserStartEvent, self.on_BrowserStartEvent)
|
|
404
|
+
BaseWatchdog.attach_handler_to_session(self, BrowserStopEvent, self.on_BrowserStopEvent)
|
|
405
|
+
BaseWatchdog.attach_handler_to_session(self, NavigateToUrlEvent, self.on_NavigateToUrlEvent)
|
|
406
|
+
BaseWatchdog.attach_handler_to_session(self, SwitchTabEvent, self.on_SwitchTabEvent)
|
|
407
|
+
BaseWatchdog.attach_handler_to_session(self, TabCreatedEvent, self.on_TabCreatedEvent)
|
|
408
|
+
BaseWatchdog.attach_handler_to_session(self, TabClosedEvent, self.on_TabClosedEvent)
|
|
409
|
+
BaseWatchdog.attach_handler_to_session(self, AgentFocusChangedEvent, self.on_AgentFocusChangedEvent)
|
|
410
|
+
# BaseWatchdog.attach_handler_to_session(self, FileDownloadedEvent, self.on_FileDownloadedEvent)
|
|
411
|
+
BaseWatchdog.attach_handler_to_session(self, CloseTabEvent, self.on_CloseTabEvent)
|
|
412
|
+
|
|
387
413
|
async def attach_all_watchdogs(self) -> None:
|
|
388
414
|
"""Initialize and attach all watchdogs EXCEPT AboutBlankWatchdog to disable DVD animation."""
|
|
389
415
|
# Prevent duplicate watchdog attachment
|
|
@@ -6,6 +6,9 @@ import enum
|
|
|
6
6
|
import base64
|
|
7
7
|
import mimetypes
|
|
8
8
|
import datetime
|
|
9
|
+
import aiohttp
|
|
10
|
+
import re
|
|
11
|
+
import urllib.parse
|
|
9
12
|
from pathvalidate import sanitize_filename
|
|
10
13
|
from typing import Optional, Type, Callable, Dict, Any, Union, Awaitable, TypeVar
|
|
11
14
|
from pydantic import BaseModel
|
|
@@ -40,7 +43,7 @@ from browser_use.browser.views import BrowserError
|
|
|
40
43
|
from browser_use.mcp.client import MCPClient
|
|
41
44
|
|
|
42
45
|
from vibe_surf.browser.agent_browser_session import AgentBrowserSession
|
|
43
|
-
from vibe_surf.tools.views import HoverAction, ExtractionAction, FileExtractionAction
|
|
46
|
+
from vibe_surf.tools.views import HoverAction, ExtractionAction, FileExtractionAction, DownloadMediaAction
|
|
44
47
|
from vibe_surf.tools.mcp_client import CustomMCPClient
|
|
45
48
|
from vibe_surf.tools.file_system import CustomFileSystem
|
|
46
49
|
from vibe_surf.logger import get_logger
|
|
@@ -501,3 +504,167 @@ class BrowserUseTools(Tools, VibeSurfTools):
|
|
|
501
504
|
error_msg = f'❌ Failed to take screenshot: {str(e)}'
|
|
502
505
|
logger.error(error_msg)
|
|
503
506
|
return ActionResult(error=error_msg)
|
|
507
|
+
|
|
508
|
+
@self.registry.action(
|
|
509
|
+
'Download media from URL and save to filesystem downloads folder',
|
|
510
|
+
param_model=DownloadMediaAction
|
|
511
|
+
)
|
|
512
|
+
async def download_media(params: DownloadMediaAction, file_system: FileSystem):
|
|
513
|
+
"""Download media from URL with automatic file format detection"""
|
|
514
|
+
try:
|
|
515
|
+
# Get file system directory path (Path type)
|
|
516
|
+
fs_dir = file_system.get_dir()
|
|
517
|
+
|
|
518
|
+
# Create downloads directory if it doesn't exist
|
|
519
|
+
downloads_dir = fs_dir / "downloads"
|
|
520
|
+
downloads_dir.mkdir(exist_ok=True)
|
|
521
|
+
|
|
522
|
+
# Download the file and detect format
|
|
523
|
+
async with aiohttp.ClientSession() as session:
|
|
524
|
+
async with session.get(params.url) as response:
|
|
525
|
+
if response.status != 200:
|
|
526
|
+
raise Exception(f"HTTP {response.status}: Failed to download from {params.url}")
|
|
527
|
+
|
|
528
|
+
# Get content
|
|
529
|
+
content = await response.read()
|
|
530
|
+
|
|
531
|
+
# Detect file format and extension
|
|
532
|
+
file_extension = await self._detect_file_format(params.url, response.headers, content)
|
|
533
|
+
|
|
534
|
+
# Generate filename
|
|
535
|
+
if params.filename:
|
|
536
|
+
# Use provided filename, add extension if missing
|
|
537
|
+
filename = params.filename
|
|
538
|
+
if not filename.endswith(file_extension):
|
|
539
|
+
filename = f"{filename}{file_extension}"
|
|
540
|
+
else:
|
|
541
|
+
# Generate filename from URL or timestamp
|
|
542
|
+
url_path = urllib.parse.urlparse(params.url).path
|
|
543
|
+
url_filename = os.path.basename(url_path)
|
|
544
|
+
|
|
545
|
+
if url_filename and not url_filename.startswith('.'):
|
|
546
|
+
# Use URL filename, ensure correct extension
|
|
547
|
+
filename = url_filename
|
|
548
|
+
if not filename.endswith(file_extension):
|
|
549
|
+
base_name = os.path.splitext(filename)[0]
|
|
550
|
+
filename = f"{base_name}{file_extension}"
|
|
551
|
+
else:
|
|
552
|
+
# Generate timestamp-based filename
|
|
553
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
554
|
+
filename = f"media_{timestamp}{file_extension}"
|
|
555
|
+
|
|
556
|
+
# Sanitize filename
|
|
557
|
+
filename = sanitize_filename(filename)
|
|
558
|
+
filepath = downloads_dir / filename
|
|
559
|
+
|
|
560
|
+
# Save file
|
|
561
|
+
with open(filepath, "wb") as f:
|
|
562
|
+
f.write(content)
|
|
563
|
+
|
|
564
|
+
# Calculate file size for display
|
|
565
|
+
file_size = len(content)
|
|
566
|
+
size_str = self._format_file_size(file_size)
|
|
567
|
+
|
|
568
|
+
msg = f'📥 Downloaded media to: {str(filepath.relative_to(fs_dir))} ({size_str})'
|
|
569
|
+
logger.info(msg)
|
|
570
|
+
return ActionResult(
|
|
571
|
+
extracted_content=msg,
|
|
572
|
+
include_in_memory=True,
|
|
573
|
+
long_term_memory=f'Downloaded media from {params.url} to {str(filepath.relative_to(fs_dir))}',
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
except Exception as e:
|
|
577
|
+
error_msg = f'❌ Failed to download media: {str(e)}'
|
|
578
|
+
logger.error(error_msg)
|
|
579
|
+
return ActionResult(error=error_msg)
|
|
580
|
+
|
|
581
|
+
async def _detect_file_format(self, url: str, headers: dict, content: bytes) -> str:
|
|
582
|
+
"""Detect file format from URL, headers, and content"""
|
|
583
|
+
|
|
584
|
+
# Try Content-Type header first
|
|
585
|
+
content_type = headers.get('content-type', '').lower()
|
|
586
|
+
if content_type:
|
|
587
|
+
# Common image formats
|
|
588
|
+
if 'image/jpeg' in content_type or 'image/jpg' in content_type:
|
|
589
|
+
return '.jpg'
|
|
590
|
+
elif 'image/png' in content_type:
|
|
591
|
+
return '.png'
|
|
592
|
+
elif 'image/gif' in content_type:
|
|
593
|
+
return '.gif'
|
|
594
|
+
elif 'image/webp' in content_type:
|
|
595
|
+
return '.webp'
|
|
596
|
+
elif 'image/svg' in content_type:
|
|
597
|
+
return '.svg'
|
|
598
|
+
elif 'image/bmp' in content_type:
|
|
599
|
+
return '.bmp'
|
|
600
|
+
elif 'image/tiff' in content_type:
|
|
601
|
+
return '.tiff'
|
|
602
|
+
# Video formats
|
|
603
|
+
elif 'video/mp4' in content_type:
|
|
604
|
+
return '.mp4'
|
|
605
|
+
elif 'video/webm' in content_type:
|
|
606
|
+
return '.webm'
|
|
607
|
+
elif 'video/avi' in content_type:
|
|
608
|
+
return '.avi'
|
|
609
|
+
elif 'video/mov' in content_type or 'video/quicktime' in content_type:
|
|
610
|
+
return '.mov'
|
|
611
|
+
# Audio formats
|
|
612
|
+
elif 'audio/mpeg' in content_type or 'audio/mp3' in content_type:
|
|
613
|
+
return '.mp3'
|
|
614
|
+
elif 'audio/wav' in content_type:
|
|
615
|
+
return '.wav'
|
|
616
|
+
elif 'audio/ogg' in content_type:
|
|
617
|
+
return '.ogg'
|
|
618
|
+
elif 'audio/webm' in content_type:
|
|
619
|
+
return '.webm'
|
|
620
|
+
|
|
621
|
+
# Try magic number detection
|
|
622
|
+
if len(content) >= 8:
|
|
623
|
+
# JPEG
|
|
624
|
+
if content.startswith(b'\xff\xd8\xff'):
|
|
625
|
+
return '.jpg'
|
|
626
|
+
# PNG
|
|
627
|
+
elif content.startswith(b'\x89PNG\r\n\x1a\n'):
|
|
628
|
+
return '.png'
|
|
629
|
+
# GIF
|
|
630
|
+
elif content.startswith(b'GIF87a') or content.startswith(b'GIF89a'):
|
|
631
|
+
return '.gif'
|
|
632
|
+
# WebP
|
|
633
|
+
elif content[8:12] == b'WEBP':
|
|
634
|
+
return '.webp'
|
|
635
|
+
# BMP
|
|
636
|
+
elif content.startswith(b'BM'):
|
|
637
|
+
return '.bmp'
|
|
638
|
+
# TIFF
|
|
639
|
+
elif content.startswith(b'II*\x00') or content.startswith(b'MM\x00*'):
|
|
640
|
+
return '.tiff'
|
|
641
|
+
# MP4
|
|
642
|
+
elif b'ftyp' in content[4:12]:
|
|
643
|
+
return '.mp4'
|
|
644
|
+
# PDF
|
|
645
|
+
elif content.startswith(b'%PDF'):
|
|
646
|
+
return '.pdf'
|
|
647
|
+
|
|
648
|
+
# Try URL path extension
|
|
649
|
+
url_path = urllib.parse.urlparse(url).path
|
|
650
|
+
if url_path:
|
|
651
|
+
ext = os.path.splitext(url_path)[1].lower()
|
|
652
|
+
if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.tiff',
|
|
653
|
+
'.mp4', '.webm', '.avi', '.mov', '.wmv', '.flv',
|
|
654
|
+
'.mp3', '.wav', '.ogg', '.aac', '.flac',
|
|
655
|
+
'.pdf', '.doc', '.docx', '.txt']:
|
|
656
|
+
return ext
|
|
657
|
+
|
|
658
|
+
# Default fallback
|
|
659
|
+
return '.bin'
|
|
660
|
+
|
|
661
|
+
def _format_file_size(self, size_bytes: int) -> str:
|
|
662
|
+
"""Format file size in human readable format"""
|
|
663
|
+
if size_bytes == 0:
|
|
664
|
+
return "0 B"
|
|
665
|
+
size_names = ["B", "KB", "MB", "GB", "TB"]
|
|
666
|
+
i = 0
|
|
667
|
+
while size_bytes >= 1024.0 and i < len(size_names) - 1:
|
|
668
|
+
size_bytes /= 1024.0
|
|
669
|
+
i += 1
|
|
670
|
+
return f"{size_bytes:.1f} {size_names[i]}"
|