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.
- gitinstall/__init__.py +61 -0
- gitinstall/_sdk.py +541 -0
- gitinstall/academic.py +831 -0
- gitinstall/admin.html +327 -0
- gitinstall/auto_update.py +384 -0
- gitinstall/autopilot.py +349 -0
- gitinstall/badge.py +476 -0
- gitinstall/checkpoint.py +330 -0
- gitinstall/cicd.py +499 -0
- gitinstall/clawhub.html +718 -0
- gitinstall/config_schema.py +353 -0
- gitinstall/db.py +984 -0
- gitinstall/db_backend.py +445 -0
- gitinstall/dep_chain.py +337 -0
- gitinstall/dependency_audit.py +1153 -0
- gitinstall/detector.py +542 -0
- gitinstall/doctor.py +493 -0
- gitinstall/education.py +869 -0
- gitinstall/enterprise.py +802 -0
- gitinstall/error_fixer.py +953 -0
- gitinstall/event_bus.py +251 -0
- gitinstall/executor.py +577 -0
- gitinstall/feature_flags.py +138 -0
- gitinstall/fetcher.py +921 -0
- gitinstall/huggingface.py +922 -0
- gitinstall/hw_detect.py +988 -0
- gitinstall/i18n.py +664 -0
- gitinstall/installer_registry.py +362 -0
- gitinstall/knowledge_base.py +379 -0
- gitinstall/license_check.py +605 -0
- gitinstall/llm.py +569 -0
- gitinstall/log.py +236 -0
- gitinstall/main.py +1408 -0
- gitinstall/mcp_agent.py +841 -0
- gitinstall/mcp_server.py +386 -0
- gitinstall/monorepo.py +810 -0
- gitinstall/multi_source.py +425 -0
- gitinstall/onboard.py +276 -0
- gitinstall/planner.py +222 -0
- gitinstall/planner_helpers.py +323 -0
- gitinstall/planner_known_projects.py +1010 -0
- gitinstall/planner_templates.py +996 -0
- gitinstall/remote_gpu.py +633 -0
- gitinstall/resilience.py +608 -0
- gitinstall/run_tests.py +572 -0
- gitinstall/skills.py +476 -0
- gitinstall/tool_schemas.py +324 -0
- gitinstall/trending.py +279 -0
- gitinstall/uninstaller.py +415 -0
- gitinstall/validate_top100.py +607 -0
- gitinstall/watchdog.py +180 -0
- gitinstall/web.py +1277 -0
- gitinstall/web_ui.html +2277 -0
- gitinstall-1.1.0.dist-info/METADATA +275 -0
- gitinstall-1.1.0.dist-info/RECORD +59 -0
- gitinstall-1.1.0.dist-info/WHEEL +5 -0
- gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
- gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
- 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()
|