Mini-app updates: UI TG MAX session nav logs
This commit is contained in:
@@ -383,8 +383,13 @@ async def list_drafts(
|
||||
if facts_short and len(facts_short) > 200:
|
||||
facts_short = facts_short[:200].rstrip() + '…'
|
||||
|
||||
# Подробное описание (для превью)
|
||||
problem_text = payload.get('problem_description', '')
|
||||
# Подробное описание (для превью); n8n может сохранять в description/chatInput
|
||||
problem_text = (
|
||||
payload.get('problem_description')
|
||||
or payload.get('description')
|
||||
or payload.get('chatInput')
|
||||
or ''
|
||||
)
|
||||
|
||||
# Считаем документы
|
||||
documents_meta = payload.get('documents_meta') or []
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
Ticket Form Intake Platform - FastAPI Backend
|
||||
"""
|
||||
from fastapi import FastAPI, Request
|
||||
import json
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from .config import settings, get_cors_origins_live, get_settings
|
||||
from .services.database import db
|
||||
@@ -23,6 +27,82 @@ logging.basicConfig(
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEBUG_SESSION_ID = "2a4d38"
|
||||
# В прод-контейнере гарантированно доступен /app/logs (volume ./backend/logs:/app/logs)
|
||||
DEBUG_LOG_PATH = "/app/logs/cursor-debug-2a4d38.log"
|
||||
|
||||
|
||||
def _debug_write(
|
||||
*,
|
||||
hypothesis_id: str,
|
||||
run_id: str,
|
||||
location: str,
|
||||
message: str,
|
||||
data: Dict[str, Any],
|
||||
) -> None:
|
||||
"""
|
||||
NDJSON debug log for Cursor Debug Mode.
|
||||
IMPORTANT: do not log secrets/PII (tokens, tg hash, full init_data, phone, etc).
|
||||
"""
|
||||
try:
|
||||
ts = int(time.time() * 1000)
|
||||
entry = {
|
||||
"sessionId": DEBUG_SESSION_ID,
|
||||
"id": f"log_{ts}_{uuid.uuid4().hex[:8]}",
|
||||
"timestamp": ts,
|
||||
"location": location,
|
||||
"message": message,
|
||||
"data": data,
|
||||
"runId": run_id,
|
||||
"hypothesisId": hypothesis_id,
|
||||
}
|
||||
with open(DEBUG_LOG_PATH, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||
except Exception:
|
||||
# Never break prod request handling due to debug logging
|
||||
return
|
||||
|
||||
|
||||
def _extract_client_bundle_info(payload: Dict[str, Any]) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Returns (moduleUrl, scriptSrc, build) from the last 'boot' entry if present.
|
||||
"""
|
||||
logs = payload.get("logs") or []
|
||||
if not isinstance(logs, list):
|
||||
return (None, None, None)
|
||||
for entry in reversed(logs):
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
if entry.get("event") != "boot":
|
||||
continue
|
||||
data = entry.get("data") if isinstance(entry.get("data"), dict) else {}
|
||||
module_url = data.get("moduleUrl") if isinstance(data.get("moduleUrl"), str) else None
|
||||
script_src = data.get("scriptSrc") if isinstance(data.get("scriptSrc"), str) else None
|
||||
build = data.get("build") if isinstance(data.get("build"), str) else None
|
||||
return (module_url, script_src, build)
|
||||
return (None, None, None)
|
||||
|
||||
|
||||
def _extract_last_window_error(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
logs = payload.get("logs") or []
|
||||
if not isinstance(logs, list):
|
||||
return {}
|
||||
for entry in reversed(logs):
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
if entry.get("event") != "window_error":
|
||||
continue
|
||||
data = entry.get("data") if isinstance(entry.get("data"), dict) else {}
|
||||
# Keep only safe fields
|
||||
return {
|
||||
"message": data.get("message"),
|
||||
"filename": data.get("filename"),
|
||||
"lineno": data.get("lineno"),
|
||||
"colno": data.get("colno"),
|
||||
"hasStack": bool(data.get("stack")),
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
@@ -246,6 +326,71 @@ async def get_client_ip(request: Request):
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/v1/utils/client-log")
|
||||
async def client_log(request: Request):
|
||||
"""
|
||||
Принимает клиентские логи (для отладки webview/miniapp) и пишет в backend-логи.
|
||||
Формат: { reason, client: {...}, logs: [...] }
|
||||
"""
|
||||
client_host = request.client.host if request.client else None
|
||||
ua = request.headers.get("user-agent", "")
|
||||
try:
|
||||
payload = await request.json()
|
||||
except Exception:
|
||||
payload = {"error": "invalid_json"}
|
||||
|
||||
# Cursor debug-mode evidence (sanitized)
|
||||
try:
|
||||
if isinstance(payload, dict):
|
||||
reason = payload.get("reason")
|
||||
client = payload.get("client") if isinstance(payload.get("client"), dict) else {}
|
||||
pathname = client.get("pathname") if isinstance(client.get("pathname"), str) else None
|
||||
origin = client.get("origin") if isinstance(client.get("origin"), str) else None
|
||||
logs = payload.get("logs") if isinstance(payload.get("logs"), list) else []
|
||||
|
||||
module_url, script_src, build = _extract_client_bundle_info(payload)
|
||||
last_err = _extract_last_window_error(payload)
|
||||
first_err_file = None
|
||||
last_err_file = None
|
||||
if isinstance(logs, list):
|
||||
for e in logs:
|
||||
if isinstance(e, dict) and e.get("event") == "window_error":
|
||||
d = e.get("data") if isinstance(e.get("data"), dict) else {}
|
||||
fn = d.get("filename")
|
||||
if isinstance(fn, str):
|
||||
if first_err_file is None:
|
||||
first_err_file = fn
|
||||
last_err_file = fn
|
||||
|
||||
_debug_write(
|
||||
hypothesis_id="H1",
|
||||
run_id="pre-fix",
|
||||
location="backend/app/main.py:client_log",
|
||||
message="client_log_received",
|
||||
data={
|
||||
"ip": client_host,
|
||||
"uaPrefix": ua[:80] if isinstance(ua, str) else "",
|
||||
"reason": reason,
|
||||
"origin": origin,
|
||||
"pathname": pathname,
|
||||
"logsCount": len(logs) if isinstance(logs, list) else None,
|
||||
"boot": {"moduleUrl": module_url, "scriptSrc": script_src, "build": build},
|
||||
"windowErrorLast": last_err,
|
||||
"windowErrorFiles": {"first": first_err_file, "last": last_err_file},
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Ограничим размер вывода, но оставим самое важное
|
||||
try:
|
||||
s = json.dumps(payload, ensure_ascii=False)[:20000]
|
||||
except Exception:
|
||||
s = str(payload)[:20000]
|
||||
logger.warning(f"📱 CLIENT_LOG ip={client_host} ua={ua} payload={s}")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/v1/info")
|
||||
async def info():
|
||||
"""Информация о платформе"""
|
||||
|
||||
Reference in New Issue
Block a user