bep40 commited on
Commit
888eb4e
·
verified ·
1 Parent(s): eb1a9f8

Emergency fix: ZGR logs page now displays ALL webhook events including sender_id, text, image_url, and metadata. Previous version had no logs because sender_id filter was too strict.

Browse files
Files changed (1) hide show
  1. app.py +122 -113
app.py CHANGED
@@ -1,4 +1,4 @@
1
- import os, sys, json, secrets, logging, asyncio, re, time, threading, base64
2
  from html import escape
3
 
4
  try:
@@ -17,30 +17,13 @@ except ImportError:
17
  setattr(_stub, _n, lambda *a, **k: b"" if "lin" in _n or "sample" in _n else 0)
18
  sys.modules["audioop"] = _stub
19
 
20
- try:
21
- from huggingface_hub import HfFolder
22
- except (ImportError, AttributeError):
23
- import huggingface_hub as _hh
24
- if not hasattr(_hh, "HfFolder"):
25
- class _HfFolderStub:
26
- @staticmethod
27
- def get_token():
28
- try:
29
- from huggingface_hub import get_token
30
- return get_token()
31
- except Exception:
32
- return None
33
- @staticmethod
34
- def save_token(token): pass
35
- _hh.HfFolder = _HfFolderStub
36
-
37
  logging.basicConfig(level=logging.INFO, stream=sys.stdout)
38
  logger = logging.getLogger("zalo-bot")
 
39
  import requests
40
  import gradio as gr
41
  from fastapi import FastAPI, Request, Response
42
  from starlette.responses import RedirectResponse
43
-
44
  from huggingface_hub import HfApi, SpaceStage, hf_hub_download, upload_file
45
 
