Spaces:
Running
Running
Add frontend safeJson injection for robust AI post creation
Browse files- patch_runtime.py +98 -32
patch_runtime.py
CHANGED
|
@@ -1,17 +1,16 @@
|
|
| 1 |
"""Runtime patch layer for VNEWS.
|
| 2 |
-
|
| 3 |
Keeps the current large app intact, but replaces fragile AI wall endpoints with
|
| 4 |
-
stable JSON endpoints
|
| 5 |
-
throws `Unexpected token < in JSON`.
|
| 6 |
"""
|
| 7 |
import hashlib
|
| 8 |
import time
|
|
|
|
| 9 |
from urllib.parse import quote
|
| 10 |
|
| 11 |
import requests
|
| 12 |
from bs4 import BeautifulSoup
|
| 13 |
from fastapi import Request
|
| 14 |
-
from fastapi.responses import JSONResponse
|
| 15 |
|
| 16 |
import main as _main
|
| 17 |
|
|
@@ -28,32 +27,24 @@ def _safe_text(v):
|
|
| 28 |
|
| 29 |
|
| 30 |
def _ensure_article(url: str):
|
| 31 |
-
"""Return article-like dict with title, summary, og_image, body.
|
| 32 |
-
Never raises.
|
| 33 |
-
"""
|
| 34 |
data = None
|
| 35 |
try:
|
| 36 |
if hasattr(_main, "_article_by_url"):
|
| 37 |
data = _main._article_by_url(url)
|
| 38 |
except Exception:
|
| 39 |
data = None
|
| 40 |
-
|
| 41 |
if not data:
|
| 42 |
try:
|
| 43 |
data = _main._scrape_generic_article(url) if hasattr(_main, "_scrape_generic_article") else None
|
| 44 |
except Exception:
|
| 45 |
data = None
|
| 46 |
-
|
| 47 |
if not data:
|
| 48 |
data = {"title": "", "summary": "", "og_image": "", "body": [], "url": url, "source": "generic"}
|
| 49 |
-
|
| 50 |
title = _safe_text(data.get("title"))
|
| 51 |
summary = _safe_text(data.get("summary"))
|
| 52 |
img = _safe_text(data.get("og_image"))
|
| 53 |
body = data.get("body") or []
|
| 54 |
-
|
| 55 |
-
# Last-resort OG scrape for title/summary/image.
|
| 56 |
-
if not title or not summary or not img:
|
| 57 |
try:
|
| 58 |
r = requests.get(url, headers=getattr(_main, "HEADERS", {}), timeout=15)
|
| 59 |
r.encoding = "utf-8"
|
|
@@ -73,22 +64,20 @@ def _ensure_article(url: str):
|
|
| 73 |
t = p.get_text(" ", strip=True)
|
| 74 |
if len(t) > 40:
|
| 75 |
ps.append({"type": "p", "text": t})
|
| 76 |
-
if len(ps) >=
|
| 77 |
break
|
| 78 |
body = ps
|
| 79 |
except Exception:
|
| 80 |
pass
|
| 81 |
-
|
| 82 |
if not summary and body:
|
| 83 |
first = next((b.get("text", "") for b in body if b.get("type") == "p" and b.get("text")), "")
|
| 84 |
-
summary = first[:
|
| 85 |
if not title:
|
| 86 |
title = url
|
| 87 |
if not img:
|
| 88 |
img = DEFAULT_IMG
|
| 89 |
if not body and summary:
|
| 90 |
body = [{"type": "p", "text": summary}]
|
| 91 |
-
|
| 92 |
data.update({"title": title, "summary": summary, "og_image": img, "body": body, "url": url})
|
| 93 |
return data
|
| 94 |
|
|
@@ -106,11 +95,20 @@ def _rewrite(data, tone="tu-nhien"):
|
|
| 106 |
ps = [b.get("text", "") for b in data.get("body", []) if b.get("type") == "p" and b.get("text")]
|
| 107 |
lead = summary or (ps[0] if ps else "")
|
| 108 |
points = "\n".join(["• " + p[:220] + ("..." if len(p) > 220 else "") for p in ps[:5]])
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
|
| 112 |
def _save_post(post):
|
| 113 |
-
posts = []
|
| 114 |
try:
|
| 115 |
posts = _main._load_wall() if hasattr(_main, "_load_wall") else []
|
| 116 |
except Exception:
|
|
@@ -124,7 +122,7 @@ def _save_post(post):
|
|
| 124 |
return post
|
| 125 |
|
| 126 |
|
| 127 |
-
_remove_routes(["/api/url_wall", "/api/topic_post", "/api/rewrite_share"])
|
| 128 |
|
| 129 |
|
| 130 |
@app.post("/api/url_wall")
|
|
@@ -158,7 +156,6 @@ async def patched_url_wall(request: Request):
|
|
| 158 |
|
| 159 |
@app.post("/api/rewrite_share")
|
| 160 |
async def patched_rewrite_share(request: Request):
|
| 161 |
-
# Same robust behavior as url_wall, but accepts url/tone from article view.
|
| 162 |
return await patched_url_wall(request)
|
| 163 |
|
| 164 |
|
|
@@ -183,21 +180,14 @@ async def patched_topic_post(request: Request):
|
|
| 183 |
context = ""
|
| 184 |
if not context:
|
| 185 |
context = f"Chủ đề: {topic}"
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
"summary": context[:420],
|
| 189 |
-
"og_image": getattr(_main, "_image_for_topic", lambda x: DEFAULT_IMG)(topic),
|
| 190 |
-
"body": [{"type": "p", "text": context}],
|
| 191 |
-
"source": "topic",
|
| 192 |
-
"url": "",
|
| 193 |
-
}
|
| 194 |
-
text = _rewrite(prompt_data, tone=tone)
|
| 195 |
post = {
|
| 196 |
"id": hashlib.md5((topic + str(time.time())).encode()).hexdigest()[:12],
|
| 197 |
"url": "",
|
| 198 |
"title": topic,
|
| 199 |
-
"summary":
|
| 200 |
-
"img":
|
| 201 |
"text": text or context,
|
| 202 |
"source": "topic",
|
| 203 |
"ts": int(time.time()),
|
|
@@ -206,3 +196,79 @@ async def patched_topic_post(request: Request):
|
|
| 206 |
return JSONResponse({"post": post})
|
| 207 |
except Exception as e:
|
| 208 |
return JSONResponse({"error": "Không tạo được bài theo chủ đề", "detail": str(e)[:300]}, status_code=500)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Runtime patch layer for VNEWS.
|
|
|
|
| 2 |
Keeps the current large app intact, but replaces fragile AI wall endpoints with
|
| 3 |
+
stable JSON endpoints and injects frontend safeJson wrappers.
|
|
|
|
| 4 |
"""
|
| 5 |
import hashlib
|
| 6 |
import time
|
| 7 |
+
import os
|
| 8 |
from urllib.parse import quote
|
| 9 |
|
| 10 |
import requests
|
| 11 |
from bs4 import BeautifulSoup
|
| 12 |
from fastapi import Request
|
| 13 |
+
from fastapi.responses import JSONResponse, HTMLResponse
|
| 14 |
|
| 15 |
import main as _main
|
| 16 |
|
|
|
|
| 27 |
|
| 28 |
|
| 29 |
def _ensure_article(url: str):
|
|
|
|
|
|
|
|
|
|
| 30 |
data = None
|
| 31 |
try:
|
| 32 |
if hasattr(_main, "_article_by_url"):
|
| 33 |
data = _main._article_by_url(url)
|
| 34 |
except Exception:
|
| 35 |
data = None
|
|
|
|
| 36 |
if not data:
|
| 37 |
try:
|
| 38 |
data = _main._scrape_generic_article(url) if hasattr(_main, "_scrape_generic_article") else None
|
| 39 |
except Exception:
|
| 40 |
data = None
|
|
|
|
| 41 |
if not data:
|
| 42 |
data = {"title": "", "summary": "", "og_image": "", "body": [], "url": url, "source": "generic"}
|
|
|
|
| 43 |
title = _safe_text(data.get("title"))
|
| 44 |
summary = _safe_text(data.get("summary"))
|
| 45 |
img = _safe_text(data.get("og_image"))
|
| 46 |
body = data.get("body") or []
|
| 47 |
+
if not title or not summary or not img or not body:
|
|
|
|
|
|
|
| 48 |
try:
|
| 49 |
r = requests.get(url, headers=getattr(_main, "HEADERS", {}), timeout=15)
|
| 50 |
r.encoding = "utf-8"
|
|
|
|
| 64 |
t = p.get_text(" ", strip=True)
|
| 65 |
if len(t) > 40:
|
| 66 |
ps.append({"type": "p", "text": t})
|
| 67 |
+
if len(ps) >= 30:
|
| 68 |
break
|
| 69 |
body = ps
|
| 70 |
except Exception:
|
| 71 |
pass
|
|
|
|
| 72 |
if not summary and body:
|
| 73 |
first = next((b.get("text", "") for b in body if b.get("type") == "p" and b.get("text")), "")
|
| 74 |
+
summary = first[:360]
|
| 75 |
if not title:
|
| 76 |
title = url
|
| 77 |
if not img:
|
| 78 |
img = DEFAULT_IMG
|
| 79 |
if not body and summary:
|
| 80 |
body = [{"type": "p", "text": summary}]
|
|
|
|
| 81 |
data.update({"title": title, "summary": summary, "og_image": img, "body": body, "url": url})
|
| 82 |
return data
|
| 83 |
|
|
|
|
| 95 |
ps = [b.get("text", "") for b in data.get("body", []) if b.get("type") == "p" and b.get("text")]
|
| 96 |
lead = summary or (ps[0] if ps else "")
|
| 97 |
points = "\n".join(["• " + p[:220] + ("..." if len(p) > 220 else "") for p in ps[:5]])
|
| 98 |
+
body = "\n\n".join(ps[:10])
|
| 99 |
+
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()
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def _topic_image(topic):
|
| 103 |
+
try:
|
| 104 |
+
if hasattr(_main, "_image_for_topic"):
|
| 105 |
+
return _main._image_for_topic(topic)
|
| 106 |
+
except Exception:
|
| 107 |
+
pass
|
| 108 |
+
return "https://image.pollinations.ai/prompt/" + quote("editorial illustration Vietnamese news " + topic, safe="") + "?width=1024&height=576&nologo=true"
|
| 109 |
|
| 110 |
|
| 111 |
def _save_post(post):
|
|
|
|
| 112 |
try:
|
| 113 |
posts = _main._load_wall() if hasattr(_main, "_load_wall") else []
|
| 114 |
except Exception:
|
|
|
|
| 122 |
return post
|
| 123 |
|
| 124 |
|
| 125 |
+
_remove_routes(["/api/url_wall", "/api/topic_post", "/api/rewrite_share", "/"])
|
| 126 |
|
| 127 |
|
| 128 |
@app.post("/api/url_wall")
|
|
|
|
| 156 |
|
| 157 |
@app.post("/api/rewrite_share")
|
| 158 |
async def patched_rewrite_share(request: Request):
|
|
|
|
| 159 |
return await patched_url_wall(request)
|
| 160 |
|
| 161 |
|
|
|
|
| 180 |
context = ""
|
| 181 |
if not context:
|
| 182 |
context = f"Chủ đề: {topic}"
|
| 183 |
+
data = {"title": topic, "summary": context[:420], "og_image": _topic_image(topic), "body": [{"type": "p", "text": context}], "source": "topic", "url": ""}
|
| 184 |
+
text = _rewrite(data, tone=tone)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
post = {
|
| 186 |
"id": hashlib.md5((topic + str(time.time())).encode()).hexdigest()[:12],
|
| 187 |
"url": "",
|
| 188 |
"title": topic,
|
| 189 |
+
"summary": data["summary"],
|
| 190 |
+
"img": data["og_image"] or DEFAULT_IMG,
|
| 191 |
"text": text or context,
|
| 192 |
"source": "topic",
|
| 193 |
"ts": int(time.time()),
|
|
|
|
| 196 |
return JSONResponse({"post": post})
|
| 197 |
except Exception as e:
|
| 198 |
return JSONResponse({"error": "Không tạo được bài theo chủ đề", "detail": str(e)[:300]}, status_code=500)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
_FRONTEND_PATCH = r'''
|
| 202 |
+
<script>
|
| 203 |
+
(function(){
|
| 204 |
+
async function safeJson(res){
|
| 205 |
+
const text = await res.text();
|
| 206 |
+
try { return JSON.parse(text); }
|
| 207 |
+
catch(e){ return { error: (text || 'Server không trả JSON').slice(0,500) }; }
|
| 208 |
+
}
|
| 209 |
+
window.safeJson = safeJson;
|
| 210 |
+
window.createUrlPost = function(){
|
| 211 |
+
let inp=document.getElementById('ai-url-input');
|
| 212 |
+
let url=(inp&&inp.value||'').trim();
|
| 213 |
+
if(!url){ alert('Dán URL trước'); return; }
|
| 214 |
+
fetch('/api/url_wall',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url})})
|
| 215 |
+
.then(safeJson).then(j=>{
|
| 216 |
+
if(j&&j.post){
|
| 217 |
+
if(!j.post.img) j.post.img='https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg';
|
| 218 |
+
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.';
|
| 219 |
+
if(typeof prependWallPost==='function') prependWallPost(j.post);
|
| 220 |
+
alert('Đã tóm tắt URL và đăng lên tường');
|
| 221 |
+
if(inp) inp.value='';
|
| 222 |
+
} else alert((j&&j.error)||'Lỗi URL');
|
| 223 |
+
}).catch(e=>alert('Lỗi URL: '+e.message));
|
| 224 |
+
};
|
| 225 |
+
window.createTopicPost = function(){
|
| 226 |
+
let inp=document.getElementById('ai-topic-input');
|
| 227 |
+
let topic=(inp&&inp.value||'').trim();
|
| 228 |
+
if(!topic){ alert('Nhập chủ đề trước'); return; }
|
| 229 |
+
fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic})})
|
| 230 |
+
.then(safeJson).then(j=>{
|
| 231 |
+
if(j&&j.post){
|
| 232 |
+
if(!j.post.img) j.post.img='https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg';
|
| 233 |
+
if(!j.post.text) j.post.text=j.post.summary||j.post.title||'Không lấy được nội dung.';
|
| 234 |
+
if(typeof prependWallPost==='function') prependWallPost(j.post);
|
| 235 |
+
alert('Đã tạo bài và đăng lên tường');
|
| 236 |
+
if(inp) inp.value='';
|
| 237 |
+
} else alert((j&&j.error)||'Lỗi tạo bài');
|
| 238 |
+
}).catch(e=>alert('Lỗi tạo bài: '+e.message));
|
| 239 |
+
};
|
| 240 |
+
window.rewriteCurrentArticle = function(){
|
| 241 |
+
if(!window._currentArticle && typeof _currentArticle!=='undefined') window._currentArticle=_currentArticle;
|
| 242 |
+
let ca = (typeof _currentArticle!=='undefined') ? _currentArticle : window._currentArticle;
|
| 243 |
+
if(!ca || !ca.url){ alert('Chưa có bài viết để rewrite'); return; }
|
| 244 |
+
let tone=document.getElementById('rewrite-tone')?.value||'nghiem-tuc';
|
| 245 |
+
let btn=document.querySelector('.article-actions button.primary');
|
| 246 |
+
if(btn){btn.textContent='Đang rewrite...';btn.disabled=true;}
|
| 247 |
+
fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:ca.url,tone})})
|
| 248 |
+
.then(safeJson).then(j=>{
|
| 249 |
+
if(j&&j.post){
|
| 250 |
+
if(!j.post.img) j.post.img='https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg';
|
| 251 |
+
if(!j.post.text) j.post.text=j.post.summary||j.post.title||'Không lấy được nội dung.';
|
| 252 |
+
let box=document.getElementById('rewrite-result');
|
| 253 |
+
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>';
|
| 254 |
+
if(typeof prependWallPost==='function') prependWallPost(j.post);
|
| 255 |
+
alert('Đã đăng lên Tường AI');
|
| 256 |
+
} else alert((j&&j.error)||'Không tạo được bài AI');
|
| 257 |
+
}).catch(e=>alert('Lỗi tạo bài AI: '+e.message))
|
| 258 |
+
.finally(()=>{if(btn){btn.textContent='🤖 AI viết lại & đăng tường';btn.disabled=false;}});
|
| 259 |
+
};
|
| 260 |
+
})();
|
| 261 |
+
</script>
|
| 262 |
+
'''
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
@app.get("/")
|
| 266 |
+
async def patched_index():
|
| 267 |
+
try:
|
| 268 |
+
with open("/app/static/index.html", "r", encoding="utf-8") as f:
|
| 269 |
+
html = f.read()
|
| 270 |
+
if "window.safeJson" not in html:
|
| 271 |
+
html = html.replace("</body>", _FRONTEND_PATCH + "</body>")
|
| 272 |
+
return HTMLResponse(content=html)
|
| 273 |
+
except Exception as e:
|
| 274 |
+
return HTMLResponse(content=f"<pre>Index error: {str(e)}</pre>", status_code=500)
|