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/doctor.py
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""
|
|
2
|
+
doctor.py - gitinstall 诊断系统
|
|
3
|
+
================================
|
|
4
|
+
|
|
5
|
+
灵感来源:OpenClaw `openclaw doctor` 诊断工具
|
|
6
|
+
|
|
7
|
+
功能:
|
|
8
|
+
1. 系统环境全面检查(OS、Python、Git、包管理器)
|
|
9
|
+
2. GitHub API 连通性 + 配额检测
|
|
10
|
+
3. LLM API Key 可用性验证
|
|
11
|
+
4. 缓存健康度检查(大小、过期条目)
|
|
12
|
+
5. 数据库完整性校验
|
|
13
|
+
6. GPU / AI 硬件就绪度
|
|
14
|
+
7. 安全配置审计
|
|
15
|
+
8. 已知问题自动修复建议
|
|
16
|
+
|
|
17
|
+
零外部依赖,纯 Python 标准库。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import platform
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
import time
|
|
29
|
+
import urllib.error
|
|
30
|
+
import urllib.request
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Optional
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── 诊断结果等级 ──
|
|
37
|
+
LEVEL_OK = "ok" # ✅ 正常
|
|
38
|
+
LEVEL_WARN = "warn" # ⚠️ 警告(不阻塞,但建议修复)
|
|
39
|
+
LEVEL_ERROR = "error" # ❌ 错误(会影响功能)
|
|
40
|
+
LEVEL_INFO = "info" # ℹ️ 纯信息
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class CheckResult:
|
|
45
|
+
"""单项检查结果"""
|
|
46
|
+
name: str
|
|
47
|
+
level: str
|
|
48
|
+
message: str
|
|
49
|
+
detail: str = ""
|
|
50
|
+
fix_hint: str = ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class DoctorReport:
|
|
55
|
+
"""完整诊断报告"""
|
|
56
|
+
checks: list[CheckResult] = field(default_factory=list)
|
|
57
|
+
timestamp: float = 0.0
|
|
58
|
+
duration_ms: float = 0.0
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def ok_count(self) -> int:
|
|
62
|
+
return sum(1 for c in self.checks if c.level == LEVEL_OK)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def warn_count(self) -> int:
|
|
66
|
+
return sum(1 for c in self.checks if c.level == LEVEL_WARN)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def error_count(self) -> int:
|
|
70
|
+
return sum(1 for c in self.checks if c.level == LEVEL_ERROR)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def all_ok(self) -> bool:
|
|
74
|
+
return self.error_count == 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ─────────────────────────────────────────────
|
|
78
|
+
# 各项检查
|
|
79
|
+
# ─────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
def _check_python() -> CheckResult:
|
|
82
|
+
"""检查 Python 版本"""
|
|
83
|
+
ver = sys.version_info
|
|
84
|
+
ver_str = f"{ver.major}.{ver.minor}.{ver.micro}"
|
|
85
|
+
if ver >= (3, 10):
|
|
86
|
+
return CheckResult("Python 版本", LEVEL_OK, f"Python {ver_str}")
|
|
87
|
+
elif ver >= (3, 8):
|
|
88
|
+
return CheckResult("Python 版本", LEVEL_WARN, f"Python {ver_str}(推荐 3.10+)",
|
|
89
|
+
fix_hint="升级 Python: brew install python3 / apt install python3.12")
|
|
90
|
+
else:
|
|
91
|
+
return CheckResult("Python 版本", LEVEL_ERROR, f"Python {ver_str} 版本过低",
|
|
92
|
+
fix_hint="需要 Python 3.8+,推荐 3.10+")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _check_git() -> CheckResult:
|
|
96
|
+
"""检查 Git 可用性"""
|
|
97
|
+
git = shutil.which("git")
|
|
98
|
+
if not git:
|
|
99
|
+
return CheckResult("Git", LEVEL_ERROR, "未安装 Git",
|
|
100
|
+
fix_hint="安装: brew install git / apt install git / winget install Git.Git")
|
|
101
|
+
try:
|
|
102
|
+
result = subprocess.run([git, "--version"], capture_output=True, text=True, timeout=5)
|
|
103
|
+
ver = result.stdout.strip()
|
|
104
|
+
return CheckResult("Git", LEVEL_OK, ver)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return CheckResult("Git", LEVEL_WARN, f"Git 存在但无法运行: {e}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _check_package_managers() -> list[CheckResult]:
|
|
110
|
+
"""检查包管理器"""
|
|
111
|
+
results = []
|
|
112
|
+
managers = {
|
|
113
|
+
"brew": ("Homebrew", "macOS/Linux 包管理器"),
|
|
114
|
+
"apt": ("APT", "Debian/Ubuntu 包管理器"),
|
|
115
|
+
"yum": ("YUM", "RHEL/CentOS 包管理器"),
|
|
116
|
+
"dnf": ("DNF", "Fedora 包管理器"),
|
|
117
|
+
"pacman": ("Pacman", "Arch Linux 包管理器"),
|
|
118
|
+
"pip": ("pip", "Python 包管理器"),
|
|
119
|
+
"npm": ("npm", "Node.js 包管理器"),
|
|
120
|
+
"cargo": ("Cargo", "Rust 包管理器"),
|
|
121
|
+
"go": ("Go", "Go 语言工具链"),
|
|
122
|
+
"docker": ("Docker", "容器运行时"),
|
|
123
|
+
"conda": ("Conda", "科学计算环境管理器"),
|
|
124
|
+
}
|
|
125
|
+
found_any = False
|
|
126
|
+
for cmd, (display_name, desc) in managers.items():
|
|
127
|
+
path = shutil.which(cmd)
|
|
128
|
+
if path:
|
|
129
|
+
found_any = True
|
|
130
|
+
results.append(CheckResult(display_name, LEVEL_OK, f"已安装 ({path})"))
|
|
131
|
+
|
|
132
|
+
if not found_any:
|
|
133
|
+
results.append(CheckResult("包管理器", LEVEL_ERROR,
|
|
134
|
+
"未检测到任何包管理器",
|
|
135
|
+
fix_hint="至少需要一个包管理器才能安装项目依赖"))
|
|
136
|
+
return results
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _check_github_api() -> CheckResult:
|
|
140
|
+
"""检查 GitHub API 连通性和配额"""
|
|
141
|
+
token = os.getenv("GITHUB_TOKEN", "").strip()
|
|
142
|
+
headers = {"User-Agent": "gitinstall-doctor/1.0"}
|
|
143
|
+
if token:
|
|
144
|
+
headers["Authorization"] = f"token {token}"
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
req = urllib.request.Request("https://api.github.com/rate_limit", headers=headers)
|
|
148
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
149
|
+
data = json.loads(resp.read().decode())
|
|
150
|
+
core = data.get("resources", {}).get("core", {})
|
|
151
|
+
remaining = core.get("remaining", 0)
|
|
152
|
+
limit = core.get("limit", 0)
|
|
153
|
+
reset_ts = core.get("reset", 0)
|
|
154
|
+
|
|
155
|
+
if token:
|
|
156
|
+
auth_info = f"已认证(GITHUB_TOKEN),配额 {remaining}/{limit}"
|
|
157
|
+
else:
|
|
158
|
+
auth_info = f"未认证,配额 {remaining}/{limit}"
|
|
159
|
+
|
|
160
|
+
if remaining < 5:
|
|
161
|
+
reset_time = time.strftime("%H:%M:%S", time.localtime(reset_ts))
|
|
162
|
+
return CheckResult("GitHub API", LEVEL_WARN,
|
|
163
|
+
f"{auth_info},配额即将耗尽,{reset_time} 重置",
|
|
164
|
+
fix_hint="设置 GITHUB_TOKEN 环境变量提升到 5000 次/小时,或使用 --local 模式")
|
|
165
|
+
if not token:
|
|
166
|
+
return CheckResult("GitHub API", LEVEL_WARN,
|
|
167
|
+
f"{auth_info}(限制 60 次/小时)",
|
|
168
|
+
fix_hint="设置 GITHUB_TOKEN=ghp_xxx 提升到 5000 次/小时")
|
|
169
|
+
return CheckResult("GitHub API", LEVEL_OK, auth_info)
|
|
170
|
+
except urllib.error.URLError as e:
|
|
171
|
+
return CheckResult("GitHub API", LEVEL_ERROR, f"无法连接 GitHub API: {e}",
|
|
172
|
+
fix_hint="检查网络连接,或设置 HTTP_PROXY 环境变量")
|
|
173
|
+
except Exception as e:
|
|
174
|
+
return CheckResult("GitHub API", LEVEL_ERROR, f"检查失败: {e}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _check_llm_keys() -> list[CheckResult]:
|
|
178
|
+
"""检查 LLM API Key 配置"""
|
|
179
|
+
results = []
|
|
180
|
+
keys = {
|
|
181
|
+
"ANTHROPIC_API_KEY": ("Anthropic Claude", "sk-ant-"),
|
|
182
|
+
"OPENAI_API_KEY": ("OpenAI GPT", "sk-"),
|
|
183
|
+
"OPENROUTER_API_KEY": ("OpenRouter", "sk-or-"),
|
|
184
|
+
"GEMINI_API_KEY": ("Google Gemini", "AI"),
|
|
185
|
+
"GROQ_API_KEY": ("Groq Llama", "gsk_"),
|
|
186
|
+
"DEEPSEEK_API_KEY": ("DeepSeek", "sk-"),
|
|
187
|
+
}
|
|
188
|
+
configured = []
|
|
189
|
+
for env_var, (display, prefix) in keys.items():
|
|
190
|
+
val = os.getenv(env_var, "").strip()
|
|
191
|
+
if val:
|
|
192
|
+
# 简单格式校验(不发送请求)
|
|
193
|
+
if prefix and not val.startswith(prefix):
|
|
194
|
+
results.append(CheckResult(display, LEVEL_WARN,
|
|
195
|
+
f"{env_var} 已设置但格式可能有误(期望 {prefix}... 开头)"))
|
|
196
|
+
else:
|
|
197
|
+
configured.append(display)
|
|
198
|
+
results.append(CheckResult(display, LEVEL_OK, f"{env_var} 已配置"))
|
|
199
|
+
|
|
200
|
+
# 检查本地 LLM
|
|
201
|
+
local_llms = [
|
|
202
|
+
("localhost:11434", "Ollama"),
|
|
203
|
+
("localhost:1234", "LM Studio"),
|
|
204
|
+
]
|
|
205
|
+
for addr, name in local_llms:
|
|
206
|
+
try:
|
|
207
|
+
req = urllib.request.Request(f"http://{addr}/")
|
|
208
|
+
with urllib.request.urlopen(req, timeout=3):
|
|
209
|
+
configured.append(name)
|
|
210
|
+
results.append(CheckResult(name, LEVEL_OK, f"{name} 本地服务运行中"))
|
|
211
|
+
except Exception:
|
|
212
|
+
pass # 不报错,本地 LLM 是可选的
|
|
213
|
+
|
|
214
|
+
if not configured:
|
|
215
|
+
results.append(CheckResult("LLM 配置", LEVEL_INFO,
|
|
216
|
+
"未配置任何 LLM API Key(gitinstall 无需 AI 也能工作)",
|
|
217
|
+
detail="SmartPlanner 内置 80+ 已知项目 + 50+ 语言模板,覆盖大部分场景",
|
|
218
|
+
fix_hint="如需 AI 增强,设置任一 API Key: ANTHROPIC_API_KEY, OPENAI_API_KEY, GROQ_API_KEY 等"))
|
|
219
|
+
|
|
220
|
+
return results
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _check_cache() -> CheckResult:
|
|
224
|
+
"""检查缓存健康度"""
|
|
225
|
+
cache_dir = Path.home() / ".cache" / "gitinstall"
|
|
226
|
+
if not cache_dir.exists():
|
|
227
|
+
return CheckResult("缓存", LEVEL_OK, "缓存目录尚未创建(首次使用时自动创建)")
|
|
228
|
+
|
|
229
|
+
total_size = 0
|
|
230
|
+
file_count = 0
|
|
231
|
+
expired_count = 0
|
|
232
|
+
now = time.time()
|
|
233
|
+
ttl = int(os.getenv("GITINSTALL_CACHE_TTL", str(24 * 3600)))
|
|
234
|
+
|
|
235
|
+
for f in cache_dir.rglob("*"):
|
|
236
|
+
if f.is_file():
|
|
237
|
+
file_count += 1
|
|
238
|
+
total_size += f.stat().st_size
|
|
239
|
+
if now - f.stat().st_mtime > ttl:
|
|
240
|
+
expired_count += 1
|
|
241
|
+
|
|
242
|
+
size_mb = total_size / (1024 * 1024)
|
|
243
|
+
|
|
244
|
+
if size_mb > 100:
|
|
245
|
+
return CheckResult("缓存", LEVEL_WARN,
|
|
246
|
+
f"缓存较大: {size_mb:.1f}MB ({file_count} 文件, {expired_count} 已过期)",
|
|
247
|
+
fix_hint=f"清理缓存: rm -rf {cache_dir}")
|
|
248
|
+
return CheckResult("缓存", LEVEL_OK,
|
|
249
|
+
f"{size_mb:.1f}MB, {file_count} 文件" +
|
|
250
|
+
(f" ({expired_count} 已过期)" if expired_count else ""))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _check_database() -> CheckResult:
|
|
254
|
+
"""检查数据库完整性"""
|
|
255
|
+
try:
|
|
256
|
+
from db_backend import get_backend
|
|
257
|
+
backend = get_backend()
|
|
258
|
+
except Exception:
|
|
259
|
+
return CheckResult("数据库", LEVEL_OK, "尚未创建(首次安装时自动初始化)")
|
|
260
|
+
|
|
261
|
+
db_path = Path.home() / ".gitinstall" / "data.db"
|
|
262
|
+
if backend.backend_type == "sqlite" and not db_path.exists():
|
|
263
|
+
return CheckResult("数据库", LEVEL_OK, "尚未创建(首次安装时自动初始化)")
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
# 完整性检查
|
|
267
|
+
integrity = backend.integrity_check()
|
|
268
|
+
if integrity != "ok":
|
|
269
|
+
return CheckResult("数据库", LEVEL_ERROR, f"数据库损坏: {integrity}",
|
|
270
|
+
fix_hint=f"备份后删除: mv {db_path} {db_path}.bak")
|
|
271
|
+
|
|
272
|
+
# 统计数据
|
|
273
|
+
tables = {}
|
|
274
|
+
for table in ["events", "install_telemetry", "plans_history", "users"]:
|
|
275
|
+
try:
|
|
276
|
+
tables[table] = backend.table_row_count(table)
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
# 文件权限检查(仅 SQLite)
|
|
281
|
+
perm_ok = True
|
|
282
|
+
if backend.backend_type == "sqlite" and db_path.exists():
|
|
283
|
+
mode = oct(db_path.stat().st_mode)[-3:]
|
|
284
|
+
perm_ok = mode in ("600", "644", "700")
|
|
285
|
+
|
|
286
|
+
stats = ", ".join(f"{k}: {v}" for k, v in tables.items() if v > 0)
|
|
287
|
+
msg = f"正常 ({stats})" if stats else "正常(空数据库)"
|
|
288
|
+
if not perm_ok:
|
|
289
|
+
return CheckResult("数据库", LEVEL_WARN, f"{msg},权限 {mode} 不安全",
|
|
290
|
+
fix_hint=f"修复: chmod 600 {db_path}")
|
|
291
|
+
return CheckResult("数据库", LEVEL_OK, msg)
|
|
292
|
+
|
|
293
|
+
except Exception as e:
|
|
294
|
+
return CheckResult("数据库", LEVEL_ERROR, f"无法打开: {e}")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _check_gpu() -> CheckResult:
|
|
298
|
+
"""检查 GPU / AI 硬件"""
|
|
299
|
+
try:
|
|
300
|
+
from hw_detect import get_gpu_info
|
|
301
|
+
gpu = get_gpu_info()
|
|
302
|
+
gpu_type = gpu.get("type", "cpu_only")
|
|
303
|
+
gpu_name = gpu.get("name", "")
|
|
304
|
+
vram = gpu.get("vram_gb")
|
|
305
|
+
|
|
306
|
+
if gpu_type == "apple_mps":
|
|
307
|
+
return CheckResult("GPU / AI 硬件", LEVEL_OK,
|
|
308
|
+
f"Apple Silicon: {gpu_name} (MPS 已就绪)")
|
|
309
|
+
elif gpu_type == "nvidia_cuda":
|
|
310
|
+
cuda = gpu.get("cuda_version", "?")
|
|
311
|
+
vram_str = f", {vram}GB VRAM" if vram else ""
|
|
312
|
+
return CheckResult("GPU / AI 硬件", LEVEL_OK,
|
|
313
|
+
f"NVIDIA: {gpu_name} (CUDA {cuda}{vram_str})")
|
|
314
|
+
elif gpu_type == "amd_rocm":
|
|
315
|
+
return CheckResult("GPU / AI 硬件", LEVEL_OK,
|
|
316
|
+
f"AMD: {gpu_name} (ROCm)")
|
|
317
|
+
else:
|
|
318
|
+
return CheckResult("GPU / AI 硬件", LEVEL_INFO,
|
|
319
|
+
"仅 CPU(AI/ML 项目可能较慢)",
|
|
320
|
+
detail="大部分项目不需要 GPU,AI/ML 项目推荐 GPU 加速")
|
|
321
|
+
except Exception:
|
|
322
|
+
return CheckResult("GPU / AI 硬件", LEVEL_INFO, "检测跳过")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _check_disk_space() -> CheckResult:
|
|
326
|
+
"""检查磁盘空间"""
|
|
327
|
+
try:
|
|
328
|
+
total, used, free = shutil.disk_usage(str(Path.home()))
|
|
329
|
+
free_gb = free / (1024 ** 3)
|
|
330
|
+
total_gb = total / (1024 ** 3)
|
|
331
|
+
pct = (used / total) * 100
|
|
332
|
+
|
|
333
|
+
if free_gb < 1:
|
|
334
|
+
return CheckResult("磁盘空间", LEVEL_ERROR,
|
|
335
|
+
f"仅剩 {free_gb:.1f}GB 可用空间",
|
|
336
|
+
fix_hint="磁盘空间不足,安装大项目可能失败")
|
|
337
|
+
elif free_gb < 5:
|
|
338
|
+
return CheckResult("磁盘空间", LEVEL_WARN,
|
|
339
|
+
f"剩余 {free_gb:.1f}GB / {total_gb:.0f}GB ({pct:.0f}% 已用)",
|
|
340
|
+
fix_hint="建议保留至少 5GB 空间用于项目安装")
|
|
341
|
+
return CheckResult("磁盘空间", LEVEL_OK,
|
|
342
|
+
f"剩余 {free_gb:.1f}GB / {total_gb:.0f}GB ({pct:.0f}% 已用)")
|
|
343
|
+
except Exception:
|
|
344
|
+
return CheckResult("磁盘空间", LEVEL_INFO, "检测跳过")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _check_security() -> CheckResult:
|
|
348
|
+
"""安全配置审计"""
|
|
349
|
+
issues = []
|
|
350
|
+
# 检查 .gitinstall 目录权限
|
|
351
|
+
gi_dir = Path.home() / ".gitinstall"
|
|
352
|
+
if gi_dir.exists():
|
|
353
|
+
mode = oct(gi_dir.stat().st_mode)[-3:]
|
|
354
|
+
if mode not in ("700", "755"):
|
|
355
|
+
issues.append(f"~/.gitinstall 目录权限 {mode},建议 700")
|
|
356
|
+
|
|
357
|
+
# 检查是否存在不安全的 API key 存储
|
|
358
|
+
shell_files = [".bashrc", ".zshrc", ".bash_profile", ".profile"]
|
|
359
|
+
for sf in shell_files:
|
|
360
|
+
p = Path.home() / sf
|
|
361
|
+
if p.exists():
|
|
362
|
+
try:
|
|
363
|
+
content = p.read_text(errors="ignore")
|
|
364
|
+
for key_name in ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GITHUB_TOKEN"]:
|
|
365
|
+
if f"export {key_name}=" in content:
|
|
366
|
+
# 只是信息提示,不算错误
|
|
367
|
+
pass
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
if issues:
|
|
372
|
+
return CheckResult("安全", LEVEL_WARN, f"发现 {len(issues)} 项安全建议",
|
|
373
|
+
detail="; ".join(issues))
|
|
374
|
+
return CheckResult("安全", LEVEL_OK, "配置安全")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _check_skills() -> CheckResult:
|
|
378
|
+
"""检查 Skills 插件目录"""
|
|
379
|
+
skills_dir = Path.home() / ".gitinstall" / "skills"
|
|
380
|
+
if not skills_dir.exists():
|
|
381
|
+
return CheckResult("Skills 插件", LEVEL_INFO,
|
|
382
|
+
"未安装任何 Skill(使用 gitinstall skills install 安装)")
|
|
383
|
+
|
|
384
|
+
skills = [d.name for d in skills_dir.iterdir() if d.is_dir() and (d / "skill.json").exists()]
|
|
385
|
+
if skills:
|
|
386
|
+
return CheckResult("Skills 插件", LEVEL_OK, f"已安装 {len(skills)} 个: {', '.join(skills[:5])}")
|
|
387
|
+
return CheckResult("Skills 插件", LEVEL_INFO, "Skills 目录存在但无已安装插件")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
# ─────────────────────────────────────────────
|
|
391
|
+
# 主诊断入口
|
|
392
|
+
# ─────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
def run_doctor(verbose: bool = False) -> DoctorReport:
|
|
395
|
+
"""运行完整诊断,返回报告"""
|
|
396
|
+
start = time.time()
|
|
397
|
+
report = DoctorReport(timestamp=start)
|
|
398
|
+
|
|
399
|
+
# 基础环境
|
|
400
|
+
report.checks.append(_check_python())
|
|
401
|
+
report.checks.append(_check_git())
|
|
402
|
+
report.checks.append(_check_disk_space())
|
|
403
|
+
|
|
404
|
+
# 包管理器
|
|
405
|
+
report.checks.extend(_check_package_managers())
|
|
406
|
+
|
|
407
|
+
# GPU
|
|
408
|
+
report.checks.append(_check_gpu())
|
|
409
|
+
|
|
410
|
+
# 网络与 API
|
|
411
|
+
report.checks.append(_check_github_api())
|
|
412
|
+
|
|
413
|
+
# LLM
|
|
414
|
+
report.checks.extend(_check_llm_keys())
|
|
415
|
+
|
|
416
|
+
# 数据存储
|
|
417
|
+
report.checks.append(_check_cache())
|
|
418
|
+
report.checks.append(_check_database())
|
|
419
|
+
|
|
420
|
+
# 安全
|
|
421
|
+
report.checks.append(_check_security())
|
|
422
|
+
|
|
423
|
+
# Skills
|
|
424
|
+
report.checks.append(_check_skills())
|
|
425
|
+
|
|
426
|
+
report.duration_ms = (time.time() - start) * 1000
|
|
427
|
+
return report
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def format_doctor_report(report: DoctorReport) -> str:
|
|
431
|
+
"""格式化诊断报告为终端可读字符串"""
|
|
432
|
+
lines = []
|
|
433
|
+
lines.append("")
|
|
434
|
+
lines.append("🩺 gitinstall doctor — 系统诊断报告")
|
|
435
|
+
lines.append("═" * 55)
|
|
436
|
+
|
|
437
|
+
icon_map = {
|
|
438
|
+
LEVEL_OK: "✅",
|
|
439
|
+
LEVEL_WARN: "⚠️ ",
|
|
440
|
+
LEVEL_ERROR: "❌",
|
|
441
|
+
LEVEL_INFO: "ℹ️ ",
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
# 按类型分组显示
|
|
445
|
+
for check in report.checks:
|
|
446
|
+
icon = icon_map.get(check.level, "?")
|
|
447
|
+
lines.append(f" {icon} {check.name}: {check.message}")
|
|
448
|
+
if check.detail:
|
|
449
|
+
lines.append(f" {check.detail}")
|
|
450
|
+
if check.fix_hint and check.level in (LEVEL_WARN, LEVEL_ERROR):
|
|
451
|
+
lines.append(f" 💡 {check.fix_hint}")
|
|
452
|
+
|
|
453
|
+
# 汇总
|
|
454
|
+
lines.append("")
|
|
455
|
+
lines.append("─" * 55)
|
|
456
|
+
total = len(report.checks)
|
|
457
|
+
summary_parts = [f"{report.ok_count} 通过"]
|
|
458
|
+
if report.warn_count:
|
|
459
|
+
summary_parts.append(f"{report.warn_count} 警告")
|
|
460
|
+
if report.error_count:
|
|
461
|
+
summary_parts.append(f"{report.error_count} 错误")
|
|
462
|
+
|
|
463
|
+
status = "✅ 系统就绪" if report.all_ok else "⚠️ 存在需要关注的问题"
|
|
464
|
+
lines.append(f" {status} — {total} 项检查: {', '.join(summary_parts)}")
|
|
465
|
+
lines.append(f" 诊断耗时: {report.duration_ms:.0f}ms")
|
|
466
|
+
lines.append("")
|
|
467
|
+
|
|
468
|
+
return "\n".join(lines)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def doctor_to_dict(report: DoctorReport) -> dict:
|
|
472
|
+
"""将诊断报告转为 JSON 可序列化的 dict"""
|
|
473
|
+
return {
|
|
474
|
+
"status": "ok" if report.all_ok else "warning" if report.error_count == 0 else "error",
|
|
475
|
+
"timestamp": report.timestamp,
|
|
476
|
+
"duration_ms": report.duration_ms,
|
|
477
|
+
"summary": {
|
|
478
|
+
"total": len(report.checks),
|
|
479
|
+
"ok": report.ok_count,
|
|
480
|
+
"warn": report.warn_count,
|
|
481
|
+
"error": report.error_count,
|
|
482
|
+
},
|
|
483
|
+
"checks": [
|
|
484
|
+
{
|
|
485
|
+
"name": c.name,
|
|
486
|
+
"level": c.level,
|
|
487
|
+
"message": c.message,
|
|
488
|
+
"detail": c.detail,
|
|
489
|
+
"fix_hint": c.fix_hint,
|
|
490
|
+
}
|
|
491
|
+
for c in report.checks
|
|
492
|
+
],
|
|
493
|
+
}
|