< Project-38 Tg2Tg > TG 消息转发器 * 使用 Telegram API 收集组与主题(Group Topic)消息转发到另一个 组或主题(Group Topic)里面

Creator Dave, on 16Mar. 2025
增加了 auth.py 代码 , 第一次使用这个项目程序,必须要用到的。写这文时,当时有忘记它,已经补上。

Updated on 18Mar.2025  转发问题,修改 process_message() ;统计部分增加翻页、调整行数功能;启动后自动运行“Start Forwarde” ,不需要再点击。添加SSL证书支持(证书放在/app/里)。
代码已经在文章中更新。
入口见:https:/jpt.daven.us:9018  (禁用了 "stop forwarder" button)

目的:

Telegram 后面使用 TG 作为简称。

在 TG 中,内容可以存在 组 Group,或 话题 Topic 里面。 组还有一个特殊功能是,在里面创建很多话题 Topics

TG 中人人都可以创建组或话题是(后面用 GT 作为简称),GT 就非常的多,每个 GT 下面有各自的内容,所以信息非常分散。平时要一一点开看,里面还充斥着广告,还有各种颜色视频、文字等 “妈妈禁止” 的坏内容要过滤掉。

但,我只对信息安全、国家经济信息感兴趣,别人已经过滤的信息发到自己的组里,即:把它们汇总到一个 G/T 中可以大大地减少阅读筛选。 如果要对公众展示,依据:原创新闻按照CC-BY-4.0协议发布 (只要标注新闻来源), 你可以自由地转发这些信息。

环境:

要有一个 Telegram 账号
要在TG 获得 API ID 和 API HASH ,申请用这个官网链接  https://my.telegram.org/ 
程序可以单机运行,我把它用 DOCKER 部署在 NAS 运行 (NAS 要能上外网)
 

项目: 38.Tg2Tg ver.0.4

更新历史:

0.1 基础功能实现:填写源,目标,条件词 转发信息,有统计功能
0.2 修复几个问题:配置不会保存,按钮事件不更新。
0.3 对组下的 topic 支持,填写格式: c/<group_name>/<topic_name>, 新功能:支持多组条件,可以开启或关闭以组为单位的条件
0.4 增加 :“排除”关键词功能:默认填写 None;全总内容匹配条件:通配符 ALL 或 *

1. 界面:

2. 主要功能介绍

  • 消息监控和转发
    • 监控多个源频道/群组的消息
    • 根据关键词匹配自动转发消息到指定目标
    • 支持将原始消息包括文本和媒体文件转发至目标
  • 关键词过滤
    • 基于关键词匹配转发消息
    • 支持通配符("*"或"ALL")以转发所有消息
    • 排除关键词功能:如果消息包含特定的排除关键词,即使匹配了普通关键词也不会被转发
  • 灵活的转发配置
    • 支持创建多个转发组,每组有独立的源、目标和关键词设置
    • 每个转发组可以单独启用或禁用
    • 支持将消息转发到 Telegram 话题(topics)中
  • 统计数据和监控
    • 跟踪消息监控和转发的详细统计信息
    • 显示每个组、源和目标的消息处理情况
    • 记录关键词匹配和排除情况的统计
    • 计算转发率(被转发消息占总监控消息的百分比)
  • 用户友好的 Web 界面
    • 实时显示转发服务状态
    • 直观地管理转发组配置
    • 查看详细的统计数据
    • 可视化操作界面,无需命令行操作
  • 配置持久化
    • 自动保存转发配置和统计数据
    • 任务重启后自动恢复配置
    • 定期(每10分钟)自动保存数据

(以上是 AI 读代码后总结,要能给格式就好了。)

3. 目录结构

/
├── app.py                    # 主应用程序
├── static/                   
│   ├── css/                 
│   │   └── styles.css        
│   └── js/                   
│       └── app.js          
├── templates/                
│   └── index.html            # 主页
├── forwarder_config.json     # APP配置文件
├── message_forwarder.session # Telegram 会话文件 会自动创建
├── .env                      # API info
└── requirements.txt          # Python 需要的安装依赖包清单

4. 完整代码 (AI 修改过)

主要是5个文件

1) 主程序 app.py

from telethon import TelegramClient, events
from telethon.tl.types import PeerChannel
import asyncio
import re
import logging
import os
from dotenv import load_dotenv
from flask import Flask, request, jsonify, render_template, redirect, url_for
import threading
import json
import nest_asyncio
import time
import ssl

# 配置文件路径
CONFIG_FILE = 'forwarder_config.json'

# 证书文件路径
CERT_PATH = '/app/fullchain.pem'
KEY_PATH = '/app/privkey.pem'

def save_config():
    """保存当前配置到文件"""
    config = {
        'forwarding_groups': FORWARDING_GROUPS,
        'stats': stats
    }
    
    try:
        with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
            json.dump(config, f, ensure_ascii=False, indent=2)
        logger.info(f"Configuration saved to {CONFIG_FILE}")
    except Exception as e:
        logger.error(f"Failed to save configuration: {e}")

def load_config():
    """从文件加载配置"""
    global FORWARDING_GROUPS, stats, DEFAULT_FORWARDING_GROUPS
    
    if os.path.exists(CONFIG_FILE):
        try:
            with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                config = json.load(f)
            
            # 更新当前配置
            FORWARDING_GROUPS = config.get('forwarding_groups', DEFAULT_FORWARDING_GROUPS)
            
            # 更新统计信息
            saved_stats = config.get('stats', {})
            # 确保所有必要的统计字段都存在
            if 'total_messages_seen' in saved_stats:
                stats = saved_stats
            
            logger.info(f"Configuration loaded from {CONFIG_FILE}")
            return True
        except Exception as e:
            logger.error(f"Failed to load configuration: {e}")
    
    return False

# Apply nest_asyncio to allow nested event loops
nest_asyncio.apply()

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

# Load environment variables from .env file
load_dotenv()

# Telegram API credentials - you'll need to get these from https://my.telegram.org
API_ID = os.getenv('API_ID')
API_HASH = os.getenv('API_HASH')
SESSION_NAME = 'message_forwarder'

# 默认转发配置组
DEFAULT_FORWARDING_GROUPS = [
    {
        "id": "group1",
        "name": "Finance Group",
        "sources": ["FinanceNewsDaily"],
        "destinations": ["c/cnbbs001/258522"],
        "keywords": ["财经", "经济", "金融", "股市", "投资"],
        "exclude_keywords": [],  # 添加排除关键词列表
        "enabled": True  # 添加启用状态标志
    }
]

# 全局状态跟踪变量
is_running = False
client = None
forwarder_thread = None
stats = {
    "total_messages_seen": 0,
    "messages_forwarded": 0,
    "last_forwarded": None,
    "keyword_matches": {},
    "source_stats": {},
    "destination_stats": {},
    "group_stats": {}  # 新增:按转发配置组统计
}

FORWARDING_GROUPS = DEFAULT_FORWARDING_GROUPS.copy()

last_save_time = 0

# Create Flask app
app = Flask(__name__, static_folder='static', template_folder='templates')

