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() 不执行的假阳性
This commit is contained in:
1 parent
f7f7033e57
commit
388ac493af
7 files changed
+868
-1
No files matched your search
@@ -0,0 +1,205 @@
|
||||
# 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
|
||||
Reference in new issue
Block a user