feat(qq): media in/out, live per-turn heartbeat, and attachment buffering for QQ bot - #570
Closed
ZexinLiang wants to merge 685 commits into
Closed
feat(qq): media in/out, live per-turn heartbeat, and attachment buffering for QQ bot#570ZexinLiang wants to merge 685 commits into
ZexinLiang wants to merge 685 commits into
Conversation
… auto-fold old tasks in TUI
- 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>
… fix missing summary prompt wording
…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.
…ar Caches on Windows (lsdefine#309)
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>
…es to non-native paths
…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).
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个"的问题
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 |
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.