class MessageForwarder:
    def __init__(self, forwarding_groups=DEFAULT_FORWARDING_GROUPS, loop=None):
        # Use provided loop or create a new one
        self.loop = loop or asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
    
        # Initialize the client with the loop and additional parameters for better connection handling
        self.client = TelegramClient(
            SESSION_NAME, 
            API_ID, 
            API_HASH, 
            loop=self.loop,
            connection_retries=None,  # Infinite connection retries
            auto_reconnect=True,      # Automatically reconnect
            sequential_updates=True   # Process updates sequentially to avoid DB conflicts
        )
    
        self.forwarding_groups = forwarding_groups
        # 从所有转发组中提取唯一的源列表
        self.all_sources = self._get_all_sources()
        self.running = False
        
    def _get_all_sources(self):
        """从所有转发组中提取唯一源列表"""
        all_sources = []
        for group in self.forwarding_groups:
            # 只添加启用的组中的源
            if group.get("enabled", True):
                for source in group["sources"]:
                    if source not in all_sources:
                        all_sources.append(source)
        return all_sources
        
    async def start(self):
        """Start the client and register handlers"""
        global is_running
        
        await self.client.start()
        logger.info("Client started")
        is_running = True
        
        # 为每个唯一的源注册消息处理器
        for source in self.all_sources:
            # 创建一个闭包工厂函数
            def make_handler(src):
                async def source_handler(event):
                    logger.info(f"Received message from {src}: {event.message.text[:50] if event.message.text else 'No text'}")
                    await self.process_message(event, src)
                return source_handler
    
            # 为每个源注册独立的处理器
            handler_func = make_handler(source)
            self.client.add_event_handler(
                handler_func,
                events.NewMessage(chats=source)
            )
            
        # 打印监控信息
        sources_str = ", ".join(self.all_sources)
        logger.info(f"Monitoring messages from {sources_str}")
        
        # 打印每个转发组的配置
        for i, group in enumerate(self.forwarding_groups, 1):
            enabled_status = "Enabled" if group.get("enabled", True) else "Disabled"
            logger.info(f"Group {i} ({group['name']}) [{enabled_status}]: {', '.join(group['sources'])} -> {', '.join(group['destinations'])} | Keywords: {', '.join(group['keywords'])}")
        
        # Keep the client running
        await self.client.run_until_disconnected()
    
    async def process_message(self, event, source_chat):
        try:
            """Process incoming messages for all forwarding groups that include this source"""
            global stats
    
            message_text = event.message.text if event.message.text else ""
            logger.info(f"Processing message from {source_chat} (channel ID: {event.chat_id})")
            logger.info(f"Message content: {message_text[:100]}...")
        
            stats["total_messages_seen"] += 1

            # 调试日志
            logger.info(f"Processing message: source={source_chat}, chat_id={event.chat_id}, text={message_text[:100]}...")
    
            # Update source statistics
            if source_chat not in stats["source_stats"]:
                stats["source_stats"][source_chat] = {"messages_seen": 0, "messages_forwarded": 0}
            stats["source_stats"][source_chat]["messages_seen"] += 1

              
            # 查找包含此源的所有启用的转发组
            relevant_groups = [
                group for group in self.forwarding_groups 
                if source_chat in group["sources"] and group.get("enabled", True)
            ]

            # 调试日志 
            logger.info(f"Found {len(relevant_groups)} relevant groups for source {source_chat}")

            # 如果没有包含此源的启用的转发组,记录并返回
            if not relevant_groups:
                logger.warning(f"Received message from {source_chat} but it's not in any enabled forwarding group")
                return
        
            # 处理每个相关的转发组
            for group in relevant_groups:
                logger.info(f"Processing for group {group['name']} (ID: {group['id']})")
                group_id = group["id"]
            
                # 确保该组在统计信息中存在
                if group_id not in stats["group_stats"]:
                    stats["group_stats"][group_id] = {
                        "name": group["name"],
                        "messages_seen": 0, 
                        "messages_forwarded": 0,
                        "keyword_matches": {},
                        "keyword_exclusions":{}
                    }
            
                # 更新组统计信息
                stats["group_stats"][group_id]["messages_seen"] += 1

                # 检查是否包含排除关键词
                exclude_keywords = group.get("exclude_keywords", [])
                excluded_keywords = [kw for kw in exclude_keywords if kw in message_text]

                # 如果找到排除关键词,跳过此消息
                if excluded_keywords:
                    logger.info(f"Message from {source_chat} contains excluded keywords in group '{group['name']}': {', '.join(excluded_keywords)}")

                    # 更新排除关键词统计
                    for kw in excluded_keywords:
                        # 组排除关键词统计
                        if "keyword_exclusions" not in stats["group_stats"][group_id]:
                            stats["group_stats"][group_id]["keyword_exclusions"] = {}

                        if kw in stats["group_stats"][group_id]["keyword_exclusions"]:
                            stats["group_stats"][group_id]["keyword_exclusions"][kw] += 1
                        else:
                            stats["group_stats"][group_id]["keyword_exclusions"][kw] = 1

                    continue # 跳过处理此消息
            
                # 检查是否包含通配符或空关键词列表 (表示转发所有消息)
                has_wildcard = "*" in group["keywords"] or "ALL" in group["keywords"] or not group["keywords"]
                logger.info(f"Group '{group['name']}' wildcard check: has_wildcard={has_wildcard}, keywords={group['keywords']}")
    
                logger.info(f"Group '{group['name']}' wild check results: has_wildcard={has_wildcard}, keywords={group['keywords']}")
                if not has_wildcard:
                    matched = [kw for kw in group["keywords"] if kw in message_text]
                    logger.info(f"Regular keyword matches: {matched}")


                # 处理消息匹配
                if has_wildcard:
                    # 对于通配符组,直接匹配所有消息
                    matched_keywords = ["*"]  # 标记为通配符匹配
                    logger.info(f"Message from {source_chat} matched wildcard in group '{group['name']}'")
                else:
                    # 对于常规组,检查关键词匹配
                    matched_keywords = [kw for kw in group["keywords"] if kw in message_text]
                    if not matched_keywords:
                        # 如果没有匹配的关键词且不是通配符组,跳过此消息
                        continue
                    logger.info(f"Group '{group['name']}' wildcard check: {has_wildcard}, keywords: {group['keywords']}")
                    logger.info(f"Message from {source_chat} matched keywords in group '{group['name']}': {', '.join(matched_keywords)}")
                
                # 更新关键词匹配统计
                for kw in matched_keywords:
                    # 全局关键词统计
                    if kw in stats["keyword_matches"]:
                        stats["keyword_matches"][kw] += 1
                    else:
                        stats["keyword_matches"][kw] = 1
            
                    # 组关键词统计
                    if kw in stats["group_stats"][group_id]["keyword_matches"]:
                        stats["group_stats"][group_id]["keyword_matches"][kw] += 1
                    else:
                        stats["group_stats"][group_id]["keyword_matches"][kw] = 1
            
                # 添加来源和组信息到消息
                formatted_message = f"From: {source_chat} (Group: {group['name']})\n\n{message_text}"
            
                # 转发到该组的所有目标
                for destination in group["destinations"]:
                    try:
                        # 处理主题格式 (format: "c/group/topic_id")
                        if destination.startswith("c/") and '/' in destination[2:]:
                            # 解析目标字符串以获取组和主题
                            parts = destination.split('/')
                            if len(parts) >= 3:
                                group_name = parts[1]
                                topic_id = int(parts[2])
                            
                                # 获取组实体
                                group_entity = await self.client.get_entity(group_name)
                            
                                # 转发到具有主题ID的组
                                sent_message = await self.client.send_message(
                                    entity=group_entity,
                                    message=formatted_message,  # 发送带有源信息的文本
                                    reply_to=topic_id  # 指定主题ID
                                )
                            
                                # 如果原始消息包含媒体,单独转发
                                if event.message.media:
                                    await self.client.send_file(
                                        entity=group_entity,
                                        file=event.message.media,
                                        caption=f"Media from: {source_chat} (Group: {group['name']})",
                                        reply_to=topic_id
                                    )
                            else:
                                logger.error(f"Invalid topic format: {destination}")
                                continue
                        else:
                            # 对于常规转发,添加源信息
                            dest_entity = await self.client.get_entity(destination)

                            if event.message.media:
                                # 如果有媒体,以格式化消息作为标题发送
                                await self.client.send_file(
                                    entity=dest_entity,
                                    file=event.message.media,
                                    caption=formatted_message
                                )
                            else:
                                # 否则只发送文本消息
                                await self.client.send_message(
                                    entity=dest_entity,
                                    message=formatted_message
                                )
                    
                        # 更新目标统计信息
                        if destination not in stats["destination_stats"]:
                            stats["destination_stats"][destination] = {"messages_received": 0}
                        stats["destination_stats"][destination]["messages_received"] += 1
                    
                        # 更新源统计信息
                        stats["source_stats"][source_chat]["messages_forwarded"] += 1
                    
                        # 更新组统计信息
                        stats["group_stats"][group_id]["messages_forwarded"] += 1
                    
                        # 更新全局统计信息
                        stats["messages_forwarded"] += 1
                        stats["last_forwarded"] = {
                            "group": group["name"],
                            "source": source_chat,
                            "destination": destination,
                            "text": message_text[:100] + "..." if len(message_text) > 100 else message_text,
                            "timestamp": event.message.date.isoformat(),
                            "matched_keywords": matched_keywords
                        }
                    
                        logger.info(f"Message forwarded from {source_chat} to {destination} (Group: {group['name']})")
                    except Exception as e:
                        logger.error(f"Failed to forward message to {destination} (Group: {group['name']}): {e}")
        except Exception as e:
            logger.error(f"Error processing message from {source_chat}: {e}", exc_info=True)
        
    
    async def stop(self):
        """Stop the client"""
        if self.client:
            await self.client.disconnect()
            self.running = False
            logger.info("Client stopped")


