gitinstall 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. gitinstall/__init__.py +61 -0
  2. gitinstall/_sdk.py +541 -0
  3. gitinstall/academic.py +831 -0
  4. gitinstall/admin.html +327 -0
  5. gitinstall/auto_update.py +384 -0
  6. gitinstall/autopilot.py +349 -0
  7. gitinstall/badge.py +476 -0
  8. gitinstall/checkpoint.py +330 -0
  9. gitinstall/cicd.py +499 -0
  10. gitinstall/clawhub.html +718 -0
  11. gitinstall/config_schema.py +353 -0
  12. gitinstall/db.py +984 -0
  13. gitinstall/db_backend.py +445 -0
  14. gitinstall/dep_chain.py +337 -0
  15. gitinstall/dependency_audit.py +1153 -0
  16. gitinstall/detector.py +542 -0
  17. gitinstall/doctor.py +493 -0
  18. gitinstall/education.py +869 -0
  19. gitinstall/enterprise.py +802 -0
  20. gitinstall/error_fixer.py +953 -0
  21. gitinstall/event_bus.py +251 -0
  22. gitinstall/executor.py +577 -0
  23. gitinstall/feature_flags.py +138 -0
  24. gitinstall/fetcher.py +921 -0
  25. gitinstall/huggingface.py +922 -0
  26. gitinstall/hw_detect.py +988 -0
  27. gitinstall/i18n.py +664 -0
  28. gitinstall/installer_registry.py +362 -0
  29. gitinstall/knowledge_base.py +379 -0
  30. gitinstall/license_check.py +605 -0
  31. gitinstall/llm.py +569 -0
  32. gitinstall/log.py +236 -0
  33. gitinstall/main.py +1408 -0
  34. gitinstall/mcp_agent.py +841 -0
  35. gitinstall/mcp_server.py +386 -0
  36. gitinstall/monorepo.py +810 -0
  37. gitinstall/multi_source.py +425 -0
  38. gitinstall/onboard.py +276 -0
  39. gitinstall/planner.py +222 -0
  40. gitinstall/planner_helpers.py +323 -0
  41. gitinstall/planner_known_projects.py +1010 -0
  42. gitinstall/planner_templates.py +996 -0
  43. gitinstall/remote_gpu.py +633 -0
  44. gitinstall/resilience.py +608 -0
  45. gitinstall/run_tests.py +572 -0
  46. gitinstall/skills.py +476 -0
  47. gitinstall/tool_schemas.py +324 -0
  48. gitinstall/trending.py +279 -0
  49. gitinstall/uninstaller.py +415 -0
  50. gitinstall/validate_top100.py +607 -0
  51. gitinstall/watchdog.py +180 -0
  52. gitinstall/web.py +1277 -0
  53. gitinstall/web_ui.html +2277 -0
  54. gitinstall-1.1.0.dist-info/METADATA +275 -0
  55. gitinstall-1.1.0.dist-info/RECORD +59 -0
  56. gitinstall-1.1.0.dist-info/WHEEL +5 -0
  57. gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
  58. gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
  59. gitinstall-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ validate_top100.py - GitHub Top 100 持续兼容性验证
