uni-notify: Outlook + Teams → Telegram с LLM-фильтром

- Playwright держит открытыми веб-версии Outlook и Teams (постоянный профиль, Xvfb)
- watcher.js перехватывает уведомления клиента и изменения карточек в реальном времени
- pipeline: дедуп, baseline, VIP/MUTE, оценка важности через Haiku (OpenRouter)
- aiogram-бот: уведомления, дайджест, вопросы по журналу, /login с 2FA-кодом, ручное управление браузером
- Docker/compose, systemd-юнит, e2e-тест на поддельных страницах

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194buWJjpDjU52JPX7PjsMs
This commit is contained in:
Claude
2026-11-13 14:20:14 +01:00
commit da28d0ac1c
22 changed files with 1844 additions and 0 deletions

73
app/main.py Normal file
View File

@@ -0,0 +1,73 @@
"""Точка входа: python -m app.main"""
from __future__ import annotations
import asyncio
import logging
import signal
from . import config
from .bot import TgBot
from .browser.session import BrowserManager
from .db import DB
from .llm import LLM
from .pipeline import Pipeline
log = logging.getLogger("uni-notify")
async def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
logging.getLogger("aiogram.event").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
s = config.load()
db = DB(s.db_path)
llm = LLM(s.openrouter_key, s.model, s.tz)
tg = TgBot(s, db, llm)
pipeline = Pipeline(s, db, llm, tg.say)
browser = BrowserManager(s, pipeline.submit, tg.say)
tg.attach(browser, pipeline)
await browser.start()
await tg.say("🚀 uni-notify запущен. Проверяю сессии…", silent=True)
async def first_check():
await asyncio.sleep(25)
st = browser.states()
if all(v == "ok" for v in st.values()):
await tg.say("✅ Почта и Teams открыты, слежу.", silent=True)
else:
await tg.say(f"Состояние: {st}. Если нужен вход — /login")
stop = asyncio.Event()
loop = asyncio.get_running_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
try:
loop.add_signal_handler(sig, stop.set)
except NotImplementedError:
pass
tasks = [
asyncio.create_task(tg.run(), name="telegram"),
asyncio.create_task(pipeline.run(), name="pipeline"),
asyncio.create_task(pipeline.digest_loop(), name="digest"),
asyncio.create_task(browser.health_loop(), name="health"),
asyncio.create_task(browser.scan_loop(), name="scan"),
asyncio.create_task(first_check(), name="first_check"),
]
waiter = asyncio.create_task(stop.wait())
done, _ = await asyncio.wait([waiter, *tasks[:2]], return_when=asyncio.FIRST_COMPLETED)
for t in done:
if t is not waiter and t.exception():
log.error("Задача %s упала: %r", t.get_name(), t.exception())
log.info("Останавливаюсь…")
for t in tasks:
t.cancel()
await browser.close()
await llm.aclose()
await tg.bot.session.close()
if __name__ == "__main__":
asyncio.run(main())