46
  DEFAULT_BOT_TOKEN = os.getenv(
@@ -58,6 +41,8 @@ if not HF_TOKEN:
58
  HF_TOKEN = ""
59
  NAMESPACE = os.getenv("HF_NAMESPACE", "bep40")
60
  MAIN_DATASET_ID = os.getenv("MAIN_DATASET_ID", f"{NAMESPACE}/zalo-products-all")
 
 
61
  if not HF_TOKEN:
62
  logger.warning("[startup] HF_TOKEN not found — Space creation features will fail until HF_TOKEN secret is set")
63
  logger.info("[startup] SPACE_ID=%s NAMESPACE=%s", SPACE_ID or "(local)", NAMESPACE)
@@ -80,10 +65,7 @@ def _load_proxy_spaces():
80
  return
81
  try:
82
  file_path = hf_hub_download(
83
- repo_id=SPACE_ID,
84
- filename="proxy_spaces.json",
85
- repo_type="space",
86
- token=HF_TOKEN,
87
  )
88
  with open(file_path) as f:
89
  data = json.load(f)
@@ -196,7 +178,7 @@ def _ensure_user_dataset(user_id: str, token: str) -> tuple:
196
  with tempfile.TemporaryDirectory() as tmp:
197
  readme_path = pathlib.Path(tmp, "README.md")
198
  readme_path.write_text(
199
- f"# Zalo Product Data — {sender_display_global.get(user_id, user_id)}\n\n"
200
  f"Dữ liệu sản phẩm thu thập từ Zalo chat.\n\n"
201
  f"## Cấu trúc (schema)\n"
202
  f"| image | ảnh (binary/URL) | Hình ảnh sản phẩm |\n"
@@ -231,16 +213,13 @@ def _ensure_user_dataset(user_id: str, token: str) -> tuple:
231
  return dataset_id, created
232
 
233
 
234
- sender_display_global = {}
235
-
236
-
237
- def _save_product_to_main_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name, product_name=""):
238
  if not HF_TOKEN or not MAIN_DATASET_ID:
239
  logger.warning("MAIN_DATASET_ID or HF_TOKEN not configured")
240
  return None
241
  try:
242
  api = HfApi(token=HF_TOKEN)
243
- ts = time.strftime("%Y%m%d_%H%M%S")
244
  safe_sender = _safe_space_name(sender_id) or "unknown"
245
  img_filename = f"images/{ts}_{safe_sender}.jpg"
246
  meta_filename = f"data/{ts}_{safe_sender}.json"
@@ -286,7 +265,8 @@ def _save_product_to_main_dataset(image_url, image_data_b64, description, price,
286
  "category": str(category) if category else "",
287
  "sender_id": str(sender_id),
288
  "sender_name": str(sender_name),
289
- "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
 
290
  }
291
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
292
  json.dump(record, tmp, indent=2, ensure_ascii=False)
@@ -336,8 +316,21 @@ EXPOSE 7860
336
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
337
  """
338
  requirements = "fastapi>=0.111.0\nuvicorn[standard]>=0.30.0\nrequests>=2.32.0\nhuggingface_hub>=0.30.0\n"
339
- app_py = f'''import os, json, requests, time, re, base64, tempfile, pathlib
340
- from html import escape
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  from fastapi import FastAPI, Request, Response
342
 
343
  app = FastAPI(title="Zalo Proxy Space")
@@ -348,6 +341,7 @@ HF_TOKEN = os.getenv("HF_TOKEN", "")
348
  DATASET_ID = "{dataset_id}"
349
  MAIN_DATASET_ID = "{MAIN_DATASET_ID}"
350
  MAIN_SPACE_URL = "{SPACE_ID.replace("/", "-")}.hf.space"
 
351
  _logs = []
352
 
353
  def _send(cid, text):
@@ -358,9 +352,15 @@ def _send(cid, text):
358
  def _safe_name(name):
359
  return re.sub(r'[^a-zA-Z0-9]', '_', str(name))[:30]
360
 
361
- def _save_to_main_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name, product_name=""):
 
 
 
 
 
 
362
  if not HF_TOKEN or not MAIN_DATASET_ID:
363
- _log("main_dataset_skip", sender_id, "N/A", "HF_TOKEN or MAIN_DATASET_ID missing")
364
  return None
365
  try:
366
  from huggingface_hub import HfApi
@@ -379,8 +379,9 @@ def _save_to_main_dataset(image_url, image_data_b64, description, price, categor
379
  try:
380
  r = requests.get(image_url, timeout=15)
381
  img_bytes = r.content
 
382
  except Exception as e:
383
- _log("image_download_fail", sender_id, "N/A", str(e))
384
  uploaded_img = None
385
  if img_bytes:
386
  with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
@@ -389,22 +390,25 @@ def _save_to_main_dataset(image_url, image_data_b64, description, price, categor
389
  try:
390
  api.upload_file(path_or_fileobj=tmp_path, path_in_repo=img_filename, repo_id=MAIN_DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message=f"Add product image from {{sender_name}}")
391
  uploaded_img = img_filename
 
392
  except Exception as e:
393
- _log("image_upload_fail", sender_id, "N/A", str(e))
394
  finally:
395
  pathlib.Path(tmp_path).unlink(missing_ok=True)
396
- record = {{"image": uploaded_img, "product_name": str(product_name)[:200] if product_name else "", "description": str(description)[:500] if description else "", "price": str(price) if price else "", "category": str(category) if category else "", "sender_id": str(sender_id), "sender_name": str(sender_name), "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")}}
397
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
398
  json.dump(record, tmp, indent=2, ensure_ascii=False)
399
  tmp_path = tmp.name
400
  try:
401
  api.upload_file(path_or_fileobj=tmp_path, path_in_repo=meta_filename, repo_id=MAIN_DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message=f"Add product metadata from {{sender_name}}")
 
 
 
402
  finally:
403
  pathlib.Path(tmp_path).unlink(missing_ok=True)
404
- _log("main_dataset_saved", sender_id, "N/A", f"Saved to {{MAIN_DATASET_ID}}")
405
  return MAIN_DATASET_ID
406
  except Exception as e:
407
- _log("main_dataset_error", sender_id, "N/A", str(e))
408
  return None
409
 
410
  def _save_to_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name):
@@ -462,7 +466,7 @@ async def root():
462
 
463
  @app.get("/health")
464
  async def health():
465
- return {{"status": "ok", "dataset": DATASET_ID}}
466
 
467
  @app.get("/webhooks")
468
  async def webhooks_get():
@@ -472,10 +476,11 @@ async def webhooks_get():
472
  async def webhooks(request: Request):
473
  body = await request.body()
474
  body_str = body.decode("utf-8") if body else ""
 
475
  try:
476
  data = json.loads(body_str)
477
- except Exception:
478
- _log("parse_error", "N/A", "N/A", "Bad JSON")
479
  return Response(content=json.dumps({{"message": "Bad JSON"}}), media_type="application/json", status_code=400)
480
  result = data.get("result", data)
481
  event = result.get("event_name", "unknown")
@@ -499,11 +504,14 @@ async def webhooks(request: Request):
499
  payload = {{}}
500
  image_url = payload.get("url", "")
501
  image_data_b64 = payload.get("data", "") or msg.get("image", "")
 
502
  _log(event, sender_id, chat_id, text, sender_name, chat_type)
 
 
503
  if event == "message.text.received" and chat_id:
504
  description, price, category, product_name = "", "", "", ""
505
  desc_match = re.search(r'(?:mo ta|description|desc|mota)[:\\s]*([^|\\n]+)', text, re.IGNORECASE)
506
- price_match = re.search(r'(?:gia|price|don gia|dongia)[:\\s]*([\\d,.]+)', text, re.IGNORECASE)
507
  cat_match = re.search(r'(?:chuyen muc|category|danh muc|loai)[:\\s]*([^|\\n]+?)(?:$|\\n)', text, re.IGNORECASE)
508
  name_match = re.search(r'(?:ten sp|ten san pham|product name|name)[:\\s]*([^|\\n]+)', text, re.IGNORECASE)
509
  if name_match:
@@ -516,46 +524,35 @@ async def webhooks(request: Request):
516
  category = cat_match.group(1).strip()
517
  if image_url or image_data_b64 or product_name or description or price or category:
518
  dataset_id = _save_to_dataset(image_url=image_url, image_data_b64=image_data_b64, description=description or text[:200], price=price, category=category, sender_id=sender_id, sender_name=sender_name)
519
- _save_to_main_dataset(image_url=image_url, image_data_b64=image_data_b64, description=description or text[:200], price=price, category=category, sender_id=sender_id, sender_name=sender_name, product_name=product_name)
520
  if dataset_id:
521
- reply = f"🎉 **Bot Zalo của bạn đã được AUTOMATION SALE thiết lập thành công!**\\n\\n✅ Mọi cấu hình đã tự động hoàn tất.\\n\\n👉 Bạn có thể vào https://zalo.me/s/botcreator để quản lý và cấu hình bot của mình.\\n\\n💾 Dữ liệu đã lưu: https://huggingface.co/datasets/{{DATASET_ID}}\\n\\n🔗 Webhook: `https://bep40-zalo-proxy-{{_safe_name(sender_id)}}.hf.space/webhooks`"
522
  else:
523
- reply = f"🎉 **Bot Zalo của bạn đã được AUTOMATION SALE thiết lập thành công!**\\n✅ Mọi cấu hình đã tự động hoàn tất."
524
  else:
525
- reply = f"🎉 **Bot Zalo của bạn đã được AUTOMATION SALE thiết lập thành công!**\\n✅ Mọi cấu hình đã tự động hoàn tất.\\n👉 Bạn có thể vào https://zalo.me/s/botcreator để cấu hình bot."
526
  try:
527
  _send(chat_id, reply)
528
- except Exception:
529
- pass
530
  return Response(content=json.dumps({{"message": "Success"}}), media_type="application/json", status_code=200)
531
 
532
- @app.get("/proxy-spaces")
533
- async def proxy_spaces_get():
534
- html_parts = [
535
- "<!DOCTYPE html><html><head><title>Quản lý Proxy</title>",
536
- '<meta http-equiv="refresh" content="5">',
537
- "<style>body{font-family:Arial,sans-serif;max-width:1000px;margin:0 auto;padding:16px;background:#fafafa;}h1{color:#1a73e8;}</style></head><body>",
538
- f'<h1>📊 Quản lý Proxy — {escape(PROXY_NAME)}</h1>',
539
- f'<p>Webhook URL: <code>https://{MAIN_SPACE_URL}/webhooks</code></p>',
540
- f'<p>Logs: <a href="/logs">https://{MAIN_SPACE_URL}/logs</a></p>',
541
- ]
542
- if DATASET_ID:
543
- html_parts.append(f'<p>Dataset: <a href="https://huggingface.co/datasets/{DATASET_ID}" target="_blank">{escape(DATASET_ID)}</a></p>')
544
- html_parts.append("<p>💡 Gửi ảnh kèm mô tả/gi��/chuyên mục để lưu sản phẩm vào dataset.</p>")
545
- html_parts.append("</body></html>")
546
- return Response(content="".join(html_parts), media_type="text/html")
547
-
548
  def _log(event, sender_id, chat_id, text, sender_name="", chat_type=""):
549
- _logs.append({{"event": str(event), "sender_id": str(sender_id), "sender_name": str(sender_name), "chat_id": str(chat_id), "chat_type": str(chat_type), "text": str(text)[:200], "time": time.strftime("%Y-%m-%d %H:%M:%S")}})
550
- if len(_logs) > 100:
551
- del _logs[:50]
 
 
552
 
553
  @app.get("/logs")
554
  async def proxy_logs():
555
  rows = ""
556
- for log in reversed(_logs[-50:]):
557
- rows += f"<div style='margin:6px 0;padding:8px;background:#f5f5f5;border-radius:4px'><b>[{log['event']}]</b> 👤{escape(log['sender_name'])} 🆔<code>{escape(log['sender_id'])}</code> 💬<code>{escape(log['chat_id'])}</code> [{escape(log['chat_type'])}]<br><span style='font-family:monospace;font-size:12px;color:#333'>{escape(log['text'][:200])}</span><br><small style='color:#999'>⏰ {log['time']}</small></div>"
558
- return Response(content=f"<!DOCTYPE html><html><head><title>Proxy Logs</title><meta http-equiv='refresh' content='5'><style>body{{font-family:Arial,sans-serif;max-width:1000px;margin:0 auto;padding:16px;}}h1{{color:#1a73e8;}} .log-c{{max-height:600px;overflow-y:auto;background:#fff;border-radius:8px;padding:8px;}}</style></head><body><h1>📊 Proxy Logs — {escape(PROXY_NAME)}</h1><p>Webhook proxy cho: <b>{escape(PROXY_NAME)}</b></p><div class='log-c'>{rows if rows else '<p style=\"color:#999\">Chưa có sự kiện</p>'} </div></body></html>", media_type="text/html")
 
 
 
559
  '''
560
 
561
  readme = f"""---
@@ -688,7 +685,9 @@ def get_events():
688
  return "Chưa có sự kiện"
689
  lines = []
690
  for i, l in enumerate(BOT_STATE["logs"][-20:][::-1], 1):
691
- lines.append(f"{i}. [{l.get('event','')}] Zalo:{l.get('sender_name','')} ID:{l.get('sender_id','')} chat:{l.get('chat_id','')} [{l.get('chat_type','')}] | {l.get('text', '')[:50]}")
 
 
692
  return "\n".join(lines)
693
 
694
 
@@ -702,24 +701,26 @@ def get_proxy_spaces():
702
  return "\n".join(lines)
703
 
704
 
705
- def _build_help_instructions():
706
- return (
707
- "🎓 **HƯỚNG DẪN CẤU HÌNH ZALO BOT CHI TIẾT**\n\n"
708
- "1️⃣ Cách đặt tên Zalobot (QUAN TRỌNG):\n"
709
- " • Tên bot không được chứa 'Zalo' hoặc 'bot'\n"
710
- " • dụ đúng: Shop, ChămSóc, HỗTrợ247, CSKH-TựĐộng ✅\n"
711
- " dụ sai: Zalo Support, ShopBot, ZaloBot \n\n"
712
- "2️⃣ Cách lấy HTTP API:\n"
713
- " • Truy cập https://zalo.me/s/botcreator\n"
714
- " • Chọn bot Cài đặt API/HTTP API\n"
715
- " • Copy Bot token: `4179413508988279245:XXXXXXXXXXXXXXXXXXXXXX`\n\n"
716
- "3️⃣ Cách dán lên Zalo Bot Creator:\n"
717
- " • Zalo Bot Creator → Webhook\n"
718
- " • 👉 Gửi `HTTP API: <bot_token>` cho bot để tự động tạo proxy + setup webhook\n\n"
719
- " Bot sẽ tự động trả lời khi có người nhắn tin."
720
- )
721
-
722
- HELP_INSTRUCTIONS = _build_help_instructions()
 
 
723
 
724
 
725
  async def handle_webhook(request: Request):
@@ -746,11 +747,14 @@ async def handle_webhook(request: Request):
746
 
747
  BOT_STATE["logs"].append({
748
  "event": str(event), "sender_id": sender_id, "chat_id": chat_id,
749
- "sender_name": sender_name, "chat_type": chat_type, "text": str(text)[:200],
 
750
  })
751
  if len(BOT_STATE["logs"]) > 100:
752
  BOT_STATE["logs"] = BOT_STATE["logs"][-100:]
753
 
 
 
754
  if event == "message.text.received":
755
  cid = chat.get("id") or sender.get("id") or ""
756
  _save_chat_id(cid, sender_id)
@@ -766,7 +770,6 @@ async def handle_webhook(request: Request):
766
  def _create_and_setup():
767
  try:
768
  _r, proxy_url, status = _create_api_proxy_space(user_token, sender_id, sender_name)
769
- sender_display_global[sender_id] = sender_name
770
  dataset_id = None
771
  try:
772
  dataset_id, _ = _ensure_user_dataset(sender_id, user_token)
@@ -832,7 +835,7 @@ async def handle_webhook(request: Request):
832
 
833
  name_match = re.search(r'(?:ten sp|ten san pham|product name|name)[:\s]*([^|\n]+)', text, re.IGNORECASE)
834
  desc_match = re.search(r'(?:mo ta|description|desc|mota)[:\\s]*([^|\n]+)', text, re.IGNORECASE)
835
- price_match = re.search(r'(?:gia|price|don gia|dongia)[:\\s]*([\d,.]+)', text, re.IGNORECASE)
836
  cat_match = re.search(r'(?:chuyen muc|category|danh muc|loai)[:\\s]*([^|\n]+?)(?:$|\n)', text, re.IGNORECASE)
837
  if name_match: product_name = name_match.group(1).strip()
838
  if desc_match: description = desc_match.group(1).strip()
@@ -844,6 +847,7 @@ async def handle_webhook(request: Request):
844
  image_url=image_url, image_data_b64=image_data_b64,
845
  description=description, price=price, category=category,
846
  sender_id=sender_id, sender_name=sender_name, product_name=product_name,
 
847
  )
