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/main.py
ADDED
|
@@ -0,0 +1,1408 @@
|
|
|
1
|
+
"""
|
|
2
|
+
main.py - gitinstall 总入口
|
|
3
|
+
=====================================
|
|
4
|
+
|
|
5
|
+
支持两种调用方式:
|
|
6
|
+
|
|
7
|
+
1. OpenClaw Skill 工具调用:
|
|
8
|
+
python main.py detect
|
|
9
|
+
python main.py fetch comfyanonymous/ComfyUI
|
|
10
|
+
python main.py plan comfyanonymous/ComfyUI
|
|
11
|
+
python main.py install comfyanonymous/ComfyUI [--dir ~/AI] [--llm none]
|
|
12
|
+
|
|
13
|
+
2. 独立 CLI(pip install gitinstall 后):
|
|
14
|
+
gitinstall comfyanonymous/ComfyUI
|
|
15
|
+
gitinstall --dry-run AUTOMATIC1111/stable-diffusion-webui
|
|
16
|
+
gitinstall --llm groq ollama/ollama
|
|
17
|
+
|
|
18
|
+
所有输出为 JSON(方便 OpenClaw 解析)+ 人类可读的进度信息(stderr)。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
# ── 将 tools 目录加入 Python 路径(兼容直接执行和 import)──
|
|
29
|
+
_THIS_DIR = Path(__file__).parent
|
|
30
|
+
if str(_THIS_DIR) not in sys.path:
|
|
31
|
+
sys.path.insert(0, str(_THIS_DIR))
|
|
32
|
+
|
|
33
|
+
from log import get_logger
|
|
34
|
+
from i18n import t
|
|
35
|
+
from detector import EnvironmentDetector, format_env_summary
|
|
36
|
+
from fetcher import fetch_project, fetch_project_local, fetch_project_from_path, is_local_path, format_project_summary
|
|
37
|
+
from llm import create_provider, INSTALL_SYSTEM_PROMPT, INSTALL_SYSTEM_PROMPT_SMALL, HeuristicProvider
|
|
38
|
+
from executor import InstallExecutor, check_command_safety
|
|
39
|
+
from planner import SmartPlanner
|
|
40
|
+
from hw_detect import get_gpu_info, check_pytorch_compatibility, get_full_ai_hardware_report
|
|
41
|
+
from planner_known_projects import check_hardware_compatibility
|
|
42
|
+
|
|
43
|
+
logger = get_logger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ─────────────────────────────────────────────
|
|
47
|
+
# 子命令:detect
|
|
48
|
+
# ─────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
def cmd_detect() -> dict:
|
|
51
|
+
"""检测当前系统环境,输出 JSON"""
|
|
52
|
+
logger.info(t("install.detecting_env"))
|
|
53
|
+
env = EnvironmentDetector().detect()
|
|
54
|
+
logger.info(format_env_summary(env))
|
|
55
|
+
return {"status": "ok", "env": env}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ─────────────────────────────────────────────
|
|
59
|
+
# 子命令:fetch
|
|
60
|
+
# ─────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
def cmd_fetch(identifier: str) -> dict:
|
|
63
|
+
"""获取 GitHub 项目信息,输出 JSON"""
|
|
64
|
+
logger.info(t("install.fetching", project=identifier))
|
|
65
|
+
try:
|
|
66
|
+
info = fetch_project(identifier)
|
|
67
|
+
logger.info(format_project_summary(info))
|
|
68
|
+
return {
|
|
69
|
+
"status": "ok",
|
|
70
|
+
"project": {
|
|
71
|
+
"full_name": info.full_name,
|
|
72
|
+
"description": info.description,
|
|
73
|
+
"stars": info.stars,
|
|
74
|
+
"language": info.language,
|
|
75
|
+
"project_type": info.project_type,
|
|
76
|
+
"clone_url": info.clone_url,
|
|
77
|
+
"homepage": info.homepage,
|
|
78
|
+
"has_dockerfile": "docker" in info.project_type,
|
|
79
|
+
"dependency_files": list(info.dependency_files.keys()),
|
|
80
|
+
"readme_preview": info.readme[:500],
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
except FileNotFoundError as e:
|
|
84
|
+
return {"status": "error", "message": str(e)}
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return {"status": "error", "message": f"获取失败:{e}"}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ─────────────────────────────────────────────
|
|
90
|
+
# 子命令:plan
|
|
91
|
+
# ─────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
def cmd_plan(identifier: str, llm_force: str = None, use_local: bool = False) -> dict:
|
|
94
|
+
"""
|
|
95
|
+
生成安装计划(不执行)。
|
|
96
|
+
|
|
97
|
+
生成策略(按优先级):
|
|
98
|
+
1. SmartPlanner 已知项目数据库 → 无需任何 AI,命中率最高
|
|
99
|
+
2. SmartPlanner 类型模板 → 无需任何 AI,依赖文件驱动
|
|
100
|
+
3. 真实 LLM(如有配置) → 分析 README 生成个性化步骤
|
|
101
|
+
LLM 仅作为增强,不是必需项
|
|
102
|
+
|
|
103
|
+
数据获取模式:
|
|
104
|
+
use_local=True → git clone --depth 1 本地分析(无 API 限额)
|
|
105
|
+
use_local=False → GitHub REST API(默认,受 60 次/小时限制)
|
|
106
|
+
"""
|
|
107
|
+
# 1. 环境检测
|
|
108
|
+
logger.info("🔍 检测环境...")
|
|
109
|
+
env = EnvironmentDetector().detect()
|
|
110
|
+
|
|
111
|
+
# 2. 项目信息(本地路径 vs 本地 clone vs API 模式)
|
|
112
|
+
source_path = ""
|
|
113
|
+
try:
|
|
114
|
+
if is_local_path(identifier):
|
|
115
|
+
logger.info(f"📂 本地路径模式:直接分析目录(无需网络)...")
|
|
116
|
+
info = fetch_project_from_path(identifier)
|
|
117
|
+
source_path = str(info.clone_url) # 实际是解析后的本地绝对路径
|
|
118
|
+
elif use_local:
|
|
119
|
+
logger.info("📥 本地模式:git clone + 本地文件分析(无 API 限额)...")
|
|
120
|
+
info = fetch_project_local(identifier)
|
|
121
|
+
else:
|
|
122
|
+
logger.info("📡 获取项目信息...")
|
|
123
|
+
info = fetch_project(identifier)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
return {"status": "error", "message": str(e)}
|
|
126
|
+
|
|
127
|
+
# 3. 首先用 SmartPlanner(零 AI,零 API Key)
|
|
128
|
+
logger.info(t("install.analyzing"))
|
|
129
|
+
planner = SmartPlanner()
|
|
130
|
+
smart_plan = planner.generate_plan(
|
|
131
|
+
owner=info.owner,
|
|
132
|
+
repo=info.repo,
|
|
133
|
+
env=env,
|
|
134
|
+
project_types=info.project_type,
|
|
135
|
+
dependency_files=info.dependency_files,
|
|
136
|
+
readme=info.readme,
|
|
137
|
+
clone_url=info.clone_url if not source_path else "",
|
|
138
|
+
source_path=source_path,
|
|
139
|
+
)
|
|
140
|
+
confidence = smart_plan.get("confidence", "low")
|
|
141
|
+
strategy = smart_plan.get("strategy", "")
|
|
142
|
+
|
|
143
|
+
# 4. 判断是否需要 LLM
|
|
144
|
+
# - 已知项目(high):直接用 SmartPlanner 结果,最准确
|
|
145
|
+
# - 明确指定 --llm none:不用 LLM
|
|
146
|
+
# - 类型模板/提取(medium/low)且有可用 LLM:用 LLM 补充
|
|
147
|
+
llm = create_provider(force=llm_force)
|
|
148
|
+
use_llm = (
|
|
149
|
+
confidence != "high" # 非已知项目才需要 LLM
|
|
150
|
+
and llm_force != "none" # 未强制禁用 LLM
|
|
151
|
+
and not isinstance(llm, HeuristicProvider) # 有真正的 LLM 可用
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
plan = smart_plan # 默认用 SmartPlanner
|
|
155
|
+
|
|
156
|
+
if use_llm:
|
|
157
|
+
logger.info(f"🤖 SmartPlanner 置信度 {confidence},尝试 LLM 补充分析({llm.name})...")
|
|
158
|
+
try:
|
|
159
|
+
user_prompt = _build_plan_prompt(env, info)
|
|
160
|
+
# 小模型(名字含 1b/1.5b/1.7b/3b)用精简 prompt + 较小 max_tokens
|
|
161
|
+
model_name = llm.name.lower()
|
|
162
|
+
is_small_model = any(x in model_name for x in ["1b", "1.5b", "1.7b", "3b"])
|
|
163
|
+
sys_prompt = INSTALL_SYSTEM_PROMPT_SMALL if is_small_model else INSTALL_SYSTEM_PROMPT
|
|
164
|
+
max_tok = 800 if is_small_model else 3000
|
|
165
|
+
response = llm.complete(sys_prompt, user_prompt, max_tokens=max_tok)
|
|
166
|
+
llm_plan = _validate_plan_schema(_parse_plan_response(response))
|
|
167
|
+
# LLM 成功且步骤更丰富时采用 LLM 结果
|
|
168
|
+
if llm_plan.get("steps") and len(llm_plan["steps"]) >= len(smart_plan.get("steps", [])):
|
|
169
|
+
plan = llm_plan
|
|
170
|
+
plan["strategy"] = f"llm_enhanced({strategy})"
|
|
171
|
+
except Exception as ex:
|
|
172
|
+
logger.warning(t("llm.api_error") + f" ({ex})")
|
|
173
|
+
else:
|
|
174
|
+
source = "已知项目数据库" if confidence == "high" else "类型模板"
|
|
175
|
+
logger.info(f"✅ SmartPlanner {source} 命中(无需 LLM)")
|
|
176
|
+
|
|
177
|
+
# 5. 安全预检每个步骤
|
|
178
|
+
safe_steps = []
|
|
179
|
+
for step in plan.get("steps", []):
|
|
180
|
+
cmd = step.get("command", "")
|
|
181
|
+
is_safe, safety_warning = check_command_safety(cmd)
|
|
182
|
+
step["_safe"] = is_safe
|
|
183
|
+
if not step.get("_warning"):
|
|
184
|
+
step["_warning"] = safety_warning
|
|
185
|
+
if is_safe:
|
|
186
|
+
safe_steps.append(step)
|
|
187
|
+
else:
|
|
188
|
+
logger.info(f"🚫 已过滤危险命令:{cmd}")
|
|
189
|
+
plan["steps"] = safe_steps
|
|
190
|
+
|
|
191
|
+
llm_desc = llm.name if use_llm else "SmartPlanner(无需 AI Key)"
|
|
192
|
+
|
|
193
|
+
# 6. AI 硬件智能检测 + 兼容性检查
|
|
194
|
+
gpu_info = get_gpu_info()
|
|
195
|
+
hw_compat = check_hardware_compatibility(f"{info.owner}/{info.repo}", gpu_info, env)
|
|
196
|
+
if hw_compat.get("warnings"):
|
|
197
|
+
logger.info("\n⚡ 硬件兼容性提示:")
|
|
198
|
+
for w in hw_compat["warnings"]:
|
|
199
|
+
logger.warning(f" ⚠️ {w}")
|
|
200
|
+
for r in hw_compat.get("recommendations", []):
|
|
201
|
+
logger.info(f" 💡 {r}")
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"status": "ok",
|
|
205
|
+
"plan": plan,
|
|
206
|
+
"llm_used": llm_desc,
|
|
207
|
+
"confidence": confidence,
|
|
208
|
+
"project": info.full_name,
|
|
209
|
+
"hardware_check": hw_compat,
|
|
210
|
+
"gpu_info": {
|
|
211
|
+
"type": gpu_info.get("type"),
|
|
212
|
+
"name": gpu_info.get("name"),
|
|
213
|
+
"vram_gb": gpu_info.get("vram_gb"),
|
|
214
|
+
},
|
|
215
|
+
# 供韧性层(resilience)使用
|
|
216
|
+
"_owner": info.owner,
|
|
217
|
+
"_repo": info.repo,
|
|
218
|
+
"_project_types": info.project_type,
|
|
219
|
+
"_dependency_files": info.dependency_files,
|
|
220
|
+
"_env": env,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ─────────────────────────────────────────────
|
|
225
|
+
# 子命令:install
|
|
226
|
+
# ─────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
def cmd_install(
|
|
229
|
+
identifier: str,
|
|
230
|
+
install_dir: str = None,
|
|
231
|
+
llm_force: str = None,
|
|
232
|
+
dry_run: bool = False,
|
|
233
|
+
use_local: bool = False,
|
|
234
|
+
) -> dict:
|
|
235
|
+
"""
|
|
236
|
+
端到端安装:环境检测 → 项目信息 → 生成计划 → 执行 → 错误修复
|
|
237
|
+
新增:预检层 + 多策略回退 + 安全审计 + 许可证检查 + Skills 匹配
|
|
238
|
+
"""
|
|
239
|
+
from resilience import preflight_check, generate_fallback_plans
|
|
240
|
+
|
|
241
|
+
# 1. 获取安装计划
|
|
242
|
+
plan_result = cmd_plan(identifier, llm_force=llm_force, use_local=use_local)
|
|
243
|
+
if plan_result["status"] != "ok":
|
|
244
|
+
return plan_result
|
|
245
|
+
|
|
246
|
+
plan = plan_result["plan"]
|
|
247
|
+
steps = plan.get("steps", [])
|
|
248
|
+
owner = plan_result.get("_owner", "")
|
|
249
|
+
repo = plan_result.get("_repo", "")
|
|
250
|
+
project_types = plan_result.get("_project_types", [])
|
|
251
|
+
dependency_files = plan_result.get("_dependency_files", {})
|
|
252
|
+
env = plan_result.get("_env", {})
|
|
253
|
+
|
|
254
|
+
# 1b. 安全审计:扫描依赖中的 CVE/恶意包/typosquatting
|
|
255
|
+
audit_warnings = []
|
|
256
|
+
if dependency_files:
|
|
257
|
+
try:
|
|
258
|
+
from dependency_audit import audit_project, RISK_CRITICAL, RISK_HIGH
|
|
259
|
+
audit_results = audit_project(dependency_files)
|
|
260
|
+
for ar in audit_results:
|
|
261
|
+
for vuln in ar.vulnerabilities:
|
|
262
|
+
if vuln.risk in (RISK_CRITICAL, RISK_HIGH):
|
|
263
|
+
audit_warnings.append(f" 🚨 [{vuln.risk.upper()}] {vuln.package}: {vuln.description}")
|
|
264
|
+
if audit_warnings:
|
|
265
|
+
logger.warning(f"\n⚠️ 依赖安全审计发现 {len(audit_warnings)} 个高危问题:")
|
|
266
|
+
for w in audit_warnings[:5]:
|
|
267
|
+
logger.warning(w)
|
|
268
|
+
if len(audit_warnings) > 5:
|
|
269
|
+
logger.warning(f" ... 还有 {len(audit_warnings) - 5} 个")
|
|
270
|
+
else:
|
|
271
|
+
logger.info("✅ 依赖安全审计通过")
|
|
272
|
+
except Exception:
|
|
273
|
+
pass # 审计失败不阻塞安装
|
|
274
|
+
|
|
275
|
+
# 1c. 许可证检查
|
|
276
|
+
license_risk = ""
|
|
277
|
+
if owner and repo:
|
|
278
|
+
try:
|
|
279
|
+
from license_check import fetch_license_from_github, analyze_license
|
|
280
|
+
spdx_id, license_text = fetch_license_from_github(owner, repo)
|
|
281
|
+
if spdx_id or license_text:
|
|
282
|
+
lic_result = analyze_license(spdx_id, license_text)
|
|
283
|
+
license_risk = lic_result.risk
|
|
284
|
+
if lic_result.issues:
|
|
285
|
+
logger.info(f"\n📜 许可证({spdx_id or '未知'}):")
|
|
286
|
+
for issue in lic_result.issues[:3]:
|
|
287
|
+
logger.info(f" {issue}")
|
|
288
|
+
else:
|
|
289
|
+
logger.info(f"📜 许可证:{spdx_id or '未知'} ✅")
|
|
290
|
+
except Exception:
|
|
291
|
+
pass # 许可证检查失败不阻塞安装
|
|
292
|
+
|
|
293
|
+
# 1d. Skills 匹配:查找社区安装策略
|
|
294
|
+
matched_skills = []
|
|
295
|
+
try:
|
|
296
|
+
from skills import SkillManager
|
|
297
|
+
sm = SkillManager()
|
|
298
|
+
matched_skills = sm.find_matching_skills(
|
|
299
|
+
owner=owner, repo=repo,
|
|
300
|
+
project_types=project_types,
|
|
301
|
+
file_list=list(dependency_files.keys()),
|
|
302
|
+
)
|
|
303
|
+
if matched_skills:
|
|
304
|
+
skill_names = [s.meta.name for s in matched_skills[:3]]
|
|
305
|
+
logger.info(f"\n🧩 匹配到 {len(matched_skills)} 个 Skills:{', '.join(skill_names)}")
|
|
306
|
+
except Exception:
|
|
307
|
+
pass # Skills 匹配失败不阻塞安装
|
|
308
|
+
|
|
309
|
+
if not steps:
|
|
310
|
+
return {"status": "error", "message": "未能生成有效的安装步骤,请手动查阅 README。"}
|
|
311
|
+
|
|
312
|
+
# 2. 预检层:检查计划中缺失的工具,提前安装
|
|
313
|
+
pf = preflight_check(steps)
|
|
314
|
+
if not pf.all_ready:
|
|
315
|
+
logger.info(f"\n🔧 预检发现 {len(pf.missing_tools)} 个缺失工具:{', '.join(pf.missing_tools)}")
|
|
316
|
+
# 把预检安装步骤插入到计划最前面
|
|
317
|
+
steps = pf.install_commands + steps
|
|
318
|
+
plan["steps"] = steps
|
|
319
|
+
|
|
320
|
+
# 3. 展示计划(dry-run 在此结束)
|
|
321
|
+
logger.info("\n" + "─" * 50)
|
|
322
|
+
logger.info(f"📋 安装计划:{plan_result['project']}")
|
|
323
|
+
logger.info(f" LLM:{plan_result['llm_used']}")
|
|
324
|
+
logger.info("─" * 50)
|
|
325
|
+
for i, step in enumerate(steps, 1):
|
|
326
|
+
warning = step.get("_warning", "")
|
|
327
|
+
warn_icon = "⚠️ " if warning else ""
|
|
328
|
+
logger.info(f" {i}. {warn_icon}{step.get('description', '')}:")
|
|
329
|
+
logger.info(f" $ {step.get('command', '')}")
|
|
330
|
+
if plan.get("launch_command"):
|
|
331
|
+
logger.info(f"\n ▶ 启动命令:{plan['launch_command']}")
|
|
332
|
+
logger.info("─" * 50)
|
|
333
|
+
|
|
334
|
+
if dry_run:
|
|
335
|
+
logger.info("\n(--dry-run 模式,不执行任何命令)")
|
|
336
|
+
return {"status": "ok", "dry_run": True, "plan": plan}
|
|
337
|
+
|
|
338
|
+
# 4. 执行主计划
|
|
339
|
+
llm = create_provider(force=llm_force)
|
|
340
|
+
executor = InstallExecutor(llm_provider=llm, verbose=True)
|
|
341
|
+
|
|
342
|
+
# 调整第一步的工作目录(如果指定了安装目录)
|
|
343
|
+
base_dir = install_dir
|
|
344
|
+
if base_dir:
|
|
345
|
+
base_dir = str(Path(base_dir).expanduser())
|
|
346
|
+
Path(base_dir).mkdir(parents=True, exist_ok=True)
|
|
347
|
+
executor.executor.work_dir = base_dir
|
|
348
|
+
executor.executor._current_dir = base_dir
|
|
349
|
+
|
|
350
|
+
result = executor.execute_plan(plan, project_name=plan_result["project"])
|
|
351
|
+
|
|
352
|
+
# 5. 主计划失败 → 自动触发多策略回退
|
|
353
|
+
if not result.success and owner and repo:
|
|
354
|
+
primary_strategy = plan.get("strategy", "")
|
|
355
|
+
logger.info(f"\n{'='*50}")
|
|
356
|
+
logger.info(f"⚡ 主计划失败(策略:{primary_strategy}),启动多策略回退...")
|
|
357
|
+
|
|
358
|
+
fallback_plans = generate_fallback_plans(owner, repo, project_types, env, dependency_files=dependency_files)
|
|
359
|
+
tried = {primary_strategy}
|
|
360
|
+
|
|
361
|
+
for fb in fallback_plans:
|
|
362
|
+
if fb.strategy in tried:
|
|
363
|
+
continue
|
|
364
|
+
tried.add(fb.strategy)
|
|
365
|
+
|
|
366
|
+
logger.info(f"\n🔄 尝试回退策略 Tier-{fb.tier}:{fb.strategy}(置信度:{fb.confidence})")
|
|
367
|
+
|
|
368
|
+
# 构建回退 plan dict
|
|
369
|
+
fb_plan = {
|
|
370
|
+
"steps": fb.steps,
|
|
371
|
+
"launch_command": plan.get("launch_command", ""),
|
|
372
|
+
"strategy": fb.strategy,
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# 重置执行器状态
|
|
376
|
+
executor.executor.reset(base_dir)
|
|
377
|
+
|
|
378
|
+
fb_result = executor.execute_plan(fb_plan, project_name=plan_result["project"])
|
|
379
|
+
if fb_result.success:
|
|
380
|
+
logger.info(f"\n✅ 回退策略 {fb.strategy} 成功!")
|
|
381
|
+
result = fb_result
|
|
382
|
+
break
|
|
383
|
+
else:
|
|
384
|
+
logger.warning(f" ❌ 回退策略 {fb.strategy} 也失败")
|
|
385
|
+
|
|
386
|
+
# 6. 序列化输出 + 安装遥测
|
|
387
|
+
try:
|
|
388
|
+
from db import record_install_telemetry
|
|
389
|
+
record_install_telemetry(
|
|
390
|
+
project=plan_result["project"],
|
|
391
|
+
strategy=plan.get("strategy", ""),
|
|
392
|
+
gpu_info=plan_result.get("gpu_info"),
|
|
393
|
+
env=env,
|
|
394
|
+
success=result.success,
|
|
395
|
+
error_type="step_failed" if not result.success else None,
|
|
396
|
+
error_message=result.error_summary if not result.success else None,
|
|
397
|
+
duration_sec=None,
|
|
398
|
+
steps_total=len(result.steps),
|
|
399
|
+
steps_completed=sum(1 for s in result.steps if s.success),
|
|
400
|
+
)
|
|
401
|
+
except Exception:
|
|
402
|
+
pass # 遥测失败不影响安装流程
|
|
403
|
+
|
|
404
|
+
# 7. 安装成功 → 记录到 InstallTracker(供 updates/uninstall 使用)
|
|
405
|
+
if result.success and owner and repo:
|
|
406
|
+
try:
|
|
407
|
+
from auto_update import InstallTracker
|
|
408
|
+
tracker = InstallTracker()
|
|
409
|
+
tracker.record_install(
|
|
410
|
+
owner=owner,
|
|
411
|
+
repo=repo,
|
|
412
|
+
install_dir=result.install_dir or install_dir or "",
|
|
413
|
+
)
|
|
414
|
+
logger.info("📝 已记录安装信息(支持 updates/uninstall)")
|
|
415
|
+
except Exception:
|
|
416
|
+
pass # 记录失败不影响安装结果
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
"status": "ok" if result.success else "error",
|
|
420
|
+
"project": result.project,
|
|
421
|
+
"success": result.success,
|
|
422
|
+
"plan_strategy": plan.get("strategy", ""),
|
|
423
|
+
"install_dir": result.install_dir,
|
|
424
|
+
"launch_command": result.launch_command,
|
|
425
|
+
"plan": plan,
|
|
426
|
+
"error_summary": result.error_summary,
|
|
427
|
+
"steps_completed": sum(1 for s in result.steps if s.success),
|
|
428
|
+
"steps_total": len(result.steps),
|
|
429
|
+
"audit_warnings": len(audit_warnings),
|
|
430
|
+
"license_risk": license_risk,
|
|
431
|
+
"matched_skills": [s.meta.name for s in matched_skills],
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
# ─────────────────────────────────────────────
|
|
436
|
+
# 辅助函数
|
|
437
|
+
# ─────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
def _sanitize_for_prompt(text: str) -> str:
|
|
440
|
+
"""
|
|
441
|
+
清洗用户可控文本(README、依赖文件等),防止 prompt 注入。
|
|
442
|
+
移除可能影响 LLM 指令理解的标记。
|
|
443
|
+
"""
|
|
444
|
+
import re as _re
|
|
445
|
+
# 移除常见 prompt 注入标记
|
|
446
|
+
injection_patterns = [
|
|
447
|
+
r'(?i)ignore\s+(?:all\s+)?previous\s+instructions?',
|
|
448
|
+
r'(?i)forget\s+(?:all\s+)?(?:above|previous)',
|
|
449
|
+
r'(?i)you\s+are\s+now\s+(?:a|an)',
|
|
450
|
+
r'(?i)new\s+instructions?\s*:',
|
|
451
|
+
r'(?i)system\s*(?:prompt|message)\s*:',
|
|
452
|
+
r'(?i)assistant\s*(?:prompt|message)\s*:',
|
|
453
|
+
r'(?i)\[INST\]',
|
|
454
|
+
r'(?i)<\|(?:im_start|im_end|system|user|assistant)\|>',
|
|
455
|
+
]
|
|
456
|
+
cleaned = text
|
|
457
|
+
for pat in injection_patterns:
|
|
458
|
+
cleaned = _re.sub(pat, '[FILTERED]', cleaned)
|
|
459
|
+
return cleaned
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _build_plan_prompt(env: dict, info) -> str:
|
|
463
|
+
"""构建发给 LLM 的安装计划请求 prompt"""
|
|
464
|
+
os_info = env.get("os", {})
|
|
465
|
+
gpu_info = env.get("gpu", {})
|
|
466
|
+
pms = env.get("package_managers", {})
|
|
467
|
+
runtimes = env.get("runtimes", {})
|
|
468
|
+
|
|
469
|
+
available_pms = [k for k, v in pms.items() if v.get("available")]
|
|
470
|
+
python_ver = runtimes.get("python", {}).get("version", "unknown")
|
|
471
|
+
has_git = runtimes.get("git", {}).get("available", False)
|
|
472
|
+
has_docker = runtimes.get("docker", {}).get("available", False)
|
|
473
|
+
|
|
474
|
+
# 依赖文件摘要(限制长度防止注入)
|
|
475
|
+
dep_summary = ""
|
|
476
|
+
for fname, content in info.dependency_files.items():
|
|
477
|
+
safe_content = _sanitize_for_prompt(content[:500])
|
|
478
|
+
dep_summary += f"\n### {fname}\n```\n{safe_content}\n```\n"
|
|
479
|
+
|
|
480
|
+
# 清洗 README 内容,防止 prompt 注入
|
|
481
|
+
safe_readme = _sanitize_for_prompt(info.readme[:8000])
|
|
482
|
+
|
|
483
|
+
return f"""
|
|
484
|
+
## 目标项目
|
|
485
|
+
仓库:{info.full_name}
|
|
486
|
+
描述:{info.description}
|
|
487
|
+
主要语言:{info.language}
|
|
488
|
+
项目类型:{', '.join(info.project_type)}
|
|
489
|
+
克隆地址:{info.clone_url}
|
|
490
|
+
|
|
491
|
+
## 当前用户系统环境
|
|
492
|
+
操作系统:{os_info.get('type', 'unknown')} {os_info.get('version', '')} ({os_info.get('arch', '')})
|
|
493
|
+
{"芯片:" + os_info.get('chip', '') if os_info.get('chip') else ''}
|
|
494
|
+
{"WSL2:是" if os_info.get('is_wsl') else ''}
|
|
495
|
+
GPU:{gpu_info.get('name', '无')} - {gpu_info.get('type', 'cpu_only')}
|
|
496
|
+
{"CUDA:" + gpu_info.get('cuda_version', '') if gpu_info.get('cuda_version') else ''}
|
|
497
|
+
Python:{python_ver}
|
|
498
|
+
Git:{'已安装' if has_git else '未安装'}
|
|
499
|
+
Docker:{'已安装' if has_docker else '未安装'}
|
|
500
|
+
可用包管理器:{', '.join(available_pms)}
|
|
501
|
+
|
|
502
|
+
## 项目 README(节选)
|
|
503
|
+
{safe_readme}
|
|
504
|
+
|
|
505
|
+
## 依赖文件
|
|
506
|
+
{dep_summary if dep_summary else "(无检测到的依赖文件)"}
|
|
507
|
+
|
|
508
|
+
## 要求
|
|
509
|
+
请根据以上环境,生成适配 {os_info.get('type', '')} {os_info.get('arch', '')} 的完整安装步骤。
|
|
510
|
+
安装目标目录:~/(用户主目录)
|
|
511
|
+
"""
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _parse_plan_response(response: str) -> dict:
|
|
515
|
+
"""解析 LLM 返回的安装计划 JSON"""
|
|
516
|
+
# 尝试直接解析
|
|
517
|
+
try:
|
|
518
|
+
return json.loads(response)
|
|
519
|
+
except json.JSONDecodeError:
|
|
520
|
+
pass
|
|
521
|
+
|
|
522
|
+
# 提取代码块中的 JSON
|
|
523
|
+
import re
|
|
524
|
+
match = re.search(r'```(?:json)?\n(.*?)```', response, re.DOTALL)
|
|
525
|
+
if match:
|
|
526
|
+
try:
|
|
527
|
+
return json.loads(match.group(1))
|
|
528
|
+
except json.JSONDecodeError:
|
|
529
|
+
pass
|
|
530
|
+
|
|
531
|
+
# 尝试找到第一个 { 开始的 JSON
|
|
532
|
+
start = response.find("{")
|
|
533
|
+
end = response.rfind("}") + 1
|
|
534
|
+
if start >= 0 and end > start:
|
|
535
|
+
try:
|
|
536
|
+
return json.loads(response[start:end])
|
|
537
|
+
except json.JSONDecodeError:
|
|
538
|
+
pass
|
|
539
|
+
|
|
540
|
+
# 无法解析,返回空计划
|
|
541
|
+
return {"project_name": "", "steps": [], "launch_command": ""}
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _validate_plan_schema(plan: dict) -> dict:
|
|
545
|
+
"""
|
|
546
|
+
验证 LLM 返回的 plan 结构,防止 prompt 注入攻击。
|
|
547
|
+
仅保留合法字段,过滤异常数据。
|
|
548
|
+
"""
|
|
549
|
+
safe = {
|
|
550
|
+
"project_name": str(plan.get("project_name", ""))[:200],
|
|
551
|
+
"steps": [],
|
|
552
|
+
"launch_command": str(plan.get("launch_command", ""))[:500],
|
|
553
|
+
}
|
|
554
|
+
for step in plan.get("steps", []):
|
|
555
|
+
if not isinstance(step, dict):
|
|
556
|
+
continue
|
|
557
|
+
s = {
|
|
558
|
+
"description": str(step.get("description", ""))[:500],
|
|
559
|
+
"command": str(step.get("command", ""))[:2000],
|
|
560
|
+
}
|
|
561
|
+
# 只保留非空命令
|
|
562
|
+
if s["command"].strip():
|
|
563
|
+
safe["steps"].append(s)
|
|
564
|
+
return safe
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# ─────────────────────────────────────────────
|
|
568
|
+
# 子命令:doctor(系统诊断)
|
|
569
|
+
# ─────────────────────────────────────────────
|
|
570
|
+
|
|
571
|
+
def cmd_doctor(json_output: bool = False) -> dict:
|
|
572
|
+
"""运行系统诊断"""
|
|
573
|
+
from doctor import run_doctor, format_doctor_report, doctor_to_dict
|
|
574
|
+
|
|
575
|
+
logger.info("🩺 正在运行系统诊断...")
|
|
576
|
+
report = run_doctor()
|
|
577
|
+
|
|
578
|
+
if json_output:
|
|
579
|
+
return doctor_to_dict(report)
|
|
580
|
+
|
|
581
|
+
logger.info(format_doctor_report(report))
|
|
582
|
+
return doctor_to_dict(report)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# ─────────────────────────────────────────────
|
|
586
|
+
# 子命令:skills(插件管理)
|
|
587
|
+
# ─────────────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
def cmd_skills(args) -> dict:
|
|
590
|
+
"""Skills 插件管理"""
|
|
591
|
+
from skills import SkillManager, ensure_builtin_skills, format_skills_list
|
|
592
|
+
|
|
593
|
+
mgr = SkillManager()
|
|
594
|
+
|
|
595
|
+
if args.skills_action == "list":
|
|
596
|
+
skills = mgr.list_skills()
|
|
597
|
+
logger.info(f"\n🔧 已安装 Skills ({len(skills)} 个):")
|
|
598
|
+
logger.info(format_skills_list(skills))
|
|
599
|
+
return {
|
|
600
|
+
"status": "ok",
|
|
601
|
+
"count": len(skills),
|
|
602
|
+
"skills": [{"name": s.meta.name, "version": s.meta.version,
|
|
603
|
+
"description": s.meta.description, "enabled": s.enabled}
|
|
604
|
+
for s in skills],
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
elif args.skills_action == "init":
|
|
608
|
+
ensure_builtin_skills()
|
|
609
|
+
skills = mgr.list_skills()
|
|
610
|
+
logger.info(f"✅ 已初始化 {len(skills)} 个内建 Skills")
|
|
611
|
+
return {"status": "ok", "initialized": len(skills)}
|
|
612
|
+
|
|
613
|
+
elif args.skills_action == "create":
|
|
614
|
+
try:
|
|
615
|
+
path = mgr.create_skill(
|
|
616
|
+
name=args.name,
|
|
617
|
+
description=args.desc,
|
|
618
|
+
steps=[],
|
|
619
|
+
)
|
|
620
|
+
logger.info(f"✅ Skill '{args.name}' 已创建: {path}")
|
|
621
|
+
return {"status": "ok", "name": args.name, "path": str(path)}
|
|
622
|
+
except (ValueError, FileExistsError) as e:
|
|
623
|
+
return {"status": "error", "message": str(e)}
|
|
624
|
+
|
|
625
|
+
elif args.skills_action == "remove":
|
|
626
|
+
if mgr.remove_skill(args.name):
|
|
627
|
+
logger.info(f"✅ Skill '{args.name}' 已删除")
|
|
628
|
+
return {"status": "ok", "removed": args.name}
|
|
629
|
+
return {"status": "error", "message": f"Skill '{args.name}' 不存在"}
|
|
630
|
+
|
|
631
|
+
elif args.skills_action == "export":
|
|
632
|
+
data = mgr.export_skill(args.name)
|
|
633
|
+
if data:
|
|
634
|
+
return {"status": "ok", "skill_data": data}
|
|
635
|
+
return {"status": "error", "message": f"Skill '{args.name}' 不存在"}
|
|
636
|
+
|
|
637
|
+
return {"status": "error", "message": "未知 skills 子命令"}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# ─────────────────────────────────────────────
|
|
641
|
+
# 子命令:config(配置管理)
|
|
642
|
+
# ─────────────────────────────────────────────
|
|
643
|
+
|
|
644
|
+
def cmd_config(args) -> dict:
|
|
645
|
+
"""配置文件管理"""
|
|
646
|
+
from config_schema import load_and_validate, format_validation_result
|
|
647
|
+
from onboard import CONFIG_FILE
|
|
648
|
+
|
|
649
|
+
if args.config_action == "show":
|
|
650
|
+
result = load_and_validate()
|
|
651
|
+
# 隐藏敏感信息
|
|
652
|
+
display_config = dict(result.config)
|
|
653
|
+
for key in ["github_token"]:
|
|
654
|
+
if display_config.get(key):
|
|
655
|
+
display_config[key] = display_config[key][:8] + "..."
|
|
656
|
+
if display_config.get("llm_key"):
|
|
657
|
+
display_config["llm_key"] = {k: v[:8] + "..." for k, v in display_config["llm_key"].items()}
|
|
658
|
+
logger.info(json.dumps(display_config, indent=2, ensure_ascii=False))
|
|
659
|
+
return {"status": "ok", "config": display_config}
|
|
660
|
+
|
|
661
|
+
elif args.config_action == "validate":
|
|
662
|
+
result = load_and_validate()
|
|
663
|
+
logger.info(format_validation_result(result))
|
|
664
|
+
return {
|
|
665
|
+
"status": "ok" if result.valid else "error",
|
|
666
|
+
"valid": result.valid,
|
|
667
|
+
"errors": [{"path": e.path, "message": e.message} for e in result.errors],
|
|
668
|
+
"warnings": result.warnings,
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
elif args.config_action == "path":
|
|
672
|
+
logger.info(str(CONFIG_FILE))
|
|
673
|
+
return {"status": "ok", "path": str(CONFIG_FILE)}
|
|
674
|
+
|
|
675
|
+
return {"status": "error", "message": "未知 config 子命令"}
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
# ─────────────────────────────────────────────
|
|
679
|
+
# 子命令:platforms(平台列表)
|
|
680
|
+
# ─────────────────────────────────────────────
|
|
681
|
+
|
|
682
|
+
def cmd_platforms() -> dict:
|
|
683
|
+
"""列出支持的代码托管平台"""
|
|
684
|
+
from multi_source import get_supported_platforms
|
|
685
|
+
|
|
686
|
+
platforms = get_supported_platforms()
|
|
687
|
+
logger.info("\n📦 支持的代码托管平台:")
|
|
688
|
+
for p in platforms:
|
|
689
|
+
token_status = ""
|
|
690
|
+
if p["env_token"]:
|
|
691
|
+
import os
|
|
692
|
+
has_token = bool(os.getenv(p["env_token"], "").strip())
|
|
693
|
+
token_status = " ✅" if has_token else f" (设置 {p['env_token']} 提升配额)"
|
|
694
|
+
logger.info(f" • {p['name']:12s} {p['domain']}{token_status}")
|
|
695
|
+
logger.info(" 用法: gitinstall install <platform-url>/owner/repo")
|
|
696
|
+
logger.info(" 示例: gitinstall install gitlab.com/inkscape/inkscape")
|
|
697
|
+
return {"status": "ok", "platforms": platforms}
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
# ─────────────────────────────────────────────
|
|
701
|
+
# 子命令:audit(依赖安全审计)
|
|
702
|
+
# ─────────────────────────────────────────────
|
|
703
|
+
|
|
704
|
+
def cmd_audit(identifier: str, online: bool = False) -> dict:
|
|
705
|
+
"""审计项目依赖的安全风险"""
|
|
706
|
+
from dependency_audit import audit_project, format_audit_results, audit_to_dict
|
|
707
|
+
|
|
708
|
+
logger.info(f"🔍 正在审计 {identifier} 的依赖安全...")
|
|
709
|
+
try:
|
|
710
|
+
info = fetch_project(identifier)
|
|
711
|
+
if not info.dependency_files:
|
|
712
|
+
logger.warning(" ⚠️ 未找到依赖文件")
|
|
713
|
+
return {"status": "ok", "message": "未找到依赖文件", "results": []}
|
|
714
|
+
|
|
715
|
+
results = audit_project(info.dependency_files, online=online)
|
|
716
|
+
logger.info(format_audit_results(results))
|
|
717
|
+
return {"status": "ok", **audit_to_dict(results)}
|
|
718
|
+
except Exception as e:
|
|
719
|
+
return {"status": "error", "message": f"审计失败:{e}"}
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
# ─────────────────────────────────────────────
|
|
723
|
+
# 子命令:license(许可证检查)
|
|
724
|
+
# ─────────────────────────────────────────────
|
|
725
|
+
|
|
726
|
+
def cmd_license(identifier: str) -> dict:
|
|
727
|
+
"""检查项目许可证兼容性"""
|
|
728
|
+
from license_check import (
|
|
729
|
+
analyze_license, format_license_result, license_to_dict,
|
|
730
|
+
fetch_license_from_github,
|
|
731
|
+
)
|
|
732
|
+
from fetcher import parse_repo_identifier
|
|
733
|
+
|
|
734
|
+
logger.info(f"📜 正在检查 {identifier} 的许可证...")
|
|
735
|
+
try:
|
|
736
|
+
owner, repo = parse_repo_identifier(identifier)
|
|
737
|
+
spdx_id, license_text = fetch_license_from_github(owner, repo)
|
|
738
|
+
|
|
739
|
+
if not spdx_id and not license_text:
|
|
740
|
+
logger.warning(" ⚠️ 未找到许可证信息")
|
|
741
|
+
return {"status": "ok", "message": "项目未声明许可证", "risk": "warning"}
|
|
742
|
+
|
|
743
|
+
result = analyze_license(spdx_id, license_text)
|
|
744
|
+
logger.info(format_license_result(result))
|
|
745
|
+
return {"status": "ok", **license_to_dict(result)}
|
|
746
|
+
except Exception as e:
|
|
747
|
+
return {"status": "error", "message": f"许可证检查失败:{e}"}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
# ─────────────────────────────────────────────
|
|
751
|
+
# 子命令:updates(更新检查)
|
|
752
|
+
# ─────────────────────────────────────────────
|
|
753
|
+
|
|
754
|
+
def cmd_updates(args) -> dict:
|
|
755
|
+
"""管理已安装项目和更新检查"""
|
|
756
|
+
from auto_update import (
|
|
757
|
+
InstallTracker, check_all_updates,
|
|
758
|
+
format_installed_list, format_update_results, updates_to_dict,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
tracker = InstallTracker()
|
|
762
|
+
|
|
763
|
+
if args.updates_action == "list":
|
|
764
|
+
projects = tracker.list_installed()
|
|
765
|
+
logger.info(format_installed_list(projects))
|
|
766
|
+
return {
|
|
767
|
+
"status": "ok",
|
|
768
|
+
"installed": [p.to_dict() for p in projects],
|
|
769
|
+
"total": len(projects),
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
elif args.updates_action == "check":
|
|
773
|
+
logger.info("🔄 正在检查所有项目的更新...")
|
|
774
|
+
results = check_all_updates(tracker)
|
|
775
|
+
logger.info(format_update_results(results))
|
|
776
|
+
return {"status": "ok", **updates_to_dict(results)}
|
|
777
|
+
|
|
778
|
+
elif args.updates_action == "remove":
|
|
779
|
+
name = args.name
|
|
780
|
+
parts = name.split("/")
|
|
781
|
+
if len(parts) != 2:
|
|
782
|
+
return {"status": "error", "message": f"格式错误:应为 owner/repo,收到 '{name}'"}
|
|
783
|
+
ok = tracker.remove_project(parts[0], parts[1])
|
|
784
|
+
if ok:
|
|
785
|
+
logger.info(f" ✅ 已移除 {name} 的记录")
|
|
786
|
+
else:
|
|
787
|
+
logger.warning(f" ❌ 未找到 {name}")
|
|
788
|
+
return {"status": "ok" if ok else "error", "removed": ok}
|
|
789
|
+
|
|
790
|
+
return {"status": "error", "message": "未知子命令"}
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
# ─────────────────────────────────────────────
|
|
794
|
+
# 子命令:resume(断点恢复)
|
|
795
|
+
# ─────────────────────────────────────────────
|
|
796
|
+
|
|
797
|
+
def cmd_resume(identifier: str = None, llm_force: str = None,
|
|
798
|
+
install_dir: str = None) -> dict:
|
|
799
|
+
"""恢复中断的安装"""
|
|
800
|
+
from checkpoint import CheckpointManager, format_checkpoint_list, format_resume_plan
|
|
801
|
+
|
|
802
|
+
mgr = CheckpointManager()
|
|
803
|
+
|
|
804
|
+
if not identifier:
|
|
805
|
+
# 列出可恢复的安装
|
|
806
|
+
resumable = mgr.get_resumable()
|
|
807
|
+
if not resumable:
|
|
808
|
+
logger.info(" ✅ 没有中断的安装任务")
|
|
809
|
+
return {"status": "ok", "resumable": []}
|
|
810
|
+
logger.info(format_checkpoint_list(resumable))
|
|
811
|
+
return {
|
|
812
|
+
"status": "ok",
|
|
813
|
+
"resumable": [cp.project for cp in resumable],
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
# 找到指定项目的断点
|
|
817
|
+
from fetcher import parse_repo_identifier
|
|
818
|
+
try:
|
|
819
|
+
owner, repo = parse_repo_identifier(identifier)
|
|
820
|
+
except Exception as e:
|
|
821
|
+
return {"status": "error", "message": f"无法解析项目: {e}"}
|
|
822
|
+
|
|
823
|
+
cp = mgr.get_checkpoint(owner, repo)
|
|
824
|
+
if not cp:
|
|
825
|
+
return {"status": "error", "message": f"未找到 {owner}/{repo} 的断点记录"}
|
|
826
|
+
|
|
827
|
+
resume_idx = mgr.get_resume_step(owner, repo)
|
|
828
|
+
if resume_idx is None:
|
|
829
|
+
return {"status": "ok", "message": "该安装已完成,无需恢复"}
|
|
830
|
+
|
|
831
|
+
logger.info(format_resume_plan(cp, resume_idx))
|
|
832
|
+
|
|
833
|
+
# 从断点恢复执行
|
|
834
|
+
remaining_steps = [s for s in cp.steps[resume_idx:]
|
|
835
|
+
if s.status in ("pending", "failed")]
|
|
836
|
+
if not remaining_steps:
|
|
837
|
+
return {"status": "ok", "message": "所有步骤已完成"}
|
|
838
|
+
|
|
839
|
+
plan = {
|
|
840
|
+
"steps": [{"command": s.command, "description": s.description}
|
|
841
|
+
for s in remaining_steps],
|
|
842
|
+
"launch_command": cp.plan.get("launch_command", ""),
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
result = cmd_install(identifier, install_dir=install_dir,
|
|
846
|
+
llm_force=llm_force)
|
|
847
|
+
return result
|
|
848
|
+
|
|
849
|
+
|
|
850
|
+
# ─────────────────────────────────────────────
|
|
851
|
+
# 子命令:flags(功能开关)
|
|
852
|
+
# ─────────────────────────────────────────────
|
|
853
|
+
|
|
854
|
+
def cmd_flags(args) -> dict:
|
|
855
|
+
"""查看/管理功能开关"""
|
|
856
|
+
from feature_flags import get_all_status, format_flags_table, list_flags, is_enabled
|
|
857
|
+
|
|
858
|
+
if args.flags_action == "list":
|
|
859
|
+
group = getattr(args, "group", None)
|
|
860
|
+
if group:
|
|
861
|
+
flags = list_flags(group)
|
|
862
|
+
logger.info(f"\n🚩 功能开关({group} 组):")
|
|
863
|
+
for f in flags:
|
|
864
|
+
icon = "✅" if is_enabled(f.name) else "❌"
|
|
865
|
+
logger.info(f" {icon} {f.name}: {f.description}")
|
|
866
|
+
return {"status": "ok", "flags": [{"name": f.name, "enabled": is_enabled(f.name), "description": f.description} for f in flags]}
|
|
867
|
+
status = get_all_status()
|
|
868
|
+
logger.info(format_flags_table())
|
|
869
|
+
return {"status": "ok", "flags": status}
|
|
870
|
+
|
|
871
|
+
elif args.flags_action == "show":
|
|
872
|
+
status = get_all_status()
|
|
873
|
+
logger.info(format_flags_table())
|
|
874
|
+
return {"status": "ok", "flags": status}
|
|
875
|
+
|
|
876
|
+
return {"status": "error", "message": "未知 flags 子命令"}
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
# ─────────────────────────────────────────────
|
|
880
|
+
# 子命令:registry(安装器注册表)
|
|
881
|
+
# ─────────────────────────────────────────────
|
|
882
|
+
|
|
883
|
+
def cmd_registry(args) -> dict:
|
|
884
|
+
"""查看安装器注册表"""
|
|
885
|
+
from installer_registry import InstallerRegistry
|
|
886
|
+
|
|
887
|
+
registry = InstallerRegistry()
|
|
888
|
+
|
|
889
|
+
if args.registry_action == "list":
|
|
890
|
+
all_installers = registry.list_all()
|
|
891
|
+
available = registry.list_available()
|
|
892
|
+
logger.info(registry.format_registry())
|
|
893
|
+
return {
|
|
894
|
+
"status": "ok",
|
|
895
|
+
"total": len(all_installers),
|
|
896
|
+
"available": [i.info.name for i in available],
|
|
897
|
+
"installers": registry.to_dict(),
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
elif args.registry_action == "detect":
|
|
901
|
+
available = registry.list_available()
|
|
902
|
+
logger.info(f"\n🔧 检测到 {len(available)} 个可用安装器:")
|
|
903
|
+
for inst in available:
|
|
904
|
+
logger.info(f" ✅ {inst.info.name} v{inst.info.version or '?'}")
|
|
905
|
+
return {
|
|
906
|
+
"status": "ok",
|
|
907
|
+
"available": [{
|
|
908
|
+
"name": inst.info.name,
|
|
909
|
+
"version": inst.info.version or "",
|
|
910
|
+
"ecosystems": inst.info.ecosystems,
|
|
911
|
+
} for inst in available],
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return {"status": "error", "message": "未知 registry 子命令"}
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
# ─────────────────────────────────────────────
|
|
918
|
+
# 子命令:events(事件历史)
|
|
919
|
+
# ─────────────────────────────────────────────
|
|
920
|
+
|
|
921
|
+
def cmd_events(args) -> dict:
|
|
922
|
+
"""查看事件历史"""
|
|
923
|
+
from event_bus import get_event_bus
|
|
924
|
+
|
|
925
|
+
bus = get_event_bus()
|
|
926
|
+
event_type = getattr(args, "type", None)
|
|
927
|
+
limit = getattr(args, "limit", 50)
|
|
928
|
+
history = bus.get_history(event_type=event_type, limit=limit)
|
|
929
|
+
|
|
930
|
+
if not history:
|
|
931
|
+
logger.info(" 📭 暂无事件记录")
|
|
932
|
+
return {"status": "ok", "events": [], "total": 0}
|
|
933
|
+
|
|
934
|
+
logger.info(f"\n📡 事件历史(最近 {len(history)} 条):")
|
|
935
|
+
for evt in history:
|
|
936
|
+
logger.info(f" [{evt.timestamp}] {evt.event_type} - {evt.project}")
|
|
937
|
+
if evt.data:
|
|
938
|
+
for k, v in list(evt.data.items())[:3]:
|
|
939
|
+
logger.info(f" {k}: {v}")
|
|
940
|
+
|
|
941
|
+
return {
|
|
942
|
+
"status": "ok",
|
|
943
|
+
"events": [e.to_dict() for e in history],
|
|
944
|
+
"total": len(history),
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
# ─────────────────────────────────────────────
|
|
949
|
+
# 子命令:chain(依赖链)
|
|
950
|
+
# ─────────────────────────────────────────────
|
|
951
|
+
|
|
952
|
+
def cmd_chain(identifier: str, llm_force: str = None,
|
|
953
|
+
use_local: bool = False) -> dict:
|
|
954
|
+
"""可视化项目安装依赖链"""
|
|
955
|
+
from dep_chain import build_chain_from_plan, format_dep_chain
|
|
956
|
+
|
|
957
|
+
plan_result = cmd_plan(identifier, llm_force=llm_force, use_local=use_local)
|
|
958
|
+
if plan_result["status"] != "ok":
|
|
959
|
+
return plan_result
|
|
960
|
+
|
|
961
|
+
plan = plan_result["plan"]
|
|
962
|
+
chain = build_chain_from_plan(plan)
|
|
963
|
+
logger.info(format_dep_chain(chain))
|
|
964
|
+
|
|
965
|
+
return {
|
|
966
|
+
"status": "ok",
|
|
967
|
+
"project": plan_result.get("project", identifier),
|
|
968
|
+
"chain": chain.to_dict(),
|
|
969
|
+
"has_cycle": chain.has_cycle(),
|
|
970
|
+
"node_count": len(chain.nodes),
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
# ─────────────────────────────────────────────
|
|
975
|
+
# 子命令:kb(安装知识库)
|
|
976
|
+
# ─────────────────────────────────────────────
|
|
977
|
+
|
|
978
|
+
def cmd_kb(args) -> dict:
|
|
979
|
+
"""管理安装知识库"""
|
|
980
|
+
from knowledge_base import KnowledgeBase, format_kb_stats, format_search_results
|
|
981
|
+
|
|
982
|
+
kb = KnowledgeBase()
|
|
983
|
+
|
|
984
|
+
if args.kb_action == "stats":
|
|
985
|
+
stats = kb.get_stats()
|
|
986
|
+
logger.info(format_kb_stats(stats))
|
|
987
|
+
return {"status": "ok", **stats}
|
|
988
|
+
|
|
989
|
+
elif args.kb_action == "search":
|
|
990
|
+
query = getattr(args, "query", "")
|
|
991
|
+
if not query:
|
|
992
|
+
return {"status": "error", "message": "请指定搜索关键词"}
|
|
993
|
+
results = kb.search(project=query)
|
|
994
|
+
logger.info(format_search_results(results))
|
|
995
|
+
return {
|
|
996
|
+
"status": "ok",
|
|
997
|
+
"results": [{
|
|
998
|
+
"project": r.entry.project,
|
|
999
|
+
"score": r.score,
|
|
1000
|
+
"success": r.entry.success,
|
|
1001
|
+
"strategy": r.entry.strategy,
|
|
1002
|
+
"reasons": r.match_reasons,
|
|
1003
|
+
} for r in results],
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
elif args.kb_action == "rate":
|
|
1007
|
+
project = getattr(args, "project", "")
|
|
1008
|
+
rate_info = kb.get_success_rate(project)
|
|
1009
|
+
rate_pct = f"{rate_info['rate']:.1%}"
|
|
1010
|
+
logger.info(f" 📊 成功率:{rate_pct}({rate_info['success']}/{rate_info['total']})")
|
|
1011
|
+
return {"status": "ok", **rate_info}
|
|
1012
|
+
|
|
1013
|
+
return {"status": "error", "message": "未知 kb 子命令"}
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
# ─────────────────────────────────────────────
|
|
1017
|
+
# 子命令:autopilot(批量安装)
|
|
1018
|
+
# ─────────────────────────────────────────────
|
|
1019
|
+
|
|
1020
|
+
def cmd_autopilot(args) -> dict:
|
|
1021
|
+
"""批量自动安装"""
|
|
1022
|
+
from autopilot import (
|
|
1023
|
+
parse_project_list, run_autopilot, resume_autopilot,
|
|
1024
|
+
format_batch_result,
|
|
1025
|
+
)
|
|
1026
|
+
|
|
1027
|
+
if getattr(args, "autopilot_action", "") == "resume":
|
|
1028
|
+
logger.info("🚗 恢复上次自动驾驶...")
|
|
1029
|
+
result = resume_autopilot(
|
|
1030
|
+
llm_force=getattr(args, "llm", None),
|
|
1031
|
+
install_dir=getattr(args, "dir", None),
|
|
1032
|
+
)
|
|
1033
|
+
if not result:
|
|
1034
|
+
return {"status": "error", "message": "没有可恢复的自动驾驶任务"}
|
|
1035
|
+
logger.info(format_batch_result(result))
|
|
1036
|
+
return {"status": "ok", **result.to_dict()}
|
|
1037
|
+
|
|
1038
|
+
# run
|
|
1039
|
+
source = getattr(args, "projects", "")
|
|
1040
|
+
if not source:
|
|
1041
|
+
return {"status": "error", "message": "请指定项目列表或文件"}
|
|
1042
|
+
|
|
1043
|
+
projects = parse_project_list(source)
|
|
1044
|
+
if not projects:
|
|
1045
|
+
return {"status": "error", "message": f"未能解析出有效项目:{source}"}
|
|
1046
|
+
|
|
1047
|
+
logger.info(f"🚗 自动驾驶模式:{len(projects)} 个项目")
|
|
1048
|
+
for i, p in enumerate(projects, 1):
|
|
1049
|
+
logger.info(f" {i}. {p}")
|
|
1050
|
+
|
|
1051
|
+
dry_run = getattr(args, "dry_run", False)
|
|
1052
|
+
if dry_run:
|
|
1053
|
+
return {
|
|
1054
|
+
"status": "ok",
|
|
1055
|
+
"dry_run": True,
|
|
1056
|
+
"projects": projects,
|
|
1057
|
+
"total": len(projects),
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
result = run_autopilot(
|
|
1061
|
+
projects,
|
|
1062
|
+
install_dir=getattr(args, "dir", None),
|
|
1063
|
+
llm_force=getattr(args, "llm", None),
|
|
1064
|
+
)
|
|
1065
|
+
logger.info(format_batch_result(result))
|
|
1066
|
+
return {"status": "ok", **result.to_dict()}
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
# ─────────────────────────────────────────────
|
|
1070
|
+
# 子命令:uninstall(安全卸载)
|
|
1071
|
+
# ─────────────────────────────────────────────
|
|
1072
|
+
|
|
1073
|
+
def cmd_uninstall(identifier: str, keep_config: bool = False,
|
|
1074
|
+
clean_only: bool = False, confirm: bool = False) -> dict:
|
|
1075
|
+
"""安全卸载已安装的项目"""
|
|
1076
|
+
from auto_update import InstallTracker
|
|
1077
|
+
from uninstaller import (
|
|
1078
|
+
plan_uninstall, execute_uninstall,
|
|
1079
|
+
format_uninstall_plan, uninstall_to_dict,
|
|
1080
|
+
)
|
|
1081
|
+
from fetcher import parse_repo_identifier
|
|
1082
|
+
|
|
1083
|
+
try:
|
|
1084
|
+
owner, repo = parse_repo_identifier(identifier)
|
|
1085
|
+
except Exception as e:
|
|
1086
|
+
return {"status": "error", "message": f"无法解析项目: {e}"}
|
|
1087
|
+
|
|
1088
|
+
tracker = InstallTracker()
|
|
1089
|
+
project = tracker.get_project(owner, repo)
|
|
1090
|
+
|
|
1091
|
+
if not project:
|
|
1092
|
+
return {"status": "error", "message": f"未找到 {owner}/{repo} 的安装记录。使用 'gitinstall updates list' 查看已安装项目。"}
|
|
1093
|
+
|
|
1094
|
+
plan = plan_uninstall(
|
|
1095
|
+
owner, repo, project.install_dir,
|
|
1096
|
+
keep_config=keep_config, clean_only=clean_only,
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
logger.info(format_uninstall_plan(plan))
|
|
1100
|
+
|
|
1101
|
+
if plan.error:
|
|
1102
|
+
return {"status": "error", **uninstall_to_dict(plan)}
|
|
1103
|
+
|
|
1104
|
+
if not confirm:
|
|
1105
|
+
logger.warning("\n ⚠️ 添加 --confirm 确认执行卸载")
|
|
1106
|
+
return {"status": "ok", "action": "dry_run", **uninstall_to_dict(plan)}
|
|
1107
|
+
|
|
1108
|
+
# 执行卸载
|
|
1109
|
+
result = execute_uninstall(plan, keep_config=keep_config)
|
|
1110
|
+
if result["success"]:
|
|
1111
|
+
tracker.remove_project(owner, repo)
|
|
1112
|
+
logger.info(f"\n ✅ 已卸载 {owner}/{repo},释放 {result['freed_mb']} MB")
|
|
1113
|
+
else:
|
|
1114
|
+
logger.warning(f"\n ⚠️ 部分清理失败: {result['errors']}")
|
|
1115
|
+
|
|
1116
|
+
return {"status": "ok" if result["success"] else "partial", **result}
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
# ─────────────────────────────────────────────
|
|
1120
|
+
# CLI 入口
|
|
1121
|
+
# ─────────────────────────────────────────────
|
|
1122
|
+
|
|
1123
|
+
def main():
|
|
1124
|
+
import argparse
|
|
1125
|
+
|
|
1126
|
+
parser = argparse.ArgumentParser(
|
|
1127
|
+
prog="gitinstall",
|
|
1128
|
+
description="一句话安装任何开源项目(支持 GitHub/GitLab/Bitbucket/Gitee/Codeberg/本地路径)",
|
|
1129
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1130
|
+
epilog="""
|
|
1131
|
+
示例:
|
|
1132
|
+
python main.py web # 启动 Web 界面(推荐)
|
|
1133
|
+
python main.py detect
|
|
1134
|
+
python main.py plan comfyanonymous/ComfyUI
|
|
1135
|
+
python main.py install comfyanonymous/ComfyUI
|
|
1136
|
+
python main.py install gitlab.com/user/project # GitLab 项目
|
|
1137
|
+
python main.py install gitee.com/user/project # Gitee 国内项目
|
|
1138
|
+
python main.py install ./my-local-project # 本地目录(无需网络)
|
|
1139
|
+
python main.py install ~/projects/my-app # 本地绝对路径
|
|
1140
|
+
python main.py install comfyanonymous/ComfyUI --dir ~/AI --llm groq
|
|
1141
|
+
python main.py install comfyanonymous/ComfyUI --dry-run
|
|
1142
|
+
python main.py doctor # 系统诊断
|
|
1143
|
+
python main.py onboard # 交互式引导
|
|
1144
|
+
python main.py skills list # 查看已安装 Skills
|
|
1145
|
+
python main.py audit pytorch/pytorch # 依赖安全审计
|
|
1146
|
+
python main.py license torvalds/linux # 许可证检查
|
|
1147
|
+
python main.py updates check # 检查已安装项目更新
|
|
1148
|
+
python main.py uninstall owner/repo --confirm # 安全卸载项目
|
|
1149
|
+
python main.py validate # 验证 Top 100 兼容性
|
|
1150
|
+
python main.py validate --quick # 仅验证新入榜项目
|
|
1151
|
+
python main.py resume # 列出可恢复的安装
|
|
1152
|
+
python main.py flags list # 查看功能开关
|
|
1153
|
+
python main.py registry list # 查看安装器注册表
|
|
1154
|
+
python main.py events # 查看事件历史
|
|
1155
|
+
python main.py chain owner/repo # 可视化依赖链
|
|
1156
|
+
python main.py kb stats # 知识库统计
|
|
1157
|
+
python main.py autopilot run projects.txt # 批量自动安装
|
|
1158
|
+
""",
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
1162
|
+
|
|
1163
|
+
# detect
|
|
1164
|
+
subparsers.add_parser("detect", help="检测当前系统环境")
|
|
1165
|
+
|
|
1166
|
+
# fetch
|
|
1167
|
+
fetch_p = subparsers.add_parser("fetch", help="获取项目信息(支持多平台)")
|
|
1168
|
+
fetch_p.add_argument("project", help="URL / owner/repo(支持 GitHub/GitLab/Bitbucket/Gitee/Codeberg)")
|
|
1169
|
+
|
|
1170
|
+
# plan
|
|
1171
|
+
plan_p = subparsers.add_parser("plan", help="生成安装计划(不执行)")
|
|
1172
|
+
plan_p.add_argument("project", help="URL / owner/repo / 本地路径(支持多平台)")
|
|
1173
|
+
plan_p.add_argument("--llm", default=None, help="指定 LLM: anthropic/openai/groq/ollama/lmstudio/none")
|
|
1174
|
+
plan_p.add_argument("--local", action="store_true", help="本地模式:git clone 后本地分析(不消耗 API 配额)")
|
|
1175
|
+
|
|
1176
|
+
# install
|
|
1177
|
+
install_p = subparsers.add_parser("install", help="安装开源项目(支持多平台)")
|
|
1178
|
+
install_p.add_argument("project", help="URL / owner/repo / 本地路径(支持 GitHub/GitLab/Bitbucket/Gitee/Codeberg/本地目录)")
|
|
1179
|
+
install_p.add_argument("--dir", default=None, help="指定安装目录(默认 ~/项目名)")
|
|
1180
|
+
install_p.add_argument("--llm", default=None, help="指定 LLM: anthropic/openai/groq/ollama/lmstudio/none")
|
|
1181
|
+
install_p.add_argument("--local", action="store_true", help="本地模式:git clone 后本地分析(不消耗 API 配额)")
|
|
1182
|
+
install_p.add_argument("--dry-run", action="store_true", help="只生成计划,不执行")
|
|
1183
|
+
|
|
1184
|
+
# doctor
|
|
1185
|
+
doctor_p = subparsers.add_parser("doctor", help="🩺 系统诊断(检查环境、API、缓存、GPU)")
|
|
1186
|
+
doctor_p.add_argument("--json", action="store_true", dest="json_output", help="输出 JSON 格式")
|
|
1187
|
+
|
|
1188
|
+
# onboard
|
|
1189
|
+
subparsers.add_parser("onboard", help="🦀 交互式引导向导(首次使用推荐)")
|
|
1190
|
+
|
|
1191
|
+
# skills
|
|
1192
|
+
skills_p = subparsers.add_parser("skills", help="🔧 Skills 插件管理")
|
|
1193
|
+
skills_sub = skills_p.add_subparsers(dest="skills_action", required=True)
|
|
1194
|
+
skills_sub.add_parser("list", help="列出已安装的 Skills")
|
|
1195
|
+
skills_sub.add_parser("init", help="初始化内建 Skills")
|
|
1196
|
+
skills_create = skills_sub.add_parser("create", help="创建新 Skill")
|
|
1197
|
+
skills_create.add_argument("name", help="Skill 名称(小写字母+连字符)")
|
|
1198
|
+
skills_create.add_argument("--desc", required=True, help="Skill 描述")
|
|
1199
|
+
skills_remove = skills_sub.add_parser("remove", help="删除 Skill")
|
|
1200
|
+
skills_remove.add_argument("name", help="要删除的 Skill 名称")
|
|
1201
|
+
skills_export = skills_sub.add_parser("export", help="导出 Skill 为 JSON")
|
|
1202
|
+
skills_export.add_argument("name", help="要导出的 Skill 名称")
|
|
1203
|
+
|
|
1204
|
+
# config
|
|
1205
|
+
config_p = subparsers.add_parser("config", help="⚙️ 配置管理")
|
|
1206
|
+
config_sub = config_p.add_subparsers(dest="config_action", required=True)
|
|
1207
|
+
config_sub.add_parser("show", help="显示当前配置")
|
|
1208
|
+
config_sub.add_parser("validate", help="验证配置文件")
|
|
1209
|
+
config_sub.add_parser("path", help="显示配置文件路径")
|
|
1210
|
+
|
|
1211
|
+
# platforms
|
|
1212
|
+
subparsers.add_parser("platforms", help="📦 列出支持的代码托管平台")
|
|
1213
|
+
|
|
1214
|
+
# audit
|
|
1215
|
+
audit_p = subparsers.add_parser("audit", help="🔒 依赖安全审计(CVE/误植/废弃包)")
|
|
1216
|
+
audit_p.add_argument("project", help="URL / owner/repo")
|
|
1217
|
+
audit_p.add_argument("--online", action="store_true", help="查询在线漏洞数据库(更全面但较慢)")
|
|
1218
|
+
|
|
1219
|
+
# license
|
|
1220
|
+
license_p = subparsers.add_parser("license", help="📜 许可证兼容性检查")
|
|
1221
|
+
license_p.add_argument("project", help="URL / owner/repo")
|
|
1222
|
+
|
|
1223
|
+
# updates
|
|
1224
|
+
updates_p = subparsers.add_parser("updates", help="🔄 已安装项目更新管理")
|
|
1225
|
+
updates_sub = updates_p.add_subparsers(dest="updates_action", required=True)
|
|
1226
|
+
updates_sub.add_parser("list", help="列出已安装项目")
|
|
1227
|
+
updates_sub.add_parser("check", help="检查所有项目更新")
|
|
1228
|
+
updates_remove = updates_sub.add_parser("remove", help="移除安装记录")
|
|
1229
|
+
updates_remove.add_argument("name", help="owner/repo")
|
|
1230
|
+
|
|
1231
|
+
# uninstall
|
|
1232
|
+
uninstall_p = subparsers.add_parser("uninstall", help="🗑️ 安全卸载已安装项目")
|
|
1233
|
+
uninstall_p.add_argument("project", help="owner/repo")
|
|
1234
|
+
uninstall_p.add_argument("--keep-config", action="store_true", help="保留配置文件")
|
|
1235
|
+
uninstall_p.add_argument("--clean-only", action="store_true", help="仅清理缓存和编译产物")
|
|
1236
|
+
uninstall_p.add_argument("--confirm", action="store_true", help="确认执行卸载(否则仅预览)")
|
|
1237
|
+
|
|
1238
|
+
# mcp
|
|
1239
|
+
subparsers.add_parser("mcp", help="🤖 启动 MCP 服务器(供 Claude Desktop / Cursor 等 AI 工具调用)")
|
|
1240
|
+
|
|
1241
|
+
# schema
|
|
1242
|
+
schema_p = subparsers.add_parser("schema", help="📋 输出 AI 工具调用 Schema(OpenAI/Anthropic/Gemini/JSON)")
|
|
1243
|
+
schema_p.add_argument("--format", default="openai",
|
|
1244
|
+
choices=["openai", "anthropic", "gemini", "json_schema"],
|
|
1245
|
+
help="Schema 格式(默认: openai,兼容 Ollama/vLLM/LM Studio/任意 OpenAI 兼容 API)")
|
|
1246
|
+
|
|
1247
|
+
# web
|
|
1248
|
+
web_p = subparsers.add_parser("web", help="启动 Web 图形界面(推荐)")
|
|
1249
|
+
web_p.add_argument("--port", type=int, default=8080, help="端口号 (默认: 8080)")
|
|
1250
|
+
web_p.add_argument("--host", default="", help="绑定地址 (默认: 127.0.0.1,生产环境用 0.0.0.0)")
|
|
1251
|
+
web_p.add_argument("--no-open", action="store_true", help="不自动打开浏览器")
|
|
1252
|
+
|
|
1253
|
+
# validate
|
|
1254
|
+
val_p = subparsers.add_parser("validate", help="验证 Top 100 热门项目兼容性(内部 CI)")
|
|
1255
|
+
val_p.add_argument("--quick", action="store_true", help="仅验证新入榜项目")
|
|
1256
|
+
val_p.add_argument("--report", action="store_true", help="仅显示上次验证报告")
|
|
1257
|
+
val_p.add_argument("--category", default=None, help="按分类过滤: AI/Web/工具/IoT")
|
|
1258
|
+
|
|
1259
|
+
# resume
|
|
1260
|
+
resume_p = subparsers.add_parser("resume", help="🔄 恢复中断的安装(断点续装)")
|
|
1261
|
+
resume_p.add_argument("project", nargs="?", default=None, help="owner/repo(不指定则列出可恢复的)")
|
|
1262
|
+
resume_p.add_argument("--dir", default=None, help="安装目录")
|
|
1263
|
+
resume_p.add_argument("--llm", default=None, help="指定 LLM")
|
|
1264
|
+
|
|
1265
|
+
# flags
|
|
1266
|
+
flags_p = subparsers.add_parser("flags", help="🚩 功能开关管理")
|
|
1267
|
+
flags_sub = flags_p.add_subparsers(dest="flags_action", required=True)
|
|
1268
|
+
flags_list = flags_sub.add_parser("list", help="列出所有功能开关")
|
|
1269
|
+
flags_list.add_argument("--group", default=None, help="按组过滤: security/experimental/performance/general")
|
|
1270
|
+
flags_sub.add_parser("show", help="显示功能开关状态")
|
|
1271
|
+
|
|
1272
|
+
# registry
|
|
1273
|
+
registry_p = subparsers.add_parser("registry", help="🔧 安装器注册表")
|
|
1274
|
+
registry_sub = registry_p.add_subparsers(dest="registry_action", required=True)
|
|
1275
|
+
registry_sub.add_parser("list", help="列出所有安装器")
|
|
1276
|
+
registry_sub.add_parser("detect", help="检测可用安装器")
|
|
1277
|
+
|
|
1278
|
+
# events
|
|
1279
|
+
events_p = subparsers.add_parser("events", help="📡 安装事件历史")
|
|
1280
|
+
events_p.add_argument("--type", default=None, help="筛选事件类型")
|
|
1281
|
+
events_p.add_argument("--limit", type=int, default=50, help="最大条数")
|
|
1282
|
+
|
|
1283
|
+
# chain
|
|
1284
|
+
chain_p = subparsers.add_parser("chain", help="🔗 可视化安装依赖链")
|
|
1285
|
+
chain_p.add_argument("project", help="owner/repo")
|
|
1286
|
+
chain_p.add_argument("--llm", default=None, help="指定 LLM")
|
|
1287
|
+
chain_p.add_argument("--local", action="store_true", help="本地模式")
|
|
1288
|
+
|
|
1289
|
+
# kb
|
|
1290
|
+
kb_p = subparsers.add_parser("kb", help="📚 安装知识库")
|
|
1291
|
+
kb_sub = kb_p.add_subparsers(dest="kb_action", required=True)
|
|
1292
|
+
kb_sub.add_parser("stats", help="知识库统计")
|
|
1293
|
+
kb_search = kb_sub.add_parser("search", help="搜索相似安装案例")
|
|
1294
|
+
kb_search.add_argument("query", help="搜索关键词(project/type/language)")
|
|
1295
|
+
kb_rate = kb_sub.add_parser("rate", help="查看项目安装成功率")
|
|
1296
|
+
kb_rate.add_argument("project", nargs="?", default="", help="owner/repo(不指定则全局)")
|
|
1297
|
+
|
|
1298
|
+
# autopilot
|
|
1299
|
+
autopilot_p = subparsers.add_parser("autopilot", help="🚗 批量自动安装")
|
|
1300
|
+
autopilot_sub = autopilot_p.add_subparsers(dest="autopilot_action", required=True)
|
|
1301
|
+
ap_run = autopilot_sub.add_parser("run", help="执行批量安装")
|
|
1302
|
+
ap_run.add_argument("projects", help="项目列表(文件路径 / 空格分隔 owner/repo)")
|
|
1303
|
+
ap_run.add_argument("--dir", default=None, help="安装目录")
|
|
1304
|
+
ap_run.add_argument("--llm", default=None, help="指定 LLM")
|
|
1305
|
+
ap_run.add_argument("--dry-run", action="store_true", help="仅预览")
|
|
1306
|
+
autopilot_sub.add_parser("resume", help="恢复上次自动驾驶")
|
|
1307
|
+
|
|
1308
|
+
args = parser.parse_args()
|
|
1309
|
+
|
|
1310
|
+
# 路由到对应命令
|
|
1311
|
+
if args.command == "detect":
|
|
1312
|
+
result = cmd_detect()
|
|
1313
|
+
elif args.command == "fetch":
|
|
1314
|
+
result = cmd_fetch(args.project)
|
|
1315
|
+
elif args.command == "plan":
|
|
1316
|
+
result = cmd_plan(args.project, llm_force=args.llm, use_local=args.local)
|
|
1317
|
+
elif args.command == "install":
|
|
1318
|
+
result = cmd_install(
|
|
1319
|
+
args.project,
|
|
1320
|
+
install_dir=args.dir,
|
|
1321
|
+
llm_force=args.llm,
|
|
1322
|
+
dry_run=args.dry_run,
|
|
1323
|
+
use_local=args.local,
|
|
1324
|
+
)
|
|
1325
|
+
elif args.command == "doctor":
|
|
1326
|
+
result = cmd_doctor(json_output=args.json_output)
|
|
1327
|
+
elif args.command == "onboard":
|
|
1328
|
+
from onboard import run_onboard
|
|
1329
|
+
run_onboard()
|
|
1330
|
+
return
|
|
1331
|
+
elif args.command == "skills":
|
|
1332
|
+
result = cmd_skills(args)
|
|
1333
|
+
elif args.command == "config":
|
|
1334
|
+
result = cmd_config(args)
|
|
1335
|
+
elif args.command == "platforms":
|
|
1336
|
+
result = cmd_platforms()
|
|
1337
|
+
elif args.command == "audit":
|
|
1338
|
+
result = cmd_audit(args.project, online=args.online)
|
|
1339
|
+
elif args.command == "license":
|
|
1340
|
+
result = cmd_license(args.project)
|
|
1341
|
+
elif args.command == "updates":
|
|
1342
|
+
result = cmd_updates(args)
|
|
1343
|
+
elif args.command == "uninstall":
|
|
1344
|
+
result = cmd_uninstall(
|
|
1345
|
+
args.project,
|
|
1346
|
+
keep_config=args.keep_config,
|
|
1347
|
+
clean_only=args.clean_only,
|
|
1348
|
+
confirm=args.confirm,
|
|
1349
|
+
)
|
|
1350
|
+
elif args.command == "mcp":
|
|
1351
|
+
from mcp_server import serve
|
|
1352
|
+
serve()
|
|
1353
|
+
return
|
|
1354
|
+
elif args.command == "schema":
|
|
1355
|
+
from tool_schemas import to_json
|
|
1356
|
+
print(to_json(args.format))
|
|
1357
|
+
return
|
|
1358
|
+
elif args.command == "web":
|
|
1359
|
+
from web import start_server
|
|
1360
|
+
start_server(port=args.port, host=args.host, open_browser=not args.no_open)
|
|
1361
|
+
return
|
|
1362
|
+
elif args.command == "validate":
|
|
1363
|
+
from validate_top100 import cmd_validate
|
|
1364
|
+
result = cmd_validate(
|
|
1365
|
+
quick=args.quick,
|
|
1366
|
+
report_only=args.report,
|
|
1367
|
+
category=args.category,
|
|
1368
|
+
)
|
|
1369
|
+
elif args.command == "resume":
|
|
1370
|
+
result = cmd_resume(
|
|
1371
|
+
identifier=args.project,
|
|
1372
|
+
llm_force=args.llm,
|
|
1373
|
+
install_dir=args.dir,
|
|
1374
|
+
)
|
|
1375
|
+
elif args.command == "flags":
|
|
1376
|
+
result = cmd_flags(args)
|
|
1377
|
+
elif args.command == "registry":
|
|
1378
|
+
result = cmd_registry(args)
|
|
1379
|
+
elif args.command == "events":
|
|
1380
|
+
result = cmd_events(args)
|
|
1381
|
+
elif args.command == "chain":
|
|
1382
|
+
result = cmd_chain(args.project, llm_force=args.llm, use_local=args.local)
|
|
1383
|
+
elif args.command == "kb":
|
|
1384
|
+
result = cmd_kb(args)
|
|
1385
|
+
elif args.command == "autopilot":
|
|
1386
|
+
result = cmd_autopilot(args)
|
|
1387
|
+
else:
|
|
1388
|
+
parser.print_help()
|
|
1389
|
+
sys.exit(1)
|
|
1390
|
+
|
|
1391
|
+
# 输出 JSON(OpenClaw 读取 stdout)
|
|
1392
|
+
_output_and_exit(result)
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
def _output_and_exit(result: dict):
|
|
1396
|
+
"""CLI 专用:输出 JSON 并根据状态设置退出码。SDK 不调用此函数。"""
|
|
1397
|
+
import json as _json
|
|
1398
|
+
print(_json.dumps(result, ensure_ascii=False, indent=2))
|
|
1399
|
+
if result.get("status") == "error":
|
|
1400
|
+
sys.exit(1)
|
|
1401
|
+
|
|
1402
|
+
|
|
1403
|
+
# pyproject.toml 入口别名
|
|
1404
|
+
cli_main = main
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
if __name__ == "__main__":
|
|
1408
|
+
main()
|