feat: add matrix staging list and remove flow
This commit is contained in:
parent
83c9a1513b
commit
f111ed3348
2 changed files with 510 additions and 20 deletions
|
|
@ -6,6 +6,7 @@ from dataclasses import dataclass
|
|||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
from dotenv import load_dotenv
|
||||
from nio import (
|
||||
AsyncClient,
|
||||
AsyncClientConfig,
|
||||
|
|
@ -15,28 +16,38 @@ from nio import (
|
|||
RoomMessageText,
|
||||
)
|
||||
from nio.responses import SyncResponse
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from adapter.matrix.converter import from_room_event
|
||||
from adapter.matrix.files import (
|
||||
download_matrix_attachment,
|
||||
matrix_msgtype_for_attachment,
|
||||
resolve_workspace_attachment_path,
|
||||
)
|
||||
from adapter.matrix.handlers import register_matrix_handlers
|
||||
from adapter.matrix.handlers.auth import handle_invite, provision_workspace_chat
|
||||
from adapter.matrix.handlers.context_commands import (
|
||||
LOAD_PROMPT,
|
||||
)
|
||||
from adapter.matrix.handlers.auth import handle_invite, provision_workspace_chat
|
||||
from adapter.matrix.room_router import resolve_chat_id
|
||||
from adapter.matrix.store import (
|
||||
add_staged_attachment,
|
||||
clear_load_pending,
|
||||
clear_staged_attachments,
|
||||
get_load_pending,
|
||||
get_room_meta,
|
||||
get_staged_attachments,
|
||||
remove_staged_attachment_at,
|
||||
set_pending_confirm,
|
||||
set_platform_chat_id,
|
||||
set_room_meta,
|
||||
set_pending_confirm,
|
||||
)
|
||||
from core.auth import AuthManager
|
||||
from core.chat import ChatManager
|
||||
from core.handler import EventDispatcher
|
||||
from core.handlers import register_all
|
||||
from core.protocol import (
|
||||
IncomingCommand,
|
||||
IncomingMessage,
|
||||
OutgoingEvent,
|
||||
OutgoingMessage,
|
||||
OutgoingNotification,
|
||||
|
|
@ -197,6 +208,44 @@ class MatrixBot:
|
|||
incoming = from_room_event(event, room_id=room.room_id, chat_id=dispatch_chat_id)
|
||||
if incoming is None:
|
||||
return
|
||||
if isinstance(incoming, IncomingCommand) and incoming.command in {
|
||||
"matrix_list_attachments",
|
||||
"matrix_remove_attachment",
|
||||
}:
|
||||
outgoing = await self._handle_staged_attachment_command(
|
||||
room.room_id,
|
||||
sender,
|
||||
incoming,
|
||||
)
|
||||
await self._send_all(room.room_id, outgoing)
|
||||
return
|
||||
if self._is_file_only_event(event, incoming):
|
||||
materialized = await self._materialize_incoming_attachments(
|
||||
room.room_id,
|
||||
sender,
|
||||
incoming,
|
||||
)
|
||||
await self._stage_attachments(room.room_id, sender, materialized.attachments)
|
||||
await self._send_all(
|
||||
room.room_id,
|
||||
[
|
||||
OutgoingMessage(
|
||||
chat_id=dispatch_chat_id,
|
||||
text=await self._format_staged_attachments(
|
||||
room.room_id,
|
||||
sender,
|
||||
include_hint=True,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
return
|
||||
if isinstance(incoming, IncomingMessage) and incoming.attachments:
|
||||
incoming = await self._materialize_incoming_attachments(
|
||||
room.room_id,
|
||||
sender,
|
||||
incoming,
|
||||
)
|
||||
try:
|
||||
outgoing = await self.runtime.dispatcher.dispatch(incoming)
|
||||
except PlatformError as exc:
|
||||
|
|
@ -210,11 +259,125 @@ class MatrixBot:
|
|||
outgoing = [
|
||||
OutgoingMessage(
|
||||
chat_id=dispatch_chat_id,
|
||||
text="Сервис временно недоступен. Попробуйте ещё раз позже."
|
||||
text="Сервис временно недоступен. Попробуйте ещё раз позже.",
|
||||
)
|
||||
]
|
||||
await self._send_all(room.room_id, outgoing)
|
||||
|
||||
def _is_file_only_event(
|
||||
self, event: RoomMessageText, incoming: IncomingMessage | IncomingCommand
|
||||
) -> bool:
|
||||
return (
|
||||
isinstance(incoming, IncomingMessage)
|
||||
and bool(incoming.attachments)
|
||||
and getattr(event, "msgtype", None) != "m.text"
|
||||
)
|
||||
|
||||
async def _stage_attachments(
|
||||
self,
|
||||
room_id: str,
|
||||
user_id: str,
|
||||
attachments: list,
|
||||
) -> None:
|
||||
for attachment in attachments:
|
||||
await add_staged_attachment(
|
||||
self.runtime.store,
|
||||
room_id,
|
||||
user_id,
|
||||
{
|
||||
"type": attachment.type,
|
||||
"url": attachment.url,
|
||||
"filename": attachment.filename,
|
||||
"mime_type": attachment.mime_type,
|
||||
"workspace_path": attachment.workspace_path,
|
||||
},
|
||||
)
|
||||
|
||||
async def _format_staged_attachments(
|
||||
self,
|
||||
room_id: str,
|
||||
user_id: str,
|
||||
*,
|
||||
include_hint: bool = False,
|
||||
) -> str:
|
||||
attachments = await get_staged_attachments(self.runtime.store, room_id, user_id)
|
||||
if not attachments:
|
||||
return "Нет сохраненных вложений."
|
||||
|
||||
lines = ["Вложения в очереди:"]
|
||||
for index, attachment in enumerate(attachments, start=1):
|
||||
lines.append(f"{index}. {attachment.get('filename') or 'attachment'}")
|
||||
if include_hint:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"Следующее сообщение отправит файлы агенту.",
|
||||
"Команды: !list, !remove <n>, !remove all",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
async def _handle_staged_attachment_command(
|
||||
self,
|
||||
room_id: str,
|
||||
user_id: str,
|
||||
incoming: IncomingCommand,
|
||||
) -> list[OutgoingEvent]:
|
||||
if incoming.command == "matrix_list_attachments":
|
||||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=incoming.chat_id,
|
||||
text=await self._format_staged_attachments(room_id, user_id),
|
||||
)
|
||||
]
|
||||
|
||||
arg = incoming.args[0] if incoming.args else ""
|
||||
if arg == "all":
|
||||
await clear_staged_attachments(self.runtime.store, room_id, user_id)
|
||||
return [OutgoingMessage(chat_id=incoming.chat_id, text="Все вложения удалены.")]
|
||||
|
||||
try:
|
||||
index = int(arg) - 1
|
||||
except ValueError:
|
||||
return [OutgoingMessage(chat_id=incoming.chat_id, text="Нет такого вложения.")]
|
||||
|
||||
removed = await remove_staged_attachment_at(self.runtime.store, room_id, user_id, index)
|
||||
if removed is None:
|
||||
return [OutgoingMessage(chat_id=incoming.chat_id, text="Нет такого вложения.")]
|
||||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=incoming.chat_id,
|
||||
text=await self._format_staged_attachments(room_id, user_id),
|
||||
)
|
||||
]
|
||||
|
||||
async def _materialize_incoming_attachments(
|
||||
self,
|
||||
room_id: str,
|
||||
matrix_user_id: str,
|
||||
incoming: IncomingMessage,
|
||||
) -> IncomingMessage:
|
||||
workspace_root = Path(os.environ.get("SURFACES_WORKSPACE_DIR", "/workspace"))
|
||||
materialized = []
|
||||
for attachment in incoming.attachments:
|
||||
materialized.append(
|
||||
await download_matrix_attachment(
|
||||
client=self.client,
|
||||
workspace_root=workspace_root,
|
||||
matrix_user_id=matrix_user_id,
|
||||
room_id=room_id,
|
||||
attachment=attachment,
|
||||
)
|
||||
)
|
||||
return IncomingMessage(
|
||||
user_id=incoming.user_id,
|
||||
platform=incoming.platform,
|
||||
chat_id=incoming.chat_id,
|
||||
text=incoming.text,
|
||||
attachments=materialized,
|
||||
reply_to=incoming.reply_to,
|
||||
)
|
||||
|
||||
async def _bootstrap_unregistered_room(
|
||||
self,
|
||||
room: MatrixRoom,
|
||||
|
|
@ -251,11 +414,6 @@ class MatrixBot:
|
|||
f"Привет, {created['user'].display_name or sender}! Пиши — я здесь.\n\n"
|
||||
"Команды: !new · !chats · !rename · !archive · !context · !save · !load · !help"
|
||||
)
|
||||
await self.client.room_send(
|
||||
created["chat_room_id"],
|
||||
"m.room.message",
|
||||
{"msgtype": "m.text", "body": welcome},
|
||||
)
|
||||
await set_room_meta(
|
||||
self.runtime.store,
|
||||
room.room_id,
|
||||
|
|
@ -265,12 +423,18 @@ class MatrixBot:
|
|||
"redirect_chat_id": created["chat_id"],
|
||||
},
|
||||
)
|
||||
await self.client.room_send(
|
||||
created["chat_room_id"],
|
||||
"m.room.message",
|
||||
{"msgtype": "m.text", "body": welcome},
|
||||
)
|
||||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=room.room_id,
|
||||
text=(
|
||||
f"Создал рабочий чат {created['room_name']} ({created['chat_id']}) "
|
||||
"и добавил его в пространство Lambda. Открой приглашённую комнату для продолжения."
|
||||
"и добавил его в пространство Lambda. "
|
||||
"Открой приглашённую комнату для продолжения."
|
||||
),
|
||||
)
|
||||
]
|
||||
|
|
@ -323,7 +487,9 @@ class MatrixBot:
|
|||
except Exception as exc:
|
||||
logger.warning("load_agent_call_failed", error=str(exc))
|
||||
return [OutgoingMessage(chat_id=room_id, text=f"Ошибка при загрузке: {exc}")]
|
||||
return [OutgoingMessage(chat_id=room_id, text=f"Запрос на загрузку отправлен агенту: {name}")]
|
||||
return [
|
||||
OutgoingMessage(chat_id=room_id, text=f"Запрос на загрузку отправлен агенту: {name}")
|
||||
]
|
||||
|
||||
async def on_member(self, room: MatrixRoom, event: RoomMemberEvent) -> None:
|
||||
if getattr(event, "sender", None) == self.client.user_id:
|
||||
|
|
@ -351,6 +517,7 @@ async def prepare_live_sync(client: AsyncClient) -> str | None:
|
|||
return response.next_batch
|
||||
return None
|
||||
|
||||
|
||||
async def send_outgoing(
|
||||
client: AsyncClient,
|
||||
room_id: str,
|
||||
|
|
@ -365,7 +532,37 @@ async def send_outgoing(
|
|||
await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": body})
|
||||
return
|
||||
if isinstance(event, OutgoingMessage):
|
||||
await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": event.text})
|
||||
if event.text:
|
||||
await client.room_send(
|
||||
room_id, "m.room.message", {"msgtype": "m.text", "body": event.text}
|
||||
)
|
||||
if event.attachments:
|
||||
workspace_root = Path(os.environ.get("SURFACES_WORKSPACE_DIR", "/workspace"))
|
||||
for attachment in event.attachments:
|
||||
if not attachment.workspace_path:
|
||||
continue
|
||||
file_path = resolve_workspace_attachment_path(
|
||||
workspace_root, attachment.workspace_path
|
||||
)
|
||||
with file_path.open("rb") as handle:
|
||||
upload_response, _ = await client.upload(
|
||||
handle,
|
||||
content_type=attachment.mime_type or "application/octet-stream",
|
||||
filename=attachment.filename or file_path.name,
|
||||
filesize=file_path.stat().st_size,
|
||||
)
|
||||
content_uri = getattr(upload_response, "content_uri", None)
|
||||
if not content_uri:
|
||||
raise RuntimeError(f"Matrix upload failed for {file_path}")
|
||||
await client.room_send(
|
||||
room_id,
|
||||
"m.room.message",
|
||||
{
|
||||
"msgtype": matrix_msgtype_for_attachment(attachment),
|
||||
"body": attachment.filename or file_path.name,
|
||||
"url": content_uri,
|
||||
},
|
||||
)
|
||||
return
|
||||
if isinstance(event, OutgoingUI):
|
||||
lines = [event.text]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue