diff --git a/backend/app/mcp/tools.py b/backend/app/mcp/tools.py index fa11d7d..884de37 100644 --- a/backend/app/mcp/tools.py +++ b/backend/app/mcp/tools.py @@ -27,7 +27,6 @@ from app.mcp.serializers import ( dialog_dict, entity_dict, - marked_peer_id, media_meta, message_dict, ) @@ -47,7 +46,6 @@ ) HISTORY_CAP = 200 -PARTICIPANTS_CAP = 200 # -------------------------------------------------------------------------- @@ -74,10 +72,6 @@ return {"error": 500, "detail": f"{type(exc).__name__}: {exc}"} -def _data_error(code: int, detail: str) -> dict: - return {"error": code, "detail": detail} - - async def _own_account_rows(db) -> list: """Строки accounts текущего mcp-юзера; админ — все (с пометкой владельца).""" if is_admin(): @@ -133,12 +127,16 @@ async def _client_for_dialog(db, account_id, dialog_id, *, writable: bool = False): - if dialog_id in (None, 0): - code = 422 + if dialog_id in (None, "", "0", 0): raise DomainError(422, "dialog_id обязателен (маркированный int из dialogs_list)") client, row = await _resolve_client(db, account_id, writable=writable) + if not isinstance(dialog_id, str): + dialog_id = str(dialog_id) try: - entity = await client.get_input_entity(dialog_id) + if dialog_id.lstrip("-").isdigit(): + entity = await client.get_input_entity(int(dialog_id)) + else: # username-ссылка — сетевой резолв, попадёт в кеш сессии + entity = await client.get_entity(dialog_id.removeprefix("@")) except Exception as exc: # noqa: BLE001 — не найден в кеше/у этого акка raise DomainError(404, f"dialog {dialog_id} недоступен на этом аккаунте" f" (id не найден в сессии — проверьте dialogs_list)") from exc @@ -239,9 +237,9 @@ @mcp.tool() - async def chat_info(account_id: int | None = None, dialog_id: int = 0) -> dict: + async def chat_info(account_id: int | None = None, dialog_id: str = "") -> dict: """ℹ️ Инфо о чате/канале/юзере: полное описание, счётчики участников. - dialog_id — маркированный int из dialogs_list или @username.""" + dialog_id — маркированный int из dialogs_list или username («@name»).""" return await _tool(lambda: _chat_info(account_id, dialog_id)) @@ -266,7 +264,7 @@ @mcp.tool() async def chat_participants(account_id: int | None = None, dialog_id: int = 0, - limit: int = 50) -> list: + limit: int = 50) -> dict: """👥 Участники чата/канала (limit ≤ 200): id, type, title/имя, username.""" return await _tool(lambda: _chat_participants(account_id, dialog_id, limit)) @@ -347,11 +345,20 @@ if dialog_id in (None, 0): if not query: raise DomainError(422, "нужен query или dialog_id") - # глобальный поиск: по всем диалогам (GetSearchRequest global=True) + if from_user: + raise DomainError( + 422, "from_user доступен только при заданном dialog_id (глобальный поиск Telegram не фильтрует по автору)" + ) + # глобальный поиск: по всем диалогам (SearchGlobal, InputPeerEmpty) client, _row = await _resolve_client(db, account_id) - from_user_id = await _resolve_from_user(client, from_user) if from_user else None + from telethon.tl.types import InputMessagesFilterEmpty, InputPeerEmpty + result = await client(functions.messages.SearchGlobalRequest( - q=query, from_id=from_user_id, limit=limit, + q=query, + filter=InputMessagesFilterEmpty(), + min_date=None, max_date=None, + offset_rate=0, offset_peer=InputPeerEmpty(), + offset_id=0, limit=limit, )) for message in getattr(result, "messages", []) or []: out.append(message_dict(message)) @@ -550,7 +557,7 @@ async def _upload_voice(account_id, dialog_id, data_base64, duration, waveform_samples) -> dict: db = get_db() client, row, entity = await _client_for_dialog(db, account_id, dialog_id, writable=True) - data = _decode_payload(data_base64, expect_mime="audio/ogg") + data = _decode_payload(data_base64) if duration <= 0: duration = ogg_opus_duration(data) or 0 if duration <= 0: @@ -585,7 +592,7 @@ async def _upload_round(account_id, dialog_id, data_base64, duration, size) -> dict: db = get_db() client, row, entity = await _client_for_dialog(db, account_id, dialog_id, writable=True) - data = _decode_payload(data_base64, expect_mime="video/mp4") + data = _decode_payload(data_base64) if duration <= 0: raise DomainError(422, "duration (сек) обязателен для кружка") if duration > 60: @@ -666,14 +673,14 @@ return {"status": "ok", "login_id": login_id, "step": "cancelled"} - # --- Памятка агентам (prompt agent_guide в server.py) -------------------- + # --- Памятка агентам — prompt agent_guide в server.py --------------------- _login_windows: dict[str, list[float]] = {} def _login_guard() -> dict | None: - """5 логинов в час на mcp-юзера (защита от лупящего агента).""" + """5 логинов в час на mcp-юзера (защита от зациклившегося агента).""" import time now = time.monotonic() @@ -693,7 +700,7 @@ return call_with_flood_guard(manager, account_id, coro_fn) -def _decode_payload(data_base64: str, *, expect_mime: str = "") -> bytes: +def _decode_payload(data_base64: str) -> bytes: """base64-payload тула → bytes; лимит TGCLIENT_MEDIA_MAX_BYTES → 413-данные.""" if not data_base64: raise DomainError(422, "data_base64 пустой") diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..79176a7 --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1 @@ +allow-git=git.gnexus.space \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..f176e89 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,15 @@ + + + +
+ + + + +