def start_forwarder(forwarding_groups=DEFAULT_FORWARDING_GROUPS):
    """Start the forwarder in a separate thread"""
    global client, forwarder_thread
    
    # Create event loop for this thread
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    
    # Create forwarder with the loop
    forwarder = MessageForwarder(forwarding_groups, loop=loop)
    client = forwarder
    
    # Run the client
    loop.run_until_complete(forwarder.start())

def periodic_save():
    """定期保存配置"""
    global last_save_time
    
    current_time = time.time()
    # 每10分钟保存一次配置
    if current_time - last_save_time > 600:  # 600秒 = 10分钟
        save_config()
        last_save_time = current_time

def generate_group_id():
    """生成唯一的组ID"""
    import uuid
    return f"group_{str(uuid.uuid4())[:8]}"

# Flask routes
@app.route('/')
def index():
    """Main page with status and controls"""
    return render_template(
        'index.html', 
        is_running=is_running, 
        stats=stats,
        forwarding_groups=FORWARDING_GROUPS
    )

@app.route('/start', methods=['POST'])
def start():
    """Start the forwarder"""
    global forwarder_thread, is_running, stats, FORWARDING_GROUPS
    
    if is_running:
        return jsonify({"status": "already_running"})
    
    # 从请求中获取转发组配置
    forwarding_groups_data = request.json.get('forwarding_groups')
    
    if forwarding_groups_data:
        # 更新当前配置
        FORWARDING_GROUPS = forwarding_groups_data
    
    # 处理统计信息重置
    reset_stats = request.json.get('reset_stats', False)
    if reset_stats:
        stats = {
            "total_messages_seen": 0,
            "messages_forwarded": 0,
            "last_forwarded": None,
            "keyword_matches": {},
            "source_stats": {},
            "destination_stats": {},
            "group_stats": {}
        }
    
    # 保存当前配置
    save_config()
    
    # 在新线程中启动转发器
    forwarder_thread = threading.Thread(
        target=start_forwarder, 
        args=(FORWARDING_GROUPS,)
    )
    forwarder_thread.daemon = True
    forwarder_thread.start()
    
    return jsonify({
        "status": "started",
        "is_running": True,
        "forwarding_groups": FORWARDING_GROUPS
    })

@app.route('/stop', methods=['POST'])
def stop():
    """Stop the forwarder"""
    global client, is_running
    
    if not is_running:
        return jsonify({"status": "not_running"})
    
    # 保存配置
    save_config()
    
    # 创建事件循环并停止客户端
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    
    if client:
        loop.run_until_complete(client.stop())
    
    is_running = False
    
    return jsonify({
        "status": "stopped",
        "is_running": False
    })

@app.route('/check_status')
def check_status():
    """返回当前服务运行状态"""
    global is_running, FORWARDING_GROUPS
    
    return jsonify({
        "is_running": is_running,
        "forwarding_groups": FORWARDING_GROUPS
    })

@app.route('/stats')
def get_stats():
    """Get current statistics"""
    # 定期保存配置
    periodic_save()
    return jsonify(stats)

@app.route('/save_config', methods=['POST'])
def save_config_route():
    """Save current configuration"""
    # 从请求中获取新的转发组配置
    global FORWARDING_GROUPS
    
    forwarding_groups_data = request.json.get('forwarding_groups')
    if forwarding_groups_data:
        FORWARDING_GROUPS = forwarding_groups_data
    
    save_config()
    return jsonify({"status": "saved"})

@app.route('/add_group', methods=['POST'])
def add_group():
    """添加新的转发组"""
    global FORWARDING_GROUPS
    
    # 获取提交的表单数据
    group_data = request.json
    
    # 给新组生成唯一ID
    if not group_data.get("id"):
        group_data["id"] = generate_group_id()
    
    # 确保启用状态存在
    if "enabled" not in group_data:
        group_data["enabled"] = True
    
    # 添加到转发组列表
    FORWARDING_GROUPS.append(group_data)
    
    # 保存配置
    save_config()
    
    return jsonify({
        "status": "added",
        "group": group_data,
        "forwarding_groups": FORWARDING_GROUPS
    })

@app.route('/update_group', methods=['POST'])
def update_group():
    """更新现有转发组"""
    global FORWARDING_GROUPS
    
    # 获取提交的组数据
    group_data = request.json
    group_id = group_data.get("id")
    
    if not group_id:
        return jsonify({"status": "error", "message": "Group ID is required"}), 400
    
    # 查找和更新组
    for i, group in enumerate(FORWARDING_GROUPS):
        if group["id"] == group_id:
            # 确保启用状态存在
            if "enabled" not in group_data:
                group_data["enabled"] = group.get("enabled", True)
                
            FORWARDING_GROUPS[i] = group_data
            
            # 保存配置
            save_config()
            
            return jsonify({
                "status": "updated",
                "group": group_data,
                "forwarding_groups": FORWARDING_GROUPS
            })
    
    return jsonify({"status": "error", "message": "Group not found"}), 404

@app.route('/delete_group', methods=['POST'])
def delete_group():
    """删除转发组"""
    global FORWARDING_GROUPS
    
    # 获取要删除的组ID
    group_id = request.json.get("id")
    
    if not group_id:
        return jsonify({"status": "error", "message": "Group ID is required"}), 400
    
    # 查找和删除组
    for i, group in enumerate(FORWARDING_GROUPS):
        if group["id"] == group_id:
            del FORWARDING_GROUPS[i]
            
            # 保存配置
            save_config()
            
            return jsonify({
                "status": "deleted",
                "group_id": group_id,
                "forwarding_groups": FORWARDING_GROUPS
            })
    
    return jsonify({"status": "error", "message": "Group not found"}), 404

@app.route('/toggle_group', methods=['POST'])
def toggle_group():
    """Enable or disable a forwarding group"""
    global FORWARDING_GROUPS
    
    # Get the group ID and desired state
    group_id = request.json.get("id")
    enabled = request.json.get("enabled")
    
    if group_id is None or enabled is None:
        return jsonify({"status": "error", "message": "Group ID and enabled state are required"}), 400
    
    # Find and update the group
    for i, group in enumerate(FORWARDING_GROUPS):
        if group["id"] == group_id:
            FORWARDING_GROUPS[i]["enabled"] = enabled
            
            # Save configuration
            save_config()
            
            logger.info(f"Group '{group['name']}' {('enabled' if enabled else 'disabled')}")
            
            return jsonify({
                "status": "updated",
                "group_id": group_id,
                "enabled": enabled,
                "forwarding_groups": FORWARDING_GROUPS
            })
    
    return jsonify({"status": "error", "message": "Group not found"}), 404

# 启动时加载配置
load_config()

# 创建SSL上下文
def create_ssl_context():
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    try:
        context.load_cert_chain(CERT_PATH, KEY_PATH)
        return context
    except Exception as e:
        logger.error(f"Failed to load SSL certificates: {e}")
        return None

# Start Flask app
if __name__ == '__main__':
    # 在启动Flask应用前自动启动转发器
    if not is_running:
        logger.info("Auto-starting message forwarder...")
        forwarder_thread = threading.Thread(
            target=start_forwarder, 
            args=(FORWARDING_GROUPS,)
        )
        forwarder_thread.daemon = True
        forwarder_thread.start()
    
    # 获取SSL上下文
    ssl_context = create_ssl_context()
    
    if ssl_context:
        # 使用HTTPS启动Flask
        logger.info("Starting Flask with HTTPS support...")
        app.run(host='0.0.0.0', port=9018, debug=False, ssl_context=ssl_context)
    else:
        # 回退到HTTP
        logger.warning("Failed to load SSL certificates, starting with HTTP only...")
        app.run(host='0.0.0.0', port=9018, debug=False)

2) templates\index.html

<!DOCTYPE html>
<html>
<head>
    <title>Telegram Message Forwarder</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
    <script src="{{ url_for('static', filename='js/app.js') }}" defer></script>
