This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
openclaw-skills/skills/obsidian-kb/shortlink_resolver.py
T
OpenClaw Agent fbcc3df2cf feat(obsidian-kb): 实现短链接解析功能
- 新增 shortlink_resolver.py 模块
  * 检测 B站短链接(b23.tv)和其他短链接服务
  * 提供友好的用户引导信息
  * 支持 Playwright 浏览器自动化解析(可选)

- 集成到 handler.py
  * 短链接自动检测和提示
  * 智能区分短链接和完整 URL

- 更新 add_video.py
  * 支持短链接解析
  * 添加 --playwright 参数启用浏览器解析

- 新增 test_shortlink.py 测试脚本
  * 测试单个短链接检测
  * 测试多个短链接检测
  * 测试混合内容场景
  * 所有测试通过

- 更新文档
  * SKILL.md: 添加短链接说明
  * AUTO_TRIGGER.md: 添加短链接章节
  * obsidian-kb-improvements.md: 标记任务2为已完成

功能特点:
- 方案A(用户引导):已实现并作为默认行为
- 方案B(浏览器自动化):提供 PlaywrightResolver 类,可选使用
- 方案C(代理池):未实现(复杂度高,收益低)

验收标准达成:
✅ 短链接能够友好处理(显示引导信息)
✅ 用户不会感到困惑
✅ 支持 b23.tv 和其他短链接服务(检测)
✅ 集成到 handler.py
✅ 测试各种短链接场景
2026-02-06 21:22:45 +08:00

