修复代码格式和文件名大小写问题

This commit is contained in:
Windpicker-owo
2025-08-31 20:50:17 +08:00
parent df29014e41
commit 8149731925
218 changed files with 6913 additions and 8257 deletions

View File

@@ -17,51 +17,51 @@ logger = get_logger("bilibili_tool")
class BilibiliVideoAnalyzer:
"""哔哩哔哩视频分析器集成视频下载和AI分析功能"""
def __init__(self):
self.video_analyzer = get_video_analyzer()
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Referer': 'https://www.bilibili.com/',
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Referer": "https://www.bilibili.com/",
}
def extract_bilibili_url(self, text: str) -> Optional[str]:
"""从文本中提取哔哩哔哩视频链接"""
# 哔哩哔哩短链接模式
short_pattern = re.compile(r'https?://b23\.tv/[\w]+', re.IGNORECASE)
short_pattern = re.compile(r"https?://b23\.tv/[\w]+", re.IGNORECASE)
# 哔哩哔哩完整链接模式
full_pattern = re.compile(r'https?://(?:www\.)?bilibili\.com/video/(?:BV[\w]+|av\d+)', re.IGNORECASE)
full_pattern = re.compile(r"https?://(?:www\.)?bilibili\.com/video/(?:BV[\w]+|av\d+)", re.IGNORECASE)
# 先匹配短链接
short_match = short_pattern.search(text)
if short_match:
return short_match.group(0)
# 再匹配完整链接
full_match = full_pattern.search(text)
if full_match:
return full_match.group(0)
return None
async def get_video_info(self, url: str) -> Optional[Dict[str, Any]]:
"""获取哔哩哔哩视频基本信息"""
try:
logger.info(f"🔍 解析视频URL: {url}")
# 如果是短链接,先解析为完整链接
if 'b23.tv' in url:
if "b23.tv" in url:
logger.info("🔗 检测到短链接,正在解析...")
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(url, headers=self.headers, allow_redirects=True) as response:
url = str(response.url)
logger.info(f"✅ 短链接解析完成: {url}")
# 提取BV号或AV号
bv_match = re.search(r'BV([\w]+)', url)
av_match = re.search(r'av(\d+)', url)
bv_match = re.search(r"BV([\w]+)", url)
av_match = re.search(r"av(\d+)", url)
if bv_match:
bvid = f"BV{bv_match.group(1)}"
api_url = f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}"
@@ -73,7 +73,7 @@ class BilibiliVideoAnalyzer:
else:
logger.error("❌ 无法从URL中提取视频ID")
return None
# 获取视频信息
logger.info("📡 正在获取视频信息...")
timeout = aiohttp.ClientTimeout(total=30)
@@ -83,38 +83,39 @@ class BilibiliVideoAnalyzer:
logger.error(f"❌ API请求失败状态码: {response.status}")
return None
data = await response.json()
if data.get('code') != 0:
error_msg = data.get('message', '未知错误')
if data.get("code") != 0:
error_msg = data.get("message", "未知错误")
logger.error(f"❌ B站API返回错误: {error_msg} (code: {data.get('code')})")
return None
video_data = data['data']
video_data = data["data"]
# 验证必要字段
if not video_data.get('title'):
if not video_data.get("title"):
logger.error("❌ 视频数据不完整,缺少标题")
return None
result = {
'title': video_data.get('title', ''),
'desc': video_data.get('desc', ''),
'duration': video_data.get('duration', 0),
'view': video_data.get('stat', {}).get('view', 0),
'like': video_data.get('stat', {}).get('like', 0),
'coin': video_data.get('stat', {}).get('coin', 0),
'favorite': video_data.get('stat', {}).get('favorite', 0),
'share': video_data.get('stat', {}).get('share', 0),
'owner': video_data.get('owner', {}).get('name', ''),
'pubdate': video_data.get('pubdate', 0),
'aid': video_data.get('aid'),
'bvid': video_data.get('bvid'),
'cid': video_data.get('cid') or (video_data.get('pages', [{}])[0].get('cid') if video_data.get('pages') else None)
"title": video_data.get("title", ""),
"desc": video_data.get("desc", ""),
"duration": video_data.get("duration", 0),
"view": video_data.get("stat", {}).get("view", 0),
"like": video_data.get("stat", {}).get("like", 0),
"coin": video_data.get("stat", {}).get("coin", 0),
"favorite": video_data.get("stat", {}).get("favorite", 0),
"share": video_data.get("stat", {}).get("share", 0),
"owner": video_data.get("owner", {}).get("name", ""),
"pubdate": video_data.get("pubdate", 0),
"aid": video_data.get("aid"),
"bvid": video_data.get("bvid"),
"cid": video_data.get("cid")
or (video_data.get("pages", [{}])[0].get("cid") if video_data.get("pages") else None),
}
logger.info(f"✅ 视频信息获取成功: {result['title']}")
return result
except asyncio.TimeoutError:
logger.error("❌ 获取视频信息超时")
return None
@@ -125,15 +126,15 @@ class BilibiliVideoAnalyzer:
logger.error(f"❌ 获取哔哩哔哩视频信息时发生未知错误: {e}")
logger.exception("详细错误信息:")
return None
async def get_video_stream_url(self, aid: int, cid: int) -> Optional[str]:
"""获取视频流URL"""
try:
logger.info(f"🎥 获取视频流URL: aid={aid}, cid={cid}")
# 构建播放信息API请求
api_url = f"https://api.bilibili.com/x/player/playurl?avid={aid}&cid={cid}&qn=80&type=&otype=json&fourk=1&fnver=0&fnval=4048&session="
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(api_url, headers=self.headers) as response:
@@ -141,38 +142,38 @@ class BilibiliVideoAnalyzer:
logger.error(f"❌ 播放信息API请求失败状态码: {response.status}")
return None
data = await response.json()
if data.get('code') != 0:
error_msg = data.get('message', '未知错误')
if data.get("code") != 0:
error_msg = data.get("message", "未知错误")
logger.error(f"❌ 获取播放信息失败: {error_msg} (code: {data.get('code')})")
return None
play_data = data['data']
play_data = data["data"]
# 尝试获取DASH格式的视频流
if 'dash' in play_data and play_data['dash'].get('video'):
videos = play_data['dash']['video']
if "dash" in play_data and play_data["dash"].get("video"):
videos = play_data["dash"]["video"]
logger.info(f"🎬 找到 {len(videos)} 个DASH视频流")
# 选择最高质量的视频流
video_stream = max(videos, key=lambda x: x.get('bandwidth', 0))
stream_url = video_stream.get('baseUrl') or video_stream.get('base_url')
video_stream = max(videos, key=lambda x: x.get("bandwidth", 0))
stream_url = video_stream.get("baseUrl") or video_stream.get("base_url")
if stream_url:
logger.info(f"✅ 获取到DASH视频流URL (带宽: {video_stream.get('bandwidth', 0)})")
return stream_url
# 降级到FLV格式
if 'durl' in play_data and play_data['durl']:
if "durl" in play_data and play_data["durl"]:
logger.info("📹 使用FLV格式视频流")
stream_url = play_data['durl'][0].get('url')
stream_url = play_data["durl"][0].get("url")
if stream_url:
logger.info("✅ 获取到FLV视频流URL")
return stream_url
logger.error("❌ 未找到可用的视频流")
return None
except asyncio.TimeoutError:
logger.error("❌ 获取视频流URL超时")
return None
@@ -183,55 +184,55 @@ class BilibiliVideoAnalyzer:
logger.error(f"❌ 获取视频流URL时发生未知错误: {e}")
logger.exception("详细错误信息:")
return None
async def download_video_bytes(self, stream_url: str, max_size_mb: int = 100) -> Optional[bytes]:
"""下载视频字节数据
Args:
stream_url: 视频流URL
max_size_mb: 最大下载大小限制MB默认100MB
Returns:
视频字节数据或None
"""
try:
logger.info(f"📥 开始下载视频: {stream_url[:50]}...")
# 设置超时和大小限制
timeout = aiohttp.ClientTimeout(total=300, connect=30) # 5分钟总超时30秒连接超时
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(stream_url, headers=self.headers) as response:
if response.status != 200:
logger.error(f"❌ 下载失败HTTP状态码: {response.status}")
return None
# 检查内容长度
content_length = response.headers.get('content-length')
content_length = response.headers.get("content-length")
if content_length:
size_mb = int(content_length) / 1024 / 1024
if size_mb > max_size_mb:
logger.error(f"❌ 视频文件过大: {size_mb:.1f}MB > {max_size_mb}MB")
return None
logger.info(f"📊 预计下载大小: {size_mb:.1f}MB")
# 分块下载并监控大小
video_bytes = bytearray()
downloaded_mb = 0
async for chunk in response.content.iter_chunked(8192): # 8KB块
video_bytes.extend(chunk)
downloaded_mb = len(video_bytes) / 1024 / 1024
# 检查大小限制
if downloaded_mb > max_size_mb:
logger.error(f"❌ 下载中止,文件过大: {downloaded_mb:.1f}MB > {max_size_mb}MB")
return None
final_size_mb = len(video_bytes) / 1024 / 1024
logger.info(f"✅ 视频下载完成,实际大小: {final_size_mb:.2f}MB")
return bytes(video_bytes)
except asyncio.TimeoutError:
logger.error("❌ 下载超时")
return None
@@ -242,93 +243,84 @@ class BilibiliVideoAnalyzer:
logger.error(f"❌ 下载视频时发生未知错误: {e}")
logger.exception("详细错误信息:")
return None
async def analyze_bilibili_video(self, url: str, prompt: str = None) -> Dict[str, Any]:
"""分析哔哩哔哩视频并返回详细信息和AI分析结果"""
try:
logger.info(f"🎬 开始分析哔哩哔哩视频: {url}")
# 1. 获取视频基本信息
video_info = await self.get_video_info(url)
if not video_info:
logger.error("❌ 无法获取视频基本信息")
return {"error": "无法获取视频信息"}
logger.info(f"📺 视频标题: {video_info['title']}")
logger.info(f"👤 UP主: {video_info['owner']}")
logger.info(f"⏱️ 时长: {video_info['duration']}")
# 2. 获取视频流URL
stream_url = await self.get_video_stream_url(video_info['aid'], video_info['cid'])
stream_url = await self.get_video_stream_url(video_info["aid"], video_info["cid"])
if not stream_url:
logger.warning("⚠️ 无法获取视频流,仅返回基本信息")
return {
"video_info": video_info,
"error": "无法获取视频流,仅返回基本信息"
}
return {"video_info": video_info, "error": "无法获取视频流,仅返回基本信息"}
# 3. 下载视频
video_bytes = await self.download_video_bytes(stream_url)
if not video_bytes:
logger.warning("⚠️ 视频下载失败,仅返回基本信息")
return {
"video_info": video_info,
"error": "视频下载失败,仅返回基本信息"
}
return {"video_info": video_info, "error": "视频下载失败,仅返回基本信息"}
# 4. 构建增强的元数据信息
enhanced_metadata = {
"title": video_info['title'],
"uploader": video_info['owner'],
"duration": video_info['duration'],
"view_count": video_info['view'],
"like_count": video_info['like'],
"description": video_info['desc'],
"bvid": video_info['bvid'],
"aid": video_info['aid'],
"title": video_info["title"],
"uploader": video_info["owner"],
"duration": video_info["duration"],
"view_count": video_info["view"],
"like_count": video_info["like"],
"description": video_info["desc"],
"bvid": video_info["bvid"],
"aid": video_info["aid"],
"file_size": len(video_bytes),
"source": "bilibili"
"source": "bilibili",
}
# 5. 使用新的视频分析API传递完整的元数据
logger.info("🤖 开始AI视频分析...")
analysis_result = await self.video_analyzer.analyze_video_from_bytes(
video_bytes=video_bytes,
filename=f"{video_info['title']}.mp4",
prompt=prompt # 使用新API的prompt参数而不是user_question
prompt=prompt, # 使用新API的prompt参数而不是user_question
)
# 6. 检查分析结果
if not analysis_result or not analysis_result.get('summary'):
if not analysis_result or not analysis_result.get("summary"):
logger.error("❌ 视频分析失败或返回空结果")
return {
"video_info": video_info,
"error": "视频分析失败,仅返回基本信息"
}
return {"video_info": video_info, "error": "视频分析失败,仅返回基本信息"}
# 7. 格式化返回结果
duration_str = f"{video_info['duration'] // 60}{video_info['duration'] % 60}"
result = {
"video_info": {
"标题": video_info['title'],
"UP主": video_info['owner'],
"标题": video_info["title"],
"UP主": video_info["owner"],
"时长": duration_str,
"播放量": f"{video_info['view']:,}",
"点赞": f"{video_info['like']:,}",
"投币": f"{video_info['coin']:,}",
"收藏": f"{video_info['favorite']:,}",
"转发": f"{video_info['share']:,}",
"简介": video_info['desc'][:200] + "..." if len(video_info['desc']) > 200 else video_info['desc']
"简介": video_info["desc"][:200] + "..." if len(video_info["desc"]) > 200 else video_info["desc"],
},
"ai_analysis": analysis_result.get('summary', ''),
"ai_analysis": analysis_result.get("summary", ""),
"success": True,
"metadata": enhanced_metadata # 添加元数据信息
"metadata": enhanced_metadata, # 添加元数据信息
}
logger.info("✅ 哔哩哔哩视频分析完成")
return result
except Exception as e:
error_msg = f"分析哔哩哔哩视频时发生异常: {str(e)}"
logger.error(f"{error_msg}")
@@ -339,9 +331,10 @@ class BilibiliVideoAnalyzer:
# 全局实例
_bilibili_analyzer = None
def get_bilibili_analyzer() -> BilibiliVideoAnalyzer:
"""获取哔哩哔哩视频分析器实例(单例模式)"""
global _bilibili_analyzer
if _bilibili_analyzer is None:
_bilibili_analyzer = BilibiliVideoAnalyzer()
return _bilibili_analyzer
return _bilibili_analyzer

