From 943006e936770847d0762efedfea20a989a4b958 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Fri, 23 Oct 2026 18:42:22 -0400 Subject: [PATCH 1/2] fix(tools): accept JSON-encoded messages in send_message_to_user Co-authored-by: Cursor --- astrbot/core/tools/message_tools.py | 7 +++++++ tests/unit/test_message_tools.py | 27 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/astrbot/core/tools/message_tools.py b/astrbot/core/tools/message_tools.py index 344e0d3c7e5..65c0a6cbd49 100644 --- a/astrbot/core/tools/message_tools.py +++ b/astrbot/core/tools/message_tools.py @@ -220,6 +220,13 @@ async def call( ): return permission_error messages = kwargs.get("messages") + if isinstance(messages, str): + # Some models send the array as a JSON-encoded string, especially + # for long multi-line text. + try: + messages = json.loads(messages) + except json.JSONDecodeError: + pass if not isinstance(messages, list) or not messages: return "error: messages parameter is empty or invalid." diff --git a/tests/unit/test_message_tools.py b/tests/unit/test_message_tools.py index d903a7e1b1d..a273b63e9f4 100644 --- a/tests/unit/test_message_tools.py +++ b/tests/unit/test_message_tools.py @@ -307,6 +307,29 @@ async def test_send_message_empty_messages_returns_error(): assert "messages" in result.lower() +@pytest.mark.asyncio +async def test_send_message_accepts_json_encoded_messages_string(): + """A JSON-encoded messages array is decoded instead of rejected.""" + tool = SendMessageToUserTool() + ctx = _make_context(current_session="feishu:GroupMessage:oc_xxx") + result = await tool.call( + ctx, + messages='[{"type": "plain", "text": "line 1\\n\\nline \\"2\\""}]', + ) + assert "Message sent to session" in result + chain = ctx.context.context.send_message.call_args[0][1] + assert chain.chain[0].text == 'line 1\n\nline "2"' + + +@pytest.mark.asyncio +async def test_send_message_rejects_non_json_messages_string(): + tool = SendMessageToUserTool() + ctx = _make_context() + result = await tool.call(ctx, messages="hello") + assert result == "error: messages parameter is empty or invalid." + ctx.context.context.send_message.assert_not_called() + + @pytest.mark.asyncio async def test_send_message_missing_image_path_stops_before_send(tmp_path, monkeypatch): """Missing image paths fail before sending any message components.""" @@ -356,7 +379,9 @@ async def test_restricted_role_cannot_send_arbitrary_local_absolute_file( tool = SendMessageToUserTool() ctx = _make_context( role=role, - local_permissions={role: {"filesystem_scope": scope, "allow_execution": False}} if scope else None, + local_permissions={role: {"filesystem_scope": scope, "allow_execution": False}} + if scope + else None, ) secret_path = tmp_path / "secret.txt" secret_path.write_text("secret", encoding="utf-8") From dddfc67cc4d475e991af03192d8a74dc2d28fa17 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 26 Oct 2026 13:24:09 -0400 Subject: [PATCH 2/2] fix(tools): handle JSON decoder limits without sending messages --- astrbot/core/tools/message_tools.py | 2 +- tests/unit/test_message_tools.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/astrbot/core/tools/message_tools.py b/astrbot/core/tools/message_tools.py index 65c0a6cbd49..be6b456a440 100644 --- a/astrbot/core/tools/message_tools.py +++ b/astrbot/core/tools/message_tools.py @@ -225,7 +225,7 @@ async def call( # for long multi-line text. try: messages = json.loads(messages) - except json.JSONDecodeError: + except (ValueError, RecursionError): pass if not isinstance(messages, list) or not messages: return "error: messages parameter is empty or invalid." diff --git a/tests/unit/test_message_tools.py b/tests/unit/test_message_tools.py index a273b63e9f4..d75bab16beb 100644 --- a/tests/unit/test_message_tools.py +++ b/tests/unit/test_message_tools.py @@ -321,6 +321,25 @@ async def test_send_message_accepts_json_encoded_messages_string(): assert chain.chain[0].text == 'line 1\n\nline "2"' +@pytest.mark.asyncio +@pytest.mark.parametrize( + "messages", + ["[" * 10000 + "]" * 10000, "[" + "1" * 10000 + "]"], + ids=["deep-nesting", "oversized-integer"], +) +async def test_send_message_rejects_json_decoder_resource_limits(messages): + """Decoder limits return a validation error before any message is sent. + + Args: + messages: JSON exceeding the nesting or integer-conversion limit. + """ + tool = SendMessageToUserTool() + ctx = _make_context() + result = await tool.call(ctx, messages=messages) + assert result == "error: messages parameter is empty or invalid." + ctx.context.context.send_message.assert_not_called() + + @pytest.mark.asyncio async def test_send_message_rejects_non_json_messages_string(): tool = SendMessageToUserTool()