Files
my-project/tools/why.py
T

510 lines
20 KiB
Python

#!/usr/bin/env python3
"""
OpenClaw 问题诊断工具
仅识别问题,不自动修复
"""
import os
import sys
import json
import psutil
import socket
import requests
import subprocess
from datetime import datetime
from pathlib import Path
class OpenClawDiagnoser:
def __init__(self):
self.problems = []
self.config_path = Path.home() / ".openclaw" / "openclaw.json"
self.log_dir = Path.home() / ".openclaw"
self.tmp_dir = Path("/tmp")
def log_problem(self, severity, category, message, evidence=None):
"""记录发现的问题"""
problem = {
"severity": severity, # CRITICAL, HIGH, MEDIUM, LOW, INFO
"category": category,
"message": message,
"evidence": evidence,
"timestamp": datetime.now().isoformat()
}
self.problems.append(problem)
print(f"[{severity}] {category}: {message}")
if evidence:
print(f" 证据: {evidence}")
def check_gateway_process(self):
"""检查网关进程状态"""
print("\n🔍 检查网关进程...")
gateway_processes = []
for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'status']):
try:
if proc.info['cmdline']:
cmdline = ' '.join(proc.info['cmdline'])
if 'openclaw' in cmdline.lower() and 'gateway' in cmdline.lower():
gateway_processes.append({
'pid': proc.info['pid'],
'cmdline': cmdline,
'status': proc.info['status']
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
if not gateway_processes:
self.log_problem("CRITICAL", "进程", "未找到OpenClaw网关进程")
return False
print(f"找到 {len(gateway_processes)} 个网关进程:")
for proc in gateway_processes:
print(f" PID: {proc['pid']}, 状态: {proc['status']}")
# 检查进程状态
if proc['status'] not in ['running', 'sleeping']:
self.log_problem("HIGH", "进程", f"进程状态异常: {proc['status']}",
f"PID: {proc['pid']}")
return True
def check_port_listening(self):
"""检查端口监听状态"""
print("\n🔍 检查端口监听...")
port = 18789
try:
# 检查端口是否被占用
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
result = sock.connect_ex(('127.0.0.1', port))
sock.close()
if result == 0:
print(f"✅ 端口 {port} 正在监听")
# 查看哪个进程在监听
try:
output = subprocess.check_output(
f"ss -tulpn | grep :{port}",
shell=True,
text=True
).strip()
if output:
print(f" 监听进程: {output}")
except:
pass
else:
self.log_problem("HIGH", "网络", f"端口 {port} 未监听",
"网关可能未启动或绑定失败")
return False
except Exception as e:
self.log_problem("MEDIUM", "网络", f"检查端口时出错: {str(e)}")
return True
def check_web_access(self):
"""检查Web服务访问"""
print("\n🔍 检查Web服务...")
urls = [
("http://localhost:18789/", "本地访问"),
("http://127.0.0.1:18789/", "本地回环"),
]
accessible = False
for url, desc in urls:
try:
response = requests.get(url, timeout=5)
if response.status_code == 200:
print(f"✅ {desc}: 成功 (HTTP {response.status_code})")
accessible = True
# 检查返回内容
if "openclaw" in response.text.lower():
print(" 页面包含 'openclaw'")
else:
self.log_problem("MEDIUM", "服务", "返回页面可能不是OpenClaw控制台")
else:
print(f"⚠️ {desc}: HTTP {response.status_code}")
self.log_problem("MEDIUM", "服务", f"{desc}返回异常状态码",
f"URL: {url}, 状态码: {response.status_code}")
except requests.ConnectionError:
print(f"❌ {desc}: 连接失败")
if desc == "本地访问":
self.log_problem("HIGH", "服务", "无法连接到本地网关", url)
except requests.Timeout:
print(f"❌ {desc}: 连接超时")
self.log_problem("MEDIUM", "服务", "连接网关超时", url)
except Exception as e:
print(f"❌ {desc}: 错误 {str(e)}")
return accessible
def check_lock_files(self):
"""检查锁文件"""
print("\n🔍 检查锁文件...")
lock_files = []
# 检查配置目录
if self.log_dir.exists():
lock_patterns = ['*.lock', '*lock*']
for pattern in lock_patterns:
for lock_file in self.log_dir.rglob(pattern):
if lock_file.is_file():
lock_files.append(str(lock_file))
# 检查/tmp目录
for pattern in ['openclaw*lock*', '*openclaw*lock*']:
for lock_file in self.tmp_dir.glob(pattern):
if lock_file.is_file():
lock_files.append(str(lock_file))
if lock_files:
print(f"⚠️ 发现 {len(lock_files)} 个锁文件:")
for lock in sorted(lock_files)[:10]: # 只显示前10个
print(f" {lock}")
if len(lock_files) > 10:
print(f" ... 还有 {len(lock_files)-10} 个未显示")
if len(lock_files) > 5:
self.log_problem("HIGH", "文件锁", f"发现 {len(lock_files)} 个锁文件",
"过多的锁文件可能导致死锁")
elif len(lock_files) > 0:
self.log_problem("MEDIUM", "文件锁", f"发现 {len(lock_files)} 个锁文件")
else:
print("✅ 未发现锁文件")
return len(lock_files)
def check_config_file(self):
"""检查配置文件"""
print("\n🔍 检查配置文件...")
if not self.config_path.exists():
self.log_problem("CRITICAL", "配置", "配置文件不存在", str(self.config_path))
return False
try:
with open(self.config_path, 'r') as f:
config = json.load(f)
print(f"✅ 配置文件存在: {self.config_path}")
print(f" 文件大小: {os.path.getsize(self.config_path)} 字节")
# 检查关键配置项
required_sections = ['models', 'agents']
for section in required_sections:
if section in config:
print(f" ✅ 包含 {section} 配置")
else:
self.log_problem("HIGH", "配置", f"缺少关键配置项: {section}")
# 检查模型配置
if 'models' in config and 'providers' in config['models']:
providers = config['models']['providers']
print(f" 配置了 {len(providers)} 个模型提供商")
for provider, details in providers.items():
if provider == 'deepseek':
if 'apiKey' in details:
key_len = len(details['apiKey'])
masked = details['apiKey'][:4] + '...' + details['apiKey'][-4:] if key_len > 8 else '***'
print(f" DeepSeek API密钥: {masked}")
else:
self.log_problem("HIGH", "配置", "DeepSeek缺少API密钥")
return True
except json.JSONDecodeError as e:
self.log_problem("CRITICAL", "配置", "配置文件JSON格式错误", str(e))
return False
except Exception as e:
self.log_problem("HIGH", "配置", "读取配置文件时出错", str(e))
return False
def check_model_connectivity(self):
"""检查模型连接性"""
print("\n🔍 检查模型连接性...")
# 从配置文件读取API密钥
api_key = None
try:
if self.config_path.exists():
with open(self.config_path, 'r') as f:
config = json.load(f)
if 'models' in config and 'providers' in config['models']:
providers = config['models']['providers']
if 'deepseek' in providers and 'apiKey' in providers['deepseek']:
api_key = providers['deepseek']['apiKey']
except:
pass
if not api_key:
print("⚠️ 未找到DeepSeek API密钥,跳过连接测试")
return
# 测试DeepSeek API
print("测试DeepSeek API连接...")
try:
headers = {
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
}
data = {
'model': 'deepseek-chat',
'messages': [{'role': 'user', 'content': 'ping'}],
'max_tokens': 5
}
response = requests.post(
'https://api.deepseek.com/v1/chat/completions',
headers=headers,
json=data,
timeout=10
)
if response.status_code == 200:
print(f"✅ DeepSeek API: 连接成功 (HTTP 200)")
try:
result = response.json()
if 'choices' in result and len(result['choices']) > 0:
print(f" 模型响应正常")
else:
self.log_problem("MEDIUM", "模型", "API返回异常格式")
except:
print(f" API返回: {response.text[:100]}...")
elif response.status_code == 401:
self.log_problem("CRITICAL", "模型", "DeepSeek API密钥无效或过期", "HTTP 401")
elif response.status_code == 429:
self.log_problem("HIGH", "模型", "DeepSeek API调用频率限制", "HTTP 429")
elif response.status_code == 500:
self.log_problem("MEDIUM", "模型", "DeepSeek服务器内部错误", "HTTP 500")
else:
self.log_problem("MEDIUM", "模型", f"DeepSeek API返回异常状态码",
f"HTTP {response.status_code}")
except requests.Timeout:
self.log_problem("HIGH", "模型", "DeepSeek API连接超时", "10秒超时")
except requests.ConnectionError:
self.log_problem("HIGH", "模型", "无法连接到DeepSeek API", "网络连接失败")
except Exception as e:
self.log_problem("MEDIUM", "模型", f"测试DeepSeek API时出错: {str(e)}")
def check_system_resources(self):
"""检查系统资源"""
print("\n🔍 检查系统资源...")
# 内存使用
memory = psutil.virtual_memory()
print(f"内存: {memory.percent}% 已使用 ({memory.used//1024//1024}MB/{memory.total//1024//1024}MB)")
if memory.percent > 90:
self.log_problem("HIGH", "资源", "系统内存使用过高", f"{memory.percent}% 已使用")
elif memory.percent > 80:
self.log_problem("MEDIUM", "资源", "系统内存使用较高", f"{memory.percent}% 已使用")
# 查找OpenClaw进程内存使用
openclaw_memory = 0
for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
try:
if 'openclaw' in proc.info['name'].lower():
mem = proc.info['memory_info']
if mem:
openclaw_memory += mem.rss
except:
continue
if openclaw_memory > 0:
print(f"OpenClaw进程内存: {openclaw_memory//1024//1024}MB")
if openclaw_memory > 1024*1024*1024: # 超过1GB
self.log_problem("HIGH", "资源", "OpenClaw内存使用过高",
f"{openclaw_memory//1024//1024}MB")
def check_log_files(self):
"""检查日志文件"""
print("\n🔍 检查日志文件...")
log_files = []
error_patterns = ['error', 'fail', 'timeout', 'locked', 'exception', 'crash']
# 查找日志文件
log_patterns = ['*.log', 'gateway*.log', 'openclaw*.log']
for pattern in log_patterns:
# 检查 ~/.openclaw
for log_file in self.log_dir.rglob(pattern):
if log_file.is_file():
log_files.append(log_file)
# 检查 /tmp/openclaw
tmp_log_dir = self.tmp_dir / "openclaw"
if tmp_log_dir.exists():
for log_file in tmp_log_dir.rglob(pattern):
if log_file.is_file():
log_files.append(log_file)
if log_files:
print(f"找到 {len(log_files)} 个日志文件")
recent_errors = []
for log_file in sorted(log_files, key=lambda x: x.stat().st_mtime, reverse=True)[:3]:
print(f" {log_file} (修改时间: {datetime.fromtimestamp(log_file.stat().st_mtime)})")
# 检查最近错误
try:
with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
lines = f.readlines()[-100:] # 最后100行
errors = []
for i, line in enumerate(lines):
line_lower = line.lower()
if any(pattern in line_lower for pattern in error_patterns):
# 获取上下文
context_start = max(0, i-1)
context_end = min(len(lines), i+2)
context = ''.join(lines[context_start:context_end])
errors.append(context.strip())
if errors:
recent_errors.extend(errors[-3:]) # 最多记录3个错误
except Exception as e:
print(f" 读取日志失败: {str(e)}")
if recent_errors:
print(f" 发现 {len(recent_errors)} 个最近错误")
for err in recent_errors[:3]:
print(f" - {err[:200]}...")
self.log_problem("MEDIUM", "日志", "日志中发现错误信息",
f"共 {len(recent_errors)} 个错误")
else:
print("未找到日志文件")
def run_diagnostics(self):
"""运行所有诊断"""
print("="*60)
print("OpenClaw 问题诊断工具")
print(f"运行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("="*60)
# 运行所有检查
checks = [
("系统资源", self.check_system_resources),
("配置文件", self.check_config_file),
("网关进程", self.check_gateway_process),
("端口监听", self.check_port_listening),
("Web服务", self.check_web_access),
("文件锁", self.check_lock_files),
("模型连接", self.check_model_connectivity),
("日志文件", self.check_log_files),
]
for name, check_func in checks:
try:
check_func()
except Exception as e:
print(f"❌ 检查 {name} 时出错: {str(e)}")
# 生成报告
self.generate_report()
def generate_report(self):
"""生成诊断报告"""
print("\n" + "="*60)
print("诊断报告摘要")
print("="*60)
if not self.problems:
print("✅ 未发现严重问题")
return
# 按严重程度分组
severity_order = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO']
problems_by_severity = {severity: [] for severity in severity_order}
for problem in self.problems:
problems_by_severity[problem['severity']].append(problem)
# 输出摘要
for severity in severity_order:
problems = problems_by_severity[severity]
if problems:
print(f"\n{severity}级别问题 ({len(problems)}个):")
for i, problem in enumerate(problems, 1):
print(f" {i}. {problem['category']}: {problem['message']}")
# 保存详细报告
report_path = Path.home() / ".openclaw" / f"diagnosis_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
try:
report = {
"timestamp": datetime.now().isoformat(),
"summary": {
"total_problems": len(self.problems),
"critical": len(problems_by_severity['CRITICAL']),
"high": len(problems_by_severity['HIGH']),
"medium": len(problems_by_severity['MEDIUM']),
"low": len(problems_by_severity['LOW']),
},
"problems": self.problems
}
with open(report_path, 'w', encoding='utf-8') as f:
json.dump(report, f, indent=2, ensure_ascii=False)
print(f"\n📄 详细报告已保存到: {report_path}")
except Exception as e:
print(f"保存报告失败: {str(e)}")
def get_recommendations(self):
"""根据问题提供建议"""
recommendations = []
for problem in self.problems:
if problem['severity'] in ['CRITICAL', 'HIGH']:
if "未找到OpenClaw网关进程" in problem['message']:
recommendations.append("建议: 启动网关 - `openclaw gateway --force &`")
elif "端口未监听" in problem['message']:
recommendations.append("建议: 检查网关是否启动,或尝试不同端口")
elif "DeepSeek API密钥无效" in problem['message']:
recommendations.append("建议: 更新DeepSeek API密钥 - `openclaw config set provider.deepseek.api_key '新密钥'`")
elif "文件锁" in problem['message'] and "过多" in problem['message']:
recommendations.append("建议: 清理锁文件 - `find ~/.openclaw -name '*.lock' -delete`")
if recommendations:
print("\n💡 建议操作:")
for i, rec in enumerate(recommendations, 1):
print(f" {i}. {rec}")
def main():
"""主函数"""
diagnoser = OpenClawDiagnoser()
try:
diagnoser.run_diagnostics()
diagnoser.get_recommendations()
except KeyboardInterrupt:
print("\n\n诊断被用户中断")
sys.exit(1)
except Exception as e:
print(f"诊断过程中发生错误: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
# 检查Python版本和依赖
if sys.version_info < (3, 6):
print("需要Python 3.6或更高版本")
sys.exit(1)
try:
import psutil
import requests
except ImportError as e:
print(f"缺少依赖: {e}")
print("请安装: pip install psutil requests")
sys.exit(1)
main()