View File

@@ -15,75 +15,78 @@ logger = get_logger("bilibili_tool")
class BilibiliTool(BaseTool):
"""哔哩哔哩视频观看体验工具 - 像真实用户一样观看和评价用户分享的哔哩哔哩视频"""
name = "bilibili_video_watcher"
description = "观看用户分享的哔哩哔哩视频,以真实用户视角给出观看感受和评价"
available_for_llm = True
parameters = [
("url", ToolParamType.STRING, "用户分享给我的哔哩哔哩视频链接,我会认真观看这个视频并给出真实的观看感受", True, None),
("interest_focus", ToolParamType.STRING, "你特别感兴趣的方面(如:搞笑内容、学习资料、美食、游戏、音乐等),我会重点关注这些内容", False, None)
(
"url",
ToolParamType.STRING,
"用户分享给我的哔哩哔哩视频链接,我会认真观看这个视频并给出真实的观看感受",
True,
None,
),
(
"interest_focus",
ToolParamType.STRING,
"你特别感兴趣的方面(如:搞笑内容、学习资料、美食、游戏、音乐等),我会重点关注这些内容",
False,
None,
),
]
def __init__(self, plugin_config: dict = None):
super().__init__(plugin_config)
self.analyzer = get_bilibili_analyzer()
async def execute(self, function_args: Dict[str, Any]) -> Dict[str, Any]:
"""执行哔哩哔哩视频观看体验"""
try:
url = function_args.get("url", "").strip()
interest_focus = function_args.get("interest_focus", "").strip() or None
if not url:
return {
"name": self.name,
"content": "🤔 你想让我看哪个视频呢?给我个链接吧!"
}
return {"name": self.name, "content": "🤔 你想让我看哪个视频呢?给我个链接吧!"}
logger.info(f"开始'观看'哔哩哔哩视频: {url}")
# 验证是否为哔哩哔哩链接
extracted_url = self.analyzer.extract_bilibili_url(url)
if not extracted_url:
return {
"name": self.name,
"content": "🤨 这好像不是哔哩哔哩的链接诶,我只会看哔哩哔哩的视频哦~ 给我一个bilibili.com或b23.tv的链接吧"
"content": "🤨 这好像不是哔哩哔哩的链接诶,我只会看哔哩哔哩的视频哦~ 给我一个bilibili.com或b23.tv的链接吧",
}
# 构建个性化的观看提示词
watch_prompt = self._build_watch_prompt(interest_focus)
# 执行视频分析
result = await self.analyzer.analyze_bilibili_video(extracted_url, watch_prompt)
if result.get("error"):
return {
"name": self.name,
"content": f"😔 唉,这个视频我看不了... {result['error']}\n可能是网络问题或者视频有限制"
"content": f"😔 唉,这个视频我看不了... {result['error']}\n可能是网络问题或者视频有限制",
}
# 格式化输出结果
video_info = result.get("video_info", {})
ai_analysis = result.get("ai_analysis", "")
# 构建个性化的观看体验报告
content = self._format_watch_experience(video_info, ai_analysis, interest_focus)
logger.info("✅ 哔哩哔哩视频观看体验完成")
return {
"name": self.name,
"content": content.strip()
}
return {"name": self.name, "content": content.strip()}
except Exception as e:
error_msg = f"😅 看视频的时候出了点问题: {str(e)}"
logger.error(error_msg)
return {
"name": self.name,
"content": error_msg
}
return {"name": self.name, "content": error_msg}
def _build_watch_prompt(self, interest_focus: str = None) -> str:
"""构建个性化的观看提示词"""
base_prompt = """请以一个真实哔哩哔哩用户的视角来观看用户分享给我的这个视频。用户特意分享了这个视频给我,我需要认真观看并给出真实的反馈。
@@ -95,17 +98,17 @@ class BilibiliTool(BaseTool):
4. 用轻松、自然的语气表达,就像在和分享视频的朋友聊天
5. 可以表达个人偏好,比如"我比较喜欢...""这种类型不太符合我的口味"
7. 对用户的分享表示感谢,体现出这是用户主动分享给我的内容"""
if interest_focus:
base_prompt += f"\n\n特别关注点:我对 {interest_focus} 相关的内容比较感兴趣,请重点评价这方面的内容。"
return base_prompt
def _format_watch_experience(self, video_info: Dict, ai_analysis: str, interest_focus: str = None) -> str:
"""格式化观看体验报告"""
# 根据播放量生成热度评价
view_count = video_info.get('播放量', '0').replace(',', '')
view_count = video_info.get("播放量", "0").replace(",", "")
if view_count.isdigit():
views = int(view_count)
if views > 1000000:
@@ -118,40 +121,42 @@ class BilibiliTool(BaseTool):
popularity = "🆕 比较新"
else:
popularity = "🤷‍♀️ 数据不明"
# 生成时长评价
duration = video_info.get('时长', '')
if '' in duration:
duration = video_info.get("时长", "")
if "" in duration:
time_comment = self._get_duration_comment(duration)
else:
time_comment = ""
content = f"""🎬 **谢谢你分享的这个哔哩哔哩视频!我认真看了一下~**
📺 **视频速览**
• 标题:{video_info.get('标题', '未知')}
• UP主{video_info.get('UP主', '未知')}
• 标题:{video_info.get("标题", "未知")}
• UP主{video_info.get("UP主", "未知")}
• 时长:{duration} {time_comment}
• 热度:{popularity} ({video_info.get('播放量', '0')}播放)
• 互动:👍{video_info.get('点赞', '0')} 🪙{video_info.get('投币', '0')}{video_info.get('收藏', '0')}
• 热度:{popularity} ({video_info.get("播放量", "0")}播放)
• 互动:👍{video_info.get("点赞", "0")} 🪙{video_info.get("投币", "0")}{video_info.get("收藏", "0")}
📝 **UP主说了什么**
{video_info.get('简介', '这个UP主很懒什么都没写...')[:150]}{'...' if len(video_info.get('简介', '')) > 150 else ''}
{video_info.get("简介", "这个UP主很懒什么都没写...")[:150]}{"..." if len(video_info.get("简介", "")) > 150 else ""}
🤔 **我的观看感受**
{ai_analysis}
"""
if interest_focus:
content += f"\n💭 **关于你感兴趣的'{interest_focus}'**\n我特别注意了这方面的内容,感觉{self._get_focus_comment()}~"
content += (
f"\n💭 **关于你感兴趣的'{interest_focus}'**\n我特别注意了这方面的内容,感觉{self._get_focus_comment()}~"
)
return content
def _get_duration_comment(self, duration: str) -> str:
"""根据时长生成评价"""
if '' in duration:
if "" in duration:
try:
minutes = int(duration.split('')[0])
minutes = int(duration.split("")[0])
if minutes < 3:
return "(短小精悍)"
elif minutes < 10:
@@ -163,17 +168,18 @@ class BilibiliTool(BaseTool):
except:
return ""
return ""
def _get_focus_comment(self) -> str:
"""生成关注点评价"""
import random
comments = [
"挺符合你的兴趣的",
"内容还算不错",
"可能会让你感兴趣",
"值得一看",
"可能不太符合你的口味",
"内容比较一般"
"内容比较一般",
]
return random.choice(comments)
@@ -190,11 +196,7 @@ class BilibiliPlugin(BasePlugin):
config_file_name: str = "config.toml"
# 配置节描述
config_section_descriptions = {
"plugin": "插件基本信息",
"bilibili": "哔哩哔哩视频观看配置",
"tool": "工具配置"
}
config_section_descriptions = {"plugin": "插件基本信息", "bilibili": "哔哩哔哩视频观看配置", "tool": "工具配置"}
# 配置Schema定义
config_schema: dict = {
@@ -212,12 +214,12 @@ class BilibiliPlugin(BasePlugin):
"tool": {
"available_for_llm": ConfigField(type=bool, default=True, description="是否对LLM可用"),
"name": ConfigField(type=str, default="bilibili_video_watcher", description="工具名称"),
"description": ConfigField(type=str, default="观看用户分享的哔哩哔哩视频并给出真实观看体验", description="工具描述"),
}
"description": ConfigField(
type=str, default="观看用户分享的哔哩哔哩视频并给出真实观看体验", description="工具描述"
),
},
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的工具组件"""
return [
(BilibiliTool.get_tool_info(), BilibiliTool)
]
return [(BilibiliTool.get_tool_info(), BilibiliTool)]