</head>
<body>
    <h1>Telegram Message Forwarder</h1>
    
    <div class="status {{ 'running' if is_running else 'stopped' }}">
        Status: <strong>{{ 'Running' if is_running else 'Stopped' }}</strong>
    </div>
    
    <!-- 转发组列表 -->
    <div class="card">
        <div class="header-with-button">
            <h2>Forwarding Groups</h2>
            <button id="addGroupBtn" class="add" {{ 'disabled' if is_running else '' }}>+ Add Group</button>
        </div>
        
        <div id="forwardingGroups">
            {% for group in forwarding_groups %}
            <div class="forwarding-group" data-group-id="{{ group.id }}">
                <div class="group-header">
                    <h3>{{ group.name }}</h3>
                    <div class="group-actions">
                        <button class="toggle-group" data-enabled="{{ group.get('enabled', True) }}" {{ 'disabled' if is_running else '' }}>
                            {{ 'Enabled' if group.get('enabled', True) else 'Disabled' }}
                        </button>
                        <button class="edit-group" {{ 'disabled' if is_running else '' }}>Edit</button>
                        <button class="delete-group" {{ 'disabled' if is_running else '' }}>Delete</button>
                    </div>
                </div>
                <div class="group-details">
                    <div class="detail-item">
                        <span class="status-badge {{ 'enabled' if group.get('enabled', True) else 'disabled' }}">
                            {{ 'Enabled' if group.get('enabled', True) else 'Disabled' }}
                        </span>
                    </div>
                    <div class="detail-item">
                        <strong>Sources:</strong> {{ ", ".join(group.sources) }}
                    </div>
                    <div class="detail-item">
                        <strong>Destinations:</strong> {{ ", ".join(group.destinations) }}
                    </div>
                    <div class="detail-item">
                        <strong>Keywords:</strong> {{ ", ".join(group.keywords) }}
                    </div>
                    <div class="detail-item">
                        <strong>Exclude Keywords:</strong> {{ ", ".join(group.get('exclude_keywords', [])) or 'None' }}
                    </div>
                </div>
            </div>
            {% endfor %}
        </div>
        
        <div class="button-group">
            <button id="startForwarderBtn" class="start" {{ 'disabled' if is_running else '' }}>Start Forwarder</button>
            <button id="stopForwarderBtn" class="stop" {{ 'disabled' if not is_running else '' }}>Stop Forwarder</button>
            <button id="saveConfigBtn" class="save">Save Configuration</button>
        </div>
    </div>
    
    <!-- 添加/编辑组对话框 -->
    <div id="groupModal" class="modal">
        <div class="modal-content">
            <span class="close">&times;</span>
            <h2 id="modalTitle">Add Forwarding Group</h2>
            <form id="groupForm">
                <input type="hidden" id="groupId" name="groupId">
                <div class="form-group">
                    <label for="groupName">Group Name:</label>
                    <input type="text" id="groupName" name="groupName" required>
                </div>
                <div class="form-group">
                    <label for="groupSources">Source Channels (comma separated):</label>
                    <input type="text" id="groupSources" name="groupSources" required>
                    <small>Multiple sources separated by commas (e.g., FinanceNewsDaily, CryptoNews, StockAlerts)</small>
                </div>
                <div class="form-group">
                    <label for="groupDestinations">Destinations (comma separated):</label>
                    <input type="text" id="groupDestinations" name="groupDestinations" required>
                    <small>Multiple destinations separated by commas (e.g., group1, channel2, user3)</small>
                </div>
                <div class="form-group">
                    <label for="groupKeywords">Keywords (comma separated):</label>
                    <input type="text" id="groupKeywords" name="groupKeywords" required>
                    <small>Messages containing any of these keywords will be forwarded</small>
                </div>
                <div class="form-group">
                    <label for="groupExcludeKeywords">Exclude Keywords (comma separated):</label>
                    <input type="text" id="groupExcludeKeywords" name="groupExcludeKeywords">
                    <small>Messages containing any of these keywords will NOT be forwarded, even if they contain matching keywords</small>
                </div>
                <div class="form-group">
                    <div class="checkbox-container">
                        <input type="checkbox" id="groupEnabled" name="groupEnabled" checked>
                        <label for="groupEnabled">Group Enabled</label>
                    </div>
                </div>
                <div class="form-group">
                    <button type="submit" class="save">Save Group</button>
                </div>
            </form>
        </div>
    </div>
    
    <div class="card">
        <h2>Global Statistics</h2>
        <table>
            <tr>
                <td>Total Messages Seen:</td>
                <td id="totalMessages">{{ stats.total_messages_seen }}</td>
            </tr>
            <tr>
                <td>Messages Forwarded:</td>
                <td id="forwardedMessages">{{ stats.messages_forwarded }}</td>
            </tr>
        </table>
        
        <h3>Keyword Matches</h3>
        <!-- Pagination controls will be inserted here by JavaScript -->
        <table>
            <thead>
                <tr>
                    <th>Keyword</th>
                    <th>Matches</th>
                </tr>
            </thead>
            <tbody id="keywordStats">
                {% for keyword, count in stats.keyword_matches.items() %}
                <tr>
                    <td>{{ keyword }}</td>
                    <td>{{ count }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
        
        <h3>Last Forwarded Message</h3>
        <pre id="lastMessage">{{ stats.last_forwarded | tojson(indent=2) if stats.last_forwarded else 'No messages forwarded yet' }}</pre>
    </div>
    
    <div class="card">
        <h2>Group Statistics</h2>
        <div id="groupStats">
            {% for group_id, group_data in stats.group_stats.items() %}
            <div class="group-stat-card">
                <h3>{{ group_data.name }}</h3>
                <table>
                    <tr>
                        <td>Messages Seen:</td>
                        <td>{{ group_data.messages_seen }}</td>
                    </tr>
                    <tr>
                        <td>Messages Forwarded:</td>
                        <td>{{ group_data.messages_forwarded }}</td>
                    </tr>
                    <tr>
                        <td>Forward Rate:</td>
                        <td>
                            {% if group_data.messages_seen > 0 %}
                                {{ (group_data.messages_forwarded / group_data.messages_seen * 100) | round(1) }}%
                            {% else %}
                                0%
                            {% endif %}
                        </td>
                    </tr>
                </table>
                
                {% if group_data.keyword_matches %}
                <h4>Keyword Matches</h4>
                <table>
                    <thead>
                        <tr>
                            <th>Keyword</th>
                            <th>Matches</th>
                        </tr>
                    </thead>
                    <tbody>
                        {% for keyword, count in group_data.keyword_matches.items() %}
                        <tr>
                            <td>{{ keyword }}</td>
                            <td>{{ count }}</td>
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
                {% endif %}

                {% if group_data.keyword_exclusions %}
                <h4>Keyword Exclusions</h4>
                <table>
                    <thead>
                        <tr>
                            <th>Excluded Keyword</th>
                            <th>Occurrences</th>
                        </tr>
                    </thead>
                    <tbody>
                        {% for keyword, count in group_data.keyword_exclusions.items() %}
                        <tr>
                            <td>{{ keyword }}</td>
                            <td>{{ count }}</td>
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
                {% endif %}
            </div>
            {% endfor %}
        </div>
    </div>
    
    <div class="card">
        <h2>Source Statistics</h2>
        <!-- Pagination controls will be inserted here by JavaScript -->
        <table>
            <thead>
                <tr>
                    <th>Source</th>
                    <th>Messages Seen</th>
                    <th>Messages Forwarded</th>
                    <th>Forward Rate</th>
                </tr>
            </thead>
            <tbody id="sourceStats">
                {% for source, data in stats.source_stats.items() %}
                <tr>
                    <td>{{ source }}</td>
                    <td>{{ data.messages_seen }}</td>
                    <td>{{ data.messages_forwarded }}</td>
                    <td>
                        {% if data.messages_seen > 0 %}
                            {{ (data.messages_forwarded / data.messages_seen * 100) | round(1) }}%
                        {% else %}
                            0%
                        {% endif %}
                    </td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>
    
    <div class="card">
        <h2>Destination Statistics</h2>
        <!-- Pagination controls will be inserted here by JavaScript -->
        <table>
            <thead>
                <tr>
                    <th>Destination</th>
                    <th>Messages Received</th>
                </tr>
            </thead>
            <tbody id="destinationStats">
                {% for dest, data in stats.destination_stats.items() %}
                <tr>
                    <td>{{ dest }}</td>
                    <td>{{ data.messages_received }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>
</body>
</html>

3) static\css\styles.css

body { 
    font-family: Arial, sans-serif; 
    max-width: 800px; 
    margin: 0 auto; 
    padding: 20px; 
    line-height: 1.6;
}

h1, h2, h3, h4 {
    color: #333;
}

.status { 
    padding: 10px; 
    border-radius: 5px; 
    margin-bottom: 20px; 
    font-weight: bold;
}

.running { 
    background-color: #d4edda; 
    color: #155724; 
    border: 1px solid #c3e6cb;
}

.stopped { 
    background-color: #f8d7da; 
    color: #721c24; 
    border: 1px solid #f5c6cb;
}

.card { 
    border: 1px solid #ddd; 
    border-radius: 5px; 
    padding: 15px; 
    margin-bottom: 20px; 
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

button { 
    padding: 10px 15px; 
    border: none; 
    border-radius: 5px; 
    cursor: pointer; 
    margin-right: 10px; 
    font-weight: bold;
    transition: background-color 0.3s ease;
}

button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.start { 
    background-color: #28a745; 
    color: white; 
}

.start:hover:not([disabled]) {
    background-color: #218838;
}

.stop { 
    background-color: #dc3545; 
    color: white; 
}

.stop:hover:not([disabled]) {
    background-color: #c82333;
}

.save { 
    background-color: #17a2b8; 
    color: white; 
}

.save:hover:not([disabled]) {
    background-color: #138496;
}

.add {
    background-color: #007bff;
    color: white;
}

.add:hover:not([disabled]) {
    background-color: #0069d9;
}

.edit-group {
    background-color: #ffc107;
    color: #212529;
    padding: 5px 10px;
    font-size: 0.9em;
}

.edit-group:hover:not([disabled]) {
    background-color: #e0a800;
}

.delete-group {
    background-color: #dc3545;
    color: white;
    padding: 5px 10px;
    font-size: 0.9em;
}

.delete-group:hover:not([disabled]) {
    background-color: #c82333;
}

/* Toggle group button */
.toggle-group {
    background-color: #6c757d;
    color: white;
    padding: 5px 10px;
    font-size: 0.9em;
}

.toggle-group[data-enabled="true"] {
    background-color: #28a745;
}

.toggle-group[data-enabled="false"] {
    background-color: #dc3545;
}

.toggle-group:hover:not([disabled]) {
    opacity: 0.9;
}

/* Status badge for enabled/disabled groups */
.status-badge {
    display: inline-block;
    padding: 3px 8px;
    border-radius: 10px;
    font-size: 0.8em;
    font-weight: bold;
    margin-bottom: 8px;
}

.status-badge.enabled {
    background-color: #d4edda;
    color: #155724;
    border: 1px solid #c3e6cb;
}

.status-badge.disabled {
    background-color: #f8d7da;
    color: #721c24;
    border: 1px solid #f5c6cb;
}

table { 
    width: 100%; 
    border-collapse: collapse; 
    margin-bottom: 15px;
}

th, td { 
    text-align: left; 
    padding: 8px; 
    border-bottom: 1px solid #ddd; 
}

th {
    background-color: #f8f9fa;
}

pre { 
    background-color: #f8f9fa; 
    padding: 10px; 
    border-radius: 5px; 
    overflow: auto; 
    border: 1px solid #eee;
    font-family: monospace;
    font-size: 14px;
}

.form-group {
    margin-bottom: 15px;
}

.form-group label {
    display: block;
    margin-bottom: 5px;
    font-weight: bold;
}

.form-group input {
    width: 100%;
    padding: 8px;
    border: 1px solid #ddd;
    border-radius: 4px;
    box-sizing: border-box;
}

.checkbox-container {
    display: flex;
    align-items: center;
    margin-bottom: 10px;
}

.checkbox-container input[type="checkbox"] {
    width: auto;
    margin-right: 10px;
}

.form-group small {
    display: block;
    color: #6c757d;
    font-size: 0.85em;
    margin-top: 4px;
}

.button-group {
    margin: 15px 0;
    display: flex;
    gap: 10px;
}

/* 转发组样式 */
.forwarding-group {
    border: 1px solid #ddd;
    border-radius: 5px;
    margin-bottom: 15px;
    overflow: hidden;
}

.group-header {
    background-color: #f8f9fa;
    padding: 10px 15px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    border-bottom: 1px solid #ddd;
}

.group-header h3 {
    margin: 0;
}

.group-actions {
    display: flex;
    gap: 5px;
}

.group-details {
    padding: 10px 15px;
}

.detail-item {
    margin-bottom: 5px;
}

.header-with-button {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
}

.header-with-button h2 {
    margin: 0;
}

/* 组统计卡片 */
#groupStats {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
    gap: 15px;
}

.group-stat-card {
    border: 1px solid #ddd;
    border-radius: 5px;
    padding: 15px;
}

/* 模态对话框 */
.modal {
    display: none;
    position: fixed;
    z-index: 1000;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    overflow: auto;
    background-color: rgba(0,0,0,0.4);
}

.modal-content {
    background-color: #fefefe;
    margin: 10% auto;
    padding: 20px;
    border: 1px solid #888;
    border-radius: 5px;
    width: 80%;
    max-width: 600px;
}

.close {
    color: #aaa;
    float: right;
    font-size: 28px;
    font-weight: bold;
    cursor: pointer;
}

.close:hover,
.close:focus {
    color: black;
    text-decoration: none;
}

@media (max-width: 700px) {
    #groupStats {
        grid-template-columns: 1fr;
    }
    
    .button-group {
        flex-direction: column;
    }
    
    button {
        width: 100%;
        margin-right: 0;
        margin-bottom: 10px;
    }
}

