Termux之auto189.py、casplay.py


安装自动转存与秒传播放脚本

一、初始化 Termux 环境

1.更新系统并安装基础工具

pkg update -y && pkg upgrade -y

2.安装Python与核心编译工具

pkg install python clang make libffi -y
pip install requests pycryptodome python-dotenv schedule

(如果提示 [Y/n],直接敲 y 回车)

3.安装必要的 Python 引擎库

pip install flask requests

4.库出错 这个 ModuleNotFoundError: No module named ‘Crypto’ 是 Python 界一个极其经典且烦人的“坑”,几乎所有第一次折腾加密库的人都会踩中 原因很简单:代码里调用的名字叫 Crypto,但它对应的现代库其实叫 pycryptodome。有时候 Python 环境会犯傻,或者之前残留了一些废弃的老库(比如老古董 pycrypto),导致它“认错人”了 直接复制下面这两行命令,依次在 Termux 里回车(第一行可能会提示未找到某些库,不用管,直接让它执行完):

pip uninstall crypto pycrypto pycryptodome -y
pip install pycryptodome

现在冒出来的这个 No module named ‘schedule’ 报错,是因为咱们最开始那步批量装库的时候,可能因为网络波动中断了,导致 schedule(用来做定时任务的库)没装上。

咱们现在就玩“打地鼠”,它缺啥咱们补啥!为了防止等会儿它再报别的库没装,咱们干脆把脚本需要的剩下几个第三方库一次性全补齐。 直接复制这行回车:

pip install schedule python-dotenv requests

5.其它补充

在 Termux 中安装第三方库经常需要现场编译,因此 clang(C/C++ 编译器)是必不可少的。

pkg install python -y
pkg install clang make cmake -y

安装关键的底层系统运行库(最容易报错的环节) 结合 auto_189(通常涉及 requests, 数据解析, 可能涉及 RSA/AES 加密登录)和 cas_server(认证服务端,通常需要密码学库、JWT 生成等),你需要安装以下底层依赖:

# 1. 密码学与安全连接底层库 (为 cryptography, pyOpenSSL, bcrypt 等库准备)
pkg install libffi openssl -y

# 2. 网页/XML 解析底层库 (为 lxml, beautifulsoup4 等库准备)
pkg install libxml2 libxslt -y

# 3. 图像处理底层库 (如果脚本包含验证码识别,通常需要 Pillow 库)
pkg install libjpeg-turbo zlib freetype -y

升级 pip 并安装 Python 依赖 底层库安装完毕后,就可以使用 pip 安装 Python 的第三方库了。建议先升级 pip:

python -m pip install --upgrade pip

手动安装以下核心库:

# 基础网络与服务端库 (如 Flask, FastAPI, Requests)
pip install requests flask uvicorn

# 加解密与认证库 (CAS Server 必备)
pip install cryptography pyjwt

# 网页解析 (Auto 189 必备)
pip install beautifulsoup4 lxml

注:在 Termux 中安装 cryptography 或 lxml 可能会花费较长时间(几分钟),因为它正在调用 clang 进行本地编译,请耐心等待,不要中断。

3.拼音库

pip install pypinyin

二、自动转存auto189

1.建立专属工作台与配置文件

# 1. 创建专属文件夹并进去

mkdir -p ~/189py/db
cd ~/189py

# 2. 创建环境变量文件并编辑
nano sys.env

执行完 nano sys.env 后,屏幕会变成黑底白字的编辑器。把下面这段内容修改成你自己的真实信息后,粘贴进去(注意等号两边不要有空格):

# 你的天翼云盘账号和密码
ENV_189_CLIENT_ID=17707372266
ENV_189_CLIENT_SECRET=1127&xxskY
# 你的 TG 机器人配置
ENV_TG_BOT_TOKEN=7548615667:AAHn0ls4aBPKBPI2-gpwykwVdEKd0ywOlsc
ENV_TG_ADMIN_USER_ID=-1002906711199

# 新增这行
ENV_TMDB_API_KEY=9c88e18e43543c8ff195c631aaa0d2fa

填完后,按 Ctrl + O(字母O),回车保存;然后按 Ctrl + X 退出。

3.auto189.py脚本

3.1在电脑中新建文件auto189.py内容如下:

import os
import sys
import json
import time
import requests
import urllib3
# 🚨 终极核武器:直接在底层网络库中物理阉割 IPv6,防止天翼云 IP 漂移拦截
urllib3.util.connection.HAS_IPV6 = False
import re
import subprocess
import random
import socket
from urllib import parse
from Crypto.Cipher import PKCS1_v1_5 as Cipher_pksc1_v1_5
from Crypto.PublicKey import RSA
import logging
import schedule
from dotenv import load_dotenv
from datetime import datetime
import threading
from flask import Flask, request

# ==========================================
# ⚙️ 全局核心变量与服务配置 (统一在这里修改,告别死固定地址)
# ==========================================
# --- 🔗 内部微服务与 API 地址 ---
API_5000_URL = "http://127.0.0.1:5000"  # 189管家服务地址
API_5244_URL = "http://127.0.0.1:5244"  # AList/OpenList 本地地址
OLIST_USER = "admin"                    # OpenList 账号
OLIST_PASS = "xxsky1127"                # OpenList 密码
TRIGGER_PORT = 5555                     # 本地监听触发端口

# --- 📂 本地物理路径与环境配置 (可在 settings.json 中覆盖) ---
DEFAULT_BASH_PATH = "/data/data/com.termux/files/usr/bin/bash"
DEFAULT_REFRESH_SH = "/data/data/com.termux/files/home/refresh.sh"
DEFAULT_LOCAL_DROPBOX = "/storage/emulated/0/Download/189cas"
DEFAULT_LOCAL_STRM = "/storage/emulated/0/Download/cas_strm"
DEFAULT_LOCAL_ROOT = "/storage/"  # 用来智能判定是否为本地物理路径的通用前缀

# ==========================================
# 🛡️ 网络底层与 IPv6 拦截
# ==========================================
old_getaddrinfo = socket.getaddrinfo
def new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
    responses = old_getaddrinfo(host, port, family, type, proto, flags)
    # 🌟 智能放行:如果 Flask 试图监听所有 IPv6 通配符,直接放行
    if host == '::':
        return responses
    # 🛡️ 强制锁定:外部请求(天翼云等)全部强杀 IPv6,只保留 IPv4 (AF_INET)
    return [res for res in responses if res[0] == socket.AF_INET]
socket.getaddrinfo = new_getaddrinfo

# ==========================================
# 🛡️ 基础配置与绝对路径定位 (防乱窜装甲)
# ==========================================
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_DIR = os.path.join(BASE_DIR, "db")
os.makedirs(DB_DIR, exist_ok=True)

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
logger = logging.getLogger(__name__)

# --- 📡 远程对讲机模块 (将日志实时发送给 Web 看板) ---
class RemoteLogHandler(logging.Handler):
    def emit(self, record):
        try:
            msg = self.format(record)
            requests.post(f"{API_5000_URL}/api/remote_log", 
                          json={'level': record.levelname, 'msg': msg}, timeout=0.3)
        except: pass

remote_handler = RemoteLogHandler()
remote_handler.setFormatter(logging.Formatter('%(message)s'))
logger.addHandler(remote_handler)

load_dotenv(dotenv_path=os.path.join(BASE_DIR, "sys.env"), override=True)

ENV_189_CLIENT_ID = os.getenv("ENV_189_CLIENT_ID", "")
ENV_189_CLIENT_SECRET = os.getenv("ENV_189_CLIENT_SECRET", "")
TG_BOT_TOKEN = os.getenv("ENV_TG_BOT_TOKEN", "")
TG_ADMIN_USER_ID = os.getenv("ENV_TG_ADMIN_USER_ID", "")

# ==========================================
# 🌟 精准局部代理通道 (只给境外 API 开小灶)
# ==========================================
LOCAL_PROXIES = {
    "http": "http://127.0.0.1:7890",
    "https": "http://127.0.0.1:7890"
}

# ==========================================
# 🎬 新增:TMDB 终极翻译与扩搜引擎 (带拼音与英文反查)
# ==========================================
TMDB_API_KEY = os.getenv("ENV_TMDB_API_KEY", "")

def get_tmdb_info(keyword):
    """输入中文,自动反查英文名、TMDB ID 和拼音特征"""
    if not TMDB_API_KEY: return None
    
    info = {
        "id": "",
        "cn_name": "",
        "en_name": "",
        "pinyin_full": "",
        "pinyin_initial": ""
    }
    
    # 1. 尝试生成拼音特征 (兼容没装 pypinyin 的情况)
    try:
        from pypinyin import pinyin, Style
        info["pinyin_full"] = "".join([p[0] for p in pinyin(keyword, style=Style.NORMAL)])
        info["pinyin_initial"] = "".join([p[0][0] for p in pinyin(keyword, style=Style.FIRST_LETTER)])
    except ImportError:
        pass # 如果没装库就不强求拼音

    # 2. 第一次请求:用中文去查,拿到正确的 ID
    url_cn = f"https://api.themoviedb.org/3/search/multi?api_key={TMDB_API_KEY}&language=zh-CN&query={parse.quote(keyword)}&page=1"
    try:
        res_cn = requests.get(url_cn, timeout=5, proxies=LOCAL_PROXIES).json()
        if res_cn.get("results"):
            top = res_cn["results"][0]
            media_type = top.get("media_type", "tv") # 判断是电影还是剧集
            tmdb_id = str(top.get("id"))
            info["id"] = tmdb_id
            info["cn_name"] = top.get("name") or top.get("title", "")
            
            # 3. 第二次请求:用拿到的 ID,伪装成英文环境再去问一次,榨出官方英文名!
            url_en = f"https://api.themoviedb.org/3/{media_type}/{tmdb_id}?api_key={TMDB_API_KEY}&language=en-US"
            res_en = requests.get(url_en, timeout=5).json()
            # 提取英文环境下的剧名
            info["en_name"] = res_en.get("name") or res_en.get("title") or top.get("original_name") or ""
            
            return info
    except Exception as e:
        logger.error(f"TMDB检索与反查异常: {e}")
        
    # 如果 TMDB 彻底没查到,把拼音特征传回去保底
    return info if (info["pinyin_full"] or info["pinyin_initial"]) else None

def translate_folder_name(folder_name):
    """提取文件夹名中的TMDB ID,翻译成人话(中文剧名)"""
    if not TMDB_API_KEY: return folder_name
    match = re.search(r'(?i)tmdb[-_]?(\d+)', folder_name)
    if not match: return folder_name
    
    tmdb_id = match.group(1)
    try:
        # 先按剧集查
        res_tv = requests.get(f"https://api.themoviedb.org/3/tv/{tmdb_id}?api_key={TMDB_API_KEY}&language=zh-CN", timeout=3, proxies=LOCAL_PROXIES).json()
        if "name" in res_tv:
            cn_name = res_tv["name"]
            s_match = re.search(r'(?i)(S\d+|Season\s*\d+)', folder_name)
            s_tag = f" {s_match.group(1)}" if s_match else ""
            return f"📺 {cn_name}{s_tag} (TMDB-{tmdb_id})"
            
        # 查不到再按电影查
        res_movie = requests.get(f"https://api.themoviedb.org/3/movie/{tmdb_id}?api_key={TMDB_API_KEY}&language=zh-CN", timeout=3, proxies=LOCAL_PROXIES).json()
        if "title" in res_movie:
            return f"🎬 {res_movie['title']} (TMDB-{tmdb_id})"
    except: pass
    return folder_name

def fetch_tmdb_rich_info(keyword):
    """通过 TMDB 获取影视剧的详细多维信息"""
    if not TMDB_API_KEY:
        return "❌ 系统未配置 TMDB_API_KEY,无法查询。"
        
    try:
        # 1. 基础搜索,先锁定是最贴合的那部戏
        url_search = f"https://api.themoviedb.org/3/search/multi?api_key={TMDB_API_KEY}&language=zh-CN&query={parse.quote(keyword)}&page=1"
        res = requests.get(url_search, timeout=5, proxies=LOCAL_PROXIES).json()
        if not res.get("results"):
            return f"📭 TMDB 数据库中未找到关于【{keyword}】的信息。"
            
        top = res["results"][0]
        media_type = top.get("media_type", "tv")
        tmdb_id = top.get("id")
        
        # 2. 根据 ID 拿取极其详尽的完整档案
        detail_url = f"https://api.themoviedb.org/3/{media_type}/{tmdb_id}?api_key={TMDB_API_KEY}&language=zh-CN"
        detail_res = requests.get(detail_url, timeout=5, proxies=LOCAL_PROXIES).json()
        
        # 3. 基础共用信息清洗
        title = detail_res.get("name") or detail_res.get("title", "未知")
        original_title = detail_res.get("original_name") or detail_res.get("original_title", "")
        overview = detail_res.get("overview", "暂无简介")
        if len(overview) > 200: overview = overview[:197] + "..." # 防止简介太长刷屏
        vote = round(detail_res.get("vote_average", 0), 1)
        genres = ", ".join([g["name"] for g in detail_res.get("genres", [])])
        country = ", ".join(detail_res.get("origin_country", []))
        
        # 状态汉化字典
        status_trans = {
            "Returning Series": "📺 连载中", "Ended": "✅ 已完结", 
            "Canceled": "❌ 已砍掉", "Released": "✅ 已上映", 
            "Post Production": "🛠 后期制作中", "In Production": "🎥 拍摄中"
        }
        raw_status = detail_res.get("status", "未知")
        status = status_trans.get(raw_status, raw_status)

        # 4. 根据类型分发组装 Telegram 卡片
        if media_type == "tv":
            first_air = detail_res.get("first_air_date", "未知")
            year = first_air[:4] if first_air != '未知' else '未知'
            seasons = detail_res.get("number_of_seasons", 0)
            episodes = detail_res.get("number_of_episodes", 0)
            
            msg = (
                f"📺 <b>{title} ({year})</b>\n"
                f"🏷 <b>原名:</b> {original_title}\n"
                f"🌍 <b>国家:</b> {country}\n"
                f"🎭 <b>类型:</b> {genres}\n"
                f"⭐ <b>评分:</b> {vote} / 10\n"
                f"🎬 <b>状态:</b> {status}\n"
                f"📚 <b>规模:</b> 共 {seasons} 季, {episodes} 集\n"
                f"🔗 <b>TMDB ID:</b> <code>{tmdb_id}</code>\n"
                f"────────────────\n"
                f"📖 <b>简介:</b>\n{overview}"
            )
        else:
            release_date = detail_res.get("release_date", "未知")
            year = release_date[:4] if release_date != '未知' else '未知'
            runtime = detail_res.get("runtime", 0)
            
            msg = (
                f"🎬 <b>{title} ({year})</b>\n"
                f"🏷 <b>原名:</b> {original_title}\n"
                f"🌍 <b>国家:</b> {country}\n"
                f"🎭 <b>类型:</b> {genres}\n"
                f"⭐ <b>评分:</b> {vote} / 10\n"
                f"⏳ <b>时长:</b> {runtime} 分钟\n"
                f"🎬 <b>状态:</b> {status}\n"
                f"🔗 <b>TMDB ID:</b> <code>{tmdb_id}</code>\n"
                f"────────────────\n"
                f"📖 <b>简介:</b>\n{overview}"
            )
        return msg
    except Exception as e:
        logger.error(f"TMDB 详情查询异常: {e}")
        return f"❌ 查询 TMDB 时发生异常,可能是网络超时。"

# ==========================================
# 📁 核心目录与挂载配置
# ==========================================
DIR_CAS_ROOT = "/177-秒传"
DIR_VIDEO_ROOT = "/177-视频"    # 🌟 新增:新版普通视频的专属智能路由根目录      
DIR_MEDIA_PREFIX = "/177-"      
OPENLIST_MOUNT_POINT = "177"    

SUBS_FILE = os.path.join(DB_DIR, "subscriptions.json")
HARVEST_SUBS_FILE = os.path.join(DB_DIR, "harvest_subs.json") # 🌟 新增收割专属库
HISTORY_FILE = os.path.join(DB_DIR, "history.json")
COOKIES_FILE = os.path.join(DB_DIR, "cookies.json")
SETTINGS_FILE = os.path.join(DB_DIR, "settings.json") # 🌟 新增配置保存

last_login_time = 0

# 🌟 分类字典 (保留你需要的路由分类)
CAT_ROUTER = {
    "华语剧": ("电视剧", "0-电视剧"), "大陆剧": ("电视剧", "0-电视剧"), "港剧": ("电视剧", "0-电视剧"), "台剧": ("电视剧", "0-电视剧"),
    "欧美剧": ("电视剧", "1-电视剧"), "美剧": ("电视剧", "1-电视剧"), "英剧": ("电视剧", "1-电视剧"),
    "日韩剧": ("电视剧", "2-电视剧"), "韩剧": ("电视剧", "2-电视剧"), "日剧": ("电视剧", "2-电视剧"),
    "华语电影": ("电影", "0-电影"), "国语电影": ("电影", "0-电影"),
    "欧美电影": ("电影", "1-电影"), "大片": ("电影", "1-电影"),
    "日韩电影": ("电影", "2-电影"),
    "国漫": ("动漫", "0-动漫"), 
    "日漫": ("动漫", "1-动漫"), "番剧": ("动漫", "1-动漫"),
    "综艺": ("综艺", ""), "纪录片": ("纪录片", ""), "演唱会": ("演唱会", ""), "短剧": ("短剧", "")
}

def load_json(filepath):
    if os.path.exists(filepath):
        with open(filepath, 'r', encoding='utf-8') as f:
            return json.load(f)
    return {}

