feat(feishu): display tool calls in code block messages

- Tool hint messages with _tool_hint metadata now render as formatted code blocks
- Uses Feishu interactive card message type with markdown code fences
- Shows "Tool Call" header followed by code in a monospace block
- Adds comprehensive unit tests for the new functionality

Co-Authorship-Bot: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tony
2026-03-13 14:43:47 +08:00
parent df89bd2dfa
commit 7261bd8c3f
2 changed files with 27 additions and 17 deletions

View File

@@ -822,18 +822,24 @@ class FeishuChannel(BaseChannel):
receive_id_type = "chat_id" if msg.chat_id.startswith("oc_") else "open_id" receive_id_type = "chat_id" if msg.chat_id.startswith("oc_") else "open_id"
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
# Handle tool hint messages as code blocks # Handle tool hint messages as code blocks in interactive cards
if msg.metadata.get("_tool_hint"): if msg.metadata.get("_tool_hint"):
if msg.content and msg.content.strip(): if msg.content and msg.content.strip():
code_content = { # Create a simple card with a code block
"title": "Tool Call", code_text = msg.content.strip()
"code": msg.content.strip(), card = {
"language": "text" "config": {"wide_screen_mode": True},
"elements": [
{
"tag": "markdown",
"content": f"**Tool Call**\n\n```\n{code_text}\n```"
}
]
} }
await loop.run_in_executor( await loop.run_in_executor(
None, self._send_message_sync, None, self._send_message_sync,
receive_id_type, msg.chat_id, "code", receive_id_type, msg.chat_id, "interactive",
json.dumps(code_content, ensure_ascii=False), json.dumps(card, ensure_ascii=False),
) )
return return

View File

@@ -24,7 +24,7 @@ def mock_feishu_channel():
def test_tool_hint_sends_code_message(mock_feishu_channel): def test_tool_hint_sends_code_message(mock_feishu_channel):
"""Tool hint messages should be sent as code blocks.""" """Tool hint messages should be sent as interactive cards with code blocks."""
msg = OutboundMessage( msg = OutboundMessage(
channel="feishu", channel="feishu",
chat_id="oc_123456", chat_id="oc_123456",
@@ -37,20 +37,23 @@ def test_tool_hint_sends_code_message(mock_feishu_channel):
import asyncio import asyncio
asyncio.run(mock_feishu_channel.send(msg)) asyncio.run(mock_feishu_channel.send(msg))
# Verify code message was sent # Verify interactive message with card was sent
assert mock_send.call_count == 1 assert mock_send.call_count == 1
call_args = mock_send.call_args[0] call_args = mock_send.call_args[0]
receive_id_type, receive_id, msg_type, content = call_args receive_id_type, receive_id, msg_type, content = call_args
assert receive_id_type == "chat_id" assert receive_id_type == "chat_id"
assert receive_id == "oc_123456" assert receive_id == "oc_123456"
assert msg_type == "code" assert msg_type == "interactive"
# Parse content to verify structure # Parse content to verify card structure
content_dict = json.loads(content) card = json.loads(content)
assert content_dict["title"] == "Tool Call" assert card["config"]["wide_screen_mode"] is True
assert content_dict["code"] == 'web_search("test query")' assert len(card["elements"]) == 1
assert content_dict["language"] == "text" assert card["elements"][0]["tag"] == "markdown"
# Check that code block is properly formatted
expected_md = "**Tool Call**\n\n```\nweb_search(\"test query\")\n```"
assert card["elements"][0]["content"] == expected_md
def test_tool_hint_empty_content_does_not_send(mock_feishu_channel): def test_tool_hint_empty_content_does_not_send(mock_feishu_channel):
@@ -105,6 +108,7 @@ def test_tool_hint_multiple_tools_in_one_message(mock_feishu_channel):
asyncio.run(mock_feishu_channel.send(msg)) asyncio.run(mock_feishu_channel.send(msg))
call_args = mock_send.call_args[0] call_args = mock_send.call_args[0]
msg_type = call_args[2]
content = json.loads(call_args[3]) content = json.loads(call_args[3])
assert content["code"] == 'web_search("query"), read_file("/path/to/file")' assert msg_type == "interactive"
assert "\n" not in content["code"] # Single line as intended assert "web_search(\"query\"), read_file(\"/path/to/file\")" in content["elements"][0]["content"]