Commit 0c3de72

dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 06:20:12
chore(deps): bump the python-deps group across 1 directory with 8 updates (#82)
* chore(deps): bump the python-deps group across 1 directory with 8 updates Bumps the python-deps group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [anthropic](https://github.com/anthropics/anthropic-sdk-python) | `0.84.0` | `0.86.0` | | [chardet](https://github.com/chardet/chardet) | `6.0.0.post1` | `7.4.0.post2` | | [curl-cffi](https://github.com/lexiforest/curl_cffi) | `0.15.0b4` | `0.15.0rc1` | | [dashscope](https://dashscope.aliyun.com/) | `1.25.12` | `1.25.15` | | [google-genai](https://github.com/googleapis/python-genai) | `1.65.0` | `1.69.0` | | [openai](https://github.com/openai/openai-python) | `2.24.0` | `2.30.0` | | [puremagic](https://github.com/cdgriffith/puremagic) | `2.0.0` | `2.1.1` | | [pyrotgfork](https://github.com/TelegramPlayGround/Pyrogram) | `2.2.19` | `2.2.21` | Updates `anthropic` from 0.84.0 to 0.86.0 - [Release notes](https://github.com/anthropics/anthropic-sdk-python/releases) - [Changelog](https://github.com/anthropics/anthropic-sdk-python/blob/main/CHANGELOG.md) - [Commits](https://github.com/anthropics/anthropic-sdk-python/compare/v0.84.0...v0.86.0) Updates `chardet` from 6.0.0.post1 to 7.4.0.post2 - [Release notes](https://github.com/chardet/chardet/releases) - [Changelog](https://github.com/chardet/chardet/blob/main/docs/changelog.rst) - [Commits](https://github.com/chardet/chardet/compare/6.0.0.post1...7.4.0.post2) Updates `curl-cffi` from 0.15.0b4 to 0.15.0rc1 - [Release notes](https://github.com/lexiforest/curl_cffi/releases) - [Changelog](https://github.com/lexiforest/curl_cffi/blob/main/docs/changelog.rst) - [Commits](https://github.com/lexiforest/curl_cffi/compare/v0.15.0b4...v0.15.0rc1) Updates `dashscope` from 1.25.12 to 1.25.15 Updates `google-genai` from 1.65.0 to 1.69.0 - [Release notes](https://github.com/googleapis/python-genai/releases) - [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md) - [Commits](https://github.com/googleapis/python-genai/compare/v1.65.0...v1.69.0) Updates `openai` from 2.24.0 to 2.30.0 - [Release notes](https://github.com/openai/openai-python/releases) - [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md) - [Commits](https://github.com/openai/openai-python/compare/v2.24.0...v2.30.0) Updates `puremagic` from 2.0.0 to 2.1.1 - [Release notes](https://github.com/cdgriffith/puremagic/releases) - [Changelog](https://github.com/cdgriffith/puremagic/blob/master/CHANGELOG.md) - [Commits](https://github.com/cdgriffith/puremagic/compare/2.0.0...2.1.1) Updates `pyrotgfork` from 2.2.19 to 2.2.21 - [Commits](https://github.com/TelegramPlayGround/Pyrogram/compare/v2.2.19...v2.2.21) --- updated-dependencies: - dependency-name: anthropic dependency-version: 0.86.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-deps - dependency-name: chardet dependency-version: 7.4.0.post2 dependency-type: direct:production update-type: version-update:semver-major dependency-group: python-deps - dependency-name: curl-cffi dependency-version: 0.15.0rc1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-deps - dependency-name: dashscope dependency-version: 1.25.15 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-deps - dependency-name: google-genai dependency-version: 1.69.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-deps - dependency-name: openai dependency-version: 2.30.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-deps - dependency-name: puremagic dependency-version: 2.1.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: python-deps - dependency-name: pyrotgfork dependency-version: 2.2.21 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-deps ... Signed-off-by: dependabot[bot] <support@github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: benny-dou <60535774+benny-dou@users.noreply.github.com>
1 parent 6f5af59
src/ai/texts/claude.py
@@ -9,14 +9,14 @@ from anthropic import AsyncAnthropic, DefaultAioHttpClient
 from glom import Coalesce, glom
 from loguru import logger
 from pyrogram.client import Client
-from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM
+from pyrogram.parser.markdown import BLOCKQUOTE_DELIM
 from pyrogram.types import Message, ReplyParameters
 
 from ai.texts.contexts import get_anthropic_contexts
 from ai.utils import BOT_TIPS, EMOJI_REASONING_BEGIN, EMOJI_TEXT_BOT, beautify_llm_response, literal_eval, trim_none
 from config import AI, PROXY, TEXT_LENGTH
 from messages.progress import modify_progress
-from messages.utils import blockquote, count_without_entities, delete_message, smart_split
+from messages.utils import blockquote, count_without_entities, delete_message, quote, smart_split
 from utils import number_to_emoji, rand_string, strings_list
 
 
@@ -157,9 +157,9 @@ async def single_api_response(
                     is_reasoning = False
 
                 if response_type == "thinking" and len(thoughts) == 0:  # 首次收到推理内容
-                    runtime_texts += f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{chunk_thinking.lstrip()}"
+                    runtime_texts += quote(f"{EMOJI_REASONING_BEGIN}{chunk_thinking.lstrip()}")
                 elif chunk_thinking:  # 收到推理内容
-                    runtime_texts += chunk_thinking
+                    runtime_texts += chunk_thinking.replace("\n", f"\n{BLOCKQUOTE_DELIM}")
 
                 if response_type == "text":  # 收到初始回答
                     runtime_texts = chunk_answer.lstrip()
@@ -179,13 +179,11 @@ async def single_api_response(
                     if len(parts) == 1:
                         continue
                     if is_reasoning:
-                        runtime_texts = f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{parts[-1].lstrip()}"  # remove previous thinking
+                        runtime_texts = quote(f"{EMOJI_REASONING_BEGIN}{parts[-1].lstrip()}")  # remove previous thinking
                         await modify_progress(message=status_msg, text=parts[0], force_update=True)  # force send the first part
                     else:
                         await modify_progress(message=status_msg, text=blockquote(parts[0]), force_update=True)  # force send the first part
                         runtime_texts = parts[-1]  # keep the last part
-                        if is_reasoning:
-                            runtime_texts = f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{runtime_texts.lstrip()}"
                         if not silent:
                             status_msg = await client.send_message(status_cid, text=prefix + runtime_texts, reply_parameters=ReplyParameters(message_id=status_mid))  # the new message
                             sent_messages.append(status_msg)
src/ai/texts/gemini.py
@@ -8,14 +8,14 @@ from google import genai
 from google.genai import types
 from loguru import logger
 from pyrogram.client import Client
-from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM
+from pyrogram.parser.markdown import BLOCKQUOTE_DELIM
 from pyrogram.types import Message, ReplyParameters
 
 from ai.texts.contexts import get_gemini_contexts
 from ai.utils import BOT_TIPS, EMOJI_REASONING_BEGIN, EMOJI_TEXT_BOT, beautify_llm_response, literal_eval, trim_none
 from config import AI, PROXY, TEXT_LENGTH
 from messages.progress import modify_progress
-from messages.utils import blockquote, count_without_entities, smart_split
+from messages.utils import blockquote, count_without_entities, quote, smart_split
 from networking import flatten_rediercts
 from utils import number_to_emoji, strings_list
 
@@ -55,7 +55,7 @@ async def gemini_chat_completion(
         try:
             http_options = types.HttpOptions(base_url=gemini_base_url, async_client_args={"proxy": gemini_proxy}, headers=literal_eval(gemini_default_headers))
             gemini = genai.Client(api_key=api_key, http_options=http_options)
-            params = {"model": model_id, "contents": await get_gemini_contexts(client, message, gemini)}
+            params: dict = {"model": model_id, "contents": await get_gemini_contexts(client, message, gemini)}
             if conf := literal_eval(gemini_generate_content_config):
                 params["config"] = conf
             logger.debug(f"genai.Client().models.generate_content_stream(**{params})")
@@ -114,8 +114,8 @@ async def single_api_generate_content(
     sent_messages = []
     resp = {}
     try:
-        is_reasoning = False
-        reasoning_chat_flag = None  # 用于指示是否是推理对话
+        reasoning_chat_flag = None  # 是否是推理模型
+        is_reasoning = False  # 是否正在推理
         async for chunk in await gemini.aio.models.generate_content_stream(**params):
             resp = parse_chunk(chunk)
             chunk_answer = resp.get("texts", "")
@@ -124,9 +124,9 @@ async def single_api_generate_content(
                 reasoning_chat_flag = True
             if chunk_thinking and not is_reasoning:  # 首次收到推理内容
                 is_reasoning = True
-                runtime_texts += f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{chunk_thinking.lstrip()}"
+                runtime_texts += quote(f"{EMOJI_REASONING_BEGIN}{chunk_thinking.lstrip()}")
             elif chunk_thinking and is_reasoning:  # 收到推理内容且正在思考
-                runtime_texts += chunk_thinking
+                runtime_texts += chunk_thinking.replace("\n", f"\n{BLOCKQUOTE_DELIM}")
             elif reasoning_chat_flag is True and is_reasoning:  # Receiving response, close reasoning flag
                 is_reasoning = False
                 runtime_texts = chunk_answer.lstrip()
@@ -143,13 +143,11 @@ async def single_api_generate_content(
                 if len(parts) == 1:
                     continue
                 if is_reasoning:
-                    runtime_texts = f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{parts[-1].lstrip()}"  # remove previous thinking
+                    runtime_texts = quote(f"{EMOJI_REASONING_BEGIN}{parts[-1].lstrip()}")  # remove previous thinking
                     await modify_progress(message=status_msg, text=parts[0], force_update=True)  # force send the first part
                 else:
                     await modify_progress(message=status_msg, text=blockquote(parts[0]), force_update=True)  # force send the first part
                     runtime_texts = parts[-1]  # keep the last part
-                    if is_reasoning:
-                        runtime_texts = f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{runtime_texts.lstrip()}"
                     if not silent:
                         status_msg = await client.send_message(status_cid, text=prefix + runtime_texts, reply_parameters=ReplyParameters(message_id=status_mid))  # the new message
                         sent_messages.append(status_msg)
src/ai/texts/openai_chat.py
@@ -1,20 +1,19 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 import contextlib
-import re
 
 from glom import glom
 from loguru import logger
 from openai import AsyncOpenAI, DefaultAsyncHttpxClient
 from pyrogram.client import Client
-from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
+from pyrogram.parser.markdown import BLOCKQUOTE_DELIM
 from pyrogram.types import Message, ReplyParameters
 
 from ai.texts.contexts import get_openai_completion_contexts
-from ai.utils import BOT_TIPS, EMOJI_REASONING_BEGIN, EMOJI_REASONING_END, EMOJI_TEXT_BOT, beautify_llm_response, literal_eval, split_reasoning, trim_none
+from ai.utils import BOT_TIPS, EMOJI_REASONING_BEGIN, EMOJI_TEXT_BOT, beautify_llm_response, literal_eval, split_reasoning, trim_none
 from config import AI, PROXY, TEXT_LENGTH
 from messages.progress import modify_progress
-from messages.utils import blockquote, count_without_entities, delete_message, smart_split
+from messages.utils import blockquote, count_without_entities, delete_message, quote, smart_split
 from utils import strings_list
 
 
@@ -133,8 +132,8 @@ async def single_api_chat_completions(
     sent_messages = []
     resp = ""
     try:
-        is_reasoning = False
-        reasoning_chat_flag = None  # 用于指示是否是推理对话
+        reasoning_chat_flag = None  # 是否是推理模型
+        is_reasoning = False  # 是否正在推理
         async for chunk in await openai.chat.completions.create(**params):
             resp = trim_none(chunk.model_dump())
             logger.trace(resp)
@@ -148,24 +147,15 @@ async def single_api_chat_completions(
                 reasoning_chat_flag = True
             if chunk_thinking and not is_reasoning:  # 首次收到推理内容
                 is_reasoning = True
-                runtime_texts += f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{chunk_thinking.lstrip()}"
+                runtime_texts += quote(f"{EMOJI_REASONING_BEGIN}{chunk_thinking.lstrip()}")
             elif chunk_thinking and is_reasoning:  # 收到推理内容且正在思考
-                runtime_texts += chunk_thinking
+                runtime_texts += chunk_thinking.replace("\n", f"\n{BLOCKQUOTE_DELIM}")
             elif reasoning_chat_flag is True and is_reasoning:  # 收到回答, 关闭推理标志
                 is_reasoning = False
                 runtime_texts = chunk_answer.lstrip()
             else:
                 runtime_texts += chunk_answer
 
-            # Sometimes the reasoning content is included in the content field.
-            # handle "<think>...</think>\n\n"
-            if runtime_texts.removeprefix(prefix).lstrip().startswith("<think>"):
-                is_reasoning = True
-                runtime_texts = runtime_texts.replace("<think>", f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}")
-            if "</think>" in runtime_texts:
-                is_reasoning = False
-                runtime_texts = re.sub(r"</think>\s*", f"{EMOJI_REASONING_END}\n{BLOCKQUOTE_EXPANDABLE_END_DELIM}", runtime_texts, count=1)
-
             thoughts += chunk_thinking
             answers += chunk_answer
             runtime_texts = beautify_llm_response(runtime_texts)
@@ -178,13 +168,11 @@ async def single_api_chat_completions(
                 if len(parts) == 1:
                     continue
                 if is_reasoning:
-                    runtime_texts = f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{parts[-1].lstrip()}"  # remove previous thinking
+                    runtime_texts = quote(f"{EMOJI_REASONING_BEGIN}{parts[-1].lstrip()}")  # remove previous thinking
                     await modify_progress(message=status_msg, text=parts[0], force_update=True)  # force send the first part
                 else:
                     await modify_progress(message=status_msg, text=blockquote(parts[0]), force_update=True)  # force send the first part
                     runtime_texts = parts[-1]  # keep the last part
-                    if is_reasoning:
-                        runtime_texts = f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{runtime_texts.lstrip()}"
                     if not silent:
                         status_msg = await client.send_message(status_cid, text=prefix + runtime_texts, reply_parameters=ReplyParameters(message_id=status_mid))  # the new message
                         sent_messages.append(status_msg)
src/ai/texts/openai_response.py
@@ -8,7 +8,7 @@ from glom import Coalesce, glom
 from loguru import logger
 from openai import AsyncOpenAI, DefaultAsyncHttpxClient
 from pyrogram.client import Client
-from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM
+from pyrogram.parser.markdown import BLOCKQUOTE_DELIM
 from pyrogram.types import Message, ReplyParameters
 
 from ai.texts.contexts import get_openai_response_contexts
@@ -17,7 +17,7 @@ from config import AI, PROXY, TEXT_LENGTH
 from database.r2 import set_cf_r2
 from messages.parser import get_thread_id
 from messages.progress import modify_progress
-from messages.utils import blockquote, count_without_entities, delete_message, smart_split
+from messages.utils import blockquote, count_without_entities, delete_message, quote, smart_split
 from utils import number_to_emoji, strings_list
 
 
@@ -203,10 +203,10 @@ async def single_api_response(
             }:  # 推理结束
                 is_reasoning = False
 
-            if response_type == "response.reasoning_summary_part.added":  # 首次收到推理内容
-                runtime_texts += f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{chunk_thinking.lstrip()}"
+            if response_type == "response.reasoning_summary_part.added" and len(thoughts) == 0:  # 首次收到推理内容
+                runtime_texts += quote(f"{EMOJI_REASONING_BEGIN}{chunk_thinking.lstrip()}")
             elif chunk_thinking:  # 收到推理内容
-                runtime_texts += chunk_thinking
+                runtime_texts += chunk_thinking.replace("\n", f"\n{BLOCKQUOTE_DELIM}")
 
             if response_type == "response.content_part.added":  # 收到初始回答
                 runtime_texts = chunk_answer.lstrip()
@@ -223,13 +223,11 @@ async def single_api_response(
                 if len(parts) == 1:
                     continue
                 if is_reasoning:
-                    runtime_texts = f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{parts[-1].lstrip()}"  # remove previous thinking
+                    runtime_texts = quote(f"{EMOJI_REASONING_BEGIN}{parts[-1].lstrip()}")  # remove previous thinking
                     await modify_progress(message=status_msg, text=parts[0], force_update=True)  # force send the first part
                 else:
                     await modify_progress(message=status_msg, text=blockquote(parts[0]), force_update=True)  # force send the first part
                     runtime_texts = parts[-1]  # keep the last part
-                    if is_reasoning:
-                        runtime_texts = f"{BLOCKQUOTE_EXPANDABLE_DELIM}{EMOJI_REASONING_BEGIN}{runtime_texts.lstrip()}"
                     if not silent:
                         status_msg = await client.send_message(status_cid, text=prefix + runtime_texts, reply_parameters=ReplyParameters(message_id=status_mid))  # the new message
                         sent_messages.append(status_msg)
src/ai/utils.py
@@ -12,7 +12,7 @@ from glom import glom
 from google import genai
 from google.genai.types import HttpOptions
 from loguru import logger
-from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM
 
 from config import AI, PREFIX, PROXY
 from database.kv import get_cf_kv
@@ -80,8 +80,7 @@ def clean_bot_tips(text: str) -> str:
 
 def clean_reasoning(text: str) -> str:
     text = re.sub(rf"{EMOJI_REASONING_BEGIN}(.*?){EMOJI_REASONING_END}", "", text.strip(), flags=re.DOTALL).strip()
-    text = text.removeprefix(BLOCKQUOTE_EXPANDABLE_DELIM).lstrip()
-    return text.removeprefix(BLOCKQUOTE_EXPANDABLE_END_DELIM).lstrip()
+    return text.replace(BLOCKQUOTE_EXPANDABLE_DELIM, "").strip()
 
 
 def clean_context(text: str) -> str:
src/history/query.py
@@ -6,7 +6,7 @@ from io import BytesIO
 from glom import glom
 from loguru import logger
 from pyrogram.client import Client
-from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM
 from pyrogram.types import Message, User
 
 from config import HISTORY, PREFIX, TZ, cache
@@ -32,15 +32,14 @@ HELP = f"""🗣**查询当前对话聊天记录**
 4.`/hist + 日期 + @用户名 + 关键词` (日期需放在最前面)
 示例:
 {BLOCKQUOTE_EXPANDABLE_DELIM}`/hist 你好`: 查询包含“你好”关键词的记录
-`/hist 2025-01-01 你好`: 查询2025-01-01日包含“你好”的记录
-`/hist @张三 你好`: 查询用户【张三】包含“你好”的记录
-`/hist 2025 @张三 你好`: 查询2025年用户【张三】包含“你好”的记录
-
-注意:
-- 用户名和关键词需要区分大小写
-- 用户名可以为昵称 (Name)、用户名 (@username)、用户的TelegramUID
-- 如果用户名中有空格, 请去除空格。例如: 想指定用户为John Doe请使用 `@JohnDoe`
-{BLOCKQUOTE_EXPANDABLE_END_DELIM}
+{BLOCKQUOTE_EXPANDABLE_DELIM}`/hist 2025-01-01 你好`: 查询2025-01-01日包含“你好”的记录
+{BLOCKQUOTE_EXPANDABLE_DELIM}`/hist @张三 你好`: 查询用户【张三】包含“你好”的记录
+{BLOCKQUOTE_EXPANDABLE_DELIM}`/hist 2025 @张三 你好`: 查询2025年用户【张三】包含“你好”的记录
+{BLOCKQUOTE_EXPANDABLE_DELIM}
+{BLOCKQUOTE_EXPANDABLE_DELIM}注意:
+{BLOCKQUOTE_EXPANDABLE_DELIM}- 用户名和关键词需要区分大小写
+{BLOCKQUOTE_EXPANDABLE_DELIM}- 用户名可以为昵称 (Name)、用户名 (@username)、用户的TelegramUID
+{BLOCKQUOTE_EXPANDABLE_DELIM}- 如果用户名中有空格, 请去除空格。例如: 想指定用户为John Doe请使用 `@JohnDoe`
 `/history` 使用说明:
 查询所有对话的聊天记录
 但出于隐私考虑, 本命令会限制使用权限
src/messages/sender.py
@@ -7,13 +7,12 @@ from pathlib import Path
 from loguru import logger
 from pyrogram.client import Client
 from pyrogram.errors import FloodWait
-from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
 from pyrogram.types import Message, ReplyParameters
 
 from config import CAPTION_LENGTH, TID
 from messages.preprocess import preprocess_media, warp_media_group
 from messages.progress import modify_progress, telegram_uploading
-from messages.utils import delete_message, get_reply_to, smart_split, summay_media, warp_comments
+from messages.utils import delete_message, get_reply_to, smart_split, summay_media
 from utils import to_int
 
 
@@ -73,7 +72,7 @@ async def send2tg(
     media = await preprocess_media(media)
     texts = texts or ""
     if comments:  # append comments to texts
-        texts = texts + "".join(comments) + BLOCKQUOTE_EXPANDABLE_END_DELIM
+        texts = texts + "".join(comments)
 
     if send_from_user:  # prefix send_from_user
         texts = f"{send_from_user}{texts.strip()}"
@@ -88,7 +87,6 @@ async def send2tg(
 
     caption = (await smart_split(texts, CAPTION_LENGTH))[0]
     remaining_texts = texts.removeprefix(caption)
-    caption = warp_comments(caption)
     if 1 < len(media) <= 10:
         group = await warp_media_group(media, caption=caption, caption_above=caption_above)
         sent_messages.extend(await send_media_group(client, target_chat, group, reply_parameters))
@@ -130,14 +128,8 @@ async def send_texts(
     for idx, msg in enumerate(await smart_split(texts.strip())):
         if not msg:
             continue
-        texts = warp_comments(msg)
-        # we do not send comments only texts
-        if (
-            texts.startswith(BLOCKQUOTE_EXPANDABLE_DELIM)
-            and texts.endswith(BLOCKQUOTE_EXPANDABLE_END_DELIM)
-            and texts.count(BLOCKQUOTE_EXPANDABLE_DELIM) == 1
-            and texts.count(BLOCKQUOTE_EXPANDABLE_END_DELIM) == 1
-        ):
+        # we do not send comments-only texts
+        if all(s.startswith("**>") for s in msg.split("\n") if s):
             continue
         if idx != 0:
             reply_parameters = ReplyParameters()
@@ -168,7 +160,6 @@ async def send_single_media(
     logger.trace(f"Sending single media with {len(texts)} texts")
     caption = (await smart_split(texts, CAPTION_LENGTH))[0]
     remaining_texts = texts.removeprefix(caption)
-    caption = warp_comments(caption)
     message = None
     try:
         if media.get("photo"):
src/messages/utils.py
@@ -6,7 +6,7 @@ import re
 from loguru import logger
 from pyrogram.client import Client
 from pyrogram.enums import ParseMode
-from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
+from pyrogram.parser.markdown import BLOCKQUOTE_DELIM, BLOCKQUOTE_EXPANDABLE_DELIM
 from pyrogram.parser.parser import Parser
 from pyrogram.types import Message, ReactionTypeEmoji, ReplyParameters
 
@@ -159,8 +159,7 @@ async def smart_split(text: str, chars_per_string: int = TEXT_LENGTH, mode: Pars
             return strings[: matched.end()]
         return strings
 
-    # for some reason, we may need to prepend `BLOCKQUOTE_EXPANDABLE_DELIM` or append `BLOCKQUOTE_EXPANDABLE_END_DELIM`
-    chars_per_string = chars_per_string - len(BLOCKQUOTE_EXPANDABLE_DELIM) - len(BLOCKQUOTE_EXPANDABLE_END_DELIM)
+    chars_per_string = chars_per_string - len(BLOCKQUOTE_EXPANDABLE_DELIM) * text.count(BLOCKQUOTE_EXPANDABLE_DELIM)
     parts = []
     while True:
         if await count_without_entities(text, mode) < chars_per_string:
@@ -182,34 +181,14 @@ async def smart_split(text: str, chars_per_string: int = TEXT_LENGTH, mode: Pars
 
 def blockquote(s: str) -> str:
     """Block quote texts."""
-    s = s.replace(BLOCKQUOTE_EXPANDABLE_DELIM, "").replace(BLOCKQUOTE_EXPANDABLE_END_DELIM, "")
-    return BLOCKQUOTE_EXPANDABLE_DELIM + s + "\n" + BLOCKQUOTE_EXPANDABLE_END_DELIM
+    s = s.replace(BLOCKQUOTE_EXPANDABLE_DELIM, "")
+    return BLOCKQUOTE_EXPANDABLE_DELIM + s.replace("\n", f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}")
 
 
-def warp_comments(texts: str) -> str:
-    texts = texts or ""
-
-    start_idx = texts.find(BLOCKQUOTE_EXPANDABLE_DELIM)
-    end_idx = texts.find(BLOCKQUOTE_EXPANDABLE_END_DELIM)
-    if start_idx == -1 and end_idx == -1:  # no comments
-        return texts
-
-    # <comments><end_delim> ... <content> ... <start_delim><comments>
-    if start_idx > -1 and end_idx > -1 and end_idx < start_idx:
-        texts = texts.removeprefix(BLOCKQUOTE_EXPANDABLE_DELIM).removesuffix(BLOCKQUOTE_EXPANDABLE_END_DELIM)
-        return blockquote(texts)
-
-    # <content> ... <start_delim><comments>
-    if start_idx > -1 and end_idx == -1:
-        texts = texts.removesuffix(BLOCKQUOTE_EXPANDABLE_END_DELIM)
-        return texts + BLOCKQUOTE_EXPANDABLE_END_DELIM
-
-    # <comments><end_delim> ... <content>
-    if start_idx == -1 and end_idx > -1:
-        texts = texts.removeprefix(BLOCKQUOTE_EXPANDABLE_DELIM)
-        return BLOCKQUOTE_EXPANDABLE_DELIM + texts
-
-    return texts
+def quote(s: str) -> str:
+    """Quote texts."""
+    s = s.removeprefix(BLOCKQUOTE_DELIM)
+    return BLOCKQUOTE_DELIM + s.replace("\n", f"\n{BLOCKQUOTE_DELIM}")
 
 
 async def sent_from_me(client: Client, message: Message) -> bool:
src/preview/bilibili.py
@@ -82,7 +82,7 @@ async def preview_bilibili(
 
 
 @cache.memoize(ttl=30)
-async def parse_bilibili_opus(post_id: str, **kwargs) -> dict:  # type: ignore
+async def parse_bilibili_opus(post_id: str, **kwargs) -> dict:
     try:
         op = opus.Opus(int(post_id))
         resp = await op.get_info()
@@ -266,7 +266,7 @@ async def get_bilibili_comments(url_or_vid: int | str) -> list[str]:
             if cmt := glom(x, "content.message", default=""):
                 if idx == 0:
                     comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:")
-                comments.append(f"\n💬**{name}**{location}: {emojify(cmt)}")
+                comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**{name}**{location}: {emojify(cmt)}")
     except Exception as e:
         logger.error(f"Failed to get Bilibili comments: {e}")
         return []
src/preview/douyin.py
@@ -93,7 +93,7 @@ async def preview_douyin(
     comments = []
     if comments_list := await get_comments(data["aweme_id"], platform, douyin_comments_provider):
         comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:")
-        comments.extend(f"\n💬**{cmt['name']}**{cmt['region']}: {cmt['text']}" for cmt in comments_list)
+        comments.extend(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**{cmt['name']}**{cmt['region']}: {cmt['text']}" for cmt in comments_list)
 
     sent_messages = await send2tg(client, message, texts=emojify(texts), media=data.get("media", []), comments=comments, **kwargs)
     await modify_progress(del_status=True, **kwargs)
src/preview/instagram.py
@@ -110,7 +110,7 @@ async def preview_instagram(
         comment_list = [{"author": glom(node, "node.owner.username", default="user"), "text": glom(node, "node.text", default="")} for node in comment_nodes]
         if comment_list := [x for x in comment_list if x["text"]]:
             comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:")
-            comments.extend(f"\n💬**[{cmt['author']}](https://www.instagram.com/{cmt['author']})**: {cmt['text']}" for cmt in comment_list)
+            comments.extend(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**[{cmt['author']}](https://www.instagram.com/{cmt['author']})**: {cmt['text']}" for cmt in comment_list)
 
     await modify_progress(text=f"⏬正在下载:\n{summay_media(media)}", force_update=True, **kwargs)
     media = await download_media(media, **kwargs)
@@ -151,11 +151,11 @@ async def preview_ddinstagram(client: Client, message: Message, url: str, post_t
     texts = ""
     media = {}
     if tag := soup.find("meta", attrs={"property": "twitter:title"}):
-        author = tag.get("content", "Unknown")  # type: ignore
+        author = tag.get("content", "Unknown")
         texts += f"🏞**[{author}]({url})\n"
     if tag := soup.find("meta", attrs={"property": "og:description"}):
-        texts += tag.get("content", "")  # type: ignore
-    if (tag := soup.find("meta", attrs={"property": "twitter:image"})) and (img_url := tag.get("content")):  # type: ignore
+        texts += str(tag.get("content", ""))
+    if (tag := soup.find("meta", attrs={"property": "twitter:image"})) and (img_url := tag.get("content")):
         raw_url = f"{API.DDINSTAGRAM}{img_url}"
         media["photo"] = await download_file(raw_url, path=f"{DOWNLOAD_DIR}/{post_id}.jpg", proxy=PROXY.INSTAGRAM, **kwargs)
         if not bool(validate_img(media["photo"])):
@@ -163,7 +163,7 @@ async def preview_ddinstagram(client: Client, message: Message, url: str, post_t
             return
 
     if tag := soup.find("meta", attrs={"property": "og:video"}):
-        video_url = tag.get("content", "")  # type: ignore
+        video_url = tag.get("content", "")
         if video_url:
             raw_url = f"{API.DDINSTAGRAM}{video_url}"
             media["video"] = await download_file(raw_url, path=f"{DOWNLOAD_DIR}/{post_id}.mp4", proxy=PROXY.INSTAGRAM, **kwargs)
src/preview/reddit.py
@@ -86,7 +86,7 @@ async def get_reddit_info(url: str, **kwargs) -> dict:
                 continue
             if comment == "[removed]" or has_markdown_img(comment):
                 continue
-            comments.append(f"\n💬**[{author}]({author_url})**: {comment}")
+            comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**[{author}]({author_url})**: {comment}")
         if comments:
             comments.insert(0, f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:")
         await modify_progress(text=f"⏬正在下载:\n{summay_media(media)}", force_update=True, **kwargs)
src/preview/twitter.py
@@ -8,7 +8,7 @@ from zoneinfo import ZoneInfo
 from glom import glom
 from loguru import logger
 from pyrogram.client import Client
-from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
+from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM
 from pyrogram.types import Message
 
 from bridge.social import send_to_social_media_bridge
@@ -169,8 +169,7 @@ async def preview_twitter(
             for cmt in comments:
                 if str(cmt["post_id"]) == str(this_info["post_id"]):
                     continue
-                msg += f"\n💬**{cmt['author']}**: {cmt['text']}"
-            msg += BLOCKQUOTE_EXPANDABLE_END_DELIM
+                msg += f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**{cmt['author']}**: {cmt['text']}"
         media.extend(master_media)
 
     # 本条推文
@@ -197,8 +196,7 @@ async def preview_twitter(
         msg += f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:"
         for cmt in comments:
             cmt_texts = cmt["text"].strip().removeprefix(f"@{master_handle}").strip()  # 有时回推的comment前会附带被回推的handle, 这里去掉
-            msg += f"\n💬**{cmt['author']}**: {cmt_texts}"
-        msg += BLOCKQUOTE_EXPANDABLE_END_DELIM
+            msg += f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**{cmt['author']}**: {cmt_texts}"
 
     # 引用推文
     if quote_info:
src/preview/wechat.py
@@ -6,7 +6,6 @@ from urllib.parse import quote_plus
 
 from loguru import logger
 from pyrogram.client import Client
-from pyrogram.parser.markdown import BLOCKQUOTE_EXPANDABLE_DELIM, BLOCKQUOTE_EXPANDABLE_END_DELIM
 from pyrogram.types import Message
 
 from config import API, CAPTION_LENGTH, DB, DOWNLOAD_DIR, PROXY, TEXT_LENGTH, TOKEN
@@ -14,7 +13,7 @@ from database.database import get_db
 from messages.database import copy_messages_from_db, save_messages
 from messages.progress import modify_progress
 from messages.sender import send2tg
-from messages.utils import count_without_entities, summay_media
+from messages.utils import blockquote, count_without_entities, summay_media
 from networking import download_file, download_media, hx_req
 from publish import publish_telegraph
 from utils import nowstr, rand_string
@@ -47,7 +46,7 @@ async def preview_wechat(client: Client, message: Message, url: str = "", db_key
     length = await count_without_entities(post_info["header"] + post_info["markdown"])
     if not post_info.get("media"):  # 无图片
         if length < TEXT_LENGTH - 8:  # 无图片短文
-            texts = f"{post_info['header']}\n{BLOCKQUOTE_EXPANDABLE_DELIM}{post_info['markdown']}\n{BLOCKQUOTE_EXPANDABLE_END_DELIM}"
+            texts = f"{post_info['header']}\n{blockquote(post_info['markdown'])}"
             sent_messages.extend(await send2tg(client, message, texts=texts, **kwargs))
         else:  # 无图片长文
             texts = f"{post_info['header']}"
@@ -56,7 +55,7 @@ async def preview_wechat(client: Client, message: Message, url: str = "", db_key
                 texts += f"\n⚡️[即时预览]({telegraph_url})"
             sent_messages.extend(await send2tg(client, message, texts=texts, media=[{"document": post_info["html_path"]}], **kwargs))
     elif length < CAPTION_LENGTH - 8:  # 有图片短文
-        texts = f"{post_info['header']}\n{BLOCKQUOTE_EXPANDABLE_DELIM}{post_info['markdown']}\n{BLOCKQUOTE_EXPANDABLE_END_DELIM}"
+        texts = f"{post_info['header']}\n{blockquote(post_info['markdown'])}"
         sent_messages.extend(await send2tg(client, message, texts=texts, media=post_info["media"], **kwargs))
     else:  # 有图片长文
         texts = f"{post_info['header']}"
src/preview/weibo.py
@@ -145,7 +145,7 @@ async def preview_weibo(
 
 
 @cache.memoize(ttl=30)
-async def parse_weibo_info(post_id: str, data: dict | None = None, **kwargs) -> dict:  # type: ignore
+async def parse_weibo_info(post_id: str, data: dict | None = None, **kwargs) -> dict:
     info = {}
     if not data:
         weibo_url = f"https://m.weibo.cn/detail/{post_id}"
@@ -292,9 +292,9 @@ async def parse_weibo_comments(post_id: str) -> list[str]:
         uid = glom(info, "user.id", default="")
         author = glom(info, "user.screen_name", default="")
         if author and uid:
-            cmt += f"💬**[{author}](https://weibo.com/u/{uid})**"
+            cmt += f"{BLOCKQUOTE_EXPANDABLE_DELIM}💬**[{author}](https://weibo.com/u/{uid})**"
         elif author:
-            cmt += f"💬**{author}**"
+            cmt += f"{BLOCKQUOTE_EXPANDABLE_DELIM}💬**{author}**"
         if region := info.get("source", "").removeprefix("来自"):
             cmt += f"({region})"
         cmt += ":"
src/preview/youtube.py
@@ -40,7 +40,7 @@ async def get_youtube_comments(vid: str | None) -> list[str]:
             if cmt := glom(x, "snippet.topLevelComment.snippet.textDisplay", default=""):
                 if idx == 0:
                     comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**点此展开评论区**:")
-                comments.append(f"\n💬**{name}**: {cmt}")
+                comments.append(f"\n{BLOCKQUOTE_EXPANDABLE_DELIM}💬**{name}**: {cmt}")
     except Exception as e:
         logger.error(f"Failed to get YouTube comments: {e}")
         return []
@@ -76,7 +76,7 @@ async def get_youtube_vinfo(video_id: str) -> dict:
     """
     if not video_id:
         return {"downloadable": False, "error_msg": "❌未提供VideoID"}
-    info = {"downloadable": False, "error_msg": "❌无法获取此视频信息"}
+    info: dict = {"downloadable": False, "error_msg": "❌无法获取此视频信息"}
     try:
         logger.info(f"Fetch YouTube video info for {video_id=}, proxy={PROXY.GOOGLE}")
         api = "https://www.googleapis.com/youtube/v3/videos"
src/ytdlp/main.py
@@ -20,7 +20,7 @@ from messages.database import copy_messages_from_db, save_messages
 from messages.preprocess import preprocess_media
 from messages.progress import modify_progress, telegram_uploading
 from messages.sender import send2tg
-from messages.utils import count_without_entities, get_reply_to, smart_split, warp_comments
+from messages.utils import count_without_entities, get_reply_to, smart_split
 from multimedia import convert_to_h264
 from preview.bilibili import get_bilibili_comments, get_bilibili_vinfo, make_bvid_clickable
 from preview.youtube import get_youtube_comments, get_youtube_vinfo
@@ -338,7 +338,7 @@ async def send_media(
             video_messages.append(
                 await client.send_video(
                     chat_id=to_int(video_target),
-                    caption=warp_comments(caption),
+                    caption=caption,
                     reply_parameters=reply_parameters,
                     progress=telegram_uploading,
                     progress_args=(kwargs.get("progress", False), video["video"], true(kwargs.get("detail_progress"))),  # message, path, detail_progress
@@ -352,7 +352,7 @@ async def send_media(
         audio_message = await client.send_audio(
             chat_id=to_int(audio_target),
             audio=audio_path.as_posix(),
-            caption=warp_comments(caption),
+            caption=caption,
             performer=info["author"],
             title=info["title"],
             duration=round(float(info.get("duration", "0"))),
pyproject.toml
@@ -1,20 +1,20 @@
 [project]
 dependencies = [
   "aioboto3==15.5.0",
-  "anthropic==0.84.0",
+  "anthropic==0.86.0",
   "apscheduler>=3.11.0,<4.0.0",
   "beautifulsoup4==4.14.3",
   "bilibili-api-python==17.4.1",
   "brotli==1.2.0",
   "cacheout==0.16.0",
-  "chardet==6.0.0.post1",
-  "curl-cffi==0.15.0b4",
+  "chardet==7.4.0.post2",
+  "curl-cffi==0.15.0rc1",
   "cutword==0.1.1",
-  "dashscope==1.25.12",
+  "dashscope==1.25.15",
   "feedgen==1.0.0",
   "feedparser==6.0.12",
   "glom==25.12.0",
-  "google-genai==1.65.0",
+  "google-genai==1.69.0",
   "httpx-aiohttp==0.1.12",
   "httpx-curl-cffi==0.1.5",
   "httpx[http2,socks]==0.28.1",
@@ -22,13 +22,13 @@ dependencies = [
   "markdown==3.10.2",
   "markitdown[docx,pdf,pptx,xls,xlsx]==0.1.5",
   "onnxruntime>=1.23.2",
-  "openai==2.24.0",
+  "openai==2.30.0",
   "orjson==3.11.7",
   "pathvalidate==3.3.1",
   "pillow-heif==1.3.0",
   "pillow>=11.2.1",
-  "puremagic==2.0.0",
-  "pyrotgfork==2.2.19",
+  "puremagic==2.1.1",
+  "pyrotgfork==2.2.21",
   "pysocks==1.7.1",
   "pytgcrypto>=1.2.12",
   "python-ffmpeg",
uv.lock
@@ -138,7 +138,7 @@ wheels = [
 
 [[package]]
 name = "anthropic"
-version = "0.84.0"
+version = "0.86.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "anyio", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
@@ -150,9 +150,9 @@ dependencies = [
     { name = "sniffio", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
     { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/04/ea/0869d6df9ef83dcf393aeefc12dd81677d091c6ffc86f783e51cf44062f2/anthropic-0.84.0.tar.gz", hash = "sha256:72f5f90e5aebe62dca316cb013629cfa24996b0f5a4593b8c3d712bc03c43c37", size = 539457, upload-time = "2026-02-25T05:22:38.54Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/37/7a/8b390dc47945d3169875d342847431e5f7d5fa716b2e37494d57cfc1db10/anthropic-0.86.0.tar.gz", hash = "sha256:60023a7e879aa4fbb1fed99d487fe407b2ebf6569603e5047cfe304cebdaa0e5", size = 583820, upload-time = "2026-03-18T18:43:08.017Z" }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/64/ca/218fa25002a332c0aa149ba18ffc0543175998b1f65de63f6d106689a345/anthropic-0.84.0-py3-none-any.whl", hash = "sha256:861c4c50f91ca45f942e091d83b60530ad6d4f98733bfe648065364da05d29e7", size = 455156, upload-time = "2026-02-25T05:22:40.468Z" },
+    { url = "https://files.pythonhosted.org/packages/63/5f/67db29c6e5d16c8c9c4652d3efb934d89cb750cad201539141781d8eae14/anthropic-0.86.0-py3-none-any.whl", hash = "sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57", size = 469400, upload-time = "2026-03-18T18:43:06.526Z" },
 ]
 
 [[package]]
@@ -269,20 +269,20 @@ dev = [
 [package.metadata]
 requires-dist = [
     { name = "aioboto3", specifier = "==15.5.0" },
-    { name = "anthropic", specifier = "==0.84.0" },
+    { name = "anthropic", specifier = "==0.86.0" },
     { name = "apscheduler", specifier = ">=3.11.0,<4.0.0" },
     { name = "beautifulsoup4", specifier = "==4.14.3" },
     { name = "bilibili-api-python", specifier = "==17.4.1" },
     { name = "brotli", specifier = "==1.2.0" },
     { name = "cacheout", specifier = "==0.16.0" },
-    { name = "chardet", specifier = "==6.0.0.post1" },
-    { name = "curl-cffi", specifier = "==0.15.0b4" },
+    { name = "chardet", specifier = "==7.4.0.post2" },
+    { name = "curl-cffi", specifier = "==0.15.0rc1" },
     { name = "cutword", specifier = "==0.1.1" },
-    { name = "dashscope", specifier = "==1.25.12" },
+    { name = "dashscope", specifier = "==1.25.15" },
     { name = "feedgen", specifier = "==1.0.0" },
     { name = "feedparser", specifier = "==6.0.12" },
     { name = "glom", specifier = "==25.12.0" },
-    { name = "google-genai", specifier = "==1.65.0" },
+    { name = "google-genai", specifier = "==1.69.0" },
     { name = "httpx", extras = ["http2", "socks"], specifier = "==0.28.1" },
     { name = "httpx-aiohttp", specifier = "==0.1.12" },
     { name = "httpx-curl-cffi", specifier = "==0.1.5" },
@@ -290,13 +290,13 @@ requires-dist = [
     { name = "markdown", specifier = "==3.10.2" },
     { name = "markitdown", extras = ["docx", "pdf", "pptx", "xls", "xlsx"], specifier = "==0.1.5" },
     { name = "onnxruntime", specifier = ">=1.23.2" },
-    { name = "openai", specifier = "==2.24.0" },
+    { name = "openai", specifier = "==2.30.0" },
     { name = "orjson", specifier = "==3.11.7" },
     { name = "pathvalidate", specifier = "==3.3.1" },
     { name = "pillow", specifier = ">=11.2.1" },
     { name = "pillow-heif", specifier = "==1.3.0" },
-    { name = "puremagic", specifier = "==2.0.0" },
-    { name = "pyrotgfork", specifier = "==2.2.19" },
+    { name = "puremagic", specifier = "==2.1.1" },
+    { name = "pyrotgfork", specifier = "==2.2.21" },
     { name = "pysocks", specifier = "==1.7.1" },
     { name = "pytgcrypto", specifier = ">=1.2.12" },
     { name = "python-ffmpeg", url = "https://github.com/chadawagner/python-ffmpeg/archive/4614d8b7939679ea4d6ae9c32241d7607e2b136c.zip" },
@@ -450,11 +450,17 @@ wheels = [
 
 [[package]]
 name = "chardet"
-version = "6.0.0.post1"
+version = "7.4.0.post2"
 source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/7f/42/fb9436c103a881a377e34b9f58d77b5f503461c702ff654ebe86151bcfe9/chardet-6.0.0.post1.tar.gz", hash = "sha256:6b78048c3c97c7b2ed1fbad7a18f76f5a6547f7d34dbab536cc13887c9a92fa4", size = 12521798, upload-time = "2026-02-22T15:09:17.925Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/03/4b/1fe1ade6b4d33abff0224b45a8310775b04308668ad1bdef725af8e3fcaa/chardet-7.4.0.post2.tar.gz", hash = "sha256:21a6b5ca695252c03385dcfcc8b55c27907f1fe80838aa171b1ff4e356a1bb67", size = 767694, upload-time = "2026-03-29T18:07:23.19Z" }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/66/42/5de54f632c2de53cd3415b3703383d5fff43a94cbc0567ef362515261a21/chardet-6.0.0.post1-py3-none-any.whl", hash = "sha256:c894a36800549adf7bb5f2af47033281b75fdfcd2aa0f0243be0ad22a52e2dcb", size = 627245, upload-time = "2026-02-22T15:09:15.876Z" },
+    { url = "https://files.pythonhosted.org/packages/64/6f/40998484582edf32ebcbe30a51c0b33fb476aa4d22b172d4aabc3f47c5ed/chardet-7.4.0.post2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bdb9387e692dd53c837aa922f676e5ab51209895cd99b15d30c6004418e0d27", size = 854448, upload-time = "2026-03-29T18:07:02.432Z" },
+    { url = "https://files.pythonhosted.org/packages/32/ed/0fc7f4be6d346049bafec134cb4d122317e8e803b42e520f8214f02d9d13/chardet-7.4.0.post2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:422ac637f5a2a8b13151245591cb0fabdf9ec1427725f0560628cb5ad4fb1462", size = 838289, upload-time = "2026-03-29T18:07:04.026Z" },
+    { url = "https://files.pythonhosted.org/packages/27/ff/0f582b7a9369bba8abb47d72c3d1d1122c351b8fb04dcac2637683072bcb/chardet-7.4.0.post2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccdfb13b4a727d3d944157c7f350c6d64630511a0ce39e37ffa5114e90f7d3a7", size = 868537, upload-time = "2026-03-29T18:07:07.093Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/1e/8b5d54ecc873e828e9b91cddfce6bf5a058d7bb3d64007cfbbbc872b0bda/chardet-7.4.0.post2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5862b17677f7e8fcee4e37fe641f01d30762e4b075ac37ce9584e4407896e2d9", size = 853887, upload-time = "2026-03-29T18:07:12.156Z" },
+    { url = "https://files.pythonhosted.org/packages/26/17/8c2cf762c876b04036e561d2a27df8a6305435db1cb584f71c356e319c40/chardet-7.4.0.post2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:22d05c4b7e721d5330d99ef4a6f6233a9de58ae6f2275c21a098bedd778a6cb7", size = 838555, upload-time = "2026-03-29T18:07:13.689Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/b6/13cc503f45beeb1117fc9c83f294df16ebce5d75eac9f0cefb8cce4357a1/chardet-7.4.0.post2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2adfa7390e69cb5ed499b54978d31f6d476788d07d83da3426811181b7ca7682", size = 868868, upload-time = "2026-03-29T18:07:16.781Z" },
+    { url = "https://files.pythonhosted.org/packages/94/d2/22ac0b5b832bb9d2f29311dcded6c09ad0c32c23e3e53a8033aad5eb8652/chardet-7.4.0.post2-py3-none-any.whl", hash = "sha256:e0c9c6b5c296c0e5197bc8876fcc04d58a6ddfba18399e598ba353aba28b038e", size = 625322, upload-time = "2026-03-29T18:07:21.81Z" },
 ]
 
 [[package]]
@@ -542,22 +548,23 @@ wheels = [
 
 [[package]]
 name = "curl-cffi"
-version = "0.15.0b4"
+version = "0.15.0rc1"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "certifi", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
     { name = "cffi", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+    { name = "rich", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/18/38/e9cf1387d29345a9121ac0301ff5562111e73024bba88e05f40988da6c4c/curl_cffi-0.15.0b4.tar.gz", hash = "sha256:562d9a94924e822302304fd9c171a8b49f095ce953a6b77ecc79a11f135a4d4c", size = 182953, upload-time = "2026-02-26T04:13:10.023Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/a8/5291c86d73dfeb198c32c1f538d855bb775f8315c9af2760ff529b62ebdd/curl_cffi-0.15.0rc1.tar.gz", hash = "sha256:f7ea46aafec4f245eec3971e92bfb1e838ba5594d7bc004077445001a5f210f6", size = 196386, upload-time = "2026-03-30T10:58:26.254Z" }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/a6/ad/d8ee88cc6f71acbb8dcd9b1be06302cb5d31dbea2ca09bb1ee05c001787b/curl_cffi-0.15.0b4-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed794c28497a60ba369895d9fc3f3719425bb489b04eae8ab4a228e4cf3d7568", size = 2784114, upload-time = "2026-02-26T04:12:36.563Z" },
-    { url = "https://files.pythonhosted.org/packages/af/ec/009a8f49241a77740840213360cf40a92ccf993134466a17da797c2fd76b/curl_cffi-0.15.0b4-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a227038fdfdc272937ad5146e649480d5a2b76d616675a9847d035a9726383f9", size = 2562064, upload-time = "2026-02-26T04:12:38.358Z" },
-    { url = "https://files.pythonhosted.org/packages/48/cf/02e2a9c978e80369a8b87ad53d464c8bea902ef241bcf85c580b7e283a01/curl_cffi-0.15.0b4-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d26b8ff4ba9844151fcbd407d64dbf0f6abf44d6569a4ee0fde1f65706de471b", size = 11078242, upload-time = "2026-02-26T04:12:43.553Z" },
-    { url = "https://files.pythonhosted.org/packages/ab/3a/761fb2b0346dc701ef381d6d73e3701b1f63b827532abc2cfbc7078631ee/curl_cffi-0.15.0b4-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:92d88529eb20c99f2d2910cb4eae18b89f12e6a34bf0f86ab52e528144877d6c", size = 11930396, upload-time = "2026-02-26T04:12:51.516Z" },
-    { url = "https://files.pythonhosted.org/packages/f8/a8/c8568aff374fba82850c7e48daa2969f471459ada14e6702fecbf803b6ea/curl_cffi-0.15.0b4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:91fa67c8e6cbe7324146a383224216006000807a1df92f3de1f62d4bde0674eb", size = 2784678, upload-time = "2026-02-26T04:12:56.787Z" },
-    { url = "https://files.pythonhosted.org/packages/c7/59/7a065526dab9c3289c62b0b050edf22fb4fe8233b7a424e2782c1c85253f/curl_cffi-0.15.0b4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a6a8f4b710949121780ed6487bde30b05fcb9cafbd4f54e1296333153082f75c", size = 2562351, upload-time = "2026-02-26T04:12:58.266Z" },
-    { url = "https://files.pythonhosted.org/packages/8f/4d/511d56dc1eb8bd9dc24a547fbed36a90ed9827f5a611362aa486b57e66f5/curl_cffi-0.15.0b4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d39934e818295d1be263f0f3118ccf1c92b90af158e61e64cd382aa907424178", size = 11085541, upload-time = "2026-02-26T04:13:01.815Z" },
-    { url = "https://files.pythonhosted.org/packages/90/a2/0c1e4d9c5891fa19364a73cfe199cc584dc5725ca6e0ba24a78bbe2b6257/curl_cffi-0.15.0b4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf95fa9980ed0f8c6207d6bd6c6f7ca2d2bb69fa3846a318d274106d6fc83ae5", size = 11936267, upload-time = "2026-02-26T04:13:05.876Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/d3/1a94fdaa7f9504b5182cd375a7785e04095ce14e48000564e4660b8f2c34/curl_cffi-0.15.0rc1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bc4c93fc3db66b258a3a03443b1dffc37321ce9ad4ca507b2ec59deae931a859", size = 2795216, upload-time = "2026-03-30T10:57:42.58Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/12/97dbe5b3c687f88dfc96d1ba90a6277a0d32135d493e11318d4e3431749b/curl_cffi-0.15.0rc1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:69c9376c308e6aea27d90b8c7885df01f3c47445c95e32dcaf1a37f91baa94e2", size = 2573500, upload-time = "2026-03-30T10:57:44.236Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/58/f8a026be4198a359142f4c34ded275bb47eeb6a44539ab932091086073df/curl_cffi-0.15.0rc1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64ac185f09d0721f1bb72409071ca5bb5270aa7ca380576d2a7ec70ef9ed317b", size = 11090389, upload-time = "2026-03-30T10:57:51.747Z" },
+    { url = "https://files.pythonhosted.org/packages/10/3f/d445ef60f7fecb944fe275661f12677a8ab1e40495b201b85b6258750278/curl_cffi-0.15.0rc1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a73a271c43967a13107080f31f315b0ef029205a9044413ce674d51316309c67", size = 11945021, upload-time = "2026-03-30T10:58:03.067Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/b5/e5fcd247f19b7dacef156871bc5bfce251426bbb87154b2266e9c724db46/curl_cffi-0.15.0rc1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:94c984ea4140c659e06a38715b91c965651935bb38499697357038f0629137f9", size = 2795678, upload-time = "2026-03-30T10:58:09.794Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/5c/b99bbb37283918bf4c5507eb47291d48d5e2669fec602d4de4cae9741f4a/curl_cffi-0.15.0rc1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7773d5f2c52f97d7748fa49537b164a965467561e73e4553d3ebc29e969a826a", size = 2573689, upload-time = "2026-03-30T10:58:11.613Z" },
+    { url = "https://files.pythonhosted.org/packages/88/60/7d1e9f17843473dfcf0439c370232946c876da576870f163f939c3612650/curl_cffi-0.15.0rc1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b487bbd7e37cdc151bb92ca391463534a5877a1d9e1d376ba76a43abbdefa578", size = 11096069, upload-time = "2026-03-30T10:58:16.015Z" },
+    { url = "https://files.pythonhosted.org/packages/cd/26/118ad2204a3dc0e29d60e54612447b5e3e82dbc2fc8bb61c23025d553b9c/curl_cffi-0.15.0rc1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:780b4bea4aa31b59ae11458f0cc2a6d55772024bb3e111fc190c0fead376043f", size = 11949804, upload-time = "2026-03-30T10:58:21.002Z" },
 ]
 
 [[package]]
@@ -575,7 +582,7 @@ wheels = [
 
 [[package]]
 name = "dashscope"
-version = "1.25.12"
+version = "1.25.15"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "aiohttp", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
@@ -585,7 +592,7 @@ dependencies = [
     { name = "websocket-client", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
 ]
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/d9/7a/a1a9d0d293ca2cfabf54eedbea237d4d9e233251477d65c51c77667c407c/dashscope-1.25.12-py3-none-any.whl", hash = "sha256:92e98314bac0ff45b7f32f9120946771a85e614371b397f0874c1e579de60f98", size = 1342605, upload-time = "2026-02-09T09:02:50.82Z" },
+    { url = "https://files.pythonhosted.org/packages/54/39/a5a517d260e9f481e6d1e41ad7fcb021ad52327483ee2847fbb0f6350984/dashscope-1.25.15-py3-none-any.whl", hash = "sha256:0a1d2e43d8ad5447fccbbf9e29f38d7f2241c6c83c4e2c85f165a524ffb9121e", size = 1343122, upload-time = "2026-03-24T07:20:19.106Z" },
 ]
 
 [[package]]
@@ -747,7 +754,7 @@ requests = [
 
 [[package]]
 name = "google-genai"
-version = "1.65.0"
+version = "1.69.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "anyio", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
@@ -761,9 +768,9 @@ dependencies = [
     { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
     { name = "websockets", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/79/f9/cc1191c2540d6a4e24609a586c4ed45d2db57cfef47931c139ee70e5874a/google_genai-1.65.0.tar.gz", hash = "sha256:d470eb600af802d58a79c7f13342d9ea0d05d965007cae8f76c7adff3d7a4750", size = 497206, upload-time = "2026-02-26T00:20:33.824Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/00/5e/c0a5e6ff60d18d3f19819a9b1fbd6a1ef2162d025696d8660550739168dc/google_genai-1.69.0.tar.gz", hash = "sha256:5f1a6a478e0c5851506a3d337534bab27b3c33120e27bf9174507ea79dfb8673", size = 519538, upload-time = "2026-03-28T15:33:27.308Z" }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/68/3c/3fea4e7c91357c71782d7dcaad7a2577d636c90317e003386893c25bc62c/google_genai-1.65.0-py3-none-any.whl", hash = "sha256:68c025205856919bc03edb0155c11b4b833810b7ce17ad4b7a9eeba5158f6c44", size = 724429, upload-time = "2026-02-26T00:20:32.186Z" },
+    { url = "https://files.pythonhosted.org/packages/42/58/ef0586019f54b2ebb36deed7608ccb5efe1377564d2aaea6b1e295d1fadc/google_genai-1.69.0-py3-none-any.whl", hash = "sha256:252e714d724aba74949647b9de511a6a6f7804b3b317ab39ddee9cc2f001cacc", size = 760551, upload-time = "2026-03-28T15:33:24.957Z" },
 ]
 
 [[package]]
@@ -1045,6 +1052,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" },
 ]
 
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "mdurl", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
+]
+
 [[package]]
 name = "markdownify"
 version = "1.2.2"
@@ -1108,6 +1127,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
 ]
 
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
 [[package]]
 name = "mpmath"
 version = "1.3.0"
@@ -1233,7 +1261,7 @@ wheels = [
 
 [[package]]
 name = "openai"
-version = "2.24.0"
+version = "2.30.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "anyio", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
@@ -1245,9 +1273,9 @@ dependencies = [
     { name = "tqdm", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
     { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" },
 ]
 
 [[package]]
@@ -1501,11 +1529,11 @@ wheels = [
 
 [[package]]
 name = "puremagic"
-version = "2.0.0"
+version = "2.1.1"
 source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/dc/df/a2ee3bbf55f036acb9725b35732e3a785cb06f5c5b9fe47bde8c05ab873a/puremagic-2.0.0.tar.gz", hash = "sha256:224fe42b6b3467276a45914e12b5f40905dea0e87963adbe5289667e7c607851", size = 1119578, upload-time = "2026-02-20T13:37:53.262Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/df/3725f4b848095ef634c0b2226c97901e64ee2d5a82981d89d4b784ae8ce1/puremagic-2.1.1.tar.gz", hash = "sha256:b156c4ae63d84842f92a85cd49c9b9029a4f107f98ad14e7584ed652954feff4", size = 1133417, upload-time = "2026-03-23T19:08:46.929Z" }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/79/03/f4a28d560494cfdf6a619c61ae5664390fac4f4b640df82e304d48f457e3/puremagic-2.0.0-py3-none-any.whl", hash = "sha256:c86aee5c15d6a346e72c5b964f1d930d42bc7dc058afdf4ac63ab92f26086aaf", size = 65924, upload-time = "2026-02-20T13:37:51.992Z" },
+    { url = "https://files.pythonhosted.org/packages/62/d0/12b1d4113fd6660a0a75e8c40500c5d1c4febd8e24dc85aaf20cfd93e9d6/puremagic-2.1.1-py3-none-any.whl", hash = "sha256:b8862451f96254358a6e2ea7fba46e0600cf8b13ebe917d6eecdb18fc22db964", size = 68025, upload-time = "2026-03-23T19:08:45.872Z" },
 ]
 
 [[package]]
@@ -1658,15 +1686,15 @@ wheels = [
 
 [[package]]
 name = "pyrotgfork"
-version = "2.2.19"
+version = "2.2.21"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "pyaes", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
     { name = "pysocks", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/53/0f/80fa88993e4c64d85b1589a0b6b4d5f0679a899eeca9113dc0ea3bd7485c/pyrotgfork-2.2.19.tar.gz", hash = "sha256:bd83259db899228085a23210c465a31a4db7dfab1549047cafb2148f88e62299", size = 526410, upload-time = "2026-03-01T13:31:25.634Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d9/ef/5d5db32d5a65df1d002c8f958f1f038f9aa8e03c3133c050fd329922705e/pyrotgfork-2.2.21.tar.gz", hash = "sha256:2215a0b9dbae05f8f3aa34a56f2b7609e38bf2da9dbb35222df38f62aa7e165a", size = 530626, upload-time = "2026-03-07T12:20:09.816Z" }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/5f/70/69fff43fdf19347b8cd65393704617deb3a5232d0e09630544a497d5714f/pyrotgfork-2.2.19-py3-none-any.whl", hash = "sha256:d43d9d104d5782e3ef9dcee07f8cd23f26b226323f6ecf6cccdef2b86edbf18b", size = 5536333, upload-time = "2026-03-01T13:31:23.548Z" },
+    { url = "https://files.pythonhosted.org/packages/3f/63/740c31095070f3abce2bd908014552e5c4b7bf729c8c31f5b4d28e4ce6fd/pyrotgfork-2.2.21-py3-none-any.whl", hash = "sha256:469aa0c404dcae5ec5b5ca5b0cd8fa97113751e39f3a73916d04bcd095b732bd", size = 5544817, upload-time = "2026-03-07T12:20:07.376Z" },
 ]
 
 [[package]]
@@ -1830,6 +1858,19 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
 ]
 
+[[package]]
+name = "rich"
+version = "14.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "markdown-it-py", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+    { name = "pygments", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
+]
+
 [[package]]
 name = "s3transfer"
 version = "0.14.0"