From 94cc921ccd8145d3dd435bb7d3823c68abeb9e0d Mon Sep 17 00:00:00 2001 From: Simbiot Harness Date: Tue, 29 Sep 2026 15:52:46 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Telegram-=D0=B8=D0=BD=D0=B1=D0=BE=D0=BA?= =?UTF-8?q?=D1=81=20=E2=80=94=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=B0=D0=BA=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - harness/tg_inbox.py: long-polling поллер (@juno42bot) + send_reply - события MessageReceived/ReplySent/MessageIgnored (агрегат inbox) - проекция inbox в read model (кап 100) - контекст минутного тика: входящие ждущие ответа + мандат триажа - cli.py tg-reply: Telegram-отправка + ReplySent закрывает входящее - systemd simbiot-harness-tg.service - E2E: «йоу» от Алекса → минутный тик ответил в TG + дайджест в сторе --- .gitignore | 1 + README.md | 8 ++ __pycache__/cli.cpython-312.pyc | Bin 5295 -> 6567 bytes cli.py | 9 ++ harness/__pycache__/es.cpython-312.pyc | Bin 14268 -> 15379 bytes harness/__pycache__/levels.cpython-312.pyc | Bin 3541 -> 3863 bytes harness/__pycache__/scheduler.cpython-312.pyc | Bin 13420 -> 14502 bytes harness/es.py | 20 +++ harness/levels.py | 9 +- harness/scheduler.py | 11 ++ harness/tg_inbox.py | 120 ++++++++++++++++++ 11 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 harness/tg_inbox.py diff --git a/.gitignore b/.gitignore index b843b40..b79f7f8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ venv/ .agent/ __pycache__/ /tmp/ +.env diff --git a/README.md b/README.md index 831e973..3b3a308 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Strands Harness SDK как исполнительное ядро, модели ./venv/bin/python cli.py events -n 50 ./venv/bin/python cli.py requeue -r "причина" ./venv/bin/python cli.py promote -r "вне класса уровня" # S→M→L +./venv/bin/python cli.py tg-reply "текст" # ответ на входящее TG ``` ## Запуск @@ -52,6 +53,13 @@ systemctl enable --now simbiot-harness journalctl -u simbiot-harness -f ``` +## Telegram-инбокс + +Бот @juno42bot → long polling (`simbiot-harness-tg.service`) → +`MessageReceived` в стор → минутный уровень видит входящие в контексте → +отвечает `cli.py tg-reply` (Telegram-отправка + `ReplySent` закрывает входящее). +Белый список чатов в `.env` (`SIMBIOT_TG_ALLOWED`), чужие → `MessageIgnored`. + ## Модели 9router (router.cil.by), отдельный ключ `sk-simbiot` в `.env`, комбо `auto`: diff --git a/__pycache__/cli.cpython-312.pyc b/__pycache__/cli.cpython-312.pyc index 4c1d8881251781587beb1e7d94921eb64b31217f..0c609660f83fd73c5d8f9de9f22edc246d7efeb9 100644 GIT binary patch delta 1978 zcmZ{lO>7fK6vubG8++HW*ES)E^S$|~6F0c!8xn${kc5t!xB1ECE4RaOtJ0dZDLEsE5w1V{8>_$uslb zy!U^<_jYApc3)g4{%p0H8S)u?dNT}cSHwkNxM!Zf2P_8U;wspxS%CVuI#V^Qf%O>? zo#5&n4X`mI%o=DG!;BggBZ?bJ9Zm4HycaN8FY6-tC%HPd8MYwIHllAhE7I5sM=M;F zj~+ECCaO{8N$MTZQ8Axq;3lP*teYRajnv%nik3h#!lnS|KvR5WHQWMsW;%24)=TXJH#)($0WXdHu#-9L zCLP-IRTMkysvsEpleWWddWO5sbFhcpH0##e#M(_Pvu-i4w~|C(Fy$+qY2|V+8fZn` zLYrrMuCe@1THKMhawp>LL>Zxe%%M2op5+32E1A(11&EF;BJ}AX4!BQ`>x1u55Q64_ z`*lzM^8J8r9nh^61XUp$~LkMbIB9snlU~L-JiG83E#lp@2dE;@u%oVd-HmWZPGX4JJ*g^g~xqMmSZQo z9&^GYu1w|1riQVr3v*=;xbD2~_yewX{&)Kn7<=!&Q2T^8PnsvpXDm;g(j%uwb$Twr z%aNJLLg($?8@)@;o(bzXJO1ty-iqz@(~Z-m*yUXqUI^a>Sls=P?|m#-C!G_{sYCaL zx))kc`KHHW*(0%D73-&)F1CK#dMSB%WM*VZY#V2Fsix`vX>%8LcVKbrL*6?-w896B z8%V42PIRYa<2Jd6p6Oqg%+citSv217j|HMZzki021Jvs$%X7XXIs{XZpjVC(lRo@J j1a+)DP}c)M7(6>@U^vTo`aWCnE61Pie#SCfrJnsSF(@PJ delta 1115 zcma)5Pe>F|7=LeOc4u~Wc1s7h{gc|dX0EGdyXLBEYH4O2f-)h3ShTm$j(?fm3S}0% z1Ra8i?PQ96Rm?=CoFyHt6zW=}XW}c-sGLb)h4-a@8#A*)*E30CW!vfRFos~DqV#EOs( z+q;Bl57nLN;FSg|ZflKKj@ihvXoc>MCWMm5(X&$%RuMU=d8q7KbNw@+h0lx}xFMa; zZ1uHu`P4d7MJ3TOBMFMwa8wgXs@!cQSQ;af$_{@k#4GJ~E6bvTI$_5X84?@~t4p6= zWB*=P@mp24CXF+#2&v<+=ZeFWn1pYF41TweBq`EUPFuJ4AC02$RqVq_crAoiM$yeU zLM5-gljbG8Av)GPOfTs(Y|_8VkqnJAnrMKcGZcv?LS!&Zv)6cg^rHl4p2mSZVkVD! z#Twcee!7~|hG9NRw`JLy9$Sx%EQ@-3!7oKuBuyei`)dq`QOV~ZB2|fyv*x-IIcMgL zkWph@NgU}L+U%agIh5X!fexbN5Pvk2b(i>d4eh4s* zz>mW>Jl~DaJV({xFR>feK`<}(oCIQT$t5yQD<^@ zbxaDzy~A6%-g}K25`7DsEbPj^Dp%_s}yV9$uo-=0$EbA^Ix{JJV< z2%}IJvf25mxm-5;9I2CrcY=!F!ob`Dxigy^RHrF47A?#`Dm2-U#tPmmV#G-gmp`$g PFP!jTY>!1;*ew17YAMIc diff --git a/cli.py b/cli.py index 2cfc645..95666de 100644 --- a/cli.py +++ b/cli.py @@ -31,6 +31,10 @@ def main(): p_pr.add_argument("task_id") p_pr.add_argument("-r", "--reason", default="manual promote") + p_tgr = sub.add_parser("tg-reply", help="ответить на входящее TG-сообщение") + p_tgr.add_argument("msg_id", help="id входящего (m-NNN)") + p_tgr.add_argument("text") + sub.add_parser("status", help="состояние харнеса") sub.add_parser("tasks", help="доска задач") @@ -61,6 +65,11 @@ def main(): es.init_db() tid, new_size = commands.promote_task(args.task_id, args.reason) print(f"OK: задача {tid} повышена до [{new_size}]") + elif args.cmd == "tg-reply": + es.init_db() + from harness import tg_inbox + mid = tg_inbox.send_reply(args.msg_id, args.text) + print(f"OK: ответ на {mid} отправлен") elif args.cmd == "status": es.init_db() print(json.dumps(commands.status(), ensure_ascii=False, indent=1)) diff --git a/harness/__pycache__/es.cpython-312.pyc b/harness/__pycache__/es.cpython-312.pyc index a39efd1e9d43442887948957af7674b7157e1caf..804bae845739ab08292020caa35f5577eb1ae12d 100644 GIT binary patch delta 1297 zcma)6T})F~96$Ho-hSQ|DPn=Pv=nsI4g{veIWwuEqI@{PZAKp$aPHbGNa3^+Sn6Y(XtA9#wqQRaN9fGr?l?2|L87(ZoSrhdb0rs=#s@_?+2$=ENb z1|p9PDKb_KN@zGjWUMi$qB7hc8GnTlJ?3rmkO;7j01yUI7_^rZgCAo+7roxe7mz4r za1G7#^NkDQqG`djRI|+I54K-Jq=aSs9J#_aWoBbzLp61?gC~0A;TXBWf~pyWO!UU& z@tA6RtMk-bN#8<&p2&UC$gp1^;F9kE;I@7<*@KW7sp3$i4@b!;{iEtb&Pt6w&sjK5 z)eXw>2x$P5ABd9c(3k*^yBxK$-TUs+Zly}SeIuYo9s_+dtOztlU1ECJ_ePj}X|iSP z)j3mjffwq{t2TQuG~756mk~lT1k0w_r1WKz}>oRUHhLU9n0sIKh2c%BwE+pRjHc07ZS&( z-<#?xh)8l3CiTc%G#$v9+^bUYisZ^luH@&b{=DR0k?OKi-J^lL)Rf@F5 zZg#9z`tF7Agi~XWNWQX(#fGNN0GYJ_Tp7DBXY&0|vZ;=}FYBA;@nU!(yfmKw_>;`} z3z>^wX3vMRzA(tV*_HetCwU4MWUfvtdmKFXJ^iM(MNF{~uBCsg?dHV?%zTx02hNEt z5G5xWsFq&#gmLfX2$qLbL1BX*g`5QN5kMILJPZP#3h4xRK%X^yCO&26KWVAaqtnkq z^eJsT;MdPF)6UT^4*blG(z53L9xZ2w0{HhJ_W?AjePBJNgU#+zjXwoGh$O&F=ID>j zZu4_y!kZs`Lw{|4(bUax3YXLskY=Q6UT+%fL%f;Wv0fo^`t==^ob&ZjZYNizz5o9L DgePMW delta 457 zcmbPSu_vGJG%qg~0}xc*-ko(^cO##bIFkkYW-oCIc9u#8P5aGd3am_0nk;@r3P43g zIv^q#NN6(NVopyj(VRS0X%=JoWCP_o0ZpzVHK5Eb)|~vrl;WcN$)8j#8M!CxsLHz+ zfy7y|Q&S6y3_wh#@6AfuQYNGKpckswHxCqA*DAg3}uwJbHSq^J)l)CN-flX>$T z)o;w~AJ`eV6pDl=KU5akd_jwmiLq(&B^?LWNkBCxHjC>9F|keqGOupV(+^>F0Erg4 z1BqMQ@kObLDe<}aDXBTPm@|PU=Y!;UK!i1jkN^=N+lp*JgfEDgHkrvZjCBE!d3STH zsXPnYY>?=J$xW7;Y>R-*e+-j1T5e~|pPXrBA`g}m0x6IL5mP|~*eHDvYtH07RJf=7PUU3W5=j9gXIGQkXoSf8AN?@Vd7#G_$0;3 V$o5GYye#g)7)Y5&y7{6CC3ZMQ_`rO-^26 zZ9$1+ssh)f3AXvR>*p*_bHX;<7XW4=KNl4=2j5Opt zHQ9u=bBC@yJ2>Yom(U#J$OyA&DVxqClIf@$u9yp-7l)5%V(FZ%j>&1Gr?5FZD#)Ac z)0`}rkdAUxT;X%)hXe(BqHJ9-Zq$hQ%RbK+>D4r8>m0Gmtv}C4(i{?k1z7>J`(B)-n}i%hNrte#zKK)@b*ig8jd~yFhF$9 delta 291 zcmXYru}Z^G6o#7!*IG<0Iw&adK0pz~(bd7FFVI;i^a;9|w1YwdMWVPl`2aVj(Y9%? z?t=Vh@CAH=3NCK`QO|IOkN^A6xeeZT{a4Rx)HqjfWp8IA^Nn)Pob*L?f|f=mu|zVE zP+}AsRt9)LK@hjnq<(yUpmF!lE?_X#m`MmjG=rtTpd~}(D7iE45vD90(YuidB_%=!AT vD#ar2u8mim(|phCJ56`9?JiYqYx~PvU00vY^xkjR5BD2xsFo+`w;%if`dy;e diff --git a/harness/__pycache__/scheduler.cpython-312.pyc b/harness/__pycache__/scheduler.cpython-312.pyc index 69bca44a2cbcb88de12bbd01ac9bc325d04e1120..e4508ee1f9199ccddc1451a0409930fd4a23ebd8 100644 GIT binary patch delta 1725 zcmYjQeN0nV6u9l}+p1YrS@Qy3`-&kfw6?@EXV3-aCj$3%1zX)jp0DUmPK;Q!% z2JCTKa~r@h*2}a5FVnmVfI&9Q&I=5=89~6lz?l~~KEti&G8|&U>=-jzs|15OQb&hA zYzKq-uufEq7At4~&Aqg~m>1;c8v!6wqcA^HBeM>WltDu#&KG2O(!3om=y>aMJok*( z6=wKBV_1)8^k%0aY&Z?XqOg&mEzF8s0oj3cR%`{Q!5MWXAA-?7CBU=#JXMJ(bntka zn#Y_mUjxNbs+b?z5E&ZxbZ9JTlnZlWS-N0(F9^WVoG?cQp(ledk(dc>4V%bTb~=Ow zT?y5R6-4<0Ri@kG#LA~_RnfLY2Z=6v8Z~D2AnoR4lk%mcync{N_qzDGUY9wl!_0F< znfaomob!z8**V1}<*dN-aJyl^qI5$nWWrQrb2j3Itoyb{1txIFv;s&4TI7dkd5xhUA>|YDNAg+uUHP(nL5|4df_#p+XXUZI#2=T>5bm;2MTPPw@^pMS zJ}SiDA;uwjTK=?7SmNrH{JpMLpWoHfAKcgN7rdRmB|ZH@u-!@f?ibeh#C1X$kx$Fl z;%~->%N%vuIjF^NnJV!mlN;YNwGyw9U&$&0L6lNN&J^rITS<>8Rj1D{ z1yaf`dQ!!~ech;m&}cP*08c<5AO4ZI=kySVh5~hpm^ODg9I}>76r3uEu1XXvzFpv+ zL_g%$#`9|v`KuoTsM`jQyOX)*lZ|7I5yzce`{cIEJ1^{YL7o z3ZOU4rn3(<&ab~_u}Pz8M3v+$#{&`Oxb-1WXkLON8Q7PqN&x&4)Kf zY9hf%TlD40`pFej%#?d-*Ol6f!^0#z$cC`e3-u*}<6kcYlHy|3z3VH(j$z7s0{I+d}YJ_ls*j?LwmAZ|X=m`G0 z{52~rIG{xyzfeWf*CP5{=b!@(;2OIF_TgrGKS!l0O=F)|>IwR~{Yb>W+gHG0Y^$hL zO^{eLwyB~=seGT9c!RA3|Kt$hdHk=#rHv2@QJ9IXT=YN*kKiiTMeR{S3@}1D+KunJ zir@sEb8Uxz;#ZcQfD-;?sl_}_W$#hgNZ}*}jC0)8Fos`r7a1>6qMSndiB4hBU8=fH z{6y@CdmViJJPoDwr>Ui&proLpkRBraucp+UQm@n*K-rYmP`E~6ngXU^ra;GvY!t?E rX-%=2P9D*jAbR-p@z8a=qoxwx#NnC~Y&9&7Har4knca)6%eVgzWwq`E delta 880 zcmYk3Ur19?9LMju-aDt>ZEm(r=b!DWxLr^q+?o|dGR1l;l?rN%wL9D9Hs`rBYitPh zkeH?UGkO?C5lQ4jRrf+E;cB-Bd}MiAsv=Vu1qhtK(*-}m?X9_~3S6F*|c z_Xa~X$3E@Tt3;~fsqve{uT+$NOGkOVTGzmggb5Y|$8fzP(TQb>%)(~1eEYylhX zXCd?uLs?A5(@{+`F%92s?KLVB=xP?eWV3q-j5U0AnmNN6^5<)9M6)n!y+<0X65etG z($qd^ETtVEMG}O7tBb^8!u3X%Vr_I3dgW$229_l{nGPkBsZcf^87o(4Sqe!xU>ZfQ zG8@*$LUEM_;k7&>7FaD-vbS!Cx*(=ZFM5U@n3t=j;k3(x-G^z66}E^%(~$BgBnfvs zm(4g?t{S)$iDt6#@id(TS@Dr6IIXzFCFb2OT~#uIFwZ39WGC3VU8D&9?!DDFScxK@ zl``Ew1u_lidsf|7SRu!|+UPJ7ibyNjQa0hAWaxMz8o@&}x&&VD5cvYvyo;UZutV1u z7&(4wfa=+XwjyQ`*Aab)Im8SM_&U2DF;kF^gn$qcDUkh*&L=1zBW4g*#3Z5_F$-t?EmnLXikC<6 hmbUMKuE9;eo4kPM{zXY9E%*9=actQ<1|z*ge*sJO-%9`h diff --git a/harness/es.py b/harness/es.py index 00f6dec..d363571 100644 --- a/harness/es.py +++ b/harness/es.py @@ -194,6 +194,26 @@ def _apply_event(model: dict, ev: dict): spend.append(data) if len(spend) > 500: # кап: не растить модель линейно model["spend"] = spend[-500:] + elif etype == "MessageReceived": + model.setdefault("inbox", {})[aid] = { + **data, "replied": False, "ignored": False, + "ts": ev["timestamp"], + } + inbox = model["inbox"] + if len(inbox) > 100: # кап инбокса: последние 100 входящих + keep = sorted(inbox.items(), key=lambda kv: kv[1].get("ts", ""))[-100:] + model["inbox"] = dict(keep) + elif etype == "ReplySent": + m = model.setdefault("inbox", {}).get(aid) + if m: + m["replied"] = True + m["reply_text"] = data.get("text", "") + elif etype == "MessageIgnored": + m = model.setdefault("inbox", {}).get(aid) + if m: + m["ignored"] = True + else: + model["inbox"][aid] = {"ignored": True, "reason": data.get("reason"), "ts": ev["timestamp"]} # trace-события (InvocationStarted/ToolCalled/ToolResult) не материализуем — они для дебага diff --git a/harness/levels.py b/harness/levels.py index 67aed1c..c1089a8 100644 --- a/harness/levels.py +++ b/harness/levels.py @@ -43,9 +43,12 @@ LEVELS = { "min": CycleLevel( name="min", label="Минута · пульс", every=MIN, uses_llm=True, model="auto", effort="low", ctx_tokens=4_000, task_classes=("S",), - mandate=("Ты — минутный пульс харнеса Simbiot. Триаж входящих, быстрые ответы, " - "мелкие фиксы (S-задачи до 5 минут). Отвечай кратко, не углубляйся. " - "Если задача больше твоего класса — откажись и предложи промоушен."), + mandate=("Ты — минутный пульс харнеса Simbiot. Триаж входящих TG-сообщений: " + "если во ВХОДЯЩИХ есть сообщение без ответа — ответь на него командой " + "cli.py tg-reply \"текст\" (тон: кратко, по-русски, тёпло; можно " + "выполнить просьбу инструментами и ответить результатом). Быстрые " + "мелкие фиксы (S-задачи до 5 минут) тоже твои. Не растекайся. " + "Если просьба большая — подай её как задачу (cli.py task) и скажи это."), can_shell=True, ), "hour": CycleLevel( diff --git a/harness/scheduler.py b/harness/scheduler.py index b0f82f4..dd73f2d 100644 --- a/harness/scheduler.py +++ b/harness/scheduler.py @@ -89,6 +89,17 @@ def build_context(level_cfg, model: dict, task=None): for d in digests[:6]: parts.append(f"- [{d['level']}] {d['text'][:400]}") + # инбокс: входящие TG-сообщения, ждущие ответа (в приоритете) + inbox = [m for m in (model.get("inbox") or {}).values() + if not m.get("replied") and not m.get("ignored") and m.get("text")] + if inbox: + lines = [] + for m in inbox[:10]: + m_id = "m-" + str(m.get("update_id")) + who = m.get("username") or m.get("first_name") or m.get("user_id") + lines.append(f"- [{m_id}] от {who}: {m['text'][:300]}") + parts.append("ВХОДЯЩИЕ СООБЩЕНИЯ (ответь через: ./venv/bin/python cli.py tg-reply \"текст\"):\n" + "\n".join(lines)) + # доска задач tasks = [t for t in model["tasks"].values() if t["status"] in ("ready", "running")] if tasks: diff --git a/harness/tg_inbox.py b/harness/tg_inbox.py new file mode 100644 index 0000000..4f6c806 --- /dev/null +++ b/harness/tg_inbox.py @@ -0,0 +1,120 @@ +"""Simbiot Harness — Telegram-инбокс. + +Входящие сообщения Алекса → события MessageReceived в стор → +минутный уровень видит их в контексте и может ответить через +`cli.py tg-reply ` (событие ReplySent закрывает входящее). + +Поллер — отдельный процесс (systemd simbiot-harness-tg), long polling +getUpdates offset=last+1. Идемпотентность: update_id → aggregate_id. +""" +import json +import os +import time +import urllib.request +import urllib.parse + +from . import es + +_HARNESS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +_ENV_PATH = os.path.join(_HARNESS_DIR, ".env") + +TG_TOKEN = None +ALLOWED = set() + + +def _load_env(): + global TG_TOKEN, ALLOWED + if os.path.exists(_ENV_PATH): + with open(_ENV_PATH) as f: + for line in f: + line = line.strip() + if line.startswith("SIMBIOT_TG_TOKEN="): + TG_TOKEN = line.split("=", 1)[1].strip() + elif line.startswith("SIMBIOT_TG_ALLOWED="): + ALLOWED = {x.strip() for x in line.split("=", 1)[1].split(",") if x.strip()} + + +def _api(method: str, params: dict, timeout: int = 35): + url = f"https://api.telegram.org/bot{TG_TOKEN}/{method}" + data = urllib.parse.urlencode(params).encode() + req = urllib.request.Request(url, data=data) + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read().decode()) + + +def send_message(chat_id, text: str, reply_to=None): + """Отправка сообщения (используется tg-reply и алертами).""" + _load_env() + params = {"chat_id": chat_id, "text": text, "parse_mode": "HTML"} + if reply_to: + params["reply_to_message_id"] = reply_to + return _api("sendMessage", params) + + +def receive_message(update: dict): + """MessageReceived в стор. Идемпотентно по update_id.""" + msg = update.get("message") or update.get("edited_message") or {} + chat = msg.get("chat") or {} + user = msg.get("from") or {} + data = { + "update_id": update["update_id"], + "chat_id": chat.get("id"), + "user_id": user.get("id"), + "username": user.get("username"), + "first_name": user.get("first_name"), + "text": (msg.get("text") or "")[:4000], + "date": msg.get("date"), + } + es.append_event("inbox", f"m-{update['update_id']}", "MessageReceived", data) + return f"m-{update['update_id']}" + + +def send_reply(msg_id: str, text: str): + """Ответ на входящее: Telegram-отправка + событие ReplySent (закрывает входящее).""" + model = es.run_projections() + m = (model.get("inbox") or {}).get(msg_id) + if m is None: + raise ValueError(f"входящее {msg_id} не найдено") + if m.get("replied"): + raise ValueError(f"на {msg_id} уже отвечено") + r = send_message(m["chat_id"], text, reply_to=None) + es.append_event("inbox", msg_id, "ReplySent", { + "to_chat": m["chat_id"], "text": text[:2000], "tg_ok": bool(r.get("ok")), + }) + return msg_id + + +def poll_loop(): + """Long polling getUpdates → MessageReceived. Отдельный процесс.""" + _load_env() + if not TG_TOKEN: + raise SystemExit("SIMBIOT_TG_TOKEN не задан в .env") + es.init_db() + offset = 0 + print("[tg] инбокс-поллер запущен", flush=True) + while True: + try: + updates = _api("getUpdates", {"offset": offset, "timeout": 30, "limit": 16}) + for u in updates.get("result", []): + offset = max(offset, u["update_id"] + 1) + msg = u.get("message") or u.get("edited_message") + if not msg: + continue + user = (msg.get("from") or {}).get("id") + if ALLOWED and str(user) not in ALLOWED: + # чужой: пишем событие, но не отвечаем (потенциально спам) + es.append_event("inbox", f"m-{u['update_id']}", "MessageIgnored", { + "user_id": user, "username": (msg.get("from") or {}).get("username"), + "reason": "not_allowed", "text": (msg.get("text") or "")[:500], + }) + continue + mid = receive_message(u) + print(f"[tg] {mid} от {(msg.get('from') or {}).get('username') or user}: " + f"{(msg.get('text') or '')[:60]}", flush=True) + except Exception as e: + print(f"[tg] ошибка поллинга: {e}", flush=True) + time.sleep(5) + + +if __name__ == "__main__": + poll_loop() \ No newline at end of file