Added and updated repositories 2026-03-03 01:27:55

This commit is contained in:
github-actions[bot]
2026-03-03 01:27:55 +00:00
parent 3a1dbc93cd
commit 06973235e0
3 changed files with 221 additions and 47 deletions

View File

@@ -13,7 +13,7 @@ class DBMod(loader.Module):
"close_btn": "❌ Close", "close_btn": "❌ Close",
"back_btn": "⬅ Back", "back_btn": "⬅ Back",
"del_btn": "🗑 Delete", "del_btn": "🗑 Delete",
"del_all_btn": "💣 Delete all", "del_all_btn": " Delete all",
"not_found": "🔍 Key {key} not found", "not_found": "🔍 Key {key} not found",
"invalid_key": "⚠ Invalid key", "invalid_key": "⚠ Invalid key",
"page": "📄 Page {current}/{total}", "page": "📄 Page {current}/{total}",
@@ -34,7 +34,7 @@ class DBMod(loader.Module):
"close_btn": "❌ Закрыть", "close_btn": "❌ Закрыть",
"back_btn": "⬅ Назад", "back_btn": "⬅ Назад",
"del_btn": "🗑 Удалить", "del_btn": "🗑 Удалить",
"del_all_btn": "💣 Удалить все", "del_all_btn": " Удалить все",
"not_found": "🔍 Ключ {key} не найден", "not_found": "🔍 Ключ {key} не найден",
"invalid_key": "⚠ Некорректный ключ", "invalid_key": "⚠ Некорректный ключ",
"page": "📄 Страница {current}/{total}", "page": "📄 Страница {current}/{total}",
@@ -240,6 +240,7 @@ class DBMod(loader.Module):
{ {
"text": self.strings["del_all_btn"], "text": self.strings["del_all_btn"],
"callback": self.confirm_delete_all, "callback": self.confirm_delete_all,
"style": "danger",
"args": [key_path], "args": [key_path],
} }
] ]
@@ -290,6 +291,7 @@ class DBMod(loader.Module):
{ {
"text": self.strings["back_btn"], "text": self.strings["back_btn"],
"callback": self.navigate_db, "callback": self.navigate_db,
"style": "primary",
"args": [key_path[:-1], parent_page], "args": [key_path[:-1], parent_page],
} }
) )
@@ -329,6 +331,7 @@ class DBMod(loader.Module):
{ {
"text": self.strings["del_all_btn"], "text": self.strings["del_all_btn"],
"callback": self.confirm_delete_all, "callback": self.confirm_delete_all,
"style": "danger",
"args": [key_path], "args": [key_path],
} }
] ]
@@ -344,13 +347,16 @@ class DBMod(loader.Module):
{ {
"text": self.strings["del_btn"], "text": self.strings["del_btn"],
"callback": self.delete_key, "callback": self.delete_key,
"styles": "danger",
"args": [key_path], "args": [key_path],
} }
], ],
[ [
{ {
"text": self.strings["back_btn"], "text": self.strings["back_btn"],
"style": "primary",
"callback": self.navigate_db, "callback": self.navigate_db,
"style": "primary",
"args": [key_path[:-1], parent_page], "args": [key_path[:-1], parent_page],
} }
], ],
@@ -363,6 +369,7 @@ class DBMod(loader.Module):
{ {
"text": self.strings["del_btn"], "text": self.strings["del_btn"],
"callback": self.delete_key, "callback": self.delete_key,
"style": "danger",
"args": [key_path], "args": [key_path],
} }
], ],
@@ -370,6 +377,7 @@ class DBMod(loader.Module):
{ {
"text": self.strings["back_btn"], "text": self.strings["back_btn"],
"callback": self.navigate_db, "callback": self.navigate_db,
"style": "primary",
"args": [key_path[:-1], parent_page], "args": [key_path[:-1], parent_page],
} }
], ],

View File