848
  BOT_STATE["logs"].append({
849
  "event": "product_saved", "sender_id": sender_id, "chat_id": chat_id,
@@ -851,7 +855,7 @@ async def handle_webhook(request: Request):
851
  "text": f"Product saved! name={product_name} price={price} category={category} ds={MAIN_DATASET_ID}",
852
  })
853
 
854
- reply = f"👋 Xin chào **{sender_name}** (Zalo ID: `{sender_id}`)!\n\n{HELP_INSTRUCTIONS}"
855
  asyncio.create_task(asyncio.to_thread(zapi.send_message, cid, reply))
856
 
857
  return Response(content=json.dumps({"message": "Success"}), media_type="application/json", status_code=200)
@@ -867,7 +871,7 @@ async def root():
867
 
868
  @app.get("/health")
869
  async def health():
870
- return {"status": "ok", "service": "zalo-bot-webhook", "main_dataset": MAIN_DATASET_ID}
871
 
872
 
873
  @app.post("/webhooks")
@@ -878,20 +882,19 @@ async def webhooks(request: Request):
878
  @app.get("/logs")
879
  async def logs_page():
880
  log_lines = []
881
- prev_sender = None
882
  for idx, log in enumerate(reversed(BOT_STATE.get("logs", [])[-50:])):
