Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gateway/platforms/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2159,7 +2159,7 @@ def extract_media(content: str) -> Tuple[List[Tuple[str, bool]], str]:
# Extract MEDIA:<path> tags, allowing optional whitespace after the colon
# and quoted/backticked paths for LLM-formatted outputs.
media_pattern = re.compile(
r'''[`"']?MEDIA:\s*(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|txt|csv|apk|ipa)(?=[\s`"',;:)\]}]|$))[`"']?'''
r'''[`"']?MEDIA:\s*(?P<path>`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~/|/)\S+(?:[^\S\n]+\S+)*?\.(?:png|jpe?g|gif|webp|mp4|mov|avi|mkv|webm|ogg|opus|mp3|wav|m4a|flac|epub|pdf|zip|rar|7z|docx?|xlsx?|pptx?|txt|csv|apk|ipa|md|json|yaml|yml|toml|log)(?=[\s`"',;:)\]}]|$))[`"']?'''
)
for match in media_pattern.finditer(content):
path = match.group("path").strip()
Expand Down
34 changes: 29 additions & 5 deletions gateway/platforms/weixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2145,11 +2145,35 @@ async def send_weixin_direct(
adapter._token_store = token_store

last_result: Optional[SendResult] = None
cleaned = adapter.format_message(message)
if cleaned:
last_result = await adapter.send(chat_id, cleaned)
if not last_result.success:
return {"error": f"Weixin send failed: {last_result.error}"}

async def _try_send_with_refresh() -> Optional[SendResult]:
nonlocal last_result
cleaned = adapter.format_message(message)
if cleaned:
last_result = await adapter.send(chat_id, cleaned)
return last_result

# Retry once on session/rate-limit errors, then give an
# actionable error so cron/send_message can surface it.
for _retry in range(2):
last_result = await _try_send_with_refresh()
if last_result and last_result.success:
break
err = last_result.error if last_result else ""
is_rate = "rate limited" in err or "ret=-2" in err
is_session = "session" in err.lower()
if not is_rate and not is_session:
break
# Clear stale context_token and retry once without it
if context_token:
token_store._cache.pop(
token_store._key(account_id, chat_id), None
)
context_token = None
await asyncio.sleep(3.0)

if not last_result or not last_result.success:
return {"error": f"Weixin send failed: {last_result.error if last_result else 'unknown'}"}

for media_path, _is_voice in media_files or []:
ext = Path(media_path).suffix.lower()
Expand Down
13 changes: 13 additions & 0 deletions tests/gateway/test_platform_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,19 @@ def test_as_document_directive_alone_does_not_attach_voice_flag(self):
assert media == [("/tmp/x.jpg", False)] # voice flag stays False
assert "[[as_document]]" not in cleaned

@pytest.mark.parametrize(
"ext",
["md", "json", "yaml", "yml", "toml", "log"],
)
def test_media_tag_accepts_text_config_extensions(self, ext):
"""MEDIA: tags must extract text/config artifact paths so callers can
deliver them as file attachments instead of leaving the raw tag in
the platform message body (see issue #32601, bug 1)."""
content = f"MEDIA:/tmp/notes.{ext}"
media, cleaned = BasePlatformAdapter.extract_media(content)
assert media == [(f"/tmp/notes.{ext}", False)]
assert "MEDIA:" not in cleaned

def test_both_directives_can_coexist(self):
"""A response could (rarely) contain both [[audio_as_voice]] for an
ogg file AND [[as_document]] for an attached image. The voice flag
Expand Down