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,224 @@
|
||||
"""Semantic Scholar 搜索命令行工具。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("警告: 未安装 requests,已跳过 Semantic Scholar CLI。", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
API_URL = "https://api.semanticscholar.org/graph/v1/paper/search"
|
||||
REQUEST_FIELDS = "title,authors,year,venue,citationCount,externalIds,tldr"
|
||||
REQUEST_INTERVAL_SECONDS = 1.0
|
||||
REQUEST_TIMEOUT_SECONDS = 30.0
|
||||
USER_AGENT = "semantic_scholar_fetch.py/1.0"
|
||||
_LAST_REQUEST_AT: float | None = None
|
||||
|
||||
|
||||
def _normalize_filter_values(values: list[str] | None) -> str | None:
|
||||
"""将过滤参数整理为 API 需要的逗号分隔字符串。"""
|
||||
if not values:
|
||||
return None
|
||||
|
||||
items: list[str] = []
|
||||
for value in values:
|
||||
for item in value.split(","):
|
||||
cleaned = item.strip()
|
||||
if cleaned:
|
||||
items.append(cleaned)
|
||||
|
||||
if not items:
|
||||
return None
|
||||
return ",".join(items)
|
||||
|
||||
|
||||
def _respect_rate_limit() -> None:
|
||||
"""确保连续请求之间至少间隔一秒。"""
|
||||
global _LAST_REQUEST_AT
|
||||
|
||||
now = time.monotonic()
|
||||
if _LAST_REQUEST_AT is not None:
|
||||
elapsed = now - _LAST_REQUEST_AT
|
||||
if elapsed < REQUEST_INTERVAL_SECONDS:
|
||||
time.sleep(REQUEST_INTERVAL_SECONDS - elapsed)
|
||||
|
||||
_LAST_REQUEST_AT = time.monotonic()
|
||||
|
||||
|
||||
def _clean_text(value: Any) -> str:
|
||||
"""压缩并清理任意文本值。"""
|
||||
return " ".join(str(value).split())
|
||||
|
||||
|
||||
def _normalize_authors(authors: Any) -> list[str]:
|
||||
"""将作者字段统一为作者姓名列表。"""
|
||||
normalized_authors: list[str] = []
|
||||
if not isinstance(authors, list):
|
||||
return normalized_authors
|
||||
|
||||
for author in authors:
|
||||
if isinstance(author, dict):
|
||||
name = author.get("name")
|
||||
if name:
|
||||
normalized_authors.append(_clean_text(name))
|
||||
elif author:
|
||||
normalized_authors.append(_clean_text(author))
|
||||
|
||||
return normalized_authors
|
||||
|
||||
|
||||
def _normalize_external_ids(external_ids: Any) -> dict[str, str]:
|
||||
"""将 externalIds 统一整理为字符串字典,并保留 arXiv 标识。"""
|
||||
normalized: dict[str, str] = {}
|
||||
if isinstance(external_ids, dict):
|
||||
for key, value in external_ids.items():
|
||||
if value is None:
|
||||
continue
|
||||
cleaned_value = _clean_text(value)
|
||||
if cleaned_value:
|
||||
normalized[str(key)] = cleaned_value
|
||||
|
||||
arxiv_id = (
|
||||
normalized.get("ArXiv") or normalized.get("arXiv") or normalized.get("arxiv")
|
||||
)
|
||||
if arxiv_id and "ArXiv" not in normalized:
|
||||
normalized["ArXiv"] = arxiv_id
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_tldr(tldr: Any) -> Any:
|
||||
"""保留 TLDR 字段原始结构,但去除明显的空字符串。"""
|
||||
if isinstance(tldr, dict):
|
||||
normalized_tldr: dict[str, Any] = {}
|
||||
for key, value in tldr.items():
|
||||
if isinstance(value, str):
|
||||
cleaned = _clean_text(value)
|
||||
if cleaned:
|
||||
normalized_tldr[key] = cleaned
|
||||
elif value is not None:
|
||||
normalized_tldr[key] = value
|
||||
return normalized_tldr or None
|
||||
return tldr
|
||||
|
||||
|
||||
def _normalize_paper(paper: dict[str, Any]) -> dict[str, Any]:
|
||||
"""将 API 返回的论文记录压缩成稳定的输出结构。"""
|
||||
return {
|
||||
"title": _clean_text(paper.get("title", "")),
|
||||
"authors": _normalize_authors(paper.get("authors")),
|
||||
"year": paper.get("year"),
|
||||
"venue": _clean_text(paper.get("venue", "")),
|
||||
"citationCount": paper.get("citationCount"),
|
||||
"externalIds": _normalize_external_ids(paper.get("externalIds")),
|
||||
"tldr": _normalize_tldr(paper.get("tldr")),
|
||||
}
|
||||
|
||||
|
||||
def _request_json(url: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""发起受限频率控制的 GET 请求并返回 JSON 对象。"""
|
||||
_respect_rate_limit()
|
||||
response = requests.get(
|
||||
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("Semantic Scholar 返回了非对象类型的 JSON")
|
||||
return payload
|
||||
|
||||
|
||||
def search_papers(
|
||||
query: str,
|
||||
max_results: int,
|
||||
fields_of_study: list[str] | None = None,
|
||||
publication_types: list[str] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""搜索 Semantic Scholar 论文并返回标准化结果。"""
|
||||
params: dict[str, Any] = {
|
||||
"query": query,
|
||||
"limit": max_results,
|
||||
"fields": REQUEST_FIELDS,
|
||||
}
|
||||
|
||||
normalized_fields_of_study = _normalize_filter_values(fields_of_study)
|
||||
if normalized_fields_of_study is not None:
|
||||
params["fieldsOfStudy"] = normalized_fields_of_study
|
||||
|
||||
normalized_publication_types = _normalize_filter_values(publication_types)
|
||||
if normalized_publication_types is not None:
|
||||
params["publicationTypes"] = normalized_publication_types
|
||||
|
||||
payload = _request_json(API_URL, params)
|
||||
papers = payload.get("data", [])
|
||||
if not isinstance(papers, list):
|
||||
raise ValueError("Semantic Scholar 响应缺少 data 列表")
|
||||
|
||||
normalized_papers: list[dict[str, Any]] = []
|
||||
for paper in papers:
|
||||
if isinstance(paper, dict):
|
||||
normalized_papers.append(_normalize_paper(paper))
|
||||
return normalized_papers
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
"""构建命令行参数解析器。"""
|
||||
parser = argparse.ArgumentParser(description="Semantic Scholar 搜索工具")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
search_parser = subparsers.add_parser("search", help="搜索论文")
|
||||
search_parser.add_argument("query", help="搜索关键词")
|
||||
search_parser.add_argument("--max", dest="max_results", type=int, default=10)
|
||||
search_parser.add_argument(
|
||||
"--fields-of-study",
|
||||
nargs="+",
|
||||
default=None,
|
||||
help="按学科领域过滤",
|
||||
)
|
||||
search_parser.add_argument(
|
||||
"--publication-types",
|
||||
nargs="+",
|
||||
default=None,
|
||||
help="按出版类型过滤",
|
||||
)
|
||||
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_papers(
|
||||
query=args.query,
|
||||
max_results=args.max_results,
|
||||
fields_of_study=args.fields_of_study,
|
||||
publication_types=args.publication_types,
|
||||
)
|
||||
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