- 重构 core/ 底层 (7文件): app/runner/engine/parser/history/safety/display - FC原生调用为主,prompt-JSON保底 - 移除所有工具返回截断 (ai_search/git_tools/cmd_executor/browser_auto/network_tools/sub_agent/tool_search) - 删除无用文件 (旧服务器/WebUI/彩蛋/启动器等21个文件) - 清理 constants.py 死代码 - 版本号更新为 7.0.0
220 lines
8.9 KiB
Python
220 lines
8.9 KiB
Python
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}"
|