def save_json(filepath, data):
    with open(filepath, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

if not os.path.exists(SETTINGS_FILE):
    save_json(SETTINGS_FILE, {"auto_scan_cas": False, "auto_check_subs": True})

def get_openlist_path(cloud189_path):
    clean_path = cloud189_path.strip("/")
    if clean_path.startswith(f"{OPENLIST_MOUNT_POINT}/") or clean_path == OPENLIST_MOUNT_POINT:
        return f"/{clean_path}"
    return f"/{OPENLIST_MOUNT_POINT}/{clean_path}"

def clean_filename(name):
    illegal_chars = '"\\/:*?|<>'
    for char in illegal_chars:
        name = name.replace(char, '')
    return name[:255]

# 🌟 将洗名函数提到全局,让收割库也能公用
def get_match_key(text): 
    clean = re.sub(r'[(\(\[\{]?\d{4}[)\)\]\}]?', '', text)
    clean = re.sub(r'(?i)\b(4k|1080p|2160p|web-dl|sdr|hdr)\b', '', clean)
    clean = re.sub(r'(完结|连载中|全\d+集|打包|修正)', '', clean)
    clean = re.sub(r'[^\w\u4e00-\u9fa5]', '', clean)
    return clean.lower()

def rsaEncrpt(password, public_key):
    rsakey = RSA.importKey(public_key)
    cipher = Cipher_pksc1_v1_5.new(rsakey)
    return cipher.encrypt(password.encode()).hex()

def generate_smart_name(original_filename, sub_path):
    # --- 🌟 核心一:侦测并保留 .mp4.cas 这种双后缀 ---
    valid_media_exts = ['.mp4', '.mkv', '.ts', '.avi', '.rmvb', '.flv', '.wmv', '.srt', '.ass']
    lower_name = original_filename.lower()
    final_ext = ""
    
    for me in valid_media_exts:
        if lower_name.endswith(f"{me}.cas"):
            final_ext = f"{me}.cas"  # 锁定双后缀
            break
            
    if not final_ext:
        _, ext = os.path.splitext(original_filename)
        if ext.lower() in valid_media_exts or ext.lower() == '.cas':
            final_ext = ext.lower()
            
    # 🚑 抢救瞎子文件:避开图片垃圾,剩下的强行套 .cas
    if not final_ext:
        _, ext = os.path.splitext(original_filename)
        if ext.lower() in ['.jpg', '.jpeg', '.png', '.nfo', '.txt', '.torrent', '.html']:
            return None
        final_ext = '.cas'
        
    path_parts = sub_path.strip('/').split('/')
    folder_name = path_parts[-1]
    for part in reversed(path_parts):
        if re.match(r'(?i)^Season\s*\d+$|^S\d+$', part.strip()):
            continue
        folder_name = part.strip()
        break
        
    year_in_path = re.search(r'\((\d{4})\)', folder_name)
    year_str = year_in_path.group(1) if year_in_path else ""
    
    # --- 🌟 核心修改:无情绞肉机 (以年份为界,一刀切断) ---
    clean_show_name = folder_name
    # 1. 只要碰到 (四位年份),把年份和它屁股后面的“所有任意标签”一刀剁掉
    clean_show_name = re.sub(r'\s*\(\d{4}\).*$', '', clean_show_name)
    # 2. 无年份兜底:如果没写年份,用常规黑名单兜底,顺便清理 tmdb 尾巴 (连带外面的花括号/方括号一起杀)
    clean_show_name = re.sub(r'(?i)[_\-\s]*(HQ|IQ|DV|4K|1080[pP]|720[pP]|2160[pP]|WEB-DL|HDR|SDR|HD|H\.?26[45]|x\.?26[45]|BluRay|Remux)[_\-\s]*', '', clean_show_name)
    clean_show_name = re.sub(r'(?i)[\[{\(]?tmdb[-_=]?\w+[\]}\)]?', '', clean_show_name)
    # 3. 修剪首尾垃圾字符,并用点号替换空格
    clean_show_name = re.sub(r'[-_\s]+$', '', clean_show_name).strip()
    clean_show_name = clean_show_name.replace(' ', '.') 
    
    # --- 🌟 核心二:精准提取你要的组合标签 (通杀带点号的 H.265/x.264) ---
    tags_match = re.findall(r'(?i)\b(1080p|2160p|4K|DV|HQ|HDR|SDR|IQ|H\.?26[45]|x\.?26[45])\b', original_filename)
    tags = []
    for t in tags_match:
        # 第一步:全部转大写,并且无情抹除中间的点号 (把 H.265 变成 H265)
        t_upper = t.upper().replace('.', '')
        
        # 第二步:照顾命名强迫症,统一规范大小写
        if t_upper == '1080P': t_upper = '1080p'
        elif t_upper == '2160P': t_upper = '2160p'
        elif t_upper == 'X264': t_upper = 'H264'
        elif t_upper == 'X265': t_upper = 'H265'
        
        if t_upper not in tags:
            tags.append(t_upper)
            
    tag_str = "." + ".".join(tags) if tags else ""

    # 🎬 电影/单次任务组装区 (这里自动接上了 tag_str)
    if any(k in sub_path for k in ["电影", "movie", "演唱会", "纪录片"]):
        part_match = re.search(r'(?i)(part\d+|cd\d+)', original_filename)
        part_str = f".{part_match.group(1).lower()}" if part_match else ""
        year_part = f".{year_str}" if year_str else ""
        return f"{clean_show_name}{year_part}{part_str}{tag_str}{final_ext}".replace('..', '.')

    # 📺 剧集/动漫组装区
    ep_patterns = [
        r'(?i)E(?:P)?\s*(\d+)', r'第\s*(\d+)\s*[集话期]',
        r'(?:\[|\()(\d+)(?:\]|\))', r'\s+0*(\d{1,3})\s*(?:\.|$)', r'^0*(\d{1,3})\s*(?:\.|$)'  
    ]
    ep_num = None
    for pattern in ep_patterns:
        match = re.search(pattern, original_filename)
        if match:
            ep_num = int(match.group(1))
            break
            
    if ep_num is None: return original_filename
    season_num = 1
    s_match_file = re.search(r'(?i)S0*(\d+)', original_filename)
    if s_match_file:
        season_num = int(s_match_file.group(1))
    else:
        s_match_path = re.search(r'(?i)Season\s*(\d+)', sub_path)
        if s_match_path:
            season_num = int(s_match_path.group(1))
            
    year_part = f".{year_str}" if year_str else ""
    
    return f"{clean_show_name}.S{season_num:02d}E{ep_num:02d}{year_part}{tag_str}{final_ext}".replace('..', '.')

# ==========================================
# 🌟 升级版 TelegramNotifier (兼容V4.8日志机制+交互按钮)
# ==========================================
class TelegramNotifier:
    def __init__(self, bot_token, user_id):
        self.bot_token = bot_token
        self.user_id = user_id
        self.base_url = f"https://api.telegram.org/bot{self.bot_token}/" if self.bot_token else None

    def send_message(self, message, reply_markup=None):
        clean_msg = message.replace('\n', '  |  ')
        logger.info(f"📤 [TG推送] {clean_msg}")
        if not self.bot_token: return None
        payload = {"chat_id": self.user_id, "text": message, "parse_mode": "HTML"}
        if reply_markup: payload["reply_markup"] = json.dumps(reply_markup)
        try:
            res = requests.post(f"{self.base_url}sendMessage", json=payload, timeout=10, proxies=LOCAL_PROXIES).json()
            return res.get("result", {}).get("message_id")
        except: return None

    def edit_message(self, message_id, text, reply_markup=None):
        if not self.bot_token: return
        payload = {"chat_id": self.user_id, "message_id": message_id, "text": text, "parse_mode": "HTML"}
        if reply_markup: payload["reply_markup"] = json.dumps(reply_markup)
        try: requests.post(f"{self.base_url}editMessageText", json=payload, timeout=10, proxies=LOCAL_PROXIES)
        except: pass

    def answer_callback(self, callback_query_id, text=""):
        if not self.bot_token: return
        try: requests.post(f"{self.base_url}answerCallbackQuery", json={"callback_query_id": callback_query_id, "text": text}, timeout=5, proxies=LOCAL_PROXIES)
        except: pass

class Cloud189ShareInfo:
    def __init__(self, fileId, shareId, shareMode, cloud189Client, accessCode="", is_folder=True, file_name=""):
        self.shareDirFileId = fileId
        self.shareId = shareId
        self.session = cloud189Client.session
        self.client = cloud189Client
        self.shareMode = shareMode
        self.accessCode = accessCode
        self.is_folder = is_folder
        self.file_name = file_name

    def getAllShareFiles(self, folder_id=None):
        if not self.is_folder and folder_id is None:
            return {"files": [{"id": self.shareDirFileId, "name": self.file_name}], "folders": []}
        if folder_id is None: folder_id = self.shareDirFileId
        fileList, folders = [], []
        pageNumber = 1
        while True:
            result = self.session.get("https://cloud.189.cn/api/open/share/listShareDir.action", params={
                "pageNum": pageNumber, "pageSize": "10000", "fileId": folder_id,
                "shareDirFileId": self.shareDirFileId, "isFolder": "true",
                "shareId": self.shareId, "shareMode": self.shareMode,
                "orderBy": "lastOpTime", "descending": "true", "accessCode": self.accessCode,
            }).json()
            if result.get('res_code', -1) != 0: break
            fileListAO = result.get("fileListAO", {})
            fileList += fileListAO.get("fileList", [])
            folders += fileListAO.get("folderList", [])
            if fileListAO.get("fileListSize", 0) == 0 and len(fileListAO.get("folderList", [])) == 0: break
            pageNumber += 1
        return {"files": fileList, "folders": folders}

    def saveShareFiles(self, tasksInfos, targetFolderId):
        try:
            response = self.session.post("https://cloud.189.cn/api/open/batch/createBatchTask.action", data={
                "type": "SHARE_SAVE", "taskInfos": json.dumps(tasksInfos, ensure_ascii=False),
                "targetFolderId": targetFolderId, "shareId": self.shareId,
            }).json()
            if response.get("res_code") != 0: return response.get('res_message', 'UNKNOWN_ERROR')
            taskId = response["taskId"]
            while True:
                res = self.session.post("https://cloud.189.cn/api/open/batch/checkBatchTask.action", data={
                    "taskId": taskId, "type": "SHARE_SAVE"
                }).json()
                if res["taskStatus"] != 3 or res.get("errorCode"): break
                time.sleep(1)
            return res.get("errorCode")
        except Exception as e: return str(e)

class Cloud189:
    def __init__(self):
        self.session = requests.session()
        self.session.headers = {
            'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            "Accept": "application/json;charset=UTF-8",
        }

    def load_cookies(self):
        if os.path.exists(COOKIES_FILE):
            try:
                with open(COOKIES_FILE, 'r') as f:
                    self.session.cookies.update(json.load(f))
                res = self.session.post("https://cloud.189.cn/api/portal/getObjectFolderNodes.action", data={"id": -11, "orderBy": 1, "order": "ASC"}).json()
                if not isinstance(res, dict): return True
            except: pass
        return False

    def save_cookies(self):
        with open(COOKIES_FILE, 'w') as f:
            json.dump(requests.utils.dict_from_cookiejar(self.session.cookies), f)

    def getEncrypt(self):
        return self.session.post("https://open.e.189.cn/api/logbox/config/encryptConf.do", data={'appId': 'cloud'}, timeout=15).json()['data']['pubKey']

    def getRedirectURL(self):
        rsp = self.session.get('https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https://cloud.189.cn/web/redirect.html?returnURL=/main.action', timeout=15)
        return parse.parse_qs(parse.urlparse(rsp.url).query)

    def login(self, username, password):
        if self.load_cookies():
            logger.info("🍪 [系统] 成功加载本地免密通行证,跳过高危密码登录!")
            return

        encryptKey = self.getEncrypt()
        query = self.getRedirectURL()
        resData = self.session.post('https://open.e.189.cn/api/logbox/oauth2/appConf.do', data={"version": '2.0', "appKey": 'cloud'}, headers={"Referer": 'https://open.e.189.cn/', "lt": query["lt"][0], "REQID": query["reqId"][0]}, timeout=15).json()
        keyData = f"-----BEGIN PUBLIC KEY-----\n{encryptKey}\n-----END PUBLIC KEY-----"
        data = {
            "appKey": 'cloud', "version": '2.0', "accountType": '01', "mailSuffix": '@189.cn',
            "returnUrl": resData['data']['returnUrl'], "paramId": resData['data']['paramId'],
            "clientType": '1', "isOauth2": "false",
            "userName": f"{{NRP}}{rsaEncrpt(username, keyData)}",
            "password": f"{{NRP}}{rsaEncrpt(password, keyData)}",
        }
        result = self.session.post('https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do', data=data, headers={'Referer': 'https://open.e.189.cn/', 'lt': query["lt"][0], 'REQID': query["reqId"][0]}, timeout=15).json()
        if result['result'] == 0:
            self.session.get(result['toUrl'], headers={"Host": 'cloud.189.cn'}, timeout=15)
            self.save_cookies()
        else: raise Exception(result['msg'])

    def getShareInfo(self, link):
        url = parse.urlparse(link)
        try: code = parse.parse_qs(url.query)["code"][0]
        except: code = url.path.split('/')[-1]
        pwd = parse.parse_qs(url.query).get('pwd', [''])[0]
        result = self.session.get("https://cloud.189.cn/api/open/share/getShareInfoByCodeV2.action", params={"shareCode": code}).json()
        
        res_code = str(result.get('res_code', ''))
        
        if res_code == 'ShareAuditWaiting' or 'audit waiting' in str(result).lower():
            raise Exception(f"SHARE_AUDIT: 天翼云官方审核风控拦截 [{result.get('res_message', '等待审核')}]")

        if res_code in ['8001', 'ShareNotFound', 'ShareAuditNotPass', 'ShareUserInvalid'] or \
           any(kw in str(result).lower() for kw in ["失效", "取消", "不存在", "审核", "invalid", "not found", "not pass"]):
            raise Exception(f"SHARE_DEAD: 分享已失效或被和谐 [{result.get('res_message', '未知原因')}]")
            
        if result.get('res_code') != 0: raise Exception(f"获取分享失败,可能掉线: {result}")
        file_id = result.get("fileId")
        share_mode = result.get("shareMode", 1)
        share_id = result.get("shareId")
        raw_is_folder = result.get("isFolder")
        is_folder = True if raw_is_folder is None else str(raw_is_folder).lower() in ['true', '1']
        file_name = result.get("fileName", "未命名文件")
        if pwd:
            verify_res = self.session.get("https://cloud.189.cn/api/open/share/checkAccessCode.action", params={"shareCode": code, "accessCode": pwd}).json()
            if verify_res.get('res_code') != 0: raise Exception(f"提取码错误或失效: {verify_res}")
            share_id = verify_res.get("shareId")
        if not share_id: raise Exception("未能获取到 shareId,疑似掉线拦截。")
        return Cloud189ShareInfo(file_id, share_id, share_mode, self, pwd, is_folder, file_name)

    def createFolder(self, name, parentFolderId=-11):
        result = self.session.post("https://cloud.189.cn/api/open/file/createFolder.action", data={"parentFolderId": parentFolderId, "folderName": name}).json()
        return result.get("id", result.get("fileId", "-11"))

    def getObjectFolderNodes(self, folderId=-11):
        res = self.session.post("https://cloud.189.cn/api/portal/getObjectFolderNodes.action", data={"id": folderId, "orderBy": 1, "order": "ASC"}).json()
        if isinstance(res, dict): raise Exception(f"获取目录被网盘拦截或风控: {res}")
        return res

    def mkdirAll(self, path, parentFolderId=-11):
        path = path.strip("/")
        if not path: return parentFolderId
        for name in path.split("/"):
            found = False
            for node in self.getObjectFolderNodes(parentFolderId):
                if node["name"] == name:
                    parentFolderId = node["id"]
                    found = True
                    break
            if not found:
                parentFolderId = self.createFolder(name, parentFolderId)
        return parentFolderId

    def listPrivateFiles(self, folderId):
        all_files = []
        page_num = 1
        while True:
            try:
                res = self.session.get("https://cloud.189.cn/api/open/file/listFiles.action", params={"folderId": folderId, "pageNum": page_num, "pageSize": 100}, timeout=10).json()
                if res.get("res_code") == 0:
                    file_list = res.get("fileListAO", {}).get("fileList", [])
                    if not file_list: break
                    all_files.extend(file_list)
                    page_num += 1
                else: break
            except Exception: break
        return all_files

    def renameFile(self, fileId, destFileName):
        try:
            res = self.session.post("https://cloud.189.cn/api/open/file/renameFile.action", data={"fileId": fileId, "destFileName": destFileName}).json()
            return res.get("res_code") == 0
        except: return False

# ==========================================
# 🤖 核心巡逻、更新检查系统
# ==========================================
def get_all_share_files_recursive(info, folder_id=None, current_path=""):
    all_files = []
    result = info.getAllShareFiles(folder_id)
    for f in result.get("files", []):
        f["full_path"] = current_path + "/" + f["name"]
        all_files.append(f)
    for folder in result.get("folders", []):
        new_path = current_path + "/" + folder["name"]
        all_files.extend(get_all_share_files_recursive(info, folder["id"], new_path))
    return all_files

def auto_relogin(client_obj, force=False):
    global last_login_time
    current_time = time.time()
    
    # 🌟 只有在非强制唤醒时,才受 30 分钟冷却锁限制
    if not force and (current_time - last_login_time < 1800):
        logger.warning("⏳ [系统] 检测到接口报错,防风控冷却锁生效,跳过登录!")
        return False
        
    logger.info("🔄 [系统] 触发保活机制:正在彻底重洗内存与协议握手...")
    try:
        # 💥 核心修复:彻底粉碎内存中残留的旧 Session 幽灵!
        client_obj.session = requests.session()
        client_obj.session.headers = {
            'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            "Accept": "application/json;charset=UTF-8",
        }
        if os.path.exists(COOKIES_FILE):
            os.remove(COOKIES_FILE)

        client_obj.login(ENV_189_CLIENT_ID, ENV_189_CLIENT_SECRET)
        last_login_time = time.time()
        logger.info("✅ [系统] 彻底洗牌重新登录成功!安全冷却锁已重置。")
        return True
    except Exception as e:
        logger.error(f"❌ [系统] 重新登录失败: {e}")
        return False

# ==========================================
# 🌟 独立新增的 CAS 收割模块 (终极暴力认亲 + 整季合并极速批次处理版)
# ==========================================
def process_cas_via_olist_api(specific_dir=None):
    s = load_json(SETTINGS_FILE)
    WATCH_DIRS = s.get("watch_dirs", ["/family/177_cas", "/local_cas"])
    
    # 🌟 如果有外部指定的精准目录,雷达就只扫这一个!
    if specific_dir:
        WATCH_DIRS = [specific_dir]
    NO_DELETE_DIRS = s.get("no_delete_dirs", [])
    
    # 🌟 新增:把个人云目录自动硬塞进巡逻大军里,去重防止扫两遍
    WATCH_DIRS = list(set(WATCH_DIRS + NO_DELETE_DIRS))
    processed_names = []
    if not WATCH_DIRS: return processed_names 

    # --- 1. 载入【收割专属账本】(防个人云二次复制) ---
    harvest_ledger_file = os.path.join(DB_DIR, "harvest_ledger.json")
    harvest_ledger = load_json(harvest_ledger_file)
    harvest_ledger_changed = False

    # --- 2. 载入【STRM静默账本】(防 5000 端口二次强刷) ---
    strm_ledger_file = os.path.join(DB_DIR, "strm_ledger.json")
    strm_ledger = load_json(strm_ledger_file)
    strm_ledger_changed = False

    try:
        r_log = requests.post(f"{API_5244_URL}/api/auth/login", json={"username": OLIST_USER, "password": OLIST_PASS}, timeout=10)
        login_res = r_log.json()
        r_log.close()
        if login_res.get("code") != 200:
            logger.error(f"❌ [登录] OList 登录失败: {login_res.get('message')}")
            return processed_names
        token = login_res["data"]["token"]
        headers = {"Authorization": token, "Content-Type": "application/json"}
    except Exception as e:
        logger.error(f"❌ [登录] OList 接口异常: {e}")
        return processed_names

    def scan_olist_dir(path):
        files = []
        try:
            r = requests.post(f"{API_5244_URL}/api/fs/list", json={"path": path, "password": "", "page": 1, "per_page": 3000, "refresh": True}, headers=headers, timeout=45)
            res = r.json()
            r.close()
            if res.get("code") != 200:
                logger.warning(f"⚠️ [扫描] 目录访问失败: {path} - {res.get('message')}")
                return files
            
            content = (res.get("data") or {}).get("content") or []
            for item in content:
                item_path = f"{path}/{item['name']}".replace("//", "/")
                if item["is_dir"]: 
                    files.extend(scan_olist_dir(item_path))
                else:
                    if item["name"].lower().endswith(".cas"):
                        # 【核心拦截】:如果文件已在收割账本,直接跳过扫描
                        if item["name"] in harvest_ledger:
                            logger.debug(f"⏭️ [跳过] 已记录于收割账本: {item['name']}")
                        else:
                            files.append({"name": item["name"], "dir": path, "full_path": item_path})
        except Exception as e:
            logger.warning(f"⚠️ [警告] 扫描目录 {path} 时 OpenList 接口超时无响应: {e}")
        return files

    cas_files = []
    for watch_dir in WATCH_DIRS:
        logger.info(f"🌾 [收割] 正在巡逻云端目录: {watch_dir}")
        cas_files.extend(scan_olist_dir(watch_dir))

    if not cas_files: 
        logger.info("ℹ️ [收割] 暂无需要处理的 CAS 文件")
        return processed_names

    subs = load_json(SUBS_FILE)
    current_ym = datetime.now().strftime("%Y%m")
    updated_paths = set()
    created_dirs = set()
    
    batch_groups = {} 
    for cas in cas_files:
        filename = cas["name"]
        raw_dir = cas["dir"]
        active_watch_dir = next((wd for wd in WATCH_DIRS if raw_dir.startswith(wd)), WATCH_DIRS[0])
        rel_dir = raw_dir.replace(active_watch_dir, "").strip("/")
        parts = rel_dir.split("/") if rel_dir else []
        
        category_key = None
        show_folder_name = parts[0] if parts else "未分类手动入库"
        if len(parts) > 1 and parts[0] in CAT_ROUTER:
            category_key, show_folder_name = parts[0], parts[1]
        elif "#" in show_folder_name:
            for cat in CAT_ROUTER.keys():
                if f"#{cat}" in show_folder_name:
                    category_key = cat
                    show_folder_name = show_folder_name.replace(f"#{cat}", "").strip()
                    break

        raw_season_match = re.search(r'(?i)Season\s*(\d+)|S(\d+)(?!\d)', raw_dir.split('/')[-1])
        if raw_season_match: true_season_num = int(raw_season_match.group(1) or raw_season_match.group(2))
        else:
            file_s_match = re.search(r'(?i)S0*(\d+)', filename)
            true_season_num = int(file_s_match.group(1)) if file_s_match else 1
            
        temp_ep_num = int(re.search(r'(?i)E(?:P)?\s*(\d+)', filename).group(1)) if re.search(r'(?i)E(?:P)?\s*(\d+)', filename) else 1
        
        search_key = get_match_key(show_folder_name)
        target_cloud_path = None
        is_tv_show = False
        
        ignore_words = {get_match_key(DIR_CAS_ROOT), get_match_key(DIR_VIDEO_ROOT), "season", "s"}
        for cat_key, (large_cat, sub_cat) in CAT_ROUTER.items():
            ignore_words.add(get_match_key(cat_key))
            ignore_words.add(get_match_key(large_cat))
            if sub_cat: ignore_words.add(get_match_key(sub_cat))

        best_match_path = None
        try:
            h_data = load_json(HARVEST_SUBS_FILE)
            if search_key in h_data: best_match_path = h_data[search_key]
        except Exception: pass

        if not best_match_path:
            db_possible_matches = []
            for sid, info_dict in subs.items():
                if isinstance(info_dict, dict):
                    db_path = info_dict.get("path", "")
                    if DIR_CAS_ROOT not in db_path: continue 
                    db_folders = db_path.split('/')
                    for idx, f_name in enumerate(db_folders):
                        pure_f = get_match_key(f_name)
                        if not pure_f or len(pure_f) < 2 or re.match(r'^\d{4,6}$', pure_f) or pure_f in ignore_words: continue
                        if search_key == pure_f: 
                            db_possible_matches.append("/".join(db_folders[:idx+1]))
                            break
            if db_possible_matches:
                db_possible_matches.sort(key=lambda p: (0 if p.split('/')[-1].lower() == show_folder_name.lower() else 1, len(p.split('/')[-1])))
                best_match_path = db_possible_matches[0]

        if not best_match_path:
            try:
                search_roots = []
                if category_key:
                    b_large, b_sub = CAT_ROUTER[category_key]
                    search_roots.append(get_openlist_path(f"{DIR_CAS_ROOT}/{b_large}/{b_sub}".strip("/").replace("//", "/")))
                else:
                    search_roots = [get_openlist_path(f"{DIR_CAS_ROOT}/动漫/0-动漫"), get_openlist_path(f"{DIR_CAS_ROOT}/电视剧/0-电视剧")]
                
                for root_path in search_roots:
                    r = requests.post(f"{API_5244_URL}/api/fs/list", json={"path": root_path, "password": "", "page": 1, "per_page": 1000, "refresh": True}, headers=headers, timeout=20)
                    if r.json().get("code") == 200:
                        ym_nodes = [item["name"] for item in (r.json().get("data") or {}).get("content", []) if item["is_dir"] and re.match(r'^\d{4,6}$', item["name"])]
                        ym_nodes.sort(reverse=True)
                        all_months_matches = []
                        for ym in ym_nodes:
                            ym_path = f"{root_path}/{ym}"
                            r2 = requests.post(f"{API_5244_URL}/api/fs/list", json={"path": ym_path, "password": "", "page": 1, "per_page": 1000, "refresh": True}, headers=headers, timeout=20)
                            if r2.json().get("code") == 200:
                                for item in (r2.json().get("data") or {}).get("content", []):
                                    if item["is_dir"] and search_key == get_match_key(item["name"]):
                                        all_months_matches.append({"ym": ym, "name": item["name"]})
                        if all_months_matches:
                            all_months_matches.sort(key=lambda x: (0 if x["name"].lower() == show_folder_name.lower() else 1, len(x["name"]), -int(x["ym"])))
                            best_share = all_months_matches[0]
                            best_match_path = f"{DIR_CAS_ROOT}/{b_large}/{b_sub}/{best_share['ym']}/{best_share['name']}".replace("//", "/")
                    if best_match_path: break
            except: pass

        if best_match_path:
            target_cloud_path = best_match_path
            if any(k in target_cloud_path for k in ["电视剧", "动漫", "短剧"]): is_tv_show = True
            
        if not target_cloud_path:
            if category_key:
                base_large, base_sub = CAT_ROUTER[category_key]
                target_cloud_path = f"{DIR_CAS_ROOT}/{base_large}/{base_sub}/{current_ym}/{show_folder_name}".replace("//", "/")
                if base_large in ["电视剧", "动漫", "短剧"]: is_tv_show = True
            else:
                if true_season_num > 1 or temp_ep_num > 1: target_cloud_path = f"{DIR_CAS_ROOT}/电视剧/0-电视剧/{current_ym}/{show_folder_name}"; is_tv_show = True
                else: target_cloud_path = f"{DIR_CAS_ROOT}/电影/0-电影/{current_ym}/{show_folder_name}"

        base_notify_path = target_cloud_path 
        if is_tv_show:
            if not re.search(r'(?i)/Season\s*\d+$', target_cloud_path): target_cloud_path = f"{target_cloud_path}/Season {true_season_num}"
        elif true_season_num > 1 or (len(parts) > 1 and "season" in parts[-1].lower()):
            if not re.search(r'(?i)/Season\s*\d+$', target_cloud_path): target_cloud_path = f"{target_cloud_path}/Season {true_season_num}"

        # ==========================================
        # 🌟 核心保护:个人云原盘直通,绝不洗名
        # ==========================================
        is_no_delete = any(nd in raw_dir for nd in NO_DELETE_DIRS)
        if is_no_delete:
            final_name = filename  # 个人云:原封不动,保护多版本特征
            logger.debug(f"🛡️ [保护] 识别到个人云专属文件,已跳过洗名: {filename}")
        else:
            final_name = generate_smart_name(filename, target_cloud_path) or filename
            
        final_target_dir = get_openlist_path(target_cloud_path)
        
        group_key = (raw_dir, final_target_dir, base_notify_path)
        if group_key not in batch_groups: batch_groups[group_key] = []
        batch_groups[group_key].append({"orig": filename, "final": final_name})

    for (raw_dir, final_target_dir, base_notify_path), file_items in batch_groups.items():
        if final_target_dir not in created_dirs:
            requests.post(f"{API_5244_URL}/api/fs/mkdir", json={"path": final_target_dir}, headers=headers).close()
            created_dirs.add(final_target_dir)
            logger.info(f"📁 [基建] 新建目录: {final_target_dir} ...")
            time.sleep(1.5)

        # 【核心分支】:判断免删除目录
        is_no_delete = any(nd in raw_dir for nd in NO_DELETE_DIRS)
        api_endpoint = "copy" if is_no_delete else "move"
        action_name = "复制" if is_no_delete else "移动"
        
        names_to_move = []
        for item in file_items:
            orig_n, final_n = item["orig"], item["final"]
            if orig_n != final_n:
                src_path = f"{raw_dir}/{orig_n}".replace("//", "/")
                requests.post(f"{API_5244_URL}/api/fs/rename", json={"name": final_n, "path": src_path}, headers=headers).close()
                names_to_move.append(final_n)
            else: names_to_move.append(orig_n)
                
        time.sleep(5.0)
        requests.post(f"{API_5244_URL}/api/fs/list", json={"path": raw_dir, "refresh": True}, headers=headers).close()

        logger.info(f"🚚 [{action_name}] 正在处理: {final_target_dir} ({len(names_to_move)}件)")
        r_mov = requests.post(f"{API_5244_URL}/api/fs/{api_endpoint}", json={"src_dir": raw_dir, "dst_dir": final_target_dir, "names": names_to_move}, headers=headers)
        mov_res = r_mov.json()
        r_mov.close()
        
        if mov_res.get("code") == 200:
            logger.info(f"✅ [{action_name}] 批量成功!")
            processed_names.extend(names_to_move)

            # 🌟 修复:严密隔离的双轨通知机制
            if is_no_delete:
                for name in names_to_move: harvest_ledger[name] = True
                harvest_ledger_changed = True
                logger.info(f"🛑 [个人云] 纯备份完成,绝不投递管家制造垃圾 STRM")
            else:
                need_sync = False
                for name in names_to_move:
                    if name in strm_ledger:
                        logger.info(f"🤫 [静默防重] 本地已有 STRM,拦截管家二次强刷: {name}")
                        del strm_ledger[name]
                        strm_ledger_changed = True
                    else:
                        need_sync = True
                        
                if need_sync:
                    updated_paths.add(base_notify_path)
                
        else:
            logger.error(f"❌ [{action_name}] 批量失败: {mov_res.get('message')}")
            # 单件降级处理,同样继承全部逻辑并隔离
            for name in names_to_move:
                single_res = requests.post(f"{API_5244_URL}/api/fs/{api_endpoint}", json={"src_dir": raw_dir, "dst_dir": final_target_dir, "names": [name]}, headers=headers).json()
                if single_res.get("code") == 200:
                    logger.info(f"✅ [{action_name}] 单件成功: {name}")
                    processed_names.append(name)
                    
                    if is_no_delete:
                        harvest_ledger[name] = True
                        harvest_ledger_changed = True
                        logger.info(f"🛑 [个人云单件] 纯备份完成,跳过 STRM 投递: {name}")
                    else:
                        if name in strm_ledger:
                            logger.info(f"🤫 [静默防重单件] 拦截管家二次强刷: {name}")
                            del strm_ledger[name]
                            strm_ledger_changed = True
                        else:
                            updated_paths.add(base_notify_path)

    # --- 循环结束,分别保存核销好的双账本 ---
    if harvest_ledger_changed:
        save_json(harvest_ledger_file, harvest_ledger)
    if strm_ledger_changed:
        save_json(strm_ledger_file, strm_ledger)

    if updated_paths:
        logger.info("⏳ [引擎] 物理归档结束,等待管家同步...")
        time.sleep(8)
        target_media_roots = set()
        for p in updated_paths:
            parts = p.split('/')
            media_root = ""
            for i, part in enumerate(parts):
                if part.lower().startswith("season"):
                    media_root = '/'.join(parts[:i])
                    break
            if not media_root:
                last_part = parts[-1]
                if '.' in last_part and any(last_part.lower().endswith(ext) for ext in ['.mp4', '.mkv', '.ts', '.iso', '.rmvb', '.avi', '.cas', '.strm']):
                    media_root = os.path.dirname(p)
                else: media_root = p
            if media_root: target_media_roots.add(media_root)
        
        # 从设置里读取你想要的模式,默认是老版的 "cas"
        strm_mode = s.get("189_strm_mode", "cas") 
        
        for media_root in target_media_roots:
            olist_p = get_openlist_path(media_root)
            try:
                # 🌟 这里的参数完美迎合了管家,把 mode 传过去
                requests.get(f"{API_5000_URL}/api/sync", params={"path": olist_p, "mode": strm_mode}, timeout=3).close()
                logger.info(f"🔄 [管家] 触发目录同步: {media_root} (模式: {strm_mode})")
            except Exception as e:
                logger.debug(f"⚠️ 通知目录强刷闪断: {e}")
                
    return processed_names

# 🌟 查重逻辑与自动订阅拉取
def check_subscriptions(client_obj, force_target_id=None, is_first_run=False, ignore_time=False):
    subs = load_json(SUBS_FILE)
    history = load_json(HISTORY_FILE)
    notifier = TelegramNotifier(TG_BOT_TOKEN, TG_ADMIN_USER_ID)
    if not subs: return
    
    global_emby_paths = set()
    global_cas_paths = set() 
    
    if not force_target_id:
        logger.info(f"🛸 [雷达] 全频段扫描启动,当前监控池共有 {len(subs)} 个挂载节点")
    
    for target_id, sub_info in list(subs.items()): 
        try:
            if force_target_id and str(target_id) != str(force_target_id): continue
                
            share_url = sub_info if isinstance(sub_info, str) else sub_info.get("url", "")
            keyword = "" if isinstance(sub_info, str) else sub_info.get("keyword", "")
            path = "" if isinstance(sub_info, str) else sub_info.get("path", "")
            freq = "" if isinstance(sub_info, str) else sub_info.get("freq", "")

            # 🌟 加入 ignore_time 参数
            if not force_target_id and not is_first_run and not ignore_time:
                if path:
                    now = datetime.now()
                    curr_h, curr_m, curr_w = now.hour, now.minute, now.weekday()

                    if freq == "剧迷":
                        if not ((10 <= curr_h < 12) or (18 <= curr_h < 24)): continue
                    elif freq == "周更" or "周更" in path or "动漫" in path:
                        target_weekday = sub_info.get("update_weekday", 5) if isinstance(sub_info, dict) else 5
                        valid_days = [target_weekday, (target_weekday+1)%7, (target_weekday+2)%7]
                        
                        is_am = (curr_h == 10 and curr_m >= 30) or (curr_h == 11)
                        is_pm = (curr_h >= 18 and curr_m >= 30) or (curr_h >= 19)
                        
                        if not (is_am or is_pm): 
                            continue 
                            
                        curr_week = now.strftime("%Y-%V")
                        curr_day = now.strftime("%Y-%m-%d")
                        last_week = sub_info.get("last_success_week", "")
                        last_day = sub_info.get("last_success_day", "")
                        
                        if curr_w in valid_days:
                            if last_week == curr_week and last_day != curr_day:
                                continue 
                            else:
                                pass
                        else:
                            continue
                    elif freq == "日更" or "日更" in path or "电视剧" in path or "剧" in path:
                        if curr_h < 18: continue
            
            logger.info(f"📡 [侦测] 核对上游动态节点: {path} ...")
            
            info = client_obj.getShareInfo(share_url)
            all_files = get_all_share_files_recursive(info)
            
            cloud_files = client_obj.listPrivateFiles(target_id)
            existing_names = {cf["name"] for cf in cloud_files}

            new_files = []
            for f in all_files:
                if str(f["id"]) in history: continue
                if keyword and not all(k in f["full_path"].lower() for k in keyword.lower().split()): continue
                
                smart_target_name = generate_smart_name(f["name"], path)
                if smart_target_name is None: continue
                
                # ====================================================
                # 🎯 原版的进阶集数去重逻辑
                # ====================================================
                is_duplicate = False
                if smart_target_name in existing_names or f["name"] in existing_names:
                    is_duplicate = True
                else:
                    core_match = re.search(r'\.S\d+E\d+', smart_target_name)
                    if core_match:
                        core_str = core_match.group(0)
                        for ex_name in existing_names:
                            if core_str in ex_name:
                                is_duplicate = True
                                break

                if is_duplicate:
                    history[str(f["id"])] = {"name": f["name"], "sub_id": str(target_id)}
                    continue
                # ====================================================
                
                new_files.append(f)

            if new_files:
                logger.info(f"🎯 [搬运] 锁定 {len(new_files)} 个增量更新文件,开始物理下发...")
                
                taskInfos = [{"fileId": f["id"], "fileName": clean_filename(f["name"]), "isFolder": 0} for f in new_files]
                
                # ====================================================
                # 🚀 终极防毒隔离装甲:批次下发 + 幸存者强行洗名抢救
                # ====================================================
                code = info.saveShareFiles(taskInfos, target_id)
                
                # 1. 无论返回成功还是特征码报错,都强行给云端 8 秒缓冲时间
                time.sleep(8)
                fresh_cloud_files = client_obj.listPrivateFiles(target_id)
                fresh_names = [cf["name"] for cf in fresh_cloud_files]
                
                actually_saved_count = 0
                saved_tasks = []
                failed_tasks = []

                # 2. 逐一核对实体盘,把真正成功存进去的“幸存者”捞出来
                for task in taskInfos:
                    orig_name = task["fileName"]
                    expected_smart_name = generate_smart_name(orig_name, path)
                    
                    if orig_name in fresh_names or (expected_smart_name and expected_smart_name in fresh_names):
                        history[str(task["fileId"])] = {"name": orig_name, "sub_id": str(target_id)}
                        actually_saved_count += 1
                        saved_tasks.append(task)
                    else:
                        failed_tasks.append(task)

                # 3. 只要有幸存者落地,立刻存入记忆并执行智能洗名!绝不放生!
                if actually_saved_count > 0:
                    save_json(HISTORY_FILE, history)
                    if isinstance(subs.get(str(target_id)), dict):
                        now_dt = datetime.now()
                        subs[str(target_id)]["last_update"] = int(time.time())
                        if freq == "周更" or "周更" in path or "动漫" in path:
                            subs[str(target_id)]["last_success_week"] = now_dt.strftime("%Y-%V")
                            subs[str(target_id)]["last_success_day"] = now_dt.strftime("%Y-%m-%d")
                        save_json(SUBS_FILE, subs)
                        
                    # ✨ 幸存者洗名重命名
                    renamed_files_list = []
                    for cf in fresh_cloud_files:
                        hist_info = history.get(str(cf["id"]))
                        orig_name = hist_info["name"] if hist_info else cf["name"]
                        new_name = generate_smart_name(orig_name, path)
                        
                        if new_name and cf["name"] != new_name:
                            if client_obj.renameFile(cf["id"], new_name):
                                renamed_files_list.append(new_name)
                                time.sleep(0.5) 
                                
                    # 播报落地战果
                    notifier.send_message(f"✅【追剧落地报告】\n🔗 来源: {share_url}\n📂 成功入库并洗名 {actually_saved_count} 个文件!")
                    if renamed_files_list:
                        if len(renamed_files_list) > 20:
                            rename_msg = "\n".join([f" └ {n}" for n in renamed_files_list[:20]]) + f"\n...等共 {len(renamed_files_list)} 个文件"
                        else:
                            rename_msg = "\n".join([f" └ {n}" for n in renamed_files_list])
                        notifier.send_message(f"✨ 云端洗名规范化完成:\n{rename_msg}")

                # 4. 如果遇到特征码拦截,且有文件没存上,触发精准报警
                if code not in [0, '0', None, False, '']:
                    has_transfer_error = True  # 锁死上一步跟你说的归档自杀行为
                    failed_msg = "\n".join([f" ❌ {t['fileName'][:30]}" for t in failed_tasks[:10]])
                    notifier.send_message(f"⚠️ 触发天翼云特征码拦截 (错误码: {code})!\n拦截/未存上的毒文件有 {len(failed_tasks)} 个:\n{failed_msg}")

                openlist_target_path = get_openlist_path(path)
                
                if path.startswith(DIR_CAS_ROOT) or path.startswith(DIR_CAS_ROOT.strip('/')): 
                    global_cas_paths.add(openlist_target_path) 
                else: 
                    global_emby_paths.add(openlist_target_path)
            else:
                save_json(HISTORY_FILE, history)
                logger.info(f"💤 [安静] {path} 暂无新资源发布。")

            if freq in ["完结", "单次", "电影"]:
                subs_for_update = load_json(SUBS_FILE)
                if str(target_id) in subs_for_update:
                    del subs_for_update[str(target_id)]
                    save_json(SUBS_FILE, subs_for_update)
                    
                    history_data = load_json(HISTORY_FILE)
                    old_len = len(history_data)
                    history_data = {k: v for k, v in history_data.items() if not (isinstance(v, dict) and str(v.get("sub_id")) == str(target_id))}
                    save_json(HISTORY_FILE, history_data)
                    cleaned_count = old_len - len(history_data)
                    
                    logger.info(f"🎉 [归档] 完结撒花:[{path}] 资源已全部归档,清空节点。")
                    notifier.send_message(f"🎉 完结撒花:[{path}] 资源已全部归档!\n✅ 自动解除订阅,并清理了 {cleaned_count} 条关联记忆。")

        except Exception as e:
            error_msg = str(e)
            
            if "SHARE_AUDIT" in error_msg:
                bad_path = path if path else "未知目录"
                logger.warning(f"⚠️ [风控] 遭遇官方拦截: 目录 [{bad_path}] 绑定的链接正在等待审核!引擎已跳过该故障节点。")
                continue

            logger.error(f"❌ [异常] 检查链路异常: {error_msg}")
            
            if "SHARE_DEAD" in error_msg:
                subs_for_update = load_json(SUBS_FILE)
                if str(target_id) in subs_for_update:
                    dead_path = subs_for_update[str(target_id)].get("path", "未知") if isinstance(subs_for_update[str(target_id)], dict) else "未知"
                    del subs_for_update[str(target_id)]
                    save_json(SUBS_FILE, subs_for_update)
                    notifier.send_message(f"❌ 警告:监测到订阅已失效!\n📁 目录: {dead_path}\n🗑️ 已为您清理记忆。")
                    
                history_data = load_json(HISTORY_FILE)
                history_data = {k: v for k, v in history_data.items() if not (isinstance(v, dict) and str(v.get("sub_id")) == str(target_id))}
                save_json(HISTORY_FILE, history_data)
                continue
            elif any(kw in error_msg for kw in ["掉线", "失败", "拦截", "风控", "UNKNOWN_ERROR", "unknown"]): 
                # 🌟 如果是未知的底层风控,先删 Cookie 保证重登能拿到新 Token
                if os.path.exists(COOKIES_FILE): os.remove(COOKIES_FILE)
                auto_relogin(client_obj, force=True)

    if global_cas_paths:
        s = load_json(SETTINGS_FILE)
        for p in global_cas_paths:
            try:
                requests.get(f"{API_5000_URL}/api/sync", params={"path": p}, timeout=3) 
                logger.info(f"⚡ [API] 成功向管家后方下发同步指令: {p}")
                notifier.send_message(f"✅ 管家同步指令已下发: {p}")
            except Exception as e: 
                logger.error(f"❌ [API] 管家服务无法联通: {e}")
                notifier.send_message(f"❌ 管家同步无响应: {e}")
            time.sleep(1) 
        
    for p in global_emby_paths:
        try:
            s = load_json(SETTINGS_FILE)
            bash_path = s.get("bash_path", DEFAULT_BASH_PATH)
            refresh_sh = s.get("refresh_sh_path", DEFAULT_REFRESH_SH)
            subprocess.Popen([bash_path, refresh_sh, p], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
            logger.info(f"⚡ [脚本] 成功唤醒原生 Emby 刷新: {p}")
            notifier.send_message(f"✅ Emby刷新指令已下发: {p}")
        except: pass
        time.sleep(2)

# ==========================================
# 📦 本地投递箱极速雷达 (全新独立模块,不干扰云端收割)
# ==========================================
def scan_local_dropbox(specific_dir=None):
    s = load_json(SETTINGS_FILE)
    # 🌟 坚决使用变量,绝不写死路径!
    dropbox_dir = s.get("local_dropbox_dir", DEFAULT_LOCAL_DROPBOX)
    local_strm_dir = s.get("local_strm_dir", DEFAULT_LOCAL_STRM)
    strm_mode = s.get("189_strm_mode", "cas")
    
    scan_target = specific_dir if specific_dir else dropbox_dir

    history_file = os.path.join(DB_DIR, "monitor_history.json")
    monitor_history = load_json(history_file)
    changed = False
    updated_dirs = set()

    keys_to_delete = [path for path in monitor_history.keys() if not os.path.exists(path)]
    for k in keys_to_delete:
        del monitor_history[k]
        changed = True

    # ==========================================
    # 🌟 核心防空包穿透雷达 (后台安全运行,最多等 15 秒)
    # ==========================================
    cas_files = []
    
    if specific_dir:
        logger.info(f"⏳ [投递箱] 启动智能穿透雷达,寻找 .cas 踪迹...")
        for _ in range(15):
            cas_files = [] 
            if os.path.isfile(scan_target) and scan_target.lower().endswith('.cas'):
                cas_files.append(scan_target.replace("\\", "/"))
            elif os.path.isdir(scan_target):
                for root, dirs, files in os.walk(scan_target):
                    for f in files:
                        if f.lower().endswith('.cas'):
                            cas_files.append(os.path.join(root, f).replace("\\", "/"))
            
            # 只要抓到哪怕一个 .cas 文件,说明文件落地了!
            if cas_files:
                time.sleep(1.0) # 给局域网 1 秒时间把内容写完,防空包
                break
            time.sleep(1.0) # 没找到就耐心等 1 秒再看一眼
    else:
        # 定时全量扫描
        if os.path.isfile(scan_target) and scan_target.lower().endswith('.cas'):
            cas_files.append(scan_target.replace("\\", "/"))
        elif os.path.isdir(scan_target):
            for root, dirs, files in os.walk(scan_target):
                for f in files:
                    if f.lower().endswith('.cas'):
                        cas_files.append(os.path.join(root, f).replace("\\", "/"))

    if not cas_files:
        logger.warning(f"⚠️ [投递失败] 扫描完毕,没发现任何 .cas 文件!(传输太慢或路径有误): {scan_target}")
        if changed: save_json(history_file, monitor_history)
        return

    # 🧠 开始智能算命
    for file_path in cas_files:
        try:
            file_stat = os.stat(file_path)
            size = file_stat.st_size
            mtime = file_stat.st_mtime
            filename = file_path.split("/")[-1]

            # 🛡️ 恢复严苛防重 + 🌟 新增:终极物理自动探针!
            if file_path in monitor_history:
                record = monitor_history[file_path]
                if record.get('mtime') == mtime and record.get('file_size') == size:
                    # 🌟 真正的自动化:亲自去硬盘查岗!
                    strm_dir = record.get('target_strm_dir', '')
                    strm_name = record.get('strm_name', '')
                    if strm_dir and strm_name:
                        strm_full_path = os.path.join(strm_dir, strm_name)
                        # 如果你没删,文件还在,就正常防重复跳过
                        if os.path.exists(strm_full_path):
                            continue  
                        else:
                            # 如果你把那个 strm 删了,它立马察觉,直接无视历史记忆!
                            logger.info(f"♻️ [自动重置] 雷达发现目标 STRM 已被物理删除,无视旧记忆,准备重新生成: {filename}")
                    else:
                        continue 

            logger.info(f"📥 [投递箱] 雷达锁定新目标: {file_path}")            

            rel_path = file_path.replace(dropbox_dir, "").strip("/")
            parts = rel_path.split("/")
            filename = parts[-1]

            category_key = parts[0] if len(parts) > 1 else None
            show_folder_name = filename.rsplit('.', 1)[0]

            local_season_num = None
            if len(parts) > 1:
                for part in reversed(parts[:-1]):
                    s_match_dir = re.match(r'(?i)^(?:season\s*|s)(\d+)$', part.strip())
                    if s_match_dir:
                        local_season_num = int(s_match_dir.group(1))
                        continue
                    show_folder_name = part.strip()
                    break

            # 这里的 clean_show_name 只用来传给管家作剧名记录,不影响最后生成的文件名
            clean_show_name = re.sub(r'\s*\(\d{4}\)', '', show_folder_name)
            clean_show_name = re.sub(r'(?i)[_\-\s]*(HQ|IQ|DV|4K|1080[pP]|720[pP]|2160[pP]|WEB-DL|HDR|SDR|H265|x265|BluRay|Remux)[_\-\s]*', '', clean_show_name)
            clean_show_name = re.sub(r'[-_\s]+$', '', clean_show_name).strip()

            current_ym = datetime.now().strftime("%Y%m")
            
            if os.path.exists(local_strm_dir):
                for root, dirs, files in os.walk(local_strm_dir):
                    if show_folder_name in dirs:
                        ym_match = re.search(r'/(\d{6})$', root.replace('\\', '/'))
                        if ym_match:
                            current_ym = ym_match.group(1)
                            break

            b_large, b_sub = "未分类", "0-未分类"
            if category_key and category_key in CAT_ROUTER:
                b_large, b_sub = CAT_ROUTER[category_key]
            else:
                for cat_k, (l, s) in CAT_ROUTER.items():
                    if cat_k in rel_path:
                        b_large, b_sub = l, s
                        break
                if b_large == "未分类": b_large, b_sub = "电视剧", "0-电视剧"

            virtual_cloud_path = f"{DIR_CAS_ROOT}/{b_large}/{b_sub}/{current_ym}/{show_folder_name}".replace("//", "/")
            
            if local_season_num is not None:
                season_num = local_season_num
            else:
                s_match_file = re.search(r'(?i)S0*(\d+)', filename)
                season_num = int(s_match_file.group(1)) if s_match_file else 1
                
            if b_large in ["电视剧", "动漫", "短剧"] or season_num > 1:
                virtual_cloud_path = f"{virtual_cloud_path}/Season {season_num}"

            # ==========================================
            # 🌟 修复 3:完全物理阉割洗名!绝对保留所有原版压制参数
            # ==========================================
            final_name = filename
            if final_name.lower().endswith('.cas'):
                strm_name = final_name[:-4] + ".strm"
            else:
                strm_name = final_name + ".strm"

            local_sub_dir = virtual_cloud_path.replace(DIR_CAS_ROOT, "").strip("/")
            target_local_dir = os.path.join(local_strm_dir, local_sub_dir).replace("\\", "/")

            payload = {
                "source_cas_path": file_path,
                "target_local_dir": target_local_dir,
                "strm_name": strm_name,
                "show_name": clean_show_name,
                "mode": strm_mode  # 🌟 修复 4:把模式参数完美传给 5000 端口!
            }

            try:
                res = requests.post(f"{API_5000_URL}/api/make_strm", json=payload, timeout=5)
                if res.status_code == 200:
                    
                    # 🌟 关键补丁:在局域网同步工具最终把 mtime 倒拨回去之后,重新抓取一次最真实的物理 mtime!
                    try:
                        final_mtime = os.stat(file_path).st_mtime
                    except:
                        final_mtime = mtime
                        
                    monitor_history[file_path] = {
                        "process_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                        "file_size": size,
                        "mtime": final_mtime, # 记录真正的最终同步时间
                        "target_strm_dir": target_local_dir,
                        "strm_name": strm_name,
                        "status": "success"
                    }
                    changed = True
                    updated_dirs.add(target_local_dir) 
                    
                    ledger_file = os.path.join(DB_DIR, "strm_ledger.json")
                    ledger = load_json(ledger_file)
                    ledger[final_name] = True
                    save_json(ledger_file, ledger)
                else:
                    logger.error(f"❌ [投递箱] API 拒绝: {res.text}")
            except Exception as e:
                logger.error(f"❌ [投递箱] 无法连接到 管家: {e}")

        except Exception as e:
            logger.error(f"❌ [投递箱] 解析异常: {file_path} - {e}")

    if changed:
        save_json(history_file, monitor_history)

    if updated_dirs:
        logger.info(f"🎬 投递箱批量生成完毕,开始聚合局部刷新 (共 {len(updated_dirs)} 个目录)")
        s = load_json(SETTINGS_FILE)
        bash_path = s.get("bash_path", DEFAULT_BASH_PATH)
        refresh_sh = s.get("refresh_sh_path", DEFAULT_REFRESH_SH)
        for d in updated_dirs:
            try:
                subprocess.Popen([bash_path, refresh_sh, d], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
                logger.info(f"✅ [聚合刷新] 已精准触发单剧目录刷新: {d}")
                time.sleep(0.5)
            except: pass

def main_control_loop(client_obj):
    offset = 0
    notifier = TelegramNotifier(TG_BOT_TOKEN, TG_ADMIN_USER_ID)
    wizard_states = {} # 🧠 新增:记忆向导状态的“大脑”

    logger.info("🚀 [系统] 引擎核心组件自检完毕,执行初次跃迁扫描...")
    check_subscriptions(client_obj, is_first_run=True)
    logger.info("✅ [系统] 预热完毕!引擎正式切入智能静默巡航模式。")

    def scheduled_task():
        settings = load_json(SETTINGS_FILE)
        logger.info("==========================================")
        logger.info("🛸 [系统] 定时唤醒:引擎升空,接管侦测作业...")
        if settings.get("auto_check_subs", True):
            check_subscriptions(client_obj)
        if settings.get("auto_scan_cas", False):
            process_cas_via_olist_api()
            
        # 👇 新增:本地投递箱极速雷达
        if settings.get("auto_scan_local", True):
            scan_local_dropbox()
        wait_min = random.randint(25, 45)
        logger.info(f"🛌 [系统] 航线巡逻结束。进入节电待机,距下次起飞还有 {wait_min} 分钟...")
        logger.info("==========================================")
        schedule.clear('patrol')
        schedule.every(wait_min).minutes.do(scheduled_task).tag('patrol')

    scheduled_task()
    schedule.every(6).hours.do(auto_relogin, client_obj)

    # ==========================================
    # 🌟 新增:跨月防断层,每日 23:30 强制保底收割
    # ==========================================
    def daily_force_harvest():
        logger.info("⏰ [定时任务] 触发 23:30 跨月保底强行收割...")
        notifier.send_message("⏰ 触发 23:30 每日保底收割,防止跨月断层...")
        
        # 无视设置开关,直接硬调用收割核心函数
        p_names = process_cas_via_olist_api()
        
        if p_names:
            msg_str = "\n".join([f" └ {n}" for n in p_names[:20]])
            if len(p_names) > 20: msg_str += f"\n...等共 {len(p_names)} 个文件"
            notifier.send_message(f"✅ 每日保底收割完成:\n{msg_str}")
        else:
            notifier.send_message("✅ 每日保底收割完成,投递箱/云端暂无新文件。")

    # 设定时间点
    schedule.every().day.at("23:30").do(daily_force_harvest)

    while True:
        schedule.run_pending()
        try:
            url = f"https://api.telegram.org/bot{TG_BOT_TOKEN}/getUpdates?offset={offset}&timeout=10"
            res = requests.get(url, timeout=15, proxies=LOCAL_PROXIES).json()
            if res.get('ok'):
                for item in res['result']:
                    offset = item['update_id'] + 1
                    
                    # ==========================================
                    # 🔘 核心交互菜单处理 (回调)
                    # ==========================================
                    if 'callback_query' in item:
                        cb = item['callback_query']
                        chat_id = cb['message']['chat']['id']
                        msg_id = cb['message']['message_id']
                        data = cb['data']
                        
                        if str(chat_id) != str(TG_ADMIN_USER_ID): continue
                        notifier.answer_callback(cb['id']) 
                        
                        if data == "wiz_cancel":
                            notifier.edit_message(msg_id, "🚫 <b>已取消订阅向导。</b>")
                            wizard_states.pop(chat_id, None)
                            continue
                            
                        # 第二步:选择完频率后,动态判断是否需要选【周几】!
                        if data.startswith("wiz_freq_"):
                            freq = data.split("_")[2]
                            wizard_states[chat_id]["freq"] = freq
                            
                            if freq == "周更":
                                kb = {"inline_keyboard": [
                                    [{"text": "周一", "callback_data": "wiz_day_周一"}, {"text": "周二", "callback_data": "wiz_day_周二"}, {"text": "周三", "callback_data": "wiz_day_周三"}],
                                    [{"text": "周四", "callback_data": "wiz_day_周四"}, {"text": "周五", "callback_data": "wiz_day_周五"}, {"text": "周六", "callback_data": "wiz_day_周六"}],
                                    [{"text": "周日", "callback_data": "wiz_day_周日"}, {"text": "自动/默认", "callback_data": "wiz_day_未知"}],
                                    [{"text": "❌ 取消", "callback_data": "wiz_cancel"}]
                                ]}
                                notifier.edit_message(msg_id, f"✅ 剧名: {wizard_states[chat_id]['title']}\n✅ 频率: {freq}\n\n<b>📅 请选择该剧的更新时间 (周几):</b>", kb)
                            else:
                                kb = {"inline_keyboard": [
                                    [{"text": "📺 华语剧", "callback_data": "wiz_cat_华语剧"}, {"text": "📺 欧美剧", "callback_data": "wiz_cat_欧美剧"}],
                                    [{"text": "🎬 华语电影", "callback_data": "wiz_cat_华语电影"}, {"text": "🎬 欧美电影", "callback_data": "wiz_cat_欧美电影"}],
                                    [{"text": "🐼 日漫/番剧", "callback_data": "wiz_cat_日漫"}, {"text": "🐼 国漫", "callback_data": "wiz_cat_国漫"}],
                                    [{"text": "📺 日韩剧", "callback_data": "wiz_cat_日韩剧"}, {"text": "🎬 日韩电影", "callback_data": "wiz_cat_日韩电影"}],
                                    [{"text": "🎤 综艺", "callback_data": "wiz_cat_综艺"}, {"text": "🎥 纪录片", "callback_data": "wiz_cat_纪录片"}],
                                    [{"text": "📱 短剧", "callback_data": "wiz_cat_短剧"}, {"text": "🎵 演唱会", "callback_data": "wiz_cat_演唱会"}],
                                    [{"text": "❌ 取消", "callback_data": "wiz_cancel"}]
                                ]}
                                notifier.edit_message(msg_id, f"✅ 剧名: {wizard_states[chat_id]['title']}\n✅ 频率: {freq}\n\n<b>请选择【精确分类】(匹配路由):</b>", kb)

                        # 🌟 新增中间层:记录你选的周几,并接着跳到分类选项
                        elif data.startswith("wiz_day_"):
                            day = data.split("_")[2]
                            if day != "未知":
                                wizard_states[chat_id]["day"] = day
                                
                            kb = {"inline_keyboard": [
                                [{"text": "📺 华语剧", "callback_data": "wiz_cat_华语剧"}, {"text": "📺 欧美剧", "callback_data": "wiz_cat_欧美剧"}],
                                [{"text": "🎬 华语电影", "callback_data": "wiz_cat_华语电影"}, {"text": "🎬 欧美电影", "callback_data": "wiz_cat_欧美电影"}],
                                [{"text": "🐼 日漫/番剧", "callback_data": "wiz_cat_日漫"}, {"text": "🐼 国漫", "callback_data": "wiz_cat_国漫"}],
                                [{"text": "📺 日韩剧", "callback_data": "wiz_cat_日韩剧"}, {"text": "🎬 日韩电影", "callback_data": "wiz_cat_日韩电影"}],
                                [{"text": "🎤 综艺", "callback_data": "wiz_cat_综艺"}, {"text": "🎥 纪录片", "callback_data": "wiz_cat_纪录片"}],
                                [{"text": "📱 短剧", "callback_data": "wiz_cat_短剧"}, {"text": "🎵 演唱会", "callback_data": "wiz_cat_演唱会"}],
                                [{"text": "❌ 取消", "callback_data": "wiz_cancel"}]
                            ]}
                            day_str = f" ({day})" if day != "未知" else ""
                            notifier.edit_message(msg_id, f"✅ 剧名: {wizard_states[chat_id]['title']}\n✅ 频率: {wizard_states[chat_id]['freq']}{day_str}\n\n<b>请选择【精确分类】(匹配路由):</b>", kb)

                        # 第三步:选择完分类后,展示过滤规则
                        elif data.startswith("wiz_cat_"):
                            wizard_states[chat_id]["cat"] = data.split("_")[2]
                            kb = {"inline_keyboard": [
                                [{"text": "🎥 仅存视频 (MP4/MKV等)", "callback_data": "wiz_type_视频"}],
                                [{"text": "🗂️ 仅存 CAS (秒传文件)", "callback_data": "wiz_type_CAS"}],
                                [{"text": "📦 全盘转存 (不过滤)", "callback_data": "wiz_type_全盘"}],
                                [{"text": "❌ 取消", "callback_data": "wiz_cancel"}]
                            ]}
                            day_str = f" ({wizard_states[chat_id]['day']})" if "day" in wizard_states[chat_id] else ""
                            notifier.edit_message(msg_id, f"✅ 频率: {wizard_states[chat_id]['freq']}{day_str}\n✅ 分类: {wizard_states[chat_id]['cat']}\n\n<b>请选择【文件过滤规则】:</b>", kb)
                            
                        # 第四步(魔法拼接):组装给 V4.8 解析引擎
                        elif data.startswith("wiz_type_"):
                            f_type = data.split("_")[2]
                            state = wizard_states.pop(chat_id)
                            
                            kw_map = {"视频": ".mp4 .mkv .ts", "CAS": ".cas", "全盘": ""}
                            kw = kw_map.get(f_type, "")
                            
                            # 🌟 核心魔法:把刚才选的周几拼接到指令里!
                            day_tag = f" #{state['day']}" if "day" in state else ""
                            s_cmd = f"订阅{state['s_num']}" 
                            
                            # 最终组装的命令形态:订阅 绝命毒师 https... #周更 #周三 #美剧 mp4
                            cmd = f"{s_cmd} {state['title']} {state['url']} #{state['freq']}{day_tag} #{state['cat']} {kw}".strip()
                            
                            notifier.edit_message(msg_id, f"🎉 <b>向导收集完毕!</b>\n正在为您下发指令:\n<code>{cmd}</code>")
                            
                            # 把指令原样注入消息队列,喂给底下的原始逻辑
                            item['message'] = {'chat': {'id': chat_id}, 'text': cmd}
                        
                        # 🌟 拦截从“广场”点过来的订阅按钮
                        elif data.startswith("wiz_feed_"):
                            url_code = data.replace("wiz_feed_", "")
                            full_url = f"https://cloud.189.cn/t/{url_code}" # 拼装成原生的官方分享链接
                            
                            # 强行激活现有的第一步向导,把组装好的链接塞进去
                            wizard_states[chat_id] = {"step": 1, "url": full_url}
                            kb = {"inline_keyboard": [[{"text": "❌ 取消", "callback_data": "wiz_cancel"}]]}
                            notifier.edit_message(msg_id, f"🔗 <b>已从内部广场锁定直链!</b>\n\n✏️ 请直接回复本条消息,输入【干净剧名(年份)】\n<i>(如带季数,请直接写: 庆余年 2)</i>", kb)
                    # ==========================================
                    # 📝 恢复原有的文字处理逻辑
                    # ==========================================
                    msg = item.get('message', {})
                    text = msg.get('text', '')
                    chat_id = msg.get('chat', {}).get('id')

                    if str(chat_id) == str(TG_ADMIN_USER_ID):
                        text = text.strip()
                        
                        # ====== 🥇 新增:纯收割专用建库指令(纯净版,绝不污染全局) ======
                        if text.startswith("加库") or text.startswith("/hsub"):
                            try:
                                if text.startswith("加库"):
                                    clean_text = text[2:].strip()
                                else:
                                    clean_text = text[5:].strip()
                                    
                                parts = clean_text.split()
                                if len(parts) < 2:
                                    notifier.send_message("格式错误!\n常规:加库 剧名(年份) 分类\n指定老月:加库 剧名 分类 202604")
                                    continue
                                
                                # 直接使用顶部引入的全局 re 和 datetime,绝不在此局部 import
                                if parts[-1].startswith("/"):
                                    cloud_path = parts[-1].strip()
                                    show_name = " ".join(parts[:-1]).strip()
                                elif re.match(r'^\d{6}$', parts[-1]):
                                    target_ym = parts[-1].strip()
                                    category_key = parts[-2].strip()
                                    show_name = " ".join(parts[:-2]).strip()
                                    b_large, b_sub = CAT_ROUTER.get(category_key, ("未分类", "0-未分类"))
                                    cloud_path = f"{DIR_CAS_ROOT}/{b_large}/{b_sub}/{target_ym}/{show_name}".replace("//", "/")
                                else:
                                    target_ym = datetime.now().strftime("%Y%m")
                                    category_key = parts[-1].strip()
                                    show_name = " ".join(parts[:-1]).strip()
                                    b_large, b_sub = CAT_ROUTER.get(category_key, ("未分类", "0-未分类"))
                                    cloud_path = f"{DIR_CAS_ROOT}/{b_large}/{b_sub}/{target_ym}/{show_name}".replace("//", "/")

                                # 直接使用顶部定义的全局函数和全局变量
                                search_key = get_match_key(show_name)

                                if not search_key:
                                    notifier.send_message("❌ 剧名无法提取有效特征词,建档失败!")
                                    continue
                                    
                                h_data = load_json(HARVEST_SUBS_FILE)
                                h_data[search_key] = cloud_path
                                save_json(HARVEST_SUBS_FILE, h_data)
                                notifier.send_message(f"✅ 收割记录建档成功!\n📺 剧名:{show_name}\n🔍 特征词:{search_key}\n📂 目标路径:{cloud_path}")
                            except Exception as e:
                                notifier.send_message(f"🚨 代码崩溃抓包:\n<code>{str(e)}</code>")
                            continue # 核心拦截器:干完活直接掐断
                        
                        # ==========================================
                        # 🌟 189 管家 STRM 生成模式切换指令 (A/B/C 极简版)
                        # ==========================================
                        elif text.startswith("设置模式") or text.startswith("/mode"):
                            mode_input = text.replace("设置模式", "").replace("/mode", "").strip().upper()
                            mode_map = {"A": "cas", "B": "cas_native", "C": "both"}
                            
                            if mode_input not in mode_map:
                                notifier.send_message("❌ 模式错误!\n可选模式:\nA - 老版 (cas)\nB - 原生直连 (cas_native)\nC - 双开 (both)\n示例: /mode A")
                                continue
                                
                            real_mode = mode_map[mode_input]
                            s = load_json(SETTINGS_FILE)
                            s["189_strm_mode"] = real_mode
                            save_json(SETTINGS_FILE, s)
                            notifier.send_message(f"✅ 成功!以后 189 投递管家将使用模式: {mode_input} ({real_mode})")
                            continue

                        # ====== 🗑️ 新增:删除收割库记录指令 ======
                        elif text.startswith("删库") or text.startswith("/dsub"):
                            try:
                                kw = text[2:].strip() if text.startswith("删库") else text[5:].strip()
                                if not kw:
                                    notifier.send_message("格式错误!\n示例:删库 师兄啊师兄")
                                    continue
                                    
                                h_file = globals().get('HARVEST_SUBS_FILE', os.path.join(DB_DIR, "harvest_subs.json"))
                                h_data = load_json(h_file)
                                
                                if not h_data:
                                    notifier.send_message("📭 收割记录库为空,没什么可删的。")
                                    continue

                                try:
                                    search_key = get_match_key(kw)
                                except NameError:
                                    c = re.sub(r'[(\(\[\{]?\d{4}[)\)\]\}]?', '', kw)
                                    c = re.sub(r'(?i)\b(4k|1080p|2160p|web-dl|sdr|hdr)\b', '', c)
                                    c = re.sub(r'(完结|连载中|全\d+集|打包|修正)', '', c)
                                    search_key = re.sub(r'[^\w\u4e00-\u9fa5]', '', c).lower()

                                deleted_items = []
                                
                                # 1. 先尝试精确匹配
                                if search_key and search_key in h_data:
                                    deleted_items.append((search_key, h_data[search_key]))
                                    del h_data[search_key]
                                else:
                                    # 2. 如果没精确对上,启动模糊匹配兜底(只要名字里包含就干掉)
                                    keys_to_delete = []
                                    for k, v in h_data.items():
                                        if kw.lower() in k or kw.lower() in v.lower():
                                            keys_to_delete.append(k)
                                    for k in keys_to_delete:
                                        deleted_items.append((k, h_data[k]))
                                        del h_data[k]
                                        
                                if deleted_items:
                                    save_json(h_file, h_data)
                                    msg = "✅ 已成功从收割库中删除以下记录:\n"
                                    for k, p in deleted_items:
                                        msg += f" └ {k}"
                                    notifier.send_message(msg)
                                else:
                                    notifier.send_message(f"❌ 没找到与“{kw}”相关的收割记录。")
                                    
                            except Exception as e:
                                notifier.send_message(f"🚨 删库指令报错:\n<code>{str(e)}</code>")
                            continue

                        # ====== 📋 新增:查看收割库清单指令 ======
                        elif text == "查库" or text == "/lsub":
                            try:
                                h_file = globals().get('HARVEST_SUBS_FILE', os.path.join(DB_DIR, "harvest_subs.json"))
                                h_data = load_json(h_file)
                                if not h_data:
                                    notifier.send_message("📭 当前收割库为空,没有任何手动建档的记录。")
                                    continue
                                
                                msg_lines = ["📋 <b>【专属收割库】当前记录:</b>\n"]
                                for i, (k, p) in enumerate(h_data.items(), 1):
                                    msg_lines.append(f"{i}. <b>{k}</b>\n   └ 📁 {p}")
                                msg_lines.append("\n💡 提示:回复“删库 剧名”即可删除对应记录。")
                                notifier.send_message("\n".join(msg_lines))
                            except Exception as e:
                                notifier.send_message(f"🚨 查库指令报错:\n<code>{str(e)}</code>")
                            continue

                        # ====== 🕵️‍♂️ 新增:动态巡逻目录管理指令 ======
                        elif text == "查目录" or text == "/ldir":
                            s = load_json(SETTINGS_FILE)
                            watch_dirs = s.get("watch_dirs", ["/family/177_cas", "/local_cas"])
                            if not watch_dirs:
                                notifier.send_message("📭 当前没有配置任何巡逻目录,收割兵正在集体放假。")
                                continue
                            
                            msg = "📋 <b>当前自动收割的【巡逻路线】:</b>\n\n"
                            for i, wd in enumerate(watch_dirs, 1):
                                msg += f"{i}. 📁 <code>{wd}</code>\n"
                            msg += "\n💡 提示:回复“加目录 路径”或“删目录 序号”进行动态调整。"
                            notifier.send_message(msg)
                            continue

                        elif text.startswith("加目录") or text.startswith("/adir"):
                            new_dir = text[3:].strip() if text.startswith("加目录") else text[5:].strip()
                            if not new_dir:
                                notifier.send_message("格式错误!\n示例:加目录 /177-临时收割")
                                continue
                            
                            s = load_json(SETTINGS_FILE)
                            watch_dirs = s.get("watch_dirs", ["/family/177_cas", "/local_cas"])
                            
                            if new_dir not in watch_dirs:
                                watch_dirs.append(new_dir)
                                s["watch_dirs"] = watch_dirs
                                save_json(SETTINGS_FILE, s)
                                notifier.send_message(f"✅ 成功划定新的巡逻战区:\n📁 {new_dir}\n(下次收割时生效)")
                            else:
                                notifier.send_message(f"⚠️ 该目录已经在巡逻路线中了,无需重复添加:\n📁 {new_dir}")
                            continue

                        elif text.startswith("删目录") or text.startswith("/ddir"):
                            del_dir = text[3:].strip() if text.startswith("删目录") else text[5:].strip()
                            if not del_dir:
                                notifier.send_message("格式错误!\n示例:删目录 1\n(发“查目录”看序号,直接填序号或名字删)")
                                continue
                                
                            s = load_json(SETTINGS_FILE)
                            watch_dirs = s.get("watch_dirs", ["/family/177_cas", "/local_cas"])
                            
                            target_to_del = None
                            # 智能匹配:如果你发的是纯数字,按序号删;如果发的是文字,模糊匹配删
                            if del_dir.isdigit() and 1 <= int(del_dir) <= len(watch_dirs):
                                target_to_del = watch_dirs[int(del_dir) - 1]
                            else:
                                for wd in watch_dirs:
                                    if del_dir.lower() in wd.lower():
                                        target_to_del = wd
                                        break
                                        
                            if target_to_del:
                                watch_dirs.remove(target_to_del)
                                s["watch_dirs"] = watch_dirs
                                save_json(SETTINGS_FILE, s)
                                notifier.send_message(f"✅ 已撤销该战区的巡逻任务:\n🗑️ {target_to_del}")
                            else:
                                notifier.send_message(f"❌ 没找到匹配的目录:{del_dir}")
                            continue
                            
                        # ==========================================
                        # 🌟 新增:动态免删复制(个人云)目录管理指令
                        # ==========================================
                        elif text == "查个人云" or text == "/listcopy":
                            s = load_json(SETTINGS_FILE)
                            nd_list = s.get("no_delete_dirs", [])
                            if nd_list:
                                msg = "📂 <b>当前免删复制(个人云)线路</b>:\n\n"
                                for i, d in enumerate(nd_list, 1):
                                    msg += f"{i}. 📑 <code>{d}</code>\n"
                                msg += "\n💡 提示:以上目录下的 CAS 文件只执行复制,保留个人云源文件。"
                                notifier.send_message(msg)
                            else:
                                notifier.send_message("📂 当前免删名单为空,所有目录均执行默认的【移动+删除】。")
                            continue

                        elif text.startswith("加个人云") or text.startswith("/addcopy"):
                            new_dir = text[4:].strip() if text.startswith("加个人云") else text[8:].strip()
                            if not new_dir:
                                notifier.send_message("格式错误!\n示例:加个人云 /个人云/下载")
                                continue
                            
                            s = load_json(SETTINGS_FILE)
                            nd_list = s.get("no_delete_dirs", [])
                            if new_dir not in nd_list:
                                nd_list.append(new_dir)
                                s["no_delete_dirs"] = nd_list
                                save_json(SETTINGS_FILE, s)
                                notifier.send_message(f"✅ 成功划定免流复制战区:\n📂 {new_dir}\n(下次收割时生效,只复制不删CAS)")
                            else:
                                notifier.send_message(f"⚠️ 该目录已经在免删名单中了:\n📂 {new_dir}")
                            continue

                        elif text.startswith("删个人云") or text.startswith("/rmcopy"):
                            del_dir = text[4:].strip() if text.startswith("删个人云") else text[7:].strip()
                            if not del_dir:
                                notifier.send_message("格式错误!\n示例:删个人云 /个人云/下载")
                                continue
                                
                            s = load_json(SETTINGS_FILE)
                            nd_list = s.get("no_delete_dirs", [])
                            if del_dir in nd_list:
                                nd_list.remove(del_dir)
                                s["no_delete_dirs"] = nd_list
                                save_json(SETTINGS_FILE, s)
                                notifier.send_message(f"🗑️ 已移出名单!以后该目录恢复为常规【移动并删除】模式:\n📂 {del_dir}")
                            else:
                                notifier.send_message(f"⚠️ 找不到该目录,请检查路径是否正确:\n📂 {del_dir}")
                            continue   

                        # ==========================================
                        # 🌟 139移动云盘 专属 STRM 生成指令 (纯触发,不洗名不移动)
                        # ==========================================
                        elif text.startswith("同步139") or text.startswith("/sync139"):
                            # 提取你要同步的目录
                            target_path = text.replace("同步139", "").replace("/sync139", "").strip()
                            if not target_path:
                                notifier.send_message("❌ 格式错误!\n示例:同步139 /139/139cas/动漫/紫川")
                                continue
                            
                            try:
                                from urllib import parse
                                # 直接向管家发送 139 专属请求
                                url = f"{API_5000_URL}/api/sync?drive=139&path={parse.quote(target_path)}"
                                requests.get(url, timeout=5)
                                notifier.send_message(f"✅ 139同步指令已发送!\n📂 目标:{target_path}\n(管家已开始在后台生成 STRM)")
                            except Exception as e:
                                notifier.send_message(f"⚠️ 发送失败,请检查管家服务: {e}")
                            continue    

                        # 🌟 新增的动态开关与拉取指令
                        elif text in ["开启自动收割", "开启扫描", "/ascan"]:
                            s = load_json(SETTINGS_FILE); s["auto_scan_cas"] = True; save_json(SETTINGS_FILE, s)
                            notifier.send_message("✅ 已【开启】CAS自动收割巡逻。")
                            continue
                        elif text in ["关闭自动收割", "关闭扫描", "/sscan"]:
                            s = load_json(SETTINGS_FILE); s["auto_scan_cas"] = False; save_json(SETTINGS_FILE, s)
                            notifier.send_message("⏸️ 已【关闭】CAS自动收割巡逻。")
                            continue
                        elif text in ["开启自动订阅", "开启订阅检查", "/asub"]:
                            s = load_json(SETTINGS_FILE); s["auto_check_subs"] = True; save_json(SETTINGS_FILE, s)
                            notifier.send_message("✅ 已【开启】定时订阅拉取。")
                            continue
                        elif text in ["关闭自动订阅", "关闭订阅检查", "/ssub"]:
                            s = load_json(SETTINGS_FILE); s["auto_check_subs"] = False; save_json(SETTINGS_FILE, s)
                            notifier.send_message("⏸️ 已【关闭】定时订阅拉取。")
                            continue
                        elif text in ["同步订阅", "立即拉取", "全部拉取", "更新订阅", "/sync"]:
                            notifier.send_message("🚀 正在强行冲破时间门槛,全量拉取订阅中...")
                            check_subscriptions(client_obj, ignore_time=True)
                            notifier.send_message("✅ 同步拉取任务彻底执行完毕。")
                            continue
                        elif text in ["收割", "处理", "添加", "/harvest"]:
                            notifier.send_message("📥 收到【收割】指令:正在为您洗名并入库 CAS 文件...")
                            p_names = process_cas_via_olist_api()
                            if p_names:
                                msg_str = "\n".join([f" └ {n}" for n in p_names[:20]])
                                if len(p_names) > 20: msg_str += f"\n...等共 {len(p_names)} 个文件"
                                notifier.send_message(f"✅ CAS 收割入库完成:\n{msg_str}")
                            else:
                                notifier.send_message("✅ CAS 收割完成,暂无新文件。")
                            continue
                        elif text in ["扫箱子", "投递", "本地扫描", "/dropbox"]:
                            notifier.send_message("🚀 收到指令:立刻启动本地投递箱极速雷达...")
                            try:
                                scan_local_dropbox()
                                notifier.send_message("✅ 本地投递箱雷达扫描与下发任务已执行完毕!")
                            except Exception as e:
                                notifier.send_message(f"❌ 扫描本地投递箱时发生异常: {e}")
                            continue
                        elif text in ["动态", "广场", "上新", "/feed"]:
                            notifier.send_message("📡 正在连接订阅中心,拉取最新情报...")
                            try:
                                res = client_obj.session.get("https://cloud.189.cn/api/open/share/getOwnerSubscribeShare.action?pageNum=1&pageSize=8", timeout=10).json()
                                if res.get("code") == "success":
                                    file_list = res.get("data", {}).get("shareFileList", [])
                                    if not file_list:
                                        notifier.send_message("📭 订阅中心目前没有任何更新。")
                                        continue
                                    
                                    msg_lines = ["📡 <b>【订阅中心】最新动态:</b>\n"]
                                    kb_buttons = []
                                    
                                    for i, item in enumerate(file_list, 1):
                                        raw_name = item.get("name", "未知资源")
                                        name = translate_folder_name(raw_name)
                                        author = item.get("ownerAccount", "未知发布者")
                                        url_code = item.get("accessURL", "")
                                        date_str = item.get("lastOpTime", "")[5:16] 
                                        
                                        if not url_code: continue
                                        msg_lines.append(f"{i}. 📁 <code>{name}</code>\n   └ 👤 {author} | ⏱ {date_str}\n")
                                        btn_text = name.replace("📺 ", "").replace("🎬 ", "")
                                        kb_buttons.append([{"text": f"📥 订阅: {btn_text[:12]}...", "callback_data": f"wiz_feed_{url_code}"}])
                                        
                                    kb_buttons.append([{"text": "❌ 取消", "callback_data": "wiz_cancel"}])
                                    kb = {"inline_keyboard": kb_buttons}
                                    notifier.send_message("\n".join(msg_lines), kb)
                                else:
                                    # 🚨 掉线报警,抛出异常让底层接住
                                    raise Exception(f"接口掉线/风控拦截: {res}")
                            except Exception as e:
                                notifier.send_message(f"❌ 广场拉取异常: {e}")
                                err_str = str(e).upper()
                                # 👇 底层接住异常,启动自愈打针
                                if "INVALIDSESSIONKEY" in err_str or "CHECK IP ERROR" in err_str or "UNKNOWN_ERROR" in err_str or "UNKNOWN" in err_str:
                                    if os.path.exists(COOKIES_FILE): os.remove(COOKIES_FILE)
                                    auto_relogin(client_obj, force=True)
                                    notifier.send_message("✅ 引擎已重新握手自愈,请重发指令!")
                            continue

                        elif text.startswith("搜 ") or text.startswith("搜索 ") or text.startswith("/search "):
                            raw_keyword = text.split(" ", 1)[1].strip()
                            if not raw_keyword: continue
                            
                            notifier.send_message(f"🧠 正在请求 TMDB 反查 【{raw_keyword}】 的全维特征码...")
                            tmdb_info = get_tmdb_info(raw_keyword)
                            
                            keywords = set()
                            keywords.add(raw_keyword.lower())
                            if " " in raw_keyword:
                                for k in raw_keyword.split():
                                    if len(k) > 1: keywords.add(k.lower())
                                    
                            if tmdb_info:
                                if tmdb_info.get('en_name'): keywords.add(tmdb_info['en_name'].lower())
                                if tmdb_info.get('pinyin_full'): keywords.add(tmdb_info['pinyin_full'].lower())
                                if tmdb_info.get('pinyin_initial'): keywords.add(tmdb_info['pinyin_initial'].lower())
                                if tmdb_info.get('id'): 
                                    keywords.add(f"tmdb-{tmdb_info['id']}")
                                    keywords.add(f"tmdb{tmdb_info['id']}")
                                    keywords.add(str(tmdb_info['id']))
                                    
                            keyword_list = list(keywords)
                            display_kw = " | ".join(keyword_list)
                            
                            notifier.send_message(f"🔍 锁定全维特征码: 【{display_kw}】\n启动地毯式穿甲雷达,深入主页挖掘...")
                            try:
                                active_users = {} 
                                for feed_page in range(1, 5):
                                    feed_url = f"https://cloud.189.cn/api/open/share/getOwnerSubscribeShare.action?pageNum={feed_page}&pageSize=100"
                                    res_feed = client_obj.session.get(feed_url, timeout=15).json()
                                    
                                    if res_feed.get("code") == "success":
                                        items = res_feed.get("data", {}).get("shareFileList", [])
                                        if not items: break
                                        for item in items:
                                            uid = item.get("upUserId")
                                            name = item.get("ownerAccount", "未知大佬")
                                            if uid: active_users[uid] = name
                                    else:
                                        # 🚨 掉线报警 1
                                        raise Exception(f"接口掉线/风控拦截: {res_feed}")
                                
                                if not active_users:
                                    notifier.send_message("❌ 广场空空如也,未获取到任何订阅大佬的信息。")
                                    continue
                                
                                matched_items = []
                                for uid, uname in active_users.items():
                                    for page in range(1, 50):
                                        url = f"https://cloud.189.cn/api/open/share/getUpResourceShare.action?pageNum={page}&pageSize=30&upUserId={uid}"
                                        res_user = client_obj.session.get(url, timeout=10).json()
                                        
                                        if res_user.get("code") == "success":
                                            items = res_user.get("data", {}).get("fileList", []) 
                                            if not items: break 
                                            for item in items:
                                                item_name_lower = item.get("name", "").lower()
                                                if any(kw in item_name_lower for kw in keyword_list):
                                                    item["ownerAccount"] = uname 
                                                    matched_items.append(item)
                                        else:
                                            # 🚨 掉线报警 2
                                            raise Exception(f"扒主页时掉线/风控拦截: {res_user}")
                                
                                unique_matches = []
                                seen_urls = set()
                                for item in matched_items:
                                    url_code = item.get("accessURL", "")
                                    if url_code and url_code not in seen_urls:
                                        seen_urls.add(url_code)
                                        unique_matches.append(item)

                                if not unique_matches:
                                    notifier.send_message(f"📭 翻遍了 {len(active_users)} 位大佬的个人历史主页,没找到相关的资源。")
                                    continue
                                    
                                msg_lines = [f"🎯 <b>为您精准捞到了 {len(unique_matches)} 个相关资源:</b>\n"]
                                kb_buttons = []
                                
                                for i, item in enumerate(unique_matches[:8], 1):
                                    raw_name = item.get("name", "未知资源")
                                    name = translate_folder_name(raw_name)
                                    author = item.get("ownerAccount", "未知发布者")
                                    url_code = item.get("accessURL", "")
                                    date_str = item.get("lastOpTime", "")[5:16]
                                    
                                    msg_lines.append(f"{i}. 📁 <code>{name}</code>\n   └ 👤 {author} | ⏱ {date_str}\n")
                                    btn_text = name.replace("📺 ", "").replace("🎬 ", "")
                                    kb_buttons.append([{"text": f"📥 订阅: {btn_text[:12]}...", "callback_data": f"wiz_feed_{url_code}"}])
                                    
                                kb_buttons.append([{"text": "❌ 取消", "callback_data": "wiz_cancel"}])
                                kb = {"inline_keyboard": kb_buttons}
                                
                                notifier.send_message("\n".join(msg_lines), kb)
                            except Exception as e:
                                notifier.send_message(f"❌ 搜索拉取异常: {e}")
                                # 👇 自愈打针
                                err_str = str(e).upper()
                                if "INVALIDSESSIONKEY" in err_str or "CHECK IP ERROR" in err_str or "UNKNOWN_ERROR" in err_str or "UNKNOWN" in err_str:
                                    if os.path.exists(COOKIES_FILE): os.remove(COOKIES_FILE)
                                    auto_relogin(client_obj, force=True)
                                    notifier.send_message("✅ 引擎已重新握手自愈,请重发指令!")
                            continue
                        
                        # ====== 📖 新增:TMDB 影视资料百科指令 ======
                        elif text.startswith("查剧 ") or text.startswith("信息 ") or text.startswith("/info "):
                            keyword = text.split(" ", 1)[1].strip()
                            if not keyword: continue
                            
                            notifier.send_message(f"🔍 正在连接 TMDB 全球数据库,检索【{keyword}】的档案...")
                            try:
                                info_msg = fetch_tmdb_rich_info(keyword)
                                # 配合一个快速订阅的建议按钮
                                kb = {"inline_keyboard": [[{"text": "❌ 关闭面板", "callback_data": "wiz_cancel"}]]}
                                notifier.send_message(info_msg, kb)
                            except Exception as e:
                                notifier.send_message(f"❌ 检索失败: {e}")
                            continue

                        elif text.startswith("查 ") or text.startswith("查看 ") or text.startswith("/check "):
                            raw_keyword = text.split(" ", 1)[1].strip()
                            if not raw_keyword: continue
                            
                            keyword_list = [k.strip().lower() for k in raw_keyword.split() if k.strip()]
                            display_kw = " | ".join(keyword_list)
                            
                            notifier.send_message(f"🔍 启动地毯式穿甲雷达,正在遍历所有大佬个人主页挖掘: 【{display_kw}】...")
                            try:
                                # 🌟 第一步:获取活跃大佬的名单
                                res_feed = client_obj.session.get("https://cloud.189.cn/api/open/share/getOwnerSubscribeShare.action?pageNum=1&pageSize=100", timeout=15).json()
                                active_users = {} 
                                
                                if res_feed.get("code") == "success":
                                    for item in res_feed.get("data", {}).get("shareFileList", []):
                                        uid = item.get("upUserId")
                                        name = item.get("ownerAccount", "未知大佬")
                                        if uid: active_users[uid] = name
                                
                                if not active_users:
                                    notifier.send_message("❌ 广场空空如也,未获取到任何订阅大佬的信息。")
                                    continue
                                
                                # 🌟 第二步:踹门深挖
                                matched_items = []
                                for uid, uname in active_users.items():
                                    for page in range(1, 4):
                                        url = f"https://cloud.189.cn/api/open/share/getUpResourceShare.action?pageNum={page}&pageSize=30&upUserId={uid}"
                                        res_user = client_obj.session.get(url, timeout=10).json()
                                        
                                        if res_user.get("code") == "success":
                                            # 🎯 破案核心:这里是 fileList!不是 shareFileList!
                                            items = res_user.get("data", {}).get("fileList", []) 
                                            if not items: break 
                                            
                                            for item in items:
                                                item_name_lower = item.get("name", "").lower()
                                                if any(kw in item_name_lower for kw in keyword_list):
                                                    item["ownerAccount"] = uname 
                                                    matched_items.append(item)
                                        else:
                                            break
                                            
                                # 🌟 第三步:去重展示
                                unique_matches = []
                                seen_urls = set()
                                for item in matched_items:
                                    url_code = item.get("accessURL", "")
                                    if url_code and url_code not in seen_urls:
                                        seen_urls.add(url_code)
                                        unique_matches.append(item)

                                if not unique_matches:
                                    notifier.send_message(f"📭 翻遍了 {len(active_users)} 位大佬的个人历史主页,没找到与【{display_kw}】相关的资源。")
                                    continue
                                    
                                msg_lines = [f"🎯 <b>为您精准捞到了 {len(unique_matches)} 个相关资源:</b>\n"]
                                kb_buttons = []
                                
                                for i, item in enumerate(unique_matches[:8], 1):
                                    name = item.get("name", "未知资源")
                                    author = item.get("ownerAccount", "未知发布者")
                                    url_code = item.get("accessURL", "")
                                    date_str = item.get("lastOpTime", "")[5:16]
                                    
                                    msg_lines.append(f"{i}. 📁 <code>{name}</code>\n   └ 👤 {author} | ⏱ {date_str}\n")
                                    kb_buttons.append([{"text": f"📥 订阅: {name[:15]}...", "callback_data": f"wiz_feed_{url_code}"}])
                                    
                                kb_buttons.append([{"text": "❌ 取消", "callback_data": "wiz_cancel"}])
                                kb = {"inline_keyboard": kb_buttons}
                                
                                notifier.send_message("\n".join(msg_lines), kb)
                            except Exception as e:
                                notifier.send_message(f"❌ 搜索拉取异常: {e}")
                            continue

                        # 🌟 第二处:查人/查作者
                        elif text.startswith("查人 ") or text.startswith("查作者 ") or text.startswith("/author "):
                            author_kw = text.split(" ", 1)[1].strip()
                            if not author_kw: continue
                            
                            notifier.send_message(f"🕵️‍♂️ 启动多账号联合扫描:正在锁定所有包含【{author_kw}】的大佬...")
                            try:
                                res_feed = client_obj.session.get("https://cloud.189.cn/api/open/share/getOwnerSubscribeShare.action?pageNum=1&pageSize=100", timeout=15).json()
                                suspects = [] 
                                
                                if res_feed.get("code") == "success":
                                    for item in res_feed.get("data", {}).get("shareFileList", []):
                                        owner = item.get("ownerAccount", "")
                                        if author_kw.lower() in owner.lower():
                                            uid = item.get("upUserId")
                                            if uid and (uid, owner) not in suspects:
                                                suspects.append((uid, owner))
                                else:
                                    # 🚨 掉线报警 1
                                    raise Exception(f"接口掉线/风控拦截: {res_feed}")
                                
                                if not suspects:
                                    notifier.send_message(f"❌ 没找到名字里带【{author_kw}】的大佬。")
                                    continue
                                    
                                notifier.send_message(f"🔍 锁定 {len(suspects)} 个关联账号,正在合力挖掘最新动态...")
                                
                                all_blind_boxes = []
                                for uid, uname in suspects:
                                    for page in range(1, 7):
                                        url = f"https://cloud.189.cn/api/open/share/getUpResourceShare.action?pageNum={page}&pageSize=30&upUserId={uid}"
                                        res_user = client_obj.session.get(url, timeout=10).json()
                                        
                                        if res_user.get("code") == "success":
                                            items = res_user.get("data", {}).get("fileList", [])
                                            if not items: break 
                                            for itm in items:
                                                itm["_from_user"] = uname 
                                                all_blind_boxes.append(itm)
                                        else:
                                            # 🚨 掉线报警 2
                                            raise Exception(f"扒主页时掉线/风控拦截: {res_user}")

                                if not all_blind_boxes:
                                    notifier.send_message("📭 选中的大佬们最近都没有发过任何东西。")
                                    continue

                                all_blind_boxes.sort(key=lambda x: x.get("lastOpTime", ""), reverse=True)
                                    
                                msg_lines = [f"🕵️‍♂️ <b>多账号联合情报(真实时间线):</b>\n"]
                                kb_buttons = []
                                
                                for i, item in enumerate(all_blind_boxes[:20], 1):
                                    raw_name = item.get("name", "未知资源")
                                    name = translate_folder_name(raw_name)
                                    author = item.get("_from_user", "未知")
                                    url_code = item.get("accessURL", "")
                                    date_str = item.get("lastOpTime", "")[5:16]
                                    
                                    if not url_code: continue
                                    msg_lines.append(f"{i}. 📁 <code>{name}</code>\n   └ 👤 {author} | ⏱ {date_str}\n")
                                    btn_text = name.replace("📺 ", "").replace("🎬 ", "")
                                    kb_buttons.append([{"text": f"📥 订阅: {btn_text[:12]}...", "callback_data": f"wiz_feed_{url_code}"}])
                                    
                                kb_buttons.append([{"text": "❌ 取消", "callback_data": "wiz_cancel"}])
                                kb = {"inline_keyboard": kb_buttons}
                                
                                notifier.send_message("\n".join(msg_lines), kb)
                            except Exception as e:
                                notifier.send_message(f"❌ 联合查水表异常: {e}")
                                # 👇 自愈打针
                                err_str = str(e).upper()
                                if "INVALIDSESSIONKEY" in err_str or "CHECK IP ERROR" in err_str or "UNKNOWN_ERROR" in err_str or "UNKNOWN" in err_str:
                                    if os.path.exists(COOKIES_FILE): os.remove(COOKIES_FILE)
                                    auto_relogin(client_obj, force=True)
                                    notifier.send_message("✅ 引擎已重新握手自愈,请重发指令!")
                            continue

                        # ==========================================
                        # 🧙‍♂️ 智能嗅探向导拦截器
                        # ==========================================
                        if re.match(r'^http[s]?://(cloud\.189\.cn|t\.189\.cn)/', text):
                            wizard_states[chat_id] = {"step": 1, "url": text}
                            kb = {"inline_keyboard": [[{"text": "❌ 取消", "callback_data": "wiz_cancel"}]]}
                            notifier.send_message(f"🔗 <b>嗅探到天翼云链接!</b>\n\n✏️ 请直接回复本条消息,输入【剧名(年份)】\n<i>(如带季数,请直接写: 庆余年 2)</i>", kb)
                            continue
                            
                        if chat_id in wizard_states and wizard_states[chat_id].get("step") == 1 and not text.startswith("/"):
                            m = re.match(r'^(.*?)\s+[sS第]?0?(\d{1,2})[季]?$', text)
                            if m:
                                wizard_states[chat_id]["title"] = m.group(1).strip()
                                wizard_states[chat_id]["s_num"] = m.group(2)
                            else:
                                wizard_states[chat_id]["title"] = text.strip()
                                wizard_states[chat_id]["s_num"] = ""

                            wizard_states[chat_id]["step"] = 2
                            kb = {"inline_keyboard": [
                                [{"text": "🌞 日更 (国产剧/短剧等)", "callback_data": "wiz_freq_日更"}],
                                [{"text": "📅 周更/连载 (美剧/动漫等)", "callback_data": "wiz_freq_周更"}],
                                [{"text": "✅ 已完结 (全集一波流)", "callback_data": "wiz_freq_完结"}],
                                [{"text": "🎬 单次任务 (电影/演唱会等)", "callback_data": "wiz_freq_单次"}], # 🌟 改为单次
                                [{"text": "❌ 取消", "callback_data": "wiz_cancel"}]
                            ]}
                            
                            s_tip = f" (第 {wizard_states[chat_id]['s_num']} 季)" if wizard_states[chat_id]["s_num"] else ""
                            notifier.send_message(f"✅ 已记录剧名: {wizard_states[chat_id]['title']}{s_tip}\n\n<b>🏷️ 请选择【更新频率】:</b>", kb)
                            continue
                            
                        # === 原版指令识别开始 ===
                        logger.info(f"🛠️ [指令] 接收到远程终端最高权限指令: {text}")
                        # 🌟 净水器:一刀切掉 TG 菜单自带的尾巴
                        if text and "@xushangjun_bot" in text:
                            text = text.replace("@xushangjun_bot", "")

                        if text == "列表" or text == "清单" or text == "/list":
                            subs = load_json(SUBS_FILE)
                            if not subs:
                                notifier.send_message("📭 当前没有任何活跃的监控任务。")
                                continue
                            msg_lines = ["📋 当前监控清单:"]
                            for i, (sid, info) in enumerate(subs.items(), 1):
                                p = info.get("path", "")
                                freq = info.get("freq", "常规")
                                msg_lines.append(f"{i}. [{freq}] {p}")
                            msg_lines.append("\n💡 提示:回复“取消+序号”(如: 取消1) 即可解除监控并清理记忆。")
                            notifier.send_message("\n".join(msg_lines))

                        elif text.startswith("取消") or text.startswith("/cancel"):
                            kw = text.replace("取消", "").strip()
                            if not kw: continue
                            
                            subs = load_json(SUBS_FILE)
                            target_id = None
                            
                            if kw.isdigit():
                                idx = int(kw) - 1
                                if 0 <= idx < len(subs):
                                    target_id = list(subs.keys())[idx]
                            
                            if not target_id:
                                for sid, info in subs.items():
                                    p = info.get("path", "")
                                    if kw.lower() in p.lower():
                                        target_id = sid; break
                            
                            if target_id:
                                path_to_del = subs[target_id].get("path", "未知")
                                del subs[str(target_id)]
                                save_json(SUBS_FILE, subs)
                                
                                history_data = load_json(HISTORY_FILE)
                                old_len = len(history_data)
                                history_data = {k: v for k, v in history_data.items() if not (isinstance(v, dict) and str(v.get("sub_id")) == str(target_id))}
                                save_json(HISTORY_FILE, history_data)
                                
                                notifier.send_message(f"✅ 已解除订阅:{path_to_del}\n🗑️ 同步粉碎了 {old_len - len(history_data)} 条关联记忆。")
                            else:
                                notifier.send_message(f"❌ 没找到与“{kw}”相关的订阅任务。")

                        elif text == "体检":
                            notifier.send_message("🩺 正在为您执行全库深度体检与垃圾回收...")
                            subs = load_json(SUBS_FILE)
                            dead_count, dead_list = 0, []
                            for sid, info in list(subs.items()):
                                try:
                                    res_ch = client_obj.session.get("https://cloud.189.cn/api/open/file/listFiles.action", params={"folderId": sid, "pageNum": 1, "pageSize": 1}, timeout=10).json()
                                    if str(res_ch.get("res_code", "")) == "0":
                                        file_list_ao = res_ch.get("fileListAO", {})
                                        files = file_list_ao.get("fileList", [])
                                        folders = file_list_ao.get("folderList", [])
                                        if not files and not folders:
                                            dead_count += 1
                                            p = info.get("path", "未知路径") if isinstance(info, dict) else info
                                            dead_list.append(p)
                                            del subs[sid]
                                    else:
                                        logger.warning(f"⚠️ 目录 {sid} 接口异常,启动防误杀保护跳过!")
                                except Exception as e:
                                    logger.warning(f"⚠️ 目录 {sid} 检查失败跳过: {e}")
                                        
                            if dead_count > 0: save_json(SUBS_FILE, subs)
                            
                            history_data = load_json(HISTORY_FILE)
                            old_len = len(history_data)
                            history_data = {k: v for k, v in history_data.items() if not (isinstance(v, dict) and str(v.get("sub_id")) not in subs)}
                            save_json(HISTORY_FILE, history_data)
                            ghost_count = old_len - len(history_data)
                            
                            msg_str = ""
                            if dead_count > 0: msg_str += f"🚨 成功拔除 {dead_count} 个失效死目录:\n" + "\n".join(dead_list) + "\n\n"
                            else: msg_str += "✅ 您的所有订阅目录均健康在线。\n\n"
                                
                            if ghost_count > 0: msg_str += f"👻 执行垃圾回收:清除了 {ghost_count} 条残留历史记忆!"
                            else: msg_str += "✨ 历史记录库非常干净,无残留垃圾。"
                            notifier.send_message(f"🩺 体检报告:\n{msg_str}")

                        elif text.startswith("刷新") or text.startswith("入库"):
                            match_refresh = re.match(r'^(刷新|入库)\s+(.*)', text)
                            if match_refresh:
                                keyword_input = match_refresh.group(2).strip()
                                # 🌟 修复正则:限制季数为1-2位,并要求前方必须有空格或 S/第 等标识,保护带年份的单剧
                                m = re.match(r'^(.*?)\s+(?:[sS第]\s*)?0?(\d{1,2})[季]?$', keyword_input, re.IGNORECASE)
                                if m and m.group(1).strip(): 
                                    base_kw, s_num = m.group(1).strip(), int(m.group(2))
                                else: 
                                    base_kw, s_num = keyword_input, None

                                notifier.send_message(f"🔍 收到入库指令,正在检索: {base_kw}...")
                                
                                subs = load_json(SUBS_FILE)
                                matched_paths = []
                                
                                # 👇 新增:如果输入的是带斜杠的绝对路径,直接无脑锁定,不查库!
                                # =================================================================
                                # 🌟 终极智能寻轨雷达替换开始:彻底解放人脑,全自动找回带年份的绝对路径
                                # =================================================================
                                if base_kw.startswith("/"):
                                    matched_paths.append(base_kw.strip())
                                else:
                                    # 1. 优先极速比对本地活跃记忆库
                                    for t_id, info in subs.items():
                                        path_in_db = info.get("path", "") if isinstance(info, dict) else ""
                                        if base_kw.lower() in path_in_db.lower():
                                            if path_in_db not in matched_paths: matched_paths.append(path_in_db)

                                    # 2. 🌟 终极雷达:如果大脑没记住这剧,立刻启动底层 Openlist 接口全自动搜寻物理路径!
                                    if not matched_paths:
                                        notifier.send_message(f"🧠 记忆库未收录老剧,正启动 Openlist 穿透雷达自动检索真实路径: [{base_kw}] ...")
                                        try:
                                            r_log = requests.post(f"{API_5244_URL}/api/auth/login", 
                                                                  json={"username": OLIST_USER, "password": OLIST_PASS}, timeout=5).json()
                                            if r_log.get("code") == 200:
                                                o_headers = {"Authorization": r_log["data"]["token"], "Content-Type": "application/json"}
                                                
                                                # 提取所有分类的总分区路径作为雷达扫描基点
                                                radar_bases = set()
                                                for l_cat, s_cat in CAT_ROUTER.values():
                                                    sub_p = f"{l_cat}/{s_cat}".strip('/') if s_cat else l_cat
                                                    radar_bases.add(get_openlist_path(f"{DIR_CAS_ROOT}/{sub_p}".replace("//", "/")))
                                                    radar_bases.add(get_openlist_path(f"{DIR_VIDEO_ROOT}/{sub_p}".replace("//", "/")))
                                                
                                                for base_p in radar_bases:
                                                    # 极速读取 Openlist 年月目录列表 (利用接口轻量级穿透)
                                                    r_list = requests.post(f"{API_5244_URL}/api/fs/list", 
                                                                           json={"path": base_p}, headers=o_headers, timeout=5).json()
                                                    if r_list.get("code") == 200:
                                                        content = (r_list.get("data") or {}).get("content") or []
                                                        ym_dirs = [item["name"] for item in content if item["is_dir"] and re.match(r'^\d{4,6}$', item["name"])]
                                                        ym_dirs.sort(reverse=True) # 优先从最新的月份往老月份找
                                                        
                                                        for ym in ym_dirs:
                                                            ym_path = f"{base_p}/{ym}"
                                                            r_shows = requests.post(f"{API_5244_URL}/api/fs/list", 
                                                                                    json={"path": ym_path}, headers=o_headers, timeout=5).json()
                                                            if r_shows.get("code") == 200:
                                                                shows = (r_shows.get("data") or {}).get("content") or []
                                                                for s_item in shows:
                                                                    # 只要文件夹名字包含输入的关键词,当场锁定!
                                                                    if s_item["is_dir"] and base_kw.lower() in s_item["name"].lower():
                                                                        exact_path = f"{ym_path}/{s_item['name']}"
                                                                        matched_paths.append(exact_path)
                                                                        break
                                                            if matched_paths: break
                                                    if matched_paths: break
                                        except Exception as radar_err:
                                            logger.warning(f"Openlist自动寻轨雷达异常: {radar_err}")
                                        
                                        if matched_paths:
                                            notifier.send_message(f"🎯 雷达寻轨成功!全自动还原出带年份的物理绝对路径:\n📁 {matched_paths[0]}")
                                        else:
                                            notifier.send_message(f"📭 雷达遍历了全部分区,未找到包含【{base_kw}】的实体文件夹,已跳过。")
                                # =================================================================
                                # 🌟 终极智能寻轨雷达替换结束
                                # =================================================================
                                            
                                # =================================================================
                                # 🌟 终极双轨驱动装甲替换开始 (涵盖单点刷新与全局扫描)
                                # =================================================================
                                if matched_paths:
                                    notifier.send_message(f"🎯 共命中 {len(matched_paths)} 个关联目录,执行双轨刷新...")
                                    
                                    # 优化:提到循环外登录 Openlist 一次,拿到通用 Token,大幅提升效率
                                    o_headers = None
                                    try:
                                        r_log = requests.post(f"{API_5244_URL}/api/auth/login", 
                                                              json={"username": OLIST_USER, "password": OLIST_PASS}, timeout=5).json()
                                        if r_log.get("code") == 200:
                                            o_headers = {"Authorization": r_log["data"]["token"], "Content-Type": "application/json"}
                                    except: pass

                                    for mp in matched_paths:
                                        openlist_p = get_openlist_path(mp)
                                        butler_path = re.sub(r'(?i)/Season\s*\d+/?$', '', openlist_p)
                                        
                                        # 💥 第一步:无论 CAS 还是普通视频,必须先强行穿透 Openlist 缓存!
                                        # 只有 Openlist 物理层重载了,它自身才能扫描到新文件并映射出普通视频的 STRM!
                                        if o_headers:
                                            notifier.send_message(f"⚡ 强制穿透 Openlist 缓存: {butler_path} ...")
                                            try:
                                                requests.post(f"{API_5244_URL}/api/fs/list", 
                                                              json={"path": butler_path, "refresh": True}, headers=o_headers, timeout=15).close()
                                                time.sleep(3.0) # 给底层留出落盘和 Openlist 自建 strm 的缓冲时间
                                            except: pass

                                        # 🔀 第二步:严格的双轨路由分流
                                        if DIR_CAS_ROOT in openlist_p:
                                            # 177-秒传 -> 交给 管家 收割
                                            try: 
                                                requests.get(f"{API_5000_URL}/api/sync", params={"path": butler_path}, timeout=3).close()
                                                notifier.send_message(f"✅ 管家同步指令已精准下发: {butler_path}")
                                            except Exception as e: 
                                                notifier.send_message(f"❌ 管家同步无响应: {e}")
                                        else:
                                            # 177-视频与原老目录 -> Openlist 自身已吐出 STRM,直接唤醒 Emby 原生拉取
                                            try:
                                                s = load_json(SETTINGS_FILE)
                                                bash_path = s.get("bash_path", DEFAULT_BASH_PATH)
                                                refresh_sh = s.get("refresh_sh_path", DEFAULT_REFRESH_SH)
                                                subprocess.Popen([bash_path, refresh_sh, openlist_p], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
                                                notifier.send_message(f"✅ 缓存已刷,原生Emby拉取成功: {openlist_p}")
                                            except: pass
                                        
                                    notifier.send_message("🎉 批量单点指令已全部双轨执行完毕!")

                                else:
                                    notifier.send_message("⚠️ 启动大范围全局雷达扫描,正在过滤顶级父目录防止STRM外溢...")
                                    
                                    scan_roots = set()
                                    # 核心修正:只从具体订阅库中精确组装有效的“剧集父目录”层级
                                    for tid, info in subs.items():
                                        p = info.get("path", "") if isinstance(info, dict) else ""
                                        if p:
                                            clean_p = get_openlist_path(p)
                                            safe_butler_path = re.sub(r'(?i)/Season\s*\d+/?$', '', clean_p)
                                            scan_roots.add(safe_butler_path)

                                    notifier.send_message(f"⏳ 精确锁定 {len(scan_roots)} 个安全剧集锚点,开始双轨下发...")
                                    
                                    o_headers = None
                                    try:
                                        r_log = requests.post(f"{API_5244_URL}/api/auth/login", 
                                                              json={"username": OLIST_USER, "password": OLIST_PASS}, timeout=5).json()
                                        if r_log.get("code") == 200: 
                                            o_headers = {"Authorization": r_log["data"]["token"], "Content-Type": "application/json"}
                                    except: pass
                                    
                                    for rp in scan_roots:
                                        # 💥 无论 CAS 还是普通视频,全域强制穿透 Openlist 缓存
                                        if o_headers:
                                            try:
                                                requests.post(f"{API_5244_URL}/api/fs/list", 
                                                              json={"path": rp, "refresh": True}, headers=o_headers, timeout=10).close()
                                                time.sleep(1.5) # 批量扫描稍微给点延时错峰即可
                                            except: pass

                                        # 🔀 严格的双轨路由分流
                                        if DIR_CAS_ROOT in rp:
                                            try: requests.get(f"{API_5000_URL}/api/sync", params={"path": rp}, timeout=3).close()
                                            except: pass
                                        else:
                                            try:
                                                s = load_json(SETTINGS_FILE)
                                                bash_path = s.get("bash_path", DEFAULT_BASH_PATH)
                                                refresh_sh = s.get("refresh_sh_path", DEFAULT_REFRESH_SH)
                                                subprocess.Popen([bash_path, refresh_sh, rp], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
                                            except: pass
                                        
                                    notifier.send_message(f"✅ 全区最高指令已下发!安全触发并双轨刷新了 {len(scan_roots)} 个雷达基点。")
                                # =================================================================
                                # 🌟 终极双轨驱动装甲替换结束
                                # =================================================================

                        elif text.startswith("补档"):
                            match_fill = re.match(r'^补档\s+(.*?)\s+(http[s]?://\S+)', text)
                            if match_fill:
                                keyword_input = match_fill.group(1).strip()
                                share_url = match_fill.group(2).strip()
                                m = re.match(r'^(.*?)\s*[sS第]?0?(\d+)[季]?$', keyword_input)
                                if m and m.group(1).strip(): base_kw, s_num = m.group(1).strip(), int(m.group(2))
                                else: base_kw, s_num = keyword_input, None

                                notifier.send_message(f"🔍 启动补档...\n🎯 解析剧名: {base_kw}\n🔗 链接: {share_url}")

                                subs = load_json(SUBS_FILE)
                                matched_target = None
                                for t_id, info in subs.items():
                                    path_in_db = info.get("path", "") if isinstance(info, dict) else ""
                                    kw_in_db = info.get("keyword", "") if isinstance(info, dict) else ""
                                    if base_kw.lower() in path_in_db.lower():
                                        if s_num is not None:
                                            s_patterns = [f"season {s_num}", f"s{s_num:02d}", f"s{s_num}"]
                                            if any(p in path_in_db.lower() for p in s_patterns) or str(s_num) in path_in_db.split('/')[-1]:
                                                matched_target = (t_id, path_in_db, kw_in_db); break
                                        else:
                                            matched_target = (t_id, path_in_db, kw_in_db); break

                                if not matched_target: continue

                                target_id, target_path, target_keyword = matched_target
                                notifier.send_message(f"🎯 命中目录: {target_path}\n🚀 核对云端文件中...")
                                
                                try: info_s = client_obj.getShareInfo(share_url)
                                except Exception as e:
                                    notifier.send_message(f"❌ 补档失效 ({e})")
                                    continue

                                try:
                                    all_files = get_all_share_files_recursive(info_s)
                                    if target_keyword: all_files = [f for f in all_files if all(k in f["full_path"].lower() for k in target_keyword.lower().split())]

                                    cloud_files = client_obj.listPrivateFiles(target_id)
                                    cloud_file_names = {cf["name"] for cf in cloud_files}

                                    new_files = []
                                    for f in all_files:
                                        expected_smart_name = generate_smart_name(f["name"], target_path)
                                        if expected_smart_name not in cloud_file_names and f["name"] not in cloud_file_names:
                                            new_files.append(f)

                                    openlist_target_path = get_openlist_path(target_path)

                                    if not new_files:
                                        notifier.send_message("⚠️ 核对完毕:无需重复拉取。")
                                        if target_path.startswith(DIR_CAS_ROOT) or target_path.startswith(DIR_CAS_ROOT.strip('/')):
                                            try: 
                                                requests.get(f"{API_5000_URL}/api/sync", params={"path": openlist_target_path}, timeout=3).close()
                                                notifier.send_message(f"✅ 管家同步指令已下发: {openlist_target_path}")
                                            except Exception as e: 
                                                notifier.send_message(f"❌ 管家同步无响应: {e}")
                                        else:
                                            try: 
                                                s = load_json(SETTINGS_FILE)
                                                bash_path = s.get("bash_path", DEFAULT_BASH_PATH)
                                                refresh_sh = s.get("refresh_sh_path", DEFAULT_REFRESH_SH)
                                                subprocess.Popen([bash_path, refresh_sh, openlist_target_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
                                                notifier.send_message(f"✅ Emby刷新指令已下发: {openlist_target_path}")
                                            except: pass
                                    else:
                                        taskInfos = [{"fileId": f["id"], "fileName": clean_filename(f["name"]), "isFolder": 0} for f in new_files]
                                        
                                        code = info_s.saveShareFiles(taskInfos, target_id)
                                        
                                        if code in [0, '0', None, False, '']:
                                            time.sleep(8)
                                            fresh_cloud_files = client_obj.listPrivateFiles(target_id)
                                            fresh_names = [cf["name"] for cf in fresh_cloud_files]
                                            
                                            history_data = load_json(HISTORY_FILE)
                                            actually_saved_count = 0
                                            for task in taskInfos:
                                                orig_name = task["fileName"]
                                                expected_smart_name = generate_smart_name(orig_name, target_path)
                                                if orig_name in fresh_names or (expected_smart_name and expected_smart_name in fresh_names):
                                                    history_data[str(task["fileId"])] = {"name": orig_name, "sub_id": str(target_id)}
                                                    actually_saved_count += 1
                                            
                                            if actually_saved_count > 0:
                                                renamed_files_list = []
                                                for task in taskInfos:
                                                    original_name = task["fileName"]
                                                    new_name = generate_smart_name(original_name, target_path)
                                                    if new_name != original_name:
                                                        for cf in fresh_cloud_files:
                                                            if cf["name"] == original_name:
                                                                if client_obj.renameFile(cf["id"], new_name): 
                                                                    renamed_files_list.append(new_name)
                                                                break

                                                save_json(HISTORY_FILE, history_data)
                                                notifier.send_message(f"✅ 补档完美结束!\n📂 成功抓取 {actually_saved_count} 个缺失文件。")
                                                if renamed_files_list:
                                                    if len(renamed_files_list) > 20:
                                                        r_msg = "\n".join([f" └ {n}" for n in renamed_files_list[:20]]) + f"\n...等共 {len(renamed_files_list)} 个文件"
                                                    else:
                                                        r_msg = "\n".join([f" └ {n}" for n in renamed_files_list])
                                                    notifier.send_message(f"✨ 补档云端洗名完成:\n{r_msg}")
                                                time.sleep(6)
                                                
                                                if target_path.startswith(DIR_CAS_ROOT) or target_path.startswith(DIR_CAS_ROOT.strip('/')):
                                                    try:
                                                        requests.get(f"{API_5000_URL}/api/sync", params={"path": openlist_target_path}, timeout=3).close()
                                                        notifier.send_message(f"✅ 管家同步指令已下发: {openlist_target_path}")
                                                    except Exception as e: 
                                                        notifier.send_message(f"❌ 管家同步无响应: {e}")
                                                else:
                                                    try:
                                                        s = load_json(SETTINGS_FILE)
                                                        bash_path = s.get("bash_path", DEFAULT_BASH_PATH)
                                                        refresh_sh = s.get("refresh_sh_path", DEFAULT_REFRESH_SH)
                                                        subprocess.Popen([bash_path, refresh_sh, openlist_target_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True)
                                                        notifier.send_message(f"✅ Emby刷新指令已下发: {openlist_target_path}")
                                                    except: pass
                                        else:
                                            notifier.send_message(f"❌ 天翼云拒绝转存: {code}")
                                except Exception as e:
                                    err_str = str(e).upper()
                                    if "INVALIDSESSIONKEY" in err_str or "CHECK IP ERROR" in err_str or "UNKNOWN_ERROR" in err_str or "UNKNOWN" in err_str:
                                        notifier.send_message(f"⚠️ 检测到 IP 漂移!正在自愈...")
                                        if os.path.exists(COOKIES_FILE): os.remove(COOKIES_FILE)
                                        auto_relogin(client_obj, force=True)
                                        notifier.send_message("✅ IP 漂移已修复!请重发指令。")
                                    else:
                                        notifier.send_message(f"❌ 补档异常: {e}")

                        # 🌟 修复:修正了 or 的语法错误,完美识别订阅指令
                        elif text.startswith("订阅") or text.startswith("绑定") or text.startswith("/sub "):
                            match_bind = re.match(r'^(订阅|绑定)(\d)?\s+', text)
                            if match_bind:
                                action = match_bind.group(1)
                                season_num = match_bind.group(2)
                                
                                freq_tag = ""
                                if "#周更" in text: freq_tag, text = "周更", text.replace("#周更", "").strip()
                                elif "#双更" in text: freq_tag, text = "双更", text.replace("#双更", "").strip()
                                elif "#剧迷" in text: freq_tag, text = "剧迷", text.replace("#剧迷", "").strip()
                                elif "#日更" in text: freq_tag, text = "日更", text.replace("#日更", "").strip()
                                elif "#完结" in text: freq_tag, text = "完结", text.replace("#完结", "").strip()
                                elif "#单次" in text: freq_tag, text = "单次", text.replace("#单次", "").strip()
                                elif "#电影" in text: freq_tag, text = "单次", text.replace("#电影", "").strip() # 🌟 加入电影快捷标签映射
                                
                                explicit_weekday = False
                                weekday_map = {"周一": 0, "周二": 1, "周三": 2, "周四": 3, "周五": 4, "周六": 5, "周日": 6}
                                t_weekday = 5
                                for d_name, d_code in weekday_map.items():
                                    if f"#{d_name}" in text:
                                        t_weekday = d_code
                                        explicit_weekday = True
                                        text = text.replace(f"#{d_name}", "").strip()
                                        break
                                
                                if explicit_weekday and not freq_tag:
                                    freq_tag = "周更"
                                        
                                custom_tags = []
                                for tag in CAT_ROUTER.keys():
                                    if f"#{tag}" in text:
                                        custom_tags.append(tag)
                                        text = text.replace(f"#{tag}", "").strip()
                                        
                                cat_map = {
                                    "电影": ["电影", "movie", "大电影"],
                                    "动漫": ["动漫", "动画", "新番", "番剧", "anime"],
                                    "综艺": ["综艺", "真人秀", "晚会"],
                                    "演唱会": ["演唱会", "live", "音乐会", "巡演"],
                                    "纪录片": ["纪录片", "documentary", "探索"],
                                    "短剧": ["短剧", "微短剧", "爽剧"]
                                }
                                explicit_cat = ""
                                for cat, aliases in cat_map.items():
                                    if f"#{cat}" in text:
                                        explicit_cat = cat
                                        text = text.replace(f"#{cat}", "").strip()
                                    for alias in aliases:
                                        if f"#{alias}" in text:
                                            if not explicit_cat: explicit_cat = cat
                                            text = text.replace(f"#{alias}", "").strip()
                                        
                                is_bind = (action == "绑定")
                                
                                # 🚨 核心修复:智能抓取中文提取码,防止变成瞎子
                                pwd_match = re.search(r'(?:访问码|提取码|密码)\s*[::]\s*([a-zA-Z0-9]{4})', text)
                                extracted_pwd = pwd_match.group(1) if pwd_match else None
                                # 把密码从文本里剔除,防止污染过滤关键词
                                text = re.sub(r'[\((]?\s*(?:访问码|提取码|密码)\s*[::]\s*[a-zA-Z0-9]{4}\s*[\))]?', '', text)
                                
                                parts = text.split()
                                share_url, keyword, target_path = "", "", ""

                                url_index = -1
                                for i, p in enumerate(parts):
                                    if p.startswith("http"):
                                        share_url, url_index = p, i
                                        break

                                if url_index != -1:
                                    # 把抓到的密码强行焊死在网址屁股后面
                                    if extracted_pwd:
                                        share_url += f"?pwd={extracted_pwd}" if "?" not in share_url else f"&pwd={extracted_pwd}"
                                        
                                    target_path = " ".join(parts[1:url_index])
                                    if url_index < len(parts) - 1: keyword = " ".join(parts[url_index+1:])
                                else:
                                    target_path = " ".join(parts[1:])

                                if season_num:
                                    s_num = int(season_num)
                                    if "season" not in target_path.lower(): target_path = f"{target_path.rstrip('/')}/Season {s_num}"
                                    if not re.search(r'(?i)S\d+', keyword): keyword = f"S{s_num:02d} {keyword}".strip()
                                    else: keyword = keyword.strip()

                                if not share_url:
                                    subs = load_json(SUBS_FILE)
                                    for tid, info_dict in subs.items():
                                        if isinstance(info_dict, dict) and info_dict.get("path") == target_path:
                                            share_url = info_dict.get("url", ""); break
                                            
                                    if not share_url: continue

                                is_absolute_path = target_path.startswith('/') or DIR_MEDIA_PREFIX in target_path or target_path.startswith(DIR_CAS_ROOT)
                                if not is_absolute_path:
                                    if not target_path:
                                        notifier.send_message("❌ 缺少【干净剧名】!\n为防乱套,请不要发纯链接。\n格式:订阅 剧集名称(年份) 链接")
                                        continue
                                        
                                    notifier.send_message("🔍 启动智能路由,探测通道与品类...")
                                    try:
                                        info_s = client_obj.getShareInfo(share_url)
                                        raw_name = info_s.file_name
                                        preview_files = get_all_share_files_recursive(info_s)
                                        is_cas = any(f['name'].lower().endswith('.cas') for f in preview_files)
                                        
                                        base_dir_large = "电视剧"
                                        base_dir_sub = "0-电视剧"
                                        
                                        if custom_tags:
                                            base_dir_large, base_dir_sub = CAT_ROUTER[custom_tags[0]]
                                        elif explicit_cat:
                                            base_dir_large = explicit_cat
                                            if explicit_cat == "电影": base_dir_sub = "0-电影"
                                            elif explicit_cat == "动漫": base_dir_sub = "0-动漫"
                                            else: base_dir_sub = ""
                                        else:
                                            combined_input = f"{target_path} {keyword} {freq_tag} {text}".lower()
                                            found_cat = False
                                            for cat in cat_map.keys():
                                                if f"#{cat}" in combined_input or freq_tag == cat:
                                                    base_dir_large = cat
                                                    if cat == "电影": base_dir_sub = "0-电影"
                                                    elif cat == "动漫": base_dir_sub = "0-动漫"
                                                    else: base_dir_sub = ""
                                                    found_cat = True; break
                                            
                                            if not found_cat:
                                                for cat, kws in cat_map.items():
                                                    if any(kw in raw_name.lower() for kw in kws):
                                                        base_dir_large = cat
                                                        if cat == "电影": base_dir_sub = "0-电影"
                                                        elif cat == "动漫": base_dir_sub = "0-动漫"
                                                        else: base_dir_sub = ""
                                                        break
                                        
                                        clean_user_path = target_path.strip()
                                        current_ym = datetime.now().strftime("%Y%m")
                                        
                                        # ==========================================
                                        # 🛡️ V7 真·究极形态:记忆体(DB) -> 物理雷达(云端) -> 新建(保底)
                                        # ==========================================
                                        subs_cache = load_json(SUBS_FILE)
                                        existing_path = None
                                        
                                        # 🌟 修改:废弃激进正则,保留版本号/4K等特征
                                        def get_pure(text): return text.replace(" ", "").lower()
                                        show_name = clean_user_path.split('/')[0].strip()
                                        pure_show = get_pure(show_name)
                                        
                                        best_match_path = None
                                        
                                        ignore_words = {get_pure(DIR_CAS_ROOT), get_pure(DIR_VIDEO_ROOT), "season", "s"}
                                        for cat_key, (large_cat, sub_cat) in CAT_ROUTER.items():
                                            ignore_words.add(get_pure(cat_key))
                                            ignore_words.add(get_pure(large_cat))
                                            if sub_cat: ignore_words.add(get_pure(sub_cat))
                                        
                                        # 【第一阶段】:扫描大脑记忆库 (subscriptions.json)
                                        for sid, info_dict in subs_cache.items():
                                            if isinstance(info_dict, dict):
                                                db_path = info_dict.get("path", "")
                                                db_is_cas = DIR_CAS_ROOT in db_path
                                                if is_cas != db_is_cas: continue
                                                
                                                db_folders = db_path.split('/')
                                                for idx, f_name in enumerate(db_folders):
                                                    pure_f = get_pure(f_name)
                                                    if not pure_f or len(pure_f) < 2: continue
                                                    if re.match(r'^\d{4,6}$', pure_f) or "season" in pure_f or re.match(r'^s\d+$', pure_f): continue
                                                    if pure_f in ignore_words: continue
                                                    
                                                    # 🌟 摒弃打分!严格要求版本特征一致
                                                    if pure_show == pure_f:
                                                        root_path = "/".join(db_folders[:idx+1])
                                                        best_match_path = root_path + clean_user_path[len(show_name):] if "/" in clean_user_path else root_path
                                                        break
                                                if best_match_path: break
                                        
                                        if best_match_path:
                                            existing_path = best_match_path
                                            type_msg = f"记忆库精确匹配沿用旧目录"
                                            
                                        # 【第二阶段】:如果大脑失忆了,启动天翼云物理雷达,扫描历史实体目录!
                                        if not existing_path:
                                            notifier.send_message("🧠 记忆库未找到记录,启动网盘物理层穿甲扫描,探测历史遗留目录...")
                                            try:
                                                root_for_search = DIR_CAS_ROOT if is_cas else DIR_VIDEO_ROOT
                                                base_search_path = f"{root_for_search}/{base_dir_large}/{base_dir_sub}".strip('/').replace("//", "/") if base_dir_sub else f"{root_for_search}/{base_dir_large}".strip('/')
                                                
                                                curr_id = -11
                                                valid_path = True
                                                for p in base_search_path.split('/'):
                                                    if not p: continue
                                                    nodes = client_obj.getObjectFolderNodes(curr_id)
                                                    matched = next((n for n in nodes if n["name"] == p), None)
                                                    if matched:
                                                        curr_id = matched["id"]
                                                    else:
                                                        valid_path = False; break
                                                
                                                if valid_path:
                                                    ym_nodes = client_obj.getObjectFolderNodes(curr_id)
                                                    ym_nodes.sort(key=lambda x: x["name"], reverse=True)
                                                    
                                                    phy_best_path = None
                                                    
                                                    for ym_node in ym_nodes:
                                                        if re.match(r'^\d{4,6}$', ym_node["name"]):
                                                            show_nodes = client_obj.getObjectFolderNodes(ym_node["id"])
                                                            for show_node in show_nodes:
                                                                pure_f = get_pure(show_node["name"])
                                                                # 🌟 摒弃打分!
                                                                if pure_show == pure_f:
                                                                    found_physical_path = f"/{base_search_path}/{ym_node['name']}/{show_node['name']}"
                                                                    phy_best_path = found_physical_path + clean_user_path[len(show_name):] if "/" in clean_user_path else found_physical_path
                                                                    break
                                                        if phy_best_path: break
                                                
                                                if phy_best_path:
                                                    existing_path = phy_best_path
                                                    type_msg = f"网盘实体精确寻回"
                                            except Exception as e:
                                                logger.warning(f"⚠️ 物理雷达扫描异常 (防风控跳过): {e}")
                                                
                                        # 【第三阶段】:彻底判定为全新资源,按当前系统年月打基建
                                        if existing_path:
                                            target_path = existing_path
                                        else:
                                            if is_cas:
                                                target_path = f"{DIR_CAS_ROOT}/{base_dir_large}/{base_dir_sub}/{current_ym}/{clean_user_path}".replace("//", "/") if base_dir_sub else f"{DIR_CAS_ROOT}/{base_dir_large}/{current_ym}/{clean_user_path}".replace("//", "/")
                                                type_msg = "全新CAS秒传(建新基建)"
                                            else:
                                                target_path = f"{DIR_VIDEO_ROOT}/{base_dir_large}/{base_dir_sub}/{current_ym}/{clean_user_path}".replace("//", "/") if base_dir_sub else f"{DIR_VIDEO_ROOT}/{base_dir_large}/{current_ym}/{clean_user_path}".replace("//", "/")
                                                type_msg = "全新智能直连(建新基建)"
                                        
                                        if base_dir_large == "电影" and not freq_tag:
                                            freq_tag = "单次"
                                            
                                        notifier.send_message(f"🧠 路由组装完毕 [{type_msg}]!\n📂 建档至: {target_path}")
                                    except Exception as e:
                                        err_str = str(e).upper()
                                        if "INVALIDSESSIONKEY" in err_str or "CHECK IP ERROR" in err_str or "UNKNOWN_ERROR" in err_str or "UNKNOWN" in err_str:
                                            notifier.send_message(f"⚠️ 探测到 IP 漂移!正在自愈...")
                                            if os.path.exists(COOKIES_FILE): os.remove(COOKIES_FILE)
                                            auto_relogin(client_obj, force=True)
                                            notifier.send_message("✅ 自愈完成,请重发指令。")
                                        else:
                                            notifier.send_message(f"❌ 智能解析失败 ({e})")
                                        continue

                                tag_msg = f" ⏱️ 频率: {freq_tag}" if freq_tag else ""
                                kw_msg = f" 🎯 过滤: {keyword}" if keyword else ""
                                notifier.send_message(f"⏳ 正在处理{action}:\n📁 {target_path}{tag_msg}{kw_msg} ...")
                                
                                try: info_s = client_obj.getShareInfo(share_url)
                                except Exception as e:
                                    notifier.send_message(f"❌ {action}失败:{e}")
                                    continue
                                    
                                try:
                                    target_id = client_obj.mkdirAll(target_path)
                                    subs = load_json(SUBS_FILE)
                                    subs[str(target_id)] = {"url": share_url, "keyword": keyword, "path": target_path, "last_update": 0, "freq": freq_tag, "update_weekday": t_weekday, "next_check_time": 0}
                                    save_json(SUBS_FILE, subs)
                                    
                                    if is_bind:
                                        all_files = get_all_share_files_recursive(info_s)
                                        if keyword: all_files = [f for f in all_files if all(k in f["full_path"].lower() for k in keyword.lower().split())]
                                        history_data = load_json(HISTORY_FILE)
                                        for f in all_files: history_data[str(f["id"])] = {"name": f["name"], "sub_id": str(target_id)}
                                        save_json(HISTORY_FILE, history_data)
                                        notifier.send_message(f"✅ 成功绑定!\n❇️ 标记了 {len(all_files)} 个旧文件。")
                                    else:
                                        notifier.send_message(f"✅ 添加成功!优先拉取资源...")
                                        check_subscriptions(client_obj, force_target_id=target_id) 
                                except Exception as e:
                                    err_str = str(e).upper()
                                    if "INVALIDSESSIONKEY" in err_str or "CHECK IP ERROR" in err_str or "UNKNOWN_ERROR" in err_str or "UNKNOWN" in err_str:       
                                        notifier.send_message(f"⚠️ 建档探测到 IP 漂移!正在自愈...")
                                        if os.path.exists(COOKIES_FILE): os.remove(COOKIES_FILE)
                                        auto_relogin(client_obj, force=True)
                                        notifier.send_message("✅ 自愈完成,请重发指令。")
                                    else:
                                        notifier.send_message(f"❌ 云端拦截: {e}")
        except Exception: pass 
        time.sleep(2)


trigger_app = Flask(__name__)
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
# ==========================================
# 🌟 5555 端口:全域双轨智能路由 (支持本地与云端混动)
# ==========================================
@trigger_app.route('/force_harvest', methods=['GET', 'POST'])
def force_harvest():
    target_dir = request.args.get('path')
    if not target_dir and request.is_json:
        target_dir = request.json.get('path')
        
    if target_dir:
        s = load_json(SETTINGS_FILE)
        # 🌟 坚决使用顶部的变量,彻底消灭 500 报错!
        local_base = s.get("local_dropbox_dir", DEFAULT_LOCAL_DROPBOX)
        local_root = s.get("local_storage_root", DEFAULT_LOCAL_ROOT)
        
        if target_dir.startswith(local_base) or target_dir.startswith(local_root):
            logger.info(f"⚡ 收到【本地投递】闪电信号!目标: {target_dir}")
            # 🌟 扔进后台线程!瞬间回复外部脚本 200,绝不让外部脚本因为等待而报错超时!
            threading.Thread(target=scan_local_dropbox, args=(target_dir,)).start()
        else:
            logger.info(f"⚡ 收到【云端收割】精准信号!目标: {target_dir}")
            threading.Thread(target=process_cas_via_olist_api, args=(target_dir,)).start()
    else:
        logger.info("⚡ 收到全量收割信号!开始双轨扫描所有目录...")
        threading.Thread(target=scan_local_dropbox).start()
        threading.Thread(target=process_cas_via_olist_api).start()
        
    return "✅ 收割指令已交由后台接管", 200


if __name__ == '__main__':
    os.makedirs(DB_DIR, exist_ok=True)
    
    # 启动后台守护线程监听 5555
    threading.Thread(target=lambda: trigger_app.run(host='0.0.0.0', port=TRIGGER_PORT, debug=False, use_reloader=False), daemon=True).start()
    logger.info(f"🎧 本地监听端口 {TRIGGER_PORT} 已启动,随时等待下载脚本呼叫...")

    notifier = TelegramNotifier(TG_BOT_TOKEN, TG_ADMIN_USER_ID)
    notifier.send_message(f"🤖 追剧转存引擎 (V7.8 智能路由版) 启动,获取凭证...")
    
    try:
        logger.info("✅ [系统] 189底层接口握手成功,正在挂载凭证...")
        client = Cloud189()
        client.login(ENV_189_CLIENT_ID, ENV_189_CLIENT_SECRET)
        last_login_time = time.time()
        notifier.send_message("✅ 网盘登录成功!全天候监控已就位。")
    except Exception as e:
        logger.error(f"❌ [系统] 登录失败: {e}")
        notifier.send_message(f"❌ 首次登录失败: {e}\n(脚本已进入休眠模式防止被封)")
        time.sleep(600)
        sys.exit(-1)
        
    main_control_loop(client)

3.2电脑文件传到手机storage/downloads目录下

3.3复制脚本

cp ~/storage/downloads/auto189.py ~/189py/auto189.py

4.点火升空 & 日常操作 前台启动

python ~/189py/auto189.py

后台静默启动

nohup python ~/189py/auto189.py > run.log 2>&1 &

pm2启动:

cd ~/189py && pm2 start auto189.py --name "auto189" --interpreter python

(cd ~/189py && nohup python3 auto_189.py > run.log 2>&1 &) && echo "✅ TG 追剧管家已启动,日志记录于 ~/run.log"

5.清理脏数据,正式接客

在 Termux 里直接执行这两行命令(先清空数据库,再重新启动):

# 1. 强制清空订阅数据库和历史记录(把脏数据扬了)
pkill -f auto189.py
rm -f ~/189py/db/history.json ~/189py/db/subscriptions.json

# 2. 重新启动脚本!
python ~/189py/auto189.py

三、秒传播放casplay.py

casplay.py与auto189.py同目录下操作方法一样

1.py内容:

import base64, json, time, random, hashlib, hmac, urllib.parse, threading, uuid, os, requests, logging, subprocess, math
import socket, re, functools
import urllib3
urllib3.util.connection.HAS_IPV6 = False
from collections import deque
from flask import Flask, request, redirect, render_template_string, jsonify
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import pad

old_getaddrinfo = socket.getaddrinfo
def new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
    responses = old_getaddrinfo(host, port, family, type, proto, flags)
    if host == '::':
        return responses
    return [res for res in responses if res[0] == socket.AF_INET]
socket.getaddrinfo = new_getaddrinfo

# ==========================================
# 🏠 局域网探针
# ==========================================
def get_lan_server_ip(req):
    host = req.host.split(':')[0]
    if re.match(r'^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|127\.0\.0\.1)', host):
        return host
    return None

# ==========================================
# ⚙️ 默认系统配置 (主服务)
# ==========================================
DEFAULT_CONFIG = {
    "server_host": "https://play.363689.xyz",
    "delete_delay": 600,
    "shield_delay": 2700,
    "cloud_strategy": "hash", 
    "force_mode_b": "false", # 🔥 坚决保留:用户指定的全局强制直连开关
    "family_clouds": [],
    "openlist_host": "http://127.0.0.1:5244",
    "openlist_token": "", 
    
    "local_cas_source_dir": "/storage/emulated/0/Download/cas_source",
    
    "network_cas_path": "/177/177-秒传",
    "local_strm_dir": "/storage/emulated/0/Download/cas_strm_modeA",
    
    "network_cas_path_native": "/177/177-原生直连",
    "local_strm_dir_native": "/storage/emulated/0/Download/cas_strm_modeB",
    
    "network_media_path": "/177/177-常规视频",
    "local_strm_dir_media": "/storage/emulated/0/Download/cas_strm_media",

    "network_cas_path_139": "/139/139-秒传",
    "local_strm_dir_139": "/storage/emulated/0/Download/cas_strm_139",
    "yun139_auth": "",
    "yun139_host": "https://caiyun.feixin.10086.cn:7071",
    "openlist_host_139": "http://127.0.0.1:5255",
    "openlist_token_139": "",
    
    "pushplus_token": "",
    "tg_bot_token": "",
    "tg_chat_id": ""
}

EMBY_HOST = "http://127.0.0.1:8096"
API_KEY_LINUX = "751c095055f8493d8e63eb755369b9aa"
API_KEY_APP = "66644805d4bc45ea91b2a5e5eca22105"

app_main = Flask('cas_server_5000')
app_302 = Flask('nginx_302_5001')

last_refresh_time = 0
upload_cache = {}
native_link_cache = {}
cache_lock = threading.Lock()
rr_index = 0

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_DIR = os.path.join(BASE_DIR, "db")
os.makedirs(DB_DIR, exist_ok=True)

def get_db_path(): return os.path.join(DB_DIR, "config.json")

def format_size(size_in_bytes):
    try:
        size = float(size_in_bytes)
        if size < 1024 * 1024 * 1024: return f"{size / (1024 * 1024):.2f} MB"
        else: return f"{size / (1024 * 1024 * 1024):.2f} GB"
    except: return "未知大小"

# ==========================================
# 🔔 统一看板日志与推送系统
# ==========================================
log_buffer = deque(maxlen=150) 
class MemoryHandler(logging.Handler):
    def emit(self, record):
        msg = self.format(record)
        log_buffer.append({'time': time.strftime("%H:%M:%S"), 'level': record.levelname, 'msg': f"💖 [管家] {msg}"})

logger = logging.getLogger('CAS_Server')
logger.setLevel(logging.INFO)
mem_handler = MemoryHandler()
mem_handler.setFormatter(logging.Formatter('%(message)s'))
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(mem_handler)
logger.addHandler(stream_handler)

logging.getLogger('werkzeug').setLevel(logging.ERROR)

@app_main.route('/api/remote_log', methods=['POST'])
def receive_remote_log():
    try:
        data = request.json
        log_buffer.append({'time': time.strftime("%H:%M:%S"), 'level': data.get('level', 'INFO'), 'msg': f"💜 [引擎] {data.get('msg', '')}"})
        return "OK", 200
    except: return "Error", 400

def send_push(title, content):
    def _do_push():
        cfg = read_config()
        if cfg.get('pushplus_token'):
            try: requests.get(f"http://www.pushplus.plus/send?token={cfg['pushplus_token']}&title={urllib.parse.quote(title)}&content={urllib.parse.quote(content)}&template=html", timeout=5)
            except Exception as e: logger.error(f"微信推送失败: {e}")
        if cfg.get('tg_bot_token') and cfg.get('tg_chat_id'):
            proxy_config = {"http": "http://127.0.0.1:7890", "https": "http://127.0.0.1:7890"}
            try: requests.post(f"https://api.telegram.org/bot{cfg['tg_bot_token']}/sendMessage", data={"chat_id": cfg['tg_chat_id'], "text": f"🚨 <b>{title}</b>\n\n{content.replace('<br>', '\n')}", "parse_mode": "HTML"}, proxies=proxy_config, timeout=5)
            except Exception as e: logger.error(f"TG推送失败: {e}")
    threading.Thread(target=_do_push, daemon=True).start()

# ==========================================
# 🔑 天翼云独立鉴权引擎
# ==========================================
def rsaEncrpt(password, public_key):
    rsakey = RSA.importKey(public_key)
    cipher = PKCS1_v1_5.new(rsakey)
    return cipher.encrypt(password.encode()).hex()

def get_session_key_via_api(session_obj, source="未知来源"):
    try:
        url = "https://cloud.189.cn/v2/getUserBriefInfo.action"
        headers = {"Accept": "application/json;charset=UTF-8", "Referer": "https://cloud.189.cn/"}
        res = session_obj.get(url, headers=headers, timeout=10).json()
        sk = res.get("sessionKey")
        if sk: logger.info(f"✨ [{source} 获取成功] 拿到新鲜 SESSION_KEY!凭证尾号: {sk[-6:]}")
        return sk
    except Exception as e:
        logger.error(f"提取 sessionKey 失败: {e}")
        return None

class Cloud189AuthEngine:
    def __init__(self):
        self.session = requests.session()
        self.session.headers = {'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json;charset=UTF-8"}

    def getEncrypt(self): return self.session.post("https://open.e.189.cn/api/logbox/config/encryptConf.do", data={'appId': 'cloud'}, timeout=15).json()['data']['pubKey']

    def getRedirectURL(self):
        rsp = self.session.get('https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https://cloud.189.cn/web/redirect.html?returnURL=/main.action', timeout=15)
        return urllib.parse.parse_qs(urllib.parse.urlparse(rsp.url).query)

    def do_login_and_get_key(self, username, password, slot_name="卡槽自愈"):
        encryptKey = self.getEncrypt()
        query = self.getRedirectURL()
        resData = self.session.post('https://open.e.189.cn/api/logbox/oauth2/appConf.do', data={"version": '2.0', "appKey": 'cloud'}, headers={"Referer": 'https://open.e.189.cn/', "lt": query["lt"][0], "REQID": query["reqId"][0]}, timeout=15).json()
        keyData = f"-----BEGIN PUBLIC KEY-----\n{encryptKey}\n-----END PUBLIC KEY-----"
        data = {"appKey": 'cloud', "version": '2.0', "accountType": '01', "mailSuffix": '@189.cn', "returnUrl": resData['data']['returnUrl'], "paramId": resData['data']['paramId'], "clientType": '1', "isOauth2": "false", "userName": f"{{NRP}}{rsaEncrpt(username, keyData)}", "password": f"{{NRP}}{rsaEncrpt(password, keyData)}"}
        result = self.session.post('https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do', data=data, headers={'Referer': 'https://open.e.189.cn/', 'lt': query["lt"][0], 'REQID': query["reqId"][0]}, timeout=15).json()
        if result['result'] == 0:
            self.session.get(result['toUrl'], headers={"Host": 'cloud.189.cn'}, timeout=15)
            sk = get_session_key_via_api(self.session, slot_name)
            if sk: return sk
            else: raise Exception("账号密码验证成功,但在接口中未找到 sessionKey")
        else: raise Exception(result['msg'])

slot6_cache = {"sk": "", "cookie_mtime": 0}

def get_auto189_session_key():
    cookie_file = os.path.join(DB_DIR, "cookies.json")
    if not os.path.exists(cookie_file): return ""
    mtime = os.path.getmtime(cookie_file)
    if slot6_cache["sk"] and slot6_cache["cookie_mtime"] == mtime: return slot6_cache["sk"]
    session = requests.Session()
    try:
        with open(cookie_file, 'r', encoding='utf-8') as f: session.cookies.update(json.load(f))
        sk = get_session_key_via_api(session, "卡槽6-外部同步") or ""
        slot6_cache["sk"] = sk
        slot6_cache["cookie_mtime"] = mtime
        return sk
    except: return ""

def save_config(cfg):
    cfg_path = get_db_path()
    with open(cfg_path, 'w', encoding='utf-8') as f: json.dump(cfg, f, ensure_ascii=False, indent=4)

def read_config():
    cfg_path = get_db_path()
    cfg = DEFAULT_CONFIG.copy()
    try:
        if os.path.exists(cfg_path):
            with open(cfg_path, 'r', encoding='utf-8') as f: cfg.update(json.load(f))
    except: pass
    
    if len(cfg.get('family_clouds', [])) > 5:
        if not cfg['family_clouds'][5].get('username'):
            old_sk = cfg['family_clouds'][5].get('session_key', '')
            new_sk = get_auto189_session_key()
            if new_sk:
                cfg['family_clouds'][5]['session_key'] = new_sk
                if old_sk != new_sk: save_config(cfg)
    return cfg

def refresh_slot_logic(slot_idx, cfg):
    if slot_idx < len(cfg.get('family_clouds', [])):
        fc = cfg['family_clouds'][slot_idx]
        user, pwd = fc.get('username'), fc.get('password')
        if user and pwd:
            logger.info(f"🔄 卡槽 {slot_idx+1} 凭证失效,正在使用账号 {user} 自动重登自愈...")
            send_push("🔄 追剧管家自愈启动", f"检测到 卡槽 {slot_idx+1} 凭证失效,引擎已介入执行自动重登。")
            try:
                auth = Cloud189AuthEngine()
                new_sk = auth.do_login_and_get_key(user, pwd, f"卡槽{slot_idx+1}")
                if new_sk:
                    fc['session_key'] = new_sk
                    latest_cfg = read_config()
                    if slot_idx < len(latest_cfg.get('family_clouds', [])):
                        latest_cfg['family_clouds'][slot_idx]['session_key'] = new_sk
                        save_config(latest_cfg)
                        
                    if 'session_key' in fc and fc['session_key'] in client.rsa_keys: del client.rsa_keys[fc['session_key']]
                    logger.info(f"✅ 卡槽 {slot_idx+1} 满血复活!")
                    return new_sk
            except Exception as e:
                logger.error(f"❌ 卡槽 {slot_idx+1} 自动续期失败: {e}")
                send_push("❌ 追剧管家自愈失败", f"卡槽 {slot_idx+1} 尝试自动重登失败!<br>报错信息: {e}")
        elif slot_idx == 5: 
            logger.warning("🚨 备用卡槽 6 外部凭证失效,已清理本地废弃凭证,等待外部定时任务更新。")
            send_push("⚠️ 卡槽 6 离线转移", "卡槽 6 外部凭证失效。已自动丢弃旧凭证,当前播放请求将无缝转移至主卡槽。")
            cookie_path = os.path.join(DB_DIR, "cookies.json")
            if os.path.exists(cookie_path): os.remove(cookie_path)
            slot6_cache["sk"] = ""
            if len(cfg.get('family_clouds', [])) > 5:
                latest_cfg = read_config()
                if len(latest_cfg.get('family_clouds', [])) > 5:
                    latest_cfg['family_clouds'][5]['session_key'] = ""
                    save_config(latest_cfg)
            return None
        else: logger.error(f"❌ 卡槽 {slot_idx+1} 缺少账号密码配置,无法执行自愈!")
    return None

def get_target_cloud(cfg, bind_key="", file_size=0):
    global rr_index
    raw_clouds = cfg.get('family_clouds', [])
    def is_valid(c): return c and c.get('family_id') and c.get('hard_folder_id') and c.get('openlist_prefix')

    valid_clouds = [(c, i) for i, c in enumerate(raw_clouds) if is_valid(c)]
    if not valid_clouds: return None, -1

    strategy = cfg.get('cloud_strategy', 'hash')
    
    if strategy == 'primary' and len(raw_clouds) > 0 and is_valid(raw_clouds[0]): return raw_clouds[0], 0
    elif strategy == 'slot2' and len(raw_clouds) > 1 and is_valid(raw_clouds[1]): return raw_clouds[1], 1
    elif strategy == 'slot3' and len(raw_clouds) > 2 and is_valid(raw_clouds[2]): return raw_clouds[2], 2
    elif strategy == 'slot4' and len(raw_clouds) > 3 and is_valid(raw_clouds[3]): return raw_clouds[3], 3
    elif strategy == 'slot5' and len(raw_clouds) > 4 and is_valid(raw_clouds[4]): return raw_clouds[4], 4
    elif strategy == 'slot6' and len(raw_clouds) > 5 and is_valid(raw_clouds[5]): return raw_clouds[5], 5

    try: size_int = int(file_size)
    except: size_int = 0
    if size_int > 28 * 1024 * 1024 * 1024:
        big_clouds = [(c, i) for c, i in valid_clouds if i in [4, 5]]
        if big_clouds:
            if bind_key:
                hash_idx = int(hashlib.md5(bind_key.encode('utf-8')).hexdigest(), 16) % len(big_clouds)
                target = big_clouds[hash_idx]
            else: target = random.choice(big_clouds)
            logger.info(f"⚖️ 容量调度介入:检测到超大文件 ({size_int/(1024**3):.2f} GB),无视常规策略,强制锁定大容量 [卡槽 {target[1]+1}]")
            return target[0], target[1]

    general_clouds = [(c, i) for c, i in valid_clouds if i < 4]
    if not general_clouds: general_clouds = valid_clouds 

    if strategy == 'hash':
        if bind_key:
            hash_idx = int(hashlib.md5(bind_key.encode('utf-8')).hexdigest(), 16) % len(general_clouds)
            return general_clouds[hash_idx][0], general_clouds[hash_idx][1]
    elif strategy == 'random': return random.choice(general_clouds)[0], random.choice(general_clouds)[1]
    elif strategy == 'round_robin':
        target = general_clouds[rr_index % len(general_clouds)]
        rr_index += 1
        return target[0], target[1]
        
    return valid_clouds[0][0], valid_clouds[0][1]

# ==========================================
# 🧹 全量清空逻辑
# ==========================================
def force_clear_all_worker():
    logger.info("🧨 收到最高指令:开始精准清理家庭云缓存及回收站...")
    cfg = read_config()
    with cache_lock: upload_cache.clear()
    for i, fc in enumerate(cfg.get('family_clouds', [])):
        fam_id = fc.get('family_id')
        fold_id = fc.get('hard_folder_id')
        sk = fc.get('session_key')
        if not sk: sk = refresh_slot_logic(i, cfg)
        if fam_id and fold_id and sk:
            try:
                items = client.get_family_items(fam_id, fold_id, sk)
                del_count = 0
                for item in items:
                    if client.delete_item(fam_id, item['fileId'], sk): del_count += 1
                time.sleep(1)
                if client.empty_family_recycle(fam_id, sk): logger.info(f"✅ 卡槽 {i+1} 清理完毕: 移除了 {del_count} 个秒传文件。")
            except Exception as e: logger.error(f"❌ 卡槽 {i+1} 清理异常: {e}")
    logger.info("🎉 缓存清空作业已圆满完成!")

@app_main.route('/api/clear_all', methods=['POST'])
def api_clear_all():
    threading.Thread(target=force_clear_all_worker, daemon=True).start()
    return "✅ 清空指令下发成功", 200

# ==========================================
# 🖥️ ADMIN 界面与配置路由
# ==========================================
@app_main.route('/admin/config', methods=['POST'])
def update_global_config():
    old_cfg = read_config()
    old_clouds = old_cfg.get('family_clouds', [])
    cfg = DEFAULT_CONFIG.copy()
    for k, v in old_cfg.items(): cfg[k] = v
    
    clouds = []
    for i in range(6):
        fid = request.form.get(f'fc_id_{i}', '').strip()
        hid = request.form.get(f'fc_fd_{i}', '').strip()
        px = request.form.get(f'fc_prefix_{i}', '').strip()
        mt = request.form.get(f'fc_mount_{i}', '').strip()
        user = request.form.get(f'fc_user_{i}', '').strip()
        pwd = request.form.get(f'fc_pwd_{i}', '').strip()
        sk = request.form.get(f'fc_sk_{i}', '').strip()
        
        if i < len(old_clouds):
            old_user = old_clouds[i].get('username', '')
            old_pwd = old_clouds[i].get('password', '')
            if user != old_user or pwd != old_pwd: sk = "" 

        if fid and hid: clouds.append({"family_id": fid, "hard_folder_id": hid, "openlist_prefix": px, "openlist_mount_path": mt, "username": user, "password": pwd, "session_key": sk})
        else: clouds.append({}) 
            
    cfg['family_clouds'] = clouds
    cfg['cloud_strategy'] = request.form.get('cloud_strategy', 'hash')
    # 🔥 保存强制直连的复选框状态
    cfg['force_mode_b'] = request.form.get('force_mode_b', 'false')
    
    for k in cfg.keys():
        if k not in ['family_clouds', 'cloud_strategy', 'force_mode_b'] and k in request.form:
            val = request.form.get(k, '').strip()
            if k in ['delete_delay', 'shield_delay']: cfg[k] = int(val) if val else (600 if k == 'delete_delay' else 2700)
            else: cfg[k] = val
    save_config(cfg)
    client.rsa_keys.clear() 
    logger.info(f"⚙️ 矩阵重组!所有配置均已保存。")
    return redirect("/admin?msg=所有配置已保存并实时生效")

ADMIN_HTML = """
<!DOCTYPE html>
<html>
<head>
    <title>💖 追剧管家后台面板</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f4f6f9; margin: 0; padding: 20px; color: #333; }
        .container { max-width: 950px; margin: 0 auto; }
        .header { background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; border-left: 5px solid #6366f1; }
        .card { background: #fff; padding: 25px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0,0,0,0.03); margin-bottom: 20px; }
        h2 { margin: 0; color: #1e293b; } h3 { margin-top: 0; color: #334155; font-size: 1.1rem; border-bottom: 1px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 15px; }
        h4 { color: #475569; margin-bottom: 10px; padding-bottom: 5px; border-bottom: 1px dashed #cbd5e1; }
        .badge { background: #10b981; color: white; padding: 5px 12px; border-radius: 20px; font-size: 12px; font-weight: bold; letter-spacing: 1px; }
        label { display: block; margin-bottom: 6px; font-weight: 600; color: #64748b; font-size: 12px; }
        input, select { width: 100%; padding: 10px; margin-bottom: 15px; border: 1px solid #cbd5e1; border-radius: 6px; box-sizing: border-box; background: #f8fafc; transition: all 0.3s; font-size: 13px; }
        input:focus, select:focus { border-color: #6366f1; outline: none; box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); background: #fff; }
        button { background: #6366f1; color: white; border: none; padding: 10px 18px; border-radius: 6px; cursor: pointer; font-weight: bold; transition: 0.2s; }
        button:hover { background: #4f46e5; transform: translateY(-1px); }
        .btn-purple { background: #8b5cf6; width: 100%; } .btn-purple:hover { background: #7c3aed; }
        .btn-green { background: #10b981; width: 100%; } .btn-green:hover { background: #059669; }
        .btn-blue { background: #0ea5e9; width: 100%; } .btn-blue:hover { background: #0284c7; }
        .btn-orange { background: #f97316; width: 100%; } .btn-orange:hover { background: #ea580c; }
        .grid { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: 20px; }
        .status-grid { display: grid; grid-template-columns: 0.8fr 1.2fr 1fr; gap: 20px; align-items: center; }
        .status-msg { padding: 12px; border-radius: 6px; margin-bottom: 20px; background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; text-align: center; font-weight: bold; }
        .cloud-box { border: 1px dashed #cbd5e1; padding: 15px; border-radius: 8px; margin-bottom: 15px; background: #fafafa;}
        .cloud-title { font-size: 13px; font-weight: bold; color: #6366f1; margin-bottom: 10px;}
        .path-box { background: #f1f5f9; padding: 15px; border-radius: 8px; margin-bottom: 15px; border: 1px solid #e2e8f0; }
        .mac-window { background: #1e293b; border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); overflow: hidden; margin-bottom: 20px; }
        .mac-header { background: #0f172a; padding: 10px 15px; display: flex; gap: 8px; align-items: center; }
        .mac-btn { width: 12px; height: 12px; border-radius: 50%; }
        .btn-close { background: #ef4444; } .btn-min { background: #f59e0b; } .btn-max { background: #10b981; }
        .mac-title { color: #64748b; font-size: 12px; margin-left: 10px; font-weight: bold; letter-spacing: 1px; }
        .console { background: #1e293b; color: #cbd5e1; padding: 15px; height: 350px; overflow-y: auto; overflow-x: hidden; font-family: 'Consolas', 'Courier New', monospace; font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
        .log-time { color: #64748b; margin-right: 8px; display: inline-block; vertical-align: top; }
        .log-msg { display: inline; }
        .log-INFO { color: #34d399; } .log-WARNING { color: #fbbf24; } .log-ERROR { color: #f87171; font-weight: bold; }
        .log-SUCCESS { color: #10b981; font-weight: bold; }
        @media (max-width: 768px) { .grid { grid-template-columns: 1fr; } .status-grid { grid-template-columns: 1fr; gap: 15px; } }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h2>💖 追剧管家 V9.2 <span style="font-size:12px; color:#8b5cf6;">双模式版本</span></h2>
            <span class="badge">SYSTEM ONLINE</span>
        </div>
        {% if msg %}<div class="status-msg">{{ msg }}</div>{% endif %}
        <div class="mac-window">
            <div class="mac-header">
                <div class="mac-btn btn-close"></div><div class="mac-btn btn-min"></div><div class="mac-btn btn-max"></div>
                <div class="mac-title">追剧控制台 - 实时运行日志</div>
            </div>
            <div class="console" id="logBox">Loading terminal...</div>
        </div>
        <div class="card">
            <h3>📊 核心控制台 & 凭证雷达监控</h3>
            <div class="status-grid">
                <p style="color:#64748b; margin:0;">转存中军火库:<br><b style="color:#1e293b; font-size:1.8rem;">{{ cache_count }}</b> <span style="font-size:12px;">部剧集</span></p>
                <div style="color:#64748b; margin:0; font-size: 13px; line-height: 1.8; border-left: 2px solid #e2e8f0; padding-left: 15px;">
                    <b>🔑 矩阵凭证空投雷达:</b><br>
                    {% for i in range(6) %}
                        {% set fc = cfg.family_clouds[i] if i < cfg.family_clouds|length else {} %}
                        {% set sk = fc.get('session_key', '') %}
                        天翼卡槽 {{ i+1 }}: 
                        {% if sk %}<span style="color:#10b981; font-weight:bold;">已挂载 (尾号{{ sk[-4:] }})</span>
                        {% else %}<span style="color:#f43f5e; font-weight:bold;">等待获取...</span>{% endif %}<br>
                    {% endfor %}
                    <b>🟠 移动云139:</b>
                    {% if cfg.openlist_host_139 %}<span style="color:#10b981; font-weight:bold;">配置已连接</span>
                    {% else %}<span style="color:#f43f5e; font-weight:bold;">未配置接口</span>{% endif %}
                </div>
                <div style="display:flex; flex-direction:column; gap:8px;">
                    <button type="button" onclick="syncOpenList('189', 'cas')" class="btn-purple" style="height:38px;">🔄 云端同步 (模式A: 稳健秒传)</button>
                    <button type="button" onclick="syncOpenList('189', 'cas_native')" class="btn-green" style="height:38px;">⚡ 云端同步 (模式B: 虚空直通)</button>
                    <button type="button" onclick="syncOpenList('189', 'media')" class="btn-blue" style="height:38px;">🎬 云端同步 (常规真实视频)</button>
                    <button type="button" onclick="syncLocalCas()" style="background:#8b5cf6; color:white; border:none; border-radius:6px; height:38px; cursor:pointer; font-weight:bold; transition: 0.2s;" onmouseover="this.style.background='#7c3aed'" onmouseout="this.style.background='#8b5cf6'">🗂️ 批量扫描本地 CAS (生成双轨 STRM)</button>
                    <button type="button" onclick="clearAllCache()" style="background:#ef4444; color:white; border:none; border-radius:6px; height:38px; cursor:pointer; font-weight:bold; transition: 0.2s;" onmouseover="this.style.background='#dc2626'" onmouseout="this.style.background='#ef4444'">🗑️ 一键清空189回收站</button>
                </div>
            </div>
        </div>
        <div class="card">
            <h3>⚙️ 综合配置中心</h3>
            <form method="POST" action="/admin/config">
            
                <div style="margin-bottom: 20px; padding: 15px; background: #fffbeb; border: 1px solid #fde68a; border-radius: 8px;">
                    <h4 style="color:#d97706; margin-top:0; border-bottom:none;">⚡全局强制openlist链接播放(无视播放器选择)</h4>
                    <label style="display:flex; align-items:center; font-size:14px; color:#b45309; cursor:pointer;">
                        <input type="checkbox" name="force_mode_b" value="true" {% if cfg.get('force_mode_b', '') | string | lower == 'true' %}checked{% endif %} style="width:20px; height:20px; margin-right:10px; margin-bottom:0;">
                        勾选此项,将无视播放器,强行将所有189播放请求跃迁为openlist直连 (模式B)!<br>(关闭则根据点击的STRM文件自然切换)
                    </label>
                </div>
                
                <div style="margin-bottom: 20px;">
                    <label>天翼点播分发策略 (Load Balancing)</label>
                    <select name="cloud_strategy">
                        <option value="hash" {% if cfg.cloud_strategy == 'hash' %}selected{% endif %}>🔗 剧名哈希绑定 (仅前4槽参与)</option>
                        <option value="round_robin" {% if cfg.cloud_strategy == 'round_robin' %}selected{% endif %}>🔁 顺序轮询分发 (仅前4槽参与)</option>
                        <option value="primary" {% if cfg.cloud_strategy == 'primary' %}selected{% endif %}>🥇 仅卡槽1分发</option>
                        <option value="slot2" {% if cfg.cloud_strategy == 'slot2' %}selected{% endif %}>🥈 仅卡槽2分发</option>
                        <option value="slot3" {% if cfg.cloud_strategy == 'slot3' %}selected{% endif %}>🥉 仅卡槽3分发</option>
                        <option value="slot4" {% if cfg.cloud_strategy == 'slot4' %}selected{% endif %}>💎 仅卡槽4分发</option>
                        <option value="slot5" {% if cfg.cloud_strategy == 'slot5' %}selected{% endif %}>🚀 仅卡槽5分发 (>28G重装甲)</option>
                        <option value="slot6" {% if cfg.cloud_strategy == 'slot6' %}selected{% endif %}>🛸 仅卡槽6分发 (备用大爷号专属)</option>
                        <option value="random" {% if cfg.cloud_strategy == 'random' %}selected{% endif %}>🎲 完全随机分发 (仅前4槽参与)</option>
                    </select>
                </div>
                <div class="grid">
                    <div>
                        <h3 style="margin-top:0; border:none;">🔵 天翼云 (189) 卡槽区</h3>
                        {% for i in range(6) %}
                        {% set fc = cfg.family_clouds[i] if i < cfg.family_clouds|length else {} %}
                        <div class="cloud-box">
                            <div class="cloud-title">📌 独立挂载槽位 {{ i + 1 }} {% if i == 5 %}(填账号则自愈){% else %}(静默自愈){% endif %}</div>
                            <input type="text" name="fc_id_{{ i }}" value="{{ fc.get('family_id', '') }}" placeholder="Family ID (留空则禁用此槽)">
                            <input type="text" name="fc_fd_{{ i }}" value="{{ fc.get('hard_folder_id', '') }}" placeholder="Folder ID">
                            <div style="display:flex; gap:10px;">
                                <input type="text" name="fc_user_{{ i }}" value="{{ fc.get('username', '') }}" placeholder="天翼云账号">
                                <input type="password" name="fc_pwd_{{ i }}" value="{{ fc.get('password', '') }}" placeholder="天翼云密码">
                            </div>
                            <input type="hidden" name="fc_sk_{{ i }}" value="{{ fc.get('session_key', '') }}">
                            <input type="text" name="fc_prefix_{{ i }}" value="{{ fc.get('openlist_prefix', '') }}" placeholder="OpenList 专属播放前缀">
                            <input type="text" name="fc_mount_{{ i }}" value="{{ fc.get('openlist_mount_path', '') }}" placeholder="OpenList 专属挂载目录" style="margin-bottom:0;">
                        </div>
                        {% endfor %}
                    </div>
                    <div>
                        <h3 style="margin-top:0; border:none;">📁 绝对物理隔离路径设置</h3>
                        
                        <div class="path-box">
                            <h4>📁 本地原始 CAS 库 (其他脚本下载存放处)</h4>
                            <label>本地 CAS 源目录</label>
                            <input type="text" name="local_cas_source_dir" value="{{ cfg.local_cas_source_dir }}" required style="margin-bottom:0;">
                        </div>

                        <div class="path-box">
                            <h4>🌐 189 模式A (稳健自愈秒传)</h4>
                            <label>云端 CAS 库扫描源目录</label>
                            <input type="text" name="network_cas_path" value="{{ cfg.network_cas_path }}" required>
                            <label>模式A 本地 STRM 独立保存路径</label>
                            <input type="text" name="local_strm_dir" value="{{ cfg.local_strm_dir }}" required style="margin-bottom:0;">
                        </div>

                        <div class="path-box" style="border-color: #10b981; background: #ecfdf5;">
                            <h4 style="color:#059669;">⚡ 189 模式B (极速虚空直通)</h4>
                            <label>云端临时挂载目录 (虚空造物目标路径)</label>
                            <input type="text" name="network_cas_path_native" value="{{ cfg.network_cas_path_native }}" required>
                            <label>模式B 本地 STRM 独立保存路径</label>
                            <input type="text" name="local_strm_dir_native" value="{{ cfg.local_strm_dir_native }}" required style="margin-bottom:0;">
                        </div>

                        <div class="path-box" style="border-color: #0ea5e9; background: #f0f9ff;">
                            <h4 style="color:#0284c7;">🎬 189 常规媒体 (真实视频解析)</h4>
                            <label>云端常规视频库 扫描源目录</label>
                            <input type="text" name="network_media_path" value="{{ cfg.network_media_path }}" required>
                            <label>常规媒体 本地 STRM 独立保存路径</label>
                            <input type="text" name="local_strm_dir_media" value="{{ cfg.local_strm_dir_media }}" required style="margin-bottom:0;">
                        </div>

                        <div class="path-box" style="border-color: #f97316; background: #fff7ed;">
                            <h4 style="color:#ea580c;">🟠 139 移动云盘设置</h4>
                            <label>139 OpenList 接口 (例如 5255)</label>
                            <input type="text" name="openlist_host_139" value="{{ cfg.openlist_host_139 }}">
                            <label>139 OpenList 授权 Token</label>
                            <input type="password" name="openlist_token_139" value="{{ cfg.openlist_token_139 }}">
                            <label>云端 139 CAS 扫描源目录</label>
                            <input type="text" name="network_cas_path_139" value="{{ cfg.network_cas_path_139 }}">
                            <label>139 本地 STRM 独立保存路径</label>
                            <input type="text" name="local_strm_dir_139" value="{{ cfg.local_strm_dir_139 }}" style="margin-bottom:0;">
                        </div>
                        
                        <h4 style="margin-top:20px;">🌐 全局基础与 API</h4>
                        <label>基础外网域名 (Server Host)</label>
                        <input type="text" name="server_host" value="{{ cfg.server_host }}" required>
                        <label>189 OpenList 接口地址 (例如 5244)</label>
                        <input type="text" name="openlist_host" value="{{ cfg.openlist_host }}" required>
                        <label>189 OpenList 授权 Token</label>
                        <input type="password" name="openlist_token" value="{{ cfg.openlist_token }}" placeholder="填入189 OpenList Token">
                        <div style="display: flex; gap: 10px; margin-bottom: 15px;">
                            <div style="flex: 1;"><label>绝对销毁倒计时</label><input type="number" name="delete_delay" value="{{ cfg.delete_delay }}" style="margin-bottom:0;" required></div>
                            <div style="flex: 1;"><label>预加载长效护盾</label><input type="number" name="shield_delay" value="{{ cfg.shield_delay }}" style="margin-bottom:0;" required></div>
                        </div>
                        
                        <h4 style="margin-top:20px;">📱 消息推送</h4>
                        <label>PushPlus Token (微信推送)</label><input type="text" name="pushplus_token" value="{{ cfg.pushplus_token }}" placeholder="留空则不推送">
                        <label>Telegram Bot Token</label><input type="text" name="tg_bot_token" value="{{ cfg.tg_bot_token }}">
                        <label>Telegram Chat ID</label><input type="text" name="tg_chat_id" value="{{ cfg.tg_chat_id }}">
                    </div>
                </div>
                <button type="submit" style="width:100%; margin-top:15px; font-size:16px;">💾 写入配置并重启引擎矩阵</button>
            </form>
        </div><div style="height: 40px;"></div>
    </div>
    <script>
        function syncOpenList(driveType, fileType='cas') { 
            let path = prompt("请输入要扫描的 OpenList 云端目录路径 (留空则扫描系统配置的默认目录):");
            if (path === null) return;
            let url = '/api/sync?drive=' + driveType + '&type=' + fileType;
            if (path) url += '&path=' + encodeURIComponent(path);
            fetch(url).then(r => alert('同步指令已下发!请看上方日志。')); 
        }
        function syncLocalCas() { 
            if(confirm("将扫描本地配置的 CAS 目录,生成双轨 STRM,确认执行?")) {
                fetch('/api/sync_local').then(r => alert('本地扫描指令已下发!请看上方日志。')); 
            }
        }
        function clearAllCache() { if(confirm('⚠️ 确定要清空吗?')) { fetch('/api/clear_all', {method: 'POST'}).then(r => alert('🚀 核弹已发射!')); } }
        function fetchLogs() {
            fetch('/admin/logs').then(r => r.json()).then(logs => {
                const box = document.getElementById('logBox');
                const oldScrollTop = box.scrollTop, oldScrollHeight = box.scrollHeight, clientHeight = box.clientHeight;
                box.innerHTML = logs.map(l => `<span class="log-time">[${l.time}]</span><span class="log-msg ${l.msg.includes('✅') ? 'log-SUCCESS' : 'log-' + l.level}">${l.msg}</span><br>`).join('');
                if (oldScrollHeight - clientHeight - oldScrollTop < 30) { box.scrollTop = box.scrollHeight; } else { box.scrollTop = oldScrollTop; }
            });
        }
        setInterval(fetchLogs, 2000); fetchLogs();
    </script>
</body>
</html>
"""

@app_main.route('/admin')
def admin_index():
    cfg = read_config()
    with cache_lock: count = len(upload_cache)
    return render_template_string(ADMIN_HTML, cfg=cfg, cache_count=count, msg=request.args.get('msg'))

@app_main.route('/admin/logs')
def get_logs(): return jsonify(list(log_buffer))

# ==========================================
# ☁️ 天翼云核心功能类
# ==========================================
class TianyiFinalUploader:
    def __init__(self):
        self.rsa_keys = {}
        self.session = requests.Session()

    def get_base_headers(self, session_key):
        return {'User-Agent': 'ecloud/10.2.1 (Windows NT 10.0; Win64; x64)', 'Cookie': f"SESSION_KEY={session_key}; cookieUserSession={session_key}", 'Accept': 'application/json;charset=UTF-8', 'clientType': 'TELEMAC'}

    def _random_string(self, length=16): return ''.join(random.choices('0123456789abcdef', k=length))
    def _get_timestamp(self): return str(int(time.time() * 1000))

    def _get_slice_size(self, file_size):
        try: size = int(file_size)
        except: return '10485760'  
        D = 10485760
        if size > D * 2 * 999: return str(max(math.ceil(size / 1999 / D), 5) * D)
        elif size > D * 999: return str(D * 2)
        return str(D)

    def get_rsa_key(self, session_key):
        if session_key in self.rsa_keys: return self.rsa_keys[session_key]
        url = f"https://cloud.189.cn/api/security/generateRsaKey.action?sessionKey={urllib.parse.quote(session_key)}"
        for _ in range(3):
            try:
                res = self.session.get(url, headers=self.get_base_headers(session_key), timeout=10).json()
                if 'pubKey' in res:
                    self.rsa_keys[session_key] = res
                    return res
                if str(res.get('res_code')) == '111' or 'Session' in str(res): raise Exception("公钥获取拦截_AUTH_FAIL")
            except Exception as e: 
                if "AUTH_FAIL" in str(e): raise e
            time.sleep(2)
        raise Exception("无法获取公钥_AUTH_FAIL")

    def build_request(self, params, request_uri, req_id, session_key):
        rsa_key = self.get_rsa_key(session_key)
        uuid_key = self._random_string(16)
        ts = self._get_timestamp()
        p_str = '&'.join([f"{k}={v}" for k, v in params.items()])
        cipher = AES.new(uuid_key.encode('utf-8'), AES.MODE_ECB)
        enc_p = cipher.encrypt(pad(p_str.encode('utf-8'), 16)).hex().upper()
        rsa_cipher = PKCS1_v1_5.new(RSA.import_key(f"-----BEGIN PUBLIC KEY-----\n{rsa_key['pubKey']}\n-----END PUBLIC KEY-----"))
        enc_t = base64.b64encode(rsa_cipher.encrypt(uuid_key.encode('utf-8'))).decode('utf-8')
        sign = hmac.new(uuid_key.encode('utf-8'), f"SessionKey={session_key}&Operate=GET&RequestURI={request_uri}&Date={ts}&params={enc_p}".encode('utf-8'), hashlib.sha1).hexdigest().upper()
        h = self.get_base_headers(session_key)
        h.update({'X-Request-Date': ts, 'X-Request-ID': req_id, 'SessionKey': session_key, 'EncryptionText': enc_t, 'PkId': rsa_key['pkId'], 'Signature': sign})
        return f"https://upload.cloud.189.cn{request_uri}?params={enc_p}", h

    def get_family_items(self, family_id, folder_id, session_key):
        all_items = []
        params = {"familyId": family_id, "folderId": folder_id, "pageSize": 60, "sessionKey": session_key}
        res = self.session.get("https://cloud.189.cn/api/open/family/file/listFiles.action", params=params, headers=self.get_base_headers(session_key), timeout=10).json()
        if str(res.get('res_code')) == '111' or 'Session' in str(res): raise Exception("接口返回111_AUTH_FAIL")
        for f in res.get('fileListAO', {}).get('fileList', []): all_items.append({'fileName': f['name'], 'fileId': f['id']})
        return all_items

    def delete_item(self, family_id, file_id, session_key):
        url = "https://cloud.189.cn/api/open/family/file/deleteFile.action"
        p = {"familyId": family_id, "fileId": file_id, "sessionKey": session_key}
        try: return self.session.post(url, params=p, headers=self.get_base_headers(session_key), timeout=10).status_code == 200
        except: return False

    def empty_family_recycle(self, family_id, session_key):
        url = "https://cloud.189.cn/api/open/batch/createBatchTask.action"
        payload = {"type": "EMPTY_RECYCLE", "taskInfos": "[]", "targetFolderId": "", "familyId": family_id, "sessionKey": session_key}
        try:
            res = self.session.post(url, data=payload, headers=self.get_base_headers(session_key), timeout=10).json()
            if str(res.get("res_code")) == "0": return True
        except: pass
        return False

    def rapid_upload(self, family_id, parent_folder_id, md5, size, smd5, safe_name, session_key):
        req_id = str(uuid.uuid4())
        slice_size = self._get_slice_size(size)  
        init_p = {'familyId': family_id, 'parentFolderId': parent_folder_id, 'fileName': urllib.parse.quote(safe_name), 'fileSize': str(size), 'sliceSize': slice_size, 'fileMd5': md5, 'sliceMd5': smd5, 'lazyCheck': '1', 'opertype': '3'}
        url, h = self.build_request(init_p, '/family/initMultiUpload', req_id, session_key)
        res = self.session.get(url, headers=h).json()
        if res.get('code') != 'SUCCESS': 
            msg_str = str(res.get('msg', ''))
            if any(k in msg_str.lower() for k in ['session', 'privatekey', '111']): raise Exception(f"秒传初始化拒绝_AUTH_FAIL: {msg_str}")
            raise Exception(f"秒传初始化失败: {msg_str}")
        
        up_id = res['data']['uploadFileId']
        ck_p = {'familyId': family_id, 'fileMd5': md5, 'sliceMd5': smd5, 'uploadFileId': up_id}
        url, h = self.build_request(ck_p, '/family/checkTransSecond', req_id, session_key)
        if not self.session.get(url, headers=h).json().get('data', {}).get('fileDataExists'): raise Exception("云端无此文件")
        
        cm_p = {'familyId': family_id, 'uploadFileId': up_id, 'fileMd5': md5, 'sliceMd5': smd5, 'lazyCheck': '1', 'opertype': '3'}
        url, h = self.build_request(cm_p, '/family/commitMultiUploadFile', req_id, session_key)
        commit_res = self.session.get(url, headers=h).json()
        file_info = commit_res.get('file')
        if not file_info: raise Exception(f"秒传确认失败: {commit_res.get('msg', '未知错误')}")
        return file_info['userFileId']

    def get_download_url(self, family_id, file_id, session_key, client_ua):
        url = "https://cloud.189.cn/api/open/family/file/getFileDownloadUrl.action"
        params = {"familyId": family_id, "fileId": file_id, "sessionKey": session_key}
        
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/118.0.0.0 Safari/537.36",
            "Cookie": f"SESSION_KEY={session_key}; cookieUserSession={session_key}",
            "Accept": "application/json;charset=UTF-8"
        }
        
        try:
            logger.info(f"🐛 [直链破冰] 1. 开始向天翼云请求底层网关链接...")
            res = self.session.get(url, params=params, headers=headers, timeout=10).json()
            if 'fileDownloadUrl' in res:
                api_url = res['fileDownloadUrl'].replace('&amp;', '&')
                logger.info(f"🐛 [直链破冰] 2. 成功获取并修复网关链接: {api_url[:80]}...")
                
                unwrap_headers = {
                    "User-Agent": client_ua if client_ua else headers["User-Agent"],
                    "Cookie": f"SESSION_KEY={session_key}; cookieUserSession={session_key}",
                    "Accept-Encoding": "identity"
                }
                
                logger.info(f"🐛 [直链破冰] 3. 携带真实签名进行底层破冰追踪...")
                unwrap_res = requests.get(api_url, headers=unwrap_headers, allow_redirects=False, timeout=10)
                status_code = unwrap_res.status_code
                
                if status_code in [301, 302, 303, 307, 308]:
                    cdn_url = unwrap_res.headers.get('Location')
                    logger.info(f"✅ [直链就绪] 4. 破冰成功!获取到无鉴权 CDN 裸链,已移交播放器!")
                    return cdn_url
                elif status_code == 200:
                    logger.info(f"✅ [直链就绪] 4. 网关直接放行,返回数据流。")
                    return api_url
                else:
                    logger.error(f"❌ [直链破冰] 失败!HTTP {status_code},响应: {unwrap_res.text[:150]}")
                    raise Exception(f"底层破冰失败 (HTTP {status_code})")
                    
            raise Exception(f"提取网关链接失败: {res.get('msg', res)}")
        except Exception as e:
            logger.error(f"❌ 提取直链时发生异常: {e}")
            raise e

client = TianyiFinalUploader()

# ==========================================
# 🧹 虚空造物清理工
# ==========================================
def delayed_delete_openlist_file(host, token, dir_path, file_name, delay=120):
    time.sleep(delay)
    try:
        headers = {"Authorization": token} if token else {}
        payload = {"dir": dir_path, "names": [file_name]}
        res = requests.post(f"{host}/api/fs/remove", json=payload, headers=headers, timeout=10)
        if res.status_code == 200:
            logger.info(f"🧹 [虚空清理] 阅后即焚成功!临时文件已销毁: {file_name}")
        else:
            logger.warning(f"⚠️ [虚空清理] 销毁失败: {res.text}")
    except Exception as e:
        logger.error(f"❌ [虚空清理] 请求异常: {e}")

def cleanup_worker(name, f_md5, fam_id, fold_id, session_key):
    with cache_lock:
        if f_md5 not in upload_cache: return
        expire_time = upload_cache[f_md5]['expire']
        
    expire_str = time.strftime("%H:%M:%S", time.localtime(expire_time))
    logger.info(f"⏳ 销毁启动: [{name}] 预计于 {expire_str} 删除")
    
    while True:
        with cache_lock:
            if f_md5 not in upload_cache: return
            expire_time = upload_cache[f_md5]['expire']
            
        now = time.time()
        if now >= expire_time: break
        
        sleep_time = expire_time - now
        if sleep_time > 0:
            time.sleep(sleep_time + 1)
        
    logger.info(f"🗑️ 视频删除: [{name}]")

    try:
        items = client.get_family_items(fam_id, fold_id, session_key)
        real_fid = next((i['fileId'] for i in items if f_md5 in i['fileName'] or i['fileName'] == name), None)
        
        deleted = False
        if real_fid and client.delete_item(fam_id, real_fid, session_key):
            deleted = True
            time.sleep(2) 
            if client.empty_family_recycle(fam_id, session_key):
                logger.info(f"♻️ 清空回收站: 存储卡槽 [{fam_id[-4:]}] 空间已释放")
                
        if not deleted:
            logger.warning(f"⚠️ 常规删除受阻,触发兜底: 尝试调用卡槽 6 凭证强删!")
            cfg = read_config()
            if len(cfg.get('family_clouds', [])) > 5 and cfg['family_clouds'][5].get('session_key'):
                master_sk = cfg['family_clouds'][5]['session_key']
                master_items = client.get_family_items(fam_id, fold_id, master_sk)
                master_fid = next((i['fileId'] for i in master_items if f_md5 in i['fileName'] or i['fileName'] == name), None)
                
                if master_fid and client.delete_item(fam_id, master_fid, master_sk):
                    logger.info(f"✅ 兜底强删成功: 已使用卡槽 6 凭证销毁 [{name}]")
                    time.sleep(2)
                    if client.empty_family_recycle(fam_id, master_sk):
                        logger.info(f"♻️ 借用主号权限清空回收站完毕。")
                else:
                    logger.error(f"❌ 兜底失败:卡槽 6 凭证也无法删除该文件。")
            else:
                logger.warning(f"❌ 无法执行兜底删除:卡槽 6 未配置或凭证无效。")

    except Exception as e:
        logger.error(f"🗑️ 后台删除作业受阻: {e}")
        
    with cache_lock:
        if f_md5 in upload_cache: del upload_cache[f_md5]

@app_main.route('/play', methods=['GET', 'HEAD'])
def play():
    cas = request.args.get('cas')
    drive_type = request.args.get('drive', '189').strip()
    file_path_param = request.args.get('path', '').strip()
    show_name_from_url = request.args.get('show', '').strip()
    
    client_ua = request.headers.get('User-Agent', '')
    cfg = read_config()

    if request.method == 'HEAD': 
        try:
            raw_size = 0
            ext = 'mp4'

            if drive_type in ['139', 'direct'] and file_path_param:
                is_139 = (drive_type == '139')
                ol_host = cfg.get('openlist_host_139' if is_139 else 'openlist_host', 'http://127.0.0.1:5244').rstrip('/')
                ol_token = cfg.get('openlist_token_139' if is_139 else 'openlist_token', '')
                headers = {"Authorization": ol_token} if ol_token else {}
                
                try:
                    res = requests.post(f"{ol_host}/api/fs/get", json={"path": file_path_param, "password": ""}, headers=headers, timeout=5).json()
                    raw_url = res.get('data', {}).get('raw_url')
                    if is_139 and raw_url:
                        dl_headers = headers.copy()
                        dl_headers['Accept-Encoding'] = 'identity'
                        cas_res = requests.get(raw_url, headers=dl_headers, timeout=5)
                        j = json.loads(cas_res.text)
                        raw_size = j.get('size') or j.get('fileSize') or 0
                    else:
                        raw_size = res.get('data', {}).get('size', 0)
                except Exception as e:
                    logger.error(f"⚠️ HEAD探针查询 OpenList 真实大小失败: {e}")
                    
                ext = file_path_param.split('.')[-1].lower() if '.' in file_path_param else 'mp4'

            elif cas:
                cas_b64 = cas.replace(' ', '+')
                cas_b64 += "=" * ((4 - len(cas_b64) % 4) % 4)
                j = json.loads(base64.b64decode(cas_b64).decode('utf-8'))
                raw_size = j.get('size') or j.get('fileSize') or 0
                name = j.get('name') or j.get('fileName') or ""
                ext = name.split('.')[-1].lower() if '.' in name else 'mp4'
            
            mime_dict = {
                'mp4': 'video/mp4', 'mkv': 'video/x-matroska', 'ts':  'video/mp2t',
                'avi': 'video/x-msvideo', 'mov': 'video/quicktime', 'webm':'video/webm'
            }
            content_type = mime_dict.get(ext, 'video/mp4')

            return "", 200, {
                'Content-Type': content_type, 
                'Accept-Ranges': 'bytes',
                'Content-Length': str(raw_size)
            }
        except:
            return "", 200, {'Content-Type': 'video/mp4', 'Accept-Ranges': 'bytes'}

    # ==========================================
    # ⚡ 模式 B:189 原生虚空直连解析 
    # ==========================================
    if drive_type == '189_native':
        if not cas: return "❌ 缺失 cas 核心代码", 400
        
        ol_host = cfg.get('openlist_host', 'http://127.0.0.1:5244').rstrip('/')
        ol_token = cfg.get('openlist_token', '')
        headers = {"Authorization": ol_token} if ol_token else {}
        
        try:
            cas_b64 = cas.replace(' ', '+')
            cas_b64 += "=" * ((4 - len(cas_b64) % 4) % 4)
            cas_content = base64.b64decode(cas_b64).decode('utf-8')
            j = json.loads(cas_content)
            f_md5 = j.get('md5') or j.get('fileMd5') or j.get('fileMD5')
            safe_name = j.get('name') or j.get('fileName') or "unknown.mp4"
            
            with cache_lock:
                if f_md5 in native_link_cache:
                    cached_url, expire_time = native_link_cache[f_md5]
                    if time.time() < expire_time:
                        return redirect(cached_url)
                    else:
                        del native_link_cache[f_md5] 
            
            logger.info(f"========== 🕵️‍♂️ 原生直连(模式B) 链路日志 START ==========")
            logger.info(f"▶️ [1] 请求原生直通: {safe_name}")
            
            target_dir = cfg.get('network_cas_path_native', '/177/177-原生直连').rstrip('/')
            temp_file_name = f"temp_play_{f_md5}.cas"
            target_path = f"{target_dir}/{temp_file_name}"
            
            put_headers = headers.copy()
            put_headers["File-Path"] = urllib.parse.quote(target_path)
            put_headers["Content-Length"] = str(len(cas_content.encode('utf-8')))
            put_headers["Content-Type"] = "application/octet-stream"
            
            logger.info(f"☁️ [2] 虚空造物:正在向云端动态写入临时伪装凭证...")
            requests.put(f"{ol_host}/api/fs/put", data=cas_content.encode('utf-8'), headers=put_headers, timeout=10)
            
            get_res = requests.post(f"{ol_host}/api/fs/get", json={"path": target_path, "password": ""}, headers=headers, timeout=10).json()
            raw_url = get_res.get('data', {}).get('raw_url')
            
            if raw_url:
                logger.info(f"📥 [3] 成功获取底层 raw_url, 启动防风控嗅探...")
                
                unwrap_headers = {k: v for k, v in request.headers if k.lower() not in ['host', 'accept-encoding', 'authorization']}
                unwrap_headers['Accept-Encoding'] = 'identity'
                if ol_token: unwrap_headers["Authorization"] = ol_token
                if not any(k.lower() == 'range' for k in unwrap_headers): unwrap_headers["Range"] = "bytes=0-"
                
                try:
                    unwrap_res = requests.get(raw_url, headers=unwrap_headers, allow_redirects=False, timeout=15, stream=True)
                    status_code = unwrap_res.status_code
                    headers_dict = dict(unwrap_res.headers)
                    unwrap_res.close()

                    if status_code == 500:
                        logger.warning(f"⚠️ [警告] 嗅探遇 500,去 Range 破冰...")
                        del unwrap_headers["Range"]
                        unwrap_res = requests.get(raw_url, headers=unwrap_headers, allow_redirects=False, timeout=15, stream=True)
                        status_code = unwrap_res.status_code
                        headers_dict = dict(unwrap_res.headers)
                        unwrap_res.close()

                    logger.info(f"🔍 [4] 嗅探完成!状态码: {status_code}")
                    
                    if not hasattr(delayed_delete_openlist_file, "last_trigger"):
                        delayed_delete_openlist_file.last_trigger = {}
                    
                    now_time = time.time()
                    if now_time - delayed_delete_openlist_file.last_trigger.get(temp_file_name, 0) > 120:
                        delayed_delete_openlist_file.last_trigger[temp_file_name] = now_time
                        threading.Thread(target=delayed_delete_openlist_file, args=(ol_host, ol_token, target_dir, temp_file_name, 120), daemon=True).start()

                    final_return_url = None
                    if status_code in [301, 302, 303, 307, 308]:
                        final_return_url = headers_dict.get('Location')
                        if final_return_url:
                            logger.info(f"✅ [5] 完美触发!已拿到 189 官方大带宽直链!")
                            logger.info(f"🚀 [6] 302 重定向发射,体验零秒起飞...")
                    elif status_code in [200, 206]:
                        logger.info(f"✅ [5] 检测到官方直接返回数据流,移交客户端!")
                        final_return_url = raw_url
                        
                    if final_return_url:
                        with cache_lock: native_link_cache[f_md5] = (final_return_url, time.time() + 7200)
                        logger.info(f"========== 🕵️‍♂️ 原生直连(模式B) 链路日志 END ==========\n")
                        return redirect(final_return_url)
                    else: return "获取直链异常", 500

                except Exception as unwrap_e:
                    logger.error(f"❌ 获取直链异常: {unwrap_e}")
                    return "获取直链异常", 500
            else:
                logger.error("❌ 无法获取临时 CAS 文件的 raw_url")
                return "无 raw_url", 500
        except Exception as e:
            logger.error(f"❌ 原生直连处理异常: {e}")
            return "处理异常", 500

    # ==========================================
    # 🎬 常规媒体直通 / 🟠 移动云 139 通用跳转
    # ==========================================
    if drive_type in ['139', 'direct']:
        if not file_path_param: return "❌ 请求缺少 path 参数", 400

        is_139 = (drive_type == '139')
        ol_host = cfg.get('openlist_host_139' if is_139 else 'openlist_host', 'http://127.0.0.1:5244').rstrip('/')
        ol_token = cfg.get('openlist_token_139' if is_139 else 'openlist_token', '')
        headers = {"Authorization": ol_token} if ol_token else {}

        log_tag = "139云盘" if is_139 else "常规真视频"
        logger.info(f"========== 🕵️‍♂️ {log_tag} 链路日志 START ==========")
        logger.info(f"▶️ [1] 触发请求: {file_path_param}")
        
        try:
            get_res = requests.post(f"{ol_host}/api/fs/get", json={"path": file_path_param, "password": ""}, headers=headers, timeout=15)
            if get_res.status_code == 200:
                raw_url = get_res.json().get('data', {}).get('raw_url')
                if raw_url:
                    logger.info(f"📥 [2] 成功获取底层 raw_url: {raw_url[:100]}...")
                    
                    unwrap_headers = {k: v for k, v in request.headers if k.lower() not in ['host', 'accept-encoding', 'authorization']}
                    unwrap_headers['Accept-Encoding'] = 'identity'
                    if ol_token: unwrap_headers["Authorization"] = ol_token
                    if not any(k.lower() == 'range' for k in unwrap_headers): unwrap_headers["Range"] = "bytes=0-"
                    
                    logger.info(f"🔎 [3] 伪装播放器头,启动 {ol_host.split(':')[-1]} 底层嗅探!")
                    try:
                        unwrap_res = requests.get(raw_url, headers=unwrap_headers, allow_redirects=False, timeout=15, stream=True)
                        status_code = unwrap_res.status_code
                        headers_dict = dict(unwrap_res.headers)
                        unwrap_res.close()

                        if status_code == 500:
                            logger.warning(f"⚠️ [警告] 嗅探遇到 500 崩溃,疑似 Range 冲突!去掉 Range 二次破冰...")
                            del unwrap_headers["Range"]
                            unwrap_res = requests.get(raw_url, headers=unwrap_headers, allow_redirects=False, timeout=15, stream=True)
                            status_code = unwrap_res.status_code
                            headers_dict = dict(unwrap_res.headers)
                            unwrap_res.close()

                        logger.info(f"🔍 [4] 嗅探完成!状态码: {status_code}")
                        
                        if status_code in [301, 302, 303, 307, 308]:
                            final_cdn_url = headers_dict.get('Location')
                            if final_cdn_url:
                                logger.info(f"✅ [5] 完美触发!已拿到官方视频大带宽直链!")
                                logger.info(f"🚀 [6] 302 重定向发射,零中转直接起飞...")
                                logger.info(f"========== 🕵️‍♂️ {log_tag} 链路日志 END ==========\n")
                                return redirect(final_cdn_url)
                            else: return "缺失直链", 500
                        elif status_code in [200, 206]:
                            logger.info(f"✅ [5] 检测到官方直接返回数据流,将直链移交客户端!")
                            logger.info(f"========== 🕵️‍♂️ {log_tag} 链路日志 END ==========\n")
                            return redirect(raw_url)
                        else: return "状态码异常", 500
                    except Exception as unwrap_e:
                        logger.error(f"❌ 真实直链获取异常: {unwrap_e}")
                        return "获取直链异常", 500
                else: return "无 raw_url", 500
            else: return f"{log_tag} 破冰失败", 500
        except Exception as e:
            logger.error(f"❌ 接口通信异常: {e}")
            return "接口通信异常", 500

    # ==========================================
    # 🔵 模式 A:天翼云 189 CAS稳健秒传 专区逻辑 
    # ==========================================
    if cas:
        cas = cas.replace(' ', '+')  
        cas += "=" * ((4 - len(cas) % 4) % 4)  
        
    safe_name = "未知文件"
    try:
        j = json.loads(base64.b64decode(cas).decode('utf-8'))
        f_md5 = str(j.get('md5') or j.get('fileMd5') or j.get('fileMD5')).upper()
        s_md5 = str(j.get('slice_md5') or j.get('sliceMd5') or j.get('sliceMD5')).upper()
        raw_size = j.get('size') or j.get('fileSize')
        human_size = format_size(raw_size)
        name = j.get('name') or j.get('fileName')
        base_safe_name = "".join(x for x in name if x not in r'\/:*?"<>|')
        
        if show_name_from_url: 
            show_identifier = re.sub(r'\s*\(\d{4}\)', '', show_name_from_url).strip()
        else:
            guess = re.split(r'(?i)\.S\d+|\.E\d+|-第\d+集', base_safe_name)[0]
            show_identifier = re.sub(r'\s*\(\d{4}\)', '', guess).strip()
            
        bind_key = show_identifier
        ext = os.path.splitext(base_safe_name)[1]
        if not ext or len(ext) > 6: ext = ".mp4"

        ep_num = None
        for p in [r'(?i)E(?:P)?\s*0*(\d+)', r'第\s*0*(\d+)\s*[集话期]', r'(?:\[|\()0*(\d+)(?:\]|\))', r'(?i)episode\s*0*(\d+)']:
            m = re.search(p, base_safe_name)
            if m: 
                ep_num = int(m.group(1))
                break
        s_match = re.search(r'(?i)S0*(\d+)', base_safe_name)
        s_num = int(s_match.group(1)) if s_match else 1

        year_match = re.search(r'(?<!\d)(19\d{2}|20\d{2})(?!\d)', base_safe_name)
        year_str = f".{year_match.group(1)}" if year_match else ""

        if show_identifier and ep_num is not None: 
            safe_name = f"{show_identifier}.S{s_num:02d}E{ep_num:02d}{year_str}{ext}"
        else:
            if show_identifier: safe_name = f"{show_identifier}{year_str}{ext}"
            else: safe_name = base_safe_name

        tags_match = re.findall(r'(?i)\b(1080p|2160p|4K|DV|HQ|HDR|SDR|IQ|H\.?26[45]|x\.?26[45])\b', base_safe_name)
        tags = []
        for t in tags_match:
            t_upper = t.upper().replace('.', '')
            if t_upper == '1080P': t_upper = '1080p'
            elif t_upper == '2160P': t_upper = '2160p'
            elif t_upper == 'X264': t_upper = 'H264'
            elif t_upper == 'X265': t_upper = 'H265'
            if t_upper not in tags:
                tags.append(t_upper)
                
        tag_str = "." + ".".join(tags) if tags else ""
        
        if tag_str:
            if safe_name.endswith(ext): safe_name = safe_name[:-len(ext)] + tag_str + ext
            else: safe_name = safe_name + tag_str + ext

        with cache_lock:
            name_collided = any(
                v.get('fid') != 'processing' and f_md5 != k and safe_name == (v.get('show_name') + ext if 'show_name' in v else "")
                for k, v in upload_cache.items()
            )
            if name_collided:
                size_tag = f".{human_size.replace(' ', '')}"
                if safe_name.endswith(ext): 
                    safe_name = safe_name[:-len(ext)] + size_tag + ext
                else: 
                    safe_name = safe_name + size_tag + ext
                logger.info(f"⚠️ 探测到多版本文件名严重撞车!已重命名为: [{safe_name}]")
                
        current_time = time.time()
        fam_fid, target_fam_id, target_fold_id, target_session_key, is_new, requires_cleanup = None, None, None, None, False, False
        s_idx = 0
        download_url = None

        with cache_lock:
            if f_md5 in upload_cache:
                last_log_time = upload_cache[f_md5].get('last_log', 0)
                if current_time - last_log_time > 60: 
                    expire_self_str = time.strftime("%H:%M:%S", time.localtime(upload_cache[f_md5]['expire']))
                    logger.info(f"📡 探针侦测: [{safe_name}] 预计于 {expire_self_str} 准时处决!")
                    upload_cache[f_md5]['last_log'] = current_time
                download_url = upload_cache[f_md5].get('download_url')
            else:
                cloud, s_idx = get_target_cloud(cfg, bind_key, file_size=raw_size)
                if not cloud: raise Exception("未配置有效的家庭云阵列!请检查后台。")
                target_fam_id = cloud['family_id']
                target_fold_id = cloud['hard_folder_id']
                target_session_key = cloud['session_key']

                brother_exists = any(v.get('show_name') == bind_key for v in upload_cache.values())
                if brother_exists: initial_expire = current_time + cfg.get('shield_delay', 2700)
                else: initial_expire = current_time + cfg.get('delete_delay', 600)

                upload_cache[f_md5] = {'fid': 'processing', 'expire': initial_expire, 'fam_id': target_fam_id, 'fold_id': target_fold_id, 'session_key': target_session_key, 'show_name': bind_key, 'download_url': None}
                requires_cleanup = True
                
        if requires_cleanup:
            client_ua = request.headers.get('User-Agent', '')
            try:
                if not target_session_key: raise Exception("AUTH_FAIL: 本地凭证为空,强制触发自愈流程")
                items = client.get_family_items(target_fam_id, target_fold_id, target_session_key)
                fam_fid = next((i['fileId'] for i in items if f_md5 in i['fileName'] or i['fileName'] == safe_name), None)
                if not fam_fid:
                    logger.info(f"🔄 路由调度: [{bind_key}] ({human_size}) -> 卡槽 {s_idx+1}")
                    fam_fid = client.rapid_upload(target_fam_id, target_fold_id, f_md5, raw_size, s_md5, safe_name, target_session_key)
                    is_new = True
                
                download_url = client.get_download_url(target_fam_id, fam_fid, target_session_key, client_ua)

            except Exception as e:
                err_str = str(e).lower()
                if any(k in err_str for k in ["auth_fail", "invalidsessionkey", "sessionkey", "session", "111", "privatekey"]):
                    logger.warning(f"⚠️ 探测到卡槽 {s_idx+1} 凭证失效(或私钥丢失),引擎介入执行自愈...")
                    new_sk = refresh_slot_logic(s_idx, cfg)
                    if new_sk:
                        target_session_key = new_sk
                        with cache_lock: upload_cache[f_md5]['session_key'] = new_sk
                        items = client.get_family_items(target_fam_id, target_fold_id, target_session_key)
                        fam_fid = next((i['fileId'] for i in items if f_md5 in i['fileName'] or i['fileName'] == safe_name), None)
                        if not fam_fid:
                            fam_fid = client.rapid_upload(target_fam_id, target_fold_id, f_md5, raw_size, s_md5, safe_name, target_session_key)
                            is_new = True
                        download_url = client.get_download_url(target_fam_id, fam_fid, target_session_key, client_ua)
                    else:
                        if s_idx == 5 and not cfg['family_clouds'][5].get('username'):
                            logger.warning("🔄 卡槽 6 外部凭证失效,正在触发【无缝故障转移】至主卡槽(卡槽1)...")
                            if len(cfg['family_clouds']) > 0 and cfg['family_clouds'][0].get('session_key'):
                                target_fam_id = cfg['family_clouds'][0]['family_id']
                                target_fold_id = cfg['family_clouds'][0]['hard_folder_id']
                                target_session_key = cfg['family_clouds'][0]['session_key']
                                with cache_lock:
                                    upload_cache[f_md5].update({'fam_id': target_fam_id, 'fold_id': target_fold_id, 'session_key': target_session_key})
                                items = client.get_family_items(target_fam_id, target_fold_id, target_session_key)
                                fam_fid = next((i['fileId'] for i in items if f_md5 in i['fileName'] or i['fileName'] == safe_name), None)
                                if not fam_fid:
                                    fam_fid = client.rapid_upload(target_fam_id, target_fold_id, f_md5, raw_size, s_md5, safe_name, target_session_key)
                                    is_new = True
                                download_url = client.get_download_url(target_fam_id, fam_fid, target_session_key, client_ua)
                            else: raise Exception("卡槽 6 无法恢复,且主卡槽(1)不可用,降级转移彻底失败!")
                        else: raise Exception(f"卡槽 {s_idx+1} 防线自愈失败,无法重建有效凭证!")
                else: raise e
                
            if fam_fid and download_url:
                if is_new:
                    if brother_exists: logger.info(f"✅ 秒传成功: [{safe_name}] ({human_size}) (被动预加载,已天生加持长效护盾)")
                    else: 
                        logger.info(f"✅ 秒传成功: [{safe_name}] ({human_size}) 已利用 卡槽 {s_idx+1} 上传!")
                        send_push("▶️ 新剧集入库并播放", f"<b>{safe_name}</b> ({human_size})<br>已成功利用 <b>卡槽 {s_idx+1}</b> 秒传并提取官方直链。")
                with cache_lock: 
                    upload_cache[f_md5]['fid'] = fam_fid
                    upload_cache[f_md5]['download_url'] = download_url
                threading.Thread(target=cleanup_worker, args=(safe_name, f_md5, target_fam_id, target_fold_id, target_session_key), daemon=True).start()
        else:
            if not download_url:
                for _ in range(50):
                    time.sleep(0.2)
                    with cache_lock:
                        if f_md5 in upload_cache and upload_cache[f_md5].get('download_url'):
                            download_url = upload_cache[f_md5]['download_url']
                            break
        
        if download_url:
            logger.info(f"🔄 路由导向: [{safe_name}] 抛弃 OpenList,已直达天翼官方 CDN!")
            return redirect(download_url, code=302)
        else:
            raise Exception("等待转存或获取官方直链超时")

    except Exception as e:
        logger.error(f"❌ 链路故障: {e}")
        with cache_lock:
            if 'f_md5' in locals() and f_md5 in upload_cache and upload_cache[f_md5].get('fid') == 'processing':
                del upload_cache[f_md5]
                logger.info("🧹 已清除因报错假死的转存缓存,通道已重新释放!")
        if "云端无此文件" in str(e) or "秒传初始化失败" in str(e) or "AUTH_FAIL" in str(e) or "privatekey" in str(e).lower():
            send_push("💔 播放链路故障", f"点播 <b>{safe_name}</b> 失败!<br>原因: {e}")
        return f"错误: {e}", 500

def warm_up_parent(target_path, headers, api_host):
    if not target_path: return
    cfg = read_config()
    base_path = cfg.get('network_cas_path', '').rstrip('/')
    if target_path.startswith(base_path):
        rel_path = target_path[len(base_path):].strip('/')
        parts = rel_path.split('/')
        current_path = base_path
        for part in parts[:-1]:
            current_path = f"{current_path}/{part}"
            logger.info(f"🧊 破冰行动:逐级向下唤醒缓存 -> {current_path}")
            try:
                requests.post(f"{api_host}/api/fs/list", json={"path": current_path, "page": 1, "per_page": 1000, "refresh": True}, headers=headers, timeout=5)
                time.sleep(0.5)
            except: pass

def scan_openlist_recursive(current_path, headers, result_list, api_host, file_type='cas'):
    logger.info(f"🔎 正在探测目录: {current_path}")
    try:
        res = requests.post(f"{api_host}/api/fs/list", json={"path": current_path, "page": 1, "per_page": 1000, "refresh": True}, headers=headers, timeout=15).json()
        if res.get("code") != 200: 
            logger.error(f"❌ OpenList 扫描拒绝: {res.get('message')}")
            return
        for f in res.get("data", {}).get("content", []):
            if f.get("is_dir"): scan_openlist_recursive(f"{current_path}/{f['name']}", headers, result_list, api_host, file_type)
            else:
                ext = f['name'].lower().split('.')[-1] if '.' in f['name'] else ''
                if file_type in ['cas', 'cas_native'] and ext == 'cas':
                    result_list.append(f"{current_path}/{f['name']}")
                elif file_type == 'media' and ext in ['mp4', 'mkv', 'ts', 'avi', 'mov', 'webm', 'flv', 'iso']:
                    result_list.append(f"{current_path}/{f['name']}")
    except Exception as e: logger.error(f"❌ 探测目录失败 {current_path}: {e}")

def generate_strm_from_openlist_to_local(target_path=None, drive_type='189', file_type='cas'):
    cfg = read_config()
    
    if drive_type == '139':
        scan_root = target_path if target_path else cfg.get('network_cas_path_139', '')
        base_cas_path = cfg.get('network_cas_path_139', '')
        local_strm_dir = cfg.get('local_strm_dir_139', '')
        api_host = cfg.get('openlist_host_139', 'http://127.0.0.1:5255').rstrip('/')
        api_token = cfg.get('openlist_token_139', '')
    else:
        api_host = cfg.get('openlist_host', 'http://127.0.0.1:5244').rstrip('/')
        api_token = cfg.get('openlist_token', '')
        if file_type == 'cas':
            scan_root = target_path if target_path else cfg.get('network_cas_path', '')
            base_cas_path = cfg.get('network_cas_path', '')
            local_strm_dir = cfg.get('local_strm_dir', '')
        elif file_type == 'cas_native':
            scan_root = target_path if target_path else cfg.get('network_cas_path', '')
            base_cas_path = cfg.get('network_cas_path', '')
            local_strm_dir = cfg.get('local_strm_dir_native', '')
        elif file_type == 'media':
            scan_root = target_path if target_path else cfg.get('network_media_path', '')
            base_cas_path = cfg.get('network_media_path', '')
            local_strm_dir = cfg.get('local_strm_dir_media', '')
        
    os.makedirs(local_strm_dir, exist_ok=True)
    headers = {"Authorization": api_token} if api_token else {}
    
    if target_path and drive_type != '139': 
        warm_up_parent(target_path, headers, api_host)
    
    logger.info(f"🔄 启动 OpenList [{drive_type}] 扫描 -> 模式: {file_type.upper()}, 区域: {scan_root}")
    cas_files = []
    scan_openlist_recursive(scan_root, headers, cas_files, api_host, file_type)
    if not cas_files: return logger.info(f"⚠️ 扫描完毕:该区域下未找到任何目标文件")
        
    count = 0
    req_session = requests.Session() 
    
    for full_path in cas_files:
        try:
            if full_path.startswith(base_cas_path): rel_path = full_path[len(base_cas_path):].lstrip('/')
            else: rel_path = full_path.split('/')[-1]
            rel_dir = os.path.dirname(rel_path)
            dir_parts = [p for p in rel_dir.split('/') if p]
            show_name = ""
            for part in reversed(dir_parts):
                if not re.match(r'(?i)^(season\s*\d+|specials|电视剧|电影|动漫|纪录片|综艺)$', part):
                    show_name = part; break
            if not show_name and dir_parts: show_name = dir_parts[-1] 
            if not show_name: show_name = "未知剧集"
            
            show_name = re.sub(r'\s*\(\d{4}\)', '', show_name)
            show_name = re.sub(r'(?i)\s*(HQ|IQ|HDR|SDR|DV|4K|1080p|720p)\b', '', show_name)
            show_name = re.sub(r'[《》]', '', show_name).strip()
            
            base_name = os.path.basename(rel_path).rsplit('.', 1)[0]
            
            target_local_dir = os.path.join(local_strm_dir, rel_dir)
            os.makedirs(target_local_dir, exist_ok=True)
            
            if file_type == 'cas_native':
                strm_path = os.path.join(target_local_dir, f"{base_name}-Direct.strm")
            else:
                strm_path = os.path.join(target_local_dir, f"{base_name}.strm")
                
            if os.path.exists(strm_path): continue
            
            if count > 0 and count % 50 == 0:
                logger.info(f"🚦 [智能防刷] 已极速突发拉取 {count} 集,触发冷却机制,让接口喘息 3 秒...")
                time.sleep(3)
                
            if file_type == 'media':
                strm_data = f"{cfg['server_host']}/play?drive=direct&path={urllib.parse.quote(full_path)}&show={urllib.parse.quote(show_name)}"
                with open(strm_path, "w", encoding="utf-8") as f: f.write(strm_data)
                count += 1
                continue
                
            if drive_type == '139':
                strm_data = f"{cfg['server_host']}/play?drive=139&path={urllib.parse.quote(full_path)}&show={urllib.parse.quote(show_name)}"
                with open(strm_path, "w", encoding="utf-8") as f: f.write(strm_data)
                count += 1
                continue
            
            get_res = req_session.post(f"{api_host}/api/fs/get", json={"path": full_path}, headers=headers, timeout=10).json()
            raw_url = get_res.get("data", {}).get("raw_url")
            if not raw_url: continue
            
            cas_content = req_session.get(raw_url, timeout=10).text.strip()
            
            if file_type == 'cas_native':
                strm_data = f"{cfg['server_host']}/play?drive=189_native&cas={urllib.parse.quote(cas_content)}&show={urllib.parse.quote(show_name)}"
            else:
                strm_data = f"{cfg['server_host']}/play?cas={urllib.parse.quote(cas_content)}&show={urllib.parse.quote(show_name)}"
            
            with open(strm_path, "w", encoding="utf-8") as f: f.write(strm_data)
            count += 1
            
        except Exception as e:
            logger.error(f"❌ 读取 {base_name} 时受阻: {e}")
            time.sleep(2) 
            pass

    if count > 0: 
        logger.info(f"🎉 [{drive_type}] 同步完毕,成功生成 {count} 个带精准剧名标记的 STRM 文件")
        try: 
            subprocess.Popen(["/data/data/com.termux/files/usr/bin/bash", "/data/data/com.termux/files/home/refresh.sh", scan_root])
            logger.info("🎬 已触发本地 Emby 媒体库刷新指令")
            send_push("🎬 STRM同步成功", f"成功生成并归档了 {count} 个媒体文件,并已触发媒体库刷新。")
        except: pass

@app_main.route('/api/sync')
def trigger_sync():
    target_path = request.args.get('path') 
    drive_type = request.args.get('drive', '189')
    file_type = request.args.get('type', 'cas')
    threading.Thread(target=generate_strm_from_openlist_to_local, args=(target_path, drive_type, file_type), daemon=True).start()
    return "✅ 同步指令下发成功", 200

# ==========================================
# ⚡ 新增:纯本地 CAS 双轨全量扫描生成器 (应对UI按钮)
# ==========================================
def local_cas_sync_worker():
    cfg = read_config()
    source_dir = cfg.get('local_cas_source_dir', '')
    if not source_dir or not os.path.exists(source_dir):
        logger.error("❌ 本地 CAS 源目录不存在或未配置。")
        return

    logger.info(f"🔄 启动本地 CAS 全量双轨扫描 -> 目录: {source_dir}")
    base_dir_a = cfg.get('local_strm_dir', '')
    base_dir_b = cfg.get('local_strm_dir_native', '')
    
    count = 0
    for root, dirs, files in os.walk(source_dir):
        for file in files:
            if file.endswith('.cas'):
                full_path = os.path.join(root, file)
                rel_path = full_path[len(source_dir):].lstrip('/\\')
                rel_dir = os.path.dirname(rel_path)
                
                # 猜剧名分类
                dir_parts = [p for p in rel_dir.split('/') if p]
                show_name = "未知剧集"
                for part in reversed(dir_parts):
                    if not re.match(r'(?i)^(season\s*\d+|specials|电视剧|电影|动漫|纪录片|综艺)$', part):
                        show_name = part; break
                if show_name == "未知剧集" and dir_parts: show_name = dir_parts[-1]
                
                show_name = re.sub(r'\s*\(\d{4}\)', '', show_name)
                show_name = re.sub(r'(?i)\s*(HQ|IQ|HDR|SDR|DV|4K|1080p|720p)\b', '', show_name)
                show_name = re.sub(r'[《》]', '', show_name).strip()
                
                base_name = os.path.splitext(file)[0]
                
                try:
                    with open(full_path, 'r', encoding='utf-8') as f:
                        cas_content = f.read().strip()
                        
                    # 🛡️ 写入模式 A (稳健秒传)
                    if base_dir_a:
                        target_a = os.path.join(base_dir_a, rel_dir)
                        os.makedirs(target_a, exist_ok=True)
                        strm_a = os.path.join(target_a, f"{base_name}.strm")
                        if not os.path.exists(strm_a):
                            with open(strm_a, "w", encoding="utf-8") as fa:
                                fa.write(f"{cfg['server_host']}/play?cas={urllib.parse.quote(cas_content)}&show={urllib.parse.quote(show_name)}")
                            count += 1
                    
                    # ⚡ 写入模式 B (原生直通)
                    if base_dir_b:
                        target_b = os.path.join(base_dir_b, rel_dir)
                        os.makedirs(target_b, exist_ok=True)
                        strm_b = os.path.join(target_b, f"{base_name}-Direct.strm")
                        if not os.path.exists(strm_b):
                            with open(strm_b, "w", encoding="utf-8") as fb:
                                fb.write(f"{cfg['server_host']}/play?drive=189_native&cas={urllib.parse.quote(cas_content)}&show={urllib.parse.quote(show_name)}")
                            count += 1
                except Exception as e:
                    logger.error(f"❌ 读取 {file} 失败: {e}")
                    
    if count > 0:
        logger.info(f"🎉 本地扫描完毕,共增量生成 {count} 个 STRM 文件!")
        send_push("🎬 本地STRM同步成功", f"成功增量生成了 {count} 个双轨文件。")
    else:
        logger.info("⚠️ 扫描完毕:没有发现需要新增的 STRM。")

@app_main.route('/api/sync_local')
def api_sync_local():
    threading.Thread(target=local_cas_sync_worker, daemon=True).start()
    return "✅ 本地扫描指令下发成功", 200

# ==========================================
# ⚡ 极速双轨 API:支持外部脚本调用
# ==========================================
@app_main.route('/api/make_strm', methods=['POST'])
def api_make_strm():
    try:
        data = request.json
        source_cas_path = data.get('source_cas_path')    
        target_local_dir = data.get('target_local_dir')
        target_local_dir_native = data.get('target_local_dir_native')
        strm_name = data.get('strm_name')                
        show_name = data.get('show_name')                
        mode = data.get('mode', 'cas') 

        if not all([source_cas_path, target_local_dir, strm_name, show_name]):
            return jsonify({"code": 400, "msg": "指令参数不全"}), 400

        if not os.path.exists(source_cas_path):
            return jsonify({"code": 404, "msg": f"找不到源文件: {source_cas_path}"}), 404

        cfg = read_config()
        with open(source_cas_path, 'r', encoding='utf-8') as f:
            cas_content = f.read().strip()

        base_name = os.path.splitext(strm_name)[0]

        # 🛡️ 模式 A
        if mode in ['cas', 'both']:
            os.makedirs(target_local_dir, exist_ok=True)
            strm_path_a = os.path.join(target_local_dir, strm_name)
            strm_data_a = f"{cfg['server_host']}/play?cas={urllib.parse.quote(cas_content)}&show={urllib.parse.quote(show_name)}"
            with open(strm_path_a, "w", encoding="utf-8") as f:
                f.write(strm_data_a)

        # ⚡ 模式 B
        if mode in ['cas_native', 'both']:
            out_dir_b = target_local_dir_native if target_local_dir_native else target_local_dir
            os.makedirs(out_dir_b, exist_ok=True)
            strm_path_b = os.path.join(out_dir_b, f"{base_name}-Direct.strm")
            strm_data_b = f"{cfg['server_host']}/play?drive=189_native&cas={urllib.parse.quote(cas_content)}&show={urllib.parse.quote(show_name)}"
            with open(strm_path_b, "w", encoding="utf-8") as f:
                f.write(strm_data_b)

        logger.info(f"✨ STRM 生成完毕: {strm_name} [模式: {mode}]")
        return jsonify({"code": 200, "msg": "success"}), 200

    except Exception as e:
        logger.error(f"❌ make_strm 接口异常: {e}")
        return jsonify({"code": 500, "msg": str(e)}), 500


# ==========================================
# 🔀 独立服务二号头:5001 端口专属 302 劫持 (包含多版本彻底修复)
# ==========================================
emby_session = requests.Session()

@functools.lru_cache(maxsize=256)
def get_emby_item_path(item_id, media_source_id=None):
    # 彻底修复 strm 多版本合并时,只读取默认版本的 Bug
    clean_media_id = str(media_source_id).replace('mediasource_', '').strip() if media_source_id else None
    query_ids = f"{clean_media_id},{item_id}" if clean_media_id else item_id
    
    def _extract_path(api_key, desc):
        url = f"{EMBY_HOST}/emby/Items?Ids={query_ids}&Fields=Path,MediaSources&api_key={api_key}"
        try:
            res = emby_session.get(url, timeout=3)
            if res.status_code == 200:
                items = res.json().get('Items', [])
                
                # 1. 优先尝试精确匹配用户点击的附加版本
                if clean_media_id:
                    for item in items:
                        if str(item.get('Id')) == clean_media_id:
                            return item.get('Path', ''), f"{desc}-多版本精准命中"
                        for source in item.get('MediaSources', []):
                            if str(source.get('Id')) == clean_media_id:
                                # 注意:如果是源匹配,直接返回它原本对应的路径
                                return source.get('Path', item.get('Path', '')), f"{desc}-多版本精准命中"
                                
                # 2. 兜底
                if items:
                    sources = items[0].get('MediaSources', [])
                    if sources:
                        return sources[0].get('Path', ''), f"{desc}-默认版本"
                    return items[0].get('Path', ''), f"{desc}-兜底主路径"
        except: pass
        return None, None

    res_path, res_desc = _extract_path(API_KEY_LINUX, "Linux(主力)")
    if res_path: return res_path, res_desc
    return _extract_path(API_KEY_APP, "APP(备用)")

@app_302.route('/', defaults={'path': ''}, methods=['GET', 'HEAD', 'POST', 'OPTIONS'])
@app_302.route('/<path:full_path>', methods=['GET', 'HEAD', 'POST', 'OPTIONS'])
def catch_all_for_emby(full_path):
    match = re.search(r'/(?:videos|Items)/(\d+)/(?:stream|original|Download)', request.path, re.IGNORECASE)
    if not match:
        return redirect(f"{EMBY_HOST}{request.full_path}", code=302)
    
    item_id = match.group(1)
    media_source_id = request.args.get('MediaSourceId') or request.args.get('mediaSourceId') or request.args.get('mediasourceid')
    
    try:
        file_path, version = get_emby_item_path(item_id, media_source_id)
        if not file_path: return redirect(f"{EMBY_HOST}{request.full_path}", code=302)
        
        strm_url = None
        file_name = file_path.split('/')[-1] if file_path else "未知文件"

        if file_path.startswith('http://') or file_path.startswith('https://') or file_path.startswith('play?'):
            strm_url = file_path
            file_name = "直通解析链接(MediaSource)"
            
        elif file_path.lower().endswith('.strm') and os.path.exists(file_path):
            with open(file_path, 'r', encoding='utf-8') as f: 
                strm_url = f.read().strip()

        if strm_url:
            cfg = read_config()
            lan_ip = get_lan_server_ip(request)
            
            parsed_url = urllib.parse.urlparse(strm_url)
            query_params = urllib.parse.parse_qs(parsed_url.query)
            
            # 🔥 坚决保留:用户指定的全局强制直连开关
            is_direct = False
            if 'direct' in file_path.lower() or 'modeb' in file_path.lower():
                is_direct = True
                
            if str(cfg.get('force_mode_b', 'false')).lower() == 'true':
                is_direct = True
                
            if is_direct and 'cas' in query_params and 'drive' not in query_params:
                query_params['drive'] = ['189_native']
                logger.warning(f"⚠️ 触发全局/多轨直连!已强行挂载 189_native 极速直通引擎!")

            new_query = urllib.parse.urlencode(query_params, doseq=True)
            path_str = parsed_url.path if parsed_url.path.startswith('/') else '/' + parsed_url.path
            
            if not parsed_url.netloc:
                host_base = f"http://{lan_ip}:5000" if lan_ip else cfg['server_host']
                strm_url = f"{host_base}{path_str}?{new_query}"
            else:
                if lan_ip:
                    strm_url = f"http://{lan_ip}:5000{path_str}?{new_query}"
                else:
                    strm_url = f"{parsed_url.scheme}://{parsed_url.netloc}{path_str}?{new_query}"
            
            if lan_ip:
                logger.info(f"🏠 [内网嗅探] 识别到局域网播放,带参重组路由至: 5000端口本地流")
                
            logger.info(f"✅ 🔺[{version}] 劫持放行! 目标流 -> {file_name}")
            return redirect(strm_url, code=302)
        else:
            logger.info(f"▶️ 🔻[{version}常规播放] 丢回Emby -> {file_name}")
            return redirect(f"{EMBY_HOST}{request.full_path}", code=302)
    except Exception as e:
        logger.error(f"❌ [劫持出错] 兜底放行: {e}")
        return redirect(f"{EMBY_HOST}{request.full_path}", code=302)

# ==========================================
# 🚀 启动
# ==========================================
def run_main():
    app_main.run(host='0.0.0.0', port=5000, use_reloader=False)

def run_302():
    app_302.run(host='0.0.0.0', port=5001, use_reloader=False)

def keep_alive_worker():
    logger.info("🛡️ 后台错峰保活机制已启动...")
    time.sleep(600)
    while True:
        try:
            cfg = read_config()
            clouds = cfg.get('family_clouds', [])
            for i, fc in enumerate(clouds):
                if not fc.get('username') or not fc.get('password'):
                    continue
                sk = fc.get('session_key')
                fam_id = fc.get('family_id')
                fold_id = fc.get('hard_folder_id')
                
                is_alive = False
                if sk and fam_id and fold_id:
                    try:
                        client.get_family_items(fam_id, fold_id, sk)
                        is_alive = True
                    except Exception as e:
                        err_str = str(e).lower()
                        if any(k in err_str for k in ["auth_fail", "111", "session"]):
                            is_alive = False 
                        else:
                            is_alive = True
                if not is_alive:
                    refresh_slot_logic(i, cfg)
                sleep_time = random.randint(600, 900)
                time.sleep(sleep_time)
        except Exception as e:
            logger.error(f"❌ 保活线程意外: {e}")
        time.sleep(7200)

if __name__ == '__main__':
    logger.info("✅ 🚦 媒体中心管家启动!(5000端口负责直链/后台 | 5001端口负责Emby劫持)")
    threading.Thread(target=keep_alive_worker, daemon=True).start()
    t1 = threading.Thread(target=run_main)
    t2 = threading.Thread(target=run_302)
    t1.start()
    t2.start()
    t1.join()
    t2.join()

2.pm2启动

cd ~/189py && pm2 start casplay.py --name "casplay" --interpreter python

四、添加机器人指令

1.打开BotFather机器人

2.发指令/setcommands

3.选择自己的机器人

4.粘贴如下内容:

sub - 📥 [订阅/绑定] 绑定外部链接追剧
dropbox - 🚜 [投递/本地扫描/扫箱子] 洗名并入库本地CAS文件
harvest - 🚜 [收割/处理/添加] 洗名并入库云端CAS文件
feed - 📡 [动态/广场] 订阅中心最新情报
search - 🔍 [搜 关键词] 穿甲雷达搜索
check - 🔍 [查 剧名] 剧名查找
author - 🕵️‍♂️ [查作者\查人] 大佬真实时间线
info - 🎞 [查剧\信息] TMDB影视资料
refresh- 🔄 [刷新\入库] 刷新入库某剧
sync - 🔄 [同步订阅] 强制检查所有更新
list - 📋 [列表] 查看当前追剧清单
ascan - ✔️ [开启自动收割] 自动收割扫描
sscan - ⭕️ [关闭自动收割] 关闭收割扫描
asub - ✅ [开启订阅检查] 开启订阅检查
ssub - ❎ [关闭订阅检查] 关闭订阅检查
ldir - 🔍 [查目录] 查看收割家庭云目录
adir - ➕ [加目录] 增加收割家庭云目录
ddir - ❌ [删目录] 删除收割家庭云目录
listcopy - 🔍 [查个人云] 查看收割个人云目录
addcopy - ➕ [加个人云] 增加收割个人云目录
rmcopy - ❌ [删个人云] 删除收割个人云目录
hsub - ➕ [加库] 增加收割入库记录
dsub - ❌ [删库] 删除收割入库记录
lsub - 🔍 [查库] 查看收割入库清单
mode - ⚙️ [设置模式] 切换189管家STRM生成模式(A/B/C)
sync139 - ☁️ [同步139] 触发139移动云盘专属STRM生成
cancel - 🚫 [取消] 解除监控任务并清理关联记忆

也可以看看