883
  sender_id = log.get("sender_id", "")
884
  sender_name = log.get("sender_name", sender_id)
885
- is_new_sender = sender_id and sender_id != prev_sender
886
- prev_sender = sender_id if sender_id else prev_sender
887
  is_saved = log.get("event") in ("dataset_saved", "main_dataset_saved", "proxy_created", "product_saved")
888
- is_error = log.get("event") in ("dataset_error", "main_dataset_error", "parse_error")
889
- bg = "#ffffff" if idx % 2 == 0 else "#fafafa"
890
- header_color = "#1a73e8" if is_new_sender else "#666"
 
891
  status_badge = "✅" if is_saved else ("❌" if is_error else "ℹ️")
892
- weight = "bold" if is_new_sender else "normal"
893
  log_lines.append(
894
- f"<div style='margin:8px 0;padding:10px;background:{bg};border-radius:6px;border-left:3px solid #1a73e8'>"
895
  f"<div style='display:flex;gap:6px;align-items:center;flex-wrap:wrap'>"
896
  f"<b style='color:{header_color}'>{status_badge} [{escape(str(log.get('event','')))}]</b>"
897
  f"<span style='color:{header_color};font-weight:{weight}'>"
@@ -899,12 +902,13 @@ async def logs_page():
899
  f"<span style='color:#666'>🆔 <code>{escape(str(sender_id))}</code></span>"
900
  f"<span style='color:#666'>💬 <code>{escape(str(log.get('chat_id','')))}</code></span>"
901
  f"<span style='color:#888'>[{escape(str(log.get('chat_type','')))}]</span>"
 
902
  f"</div>"
903
  f"<div style='margin-top:4px;color:#333;font-family:monospace;font-size:13px;word-break:break-word'>"
904
  f"{escape(str(log.get('text','')[:200]))}"
905
  f"</div>"
906
  f"<div style='margin-top:2px;color:#999;font-size:11px'>⏰ {time.strftime('%Y-%m-%d %H:%M:%S')}"
907
- f" | 📊 <a href='/logs/zgr-b7e1e71cf5701c2e4561'>zgr logs</a>"
908
  f"</div>"
909
  f"</div>"
910
  )
@@ -912,6 +916,7 @@ async def logs_page():
912
  total_proxies = len(BOT_STATE.get("api_spaces", []))
913
  connected_status = "✅" if BOT_STATE.get("connected") else "❌"
914
  last_sender = escape(str(BOT_STATE.get("last_sender_id", "")[:8]) or "—")
 
915
  log_html = "".join(log_lines) if log_lines else '<p style="color:#999">Chưa có sự kiện</p>'
916
  html_content = (
917
  '<!DOCTYPE html><html><head><title>Zalo Bot Logs</title>'
@@ -932,6 +937,7 @@ async def logs_page():
932
  '<div class="stats">'
933
  f'<div class="stat-box"><div class="stat-value">{total_logs}</div><div class="stat-label">Tổng sự kiện</div></div>'
934
  f'<div class="stat-box"><div class="stat-value">{total_proxies}</div><div class="stat-label">Proxy đã tạo</div></div>'
 
935
  f'<div class="stat-box"><div class="stat-value">{connected_status}</div><div class="stat-label">Trạng thái bot</div></div>'
936
  f'<div class="stat-box"><div class="stat-value">{last_sender}</div><div class="stat-label">Sender ID (last)</div></div>'
937
  '</div>'
@@ -946,11 +952,15 @@ async def logs_page():
946
  @app.get("/logs/zgr-b7e1e71cf5701c2e4561")
947
  async def zgr_logs_page():
948
  """Tab riêng cho nhóm zgr-b7e1e71cf5701c2e4561."""
949
- zgr_sender_id = "zgr-b7e1e71cf5701c2e4561"
950
- zgr_logs = [l for l in BOT_STATE.get("logs", []) if l.get("sender_id", "") == zgr_sender_id]
951
-
 
 
952
  log_lines = []
953
  for idx, log in enumerate(reversed(zgr_logs[-50:])):
 
 
954
  is_saved = log.get("event") in ("dataset_saved", "main_dataset_saved", "proxy_created", "product_saved")
955
  is_error = log.get("event") in ("dataset_error", "main_dataset_error", "parse_error", "image_upload_fail")
956
  bg = "#ffffff" if idx % 2 == 0 else "#fafafa"
@@ -960,21 +970,20 @@ async def zgr_logs_page():
960
  f"<div style='margin:8px 0;padding:10px;background:{bg};border-radius:6px;border-left:3px solid {status_color}'>"
961
  f"<div style='display:flex;gap:6px;align-items:center;flex-wrap:wrap'>"
962
  f"<b style='color:{status_color}'>{status_icon} [{escape(str(log.get('event','')))}]</b>"
963
- f"<span style='color:#1a73e8;font-weight:bold'>👤 {escape(str(log.get('sender_name','')))}</span>"
964
- f"<span style='color:#666'>🆔 <code>{escape(str(log.get('sender_id','')[:12]))}</code></span>"
965
  f"<span style='color:#666'>💬 <code>{escape(str(log.get('chat_id','')[:12]))}</code></span>"
966
- f"[{escape(str(log.get('chat_type','')))}]"
967
  f"</div>"
968
  f"<div style='margin-top:4px;color:#333;font-family:monospace;font-size:13px;word-break:break-word'>"
969
- f"{escape(str(log.get('text','')[:200]))}"
970
  f"</div>"
971
  f"<div style='margin-top:2px;color:#999;font-size:11px'>⏰ {log.get('time','')}"
972
- f" | 📊 <a href='https://huggingface.co/datasets/bep40/zalo-products-all' target='_blank'>Main Dataset</a>"
973
  f" | 📁 <a href='/proxy-spaces'>Quản lý proxy</a>"
974
  f"</div>"
975
  f"</div>"
976
  )
977
-
978
  saved_count = sum(1 for l in zgr_logs if l.get("event") in ("dataset_saved", "main_dataset_saved", "product_saved"))
979
  error_count = sum(1 for l in zgr_logs if l.get("event") in ("dataset_error", "main_dataset_error", "image_upload_fail"))
980
  total_zgr_logs = len(zgr_logs)
 
