mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 02:46:02 +00:00
fix(channels): accept leading @mentions before /connect bind codes (#4222)
* fix(channels): accept leading @mentions before /connect bind codes Group chats often deliver "@bot /connect <code>" (Feishu/DingTalk leave the mention in the text). extract_connect_code required the message to start with /connect, so those binds silently failed while Slack/Discord already strip mentions before parsing. Skip leading mention tokens in the shared helper. * test(channels): pin mention variants and case-insensitive /connect parsing
This commit is contained in:
parent
94a34f382d
commit
b3a0dac8ad
@ -21,14 +21,42 @@ KNOWN_CHANNEL_COMMANDS: frozenset[str] = frozenset(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_leading_mention_token(token: str) -> bool:
|
||||||
|
"""Return whether *token* looks like a platform bot/user mention.
|
||||||
|
|
||||||
|
Group chats often require ``@bot`` before the message is delivered. Slack
|
||||||
|
and Discord strip those tokens before connect parsing; Feishu / DingTalk
|
||||||
|
leave them in the text (``@_user_1``, ``@bot``, ``<@id>``). Treat them as
|
||||||
|
transport noise only when they lead the message so
|
||||||
|
``@bot /connect <code>`` still binds.
|
||||||
|
"""
|
||||||
|
if not token:
|
||||||
|
return False
|
||||||
|
# Slack / Discord style: <@U123> or <@!U123> or <@U123|name>
|
||||||
|
if token.startswith("<@") and token.endswith(">"):
|
||||||
|
return True
|
||||||
|
# Feishu / DingTalk / generic: @_user_1, @bot, @nickname
|
||||||
|
if token.startswith("@") and len(token) > 1:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def extract_connect_code(text: str) -> str | None:
|
def extract_connect_code(text: str) -> str | None:
|
||||||
"""Extract the one-time channel binding code from a connect command."""
|
"""Extract the one-time channel binding code from a connect command.
|
||||||
|
|
||||||
|
Accepts a leading platform mention so group ``@bot /connect <code>``
|
||||||
|
messages bind the same way as bare ``/connect <code>`` (Slack/Discord
|
||||||
|
already strip mentions before calling this helper).
|
||||||
|
"""
|
||||||
parts = text.strip().split()
|
parts = text.strip().split()
|
||||||
if len(parts) < 2:
|
index = 0
|
||||||
|
while index < len(parts) and _is_leading_mention_token(parts[index]):
|
||||||
|
index += 1
|
||||||
|
if index + 1 >= len(parts):
|
||||||
return None
|
return None
|
||||||
command = parts[0].lower()
|
command = parts[index].lower()
|
||||||
if command in {"/connect", "connect"}:
|
if command in {"/connect", "connect"}:
|
||||||
return parts[1]
|
return parts[index + 1]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from datetime import UTC, datetime, timedelta
|
|||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
from app.channels.base import Channel
|
from app.channels.base import Channel
|
||||||
|
from app.channels.commands import extract_connect_code
|
||||||
from app.channels.message_bus import InboundMessage, MessageBus, OutboundMessage
|
from app.channels.message_bus import InboundMessage, MessageBus, OutboundMessage
|
||||||
|
|
||||||
|
|
||||||
@ -29,11 +30,42 @@ def test_pending_connect_code_extracts_code_when_connections_configured():
|
|||||||
assert channel._pending_connect_code("hello world") is None
|
assert channel._pending_connect_code("hello world") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_pending_connect_code_accepts_leading_platform_mentions():
|
||||||
|
"""Group chats prefix @bot; Feishu/DingTalk leave that noise in the text.
|
||||||
|
|
||||||
|
Slack/Discord strip mentions before this helper; the shared parser must
|
||||||
|
still accept the unstripped form so @bot /connect <code> binds.
|
||||||
|
"""
|
||||||
|
assert extract_connect_code("@_user_1 /connect abc123") == "abc123"
|
||||||
|
assert extract_connect_code("@bot /connect code-xyz") == "code-xyz"
|
||||||
|
assert extract_connect_code("@bot @_user_2 /connect multi") == "multi"
|
||||||
|
# All three Slack/Discord mention forms the _is_leading_mention_token
|
||||||
|
# docstring enumerates: plain, ping (<@!id>), and name (<@id|name>).
|
||||||
|
assert extract_connect_code("<@U123ABC> /connect slackish") == "slackish"
|
||||||
|
assert extract_connect_code("<@!U123ABC> /connect pinged") == "pinged"
|
||||||
|
assert extract_connect_code("<@U123ABC|alice> /connect named") == "named"
|
||||||
|
# Command match is case-insensitive after the mention prefix.
|
||||||
|
assert extract_connect_code("@bot /Connect cased") == "cased"
|
||||||
|
# Mentions without a connect command stay non-binding.
|
||||||
|
assert extract_connect_code("@_user_1 hello") is None
|
||||||
|
# Mention-only / empty input yields no code (boundary guard).
|
||||||
|
assert extract_connect_code("@bot") is None
|
||||||
|
assert extract_connect_code("") is None
|
||||||
|
# Connect not at the command position after mentions does not bind.
|
||||||
|
assert extract_connect_code("please @bot /connect leaked") is None
|
||||||
|
# Mid-sentence /connect must not bind (unchanged).
|
||||||
|
assert extract_connect_code("hi /connect abc123") is None
|
||||||
|
|
||||||
|
channel = _StubChannel(name="stub", bus=MessageBus(), config={"connection_repo": object()})
|
||||||
|
assert channel._pending_connect_code("@_user_1 /connect via-base") == "via-base"
|
||||||
|
|
||||||
|
|
||||||
def test_pending_connect_code_is_none_when_connections_disabled():
|
def test_pending_connect_code_is_none_when_connections_disabled():
|
||||||
# With no connection repo, binding is not configured and connect codes are
|
# With no connection repo, binding is not configured and connect codes are
|
||||||
# ignored so the message falls through to normal handling.
|
# ignored so the message falls through to normal handling.
|
||||||
channel = _StubChannel(name="stub", bus=MessageBus(), config={})
|
channel = _StubChannel(name="stub", bus=MessageBus(), config={})
|
||||||
assert channel._pending_connect_code("/connect abc123") is None
|
assert channel._pending_connect_code("/connect abc123") is None
|
||||||
|
assert channel._pending_connect_code("@bot /connect abc123") is None
|
||||||
|
|
||||||
|
|
||||||
async def _make_repo(tmp_path, name: str):
|
async def _make_repo(tmp_path, name: str):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user