gitlink-cli/scripts/research/profile.py

206 lines
8.4 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.

"""profile.py — 主体画像Pillar 2
模式(三选一):
--login <user> 学者画像(主题向量/语言/活跃度/协作开放度 + 基本信息)
--owner O --repo R 项目画像(仓库元数据/贡献者/主题/语言/研究维度评分)
--category <域> --top N 领域核心学者(从热点榜 core_scholars 取 top-N 逐个画像)
产物profile.json + report.md画像卡片
复用match.profile_candidate / collect.user_info/user_repos/contributors/repo_info / topics。
"""
from __future__ import annotations
import argparse
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import collect as c
import topics as T
import match as M # 复用 profile_candidate
import hotspot as H # --category 模式取 core_scholars
_BOT_HINTS = ("bot", "i-robot", "dependabot", "renovate", "semantic-release-bot")
def _is_bot(login: str) -> bool:
low = login.lower()
return any(h in low for h in _BOT_HINTS)
# ---------------------------------------------------------------------------
# 学者画像
# ---------------------------------------------------------------------------
def scholar_profile(login: str) -> dict:
"""学者画像:复用 match.profile_candidatetopic_vec/langs/activity/collab+ user_info。"""
info = c.user_info(login) or {}
prof = M.profile_candidate(login, {})
topic_vec = prof.get("topic_vec", {}) or {}
topics_top = sorted(topic_vec.items(), key=lambda x: -x[1])[:8]
return {
"type": "scholar",
"login": login,
"name": info.get("name") or info.get("username") or login,
"bio": info.get("bio") or info.get("description") or "",
"topic_vec": topic_vec,
"top_topics": [{"topic": t, "count": n} for t, n in topics_top],
"languages": prof.get("langs", []),
"activity": round(prof.get("activity", 0), 2),
"collab": round(prof.get("collab", 0), 2),
"repo_count": prof.get("repo_count", 0),
}
# ---------------------------------------------------------------------------
# 项目画像
# ---------------------------------------------------------------------------
def project_profile(owner: str, repo: str) -> dict:
"""项目画像:仓库元数据 + 贡献者 + 主题/语言 + 研究维度评分040"""
info = c.repo_info(owner, repo) or {}
contribs = c.contributors(owner, repo) or []
readme = (c.readme(owner, repo) or "")[:4000]
langs = c.languages(owner, repo) or {}
desc = info.get("description") or ""
tc = T.topic_counter([desc, readme])
topics_top = [{"topic": t, "count": n} for t, n in tc.most_common(8)]
stars = c.as_int(info.get("praises_count") or info.get("watchers_count") or 0)
forks = c.as_int(info.get("forked_count") or 0)
visits = c.as_int(info.get("visits") or 0)
score = {
"doc": 5 if readme else 0,
"license": 5 if info.get("license") else 0,
"collab": min(len(contribs), 10) * 1, # 贡献者封顶10
"impact": min(stars + forks, 10) * 2, # star+fork封顶20
}
logins = [c.login_of(x) for x in contribs]
logins = [l for l in logins if l and not _is_bot(l)][:8]
return {
"type": "project",
"repo": f"{owner}/{repo}",
"name": info.get("name") or repo,
"description": desc,
"topics": topics_top,
"languages": (list(langs.keys()) if isinstance(langs, dict) else list(langs))[:8],
"stars": stars, "forks": forks, "visits": visits,
"contributors_count": len(contribs),
"top_contributors": logins,
"score": score,
"score_total": sum(score.values()),
}
# ---------------------------------------------------------------------------
# 领域核心学者
# ---------------------------------------------------------------------------
def category_scholars(category: str, top: int = 5, limit: int = 20) -> list[dict]:
"""领域核心学者:热点榜 core_scholars 取 top-N逐个画像。"""
raw = H.collect(category=category, limit=limit)
scholars = (H.compute(raw).get("core_scholars") or [])[:top]
out = []
for s in scholars:
login = s.get("login")
if not login:
continue
prof = scholar_profile(login)
prof["category_repos"] = s.get("repo_count")
out.append(prof)
return out
# ---------------------------------------------------------------------------
# 报告
# ---------------------------------------------------------------------------
def render_report(result: dict) -> str:
lines = ["# 🪪 主体画像报告", ""]
def _scholar_card(p: dict, depth: int = 2):
h = "#" * depth
lines.append(f"{h} 👤 {p.get('name')} (`{p.get('login')}`)")
if p.get("bio"):
lines.append(f"> {p['bio'][:120]}")
lines.append("")
lines.append(f"- 活跃度 **{p.get('activity')}** · 协作开放度 **{p.get('collab')}** · 公开仓库 **{p.get('repo_count')}**")
if p.get("category_repos") is not None:
lines.append(f"- 该领域关联仓库 **{p.get('category_repos')}**")
if p.get("top_topics"):
lines.append("- 研究主题:" + "".join(f"`{t['topic']}`({t['count']})" for t in p["top_topics"][:6]))
if p.get("languages"):
lines.append("- 语言:" + "".join(p["languages"][:6]))
lines.append("")
def _project_card(p: dict):
lines.append(f"## 📦 {p.get('name')} (`{p.get('repo')}`)")
if p.get("description"):
lines.append(f"> {p['description'][:140]}")
lines.append("")
lines.append(f"- ★{p.get('stars')}{p.get('forks')} 👁{p.get('visits')} · 贡献者 {p.get('contributors_count')} · 研究维度评分 **{p.get('score_total')}/40**")
if p.get("topics"):
lines.append("- 主题:" + "".join(f"`{t['topic']}`" for t in p["topics"][:6]))
if p.get("top_contributors"):
lines.append("- 核心贡献者:" + "".join(f"`{l}`" for l in p["top_contributors"][:6]))
lines.append("")
profiles = result.get("profiles") or []
if result.get("mode") == "project":
_project_card(profiles[0] if profiles else {})
else:
for p in profiles:
_scholar_card(p)
lines.append("---\n*由 gitlink-research-profile 生成*")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
ap = argparse.ArgumentParser(description="主体画像 — 学者 / 项目 / 领域核心学者")
ap.add_argument("--login", default="", help="学者 login学者画像")
ap.add_argument("--owner", default="", help="仓库 owner项目画像")
ap.add_argument("--repo", default="", help="仓库名(项目画像)")
ap.add_argument("--category", default="", help="领域分类(领域核心学者)")
ap.add_argument("--top", type=int, default=5, help="领域模式取 top-N 学者(默认 5")
ap.add_argument("--limit", type=int, default=20, help="领域模式热点榜上限(默认 20")
ap.add_argument("--out", "-o", default="", help="输出目录(不传则打印 JSON")
args = ap.parse_args()
if args.login:
profiles = [scholar_profile(args.login)]
mode = "scholar"
elif args.owner and args.repo:
profiles = [project_profile(args.owner, args.repo)]
mode = "project"
elif args.category:
profiles = category_scholars(args.category, top=args.top, limit=args.limit)
mode = "category_scholars"
else:
print(json.dumps({"ok": False, "error": "need --login OR --owner/--repo OR --category"},
ensure_ascii=False))
sys.exit(1)
result = {"scenario": "profile", "mode": mode, "profiles": profiles}
json_text = json.dumps(result, ensure_ascii=False, indent=2)
if args.out:
os.makedirs(args.out, exist_ok=True)
with open(os.path.join(args.out, "profile.json"), "w", encoding="utf-8") as f:
f.write(json_text)
with open(os.path.join(args.out, "report.md"), "w", encoding="utf-8") as f:
f.write(render_report(result))
sys.stderr.write(f"[profile] ✓ mode={mode} 产物落 {args.out}\n")
else:
print(json_text)
if __name__ == "__main__":
main()