Prechádzať zdrojové kódy

Add a plan for the missing BasePlatformAdapter overrides

Records what gateway/platforms/base.py offers, what chattolib can do, and
which of the two the adapter currently leaves on the table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Paul-Dieter Klumpp 1 týždeň pred
rodič
commit
9e03b89614
1 zmenil súbory, kde vykonal 125 pridanie a 0 odobranie
  1. 125 0
      PLAN.md

+ 125 - 0
PLAN.md

@@ -0,0 +1,125 @@
+# Plan: BasePlatformAdapter-Overrides nachziehen (Punkte 1–8)
+
+Referenz: `gateway/platforms/base.py` aus dem Hermes-Checkout (HEAD `7b25941`,
+identisch mit github.com/nousresearch/hermes-agent `main`).
+
+Jeder Punkt: was die Basis anbietet, was chattolib kann, was implementiert wird.
+
+---
+
+## 1. `edit_message` — base.py:4035, Default `SendResult(success=False, "Not supported")`
+
+chattolib: `client.update_message(room_id, event_id, body=...)`.
+
+Aufrufer: `gateway/stream_consumer.py:412` (Zeilen 1451, 1824, 2233) und
+`gateway/run.py:4530/28592/29493`. Solange `False` zurückkommt, wird jedes
+Streaming-Update als *neue* Nachricht gesendet.
+
+- Signatur exakt wie Basis, inkl. `finalize: bool = False` (für Chatto ein
+  No-op — kein Rich-Card-Lifecycle, `REQUIRES_EDIT_FINALIZE` bleibt aus).
+- Content länger als `MAX_MESSAGE_LENGTH` → `success=False` zurückgeben statt
+  still zu kürzen, damit der Aufrufer auf `send()` (das splittet) zurückfällt.
+- Fehler von chattolib → `success=False, retryable=True`.
+- Editierte ID via `_mark_seen()`, damit das eigene `message_edited`-Event
+  nicht zurückläuft.
+
+## 2. `delete_message` — base.py:4064, Default `False`
+
+chattolib: `client.delete_message(room_id, event_id) -> bool`.
+
+Genutzt vom Fresh-Final-Cleanup des Stream-Consumers und von
+`_schedule_ephemeral_delete` (EphemeralReply-TTL). Durchreichen, Exceptions zu
+`False`.
+
+## 3. Eingehende Attachments → `MessageEvent.media_urls` / `media_types`
+
+Kein Override, sondern eine Lücke: `_dispatch_message_posted` verwirft
+Nachrichten mit leerem `body` — ein reines Bild/PDF an Hermes verschwindet.
+
+- `Message.attachments` (`list[MessageAttachment]`) trägt `asset_url.url`
+  (vorsignierte URL), `filename`, `content_type` — kein `get_asset`-Roundtrip
+  nötig.
+- Bytes laden (httpx, vendored), gegen `validate_inbound_media_size` prüfen,
+  durch `cache_media_bytes(data, filename=…, mime_type=…)` schicken — das ist
+  der geteilte Funnel aller Adapter (base.py:2212).
+- `media_urls` = lokale Cache-Pfade, `media_types` = MIME, `message_type` nach
+  derselben Präzedenz wie Teams/Signal: DOCUMENT > PHOTO > VIDEO > AUDIO > TEXT.
+- Early-Return nur noch, wenn *weder* Body *noch* Attachments da sind.
+
+## 4. `create_handoff_thread` — base.py:4008, Default `None`
+
+Seed-Message in den Parent-Room posten, deren ID ist der Thread-Root
+(dasselbe Muster wie Slack, adapter.py:2278 dort). Best-effort
+`follow_thread`. Bei DM-Räumen `None` (Chatto-DMs können keine Threads).
+
+## 5. `send_document` / `send_video` / `send_voice` — base.py:4718 / 4691 / 4545
+
+Defaults schicken „⚠️ Couldn't deliver …" als Text. Die Upload-Mechanik gibt es
+schon (`_upload_asset`, chunked), sie hängt nur an `send_image_file`.
+
+- Gemeinsamen Helper `_send_local_attachment()` aus `send_image_file`
+  herausziehen; die vier Methoden werden dünne Wrapper.
+- `validate_media_delivery_path()` bleibt der Gate vor jedem Upload.
+- Fällt der Upload aus, greift weiter der Textkommentar der Basis-Semantik
+  (nie den Host-Pfad in den Chat schreiben).
+
+## 6. `send_multiple_images` — base.py:4398
+
+Default sendet einzeln. Chatto-Nachrichten tragen mehrere Attachments:
+alle hochladen, *ein* `post_message` mit `attachment_asset_ids=[…]`.
+Bei ≤1 Bild oder Upload-Fehler auf `super()` zurückfallen.
+`file://`-URIs entquoten, `http(s)://` vorher herunterladen.
+
+## 7. Reaction-Events → `_reaction_handler` (`set_reaction_handler`, base.py:3691)
+
+`reaction_added`/`reaction_removed` werden in `_handle_realtime_event` heute
+explizit verworfen. `ReactionPayload` (`room_id`, `message_event_id`, `emoji`)
+ist fertig dekodiert.
+
+- Normalisiertes Dict exakt im Slack-Format: `platform`, `event_name`
+  (`reaction:added` / `reaction:removed`), `reaction`, `user_id`,
+  `item_user_id`, `item_type`, `channel_id`, `message_ts`, `event_ts`,
+  `raw_event`.
+- Eigene Reaktionen (Lifecycle 👀/✅/❌) rausfiltern, sonst Endlosschleife.
+- Handler-Aufruf non-blocking (try/except), `getattr`-Guard wie bei Slack.
+- Nebenbei: `message_edited`/`message_retracted` in die Known-Kinds-Liste,
+  damit sie nicht als `unknown event kind` geloggt werden.
+
+## 8. `format_message` — base.py:7235, Default = Identität
+
+Bewusst minimal: Chatto rendert Markdown nativ, es gibt nichts zu escapen.
+Implementiert wird nur, was messbar schiefgeht — CRLF-Normalisierung und das
+Kappen von >2 Leerzeilen am Stück. Zusätzlich der `hasattr`-Aufruf in `send()`
+(adapter.py:681) durch einen direkten Aufruf ersetzt, der heute effektiv ein
+No-op ist. **Dünnster Punkt der Liste** — mehr wäre Spekulation über
+Chatto-Renderer-Details, die sich real-testen lässt.
+
+---
+
+## Tests
+
+Alle in `test_adapter.py`, gleicher Stil (AsyncMock-Client, keine Netzwerk-IO):
+
+- `TestMessageEditing`: die beiden `xfail(strict=True)`-Marker entfernen,
+  ergänzen: Overlong-Content → `success=False`, chattolib-Fehler → `retryable`.
+- `TestInboundAttachments`: Bild-Attachment → `media_urls`/`media_types`
+  gesetzt, `message_type == PHOTO`; Body leer + Attachment → nicht verworfen;
+  Body leer + kein Attachment → weiterhin verworfen; Download-Fehler → Event
+  trotzdem zugestellt.
+- `TestHandoffThread`: Room → Seed-ID; DM → `None`.
+- `TestNativeSends`: `send_document`/`send_video`/`send_voice` laden hoch und
+  posten mit `attachment_asset_ids`; unsicherer Pfad → Fallback-Text.
+- `TestSendMultipleImages`: zwei lokale Dateien → *ein* `post_message` mit zwei
+  Asset-IDs.
+- `TestReactionForwarding`: Fremd-Reaktion → Handler mit korrektem Dict;
+  Selbst-Reaktion → kein Handler-Aufruf; Handler-Exception → kein Absturz.
+- `TestFormatMessage`: CRLF und Leerzeilen.
+
+Lauf: `HERMES_ROOT=/tmp/hermes uv run --group dev --with pyyaml pytest -q`
+(das Plugin hängt nicht am Agent; `HERMES_ROOT` zeigt auf ein Checkout).
+
+## Nicht in Scope
+
+`send_draft`/`supports_draft_streaming` (Telegram-Draft-API, kein Chatto-Pendant),
+Streaming-TTS, `enforces_own_access_policy` (Security-Entscheidung, gehört
+separat besprochen — siehe Punkt 9 der Analyse).