From 586d4e24114d20f66b9845201bcc40437d4fdf21 Mon Sep 17 00:00:00 2001 From: "xzq.xu" Date: Wed, 8 Apr 2026 11:49:27 +0800 Subject: [PATCH] fix(feishu): preserve tool hints in final card content Tool hints should be kept as permanent content in the streaming card so users can see which tools were called (matching the standalone card behavior). Previously, hints were stripped when new deltas arrived or when the stream ended, causing tool call information to disappear. Now: - New delta: hint becomes permanent content, delta appends after it - New tool hint: replaces the previous hint (unchanged) - Resuming/stream_end: hint is preserved in the final text Updated 3 tests to verify hint preservation semantics. Made-with: Cursor --- nanobot/channels/feishu.py | 9 ++------- tests/channels/test_feishu_streaming.py | 21 ++++++++++++--------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/nanobot/channels/feishu.py b/nanobot/channels/feishu.py index 34d59bc5a..2441c20c5 100644 --- a/nanobot/channels/feishu.py +++ b/nanobot/channels/feishu.py @@ -1287,9 +1287,7 @@ class FeishuChannel(BaseChannel): # next segment appends to the same card. buf = self._stream_bufs.get(chat_id) if buf and buf.card_id and buf.text: - if buf.tool_hint_len > 0: - buf.text = buf.text[:-buf.tool_hint_len] - buf.tool_hint_len = 0 + buf.tool_hint_len = 0 buf.sequence += 1 await loop.run_in_executor( None, self._stream_update_text_sync, buf.card_id, buf.text, buf.sequence, @@ -1299,9 +1297,7 @@ class FeishuChannel(BaseChannel): buf = self._stream_bufs.pop(chat_id, None) if not buf or not buf.text: return - if buf.tool_hint_len > 0: - buf.text = buf.text[:-buf.tool_hint_len] - buf.tool_hint_len = 0 + buf.tool_hint_len = 0 if buf.card_id: buf.sequence += 1 await loop.run_in_executor( @@ -1338,7 +1334,6 @@ class FeishuChannel(BaseChannel): buf = _FeishuStreamBuf() self._stream_bufs[chat_id] = buf if buf.tool_hint_len > 0: - buf.text = buf.text[:-buf.tool_hint_len] buf.tool_hint_len = 0 buf.text += delta if not buf.text.strip(): diff --git a/tests/channels/test_feishu_streaming.py b/tests/channels/test_feishu_streaming.py index 3683d0f07..1559ef8d3 100644 --- a/tests/channels/test_feishu_streaming.py +++ b/tests/channels/test_feishu_streaming.py @@ -316,8 +316,8 @@ class TestToolHintInlineStreaming: ch._client.im.v1.message.create.assert_not_called() @pytest.mark.asyncio - async def test_tool_hint_stripped_on_next_delta(self): - """When new delta arrives, the previously appended tool hint is removed.""" + async def test_tool_hint_preserved_on_next_delta(self): + """When new delta arrives, the tool hint is kept as permanent content and delta appends after it.""" ch = _make_channel() suffix = "\n\n---\nšŸ”§ web_fetch(\"url\")" ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf( @@ -330,7 +330,9 @@ class TestToolHintInlineStreaming: await ch.send_delta("oc_chat1", " continued") buf = ch._stream_bufs["oc_chat1"] - assert buf.text == "Partial answer continued" + assert "Partial answer" in buf.text + assert "šŸ”§ web_fetch" in buf.text + assert buf.text.endswith(" continued") assert buf.tool_hint_len == 0 @pytest.mark.asyncio @@ -377,8 +379,8 @@ class TestToolHintInlineStreaming: assert buf.text.endswith("šŸ”§ $ git status") @pytest.mark.asyncio - async def test_tool_hint_stripped_on_resuming_flush(self): - """When _resuming flushes the buffer, tool hint suffix is cleaned.""" + async def test_tool_hint_preserved_on_resuming_flush(self): + """When _resuming flushes the buffer, tool hint is kept as permanent content.""" ch = _make_channel() suffix = "\n\n---\nšŸ”§ $ cd /project" ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf( @@ -391,12 +393,13 @@ class TestToolHintInlineStreaming: await ch.send_delta("oc_chat1", "", metadata={"_stream_end": True, "_resuming": True}) buf = ch._stream_bufs["oc_chat1"] - assert buf.text == "Partial answer" + assert "Partial answer" in buf.text + assert "šŸ”§ $ cd /project" in buf.text assert buf.tool_hint_len == 0 @pytest.mark.asyncio - async def test_tool_hint_stripped_on_final_stream_end(self): - """When final _stream_end closes the card, tool hint suffix is cleaned from text.""" + async def test_tool_hint_preserved_on_final_stream_end(self): + """When final _stream_end closes the card, tool hint is kept in the final text.""" ch = _make_channel() suffix = "\n\n---\nšŸ”§ web_fetch(\"url\")" ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf( @@ -411,7 +414,7 @@ class TestToolHintInlineStreaming: assert "oc_chat1" not in ch._stream_bufs update_call = ch._client.cardkit.v1.card_element.content.call_args[0][0] - assert "šŸ”§" not in update_call.body.content + assert "šŸ”§" in update_call.body.content class TestSendMessageReturnsId: