From ef9d218f91a3c2a04cc831b8340c7bc843ddb3d3 Mon Sep 17 00:00:00 2001 From: YooooEX Date: Tue, 21 Apr 2026 11:43:53 +0800 Subject: [PATCH] feat: add Telegram slash command parity and menu sync --- README.md | 4 +- frontends/chatapp_common.py | 28 +++++++++++++- frontends/tgapp.py | 77 ++++++++++++++++++++++++++++++++----- 3 files changed, 96 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index feb96ed..ba0ad3a 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ streamlit run frontends/stapp2.py # Alternative Streamlit UI ### Common Chat Commands -The default Streamlit desktop UI started by `python launch.pyw`, plus the QQ / Feishu / WeCom / DingTalk frontends, support these chat commands: +The default Streamlit desktop UI started by `python launch.pyw`, plus the QQ / Telegram / Feishu / WeCom / DingTalk frontends, support these chat commands: - `/new` - start a fresh conversation and clear the current context - `/continue` - list recoverable conversation snapshots @@ -393,7 +393,7 @@ streamlit run frontends/stapp2.py # 另一种 Streamlit 风格 UI ### 通用聊天命令 -默认通过 `python launch.pyw` 启动的 Streamlit 桌面 UI,以及 QQ / 飞书 / 企业微信 / 钉钉前端,都支持以下命令: +默认通过 `python launch.pyw` 启动的 Streamlit 桌面 UI,以及 QQ / Telegram / 飞书 / 企业微信 / 钉钉前端,都支持以下命令: - `/new` - 开启新对话并清空当前上下文 - `/continue` - 列出可恢复会话快照 diff --git a/frontends/chatapp_common.py b/frontends/chatapp_common.py index 1f8b3e4..2aa5035 100644 --- a/frontends/chatapp_common.py +++ b/frontends/chatapp_common.py @@ -1,6 +1,32 @@ import ast, asyncio, glob, json, os, queue as Q, re, socket, sys, time -HELP_TEXT = "📖 命令列表:\n/help - 显示帮助\n/status - 查看状态\n/stop - 停止当前任务\n/new - 开启新对话并清空当前上下文\n/restore - 恢复上次对话历史\n/continue - 列出可恢复会话\n/continue [n] - 恢复第 n 个会话\n/llm [n] - 查看或切换模型" +HELP_COMMANDS = ( + ("/help", "显示帮助"), + ("/status", "查看状态"), + ("/stop", "停止当前任务"), + ("/new", "开启新对话并清空当前上下文"), + ("/restore", "恢复上次对话历史"), + ("/continue", "列出可恢复会话"), + ("/continue [n]", "恢复第 n 个会话"), + ("/llm", "查看当前模型列表"), + ("/llm [n]", "切换到第 n 个模型"), +) +TELEGRAM_MENU_COMMANDS = ( + ("help", "显示帮助"), + ("status", "查看状态"), + ("stop", "停止当前任务"), + ("new", "开启新对话并清空当前上下文"), + ("restore", "恢复上次对话历史"), + ("continue", "列出可恢复会话;/continue n 恢复第 n 个"), + ("llm", "查看模型列表;/llm n 切换到指定模型"), +) + + +def build_help_text(commands=HELP_COMMANDS): + return "📖 命令列表:\n" + "\n".join(f"{cmd} - {desc}" for cmd, desc in commands) + + +HELP_TEXT = build_help_text() FILE_HINT = "If you need to show files to user, use [FILE:filepath] in your response." TAG_PATS = [r"<" + t + r">.*?" for t in ("thinking", "summary", "tool_use", "file_content")] PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff --git a/frontends/tgapp.py b/frontends/tgapp.py index 48ec5d4..d2fed1e 100644 --- a/frontends/tgapp.py +++ b/frontends/tgapp.py @@ -3,12 +3,14 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) _TEMP_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'temp') from agentmain import GeneraticAgent try: - from telegram import Update - from telegram.ext import ApplicationBuilder, MessageHandler, CommandHandler, filters, ContextTypes + from telegram import BotCommand + from telegram.ext import ApplicationBuilder, MessageHandler, filters, ContextTypes from telegram.request import HTTPXRequest except: print("Please ask the agent install python-telegram-bot to use telegram module.") sys.exit(1) +from chatapp_common import HELP_TEXT, TELEGRAM_MENU_COMMANDS, format_restore +from continue_cmd import handle_frontend_command, reset_conversation from llmcore import mykeys agent = GeneraticAgent() @@ -82,6 +84,26 @@ async def _stream(dq, msg): except Exception: pass break + +def _normalized_command(text): + parts = (text or '').strip().split(None, 1) + if not parts: + return '' + head = parts[0].lower() + if head.startswith('/'): + head = '/' + head[1:].split('@', 1)[0] + return head + (f" {parts[1].strip()}" if len(parts) > 1 and parts[1].strip() else '') + + +def _cancel_stream_task(ctx): + task = ctx.user_data.pop('stream_task', None) + if task and not task.done(): + task.cancel() + + +async def _sync_commands(application): + await application.bot.set_my_commands([BotCommand(command, description) for command, description in TELEGRAM_MENU_COMMANDS]) + async def handle_msg(update, ctx): uid = update.effective_user.id if ALLOWED and uid not in ALLOWED: @@ -93,11 +115,9 @@ async def handle_msg(update, ctx): ctx.user_data['stream_task'] = task async def cmd_abort(update, ctx): + _cancel_stream_task(ctx) agent.abort() - task = ctx.user_data.get('stream_task') - if task and not task.done(): - task.cancel() - await update.message.reply_text("Aborted") + await update.message.reply_text("⏹️ 正在停止...") async def cmd_llm(update, ctx): args = (update.message.text or '').split() @@ -105,13 +125,50 @@ async def cmd_llm(update, ctx): try: n = int(args[1]) agent.next_llm(n) - await update.message.reply_text(f"Switched to [{agent.llm_no}] {agent.get_llm_name()}") + await update.message.reply_text(f"✅ 已切换到 [{agent.llm_no}] {agent.get_llm_name()}") except (ValueError, IndexError): - await update.message.reply_text(f"Usage: /llm <0-{len(agent.list_llms())-1}>") + await update.message.reply_text(f"用法: /llm <0-{len(agent.list_llms())-1}>") else: lines = [f"{'→' if cur else ' '} [{i}] {name}" for i, name, cur in agent.list_llms()] await update.message.reply_text("LLMs:\n" + "\n".join(lines)) + +async def handle_command(update, ctx): + uid = update.effective_user.id + if ALLOWED and uid not in ALLOWED: + return await update.message.reply_text("no") + cmd = _normalized_command(update.message.text) + op = cmd.split()[0] if cmd else '' + if op == '/help': + return await update.message.reply_text(HELP_TEXT) + if op == '/status': + llm = agent.get_llm_name() if agent.llmclient else '未配置' + return await update.message.reply_text(f"状态: {'🔴 运行中' if agent.is_running else '🟢 空闲'}\nLLM: [{agent.llm_no}] {llm}") + if op == '/stop': + return await cmd_abort(update, ctx) + if op == '/llm': + return await cmd_llm(update, ctx) + if op == '/new': + _cancel_stream_task(ctx) + return await update.message.reply_text(reset_conversation(agent)) + if op == '/restore': + _cancel_stream_task(ctx) + try: + restored_info, err = format_restore() + if err: + return await update.message.reply_text(err) + restored, fname, count = restored_info + agent.abort() + agent.history.extend(restored) + return await update.message.reply_text(f"✅ 已恢复 {count} 轮对话\n来源: {fname}\n(仅恢复上下文,请输入新问题继续)") + except Exception as e: + return await update.message.reply_text(f"❌ 恢复失败: {e}") + if op == '/continue': + if cmd != '/continue': + _cancel_stream_task(ctx) + return await update.message.reply_text(handle_frontend_command(agent, cmd)) + return await update.message.reply_text(HELP_TEXT) + if __name__ == '__main__': # Single instance lock using socket try: @@ -141,9 +198,9 @@ if __name__ == '__main__': .token(mykeys['tg_bot_token']) .request(request) .get_updates_request(request) + .post_init(_sync_commands) .build()) - app.add_handler(CommandHandler("stop", cmd_abort)) - app.add_handler(CommandHandler("llm", cmd_llm)) + app.add_handler(MessageHandler(filters.COMMAND, handle_command)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_msg)) app.add_error_handler(_error_handler)