- 新增 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 专属功能已降级处理(保留接口,返回不支持提示)
516 lines
18 KiB
Python
516 lines
18 KiB
Python
# METADATA
|
|
# {
|
|
# "name": "crossref",
|
|
# "display_name": {
|
|
# "zh": "Crossref 学术文献查询",
|
|
# "en": "Crossref Academic Literature Search"
|
|
# },
|
|
# "description": {
|
|
# "zh": "Crossref 学术文献查询工具,提供 DOI 查询、关键词搜索、作者搜索等功能,帮助用户查找和获取学术文章元数据。",
|
|
# "en": "Crossref scholarly literature search tools: query by DOI, keyword, author, title, ISSN, and retrieve publication metadata."
|
|
# },
|
|
# "category": "Search",
|
|
# "enabledByDefault": true,
|
|
# "tools": [
|
|
# {
|
|
# "name": "search_by_doi",
|
|
# "description": { "zh": "通过 DOI (Digital Object Identifier) 查询文章的详细信息", "en": "Query article details by DOI (Digital Object Identifier)." },
|
|
# "parameters": [
|
|
# { "name": "doi", "description": { "zh": "文章的 DOI 标识符,例如 '10.1038/nature12373'", "en": "DOI identifier, e.g. '10.1038/nature12373'" }, "type": "string", "required": true }
|
|
# ]
|
|
# },
|
|
# {
|
|
# "name": "search_by_keyword",
|
|
# "description": { "zh": "通过关键词搜索学术文章", "en": "Search scholarly articles by keyword." },
|
|
# "parameters": [
|
|
# { "name": "query", "description": { "zh": "搜索关键词", "en": "Search query keyword(s)" }, "type": "string", "required": true },
|
|
# { "name": "rows", "description": { "zh": "返回结果数量,默认 10,最大 100", "en": "Number of results to return (default: 10, max: 100)" }, "type": "number", "required": false },
|
|
# { "name": "sort", "description": { "zh": "排序方式,可选值:'relevance'(相关性), 'score'(评分), 'updated'(更新时间), 'deposited'(提交时间), 'indexed'(索引时间), 'published'(发布时间)", "en": "Sort mode. Options: 'relevance', 'score', 'updated', 'deposited', 'indexed', 'published'." }, "type": "string", "required": false },
|
|
# { "name": "order", "description": { "zh": "排序顺序,可选值:'asc'(升序), 'desc'(降序),默认 'desc'", "en": "Sort order: 'asc' or 'desc' (default: 'desc')." }, "type": "string", "required": false }
|
|
# ]
|
|
# },
|
|
# {
|
|
# "name": "search_by_author",
|
|
# "description": { "zh": "通过作者名字搜索文章", "en": "Search articles by author name." },
|
|
# "parameters": [
|
|
# { "name": "author", "description": { "zh": "作者名字", "en": "Author name" }, "type": "string", "required": true },
|
|
# { "name": "rows", "description": { "zh": "返回结果数量,默认 10,最大 100", "en": "Number of results to return (default 10, max 100)" }, "type": "number", "required": false }
|
|
# ]
|
|
# },
|
|
# {
|
|
# "name": "search_by_title",
|
|
# "description": { "zh": "通过文章标题搜索", "en": "Search articles by title." },
|
|
# "parameters": [
|
|
# { "name": "title", "description": { "zh": "文章标题或标题关键词", "en": "Article title or title keyword(s)" }, "type": "string", "required": true },
|
|
# { "name": "rows", "description": { "zh": "返回结果数量,默认 10,最大 100", "en": "Number of results to return (default 10, max 100)" }, "type": "number", "required": false }
|
|
# ]
|
|
# },
|
|
# {
|
|
# "name": "search_by_issn",
|
|
# "description": { "zh": "通过期刊 ISSN 查询该期刊发表的文章", "en": "Search articles published in a journal by ISSN." },
|
|
# "parameters": [
|
|
# { "name": "issn", "description": { "zh": "期刊的 ISSN 标识符,例如 '1476-4687'", "en": "Journal ISSN identifier, e.g. '1476-4687'" }, "type": "string", "required": true },
|
|
# { "name": "rows", "description": { "zh": "返回结果数量,默认 10,最大 100", "en": "Number of results to return (default 10, max 100)" }, "type": "number", "required": false }
|
|
# ]
|
|
# }
|
|
# ]
|
|
# }
|
|
|
|
import json
|
|
import re
|
|
from urllib.parse import quote
|
|
|
|
BASE_URL = "https://api.crossref.org"
|
|
DEFAULT_ROWS = 10
|
|
MAX_ROWS = 100
|
|
|
|
|
|
def buildQueryString(params):
|
|
return "&".join(f"{quote(str(key))}={quote(str(value))}" for key, value in params.items())
|
|
|
|
|
|
# 格式化作者信息
|
|
def formatAuthors(authors):
|
|
if not authors or len(authors) == 0:
|
|
return "N/A"
|
|
names = []
|
|
for author in authors[:5]: # 只显示前5个作者
|
|
given = author.get("given") or ""
|
|
family = author.get("family") or ""
|
|
name = f"{given} {family}".strip()
|
|
if len(name) > 0:
|
|
names.append(name)
|
|
return ", ".join(names)
|
|
|
|
|
|
# 格式化日期
|
|
def formatDate(dateParts):
|
|
if not dateParts or len(dateParts) == 0 or not dateParts[0]:
|
|
return "N/A"
|
|
parts = dateParts[0]
|
|
if len(parts) == 1:
|
|
return f"{parts[0]}"
|
|
if len(parts) == 2:
|
|
return f"{parts[0]}-{str(parts[1]).rjust(2, '0')}"
|
|
if len(parts) == 3:
|
|
return f"{parts[0]}-{str(parts[1]).rjust(2, '0')}-{str(parts[2]).rjust(2, '0')}"
|
|
return "N/A"
|
|
|
|
|
|
def _get(obj, key, default=None):
|
|
if isinstance(obj, dict):
|
|
return obj.get(key, default)
|
|
return default
|
|
|
|
|
|
# 格式化单篇文章信息
|
|
def formatArticle(item, index=None):
|
|
lines = []
|
|
|
|
if index is not None:
|
|
lines.append(f"\n=== Article {index + 1} ===")
|
|
|
|
# 标题
|
|
title = (item.get("title")[0] if item.get("title") and len(item.get("title")) > 0 else "No Title")
|
|
lines.append(f"Title: {title}")
|
|
|
|
# DOI
|
|
if item.get("DOI"):
|
|
lines.append(f"DOI: {item['DOI']}")
|
|
lines.append(f"URL: https://doi.org/{item['DOI']}")
|
|
|
|
# 作者
|
|
authors = formatAuthors(item.get("author"))
|
|
lines.append(f"Authors: {authors}")
|
|
|
|
# 发表日期
|
|
published = _get(item, "published") or {}
|
|
publishedPrint = _get(item, "published-print") or {}
|
|
publishedOnline = _get(item, "published-online") or {}
|
|
publishedDate = formatDate(
|
|
_get(published, "date-parts") or _get(publishedPrint, "date-parts") or _get(publishedOnline, "date-parts")
|
|
)
|
|
lines.append(f"Published: {publishedDate}")
|
|
|
|
# 期刊/会议
|
|
containerTitle = item.get("container-title")
|
|
if containerTitle and len(containerTitle) > 0:
|
|
lines.append(f"Journal/Conference: {containerTitle[0]}")
|
|
|
|
# ISSN
|
|
issn = item.get("ISSN")
|
|
if issn and len(issn) > 0:
|
|
lines.append(f"ISSN: {', '.join(issn)}")
|
|
|
|
# 出版商
|
|
if item.get("publisher"):
|
|
lines.append(f"Publisher: {item['publisher']}")
|
|
|
|
# 类型
|
|
if item.get("type"):
|
|
lines.append(f"Type: {item['type']}")
|
|
|
|
# 引用次数
|
|
if item.get("is-referenced-by-count") is not None:
|
|
lines.append(f"Citations: {item['is-referenced-by-count']}")
|
|
|
|
# 摘要(如果有)
|
|
if item.get("abstract"):
|
|
# 移除 HTML 标签
|
|
abstractText = re.sub(r"<[^>]*>", "", item["abstract"])
|
|
lines.append(f"Abstract: {abstractText[:500]}{'...' if len(abstractText) > 500 else ''}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
async def _httpGet(url, headers):
|
|
client = OkHttp.newBuilder().build()
|
|
response = await client.newRequest().url(url).method("GET").headers(headers).build().execute()
|
|
return response
|
|
|
|
|
|
# 通过 DOI 查询文章
|
|
async def searchByDoi(params):
|
|
doi = params.get("doi")
|
|
|
|
if not doi or doi.strip() == "":
|
|
return {
|
|
"success": False,
|
|
"message": "请提供有效的 DOI"
|
|
}
|
|
|
|
try:
|
|
url = f"{BASE_URL}/works/{quote(doi, safe='')}"
|
|
response = await _httpGet(url, {
|
|
"User-Agent": "Operit/1.0 (mailto:support@example.com)"
|
|
})
|
|
|
|
if not response.isSuccessful():
|
|
return {
|
|
"success": False,
|
|
"message": f"查询失败: HTTP {response.statusCode} - {getattr(response, 'statusMessage', '')}"
|
|
}
|
|
|
|
data = json.loads(response.content)
|
|
|
|
if data.get("status") == "ok" and data.get("message"):
|
|
article = formatArticle(data["message"])
|
|
return {
|
|
"success": True,
|
|
"message": "查询成功",
|
|
"data": article
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"message": "未找到该 DOI 对应的文章"
|
|
}
|
|
except Exception as error:
|
|
return {
|
|
"success": False,
|
|
"message": f"查询失败: {str(error)}"
|
|
}
|
|
|
|
|
|
# 通过关键词搜索文章
|
|
async def searchByKeyword(params):
|
|
query = params.get("query")
|
|
rows = params.get("rows") if params.get("rows") is not None else DEFAULT_ROWS
|
|
sort = params.get("sort") if params.get("sort") is not None else "relevance"
|
|
order = params.get("order") if params.get("order") is not None else "desc"
|
|
|
|
if not query or query.strip() == "":
|
|
return {
|
|
"success": False,
|
|
"message": "请提供有效的搜索关键词"
|
|
}
|
|
|
|
actualRows = min(max(int(rows), 1), MAX_ROWS)
|
|
|
|
try:
|
|
queryString = buildQueryString({
|
|
"query": query,
|
|
"rows": str(actualRows),
|
|
"sort": sort,
|
|
"order": order
|
|
})
|
|
|
|
url = f"{BASE_URL}/works?{queryString}"
|
|
response = await _httpGet(url, {
|
|
"User-Agent": "Operit/1.0 (mailto:support@example.com)"
|
|
})
|
|
|
|
if not response.isSuccessful():
|
|
return {
|
|
"success": False,
|
|
"message": f"搜索失败: HTTP {response.statusCode} - {getattr(response, 'statusMessage', '')}"
|
|
}
|
|
|
|
data = json.loads(response.content)
|
|
|
|
if data.get("status") == "ok" and data.get("message") and data["message"].get("items"):
|
|
items = data["message"]["items"]
|
|
totalResults = data["message"].get("total-results")
|
|
|
|
results = [formatArticle(item, index) for index, item in enumerate(items)]
|
|
|
|
summary = f"Found {totalResults} results (showing {len(items)}):\n" + "\n\n".join(results)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "搜索成功",
|
|
"data": summary,
|
|
"total": totalResults,
|
|
"count": len(items)
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"message": "未找到相关文章"
|
|
}
|
|
except Exception as error:
|
|
return {
|
|
"success": False,
|
|
"message": f"搜索失败: {str(error)}"
|
|
}
|
|
|
|
|
|
# 通过作者搜索文章
|
|
async def searchByAuthor(params):
|
|
author = params.get("author")
|
|
rows = params.get("rows") if params.get("rows") is not None else DEFAULT_ROWS
|
|
|
|
if not author or author.strip() == "":
|
|
return {
|
|
"success": False,
|
|
"message": "请提供有效的作者名字"
|
|
}
|
|
|
|
actualRows = min(max(int(rows), 1), MAX_ROWS)
|
|
|
|
try:
|
|
queryString = buildQueryString({
|
|
"query.author": author,
|
|
"rows": str(actualRows)
|
|
})
|
|
|
|
url = f"{BASE_URL}/works?{queryString}"
|
|
response = await _httpGet(url, {
|
|
"User-Agent": "Operit/1.0 (mailto:support@example.com)"
|
|
})
|
|
|
|
if not response.isSuccessful():
|
|
return {
|
|
"success": False,
|
|
"message": f"搜索失败: HTTP {response.statusCode} - {getattr(response, 'statusMessage', '')}"
|
|
}
|
|
|
|
data = json.loads(response.content)
|
|
|
|
if data.get("status") == "ok" and data.get("message") and data["message"].get("items"):
|
|
items = data["message"]["items"]
|
|
totalResults = data["message"].get("total-results")
|
|
|
|
results = [formatArticle(item, index) for index, item in enumerate(items)]
|
|
|
|
summary = f"Found {totalResults} results for author \"{author}\" (showing {len(items)}):\n" + "\n\n".join(results)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "搜索成功",
|
|
"data": summary,
|
|
"total": totalResults,
|
|
"count": len(items)
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"message": f"未找到该作者 \"{author}\" 的文章"
|
|
}
|
|
except Exception as error:
|
|
return {
|
|
"success": False,
|
|
"message": f"搜索失败: {str(error)}"
|
|
}
|
|
|
|
|
|
# 通过标题搜索文章
|
|
async def searchByTitle(params):
|
|
title = params.get("title")
|
|
rows = params.get("rows") if params.get("rows") is not None else DEFAULT_ROWS
|
|
|
|
if not title or title.strip() == "":
|
|
return {
|
|
"success": False,
|
|
"message": "请提供有效的文章标题"
|
|
}
|
|
|
|
actualRows = min(max(int(rows), 1), MAX_ROWS)
|
|
|
|
try:
|
|
queryString = buildQueryString({
|
|
"query.title": title,
|
|
"rows": str(actualRows)
|
|
})
|
|
|
|
url = f"{BASE_URL}/works?{queryString}"
|
|
response = await _httpGet(url, {
|
|
"User-Agent": "Operit/1.0 (mailto:support@example.com)"
|
|
})
|
|
|
|
data = json.loads(response.content)
|
|
|
|
if data.get("status") == "ok" and data.get("message") and data["message"].get("items"):
|
|
items = data["message"]["items"]
|
|
totalResults = data["message"].get("total-results")
|
|
|
|
results = [formatArticle(item, index) for index, item in enumerate(items)]
|
|
|
|
summary = f"Found {totalResults} results matching title \"{title}\" (showing {len(items)}):\n" + "\n\n".join(results)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "搜索成功",
|
|
"data": summary,
|
|
"total": totalResults,
|
|
"count": len(items)
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"message": f"未找到标题包含 \"{title}\" 的文章"
|
|
}
|
|
except Exception as error:
|
|
return {
|
|
"success": False,
|
|
"message": f"搜索失败: {str(error)}"
|
|
}
|
|
|
|
|
|
# 通过 ISSN 查询期刊文章
|
|
async def searchByISSN(params):
|
|
issn = params.get("issn")
|
|
rows = params.get("rows") if params.get("rows") is not None else DEFAULT_ROWS
|
|
|
|
if not issn or issn.strip() == "":
|
|
return {
|
|
"success": False,
|
|
"message": "请提供有效的 ISSN"
|
|
}
|
|
|
|
actualRows = min(max(int(rows), 1), MAX_ROWS)
|
|
|
|
try:
|
|
url = f"{BASE_URL}/journals/{quote(issn, safe='')}/works?rows={actualRows}"
|
|
response = await _httpGet(url, {
|
|
"User-Agent": "Operit/1.0 (mailto:support@example.com)"
|
|
})
|
|
|
|
if not response.isSuccessful():
|
|
return {
|
|
"success": False,
|
|
"message": f"查询失败: HTTP {response.statusCode} - {getattr(response, 'statusMessage', '')}"
|
|
}
|
|
|
|
data = json.loads(response.content)
|
|
|
|
if data.get("status") == "ok" and data.get("message") and data["message"].get("items"):
|
|
items = data["message"]["items"]
|
|
totalResults = data["message"].get("total-results")
|
|
|
|
results = [formatArticle(item, index) for index, item in enumerate(items)]
|
|
|
|
summary = f"Found {totalResults} articles from journal ISSN {issn} (showing {len(items)}):\n" + "\n\n".join(results)
|
|
|
|
return {
|
|
"success": True,
|
|
"message": "查询成功",
|
|
"data": summary,
|
|
"total": totalResults,
|
|
"count": len(items)
|
|
}
|
|
else:
|
|
return {
|
|
"success": False,
|
|
"message": f"未找到 ISSN \"{issn}\" 对应期刊的文章"
|
|
}
|
|
except Exception as error:
|
|
return {
|
|
"success": False,
|
|
"message": f"查询失败: {str(error)}"
|
|
}
|
|
|
|
|
|
# 包装函数,统一处理错误
|
|
async def wrapToolExecution(func, params):
|
|
try:
|
|
result = await func(params)
|
|
complete(result)
|
|
except Exception as error:
|
|
console.error("工具执行失败", error)
|
|
complete({
|
|
"success": False,
|
|
"message": f"工具执行时发生意外错误: {str(error)}",
|
|
})
|
|
|
|
|
|
# 测试函数
|
|
async def main(params=None):
|
|
if params is None:
|
|
params = {}
|
|
console.log("=== Crossref API 测试 ===\n")
|
|
|
|
# 测试 1: 通过 DOI 查询
|
|
console.log("1. 测试通过 DOI 查询...")
|
|
doiResult = await searchByDoi({"doi": "10.1038/nature12373"})
|
|
console.log(json.dumps(doiResult, ensure_ascii=False, indent=2))
|
|
console.log("\n")
|
|
|
|
# 测试 2: 通过关键词搜索
|
|
console.log("2. 测试通过关键词搜索...")
|
|
keywordResult = await searchByKeyword({"query": "machine learning", "rows": 3})
|
|
console.log(json.dumps(keywordResult, ensure_ascii=False, indent=2))
|
|
console.log("\n")
|
|
|
|
# 测试 3: 通过作者搜索
|
|
console.log("3. 测试通过作者搜索...")
|
|
authorResult = await searchByAuthor({"author": "John Smith", "rows": 3})
|
|
console.log(json.dumps(authorResult, ensure_ascii=False, indent=2))
|
|
console.log("\n")
|
|
|
|
# 测试 4: 通过标题搜索
|
|
console.log("4. 测试通过标题搜索...")
|
|
titleResult = await searchByTitle({"title": "neural networks", "rows": 3})
|
|
console.log(json.dumps(titleResult, ensure_ascii=False, indent=2))
|
|
console.log("\n")
|
|
|
|
console.log("=== 测试完成 ===")
|
|
|
|
|
|
async def _exported_search_by_doi(params):
|
|
await wrapToolExecution(searchByDoi, params)
|
|
|
|
|
|
async def _exported_search_by_keyword(params):
|
|
await wrapToolExecution(searchByKeyword, params)
|
|
|
|
|
|
async def _exported_search_by_author(params):
|
|
await wrapToolExecution(searchByAuthor, params)
|
|
|
|
|
|
async def _exported_search_by_title(params):
|
|
await wrapToolExecution(searchByTitle, params)
|
|
|
|
|
|
async def _exported_search_by_issn(params):
|
|
await wrapToolExecution(searchByISSN, params)
|
|
|
|
|
|
exports.search_by_doi = _exported_search_by_doi
|
|
exports.search_by_keyword = _exported_search_by_keyword
|
|
exports.search_by_author = _exported_search_by_author
|
|
exports.search_by_title = _exported_search_by_title
|
|
exports.search_by_issn = _exported_search_by_issn
|
|
exports.main = main
|