diff --git a/README.md b/README.md index cdff01aec..9791c037b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用 1. **大语言模型对话**。支持各种大语言模型,包括 OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM 等,支持接入本地部署的大模型,通过 Ollama、LLMTuner。具有多轮对话、人格情境、多模态能力,支持图片理解、语音转文字(Whisper)。 2. **多消息平台接入**。支持接入 QQ(OneBot)、QQ 频道、微信(Gewechat)、飞书、Telegram。后续将支持钉钉、Discord、WhatsApp、小爱音响。支持速率限制、白名单、关键词过滤、百度内容审核。 -3. **Agent**。原生支持部分 Agent 能力,如代码执行器、自然语言待办、网页搜索。对接 [Dify 平台](https://astrbot.app/others/dify.html),便捷接入 Dify 智能助手、知识库和 Dify 工作流。 +3. **Agent**。原生支持部分 Agent 能力,如代码执行器、自然语言待办、网页搜索。对接 [Dify 平台](https://dify.ai/),便捷接入 Dify 智能助手、知识库和 Dify 工作流。 4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,极简开发。已支持安装多个插件。 5. **可视化管理面板**。支持可视化修改配置、插件管理、日志查看等功能,降低配置难度。集成 WebChat,可在面板上与大模型对话。 6. **高稳定性、高模块化**。基于事件总线和流水线的架构设计,高度模块化,低耦合。 diff --git a/README_en.md b/README_en.md index b77fe7344..249dd5e79 100644 --- a/README_en.md +++ b/README_en.md @@ -28,7 +28,7 @@ AstrBot is a loosely coupled, asynchronous chatbot and development framework tha 1. **LLM Conversations** - Supports various LLMs including OpenAI API, Google Gemini, Llama, Deepseek, ChatGLM, etc. Enables local model deployment via Ollama/LLMTuner. Features multi-turn dialogues, personality contexts, multimodal capabilities (image understanding), and speech-to-text (Whisper). 2. **Multi-platform Integration** - Supports QQ (OneBot), QQ Channels, WeChat (Gewechat), Feishu, and Telegram. Planned support for DingTalk, Discord, WhatsApp, and Xiaomi Smart Speakers. Includes rate limiting, whitelisting, keyword filtering, and Baidu content moderation. -3. **Agent Capabilities** - Native support for code execution, natural language TODO lists, web search. Integrates with [Dify Platform](https://astrbot.app/others/dify.html) for easy access to Dify assistants/knowledge bases/workflows. +3. **Agent Capabilities** - Native support for code execution, natural language TODO lists, web search. Integrates with [Dify Platform](https://dify.ai/) for easy access to Dify assistants/knowledge bases/workflows. 4. **Plugin System** - Optimized plugin mechanism with minimal development effort. Supports multiple installed plugins. 5. **Web Dashboard** - Visual configuration management, plugin controls, logging, and WebChat interface for direct LLM interaction. 6. **High Stability & Modularity** - Event bus and pipeline architecture ensures high modularization and loose coupling. diff --git a/README_ja.md b/README_ja.md index 03092818d..325857d9a 100644 --- a/README_ja.md +++ b/README_ja.md @@ -28,7 +28,7 @@ AstrBot は、疎結合、非同期、複数のメッセージプラットフォ 1. **大規模言語モデルの対話**。OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM など、さまざまな大規模言語モデルをサポートし、Ollama、LLMTuner を介してローカルにデプロイされた大規模モデルをサポートします。多輪対話、人格シナリオ、多モーダル機能を備え、画像理解、音声からテキストへの変換(Whisper)をサポートします。 2. **複数のメッセージプラットフォームの接続**。QQ(OneBot)、QQ チャンネル、WeChat(Gewechat)、Feishu、Telegram への接続をサポートします。今後、DingTalk、Discord、WhatsApp、Xiaoai 音響をサポートする予定です。レート制限、ホワイトリスト、キーワードフィルタリング、Baidu コンテンツ監査をサポートします。 -3. **エージェント**。一部のエージェント機能をネイティブにサポートし、コードエグゼキューター、自然言語タスク、ウェブ検索などを提供します。[Dify プラットフォーム](https://astrbot.app/others/dify.html)と連携し、Dify スマートアシスタント、ナレッジベース、Dify ワークフローを簡単に接続できます。 +3. **エージェント**。一部のエージェント機能をネイティブにサポートし、コードエグゼキューター、自然言語タスク、ウェブ検索などを提供します。[Dify プラットフォーム](https://dify.ai/)と連携し、Dify スマートアシスタント、ナレッジベース、Dify ワークフローを簡単に接続できます。 4. **プラグインの拡張**。深く最適化されたプラグインメカニズムを備え、[プラグインの開発](https://astrbot.app/dev/plugin.html)をサポートし、機能を拡張できます。複数のプラグインのインストールをサポートします。 5. **ビジュアル管理パネル**。設定の視覚的な変更、プラグイン管理、ログの表示などをサポートし、設定の難易度を低減します。WebChat を統合し、パネル上で大規模モデルと対話できます。 6. **高い安定性と高いモジュール性**。イベントバスとパイプラインに基づくアーキテクチャ設計により、高度にモジュール化され、低結合です。 diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 879193f43..b77ad94bc 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -522,6 +522,12 @@ CONFIG_METADATA_2 = { "model": "gemini-2.0-flash-exp", }, "gm_resp_image_modal": False, + "gm_safety_settings": { + "harassment": "BLOCK_MEDIUM_AND_ABOVE", + "hate_speech": "BLOCK_MEDIUM_AND_ABOVE", + "sexually_explicit": "BLOCK_MEDIUM_AND_ABOVE", + "dangerous_content": "BLOCK_MEDIUM_AND_ABOVE", + }, }, "DeepSeek": { "id": "deepseek_default", @@ -671,13 +677,78 @@ CONFIG_METADATA_2 = { "fishaudio-tts-character": "可莉", "timeout": "20", }, + "阿里云百炼_TTS(API)": { + "id": "dashscope_tts", + "type": "dashscope_tts", + "enable": False, + "api_key": "", + "model": "cosyvoice-v1", + "dashscope_tts_voice": "loongstella", + "timeout": "20", + }, }, "items": { + "dashscope_tts_voice": { + "description": "语音合成模型", + "type": "string", + "hint": "阿里云百炼语音合成模型名称。具体可参考 https://help.aliyun.com/zh/model-studio/developer-reference/cosyvoice-python-api 等内容", + }, "gm_resp_image_modal": { "description": "启用图片模态", "type": "bool", "hint": "启用后,将支持返回图片内容。需要模型支持,否则会报错。具体支持模型请查看 Google Gemini 官方网站。温馨提示,如果您需要生成图片,请关闭 `启用群员识别` 配置获得更好的效果。", }, + "gm_safety_settings": { + "description": "安全过滤器", + "type": "object", + "hint": "设置模型输入的内容安全过滤级别。过滤级别分类为NONE(不屏蔽)、HIGH(高风险时屏蔽)、MEDIUM_AND_ABOVE(中等风险及以上屏蔽)、LOW_AND_ABOVE(低风险及以上时屏蔽),具体参见Gemini API文档。", + "items": { + "harassment": { + "description": "骚扰内容", + "type": "string", + "hint": "负面或有害评论", + "options": [ + "BLOCK_NONE", + "BLOCK_ONLY_HIGH", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_LOW_AND_ABOVE", + ], + }, + "hate_speech": { + "description": "仇恨言论", + "type": "string", + "hint": "粗鲁、无礼或亵渎性质内容", + "options": [ + "BLOCK_NONE", + "BLOCK_ONLY_HIGH", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_LOW_AND_ABOVE", + ], + }, + "sexually_explicit": { + "description": "露骨色情内容", + "type": "string", + "hint": "包含性行为或其他淫秽内容的引用", + "options": [ + "BLOCK_NONE", + "BLOCK_ONLY_HIGH", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_LOW_AND_ABOVE", + ], + }, + "dangerous_content": { + "description": "危险内容", + "type": "string", + "hint": "宣扬、助长或鼓励有害行为的信息", + "options": [ + "BLOCK_NONE", + "BLOCK_ONLY_HIGH", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_LOW_AND_ABOVE", + ], + }, + }, + }, "rag_options": { "description": "RAG 选项", "type": "object", diff --git a/astrbot/core/db/plugin/sqlite_impl.py b/astrbot/core/db/plugin/sqlite_impl.py new file mode 100644 index 000000000..5440362af --- /dev/null +++ b/astrbot/core/db/plugin/sqlite_impl.py @@ -0,0 +1,112 @@ +import json +import aiosqlite +import os +from typing import Any +from .plugin_storage import PluginStorage + +DBPATH = "data/plugin_data/sqlite/plugin_data.db" + + +class SQLitePluginStorage(PluginStorage): + """插件数据的 SQLite 存储实现类。 + + 该类提供异步方式将插件数据存储到 SQLite 数据库中,支持数据的增删改查操作。 + 所有数据以 (plugin, key) 作为复合主键进行索引。 + """ + + _instance = None # Standalone instance of the class + _db_conn = None + db_path = None + + def __new__(cls): + """ + 创建或获取 SQLitePluginStorage 的单例实例。 + 如果实例已存在,则返回现有实例;否则创建一个新实例。 + 数据在 `data/plugin_data/sqlite/plugin_data.db` 下。 + """ + os.makedirs(os.path.dirname(DBPATH), exist_ok=True) + if cls._instance is None: + cls._instance = super(SQLitePluginStorage, cls).__new__(cls) + cls._instance.db_path = DBPATH + return cls._instance + + async def _init_db(self): + """初始化数据库连接(只执行一次)""" + if SQLitePluginStorage._db_conn is None: + SQLitePluginStorage._db_conn = await aiosqlite.connect(self.db_path) + await self._setup_db() + + async def _setup_db(self): + """ + 异步初始化数据库。 + + 创建插件数据表,如果表不存在则创建,表结构包含 plugin、key 和 value 字段, + 其中 plugin 和 key 组合作为主键。 + """ + await self._db_conn.execute(""" + CREATE TABLE IF NOT EXISTS plugin_data ( + plugin TEXT, + key TEXT, + value TEXT, + PRIMARY KEY (plugin, key) + ) + """) + await self._db_conn.commit() + + async def set(self, plugin: str, key: str, value: Any): + """ + 异步存储数据。 + + 将指定插件的键值对存入数据库,如果键已存在则更新值。 + 值会被序列化为 JSON 字符串后存储。 + + Args: + plugin: 插件标识符 + key: 数据键名 + value: 要存储的数据值(任意类型,将被 JSON 序列化) + """ + await self._init_db() + await self._db_conn.execute( + "INSERT INTO plugin_data (plugin, key, value) VALUES (?, ?, ?) " + "ON CONFLICT(plugin, key) DO UPDATE SET value = excluded.value", + (plugin, key, json.dumps(value)), + ) + await self._db_conn.commit() + + async def get(self, plugin: str, key: str) -> Any: + """ + 异步获取数据。 + + 从数据库中获取指定插件和键名对应的值, + 返回的值会从 JSON 字符串反序列化为原始数据类型。 + + Args: + plugin: 插件标识符 + key: 数据键名 + + Returns: + Any: 存储的数据值,如果未找到则返回 None + """ + await self._init_db() + async with self._db_conn.execute( + "SELECT value FROM plugin_data WHERE plugin = ? AND key = ?", + (plugin, key), + ) as cursor: + row = await cursor.fetchone() + return json.loads(row[0]) if row else None + + async def delete(self, plugin: str, key: str): + """ + 异步删除数据。 + + 从数据库中删除指定插件和键名对应的数据项。 + + Args: + plugin: 插件标识符 + key: 要删除的数据键名 + """ + await self._init_db() + await self._db_conn.execute( + "DELETE FROM plugin_data WHERE plugin = ? AND key = ?", (plugin, key) + ) + await self._db_conn.commit() diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index 4894b2e03..d7bb9583c 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -156,9 +156,7 @@ class ResultDecorateStage(Stage): self.ctx.astrbot_config["provider_tts_settings"]["enable"] and result.is_llm_result() ): - tts_provider = ( - self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst - ) + tts_provider = self.ctx.plugin_manager.context.provider_manager.curr_tts_provider_inst new_chain = [] for comp in result.chain: if isinstance(comp, Plain) and len(comp.text) > 1: diff --git a/astrbot/core/platform/sources/telegram/tg_event.py b/astrbot/core/platform/sources/telegram/tg_event.py index 87fea26c6..eab41ad84 100644 --- a/astrbot/core/platform/sources/telegram/tg_event.py +++ b/astrbot/core/platform/sources/telegram/tg_event.py @@ -1,8 +1,10 @@ +import telegramify_markdown from astrbot.api.event import AstrMessageEvent, MessageChain from astrbot.api.platform import AstrBotMessage, PlatformMetadata, MessageType from astrbot.api.message_components import Plain, Image, Reply, At, File, Record from telegram.ext import ExtBot from astrbot.core.utils.io import download_file +from astrbot import logger class TelegramPlatformEvent(AstrMessageEvent): @@ -49,7 +51,17 @@ class TelegramPlatformEvent(AstrMessageEvent): if at_user_id and not at_flag: i.text = f"@{at_user_id} " + i.text at_flag = True - await client.send_message(text=i.text, **payload) + text = i.text + try: + text = telegramify_markdown.markdownify( + i.text, max_line_length=None, normalize_whitespace=False + ) + except Exception as e: + logger.warning( + f"MarkdownV2 conversion failed: {e}. Using plain text instead." + ) + return + await client.send_message(text=text, parse_mode="MarkdownV2", **payload) elif isinstance(i, Image): image_path = await i.convert_to_file_path() await client.send_photo(photo=image_path, **payload) diff --git a/astrbot/core/provider/manager.py b/astrbot/core/provider/manager.py index 71647a5ac..a3fa65e86 100644 --- a/astrbot/core/provider/manager.py +++ b/astrbot/core/provider/manager.py @@ -198,6 +198,10 @@ class ProviderManager: from .sources.fishaudio_tts_api_source import ( ProviderFishAudioTTSAPI as ProviderFishAudioTTSAPI, ) + case "dashscope_tts": + from .sources.dashscope_tts import ( + ProviderDashscopeTTSAPI as ProviderDashscopeTTSAPI, + ) except (ImportError, ModuleNotFoundError) as e: logger.critical( f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。" diff --git a/astrbot/core/provider/sources/dashscope_tts.py b/astrbot/core/provider/sources/dashscope_tts.py new file mode 100644 index 000000000..06b390fcd --- /dev/null +++ b/astrbot/core/provider/sources/dashscope_tts.py @@ -0,0 +1,39 @@ +import dashscope +import uuid +import asyncio +from dashscope.audio.tts_v2 import * +from ..provider import TTSProvider +from ..entites import ProviderType +from ..register import register_provider_adapter + + +@register_provider_adapter( + "dashscope_tts", "Dashscope TTS API", provider_type=ProviderType.TEXT_TO_SPEECH +) +class ProviderDashscopeTTSAPI(TTSProvider): + def __init__( + self, + provider_config: dict, + provider_settings: dict, + ) -> None: + super().__init__(provider_config, provider_settings) + self.chosen_api_key: str = provider_config.get("api_key", "") + self.voice: str = provider_config.get("dashscope_tts_voice", "loongstella") + self.set_model(provider_config.get("model", None)) + self.timeout_ms = float(provider_config.get("timeout", 20))*1000 + + dashscope.api_key = self.chosen_api_key + self.synthesizer = SpeechSynthesizer( + model=self.get_model(), + voice=self.voice, + format=AudioFormat.WAV_24000HZ_MONO_16BIT, + ) + + async def get_audio(self, text: str) -> str: + path = f"data/temp/dashscope_tts_{uuid.uuid4()}.wav" + audio = await asyncio.get_event_loop().run_in_executor( + None, self.synthesizer.call, text, self.timeout_ms + ) + with open(path, "wb") as f: + f.write(audio) + return path diff --git a/astrbot/core/provider/sources/edge_tts_source.py b/astrbot/core/provider/sources/edge_tts_source.py index b6b758e29..0eadb2190 100644 --- a/astrbot/core/provider/sources/edge_tts_source.py +++ b/astrbot/core/provider/sources/edge_tts_source.py @@ -63,9 +63,7 @@ class ProviderEdgeTTS(TTSProvider): ff = FFmpeg() ff.convert(input=mp3_path, output=wav_path) except Exception as e: - logger.debug( - f"pyffmpeg 转换失败: {e}, 尝试使用 ffmpeg 命令行进行转换" - ) + logger.debug(f"pyffmpeg 转换失败: {e}, 尝试使用 ffmpeg 命令行进行转换") # use ffmpeg command line # 使用ffmpeg将MP3转换为标准WAV格式 diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index cc34784c5..9f5f7c3c1 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -43,6 +43,7 @@ class SimpleGoogleGenAIClient: system_instruction: str = "", tools: dict = None, modalities: List[str] = ["Text"], + safety_settings: List[dict] = [], ): payload = {} if system_instruction: @@ -53,6 +54,10 @@ class SimpleGoogleGenAIClient: payload["generationConfig"] = { "responseModalities": modalities, } + payload["safetySettings"] = [ + {"category": s["category"], "threshold": s["threshold"]} + for s in safety_settings + ] logger.debug(f"payload: {payload}") request_url = ( f"{self.api_base}/v1beta/models/{model}:generateContent?key={self.api_key}" @@ -106,6 +111,21 @@ class ProviderGoogleGenAI(Provider): ) self.set_model(provider_config["model_config"]["model"]) + safety_mapping = { + "harassment": "HARM_CATEGORY_HARASSMENT", + "hate_speech": "HARM_CATEGORY_HATE_SPEECH", + "sexually_explicit": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "dangerous_content": "HARM_CATEGORY_DANGEROUS_CONTENT", + } + + self.safety_settings = [] + user_safety_config = self.provider_config.get("gm_safety_settings", {}) + for config_key, harm_category in safety_mapping.items(): + if threshold := user_safety_config.get(config_key): + self.safety_settings.append( + {"category": harm_category, "threshold": threshold} + ) + async def get_models(self): return await self.client.models_list() @@ -205,6 +225,7 @@ class ProviderGoogleGenAI(Provider): system_instruction=system_instruction, tools=tool, modalities=modalites, + safety_settings=self.safety_settings, ) logger.debug(f"result: {result}") diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 766835719..f8d392404 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -221,8 +221,8 @@ class ProviderOpenAIOfficial(Provider): payloads["messages"] = new_contexts elif ( "Function calling is not enabled" in str(e) - or ("tool" in str(e) and "support" in str(e).lower()) - or ("function" in str(e) and "support" in str(e).lower()) + or ("tool" in str(e).lower() and "support" in str(e).lower()) + or ("function" in str(e).lower() and "support" in str(e).lower()) ): # openai, ollama, gemini openai, siliconcloud 的错误提示与 code 不统一,只能通过字符串匹配 logger.info( diff --git a/astrbot/dashboard/routes/static_file.py b/astrbot/dashboard/routes/static_file.py index 5d4c05c6b..4503a28e5 100644 --- a/astrbot/dashboard/routes/static_file.py +++ b/astrbot/dashboard/routes/static_file.py @@ -21,7 +21,7 @@ class StaticFileRoute(Route): "/about", "/extension-marketplace", "/conversation", - "/tool-use" + "/tool-use", ] for i in index_: self.app.add_url_rule(i, view_func=self.index) diff --git a/packages/session_controller/main.py b/packages/session_controller/main.py index 6c00bc81d..99d0a2e62 100644 --- a/packages/session_controller/main.py +++ b/packages/session_controller/main.py @@ -1,6 +1,5 @@ import astrbot.api.message_components as Comp import copy -import json from astrbot.api import logger from astrbot.api.event import AstrMessageEvent, filter from astrbot.api.star import Context, Star, register @@ -64,17 +63,11 @@ class Waiter(Star): event.unified_msg_origin ) conversation = None - context = [] if curr_cid: conversation = await self.context.conversation_manager.get_conversation( event.unified_msg_origin, curr_cid ) - context = ( - json.loads(conversation.history) - if conversation.history - else [] - ) else: # 创建新对话 curr_cid = await self.context.conversation_manager.new_conversation( diff --git a/pyproject.toml b/pyproject.toml index 20f61c95b..93ad29f42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "quart>=0.20.0", "readability-lxml>=0.8.1", "silk-python>=0.2.6", + "telegramify-markdown>=0.5.0", "wechatpy>=1.8.18", ] diff --git a/requirements.txt b/requirements.txt index 437e0807b..e20771e86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,4 +28,5 @@ dingtalk-stream defusedxml mcp certifi -pip \ No newline at end of file +pip +telegramify-markdown \ No newline at end of file diff --git a/uv.lock b/uv.lock index 835b323b3..445941a7d 100644 --- a/uv.lock +++ b/uv.lock @@ -225,6 +225,7 @@ dependencies = [ { name = "quart" }, { name = "readability-lxml" }, { name = "silk-python" }, + { name = "telegramify-markdown" }, { name = "wechatpy" }, ] @@ -260,6 +261,7 @@ requires-dist = [ { name = "quart", specifier = ">=0.20.0" }, { name = "readability-lxml", specifier = ">=0.8.1" }, { name = "silk-python", specifier = ">=0.2.6" }, + { name = "telegramify-markdown", specifier = ">=0.5.0" }, { name = "wechatpy", specifier = ">=1.8.18" }, ] @@ -1059,6 +1061,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/d1/3ff566ecf322077d861f1a68a1ff025cad337417bd66ad22a7c6f7dfcfaf/mcp-1.5.0-py3-none-any.whl", hash = "sha256:51c3f35ce93cb702f7513c12406bbea9665ef75a08db909200b07da9db641527", size = 73734 }, ] +[[package]] +name = "mistletoe" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/96/ea46a376a7c4cd56955ecdfff0ea68de43996a4e6d1aee4599729453bd11/mistletoe-1.4.0.tar.gz", hash = "sha256:1630f906e5e4bbe66fdeb4d29d277e2ea515d642bb18a9b49b136361a9818c9d", size = 107203 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/0f/b5e545f0c7962be90366af3418989b12cf441d9da1e5d89d88f2f3e5cf8f/mistletoe-1.4.0-py3-none-any.whl", hash = "sha256:44a477803861de1237ba22e375c6b617690a31d2902b47279d1f8f7ed498a794", size = 51304 }, +] + [[package]] name = "multidict" version = "6.2.0" @@ -1792,6 +1803,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b1/74babcc824a57904e919f3af16d86c08b524c0691504baf038ef2d7f655c/taskgroup-0.2.2-py2.py3-none-any.whl", hash = "sha256:e2c53121609f4ae97303e9ea1524304b4de6faf9eb2c9280c7f87976479a52fb", size = 14237 }, ] +[[package]] +name = "telegramify-markdown" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mistletoe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/b7/a9b56f856f87e1b4d743932353f71811844a413561be180a22d667ef6f5a/telegramify_markdown-0.5.0.tar.gz", hash = "sha256:70e6eff7e341e6e9c8818fa1ec53a4e25e4f5e3ef50856d7772760fc6b7a4066", size = 36017 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/87/4a39dde5b4aea91b874cbbe057edea19334e62651ce0f1f74f5a1f721439/telegramify_markdown-0.5.0-py3-none-any.whl", hash = "sha256:6f66b7029c0eba268fed5f9daf9216f56c588c6202dd591ff572f7df0d318f2f", size = 32389 }, +] + [[package]] name = "tomli" version = "2.2.1"