mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-12 22:20:33 +00:00
fix(wecom): treat "@bot /new" in group chats as a command (#8118)
WeCom leaves the bot mention in group message text, so `_publish_ws_inbound` handed "@RobotA /new" to `is_known_channel_command`, which requires a bare leading slash. The turn was classified as chat and the command never ran. Strip leading mentions for the command lookup only, mirroring Feishu (#4229) and DingTalk (#4724): ordinary chat keeps its mentions, and the stripped form is what reaches ChannelManager so it parses `/new`. The comment spells out that this drops any leading mention, not only the bot's -- WeCom text mentions carry display names, so `aibotid` cannot be matched against them. Recorded in CHANGELOG.md / CHANGELOG_zh.md under Unreleased -> Fixed. Fixes #8099 Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
2baadff446
commit
041d9e375e
@ -55,6 +55,13 @@ This section accumulates work toward the **2.3.0** milestone
|
||||
|
||||
### Fixed
|
||||
|
||||
- **channels:** A WeCom group command that follows the bot mention now runs as a
|
||||
command. WeCom keeps the mention inside the message text (`@RobotA /new`), and
|
||||
the classifier required a bare leading `/`, so `/new` and `/status` sent in a
|
||||
group became chat turns. Leading mentions are skipped for the command lookup
|
||||
only; ordinary chat keeps them and `/connect` is unchanged. Mirrors the Feishu
|
||||
and DingTalk handling. ([#8118])
|
||||
|
||||
- **frontend:** **Share** in a sidebar chat's menu now copies a link to the
|
||||
deployment you are using. On `localhost` and `127.0.0.1`, the default
|
||||
`make dev` and Docker addresses, it copied a link to the hosted demo
|
||||
@ -16309,5 +16316,6 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#8069]: https://github.com/bytedance/deer-flow/issues/8069
|
||||
[#8071]: https://github.com/bytedance/deer-flow/issues/8071
|
||||
[#8072]: https://github.com/bytedance/deer-flow/pull/8072
|
||||
[#8118]: https://github.com/bytedance/deer-flow/pull/8118
|
||||
[#8126]: https://github.com/bytedance/deer-flow/pull/8126
|
||||
[#8131]: https://github.com/bytedance/deer-flow/pull/8131
|
||||
|
||||
@ -26,6 +26,10 @@
|
||||
|
||||
### 修复
|
||||
|
||||
- **通道:** WeCom 群聊中 `@机器人 /new` 这类带提及的命令现在按命令执行。WeCom 会把提及保留在
|
||||
正文里,而命令识别要求裸的前导 `/`,于是群里的 `/new`、`/status` 被当成聊天轮次。现在仅在命令
|
||||
识别时跳过前导提及;普通聊天保留提及,`/connect` 行为不变。与飞书、钉钉的处理一致。([#8118])
|
||||
|
||||
- **前端:** 侧边栏对话菜单中的 **分享** 现在复制指向当前部署的链接。此前在 `localhost` 和
|
||||
`127.0.0.1`(`make dev` 与 Docker 的默认地址)上,复制的是托管演示站
|
||||
(`deer-flow-v2.vercel.app`)的链接:该链接无法打开对话,打开时还会把线程 ID 发送到外部主机。
|
||||
@ -12866,4 +12870,5 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#8043]: https://github.com/bytedance/deer-flow/pull/8043
|
||||
[#8046]: https://github.com/bytedance/deer-flow/pull/8046
|
||||
[#8064]: https://github.com/bytedance/deer-flow/pull/8064
|
||||
[#8118]: https://github.com/bytedance/deer-flow/pull/8118
|
||||
[#8126]: https://github.com/bytedance/deer-flow/pull/8126
|
||||
|
||||
@ -9,7 +9,7 @@ from collections.abc import Awaitable, Callable, Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
from app.channels.base import Channel, ChannelUnavailable, attachment_delivery_text
|
||||
from app.channels.commands import is_known_channel_command
|
||||
from app.channels.commands import is_known_channel_command, strip_leading_mentions
|
||||
from app.channels.connection_identity import attach_connection_identity
|
||||
from app.channels.message_bus import (
|
||||
InboundMessage,
|
||||
@ -583,7 +583,22 @@ class WeComChannel(Channel):
|
||||
if handled:
|
||||
return
|
||||
|
||||
inbound_type = InboundMessageType.COMMAND if is_known_channel_command(text) else InboundMessageType.CHAT
|
||||
# WeCom group chats deliver "@bot /new" with the mention left in the text
|
||||
# (Slack/Discord strip their own bot mention upstream). Skip a leading
|
||||
# mention only for the command path so ordinary chat keeps any @mentions
|
||||
# intact for the agent; the stripped form also flows into the inbound so
|
||||
# ChannelManager._handle_command parses the bare command. Mirrors
|
||||
# DingTalkChannel / FeishuChannel.
|
||||
#
|
||||
# This drops *any* leading mention, not only the bot's: WeCom text
|
||||
# mentions carry display names, so the frame's `aibotid` cannot be
|
||||
# matched against them and "@alice /status" is indistinguishable from
|
||||
# "@bot /status". Feishu and DingTalk accept the same tradeoff;
|
||||
# test_member_mention_before_a_command_also_classifies pins it.
|
||||
command_text = strip_leading_mentions(text)
|
||||
inbound_type = InboundMessageType.COMMAND if is_known_channel_command(command_text) else InboundMessageType.CHAT
|
||||
if inbound_type is InboundMessageType.COMMAND:
|
||||
text = command_text
|
||||
inbound_metadata: dict[str, Any] = {
|
||||
"aibotid": body.get("aibotid"),
|
||||
"chattype": body.get("chattype"),
|
||||
|
||||
@ -10,10 +10,15 @@ with a null value, so chaining ``.get``/``.strip`` on it raised
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import types
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from app.channels.message_bus import MessageBus
|
||||
import pytest
|
||||
|
||||
from app.channels.message_bus import InboundMessageType, MessageBus
|
||||
from app.channels.wecom import WeComChannel
|
||||
|
||||
|
||||
@ -70,3 +75,100 @@ class TestOnWsTextQuoteParsing:
|
||||
}
|
||||
_run(ch._on_ws_text(frame))
|
||||
ch._publish_ws_inbound.assert_called_once_with(frame, "T\nQuote message: Q")
|
||||
|
||||
|
||||
class TestGroupMentionBeforeCommand:
|
||||
"""WeCom keeps the bot mention in group text, so "@bot /new" has to be
|
||||
classified as a command and reach the dispatcher without the mention.
|
||||
|
||||
Feishu (#4229) and DingTalk (#4724) already strip a leading mention on the
|
||||
command path; ordinary chat must keep its mentions so the agent still sees
|
||||
who was addressed.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _stub_aibot(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Make ``_publish_ws_inbound`` run without the optional WeCom SDK."""
|
||||
sdk = types.ModuleType("aibot")
|
||||
sdk.generate_req_id = lambda prefix="": f"{prefix}-test"
|
||||
monkeypatch.setitem(sys.modules, "aibot", sdk)
|
||||
|
||||
@staticmethod
|
||||
def _live_channel() -> tuple[WeComChannel, MessageBus]:
|
||||
bus = MessageBus()
|
||||
channel = WeComChannel(bus=bus, config={})
|
||||
channel._ws_client = SimpleNamespace(
|
||||
reply=AsyncMock(),
|
||||
reply_stream=AsyncMock(),
|
||||
send_message=AsyncMock(),
|
||||
)
|
||||
return channel, bus
|
||||
|
||||
@staticmethod
|
||||
def _group_frame(text: str) -> dict[str, Any]:
|
||||
return {
|
||||
"body": {
|
||||
"msgid": "msg-mention",
|
||||
"aibotid": "bot-1",
|
||||
"chatid": "wr-team",
|
||||
"chattype": "group",
|
||||
"from": {"userid": "alice"},
|
||||
"msgtype": "text",
|
||||
"text": {"content": text},
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "expected"),
|
||||
[
|
||||
("@RobotA /new", "/new"),
|
||||
("@RobotA @_user_2 /status", "/status"),
|
||||
],
|
||||
)
|
||||
def test_leading_mention_before_command_is_stripped(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
text: str,
|
||||
expected: str,
|
||||
) -> None:
|
||||
self._stub_aibot(monkeypatch)
|
||||
channel, bus = self._live_channel()
|
||||
|
||||
_run(channel._on_ws_text(self._group_frame(text)))
|
||||
|
||||
inbound = bus.get_inbound_nowait()
|
||||
assert inbound.msg_type is InboundMessageType.COMMAND
|
||||
assert inbound.text == expected
|
||||
|
||||
def test_plain_chat_keeps_its_mention(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
self._stub_aibot(monkeypatch)
|
||||
channel, bus = self._live_channel()
|
||||
|
||||
_run(channel._on_ws_text(self._group_frame("@RobotA hello there")))
|
||||
|
||||
inbound = bus.get_inbound_nowait()
|
||||
assert inbound.msg_type is InboundMessageType.CHAT
|
||||
assert inbound.text == "@RobotA hello there"
|
||||
|
||||
def test_member_mention_before_a_command_also_classifies(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Accepted tradeoff: a member's mention looks the same as the bot's.
|
||||
|
||||
WeCom text mentions are display names, so the frame's ``aibotid`` cannot
|
||||
be matched against them; ``@alice /status`` is therefore classified as
|
||||
the ``/status`` command, exactly as Feishu and DingTalk behave. Pinned
|
||||
here so a future change to mention matching shows up in review.
|
||||
"""
|
||||
self._stub_aibot(monkeypatch)
|
||||
channel, bus = self._live_channel()
|
||||
|
||||
_run(channel._on_ws_text(self._group_frame("@alice /status")))
|
||||
|
||||
inbound = bus.get_inbound_nowait()
|
||||
assert inbound.msg_type is InboundMessageType.COMMAND
|
||||
assert inbound.text == "/status"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user