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/education.py
ADDED
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
"""
|
|
2
|
+
education.py — 教育科技集成引擎
|
|
3
|
+
================================
|
|
4
|
+
|
|
5
|
+
目标市场:教育科技(~$35亿,★★★☆☆)
|
|
6
|
+
|
|
7
|
+
功能:
|
|
8
|
+
1. 课堂模式(Classroom Mode)— 批量配置学生环境
|
|
9
|
+
2. 作业分发 & 回收(基于 GitHub Classroom 模式)
|
|
10
|
+
3. 分步引导安装(Step-by-step Guided Install)
|
|
11
|
+
4. 项目难度评级(Difficulty Rating)
|
|
12
|
+
5. 学习路径生成(Learning Path)
|
|
13
|
+
6. Jupyter Notebook 集成
|
|
14
|
+
7. 进度追踪 Dashboard
|
|
15
|
+
|
|
16
|
+
零外部依赖,纯 Python 标准库。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Optional
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ─────────────────────────────────────────────
|
|
32
|
+
# 数据结构
|
|
33
|
+
# ─────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class DifficultyRating:
|
|
37
|
+
"""项目难度评级"""
|
|
38
|
+
level: int = 1 # 1-5 星
|
|
39
|
+
label: str = "初学者" # 初学者/基础/中级/进阶/专家
|
|
40
|
+
factors: dict[str, int] = field(default_factory=dict)
|
|
41
|
+
setup_time_min: int = 5 # 预估安装时间(分钟)
|
|
42
|
+
prereqs: list[str] = field(default_factory=list) # 前置知识
|
|
43
|
+
explanation: str = ""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class GuidedStep:
|
|
48
|
+
"""引导安装步骤"""
|
|
49
|
+
step_num: int = 0
|
|
50
|
+
title: str = ""
|
|
51
|
+
description: str = ""
|
|
52
|
+
command: str = ""
|
|
53
|
+
expected_output: str = ""
|
|
54
|
+
troubleshooting: list[str] = field(default_factory=list)
|
|
55
|
+
is_optional: bool = False
|
|
56
|
+
checkpoint: str = "" # 验证命令
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ClassroomConfig:
|
|
61
|
+
"""课堂配置"""
|
|
62
|
+
classroom_id: str = ""
|
|
63
|
+
name: str = ""
|
|
64
|
+
instructor: str = ""
|
|
65
|
+
created_at: str = ""
|
|
66
|
+
projects: list[str] = field(default_factory=list) # GitHub repo IDs
|
|
67
|
+
student_count: int = 0
|
|
68
|
+
python_version: str = ""
|
|
69
|
+
extra_packages: list[str] = field(default_factory=list)
|
|
70
|
+
deadline: str = ""
|
|
71
|
+
notes: str = ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class StudentProgress:
|
|
76
|
+
"""学生进度"""
|
|
77
|
+
student_id: str = ""
|
|
78
|
+
classroom_id: str = ""
|
|
79
|
+
project: str = ""
|
|
80
|
+
steps_completed: int = 0
|
|
81
|
+
total_steps: int = 0
|
|
82
|
+
started_at: str = ""
|
|
83
|
+
completed_at: str = ""
|
|
84
|
+
errors: list[str] = field(default_factory=list)
|
|
85
|
+
status: str = "not_started" # not_started | in_progress | completed | stuck
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class LearningPath:
|
|
90
|
+
"""学习路径"""
|
|
91
|
+
path_id: str = ""
|
|
92
|
+
title: str = ""
|
|
93
|
+
description: str = ""
|
|
94
|
+
difficulty: str = "" # 初学者/中级/高级
|
|
95
|
+
projects: list[dict] = field(default_factory=list)
|
|
96
|
+
estimated_hours: float = 0.0
|
|
97
|
+
skills_gained: list[str] = field(default_factory=list)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ─────────────────────────────────────────────
|
|
101
|
+
# 项目难度评级
|
|
102
|
+
# ─────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
_DIFFICULTY_LABELS = {1: "初学者", 2: "基础", 3: "中级", 4: "进阶", 5: "专家"}
|
|
105
|
+
|
|
106
|
+
# 语言/技术栈复杂度权重
|
|
107
|
+
_LANG_COMPLEXITY = {
|
|
108
|
+
"python": 1, "javascript": 1, "html": 1, "markdown": 0,
|
|
109
|
+
"typescript": 2, "java": 2, "go": 2, "ruby": 1,
|
|
110
|
+
"rust": 4, "c": 3, "cpp": 4, "haskell": 4, "scala": 3,
|
|
111
|
+
"cuda": 5, "assembly": 5, "vhdl": 5,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# 工具复杂度
|
|
115
|
+
_TOOL_COMPLEXITY = {
|
|
116
|
+
"docker": 2, "kubernetes": 4, "terraform": 3,
|
|
117
|
+
"cmake": 3, "make": 2, "bazel": 3,
|
|
118
|
+
"conda": 1, "pip": 1, "npm": 1, "cargo": 2,
|
|
119
|
+
"gradle": 2, "maven": 2,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def rate_difficulty(project_dir: str, project_types: list[str] | None = None) -> DifficultyRating:
|
|
124
|
+
"""
|
|
125
|
+
评估项目安装难度(1-5 星)。
|
|
126
|
+
|
|
127
|
+
维度:
|
|
128
|
+
- 语言复杂度
|
|
129
|
+
- 依赖数量
|
|
130
|
+
- 需要外部工具 (Docker, CUDA, etc.)
|
|
131
|
+
- 配置步骤数
|
|
132
|
+
- 文档完整性
|
|
133
|
+
"""
|
|
134
|
+
root = Path(project_dir)
|
|
135
|
+
factors = {}
|
|
136
|
+
|
|
137
|
+
# 1. 语言复杂度
|
|
138
|
+
lang_score = 1
|
|
139
|
+
if project_types:
|
|
140
|
+
for pt in project_types:
|
|
141
|
+
pt_lower = pt.lower()
|
|
142
|
+
for lang, score in _LANG_COMPLEXITY.items():
|
|
143
|
+
if lang in pt_lower:
|
|
144
|
+
lang_score = max(lang_score, score)
|
|
145
|
+
factors["language"] = lang_score
|
|
146
|
+
|
|
147
|
+
# 2. 依赖数量
|
|
148
|
+
dep_count = 0
|
|
149
|
+
for dep_file in ("requirements.txt", "package.json", "Cargo.toml", "go.mod"):
|
|
150
|
+
fp = root / dep_file
|
|
151
|
+
if fp.exists():
|
|
152
|
+
try:
|
|
153
|
+
content = fp.read_text(encoding="utf-8", errors="ignore")
|
|
154
|
+
if dep_file == "requirements.txt":
|
|
155
|
+
dep_count += len([l for l in content.splitlines()
|
|
156
|
+
if l.strip() and not l.startswith("#")])
|
|
157
|
+
elif dep_file == "package.json":
|
|
158
|
+
data = json.loads(content)
|
|
159
|
+
for s in ("dependencies", "devDependencies"):
|
|
160
|
+
dep_count += len(data.get(s, {}))
|
|
161
|
+
elif dep_file == "Cargo.toml":
|
|
162
|
+
dep_count += content.count(" = ")
|
|
163
|
+
elif dep_file == "go.mod":
|
|
164
|
+
dep_count += content.count("\n\t")
|
|
165
|
+
except (OSError, json.JSONDecodeError):
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
dep_score = 1 if dep_count < 5 else 2 if dep_count < 15 else 3 if dep_count < 40 else 4 if dep_count < 100 else 5
|
|
169
|
+
factors["dependencies"] = dep_score
|
|
170
|
+
|
|
171
|
+
# 3. 外部工具需求
|
|
172
|
+
tool_score = 1
|
|
173
|
+
readme_text = ""
|
|
174
|
+
for rname in ("README.md", "README.rst", "README"):
|
|
175
|
+
rp = root / rname
|
|
176
|
+
if rp.exists():
|
|
177
|
+
try:
|
|
178
|
+
readme_text = rp.read_text(encoding="utf-8", errors="ignore")[:30000]
|
|
179
|
+
except OSError:
|
|
180
|
+
pass
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
if (root / "Dockerfile").exists() or "docker" in readme_text.lower():
|
|
184
|
+
tool_score = max(tool_score, 2)
|
|
185
|
+
factors["docker"] = 2
|
|
186
|
+
if "cuda" in readme_text.lower() or "gpu" in readme_text.lower():
|
|
187
|
+
tool_score = max(tool_score, 3)
|
|
188
|
+
factors["gpu_required"] = 3
|
|
189
|
+
if (root / "CMakeLists.txt").exists() or "cmake" in readme_text.lower():
|
|
190
|
+
tool_score = max(tool_score, 3)
|
|
191
|
+
factors["cmake"] = 3
|
|
192
|
+
if "kubernetes" in readme_text.lower() or "k8s" in readme_text.lower():
|
|
193
|
+
tool_score = max(tool_score, 4)
|
|
194
|
+
factors["kubernetes"] = 4
|
|
195
|
+
|
|
196
|
+
factors["tools"] = tool_score
|
|
197
|
+
|
|
198
|
+
# 4. 配置步骤数(从 README 推断)
|
|
199
|
+
setup_steps = len(re.findall(r'```(?:bash|shell|sh)', readme_text, re.IGNORECASE))
|
|
200
|
+
step_score = 1 if setup_steps <= 2 else 2 if setup_steps <= 5 else 3 if setup_steps <= 10 else 4
|
|
201
|
+
factors["setup_steps"] = step_score
|
|
202
|
+
|
|
203
|
+
# 5. 文档完整性(好文档降低难度)
|
|
204
|
+
doc_score = 0
|
|
205
|
+
if re.search(r'##.*install|##.*setup|##.*getting.started', readme_text, re.IGNORECASE):
|
|
206
|
+
doc_score += 1
|
|
207
|
+
if re.search(r'##.*usage|##.*example|##.*tutorial', readme_text, re.IGNORECASE):
|
|
208
|
+
doc_score += 1
|
|
209
|
+
if (root / "docs").is_dir() or (root / "doc").is_dir():
|
|
210
|
+
doc_score += 1
|
|
211
|
+
doc_penalty = max(0, 3 - doc_score) # 文档好 → 减分
|
|
212
|
+
factors["doc_penalty"] = doc_penalty
|
|
213
|
+
|
|
214
|
+
# 综合评分
|
|
215
|
+
raw_score = (
|
|
216
|
+
lang_score * 0.25 +
|
|
217
|
+
dep_score * 0.15 +
|
|
218
|
+
tool_score * 0.25 +
|
|
219
|
+
step_score * 0.15 +
|
|
220
|
+
doc_penalty * 0.20
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
level = max(1, min(5, round(raw_score)))
|
|
224
|
+
|
|
225
|
+
# 前置知识
|
|
226
|
+
prereqs = []
|
|
227
|
+
if lang_score >= 3:
|
|
228
|
+
prereqs.append("编程经验 (进阶)")
|
|
229
|
+
elif lang_score >= 2:
|
|
230
|
+
prereqs.append("基础编程")
|
|
231
|
+
if tool_score >= 3:
|
|
232
|
+
prereqs.append("命令行操作")
|
|
233
|
+
if "gpu_required" in factors:
|
|
234
|
+
prereqs.append("GPU 驱动 & CUDA")
|
|
235
|
+
if "docker" in factors:
|
|
236
|
+
prereqs.append("Docker 基础")
|
|
237
|
+
|
|
238
|
+
# 估算安装时间
|
|
239
|
+
time_min = 5 + dep_count // 5 + setup_steps * 2 + (10 if tool_score >= 3 else 0)
|
|
240
|
+
|
|
241
|
+
return DifficultyRating(
|
|
242
|
+
level=level,
|
|
243
|
+
label=_DIFFICULTY_LABELS.get(level, "未知"),
|
|
244
|
+
factors=factors,
|
|
245
|
+
setup_time_min=time_min,
|
|
246
|
+
prereqs=prereqs,
|
|
247
|
+
explanation=_explain_difficulty(level, factors),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _explain_difficulty(level: int, factors: dict) -> str:
|
|
252
|
+
"""生成难度解释"""
|
|
253
|
+
if level <= 1:
|
|
254
|
+
return "📗 非常简单,初学者友好,几分钟即可完成安装"
|
|
255
|
+
elif level == 2:
|
|
256
|
+
return "📘 基础级别,需要基本命令行知识"
|
|
257
|
+
elif level == 3:
|
|
258
|
+
return "📙 中等难度,需要一定开发经验和工具链知识"
|
|
259
|
+
elif level == 4:
|
|
260
|
+
return "📕 较高难度,需要进阶技能和特定工具链"
|
|
261
|
+
else:
|
|
262
|
+
return "📓 专家级,需要深厚系统知识和专业工具链"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def format_difficulty(rating: DifficultyRating) -> str:
|
|
266
|
+
"""格式化难度评级"""
|
|
267
|
+
stars = "⭐" * rating.level + "☆" * (5 - rating.level)
|
|
268
|
+
lines = [
|
|
269
|
+
f"难度: {stars} [{rating.label}]",
|
|
270
|
+
f" 预估安装时间: ~{rating.setup_time_min} 分钟",
|
|
271
|
+
rating.explanation,
|
|
272
|
+
]
|
|
273
|
+
if rating.prereqs:
|
|
274
|
+
lines.append(f" 前置知识: {', '.join(rating.prereqs)}")
|
|
275
|
+
return "\n".join(lines)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ─────────────────────────────────────────────
|
|
279
|
+
# 分步引导安装
|
|
280
|
+
# ─────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
def generate_guided_steps(
|
|
283
|
+
project_dir: str,
|
|
284
|
+
plan: dict[str, Any],
|
|
285
|
+
difficulty: DifficultyRating | None = None,
|
|
286
|
+
) -> list[GuidedStep]:
|
|
287
|
+
"""
|
|
288
|
+
从安装计划生成分步引导教程。
|
|
289
|
+
|
|
290
|
+
每步包含:标题、说明、命令、预期输出、排错提示。
|
|
291
|
+
"""
|
|
292
|
+
steps = []
|
|
293
|
+
step_num = 0
|
|
294
|
+
|
|
295
|
+
# Step 0: 前置检查
|
|
296
|
+
step_num += 1
|
|
297
|
+
steps.append(GuidedStep(
|
|
298
|
+
step_num=step_num,
|
|
299
|
+
title="环境检查",
|
|
300
|
+
description="首先确认您的系统是否满足要求",
|
|
301
|
+
command="python --version && git --version",
|
|
302
|
+
expected_output="Python 3.x.x\ngit version 2.x.x",
|
|
303
|
+
troubleshooting=[
|
|
304
|
+
"如果 python 命令不存在,请安装 Python: https://python.org",
|
|
305
|
+
"如果 git 不存在,请安装 Git: https://git-scm.com",
|
|
306
|
+
],
|
|
307
|
+
checkpoint="python --version",
|
|
308
|
+
))
|
|
309
|
+
|
|
310
|
+
# Step 1: 克隆项目
|
|
311
|
+
repo = plan.get("repo", "")
|
|
312
|
+
if repo:
|
|
313
|
+
step_num += 1
|
|
314
|
+
steps.append(GuidedStep(
|
|
315
|
+
step_num=step_num,
|
|
316
|
+
title="获取代码",
|
|
317
|
+
description=f"从 GitHub 克隆项目代码到本地",
|
|
318
|
+
command=f"git clone https://github.com/{repo}.git && cd {repo.split('/')[-1] if '/' in repo else repo}",
|
|
319
|
+
expected_output="Cloning into '...'...\ndone.",
|
|
320
|
+
troubleshooting=[
|
|
321
|
+
"如果速度慢,可使用镜像: git clone https://ghproxy.com/https://github.com/{repo}.git",
|
|
322
|
+
"如果 SSH key 有问题,使用 HTTPS 方式克隆",
|
|
323
|
+
],
|
|
324
|
+
))
|
|
325
|
+
|
|
326
|
+
# Step 2+: 从 plan steps 生成
|
|
327
|
+
plan_steps = plan.get("steps", [])
|
|
328
|
+
for ps in plan_steps:
|
|
329
|
+
cmds = ps.get("commands", [])
|
|
330
|
+
label = ps.get("label", "")
|
|
331
|
+
note = ps.get("note", "")
|
|
332
|
+
|
|
333
|
+
for cmd in cmds:
|
|
334
|
+
step_num += 1
|
|
335
|
+
cmd_str = cmd if isinstance(cmd, str) else str(cmd)
|
|
336
|
+
|
|
337
|
+
# 根据命令类型生成说明
|
|
338
|
+
description = note or _describe_command(cmd_str)
|
|
339
|
+
troubleshooting = _troubleshoot_command(cmd_str)
|
|
340
|
+
|
|
341
|
+
steps.append(GuidedStep(
|
|
342
|
+
step_num=step_num,
|
|
343
|
+
title=label or f"步骤 {step_num}",
|
|
344
|
+
description=description,
|
|
345
|
+
command=cmd_str,
|
|
346
|
+
expected_output=_expected_output(cmd_str),
|
|
347
|
+
troubleshooting=troubleshooting,
|
|
348
|
+
is_optional=ps.get("optional", False),
|
|
349
|
+
))
|
|
350
|
+
|
|
351
|
+
# 最后: 验证
|
|
352
|
+
step_num += 1
|
|
353
|
+
steps.append(GuidedStep(
|
|
354
|
+
step_num=step_num,
|
|
355
|
+
title="验证安装",
|
|
356
|
+
description="确认项目安装成功",
|
|
357
|
+
command=plan.get("verify_command", "echo '安装完成!'"),
|
|
358
|
+
expected_output="安装完成!",
|
|
359
|
+
checkpoint=plan.get("verify_command", ""),
|
|
360
|
+
))
|
|
361
|
+
|
|
362
|
+
return steps
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _describe_command(cmd: str) -> str:
|
|
366
|
+
"""根据命令生成中文描述"""
|
|
367
|
+
cmd_lower = cmd.lower()
|
|
368
|
+
if "pip install" in cmd_lower:
|
|
369
|
+
return "安装 Python 依赖包"
|
|
370
|
+
if "npm install" in cmd_lower:
|
|
371
|
+
return "安装 Node.js 依赖包"
|
|
372
|
+
if "conda" in cmd_lower:
|
|
373
|
+
return "使用 Conda 配置环境"
|
|
374
|
+
if "docker" in cmd_lower:
|
|
375
|
+
return "使用 Docker 构建/运行容器"
|
|
376
|
+
if "cmake" in cmd_lower:
|
|
377
|
+
return "使用 CMake 构建项目"
|
|
378
|
+
if "make" in cmd_lower:
|
|
379
|
+
return "编译项目"
|
|
380
|
+
if "cargo" in cmd_lower:
|
|
381
|
+
return "使用 Cargo 构建 Rust 项目"
|
|
382
|
+
if "python" in cmd_lower and "venv" in cmd_lower:
|
|
383
|
+
return "创建 Python 虚拟环境"
|
|
384
|
+
if "git clone" in cmd_lower:
|
|
385
|
+
return "克隆代码仓库"
|
|
386
|
+
if "apt" in cmd_lower or "brew" in cmd_lower:
|
|
387
|
+
return "安装系统依赖"
|
|
388
|
+
return f"执行: {cmd[:60]}..."
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _troubleshoot_command(cmd: str) -> list[str]:
|
|
392
|
+
"""为命令生成排错提示"""
|
|
393
|
+
tips = []
|
|
394
|
+
cmd_lower = cmd.lower()
|
|
395
|
+
|
|
396
|
+
if "pip install" in cmd_lower:
|
|
397
|
+
tips.extend([
|
|
398
|
+
"如果权限错误,添加 --user 参数或使用虚拟环境",
|
|
399
|
+
"如果编译失败,可能需要安装 C/C++ 编译器",
|
|
400
|
+
"国内用户可使用清华镜像: pip install -i https://pypi.tuna.tsinghua.edu.cn/simple ...",
|
|
401
|
+
])
|
|
402
|
+
elif "npm install" in cmd_lower:
|
|
403
|
+
tips.extend([
|
|
404
|
+
"如果权限错误,不要使用 sudo,改用 nvm 管理 Node",
|
|
405
|
+
"国内用户可配置淘宝镜像: npm config set registry https://registry.npmmirror.com",
|
|
406
|
+
])
|
|
407
|
+
elif "docker" in cmd_lower:
|
|
408
|
+
tips.extend([
|
|
409
|
+
"确保 Docker Desktop 已启动",
|
|
410
|
+
"Linux 用户需要将当前用户加入 docker 组: sudo usermod -aG docker $USER",
|
|
411
|
+
])
|
|
412
|
+
elif "cargo" in cmd_lower:
|
|
413
|
+
tips.append("确保已安装 Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh")
|
|
414
|
+
elif "cmake" in cmd_lower:
|
|
415
|
+
tips.extend([
|
|
416
|
+
"macOS: brew install cmake",
|
|
417
|
+
"Ubuntu: sudo apt install cmake",
|
|
418
|
+
])
|
|
419
|
+
|
|
420
|
+
return tips
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _expected_output(cmd: str) -> str:
|
|
424
|
+
"""预估命令的预期输出"""
|
|
425
|
+
cmd_lower = cmd.lower()
|
|
426
|
+
if "pip install" in cmd_lower:
|
|
427
|
+
return "Successfully installed ..."
|
|
428
|
+
if "npm install" in cmd_lower:
|
|
429
|
+
return "added X packages in Xs"
|
|
430
|
+
if "cmake" in cmd_lower:
|
|
431
|
+
return "-- Build files have been written to: ..."
|
|
432
|
+
if "make" in cmd_lower:
|
|
433
|
+
return "[100%] Built target ..."
|
|
434
|
+
return ""
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def format_guided_steps(steps: list[GuidedStep]) -> str:
|
|
438
|
+
"""格式化引导步骤为美观的文本"""
|
|
439
|
+
lines = [
|
|
440
|
+
"🎓 分步安装指南",
|
|
441
|
+
"=" * 50,
|
|
442
|
+
"",
|
|
443
|
+
]
|
|
444
|
+
for s in steps:
|
|
445
|
+
optional_tag = " [可选]" if s.is_optional else ""
|
|
446
|
+
lines.extend([
|
|
447
|
+
f"📍 步骤 {s.step_num}: {s.title}{optional_tag}",
|
|
448
|
+
f" {s.description}",
|
|
449
|
+
"",
|
|
450
|
+
f" $ {s.command}",
|
|
451
|
+
"",
|
|
452
|
+
])
|
|
453
|
+
if s.expected_output:
|
|
454
|
+
lines.append(f" 预期输出: {s.expected_output}")
|
|
455
|
+
lines.append("")
|
|
456
|
+
if s.troubleshooting:
|
|
457
|
+
lines.append(" ⚠️ 常见问题:")
|
|
458
|
+
for t in s.troubleshooting:
|
|
459
|
+
lines.append(f" • {t}")
|
|
460
|
+
lines.append("")
|
|
461
|
+
lines.append("─" * 50)
|
|
462
|
+
lines.append("")
|
|
463
|
+
|
|
464
|
+
return "\n".join(lines)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
# ─────────────────────────────────────────────
|
|
468
|
+
# 课堂管理
|
|
469
|
+
# ─────────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
_CLASSROOM_DIR = os.path.expanduser("~/.gitinstall/classrooms")
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def create_classroom(
|
|
475
|
+
name: str,
|
|
476
|
+
instructor: str,
|
|
477
|
+
projects: list[str],
|
|
478
|
+
student_count: int = 30,
|
|
479
|
+
python_version: str = "",
|
|
480
|
+
extra_packages: list[str] | None = None,
|
|
481
|
+
deadline: str = "",
|
|
482
|
+
) -> ClassroomConfig:
|
|
483
|
+
"""创建课堂配置"""
|
|
484
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
485
|
+
cid = hashlib.sha256(f"{name}-{instructor}-{now}".encode()).hexdigest()[:10]
|
|
486
|
+
|
|
487
|
+
config = ClassroomConfig(
|
|
488
|
+
classroom_id=cid,
|
|
489
|
+
name=name,
|
|
490
|
+
instructor=instructor,
|
|
491
|
+
created_at=now,
|
|
492
|
+
projects=projects,
|
|
493
|
+
student_count=student_count,
|
|
494
|
+
python_version=python_version,
|
|
495
|
+
extra_packages=extra_packages or [],
|
|
496
|
+
deadline=deadline,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# 持久化
|
|
500
|
+
os.makedirs(_CLASSROOM_DIR, exist_ok=True)
|
|
501
|
+
path = os.path.join(_CLASSROOM_DIR, f"{cid}.json")
|
|
502
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
503
|
+
json.dump({
|
|
504
|
+
"classroom_id": config.classroom_id,
|
|
505
|
+
"name": config.name,
|
|
506
|
+
"instructor": config.instructor,
|
|
507
|
+
"created_at": config.created_at,
|
|
508
|
+
"projects": config.projects,
|
|
509
|
+
"student_count": config.student_count,
|
|
510
|
+
"python_version": config.python_version,
|
|
511
|
+
"extra_packages": config.extra_packages,
|
|
512
|
+
"deadline": config.deadline,
|
|
513
|
+
"notes": config.notes,
|
|
514
|
+
}, f, indent=2, ensure_ascii=False)
|
|
515
|
+
|
|
516
|
+
return config
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def generate_student_setup_script(classroom: ClassroomConfig) -> str:
|
|
520
|
+
"""
|
|
521
|
+
为课堂生成学生一键安装脚本。
|
|
522
|
+
|
|
523
|
+
学生只需运行一个脚本即可配置好全部环境。
|
|
524
|
+
"""
|
|
525
|
+
lines = [
|
|
526
|
+
"#!/bin/bash",
|
|
527
|
+
f"# 课堂环境配置脚本 — {classroom.name}",
|
|
528
|
+
f"# 讲师: {classroom.instructor}",
|
|
529
|
+
f"# 生成时间: {classroom.created_at}",
|
|
530
|
+
"",
|
|
531
|
+
'set -e',
|
|
532
|
+
'echo "🎓 开始配置课堂环境..."',
|
|
533
|
+
"",
|
|
534
|
+
]
|
|
535
|
+
|
|
536
|
+
# Python 版本检查
|
|
537
|
+
if classroom.python_version:
|
|
538
|
+
lines.extend([
|
|
539
|
+
f'echo "检查 Python 版本..."',
|
|
540
|
+
f'PYTHON_VERSION=$(python3 --version 2>&1 | grep -oP "\\d+\\.\\d+")',
|
|
541
|
+
f'echo "当前 Python: $PYTHON_VERSION"',
|
|
542
|
+
"",
|
|
543
|
+
])
|
|
544
|
+
|
|
545
|
+
# 创建虚拟环境
|
|
546
|
+
venv_name = f"classroom-{classroom.classroom_id}"
|
|
547
|
+
lines.extend([
|
|
548
|
+
f'echo "创建虚拟环境 {venv_name}..."',
|
|
549
|
+
f'python3 -m venv {venv_name}',
|
|
550
|
+
f'source {venv_name}/bin/activate',
|
|
551
|
+
"",
|
|
552
|
+
f'echo "升级 pip..."',
|
|
553
|
+
f'pip install --upgrade pip',
|
|
554
|
+
"",
|
|
555
|
+
])
|
|
556
|
+
|
|
557
|
+
# 安装额外包
|
|
558
|
+
if classroom.extra_packages:
|
|
559
|
+
pkg_list = " ".join(classroom.extra_packages)
|
|
560
|
+
lines.extend([
|
|
561
|
+
f'echo "安装课程依赖..."',
|
|
562
|
+
f'pip install {pkg_list}',
|
|
563
|
+
"",
|
|
564
|
+
])
|
|
565
|
+
|
|
566
|
+
# 克隆并安装每个项目
|
|
567
|
+
for project in classroom.projects:
|
|
568
|
+
lines.extend([
|
|
569
|
+
f'echo "📦 安装项目: {project}"',
|
|
570
|
+
f'if [ ! -d "{project.split("/")[-1]}" ]; then',
|
|
571
|
+
f' git clone https://github.com/{project}.git',
|
|
572
|
+
f'fi',
|
|
573
|
+
f'cd {project.split("/")[-1]}',
|
|
574
|
+
f'if [ -f requirements.txt ]; then',
|
|
575
|
+
f' pip install -r requirements.txt',
|
|
576
|
+
f'elif [ -f setup.py ] || [ -f pyproject.toml ]; then',
|
|
577
|
+
f' pip install -e .',
|
|
578
|
+
f'fi',
|
|
579
|
+
f'cd ..',
|
|
580
|
+
"",
|
|
581
|
+
])
|
|
582
|
+
|
|
583
|
+
lines.extend([
|
|
584
|
+
'echo ""',
|
|
585
|
+
'echo "✅ 课堂环境配置完成!"',
|
|
586
|
+
f'echo "激活环境: source {venv_name}/bin/activate"',
|
|
587
|
+
])
|
|
588
|
+
|
|
589
|
+
return "\n".join(lines)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def list_classrooms() -> list[dict]:
|
|
593
|
+
"""列出所有课堂"""
|
|
594
|
+
if not os.path.isdir(_CLASSROOM_DIR):
|
|
595
|
+
return []
|
|
596
|
+
result = []
|
|
597
|
+
for fname in sorted(os.listdir(_CLASSROOM_DIR)):
|
|
598
|
+
if fname.endswith(".json"):
|
|
599
|
+
path = os.path.join(_CLASSROOM_DIR, fname)
|
|
600
|
+
try:
|
|
601
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
602
|
+
data = json.load(f)
|
|
603
|
+
result.append(data)
|
|
604
|
+
except (json.JSONDecodeError, OSError):
|
|
605
|
+
pass
|
|
606
|
+
return result
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
# ─────────────────────────────────────────────
|
|
610
|
+
# 学习路径生成
|
|
611
|
+
# ─────────────────────────────────────────────
|
|
612
|
+
|
|
613
|
+
_LEARNING_PATHS: dict[str, LearningPath] = {
|
|
614
|
+
"python_ml_beginner": LearningPath(
|
|
615
|
+
path_id="python_ml_beginner",
|
|
616
|
+
title="Python 机器学习入门",
|
|
617
|
+
description="从零开始学习 Python 和 ML",
|
|
618
|
+
difficulty="初学者",
|
|
619
|
+
projects=[
|
|
620
|
+
{"repo": "scikit-learn/scikit-learn", "name": "Scikit-learn", "skills": ["ML基础", "分类/回归"], "hours": 4},
|
|
621
|
+
{"repo": "pandas-dev/pandas", "name": "Pandas", "skills": ["数据处理"], "hours": 3},
|
|
622
|
+
{"repo": "matplotlib/matplotlib", "name": "Matplotlib", "skills": ["数据可视化"], "hours": 2},
|
|
623
|
+
{"repo": "fastai/fastai", "name": "Fast.ai", "skills": ["深度学习入门"], "hours": 5},
|
|
624
|
+
],
|
|
625
|
+
estimated_hours=14,
|
|
626
|
+
skills_gained=["Python", "机器学习", "数据处理", "数据可视化", "深度学习入门"],
|
|
627
|
+
),
|
|
628
|
+
"deep_learning": LearningPath(
|
|
629
|
+
path_id="deep_learning",
|
|
630
|
+
title="深度学习实战",
|
|
631
|
+
description="掌握主流深度学习框架",
|
|
632
|
+
difficulty="中级",
|
|
633
|
+
projects=[
|
|
634
|
+
{"repo": "pytorch/pytorch", "name": "PyTorch", "skills": ["张量、自动微分"], "hours": 8},
|
|
635
|
+
{"repo": "huggingface/transformers", "name": "Transformers", "skills": ["NLP/LLM"], "hours": 6},
|
|
636
|
+
{"repo": "ultralytics/ultralytics", "name": "YOLOv8", "skills": ["计算机视觉"], "hours": 4},
|
|
637
|
+
{"repo": "openai/whisper", "name": "Whisper", "skills": ["语音识别"], "hours": 3},
|
|
638
|
+
],
|
|
639
|
+
estimated_hours=21,
|
|
640
|
+
skills_gained=["PyTorch", "Transformers", "NLP", "CV", "语音"],
|
|
641
|
+
),
|
|
642
|
+
"llm_engineering": LearningPath(
|
|
643
|
+
path_id="llm_engineering",
|
|
644
|
+
title="LLM 工程实践",
|
|
645
|
+
description="LLM 部署、微调、应用开发",
|
|
646
|
+
difficulty="高级",
|
|
647
|
+
projects=[
|
|
648
|
+
{"repo": "vllm-project/vllm", "name": "vLLM", "skills": ["LLM 高性能推理"], "hours": 6},
|
|
649
|
+
{"repo": "ggml-org/llama.cpp", "name": "llama.cpp", "skills": ["本地 LLM 运行"], "hours": 5},
|
|
650
|
+
{"repo": "langchain-ai/langchain", "name": "LangChain", "skills": ["LLM App 开发"], "hours": 8},
|
|
651
|
+
{"repo": "run-llama/llama_index", "name": "LlamaIndex", "skills": ["RAG 检索增强"], "hours": 6},
|
|
652
|
+
{"repo": "mlc-ai/mlc-llm", "name": "MLC-LLM", "skills": ["多端部署"], "hours": 4},
|
|
653
|
+
],
|
|
654
|
+
estimated_hours=29,
|
|
655
|
+
skills_gained=["LLM部署", "量化", "RAG", "LLM应用开发", "多端推理"],
|
|
656
|
+
),
|
|
657
|
+
"web_fullstack": LearningPath(
|
|
658
|
+
path_id="web_fullstack",
|
|
659
|
+
title="全栈 Web 开发",
|
|
660
|
+
description="前后端一体化开发",
|
|
661
|
+
difficulty="初学者",
|
|
662
|
+
projects=[
|
|
663
|
+
{"repo": "expressjs/express", "name": "Express", "skills": ["Node.js后端"], "hours": 4},
|
|
664
|
+
{"repo": "vercel/next.js", "name": "Next.js", "skills": ["React SSR"], "hours": 6},
|
|
665
|
+
{"repo": "fastapi/fastapi", "name": "FastAPI", "skills": ["Python API"], "hours": 4},
|
|
666
|
+
{"repo": "prisma/prisma", "name": "Prisma", "skills": ["ORM/数据库"], "hours": 3},
|
|
667
|
+
],
|
|
668
|
+
estimated_hours=17,
|
|
669
|
+
skills_gained=["Node.js", "React", "Python API", "数据库", "全栈"],
|
|
670
|
+
),
|
|
671
|
+
"rust_systems": LearningPath(
|
|
672
|
+
path_id="rust_systems",
|
|
673
|
+
title="Rust 系统编程",
|
|
674
|
+
description="用 Rust 构建高性能系统",
|
|
675
|
+
difficulty="高级",
|
|
676
|
+
projects=[
|
|
677
|
+
{"repo": "nickel-org/nickel.rs", "name": "Nickel", "skills": ["Rust Web"], "hours": 4},
|
|
678
|
+
{"repo": "tokio-rs/tokio", "name": "Tokio", "skills": ["异步运行时"], "hours": 6},
|
|
679
|
+
{"repo": "BurntSushi/ripgrep", "name": "ripgrep", "skills": ["CLI工具开发"], "hours": 5},
|
|
680
|
+
{"repo": "denoland/deno", "name": "Deno", "skills": ["JS Runtime"], "hours": 8},
|
|
681
|
+
],
|
|
682
|
+
estimated_hours=23,
|
|
683
|
+
skills_gained=["Rust", "异步编程", "系统编程", "CLI工具", "性能优化"],
|
|
684
|
+
),
|
|
685
|
+
"devops_cloud": LearningPath(
|
|
686
|
+
path_id="devops_cloud",
|
|
687
|
+
title="DevOps 与云原生",
|
|
688
|
+
description="CI/CD、容器、编排",
|
|
689
|
+
difficulty="中级",
|
|
690
|
+
projects=[
|
|
691
|
+
{"repo": "docker/compose", "name": "Docker Compose", "skills": ["容器编排"], "hours": 4},
|
|
692
|
+
{"repo": "kubernetes/kubernetes", "name": "Kubernetes", "skills": ["集群管理"], "hours": 10},
|
|
693
|
+
{"repo": "hashicorp/terraform", "name": "Terraform", "skills": ["IaC"], "hours": 6},
|
|
694
|
+
{"repo": "prometheus/prometheus", "name": "Prometheus", "skills": ["监控告警"], "hours": 4},
|
|
695
|
+
],
|
|
696
|
+
estimated_hours=24,
|
|
697
|
+
skills_gained=["Docker", "Kubernetes", "IaC", "监控", "CI/CD"],
|
|
698
|
+
),
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def get_learning_path(path_id: str) -> LearningPath:
|
|
703
|
+
"""获取学习路径"""
|
|
704
|
+
return _LEARNING_PATHS.get(path_id, LearningPath())
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def list_learning_paths() -> list[dict]:
|
|
708
|
+
"""列出所有学习路径"""
|
|
709
|
+
result = []
|
|
710
|
+
for path_id, lp in _LEARNING_PATHS.items():
|
|
711
|
+
result.append({
|
|
712
|
+
"path_id": path_id,
|
|
713
|
+
"title": lp.title,
|
|
714
|
+
"difficulty": lp.difficulty,
|
|
715
|
+
"projects": len(lp.projects),
|
|
716
|
+
"hours": lp.estimated_hours,
|
|
717
|
+
"skills": lp.skills_gained,
|
|
718
|
+
})
|
|
719
|
+
return result
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def recommend_learning_path(
|
|
723
|
+
skill_level: str = "beginner",
|
|
724
|
+
interests: list[str] | None = None,
|
|
725
|
+
) -> list[LearningPath]:
|
|
726
|
+
"""根据技能水平和兴趣推荐学习路径"""
|
|
727
|
+
level_map = {
|
|
728
|
+
"beginner": "初学者",
|
|
729
|
+
"intermediate": "中级",
|
|
730
|
+
"advanced": "高级",
|
|
731
|
+
}
|
|
732
|
+
target_level = level_map.get(skill_level, skill_level)
|
|
733
|
+
interests = interests or []
|
|
734
|
+
interests_lower = [i.lower() for i in interests]
|
|
735
|
+
|
|
736
|
+
scored = []
|
|
737
|
+
for lp in _LEARNING_PATHS.values():
|
|
738
|
+
score = 0.0
|
|
739
|
+
|
|
740
|
+
# 难度匹配
|
|
741
|
+
if lp.difficulty == target_level:
|
|
742
|
+
score += 50
|
|
743
|
+
|
|
744
|
+
# 兴趣匹配
|
|
745
|
+
for skill in lp.skills_gained:
|
|
746
|
+
for interest in interests_lower:
|
|
747
|
+
if interest in skill.lower():
|
|
748
|
+
score += 20
|
|
749
|
+
|
|
750
|
+
scored.append((score, lp))
|
|
751
|
+
|
|
752
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
753
|
+
return [lp for _, lp in scored[:3]]
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def format_learning_path(lp: LearningPath) -> str:
|
|
757
|
+
"""格式化学习路径"""
|
|
758
|
+
lines = [
|
|
759
|
+
f"🗺️ {lp.title}",
|
|
760
|
+
f" {lp.description}",
|
|
761
|
+
f" 难度: {lp.difficulty} | 预估: {lp.estimated_hours}小时",
|
|
762
|
+
"",
|
|
763
|
+
" 步骤:",
|
|
764
|
+
]
|
|
765
|
+
for i, proj in enumerate(lp.projects, 1):
|
|
766
|
+
skills_str = ", ".join(proj.get("skills", []))
|
|
767
|
+
lines.append(f" {i}. {proj['name']} ({proj['repo']}) — {skills_str} [{proj.get('hours', '?')}h]")
|
|
768
|
+
|
|
769
|
+
lines.extend([
|
|
770
|
+
"",
|
|
771
|
+
f" 技能收获: {', '.join(lp.skills_gained)}",
|
|
772
|
+
])
|
|
773
|
+
return "\n".join(lines)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
# ─────────────────────────────────────────────
|
|
777
|
+
# Jupyter Notebook 集成
|
|
778
|
+
# ─────────────────────────────────────────────
|
|
779
|
+
|
|
780
|
+
def generate_setup_notebook(
|
|
781
|
+
project: str,
|
|
782
|
+
plan: dict[str, Any] | None = None,
|
|
783
|
+
) -> dict:
|
|
784
|
+
"""
|
|
785
|
+
生成 Jupyter Notebook (.ipynb) 内容,引导学生安装项目。
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
dict — 合法的 .ipynb JSON 结构
|
|
789
|
+
"""
|
|
790
|
+
cells = []
|
|
791
|
+
|
|
792
|
+
# 标题 cell
|
|
793
|
+
cells.append({
|
|
794
|
+
"cell_type": "markdown",
|
|
795
|
+
"metadata": {},
|
|
796
|
+
"source": [
|
|
797
|
+
f"# 📦 安装指南: {project}\n",
|
|
798
|
+
f"\n",
|
|
799
|
+
f"本 Notebook 将引导你完成 `{project}` 的环境配置和安装。\n",
|
|
800
|
+
f"请按顺序执行每个代码单元格。\n",
|
|
801
|
+
],
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
# 环境检查
|
|
805
|
+
cells.append({
|
|
806
|
+
"cell_type": "markdown",
|
|
807
|
+
"metadata": {},
|
|
808
|
+
"source": ["## 1. 环境检查\n"],
|
|
809
|
+
})
|
|
810
|
+
cells.append({
|
|
811
|
+
"cell_type": "code",
|
|
812
|
+
"metadata": {},
|
|
813
|
+
"source": [
|
|
814
|
+
"import sys\n",
|
|
815
|
+
"print(f'Python: {sys.version}')\n",
|
|
816
|
+
"print(f'Platform: {sys.platform}')\n",
|
|
817
|
+
"!git --version\n",
|
|
818
|
+
],
|
|
819
|
+
"execution_count": None,
|
|
820
|
+
"outputs": [],
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
# 安装步骤
|
|
824
|
+
if plan and "steps" in plan:
|
|
825
|
+
for i, step in enumerate(plan["steps"], 2):
|
|
826
|
+
label = step.get("label", f"步骤 {i}")
|
|
827
|
+
cmds = step.get("commands", [])
|
|
828
|
+
note = step.get("note", "")
|
|
829
|
+
|
|
830
|
+
cells.append({
|
|
831
|
+
"cell_type": "markdown",
|
|
832
|
+
"metadata": {},
|
|
833
|
+
"source": [f"## {i}. {label}\n"] + ([f"\n{note}\n"] if note else []),
|
|
834
|
+
})
|
|
835
|
+
cells.append({
|
|
836
|
+
"cell_type": "code",
|
|
837
|
+
"metadata": {},
|
|
838
|
+
"source": [f"!{cmd}\n" for cmd in cmds if isinstance(cmd, str)],
|
|
839
|
+
"execution_count": None,
|
|
840
|
+
"outputs": [],
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
# 验证
|
|
844
|
+
cells.append({
|
|
845
|
+
"cell_type": "markdown",
|
|
846
|
+
"metadata": {},
|
|
847
|
+
"source": ["## ✅ 验证安装\n"],
|
|
848
|
+
})
|
|
849
|
+
cells.append({
|
|
850
|
+
"cell_type": "code",
|
|
851
|
+
"metadata": {},
|
|
852
|
+
"source": ["print('🎉 安装完成!')\n"],
|
|
853
|
+
"execution_count": None,
|
|
854
|
+
"outputs": [],
|
|
855
|
+
})
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
"nbformat": 4,
|
|
859
|
+
"nbformat_minor": 5,
|
|
860
|
+
"metadata": {
|
|
861
|
+
"kernelspec": {
|
|
862
|
+
"display_name": "Python 3",
|
|
863
|
+
"language": "python",
|
|
864
|
+
"name": "python3",
|
|
865
|
+
},
|
|
866
|
+
"language_info": {"name": "python", "version": "3.10.0"},
|
|
867
|
+
},
|
|
868
|
+
"cells": cells,
|
|
869
|
+
}
|