/* 表格设置控件样式 */
.table-settings {
    margin: 10px 0;
    padding: 8px;
    background-color: #f8f9fa;
    border-radius: 5px;
    border: 1px solid #e9ecef;
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.table-settings label {
    margin-right: 5px;
    font-weight: bold;
}

.row-selector {
    padding: 5px;
    border: 1px solid #ced4da;
    border-radius: 4px;
    background-color: white;
}

.pagination-controls {
    display: flex;
    align-items: center;
    gap: 10px;
}

.pagination-controls button {
    padding: 4px 10px;
    background-color: #6c757d;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    font-size: 0.9em;
}

.pagination-controls button:hover:not([disabled]) {
    background-color: #5a6268;
}

.pagination-controls button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

.page-info {
    font-size: 0.9em;
    color: #495057;
    min-width: 100px;
    text-align: center;
}

/* 表格工具栏 */
.table-toolbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 10px;
}

/* 搜索框 */
.table-search {
    padding: 5px 10px;
    border: 1px solid #ced4da;
    border-radius: 4px;
    width: 200px;
}

4) static\js\app.js

document.addEventListener('DOMContentLoaded', function() {
    // 初始化和启动自动刷新
    refreshStats();
    
    // 同时也刷新服务状态
    refreshServiceStatus();
    
    // 每5秒刷新一次统计信息和服务状态
    setInterval(function() {
        refreshStats();
        refreshServiceStatus();
    }, 5000);
    
    // 初始化模态框
    const modal = document.getElementById('groupModal');
    const closeBtn = modal.querySelector('.close');
    
    // 添加组按钮
    const addGroupBtn = document.getElementById('addGroupBtn');
    if (addGroupBtn) {
        addGroupBtn.addEventListener('click', function() {
            // 重置表单
            document.getElementById('groupForm').reset();
            document.getElementById('groupId').value = '';
            document.getElementById('modalTitle').textContent = 'Add Forwarding Group';
            
            // 默认启用新组
            document.getElementById('groupEnabled').checked = true;
            
            // 显示模态框
            modal.style.display = 'block';
        });
    }
    
    // 关闭模态框
    if (closeBtn) {
        closeBtn.addEventListener('click', function() {
            modal.style.display = 'none';
        });
    }
    
    // 点击模态框外部关闭
    window.addEventListener('click', function(event) {
        if (event.target === modal) {
            modal.style.display = 'none';
        }
    });
    
    // 组表单提交
    const groupForm = document.getElementById('groupForm');
    if (groupForm) {
        groupForm.addEventListener('submit', function(e) {
            e.preventDefault();
            
            // 收集表单数据
            const groupId = document.getElementById('groupId').value;
            const groupName = document.getElementById('groupName').value;
            const sourcesStr = document.getElementById('groupSources').value;
            const destinationsStr = document.getElementById('groupDestinations').value;
            const keywordsStr = document.getElementById('groupKeywords').value;
            const excludeKeywordsStr = document.getElementById('groupExcludeKeywords').value;
            const enabled = document.getElementById('groupEnabled').checked;
            
            // 解析列表
            const sources = sourcesStr.split(',').map(s => s.trim()).filter(s => s);
            const destinations = destinationsStr.split(',').map(d => d.trim()).filter(d => d);
            const keywords = keywordsStr.split(',').map(k => k.trim()).filter(k => k);
            const exclude_keywords = excludeKeywordsStr.split(',').map(k => k.trim()).filter(k => k);
            
            // 创建组数据对象
            const groupData = {
                name: groupName,
                sources: sources,
                destinations: destinations,
                keywords: keywords,
                exclude_keywords: exclude_keywords,
                enabled: enabled
            };
            
            // 确定是添加还是更新
            let url, method;
            if (groupId) {
                // 更新现有组
                url = '/update_group';
                groupData.id = groupId;
            } else {
                // 添加新组
                url = '/add_group';
            }
            
            // 发送请求
            fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(groupData)
            })
            .then(response => response.json())
            .then(data => {
                if (data.status === 'added' || data.status === 'updated') {
                    // 关闭模态框
                    modal.style.display = 'none';
                    
                    // 刷新组列表
                    refreshForwardingGroups(data.forwarding_groups);
                } else {
                    alert('Failed to save group: ' + (data.message || 'Unknown error'));
                }
            })
            .catch(error => {
                console.error('Error saving group:', error);
                alert('Failed to save group due to an error');
            });
        });
    }
    
    // 添加编辑组事件处理
    document.addEventListener('click', function(e) {
        if (e.target.classList.contains('edit-group')) {
            const groupElement = e.target.closest('.forwarding-group');
            const groupId = groupElement.dataset.groupId;
            
            // 获取组数据
            fetch('/check_status')
                .then(response => response.json())
                .then(data => {
                    const group = data.forwarding_groups.find(g => g.id === groupId);
                    
                    if (group) {
                        // 填充表单
                        document.getElementById('groupId').value = group.id;
                        document.getElementById('groupName').value = group.name;
                        document.getElementById('groupSources').value = group.sources.join(', ');
                        document.getElementById('groupDestinations').value = group.destinations.join(', ');
                        document.getElementById('groupKeywords').value = group.keywords.join(', ');
                        document.getElementById('groupExcludeKeywords').value = group.exclude_keywords ? group.exclude_keywords.join(', ') : '';
                        document.getElementById('groupEnabled').checked = group.hasOwnProperty('enabled') ? group.enabled : true;
                        
                        // 更新模态框标题
                        document.getElementById('modalTitle').textContent = 'Edit Forwarding Group';
                        
                        // 显示模态框
                        modal.style.display = 'block';
                    }
                })
                .catch(error => {
                    console.error('Error fetching group data:', error);
                    alert('Failed to load group data');
                });
        }
    });
    
    // 添加删除组事件处理
    document.addEventListener('click', function(e) {
        if (e.target.classList.contains('delete-group')) {
            if (!confirm('Are you sure you want to delete this forwarding group?')) {
                return;
            }
            
            const groupElement = e.target.closest('.forwarding-group');
            const groupId = groupElement.dataset.groupId;
            
            // 发送删除请求
            fetch('/delete_group', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ id: groupId })
            })
            .then(response => response.json())
            .then(data => {
                if (data.status === 'deleted') {
                    // 刷新组列表
                    refreshForwardingGroups(data.forwarding_groups);
                } else {
                    alert('Failed to delete group: ' + (data.message || 'Unknown error'));
                }
            })
            .catch(error => {
                console.error('Error deleting group:', error);
                alert('Failed to delete group due to an error');
            });
        }
    });
    
    // 添加组启用/禁用切换处理
    document.addEventListener('click', function(e) {
        if (e.target.classList.contains('toggle-group')) {
            const groupElement = e.target.closest('.forwarding-group');
            const groupId = groupElement.dataset.groupId;
            const currentlyEnabled = e.target.getAttribute('data-enabled') === 'true';
            const newState = !currentlyEnabled;
            
            // 发送切换请求
            fetch('/toggle_group', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ 
                    id: groupId,
                    enabled: newState
                })
            })
            .then(response => response.json())
            .then(data => {
                if (data.status === 'updated') {
                    // 刷新组列表
                    refreshForwardingGroups(data.forwarding_groups);
                } else {
                    alert('Failed to update group status: ' + (data.message || 'Unknown error'));
                }
            })
            .catch(error => {
                console.error('Error updating group status:', error);
                alert('Failed to update group status due to an error');
            });
        }
    });
    
    // 启动转发器
    const startBtn = document.getElementById('startForwarderBtn');
    if (startBtn) {
        startBtn.addEventListener('click', function() {
            // 获取当前配置
            fetch('/check_status')
                .then(response => response.json())
                .then(data => {
                    // 发送启动请求
                    fetch('/start', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            forwarding_groups: data.forwarding_groups,
                            reset_stats: false
                        })
                    })
                    .then(response => response.json())
                    .then(startData => {
                        if (startData.status === 'started') {
                            refreshServiceStatus();
                            refreshStats();
                        } else {
                            alert('Failed to start forwarder: ' + (startData.message || 'Unknown error'));
                        }
                    })
                    .catch(error => {
                        console.error('Error starting forwarder:', error);
                        alert('Failed to start forwarder due to an error');
                    });
                })
                .catch(error => {
                    console.error('Error fetching current config:', error);
                    alert('Failed to fetch current configuration');
                });
        });
    }
    
    // 停止转发器
    const stopBtn = document.getElementById('stopForwarderBtn');
    if (stopBtn) {
        stopBtn.addEventListener('click', function() {
            fetch('/stop', {
                method: 'POST'
            })
            .then(response => response.json())
            .then(data => {
                if (data.status === 'stopped') {
                    refreshServiceStatus();
                } else {
                    alert('Failed to stop forwarder: ' + (data.message || 'Unknown error'));
                }
            })
            .catch(error => {
                console.error('Error stopping forwarder:', error);
                alert('Failed to stop forwarder due to an error');
            });
        });
    }
    
    // 保存配置
    const saveConfigBtn = document.getElementById('saveConfigBtn');
    if (saveConfigBtn) {
        saveConfigBtn.addEventListener('click', function() {
            // 获取当前配置
            fetch('/check_status')
                .then(response => response.json())
                .then(data => {
                    // 发送保存请求
                    fetch('/save_config', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({
                            forwarding_groups: data.forwarding_groups
                        })
                    })
                    .then(response => response.json())
                    .then(saveData => {
                        if (saveData.status === 'saved') {
                            alert('Configuration saved successfully');
                        } else {
                            alert('Failed to save configuration: ' + (saveData.message || 'Unknown error'));
                        }
                    })
                    .catch(error => {
                        console.error('Error saving configuration:', error);
                        alert('Failed to save configuration due to an error');
                    });
                })
                .catch(error => {
                    console.error('Error fetching current config:', error);
                    alert('Failed to fetch current configuration');
                });
        });
    }
    
    // 添加表格设置控件
    setTimeout(createTableSettings, 500); // 延迟一点以确保其他元素已加载
});

