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/autopilot.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
autopilot.py - 批量安装自动驾驶模式
|
|
3
|
+
======================================
|
|
4
|
+
|
|
5
|
+
灵感来源:ICE-cluade-SCompany 的 Autopilot Revenue 模式
|
|
6
|
+
|
|
7
|
+
从文件/列表读取多个项目,后台逐个安装:
|
|
8
|
+
gitinstall autopilot projects.txt
|
|
9
|
+
gitinstall autopilot owner1/repo1 owner2/repo2 ...
|
|
10
|
+
|
|
11
|
+
特性:
|
|
12
|
+
1. 逐个安装:成功继续,失败跳过
|
|
13
|
+
2. 进度追踪:实时显示总进度
|
|
14
|
+
3. 汇总报告:完成后输出成功/失败统计
|
|
15
|
+
4. 可恢复:中断后可从断点继续
|
|
16
|
+
5. 并发控制:可配置同时安装数(默认 1)
|
|
17
|
+
|
|
18
|
+
零外部依赖,纯 Python 标准库。
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import time
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Optional
|
|
30
|
+
|
|
31
|
+
from log import get_logger
|
|
32
|
+
from i18n import t
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class BatchItem:
|
|
39
|
+
"""批量安装项"""
|
|
40
|
+
identifier: str # owner/repo 或 URL
|
|
41
|
+
status: str = "pending" # pending, running, completed, failed, skipped
|
|
42
|
+
install_dir: str = ""
|
|
43
|
+
started_at: str = ""
|
|
44
|
+
completed_at: str = ""
|
|
45
|
+
duration_sec: float = 0.0
|
|
46
|
+
error: str = ""
|
|
47
|
+
strategy: str = ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class BatchResult:
|
|
52
|
+
"""批量安装结果"""
|
|
53
|
+
total: int = 0
|
|
54
|
+
completed: int = 0
|
|
55
|
+
failed: int = 0
|
|
56
|
+
skipped: int = 0
|
|
57
|
+
items: list[BatchItem] = field(default_factory=list)
|
|
58
|
+
started_at: str = ""
|
|
59
|
+
completed_at: str = ""
|
|
60
|
+
total_duration_sec: float = 0.0
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def success_rate(self) -> float:
|
|
64
|
+
done = self.completed + self.failed
|
|
65
|
+
if done == 0:
|
|
66
|
+
return 0.0
|
|
67
|
+
return self.completed / done
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
return {
|
|
71
|
+
"total": self.total,
|
|
72
|
+
"completed": self.completed,
|
|
73
|
+
"failed": self.failed,
|
|
74
|
+
"skipped": self.skipped,
|
|
75
|
+
"success_rate": self.success_rate,
|
|
76
|
+
"started_at": self.started_at,
|
|
77
|
+
"completed_at": self.completed_at,
|
|
78
|
+
"total_duration_sec": self.total_duration_sec,
|
|
79
|
+
"items": [
|
|
80
|
+
{
|
|
81
|
+
"identifier": it.identifier,
|
|
82
|
+
"status": it.status,
|
|
83
|
+
"install_dir": it.install_dir,
|
|
84
|
+
"duration_sec": it.duration_sec,
|
|
85
|
+
"error": it.error,
|
|
86
|
+
"strategy": it.strategy,
|
|
87
|
+
}
|
|
88
|
+
for it in self.items
|
|
89
|
+
],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── 持久化路径 ──
|
|
94
|
+
AUTOPILOT_DIR = Path.home() / ".gitinstall" / "autopilot"
|
|
95
|
+
AUTOPILOT_STATE = AUTOPILOT_DIR / "state.json"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_project_list(source: str) -> list[str]:
|
|
99
|
+
"""解析项目列表
|
|
100
|
+
|
|
101
|
+
支持:
|
|
102
|
+
- 单个 owner/repo
|
|
103
|
+
- 空格/逗号分隔多个
|
|
104
|
+
- 文件路径(每行一个或 JSON 数组)
|
|
105
|
+
"""
|
|
106
|
+
projects = []
|
|
107
|
+
|
|
108
|
+
# 检查是否是文件
|
|
109
|
+
path = Path(source)
|
|
110
|
+
if path.exists() and path.is_file():
|
|
111
|
+
content = path.read_text(encoding="utf-8").strip()
|
|
112
|
+
# 尝试 JSON 数组
|
|
113
|
+
try:
|
|
114
|
+
data = json.loads(content)
|
|
115
|
+
if isinstance(data, list):
|
|
116
|
+
for item in data:
|
|
117
|
+
if isinstance(item, str):
|
|
118
|
+
projects.append(item.strip())
|
|
119
|
+
elif isinstance(item, dict):
|
|
120
|
+
projects.append(item.get("project", item.get("repo", "")))
|
|
121
|
+
return [p for p in projects if p]
|
|
122
|
+
except json.JSONDecodeError:
|
|
123
|
+
pass
|
|
124
|
+
# 按行解析
|
|
125
|
+
for line in content.split("\n"):
|
|
126
|
+
line = line.strip()
|
|
127
|
+
if line and not line.startswith("#"):
|
|
128
|
+
# 去除行注释
|
|
129
|
+
line = line.split("#")[0].strip()
|
|
130
|
+
if line:
|
|
131
|
+
projects.append(line)
|
|
132
|
+
return projects
|
|
133
|
+
|
|
134
|
+
# 空格/逗号分隔
|
|
135
|
+
for part in re.split(r'[,\s]+', source):
|
|
136
|
+
part = part.strip()
|
|
137
|
+
if part:
|
|
138
|
+
projects.append(part)
|
|
139
|
+
|
|
140
|
+
return projects
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def run_autopilot(projects: list[str], install_dir: str = None,
|
|
144
|
+
llm_force: str = None, dry_run: bool = False,
|
|
145
|
+
on_progress: callable = None) -> BatchResult:
|
|
146
|
+
"""执行批量安装
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
projects: 项目列表
|
|
150
|
+
install_dir: 基础安装目录
|
|
151
|
+
llm_force: 强制使用的 LLM
|
|
152
|
+
dry_run: 仅规划不执行
|
|
153
|
+
on_progress: 进度回调 (current, total, item)
|
|
154
|
+
"""
|
|
155
|
+
result = BatchResult(
|
|
156
|
+
total=len(projects),
|
|
157
|
+
started_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
for item_str in projects:
|
|
161
|
+
result.items.append(BatchItem(identifier=item_str))
|
|
162
|
+
|
|
163
|
+
# 保存初始状态
|
|
164
|
+
_save_state(result)
|
|
165
|
+
|
|
166
|
+
for i, item in enumerate(result.items):
|
|
167
|
+
item.status = "running"
|
|
168
|
+
item.started_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
169
|
+
|
|
170
|
+
# 进度回调
|
|
171
|
+
if on_progress:
|
|
172
|
+
on_progress(i + 1, result.total, item)
|
|
173
|
+
|
|
174
|
+
logger.info("="*60)
|
|
175
|
+
logger.info(t("autopilot.progress", current=i+1, total=result.total, identifier=item.identifier))
|
|
176
|
+
logger.info("="*60)
|
|
177
|
+
|
|
178
|
+
start_time = time.time()
|
|
179
|
+
try:
|
|
180
|
+
# 懒加载避免循环导入
|
|
181
|
+
from main import cmd_install
|
|
182
|
+
|
|
183
|
+
install_result = cmd_install(
|
|
184
|
+
item.identifier,
|
|
185
|
+
install_dir=install_dir,
|
|
186
|
+
llm_force=llm_force,
|
|
187
|
+
dry_run=dry_run,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
item.duration_sec = time.time() - start_time
|
|
191
|
+
item.completed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
192
|
+
|
|
193
|
+
if install_result.get("success"):
|
|
194
|
+
item.status = "completed"
|
|
195
|
+
item.install_dir = install_result.get("install_dir", "")
|
|
196
|
+
item.strategy = install_result.get("plan_strategy", "")
|
|
197
|
+
result.completed += 1
|
|
198
|
+
logger.info(t("autopilot.success", duration=f"{item.duration_sec:.1f}"))
|
|
199
|
+
else:
|
|
200
|
+
item.status = "failed"
|
|
201
|
+
item.error = install_result.get("error_summary", "未知错误")[:200]
|
|
202
|
+
item.strategy = install_result.get("plan_strategy", "")
|
|
203
|
+
result.failed += 1
|
|
204
|
+
logger.error(t("autopilot.install_failed", error=item.error[:80]))
|
|
205
|
+
|
|
206
|
+
except KeyboardInterrupt:
|
|
207
|
+
item.status = "skipped"
|
|
208
|
+
item.duration_sec = time.time() - start_time
|
|
209
|
+
result.skipped += 1
|
|
210
|
+
logger.warning(t("autopilot.user_interrupted"))
|
|
211
|
+
# 剩余标记为跳过
|
|
212
|
+
for remaining in result.items[i+1:]:
|
|
213
|
+
remaining.status = "skipped"
|
|
214
|
+
result.skipped += 1
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
item.status = "failed"
|
|
219
|
+
item.error = str(e)[:200]
|
|
220
|
+
item.duration_sec = time.time() - start_time
|
|
221
|
+
result.failed += 1
|
|
222
|
+
logger.error(t("autopilot.exception", error=e))
|
|
223
|
+
|
|
224
|
+
# 每步保存状态(支持恢复)
|
|
225
|
+
_save_state(result)
|
|
226
|
+
|
|
227
|
+
result.completed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
228
|
+
result.total_duration_sec = sum(it.duration_sec for it in result.items)
|
|
229
|
+
_save_state(result)
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def resume_autopilot(llm_force: str = None,
|
|
234
|
+
install_dir: str = None) -> Optional[BatchResult]:
|
|
235
|
+
"""从上次中断处恢复"""
|
|
236
|
+
state = _load_state()
|
|
237
|
+
if not state:
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
# 找到第一个 pending/skipped 的项目
|
|
241
|
+
resume_idx = -1
|
|
242
|
+
for i, item in enumerate(state.items):
|
|
243
|
+
if item.status in ("pending", "skipped", "running"):
|
|
244
|
+
resume_idx = i
|
|
245
|
+
break
|
|
246
|
+
|
|
247
|
+
if resume_idx < 0:
|
|
248
|
+
return state # 所有项目已完成
|
|
249
|
+
|
|
250
|
+
# 恢复执行
|
|
251
|
+
pending = [it.identifier for it in state.items[resume_idx:]
|
|
252
|
+
if it.status in ("pending", "skipped", "running")]
|
|
253
|
+
|
|
254
|
+
if not pending:
|
|
255
|
+
return state
|
|
256
|
+
|
|
257
|
+
new_result = run_autopilot(pending, install_dir=install_dir,
|
|
258
|
+
llm_force=llm_force)
|
|
259
|
+
|
|
260
|
+
# 合并结果
|
|
261
|
+
for i, item in enumerate(state.items[resume_idx:]):
|
|
262
|
+
if i < len(new_result.items):
|
|
263
|
+
state.items[resume_idx + i] = new_result.items[i]
|
|
264
|
+
|
|
265
|
+
state.completed = sum(1 for it in state.items if it.status == "completed")
|
|
266
|
+
state.failed = sum(1 for it in state.items if it.status == "failed")
|
|
267
|
+
state.skipped = sum(1 for it in state.items if it.status == "skipped")
|
|
268
|
+
state.completed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
269
|
+
state.total_duration_sec = sum(it.duration_sec for it in state.items)
|
|
270
|
+
_save_state(state)
|
|
271
|
+
return state
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _save_state(result: BatchResult):
|
|
275
|
+
"""保存自动驾驶状态"""
|
|
276
|
+
AUTOPILOT_DIR.mkdir(parents=True, exist_ok=True)
|
|
277
|
+
with open(AUTOPILOT_STATE, "w", encoding="utf-8") as f:
|
|
278
|
+
json.dump(result.to_dict(), f, indent=2, ensure_ascii=False)
|
|
279
|
+
try:
|
|
280
|
+
os.chmod(AUTOPILOT_STATE, 0o600)
|
|
281
|
+
except OSError:
|
|
282
|
+
pass
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _load_state() -> Optional[BatchResult]:
|
|
286
|
+
"""加载自动驾驶状态"""
|
|
287
|
+
if not AUTOPILOT_STATE.exists():
|
|
288
|
+
return None
|
|
289
|
+
try:
|
|
290
|
+
with open(AUTOPILOT_STATE, encoding="utf-8") as f:
|
|
291
|
+
data = json.load(f)
|
|
292
|
+
result = BatchResult(
|
|
293
|
+
total=data.get("total", 0),
|
|
294
|
+
completed=data.get("completed", 0),
|
|
295
|
+
failed=data.get("failed", 0),
|
|
296
|
+
skipped=data.get("skipped", 0),
|
|
297
|
+
started_at=data.get("started_at", ""),
|
|
298
|
+
completed_at=data.get("completed_at", ""),
|
|
299
|
+
total_duration_sec=data.get("total_duration_sec", 0.0),
|
|
300
|
+
)
|
|
301
|
+
for item_data in data.get("items", []):
|
|
302
|
+
result.items.append(BatchItem(
|
|
303
|
+
identifier=item_data.get("identifier", ""),
|
|
304
|
+
status=item_data.get("status", "pending"),
|
|
305
|
+
install_dir=item_data.get("install_dir", ""),
|
|
306
|
+
duration_sec=item_data.get("duration_sec", 0.0),
|
|
307
|
+
error=item_data.get("error", ""),
|
|
308
|
+
strategy=item_data.get("strategy", ""),
|
|
309
|
+
))
|
|
310
|
+
return result
|
|
311
|
+
except (json.JSONDecodeError, OSError):
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ─────────────────────────────────────────────
|
|
316
|
+
# 格式化输出
|
|
317
|
+
# ─────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
def format_batch_result(result: BatchResult) -> str:
|
|
320
|
+
"""格式化批量安装结果"""
|
|
321
|
+
lines = [
|
|
322
|
+
"",
|
|
323
|
+
"═" * 60,
|
|
324
|
+
"🚗 自动驾驶安装报告",
|
|
325
|
+
"═" * 60,
|
|
326
|
+
f" 总计:{result.total} 个项目",
|
|
327
|
+
f" ✅ 成功:{result.completed}",
|
|
328
|
+
f" ❌ 失败:{result.failed}",
|
|
329
|
+
f" ⏭️ 跳过:{result.skipped}",
|
|
330
|
+
f" 成功率:{result.success_rate:.1%}",
|
|
331
|
+
f" 总耗时:{result.total_duration_sec:.1f}s",
|
|
332
|
+
"",
|
|
333
|
+
"─" * 60,
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
for item in result.items:
|
|
337
|
+
if item.status == "completed":
|
|
338
|
+
lines.append(f" ✅ {item.identifier} ({item.duration_sec:.1f}s)")
|
|
339
|
+
elif item.status == "failed":
|
|
340
|
+
lines.append(f" ❌ {item.identifier} ({item.duration_sec:.1f}s)")
|
|
341
|
+
if item.error:
|
|
342
|
+
lines.append(f" 错误:{item.error[:80]}")
|
|
343
|
+
elif item.status == "skipped":
|
|
344
|
+
lines.append(f" ⏭️ {item.identifier} (跳过)")
|
|
345
|
+
else:
|
|
346
|
+
lines.append(f" ⏳ {item.identifier} ({item.status})")
|
|
347
|
+
|
|
348
|
+
lines.append("═" * 60)
|
|
349
|
+
return "\n".join(lines)
|