feat(bot): add strict whitelist auth bypass, multi-STT consensus pipeline, and applicant logging

This commit is contained in:
2026-10-08 23:24:17 +03:30
parent 77c0cacbb6
commit 19c3055008
15 changed files with 3172 additions and 40 deletions
+77
View File
@@ -0,0 +1,77 @@
import asyncio
import json
import logging
import sys
from pathlib import Path
# Add project root to sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from app.config import settings
from app.bale.client import BaleClient
from app.api.client import ErpClient
from app.runtime import BotContext, LinkedUser
from app.nlu import understand
from app.handlers import quote
from app.defaults import AUTHORIZED_USERS
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s — %(message)s")
logger = logging.getLogger("send_analysis")
CHAT_ID = 1461166742
TRANSCRIPTS = [
{
"name": "مدل 1 (Whisper)",
"text": "الان شبت بخیر می خواستیم که بیست و پنج تا کیسه عرض به خدمت شما که گچ و خاک از مصطفوی بخریم و کنارش هم یه پنجاه تا کیسه سیمان پنجاه کیلویی که اونا تقریبا در می آد پامون دونه ای بیست و پنج میلیون سیمانی ها پامون در می آد حدودا چهل میلیون اینا دونه ای بهمون گفته که می تونین سه قسط بهم بدید کلی پولش ها در طول زمان و یا بیاید یه بار پولش رو امروز بهم بدین یه تخفیف ده درصدی تقریبا براتون لحاظ می کنم عددش رو کم می کنم خریدمون هم عمدتا برای منشیانه و دفتر پیش فاکتورم گفته تا حدودا بیست روزی نامه براتون اوکیه همین",
},
{
"name": "مدل 2 (Vosk)",
"text": "سلام شبت بخیر. میخواستیم که 25 تا کیسه عرض به خدمت شما که گچ و خاک از مستفدی بخریم و کنارشم یه 50 تا کیسه سیمان 50 کیلویی که اونا تقریباً در میاد پامون دونهای 25 میلیون، سیمانیا پامون در میاد حدوداً 40 میلیون اینا دونهای. بهمون گفته که میتونین سه قسم بدین کلی پولشا در طول زمان و یا بیاین یه باره پولشو امروز بم بدین یه تخفیف 10 درصدی تقریباً بره دون، لحاظ میکنم عددشو کم میکنم. خریدم اونم اومدن برای منشیانه و دفتر. پیشفاکتورم گفته تا حدود یه 20 روز اینا برادون اوکیه. همین.",
},
]
async def run():
print(f"Connecting to Bale and ERP API for Chat ID: {CHAT_ID}...")
bale = BaleClient(settings.BALE_API_BASE, settings.BOT_TOKEN)
api = ErpClient(settings.API_BASE_URL, settings.API_TIMEOUT_SECONDS, settings.SERVICE_TOKEN)
ctx = BotContext(bale, api)
user_info = AUTHORIZED_USERS.get(CHAT_ID, {})
user = LinkedUser(
chat_id=CHAT_ID,
user_id=user_info.get("user_id"),
display_name=user_info.get("display_name", "مدیر تدارکات"),
role=user_info.get("role", "procurement_manager"),
is_admin=user_info.get("is_admin", True),
)
print("Sending transcripts to NLU (LLM) for multi-model consensus and entity extraction...")
result = await understand(settings, TRANSCRIPTS)
print("NLU Result:")
print(json.dumps(result, ensure_ascii=False, indent=2))
consensus_text = result.get("consensus_text") or ""
patch = result.get("patch") or {}
intro_msg = (
f"🤖 **تحلیل هوشمند فایل صوتی (تجمیع خروجی چند مدل STT):**\n\n"
f"📝 **متن استخراج‌شده و تصحیح‌شده (Consensus):**\n«{consensus_text}»\n\n"
f"📋 پیش‌نویس استعلام خرید با مشخصات استخراج‌شده صادر گردید:"
)
await ctx.send(CHAT_ID, intro_msg)
# Create and render the draft quote
await quote.handle_entities(
ctx,
CHAT_ID,
user,
patch,
source="voice",
)
print("Draft quote sent successfully to Bale chat!")
if __name__ == "__main__":
asyncio.run(run())
+47 -11
View File
@@ -24,7 +24,7 @@ try:
except ImportError:
sys.modules.setdefault("asyncpg", types.ModuleType("asyncpg"))
CHAT = 100200300
CHAT = 1461166742
_passed = _failed = 0
@@ -297,8 +297,10 @@ def build_ctx():
return ctx, bale, api
def text_update(update_id, text, reply_to=None):
msg = {"message_id": update_id, "chat": {"id": CHAT}, "text": text, "from": {"id": CHAT}}
def text_update(update_id, text, reply_to=None, chat_id=None, from_user=None):
cid = chat_id or CHAT
fuser = from_user or {"id": cid, "first_name": "مدیر تدارکات", "username": "proc_manager"}
msg = {"message_id": update_id, "chat": {"id": cid}, "text": text, "from": fuser}
if reply_to:
msg["reply_to_message"] = {"message_id": reply_to}
return {"update_id": update_id, "message": msg}
@@ -318,12 +320,22 @@ async def main() -> int:
_install_fake_stores()
from app.drafts import build_quote_request, compute_quote_missing, merge_quote_patch, new_quote_payload
import app.handlers as H
from app.nlu import _extract_json, normalize_digits
from app.nlu import _extract_json, normalize_digits, understand
from app.config import settings
# ---- pure logic -------------------------------------------------------
check("digits: ۵۰۰→500", normalize_digits("۵۰۰ کیلو") == "500 کیلو")
check("json extract: fenced", (_extract_json('```json\n{"a": 1}\n```') or {}) == {"a": 1})
# ---- multi-STT understand pipeline support ---------------------------
multi_stt_samples = [
{"name": "whisper", "text": "25 کیسه گچ از مصطفوی"},
{"name": "vosk", "text": "بیست و پنج کیسه گچ از مصطفوی"},
]
stt_res = await understand(settings, multi_stt_samples)
check("multi-stt: pipeline accepted list input", isinstance(stt_res, dict) and "consensus_text" in stt_res)
check("multi-stt: consensus text generated", bool(stt_res.get("consensus_text")))
payload = new_quote_payload()
merge_quote_patch(payload, {
"items": [{"material_name": "میلگرد A3", "quantity": 500, "unit_price": 45000}],
@@ -341,15 +353,39 @@ async def main() -> int:
body, problems = build_quote_request(payload)
check("build: blocked without supplier", body is None)
# ---- pipeline: linking gate -------------------------------------------
# ---- pipeline: whitelist gate & unregistered blocking -----------------
ctx, bale, api = build_ctx()
await H.handle_update(ctx, text_update(1, "سلام"))
check("gate: unlinked asked username", "نام کاربری" in bale.last_text())
UNAUTH_CHAT = 999999999
from app.handlers import UNREGISTERED_FILE
if UNREGISTERED_FILE.exists():
try:
UNREGISTERED_FILE.unlink()
except Exception:
pass
await H.handle_update(ctx, text_update(2, "mohaghegh"))
await H.handle_update(ctx, text_update(3, "secret"))
check("link: login called", api.login_calls and api.login_calls[0]["username"] == "mohaghegh")
check("link: linked greeting", "اتصال انجام شد" in bale.last_text())
# 1. Unregistered user is blocked and recorded
await H.handle_update(
ctx,
text_update(
1,
"سلام من متقاضی جدید هستم",
chat_id=UNAUTH_CHAT,
from_user={"id": UNAUTH_CHAT, "first_name": "کاربر متفرقه", "username": "intruder"},
),
)
check("gate: unauthorized user blocked", "دسترسی شما هنوز تعریف نشده است" in bale.last_text())
import json
unreg_records = json.loads(UNREGISTERED_FILE.read_text(encoding="utf-8")) if UNREGISTERED_FILE.exists() else []
check(
"gate: unregistered logged to JSON file",
any(u.get("chat_id") == UNAUTH_CHAT and u.get("first_name") == "کاربر متفرقه" for u in unreg_records),
)
# 2. Whitelisted user (1461166742) bypasses Casdoor completely
await H.handle_update(ctx, text_update(2, "/whoami"))
check("gate: whitelisted user active without casdoor", "مدیر تدارکات" in bale.last_text())
check("gate: no casdoor login prompted", "نام کاربری Casdoor" not in bale.last_text())
# ---- voice → draft quote ----------------------------------------------
NLU_QUEUE.append({"intent": "register_quote", "confidence": 0.9, "patch": {