356 lines
10 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Short link resolver for obsidian-kb skill.
Provides utilities for detecting and resolving short links, particularly
Bilibili short links (b23.tv) which may be blocked by anti-scraping measures.
Supported short link services:
- Bilibili: b23.tv
- Generic URL shorteners (detection only)
"""
import re
from typing import Optional, Dict, List, Tuple
from urllib.parse import urlparse
class ShortLinkResolver:
"""Short link detection and resolution handler"""
# Pattern for Bilibili short links
BILIBILI_SHORT_PATTERN = re.compile(r'https?://b23\.tv/([a-zA-Z0-9]+)')
# Pattern for other short link services
OTHER_SHORT_PATTERNS = {
'bit.ly': re.compile(r'https?://bit\.ly/\w+'),
't.cn': re.compile(r'https?://t\.cn/\w+'),
'dwz.cn': re.compile(r'https?://dwz\.cn/\w+'),
'goo.gl': re.compile(r'https?://goo\.gl/\w+'),
'tinyurl.com': re.compile(r'https?://tinyurl\.com/\w+'),
}
@staticmethod
def detect_short_links(text: str) -> List[Dict[str, str]]:
"""
Detect short links in text.
Args:
text: Input text to search
Returns:
List of detected short links with metadata:
[
{
'url': 'https://b23.tv/xxxxx',
'platform': 'bilibili',
'is_short': True
},
...
]
"""
detected = []
# Check for Bilibili short links
for match in ShortLinkResolver.BILIBILI_SHORT_PATTERN.finditer(text):
detected.append({
'url': match.group(0),
'code': match.group(1),
'platform': 'bilibili',
'is_short': True
})
# Check for other short link services
for platform, pattern in ShortLinkResolver.OTHER_SHORT_PATTERNS.items():
for match in pattern.finditer(text):
detected.append({
'url': match.group(0),
'platform': platform,
'is_short': True
})
return detected
@staticmethod
def is_bilibili_short_link(url: str) -> bool:
"""
Check if URL is a Bilibili short link.
Args:
url: URL to check
Returns:
True if URL is a b23.tv short link
"""
return bool(ShortLinkResolver.BILIBILI_SHORT_PATTERN.match(url))
@staticmethod
def format_short_link_help(short_link: Optional[str] = None) -> str:
"""
Format help message for short links.
Args:
short_link: Optional short link URL to mention
Returns:
Help message
"""
lines = [
"🔗 检测到短链接",
"",
"⚠️ 由于反爬虫保护,短链接无法自动解析",
""
]
if short_link:
lines.append(f"📍 短链接: {short_link}")
lines.append("")
lines.extend([
"📱 手机用户:",
" 1. 点击/打开链接",
" 2. 在浏览器地址栏复制完整 URL",
" 3. 发送给助手",
"",
"💻 桌面用户:",
" 1. 发送 BV 号(如:BV1jcFqz7EFW)",
" 2. 或使用完整 URL",
"",
"💡 提示:完整 URL 格式为:https://www.bilibili.com/video/BVxxxxx"
])
return "\n".join(lines)
@staticmethod
def format_short_link_warning(short_links: List[Dict[str, str]]) -> str:
"""
Format warning message for multiple short links.
Args:
short_links: List of short link metadata
Returns:
Warning message
"""
if not short_links:
return ""
lines = [
"⚠️ 检测到短链接",
""
]
for i, link in enumerate(short_links, 1):
platform = link.get('platform', 'unknown')
url = link['url']
if platform == 'bilibili':
lines.append(f" [{i}] B站短链接: {url}")
lines.append(" 需要完整URL或BV号")
else:
lines.append(f" [{i}] {platform}: {url}")
lines.append(" 需要完整URL")
lines.append("")
lines.extend([
"💡 建议:",
" - 在浏览器打开链接后复制完整URL",
" - 或者直接发送BV号(仅限B站)"
])
return "\n".join(lines)
class PlaywrightResolver:
"""
Browser-based short link resolver using Playwright.
This is an optional implementation for environments where Playwright
is available. It can bypass anti-scraping measures by simulating
a real browser.
Requirements:
pip install playwright
playwright install chromium
"""
def __init__(self, headless: bool = True, timeout: int = 10000):
"""
Initialize Playwright resolver.
Args:
headless: Whether to run browser in headless mode
timeout: Page navigation timeout in milliseconds
"""
self.headless = headless
self.timeout = timeout
self._playwright = None
def _check_playwright_available(self) -> bool:
"""Check if Playwright is installed and available"""
try:
from playwright.sync_api import sync_playwright
return True
except ImportError:
return False
def resolve(self, short_url: str) -> Optional[str]:
"""
Resolve short link using Playwright browser automation.
Args:
short_url: Short URL to resolve
Returns:
Final URL after redirects, or None if resolution fails
"""
if not self._check_playwright_available():
raise ImportError(
"Playwright not installed. Install with: pip install playwright && playwright install chromium"
)
from playwright.sync_api import sync_playwright
try:
with sync_playwright() as p:
# Launch browser
browser = p.chromium.launch(headless=self.headless)
context = browser.new_context(
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',
viewport={'width': 1920, 'height': 1080}
)
page = context.new_page()
# Navigate to short URL
page.goto(short_url, timeout=self.timeout, wait_until='networkidle')
# Get final URL
final_url = page.url
# Close browser
browser.close()
return final_url
except Exception as e:
print(f"Playwright resolution failed: {e}")
return None
def resolve_bilibili_short(self, short_url: str) -> Optional[str]:
"""
Resolve Bilibili short link and extract BV ID.
Args:
short_url: Bilibili short URL (e.g., https://b23.tv/xxxxx)
Returns:
Full Bilibili URL with BV ID, or None if resolution fails
"""
final_url = self.resolve(short_url)
if not final_url:
return None
# Check if final URL contains BV ID
if 'bilibili.com/video/' in final_url:
return final_url
# Extract BV ID if present in URL
bv_pattern = re.compile(r'(BV[a-zA-Z0-9]{10})')
match = bv_pattern.search(final_url)
if match:
return f"https://www.bilibili.com/video/{match.group(1)}"
return None
def get_resolver() -> ShortLinkResolver:
"""
Get the default resolver instance.
Returns:
ShortLinkResolver instance
"""
return ShortLinkResolver()
def detect_and_format(text: str) -> Tuple[bool, Optional[str]]:
"""
Detect short links in text and format appropriate message.
Args:
text: Input text to search
Returns:
Tuple of (has_short_links, formatted_message)
- has_short_links: True if short links detected
- formatted_message: Help/warning message if short links found, None otherwise
"""
resolver = ShortLinkResolver()
short_links = resolver.detect_short_links(text)
if not short_links:
return False, None
# Determine appropriate message based on number and type of links
if len(short_links) == 1:
link = short_links[0]
if link['platform'] == 'bilibili':
return True, resolver.format_short_link_help(link['url'])
else:
return True, resolver.format_short_link_warning([link])
else:
return True, resolver.format_short_link_warning(short_links)
# CLI interface for testing
def main():
"""CLI for testing the short link resolver."""
import sys
if len(sys.argv) < 2:
print("Usage: python3 shortlink_resolver.py 'text to test'")
print("\nExamples:")
print(' python3 shortlink_resolver.py "Check this: https://b23.tv/oLBZZfK"')
print(' python3 shortlink_resolver.py "https://bit.ly/xxxxx"')
print('\nOptions:')
print(' --playwright Try to resolve short links using Playwright')
sys.exit(1)
text = ' '.join(sys.argv[1:])
# Check if we should try Playwright
use_playwright = '--playwright' in sys.argv
if use_playwright:
text = text.replace('--playwright', '').strip()
resolver = PlaywrightResolver()
# Find short links
short_links = ShortLinkResolver.detect_short_links(text)
for link in short_links:
url = link['url']
print(f"Resolving: {url}")
if link['platform'] == 'bilibili':
resolved = resolver.resolve_bilibili_short(url)
else:
resolved = resolver.resolve(url)
if resolved:
print(f" → {resolved}")
else:
print(f" ✗ Resolution failed")
else:
# Just detect and show help
has_links, message = detect_and_format(text)
if has_links:
print(message)
else:
print("No short links detected")
if __name__ == '__main__':
main()