From 845ee1421114db3f23f82d0c4890b5136aa57f19 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Mon, 4 May 2026 00:21:08 +0800 Subject: [PATCH 1/2] Fix CronJob message delivery when models stringify message arrays SendMessageToUser now recovers when providers deliver the messages array as a JSON string, which keeps multiline CronJob messages from failing validation before component construction. Constraint: Keep the compatibility fix local to send_message_to_user for this issue. Rejected: Global recursive tool argument parsing because it has wider schema and string-content risk. Directive: Preserve existing list validation after the JSON-string recovery path. Confidence: high Scope-risk: low Reversibility: Revert the local parsing block and its focused tests. Tested: uv run pytest tests/unit/test_message_tools.py Tested: uv run ruff check astrbot/core/tools/message_tools.py tests/unit/test_message_tools.py Tested: uv run ruff format --check astrbot/core/tools/message_tools.py tests/unit/test_message_tools.py Related: Fixes #7961 Co-authored-by: OmX --- astrbot/core/tools/message_tools.py | 8 ++++++ tests/unit/test_message_tools.py | 41 +++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/astrbot/core/tools/message_tools.py b/astrbot/core/tools/message_tools.py index c3f33a7e8b..5f8fe67ad5 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, TypeError): + 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..74d677a6f7 100644 --- a/tests/unit/test_message_tools.py +++ b/tests/unit/test_message_tools.py @@ -133,3 +133,44 @@ 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") + import json as _json + + 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() From bdfc0d53fa806d6fe6d83653e5e5d65ea7514655 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Mon, 4 May 2026 00:28:39 +0800 Subject: [PATCH 2/2] Clarify JSON-string message recovery after review The review feedback identified two small clarity issues: the TypeError handler was unreachable after the string guard, and the multiline regression test used a local json import. Narrow the exception handling and keep imports at module scope. Constraint: Keep this follow-up limited to PR review comments on #7978. Rejected: Broader tool argument normalization because it is unrelated to these review threads. Directive: Keep invalid JSON strings flowing into the existing messages validation error. Confidence: high Scope-risk: low Reversibility: Revert this cleanup commit without changing the original compatibility behavior. Tested: uv run pytest tests/unit/test_message_tools.py Tested: uv run ruff check astrbot/core/tools/message_tools.py tests/unit/test_message_tools.py Tested: uv run ruff format --check astrbot/core/tools/message_tools.py tests/unit/test_message_tools.py Related: PR #7978 review Co-authored-by: OmX --- astrbot/core/tools/message_tools.py | 2 +- tests/unit/test_message_tools.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/astrbot/core/tools/message_tools.py b/astrbot/core/tools/message_tools.py index 5f8fe67ad5..018933d0ea 100644 --- a/astrbot/core/tools/message_tools.py +++ b/astrbot/core/tools/message_tools.py @@ -138,7 +138,7 @@ async def call( if isinstance(messages, str): try: messages = json.loads(messages) - except (json.JSONDecodeError, TypeError): + 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 74d677a6f7..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 @@ -153,10 +154,9 @@ 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") - import json as _json long_text = "line1\n\nline2\nline3" - messages_str = _json.dumps([{"type": "plain", "text": long_text}]) + 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