/**
 * 刷新服务状态
 */
function refreshServiceStatus() {
    fetch('/check_status')
        .then(response => response.json())
        .then(data => {
            // 获取状态元素
            const statusElement = document.querySelector('.status');
            
            if (data.is_running) {
                // 更新为运行状态
                statusElement.classList.remove('stopped');
                statusElement.classList.add('running');
                statusElement.innerHTML = 'Status: <strong>Running</strong>';
                
                // 更新按钮状态
                document.querySelectorAll('button.start').forEach(btn => btn.disabled = true);
                document.querySelectorAll('button.stop').forEach(btn => btn.disabled = false);
                document.querySelectorAll('button.add, button.edit-group, button.delete-group, button.toggle-group').forEach(btn => btn.disabled = true);
            } else {
                // 更新为停止状态
                statusElement.classList.remove('running');
                statusElement.classList.add('stopped');
                statusElement.innerHTML = 'Status: <strong>Stopped</strong>';
                
                // 更新按钮状态
                document.querySelectorAll('button.start').forEach(btn => btn.disabled = false);
                document.querySelectorAll('button.stop').forEach(btn => btn.disabled = true);
                document.querySelectorAll('button.add, button.edit-group, button.delete-group, button.toggle-group').forEach(btn => btn.disabled = false);
            }
            
            // 刷新转发组显示
            refreshForwardingGroups(data.forwarding_groups);
        })
        .catch(error => {
            console.error('Error fetching service status:', error);
        });
}

/**
 * 刷新转发组列表
 */
