fix: recover failed Feishu streaming updates

Feishu CardKit content updates can fail without raising, leaving a blank Generating card while the final streamed response skips normal send. Reopen streaming mode and retry once, close blank cards when the first update fails, and fall back to a regular card when final updates still fail.
This commit is contained in:
chengyongru
2026-06-18 00:02:54 +08:00
committed by Xubin Ren
parent 8ecc2d69c4
commit 4219911423
2 changed files with 128 additions and 24 deletions
+54 -2
View File
@@ -113,6 +113,22 @@ class TestCloseStreamingMode:
assert ch._close_streaming_mode_sync("card_1", 10) is False
class TestStreamUpdateWithReopen:
def test_reopens_streaming_mode_and_retries_update(self):
ch = _make_channel()
ch._client.cardkit.v1.card_element.content.side_effect = [
_mock_content_response(False),
_mock_content_response(True),
]
ch._client.cardkit.v1.card.settings.return_value = _mock_content_response(True)
assert ch._stream_update_text_with_reopen_sync("card_1", "hello", 4) == (True, 6)
assert ch._client.cardkit.v1.card_element.content.call_count == 2
settings_call = ch._client.cardkit.v1.card.settings.call_args[0][0]
assert settings_call.body.sequence == 5
assert '"streaming_mode": true' in settings_call.body.settings
class TestStreamUpdateText:
def test_returns_true_on_success(self):
ch = _make_channel()
@@ -149,6 +165,24 @@ class TestSendDelta:
ch._client.im.v1.message.create.assert_called_once()
ch._client.cardkit.v1.card_element.content.assert_called_once()
@pytest.mark.asyncio
async def test_first_delta_closes_blank_card_when_initial_update_fails(self):
ch = _make_channel()
ch._client.cardkit.v1.card.create.return_value = _mock_create_card_response("card_new")
ch._client.im.v1.message.create.return_value = _mock_send_response("om_new")
ch._client.cardkit.v1.card_element.content.return_value = _mock_content_response(False)
ch._client.cardkit.v1.card.settings.return_value = _mock_content_response(True)
await ch.send_delta("oc_chat1", "Hello ")
buf = ch._stream_bufs["oc_chat1"]
assert buf.text == "Hello "
assert buf.card_id is None
assert ch._client.cardkit.v1.card_element.content.call_count == 2
assert ch._client.cardkit.v1.card.settings.call_count == 2
close_call = ch._client.cardkit.v1.card.settings.call_args_list[-1][0][0]
assert '"streaming_mode": false' in close_call.body.settings
@pytest.mark.asyncio
async def test_group_delta_uses_create_when_reply_disabled(self):
ch = _make_channel(reply_to_message=False)
@@ -338,11 +372,29 @@ class TestSendDelta:
await ch.send_delta("oc_chat1", "", metadata={"_stream_end": True})
assert "oc_chat1" not in ch._stream_bufs
# Should NOT attempt to close streaming mode since update failed
ch._client.cardkit.v1.card.settings.assert_not_called()
assert ch._client.cardkit.v1.card.settings.call_count == 2
# Should fall back to sending a regular interactive card
ch._client.im.v1.message.create.assert_called_once()
@pytest.mark.asyncio
async def test_stream_end_reopens_streaming_card_before_fallback(self):
ch = _make_channel()
ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf(
text="Recovered content", card_id="card_1", sequence=3, last_edit=0.0,
)
ch._client.cardkit.v1.card_element.content.side_effect = [
_mock_content_response(False),
_mock_content_response(True),
]
ch._client.cardkit.v1.card.settings.return_value = _mock_content_response(True)
await ch.send_delta("oc_chat1", "", metadata={"_stream_end": True})
assert "oc_chat1" not in ch._stream_bufs
assert ch._client.cardkit.v1.card_element.content.call_count == 2
assert ch._client.cardkit.v1.card.settings.call_count == 2
ch._client.im.v1.message.create.assert_not_called()
@pytest.mark.asyncio
async def test_stream_end_without_buf_is_noop(self):
ch = _make_channel()