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
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import pdb
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
import html
|
|
5
|
+
import random
|
|
6
|
+
import time
|
|
7
|
+
from typing import Dict, List, Tuple, Optional
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from urllib.parse import parse_qs, unquote, urlparse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SearchType(Enum):
|
|
13
|
+
"""Search type enumeration for YouTube"""
|
|
14
|
+
VIDEO = "video"
|
|
15
|
+
CHANNEL = "channel"
|
|
16
|
+
PLAYLIST = "playlist"
|
|
17
|
+
ALL = "all"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SortType(Enum):
|
|
21
|
+
"""Sort type enumeration for YouTube search"""
|
|
22
|
+
RELEVANCE = "relevance"
|
|
23
|
+
DATE = "date"
|
|
24
|
+
VIEW_COUNT = "viewCount"
|
|
25
|
+
RATING = "rating"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Duration(Enum):
|
|
29
|
+
"""Duration filter for YouTube search"""
|
|
30
|
+
ANY = "any"
|
|
31
|
+
SHORT = "short" # < 4 minutes
|
|
32
|
+
MEDIUM = "medium" # 4-20 minutes
|
|
33
|
+
LONG = "long" # > 20 minutes
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UploadDate(Enum):
|
|
37
|
+
"""Upload date filter for YouTube search"""
|
|
38
|
+
ANY = "any"
|
|
39
|
+
HOUR = "hour"
|
|
40
|
+
TODAY = "today"
|
|
41
|
+
WEEK = "week"
|
|
42
|
+
MONTH = "month"
|
|
43
|
+
YEAR = "year"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_visitor_data() -> str:
|
|
47
|
+
"""Generate a random visitor data string for YouTube requests"""
|
|
48
|
+
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
|
|
49
|
+
return ''.join(random.choices(chars, k=24))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extract_cookies_from_browser(web_cookies: List[Dict]) -> Tuple[str, Dict[str, str]]:
|
|
53
|
+
"""Extract and format cookies from browser, filtering only YouTube related cookies"""
|
|
54
|
+
cookie_dict = {}
|
|
55
|
+
cookie_parts = []
|
|
56
|
+
|
|
57
|
+
# YouTube domain patterns to filter
|
|
58
|
+
youtube_domains = [
|
|
59
|
+
'.youtube.com',
|
|
60
|
+
# 'www.youtube.com',
|
|
61
|
+
# 'm.youtube.com',
|
|
62
|
+
# '.google.com'
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
for cookie in web_cookies:
|
|
66
|
+
if 'name' in cookie and 'value' in cookie and 'domain' in cookie:
|
|
67
|
+
domain = cookie['domain']
|
|
68
|
+
|
|
69
|
+
# Filter only YouTube related cookies
|
|
70
|
+
if any(yt_domain in domain for yt_domain in youtube_domains):
|
|
71
|
+
name = cookie['name']
|
|
72
|
+
value = cookie['value']
|
|
73
|
+
cookie_dict[name] = value
|
|
74
|
+
cookie_parts.append(f"{name}={value}")
|
|
75
|
+
|
|
76
|
+
cookie_string = "; ".join(cookie_parts)
|
|
77
|
+
return cookie_string, cookie_dict
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def extract_video_id_from_url(youtube_url: str) -> Optional[str]:
|
|
81
|
+
"""Extract video ID from YouTube URL"""
|
|
82
|
+
patterns = [
|
|
83
|
+
r'(?:v=|\/)([0-9A-Za-z_-]{11}).*',
|
|
84
|
+
r'(?:embed\/)([0-9A-Za-z_-]{11})',
|
|
85
|
+
r'(?:youtu\.be\/)([0-9A-Za-z_-]{11})',
|
|
86
|
+
r'(?:watch\?v=)([0-9A-Za-z_-]{11})'
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
for pattern in patterns:
|
|
90
|
+
match = re.search(pattern, youtube_url)
|
|
91
|
+
if match:
|
|
92
|
+
return match.group(1)
|
|
93
|
+
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def extract_channel_id_from_url(channel_url: str) -> Optional[str]:
|
|
98
|
+
"""Extract channel ID from YouTube channel URL"""
|
|
99
|
+
patterns = [
|
|
100
|
+
r'(?:channel\/)([UC][0-9A-Za-z_-]{22})',
|
|
101
|
+
r'(?:c\/)([^\/\?]+)',
|
|
102
|
+
r'(?:user\/)([^\/\?]+)',
|
|
103
|
+
r'(?:@)([^\/\?]+)'
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
for pattern in patterns:
|
|
107
|
+
match = re.search(pattern, channel_url)
|
|
108
|
+
if match:
|
|
109
|
+
return match.group(1)
|
|
110
|
+
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def extract_playlist_id_from_url(playlist_url: str) -> Optional[str]:
|
|
115
|
+
"""Extract playlist ID from YouTube playlist URL"""
|
|
116
|
+
match = re.search(r'(?:list=)([0-9A-Za-z_-]+)', playlist_url)
|
|
117
|
+
if match:
|
|
118
|
+
return match.group(1)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_youtube_duration(duration_str: str) -> int:
|
|
123
|
+
"""Parse YouTube duration string (e.g., "PT4M13S") to seconds"""
|
|
124
|
+
if not duration_str:
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
# Remove PT prefix
|
|
128
|
+
duration_str = duration_str.replace('PT', '')
|
|
129
|
+
|
|
130
|
+
# Extract hours, minutes, seconds
|
|
131
|
+
hours = 0
|
|
132
|
+
minutes = 0
|
|
133
|
+
seconds = 0
|
|
134
|
+
|
|
135
|
+
# Hours
|
|
136
|
+
hour_match = re.search(r'(\d+)H', duration_str)
|
|
137
|
+
if hour_match:
|
|
138
|
+
hours = int(hour_match.group(1))
|
|
139
|
+
|
|
140
|
+
# Minutes
|
|
141
|
+
minute_match = re.search(r'(\d+)M', duration_str)
|
|
142
|
+
if minute_match:
|
|
143
|
+
minutes = int(minute_match.group(1))
|
|
144
|
+
|
|
145
|
+
# Seconds
|
|
146
|
+
second_match = re.search(r'(\d+)S', duration_str)
|
|
147
|
+
if second_match:
|
|
148
|
+
seconds = int(second_match.group(1))
|
|
149
|
+
|
|
150
|
+
return hours * 3600 + minutes * 60 + seconds
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def format_view_count(view_count: str) -> int:
|
|
154
|
+
"""Parse YouTube view count string to integer"""
|
|
155
|
+
if not view_count:
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
# Remove non-numeric characters except for multipliers
|
|
160
|
+
view_count = view_count.replace(',', '').replace(' ', '').lower()
|
|
161
|
+
|
|
162
|
+
multipliers = {
|
|
163
|
+
'k': 1000,
|
|
164
|
+
'm': 1000000,
|
|
165
|
+
'b': 1000000000,
|
|
166
|
+
't': 1000000000000
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for suffix, multiplier in multipliers.items():
|
|
170
|
+
if view_count.endswith(suffix):
|
|
171
|
+
number = float(view_count[:-1])
|
|
172
|
+
return int(number * multiplier)
|
|
173
|
+
|
|
174
|
+
# Try to parse as regular integer
|
|
175
|
+
return int(''.join(filter(str.isdigit, view_count)))
|
|
176
|
+
|
|
177
|
+
except (ValueError, TypeError):
|
|
178
|
+
return 0
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def parse_youtube_time(time_str: str) -> Optional[int]:
|
|
182
|
+
"""Parse YouTube time string to timestamp"""
|
|
183
|
+
if not time_str:
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
# Handle relative time like "2 hours ago", "1 day ago", etc.
|
|
188
|
+
if "ago" in time_str.lower():
|
|
189
|
+
time_str = time_str.lower().replace('ago', '').strip()
|
|
190
|
+
|
|
191
|
+
if 'second' in time_str:
|
|
192
|
+
seconds = int(re.search(r'(\d+)', time_str).group(1))
|
|
193
|
+
return int(time.time()) - seconds
|
|
194
|
+
elif 'minute' in time_str:
|
|
195
|
+
minutes = int(re.search(r'(\d+)', time_str).group(1))
|
|
196
|
+
return int(time.time()) - minutes * 60
|
|
197
|
+
elif 'hour' in time_str:
|
|
198
|
+
hours = int(re.search(r'(\d+)', time_str).group(1))
|
|
199
|
+
return int(time.time()) - hours * 3600
|
|
200
|
+
elif 'day' in time_str:
|
|
201
|
+
days = int(re.search(r'(\d+)', time_str).group(1))
|
|
202
|
+
return int(time.time()) - days * 86400
|
|
203
|
+
elif 'week' in time_str:
|
|
204
|
+
weeks = int(re.search(r'(\d+)', time_str).group(1))
|
|
205
|
+
return int(time.time()) - weeks * 604800
|
|
206
|
+
elif 'month' in time_str:
|
|
207
|
+
months = int(re.search(r'(\d+)', time_str).group(1))
|
|
208
|
+
return int(time.time()) - months * 2592000 # Approximate
|
|
209
|
+
elif 'year' in time_str:
|
|
210
|
+
years = int(re.search(r'(\d+)', time_str).group(1))
|
|
211
|
+
return int(time.time()) - years * 31536000 # Approximate
|
|
212
|
+
|
|
213
|
+
# Try to parse as timestamp
|
|
214
|
+
return int(time_str)
|
|
215
|
+
|
|
216
|
+
except (ValueError, AttributeError):
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def process_youtube_text(text: str) -> str:
|
|
221
|
+
"""Process YouTube text content, remove HTML tags and clean up"""
|
|
222
|
+
if not text:
|
|
223
|
+
return ""
|
|
224
|
+
|
|
225
|
+
# Remove HTML tags
|
|
226
|
+
text = re.sub(r'<[^>]+>', '', text)
|
|
227
|
+
|
|
228
|
+
# Decode HTML entities
|
|
229
|
+
text = html.unescape(text)
|
|
230
|
+
|
|
231
|
+
# Remove extra whitespace
|
|
232
|
+
text = re.sub(r'\s+', ' ', text).strip()
|
|
233
|
+
|
|
234
|
+
return text
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def validate_youtube_data(video_data: Dict) -> bool:
|
|
238
|
+
"""Validate if YouTube video data contains required fields"""
|
|
239
|
+
required_fields = ["videoId", "title"]
|
|
240
|
+
|
|
241
|
+
for field in required_fields:
|
|
242
|
+
if field not in video_data:
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def sanitize_filename(filename: str) -> str:
|
|
249
|
+
"""Sanitize filename for file system"""
|
|
250
|
+
# Remove invalid characters
|
|
251
|
+
filename = re.sub(r'[<>:"/\\|?*]', '', filename)
|
|
252
|
+
# Remove extra spaces
|
|
253
|
+
filename = re.sub(r'\s+', ' ', filename).strip()
|
|
254
|
+
# Limit length
|
|
255
|
+
if len(filename) > 100:
|
|
256
|
+
filename = filename[:100]
|
|
257
|
+
|
|
258
|
+
return filename or "untitled"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def extract_ytcfg_data(html_content: str) -> Optional[Dict]:
|
|
262
|
+
"""Extract ytcfg data from YouTube page HTML"""
|
|
263
|
+
try:
|
|
264
|
+
# Try to find ytcfg.set pattern
|
|
265
|
+
match = re.search(r'ytcfg\.set\s*\(\s*({.+?})\s*\)', html_content, re.DOTALL)
|
|
266
|
+
if match:
|
|
267
|
+
config_json = match.group(1)
|
|
268
|
+
return json.loads(config_json)
|
|
269
|
+
except (json.JSONDecodeError, IndexError):
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def extract_initial_data(html_content: str) -> Optional[Dict]:
|
|
276
|
+
"""Extract initial data from YouTube page HTML"""
|
|
277
|
+
try:
|
|
278
|
+
# Try to find var ytInitialData pattern
|
|
279
|
+
match = re.search(r'var ytInitialData = ({.+?});', html_content, re.DOTALL)
|
|
280
|
+
if not match:
|
|
281
|
+
# Try window.ytInitialData pattern
|
|
282
|
+
match = re.search(r'window\["ytInitialData"\] = ({.+?});', html_content, re.DOTALL)
|
|
283
|
+
|
|
284
|
+
if match:
|
|
285
|
+
initial_data_json = match.group(1)
|
|
286
|
+
return json.loads(initial_data_json)
|
|
287
|
+
except (json.JSONDecodeError, IndexError):
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_desktop_user_agent() -> str:
|
|
294
|
+
"""Get a random desktop user agent for YouTube requests"""
|
|
295
|
+
ua_list = [
|
|
296
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
|
297
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
|
298
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0",
|
|
299
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15",
|
|
300
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
|
|
301
|
+
]
|
|
302
|
+
return random.choice(ua_list)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def build_search_url(query: str, search_type: SearchType = SearchType.ALL,
|
|
306
|
+
sort_by: SortType = SortType.RELEVANCE,
|
|
307
|
+
upload_date: UploadDate = UploadDate.ANY,
|
|
308
|
+
duration: Duration = Duration.ANY) -> str:
|
|
309
|
+
"""Build YouTube search URL with filters"""
|
|
310
|
+
base_url = "https://www.youtube.com/results"
|
|
311
|
+
params = {"search_query": query}
|
|
312
|
+
|
|
313
|
+
# Add search type filter
|
|
314
|
+
if search_type != SearchType.ALL:
|
|
315
|
+
params["sp"] = _get_search_params(search_type, sort_by, upload_date, duration)
|
|
316
|
+
|
|
317
|
+
param_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
318
|
+
return f"{base_url}?{param_string}"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _get_search_params(search_type: SearchType, sort_by: SortType,
|
|
322
|
+
upload_date: UploadDate, duration: Duration) -> str:
|
|
323
|
+
"""Generate search parameters string for YouTube search filters"""
|
|
324
|
+
# This is a simplified version - YouTube's actual search parameters are more complex
|
|
325
|
+
# and may need to be reverse-engineered for full functionality
|
|
326
|
+
filters = []
|
|
327
|
+
|
|
328
|
+
if search_type == SearchType.VIDEO:
|
|
329
|
+
filters.append("EgIQAQ%253D%253D")
|
|
330
|
+
elif search_type == SearchType.CHANNEL:
|
|
331
|
+
filters.append("EgIQAg%253D%253D")
|
|
332
|
+
elif search_type == SearchType.PLAYLIST:
|
|
333
|
+
filters.append("EgIQAw%253D%253D")
|
|
334
|
+
|
|
335
|
+
return "".join(filters)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# Exception classes
|
|
339
|
+
class YouTubeError(Exception):
|
|
340
|
+
"""Base exception for YouTube API errors"""
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class NetworkError(YouTubeError):
|
|
345
|
+
"""Network connection error"""
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class DataExtractionError(YouTubeError):
|
|
350
|
+
"""Data extraction error"""
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class AuthenticationError(YouTubeError):
|
|
355
|
+
"""Authentication error"""
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
class RateLimitError(YouTubeError):
|
|
360
|
+
"""Rate limit exceeded error"""
|
|
361
|
+
pass
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class ContentNotFoundError(YouTubeError):
|
|
365
|
+
"""Content not found error"""
|
|
366
|
+
pass
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class ValidationError(YouTubeError):
|
|
370
|
+
"""Data validation error"""
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def extract_continuation_token(data: Dict) -> Optional[str]:
|
|
375
|
+
"""Extract continuation token for pagination"""
|
|
376
|
+
try:
|
|
377
|
+
# Look for continuation token in various possible locations
|
|
378
|
+
if isinstance(data, dict):
|
|
379
|
+
# Check common continuation locations
|
|
380
|
+
continuations = data.get("continuations", [])
|
|
381
|
+
if continuations and isinstance(continuations, list):
|
|
382
|
+
for continuation in continuations:
|
|
383
|
+
if isinstance(continuation, dict):
|
|
384
|
+
token = continuation.get("nextContinuationData", {}).get("continuation")
|
|
385
|
+
if token:
|
|
386
|
+
return token
|
|
387
|
+
|
|
388
|
+
# Check other possible locations
|
|
389
|
+
reload_continuation = data.get("reloadContinuationData", {}).get("continuation")
|
|
390
|
+
if reload_continuation:
|
|
391
|
+
return reload_continuation
|
|
392
|
+
except Exception:
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def decode_html_entities(text: str) -> str:
|
|
399
|
+
"""Decode HTML entities in text"""
|
|
400
|
+
if not text:
|
|
401
|
+
return ""
|
|
402
|
+
|
|
403
|
+
# Decode HTML entities
|
|
404
|
+
text = html.unescape(text)
|
|
405
|
+
|
|
406
|
+
return text
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def extract_thumbnail_url(thumbnails: List[Dict]) -> str:
|
|
410
|
+
"""Extract the best quality thumbnail URL from thumbnails list"""
|
|
411
|
+
if not thumbnails:
|
|
412
|
+
return ""
|
|
413
|
+
|
|
414
|
+
# Sort by resolution and pick the highest quality
|
|
415
|
+
sorted_thumbnails = sorted(thumbnails, key=lambda x: x.get('width', 0) * x.get('height', 0), reverse=True)
|
|
416
|
+
|
|
417
|
+
if sorted_thumbnails:
|
|
418
|
+
return sorted_thumbnails[0].get('url', '')
|
|
419
|
+
|
|
420
|
+
return ""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vibesurf
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.28
|
|
4
4
|
Summary: VibeSurf: A powerful browser assistant for vibe surfing
|
|
5
5
|
Author: Shao Warm
|
|
6
6
|
License: Apache-2.0
|
|
@@ -44,16 +44,19 @@ Requires-Dist: markdownify>=1.2.0
|
|
|
44
44
|
Requires-Dist: pathvalidate>=3.3.1
|
|
45
45
|
Requires-Dist: dashscope>=1.24.5
|
|
46
46
|
Requires-Dist: yfinance>=0.2.66
|
|
47
|
+
Requires-Dist: pyexecjs>=1.5.1
|
|
47
48
|
Dynamic: license-file
|
|
48
49
|
|
|
49
50
|
# VibeSurf: A powerful browser assistant for vibe surfing
|
|
50
|
-
[](https://discord.gg/
|
|
51
|
+
[](https://discord.gg/EZ2YnUXP)
|
|
51
52
|
[](https://x.com/warmshao)
|
|
52
53
|
|
|
53
54
|
VibeSurf is an open-source AI agentic browser that revolutionizes browser automation and research.
|
|
54
55
|
|
|
55
56
|
If you're as excited about open-source AI browsing as I am, give it a star! ⭐
|
|
56
57
|
|
|
58
|
+
[中文](README_zh.md) | [English](README.md)
|
|
59
|
+
|
|
57
60
|
## ✨ Key Features
|
|
58
61
|
|
|
59
62
|
- 🧠 **Advanced AI Automation**: Beyond browser automation, VibeSurf performs deep research, intelligent crawling, content summarization, and more to exploration.
|
|
@@ -90,11 +93,29 @@ uv pip install vibesurf -U
|
|
|
90
93
|
uv run vibesurf
|
|
91
94
|
```
|
|
92
95
|
|
|
93
|
-
##
|
|
96
|
+
## 👩💻 For Contributors
|
|
97
|
+
|
|
98
|
+
Want to contribute to VibeSurf? Here are two ways to set up your development environment:
|
|
99
|
+
|
|
100
|
+
### Method 1: Direct Server Run
|
|
101
|
+
Run the backend server directly using uvicorn:
|
|
102
|
+
```bash
|
|
103
|
+
uvicorn vibe_surf.backend.main:app --host 127.0.0.1 --port 9335
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Method 2: Editable Installation
|
|
107
|
+
Install the package in editable mode and run using the CLI:
|
|
108
|
+
```bash
|
|
109
|
+
uv pip install -e .
|
|
110
|
+
uv run vibesurf
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Choose the method that works best for your development workflow!
|
|
114
|
+
## �️ Roadmap
|
|
94
115
|
|
|
95
116
|
We're building VibeSurf to be your ultimate AI browser companion. Here's what's coming next:
|
|
96
117
|
|
|
97
|
-
- [
|
|
118
|
+
- [x] **Smart Skills System**: Add `/search` for quick information search and `/crawl` for automatic website data extraction
|
|
98
119
|
- [ ] **Powerful Coding Agent**: Build a comprehensive coding assistant for data processing and analysis directly in your browser
|
|
99
120
|
- [ ] **Third-Party Integrations**: Connect with n8n workflows and other tools to combine browsing with automation
|
|
100
121
|
- [ ] **Custom Workflow Templates**: Create reusable templates for auto-login, data collection, and complex browser automation
|
|
@@ -113,7 +134,7 @@ We're building VibeSurf to be your ultimate AI browser companion. Here's what's
|
|
|
113
134
|
|
|
114
135
|
## 📝 License
|
|
115
136
|
|
|
116
|
-
|
|
137
|
+
This repository is licensed under the [VibeSurf Open Source License](./LICENSE), based on Apache 2.0 with additional conditions.
|
|
117
138
|
|
|
118
139
|
## 👏 Acknowledgments
|
|
119
140
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
vibe_surf/__init__.py,sha256=WtduuMFGauMD_9dpk4fnRnLTAP6ka9Lfu0feAFNzLfo,339
|
|
2
|
-
vibe_surf/_version.py,sha256=
|
|
2
|
+
vibe_surf/_version.py,sha256=1F4XTGwwdJozvgbsUgvu0kddraJ7P8oKbqLP8wGuYI8,706
|
|
3
3
|
vibe_surf/cli.py,sha256=KAmUBsXfS-NkMp3ITxzNXwtFeKVmXJUDZiWqLcIC0BI,16690
|
|
4
4
|
vibe_surf/common.py,sha256=_WWMxen5wFwzUjEShn3yDVC1OBFUiJ6Vccadi6tuG6w,1215
|
|
5
5
|
vibe_surf/logger.py,sha256=k53MFA96QX6t9OfcOf1Zws8PP0OOqjVJfhUD3Do9lKw,3043
|
|
6
6
|
vibe_surf/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
vibe_surf/agents/browser_use_agent.py,sha256=jeUYV7yk6vyycw6liju_597GdjB3CW_B2wEhn2F0ekk,45957
|
|
8
8
|
vibe_surf/agents/report_writer_agent.py,sha256=pCF2k6VLyO-sSviGBqqIyVD3SLqaZtSqiW3kvNfPY1I,20967
|
|
9
|
-
vibe_surf/agents/vibe_surf_agent.py,sha256=
|
|
9
|
+
vibe_surf/agents/vibe_surf_agent.py,sha256=cOQvZJrE4WBnirgN_PW-GfVSnPKI5G5FwW2AA5bTqew,74162
|
|
10
10
|
vibe_surf/agents/views.py,sha256=yHjNJloa-aofVTGyuRy08tBYP_Y3XLqt1DUWOUmHRng,4825
|
|
11
11
|
vibe_surf/agents/prompts/__init__.py,sha256=l4ieA0D8kLJthyNN85FKLNe4ExBa3stY3l-aImLDRD0,36
|
|
12
12
|
vibe_surf/agents/prompts/report_writer_prompt.py,sha256=sZE8MUT1CDLmRzbnbEQzAvTwJjpITgh2Q8g1_eXmkzE,4454
|
|
@@ -39,7 +39,7 @@ vibe_surf/backend/utils/encryption.py,sha256=CjLNh_n0Luhfa-6BB-icfzkiiDqj5b4Gu6M
|
|
|
39
39
|
vibe_surf/backend/utils/llm_factory.py,sha256=XIJYc9Lh_L2vbwlAe96PrjptlzJtLOjCGNdHEx6fThk,9047
|
|
40
40
|
vibe_surf/browser/__init__.py,sha256=_UToO2fZfSCrfjOcxhn4Qq7ZLbYeyPuUUEmqIva-Yv8,325
|
|
41
41
|
vibe_surf/browser/agen_browser_profile.py,sha256=J06hCBJSJ-zAFVM9yDFz8UpmiLuFyWke1EMekpU45eo,5871
|
|
42
|
-
vibe_surf/browser/agent_browser_session.py,sha256=
|
|
42
|
+
vibe_surf/browser/agent_browser_session.py,sha256=9bCQkPNEy88-04gB9njBrbuoFBhIYF_SxueZfFe5RCM,35662
|
|
43
43
|
vibe_surf/browser/browser_manager.py,sha256=PFJ9flmqR2uokuRZ3PDh_Dy6_6qcQ6kH_eRc1yT6Iq8,11257
|
|
44
44
|
vibe_surf/browser/utils.py,sha256=bBtpMiyz2ixWOr31GbJwZ8pVYcnxztKjTJJO92z1BDY,35742
|
|
45
45
|
vibe_surf/browser/watchdogs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -87,18 +87,31 @@ vibe_surf/chrome_extension/styles/variables.css,sha256=enjyhsa0PeU3b-3uiXa-VkV-1
|
|
|
87
87
|
vibe_surf/llm/__init__.py,sha256=_vDVPo6STf343p1SgMQrF5023hicAx0g83pK2Gbk4Ek,601
|
|
88
88
|
vibe_surf/llm/openai_compatible.py,sha256=i0a5OLaL6QIlacVyctOG09vKr3KOi8T8Izp1v7xkD5I,16112
|
|
89
89
|
vibe_surf/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
|
-
vibe_surf/tools/browser_use_tools.py,sha256=
|
|
90
|
+
vibe_surf/tools/browser_use_tools.py,sha256=ALJOoPiwG3HSR_UZZpi_1f8cQngcAEKiap8ojIgdQlo,31586
|
|
91
91
|
vibe_surf/tools/file_system.py,sha256=Tw_6J5QjCahQ3fd26CXziF1zPvRxhYM0889oK4bDhlU,19304
|
|
92
92
|
vibe_surf/tools/finance_tools.py,sha256=E8rmblp57e_cp0tFbdZ7BY3_upNlk4Whk0bYc_SFCJE,27284
|
|
93
93
|
vibe_surf/tools/mcp_client.py,sha256=OeCoTgyx4MoY7JxXndK6pGHIoyFOhf5r7XCbx25y1Ec,2446
|
|
94
94
|
vibe_surf/tools/report_writer_tools.py,sha256=2CyTTXOahTKZo7XwyWDDhJ--1mRA0uTtUWxu_DACAY0,776
|
|
95
95
|
vibe_surf/tools/vibesurf_registry.py,sha256=Z-8d9BrJl3RFMEK0Tw1Q5xNHX2kZGsnIGCTBZ3RM-pw,2159
|
|
96
|
-
vibe_surf/tools/vibesurf_tools.py,sha256=
|
|
97
|
-
vibe_surf/tools/views.py,sha256=
|
|
96
|
+
vibe_surf/tools/vibesurf_tools.py,sha256=UY93Yft_Ni6D8k94t0afZ4x_EAbh1PGsWZ4RPr12So8,113828
|
|
97
|
+
vibe_surf/tools/views.py,sha256=1b0y9Zl1GWmDFXUiZXntsWU-8U3xrOqXdpRld5efxgI,12257
|
|
98
98
|
vibe_surf/tools/voice_asr.py,sha256=AJG0yq_Jq-j8ulDlbPhVFfK1jch9_ASesis73iki9II,4702
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
vibe_surf/tools/website_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
100
|
+
vibe_surf/tools/website_api/douyin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
101
|
+
vibe_surf/tools/website_api/douyin/client.py,sha256=fNAI_16kBoPgSH_kGkgO7NJs3v1UitrXmT2ChbAWphE,32868
|
|
102
|
+
vibe_surf/tools/website_api/douyin/helpers.py,sha256=nxXSIYxDXn9L8xpCPojyP7ZFhlH7I81ex7dB2f50Sks,6577
|
|
103
|
+
vibe_surf/tools/website_api/weibo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
104
|
+
vibe_surf/tools/website_api/weibo/client.py,sha256=VOroVWL2IDIBaoMwc5MIA23EM3a5JM6PokxDAtGYElk,32960
|
|
105
|
+
vibe_surf/tools/website_api/weibo/helpers.py,sha256=kFrbKr98Z3UydsEiNoLM0wBQhItYrpH0Q9BE-g2Y-Xg,37099
|
|
106
|
+
vibe_surf/tools/website_api/xhs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
107
|
+
vibe_surf/tools/website_api/xhs/client.py,sha256=pKtq_d78C-XqvcpmxCEGsd3zftGkfCkF66o-XTmxk00,30858
|
|
108
|
+
vibe_surf/tools/website_api/xhs/helpers.py,sha256=Dq2RyYKClBQ2ha2yEfpS1mtZswx0z9gdB2Wyljc83SI,10448
|
|
109
|
+
vibe_surf/tools/website_api/youtube/__init__.py,sha256=QWmZWSqo1O6XtaWP-SuL3HrBLYINjEWEyOy-KCytGDw,1145
|
|
110
|
+
vibe_surf/tools/website_api/youtube/client.py,sha256=GgrAvv_DWbnLHW59PnOXEHeO05s9_Abaakk-JzJ_UTc,48887
|
|
111
|
+
vibe_surf/tools/website_api/youtube/helpers.py,sha256=GPgqfNirLYjIpk1OObvoXd2Ktq-ahKOOKHO2WwQVXCw,12931
|
|
112
|
+
vibesurf-0.1.28.dist-info/licenses/LICENSE,sha256=vRmTjOYvD8RLiSGYYmFHnveYNswtO1uvSk1sd-Eu7sg,2037
|
|
113
|
+
vibesurf-0.1.28.dist-info/METADATA,sha256=U6C7JrFMHsY3tm1XEF9KqU4LCTEvxOuRO1eAL2Gyj5c,5836
|
|
114
|
+
vibesurf-0.1.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
115
|
+
vibesurf-0.1.28.dist-info/entry_points.txt,sha256=UxqpvMocL-PR33S6vLF2OmXn-kVzM-DneMeZeHcPMM8,48
|
|
116
|
+
vibesurf-0.1.28.dist-info/top_level.txt,sha256=VPZGHqSb6EEqcJ4ZX6bHIuWfon5f6HXl3c7BYpbRqnY,10
|
|
117
|
+
vibesurf-0.1.28.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Open Source License
|
|
2
|
+
|
|
3
|
+
VibeSurf is licensed under a modified version of the Apache License 2.0, with the following additional conditions:
|
|
4
|
+
|
|
5
|
+
1. VibeSurf may be utilized commercially, including as a backend service for other applications or as an application development platform for enterprises. Should the conditions below be met, a commercial license must be obtained from the producer:
|
|
6
|
+
|
|
7
|
+
a. Multi-tenant service: Unless explicitly authorized by VibeSurf in writing, you may not use the VibeSurf source code to operate a multi-tenant environment.
|
|
8
|
+
- Tenant Definition: Within the context of VibeSurf, one tenant corresponds to one workspace. The workspace provides a separated area for each tenant's data and configurations.
|
|
9
|
+
|
|
10
|
+
b. LOGO and copyright information: In the process of using VibeSurf's frontend, you may not remove or modify the LOGO or copyright information in the VibeSurf console or applications. This restriction is inapplicable to uses of VibeSurf that do not involve its frontend.
|
|
11
|
+
- Frontend Definition: For the purposes of this license, the "frontend" of VibeSurf includes all components located in the `vibe_surf/chrome_extension/` directory when running VibeSurf from the raw source code, or the corresponding frontend container/image when running VibeSurf with Docker or other containerization.
|
|
12
|
+
|
|
13
|
+
2. As a contributor, you should agree that:
|
|
14
|
+
|
|
15
|
+
a. The producer can adjust the open-source agreement to be more strict or relaxed as deemed necessary.
|
|
16
|
+
b. Your contributed code may be used for commercial purposes, including but not limited to its cloud business operations.
|
|
17
|
+
|
|
18
|
+
Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0.
|
|
19
|
+
|
|
20
|
+
The interactive design of this product is protected by appearance patent.
|
|
21
|
+
|
|
22
|
+
© 2025 VibeSurf Authors. This repository is licensed under the VibeSurf Open Source License, based on Apache 2.0 with additional conditions.
|