- 新增 operit_runtime.py: 跨平台 Python 运行时 - 实现 Tools.System/Files/Net/UI/Memory/FFmpeg/Workflow/Chat/SoftwareSettings/Tasker - 实现 OkHttp 链式 HTTP 客户端兼容层 - 实现 complete()/exports/console 全局对象 - 实现 OperitScriptLoader 脚本加载器 - 支持 Windows/macOS/Linux 三平台 - 新增 operit_py/ 目录: 47 个 Python 翻译脚本 - 搜索类: duckduckgo, google_search, tavily, zhipu_search, various_search, crossref - 绘图类: openai_draw, qwen_draw, xai_draw, minimax_draw, siliconflow_draw, zhipu_draw, nanobanana_draw - 媒体类: ffmpeg, file_converter, douyin_download, bilibili_tools, jmcomic - 文件类: extended_file_tools, hex_editor, reader, beeimg_image_uploader - 网络类: extended_http_tools, network_test - 自动化类: browser, automatic_ui_base, automatic_ui_subagent, automatic_bilibili_assistant, automatic_xiaohongshu_assistant, automatic_baidu_map_assistant - 系统类: system_tools, super_admin, daily_life, operit_editor - 对话类: ai_chat, extended_chat, qq_intelligent - 工作流类: workflow, vflow_trigger, tasker - 工具类: time, code_runner, quick_start, extended_memory_tools, baidu_map, 12306, pdf_vision_parser - 测试结果: 47/47 语法检查通过, 38 脚本加载成功, 280 个工具注册 - Android 专属功能已降级处理(保留接口,返回不支持提示)
751 lines
32 KiB
Python
751 lines
32 KiB
Python
# METADATA
|
|
# {
|
|
# "name": "various_search",
|
|
# "display_name": {"zh": "多平台搜索", "en": "Multi-Platform Search"},
|
|
# "category": "Search",
|
|
# "description": {"zh": "提供多平台搜索功能(含图片搜索),支持从必应、百度、搜狗、夸克等平台获取搜索结果。", "en": "Multi-platform search tools (including image search) that fetch results from Bing, Baidu, Sogou, Quark, and more."},
|
|
# "enabledByDefault": True,
|
|
# "tools": [
|
|
# {"name": "search_bing", "description": {"zh": "使用必应搜索引擎进行搜索", "en": "Search using the Bing search engine."}, "parameters": [
|
|
# {"name": "query", "description": {"zh": "搜索查询关键词", "en": "Search query keywords."}, "type": "string", "required": True},
|
|
# {"name": "includeLinks", "description": {"zh": "是否在结果中包含可点击的链接列表,默认为false。", "en": "Whether to include a clickable link list in results (default: false)."}, "type": "boolean", "required": False}
|
|
# ]},
|
|
# {"name": "search_baidu", "description": {"zh": "使用百度搜索引擎进行搜索", "en": "Search using the Baidu search engine."}, "parameters": [
|
|
# {"name": "query", "description": {"zh": "搜索查询关键词", "en": "Search query keywords."}, "type": "string", "required": True},
|
|
# {"name": "page", "description": {"zh": "搜索结果页码,默认为1", "en": "Result page number (default: 1)."}, "type": "string", "required": False},
|
|
# {"name": "includeLinks", "description": {"zh": "是否在结果中包含可点击的链接列表,默认为false。", "en": "Whether to include a clickable link list in results (default: false)."}, "type": "boolean", "required": False}
|
|
# ]},
|
|
# {"name": "search_sogou", "description": {"zh": "使用搜狗搜索引擎进行搜索", "en": "Search using the Sogou search engine."}, "parameters": [
|
|
# {"name": "query", "description": {"zh": "搜索查询关键词", "en": "Search query keywords."}, "type": "string", "required": True},
|
|
# {"name": "page", "description": {"zh": "搜索结果页码,默认为1", "en": "Result page number (default: 1)."}, "type": "string", "required": False},
|
|
# {"name": "includeLinks", "description": {"zh": "是否在结果中包含可点击的链接列表,默认为false。", "en": "Whether to include a clickable link list in results (default: false)."}, "type": "boolean", "required": False}
|
|
# ]},
|
|
# {"name": "search_quark", "description": {"zh": "使用夸克搜索引擎进行搜索", "en": "Search using the Quark search engine."}, "parameters": [
|
|
# {"name": "query", "description": {"zh": "搜索查询关键词", "en": "Search query keywords."}, "type": "string", "required": True},
|
|
# {"name": "page", "description": {"zh": "搜索结果页码,默认为1", "en": "Result page number (default: 1)."}, "type": "string", "required": False},
|
|
# {"name": "includeLinks", "description": {"zh": "是否在结果中包含可点击的链接列表,默认为false。", "en": "Whether to include a clickable link list in results (default: false)."}, "type": "boolean", "required": False}
|
|
# ]},
|
|
# {"name": "combined_search", "description": {"zh": "在多个平台同时执行搜索。建议用户要求搜索的时候默认使用这个工具。", "en": "Run searches across multiple platforms. Use this tool by default when the user asks to search."}, "parameters": [
|
|
# {"name": "query", "description": {"zh": "搜索查询关键词", "en": "Search query keywords."}, "type": "string", "required": True},
|
|
# {"name": "platforms", "description": {"zh": "搜索平台列表字符串,可选值包括\"bing\",\"baidu\",\"sogou\",\"quark\",多个平台用逗号分隔", "en": "Comma-separated platform list."}, "type": "string", "required": True},
|
|
# {"name": "includeLinks", "description": {"zh": "是否在结果中包含可点击的链接列表,默认为false。", "en": "Whether to include a clickable link list in results (default: false)."}, "type": "boolean", "required": False}
|
|
# ]},
|
|
# {"name": "search", "description": {"zh": "兼容工具名:等同于 combined_search。", "en": "Compatibility alias: equivalent to combined_search."}, "parameters": [
|
|
# {"name": "query", "description": {"zh": "搜索查询关键词", "en": "Search query keywords."}, "type": "string", "required": True},
|
|
# {"name": "platforms", "description": {"zh": "可选平台列表,默认 bing,baidu,sogou,quark", "en": "Optional platform list, default bing,baidu,sogou,quark."}, "type": "string", "required": False},
|
|
# {"name": "includeLinks", "description": {"zh": "是否返回链接列表,默认 false", "en": "Whether to include links in result, default false."}, "type": "boolean", "required": False}
|
|
# ]},
|
|
# {"name": "search_web", "description": {"zh": "兼容工具名:网页搜索别名。", "en": "Compatibility alias for web search."}, "parameters": [
|
|
# {"name": "query", "description": {"zh": "搜索查询关键词", "en": "Search query keywords."}, "type": "string", "required": True},
|
|
# {"name": "platforms", "description": {"zh": "可选平台列表,默认 bing,baidu,sogou,quark", "en": "Optional platform list."}, "type": "string", "required": False},
|
|
# {"name": "includeLinks", "description": {"zh": "是否返回链接列表,默认 false", "en": "Whether to include links in result, default false."}, "type": "boolean", "required": False}
|
|
# ]},
|
|
# {"name": "search_bing_images", "description": {"zh": "使用必应图片搜索引擎进行图片搜索。", "en": "Search images using Bing Images."}, "parameters": [{"name": "query", "description": {"zh": "搜索关键词", "en": "Search query keywords."}, "type": "string", "required": True}]},
|
|
# {"name": "search_wikimedia_images", "description": {"zh": "使用 Wikimedia Commons 进行图片搜索。", "en": "Search images using Wikimedia Commons."}, "parameters": [{"name": "query", "description": {"zh": "搜索关键词", "en": "Search query keywords."}, "type": "string", "required": True}]},
|
|
# {"name": "search_duckduckgo_images", "description": {"zh": "使用 DuckDuckGo Images 进行图片搜索。", "en": "Search images using DuckDuckGo Images."}, "parameters": [{"name": "query", "description": {"zh": "搜索关键词", "en": "Search query keywords."}, "type": "string", "required": True}]},
|
|
# {"name": "search_ecosia_images", "description": {"zh": "使用 Ecosia Images 进行图片搜索。", "en": "Search images using Ecosia Images."}, "parameters": [{"name": "query", "description": {"zh": "搜索关键词", "en": "Search query keywords."}, "type": "string", "required": True}]},
|
|
# {"name": "search_pexels_images", "description": {"zh": "使用 Pexels 进行图片搜索。", "en": "Search images using Pexels."}, "parameters": [{"name": "query", "description": {"zh": "搜索关键词", "en": "Search query keywords."}, "type": "string", "required": True}]},
|
|
# {"name": "search_pixabay_images", "description": {"zh": "使用 Pixabay 进行图片搜索。", "en": "Search images using Pixabay."}, "parameters": [{"name": "query", "description": {"zh": "搜索关键词", "en": "Search query keywords."}, "type": "string", "required": True}]}
|
|
# ]
|
|
# }
|
|
|
|
import json
|
|
import re
|
|
import asyncio
|
|
from urllib.parse import quote, unquote
|
|
|
|
Error = Exception
|
|
|
|
|
|
def normalizeText(input_):
|
|
if not input_:
|
|
return ""
|
|
return re.sub(r'[\s\-_.,;:!?()\[\]{}"\'`~@#$%^&*+=|\\/<>]+', '', str(input_).lower())
|
|
|
|
|
|
OUTBOUND_PATH_HINTS = ["/link", "/url", "/redirect", "/jump", "/out", "/outlink", "/ulink", "/aladdin.php"]
|
|
OUTBOUND_QUERY_KEYS = ["url", "u", "target", "targeturl", "target_url", "to", "dest", "destination", "redirect", "redirect_url"]
|
|
MULTI_PART_TLDS = ["ac.uk", "co.jp", "co.uk", "com.au", "com.cn", "com.hk", "gov.cn", "net.au", "net.cn", "org.au", "org.cn", "org.uk"]
|
|
|
|
|
|
def normalizePath(pathname):
|
|
normalized = re.sub(r'/+$', '', str(pathname or ""))
|
|
return normalized or "/"
|
|
|
|
|
|
def parseUrlParts(rawUrl, baseUrl):
|
|
rawText = str(rawUrl or "").strip()
|
|
if not rawText:
|
|
return None
|
|
absoluteText = rawText
|
|
if absoluteText.startswith("//"):
|
|
absoluteText = f"https:{absoluteText}"
|
|
elif not re.match(r'^[a-z]+://', absoluteText, re.IGNORECASE):
|
|
if not absoluteText.startswith("/"):
|
|
return None
|
|
base = parseUrlParts(baseUrl, "") if baseUrl else None
|
|
if not base:
|
|
return None
|
|
absoluteText = f"{base['protocol']}//{base['host']}{absoluteText}"
|
|
match = re.match(r'^(https?)://([^/?#]+)([^?#]*)(\?[^#]*)?(?:#.*)?$', absoluteText, re.IGNORECASE)
|
|
if not match:
|
|
return None
|
|
protocol = f"{match.group(1).lower()}:"
|
|
host = match.group(2).lower()
|
|
hostname = re.sub(r':\d+$', '', host)
|
|
pathname = normalizePath(match.group(3) or "/")
|
|
search = match.group(4) or ""
|
|
return {
|
|
"href": f"{protocol}//{host}{pathname}{search}",
|
|
"protocol": protocol,
|
|
"host": host,
|
|
"hostname": hostname,
|
|
"pathname": pathname,
|
|
"search": search,
|
|
}
|
|
|
|
|
|
def normalizeUrl(rawUrl, baseUrl):
|
|
parsed = parseUrlParts(rawUrl, baseUrl)
|
|
return parsed["href"] if parsed else ""
|
|
|
|
|
|
def parseSearchEntries(searchText):
|
|
search = re.sub(r'^\?', '', str(searchText or ""))
|
|
if not search:
|
|
return []
|
|
result = []
|
|
for item in search.split("&"):
|
|
item = item.strip()
|
|
if not item:
|
|
continue
|
|
equalIndex = item.find("=")
|
|
if equalIndex == -1:
|
|
result.append([item, ""])
|
|
else:
|
|
result.append([item[:equalIndex], item[equalIndex + 1:]])
|
|
return result
|
|
|
|
|
|
def getSiteDomain(hostname):
|
|
parts = [p for p in str(hostname or "").lower().split(".") if p]
|
|
if len(parts) <= 2:
|
|
return str(hostname or "").lower()
|
|
lastTwo = ".".join(parts[-2:])
|
|
if lastTwo in MULTI_PART_TLDS and len(parts) >= 3:
|
|
return ".".join(parts[-3:])
|
|
return lastTwo
|
|
|
|
|
|
def countPathSegments(pathname):
|
|
return len([s for s in normalizePath(pathname).split("/") if s])
|
|
|
|
|
|
def getSortedSearch(searchText):
|
|
entries = sorted(parseSearchEntries(searchText), key=lambda x: (x[0], x[1]))
|
|
return "&".join([f"{k}={v}" for k, v in entries])
|
|
|
|
|
|
def toComparableUrl(rawUrl):
|
|
if not rawUrl:
|
|
return ""
|
|
parsed = parseUrlParts(rawUrl, "")
|
|
if not parsed:
|
|
return str(rawUrl).strip()
|
|
search = getSortedSearch(parsed["search"])
|
|
return f"{parsed['protocol']}//{parsed['host']}{parsed['pathname']}" + (f"?{search}" if search else "")
|
|
|
|
|
|
def getLinkText(link):
|
|
text = link and (link.get("text") or link.get("title") or link.get("name"))
|
|
return re.sub(r'\s+', ' ', str(text)).strip() if text else ""
|
|
|
|
|
|
def looksLikeUiText(text):
|
|
raw = re.sub(r'\s+', ' ', str(text or "")).strip()
|
|
normalized = normalizeText(text)
|
|
if not normalized:
|
|
return True
|
|
if re.match(r'^\d+$', normalized):
|
|
return True
|
|
if not re.search(r'[a-z0-9\u4e00-\u9fa5]', raw, re.IGNORECASE):
|
|
return True
|
|
if len(normalized) == 1:
|
|
return True
|
|
if len(normalized) == 2 and re.match(r'^[a-z0-9]+$', normalized, re.IGNORECASE):
|
|
return True
|
|
return False
|
|
|
|
|
|
def looksLikeSearchLandingUrl(rawUrl, sourceUrl):
|
|
if not rawUrl or not sourceUrl:
|
|
return False
|
|
candidate = parseUrlParts(rawUrl, sourceUrl)
|
|
source = parseUrlParts(sourceUrl, "")
|
|
if not candidate or not source:
|
|
return False
|
|
if toComparableUrl(candidate["href"]) == toComparableUrl(source["href"]):
|
|
return True
|
|
if candidate["host"] != source["host"]:
|
|
return False
|
|
candidateSearch = getSortedSearch(candidate["search"])
|
|
sourceSearch = getSortedSearch(source["search"])
|
|
if candidate["pathname"] == "/" and not candidateSearch:
|
|
return True
|
|
return candidate["pathname"] == source["pathname"] and (not candidateSearch or candidateSearch == sourceSearch)
|
|
|
|
|
|
def looksLikeOutboundWrapper(rawUrl, sourceUrl):
|
|
candidate = parseUrlParts(rawUrl, sourceUrl)
|
|
source = parseUrlParts(sourceUrl, "")
|
|
if not candidate or not source or candidate["host"] != source["host"]:
|
|
return False
|
|
path = candidate["pathname"].lower()
|
|
if any(path == hint or path.endswith(hint) for hint in OUTBOUND_PATH_HINTS):
|
|
return True
|
|
search = re.sub(r'^\?', '', candidate["search"])
|
|
return any(re.search(r'(?:^|&)' + key + r'(?:=|&|$)', search, re.IGNORECASE) for key in OUTBOUND_QUERY_KEYS)
|
|
|
|
|
|
def looksLikeSameSiteSectionUrl(rawUrl, sourceUrl):
|
|
candidate = parseUrlParts(rawUrl, sourceUrl)
|
|
source = parseUrlParts(sourceUrl, "")
|
|
if not candidate or not source:
|
|
return False
|
|
if candidate["host"] == source["host"]:
|
|
return False
|
|
if getSiteDomain(candidate["hostname"]) != getSiteDomain(source["hostname"]):
|
|
return False
|
|
if looksLikeOutboundWrapper(candidate["href"], source["href"]):
|
|
return False
|
|
return countPathSegments(candidate["pathname"]) <= 1
|
|
|
|
|
|
def classifyLinkTarget(rawUrl, sourceUrl):
|
|
text = str(rawUrl or "").strip()
|
|
if not text:
|
|
return "invalid"
|
|
lowered = text.lower()
|
|
if lowered.startswith("javascript:") or lowered.startswith("#"):
|
|
return "invalid"
|
|
candidate = parseUrlParts(rawUrl, sourceUrl)
|
|
source = parseUrlParts(sourceUrl, "")
|
|
if not candidate or not source:
|
|
return "invalid"
|
|
if looksLikeSearchLandingUrl(candidate["href"], source["href"]):
|
|
return "landing"
|
|
if looksLikeOutboundWrapper(candidate["href"], source["href"]):
|
|
return "wrapper"
|
|
if candidate["host"] == source["host"]:
|
|
return "internal"
|
|
if looksLikeSameSiteSectionUrl(candidate["href"], source["href"]):
|
|
return "internal"
|
|
if getSiteDomain(candidate["hostname"]) == getSiteDomain(source["hostname"]):
|
|
return "internal"
|
|
return "external"
|
|
|
|
|
|
def extractUrlsFromText(rawText):
|
|
if not rawText:
|
|
return []
|
|
text = str(rawText)
|
|
matches = re.findall(r'https?://[^\s)\]>"\']+', text)
|
|
unique = []
|
|
for url in matches:
|
|
if url not in unique:
|
|
unique.append(url)
|
|
return unique
|
|
|
|
|
|
def extractBestUrlFromText(rawText, sourceUrl):
|
|
urls = extractUrlsFromText(rawText)
|
|
for item in urls:
|
|
if classifyLinkTarget(item, sourceUrl) in ("external", "wrapper"):
|
|
return item
|
|
return urls[0] if urls else ""
|
|
|
|
|
|
def stripLeadingLinkDump(rawContent):
|
|
lines = re.split(r'\r?\n', str(rawContent or ""))
|
|
index = 0
|
|
while index < len(lines) and not lines[index].strip():
|
|
index += 1
|
|
dumpedLinkCount = 0
|
|
while index < len(lines) and re.match(r'^\[\d+\]\s', lines[index].strip()):
|
|
dumpedLinkCount += 1
|
|
index += 1
|
|
if dumpedLinkCount == 0:
|
|
return str(rawContent or "")
|
|
while index < len(lines) and not lines[index].strip():
|
|
index += 1
|
|
return "\n".join(lines[index:])
|
|
|
|
|
|
def collectLinkCandidates(link, sourceUrl):
|
|
if not link:
|
|
link = {}
|
|
keys = ["realUrl", "real_url", "targetUrl", "target_url", "originUrl", "origin_url", "href", "url", "target", "rawUrl", "link"]
|
|
unique = []
|
|
for key in keys:
|
|
value = link.get(key)
|
|
normalized = normalizeUrl(str(value).strip() if value else "", sourceUrl)
|
|
if normalized and normalized not in unique:
|
|
unique.append(normalized)
|
|
return unique
|
|
|
|
|
|
def pickBestLinkUrl(link, sourceUrl):
|
|
candidates = collectLinkCandidates(link, sourceUrl)
|
|
for item in candidates:
|
|
if classifyLinkTarget(item, sourceUrl) in ("external", "wrapper"):
|
|
return item
|
|
fromText = extractBestUrlFromText(getLinkText(link), sourceUrl)
|
|
if fromText:
|
|
return fromText
|
|
return candidates[0] if candidates else ""
|
|
|
|
|
|
def scoreStructuralCandidate(text, url, sourceUrl, relativeIndex):
|
|
score = 0
|
|
if not text or looksLikeUiText(text):
|
|
score -= 6
|
|
else:
|
|
score += 2
|
|
linkType = classifyLinkTarget(url, sourceUrl)
|
|
if linkType == "external":
|
|
score += 5
|
|
elif linkType == "wrapper":
|
|
score += 4
|
|
elif linkType in ("landing", "internal"):
|
|
score -= 8
|
|
else:
|
|
score -= 4
|
|
score -= relativeIndex * 0.15
|
|
return score
|
|
|
|
|
|
def findResultBlockStart(links, sourceUrl, scanLimit=48, windowSize=6, minCandidates=3):
|
|
upperBound = min(len(links), scanLimit)
|
|
for start in range(upperBound):
|
|
candidateCount = 0
|
|
hosts = []
|
|
for offset in range(windowSize):
|
|
if start + offset >= upperBound:
|
|
break
|
|
link = links[start + offset]
|
|
text = getLinkText(link)
|
|
url = pickBestLinkUrl(link, sourceUrl)
|
|
if not text or looksLikeUiText(text):
|
|
continue
|
|
linkType = classifyLinkTarget(url, sourceUrl)
|
|
if linkType not in ("external", "wrapper"):
|
|
continue
|
|
candidateCount += 1
|
|
parsed = parseUrlParts(url, sourceUrl)
|
|
host = parsed["hostname"] if parsed else ""
|
|
if host and host not in hosts:
|
|
hosts.append(host)
|
|
if candidateCount >= minCandidates and len(hosts) >= 2:
|
|
return start
|
|
return 0
|
|
|
|
|
|
def isClusteredResultLink(links, sourceUrl, targetIndex, radius=3, minCandidates=3):
|
|
candidateCount = 0
|
|
hosts = []
|
|
start = max(0, targetIndex - radius)
|
|
end = min(len(links) - 1, targetIndex + radius)
|
|
for index in range(start, end + 1):
|
|
link = links[index]
|
|
text = getLinkText(link)
|
|
url = pickBestLinkUrl(link, sourceUrl)
|
|
if not text or looksLikeUiText(text):
|
|
continue
|
|
linkType = classifyLinkTarget(url, sourceUrl)
|
|
if linkType not in ("external", "wrapper"):
|
|
continue
|
|
candidateCount += 1
|
|
parsed = parseUrlParts(url, sourceUrl)
|
|
host = parsed["hostname"] if parsed else ""
|
|
if host and host not in hosts:
|
|
hosts.append(host)
|
|
return candidateCount >= minCandidates and len(hosts) >= 2
|
|
|
|
|
|
def buildProbeIndexes(links, sourceUrl, maxCount=8, probeWindow=24):
|
|
resultStart = findResultBlockStart(links, sourceUrl)
|
|
result = []
|
|
for relativeIndex, link in enumerate(links[resultStart:resultStart + probeWindow]):
|
|
index = resultStart + relativeIndex
|
|
text = getLinkText(link)
|
|
url = pickBestLinkUrl(link, sourceUrl)
|
|
score = scoreStructuralCandidate(text, url, sourceUrl, relativeIndex)
|
|
if score > -4 and isClusteredResultLink(links, sourceUrl, index):
|
|
result.append(index)
|
|
if len(result) >= maxCount:
|
|
break
|
|
return result
|
|
|
|
|
|
def chooseBestUrl(directUrl, resolvedUrl, sourceUrl):
|
|
candidates = [resolvedUrl, directUrl]
|
|
for candidate in candidates:
|
|
if candidate and classifyLinkTarget(candidate, sourceUrl) == "external":
|
|
return normalizeUrl(candidate, sourceUrl)
|
|
return ""
|
|
|
|
|
|
def shouldKeepLink(links, index, text, url, sourceUrl):
|
|
if not url:
|
|
return False
|
|
if not text or looksLikeUiText(text):
|
|
return False
|
|
if classifyLinkTarget(url, sourceUrl) != "external":
|
|
return False
|
|
return isClusteredResultLink(links, sourceUrl, index)
|
|
|
|
|
|
async def resolveLinkUrlsByVisitKey(response, sourceUrl, maxCount=8):
|
|
if not response or not response.get("visitKey") or not response.get("links") or not isinstance(response.get("links"), list):
|
|
return {}
|
|
resolved = {}
|
|
indexes = buildProbeIndexes(response["links"], sourceUrl, maxCount)
|
|
|
|
async def resolveOne(index):
|
|
try:
|
|
follow = await Tools.Net.visit({"visit_key": response["visitKey"], "link_number": index + 1})
|
|
followUrl = normalizeUrl(str(follow.get("url")), sourceUrl) if follow and follow.get("url") else ""
|
|
followContent = str(follow.get("content")) if follow and follow.get("content") else ""
|
|
contentUrl = extractBestUrlFromText(followContent, sourceUrl)
|
|
resolved[index] = {"url": chooseBestUrl("", followUrl, sourceUrl) or chooseBestUrl("", contentUrl, sourceUrl)}
|
|
except Exception as error:
|
|
console.error(f"[resolveLinkUrlsByVisitKey] link {index + 1} failed: {str(error)}")
|
|
resolved[index] = {"url": ""}
|
|
|
|
await asyncio.gather(*[resolveOne(index) for index in indexes])
|
|
return resolved
|
|
|
|
|
|
async def buildLinkLines(response, sourceUrl, maxItems=20):
|
|
resultStart = findResultBlockStart(response["links"], sourceUrl)
|
|
resolvedByIndex = await resolveLinkUrlsByVisitKey(response, sourceUrl)
|
|
lines = []
|
|
seen = set()
|
|
for index in range(resultStart, len(response["links"])):
|
|
link = response["links"][index]
|
|
text = getLinkText(link)
|
|
directUrl = pickBestLinkUrl(link, sourceUrl)
|
|
resolved = resolvedByIndex.get(index)
|
|
bestUrl = chooseBestUrl(directUrl, resolved["url"] if resolved and resolved.get("url") else "", sourceUrl)
|
|
if not shouldKeepLink(response["links"], index, text, bestUrl, sourceUrl):
|
|
continue
|
|
key = toComparableUrl(bestUrl)
|
|
if not key or key in seen:
|
|
continue
|
|
seen.add(key)
|
|
lines.append(f"[{index + 1}] {text} - {bestUrl}")
|
|
if len(lines) >= maxItems:
|
|
break
|
|
return lines
|
|
|
|
|
|
async def performSearch(platform, url, includeLinks=False):
|
|
try:
|
|
response = await Tools.Net.visit({"url": url})
|
|
if not response:
|
|
raise Error(f"无法获取 {platform} 搜索结果")
|
|
|
|
parts = []
|
|
hasFilteredLinks = False
|
|
if response.get("visitKey") is not None:
|
|
parts.append(str(response["visitKey"]))
|
|
if includeLinks and response.get("links") and isinstance(response.get("links"), list) and len(response["links"]) > 0:
|
|
linksLines = await buildLinkLines(response, url)
|
|
if len(linksLines) > 0:
|
|
parts.append("\n".join(linksLines))
|
|
hasFilteredLinks = True
|
|
elif includeLinks and response.get("content"):
|
|
extractedUrls = extractUrlsFromText(response["content"])
|
|
if len(extractedUrls) > 0:
|
|
lines = [f"[{i + 1}] {item}" for i, item in enumerate(extractedUrls[:20])]
|
|
parts.append("\n".join(lines))
|
|
if response.get("content") is not None and (not includeLinks or not hasFilteredLinks):
|
|
contentText = stripLeadingLinkDump(response["content"]) if includeLinks and response.get("links") and isinstance(response.get("links"), list) else str(response["content"])
|
|
parts.append(str(contentText))
|
|
|
|
return {"platform": platform, "content": "\n".join(parts)}
|
|
except Exception as error:
|
|
return {"platform": platform, "content": f"{platform} 搜索失败: {str(error)}"}
|
|
|
|
|
|
async def performImageSearch(platform, url):
|
|
try:
|
|
response = await Tools.Net.visit({"url": url, "include_image_links": True})
|
|
if not response:
|
|
raise Error(f"无法获取 {platform} 图片搜索结果")
|
|
|
|
parts = []
|
|
if response.get("visitKey") is not None:
|
|
parts.append(str(response["visitKey"]))
|
|
|
|
if response.get("imageLinks") and isinstance(response.get("imageLinks"), list) and len(response["imageLinks"]) > 0:
|
|
maxItems = 20
|
|
imagesLines = []
|
|
for index, link in enumerate(response["imageLinks"][:maxItems]):
|
|
lastSeg = str(link).split("/")[-1] or "image"
|
|
name = lastSeg.split("?")[0] or "image"
|
|
imagesLines.append(f"[{index + 1}] {name}")
|
|
parts.append("Images:")
|
|
parts.append("\n".join(imagesLines))
|
|
|
|
if response.get("content") is not None:
|
|
parts.append(str(response["content"]))
|
|
|
|
return {"platform": platform, "content": "\n".join(parts)}
|
|
except Exception as error:
|
|
return {"platform": platform, "content": f"{platform} 图片搜索失败: {str(error)}"}
|
|
|
|
|
|
async def search_bing(query, includeLinks=False):
|
|
encodedQuery = quote(query)
|
|
url = f"https://cn.bing.com/search?q={encodedQuery}&FORM=HDRSC1"
|
|
return await performSearch('bing', url, includeLinks)
|
|
|
|
|
|
async def search_baidu(query, pageStr=None, includeLinks=False):
|
|
page = 1
|
|
if pageStr:
|
|
page = int(str(pageStr), 10)
|
|
pn = (page - 1) * 10
|
|
encodedQuery = quote(query)
|
|
url = f"https://www.baidu.com/s?wd={encodedQuery}&pn={pn}"
|
|
return await performSearch('baidu', url, includeLinks)
|
|
|
|
|
|
async def search_sogou(query, pageStr=None, includeLinks=False):
|
|
page = 1
|
|
if pageStr:
|
|
page = int(str(pageStr), 10)
|
|
encodedQuery = quote(query)
|
|
url = f"https://www.sogou.com/web?query={encodedQuery}&page={page}"
|
|
return await performSearch('sogou', url, includeLinks)
|
|
|
|
|
|
async def search_quark(query, pageStr=None, includeLinks=False):
|
|
page = 1
|
|
if pageStr:
|
|
page = int(str(pageStr), 10)
|
|
encodedQuery = quote(query)
|
|
url = f"https://quark.sm.cn/s?q={encodedQuery}&page={page}"
|
|
return await performSearch('quark', url, includeLinks)
|
|
|
|
|
|
async def search_bing_images(query):
|
|
encodedQuery = quote(query)
|
|
url = f"https://www.bing.com/images/search?q={encodedQuery}"
|
|
return await performImageSearch('bing_images', url)
|
|
|
|
|
|
async def search_wikimedia_images(query):
|
|
encodedQuery = quote(query)
|
|
url = f"https://commons.wikimedia.org/wiki/Special:MediaSearch?type=image&search={encodedQuery}"
|
|
return await performImageSearch('wikimedia_images', url)
|
|
|
|
|
|
async def search_duckduckgo_images(query):
|
|
encodedQuery = quote(query)
|
|
url = f"https://duckduckgo.com/?q={encodedQuery}&iax=images&ia=images"
|
|
return await performImageSearch('duckduckgo_images', url)
|
|
|
|
|
|
async def search_ecosia_images(query):
|
|
encodedQuery = quote(query)
|
|
url = f"https://www.ecosia.org/images?q={encodedQuery}"
|
|
return await performImageSearch('ecosia_images', url)
|
|
|
|
|
|
async def search_pexels_images(query):
|
|
encodedQuery = quote(query)
|
|
url = f"https://www.pexels.com/search/{encodedQuery}/"
|
|
return await performImageSearch('pexels_images', url)
|
|
|
|
|
|
async def search_pixabay_images(query):
|
|
encodedQuery = quote(query)
|
|
url = f"https://pixabay.com/images/search/{encodedQuery}/"
|
|
return await performImageSearch('pixabay_images', url)
|
|
|
|
|
|
searchFunctions = {
|
|
"bing": search_bing,
|
|
"baidu": search_baidu,
|
|
"sogou": search_sogou,
|
|
"quark": search_quark,
|
|
}
|
|
|
|
|
|
async def combined_search(query, platforms, includeLinks=True):
|
|
platformKeysRaw = platforms.split(',')
|
|
platformKeys = [p.strip() for p in platformKeysRaw if p.strip()]
|
|
|
|
async def runPlatform(platform):
|
|
searchFn = searchFunctions.get(platform)
|
|
if searchFn:
|
|
if platform == 'bing':
|
|
return await searchFn(query, includeLinks)
|
|
else:
|
|
return await searchFn(query, '1', includeLinks)
|
|
else:
|
|
return {"platform": platform, "success": False, "message": f"不支持的搜索平台: {platform}"}
|
|
|
|
return await asyncio.gather(*[runPlatform(p) for p in platformKeys])
|
|
|
|
|
|
async def search(query, platforms=None, includeLinks=False):
|
|
return await combined_search(query, platforms or "bing,baidu,sogou,quark", includeLinks)
|
|
|
|
|
|
async def search_web(query, platforms=None, includeLinks=False):
|
|
return await search(query, platforms, includeLinks)
|
|
|
|
|
|
async def main():
|
|
result = await combined_search('如何学习编程', 'bing,baidu,sogou,quark')
|
|
console.log(json.dumps(result, indent=2, ensure_ascii=False, default=str))
|
|
|
|
|
|
def wrap(coreFunction, parameterNames):
|
|
async def wrapped(params):
|
|
args = [params.get(name) for name in parameterNames]
|
|
return await coreFunction(*args)
|
|
return wrapped
|
|
|
|
|
|
async def _complete_wrapper(func):
|
|
async def wrapper(params):
|
|
try:
|
|
result = await func(params)
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
except Exception as error:
|
|
complete({"success": False, "message": f"搜索失败: {str(error)}"})
|
|
return wrapper
|
|
|
|
|
|
async def _make_export(coreFunction, parameterNames):
|
|
wrapped = wrap(coreFunction, parameterNames)
|
|
|
|
async def exportFn(params):
|
|
try:
|
|
result = await wrapped(params)
|
|
if isinstance(result, list):
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
else:
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
except Exception as error:
|
|
complete({"success": False, "message": f"搜索失败: {str(error)}"})
|
|
return exportFn
|
|
|
|
|
|
# 由于 Python 无法在模块加载时 await,这里直接构造同步包装器
|
|
def _make_sync_export(coreFunction, parameterNames):
|
|
def wrapped_sync(params):
|
|
async def _run():
|
|
args = [params.get(name) for name in parameterNames]
|
|
result = await coreFunction(*args)
|
|
if isinstance(result, list):
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
else:
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
try:
|
|
import asyncio
|
|
asyncio.get_event_loop().run_until_complete(_run())
|
|
except Exception as error:
|
|
complete({"success": False, "message": f"搜索失败: {str(error)}"})
|
|
return wrapped_sync
|
|
|
|
|
|
# 导出:保持 async def 接口,由运行时调度
|
|
async def _search_bing_export(params):
|
|
result = await search_bing(params.get("query"), params.get("includeLinks", False))
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
|
|
|
|
async def _search_baidu_export(params):
|
|
result = await search_baidu(params.get("query"), params.get("page"), params.get("includeLinks", False))
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
|
|
|
|
async def _search_sogou_export(params):
|
|
result = await search_sogou(params.get("query"), params.get("page"), params.get("includeLinks", False))
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
|
|
|
|
async def _search_quark_export(params):
|
|
result = await search_quark(params.get("query"), params.get("page"), params.get("includeLinks", False))
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
|
|
|
|
async def _search_export(params):
|
|
result = await search(params.get("query"), params.get("platforms"), params.get("includeLinks", False))
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
|
|
|
|
async def _search_web_export(params):
|
|
result = await search_web(params.get("query"), params.get("platforms"), params.get("includeLinks", False))
|
|
complete({"success": True, "message": "搜索完成", "data": result})
|
|
|
|
|
|
async def _search_bing_images_export(params):
|
|
result = await search_bing_images(params.get("query"))
|
|
complete({"success": True, "message": "图片搜索完成", "data": result})
|
|
|
|
|
|
async def _search_wikimedia_images_export(params):
|
|
result = await search_wikimedia_images(params.get("query"))
|
|
complete({"success": True, "message": "图片搜索完成", "data": result})
|
|
|
|
|
|
async def _search_duckduckgo_images_export(params):
|
|
result = await search_duckduckgo_images(params.get("query"))
|
|
complete({"success": True, "message": "图片搜索完成", "data": result})
|
|
|
|
|
|
async def _search_ecosia_images_export(params):
|
|
result = await search_ecosia_images(params.get("query"))
|
|
complete({"success": True, "message": "图片搜索完成", "data": result})
|
|
|
|
|
|
async def _search_pexels_images_export(params):
|
|
result = await search_pexels_images(params.get("query"))
|
|
complete({"success": True, "message": "图片搜索完成", "data": result})
|
|
|
|
|
|
async def _search_pixabay_images_export(params):
|
|
result = await search_pixabay_images(params.get("query"))
|
|
complete({"success": True, "message": "图片搜索完成", "data": result})
|
|
|
|
|
|
async def _combined_search_export(params):
|
|
result = await combined_search(params.get("query"), params.get("platforms"), params.get("includeLinks", True))
|
|
complete({"success": True, "message": "聚合搜索完成", "data": result})
|
|
|
|
|
|
async def _main_export(params):
|
|
await main()
|
|
|
|
|
|
exports.search_bing = _search_bing_export
|
|
exports.search_baidu = _search_baidu_export
|
|
exports.search_sogou = _search_sogou_export
|
|
exports.search_quark = _search_quark_export
|
|
exports.search = _search_export
|
|
exports.search_web = _search_web_export
|
|
exports.search_bing_images = _search_bing_images_export
|
|
exports.search_wikimedia_images = _search_wikimedia_images_export
|
|
exports.search_duckduckgo_images = _search_duckduckgo_images_export
|
|
exports.search_ecosia_images = _search_ecosia_images_export
|
|
exports.search_pexels_images = _search_pexels_images_export
|
|
exports.search_pixabay_images = _search_pixabay_images_export
|
|
exports.combined_search = _combined_search_export
|
|
exports.main = _main_export
|