fix(gateway/weixin): correct aes_key encoding and upload_full_url CDN path - #7531
fix(gateway/weixin): correct aes_key encoding and upload_full_url CDN path#7531longsizhuo wants to merge 4 commits into
Conversation
… path
Three bugs in the outbound media pipeline that caused images sent by the bot
to appear as a grey placeholder in the WeChat client:
1. aes_key encoding in image_item (root cause of grey box)
The WeChat client expects aes_key as base64(hex_string_bytes), not
base64(raw_bytes). Its decode chain is:
base64_decode → 32 ASCII hex chars → fromhex() → 16-byte AES key
Sending base64(raw_bytes) yielded 16 decoded bytes that the client
couldn't interpret as hex, resulting in the wrong key and failed
AES-128-ECB decryption.
Before: base64.b64encode(aes_key)
After: base64.b64encode(aes_key.hex().encode("ascii"))
2. upload_full_url CDN path used PUT instead of POST
Newer iLink API instances return upload_full_url instead of upload_param.
The CDN endpoint behind upload_full_url returns 404 on PUT; POST succeeds.
Fixed by trying POST first and falling back to PUT on 404.
3. Wrong encrypted_query_param source for upload_full_url path
The encrypted_query_param embedded in upload_full_url is an upload-auth
token, not the download key. The correct download key is the
x-encrypted-param header returned by the CDN after a successful upload.
Also fix send_image_file() parameter name: the method was called with
image_path= but the parameter was declared as path=, causing a TypeError.
Also fix run_agent.py to read max_tokens from custom_providers per-model
config, so models configured under custom_providers can set their own
output token limit.
Fixes NousResearch#7529
There was a problem hiding this comment.
Pull request overview
Fixes Weixin/iLink outbound media uploads so bot-sent images decrypt/display correctly in WeChat, and extends agent configuration to support per-model max_tokens under custom_providers.
Changes:
- Correct Weixin outbound media message construction (AES key encoding) and improve
upload_full_urlupload behavior (POST-first with PUT fallback; use CDN header for download key). - Fix Weixin adapter
send_image_file()to acceptimage_pathand legacypath. - Read
max_tokensfromcustom_providersper-model config inrun_agent.py.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
gateway/platforms/weixin.py |
Fixes outbound media upload path and AES key encoding for WeChat client compatibility; updates image send API surface. |
run_agent.py |
Adds support for per-model max_tokens under custom_providers during agent initialization. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…edback
- run_agent: decouple per-model max_tokens lookup from context_length
guard so an explicit context_length no longer suppresses max_tokens
- gateway/weixin: return a clear SendResult error when send_image_file
is called without image_path/path instead of failing later with
IsADirectoryError on Path("").read_bytes()
- gateway/weixin: drop unused aes_key_hex kwarg from item_builder call
(none of the media builder lambdas read it)
|
good |
The per-model max_tokens lookup in run_agent.py is unrelated to the
Weixin image fix. Restoring it to main's state so this PR stays focused
per CONTRIBUTING.md ("One logical change per PR").
|
Great catch on the aes_key encoding — the round-trip test in the PR description was exactly the smoking gun I was chasing in my own debug pass last night. One thing I ran into while testing this locally: the same signature mismatch exists in Repro: ask the bot to send a media_result = await self.send_document(
chat_id=event.source.chat_id,
file_path=media_path,
metadata=_thread_metadata,
)but The minimal fix mirrors what you already did for async def send_document(
self,
chat_id: str,
path: Optional[str] = None,
caption: str = "",
file_path: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> SendResult:
if not self._session or not self._token:
return SendResult(success=False, error="Not connected")
target = path if path is not None else file_path
if target is None:
return SendResult(
success=False,
error="send_document requires 'file_path' (or legacy 'path')",
)
try:
message_id = await self._send_file(chat_id, target, caption)
return SendResult(success=True, message_id=message_id)
except Exception as exc:
logger.error(
"[%s] send_document failed to=%s: %s",
self.name, _safe_id(chat_id), exc,
)
return SendResult(success=False, error=str(exc))Happy to either fold this into your PR or open a small follow-up after this one lands — whichever is easier for the maintainers. |
The gateway's auto-forward code at gateway/platforms/base.py:1676 calls
send_document with `file_path=` kwarg, but WeixinAdapter.send_document
declared the argument as `path`, so any non-image/non-video file send
through the auto-forward path raised:
TypeError: WeixinAdapter.send_document() got an unexpected keyword
argument 'file_path'
Rename the primary argument to `file_path` to match the base class
(and every other platform adapter). Also accept `file_name`, `reply_to`,
and trailing `**kwargs` for full base-class parity. A legacy `path=`
kwarg is still honored through `**kwargs`, and empty calls now return
a clear SendResult error instead of ambiguously failing inside
`_send_file`.
Co-authored-by: moonaries90 <53324877+moonaries90@users.noreply.github.com>
|
Thanks @moonaries90 — nice catch. Verified locally that Folded the fix into this PR in Checked that no internal caller passes |
|
Validated this against a real Weixin/iLink personal-account setup and a real WeChat client. I reproduced three separate outbound attachment problems on current main:
After applying those fixes together, I verified end-to-end that a bot-sent document attachment:
So from a real-user validation standpoint, this PR fixes not only grey image / media issues, but also practical document attachment usability on Weixin. |
|
@teknium1 Hey, just a heads-up on this one:
No rush, just flagging so it doesn't get buried. Thanks! |
…essages Four fixes for the Weixin/WeChat adapter, synthesized from the best aspects of community PRs #8407, #8521, #8360, #7695, #8308, #8525, #7531, #8144, #8251. 1. Streaming cursor (▉) stuck permanently — WeChat doesn't support message editing, so the cursor appended during streaming can never be removed. Add SUPPORTS_MESSAGE_EDITING = False to WeixinAdapter and check it in gateway/run.py to use an empty cursor for non-edit platforms. (Fixes #8307, #8326) 2. Media upload failures — two bugs in _send_file(): a) upload_full_url path used PUT (404 on WeChat CDN); now uses POST. b) aes_key was base64(raw_bytes) but the iLink API expects base64(hex_string); images showed as grey boxes. (Fixes #8352, #7529) Also: unified both upload paths into _upload_ciphertext(), preferring upload_full_url. Added send_video/send_voice methods and voice_item media builder for audio/.silk files. Added video_md5 field. 3. Markdown links stripped — WeChat can't render [text](url), so format_message() now converts them to 'text (url)' plaintext. Code blocks are preserved. (Fixes #7617) 4. Blank message prevention — three guards: a) _split_text_for_weixin_delivery('') returns [] not [''] b) send() filters empty/whitespace chunks before _send_text_chunk c) _send_message() raises ValueError for empty text as safety net Community credit: joei4cm (#8407), lyonDan (#8521), SKFDJKLDG (#8360), tomqiaozc (#7695), joshleeeeee (#8308), luoxiao6645(#8525), longsizhuo (#7531), Astral-Yang (#8144), QingWei-Li (#8251).
…essages (#8665) Four fixes for the Weixin/WeChat adapter, synthesized from the best aspects of community PRs #8407, #8521, #8360, #7695, #8308, #8525, #7531, #8144, #8251. 1. Streaming cursor (▉) stuck permanently — WeChat doesn't support message editing, so the cursor appended during streaming can never be removed. Add SUPPORTS_MESSAGE_EDITING = False to WeixinAdapter and check it in gateway/run.py to use an empty cursor for non-edit platforms. (Fixes #8307, #8326) 2. Media upload failures — two bugs in _send_file(): a) upload_full_url path used PUT (404 on WeChat CDN); now uses POST. b) aes_key was base64(raw_bytes) but the iLink API expects base64(hex_string); images showed as grey boxes. (Fixes #8352, #7529) Also: unified both upload paths into _upload_ciphertext(), preferring upload_full_url. Added send_video/send_voice methods and voice_item media builder for audio/.silk files. Added video_md5 field. 3. Markdown links stripped — WeChat can't render [text](url), so format_message() now converts them to 'text (url)' plaintext. Code blocks are preserved. (Fixes #7617) 4. Blank message prevention — three guards: a) _split_text_for_weixin_delivery('') returns [] not [''] b) send() filters empty/whitespace chunks before _send_text_chunk c) _send_message() raises ValueError for empty text as safety net Community credit: joei4cm (#8407), lyonDan (#8521), SKFDJKLDG (#8360), tomqiaozc (#7695), joshleeeeee (#8308), luoxiao6645(#8525), longsizhuo (#7531), Astral-Yang (#8144), QingWei-Li (#8251).
|
Merged via PR #8665 which synthesizes the best fixes from ~25 community PRs into a single consolidated change. Your contribution (aes_key encoding + upload_full_url CDN path fix) was reviewed and informed the final implementation. Thank you @longsizhuo for your work on this! |
…essages (NousResearch#8665) Four fixes for the Weixin/WeChat adapter, synthesized from the best aspects of community PRs NousResearch#8407, NousResearch#8521, NousResearch#8360, NousResearch#7695, NousResearch#8308, NousResearch#8525, NousResearch#7531, NousResearch#8144, NousResearch#8251. 1. Streaming cursor (▉) stuck permanently — WeChat doesn't support message editing, so the cursor appended during streaming can never be removed. Add SUPPORTS_MESSAGE_EDITING = False to WeixinAdapter and check it in gateway/run.py to use an empty cursor for non-edit platforms. (Fixes NousResearch#8307, NousResearch#8326) 2. Media upload failures — two bugs in _send_file(): a) upload_full_url path used PUT (404 on WeChat CDN); now uses POST. b) aes_key was base64(raw_bytes) but the iLink API expects base64(hex_string); images showed as grey boxes. (Fixes NousResearch#8352, NousResearch#7529) Also: unified both upload paths into _upload_ciphertext(), preferring upload_full_url. Added send_video/send_voice methods and voice_item media builder for audio/.silk files. Added video_md5 field. 3. Markdown links stripped — WeChat can't render [text](url), so format_message() now converts them to 'text (url)' plaintext. Code blocks are preserved. (Fixes NousResearch#7617) 4. Blank message prevention — three guards: a) _split_text_for_weixin_delivery('') returns [] not [''] b) send() filters empty/whitespace chunks before _send_text_chunk c) _send_message() raises ValueError for empty text as safety net Community credit: joei4cm (NousResearch#8407), lyonDan (NousResearch#8521), SKFDJKLDG (NousResearch#8360), tomqiaozc (NousResearch#7695), joshleeeeee (NousResearch#8308), luoxiao6645(NousResearch#8525), longsizhuo (NousResearch#7531), Astral-Yang (NousResearch#8144), QingWei-Li (NousResearch#8251).
…essages (NousResearch#8665) Four fixes for the Weixin/WeChat adapter, synthesized from the best aspects of community PRs NousResearch#8407, NousResearch#8521, NousResearch#8360, NousResearch#7695, NousResearch#8308, NousResearch#8525, NousResearch#7531, NousResearch#8144, NousResearch#8251. 1. Streaming cursor (▉) stuck permanently — WeChat doesn't support message editing, so the cursor appended during streaming can never be removed. Add SUPPORTS_MESSAGE_EDITING = False to WeixinAdapter and check it in gateway/run.py to use an empty cursor for non-edit platforms. (Fixes NousResearch#8307, NousResearch#8326) 2. Media upload failures — two bugs in _send_file(): a) upload_full_url path used PUT (404 on WeChat CDN); now uses POST. b) aes_key was base64(raw_bytes) but the iLink API expects base64(hex_string); images showed as grey boxes. (Fixes NousResearch#8352, NousResearch#7529) Also: unified both upload paths into _upload_ciphertext(), preferring upload_full_url. Added send_video/send_voice methods and voice_item media builder for audio/.silk files. Added video_md5 field. 3. Markdown links stripped — WeChat can't render [text](url), so format_message() now converts them to 'text (url)' plaintext. Code blocks are preserved. (Fixes NousResearch#7617) 4. Blank message prevention — three guards: a) _split_text_for_weixin_delivery('') returns [] not [''] b) send() filters empty/whitespace chunks before _send_text_chunk c) _send_message() raises ValueError for empty text as safety net Community credit: joei4cm (NousResearch#8407), lyonDan (NousResearch#8521), SKFDJKLDG (NousResearch#8360), tomqiaozc (NousResearch#7695), joshleeeeee (NousResearch#8308), luoxiao6645(NousResearch#8525), longsizhuo (NousResearch#7531), Astral-Yang (NousResearch#8144), QingWei-Li (NousResearch#8251).
…essages (NousResearch#8665) Four fixes for the Weixin/WeChat adapter, synthesized from the best aspects of community PRs NousResearch#8407, NousResearch#8521, NousResearch#8360, NousResearch#7695, NousResearch#8308, NousResearch#8525, NousResearch#7531, NousResearch#8144, NousResearch#8251. 1. Streaming cursor (▉) stuck permanently — WeChat doesn't support message editing, so the cursor appended during streaming can never be removed. Add SUPPORTS_MESSAGE_EDITING = False to WeixinAdapter and check it in gateway/run.py to use an empty cursor for non-edit platforms. (Fixes NousResearch#8307, NousResearch#8326) 2. Media upload failures — two bugs in _send_file(): a) upload_full_url path used PUT (404 on WeChat CDN); now uses POST. b) aes_key was base64(raw_bytes) but the iLink API expects base64(hex_string); images showed as grey boxes. (Fixes NousResearch#8352, NousResearch#7529) Also: unified both upload paths into _upload_ciphertext(), preferring upload_full_url. Added send_video/send_voice methods and voice_item media builder for audio/.silk files. Added video_md5 field. 3. Markdown links stripped — WeChat can't render [text](url), so format_message() now converts them to 'text (url)' plaintext. Code blocks are preserved. (Fixes NousResearch#7617) 4. Blank message prevention — three guards: a) _split_text_for_weixin_delivery('') returns [] not [''] b) send() filters empty/whitespace chunks before _send_text_chunk c) _send_message() raises ValueError for empty text as safety net Community credit: joei4cm (NousResearch#8407), lyonDan (NousResearch#8521), SKFDJKLDG (NousResearch#8360), tomqiaozc (NousResearch#7695), joshleeeeee (NousResearch#8308), luoxiao6645(NousResearch#8525), longsizhuo (NousResearch#7531), Astral-Yang (NousResearch#8144), QingWei-Li (NousResearch#8251).
What does this PR do?
Fixes three bugs in the Weixin (iLink Bot) adapter's outbound media pipeline that caused bot-sent images to appear as a grey placeholder in the WeChat client — the image is delivered but cannot be opened or decrypted.
Root cause:
image_item.media.aes_keywas encoded asbase64(raw_16_bytes), but the WeChat client's decode chain is:Sending
base64(raw_bytes)produced 16 decoded bytes that couldn't be parsed as hex, so the client derived the wrong key and AES-128-ECB decryption failed silently, leaving the user with an unusable grey image.Two follow-on bugs in the same
_send_file()path were fixed at the same time because they blocked the image from ever reaching the client correctly:upload_full_url(returned by newer iLink API instances) requires HTTPPOST, notPUT. The old code only triedPUTand got404from the CDN.encrypted_query_paramquery argument embedded insideupload_full_urlis an upload-auth token, not the download key. The real download key is returned by the CDN in thex-encrypted-paramresponse header after a successful upload — the old code used the wrong value, which produced an encrypted reference the client could not resolve.Related Issue
Fixes #7529
Type of Change
Changes Made
All changes are in
gateway/platforms/weixin.py:_send_file()—aes_keyencoding (root cause). Changedbase64.b64encode(aes_key).decode("ascii")→base64.b64encode(aes_key.hex().encode("ascii")).decode("ascii"), with a comment documenting the client's decode chain._send_file()—upload_full_urlCDN method. Replaced the single-PUTcall with aPOST-first,PUT-on-404 fallback loop so both current and legacy CDN endpoints work._send_file()—encrypted_query_paramsource. The download key is now taken from thex-encrypted-paramresponse header returned by the CDN after the ciphertext upload, not from the query string ofupload_full_url. Falls back tofilekeyif the header is missing. A clearRuntimeErroris raised if both methods fail.send_image_file()— parameter name. Renamedpath→image_pathto match the base adapter signature (gateway/platforms/base.py:1058). Accepts legacypath=kwarg for backwards compatibility, and returnsSendResult(success=False, error=...)if neither is provided instead of lettingPath("").read_bytes()raiseIsADirectoryErrordeep in the stack._outbound_media_builder()— removed unusedaes_key_hexkwarg plumbing (review feedback: it was passed toitem_builder(...)but none of the lambdas read it).How to Test
Reproducing the original bug (before this PR):
hermes gateway setup, scan QR, confirm login).hermes gateway.拍一张照片or any prompt that triggers a local image file response).Verifying the fix (with this PR):
Code-level sanity check of the
aes_keyencoding fix:The old
base64.b64encode(aes_key).decode("ascii")produces 16 decoded bytes that are not valid ASCII hex, sobytes.fromhex(...)on the client side fails to recover the key.Checklist
Code
fix(gateway/weixin): ...)pytest tests/ -qand all tests pass — not yet re-run after the latest revert; will re-run before mergeaiohttp.ClientSessionto cover thePOST→PUTfallback, thex-encrypted-paramheader read, and thebase64(hex)aes_keyencoding in a follow-up commit on this branchDocumentation & Housekeeping
website/docs/user-guide/messaging/weixin.mdalready describes the encrypted CDN pipeline at the correct level of abstraction)cli-config.yaml.exampleif I added/changed config keys — N/A (no config changes)CONTRIBUTING.mdorAGENTS.md— N/A (no architectural/workflow change)send_image_file(image_path=...)now matches the base adapter signature, which is a fix, not a breaking change)Screenshots / Logs
Before: image bubble renders as a grey placeholder in the WeChat mobile client; tap-to-preview does nothing.

After: image renders correctly, preview and long-press actions work as expected.
Screenshots showing the before/after can be added on request; they contain personal chat context so they are not attached inline.