Update app.py
Browse files
app.py
CHANGED
|
@@ -5,46 +5,19 @@ import uvicorn
|
|
| 5 |
from fastapi import FastAPI, Form, File, UploadFile
|
| 6 |
from fastapi.responses import HTMLResponse, StreamingResponse
|
| 7 |
from playwright.async_api import async_playwright
|
| 8 |
-
from contextlib import asynccontextmanager
|
| 9 |
|
|
|
|
| 10 |
UPLOAD_DIR = "temp_uploads"
|
| 11 |
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 12 |
|
| 13 |
-
|
| 14 |
-
pw_instance = None
|
| 15 |
-
|
| 16 |
-
@asynccontextmanager
|
| 17 |
-
async def lifespan(app: FastAPI):
|
| 18 |
-
global global_browser, pw_instance
|
| 19 |
-
print("⚡ در حال راهاندازی مرورگر سراسری کروم...")
|
| 20 |
-
pw_instance = await async_playwright().start()
|
| 21 |
-
global_browser = await pw_instance.chromium.launch(
|
| 22 |
-
headless=True,
|
| 23 |
-
args=[
|
| 24 |
-
"--no-sandbox",
|
| 25 |
-
"--disable-setuid-sandbox",
|
| 26 |
-
"--disable-dev-shm-usage",
|
| 27 |
-
"--disable-blink-features=AutomationControlled"
|
| 28 |
-
]
|
| 29 |
-
)
|
| 30 |
-
print("🚀 مرورگر سراسری با موفقیت لود شد.")
|
| 31 |
-
yield
|
| 32 |
-
print("🛑 در حال بستن مرورگر سراسری...")
|
| 33 |
-
if global_browser:
|
| 34 |
-
await global_browser.close()
|
| 35 |
-
if pw_instance:
|
| 36 |
-
await pw_instance.stop()
|
| 37 |
-
|
| 38 |
-
app = FastAPI(lifespan=lifespan)
|
| 39 |
-
|
| 40 |
-
# رابط کاربری با قابلیت خواندن استریم باینری پاسخها با استفاده از ReadableStream فرچ
|
| 41 |
HTML_CHAT_INTERFACE = """
|
| 42 |
<!DOCTYPE html>
|
| 43 |
<html lang="fa" dir="rtl">
|
| 44 |
<head>
|
| 45 |
<meta charset="UTF-8">
|
| 46 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 47 |
-
<title>Google AI Free Chatbot + Streaming</title>
|
| 48 |
<style>
|
| 49 |
:root {
|
| 50 |
--bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
@@ -173,25 +146,20 @@ HTML_CHAT_INTERFACE = """
|
|
| 173 |
formData.append('message', text || "این تصویر را تحلیل کن");
|
| 174 |
if (file) formData.append('image', file);
|
| 175 |
|
| 176 |
-
// ایجاد حباب خالی هوش مصنوعی برای آمادهسازی فرآیند تایپ زنده
|
| 177 |
const aiMessageDiv = document.createElement('div');
|
| 178 |
aiMessageDiv.classList.add('message', 'ai');
|
| 179 |
messagesBox.appendChild(aiMessageDiv);
|
| 180 |
|
| 181 |
try {
|
| 182 |
const response = await fetch('/api/chat', { method: 'POST', body: formData });
|
| 183 |
-
|
| 184 |
if (!response.ok) {
|
| 185 |
-
aiMessageDiv.innerText = "خطا در برقراری ارتباط با سرور گوگل.";
|
| 186 |
setLoading(false);
|
| 187 |
return;
|
| 188 |
}
|
| 189 |
|
| 190 |
-
// خواندن باینری استریم کلمه به کلمه خروجی پایتون
|
| 191 |
const reader = response.body.getReader();
|
| 192 |
const decoder = new TextDecoder("utf-8");
|
| 193 |
-
|
| 194 |
-
// پنهان کردن انیمیشن لودینگ به محض شروع اولین کلمه دریافتی
|
| 195 |
let firstChunk = true;
|
| 196 |
|
| 197 |
while (true) {
|
|
@@ -201,7 +169,6 @@ HTML_CHAT_INTERFACE = """
|
|
| 201 |
const chunk = decoder.decode(value, { stream: true });
|
| 202 |
if (chunk) {
|
| 203 |
if (firstChunk) {
|
| 204 |
-
// حذف لودینگ سه نقطهای
|
| 205 |
const indicator = document.getElementById('typingIndicator');
|
| 206 |
if (indicator) indicator.remove();
|
| 207 |
firstChunk = false;
|
|
@@ -243,20 +210,23 @@ HTML_CHAT_INTERFACE = """
|
|
| 243 |
</html>
|
| 244 |
"""
|
| 245 |
|
| 246 |
-
# الگوریتم
|
| 247 |
def clean_text(full_text: str, message: str) -> str:
|
|
|
|
|
|
|
|
|
|
| 248 |
lines = full_text.split('\n')
|
| 249 |
cleaned_lines = []
|
| 250 |
|
| 251 |
-
# واژههای
|
| 252 |
global_junk = ["تصاویر", "ویدیوها", "اخبار", "نقشهها", "خرید کردن", "کتابها", "مالی", "حالت موضوعمحور", "ورود", "همه", "ویدئوها", "ارسال", "پاسخ «حالت هوشوارهای» آماده است", "All items removed", message]
|
| 253 |
|
| 254 |
for line in lines:
|
| 255 |
line_str = line.strip()
|
| 256 |
if not line_str or line_str in global_junk:
|
| 257 |
continue
|
| 258 |
-
# به محض رسیدن به
|
| 259 |
-
if "هوشواره ممکن است اشتباه" in line_str or "در پاسخهای" in line_str or "بیشتر بدانید" in line_str:
|
| 260 |
break
|
| 261 |
cleaned_lines.append(line_str)
|
| 262 |
|
|
@@ -268,10 +238,6 @@ async def chat_interface():
|
|
| 268 |
|
| 269 |
@app.post("/api/chat")
|
| 270 |
async def chat_endpoint(message: str = Form(...), image: UploadFile = File(None)):
|
| 271 |
-
global global_browser
|
| 272 |
-
if not global_browser:
|
| 273 |
-
return StreamingResponse(iter(["خطا: وب سرور کروم فعال نیست."]), media_type="text/plain")
|
| 274 |
-
|
| 275 |
saved_image_path = None
|
| 276 |
if image:
|
| 277 |
try:
|
|
@@ -281,103 +247,125 @@ async def chat_endpoint(message: str = Form(...), image: UploadFile = File(None)
|
|
| 281 |
except Exception as e:
|
| 282 |
return StreamingResponse(iter([f"خطا در ذخیره تصویر روی داکر: {str(e)}"]), media_type="text/plain")
|
| 283 |
|
| 284 |
-
# ساخت سشن
|
| 285 |
async def response_generator():
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
|
|
|
| 297 |
|
| 298 |
-
#
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
candidates = await page.query_selector_all("textarea, input, [contenteditable='true']")
|
| 311 |
-
for el in candidates:
|
| 312 |
-
try:
|
| 313 |
-
if await el.is_visible():
|
| 314 |
-
placeholder = await el.get_attribute("placeholder") or ""
|
| 315 |
-
if any(w in placeholder for w in ["بپرسید", "ask", "پیام", "چطور", "هرچه"]):
|
| 316 |
-
input_box = el
|
| 317 |
-
break
|
| 318 |
-
except:
|
| 319 |
-
continue
|
| 320 |
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
if input_box:
|
| 325 |
-
await input_box.focus()
|
| 326 |
-
await input_box.fill(message)
|
| 327 |
-
await page.wait_for_timeout(300)
|
| 328 |
-
await input_box.press("Enter")
|
| 329 |
|
| 330 |
-
#
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
if
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
break
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
# 🚀 حلقه مانیتورینگ استریمینگ: واکشی کلمات در زمان واقعی بدون ثانیهای معطلی ثمر بخش
|
| 343 |
-
last_sent_text = ""
|
| 344 |
-
no_change_count = 0
|
| 345 |
-
|
| 346 |
-
for _ in range(40): # حداکثر ۱۶ ثانیه پایش زنده صفحه
|
| 347 |
-
await page.wait_for_timeout(400) # بررسی صفحه هر ۴۰۰ میلیثانیه برای استریم فوق سریع
|
| 348 |
-
full_text = await page.evaluate("() => document.body.innerText")
|
| 349 |
-
current_clean_text = clean_text(full_text, message)
|
| 350 |
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
# ارسال نهایی باقیمانده کاراکترها برای احتیاط
|
| 364 |
full_text = await page.evaluate("() => document.body.innerText")
|
| 365 |
current_clean_text = clean_text(full_text, message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
if len(current_clean_text) > len(last_sent_text):
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
|
| 382 |
return StreamingResponse(response_generator(), media_type="text/event-stream")
|
| 383 |
|
|
|
|
| 5 |
from fastapi import FastAPI, Form, File, UploadFile
|
| 6 |
from fastapi.responses import HTMLResponse, StreamingResponse
|
| 7 |
from playwright.async_api import async_playwright
|
|
|
|
| 8 |
|
| 9 |
+
app = FastAPI()
|
| 10 |
UPLOAD_DIR = "temp_uploads"
|
| 11 |
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
| 12 |
|
| 13 |
+
# رابط کاربری شیشهای (Glassmorphism) با فرانتاند پایدار
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
HTML_CHAT_INTERFACE = """
|
| 15 |
<!DOCTYPE html>
|
| 16 |
<html lang="fa" dir="rtl">
|
| 17 |
<head>
|
| 18 |
<meta charset="UTF-8">
|
| 19 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 20 |
+
<title>Google AI Free Chatbot + Stealth Streaming</title>
|
| 21 |
<style>
|
| 22 |
:root {
|
| 23 |
--bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
|
|
|
| 146 |
formData.append('message', text || "این تصویر را تحلیل کن");
|
| 147 |
if (file) formData.append('image', file);
|
| 148 |
|
|
|
|
| 149 |
const aiMessageDiv = document.createElement('div');
|
| 150 |
aiMessageDiv.classList.add('message', 'ai');
|
| 151 |
messagesBox.appendChild(aiMessageDiv);
|
| 152 |
|
| 153 |
try {
|
| 154 |
const response = await fetch('/api/chat', { method: 'POST', body: formData });
|
|
|
|
| 155 |
if (!response.ok) {
|
| 156 |
+
aiMessageDiv.innerText = "خطا در برقراری ارتباط با سرور یا مسدود شدن آیپی توسط گوگل.";
|
| 157 |
setLoading(false);
|
| 158 |
return;
|
| 159 |
}
|
| 160 |
|
|
|
|
| 161 |
const reader = response.body.getReader();
|
| 162 |
const decoder = new TextDecoder("utf-8");
|
|
|
|
|
|
|
| 163 |
let firstChunk = true;
|
| 164 |
|
| 165 |
while (true) {
|
|
|
|
| 169 |
const chunk = decoder.decode(value, { stream: true });
|
| 170 |
if (chunk) {
|
| 171 |
if (firstChunk) {
|
|
|
|
| 172 |
const indicator = document.getElementById('typingIndicator');
|
| 173 |
if (indicator) indicator.remove();
|
| 174 |
firstChunk = false;
|
|
|
|
| 210 |
</html>
|
| 211 |
"""
|
| 212 |
|
| 213 |
+
# الگوریتم تصفیه رادیکال: فیلتر کامل المانهای زائد هدر/فوتر و استخراج متن ۱۰۰٪ خالص
|
| 214 |
def clean_text(full_text: str, message: str) -> str:
|
| 215 |
+
if "ترافیک غیر عادی" in full_text or "Our systems have detected unusual traffic" in full_text:
|
| 216 |
+
return "🚨 سیستم امنیتی گوگل فعال شد و آیپی سرور هاگینگفیس را مسدود کرد (کپچا رخ داد).\nبرای حل دائمی این مشکل باید روی داکر از پراکسی مسکونی (Residential Proxy) استفاده کنیم."
|
| 217 |
+
|
| 218 |
lines = full_text.split('\n')
|
| 219 |
cleaned_lines = []
|
| 220 |
|
| 221 |
+
# واژههای منوهای سرچ گوگل که نباید وارد پاسخ چتباکس شوند
|
| 222 |
global_junk = ["تصاویر", "ویدیوها", "اخبار", "نقشهها", "خرید کردن", "کتابها", "مالی", "حالت موضوعمحور", "ورود", "همه", "ویدئوها", "ارسال", "پاسخ «حالت هوشوارهای» آماده است", "All items removed", message]
|
| 223 |
|
| 224 |
for line in lines:
|
| 225 |
line_str = line.strip()
|
| 226 |
if not line_str or line_str in global_junk:
|
| 227 |
continue
|
| 228 |
+
# کات کردن پاسخ به محض رسیدن به پاپآپها یا فوتر سلب مسئولیت گوگل
|
| 229 |
+
if "هوشواره ممکن است اشتباه" in line_str or "در پاسخهای" in line_str or "بیشتر بدانید" in line_str or "پاسخ «حالت" in line_str:
|
| 230 |
break
|
| 231 |
cleaned_lines.append(line_str)
|
| 232 |
|
|
|
|
| 238 |
|
| 239 |
@app.post("/api/chat")
|
| 240 |
async def chat_endpoint(message: str = Form(...), image: UploadFile = File(None)):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
saved_image_path = None
|
| 242 |
if image:
|
| 243 |
try:
|
|
|
|
| 247 |
except Exception as e:
|
| 248 |
return StreamingResponse(iter([f"خطا در ذخیره تصویر روی داکر: {str(e)}"]), media_type="text/plain")
|
| 249 |
|
| 250 |
+
# ساخت سشن داینامیک: لود مرورگر واقعی و تازه برای هر درخواست کلاینت (عین گام اول برای فرار از ردگیری سشن)
|
| 251 |
async def response_generator():
|
| 252 |
+
async with async_playwright() as p:
|
| 253 |
+
# لانچ مرورگر نو با لایههای ضد ردگیری (Anti-Automation Footprint)
|
| 254 |
+
browser = await p.chromium.launch(
|
| 255 |
+
headless=True,
|
| 256 |
+
args=[
|
| 257 |
+
"--no-sandbox",
|
| 258 |
+
"--disable-setuid-sandbox",
|
| 259 |
+
"--disable-dev-shm-usage",
|
| 260 |
+
"--disable-blink-features=AutomationControlled",
|
| 261 |
+
"--disable-web-security"
|
| 262 |
+
]
|
| 263 |
+
)
|
| 264 |
|
| 265 |
+
# فیک کردن دقیق یوزر ایجنت و هدرهای اورجینال کروم اندروید ۱۳ سامسونگ
|
| 266 |
+
context = await browser.new_context(
|
| 267 |
+
user_agent="Mozilla/5.0 (Linux; Android 13; SM-S908B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
|
| 268 |
+
viewport={"width": 412, "height": 915},
|
| 269 |
+
locale="fa-IR", timezone_id="Asia/Tehran",
|
| 270 |
+
extra_http_headers={
|
| 271 |
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
| 272 |
+
"Accept-Language": "fa-IR,fa;q=0.9,en-US;q=0.8,en;q=0.7",
|
| 273 |
+
"Upgrade-Insecure-Requests": "1"
|
| 274 |
+
}
|
| 275 |
+
)
|
| 276 |
+
page = await context.new_page()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
+
try:
|
| 279 |
+
url = "https://www.google.com/search?q=سلام&udm=50"
|
| 280 |
+
await page.goto(url, wait_until="domcontentloaded", timeout=30000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
+
# بررسی زنده لود تصویر روی لایه فایل آپلودر گوگل لنز
|
| 283 |
+
if saved_image_path and os.path.exists(saved_image_path):
|
| 284 |
+
try:
|
| 285 |
+
file_input = await page.query_selector("input[type='file']")
|
| 286 |
+
if file_input:
|
| 287 |
+
await file_input.set_input_files(saved_image_path)
|
| 288 |
+
await page.wait_for_timeout(1500)
|
| 289 |
+
except Exception as e:
|
| 290 |
+
print(f"Bypassed image file setup: {e}")
|
| 291 |
+
|
| 292 |
+
# واکشی المنت کادر گفتگو
|
| 293 |
+
input_box = None
|
| 294 |
+
candidates = await page.query_selector_all("textarea, input, [contenteditable='true']")
|
| 295 |
+
for el in candidates:
|
| 296 |
+
try:
|
| 297 |
+
if await el.is_visible():
|
| 298 |
+
placeholder = await el.get_attribute("placeholder") or ""
|
| 299 |
+
if any(w in placeholder for w in ["بپرسید", "ask", "پیام", "چطور", "هرچه"]):
|
| 300 |
+
input_box = el
|
| 301 |
break
|
| 302 |
+
except:
|
| 303 |
+
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
+
if not input_box:
|
| 306 |
+
input_box = await page.query_selector("textarea") or await page.query_selector("input[type='text']")
|
| 307 |
+
|
| 308 |
+
if input_box:
|
| 309 |
+
await input_box.focus()
|
| 310 |
+
await input_box.fill(message)
|
| 311 |
+
await page.wait_for_timeout(300)
|
| 312 |
+
await input_box.press("Enter")
|
| 313 |
+
|
| 314 |
+
# سابمیت مکمل فرم برای کلاینتهای دیتاسنتری
|
| 315 |
+
try:
|
| 316 |
+
send_buttons = await page.query_selector_all("button, [role='button']")
|
| 317 |
+
for btn in send_buttons:
|
| 318 |
+
if await btn.is_visible():
|
| 319 |
+
aria = (await btn.get_attribute("aria-label") or "").lower()
|
| 320 |
+
if any(w in aria for w in ["send", "ارسال", "arrow", "up"]):
|
| 321 |
+
await btn.click(timeout=1000)
|
| 322 |
+
break
|
| 323 |
+
except:
|
| 324 |
+
pass
|
| 325 |
+
|
| 326 |
+
# ⚡ حلقه استریم فوق سریع: واکشی خطوط بدون تاخیرهای استاتیک با چکاپ هر ۳۰۰ میلیثانیه
|
| 327 |
+
last_sent_text = ""
|
| 328 |
+
no_change_count = 0
|
| 329 |
|
| 330 |
+
for _ in range(45):
|
| 331 |
+
await page.wait_for_timeout(300)
|
|
|
|
| 332 |
full_text = await page.evaluate("() => document.body.innerText")
|
| 333 |
current_clean_text = clean_text(full_text, message)
|
| 334 |
+
|
| 335 |
+
# مدیریت خروجی کپچا
|
| 336 |
+
if "سیستم امنیتی گوگل فعال شد" in current_clean_text:
|
| 337 |
+
yield current_clean_text
|
| 338 |
+
break
|
| 339 |
+
|
| 340 |
+
# استریم کلمات جدید رندر شده روی مرورگر داکر
|
| 341 |
if len(current_clean_text) > len(last_sent_text):
|
| 342 |
+
new_chunk = current_clean_text[len(last_sent_text):]
|
| 343 |
+
yield new_chunk
|
| 344 |
+
last_sent_text = current_clean_text
|
| 345 |
+
no_change_count = 0
|
| 346 |
+
else:
|
| 347 |
+
if len(current_clean_text) > 0:
|
| 348 |
+
no_change_count += 1
|
| 349 |
+
|
| 350 |
+
# خروج سریع به محض پایان یافتن پردازش گوگل
|
| 351 |
+
if "هوشواره ممکن است اشتباه" in full_text or "بیشتر بدانید" in full_text or "پاسخهای مربوط به افراد" in full_text:
|
| 352 |
+
full_text = await page.evaluate("() => document.body.innerText")
|
| 353 |
+
current_clean_text = clean_text(full_text, message)
|
| 354 |
+
if len(current_clean_text) > len(last_sent_text):
|
| 355 |
+
yield current_clean_text[len(last_sent_text):]
|
| 356 |
+
break
|
| 357 |
+
|
| 358 |
+
if no_change_count > 8 and len(last_sent_text) > 0:
|
| 359 |
+
break
|
| 360 |
+
|
| 361 |
+
except Exception as e:
|
| 362 |
+
yield f"خطای پردازش هوش مصنوعی: {str(e)}"
|
| 363 |
+
finally:
|
| 364 |
+
await context.close()
|
| 365 |
+
await browser.close()
|
| 366 |
+
if saved_image_path and os.path.exists(saved_image_path):
|
| 367 |
+
try: os.remove(saved_image_path)
|
| 368 |
+
except: pass
|
| 369 |
|
| 370 |
return StreamingResponse(response_generator(), media_type="text/event-stream")
|
| 371 |
|