4
+ ===================================================
5
+
6
+ 持续验证 github-installer 对 GitHub Top 100 热门项目的安装兼容性。
7
+ 每次运行自动爬取最新排名,检测新入榜项目,生成兼容性报告。
8
+
9
+ 功能:
10
+ 1. 爬取 GitHub Top 100 热门开源项目
11
+ 2. 对每个项目生成安装计划(macOS/Linux/Windows 三平台)
12
+ 3. 验证计划的安全性与合理性
13
+ 4. 检测排名变动,标记新入榜项目并优先验证
14
+ 5. 输出覆盖率报告(JSON + 终端可读)
15
+
16
+ 用法:
17
+ python tools/validate_top100.py # 完整验证
18
+ python tools/validate_top100.py --quick # 仅验证新入榜项目
19
+ python tools/validate_top100.py --report # 仅查看上次报告
20
+ python tools/validate_top100.py --category AI # 验证指定分类
21
+
22
+ 或通过 main.py 子命令:
23
+ python tools/main.py validate # 完整验证
24
+ python tools/main.py validate --quick # 仅验证新入榜
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import json
31
+ import os
32
+ import sys
33
+ import time
34
+ import io
35
+ import contextlib
36
+ from dataclasses import dataclass, field, asdict
37
+ from datetime import datetime, timezone
38
+ from pathlib import Path
39
+ from typing import Any
40
+
41
+ # ── 路径设置 ──
42
+ _THIS_DIR = Path(__file__).resolve().parent
43
+ _ROOT_DIR = _THIS_DIR.parent
44
+ _RESULTS_DIR = _ROOT_DIR / "tests" / "results"
45
+ _RESULTS_DIR.mkdir(parents=True, exist_ok=True)
46
+
47
+ sys.path.insert(0, str(_THIS_DIR))
48
+
49
+ from detector import EnvironmentDetector
50
+ from fetcher import fetch_project
51
+ from planner import SmartPlanner
52
+
53
+ # ── 颜色 ──
54
+ G = "\033[32m"; R = "\033[31m"; Y = "\033[33m"; C = "\033[36m"
55
+ BD = "\033[1m"; DM = "\033[2m"; RS = "\033[0m"
56
+
57
+ # ── 报告路径 ──
58
+ _REPORT_FILE = _RESULTS_DIR / "top100_report.json"
59
+ _HISTORY_FILE = _RESULTS_DIR / "top100_history.json"
60
+
61
+ # ── 三平台模拟环境 ──
62
+ def _make_env(os_type: str, arch: str = "x86_64",
63
+ gpu_type: str = "none", chip: str = "") -> dict:
64
+ return {
65
+ "os": {
66
+ "type": os_type,
67
+ "version": "14.0" if os_type == "macos" else "22.04" if os_type == "linux" else "11",
68
+ "arch": arch, "chip": chip,
69
+ },
70
+ "hardware": {"cpu_count": 8, "memory_gb": 32},
71
+ "gpu": {
72
+ "type": gpu_type,
73
+ "name": "NVIDIA RTX 4090" if gpu_type == "nvidia"
74
+ else "Apple M3" if gpu_type == "apple_mps" else "",
75
+ "cuda_version": "12.1" if gpu_type == "nvidia" else "",
76
+ },
77
+ "runtimes": {
78
+ "python": {"available": True, "version": "3.11.0"},
79
+ "node": {"available": True, "version": "20.0.0"},
80
+ },
81
+ "package_managers": {
82
+ "pip": {"available": True},
83
+ "npm": {"available": True},
84
+ "brew": {"available": os_type == "macos"},
85
+ "apt": {"available": os_type == "linux"},
86
+ "winget": {"available": os_type == "windows"},
87
+ "cargo": {"available": True},
88
+ },
89
+ "disk": {"free_gb": 100},
90
+ }
91
+
92
+ PLATFORMS = {
93
+ "macOS-ARM": _make_env("macos", "arm64", "apple_mps", "Apple M3 Ultra"),
94
+ "Linux-CUDA": _make_env("linux", "x86_64", "nvidia"),
95
+ "Windows-CPU": _make_env("windows", "x86_64", "none"),
96
+ }
97
+
98
+ # ── 安全验证 ──
99
+ _DANGEROUS_PATTERNS = ["rm -rf /", "mkfs", "dd if=", ":(){", "fork bomb",
100
+ "chmod 777 /", "> /dev/sd"]
101
+
102
+ def _validate_steps(steps: list[dict], os_type: str) -> list[str]:
103
+ """验证安装步骤的合理性,返回问题列表"""
104
+ issues = []
105
+ for i, s in enumerate(steps):
106
+ cmd = s.get("command", "")
107
+ if not cmd:
108
+ continue
109
+ for danger in _DANGEROUS_PATTERNS:
110
+ if danger in cmd:
111
+ issues.append(f"Step {i}: 危险命令 '{danger}'")
112
+ if os_type == "windows":
113
+ if cmd.startswith("sudo "):
114
+ issues.append(f"Step {i}: Windows 不应有 sudo")
115
+ if "apt " in cmd or "apt-get " in cmd:
116
+ issues.append(f"Step {i}: Windows 不应有 apt")
117
+ if "brew " in cmd:
118
+ issues.append(f"Step {i}: Windows 不应有 brew")
119
+ elif os_type == "macos":
120
+ if "apt " in cmd or "apt-get " in cmd:
121
+ issues.append(f"Step {i}: macOS 不应有 apt")
122
+ if "winget " in cmd or "choco " in cmd:
123
+ issues.append(f"Step {i}: macOS 不应有 winget/choco")
124
+ elif os_type == "linux":
125
+ if "winget " in cmd or "choco " in cmd:
126
+ issues.append(f"Step {i}: Linux 不应有 winget/choco")
127
+ return issues
128
+
129
+
130
+ # ── 数据结构 ──
131
+ @dataclass
132
+ class ProjectResult:
133
+ repo: str
134
+ name: str
135
+ stars: int
136
+ language: str
137
+ tag: str
138
+ platforms: dict = field(default_factory=dict) # platform → PlatformResult
139
+ fetch_ok: bool = True
140
+ fetch_error: str = ""
141
+ is_new: bool = False # 新入榜标记
142
+
143
+ @property
144
+ def all_pass(self) -> bool:
145
+ return self.fetch_ok and all(
146
+ p.get("pass", False) for p in self.platforms.values()
147
+ )
148
+
149
+ @property
150
+ def pass_count(self) -> int:
151
+ return sum(1 for p in self.platforms.values() if p.get("pass", False))
152
+
153
+
154
+ # ═══════════════════════════════════════════════
155
+ # 核心流程
156
+ # ═══════════════════════════════════════════════
157
+
158
+ def crawl_top100() -> list[dict]:
159
+ """
160
+ 爬取 GitHub Top 100 热门项目。
161
+ 复用 trending.py 的 _fetch_all() 逻辑,但不缓存(每次实时爬取)。
162
+ """
163
+ from trending import _fetch_all
164
+ print(f"\n{BD}📡 正在爬取 GitHub Top 100 热门项目...{RS}", flush=True)
165
+ projects = _fetch_all()
166
+ print(f" 获取到 {len(projects)} 个项目", flush=True)
167
+ return projects
168
+
169
+
170
+ def load_history() -> dict:
171
+ """加载上次验证的项目列表(用于增量检测)"""
172
+ if _HISTORY_FILE.exists():
173
+ try:
174
+ return json.loads(_HISTORY_FILE.read_text("utf-8"))
175
+ except Exception:
176
+ pass
177
+ return {"repos": [], "last_run": None}
178
+
179
+
180
+ def save_history(repos: list[str]):
181
+ """保存本次验证的项目列表"""
182
+ _HISTORY_FILE.write_text(json.dumps({
183
+ "repos": repos,
184
+ "last_run": datetime.now(timezone.utc).isoformat(),
185
+ }, ensure_ascii=False, indent=2), "utf-8")
186
+
187
+
188
+ def detect_new_projects(current: list[dict], history: dict) -> set[str]:
189
+ """检测新入榜项目"""
190
+ old_repos = set(r.lower() for r in history.get("repos", []))
191
+ new_repos = set()
192
+ for p in current:
193
+ repo = p["repo"].lower()
194
+ if repo not in old_repos:
195
+ new_repos.add(repo)
196
+ return new_repos
197
+
198
+
199
+ def validate_project(
200
+ repo: str,
201
+ planner: SmartPlanner,
202
+ project_info=None,
203
+ ) -> ProjectResult:
204
+ """
205
+ 对单个项目执行三平台验证。
206
+
207
+ Parameters:
208
+ repo: 如 "owner/repo"
209
+ planner: SmartPlanner 实例
210
+ project_info: 已获取的项目信息(可选,避免重复 fetch)
211
+
212
+ Returns:
213
+ ProjectResult (fetch_ok=False 且 fetch_error 以 "RATELIMIT:" 开头
214
+ 表示遇到限速,调用方可据此处理)
215
+ """
216
+ result = ProjectResult(
217
+ repo=repo, name="", stars=0, language="", tag=""
218
+ )
219
+
220
+ # 1. 获取项目信息
221
+ if project_info is None:
222
+ try:
223
+ project_info = fetch_project(repo)
224
+ except PermissionError:
225
+ # 限速专用标记,让调用方可识别
226
+ result.fetch_ok = False
227
+ result.fetch_error = "RATELIMIT: GitHub API 频率超限"
228
+ return result
229
+ except Exception as e:
230
+ result.fetch_ok = False
231
+ result.fetch_error = str(e)[:200]
232
+ return result
233
+
234
+ result.name = project_info.repo
235
+ result.language = project_info.language or ""
236
+ result.stars = project_info.stars or 0
237
+
238
+ # 2. 三平台验证
239
+ for plat_name, env in PLATFORMS.items():
240
+ os_type = env["os"]["type"]
241
+ stderr_buf = io.StringIO()
242
+ try:
243
+ with contextlib.redirect_stderr(stderr_buf):
244
+ plan = planner.generate_plan(
245
+ owner=project_info.owner,
246
+ repo=project_info.repo,
247
+ env=env,
248
+ project_types=project_info.project_type,
249
+ dependency_files=project_info.dependency_files,
250
+ readme=project_info.readme,
251
+ )
252
+ except Exception as e:
253
+ result.platforms[plat_name] = {
254
+ "pass": False,
255
+ "error": str(e)[:200],
256
+ "steps": 0,
257
+ "confidence": "error",
258
+ "strategy": "",
259
+ }
260
+ continue
261
+
262
+ steps = plan.get("steps", [])
263
+ confidence = plan.get("confidence", "unknown")
264
+ strategy = plan.get("strategy", "unknown")
265
+ issues = _validate_steps(steps, os_type)
266
+
267
+ is_pass = bool(steps) and not issues
268
+ result.platforms[plat_name] = {
269
+ "pass": is_pass,
270
+ "steps": len(steps),
271
+ "confidence": confidence,
272
+ "strategy": strategy,
273
+ "issues": issues if issues else [],
274
+ "has_launch": bool(plan.get("launch_command")),
275
+ }
276
+
277
+ return result
278
+
279
+
280
+ def run_validation(
281
+ projects: list[dict],
282
+ new_repos: set[str],
283
+ quick_mode: bool = False,
284
+ category_filter: str = None,
285
+ ) -> list[ProjectResult]:
286
+ """
287
+ 执行完整验证流程。
288
+
289
+ Parameters:
290
+ projects: 从 crawl_top100() 获取的项目列表
291
+ new_repos: 新入榜项目 repo 集合(小写)
292
+ quick_mode: True 时仅验证新入榜项目
293
+ category_filter: 按标签过滤(如 "AI", "Web")
294
+ """
295
+ planner = SmartPlanner()
296
+ results = []
297
+
298
+ # 过滤
299
+ targets = projects
300
+ if category_filter:
301
+ targets = [p for p in targets if p.get("tag", "").lower() == category_filter.lower()]
302
+ if quick_mode:
303
+ targets = [p for p in targets if p["repo"].lower() in new_repos]
304
+
305
+ total = len(targets)
306
+ if total == 0:
307
+ print(f"\n{Y}没有需要验证的项目{RS}")
308
+ if quick_mode:
309
+ print(f" (quick 模式:没有新入榜项目)")
310
+ return results
311
+
312
+ print(f"\n{BD}🔍 开始验证 {total} 个项目(三平台 × 每个项目){RS}\n", flush=True)
313
+
314
+ rate_limited_repos = [] # 因限速而跳过的项目
315
+
316
+ for idx, proj in enumerate(targets, 1):
317
+ repo = proj["repo"]
318
+ tag = proj.get("tag", "")
319
+ stars = proj.get("stars", "?")
320
+ is_new = repo.lower() in new_repos
321
+ new_badge = f" {Y}[NEW]{RS}" if is_new else ""
322
+
323
+ print(f"[{idx}/{total}] {BD}{repo}{RS} ⭐{stars} #{tag}{new_badge}", flush=True)
324
+
325
+ result = validate_project(repo, planner)
326
+ result.tag = tag
327
+ result.is_new = is_new
328
+
329
+ # 遇到限速:记录并停止继续请求(避免浪费时间)
330
+ if not result.fetch_ok and result.fetch_error.startswith("RATELIMIT:"):
331
+ rate_limited_repos.append(repo)
332
+ if len(rate_limited_repos) == 1:
333
+ print(f"\n {Y}⚠️ GitHub API 频率超限,后续未缓存的项目将跳过{RS}")
334
+ print(f" {DM}提示: 设置 GITHUB_TOKEN 环境变量可获得 5000 次/小时{RS}")
335
+ print(f" {DM}⏭ 跳过 {repo}(限速){RS}")
336
+ results.append(result)
337
+ continue
338
+
339
+ # 尝试从 crawl 数据填充 stars
340
+ if result.stars == 0 and proj.get("_stars_num"):
341
+ result.stars = proj["_stars_num"]
342
+
343
+ # 输出单项结果
344
+ if not result.fetch_ok:
345
+ print(f" {R}❌ 获取失败: {result.fetch_error[:80]}{RS}")
346
+ else:
347
+ for plat, pr in result.platforms.items():
348
+ icon = f"{G}✅{RS}" if pr["pass"] else f"{R}❌{RS}"
349
+ conf = pr["confidence"]
350
+ strat = pr["strategy"]
351
+ n_steps = pr["steps"]
352
+ print(f" {icon} {plat:14s} steps={n_steps} "
353
+ f"conf={conf:6s} strategy={strat}")
354
+ if pr.get("issues"):
355
+ for iss in pr["issues"]:
356
+ print(f" {R}⚠ {iss}{RS}")
357
+
358
+ results.append(result)
359
+
360
+ # GitHub API 限速
361
+ if idx < total:
362
+ time.sleep(1.5)
363
+
364
+ if rate_limited_repos:
365
+ print(f"\n{Y}⚠️ 因 API 限速跳过了 {len(rate_limited_repos)} 个项目{RS}")
366
+ print(f" 下次运行时已成功获取的项目会使用缓存,无需重新请求")
367
+ print(f" 建议设置: export GITHUB_TOKEN=ghp_xxxx")
368
+
369
+ return results
370
+
371
+
372
+ # ═══════════════════════════════════════════════
373
+ # 报告生成
374
+ # ═══════════════════════════════════════════════
375
+
376
+ def generate_report(results: list[ProjectResult], new_repos: set[str]) -> dict:
377
+ """生成结构化兼容性报告"""
378
+ total_projects = len(results)
379
+ total_tests = sum(len(r.platforms) for r in results)
380
+ passed_tests = sum(r.pass_count for r in results)
381
+ all_pass_projects = sum(1 for r in results if r.all_pass)
382
+ fetch_fail = sum(1 for r in results if not r.fetch_ok)
383
+
384
+ # 按置信度统计
385
+ confidence_breakdown = {"high": 0, "medium": 0, "low": 0, "error": 0}
386
+ for r in results:
387
+ for pr in r.platforms.values():
388
+ conf = pr.get("confidence", "error")
389
+ if conf in confidence_breakdown:
390
+ confidence_breakdown[conf] += 1
391
+ else:
392
+ confidence_breakdown["error"] += 1
393
+
394
+ # 新入榜项目验证
395
+ new_results = [r for r in results if r.is_new]
396
+ new_pass = sum(1 for r in new_results if r.all_pass)
397
+
398
+ # 失败项目列表
399
+ failed_projects = []
400
+ for r in results:
401
+ if not r.all_pass:
402
+ failed_plats = [
403
+ {"platform": p, **info}
404
+ for p, info in r.platforms.items()
405
+ if not info.get("pass", False)
406
+ ]
407
+ failed_projects.append({
408
+ "repo": r.repo,
409
+ "is_new": r.is_new,
410
+ "fetch_ok": r.fetch_ok,
411
+ "fetch_error": r.fetch_error,
412
+ "failed_platforms": failed_plats,
413
+ })
414
+
415
+ report = {
416
+ "timestamp": datetime.now(timezone.utc).isoformat(),
417
+ "summary": {
418
+ "total_projects": total_projects,
419
+ "total_tests": total_tests,
420
+ "passed_tests": passed_tests,
421
+ "pass_rate": round(passed_tests / total_tests * 100, 1) if total_tests else 0,
422
+ "all_pass_projects": all_pass_projects,
423
+ "project_pass_rate": round(all_pass_projects / total_projects * 100, 1) if total_projects else 0,
424
+ "fetch_failures": fetch_fail,
425
+ "new_projects": len(new_results),
426
+ "new_projects_pass": new_pass,
427
+ },
428
+ "confidence_breakdown": confidence_breakdown,
429
+ "failed_projects": failed_projects,
430
+ "all_results": [
431
+ {
432
+ "repo": r.repo,
433
+ "name": r.name,
434
+ "stars": r.stars,
435
+ "language": r.language,
436
+ "tag": r.tag,
437
+ "is_new": r.is_new,
438
+ "all_pass": r.all_pass,
439
+ "platforms": r.platforms,
440
+ }
441
+ for r in results
442
+ ],
443
+ }
444
+
445
+ return report
446
+
447
+
448
+ def save_report(report: dict):
449
+ """保存报告到 JSON 文件"""
450
+ _REPORT_FILE.write_text(
451
+ json.dumps(report, ensure_ascii=False, indent=2), "utf-8"
452
+ )
453
+ print(f"\n📄 报告已保存: {_REPORT_FILE}")
454
+
455
+
456
+ def print_report(report: dict):
457
+ """终端友好的报告输出"""
458
+ s = report["summary"]
459
+ cb = report["confidence_breakdown"]
460
+
461
+ print(f"\n{'═' * 60}")
462
+ print(f"{BD} GitHub Top 100 兼容性验证报告{RS}")
463
+ print(f" {DM}{report['timestamp']}{RS}")
464
+ print(f"{'═' * 60}")
465
+
466
+ # 核心指标
467
+ rate_color = G if s["pass_rate"] >= 90 else Y if s["pass_rate"] >= 70 else R
468
+ print(f"\n 📊 总体覆盖率: {rate_color}{BD}{s['pass_rate']}%{RS}"
469
+ f" ({s['passed_tests']}/{s['total_tests']} 测试通过)")
470
+ print(f" 📦 项目通过率: {rate_color}{BD}{s['project_pass_rate']}%{RS}"
471
+ f" ({s['all_pass_projects']}/{s['total_projects']} 全平台通过)")
472
+
473
+ if s["fetch_failures"]:
474
+ print(f" ⚠️ 获取失败: {R}{s['fetch_failures']}{RS} 个项目")
475
+
476
+ # 置信度分布
477
+ total_conf = sum(cb.values())
478
+ if total_conf:
479
+ print(f"\n 🎯 置信度分布:")
480
+ print(f" {G}high{RS}: {cb['high']:3d} "
481
+ f"({cb['high']/total_conf*100:.0f}%)")
482
+ print(f" {Y}medium{RS}: {cb['medium']:3d} "
483
+ f"({cb['medium']/total_conf*100:.0f}%)")
484
+ print(f" {R}low{RS}: {cb['low']:3d} "
485
+ f"({cb['low']/total_conf*100:.0f}%)")
486
+ if cb["error"]:
487
+ print(f" error: {cb['error']:3d}")
488
+
489
+ # 新入榜项目
490
+ if s["new_projects"]:
491
+ new_icon = G if s["new_projects_pass"] == s["new_projects"] else Y
492
+ print(f"\n 🆕 新入榜项目: {s['new_projects']} 个 "
493
+ f"({new_icon}{s['new_projects_pass']}/{s['new_projects']} 通过{RS})")
494
+
495
+ # 失败列表
496
+ failed = report.get("failed_projects", [])
497
+ if failed:
498
+ print(f"\n ❌ 失败项目 ({len(failed)}):")
499
+ for fp in failed[:20]: # 最多显示 20 个
500
+ new_tag = f" {Y}[NEW]{RS}" if fp["is_new"] else ""
501
+ if not fp["fetch_ok"]:
502
+ print(f" • {fp['repo']}{new_tag} — 获取失败: {fp['fetch_error'][:60]}")
503
+ else:
504
+ plats = ", ".join(p["platform"] for p in fp["failed_platforms"])
505
+ print(f" • {fp['repo']}{new_tag} — 失败平台: {plats}")
506
+
507
+ print(f"\n{'═' * 60}\n")
508
+
509
+
510
+ def show_last_report():
511
+ """显示上次验证报告"""
512
+ if not _REPORT_FILE.exists():
513
+ print(f"{Y}没有找到历史报告。请先运行: python tools/validate_top100.py{RS}")
514
+ return
515
+ report = json.loads(_REPORT_FILE.read_text("utf-8"))
516
+ print_report(report)
517
+
518
+
519
+ # ═══════════════════════════════════════════════
520
+ # CLI 入口
521
+ # ═══════════════════════════════════════════════
522
+
523
+ def cmd_validate(quick: bool = False, report_only: bool = False,
524
+ category: str = None) -> dict:
525
+ """
526
+ validate 子命令入口(供 main.py 调用)。
527
+
528
+ Returns:
529
+ dict 格式的报告摘要
530
+ """
531
+ if report_only:
532
+ show_last_report()
533
+ return {"status": "ok", "action": "report_shown"}
534
+
535
+ # 1. 爬取 Top 100
536
+ projects = crawl_top100()
537
+ if not projects:
538
+ return {"status": "error", "message": "爬取失败,请检查网络"}
539
+
540
+ # 2. 增量检测
541
+ history = load_history()
542
+ new_repos = detect_new_projects(projects, history)
543
+
544
+ if new_repos:
545
+ print(f"\n{Y}🆕 发现 {len(new_repos)} 个新入榜项目:{RS}")
546
+ for nr in sorted(new_repos)[:10]:
547
+ print(f" • {nr}")
548
+ if len(new_repos) > 10:
549
+ print(f" ...及其他 {len(new_repos) - 10} 个")
550
+
551
+ # 3. 执行验证
552
+ t0 = time.time()
553
+ results = run_validation(projects, new_repos,
554
+ quick_mode=quick, category_filter=category)
555
+ elapsed = time.time() - t0
556
+
557
+ if not results:
558
+ return {"status": "ok", "message": "没有需要验证的项目"}
559
+
560
+ # 4. 生成报告
561
+ report = generate_report(results, new_repos)
562
+ report["elapsed_seconds"] = round(elapsed, 1)
563
+ save_report(report)
564
+ print_report(report)
565
+
566
+ # 5. 保存历史(用于下次增量检测)
567
+ all_repos = [p["repo"] for p in projects]
568
+ save_history(all_repos)
569
+
570
+ print(f"{DM}验证完成,耗时 {elapsed:.1f}s{RS}")
571
+
572
+ return {
573
+ "status": "ok",
574
+ "summary": report["summary"],
575
+ "elapsed": round(elapsed, 1),
576
+ }
577
+
578
+
579
+ def main():
580
+ parser = argparse.ArgumentParser(
581
+ description="GitHub Top 100 持续兼容性验证",
582
+ formatter_class=argparse.RawDescriptionHelpFormatter,
583
+ epilog="""
584
+ 示例:
585
+ python tools/validate_top100.py # 完整验证
586
+ python tools/validate_top100.py --quick # 仅验证新入榜
587
+ python tools/validate_top100.py --report # 查看上次报告
588
+ python tools/validate_top100.py --category AI # 验证 AI 分类
589
+ """)
590
+ parser.add_argument("--quick", action="store_true",
591
+ help="仅验证新入榜项目(增量模式)")
592
+ parser.add_argument("--report", action="store_true",
593
+ help="仅显示上次验证报告")
594
+ parser.add_argument("--category", default=None,
595
+ help="按分类过滤: AI/Web/工具/IoT")
596
+ args = parser.parse_args()
597
+
598
+ result = cmd_validate(
599
+ quick=args.quick,
600
+ report_only=args.report,
601
+ category=args.category,
602
+ )
603
+ print(json.dumps(result, ensure_ascii=False, indent=2))
604
+
605
+
606
+ if __name__ == "__main__":
607
+ main()