chore: track claude skills, tools, templates, reference code and research-wiki
- Add all claude skills (brainstorming, commit, debugging, TDD, etc.) - Add claude hooks (pre-commit-guard, post-edit-quality) - Add research templates (experiment plan, research brief, etc.) - Add claude tools (arxiv/semantic_scholar/openalex fetch, wiki, exa) - Add TRM4 reference implementation as algorithm fidelity baseline - Add research-wiki content (plans, index, graph, query_pack) - Update .gitignore to exclude .graphify_version runtime state
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""OpenAlex 搜索命令行工具。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("警告: 未安装 requests,已跳过 OpenAlex CLI。", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
API_URL = "https://api.openalex.org/works"
|
||||
REQUEST_TIMEOUT_SECONDS = 30.0
|
||||
USER_AGENT = "openalex_fetch.py/1.0"
|
||||
|
||||
|
||||
def _clean_text(value: Any) -> str:
|
||||
"""压缩空白并清理文本值。"""
|
||||
return " ".join(str(value).split())
|
||||
|
||||
|
||||
def _normalize_year_filter(raw_year: str) -> str:
|
||||
"""将命令行年份参数转换为 OpenAlex filter 语法。"""
|
||||
value = raw_year.strip()
|
||||
if not value:
|
||||
raise ValueError("年份参数不能为空")
|
||||
|
||||
if value.isdigit() and len(value) == 4:
|
||||
return f"publication_year:{value}"
|
||||
|
||||
if value.endswith("-") and value[:-1].strip().isdigit():
|
||||
start_year = value[:-1].strip()
|
||||
return f"from_publication_date:{start_year}-01-01"
|
||||
|
||||
if value.startswith("-") and value[1:].strip().isdigit():
|
||||
end_year = value[1:].strip()
|
||||
return f"to_publication_date:{end_year}-12-31"
|
||||
|
||||
if "-" in value:
|
||||
start_year, end_year = [part.strip() for part in value.split("-", 1)]
|
||||
if start_year.isdigit() and end_year.isdigit():
|
||||
return f"publication_year:{start_year}-{end_year}"
|
||||
|
||||
raise ValueError(f"无法识别的年份参数: {raw_year}")
|
||||
|
||||
|
||||
def _normalize_sort_value(sort_value: str) -> str:
|
||||
"""将用户输入的排序参数转换为 OpenAlex sort 语法。"""
|
||||
value = sort_value.strip()
|
||||
if not value:
|
||||
raise ValueError("排序参数不能为空")
|
||||
if value in {"relevance", "relevance_score"}:
|
||||
return "relevance_score:desc"
|
||||
return value
|
||||
|
||||
|
||||
def _normalize_work(work: dict[str, Any]) -> dict[str, Any]:
|
||||
"""将 OpenAlex work 记录压缩成稳定输出结构。"""
|
||||
authorships = work.get("authorships")
|
||||
authors: list[str] = []
|
||||
institutions: list[str] = []
|
||||
if isinstance(authorships, list):
|
||||
for authorship in authorships:
|
||||
if not isinstance(authorship, dict):
|
||||
continue
|
||||
author = authorship.get("author")
|
||||
if isinstance(author, dict):
|
||||
display_name = author.get("display_name")
|
||||
if display_name:
|
||||
cleaned_name = _clean_text(display_name)
|
||||
if cleaned_name not in authors:
|
||||
authors.append(cleaned_name)
|
||||
author_institutions = authorship.get("institutions")
|
||||
if isinstance(author_institutions, list):
|
||||
for institution in author_institutions:
|
||||
if not isinstance(institution, dict):
|
||||
continue
|
||||
display_name = institution.get("display_name")
|
||||
if display_name:
|
||||
cleaned_name = _clean_text(display_name)
|
||||
if cleaned_name not in institutions:
|
||||
institutions.append(cleaned_name)
|
||||
|
||||
return {
|
||||
"title": _clean_text(work.get("display_name", "")),
|
||||
"authors": authors,
|
||||
"year": work.get("publication_year"),
|
||||
"cited_by_count": work.get("cited_by_count"),
|
||||
"institutions": institutions,
|
||||
"doi": work.get("doi"),
|
||||
}
|
||||
|
||||
|
||||
def search_openalex(
|
||||
query: str,
|
||||
max_results: int,
|
||||
year: str | None,
|
||||
work_type: str | None,
|
||||
sort: str,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""搜索 OpenAlex 并返回标准化论文结果。"""
|
||||
params: dict[str, Any] = {
|
||||
"search": query,
|
||||
"per-page": max_results,
|
||||
"sort": _normalize_sort_value(sort),
|
||||
}
|
||||
|
||||
filters: list[str] = []
|
||||
if year:
|
||||
filters.append(_normalize_year_filter(year))
|
||||
if work_type:
|
||||
filters.append(f"type:{work_type.strip()}")
|
||||
if filters:
|
||||
params["filter"] = ",".join(filters)
|
||||
|
||||
response = requests.get(
|
||||
API_URL,
|
||||
params=params,
|
||||
headers={"User-Agent": USER_AGENT},
|
||||
timeout=REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
payload = response.json()
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("OpenAlex 返回了非对象类型的 JSON")
|
||||
|
||||
works = payload.get("results", [])
|
||||
if not isinstance(works, list):
|
||||
raise ValueError("OpenAlex 响应缺少 results 列表")
|
||||
|
||||
normalized_results: list[dict[str, Any]] = []
|
||||
for work in works:
|
||||
if isinstance(work, dict):
|
||||
normalized_results.append(_normalize_work(work))
|
||||
return normalized_results
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
"""构建命令行参数解析器。"""
|
||||
parser = argparse.ArgumentParser(description="OpenAlex 搜索工具")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
search_parser = subparsers.add_parser("search", help="搜索 OpenAlex works")
|
||||
search_parser.add_argument("query", help="搜索关键词")
|
||||
search_parser.add_argument("--max", dest="max_results", type=int, default=10)
|
||||
search_parser.add_argument("--year", default=None, help="年份或年份范围")
|
||||
search_parser.add_argument(
|
||||
"--type", dest="work_type", default=None, help="作品类型"
|
||||
)
|
||||
search_parser.add_argument(
|
||||
"--sort",
|
||||
default="relevance",
|
||||
help="排序字段,relevance 会映射为 relevance_score:desc",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""CLI 入口。"""
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
try:
|
||||
if args.command == "search":
|
||||
results = search_openalex(
|
||||
query=args.query,
|
||||
max_results=args.max_results,
|
||||
year=args.year,
|
||||
work_type=args.work_type,
|
||||
sort=args.sort,
|
||||
)
|
||||
json.dump(results, sys.stdout, ensure_ascii=False, indent=2)
|
||||
sys.stdout.write("\n")
|
||||
return 0
|
||||
except (requests.RequestException, ValueError) as exc:
|
||||
print(f"错误: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
parser.error(f"未知命令: {args.command}")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user