operit_loader.py: - OkHttp 新增 newClient()(真实市场插件普遍使用,而非 newBuilder) - OkHttpRequest.execute() 改为 async,与 Operit 运行时异步语义对齐 - OkHttpResponse 新增 json() 与 statusMessage()(基于 http.HTTPStatus) operit_translate.py(新增): - Operit .ts 插件 -> xiaoli-cli .py 脚手架生成器 - 自动提取 METADATA、生成 exports 胶水,原 TS 作参考注释保留 plugins/operit_scripts/ 新增 5 个已跑通的真实插件: - market_time 时间查询/格式化 - market_duckduckgo 网页内容抓取与清洗 - market_crossref Crossref 学术文献检索 - market_network_test HTTP 连通性测试 - market_reader 本地代码递归搜索 注: 工具需注册为 async 包装函数,同步 lambda 会导致 complete() 不执行的假阳性
206 lines
8.3 KiB
Python
206 lines
8.3 KiB
Python
# METADATA
|
||
# {
|
||
# "name": "crossref",
|
||
# "display_name": { "zh": "Crossref 学术文献查询", "en": "Crossref Academic Literature Search" },
|
||
# "description": { "zh": "Crossref 学术文献查询(DOI/关键词/作者/标题/ISSN)", "en": "Crossref scholarly literature search." },
|
||
# "category": "Search",
|
||
# "tools": [
|
||
# { "name": "search_by_doi", "description": { "zh": "通过 DOI 查询文章", "en": "Query by DOI" } },
|
||
# { "name": "search_by_keyword", "description": { "zh": "通过关键词搜索", "en": "Search by keyword" } },
|
||
# { "name": "search_by_author", "description": { "zh": "通过作者搜索", "en": "Search by author" } },
|
||
# { "name": "search_by_title", "description": { "zh": "通过标题搜索", "en": "Search by title" } },
|
||
# { "name": "search_by_issn", "description": { "zh": "通过 ISSN 查询", "en": "Search by ISSN" } }
|
||
# ]
|
||
# }
|
||
import json as _json
|
||
from urllib.parse import quote_plus
|
||
|
||
HEADERS = {'User-Agent': 'Operit/1.0 (mailto:support@example.com)'}
|
||
BASE_URL = "https://api.crossref.org"
|
||
DEFAULT_ROWS = 10
|
||
MAX_ROWS = 100
|
||
|
||
|
||
def getErrorMessage(error):
|
||
return str(error) if error else "unknown"
|
||
|
||
|
||
def getErrorStack(error):
|
||
import traceback
|
||
return traceback.format_exc()
|
||
|
||
|
||
def format_authors(authors):
|
||
if not authors:
|
||
return "N/A"
|
||
return ", ".join(
|
||
f"{a.get('given', '')} {a.get('family', '')}".strip()
|
||
for a in authors[:5] if a.get('given') or a.get('family')
|
||
) or "N/A"
|
||
|
||
|
||
def format_date(date_parts):
|
||
if not date_parts or not date_parts[0]:
|
||
return "N/A"
|
||
p = date_parts[0]
|
||
if len(p) == 1:
|
||
return str(p[0])
|
||
if len(p) == 2:
|
||
return f"{p[0]}-{str(p[1]).zfill(2)}"
|
||
if len(p) == 3:
|
||
return f"{p[0]}-{str(p[1]).zfill(2)}-{str(p[2]).zfill(2)}"
|
||
return "N/A"
|
||
|
||
|
||
def format_article(item, index=None):
|
||
lines = []
|
||
if index is not None:
|
||
lines.append(f"\n=== Article {index + 1} ===")
|
||
title = item.get("title", ["No Title"])[0] if item.get("title") else "No Title"
|
||
lines.append(f"Title: {title}")
|
||
if item.get("DOI"):
|
||
lines.append(f"DOI: {item['DOI']}")
|
||
lines.append(f"URL: https://doi.org/{item['DOI']}")
|
||
lines.append(f"Authors: {format_authors(item.get('author'))}")
|
||
dp = (item.get("published") or item.get("published-print") or item.get("published-online") or {}).get("date-parts")
|
||
lines.append(f"Published: {format_date(dp)}")
|
||
if item.get("container-title"):
|
||
lines.append(f"Journal/Conference: {item['container-title'][0]}")
|
||
if item.get("ISSN"):
|
||
lines.append(f"ISSN: {', '.join(item['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"):
|
||
ab = __import__("re").sub(r"<[^>]*>", "", item["abstract"])
|
||
lines.append(f"Abstract: {ab[:500]}{'...' if len(ab) > 500 else ''}")
|
||
return "\n".join(lines)
|
||
|
||
|
||
async def _get_json(url):
|
||
client = OkHttp.newClient()
|
||
resp = await client.newRequest().url(url).method("GET").headers(HEADERS).build().execute()
|
||
if not resp.isSuccessful():
|
||
raise RuntimeError(f"HTTP {resp.statusCode} - {resp.statusMessage}")
|
||
return resp.json()
|
||
|
||
|
||
async def search_by_doi(params):
|
||
doi = (params.get("doi") or "").strip()
|
||
if not doi:
|
||
return {"success": False, "message": "请提供有效的 DOI"}
|
||
try:
|
||
data = await _get_json(f"{BASE_URL}/works/{quote_plus(doi)}")
|
||
if data.get("status") == "ok" and data.get("message"):
|
||
return {"success": True, "message": "查询成功", "data": format_article(data["message"])}
|
||
return {"success": False, "message": "未找到该 DOI 对应的文章"}
|
||
except Exception as e:
|
||
return {"success": False, "message": f"查询失败: {getErrorMessage(e)}"}
|
||
|
||
|
||
async def search_by_keyword(params):
|
||
query = (params.get("query") or "").strip()
|
||
if not query:
|
||
return {"success": False, "message": "请提供有效的搜索关键词"}
|
||
rows = max(1, min(int(params.get("rows", DEFAULT_ROWS) or DEFAULT_ROWS), MAX_ROWS))
|
||
sort = params.get("sort", "relevance")
|
||
order = params.get("order", "desc")
|
||
qs = "&".join(f"{k}={quote_plus(v)}" for k, v in
|
||
{"query": query, "rows": str(rows), "sort": sort, "order": order}.items())
|
||
try:
|
||
data = await _get_json(f"{BASE_URL}/works?{qs}")
|
||
return _summarize(data, query, f'关键词 "{query}"')
|
||
except Exception as e:
|
||
return {"success": False, "message": f"搜索失败: {getErrorMessage(e)}"}
|
||
|
||
|
||
async def search_by_author(params):
|
||
author = (params.get("author") or "").strip()
|
||
if not author:
|
||
return {"success": False, "message": "请提供有效的作者名字"}
|
||
rows = max(1, min(int(params.get("rows", DEFAULT_ROWS) or DEFAULT_ROWS), MAX_ROWS))
|
||
qs = "&".join(f"{k}={quote_plus(v)}" for k, v in
|
||
{"query.author": author, "rows": str(rows)}.items())
|
||
try:
|
||
data = await _get_json(f"{BASE_URL}/works?{qs}")
|
||
return _summarize(data, author, f'作者 "{author}"')
|
||
except Exception as e:
|
||
return {"success": False, "message": f"搜索失败: {getErrorMessage(e)}"}
|
||
|
||
|
||
async def search_by_title(params):
|
||
title = (params.get("title") or "").strip()
|
||
if not title:
|
||
return {"success": False, "message": "请提供有效的文章标题"}
|
||
rows = max(1, min(int(params.get("rows", DEFAULT_ROWS) or DEFAULT_ROWS), MAX_ROWS))
|
||
qs = "&".join(f"{k}={quote_plus(v)}" for k, v in
|
||
{"query.title": title, "rows": str(rows)}.items())
|
||
try:
|
||
data = await _get_json(f"{BASE_URL}/works?{qs}")
|
||
return _summarize(data, title, f'标题 "{title}"')
|
||
except Exception as e:
|
||
return {"success": False, "message": f"搜索失败: {getErrorMessage(e)}"}
|
||
|
||
|
||
async def search_by_issn(params):
|
||
issn = (params.get("issn") or "").strip()
|
||
if not issn:
|
||
return {"success": False, "message": "请提供有效的 ISSN"}
|
||
rows = max(1, min(int(params.get("rows", DEFAULT_ROWS) or DEFAULT_ROWS), MAX_ROWS))
|
||
try:
|
||
data = await _get_json(f"{BASE_URL}/journals/{quote_plus(issn)}/works?rows={rows}")
|
||
return _summarize(data, issn, f'ISSN {issn}')
|
||
except Exception as e:
|
||
return {"success": False, "message": f"查询失败: {getErrorMessage(e)}"}
|
||
|
||
|
||
def _summarize(data, key, label):
|
||
if data.get("status") == "ok" and data.get("message") and data.get("message", {}).get("items"):
|
||
items = data["message"]["items"]
|
||
total = data["message"].get("total-results")
|
||
results = [format_article(it, i) for i, it in enumerate(items)]
|
||
return {"success": True, "message": "搜索成功",
|
||
"data": f"Found {total} results for {label} (showing {len(items)}):\n\n" + "\n\n".join(results),
|
||
"total": total, "count": len(items)}
|
||
return {"success": False, "message": f"未找到匹配 {label} 的文章"}
|
||
|
||
|
||
async def wrap(func, params, ok_msg="操作成功", err_msg="操作失败"):
|
||
try:
|
||
result = await func(params)
|
||
complete({"success": True, "message": result.get("message", ok_msg), "data": result.get("data"),
|
||
**{k: v for k, v in result.items() if k not in ("success", "message", "data")}})
|
||
except Exception as error:
|
||
console.error(f"Error: {getErrorMessage(error)}")
|
||
complete({"success": False, "message": getErrorMessage(error), "error_stack": getErrorStack(error)})
|
||
|
||
|
||
async def search_by_doi_wrapper(params):
|
||
await wrap(search_by_doi, params, "查询成功", "查询失败")
|
||
|
||
|
||
async def search_by_keyword_wrapper(params):
|
||
await wrap(search_by_keyword, params, "搜索成功", "搜索失败")
|
||
|
||
|
||
async def search_by_author_wrapper(params):
|
||
await wrap(search_by_author, params, "搜索成功", "搜索失败")
|
||
|
||
|
||
async def search_by_title_wrapper(params):
|
||
await wrap(search_by_title, params, "搜索成功", "搜索失败")
|
||
|
||
|
||
async def search_by_issn_wrapper(params):
|
||
await wrap(search_by_issn, params, "查询成功", "查询失败")
|
||
|
||
|
||
exports.search_by_doi = search_by_doi_wrapper
|
||
exports.search_by_keyword = search_by_keyword_wrapper
|
||
exports.search_by_author = search_by_author_wrapper
|
||
exports.search_by_title = search_by_title_wrapper
|
||
exports.search_by_issn = search_by_issn_wrapper
|