import json import time from urllib.parse import quote import requests class Liugin: """网页内容工具 — jina.ai 代理 + AI 直连""" _UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36' _HEADERS = { 'User-Agent': _UA, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', } def __init__(self): self.usage = """网页内容工具 【jina.ai 代理组】— 通过 jina.ai 服务,返回干净文本: ai_url <网址> - jina 提取网页正文 ai_search <搜索词> - jina 网络搜索 ai_web <搜索词> - jina 网络搜索(同 ai_search) 【AI 直连组】— 直接 HTTP 请求,模型自行处理: url <网址> - 直接获取网页原始内容 search <搜索词> - 直接搜索(模型自行选择搜索引擎) web <搜索词> - 同 search JSON 格式示例: {"action": "use_tool", "tool": "ai_search", "args": "ai_url https://www.example.com"} {"action": "use_tool", "tool": "ai_search", "args": "ai_search Python教程"} {"action": "use_tool", "tool": "ai_search", "args": "url https://api.example.com/data"} ⚠️ 使用原则: - 优先使用 AI 直连组 (url/search/web),自己获取和处理网页内容 - 仅当直连失败、网页需要 JS 渲染、或需要 jina 的智能提取时才用 jina 代理组 - 不要滥用 jina,它有速率限制 """ self.cli = None self.timeout = 30 self.max_retries = 2 def set_cli(self, cli): self.cli = cli self.cli.register_liugin_command('ai_search', self.command_handler) def command_handler(self, args): parts = args.strip().split(maxsplit=1) if len(parts) < 1: return "请提供操作: ai_url/ai_search/ai_web/url/search/web <参数>" operation = parts[0].lower() remaining_args = parts[1] if len(parts) > 1 else "" return self.handle(f"{operation} {remaining_args}") def get_tool_info(self): return { "name": "ai_search", "description": "网页内容工具。ai_url/ai_search/ai_web 用 jina.ai 代理;url/search/web 直接 HTTP 请求让模型自行处理。优先用直连组,jina 仅作备用。", "keywords": ["网址", "url", "网页", "web", "search", "搜索", "fetch", "提取", "jina", "ai_url", "ai_search", "ai_web", "内容", "获取", "联网"], "usage": self.usage } def handle(self, args): try: parts = args.strip().split(maxsplit=1) if len(parts) < 1: return "错误:参数不足。可用操作: ai_url/ai_search/ai_web/url/search/web <参数>" operation = parts[0].lower() param = parts[1].strip() if len(parts) > 1 else "" if not param: return f"错误:请提供参数。格式: {operation} <网址或搜索词>" # ── jina.ai 代理组 ── if operation == "ai_url": return self._jina_extract(param) elif operation == "ai_search": return self._jina_search(param) elif operation == "ai_web": return self._jina_search(param) # ── AI 直连组 ── elif operation == "url": return self._fetch_raw(param) elif operation == "search": return self._direct_search(param) elif operation == "web": return self._direct_search(param) else: return f"不支持的操作 '{operation}'。可用: ai_url/ai_search/ai_web/url/search/web" except Exception as e: return f"操作错误: {str(e)}" # ═══════════════════════════════════════ # jina.ai 代理组 # ═══════════════════════════════════════ def _jina_extract(self, url): """jina.ai 提取网页正文""" jina_url = f"https://r.jina.ai/{url}" result = self._execute_curl(jina_url) if result.startswith("错误:"): return result return f"[jina 提取] 网页内容 (来自 {url}):\n\n{result}" def _jina_search(self, query): """jina.ai 网络搜索""" encoded = quote(query, safe='') jina_url = f"https://s.jina.ai/{encoded}" result = self._execute_curl(jina_url) if result.startswith("错误:"): return result return f"[jina 搜索] 结果 (搜索词: {query}):\n\n{result}" # ═══════════════════════════════════════ # AI 直连组 # ═══════════════════════════════════════ def _fetch_raw(self, url): """直接 HTTP GET 获取网页原始内容""" if not url.startswith('http://') and not url.startswith('https://'): url = 'https://' + url try: response = requests.get( url, timeout=self.timeout, headers=self._HEADERS, allow_redirects=True ) if response.status_code != 200: return f"错误:HTTP {response.status_code}" if not response.encoding or response.encoding.lower() == 'iso-8859-1': response.encoding = response.apparent_encoding or 'utf-8' content = response.text.strip() if not content: return "错误:未获取到内容" return f"[直连] 原始网页内容 (来自 {url}):\n\n{content}" except requests.exceptions.Timeout: return f"错误:请求超时 (>{self.timeout}秒)" except requests.exceptions.ConnectionError: return "错误:网络连接失败" except Exception as e: return f"错误:{str(e)}" def _direct_search(self, query): """直接搜索 — 用搜索引擎获取结果页,模型自行解析""" encoded = quote(query, safe='') # 用必应搜索,返回结果页 HTML,模型自行提取链接和摘要 search_url = f"https://www.bing.com/search?q={encoded}" try: response = requests.get( search_url, timeout=self.timeout, headers=self._HEADERS, allow_redirects=True ) if response.status_code != 200: return f"错误:HTTP {response.status_code}" if not response.encoding or response.encoding.lower() == 'iso-8859-1': response.encoding = response.apparent_encoding or 'utf-8' content = response.text.strip() if not content: return "错误:未获取到搜索结果" return f"[直连] 搜索结果页 (搜索词: {query}):\n\n{content}" except requests.exceptions.Timeout: return f"错误:请求超时 (>{self.timeout}秒)" except requests.exceptions.ConnectionError: return "错误:网络连接失败" except Exception as e: return f"错误:{str(e)}" # ═══════════════════════════════════════ # 通用请求(jina 用,带重试) # ═══════════════════════════════════════ def _execute_curl(self, url): last_error = None for attempt in range(self.max_retries + 1): try: response = requests.get( url, timeout=self.timeout, headers=self._HEADERS, allow_redirects=True ) if response.status_code != 200: return f"错误:HTTP {response.status_code}" if not response.encoding or response.encoding.lower() == 'iso-8859-1': response.encoding = response.apparent_encoding or 'utf-8' content = response.text.strip() if not content: return "错误:未获取到内容" return content except requests.exceptions.Timeout: last_error = f"请求超时 (>{self.timeout}秒)" if attempt < self.max_retries: time.sleep(1) except requests.exceptions.ConnectionError: last_error = "网络连接失败" if attempt < self.max_retries: time.sleep(1) except requests.exceptions.RequestException as e: last_error = str(e) break except Exception as e: last_error = str(e) break return f"错误:{last_error}"