You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Telegram Bot ConversationHandler异常及流程跳过问题求助

问题背景

使用python-telegram-bot开发Telegram机器人,功能是从Reddit获取视频,经moviepy编辑后,通过ConversationHandler向用户请求额外输入,目前遇到两个问题:

  1. 触发AttributeError: 'NoneType' object has no attribute 'reply_text',调用update.message.reply_text("test")时出现;
  2. 调用update.effective_message.reply_text询问用户后,机器人未等待回复直接进入下一个视频上传循环。

相关代码片段

对话启动函数

async def start_editor_questions(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Starts the conversation by sending the video and asking the first question."""
    output_filename = context.user_data.get('output_filename')
    
    await context.bot.send_video(chat_id=update.effective_chat.id, video=open(output_filename, 'rb'))
    await update.effective_message.reply_text("What height do you want? (h1/h2)")
    
    return HEIGHT

主函数

if __name__ == '__main__':
    application = Application.builder().token('YOUR_TOKEN').build()
    
    # Register handlers
    start_handler = CommandHandler('start', start)
    application.add_handler(start_handler)
    application.add_handler(CallbackQueryHandler(button_handler))

    conv_handler = ConversationHandler(
        entry_points=[CommandHandler("start", start_editor_questions)],
        states={
            HEIGHT: [MessageHandler(filters.TEXT & ~filters.COMMAND, height_response)],
            TRIM: [MessageHandler(filters.TEXT & ~filters.COMMAND, trim_response)],
            WATERMARK_COLOR: [MessageHandler(filters.TEXT & ~filters.COMMAND, watermark_color_response)],
            WATERMARK_OPACITY: [MessageHandler(filters.TEXT & ~filters.COMMAND, watermark_opacity_response)],
            TITLE: [MessageHandler(filters.TEXT & ~filters.COMMAND, title_response)],
            TITLE_COLOR: [MessageHandler(filters.TEXT & ~filters.COMMAND, title_color_response)],
            CAPTION: [MessageHandler(filters.TEXT & ~filters.COMMAND, caption_response)],
        },
        fallbacks=[CommandHandler("cancel", cancel)],
    )
    
    application.add_handler(conv_handler)
    application.run_polling(allowed_updates=Update.ALL_TYPES)

视频处理调用函数

async def process_account_videos(update: Update, context: ContextTypes.DEFAULT_TYPE, account_name, link, remaining_posts):
    print("process_account_videos")
    limit = 1
    after = None
    retrieved_list_file = 'retrieved_list.csv'
   
    file_path = retrieved_list_file
    required_columns = ['time', 'accountname', 'special_name', 'after_retr']
    
    if remaining_posts == 0:
        return
    if not os.path.exists(file_path):
        df = pd.DataFrame(columns=required_columns)
        df.to_csv(file_path, index=False)
        print(f"Created new CSV file: {file_path}")
    else:
        df = pd.read_csv(file_path)
        missing_columns = [col for col in required_columns if col not in df.columns]
        if missing_columns:
            print(f"Warning: Missing columns {missing_columns}. Recreating the file.")
            df = pd.DataFrame(columns=required_columns)
            df.to_csv(file_path, index=False)
            print(f"Recreated CSV file: {file_path}")

    retrieved_list = pd.read_csv(retrieved_list_file)
    account_data = retrieved_list[retrieved_list['accountname'] == account_name]

    i = 1
    while loop_number < 3:
        account_data = retrieved_list[retrieved_list['accountname'] == account_name]

        check = False
        if not account_data.empty:
            check = True
            account_data['time'] = pd.to_datetime(account_data['time'], errors='coerce')
            account_data = account_data.sort_values(by='time', ascending=False)
            special_name_list = account_data['special_name'].values
            after = account_data.iloc[0]['after_retr']

        url = f"{link}.json?limit={limit}"
        if after:
            url += f"&after={after}"
        print(url)
        response = requests.get(url, headers={"User-Agent": "username"})
        
        if response.status_code == 200:
            data = response.json()
            posts = data['data']['children']
            account_folder = f"./downloads/{account_name}"
            os.makedirs(account_folder, exist_ok=True)
            output_folder = f"./downloads/{account_name}/output"
            os.makedirs(output_folder, exist_ok=True)
            
            for post in posts:
                if check is True:
                    if post['data']['name'] in special_name_list:
                        continue
                if post['data']['secure_media'] is None or post['data']['over_18'] is False:
                    new_row = {
                        'time': pd.Timestamp.now(),
                        'accountname': account_name,
                        'special_name': post['data']['name'],
                        'after_retr': data['data'].get("after")
                    }
                    retrieved_list = pd.concat([retrieved_list, pd.DataFrame([new_row])], ignore_index=True)
                    retrieved_list.to_csv(retrieved_list_file, index=False)
                    continue

                video_url = post['data']['secure_media']['reddit_video']['fallback_url']
                base_video_url = video_url.split("/DASH_")[0]

                audio_url = f"{base_video_url}/DASH_AUDIO_128.mp4"
                audio_response = requests.get(audio_url, stream=True)

                if audio_response.status_code != 200:
                    audio_url = f"{base_video_url}/DASH_audio.mp4"
                    audio_response = requests.get(audio_url, stream=True)

                video_filename = os.path.join(account_folder, f"video{i}.mp4")
                with open(video_filename, "wb") as f:
                    video_response = requests.get(video_url, stream=True)
                    f.write(video_response.content)

                if audio_response.status_code == 200:
                    audio_filename = os.path.join(account_folder, f"audio{i}.mp3")
                    with open(audio_filename, "wb") as f:
                        f.write(audio_response.content)

                    output_filename = os.path.join(output_folder, f"output{i}.mp4")
                    # Using subprocess to merge video and audio
                    subprocess.run(["ffmpeg", "-i", video_filename, "-i", audio_filename, "-c", "copy", output_filename])
                else:
                    output_filename = os.path.join(output_folder, f"output{i}.mp4")
                    os.rename(video_filename, output_filename)
                
                new_row = {
                        'time': pd.Timestamp.now(),
                        'accountname': account_name,
                        'special_name': post['data']['name'],
                        'after_retr': data['data'].get("after")
                    }
                retrieved_list = pd.concat([retrieved_list, pd.DataFrame([new_row])], ignore_index=True)
                retrieved_list.to_csv(retrieved_list_file, index=False)
                
                # Increment the loop_number after each iteration
                loop_number += 1
                i += 1


                context.user_data['account_name'] = account_name
                context.user_data['output_filename'] = output_filename
                await start_editor_questions(update, context)
                
                output_folder = context.user_data.get('output_folder')
                caption_name = context.user_data.get('caption')
                
                await upload(output_folder, caption_name)  # Implement this function according to your needs
               
                if loop_number >= remaining_posts:
                    break
        else:
            print(f"Failed to connect to Reddit. Status code: {response.status_code}")
            break

问题1:AttributeError 原因及解决

原因

  • update.message为None时调用reply_text会触发该错误。这种情况通常发生在非消息类型的更新(比如回调查询触发的逻辑),此时update对象没有有效message属性,只有update.callback_query.message存在。
  • 在process_account_videos中直接调用start_editor_questions时,若当前update来自回调查询,update.message会是空值,导致调用reply_text失败。

解决方法

  1. 统一使用context.bot.send_message替代reply_text,明确指定聊天ID,不依赖update.message:
    await context.bot.send_message(chat_id=update.effective_chat.id, text="What height do you want? (h1/h2)")
    
  2. 若必须使用reply_text,先检查有效消息是否存在:
    if update.effective_message:
        await update.effective_message.reply_text("What height do you want? (h1/h2)")
    else:
        await context.bot.send_message(chat_id=update.effective_chat.id, text="What height do you want? (h1/h2)")
    
  3. 确保调用start_editor_questions时,update对象来自有效消息上下文(比如用户发送消息触发的逻辑)。

问题2:对话跳过等待输入的原因及解决

原因

  • ConversationHandler的工作逻辑是:只有当入口点被Telegram更新事件触发(比如用户发送/start命令),才会进入对话状态并等待用户回复。
  • 直接调用start_editor_questions只是执行了发送消息的逻辑,并没有让ConversationHandler接管对话状态,函数返回的HEIGHT状态无法被识别,导致机器人直接继续后续的上传循环。
  • 额外问题:代码中loop_number变量未初始化,会导致循环逻辑异常。

解决方法

  1. 不要直接调用对话状态函数,通过ConversationHandler的入口点触发对话:
    • 处理完视频后,用context.bot.send_message提示用户发送特定命令(比如/edit)启动编辑对话,同时将视频信息存入context.user_data。
  2. 整合视频处理逻辑到对话流程中:
    • 在ConversationHandler中新增前置状态,用于完成Reddit视频的获取和处理,完成后再进入提问环节。
  3. 初始化loop_number变量(比如loop_number = 0),修复循环逻辑异常。

内容的提问来源于stack exchange,提问作者Nicolas Hoechli

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 19:35:01