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
gitinstall/detector.py
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"""
|
|
2
|
+
detector.py - 全平台环境检测器
|
|
3
|
+
=====================================
|
|
4
|
+
|
|
5
|
+
覆盖:
|
|
6
|
+
- macOS (Intel / Apple Silicon M1-M4)
|
|
7
|
+
- Linux (Ubuntu/Debian/Arch/Fedora/openSUSE)
|
|
8
|
+
- Windows 10/11
|
|
9
|
+
- Windows WSL2
|
|
10
|
+
|
|
11
|
+
检测内容:
|
|
12
|
+
- OS + 发行版 + 版本 + 架构
|
|
13
|
+
- CPU + 内存 + 磁盘空间
|
|
14
|
+
- GPU (NVIDIA CUDA / AMD ROCm / Apple MPS)
|
|
15
|
+
- 已安装的包管理器
|
|
16
|
+
- 已安装的运行时 (Python/Node/Go/Rust/Docker/Git)
|
|
17
|
+
- 已配置的 LLM 环境变量
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import platform
|
|
25
|
+
import re
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Optional
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ─────────────────────────────────────────────
|
|
34
|
+
# 工具函数
|
|
35
|
+
# ─────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
def _run(cmd: list[str], timeout: int = 5) -> Optional[str]:
|
|
38
|
+
"""运行命令,返回 stdout 或 None(失败时不抛异常)"""
|
|
39
|
+
try:
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
cmd,
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
timeout=timeout,
|
|
45
|
+
# Windows 安全:不通过 shell 执行
|
|
46
|
+
)
|
|
47
|
+
return result.stdout.strip() if result.returncode == 0 else None
|
|
48
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError):
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _which(binary: str) -> Optional[str]:
|
|
53
|
+
"""查找可执行文件路径"""
|
|
54
|
+
return shutil.which(binary)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _version(binary: str, version_flag: str = "--version") -> Optional[str]:
|
|
58
|
+
"""获取工具版本号"""
|
|
59
|
+
if not _which(binary):
|
|
60
|
+
return None
|
|
61
|
+
output = _run([binary, version_flag])
|
|
62
|
+
if not output:
|
|
63
|
+
return None
|
|
64
|
+
# 提取第一行的版本号
|
|
65
|
+
first_line = output.split("\n")[0]
|
|
66
|
+
match = re.search(r'[\d]+\.[\d]+(?:\.[\d]+)?', first_line)
|
|
67
|
+
return match.group(0) if match else first_line[:50]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ─────────────────────────────────────────────
|
|
71
|
+
# 环境检测主类
|
|
72
|
+
# ─────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
class EnvironmentDetector:
|
|
75
|
+
|
|
76
|
+
def detect(self) -> dict:
|
|
77
|
+
"""执行完整环境检测,返回结构化结果"""
|
|
78
|
+
return {
|
|
79
|
+
"os": self._detect_os(),
|
|
80
|
+
"hardware": self._detect_hardware(),
|
|
81
|
+
"gpu": self._detect_gpu(),
|
|
82
|
+
"package_managers": self._detect_package_managers(),
|
|
83
|
+
"runtimes": self._detect_runtimes(),
|
|
84
|
+
"disk": self._detect_disk(),
|
|
85
|
+
"llm_configured": self._detect_llm_env(),
|
|
86
|
+
"network": self._detect_network(),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# ── OS ──────────────────────────────────
|
|
90
|
+
|
|
91
|
+
def _detect_os(self) -> dict:
|
|
92
|
+
system = platform.system()
|
|
93
|
+
|
|
94
|
+
if system == "Darwin":
|
|
95
|
+
return self._detect_macos()
|
|
96
|
+
elif system == "Linux":
|
|
97
|
+
return self._detect_linux()
|
|
98
|
+
elif system == "Windows":
|
|
99
|
+
return self._detect_windows()
|
|
100
|
+
else:
|
|
101
|
+
return {"type": "unknown", "system": system}
|
|
102
|
+
|
|
103
|
+
def _detect_macos(self) -> dict:
|
|
104
|
+
arch = platform.machine() # "arm64" 或 "x86_64"
|
|
105
|
+
mac_ver = platform.mac_ver()[0]
|
|
106
|
+
|
|
107
|
+
chip = "unknown"
|
|
108
|
+
if arch == "arm64":
|
|
109
|
+
# 检测 Apple Silicon 代别
|
|
110
|
+
chip_info = _run(["sysctl", "-n", "machdep.cpu.brand_string"]) or ""
|
|
111
|
+
if "M4" in chip_info:
|
|
112
|
+
chip = "Apple M4"
|
|
113
|
+
elif "M3" in chip_info:
|
|
114
|
+
chip = "Apple M3"
|
|
115
|
+
elif "M2" in chip_info:
|
|
116
|
+
chip = "Apple M2"
|
|
117
|
+
elif "M1" in chip_info:
|
|
118
|
+
chip = "Apple M1"
|
|
119
|
+
else:
|
|
120
|
+
chip = "Apple Silicon"
|
|
121
|
+
else:
|
|
122
|
+
chip = _run(["sysctl", "-n", "machdep.cpu.brand_string"]) or "Intel"
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"type": "macos",
|
|
126
|
+
"version": mac_ver,
|
|
127
|
+
"arch": arch,
|
|
128
|
+
"chip": chip,
|
|
129
|
+
"is_apple_silicon": arch == "arm64",
|
|
130
|
+
"shell": os.environ.get("SHELL", "/bin/zsh"),
|
|
131
|
+
"home": str(Path.home()),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
def _detect_linux(self) -> dict:
|
|
135
|
+
arch = platform.machine()
|
|
136
|
+
distro = "unknown"
|
|
137
|
+
distro_version = ""
|
|
138
|
+
|
|
139
|
+
# 读取 /etc/os-release
|
|
140
|
+
os_release = {}
|
|
141
|
+
for path in ["/etc/os-release", "/usr/lib/os-release"]:
|
|
142
|
+
try:
|
|
143
|
+
with open(path) as f:
|
|
144
|
+
for line in f:
|
|
145
|
+
line = line.strip()
|
|
146
|
+
if "=" in line:
|
|
147
|
+
k, v = line.split("=", 1)
|
|
148
|
+
os_release[k] = v.strip('"')
|
|
149
|
+
break
|
|
150
|
+
except FileNotFoundError:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
distro = os_release.get("ID", "linux")
|
|
154
|
+
distro_version = os_release.get("VERSION_ID", "")
|
|
155
|
+
distro_name = os_release.get("PRETTY_NAME", distro)
|
|
156
|
+
|
|
157
|
+
# 检测 WSL
|
|
158
|
+
is_wsl = False
|
|
159
|
+
try:
|
|
160
|
+
with open("/proc/version") as f:
|
|
161
|
+
if "microsoft" in f.read().lower():
|
|
162
|
+
is_wsl = True
|
|
163
|
+
except FileNotFoundError:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"type": "linux",
|
|
168
|
+
"distro": distro, # ubuntu / arch / fedora / ...
|
|
169
|
+
"distro_name": distro_name,
|
|
170
|
+
"version": distro_version,
|
|
171
|
+
"arch": arch,
|
|
172
|
+
"is_wsl": is_wsl,
|
|
173
|
+
"shell": os.environ.get("SHELL", "/bin/bash"),
|
|
174
|
+
"home": str(Path.home()),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def _detect_windows(self) -> dict:
|
|
178
|
+
arch = platform.machine() # "AMD64" 或 "ARM64"
|
|
179
|
+
win_ver = platform.version()
|
|
180
|
+
release = platform.release()
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
"type": "windows",
|
|
184
|
+
"version": win_ver,
|
|
185
|
+
"release": release, # "10" 或 "11"
|
|
186
|
+
"arch": arch,
|
|
187
|
+
"is_wsl": False,
|
|
188
|
+
"shell": os.environ.get("COMSPEC", "cmd.exe"),
|
|
189
|
+
"home": str(Path.home()),
|
|
190
|
+
"powershell": bool(_which("powershell") or _which("pwsh")),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# ── 硬件 ──────────────────────────────────
|
|
194
|
+
|
|
195
|
+
def _detect_hardware(self) -> dict:
|
|
196
|
+
result = {
|
|
197
|
+
"cpu_count": os.cpu_count(),
|
|
198
|
+
"ram_gb": self._detect_ram_gb(),
|
|
199
|
+
}
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
def _detect_ram_gb(self) -> Optional[float]:
|
|
203
|
+
system = platform.system()
|
|
204
|
+
try:
|
|
205
|
+
if system == "Darwin":
|
|
206
|
+
output = _run(["sysctl", "-n", "hw.memsize"])
|
|
207
|
+
if output:
|
|
208
|
+
return round(int(output) / (1024 ** 3), 1)
|
|
209
|
+
elif system == "Linux":
|
|
210
|
+
with open("/proc/meminfo") as f:
|
|
211
|
+
for line in f:
|
|
212
|
+
if line.startswith("MemTotal:"):
|
|
213
|
+
kb = int(line.split()[1])
|
|
214
|
+
return round(kb / (1024 ** 2), 1)
|
|
215
|
+
elif system == "Windows":
|
|
216
|
+
output = _run(["wmic", "ComputerSystem", "get", "TotalPhysicalMemory"])
|
|
217
|
+
if output:
|
|
218
|
+
for line in output.split("\n"):
|
|
219
|
+
line = line.strip()
|
|
220
|
+
if line.isdigit():
|
|
221
|
+
return round(int(line) / (1024 ** 3), 1)
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
# ── GPU ──────────────────────────────────
|
|
227
|
+
|
|
228
|
+
def _detect_gpu(self) -> dict:
|
|
229
|
+
system = platform.system()
|
|
230
|
+
arch = platform.machine()
|
|
231
|
+
|
|
232
|
+
# Apple Silicon → MPS
|
|
233
|
+
if system == "Darwin" and arch == "arm64":
|
|
234
|
+
return {
|
|
235
|
+
"type": "apple_mps",
|
|
236
|
+
"name": "Apple Neural Engine + GPU",
|
|
237
|
+
"pytorch_flag": "mps",
|
|
238
|
+
"cuda_available": False,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# NVIDIA CUDA
|
|
242
|
+
nvidia = self._detect_nvidia()
|
|
243
|
+
if nvidia:
|
|
244
|
+
return nvidia
|
|
245
|
+
|
|
246
|
+
# AMD ROCm(Linux only)
|
|
247
|
+
if system == "Linux":
|
|
248
|
+
rocm = self._detect_rocm()
|
|
249
|
+
if rocm:
|
|
250
|
+
return rocm
|
|
251
|
+
|
|
252
|
+
# 集成显卡 / 无独显
|
|
253
|
+
return {
|
|
254
|
+
"type": "cpu_only",
|
|
255
|
+
"name": "No dedicated GPU",
|
|
256
|
+
"pytorch_flag": "cpu",
|
|
257
|
+
"cuda_available": False,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def _detect_nvidia(self) -> Optional[dict]:
|
|
261
|
+
"""检测 NVIDIA GPU 和 CUDA 版本"""
|
|
262
|
+
nvidia_smi = _which("nvidia-smi")
|
|
263
|
+
if not nvidia_smi:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
gpu_name = _run(["nvidia-smi", "--query-gpu=name", "--format=csv,noheader,nounits"])
|
|
267
|
+
|
|
268
|
+
# 解析 CUDA 版本
|
|
269
|
+
cuda_ver = None
|
|
270
|
+
nvcc_output = _run(["nvcc", "--version"])
|
|
271
|
+
if nvcc_output:
|
|
272
|
+
match = re.search(r'release (\d+\.\d+)', nvcc_output)
|
|
273
|
+
if match:
|
|
274
|
+
cuda_ver = match.group(1)
|
|
275
|
+
|
|
276
|
+
if not cuda_ver:
|
|
277
|
+
# 从 nvidia-smi 尝试解析
|
|
278
|
+
smi_output = _run(["nvidia-smi"])
|
|
279
|
+
if smi_output:
|
|
280
|
+
match = re.search(r'CUDA Version:\s*([\d.]+)', smi_output)
|
|
281
|
+
if match:
|
|
282
|
+
cuda_ver = match.group(1)
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
"type": "nvidia_cuda",
|
|
286
|
+
"name": (gpu_name or "NVIDIA GPU").split("\n")[0].strip(),
|
|
287
|
+
"cuda_version": cuda_ver,
|
|
288
|
+
"pytorch_flag": f"cu{cuda_ver.replace('.', '')[:3]}" if cuda_ver else "cu121",
|
|
289
|
+
"cuda_available": True,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
def _detect_rocm(self) -> Optional[dict]:
|
|
293
|
+
"""检测 AMD ROCm"""
|
|
294
|
+
if not _which("rocm-smi") and not Path("/opt/rocm").exists():
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
rocm_version = None
|
|
298
|
+
rocm_info = _run(["rocm-smi", "--showfwinfo"])
|
|
299
|
+
if rocm_info:
|
|
300
|
+
match = re.search(r'ROCm\s+([\d.]+)', rocm_info)
|
|
301
|
+
if match:
|
|
302
|
+
rocm_version = match.group(1)
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
"type": "amd_rocm",
|
|
306
|
+
"name": "AMD GPU (ROCm)",
|
|
307
|
+
"rocm_version": rocm_version,
|
|
308
|
+
"pytorch_flag": "rocm",
|
|
309
|
+
"cuda_available": False,
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# ── 包管理器 ──────────────────────────────
|
|
313
|
+
|
|
314
|
+
def _detect_package_managers(self) -> dict:
|
|
315
|
+
"""检测所有平台的包管理器"""
|
|
316
|
+
managers = {}
|
|
317
|
+
|
|
318
|
+
checks = [
|
|
319
|
+
# 通用
|
|
320
|
+
("pip", ["pip", "--version"]),
|
|
321
|
+
("pip3", ["pip3", "--version"]),
|
|
322
|
+
("conda", ["conda", "--version"]),
|
|
323
|
+
("uv", ["uv", "--version"]), # 新一代 Python 包管理
|
|
324
|
+
# macOS
|
|
325
|
+
("brew", ["brew", "--version"]),
|
|
326
|
+
# Linux
|
|
327
|
+
("apt", ["apt", "--version"]),
|
|
328
|
+
("apt-get", ["apt-get", "--version"]),
|
|
329
|
+
("dnf", ["dnf", "--version"]),
|
|
330
|
+
("pacman", ["pacman", "--version"]),
|
|
331
|
+
("yay", ["yay", "--version"]), # Arch AUR
|
|
332
|
+
("zypper", ["zypper", "--version"]),
|
|
333
|
+
("snap", ["snap", "--version"]),
|
|
334
|
+
# Windows
|
|
335
|
+
("winget", ["winget", "--version"]),
|
|
336
|
+
("choco", ["choco", "--version"]),
|
|
337
|
+
("scoop", ["scoop", "--version"]),
|
|
338
|
+
# 语言级
|
|
339
|
+
("npm", ["npm", "--version"]),
|
|
340
|
+
("pnpm", ["pnpm", "--version"]),
|
|
341
|
+
("yarn", ["yarn", "--version"]),
|
|
342
|
+
("bun", ["bun", "--version"]),
|
|
343
|
+
("cargo", ["cargo", "--version"]),
|
|
344
|
+
("go", ["go", "version"]),
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
for name, cmd in checks:
|
|
348
|
+
if _which(cmd[0]):
|
|
349
|
+
ver = _version(cmd[0], cmd[1] if len(cmd) > 1 else "--version")
|
|
350
|
+
managers[name] = {"available": True, "version": ver}
|
|
351
|
+
|
|
352
|
+
return managers
|
|
353
|
+
|
|
354
|
+
# ── 运行时 ──────────────────────────────────
|
|
355
|
+
|
|
356
|
+
def _detect_runtimes(self) -> dict:
|
|
357
|
+
"""检测主要开发运行时"""
|
|
358
|
+
runtimes = {}
|
|
359
|
+
|
|
360
|
+
# Python(最重要)—— 独立检测系统可用的 python3,而不是当前解释器
|
|
361
|
+
py_version = sys.version.split()[0]
|
|
362
|
+
py_executable = sys.executable
|
|
363
|
+
py_path = _which("python3") or _which("python")
|
|
364
|
+
# 如果系统 python3 与当前解释器不同,优先报告系统的
|
|
365
|
+
if py_path and os.path.realpath(py_path) != os.path.realpath(sys.executable):
|
|
366
|
+
sys_py_ver = _version("python3") or _version("python")
|
|
367
|
+
if sys_py_ver:
|
|
368
|
+
py_version = sys_py_ver
|
|
369
|
+
py_executable = py_path
|
|
370
|
+
runtimes["python"] = {
|
|
371
|
+
"available": True,
|
|
372
|
+
"version": py_version,
|
|
373
|
+
"executable": py_executable,
|
|
374
|
+
"path": py_path,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Node.js
|
|
378
|
+
node_ver = _version("node")
|
|
379
|
+
if node_ver:
|
|
380
|
+
runtimes["node"] = {"available": True, "version": node_ver}
|
|
381
|
+
|
|
382
|
+
# Git(安装几乎所有项目都需要)
|
|
383
|
+
git_ver = _version("git")
|
|
384
|
+
runtimes["git"] = {
|
|
385
|
+
"available": bool(git_ver),
|
|
386
|
+
"version": git_ver,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
# Docker
|
|
390
|
+
docker_ver = _version("docker")
|
|
391
|
+
if docker_ver:
|
|
392
|
+
# 检测 Docker 是否真正运行
|
|
393
|
+
docker_running = _run(["docker", "ps"]) is not None
|
|
394
|
+
runtimes["docker"] = {
|
|
395
|
+
"available": True,
|
|
396
|
+
"version": docker_ver,
|
|
397
|
+
"daemon_running": docker_running,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
# Rust
|
|
401
|
+
rust_ver = _version("rustc")
|
|
402
|
+
if rust_ver:
|
|
403
|
+
runtimes["rust"] = {"available": True, "version": rust_ver}
|
|
404
|
+
|
|
405
|
+
# Go
|
|
406
|
+
go_ver = _version("go", "version")
|
|
407
|
+
if go_ver:
|
|
408
|
+
runtimes["go"] = {"available": True, "version": go_ver}
|
|
409
|
+
|
|
410
|
+
# Java(部分工具需要)
|
|
411
|
+
java_ver = _version("java", "-version")
|
|
412
|
+
if java_ver:
|
|
413
|
+
runtimes["java"] = {"available": True, "version": java_ver}
|
|
414
|
+
|
|
415
|
+
# ffmpeg(视频处理类项目)
|
|
416
|
+
ffmpeg_ver = _version("ffmpeg", "-version")
|
|
417
|
+
if ffmpeg_ver:
|
|
418
|
+
runtimes["ffmpeg"] = {"available": True, "version": ffmpeg_ver}
|
|
419
|
+
|
|
420
|
+
return runtimes
|
|
421
|
+
|
|
422
|
+
# ── 磁盘空间 ──────────────────────────────
|
|
423
|
+
|
|
424
|
+
def _detect_disk(self) -> dict:
|
|
425
|
+
"""检测 home 目录所在分区的可用空间"""
|
|
426
|
+
try:
|
|
427
|
+
stat = os.statvfs(str(Path.home()))
|
|
428
|
+
free_gb = round((stat.f_frsize * stat.f_bavail) / (1024 ** 3), 1)
|
|
429
|
+
total_gb = round((stat.f_frsize * stat.f_blocks) / (1024 ** 3), 1)
|
|
430
|
+
return {"free_gb": free_gb, "total_gb": total_gb, "path": str(Path.home())}
|
|
431
|
+
except AttributeError:
|
|
432
|
+
# Windows 不支持 statvfs
|
|
433
|
+
import shutil as sh
|
|
434
|
+
usage = sh.disk_usage(str(Path.home()))
|
|
435
|
+
return {
|
|
436
|
+
"free_gb": round(usage.free / (1024 ** 3), 1),
|
|
437
|
+
"total_gb": round(usage.total / (1024 ** 3), 1),
|
|
438
|
+
"path": str(Path.home()),
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# ── LLM 环境变量检测 ──────────────────────
|
|
442
|
+
|
|
443
|
+
def _detect_llm_env(self) -> dict:
|
|
444
|
+
"""检测已配置的 LLM API Keys(只检测是否存在,不暴露值)"""
|
|
445
|
+
keys = {
|
|
446
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
447
|
+
"openai": "OPENAI_API_KEY",
|
|
448
|
+
"openrouter": "OPENROUTER_API_KEY",
|
|
449
|
+
"gemini": "GEMINI_API_KEY",
|
|
450
|
+
"groq": "GROQ_API_KEY",
|
|
451
|
+
"deepseek": "DEEPSEEK_API_KEY",
|
|
452
|
+
}
|
|
453
|
+
return {name: bool(os.getenv(env_var, "").strip()) for name, env_var in keys.items()}
|
|
454
|
+
|
|
455
|
+
# ── 网络检测 ──────────────────────────────
|
|
456
|
+
|
|
457
|
+
def _detect_network(self) -> dict:
|
|
458
|
+
"""检测网络可达性(主要检测 GitHub)"""
|
|
459
|
+
import socket
|
|
460
|
+
result = {}
|
|
461
|
+
targets = [
|
|
462
|
+
("github", "github.com", 443),
|
|
463
|
+
("pypi", "pypi.org", 443),
|
|
464
|
+
]
|
|
465
|
+
for name, host, port in targets:
|
|
466
|
+
try:
|
|
467
|
+
socket.create_connection((host, port), timeout=10).close()
|
|
468
|
+
result[name] = True
|
|
469
|
+
except (socket.timeout, OSError):
|
|
470
|
+
result[name] = False
|
|
471
|
+
return result
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# ─────────────────────────────────────────────
|
|
475
|
+
# 格式化输出(供 CLI 使用)
|
|
476
|
+
# ─────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
def format_env_summary(env: dict) -> str:
|
|
479
|
+
"""格式化环境信息为人类可读的摘要"""
|
|
480
|
+
lines = []
|
|
481
|
+
os_info = env.get("os", {})
|
|
482
|
+
gpu_info = env.get("gpu", {})
|
|
483
|
+
hw = env.get("hardware", {})
|
|
484
|
+
disk = env.get("disk", {})
|
|
485
|
+
|
|
486
|
+
# OS 行
|
|
487
|
+
if os_info.get("type") == "macos":
|
|
488
|
+
lines.append(f"💻 {os_info.get('chip', 'Mac')} / macOS {os_info.get('version', '')} ({os_info.get('arch', '')})")
|
|
489
|
+
elif os_info.get("type") == "linux":
|
|
490
|
+
wsl = " [WSL2]" if os_info.get("is_wsl") else ""
|
|
491
|
+
lines.append(f"🐧 {os_info.get('distro_name', 'Linux')}{wsl} ({os_info.get('arch', '')})")
|
|
492
|
+
elif os_info.get("type") == "windows":
|
|
493
|
+
lines.append(f"🪟 Windows {os_info.get('release', '')} ({os_info.get('arch', '')})")
|
|
494
|
+
|
|
495
|
+
# 硬件
|
|
496
|
+
ram = hw.get("ram_gb")
|
|
497
|
+
cpu = hw.get("cpu_count")
|
|
498
|
+
if ram and cpu:
|
|
499
|
+
lines.append(f"⚙️ {cpu} 核 / {ram} GB RAM / 磁盘剩余 {disk.get('free_gb', '?')} GB")
|
|
500
|
+
|
|
501
|
+
# GPU
|
|
502
|
+
gpu_type = gpu_info.get("type", "cpu_only")
|
|
503
|
+
if gpu_type == "apple_mps":
|
|
504
|
+
lines.append("🎮 GPU: Apple MPS ✅")
|
|
505
|
+
elif gpu_type == "nvidia_cuda":
|
|
506
|
+
cuda = gpu_info.get("cuda_version", "未知")
|
|
507
|
+
lines.append(f"🎮 GPU: {gpu_info.get('name', 'NVIDIA')} / CUDA {cuda} ✅")
|
|
508
|
+
elif gpu_type == "amd_rocm":
|
|
509
|
+
lines.append(f"🎮 GPU: {gpu_info.get('name', 'AMD')} / ROCm ✅")
|
|
510
|
+
else:
|
|
511
|
+
lines.append("🎮 GPU: 无独立显卡(将使用 CPU 模式)")
|
|
512
|
+
|
|
513
|
+
# 运行时
|
|
514
|
+
runtimes = env.get("runtimes", {})
|
|
515
|
+
rt_parts = []
|
|
516
|
+
if "python" in runtimes:
|
|
517
|
+
rt_parts.append(f"Python {runtimes['python']['version']}")
|
|
518
|
+
if "node" in runtimes:
|
|
519
|
+
rt_parts.append(f"Node {runtimes['node']['version']}")
|
|
520
|
+
if "git" in runtimes and runtimes["git"]["available"]:
|
|
521
|
+
rt_parts.append("git ✓")
|
|
522
|
+
if "docker" in runtimes:
|
|
523
|
+
running = "✅" if runtimes["docker"].get("daemon_running") else "⚠️(未运行)"
|
|
524
|
+
rt_parts.append(f"Docker {running}")
|
|
525
|
+
if rt_parts:
|
|
526
|
+
lines.append("🔧 运行时:" + " | ".join(rt_parts))
|
|
527
|
+
|
|
528
|
+
# 包管理器
|
|
529
|
+
pms = env.get("package_managers", {})
|
|
530
|
+
available_pms = [name for name, info in pms.items() if info.get("available")]
|
|
531
|
+
if available_pms:
|
|
532
|
+
lines.append("📦 包管理器:" + " | ".join(available_pms[:6]))
|
|
533
|
+
|
|
534
|
+
return "\n".join(lines)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
if __name__ == "__main__":
|
|
538
|
+
detector = EnvironmentDetector()
|
|
539
|
+
env = detector.detect()
|
|
540
|
+
print(json.dumps(env, ensure_ascii=False, indent=2))
|
|
541
|
+
print("\n" + "─" * 50)
|
|
542
|
+
print(format_env_summary(env))
|