From 5c25eaad815f8bc4c69bf972f5350515af53bb4c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 7 Feb 2026 01:23:08 +0000 Subject: [PATCH 1/2] Added and updated repositories 2026-02-07 01:23:08 --- SenkoGuardian/SenModules/Gemini.py | 247 ++++++++++++++++++++++------- 1 file changed, 189 insertions(+), 58 deletions(-) diff --git a/SenkoGuardian/SenModules/Gemini.py b/SenkoGuardian/SenModules/Gemini.py index 2117455..971b02d 100644 --- a/SenkoGuardian/SenModules/Gemini.py +++ b/SenkoGuardian/SenModules/Gemini.py @@ -3,7 +3,7 @@ # This software is released under the MIT License. # https://opensource.org/licenses/MIT -__version__ = (5, 8, 1) #фыр +__version__ = (6, 0, 0) #фыр # meta developer: @SenkoGuardianModules @@ -22,12 +22,10 @@ import socket import base64 import uuid import json -from PIL import Image import asyncio import logging import tempfile import aiohttp -from datetime import datetime from markdown_it import MarkdownIt import pytz @@ -41,6 +39,8 @@ except ImportError: GOOGLE_AVAILABLE = False google_exceptions = None +from PIL import Image +from datetime import datetime from telethon import types as tg_types from telethon.tl.types import Message, DocumentAttributeFilename, DocumentAttributeSticker from telethon.utils import get_display_name, get_peer_id @@ -84,7 +84,15 @@ class Gemini(loader.Module): "cfg_google_search_doc": "Включить поиск Google (Grounding) для актуальной информации.", "cfg_image_model_doc": "Модель Gemini для генерации изображений (например: gemini-2.5-flash-image).", "cfg_inline_pagination_doc": "Использовать инлайн-кнопки для длинных ответов.", - "no_api_key": '❗️ Api ключ(и) не настроен(ы).\nПолучить Api ключ можно здесь.\nДобавьте ключ(и) в конфиге модуля: .cfg gemini api_key', + "no_api_key": ( + '❗️ Api ключ(и) не настроен(ы).\nПолучить Api ключ можно здесь.\n' + 'Добавьте ключ(и) в конфиге модуля: .cfg gemini api_key\n' + 'Так же можно использовать провайдера Openrouter .cfg gemini provider\n' + 'ℹ️ Получить Openrouter ключ можно здесь' + ), + "no_api_key_Openrouter": '❗️ API ключ для OpenRouter не настроен.\nПолучить ключ можно здесь.\nДобавьте ключ в конфиге модуля: .cfg gemini Openrouter_api_key', + "invalid_api_key_Openrouter": '❗️ Предоставленный API ключ OpenRouter недействителен.\nУбедитесь, что он правильно скопирован из OpenRouter.', + "gmodel_list_title_Openrouter": "📋 Доступные модели OpenRouter:", "invalid_api_key": '❗️ Предоставленный API ключ недействителен.\nУбедитесь, что он правильно скопирован из Google AI Studio и что для него включен Gemini API.', "all_keys_exhausted": "❗️ Все доступные API ключи ({}) исчерпали свою квоту.\nПопробуйте позже или добавьте новые ключи в конфиге: .cfg gemini api_key", "no_prompt_or_media": "⚠️ Нужен текст или ответ на медиа/файл.", @@ -159,6 +167,8 @@ class Gemini(loader.Module): def __init__(self): self.config = loader.ModuleConfig( loader.ConfigValue("api_key", "", self.strings["cfg_api_key_doc"], validator=loader.validators.Hidden()), + loader.ConfigValue("Openrouter_api_key", "", "API Key от OpenRouter (получить тут).", validator=loader.validators.Hidden()), + loader.ConfigValue("provider", "google", "Провайдер API: 'google' или 'openrouter'.", validator=loader.validators.Choice(["google", "openrouter"])), loader.ConfigValue("model_name", "gemini-2.5-flash", self.strings["cfg_model_name_doc"]), loader.ConfigValue("interactive_buttons", True, self.strings["cfg_buttons_doc"], validator=loader.validators.Boolean()), loader.ConfigValue("system_instruction", "", self.strings["cfg_system_instruction_doc"], validator=loader.validators.String()), @@ -315,7 +325,7 @@ class Gemini(loader.Module): final_parts.insert(0, types.Part(text=full_prompt_text)) return final_parts, warnings - async def _send_to_gemini(self, message, parts: list, regeneration: bool=False, call: InlineCall=None, status_msg=None, chat_id_override: int=None, impersonation_mode: bool=False, use_url_context: bool=False, display_prompt: str=None): + async def _send_to_gemini(self, message, parts: list, regeneration: bool=False, call: InlineCall=None, status_msg=None, chat_id_override: int=None, impersonation_mode: bool=False, use_url_context: bool=False, display_prompt: str=None): msg_obj = None if regeneration: chat_id = chat_id_override; base_message_id = message @@ -323,6 +333,56 @@ class Gemini(loader.Module): except Exception: msg_obj = None else: chat_id = utils.get_chat_id(message); base_message_id = message.id; msg_obj = message + if self.config["provider"] == "openrouter": + if regeneration: + current_turn_parts, request_text_for_display = self.last_requests.get(f"{chat_id}:{base_message_id}", (parts, "[регенерация]")) + else: + current_turn_parts = parts + user_text_from_parts = " ".join([p.text for p in parts if hasattr(p, "text") and p.text]) + request_text_for_display = display_prompt or user_text_from_parts or "[медиа-запрос]" + self.last_requests[f"{chat_id}:{base_message_id}"] = (current_turn_parts, request_text_for_display) + + try: + sys_instruct = self.config["system_instruction"] or None + if impersonation_mode: + my_name = get_display_name(self.me) + chat_history_text = await self._get_recent_chat_text(chat_id) + sys_instruct = self.config["impersonation_prompt"].format(my_name=my_name, chat_history=chat_history_text) + raw_hist = self._get_structured_history(chat_id, gauto=impersonation_mode) + if regeneration and raw_hist: raw_hist = raw_hist[:-2] + openai_messages = self._convert_google_history_to_openai(raw_hist, sys_instruct) + user_text_prompt = " ".join([p.text for p in current_turn_parts if hasattr(p, "text") and p.text]) + if not user_text_prompt: user_text_prompt = request_text_for_display + openai_messages.append({"role": "user", "content": user_text_prompt}) + target_model = self.config["model_name"] + result_text = await self._send_to_Openrouter_api(target_model, openai_messages, self.config["temperature"]) + if self._is_memory_enabled(str(chat_id)): + self._update_history(chat_id, current_turn_parts, result_text, regeneration, msg_obj, gauto=impersonation_mode) + if impersonation_mode: return result_text + hist_len = len(self._get_structured_history(chat_id)) // 2 + mem_ind = self.strings["memory_status"].format(hist_len, self.config["max_history_length"]) + model_info = f"OpenRouter: {target_model}" + response_html = self._markdown_to_html(result_text) + formatted_body = self._format_response_with_smart_separation(response_html) + question_html = f"
{utils.escape_html(request_text_for_display[:200])}
" + text_to_send = f"{mem_ind}\n{model_info}\n\n{self.strings['question_prefix']}\n{question_html}\n\n{self.strings['response_prefix']}\n{formatted_body}" + buttons = self._get_inline_buttons(chat_id, base_message_id) if self.config["interactive_buttons"] else None + if len(text_to_send) > 4096: + file = io.BytesIO(result_text.encode("utf-8")); file.name = "Gemini_response.txt" + if call: await self.client.send_file(call.chat_id, file, caption="Response too long", reply_to=call.message_id) + elif status_msg: + await status_msg.delete() + await self.client.send_file(chat_id, file, caption="Response too long", reply_to=base_message_id) + else: + if call: await call.edit(text_to_send, reply_markup=buttons) + elif status_msg: await utils.answer(status_msg, text_to_send, reply_markup=buttons) + return "" + except Exception as e: + error_text = self._handle_error(e) + if impersonation_mode: logger.error(f"Gauto/Openrouter error: {error_text}") + elif call: await call.edit(error_text) + elif status_msg: await utils.answer(status_msg, error_text) + return None api_key_str = self.config["api_key"] self.api_keys = [k.strip() for k in api_key_str.split(",") if k.strip()] if api_key_str else [] if not self.api_keys: @@ -814,25 +874,66 @@ class Gemini(loader.Module): @loader.command() async def gmodel(self, message: Message): - """[model или пусто] — Узнать/сменить модель. -s — список доступных моделей в файле.""" - args = utils.get_args_raw(message).strip().lower() - if '-s' in args: - if not self.api_keys: return await utils.answer(message, self.strings['no_api_key']) - sts = await utils.answer(message, self.strings["processing"]) + """[model] [-s] — Узнать/сменить модель. -s — список.""" + args_raw = utils.get_args_raw(message).strip() + args_list = args_raw.split() + is_list_request = "-s" in [arg.lower() for arg in args_list] + provider = self.config["provider"] + if is_list_request: + status_msg = await utils.answer(message, self.strings["processing"]) try: - client = genai.Client(api_key=self.api_keys[0]) - models = await asyncio.to_thread(client.models.list) - txt = "\n".join([f"• {m.name.split('/')[-1]} ({m.display_name})" for m in models]) - f = io.BytesIO((self.strings["gmodel_list_title"] + "\n" + txt).encode('utf-8')) - f.name = "models_list.txt" - await self.client.send_file(message.chat_id, file=f, caption="📋 Список доступных моделей", reply_to=message.id) - await sts.delete() - except Exception as e: await utils.answer(sts, self.strings["gmodel_list_error"].format(self._handle_error(e))) + if provider == "openrouter": + api_key = self.config["Openrouter_api_key"] + if not api_key: return await utils.answer(status_msg, self.strings['no_api_key_Openrouter']) + async with aiohttp.ClientSession() as session: + async with session.get( + "https://openrouter.ai/api/v1/models", + headers={"Authorization": f"Bearer {api_key}"} + ) as resp: + if resp.status != 200: raise ValueError(f"HTTP {resp.status}") + data = await resp.json() + models_data = data.get("data", []) + models_data.sort(key=lambda x: x["id"]) + top_list = [] + other_list = [] + favs = ["google/gemini-2.0-flash-001", "openai/gpt-4o", "anthropic/claude-3.5-sonnet", "deepseek/deepseek-r1"] + for m in models_data: + mid = m["id"] + line = f"• {mid}" + if mid in favs: top_list.append(line) + elif any(x in mid for x in ["gemini", "gpt", "claude", "deepseek"]): other_list.append(line) + text = self.strings.get("gmodel_list_title_Openrouter", "📋 Models:") + "\n" + "\n".join(top_list) + "\n\n" + "\n".join(other_list[:50]) + file = io.BytesIO(text.encode("utf-8")); file.name = "openrouter_models.txt" + await self.client.send_file(message.chat_id, file=file, caption="📋 OpenRouter Models", reply_to=message.id) + await status_msg.delete() + else: + if not self.api_keys: return await utils.answer(status_msg, self.strings['no_api_key']) + client = genai.Client(api_key=self.api_keys[0]) + models = await asyncio.to_thread(client.models.list) + txt = "\n".join([f"• {m.name.split('/')[-1]} ({m.display_name})" for m in models]) + f = io.BytesIO((self.strings["gmodel_list_title"] + "\n" + txt).encode('utf-8')) + f.name = "models_list.txt" + await self.client.send_file(message.chat_id, file=f, caption="📋 Список доступных моделей", reply_to=message.id) + await status_msg.delete() + except Exception as e: + await utils.answer(status_msg, self.strings["gmodel_list_error"].format(self._handle_error(e))) return - - if not args: return await utils.answer(message, f"Текущая модель: {self.config['model_name']}") - self.config["model_name"] = args - await utils.answer(message, f"Модель Gemini установлена: {args}") + if not args_raw: + return await utils.answer(message, f"🔮 Провайдер: {provider}\n🧠 Модель: {self.config['model_name']}") + self.config["model_name"] = args_raw + warning = "" + if provider == "google" and ("/" in args_raw or any(x in args_raw.lower() for x in ["gpt", "claude", "deepseek", "llama"])): + warning = ( + "\n\n⚠️ Конфликт настроек!\n" + f"Вы установили модель {args_raw}, но провайдер остался Google.\n" + "Смените провайдера командой:\n.cfg gemini provider openrouter" + ) + elif provider == "openrouter" and "/" not in args_raw and "gemini" in args_raw.lower(): + warning = ( + "\n\n⚠️ Совет: Для OpenRouter лучше использовать полные ID.\n" + f"Например: google/{args_raw}" + ) + await utils.answer(message, f"✅ Модель установлена: {args_raw}{warning}") @loader.command() async def gres(self, message: Message): @@ -1107,16 +1208,26 @@ class Gemini(loader.Module): msgs = await self.client.get_messages(cid, limit=lim) if skip_last and msgs: msgs = msgs[1:] for m in msgs: - if not m or (not m.text and not m.media): continue + if not m: continue + if not (m.text or m.sticker or m.photo or m.file or m.media): + continue name = get_display_name(await m.get_sender()) or "Unknown" - txt = m.text or ("[Media]" if m.media else "") + txt = m.text or "" if m.sticker: - alt = next((a.alt for a in m.sticker.attributes if isinstance(a, DocumentAttributeSticker)), "?") + alt = "?" + if hasattr(m.sticker, 'attributes'): + alt = next((a.alt for a in m.sticker.attributes if isinstance(a, DocumentAttributeSticker)), "?") txt += f" [Стикер: {alt}]" - elif m.photo: txt += " [Фото]" - elif m.document and not hasattr(m.media, "webpage"): txt += " [Файл]" - if txt.strip(): lines.append(f"{name}: {txt.strip()}") - except: pass + elif m.photo: + txt += " [Фото]" + elif m.file: + txt += " [Файл]" + elif m.media and not txt: + txt += " [Медиа]" + if txt.strip(): + lines.append(f"{name}: {txt.strip()}") + except Exception as e: + pass return "\n".join(reversed(lines)) def _handle_error(self, e: Exception) -> str: @@ -1202,35 +1313,6 @@ class Gemini(loader.Module): self._clear_history(chat_id, gauto=False) await call.edit(self.strings["memory_cleared"], reply_markup=None) - async def _get_recent_chat_text(self, chat_id: int, count: int = None, skip_last: bool = False) -> str: - history_limit = count or self.config["impersonation_history_limit"] - fetch_limit = history_limit + 1 if skip_last else history_limit - chat_history_lines = [] - try: - messages = await self.client.get_messages(chat_id, limit=fetch_limit) - if skip_last and messages: - messages = messages[1:] - for msg in messages: - if not msg: continue - if not msg.text and not msg.sticker and not msg.photo and not (msg.media and not hasattr(msg.media, "webpage")): - continue - sender = await msg.get_sender() - sender_name = get_display_name(sender) if sender else "Unknown" - text_content = msg.text or "" - if msg.sticker and hasattr(msg.sticker, 'attributes'): - alt_text = next((attr.alt for attr in msg.sticker.attributes if isinstance(attr, DocumentAttributeSticker)), None) - text_content += f" [Стикер: {alt_text or '?'}]" - elif msg.photo: - text_content += " [Фото]" - elif msg.document and not hasattr(msg.media, "webpage"): - text_content += " [Файл]" - - if text_content.strip(): - chat_history_lines.append(f"{sender_name}: {text_content.strip()}") - except Exception as e: - logger.warning(f"Не удалось получить историю для авто-ответа: {e}") - return "\n".join(reversed(chat_history_lines)) - async def _scan_keys(self, force=False): """ Сканирует ключи на валидность. @@ -1336,6 +1418,55 @@ class Gemini(loader.Module): return out.getvalue() except: return img_bytes + async def _send_to_Openrouter_api(self, model, messages, temperature): + """Отправка запроса в OpenRouter (OpenAI format)""" + api_key = self.config["Openrouter_api_key"] + if not api_key: + raise ValueError("Не указан OpenRouter API Key! Установите его в .cfg") + url = "https://openrouter.ai/api/v1/chat/completions" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "HTTP-Referer": "https://github.com/SenkoGuardian", + "X-Title": "Gemini Module for Heroku Telegram-userbot", + } + payload = { + "model": model, + "messages": messages, + "temperature": min(temperature, 1.0) + } + async with aiohttp.ClientSession() as session: + async with session.post(url, headers=headers, json=payload, timeout=GEMINI_TIMEOUT) as resp: + text = await resp.text() + + if resp.status != 200: + try: + err_json = json.loads(text) + err_msg = err_json.get('error', {}).get('message', text) + except: + err_msg = text + raise ConnectionError(f"OpenRouter API Error {resp.status}: {err_msg}") + try: + result = json.loads(text) + except json.JSONDecodeError: + raise ValueError(f"OpenRouter вернул не JSON: {text[:100]}...") + if "choices" not in result or not result["choices"]: + if "error" in result: + raise ValueError(f"OpenRouter Logic Error: {result['error']}") + raise ValueError(f"Пустой ответ (нет 'choices'). Raw: {text}") + return result["choices"][0]["message"]["content"] + + def _convert_google_history_to_openai(self, history: list, system_prompt: str) -> list: + """Конвертирует историю из формата Google в формат OpenAI.""" + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + for item in history: + role = "assistant" if item['role'] == "model" else "user" + content = item.get("content", "") + messages.append({"role": role, "content": content}) + return messages + def _is_memory_enabled(self, chat_id: str) -> bool: return chat_id not in self.memory_disabled_chats def _disable_memory(self, chat_id: int): self.memory_disabled_chats.add(str(chat_id)) def _enable_memory(self, chat_id: int): self.memory_disabled_chats.discard(str(chat_id)) From 52f6fc53c35da0cd0956b0ce60f752ff4af72bf7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 7 Feb 2026 01:23:38 +0000 Subject: [PATCH 2/2] Updated modules.json after parse 2026-02-07 01:23:38 --- modules.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules.json b/modules.json index 4cc5b94..d543137 100644 --- a/modules.json +++ b/modules.json @@ -131,6 +131,7 @@ "nonlongermaintained": "No Longer Maintained Repository", "newbie": "Newbie" }, + "indexing_in_progress": "⚠️ Database is busy, try again later. If issue persists, try removing limoka_index in the userbot's root folder. If error persists again, report to developers", "name_ru": "Limoka", "wait_ru": "Подождите\n🔍 Идёт поиск среди {count} модулей по запросу: {query}\n{fact}", "found_header_ru": "🔍 Найден модуль {name} по запросу: {query}\n\nℹ️ Описание: {description}\n🧑‍💻 Разработчик: {username}\n\n🏷 Теги: {tags}\n\n", @@ -188,7 +189,8 @@ "watcher_signature_invalid_ru": "❌ Неверная подпись! Установка отменена.", "watcher_loader_missing_ru": "❌ Модуль загрузчика не найден.", "watcher_module_not_found_ru": "❌ Модуль не найден в базе Limoka: {path}", - "watcher_critical_ru": "❌ Критическая ошибка: {error}" + "watcher_critical_ru": "❌ Критическая ошибка: {error}", + "indexing_in_progress_ru": "⚠️ База данных занята, попробуйте снова через несколько секунд. Если ошибка сохраняется, попробуйте удалить limoka_index в корневой папке юзербота. Если ошибка сохраняется снова, сообщите разработчикам" }, "has_on_load": false, "has_on_unload": false, @@ -38599,7 +38601,7 @@ "sgamerps": "- Start the game «Rock, Paper, Scissors» | (RU) - Начать игру «Камень, ножницы, бумага» | (UZ) - «Tosh, qog'oz, qaychi» o'yinining boshlanishi | (DE) - Beginn des Spiels «Stein, Papier, Schere» | (ES) - El comienzo del juego «Piedra, papel o tijera»" }, { - "cleargames": "- Complete all running games. | (RU) | (UZ) - Barcha faol o'yinlarni tugatish. | (DE) - Alle aktiven Spiele beenden. | (ES) - Completar todos los juegos en curso." + "cleargames": "- Complete all running games. | (RU) | (UZ) - Barcha faol o'yinlarni tugatish. | (DE) - Alle aktiven Spiele beenden. | (ES) - Completar todos los juegos en curso." } ], "new_commands": [ @@ -38625,7 +38627,7 @@ "original_name": "cleargames", "description": { "default": "- Complete all running games.", - "ru": "", + "ru": "", "uz": "- Barcha faol o'yinlarni tugatish.", "de": "- Alle aktiven Spiele beenden.", "es": "- Completar todos los juegos en curso." @@ -81807,6 +81809,6 @@ }, "meta": { "total_modules": 1017, - "generated_at": "2026-02-06T01:24:40.647732" + "generated_at": "2026-02-07T01:23:38.497674" } } \ No newline at end of file