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:
shuiyu67 committed 2026-08-07 16:38:58 +08:00
1 parent f7f7033e57
commit 388ac493af
7 files changed
+868 -1

No files matched your search

+205
View File
@@ -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