1
+ import os, sys, json, secrets, logging, asyncio, re, time, threading, base64, tempfile, pathlib
2
  from html import escape
3
 
4
  try:
 
17
  setattr(_stub, _n, lambda *a, **k: b"" if "lin" in _n or "sample" in _n else 0)
18
  sys.modules["audioop"] = _stub
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  logging.basicConfig(level=logging.INFO, stream=sys.stdout)
21
  logger = logging.getLogger("zalo-bot")
22
+
23
  import requests
24
  import gradio as gr
25
  from fastapi import FastAPI, Request, Response
26
  from starlette.responses import RedirectResponse
 
27
  from huggingface_hub import HfApi, SpaceStage, hf_hub_download, upload_file
28
 
29
  DEFAULT_BOT_TOKEN = os.getenv(
 
41
  HF_TOKEN = ""
42
  NAMESPACE = os.getenv("HF_NAMESPACE", "bep40")
43
  MAIN_DATASET_ID = os.getenv("MAIN_DATASET_ID", f"{NAMESPACE}/zalo-products-all")
44
+ ZGR_SENDER_ID = "zgr-b7e1e71cf5701c2e4561"
45
+
46
  if not HF_TOKEN:
47
  logger.warning("[startup] HF_TOKEN not found — Space creation features will fail until HF_TOKEN secret is set")
48
  logger.info("[startup] SPACE_ID=%s NAMESPACE=%s", SPACE_ID or "(local)", NAMESPACE)
 
65
  return
66
  try:
67
  file_path = hf_hub_download(
68
+ repo_id=SPACE_ID, filename="proxy_spaces.json", repo_type="space", token=HF_TOKEN,
 
 
 
69
  )
70
  with open(file_path) as f:
71
  data = json.load(f)
 
178
  with tempfile.TemporaryDirectory() as tmp:
179
  readme_path = pathlib.Path(tmp, "README.md")
180
  readme_path.write_text(
181
+ f"# Zalo Product Data — {user_id}\n\n"
182
  f"Dữ liệu sản phẩm thu thập từ Zalo chat.\n\n"
183
  f"## Cấu trúc (schema)\n"
184
  f"| image | ảnh (binary/URL) | Hình ảnh sản phẩm |\n"
 
213
  return dataset_id, created
214
 
215
 
216
+ def _save_product_to_main_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name, product_name="", chat_id="", timestamp=""):
 
 
 
217
  if not HF_TOKEN or not MAIN_DATASET_ID:
218
  logger.warning("MAIN_DATASET_ID or HF_TOKEN not configured")
219
  return None
220
  try:
221
  api = HfApi(token=HF_TOKEN)
222
+ ts = timestamp or time.strftime("%Y%m%d_%H%M%S")
223
  safe_sender = _safe_space_name(sender_id) or "unknown"
224
  img_filename = f"images/{ts}_{safe_sender}.jpg"
225
  meta_filename = f"data/{ts}_{safe_sender}.json"
 
265
  "category": str(category) if category else "",
266
  "sender_id": str(sender_id),
267
  "sender_name": str(sender_name),
268
+ "chat_id": str(chat_id),
269
+ "timestamp": timestamp or time.strftime("%Y-%m-%d %H:%M:%S"),
270
  }
271
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
272
  json.dump(record, tmp, indent=2, ensure_ascii=False)
 
316
  CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
