fix(discord): skip auto-threading reply messages

This commit is contained in:
OwenYWT 2026-04-14 19:37:46 +08:00 committed by Teknium
parent 93fe4b357d
commit f5dc4e905d
2 changed files with 66 additions and 5 deletions

View file

@ -2285,6 +2285,26 @@ class DiscordAdapter(BasePlatformAdapter):
from gateway.platforms.base import resolve_channel_prompt from gateway.platforms.base import resolve_channel_prompt
return resolve_channel_prompt(self.config.extra, channel_id, parent_id) return resolve_channel_prompt(self.config.extra, channel_id, parent_id)
def _discord_require_mention(self) -> bool:
"""Return whether Discord channel messages require a bot mention."""
configured = self.config.extra.get("require_mention")
if configured is not None:
if isinstance(configured, str):
return configured.lower() not in ("false", "0", "no", "off")
return bool(configured)
return os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off")
def _discord_free_response_channels(self) -> set:
"""Return Discord channel IDs where no bot mention is required."""
raw = self.config.extra.get("free_response_channels")
if raw is None:
raw = os.getenv("DISCORD_FREE_RESPONSE_CHANNELS", "")
if isinstance(raw, list):
return {str(part).strip() for part in raw if str(part).strip()}
if isinstance(raw, str) and raw.strip():
return {part.strip() for part in raw.split(",") if part.strip()}
return set()
def _thread_parent_channel(self, channel: Any) -> Any: def _thread_parent_channel(self, channel: Any) -> Any:
"""Return the parent text channel when invoked from a thread.""" """Return the parent text channel when invoked from a thread."""
return getattr(channel, "parent", None) or channel return getattr(channel, "parent", None) or channel
@ -2745,12 +2765,11 @@ class DiscordAdapter(BasePlatformAdapter):
logger.debug("[%s] Ignoring message in ignored channel: %s", self.name, channel_ids) logger.debug("[%s] Ignoring message in ignored channel: %s", self.name, channel_ids)
return return
free_channels_raw = os.getenv("DISCORD_FREE_RESPONSE_CHANNELS", "") free_channels = self._discord_free_response_channels()
free_channels = {ch.strip() for ch in free_channels_raw.split(",") if ch.strip()}
if parent_channel_id: if parent_channel_id:
channel_ids.add(parent_channel_id) channel_ids.add(parent_channel_id)
require_mention = os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no") require_mention = self._discord_require_mention()
# Voice-linked text channels act as free-response while voice is active. # Voice-linked text channels act as free-response while voice is active.
# Only the exact bound channel gets the exemption, not sibling threads. # Only the exact bound channel gets the exemption, not sibling threads.
voice_linked_ids = {str(ch_id) for ch_id in self._voice_text_channels.values()} voice_linked_ids = {str(ch_id) for ch_id in self._voice_text_channels.values()}
@ -2780,7 +2799,8 @@ class DiscordAdapter(BasePlatformAdapter):
no_thread_channels = {ch.strip() for ch in no_thread_channels_raw.split(",") if ch.strip()} no_thread_channels = {ch.strip() for ch in no_thread_channels_raw.split(",") if ch.strip()}
skip_thread = bool(channel_ids & no_thread_channels) or is_free_channel skip_thread = bool(channel_ids & no_thread_channels) or is_free_channel
auto_thread = os.getenv("DISCORD_AUTO_THREAD", "true").lower() in ("true", "1", "yes") auto_thread = os.getenv("DISCORD_AUTO_THREAD", "true").lower() in ("true", "1", "yes")
if auto_thread and not skip_thread and not is_voice_linked_channel: is_reply_message = getattr(message, "type", None) == discord.MessageType.reply
if auto_thread and not skip_thread and not is_voice_linked_channel and not is_reply_message:
thread = await self._auto_create_thread(message) thread = await self._auto_create_thread(message)
if thread: if thread:
is_thread = True is_thread = True

View file

@ -96,7 +96,7 @@ def adapter(monkeypatch):
return adapter return adapter
def make_message(*, channel, content: str, mentions=None): def make_message(*, channel, content: str, mentions=None, msg_type=None):
author = SimpleNamespace(id=42, display_name="Jezza", name="Jezza") author = SimpleNamespace(id=42, display_name="Jezza", name="Jezza")
return SimpleNamespace( return SimpleNamespace(
id=123, id=123,
@ -107,6 +107,7 @@ def make_message(*, channel, content: str, mentions=None):
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
channel=channel, channel=channel,
author=author, author=author,
type=msg_type if msg_type is not None else discord_platform.discord.MessageType.default,
) )
@ -204,6 +205,21 @@ async def test_discord_free_response_channel_overrides_mention_requirement(adapt
assert event.text == "allowed without mention" assert event.text == "allowed without mention"
@pytest.mark.asyncio
async def test_discord_free_response_channel_can_come_from_config_extra(adapter, monkeypatch):
monkeypatch.delenv("DISCORD_REQUIRE_MENTION", raising=False)
monkeypatch.delenv("DISCORD_FREE_RESPONSE_CHANNELS", raising=False)
adapter.config.extra["free_response_channels"] = ["789", "999"]
message = make_message(channel=FakeTextChannel(channel_id=789), content="allowed from config")
await adapter._handle_message(message)
adapter.handle_message.assert_awaited_once()
event = adapter.handle_message.await_args.args[0]
assert event.text == "allowed from config"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_discord_forum_parent_in_free_response_list_allows_forum_thread(adapter, monkeypatch): async def test_discord_forum_parent_in_free_response_list_allows_forum_thread(adapter, monkeypatch):
monkeypatch.setenv("DISCORD_REQUIRE_MENTION", "true") monkeypatch.setenv("DISCORD_REQUIRE_MENTION", "true")
@ -276,6 +292,31 @@ async def test_discord_auto_thread_enabled_by_default(adapter, monkeypatch):
assert event.source.thread_id == "999" assert event.source.thread_id == "999"
@pytest.mark.asyncio
async def test_discord_reply_message_skips_auto_thread(adapter, monkeypatch):
"""Quote-replies should stay in-channel instead of trying to create a thread."""
monkeypatch.delenv("DISCORD_AUTO_THREAD", raising=False)
monkeypatch.setenv("DISCORD_REQUIRE_MENTION", "true")
monkeypatch.setenv("DISCORD_FREE_RESPONSE_CHANNELS", "123")
adapter._auto_create_thread = AsyncMock()
message = make_message(
channel=FakeTextChannel(channel_id=123),
content="reply without mention",
msg_type=discord_platform.discord.MessageType.reply,
)
await adapter._handle_message(message)
adapter._auto_create_thread.assert_not_awaited()
adapter.handle_message.assert_awaited_once()
event = adapter.handle_message.await_args.args[0]
assert event.text == "reply without mention"
assert event.source.chat_id == "123"
assert event.source.chat_type == "group"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_discord_auto_thread_can_be_disabled(adapter, monkeypatch): async def test_discord_auto_thread_can_be_disabled(adapter, monkeypatch):
"""Setting auto_thread to false skips thread creation.""" """Setting auto_thread to false skips thread creation."""