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/handler.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

337 lines
9.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Auto-trigger handler for obsidian-kb skill.
Detects video/article links in user messages and automatically processes them.
Supports Bilibili, YouTube, and Zhihu.
"""
import re
import sys
import os
from typing import List, Dict, Optional, Tuple
from urllib.parse import urlparse
# Add path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'src'))
# Import short link resolver
from shortlink_resolver import (
ShortLinkResolver,
detect_and_format,
PlaywrightResolver
)
# Pattern definitions for auto-detection
PATTERNS = {
'bilibili_full': re.compile(r'https?://(?:www\.)?bilibili\.com/video/(BV\w+)'),
'bilibili_short': re.compile(r'https?://b23\.tv/(\w+)'),
'bilibili_bv': re.compile(r'\b(BV[a-zA-Z0-9]{10})\b'),
'youtube_full': re.compile(r'https?://(?:www\.)?youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})'),
'youtube_short': re.compile(r'https?://youtu\.be/([a-zA-Z0-9_-]{11})'),
'zhihu_article': re.compile(r'https?://zhuanlan\.zhihu\.com/p/(\d+)'),
'zhihu_answer': re.compile(r'https?://(?:www\.)?zhihu\.com/question/\d+/answer/(\d+)'),
}
def detect_links(text: str) -> List[Dict[str, str]]:
"""
Detect video/article links in text.
Args:
text: Input text to search
Returns:
List of detected links with metadata:
[
{
'url': 'https://...',
'platform': 'bilibili',
'type': 'video',
'id': 'BVxxx',
'is_short': False
},
...
]
"""
detected = []
seen_ids = set() # Track IDs to avoid duplicates
for pattern_name, pattern in PATTERNS.items():
matches = pattern.finditer(text)
for match in matches:
# Determine platform and type
if 'bilibili' in pattern_name:
platform = 'bilibili'
content_type = 'video'
if 'short' in pattern_name:
is_short = True
url = match.group(0)
content_id = None # Need to resolve short URL
else:
is_short = False
if pattern_name == 'bilibili_bv':
url = f"https://www.bilibili.com/video/{match.group(1)}"
else:
url = match.group(0)
content_id = match.group(1)
elif 'youtube' in pattern_name:
platform = 'youtube'
content_type = 'video'
is_short = 'short' in pattern_name
url = match.group(0)
content_id = match.group(1)
elif 'zhihu' in pattern_name:
platform = 'zhihu'
content_type = 'article'
is_short = False
url = match.group(0)
content_id = match.group(1)
else:
continue
# Skip duplicates by URL or ID
unique_key = url if (not content_id or is_short) else content_id
if unique_key in seen_ids:
continue
seen_ids.add(unique_key)
detected.append({
'url': url,
'platform': platform,
'type': content_type,
'id': content_id,
'is_short': is_short,
'pattern': pattern_name
})
return detected
def format_preview(detected: List[Dict[str, str]]) -> str:
"""
Format a preview message for detected links.
Args:
detected: List of detected links
Returns:
Formatted preview message
"""
if not detected:
return ""
lines = ["🔍 检测到内容链接:\n"]
for i, item in enumerate(detected, 1):
platform_emoji = {
'bilibili': '📺',
'youtube': '▶️',
'zhihu': '📝'
}.get(item['platform'], '🔗')
platform_name = {
'bilibili': 'B站视频',
'youtube': 'YouTube视频',
'zhihu': '知乎文章'
}.get(item['platform'], item['platform'])
lines.append(f"{platform_emoji} [{i}] {platform_name}")
lines.append(f" URL: {item['url']}")
if item['is_short']:
lines.append(f" ⚠️ 短链接,需要用户提供完整URL或ID")
elif item['id']:
lines.append(f" ID: {item['id']}")
lines.append("")
return "\n".join(lines)
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
"""
return ShortLinkResolver.format_short_link_help(short_link)
def get_action_prompt(detected: List[Dict[str, str]]) -> str:
"""
Generate prompt asking user what to do.
Args:
detected: List of detected links
Returns:
Action prompt
"""
has_short = any(item['is_short'] for item in detected)
if has_short:
return f"{format_short_link_help()}\n\n其他链接是否立即处理?(y/n)"
count = len(detected)
if count == 1:
return "是否添加到收件箱并处理?(y/n)"
else:
return f"是否将上述 {count} 个内容添加到收件箱?(y/n)"
def should_auto_process(text: str) -> bool:
"""
Check if user wants to auto-process based on their message.
Args:
text: User's message
Returns:
True if user indicates they want immediate processing
"""
auto_keywords = ['立即处理', '处理', 'process', '--process', '马上']
return any(keyword in text.lower() for keyword in auto_keywords)
def process_link(url: str, platform: str, auto_process: bool = False) -> Tuple[bool, str]:
"""
Process a single link (extract metadata, add to inbox).
Args:
url: Content URL
platform: Platform name (bilibili/youtube/zhihu)
auto_process: Whether to immediately process after adding
Returns:
(success, message) tuple
"""
try:
# Import here to avoid issues if dependencies not available
from sources.bilibili import BilibiliAdapter
from models import Content, ContentStatus
from storage import Storage
# Only support Bilibili for now
if platform != 'bilibili':
return False, f"暂不支持 {platform},请手动添加"
# Handle short links
if 'b23.tv' in url:
return False, "短链接无法自动处理,请提供完整URL或BV号"
# Create adapter
adapter = BilibiliAdapter()
# Extract metadata
content = adapter.extract_content(url)
if not content:
return False, "无法提取视频信息"
# Get transcript
transcript = adapter.get_transcript(content)
if transcript:
content.transcript = transcript
# Save to inbox
content.status = ContentStatus.CONFIRMED
storage = Storage()
storage.save_content(content)
# Build success message
msg = f"✅ 已添加到收件箱\n"
msg += f" 标题: {content.title}\n"
msg += f" UP主: {content.author}\n"
msg += f" 时长: {content.duration}秒\n"
if auto_process:
# Process immediately
try:
from src.pipeline import ProcessingPipeline
pipeline = ProcessingPipeline()
content_text = content.transcript or content.summary or ""
result = pipeline.process_full(content, content_text)
storage.save_content(result)
msg += f"\n🔄 处理完成\n"
msg += f" 📄 笔记: {result.output_path or 'N/A'}\n"
msg += f" 🏷️ 标签: {', '.join(result.tags)}"
except Exception as e:
msg += f"\n⚠️ 处理失败: {e}"
return True, msg
except ImportError as e:
return False, f"依赖模块未安装: {e}"
except Exception as e:
return False, f"处理失败: {e}"
def handle_message(text: str) -> Optional[str]:
"""
Main entry point: handle incoming message and return response.
Args:
text: User's message text
Returns:
Response message, or None if no action needed
"""
# First, check for short links and provide help
has_short, short_msg = detect_and_format(text)
# Detect all links (including short ones)
detected = detect_links(text)
if not detected:
return None
# If we have short links, show help message first
if has_short:
return short_msg
# Check if this is a confirmation (y/yes)
text_lower = text.lower().strip()
if text_lower in ('y', 'yes', '是', '好的', 'ok'):
# This would need state management - for now return a message
# about using explicit command
return "请使用 /kb-add <URL> 命令添加内容,或重新发送链接"
# Show preview
preview = format_preview(detected)
prompt = get_action_prompt(detected)
return f"{preview}\n{prompt}"
# CLI interface for testing
def main():
"""CLI for testing the handler."""
if len(sys.argv) < 2:
print("Usage: python3 handler.py 'text to test'")
print("\nExamples:")
print(' python3 handler.py "https://www.bilibili.com/video/BV1xx411c7mD"')
print(' python3 handler.py "Check this video: https://b23.tv/xxxxx"')
sys.exit(1)
text = sys.argv[1]
response = handle_message(text)
if response:
print(response)
else:
print("No content links detected")
if __name__ == '__main__':
main()