function refreshForwardingGroups(groups) {
    const container = document.getElementById('forwardingGroups');
    if (!container) return;
    
    // 清空容器
    container.innerHTML = '';
    
    // 添加每个组
    groups.forEach(group => {
        const isEnabled = group.hasOwnProperty('enabled') ? group.enabled : true;
        const groupElement = document.createElement('div');
        groupElement.className = 'forwarding-group';
        groupElement.dataset.groupId = group.id;
        
        // 获取排除关键词
        const excludeKeywords = group.exclude_keywords || [];
        
        groupElement.innerHTML = `
            <div class="group-header">
                <h3>${group.name}</h3>
                <div class="group-actions">
                    <button class="toggle-group" data-enabled="${isEnabled}" ${document.querySelector('.status').classList.contains('running') ? 'disabled' : ''}>
                        ${isEnabled ? 'Enabled' : 'Disabled'}
                    </button>
                    <button class="edit-group" ${document.querySelector('.status').classList.contains('running') ? 'disabled' : ''}>Edit</button>
                    <button class="delete-group" ${document.querySelector('.status').classList.contains('running') ? 'disabled' : ''}>Delete</button>
                </div>
            </div>
            <div class="group-details">
                <div class="detail-item">
                    <span class="status-badge ${isEnabled ? 'enabled' : 'disabled'}">
                        ${isEnabled ? 'Enabled' : 'Disabled'}
                    </span>
                </div>
                <div class="detail-item">
                    <strong>Sources:</strong> ${group.sources.join(', ')}
                </div>
                <div class="detail-item">
                    <strong>Destinations:</strong> ${group.destinations.join(', ')}
                </div>
                <div class="detail-item">
                    <strong>Keywords:</strong> ${group.keywords.join(', ')}
                </div>
                <div class="detail-item">
                    <strong>Exclude Keywords:</strong> ${excludeKeywords.join(', ') || 'None'}
                </div>
            </div>
        `;
        
        container.appendChild(groupElement);
    });
}

/**
 * 获取并更新统计数据
 */
function refreshStats() {
    fetch('/stats')
        .then(response => response.json())
        .then(data => {
            updateStatistics(data);
        })
        .catch(error => {
            console.error('Error fetching statistics:', error);
        });
}

/**
 * 更新UI显示的统计数据
 */
function updateStatistics(data) {
    // 更新基本计数器
    document.getElementById('totalMessages').textContent = data.total_messages_seen;
    document.getElementById('forwardedMessages').textContent = data.messages_forwarded;
    
    // 更新关键词统计
    const keywordStats = document.getElementById('keywordStats');
    keywordStats.innerHTML = '';
    
    // 按匹配次数降序排列关键词
    const sortedKeywords = Object.entries(data.keyword_matches || {})
        .sort((a, b) => b[1] - a[1]);
    
    for (const [keyword, count] of sortedKeywords) {
        const row = document.createElement('tr');
        row.innerHTML = `<td>${keyword}</td><td>${count}</td>`;
        keywordStats.appendChild(row);
    }
    
    // 更新组统计信息
    const groupStats = document.getElementById('groupStats');
    if (groupStats) {
        groupStats.innerHTML = '';
        
        // 将组统计数据转换为数组
        const groupsData = Object.entries(data.group_stats || {});
        
        if (groupsData.length === 0) {
            groupStats.innerHTML = '<p>No group statistics available</p>';
        } else {
            // 遍历每个组
            for (const [groupId, group] of groupsData) {
                const groupElement = document.createElement('div');
                groupElement.className = 'group-stat-card';
                
                // 计算转发率
                const forwardRate = group.messages_seen > 0 
                    ? ((group.messages_forwarded / group.messages_seen) * 100).toFixed(1) 
                    : 0;
                
                let keywordsHtml = '';
                if (group.keyword_matches && Object.keys(group.keyword_matches).length > 0) {
                    // 按匹配次数排序关键词
                    const sortedGroupKeywords = Object.entries(group.keyword_matches)
                        .sort((a, b) => b[1] - a[1]);
                    
                    keywordsHtml = `
                        <h4>Keyword Matches</h4>
                        <table>
                            <thead>
                                <tr>
                                    <th>Keyword</th>
                                    <th>Matches</th>
                                </tr>
                            </thead>
                            <tbody>
                                ${sortedGroupKeywords.map(([kw, count]) => 
                                    `<tr><td>${kw}</td><td>${count}</td></tr>`
                                ).join('')}
                            </tbody>
                        </table>
                    `;
                }
                
                // 添加排除关键词统计
                let exclusionsHtml = '';
                if (group.keyword_exclusions && Object.keys(group.keyword_exclusions).length > 0) {
                    // 按匹配次数排序排除关键词
                    const sortedExcludeKeywords = Object.entries(group.keyword_exclusions)
                        .sort((a, b) => b[1] - a[1]);
                    
                    exclusionsHtml = `
                        <h4>Keyword Exclusions</h4>
                        <table>
                            <thead>
                                <tr>
                                    <th>Excluded Keyword</th>
                                    <th>Occurrences</th>
                                </tr>
                            </thead>
                            <tbody>
                                ${sortedExcludeKeywords.map(([kw, count]) => 
                                    `<tr><td>${kw}</td><td>${count}</td></tr>`
                                ).join('')}
                            </tbody>
                        </table>
                    `;
                }
                
                groupElement.innerHTML = `
                    <h3>${group.name}</h3>
                    <table>
                        <tr>
                            <td>Messages Seen:</td>
                            <td>${group.messages_seen}</td>
                        </tr>
                        <tr>
                            <td>Messages Forwarded:</td>
                            <td>${group.messages_forwarded}</td>
                        </tr>
                        <tr>
                            <td>Forward Rate:</td>
                            <td>${forwardRate}%</td>
                        </tr>
                    </table>
                    ${keywordsHtml}
                    ${exclusionsHtml}
                `;
                
                groupStats.appendChild(groupElement);
            }
        }
    }
    
    // 更新源统计信息
    const sourceStats = document.getElementById('sourceStats');
    if (sourceStats) {
        sourceStats.innerHTML = '';
        
        const sources = Object.entries(data.source_stats || {});
        if (sources.length === 0) {
            sourceStats.innerHTML = '<tr><td colspan="4">No source statistics available</td></tr>';
        } else {
            for (const [source, stats] of sources) {
                const row = document.createElement('tr');
                const forwardRate = stats.messages_seen > 0 
                    ? ((stats.messages_forwarded / stats.messages_seen) * 100).toFixed(1) + '%' 
                    : '0%';
                
                row.innerHTML = `
                    <td>${source}</td>
                    <td>${stats.messages_seen}</td>
                    <td>${stats.messages_forwarded}</td>
                    <td>${forwardRate}</td>
                `;
                sourceStats.appendChild(row);
            }
        }
    }
    
    // 更新目标统计信息
    const destinationStats = document.getElementById('destinationStats');
    if (destinationStats) {
        destinationStats.innerHTML = '';
        
        const destinations = Object.entries(data.destination_stats || {});
        if (destinations.length === 0) {
            destinationStats.innerHTML = '<tr><td colspan="2">No destination statistics available</td></tr>';
        } else {
            for (const [destination, stats] of destinations) {
                const row = document.createElement('tr');
                row.innerHTML = `
                    <td>${destination}</td>
                    <td>${stats.messages_received}</td>
                `;
                destinationStats.appendChild(row);
            }
        }
    }
    
    // 更新最近转发消息信息
    const lastMessage = document.getElementById('lastMessage');
    if (lastMessage) {
        if (data.last_forwarded) {
            lastMessage.textContent = JSON.stringify(data.last_forwarded, null, 2);
        } else {
            lastMessage.textContent = 'No messages forwarded yet';
        }
    }
    
    // 更新表格分页
    updateTableDisplay('keywordStats');
    updateTableDisplay('sourceStats');
    updateTableDisplay('destinationStats');
}

// 表格设置
const tableSettings = {
  keywordStats: {
    maxRows: 10,
    currentPage: 1
  },
  sourceStats: {
    maxRows: 10,
    currentPage: 1
  },
  destinationStats: {
    maxRows: 10, 
    currentPage: 1
  }
};

