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
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
@@ -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` - 开启新对话并清空当前上下文
- `/continue` - 列出可恢复会话快照

View File

@@ -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">.*?</" + t + r">" for t in ("thinking", "summary", "tool_use", "file_content")]
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')
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)