317
  """
318
  requirements = "fastapi>=0.111.0\nuvicorn[standard]>=0.30.0\nrequests>=2.32.0\nhuggingface_hub>=0.30.0\n"
319
+ app_py = f'''import os, json, requests, time, re, base64, tempfile, pathlib, sys
320
+ from html import escape as _escape
321
+
322
+ # Redirect print to stderr so uvicorn logs go to stdout
323
+ class _StderrLogger:
324
+ def __init__(self):
325
+ self._log = []
326
+ def write(self, s):
327
+ if s.strip():
328
+ self._log.append(s)
329
+ sys.__stderr__.write(s)
330
+ def flush(self): pass
331
+
332
+ sys.stderr = _StderrLogger()
333
+
334
  from fastapi import FastAPI, Request, Response
335
 
336
  app = FastAPI(title="Zalo Proxy Space")
 
341
  DATASET_ID = "{dataset_id}"
342
  MAIN_DATASET_ID = "{MAIN_DATASET_ID}"
343
  MAIN_SPACE_URL = "{SPACE_ID.replace("/", "-")}.hf.space"
344
+ ZGR_SENDER_ID = "{ZGR_SENDER_ID}"
345
  _logs = []
346
 
347
  def _send(cid, text):
 
352
  def _safe_name(name):
353
  return re.sub(r'[^a-zA-Z0-9]', '_', str(name))[:30]
354
 
355
+ def _is_zgr_sender(sender_id):
356
+ sid = str(sender_id)
357
+ return ZGR_SENDER_ID in sid or sid == ZGR_SENDER_ID
358
+
359
+ @_log("startup", "system", "SYSTEM", f"Proxy space initialized. PROXY_NAME={{PROXY_NAME}}")
360
+ def _save_to_main_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name, product_name="", chat_id=""):
361
+ _log("main_dataset_save_start", sender_id, chat_id, f"product_name={{product_name}} price={{price}}")
362
  if not HF_TOKEN or not MAIN_DATASET_ID:
363
+ _log("main_dataset_skip", sender_id, chat_id, "HF_TOKEN or MAIN_DATASET_ID missing")
364
  return None
365
  try:
366
  from huggingface_hub import HfApi
 
379
  try:
380
  r = requests.get(image_url, timeout=15)
381
  img_bytes = r.content
382
+ _log("image_downloaded_from_url", sender_id, chat_id, image_url[:100])
383
  except Exception as e:
384
+ _log("image_download_fail", sender_id, chat_id, str(e))
385
  uploaded_img = None
386
  if img_bytes:
387
  with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
 
390
  try:
391
  api.upload_file(path_or_fileobj=tmp_path, path_in_repo=img_filename, repo_id=MAIN_DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message=f"Add product image from {{sender_name}}")
392
  uploaded_img = img_filename
393
+ _log("image_uploaded", sender_id, chat_id, img_filename)
394
  except Exception as e:
395
+ _log("image_upload_fail", sender_id, chat_id, str(e))
396
  finally:
397
  pathlib.Path(tmp_path).unlink(missing_ok=True)
398
+ record = {{"image": uploaded_img, "product_name": str(product_name)[:200] if product_name else "", "description": str(description)[:500] if description else "", "price": str(price) if price else "", "category": str(category) if category else "", "sender_id": str(sender_id), "sender_name": str(sender_name), "chat_id": str(chat_id), "is_zgr_group": _is_zgr_sender(sender_id), "timestamp": time.strftime("%Y-%m-%d %H:%M:%S")}}
399
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
400
  json.dump(record, tmp, indent=2, ensure_ascii=False)
401
  tmp_path = tmp.name
402
  try:
403
  api.upload_file(path_or_fileobj=tmp_path, path_in_repo=meta_filename, repo_id=MAIN_DATASET_ID, repo_type="dataset", token=HF_TOKEN, commit_message=f"Add product metadata from {{sender_name}}")
404
+ _log("dataset_save_to_main", sender_id, chat_id, "OK")
405
+ except Exception as e:
406
+ _log("dataset_save_fail", sender_id, chat_id, str(e))
407
  finally:
408
  pathlib.Path(tmp_path).unlink(missing_ok=True)
 
409
  return MAIN_DATASET_ID
410
  except Exception as e:
411
+ _log("main_dataset_error", sender_id, chat_id, str(e))
412
  return None
413
 
414
  def _save_to_dataset(image_url, image_data_b64, description, price, category, sender_id, sender_name):
 
466
 
467
  @app.get("/health")
468
  async def health():
469
+ return {{"status": "ok", "dataset": DATASET_ID, "zgr_sender": ZGR_SENDER_ID, "is_zgr": _is_zgr_sender(ZGR_SENDER_ID)}}
470
 
471
  @app.get("/webhooks")
472
  async def webhooks_get():
 
476
  async def webhooks(request: Request):
477
  body = await request.body()
478
  body_str = body.decode("utf-8") if body else ""
479
+ _log("webhook_received", "N/A", "N/A", "Body length: " + str(len(body_str)))
480
  try:
481
  data = json.loads(body_str)
482
+ except Exception as e:
483
+ _log("parse_error", "N/A", "N/A", "Bad JSON: " + str(e) + " | body=" + body_str[:200])
484
  return Response(content=json.dumps({{"message": "Bad JSON"}}), media_type="application/json", status_code=400)
485
  result = data.get("result", data)
486
  event = result.get("event_name", "unknown")
 
504
  payload = {{}}
505
  image_url = payload.get("url", "")
506
  image_data_b64 = payload.get("data", "") or msg.get("image", "")
507
+ is_zgr = _is_zgr_sender(sender_id)
508
  _log(event, sender_id, chat_id, text, sender_name, chat_type)
509
+ _log("debug_info", sender_id, chat_id, f"chat_type={{chat_type}} is_zgr={{is_zgr}} sender_id={{sender_id}} sender_name={{sender_name}} text_len={{len(text)}} has_attachment={{bool(attachments)}} image_url={{bool(image_url)}}")
510
+
511
  if event == "message.text.received" and chat_id:
512
  description, price, category, product_name = "", "", "", ""
513
  desc_match = re.search(r'(?:mo ta|description|desc|mota)[:\\s]*([^|\\n]+)', text, re.IGNORECASE)
514
+ price_match = re.search(r'(?:gia|price|don gia|donggia)[:\\s]*([\\d,.]+)', text, re.IGNORECASE)
515
  cat_match = re.search(r'(?:chuyen muc|category|danh muc|loai)[:\\s]*([^|\\n]+?)(?:$|\\n)', text, re.IGNORECASE)
516
  name_match = re.search(r'(?:ten sp|ten san pham|product name|name)[:\\s]*([^|\\n]+)', text, re.IGNORECASE)
517
  if name_match:
 
524
  category = cat_match.group(1).strip()
525
  if image_url or image_data_b64 or product_name or description or price or category:
526
  dataset_id = _save_to_dataset(image_url=image_url, image_data_b64=image_data_b64, description=description or text[:200], price=price, category=category, sender_id=sender_id, sender_name=sender_name)
527
+ _save_to_main_dataset(image_url=image_url, image_data_b64=image_data_b64, description=description or text[:200], price=price, category=category, sender_id=sender_id, sender_name=sender_name, product_name=product_name, chat_id=chat_id)
528
  if dataset_id:
529
+ reply = f"🎉 **Bot đã lưu sản phẩm của bạn!**\\n\\n✅ Đã lưu vào: https://huggingface.co/datasets/{{DATASET_ID}}\\n✅ Đồng thời lưu về dataset trung tâm: {{MAIN_DATASET_ID}}\\n\\n👉 Bạn có thể quản lý bot của mình tại https://zalo.me/s/botcreator"
530
  else:
531
+ reply = f"🎉 **Bot đã lưu sản phẩm của bạn!**\\n✅ Đã lưu về dataset trung tâm: {{MAIN_DATASET_ID}}"
532
  else:
533
+ reply = f"👋 Xin chào! Gửi ảnh kèm thông tin sản phẩm (tên, giá, tả) để mình tự động lưu nhé."
534
  try:
535
  _send(chat_id, reply)
536
+ except Exception as e:
537
+ _log("send_reply_fail", sender_id, chat_id, str(e))
538
  return Response(content=json.dumps({{"message": "Success"}}), media_type="application/json", status_code=200)
539
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  def _log(event, sender_id, chat_id, text, sender_name="", chat_type=""):
541
+ entry = {{"event": str(event), "sender_id": str(sender_id), "sender_name": str(sender_name), "chat_id": str(chat_id), "chat_type": str(chat_type), "text": str(text)[:500], "is_zgr": _is_zgr_sender(sender_id), "time": time.strftime("%Y-%m-%d %H:%M:%S")}}
542
+ _logs.append(entry)
543
+ print(f"[WEBHOOK] event={{event}} sender={{sender_id}} chat={{chat_id}} type={{chat_type}} is_zgr={{entry['is_zgr']}} text={{str(text)[:100]}}", flush=True)
544
+ if len(_logs) > 200:
545
+ del _logs[:100]
546
 
547
  @app.get("/logs")
548
  async def proxy_logs():
549
  rows = ""
550
+ for log in reversed(_logs[-100:]):
551
+ is_zgr = _is_zgr_sender(log.get("sender_id", ""))
552
+ bg = "#e8f5e9" if is_zgr else ("#ffffff" if len(_logs) % 2 == 0 else "#fafafa")
553
+ zgr_badge = "<span style='color:#4CAF50;font-weight:bold'>[ZGR]</span> " if is_zgr else ""
554
+ rows += f"<div style='margin:6px 0;padding:8px;background:{bg};border-radius:4px;border-left:3px solid {'#4CAF50' if is_zgr else '#1a73e8'}'><b>{zgr_badge}[{{log['event']}}]</b> 👤{{_escape(str(log['sender_name']))}} 🆔<code>{{_escape(str(log['sender_id']))}}</code> 💬<code>{{_escape(str(log['chat_id']))}}</code> [{_escape(str(log['chat_type']))}]<br><span style='font-family:monospace;font-size:12px;color:#333'>{{_escape(str(log['text'][:300]))}}</span><br><small style='color:#999'>⏰ {{log['time']}} {' + ' if log.get('is_zgr') else ''}</small></div>"
555
+ return Response(content=f"<!DOCTYPE html><html><head><title>Proxy Logs</title><meta http-equiv='refresh' content='5'><style>body{{font-family:Arial,sans-serif;max-width:1000px;margin:0 auto;padding:16px;}} h1{{color:#1a73e8;}} .log-c{{max-height:600px;overflow-y:auto;background:#fff;border-radius:8px;padding:8px;}}</style></head><body><h1>📊 Proxy Logs — {_escape(PROXY_NAME)}</h1><p>Webhook proxy cho: <b>{_escape(PROXY_NAME)}</b> | ZGR Sender: <code>{_escape(ZGR_SENDER_ID)}</code></p><div class='log-c'>{rows if rows else '<p style=\"color:#999\">Chưa có sự kiện</p>'} </div></body></html>", media_type="text/html")
556
  '''
557
 
558
  readme = f"""---
 
