瀏覽代碼

feat(chatto): add chatto_mark_thread_as_read tool

Expose chattolib's mark_thread_as_read so the bot can update its read
marker in a thread — useful after processing a thread's full context.

- chatto_mark_thread_as_read: room_id/name + thread_root_event_id
  returns previous_last_read_at timestamp on success.
Paul Klumpp 1 周之前
父節點
當前提交
de8aa90dca
共有 3 個文件被更改,包括 87 次插入 和 0 次删除
  1. 85 0
      adapter.py
  2. 1 0
      plugin.yaml
  3. 1 0
      test_adapter.py

+ 85 - 0
adapter.py

@@ -4693,6 +4693,38 @@ CHATTO_SEND_ROOM_SCHEMA = {
     },
 }
 
+CHATTO_MARK_THREAD_AS_READ_SCHEMA = {
+    "name": "chatto_mark_thread_as_read",
+    "description": (
+        "Mark a thread as read for the bot. This updates the server's read "
+        "marker so Chatto clients show the thread as read. Useful after the bot "
+        "has processed a thread's full context."
+    ),
+    "parameters": {
+        "type": "object",
+        "properties": {
+            "room_id": {
+                "type": "string",
+                "description": "The Chatto room ID of the thread.",
+            },
+            "room_name": {
+                "type": "string",
+                "description": (
+                    "Optional: room name — one of ``room_id`` or ``room_name`` is required."
+                ),
+            },
+            "thread_root_event_id": {
+                "type": "string",
+                "description": (
+                    "The event ID of the thread root message."
+                ),
+            },
+        },
+        "required": ["thread_root_event_id"],
+        "additionalProperties": False,
+    },
+}
+
 
 CHATTO_GET_MESSAGE_SCHEMA = {
     "name": "chatto_get_message",
@@ -5940,6 +5972,36 @@ async def _fetch_link_preview(args: dict) -> str:
         )
 
 
+async def _mark_thread_as_read(args: dict) -> str:
+    """Mark a thread as read. See _handle_chatto_mark_thread_as_read."""
+    room_id = args.get("room_id") or ""
+    room_name = args.get("room_name") or ""
+    thread_root_event_id = args.get("thread_root_event_id") or ""
+
+    if not room_id and not room_name:
+        raise _refuse("Exactly one of room_id or room_name is required")
+    if not thread_root_event_id:
+        raise _refuse("thread_root_event_id is required")
+
+    async with _tool_client(_chatto_config_for_tools()) as client:
+        if not room_id:
+            resolved = await _resolve_room(client, room_id, room_name)
+            room_id = resolved.room.id
+        prev_last_read = await client.mark_thread_as_read(
+            room_id, thread_root_event_id
+        )
+        return json.dumps(
+            {
+                "success": True,
+                "room_id": room_id,
+                "thread_root_event_id": thread_root_event_id,
+                "previous_last_read_at": (
+                    _iso_or_none(prev_last_read) if prev_last_read else None
+                ),
+            }
+        )
+
+
 async def _handle_chatto_fetch_link_preview(args: dict) -> str:
     """Tool handler for chatto_fetch_link_preview.
 
@@ -5958,6 +6020,24 @@ async def _handle_chatto_fetch_link_preview(args: dict) -> str:
         return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
 
 
+async def _handle_chatto_mark_thread_as_read(args: dict) -> str:
+    """Tool handler for chatto_mark_thread_as_read.
+
+    Returns a JSON string: Hermes' tool contract accepts no other type, and a
+    dict (or an unflagged coroutine) fails at dispatch with "unsupported result
+    type". Every early exit arrives here as a _ToolRefusal and leaves as that
+    refusal's own JSON, so no path escapes as an unhandled error.
+    """
+    try:
+        return await _mark_thread_as_read(args)
+    except _ToolRefusal as refusal:
+        return refusal.result_json
+    except ChattoAuthError as exc:
+        return json.dumps({"success": False, "error": f"Authentication error: {exc}"})
+    except ChattoError as exc:
+        return json.dumps({"success": False, "error": f"Chatto API error: {exc}"})
+
+
 async def _handle_chatto_get_message(args: dict) -> str:
     """Tool handler for chatto_get_message.
 
@@ -6116,6 +6196,11 @@ _PLUGIN_TOOLS = (
         CHATTO_FETCH_LINK_PREVIEW_SCHEMA,
         _handle_chatto_fetch_link_preview,
     ),
+    (
+        "chatto_mark_thread_as_read",
+        CHATTO_MARK_THREAD_AS_READ_SCHEMA,
+        _handle_chatto_mark_thread_as_read,
+    ),
 )
 
 

+ 1 - 0
plugin.yaml

@@ -22,6 +22,7 @@ provides_tools:
   - chatto_set_custom_status
   - chatto_delete_custom_status
   - chatto_fetch_link_preview
+  - chatto_mark_thread_as_read
 requires_env:
   - name: CHATTO_BASE_URL
     description: "Base URL of the Chatto server (e.g. https://chat.example.com) leave blank for ChattoHQ (https://chat.chatto.run)"

+ 1 - 0
test_adapter.py

@@ -4474,6 +4474,7 @@ class TestPluginToolSchemas:
             "chatto_set_custom_status",
             "chatto_delete_custom_status",
             "chatto_fetch_link_preview",
+            "chatto_mark_thread_as_read",
         ]
         for tool in ctx.registered_tools:
             assert tool["is_async"] is True, tool["name"]