Files
xiaoli-cli/plugins/operit_scripts/market_crossref.py
T
shuiyu67 388ac493af feat(operit): 增强转译层并接入 5 个 Operit 市场插件
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() 不执行的假阳性
2026-08-07 16:38:58 +08:00

206 lines
8.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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