fix(telegram,core): first message seeds the branch, registered commands, no ellipses, html drafts

This commit is contained in:
hh
2026-08-28 18:25:46 +02:00
parent b0f280b3ca
commit 3995741cd5
5 changed files with 111 additions and 78 deletions
+1 -1
View File
@@ -535,7 +535,7 @@ class Conversations:
await self._queue.push(
conversation_id=cast("int", conv.id),
priority="user",
origin=f"сид:{seed}",
origin=origin if text and seed != "brief" else f"сид:{seed}",
text=prompt,
)
self._ensure_worker(cast("int", conv.id))
@@ -15,7 +15,9 @@ import time
import zlib
from typing import TYPE_CHECKING
from aiogram.exceptions import TelegramAPIError
from aiogram.exceptions import TelegramAPIError, TelegramBadRequest
from beaver_gateway.frontends.telegram.render import to_html
if TYPE_CHECKING:
from aiogram import Bot
@@ -37,7 +39,7 @@ class Draft:
thread_id: int | None,
turn_id: str,
interval: float = 0.7,
status: str = "⏳ думаю",
status: str = "⏳ думаю",
) -> None:
self._bot = bot
self._chat_id = chat_id
@@ -82,20 +84,27 @@ class Draft:
async def _push(self) -> None:
self._dirty = False
self._last_sent = time.monotonic()
text = self._render()
try:
await self._bot.send_message_draft(
chat_id=self._chat_id,
draft_id=self._draft_id,
message_thread_id=self._thread_id,
text=self._render(),
parse_mode=None,
)
try:
await self._send(to_html(text), "HTML")
except TelegramBadRequest:
await self._send(text, None)
except TelegramAPIError as exc:
self._broken = True
_log.warning(
"draft to %s/%s stopped: %s", self._chat_id, self._thread_id, exc
)
async def _send(self, text: str, parse_mode: str | None) -> None:
await self._bot.send_message_draft(
chat_id=self._chat_id,
draft_id=self._draft_id,
message_thread_id=self._thread_id,
text=text,
parse_mode=parse_mode,
)
def _render(self) -> str:
tail = self.text[-_TAIL:]
return f"{self.status}\n\n{tail}" if tail.strip() else self.status
@@ -26,6 +26,7 @@ from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from aiogram.exceptions import TelegramAPIError
from aiogram.types import (
BotCommand,
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
@@ -53,13 +54,16 @@ _log = logging.getLogger("beaver_gateway.frontends.telegram")
FRONTEND = "telegram"
_DONE = "done"
_COMMANDS = ("merge", "new", "chat", "status", "start", "help")
_HELP = (
"General - мастер, топик - ветка. Создай топик и пиши в него.\n"
"/merge - слить ветку в мастер\n"
"/new [название] - новая ветка (в General - новый топик)\n"
"/chat <тема> - открыть глубокий чат в vault\n"
"/status - что с этим разговором" # noqa: RUF001
_COMMAND_HELP = (
("merge", "слить ветку в мастер"),
("new", "новая ветка: в General - новый топик, в топике - заново на нём"),
("chat", "открыть глубокий чат в vault: /chat тема"),
("status", "что с этим разговором"), # noqa: RUF001
("help", "команды"),
)
_COMMANDS = (*(c for c, _ in _COMMAND_HELP), "start")
_HELP = "General - мастер, любой другой топик - ветка.\n" + "\n".join(
f"/{c} - {d}" for c, d in _COMMAND_HELP
)
@@ -171,6 +175,9 @@ class TelegramFrontend(Frontend):
getattr(me, "has_topics_enabled", None),
)
self._sweep_attachments()
await self.bot.set_my_commands(
[BotCommand(command=c, description=d) for c, d in _COMMAND_HELP]
)
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(self.inbox.run())
@@ -305,25 +312,35 @@ class TelegramFrontend(Frontend):
self._master_window = self._ext(topic.message_thread_id)
return self._master_window
async def _master(self) -> Conversation:
async def _master(self, text: str | None = None) -> tuple[Conversation, bool]:
"""The open master behind its window, spawning one when there is none.
``text`` rides with the seed of a fresh master; the second value says
whether it was consumed that way.
"""
ext = await self._master_ext()
conv = await self.conversations.find_bound(frontend=FRONTEND, external_id=ext)
if conv is not None and conv.status == "open":
return conv
return conv, False
masters = await self.conversations.find(kind="master", status="open", limit=1)
if masters:
await self.conversations.bind(
masters[0], frontend=FRONTEND, external_id=ext
)
return masters[0]
return await self.conversations.spawn(
kind="master", seed="clean", origin=FRONTEND, binding=(FRONTEND, ext)
return masters[0], False
conv = await self.conversations.spawn(
kind="master",
seed="clean",
text=text,
origin=FRONTEND,
binding=(FRONTEND, ext),
)
return conv, text is not None
async def _branch(
self, thread_id: int, *, title: str | None, text: str | None
) -> Conversation:
master = await self._master()
master, _ = await self._master()
return await self.conversations.spawn(
kind="branch",
seed="morning",
@@ -343,10 +360,13 @@ class TelegramFrontend(Frontend):
await self._on_callback(update.callback_query)
async def _on_message(self, message: Message) -> None:
if message.forum_topic_created is not None and message.message_thread_id:
self._topic_names[message.message_thread_id] = (
message.forum_topic_created.name
)
created = message.forum_topic_created
if (
created is not None
and message.message_thread_id
and not created.is_name_implicit
):
self._topic_names[message.message_thread_id] = created.name
if message.from_user is None or message.from_user.id != self.user_id:
_log.warning(
"ignoring message from %s", getattr(message.from_user, "id", None)
@@ -359,11 +379,7 @@ class TelegramFrontend(Frontend):
is_master = (
thread_id is None or self._ext(thread_id) == await self._master_ext()
)
if message.forum_topic_created is not None and thread_id is not None:
if not is_master and await self._live(thread_id) is None:
await self._branch(
thread_id, title=message.forum_topic_created.name, text=None
)
if message.forum_topic_created is not None:
return
if message.forum_topic_edited is not None and thread_id is not None:
if message.forum_topic_edited.name:
@@ -382,11 +398,14 @@ class TelegramFrontend(Frontend):
await self._command(command, args.strip(), message, thread_id)
return
if is_master:
conv = await self._master()
conv, consumed = await self._master(text)
if consumed:
return
else:
conv = await self._live(thread_id)
if conv is None:
await self._branch(thread_id, title=self._title_from(text), text=text)
title = self._topic_names.get(thread_id) or self._title_from(text)
await self._branch(thread_id, title=title, text=text)
return
pending = self.conversations.pending_question(conv.external_id)
if pending is not None and self.conversations.answer(pending[0], text):
@@ -522,14 +541,14 @@ class TelegramFrontend(Frontend):
is_master = (
thread_id is None or self._ext(thread_id) == await self._master_ext()
)
conv = await self._master() if is_master else await self._live(thread_id)
conv = (await self._master())[0] if is_master else await self._live(thread_id)
if command == "status":
return await self._status(conv)
if command == "merge":
if conv is None or conv.kind != "branch":
return "сливать нечего: это не открытая ветка"
self._spawn_task(self._merge(conv))
return "🔀 сливаю в мастер"
return "🔀 сливаю в мастер"
if command == "new":
if is_master or thread_id is None:
child = await self.conversations.spawn(
@@ -649,6 +668,10 @@ class TelegramFrontend(Frontend):
key=f"{event.get('turn_id')}:error",
)
case "reply":
if conv.kind == "master" and str(
event.get("item_origin") or ""
).startswith("сид"):
return
await self._on_reply(conv, event)
case "say":
await self._deliver(
@@ -711,10 +734,10 @@ class TelegramFrontend(Frontend):
if kind == "content_block_delta":
delta = raw.get("delta") or {}
if delta.get("type") == "text_delta":
draft.set_status("✍️ пишу")
draft.set_status("✍️ пишу")
draft.append(str(delta.get("text") or ""))
elif delta.get("type") == "thinking_delta":
draft.set_status("🤔 думаю")
draft.set_status("🤔 думаю")
elif kind == "content_block_start":
block = raw.get("content_block") or {}
if block.get("type") == "tool_use":
+21 -21
View File
@@ -20,25 +20,25 @@ _BULLET = re.compile(r"^(\s*)[-*]\s+", re.MULTILINE)
_STRIKE = re.compile(r"~~(.+?)~~")
_LABELS: dict[str, str] = {
"Read": "читаю vault",
"Glob": "ищу файлы",
"Grep": "ищу в vault",
"Edit": "правлю файл",
"Write": "пишу файл",
"MultiEdit": "правлю файлы",
"Bash": "выполняю команду",
"WebSearch": "ищу в сети",
"WebFetch": "читаю страницу",
"Task": "запустил сабагента",
"Agent": "запустил сабагента",
"AskUserQuestion": "спрашиваю",
"TodoWrite": "планирую",
"Skill": "открываю скилл",
"mcp__gateway__spawn": "открываю разговор",
"mcp__gateway__read_conversation": "читаю разговор",
"mcp__gateway__say": "говорю",
"mcp__gateway__schedule": "ставлю напоминание",
"mcp__gateway__inject": "передаю в другой разговор",
"Read": "читаю vault",
"Glob": "ищу файлы",
"Grep": "ищу в vault",
"Edit": "правлю файл",
"Write": "пишу файл",
"MultiEdit": "правлю файлы",
"Bash": "выполняю команду",
"WebSearch": "ищу в сети",
"WebFetch": "читаю страницу",
"Task": "запустил сабагента",
"Agent": "запустил сабагента",
"AskUserQuestion": "спрашиваю",
"TodoWrite": "планирую",
"Skill": "открываю скилл",
"mcp__gateway__spawn": "открываю разговор",
"mcp__gateway__read_conversation": "читаю разговор",
"mcp__gateway__say": "говорю",
"mcp__gateway__schedule": "ставлю напоминание",
"mcp__gateway__inject": "передаю в другой разговор",
}
@@ -100,7 +100,7 @@ def status_label(name: str, tool_input: dict[str, Any] | None = None) -> str:
parts = name.split("__", 2)
server = parts[1]
tool = parts[2] if len(parts) == 3 else ""
return f"{server}: {tool}" if tool else f"{server}"
return f"{server}: {tool}" if tool else server
hint = ""
if tool_input:
for key in ("description", "command", "file_path", "pattern", "query"):
@@ -108,4 +108,4 @@ def status_label(name: str, tool_input: dict[str, Any] | None = None) -> str:
if isinstance(value, str) and value.strip():
hint = value.strip().splitlines()[0][:60]
break
return f"{name} {hint}".strip() if hint else f"{name}"
return f"{name} {hint}" if hint else name