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.
Files changed (59) hide show
  1. gitinstall/__init__.py +61 -0
  2. gitinstall/_sdk.py +541 -0
  3. gitinstall/academic.py +831 -0
  4. gitinstall/admin.html +327 -0
  5. gitinstall/auto_update.py +384 -0
  6. gitinstall/autopilot.py +349 -0
  7. gitinstall/badge.py +476 -0
  8. gitinstall/checkpoint.py +330 -0
  9. gitinstall/cicd.py +499 -0
  10. gitinstall/clawhub.html +718 -0
  11. gitinstall/config_schema.py +353 -0
  12. gitinstall/db.py +984 -0
  13. gitinstall/db_backend.py +445 -0
  14. gitinstall/dep_chain.py +337 -0
  15. gitinstall/dependency_audit.py +1153 -0
  16. gitinstall/detector.py +542 -0
  17. gitinstall/doctor.py +493 -0
  18. gitinstall/education.py +869 -0
  19. gitinstall/enterprise.py +802 -0
  20. gitinstall/error_fixer.py +953 -0
  21. gitinstall/event_bus.py +251 -0
  22. gitinstall/executor.py +577 -0
  23. gitinstall/feature_flags.py +138 -0
  24. gitinstall/fetcher.py +921 -0
  25. gitinstall/huggingface.py +922 -0
  26. gitinstall/hw_detect.py +988 -0
  27. gitinstall/i18n.py +664 -0
  28. gitinstall/installer_registry.py +362 -0
  29. gitinstall/knowledge_base.py +379 -0
  30. gitinstall/license_check.py +605 -0
  31. gitinstall/llm.py +569 -0
  32. gitinstall/log.py +236 -0
  33. gitinstall/main.py +1408 -0
  34. gitinstall/mcp_agent.py +841 -0
  35. gitinstall/mcp_server.py +386 -0
  36. gitinstall/monorepo.py +810 -0
  37. gitinstall/multi_source.py +425 -0
  38. gitinstall/onboard.py +276 -0
  39. gitinstall/planner.py +222 -0
  40. gitinstall/planner_helpers.py +323 -0
  41. gitinstall/planner_known_projects.py +1010 -0
  42. gitinstall/planner_templates.py +996 -0
  43. gitinstall/remote_gpu.py +633 -0
  44. gitinstall/resilience.py +608 -0
  45. gitinstall/run_tests.py +572 -0
  46. gitinstall/skills.py +476 -0
  47. gitinstall/tool_schemas.py +324 -0
  48. gitinstall/trending.py +279 -0
  49. gitinstall/uninstaller.py +415 -0
  50. gitinstall/validate_top100.py +607 -0
  51. gitinstall/watchdog.py +180 -0
  52. gitinstall/web.py +1277 -0
  53. gitinstall/web_ui.html +2277 -0
  54. gitinstall-1.1.0.dist-info/METADATA +275 -0
  55. gitinstall-1.1.0.dist-info/RECORD +59 -0
  56. gitinstall-1.1.0.dist-info/WHEEL +5 -0
  57. gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
  58. gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
  59. gitinstall-1.1.0.dist-info/top_level.txt +1 -0
