From a5327b5a87f7a6df807a2d5222894a84ecf7efd8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 15:51:21 +0000 Subject: [PATCH] feat(st): add delete, mkdir, and upload to storage browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 🗑️ delete button next to every file/folder in the listing - Delete shows a confirmation dialog before proceeding - Add 📁+ 新建文件夹 button to create directories in the current path - Add 📤 上传文件 button to upload a Telegram document to the current path - File list auto-refreshes after delete/mkdir/upload operations - Add fs_remove, fs_mkdir, fs_put_bytes API methods to OpenListAPI https://claude.ai/code/session_01HNCfZLiNXHZcchtifn6b5T --- api/openlist/openlist_api.py | 23 ++ module/storage_browse/storage_browse.py | 469 +++++++++++++++++------- 2 files changed, 360 insertions(+), 132 deletions(-) diff --git a/api/openlist/openlist_api.py b/api/openlist/openlist_api.py index c4a6e8f..6d0f15c 100644 --- a/api/openlist/openlist_api.py +++ b/api/openlist/openlist_api.py @@ -264,6 +264,29 @@ async def clear_offline_download_done_task(self): url = "/api/admin/task/offline_download/clear_done" return await self._request("POST", url) + async def fs_remove(self, dir_path: str, names: list[str]) -> OpenListAPIResponse: + """删除文件/文件夹""" + await self._ensure_token() + body = {"dir": dir_path, "names": names} + return await self._request("POST", "/api/fs/remove", json=body) + + async def fs_mkdir(self, path: str) -> OpenListAPIResponse: + """创建文件夹""" + await self._ensure_token() + body = {"path": path} + return await self._request("POST", "/api/fs/mkdir", json=body) + + async def fs_put_bytes(self, file_data: bytes, remote_path: str, file_name: str) -> OpenListAPIResponse: + """上传文件(bytes数据)""" + await self._ensure_token() + headers = { + "As-Task": "false", + "Authorization": self._get_auth_token(), + "File-Path": parse.quote(f"{remote_path.rstrip('/')}/{file_name}"), + "Content-Length": str(len(file_data)), + } + return await self._request("PUT", "/api/fs/put", headers=headers, data=file_data, timeout=120) + @staticmethod def sign(path) -> str: """计算签名""" diff --git a/module/storage_browse/storage_browse.py b/module/storage_browse/storage_browse.py index d7f824f..06f7b57 100644 --- a/module/storage_browse/storage_browse.py +++ b/module/storage_browse/storage_browse.py @@ -1,6 +1,7 @@ # -*- coding: UTF-8 -*- import asyncio import math +import time import urllib.parse from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ( @@ -8,6 +9,8 @@ CommandHandler, CallbackQueryHandler, ContextTypes, + MessageHandler, + filters, ) from api.openlist.openlist_api import openlist @@ -21,6 +24,10 @@ PATH_COUNTER = 0 PATH_MAP = {} +STATE_NONE = 0 +STATE_AWAITING_MKDIR = 1 +STATE_AWAITING_UPLOAD = 2 + async def send_typing(context: ContextTypes.DEFAULT_TYPE, chat_id: int): """发送 typing 状态""" @@ -48,8 +55,7 @@ async def st_command(update: Update, context: ContextTypes.DEFAULT_TYPE, message """存储浏览""" if not await check_is_admin(update): return - - # 确定使用哪个消息对象发送回复 + send_message = None if message: send_message = message.reply_text @@ -57,16 +63,15 @@ async def st_command(update: Update, context: ContextTypes.DEFAULT_TYPE, message send_message = update.message.reply_text elif update.callback_query and update.callback_query.message: send_message = update.callback_query.message.reply_text - + if not send_message: logger.error("无法获取发送消息的方法") return - + try: await openlist.login() result = await openlist.storage_list() - - # 处理返回数据 - data 可能是 StorageInfo 对象列表 + storages = [] if result.data: if isinstance(result.data, list): @@ -74,33 +79,28 @@ async def st_command(update: Update, context: ContextTypes.DEFAULT_TYPE, message elif isinstance(result.data, dict): content = result.data.get("content", []) storages = content if isinstance(content, list) else [] - + if not storages: await send_message("暂无存储") return except Exception as e: - import traceback logger.error(f"获取存储列表失败: {e}") await send_message(f"获取存储列表失败: {e}") return - - # 构建存储列表按钮 + buttons = [] for storage in storages: name = storage.remark or storage.mount_path or str(storage.id) - # disabled is True means it's disabled status = "❎" if storage.disabled else "✅" - # 回调数据包含 id 和 mount_path mount_path = storage.mount_path or "/" btn = InlineKeyboardButton( f"{status}{name}", callback_data=f"storage_{storage.id}:{mount_path}" ) buttons.append([btn]) - - # 添加取消按钮 + buttons.append([InlineKeyboardButton("❌ 取消", callback_data="st_cancel")]) - + await send_message( "选择存储:", reply_markup=InlineKeyboardMarkup(buttons) @@ -111,29 +111,22 @@ async def storage_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): """处理存储选择""" query = update.callback_query await query.answer() - - # 发送 typing 状态 + chat_id = query.message.chat.id asyncio.create_task(send_typing(context, chat_id)) - - # 解析存储 ID 和 mount_path + data = query.data.replace("storage_", "") parts = data.split(":", 1) storage_id = parts[0] mount_path = parts[1] if len(parts) > 1 else "/" - + logger.info(f"storage_callback: 选择存储 id={storage_id}, mount_path={mount_path}") - - # 获取该存储的根目录文件 + try: asyncio.create_task(send_typing(context, chat_id)) result = await openlist.fs_list(mount_path) logger.info(f"fs_list 返回: code={result.code}, path={mount_path}") - if result.data: - content = result.data.get("content", []) if isinstance(result.data, dict) else [] - logger.info(f"内容数量: {len(content) if content else 0}") - - # 处理返回数据 + content = [] if hasattr(result, 'data') and isinstance(result.data, dict): content = result.data.get("content", []) @@ -142,21 +135,15 @@ async def storage_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): except Exception as e: await query.message.reply_text(f"获取文件列表失败: {e}") return - - if not content: - await query.message.reply_text("该存储为空") - return - - # 保存到缓存 - chat_id = query.message.chat.id + FILE_CACHE[f"{chat_id}_storage"] = storage_id FILE_CACHE[f"{chat_id}_root_path"] = mount_path FILE_CACHE[f"{chat_id}_path"] = mount_path FILE_CACHE[f"{chat_id}_files"] = content - - # 构建文件列表 - text, buttons = await build_file_list(content, mount_path, query.message.message_id) - + + msg_id = query.message.message_id + text, buttons = await build_file_list(content, mount_path, msg_id, chat_id) + await query.message.edit_text( text=text, reply_markup=InlineKeyboardMarkup(buttons) @@ -167,16 +154,14 @@ async def file_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): """处理文件/目录点击""" query = update.callback_query await query.answer() - + data = query.data chat_id = query.message.chat.id msg_id = query.message.message_id - - # 发送 typing 状态 + asyncio.create_task(send_typing(context, chat_id)) - + if data.startswith("file_"): - # 点击文件,获取路径ID并查找实际路径 path_id = data.replace("file_", "") file_path = get_path(path_id) if not file_path: @@ -185,9 +170,8 @@ async def file_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): asyncio.create_task(send_typing(context, chat_id)) await copy_file_link(query, file_path) return - + if data.startswith("cd_"): - # 目录导航 - 获取实际路径 path_id = data.replace("cd_", "") path = get_path(path_id) if not path: @@ -197,41 +181,36 @@ async def file_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): asyncio.create_task(send_typing(context, chat_id)) await navigate_to_path(query, chat_id, msg_id, path, context) return - + if data.startswith("page_"): - # 分页导航 page = int(data.replace("page_", "")) current_path = FILE_CACHE.get(f"{chat_id}_path", "/") content = FILE_CACHE.get(f"{chat_id}_files", []) asyncio.create_task(send_typing(context, chat_id)) - text, buttons = await build_file_list(content, current_path, msg_id, page=page) + text, buttons = await build_file_list(content, current_path, msg_id, chat_id, page=page) await query.message.edit_text(text=text, reply_markup=InlineKeyboardMarkup(buttons)) return - + if data == "back": logger.info(f"file_callback: back 点击") asyncio.create_task(send_typing(context, chat_id)) - # 返回上一层 - chat_id = query.message.chat.id current_path = FILE_CACHE.get(f"{chat_id}_path", "/") root_path = FILE_CACHE.get(f"{chat_id}_root_path", "/") - + logger.info(f"back: current_path={current_path}, root_path={root_path}") - + if current_path == root_path: - # 已经是根目录,返回存储列表 await st_command(update, context, message=query.message) return - - # 计算上级目录 + parent_path = "/".join(current_path.rstrip("/").split("/")[:-1]) logger.info(f"back: 计算的 parent_path={parent_path}") - + if not parent_path.startswith(root_path.rstrip("/")): parent_path = root_path if not parent_path: parent_path = root_path - + logger.info(f"back: 最终 parent_path={parent_path}") await navigate_to_path(query, chat_id, msg_id, parent_path, context) @@ -240,28 +219,25 @@ async def copy_file_link(query, file_path: str): """复制文件链接""" try: result = await openlist.fs_get(file_path) - - # 处理返回数据 + raw_url = "" if hasattr(result, 'data') and isinstance(result.data, dict): raw_url = result.data.get("raw_url", "") elif hasattr(result, 'data') and isinstance(result.data, list) and result.data: raw_url = getattr(result.data[0], 'raw_url', '') - - # 构建完整URL + file_name = file_path.split("/")[-1] - - # 确保 URL 正确拼接 + web_url = bot_cfg.openlist_web.rstrip("/") if not file_path.startswith("/"): file_path = "/" + file_path full_url = f"{web_url}{file_path}" - + if raw_url: text = f"文件: `{file_name}`\n\n直链: {raw_url}\n\n打开链接: {full_url}" else: text = f"文件: `{file_name}`\n\n打开链接: {full_url}" - + await query.message.reply_text(text, disable_web_page_preview=True) except Exception as e: await query.message.reply_text(f"获取链接失败: {e}") @@ -270,16 +246,14 @@ async def copy_file_link(query, file_path: str): async def navigate_to_path(query, chat_id: int, msg_id: int, path: str, context: ContextTypes.DEFAULT_TYPE = None): """导航到指定路径""" logger.info(f"navigate_to_path: 进入 path={path}") - - # 发送 typing 状态 + if context: asyncio.create_task(send_typing(context, chat_id)) - + try: result = await openlist.fs_list(path) logger.info(f"fs_list 返回: code={result.code}, message={result.message}") - - # 处理返回数据 + content = [] if hasattr(result, 'data') and isinstance(result.data, dict): content = result.data.get("content", []) @@ -288,41 +262,41 @@ async def navigate_to_path(query, chat_id: int, msg_id: int, path: str, context: except Exception as e: await query.message.reply_text(f"获取文件列表失败: {e}") return - + + FILE_CACHE[f"{chat_id}_path"] = path + FILE_CACHE[f"{chat_id}_files"] = content + FILE_CACHE[f"{chat_id}_browse_msg_id"] = msg_id + if not content: - # 空目录也显示返回按钮 text = f"📁 `{path}`\n\n(空目录)" path_parts = [p for p in path.strip("/").split("/") if p] - buttons = [] - if len(path_parts) > 0: - buttons.append([ - InlineKeyboardButton("⬅️ 返回上一级", callback_data="back") - ]) - await query.message.edit_text(text=text, reply_markup=InlineKeyboardMarkup(buttons)) + btns = [] + if path_parts: + btns.append([InlineKeyboardButton("⬅️ 返回上一级", callback_data="back")]) + btns.append([ + InlineKeyboardButton("📁+ 新建文件夹", callback_data="st_mkdir"), + InlineKeyboardButton("📤 上传文件", callback_data="st_upload"), + ]) + await query.message.edit_text(text=text, reply_markup=InlineKeyboardMarkup(btns)) return - - # 更新缓存 - FILE_CACHE[f"{chat_id}_path"] = path - FILE_CACHE[f"{chat_id}_files"] = content - - text, buttons = await build_file_list(content, path, msg_id) - + + text, buttons = await build_file_list(content, path, msg_id, chat_id) + await query.message.edit_text( text=text, reply_markup=InlineKeyboardMarkup(buttons) ) -async def build_file_list(content, current_path: str, msg_id: int, page: int = 1, per_page: int = 10): +async def build_file_list(content, current_path: str, msg_id: int, chat_id: int = None, page: int = 1, per_page: int = 10): """构建文件列表消息(分页)""" - buttons = [] - - # 分类文件和文件夹 + if chat_id: + FILE_CACHE[f"{chat_id}_browse_msg_id"] = msg_id + dirs = [] files = [] - + for item in content: - # item can be a dict or an object if isinstance(item, dict): is_dir = item.get("is_dir", False) name = item.get("name", "") @@ -331,55 +305,43 @@ async def build_file_list(content, current_path: str, msg_id: int, page: int = 1 is_dir = getattr(item, "is_dir", False) name = getattr(item, "name", "") size = getattr(item, "size", 0) - + if is_dir: dirs.append({"name": name}) else: files.append({"name": name, "size": size}) - - # 计算总数和页数 + total_items = len(dirs) + len(files) total_pages = max(1, (total_items + per_page - 1) // per_page) page = max(1, min(page, total_pages)) - - # 获取当前页的项目 + start_idx = (page - 1) * per_page end_idx = start_idx + per_page all_items = dirs + files page_items = all_items[start_idx:end_idx] - - # 构建文本 + text = f"📁 `{current_path}`\n" text += f"第 {page}/{total_pages} 页,共 {total_items} 项\n\n" - - # 添加目录 - item_idx = 0 + + buttons = [] for item in page_items: name = item["name"] + base = current_path.rstrip("/") if "size" in item: - # 文件 - size = format_size(item["size"]) - if current_path.endswith("/"): - file_path = f"{current_path}{name}" - else: - file_path = f"{current_path}/{name}" + file_path = f"{base}/{name}" path_id = register_path(file_path) buttons.append([ - InlineKeyboardButton(f"📄 {name} ({size})", callback_data=f"file_{path_id}") + InlineKeyboardButton(f"📄 {name} ({format_size(item['size'])})", callback_data=f"file_{path_id}"), + InlineKeyboardButton("🗑️", callback_data=f"del_{path_id}"), ]) else: - # 目录 - if current_path.endswith("/"): - btn_path = f"{current_path}{name}/" - else: - btn_path = f"{current_path}/{name}/" - path_id = register_path(btn_path) + dir_path = f"{base}/{name}" + path_id = register_path(dir_path) buttons.append([ - InlineKeyboardButton(f"📁 {name}", callback_data=f"cd_{path_id}") + InlineKeyboardButton(f"📁 {name}", callback_data=f"cd_{path_id}"), + InlineKeyboardButton("🗑️", callback_data=f"del_{path_id}"), ]) - item_idx += 1 - - # 添加分页按钮 + page_buttons = [] if page > 1: page_buttons.append(InlineKeyboardButton("⬅️ 上一页", callback_data=f"page_{page-1}")) @@ -387,14 +349,16 @@ async def build_file_list(content, current_path: str, msg_id: int, page: int = 1 page_buttons.append(InlineKeyboardButton("➡️ 下一页", callback_data=f"page_{page+1}")) if page_buttons: buttons.append(page_buttons) - - # 添加返回按钮 - 始终显示(只要不是根目录) + + buttons.append([ + InlineKeyboardButton("📁+ 新建文件夹", callback_data="st_mkdir"), + InlineKeyboardButton("📤 上传文件", callback_data="st_upload"), + ]) + path_parts = [p for p in current_path.strip("/").split("/") if p] - if len(path_parts) > 0: - buttons.append([ - InlineKeyboardButton("⬅️ 返回上一级", callback_data="back") - ]) - + if path_parts: + buttons.append([InlineKeyboardButton("⬅️ 返回上一级", callback_data="back")]) + return text, buttons @@ -404,7 +368,7 @@ def format_size(size): size = float(size) except (ValueError, TypeError): return "0B" - + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: if size < 1024: return f"{size:.1f}{unit}" @@ -412,15 +376,256 @@ def format_size(size): return f"{size:.1f}PB" -def register_handlers(app: Application): - app.add_handler(CommandHandler("st", st_command)) - app.add_handler(CallbackQueryHandler(storage_callback, pattern=r"^storage_")) - app.add_handler(CallbackQueryHandler(file_callback, pattern=r"^(file_|cd_|back|page_)")) - app.add_handler(CallbackQueryHandler(st_cancel_callback, pattern=r"^st_cancel$")) +async def refresh_browse_message(context: ContextTypes.DEFAULT_TYPE, chat_id: int, msg_id: int, path: str): + """刷新浏览消息""" + try: + result = await openlist.fs_list(path) + content = [] + if hasattr(result, 'data') and isinstance(result.data, dict): + content = result.data.get("content", []) + elif hasattr(result, 'data') and isinstance(result.data, list): + content = result.data + + FILE_CACHE[f"{chat_id}_files"] = content or [] + + if not content: + text = f"📁 `{path}`\n\n(空目录)" + path_parts = [p for p in path.strip("/").split("/") if p] + btns = [] + if path_parts: + btns.append([InlineKeyboardButton("⬅️ 返回上一级", callback_data="back")]) + btns.append([ + InlineKeyboardButton("📁+ 新建文件夹", callback_data="st_mkdir"), + InlineKeyboardButton("📤 上传文件", callback_data="st_upload"), + ]) + await context.bot.edit_message_text( + chat_id=chat_id, message_id=msg_id, text=text, + reply_markup=InlineKeyboardMarkup(btns), parse_mode="Markdown", + ) + return + + text, btns = await build_file_list(content, path, msg_id, chat_id) + await context.bot.edit_message_text( + chat_id=chat_id, message_id=msg_id, text=text, + reply_markup=InlineKeyboardMarkup(btns), parse_mode="Markdown", + ) + except Exception as e: + logger.error(f"refresh_browse_message 失败: {e}") + + +# ─── Delete ─────────────────────────────────────────────────────────────────── + +async def del_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + """显示删除确认""" + query = update.callback_query + await query.answer() + chat_id = query.message.chat.id + + path_id = query.data[4:] # strip "del_" + full_path = get_path(path_id) + if not full_path: + await query.answer("路径已过期,请重新进入", show_alert=True) + return + + full_path = full_path.rstrip("/") + item_name = full_path.split("/")[-1] + FILE_CACHE[f"{chat_id}_pending_delete_path"] = full_path + + btns = [[ + InlineKeyboardButton("✅ 确认删除", callback_data="del_confirm"), + InlineKeyboardButton("❌ 取消", callback_data="del_cancel_msg"), + ]] + await query.message.reply_text( + f"确认删除 `{item_name}`?\n\n此操作不可撤销!", + reply_markup=InlineKeyboardMarkup(btns), + parse_mode="Markdown", + ) + + +async def del_confirm_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + """执行删除""" + query = update.callback_query + await query.answer() + chat_id = query.message.chat.id + + full_path = FILE_CACHE.pop(f"{chat_id}_pending_delete_path", None) + if not full_path: + await query.message.edit_text("❌ 删除失败:找不到目标路径") + return + + full_path = full_path.rstrip("/") + dir_path = "/".join(full_path.split("/")[:-1]) or "/" + item_name = full_path.split("/")[-1] + + try: + result = await openlist.fs_remove(dir_path, [item_name]) + if result.code == 200: + await query.message.edit_text(f"✅ 已删除 `{item_name}`", parse_mode="Markdown") + else: + await query.message.edit_text(f"❌ 删除失败: {result.message}") + return + except Exception as e: + await query.message.edit_text(f"❌ 删除失败: {e}") + return + + current_path = FILE_CACHE.get(f"{chat_id}_path", "/") + browse_msg_id = FILE_CACHE.get(f"{chat_id}_browse_msg_id") + if browse_msg_id: + await refresh_browse_message(context, chat_id, browse_msg_id, current_path) + + +async def del_cancel_msg_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + """取消删除""" + query = update.callback_query + await query.answer() + chat_id = query.message.chat.id + FILE_CACHE.pop(f"{chat_id}_pending_delete_path", None) + await query.message.edit_text("❌ 已取消") + + +# ─── Mkdir ──────────────────────────────────────────────────────────────────── + +async def st_mkdir_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + """触发新建文件夹""" + query = update.callback_query + await query.answer() + chat_id = query.message.chat.id + + FILE_CACHE[f"{chat_id}_state"] = STATE_AWAITING_MKDIR + btns = [[InlineKeyboardButton("❌ 取消", callback_data="st_op_cancel")]] + prompt = await query.message.reply_text( + "请输入新文件夹名称:", + reply_markup=InlineKeyboardMarkup(btns), + ) + FILE_CACHE[f"{chat_id}_prompt_msg_id"] = prompt.message_id + + +async def handle_text_input(update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理文本输入(新建文件夹)""" + chat_id = update.message.chat.id + if FILE_CACHE.get(f"{chat_id}_state") != STATE_AWAITING_MKDIR: + return + + folder_name = update.message.text.strip() + if not folder_name: + return + + FILE_CACHE[f"{chat_id}_state"] = STATE_NONE + current_path = FILE_CACHE.get(f"{chat_id}_path", "/") + new_path = f"{current_path.rstrip('/')}/{folder_name}" + + prompt_msg_id = FILE_CACHE.pop(f"{chat_id}_prompt_msg_id", None) + if prompt_msg_id: + try: + await context.bot.delete_message(chat_id=chat_id, message_id=prompt_msg_id) + except Exception: + pass + + try: + await update.message.delete() + except Exception: + pass + + try: + result = await openlist.fs_mkdir(new_path) + if result.code == 200: + browse_msg_id = FILE_CACHE.get(f"{chat_id}_browse_msg_id") + if browse_msg_id: + await refresh_browse_message(context, chat_id, browse_msg_id, current_path) + else: + await context.bot.send_message(chat_id=chat_id, text=f"❌ 创建文件夹失败: {result.message}") + except Exception as e: + await context.bot.send_message(chat_id=chat_id, text=f"❌ 创建文件夹失败: {e}") + + +# ─── Upload ─────────────────────────────────────────────────────────────────── + +async def st_upload_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + """触发文件上传""" + query = update.callback_query + await query.answer() + chat_id = query.message.chat.id + + FILE_CACHE[f"{chat_id}_state"] = STATE_AWAITING_UPLOAD + btns = [[InlineKeyboardButton("❌ 取消", callback_data="st_op_cancel")]] + prompt = await query.message.reply_text( + "请发送要上传的文件:", + reply_markup=InlineKeyboardMarkup(btns), + ) + FILE_CACHE[f"{chat_id}_prompt_msg_id"] = prompt.message_id + + +async def handle_document_input(update: Update, context: ContextTypes.DEFAULT_TYPE): + """处理文件上传""" + chat_id = update.message.chat.id + if FILE_CACHE.get(f"{chat_id}_state") != STATE_AWAITING_UPLOAD: + return + + doc = update.message.document + if not doc: + return + + file_name = doc.file_name or f"upload_{int(time.time())}" + current_path = FILE_CACHE.get(f"{chat_id}_path", "/") + + FILE_CACHE[f"{chat_id}_state"] = STATE_NONE + + prompt_msg_id = FILE_CACHE.pop(f"{chat_id}_prompt_msg_id", None) + if prompt_msg_id: + try: + await context.bot.delete_message(chat_id=chat_id, message_id=prompt_msg_id) + except Exception: + pass + prog_msg = await update.message.reply_text(f"⏳ 正在上传 `{file_name}`...", parse_mode="Markdown") + + try: + tg_file = await doc.get_file() + file_data = await tg_file.download_as_bytearray() + result = await openlist.fs_put_bytes(bytes(file_data), current_path, file_name) + if result.code == 200: + await prog_msg.edit_text(f"✅ `{file_name}` 上传成功", parse_mode="Markdown") + else: + await prog_msg.edit_text(f"❌ 上传失败: {result.message}") + return + except Exception as e: + await prog_msg.edit_text(f"❌ 上传失败: {e}") + return + + browse_msg_id = FILE_CACHE.get(f"{chat_id}_browse_msg_id") + if browse_msg_id: + await refresh_browse_message(context, chat_id, browse_msg_id, current_path) + + +async def st_op_cancel_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): + """取消待处理的操作""" + query = update.callback_query + await query.answer() + chat_id = query.message.chat.id + FILE_CACHE[f"{chat_id}_state"] = STATE_NONE + FILE_CACHE.pop(f"{chat_id}_prompt_msg_id", None) + await query.message.edit_text("❌ 已取消") + + +# ─── Misc ───────────────────────────────────────────────────────────────────── async def st_cancel_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): """取消存储浏览""" query = update.callback_query await query.answer() await query.message.edit_text("❌ 已取消") + + +def register_handlers(app: Application): + app.add_handler(CommandHandler("st", st_command)) + app.add_handler(CallbackQueryHandler(storage_callback, pattern=r"^storage_")) + app.add_handler(CallbackQueryHandler(file_callback, pattern=r"^(file_|cd_|back|page_)")) + app.add_handler(CallbackQueryHandler(st_cancel_callback, pattern=r"^st_cancel$")) + app.add_handler(CallbackQueryHandler(del_callback, pattern=r"^del_\d+$")) + app.add_handler(CallbackQueryHandler(del_confirm_callback, pattern=r"^del_confirm$")) + app.add_handler(CallbackQueryHandler(del_cancel_msg_callback, pattern=r"^del_cancel_msg$")) + app.add_handler(CallbackQueryHandler(st_mkdir_callback, pattern=r"^st_mkdir$")) + app.add_handler(CallbackQueryHandler(st_upload_callback, pattern=r"^st_upload$")) + app.add_handler(CallbackQueryHandler(st_op_cancel_callback, pattern=r"^st_op_cancel$")) + app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text_input), group=2) + app.add_handler(MessageHandler(filters.Document.ALL, handle_document_input), group=2)