From 837191af1f57aa3e00066909953420d8700233ee Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 17:43:58 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20/od=E5=A4=9A=E7=BA=A7=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E3=80=81/sb=E4=B8=8B=E8=BD=BD=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E9=80=9A=E7=9F=A5=E7=BC=BA=E5=A4=B1=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E/s=E7=A3=81=E5=8A=9B=E7=9B=B4=E6=8E=A5=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(od): 路径浏览改用短ID注册表,彻底解决 callback_data 超过 64 字节导致多级目录报错的问题 - fix(od): 通知逻辑从跟踪 undone 改为跟踪 done,解决任务在两次轮 询间隙快速完成时漏通知的问题(适用于 /od 和 /sb 提交的任务) - feat(s): 搜索结果中的磁力/电驴条目新增 📥#N 下载按钮,点击后进 入与 /sb 相同的路径/工具确认流程,最终提交到 OpenList 离线下载 https://claude.ai/code/session_01HNCfZLiNXHZcchtifn6b5T --- module/offline_download/offline_download.py | 64 +++++++++---- module/search/search.py | 100 ++++++++++++++++---- 2 files changed, 130 insertions(+), 34 deletions(-) diff --git a/module/offline_download/offline_download.py b/module/offline_download/offline_download.py index 93d2042..9e6f15f 100644 --- a/module/offline_download/offline_download.py +++ b/module/offline_download/offline_download.py @@ -18,26 +18,50 @@ DOWNLOAD_CACHE = {} STEP_SELECTING_TOOL = "tool" +# ─── 路径 ID 注册(避免 callback_data 超过 64 字节限制)───────────────────────── +OD_PATH_COUNTER = 0 +OD_PATH_MAP: dict[str, str] = {} + + +def od_register_path(path: str) -> str: + global OD_PATH_COUNTER + OD_PATH_COUNTER += 1 + pid = str(OD_PATH_COUNTER) + OD_PATH_MAP[pid] = path + return pid + + +def od_get_path(pid: str) -> str: + return OD_PATH_MAP.get(pid, "") + + # ─── 下载完成通知 ────────────────────────────────────────────────────────────── -PREV_UNDONE_IDS: set[str] = set() +PREV_DONE_IDS: set[str] = set() +_COMPLETION_INITIALIZED = False async def check_download_completion(context) -> None: """定时检查离线下载任务,完成时主动推送通知(JobQueue 回调)""" - global PREV_UNDONE_IDS + global PREV_DONE_IDS, _COMPLETION_INITIALIZED try: - undone = await openlist.get_offline_download_undone_task() done = await openlist.get_offline_download_done_task() - current_undone_ids: set[str] = set() - if undone.data and isinstance(undone.data, list): - current_undone_ids = {str(t.get("id", "")) for t in undone.data if t.get("id")} - + current_done_ids: set[str] = set() done_map: dict[str, dict] = {} if done.data and isinstance(done.data, list): - done_map = {str(t.get("id", "")): t for t in done.data if t.get("id")} + for t in done.data: + if t.get("id"): + tid = str(t["id"]) + current_done_ids.add(tid) + done_map[tid] = t + + # 首次运行:记录已有完成任务,不触发通知 + if not _COMPLETION_INITIALIZED: + PREV_DONE_IDS = current_done_ids + _COMPLETION_INITIALIZED = True + return - newly_done = PREV_UNDONE_IDS & done_map.keys() + newly_done = current_done_ids - PREV_DONE_IDS for task_id in newly_done: task = done_map[task_id] name, path = extract_file_info(task.get("name", "")) @@ -52,7 +76,7 @@ async def check_download_completion(context) -> None: except Exception as e: logger.error(f"发送下载通知失败: {e}") - PREV_UNDONE_IDS = current_undone_ids + PREV_DONE_IDS = current_done_ids except Exception as e: logger.error(f"check_download_completion 失败: {e}") STEP_BROWSING_PATH = "browse_path" @@ -233,25 +257,27 @@ async def show_path_browser(query, cache, path): files.append({"name": name}) buttons = [] - + # 添加子目录 for d in dirs[:10]: name = d["name"] sub_path = f"{path.rstrip('/')}/{name}/" - buttons.append([InlineKeyboardButton(f"📁 {name}", callback_data=f"od_cd_{sub_path}")]) - + path_id = od_register_path(sub_path) + buttons.append([InlineKeyboardButton(f"📁 {name}", callback_data=f"od_cd_{path_id}")]) + # 添加文件(只显示前几个作为参考) for f in files[:5]: name = f["name"] buttons.append([InlineKeyboardButton(f"📄 {name}", callback_data=f"od_file_{name}")]) - + # 添加确认按钮 buttons.append([InlineKeyboardButton("✅ 确认路径", callback_data="od_confirm_path")]) - + # 添加返回按钮 if path != "/": parent_path = "/".join(path.rstrip("/").split("/")[:-1]) or "/" - buttons.append([InlineKeyboardButton("⬅️ 返回上一级", callback_data=f"od_cd_{parent_path}")]) + parent_id = od_register_path(parent_path) + buttons.append([InlineKeyboardButton("⬅️ 返回上一级", callback_data=f"od_cd_{parent_id}")]) cache["path"] = path @@ -268,7 +294,11 @@ async def show_path_browser(query, cache, path): async def handle_path_browse(query, cache, data): """处理路径浏览回调""" if data.startswith("od_cd_"): - path = data.replace("od_cd_", "") + path_id = data.replace("od_cd_", "") + path = od_get_path(path_id) + if not path: + await query.answer("路径已过期,请重新选择", show_alert=True) + return await show_path_browser(query, cache, path) return diff --git a/module/search/search.py b/module/search/search.py index 41bbd07..f5453ab 100644 --- a/module/search/search.py +++ b/module/search/search.py @@ -18,6 +18,9 @@ PAGE: dict[str, "Page"] = {} +# 结果缓存,用于下载按钮回调(key: "{cmid}_{global_index}") +S_RESULT_CACHE: dict[str, PanSouResult] = {} + PAN_TYPE_EMOJI = { "baidu": "🔵百度", "aliyun": "🟢阿里", @@ -58,10 +61,11 @@ def build_result_item_sync(count: int, item: PanSouResult) -> str: class Page: PER_PAGE = 5 - - def __init__(self, results: list[PanSouResult], keyword: str = ""): + + def __init__(self, results: list[PanSouResult], keyword: str = "", cmid: str = ""): self.all_results = results self.keyword = keyword + self.cmid = cmid self.filter_type: Optional[str] = None self.index = 0 self.per_page = self.PER_PAGE @@ -115,31 +119,49 @@ def previous_page(self) -> str: @property def btn(self) -> list: + buttons = [] + + # 为当前页的磁力/电驴结果添加下载按钮 + if self.cmid: + start = self.index * self.per_page + items = self.filtered_results[start:start + self.per_page] + dl_row = [] + for i, item in enumerate(items): + if item.pan_type in ("magnet", "ed2k") and item.url: + global_idx = self.all_results.index(item) + count = start + i + 1 + dl_row.append(InlineKeyboardButton( + f"📥#{count}", + callback_data=f"s_dl_{self.cmid}_{global_idx}" + )) + for j in range(0, len(dl_row), 3): + buttons.append(dl_row[j:j + 3]) + pan_types = list(set(r.pan_type for r in self.all_results)) pan_types.sort() - - filter_buttons = [] - row = [InlineKeyboardButton("全部", callback_data="search_filter_all")] + + filter_row = [InlineKeyboardButton("全部", callback_data="search_filter_all")] for pt in pan_types[:5]: emoji = PAN_TYPE_EMOJI.get(pt, "📦") - row.append(InlineKeyboardButton(emoji, callback_data=f"search_filter_{pt}")) - filter_buttons.append(row) - + filter_row.append(InlineKeyboardButton(emoji, callback_data=f"search_filter_{pt}")) + buttons.append(filter_row) + if len(pan_types) > 5: row = [] for pt in pan_types[5:10]: emoji = PAN_TYPE_EMOJI.get(pt, "📦") row.append(InlineKeyboardButton(emoji, callback_data=f"search_filter_{pt}")) if row: - filter_buttons.append(row) + buttons.append(row) nav_buttons = [ InlineKeyboardButton("⬆️上一页", callback_data="search_previous_page"), InlineKeyboardButton(f"{self.index + 1}/{self.page_count}", callback_data="search_pages"), InlineKeyboardButton("⬇️下一页", callback_data="search_next_page"), ] - - return filter_buttons + [nav_buttons] + buttons.append(nav_buttons) + + return buttons async def s_command(update: Update, context: ContextTypes.DEFAULT_TYPE): @@ -172,12 +194,18 @@ async def build_result(content: list[PanSouResult], msg, keyword: str): chat_id = msg.chat.id message_id = msg.message_id cmid = f"{chat_id}|{message_id}" - - page = Page(content, keyword) + + # 清理旧缓存,填充新结果 + for k in [k for k in S_RESULT_CACHE if k.startswith(f"{cmid}_")]: + del S_RESULT_CACHE[k] + for i, item in enumerate(content): + S_RESULT_CACHE[f"{cmid}_{i}"] = item + + page = Page(content, keyword, cmid) PAGE[cmid] = page - + text = page.get_text_with_info() - + return text, page.btn @@ -212,9 +240,47 @@ async def search_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): pan_type = data.replace("search_filter_", "") page.set_filter(pan_type) text = page.get_text_with_info() + elif data.startswith("s_dl_"): + # 解析 s_dl_{cmid}_{global_idx},cmid 含 "|" 故从右截取最后一段 + rest = data[5:] + last_sep = rest.rfind("_") + cmid_part = rest[:last_sep] + try: + global_idx = int(rest[last_sep + 1:]) + except ValueError: + return + + item = S_RESULT_CACHE.get(f"{cmid_part}_{global_idx}") + if not item or not item.url: + await query.answer("链接已过期,请重新搜索", show_alert=True) + return + + try: + from config.config import reload_od_cfg + cfg = reload_od_cfg() + dl_path = cfg.download_path if cfg and cfg.download_path else "/" + dl_tool = cfg.download_tool if cfg and cfg.download_tool else "qbittorrent" + except Exception: + dl_path = "/" + dl_tool = "qbittorrent" + + file_name = item.name[:30] + "..." if len(item.name) > 30 else item.name + from module.torrent_search.torrent_search import DOWNLOAD_INFO, build_download_confirm_message + DOWNLOAD_INFO[msg.chat.id] = { + "result": item, + "index": global_idx, + "cmid": cmid_part, + "path": dl_path, + "tool": dl_tool, + "url": item.url, + "file_name": file_name, + } + text_confirm, keyboard = await build_download_confirm_message(msg.chat.id, DOWNLOAD_INFO[msg.chat.id]) + await msg.edit_text(text_confirm, reply_markup=keyboard, parse_mode="Markdown") + return else: return - + try: await msg.edit_text(text, reply_markup=InlineKeyboardMarkup(page.btn), disable_web_page_preview=True) except Exception: @@ -223,4 +289,4 @@ async def search_callback(update: Update, context: ContextTypes.DEFAULT_TYPE): def register_handlers(app: Application): app.add_handler(CommandHandler("s", s_command)) - app.add_handler(CallbackQueryHandler(search_callback, pattern=r"^search")) + app.add_handler(CallbackQueryHandler(search_callback, pattern=r"^(search|s_dl_)"))