gitinstall/executor.py ADDED
@@ -0,0 +1,577 @@
1
+ """
2
+ executor.py - 安全的跨平台命令执行器
3
+ =====================================
4
+
5
+ 设计原则:
6
+ 1. 安全第一:执行前过滤危险命令,有白名单机制
7
+ 2. 实时输出:边执行边打印,用户知道发生了什么
8
+ 3. 错误恢复:捕获报错,调用 LLM 分析修复方案
9
+ 4. 跨平台:macOS / Linux / Windows 三端适配
10
+ 5. 可回滚:记录每一步,出错可提示用户撤销
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import platform
18
+ import re
19
+ import subprocess
20
+ import sys
21
+ import time
22
+ from dataclasses import dataclass, field
23
+ from pathlib import Path
24
+ from typing import Optional
25
+
26
+ from log import get_logger
27
+ from i18n import t
28
+
29
+ logger = get_logger(__name__)
30
+
31
+
32
+ # ─────────────────────────────────────────────
33
+ # 数据结构
34
+ # ─────────────────────────────────────────────
35
+
36
+ @dataclass
37
+ class StepResult:
38
+ step_id: int
39
+ command: str
40
+ success: bool
41
+ stdout: str
42
+ stderr: str
43
+ exit_code: int
44
+ duration_sec: float
45
+ error_message: str = ""
46
+ fix_applied: bool = False
47
+ fix_command: str = ""
48
+
49
+
50
+ @dataclass
51
+ class InstallResult:
52
+ project: str
53
+ success: bool
54
+ steps: list[StepResult] = field(default_factory=list)
55
+ launch_command: str = ""
56
+ install_dir: str = ""
57
+ error_summary: str = ""
58
+
59
+
60
+ # ─────────────────────────────────────────────
61
+ # 危险命令检测
62
+ # ─────────────────────────────────────────────
63
+
64
+ # 绝对禁止执行的命令模式(正则)
65
+ BLOCKED_PATTERNS = [
66
+ r'(?:^|/)rm\s+-[^\s]*r[^\s]*\s+/', # rm -rf /
67
+ r'(?:^|/)rm\s+-[^\s]*r[^\s]*\s+~\b', # rm -rf ~
68
+ r':\(\)\s*\{', # fork bomb
69
+ r'\bformat\s+[cCdDeE]:', # Windows format
70
+ r'\bmkfs\.\w', # mkfs.*
71
+ r'\bdd\s+if=/', # dd if=/dev/...
72
+ r'(?:^|/)chmod\s+777\s+/', # chmod 777 /
73
+ r'(?:^|/)chown\s+.*\s+/', # chown ... /
74
+ r'>\s*/dev/sda', # overwrite disk
75
+ r'\biptables\s+-F', # flush firewall
76
+ r'\buserdel\s+-r\b', # delete user
77
+ r'shutdown\s+-[hrPf]', # system shutdown
78
+ r'\breboot\b', # reboot
79
+ r'curl[^\n]+\|\s*sudo\s+(?:bash|sh)\b', # curl | sudo bash
80
+ r'wget[^\n]+\|\s*sudo\s+(?:bash|sh)\b', # wget | sudo sh
81
+ # ── G1: 编码绕过防护 ──
82
+ r'base64\s+(?:-d|--decode)\s*\|', # base64 -d | ...
83
+ r'xxd\s+(?:-r|--revert)\s*\|', # xxd -r | ...
84
+ r"""python3?\s+-c\s+.*(?:base64|codecs|decode|exec|eval|import\s+os|import\s+subprocess|import\s+shutil)""", # python -c payload
85
+ r'printf\s+[\'"]\\x[0-9a-f].*\|\s*(?:bash|sh)', # printf hex | bash
86
+ r'echo\s+-e\s+.*\\x[0-9a-f].*\|\s*(?:bash|sh)', # echo -e hex | bash
87
+ # ── G2: heredoc 绕过防护 ──
88
+ r'(?:bash|sh|zsh)\s*<<', # bash << heredoc
89
+ r'\beval\s+', # eval "..."
90
+ # ── G4: 额外磁盘/设备防护 ──
91
+ r'\bdd\s+.*\bof=\s*/dev/', # dd of=/dev/xxx
92
+ r'>\s*/dev/(?:sd|nvme|vd|hd)', # redirect to disk
93
+ # ── 补充:rm -rf 不带空格变体 ──
94
+ r'rm\s+-rf\s+/(?:\s|$)', # rm -rf / (strict)
95
+ ]
96
+
97
+ # 需要用户额外确认的高风险命令
98
+ WARN_PATTERNS = [
99
+ (r'\bsudo\b', "⚠️ 该命令需要管理员权限"),
100
+ (r'\bchmod\b', "⚠️ 该命令修改文件权限"),
101
+ (r'\bcurl[^\n]+\|', "⚠️ 该命令从网络下载并执行脚本"),
102
+ (r'\bwget[^\n]+\|', "⚠️ 该命令从网络下载并执行脚本"),
103
+ (r'\bdocker run[^\n]+--privileged', "⚠️ 该 Docker 容器以特权模式运行"),
104
+ (r'\$\{?\w+\}?\s*/', "⚠️ 命令使用变量引用路径,可能含风险"),
105
+ ]
106
+
107
+
108
+ def _strip_comments(cmd: str) -> str:
109
+ """移除 shell 行尾注释(简单场景,不处理引号内的 #)。"""
110
+ return re.sub(r'(?<!\S)#.*$', '', cmd, flags=re.MULTILINE)
111
+
112
+
113
+ def check_command_safety(command: str) -> tuple[bool, str]:
114
+ """
115
+ 检查命令安全性。
116
+ Returns:
117
+ (is_safe, warning_message)
118
+ is_safe=False 表示绝对禁止,warning_message 非空表示需要确认
119
+ """
120
+ # G6: 先移除注释,防止注释混淆
121
+ command_clean = _strip_comments(command)
122
+
123
+ # 拆分 &&、||、; 分隔的子命令段,每段都要检查
124
+ segments = re.split(r'\s*(?:&&|\|\||;)\s*', command_clean)
125
+ for segment in segments:
126
+ segment = segment.strip()
127
+ if not segment:
128
+ continue
129
+ for pattern in BLOCKED_PATTERNS:
130
+ if re.search(pattern, segment, re.IGNORECASE):
131
+ return False, f"🚫 危险命令,已拒绝执行:{command}"
132
+
133
+ # 也对完整命令做一次检查(跨段的管道模式)
134
+ for pattern in BLOCKED_PATTERNS:
135
+ if re.search(pattern, command_clean, re.IGNORECASE):
136
+ return False, f"🚫 危险命令,已拒绝执行:{command}"
137
+
138
+ warnings = []
139
+ for pattern, msg in WARN_PATTERNS:
140
+ if re.search(pattern, command_clean, re.IGNORECASE):
141
+ warnings.append(msg)
142
+
143
+ return True, "\n".join(warnings)
144
+
145
+
146
+ # ─────────────────────────────────────────────
147
+ # 路径适配
148
+ # ─────────────────────────────────────────────
149
+
150
+ def adapt_path_for_os(path: str) -> str:
151
+ """将路径中的 ~ 展开,并处理 Windows 路径分隔符"""
152
+ path = path.replace("~", str(Path.home()))
153
+ if platform.system() == "Windows":
154
+ # 只转换本地路径中的分隔符,不影响 URL
155
+ if not re.match(r'https?://', path):
156
+ path = path.replace("/", "\\")
157
+ return path
158
+
159
+
160
+ def get_shell() -> list[str]:
161
+ """根据平台返回合适的 shell 前缀"""
162
+ if platform.system() == "Windows":
163
+ # 优先 PowerShell,其次 cmd
164
+ if os.path.exists(r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"):
165
+ return ["powershell", "-NonInteractive", "-Command"]
166
+ return ["cmd", "/c"]
167
+ else:
168
+ shell = os.environ.get("SHELL", "/bin/bash")
169
+ return [shell, "-c"]
170
+
171
+
172
+ # ─────────────────────────────────────────────
173
+ # 命令执行器
174
+ # ─────────────────────────────────────────────
175
+
176
+ class CommandExecutor:
177
+ """
178
+ 安全的命令执行器,支持实时输出、超时控制、错误捕获。
179
+ """
180
+
181
+ def __init__(
182
+ self,
183
+ work_dir: Optional[str] = None,
184
+ timeout_sec: int = 600, # 默认 10 分钟超时
185
+ verbose: bool = True,
186
+ ):
187
+ self.work_dir = work_dir or str(Path.home())
188
+ self.timeout_sec = timeout_sec
189
+ self.verbose = verbose
190
+ self._current_dir = self.work_dir
191
+ self._base_env = os.environ.copy()
192
+ self._env = self._base_env.copy()
193
+
194
+ def reset(self, work_dir: Optional[str] = None):
195
+ """重置工作目录(用于回退策略切换)"""
196
+ if work_dir:
197
+ self.work_dir = work_dir
198
+ self._current_dir = self.work_dir
199
+ self._env = self._base_env.copy()
200
+
201
+ def _activate_virtualenv(self, command: str, cwd: str) -> StepResult | None:
202
+ """处理虚拟环境激活命令,并将环境持久化到后续步骤。"""
203
+ activate_cmd = command.strip()
204
+ activate_path = ""
205
+
206
+ if activate_cmd.startswith("source "):
207
+ activate_path = activate_cmd[len("source "):].strip().strip('"').strip("'")
208
+ elif activate_cmd.endswith("activate") or activate_cmd.endswith("Activate.ps1"):
209
+ activate_path = activate_cmd.strip().strip('"').strip("'")
210
+ else:
211
+ return None
212
+
213
+ activate_file = Path(adapt_path_for_os(activate_path))
214
+ if not activate_file.is_absolute():
215
+ activate_file = Path(cwd) / activate_file
216
+ activate_file = activate_file.resolve()
217
+
218
+ if not activate_file.is_file():
219
+ # Windows: 尝试 Scripts\activate 替代 bin/activate
220
+ if platform.system() == "Windows":
221
+ alt = activate_file.parent.parent / "Scripts" / "activate"
222
+ if alt.is_file():
223
+ activate_file = alt
224
+ else:
225
+ return None
226
+ else:
227
+ return None
228
+
229
+ bin_dir = activate_file.parent
230
+ venv_dir = bin_dir.parent
231
+ old_path = self._env.get("PATH", "")
232
+ path_parts = [part for part in old_path.split(os.pathsep) if part]
233
+ bin_dir_str = str(bin_dir)
234
+ path_parts = [part for part in path_parts if part != bin_dir_str]
235
+ self._env["VIRTUAL_ENV"] = str(venv_dir)
236
+ self._env["PATH"] = os.pathsep.join([bin_dir_str] + path_parts)
237
+
238
+ return StepResult(0, command, True, "", "", 0, 0.0)
239
+
240
+ def run(self, command: str, working_dir: Optional[str] = None) -> StepResult:
241
+ """
242
+ 执行单条命令。
243
+
244
+ - 自动处理 cd 命令(更新当前目录状态)
245
+ - 实时打印输出
246
+ - 返回详细执行结果
247
+ """
248
+ cwd = working_dir or self._current_dir
249
+ cwd = adapt_path_for_os(cwd)
250
+ command = command.strip()
251
+
252
+ # 处理 cd 命令(不真正执行,只更新目录状态)
253
+ if command.startswith("cd "):
254
+ target = command[3:].strip().strip('"').strip("'")
255
+ target = adapt_path_for_os(target)
256
+ # 处理相对路径
257
+ if not os.path.isabs(target):
258
+ target = str(Path(cwd) / target)
259
+ if os.path.isdir(target):
260
+ self._current_dir = target
261
+ return StepResult(0, command, True, "", "", 0, 0.0)
262
+ else:
263
+ return StepResult(
264
+ 0, command, False, "", f"目录不存在: {target}", 1, 0.0,
265
+ error_message=f"目录不存在: {target}",
266
+ )
267
+
268
+ venv_result = self._activate_virtualenv(command, cwd)
269
+ if venv_result is not None:
270
+ return venv_result
271
+
272
+ # 处理 mkdir 命令(提前创建目录)
273
+ if re.match(r'mkdir\s+(?:-p\s+)?(.+)', command):
274
+ match = re.match(r'mkdir\s+(?:-p\s+)?(.+)', command)
275
+ if match:
276
+ dir_path = adapt_path_for_os(match.group(1).strip())
277
+ if not os.path.isabs(dir_path):
278
+ dir_path = str(Path(cwd) / dir_path)
279
+ Path(dir_path).mkdir(parents=True, exist_ok=True)
280
+ return StepResult(0, command, True, "", "", 0, 0.0)
281
+
282
+ if self.verbose:
283
+ logger.info(" $ %s", command)
284
+
285
+ start = time.time()
286
+ stdout_lines = []
287
+ stderr_lines = []
288
+
289
+ try:
290
+ # 确保目录存在
291
+ Path(cwd).mkdir(parents=True, exist_ok=True)
292
+
293
+ proc = subprocess.Popen(
294
+ command,
295
+ shell=True,
296
+ cwd=cwd,
297
+ env=self._env,
298
+ stdout=subprocess.PIPE,
299
+ stderr=subprocess.PIPE,
300
+ text=True,
301
+ encoding="utf-8",
302
+ errors="replace",
303
+ # Windows 需要这个避免弹出额外窗口
304
+ creationflags=subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0,
305
+ )
306
+
307
+ # 实时读取输出
308
+ import threading
309
+
310
+ def read_stream(stream, lines, prefix=""):
311
+ for line in stream:
312
+ lines.append(line.rstrip())
313
+ if self.verbose:
314
+ sys.stdout.write(f" {prefix}{line}")
315
+ sys.stdout.flush()
316
+
317
+ t_out = threading.Thread(target=read_stream, args=(proc.stdout, stdout_lines))
318
+ t_err = threading.Thread(target=read_stream, args=(proc.stderr, stderr_lines, "⚠ "))
319
+ t_out.start()
320
+ t_err.start()
321
+
322
+ try:
323
+ proc.wait(timeout=self.timeout_sec)
324
+ except subprocess.TimeoutExpired:
325
+ # 渐进式关闭:先 SIGTERM(graceful),等 5 秒,再 SIGKILL
326
+ import signal
327
+ if platform.system() != "Windows":
328
+ try:
329
+ proc.send_signal(signal.SIGTERM)
330
+ proc.wait(timeout=5)
331
+ except (subprocess.TimeoutExpired, OSError):
332
+ proc.kill()
333
+ else:
334
+ proc.kill()
335
+ return StepResult(
336
+ 0, command, False, "", "命令超时", -1,
337
+ time.time() - start,
338
+ error_message=f"命令执行超时({self.timeout_sec}秒)",
339
+ )
340
+
341
+ t_out.join()
342
+ t_err.join()
343
+
344
+ duration = time.time() - start
345
+ success = proc.returncode == 0
346
+ stdout = "\n".join(stdout_lines)
347
+ stderr = "\n".join(stderr_lines)
348
+
349
+ return StepResult(
350
+ step_id=0,
351
+ command=command,
352
+ success=success,
353
+ stdout=stdout[-3000:], # 只保留最后 3000 字符
354
+ stderr=stderr[-3000:],
355
+ exit_code=proc.returncode,
356
+ duration_sec=round(duration, 1),
357
+ error_message=stderr[:500] if not success else "",
358
+ )
359
+
360
+ except Exception as e:
361
+ return StepResult(
362
+ 0, command, False, "", str(e), -1,
363
+ time.time() - start,
364
+ error_message=str(e),
365
+ )
366
+
367
+
368
+ # ─────────────────────────────────────────────
369
+ # 安装计划执行器
370
+ # ─────────────────────────────────────────────
371
+
372
+ class InstallExecutor:
373
+ """
374
+ 执行完整安装计划,支持自动错误修复。
375
+ """
376
+
377
+ def __init__(self, llm_provider=None, verbose: bool = True):
378
+ self.llm = llm_provider
379
+ self.verbose = verbose
380
+ self.executor = CommandExecutor(verbose=verbose)
381
+
382
+ def execute_plan(self, plan: dict, project_name: str = "") -> InstallResult:
383
+ """
384
+ 执行 LLM 生成的安装计划。
385
+
386
+ Args:
387
+ plan: {"steps": [...], "launch_command": "...", ...}
388
+ project_name: 项目名称(用于日志)
389
+ """
390
+ steps = plan.get("steps", [])
391
+ launch_command = plan.get("launch_command", "")
392
+
393
+ result = InstallResult(
394
+ project=project_name,
395
+ success=False,
396
+ launch_command=launch_command,
397
+ )
398
+
399
+ logger.info("="*50)
400
+ logger.info(t("exec.install_start", project=project_name, steps=len(steps)))
401
+ logger.info("="*50)
402
+
403
+ for i, step in enumerate(steps, 1):
404
+ command = step.get("command", "").strip()
405
+ description = step.get("description", f"步骤 {i}")
406
+ working_dir = step.get("working_dir", "")
407
+
408
+ if not command:
409
+ continue
410
+
411
+ # 扩展路径中的 ~
412
+ if working_dir:
413
+ working_dir = adapt_path_for_os(working_dir)
414
+
415
+ logger.info(t("exec.step_progress", current=i, total=len(steps), description=description))
416
+
417
+ # 安全检查
418
+ is_safe, warning = check_command_safety(command)
419
+ if not is_safe:
420
+ logger.warning(warning)
421
+ result.error_summary = warning
422
+ return result
423
+
424
+ if warning and self.verbose:
425
+ logger.warning(warning)
426
+
427
+ # 执行命令
428
+ step_result = self.executor.run(command, working_dir or None)
429
+ step_result.step_id = i
430
+ result.steps.append(step_result)
431
+
432
+ if step_result.success:
433
+ dur = step_result.duration_sec
434
+ logger.info(t("exec.step_done", duration=dur))
435
+ else:
436
+ logger.error(t("exec.step_failed", code=step_result.exit_code))
437
+
438
+ # 尝试自动修复(规则引擎优先,LLM 兜底)
439
+ if step_result.stderr or step_result.stdout:
440
+ fixed = self._try_fix(step_result, i, len(steps))
441
+ if fixed:
442
+ step_result.fix_applied = True
443
+ result.steps[-1] = step_result
444
+ continue
445
+
446
+ # 无法修复,终止
447
+ result.error_summary = (
448
+ f"第 {i} 步失败:{description}\n"
449
+ f"命令:{command}\n"
450
+ f"报错:{step_result.stderr[:500]}"
451
+ )
452
+ return result
453
+
454
+ # 记录安装目录(通常是第一个 git clone 后的目录)
455
+ result.install_dir = self.executor._current_dir
456
+ result.success = True
457
+ logger.info("="*50)
458
+ logger.info(t("exec.install_done", project=project_name))
459
+ if launch_command:
460
+ logger.info(t("exec.launch_cmd", cmd=launch_command))
461
+ logger.info(t("exec.install_dir", dir=result.install_dir))
462
+ logger.info("="*50)
463
+
464
+ return result
465
+
466
+ def _try_fix(self, step_result: StepResult, step_num: int, total: int) -> bool:
467
+ """
468
+ 尝试自动修复失败的命令。
469
+ 策略:规则引擎优先(确定性修复)→ LLM 兜底(智能分析)。
470
+ 返回 True 表示修复成功。
471
+ """
472
+ # ── 阶段1:规则引擎(无需 LLM,毫秒级响应)──
473
+ try:
474
+ from .error_fixer import diagnose
475
+ except ImportError:
476
+ from error_fixer import diagnose
477
+
478
+ fix = diagnose(step_result.command, step_result.stderr, step_result.stdout)
479
+ if fix:
480
+ logger.info(t("exec.rule_diagnosis", cause=fix.root_cause, confidence=fix.confidence))
481
+
482
+ if not fix.fix_commands and not fix.retry_original:
483
+ if fix.outcome == "trusted_failure":
484
+ step_result.error_message = fix.root_cause
485
+ return False
486
+ # 无需修复,直接标记成功(如 git clone 目录已存在、npm audit 警告)
487
+ step_result.success = True
488
+ step_result.fix_applied = True
489
+ step_result.fix_command = "(跳过)"
490
+ return True
491
+
492
+ # 执行修复命令
493
+ for fix_cmd in fix.fix_commands:
494
+ logger.info(t("exec.fix_cmd", cmd=fix_cmd))
495
+ fix_result = self.executor.run(fix_cmd)
496
+ if not fix_result.success:
497
+ logger.warning(t("exec.fix_failed"))
498
+ break
499
+ else:
500
+ # 所有修复命令成功
501
+ if fix.retry_original:
502
+ logger.info(t("exec.retrying"))
503
+ retry_result = self.executor.run(step_result.command)
504
+ if retry_result.success:
505
+ logger.info(t("exec.rule_fix_ok"))
506
+ step_result.fix_applied = True
507
+ step_result.fix_command = " && ".join(fix.fix_commands)
508
+ step_result.success = True
509
+ return True
510
+ else:
511
+ # fix_commands 自身就是替代方案,不需要重试原命令
512
+ logger.info(t("exec.rule_fix_ok"))
513
+ step_result.fix_applied = True
514
+ step_result.fix_command = " && ".join(fix.fix_commands)
515
+ step_result.success = True
516
+ return True
517
+
518
+ # ── 阶段2:LLM 智能修复(需要 provider 可用)──
519
+ if not self.llm:
520
+ return False
521
+
522
+ logger.info(t("exec.llm_fallback"))
523
+
524
+ try:
525
+ from .llm import ERROR_FIX_SYSTEM_PROMPT
526
+ except ImportError:
527
+ from llm import ERROR_FIX_SYSTEM_PROMPT
528
+ user_prompt = f"""
529
+ 报错命令:{step_result.command}
530
+
531
+ STDERR 输出:
532
+ {step_result.stderr[:2000]}
533
+
534
+ STDOUT 输出(最后部分):
535
+ {step_result.stdout[-1000:]}
536
+
537
+ 系统:{platform.system()} {platform.machine()}
538
+ """
539
+ try:
540
+ response = self.llm.complete(ERROR_FIX_SYSTEM_PROMPT, user_prompt, max_tokens=1024)
541
+ fix_data = json.loads(response)
542
+ fix_commands = fix_data.get("fix_commands", [])
543
+ root_cause = fix_data.get("root_cause", "")
544
+
545
+ if root_cause:
546
+ logger.info(t("exec.llm_root_cause", cause=root_cause))
547
+
548
+ if not fix_commands:
549
+ return False
550
+
551
+ # 执行修复命令
552
+ for fix_cmd in fix_commands:
553
+ # 安全检查:LLM 生成的修复命令也必须通过安全过滤
554
+ is_safe, safety_msg = check_command_safety(fix_cmd)
555
+ if not is_safe:
556
+ logger.warning(t("exec.fix_rejected", cmd=fix_cmd))
557
+ return False
558
+ logger.info(t("exec.fix_cmd", cmd=fix_cmd))
559
+ fix_result = self.executor.run(fix_cmd)
560
+ if not fix_result.success:
561
+ logger.error(t("exec.fix_cmd_failed"))
562
+ return False
563
+
564
+ # 重新执行原命令
565
+ logger.info(t("exec.retrying"))
566
+ retry_result = self.executor.run(step_result.command)
567
+ if retry_result.success:
568
+ logger.info(t("exec.llm_fix_ok"))
569
+ step_result.fix_applied = True
570
+ step_result.fix_command = " && ".join(fix_commands)
571
+ step_result.success = True
572
+ return True
573
+
574
+ except (json.JSONDecodeError, Exception) as e:
575
+ logger.warning(t("exec.llm_fix_error", error=e))
576
+
577
+ return False