Termux之双上传autotg.py


双下载双上传

一、机器人指令菜单

1.@BotFather

2.在对话框里发送指令:/mybots

3.屏幕上会弹出你创建过的机器人列表,点击咱们正在用的这个机器人的名字

4.接着点击弹出面板上的 Edit Bot

5.然后点击 Edit Commands

6.把这套菜单直接喂给它

sub - ➕ 自动订阅 (可直接回复视频抓取频道)
unsub - 🗑️ 取消订阅 (全网通杀或精准单杀)
go - 🚀 手动发车 (开辟航线并锁死文件夹)
mag - 🧲[磁力链接]
reup - 🚀 [剧名关键字] [分类] [可选:年份] [c139|only139]
list - 📡 查看雷达大盘排班与状态
scan - 🔍 强行触发一次全网补漏扫荡
history - 📋 查看曾经发过车的历史航线记录
rm - ❌ 删除历史航线! 把不再需要的死档从history 里永久抹除
rmh - ❎ 带剧名删除下载历史记录
clh - ❎ 清除没订阅的下载历史记录
clean - ⭕️ 清除下载碎片
cancel - ⭕️ 终止下载 (all/剧名(+e01/s01)/.mp4)
setdir - ❇️ 设置上传目录
ping - 💓 查看系统是否活着、存活时间、雷达正在盯防多少部剧
help - 📖 查看随身说明书与指令语法

二、脚本

import os
import re
import time
import json
import asyncio
from datetime import datetime
from urllib.parse import quote
import httpx

# 🔥 必须放在导入 pyrogram 之前!修复 Python 3.12+ 兼容性问题
try:
    asyncio.get_running_loop()
except RuntimeError:
    asyncio.set_event_loop(asyncio.new_event_loop())

from pyrogram import Client, filters, enums, idle
import logging
import shutil

# =================================================================
# ⚙️ 核心网关、路径与凭证配置区域
# =================================================================
API_ID = 33349348              
API_HASH = "44bde7f01d2b6001589c28cea93716af"        

COMMAND_CENTER_CHAT = "@xxskyemby_bot"

# --- 189 天翼云配置 (端口 5244) ---
OLIST_URL = "http://127.0.0.1:5244"
OLIST_TOKEN = "openlist-a87614da-32dd-4b80-9150-6447de823da8f33x53ymkrx0aPKG0HUcsFHmjFRYTKFhSADLRhoQLkXa7ogaiByhWRNEXCjpblp9" 

STEWARD_BASE_URL = "http://127.0.0.1:5000" 

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
TG_ROUTE_DB = os.path.join(BASE_DIR, "tg_manual_routes.json")
LOCAL_TEMP_DIR = os.path.join(BASE_DIR, "tg_temp")
TG_LISTENER_DB = os.path.join(BASE_DIR, "tg_listener_config.json") 
TG_HISTORY_DB = os.path.join(BASE_DIR, "tg_download_history.json") 
TG_MAGNET_DB = os.path.join(BASE_DIR, "tg_magnet_tasks.json")
os.makedirs(LOCAL_TEMP_DIR, exist_ok=True)

TMDB_API_KEY = "9c88e18e43543c8ff195c631aaa0d2fa" 
START_TIME = time.time()

TG_SETTINGS_DB = os.path.join(BASE_DIR, "tg_settings.json")

# =================================================================
# 🧲 Aria2 磁力引擎配置
# =================================================================
ARIA2_RPC_URL = "http://127.0.0.1:6800/jsonrpc"
ARIA2_RPC_SECRET = "xxsky1127"
ARIA2_DOWNLOAD_DIR = "/data/data/com.termux/files/home/downloads"  

# ----------------------- 你的代理 -----------------------
TG_PROXY = {
    "scheme": "http",
    "hostname": "127.0.0.1",
    "port": 7890
}

def get_mount_root():
    if os.path.exists(TG_SETTINGS_DB):
        try:
            with open(TG_SETTINGS_DB, "r", encoding="utf-8") as f: 
                return json.load(f).get("mount_root", "/family/177_cas")
        except: pass
    return "/family/177_cas"
    
def set_mount_root(path):
    settings = {}
    if os.path.exists(TG_SETTINGS_DB):
        try:
            with open(TG_SETTINGS_DB, "r", encoding="utf-8") as f: 
                settings = json.load(f)
        except: pass
    settings["mount_root"] = path
    with open(TG_SETTINGS_DB, "w", encoding="utf-8") as f: 
        json.dump(settings, f, ensure_ascii=False, indent=2)

# =================================================================
# 🤫 日志与全局状态
# =================================================================
logging.basicConfig(level=logging.INFO, format='[%(asctime)s] %(message)s', datefmt='%H:%M:%S')
logging.getLogger("pyrogram").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logger = logging.getLogger("TGEngine")

app = Client(
    "tg_robust_leecher_v15", 
    api_id=API_ID, 
    api_hash=API_HASH, 
    proxy=TG_PROXY
)

# 🚦 全局大盘与锁
GLOBAL_ROUTE_CACHE = {"folder_name": "", "category": "", "folder_season": 1, "file_season": 1, "year": "", "version": "", "expire_time": 0, "manual_ep": None, "enable_139": False}
GLOBAL_ACTIVE_LOCKS = set() 
GLOBAL_CANCEL_TASKS = set() 
GLOBAL_TRACKED_GIDS = set()  
ORPHAN_PROMPTED_GIDS = set() 

# 🔥 核心更新:完全独立的双轨锁 (互不阻塞)
GLOBAL_TRANSFER_LOCK = asyncio.Semaphore(1)   # TG 下载专属锁
GLOBAL_UPLOAD_LOCK = asyncio.Semaphore(1)     # 189 高速上传锁
MAGNET_UPLOAD_LOCK = asyncio.Semaphore(1)     # 189 磁力上传锁
GLOBAL_139_UPLOAD_LOCK = asyncio.Semaphore(1) # 139 限速上传独立锁 (TG与磁力共用排队)

PENDING_MAGNETS = {} 
GLOBAL_STOP_SWEEP = False

async def notify_steward_log(msg, level="INFO"):
    logger.info(f"[{level}] {msg}")
    try:
        async with httpx.AsyncClient(timeout=2.0) as client:
            await client.post(f"{STEWARD_BASE_URL}/api/remote_log", json={"level": level, "msg": msg})
    except Exception: pass

def load_magnet_tasks():
    if os.path.exists(TG_MAGNET_DB):
        try:
            with open(TG_MAGNET_DB, 'r', encoding='utf-8') as f: return json.load(f)
        except Exception: pass
    return {}

def save_magnet_tasks(data):
    try:
        with open(TG_MAGNET_DB, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2)
    except Exception: pass

def add_magnet_task(gid, magnet_link, config_text):
    tasks = load_magnet_tasks()
    tasks[gid] = {"magnet_link": magnet_link, "config_text": config_text, "added_at": int(time.time())}
    save_magnet_tasks(tasks)

def remove_magnet_task(gid):
    tasks = load_magnet_tasks()
    if gid in tasks:
        del tasks[gid]
        save_magnet_tasks(tasks)

def clean_orphan_temp_files(max_age_hours=24):
    if os.path.exists(LOCAL_TEMP_DIR):
        now = time.time()
        for f in os.listdir(LOCAL_TEMP_DIR):
            file_path = os.path.join(LOCAL_TEMP_DIR, f)
            try:
                if os.path.isfile(file_path) and (now - os.path.getmtime(file_path)) > (max_age_hours * 3600):
                    os.remove(file_path)
            except Exception: pass

def load_listener_config():
    if not os.path.exists(TG_LISTENER_DB):
        dummy = {"trusted_channels": {}}
        with open(TG_LISTENER_DB, "w", encoding="utf-8") as f: json.dump(dummy, f, ensure_ascii=False, indent=4)
        return dummy
    try:
        with open(TG_LISTENER_DB, "r", encoding="utf-8") as f: return json.load(f)
    except Exception: return {"trusted_channels": {}}

def load_history():
    if os.path.exists(TG_HISTORY_DB):
        try:
            with open(TG_HISTORY_DB, 'r', encoding='utf-8') as f: 
                return json.load(f) 
        except Exception: pass
    return {}

def check_history(drama, file_season, ep, version=""):
    history = load_history()
    ver_tag = f".{version}" if version else ""
    base_name = drama.rstrip(".").replace(" ", ".")
    key = f"{base_name}.S{file_season:02d}E{ep:02d}{ver_tag}"
    if key in history: return True
    prefix = f"{base_name}.S{file_season:02d}E{ep:02d}"
    current_tags = set(t.lower() for t in version.split('.') if t)
    for hist_key in history:
        if hist_key.startswith(prefix):
            hist_version = hist_key[len(prefix):].strip(".")
            hist_tags = set(t.lower() for t in hist_version.split('.') if t)
            if current_tags == hist_tags: return True
    return False

def record_history(drama, file_season, ep, version=""):
    history = load_history()
    ver_tag = f".{version}" if version else ""
    base_name = drama.rstrip(".").replace(" ", ".")
    key = f"{base_name}.S{file_season:02d}E{ep:02d}{ver_tag}"
    if key not in history:
        history[key] = int(time.time())
        try:
            with open(TG_HISTORY_DB, 'w', encoding='utf-8') as f: json.dump(history, f, ensure_ascii=False, indent=2)
        except Exception: pass

def load_tg_routes():
    if os.path.exists(TG_ROUTE_DB):
        try:
            with open(TG_ROUTE_DB, 'r', encoding='utf-8') as f: return json.load(f)
        except Exception: pass
    return {}

def save_tg_routes(data):
    try:
        with open(TG_ROUTE_DB, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2)
    except Exception: pass

async def fetch_tmdb_details(cn_title):
    if not TMDB_API_KEY: return datetime.now().strftime("%Y"), 9999
    clean_q = re.sub(r'S\d+$|\s+\d+$', '', cn_title).strip()
    url = f"https://api.themoviedb.org/3/search/multi?api_key={TMDB_API_KEY}&language=zh-CN&query={quote(clean_q)}&page=1"
    try:
        async with httpx.AsyncClient(timeout=5.0) as client:
            res = await client.get(url)
            results = res.json().get("results")
            if results:
                item = results[0]
                year = (item.get("first_air_date") or item.get("release_date") or "")[:4]
                if not year: year = datetime.now().strftime("%Y")
                tmdb_id = item.get("id")
                media_type = item.get("media_type", "tv")
                total_eps = 9999
                if media_type == "tv" and tmdb_id:
                    det_url = f"https://api.themoviedb.org/3/tv/{tmdb_id}?api_key={TMDB_API_KEY}&language=zh-CN"
                    det_res = await client.get(det_url)
                    total_eps = det_res.json().get("number_of_episodes", 9999)
                return year, total_eps
    except Exception: pass
    return datetime.now().strftime("%Y"), 9999

def extract_pure_episode(search_text, drama_anchor=None):
    text = search_text
    if drama_anchor:
        try: text = re.compile(re.escape(drama_anchor), re.IGNORECASE).sub(' ', text)
        except: pass
    m = re.search(r'(?i)E(?:P)?0*(\d+)', text)
    if m: return int(m.group(1))
    m = re.search(r'第\d+[季部].*?第\s*(\d+)\s*[集话期更]', text)
    if m: return int(m.group(1))
    m = re.search(r'第\s*(\d+)\s*[集话期更]', text)
    if m: return int(m.group(1))
    m = re.search(r'(?<![第\d])\s*(\d+)\s*[集话期更]', text)
    if m: return int(m.group(1))
    m_cn = re.search(r'第\s*([一二三四五六七八九十零百]+)\s*[集话期更]', text)
    if m_cn:
        cn_str = m_cn.group(1)
        cn_map = {"一":1, "二":2, "三":3, "四":4, "五":5, "六":6, "七":7, "八":8, "九":9, "十":10}
        if cn_str in cn_map: return cn_map[cn_str]
        if cn_str.startswith("十") and len(cn_str) == 2: return 10 + cn_map.get(cn_str[1], 0)
        if len(cn_str) == 3 and cn_str[1] == "十": return cn_map.get(cn_str[0],0)*10 + cn_map.get(cn_str[2],0)
        if cn_str.endswith("十") and len(cn_str) == 2: return cn_map.get(cn_str[0],0)*10
    clean_text = re.sub(r'(?i)(h264|h265|x264|x265|720p|1080p|2160p|4k|8k|web-dl|webrip)', '', text)
    m_trail = re.search(r'(?<!\d)0*(\d{1,3})(?!\d)', clean_text)
    if m_trail and not (1900 < int(m_trail.group(1)) < 2100): return int(m_trail.group(1))
    return None

def extract_movie_part(search_text):
    m = re.search(r'(?i)(?:cd|part|pt)[\s_.-]*(\d{1,2})(?!\d)', search_text)
    if m: return f"cd{m.group(1)}" 
    return ""

def extract_media_tags(search_text):
    text = search_text.upper() 
    tags = []
    if re.search(r'(2160P|4K)', text): tags.append("2160p") 
    elif re.search(r'(1080P)', text): tags.append("1080p")
    elif re.search(r'(720P)', text): tags.append("720p")
    if re.search(r'(DV|DOVI|DOLBY VISION)', text): tags.append("DV")
    if re.search(r'(HDR10\+|HDR10|HDR)', text): tags.append("HDR")
    if re.search(r'(SDR)', text): tags.append("SDR")
    if re.search(r'(HQ|HIGH QUALITY)', text): tags.append("HQ")
    if re.search(r'(IQ)', text): tags.append("IQ")
    if re.search(r'(60FPS)', text): tags.append("60fps")
    return ".".join(tags)

def smart_rename(orig_name, drama_name, ep_num=None, season=None, is_movie=False):
    has_cn = re.search(r'[\u4e00-\u9fa5]', orig_name)
    if has_cn:
        return orig_name.replace(" ", ".")
    
    clean_drama = drama_name.replace(" ", ".")
    if is_movie:
        return f"{clean_drama}.{orig_name}".replace("..", ".")
    else:
        has_std_ep = re.search(r'(?i)(S\d+E\d+|EP?\d+)', orig_name)
        if has_std_ep:
            return f"{clean_drama}.{orig_name}".replace("..", ".")
        else:
            ep_tag = f"S{season:02d}E{ep_num:02d}" if season and ep_num else ""
            if ep_tag:
                return f"{clean_drama}.{ep_tag}.{orig_name}".replace("..", ".")
            return f"{clean_drama}.{orig_name}".replace("..", ".")