// 创建表格设置控制面板
function createTableSettings() {
  // 获取所有统计卡片
  const cards = document.querySelectorAll('.card');
  
  // 为每个卡片添加设置控件
  cards.forEach(card => {
    // 获取卡片标题
    const title = card.querySelector('h2');
    if (!title) return;
    
    // 检查是否是我们要添加设置的卡片
    const titleText = title.textContent.trim();
    let tableId;
    
    if (titleText === 'Global Statistics') {
      tableId = 'keywordStats';
    } else if (titleText === 'Source Statistics') {
      tableId = 'sourceStats';
    } else if (titleText === 'Destination Statistics') {
      tableId = 'destinationStats';
    } else {
      return; // 不处理其他卡片
    }
    
    // 创建设置容器
    const settingsContainer = document.createElement('div');
    settingsContainer.className = 'table-settings';
    
    // 添加行数选择器
    const rowSelector = document.createElement('div');
    rowSelector.innerHTML = `
      <label for="${tableId}-rows">每页显示行数:</label>
      <select id="${tableId}-rows" class="row-selector">
        <option value="5">5</option>
        <option value="10" selected>10</option>
        <option value="15">15</option>
        <option value="20">20</option>
        <option value="50">50</option>
        <option value="100">100</option>
        <option value="1000">全部</option>
      </select>
    `;
    
    // 添加分页控件
    const paginationControls = document.createElement('div');
    paginationControls.className = 'pagination-controls';
    paginationControls.innerHTML = `
      <button class="page-prev" data-table="${tableId}">上一页</button>
      <span class="page-info" id="${tableId}-page-info">第1页</span>
      <button class="page-next" data-table="${tableId}">下一页</button>
    `;
    
    // 添加控件到容器
    settingsContainer.appendChild(rowSelector);
    settingsContainer.appendChild(paginationControls);
    
    // 插入设置容器
    const targetTable = card.querySelector(`#${tableId}`);
    if (targetTable) {
      targetTable.parentNode.insertBefore(settingsContainer, targetTable);
      
      // 设置选择器事件
      const selector = settingsContainer.querySelector(`#${tableId}-rows`);
      selector.addEventListener('change', function() {
        tableSettings[tableId].maxRows = parseInt(this.value);
        tableSettings[tableId].currentPage = 1; // 重置到第一页
        updateTableDisplay(tableId);
      });
      
      // 分页按钮事件
      const prevBtn = settingsContainer.querySelector('.page-prev');
      const nextBtn = settingsContainer.querySelector('.page-next');
      
      prevBtn.addEventListener('click', function() {
        if (tableSettings[tableId].currentPage > 1) {
          tableSettings[tableId].currentPage--;
          updateTableDisplay(tableId);
        }
      });
      
      nextBtn.addEventListener('click', function() {
        const table = document.getElementById(tableId);
        const totalRows = table.querySelectorAll('tr').length;
        const totalPages = Math.ceil(totalRows / tableSettings[tableId].maxRows);
        
        if (tableSettings[tableId].currentPage < totalPages) {
          tableSettings[tableId].currentPage++;
          updateTableDisplay(tableId);
        }
      });
    }
  });
}

// 更新表格显示
function updateTableDisplay(tableId) {
  const table = document.getElementById(tableId);
  if (!table) return;
  
  const settings = tableSettings[tableId];
  const rows = table.querySelectorAll('tr');
  const totalRows = rows.length;
  const totalPages = Math.max(1, Math.ceil(totalRows / settings.maxRows));
  
  // 约束页码在有效范围内
  if (settings.currentPage > totalPages) {
    settings.currentPage = totalPages;
  }
  
  // 计算要显示的行范围
  const startIndex = (settings.currentPage - 1) * settings.maxRows;
  const endIndex = Math.min(startIndex + settings.maxRows, totalRows);
  
  // 更新页码信息
  const pageInfo = document.getElementById(`${tableId}-page-info`);
  if (pageInfo) {
    pageInfo.textContent = `第${settings.currentPage}页 / 共${totalPages}页`;
  }
  
  // 更新行的可见性
  rows.forEach((row, index) => {
    if (index >= startIndex && index < endIndex) {
      row.style.display = '';
    } else {
      row.style.display = 'none';
    }
  });
  
  // 更新分页按钮状态
  const prevBtn = document.querySelector(`.page-prev[data-table="${tableId}"]`);
  const nextBtn = document.querySelector(`.page-next[data-table="${tableId}"]`);
  
  if (prevBtn) {
    prevBtn.disabled = settings.currentPage === 1;
  }
  
  if (nextBtn) {
    nextBtn.disabled = settings.currentPage === totalPages;
  }
}

5) .env  文件第一个字符是点

# Telegram API credentials (get from https://my.telegram.org)
API_ID=666666666
API_HASH=999999999999999999

使用自己的 API KEY

5. Docker 部署

1) Dockerfiles

FROM python:3.11-slim

WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    python3-dev \
    && rm -rf /var/lib/apt/lists/*

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Create directory structure
RUN mkdir -p /app/static/css /app/static/js /app/templates /app/sessions

# Copy application files
COPY app.py .
COPY templates/ ./templates/
COPY static/ ./static/
COPY .env .

# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV SESSION_NAME=/app/sessions/message_forwarder

# Expose the port
EXPOSE 9018

# Run the application
CMD ["python", "app.py"]

如果不在 DOCKER 运行,python 最好使用 3.11.x ,不然在安装 PyPi 会有错误 (telethon aiohttp)可以解决,但无法批处理运行。

2)requirements.txt

telethon==1.28.5
flask==2.3.3
Werkzeug==2.3.7
python-dotenv==1.0.0
aiohttp==3.8.5
async-timeout==4.0.3
nest-asyncio==1.5.8
pytz==2023.3

3) 部署命令:

将占用服务器端口 9018

6. 第一次必须 运行验证 (Telegram认证问题)

文件 .env 虽然有 api key 但第一次使用必须做:Telegram认证问题

错误描述:“telethon.errors.rpcerrorlist.AuthKeyUnregisteredError: The key is not registered in the system (caused by GetUsersRequest)”

我的办法是:使用单独的 auth.py 来验证身份

auth.py 代码:(如果不是在 Container 中,要在Python 环境中运行)
from telethon import TelegramClient
import asyncio

# 使用与主应用程序相同的API凭据和会话名称
api_id = '666666'
api_hash = '999999999999999999999'
session_name = 'message_forwarder'

async def main():
    # 连接并认证
    client = TelegramClient(session_name, api_id, api_hash)
    await client.start()
    
    # 确认登录成功
    me = await client.get_me()
    print(f"认证成功!已登录为 {me.first_name}")
    print("您可以关闭此窗口并启动主应用程序了")
    
    # 等待一段时间再断开连接(允许用户看到结果)
    await asyncio.sleep(10)
    await client.disconnect()

# 运行认证
asyncio.run(main())

因为是字符串要使用引号把 api_id  api_hash 的值包住

在 Container 中运行:

python auth.py

按提示,输入手机号后, 在 TG 中找到:login code

输入后,如果成功会返回:

终端程序,从容器中退出,即可使用。

总结:

希望能看到

上一个绿豆芽项目:从左到右 第二批,第三批,第四批已经泡2天。 拍照很烦恼,前天刚把 PicTool < Project-37 PicsAdjTool> 照片白平衡批处理调整 可以批量裁剪工具 Ver.0.3 -- Last updated on 15Mar.2025 添加剪切全部应用功能-优快云博客补好,慢慢来。

豆芽生长现状,这个学来的方法,种豆芽,真方便,就是取出很难。

内容概要:本文介绍了MATLAB实现DBN-RBF深度置信网络结合RBF神经网络多输入单输出回归预测的详细项目实例。项目旨在通过深度置信网络(DBN)和径向基函数神经网络(RBF)的结合,设计出一种高效的回归预测模型,以应对高维数据和非线性关系的挑战。DBN用于无监督特征提取,RBF用于快速回归,两者结合显著提升了预测精度和模型泛化能力。文中详细描述了项目的背景、目标、挑战、解决方案、模型架构、代码实现、GUI设计、性能评估及未来改进方向。 适合人群:具备一定编程基础,对机器学习和深度学习有一定了解的研发人员,尤其是从事金融预测、医疗健康、智能制造等领域的工程师和技术人员。 使用场景及目标:①解决高维数据的特征提取难题,提升非线性回归的拟合精度;②通过无监督学习快速训练能力的结合,提高模型的预测精度和泛化能力;③应用于金融预测、医疗健康、智能制造等多个领域,提供高效的回归预测工具;④通过实时数据流处理和GPU加速推理,确保系统在实时应用中的快速响应。 其他说明:此项目不仅提供了详细的理论分析和代码实现,还涵盖了系统架构设计、模型部署应用、安全性用户隐私保护等方面的全面指导。通过结合其他深度学习模型、多任务学习、增量学习等技术,项目具备广阔的扩展性和应用前景。系统还支持自动化CI/CD管道、API服务业务集成、前端展示结果导出等功能,确保了系统的高可用性和易用性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值