mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-26 07:57:57 +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:
|
||||
"""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()
|
||||
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
|
||||
command = parts[0].lower()
|
||||
command = parts[index].lower()
|
||||
if command in {"/connect", "connect"}:
|
||||
return parts[1]
|
||||
return parts[index + 1]
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from app.channels.base import Channel
|
||||
from app.channels.commands import extract_connect_code
|
||||
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
|
||||
|
||||
|
||||
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():
|
||||
# With no connection repo, binding is not configured and connect codes are
|
||||
# ignored so the message falls through to normal handling.
|
||||
channel = _StubChannel(name="stub", bus=MessageBus(), config={})
|
||||
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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user