async def wipe_magnet_task(gids_to_wipe):
    if isinstance(gids_to_wipe, str): gids_to_wipe = [gids_to_wipe]
    target_gids = set(gids_to_wipe)
    paths_to_delete = set()
    info_hashes = set()  
    
    await notify_steward_log(f"🧹 [清理系统启动] 开始清理 Aria2 任务,锁入 GID 链: {list(target_gids)}")
    try:
        async with httpx.AsyncClient(timeout=10.0) as h:
            for gid in list(target_gids):
                try:
                    res = await h.post(ARIA2_RPC_URL, json={"jsonrpc":"2.0", "id":"tg_bot", "method":"aria2.tellStatus", "params":[f"token:{ARIA2_RPC_SECRET}", gid]})
                    st = res.json().get("result", {})
                    if "followedBy" in st and st["followedBy"]: target_gids.update(st["followedBy"])
                    if "belongsTo" in st and st["belongsTo"]: target_gids.add(st["belongsTo"])
                except: pass
            for g in list(target_gids):
                try:
                    res_g = await h.post(ARIA2_RPC_URL, json={"jsonrpc":"2.0", "id":"tg_bot", "method":"aria2.tellStatus", "params":[f"token:{ARIA2_RPC_SECRET}", g]})
                    st_data = res_g.json().get("result", {})
                    if "infoHash" in st_data and st_data["infoHash"]: info_hashes.add(st_data["infoHash"])
                    files = st_data.get("files", [])
                    for f in files:
                        if f.get("path"): paths_to_delete.add(f.get("path"))
                except: pass
    except: pass
    try:
        async with httpx.AsyncClient(timeout=10.0) as h:
            for g in target_gids:
                try: await h.post(ARIA2_RPC_URL, json={"jsonrpc":"2.0", "id":"tg_bot", "method":"aria2.forceRemove", "params":[f"token:{ARIA2_RPC_SECRET}", g]})
                except: pass
    except: pass
    await asyncio.sleep(3.0)
    for info_hash in info_hashes:
        torrent_path = os.path.join(ARIA2_DOWNLOAD_DIR, f"{info_hash}.torrent")
        if os.path.exists(torrent_path) and os.path.isfile(torrent_path):
            try: os.remove(torrent_path)
            except: pass
    top_dirs_to_delete = set()
    for f_path in paths_to_delete:
        if f_path.startswith(ARIA2_DOWNLOAD_DIR):
            rel = os.path.relpath(f_path, ARIA2_DOWNLOAD_DIR)
            parts = rel.split(os.sep)
            if len(parts) > 0:
                top_dir = os.path.join(ARIA2_DOWNLOAD_DIR, parts[0])
                if os.path.isdir(top_dir) and top_dir != ARIA2_DOWNLOAD_DIR:
                    top_dirs_to_delete.add(top_dir)
        if os.path.exists(f_path) and os.path.isfile(f_path):
            try: os.remove(f_path)
            except: pass
        aria2_path = f"{f_path}.aria2"
        if os.path.exists(aria2_path) and os.path.isfile(aria2_path):
            try: os.remove(aria2_path)
            except: pass
    for top_dir in top_dirs_to_delete:
        try:
            if os.path.exists(top_dir): shutil.rmtree(top_dir, ignore_errors=True)
        except: pass
    try:
        async with httpx.AsyncClient(timeout=10.0) as h:
            for g in target_gids:
                for attempt in range(1, 6): 
                    try:
                        res = await h.post(ARIA2_RPC_URL, json={"jsonrpc":"2.0", "id":"tg_bot", "method":"aria2.removeDownloadResult", "params":[f"token:{ARIA2_RPC_SECRET}", g]})
                        if "error" not in res.json(): break
                    except: pass
                    await asyncio.sleep(1.5)
    except: pass
    await notify_steward_log("🧹 [清理系统完毕] 磁力任务关联的实体文件与面板记录已无痕销毁。")

async def bg_fetch_cas_task(cas_target_full, final_cas_path, sub_path, cas_file_name, status_msg=None, olist_url=OLIST_URL, olist_token=OLIST_TOKEN, drive_name="189"):
    try:
        await asyncio.sleep(10)
        get_info_url = f"{olist_url}/api/fs/get"
        headers_get = {"Authorization": olist_token, "Content-Type": "application/json"}
        cas_downloaded = False
        
        for attempt in range(30):
            try:
                async with httpx.AsyncClient(timeout=20.0, follow_redirects=True) as client_get:
                    resp = await client_get.post(get_info_url, json={"path": cas_target_full}, headers=headers_get)
                    resp_data = resp.json()
                    
                    if resp_data.get("code") == 200:
                        raw_url = resp_data["data"]["raw_url"]
                        resp_dl = await client_get.get(raw_url)
                        
                        if resp_dl.status_code == 200:
                            temp_cas_path = final_cas_path + ".tmp"
                            with open(temp_cas_path, "wb") as f_cas:
                                f_cas.write(resp_dl.content)
                            os.rename(temp_cas_path, final_cas_path)
                            await notify_steward_log(f"🔗 [{drive_name} CAS下发成功] `{sub_path}/{cas_file_name}`")
                            
                            if status_msg:
                                try: await status_msg.edit_text(f"🔗 **[{drive_name} CAS就绪]**\n📥 `{cas_file_name}`\n✅ 镜像完毕,等待接管!")
                                except: pass
                            else:
                                try: await app.send_message(COMMAND_CENTER_CHAT, f"🔗 **[{drive_name} CAS就绪]**\n📥 `{cas_file_name}`\n✅ 镜像完毕,等待接管!")
                                except: pass
                            # 🔥 新增:成功拿到 CAS 后,通知 5555 脚本去干活
                            # 这里我把 final_cas_path (也就是下载好的本地 .cas 文件完整路径) 传给了 path 参数,如果 5555 需要的不是本地 .cas 路径,而是云端目录,你只需把代码里的 quote(final_cas_path) 改成 quote(cas_target_full) 即可
                            trigger_url = f"http://127.0.0.1:5555/force_harvest?path={quote(final_cas_path)}"
                            asyncio.create_task(trigger_strm_sync(trigger_url, cas_file_name, "189联动"))

                            cas_downloaded = True
                            break
            except Exception: pass 
            if not cas_downloaded: await asyncio.sleep(10)

        if not cas_downloaded:
            if status_msg:
                try: await status_msg.edit_text(f"⚠️ **[{drive_name} CAS拉取失败]**\n❌ `{cas_file_name}`\n云端未生成或直链被拦截。")
                except: pass
            else:
                try: await app.send_message(COMMAND_CENTER_CHAT, f"⚠️ **[{drive_name} CAS拉取失败]**\n❌ `{cas_file_name}`\n云端未生成或直链被拦截。")
                except: pass
    except Exception as e:
        await notify_steward_log(f"⚠️ [后台CAS派发失败]: {e}", level="WARNING")

# 🔥 核心更新:精简 5000 端口生成 STRM 和入库日志
async def trigger_strm_sync(url, file_name, drive_name):
    try:
        await asyncio.sleep(5)  # 稍微等待 5 秒,确保 OpenList 云端缓存已刷新
        async with httpx.AsyncClient(timeout=15.0) as client:
            resp = await client.get(url)
            if resp.status_code == 200:
                await notify_steward_log(f"🎬 [{drive_name} STRM同步触发] `{file_name}`")
            else:
                await notify_steward_log(f"⚠️ [{drive_name} STRM同步异常] HTTP {resp.status_code}", level="WARNING")
    except Exception as e:
        await notify_steward_log(f"⚠️ [{drive_name} STRM同步失败]: {e}", level="WARNING")

# =================================================================
# 🚀 【磁力专属】后台双轨接力直推
# =================================================================
async def magnet_upload_task(local_path, target_dir_189, cat, folder_name, folder_season, total_bytes, clean_base, file_season, ep_num, is_movie, standard_name, version_suffix="", status_msg=None, original_gid=None, enable_139=False, skip_189=False):
    msg_189 = status_msg
    try:
        # ----- 阶段 1:天翼 189 高速直推 -----
        target_full_189 = f"{target_dir_189}/{standard_name}".replace("//", "/")
        await asyncio.sleep(2)  
        
        if not skip_189:
            retry_count = 0
            while os.path.exists(local_path):
                try:
                    put_url = f"{OLIST_URL}/api/fs/put"
                    headers = {"Authorization": OLIST_TOKEN, "File-Path": quote(target_full_189), "Content-Length": str(total_bytes), "Content-Type": "application/octet-stream"}
                    
                    # 🔥 统一使用 GLOBAL_UPLOAD_LOCK 确保与 TG 下载任务严格排队
                    async with GLOBAL_UPLOAD_LOCK:
                        start_time = time.time()
                        last_ui_time = start_time
                        
                        with open(local_path, "rb") as f_upload:
                            async def file_iter():
                                nonlocal last_ui_time
                                sent = 0
                                chunk_size = 1024 * 1024
                                while True:
                                    chunk = f_upload.read(chunk_size)
                                    if not chunk: break
                                    sent += len(chunk)
                                    pct = int(sent * 100 / total_bytes)
                                    
                                    now = time.time()
                                    if now - last_ui_time > 8.0 or sent == total_bytes:
                                        last_ui_time = now
                                        duration = now - start_time
                                        speed_bps = sent / duration if duration > 0 else 0
                                        speed_mb = speed_bps / (1024 * 1024)
                                        rem_bytes = total_bytes - sent
                                        eta_sec = rem_bytes / speed_bps if speed_bps > 0 else 0
                                        eta_txt = f"{int(eta_sec // 60)}分{int(eta_sec % 60)}秒" if eta_sec > 60 else f"{int(eta_sec)}秒"
                                        
                                        if msg_189:
                                            try: await msg_189.edit_text(f"🚀 **[磁力推天翼]** `{standard_name}`\n📈 速度: **{speed_mb:.2f} MB/s**\n⏳ 进度: **{pct}%** | 剩余: **{eta_txt}**")
                                            except: pass
                                    yield chunk
                                    
                            async with httpx.AsyncClient(timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=None), trust_env=False) as h: 
                                resp = await h.put(put_url, content=file_iter(), headers=headers)
                        
                    if resp.status_code != 200:
                        raise Exception(f"HTTP状态码异常: {resp.status_code}")
                        
                    resp_data = resp.json()
                    if resp_data.get("code") == 200:
                        ver_tag = f".{version_suffix}" if version_suffix else ""
                        if not is_movie and ep_num is not None:
                            record_history(clean_base, file_season, ep_num, version_suffix)
                            await notify_steward_log(f"📝 [云端入账] {clean_base}.S{file_season:02d}E{ep_num:02d}{ver_tag}")
                        
                        try:
                            current_mount = get_mount_root()
                            STAGING_BASE_DIR = "/storage/emulated/0/Download/189cas"
                            sub_path = target_dir_189.replace(current_mount, "", 1)
                            local_cas_dir = f"{STAGING_BASE_DIR}{sub_path}"
                            os.makedirs(local_cas_dir, exist_ok=True)
                            final_cas_path = os.path.join(local_cas_dir, f"{standard_name}.cas")
                            asyncio.create_task(bg_fetch_cas_task(f"{target_full_189}.cas", final_cas_path, sub_path, f"{standard_name}.cas", status_msg=msg_189, olist_url=OLIST_URL, olist_token=OLIST_TOKEN, drive_name="189"))
                        except Exception: pass
                        break # 跳出 189 重试循环
                    else:
                        raise Exception(f"API报错: {resp_data.get('message', '未知错误')}")
                except Exception as e:
                    retry_count += 1
                    delay = 120 if retry_count == 1 else (300 if retry_count == 2 else (600 if retry_count == 3 else (900 if retry_count == 4 else 1800)))
                    
                    if msg_189 and retry_count < 5:
                        try: await msg_189.edit_text(f"⚠️ **[189上传异常]** `{standard_name}`\n❌ 报错: `{str(e)[:50]}`\n⏳ 等待重试 ({retry_count}/5)...")
                        except: pass
                    elif retry_count == 5 and msg_189:
                        try: await msg_189.edit_text(f"⚠️ **[189转后台重试]** `{standard_name}`\n已失败5次,转入后台静默重试。")
                        except: pass
                        
                    await notify_steward_log(f"⚠️ [189重试] {standard_name}: {str(e)[:100]}, 等待 {delay}s", level="WARNING")
                    await asyncio.sleep(delay)
                    
        else:
            if msg_189:
                try: await msg_189.edit_text(f"⏭️ **[跳过天翼 189]** `{standard_name}`\n已指定仅推 139,直接切入移动云盘队列...")
                except: pass

        # ----- 阶段 2:移动 139 限速接力 (140 占位 + CAS 战术) -----
        if enable_139 and os.path.exists(local_path):
            rel_category_path = f"{cat}/{folder_name}" if is_movie else f"{cat}/{folder_name}/Season {folder_season}"
            
            msg_chat_id = msg_189.chat.id if msg_189 else COMMAND_CENTER_CHAT
            msg_139 = None
            if msg_189:
                try: msg_139 = await app.send_message(msg_chat_id, f"🚀 **[移动139战术等待中]** `{standard_name}`\n🚦 正在排队,等待前面任务执行完毕...")
                except: pass
                
            retry_count = 0
            step_139 = 1  
            
            while os.path.exists(local_path):
                try:
                    temp_cloud_dir = "/140/139cas"
                    temp_cloud_path = f"{temp_cloud_dir}/{standard_name}".replace("//", "/")
                    put_url = f"{OLIST_URL}/api/fs/put" 
                    final_cloud_dir = f"/139/139cas/{rel_category_path}" 
                    
                    async with GLOBAL_139_UPLOAD_LOCK:
                        if step_139 == 1:
                            asyncio.create_task(notify_steward_log(f"🟢 [139战术] 开始140占位及CAS计算: {standard_name}"))
                        
                        # 1. 140 占位
                        if step_139 <= 1:
                            headers = {"Authorization": OLIST_TOKEN, "File-Path": quote(temp_cloud_path), "Content-Length": str(total_bytes), "Content-Type": "application/octet-stream"}
                            start_time = time.time()
                            last_ui_time = start_time
                            
                            with open(local_path, "rb") as f_upload:
                                async def file_iter_139():
                                    nonlocal last_ui_time
                                    sent = 0
                                    chunk_size = 1024 * 1024 
                                    while True:
                                        chunk = f_upload.read(chunk_size)
                                        if not chunk: break
                                        sent += len(chunk)
                                        pct = int(sent * 100 / total_bytes)
                                        
                                        now = time.time()
                                        if now - last_ui_time > 8.0 or sent == total_bytes:
                                            last_ui_time = now
                                            duration = now - start_time
                                            speed_bps = sent / duration if duration > 0 else 0
                                            speed_mb = speed_bps / (1024 * 1024)
                                            rem_bytes = total_bytes - sent
                                            eta_sec = rem_bytes / speed_bps if speed_bps > 0 else 0
                                            eta_txt = f"{int(eta_sec // 60)}分{int(eta_sec % 60)}秒" if eta_sec > 60 else f"{int(eta_sec)}秒"
                                            
                                            if msg_139:
                                                try: await msg_139.edit_text(f"🚀 **[移动140占位]** `{standard_name}`\n📈 速度: **{speed_mb:.2f} MB/s**\n⏳ 进度: **{pct}%** | 剩余: **{eta_txt}**")
                                                except: pass
                                        yield chunk
                                        
                                async with httpx.AsyncClient(timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=None), trust_env=False) as h: 
                                    resp = await h.put(put_url, content=file_iter_139(), headers=headers)
                                
                                if resp.status_code != 200 or resp.json().get("code") != 200:
                                    raise Exception(f"140占位上传失败: HTTP {resp.status_code}")

                            step_139 = 2  

                        # 2. 本地计算 CAS
                        if step_139 <= 2:
                            if msg_139:
                                try: await msg_139.edit_text(f"🚀 **[移动139战术]** `{standard_name}`\n✅ 140 占位成功!\n⏳ 正在提取微型 CAS 指纹...")
                                except: pass
                                
                            cas_script_path = os.path.join(BASE_DIR, "cas_server.py")
                            cmd_cas = [
                                "python3", cas_script_path, 
                                "--cli", "--file", local_path, "--cloud", "139", "--category", rel_category_path
                            ]
                            proc_cas = await asyncio.create_subprocess_exec(*cmd_cas, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
                            stdout_data, stderr_data = await proc_cas.communicate()
                            
                            out_log = stdout_data.decode('utf-8', errors='ignore').strip()
                            err_log = stderr_data.decode('utf-8', errors='ignore').strip()
                            if err_log or "error" in out_log.lower() or "failed" in out_log.lower():
                                await notify_steward_log(f"⚠️ [CAS提取异常] {err_log or out_log}", level="WARNING")
                            step_139 = 3  

                        # 3. 将 CAS 上传到 139
                        if step_139 <= 3:
                            local_cas_path_1 = f"/storage/emulated/0/Download/139cas/{rel_category_path}/{standard_name}.cas".replace("//", "/")
                            local_cas_path_2 = f"{local_path}.cas"
                            local_cas_path_3 = os.path.splitext(local_path)[0] + ".cas"
                            local_cas_path_4 = os.path.join(BASE_DIR, "cas_output", f"{standard_name}.cas") 
                            
                            actual_cas_path = None
                            for cp in [local_cas_path_1, local_cas_path_2, local_cas_path_3, local_cas_path_4]:
                                if os.path.exists(cp):
                                    actual_cas_path = cp
                                    break
                                    
                            if not actual_cas_path:
                                raise Exception("CAS提取失败,找不到 .cas 文件!")

                            local_cas_path_final = actual_cas_path

                            if msg_139:
                                try: await msg_139.edit_text(f"🚀 **[移动139战术]** `{standard_name}`\n📤 CAS 提取成功!正在写入 139 目录...")
                                except: pass
                                
                            final_cloud_path = f"{final_cloud_dir}/{standard_name}.cas".replace("//", "/")
                            cas_bytes = os.path.getsize(local_cas_path_final)
                            headers_cas = {"Authorization": OLIST_TOKEN, "File-Path": quote(final_cloud_path), "Content-Length": str(cas_bytes), "Content-Type": "application/octet-stream"}
                            
                            async with httpx.AsyncClient(timeout=30.0) as h:
                                with open(local_cas_path_final, "rb") as f_cas:
                                    cas_resp = await h.put(put_url, content=f_cas.read(), headers=headers_cas)
                                    if cas_resp.status_code != 200 or cas_resp.json().get("code") != 200:
                                        raise Exception("CAS文件上传到139失败")

                            step_139 = 4  

                        # 4. 删除 140 占位大文件
                        if step_139 <= 4:
                            if msg_139:
                                try: await msg_139.edit_text(f"🚀 **[移动139战术]** `{standard_name}`\n🗑️ 狸猫换太子!抹除 140 占位大文件,释放云盘空间...")
                                except: pass
                                
                            remove_url = f"{OLIST_URL}/api/fs/remove"
                            remove_payload = {"names": [standard_name], "dir": temp_cloud_dir}
                            async with httpx.AsyncClient(timeout=30.0) as h:
                                try: 
                                    rem_resp = await h.post(remove_url, json=remove_payload, headers={"Authorization": OLIST_TOKEN, "Content-Type": "application/json"})
                                    if rem_resp.status_code == 200 and rem_resp.json().get("code") == 200:
                                        await notify_steward_log(f"✅ [139战术] 140占位文件删除成功: {standard_name}")
                                    else:
                                        await notify_steward_log(f"⚠️ [139战术] 抹除占位失败, 状态码:{rem_resp.status_code}, 返回:{rem_resp.text}", level="WARNING")
                                except Exception as rem_err: 
                                    await notify_steward_log(f"⚠️ [139战术] 抹除占位异常: {rem_err}", level="WARNING")
                            step_139 = 5  

                        # 5. 触发 5000 管家入库
                        if step_139 <= 5:
                            sync_url = f"{STEWARD_BASE_URL}/api/sync?drive=139&path={quote(final_cloud_dir)}"
                            asyncio.create_task(trigger_strm_sync(sync_url, f"{standard_name}.cas", "139"))
                            
                            if msg_139:
                                try: await msg_139.edit_text(f"🎉 **[139 战术大圆满]** `{standard_name}`\n✅ 空间已白嫖,CAS入库指令已发射!")
                                except: pass
                            
                            break # 跳出 139 重试循环
                        
                except Exception as e:
                    retry_count += 1
                    delay = 120 if retry_count == 1 else (300 if retry_count == 2 else (600 if retry_count == 3 else (900 if retry_count == 4 else 1800)))
                    if msg_139 and retry_count < 5:
                        try: await msg_139.edit_text(f"⚠️ **[139战术异常]** `{standard_name}`\n❌ 报错: `{str(e)[:50]}`\n⏳ 等待重试 ({retry_count}/5)...")
                        except: pass
                    elif retry_count == 5 and msg_139:
                        try: await msg_139.edit_text(f"⚠️ **[139转后台重试]** `{standard_name}`\n已失败5次,转入后台静默重试。")
                        except: pass
                    await notify_steward_log(f"⚠️ [139战术重试] {standard_name}: {str(e)[:100]}, 等待 {delay}s", level="WARNING")
                    await asyncio.sleep(delay)
            
        success_msg = f"🎉 **[双端圆满落盘]** ➔ `{standard_name}`\n✅ 该文件已成功推送,释放本地空间!"
        if msg_189:
            try: await msg_189.edit_text(success_msg)
            except: pass
        else:
            try: await app.send_message(COMMAND_CENTER_CHAT, success_msg)
            except Exception:
                try: await app.send_message("me", success_msg)
                except: pass

    finally:
        if os.path.exists(local_path):
            try: os.remove(local_path)
            except: pass

# =================================================================
# 🚀 【TG专属】后台双轨接力直推
# =================================================================
async def bg_upload_retry_task(local_path, target_dir_189, cat, folder_name, folder_season, total_bytes, clean_base, file_season, ep_num, is_movie, standard_name, history_tags="", status_msg=None, enable_139=False):
    task_lock_key = f"{clean_base}.S{file_season:02d}E{ep_num:02d}{history_tags}" if not is_movie else f"{clean_base}{history_tags}"
    try:
        await asyncio.sleep(10)  
        
        # ----- 阶段 1:天翼 189 高速直推 -----
        target_full_189 = f"{target_dir_189}/{standard_name}".replace("//", "/")
        msg_189 = status_msg
        retry_count = 0
        
        while os.path.exists(local_path):
            try:
                put_url = f"{OLIST_URL}/api/fs/put"
                headers = {"Authorization": OLIST_TOKEN, "File-Path": quote(target_full_189), "Content-Length": str(total_bytes), "Content-Type": "application/octet-stream"}
                
                async with GLOBAL_UPLOAD_LOCK:
                    start_time = time.time()
                    last_ui_time = start_time
                    
                    with open(local_path, "rb") as f_upload:
                        async def file_iter():
                            nonlocal last_ui_time
                            sent = 0
                            chunk_size = 1024 * 1024 
                            while True:
                                chunk = f_upload.read(chunk_size)
                                if not chunk: break
                                sent += len(chunk)
                                pct = int(sent * 100 / total_bytes)
                                
                                now = time.time()
                                if now - last_ui_time > 8.0 or sent == total_bytes:
                                    last_ui_time = now
                                    duration = now - start_time
                                    speed_bps = sent / duration if duration > 0 else 0
                                    speed_mb = speed_bps / (1024 * 1024)
                                    rem_bytes = total_bytes - sent
                                    eta_sec = rem_bytes / speed_bps if speed_bps > 0 else 0
                                    eta_txt = f"{int(eta_sec // 60)}分{int(eta_sec % 60)}秒" if eta_sec > 60 else f"{int(eta_sec)}秒"
                                    
                                    if msg_189:
                                        try: await msg_189.edit_text(f"🚀 **[天翼189云推]** `{standard_name}`\n📈 速度: **{speed_mb:.2f} MB/s**\n⏳ 进度: **{pct}%** | 剩余: **{eta_txt}**")
                                        except: pass
                                yield chunk
                                
                        async with httpx.AsyncClient(timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=None), trust_env=False) as h: 
                            resp = await h.put(put_url, content=file_iter(), headers=headers)
                        
                    if resp.status_code != 200:
                        raise Exception(f"HTTP状态码异常: {resp.status_code}")
                        
                    resp_data = resp.json()
                    if resp_data.get("code") == 200:
                        ver_tag = f".{history_tags}" if history_tags else ""
                        if not is_movie and ep_num is not None:
                            record_history(clean_base, file_season, ep_num, history_tags)
                            await notify_steward_log(f"📝 [后台重推-补录] {clean_base}.S{file_season:02d}E{ep_num:02d}{ver_tag}")
                        
                        try:
                            current_mount = get_mount_root()
                            STAGING_BASE_DIR = "/storage/emulated/0/Download/189cas"
                            sub_path = target_dir_189.replace(current_mount, "", 1)
                            local_cas_dir = f"{STAGING_BASE_DIR}{sub_path}"
                            os.makedirs(local_cas_dir, exist_ok=True)
                            final_cas_path = os.path.join(local_cas_dir, f"{standard_name}.cas")
                            asyncio.create_task(bg_fetch_cas_task(f"{target_full_189}.cas", final_cas_path, sub_path, f"{standard_name}.cas", status_msg=msg_189, olist_url=OLIST_URL, olist_token=OLIST_TOKEN, drive_name="189"))
                        except Exception: pass
                        break # 跳出 189 重试循环
                    else:
                        raise Exception(f"API报错: {resp_data.get('message', '未知错误')}")
            except Exception as e:
                retry_count += 1
                delay = 120 if retry_count == 1 else (300 if retry_count == 2 else (600 if retry_count == 3 else (900 if retry_count == 4 else 1800)))
                
                if msg_189 and retry_count < 5:
                    try: await msg_189.edit_text(f"⚠️ **[189上传异常]** `{standard_name}`\n❌ 报错: `{str(e)[:50]}`\n⏳ 等待重试 ({retry_count}/5)...")
                    except: pass
                elif retry_count == 5 and msg_189:
                    try: await msg_189.edit_text(f"⚠️ **[189转后台重试]** `{standard_name}`\n已失败5次,转入后台静默重试。")
                    except: pass
                    
                await notify_steward_log(f"⚠️ [189重试] {standard_name}: {str(e)[:100]}, 等待 {delay}s", level="WARNING")
                await asyncio.sleep(delay)

        # ----- 阶段 2:移动 139 限速接力 (140 占位 + CAS 战术) -----
        if enable_139 and os.path.exists(local_path):
            rel_category_path = f"{cat}/{folder_name}" if is_movie else f"{cat}/{folder_name}/Season {folder_season}"
            
            msg_chat_id = msg_189.chat.id if msg_189 else COMMAND_CENTER_CHAT
            msg_139 = None
            if msg_189:
                try: msg_139 = await app.send_message(msg_chat_id, f"🚀 **[移动139战术等待中]** `{standard_name}`\n🚦 正在排队,等待前面任务执行完毕...")
                except: pass
                
            retry_count = 0
            step_139 = 1  
            
            while os.path.exists(local_path):
                try:
                    temp_cloud_dir = "/140/139cas"
                    temp_cloud_path = f"{temp_cloud_dir}/{standard_name}".replace("//", "/")
                    put_url = f"{OLIST_URL}/api/fs/put" 
                    final_cloud_dir = f"/139/139cas/{rel_category_path}" 
                    
                    async with GLOBAL_139_UPLOAD_LOCK:
                        if step_139 == 1:
                            asyncio.create_task(notify_steward_log(f"🟢 [139战术] 开始140占位及CAS计算: {standard_name}"))
                        
                        # 1. 140 占位
                        if step_139 <= 1:
                            headers = {"Authorization": OLIST_TOKEN, "File-Path": quote(temp_cloud_path), "Content-Length": str(total_bytes), "Content-Type": "application/octet-stream"}
                            start_time = time.time()
                            last_ui_time = start_time
                            
                            with open(local_path, "rb") as f_upload:
                                async def file_iter_139():
                                    nonlocal last_ui_time
                                    sent = 0
                                    chunk_size = 1024 * 1024 
                                    while True:
                                        chunk = f_upload.read(chunk_size)
                                        if not chunk: break
                                        sent += len(chunk)
                                        pct = int(sent * 100 / total_bytes)
                                        
                                        now = time.time()
                                        if now - last_ui_time > 8.0 or sent == total_bytes:
                                            last_ui_time = now
                                            duration = now - start_time
                                            speed_bps = sent / duration if duration > 0 else 0
                                            speed_mb = speed_bps / (1024 * 1024)
                                            rem_bytes = total_bytes - sent
                                            eta_sec = rem_bytes / speed_bps if speed_bps > 0 else 0
                                            eta_txt = f"{int(eta_sec // 60)}分{int(eta_sec % 60)}秒" if eta_sec > 60 else f"{int(eta_sec)}秒"
                                            
                                            if msg_139:
                                                try: await msg_139.edit_text(f"🚀 **[移动140占位]** `{standard_name}`\n📈 速度: **{speed_mb:.2f} MB/s**\n⏳ 进度: **{pct}%** | 剩余: **{eta_txt}**")
                                                except: pass
                                        yield chunk
                                        
                                async with httpx.AsyncClient(timeout=httpx.Timeout(connect=10.0, read=None, write=None, pool=None), trust_env=False) as h: 
                                    resp = await h.put(put_url, content=file_iter_139(), headers=headers)
                                
                                if resp.status_code != 200 or resp.json().get("code") != 200:
                                    raise Exception(f"140占位上传失败: HTTP {resp.status_code}")

                            step_139 = 2  

                        # 2. 本地计算 CAS
                        if step_139 <= 2:
                            if msg_139:
                                try: await msg_139.edit_text(f"🚀 **[移动139战术]** `{standard_name}`\n✅ 140 占位成功!\n⏳ 正在提取微型 CAS 指纹...")
                                except: pass
                                
                            cas_script_path = os.path.join(BASE_DIR, "cas_server.py")
                            cmd_cas = [
                                "python3", cas_script_path, 
                                "--cli", "--file", local_path, "--cloud", "139", "--category", rel_category_path
                            ]
                            proc_cas = await asyncio.create_subprocess_exec(*cmd_cas, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
                            stdout_data, stderr_data = await proc_cas.communicate()
                            
                            out_log = stdout_data.decode('utf-8', errors='ignore').strip()
                            err_log = stderr_data.decode('utf-8', errors='ignore').strip()
                            if err_log or "error" in out_log.lower() or "failed" in out_log.lower():
                                await notify_steward_log(f"⚠️ [CAS提取异常] {err_log or out_log}", level="WARNING")
                            step_139 = 3  

                        # 3. 将 CAS 上传到 139
                        if step_139 <= 3:
                            local_cas_path_1 = f"/storage/emulated/0/Download/139cas/{rel_category_path}/{standard_name}.cas".replace("//", "/")
                            local_cas_path_2 = f"{local_path}.cas"
                            local_cas_path_3 = os.path.splitext(local_path)[0] + ".cas"
                            local_cas_path_4 = os.path.join(BASE_DIR, "cas_output", f"{standard_name}.cas") 
                            
                            actual_cas_path = None
                            for cp in [local_cas_path_1, local_cas_path_2, local_cas_path_3, local_cas_path_4]:
                                if os.path.exists(cp):
                                    actual_cas_path = cp
                                    break
                                    
                            if not actual_cas_path:
                                raise Exception("CAS提取失败,找不到 .cas 文件!")

                            local_cas_path_final = actual_cas_path

                            if msg_139:
                                try: await msg_139.edit_text(f"🚀 **[移动139战术]** `{standard_name}`\n📤 CAS 提取成功!正在写入 139 目录...")
                                except: pass
                                
                            final_cloud_path = f"{final_cloud_dir}/{standard_name}.cas".replace("//", "/")
                            cas_bytes = os.path.getsize(local_cas_path_final)
                            headers_cas = {"Authorization": OLIST_TOKEN, "File-Path": quote(final_cloud_path), "Content-Length": str(cas_bytes), "Content-Type": "application/octet-stream"}
                            
                            async with httpx.AsyncClient(timeout=30.0) as h:
                                with open(local_cas_path_final, "rb") as f_cas:
                                    cas_resp = await h.put(put_url, content=f_cas.read(), headers=headers_cas)
                                    if cas_resp.status_code != 200 or cas_resp.json().get("code") != 200:
                                        raise Exception("CAS文件上传到139失败")

                            step_139 = 4  

                        # 4. 删除 140 占位大文件
                        if step_139 <= 4:
                            if msg_139:
                                try: await msg_139.edit_text(f"🚀 **[移动139战术]** `{standard_name}`\n🗑️ 狸猫换太子!抹除 140 占位大文件,释放云盘空间...")
                                except: pass
                                
                            remove_url = f"{OLIST_URL}/api/fs/remove"
                            remove_payload = {"names": [standard_name], "dir": temp_cloud_dir}
                            async with httpx.AsyncClient(timeout=30.0) as h:
                                try: 
                                    rem_resp = await h.post(remove_url, json=remove_payload, headers={"Authorization": OLIST_TOKEN, "Content-Type": "application/json"})
                                    if rem_resp.status_code == 200 and rem_resp.json().get("code") == 200:
                                        await notify_steward_log(f"✅ [139战术] 140占位文件删除成功: {standard_name}")
                                    else:
                                        await notify_steward_log(f"⚠️ [139战术] 抹除占位失败, 状态码:{rem_resp.status_code}, 返回:{rem_resp.text}", level="WARNING")
                                except Exception as rem_err: 
                                    await notify_steward_log(f"⚠️ [139战术] 抹除占位异常: {rem_err}", level="WARNING")
                            step_139 = 5  

                        # 5. 触发 5000 管家入库
                        if step_139 <= 5:
                            sync_url = f"{STEWARD_BASE_URL}/api/sync?drive=139&path={quote(final_cloud_dir)}"
                            asyncio.create_task(trigger_strm_sync(sync_url, f"{standard_name}.cas", "139"))
                            
                            if msg_139:
                                try: await msg_139.edit_text(f"🎉 **[139 战术大圆满]** `{standard_name}`\n✅ 空间已白嫖,CAS入库指令已发射!")
                                except: pass
                            
                            break # 跳出 139 重试循环
                        
                except Exception as e:
                    retry_count += 1
                    delay = 120 if retry_count == 1 else (300 if retry_count == 2 else (600 if retry_count == 3 else (900 if retry_count == 4 else 1800)))
                    if msg_139 and retry_count < 5:
                        try: await msg_139.edit_text(f"⚠️ **[139战术异常]** `{standard_name}`\n❌ 报错: `{str(e)[:50]}`\n⏳ 等待重试 ({retry_count}/5)...")
                        except: pass
                    elif retry_count == 5 and msg_139:
                        try: await msg_139.edit_text(f"⚠️ **[139转后台重试]** `{standard_name}`\n已失败5次,转入后台静默重试。")
                        except: pass
                    await notify_steward_log(f"⚠️ [139战术重试] {standard_name}: {str(e)[:100]}, 等待 {delay}s", level="WARNING")
                    await asyncio.sleep(delay)
            
        success_msg = f"🎉 **[双端圆满落盘]** ➔ `{standard_name}`\n✅ 该文件已成功推送,释放本地空间!"
        if msg_189:
            try: await msg_189.edit_text(success_msg)
            except: pass
        else:
            try: await app.send_message(COMMAND_CENTER_CHAT, success_msg)
            except Exception:
                try: await app.send_message("me", success_msg)
                except: pass

    finally:
        if os.path.exists(local_path):
            try: os.remove(local_path)
            except: pass
        GLOBAL_ACTIVE_LOCKS.discard(task_lock_key)

# =================================================================
# 🧲 Aria2 异步执行任务 (磁力引擎)
# =================================================================
async def handle_magnet_execution(client, message, magnet_link, config_text, resume_gid=None):
    args = config_text.split()
    if len(args) < 2: 
        if message: 
            try: await message.reply_text("⚠️ 格式错误。请至少提供 `剧名` 和 `分类`。")
            except: pass
        return

    # 🔥 拦截 c139 和 proxy
    enable_139 = False
    use_proxy = False
    for i in range(len(args)-1, -1, -1):
        if args[i].lower() == "c139":
            enable_139 = True
            args.pop(i)
        elif args[i].lower() == "proxy":
            use_proxy = True
            args.pop(i)

    STANDARD_CATS = ["华语剧", "欧美剧", "日韩剧", "短剧", "华语电影", "欧美电影", "日韩电影", "演唱会", "国漫", "日漫", "综艺", "纪录片"]
    cat_idx = next((i for i, arg in enumerate(args) if arg in STANDARD_CATS), -1)
    if cat_idx == -1: 
        if message: 
            try: await message.reply_text("⚠️ 必须提供有效分类(如 国漫, 华语剧, 电影 等)。")
            except: pass
        return
    category = args.pop(cat_idx)

    custom_year = next((args.pop(i) for i, arg in enumerate(args) if arg.isdigit() and len(arg) == 4 and (1900 < int(arg) < 2100)), None)
    
    file_season = None
    folder_season = None
    version_suffix = ""
    for i in range(len(args)-1, -1, -1):
        if re.match(r'^s\d+$', args[i], re.IGNORECASE): 
            file_season = int(args.pop(i)[1:])
        elif args[i].lower().startswith("v="):
            version_suffix = args.pop(i)[2:]

    if args and args[-1].isdigit(): folder_season = int(args.pop(-1))
    if folder_season is None: folder_season = file_season if file_season is not None else 1

    # 🔥 修正:针对磁力任务,防止参数被抽空变成“未知磁力”
    raw_input = " ".join(args).strip()
    if not raw_input:
        if message:
            try: await message.reply_text("⚠️ **磁力解析失败**:未能提取到剧名!\n参数可能被系统误判全部抽走(如纯数字剧名)。请检查格式。")
            except: pass
        return
        
    if "|" in raw_input: search_kw, drama_name = [x.strip() for x in raw_input.split("|", 1)]
    else: search_kw = drama_name = raw_input

    year, _ = await fetch_tmdb_details(drama_name)
    if custom_year: year = custom_year

    folder_name = f"{drama_name} ({year})" if year else drama_name
    if version_suffix: folder_name = f"{folder_name} {version_suffix}"
    is_movie = "电影" in category or category in ["演唱会", "纪录片"]

    magnet_task_key = f"磁力_{drama_name}"
    GLOBAL_ACTIVE_LOCKS.add(magnet_task_key)
    
    original_gid = None

    try:
        if not resume_gid:
            # 🔥 新增 Aria2 代理注入逻辑 (此处 7890 替换为你的本地 HTTP 代理端口)
            aria2_options = {"dir": ARIA2_DOWNLOAD_DIR}
            if use_proxy:
                aria2_options["all-proxy"] = "http://127.0.0.1:7890"

            payload = {
                "jsonrpc": "2.0", "id": "tg_bot", "method": "aria2.addUri",
                "params": [f"token:{ARIA2_RPC_SECRET}", [magnet_link], aria2_options]
            }

            try:
                if message: status_msg = await message.reply_text(f"🚀 **[Aria2 涡轮点火]**\n{'🔗 **已开启 Tracker 代理加速**' if use_proxy else ''}\n正在将磁力喂给后端 P2P 引擎...")
                else: status_msg = await client.send_message(COMMAND_CENTER_CHAT, f"🚀 **[Aria2 涡轮点火]**\n{'🔗 **已开启 Tracker 代理加速**' if use_proxy else ''}\n正在将磁力喂给后端 P2P 引擎...")
                
                # 🔥 补回丢失的核心代码,不再报缩进错误!
                async with httpx.AsyncClient(timeout=10.0) as h_client:
                    resp = await h_client.post(ARIA2_RPC_URL, json=payload)
                    res_json = resp.json()
                    if "error" in res_json:
                        error_msg = res_json["error"].get("message", "")
                        if "already registered" in error_msg.lower():
                            try: await status_msg.edit_text(f"❌ **添加失败**: 任务已在 Aria2 队列中,请勿重复添加!")
                            except: pass
                            return 
                        else:
                            try: await status_msg.edit_text(f"❌ Aria2 拒绝接单: `{error_msg}`")
                            except: pass
                            return 
                    gid = res_json.get("result")
                    if not gid: 
                        try: await status_msg.edit_text(f"❌ Aria2 未返回 GID!")
                        except: pass
                        return 
                    
                    original_gid = gid
                    add_magnet_task(original_gid, magnet_link, config_text)
                    tracked_gids = {gid} 
                    GLOBAL_TRACKED_GIDS.add(gid)
                    
            except Exception as e:
                try: await status_msg.edit_text(f"❌ Aria2 连接失败: {e}")
                except: pass
                return
        else:
            gid = resume_gid
            original_gid = resume_gid
            tracked_gids = {gid}
            GLOBAL_TRACKED_GIDS.add(gid)
            add_magnet_task(original_gid, magnet_link, config_text)
            
            if message: status_msg = await message.reply_text(f"🔄 **[任务恢复]** 重新接管磁力任务 `{drama_name}`...")
            else: status_msg = await client.send_message(COMMAND_CENTER_CHAT, f"🔄 **[任务恢复]** 重新接管磁力任务 `{drama_name}`...")

        last_ui_time = 0
        meta_wait_time = 0
        st_data = {}
        
        notified_50 = False
        notified_100 = False
        
        while True:
            await asyncio.sleep(3.0)
            
            if GLOBAL_STOP_SWEEP or "ALL" in GLOBAL_CANCEL_TASKS or magnet_task_key in GLOBAL_CANCEL_TASKS:
                await wipe_magnet_task(tracked_gids)
                if original_gid: remove_magnet_task(original_gid)
                try: await status_msg.edit_text(f"🛑 **[任务拉闸]** 磁力任务 `{drama_name}` 已被强行单独销毁!")
                except: pass
                return 
                
            try:
                p2 = {"jsonrpc": "2.0", "id": "tg_bot", "method": "aria2.tellStatus", "params": [f"token:{ARIA2_RPC_SECRET}", gid]}
                async with httpx.AsyncClient(timeout=5.0) as h_client:
                    resp2 = await h_client.post(ARIA2_RPC_URL, json=p2)
                    res_json = resp2.json()
                    
                    if "error" in res_json:
                        try: await status_msg.edit_text("🛑 任务已从Aria2面板中消失或被外部手动删除!")
                        except: pass
                        return
                        
                    st_data = res_json.get("result", {})

                followed_by = st_data.get("followedBy")
                if followed_by and len(followed_by) > 0:
                    gid = followed_by[0]  
                    tracked_gids.add(gid)
                    GLOBAL_TRACKED_GIDS.add(gid)
                    meta_wait_time = 0
                    notified_50 = False
                    notified_100 = False
                    
                    try: await status_msg.edit_text(f"🧲 **[解析成功]** 获取到种子文件,开始正式拉取实体视频...")
                    except: pass
                    continue

                status = st_data.get("status")
                if not status: continue
                
                info_hash = st_data.get("infoHash")
                bt_info = st_data.get("bittorrent", {})
                is_metadata = False
                
                if info_hash and "info" not in bt_info:
                    is_metadata = True

                if is_metadata:
                    if status == "error":
                        try: await status_msg.edit_text(f"❌ **种子解析报错**: {st_data.get('errorMessage')}")
                        except: pass
                        return
                    
                    if status == "complete":
                        try:
                            p_active = {"jsonrpc": "2.0", "id": "tg_bot", "method": "aria2.tellActive", "params": [f"token:{ARIA2_RPC_SECRET}"]}
                            p_waiting = {"jsonrpc": "2.0", "id": "tg_bot", "method": "aria2.tellWaiting", "params": [f"token:{ARIA2_RPC_SECRET}", 0, 100]}
                            p_stopped = {"jsonrpc": "2.0", "id": "tg_bot", "method": "aria2.tellStopped", "params": [f"token:{ARIA2_RPC_SECRET}", 0, 100]}
                            async with httpx.AsyncClient() as h:
                                res_a = await h.post(ARIA2_RPC_URL, json=p_active)
                                res_w = await h.post(ARIA2_RPC_URL, json=p_waiting)
                                res_s = await h.post(ARIA2_RPC_URL, json=p_stopped)
                                tasks = res_a.json().get("result", []) + res_w.json().get("result", []) + res_s.json().get("result", [])
                                for t in tasks:
                                    if t.get("infoHash") == info_hash and "info" in t.get("bittorrent", {}):
                                        gid = t.get("gid")
                                        tracked_gids.add(gid)
                                        GLOBAL_TRACKED_GIDS.add(gid)
                                        try: await status_msg.edit_text(f"🧲 **[解析成功]** 跨队列捕获实体任务,无缝切入...")
                                        except: pass
                                        break
                        except: pass
                    
                    meta_wait_time += 3
                    if meta_wait_time > 300:
                        await wipe_magnet_task(tracked_gids)
                        try: await status_msg.edit_text("❌ **超时中止**:连续 5 分钟未获取到种子(死链),已销毁!")
                        except: pass
                        return
                    
                    now = time.time()
                    if now - last_ui_time > 6.0:
                        last_ui_time = now
                        try: await status_msg.edit_text(f"🧲 **[磁力涡轮]** `{drama_name}`\n⏳ 正在获取元数据(种子)中...")
                        except: pass
                    continue 

                if status == "error":
                    try: await status_msg.edit_text(f"❌ **Aria2 下载报错**: {st_data.get('errorMessage')}")
                    except: pass
                    return

                completed = int(st_data.get("completedLength", 0))
                total = int(st_data.get("totalLength", 0))

                if status == "active" and total > 0 and completed >= total:
                    asyncio.create_task(notify_steward_log(f"✅ [磁力涡轮] 进度达100%,主动结束状态监听,向清理流程移交控制权..."))
                    break

                if status in ["complete", "removed"]: 
                    break

                now = time.time()
                if now - last_ui_time > 6.0:
                    last_ui_time = now
                    dl_speed = int(st_data.get("downloadSpeed", 0))
                    up_speed = int(st_data.get("uploadSpeed", 0))
                    sp_mb = dl_speed / (1024*1024)
                    up_mb = up_speed / (1024*1024)
                    connections = st_data.get("connections", "0")
                    
                    if total > 0:
                        pct = int(completed * 100 / total)
                        eta_sec = (total - completed) / dl_speed if dl_speed > 0 else 0
                        eta_txt = f"{int(eta_sec//60)}分{int(eta_sec%60)}秒" if eta_sec > 0 else "计算中"
                        
                        if pct >= 50 and not notified_50:
                            asyncio.create_task(notify_steward_log(f"📥 [磁力下载] {drama_name} 已完成 50%"))
                            notified_50 = True
                        if pct == 100 and not notified_100:
                            asyncio.create_task(notify_steward_log(f"📥 [磁力下载] {drama_name} 已完成 100%"))
                            notified_100 = True
                    else:
                        pct = 0
                        eta_txt = "连接节点中..."

                    try: await status_msg.edit_text(f"🧲 **[磁力涡轮]** `{drama_name}`\n📈 下载: **{sp_mb:.2f} MB/s** | 📤 上传: **{up_mb:.2f} MB/s**\n🤝 连接: **{connections}** 节点\n⏳ 进度: **{pct}%** | 剩余: **{eta_txt}**")
                    except: pass
            except Exception: pass

        try: await status_msg.edit_text(f"✅ **[下载完毕]** 启动清道夫协议!\n正在全量检索下载物,提取视频并执行智能洗名...")
        except: pass

        files = st_data.get("files", [])
        VIDEO_EXTS = ['.mp4', '.mkv', '.ts', '.avi', '.rmvb', '.flv', '.webm', '.iso']
        extracted_videos = []
        
        for f in files:
            f_path = f.get("path")
            if not f_path or not os.path.exists(f_path): continue
            if os.path.getsize(f_path) <= 0: continue 

            if os.path.splitext(f_path)[1].lower() in VIDEO_EXTS:
                try:
                    orig_name = os.path.basename(f_path)
                    file_ep_num = extract_pure_episode(orig_name, drama_anchor=drama_name)
                    new_name = smart_rename(orig_name, drama_name, ep_num=file_ep_num, season=folder_season, is_movie=is_movie)
                    
                    safe_dest = os.path.join(LOCAL_TEMP_DIR, new_name)
                    if os.path.exists(safe_dest):
                        os.remove(safe_dest)
                    shutil.move(f_path, safe_dest)
                    
                    extracted_videos.append({
                        "path": safe_dest,
                        "name": new_name,
                        "size": os.path.getsize(safe_dest),
                        "ep_num": file_ep_num
                    })
                except Exception as e:
                    await notify_steward_log(f"⚠️ 提取视频 {f_path} 时出错: {e}", level="WARNING")

        await wipe_magnet_task(tracked_gids)

        if not extracted_videos:
            if st_data.get("status") == "removed": 
                try: await status_msg.edit_text("🛑 任务已被手动从后台终止并清理。")
                except: pass
                return
            try: await status_msg.edit_text("❌ 提取失败:该链接仅包含垃圾文件或死链残片。所有残留已干干净净地销毁。")
            except: pass
            return

        try: await status_msg.edit_text(f"🎉 **[清洗成功]** 共提取 {len(extracted_videos)} 个纯净视频!\n原始垃圾已焚毁,正由磁力专属双轨接力直推...")
        except: pass

        current_mount = get_mount_root()
        for idx, v in enumerate(extracted_videos):
            if is_movie: target_dir = f"{current_mount}/{category}/{folder_name}"
            else: target_dir = f"{current_mount}/{category}/{folder_name}/Season {folder_season}"
            
            pass_msg = status_msg if idx == 0 else None
            
            asyncio.create_task(magnet_upload_task(
                local_path=v['path'], 
                target_dir_189=target_dir, 
                cat=category,
                folder_name=folder_name,
                folder_season=folder_season,
                total_bytes=v['size'], 
                clean_base=drama_name.replace(" ", "."), 
                file_season=folder_season, 
                ep_num=v['ep_num'], 
                is_movie=is_movie, 
                standard_name=v['name'], 
                version_suffix=version_suffix,
                status_msg=pass_msg,
                original_gid=original_gid,
                enable_139=enable_139,
                skip_189=False
            ))
            
    finally:
        GLOBAL_ACTIVE_LOCKS.discard(magnet_task_key)
        GLOBAL_CANCEL_TASKS.discard(magnet_task_key)
        if original_gid:
            remove_magnet_task(original_gid)  # <--- 加上这两行,确保无论如何都会清理 JSON

# =================================================================
# 🎯 [指令响应大网关] 
# =================================================================
STANDARD_CATS = ["华语剧", "欧美剧", "日韩剧", "短剧", "华语电影", "欧美电影", "日韩电影", "演唱会", "国漫", "日漫", "综艺", "纪录片"]

@app.on_message(filters.chat(COMMAND_CENTER_CHAT) & filters.command(["sub", "unsub", "list", "add", "del", "go", "history", "ping", "rm", "clean", "scan", "rmh", "setdir", "cancel", "mag", "reup", "clh"]) & filters.user("me"))
async def manage_system_commands(client, message):
    global GLOBAL_STOP_SWEEP
    command = message.command[0].lower()
    config = load_listener_config()
    
    # 🔥 核心更新:新增的本地遗留文件强制重推引擎
    if command == "reup":
        args = message.command[1:]
        if len(args) < 2: 
            return await message.reply_text("⚠️ 语法:`/reup [剧名关键字] [分类] [可选:年份] [c139|only139]`\n例如:`/reup 灿如繁星 欧美剧 2026 only139`")
        
        enable_139 = False
        skip_189 = False
        for i in range(len(args)-1, -1, -1):
            arg_lower = args[i].lower()
            if arg_lower == "c139":
                enable_139 = True
                args.pop(i)
            elif arg_lower == "only139":
                enable_139 = True
                skip_189 = True
                args.pop(i)
                
        # 提取可选的手动年份
        custom_year = next((args.pop(i) for i, arg in enumerate(args) if arg.isdigit() and len(arg) == 4 and (1900 < int(arg) < 2100)), None)
                
        # 提取分类
        cat_idx = next((i for i, arg in enumerate(args) if arg in STANDARD_CATS), -1)
        if cat_idx != -1: category = args.pop(cat_idx)
        else: category = args.pop(-1)

        kw = " ".join(args).lower()
        drama_name_input = args[0]  # 🔥 提取纯中文剧名
        
        found_files = []
        if os.path.exists(LOCAL_TEMP_DIR):
            for f in os.listdir(LOCAL_TEMP_DIR):
                if kw in f.lower() and os.path.isfile(os.path.join(LOCAL_TEMP_DIR, f)):
                    found_files.append(f)
                    
        if not found_files:
            return await message.reply_text(f"❌ 在本地缓存文件夹(tg_temp)中未找到包含 `{kw}` 的视频文件,可能已被清理。")
            
        await message.reply_text(f"🔍 成功截获 {len(found_files)} 个未上传的本地遗留文件,启动本地补推引擎!")
        
        for fname in found_files:
            local_path = os.path.join(LOCAL_TEMP_DIR, fname)
            total_bytes = os.path.getsize(local_path)
            
            # 智能反向推导剧名、季数、集数
            m_tv = re.search(r'^(.*?)\.S(\d+)E(\d+)\.(.*?)$', fname, re.IGNORECASE)
            m_movie = re.search(r'^(.*?)\.(19\d{2}|20\d{2})\.(.*?)$', fname)
            
            is_movie = "电影" in category or category in ["演唱会", "纪录片"]
            year = ""
            clean_base = fname.split('.')[0]
            file_season = folder_season = ep_num = 1
            
            if m_tv and not is_movie:
                clean_base = m_tv.group(1)
                file_season = folder_season = int(m_tv.group(2))
                ep_num = int(m_tv.group(3))
                m_year = re.search(r'\b(20\d{2})\b', fname)
                if m_year: year = m_year.group(1)
            elif m_movie and is_movie:
                clean_base = m_movie.group(1)
                year = m_movie.group(2)
            
            # 🔥 年份优先级:手动指定 > 文件名提取 > 留空
            final_year = custom_year if custom_year else year
            folder_name = f"{drama_name_input} ({final_year})" if final_year else drama_name_input
            
            current_mount = get_mount_root()
            if is_movie: target_dir = f"{current_mount}/{category}/{folder_name}"
            else: target_dir = f"{current_mount}/{category}/{folder_name}/Season {folder_season}"
            
            status_msg = await message.reply_text(f"🚀 **[本地补录推流]** 准备上传 `{fname}` ...")
            
            # 直接调用磁力的双轨上传队列,跳过下载环节
            asyncio.create_task(magnet_upload_task(
                local_path=local_path, 
                target_dir_189=target_dir, 
                cat=category,
                folder_name=folder_name,
                folder_season=folder_season,
                total_bytes=total_bytes, 
                clean_base=clean_base, 
                file_season=file_season, 
                ep_num=ep_num, 
                is_movie=is_movie, 
                standard_name=fname, 
                version_suffix="",
                status_msg=status_msg,
                original_gid=None,
                enable_139=enable_139,
                skip_189=skip_189
            ))
        return

    if command == "ping":
        uptime_minutes = int((time.time() - START_TIME) / 60)
        return await message.reply_text(f"🟢 **系统健康度 [优秀]**\n⏱️ 存活: `{uptime_minutes}` 分钟")

    if command == "setdir":
        if len(message.command) < 2: return await message.reply_text(f"📁 当前上传目录: `{get_mount_root()}`\n⚠️ 语法:`/setdir [新目录路径]`")
        new_dir = message.command[1]
        set_mount_root(new_dir)
        return await message.reply_text(f"✅ 上传主目录已成功切换至: `{new_dir}`")

    if command == "clean":
        count = 0
        if os.path.exists(LOCAL_TEMP_DIR):
            for f in os.listdir(LOCAL_TEMP_DIR):
                try: os.remove(os.path.join(LOCAL_TEMP_DIR, f)); count += 1
                except: pass
        return await message.reply_text(f"🧹 抹除 `{count}` 个遗留碎片!")

    if command == "cancel":
        kw = " ".join(message.command[1:]).lower() if len(message.command) > 1 else "all"
        if kw == "all":
            GLOBAL_STOP_SWEEP = True
            GLOBAL_CANCEL_TASKS.add("ALL")
            GLOBAL_ACTIVE_LOCKS.clear()
            
            # 🔥 修复补丁:5秒后自动解除全局封印,防止永久阻塞后续新任务
            async def clear_all_lock():
                await asyncio.sleep(5)
                GLOBAL_CANCEL_TASKS.discard("ALL")
            asyncio.create_task(clear_all_lock())
            
            return await message.reply_text("🛑 已下达最高追杀令:全局清空 TG 队列、拉闸后台扫荡、销毁底层 Aria2 任务!")
        else:
            kws = kw.split()
            matched = [name for name in GLOBAL_ACTIVE_LOCKS if all(k in name.lower() for k in kws)]
            if not matched: return await message.reply_text(f"❌ 没找到包含 `{kw}` 的任务。")
            for name in matched: GLOBAL_CANCEL_TASKS.add(name)
            return await message.reply_text(f"🛑 已取消:\n" + "\n".join([f"`{name}`" for name in matched]))

    if command == "mag":
        args = message.command[1:]
        if not args or not args[0].startswith("magnet:?"): return await message.reply_text("⚠️ 语法:`/mag [磁力链接]`\n(建议直接把链接发给机器人)")
        magnet_link = args.pop(0)
        prompt = await message.reply_text("🧲 磁力链接已直接捕获!请回复: `剧名|别名 分类 [S季数] [年份] [v=版本] [c139]`")
        chat_id = message.chat.id
        if chat_id not in PENDING_MAGNETS:
            PENDING_MAGNETS[chat_id] = {}
        PENDING_MAGNETS[chat_id][prompt.id] = {"link": magnet_link, "mag_msg_id": message.id, "prompt_msg_id": prompt.id}
        return

    if command == "rm":
        if len(message.command) < 2: return await message.reply_text("⚠️ 语法:`/rm [航线剧名关键字]` 或 `/rm all`")
        kw = " ".join(message.command[1:]).lower()
        routes = load_tg_routes()
        
        # 🔥 新增:全局清空模式
        if kw == "all":
            route_count = len(routes)
            if route_count == 0: return await message.reply_text("📭 航线记录已经是空的了。")
            routes.clear()
            save_tg_routes(routes)
            return await message.reply_text(f"🗑️ **[最高指令生效]** 已彻底清空全部 `{route_count}` 条记忆航线!")
            
        # 常规模糊匹配删除模式
        matched_keys = [k for k in routes.keys() if kw in k.lower()]
        if not matched_keys: return await message.reply_text(f"❌ 没找到包含 `{kw}` 的航线")
        for k in matched_keys: del routes[k]
        save_tg_routes(routes)
        return await message.reply_text(f"🗑️ 已彻底删除 {len(matched_keys)} 条航线记录!")

    if command == "rmh":
        if len(message.command) < 2: return await message.reply_text("⚠️ `/rmh [剧名关键字]`")
        kw = " ".join(message.command[1:]).lower()
        history = load_history()
        matched_keys = [k for k in history.keys() if kw in k.lower()]
        if not matched_keys: return await message.reply_text(f"❌ 历史库里没找到包含 `{kw}` 的记录")
        for k in matched_keys: del history[k]
        with open(TG_HISTORY_DB, 'w', encoding='utf-8') as f: json.dump(history, f, ensure_ascii=False, indent=2)
        return await message.reply_text(f"🗑️ 已抹除 {len(matched_keys)} 条历史记录!")

    if command == "history":
        history = load_history()
        if not history: return await message.reply_text("📭 历史账本目前是空的。")
        sorted_hist = sorted(history.items(), key=lambda x: x[1], reverse=True)[:15]
        msg_text = "📜 **[最近入库记录]**\n\n"
        for k, v in sorted_hist:
            time_str = datetime.fromtimestamp(v).strftime('%m-%d %H:%M')
            msg_text += f"🔹 `{k}`  *(于 {time_str})*\n"
        return await message.reply_text(msg_text)
    
    if command == "clh":
        config = load_listener_config()
        history = load_history()
        
        if not history: 
            return await message.reply_text("📭 历史账本目前是空的,无需清理。")
        
        # 1. 提取当前所有在追订阅的剧名基准 (剔除版本号后缀,并转换为历史记录的 '.' 格式)
        active_bases = set()
        for chat_id, info in config.get("trusted_channels", {}).items():
            for drama_key, d_info in info.get("monitored_dramas", {}).items():
                db_version = d_info.get("version", "")
                pure_drama = drama_key[:-len(f"_{db_version}")] if db_version and drama_key.endswith(f"_{db_version}") else drama_key
                active_bases.add(pure_drama.replace(" ", "."))
                
        # 2. 遍历历史记录,找出已退订或不存在的孤儿记录
        keys_to_delete = []
        for hist_key in history.keys():
            is_active = False
            for base in active_bases:
                # 兼容历史键值格式:剧集通常带 .SxxExx,电影可能是纯名或带版本号
                if hist_key.startswith(base + ".") or hist_key == base:
                    is_active = True
                    break
            if not is_active:
                keys_to_delete.append(hist_key)
                
        # 3. 执行物理抹除并落盘保存
        if not keys_to_delete:
            return await message.reply_text("✅ 历史账本很干净,所有记录均匹配当前订阅,无陈旧孤儿记录。")
            
        for k in keys_to_delete:
            del history[k]
            
        with open(TG_HISTORY_DB, 'w', encoding='utf-8') as f:
            json.dump(history, f, ensure_ascii=False, indent=2)
            
        return await message.reply_text(f"🧹 **[无情清道夫]** 交叉比对完毕!\n🗑️ 共抹除了 `{len(keys_to_delete)}` 条已退订或陈旧的历史记录。")

    if command == "add":
        if len(message.command) < 3: return await message.reply_text("⚠️ `/add [频道ID] [别名]`")
        c_id, c_name = message.command[1], " ".join(message.command[2:])
        if c_id not in config["trusted_channels"]: config["trusted_channels"][c_id] = {"monitored_dramas": {}}
        config["trusted_channels"][c_id]["channel_name"] = c_name
        with open(TG_LISTENER_DB, "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=4)
        return await message.reply_text(f"🏢 **频道挂载成功**: {c_name}")

    if command == "del":
        if len(message.command) < 2: return
        if message.command[1] in config["trusted_channels"]:
            del config["trusted_channels"][message.command[1]]
            with open(TG_LISTENER_DB, "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=4)
            return await message.reply_text("🗑️ 频道已拔除。")

    if command == "list":
        msg_text = "📡 **[雷达大盘状态]**\n\n"
        for chat_id, info in config.get("trusted_channels", {}).items():
            msg_text += f"🏢 **频道**: {info.get('channel_name', chat_id)}\n"
            dramas = info.get("monitored_dramas", {})
            if not dramas: msg_text += "  └ 📭 (空)\n"
            else:
                for d_name, d_info in dramas.items(): 
                    f_season = int(d_info.get('file_season', 1))
                    min_mb = d_info.get('min_mb', 0)
                    max_mb = d_info.get('max_mb', 999999)
                    freq = d_info.get('frequency', '日更')
                    end_ep = d_info.get('end_ep', 9999)
                    search_kw = d_info.get('search_kw', d_name)
                    ver = d_info.get('version', '')
                    e139 = " [139双传]" if d_info.get('enable_139') else ""
                    s_text = f"S{f_season:02d}"
                    size_txt = f" / {min_mb}-{max_mb if max_mb != 999999 else '上限'}MB" if (min_mb or max_mb != 999999) else ""
                    end_txt = f" / 🏁{end_ep}集" if end_ep != 9999 else " / ♾️连载"
                    ver_txt = f" / 🏷️{ver}" if ver else ""
                    line = f"  └ 🎬 `{search_kw}` ({s_text}{size_txt} / ⏱️{freq}{end_txt}{ver_txt}){e139}\n"
                    if len(msg_text) + len(line) > 4000:
                        await message.reply_text(msg_text)
                        msg_text = ""
                    msg_text += line
        if msg_text: await message.reply_text(msg_text)
        return

    if command == "scan":
        GLOBAL_STOP_SWEEP = False
        GLOBAL_CANCEL_TASKS.discard("ALL")  # 🔥 修复补丁:启动扫荡前,强制清空全局追杀标记
        args = message.command[1:]
        target_kw = " ".join(args).lower() if args else None
        await message.reply_text("🔍 **[手动扫荡触发]** ➔ 正在全自动翻找目标漏网之鱼...")
        for chat_id, info in config.get("trusted_channels", {}).items():
            for drama_name, d_info in info.get("monitored_dramas", {}).items():
                if target_kw and target_kw not in drama_name.lower(): continue
                folder_s = d_info.get("folder_season")
                file_s = d_info.get("file_season")
                if folder_s is None: folder_s = file_s if file_s is not None else 1
                if file_s is None: file_s = folder_s
                min_ep = d_info.get("min_ep", 1)
                cat = d_info.get("category", "未分类")
                min_mb = d_info.get("min_mb", 0)
                max_mb = d_info.get("max_mb", 999999)
                d_year = d_info.get("year")
                asyncio.create_task(sweep_existing_history(client, chat_id, drama_name, cat, folder_s, file_s, min_ep, min_mb, max_mb, fetch_limit=200, year=d_year))
        return

    if command == "sub":
        args = message.command[1:]
        if len(args) < 2: return await message.reply_text("⚠️ 语法:`/sub [剧名] [分类] [频率] [v=版本] [f=参数] [end=完结集数] [可选:年份/季数] [c139]`")
        
        # 🔥 拦截 c139 标识
        enable_139 = False
        for i in range(len(args)-1, -1, -1):
            if args[i].lower() == "c139":
                enable_139 = True
                args.pop(i)

        cat_idx = next((i for i, arg in enumerate(args) if arg in STANDARD_CATS), -1)
        if cat_idx == -1: return await message.reply_text("⚠️ 请提供有效分类。")
        category = args.pop(cat_idx)
        
        freq_list = ["日更", "周一", "周二", "周三", "周四", "周五", "周六", "周日"]
        frequency = "日更"
        for i, arg in enumerate(args):
            if arg in freq_list: frequency = args.pop(i); break
        
        min_mb, max_mb = 0, 999999
        size_idx = next((i for i, arg in enumerate(args) if re.match(r'^\d+-\d+$', arg)), -1)
        if size_idx != -1:
            size_str = args.pop(size_idx)
            min_mb, max_mb = map(int, size_str.split('-'))
        
        replied = message.reply_to_message
        if replied and replied.forward_from_chat:
            specific_channel = str(replied.forward_from_chat.id)
            channel_title = replied.forward_from_chat.title or specific_channel
            if specific_channel not in config.setdefault("trusted_channels", {}):
                config["trusted_channels"][specific_channel] = {"channel_name": channel_title, "monitored_dramas": {}}
        else:
            specific_channel = next((args.pop(i) for i, arg in enumerate(args) if arg.startswith("-100")), None)
            
        if not specific_channel:
            return await message.reply_text("⚠️ **安全拦截**:未指定目标频道!\n为防止多频道标签(f=)相互覆盖导致串台,请**回复该频道的一条消息**发送 `/sub`,或在参数中指定 `-100xxx` 频道ID。")
            
        target_pools = [specific_channel]

        custom_year = next((args.pop(i) for i, arg in enumerate(args) if arg.isdigit() and len(arg) == 4 and (1900 < int(arg) < 2100)), None)
        version_suffix, file_suffix, end_ep = "", "", None
        for i in range(len(args)-1, -1, -1):
            if args[i].lower().startswith("v="): version_suffix = args.pop(i)[2:]
            elif args[i].lower().startswith("f="): file_suffix = args.pop(i)[2:]
            elif args[i].lower().startswith("end="): end_ep = int(args.pop(i)[4:])

        file_season, folder_season, min_ep = None, None, 1
        for i in range(len(args)-1, -1, -1):
            if re.match(r'^s\d+$', args[i], re.IGNORECASE): file_season = int(args.pop(i)[1:])
            elif re.match(r'^e\d+$', args[i], re.IGNORECASE): min_ep = int(args.pop(i)[1:])
                
        if args and args[-1].isdigit():
            val = int(args.pop(-1))
            if args and args[-1].isdigit():
                folder_season = int(args.pop(-1))
                min_ep = val
            else: min_ep = val
                
        if folder_season is None: folder_season = file_season if file_season is not None else 1
        if file_season is None: file_season = folder_season
        
        raw_input = " ".join(args) if args else "未知目标"
        if "|" in raw_input: search_kw, drama_name = [x.strip() for x in raw_input.split("|", 1)]
        else: search_kw = drama_name = raw_input

        drama_key = f"{drama_name}_{version_suffix}" if version_suffix else drama_name
        fetched_year, fetched_end = await fetch_tmdb_details(drama_name)
        if not custom_year: custom_year = fetched_year
        if end_ep is None: end_ep = 9999 if category in ["国漫", "日漫", "综艺", "短剧"] else fetched_end
                
        for chat_id in target_pools:
            if "monitored_dramas" not in config["trusted_channels"][chat_id]: config["trusted_channels"][chat_id]["monitored_dramas"] = {}
            config["trusted_channels"][chat_id]["monitored_dramas"][drama_key] = {
                "search_kw": search_kw, "version": version_suffix, "file_version": file_suffix, 
                "category": category, "folder_season": folder_season, "file_season": file_season, 
                "min_ep": min_ep, "min_mb": min_mb, "max_mb": max_mb, "frequency": frequency,
                "year": custom_year, "end_ep": end_ep, "enable_139": enable_139
            }
            
        with open(TG_LISTENER_DB, "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=4)
        end_display = "♾️无限连载" if end_ep == 9999 else f"{end_ep} 集杀青"
        e139_display = "\n🚀 **附加**: 已开启139云盘同步接力" if enable_139 else ""
        
        try:
            await asyncio.sleep(2.5)
            await message.delete()
        except: pass
        await message.reply_text(f"✅ **订阅成功**: `{drama_name}`\n🏷️ **追踪**: `{version_suffix or '默认'}`\n📁 **路径**: `Season {folder_season}` (命名: `S{file_season:02d}`)\n⚖️ **画质锁定**: `{min_mb}-{max_mb} MB`\n🏁 **完结**: `{end_display}`{e139_display}\n🚀 **启动扫荡!**")
        
        for chat_id in target_pools:
            asyncio.create_task(sweep_existing_history(client, chat_id, drama_key, category, folder_season, file_season, min_ep, min_mb, max_mb, fetch_limit=200, year=custom_year))
        return
    
    if command in ["unsub", "del"]:
        args = message.command[1:]
        if not args: return await message.reply_text("⚠️ 语法:`/unsub [剧名] [可选: v=版本号] [可选:-100...]`")
        specific_channel = next((arg for arg in args if arg.startswith("-100")), None)
        if specific_channel: args.remove(specific_channel)
        version_suffix = ""
        for i, arg in enumerate(args):
            if arg.lower().startswith("v="): version_suffix = args.pop(i)[2:]; break
        drama_name = " ".join(args)
        if not drama_name: return await message.reply_text("⚠️ 请输入剧名!")
        drama_key = f"{drama_name}_{version_suffix}" if version_suffix else drama_name
        deleted_count = 0
        target_pools = [specific_channel] if specific_channel else list(config.get("trusted_channels", {}).keys())
        for chat_id in target_pools:
            if chat_id in config.get("trusted_channels", {}) and "monitored_dramas" in config["trusted_channels"][chat_id]:
                if drama_key in config["trusted_channels"][chat_id]["monitored_dramas"]:
                    del config["trusted_channels"][chat_id]["monitored_dramas"][drama_key]
                    deleted_count += 1
                elif drama_name in config["trusted_channels"][chat_id]["monitored_dramas"]:
                    del config["trusted_channels"][chat_id]["monitored_dramas"][drama_name]
                    deleted_count += 1
        if deleted_count > 0:
            with open(TG_LISTENER_DB, "w", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=4)
            return await message.reply_text(f"✅ **精确打击成功**\n💣 目标: `{drama_key}`\n📉 共从 **{deleted_count}** 个频道中移除!")
        else: return await message.reply_text(f"⚠️ **未找到目标**: `{drama_key}`")

    if command == "go":
        args = message.command[1:]
        if not args: return await message.reply_text("⚠️ 语法:`/go [剧名] [分类] [年份] [v=版本] [季数] [c139]`")
        
        # 🔥 拦截 c139
        enable_139 = False
        for i in range(len(args)-1, -1, -1):
            if args[i].lower() == "c139":
                enable_139 = True
                args.pop(i)

        routes = load_tg_routes()
        version_suffix, file_suffix = "", ""
        for i in range(len(args)-1, -1, -1):
            if args[i].lower().startswith("v="): version_suffix = args.pop(i)[2:]
            elif args[i].lower().startswith("f="): file_suffix = args.pop(i)[2:]
        custom_year = next((args.pop(i) for i, arg in enumerate(args) if arg.isdigit() and len(arg) == 4 and (1900 < int(arg) < 2100)), None)
        cat_idx = next((i for i, arg in enumerate(args) if arg in STANDARD_CATS), -1)
        if cat_idx == -1:
            search_kw = " ".join(args).lower()
            matched_item = None
            for k, v in routes.items():
                if search_kw in k.lower() or search_kw in v.get("folder_name", "").lower():
                    matched_item = v; break
            if matched_item:
                folder_s = matched_item.get("folder_season", 1)
                file_s = matched_item.get("file_season", folder_s)
                GLOBAL_ROUTE_CACHE.update({"folder_name": matched_item["folder_name"], "category": matched_item["category"], "folder_season": folder_s, "file_season": file_s, "year": matched_item.get("year", ""), "version": matched_item.get("version", ""), "expire_time": time.time() + 300, "manual_ep": None, "enable_139": matched_item.get("enable_139", False)})
                
                try:
                    await asyncio.sleep(2.5)
                    await message.delete()
                except: pass
                return await message.reply_text(f"✅ 命中记忆航线!已锁定目标: `{matched_item['folder_name']}`")
            return await message.reply_text("⚠️ 未匹配到历史航线。")
        cat = args.pop(cat_idx)
        file_season, folder_season = None, None
        for i in range(len(args)-1, -1, -1):
            if re.match(r'^s\d+$', args[i], re.IGNORECASE): file_season = int(args.pop(i)[1:])
        if args and args[-1].isdigit(): folder_season = int(args.pop(-1))
        if folder_season is None: folder_season = file_season if file_season is not None else 1
        if file_season is None: file_season = folder_season 
        pure_title = " ".join(args)
        year, _ = await fetch_tmdb_details(pure_title)
        if custom_year: year = custom_year
        folder_name = f"{pure_title} ({year})" if year else pure_title
        if version_suffix: folder_name = f"{folder_name} {version_suffix}"
        routes[folder_name] = {"folder_name": folder_name, "category": cat, "folder_season": folder_season, "file_season": file_season, "year": year, "version": version_suffix, "file_version": file_suffix, "created_at": int(time.time()), "enable_139": enable_139}
        save_tg_routes(routes)
        GLOBAL_ROUTE_CACHE.update({"folder_name": folder_name, "category": cat, "folder_season": folder_season, "file_season": file_season, "year": year, "version": version_suffix, "file_version": file_suffix, "expire_time": time.time() + 360, "manual_ep": None, "enable_139": enable_139})
        
        try:
            await asyncio.sleep(2.5)
            await message.delete()
        except: pass
        e139_display = "\n🚀 **[已开启139双端接力]**" if enable_139 else ""
        return await message.reply_text(f"✅ 新航线已打通:\n📁 目标锁定: `{folder_name}`{e139_display}\n请直接转发视频!")

# 🔥 补丁:给磁力直贴功能也加上控制台专属锁,防止旧手机“跨界接单”
@app.on_message(filters.chat(COMMAND_CENTER_CHAT) & filters.text & filters.regex(r"(?i)^magnet:\?xt=") & filters.user("me"))
async def catch_magnet_link(client, message):
    magnet_link = message.text.strip()
    prompt = await message.reply_text("🧲 **[雷达警报] 已直接捕获磁力!**\n请直接回复本条消息提供归属信息:\n👉 **格式**: `剧名|别名 分类 [S季数] [年份] [v=版本] [c139] [proxy]`")
    chat_id = message.chat.id
    if chat_id not in PENDING_MAGNETS:
        PENDING_MAGNETS[chat_id] = {}
    PENDING_MAGNETS[chat_id][prompt.id] = {"link": magnet_link, "mag_msg_id": message.id, "prompt_msg_id": prompt.id}

# 🔥 补丁2:让所有的普通文字回复(比如回复配置信息)也只在对应的控制台里生效
@app.on_message(filters.chat(COMMAND_CENTER_CHAT) & filters.text & ~filters.media & filters.user("me"))
async def process_text_commands(client, message):
    text = message.text.strip().upper()
    if text.startswith("#") and GLOBAL_ROUTE_CACHE.get("expire_time", 0) > time.time():
        m_se = re.search(r'^#S(\d+)E(\d+)', text)
        m_s = re.search(r'^#S(\d+)$', text)
        m_e = re.search(r'^#E(\d+)$', text)
        reply_msg = ""
        if m_se:
            GLOBAL_ROUTE_CACHE["file_season"] = int(m_se.group(1)); GLOBAL_ROUTE_CACHE["manual_ep"] = int(m_se.group(2))
            reply_msg = f"🎛️ **重塑**: S{int(m_se.group(1)):02d}E{int(m_se.group(2)):02d}"
        elif m_s:
            GLOBAL_ROUTE_CACHE["file_season"] = int(m_s.group(1)); reply_msg = f"🎛️ **重塑**: S{int(m_s.group(1)):02d}"
        elif m_e:
            GLOBAL_ROUTE_CACHE["manual_ep"] = int(m_e.group(1)); reply_msg = f"🎛️ **重塑**: E{int(m_e.group(1)):02d}"
        if reply_msg: return await message.reply_text(reply_msg)

    chat_id = message.chat.id
    if chat_id in PENDING_MAGNETS and PENDING_MAGNETS[chat_id]:
        mag_data = None
        if message.reply_to_message and message.reply_to_message.id in PENDING_MAGNETS[chat_id]:
            mag_data = PENDING_MAGNETS[chat_id].pop(message.reply_to_message.id)
        elif not message.text.startswith("/"):
            first_key = list(PENDING_MAGNETS[chat_id].keys())[0]
            mag_data = PENDING_MAGNETS[chat_id].pop(first_key)
            
        if mag_data:
            if not PENDING_MAGNETS[chat_id]:
                del PENDING_MAGNETS[chat_id]
                
            config_text = message.text.strip()
            
            msgs_to_delete = [message.id, mag_data.get("prompt_msg_id")]
            if "mag_msg_id" in mag_data and mag_data["mag_msg_id"]:
                msgs_to_delete.append(mag_data["mag_msg_id"])
                
            async def delay_delete():
                await asyncio.sleep(10)
                try: await client.delete_messages(chat_id, [m for m in msgs_to_delete if m])
                except: pass
            asyncio.create_task(delay_delete())
            
            if "resume_gid" in mag_data:
                await handle_magnet_execution(client, message, magnet_link="", config_text=config_text, resume_gid=mag_data["resume_gid"])
            else:
                await handle_magnet_execution(client, message, mag_data["link"], config_text)
            return

@app.on_message(filters.video | filters.document)
@app.on_edited_message(filters.video | filters.document)
async def media_routing_gateway(client, message):
    try:
        config = load_listener_config()
        chat_id_to_check = str(message.chat.id) if message.chat else ""
        original_channel_id = ""
        parent_text = ""
        
        if message.forward_from_chat:
            original_channel_id = str(message.forward_from_chat.id)
        elif message.sender_chat:
            original_channel_id = str(message.sender_chat.id)

        if message.reply_to_message:
            parent = message.reply_to_message
            parent_text = parent.caption or parent.text or ""
            if not original_channel_id:
                if parent.forward_from_chat: original_channel_id = str(parent.forward_from_chat.id)
                elif parent.sender_chat: original_channel_id = str(parent.sender_chat.id) 
            if parent.reply_to_message: parent_text += f" {parent.reply_to_message.caption or parent.reply_to_message.text or ''}"

        matched_channel = None
        c_test = chat_id_to_check.replace("-100", "").strip("-") if chat_id_to_check else ""
        c_orig = original_channel_id.replace("-100", "").strip("-") if original_channel_id else ""
        
        for k in config.get("trusted_channels", {}):
            k_norm = str(k).replace("-100", "").strip("-")
            if c_test and k_norm == c_test: 
                matched_channel = k
                break
            if c_orig and k_norm == c_orig: 
                matched_channel = k
                break

        if matched_channel:
            channel_info = config["trusted_channels"][matched_channel]
            media = message.video or message.document
            text_to_scan = f"{message.caption or ''} {getattr(media, 'file_name', '')} {parent_text}"
            matched_routes = []
            for drama_key, route_info in channel_info.get("monitored_dramas", {}).items():
                if route_info.get("search_kw", drama_key).lower() in text_to_scan.lower(): matched_routes.append((drama_key, route_info))
            if not matched_routes: return
            
            best_match = None
            for key, route in matched_routes:
                if route.get("version", "") and route.get("version", "").lower() in text_to_scan.lower(): best_match = (key, route); break
            if not best_match:
                for key, route in matched_routes:
                    if not route.get("version", ""): best_match = (key, route); break
            if not best_match: return
            drama_key, route_info = best_match

            db_version, db_file_version = route_info.get("version", ""), route_info.get("file_version", "")
            search_kw = route_info.get("search_kw", drama_key)
            ep_num = extract_pure_episode(text_to_scan, drama_anchor=search_kw)
            if ep_num is not None and ep_num < route_info.get("min_ep", 1): return 
            
            min_mb, max_mb = route_info.get("min_mb", 0), route_info.get("max_mb", 999999)
            file_size_mb = media.file_size / (1024 * 1024) if getattr(media, "file_size", 0) else 0
            if not (min_mb <= file_size_mb <= max_mb): return 
            
            if db_file_version:
                combined_physical_tags = db_file_version.replace('.', '.')
                combined_history_tags = (db_file_version + "." + db_version).strip('.')
            else:
                auto_list = extract_media_tags(text_to_scan).split('.')
                res_list = [t for t in auto_list if t.lower() in ["2160p", "1080p", "720p", "4k", "8k"]]
                temp_tags = res_list + (db_version.split('.') if db_version else [])
                combined_physical_tags = ".".join(dict.fromkeys([t for t in temp_tags if t]))
                combined_history_tags = combined_physical_tags
            
            ver_tag_physical = f".{combined_physical_tags}" if combined_physical_tags else ""
            ver_tag_history = f".{combined_history_tags}" if combined_history_tags else ""
            
            folder_season = route_info.get("folder_season", route_info.get("file_season", 1))
            file_season = route_info.get("file_season", folder_season)
            year = route_info.get("year")
            if not year: year, _ = await fetch_tmdb_details(search_kw)
            
            pure_drama_name = drama_key[:-len(f"_{db_version}")] if db_version and drama_key.endswith(f"_{db_version}") else drama_key
            folder_name = f"{pure_drama_name} ({year})" if year else pure_drama_name
            if db_version: folder_name = f"{folder_name} {db_version}"  
            
            clean_base_for_check = pure_drama_name.replace(" ", ".")
            if check_history(clean_base_for_check, file_season, ep_num, combined_history_tags): return
            
            # 🔥 加入 enable_139
            enable_139 = route_info.get("enable_139", False)
            override_info = (folder_name, route_info["category"], folder_season, file_season, year, ep_num, db_version, db_file_version, matched_channel, drama_key, route_info.get("end_ep", 9999), enable_139)
            try: status = await client.send_message(COMMAND_CENTER_CHAT, f"🎯 **[实时发车]**\n📺 `{search_kw}` ➔ S{file_season:02d}E{ep_num:02d}")
            except: status = await client.send_message("me", f"🎯 **[备用发车]**\n📺 `{search_kw}` ➔ S{file_season:02d}E{ep_num:02d}")
            asyncio.create_task(process_media_transfer(client, message, status, override_info))
            return

        if message.chat and message.chat.type in [enums.ChatType.PRIVATE, enums.ChatType.BOT]:
            if time.time() > GLOBAL_ROUTE_CACHE["expire_time"] or not GLOBAL_ROUTE_CACHE["folder_name"]: return
            try: status = await message.reply_text("⚡ 转发航线认证通过,正向引流拉取...")
            except: return
            asyncio.create_task(process_media_transfer(client, message, status))
    except: pass

# =================================================================
# 🚀 TG 自动下载后台任务处理
# =================================================================
async def process_media_transfer(client, message, status, override_info=None):
    media = message.video or message.document
    if not media: return
    raw_file = getattr(media, "file_name", f"TG_{message.id}.mp4")
    _, ext = os.path.splitext(raw_file)
    if not ext: ext = ".mp4"
    
    src_chat_id = src_drama_key = src_end_ep = None
    if override_info:
        if len(override_info) >= 12: 
            folder, cat, folder_season, file_season, year, ep_num, version_suffix, file_suffix, src_chat_id, src_drama_key, src_end_ep, enable_139 = override_info[:12]
        elif len(override_info) >= 11:
            folder, cat, folder_season, file_season, year, ep_num, version_suffix, file_suffix, src_chat_id, src_drama_key, src_end_ep = override_info[:11]
            enable_139 = False
        elif len(override_info) >= 8: 
            folder, cat, folder_season, file_season, year, ep_num, version_suffix, file_suffix = override_info[:8]
            enable_139 = False
        else:
            folder, cat, folder_season, file_season, year, ep_num = override_info[:6]
            version_suffix = file_suffix = ""
            enable_139 = False
    else:
        folder, cat = GLOBAL_ROUTE_CACHE["folder_name"], GLOBAL_ROUTE_CACHE["category"]
        folder_season, file_season = GLOBAL_ROUTE_CACHE["folder_season"], GLOBAL_ROUTE_CACHE["file_season"]
        year = GLOBAL_ROUTE_CACHE.get("year", "")
        version_suffix = GLOBAL_ROUTE_CACHE.get("version", "")
        file_suffix = GLOBAL_ROUTE_CACHE.get("file_version", "") 
        enable_139 = GLOBAL_ROUTE_CACHE.get("enable_139", False)
        if GLOBAL_ROUTE_CACHE["manual_ep"] is not None:
            ep_num = GLOBAL_ROUTE_CACHE["manual_ep"]
            GLOBAL_ROUTE_CACHE["manual_ep"] += 1
        else: ep_num = extract_pure_episode(f"{message.caption or ''} {raw_file}")
            
    text_to_scan = f"{message.caption or ''} {raw_file}"
    auto_tags = extract_media_tags(text_to_scan)
    is_movie = "电影" in cat or cat in ["演唱会", "纪录片"]
    movie_part_tag = extract_movie_part(text_to_scan) if is_movie else ""
    
    clean_base = re.sub(r'\s*\(\d{4}\)', '', folder).strip()
    if version_suffix and clean_base.endswith(version_suffix): clean_base = clean_base[:-len(version_suffix)].strip()
    clean_base = clean_base.replace(" ", ".")
    
    auto_list = auto_tags.split('.') if auto_tags else []
    f_list = file_suffix.split('.') if file_suffix else []
    res_list = [t for t in auto_list if t.lower() in ["2160p", "1080p", "720p", "4k", "8k"]]
    other_auto = [t for t in auto_list if t not in res_list]

    raw_physical_tags = []
    if movie_part_tag: raw_physical_tags.append(movie_part_tag) 
    raw_physical_tags.extend(res_list); raw_physical_tags.extend(f_list); raw_physical_tags.extend(other_auto) 
    final_physical_tags = []
    seen_physical = set()
    for t in raw_physical_tags:
        if t and t.lower() not in seen_physical: final_physical_tags.append(t); seen_physical.add(t.lower())
    combined_physical_tags = ".".join(final_physical_tags)
    ver_tag_physical = f".{combined_physical_tags}" if combined_physical_tags else ""
    
    raw_history_tags = []
    if movie_part_tag: raw_history_tags.append(movie_part_tag)
    raw_history_tags.extend(res_list); raw_history_tags.extend(f_list)   
    if version_suffix: raw_history_tags.extend(version_suffix.split('.'))
    final_history_tags = []
    seen_history = set()
    for t in raw_history_tags:
        if t and t.lower() not in seen_history: final_history_tags.append(t); seen_history.add(t.lower())
    combined_history_tags = ".".join(final_history_tags)
    ver_tag_history = f".{combined_history_tags}" if combined_history_tags else ""
    
    current_mount = get_mount_root()
    if is_movie:
        standard_name = f"{clean_base}.{year}{ver_tag_physical}{ext}" if year else f"{clean_base}{ver_tag_physical}{ext}"
        target_dir_189 = f"{current_mount}/{cat}/{folder}"
    else:
        if ep_num is None:
            await notify_steward_log(f"⚠️ [过滤跳过] `{clean_base}` 未提取到集数(疑似预告),已过滤。")
            try: await status.edit_text(f"⚠️ `{clean_base}` 未能提取到集数,已被防垃圾机制拦截!")
            except: pass
            return
        if check_history(clean_base, file_season, ep_num, combined_history_tags):
            hist_key = f"{clean_base}.S{file_season:02d}E{ep_num:02d}{ver_tag_history}"
            await notify_steward_log(f"🛡️ [账本拦截] `{hist_key}` 账本中已存在,自动销毁重复发车!")
            try: await status.delete() 
            except: pass
            return
        ep_str = f"{ep_num:02d}"
        target_dir_189 = f"{current_mount}/{cat}/{folder}/Season {folder_season}"
        standard_name = f"{clean_base}.S{file_season:02d}E{ep_str}.{year}{ver_tag_physical}{ext}" if year else f"{clean_base}.S{file_season:02d}E{ep_str}{ver_tag_physical}{ext}"
        
    task_lock_key = f"{clean_base}.S{file_season:02d}E{ep_num:02d}{ver_tag_history}" if not is_movie else f"{clean_base}{ver_tag_history}"
    if task_lock_key in GLOBAL_ACTIVE_LOCKS:
        try: await status.delete()
        except: pass
        return
    
    GLOBAL_ACTIVE_LOCKS.add(task_lock_key)
    bg_task_spawned = False
    downloaded_bytes = 0

    try:
        local_temp_name = f"[{version_suffix}]_{standard_name}" if version_suffix else standard_name
        local_path = os.path.join(LOCAL_TEMP_DIR, local_temp_name)
        total_bytes = media.file_size
        file_size_mb = total_bytes / (1024 * 1024) if total_bytes else 0
        source_name = message.forward_from_chat.title if message.forward_from_chat and message.forward_from_chat.title else (message.chat.title if message.chat and message.chat.title else "未知来源")
            
        try: await status.edit_text(f"⏳ **[TG下载排队中]** `{standard_name}`\n🚦 正在等待下载通道空闲...")
        except: pass

        async with GLOBAL_TRANSFER_LOCK:
            await notify_steward_log(f"📥 [TG拉取启动] 来源: {source_name} | 大小: {file_size_mb:.2f} MB")
            chunk_size = 1024 * 1024
            retry_count = 0
            max_retries = 999999  
            last_stuck_chunks = -1
            stuck_loop_count = 0
            last_ui_time = time.time()
            last_ui_bytes = 0
            speed_text, eta_text = "0.00 MB/s", "计算中..."
            
            notified_50_dl = False
            notified_98_dl = False
            
            while True:
                if retry_count >= max_retries: break
                current_bytes = os.path.getsize(local_path) if os.path.exists(local_path) else 0
                if current_bytes > total_bytes:
                    try: os.remove(local_path)
                    except: pass
                    current_bytes = 0
                if current_bytes == total_bytes:
                    downloaded_bytes = current_bytes; break
                    
                completed_chunks = current_bytes // chunk_size
                secure_bytes = completed_chunks * chunk_size
                if secure_bytes > 0:
                    try:
                        with open(local_path, "r+b") as f: f.truncate(secure_bytes)
                    except: pass
                    downloaded_bytes = secure_bytes
                    open_mode = "ab"
                else: 
                    downloaded_bytes, completed_chunks = 0, 0
                    open_mode = "wb"
                    
                try:
                    buffer_queue = asyncio.Queue(maxsize=50)
                    writer_error = None
                    async def disk_writer():
                        nonlocal writer_error
                        try:
                            with open(local_path, open_mode) as f:
                                while True:
                                    chunk = await buffer_queue.get()
                                    if chunk is None: break 
                                    f.write(chunk); buffer_queue.task_done()
                        except Exception as e: writer_error = e

                    writer_task = asyncio.create_task(disk_writer())
                    yielded_any = False
                    
                    async for chunk in client.stream_media(message, offset=completed_chunks):
                        yielded_any = True
                        if task_lock_key in GLOBAL_CANCEL_TASKS or "ALL" in GLOBAL_CANCEL_TASKS: break 
                        if writer_error: raise writer_error 
                        await buffer_queue.put(chunk)
                        downloaded_bytes += len(chunk)
                        
                        if total_bytes > 0:
                            pct = int(downloaded_bytes * 100 / total_bytes)
                            
                            if pct >= 50 and not notified_50_dl:
                                asyncio.create_task(notify_steward_log(f"📥 [TG拉取] {standard_name} 进度达 50%"))
                                notified_50_dl = True
                            if pct >= 98 and not notified_98_dl:
                                asyncio.create_task(notify_steward_log(f"📥 [TG拉取] {standard_name} 进度达 98%"))
                                notified_98_dl = True

                        now = time.time()
                        duration = now - last_ui_time
                        if duration >= 8.0:
                            bytes_diff = downloaded_bytes - last_ui_bytes
                            speed_mb = (bytes_diff / duration) / (1024 * 1024)
                            speed_text = f"{speed_mb:.2f} MB/s"
                            rem_bytes = total_bytes - downloaded_bytes
                            speed_bps = bytes_diff / duration
                            if speed_bps > 0:
                                eta_sec = rem_bytes / speed_bps
                                eta_text = f"{int(eta_sec // 60)}分{int(eta_sec % 60)}秒" if eta_sec > 60 else f"{int(eta_sec)}秒"
                            else: eta_text = "卡顿"
                            pct = int(downloaded_bytes * 100 / total_bytes)
                            last_ui_time, last_ui_bytes = now, downloaded_bytes
                            try:
                                asyncio.create_task(status.edit_text(f"🚀 **[极速拉取]** `{standard_name}`\n📡 来源: **{source_name}** | ⚖️ 大小: **{file_size_mb:.2f} MB**\n📈 实时网速: **{speed_text}**\n⏳ 预计剩余: **{eta_text}**\n⚡ 涡轮进度: **{pct}%**"))
                            except: pass
                    
                    await buffer_queue.put(None)
                    await writer_task

                    if task_lock_key in GLOBAL_CANCEL_TASKS or "ALL" in GLOBAL_CANCEL_TASKS: break 
                    if downloaded_bytes >= total_bytes: 
                        try: await message.delete()
                        except: pass
                        break
                    else:
                        retry_count += 1
                        if not yielded_any:
                            try: message = await client.get_messages(message.chat.id, message.id)
                            except: pass
                        if completed_chunks == last_stuck_chunks:
                            stuck_loop_count += 1
                            try: await status.edit_text(f"⚠️ **[节点堵塞]** 断点死磕中 ({retry_count})...")
                            except: pass
                        else: last_stuck_chunks, stuck_loop_count = completed_chunks, 0
                        await asyncio.sleep(3.0)
                        
                except Exception as e:
                    if 'writer_task' in locals() and not writer_task.done(): writer_task.cancel()
                    retry_count += 1
                    if retry_count % 5 == 0:
                        try: message = await client.get_messages(message.chat.id, message.id)
                        except: pass
                    await asyncio.sleep(3.0)
                    
            if downloaded_bytes < total_bytes: 
                try:
                    if os.path.exists(local_path): os.remove(local_path)
                    await status.edit_text("❌ 任务已被彻底终止或残片销毁。")
                except: pass
                return

        try: await status.edit_text("✅ 本地落盘完成,进入云端上传排队队列...")
        except: pass

        if ext.lower() in [".cas", ".zip"]:
            import zipfile
            STAGING_DIR = "/data/data/com.termux/files/home/sharecas"
            os.makedirs(STAGING_DIR, exist_ok=True)
            final_cas_path = os.path.join(STAGING_DIR, standard_name.replace(ext, ".cas"))
            try:
                if ext.lower() == ".zip":
                    with zipfile.ZipFile(local_path, "r") as z:
                        cas_files = [f for f in z.namelist() if f.lower().endswith(".cas")]
                        if cas_files:
                            with open(final_cas_path, "wb") as f_out: f_out.write(z.read(cas_files[0]))
                else: shutil.move(local_path, final_cas_path)
                try: os.remove(local_path) 
                except: pass
                try: await status.edit_text(f"🎉 **CAS截留成功**\n已洗名并存入本地 `{STAGING_DIR}`,等待接管。")
                except: pass
                return
            except Exception as e: return
        
        asyncio.create_task(bg_upload_retry_task(
            local_path=local_path, 
            target_dir_189=target_dir_189, 
            cat=cat,
            folder_name=folder,
            folder_season=folder_season,
            total_bytes=total_bytes, 
            clean_base=clean_base, 
            file_season=file_season, 
            ep_num=ep_num, 
            is_movie=is_movie, 
            standard_name=standard_name, 
            history_tags=combined_history_tags,
            status_msg=status,
            enable_139=enable_139
        ))
        bg_task_spawned = True
        
        if src_chat_id and src_drama_key and src_end_ep and ep_num >= src_end_ep:
            config_end = load_listener_config()
            if src_chat_id in config_end.get("trusted_channels", {}) and src_drama_key in config_end["trusted_channels"][src_chat_id].get("monitored_dramas", {}):
                del config_end["trusted_channels"][src_chat_id]["monitored_dramas"][src_drama_key]
                try:
                    with open(TG_LISTENER_DB, "w", encoding="utf-8") as f: 
                        json.dump(config_end, f, ensure_ascii=False, indent=4)
                except: pass
                try: await client.send_message(COMMAND_CENTER_CHAT, f"🎉 **[圆满杀青]** `{src_drama_key}` 已达 {src_end_ep} 集完结线,自动解除监控!")
                except: pass
        
    finally:
        if not bg_task_spawned: GLOBAL_ACTIVE_LOCKS.discard(task_lock_key)
        GLOBAL_CANCEL_TASKS.discard(task_lock_key)

async def sweep_existing_history(client, chat_id, drama_key, category, folder_season, file_season, min_ep, min_mb=0, max_mb=999999, fetch_limit=200, year=None):
    try:
        chat_id_int = int(chat_id) if str(chat_id).lstrip('-').isdigit() else chat_id
        config = load_listener_config()
        d_info = config.get("trusted_channels", {}).get(str(chat_id), {}).get("monitored_dramas", {}).get(drama_key, {})
        search_kw = d_info.get("search_kw", drama_key)
        db_version, db_file_version, end_ep = d_info.get("version", ""), d_info.get("file_version", ""), d_info.get("end_ep", 9999)
        enable_139 = d_info.get("enable_139", False)
        
        if not year: year, _ = await fetch_tmdb_details(search_kw)
        
        global GLOBAL_STOP_SWEEP
        async for old_msg in client.get_chat_history(chat_id_int, limit=fetch_limit):
            if GLOBAL_STOP_SWEEP: break
            media = old_msg.video or old_msg.document
            if not media: continue
            text_to_scan = f"{old_msg.caption or ''} {getattr(media, 'file_name', '')}"
            if old_msg.reply_to_message: text_to_scan += f" {old_msg.reply_to_message.caption or old_msg.reply_to_message.text or ''}"
            
            if search_kw.lower() in text_to_scan.lower():
                skip_due_to_exclusive = False
                if not db_version:
                    for other_key, other_info in config.get("trusted_channels", {}).get(str(chat_id), {}).get("monitored_dramas", {}).items():
                        if other_info.get("version") and other_info["version"].lower() in text_to_scan.lower() and other_info.get("search_kw", "").lower() in text_to_scan.lower():
                            skip_due_to_exclusive = True; break
                if skip_due_to_exclusive: continue
                
                ep_num = extract_pure_episode(text_to_scan, drama_anchor=search_kw)
                if ep_num is None or ep_num < min_ep: continue
                file_size_mb = media.file_size / (1024 * 1024) if getattr(media, "file_size", 0) else 0
                if not (min_mb <= file_size_mb <= max_mb): continue
                
                auto_list = extract_media_tags(text_to_scan).split('.')
                res_list = [t for t in auto_list if t.lower() in ["2160p", "1080p", "720p", "4k", "8k"]]
                temp_tags = res_list + (db_file_version.split('.') if db_file_version else []) + (db_version.split('.') if db_version else [])
                preview_tags = ".".join(dict.fromkeys(temp_tags))
                
                pure_drama_name = drama_key[:-len(f"_{db_version}")] if db_version and drama_key.endswith(f"_{db_version}") else drama_key
                folder_name = f"{pure_drama_name} ({year})" if year else pure_drama_name
                if db_version: folder_name = f"{folder_name} {db_version}" 
                
                clean_base_for_check = pure_drama_name.replace(" ", ".")
                if check_history(clean_base_for_check, file_season, ep_num, preview_tags): continue
                
                override_info = (folder_name, category, folder_season, file_season, year, ep_num, db_version, db_file_version, str(chat_id), drama_key, end_ep, enable_139)
                try: status = await client.send_message(COMMAND_CENTER_CHAT, f"🎯 **[哨发发车]**\n📺 `{search_kw}` ({db_version or '默认'}) ➔ S{file_season:02d}E{ep_num:02d}")
                except: status = await client.send_message("me", f"🎯 **[备用嗅探]**\n📺 `{search_kw}` ({db_version or '默认'}) ➔ S{file_season:02d}E{ep_num:02d}")
                asyncio.create_task(process_media_transfer(client, old_msg, status, override_info))
                await asyncio.sleep(5) 
    except Exception as e: print(f"⚠️ [扫荡崩溃]: {e}")

async def smart_patrol_daemon(client):
    await asyncio.sleep(60) 
    while True:
        try:
            current_hour = datetime.now().hour
            if 0 <= current_hour < 9: await asyncio.sleep(3600); continue
            await asyncio.sleep(900)
            today_cn = {0: "周一", 1: "周二", 2: "周三", 3: "周四", 4: "周五", 5: "周六", 6: "周日"}[datetime.now().weekday()]
            config = load_listener_config()
            for chat_id, info in config.get("trusted_channels", {}).items():
                for drama_name, d_info in info.get("monitored_dramas", {}).items():
                    if d_info.get("frequency", "日更") not in ["日更", today_cn]: continue
                    f_s = d_info.get("file_season", d_info.get("folder_season", 1))
                    fo_s = d_info.get("folder_season", f_s)
                    asyncio.create_task(sweep_existing_history(client, chat_id, drama_name, d_info.get("category", "未分类"), fo_s, f_s, d_info.get("min_ep", 1), d_info.get("min_mb", 0), d_info.get("max_mb", 999999), 10, d_info.get("year")))
        except: await asyncio.sleep(60)

async def startup_catchup_sweep(client):
    await asyncio.sleep(5)
    try:
        config = load_listener_config()
        channels = config.get("trusted_channels", {})
        if not channels: return
        
        await notify_steward_log("🔍 [开机扫荡] 启动开机自检,正在扫描漏网之鱼...")
        try: await client.send_message(COMMAND_CENTER_CHAT, "🔍 **[系统自检]** 机器人重启,正在自动扫荡频道漏网之鱼...")
        except: pass

        for chat_id, info in channels.items():
            for drama_name, d_info in info.get("monitored_dramas", {}).items():
                f_s = d_info.get("file_season", d_info.get("folder_season", 1))
                fo_s = d_info.get("folder_season", f_s)
                await sweep_existing_history(client, chat_id, drama_name, d_info.get("category", "未分类"), fo_s, f_s, d_info.get("min_ep", 1), d_info.get("min_mb", 0), d_info.get("max_mb", 999999), 200, d_info.get("year"))
                await asyncio.sleep(5)
                
        await notify_steward_log("✅ [开机扫荡] 所有频道历史自检完成!")
    except: pass

# =================================================================
# 🚀 磁力任务断点接管协议 (开机激活)
# =================================================================
async def resume_magnet_tasks(client):
    tasks = load_magnet_tasks()
    if not tasks: return
    
    await notify_steward_log(f"🔄 [记忆恢复] 发现 {len(tasks)} 个未完成的磁力历史任务,正在尝试重新接管...")
    for gid, info in list(tasks.items()):
        try:
            async with httpx.AsyncClient(timeout=5.0) as h:
                res = await h.post(ARIA2_RPC_URL, json={"jsonrpc":"2.0", "id":"tg_bot", "method":"aria2.tellStatus", "params":[f"token:{ARIA2_RPC_SECRET}", gid]})
                res_json = res.json()
                if "error" in res_json:
                    continue
                
                GLOBAL_TRACKED_GIDS.add(gid)
                asyncio.create_task(handle_magnet_execution(client, None, info["magnet_link"], info["config_text"], resume_gid=gid))
                await asyncio.sleep(2)
        except Exception: pass

# =================================================================
# 🛸 野生任务捕获雷达 (自动逆向监控 Aria2)
# =================================================================
async def orphan_magnet_scanner(client):
    await asyncio.sleep(20) 
    while True:
        try:
            async with httpx.AsyncClient(timeout=10.0) as h:
                res_a = await h.post(ARIA2_RPC_URL, json={"jsonrpc":"2.0", "id":"tg_bot", "method":"aria2.tellActive", "params":[f"token:{ARIA2_RPC_SECRET}"]})
                res_w = await h.post(ARIA2_RPC_URL, json={"jsonrpc":"2.0", "id":"tg_bot", "method":"aria2.tellWaiting", "params":[f"token:{ARIA2_RPC_SECRET}", 0, 100]})
                
                tasks_a = res_a.json().get("result", []) if res_a.status_code == 200 else []
                tasks_w = res_w.json().get("result", []) if res_w.status_code == 200 else []
                all_tasks = tasks_a + tasks_w
                
                for t in all_tasks:
                    gid = t.get("gid")
                    belongs_to = t.get("belongsTo")
                    
                    is_bt = "bittorrent" in t or "infoHash" in t
                    if not is_bt: continue
                    
                    if gid in GLOBAL_TRACKED_GIDS or (belongs_to and belongs_to in GLOBAL_TRACKED_GIDS):
                        if gid not in GLOBAL_TRACKED_GIDS: 
                            GLOBAL_TRACKED_GIDS.add(gid)
                        continue
                        
                    if gid in ORPHAN_PROMPTED_GIDS: 
                        continue
                        
                    ORPHAN_PROMPTED_GIDS.add(gid)
                    
                    name = "未知磁力任务"
                    if "bittorrent" in t and "info" in t["bittorrent"]:
                        name = t["bittorrent"]["info"].get("name", name)
                    else:
                        name = f"种子元数据 (GID: {gid[:8]}...)"
                        
                    try:
                        prompt_msg = await client.send_message(
                            COMMAND_CENTER_CHAT, 
                            f"👽 **[野生任务捕获]** 发现外部添加的未监控下载任务:\n📄 `{name}`\n\n请**回复本条消息**(或直接在下方输入)提供归属信息以接管:\n👉 **格式**: `剧名|别名 分类 [S季数] [年份] [v=版本] [c139]`"
                        )
                        chat_id = prompt_msg.chat.id
                        if chat_id not in PENDING_MAGNETS:
                            PENDING_MAGNETS[chat_id] = {}
                        PENDING_MAGNETS[chat_id][prompt_msg.id] = {"resume_gid": gid, "prompt_msg_id": prompt_msg.id}
                    except: pass
        except Exception: pass
        await asyncio.sleep(60)

if __name__ == "__main__":
    clean_orphan_temp_files(max_age_hours=24) 
    load_listener_config()
    async def start_system():
        await app.start()
        
        # 🔥 新增:开机强制同步通讯录,彻底解决 Peer id invalid 报错
        try:
            await notify_steward_log("🔄 正在同步云端通讯录至本地数据库")
            async for _ in app.get_dialogs(limit=200):
                pass
        except Exception:
            pass
            
        await notify_steward_log("✅ 机器人重启完毕,双下载与上传引擎已全线上线!")
        asyncio.create_task(startup_catchup_sweep(app))
        asyncio.create_task(smart_patrol_daemon(app))
        asyncio.create_task(resume_magnet_tasks(app)) 
        asyncio.create_task(orphan_magnet_scanner(app)) 
        try: await app.send_message(COMMAND_CENTER_CHAT, "🤖 下载与上传推引擎已启动!")
        except: pass
        await idle()
        await app.stop()
    app.run(start_system())

也可以看看