Merge branch 'main' into bb/gui
This commit is contained in:
@@ -829,6 +829,9 @@ SUPPORTED_DOCUMENT_TYPES = {
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
".ts": "text/plain",
|
||||
".py": "text/plain",
|
||||
".sh": "text/plain",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3564,6 +3564,43 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||
return bool(configured)
|
||||
return os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in {"false", "0", "no", "off"}
|
||||
|
||||
def _discord_allow_any_attachment(self) -> bool:
|
||||
"""Return whether Discord attachments bypass the SUPPORTED_DOCUMENT_TYPES allowlist.
|
||||
|
||||
When True, any uploaded file is cached to disk and surfaced to the
|
||||
agent as a local path so it can be inspected via terminal / read_file
|
||||
/ ffprobe / etc. Default False preserves the historical behaviour of
|
||||
dropping unsupported types with a warning log.
|
||||
"""
|
||||
configured = self.config.extra.get("allow_any_attachment")
|
||||
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_ALLOW_ANY_ATTACHMENT", "false").lower() in {"true", "1", "yes", "on"}
|
||||
|
||||
def _discord_max_attachment_bytes(self) -> int:
|
||||
"""Return the per-attachment byte cap. 0 means unlimited.
|
||||
|
||||
The whole attachment is held in memory while being written to the
|
||||
cache, so unlimited carries a real memory cost. Default 32 MiB
|
||||
matches the historical hardcoded value.
|
||||
"""
|
||||
configured = self.config.extra.get("max_attachment_bytes")
|
||||
if configured is None:
|
||||
configured = os.getenv("DISCORD_MAX_ATTACHMENT_BYTES")
|
||||
if configured is None or configured == "":
|
||||
return 32 * 1024 * 1024
|
||||
try:
|
||||
value = int(configured)
|
||||
except (TypeError, ValueError):
|
||||
logger.warning(
|
||||
"[Discord] Invalid max_attachment_bytes value %r, falling back to 32 MiB",
|
||||
configured,
|
||||
)
|
||||
return 32 * 1024 * 1024
|
||||
return max(0, value)
|
||||
|
||||
def _discord_free_response_channels(self) -> set:
|
||||
"""Return Discord channel IDs where no bot mention is required.
|
||||
|
||||
@@ -4495,6 +4532,7 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||
if normalized_content.startswith("/"):
|
||||
msg_type = MessageType.COMMAND
|
||||
elif all_attachments:
|
||||
_allow_any = self._discord_allow_any_attachment()
|
||||
# Check attachment types
|
||||
for att in all_attachments:
|
||||
if att.content_type:
|
||||
@@ -4509,9 +4547,15 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||
if att.filename:
|
||||
_, doc_ext = os.path.splitext(att.filename)
|
||||
doc_ext = doc_ext.lower()
|
||||
if doc_ext in SUPPORTED_DOCUMENT_TYPES:
|
||||
if doc_ext in SUPPORTED_DOCUMENT_TYPES or _allow_any:
|
||||
msg_type = MessageType.DOCUMENT
|
||||
break
|
||||
elif _allow_any:
|
||||
# No content_type at all (rare — discord usually fills it
|
||||
# in). Treat as a document so downstream pipelines surface
|
||||
# the path to the agent.
|
||||
msg_type = MessageType.DOCUMENT
|
||||
break
|
||||
|
||||
# When auto-threading kicked in, route responses to the new thread
|
||||
effective_channel = auto_threaded_channel or message.channel
|
||||
@@ -4594,31 +4638,48 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||
if not ext and content_type:
|
||||
mime_to_ext = {v: k for k, v in SUPPORTED_DOCUMENT_TYPES.items()}
|
||||
ext = mime_to_ext.get(content_type, "")
|
||||
if ext not in SUPPORTED_DOCUMENT_TYPES:
|
||||
allow_any_attachment = self._discord_allow_any_attachment()
|
||||
in_allowlist = ext in SUPPORTED_DOCUMENT_TYPES
|
||||
if not in_allowlist and not allow_any_attachment:
|
||||
logger.warning(
|
||||
"[Discord] Unsupported document type '%s' (%s), skipping",
|
||||
ext or "unknown", content_type,
|
||||
)
|
||||
else:
|
||||
MAX_DOC_BYTES = 32 * 1024 * 1024
|
||||
if att.size and att.size > MAX_DOC_BYTES:
|
||||
max_doc_bytes = self._discord_max_attachment_bytes()
|
||||
if max_doc_bytes and att.size and att.size > max_doc_bytes:
|
||||
logger.warning(
|
||||
"[Discord] Document too large (%s bytes), skipping: %s",
|
||||
att.size, att.filename,
|
||||
"[Discord] Document too large (%s bytes > cap %s), skipping: %s",
|
||||
att.size, max_doc_bytes, att.filename,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
raw_bytes = await self._cache_discord_document(att, ext)
|
||||
cached_path = cache_document_from_bytes(
|
||||
raw_bytes, att.filename or f"document{ext}"
|
||||
raw_bytes, att.filename or f"document{ext or '.bin'}"
|
||||
)
|
||||
doc_mime = SUPPORTED_DOCUMENT_TYPES[ext]
|
||||
if in_allowlist:
|
||||
doc_mime = SUPPORTED_DOCUMENT_TYPES[ext]
|
||||
else:
|
||||
# allow_any_attachment path: untyped file. Use the
|
||||
# source content_type if discord gave us one,
|
||||
# otherwise fall back to octet-stream so the agent
|
||||
# knows it's binary and reaches for terminal tools.
|
||||
doc_mime = (
|
||||
content_type
|
||||
if content_type and content_type != "unknown"
|
||||
else "application/octet-stream"
|
||||
)
|
||||
media_urls.append(cached_path)
|
||||
media_types.append(doc_mime)
|
||||
logger.info("[Discord] Cached user document: %s", cached_path)
|
||||
logger.info(
|
||||
"[Discord] Cached user %s: %s",
|
||||
"document" if in_allowlist else "attachment",
|
||||
cached_path,
|
||||
)
|
||||
# Inject text content for plain-text documents (capped at 100 KB)
|
||||
MAX_TEXT_INJECT_BYTES = 100 * 1024
|
||||
if ext in {".md", ".txt", ".log"} and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
|
||||
if in_allowlist and ext in {".md", ".txt", ".log"} and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES:
|
||||
try:
|
||||
text_content = raw_bytes.decode("utf-8")
|
||||
display_name = att.filename or f"document{ext}"
|
||||
@@ -4630,6 +4691,13 @@ class DiscordAdapter(BasePlatformAdapter):
|
||||
pending_text_injection = injection
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
# NOTE: for the allow_any_attachment path we deliberately
|
||||
# do NOT inject a path string here. ``gateway/run.py``
|
||||
# already detects DOCUMENT-typed events with
|
||||
# ``application/octet-stream`` MIME and emits a context
|
||||
# note with the sandbox-translated cache path via
|
||||
# ``to_agent_visible_cache_path()`` (important for
|
||||
# Docker/Modal terminal backends).
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"[Discord] Failed to cache document %s: %s",
|
||||
|
||||
@@ -54,6 +54,13 @@ from gateway.platforms.base import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_BUILTIN_DELIVER_PLATFORMS = {
|
||||
"telegram", "discord", "slack", "signal", "sms", "whatsapp",
|
||||
"matrix", "mattermost", "homeassistant", "email", "dingtalk",
|
||||
"feishu", "wecom", "wecom_callback", "weixin", "bluebubbles",
|
||||
"qqbot", "yuanbao",
|
||||
}
|
||||
|
||||
DEFAULT_HOST = "0.0.0.0"
|
||||
DEFAULT_PORT = 8644
|
||||
_INSECURE_NO_AUTH = "INSECURE_NO_AUTH"
|
||||
@@ -238,12 +245,6 @@ class WebhookAdapter(BasePlatformAdapter):
|
||||
|
||||
# Cross-platform delivery — any platform with a gateway adapter.
|
||||
# Check both built-in names and plugin-registered platforms.
|
||||
_BUILTIN_DELIVER_PLATFORMS = {
|
||||
"telegram", "discord", "slack", "signal", "sms", "whatsapp",
|
||||
"matrix", "mattermost", "homeassistant", "email", "dingtalk",
|
||||
"feishu", "wecom", "wecom_callback", "weixin", "bluebubbles",
|
||||
"qqbot", "yuanbao",
|
||||
}
|
||||
_is_known_platform = deliver_type in _BUILTIN_DELIVER_PLATFORMS
|
||||
if not _is_known_platform:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user