Blog

  • removeplus

    import sys
    import subprocess
    import os
    import asyncio
    from datetime import datetime, timedelta, timezone

    ==================== 0. 面板专属:环境依赖自动安装器 ====================

    lib_path = os.path.join(os.path.dirname(os.path.abspath(file)), ‘libs’)
    sys.path.insert(0, lib_path)

    try:
    import telethon
    import dotenv
    except ImportError:
    print(“>>> 首次运行:检测到缺少依赖,正在局部目录(libs)自动安装,请稍候…”)
    subprocess.check_call([
    sys.executable, “-m”, “pip”, “install”, “-t”, lib_path,
    “telethon”, “python-dotenv”
    ])
    print(“>>> 依赖安装完成!准备启动程序…”)

    from telethon import TelegramClient, errors
    from telethon.tl.types import (
    MessageMediaDocument,
    DocumentAttributeFilename,
    InputMessagesFilterVideo
    )
    from dotenv import load_dotenv

    ================== 1. 读取配置 ==================

    load_dotenv()

    try:
    api_id = int(os.getenv(“API_ID”))
    api_hash = os.getenv(“API_HASH”)
    PHONE_NUMBER = os.getenv(“PHONE_NUMBER”)
    TWO_STEP_PASSWORD = os.getenv(“TWO_STEP_PASSWORD”) or None

    raw_target = os.getenv("TARGET_CHANNEL")
    try:
        TARGET_CHANNEL = int(raw_target)
    except (ValueError, TypeError):
        TARGET_CHANNEL = raw_target
    
    # 🎯 数量限制(0 = 不限制)
    TARGET_VIDEO_COUNT = int(os.getenv("SCAN_LIMIT", "0"))
    
    # ⏰ 时间限制
    DAYS_LIMIT = int(os.getenv("DAYS_LIMIT", "0"))
    HOURS_LIMIT = int(os.getenv("HOURS_LIMIT", "0"))
    
    time_threshold = None
    if DAYS_LIMIT > 0:
        time_threshold = datetime.now(timezone.utc) - timedelta(days=DAYS_LIMIT)
    elif HOURS_LIMIT > 0:
        time_threshold = datetime.now(timezone.utc) - timedelta(hours=HOURS_LIMIT)

    except Exception as e:
    print(f”❌ 配置错误: {e}”)
    sys.exit(1)

    提前创建 session 文件夹,防止面板报错

    if not os.path.exists(“session”):
    os.makedirs(“session”)

    client = TelegramClient(“session/user_session”, api_id, api_hash)

    ================== 2. 工具函数 ==================

    def get_video_info(message):
    if not message.media or not isinstance(message.media, MessageMediaDocument):
    return False, None, None

    doc = message.media.document
    
    if not (doc.mime_type and doc.mime_type.startswith("video/")):
        return False, None, None
    
    file_name = "未知文件名"
    duration = 0
    
    for attr in doc.attributes:
        if isinstance(attr, DocumentAttributeFilename):
            file_name = attr.file_name
        if hasattr(attr, "duration"):
            duration = attr.duration
    
    # ✅ 推荐判重 key(增强版)
    key = (doc.size, doc.mime_type, doc.dc_id, duration)
    
    return True, key, file_name

    ================== 3. 主逻辑 ==================

    async def main():
    print(“🔐 正在登录 Telegram…”)
    await client.start(phone=PHONE_NUMBER, password=TWO_STEP_PASSWORD)
    print(“✅ 登录成功”)

    try:
        target = await client.get_entity(TARGET_CHANNEL)
        target_name = getattr(target, "title", TARGET_CHANNEL)
    except Exception as e:
        print(f"❌ 无法获取频道: {e}")
        return
    
    print("\n📺 目标频道:", target_name)
    
    if DAYS_LIMIT > 0:
        print(f"🕒 时间范围:最近 {DAYS_LIMIT} 天")
    elif HOURS_LIMIT > 0:
        print(f"🕒 时间范围:最近 {HOURS_LIMIT} 小时")
    else:
        print("🕒 时间范围:无限制")
    
    if TARGET_VIDEO_COUNT == 0:
        print("🎯 视频数量:无限制")
    else:
        print(f"🎯 视频数量:最多 {TARGET_VIDEO_COUNT} 个")
    
    print("⚙️ 判重策略:size + mime + dc + duration")
    print("-" * 40)
    
    seen_keys = set()
    duplicates = []
    
    scanned_msgs = 0
    found_videos = 0
    
    print("⏳ 扫描中(仅视频)...")
    
    async for msg in client.iter_messages(
        target,
        limit=None,
        filter=InputMessagesFilterVideo
    ):
        scanned_msgs += 1
    
        # ⏰ 时间限制
        if time_threshold and msg.date < time_threshold:
            print("⏹ 已超出时间范围,停止扫描")
            break
    
        is_vid, key, file_name = get_video_info(msg)
        if not is_vid:
            continue
    
        found_videos += 1
    
        if key in seen_keys:
            duplicates.append((msg.id, file_name))
        else:
            seen_keys.add(key)
    
        # 📊 进度
        if found_videos % 20 == 0:
            print(f"   已处理 {found_videos} 个视频")
    
        # 🎯 数量限制
        if TARGET_VIDEO_COUNT != 0 and found_videos >= TARGET_VIDEO_COUNT:
            print("✅ 达到数量限制,停止扫描")
            break
    
    print("-" * 40)
    print("📊 统计结果")
    print(f"扫描消息数:{scanned_msgs}")
    print(f"视频数:{found_videos}")
    print(f"重复数:{len(duplicates)}")
    
    if not duplicates:
        print("✅ 没有重复视频")
        return
    
    # 这里会暂停并等待输入 
    print("✅ 按Y输入重复视频")
    confirm = input(f"\n⚠️ 删除 {len(duplicates)} 条重复视频?(y确认): ").strip().lower()
    if confirm != "y":
        print("🚫 已取消")
        return
    
    print("🗑️ 开始删除...")
    
    delete_ids = [d[0] for d in duplicates]
    batch_size = 50
    
    for i in range(0, len(delete_ids), batch_size):
        batch = delete_ids[i:i + batch_size]
        try:
            await client.delete_messages(target, batch)
            print(f"   已删除 {min(i+batch_size, len(delete_ids))}/{len(delete_ids)}")
    
            await asyncio.sleep(2)
    
        except errors.FloodWaitError as e:
            print(f"⏳ FloodWait: 等待 {e.seconds}s")
            await asyncio.sleep(e.seconds + 2)
    
        except errors.MessageIdInvalidError:
            print("⚠️ 某些消息已不存在,跳过")
        except Exception as e:
            print(f"❌ 删除失败: {e}")
    
    print("✅ 清理完成")

    ================== 4. 入口 ==================

    if name == “main“:
    with client:
    client.loop.run_until_complete(main())

  • getnamelist

    import sys
    import subprocess
    import os
    import csv
    import datetime

    ==================== 0. 面板专属:环境依赖自动安装器 ====================

    lib_path = os.path.join(os.path.dirname(os.path.abspath(file)), ‘libs’)
    sys.path.insert(0, lib_path)

    try:
    import telethon
    import dotenv
    except ImportError:
    print(“>>> 首次运行:检测到缺少依赖,正在局部目录(libs)自动安装,请稍候…”)
    subprocess.check_call([
    sys.executable, “-m”, “pip”, “install”, “-t”, lib_path,
    “telethon”, “python-dotenv”
    ])
    print(“>>> 依赖安装完成!准备启动程序…”)

    确保安装后可以正常导入

    from telethon import TelegramClient
    from dotenv import load_dotenv

    ==================== 1. 基础配置与初始化 ====================

    load_dotenv()

    API_ID = os.getenv(“API_ID”)
    API_HASH = os.getenv(“API_HASH”)
    PHONE_NUMBER = os.getenv(“PHONE_NUMBER”)
    TWO_STEP_PASSWORD = os.getenv(“TWO_STEP_PASSWORD”)

    if not API_ID or not API_HASH:
    print(“❌ 错误: 请确保 .env 文件中配置了 API_ID 和 API_HASH”)
    sys.exit(1)

    提前创建 session 文件夹,防止面板系统因无权限报错

    if not os.path.exists(“session”):
    os.makedirs(“session”)

    client = TelegramClient(‘session/user_session’, int(API_ID), API_HASH)

    def get_bot_api_id(entity, entity_type):
    “””
    根据实体类型将 Telethon ID 转换为 Bot API ID
    Bot API 规则:
    – 频道/超级群: -100 + ID
    – 普通小群: – + ID
    – 用户: ID (不变)
    “””
    raw_id = entity.id
    if entity_type in [“频道”, “超级群”, “频道(未知)”]:
    return int(f”-100{raw_id}”)
    elif entity_type == “普通群”:
    return int(f”-{raw_id}”)
    else:
    return raw_id

    async def list_and_export_chats():
    “””列出并导出账号加入的所有频道和群组”””
    print(“📃 正在获取对话列表,请稍候…”)

    chat_data_list = []
    
    async for dialog in client.iter_dialogs():
        entity = dialog.entity
        entity_type = "未知"
        
        # --- 分类逻辑 ---
        if dialog.is_user:
            entity_type = "私聊"
        elif dialog.is_channel:
            if getattr(entity, 'broadcast', False):
                entity_type = "频道"
            elif getattr(entity, 'megagroup', False):
                entity_type = "超级群"
            else:
                entity_type = "频道(未知)"
        elif dialog.is_group:
            entity_type = "普通群"
        
        # 过滤掉私聊 (如果需要私聊ID,注释掉下面这行)
        if entity_type != "私聊":
            # 获取两种格式的 ID
            raw_id = entity.id
            bot_api_id = get_bot_api_id(entity, entity_type)
    
            chat_info = {
                "类型": entity_type,
                "名称": dialog.name,
                "Bot_API_ID": bot_api_id,
                "原始_ID": raw_id,
                "用户名": getattr(entity, 'username', '无') or '无',
                "成员数": getattr(entity, 'participants_count', '未知')
            }
            chat_data_list.append(chat_info)
            
            print(f"[{entity_type}] {dialog.name} | Bot_ID: {bot_api_id}")
    
    # === 导出到 CSV 文件 ===
    if chat_data_list:
        # --- 文件夹处理逻辑 ---
        folder_name = "namelist"
        if not os.path.exists(folder_name):
            os.makedirs(folder_name)
            print(f"📁 已自动创建文件夹: {folder_name}")
    
        timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"telegram_chats_{timestamp}.csv"
        
        # 使用 os.path.join 确保跨平台兼容性
        file_path = os.path.join(folder_name, filename)
        
        # 更新表头
        headers = ["类型", "名称", "Bot_API_ID", "原始_ID", "用户名", "成员数"]
        
        try:
            with open(file_path, mode='w', encoding='utf-8-sig', newline='') as f:
                writer = csv.DictWriter(f, fieldnames=headers)
                writer.writeheader()
                writer.writerows(chat_data_list)
            
            print("-" * 50)
            print(f"✅ 成功!共导出 {len(chat_data_list)} 个群组/频道。")
            print(f"📁 文件已保存到: {os.path.abspath(file_path)}")
            
        except Exception as e:
            print(f"❌ 导出文件失败: {e}")
    else:
        print("⚠️ 未找到任何群组或频道。")

    async def main():
    await client.start(phone=PHONE_NUMBER, password=TWO_STEP_PASSWORD)
    print(“✅ 登录成功”)
    await list_and_export_chats()

    if name == “main“:
    with client:
    client.loop.run_until_complete(main())

  • forwardplus

    import sys
    import subprocess
    import os
    import asyncio
    import random
    import logging
    import hashlib
    from typing import List, Dict

    ==================== 0. 面板专属:环境依赖自动安装器 ====================

    lib_path = os.path.join(os.path.dirname(os.path.abspath(file)), ‘libs’)
    sys.path.insert(0, lib_path)

    try:
    import telethon
    import aiosqlite
    import dotenv
    except ImportError:
    print(“>>> 首次运行:检测到缺少依赖,正在局部目录(libs)自动安装,请稍候…”)
    subprocess.check_call([
    sys.executable, “-m”, “pip”, “install”, “-t”, lib_path,
    “telethon”, “aiosqlite”, “python-dotenv”
    ])
    print(“>>> 依赖安装完成!准备启动程序…”)

    import aiosqlite
    from telethon import TelegramClient
    from telethon.tl.types import MessageMediaDocument, DocumentAttributeVideo, Message
    from telethon.errors import (
    FloodWaitError,
    SecurityError,
    FileReferenceExpiredError,
    rpcerrorlist
    )
    from dotenv import load_dotenv

    加载环境变量

    load_dotenv()

    ==================== 📁 文件夹初始化 ====================

    DB_FOLDER = “database”
    SESSION_FOLDER = “session”

    for folder in [DB_FOLDER, SESSION_FOLDER]:
    if not os.path.exists(folder):
    os.makedirs(folder)

    ==================== 🛠️ 配置读取帮助函数 ====================

    def get_env_float(key, default=0.0):
    val = os.getenv(key, “”)
    if not val: return default
    try:
    return float(val)
    except ValueError:
    print(f”⚠️ 配置错误: {key} 必须是数字,当前为 ‘{val}’,已重置为 {default}”)
    return default

    def get_env_int(key, default=0):
    val = os.getenv(key, “”)
    if not val: return default
    try:
    return int(val)
    except ValueError:
    return default

    ==================== ⚙️ 基础配置 ====================

    API_ID = get_env_int(“API_ID”)
    API_HASH = os.getenv(“API_HASH”)
    PHONE_NUMBER = os.getenv(“PHONE_NUMBER”)

    MIN_SIZE_MB = get_env_float(“MIN_SIZE_MB”, 0)
    MAX_SIZE_MB = get_env_float(“MAX_SIZE_MB”, 0)
    MIN_DURATION = get_env_int(“MIN_DURATION”, 0)
    MAX_DURATION = get_env_int(“MAX_DURATION”, 0)

    STOP_AFTER_DUPLICATES = 100
    MAX_SCAN_COUNT = 22000

    MIN_INTERVAL = 2
    MAX_INTERVAL = 5
    ALBUM_WAIT_TIME = 3.0

    ==================== 📋 任务清单 ====================

    TASK_CONFIG = [


    ]

    ==================== 日志初始化 ====================

    logging.basicConfig(
    level=logging.INFO,
    format=”%(asctime)s – %(message)s”,
    datefmt=”%H:%M:%S”
    )
    logger = logging.getLogger(“AutoBot”)

    session_path = os.path.join(SESSION_FOLDER, “user_session”)
    client = TelegramClient(session_path, API_ID, API_HASH)

    全局状态变量

    forward_queue = None
    pending_albums = {}
    album_timers = {}
    db = None

    ==================== 🗄️ 数据库类 ====================

    class AsyncDB:
    def init(self, path):
    self.path = path
    self.conn = None
    self.lock = asyncio.Lock()

    async def connect(self):
        self.conn = await aiosqlite.connect(self.path)
        await self.conn.execute("CREATE TABLE IF NOT EXISTS videos (video_key TEXT PRIMARY KEY)")
        await self.conn.commit()
    
    async def close(self):
        if self.conn: await self.conn.close()
    
    async def seen(self, key):
        async with self.lock:
            async with self.conn.execute("SELECT 1 FROM videos WHERE video_key=?", (key,)) as cursor:
                return await cursor.fetchone() is not None
    
    async def mark(self, key):
        async with self.lock:
            await self.conn.execute("INSERT OR IGNORE INTO videos (video_key) VALUES (?)", (key,))
            await self.conn.commit()

    ==================== 🛠️ 核心工具函数 ====================

    def get_unique_key(msg: Message) -> str:
    return f”{msg.chat_id}_{msg.media.document.id}”

    def generate_safe_filename(sources: List[int], target: int) -> str:
    sources_str = “_”.join([str(abs(s)) for s in sources])
    target_str = str(abs(target))

    if len(sources_str) > 50:
        hash_object = hashlib.md5(sources_str.encode())
        short_hash = hash_object.hexdigest()[:8]
        preview = "_".join([str(abs(s)) for s in sources[:2]])
        fname = f"{preview}_etc_{short_hash}_to_{target_str}.db"
    else:
        fname = f"{sources_str}to{target_str}.db"
    
    return os.path.join(DB_FOLDER, fname)

    def is_video(msg: Message) -> bool:
    if not msg.media or not isinstance(msg.media, MessageMediaDocument):
    return False
    doc = msg.media.document
    if not getattr(doc, ‘mime_type’, None) or not doc.mime_type.startswith(“video”):
    return False
    video_attr = next((a for a in doc.attributes if isinstance(a, DocumentAttributeVideo)), None)
    if getattr(video_attr, “round_message”, False):
    return False
    size_mb = doc.size / (1024.0 * 1024.0)
    if MIN_SIZE_MB > 0 and size_mb < MIN_SIZE_MB: return False
    if MAX_SIZE_MB > 0 and size_mb > MAX_SIZE_MB: return False
    if video_attr:
    duration = video_attr.duration
    if MIN_DURATION > 0 and duration < MIN_DURATION: return False
    if MAX_DURATION > 0 and duration > MAX_DURATION: return False
    return True

    ==================== 🔄 异步逻辑 ====================

    async def process_album_later(grouped_id):
    try:
    await asyncio.sleep(ALBUM_WAIT_TIME)
    if grouped_id in pending_albums:
    messages = pending_albums.pop(grouped_id)
    album_timers.pop(grouped_id, None)
    if messages:
    messages.sort(key=lambda x: x.id)
    await forward_queue.put(messages)
    except asyncio.CancelledError:
    pass

    async def queue_message(message: Message):
    if message.grouped_id:
    gid = message.grouped_id
    if gid not in pending_albums: pending_albums[gid] = []
    pending_albums[gid].append(message)
    if gid in album_timers: album_timers[gid].cancel()
    album_timers[gid] = asyncio.create_task(process_album_later(gid))
    else:
    await forward_queue.put([message])

    async def worker(target_channel_id):
    processed_count = 0
    while True:
    try:
    batch = await forward_queue.get()
    if batch is None:
    forward_queue.task_done()
    break
    files = [m.media for m in batch]
    caption = batch[0].text or “”
    try:
    await client.send_file(target_channel_id, files, caption=caption)
    processed_count += len(batch)
    logger.info(f”✅ 发送成功 | 本次: {len(batch)} | 总计: {processed_count}”)
    for m in batch:
    await db.mark(get_unique_key(m))
    await asyncio.sleep(random.uniform(MIN_INTERVAL, MAX_INTERVAL))
    except FloodWaitError as e:
    logger.warning(f”⏳ 触发流控: 暂停 {e.seconds} 秒”)
    await asyncio.sleep(e.seconds + 2)
    except FileReferenceExpiredError:
    logger.error(“❌ 文件引用过期,跳过”)
    except Exception as e:
    logger.error(f”❌ 发送异常: {e}”)
    forward_queue.task_done()
    except Exception as e:
    logger.error(f”Worker Error: {e}”)

    async def scanner(source_channels, forward_limit):
    all_collected_videos = []
    for ch in source_channels:
    channel_collected = 0
    logger.info(f”🔍 正在扫描: {ch} (目标: {forward_limit})”)
    consecutive_duplicates = 0
    scanned_count = 0
    async for msg in client.iter_messages(ch, limit=MAX_SCAN_COUNT):
    scanned_count += 1
    if scanned_count % 200 == 0:
    print(f”\r …扫描深度: {scanned_count} 条”, end=””)
    if not is_video(msg): continue
    if await db.seen(get_unique_key(msg)):
    consecutive_duplicates += 1
    if consecutive_duplicates >= STOP_AFTER_DUPLICATES:
    print(f”\n🛑 [频道 {ch}] 连续 {STOP_AFTER_DUPLICATES} 条重复,停止扫描。”)
    break
    else:
    consecutive_duplicates = 0
    all_collected_videos.append(msg)
    channel_collected += 1
    print(f”\r📦 [频道 {ch}] 命中: {channel_collected}/{forward_limit}”, end=””)
    if channel_collected >= forward_limit:
    print(f”\n✅ [频道 {ch}] 额度已满”)
    break
    print(“”)

    if all_collected_videos:
        logger.info(f"📊 找到 {len(all_collected_videos)} 个新视频。排序入队...")
        all_collected_videos.sort(key=lambda x: x.date)
        for msg in all_collected_videos:
            await queue_message(msg)
        if pending_albums:
            logger.info("⏳ 等待相册分组...")
            while pending_albums or album_timers:
                finished = [k for k, t in album_timers.items() if t.done()]
                for k in finished: del album_timers[k]
                if not album_timers and not pending_albums: break
                await asyncio.sleep(0.5)
    else:
        logger.info("⚠️ 没有发现任何新视频。")
    await forward_queue.put(None)

    async def run_single_task(task):
    global db, forward_queue, pending_albums, album_timers

    sources = task['sources']
    target = task['target']
    limit = task.get('limit', 200)
    
    full_db_path = generate_safe_filename(sources, target)
    logger.info(f"\n🚀 >>> 启动任务: {os.path.basename(full_db_path)} <<<")
    
    forward_queue = asyncio.Queue()
    pending_albums = {}
    album_timers = {}
    
    db = AsyncDB(full_db_path)
    await db.connect()
    
    worker_task = asyncio.create_task(worker(target))
    
    try:
        await scanner(sources, limit)
        await forward_queue.join()
    except Exception as e:
        logger.error(f"❌ 任务运行异常: {e}")
    finally:
        if not worker_task.done():
            worker_task.cancel()
        await db.close()
        logger.info(f"🏁 >>> 任务结束: {os.path.basename(full_db_path)} <<<\n")

    ==================== 🚀 主程序入口 ====================

    async def main():
    print(“=”*50)
    print(“🎥 Auto-Forwarder Pro (Task Sequencer)”)
    print(f”📉 最小视频限制: {MIN_SIZE_MB} MB”)
    print(f”📈 最大视频限制: {MAX_SIZE_MB if MAX_SIZE_MB > 0 else ‘不限’} MB”)
    print(“=”*50)

    if not TASK_CONFIG:
        logger.error("❌ TASK_CONFIG 为空,请在代码中配置任务列表!")
        return
    
    await client.start(PHONE_NUMBER)
    
    try:
        total_tasks = len(TASK_CONFIG)
        for index, task in enumerate(TASK_CONFIG):
            await run_single_task(task)
            
            if index < total_tasks - 1:
                wait_sec = 10
                logger.info(f"⏳ 任务 [{index + 1}/{total_tasks}] 已完成。")
                logger.info(f"⏳ 冷却中... {wait_sec} 秒后执行下一个任务...")
                await asyncio.sleep(wait_sec)
            else:
                logger.info("🎉 所有任务已按顺序执行完毕!")
    
    except KeyboardInterrupt:
        print("\n👋 用户手动停止程序")
    except Exception as e:
        logger.error(f"🔥 系统致命错误: {e}")
    finally:
        await client.disconnect()
        logger.info("🔌 已安全退出并断开连接")

    if name == “main“:
    asyncio.run(main())

  • forward

    import sys
    import subprocess
    import os

    ==================== 0. 面板专属:环境依赖自动安装器 ====================

    lib_path = os.path.join(os.path.dirname(os.path.abspath(file)), ‘libs’)
    sys.path.insert(0, lib_path)

    try:
    import telethon
    import aiosqlite
    import dotenv
    except ImportError:
    print(“>>> 首次运行:检测到缺少依赖,正在局部目录(libs)自动安装,请稍候…”)
    subprocess.check_call([
    sys.executable, “-m”, “pip”, “install”, “-t”, lib_path,
    “telethon”, “aiosqlite”, “python-dotenv”
    ])
    print(“>>> 依赖安装完成!准备启动机器人…”)

    确保安装后可以正常导入

    import asyncio
    import random
    import re
    import logging
    from logging.handlers import RotatingFileHandler
    from typing import List, Dict, Optional, Set
    import aiosqlite
    from telethon import TelegramClient
    from telethon.tl.types import MessageMediaDocument, Message, DocumentAttributeVideo
    from telethon.errors import FloodWaitError
    from dotenv import load_dotenv

    ==================== 1. 基础配置与初始化 ====================

    DB_FOLDER = “database”
    SESSION_FOLDER = “session”
    for folder in [DB_FOLDER, SESSION_FOLDER]:
    if not os.path.exists(folder):
    os.makedirs(folder)

    logging.basicConfig(
    level=logging.INFO,
    format=”%(asctime)s – %(levelname)s – [%(name)s] – %(message)s”,
    handlers=[
    RotatingFileHandler(os.path.join(DB_FOLDER, “bot_work.log”), maxBytes=1010241024, backupCount=5, encoding=”utf-8″),
    logging.StreamHandler()
    ]
    )
    logger = logging.getLogger(“SuperForwarder”)

    load_dotenv()
    API_ID = int(os.getenv(“API_ID”, 0))
    API_HASH = os.getenv(“API_HASH”, “”)
    PHONE_NUMBER = os.getenv(“PHONE_NUMBER”, “”)
    TWO_STEP_PASSWORD = os.getenv(“TWO_STEP_PASSWORD”, “”)
    TARGET_CHANNEL = int(os.getenv(“TARGET_CHANNEL”, 0))
    SOURCE_CHANNELS = [int(x.strip()) for x in os.getenv(“SOURCE_CHANNELS”, “”).split(“,”) if x.strip()]

    MAX_WORKERS = int(os.getenv(“MAX_WORKERS”, 3))
    MIN_INTERVAL = float(os.getenv(“MIN_INTERVAL”, 2.0))
    MAX_INTERVAL = float(os.getenv(“MAX_INTERVAL”, 5.0))
    ALBUM_WAIT_TIME = 4.0

    MIN_SIZE_MB = float(os.getenv(“MIN_SIZE_MB”, 0))
    MAX_SIZE_MB = float(os.getenv(“MAX_SIZE_MB”, 0))
    MIN_HEIGHT = int(os.getenv(“MIN_HEIGHT”, 0))
    MAX_HEIGHT = int(os.getenv(“MAX_HEIGHT”, 0))

    AD_PATTERNS = [
    r”https?://\S+”,
    r”t.me/\S+”,
    r”@\w+”,
    r”Via .“,
    r”[.
    ?](https?://.*?)”
    ]

    processing_msg_ids: Set[int] = set()

    ==================== 2. 数据库管理 ====================

    class AsyncDB:
    def init(self, path):
    self.path = path
    self.conn: Optional[aiosqlite.Connection] = None

    async def connect(self):
        self.conn = await aiosqlite.connect(self.path)
        await self.conn.execute("PRAGMA journal_mode=WAL;")
        await self.conn.execute("""
            CREATE TABLE IF NOT EXISTS progress (
                channel_id TEXT PRIMARY KEY, 
                last_msg_id INTEGER DEFAULT 0, 
                min_msg_id INTEGER DEFAULT 0
            )""")
        await self.conn.execute("CREATE TABLE IF NOT EXISTS videos (video_key TEXT PRIMARY KEY)")
        await self.conn.execute("CREATE INDEX IF NOT EXISTS idx_vkey ON videos (video_key);")
        await self.conn.commit()
    
    async def close(self):
        if self.conn:
            await self.conn.close()
    
    async def is_seen(self, key: str) -> bool:
        async with self.conn.execute("SELECT 1 FROM videos WHERE video_key=?", (key,)) as cursor:
            return await cursor.fetchone() is not None
    
    async def mark_seen_batch(self, keys: List[str]):
        if not keys: return
        await self.conn.executemany(
            "INSERT OR IGNORE INTO videos (video_key) VALUES (?)", 
            [(k,) for k in keys]
        )
        await self.conn.commit()
    
    async def get_prog(self, cid: int):
        async with self.conn.execute("SELECT last_msg_id, min_msg_id FROM progress WHERE channel_id=?", (str(cid),)) as cursor:
            r = await cursor.fetchone()
            return r if r else (0, 0)
    
    async def update_prog(self, cid: int, last_id: Optional[int] = None, min_id: Optional[int] = None):
        if last_id is not None:
            await self.conn.execute(
                "INSERT INTO progress (channel_id, last_msg_id) VALUES (?, ?) ON CONFLICT(channel_id) DO UPDATE SET last_msg_id=?", 
                (str(cid), last_id, last_id)
            )
        if min_id is not None:
            await self.conn.execute(
                "INSERT INTO progress (channel_id, min_msg_id) VALUES (?, ?) ON CONFLICT(channel_id) DO UPDATE SET min_msg_id=?", 
                (str(cid), min_id, min_id)
            )
        await self.conn.commit()

    ==================== 3. 工具与过滤逻辑 ====================

    def clean_caption(text: str) -> str:
    if not text: return “”
    for p in AD_PATTERNS:
    text = re.sub(p, “”, text, flags=re.I)
    return text.strip()

    def is_video(msg: Message) -> bool:
    if not msg or not msg.media or not isinstance(msg.media, MessageMediaDocument):
    return False

    doc = msg.media.document
    if getattr(doc, 'mime_type', None) is None or not doc.mime_type.startswith("video"): 
        return False
    
    size_mb = doc.size / 1048576
    if MIN_SIZE_MB > 0 and size_mb < MIN_SIZE_MB: return False
    if MAX_SIZE_MB > 0 and size_mb > MAX_SIZE_MB: return False
    
    height = 0
    if doc.attributes:
        for attr in doc.attributes:
            if isinstance(attr, DocumentAttributeVideo):
                height = attr.h
                break
    
    if MIN_HEIGHT > 0 and height < MIN_HEIGHT: return False
    if MAX_HEIGHT > 0 and height > MAX_HEIGHT: return False
    
    return True

    ==================== 4. 相册防抖聚合逻辑 ====================

    forward_queue = asyncio.Queue(maxsize=500)
    pending_albums: Dict[int, List[Message]] = {}
    album_tasks: Dict[int, asyncio.TimerHandle] = {}

    async def _flush_album(grouped_id: int):
    await asyncio.sleep(ALBUM_WAIT_TIME)
    msgs = pending_albums.pop(grouped_id, None)
    album_tasks.pop(grouped_id, None)
    if msgs:
    msgs.sort(key=lambda x: x.id)
    await forward_queue.put(msgs)

    async def handle_incoming(msg: Message):
    if msg.id in processing_msg_ids: return
    processing_msg_ids.add(msg.id)

    if len(processing_msg_ids) > 10000:
        processing_msg_ids.clear()
    
    if msg.grouped_id:
        gid = msg.grouped_id
        if gid not in pending_albums:
            pending_albums[gid] = []
        pending_albums[gid].append(msg)
    
        if gid in album_tasks:
            album_tasks[gid].cancel()
        album_tasks[gid] = asyncio.create_task(_flush_album(gid))
    else:
        await forward_queue.put([msg])

    ==================== 5. 转发 Worker ====================

    async def worker(wid: int):
    while True:
    batch = await forward_queue.get()
    try:
    to_send = []
    for m in batch:
    if not m.media or not hasattr(m.media, ‘document’):
    continue
    v_key = str(m.media.document.id)
    if not await db.is_seen(v_key):
    to_send.append(m)

            if to_send:
                caption = ""
                for m in batch:
                    if m.text:
                        caption = clean_caption(m.text)
                        break
                
                files = [m.media for m in to_send]
                await client.send_file(TARGET_CHANNEL, file=files, caption=caption, supports_streaming=True)
                
                seen_keys = [str(m.media.document.id) for m in to_send]
                await db.mark_seen_batch(seen_keys)
                
                logger.info(f"Worker-{wid} | 成功转发 {len(to_send)} 个视频")
                await asyncio.sleep(random.uniform(MIN_INTERVAL, MAX_INTERVAL))
        except FloodWaitError as e:
            logger.warning(f"Worker-{wid} 遇到 FloodWait,暂停 {e.seconds + 5} 秒")
            await asyncio.sleep(e.seconds + 5)
        except Exception as e:
            logger.error(f"Worker-{wid} 转发异常: {e}", exc_info=True)
        finally:
            forward_queue.task_done()

    ==================== 6. 扫描与同步任务 ====================

    async def scan_latest_task(cid: int):
    last_id, _ = await db.get_prog(cid)

    if last_id == 0:
        async for msg in client.iter_messages(cid, limit=1):
            last_id = msg.id
            await db.update_prog(cid, last_id=last_id)
        logger.info(f"频道 {cid} 实时监控已就绪,当前最新消息 ID: {last_id}")
    
    while True:
        try:
            async for msg in client.iter_messages(cid, min_id=last_id, reverse=True):
                if is_video(msg): 
                    await handle_incoming(msg)
                last_id = max(last_id, msg.id)
                await db.update_prog(cid, last_id=last_id)
            
            await asyncio.sleep(15)
        except FloodWaitError as e:
            await asyncio.sleep(e.seconds + 5)
        except Exception as e:
            logger.error(f"频道 {cid} 实时监控出错: {e}")
            await asyncio.sleep(30)

    async def backfill_history_task(cid: int):
    logger.info(f”历史补全任务启动: {cid}”)
    _, min_id = await db.get_prog(cid)

    if min_id == 0:
        async for msg in client.iter_messages(cid, limit=1):
            min_id = msg.id + 1
        await db.update_prog(cid, min_id=min_id)
    
    while True:
        try:
            if min_id <= 1:
                logger.info(f"频道 {cid} 历史消息已全部补全完毕!")
                break
            
            if forward_queue.qsize() < 100:
                fetched_count = 0
                async for msg in client.iter_messages(cid, offset_id=min_id, limit=50):
                    fetched_count += 1
                    min_id = msg.id
                    if is_video(msg): 
                        await handle_incoming(msg)
                
                await db.update_prog(cid, min_id=min_id)
                
                if fetched_count == 0:
                    logger.info(f"频道 {cid} 历史消息回溯完毕 (触底)。")
                    await db.update_prog(cid, min_id=1)
                    break
                    
                await asyncio.sleep(random.uniform(3.0, 7.0))
            else:
                await asyncio.sleep(10)
        except FloodWaitError as e:
            logger.warning(f"历史补全触发限制,等待 {e.seconds + 5} 秒")
            await asyncio.sleep(e.seconds + 5)
        except Exception as e:
            logger.error(f"频道 {cid} 历史补全错误: {e}")
            await asyncio.sleep(30)

    ==================== 7. 启动入口 ====================

    s_tag = str(SOURCE_CHANNELS[0]) if SOURCE_CHANNELS else “unknown”
    t_tag = str(TARGET_CHANNEL)
    db_filename = f”{s_tag}to{t_tag}.db”
    db_path = os.path.join(DB_FOLDER, db_filename)

    session_path = os.path.join(SESSION_FOLDER, “forwarder_session”)
    client = TelegramClient(session_path, API_ID, API_HASH)
    db = AsyncDB(db_path)

    async def main():
    await db.connect()
    await client.start(PHONE_NUMBER, TWO_STEP_PASSWORD)
    logger.info(f”— 登录成功 | 数据库: {db_filename} —“)

    workers = [asyncio.create_task(worker(i + 1)) for i in range(MAX_WORKERS)]
    
    tasks = []
    for cid in SOURCE_CHANNELS:
        tasks.append(asyncio.create_task(scan_latest_task(cid)))
        tasks.append(asyncio.create_task(backfill_history_task(cid)))
    
    try:
        await client.run_until_disconnected()
    finally:
        for t in tasks + workers:
            t.cancel()
        await db.close()

    if name == “main“:
    try:
    asyncio.run(main())
    except KeyboardInterrupt:
    logger.info(“程序已被用户手动终止”)