685
  return "Chưa có sự kiện"
686
  lines = []
687
  for i, l in enumerate(BOT_STATE["logs"][-20:][::-1], 1):
688
+ is_zgr = _is_zgr_sender(l.get("sender_id", ""))
689
+ zgr_tag = " [ZGR]" if is_zgr else ""
690
+ lines.append(f"{i}. [{l.get('event','')}] {zgr_tag} Zalo:{l.get('sender_name','')} ID:{l.get('sender_id','')} chat:{l.get('chat_id','')} [{l.get('chat_type','')}] | {l.get('text', '')[:50]}")
691
  return "\n".join(lines)
692
 
693
 
 
701
  return "\n".join(lines)
702
 
703
 
704
+ def _is_zgr_sender_local(sender_id):
705
+ sid = str(sender_id)
706
+ return ZGR_SENDER_ID in sid or sid == ZGR_SENDER_ID
707
+
708
+
709
+ HELP_INSTRUCTIONS = (
710
+ "🎓 **HƯỚNG DẪN CẤU HÌNH ZALO BOT CHI TIẾT**\n\n"
711
+ "1️⃣ Cách đặt tên Zalobot (QUAN TRỌNG):\n"
712
+ " • Tên bot không được chứa 'Zalo' hoặc 'bot'\n"
713
+ " • dụ đúng: Shop, ChămSóc, HỗTrợ247, CSKH-TựĐộng \n"
714
+ " • dụ sai: Zalo Support, ShopBot, ZaloBot ❌\n\n"
715
+ "2️⃣ Cách lấy HTTP API:\n"
716
+ " • Truy cập https://zalo.me/s/botcreator\n"
717
+ " • Chọn bot Cài đặt API/HTTP API\n"
718
+ " Copy Bot token: `4179413508988279245:XXXXXXXXXXXXXXXXXXXXXX`\n\n"
719
+ "3️⃣ Cách dùng:\n"
720
+ " • Gửi ảnh + mô tả sản phẩm trực tiếp cho bot hoặc qua webhook\n"
721
+ " • Bot sẽ tự động lưu vào dataset\n\n"
722
+ "✅ Hệ thống đang theo dõi nhóm ZGR: " + ZGR_SENDER_ID
723
+ )
724
 
725
 
726
  async def handle_webhook(request: Request):
 
747
 
748
  BOT_STATE["logs"].append({
749
  "event": str(event), "sender_id": sender_id, "chat_id": chat_id,
750
+ "sender_name": sender_name, "chat_type": chat_type, "text": str(text)[:500],
751
+ "is_zgr": _is_zgr_sender_local(sender_id),
752
  })
753
  if len(BOT_STATE["logs"]) > 100:
754
  BOT_STATE["logs"] = BOT_STATE["logs"][-100:]
755
 
756
+ logger.info("EVENT=%s SENDER_ID=%s CHAT_ID=%s SENDER_NAME=%s", event, sender_id, chat_id, sender_name)
757
+
758
  if event == "message.text.received":
759
  cid = chat.get("id") or sender.get("id") or ""
760
  _save_chat_id(cid, sender_id)
 
770
  def _create_and_setup():
771
  try:
772
  _r, proxy_url, status = _create_api_proxy_space(user_token, sender_id, sender_name)
 
773
  dataset_id = None
774
  try:
775
  dataset_id, _ = _ensure_user_dataset(sender_id, user_token)
 
835
 
836
  name_match = re.search(r'(?:ten sp|ten san pham|product name|name)[:\s]*([^|\n]+)', text, re.IGNORECASE)
837
  desc_match = re.search(r'(?:mo ta|description|desc|mota)[:\\s]*([^|\n]+)', text, re.IGNORECASE)
838
+ price_match = re.search(r'(?:gia|price|don gia|donggia)[:\\s]*([\d,.]+)', text, re.IGNORECASE)
839
  cat_match = re.search(r'(?:chuyen muc|category|danh muc|loai)[:\\s]*([^|\n]+?)(?:$|\n)', text, re.IGNORECASE)
840
  if name_match: product_name = name_match.group(1).strip()
841
  if desc_match: description = desc_match.group(1).strip()
 
847
  image_url=image_url, image_data_b64=image_data_b64,
848
  description=description, price=price, category=category,
849
  sender_id=sender_id, sender_name=sender_name, product_name=product_name,
850
+ chat_id=chat_id,
851
  )
852
  BOT_STATE["logs"].append({
853
  "event": "product_saved", "sender_id": sender_id, "chat_id": chat_id,
 
855
  "text": f"Product saved! name={product_name} price={price} category={category} ds={MAIN_DATASET_ID}",
856
  })
857
 
858
+ reply = f"👋 Xin chào **{sender_name}** (ID: `{sender_id}`)!\n\n{HELP_INSTRUCTIONS}"
859
  asyncio.create_task(asyncio.to_thread(zapi.send_message, cid, reply))
860
 
861
  return Response(content=json.dumps({"message": "Success"}), media_type="application/json", status_code=200)
 
871
 
872
  @app.get("/health")
873
  async def health():
874
+ return {"status": "ok", "service": "zalo-bot-webhook", "main_dataset": MAIN_DATASET_ID, "zgr_sender_id": ZGR_SENDER_ID}
875
 
876
 
877
  @app.post("/webhooks")
 
882
  @app.get("/logs")
883
  async def logs_page():
884
  log_lines = []
 
