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 @@ + + + + + + + + + tgclient + + +
+ + + \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..7db1b95 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "tgclient-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "gnexus-ui-kit": "git+https://git.gnexus.space/git/root/gnexus-ui-kit.git#master", + "vue": "^3.5.0", + "vue-router": "^4.4.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.0", + "vite": "^5.4.0" + }, + "allowScripts": { + "esbuild@0.21.5": true + } +} \ No newline at end of file diff --git a/frontend/public/logo.svg b/frontend/public/logo.svg new file mode 100644 index 0000000..119a318 --- /dev/null +++ b/frontend/public/logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..b12a4e0 --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,32 @@ +import path from 'node:path' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + // exports-карта кита 1.0 пропускает только kit.css (и ./dist/assets/*); + // файлы цветовых тем (kit-verdant и т.п.) не перечислены — отдаём файл + // напрямую мимо exports через алиас (как в hard-panel) + resolve: { + alias: { + 'gnexus-ui-kit/dist/css/kit-verdant.css': path.resolve( + 'node_modules/gnexus-ui-kit/dist/css/kit-verdant.css', + ), + }, + }, + server: { + // в dev фронт на :5173, API и auth проксируются на локальный бэкенд + // (порт наружу tgclient — TGCLIENT_PORT, по умолчанию 8710) + proxy: { + '/api': 'http://127.0.0.1:8710', + '/auth': 'http://127.0.0.1:8710', + '/webhooks': 'http://127.0.0.1:8710', + }, + }, + build: { + outDir: 'dist', + // icons.css кита держит шрифт phosphor вшитым data-URI (~7 МБ) — + // не ругаемся на большие чанки + chunkSizeWarningLimit: 9000, + }, +}) \ No newline at end of file