bep40 commited on
Commit
8036cd3
·
verified ·
1 Parent(s): 5a381fd

Add guarded hotfix patch for safe app

Browse files
Files changed (1) hide show
  1. safe_patch.py +223 -0
safe_patch.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Guarded hotfixes for VNEWS Safe App.
2
+
3
+ Never raise during import/apply. All patches are best-effort.
4
+ """
5
+ import re
6
+ import time
7
+ import requests
8
+ from urllib.parse import quote
9
+
10
+
11
+ def apply(app):
12
+ try:
13
+ from fastapi import Request, Query
14
+ from fastapi.responses import JSONResponse, HTMLResponse
15
+ import ai_ext as base
16
+ except Exception:
17
+ return
18
+
19
+ shorts_cache = {"t": 0, "d": []}
20
+ channels = ["baodantri7941", "baosuckhoedoisongboyte"]
21
+
22
+ def clean(s):
23
+ import html as html_lib
24
+ return re.sub(r"\s+", " ", html_lib.unescape(s or "")).strip()
25
+
26
+ bad_topic_lines = [
27
+ "Chỉ xuất bản bài viết cuối cùng",
28
+ "không nhắc lại yêu cầu",
29
+ "không liệt kê chỉ dẫn",
30
+ "Không sao chép nguyên văn",
31
+ "hãy tổng hợp và diễn đạt lại",
32
+ "Bài có tiêu đề",
33
+ "sapo",
34
+ "các đoạn phân tích",
35
+ "Nhiệm vụ: viết một bài báo tiếng Việt hoàn chỉnh",
36
+ ]
37
+
38
+ def clean_topic_output(text):
39
+ if not text:
40
+ return text
41
+ lines = []
42
+ for ln in str(text).splitlines():
43
+ low = ln.strip().lower()
44
+ if any(b.lower() in low for b in bad_topic_lines):
45
+ continue
46
+ if low.startswith(("yêu cầu:", "nhiệm vụ:", "bắt buộc:", "đầu ra:", "chỉ xuất bản")):
47
+ continue
48
+ lines.append(ln)
49
+ return re.sub(r"\n{3,}", "\n\n", "\n".join(lines).strip())
50
+
51
+ def fallback_shorts():
52
+ hard = [
53
+ ("Lu_iCQ5YwNM", "Công an lập hồ sơ xử lý người phụ nữ chửi bới, tát tài xế ô tô | Dân trí", "baodantri7941"),
54
+ ("CwWvijF8BOA", "Chú rể bật khóc nhận món quà bí mật người cha quá cố gửi 26 năm trước | Dân trí", "baodantri7941"),
55
+ ("tvPewsc2ph4", "Tính năng ẩn trên iPhone giúp giảm mỏi mắt | Dân trí", "baodantri7941"),
56
+ ("7Pd6vZ2Lz1M", "Hành động ấm lòng trong tìm kiếm học sinh tử vong ở sông Lô | SKĐS", "baosuckhoedoisongboyte"),
57
+ ("SlHLt_ZyPiE", "Xử phạt người đàn ông xóa số điện thoại cứu hộ trên cao tốc Bắc - Nam | SKĐS", "baosuckhoedoisongboyte"),
58
+ ("IUOprcJyYr4", "Phụ nữ táo bón có phải do lười ăn rau? | SKĐS", "baosuckhoedoisongboyte"),
59
+ ]
60
+ return [
61
+ {
62
+ "id": vid,
63
+ "title": title,
64
+ "channel": ch,
65
+ "link": "https://www.youtube.com/watch?v=" + vid,
66
+ "img": "https://i.ytimg.com/vi/" + vid + "/hqdefault.jpg",
67
+ "source": "yt",
68
+ }
69
+ for vid, title, ch in hard
70
+ ]
71
+
72
+ def yt_html(handle, count=18):
73
+ try:
74
+ headers = getattr(base, "HEADERS", {"User-Agent": "Mozilla/5.0"})
75
+ html = requests.get(f"https://www.youtube.com/@{handle}/shorts", headers=headers, timeout=6).text
76
+ ids, out = [], []
77
+ for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"', html):
78
+ vid = m.group(1)
79
+ if vid in ids:
80
+ continue
81
+ ids.append(vid)
82
+ snip = html[max(0, m.start() - 900):m.start() + 1600]
83
+ mt = re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"', snip) or re.search(r'"accessibilityText":"([^"]+)"', snip)
84
+ title = clean(mt.group(1).replace('\\n', ' ')) if mt else "YouTube Short"
85
+ out.append({
86
+ "id": vid,
87
+ "title": title,
88
+ "channel": handle,
89
+ "link": "https://www.youtube.com/watch?v=" + vid,
90
+ "img": "https://i.ytimg.com/vi/" + vid + "/hqdefault.jpg",
91
+ "source": "yt",
92
+ })
93
+ if len(out) >= count:
94
+ break
95
+ return out
96
+ except Exception:
97
+ return []
98
+
99
+ def fresh_shorts():
100
+ seen, out = set(), []
101
+ for ch in channels:
102
+ for v in yt_html(ch, 18):
103
+ if v["id"] not in seen:
104
+ seen.add(v["id"])
105
+ out.append(v)
106
+ for v in fallback_shorts():
107
+ if v["id"] not in seen:
108
+ seen.add(v["id"])
109
+ out.append(v)
110
+ return out[:40]
111
+
112
+ def remove_routes(paths):
113
+ app.router.routes = [r for r in app.router.routes if getattr(r, "path", None) not in set(paths)]
114
+
115
+ remove_routes(["/api/shorts", "/api/topic_post", "/api/rewrite_topic"])
116
+
117
+ @app.get("/api/shorts")
118
+ def api_shorts(refresh: int = Query(default=0)):
119
+ now = time.time()
120
+ if not refresh and shorts_cache["d"] and now - shorts_cache["t"] < 600:
121
+ return JSONResponse(shorts_cache["d"])
122
+ data = fresh_shorts()
123
+ shorts_cache.update({"t": now, "d": data})
124
+ return JSONResponse(data)
125
+
126
+ @app.post("/api/topic_post")
127
+ async def topic_post(request: Request):
128
+ body = await request.json()
129
+ topic = clean(body.get("topic", ""))
130
+ if not topic:
131
+ return JSONResponse({"error": "missing topic"}, status_code=400)
132
+ try:
133
+ img = base.pollinations_image_url(topic)
134
+ except Exception:
135
+ img = "https://image.pollinations.ai/prompt/" + quote("Vietnamese news editorial illustration " + topic) + "?width=1024&height=576&nologo=true"
136
+ prompt = f"""Viết một bài báo tiếng Việt hoàn chỉnh, chất lượng cao về chủ đề: {topic}
137
+
138
+ Hãy sử dụng kiến thức tổng hợp của bạn để tạo nội dung có giá trị thực sự cho độc giả.
139
+
140
+ Phong cách:
141
+ - Như một bài báo/tạp chí đã hoàn thiện, không phải dàn ý.
142
+ - Không nhắc lại yêu cầu của người dùng.
143
+ - Không liệt kê chỉ dẫn viết bài.
144
+ - Không sao chép nguyên văn nguồn nào.
145
+
146
+ Nội dung cần có:
147
+ - Tiêu đề cụ thể.
148
+ - Sapo ngắn, hấp dẫn.
149
+ - Các đoạn phân tích bối cảnh, nguyên nhân, tác động, ví dụ và nhận định.
150
+ - Nếu chủ đề là thể thao như World Cup, hãy nói về ý nghĩa giải đấu, lịch sử, tác động tới bóng đá, đội tuyển/cầu thủ, kinh tế - truyền thông và cảm xúc người hâm mộ.
151
+ - Nếu thiếu dữ kiện thời sự mới, hãy diễn đạt thận trọng và tập trung vào kiến thức nền.
152
+ - Cuối bài có mục Nguồn tham khảo ngắn: Qwen2.5-VL / kiến thức tổng hợp.
153
+ """
154
+ try:
155
+ text = await base.qwen_generate(prompt, image_url=img, max_tokens=1800)
156
+ except Exception:
157
+ text = ""
158
+ if not text:
159
+ text = f"{topic}\n\n{topic} là một chủ đề có nhiều lớp ý nghĩa, từ bối cảnh lịch sử đến tác động xã hội, truyền thông và đời sống người hâm mộ. Bài viết này tổng hợp các kiến thức nền và những điểm đáng chú ý nhất để người đọc hiểu rõ hơn về chủ đề.\n\nNguồn tham khảo: Qwen2.5-VL / kiến thức tổng hợp."
160
+ text = clean_topic_output(text)
161
+ post = base.make_post(topic, text, img, "", "topic_qwen", sources=[{"title": "Qwen2.5-VL / kiến thức tổng hợp", "url": "", "via": "Qwen2.5-VL"}])
162
+ post["images"] = [img]
163
+ posts = base._load_ai_wall()
164
+ posts.insert(0, post)
165
+ base._save_ai_wall(posts)
166
+ return JSONResponse({"post": post})
167
+
168
+ @app.post("/api/rewrite_topic")
169
+ async def rewrite_topic(request: Request):
170
+ body = await request.json()
171
+ post_id = str(body.get("id", "")).strip()
172
+ posts = base._load_ai_wall()
173
+ p = next((x for x in posts if str(x.get("id")) == post_id), None)
174
+ if not p:
175
+ return JSONResponse({"error": "post not found"}, status_code=404)
176
+ prompt = f"""Viết lại bài sau theo phong cách báo chí phân tích, mạch lạc, hấp dẫn. Chỉ xuất bản bản viết lại cuối cùng.
177
+
178
+ Tiêu đề: {p.get('title','')}
179
+ Nội dung:
180
+ {(p.get('text') or '')[:12000]}
181
+ """
182
+ try:
183
+ txt = await base.qwen_generate(prompt, image_url=p.get("img") or None, max_tokens=1600)
184
+ except Exception:
185
+ txt = ""
186
+ if not txt:
187
+ txt = p.get("text", "")
188
+ txt = clean_topic_output(txt)
189
+ new = dict(p)
190
+ new["id"] = str(int(time.time() * 1000))
191
+ new["text"] = txt
192
+ new["kind"] = "topic_rewrite"
193
+ posts.insert(0, new)
194
+ base._save_ai_wall(posts)
195
+ return JSONResponse({"post": new})
196
+
197
+ # Patch homepage without replacing the whole index route.
198
+ @app.middleware("http")
199
+ async def inject_hotfix(request, call_next):
200
+ response = await call_next(request)
201
+ try:
202
+ if request.url.path == "/" and response.headers.get("content-type", "").startswith("text/html"):
203
+ body = b""
204
+ async for chunk in response.body_iterator:
205
+ body += chunk
206
+ html = body.decode("utf-8", "ignore")
207
+ inject = r'''
208
+ <script>
209
+ (function(){
210
+ function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));}
211
+ function cleanTopicText(){document.querySelectorAll('.wall-text,.article-p,.rewrite-text').forEach(el=>{let t=el.textContent||'';let bad=['Chỉ xuất bản bài viết cuối cùng','Không sao chép nguyên văn','Bài có tiêu đề','Nhiệm vụ: viết một bài báo tiếng Việt hoàn chỉnh'];if(bad.some(b=>t.includes(b))){el.textContent=t.split('\n').filter(l=>!bad.some(b=>l.includes(b))).join('\n')}})}
212
+ async function ensureFastShorts(){let home=document.getElementById('view-home');if(!home||document.getElementById('shorts-hotfix-fast'))return;let sh=await fetch('/api/shorts?refresh=1').then(r=>r.json()).catch(()=>[]);if(!sh.length)return;let wrap=document.createElement('div');wrap.id='shorts-hotfix-fast';wrap.className='slider-wrap';let h='<div class="slider-header"><span class="slider-label">📱 Shorts Dân trí & SKĐS</span><span class="slider-note">mới nhất / tải nhanh</span></div><div class="slider-track">';sh.slice(0,24).forEach((a,i)=>{h+=`<div class="slider-item shorts-item" onclick="openShortsHotfix(${i})"><div class="slider-thumb shorts-thumb">${a.img?`<img src="${a.img}">`:''}<div class="card-play">▶</div></div><div class="slider-title">${esc(a.title)}</div></div>`});h+='</div>';wrap.innerHTML=h;(document.querySelector('.ai-compose')||home.firstChild).after(wrap);window.__hotfixShorts=sh;}
213
+ window.openShortsHotfix=function(start){let arts=window.__hotfixShorts||[];if(!arts.length)return alert('Chưa tải được Shorts');let ordered=start>0?arts.slice(start).concat(arts.slice(0,start)):arts;showView('view-tiktok');let h='<button class="back-btn" onclick="switchCat(\'home\')">← Shorts</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';ordered.forEach((v,i)=>{let src='https://www.youtube.com/embed/'+v.id+'?autoplay=1&rel=0&playsinline=1';h+=`<div class="tiktok-slide"><iframe src="${src}" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture"></iframe><div class="tiktok-bottom"><span class="badge badge-fpt">YT</span><p class="tiktok-title">${esc(v.title)}</p></div><span class="tiktok-counter">${i+1}/${ordered.length}</span></div>`});h+='</div></div>';document.getElementById('view-tiktok').innerHTML=h;}
214
+ setInterval(()=>{cleanTopicText();if(document.getElementById('view-home')?.classList.contains('active'))ensureFastShorts();},1200);setTimeout(ensureFastShorts,800);
215
+ })();
216
+ </script>
217
+ '''
218
+ if "</body>" in html:
219
+ html = html.replace("</body>", inject + "</body>")
220
+ return HTMLResponse(html)
221
+ except Exception:
222
+ pass
223
+ return response