From 9c3fc35e8e894475149d101d89ed52ff5812244c Mon Sep 17 00:00:00 2001 From: laowang Date: Sat, 14 Mar 2026 12:07:05 +0800 Subject: [PATCH] list --- .env | 4 + README.md | 102 ++++++++++++ tg_bot.py | 490 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 596 insertions(+) create mode 100644 .env create mode 100644 README.md create mode 100644 tg_bot.py diff --git a/.env b/.env new file mode 100644 index 0000000..b91bd9f --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +# .env 文件 +TELEGRAM_BOT_TOKEN= +NVIDIA_API_KEY= +ALLOWED_USERS= \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d5b4b32 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# 🤖 AI 代理机器人 (TG Bot) + +一个基于 NVIDIA API 的多模态 Telegram 聊天机器人,具备自主搜索、深度阅读、图片识别及长期记忆管理能力。 + +## ✨ 核心特性 + +### 1. 灵活的核心架构 +- **API 适配层**:默认对接 `https://integrate.api.nvidia.com/v1/chat/completions`,完全兼容所有 NVIDIA NeMo 微服务及第三方 OpenAI 协议接口。 +- **动态模型切换**:内置 `/model` 指令,实时并发检测所有注册模型的可用性(显示 ✅/❌ 状态),一键无缝切换推理引擎。 +- **主脚本配置**:所有核心参数(API 地址、密钥、模型列表)均在 `tg_bot.py` 顶部配置,无需额外配置文件即可快速迁移或私有化部署。 + +### 2. 智能 Agent 能力 +- **自主决策流程**: + - 🔍 **Web Search**:优先使用搜索引擎获取最新信息。 + - 📄 **Web Fetch**:自动判断是否需要深度阅读全文(包含防 PDF 陷阱机制)。 + - 🖼️ **Image Search**:支持按关键词实时检索图片并返回 Markdown 格式展示。 + - 🧠 **Memory Mgmt**:内置单用户级长期记忆存储 (`user_memory.json`),支持添加、查询、删除关键个人偏好。 +- **上下文优化**: + - 自动压缩长对话历史,保留 System Prompt 和最近 20 轮有效信息。 + - 防止重复抓取同一 URL,节省 Token 消耗。 + +### 3. 输出优化 +- **移动端友好**:强制禁用 Markdown 表格,转换为清晰的无序列表格式,完美适配 Telegram 客户端渲染。 +- **流式状态反馈**:发送消息后立即编辑占位符文字(如“正在搜索..."、“正在深度阅读...”),提升交互体验。 + +--- + +## 🚀 快速开始 + +### 1. 环境依赖 +确保已安装 Python 3.8+ 及以下库: +```bash +pip install pyTelegramBotAPI python-dotenv duckduckgo-search beautifulsoup4 requests urllib3 +``` + +### 2. 环境变量配置 (.env) +在项目根目录创建 `.env` 文件填入凭证: +```ini +# Telegram Bot Token +TELEGRAM_BOT_TOKEN=your_telegram_token_here + +# NVIDIA API Key (或其他兼容接口的 Key) +NVIDIA_API_KEY=nvapi-your-key-here + +# 允许使用的用户 ID (逗号分隔,为空则不限) +ALLOWED_USERS=123456789,987654321 +``` + +### 3. 运行程序 +```bash +cd E:\AI_Workspace +python tg_bot.py +``` + +--- + +## ⚙️ 高级定制指南 + +### 修改 API 地址与模型 +如需适配非 NVIDIA 官方接口(例如本地 Ollama 或其他云厂商): + +1. 打开 `tg_bot.py`。 +2. 定位至 **核心配置区**(约第 25 行): + ```python + NVIDIA_API_URL = "https://integrate.api.nvidia.com/v1/chat/completions" # 修改此处 + DEFAULT_MODEL = "openai/gpt-oss-120b" # 修改默认模型 + ``` +3. 更新 `MODEL_MAP` 字典以匹配新提供商支持的模型 ID: + ```python + MODEL_MAP = { + "my-custom-model": "namespace/model-id", # 格式:display_name: api_model_id + ... + } + ``` + +### 扩展自定义工具 +在 `TOOLS` 列表中定义新的 Function Schema,并在 `execute_*` 系列函数中实现具体逻辑。 + +--- + +## 📂 项目结构 + +| 文件名 | 说明 | +| :--- | :--- | +| `tg_bot.py` | **主程序**。包含核心逻辑、Tool 定义、API 调用及 Telegram 事件处理。 | +| `.env` | **敏感配置**。Token 与 API Key 存放处,严禁提交到公共仓库。 | +| `user_memory.json` | **运行时自动生成**。存储每个 ChatID 对应的长期记忆数据。 | +| `README.md` | **本项目文档**。 | + +--- + +## 💡 常见问题 (FAQ) + +- **Q: 为什么某些模型显示 ❌?** + A: 可能是网络超时或 API Key 权限不足。检查 `.env` 中的 Key 是否正确,并确保网络能访问目标端点。 +- **Q: 内存占用过高怎么办?** + A: 每次重启程序会重置 `chat_memory` 缓存(但保留 `user_memory.json` 中的持久记忆)。长时间运行建议定期 `/reset` 清理过长的上下文窗口。 +- **Q: 图片如何处理?** + A: 上传的图片会被编码为 Base64 并通过 `image_url` 字段发送给支持视觉的模型;若需搜图,请使用自然语言描述意图(如“帮我找一张...的照片”),触发 `image_search` 工具。 + +--- +*最后更新时间:2026 年 03 月 14 日* diff --git a/tg_bot.py b/tg_bot.py new file mode 100644 index 0000000..181e50b --- /dev/null +++ b/tg_bot.py @@ -0,0 +1,490 @@ +import os +import json +import base64 +import datetime +import requests +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) +import telebot +from telebot import types +from concurrent.futures import ThreadPoolExecutor, as_completed +from dotenv import load_dotenv +from ddgs import DDGS +from bs4 import BeautifulSoup + +# ================= 1. 核心配置与环境变量 ================= +load_dotenv() + +TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN') +NVIDIA_API_KEY = os.getenv('NVIDIA_API_KEY') +NVIDIA_API_URL = "https://integrate.api.nvidia.com/v1/chat/completions" + +# 解析允许的用户 ID 列表 +try: + ALLOWED_USERS = [int(u.strip()) for u in os.getenv('ALLOWED_USERS', '').split(',') if u.strip()] +except ValueError: + ALLOWED_USERS = [] + + +MODEL_MAP = { + "gpt-oss-120b": "openai/gpt-oss-120b", + "qwen3-next-80b-a3b-thinking": "qwen/qwen3-next-80b-a3b-thinking", + "qwen3.5-122b-a10b": "qwen/qwen3.5-122b-a10b", + "qwen3.5-397b-a17b": "qwen/qwen3.5-397b-a17b", + "qwen3-coder-480b-a35b-instruct": "qwen/qwen3-coder-480b-a35b-instruct", + "kimi-k2.5": "moonshotai/kimi-k2.5", + "llama-3.1-70b-instruct": "meta/llama-3.1-70b-instruct", + "llama-3.1-405b-instruct": "meta/llama-3.1-405b-instruct", + "llama-3.3-70b-instruct": "meta/llama-3.3-70b-instruct", + "deepseek-v3.2": "deepseek-ai/deepseek-v3.2", + "deepseek-v3.1": "deepseek-ai/deepseek-v3.1", + "deepseek-v3.1-terminus": "deepseek-ai/deepseek-v3.1-terminus", + "minimax-m2.5": "minimaxai/minimax-m2.5", + "mistral-large-3-675b-instruct-2512": "mistralai/mistral-large-3-675b-instruct-2512" , +} + + +DEFAULT_MODEL = "openai/gpt-oss-120b" +CTX_SIZE = 16000 + +bot = telebot.TeleBot(TELEGRAM_BOT_TOKEN) +chat_memory = {} + +# ================= 记忆持久化模块 ================= +MEMORY_FILE = "user_memory.json" + +def load_memory(): + if os.path.exists(MEMORY_FILE): + with open(MEMORY_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + return {} + +def save_memory(data): + with open(MEMORY_FILE, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + +def execute_manage_memory(chat_id, action, content=None): + memories = load_memory() + user_mem = memories.get(str(chat_id), []) + + if action == "add" and content: + if content not in user_mem: + user_mem.append(content) + memories[str(chat_id)] = user_mem + save_memory(memories) + return f"已成功将 '{content}' 添加到长期记忆。" + elif action == "delete" and content: + # 模糊匹配删除 + user_mem = [m for m in user_mem if content not in m] + memories[str(chat_id)] = user_mem + save_memory(memories) + return f"已删除包含 '{content}' 的记忆。" + elif action == "list": + if not user_mem: return "当前没有任何长期记忆。" + return "【用户当前记忆列表】:\n" + "\n".join([f"- {m}" for m in user_mem]) + return "操作无效或缺少 content 参数。" + +user_selected_model = {} + +# ================= 2. 注册 TG 快捷菜单 ================= +def setup_bot_commands(): + try: + commands = [ + types.BotCommand("start", "🚀 启动并查看帮助"), + types.BotCommand("model", "🧠 切换 AI 模型"), + types.BotCommand("reset", "🧹 清空对话记忆") + ] + bot.set_my_commands(commands) + print("✅ Telegram 快捷菜单注册成功!") + except Exception as e: + print(f"❌ 快捷菜单注册失败: {e}") + +# ================= 3. 工具库 (Tools Definition) ================= +# 这是大模型的“使用说明书” +# ================= 3. 工具库 (Tools Definition) ================= +TOOLS = [ + { + "type": "function", + "function": { + "name": "web_search", + "description": "搜索互联网并返回最新的标题、链接和摘要。", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "搜索关键词"} + }, + "required": ["query"] + } + } + }, + { + "type": "function", + "function": { + "name": "web_fetch", + "description": "抓取特定网页的正文。遇到需要深度阅读的内容时调用。", + "parameters": { + "type": "object", + "properties": { + "url": {"type": "string", "description": "网页URL"} + }, + "required": ["url"] + } + } + }, + # 👇 新增:图片搜索 + { + "type": "function", + "function": { + "name": "image_search", + "description": "按关键词搜索图片并返回图片的直链。当用户需要找图、看图时调用。", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "图片搜索关键词"} + }, + "required": ["query"] + } + } + }, + # 👇 新增:主动记忆 + { + "type": "function", + "function": { + "name": "manage_memory", + "description": "读取、添加或删除关于用户的长期记忆(偏好、习惯、身份等)。当用户说'记住...'或'你记得我什么'时调用。", + "parameters": { + "type": "object", + "properties": { + "action": {"type": "string", "enum": ["add", "list", "delete"], "description": "操作类型"}, + "content": {"type": "string", "description": "要添加或删除的具体记忆内容(list操作时可留空)"} + }, + "required": ["action"] + } + } + } +] + +# 👇 紧接着加上搜图的执行逻辑(和 web_search 放在一起) +def execute_image_search(query): + print(f"🖼️ [Agent] 正在搜图: {query}") + try: + with DDGS(timeout=15) as ddgs: + results = list(ddgs.images(query, max_results=3)) + if not results: return "未找到相关图片。" + # 直接返回 Markdown 格式给大模型,让它输出给用户 + return "\n".join([f"![{r['title']}]({r['image']})" for r in results]) + except Exception as e: + return f"图片搜索失败: {e}" + +# 实际的工具执行函数 +def execute_web_search(query): + print(f"🔍 [Agent] 正在搜索: {query}") + try: + with DDGS(timeout=15) as ddgs: + results = list(ddgs.text(query, max_results=5)) + if not results: return "未找到相关结果。" + + formatted = "" + for i, r in enumerate(results): + formatted += f"[{i+1}] {r['title']}\nURL: {r['href']}\n摘要: {r['body']}\n\n" + return formatted + except Exception as e: + return f"搜索失败: {e}" + +def execute_web_fetch(url): + print(f"📄 [Agent] 正在阅读网页: {url}") + + # 【新增】防 PDF 陷阱机制 + if url.lower().endswith('.pdf'): + print("⚠️ [Agent] 拦截 PDF 读取,提示模型切换策略。") + return "【工具报错】:该链接是 PDF 文件,当前工具无法解析二进制内容。如果是论文,请尝试读取该论文的 HTML 摘要页(如将 /pdf/ 替换为 /abs/),或重新搜索相关的科技新闻报道。" + + headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} + try: + res = requests.get(url, headers=headers, timeout=10, verify=False) + res.raise_for_status() + soup = BeautifulSoup(res.text, 'html.parser') + + for tag in soup(['script', 'style', 'nav', 'footer', 'header']): + tag.decompose() + text = '\n'.join([line.strip() for line in soup.get_text().splitlines() if line.strip()]) + return text[:3500] + except Exception as e: + return f"网页抓取失败: {e}" + +# ================= 4. 核心逻辑与 API 调用 ================= +def get_chat_history(chat_id): + today = datetime.date.today().strftime("%Y年%m月%d日") + + # 强化版系统提示词:增加负面约束与正面示例 + ADVANCED_SYSTEM_PROMPT = f"""当前时间是 {today}。你是一个强大的多模态 AI 助手。 + +【核心行动手册】 +1. **知识调用**:你的自身知识库极其渊博!对于通用知识、编程基础、历史常识、常规聊天等,【必须】直接回答,**绝对禁止**调用工具!只有实时资讯或极冷门知识才允许使用 `web_search`。 +2. **工具节制(极度重要)**:你的上下文记忆极其有限! + - 搜新闻或列表时,**仅依靠 web_search 的摘要即可作答**,绝对禁止为了完美而去 web_fetch 每一篇文章! + - 一旦收集到能基本回答用户问题的信息,必须**立即停止调用工具**并输出最终结果。 +2. **排版极端禁令**:**绝对严禁**使用 Markdown 表格语法(即包含 | 和 - 的结构)。Telegram 无法正常渲染表格,会导致用户体验极差。 +3. **强制替代格式**:如果需要对比或列举数据,请严格按照以下格式: + + ### [类别名称/标题] + - **项目 A**:描述内容 + - **项目 B**:描述内容 + --- + *(重复上述结构直到完成对比)* + +4. **手机端优化**:必须使用【加粗标题】和【- 分点列表】。 + +【执行反馈】 +- 哪怕模型内部逻辑认为表格更清晰,也必须为了 Telegram 的兼容性将其转化为上述列表格式。""" + + # 👇 每次都刷新系统提示词,确保最新的约束生效 + if chat_id not in chat_memory: + chat_memory[chat_id] = [{"role": "system", "content": ADVANCED_SYSTEM_PROMPT}] + else: + # 确保索引 0 的 system 消息始终包含最新的 Prompt + chat_memory[chat_id][0]["content"] = ADVANCED_SYSTEM_PROMPT + + # === 上下文自动压缩机制 === + current_history = chat_memory[chat_id] + # 如果对话轮数过多,剔除最旧的一轮 user 和 assistant 回复,保留索引 0 的 system + while len(current_history) > 20: + current_history.pop(1) + + return current_history + +# 👇 注意增加了 status_msg 参数 +def chat_with_agent(chat_id, user_message, status_msg, max_loops=15): + history = get_chat_history(chat_id) + history.append(user_message) + + headers = {'Authorization': f'Bearer {NVIDIA_API_KEY}', 'Content-Type': 'application/json'} + model = user_selected_model.get(chat_id, DEFAULT_MODEL) + print(f"🧠 当前使用模型: {model}") # --- 新增日志 --- + + # 👇 新增:本轮防重复抓取记录(Token 保护机制) + visited_urls = set() + + for loop in range(max_loops): + print(f"🔄 思考循环: 第 {loop + 1} 次") # --- 新增日志 --- + payload = {"model": model, "messages": history, "tools": TOOLS, "temperature": 0.6, "max_tokens": 4096} + + try: + print("🌐 正在请求 NVIDIA API...") # --- 新增日志 --- + response = requests.post(NVIDIA_API_URL, headers=headers, json=payload, timeout=120) + response.raise_for_status() + print("🌐 API 请求成功") # --- 新增日志 --- + except requests.exceptions.ReadTimeout as e: + print(f"❌ API 超时: {e}") # --- 新增日志 --- + return "❌ API 接口超时,请稍后再试或切换模型。" + except Exception as e: + print(f"❌ API 请求异常: {e}") # --- 新增日志 --- + return f"❌ API 请求异常: {e}" + + data = response.json() + message = data['choices'][0]['message'] + + if not message.get("tool_calls"): + print("🏁 模型决定直接回复,无工具调用") # --- 新增日志 --- + + final_content = message.get("content") + # 修复:确保将 None 或纯空格转为空字符串处理 + if not final_content or not str(final_content).strip(): + print("⚠️ 警告:模型返回了空文本,触发保护机制。") + final_content = "【系统提示】:由于检索的信息量过大,模型未能成功生成文字回复。这通常是因为查阅了太多网页导致上下文超载。\n\n💡 **建议**:请发送 `/reset` 清空记忆,然后要求我“只搜索不深入阅读”,或者缩小搜索范围。" + + # 修复:这里必须使用 final_content,而不是 message["content"] + history.append({"role": "assistant", "content": final_content}) + return final_content + + history.append(message) + print(f"🛠️ 模型决定调用 {len(message['tool_calls'])} 个工具") # --- 新增日志 --- + + for tool_call in message["tool_calls"]: + func_name = tool_call["function"]["name"] + + try: + args = json.loads(tool_call["function"]["arguments"]) + except json.JSONDecodeError as e: + print(f"⚠️ 工具参数解析失败: {e}") # --- 新增日志 --- + args = {} + + tool_result = "" + + # 👇 动态修改那条 Telegram 消息! + if func_name == "web_search": + print(f"执行工具: web_search, 参数: {args}") # --- 新增日志 --- + bot.edit_message_text(f"🔍 正在检索: `{args.get('query', '')}`", chat_id=chat_id, message_id=status_msg.message_id, parse_mode="Markdown") + tool_result = execute_web_search(args.get("query", "")) + + elif func_name == "web_fetch": + url = args.get("url", "").strip() + + # 👇 新增:防止模型传一个空的网址过来浪费时间 + if not url: + print("🛑 拦截空网址抓取") + tool_result = "【系统警告】:你提供了一个空的 URL!请检查搜索结果,提取正确的 href 链接后再调用 web_fetch 工具。" + # 下面是原有的去重逻辑 + elif url in visited_urls: + print(f"🛑 拦截重复抓取,保护 Token: {url}") + bot.edit_message_text(f"🛑 拦截重复阅读,节省 Token...", chat_id=chat_id, message_id=status_msg.message_id) + tool_result = "【系统警告】:您已经抓取并阅读过该 URL 的内容,内容已在您的上下文记忆中,请勿重复调用 web_fetch 浪费 Token!请基于已有信息回答,或搜索新的线索。" + else: + visited_urls.add(url) + print(f"执行工具: web_fetch, 参数: {args}") + bot.edit_message_text(f"📄 正在深度阅读网页...", chat_id=chat_id, message_id=status_msg.message_id, parse_mode="Markdown") + tool_result = execute_web_fetch(url) + + elif func_name == "image_search": + print(f"执行工具: image_search, 参数: {args}") # --- 新增日志 --- + bot.edit_message_text(f"🖼️ 正在搜图: `{args.get('query', '')}`", chat_id=chat_id, message_id=status_msg.message_id, parse_mode="Markdown") + tool_result = execute_image_search(args.get("query", "")) + elif func_name == "manage_memory": + print(f"执行工具: manage_memory, 参数: {args}") # --- 新增日志 --- + bot.edit_message_text(f"🧠 正在整理记忆...", chat_id=chat_id, message_id=status_msg.message_id) + tool_result = execute_manage_memory(chat_id, args.get("action", ""), args.get("content", "")) + else: + print(f"⚠️ 未知工具: {func_name}") # --- 新增日志 --- + + history.append({ + "role": "tool", + "tool_call_id": tool_call["id"], + "name": func_name, + "content": tool_result + }) + + # 查完资料后,更新状态为“整理回答中” + bot.edit_message_text("✍️ 正在整理最终回答...", chat_id=chat_id, message_id=status_msg.message_id) + print("✍️ 工具执行完毕,准备进行下一轮循环") # --- 新增日志 --- + + print("❌ 思考循环达到上限") # --- 新增日志 --- + return "思考深度超过限制,未能得出最终结论。" + +def check_model_status(model_id): + """测试单个模型是否可用,返回布尔值 (True/False)""" + headers = { + "Authorization": f"Bearer {NVIDIA_API_KEY}", + "Content-Type": "application/json" + } + payload = { + "model": model_id, + "messages": [{"role": "user", "content": "hi"}], + "max_tokens": 1 # 只需要1个token来探测连通性 + } + try: + # 注意:这里把 timeout 设置为 8 秒。 + # 因为这是用户点菜单时的实时检测,等太久体验不好。8秒连不上就视为不可用。 + res = requests.post(NVIDIA_API_URL, headers=headers, json=payload, timeout=8) + return res.status_code == 200 + except Exception: + return False + +# ================= 5. 消息处理与入口 ================= +def check_auth(message): + if not ALLOWED_USERS or message.from_user.id not in ALLOWED_USERS: + print(f"⚠️ 拦截非法访问: {message.from_user.id}") + return False + return True + +@bot.message_handler(commands=['start', 'help']) +def send_welcome(message): + if not check_auth(message): return + bot.reply_to(message, "👋 **全功能 Agent 已上线**\n\n• 我能自主决定何时搜索、何时阅读网页\n• `/model` 切换引擎\n• `/reset` 清空记忆", parse_mode="Markdown") + +@bot.message_handler(commands=['model']) +def show_model_menu(message): + if not check_auth(message): return + + # 1. 先发一条提示消息,因为测速需要几秒钟 + status_msg = bot.reply_to(message, "⏳ 正在并发检测各节点的 API 连通性,请稍候...") + + # 2. 并发测试所有模型 + model_status = {} + with ThreadPoolExecutor(max_workers=len(MODEL_MAP)) as executor: + # 提交所有测试任务 + future_to_model = {executor.submit(check_model_status, v): k for k, v in MODEL_MAP.items()} + + # 收集测试结果 + for future in as_completed(future_to_model): + model_name = future_to_model[future] + try: + is_ok = future.result() + except Exception: + is_ok = False + model_status[model_name] = is_ok + + # 3. 动态构建带 ✅/❌ 状态的键盘 + markup = types.InlineKeyboardMarkup(row_width=1) + for name, model_id in MODEL_MAP.items(): + status_icon = "✅" if model_status.get(name) else "❌" + # 按钮文本带图标,但 callback_data 保持原样,这样不影响后续切换逻辑 + btn_text = f"{status_icon} {name}" + markup.add(types.InlineKeyboardButton(text=btn_text, callback_data=f"set_model_{name}")) + + # 4. 把刚才的“等待中”消息修改为真正的菜单 + bot.edit_message_text( + text="请选择思考引擎(✅ 可用 / ❌ 异常或超时):", + chat_id=message.chat.id, + message_id=status_msg.message_id, + reply_markup=markup + ) + +@bot.callback_query_handler(func=lambda call: call.data.startswith('set_model_')) +def handle_model_selection(call): + name = call.data.replace('set_model_', '') + user_selected_model[call.message.chat.id] = MODEL_MAP[name] + bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text=f"✅ 已切换至: {name}") + +@bot.message_handler(commands=['reset']) +def reset_memory(message): + if not check_auth(message): return + if message.chat.id in chat_memory: del chat_memory[message.chat.id] + bot.reply_to(message, "🧹 对话上下文及工具执行记录已清空。") + +@bot.message_handler(content_types=['text', 'photo']) +def handle_message(message): + if not check_auth(message): return + if message.text and message.text.startswith('/'): return + + chat_id = message.chat.id + user_text = message.text or message.caption or "请描述图片" + + # --- 新增日志 --- + print(f"\n[{datetime.datetime.now().strftime('%H:%M:%S')}] 收到用户 {chat_id} 的消息: {user_text[:20]}...") + + current_content = [{"type": "text", "text": user_text}] + if message.photo: + try: + file_info = bot.get_file(message.photo[-1].file_id) + img_b64 = base64.b64encode(bot.download_file(file_info.file_path)).decode('utf-8') + current_content.append({"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}) + print("📸 已接收并处理图片附件") # --- 新增日志 --- + except Exception as e: + print(f"❌ 图片处理失败: {e}") # --- 新增日志 --- + + user_message = {"role": "user", "content": current_content} + + # 👇 视觉感核心:先发送一条状态占位消息 + status_msg = bot.reply_to(message, "⏳ 思考中...") + print("⏳ 已发送状态占位消息") # --- 新增日志 --- + + try: + # 把占位消息的 ID 传给 agent + reply = chat_with_agent(chat_id, user_message, status_msg) + + # 最终生成完毕后,覆盖那条状态消息 + try: + bot.edit_message_text(reply, chat_id=chat_id, message_id=status_msg.message_id, parse_mode="Markdown") + except Exception: + bot.edit_message_text(reply, chat_id=chat_id, message_id=status_msg.message_id) + + print("✅ 最终回复已发送") # --- 新增日志 --- + + except Exception as e: + print(f"❌ 系统异常: {e}") # --- 新增日志 --- + bot.edit_message_text(f"❌ 系统异常: {e}", chat_id=chat_id, message_id=status_msg.message_id) + +if __name__ == "__main__": + setup_bot_commands() + print("🚀 Bot 开始运行...") + bot.infinity_polling() \ No newline at end of file