"""Runtime patch layer for VNEWS. Keeps the current large app intact, but replaces fragile AI wall endpoints with stable JSON endpoints and injects frontend safeJson wrappers. """ import hashlib import time import os from urllib.parse import quote import requests from bs4 import BeautifulSoup from fastapi import Request from fastapi.responses import JSONResponse, HTMLResponse import main as _main app = _main.app DEFAULT_IMG = "https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg" def _remove_routes(paths): app.router.routes = [r for r in app.router.routes if getattr(r, "path", None) not in set(paths)] def _safe_text(v): return (v or "").strip() def _ensure_article(url: str): data = None try: if hasattr(_main, "_article_by_url"): data = _main._article_by_url(url) except Exception: data = None if not data: try: data = _main._scrape_generic_article(url) if hasattr(_main, "_scrape_generic_article") else None except Exception: data = None if not data: data = {"title": "", "summary": "", "og_image": "", "body": [], "url": url, "source": "generic"} title = _safe_text(data.get("title")) summary = _safe_text(data.get("summary")) img = _safe_text(data.get("og_image")) body = data.get("body") or [] if not title or not summary or not img or not body: try: r = requests.get(url, headers=getattr(_main, "HEADERS", {}), timeout=15) r.encoding = "utf-8" soup = BeautifulSoup(r.text, "lxml") if not title: tag = soup.find("meta", property="og:title") or soup.find("title") title = tag.get("content", "").strip() if tag and tag.name == "meta" else (tag.get_text(strip=True) if tag else "") if not summary: tag = soup.find("meta", property="og:description") or soup.find("meta", attrs={"name": "description"}) summary = tag.get("content", "").strip() if tag else "" if not img: tag = soup.find("meta", property="og:image") or soup.find("meta", attrs={"name": "twitter:image"}) img = tag.get("content", "").strip() if tag else "" if not body: ps = [] for p in soup.find_all("p"): t = p.get_text(" ", strip=True) if len(t) > 40: ps.append({"type": "p", "text": t}) if len(ps) >= 30: break body = ps except Exception: pass if not summary and body: first = next((b.get("text", "") for b in body if b.get("type") == "p" and b.get("text")), "") summary = first[:360] if not title: title = url if not img: img = DEFAULT_IMG if not body and summary: body = [{"type": "p", "text": summary}] data.update({"title": title, "summary": summary, "og_image": img, "body": body, "url": url}) return data def _rewrite(data, tone="tu-nhien"): try: if hasattr(_main, "_ai_rewrite_article"): text = _main._ai_rewrite_article(data, tone=tone) if text and len(text.strip()) > 50: return text.strip() except Exception: pass title = data.get("title", "") summary = data.get("summary", "") ps = [b.get("text", "") for b in data.get("body", []) if b.get("type") == "p" and b.get("text")] lead = summary or (ps[0] if ps else "") points = "\n".join(["• " + p[:220] + ("..." if len(p) > 220 else "") for p in ps[:5]]) body = "\n\n".join(ps[:10]) return (f"Bản tin AI viết lại: {title}\n\n{lead}\n\n{body}\n\nĐiểm chính:\n{points}").strip() def _topic_image(topic): try: if hasattr(_main, "_image_for_topic"): return _main._image_for_topic(topic) except Exception: pass return "https://image.pollinations.ai/prompt/" + quote("editorial illustration Vietnamese news " + topic, safe="") + "?width=1024&height=576&nologo=true" def _save_post(post): try: posts = _main._load_wall() if hasattr(_main, "_load_wall") else [] except Exception: posts = [] posts.insert(0, post) try: if hasattr(_main, "_save_wall"): _main._save_wall(posts) except Exception: pass return post _remove_routes(["/api/url_wall", "/api/topic_post", "/api/rewrite_share", "/"]) @app.post("/api/url_wall") async def patched_url_wall(request: Request): try: body = await request.json() except Exception: body = {} url = _safe_text(body.get("url")) tone = _safe_text(body.get("tone")) or "tu-nhien" if not url: return JSONResponse({"error": "missing url"}, status_code=400) try: data = _ensure_article(url) text = _rewrite(data, tone=tone) post = { "id": hashlib.md5((url + str(time.time())).encode()).hexdigest()[:12], "url": url, "title": data.get("title") or url, "summary": data.get("summary") or "", "img": data.get("og_image") or DEFAULT_IMG, "text": text or (data.get("summary") or data.get("title") or url), "source": data.get("source", "url"), "ts": int(time.time()), } _save_post(post) return JSONResponse({"post": post}) except Exception as e: return JSONResponse({"error": "Không tạo được tóm tắt URL", "detail": str(e)[:300]}, status_code=500) @app.post("/api/rewrite_share") async def patched_rewrite_share(request: Request): return await patched_url_wall(request) @app.post("/api/topic_post") async def patched_topic_post(request: Request): try: body = await request.json() except Exception: body = {} topic = _safe_text(body.get("topic")) tone = _safe_text(body.get("tone")) or "tu-nhien" if not topic: return JSONResponse({"error": "missing topic"}, status_code=400) try: context = "" try: if hasattr(_main, "_topic_article_context"): context = _main._topic_article_context(topic) if not context and hasattr(_main, "_web_context"): context = _main._web_context(topic) except Exception: context = "" if not context: context = f"Chủ đề: {topic}" data = {"title": topic, "summary": context[:420], "og_image": _topic_image(topic), "body": [{"type": "p", "text": context}], "source": "topic", "url": ""} text = _rewrite(data, tone=tone) post = { "id": hashlib.md5((topic + str(time.time())).encode()).hexdigest()[:12], "url": "", "title": topic, "summary": data["summary"], "img": data["og_image"] or DEFAULT_IMG, "text": text or context, "source": "topic", "ts": int(time.time()), } _save_post(post) return JSONResponse({"post": post}) except Exception as e: return JSONResponse({"error": "Không tạo được bài theo chủ đề", "detail": str(e)[:300]}, status_code=500) _FRONTEND_PATCH = r''' ''' @app.get("/") async def patched_index(): try: with open("/app/static/index.html", "r", encoding="utf-8") as f: html = f.read() if "window.safeJson" not in html: html = html.replace("", _FRONTEND_PATCH + "") return HTMLResponse(content=html) except Exception as e: return HTMLResponse(content=f"
Index error: {str(e)}
", status_code=500)