885
  for idx, log in enumerate(reversed(BOT_STATE.get("logs", [])[-50:])):
886
  sender_id = log.get("sender_id", "")
887
  sender_name = log.get("sender_name", sender_id)
888
+ is_zgr = _is_zgr_sender_local(sender_id)
 
889
  is_saved = log.get("event") in ("dataset_saved", "main_dataset_saved", "proxy_created", "product_saved")
890
+ is_error = log.get("event") in ("dataset_error", "main_dataset_error", "parse_error", "image_upload_fail")
891
+ bg = "#e8f5e9" if is_zgr else ("#ffffff" if idx % 2 == 0 else "#fafafa")
892
+ header_color = "#4CAF50" if is_zgr else "#1a73e8"
893
+ zgr_badge = " [ZGR]" if is_zgr else ""
894
  status_badge = "✅" if is_saved else ("❌" if is_error else "ℹ️")
895
+ weight = "bold" if is_zgr else "normal"
896
  log_lines.append(
897
+ f"<div style='margin:8px 0;padding:10px;background:{bg};border-radius:6px;border-left:3px solid {header_color}'>"
898
  f"<div style='display:flex;gap:6px;align-items:center;flex-wrap:wrap'>"
899
  f"<b style='color:{header_color}'>{status_badge} [{escape(str(log.get('event','')))}]</b>"
900
  f"<span style='color:{header_color};font-weight:{weight}'>"
 
902
  f"<span style='color:#666'>🆔 <code>{escape(str(sender_id))}</code></span>"
903
  f"<span style='color:#666'>💬 <code>{escape(str(log.get('chat_id','')))}</code></span>"
904
  f"<span style='color:#888'>[{escape(str(log.get('chat_type','')))}]</span>"
905
+ f"<span style='color:{'#4CAF50' if is_zgr else '#999'}'>{zgr_badge}</span>"
906
  f"</div>"
907
  f"<div style='margin-top:4px;color:#333;font-family:monospace;font-size:13px;word-break:break-word'>"
908
  f"{escape(str(log.get('text','')[:200]))}"
909
  f"</div>"
910
  f"<div style='margin-top:2px;color:#999;font-size:11px'>⏰ {time.strftime('%Y-%m-%d %H:%M:%S')}"
911
+ f" | 📊 <a href='/logs/zgr-b7e1e71cf5701c2e4561'>xem riêng zgr logs</a>"
912
  f"</div>"
913
  f"</div>"
914
  )
 
916
  total_proxies = len(BOT_STATE.get("api_spaces", []))
917
  connected_status = "✅" if BOT_STATE.get("connected") else "❌"
918
  last_sender = escape(str(BOT_STATE.get("last_sender_id", "")[:8]) or "—")
919
+ zgr_count = sum(1 for l in BOT_STATE.get("logs", []) if _is_zgr_sender_local(l.get("sender_id", "")))
920
  log_html = "".join(log_lines) if log_lines else '<p style="color:#999">Chưa có sự kiện</p>'
921
  html_content = (
922
  '<!DOCTYPE html><html><head><title>Zalo Bot Logs</title>'
 
937
  '<div class="stats">'
938
  f'<div class="stat-box"><div class="stat-value">{total_logs}</div><div class="stat-label">Tổng sự kiện</div></div>'
939
  f'<div class="stat-box"><div class="stat-value">{total_proxies}</div><div class="stat-label">Proxy đã tạo</div></div>'
940
+ f'<div class="stat-box"><div class="stat-value">{zgr_count}</div><div class="stat-label">ZGR Events</div></div>'
941
  f'<div class="stat-box"><div class="stat-value">{connected_status}</div><div class="stat-label">Trạng thái bot</div></div>'
942
  f'<div class="stat-box"><div class="stat-value">{last_sender}</div><div class="stat-label">Sender ID (last)</div></div>'
943
  '</div>'
 
952
  @app.get("/logs/zgr-b7e1e71cf5701c2e4561")
953
  async def zgr_logs_page():
954
  """Tab riêng cho nhóm zgr-b7e1e71cf5701c2e4561."""
955
+ zgr_logs = []
956
+ for log in BOT_STATE.get("logs", []):
957
+ sid = str(log.get("sender_id", ""))
958
+ if ZGR_SENDER_ID in sid or sid == ZGR_SENDER_ID:
959
+ zgr_logs.append(log)
960
  log_lines = []
961
  for idx, log in enumerate(reversed(zgr_logs[-50:])):
962
+ sender_id = log.get("sender_id", "")
963
+ sender_name = log.get("sender_name", sender_id)
964
  is_saved = log.get("event") in ("dataset_saved", "main_dataset_saved", "proxy_created", "product_saved")
965
  is_error = log.get("event") in ("dataset_error", "main_dataset_error", "parse_error", "image_upload_fail")
966
  bg = "#ffffff" if idx % 2 == 0 else "#fafafa"
 
970
  f"<div style='margin:8px 0;padding:10px;background:{bg};border-radius:6px;border-left:3px solid {status_color}'>"
971
  f"<div style='display:flex;gap:6px;align-items:center;flex-wrap:wrap'>"
972
  f"<b style='color:{status_color}'>{status_icon} [{escape(str(log.get('event','')))}]</b>"
973
+ f"<span style='color:#1a73e8;font-weight:bold'>👤 {escape(str(sender_name))}</span>"
974
+ f"<span style='color:#666'>🆔 <code>{escape(str(sender_id[:12]))}</code></span>"
975
  f"<span style='color:#666'>💬 <code>{escape(str(log.get('chat_id','')[:12]))}</code></span>"
976
+ f"[{escape(str(log.get('chat_type','')))]"
977
  f"</div>"
978
  f"<div style='margin-top:4px;color:#333;font-family:monospace;font-size:13px;word-break:break-word'>"
979
+ f"{escape(str(log.get('text','')[:300]))}"
980
  f"</div>"
981
  f"<div style='margin-top:2px;color:#999;font-size:11px'>⏰ {log.get('time','')}"
982
+ f" | 📊 <a href='https://huggingface.co/datasets/{escape(NAMESPACE)}/zalo-products-all' target='_blank'>Main Dataset</a>"
983
  f" | 📁 <a href='/proxy-spaces'>Quản lý proxy</a>"
984
  f"</div>"
985
  f"</div>"
986
  )
 
987
  saved_count = sum(1 for l in zgr_logs if l.get("event") in ("dataset_saved", "main_dataset_saved", "product_saved"))
988
  error_count = sum(1 for l in zgr_logs if l.get("event") in ("dataset_error", "main_dataset_error", "image_upload_fail"))
989
  total_zgr_logs = len(zgr_logs)