diff --git a/astrbot/core/tools/message_tools.py b/astrbot/core/tools/message_tools.py index c3f33a7e8b..018933d0ea 100644 --- a/astrbot/core/tools/message_tools.py +++ b/astrbot/core/tools/message_tools.py @@ -132,6 +132,14 @@ async def call( ): return permission_error messages = kwargs.get("messages") + # Some LLMs (e.g. MiniMax) may serialize the array value as a JSON string + # when the text contains newlines. Try to recover. + # https://github.com/AstrBotDevs/AstrBot/issues/7961 + if isinstance(messages, str): + 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 eedee80abb..3e30cbcec7 100644 --- a/tests/unit/test_message_tools.py +++ b/tests/unit/test_message_tools.py @@ -1,5 +1,6 @@ """Tests for send_message_to_user session handling.""" +import json from types import SimpleNamespace from unittest.mock import AsyncMock @@ -133,3 +134,43 @@ async def test_send_message_empty_messages_returns_error(): result = await tool.call(ctx, messages=[], session="oc_xxx") assert "error:" in result assert "messages" in result.lower() + + +# JSON-string messages compatibility for issue #7961. + + +@pytest.mark.asyncio +async def test_messages_as_json_string_parsed(): + """JSON-string array messages are parsed before validation.""" + tool = SendMessageToUserTool() + ctx = _make_context(current_session="feishu:GroupMessage:oc_xxx") + messages_str = '[{"type": "plain", "text": "hello from string"}]' + result = await tool.call(ctx, messages=messages_str, session="oc_xxx") + assert "Message sent to session" in result + + +@pytest.mark.asyncio +async def test_messages_as_json_string_with_newlines(): + """JSON-string messages preserve multiline text after parsing.""" + tool = SendMessageToUserTool() + ctx = _make_context(current_session="feishu:GroupMessage:oc_xxx") + + long_text = "line1\n\nline2\nline3" + messages_str = json.dumps([{"type": "plain", "text": long_text}]) + result = await tool.call(ctx, messages=messages_str, session="oc_xxx") + assert "Message sent to session" in result + call_args = ctx.context.context.send_message.call_args + chain = call_args[0][1] + assert len(chain.chain) == 1 + assert chain.chain[0].text == long_text + + +@pytest.mark.asyncio +async def test_messages_as_invalid_json_string_returns_error(): + """Invalid JSON-string messages still return a validation error.""" + tool = SendMessageToUserTool() + ctx = _make_context() + result = await tool.call(ctx, messages="not valid json at all", session="oc_xxx") + assert "error:" in result or "invalid" in result.lower() + result2 = await tool.call(ctx, messages="", session="oc_xxx") + assert "error:" in result2 or "invalid" in result2.lower()