feat(qq): media in/out, live per-turn heartbeat, and attachment buffering for QQ bot#570
Open
ZexinLiang wants to merge 4 commits into
Open
feat(qq): media in/out, live per-turn heartbeat, and attachment buffering for QQ bot#570ZexinLiang wants to merge 4 commits into
ZexinLiang wants to merge 4 commits into
Conversation
Inbound: parse data.attachments (image/voice/file) instead of text-only, so the agent can read images, files and voice URLs sent by users. Outbound: send_done emits files marked with [FILE:path] as rich media via the two-step QQ API (post_c2c_file/post_group_file -> post_*_message msg_type=7). Public URL for QQ's reverse-fetch is provided by a new self-contained module frontends/puburl.py using a cloudflared quick tunnel (auto-downloads the binary on demand, grabs the tunnel URL at runtime, warms up the edge to avoid first-request SSL EOF). No account, fixed domain or manual config needed; reproducible on any machine with outbound network. Reuses existing extract_files/strip_files in chatapp_common; no new deps (aiohttp already declared).
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds QQ rich-media support by downloading inbound attachments and enabling outbound file/media sending via a temporary public URL tunnel.
Changes:
- Download QQ message attachments to a local temp folder and inject them into the agent prompt.
- Send agent-produced files as QQ rich media by publishing local files to a public HTTPS URL.
- Introduce
puburl.pyto run a local HTTP server and create a Cloudflare quick tunnel for reverse-pull uploads.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| frontends/qqapp.py | Handles inbound attachments, builds prompts containing temp file paths, and adds outbound rich-media sending via puburl. |
| frontends/puburl.py | New helper that publishes local files via an embedded HTTP server + cloudflared quick tunnel to generate public HTTPS URLs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+210
to
+212
| fname = os.path.basename(local_path) | ||
| shutil.copy2(local_path, os.path.join(dest_dir, fname)) | ||
| return f"{url}/{token}/{urllib.request.quote(fname)}" |
Comment on lines
+95
to
+102
| fpath = os.path.join(_TEMP_DIR, fname) | ||
| try: | ||
| async with sess.get(url, timeout=aiohttp.ClientTimeout(total=60)) as resp: | ||
| resp.raise_for_status() | ||
| body = await resp.read() | ||
| with open(fpath, "wb") as f: | ||
| f.write(body) | ||
| saved.append((kind, f"temp/{fname}")) |
Comment on lines
+97
to
+101
| async with sess.get(url, timeout=aiohttp.ClientTimeout(total=60)) as resp: | ||
| resp.raise_for_status() | ||
| body = await resp.read() | ||
| with open(fpath, "wb") as f: | ||
| f.write(body) |
|
|
||
|
|
||
| # QQ 附件 content_type: 1=图片 2=视频 3=语音 4=文件;不同消息类型字段可能不全,按后缀/url 兜底 | ||
| def _guess_ext(att, kind): |
Comment on lines
+205
to
+207
| except Exception as e: | ||
| print(f"[QQ] send_file failed ({name}): {e}") | ||
| await self.send_text(chat_id, f"⚠️ 文件「{name}」发送失败:{e}", msg_id=msg_id, is_group=is_group) |
Comment on lines
+92
to
+97
| url = f"https://github.com/cloudflare/cloudflared/releases/latest/download/{asset}" | ||
| _log(f"cloudflared 未找到,开始下载: {asset}") | ||
| tmp = binpath + ".part" | ||
| req = urllib.request.Request(url, headers={"User-Agent": "GA-puburl"}) | ||
| with urllib.request.urlopen(req, timeout=120) as resp, open(tmp, "wb") as f: | ||
| shutil.copyfileobj(resp, f) |
Comment on lines
+146
to
+148
| if not found.wait(timeout=45): | ||
| raise RuntimeError("等待 cloudflared 隧道 URL 超时") | ||
| self._warmup(self._tunnel_url) |
- run_agent 拆分为 classic/streaming 两种模式 - 经典模式用真实进度快照替换"还在处理中"占位 - 新增 format_turn_log/send_turn/send_done_files/format_done_message - 流式模式逐 turn 推送日志, 收尾不再汇总仅补发文件, 结束发结束语
- 入站附件下载到 temp/qq_inbox 并按 TTL 自动清理 - 出站时排除入站附件, 避免回传用户自己发来的文件 - _send_file 富媒体被腾讯拒收时降级为公网下载链接(TTL 1h) - 启用 stream_turns; send_done_files 收尾仅补发生成的文件
- PENDING 缓冲附件, 待用户文字指令一并触发模型 - 任务运行中收到附件给出缓存回执, 防止重复发送 - /clearfiles 撤销已缓存附件(命令分发前拦截) - 并发分支改用缓冲累计数替代单条 len(attachments), 修复连发文件始终显示"已收到1个"的问题
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR grew from the original media in/out work into a small set of related QQ-bot improvements. It contains 4 commits.
1. Media in/out (
bc648ee)data.attachments(image/voice/file) instead of text-only, so the agent can read images, files and voice URLs sent by users.send_doneemits files marked with[FILE:path]as rich media via the two-step QQ API (post_c2c_file/post_group_file->post_*_messagemsg_type=7).frontends/puburl.pyusing a cloudflared quick tunnel (auto-downloads the binary on demand, grabs the tunnel URL at runtime, warms up the edge to avoid first-request SSL EOF). No account, fixed domain or manual config needed; reproducible on any machine with outbound network.extract_files/strip_filesinchatapp_common; no new deps (aiohttpalready declared).2. Live per-turn heartbeat + closing message (
d74eb84,frontends/chatapp_common.py)3. Inbound attachment isolation + large-file degradation (
b395af2,frontends/qqapp.py)temp/qq_inbox/dir with a TTL cleanup, so they no longer mix with outbound files and are easy to reap._is_inboundguard).stream_turnsfor the QQ frontend so the heartbeat above is active.4. Attachment buffering +
/clearfiles(a19763c,frontends/qqapp.py)PENDING) and merged into the next text instruction, with a clear "buffered" prompt so users don't re-send./clearfileslets the user discard buffered attachments before triggering.Notes
frontends/chatapp_common.py,frontends/qqapp.py,frontends/puburl.py.py_compileand local simulation; deployed and running on the test bot.