feat: finalize matrix platform audit and docs
This commit is contained in:
parent
6422c7db58
commit
4524a6abc8
30 changed files with 3093 additions and 176 deletions
|
|
@ -41,6 +41,7 @@ from adapter.matrix.store import (
|
|||
get_load_pending,
|
||||
get_room_meta,
|
||||
get_staged_attachments,
|
||||
next_platform_chat_id,
|
||||
remove_staged_attachment_at,
|
||||
set_pending_confirm,
|
||||
set_platform_chat_id,
|
||||
|
|
@ -163,7 +164,11 @@ class MatrixBot:
|
|||
return
|
||||
if room_meta.get("platform_chat_id"):
|
||||
return
|
||||
await set_platform_chat_id(self.runtime.store, room_id, f"matrix:{room_id}")
|
||||
await set_platform_chat_id(
|
||||
self.runtime.store,
|
||||
room_id,
|
||||
await next_platform_chat_id(self.runtime.store),
|
||||
)
|
||||
|
||||
async def on_room_message(self, room: MatrixRoom, event: RoomMessageText) -> None:
|
||||
if getattr(event, "sender", None) == self.client.user_id:
|
||||
|
|
|
|||
|
|
@ -41,12 +41,7 @@ def build_workspace_attachment_path(
|
|||
safe_room = _sanitize_component(room_id.lstrip("!"))
|
||||
safe_name = _sanitize_component(filename) or "attachment.bin"
|
||||
relative_path = (
|
||||
Path("surfaces")
|
||||
/ "matrix"
|
||||
/ safe_user
|
||||
/ safe_room
|
||||
/ "inbox"
|
||||
/ f"{stamp}-{safe_name}"
|
||||
Path("surfaces") / "matrix" / safe_user / safe_room / "inbox" / f"{stamp}-{safe_name}"
|
||||
)
|
||||
return relative_path.as_posix(), workspace_root / relative_path
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ from adapter.matrix.handlers.settings import (
|
|||
handle_help,
|
||||
handle_settings,
|
||||
handle_settings_connectors,
|
||||
handle_unknown_command,
|
||||
handle_settings_plan,
|
||||
handle_settings_safety,
|
||||
handle_settings_skills,
|
||||
|
|
@ -25,6 +24,7 @@ from adapter.matrix.handlers.settings import (
|
|||
handle_settings_status,
|
||||
handle_settings_whoami,
|
||||
handle_toggle_skill,
|
||||
handle_unknown_command,
|
||||
)
|
||||
from core.handler import EventDispatcher
|
||||
from core.protocol import IncomingCallback, IncomingCommand
|
||||
|
|
@ -44,7 +44,13 @@ def register_matrix_handlers(
|
|||
dispatcher.register(IncomingCommand, "archive", make_handle_archive(client, store))
|
||||
dispatcher.register(IncomingCommand, "help", handle_help)
|
||||
dispatcher.register(IncomingCommand, "settings", handle_settings)
|
||||
dispatcher.register(IncomingCommand, "reset", make_handle_reset(store, prototype_state) if prototype_state is not None else handle_settings)
|
||||
dispatcher.register(
|
||||
IncomingCommand,
|
||||
"reset",
|
||||
make_handle_reset(store, prototype_state)
|
||||
if prototype_state is not None
|
||||
else handle_settings,
|
||||
)
|
||||
dispatcher.register(IncomingCommand, "settings_skills", handle_settings_skills)
|
||||
dispatcher.register(IncomingCommand, "settings_connectors", handle_settings_connectors)
|
||||
dispatcher.register(IncomingCommand, "settings_soul", handle_settings_soul)
|
||||
|
|
@ -59,6 +65,10 @@ def register_matrix_handlers(
|
|||
dispatcher.register(IncomingCommand, "*", handle_unknown_command)
|
||||
|
||||
if agent_api is not None and prototype_state is not None:
|
||||
dispatcher.register(IncomingCommand, "save", make_handle_save(agent_api, store, prototype_state))
|
||||
dispatcher.register(
|
||||
IncomingCommand,
|
||||
"save",
|
||||
make_handle_save(agent_api, store, prototype_state),
|
||||
)
|
||||
dispatcher.register(IncomingCommand, "load", make_handle_load(store, prototype_state))
|
||||
dispatcher.register(IncomingCommand, "context", make_handle_context(store, prototype_state))
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import structlog
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from nio.api import RoomVisibility
|
||||
from nio.responses import RoomCreateError
|
||||
|
||||
from adapter.matrix.store import (
|
||||
get_user_meta,
|
||||
next_chat_id,
|
||||
next_platform_chat_id,
|
||||
set_room_meta,
|
||||
set_user_meta,
|
||||
)
|
||||
|
|
@ -62,6 +62,7 @@ async def provision_workspace_chat(
|
|||
|
||||
next_chat_index = int(user_meta.get("next_chat_index", 1))
|
||||
chat_id = f"C{next_chat_index}"
|
||||
platform_chat_id = await next_platform_chat_id(store)
|
||||
room_name = room_name_override or _default_room_name(chat_id)
|
||||
chat_resp = await client.room_create(
|
||||
name=room_name,
|
||||
|
|
@ -98,7 +99,7 @@ async def provision_workspace_chat(
|
|||
"display_name": room_name,
|
||||
"matrix_user_id": matrix_user_id,
|
||||
"space_id": space_id,
|
||||
"platform_chat_id": f"matrix:{chat_room_id}",
|
||||
"platform_chat_id": platform_chat_id,
|
||||
},
|
||||
)
|
||||
await chat_mgr.get_or_create(
|
||||
|
|
@ -118,7 +119,15 @@ async def provision_workspace_chat(
|
|||
}
|
||||
|
||||
|
||||
async def handle_invite(client: Any, room: Any, event: Any, platform, store, auth_mgr, chat_mgr) -> None:
|
||||
async def handle_invite(
|
||||
client: Any,
|
||||
room: Any,
|
||||
event: Any,
|
||||
platform,
|
||||
store,
|
||||
auth_mgr,
|
||||
chat_mgr,
|
||||
) -> None:
|
||||
matrix_user_id = getattr(event, "sender", "")
|
||||
display_name = getattr(room, "display_name", None) or matrix_user_id
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Awaitable, Callable
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
import structlog
|
||||
from nio.api import RoomVisibility
|
||||
from nio.responses import RoomCreateError
|
||||
|
||||
from adapter.matrix.store import get_user_meta, next_chat_id, set_room_meta
|
||||
from adapter.matrix.store import (
|
||||
get_user_meta,
|
||||
next_chat_id,
|
||||
next_platform_chat_id,
|
||||
set_room_meta,
|
||||
)
|
||||
from core.protocol import IncomingCommand, OutgoingMessage
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
|
@ -69,6 +75,7 @@ def make_handle_new_chat(
|
|||
|
||||
name = " ".join(event.args).strip() if event.args else ""
|
||||
chat_id = await next_chat_id(store, event.user_id)
|
||||
platform_chat_id = await next_platform_chat_id(store)
|
||||
room_name = name or f"Чат {chat_id}"
|
||||
|
||||
response = await client.room_create(
|
||||
|
|
@ -106,7 +113,7 @@ def make_handle_new_chat(
|
|||
"display_name": room_name,
|
||||
"matrix_user_id": event.user_id,
|
||||
"space_id": space_id,
|
||||
"platform_chat_id": f"matrix:{room_id}",
|
||||
"platform_chat_id": platform_chat_id,
|
||||
},
|
||||
)
|
||||
ctx = await chat_mgr.get_or_create(
|
||||
|
|
@ -151,7 +158,10 @@ def make_handle_rename(
|
|||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=event.chat_id,
|
||||
text="Этот чат не найден в локальном состоянии бота. Открой зарегистрированную комнату или создай новый чат через !new.",
|
||||
text=(
|
||||
"Этот чат не найден в локальном состоянии бота. "
|
||||
"Открой зарегистрированную комнату или создай новый чат через !new."
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
|
|
@ -181,7 +191,10 @@ def make_handle_archive(
|
|||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=event.chat_id,
|
||||
text="Этот чат не найден в локальном состоянии бота. Создай новый чат через !new.",
|
||||
text=(
|
||||
"Этот чат не найден в локальном состоянии бота. "
|
||||
"Создай новый чат через !new."
|
||||
),
|
||||
)
|
||||
]
|
||||
ctx = await chat_mgr.get(event.chat_id, user_id=event.user_id)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ from typing import TYPE_CHECKING
|
|||
import httpx
|
||||
import structlog
|
||||
|
||||
from adapter.matrix.store import get_room_meta, set_load_pending, set_platform_chat_id
|
||||
from adapter.matrix.store import (
|
||||
get_room_meta,
|
||||
next_platform_chat_id,
|
||||
set_load_pending,
|
||||
set_platform_chat_id,
|
||||
)
|
||||
from core.protocol import IncomingCommand, OutgoingEvent, OutgoingMessage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -45,7 +50,7 @@ async def _resolve_room_id(event: IncomingCommand, chat_mgr) -> str:
|
|||
|
||||
async def _resolve_context_scope(
|
||||
event: IncomingCommand,
|
||||
store: "StateStore",
|
||||
store: StateStore,
|
||||
chat_mgr,
|
||||
) -> tuple[str, str | None]:
|
||||
room_id = await _resolve_room_id(event, chat_mgr)
|
||||
|
|
@ -54,7 +59,7 @@ async def _resolve_context_scope(
|
|||
return room_id, platform_chat_id
|
||||
|
||||
|
||||
def make_handle_save(agent_api, store: "StateStore", prototype_state: "PrototypeStateStore"):
|
||||
def make_handle_save(agent_api, store: StateStore, prototype_state: PrototypeStateStore):
|
||||
async def handle_save(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list[OutgoingEvent]:
|
||||
|
|
@ -96,7 +101,7 @@ def make_handle_save(agent_api, store: "StateStore", prototype_state: "Prototype
|
|||
return handle_save
|
||||
|
||||
|
||||
def make_handle_load(store: "StateStore", prototype_state: "PrototypeStateStore"):
|
||||
def make_handle_load(store: StateStore, prototype_state: PrototypeStateStore):
|
||||
async def handle_load(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list[OutgoingEvent]:
|
||||
|
|
@ -123,17 +128,15 @@ def make_handle_load(store: "StateStore", prototype_state: "PrototypeStateStore"
|
|||
return handle_load
|
||||
|
||||
|
||||
def make_handle_reset(store: "StateStore", prototype_state: "PrototypeStateStore"):
|
||||
def make_handle_reset(store: StateStore, prototype_state: PrototypeStateStore):
|
||||
async def handle_reset(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list[OutgoingEvent]:
|
||||
import time
|
||||
|
||||
room_id = await _resolve_room_id(event, chat_mgr)
|
||||
room_meta = await get_room_meta(store, room_id)
|
||||
old_chat_id = (room_meta or {}).get("platform_chat_id") or room_id
|
||||
|
||||
new_chat_id = f"matrix:{room_id}#{int(time.time())}"
|
||||
new_chat_id = await next_platform_chat_id(store)
|
||||
await set_platform_chat_id(store, room_id, new_chat_id)
|
||||
|
||||
disconnect = getattr(platform, "disconnect_chat", None)
|
||||
|
|
@ -142,7 +145,12 @@ def make_handle_reset(store: "StateStore", prototype_state: "PrototypeStateStore
|
|||
|
||||
await prototype_state.clear_current_session(new_chat_id)
|
||||
|
||||
return [OutgoingMessage(chat_id=event.chat_id, text="Контекст сброшен. Агент не помнит предыдущий разговор.")]
|
||||
return [
|
||||
OutgoingMessage(
|
||||
chat_id=event.chat_id,
|
||||
text="Контекст сброшен. Агент не помнит предыдущий разговор.",
|
||||
)
|
||||
]
|
||||
|
||||
return handle_reset
|
||||
|
||||
|
|
@ -170,7 +178,7 @@ async def _call_reset_endpoint(agent_base_url: str, chat_id: str) -> list[Outgoi
|
|||
return [OutgoingMessage(chat_id=chat_id, text="Контекст сброшен.")]
|
||||
|
||||
|
||||
def make_handle_context(store: "StateStore", prototype_state: "PrototypeStateStore"):
|
||||
def make_handle_context(store: StateStore, prototype_state: PrototypeStateStore):
|
||||
async def handle_context(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list[OutgoingEvent]:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ from __future__ import annotations
|
|||
|
||||
from core.protocol import IncomingCommand, OutgoingMessage
|
||||
|
||||
|
||||
HELP_TEXT = "\n".join(
|
||||
[
|
||||
"Команды",
|
||||
|
|
@ -32,9 +31,7 @@ async def handle_settings(
|
|||
return [OutgoingMessage(chat_id=event.chat_id, text=MVP_UNAVAILABLE_TEXT)]
|
||||
|
||||
|
||||
async def handle_help(
|
||||
event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr
|
||||
) -> list:
|
||||
async def handle_help(event: IncomingCommand, auth_mgr, platform, chat_mgr, settings_mgr) -> list:
|
||||
return [OutgoingMessage(chat_id=event.chat_id, text=HELP_TEXT)]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ PENDING_CONFIRM_PREFIX = "matrix_pending_confirm:"
|
|||
LOAD_PENDING_PREFIX = "matrix_load_pending:"
|
||||
RESET_PENDING_PREFIX = "matrix_reset_pending:"
|
||||
STAGED_ATTACHMENTS_PREFIX = "matrix_staged_attachments:"
|
||||
PLATFORM_CHAT_SEQ_KEY = "matrix_platform_chat_seq"
|
||||
_STAGED_ATTACHMENTS_LOCKS: WeakValueDictionary[str, asyncio.Lock] = WeakValueDictionary()
|
||||
_PLATFORM_CHAT_SEQ_LOCK = asyncio.Lock()
|
||||
|
||||
|
||||
async def get_room_meta(store: StateStore, room_id: str) -> dict | None:
|
||||
|
|
@ -29,9 +31,7 @@ async def get_platform_chat_id(store: StateStore, room_id: str) -> str | None:
|
|||
return meta.get("platform_chat_id") if meta else None
|
||||
|
||||
|
||||
async def set_platform_chat_id(
|
||||
store: StateStore, room_id: str, platform_chat_id: str
|
||||
) -> None:
|
||||
async def set_platform_chat_id(store: StateStore, room_id: str, platform_chat_id: str) -> None:
|
||||
meta = dict(await get_room_meta(store, room_id) or {})
|
||||
meta["platform_chat_id"] = platform_chat_id
|
||||
await set_room_meta(store, room_id, meta)
|
||||
|
|
@ -71,16 +71,29 @@ async def next_chat_id(store: StateStore, matrix_user_id: str) -> str:
|
|||
return f"C{index}"
|
||||
|
||||
|
||||
async def next_platform_chat_id(store: StateStore) -> str:
|
||||
async with _PLATFORM_CHAT_SEQ_LOCK:
|
||||
data = await store.get(PLATFORM_CHAT_SEQ_KEY)
|
||||
index = int((data or {}).get("next_platform_chat_index", 1))
|
||||
await store.set(
|
||||
PLATFORM_CHAT_SEQ_KEY,
|
||||
{"next_platform_chat_index": index + 1},
|
||||
)
|
||||
return str(index)
|
||||
|
||||
|
||||
def _pending_confirm_key(user_id: str, room_id: str | None = None) -> str:
|
||||
if room_id is None:
|
||||
return f"{PENDING_CONFIRM_PREFIX}{user_id}"
|
||||
return f"{PENDING_CONFIRM_PREFIX}{user_id}:{room_id}"
|
||||
|
||||
|
||||
async def get_pending_confirm(
|
||||
store: StateStore, user_id: str, room_id: str | None = None
|
||||
) -> dict | None:
|
||||
return await store.get(_pending_confirm_key(user_id, room_id))
|
||||
|
||||
|
||||
async def set_pending_confirm(
|
||||
store: StateStore, user_id: str, room_id: str | dict, meta: dict | None = None
|
||||
) -> None:
|
||||
|
|
@ -146,9 +159,7 @@ def _staged_attachments_lock(room_id: str, user_id: str) -> asyncio.Lock:
|
|||
return lock
|
||||
|
||||
|
||||
async def get_staged_attachments(
|
||||
store: StateStore, room_id: str, user_id: str
|
||||
) -> list[dict]:
|
||||
async def get_staged_attachments(store: StateStore, room_id: str, user_id: str) -> list[dict]:
|
||||
data = await store.get(_staged_attachments_key(room_id, user_id))
|
||||
if not isinstance(data, dict):
|
||||
return []
|
||||
|
|
@ -166,9 +177,7 @@ async def add_staged_attachment(
|
|||
async with _staged_attachments_lock(room_id, user_id):
|
||||
attachments = await get_staged_attachments(store, room_id, user_id)
|
||||
attachments.append(attachment)
|
||||
await store.set(
|
||||
_staged_attachments_key(room_id, user_id), {"attachments": attachments}
|
||||
)
|
||||
await store.set(_staged_attachments_key(room_id, user_id), {"attachments": attachments})
|
||||
|
||||
|
||||
async def remove_staged_attachment_at(
|
||||
|
|
@ -181,16 +190,12 @@ async def remove_staged_attachment_at(
|
|||
|
||||
removed = attachments.pop(index)
|
||||
if attachments:
|
||||
await store.set(
|
||||
_staged_attachments_key(room_id, user_id), {"attachments": attachments}
|
||||
)
|
||||
await store.set(_staged_attachments_key(room_id, user_id), {"attachments": attachments})
|
||||
else:
|
||||
await store.delete(_staged_attachments_key(room_id, user_id))
|
||||
return removed
|
||||
|
||||
|
||||
async def clear_staged_attachments(
|
||||
store: StateStore, room_id: str, user_id: str
|
||||
) -> None:
|
||||
async def clear_staged_attachments(store: StateStore, room_id: str, user_id: str) -> None:
|
||||
async with _staged_attachments_lock(room_id, user_id):
|
||||
await store.delete(_staged_attachments_key(room_id, user_id))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue