Spaces:
Running
Running
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
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 — {
|
| 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 |
-
|
| 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 |
-
"
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
if not HF_TOKEN or not MAIN_DATASET_ID:
|
| 363 |
-
_log("main_dataset_skip", sender_id,
|
| 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,
|
| 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,
|
| 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,
|
| 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|
|
| 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
|
| 522 |
else:
|
| 523 |
-
reply = f"🎉 **Bot
|
| 524 |
else:
|
| 525 |
-
reply = f"
|
| 526 |
try:
|
| 527 |
_send(chat_id, reply)
|
| 528 |
-
except Exception:
|
| 529 |
-
|
| 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 |
-
|
| 550 |
-
|
| 551 |
-
|
|
|
|
|
|
|
| 552 |
|
| 553 |
@app.get("/logs")
|
| 554 |
async def proxy_logs():
|
| 555 |
rows = ""
|
| 556 |
-
for log in reversed(_logs[-
|
| 557 |
-
|
| 558 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 692 |
return "\n".join(lines)
|
| 693 |
|
| 694 |
|
|
@@ -702,24 +701,26 @@ def get_proxy_spaces():
|
|
| 702 |
return "\n".join(lines)
|
| 703 |
|
| 704 |
|
| 705 |
-
def
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
|
|
|
|
|
|
| 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)[:
|
|
|
|
| 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|
|
| 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}** (
|
| 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 |
-
|
| 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 = "#
|
|
|
|
| 891 |
status_badge = "✅" if is_saved else ("❌" if is_error else "ℹ️")
|
| 892 |
-
weight = "bold" if
|
| 893 |
log_lines.append(
|
| 894 |
-
f"<div style='margin:8px 0;padding:10px;background:{bg};border-radius:6px;border-left:3px solid
|
| 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 |
-
|
| 950 |
-
|
| 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(
|
| 964 |
-
f"<span style='color:#666'>🆔 <code>{escape(str(
|
| 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','')[:
|
| 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/
|
| 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á, mô 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 |
+
" • Ví dụ đúng: Shop, ChămSóc, HỗTrợ247, CSKH-TựĐộng ✅\n"
|
| 714 |
+
" • Ví 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)
|