@@ -11,26 +11,54 @@
# Description: Взаимодействие с Cocoon от HikkaHost # Description: Взаимодействие с Cocoon от HikkaHost
# meta developer: @FAmods & @vsecoder_m # meta developer: @FAmods & @vsecoder_m
# meta banner: https://github.com/FajoX1/FAmods/blob/main/assets/banners/cocoon.png?raw=true # meta banner: https://github.com/FajoX1/FAmods/blob/main/assets/banners/cocoon.png?raw=true
# requires: openai httpx aiohttp # requires: openai httpx aiohttp bs4 markdown
# --------------------------------------------------------------------------------- # ---------------------------------------------------------------------------------
import re
import html import html
import uuid
import httpx import httpx
import asyncio import asyncio
import logging import logging
import markdown
from openai import AsyncOpenAI from openai import AsyncOpenAI
from typing import Optional, Any from typing import Optional, Any
from dataclasses import dataclass from dataclasses import dataclass
from bs4 import BeautifulSoup, NavigableString
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
from telethon.tl.types import User
from telethon.errors import MessageNotModifiedError from telethon.errors import MessageNotModifiedError
from aiogram.exceptions import TelegramBadRequest
from .. import loader, utils from .. import loader, utils
from ..inline.types import InlineCall
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
TG_ALLOWED = {
"b",
"strong",
"i",
"em",
"u",
"ins",
"s",
"strike",
"del",
"a",
"code",
"pre",
"blockquote",
"emoji",
"tg-emoji",
}
TAG_MAP = {"strong": "b", "em": "i", "del": "s", "strike": "s", "ins": "u"}
@dataclass(frozen=True) @dataclass(frozen=True)
class Usage: class Usage:
spent_nano: int spent_nano: int
@@ -110,6 +138,92 @@ def _percent_remaining(spent: int, total: int) -> float:
return (remaining / total) * 100.0 return (remaining / total) * 100.0
def md_to_tg_html(text: str) -> str:
if not text:
return ""
raw_html = markdown.markdown(text, extensions=["fenced_code", "tables", "nl2br"])
soup = BeautifulSoup(raw_html, "html.parser")
def stringify(node, lang=None):
res = ""
for child in node.children:
if isinstance(child, NavigableString):
res += html.escape(str(child))
elif child.name:
tag_name = child.name
if tag_name in ["h1", "h2", "h3", "h4", "h5", "h6"]:
content = stringify(child)
res += f"<b>{content}</b>\n\n"
elif tag_name == "p":
res += stringify(child) + "\n\n"
elif tag_name == "br":
res += "\n"
elif tag_name == "li":
res += f"{stringify(child)}\n"
elif tag_name in ["ul", "ol"]:
res += stringify(child) + "\n"
elif tag_name == "tr":
res += "| " + stringify(child) + "\n"
elif tag_name in ["td", "th"]:
res += stringify(child) + " | "
else:
target_tag = TAG_MAP.get(tag_name, tag_name)
if target_tag in TG_ALLOWED:
inner_html = stringify(child)
if not inner_html.strip() and target_tag not in ["code", "pre"]:
res += inner_html
continue
if target_tag == "a":
href = child.get("href", "")
if href:
res += f'<a href="{html.escape(href)}">{inner_html}</a>'
else:
res += inner_html
elif target_tag == "code":
cls = child.get("class", [])
if cls and cls[0].startswith("language-"):
res += f'<code class="{cls[0]}">{inner_html}</code>'
else:
res += f"<code>{inner_html}</code>"
elif target_tag == "pre":
res += f"<pre>{inner_html}</pre>"
else:
res += f"<{target_tag}>{inner_html}</{target_tag}>"
else:
res += stringify(child)
return res
final_text = stringify(soup)
final_text = re.sub(r"\n{3,}", "\n\n", final_text)
return final_text.strip()
def repair_html_tags(html_chunk: str) -> str:
if not html_chunk:
return ""
newline_placeholder = f"MARKER_{uuid.uuid4().hex}"
protected_html = html_chunk.replace("\n", newline_placeholder)
soup = BeautifulSoup(protected_html, "html.parser")
repaired_html = soup.decode_contents(formatter=None)
final_html = repaired_html.replace(newline_placeholder, "\n")
return final_html
@loader.tds @loader.tds
class Cocoon(loader.Module): class Cocoon(loader.Module):
"""Взаимодействие с Cocoon от HikkaHost""" """Взаимодействие с Cocoon от HikkaHost"""
@@ -126,24 +240,25 @@ class Cocoon(loader.Module):
"<b><emoji document_id=5456307331644037599>❌</emoji>Неверный токен или у вас нет подписки <emoji document_id=5188377234380954537>🌘</emoji> HikkaHost.</b>\n\n" "<b><emoji document_id=5456307331644037599>❌</emoji>Неверный токен или у вас нет подписки <emoji document_id=5188377234380954537>🌘</emoji> HikkaHost.</b>\n\n"
"<emoji document_id=5456672880605565619>🌘</emoji> Получить токен: @hikkahost_bot → <emoji document_id=5208521532942358129>🥚</emoji> Cocoon</b>" "<emoji document_id=5456672880605565619>🌘</emoji> Получить токен: @hikkahost_bot → <emoji document_id=5208521532942358129>🥚</emoji> Cocoon</b>"
), ),
"sending_request_to_cocoon": "<emoji document_id=5197252827247841976>🐣</emoji> <b>Обрабатываю запрос в Cocoon...</b>", "sending_request_to_cocoon": "<tg-emoji emoji-id=5197252827247841976>🐣</tg-emoji> <b>Обрабатываю запрос в Cocoon...</b>",
"thinking": ( "thinking": (
"<emoji document_id=5197252827247841976>🐣</emoji> <b>Думаю...</b>\n\n" "<tg-emoji emoji-id=5197252827247841976>🐣</tg-emoji> <b>Думаю...</b>\n\n"
"<blockquote>{thoughts}…</blockquote>" "<blockquote>{thoughts}…</blockquote>"
), ),
"answer": ( "answer": (
"<emoji document_id=5456217626957091223>🌘</emoji> <b>Вопрос:</b> {question}\n\n" "<tg-emoji emoji-id=5456217626957091223>🌘</tg-emoji> <b>Вопрос:</b> {question}\n\n"
"<emoji document_id=5197252827247841976>🐣</emoji> <b>Размышления:</b>\n" "<tg-emoji emoji-id=5197252827247841976>🐣</tg-emoji> <b>Размышления:</b>\n"
"<blockquote expandable>{thoughts}</blockquote>\n\n" "<blockquote expandable>{thoughts}</blockquote>\n\n"
"<emoji document_id=5208521532942358129>🥚</emoji> {answer}\n\n" "<tg-emoji emoji-id=5208521532942358129>🥚</tg-emoji> {answer}\n\n"
"<emoji document_id=5458567764341985638>🚀</emoji> <b>Модель</b>: <code>{model}</code>" "<tg-emoji emoji-id=5458567764341985638>🚀</tg-emoji> <b>Модель</b>: <code>{model}</code>"
), ),
"usage": ( "usage": (
"<b><emoji document_id=5208521532942358129>🥚</emoji> Cocoon API\n\n" "<b><tg-emoji emoji-id=5208521532942358129>🥚</tg-emoji> Cocoon API\n\n"
"<emoji document_id=5458805877328875335>💡</emoji> Использовано:\n" "<tg-emoji emoji-id=5458805877328875335>💡</tg-emoji> Использовано:\n"
"</b><i>• {current}/{total} ({percent}% осталось)</i><b>\n\n" "</b><i>• {current}/{total} ({percent}% осталось)</i><b>\n\n"
"<emoji document_id=5456591761558245861>⏳</emoji> Лимит сбросится через {days} день(-ей).</b>" "<tg-emoji emoji-id=5456591761558245861>⏳</tg-emoji> Лимит сбросится через {days} день(-ей).</b>"
), ),
"again_kb": "🔄 Сгенерировать ещё раз",
} }
def __init__(self): def __init__(self):
@@ -163,6 +278,9 @@ class Cocoon(loader.Module):
"role", "role",
"user", "user",
lambda: "Роль user-сообщения (обычно user).", lambda: "Роль user-сообщения (обычно user).",
validator=loader.validators.Choice(
["system", "developer", "user", "assistant", "tool"]
),
), ),
loader.ConfigValue( loader.ConfigValue(
"system_prompt", "system_prompt",
@@ -201,12 +319,13 @@ class Cocoon(loader.Module):
): ):
self._rebuild_openai_client() self._rebuild_openai_client()
async def _answer(self, message, text): async def _answer(self, message, text, *args, **kwargs):
try: try:
if len(text) > 4096: if len(text) > 4096:
text = text[:4090] + "..." text = text[:4090] + "..."
return await utils.answer(message, text)
except MessageNotModifiedError: return await utils.answer(message, repair_html_tags(text), *args, **kwargs)
except (MessageNotModifiedError, TelegramBadRequest):
return message return message
async def _fetch_usage(self) -> Optional[Usage]: async def _fetch_usage(self) -> Optional[Usage]:
@@ -245,6 +364,9 @@ class Cocoon(loader.Module):
updated_at=(_safe_int(data.get("updated_at"), 0) or None), updated_at=(_safe_int(data.get("updated_at"), 0) or None),
) )
async def _regenerate(self, call: InlineCall, arg1, arg2):
await self.cocoon(arg1, inline_message=arg2)
@loader.command() @loader.command()
async def ccusage(self, message): async def ccusage(self, message):
"""Статистика использования Cocoon""" """Статистика использования Cocoon"""
@@ -280,8 +402,8 @@ class Cocoon(loader.Module):
) )
@loader.command() @loader.command()
async def cocoon(self, message): async def cocoon(self, message, inline_message=None):
"""Задать вопрос к ИИ""" """Задать вопрос к ИИ (поддерживает ответ на сообщение)"""
q = utils.get_args_raw(message) q = utils.get_args_raw(message)
if not q: if not q:
@@ -299,26 +421,72 @@ class Cocoon(loader.Module):
if not usage: if not usage:
return await utils.answer(message, self.strings["invalid_token_or_no_sub"]) return await utils.answer(message, self.strings["invalid_token_or_no_sub"])
message = await utils.answer(message, self.strings["sending_request_to_cocoon"]) user_message = message
if not inline_message:
message = await self.inline.form(
text="...",
message=message,
force_me=False,
)
else:
message = inline_message
await utils.answer(message, self.strings["sending_request_to_cocoon"])
self._ensure_client() self._ensure_client()
client = AsyncOpenAI(api_key=self.config["token"], base_url=self.api_url)
system_prompt = (self.config.get("system_prompt") or "").strip() system_prompt = (self.config.get("system_prompt") or "").strip()
messages = [] messages = []
if system_prompt: if system_prompt:
messages.append({"role": "system", "content": system_prompt}) messages.append({"role": "system", "content": system_prompt})
messages.append({"role": self.config["role"] or "user", "content": q})
if user_message.reply_to:
reply = await user_message.get_reply_message()
entity_id = None
if hasattr(reply, "from_id") and reply.from_id:
if hasattr(reply.from_id, "user_id") and reply.from_id.user_id:
entity_id = reply.from_id.user_id
elif hasattr(reply.from_id, "channel_id") and reply.from_id.channel_id:
entity_id = reply.from_id.channel_id
if entity_id is None:
if hasattr(reply.peer_id, "user_id") and reply.peer_id.user_id:
entity_id = reply.peer_id.user_id
else:
entity_id = reply.peer_id.channel_id
entity = await self.client.get_entity(entity_id)
date = reply.date.strftime("%H:%M %d.%m.%Y UTC")
messages.append(
{
"role": "user",
"content": (
(
f"{entity.first_name} {entity.last_name or ''} (user id: {entity.id}) "
if isinstance(entity, User)
else f"Channel {entity.title} (channel id: {entity.id}) "
)
+ f"msg id: {reply.id}, date: {date}: "
+ reply.message
),
}
)
messages.append({"role": self.config.get("role", "user"), "content": q})
try: try:
response = await client.chat.completions.create( response = await self._openai.chat.completions.create(
messages=messages, messages=messages,
stream=True, stream=True,
max_tokens=self.config.get("max_tokens", 3900), max_tokens=self.config.get("max_tokens", 3900),
model=self.config.get("model", "Qwen/Qwen3-32B"), model=self.config.get("model", "Qwen/Qwen3-32B"),
temperature=self.config.get("temperature", 0.2) temperature=self.config.get("temperature", 0.2),
) )
response_text = "" response_text = ""
@@ -328,60 +496,57 @@ class Cocoon(loader.Module):
if chunk.choices and chunk.choices[0].delta.content: if chunk.choices and chunk.choices[0].delta.content:
chunk_buffer += chunk.choices[0].delta.content chunk_buffer += chunk.choices[0].delta.content
if len(chunk_buffer) >= 150: if len(chunk_buffer) >= 100:
response_text += chunk_buffer response_text += chunk_buffer
chunk_buffer = "" chunk_buffer = ""
thoughts = ( thoughts = response_text.split("</think>", 1)[0].replace(
response_text.replace("<think>", "") "<think>", ""
.replace("</think>", "")
.strip()
) )
if "</think>" in response_text: if "</think>" in response_text:
after_think = response_text.split("</think>", 1)[-1].strip() after_think = response_text.split("</think>", 1)[1].strip()
await self._answer( await self._answer(
message, message,
self.strings["answer"].format( self.strings["answer"].format(
thoughts=thoughts[:300], thoughts=thoughts[:500],
question=q, question=q,
answer=_escape_text(after_think) + "", answer=md_to_tg_html(_escape_text(after_think) + ""),
model=self.config["model"], model=self.config["model"],
), ),
) )
else: else:
thinking_text = (
response_text.replace("<think>", "")
.replace("</think>", "")
.strip()
)
await self._answer( await self._answer(
message, message,
self.strings["thinking"].format( self.strings["thinking"].format(
thoughts=_escape_text(thinking_text) thoughts=md_to_tg_html(_escape_text(thoughts) + "")
), ),
) )
await asyncio.sleep(2) await asyncio.sleep(0.2)
if chunk_buffer: if chunk_buffer:
response_text += chunk_buffer response_text += chunk_buffer
if "</think>" in response_text: responses_data = response_text.split("</think>", 1)
after_think = response_text.split("</think>", 1)[-1].strip() thoughts = responses_data[0].strip().replace("<think>", "")
else: after_think = responses_data[1].strip()
after_think = (
response_text.replace("<think>", "").replace("</think>", "").strip()
)
await self._answer( await self._answer(
message, message,
self.strings["answer"].format( self.strings["answer"].format(
thoughts=thoughts,
question=q, question=q,
answer=_escape_text(after_think), thoughts=md_to_tg_html(_escape_text(thoughts[:500])),
answer=md_to_tg_html(_escape_text(after_think)),
model=self.config["model"], model=self.config["model"],
), ),
reply_markup=[
{
"text": self.strings["again_kb"],
"callback": self._regenerate,
"args": [user_message, message],
}
],
) )
except httpx.RemoteProtocolError: except httpx.RemoteProtocolError:

View File

@@ -44,3 +44,4 @@ spotify4ik
picme picme
hetsu hetsu
ptichki ptichki
cocoon