from __future__ import annotations import json from datetime import datetime from pathlib import Path from zipfile import ZIP_DEFLATED, ZipFile from app.core.files import runtime_root, tail_lines from app.services.detect_service import get_detect_status from app.services.runtime_status_service import get_runtime_status from app.services.settings_service import get_settings_payload def _tail_api_runtime_log(filename: str, max_lines: int = 80) -> list[str]: path = Path(__file__).resolve().parents[2] / "runtime" / "logs" / filename if not path.exists(): return [] with path.open("r", encoding="utf-8", errors="replace") as handle: return handle.read().splitlines()[-max_lines:] def latest_logs() -> dict: worker_lines = tail_lines("detect_worker.log", max_lines=80) desktop_lines = tail_lines("logs/app.log", max_lines=60) api_stdout_lines = _tail_api_runtime_log("domain-api.stdout.log", max_lines=60) api_stderr_lines = _tail_api_runtime_log("domain-api.stderr.log", max_lines=60) api_lines = api_stderr_lines + api_stdout_lines + desktop_lines summary = "未发现显著异常" level = "info" if any("Redis订阅失败" in line for line in worker_lines): summary = "检测端存在 Redis 订阅读超时重连,需要后续继续优化订阅策略。" level = "warning" elif any("无可用代理" in line for line in worker_lines): summary = "代理池存在无可用代理情况,检测端当前可能回落直连或等待代理。" level = "warning" elif any("Traceback" in line or "ERROR:" in line for line in api_lines): summary = "API 运行日志中发现异常堆栈,请优先检查 domain-api stderr 日志。" level = "warning" return { "worker": worker_lines, "api": api_lines, "diagnostics": { "summary": summary, "level": level, }, } def build_diagnostic_bundle() -> tuple[Path, str]: payload = latest_logs() generated_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") bundle_dir = runtime_root() / "diagnostics" bundle_dir.mkdir(parents=True, exist_ok=True) zip_path = bundle_dir / f"diagnostic_bundle_{timestamp}.zip" settings_payload = get_settings_payload() detect_status = get_detect_status() runtime_status = get_runtime_status() with ZipFile(zip_path, "w", compression=ZIP_DEFLATED) as archive: archive.writestr( "summary.json", json.dumps( { "generated_at": generated_at, "diagnostics": payload["diagnostics"], "runtime_status": runtime_status, "detect_status": detect_status, }, ensure_ascii=False, indent=2, default=str, ), ) archive.writestr( "settings_snapshot.json", json.dumps(settings_payload, ensure_ascii=False, indent=2, default=str), ) archive.writestr("logs/worker.log", "\n".join(payload["worker"])) archive.writestr("logs/api.log", "\n".join(payload["api"])) return zip_path, zip_path.name