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
This commit is contained in:
xzq.xu 2026-04-08 11:49:27 +08:00 committed by chengyongru
parent a4bb1923ac
commit 586d4e2411
2 changed files with 14 additions and 16 deletions

View File

@ -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():

View File

@ -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: