vibesurf 0.1.26__py3-none-any.whl → 0.1.27__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.
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.26'
32
- __version_tuple__ = version_tuple = (0, 1, 26)
31
+ __version__ = version = '0.1.27'
32
+ __version_tuple__ = version_tuple = (0, 1, 27)
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 in ["skill_search", "skill_crawl", "skill_summary", "skill_finance"]:
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
- return state
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]}"
@@ -31,7 +31,8 @@ from browser_use.tools.views import NoParamsAction
31
31
  from vibe_surf.browser.agent_browser_session import AgentBrowserSession
32
32
  from vibe_surf.tools.views import HoverAction, ExtractionAction, FileExtractionAction, BrowserUseAgentExecution, \
33
33
  ReportWriterTask, TodoGenerateAction, TodoModifyAction, VibeSurfDoneAction, SkillSearchAction, SkillCrawlAction, \
34
- SkillSummaryAction, SkillTakeScreenshotAction, SkillDeepResearchAction, SkillCodeAction, SkillFinanceAction
34
+ SkillSummaryAction, SkillTakeScreenshotAction, SkillDeepResearchAction, SkillCodeAction, SkillFinanceAction, \
35
+ SkillXhsAction, SkillDouyinAction, SkillYoutubeAction, SkillWeiboAction
35
36
  from vibe_surf.tools.finance_tools import FinanceDataRetriever, FinanceMarkdownFormatter, FinanceMethod
36
37
  from vibe_surf.tools.mcp_client import CustomMCPClient
37
38
  from vibe_surf.tools.file_system import CustomFileSystem
@@ -516,7 +517,7 @@ Format: [index1, index2, index3, ...]
516
517
  with open(filepath, "wb") as f:
517
518
  f.write(base64.b64decode(screenshot))
518
519
 
519
- msg = f'📸 Screenshot saved to path: {str(filepath.relative_to(fs_dir))}'
520
+ msg = f'📸 Screenshot saved to path: [{filename}]({str(filepath.relative_to(fs_dir))})'
520
521
  logger.info(msg)
