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:
@@ -25,6 +25,7 @@ from nanobot.bus.events import OutboundMessage
|
|||||||
from nanobot.bus.outbound_events import ProgressEvent
|
from nanobot.bus.outbound_events import ProgressEvent
|
||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
from nanobot.channels.base import BaseChannel
|
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.paths import get_media_dir
|
||||||
from nanobot.config.schema import Base
|
from nanobot.config.schema import Base
|
||||||
from nanobot.utils.helpers import safe_filename
|
from nanobot.utils.helpers import safe_filename
|
||||||
@@ -555,6 +556,10 @@ def _qr_register_inner(
|
|||||||
|
|
||||||
|
|
||||||
_STREAM_ELEMENT_ID = "streaming_md"
|
_STREAM_ELEMENT_ID = "streaming_md"
|
||||||
|
_NEW_SESSION_DIVIDER_CONTENT = json.dumps({
|
||||||
|
"type": "divider",
|
||||||
|
"params": {"divider_text": {"text": "New session started."}},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -2010,6 +2015,14 @@ class FeishuChannel(BaseChannel):
|
|||||||
)
|
)
|
||||||
return
|
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.
|
# 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
|
# 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.
|
# 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:
|
if not content and not media_paths:
|
||||||
return
|
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.
|
# 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 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.
|
# If topic_isolation is False: all messages in group share the same session.
|
||||||
|
|||||||
@@ -550,6 +550,64 @@ async def test_on_message_no_extra_api_call_when_no_parent_id() -> None:
|
|||||||
assert len(captured) == 1
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_on_message_strips_required_leading_bot_mention_for_commands() -> None:
|
async def test_on_message_strips_required_leading_bot_mention_for_commands() -> None:
|
||||||
channel = _make_feishu_channel(group_policy="mention")
|
channel = _make_feishu_channel(group_policy="mention")
|
||||||
|
|||||||
Reference in New Issue
Block a user