feat: add Telegram slash command parity and menu sync (#125)

feat: add Telegram slash command parity and menu sync
This commit is contained in:
LJQ
2026-04-21 23:14:38 +08:00
committed by GitHub
3 changed files with 96 additions and 13 deletions

View File

@@ -118,7 +118,7 @@ streamlit run frontends/stapp2.py # Alternative Streamlit UI
### Common Chat Commands ### 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 - `/new` - start a fresh conversation and clear the current context
- `/continue` - list recoverable conversation snapshots - `/continue` - list recoverable conversation snapshots
@@ -394,7 +394,7 @@ streamlit run frontends/stapp2.py # 另一种 Streamlit 风格 UI
### 通用聊天命令 ### 通用聊天命令
默认通过 `python launch.pyw` 启动的 Streamlit 桌面 UI以及 QQ / 飞书 / 企业微信 / 钉钉前端,都支持以下命令: 默认通过 `python launch.pyw` 启动的 Streamlit 桌面 UI以及 QQ / Telegram / 飞书 / 企业微信 / 钉钉前端,都支持以下命令:
- `/new` - 开启新对话并清空当前上下文 - `/new` - 开启新对话并清空当前上下文
- `/continue` - 列出可恢复会话快照 - `/continue` - 列出可恢复会话快照

View File

@@ -1,6 +1,32 @@
import ast, asyncio, glob, json, os, queue as Q, re, socket, sys, time 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." FILE_HINT = "If you need to show files to user, use [FILE:filepath] in your response."
TAG_PATS = [r"<" + t + r">.*?</" + t + r">" for t in ("thinking", "summary", "tool_use", "file_content")] TAG_PATS = [r"<" + t + r">.*?</" + t + r">" for t in ("thinking", "summary", "tool_use", "file_content")]
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

View File

@@ -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') _TEMP_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'temp')
from agentmain import GeneraticAgent from agentmain import GeneraticAgent
try: try:
from telegram import Update from telegram import BotCommand
from telegram.ext import ApplicationBuilder, MessageHandler, CommandHandler, filters, ContextTypes from telegram.ext import ApplicationBuilder, MessageHandler, filters, ContextTypes
from telegram.request import HTTPXRequest from telegram.request import HTTPXRequest
except: except:
print("Please ask the agent install python-telegram-bot to use telegram module.") print("Please ask the agent install python-telegram-bot to use telegram module.")
sys.exit(1) 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 from llmcore import mykeys
agent = GeneraticAgent() agent = GeneraticAgent()
@@ -82,6 +84,26 @@ async def _stream(dq, msg):
except Exception: pass except Exception: pass
break 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): async def handle_msg(update, ctx):
uid = update.effective_user.id uid = update.effective_user.id
if ALLOWED and uid not in ALLOWED: if ALLOWED and uid not in ALLOWED:
@@ -93,11 +115,9 @@ async def handle_msg(update, ctx):
ctx.user_data['stream_task'] = task ctx.user_data['stream_task'] = task
async def cmd_abort(update, ctx): async def cmd_abort(update, ctx):
_cancel_stream_task(ctx)
agent.abort() agent.abort()
task = ctx.user_data.get('stream_task') await update.message.reply_text("⏹️ 正在停止...")
if task and not task.done():
task.cancel()
await update.message.reply_text("Aborted")
async def cmd_llm(update, ctx): async def cmd_llm(update, ctx):
args = (update.message.text or '').split() args = (update.message.text or '').split()
@@ -105,13 +125,50 @@ async def cmd_llm(update, ctx):
try: try:
n = int(args[1]) n = int(args[1])
agent.next_llm(n) 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): 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: else:
lines = [f"{'' if cur else ' '} [{i}] {name}" for i, name, cur in agent.list_llms()] 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)) 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__': if __name__ == '__main__':
# Single instance lock using socket # Single instance lock using socket
try: try:
@@ -141,9 +198,9 @@ if __name__ == '__main__':
.token(mykeys['tg_bot_token']) .token(mykeys['tg_bot_token'])
.request(request) .request(request)
.get_updates_request(request) .get_updates_request(request)
.post_init(_sync_commands)
.build()) .build())
app.add_handler(CommandHandler("stop", cmd_abort)) app.add_handler(MessageHandler(filters.COMMAND, handle_command))
app.add_handler(CommandHandler("llm", cmd_llm))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_msg)) app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_msg))
app.add_error_handler(_error_handler) app.add_error_handler(_error_handler)