Skip to content

feat(qq): media in/out, live per-turn heartbeat, and attachment buffering for QQ bot#570

Open
ZexinLiang wants to merge 4 commits into
lsdefine:mainfrom
ZexinLiang:feat/qq-media-io
Open

feat(qq): media in/out, live per-turn heartbeat, and attachment buffering for QQ bot#570
ZexinLiang wants to merge 4 commits into
lsdefine:mainfrom
ZexinLiang:feat/qq-media-io

Conversation

@ZexinLiang
Copy link
Copy Markdown

@ZexinLiang ZexinLiang commented Jun 5, 2026

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)

  • 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).

2. Live per-turn heartbeat + closing message (d74eb84, frontends/chatapp_common.py)

  • Streams a per-turn heartbeat to the user as each turn finishes, so a long-running task can be monitored in real time instead of going silent.
  • Removes the old "still processing, please wait" placeholder and drops the single end-of-run dump of all turns (the heartbeats already cover it; files are still delivered).
  • Adds a closing message when the conversation fully ends, as a clear "done" signal.

3. Inbound attachment isolation + large-file degradation (b395af2, frontends/qqapp.py)

  • Inbound attachments are stored under a dedicated temp/qq_inbox/ dir with a TTL cleanup, so they no longer mix with outbound files and are easy to reap.
  • Outbound replies never re-send inbound files (_is_inbound guard).
  • Files too large to push as native QQ media fall back to a download link.
  • Enables stream_turns for the QQ frontend so the heartbeat above is active.

4. Attachment buffering + /clearfiles (a19763c, frontends/qqapp.py)

  • Attachments sent before/while a task is running are buffered (PENDING) and merged into the next text instruction, with a clear "buffered" prompt so users don't re-send.
  • /clearfiles lets the user discard buffered attachments before triggering.
  • Fixes the consecutive-send counter so multiple buffered files report the correct count (1 -> 2 -> ...), not always "1 received".

Notes

  • No new dependencies introduced by commits 2-4.
  • Files changed: frontends/chatapp_common.py, frontends/qqapp.py, frontends/puburl.py.
  • Verified with py_compile and local simulation; deployed and running on the test bot.

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).
Copilot AI review requested due to automatic review settings June 5, 2026 01:56
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.py to 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 thread frontends/puburl.py
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 thread frontends/qqapp.py Outdated
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 thread frontends/qqapp.py
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)
Comment thread frontends/qqapp.py


# QQ 附件 content_type: 1=图片 2=视频 3=语音 4=文件;不同消息类型字段可能不全,按后缀/url 兜底
def _guess_ext(att, kind):
Comment thread frontends/qqapp.py Outdated
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 thread frontends/puburl.py
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 thread frontends/puburl.py
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个"的问题
@ZexinLiang ZexinLiang changed the title feat(qqapp): support media in/out for QQ bot feat(qq): media in/out, live per-turn heartbeat, and attachment buffering for QQ bot Jun 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants