Files
xiaoli-cli/operit_py/various_search.py
T
shuiyu1123 dc77dfd304 feat: 将 47 个 Operit TypeScript 脚本全面翻译为 Python
- 新增 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 专属功能已降级处理(保留接口,返回不支持提示)
2026-06-23 12:27:18 +00:00

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