Skip to content

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

Closed
ZexinLiang wants to merge 685 commits into
lsdefine:mainfrom
ZexinLiang:feat/qq-media-io
Closed

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

Conversation

@ZexinLiang

@ZexinLiang ZexinLiang commented Jun 5, 2026

Copy link
Copy Markdown

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.

lsdefine and others added 30 commits May 7, 2026 17:41
- subagent.md: 隐藏--nobg选项防止同步阻塞超时
- task_planning.md: 加第7条无限重试禁自评+探测策略重排序
- autonomous_operation_sop.md: set_todo前缀提示
)

* feat: add setup.py -- interactive initialization wizard

* fix: address PR lsdefine#295 review — rename to configure.py, improve all feedback

- Rename setup.py → configure.py to avoid pip/setuptools conflict
- Fix lark exception: catch generic Exception instead of non-existent AppAccessDeniedError/AppExpiredError
- Add proxy support to probe_models via HTTPS_PROXY/HTTP_PROXY env vars
- Implement masked_input() with real-time API key masking
- Add pip install hints for messaging platform dependencies
- Add 2 new providers: CRS Gemini Ultra (Antigravity), Kimi/Moonshot OAI
- Restructure main menu: LLM only / platform only / or both with cascade prompts
- Add Python 3.14 compatibility warning and reconfiguration detection
- Fix welcome_message: WeCom only, correct variable name
- Show GETTING_STARTED example commands after configuration
- Add advanced LLM options: proxy, context_win, stream, user_agent

---------

Co-authored-by: guowenjiao54 <guowenjiao54@users.noreply.github.com>
…sdefine#307)

When the model only outputs tool calls with no meaningful text,
combined_content may contain only whitespace text blocks (e.g. '\n').
Strict API proxies reject this with HTTP 400 'empty content' error.
Filter out such blocks before sending to the API.
Co-authored-by: Kailigithub <Kailigithub@users.noreply.github.com>
Mirrors Claude Code's /btw: ask the agent a quick question without
interrupting the in-flight main task.

- frontends/btw_cmd.py: snapshot backend.history under lock + deepcopy →
  background-thread backend.raw_ask single-turn answer; never writes
  backend.history; never touches task_queue. Reuses backend.raw_ask +
  make_messages, no new LLM instance.
- frontends/chatapp_common.py: register /btw in HELP_COMMANDS; handle
  /btw in AgentChatMixin (sync via asyncio.to_thread); install() at
  module bottom so the slash hook is wired for any frontend that
  imports chatapp_common.
- frontends/stapp.py: persist display_queue + partial_response in
  st.session_state so the streaming bubble survives Streamlit reruns;
  new render_main_stream() helper covers both new tasks and resume;
  /btw branch uses plain st.rerun() (preserves dq) so the main task's
  remaining output renders in a follow-up bubble; non-/btw prompts
  explicitly call agent.abort() to keep the original "submit cancels"
  UX. Strip <summary> meta tags from text segments and drop dangling
  "LLM Running (Turn N) ..." markers in resumed partials to clean up
  the rendered chat.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lsdefine and others added 21 commits May 28, 2026 22:28
…fine#523)

CommonStack (https://commonstack.ai) is an OAI-compatible gateway
aggregating Claude, GPT, Gemini, DeepSeek, MiniMax, Zhipu, xAI,
Moonshot, Qwen and Xiaomi models behind a single key.

Adds it to the configure_mykey wizard as a native_oai entry,
placed alongside other multi-provider relays (openrouter, crs, gmi).
The wizard's existing probe_models() flow fetches the live
/v1/models list at config time; model_choices keeps the same
2-item fallback shape as openrouter.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tui): drain exit-boundary intervened replay

* refine(tui): soften intervened message prompt
Update WeChat group QR code (group 19 → group 20)
…search nav (lsdefine#554)

* fix(tui): stop CRLF stdout rendering as blank lines (v2)

Windows child stdout is `\r\n`; the streaming path fed it straight to
`Text.from_ansi`, which treats `\r` as a carriage return and erases each
line's text, so a `[Stdout]` block rendered as a run of blank lines until
the turn finished (the done-state Markdown render strips `\r`). Normalise
`\r\n` -> `\n` before `from_ansi`; lone `\r` is kept so progress-bar
overwrites still work.

* fix(tui): stop leaking empty per-session task_dir signal folders

Both v2 and v3 eagerly `os.makedirs` the per-session `_intervene` /
`_keyinfo` signal dir, but the only writer (`_intervene`) already creates
it lazily — so every session that never used intervene left a stale empty
`temp/_tui_v2_<pid>_<id>` / `temp/_tui_v3_<pid>` behind (85+ accumulated).
`consume_file` tolerates a missing dir, so the eager create is redundant.

- Set only the task_dir *path*; let the writer create it on first inject.
- Remove empty dirs on session close (v2 `_cmd_close`), on graceful exit
  (v2 `on_unmount`, v3 `_cleanup`), and sweep stale ones left by crashes
  at startup (v2 `on_mount`, v3 `main`). `os.rmdir` only removes empty
  dirs, so a still-pending `_intervene` is never clobbered.

* fix(tui): repair /continue search box cursor & up-nav loop (v2)

The SearchablePicker swallowed Right while the search Input had focus
(always treating it as "select"), so the caret could never move right in
the query. And Up could leave the list but never return: from the search
box Down entered the list, yet Up at the top row was stuck.

- Right commits the highlight only when the caret is already at end of the
  query; otherwise it moves the caret within the search text.
- Up on the first list row returns focus to the search box (scoped to
  /continue — no-op for ChoiceLists outside a SearchablePicker), clearing
  the highlight so the next Down re-enters at the first row, not the second.
- Up from the search box wraps around to the bottom of the list.
fix: add plugins/__init__.py for proper package recognition
…证后增补验收线/锁边界质疑目标/遗留项如实入报告; TMWebDriver: 修mark_disconnected重复打印
/continue list previews showed JSON debris (e.g. `{`, `" } ]} === Response ===`)
for some sessions. Two causes in _preview_from_file:

1. Models sometimes emit an unclosed <summary>, so the non-greedy DOTALL regex
   pairs it with a far-away </summary> and captures === block headers / JSON
   across rounds. Now only the latest <summary> is used, and it is rejected if
   it looks like such a cross-segment capture (contains `=== `/`"role"` or
   >200 chars) — falling through instead of digging for older summaries.
2. The fallback scanned for the first non-=== line, returning JSON structure
   lines. It now uses the last real user prompt via a new _last_user(), which
   scans Prompt blocks directly (so response-less/aborted sessions still
   preview) and reuses _user_text() to skip tool_result continuations and all
   _INJECT_MARKERS (WORKING MEMORY / SYSTEM TIPS / DANGER ...).

Scanned 261 local logs: garbage previews 26 -> 0.
…ew-garbage

fix(continue): robust session preview for malformed logs
…loop, log name collision fix, checklist poll hard cap
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

Copilot AI left a comment

Copy link
Copy Markdown

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
@lsdefine

Copy link
Copy Markdown
Owner

Closing because the base branch history was force-pushed. This PR now includes hundreds of unrelated commits/files and can no longer be reviewed or merged safely. If the change is still needed, please open a fresh PR rebased onto the current main. Thank you for the contribution.

@lsdefine lsdefine closed this Jul 29, 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.