5c62a21f32
全局重命名: - 包名: agent_eyes → agent_reach - CLI: agent-eyes → agent-reach - 类名: AgentEyes → AgentReach - 显示名: Agent Eyes → Agent Reach - GitHub: Panniantong/agent-eyes → Panniantong/Agent-Reach 所有 36 个测试通过,CLI/doctor/read/search 全部正常。
141 lines
4.0 KiB
Python
141 lines
4.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Channel base class — the universal interface for all platforms.
|
|
|
|
Every channel (YouTube, Twitter, GitHub, etc.) implements this interface.
|
|
The backend tool can be swapped anytime without changing anything else.
|
|
|
|
Example:
|
|
class YouTubeChannel(Channel):
|
|
name = "youtube"
|
|
backends = ["yt-dlp"] # current backend, can be swapped
|
|
|
|
async def read(self, url, config):
|
|
# Just call yt-dlp, return standardized dict
|
|
...
|
|
"""
|
|
|
|
import shutil
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
|
|
@dataclass
|
|
class ReadResult:
|
|
"""Standardized read result. Every channel returns this."""
|
|
title: str
|
|
content: str
|
|
url: str
|
|
author: str = ""
|
|
date: str = ""
|
|
platform: str = ""
|
|
extra: dict = None
|
|
|
|
def __post_init__(self):
|
|
self.extra = self.extra or {}
|
|
|
|
def to_dict(self) -> dict:
|
|
d = {
|
|
"title": self.title,
|
|
"content": self.content,
|
|
"url": self.url,
|
|
"platform": self.platform,
|
|
}
|
|
if self.author:
|
|
d["author"] = self.author
|
|
if self.date:
|
|
d["date"] = self.date
|
|
if self.extra:
|
|
d["extra"] = self.extra
|
|
return d
|
|
|
|
|
|
@dataclass
|
|
class SearchResult:
|
|
"""Standardized search result."""
|
|
title: str
|
|
url: str
|
|
snippet: str = ""
|
|
author: str = ""
|
|
date: str = ""
|
|
score: float = 0
|
|
extra: dict = None
|
|
|
|
def __post_init__(self):
|
|
self.extra = self.extra or {}
|
|
|
|
def to_dict(self) -> dict:
|
|
d = {
|
|
"title": self.title,
|
|
"url": self.url,
|
|
"snippet": self.snippet,
|
|
}
|
|
if self.author:
|
|
d["author"] = self.author
|
|
if self.date:
|
|
d["date"] = self.date
|
|
if self.extra:
|
|
d["extra"] = self.extra
|
|
return d
|
|
|
|
|
|
class Channel(ABC):
|
|
"""
|
|
Base class for all channels.
|
|
|
|
Subclasses just need to implement:
|
|
- read(url, config) → ReadResult
|
|
- can_handle(url) → bool
|
|
- check(config) → (status, message)
|
|
|
|
Optionally:
|
|
- search(query, config, **kwargs) → list[SearchResult]
|
|
"""
|
|
|
|
name: str = "" # e.g. "youtube"
|
|
description: str = "" # e.g. "YouTube video transcripts"
|
|
backends: List[str] = [] # e.g. ["yt-dlp"] — what external tool is used
|
|
requires_config: List[str] = [] # e.g. ["reddit_proxy"]
|
|
requires_tools: List[str] = [] # e.g. ["yt-dlp"]
|
|
tier: int = 0 # 0=zero-config, 1=needs free key, 2=needs setup
|
|
|
|
@abstractmethod
|
|
async def read(self, url: str, config=None) -> ReadResult:
|
|
"""Read content from a URL. Must return ReadResult."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def can_handle(self, url: str) -> bool:
|
|
"""Check if this channel can handle this URL."""
|
|
...
|
|
|
|
def check(self, config=None) -> Tuple[str, str]:
|
|
"""
|
|
Check if this channel is available.
|
|
Returns (status, message) where status is 'ok'/'warn'/'off'/'error'.
|
|
"""
|
|
# Check required tools
|
|
for tool in self.requires_tools:
|
|
if not shutil.which(tool):
|
|
return "off", f"需要安装:pip install {tool}"
|
|
|
|
# Check required config
|
|
for key in self.requires_config:
|
|
if config and not config.get(key):
|
|
return "off", f"需要配置 {key},运行 agent-reach setup"
|
|
|
|
return "ok", f"{'、'.join(self.backends) if self.backends else '内置'}"
|
|
|
|
async def search(self, query: str, config=None, **kwargs) -> List[SearchResult]:
|
|
"""Search this platform. Override if supported."""
|
|
raise NotImplementedError(f"{self.name} does not support search")
|
|
|
|
def can_search(self) -> bool:
|
|
"""Whether this channel supports search."""
|
|
try:
|
|
# Check if search is overridden
|
|
return type(self).search is not Channel.search
|
|
except:
|
|
return False
|