bep40 commited on
Commit
84dd1f0
·
verified ·
1 Parent(s): da4abcc

Add frontend safeJson injection for robust AI post creation

Browse files
Files changed (1) hide show
  1. 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 so the frontend never receives HTML/traceback and never
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) >= 20:
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[:320]
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
- return (f"Bản tin AI viết lại: {title}\n\n{lead}\n\nĐiểm chính:\n{points}").strip()
 
 
 
 
 
 
 
 
 
 
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
- prompt_data = {
187
- "title": topic,
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": prompt_data["summary"],
200
- "img": prompt_data["og_image"] or DEFAULT_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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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)