feat(feishu): render new session divider

maintainer edit: remove out-of-scope reasoning panel changes and keep this PR focused on the /new session divider.
This commit is contained in:
chengyongru
2026-07-06 12:14:54 +08:00
committed by Xubin Ren
parent 937f04ac86
commit 5e51c5014f
2 changed files with 82 additions and 0 deletions
+24
View File
@@ -25,6 +25,7 @@ from nanobot.bus.events import OutboundMessage
from nanobot.bus.outbound_events import ProgressEvent
from nanobot.bus.queue import MessageBus
from nanobot.channels.base import BaseChannel
from nanobot.command.router import normalize_command_text
from nanobot.config.paths import get_media_dir
from nanobot.config.schema import Base
from nanobot.utils.helpers import safe_filename
@@ -555,6 +556,10 @@ def _qr_register_inner(
_STREAM_ELEMENT_ID = "streaming_md"
_NEW_SESSION_DIVIDER_CONTENT = json.dumps({
"type": "divider",
"params": {"divider_text": {"text": "New session started."}},
})
@dataclass
@@ -2010,6 +2015,14 @@ class FeishuChannel(BaseChannel):
)
return
if (
msg.content.strip() == "New session started."
and msg.metadata.get("chat_type") == "p2p"
and not msg.media
and not msg.buttons
):
return
# Determine whether the first message should quote the user's message.
# Only the very first send (media or text) in this call uses reply; subsequent
# chunks/media fall back to plain create to avoid redundant quote bubbles.
@@ -2260,6 +2273,17 @@ class FeishuChannel(BaseChannel):
if not content and not media_paths:
return
if chat_type == "p2p" and normalize_command_text(content).lower() == "/new":
loop = asyncio.get_running_loop()
await loop.run_in_executor(
None,
self._send_message_sync,
"open_id",
sender_id,
"system",
_NEW_SESSION_DIVIDER_CONTENT,
)
# Build session key for conversation isolation.
# If topic_isolation is True: each topic gets its own session via root_id/message_id.
# If topic_isolation is False: all messages in group share the same session.
+58
View File
@@ -550,6 +550,64 @@ async def test_on_message_no_extra_api_call_when_no_parent_id() -> None:
assert len(captured) == 1
@pytest.mark.parametrize(("chat_type", "group_policy", "should_send"), [
("p2p", "mention", True),
("group", "open", False),
])
@pytest.mark.asyncio
async def test_on_message_new_system_divider_only_in_p2p(
chat_type: str,
group_policy: str,
should_send: bool,
) -> None:
channel = _make_feishu_channel(group_policy=group_policy)
channel._processed_message_ids.clear()
channel._send_message_sync = MagicMock(return_value="om_system")
channel._handle_message = AsyncMock()
with patch.object(channel, "_add_reaction", return_value=None):
await channel._on_message(_make_feishu_event(
chat_type=chat_type,
content='{"text": "/new"}',
))
channel._handle_message.assert_awaited_once()
assert channel._handle_message.call_args.kwargs["content"] == "/new"
if not should_send:
channel._send_message_sync.assert_not_called()
return
_, receive_id, msg_type, content = channel._send_message_sync.call_args.args
assert receive_id == "ou_alice"
assert msg_type == "system"
assert json.loads(content)["type"] == "divider"
@pytest.mark.asyncio
async def test_send_new_session_text_suppressed_in_p2p_only() -> None:
p2p = _make_feishu_channel()
p2p._send_message_sync = MagicMock()
await p2p.send(OutboundMessage(
channel="feishu",
chat_id="ou_alice",
content="New session started.",
metadata={"chat_type": "p2p"},
))
group = _make_feishu_channel()
group._send_message_sync = MagicMock(return_value="om_text")
await group.send(OutboundMessage(
channel="feishu",
chat_id="oc_group",
content="New session started.",
metadata={"chat_type": "group"},
))
p2p._send_message_sync.assert_not_called()
assert group._send_message_sync.call_args.args[2] == "text"
@pytest.mark.asyncio
async def test_on_message_strips_required_leading_bot_mention_for_commands() -> None:
channel = _make_feishu_channel(group_policy="mention")