mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-05-20 16:42:25 +00:00
feat(feishu): add reply_in_thread for visual topic grouping
When reply_to_message config is enabled, the bot's first reply now uses reply_in_thread=True to create a visual topic/thread in the Feishu client. Subsequent chunks fall back to regular create. The reply_to_message default remains False for backward compatibility. Failed replies still fall back to regular send — messages are never silently dropped.
This commit is contained in:
parent
934372d90b
commit
a0e97e360e
@ -1101,17 +1101,23 @@ class FeishuChannel(BaseChannel):
|
|||||||
logger.debug("Feishu: error fetching parent message {}: {}", message_id, e)
|
logger.debug("Feishu: error fetching parent message {}: {}", message_id, e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _reply_message_sync(self, parent_message_id: str, msg_type: str, content: str) -> bool:
|
def _reply_message_sync(self, parent_message_id: str, msg_type: str, content: str, *, reply_in_thread: bool = False) -> bool:
|
||||||
"""Reply to an existing Feishu message using the Reply API (synchronous)."""
|
"""Reply to an existing Feishu message using the Reply API (synchronous).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reply_in_thread: If True, reply as a thread/topic message
|
||||||
|
in the Feishu client.
|
||||||
|
"""
|
||||||
from lark_oapi.api.im.v1 import ReplyMessageRequest, ReplyMessageRequestBody
|
from lark_oapi.api.im.v1 import ReplyMessageRequest, ReplyMessageRequestBody
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
body_builder = ReplyMessageRequestBody.builder().msg_type(msg_type).content(content)
|
||||||
|
if reply_in_thread:
|
||||||
|
body_builder = body_builder.reply_in_thread(True)
|
||||||
request = (
|
request = (
|
||||||
ReplyMessageRequest.builder()
|
ReplyMessageRequest.builder()
|
||||||
.message_id(parent_message_id)
|
.message_id(parent_message_id)
|
||||||
.request_body(
|
.request_body(body_builder.build())
|
||||||
ReplyMessageRequestBody.builder().msg_type(msg_type).content(content).build()
|
|
||||||
)
|
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
response = self._client.im.v1.message.reply(request)
|
response = self._client.im.v1.message.reply(request)
|
||||||
@ -1430,11 +1436,18 @@ class FeishuChannel(BaseChannel):
|
|||||||
first_send = True # tracks whether the reply has already been used
|
first_send = True # tracks whether the reply has already been used
|
||||||
|
|
||||||
def _do_send(m_type: str, content: str) -> None:
|
def _do_send(m_type: str, content: str) -> None:
|
||||||
"""Send via reply (first message) or create (subsequent)."""
|
"""Send via reply (first message) or create (subsequent).
|
||||||
|
|
||||||
|
When reply_to_message is enabled, the first message uses
|
||||||
|
reply_in_thread=True to create a visual topic thread.
|
||||||
|
"""
|
||||||
nonlocal first_send
|
nonlocal first_send
|
||||||
if reply_message_id and first_send:
|
if reply_message_id and first_send:
|
||||||
first_send = False
|
first_send = False
|
||||||
ok = self._reply_message_sync(reply_message_id, m_type, content)
|
ok = self._reply_message_sync(
|
||||||
|
reply_message_id, m_type, content,
|
||||||
|
reply_in_thread=self.config.reply_to_message,
|
||||||
|
)
|
||||||
if ok:
|
if ok:
|
||||||
return
|
return
|
||||||
# Fall back to regular send if reply fails
|
# Fall back to regular send if reply fails
|
||||||
|
|||||||
@ -534,3 +534,75 @@ async def test_session_key_private_chat_no_override() -> None:
|
|||||||
|
|
||||||
assert len(bus_spy) == 1
|
assert len(bus_spy) == 1
|
||||||
assert bus_spy[0].session_key_override is None
|
assert bus_spy[0].session_key_override is None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# reply_in_thread tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reply_uses_reply_in_thread_when_enabled() -> None:
|
||||||
|
"""When reply_to_message is True, reply includes reply_in_thread=True."""
|
||||||
|
channel = _make_feishu_channel(reply_to_message=True)
|
||||||
|
|
||||||
|
reply_resp = MagicMock()
|
||||||
|
reply_resp.success.return_value = True
|
||||||
|
channel._client.im.v1.message.reply.return_value = reply_resp
|
||||||
|
|
||||||
|
await channel.send(OutboundMessage(
|
||||||
|
channel="feishu",
|
||||||
|
chat_id="oc_abc",
|
||||||
|
content="hello",
|
||||||
|
metadata={"message_id": "om_001"},
|
||||||
|
))
|
||||||
|
|
||||||
|
channel._client.im.v1.message.reply.assert_called_once()
|
||||||
|
call_args = channel._client.im.v1.message.reply.call_args
|
||||||
|
request = call_args[0][0]
|
||||||
|
assert request.request_body.reply_in_thread is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reply_without_reply_in_thread_when_disabled() -> None:
|
||||||
|
"""When reply_to_message is False, reply does NOT use reply_in_thread."""
|
||||||
|
channel = _make_feishu_channel(reply_to_message=False)
|
||||||
|
|
||||||
|
create_resp = MagicMock()
|
||||||
|
create_resp.success.return_value = True
|
||||||
|
channel._client.im.v1.message.create.return_value = create_resp
|
||||||
|
|
||||||
|
await channel.send(OutboundMessage(
|
||||||
|
channel="feishu",
|
||||||
|
chat_id="oc_abc",
|
||||||
|
content="hello",
|
||||||
|
))
|
||||||
|
|
||||||
|
# No message_id in metadata → no reply attempt, direct create
|
||||||
|
channel._client.im.v1.message.create.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reply_keeps_fallback_when_reply_fails() -> None:
|
||||||
|
"""Even with reply_to_message=True, fallback to create on reply failure."""
|
||||||
|
channel = _make_feishu_channel(reply_to_message=True)
|
||||||
|
|
||||||
|
reply_resp = MagicMock()
|
||||||
|
reply_resp.success.return_value = False
|
||||||
|
reply_resp.code = 99991400
|
||||||
|
reply_resp.msg = "rate limited"
|
||||||
|
channel._client.im.v1.message.reply.return_value = reply_resp
|
||||||
|
|
||||||
|
create_resp = MagicMock()
|
||||||
|
create_resp.success.return_value = True
|
||||||
|
channel._client.im.v1.message.create.return_value = create_resp
|
||||||
|
|
||||||
|
await channel.send(OutboundMessage(
|
||||||
|
channel="feishu",
|
||||||
|
chat_id="oc_abc",
|
||||||
|
content="hello",
|
||||||
|
metadata={"message_id": "om_001"},
|
||||||
|
))
|
||||||
|
|
||||||
|
channel._client.im.v1.message.reply.assert_called()
|
||||||
|
channel._client.im.v1.message.create.assert_called()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user