521
522
  return ActionResult(
522
523
  extracted_content=msg,
@@ -942,7 +943,428 @@ Please generate alternative JavaScript code that avoids this system error:"""
942
943
  except Exception as e:
943
944
  error_msg = f'❌ Failed to retrieve financial data for {params.symbol}: {str(e)}'
944
945
  logger.error(error_msg)
945
- return ActionResult(error=error_msg)
946
+ return ActionResult(error=error_msg, extracted_content=error_msg)
947
+
948
+
949
+ @self.registry.action(
950
+ 'Skill: Xiaohongshu API - Access Xiaohongshu (Little Red Book) platform data including search, content details, comments, user profiles, and recommendations. Methods: search_content_by_keyword, fetch_content_details, fetch_all_content_comments, get_user_profile, fetch_all_user_content, get_home_recommendations.',
951
+ param_model=SkillXhsAction,
952
+ )
953
+ async def skill_xhs(
954
+ params: SkillXhsAction,
955
+ browser_manager: BrowserManager,
956
+ file_system: CustomFileSystem
957
+ ):
958
+ """
959
+ Skill: Xiaohongshu API integration
960
+
961
+ Available methods:
962
+ - search_content_by_keyword: Search content by keyword with sorting options
963
+ - fetch_content_details: Get detailed information about specific content
964
+ - fetch_all_content_comments: Get all comments for specific content
965
+ - get_user_profile: Get user profile information
966
+ - fetch_all_user_content: Get all content posted by a user
967
+ - get_home_recommendations: Get homepage recommended content
968
+ """
969
+ try:
970
+ from vibe_surf.tools.website_api.xhs.client import XiaoHongShuApiClient
971
+
972
+ # Initialize client
973
+ xhs_client = XiaoHongShuApiClient(browser_session=browser_manager.main_browser_session)
974
+ await xhs_client.setup()
975
+
976
+ # Parse params JSON string
977
+ import json
978
+ from json_repair import repair_json
979
+ try:
980
+ method_params = json.loads(params.params)
981
+ except json.JSONDecodeError:
982
+ method_params = json.loads(repair_json(params.params))
983
+
984
+ # Execute the requested method
985
+ result = None
986
+ if params.method == "search_content_by_keyword":
987
+ result = await xhs_client.search_content_by_keyword(**method_params)
988
+ elif params.method == "fetch_content_details":
989
+ result = await xhs_client.fetch_content_details(**method_params)
990
+ elif params.method == "fetch_all_content_comments":
991
+ result = await xhs_client.fetch_all_content_comments(**method_params)
992
+ elif params.method == "get_user_profile":
993
+ result = await xhs_client.get_user_profile(**method_params)
994
+ elif params.method == "fetch_all_user_content":
995
+ result = await xhs_client.fetch_all_user_content(**method_params)
996
+ elif params.method == "get_home_recommendations":
997
+ result = await xhs_client.get_home_recommendations()
998
+ else:
999
+ return ActionResult(error=f"Unknown method: {params.method}")
1000
+
1001
+ # Save result to file
1002
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1003
+ filename = f"xhs_{params.method}_{timestamp}.json"
1004
+ filepath = file_system.get_dir() / "data" / filename
1005
+ filepath.parent.mkdir(exist_ok=True)
1006
+
1007
+ with open(filepath, "w", encoding="utf-8") as f:
1008
+ json.dump(result, f, ensure_ascii=False, indent=2)
1009
+
1010
+ # Format result as markdown
1011
+ if isinstance(result, list):
1012
+ display_count = min(5, len(result))
1013
+ md_content = f"## Xiaohongshu {params.method.replace('_', ' ').title()}\n\n"
1014
+ md_content += f"Showing {display_count} of {len(result)} results:\n\n"
1015
+ for i, item in enumerate(result[:display_count]):
1016
+ md_content += f"### Result {i+1}\n"
1017
+ for key, value in item.items():
1018
+ if not value:
1019
+ continue
1020
+ if isinstance(value, str) and len(value) > 200:
1021
+ md_content += f"- **{key}**: {value[:200]}...\n"
1022
+ else:
1023
+ md_content += f"- **{key}**: {value}\n"
1024
+ md_content += "\n"
1025
+ else:
1026
+ md_content = f"## Xiaohongshu {params.method.replace('_', ' ').title()}\n\n"
1027
+ for key, value in result.items():
1028
+ if isinstance(value, str) and len(value) > 200:
1029
+ md_content += f"- **{key}**: {value[:200]}...\n"
1030
+ else:
1031
+ md_content += f"- **{key}**: {value}\n"
1032
+ md_content += "\n"
1033
+
1034
+ # Add file path to markdown
1035
+ relative_path = str(filepath.relative_to(file_system.get_dir()))
1036
+ md_content += f"\n> 📁 Full data saved to: [{filename}]({relative_path})\n"
1037
+ md_content += f"> 💡 Click the link above to view all results.\n"
1038
+
1039
+ logger.info(f'📕 Xiaohongshu data retrieved with method: {params.method}')
1040
+
1041
+ # Close client
1042
+ await xhs_client.close()
1043
+
1044
+ return ActionResult(
1045
+ extracted_content=md_content
1046
+ )
1047
+
1048
+ except Exception as e:
1049
+ error_msg = f'❌ Failed to retrieve Xiaohongshu data: {str(e)}'
1050
+ logger.error(error_msg)
1051
+ return ActionResult(error=error_msg, extracted_content=error_msg)
1052
+
1053
+
1054
+ @self.registry.action(
1055
+ 'Skill: Weibo API - Access Weibo platform data including search, post details, comments, user profiles, hot posts, and trending lists. Methods: search_posts_by_keyword, get_post_detail, get_all_post_comments, get_user_info, get_all_user_posts, get_hot_posts(推荐榜), get_trending_posts(热搜榜).',
1056
+ param_model=SkillWeiboAction,
1057
+ )
1058
+ async def skill_weibo(
1059
+ params: SkillWeiboAction,
1060
+ browser_manager: BrowserManager,
1061
+ file_system: CustomFileSystem
1062
+ ):
1063
+ """
1064
+ Skill: Weibo API integration
1065
+
1066
+ Available methods:
1067
+ - search_posts_by_keyword: Search posts by keyword with sorting options
1068
+ - get_post_detail: Get detailed information about specific post
1069
+ - get_all_post_comments: Get all comments for specific post
1070
+ - get_user_info: Get user profile information
1071
+ - get_all_user_posts: Get all posts by a user
1072
+ - get_hot_posts: Get hot posts
1073
+ - get_trending_list: Get trending list
1074
+ """
1075
+ try:
1076
+ from vibe_surf.tools.website_api.weibo.client import WeiboApiClient
1077
+
1078
+ # Initialize client
1079
+ wb_client = WeiboApiClient(browser_session=browser_manager.main_browser_session)
1080
+ await wb_client.setup()
1081
+
1082
+ # Parse params JSON string
1083
+ import json
1084
+ from json_repair import repair_json
1085
+ try:
1086
+ method_params = json.loads(params.params)
1087
+ except json.JSONDecodeError:
1088
+ method_params = json.loads(repair_json(params.params))
1089
+
1090
+ # Execute the requested method
1091
+ result = None
1092
+ if params.method == "search_posts_by_keyword":
1093
+ result = await wb_client.search_posts_by_keyword(**method_params)
1094
+ elif params.method == "get_post_detail":
1095
+ result = await wb_client.get_post_detail(**method_params)
1096
+ elif params.method == "get_all_post_comments":
1097
+ result = await wb_client.get_all_post_comments(**method_params)
1098
+ elif params.method == "get_user_info":
1099
+ result = await wb_client.get_user_info(**method_params)
1100
+ elif params.method == "get_all_user_posts":
1101
+ result = await wb_client.get_all_user_posts(**method_params)
1102
+ elif params.method == "get_hot_posts":
1103
+ result = await wb_client.get_hot_posts()
1104
+ elif params.method == "get_trending_posts":
1105
+ result = await wb_client.get_trending_posts()
1106
+ else:
1107
+ return ActionResult(error=f"Unknown method: {params.method}")
1108
+
1109
+ # Save result to file
1110
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1111
+ filename = f"weibo_{params.method}_{timestamp}.json"
1112
+ filepath = file_system.get_dir() / "data" / filename
1113
+ filepath.parent.mkdir(exist_ok=True)
1114
+
1115
+ with open(filepath, "w", encoding="utf-8") as f:
1116
+ json.dump(result, f, ensure_ascii=False, indent=2)
1117
+ # Format result as markdown
1118
+ if isinstance(result, list):
1119
+ display_count = min(5, len(result))
1120
+ md_content = f"## Weibo {params.method.replace('_', ' ').title()}\n\n"
1121
+ md_content += f"Showing {display_count} of {len(result)} results:\n\n"
1122
+ for i, item in enumerate(result[:display_count]):
1123
+ md_content += f"### Result {i+1}\n"
1124
+ for key, value in item.items():
1125
+ if not value:
1126
+ continue
1127
+ if isinstance(value, str) and len(value) > 200:
1128
+ md_content += f"- **{key}**: {value[:200]}...\n"
1129
+ else:
1130
+ md_content += f"- **{key}**: {value}\n"
1131
+ md_content += "\n"
1132
+ else:
1133
+ md_content = f"## Weibo {params.method.replace('_', ' ').title()}\n\n"
1134
+ for key, value in result.items():
1135
+ if isinstance(value, str) and len(value) > 200:
1136
+ md_content += f"- **{key}**: {value[:200]}...\n"
1137
+ else:
1138
+ md_content += f"- **{key}**: {value}\n"
1139
+ md_content += "\n"
1140
+
1141
+ # Add file path to markdown
1142
+ relative_path = str(filepath.relative_to(file_system.get_dir()))
1143
+ md_content += f"\n> 📁 Full data saved to: [{filename}]({relative_path})\n"
1144
+ md_content += f"> 💡 Click the link above to view all results.\n"
1145
+
1146
+ logger.info(f'🐦 Weibo data retrieved with method: {params.method}')
1147
+
1148
+ # Close client
1149
+ await wb_client.close()
1150
+
1151
+ return ActionResult(
1152
+ extracted_content=md_content
1153
+ )
1154
+
1155
+ except Exception as e:
1156
+ import traceback
1157
+ traceback.print_exc()
1158
+ error_msg = f'❌ Failed to retrieve Weibo data: {str(e)}. \nMost likely you are not login, please go to: [Weibo login page](https://passport.weibo.com/sso/signin?entry=miniblog&source=miniblog) and login.'
1159
+ logger.error(error_msg)
1160
+ return ActionResult(error=error_msg, extracted_content=error_msg)
1161
+
1162
+
1163
+ @self.registry.action(
1164
+ 'Skill: Douyin API - Access Douyin platform data including search, video details, comments, user profiles, and videos. Methods: search_content_by_keyword, fetch_video_details, fetch_all_video_comments, fetch_user_info, fetch_all_user_videos.',
1165
+ param_model=SkillDouyinAction,
1166
+ )
1167
+ async def skill_douyin(
1168
+ params: SkillDouyinAction,
1169
+ browser_manager: BrowserManager,
1170
+ file_system: CustomFileSystem
1171
+ ):
1172
+ """
1173
+ Skill: Douyin API integration
1174
+
1175
+ Available methods:
1176
+ - search_content_by_keyword: Search content by keyword with filtering options
1177
+ - fetch_video_details: Get detailed information about specific video
1178
+ - fetch_all_video_comments: Get all comments for specific video
1179
+ - fetch_user_info: Get user profile information
1180
+ - fetch_all_user_videos: Get all videos by a user
1181
+ """
1182
+ try:
1183
+ from vibe_surf.tools.website_api.douyin.client import DouyinApiClient
1184
+
1185
+ # Initialize client
1186
+ dy_client = DouyinApiClient(browser_session=browser_manager.main_browser_session)
1187
+ await dy_client.setup()
1188
+
1189
+ # Parse params JSON string
1190
+ import json
1191
+ from json_repair import repair_json
1192
+ try:
1193
+ method_params = json.loads(params.params)
1194
+ except json.JSONDecodeError:
1195
+ method_params = json.loads(repair_json(params.params))
1196
+
1197
+ # Execute the requested method
1198
+ result = None
1199
+ if params.method == "search_content_by_keyword":
1200
+ result = await dy_client.search_content_by_keyword(**method_params)
1201
+ elif params.method == "fetch_video_details":
1202
+ result = await dy_client.fetch_video_details(**method_params)
1203
+ elif params.method == "fetch_all_video_comments":
1204
+ result = await dy_client.fetch_all_video_comments(**method_params)
1205
+ elif params.method == "fetch_user_info":
1206
+ result = await dy_client.fetch_user_info(**method_params)
1207
+ elif params.method == "fetch_all_user_videos":
1208
+ result = await dy_client.fetch_all_user_videos(**method_params)
1209
+ else:
1210
+ return ActionResult(error=f"Unknown method: {params.method}")
1211
+
1212
+ # Save result to file
1213
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1214
+ filename = f"douyin_{params.method}_{timestamp}.json"
1215
+ filepath = file_system.get_dir() / "data" / filename
1216
+ filepath.parent.mkdir(exist_ok=True)
1217
+
1218
+ with open(filepath, "w", encoding="utf-8") as f:
1219
+ json.dump(result, f, ensure_ascii=False, indent=2)
1220
+
1221
+ # Format result as markdown
1222
+ if isinstance(result, list):
1223
+ display_count = min(5, len(result))
1224
+ md_content = f"## Douyin {params.method.replace('_', ' ').title()}\n\n"
1225
+ md_content += f"Showing {display_count} of {len(result)} results:\n\n"
1226
+ for i, item in enumerate(result[:display_count]):
1227
+ md_content += f"### Result {i+1}\n"
1228
+ for key, value in item.items():
1229
+ if not value:
1230
+ continue
1231
+ if isinstance(value, str) and len(value) > 200:
1232
+ md_content += f"- **{key}**: {value[:200]}...\n"
1233
+ else:
1234
+ md_content += f"- **{key}**: {value}\n"
1235
+ md_content += "\n"
1236
+ else:
1237
+ md_content = f"## Douyin {params.method.replace('_', ' ').title()}\n\n"
1238
+ for key, value in result.items():
1239
+ if isinstance(value, str) and len(value) > 200:
1240
+ md_content += f"- **{key}**: {value[:200]}...\n"
1241
+ else:
1242
+ md_content += f"- **{key}**: {value}\n"
1243
+ md_content += "\n"
1244
+
1245
+ # Add file path to markdown
1246
+ relative_path = str(filepath.relative_to(file_system.get_dir()))
1247
+ md_content += f"\n> 📁 Full data saved to: [{filename}]({relative_path})\n"
1248
+ md_content += f"> 💡 Click the link above to view all results.\n"
1249
+
1250
+ logger.info(f'🎵 Douyin data retrieved with method: {params.method}')
1251
+
1252
+ # Close client
1253
+ await dy_client.close()
1254
+
1255
+ return ActionResult(
1256
+ extracted_content=md_content
1257
+ )
1258
+
1259
+ except Exception as e:
1260
+ error_msg = f'❌ Failed to retrieve Douyin data: {str(e)}'
1261
+ logger.error(error_msg)
1262
+ return ActionResult(error=error_msg, extracted_content=error_msg)
1263
+
1264
+
1265
+ @self.registry.action(
1266
+ 'Skill: YouTube API - Access YouTube platform data including search, video details, comments, channel info, and trending videos. Methods: search_videos, get_video_details, get_video_comments, get_channel_info, get_channel_videos, get_trending_videos.',
1267
+ param_model=SkillYoutubeAction,
1268
+ )
1269
+ async def skill_youtube(
1270
+ params: SkillYoutubeAction,
1271
+ browser_manager: BrowserManager,
1272
+ file_system: CustomFileSystem
1273
+ ):
1274
+ """
1275
+ Skill: YouTube API integration
1276
+
1277
+ Available methods:
1278
+ - search_videos: Search videos by keyword
1279
+ - get_video_details: Get detailed information about specific video
1280
+ - get_video_comments: Get comments for specific video
1281
+ - get_channel_info: Get channel information
1282
+ - get_channel_videos: Get videos from specific channel
1283
+ - get_trending_videos: Get trending videos
1284
+ """
1285
+ try:
1286
+ from vibe_surf.tools.website_api.youtube.client import YouTubeApiClient
1287
+
1288
+ # Initialize client
1289
+ yt_client = YouTubeApiClient(browser_session=browser_manager.main_browser_session)
1290
+ await yt_client.setup()
1291
+
1292
+ # Parse params JSON string
1293
+ import json
1294
+ from json_repair import repair_json
1295
+ try:
1296
+ method_params = json.loads(params.params)
1297
+ except json.JSONDecodeError:
1298
+ method_params = json.loads(repair_json(params.params))
1299
+
1300
+ # Execute the requested method
1301
+ result = None
1302
+ if params.method == "search_videos":
1303
+ result = await yt_client.search_videos(**method_params)
1304
+ elif params.method == "get_video_details":
1305
+ result = await yt_client.get_video_details(**method_params)
1306
+ elif params.method == "get_video_comments":
1307
+ result = await yt_client.get_video_comments(**method_params)
1308
+ elif params.method == "get_channel_info":
1309
+ result = await yt_client.get_channel_info(**method_params)
1310
+ elif params.method == "get_channel_videos":
1311
+ result = await yt_client.get_channel_videos(**method_params)
1312
+ elif params.method == "get_trending_videos":
1313
+ result = await yt_client.get_trending_videos()
1314
+ else:
1315
+ return ActionResult(error=f"Unknown method: {params.method}")
1316
+
1317
+ # Save result to file
1318
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1319
+ filename = f"youtube_{params.method}_{timestamp}.json"
1320
+ filepath = file_system.get_dir() / "data" / filename
1321
+ filepath.parent.mkdir(exist_ok=True)
1322
+
1323
+ with open(filepath, "w", encoding="utf-8") as f:
1324
+ json.dump(result, f, ensure_ascii=False, indent=2)
1325
+
1326
+ # Format result as markdown
1327
+ if isinstance(result, list):
1328
+ display_count = min(5, len(result))
1329
+ md_content = f"## YouTube {params.method.replace('_', ' ').title()}\n\n"
1330
+ md_content += f"Showing {display_count} of {len(result)} results:\n\n"
1331
+ for i, item in enumerate(result[:display_count]):
1332
+ md_content += f"### Result {i+1}\n"
1333
+ for key, value in item.items():
1334
+ if not value:
1335
+ continue
1336
+ if isinstance(value, str) and len(value) > 200:
1337
+ md_content += f"- **{key}**: {value[:200]}...\n"
1338
+ else:
1339
+ md_content += f"- **{key}**: {value}\n"
1340
+ md_content += "\n"
1341
+ else:
1342
+ md_content = f"## YouTube {params.method.replace('_', ' ').title()}\n\n"
1343
+ for key, value in result.items():
1344
+ if isinstance(value, str) and len(value) > 200:
1345
+ md_content += f"- **{key}**: {value[:200]}...\n"
1346
+ else:
1347
+ md_content += f"- **{key}**: {value}\n"
1348
+ md_content += "\n"
1349
+
1350
+ # Add file path to markdown
1351
+ relative_path = str(filepath.relative_to(file_system.get_dir()))
1352
+ md_content += f"\n> 📁 Full data saved to: [{filename}]({relative_path})\n"
1353
+ md_content += f"> 💡 Click the link above to view all results.\n"
1354
+
1355
+ logger.info(f'🎬 YouTube data retrieved with method: {params.method}')
1356
+
1357
+ # Close client
1358
+ await yt_client.close()
1359
+
1360
+ return ActionResult(
1361
+ extracted_content=md_content
1362
+ )
1363
+
1364
+ except Exception as e:
1365
+ error_msg = f'❌ Failed to retrieve YouTube data: {str(e)}'
1366
+ logger.error(error_msg)
1367
+ return ActionResult(error=error_msg, extracted_content=error_msg)
946
1368
 
947
1369
 
948
1370
  async def _extract_google_results_rule_based(self, browser_session):
vibe_surf/tools/views.py CHANGED
@@ -211,3 +211,78 @@ class SkillFinanceAction(BaseModel):
211
211
  ge=1,
212
212
  le=20,
213
213
  )
214
+
215
+
216
+ class DownloadMediaAction(BaseModel):
217
+ """Parameters for downloading media from URL"""
218
+ url: str = Field(
219
+ description='URL of the media to download',
220
+ )
221
+ filename: str | None = Field(
222
+ default=None,
223
+ description='Optional custom filename. If not provided, will auto-detect from URL or Content-Disposition header',
224
+ )
225
+
226
+
227
+ class SkillXhsAction(BaseModel):
228
+ """Parameters for skill_xhs action - Xiaohongshu API skill"""
229
+ method: str = Field(
230
+ description='''Xiaohongshu API method name. Available methods:
231
+ - search_content_by_keyword: Search content by keyword, params required: {"keyword": "search keyword", "page": 1, "page_size": 20}
232
+ - fetch_content_details: Get content details, params required: {"content_id": "content ID", "xsec_token": "security token"}
233
+ - fetch_all_content_comments: Get all comments for content, params required: {"content_id": "content ID", "xsec_token": "security token", "max_comments": 100}
234
+ - get_user_profile: Get user profile, params required: {"user_id": "user ID"}
235
+ - fetch_all_user_content: Get all content by user, params required: {"user_id": "user ID", "max_content": 100}
236
+ - get_home_recommendations: Get home page recommendations, params: {}'''
237
+ )
238
+ params: str = Field(
239
+ description='JSON string of method parameters, provide corresponding parameters according to the method parameter. Example: {"keyword": "food"}'
240
+ )
241
+
242
+
243
+ class SkillWeiboAction(BaseModel):
244
+ """Parameters for skill_weibo action - Weibo API skill"""
245
+ method: str = Field(
246
+ description='''Weibo API method name. Available methods:
247
+ - search_posts_by_keyword: Search posts by keyword, params required: {"keyword": "search keyword", "page": 1}
248
+ - get_post_detail: Get post details, params required: {"mid": "post ID"}
249
+ - get_all_post_comments: Get all comments for post, params required: {"mid": "post ID", "max_comments": 100}
250
+ - get_user_info: Get user information, params required: {"user_id": "user ID"}
251
+ - get_all_user_posts: Get all posts by user, params required: {"user_id": "user ID", "max_posts": 100}
252
+ - get_hot_posts: Get hot posts(推荐榜), params: {}
253
+ - get_trending_posts: Get trending posts(热搜榜), params: {}'''
254
+ )
255
+ params: str = Field(
256
+ description='JSON string of method parameters, provide corresponding parameters according to the method parameter. Example: {"keyword": "AI trending"}'
257
+ )
258
+
259
+
260
+ class SkillDouyinAction(BaseModel):
261
+ """Parameters for skill_douyin action - Douyin API skill"""
262
+ method: str = Field(
263
+ description='''Douyin API method name. Available methods:
264
+ - search_content_by_keyword: Search videos by keyword, params required: {"keyword": "search keyword", "offset": 0}
265
+ - fetch_video_details: Get video details, params required: {"aweme_id": "video ID"}
266
+ - fetch_all_video_comments: Get all comments for video, params required: {"aweme_id": "video ID", "max_comments": 100}
267
+ - fetch_user_info: Get user information, params required: {"sec_user_id": "user security ID"}
268
+ - fetch_all_user_videos: Get all videos by user, params required: {"sec_user_id": "user security ID", "max_videos": 100}'''
269
+ )
270
+ params: str = Field(
271
+ description='JSON string of method parameters, provide corresponding parameters according to the method parameter. Example: {"keyword": "music"}'
272
+ )
273
+
274
+
275
+ class SkillYoutubeAction(BaseModel):
276
+ """Parameters for skill_youtube action - YouTube API skill"""
277
+ method: str = Field(
278
+ description='''YouTube API method name. Available methods:
279
+ - search_videos: Search videos, params required: {"query": "search keyword", "max_results": 20}
280
+ - get_video_details: Get video details, params required: {"video_id": "video ID"}
281
+ - get_video_comments: Get video comments, params required: {"video_id": "video ID", "max_comments": 200}
282
+ - get_channel_info: Get channel information, params required: {"channel_id": "channel ID"}
283
+ - get_channel_videos: Get channel videos, params required: {"channel_id": "channel ID", "max_videos": 20}
284
+ - get_trending_videos: Get trending videos, params: {}'''
285
+ )
286
+ params: str = Field(
287
+ description='JSON string of method parameters, provide corresponding parameters according to the method parameter. Example: {"query": "tech tutorial", "max_results": 30}'
288
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibesurf
3
- Version: 0.1.26
3
+ Version: 0.1.27
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
- [![Discord](https://img.shields.io/discord/1303749220842340412?color=7289DA&label=Discord&logo=discord&logoColor=white)](https://discord.gg/TXNnP9gJ)
51
+ [![Discord](https://img.shields.io/discord/1303749220842340412?color=7289DA&label=Discord&logo=discord&logoColor=white)](https://discord.gg/EZ2YnUXP)
51
52
  [![WarmShao](https://img.shields.io/twitter/follow/warmshao?style=social)](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
- ## 🗺️ Roadmap
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
- - [ ] **Smart Skills System**: Add `/search` for quick information search and `/crawl` for automatic website data extraction
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
- Licensed under the [Apache License 2.0](LICENSE).
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=Y9o7KiJWiG6n9XbSpMICgNgajFRbL4an-gN1BQc-jwM,706
2
+ vibe_surf/_version.py,sha256=o2dyLbB_Uhc2yY2R7iheES_lRnDBGV9Hc4iNgiJ_XTo,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=sTUO4xAiznr7RRzdrRYzXENos9XovicZw8ow2UuJsyI,74286
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=xV0nHo_TCb7b7QYhIee4cLzH-1rqJswYwH7GEwyQmqc,33980
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,18 @@ 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=tacxKUJL6uOt04f52_iIw1cs-FT-mBgIPmAsIc4Hww0,23730
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=O8y1noWyY8y-j8I7vF4oOaVDybINNXiNXWNwGJJ5xsM,91500
97
- vibe_surf/tools/views.py,sha256=AEAPzML-lqWJ7dBMjXTl7o-rk4hp5PGaPRqLyilJUl8,7789
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
- vibesurf-0.1.26.dist-info/licenses/LICENSE,sha256=czn6QYya0-jhLnStD9JqnMS-hwP5wRByipkrGTvoXLI,11355
100
- vibesurf-0.1.26.dist-info/METADATA,sha256=8Bdh3-15Hl0KbmB0ghk9KNQiveDnVGnhzZ-4Dv0MVjc,5190
101
- vibesurf-0.1.26.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
102
- vibesurf-0.1.26.dist-info/entry_points.txt,sha256=UxqpvMocL-PR33S6vLF2OmXn-kVzM-DneMeZeHcPMM8,48
103
- vibesurf-0.1.26.dist-info/top_level.txt,sha256=VPZGHqSb6EEqcJ4ZX6bHIuWfon5f6HXl3c7BYpbRqnY,10
104
- vibesurf-0.1.26.dist-info/RECORD,,
99
+ vibesurf-0.1.27.dist-info/licenses/LICENSE,sha256=vRmTjOYvD8RLiSGYYmFHnveYNswtO1uvSk1sd-Eu7sg,2037
100
+ vibesurf-0.1.27.dist-info/METADATA,sha256=JAb_jozN1kp1YVRowpkkoX0xx1eWm_3bo-GLST2bjPo,5836
101
+ vibesurf-0.1.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
102
+ vibesurf-0.1.27.dist-info/entry_points.txt,sha256=UxqpvMocL-PR33S6vLF2OmXn-kVzM-DneMeZeHcPMM8,48
103
+ vibesurf-0.1.27.dist-info/top_level.txt,sha256=VPZGHqSb6EEqcJ4ZX6bHIuWfon5f6HXl3c7BYpbRqnY,10
104
+ vibesurf-0.1.27.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.
@@ -1,201 +0,0 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the purposes
44
- of this License, Derivative Works shall not include works that remain
45
- separable from, or merely link (or bind by name) to the interfaces of,
46
- the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including
49
- the original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright owner
52
- or by an individual or Legal Entity authorized to submit on behalf of
53
- the copyright owner. For the purposes of this definition, "submitted"
54
- means any form of electronic, verbal, or written communication sent
55
- to the Licensor or its representatives, including but not limited to
56
- communication on electronic mailing lists, source code control systems,
57
- and issue tracking systems that are managed by, or on behalf of, the
58
- Licensor for the purpose of discussing and improving the Work, but
59
- excluding communication that is conspicuously marked or otherwise
60
- designated in writing by the copyright owner as "Not a Contribution."
61
-
62
- "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by Licensor and
64
- subsequently incorporated within the Work.
65
-
66
- 2. Grant of Copyright License. Subject to the terms and conditions of
67
- this License, each Contributor hereby grants to You a perpetual,
68
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
- copyright license to reproduce, prepare Derivative Works of,
70
- publicly display, publicly perform, sublicense, and distribute the
71
- Work and such Derivative Works in Source or Object form.
72
-
73
- 3. Grant of Patent License. Subject to the terms and conditions of
74
- this License, each Contributor hereby grants to You a perpetual,
75
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
- (except as stated in this section) patent license to make, have made,
77
- use, offer to sell, sell, import, and otherwise transfer the Work,
78
- where such license applies only to those patent claims licensable
79
- by such Contributor that are necessarily infringed by their
80
- Contribution(s) alone or by combination of their Contribution(s)
81
- with the Work to which such Contribution(s) was submitted. If You
82
- institute patent litigation against any entity (including a
83
- cross-claim or counterclaim in a lawsuit) alleging that the Work
84
- or a Contribution incorporated within the Work constitutes direct
85
- or contributory patent infringement, then any patent licenses
86
- granted to You under this License for that Work shall terminate
87
- as of the date such litigation is filed.
88
-
89
- 4. Redistribution. You may reproduce and distribute copies of the
90
- Work or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You
92
- meet the following conditions:
93
-
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
96
-
97
- (b) You must cause any modified files to carry prominent notices
98
- stating that You changed the files; and
99
-
100
- (c) You must retain, in the Source form of any Derivative Works
101
- that You distribute, all copyright, patent, trademark, and
102
- attribution notices from the Source form of the Work,
103
- excluding those notices that do not pertain to any part of
104
- the Derivative Works; and
105
-
106
- (d) If the Work includes a "NOTICE" text file as part of its
107
- distribution, then any Derivative Works that You distribute must
108
- include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding those notices that do not
110
- pertain to any part of the Derivative Works, in at least one
111
- of the following places: within a NOTICE text file distributed
112
- as part of the Derivative Works; within the Source form or
113
- documentation, if provided along with the Derivative Works; or,
114
- within a display generated by the Derivative Works, if and
115
- wherever such third-party notices normally appear. The contents
116
- of the NOTICE file are for informational purposes only and
117
- do not modify the License. You may add Your own attribution
118
- notices within Derivative Works that You distribute, alongside
119
- or as an addendum to the NOTICE text from the Work, provided
120
- that such additional attribution notices cannot be construed
121
- as modifying the License.
122
-
123
- You may add Your own copyright statement to Your modifications and
124
- may provide additional or different license terms and conditions
125
- for use, reproduction, or distribution of Your modifications, or
126
- for any such Derivative Works as a whole, provided Your use,
127
- reproduction, and distribution of the Work otherwise complies with
128
- the conditions stated in this License.
129
-
130
- 5. Submission of Contributions. Unless You explicitly state otherwise,
131
- any Contribution intentionally submitted for inclusion in the Work
132
- by You to the Licensor shall be under the terms and conditions of
133
- this License, without any additional terms or conditions.
134
- Notwithstanding the above, nothing herein shall supersede or modify
135
- the terms of any separate license agreement you may have executed
136
- with Licensor regarding such Contributions.
137
-
138
- 6. Trademarks. This License does not grant permission to use the trade
139
- names, trademarks, service marks, or product names of the Licensor,
140
- except as required for reasonable and customary use in describing the
141
- origin of the Work and reproducing the content of the NOTICE file.
142
-
143
- 7. Disclaimer of Warranty. Unless required by applicable law or
144
- agreed to in writing, Licensor provides the Work (and each
145
- Contributor provides its Contributions) on an "AS IS" BASIS,
146
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
- implied, including, without limitation, any warranties or conditions
148
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
- PARTICULAR PURPOSE. You are solely responsible for determining the
150
- appropriateness of using or redistributing the Work and assume any
151
- risks associated with Your exercise of permissions under this License.
152
-
153
- 8. Limitation of Liability. In no event and under no legal theory,
154
- whether in tort (including negligence), contract, or otherwise,
155
- unless required by applicable law (such as deliberate and grossly
156
- negligent acts) or agreed to in writing, shall any Contributor be
157
- liable to You for damages, including any direct, indirect, special,
158
- incidental, or consequential damages of any character arising as a
159
- result of this License or out of the use or inability to use the
160
- Work (including but not limited to damages for loss of goodwill,
161
- work stoppage, computer failure or malfunction, or any and all
162
- other commercial damages or losses), even if such Contributor
163
- has been advised of the possibility of such damages.
164
-
165
- 9. Accepting Warranty or Additional Liability. While redistributing
166
- the Work or Derivative Works thereof, You may choose to offer,
167
- and charge a fee for, acceptance of support, warranty, indemnity,
168
- or other liability obligations and/or rights consistent with this
169
- License. However, in accepting such obligations, You may act only
170
- on Your own behalf and on Your sole responsibility, not on behalf
171
- of any other Contributor, and only if You agree to indemnify,
172
- defend, and hold each Contributor harmless for any liability
173
- incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or additional liability.
175
-
176
- END OF TERMS AND CONDITIONS
177
-
178
- APPENDIX: How to apply the Apache License to your work.
179
-
180
- To apply the Apache License to your work, attach the following
181
- boilerplate notice, with the fields enclosed by brackets "[]"
182
- replaced with your own identifying information. (Don't include
183
- the brackets!) The text should be enclosed in the appropriate
184
- comment syntax for the file format. We also recommend that a
185
- file or class name and description of purpose be included on the
186
- same "printed page" as the copyright notice for easier
187
- identification within third-party archives.
188
-
189
- Copyright [yyyy] [name of copyright owner]
190
-
191
- Licensed under the Apache License, Version 2.0 (the "License");
192
- you may not use this file except in compliance with the License.
193
- You may obtain a copy of the License at
194
-
195
- http://www.apache.org/licenses/LICENSE-2.0
196
-
197
- Unless required by applicable law or agreed to in writing, software
198
- distributed under the License is distributed on an "AS IS" BASIS,
199
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
- See the License for the specific language governing permissions and
201
- limitations under the License.