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 专属功能已降级处理(保留接口,返回不支持提示)
This commit is contained in:
1 parent
6fa759665d
commit
dc77dfd304
48 files changed
+22273
No files matched your search
@@ -0,0 +1,750 @@
|
||||
# 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
|
||||
Reference in new issue
Block a user