* feat(platform/max): add MAX messenger bridge with bidirectional file and voice support
- Implement core.Platform plus InlineButtonSender, ImageSender, FileSender,
AudioSender, MessageUpdater, FormattingInstructionProvider,
ReplyContextReconstructor
- Two-step attachment upload via POST /uploads + multipart to CDN
- Route incoming audio (type=audio and audio/* file mime) to
core.Message.Audio so the speech pipeline transcribes it for the agent
- Outgoing voice replies via SendAudio for the TTS pipeline
- Retry on attachment.not.ready with exponential backoff
- Long poll /updates with reconnect backoff and allow_from filtering
- Register max in Makefile ALL_PLATFORMS and document in config.example.toml
- Tighten .gitignore: ignore config.*.toml (except example) and *.old
- 19 unit/integration tests with httptest mock, passes -race
Refs #741
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(platform/max): handle real MAX CDN response shapes and audio download
Issues discovered when testing end-to-end with the live MAX API:
1. Image CDN returns nested {"photos": {<id>: {"token": ...}}}, not a flat
{"token": ...}. Parse the right shape per kind.
2. Audio CDN returns XML "<retval>1</retval>" rather than JSON. The real
attachment token is delivered in the /uploads response itself for
video/audio, so we use urlInfo.Token as the source of truth and only
scan the CDN body opportunistically.
3. /audios/{token} endpoint does not exist in MAX (returns 404). Incoming
voice payloads already carry the download URL directly, mirroring
images/files. Use payload.url; fall back to resolveMediaURL only if
empty (for video where token resolution may still apply).
Also raise the speech HTTP client timeout from 60s to 5m — enough for the
first-request cold-start case when a local Whisper backend has to download
or JIT-load the model before responding.
Tests updated to emit the real CDN response shapes per attachment type.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(platform/max): address review — dedup + idiomatic session key parse
- Add core.MessageDedup to Platform and check msg.Body.Mid / callback ID
in handleMessage and handleCallback. Guards against reprocessing on
poll-loop reconnect edge cases, matching the pattern used by feishu/
dingtalk/wecom platforms.
- Replace fmt.Sscanf in ReconstructReplyCtx with strings.CutPrefix +
strings.Cut — more idiomatic Go, simpler to read, and handles the
"max:" prefix and optional ":userID" suffix without a manual loop.
- Add TestHandleMessageDedupsByID regression test.
Addresses review feedback on #742.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>