Spaces:
Running
Running
| """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", "/"]) | |
| 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) | |
| async def patched_rewrite_share(request: Request): | |
| return await patched_url_wall(request) | |
| 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''' | |
| <script> | |
| (function(){ | |
| async function safeJson(res){ | |
| const text = await res.text(); | |
| try { return JSON.parse(text); } | |
| catch(e){ return { error: (text || 'Server không trả JSON').slice(0,500) }; } | |
| } | |
| window.safeJson = safeJson; | |
| window.createUrlPost = function(){ | |
| let inp=document.getElementById('ai-url-input'); | |
| let url=(inp&&inp.value||'').trim(); | |
| if(!url){ alert('Dán URL trước'); return; } | |
| fetch('/api/url_wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})}) | |
| .then(safeJson).then(j=>{ | |
| if(j&&j.post){ | |
| if(!j.post.img) j.post.img='https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg'; | |
| if(!j.post.text) j.post.text=j.post.summary||j.post.title||'Không lấy được nội dung tóm tắt.'; | |
| if(typeof prependWallPost==='function') prependWallPost(j.post); | |
| alert('Đã tóm tắt URL và đăng lên tường'); | |
| if(inp) inp.value=''; | |
| } else alert((j&&j.error)||'Lỗi URL'); | |
| }).catch(e=>alert('Lỗi URL: '+e.message)); | |
| }; | |
| window.createTopicPost = function(){ | |
| let inp=document.getElementById('ai-topic-input'); | |
| let topic=(inp&&inp.value||'').trim(); | |
| if(!topic){ alert('Nhập chủ đề trước'); return; } | |
| fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic})}) | |
| .then(safeJson).then(j=>{ | |
| if(j&&j.post){ | |
| if(!j.post.img) j.post.img='https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg'; | |
| if(!j.post.text) j.post.text=j.post.summary||j.post.title||'Không lấy được nội dung.'; | |
| if(typeof prependWallPost==='function') prependWallPost(j.post); | |
| alert('Đã tạo bài và đăng lên tường'); | |
| if(inp) inp.value=''; | |
| } else alert((j&&j.error)||'Lỗi tạo bài'); | |
| }).catch(e=>alert('Lỗi tạo bài: '+e.message)); | |
| }; | |
| window.rewriteCurrentArticle = function(){ | |
| if(!window._currentArticle && typeof _currentArticle!=='undefined') window._currentArticle=_currentArticle; | |
| let ca = (typeof _currentArticle!=='undefined') ? _currentArticle : window._currentArticle; | |
| if(!ca || !ca.url){ alert('Chưa có bài viết để rewrite'); return; } | |
| let tone=document.getElementById('rewrite-tone')?.value||'nghiem-tuc'; | |
| let btn=document.querySelector('.article-actions button.primary'); | |
| if(btn){btn.textContent='Đang rewrite...';btn.disabled=true;} | |
| fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:ca.url,tone})}) | |
| .then(safeJson).then(j=>{ | |
| if(j&&j.post){ | |
| if(!j.post.img) j.post.img='https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg'; | |
| if(!j.post.text) j.post.text=j.post.summary||j.post.title||'Không lấy được nội dung.'; | |
| let box=document.getElementById('rewrite-result'); | |
| if(box) box.innerHTML='<div class="rewrite-box"><div class="rewrite-title">Đã rewrite và đăng lên Tường AI</div><div class="rewrite-text">'+(j.post.text||'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))+'</div></div>'; | |
| if(typeof prependWallPost==='function') prependWallPost(j.post); | |
| alert('Đã đăng lên Tường AI'); | |
| } else alert((j&&j.error)||'Không tạo được bài AI'); | |
| }).catch(e=>alert('Lỗi tạo bài AI: '+e.message)) | |
| .finally(()=>{if(btn){btn.textContent='🤖 AI viết lại & đăng tường';btn.disabled=false;}}); | |
| }; | |
| })(); | |
| </script> | |
| ''' | |
| 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("</body>", _FRONTEND_PATCH + "</body>") | |
| return HTMLResponse(content=html) | |
| except Exception as e: | |
| return HTMLResponse(content=f"<pre>Index error: {str(e)}</pre>", status_code=500) | |