""" Canvas 诊断报告生成器 (v5.0) 自动生成交互式的诊断报告 HTML 页面 功能: - 封装 CanvasSnapshot 工具调用 - HTML+JS 模板渲染 - Canvas URL 注册和持久化 - PNG/HTML 格式输出 使用方法: from tools.canvas_report_generator import CanvasReportGenerator generator = CanvasReportGenerator() html = generator.generate_report(diag_data) png_url = generator.generate_png_screenshot(html, timeoutMs=15000) """ import json import time from pathlib import Path from datetime import datetime from typing import Dict, Any, Optional, Tuple class CanvasReportGenerator: """ Canvas 诊断报告生成器 v5.0 功能: - 封装 CanvasSnapshot 工具调用 - HTML+JS 模板渲染 - Canvas URL 注册和持久化 - PNG/HTML 格式输出 """ def __init__(self, canvas_root: str = str(Path.home() / ".openclaw" / "canvas")): """ 初始化 Canvas 报告生成器 Args: canvas_root: Canvas 文档根目录路径(默认使用 OpenClaw 标准路径) """ self.canvas_root = Path(canvas_root) / "documents" / "reports" def _generate_timestamp(self) -> str: """生成时间戳用于文件命名""" return datetime.now().strftime("%Y%m%d_%H%M%S") def generate_diagnosis_data( self, exec_output: str, problem_context: str = "CLI/Config", evidence_chain: Optional[Dict[str, Any]] = None ) -> Dict[str, Any]: """ 生成诊断报告所需的数据结构 Args: exec_output: 原始 exec 输出(包含错误信息) problem_context: 问题上下文(如:"CLI/Config", "Tooling") evidence_chain: 来自 MODULE_02_SearchChain 的证据链数据 Returns: Dict[str, Any]: 结构化诊断数据 """ # 提取关键信息(简化版,实际可使用 ELIS 分析) risk_level = self._infer_risk_level(exec_output) problem_type = f"MRE-{problem_context} Failure" affected_tools = ["openclaw", "exec"] error_logs = exec_output[:1000] if len(exec_output) > 1000 else exec_output # AI 根因分析(简化版) root_cause = { "core_issue": self._extract_core_issue(exec_output), "causes": self._extract_possible_causes(exec_output), "confidence_score": self._estimate_confidence(exec_output) } # 修复建议 fix_command = self._generate_fix_command(problem_context, exec_output) # 回滚命令 rollback_command = f"""# 回滚命令 (如需要撤销): # openclaw gateway status # git checkout HEAD~1 -- .openclaw/ # python -m venv {Path.home()}\\.venv # pip install -r ~/.openclaw/workspace/.openclaw/requirements.txt""" # 证据链信息(如果提供) evidence_info = { "docs_match": evidence_chain.get("docs_match", False) if evidence_chain else False, "gh_match": evidence_chain.get("gh_match", False) if evidence_chain else False, "pattern_matches": evidence_chain.get("pattern_matches", []) if evidence_chain else [], "docs_confidence": evidence_chain.get("docs_confidence", 0.85) if evidence_chain else 0.85, "gh_issue": evidence_chain.get("gh_issue", "N/A") if evidence_chain else "N/A", "confidence_score": evidence_chain.get("confidence_score", 0.92) if evidence_chain else 0.92 } if evidence_chain else { "docs_match": False, "gh_match": False, "pattern_matches": [], "docs_confidence": 0.85, "gh_issue": "N/A", "confidence_score": 0.92 } return { "riskLevel": risk_level, "problemType": problem_type, "affectedTools": affected_tools, "errorLogs": error_logs, "rootCause": root_cause, "fixCommand": fix_command, "rollbackCommand": rollback_command, "evidenceChain": evidence_info } def _infer_risk_level(self, exec_output: str) -> str: """根据错误类型推断风险等级""" keywords = { "Critical": ["fatal", "crash", "segfault"], "Medium": ["error", "fail", "exception"], "Low": ["warning", "notice", "info"] } for level, keyword_list in keywords.items(): if any(kw.lower() in exec_output.lower() for kw in keyword_list): return level return "Medium" # 默认中风险 def _extract_core_issue(self, exec_output: str) -> str: """提取核心问题(简化版,实际需使用 ELIS)""" lines = exec_output.strip().split('\n') for line in lines: if 'error' in line.lower() or 'fail' in line.lower(): return line.strip()[:100] # 取前 100 字符 return "未检测到明确错误信息" def _extract_possible_causes(self, exec_output: str) -> list[str]: """提取可能原因(简化版)""" causes = [] if 'pty' in exec_output.lower(): causes.append("当前会话配置中缺少 pty 参数") if 'permission' in exec_output.lower(): causes.append("执行命令需要 sudo/管理员权限") if 'timeout' in exec_output.lower(): causes.append("执行超时或卡住(未指定 yieldMs)") if 'node' in exec_output.lower() or 'python' in exec_output.lower(): causes.append("依赖项安装不完整或版本不匹配") if not causes: causes = ["环境配置可能需要重新初始化"] return causes[:3] # 最多 3 个原因 def _estimate_confidence(self, exec_output: str) -> float: """估计置信度(简化版)""" error_count = exec_output.lower().count('error') + exec_output.lower().count('fail') if error_count == 0: return 0.5 elif error_count <= 3: return 0.75 else: return min(0.6, 0.8 - (error_count * 0.1)) def _generate_fix_command(self, problem_context: str, exec_output: str) -> str: """生成修复建议命令(简化版)""" base_command = "openclaw doctor --fix" if 'pty' in exec_output.lower(): return f"{base_command} --pty=true --yieldMs=15000" elif 'permission' in exec_output.lower(): return f"sudo {base_command}" elif 'timeout' in exec_output.lower(): return f"{base_command} --timeout=300" return base_command def generate_report(self, diag_data: Dict[str, Any], output_format: str = "html") -> str: """ 生成诊断报告 Args: diag_data: generate_diagnosis_data() 返回的诊断数据 output_format: 输出格式("html" 或 "png") Returns: 生成的报告内容(HTML 字符串或 PNG 路径) """ # 使用 CanvasScript_DiagnosticReport.js 中的模板 script_path = Path(__file__).parent / "CanvasScript_DiagnosticReport.js" if output_format == "html": return self._render_html_template(diag_data, script_path) else: return self._write_to_canvas_and_screenshot(diag_data, output_format) def _render_html_template(self, diag_data: Dict[str, Any], script_path: Path) -> str: """渲染 HTML 模板(简化版,实际使用 CanvasScript_DiagnosticReport.js)""" risk_level = diag_data["riskLevel"] problem_type = diag_data["problemType"] # 风险颜色 risk_colors = { "Critical": "#dc3545", "Medium": "#ffc107", "Low": "#28a745" } risk_color = risk_colors.get(risk_level, "#6c757d") # 简化的 HTML 模板(实际使用 CanvasScript_DiagnosticReport.js) html_content = f"""
{problem_type}
{diag_data['errorLogs']}
核心问题: {diag_data['rootCause']['core_issue']}
可能原因:
{diag_data['fixCommand']}