-感谢您的支持!
+Current version: v1.7.2
+Changelog: /changelog
+
+Thank you for your support!
"""
\ No newline at end of file
From bd10e92f362e198b85c63b53703018e26c1784a9 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:04:26 +0000
Subject: [PATCH 02/76] Translate Chinese to English in .env.example
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
.env.example | 103 +++++++++++++++++++++++++--------------------------
1 file changed, 51 insertions(+), 52 deletions(-)
diff --git a/.env.example b/.env.example
index fa1fef5..617bfba 100644
--- a/.env.example
+++ b/.env.example
@@ -1,141 +1,140 @@
-######### 必填项 #########
-# Telegram API 配置 (从 https://my.telegram.org/apps 获取)
+######### Required #########
+# Telegram API configuration (obtain from https://my.telegram.org/apps)
API_ID=
API_HASH=
-# 用户账号登录用的手机号 (格式如: +8613812345678)
+# Phone number for user account login (format: +8613812345678)
PHONE_NUMBER=
# Bot Token
BOT_TOKEN=
-# 用户ID (从 @userinfobot 获取)
+# User ID (obtain from @userinfobot)
USER_ID=
-################ 以下均为可选项 ##################
+################ All below are optional ##################
-# 管理员列表(此处填user_id,留空默认上方的USER_ID,多个用户用逗号分隔)
+# Admin list (enter user_id here, leave empty to default to USER_ID above, separate multiple users with commas)
ADMINS=
-# bot消息删除时间 (秒),0表示立即删除, -1表示不删除
+# Bot message deletion timeout (seconds), 0 means delete immediately, -1 means do not delete
BOT_MESSAGE_DELETE_TIMEOUT=300
-# 是否自动删除用户发送的指令消息 (true/false)
+# Whether to automatically delete command messages sent by users (true/false)
USER_MESSAGE_DELETE_ENABLE=false
-# 默认最大媒体文件大小限制(单位:MB)
+# Default maximum media file size limit (unit: MB)
DEFAULT_MAX_MEDIA_SIZE=15
-# 默认时区
+# Default timezone
DEFAULT_TIMEZONE=Asia/Shanghai
-# 自动更新数据库中聊天窗口名字时间 (24小时制)
+# Time to auto-update chat names in the database (24-hour format)
CHAT_UPDATE_TIME=03:00
-# 数据库配置
+# Database configuration
DATABASE_URL=sqlite:///./db/forward.db
-######### UI 布局配置 #########
+######### UI Layout Configuration #########
AI_MODELS_PER_PAGE=10
KEYWORDS_PER_PAGE=10
PUSH_CHANNEL_PER_PAGE=10
-# 总结列表(行)
-SUMMARY_TIME_ROWS=10
-# 总结列表(列)
+# Summary list (rows)
+SUMMARY_TIME_ROWS=10
+# Summary list (columns)
SUMMARY_TIME_COLS=6
-# 延迟时间列表(行)
+# Delay time list (rows)
DELAY_TIME_ROWS=10
-# 延迟时间列表(列)
+# Delay time list (columns)
DELAY_TIME_COLS=6
-# 媒体大小列表(行)
+# Media size list (rows)
MEDIA_SIZE_ROWS=10
-# 媒体大小列表(列)
+# Media size list (columns)
MEDIA_SIZE_COLS=6
-# 媒体扩展名列表(行)
+# Media extension list (rows)
MEDIA_EXTENSIONS_ROWS=10
-# 媒体扩展名列表(列)
+# Media extension list (columns)
MEDIA_EXTENSIONS_COLS=6
-# 每页显示的规则数量
+# Number of rules displayed per page
RULES_PER_PAGE=20
- ######### AI设置 #########
+ ######### AI Settings #########
-# 默认AI模型
+# Default AI model
DEFAULT_AI_MODEL=gemini-2.0-flash
# OpenAi API Key
OPENAI_API_KEY=your_openai_api_key
-# 留空使用官方接口 https://api.openai.com/v1
-OPENAI_API_BASE=
+# Leave empty to use the official API https://api.openai.com/v1
+OPENAI_API_BASE=
# Claude API Key
CLAUDE_API_KEY=your_claude_api_key
-# 留空使用官方接口
+# Leave empty to use the official API
CLAUDE_API_BASE=
# Gemini API Key
-# 默认使用官方接口
+# Uses the official API by default
GEMINI_API_KEY=your_gemini_api_key
-# 兼容OpenAI接口标准的第三方API Base,如官方的:https://generativelanguage.googleapis.com/v1beta
+# Third-party API Base compatible with OpenAI API standard, e.g. the official one: https://generativelanguage.googleapis.com/v1beta
GEMINI_API_BASE=
# DeepSeek API Key
DEEPSEEK_API_KEY=your_deepseek_api_key
-# 留空使用官方接口 https://api.deepseek.com/v1
-DEEPSEEK_API_BASE=
+# Leave empty to use the official API https://api.deepseek.com/v1
+DEEPSEEK_API_BASE=
# Qwen API Key
QWEN_API_KEY=your_qwen_api_key
-# 留空使用官方接口 https://dashscope.aliyuncs.com/compatible-mode/v1
-QWEN_API_BASE=
+# Leave empty to use the official API https://dashscope.aliyuncs.com/compatible-mode/v1
+QWEN_API_BASE=
# Grok API Key
GROK_API_KEY=your_grok_api_key
-# 留空使用官方接口 https://api.x.ai/v1
-GROK_API_BASE=
+# Leave empty to use the official API https://api.x.ai/v1
+GROK_API_BASE=
-# 默认AI提示词
-DEFAULT_AI_PROMPT=请尊重原意,保持原有格式不变,用简体中文重写下面的内容:
+# Default AI prompt
+DEFAULT_AI_PROMPT=Please respect the original meaning, keep the original format unchanged, and rewrite the following content in Simplified Chinese:
-# 默认AI总结提示词
-DEFAULT_SUMMARY_PROMPT=请总结以下频道/群组24小时内的消息。
-# 默认总结时间 (24小时制)
+# Default AI summary prompt
+DEFAULT_SUMMARY_PROMPT=Please summarize the messages from the following channel/group within the past 24 hours.
+# Default summary time (24-hour format)
DEFAULT_SUMMARY_TIME=07:00
-# AI总结每次爬取消息数量
+# Number of messages fetched per AI summary batch
SUMMARY_BATCH_SIZE=20
-# AI总结每次爬取消息间隔时间(秒)
+# Interval between AI summary message fetches (seconds)
SUMMARY_BATCH_DELAY=2
-######### RSS配置 #########
-# 是否启用RSS功能 (true/false)
+######### RSS Configuration #########
+# Whether to enable RSS functionality (true/false)
RSS_ENABLED=false
-# RSS基础访问URL
+# RSS base access URL
RSS_BASE_URL=
-# RSS媒体文件基础URL
+# RSS media file base URL
RSS_MEDIA_BASE_URL=
-######### 扩展内容 #########
+######### Extensions #########
-# 是否开启与通用论坛屏蔽插件服务端的同步服务 (true/false)
+# Whether to enable sync service with universal forum blocker plugin server (true/false)
UFB_ENABLED=false
-# 服务端地址
+# Server address
UFB_SERVER_URL=
-# 用户API_KEY
+# User API_KEY
UFB_TOKEN=
-
From 0e7aad512ef22910e5cbcc21011ef82df4977b34 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:04:51 +0000
Subject: [PATCH 03/76] Translate Chinese to English in .gitignore
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
.gitignore | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/.gitignore b/.gitignore
index 5b728e1..f620a7e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
-# 环境变量文件
+# Environment variable files
.env
-# 数据库文件
+# Database files
*.db
# Python
@@ -9,14 +9,14 @@ __pycache__/
*.py[cod]
*$py.class
-# 虚拟环境
+# Virtual environments
venv/
env/
ENV/
-# Telethon session 文件
+# Telethon session files
*.session
-*.session-journal
+*.session-journal
/.idea
/example
/config
From 0f1d4e30b3ac5e6ce3c1581f154e199e1e059f52 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:04:58 +0000
Subject: [PATCH 04/76] Translate Chinese to English in .dockerignore
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
.dockerignore | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/.dockerignore b/.dockerignore
index 4bc78bf..f091260 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,49 +1,49 @@
-# 忽略环境变量文件
+# Ignore environment variable files
.env
.env.example
-# 忽略数据库文件
+# Ignore database files
*.db
db/forward.db1
-# 忽略 Python 生成的缓存文件
+# Ignore Python generated cache files
**/__pycache__/
*.py[cod]
*$py.class
-# 忽略虚拟环境
+# Ignore virtual environments
**/venv/
**/env/
**/ENV/
-# 忽略 Telethon 会话文件
+# Ignore Telethon session files
*.session
-*.session-journal
+*.session-journal
-# 忽略 IDE 配置文件
+# Ignore IDE configuration files
.idea/
ufb/.idea
-# 忽略示例和临时配置文件
+# Ignore example and temporary configuration files
/example
/config/*
ufb/config/*
-# 忽略无用的图片和测试目录
+# Ignore unused image and test directories
**/test/
**/images/
-# 忽略 RSS 相关数据和临时文件
+# Ignore RSS related data and temporary files
/rss/media/*
/rss/data/*
-# 忽略日志文件
+# Ignore log files
logs/*
-# 忽略临时文件夹
+# Ignore temporary folders
/temp/*
-# 额外忽略 `.git` 和 Docker 相关文件,防止意外复制
+# Additionally ignore .git and Docker related files to prevent accidental copying
.git
.gitignore
.dockerignore
From f51090a24d60158c6760d264894ee90ceeb534dd Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:05:07 +0000
Subject: [PATCH 05/76] Translate Chinese to English in Dockerfile
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
Dockerfile | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 977292f..052273a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,13 +1,13 @@
FROM python:3.11-slim
-# 设置工作目录
+# Set working directory
WORKDIR /app
-# 设置Docker日志配置
+# Set Docker log configuration
ENV DOCKER_LOG_MAX_SIZE=10m
ENV DOCKER_LOG_MAX_FILE=3
-# 安装系统依赖
+# Install system dependencies
RUN apt-get update && apt-get install -y \
tzdata \
&& ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
@@ -17,18 +17,18 @@ RUN apt-get update && apt-get install -y \
python3-dev \
&& rm -rf /var/lib/apt/lists/*
-# 复制依赖文件并安装
+# Copy dependency files and install
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
-# 创建临时文件目录
+# Create temporary file directory
RUN mkdir -p /app/temp
-# 复制应用代码
+# Copy application code
COPY . .
-# 设置环境变量
+# Set environment variables
ENV PYTHONUNBUFFERED=1
-# 启动命令
+# Start command
CMD ["python", "main.py"]
\ No newline at end of file
From 2702a2dd9ef234eeba808f32aca15bc74d6e10cd Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:05:15 +0000
Subject: [PATCH 06/76] Translate Chinese to English in docker-compose.yml
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
docker-compose.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 758803a..55b543a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,7 +2,7 @@ services:
telegram-forwarder:
image: heavrnl/telegramforwarder:latest
container_name: telegram-forwarder
- # 如果需要使用 RSS 功能,请取消以下注释
+ # If you need to use RSS functionality, uncomment the following
# ports:
# - 9804:8000
restart: unless-stopped
From 36999aa2cbd0eba60f5c769f75f9e477bea9f373 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:06:11 +0000
Subject: [PATCH 07/76] Translate Chinese to English in main.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
main.py | 178 ++++++++++++++++++++++++++++----------------------------
1 file changed, 89 insertions(+), 89 deletions(-)
diff --git a/main.py b/main.py
index b3bf10f..806f240 100644
--- a/main.py
+++ b/main.py
@@ -16,25 +16,25 @@
from rss.main import app as rss_app
from utils.log_config import setup_logging
-# 设置Docker日志的默认配置,如果docker-compose.yml中没有配置日志选项将使用这些值
+# Set default Docker log configuration; these values will be used if not configured in docker-compose.yml
os.environ.setdefault('DOCKER_LOG_MAX_SIZE', '10m')
os.environ.setdefault('DOCKER_LOG_MAX_FILE', '3')
-# 设置日志配置
+# Set up logging configuration
setup_logging()
logger = logging.getLogger(__name__)
-# 加载环境变量
+# Load environment variables
load_dotenv()
-# 从环境变量获取配置
+# Get configuration from environment variables
api_id = os.getenv('API_ID')
api_hash = os.getenv('API_HASH')
bot_token = os.getenv('BOT_TOKEN')
phone_number = os.getenv('PHONE_NUMBER')
-# 创建 DBOperations 实例
+# Create DBOperations instance
db_ops = None
scheduler = None
@@ -42,34 +42,34 @@
async def init_db_ops():
- """初始化 DBOperations 实例"""
+ """Initialize DBOperations instance"""
global db_ops
if db_ops is None:
db_ops = await DBOperations.create()
return db_ops
-# 创建文件夹
+# Create directories
os.makedirs('./sessions', exist_ok=True)
os.makedirs('./temp', exist_ok=True)
-# 清空./temp文件夹
+# Clear ./temp directory
def clear_temp_dir():
for file in os.listdir('./temp'):
os.remove(os.path.join('./temp', file))
-# 创建客户端
+# Create clients
user_client = TelegramClient('./sessions/user', api_id, api_hash)
bot_client = TelegramClient('./sessions/bot', api_id, api_hash)
-# 初始化数据库
+# Initialize database
engine = init_db()
def run_rss_server(host: str, port: int):
- """在新进程中运行 RSS 服务器"""
+ """Run RSS server in a new process"""
uvicorn.run(
rss_app,
host=host,
@@ -78,271 +78,271 @@ def run_rss_server(host: str, port: int):
async def start_clients():
- # 初始化 DBOperations
+ # Initialize DBOperations
global db_ops, scheduler, chat_updater
db_ops = await DBOperations.create()
try:
- # 启动用户客户端
+ # Start user client
await user_client.start(phone=phone_number)
me_user = await user_client.get_me()
- print(f'用户客户端已启动: {me_user.first_name} (@{me_user.username})')
+ print(f'User client started: {me_user.first_name} (@{me_user.username})')
- # 启动机器人客户端
+ # Start bot client
await bot_client.start(bot_token=bot_token)
me_bot = await bot_client.get_me()
- print(f'机器人客户端已启动: {me_bot.first_name} (@{me_bot.username})')
+ print(f'Bot client started: {me_bot.first_name} (@{me_bot.username})')
- # 设置消息监听器
+ # Set up message listeners
await setup_listeners(user_client, bot_client)
- # 注册命令
+ # Register commands
await register_bot_commands(bot_client)
- # 创建并启动调度器
+ # Create and start scheduler
scheduler = SummaryScheduler(user_client, bot_client)
await scheduler.start()
-
- # 创建并启动聊天信息更新器
+
+ # Create and start chat info updater
chat_updater = ChatUpdater(user_client)
await chat_updater.start()
- # 如果启用了 RSS 服务
+ # If RSS service is enabled
if os.getenv('RSS_ENABLED', '').lower() == 'true':
try:
rss_host = os.getenv('RSS_HOST', '0.0.0.0')
rss_port = int(os.getenv('RSS_PORT', '8000'))
- logger.info(f"正在启动 RSS 服务 (host={rss_host}, port={rss_port})")
-
- # 在新进程中启动 RSS 服务
+ logger.info(f"Starting RSS service (host={rss_host}, port={rss_port})")
+
+ # Start RSS service in a new process
rss_process = multiprocessing.Process(
target=run_rss_server,
args=(rss_host, rss_port)
)
rss_process.start()
- logger.info("RSS 服务启动成功")
+ logger.info("RSS service started successfully")
except Exception as e:
- logger.error(f"启动 RSS 服务失败: {str(e)}")
+ logger.error(f"Failed to start RSS service: {str(e)}")
logger.exception(e)
else:
- logger.info("RSS 服务未启用")
+ logger.info("RSS service is not enabled")
- # 发送欢迎消息
+ # Send welcome message
await send_welcome_message(bot_client)
- # 等待两个客户端都断开连接
+ # Wait for both clients to disconnect
await asyncio.gather(
user_client.run_until_disconnected(),
bot_client.run_until_disconnected()
)
finally:
- # 关闭 DBOperations
+ # Close DBOperations
if db_ops and hasattr(db_ops, 'close'):
await db_ops.close()
- # 停止调度器
+ # Stop scheduler
if scheduler:
scheduler.stop()
- # 停止聊天信息更新器
+ # Stop chat info updater
if chat_updater:
chat_updater.stop()
- # 如果 RSS 服务在运行,停止它
+ # If RSS service is running, stop it
if 'rss_process' in locals() and rss_process.is_alive():
rss_process.terminate()
rss_process.join()
async def register_bot_commands(bot):
- """注册机器人命令"""
- # # 先清空现有命令
+ """Register bot commands"""
+ # # Clear existing commands first
# try:
# await bot(SetBotCommandsRequest(
# scope=types.BotCommandScopeDefault(),
# lang_code='',
- # commands=[] # 空列表清空所有命令
+ # commands=[] # Empty list to clear all commands
# ))
- # logger.info('已清空现有机器人命令')
+ # logger.info('Existing bot commands cleared')
# except Exception as e:
- # logger.error(f'清空机器人命令时出错: {str(e)}')
+ # logger.error(f'Error clearing bot commands: {str(e)}')
commands = [
- # 基础命令
+ # Basic commands
BotCommand(
command='start',
- description='开始使用'
+ description='Get started'
),
BotCommand(
command='help',
- description='查看帮助'
+ description='View help'
),
- # 绑定和设置
+ # Binding and settings
BotCommand(
command='bind',
- description='绑定源聊天'
+ description='Bind source chat'
),
BotCommand(
command='settings',
- description='管理转发规则'
+ description='Manage forwarding rules'
),
BotCommand(
command='switch',
- description='切换当前需要设置的聊天规则'
+ description='Switch the chat rule currently being configured'
),
- # 关键字管理
+ # Keyword management
BotCommand(
command='add',
- description='添加关键字'
+ description='Add keyword'
),
BotCommand(
command='add_regex',
- description='添加正则关键字'
+ description='Add regex keyword'
),
BotCommand(
command='add_all',
- description='添加普通关键字到所有规则'
+ description='Add plain keyword to all rules'
),
BotCommand(
command='add_regex_all',
- description='添加正则表达式到所有规则'
+ description='Add regex to all rules'
),
BotCommand(
command='list_keyword',
- description='列出所有关键字'
+ description='List all keywords'
),
BotCommand(
command='remove_keyword',
- description='删除关键字'
+ description='Remove keyword'
),
BotCommand(
command='remove_keyword_by_id',
- description='按ID删除关键字'
+ description='Remove keyword by ID'
),
BotCommand(
command='remove_all_keyword',
- description='删除当前频道绑定的所有规则的指定关键字'
+ description='Remove specified keyword from all rules bound to current channel'
),
- # 替换规则管理
+ # Replace rule management
BotCommand(
command='replace',
- description='添加替换规则'
+ description='Add replace rule'
),
BotCommand(
command='replace_all',
- description='添加替换规则到所有规则'
+ description='Add replace rule to all rules'
),
BotCommand(
command='list_replace',
- description='列出所有替换规则'
+ description='List all replace rules'
),
BotCommand(
command='remove_replace',
- description='删除替换规则'
+ description='Remove replace rule'
),
- # 导入导出功能
+ # Import/export functionality
BotCommand(
command='export_keyword',
- description='导出当前规则的关键字'
+ description='Export keywords of current rule'
),
BotCommand(
command='export_replace',
- description='导出当前规则的替换规则'
+ description='Export replace rules of current rule'
),
BotCommand(
command='import_keyword',
- description='导入普通关键字'
+ description='Import plain keywords'
),
BotCommand(
command='import_regex_keyword',
- description='导入正则表达式关键字'
+ description='Import regex keywords'
),
BotCommand(
command='import_replace',
- description='导入替换规则'
+ description='Import replace rules'
),
- # UFB相关功能
+ # UFB related features
BotCommand(
command='ufb_bind',
- description='绑定ufb域名'
+ description='Bind UFB domain'
),
BotCommand(
command='ufb_unbind',
- description='解绑ufb域名'
+ description='Unbind UFB domain'
),
BotCommand(
command='ufb_item_change',
- description='切换ufb同步配置类型'
+ description='Switch UFB sync configuration type'
),
BotCommand(
command='clear_all_keywords',
- description='清除当前规则的所有关键字'
+ description='Clear all keywords of current rule'
),
BotCommand(
command='clear_all_keywords_regex',
- description='清除当前规则的所有正则关键字'
+ description='Clear all regex keywords of current rule'
),
BotCommand(
command='clear_all_replace',
- description='清除当前规则的所有替换规则'
+ description='Clear all replace rules of current rule'
),
BotCommand(
command='copy_keywords',
- description='复制参数规则的关键字到当前规则'
+ description='Copy keywords from specified rule to current rule'
),
BotCommand(
command='copy_keywords_regex',
- description='复制参数规则的正则关键字到当前规则'
+ description='Copy regex keywords from specified rule to current rule'
),
BotCommand(
command='copy_replace',
- description='复制参数规则的替换规则到当前规则'
+ description='Copy replace rules from specified rule to current rule'
),
BotCommand(
command='copy_rule',
- description='复制参数规则到当前规则'
+ description='Copy specified rule to current rule'
),
BotCommand(
command='changelog',
- description='查看更新日志'
+ description='View changelog'
),
BotCommand(
command='list_rule',
- description='列出所有转发规则'
+ description='List all forwarding rules'
),
BotCommand(
command='delete_rule',
- description='删除转发规则'
+ description='Delete forwarding rule'
),
BotCommand(
command='delete_rss_user',
- description='删除RSS用户'
+ description='Delete RSS user'
),
# BotCommand(
# command='clear_all',
- # description='慎用!清空所有数据'
+ # description='Use with caution! Clear all data'
# ),
]
try:
result = await bot(SetBotCommandsRequest(
scope=types.BotCommandScopeDefault(),
- lang_code='', # 空字符串表示默认语言
+ lang_code='', # Empty string means default language
commands=commands
))
if result:
- logger.info('已成功注册机器人命令')
+ logger.info('Bot commands registered successfully')
else:
- logger.error('注册机器人命令失败')
+ logger.error('Failed to register bot commands')
except Exception as e:
- logger.error(f'注册机器人命令时出错: {str(e)}')
+ logger.error(f'Error registering bot commands: {str(e)}')
if __name__ == '__main__':
- # 运行事件循环
+ # Run event loop
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(start_clients())
except KeyboardInterrupt:
- print("正在关闭客户端...")
+ print("Shutting down clients...")
finally:
- loop.close()
\ No newline at end of file
+ loop.close()
From b588a6fd7b3629d998ecb359df428cce23496e2f Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:06:27 +0000
Subject: [PATCH 08/76] Translate Chinese to English in enums/enums.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
enums/enums.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/enums/enums.py b/enums/enums.py
index 2dffe48..36040e8 100644
--- a/enums/enums.py
+++ b/enums/enums.py
@@ -1,6 +1,6 @@
import enum
-# 四个模式,仅黑名单,仅白名单,先黑名单后白名单,先白名单后黑名单
+# Four modes: blacklist only, whitelist only, blacklist then whitelist, whitelist then blacklist
class ForwardMode(enum.Enum):
WHITELIST = 'whitelist'
BLACKLIST = 'blacklist'
@@ -11,7 +11,7 @@ class ForwardMode(enum.Enum):
class PreviewMode(enum.Enum):
ON = 'on'
OFF = 'off'
- FOLLOW = 'follow' # 跟随原消息的预览设置
+ FOLLOW = 'follow' # Follow the original message's preview setting
class MessageMode(enum.Enum):
MARKDOWN = 'Markdown'
From 440977a6e971bd9fce5211981f3052775f5b579d Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:07:15 +0000
Subject: [PATCH 09/76] Translate Chinese to English in message_listener.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
message_listener.py | 189 ++++++++++++++++++++++----------------------
1 file changed, 94 insertions(+), 95 deletions(-)
diff --git a/message_listener.py b/message_listener.py
index 5115a1f..c7f834b 100644
--- a/message_listener.py
+++ b/message_listener.py
@@ -10,201 +10,200 @@
from managers.state_manager import state_manager
from telethon.tl import types
from filters.process import process_forward_rule
-# 加载环境变量
+# Load environment variables
load_dotenv()
-# 获取logger
+# Get logger
logger = logging.getLogger(__name__)
-# 添加一个缓存来存储已处理的媒体组
+# Add a cache to store processed media groups
PROCESSED_GROUPS = set()
BOT_ID = None
async def setup_listeners(user_client, bot_client):
"""
- 设置消息监听器
-
+ Set up message listeners
+
Args:
- user_client: 用户客户端(用于监听消息和转发)
- bot_client: 机器人客户端(用于处理命令和转发)
+ user_client: User client (for listening to messages and forwarding)
+ bot_client: Bot client (for handling commands and forwarding)
"""
global BOT_ID
-
- # 直接获取机器人ID
+
+ # Directly get bot ID
try:
me = await bot_client.get_me()
BOT_ID = me.id
- logger.info(f"获取到机器人ID: {BOT_ID} (类型: {type(BOT_ID)})")
+ logger.info(f"Got bot ID: {BOT_ID} (type: {type(BOT_ID)})")
except Exception as e:
- logger.error(f"获取机器人ID时出错: {str(e)}")
-
- # 过滤器,排除机器人自己的消息
+ logger.error(f"Error getting bot ID: {str(e)}")
+
+ # Filter to exclude bot's own messages
async def not_from_bot(event):
if BOT_ID is None:
- return True # 如果未获取到机器人ID,不进行过滤
-
+ return True # If bot ID not obtained, do not filter
+
sender = event.sender_id
try:
sender_id = int(sender) if sender is not None else None
is_not_bot = sender_id != BOT_ID
if not is_not_bot:
- logger.info(f"过滤器识别到机器人消息,忽略处理: {sender_id}")
+ logger.info(f"Filter detected bot message, skipping processing: {sender_id}")
return is_not_bot
except (ValueError, TypeError):
- return True # 转换失败时不过滤
-
- # 用户客户端监听器 - 使用过滤器,避免处理机器人消息
+ return True # Do not filter on conversion failure
+
+ # User client listener - use filter to avoid processing bot messages
@user_client.on(events.NewMessage(func=not_from_bot))
async def user_message_handler(event):
await handle_user_message(event, user_client, bot_client)
-
- # 机器人客户端监听器 - 使用过滤器
+
+ # Bot client listener - use filter
@bot_client.on(events.NewMessage(func=not_from_bot))
async def bot_message_handler(event):
- # logger.info(f"机器人收到非自身消息, 发送者ID: {event.sender_id}")
+ # logger.info(f"Bot received non-self message, sender ID: {event.sender_id}")
await handle_bot_message(event, bot_client)
-
- # 注册机器人回调处理器
+
+ # Register bot callback handler
bot_client.add_event_handler(bot_handler.callback_handler)
async def handle_user_message(event, user_client, bot_client):
- """处理用户客户端收到的消息"""
- # logger.info("handle_user_message:开始处理用户消息")
-
+ """Handle messages received by user client"""
+ # logger.info("handle_user_message: Starting to process user message")
+
chat = await event.get_chat()
chat_id = abs(chat.id)
- # logger.info(f"handle_user_message:获取到聊天ID: {chat_id}")
+ # logger.info(f"handle_user_message: Got chat ID: {chat_id}")
- # 检查是否频道消息
+ # Check if it's a channel message
if isinstance(event.chat, types.Channel) and state_manager.check_state():
- # logger.info("handle_user_message:检测到频道消息且存在状态")
+ # logger.info("handle_user_message: Detected channel message with existing state")
sender_id = os.getenv('USER_ID')
- # 频道ID需要加上100前缀
+ # Channel ID needs 100 prefix
chat_id = int(f"100{chat_id}")
- # logger.info(f"handle_user_message:频道消息处理: sender_id={sender_id}, chat_id={chat_id}")
+ # logger.info(f"handle_user_message: Channel message processing: sender_id={sender_id}, chat_id={chat_id}")
else:
sender_id = event.sender_id
- # logger.info(f"handle_user_message:非频道消息处理: sender_id={sender_id}")
+ # logger.info(f"handle_user_message: Non-channel message processing: sender_id={sender_id}")
- # 检查用户状态
+ # Check user state
current_state, message, state_type = state_manager.get_state(sender_id, chat_id)
- # logger.info(f'handle_user_message:当前是否有状态: {state_manager.check_state()}')
- # logger.info(f"handle_user_message:当前用户ID和聊天ID: {sender_id}, {chat_id}")
- # logger.info(f"handle_user_message:获取当前聊天窗口的用户状态: {current_state}")
-
+ # logger.info(f'handle_user_message: Current state exists: {state_manager.check_state()}')
+ # logger.info(f"handle_user_message: Current user ID and chat ID: {sender_id}, {chat_id}")
+ # logger.info(f"handle_user_message: Got current chat window user state: {current_state}")
+
if current_state:
- # logger.info(f"检测到用户状态: {current_state}")
- # 处理提示词设置
- # logger.info("准备处理提示词设置")
+ # logger.info(f"Detected user state: {current_state}")
+ # Handle prompt setting
+ # logger.info("Preparing to handle prompt setting")
if await handle_prompt_setting(event, bot_client, sender_id, chat_id, current_state, message):
- # logger.info("提示词设置处理完成,返回")
+ # logger.info("Prompt setting processing completed, returning")
return
- # logger.info("提示词设置处理未完成,继续执行")
+ # logger.info("Prompt setting processing not completed, continuing execution")
- # 检查是否是媒体组消息
+ # Check if it's a media group message
if event.message.grouped_id:
- # 如果这个媒体组已经处理过,就跳过
+ # If this media group has already been processed, skip it
group_key = f"{chat_id}:{event.message.grouped_id}"
if group_key in PROCESSED_GROUPS:
return
- # 标记这个媒体组为已处理
+ # Mark this media group as processed
PROCESSED_GROUPS.add(group_key)
asyncio.create_task(clear_group_cache(group_key))
-
- # 首先检查数据库中是否有该聊天的转发规则
+
+ # First check if there are forwarding rules for this chat in the database
session = get_session()
try:
- # 查询源聊天
+ # Query source chat
source_chat = session.query(Chat).filter(
Chat.telegram_chat_id == str(chat_id)
).first()
-
+
if not source_chat:
return
-
- # 添加日志:查询转发规则
- logger.info(f'找到源聊天: {source_chat.name} (ID: {source_chat.id})')
-
- # 查找以当前聊天为源的规则
+
+ # Add log: query forwarding rules
+ logger.info(f'Found source chat: {source_chat.name} (ID: {source_chat.id})')
+
+ # Find rules where current chat is the source
rules = session.query(ForwardRule).filter(
ForwardRule.source_chat_id == source_chat.id
).all()
-
+
if not rules:
- logger.info(f'聊天 {source_chat.name} 没有转发规则')
+ logger.info(f'Chat {source_chat.name} has no forwarding rules')
return
-
- # 有转发规则时,才记录消息信息
+
+ # Only log message info when there are forwarding rules
if event.message.grouped_id:
- logger.info(f'[用户] 收到媒体组消息 来自聊天: {source_chat.name} ({chat_id}) 组ID: {event.message.grouped_id}')
+ logger.info(f'[User] Received media group message from chat: {source_chat.name} ({chat_id}) Group ID: {event.message.grouped_id}')
else:
- logger.info(f'[用户] 收到新消息 来自聊天: {source_chat.name} ({chat_id}) 内容: {event.message.text}')
-
- # 添加日志:处理规则
- logger.info(f'找到 {len(rules)} 条转发规则')
-
- # 处理每条转发规则
+ logger.info(f'[User] Received new message from chat: {source_chat.name} ({chat_id}) Content: {event.message.text}')
+
+ # Add log: process rules
+ logger.info(f'Found {len(rules)} forwarding rules')
+
+ # Process each forwarding rule
for rule in rules:
target_chat = rule.target_chat
if not rule.enable_rule:
- logger.info(f'规则 {rule.id} 未启用')
+ logger.info(f'Rule {rule.id} is not enabled')
continue
- logger.info(f'处理转发规则 ID: {rule.id} (从 {source_chat.name} 转发到: {target_chat.name})')
+ logger.info(f'Processing forwarding rule ID: {rule.id} (from {source_chat.name} to: {target_chat.name})')
if rule.use_bot:
- # 直接使用过滤器模块中的process_forward_rule函数
+ # Directly use process_forward_rule function from filter module
await process_forward_rule(bot_client, event, str(chat_id), rule)
else:
await user_handler.process_forward_rule(user_client, event, str(chat_id), rule)
-
+
except Exception as e:
- logger.error(f'处理用户消息时发生错误: {str(e)}')
- logger.exception(e) # 添加详细的错误堆栈
+ logger.error(f'Error processing user message: {str(e)}')
+ logger.exception(e) # Add detailed error stack trace
finally:
session.close()
async def handle_bot_message(event, bot_client):
- """处理机器人客户端收到的消息(命令)"""
+ """Handle messages (commands) received by bot client"""
try:
-
- # logger.info("handle_bot_message:开始处理机器人消息")
-
+
+ # logger.info("handle_bot_message: Starting to process bot message")
+
chat = await event.get_chat()
chat_id = abs(chat.id)
- # logger.info(f"handle_bot_message:获取到聊天ID: {chat_id}")
+ # logger.info(f"handle_bot_message: Got chat ID: {chat_id}")
- # 检查是否频道消息
+ # Check if it's a channel message
if isinstance(event.chat, types.Channel) and state_manager.check_state():
- # logger.info("handle_bot_message:检测到频道消息且存在状态")
+ # logger.info("handle_bot_message: Detected channel message with existing state")
sender_id = os.getenv('USER_ID')
- # 频道ID需要加上100前缀
+ # Channel ID needs 100 prefix
chat_id = int(f"100{chat_id}")
- # logger.info(f"handle_bot_message:频道消息处理: sender_id={sender_id}, chat_id={chat_id}")
+ # logger.info(f"handle_bot_message: Channel message processing: sender_id={sender_id}, chat_id={chat_id}")
else:
sender_id = event.sender_id
- # logger.info(f"handle_bot_message:非频道消息处理: sender_id={sender_id}")
+ # logger.info(f"handle_bot_message: Non-channel message processing: sender_id={sender_id}")
- # 检查用户状态
+ # Check user state
current_state, message, state_type = state_manager.get_state(sender_id, chat_id)
- # logger.info(f'handle_bot_message:当前是否有状态: {state_manager.check_state()}')
- # logger.info(f"handle_bot_message:当前用户ID和聊天ID: {sender_id}, {chat_id}")
- # logger.info(f"handle_bot_message:获取当前聊天窗口的用户状态: {current_state}")
+ # logger.info(f'handle_bot_message: Current state exists: {state_manager.check_state()}')
+ # logger.info(f"handle_bot_message: Current user ID and chat ID: {sender_id}, {chat_id}")
+ # logger.info(f"handle_bot_message: Got current chat window user state: {current_state}")
+
-
-
- # 处理提示词设置
+
+ # Handle prompt setting
if current_state:
await handle_prompt_setting(event, bot_client, sender_id, chat_id, current_state, message)
return
- # 如果没有特殊状态,则处理常规命令
+ # If no special state, handle regular commands
await bot_handler.handle_command(bot_client, event)
except Exception as e:
- logger.error(f'处理机器人命令时发生错误: {str(e)}')
+ logger.error(f'Error processing bot command: {str(e)}')
logger.exception(e)
-async def clear_group_cache(group_key, delay=300): # 5分钟后清除缓存
- """清除已处理的媒体组记录"""
+async def clear_group_cache(group_key, delay=300): # Clear cache after 5 minutes
+ """Clear processed media group records"""
await asyncio.sleep(delay)
- PROCESSED_GROUPS.discard(group_key)
-
+ PROCESSED_GROUPS.discard(group_key)
From 8c697ec98862113a1b379f7e45d3ca6b84ef2179 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:10:03 +0000
Subject: [PATCH 10/76] Translate Chinese to English in README.md
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
README.md | 686 +++++++++++++++++++++++++++---------------------------
1 file changed, 343 insertions(+), 343 deletions(-)
diff --git a/README.md b/README.md
index 962dbf8..01228d6 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-
Telegram 转发器 | Telegram Forwarder
+
Telegram Forwarder
---
@@ -12,109 +12,109 @@
-## 📖 简介
-Telegram 转发器是一个强大的消息转发工具,只需要你的账号加入频道/群聊即可以将指定聊天中的消息转发到其他聊天,不需要bot进入对应的频道/群组即可监听。可用于信息流整合过滤,消息提醒,内容收藏等多种场景, 不受转发/复制禁止的限制。此外,利用 Apprise 强大的推送功能,你可以轻松将消息分发至聊天软件、邮件、短信、Webhooks、APIs 等各种平台。
-
-## ✨ 特性
-
-- 🔄 **多源转发**:支持从多个来源转发到指定目标
-- 🔍 **关键词过滤**:支持白名单和黑名单模式
-- 📝 **正则匹配**:支持正则表达式匹配目标文本
-- 📋 **内容修改**:支持多种方式修改消息内容
-- 🤖 **AI 处理**:支持使用各大厂商的AI接口
-- 📹 **媒体过滤**:支持过滤指定类型的媒体文件
-- 📰 **RSS订阅**:支持RSS订阅
-- 📢 **多平台推送**:支持通过Apprise推送到多个平台
-
-## 📋 目录
-
-- [📖 简介](#-简介)
-- [✨ 特性](#-特性)
-- [🚀 快速开始](#-快速开始)
- - [1️⃣ 准备工作](#1️⃣-准备工作)
- - [2️⃣ 配置环境](#2️⃣-配置环境)
- - [3️⃣ 启动服务](#3️⃣-启动服务)
- - [4️⃣ 更新](#4️⃣-更新)
-- [📚 使用指南](#-使用指南)
- - [🌟 基础使用示例](#-基础使用示例)
- - [🔧 特殊使用场景示例](#-特殊使用场景示例)
-- [🛠️ 功能详解](#️-功能详解)
- - [⚡ 过滤流程](#-过滤流程)
- - [⚙️ 设置说明](#️-设置说明)
- - [主设置说明](#主设置说明)
- - [媒体设置说明](#媒体设置说明)
- - [🤖 AI功能](#-ai功能)
- - [配置说明](#配置)
- - [自定义模型](#自定义模型)
- - [AI处理能力](#ai-处理)
- - [定时总结功能](#定时总结)
- - [📢 推送功能](#-推送功能)
- - [设置说明](#设置说明)
- - [📰 RSS订阅](#-RSS订阅)
- - [启用RSS功能](#启用rss功能)
- - [访问RSS仪表盘](#访问rss仪表盘)
- - [Nginx配置](#nginx配置)
- - [RSS配置说明](#rss配置管理)
- - [特殊设置项](#特殊设置项)
- - [注意事项](#注意事项)
-
-- [🎯 特殊功能](#-特殊功能)
- - [🔗 链接转发功能](#-链接转发功能)
-- [📝 命令列表](#-命令列表)
-- [💐 致谢](#-致谢)
-- [☕ 捐赠](#-捐赠)
-- [📄 开源协议](#-开源协议)
-
-
-
-## 🚀 快速开始
-
-### 1️⃣ 准备工作
-
-1. 获取 Telegram API 凭据:
- - 访问 https://my.telegram.org/apps
- - 创建一个应用获取 `API_ID` 和 `API_HASH`
-
-2. 获取机器人 Token:
- - 与 @BotFather 对话创建机器人
- - 获取机器人的 `BOT_TOKEN`
-
-3. 获取用户 ID:
- - 与 @userinfobot 对话获取你的 `USER_ID`
-
-### 2️⃣ 配置环境
-
-新建文件夹
+## 📖 Introduction
+Telegram Forwarder is a powerful message forwarding tool. As long as your account has joined a channel/group, it can forward messages from specified chats to other chats without requiring the bot to be in the corresponding channel/group for monitoring. It can be used for information stream aggregation and filtering, message notifications, content bookmarking, and many other scenarios, without being restricted by forwarding/copying limitations. Additionally, leveraging the powerful push capabilities of Apprise, you can easily distribute messages to chat apps, email, SMS, Webhooks, APIs, and various other platforms.
+
+## ✨ Features
+
+- 🔄 **Multi-source Forwarding**: Support forwarding from multiple sources to a specified target
+- 🔍 **Keyword Filtering**: Support whitelist and blacklist modes
+- 📝 **Regex Matching**: Support regular expression matching for target text
+- 📋 **Content Modification**: Support multiple ways to modify message content
+- 🤖 **AI Processing**: Support AI APIs from various major providers
+- 📹 **Media Filtering**: Support filtering specified types of media files
+- 📰 **RSS Subscription**: Support RSS subscription
+- 📢 **Multi-platform Push**: Support pushing to multiple platforms via Apprise
+
+## 📋 Table of Contents
+
+- [📖 Introduction](#-introduction)
+- [✨ Features](#-features)
+- [🚀 Quick Start](#-quick-start)
+ - [1️⃣ Prerequisites](#1️⃣-prerequisites)
+ - [2️⃣ Configure Environment](#2️⃣-configure-environment)
+ - [3️⃣ Start Service](#3️⃣-start-service)
+ - [4️⃣ Update](#4️⃣-update)
+- [📚 User Guide](#-user-guide)
+ - [🌟 Basic Usage Example](#-basic-usage-example)
+ - [🔧 Special Use Case Examples](#-special-use-case-examples)
+- [🛠️ Feature Details](#️-feature-details)
+ - [⚡ Filtering Process](#-filtering-process)
+ - [⚙️ Settings Description](#️-settings-description)
+ - [Main Settings Description](#main-settings-description)
+ - [Media Settings Description](#media-settings-description)
+ - [🤖 AI Features](#-ai-features)
+ - [Configuration](#configuration)
+ - [Custom Models](#custom-models)
+ - [AI Processing](#ai-processing)
+ - [Scheduled Summary](#scheduled-summary)
+ - [📢 Push Feature](#-push-feature)
+ - [Settings Description](#settings-description)
+ - [📰 RSS Subscription](#-rss-subscription)
+ - [Enable RSS Feature](#enable-rss-feature)
+ - [Access RSS Dashboard](#access-rss-dashboard)
+ - [Nginx Configuration](#nginx-configuration)
+ - [RSS Configuration Management](#rss-configuration-management)
+ - [Special Settings](#special-settings)
+ - [Notes](#notes)
+
+- [🎯 Special Features](#-special-features)
+ - [🔗 Link Forwarding Feature](#-link-forwarding-feature)
+- [📝 Command List](#-command-list)
+- [💐 Acknowledgments](#-acknowledgments)
+- [☕ Donate](#-donate)
+- [📄 License](#-license)
+
+
+
+## 🚀 Quick Start
+
+### 1️⃣ Prerequisites
+
+1. Obtain Telegram API credentials:
+ - Visit https://my.telegram.org/apps
+ - Create an application to get `API_ID` and `API_HASH`
+
+2. Get bot Token:
+ - Chat with @BotFather to create a bot
+ - Obtain the bot's `BOT_TOKEN`
+
+3. Get user ID:
+ - Chat with @userinfobot to get your `USER_ID`
+
+### 2️⃣ Configure Environment
+
+Create a new directory
```bash
mkdir ./TelegramForwarder && cd ./TelegramForwarder
```
-下载仓库的 [**docker-compose.yml**](https://github.com/Heavrnl/TelegramForwarder/blob/main/docker-compose.yml) 到目录下
+Download the repository's [**docker-compose.yml**](https://github.com/Heavrnl/TelegramForwarder/blob/main/docker-compose.yml) to the directory
-接着下载或复制仓库的 **[.env.example](./.env.example)** 文件,填入必填项,然后重命名为`.env`
+Then download or copy the repository's **[.env.example](./.env.example)** file, fill in the required fields, and rename it to `.env`
```bash
wget https://raw.githubusercontent.com/Heavrnl/TelegramForwarder/refs/heads/main/.env.example -O .env
```
-### 3️⃣ 启动服务
+### 3️⃣ Start Service
-首次运行(需要验证):
+First run (requires verification):
```bash
docker-compose run -it telegram-forwarder
```
-CTRL+C 退出容器
+CTRL+C to exit the container
-修改 docker-compose.yml 文件,修改 `stdin_open: false` 和 `tty: false`
+Modify the docker-compose.yml file, set `stdin_open: false` and `tty: false`
-后台运行:
+Run in background:
```bash
docker-compose up -d
```
-### 4️⃣ 更新
-注意:docker-compose运行不需要拉取仓库源码,除非你打算自己build,否则只需要在项目目录执行以下命令即可更新。
+### 4️⃣ Update
+Note: Running with docker-compose does not require pulling the repository source code. Unless you plan to build it yourself, you only need to execute the following commands in the project directory to update.
```bash
docker-compose down
```
@@ -124,289 +124,289 @@ docker-compose pull
```bash
docker-compose up -d
```
-## 📚 使用指南
+## 📚 User Guide
-### 🌟 基础使用示例
+### 🌟 Basic Usage Example
-假设订阅了频道 "TG 新闻" (https://t.me/tgnews) 和 "TG 阅读" (https://t.me/tgread) ,但想过滤掉一些不感兴趣的内容:
+Suppose you've subscribed to channels "TG News" (https://t.me/tgnews) and "TG Read" (https://t.me/tgread), but want to filter out some uninteresting content:
-1. 创建一个 Telegram 群组/频道(例如:"My TG Filter")
-2. 将机器人添加到群组/频道,并设置为管理员
-3. 在**新创建**的群组/频道中发送命令:
+1. Create a Telegram group/channel (e.g., "My TG Filter")
+2. Add the bot to the group/channel and set it as admin
+3. Send commands in the **newly created** group/channel:
```bash
- /bind https://t.me/tgnews 或者 /bind "TG 新闻"
- /bind https://t.me/tgread 或者 /bind "TG 阅读"
+ /bind https://t.me/tgnews or /bind "TG News"
+ /bind https://t.me/tgread or /bind "TG Read"
```
-4. 设置消息处理模式:
+4. Set message processing mode:
```bash
/settings
```
- 选择要操作的对应频道的规则,根据喜好设置
-
- 详细设置说明请查看 [🛠️ 功能详解](#️-功能详解)
+ Select the rule for the corresponding channel and configure according to your preferences
-5. 添加屏蔽关键词:
+ For detailed settings, see [🛠️ Feature Details](#️-feature-details)
+
+5. Add blocked keywords:
```bash
- /add 广告 推广 '这是 广告'
+ /add ad promotion 'this is an ad'
```
-6. 如果发现转发的消息格式有问题(比如有多余的符号),可以使用正则表达式处理:
+6. If you find formatting issues with forwarded messages (e.g., extra symbols), you can use regex to handle them:
```bash
/replace \*\*
```
- 这会删除消息中的所有 `**` 符号
+ This will remove all `**` symbols from messages
->注意:以上增删改查操作,只对第一个绑定的规则生效,示例里是TG 新闻。若想对TG 阅读进行操作,需要先使用`/settings(/s)`,选择TG 阅读,再点击"应用当前规则",就可以对此进行增删改查操作了。也可以使用`/add_all(/aa)`,`/replace_all(/ra)`等指令同时对两条规则生效
+>Note: The above add/remove/modify/query operations only apply to the first bound rule, which is TG News in this example. To operate on TG Read, you need to first use `/settings(/s)`, select TG Read, then click "Apply current rule" to perform add/remove/modify/query operations on it. You can also use `/add_all(/aa)`, `/replace_all(/ra)` and similar commands to apply to both rules simultaneously.
-这样,你就能收到经过过滤和格式化的频道消息了
+This way, you'll receive filtered and formatted channel messages.
-### 🔧 特殊使用场景示例
+### 🔧 Special Use Case Examples
-#### 1. TG 频道的部分消息由于文字嵌入链接,点击会让你确认再跳转,例如 NodeSeek 的官方通知频道
+#### 1. Some messages in TG channels have text embedded with links, clicking them requires confirmation before redirecting, e.g., NodeSeek's official notification channel
-频道的原始消息格式
+Original message format from the channel
```markdown
-[**贴子标题**](https://www.nodeseek.com/post-xxxx-1)
-```
-可以对通知频道的转发规则 **依次** 使用以下指令:
+[**Post Title**](https://www.nodeseek.com/post-xxxx-1)
+```
+You can use the following commands **sequentially** on the notification channel's forwarding rule:
```plaintext
/replace \*\*
/replace \[(?:\[([^\]]+)\])?([^\]]+)\]\(([^)]+)\) [\1]\2\n(\3)
/replace \[\]\s*
-```
-最终所有转发的消息都会变成以下格式,这样直接点击链接就无需确认跳转:
+```
+All forwarded messages will then become the following format, allowing direct link clicks without confirmation:
```plaintext
-贴子标题
+Post Title
(https://www.nodeseek.com/post-xxxx-1)
-```
+```
---
-#### 2. 监听用户消息格式不美观,可优化消息显示方式
+#### 2. Monitored user messages have unattractive formatting, can optimize message display
-**依次** 使用以下指令:
+Use the following commands **sequentially**:
```plaintext
/r ^(?=.)
/r (?<=.)(?=$)
-```
-然后设置消息格式为 **HTML**,这样监听用户消息时,消息格式就会美观很多:
+```
+Then set the message format to **HTML**, which will make monitored user messages look much better:
-
+
---
-#### 3. 同步规则操作
+#### 3. Sync rule operations
-在 **设置菜单** 中开启 **"同步规则"**,并选择 **目标规则**,当前规则的所有操作将同步到选定的规则。
+Enable **"Sync rules"** in the **settings menu** and select the **target rule**. All operations on the current rule will be synced to the selected rule.
-适用于以下场景:
-- 不想在当前窗口处理规则
-- 需要同时操作多个规则
+Applicable scenarios:
+- Don't want to manage rules in the current window
+- Need to operate on multiple rules simultaneously
-如果当前规则仅用于同步而不需实际生效,可将 **"是否启用规则"** 设置为 **"否"**。
+If the current rule is only for syncing and doesn't need to take effect, you can set **"Enable rule"** to **"No"**.
---
-#### 4. 如何转发到收藏夹 (Saved Messages)
-> 不推荐,操作比较繁琐
-1. 在你的 bot 管理的任意群组或频道中发送以下命令:
+#### 4. How to forward to Saved Messages
+> Not recommended, the process is quite tedious
+1. In any group or channel managed by your bot, send the following command:
```bash
- /bind https://t.me/tgnews 你的用户名(即展示的名称)
- ```
+ /bind https://t.me/tgnews Your Username (i.e., display name)
+ ```
-2. 随意新建一个规则,并进行以下设置:
- - **开启同步功能**,同步到 **转发收藏夹的规则**
- - **转发模式** 选择 **"用户模式"**
- - **禁用规则**(将规则”是否启用规则“设置为关闭)
+2. Create any new rule and configure the following:
+ - **Enable sync feature**, sync to the **forward-to-saved-messages rule**
+ - **Forwarding mode** select **"User mode"**
+ - **Disable rule** (set "Enable rule" to off)
-这样,你就可以在其他规则中管理收藏夹的规则,所有操作都会同步到 **转发收藏夹** 规则中。
+This way, you can manage the saved messages rule from other rules, and all operations will be synced to the **forward-to-saved-messages** rule.
-## 🛠️ 功能详解
+## 🛠️ Feature Details
-### ⚡ 过滤流程
-首先要清楚消息过滤顺序,括号里对应设置里的选项:
+### ⚡ Filtering Process
+First, understand the message filtering order (options in parentheses correspond to settings):

-### ⚙️ 设置说明
-| 主设置界面 | AI设置界面 | 媒体设置界面 |
+### ⚙️ Settings Description
+| Main Settings Interface | AI Settings Interface | Media Settings Interface |
|---------|------|------|
|  |  |  |
-#### 主设置说明
-以下对设置选项进行说明
-| 设置选项 | 说明 |
+#### Main Settings Description
+The following describes the settings options
+| Setting Option | Description |
|---------|------|
-| 应用当前规则 | 选择后,关键字指令(/add,/remove_keyword,/list_keyword等)和替换指令(/replace,/list_replace等)的增删改查导入导出将作用于当前规则 |
-| 是否启用规则 | 选择后,当前规则将被启用,否则将被禁用 |
-| 当前关键字添加模式 | 点击可切换黑/白名单模式,由于黑白名单是分开处理的,需要手动切换,注意,此时关键字的增删改查都和这里的模式有关,如果要使用指令对当前规则的白名单进行增删改查操作,请确保这里的模式是白名单 |
-| 过滤关键字时是否附带发送者名称和ID | 启用后,过滤关键字时会包含发送者名称和ID信息(不会添加到实际消息中),可用于针对特定用户进行过滤 |
-| 处理模式 | 可切换编辑/转发模式。编辑模式下会直接修改原消息;转发模式下会将处理后的消息转发到目标聊天。注意:编辑模式仅适用于你是管理员的且原消息是频道消息或群组中自己发送的消息 |
-| 过滤模式 | 可切换仅黑名单/仅白名单/先黑后白/先白后黑模式。由于黑白名单分开存储,可根据需要选择不同的过滤方式 |
-| 转发模式 | 可切换用户/机器人模式。用户模式下使用用户账号转发消息;机器人模式下使用机器人账号发送消息 |
-| 替换模式 | 启用后将根据已设置的替换规则对消息进行处理 |
-| 消息格式 | 可切换Markdown/HTML格式,在最终发送阶段生效,一般使用默认的Markdown即可 |
-| 预览模式 | 可切换开启/关闭/跟随原消息。开启后会预览消息中的第一个链接,默认跟随原消息的预览状态 |
-| 原始发送者/原始链接/发送时间 | 启用后会在消息发送时添加这些信息,默认关闭,可在"其他设置"菜单中设置自定义模板 |
-| 延时处理 | 启用后会按设定的延迟时间重新获取原消息内容,再开始处理流程,适用于频繁修改消息的频道/群组,可在 config/delay_time.txt 中添加自定义延迟时间 |
-| 删除原始消息 | 启用后会删除原消息,使用前请确认是否有删除权限 |
-| 评论区直达按钮 | 启用后在转发后的消息下发添加评论区直达按钮,前提是原消息有评论区 |
-| 同步到其他规则 | 启用后会同步当前规则的操作到其他规则,除了"是否启用规则"和"开启同步"其他设置都会同步 |
-
-#### 媒体设置说明
-| 设置选项 | 说明 |
+| Apply current rule | After selection, keyword commands (/add, /remove_keyword, /list_keyword, etc.) and replace commands (/replace, /list_replace, etc.) add/remove/modify/query/import/export operations will apply to the current rule |
+| Enable rule | After selection, the current rule will be enabled; otherwise it will be disabled |
+| Current keyword add mode | Click to switch between blacklist/whitelist mode. Since blacklist and whitelist are processed separately, you need to switch manually. Note: keyword add/remove/modify/query operations are related to this mode. To use commands for add/remove/modify/query on the current rule's whitelist, make sure the mode here is set to whitelist |
+| Include sender name and ID when filtering keywords | When enabled, keyword filtering will include sender name and ID information (not added to the actual message), which can be used to filter specific users |
+| Processing mode | Switch between edit/forward mode. In edit mode, the original message is modified directly; in forward mode, the processed message is forwarded to the target chat. Note: edit mode only works when you are an admin and the original message is a channel message or a message you sent in a group |
+| Filter mode | Switch between blacklist only/whitelist only/blacklist then whitelist/whitelist then blacklist modes. Since blacklist and whitelist are stored separately, choose different filtering methods as needed |
+| Forwarding mode | Switch between user/bot mode. In user mode, the user account forwards messages; in bot mode, the bot account sends messages |
+| Replace mode | When enabled, messages will be processed according to configured replace rules |
+| Message format | Switch between Markdown/HTML format, takes effect at the final sending stage. Generally use the default Markdown |
+| Preview mode | Switch between on/off/follow original message. When on, previews the first link in the message. Default follows the original message's preview state |
+| Original sender/Original link/Send time | When enabled, this information is added when sending messages. Default off, custom templates can be set in the "Other settings" menu |
+| Delayed processing | When enabled, the original message content is re-fetched after the set delay time before starting the processing flow. Suitable for channels/groups that frequently edit messages. Custom delay times can be added in config/delay_time.txt |
+| Delete original message | When enabled, the original message is deleted. Please confirm you have deletion permissions before use |
+| Comment section shortcut button | When enabled, a comment section shortcut button is added below the forwarded message, provided the original message has a comment section |
+| Sync to other rules | When enabled, operations on the current rule are synced to other rules. All settings are synced except "Enable rule" and "Enable sync" |
+
+#### Media Settings Description
+| Setting Option | Description |
|---------|------|
-| 媒体类型过滤 | 启用后会过滤掉非选中的媒体类型 |
-| 选择的媒体类型 | 选择要**屏蔽**的媒体类型,注意:Telegram对媒体文件的分类是固定的,主要就是这几种,图片 (photo),文档 (document),视频 (video),音频 (audio),语音 (voice),其中所有不属于图片、视频、音频、语音的文件都会被归类为"文档"类型。比如病毒文件(.exe)、压缩包(.zip)、文本文件(.txt)等,在 Telegram 中都属于"文档"类型。 |
-| 媒体大小过滤 | 启用后会过滤掉超过设置大小的媒体 |
-| 媒体大小限制 | 设置媒体大小限制,单位:MB,可在 config/media_size.txt 中添加自定义大小 |
-| 媒体大小超限时发送提醒 | 启用后媒体超限会发送提醒消息 |
-| 媒体扩展名过滤 | 启用后会过滤掉选中的媒体扩展名 |
-| 媒体扩展名过滤模式 | 切换黑/白名单模式 |
-| 选择的媒体扩展名 | 选择要过滤的的媒体扩展名,可在 config/media_extensions.txt 中添加自定义扩展名 |
-| 放行文本 | 开启后过滤媒体时不会屏蔽整条消息,而是单独转发文本 |
-
-#### 其他设置说明
-
-其他设置菜单中整合了常用的几个指令,使其可以在界面直接交互,包括:
-- 复制规则
-- 复制关键字
-- 复制替换规则
-- 清除关键字
-- 清除替换规则
-- 删除规则
-
-其中清除关键字、清除替换规则、删除规则可以对其他规则生效
-
-同时你可以在这里设置自定义模板,包括:用户信息模板、时间模板、原始链接模板
-| 设置选项 | 说明 |
+| Media type filter | When enabled, non-selected media types will be filtered out |
+| Selected media types | Select media types to **block**. Note: Telegram's classification of media files is fixed, mainly these types: photo, document, video, audio, voice. All files that don't belong to photo, video, audio, or voice categories are classified as "document" type. For example, executable files (.exe), archives (.zip), text files (.txt) are all classified as "document" type in Telegram |
+| Media size filter | When enabled, media exceeding the set size will be filtered out |
+| Media size limit | Set media size limit in MB. Custom sizes can be added in config/media_size.txt |
+| Send notification when media exceeds size limit | When enabled, a notification message is sent when media exceeds the limit |
+| Media extension filter | When enabled, selected media extensions will be filtered out |
+| Media extension filter mode | Switch between blacklist/whitelist mode |
+| Selected media extensions | Select media extensions to filter. Custom extensions can be added in config/media_extensions.txt |
+| Pass through text | When enabled, filtering media won't block the entire message; text will be forwarded separately |
+
+#### Other Settings Description
+
+The other settings menu integrates several commonly used commands for direct UI interaction, including:
+- Copy rule
+- Copy keywords
+- Copy replace rules
+- Clear keywords
+- Clear replace rules
+- Delete rule
+
+Clear keywords, clear replace rules, and delete rule can also be applied to other rules.
+
+You can also set custom templates here, including: user info template, time template, original link template
+| Setting Option | Description |
|---------|------|
-|反转黑名单| 启用后,将把黑名单当成白名单处理,若使用先白后黑模式,黑名单会作为第二重白名单处理|
-|反转白名单| 启用后,将把白名单当成黑名单处理,若使用先白后黑模式,白名单会作为第二重黑名单处理|
+| Invert blacklist | When enabled, the blacklist is treated as a whitelist. In whitelist-then-blacklist mode, the blacklist acts as a second-level whitelist |
+| Invert whitelist | When enabled, the whitelist is treated as a blacklist. In whitelist-then-blacklist mode, the whitelist acts as a second-level blacklist |
-结合“先 X 后 X”模式,可实现双层黑/白名单机制。例如,反转黑名单后,“先白后黑”中的黑名单将变为第二层级的白名单,适用于监听特定用户并筛选其特殊关键词等多种场景。
+Combined with "X then X" modes, a dual-layer blacklist/whitelist mechanism can be achieved. For example, after inverting the blacklist, the blacklist in "whitelist then blacklist" mode becomes a second-level whitelist, suitable for monitoring specific users and filtering their special keywords, among other scenarios.
-### 🤖 AI功能
+### 🤖 AI Features
-项目内置了各大厂商的AI接口,可以帮你:
-- 自动翻译外语内容
-- 定时总结群组消息
-- 智能过滤广告信息
-- 自动为内容打标签
+The project has built-in AI APIs from various major providers that can help you:
+- Automatically translate foreign language content
+- Scheduled group message summaries
+- Intelligently filter advertisements
+- Automatically tag content
....
-
-#### 配置
-1. 在 `.env` 文件中配置你的 AI 接口:
+#### Configuration
+
+1. Configure your AI API in the `.env` file:
```ini
# OpenAI API
OPENAI_API_KEY=your_key
-OPENAI_API_BASE= # 可选,默认官方接口
+OPENAI_API_BASE= # Optional, defaults to official API
# Claude API
CLAUDE_API_KEY=your_key
-# 其他支持的接口...
+# Other supported APIs...
```
-#### 自定义模型
+#### Custom Models
-没找到想要的模型名字?在 `config/ai_models.json` 中添加即可。
+Can't find the model name you want? Add it in `config/ai_models.json`.
-#### AI 处理
+#### AI Processing
-AI处理提示词中可以使用以下格式:
-- `{source_message_context:数字}` - 获取源聊天窗口最新的指定数量消息
-- `{target_message_context:数字}` - 获取目标聊天窗口最新的指定数量消息
-- `{source_message_time:数字}` - 获取源聊天窗口最近指定分钟数的消息
-- `{target_message_time:数字}` - 获取目标聊天窗口最近指定分钟数的消息
+The following formats can be used in AI processing prompts:
+- `{source_message_context:number}` - Get the latest specified number of messages from the source chat window
+- `{target_message_context:number}` - Get the latest specified number of messages from the target chat window
+- `{source_message_time:number}` - Get messages from the source chat window within the specified number of minutes
+- `{target_message_time:number}` - Get messages from the target chat window within the specified number of minutes
-提示词示例:
+Prompt example:
-前置:开启AI处理后再次执行关键词过滤,把“#不转发”添加到过滤关键字中
+Prerequisite: After enabling AI processing, perform keyword filtering again. Add "#donotforward" to the filter keywords.
```
-这是一个资讯整合频道,从多个源获取消息,现在你要判断新资讯是否和历史资讯内容重复了,若重复,则只需要回复“#不转发”,否则请返回新资讯的原文并保持格式。
-记住,你只能返回“#不转发”或者新资讯的原文。
-以下是历史资讯:{target_message_context:10}
-以下是新资讯:
+This is a news aggregation channel that collects messages from multiple sources. You need to determine whether the new article duplicates existing articles. If it's a duplicate, just reply "#donotforward". Otherwise, return the original text of the new article while preserving the format.
+Remember, you can only return "#donotforward" or the original text of the new article.
+Here are the historical articles: {target_message_context:10}
+Here is the new article:
```
-#### 定时总结
+#### Scheduled Summary
-开启定时总结后,机器人会在指定时间(默认每天早上 7 点)自动总结过去 24 小时的消息。
+After enabling scheduled summary, the bot will automatically summarize messages from the past 24 hours at the specified time (default: 7 AM daily).
-- 可在 `config/summary_time.txt` 中添加多个总结时间点
-- 在 `.env` 中设置默认时区
-- 自定义总结的提示词
+- Multiple summary time points can be added in `config/summary_time.txt`
+- Set the default timezone in `.env`
+- Customize the summary prompt
-> 注意:总结功能会消耗较多的 API 额度,请根据需要开启。
+> Note: The summary feature consumes a significant amount of API quota. Please enable it based on your needs.
-### 📢 推送功能
+### 📢 Push Feature
-除了telegram内部消息转发外,项目还集成了Apprise,利用其强大的推送功能,你可以轻松将消息分发至聊天软件、邮件、短信、Webhooks、APIs 等各种平台。
+In addition to internal Telegram message forwarding, the project also integrates Apprise. Leveraging its powerful push capabilities, you can easily distribute messages to chat apps, email, SMS, Webhooks, APIs, and various other platforms.
-| 推送设置主界面 | 推送设置子界面 |
+| Push Settings Main Interface | Push Settings Sub-interface |
|---------|------|
|  |  |
-#### 设置说明
+#### Settings Description
-| 设置选项 | 说明 |
+| Setting Option | Description |
|---------|------|
-| 只转发到推送配置 | 开启后跳过转发过滤器,直接跳到推送过滤器 |
-| 媒体发送方式 | 支持两种模式: - 单个:每个媒体文件单独推送一条消息 - 全部:将所有媒体文件合并到一条消息中推送 具体使用哪种模式取决于目标平台是否支持一次推送多个附件 |
+| Only forward to push configuration | When enabled, skips the forwarding filter and goes directly to the push filter |
+| Media sending method | Supports two modes: - Single: Each media file is pushed as a separate message - All: All media files are combined into one message for pushing Which mode to use depends on whether the target platform supports pushing multiple attachments at once |
-### 如何添加推送配置?
-完整的推送平台列表和配置格式请参考 [Apprise Wiki](https://github.com/caronc/apprise/wiki)
+### How to add push configuration?
+For the complete list of push platforms and configuration formats, refer to [Apprise Wiki](https://github.com/caronc/apprise/wiki)
-**示例:使用 ntfy.sh 推送**
+**Example: Push using ntfy.sh**
-* 假设你想推送到 ntfy.sh 上的一个名为 `my_topic` 的主题。
-* 根据 Apprise Wiki,其格式为 `ntfy://ntfy.sh/你的主题名`。
-* 那么,你需要添加的配置 URL 就是:
+* Suppose you want to push to a topic named `my_topic` on ntfy.sh.
+* According to Apprise Wiki, the format is `ntfy://ntfy.sh/your_topic_name`.
+* The configuration URL you need to add is:
```
ntfy://ntfy.sh/my_topic
```
-## 📰 RSS订阅
+## 📰 RSS Subscription
-项目集成了将Telegram消息转换为RSS Feed的功能,可以轻松地将Telegram频道/群组内容转为标准RSS格式,方便通过RSS阅读器跟踪。
+The project integrates functionality to convert Telegram messages into RSS Feeds, making it easy to convert Telegram channel/group content into standard RSS format for tracking via RSS readers.
-### 启用RSS功能
+### Enable RSS Feature
-1. 在 `.env` 文件中配置RSS相关参数:
+1. Configure RSS related parameters in the `.env` file:
```ini
- # RSS配置
- # 是否启用RSS功能 (true/false)
+ # RSS Configuration
+ # Whether to enable RSS functionality (true/false)
RSS_ENABLED=true
- # RSS基础访问URL,留空则使用默认的访问URL(例如:https://rss.example.com)
+ # RSS base access URL, leave empty to use the default access URL (e.g., https://rss.example.com)
RSS_BASE_URL=
- # RSS媒体文件基础URL,留空则使用默认的访问URL(例如:https://media.example.com)
+ # RSS media file base URL, leave empty to use the default access URL (e.g., https://media.example.com)
RSS_MEDIA_BASE_URL=
```
-2. docker-compose.yml取消注释
+2. Uncomment in docker-compose.yml
```
- # 如果需要使用 RSS 功能,请取消以下注释
+ # If you need to use RSS functionality, uncomment the following
ports:
- 9804:8000
```
-3. 重启服务以启用RSS功能:
+3. Restart the service to enable RSS functionality:
```bash
docker-compose restart
```
-> 注意:旧版本用户需要用新的docker-compose.yml文件重新部署:[docker-compose.yml](./docker-compose.yml)
-### 访问RSS仪表盘
+> Note: Users of older versions need to redeploy with the new docker-compose.yml file: [docker-compose.yml](./docker-compose.yml)
+### Access RSS Dashboard
-浏览器访问 `http://你的服务器地址:9804/`
+Access `http://your_server_address:9804/` in your browser
-### Nginx配置
+### Nginx Configuration
```
location / {
proxy_pass http://127.0.0.1:9804;
@@ -420,138 +420,138 @@ AI处理提示词中可以使用以下格式:
}
```
-### RSS配置管理
+### RSS Configuration Management
-相关界面
+Related interfaces
-| 登录界面 | Dashboard界面 | 新建/编辑配置界面 |
+| Login Interface | Dashboard Interface | Create/Edit Configuration Interface |
|---------|------|------|
|  |  |  |
-### 新建/编辑配置界面说明
-| 设置选项 | 说明 |
+### Create/Edit Configuration Interface Description
+| Setting Option | Description |
|---------|------|
-| 规则ID | 选择现有的一个转发规则,用于生成RSS订阅 |
-| 复制已有配置 | 选择现有的一个RSS配置,复制它的配置到当前表单|
-|订阅源标题| 设置订阅源标题 |
-|自动填充| 点击后自动根据规则的源聊天窗口名字生成订阅源标题 |
-|订阅源描述| 设置订阅源描述 |
-|语言| 占位,暂无特殊功能 |
-|最大条目数| 设置RSS订阅源的最大条目数,默认50,对于媒体比较多的聊天源,请根据硬盘实际硬盘大小设置 |
-|使用 AI 提取标题和内容| 启用后,将使用AI服务自动分析消息,提取标题和内容和整理格式,AI模型请在bot中设置,不受bot中“是否开启 AI 处理”选项影响,此选项开启后和下面所有配置互斥 |
-|AI 提取提示词| 设置AI提取标题和内容的提示词,如需自定义,请务必让AI返回以下json格式内容:`{ "title": "标题", "content": "正文内容" }` |
-|自动提取标题| 启用后,由预设好的正则表达式自动提取标题 |
-|自动提取内容| 启用后,由预设好的正则表达式自动提取内容 |
-|自动将 Markdown 转换为 HTML| 启用后,将使用相关库自动将Telegram中的Markdown格式转换为标准HTML,如需自行处理,请在bot中使用 `/replace` 自行替换 |
-|启用自定义标题提取正则表达式| 启用后,将使用自定义正则表达式提取标题 |
-|启用自定义内容提取正则表达式| 启用后,将使用自定义正则表达式提取内容 |
-|优先级| 设置正则表达式的执行顺序,数字越小优先级越高。系统会按优先级从高到低依次执行正则表达式,**前一个正则表达式提取的结果会作为下一个的输入**,直到完成所有提取 |
-|正则表达式测试| 可用于测试当前正则表达式是否匹配目标文本 |
+| Rule ID | Select an existing forwarding rule to generate RSS subscription |
+| Copy existing configuration | Select an existing RSS configuration and copy its settings to the current form |
+| Subscription source title | Set the subscription source title |
+| Auto-fill | Click to automatically generate a subscription source title based on the rule's source chat window name |
+| Subscription source description | Set the subscription source description |
+| Language | Placeholder, no special function currently |
+| Maximum entries | Set the maximum number of entries for the RSS subscription source, default 50. For chat sources with lots of media, set according to actual disk size |
+| Use AI to extract title and content | When enabled, AI service will automatically analyze messages, extract titles and content, and organize formatting. Set the AI model in the bot; this is not affected by the "Enable AI processing" option in the bot. When this option is enabled, it is mutually exclusive with all configurations below |
+| AI extraction prompt | Set the prompt for AI title and content extraction. If customizing, make sure AI returns content in the following JSON format: `{ "title": "title", "content": "body content" }` |
+| Auto-extract title | When enabled, titles are automatically extracted using preset regular expressions |
+| Auto-extract content | When enabled, content is automatically extracted using preset regular expressions |
+| Auto-convert Markdown to HTML | When enabled, Markdown format in Telegram will be automatically converted to standard HTML using relevant libraries. For manual handling, use `/replace` in the bot for custom replacements |
+| Enable custom title extraction regex | When enabled, custom regular expressions will be used to extract titles |
+| Enable custom content extraction regex | When enabled, custom regular expressions will be used to extract content |
+| Priority | Set the execution order of regular expressions; lower numbers mean higher priority. The system executes regex from highest to lowest priority, where **the result of the previous regex becomes the input for the next one**, until all extractions are complete |
+| Regex test | Can be used to test whether the current regular expression matches the target text |
-### 特殊说明
-- 若只开启自动提取标题,而不开启自动提取内容,则内容会是包含提取了标题的完整的Telegram消息内容
-- 若内容处理选项和正则表达式配置都为空,会自动匹配前20个字符作为标题,内容则为原始消息
+### Special Notes
+- If only auto-extract title is enabled without auto-extract content, the content will be the complete Telegram message including the extracted title
+- If content processing options and regex configurations are both empty, the first 20 characters are automatically matched as the title, and the content is the original message
-### 特殊设置项
-若在.env中开启`RSS_ENABLED=true`,则会在bot的设置中会新增一个`只转发到RSS`的选项,启用后,消息经过各种处理后会在RSS过滤器处理后中断,不会执行转发/编辑
+### Special Settings
+If `RSS_ENABLED=true` is set in .env, a new "Only forward to RSS" option will appear in the bot's settings. When enabled, messages will be interrupted at the RSS filter after going through various processing, and will not execute forwarding/editing
-### 注意事项
+### Notes
-- 没有找回密码功能,请妥善保管你的账号密码
+- There is no password recovery feature; please keep your credentials safe
-## 🎯 特殊功能
+## 🎯 Special Features
-### 🔗 链接转发功能
+### 🔗 Link Forwarding Feature
-向bot发送消息链接,即可把那条消息转发到当前聊天窗口,无视禁止转发和复制的限制(项目自身功能已无视转发和复制限制
+Send a message link to the bot, and it will forward that message to the current chat window, bypassing restrictions on forwarding and copying (the project's own functionality already bypasses forwarding and copying restrictions).
-### 🔄 与通用论坛屏蔽插件联动
+### 🔄 Integration with Universal Forum Blocker Plugin
> https://github.com/heavrnl/universalforumblock
-确保.env文件中已配置相关参数,在已经绑定好的聊天窗口中使用`/ufb_bind <论坛域名>`,即可实现三端联动屏蔽,使用`/ufb_item_change`切换要同步当前域名的主页关键字/主页用户名/内容页关键字/内容页用户名
+Make sure the relevant parameters are configured in the .env file. In a chat window that has already been bound, use `/ufb_bind ` to achieve three-way synchronized blocking. Use `/ufb_item_change` to switch between syncing the current domain's homepage keywords/homepage usernames/content page keywords/content page usernames.
-## 📝 命令列表
+## 📝 Command List
```bash
-命令列表
-
-基础命令
-/start - 开始使用
-/help(/h) - 显示此帮助信息
-
-绑定和设置
-/bind(/b) <源聊天链接或名称> [目标聊天链接或名称] - 绑定源聊天
-/settings(/s) [规则ID] - 管理转发规则
-/changelog(/cl) - 查看更新日志
-
-转发规则管理
-/copy_rule(/cr) <源规则ID> [目标规则ID] - 复制指定规则的所有设置到当前规则或目标规则ID
-/delete_rule(/dr) <规则ID> [规则ID] [规则ID] ... - 删除指定规则
-/list_rule(/lr) - 列出所有转发规则
-
-关键字管理
-/add(/a) <关键字> [关键字] ["关 键 字"] ['关 键 字'] ... - 添加普通关键字
-/add_regex(/ar) <正则表达式> [正则表达式] [正则表达式] ... - 添加正则表达式
-/add_all(/aa) <关键字> [关键字] [关键字] ... - 添加普通关键字到当前频道绑定的所有规则
-/add_regex_all(/ara) <正则表达式> [正则表达式] [正则表达式] ... - 添加正则关键字到所有规则
-/list_keyword(/lk) - 列出所有关键字
-/remove_keyword(/rk) <关键词> ["关 键 字"] ['关 键 字'] ... - 删除关键字
-/remove_keyword_by_id(/rkbi) [ID] [ID] ... - 按ID删除关键字
-/remove_all_keyword(/rak) <关键词> ["关 键 字"] ['关 键 字'] ... - 删除当前频道绑定的所有规则的指定关键字
-/clear_all_keywords(/cak) - 清除当前规则的所有关键字
-/clear_all_keywords_regex(/cakr) - 清除当前规则的所有正则关键字
-/copy_keywords(/ck) <规则ID> - 复制指定规则的关键字到当前规则
-/copy_keywords_regex(/ckr) <规则ID> - 复制指定规则的正则关键字到当前规则
-/copy_replace(/crp) <规则ID> - 复制指定规则的替换规则到当前规则
-/copy_rule(/cr) <规则ID> - 复制指定规则的所有设置到当前规则(包括关键字、正则、替换规则、媒体设置等)
-
-替换规则管理
-/replace(/r) <正则表达式> [替换内容] - 添加替换规则
-/replace_all(/ra) <正则表达式> [替换内容] - 添加替换规则到所有规则
-/list_replace(/lrp) - 列出所有替换规则
-/remove_replace(/rr) <序号> - 删除替换规则
-/clear_all_replace(/car) - 清除当前规则的所有替换规则
-/copy_replace(/crp) <规则ID> - 复制指定规则的替换规则到当前规则
-
-导入导出
-/export_keyword(/ek) - 导出当前规则的关键字
-/export_replace(/er) - 导出当前规则的替换规则
-/import_keyword(/ik) <同时发送文件> - 导入普通关键字
-/import_regex_keyword(/irk) <同时发送文件> - 导入正则关键字
-/import_replace(/ir) <同时发送文件> - 导入替换规则
-
-RSS相关
-/delete_rss_user(/dru) [用户名] - 删除RSS用户
-
-UFB相关
-/ufb_bind(/ub) <域名> - 绑定UFB域名
-/ufb_unbind(/uu) - 解绑UFB域名
-/ufb_item_change(/uic) - 切换UFB同步配置类型
-
-提示
-• 括号内为命令的简写形式
-• 尖括号 <> 表示必填参数
-• 方括号 [] 表示可选参数
-• 导入命令需要同时发送文件
+Command List
+
+Basic Commands
+/start - Get started
+/help(/h) - Show this help information
+
+Binding and Settings
+/bind(/b) [target chat link or name] - Bind source chat
+/settings(/s) [rule ID] - Manage forwarding rules
+/changelog(/cl) - View changelog
+
+Forwarding Rule Management
+/copy_rule(/cr) [target rule ID] - Copy all settings from specified rule to current rule or target rule ID
+/delete_rule(/dr) [rule ID] [rule ID] ... - Delete specified rules
+/list_rule(/lr) - List all forwarding rules
+
+Keyword Management
+/add(/a) [keyword] ["key word"] ['key word'] ... - Add plain keywords
+/add_regex(/ar) [regex] [regex] ... - Add regular expressions
+/add_all(/aa) [keyword] [keyword] ... - Add plain keywords to all rules bound to current channel
+/add_regex_all(/ara) [regex] [regex] ... - Add regex keywords to all rules
+/list_keyword(/lk) - List all keywords
+/remove_keyword(/rk) ["key word"] ['key word'] ... - Remove keywords
+/remove_keyword_by_id(/rkbi) [ID] [ID] ... - Remove keywords by ID
+/remove_all_keyword(/rak) ["key word"] ['key word'] ... - Remove specified keyword from all rules bound to current channel
+/clear_all_keywords(/cak) - Clear all keywords of current rule
+/clear_all_keywords_regex(/cakr) - Clear all regex keywords of current rule
+/copy_keywords(/ck) - Copy keywords from specified rule to current rule
+/copy_keywords_regex(/ckr) - Copy regex keywords from specified rule to current rule
+/copy_replace(/crp) - Copy replace rules from specified rule to current rule
+/copy_rule(/cr) - Copy all settings from specified rule to current rule (including keywords, regex, replace rules, media settings, etc.)
+
+Replace Rule Management
+/replace(/r) [replacement] - Add replace rule
+/replace_all(/ra) [replacement] - Add replace rule to all rules
+/list_replace(/lrp) - List all replace rules
+/remove_replace(/rr) - Remove replace rule
+/clear_all_replace(/car) - Clear all replace rules of current rule
+/copy_replace(/crp) - Copy replace rules from specified rule to current rule
+
+Import/Export
+/export_keyword(/ek) - Export keywords of current rule
+/export_replace(/er) - Export replace rules of current rule
+/import_keyword(/ik) - Import plain keywords
+/import_regex_keyword(/irk) - Import regex keywords
+/import_replace(/ir) - Import replace rules
+
+RSS Related
+/delete_rss_user(/dru) [username] - Delete RSS user
+
+UFB Related
+/ufb_bind(/ub) - Bind UFB domain
+/ufb_unbind(/uu) - Unbind UFB domain
+/ufb_item_change(/uic) - Switch UFB sync configuration type
+
+Tips
+• Content in parentheses is the shorthand form of the command
+• Angle brackets <> indicate required parameters
+• Square brackets [] indicate optional parameters
+• Import commands require attaching a file
```
-## 💐 致谢
+## 💐 Acknowledgments
- [Apprise](https://github.com/caronc/apprise)
- [Telethon](https://github.com/LonamiWebs/Telethon)
-## ☕ 捐赠
+## ☕ Donate
-如果你觉得这个项目对你有帮助,欢迎通过以下方式请我喝杯咖啡:
+If you find this project helpful, feel free to buy me a coffee through the following:
[](https://ko-fi.com/0heavrnl)
-## 📄 开源协议
+## 📄 License
-本项目采用 [GPL-3.0](LICENSE) 开源协议,详细信息请参阅 [LICENSE](LICENSE) 文件。
+This project is licensed under the [GPL-3.0](LICENSE) license. For details, please refer to the [LICENSE](LICENSE) file.
From a1a6d70dcd8d9cefe579bed19bd066a84f5a9ab9 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:10:50 +0000
Subject: [PATCH 11/76] Translate Chinese to English in utils/media.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
utils/media.py | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/utils/media.py b/utils/media.py
index 7f46b39..730691e 100644
--- a/utils/media.py
+++ b/utils/media.py
@@ -4,34 +4,34 @@
logger = logging.getLogger(__name__)
async def get_media_size(media):
- """获取媒体文件大小"""
+ """Get media file size"""
if not media:
return 0
try:
- # 对于所有类型的媒体,先尝试获取 document
+ # For all types of media, first try to get the document
if hasattr(media, 'document') and media.document:
return media.document.size
- # 对于照片,获取最大尺寸
+ # For photos, get the largest size
if hasattr(media, 'photo') and media.photo:
- # 获取最大尺寸的照片
+ # Get the photo with the largest size
largest_photo = max(media.photo.sizes, key=lambda x: x.size if hasattr(x, 'size') else 0)
return largest_photo.size if hasattr(largest_photo, 'size') else 0
- # 如果是其他类型,尝试直接获取 size 属性
+ # If it's another type, try to directly get the size attribute
if hasattr(media, 'size'):
return media.size
except Exception as e:
- logger.error(f'获取媒体大小时出错: {str(e)}')
+ logger.error(f'Error getting media size: {str(e)}')
return 0
async def get_max_media_size():
- """获取媒体文件大小上限"""
+ """Get media file size upper limit"""
max_media_size_str = os.getenv('MAX_MEDIA_SIZE')
if not max_media_size_str:
- logger.error('未设置 MAX_MEDIA_SIZE 环境变量')
- raise ValueError('必须在 .env 文件中设置 MAX_MEDIA_SIZE')
- return float(max_media_size_str) * 1024 * 1024 # 转换为字节,支持小数
\ No newline at end of file
+ logger.error('MAX_MEDIA_SIZE environment variable is not set')
+ raise ValueError('MAX_MEDIA_SIZE must be set in the .env file')
+ return float(max_media_size_str) * 1024 * 1024 # Convert to bytes, supports decimals
From 739ec655292ab4b71089af06292911404f48bc27 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:11:20 +0000
Subject: [PATCH 12/76] Translate Chinese to English in ai/gemini_provider.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
ai/gemini_provider.py | 108 +++++++++++++++++++++---------------------
1 file changed, 54 insertions(+), 54 deletions(-)
diff --git a/ai/gemini_provider.py b/ai/gemini_provider.py
index 8d6e32f..f4bcba9 100644
--- a/ai/gemini_provider.py
+++ b/ai/gemini_provider.py
@@ -1,6 +1,6 @@
from typing import Optional, List, Dict
import google.generativeai as genai
-# 移除对不存在的模块的导入
+# Remove import of non-existent module
# from google.genai import types
from .base import BaseAIProvider
from .openai_base_provider import OpenAIBaseProvider
@@ -11,46 +11,46 @@
logger = logging.getLogger(__name__)
class GeminiOpenAIProvider(OpenAIBaseProvider):
- """使用OpenAI兼容接口的Gemini提供者"""
+ """Gemini provider using OpenAI-compatible interface"""
def __init__(self):
super().__init__(
env_prefix='GEMINI',
default_model='gemini-pro',
- default_api_base='' # API_BASE必须在环境变量中提供
+ default_api_base='' # API_BASE must be provided in environment variables
)
class GeminiProvider(BaseAIProvider):
def __init__(self):
self.model = None
- self.model_name = None # 添加model_name属性
+ self.model_name = None # Add model_name attribute
self.provider = None
-
+
async def initialize(self, **kwargs):
- """初始化Gemini客户端"""
- # 检查是否配置了GEMINI_API_BASE,如果有则使用兼容OpenAI的接口
+ """Initialize Gemini client"""
+ # Check if GEMINI_API_BASE is configured, if so use OpenAI-compatible interface
api_base = os.getenv('GEMINI_API_BASE', '').strip()
-
+
if api_base:
- logger.info(f"检测到GEMINI_API_BASE环境变量: {api_base},使用兼容OpenAI的接口")
+ logger.info(f"Detected GEMINI_API_BASE environment variable: {api_base}, using OpenAI-compatible interface")
self.provider = GeminiOpenAIProvider()
await self.provider.initialize(**kwargs)
return
-
- # 原来的Gemini API初始化代码
+
+ # Original Gemini API initialization code
api_key = os.getenv('GEMINI_API_KEY')
if not api_key:
- raise ValueError("未设置GEMINI_API_KEY环境变量")
+ raise ValueError("GEMINI_API_KEY environment variable is not set")
- # 使用传入的model参数,如果没有才使用默认值
- if not self.model_name: # 如果model_name还没设置
+ # Use the passed model parameter, only use default if not provided
+ if not self.model_name: # If model_name is not set yet
self.model_name = kwargs.get('model')
-
- if not self.model_name: # 如果kwargs中也没有model
- self.model_name = 'gemini-pro' # 最后才使用默认值
-
- logger.info(f"初始化Gemini模型: {self.model_name}")
-
- # 配置安全设置 - 只使用基本类别
+
+ if not self.model_name: # If model is not in kwargs either
+ self.model_name = 'gemini-pro' # Only then use default value
+
+ logger.info(f"Initializing Gemini model: {self.model_name}")
+
+ # Configure safety settings - only use basic categories
safety_settings = [
{
"category": "HARM_CATEGORY_HARASSMENT",
@@ -69,87 +69,87 @@ async def initialize(self, **kwargs):
"threshold": "BLOCK_NONE"
}
]
-
+
genai.configure(api_key=api_key)
- # 使用self.model_name初始化模型
+ # Initialize model using self.model_name
self.model = genai.GenerativeModel(
model_name=self.model_name,
safety_settings=safety_settings
)
-
- async def process_message(self,
- message: str,
+
+ async def process_message(self,
+ message: str,
prompt: Optional[str] = None,
images: Optional[List[Dict[str, str]]] = None,
**kwargs) -> str:
- """处理消息"""
+ """Process message"""
try:
if not self.provider and not self.model:
await self.initialize(**kwargs)
-
- # 如果使用的是OpenAI兼容接口,则调用该接口的处理方法
+
+ # If using OpenAI-compatible interface, call its processing method
if self.provider:
return await self.provider.process_message(message, prompt, images, **kwargs)
-
- # 使用Gemini API的流式处理
- logger.info(f"实际使用的Gemini模型: {self.model_name}")
- # 组合提示词和消息
+ # Stream processing using Gemini API
+ logger.info(f"Actual Gemini model in use: {self.model_name}")
+
+ # Combine prompt and message
if prompt:
user_message = f"{prompt}\n\n{message}"
else:
user_message = message
-
- # 检查是否有图片
+
+ # Check if there are images
if images and len(images) > 0:
try:
- # 使用MultimodalContent添加图片
+ # Use MultimodalContent to add images
contents = []
- # 添加文本
+ # Add text
contents.append({"role": "user", "parts": [{"text": user_message}]})
-
- # 对每张图片进行处理
+
+ # Process each image
for img in images:
try:
- # 直接添加图片字节到模型的输入
+ # Directly add image bytes to model input
image_part = {
"inline_data": {
"mime_type": img["mime_type"],
- "data": img["data"] # 使用原始base64数据
+ "data": img["data"] # Use original base64 data
}
}
contents[0]["parts"].append(image_part)
- logger.info(f"已添加一张类型为 {img['mime_type']} 的图片,大小约 {len(img['data']) // 1000} KB")
+ logger.info(f"Added an image of type {img['mime_type']}, size approximately {len(img['data']) // 1000} KB")
except Exception as img_error:
- logger.error(f"处理单张图片时出错: {str(img_error)}")
-
- # 使用流式输出 - 不设置额外参数,使用默认值
+ logger.error(f"Error processing a single image: {str(img_error)}")
+
+ # Use streaming output - no extra parameters, use defaults
response_stream = self.model.generate_content(
contents,
stream=True
)
except Exception as e:
- logger.error(f"Gemini处理带图片消息时出错: {str(e)}")
- # 如果处理图片失败,尝试只用文本
+ logger.error(f"Error processing message with images in Gemini: {str(e)}")
+ # If image processing fails, try with text only
response_stream = self.model.generate_content(
[{"role": "user", "parts": [{"text": user_message}]}],
stream=True
)
else:
- # 无图片,使用流式输出
+ # No images, use streaming output
response_stream = self.model.generate_content(
[{"role": "user", "parts": [{"text": user_message}]}],
stream=True
)
-
- # 收集完整响应
+
+ # Collect complete response
full_response = ""
for chunk in response_stream:
if hasattr(chunk, 'text'):
full_response += chunk.text
-
+
return full_response
-
+
except Exception as e:
- logger.error(f"Gemini处理消息时出错: {str(e)}")
- return f"AI处理失败: {str(e)}"
\ No newline at end of file
+ logger.error(f"Error processing message in Gemini: {str(e)}")
+ return f"AI processing failed: {str(e)}"
From 3976c096ad0b84f2821c9c7640695b6631cc9564 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:11:27 +0000
Subject: [PATCH 13/76] Translate Chinese to English in utils/settings.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
utils/settings.py | 80 +++++++++++++++++++++++------------------------
1 file changed, 40 insertions(+), 40 deletions(-)
diff --git a/utils/settings.py b/utils/settings.py
index 1fa171e..7de6508 100644
--- a/utils/settings.py
+++ b/utils/settings.py
@@ -8,109 +8,109 @@
def load_ai_models(type="list"):
"""
- 加载AI模型配置
-
- 参数:
- type (str): 返回类型
- - "list": 返回所有模型的平铺列表 [model1, model2, ...]
- - "dict"/"json": 返回原始配置格式 {provider: [model1, model2, ...]}
-
- 返回值:
- 根据type参数返回不同格式的模型配置
+ Load AI model configuration
+
+ Args:
+ type (str): Return type
+ - "list": Return a flat list of all models [model1, model2, ...]
+ - "dict"/"json": Return the original configuration format {provider: [model1, model2, ...]}
+
+ Returns:
+ Model configuration in different formats depending on the type parameter
"""
try:
models_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'ai_models.json')
-
- # 如果配置文件不存在,创建默认配置
+
+ # If the configuration file does not exist, create default configuration
if not os.path.exists(models_path):
create_default_configs()
-
- # 读取JSON配置文件
+
+ # Read JSON configuration file
with open(models_path, 'r', encoding='utf-8') as f:
models_config = json.load(f)
-
- # 根据type参数返回不同格式
+
+ # Return different formats based on the type parameter
if type.lower() in ["dict", "json"]:
return models_config
-
- # 默认返回模型列表
+
+ # Default: return model list
all_models = []
for provider, models in models_config.items():
all_models.extend(models)
-
- # 确保列表不为空
+
+ # Ensure the list is not empty
if all_models:
return all_models
-
+
except (FileNotFoundError, IOError, json.JSONDecodeError) as e:
- logger.error(f"加载AI模型配置失败: {e}")
-
- # 如果出现任何问题,根据type返回默认值
+ logger.error(f"Failed to load AI model configuration: {e}")
+
+ # If any issue occurs, return default value based on type
if type.lower() in ["dict", "json"]:
return AI_MODELS_CONFIG
-
- # 默认返回模型列表
+
+ # Default: return model list
return ["gpt-3.5-turbo", "gemini-1.5-flash", "claude-3-sonnet"]
def load_summary_times():
- """加载总结时间列表"""
+ """Load summary time list"""
try:
times_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'summary_times.txt')
if not os.path.exists(times_path):
create_default_configs()
-
+
with open(times_path, 'r', encoding='utf-8') as f:
times = [line.strip() for line in f if line.strip()]
if times:
return times
except (FileNotFoundError, IOError) as e:
- logger.warning(f"summary_times.txt 加载失败: {e},使用默认时间列表")
+ logger.warning(f"Failed to load summary_times.txt: {e}, using default time list")
return ['00:00', '06:00', '12:00', '18:00']
def load_delay_times():
- """加载延迟时间列表"""
+ """Load delay time list"""
try:
times_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'delay_times.txt')
if not os.path.exists(times_path):
create_default_configs()
-
+
with open(times_path, 'r', encoding='utf-8') as f:
times = [line.strip() for line in f if line.strip()]
if times:
return times
except (FileNotFoundError, IOError) as e:
- logger.warning(f"delay_times.txt 加载失败: {e},使用默认时间列表")
+ logger.warning(f"Failed to load delay_times.txt: {e}, using default time list")
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def load_max_media_size():
- """加载媒体大小限制"""
+ """Load media size limit"""
try:
size_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'max_media_size.txt')
if not os.path.exists(size_path):
create_default_configs()
-
+
with open(size_path, 'r', encoding='utf-8') as f:
size = [line.strip() for line in f if line.strip()]
if size:
return size
-
+
except (FileNotFoundError, IOError) as e:
- logger.warning(f"max_media_size.txt 加载失败: {e},使用默认大小限制")
+ logger.warning(f"Failed to load max_media_size.txt: {e}, using default size limit")
return [5,10,15,20,50,100,200,300,500,1024,2048]
def load_media_extensions():
- """加载媒体扩展名"""
+ """Load media extensions"""
try:
size_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config', 'media_extensions.txt')
if not os.path.exists(size_path):
create_default_configs()
-
+
with open(size_path, 'r', encoding='utf-8') as f:
size = [line.strip() for line in f if line.strip()]
if size:
return size
-
+
except (FileNotFoundError, IOError) as e:
- logger.warning(f"media_extensions.txt 加载失败: {e},使用默认扩展名")
- return ['无扩展名','txt','jpg','png','gif','mp4','mp3','wav','ogg','flac','aac','wma','m4a','m4v','mov','avi','mkv','webm','mpg','mpeg','mpe','mp3','mp2','m4a','m4p','m4b','m4r','m4v','mpg','mpeg','mp2','mp3','mp4','mpc','oga','ogg','wav','wma','3gp','3g2','3gpp','3gpp2','amr','awb','caf','flac','m4a','m4b','m4p','oga','ogg','opus','spx','vorbis','wav','wma','webm','aac','ac3','dts','dtshd','flac','mp3','mp4','m4a','m4b','m4p','oga','ogg','wav','wma','webm','aac','ac3','dts','dtshd','flac','mp3','mp4','m4a','m4b','m4p','oga','ogg','wav','wma','webm']
\ No newline at end of file
+ logger.warning(f"Failed to load media_extensions.txt: {e}, using default extensions")
+ return ['No extension','txt','jpg','png','gif','mp4','mp3','wav','ogg','flac','aac','wma','m4a','m4v','mov','avi','mkv','webm','mpg','mpeg','mpe','mp3','mp2','m4a','m4p','m4b','m4r','m4v','mpg','mpeg','mp2','mp3','mp4','mpc','oga','ogg','wav','wma','3gp','3g2','3gpp','3gpp2','amr','awb','caf','flac','m4a','m4b','m4p','oga','ogg','opus','spx','vorbis','wav','wma','webm','aac','ac3','dts','dtshd','flac','mp3','mp4','m4a','m4b','m4p','oga','ogg','wav','wma','webm','aac','ac3','dts','dtshd','flac','mp3','mp4','m4a','m4b','m4p','oga','ogg','wav','wma','webm']
From ac76cb095edd65269f4eebed4d239bca91d157e9 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:11:51 +0000
Subject: [PATCH 14/76] Translate Chinese to English in
ai/openai_base_provider.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
ai/openai_base_provider.py | 50 +++++++++++++++++++-------------------
1 file changed, 25 insertions(+), 25 deletions(-)
diff --git a/ai/openai_base_provider.py b/ai/openai_base_provider.py
index fc34dc1..57b8454 100644
--- a/ai/openai_base_provider.py
+++ b/ai/openai_base_provider.py
@@ -10,12 +10,12 @@ class OpenAIBaseProvider(BaseAIProvider):
def __init__(self, env_prefix: str = 'OPENAI', default_model: str = 'gpt-4o-mini',
default_api_base: str = 'https://api.openai.com/v1'):
"""
- 初始化基础OpenAI格式提供者
+ Initialize base OpenAI-format provider
Args:
- env_prefix: 环境变量前缀,如 'OPENAI', 'GROK', 'DEEPSEEK', 'QWEN'
- default_model: 默认模型名称
- default_api_base: 默认API基础URL
+ env_prefix: Environment variable prefix, e.g. 'OPENAI', 'GROK', 'DEEPSEEK', 'QWEN'
+ default_model: Default model name
+ default_api_base: Default API base URL
"""
super().__init__()
self.env_prefix = env_prefix
@@ -25,11 +25,11 @@ def __init__(self, env_prefix: str = 'OPENAI', default_model: str = 'gpt-4o-mini
self.model = None
async def initialize(self, **kwargs) -> None:
- """初始化OpenAI客户端"""
+ """Initialize OpenAI client"""
try:
api_key = os.getenv(f'{self.env_prefix}_API_KEY')
if not api_key:
- raise ValueError(f"未设置 {self.env_prefix}_API_KEY 环境变量")
+ raise ValueError(f"{self.env_prefix}_API_KEY environment variable is not set")
api_base = os.getenv(f'{self.env_prefix}_API_BASE', '').strip() or self.default_api_base
@@ -39,10 +39,10 @@ async def initialize(self, **kwargs) -> None:
)
self.model = kwargs.get('model', self.default_model)
- logger.info(f"初始化OpenAI模型: {self.model}")
+ logger.info(f"Initializing OpenAI model: {self.model}")
except Exception as e:
- error_msg = f"初始化 {self.env_prefix} 客户端时出错: {str(e)}"
+ error_msg = f"Error initializing {self.env_prefix} client: {str(e)}"
logger.error(error_msg, exc_info=True)
raise
@@ -51,7 +51,7 @@ async def process_message(self,
prompt: Optional[str] = None,
images: Optional[List[Dict[str, str]]] = None,
**kwargs) -> str:
- """处理消息"""
+ """Process message"""
try:
if not self.client:
await self.initialize(**kwargs)
@@ -60,18 +60,18 @@ async def process_message(self,
if prompt:
messages.append({"role": "system", "content": prompt})
- # 如果有图片,需要添加到消息中
+ # If there are images, add them to the message
if images and len(images) > 0:
- # 创建包含文本和图片的内容数组
+ # Create content array containing text and images
content = []
- # 添加文本
+ # Add text
content.append({
"type": "text",
"text": message
})
- # 添加每张图片
+ # Add each image
for img in images:
content.append({
"type": "image_url",
@@ -79,23 +79,23 @@ async def process_message(self,
"url": f"data:{img['mime_type']};base64,{img['data']}"
}
})
- logger.info(f"已添加一张类型为 {img['mime_type']} 的图片,大小约 {len(img['data']) // 1000} KB")
+ logger.info(f"Added an image of type {img['mime_type']}, size approximately {len(img['data']) // 1000} KB")
messages.append({"role": "user", "content": content})
else:
- # 没有图片,只添加文本
+ # No images, only add text
messages.append({"role": "user", "content": message})
- logger.info(f"实际使用的OpenAI模型: {self.model}")
+ logger.info(f"Actual OpenAI model in use: {self.model}")
- # 所有模型统一使用流式调用
+ # All models use streaming calls uniformly
completion = await self.client.chat.completions.create(
model=self.model,
messages=messages,
stream=True
)
- # 收集所有内容
+ # Collect all content
collected_content = ""
collected_reasoning = ""
@@ -105,21 +105,21 @@ async def process_message(self,
delta = chunk.choices[0].delta
- # 处理思考内容(如果存在)
+ # Process thinking content (if present)
if hasattr(delta, 'reasoning_content') and delta.reasoning_content is not None:
collected_reasoning += delta.reasoning_content
- # 处理回答内容
+ # Process response content
if hasattr(delta, 'content') and delta.content is not None:
collected_content += delta.content
- # 如果没有内容但有思考过程,可能是思考模型只返回了思考过程
+ # If no content but has thinking process, the thinking model may have only returned the thinking process
if not collected_content and collected_reasoning:
- logger.warning("模型只返回了思考过程,没有最终回答")
- return "模型未能生成有效回答"
+ logger.warning("Model only returned thinking process, no final answer")
+ return "Model failed to generate a valid answer"
return collected_content
except Exception as e:
- logger.error(f"{self.env_prefix} API 调用失败: {str(e)}", exc_info=True)
- return f"AI处理失败: {str(e)}"
+ logger.error(f"{self.env_prefix} API call failed: {str(e)}", exc_info=True)
+ return f"AI processing failed: {str(e)}"
From be7ebf8fe3994db1295551ee2970ee4314678e23 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:11:55 +0000
Subject: [PATCH 15/76] Translate Chinese to English in
scheduler/summary_scheduler.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
scheduler/summary_scheduler.py | 160 ++++++++++++++++-----------------
1 file changed, 80 insertions(+), 80 deletions(-)
diff --git a/scheduler/summary_scheduler.py b/scheduler/summary_scheduler.py
index 4b10edf..0d1fbbf 100644
--- a/scheduler/summary_scheduler.py
+++ b/scheduler/summary_scheduler.py
@@ -21,67 +21,67 @@
class SummaryScheduler:
def __init__(self, user_client: TelegramClient, bot_client: TelegramClient):
- self.tasks = {} # 存储所有定时任务 {rule_id: task}
+ self.tasks = {} # Store all scheduled tasks {rule_id: task}
self.timezone = pytz.timezone(DEFAULT_TIMEZONE)
self.user_client = user_client
self.bot_client = bot_client
- # 添加信号量来限制并发请求
- self.request_semaphore = asyncio.Semaphore(2) # 最多同时执行2个请求
- # 从环境变量获取配置
+ # Add semaphore to limit concurrent requests
+ self.request_semaphore = asyncio.Semaphore(2) # Execute at most 2 requests simultaneously
+ # Get configuration from environment variables
self.batch_size = int(os.getenv('SUMMARY_BATCH_SIZE', 20))
self.batch_delay = int(os.getenv('SUMMARY_BATCH_DELAY', 2))
async def schedule_rule(self, rule):
- """为规则创建或更新定时任务"""
+ """Create or update a scheduled task for a rule"""
try:
- # 如果规则已有任务,先取消
+ # If the rule already has a task, cancel it first
if rule.id in self.tasks:
old_task = self.tasks[rule.id]
old_task.cancel()
- logger.info(f"已取消规则 {rule.id} 的旧任务")
+ logger.info(f"Cancelled old task for rule {rule.id}")
del self.tasks[rule.id]
- # 如果启用了AI总结,创建新任务
+ # If AI summary is enabled, create a new task
if rule.is_summary:
- # 计算下一次执行时间
+ # Calculate the next execution time
now = datetime.now(self.timezone)
next_time = self._get_next_run_time(now, rule.summary_time)
wait_seconds = (next_time - now).total_seconds()
- logger.info(f"规则 {rule.id} 的下一次执行时间: {next_time.strftime('%Y-%m-%d %H:%M:%S')}")
- logger.info(f"等待时间: {wait_seconds:.2f} 秒")
+ logger.info(f"Next execution time for rule {rule.id}: {next_time.strftime('%Y-%m-%d %H:%M:%S')}")
+ logger.info(f"Wait time: {wait_seconds:.2f} seconds")
task = asyncio.create_task(self._run_summary_task(rule))
self.tasks[rule.id] = task
- logger.info(f"已为规则 {rule.id} 创建新的总结任务,时间: {rule.summary_time}")
+ logger.info(f"Created new summary task for rule {rule.id}, time: {rule.summary_time}")
else:
- logger.info(f"规则 {rule.id} 的总结功能已关闭,不创建新任务")
+ logger.info(f"Summary feature for rule {rule.id} is disabled, not creating a new task")
except Exception as e:
- logger.error(f"调度规则 {rule.id} 时出错: {str(e)}")
- logger.error(f"错误详情: {traceback.format_exc()}")
+ logger.error(f"Error scheduling rule {rule.id}: {str(e)}")
+ logger.error(f"Error details: {traceback.format_exc()}")
async def _run_summary_task(self, rule):
- """运行单个规则的总结任务"""
+ """Run the summary task for a single rule"""
while True:
try:
- # 计算下一次执行时间
+ # Calculate the next execution time
now = datetime.now(self.timezone)
target_time = self._get_next_run_time(now, rule.summary_time)
- # 等待到执行时间
+ # Wait until execution time
wait_seconds = (target_time - now).total_seconds()
await asyncio.sleep(wait_seconds)
- # 执行总结任务
+ # Execute the summary task
await self._execute_summary(rule.id)
except asyncio.CancelledError:
- logger.info(f"规则 {rule.id} 的旧任务已取消")
+ logger.info(f"Old task for rule {rule.id} has been cancelled")
break
except Exception as e:
- logger.error(f"规则 {rule.id} 的总结任务出错: {str(e)}")
- await asyncio.sleep(60) # 出错后等待一分钟再重试
+ logger.error(f"Summary task for rule {rule.id} encountered an error: {str(e)}")
+ await asyncio.sleep(60) # Wait one minute before retrying after an error
def _split_message(self, text: str, max_length: int = MAX_MESSAGE_PART_LENGTH):
if not text:
@@ -114,7 +114,7 @@ def _split_message(self, text: str, max_length: int = MAX_MESSAGE_PART_LENGTH):
return parts
def _get_next_run_time(self, now, target_time):
- """计算下一次运行时间"""
+ """Calculate the next run time"""
hour, minute = map(int, target_time.split(':'))
next_time = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
@@ -124,7 +124,7 @@ def _get_next_run_time(self, now, target_time):
return next_time
async def _execute_summary(self, rule_id, is_now=False):
- """执行单个规则的总结任务"""
+ """Execute the summary task for a single rule"""
session = get_session()
try:
rule = session.query(ForwardRule).get(rule_id)
@@ -138,14 +138,14 @@ async def _execute_summary(self, rule_id, is_now=False):
messages = []
- # 计算时间范围
+ # Calculate time range
now = datetime.now(self.timezone)
summary_hour, summary_minute = map(int, rule.summary_time.split(':'))
- # 设置结束时间为当前时间
+ # Set end time to current time
end_time = now
- # 设置开始时间为前一天的总结时间
+ # Set start time to the summary time of the previous day
start_time = now.replace(
hour=summary_hour,
minute=summary_minute,
@@ -153,14 +153,14 @@ async def _execute_summary(self, rule_id, is_now=False):
microsecond=0
) - timedelta(days=1)
- logger.info(f'规则 {rule_id} 获取消息时间范围: {start_time} 到 {end_time}')
+ logger.info(f'Rule {rule_id} message retrieval time range: {start_time} to {end_time}')
async with self.request_semaphore:
messages = []
current_offset = 0
while True:
- batch = [] # 移到循环外部
+ batch = [] # Moved outside the loop
messages_batch = await self.user_client.get_messages(
source_chat_id,
limit=self.batch_size,
@@ -170,60 +170,60 @@ async def _execute_summary(self, rule_id, is_now=False):
)
if not messages_batch:
- logger.info(f'规则 {rule_id} 没有获取到新消息,退出循环')
+ logger.info(f'Rule {rule_id} no new messages retrieved, exiting loop')
break
- logger.info(f'规则 {rule_id} 获取到批次消息数量: {len(messages_batch)}')
+ logger.info(f'Rule {rule_id} batch message count retrieved: {len(messages_batch)}')
should_break = False
for message in messages_batch:
msg_time = message.date.astimezone(self.timezone)
preview = message.text[:20] + '...' if message.text else 'None'
- logger.info(f'规则 {rule_id} 处理消息 - 时间: {msg_time}, 预览: {preview}, 长度: {len(message.text) if message.text else 0}')
+ logger.info(f'Rule {rule_id} processing message - time: {msg_time}, preview: {preview}, length: {len(message.text) if message.text else 0}')
- # 跳过未来时间的消息
+ # Skip messages with future timestamps
if msg_time > end_time:
continue
- # 如果消息在有效时间范围内,添加到批次
+ # If the message is within the valid time range, add it to the batch
if start_time <= msg_time <= end_time and message.text:
batch.append(message.text)
- # 如果遇到早于开始时间的消息,标记退出
+ # If a message earlier than the start time is encountered, mark for exit
if msg_time < start_time:
- logger.info(f'规则 {rule_id} 消息时间 {msg_time} 早于开始时间 {start_time},停止获取')
+ logger.info(f'Rule {rule_id} message time {msg_time} is earlier than start time {start_time}, stopping retrieval')
should_break = True
break
- # 如果当前批次有消息,添加到总消息列表
+ # If the current batch has messages, add them to the total message list
if batch:
messages.extend(batch)
- logger.info(f'规则 {rule_id} 当前批次添加了 {len(batch)} 条消息,总消息数: {len(messages)}')
+ logger.info(f'Rule {rule_id} current batch added {len(batch)} messages, total messages: {len(messages)}')
- # 更新offset为最后一条消息的ID
+ # Update offset to the ID of the last message
current_offset = messages_batch[-1].id
- # 如果需要退出循环
+ # If the loop needs to exit
if should_break:
break
- # 在批次之间等待
+ # Wait between batches
await asyncio.sleep(self.batch_delay)
if not messages:
- logger.info(f'规则 {rule_id} 没有需要总结的消息')
+ logger.info(f'Rule {rule_id} no messages to summarize')
return
all_messages = '\n'.join(messages)
- # 检查AI模型设置,如未设置则使用默认模型
+ # Check AI model setting, use default model if not set
if not rule.ai_model:
rule.ai_model = DEFAULT_AI_MODEL
- logger.info(f"使用默认AI模型进行总结: {rule.ai_model}")
+ logger.info(f"Using default AI model for summary: {rule.ai_model}")
else:
- logger.info(f"使用规则配置的AI模型进行总结: {rule.ai_model}")
+ logger.info(f"Using rule-configured AI model for summary: {rule.ai_model}")
- # 获取AI提供者并处理总结
+ # Get AI provider and process the summary
provider = await get_ai_provider(rule.ai_model)
summary = await provider.process_message(
all_messages,
@@ -234,9 +234,9 @@ async def _execute_summary(self, rule_id, is_now=False):
if summary:
duration_hours = round((end_time - start_time).total_seconds() / 3600)
- header = f"📋 {rule.source_chat.name} - {duration_hours}小时消息总结\n"
- header += f"🕐 时间范围: {start_time.strftime('%Y-%m-%d %H:%M')} - {end_time.strftime('%Y-%m-%d %H:%M')}\n"
- header += f"📊 消息数量: {len(messages)} 条\n\n"
+ header = f"📋 {rule.source_chat.name} - {duration_hours}-hour message summary\n"
+ header += f"🕐 Time range: {start_time.strftime('%Y-%m-%d %H:%M')} - {end_time.strftime('%Y-%m-%d %H:%M')}\n"
+ header += f"📊 Message count: {len(messages)} messages\n\n"
summary_parts = self._split_message(summary, MAX_MESSAGE_PART_LENGTH)
@@ -245,9 +245,9 @@ async def _execute_summary(self, rule_id, is_now=False):
if i == 0:
message_to_send = header + part
else:
- message_to_send = f"📋 {rule.source_chat.name} - 总结报告 (续 {i+1}/{len(summary_parts)})\n\n" + part
+ message_to_send = f"📋 {rule.source_chat.name} - Summary report (continued {i+1}/{len(summary_parts)})\n\n" + part
- # 发送消息,支持重试机制
+ # Send message with retry mechanism
current_message = None
use_markdown = True
attempt = 0
@@ -271,31 +271,31 @@ async def _execute_summary(self, rule_id, is_now=False):
except errors.MarkupInvalidError as e:
if use_markdown:
- logger.warning(f"Markdown解析失败: {e}. 降级为纯文本后重试。")
+ logger.warning(f"Markdown parsing failed: {e}. Falling back to plain text and retrying.")
use_markdown = False
- continue # 立即重试,使用纯文本格式
+ continue # Retry immediately using plain text format
else:
# This should not happen, but if it does, it's a bug.
- logger.error(f"纯文本发送时出现意外的 MarkupInvalidError : {e}")
+ logger.error(f"Unexpected MarkupInvalidError when sending plain text: {e}")
raise # Fail fast
except errors.FloodWaitError as fwe:
if attempt < MAX_SEND_ATTEMPTS - 1:
- logger.warning(f"触发Telegram发送频率限制,等待 {fwe.seconds} 秒后重试...")
+ logger.warning(f"Telegram rate limit triggered, waiting {fwe.seconds} seconds before retrying...")
await asyncio.sleep(fwe.seconds)
attempt += 1
else:
- logger.error("重试次数已达上限,发送失败。")
+ logger.error("Maximum retry attempts reached, sending failed.")
raise
except Exception as send_error:
- logger.error(f"发送总结第 {i+1} 部分时出错: {str(send_error)}")
+ logger.error(f"Error sending summary part {i+1}: {str(send_error)}")
if attempt >= MAX_SEND_ATTEMPTS - 1:
raise # Re-raise on last attempt
await asyncio.sleep(1) # Wait a bit before retrying on other errors
attempt += 1
- # 统一处理第一条消息的赋值
+ # Assign the first message uniformly
if i == 0:
summary_message = current_message
@@ -303,67 +303,67 @@ async def _execute_summary(self, rule_id, is_now=False):
try:
await self.bot_client.pin_message(target_chat_id, summary_message)
except Exception as pin_error:
- logger.warning(f"置顶总结消息失败: {str(pin_error)}")
+ logger.warning(f"Failed to pin summary message: {str(pin_error)}")
- logger.info(f'规则 {rule_id} 总结完成,共处理 {len(messages)} 条消息,分为 {len(summary_parts)} 部分发送')
+ logger.info(f'Rule {rule_id} summary completed, processed {len(messages)} messages, sent in {len(summary_parts)} parts')
except Exception as e:
- logger.error(f'执行规则 {rule_id} 的总结任务时出错: {str(e)}')
- logger.error(f'错误详情: {traceback.format_exc()}')
+ logger.error(f'Error executing summary task for rule {rule_id}: {str(e)}')
+ logger.error(f'Error details: {traceback.format_exc()}')
finally:
session.close()
async def start(self):
- """启动调度器"""
- logger.info("开始启动调度器...")
+ """Start the scheduler"""
+ logger.info("Starting scheduler...")
session = get_session()
try:
- # 获取所有启用了总结功能的规则
+ # Get all rules with summary feature enabled
rules = session.query(ForwardRule).filter_by(is_summary=True).all()
- logger.info(f"找到 {len(rules)} 个启用了总结功能的规则")
+ logger.info(f"Found {len(rules)} rules with summary feature enabled")
for rule in rules:
- logger.info(f"正在为规则 {rule.id} ({rule.source_chat.name} -> {rule.target_chat.name}) 创建调度任务")
- logger.info(f"总结时间: {rule.summary_time}")
+ logger.info(f"Creating scheduled task for rule {rule.id} ({rule.source_chat.name} -> {rule.target_chat.name})")
+ logger.info(f"Summary time: {rule.summary_time}")
- # 计算下一次执行时间
+ # Calculate the next execution time
now = datetime.now(self.timezone)
next_time = self._get_next_run_time(now, rule.summary_time)
wait_seconds = (next_time - now).total_seconds()
- logger.info(f"下一次执行时间: {next_time.strftime('%Y-%m-%d %H:%M:%S')}")
- logger.info(f"等待时间: {wait_seconds:.2f} 秒")
+ logger.info(f"Next execution time: {next_time.strftime('%Y-%m-%d %H:%M:%S')}")
+ logger.info(f"Wait time: {wait_seconds:.2f} seconds")
await self.schedule_rule(rule)
if not rules:
- logger.info("没有找到启用了总结功能的规则")
+ logger.info("No rules with summary feature enabled found")
- logger.info("调度器启动完成")
+ logger.info("Scheduler startup completed")
except Exception as e:
- logger.error(f"启动调度器时出错: {str(e)}")
- logger.error(f"错误详情: {traceback.format_exc()}")
+ logger.error(f"Error starting scheduler: {str(e)}")
+ logger.error(f"Error details: {traceback.format_exc()}")
finally:
session.close()
def stop(self):
- """停止所有任务"""
+ """Stop all tasks"""
for task in self.tasks.values():
task.cancel()
self.tasks.clear()
async def execute_all_summaries(self):
- """立即执行所有启用了总结功能的规则"""
+ """Immediately execute all rules with summary feature enabled"""
session = get_session()
try:
rules = session.query(ForwardRule).filter_by(is_summary=True).all()
- # 使用 gather 但限制并发数
+ # Use gather but limit concurrency
tasks = [self._execute_summary(rule.id, is_now=True) for rule in rules]
- for i in range(0, len(tasks), 2): # 每次执行2个任务
+ for i in range(0, len(tasks), 2): # Execute 2 tasks at a time
batch = tasks[i:i+2]
await asyncio.gather(*batch)
- await asyncio.sleep(1) # 每批次之间稍微暂停
+ await asyncio.sleep(1) # Brief pause between batches
finally:
session.close()
From 3a4a9feb452ac7c777b54680ca941d3023a154f1 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:12:13 +0000
Subject: [PATCH 16/76] Translate Chinese to English in ai/openai_provider.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
ai/openai_provider.py | 36 ++++++++++++++++++------------------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/ai/openai_provider.py b/ai/openai_provider.py
index fba9eb1..92b4633 100644
--- a/ai/openai_provider.py
+++ b/ai/openai_provider.py
@@ -15,32 +15,32 @@ def __init__(self):
default_api_base='https://api.openai.com/v1'
)
- async def process_message(self,
- message: str,
+ async def process_message(self,
+ message: str,
prompt: Optional[str] = None,
images: Optional[List[Dict[str, str]]] = None,
**kwargs) -> str:
- """处理消息"""
+ """Process message"""
try:
if not self.client:
await self.initialize(**kwargs)
-
+
messages = []
if prompt:
messages.append({"role": "system", "content": prompt})
-
- # 如果有图片,需要添加到消息中
+
+ # If there are images, add them to the message
if images and len(images) > 0:
- # 创建包含文本和图片的内容数组
+ # Create content array containing text and images
content = []
-
- # 添加文本
+
+ # Add text
content.append({
"type": "text",
"text": message
})
-
- # 添加每张图片
+
+ # Add each image
for img in images:
content.append({
"type": "image_url",
@@ -48,19 +48,19 @@ async def process_message(self,
"url": f"data:{img['mime_type']};base64,{img['data']}"
}
})
-
+
messages.append({"role": "user", "content": content})
else:
- # 没有图片,只添加文本
+ # No images, only add text
messages.append({"role": "user", "content": message})
-
+
response = await self.client.chat.completions.create(
model=self.model,
messages=messages
)
-
+
return response.choices[0].message.content
-
+
except Exception as e:
- logger.error(f"OpenAI处理消息时出错: {str(e)}", exc_info=True)
- return f"AI处理失败: {str(e)}"
\ No newline at end of file
+ logger.error(f"Error processing message in OpenAI: {str(e)}", exc_info=True)
+ return f"AI processing failed: {str(e)}"
\ No newline at end of file
From fdd3e2fff35dc8e5f7d711bb0ba37abb1e5e8f89 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:12:36 +0000
Subject: [PATCH 17/76] Translate Chinese to English in ai/__init__.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
ai/__init__.py | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/ai/__init__.py b/ai/__init__.py
index bcfb7d6..59ad6f5 100644
--- a/ai/__init__.py
+++ b/ai/__init__.py
@@ -10,23 +10,23 @@
from utils.settings import load_ai_models
from utils.constants import DEFAULT_AI_MODEL
-# 获取日志记录器
+# Get logger
logger = logging.getLogger(__name__)
async def get_ai_provider(model=None):
- """获取AI提供者实例"""
+ """Get AI provider instance"""
if not model:
model = DEFAULT_AI_MODEL
-
- # 加载提供商配置(使用dict格式)
+
+ # Load provider configuration (using dict format)
providers_config = load_ai_models(type="dict")
-
- # 根据模型名称选择对应的提供者
+
+ # Select corresponding provider based on model name
provider = None
-
- # 遍历配置中的每个提供商
+
+ # Iterate through each provider in configuration
for provider_name, models_list in providers_config.items():
- # 检查完全匹配
+ # Check for exact match
if model in models_list:
if provider_name == "openai":
provider = OpenAIProvider()
@@ -41,9 +41,9 @@ async def get_ai_provider(model=None):
elif provider_name == "claude":
provider = ClaudeProvider()
break
-
+
if not provider:
- raise ValueError(f"不支持的模型: {model}")
+ raise ValueError(f"Unsupported model: {model}")
return provider
From bfe28070ac000b2aa44a3a434374e916dde4c3a2 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:12:41 +0000
Subject: [PATCH 18/76] Translate Chinese to English in
scheduler/chat_updater.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
scheduler/chat_updater.py | 134 +++++++++++++++++++-------------------
1 file changed, 67 insertions(+), 67 deletions(-)
diff --git a/scheduler/chat_updater.py b/scheduler/chat_updater.py
index ba93b6b..77b5a46 100644
--- a/scheduler/chat_updater.py
+++ b/scheduler/chat_updater.py
@@ -15,138 +15,138 @@ def __init__(self, user_client: TelegramClient):
self.user_client = user_client
self.timezone = pytz.timezone(DEFAULT_TIMEZONE)
self.task = None
- # 从环境变量获取更新时间,默认凌晨3点
+ # Get update time from environment variable, default is 3:00 AM
self.update_time = os.getenv('CHAT_UPDATE_TIME', "03:00")
-
+
async def start(self):
- """启动定时更新任务"""
- logger.info("开始启动聊天信息更新器...")
+ """Start the scheduled update task"""
+ logger.info("Starting chat info updater...")
try:
- # 计算下一次执行时间
+ # Calculate the next execution time
now = datetime.now(self.timezone)
next_time = self._get_next_run_time(now, self.update_time)
wait_seconds = (next_time - now).total_seconds()
-
- logger.info(f"下一次聊天信息更新时间: {next_time.strftime('%Y-%m-%d %H:%M:%S')}")
- logger.info(f"等待时间: {wait_seconds:.2f} 秒")
-
- # 创建定时任务
+
+ logger.info(f"Next chat info update time: {next_time.strftime('%Y-%m-%d %H:%M:%S')}")
+ logger.info(f"Wait time: {wait_seconds:.2f} seconds")
+
+ # Create scheduled task
self.task = asyncio.create_task(self._run_update_task())
- logger.info("聊天信息更新器启动完成")
+ logger.info("Chat info updater startup completed")
except Exception as e:
- logger.error(f"启动聊天信息更新器时出错: {str(e)}")
- logger.error(f"错误详情: {traceback.format_exc()}")
-
+ logger.error(f"Error starting chat info updater: {str(e)}")
+ logger.error(f"Error details: {traceback.format_exc()}")
+
def _get_next_run_time(self, now, target_time):
- """计算下一次运行时间"""
+ """Calculate the next run time"""
hour, minute = map(int, target_time.split(':'))
next_time = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
-
+
if next_time <= now:
next_time += timedelta(days=1)
-
+
return next_time
-
+
async def _run_update_task(self):
- """运行更新任务"""
+ """Run the update task"""
while True:
try:
- # 计算下一次执行时间
+ # Calculate the next execution time
now = datetime.now(self.timezone)
target_time = self._get_next_run_time(now, self.update_time)
-
- # 等待到执行时间
+
+ # Wait until execution time
wait_seconds = (target_time - now).total_seconds()
await asyncio.sleep(wait_seconds)
-
- # 执行更新任务
+
+ # Execute the update task
await self._update_all_chats()
-
+
except asyncio.CancelledError:
- logger.info("聊天信息更新任务已取消")
+ logger.info("Chat info update task has been cancelled")
break
except Exception as e:
- logger.error(f"聊天信息更新任务出错: {str(e)}")
- logger.error(f"错误详情: {traceback.format_exc()}")
- await asyncio.sleep(60) # 出错后等待一分钟再重试
-
+ logger.error(f"Chat info update task encountered an error: {str(e)}")
+ logger.error(f"Error details: {traceback.format_exc()}")
+ await asyncio.sleep(60) # Wait one minute before retrying after an error
+
async def _update_all_chats(self):
- """更新所有聊天信息"""
- logger.info("开始更新所有聊天信息...")
+ """Update all chat info"""
+ logger.info("Starting to update all chat info...")
session = get_session()
try:
- # 获取所有聊天
+ # Get all chats
chats = session.query(Chat).all()
total_chats = len(chats)
- logger.info(f"找到 {total_chats} 个聊天需要更新信息")
-
+ logger.info(f"Found {total_chats} chats that need info updates")
+
updated_count = 0
skipped_count = 0
error_count = 0
-
- # 处理每个聊天
+
+ # Process each chat
for i, chat in enumerate(chats, 1):
try:
- # 每10个聊天报告一次进度
+ # Report progress every 10 chats
if i % 10 == 0 or i == total_chats:
- logger.info(f"进度: {i}/{total_chats} ({i/total_chats*100:.1f}%)")
-
+ logger.info(f"Progress: {i}/{total_chats} ({i/total_chats*100:.1f}%)")
+
chat_id = chat.telegram_chat_id
- # 尝试获取聊天实体
+ # Try to get chat entity
try:
- # 尝试转换聊天ID为整数
+ # Try to convert chat ID to integer
try:
chat_id_int = int(chat_id)
except ValueError:
- logger.warning(f"聊天ID '{chat_id}' 不是有效的数字格式")
+ logger.warning(f"Chat ID '{chat_id}' is not a valid numeric format")
skipped_count += 1
continue
-
+
entity = await self.user_client.get_entity(chat_id_int)
- # 更新聊天名称
+ # Update chat name
new_name = entity.title if hasattr(entity, 'title') else (
- f"{entity.first_name} {entity.last_name}" if hasattr(entity, 'last_name') and entity.last_name
- else entity.first_name if hasattr(entity, 'first_name')
- else "私聊"
+ f"{entity.first_name} {entity.last_name}" if hasattr(entity, 'last_name') and entity.last_name
+ else entity.first_name if hasattr(entity, 'first_name')
+ else "Private Chat"
)
-
- # 只有当名称有变化时才更新
+
+ # Only update when the name has changed
if chat.name != new_name:
- old_name = chat.name or "未命名"
+ old_name = chat.name or "Unnamed"
chat.name = new_name
session.commit()
- logger.info(f"已更新聊天 {chat_id}: {old_name} -> {new_name}")
+ logger.info(f"Updated chat {chat_id}: {old_name} -> {new_name}")
updated_count += 1
else:
skipped_count += 1
-
+
except ValueError as e:
- logger.warning(f"无法获取聊天 {chat_id} 的信息: 无效的ID格式 - {str(e)}")
+ logger.warning(f"Unable to get info for chat {chat_id}: Invalid ID format - {str(e)}")
skipped_count += 1
continue
except Exception as e:
- logger.warning(f"无法获取聊天 {chat_id} 的信息: {str(e)}")
+ logger.warning(f"Unable to get info for chat {chat_id}: {str(e)}")
skipped_count += 1
continue
-
+
except Exception as e:
- logger.error(f"处理聊天 {chat.telegram_chat_id} 时出错: {str(e)}")
+ logger.error(f"Error processing chat {chat.telegram_chat_id}: {str(e)}")
error_count += 1
continue
-
- # 每个聊天处理后暂停一会,避免请求过于频繁
+
+ # Pause briefly after processing each chat to avoid excessive request frequency
await asyncio.sleep(1)
-
- logger.info(f"聊天信息更新完成。总计: {total_chats}, 更新: {updated_count}, 跳过: {skipped_count}, 错误: {error_count}")
-
+
+ logger.info(f"Chat info update completed. Total: {total_chats}, Updated: {updated_count}, Skipped: {skipped_count}, Errors: {error_count}")
+
except Exception as e:
- logger.error(f"更新聊天信息时出错: {str(e)}")
- logger.error(f"错误详情: {traceback.format_exc()}")
+ logger.error(f"Error updating chat info: {str(e)}")
+ logger.error(f"Error details: {traceback.format_exc()}")
finally:
session.close()
-
+
def stop(self):
- """停止定时任务"""
+ """Stop the scheduled task"""
if self.task:
self.task.cancel()
- logger.info("聊天信息更新任务已停止")
\ No newline at end of file
+ logger.info("Chat info update task has been stopped")
From 32b97bcba4d883b8c49150aef96625c23421b06c Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:12:50 +0000
Subject: [PATCH 19/76] Translate Chinese to English in filters/rss_filter.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
filters/rss_filter.py | 628 +++++++++++++++++++++---------------------
1 file changed, 314 insertions(+), 314 deletions(-)
diff --git a/filters/rss_filter.py b/filters/rss_filter.py
index df23320..a8f0404 100644
--- a/filters/rss_filter.py
+++ b/filters/rss_filter.py
@@ -17,77 +17,77 @@
class RSSFilter(BaseFilter):
"""
- RSS过滤器,用于将符合条件的消息添加到RSS订阅源中
+ RSS filter, used to add messages that meet conditions to the RSS feed
"""
-
+
def __init__(self):
super().__init__()
self.rss_host = RSS_HOST
self.rss_port = RSS_PORT
self.rss_base_url = f"http://{self.rss_host}:{self.rss_port}"
-
- # 使用统一的路径常量
+
+ # Use unified path constants
self.rss_media_path = RSS_MEDIA_DIR
self.temp_dir = TEMP_DIR
-
- logger.info(f"RSS媒体文件根目录: {self.rss_media_path}")
- logger.info(f"临时文件存储路径: {self.temp_dir}")
-
- # 确保媒体文件存储根目录存在
+
+ logger.info(f"RSS media file root directory: {self.rss_media_path}")
+ logger.info(f"Temporary file storage path: {self.temp_dir}")
+
+ # Ensure the media file storage root directory exists
Path(self.rss_media_path).mkdir(parents=True, exist_ok=True)
-
+
def _get_rule_media_path(self, rule_id):
- """获取规则特定的媒体目录"""
+ """Get the rule-specific media directory"""
return get_rule_media_dir(rule_id)
-
+
async def _process(self, context):
- """处理RSS过滤器逻辑"""
-
+ """Process RSS filter logic"""
+
if not RSS_ENABLED:
- logger.info("RSS未启用,跳过RSS处理")
+ logger.info("RSS is not enabled, skipping RSS processing")
return True
-
+
if not context.should_forward:
return False
-
+
db_ops = await get_db_ops()
session = get_session()
rss_config = await db_ops.get_rss_config(session, context.rule.id)
- logger.info(f"规则ID: {context.rule.id}")
- logger.info(f"RSS配置: {rss_config}")
+ logger.info(f"Rule ID: {context.rule.id}")
+ logger.info(f"RSS config: {rss_config}")
- # 检查RSS配置是否存在
+ # Check if RSS configuration exists
if rss_config is None:
- logger.error(f"找不到规则ID为 {context.rule.id} 的RSS配置,跳过RSS处理")
+ logger.error(f"Cannot find RSS configuration for rule ID {context.rule.id}, skipping RSS processing")
session.close()
return True
-
- # 检查是否启用RSS
+
+ # Check if RSS is enabled
if not rss_config.enable_rss:
- logger.info(f"规则ID为 {context.rule.id} 的RSS未启用,跳过RSS处理")
+ logger.info(f"RSS is not enabled for rule ID {context.rule.id}, skipping RSS processing")
session.close()
return True
- # 执行RSS规则前,先确保媒体文件已经下载
- # 媒体组消息需要特殊处理
+ # Before executing RSS rule, ensure media files have been downloaded
+ # Media group messages need special handling
if context.is_media_group:
rule = context.rule
await self._process_media_group(context, rule)
else:
- # 获取消息和规则
+ # Get message and rule
message = context.event.message
client = context.client
rule = context.rule
-
+
try:
- # 准备条目数据
+ # Prepare entry data
entry_data = await self._prepare_entry_data(client, message, rule, context)
-
- # 如果准备数据失败,记录错误并尝试生成简单的数据
+
+ # If data preparation failed, log error and try to generate simple data
if entry_data is None:
- logger.warning("生成RSS条目数据失败,尝试创建简单数据")
- # 尝试从消息中提取最基本的信息
- message_text = getattr(message, 'text', '') or getattr(message, 'caption', '') or '文件消息'
+ logger.warning("Failed to generate RSS entry data, attempting to create simple data")
+ # Try to extract the most basic information from the message
+ message_text = getattr(message, 'text', '') or getattr(message, 'caption', '') or 'File message'
entry_data = {
"id": str(message.id),
"title": message_text[:20] + ('...' if len(message_text) > 20 else ''),
@@ -97,8 +97,8 @@ async def _process(self, context):
"link": "",
"media": []
}
-
- # 如果消息有媒体,尝试处理
+
+ # If the message has media, try to process it
if hasattr(message, 'media') and message.media:
media_info = await self._process_media(client, message, context)
if media_info:
@@ -106,56 +106,56 @@ async def _process(self, context):
entry_data["media"].extend(media_info)
else:
entry_data["media"].append(media_info)
-
- # 发送到RSS服务
+
+ # Send to RSS service
if entry_data:
success = await self._send_to_rss_service(rule.id, entry_data)
if success:
- logger.info(f"成功将消息添加到规则 {rule.id} 的RSS订阅源")
+ logger.info(f"Successfully added message to RSS feed for rule {rule.id}")
else:
- logger.error(f"无法将消息添加到规则 {rule.id} 的RSS订阅源")
+ logger.error(f"Failed to add message to RSS feed for rule {rule.id}")
else:
- logger.error("无法生成有效的RSS条目数据")
-
+ logger.error("Unable to generate valid RSS entry data")
+
except Exception as e:
- logger.error(f"RSS处理时出错: {str(e)}")
-
+ logger.error(f"Error during RSS processing: {str(e)}")
+
if rule.only_rss:
- logger.info('只转发到RSS,RSS过滤器已完成,结束过滤链')
+ logger.info('Only forwarding to RSS, RSS filter completed, ending filter chain')
return False
-
+
return True
-
+
async def _prepare_entry_data(self, client, message, rule, context=None):
- """准备RSS条目数据"""
+ """Prepare RSS entry data"""
try:
- # 获取标题(使用自定义方法)
+ # Get title (using custom method)
title = self._get_message_title(message)
-
- # 安全获取消息内容
+
+ # Safely get message content
content = ""
if hasattr(message, 'text') and message.text:
content = message.text
elif hasattr(message, 'caption') and message.caption:
content = message.caption
-
- # 获取发送人名称
+
+ # Get sender name
author = await self._get_sender_name(client, message)
-
- # 获取消息链接(如果有)
+
+ # Get message link (if available)
link = self._get_message_link(message)
-
- # 获取媒体(如果有)
+
+ # Get media (if available)
media_list = []
-
- # 处理媒体组消息
+
+ # Process media group messages
if context and hasattr(context, "is_media_group") and context.is_media_group:
- logger.debug("处理媒体组消息")
- # 由于媒体组已经在其他地方处理,这里不再重复处理
- # 仅记录
- logger.debug("媒体组在其他地方处理")
+ logger.debug("Processing media group messages")
+ # Since the media group has already been processed elsewhere, no need to process again here
+ # Only log
+ logger.debug("Media group processed elsewhere")
else:
- # 处理单个消息的媒体
+ # Process single message media
media_info = await self._process_media(client, message, context)
if media_info:
if isinstance(media_info, list):
@@ -163,28 +163,28 @@ async def _prepare_entry_data(self, client, message, rule, context=None):
else:
media_list.append(media_info)
elif media_list:
- logger.debug(f"_process_media返回了多个媒体: {len(media_list)}")
-
- # 检查媒体是否在skipped_media列表中
+ logger.debug(f"_process_media returned multiple media: {len(media_list)}")
+
+ # Check if media is in the skipped_media list
if context and hasattr(context, 'skipped_media') and context.skipped_media:
for skipped_msg, size, name in context.skipped_media:
if skipped_msg.id == message.id:
- logger.info(f"媒体文件 {name or ''} (大小: {size}MB) 已在skipped_media列表中,添加标记到条目数据")
- # 可以选择在content中添加标记,表明该媒体因大小超限而被跳过
- note = f"\n\n[注意:包含超过大小限制的媒体文件 {name or ''},大小: {size}MB]"
+ logger.info(f"Media file {name or ''} (size: {size}MB) is in the skipped_media list, adding marker to entry data")
+ # Optionally add a marker in content indicating the media was skipped due to size limit
+ note = f"\n\n[Note: Contains media file exceeding size limit {name or ''}, size: {size}MB]"
if hasattr(message, 'text') and message.text:
content = message.text + note
elif hasattr(message, 'caption') and message.caption:
content = message.caption + note
else:
content = note.strip()
-
- # 尝试记录媒体信息
+
+ # Try to log media information
if media_list:
for i, media in enumerate(media_list):
- logger.debug(f"媒体{i+1}: {media.get('filename', 'unknown')}, 类型: {media.get('type', 'unknown')}, 原始文件名: {media.get('original_name', 'unknown')}")
-
- # 构建条目数据
+ logger.debug(f"Media {i+1}: {media.get('filename', 'unknown')}, type: {media.get('type', 'unknown')}, original filename: {media.get('original_name', 'unknown')}")
+
+ # Build entry data
entry_data = {
"id": str(message.id),
"title": title,
@@ -196,147 +196,147 @@ async def _prepare_entry_data(self, client, message, rule, context=None):
"original_link": context.original_link,
"sender_info": context.sender_info,
}
-
+
return entry_data
-
+
except Exception as e:
- logger.error(f"准备RSS条目数据时出错: {str(e)}")
+ logger.error(f"Error preparing RSS entry data: {str(e)}")
return None
-
+
def _get_message_title(self, message):
- """获取消息标题"""
- # 使用消息的前20个字符作为标题
+ """Get message title"""
+ # Use the first 20 characters of the message as the title
text = ""
if hasattr(message, 'text') and message.text:
text = message.text
elif hasattr(message, 'caption') and message.caption:
text = message.caption
-
+
title = text.split('\n')[0][:20].strip() + "..." if text and len(text.split('\n')[0]) >= 20 else text.split('\n')[0].strip() if text else ""
-
- # 如果标题为空,使用默认标题
+
+ # If the title is empty, use a default title
if not title:
- # 检测各种媒体类型
+ # Detect various media types
has_photo = hasattr(message, 'photo') and message.photo
has_video = hasattr(message, 'video') and message.video
has_document = hasattr(message, 'document') and message.document
has_audio = hasattr(message, 'audio') and message.audio
has_voice = hasattr(message, 'voice') and message.voice
-
+
if has_photo:
- title = "图片消息"
+ title = "Photo message"
elif has_video:
- title = "视频消息"
+ title = "Video message"
elif has_document:
doc_name = ""
if hasattr(message.document, 'file_name') and message.document.file_name:
doc_name = message.document.file_name
- title = f"文件: {doc_name}" if doc_name else "文件消息"
+ title = f"File: {doc_name}" if doc_name else "File message"
elif has_audio:
audio_name = ""
if hasattr(message.audio, 'file_name') and message.audio.file_name:
audio_name = message.audio.file_name
- title = f"音频: {audio_name}" if audio_name else "音频消息"
+ title = f"Audio: {audio_name}" if audio_name else "Audio message"
elif has_voice:
- title = "语音消息"
+ title = "Voice message"
else:
- title = "新消息"
-
+ title = "New message"
+
return title
-
+
async def _get_sender_name(self, client, message):
- """获取发送者名称"""
+ """Get sender name"""
try:
- # 检查是否是频道消息
+ # Check if it's a channel message
if hasattr(message, 'sender_chat') and message.sender_chat:
return message.sender_chat.title
- # 检查是否有发送者信息
+ # Check if there is sender information
elif hasattr(message, 'from_user') and message.from_user:
return message.from_user.first_name + (f" {message.from_user.last_name}" if message.from_user.last_name else "")
- # 尝试从聊天获取名称
+ # Try to get name from chat
elif hasattr(message, 'chat') and message.chat:
if hasattr(message.chat, 'title') and message.chat.title:
return message.chat.title
elif hasattr(message.chat, 'first_name'):
return message.chat.first_name + (f" {message.chat.last_name}" if hasattr(message.chat, 'last_name') and message.chat.last_name else "")
- return "未知用户"
+ return "Unknown user"
except Exception as e:
- logger.error(f"获取发送者名称时出错: {str(e)}")
- return "未知用户"
-
+ logger.error(f"Error getting sender name: {str(e)}")
+ return "Unknown user"
+
def _get_message_link(self, message):
- """获取消息链接"""
+ """Get message link"""
try:
if hasattr(message, 'chat') and message.chat:
chat_id = getattr(message.chat, 'id', None)
username = getattr(message.chat, 'username', None)
message_id = getattr(message, 'id', None)
-
+
if message_id is None:
return ""
-
+
if username:
return f"https://t.me/{username}/{message_id}"
elif chat_id:
- # 使用chat_id创建链接
+ # Create link using chat_id
chat_id_str = str(chat_id)
- # 移除前导负号(如果有)
+ # Remove leading negative sign (if present)
if chat_id_str.startswith('-100'):
- chat_id_str = chat_id_str[4:] # 去掉'-100'
+ chat_id_str = chat_id_str[4:] # Remove '-100'
elif chat_id_str.startswith('-'):
- chat_id_str = chat_id_str[1:] # 去掉'-'
+ chat_id_str = chat_id_str[1:] # Remove '-'
return f"https://t.me/c/{chat_id_str}/{message_id}"
return ""
except Exception as e:
- logger.error(f"获取消息链接时出错: {str(e)}")
+ logger.error(f"Error getting message link: {str(e)}")
return ""
-
+
async def _process_media(self, client, message, context=None, rule_id=None):
- """处理媒体内容"""
+ """Process media content"""
media_list = []
-
+
try:
- # 检查消息是否在skipped_media列表中
+ # Check if the message is in the skipped_media list
if context and hasattr(context, 'skipped_media') and context.skipped_media:
for skipped_msg, size, name in context.skipped_media:
if skipped_msg.id == message.id:
- logger.info(f"媒体文件 {name or ''} (大小: {size}MB) 已在skipped_media列表中,RSS过滤器跳过下载")
+ logger.info(f"Media file {name or ''} (size: {size}MB) is in the skipped_media list, RSS filter skipping download")
return media_list
- # 处理文档类型
+ # Process document type
if hasattr(message, 'document') and message.document:
- # 获取原始文件名
+ # Get original filename
original_name = None
for attr in message.document.attributes:
if hasattr(attr, 'file_name'):
original_name = attr.file_name
break
-
- # 生成文件名
+
+ # Generate filename
message_id = getattr(message, 'id', 'unknown')
file_name = original_name if original_name else f"document_{message_id}"
file_name = self._sanitize_filename(file_name)
-
- # 获取规则ID,优先使用传入的rule_id
+
+ # Get rule ID, prefer the passed-in rule_id
current_rule_id = rule_id
if current_rule_id is None and context and hasattr(context, 'rule') and hasattr(context.rule, 'id'):
current_rule_id = context.rule.id
-
- # 使用规则特定的媒体目录
+
+ # Use rule-specific media directory
rule_media_path = self._get_rule_media_path(current_rule_id) if current_rule_id else self.rss_media_path
-
- # 下载文件
+
+ # Download file
local_path = os.path.join(rule_media_path, file_name)
try:
if not os.path.exists(local_path):
await message.download_media(local_path)
- logger.info(f"下载媒体文件到: {local_path}")
-
- # 获取文件大小和MIME类型
+ logger.info(f"Downloaded media file to: {local_path}")
+
+ # Get file size and MIME type
file_size = os.path.getsize(local_path)
mime_type = message.document.mime_type or mimetypes.guess_type(file_name)[0] or "application/octet-stream"
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{current_rule_id}/{file_name}" if current_rule_id else f"/media/{file_name}",
"type": mime_type,
@@ -345,77 +345,77 @@ async def _process_media(self, client, message, context=None, rule_id=None):
"original_name": original_name or file_name
}
media_list.append(media_info)
- logger.info(f"添加文档到RSS: {file_name}, 原始文件名: {original_name or '未知'}")
+ logger.info(f"Added document to RSS: {file_name}, original filename: {original_name or 'unknown'}")
except Exception as e:
- logger.error(f"处理文档时出错: {str(e)}")
-
- # 处理图片类型
+ logger.error(f"Error processing document: {str(e)}")
+
+ # Process photo type
elif hasattr(message, 'photo') and message.photo:
message_id = getattr(message, 'id', 'unknown')
-
- # 获取规则ID,优先使用传入的rule_id
+
+ # Get rule ID, prefer the passed-in rule_id
current_rule_id = rule_id
if current_rule_id is None and context and hasattr(context, 'rule') and hasattr(context.rule, 'id'):
current_rule_id = context.rule.id
-
- # 使用规则特定的媒体目录
+
+ # Use rule-specific media directory
rule_media_path = self._get_rule_media_path(current_rule_id) if current_rule_id else self.rss_media_path
local_path = os.path.join(rule_media_path, f"photo_{message_id}.jpg")
-
+
try:
if not os.path.exists(local_path):
await message.download_media(local_path)
- logger.info(f"下载图片到: {local_path}")
-
- # 获取文件大小
+ logger.info(f"Downloaded photo to: {local_path}")
+
+ # Get file size
file_size = os.path.getsize(local_path)
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{current_rule_id}/{f'photo_{message_id}.jpg'}" if current_rule_id else f"/media/{f'photo_{message_id}.jpg'}",
"type": "image/jpeg",
"size": file_size,
"filename": f"photo_{message_id}.jpg",
- "original_name": "photo.jpg" # 照片没有原始文件名
+ "original_name": "photo.jpg" # Photos don't have original filenames
}
media_list.append(media_info)
- logger.info(f"添加图片到RSS: {f'photo_{message_id}.jpg'}")
+ logger.info(f"Added photo to RSS: {f'photo_{message_id}.jpg'}")
except Exception as e:
- logger.error(f"处理图片时出错: {str(e)}")
-
- # 处理视频类型
+ logger.error(f"Error processing photo: {str(e)}")
+
+ # Process video type
elif hasattr(message, 'video') and message.video:
message_id = getattr(message, 'id', 'unknown')
-
- # 获取原始文件名
+
+ # Get original filename
original_name = None
for attr in message.video.attributes:
if hasattr(attr, 'file_name'):
original_name = attr.file_name
break
-
+
file_name = original_name if original_name else f"video_{message_id}.mp4"
file_name = self._sanitize_filename(file_name)
-
- # 获取规则ID,优先使用传入的rule_id
+
+ # Get rule ID, prefer the passed-in rule_id
current_rule_id = rule_id
if current_rule_id is None and context and hasattr(context, 'rule') and hasattr(context.rule, 'id'):
current_rule_id = context.rule.id
-
- # 使用规则特定的媒体目录
+
+ # Use rule-specific media directory
rule_media_path = self._get_rule_media_path(current_rule_id) if current_rule_id else self.rss_media_path
local_path = os.path.join(rule_media_path, file_name)
-
+
try:
if not os.path.exists(local_path):
await message.download_media(local_path)
- logger.info(f"下载视频到: {local_path}")
-
- # 获取文件大小和MIME类型
+ logger.info(f"Downloaded video to: {local_path}")
+
+ # Get file size and MIME type
file_size = os.path.getsize(local_path)
mime_type = message.video.mime_type or "video/mp4"
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{current_rule_id}/{file_name}" if current_rule_id else f"/media/{file_name}",
"type": mime_type,
@@ -424,43 +424,43 @@ async def _process_media(self, client, message, context=None, rule_id=None):
"original_name": original_name or file_name
}
media_list.append(media_info)
- logger.info(f"添加视频到RSS: {file_name}")
+ logger.info(f"Added video to RSS: {file_name}")
except Exception as e:
- logger.error(f"处理视频时出错: {str(e)}")
-
- # 处理音频类型
+ logger.error(f"Error processing video: {str(e)}")
+
+ # Process audio type
elif hasattr(message, 'audio') and message.audio:
message_id = getattr(message, 'id', 'unknown')
-
- # 获取原始文件名
+
+ # Get original filename
original_name = None
for attr in message.audio.attributes:
if hasattr(attr, 'file_name'):
original_name = attr.file_name
break
-
+
file_name = original_name if original_name else f"audio_{message_id}.mp3"
file_name = self._sanitize_filename(file_name)
-
- # 获取规则ID,优先使用传入的rule_id
+
+ # Get rule ID, prefer the passed-in rule_id
current_rule_id = rule_id
if current_rule_id is None and context and hasattr(context, 'rule') and hasattr(context.rule, 'id'):
current_rule_id = context.rule.id
-
- # 使用规则特定的媒体目录
+
+ # Use rule-specific media directory
rule_media_path = self._get_rule_media_path(current_rule_id) if current_rule_id else self.rss_media_path
local_path = os.path.join(rule_media_path, file_name)
-
+
try:
if not os.path.exists(local_path):
await message.download_media(local_path)
- logger.info(f"下载音频到: {local_path}")
-
- # 获取文件大小和MIME类型
+ logger.info(f"Downloaded audio to: {local_path}")
+
+ # Get file size and MIME type
file_size = os.path.getsize(local_path)
mime_type = message.audio.mime_type or "audio/mpeg"
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{current_rule_id}/{file_name}" if current_rule_id else f"/media/{file_name}",
"type": mime_type,
@@ -469,138 +469,138 @@ async def _process_media(self, client, message, context=None, rule_id=None):
"original_name": original_name or file_name
}
media_list.append(media_info)
- logger.info(f"添加音频到RSS: {file_name}")
+ logger.info(f"Added audio to RSS: {file_name}")
except Exception as e:
- logger.error(f"处理音频时出错: {str(e)}")
-
- # 处理语音消息
+ logger.error(f"Error processing audio: {str(e)}")
+
+ # Process voice message
elif hasattr(message, 'voice') and message.voice:
message_id = getattr(message, 'id', 'unknown')
file_name = f"voice_{message_id}.ogg"
-
- # 获取规则ID,优先使用传入的rule_id
+
+ # Get rule ID, prefer the passed-in rule_id
current_rule_id = rule_id
if current_rule_id is None and context and hasattr(context, 'rule') and hasattr(context.rule, 'id'):
current_rule_id = context.rule.id
-
- # 使用规则特定的媒体目录
+
+ # Use rule-specific media directory
rule_media_path = self._get_rule_media_path(current_rule_id) if current_rule_id else self.rss_media_path
local_path = os.path.join(rule_media_path, file_name)
-
+
try:
if not os.path.exists(local_path):
await message.download_media(local_path)
- logger.info(f"下载语音到: {local_path}")
-
- # 获取文件大小
+ logger.info(f"Downloaded voice to: {local_path}")
+
+ # Get file size
file_size = os.path.getsize(local_path)
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{current_rule_id}/{file_name}" if current_rule_id else f"/media/{file_name}",
"type": "audio/ogg",
"size": file_size,
"filename": file_name,
- "original_name": "voice.ogg" # 语音消息没有原始文件名
+ "original_name": "voice.ogg" # Voice messages don't have original filenames
}
media_list.append(media_info)
- logger.info(f"添加语音到RSS: {file_name}")
+ logger.info(f"Added voice to RSS: {file_name}")
except Exception as e:
- logger.error(f"处理语音时出错: {str(e)}")
-
+ logger.error(f"Error processing voice: {str(e)}")
+
except Exception as e:
- logger.error(f"处理媒体内容时出错: {str(e)}")
-
+ logger.error(f"Error processing media content: {str(e)}")
+
return media_list
-
+
def _sanitize_filename(self, filename):
- """处理文件名,去除不合法字符"""
- # 替换Windows和Unix系统不支持的文件名字符
+ """Process filename, remove invalid characters"""
+ # Replace filename characters not supported by Windows and Unix systems
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
filename = filename.replace(char, '_')
return filename
-
+
async def _send_to_rss_service(self, rule_id, entry_data):
- """发送数据到RSS服务"""
+ """Send data to RSS service"""
try:
url = f"{self.rss_base_url}/api/entries/{rule_id}/add"
-
- # 记录要发送的数据(只记录非二进制数据)
+
+ # Log the data to be sent (only log non-binary data)
debug_data = entry_data.copy()
if "media" in debug_data:
media_files = []
for media in debug_data["media"]:
if isinstance(media, dict):
- original_name = media.get("original_name", "未知")
- filename = media.get("filename", "未知")
+ original_name = media.get("original_name", "unknown")
+ filename = media.get("filename", "unknown")
media_files.append(f"{original_name}({filename})")
else:
media_files.append(str(media))
- debug_data["media"] = f"{len(debug_data['media'])} 个媒体文件: {', '.join(media_files)}"
- logger.info(f"发送到RSS服务: {url}, 数据: {debug_data}")
-
+ debug_data["media"] = f"{len(debug_data['media'])} media files: {', '.join(media_files)}"
+ logger.info(f"Sending to RSS service: {url}, data: {debug_data}")
+
async with aiohttp.ClientSession() as session:
async with session.post(url, json=entry_data) as response:
response_text = await response.text()
if response.status != 200:
- logger.error(f"发送到RSS服务失败: {response.status} - {response_text}")
+ logger.error(f"Failed to send to RSS service: {response.status} - {response_text}")
return False
-
- logger.info(f"成功发送到RSS服务, 规则ID: {rule_id}, 响应: {response_text}")
+
+ logger.info(f"Successfully sent to RSS service, rule ID: {rule_id}, response: {response_text}")
return True
-
+
except Exception as e:
- logger.error(f"发送到RSS服务时出错: {str(e)}")
+ logger.error(f"Error sending to RSS service: {str(e)}")
return False
-
+
async def _process_media_group(self, context, rule):
- """处理媒体组消息"""
+ """Process media group messages"""
try:
- # 获取规则ID
+ # Get rule ID
rule_id = rule.id
-
- # 获取规则特定的媒体目录
+
+ # Get rule-specific media directory
rule_media_path = self._get_rule_media_path(rule_id)
-
- # 获取已下载的本地媒体文件
+
+ # Get already downloaded local media files
local_media_files = []
if hasattr(context, 'media_files') and context.media_files:
local_media_files = context.media_files
-
- # 记录已下载的媒体文件数量
- logger.info(f"处理媒体组消息,已下载的媒体文件: {len(local_media_files)}")
-
- # 准备媒体列表
+
+ # Log the number of downloaded media files
+ logger.info(f"Processing media group messages, downloaded media files: {len(local_media_files)}")
+
+ # Prepare media list
media_list = []
-
- # 如果有已下载的媒体文件,使用它们
+
+ # If there are already downloaded media files, use them
if local_media_files:
- # 使用已下载的媒体文件
+ # Use already downloaded media files
for local_file in local_media_files:
try:
- # 从文件名猜测媒体类型
+ # Guess media type from filename
media_type = mimetypes.guess_type(local_file)[0] or "application/octet-stream"
filename = os.path.basename(local_file)
-
- # 复制文件到规则特定的RSS媒体目录
+
+ # Copy file to rule-specific RSS media directory
target_path = os.path.join(rule_media_path, filename)
if not os.path.exists(target_path):
shutil.copy2(local_file, target_path)
- logger.info(f"复制媒体文件到: {target_path}")
-
- # 获取文件大小
+ logger.info(f"Copied media file to: {target_path}")
+
+ # Get file size
file_size = os.path.getsize(target_path)
-
- # 尝试从原始消息中获取文件名
+
+ # Try to get filename from original message
original_name = None
for msg in context.media_group_messages:
if hasattr(msg, 'document') and msg.document:
original_name = getattr(msg.document, 'file_name', None)
if original_name:
break
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{rule_id}/{filename}",
"type": media_type,
@@ -609,131 +609,131 @@ async def _process_media_group(self, context, rule):
"original_name": original_name or filename
}
media_list.append(media_info)
- logger.info(f"添加媒体组文件到RSS: {filename}, 原始文件名: {original_name or '未知'}")
+ logger.info(f"Added media group file to RSS: {filename}, original filename: {original_name or 'unknown'}")
except Exception as e:
- logger.error(f"处理媒体组文件时出错: {str(e)}")
+ logger.error(f"Error processing media group file: {str(e)}")
else:
- # 没有已下载的媒体文件,尝试直接从媒体组消息下载
+ # No already downloaded media files, try to download directly from media group messages
if hasattr(context, 'media_group_messages') and context.media_group_messages:
- logger.warning("媒体组没有已下载的文件,尝试从media_group_messages获取")
-
- # 直接处理媒体组消息
+ logger.warning("Media group has no downloaded files, trying to get from media_group_messages")
+
+ # Process media group messages directly
for msg in context.media_group_messages:
try:
- # 检查消息是否在skipped_media列表中
+ # Check if the message is in the skipped_media list
if hasattr(context, 'skipped_media') and context.skipped_media:
skip_msg = False
for skipped_msg, size, name in context.skipped_media:
if skipped_msg.id == msg.id:
- logger.info(f"媒体组中的媒体文件 {name or ''} (大小: {size}MB) 已在skipped_media列表中,RSS过滤器跳过下载")
+ logger.info(f"Media file {name or ''} (size: {size}MB) in media group is in the skipped_media list, RSS filter skipping download")
skip_msg = True
break
if skip_msg:
continue
- # 处理图片类型
+ # Process photo type
if hasattr(msg, 'photo') and msg.photo:
message_id = getattr(msg, 'id', 'unknown')
file_name = f"photo_{message_id}.jpg"
-
+
try:
- # 使用规则特定的媒体目录
+ # Use rule-specific media directory
local_path = os.path.join(rule_media_path, file_name)
-
- # 如果文件已存在且大小正常,跳过下载
+
+ # If the file already exists and has valid size, skip download
if os.path.exists(local_path) and os.path.getsize(local_path) > 0:
- logger.info(f"媒体文件已存在,跳过下载: {local_path}")
+ logger.info(f"Media file already exists, skipping download: {local_path}")
else:
try:
await msg.download_media(local_path)
- logger.info(f"直接下载图片到: {local_path}")
+ logger.info(f"Directly downloaded photo to: {local_path}")
except Exception as e:
if "file reference has expired" in str(e):
- logger.warning(f"文件引用已过期,尝试重新获取消息")
+ logger.warning(f"File reference has expired, trying to re-fetch message")
try:
- # 尝试重新获取消息
+ # Try to re-fetch the message
refreshed_msg = await context.client.get_messages(
msg.chat_id, ids=msg.id
)
if refreshed_msg:
await refreshed_msg.download_media(local_path)
- logger.info(f"成功重新下载图片到: {local_path}")
+ logger.info(f"Successfully re-downloaded photo to: {local_path}")
else:
- logger.error("无法重新获取消息")
+ logger.error("Unable to re-fetch message")
continue
except Exception as refresh_error:
- logger.error(f"重新获取消息时出错: {str(refresh_error)}")
+ logger.error(f"Error re-fetching message: {str(refresh_error)}")
continue
else:
- logger.error(f"下载媒体组图片时出错: {str(e)}")
+ logger.error(f"Error downloading media group photo: {str(e)}")
continue
-
- # 获取文件大小
+
+ # Get file size
if os.path.exists(local_path):
file_size = os.path.getsize(local_path)
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{rule_id}/{file_name}",
"type": "image/jpeg",
"size": file_size,
"filename": file_name,
- "original_name": "photo.jpg" # 照片没有原始文件名
+ "original_name": "photo.jpg" # Photos don't have original filenames
}
media_list.append(media_info)
- logger.info(f"添加媒体组图片到RSS: {file_name}")
+ logger.info(f"Added media group photo to RSS: {file_name}")
except Exception as e:
- logger.error(f"处理媒体组图片时出错: {str(e)}")
+ logger.error(f"Error processing media group photo: {str(e)}")
elif hasattr(msg, 'document') and msg.document:
- # 获取消息ID,用于生成默认文件名
+ # Get message ID for generating default filename
message_id = getattr(msg, 'id', 'unknown')
original_name = None
for attr in msg.document.attributes:
if hasattr(attr, 'file_name'):
original_name = attr.file_name
break
-
+
file_name = original_name if original_name else f"document_{message_id}"
file_name = self._sanitize_filename(file_name)
-
+
try:
- # 使用规则特定的媒体目录
+ # Use rule-specific media directory
local_path = os.path.join(rule_media_path, file_name)
-
- # 如果文件已存在且大小正常,跳过下载
+
+ # If the file already exists and has valid size, skip download
if os.path.exists(local_path) and os.path.getsize(local_path) > 0:
- logger.info(f"媒体文件已存在,跳过下载: {local_path}")
+ logger.info(f"Media file already exists, skipping download: {local_path}")
else:
try:
await msg.download_media(local_path)
- logger.info(f"直接下载文档到: {local_path}")
+ logger.info(f"Directly downloaded document to: {local_path}")
except Exception as e:
if "file reference has expired" in str(e):
- logger.warning(f"文件引用已过期,尝试重新获取消息")
+ logger.warning(f"File reference has expired, trying to re-fetch message")
try:
- # 尝试重新获取消息
+ # Try to re-fetch the message
refreshed_msg = await context.client.get_messages(
msg.chat_id, ids=msg.id
)
if refreshed_msg:
await refreshed_msg.download_media(local_path)
- logger.info(f"成功重新下载文档到: {local_path}")
+ logger.info(f"Successfully re-downloaded document to: {local_path}")
else:
- logger.error("无法重新获取消息")
+ logger.error("Unable to re-fetch message")
continue
except Exception as refresh_error:
- logger.error(f"重新获取消息时出错: {str(refresh_error)}")
+ logger.error(f"Error re-fetching message: {str(refresh_error)}")
continue
else:
- logger.error(f"下载媒体组文档时出错: {str(e)}")
+ logger.error(f"Error downloading media group document: {str(e)}")
continue
-
- # 获取文件大小和MIME类型
+
+ # Get file size and MIME type
if os.path.exists(local_path):
file_size = os.path.getsize(local_path)
mime_type = msg.document.mime_type or mimetypes.guess_type(file_name)[0] or "application/octet-stream"
-
- # 添加到媒体列表,使用规则特定的URL
+
+ # Add to media list, using rule-specific URL
media_info = {
"url": f"/media/{rule_id}/{file_name}",
"type": mime_type,
@@ -742,32 +742,32 @@ async def _process_media_group(self, context, rule):
"original_name": original_name or file_name
}
media_list.append(media_info)
- logger.info(f"添加媒体组文档到RSS: {file_name}, 原始文件名: {original_name or '未知'}")
+ logger.info(f"Added media group document to RSS: {file_name}, original filename: {original_name or 'unknown'}")
except Exception as e:
- logger.error(f"处理媒体组文档时出错: {str(e)}")
-
- # 其他媒体类型处理可以类似添加
-
+ logger.error(f"Error processing media group document: {str(e)}")
+
+ # Other media types can be handled similarly
+
except Exception as e:
- logger.error(f"处理媒体组消息时出错: {str(e)}")
-
- # 准备条目数据
- # 获取消息文本内容
+ logger.error(f"Error processing media group message: {str(e)}")
+
+ # Prepare entry data
+ # Get message text content
message_text = context.message_text or ""
-
- # 构建标题:优先使用消息文本内容,没有文本内容时使用默认标题
+
+ # Build title: prefer message text content, use default title when there's no text content
if message_text.strip():
- # 使用第一行文本或前30个字符(以较短者为准)作为标题
+ # Use the first line of text or the first 30 characters (whichever is shorter) as the title
first_line = message_text.split('\n')[0].strip()
title = first_line[:30] + ('...' if len(first_line) > 30 else '')
else:
- # 没有文本内容时,使用默认标题
+ # When there's no text content, use default title
if media_list:
- title = f"媒体组消息 ({len(media_list)}个文件)"
+ title = f"Media group message ({len(media_list)} files)"
else:
- title = "媒体组消息"
-
- # 构建条目数据
+ title = "Media group message"
+
+ # Build entry data
entry_data = {
"id": str(context.event.message.id),
"title": title,
@@ -777,19 +777,19 @@ async def _process_media_group(self, context, rule):
"link": self._get_message_link(context.event.message),
"media": media_list
}
-
- # 记录媒体组条目数据
- logger.info(f"媒体组条目数据: 标题={title}, 媒体数量={len(media_list)}")
-
- # 如果有有效的媒体文件,添加到RSS订阅源
+
+ # Log media group entry data
+ logger.info(f"Media group entry data: title={title}, media count={len(media_list)}")
+
+ # If there are valid media files, add to RSS feed
if media_list:
await self._send_to_rss_service(rule.id, entry_data)
- logger.info(f"成功将媒体组消息添加到规则 {rule.id} 的RSS订阅源")
+ logger.info(f"Successfully added media group message to RSS feed for rule {rule.id}")
else:
- logger.warning("媒体组消息没有有效的媒体文件,跳过添加到RSS订阅源")
-
+ logger.warning("Media group message has no valid media files, skipping addition to RSS feed")
+
except Exception as e:
- logger.error(f"处理媒体组消息时出错: {str(e)}")
+ logger.error(f"Error processing media group message: {str(e)}")
return False
-
+
return True
From a2ccc4e273cad23cec781e23fc805a75c508f909 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:12:56 +0000
Subject: [PATCH 20/76] Translate Chinese to English in ai/base.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
ai/base.py | 28 ++++++++++++++--------------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/ai/base.py b/ai/base.py
index 7bd535c..92df68d 100644
--- a/ai/base.py
+++ b/ai/base.py
@@ -2,29 +2,29 @@
from typing import Optional, Dict, Any, List
class BaseAIProvider(ABC):
- """AI提供者的基类"""
-
+ """Base class for AI providers"""
+
@abstractmethod
- async def process_message(self,
- message: str,
+ async def process_message(self,
+ message: str,
prompt: Optional[str] = None,
images: Optional[List[Dict[str, str]]] = None,
**kwargs) -> str:
"""
- 处理消息的抽象方法
-
+ Abstract method for processing messages
+
Args:
- message: 要处理的消息内容
- prompt: 可选的提示词
- images: 可选的图片列表,每个图片是一个字典,包含data和mime_type
- **kwargs: 其他参数
-
+ message: The message content to process
+ prompt: Optional prompt text
+ images: Optional list of images, each image is a dict containing data and mime_type
+ **kwargs: Other parameters
+
Returns:
- str: 处理后的消息
+ str: The processed message
"""
pass
-
+
@abstractmethod
async def initialize(self, **kwargs) -> None:
- """初始化AI提供者"""
+ """Initialize AI provider"""
pass
\ No newline at end of file
From 47ea54d4e1c9e3dbff1dcd850f805423bb7415a9 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:13:06 +0000
Subject: [PATCH 21/76] Translate Chinese to English in utils/common.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
utils/common.py | 440 ++++++++++++++++++++++++------------------------
1 file changed, 220 insertions(+), 220 deletions(-)
diff --git a/utils/common.py b/utils/common.py
index 71eb2f5..5a66b0e 100644
--- a/utils/common.py
+++ b/utils/common.py
@@ -16,11 +16,11 @@
logger = logging.getLogger(__name__)
async def get_main_module():
- """获取 main 模块"""
+ """Get the main module"""
try:
return sys.modules['__main__']
except KeyError:
- # 如果找不到 main 模块,尝试手动导入
+ # If the main module is not found, try to import it manually
spec = importlib.util.spec_from_file_location(
"main",
os.path.join(os.path.dirname(os.path.dirname(__file__)), "main.py")
@@ -30,58 +30,58 @@ async def get_main_module():
return main
async def get_user_client():
- """获取用户客户端"""
+ """Get the user client"""
main = await get_main_module()
return main.user_client
async def get_bot_client():
- """获取机器人客户端"""
+ """Get the bot client"""
main = await get_main_module()
return main.bot_client
async def get_db_ops():
- """获取 main.py 中的 db_ops 实例"""
+ """Get the db_ops instance from main.py"""
main = await get_main_module()
if main.db_ops is None:
main.db_ops = await main.init_db_ops()
return main.db_ops
async def get_user_id():
- """获取用户ID,确保环境变量已加载"""
+ """Get the user ID, ensuring environment variables are loaded"""
user_id_str = os.getenv('USER_ID')
if not user_id_str:
- logger.error('未设置 USER_ID 环境变量')
- raise ValueError('必须在 .env 文件中设置 USER_ID')
+ logger.error('USER_ID environment variable is not set')
+ raise ValueError('USER_ID must be set in the .env file')
return int(user_id_str)
async def get_current_rule(session, event):
- """获取当前选中的规则"""
+ """Get the currently selected rule"""
try:
- # 获取当前聊天
+ # Get the current chat
current_chat = await event.get_chat()
- logger.info(f'获取当前聊天: {current_chat.id}')
+ logger.info(f'Getting current chat: {current_chat.id}')
current_chat_db = session.query(Chat).filter(
Chat.telegram_chat_id == str(current_chat.id)
).first()
if not current_chat_db or not current_chat_db.current_add_id:
- logger.info('未找到当前聊天或未选择源聊天')
- await reply_and_delete(event,'请先使用 /switch 选择一个源聊天')
+ logger.info('Current chat not found or no source chat selected')
+ await reply_and_delete(event,'Please use /switch to select a source chat first')
return None
- logger.info(f'当前选中的源聊天ID: {current_chat_db.current_add_id}')
+ logger.info(f'Currently selected source chat ID: {current_chat_db.current_add_id}')
- # 查找对应的规则
+ # Find the corresponding rule
source_chat = session.query(Chat).filter(
Chat.telegram_chat_id == current_chat_db.current_add_id
).first()
if source_chat:
- logger.info(f'找到源聊天: {source_chat.name}')
+ logger.info(f'Found source chat: {source_chat.name}')
else:
- logger.error('未找到源聊天')
+ logger.error('Source chat not found')
return None
rule = session.query(ForwardRule).filter(
@@ -90,152 +90,152 @@ async def get_current_rule(session, event):
).first()
if not rule:
- logger.info('未找到对应的转发规则')
- await reply_and_delete(event,'转发规则不存在')
+ logger.info('Corresponding forward rule not found')
+ await reply_and_delete(event,'Forward rule does not exist')
return None
- logger.info(f'找到转发规则 ID: {rule.id}')
+ logger.info(f'Found forward rule ID: {rule.id}')
return rule, source_chat
except Exception as e:
- logger.error(f'获取当前规则时出错: {str(e)}')
+ logger.error(f'Error getting current rule: {str(e)}')
logger.exception(e)
- await reply_and_delete(event,'获取当前规则时出错,请检查日志')
+ await reply_and_delete(event,'Error getting current rule, please check the logs')
return None
async def get_all_rules(session, event):
- """获取当前聊天的所有规则"""
+ """Get all rules for the current chat"""
try:
- # 获取当前聊天
+ # Get the current chat
current_chat = await event.get_chat()
- logger.info(f'获取当前聊天: {current_chat.id}')
+ logger.info(f'Getting current chat: {current_chat.id}')
current_chat_db = session.query(Chat).filter(
Chat.telegram_chat_id == str(current_chat.id)
).first()
if not current_chat_db:
- logger.info('未找到当前聊天')
- await reply_and_delete(event,'当前聊天没有任何转发规则')
+ logger.info('Current chat not found')
+ await reply_and_delete(event,'No forward rules exist for the current chat')
return None
- logger.info(f'找到当前聊天数据库记录 ID: {current_chat_db.id}')
+ logger.info(f'Found current chat database record ID: {current_chat_db.id}')
- # 查找所有以当前聊天为目标的规则
+ # Find all rules targeting the current chat
rules = session.query(ForwardRule).filter(
ForwardRule.target_chat_id == current_chat_db.id
).all()
if not rules:
- logger.info('未找到任何转发规则')
- await reply_and_delete(event,'当前聊天没有任何转发规则')
+ logger.info('No forward rules found')
+ await reply_and_delete(event,'No forward rules exist for the current chat')
return None
- logger.info(f'找到 {len(rules)} 条转发规则')
+ logger.info(f'Found {len(rules)} forward rule(s)')
return rules
except Exception as e:
- logger.error(f'获取所有规则时出错: {str(e)}')
+ logger.error(f'Error getting all rules: {str(e)}')
logger.exception(e)
- await reply_and_delete(event,'获取规则时出错,请检查日志')
+ await reply_and_delete(event,'Error getting rules, please check the logs')
return None
-# 添加缓存字典
+# Add cache dictionary
_admin_cache = {}
-_CACHE_DURATION = timedelta(minutes=30) # 缓存30分钟
+_CACHE_DURATION = timedelta(minutes=30) # Cache for 30 minutes
async def get_channel_admins(client, chat_id):
- """获取频道管理员列表,带缓存机制"""
+ """Get channel admin list with caching mechanism"""
current_time = datetime.now()
-
- # 检查缓存是否存在且未过期
+
+ # Check if cache exists and is not expired
if chat_id in _admin_cache:
cache_data = _admin_cache[chat_id]
if current_time - cache_data['timestamp'] < _CACHE_DURATION:
return cache_data['admin_ids']
-
- # 缓存不存在或已过期,重新获取管理员列表
+
+ # Cache does not exist or has expired, re-fetch admin list
try:
admins = await client.get_participants(chat_id, filter=ChannelParticipantsAdmins)
admin_ids = [admin.id for admin in admins]
-
- # 更新缓存
+
+ # Update cache
_admin_cache[chat_id] = {
'admin_ids': admin_ids,
'timestamp': current_time
}
return admin_ids
except Exception as e:
- logger.error(f'获取频道管理员列表失败: {str(e)}')
+ logger.error(f'Failed to get channel admin list: {str(e)}')
return None
async def is_admin(event):
- """检查用户是否为频道/群组管理员
-
+ """Check if the user is a channel/group admin
+
Args:
- event: 事件对象
+ event: Event object
Returns:
- bool: 是否是管理员
+ bool: Whether the user is an admin
"""
try:
- # 获取所有机器人管理员列表
+ # Get all bot admin list
bot_admins = get_admin_list()
- # 检查是否有message属性
+ # Check if the message attribute exists
if not hasattr(event, 'message'):
- # 没有message属性,是回调处理
+ # No message attribute, this is a callback handler
if event.sender_id in bot_admins:
return True
else:
- logger.info(f'用户 {event.sender_id} 非管理员,操作已被忽略')
+ logger.info(f'User {event.sender_id} is not an admin, operation ignored')
return False
-
+
message = event.message
main = await get_main_module()
client = main.user_client
-
-
-
+
+
+
if message.is_channel and not message.is_group:
- # 获取频道管理员列表(使用缓存)
+ # Get channel admin list (using cache)
channel_admins = await get_channel_admins(client, event.chat_id)
if channel_admins is None:
return False
-
-
-
- # 检查机器人管理员是否在频道管理员列表中
+
+
+
+ # Check if bot admin is in the channel admin list
admin_in_channel = any(admin_id in channel_admins for admin_id in bot_admins)
if not admin_in_channel:
- logger.info(f'机器人管理员不在频道管理员列表中,已忽略')
+ logger.info(f'Bot admin is not in the channel admin list, ignored')
return False
return True
else:
- # 检查发送者ID
- user_id = event.sender_id # 使用 sender_id 作为主要ID来源
- logger.info(f'发送者ID:{user_id}')
-
+ # Check sender ID
+ user_id = event.sender_id # Use sender_id as the primary ID source
+ logger.info(f'Sender ID: {user_id}')
+
bot_admins = get_admin_list()
- # 检查是否是机器人管理员
+ # Check if the user is a bot admin
if user_id not in bot_admins:
- logger.info(f'非管理员的消息,已忽略')
+ logger.info(f'Non-admin message, ignored')
return False
return True
except Exception as e:
- logger.error(f"检查管理员权限时出错: {str(e)}")
+ logger.error(f"Error checking admin permissions: {str(e)}")
return False
async def get_media_settings_text():
- """生成媒体设置页面的文本"""
+ """Generate text for the media settings page"""
return MEDIA_SETTINGS_TEXT
async def get_ai_settings_text(rule):
- """生成AI设置页面的文本"""
- ai_prompt = rule.ai_prompt or os.getenv('DEFAULT_AI_PROMPT', '未设置')
- summary_prompt = rule.summary_prompt or os.getenv('DEFAULT_SUMMARY_PROMPT', '未设置')
+ """Generate text for the AI settings page"""
+ ai_prompt = rule.ai_prompt or os.getenv('DEFAULT_AI_PROMPT', 'Not set')
+ summary_prompt = rule.summary_prompt or os.getenv('DEFAULT_SUMMARY_PROMPT', 'Not set')
return AI_SETTINGS_TEXT.format(
ai_prompt=ai_prompt,
@@ -244,137 +244,137 @@ async def get_ai_settings_text(rule):
async def get_sender_info(event, rule_id):
"""
- 获取发送者信息
-
+ Get sender information
+
Args:
- event: 消息事件
- rule_id: 规则ID
-
+ event: Message event
+ rule_id: Rule ID
+
Returns:
- str: 发送者信息
+ str: Sender information
"""
try:
- logger.info("开始获取发送者信息")
+ logger.info("Starting to get sender information")
sender_name = None
if hasattr(event.message, 'sender_chat') and event.message.sender_chat:
- # 用户以频道身份发送消息
+ # User sent the message as a channel
sender = event.message.sender_chat
sender_name = sender.title if hasattr(sender, 'title') else None
- logger.info(f"使用频道信息: {sender_name}")
+ logger.info(f"Using channel info: {sender_name}")
elif event.sender:
- # 用户以个人身份发送消息
+ # User sent the message as an individual
sender = event.sender
sender_name = (
sender.title if hasattr(sender, 'title')
else f"{sender.first_name or ''} {sender.last_name or ''}".strip()
)
- logger.info(f"使用发送者信息: {sender_name}")
+ logger.info(f"Using sender info: {sender_name}")
elif hasattr(event.message, 'peer_id') and event.message.peer_id:
- # 尝试从 peer_id 获取信息
+ # Try to get info from peer_id
peer = event.message.peer_id
if hasattr(peer, 'channel_id'):
try:
- # 尝试获取频道信息
+ # Try to get channel info
channel = await event.client.get_entity(peer)
sender_name = channel.title if hasattr(channel, 'title') else None
- logger.info(f"使用peer_id信息: {sender_name}")
+ logger.info(f"Using peer_id info: {sender_name}")
except Exception as ce:
- logger.error(f'获取频道信息失败: {str(ce)}')
+ logger.error(f'Failed to get channel info: {str(ce)}')
if sender_name:
return sender_name
else:
- logger.warning(f"规则 ID: {rule_id} - 无法获取发送者信息")
+ logger.warning(f"Rule ID: {rule_id} - Unable to get sender information")
return None
except Exception as e:
- logger.error(f'获取发送者信息出错: {str(e)}')
+ logger.error(f'Error getting sender information: {str(e)}')
return None
async def check_and_clean_chats(session, rule=None):
"""
- 检查并清理不再与任何规则关联的聊天记录
-
+ Check and clean chat records that are no longer associated with any rules
+
Args:
- session: 数据库会话
- rule: 被删除的规则对象(可选),如果提供则从中获取聊天ID
-
+ session: Database session
+ rule: Deleted rule object (optional), if provided, chat IDs are extracted from it
+
Returns:
- int: 删除的聊天记录数量
+ int: Number of deleted chat records
"""
deleted_count = 0
-
+
try:
- # 获取所有聊天ID
+ # Get all chat IDs
chat_ids_to_check = set()
-
- # 如果提供了规则,先检查这些受影响的聊天
+
+ # If a rule is provided, first check these affected chats
if rule:
if rule.source_chat_id:
chat_ids_to_check.add(rule.source_chat_id)
if rule.target_chat_id:
chat_ids_to_check.add(rule.target_chat_id)
else:
- # 如果没有提供规则,则获取所有聊天
+ # If no rule is provided, get all chats
all_chats = session.query(Chat.id).all()
chat_ids_to_check = set(chat[0] for chat in all_chats)
-
- # 对每个聊天ID进行检查
+
+ # Check each chat ID
for chat_id in chat_ids_to_check:
- # 检查此聊天是否还被任何规则引用
+ # Check if this chat is still referenced by any rule
as_source = session.query(ForwardRule).filter(
ForwardRule.source_chat_id == chat_id
).count()
-
+
as_target = session.query(ForwardRule).filter(
ForwardRule.target_chat_id == chat_id
).count()
-
- # 如果聊天不再被任何规则引用
+
+ # If the chat is no longer referenced by any rule
if as_source == 0 and as_target == 0:
chat = session.query(Chat).get(chat_id)
if chat:
- # 获取telegram_chat_id以便日志记录
+ # Get telegram_chat_id for logging purposes
telegram_chat_id = chat.telegram_chat_id
- name = chat.name or "未命名聊天"
-
- # 清理所有引用此聊天作为current_add_id的记录
+ name = chat.name or "Unnamed chat"
+
+ # Clean up all records referencing this chat as current_add_id
chats_using_this = session.query(Chat).filter(
Chat.current_add_id == telegram_chat_id
).all()
-
+
for other_chat in chats_using_this:
other_chat.current_add_id = None
- logger.info(f'清除聊天 {other_chat.name} 的current_add_id设置')
-
- # 删除聊天记录
+ logger.info(f'Cleared current_add_id setting for chat {other_chat.name}')
+
+ # Delete the chat record
session.delete(chat)
- logger.info(f'删除未使用的聊天: {name} (ID: {telegram_chat_id})')
+ logger.info(f'Deleted unused chat: {name} (ID: {telegram_chat_id})')
deleted_count += 1
-
- # 如果有删除操作,提交更改
+
+ # If there were deletions, commit the changes
if deleted_count > 0:
session.commit()
- logger.info(f'共清理了 {deleted_count} 个未使用的聊天记录')
-
+ logger.info(f'Cleaned up {deleted_count} unused chat record(s) in total')
+
return deleted_count
-
+
except Exception as e:
- logger.error(f'检查和清理聊天记录时出错: {str(e)}')
+ logger.error(f'Error checking and cleaning chat records: {str(e)}')
session.rollback()
return 0
def get_admin_list():
- """获取管理员ID列表,如果ADMINS为空则使用USER_ID"""
+ """Get admin ID list, use USER_ID if ADMINS is empty"""
admin_str = os.getenv('ADMINS', '')
if not admin_str:
user_id = os.getenv('USER_ID')
if not user_id:
- logger.error('未设置 USER_ID 环境变量')
- raise ValueError('必须在 .env 文件中设置 USER_ID')
+ logger.error('USER_ID environment variable is not set')
+ raise ValueError('USER_ID must be set in the .env file')
return [int(user_id)]
return [int(admin.strip()) for admin in admin_str.split(',') if admin.strip()]
@@ -383,132 +383,132 @@ def get_admin_list():
async def check_keywords(rule, message_text, event = None):
"""
- 检查消息是否匹配关键字规则
+ Check if a message matches keyword rules
Args:
- rule: 转发规则对象,包含 forward_mode 和 keywords 属性
- message_text: 要检查的消息文本
- event: 可选的消息事件对象
+ rule: Forward rule object, containing forward_mode and keywords attributes
+ message_text: Message text to check
+ event: Optional message event object
Returns:
- bool: 是否应该转发消息
+ bool: Whether the message should be forwarded
"""
reverse_blacklist = rule.enable_reverse_blacklist
reverse_whitelist = rule.enable_reverse_whitelist
- logger.info(f"反转黑名单: {reverse_blacklist}, 反转白名单: {reverse_whitelist}")
+ logger.info(f"Reverse blacklist: {reverse_blacklist}, Reverse whitelist: {reverse_whitelist}")
- # 处理用户信息过滤
+ # Process user info filtering
if rule.is_filter_user_info and event:
message_text = await process_user_info(event, rule.id, message_text)
- logger.info("开始检查关键字规则")
- logger.info(f"当前转发模式: {rule.forward_mode}")
+ logger.info("Starting keyword rule check")
+ logger.info(f"Current forward mode: {rule.forward_mode}")
forward_mode = rule.forward_mode
- # 仅白名单模式
+ # Whitelist only mode
if forward_mode == ForwardMode.WHITELIST:
return await process_whitelist_mode(rule, message_text, reverse_blacklist)
- # 仅黑名单模式
+ # Blacklist only mode
elif forward_mode == ForwardMode.BLACKLIST:
return await process_blacklist_mode(rule, message_text, reverse_whitelist)
- # 先白后黑模式
+ # Whitelist then blacklist mode
elif forward_mode == ForwardMode.WHITELIST_THEN_BLACKLIST:
return await process_whitelist_then_blacklist_mode(rule, message_text, reverse_blacklist)
- # 先黑后白模式
+ # Blacklist then whitelist mode
elif forward_mode == ForwardMode.BLACKLIST_THEN_WHITELIST:
return await process_blacklist_then_whitelist_mode(rule, message_text, reverse_whitelist)
- logger.error(f"未知的转发模式: {forward_mode}")
+ logger.error(f"Unknown forward mode: {forward_mode}")
return False
async def process_whitelist_mode(rule, message_text, reverse_blacklist):
- """处理仅白名单模式"""
- logger.info("进入仅白名单模式")
+ """Process whitelist only mode"""
+ logger.info("Entering whitelist only mode")
should_forward = False
- # 检查普通白名单关键词
+ # Check regular whitelist keywords
whitelist_keywords = [k for k in rule.keywords if not k.is_blacklist]
- logger.info(f"普通白名单关键词: {[k.keyword for k in whitelist_keywords]}")
-
+ logger.info(f"Regular whitelist keywords: {[k.keyword for k in whitelist_keywords]}")
+
for keyword in whitelist_keywords:
if await check_keyword_match(keyword, message_text):
should_forward = True
break
-
+
if not should_forward:
- logger.info("未匹配到普通白名单关键词,不转发")
+ logger.info("No regular whitelist keywords matched, not forwarding")
return False
- # 如果启用了黑名单反转,还需要匹配反转后的黑名单(作为第二重白名单)
+ # If blacklist reversal is enabled, also need to match reversed blacklist (as a second whitelist)
if reverse_blacklist:
- logger.info("检查反转后的黑名单关键词(作为白名单)")
+ logger.info("Checking reversed blacklist keywords (as whitelist)")
reversed_blacklist = [k for k in rule.keywords if k.is_blacklist]
- logger.info(f"反转后的黑名单关键词: {[k.keyword for k in reversed_blacklist]}")
-
+ logger.info(f"Reversed blacklist keywords: {[k.keyword for k in reversed_blacklist]}")
+
reversed_match = False
for keyword in reversed_blacklist:
if await check_keyword_match(keyword, message_text):
reversed_match = True
break
-
+
if not reversed_match:
- logger.info("未匹配到反转后的黑名单关键词,不转发")
+ logger.info("No reversed blacklist keywords matched, not forwarding")
return False
- logger.info("所有白名单条件都满足,允许转发")
+ logger.info("All whitelist conditions are met, allowing forward")
return True
async def process_blacklist_mode(rule, message_text, reverse_whitelist):
- """处理仅黑名单模式"""
- logger.info("进入仅黑名单模式")
+ """Process blacklist only mode"""
+ logger.info("Entering blacklist only mode")
- # 检查普通黑名单关键词
+ # Check regular blacklist keywords
blacklist_keywords = [k for k in rule.keywords if k.is_blacklist]
- logger.info(f"普通黑名单关键词: {[k.keyword for k in blacklist_keywords]}")
-
+ logger.info(f"Regular blacklist keywords: {[k.keyword for k in blacklist_keywords]}")
+
for keyword in blacklist_keywords:
if await check_keyword_match(keyword, message_text):
- logger.info(f"匹配到黑名单关键词 '{keyword.keyword}',不转发")
+ logger.info(f"Matched blacklist keyword '{keyword.keyword}', not forwarding")
return False
- # 如果启用了白名单反转,检查反转后的白名单(作为黑名单)
+ # If whitelist reversal is enabled, check reversed whitelist (as blacklist)
if reverse_whitelist:
- logger.info("检查反转后的白名单关键词(作为黑名单)")
+ logger.info("Checking reversed whitelist keywords (as blacklist)")
reversed_whitelist = [k for k in rule.keywords if not k.is_blacklist]
- logger.info(f"反转后的白名单关键词: {[k.keyword for k in reversed_whitelist]}")
-
+ logger.info(f"Reversed whitelist keywords: {[k.keyword for k in reversed_whitelist]}")
+
for keyword in reversed_whitelist:
if await check_keyword_match(keyword, message_text):
- logger.info(f"匹配到反转后的白名单关键词 '{keyword.keyword}',不转发")
+ logger.info(f"Matched reversed whitelist keyword '{keyword.keyword}', not forwarding")
return False
- logger.info("未匹配到任何黑名单关键词,允许转发")
+ logger.info("No blacklist keywords matched, allowing forward")
return True
async def check_keyword_match(keyword, message_text):
- """检查单个关键词是否匹配"""
- logger.info(f"检查关键字: {keyword.keyword} (正则: {keyword.is_regex})")
+ """Check if a single keyword matches"""
+ logger.info(f"Checking keyword: {keyword.keyword} (regex: {keyword.is_regex})")
if keyword.is_regex:
try:
if re.search(keyword.keyword, message_text):
- logger.info(f"正则匹配成功: {keyword.keyword}")
+ logger.info(f"Regex match successful: {keyword.keyword}")
return True
except re.error:
- logger.error(f"正则表达式错误: {keyword.keyword}")
+ logger.error(f"Regex expression error: {keyword.keyword}")
else:
if keyword.keyword.lower() in message_text.lower():
- logger.info(f"关键字匹配成功: {keyword.keyword}")
+ logger.info(f"Keyword match successful: {keyword.keyword}")
return True
return False
async def process_user_info(event, rule_id, message_text):
- """处理用户信息过滤"""
+ """Process user info filtering"""
username = await get_sender_info(event, rule_id)
name = None
-
+
if hasattr(event.message, 'sender_chat') and event.message.sender_chat:
sender = event.message.sender_chat
name = sender.title if hasattr(sender, 'title') else None
@@ -518,112 +518,112 @@ async def process_user_info(event, rule_id, message_text):
sender.title if hasattr(sender, 'title')
else f"{sender.first_name or ''} {sender.last_name or ''}".strip()
)
-
+
if username and name:
- logger.info(f"成功获取用户信息: {username} {name}")
+ logger.info(f"Successfully retrieved user info: {username} {name}")
return f"{username} {name}:\n{message_text}"
elif username:
- logger.info(f"成功获取用户信息: {username}")
+ logger.info(f"Successfully retrieved user info: {username}")
return f"{username}:\n{message_text}"
elif name:
- logger.info(f"成功获取用户信息: {name}")
+ logger.info(f"Successfully retrieved user info: {name}")
return f"{name}:\n{message_text}"
else:
- logger.warning(f"规则 ID: {rule_id} - 无法获取发送者信息")
+ logger.warning(f"Rule ID: {rule_id} - Unable to get sender information")
return message_text
async def process_whitelist_then_blacklist_mode(rule, message_text, reverse_blacklist):
- """处理先白后黑模式
-
- 先检查白名单(必须匹配),然后检查黑名单(不能匹配)
- 如果启用黑名单反转,则黑名单变成第二重白名单(必须匹配)
+ """Process whitelist then blacklist mode
+
+ First check whitelist (must match), then check blacklist (must not match)
+ If blacklist reversal is enabled, blacklist becomes a second whitelist (must match)
"""
- logger.info("进入先白后黑模式")
+ logger.info("Entering whitelist then blacklist mode")
- # 检查普通白名单(必须匹配)
+ # Check regular whitelist (must match)
whitelist_match = False
whitelist_keywords = [k for k in rule.keywords if not k.is_blacklist]
- logger.info(f"检查普通白名单关键词: {[k.keyword for k in whitelist_keywords]}")
-
+ logger.info(f"Checking regular whitelist keywords: {[k.keyword for k in whitelist_keywords]}")
+
for keyword in whitelist_keywords:
if await check_keyword_match(keyword, message_text):
whitelist_match = True
break
-
+
if not whitelist_match:
- logger.info("未匹配到白名单关键词,不转发")
+ logger.info("No whitelist keywords matched, not forwarding")
return False
- # 根据反转设置处理黑名单
+ # Process blacklist based on reversal setting
blacklist_keywords = [k for k in rule.keywords if k.is_blacklist]
-
+
if reverse_blacklist:
- # 黑名单反转为白名单,必须匹配才转发
- logger.info("黑名单已反转,作为第二重白名单检查")
- logger.info(f"反转后的黑名单关键词: {[k.keyword for k in blacklist_keywords]}")
-
+ # Blacklist reversed to whitelist, must match to forward
+ logger.info("Blacklist reversed, checking as second whitelist")
+ logger.info(f"Reversed blacklist keywords: {[k.keyword for k in blacklist_keywords]}")
+
blacklist_match = False
for keyword in blacklist_keywords:
if await check_keyword_match(keyword, message_text):
blacklist_match = True
break
-
+
if not blacklist_match:
- logger.info("未匹配到反转后的黑名单关键词,不转发")
+ logger.info("No reversed blacklist keywords matched, not forwarding")
return False
else:
- # 正常黑名单,匹配则不转发
- logger.info(f"检查普通黑名单关键词: {[k.keyword for k in blacklist_keywords]}")
+ # Normal blacklist, match means do not forward
+ logger.info(f"Checking regular blacklist keywords: {[k.keyword for k in blacklist_keywords]}")
for keyword in blacklist_keywords:
if await check_keyword_match(keyword, message_text):
- logger.info(f"匹配到黑名单关键词 '{keyword.keyword}',不转发")
+ logger.info(f"Matched blacklist keyword '{keyword.keyword}', not forwarding")
return False
- logger.info("所有条件都满足,允许转发")
+ logger.info("All conditions are met, allowing forward")
return True
async def process_blacklist_then_whitelist_mode(rule, message_text, reverse_whitelist):
- """处理先黑后白模式
-
- 先检查黑名单(不能匹配),然后检查白名单(必须匹配)
- 如果启用白名单反转,则白名单变成第二重黑名单(不能匹配)
+ """Process blacklist then whitelist mode
+
+ First check blacklist (must not match), then check whitelist (must match)
+ If whitelist reversal is enabled, whitelist becomes a second blacklist (must not match)
"""
- logger.info("进入先黑后白模式")
+ logger.info("Entering blacklist then whitelist mode")
- # 检查普通黑名单(匹配则拒绝)
+ # Check regular blacklist (match means reject)
blacklist_keywords = [k for k in rule.keywords if k.is_blacklist]
- logger.info(f"检查普通黑名单关键词: {[k.keyword for k in blacklist_keywords]}")
-
+ logger.info(f"Checking regular blacklist keywords: {[k.keyword for k in blacklist_keywords]}")
+
for keyword in blacklist_keywords:
if await check_keyword_match(keyword, message_text):
- logger.info(f"匹配到黑名单关键词 '{keyword.keyword}',不转发")
+ logger.info(f"Matched blacklist keyword '{keyword.keyword}', not forwarding")
return False
- # 处理白名单
+ # Process whitelist
whitelist_keywords = [k for k in rule.keywords if not k.is_blacklist]
-
+
if reverse_whitelist:
- # 白名单反转为黑名单,匹配则不转发
- logger.info("白名单已反转,作为第二重黑名单检查")
- logger.info(f"反转后的白名单关键词: {[k.keyword for k in whitelist_keywords]}")
-
+ # Whitelist reversed to blacklist, match means do not forward
+ logger.info("Whitelist reversed, checking as second blacklist")
+ logger.info(f"Reversed whitelist keywords: {[k.keyword for k in whitelist_keywords]}")
+
for keyword in whitelist_keywords:
if await check_keyword_match(keyword, message_text):
- logger.info(f"匹配到反转后的白名单关键词 '{keyword.keyword}',不转发")
+ logger.info(f"Matched reversed whitelist keyword '{keyword.keyword}', not forwarding")
return False
else:
- # 正常白名单,必须匹配才转发
- logger.info(f"检查普通白名单关键词: {[k.keyword for k in whitelist_keywords]}")
+ # Normal whitelist, must match to forward
+ logger.info(f"Checking regular whitelist keywords: {[k.keyword for k in whitelist_keywords]}")
whitelist_match = False
for keyword in whitelist_keywords:
if await check_keyword_match(keyword, message_text):
whitelist_match = True
break
-
+
if not whitelist_match:
- logger.info("未匹配到白名单关键词,不转发")
+ logger.info("No whitelist keywords matched, not forwarding")
return False
- logger.info("所有条件都满足,允许转发")
+ logger.info("All conditions are met, allowing forward")
return True
From 1118b7a457143713ac93bd43246b5297ff764dad Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:13:23 +0000
Subject: [PATCH 22/76] Translate Chinese to English in ai/claude_provider.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
ai/claude_provider.py | 64 +++++++++++++++++++++----------------------
1 file changed, 32 insertions(+), 32 deletions(-)
diff --git a/ai/claude_provider.py b/ai/claude_provider.py
index dfcabbe..e4dd6b9 100644
--- a/ai/claude_provider.py
+++ b/ai/claude_provider.py
@@ -11,54 +11,54 @@ def __init__(self):
self.client = None
self.model = None
self.default_model = 'claude-3-5-sonnet-latest'
-
+
async def initialize(self, **kwargs):
- """初始化Claude客户端"""
+ """Initialize Claude client"""
api_key = os.getenv('CLAUDE_API_KEY')
if not api_key:
- raise ValueError("未设置CLAUDE_API_KEY环境变量")
-
- # 检查是否配置了自定义API基础URL
+ raise ValueError("CLAUDE_API_KEY environment variable is not set")
+
+ # Check if a custom API base URL is configured
api_base = os.getenv('CLAUDE_API_BASE', '').strip()
if api_base:
- logger.info(f"使用自定义Claude API基础URL: {api_base}")
+ logger.info(f"Using custom Claude API base URL: {api_base}")
self.client = anthropic.Anthropic(
api_key=api_key,
base_url=api_base
)
else:
- # 使用默认URL
+ # Use default URL
self.client = anthropic.Anthropic(api_key=api_key)
-
+
self.model = kwargs.get('model', self.default_model)
-
- async def process_message(self,
- message: str,
+
+ async def process_message(self,
+ message: str,
prompt: Optional[str] = None,
images: Optional[List[Dict[str, str]]] = None,
**kwargs) -> str:
- """处理消息"""
+ """Process message"""
try:
if not self.client:
await self.initialize(**kwargs)
-
- # 构建消息列表
+
+ # Build message list
messages = []
if prompt:
messages.append({"role": "system", "content": prompt})
-
- # 如果有图片,需要添加到消息中
+
+ # If there are images, add them to the message
if images and len(images) > 0:
- # 构建包含图片的内容列表
+ # Build content list containing images
content = []
-
- # 添加文本
+
+ # Add text
content.append({
"type": "text",
"text": message
})
-
- # 添加每张图片
+
+ # Add each image
for img in images:
content.append({
"type": "image",
@@ -68,27 +68,27 @@ async def process_message(self,
"data": img["data"]
}
})
- logger.info(f"已添加一张类型为 {img['mime_type']} 的图片,大小约 {len(img['data']) // 1000} KB")
-
- # 添加用户消息
+ logger.info(f"Added an image of type {img['mime_type']}, size approximately {len(img['data']) // 1000} KB")
+
+ # Add user message
messages.append({"role": "user", "content": content})
else:
- # 没有图片,只添加文本
+ # No images, only add text
messages.append({"role": "user", "content": message})
-
- # 使用流式输出 - 按照官方文档正确实现
+
+ # Use streaming output - correctly implemented per official documentation
with self.client.messages.stream(
model=self.model,
max_tokens=4096,
messages=messages
) as stream:
- # 使用专用的text_stream迭代器直接获取文本
+ # Use dedicated text_stream iterator to directly get text
full_response = ""
for text in stream.text_stream:
full_response += text
-
+
return full_response
-
+
except Exception as e:
- logger.error(f"Claude API 调用失败: {str(e)}")
- return f"AI处理失败: {str(e)}"
\ No newline at end of file
+ logger.error(f"Claude API call failed: {str(e)}")
+ return f"AI processing failed: {str(e)}"
From 57383dcbc12dc2ceb1e963763d0f0c09caf17ef3 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:13:43 +0000
Subject: [PATCH 23/76] Translate Chinese to English in utils/constants.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
utils/constants.py | 66 +++++++++++++++++++++++-----------------------
1 file changed, 33 insertions(+), 33 deletions(-)
diff --git a/utils/constants.py b/utils/constants.py
index dd588a5..86d43ce 100644
--- a/utils/constants.py
+++ b/utils/constants.py
@@ -2,20 +2,20 @@
from pathlib import Path
from dotenv import load_dotenv
-# 加载环境变量
+# Load environment variables
load_dotenv()
-# 目录配置
+# Directory configuration
BASE_DIR = Path(__file__).parent.parent
TEMP_DIR = os.path.join(BASE_DIR, 'temp')
RSS_HOST = os.getenv('RSS_HOST', '127.0.0.1')
RSS_PORT = os.getenv('RSS_PORT', '8000')
-# RSS基础URL,如果未设置,则使用请求的URL
+# RSS base URL, if not set, use the request URL
RSS_BASE_URL = os.environ.get('RSS_BASE_URL', None)
-# RSS媒体文件的基础URL,用于生成媒体链接,如果未设置,则使用请求的URL
+# RSS media file base URL, used for generating media links, if not set, use the request URL
RSS_MEDIA_BASE_URL = os.getenv('RSS_MEDIA_BASE_URL', '')
RSS_ENABLED = os.getenv('RSS_ENABLED', 'false')
@@ -26,32 +26,32 @@
DEFAULT_TIMEZONE = os.getenv('DEFAULT_TIMEZONE', 'Asia/Shanghai')
PROJECT_NAME = os.getenv('PROJECT_NAME', 'TG Forwarder RSS')
-# RSS相关路径配置
+# RSS related path configuration
RSS_MEDIA_PATH = os.getenv('RSS_MEDIA_PATH', './rss/media')
-# 转换为绝对路径
-RSS_MEDIA_DIR = os.path.abspath(os.path.join(BASE_DIR, RSS_MEDIA_PATH)
- if not os.path.isabs(RSS_MEDIA_PATH)
+# Convert to absolute path
+RSS_MEDIA_DIR = os.path.abspath(os.path.join(BASE_DIR, RSS_MEDIA_PATH)
+ if not os.path.isabs(RSS_MEDIA_PATH)
else RSS_MEDIA_PATH)
-# RSS数据路径
+# RSS data path
RSS_DATA_PATH = os.getenv('RSS_DATA_PATH', './rss/data')
RSS_DATA_DIR = os.path.abspath(os.path.join(BASE_DIR, RSS_DATA_PATH)
if not os.path.isabs(RSS_DATA_PATH)
else RSS_DATA_PATH)
-# 默认AI模型
+# Default AI model
DEFAULT_AI_MODEL = os.getenv('DEFAULT_AI_MODEL', 'gpt-4o')
-# 默认AI总结提示词
-DEFAULT_SUMMARY_PROMPT = os.getenv('DEFAULT_SUMMARY_PROMPT', '请总结以下频道/群组24小时内的消息。')
-# 默认AI提示词
-DEFAULT_AI_PROMPT = os.getenv('DEFAULT_AI_PROMPT', '请尊重原意,保持原有格式不变,用简体中文重写下面的内容:')
+# Default AI summary prompt
+DEFAULT_SUMMARY_PROMPT = os.getenv('DEFAULT_SUMMARY_PROMPT', 'Please summarize the following channel/group messages from the last 24 hours.')
+# Default AI prompt
+DEFAULT_AI_PROMPT = os.getenv('DEFAULT_AI_PROMPT', 'Please respect the original meaning, keep the original format unchanged, and rewrite the following content in Simplified Chinese:')
-# 分页配置
+# Pagination configuration
MODELS_PER_PAGE = int(os.getenv('AI_MODELS_PER_PAGE', 10))
KEYWORDS_PER_PAGE = int(os.getenv('KEYWORDS_PER_PAGE', 50))
-# 按钮布局配置
+# Button layout configuration
SUMMARY_TIME_ROWS = int(os.getenv('SUMMARY_TIME_ROWS', 10))
SUMMARY_TIME_COLS = int(os.getenv('SUMMARY_TIME_COLS', 6))
@@ -67,48 +67,48 @@
LOG_MAX_SIZE_MB = 10
LOG_BACKUP_COUNT = 3
-# 默认消息删除时间 (秒)
+# Default message deletion time (seconds)
BOT_MESSAGE_DELETE_TIMEOUT = int(os.getenv("BOT_MESSAGE_DELETE_TIMEOUT", 300))
-# 自动删除用户发送的指令消息
+# Auto-delete user-sent command messages
USER_MESSAGE_DELETE_ENABLE = os.getenv("USER_MESSAGE_DELETE_ENABLE", "false")
-# 是否启用UFB
+# Whether to enable UFB
UFB_ENABLED = os.getenv("UFB_ENABLED", "false")
-# 菜单标题
+# Menu title
AI_SETTINGS_TEXT = """
-当前AI提示词:
+Current AI prompt:
`{ai_prompt}`
-当前总结提示词:
+Current summary prompt:
`{summary_prompt}`
"""
-# 媒体设置文本
+# Media settings text
MEDIA_SETTINGS_TEXT = """
-媒体设置:
+Media settings:
"""
PUSH_SETTINGS_TEXT = """
-推送设置:
-请前往 https://github.com/caronc/apprise/wiki 查看添加推送配置格式说明
-如 `ntfy://ntfy.sh/你的主题名`
+Push settings:
+Please visit https://github.com/caronc/apprise/wiki to see the push configuration format instructions
+e.g. `ntfy://ntfy.sh/your_topic_name`
"""
-# 为每个规则生成特定的路径
+# Generate specific paths for each rule
def get_rule_media_dir(rule_id):
- """获取指定规则的媒体目录"""
+ """Get the media directory for a specified rule"""
rule_path = os.path.join(RSS_MEDIA_DIR, str(rule_id))
- # 确保目录存在
+ # Ensure the directory exists
os.makedirs(rule_path, exist_ok=True)
return rule_path
def get_rule_data_dir(rule_id):
- """获取指定规则的数据目录"""
+ """Get the data directory for a specified rule"""
rule_path = os.path.join(RSS_DATA_DIR, str(rule_id))
- # 确保目录存在
+ # Ensure the directory exists
os.makedirs(rule_path, exist_ok=True)
- return rule_path
\ No newline at end of file
+ return rule_path
From 25cfd789d14ba3aff70ca78c5fb540915e731fd7 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:13:48 +0000
Subject: [PATCH 24/76] Translate Chinese to English in ufb/ufb_client.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
ufb/ufb_client.py | 210 +++++++++++++++++++++++-----------------------
1 file changed, 105 insertions(+), 105 deletions(-)
diff --git a/ufb/ufb_client.py b/ufb/ufb_client.py
index 5869a8e..d05d1ea 100644
--- a/ufb/ufb_client.py
+++ b/ufb/ufb_client.py
@@ -12,11 +12,11 @@
logger = logging.getLogger(__name__)
async def get_main_module():
- """获取 main 模块"""
+ """Get the main module"""
try:
return sys.modules['__main__']
except KeyError:
- # 如果找不到 main 模块,尝试手动导入
+ # If main module is not found, try to import it manually
spec = importlib.util.spec_from_file_location(
"main",
os.path.join(os.path.dirname(os.path.dirname(__file__)), "main.py")
@@ -26,7 +26,7 @@ async def get_main_module():
return main
async def get_db_ops():
- """获取 main.py 中的 db_ops 实例"""
+ """Get the db_ops instance from main.py"""
main = await get_main_module()
if main.db_ops is None:
main.db_ops = await main.init_db_ops()
@@ -34,132 +34,132 @@ async def get_db_ops():
class UFBClient:
def __init__(self, config_dir: str = "./ufb/config"):
- # 获取当前文件所在目录(ufb目录)
+ # Get the directory where the current file is located (ufb directory)
current_file_dir = Path(__file__).parent
- # 获取项目根目录(当前文件的上级目录)
+ # Get the project root directory (parent of the current file's directory)
project_root = current_file_dir.parent
-
+
self.server_url: Optional[str] = None
self.token: Optional[str] = None
-
- # 使用项目根目录作为基准
+
+ # Use the project root directory as the base
self.config_dir = (project_root / config_dir).resolve()
- # logger.info(f"配置目录: {self.config_dir}")
-
+ # logger.info(f"Config directory: {self.config_dir}")
+
self.config_path = self.config_dir / "config.json"
self.websocket: Optional[websockets.WebSocketClientProtocol] = None
self.is_connected = False
self.on_config_update_callbacks: list[Callable[[Dict[str, Any]], None]] = []
- self.reconnect_task = None # 用于存储重连任务
-
- # 确保配置目录存在
+ self.reconnect_task = None # Used to store the reconnect task
+
+ # Ensure the config directory exists
self.config_dir.mkdir(parents=True, exist_ok=True)
async def ensure_config_dir(self):
- """确保配置目录存在"""
+ """Ensure the config directory exists"""
self.config_dir.mkdir(parents=True, exist_ok=True)
def load_config(self) -> Dict[str, Any]:
- """加载本地配置"""
+ """Load local config"""
if self.config_path.exists():
try:
return json.loads(self.config_path.read_text(encoding='utf-8'))
except json.JSONDecodeError:
- logger.error("配置文件损坏")
+ logger.error("Config file is corrupted")
return {}
return {}
async def save_config(self, config: Dict[str, Any], to_client: bool = False):
- """保存配置到本地"""
- logger.info(f"保存配置到本地: {self.config_path.absolute()}")
+ """Save config locally"""
+ logger.info(f"Saving config locally: {self.config_path.absolute()}")
self.config_path.write_text(json.dumps(config, ensure_ascii=False, indent=2), encoding='utf-8')
if to_client:
db_ops = await get_db_ops()
await db_ops.sync_from_json(config)
-
+
def merge_configs(self, local_config: Dict[str, Any], cloud_config: Dict[str, Any]) -> Dict[str, Any]:
- """递归合并本地和云端配置
- 策略:
- 1. 如果本地配置为空,使用云端配置
- 2. 如果是字典类型,递归合并
- 3. 如果是列表类型,合并列表(去重)
- 4. 如果是其他类型,使用云端的值覆盖本地值
+ """Recursively merge local and cloud configs
+ Strategy:
+ 1. If local config is empty, use cloud config
+ 2. If it's a dict type, merge recursively
+ 3. If it's a list type, merge lists (deduplicate)
+ 4. If it's another type, use cloud value to override local value
"""
- # 如果本地配置为空,直接使用云端配置
+ # If local config is empty, use cloud config directly
if not local_config:
return cloud_config.copy()
-
- # 如果云端配置为空,使用本地配置
+
+ # If cloud config is empty, use local config
if not cloud_config:
return local_config.copy()
- # 开始递归合并
+ # Begin recursive merge
merged = local_config.copy()
-
+
for key, cloud_value in cloud_config.items():
- # 如果是字典类型,递归合并
+ # If it's a dict type, merge recursively
if isinstance(cloud_value, dict):
if key not in merged:
merged[key] = {}
if isinstance(merged[key], dict):
merged[key] = self.merge_configs(merged[key], cloud_value)
else:
- # 如果本地值不是字典类型,但云端是字典类型,使用云端的值
+ # If local value is not a dict type but cloud is, use cloud value
merged[key] = cloud_value.copy()
- # 如果是列表类型,合并列表
+ # If it's a list type, merge lists
elif isinstance(cloud_value, list):
if key not in merged or not isinstance(merged[key], list):
merged[key] = cloud_value.copy()
else:
- # 合并列表,去重
+ # Merge lists, deduplicate
merged_list = merged[key].copy()
for item in cloud_value:
if item not in merged_list:
merged_list.append(item)
merged[key] = merged_list
else:
- # 非字典和列表类型,使用云端的值
+ # Non-dict and non-list types, use cloud value
merged[key] = cloud_value
return merged
def on_config_update(self, callback: Callable[[Dict[str, Any]], None]):
- """注册配置更新回调"""
+ """Register a config update callback"""
self.on_config_update_callbacks.append(callback)
def notify_config_update(self, config: Dict[str, Any]):
- """通知所有监听器配置已更新"""
+ """Notify all listeners that config has been updated"""
for callback in self.on_config_update_callbacks:
try:
callback(config)
except Exception as e:
- logger.error(f"配置更新回调执行失败: {e}")
+ logger.error(f"Config update callback execution failed: {e}")
async def handle_config_conflict(self, conflict_data: Dict[str, Any], local_config: Dict[str, Any]) -> Dict[str, Any]:
- """处理配置冲突
- 返回最终使用的配置
+ """Handle config conflict
+ Returns the final config to use
"""
- logger.info(f"配置冲突: \n云端时间: {conflict_data['cloudTime']}\n本地时间: {conflict_data['localTime']}")
-
- # 总是选择使用云端配置
+ logger.info(f"Config conflict: \nCloud time: {conflict_data['cloudTime']}\nLocal time: {conflict_data['localTime']}")
+
+ # Always choose to use cloud config
await self.websocket.send(json.dumps({
"type": "resolveConflict",
"choice": "useCloud"
}))
-
- # 等待服务器响应
+
+ # Wait for server response
cloud_config = json.loads(await self.websocket.recv())
- logger.info(f"收到云端配置: {json.dumps(cloud_config, ensure_ascii=False, indent=2)}")
-
- # 合并云端和本地配置
+ logger.info(f"Received cloud config: {json.dumps(cloud_config, ensure_ascii=False, indent=2)}")
+
+ # Merge cloud and local configs
merged_config = self.merge_configs(local_config, cloud_config)
- logger.info(f"合并后的配置: {json.dumps(merged_config, ensure_ascii=False, indent=2)}")
-
+ logger.info(f"Merged config: {json.dumps(merged_config, ensure_ascii=False, indent=2)}")
+
return merged_config
async def connect(self, server_url: str, token: str):
- """建立WebSocket连接"""
+ """Establish WebSocket connection"""
if self.is_connected:
await self.close()
@@ -169,68 +169,68 @@ async def connect(self, server_url: str, token: str):
try:
self.websocket = await websockets.connect(f"{server_url}/ws/config/{token}")
self.is_connected = True
- logger.info("WebSocket连接已建立")
-
- # 连接成功后取消重连任务
+ logger.info("WebSocket connection established")
+
+ # Cancel reconnect task after successful connection
if self.reconnect_task:
self.reconnect_task.cancel()
self.reconnect_task = None
-
+
except Exception as e:
- logger.error(f"WebSocket连接失败: {e}")
- # 启动重连
+ logger.error(f"WebSocket connection failed: {e}")
+ # Start reconnect
await self.start_reconnect()
raise
async def reconnect(self):
- """重连逻辑"""
+ """Reconnect logic"""
while True:
try:
if not self.is_connected and self.server_url and self.token:
- logger.info("尝试重新连接...")
+ logger.info("Attempting to reconnect...")
self.websocket = await websockets.connect(f"{self.server_url}/ws/config/{self.token}")
self.is_connected = True
- logger.info("重连成功")
-
- # 重新启动消息处理
+ logger.info("Reconnection successful")
+
+ # Restart message handling
asyncio.create_task(self._handle_messages())
-
- # 重新发送配置更新
+
+ # Resend config update
local_config = self.load_config()
await self.websocket.send(json.dumps({
"type": "update",
**local_config
}))
-
- # 重连成功后退出循环
+
+ # Exit loop after successful reconnection
break
except Exception as e:
- logger.error(f"重连失败: {e}")
- await asyncio.sleep(10) # 等待10秒后重试
+ logger.error(f"Reconnection failed: {e}")
+ await asyncio.sleep(10) # Wait 10 seconds before retrying
async def start_reconnect(self):
- """启动重连任务"""
+ """Start reconnect task"""
if not self.reconnect_task or self.reconnect_task.done():
self.reconnect_task = asyncio.create_task(self.reconnect())
async def start(self, server_url: Optional[str] = None, token: Optional[str] = None):
- """启动客户端"""
- logger.info("启动客户端")
+ """Start the client"""
+ logger.info("Starting client")
await self.ensure_config_dir()
-
+
if server_url and token:
await self.connect(server_url, token)
elif self.server_url and self.token:
await self.connect(self.server_url, self.token)
else:
- logger.info("等待连接参数...")
+ logger.info("Waiting for connection parameters...")
return
- # 检查本地配置
+ # Check local config
local_config = self.load_config()
current_timestamp = int(time.time() * 1000)
- # 确保配置结构完整
+ # Ensure config structure is complete
if not local_config:
local_config = {
"globalConfig": {
@@ -247,35 +247,35 @@ async def start(self, server_url: Optional[str] = None, token: Optional[str] = N
if "lastSyncTime" not in local_config["globalConfig"]["SYNC_CONFIG"]:
local_config["globalConfig"]["SYNC_CONFIG"]["lastSyncTime"] = current_timestamp
- # 检查是否为首次同步(配置文件不存在或为空)
+ # Check if this is a first-time sync (config file doesn't exist or is empty)
if not self.config_path.exists() or not local_config:
- # 发送首次同步请求
+ # Send first sync request
await self.websocket.send(json.dumps({
"type": "firstSync",
**local_config
}))
else:
- # 非首次同步,直接检查配置是否需要更新
+ # Not first sync, directly check if config needs updating
await self.websocket.send(json.dumps({
"type": "update",
**local_config
}))
- # 创建后台任务处理消息
+ # Create background task to handle messages
asyncio.create_task(self._handle_messages())
async def _handle_messages(self):
- """在后台处理WebSocket消息"""
+ """Handle WebSocket messages in the background"""
try:
async for message in self.websocket:
try:
data = json.loads(message)
- logger.info(f"收到服务器消息")
+ logger.info(f"Received server message")
msg_type = data.get("type")
if msg_type == "firstSync":
if data.get("message") == "firstSync_success":
- logger.info("首次同步成功")
+ logger.info("First sync successful")
await self.save_config(data)
self.notify_config_update(data)
@@ -286,63 +286,63 @@ async def _handle_messages(self):
else:
await self.save_config(data)
self.notify_config_update(data)
-
+
if data.get("message") == "config_updated":
- logger.info("配置已更新")
+ logger.info("Config has been updated")
elif msg_type == "configConflict":
- # 获取时间戳
+ # Get timestamps
cloud_time = data.get("cloudTime")
local_time = data.get("localTime")
newer_config = data.get("newerConfig")
-
- logger.info(f"配置冲突:\n云端时间: {cloud_time}\n本地时间: {local_time}\n较新配置: {newer_config}")
-
- # 加载本地配置
+
+ logger.info(f"Config conflict:\nCloud time: {cloud_time}\nLocal time: {local_time}\nNewer config: {newer_config}")
+
+ # Load local config
local_config = self.load_config()
-
- # 总是使用云端配置
+
+ # Always use cloud config
await self.websocket.send(json.dumps({
"type": "resolveConflict",
"choice": "useCloud"
}))
-
- # 等待服务器响应
+
+ # Wait for server response
response = json.loads(await self.websocket.recv())
- # 合并配置
+ # Merge configs
merged_config = self.merge_configs(local_config, response)
await self.save_config(merged_config)
self.notify_config_update(merged_config)
elif msg_type == "delete":
if data.get("success"):
- logger.info("配置删除成功")
+ logger.info("Config deletion successful")
else:
- logger.error(f"配置删除失败: {data.get('message', '')}")
+ logger.error(f"Config deletion failed: {data.get('message', '')}")
except json.JSONDecodeError:
- logger.error("收到无效的JSON消息")
+ logger.error("Received invalid JSON message")
except Exception as e:
- logger.error(f"处理消息时出错: {e}")
+ logger.error(f"Error processing message: {e}")
except websockets.ConnectionClosed:
- logger.info("WebSocket连接已关闭")
+ logger.info("WebSocket connection closed")
self.is_connected = False
- # 启动重连
+ # Start reconnect
await self.start_reconnect()
except Exception as e:
- logger.error(f"WebSocket错误: {e}")
+ logger.error(f"WebSocket error: {e}")
self.is_connected = False
- # 启动重连
+ # Start reconnect
await self.start_reconnect()
async def close(self):
- """关闭客户端"""
+ """Close the client"""
if self.websocket:
await self.websocket.close()
self.is_connected = False
- logger.info("WebSocket连接已关闭")
- # 取消重连任务
+ logger.info("WebSocket connection closed")
+ # Cancel reconnect task
if self.reconnect_task:
self.reconnect_task.cancel()
self.reconnect_task = None
From a078d1f5ad5cd081c7a0b39426232cdf64f63622 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:14:00 +0000
Subject: [PATCH 25/76] Translate Chinese to English in
filters/sender_filter.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
filters/sender_filter.py | 240 +++++++++++++++++++--------------------
1 file changed, 120 insertions(+), 120 deletions(-)
diff --git a/filters/sender_filter.py b/filters/sender_filter.py
index a22864f..59d87c8 100644
--- a/filters/sender_filter.py
+++ b/filters/sender_filter.py
@@ -8,116 +8,116 @@
class SenderFilter(BaseFilter):
"""
- 消息发送过滤器,用于发送处理后的消息
+ Message sending filter, used to send processed messages
"""
-
+
async def _process(self, context):
"""
- 发送处理后的消息
-
+ Send processed messages
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
rule = context.rule
client = context.client
event = context.event
-
+
if not context.should_forward:
- logger.info('消息不满足转发条件,跳过发送')
+ logger.info('Message does not meet forwarding conditions, skipping send')
return False
-
+
if rule.enable_only_push:
- logger.info('只转发到推送配置,跳过发送')
+ logger.info('Only forwarding to push configuration, skipping send')
return True
-
- # 获取目标聊天信息
+
+ # Get target chat information
target_chat = rule.target_chat
target_chat_id = int(target_chat.telegram_chat_id)
-
- # 预先获取目标聊天实体
+
+ # Pre-fetch target chat entity
try:
entity = None
try:
- # 直接使用ID
+ # Use ID directly
entity = await client.get_entity(target_chat_id)
- logger.info(f'成功获取目标聊天实体: {target_chat.name} (ID: {target_chat_id})')
+ logger.info(f'Successfully got target chat entity: {target_chat.name} (ID: {target_chat_id})')
except Exception as e1:
try:
- # 尝试添加-100前缀
+ # Try adding -100 prefix
if not str(target_chat_id).startswith('-100'):
super_group_id = int(f'-100{abs(target_chat_id)}')
entity = await client.get_entity(super_group_id)
- target_chat_id = super_group_id # 更新使用正确的ID
- logger.info(f'使用私有群组ID格式成功获取实体: {target_chat.name} (ID: {target_chat_id})')
+ target_chat_id = super_group_id # Update to use correct ID
+ logger.info(f'Successfully got entity using private group ID format: {target_chat.name} (ID: {target_chat_id})')
except Exception as e2:
try:
- # 尝试常规群组格式
+ # Try regular group format
if not str(target_chat_id).startswith('-'):
group_id = int(f'-{abs(target_chat_id)}')
entity = await client.get_entity(group_id)
- target_chat_id = group_id # 更新使用正确的ID
- logger.info(f'使用常规群组ID格式成功获取实体: {target_chat.name} (ID: {target_chat_id})')
+ target_chat_id = group_id # Update to use correct ID
+ logger.info(f'Successfully got entity using regular group ID format: {target_chat.name} (ID: {target_chat_id})')
except Exception as e3:
- logger.warning(f'无法获取目标聊天实体,尝试继续发送: {e1}, {e2}, {e3}')
+ logger.warning(f'Unable to get target chat entity, attempting to continue sending: {e1}, {e2}, {e3}')
except Exception as e:
- logger.warning(f'获取目标聊天实体时出错: {str(e)}')
-
- # 设置消息格式
- parse_mode = rule.message_mode.value # 使用枚举的值(字符串)
- logger.info(f'使用消息格式: {parse_mode}')
-
+ logger.warning(f'Error getting target chat entity: {str(e)}')
+
+ # Set message format
+ parse_mode = rule.message_mode.value # Use enum value (string)
+ logger.info(f'Using message format: {parse_mode}')
+
try:
- # 处理媒体组消息
+ # Process media group messages
if context.is_media_group or (context.media_group_messages and context.skipped_media):
- logger.info(f'准备发送媒体组消息')
+ logger.info(f'Preparing to send media group message')
await self._send_media_group(context, target_chat_id, parse_mode)
- # 处理单条媒体消息
+ # Process single media message
elif context.media_files or context.skipped_media:
- logger.info(f'准备发送单条媒体消息')
+ logger.info(f'Preparing to send single media message')
await self._send_single_media(context, target_chat_id, parse_mode)
- # 处理纯文本消息
+ # Process plain text message
else:
- logger.info(f'准备发送纯文本消息')
+ logger.info(f'Preparing to send plain text message')
await self._send_text_message(context, target_chat_id, parse_mode)
-
- logger.info(f'消息已发送到: {target_chat.name} ({target_chat_id})')
+
+ logger.info(f'Message sent to: {target_chat.name} ({target_chat_id})')
return True
except FloodWaitError as e:
wait_time = e.seconds
- logger.error(f'发送消息频率限制,需要等待 {wait_time} 秒')
- context.errors.append(f"发送消息频率限制,需要等待 {wait_time} 秒")
+ logger.error(f'Message sending rate limited, need to wait {wait_time} seconds')
+ context.errors.append(f"Message sending rate limited, need to wait {wait_time} seconds")
return False
except Exception as e:
- logger.error(f'发送消息时出错: {str(e)}')
- context.errors.append(f"发送消息错误: {str(e)}")
+ logger.error(f'Error sending message: {str(e)}')
+ context.errors.append(f"Message sending error: {str(e)}")
return False
-
+
async def _send_media_group(self, context, target_chat_id, parse_mode):
- """发送媒体组消息"""
+ """Send media group message"""
rule = context.rule
client = context.client
event = context.event
- # 初始化转发消息列表
+ # Initialize forwarded messages list
context.forwarded_messages = []
-
+
# if not context.media_group_messages:
- # logger.info(f'所有媒体都超限,发送文本和提示')
- # # 构建提示信息
+ # logger.info(f'All media exceeded limit, sending text and prompt')
+ # # Build prompt information
# text_to_send = context.message_text or ''
- # # 设置原始消息链接
- # context.original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
-
- # # 添加每个超限文件的信息
+ # # Set original message link
+ # context.original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+
+ # # Add information for each exceeded file
# for message, size, name in context.skipped_media:
- # text_to_send += f"\n\n⚠️ 媒体文件 {name if name else '未命名文件'} ({size}MB) 超过大小限制"
-
- # # 组合完整文本
+ # text_to_send += f"\n\n⚠️ Media file {name if name else 'unnamed file'} ({size}MB) exceeds size limit"
+
+ # # Combine complete text
# text_to_send = context.sender_info + text_to_send + context.time_info + context.original_link
-
+
# await client.send_message(
# target_chat_id,
# text_to_send,
@@ -125,10 +125,10 @@ async def _send_media_group(self, context, target_chat_id, parse_mode):
# link_preview=True,
# buttons=context.buttons
# )
- # logger.info(f'媒体组所有文件超限,已发送文本和提示')
+ # logger.info(f'All media group files exceeded limit, sent text and prompt')
# return
-
- # 如果有可以发送的媒体,作为一个组发送
+
+ # If there are media that can be sent, send as a group
files = []
try:
for message in context.media_group_messages:
@@ -136,29 +136,29 @@ async def _send_media_group(self, context, target_chat_id, parse_mode):
file_path = await message.download_media(os.path.join(os.getcwd(), 'temp'))
if file_path:
files.append(file_path)
-
- # 修改:保存下载的文件路径到context.media_files
+
+ # Modification: save downloaded file paths to context.media_files
if files:
- # 初始化 media_files 如果它不存在
+ # Initialize media_files if it doesn't exist
if not hasattr(context, 'media_files') or context.media_files is None:
context.media_files = []
- # 将当前下载的文件添加到列表中
+ # Add currently downloaded files to the list
context.media_files.extend(files)
- logger.info(f'已将 {len(files)} 个下载的媒体文件路径保存到context.media_files')
-
- # 添加发送者信息和消息文本
+ logger.info(f'Saved {len(files)} downloaded media file paths to context.media_files')
+
+ # Add sender info and message text
caption_text = context.sender_info + context.message_text
-
- # 如果有超限文件,添加提示信息
+
+ # If there are exceeded files, add prompt information
for message, size, name in context.skipped_media:
- caption_text += f"\n\n⚠️ 媒体文件 {name if name else '未命名文件'} ({size}MB) 超过大小限制"
-
+ caption_text += f"\n\n⚠️ Media file {name if name else 'unnamed file'} ({size}MB) exceeds size limit"
+
if context.skipped_media:
- context.original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
- # 添加时间信息和原始链接
+ context.original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+ # Add time info and original link
caption_text += context.time_info + context.original_link
-
- # 作为一个组发送所有文件
+
+ # Send all files as a group
sent_messages = await client.send_file(
target_chat_id,
files,
@@ -171,49 +171,49 @@ async def _send_media_group(self, context, target_chat_id, parse_mode):
PreviewMode.FOLLOW: context.event.message.media is not None
}[rule.is_preview]
)
- # 保存发送的消息到上下文
+ # Save sent messages to context
if isinstance(sent_messages, list):
context.forwarded_messages = sent_messages
else:
context.forwarded_messages = [sent_messages]
-
- logger.info(f'媒体组消息已发送,保存了 {len(context.forwarded_messages)} 条已转发消息')
+
+ logger.info(f'Media group message sent, saved {len(context.forwarded_messages)} forwarded messages')
except Exception as e:
- logger.error(f'发送媒体组消息时出错: {str(e)}')
+ logger.error(f'Error sending media group message: {str(e)}')
raise
finally:
- # 删除临时文件,但如果启用了推送则保留
+ # Delete temporary files, but keep them if push is enabled
if not rule.enable_push:
for file_path in files:
try:
os.remove(file_path)
- logger.info(f'删除临时文件: {file_path}')
+ logger.info(f'Deleted temporary file: {file_path}')
except Exception as e:
- logger.error(f'删除临时文件失败: {str(e)}')
+ logger.error(f'Failed to delete temporary file: {str(e)}')
else:
- logger.info(f'推送功能已启用,保留临时文件')
-
+ logger.info(f'Push feature enabled, keeping temporary files')
+
async def _send_single_media(self, context, target_chat_id, parse_mode):
- """发送单条媒体消息"""
+ """Send single media message"""
rule = context.rule
client = context.client
event = context.event
-
- logger.info(f'发送单条媒体消息')
-
- # 检查是否所有媒体都超限
+
+ logger.info(f'Sending single media message')
+
+ # Check if all media exceeded limit
if context.skipped_media and not context.media_files:
- # 构建提示信息
+ # Build prompt information
file_size = context.skipped_media[0][1]
file_name = context.skipped_media[0][2]
- original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
-
+ original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+
text_to_send = context.message_text or ''
- text_to_send += f"\n\n⚠️ 媒体文件 {file_name} ({file_size}MB) 超过大小限制"
+ text_to_send += f"\n\n⚠️ Media file {file_name} ({file_size}MB) exceeds size limit"
text_to_send = context.sender_info + text_to_send + context.time_info
-
+
text_to_send += original_link
-
+
await client.send_message(
target_chat_id,
text_to_send,
@@ -221,23 +221,23 @@ async def _send_single_media(self, context, target_chat_id, parse_mode):
link_preview=True,
buttons=context.buttons
)
- logger.info(f'媒体文件超过大小限制,仅转发文本')
+ logger.info(f'Media file exceeds size limit, only forwarding text')
return
-
- # 确保context.media_files存在
+
+ # Ensure context.media_files exists
if not hasattr(context, 'media_files') or context.media_files is None:
context.media_files = []
-
- # 发送媒体文件
+
+ # Send media files
for file_path in context.media_files:
try:
caption = (
- context.sender_info +
- context.message_text +
- context.time_info +
+ context.sender_info +
+ context.message_text +
+ context.time_info +
context.original_link
)
-
+
await client.send_file(
target_chat_id,
file_path,
@@ -250,40 +250,40 @@ async def _send_single_media(self, context, target_chat_id, parse_mode):
PreviewMode.FOLLOW: context.event.message.media is not None
}[rule.is_preview]
)
- logger.info(f'媒体消息已发送')
+ logger.info(f'Media message sent')
except Exception as e:
- logger.error(f'发送媒体消息时出错: {str(e)}')
+ logger.error(f'Error sending media message: {str(e)}')
raise
finally:
- # 删除临时文件,但如果启用了推送则保留
+ # Delete temporary files, but keep them if push is enabled
if not rule.enable_push:
try:
os.remove(file_path)
- logger.info(f'删除临时文件: {file_path}')
+ logger.info(f'Deleted temporary file: {file_path}')
except Exception as e:
- logger.error(f'删除临时文件失败: {str(e)}')
+ logger.error(f'Failed to delete temporary file: {str(e)}')
else:
- logger.info(f'推送功能已启用,保留临时文件: {file_path}')
-
+ logger.info(f'Push feature enabled, keeping temporary file: {file_path}')
+
async def _send_text_message(self, context, target_chat_id, parse_mode):
- """发送纯文本消息"""
+ """Send plain text message"""
rule = context.rule
client = context.client
-
+
if not context.message_text:
- logger.info('没有文本内容,不发送消息')
+ logger.info('No text content, not sending message')
return
-
- # 根据预览模式设置 link_preview
+
+ # Set link_preview based on preview mode
link_preview = {
PreviewMode.ON: True,
PreviewMode.OFF: False,
- PreviewMode.FOLLOW: context.event.message.media is not None # 跟随原消息
+ PreviewMode.FOLLOW: context.event.message.media is not None # Follow original message
}[rule.is_preview]
-
- # 组合消息文本
+
+ # Combine message text
message_text = context.sender_info + context.message_text + context.time_info + context.original_link
-
+
await client.send_message(
target_chat_id,
str(message_text),
@@ -291,4 +291,4 @@ async def _send_text_message(self, context, target_chat_id, parse_mode):
link_preview=link_preview,
buttons=context.buttons
)
- logger.info(f'{"带预览的" if link_preview else "无预览的"}文本消息已发送')
\ No newline at end of file
+ logger.info(f'{"Text message with preview" if link_preview else "Text message without preview"} sent')
From 12e64568c95f845d4b2f01999fea1f7a89f1f280 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:14:14 +0000
Subject: [PATCH 26/76] Translate Chinese to English in rss/main.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
rss/main.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/rss/main.py b/rss/main.py
index f0291fd..89e5138 100644
--- a/rss/main.py
+++ b/rss/main.py
@@ -17,25 +17,25 @@
sys.path.append(str(root_dir))
-# 获取日志记录器
+# Get logger
logger = logging.getLogger(__name__)
app = FastAPI(title="TG Forwarder RSS")
-# 注册路由
+# Register routes
app.include_router(auth_router)
app.include_router(rss_router)
app.include_router(feed.router)
-# 模板配置
+# Template configuration
templates = Jinja2Templates(directory="rss/app/templates")
def run_server(host: str = "0.0.0.0", port: int = 8000):
- """运行 RSS 服务器"""
+ """Run the RSS server"""
uvicorn.run(app, host=host, port=port)
-# 添加直接运行支持
+# Add direct run support
if __name__ == "__main__":
- # 只有在直接运行时才设置日志(而不是被导入时)
+ # Only set up logging when running directly (not when imported)
setup_logging()
- run_server()
\ No newline at end of file
+ run_server()
From 14bfb87c3f775cb7ac06788a1d1f75dee5dbd03f Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:14:25 +0000
Subject: [PATCH 27/76] Translate Chinese to English in utils/file_creator.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
utils/file_creator.py | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/utils/file_creator.py b/utils/file_creator.py
index f797e7f..4940960 100644
--- a/utils/file_creator.py
+++ b/utils/file_creator.py
@@ -4,7 +4,7 @@
logger = logging.getLogger(__name__)
-# 默认AI模型配置(JSON格式)
+# Default AI model configuration (JSON format)
AI_MODELS_CONFIG = {
"openai": [
"gpt-4o",
@@ -73,7 +73,7 @@
]
}
-# 汇总时间列表
+# Summary time list
SUMMARY_TIMES_CONTENT = """00:00
00:30
01:00
@@ -124,7 +124,7 @@
23:30
23:50"""
-# 延迟时间列表
+# Delay time list
DELAY_TIMES_CONTENT = """1
2
3
@@ -136,7 +136,7 @@
9
10"""
-# 最大媒体大小列表
+# Maximum media size list
MAX_MEDIA_SIZE_CONTENT = """1
2
3
@@ -186,7 +186,7 @@
2048
"""
-MEDIA_EXTENSIONS_CONTENT = """无扩展名
+MEDIA_EXTENSIONS_CONTENT = """No extension
jpg
jpeg
png
@@ -250,11 +250,11 @@
def create_default_configs():
- """创建默认配置文件"""
+ """Create default configuration files"""
config_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config')
os.makedirs(config_dir, exist_ok=True)
- # 定义默认配置内容
+ # Define default configuration content
default_configs = {
'summary_times.txt': SUMMARY_TIMES_CONTENT,
'delay_times.txt': DELAY_TIMES_CONTENT,
@@ -262,15 +262,15 @@ def create_default_configs():
'media_extensions.txt': MEDIA_EXTENSIONS_CONTENT,
}
- # 检查并创建每个配置文件
+ # Check and create each configuration file
for filename, content in default_configs.items():
file_path = os.path.join(config_dir, filename)
if not os.path.exists(file_path):
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content.strip())
logger.info(f"Created {filename}")
-
- # 创建JSON格式的AI模型配置文件
+
+ # Create JSON format AI model configuration file
json_config_path = os.path.join(config_dir, 'ai_models.json')
if not os.path.exists(json_config_path):
try:
@@ -278,4 +278,4 @@ def create_default_configs():
json.dump(AI_MODELS_CONFIG, f, ensure_ascii=False, indent=4)
logger.info("Created ai_models.json")
except Exception as e:
- logger.error(f"创建 ai_models.json 失败: {e}")
\ No newline at end of file
+ logger.error(f"Failed to create ai_models.json: {e}")
From ca13fb6655ab6d38057c6e581a32233c95a1aff4 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:14:41 +0000
Subject: [PATCH 28/76] Translate Chinese to English in
handlers/prompt_handlers.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
handlers/prompt_handlers.py | 172 ++++++++++++++++++------------------
1 file changed, 86 insertions(+), 86 deletions(-)
diff --git a/handlers/prompt_handlers.py b/handlers/prompt_handlers.py
index 8fc8139..88c69d4 100644
--- a/handlers/prompt_handlers.py
+++ b/handlers/prompt_handlers.py
@@ -13,11 +13,11 @@
logger = logging.getLogger(__name__)
async def handle_prompt_setting(event, client, sender_id, chat_id, current_state, message):
- """处理设置提示词的逻辑"""
- logger.info(f"开始处理提示词设置,用户ID:{sender_id},聊天ID:{chat_id},当前状态:{current_state}")
+ """Handle the logic for setting prompts"""
+ logger.info(f"Starting to process prompt setting, user ID: {sender_id}, chat ID: {chat_id}, current state: {current_state}")
if not current_state:
- logger.info("当前无状态,返回False")
+ logger.info("No current state, returning False")
return False
rule_id = None
@@ -28,91 +28,91 @@ async def handle_prompt_setting(event, client, sender_id, chat_id, current_state
if current_state.startswith("set_summary_prompt:"):
rule_id = current_state.split(":")[1]
field_name = "summary_prompt"
- prompt_type = "AI总结"
+ prompt_type = "AI Summary"
template_type = "ai"
- logger.info(f"检测到设置总结提示词,规则ID:{rule_id}")
+ logger.info(f"Detected setting summary prompt, rule ID: {rule_id}")
elif current_state.startswith("set_ai_prompt:"):
rule_id = current_state.split(":")[1]
field_name = "ai_prompt"
prompt_type = "AI"
template_type = "ai"
- logger.info(f"检测到设置AI提示词,规则ID:{rule_id}")
+ logger.info(f"Detected setting AI prompt, rule ID: {rule_id}")
elif current_state.startswith("set_userinfo_template:"):
rule_id = current_state.split(":")[1]
field_name = "userinfo_template"
- prompt_type = "用户信息"
+ prompt_type = "User Info"
template_type = "userinfo"
- logger.info(f"检测到设置用户信息模板,规则ID:{rule_id}")
+ logger.info(f"Detected setting user info template, rule ID: {rule_id}")
elif current_state.startswith("set_time_template:"):
rule_id = current_state.split(":")[1]
field_name = "time_template"
- prompt_type = "时间"
+ prompt_type = "Time"
template_type = "time"
- logger.info(f"检测到设置时间模板,规则ID:{rule_id}")
+ logger.info(f"Detected setting time template, rule ID: {rule_id}")
elif current_state.startswith("set_original_link_template:"):
rule_id = current_state.split(":")[1]
field_name = "original_link_template"
- prompt_type = "原始链接"
+ prompt_type = "Original Link"
template_type = "link"
- logger.info(f"检测到设置原始链接模板,规则ID:{rule_id}")
+ logger.info(f"Detected setting original link template, rule ID: {rule_id}")
elif current_state.startswith("add_push_channel:"):
- # 处理添加推送频道
+ # Handle adding push channel
rule_id = current_state.split(":")[1]
- logger.info(f"检测到添加推送频道,规则ID:{rule_id}")
+ logger.info(f"Detected adding push channel, rule ID: {rule_id}")
return await handle_add_push_channel(event, client, sender_id, chat_id, rule_id, message)
else:
- logger.info(f"未知的状态类型:{current_state}")
+ logger.info(f"Unknown state type: {current_state}")
return False
- logger.info(f"处理设置{prompt_type}提示词/模板,规则ID:{rule_id},字段名:{field_name}")
+ logger.info(f"Processing setting {prompt_type} prompt/template, rule ID: {rule_id}, field name: {field_name}")
session = get_session()
try:
- logger.info(f"查询规则ID:{rule_id}")
+ logger.info(f"Querying rule ID: {rule_id}")
rule = session.query(ForwardRule).get(int(rule_id))
if rule:
old_prompt = getattr(rule, field_name) if hasattr(rule, field_name) else None
new_prompt = event.message.text
- logger.info(f"找到规则,原提示词/模板:{old_prompt}")
- logger.info(f"准备更新为新提示词/模板:{new_prompt}")
+ logger.info(f"Rule found, original prompt/template: {old_prompt}")
+ logger.info(f"Preparing to update to new prompt/template: {new_prompt}")
setattr(rule, field_name, new_prompt)
session.commit()
- logger.info(f"已更新规则{rule_id}的{prompt_type}提示词/模板")
+ logger.info(f"Updated rule {rule_id}'s {prompt_type} prompt/template")
- # 检查是否启用了同步功能
+ # Check if sync feature is enabled
if rule.enable_sync:
- logger.info(f"规则 {rule.id} 启用了同步功能,正在同步提示词/模板设置到关联规则")
- # 获取需要同步的规则列表
+ logger.info(f"Rule {rule.id} has sync enabled, syncing prompt/template settings to associated rules")
+ # Get list of rules that need to be synced
sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all()
- # 为每个同步规则应用相同的提示词设置
+ # Apply the same prompt settings for each sync rule
for sync_rule in sync_rules:
sync_rule_id = sync_rule.sync_rule_id
- logger.info(f"正在同步{prompt_type}提示词/模板到规则 {sync_rule_id}")
-
- # 获取同步目标规则
+ logger.info(f"Syncing {prompt_type} prompt/template to rule {sync_rule_id}")
+
+ # Get sync target rule
target_rule = session.query(ForwardRule).get(sync_rule_id)
if not target_rule:
- logger.warning(f"同步目标规则 {sync_rule_id} 不存在,跳过")
+ logger.warning(f"Sync target rule {sync_rule_id} does not exist, skipping")
continue
-
- # 更新同步目标规则的提示词设置
+
+ # Update sync target rule's prompt settings
try:
- # 记录旧提示词
+ # Record old prompt
old_target_prompt = getattr(target_rule, field_name) if hasattr(target_rule, field_name) else None
- # 设置新提示词
+ # Set new prompt
setattr(target_rule, field_name, new_prompt)
- logger.info(f"同步规则 {sync_rule_id} 的{prompt_type}提示词/模板从 '{old_target_prompt}' 到 '{new_prompt}'")
+ logger.info(f"Synced rule {sync_rule_id}'s {prompt_type} prompt/template from '{old_target_prompt}' to '{new_prompt}'")
except Exception as e:
- logger.error(f"同步{prompt_type}提示词/模板到规则 {sync_rule_id} 时出错: {str(e)}")
+ logger.error(f"Error syncing {prompt_type} prompt/template to rule {sync_rule_id}: {str(e)}")
continue
session.commit()
- logger.info("所有同步提示词/模板更改已提交")
+ logger.info("All synced prompt/template changes have been committed")
- logger.info(f"清除用户状态,用户ID:{sender_id},聊天ID:{chat_id}")
+ logger.info(f"Clearing user state, user ID: {sender_id}, chat ID: {chat_id}")
state_manager.clear_state(sender_id, chat_id)
@@ -123,58 +123,58 @@ async def handle_prompt_setting(event, client, sender_id, chat_id, current_state
try:
await async_delete_user_message(bot_client, message_chat_id, event.message.id, 0)
except Exception as e:
- logger.error(f"删除用户消息失败: {str(e)}")
+ logger.error(f"Failed to delete user message: {str(e)}")
await message.delete()
- logger.info("准备发送更新后的设置消息")
-
- # 根据模板类型选择不同的显示页面
+ logger.info("Preparing to send updated settings message")
+
+ # Choose different display page based on template type
if template_type == "ai":
- # AI设置页面
+ # AI settings page
await client.send_message(
chat_id,
await get_ai_settings_text(rule),
buttons=await bot_handler.create_ai_settings_buttons(rule)
)
elif template_type in ["userinfo", "time", "link"]:
- # 其他设置页面
+ # Other settings page
await client.send_message(
chat_id,
- f"已更新规则 {rule_id} 的{prompt_type}模板",
+ f"Updated rule {rule_id}'s {prompt_type} template",
buttons=await bot_handler.create_other_settings_buttons(rule_id=rule_id)
)
- # 删除用户消息
- logger.info("设置消息发送成功")
+ # Delete user message
+ logger.info("Settings message sent successfully")
return True
else:
- logger.warning(f"未找到规则ID:{rule_id}")
+ logger.warning(f"Rule ID not found: {rule_id}")
except Exception as e:
- logger.error(f"处理提示词/模板设置时发生错误:{str(e)}")
+ logger.error(f"Error occurred while processing prompt/template setting: {str(e)}")
raise
finally:
session.close()
- logger.info("数据库会话已关闭")
+ logger.info("Database session closed")
return True
async def handle_add_push_channel(event, client, sender_id, chat_id, rule_id, message):
- """处理添加推送频道的逻辑"""
- logger.info(f"开始处理添加推送频道,规则ID:{rule_id}")
+ """Handle the logic for adding push channel"""
+ logger.info(f"Starting to process adding push channel, rule ID: {rule_id}")
session = get_session()
try:
- # 获取规则
+ # Get rule
rule = session.query(ForwardRule).get(int(rule_id))
if not rule:
- logger.warning(f"未找到规则ID:{rule_id}")
+ logger.warning(f"Rule ID not found: {rule_id}")
return False
- # 获取用户输入的推送频道信息
+ # Get push channel info entered by user
push_channel = event.message.text.strip()
- logger.info(f"用户输入的推送频道: {push_channel}")
+ logger.info(f"Push channel entered by user: {push_channel}")
try:
- # 创建新的推送配置
+ # Create new push configuration
is_email = push_channel.startswith(('mailto://', 'mailtos://', 'email://'))
push_config = PushConfig(
rule_id=int(rule_id),
@@ -184,38 +184,38 @@ async def handle_add_push_channel(event, client, sender_id, chat_id, rule_id, me
)
session.add(push_config)
- # 启用规则的推送功能
+ # Enable push feature for the rule
rule.enable_push = True
- # 检查是否启用了同步功能
+ # Check if sync feature is enabled
if rule.enable_sync:
- logger.info(f"规则 {rule.id} 启用了同步功能,正在同步推送配置到关联规则")
-
- # 获取需要同步的规则列表
+ logger.info(f"Rule {rule.id} has sync enabled, syncing push configuration to associated rules")
+
+ # Get list of rules that need to be synced
sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule.id).all()
- # 为每个同步规则创建相同的推送配置
+ # Create the same push configuration for each sync rule
for sync_rule in sync_rules:
sync_rule_id = sync_rule.sync_rule_id
- logger.info(f"正在同步推送配置到规则 {sync_rule_id}")
-
- # 获取同步目标规则
+ logger.info(f"Syncing push configuration to rule {sync_rule_id}")
+
+ # Get sync target rule
target_rule = session.query(ForwardRule).get(sync_rule_id)
if not target_rule:
- logger.warning(f"同步目标规则 {sync_rule_id} 不存在,跳过")
+ logger.warning(f"Sync target rule {sync_rule_id} does not exist, skipping")
continue
-
- # 检查目标规则是否已存在相同推送频道
+
+ # Check if target rule already has the same push channel
existing_config = session.query(PushConfig).filter_by(
rule_id=sync_rule_id,
push_channel=push_channel
).first()
if existing_config:
- logger.info(f"目标规则 {sync_rule_id} 已存在推送频道 {push_channel},跳过")
+ logger.info(f"Target rule {sync_rule_id} already has push channel {push_channel}, skipping")
continue
- # 创建新的推送配置
+ # Create new push configuration
try:
sync_push_config = PushConfig(
rule_id=sync_rule_id,
@@ -225,61 +225,61 @@ async def handle_add_push_channel(event, client, sender_id, chat_id, rule_id, me
)
session.add(sync_push_config)
- # 启用目标规则的推送功能
+ # Enable push feature for target rule
target_rule.enable_push = True
- logger.info(f"已为规则 {sync_rule_id} 添加推送频道 {push_channel}")
+ logger.info(f"Added push channel {push_channel} for rule {sync_rule_id}")
except Exception as e:
- logger.error(f"为规则 {sync_rule_id} 添加推送配置时出错: {str(e)}")
+ logger.error(f"Error adding push configuration for rule {sync_rule_id}: {str(e)}")
continue
- # 提交更改
+ # Commit changes
session.commit()
success = True
- message_text = "成功添加推送配置"
+ message_text = "Successfully added push configuration"
except Exception as db_error:
session.rollback()
success = False
- message_text = f"添加推送配置失败: {str(db_error)}"
- logger.error(f"添加推送配置到数据库时出错: {str(db_error)}")
+ message_text = f"Failed to add push configuration: {str(db_error)}"
+ logger.error(f"Error adding push configuration to database: {str(db_error)}")
- # 清除状态
+ # Clear state
state_manager.clear_state(sender_id, chat_id)
- # 删除用户消息
+ # Delete user message
message_chat_id = event.message.chat_id
bot_client = await get_bot_client()
try:
await async_delete_user_message(bot_client, message_chat_id, event.message.id, 0)
except Exception as e:
- logger.error(f"删除用户消息失败: {str(e)}")
-
- # 删除原始消息并显示结果
+ logger.error(f"Failed to delete user message: {str(e)}")
+
+ # Delete original message and display result
await message.delete()
- # 获取主界面
+ # Get main module
main_module = await get_main_module()
bot_client = main_module.bot_client
- # 发送结果通知
+ # Send result notification
if success:
await send_message_and_delete(
bot_client,
chat_id,
- f"已成功添加推送频道: {push_channel}",
+ f"Successfully added push channel: {push_channel}",
buttons=await bot_handler.create_push_settings_buttons(rule_id)
)
else:
await send_message_and_delete(
bot_client,
chat_id,
- f"添加推送频道失败: {message_text}",
+ f"Failed to add push channel: {message_text}",
buttons=await bot_handler.create_push_settings_buttons(rule_id)
)
return True
except Exception as e:
- logger.error(f"处理添加推送频道时出错: {str(e)}")
+ logger.error(f"Error processing adding push channel: {str(e)}")
logger.error(traceback.format_exc())
return False
finally:
From 2232e01f8244bf6163e9887ff02e5afc7abcf874 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:14:47 +0000
Subject: [PATCH 29/76] Translate Chinese to English in utils/log_config.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
utils/log_config.py | 36 ++++++++++++++++++------------------
1 file changed, 18 insertions(+), 18 deletions(-)
diff --git a/utils/log_config.py b/utils/log_config.py
index 97bb3d0..7013094 100644
--- a/utils/log_config.py
+++ b/utils/log_config.py
@@ -5,29 +5,29 @@
def setup_logging():
"""
- 配置日志系统,将所有日志输出到标准输出,
- 由Docker收集并管理日志
+ Configure the logging system, output all logs to stdout,
+ collected and managed by Docker
"""
- # 加载环境变量
+ # Load environment variables
load_dotenv()
-
- # 创建根日志记录器
+
+ # Create root logger
root_logger = logging.getLogger()
-
- # 设置日志级别 - 默认使用INFO级别
+
+ # Set log level - default to INFO level
root_logger.setLevel(logging.INFO)
-
- # 创建一个处理器,用于将日志输出到控制台
+
+ # Create a handler to output logs to the console
console_handler = logging.StreamHandler()
-
- # 创建格式化器
+
+ # Create formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-
- # 将格式化器添加到处理器
+
+ # Add formatter to handler
console_handler.setFormatter(formatter)
-
- # 将处理器添加到根日志记录器
+
+ # Add handler to root logger
root_logger.addHandler(console_handler)
-
- # 返回配置的日志记录器
- return root_logger
\ No newline at end of file
+
+ # Return the configured logger
+ return root_logger
From 1849af7a7e6bd73a73d5fdf62b6e49830153d057 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:15:17 +0000
Subject: [PATCH 30/76] Translate Chinese to English in utils/auto_delete.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
utils/auto_delete.py | 131 +++++++++++++++++++++----------------------
1 file changed, 65 insertions(+), 66 deletions(-)
diff --git a/utils/auto_delete.py b/utils/auto_delete.py
index 5a8afd4..8dce6e3 100644
--- a/utils/auto_delete.py
+++ b/utils/auto_delete.py
@@ -5,120 +5,119 @@
from utils.constants import BOT_MESSAGE_DELETE_TIMEOUT, USER_MESSAGE_DELETE_ENABLE
logger = logging.getLogger(__name__)
-# 从环境变量获取默认超时时间
+# Get default timeout from environment variable
async def delete_after(message, seconds):
- """等待指定秒数后删除消息
-
- 参数:
- message: 要删除的消息
- seconds: 等待多少秒后删除, 0表示立即删除, -1表示不删除
+ """Wait for the specified number of seconds then delete the message
+
+ Args:
+ message: The message to delete
+ seconds: Number of seconds to wait before deleting, 0 means delete immediately, -1 means do not delete
"""
- if seconds == -1: # -1 表示不删除
+ if seconds == -1: # -1 means do not delete
return
-
- if seconds > 0: # 正数表示等待指定秒数再删除
+
+ if seconds > 0: # Positive number means wait the specified seconds before deleting
await asyncio.sleep(seconds)
-
+
try:
await message.delete()
except Exception as e:
- logger.error(f"删除消息失败: {e}")
+ logger.error(f"Failed to delete message: {e}")
async def reply_and_delete(event, text, delete_after_seconds=None, **kwargs):
- """回复消息并安排自动删除
-
- 参数:
- event: Telethon事件对象
- text: 要发送的文本
- delete_after_seconds: 多少秒后删除消息,None使用默认值,0表示立即删除,-1表示不删除
- **kwargs: 传递给reply方法的其他参数
+ """Reply to a message and schedule auto-deletion
+
+ Args:
+ event: Telethon event object
+ text: Text to send
+ delete_after_seconds: Number of seconds before deleting the message, None uses default, 0 means delete immediately, -1 means do not delete
+ **kwargs: Other parameters passed to the reply method
"""
- # 如果没有指定删除时间,使用环境变量中的默认值
+ # If no deletion time is specified, use the default from environment variable
if delete_after_seconds is None:
deletion_timeout = BOT_MESSAGE_DELETE_TIMEOUT
else:
deletion_timeout = delete_after_seconds
-
- # 发送回复
+
+ # Send reply
message = await event.reply(text, **kwargs)
-
- # 安排删除任务,只有当deletion_timeout不等于-1时才删除
+
+ # Schedule deletion task, only delete when deletion_timeout is not -1
if deletion_timeout != -1:
asyncio.create_task(delete_after(message, deletion_timeout))
-
+
return message
async def respond_and_delete(event, text, delete_after_seconds=None, **kwargs):
- """使用respond回复消息并安排自动删除
-
- 参数:
- event: Telethon事件对象
- text: 要发送的文本
- delete_after_seconds: 多少秒后删除消息,None使用默认值,0表示立即删除,-1表示不删除
- **kwargs: 传递给respond方法的其他参数
+ """Use respond to reply to a message and schedule auto-deletion
+
+ Args:
+ event: Telethon event object
+ text: Text to send
+ delete_after_seconds: Number of seconds before deleting the message, None uses default, 0 means delete immediately, -1 means do not delete
+ **kwargs: Other parameters passed to the respond method
"""
- # 如果没有指定删除时间,使用环境变量中的默认值
+ # If no deletion time is specified, use the default from environment variable
if delete_after_seconds is None:
deletion_timeout = BOT_MESSAGE_DELETE_TIMEOUT
else:
deletion_timeout = delete_after_seconds
-
- # 发送回复
+
+ # Send reply
message = await event.respond(text, **kwargs)
-
- # 安排删除任务,只有当deletion_timeout不等于-1时才删除
+
+ # Schedule deletion task, only delete when deletion_timeout is not -1
if deletion_timeout != -1:
asyncio.create_task(delete_after(message, deletion_timeout))
-
+
return message
async def send_message_and_delete(client, entity, text, delete_after_seconds=None, **kwargs):
- """发送消息并安排自动删除
-
- 参数:
- client: Telethon客户端对象
- entity: 聊天对象或ID
- text: 要发送的文本
- delete_after_seconds: 多少秒后删除消息,None使用默认值,0表示立即删除,-1表示不删除
- **kwargs: 传递给send_message方法的其他参数
+ """Send a message and schedule auto-deletion
+
+ Args:
+ client: Telethon client object
+ entity: Chat object or ID
+ text: Text to send
+ delete_after_seconds: Number of seconds before deleting the message, None uses default, 0 means delete immediately, -1 means do not delete
+ **kwargs: Other parameters passed to the send_message method
"""
- # 如果没有指定删除时间,使用环境变量中的默认值
+ # If no deletion time is specified, use the default from environment variable
if delete_after_seconds is None:
deletion_timeout = BOT_MESSAGE_DELETE_TIMEOUT
else:
deletion_timeout = delete_after_seconds
-
- # 发送消息
+
+ # Send message
message = await client.send_message(entity, text, **kwargs)
-
- # 安排删除任务,只有当deletion_timeout不等于-1时才删除
+
+ # Schedule deletion task, only delete when deletion_timeout is not -1
if deletion_timeout != -1:
asyncio.create_task(delete_after(message, deletion_timeout))
-
+
return message
-# 删除用户消息
+# Delete user message
async def async_delete_user_message(client, chat_id, message_id, seconds):
- """删除用户消息
-
- 参数:
- client: bot客户端
- chat_id: 聊天ID
- message_id: 消息ID
- seconds: 等待多少秒后删除, 0表示立即删除, -1表示不删除
+ """Delete a user message
+
+ Args:
+ client: Bot client
+ chat_id: Chat ID
+ message_id: Message ID
+ seconds: Number of seconds to wait before deleting, 0 means delete immediately, -1 means do not delete
"""
if USER_MESSAGE_DELETE_ENABLE == "false":
return
-
- if seconds == -1: # -1 表示不删除
+
+ if seconds == -1: # -1 means do not delete
return
-
- if seconds > 0: # 正数表示等待指定秒数再删除
+
+ if seconds > 0: # Positive number means wait the specified seconds before deleting
await asyncio.sleep(seconds)
-
+
try:
await client.delete_messages(chat_id, message_id)
except Exception as e:
- logger.error(f"删除用户消息失败: {e}")
-
+ logger.error(f"Failed to delete user message: {e}")
From bba0d3e668428159d51b60579b156907dfd36999 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:15:19 +0000
Subject: [PATCH 31/76] Translate Chinese to English in filters/push_filter.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
filters/push_filter.py | 402 ++++++++++++++++++++---------------------
1 file changed, 201 insertions(+), 201 deletions(-)
diff --git a/filters/push_filter.py b/filters/push_filter.py
index 3eaae33..fdba677 100644
--- a/filters/push_filter.py
+++ b/filters/push_filter.py
@@ -14,361 +14,361 @@
class PushFilter(BaseFilter):
"""
- 推送过滤器,利用apprise库推送消息
+ Push filter, uses the apprise library to push messages
"""
-
+
async def _process(self, context):
"""
- 推送消息
-
+ Push messages
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 若消息应继续处理则返回True,否则返回False
+ bool: Returns True if the message should continue processing, False otherwise
"""
rule = context.rule
client = context.client
event = context.event
-
- # 如果规则没有启用推送,直接返回
+
+ # If the rule does not have push enabled, return directly
if not rule.enable_push:
- logger.info('推送未启用,跳过推送')
+ logger.info('Push not enabled, skipping push')
return True
-
- # 获取规则ID和所有启用的推送配置
+
+ # Get rule ID and all enabled push configurations
rule_id = rule.id
session = get_session()
-
-
- logger.info(f"推送过滤器开始处理 - 规则ID: {rule_id}")
- logger.info(f"是否是媒体组: {context.is_media_group}")
- logger.info(f"媒体组消息数量: {len(context.media_group_messages) if context.media_group_messages else 0}")
- logger.info(f"已有媒体文件数量: {len(context.media_files) if context.media_files else 0}")
- logger.info(f"是否只推送不转发: {rule.enable_only_push}")
-
- # 跟踪已处理的文件
+
+
+ logger.info(f"Push filter starting processing - Rule ID: {rule_id}")
+ logger.info(f"Is media group: {context.is_media_group}")
+ logger.info(f"Media group message count: {len(context.media_group_messages) if context.media_group_messages else 0}")
+ logger.info(f"Existing media file count: {len(context.media_files) if context.media_files else 0}")
+ logger.info(f"Push only without forwarding: {rule.enable_only_push}")
+
+ # Track processed files
processed_files = []
-
+
try:
- # 获取所有启用的推送配置
+ # Get all enabled push configurations
push_configs = session.query(PushConfig).filter(
PushConfig.rule_id == rule_id,
PushConfig.enable_push_channel == True
).all()
-
+
if not push_configs:
- logger.info(f'规则 {rule_id} 没有启用的推送配置,跳过推送')
+ logger.info(f'Rule {rule_id} has no enabled push configurations, skipping push')
return True
-
- # 对媒体组消息进行推送
+
+ # Push media group messages
if context.is_media_group or (context.media_group_messages and context.skipped_media):
processed_files = await self._push_media_group(context, push_configs)
- # 对单条媒体消息进行推送
+ # Push single media message
elif context.media_files or context.skipped_media:
processed_files = await self._push_single_media(context, push_configs)
- # 对纯文本消息进行推送
+ # Push plain text message
else:
processed_files = await self._push_text_message(context, push_configs)
-
- logger.info(f'推送已发送到 {len(push_configs)} 个配置')
+
+ logger.info(f'Push sent to {len(push_configs)} configurations')
return True
-
+
except Exception as e:
- logger.error(f'推送过滤器处理出错: {str(e)}')
+ logger.error(f'Push filter processing error: {str(e)}')
logger.error(traceback.format_exc())
- context.errors.append(f"推送错误: {str(e)}")
+ context.errors.append(f"Push error: {str(e)}")
return False
finally:
session.close()
-
- # 只清理已处理的媒体文件
+
+ # Only clean up processed media files
if processed_files:
- logger.info(f'清理已处理的媒体文件,共 {len(processed_files)} 个')
+ logger.info(f'Cleaning up processed media files, total {len(processed_files)}')
for file_path in processed_files:
try:
if os.path.exists(str(file_path)):
os.remove(file_path)
- logger.info(f'删除已处理的媒体文件: {file_path}')
+ logger.info(f'Deleted processed media file: {file_path}')
except Exception as e:
- logger.error(f'删除媒体文件失败: {str(e)}')
-
+ logger.error(f'Failed to delete media file: {str(e)}')
+
async def _push_media_group(self, context, push_configs):
- """推送媒体组消息"""
+ """Push media group messages"""
rule = context.rule
client = context.client
event = context.event
-
- # 初始化文件列表
+
+ # Initialize file list
files = []
need_cleanup = False
-
+
try:
- # 如果没有媒体组消息(都超限了),发送文本和提示
+ # If there are no media group messages (all exceeded limit), send text and prompt
if not context.media_group_messages and context.skipped_media:
- logger.info(f'所有媒体都超限,发送文本和提示')
- # 构建提示信息
+ logger.info(f'All media exceeded limit, sending text and prompt')
+ # Build prompt information
text_to_send = context.message_text or ''
-
- # 设置原始消息链接
+
+ # Set original message link
if rule.is_original_link:
- context.original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
-
- # 添加每个超限文件的信息
+ context.original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+
+ # Add information for each exceeded file
for message, size, name in context.skipped_media:
- text_to_send += f"\n\n⚠️ 媒体文件 {name if name else '未命名文件'} ({size}MB) 超过大小限制"
-
- # 组合完整文本
+ text_to_send += f"\n\n⚠️ Media file {name if name else 'unnamed file'} ({size}MB) exceeds size limit"
+
+ # Combine complete text
if rule.is_original_sender:
text_to_send = context.sender_info + text_to_send
if rule.is_original_time:
text_to_send += context.time_info
if rule.is_original_link:
text_to_send += context.original_link
-
- # 发送文本推送
+
+ # Send text push
await self._send_push_notification(push_configs, text_to_send)
return
-
- # 检查是否有媒体组消息但没有媒体文件(这是关键修复)
+
+ # Check if there are media group messages but no media files (this is the key fix)
if context.media_group_messages and not context.media_files:
- logger.info(f'检测到媒体组消息但没有媒体文件,开始下载...')
+ logger.info(f'Detected media group messages but no media files, starting download...')
need_cleanup = True
for message in context.media_group_messages:
if message.media:
file_path = await message.download_media(os.path.join(os.getcwd(), 'temp'))
if file_path:
files.append(file_path)
- logger.info(f'已下载媒体组文件: {file_path}')
- # 如果SenderFilter已经下载了文件,使用它们
+ logger.info(f'Downloaded media group file: {file_path}')
+ # If SenderFilter already downloaded files, use them
elif context.media_files:
- logger.info(f'使用SenderFilter已下载的文件: {len(context.media_files)}个')
+ logger.info(f'Using files already downloaded by SenderFilter: {len(context.media_files)}')
files = context.media_files
- # 否则,需要自己下载文件
+ # Otherwise, need to download files ourselves
elif rule.enable_only_push:
- logger.info(f'需要自己下载文件,开始下载媒体组消息...')
+ logger.info(f'Need to download files ourselves, starting media group message download...')
need_cleanup = True
for message in context.media_group_messages:
if message.media:
file_path = await message.download_media(os.path.join(os.getcwd(), 'temp'))
if file_path:
files.append(file_path)
- logger.info(f'已下载媒体文件: {file_path}')
-
- # 如果有可用的媒体文件,构建推送内容
+ logger.info(f'Downloaded media file: {file_path}')
+
+ # If there are available media files, build push content
if files:
- # 添加发送者信息和消息文本
+ # Add sender info and message text
caption_text = ""
if rule.is_original_sender and context.sender_info:
caption_text += context.sender_info
caption_text += context.message_text or ""
-
- # 如果有超限文件,添加提示信息
+
+ # If there are exceeded files, add prompt information
for message, size, name in context.skipped_media:
- caption_text += f"\n\n⚠️ 媒体文件 {name if name else '未命名文件'} ({size}MB) 超过大小限制"
-
- # 添加原始链接
+ caption_text += f"\n\n⚠️ Media file {name if name else 'unnamed file'} ({size}MB) exceeds size limit"
+
+ # Add original link
if rule.is_original_link and context.skipped_media:
- original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+ original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
caption_text += original_link
-
- # 添加时间信息
+
+ # Add time info
if rule.is_original_time and context.time_info:
caption_text += context.time_info
-
- # 设置默认描述(如果没有文本内容)
- default_caption = f"收到一组媒体文件 (共{len(files)}个)"
-
- # 按配置的媒体发送方式分别处理每个推送配置
+
+ # Set default description (if no text content)
+ default_caption = f"Received a group of media files (total {len(files)})"
+
+ # Handle each push configuration separately based on configured media send mode
processed_files = []
-
+
for config in push_configs:
- # 获取该配置的媒体发送模式
- send_mode = config.media_send_mode # "Single" 或 "Multiple"
-
- # 检查所有文件是否存在
+ # Get the media send mode for this configuration
+ send_mode = config.media_send_mode # "Single" or "Multiple"
+
+ # Check if all files exist
valid_files = [f for f in files if os.path.exists(str(f))]
if not valid_files:
continue
-
- # 根据媒体发送模式来决定发送方式
+
+ # Decide send method based on media send mode
if send_mode == "Multiple":
try:
- logger.info(f'尝试一次性发送 {len(valid_files)} 个文件到 {config.push_channel},模式: {send_mode}')
+ logger.info(f'Trying to send {len(valid_files)} files at once to {config.push_channel}, mode: {send_mode}')
await self._send_push_notification(
- [config],
- caption_text or f"收到一组媒体文件 (共{len(valid_files)}个)",
- None, # 不使用单附件参数
- valid_files # 使用多附件参数
+ [config],
+ caption_text or f"Received a group of media files (total {len(valid_files)})",
+ None, # Don't use single attachment parameter
+ valid_files # Use multiple attachments parameter
)
processed_files.extend(valid_files)
except Exception as e:
- logger.error(f'尝试一次性发送多个文件失败,错误: {str(e)}')
- # 如果一次性发送失败,则尝试逐个发送
+ logger.error(f'Failed to send multiple files at once, error: {str(e)}')
+ # If sending at once fails, try sending one by one
for i, file_path in enumerate(valid_files):
- # 第一个文件使用完整文本,后续文件使用简短描述
- file_caption = caption_text if i == 0 else f"媒体组的第 {i+1} 个文件"
+ # First file uses full text, subsequent files use short description
+ file_caption = caption_text if i == 0 else f"File {i+1} of the media group"
await self._send_push_notification([config], file_caption, file_path)
processed_files.append(file_path)
- # 逐个发送文件
+ # Send files one by one
else:
for i, file_path in enumerate(valid_files):
- # 第一个文件使用完整文本,后续文件使用简短描述
+ # First file uses full text, subsequent files use short description
if i == 0:
- file_caption = caption_text or f"收到一组媒体文件 (共{len(valid_files)}个)"
+ file_caption = caption_text or f"Received a group of media files (total {len(valid_files)})"
else:
- file_caption = f"媒体组的第 {i+1} 个文件" if len(valid_files) > 1 else ""
-
+ file_caption = f"File {i+1} of the media group" if len(valid_files) > 1 else ""
+
await self._send_push_notification([config], file_caption, file_path)
processed_files.append(file_path)
-
+
except Exception as e:
- logger.error(f'推送媒体组消息时出错: {str(e)}')
+ logger.error(f'Error pushing media group message: {str(e)}')
logger.error(traceback.format_exc())
raise
finally:
- # 如果是自己下载的文件,立即清理
+ # If files were downloaded by us, clean up immediately
if need_cleanup:
for file_path in files:
try:
if os.path.exists(str(file_path)):
os.remove(file_path)
- logger.info(f'删除临时文件: {file_path}')
- # 移除已删除的文件,避免重复删除
+ logger.info(f'Deleted temporary file: {file_path}')
+ # Remove already deleted files to avoid duplicate deletion
if file_path in processed_files:
processed_files.remove(file_path)
except Exception as e:
- logger.error(f'删除临时文件失败: {str(e)}')
-
- # 返回处理过但未删除的文件
+ logger.error(f'Failed to delete temporary file: {str(e)}')
+
+ # Return processed but not yet deleted files
return processed_files
-
+
async def _push_single_media(self, context, push_configs):
- """推送单条媒体消息"""
+ """Push single media message"""
rule = context.rule
client = context.client
event = context.event
-
- logger.info(f'推送单条媒体消息')
-
- # 初始化处理文件列表
+
+ logger.info(f'Pushing single media message')
+
+ # Initialize processed file list
processed_files = []
-
- # 检查是否所有媒体都超限
+
+ # Check if all media exceeded limit
if context.skipped_media and not context.media_files:
- # 构建提示信息
+ # Build prompt information
file_size = context.skipped_media[0][1]
file_name = context.skipped_media[0][2]
-
+
text_to_send = context.message_text or ''
- text_to_send += f"\n\n⚠️ 媒体文件 {file_name} ({file_size}MB) 超过大小限制"
-
- # 添加发送者信息
+ text_to_send += f"\n\n⚠️ Media file {file_name} ({file_size}MB) exceeds size limit"
+
+ # Add sender info
if rule.is_original_sender:
text_to_send = context.sender_info + text_to_send
-
- # 添加时间信息
+
+ # Add time info
if rule.is_original_time:
text_to_send += context.time_info
-
- # 添加原始链接
+
+ # Add original link
if rule.is_original_link:
- original_link = f"\n原始消息: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
+ original_link = f"\nOriginal message: https://t.me/c/{str(event.chat_id)[4:]}/{event.message.id}"
text_to_send += original_link
-
- # 发送文本推送
+
+ # Send text push
await self._send_push_notification(push_configs, text_to_send)
return processed_files
-
- # 处理媒体文件
+
+ # Process media files
files = []
need_cleanup = False
-
+
try:
- # 如果SenderFilter已经下载了文件,使用它们
+ # If SenderFilter already downloaded files, use them
if context.media_files:
- logger.info(f'使用SenderFilter已下载的文件: {len(context.media_files)}个')
+ logger.info(f'Using files already downloaded by SenderFilter: {len(context.media_files)}')
files = context.media_files
- # 否则,需要自己下载文件
+ # Otherwise, need to download files ourselves
elif rule.enable_only_push and event.message and event.message.media:
- logger.info(f'需要自己下载文件,开始下载单个媒体消息...')
+ logger.info(f'Need to download files ourselves, starting single media message download...')
need_cleanup = True
file_path = await event.message.download_media(os.path.join(os.getcwd(), 'temp'))
if file_path:
files.append(file_path)
- logger.info(f'已下载媒体文件: {file_path}')
-
- # 发送媒体文件
+ logger.info(f'Downloaded media file: {file_path}')
+
+ # Send media files
for file_path in files:
try:
- # 构建推送内容
+ # Build push content
caption = ""
if rule.is_original_sender and context.sender_info:
caption += context.sender_info
caption += context.message_text or ""
-
- # 添加时间信息
+
+ # Add time info
if rule.is_original_time and context.time_info:
caption += context.time_info
-
- # 添加原始链接
+
+ # Add original link
if rule.is_original_link and context.original_link:
caption += context.original_link
-
- # 如果没有文本内容,添加默认描述
+
+ # If no text content, add default description
if not caption:
- # 根据文件类型设置描述
+ # Set description based on file type
caption = " "
# ext = os.path.splitext(str(file_path))[1].lower()
# if ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
- # caption = "收到一张图片"
+ # caption = "Received a photo"
# elif ext in ['.mp4', '.avi', '.mkv', '.mov', '.webm']:
- # caption = "收到一个视频"
+ # caption = "Received a video"
# elif ext in ['.mp3', '.wav', '.ogg', '.flac']:
- # caption = "收到一个音频文件"
+ # caption = "Received an audio file"
# else:
- # caption = f"收到一个文件 ({ext})"
-
- # 发送推送
+ # caption = f"Received a file ({ext})"
+
+ # Send push
await self._send_push_notification(push_configs, caption, file_path)
- # 添加到已处理文件列表
+ # Add to processed file list
processed_files.append(file_path)
-
+
except Exception as e:
- logger.error(f'推送单个媒体文件时出错: {str(e)}')
+ logger.error(f'Error pushing single media file: {str(e)}')
logger.error(traceback.format_exc())
raise
-
+
except Exception as e:
- logger.error(f'推送单条媒体消息时出错: {str(e)}')
+ logger.error(f'Error pushing single media message: {str(e)}')
logger.error(traceback.format_exc())
raise
finally:
- # 如果是自己下载的文件,需要清理
+ # If files were downloaded by us, need to clean up
if need_cleanup:
for file_path in files:
try:
if os.path.exists(str(file_path)):
os.remove(file_path)
- logger.info(f'删除临时文件: {file_path}')
- # 从已处理列表中移除
+ logger.info(f'Deleted temporary file: {file_path}')
+ # Remove from processed list
if file_path in processed_files:
processed_files.remove(file_path)
except Exception as e:
- logger.error(f'删除临时文件失败: {str(e)}')
-
- # 返回处理过但未删除的文件
+ logger.error(f'Failed to delete temporary file: {str(e)}')
+
+ # Return processed but not yet deleted files
return processed_files
-
+
async def _push_text_message(self, context, push_configs):
- """推送纯文本消息"""
+ """Push plain text message"""
rule = context.rule
-
+
if not context.message_text:
- logger.info('没有文本内容,不发送推送')
+ logger.info('No text content, not sending push')
return []
-
- # 组合消息文本
+
+ # Combine message text
message_text = ""
if rule.is_original_sender and context.sender_info:
message_text += context.sender_info
@@ -377,63 +377,63 @@ async def _push_text_message(self, context, push_configs):
message_text += context.time_info
if rule.is_original_link and context.original_link:
message_text += context.original_link
-
- # 发送推送
+
+ # Send push
await self._send_push_notification(push_configs, message_text)
- logger.info(f'文本消息推送已发送')
-
- # 返回空列表,表示没有处理任何文件
+ logger.info(f'Text message push sent')
+
+ # Return empty list, indicating no files were processed
return []
-
+
async def _send_push_notification(self, push_configs, body, attachment=None, all_attachments=None):
- """发送推送通知"""
+ """Send push notification"""
if not body and not attachment and not all_attachments:
- logger.warning('没有内容可推送')
+ logger.warning('No content to push')
return
-
+
for config in push_configs:
try:
- # 创建Apprise对象
+ # Create Apprise object
apobj = apprise.Apprise()
-
- # 添加推送服务
+
+ # Add push service
service_url = config.push_channel
if apobj.add(service_url):
- logger.info(f'成功添加推送服务: {service_url}')
+ logger.info(f'Successfully added push service: {service_url}')
else:
- logger.error(f'添加推送服务失败: {service_url}')
+ logger.error(f'Failed to add push service: {service_url}')
continue
-
- # 发送推送
+
+ # Send push
if all_attachments and len(all_attachments) > 0 and config.media_send_mode == "Multiple":
- # 尝试一次性发送所有附件
- logger.info(f'发送带{len(all_attachments)}个附件的推送,模式: {config.media_send_mode}')
+ # Try to send all attachments at once
+ logger.info(f'Sending push with {len(all_attachments)} attachments, mode: {config.media_send_mode}')
send_result = await asyncio.to_thread(
apobj.notify,
- body=body or f"收到{len(all_attachments)}个媒体文件",
+ body=body or f"Received {len(all_attachments)} media files",
attach=all_attachments
)
elif attachment and os.path.exists(str(attachment)):
- # 单附件推送
- logger.info(f'发送带单个附件的推送: {os.path.basename(str(attachment))}')
+ # Single attachment push
+ logger.info(f'Sending push with single attachment: {os.path.basename(str(attachment))}')
send_result = await asyncio.to_thread(
apobj.notify,
body=body or " ",
attach=attachment
)
else:
- # 纯文本推送
- logger.info('发送纯文本推送')
+ # Plain text push
+ logger.info('Sending plain text push')
send_result = await asyncio.to_thread(
apobj.notify,
body=body
)
-
+
if send_result:
- logger.info(f'推送发送成功: {service_url}')
+ logger.info(f'Push sent successfully: {service_url}')
else:
- logger.error(f'推送发送失败: {service_url}')
-
+ logger.error(f'Push sending failed: {service_url}')
+
except Exception as e:
- logger.error(f'发送推送时出错: {str(e)}')
- logger.error(traceback.format_exc())
\ No newline at end of file
+ logger.error(f'Error sending push: {str(e)}')
+ logger.error(traceback.format_exc())
From c52335e83a92b856086b9cc32833f8676284d50a Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:15:42 +0000
Subject: [PATCH 32/76] Translate Chinese to English in
filters/replace_filter.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
filters/replace_filter.py | 52 +++++++++++++++++++--------------------
1 file changed, 26 insertions(+), 26 deletions(-)
diff --git a/filters/replace_filter.py b/filters/replace_filter.py
index f2567a8..6d45d02 100644
--- a/filters/replace_filter.py
+++ b/filters/replace_filter.py
@@ -6,39 +6,39 @@
class ReplaceFilter(BaseFilter):
"""
- 替换过滤器,根据规则替换消息文本
+ Replace filter, replaces message text based on rules
"""
-
+
async def _process(self, context):
"""
- 处理消息文本替换
-
+ Process message text replacement
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
rule = context.rule
message_text = context.message_text
- #打印context的所有属性
- # logger.info(f"ReplaceFilter处理消息前,context: {context.__dict__}")
- # 如果不需要替换,直接返回
+ # Print all attributes of context
+ # logger.info(f"Before ReplaceFilter processing, context: {context.__dict__}")
+ # If replacement is not needed, return directly
if not rule.is_replace or not message_text:
return True
-
+
try:
- # 应用所有替换规则
+ # Apply all replacement rules
for replace_rule in rule.replace_rules:
if replace_rule.pattern == '.*':
- # 全文替换
- logger.info(f'执行全文替换:\n原文: "{message_text}"\n替换为: "{replace_rule.content or ""}"')
+ # Full text replacement
+ logger.info(f'Performing full text replacement:\nOriginal: "{message_text}"\nReplaced with: "{replace_rule.content or ""}"')
message_text = replace_rule.content or ''
- break # 如果是全文替换,就不继续处理其他规则
+ break # If it's a full text replacement, don't continue processing other rules
else:
try:
- # 正则替换
+ # Regex replacement
old_text = message_text
matches = re.finditer(replace_rule.pattern, message_text)
message_text = re.sub(
@@ -48,19 +48,19 @@ async def _process(self, context):
)
if old_text != message_text:
matched_texts = [m.group(0) for m in matches]
- logger.info(f'执行部分替换:\n原文: "{old_text}"\n匹配内容: {matched_texts}\n替换规则: "{replace_rule.pattern}" -> "{replace_rule.content}"\n替换后: "{message_text}"')
+ logger.info(f'Performing partial replacement:\nOriginal: "{old_text}"\nMatched content: {matched_texts}\nReplacement rule: "{replace_rule.pattern}" -> "{replace_rule.content}"\nAfter replacement: "{message_text}"')
except re.error as e:
- logger.error(f'替换规则格式错误: {replace_rule.pattern}, 错误: {str(e)}')
-
- # 更新上下文中的消息文本
+ logger.error(f'Replacement rule format error: {replace_rule.pattern}, error: {str(e)}')
+
+ # Update message text in context
context.message_text = message_text
context.check_message_text = message_text
-
+
return True
except Exception as e:
- logger.error(f'应用替换规则时出错: {str(e)}')
- context.errors.append(f"替换规则错误: {str(e)}")
- return True # 即使替换出错,仍然继续处理
+ logger.error(f'Error applying replacement rules: {str(e)}')
+ context.errors.append(f"Replacement rule error: {str(e)}")
+ return True # Even if replacement fails, continue processing
finally:
- # logger.info(f"ReplaceFilter处理消息后,context: {context.__dict__}")
- pass
\ No newline at end of file
+ # logger.info(f"After ReplaceFilter processing, context: {context.__dict__}")
+ pass
From 2b536a8b841e054e1eb23d6cb98a7b3b50eea5b5 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:15:45 +0000
Subject: [PATCH 33/76] Translate Chinese to English in models/db_operations.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
models/db_operations.py | 884 ++++++++++++++++++++--------------------
1 file changed, 442 insertions(+), 442 deletions(-)
diff --git a/models/db_operations.py b/models/db_operations.py
index 5ed62da..e35778b 100644
--- a/models/db_operations.py
+++ b/models/db_operations.py
@@ -24,24 +24,24 @@ def __init__(self):
@classmethod
async def create(cls):
- """创建DBOperations实例"""
+ """Create DBOperations instance"""
instance = cls()
await instance.init_ufb()
return instance
async def init_ufb(self):
- """初始化UFB客户端"""
+ """Initialize UFB client"""
try:
- # 从环境变量获取UFB配置
- logger.info("初始化UFB客户端")
+ # Get UFB configuration from environment variables
+ logger.info("Initializing UFB client")
is_ufb = os.getenv('UFB_ENABLED', 'false').lower() == 'true'
if is_ufb:
server_url = os.getenv('UFB_SERVER_URL', '')
token = os.getenv('UFB_TOKEN')
- logger.info(f"UFB配置: server_url={server_url}, token={token and '***'}")
-
+ logger.info(f"UFB configuration: server_url={server_url}, token={token and '***'}")
+
if server_url and token:
- # 处理URL
+ # Process URL
if not server_url.startswith(('ws://', 'wss://')):
if server_url.startswith('http://'):
server_url = f"ws://{server_url[7:]}"
@@ -49,57 +49,57 @@ async def init_ufb(self):
server_url = f"wss://{server_url[8:]}"
else:
server_url = f"wss://{server_url}"
-
- logger.info(f"处理后的URL: {server_url}")
+
+ logger.info(f"Processed URL: {server_url}")
self.ufb_client = UFBClient()
- logger.info("UFB客户端已创建")
-
+ logger.info("UFB client created")
+
try:
await self.ufb_client.start(server_url=server_url, token=token)
- logger.info("UFB客户端已启动")
+ logger.info("UFB client started")
except Exception as e:
- logger.error(f"UFB客户端启动失败: {str(e)}")
+ logger.error(f"UFB client failed to start: {str(e)}")
self.ufb_client = None
else:
- logger.warning("UFB配置不完整,未启用UFB功能")
+ logger.warning("UFB configuration is incomplete, UFB feature not enabled")
self.ufb_client = None
else:
- logger.info("UFB未启用")
+ logger.info("UFB is not enabled")
except Exception as e:
- logger.error(f"初始化UFB时出错: {str(e)}")
+ logger.error(f"Error initializing UFB: {str(e)}")
self.ufb_client = None
-
-
+
+
async def sync_to_server(self,session,rule_id):
- """同步UFB配置"""
+ """Sync UFB configuration"""
if self.ufb_client and os.getenv('UFB_ENABLED').lower() == 'true':
- # 通过rule_id获取规则ufb是否开启
+ # Check if UFB is enabled for this rule via rule_id
rule = session.query(ForwardRule).filter(ForwardRule.id == rule_id).first()
ufb_domain = rule.ufb_domain
if rule.is_ufb and ufb_domain:
item = rule.ufb_item
- # 获取规则的所有非正则表达关键字
+ # Get all non-regex keywords for the rule
normal_keywords = session.query(Keyword).filter(
Keyword.rule_id == rule_id,
Keyword.is_regex == False
).all()
-
- # 获取规则的所有正则表达关键字
+
+ # Get all regex keywords for the rule
regex_keywords = session.query(Keyword).filter(
Keyword.rule_id == rule_id,
Keyword.is_regex == True
).all()
- # 获取../ufb/config/config.json文件
+ # Get ../ufb/config/config.json file
config_file = Path(__file__).parent.parent / 'ufb' / 'config' / 'config.json'
- # 读取文件
+ # Read file
with open(config_file, 'r', encoding='utf-8') as file:
config = json.load(file)
- # 在userConfig中找到对应domain的配置
+ # Find the configuration for the corresponding domain in userConfig
for user_config in config.get('userConfig', []):
if user_config.get('domain') == ufb_domain:
- # 根据item类型更新关键字
+ # Update keywords based on item type
if item == 'main':
keywords_config = user_config.get('mainAndSubPageKeywords', {})
elif item == 'content':
@@ -109,12 +109,12 @@ async def sync_to_server(self,session,rule_id):
elif item == 'content_username':
keywords_config = user_config.get('contentPageUserKeywords', {})
- # 更新关键字列表
+ # Update keyword lists
keywords_config['keywords'] = [k.keyword for k in normal_keywords]
keywords_config['regexPatterns'] = [k.keyword for k in regex_keywords]
- # 保存回对应的位置
- if item == 'main':
+ # Save back to corresponding location
+ if item == 'main':
user_config['mainAndSubPageKeywords'] = keywords_config
elif item == 'content':
user_config['contentPageKeywords'] = keywords_config
@@ -123,66 +123,66 @@ async def sync_to_server(self,session,rule_id):
elif item == 'content_username':
user_config['contentPageUserKeywords'] = keywords_config
else:
- logger.error(f"未设置UFB_ITEM环境变量")
+ logger.error(f"UFB_ITEM environment variable is not set")
return
break
-
- # 更新时间戳
+
+ # Update timestamp
config['globalConfig']['SYNC_CONFIG']['lastSyncTime'] = int(time.time() * 1000)
- # 保存到本地文件
+ # Save to local file
with open(config_file, 'w', encoding='utf-8') as file:
json.dump(config, file, ensure_ascii=False, indent=2)
- # 更新配置到服务器
+ # Update configuration to server
if self.ufb_client.is_connected:
await self.ufb_client.websocket.send(json.dumps({
"additional_info": "to_server",
"type": "update",
**config
}))
- logger.info("UFB配置已同步")
+ logger.info("UFB configuration synced")
else:
- logger.warning("UFB客户端未连接,无法同步配置")
+ logger.warning("UFB client is not connected, unable to sync configuration")
else:
- logger.warning("UFB未开启,无法同步配置")
+ logger.warning("UFB is not enabled, unable to sync configuration")
else:
- logger.warning("UFB客户端未初始化,无法同步配置")
+ logger.warning("UFB client is not initialized, unable to sync configuration")
async def sync_from_json(self, config):
- """从收到的JSON配置同步关键字到数据库
-
+ """Sync keywords from received JSON configuration to database
+
Args:
- config: 收到的配置数据
+ config: Received configuration data
"""
- logger.info(f"从JSON同步关键字到数据库")
+ logger.info(f"Syncing keywords from JSON to database")
session = get_session()
try:
- # 获取所有启用了UFB的规则
+ # Get all rules with UFB enabled
ufb_rules = session.query(ForwardRule).filter(
ForwardRule.is_ufb == True,
ForwardRule.ufb_domain != None
).all()
-
+
if not ufb_rules:
- logger.info("没有找到启用UFB的规则")
+ logger.info("No rules with UFB enabled found")
return
logger.info(f"ufb_rules: {ufb_rules}")
-
- # 遍历所有启用UFB的规则
+
+ # Iterate through all UFB-enabled rules
for rule in ufb_rules:
- # 获取item类型
+ # Get item type
item = rule.ufb_item
logger.info(f"item: {item}")
if not item:
- logger.error("未设置UFB_ITEM环境变量")
- continue # 跳过没有设置 item 的规则
-
- # 在收到的配置中查找对应domain的配置
+ logger.error("UFB_ITEM environment variable is not set")
+ continue # Skip rules without item set
+
+ # Find the configuration for the corresponding domain in received config
for user_config in config.get('userConfig', []):
if user_config.get('domain') == rule.ufb_domain:
- logger.info(f"找到匹配的domain配置: {rule.ufb_domain}")
-
- # 根据item类型获取关键字配置
+ logger.info(f"Found matching domain configuration: {rule.ufb_domain}")
+
+ # Get keyword configuration based on item type
if item == 'main':
keywords_config = user_config.get('mainAndSubPageKeywords', {})
elif item == 'content':
@@ -192,15 +192,15 @@ async def sync_from_json(self, config):
elif item == 'content_username':
keywords_config = user_config.get('contentPageUserKeywords', {})
else:
- logger.error(f"未设置UFB_ITEM环境变量")
+ logger.error(f"UFB_ITEM environment variable is not set")
continue
-
- # 清空现有关键字
+
+ # Clear existing keywords
session.query(Keyword).filter(
Keyword.rule_id == rule.id
).delete()
-
- # 添加普通关键字
+
+ # Add normal keywords
for keyword in keywords_config.get('keywords', []):
new_keyword = Keyword(
rule_id=rule.id,
@@ -208,8 +208,8 @@ async def sync_from_json(self, config):
is_regex=False
)
session.add(new_keyword)
-
- # 添加正则关键字
+
+ # Add regex keywords
for pattern in keywords_config.get('regexPatterns', []):
new_keyword = Keyword(
rule_id=rule.id,
@@ -217,39 +217,39 @@ async def sync_from_json(self, config):
is_regex=True
)
session.add(new_keyword)
-
+
session.commit()
- logger.info(f"已从JSON同步关键字到规则 {rule.id} (domain: {rule.ufb_domain})")
- break # 找到匹配的domain后跳出内层循环
+ logger.info(f"Synced keywords from JSON to rule {rule.id} (domain: {rule.ufb_domain})")
+ break # Break inner loop after finding matching domain
finally:
session.close()
async def add_keywords(self, session, rule_id, keywords, is_regex=False, is_blacklist=False):
- """添加关键字到规则
+ """Add keywords to rule
Args:
- session: 数据库会话
- rule_id: 规则ID
- keywords: 关键字列表
- is_regex: 是否是正则表达式
- is_blacklist: 是否为黑名单关键字
+ session: Database session
+ rule_id: Rule ID
+ keywords: Keyword list
+ is_regex: Whether it is a regular expression
+ is_blacklist: Whether it is a blacklist keyword
Returns:
- tuple: (成功数量, 重复数量)
+ tuple: (success count, duplicate count)
"""
success_count = 0
duplicate_count = 0
- # 获取当前规则
+ # Get current rule
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- logger.error(f"规则ID {rule_id} 不存在")
+ logger.error(f"Rule ID {rule_id} does not exist")
return 0, 0
- # 处理单个规则的关键字添加
+ # Process keyword addition for single rule
for keyword in keywords:
try:
- # 检查是否存在相同的关键字(考虑黑白名单)
+ # Check if the same keyword exists (considering blacklist/whitelist)
existing_keyword = session.query(Keyword).filter(
Keyword.rule_id == rule_id,
Keyword.keyword == keyword,
@@ -270,45 +270,45 @@ async def add_keywords(self, session, rule_id, keywords, is_regex=False, is_blac
session.flush()
success_count += 1
except Exception as e:
- logger.error(f"添加关键字时出错: {str(e)}")
+ logger.error(f"Error adding keyword: {str(e)}")
session.rollback()
duplicate_count += 1
continue
- # 检查是否启用了同步功能
+ # Check if sync feature is enabled
if rule.enable_sync:
- logger.info(f"规则 {rule_id} 启用了同步功能,正在同步关键字到关联规则")
- # 获取需要同步的规则列表
+ logger.info(f"Rule {rule_id} has sync enabled, syncing keywords to associated rules")
+ # Get list of rules to sync
sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule_id).all()
-
- # 为每个同步规则添加相同的关键字
+
+ # Add same keywords to each sync rule
for sync_rule in sync_rules:
sync_rule_id = sync_rule.sync_rule_id
- logger.info(f"正在同步关键字到规则 {sync_rule_id}")
-
- # 获取同步目标规则
+ logger.info(f"Syncing keywords to rule {sync_rule_id}")
+
+ # Get sync target rule
target_rule = session.query(ForwardRule).get(sync_rule_id)
if not target_rule:
- logger.warning(f"同步目标规则 {sync_rule_id} 不存在,跳过")
+ logger.warning(f"Sync target rule {sync_rule_id} does not exist, skipping")
continue
-
- # 为同步目标规则添加关键字
+
+ # Add keywords to sync target rule
sync_success = 0
sync_duplicate = 0
for keyword in keywords:
try:
- # 检查同步规则是否已有此关键字
+ # Check if sync rule already has this keyword
existing_keyword = session.query(Keyword).filter(
Keyword.rule_id == sync_rule_id,
Keyword.keyword == keyword,
Keyword.is_blacklist == is_blacklist
).first()
-
+
if existing_keyword:
sync_duplicate += 1
continue
-
- # 添加新关键字到同步规则
+
+ # Add new keyword to sync rule
new_keyword = Keyword(
rule_id=sync_rule_id,
keyword=keyword,
@@ -319,23 +319,23 @@ async def add_keywords(self, session, rule_id, keywords, is_regex=False, is_blac
session.flush()
sync_success += 1
except Exception as e:
- logger.error(f"同步关键字到规则 {sync_rule_id} 时出错: {str(e)}")
+ logger.error(f"Error syncing keyword to rule {sync_rule_id}: {str(e)}")
continue
-
- logger.info(f"同步规则 {sync_rule_id} 的结果: 成功={sync_success}, 重复={sync_duplicate}")
+
+ logger.info(f"Sync rule {sync_rule_id} result: success={sync_success}, duplicate={sync_duplicate}")
await self.sync_to_server(session, rule_id)
return success_count, duplicate_count
async def get_keywords(self, session, rule_id, add_mode):
- """获取规则的所有关键字
-
+ """Get all keywords for a rule
+
Args:
- session: 数据库会话
- rule_id: 规则ID
-
+ session: Database session
+ rule_id: Rule ID
+
Returns:
- list: 关键字列表
+ list: Keyword list
"""
return session.query(Keyword).filter(
Keyword.rule_id == rule_id,
@@ -343,31 +343,31 @@ async def get_keywords(self, session, rule_id, add_mode):
).all()
async def delete_keywords(self, session, rule_id, indices):
- """删除指定索引的关键字
-
+ """Delete keywords at specified indices
+
Args:
- session: 数据库会话
- rule_id: 规则ID
- indices: 要删除的索引列表(1-based)
-
+ session: Database session
+ rule_id: Rule ID
+ indices: List of indices to delete (1-based)
+
Returns:
- tuple: (删除数量, 剩余关键字列表)
+ tuple: (deleted count, remaining keyword list)
"""
- # 获取当前规则
+ # Get current rule
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- logger.error(f"规则ID {rule_id} 不存在")
+ logger.error(f"Rule ID {rule_id} does not exist")
return 0, []
-
- # 获取当前规则的关键字
+
+ # Get keywords for current rule
keywords = await self.get_keywords(session, rule_id, 'blacklist' if rule.add_mode == AddMode.BLACKLIST else 'whitelist')
if not keywords:
return 0, []
-
+
deleted_count = 0
max_id = len(keywords)
-
- # 保存要删除的关键字信息,用于后续同步
+
+ # Save keyword info to delete for subsequent sync
keywords_to_delete = []
for idx in indices:
if 1 <= idx <= max_id:
@@ -379,88 +379,88 @@ async def delete_keywords(self, session, rule_id, indices):
})
session.delete(keyword)
deleted_count += 1
-
- # 检查是否启用了同步功能
+
+ # Check if sync feature is enabled
if rule.enable_sync and keywords_to_delete:
- logger.info(f"规则 {rule_id} 启用了同步功能,正在同步删除关联规则的关键字")
- # 获取需要同步的规则列表
+ logger.info(f"Rule {rule_id} has sync enabled, syncing keyword deletion to associated rules")
+ # Get list of rules to sync
sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule_id).all()
-
- # 为每个同步规则删除相同的关键字
+
+ # Delete same keywords from each sync rule
for sync_rule in sync_rules:
sync_rule_id = sync_rule.sync_rule_id
- logger.info(f"正在同步删除规则 {sync_rule_id} 的关键字")
-
- # 获取同步目标规则
+ logger.info(f"Syncing keyword deletion to rule {sync_rule_id}")
+
+ # Get sync target rule
target_rule = session.query(ForwardRule).get(sync_rule_id)
if not target_rule:
- logger.warning(f"同步目标规则 {sync_rule_id} 不存在,跳过")
+ logger.warning(f"Sync target rule {sync_rule_id} does not exist, skipping")
continue
-
- # 在同步目标规则中删除相同的关键字
+
+ # Delete same keywords from sync target rule
sync_deleted = 0
for kw_info in keywords_to_delete:
try:
- # 查找目标规则中匹配的关键字
+ # Find matching keywords in target rule
target_keywords = session.query(Keyword).filter(
Keyword.rule_id == sync_rule_id,
Keyword.keyword == kw_info['keyword'],
Keyword.is_regex == kw_info['is_regex'],
Keyword.is_blacklist == kw_info['is_blacklist']
).all()
-
- # 删除匹配的关键字
+
+ # Delete matching keywords
for target_kw in target_keywords:
session.delete(target_kw)
sync_deleted += 1
except Exception as e:
- logger.error(f"同步删除规则 {sync_rule_id} 的关键字时出错: {str(e)}")
+ logger.error(f"Error syncing keyword deletion to rule {sync_rule_id}: {str(e)}")
continue
-
- logger.info(f"同步删除规则 {sync_rule_id} 的关键字: 删除了 {sync_deleted} 个")
+
+ logger.info(f"Synced keyword deletion to rule {sync_rule_id}: deleted {sync_deleted} items")
await self.sync_to_server(session, rule_id)
return deleted_count, await self.get_keywords(session, rule_id, 'blacklist' if rule.add_mode == AddMode.BLACKLIST else 'whitelist')
async def add_replace_rules(self, session, rule_id, patterns, contents=None):
- """添加替换规则
-
+ """Add replace rules
+
Args:
- session: 数据库会话
- rule_id: 规则ID
- patterns: 匹配模式列表
- contents: 替换内容列表(可选)
-
+ session: Database session
+ rule_id: Rule ID
+ patterns: Match pattern list
+ contents: Replacement content list (optional)
+
Returns:
- tuple: (成功数量, 重复数量)
+ tuple: (success count, duplicate count)
"""
- # 获取当前规则
+ # Get current rule
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- logger.error(f"规则ID {rule_id} 不存在")
+ logger.error(f"Rule ID {rule_id} does not exist")
return 0, 0
-
+
success_count = 0
duplicate_count = 0
-
+
if contents is None:
contents = [''] * len(patterns)
-
- # 添加替换规则到主规则
- added_rules = [] # 存储成功添加的规则,用于后续同步
+
+ # Add replace rules to main rule
+ added_rules = [] # Store successfully added rules for subsequent sync
for pattern, content in zip(patterns, contents):
try:
- # 检查是否已存在相同的替换规则
+ # Check if the same replace rule already exists
existing_rule = session.query(ReplaceRule).filter(
ReplaceRule.rule_id == rule_id,
ReplaceRule.pattern == pattern,
ReplaceRule.content == content
).first()
-
+
if existing_rule:
duplicate_count += 1
continue
-
+
new_rule = ReplaceRule(
rule_id=rule_id,
pattern=pattern,
@@ -474,41 +474,41 @@ async def add_replace_rules(self, session, rule_id, patterns, contents=None):
session.rollback()
duplicate_count += 1
continue
-
- # 检查是否启用了同步功能
+
+ # Check if sync feature is enabled
if rule.enable_sync and added_rules:
- logger.info(f"规则 {rule_id} 启用了同步功能,正在同步添加替换规则到关联规则")
- # 获取需要同步的规则列表
+ logger.info(f"Rule {rule_id} has sync enabled, syncing replace rules to associated rules")
+ # Get list of rules to sync
sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule_id).all()
-
- # 为每个同步规则添加相同的替换规则
+
+ # Add same replace rules to each sync rule
for sync_rule in sync_rules:
sync_rule_id = sync_rule.sync_rule_id
- logger.info(f"正在同步添加替换规则到规则 {sync_rule_id}")
-
- # 获取同步目标规则
+ logger.info(f"Syncing replace rules to rule {sync_rule_id}")
+
+ # Get sync target rule
target_rule = session.query(ForwardRule).get(sync_rule_id)
if not target_rule:
- logger.warning(f"同步目标规则 {sync_rule_id} 不存在,跳过")
+ logger.warning(f"Sync target rule {sync_rule_id} does not exist, skipping")
continue
-
- # 为同步目标规则添加替换规则
+
+ # Add replace rules to sync target rule
sync_success = 0
sync_duplicate = 0
for rule_info in added_rules:
try:
- # 检查同步规则是否已有此替换规则
+ # Check if sync rule already has this replace rule
existing_rule = session.query(ReplaceRule).filter(
ReplaceRule.rule_id == sync_rule_id,
ReplaceRule.pattern == rule_info['pattern'],
ReplaceRule.content == rule_info['content']
).first()
-
+
if existing_rule:
sync_duplicate += 1
continue
-
- # 添加新替换规则到同步规则
+
+ # Add new replace rule to sync rule
new_rule = ReplaceRule(
rule_id=sync_rule_id,
pattern=rule_info['pattern'],
@@ -518,52 +518,52 @@ async def add_replace_rules(self, session, rule_id, patterns, contents=None):
session.flush()
sync_success += 1
except Exception as e:
- logger.error(f"同步添加替换规则到规则 {sync_rule_id} 时出错: {str(e)}")
+ logger.error(f"Error syncing replace rule to rule {sync_rule_id}: {str(e)}")
continue
-
- logger.info(f"同步规则 {sync_rule_id} 的替换规则添加结果: 成功={sync_success}, 重复={sync_duplicate}")
-
+
+ logger.info(f"Sync rule {sync_rule_id} replace rule addition result: success={sync_success}, duplicate={sync_duplicate}")
+
return success_count, duplicate_count
async def get_replace_rules(self, session, rule_id):
- """获取规则的所有替换规则
-
+ """Get all replace rules for a rule
+
Args:
- session: 数据库会话
- rule_id: 规则ID
-
+ session: Database session
+ rule_id: Rule ID
+
Returns:
- list: 替换规则列表
+ list: Replace rule list
"""
return session.query(ReplaceRule).filter(
ReplaceRule.rule_id == rule_id
).all()
async def delete_replace_rules(self, session, rule_id, indices):
- """删除指定索引的替换规则
-
+ """Delete replace rules at specified indices
+
Args:
- session: 数据库会话
- rule_id: 规则ID
- indices: 要删除的索引列表(1-based)
-
+ session: Database session
+ rule_id: Rule ID
+ indices: List of indices to delete (1-based)
+
Returns:
- tuple: (删除数量, 剩余替换规则列表)
+ tuple: (deleted count, remaining replace rule list)
"""
- # 获取当前规则
+ # Get current rule
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- logger.error(f"规则ID {rule_id} 不存在")
+ logger.error(f"Rule ID {rule_id} does not exist")
return 0, []
-
+
rules = await self.get_replace_rules(session, rule_id)
if not rules:
return 0, []
-
+
deleted_count = 0
max_id = len(rules)
-
- # 保存要删除的替换规则信息,用于后续同步
+
+ # Save replace rule info to delete for subsequent sync
rules_to_delete = []
for idx in indices:
if 1 <= idx <= max_id:
@@ -574,57 +574,57 @@ async def delete_replace_rules(self, session, rule_id, indices):
})
session.delete(replace_rule)
deleted_count += 1
-
- # 检查是否启用了同步功能
+
+ # Check if sync feature is enabled
if rule.enable_sync and rules_to_delete:
- logger.info(f"规则 {rule_id} 启用了同步功能,正在同步删除关联规则的替换规则")
- # 获取需要同步的规则列表
+ logger.info(f"Rule {rule_id} has sync enabled, syncing replace rule deletion to associated rules")
+ # Get list of rules to sync
sync_rules = session.query(RuleSync).filter(RuleSync.rule_id == rule_id).all()
-
- # 为每个同步规则删除相同的替换规则
+
+ # Delete same replace rules from each sync rule
for sync_rule in sync_rules:
sync_rule_id = sync_rule.sync_rule_id
- logger.info(f"正在同步删除规则 {sync_rule_id} 的替换规则")
-
- # 获取同步目标规则
+ logger.info(f"Syncing replace rule deletion to rule {sync_rule_id}")
+
+ # Get sync target rule
target_rule = session.query(ForwardRule).get(sync_rule_id)
if not target_rule:
- logger.warning(f"同步目标规则 {sync_rule_id} 不存在,跳过")
+ logger.warning(f"Sync target rule {sync_rule_id} does not exist, skipping")
continue
-
- # 在同步目标规则中删除相同的替换规则
+
+ # Delete same replace rules from sync target rule
sync_deleted = 0
for rule_info in rules_to_delete:
try:
- # 查找目标规则中匹配的替换规则
+ # Find matching replace rules in target rule
target_rules = session.query(ReplaceRule).filter(
ReplaceRule.rule_id == sync_rule_id,
ReplaceRule.pattern == rule_info['pattern'],
ReplaceRule.content == rule_info['content']
).all()
-
- # 删除匹配的替换规则
+
+ # Delete matching replace rules
for target_rule in target_rules:
session.delete(target_rule)
sync_deleted += 1
except Exception as e:
- logger.error(f"同步删除规则 {sync_rule_id} 的替换规则时出错: {str(e)}")
+ logger.error(f"Error syncing replace rule deletion to rule {sync_rule_id}: {str(e)}")
continue
-
- logger.info(f"同步删除规则 {sync_rule_id} 的替换规则: 删除了 {sync_deleted} 个")
-
+
+ logger.info(f"Synced replace rule deletion to rule {sync_rule_id}: deleted {sync_deleted} items")
+
return deleted_count, await self.get_replace_rules(session, rule_id)
async def get_media_types(self, session, rule_id):
- """获取媒体类型设置"""
+ """Get media type settings"""
try:
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- return False, "规则不存在", None
-
+ return False, "Rule does not exist", None
+
media_types = session.query(MediaTypes).filter_by(rule_id=rule_id).first()
if not media_types:
- # 如果不存在则创建默认设置
+ # Create default settings if not exist
media_types = MediaTypes(
rule_id=rule_id,
photo=False,
@@ -635,181 +635,181 @@ async def get_media_types(self, session, rule_id):
)
session.add(media_types)
session.commit()
-
- return True, "获取媒体类型设置成功", media_types
+
+ return True, "Successfully retrieved media type settings", media_types
except Exception as e:
- logger.error(f"获取媒体类型设置时出错: {str(e)}")
+ logger.error(f"Error getting media type settings: {str(e)}")
session.rollback()
- return False, f"获取媒体类型设置时出错: {str(e)}", None
+ return False, f"Error getting media type settings: {str(e)}", None
async def update_media_types(self, session, rule_id, media_types_dict):
- """更新媒体类型设置"""
+ """Update media type settings"""
try:
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- return False, "规则不存在"
-
+ return False, "Rule does not exist"
+
media_types = session.query(MediaTypes).filter_by(rule_id=rule_id).first()
if not media_types:
media_types = MediaTypes(rule_id=rule_id)
session.add(media_types)
-
- # 更新媒体类型设置
+
+ # Update media type settings
for field in ['photo', 'document', 'video', 'audio', 'voice']:
if field in media_types_dict:
setattr(media_types, field, media_types_dict[field])
-
+
session.commit()
- return True, "更新媒体类型设置成功"
+ return True, "Successfully updated media type settings"
except Exception as e:
- logger.error(f"更新媒体类型设置时出错: {str(e)}")
+ logger.error(f"Error updating media type settings: {str(e)}")
session.rollback()
- return False, f"更新媒体类型设置时出错: {str(e)}"
+ return False, f"Error updating media type settings: {str(e)}"
async def toggle_media_type(self, session, rule_id, media_type):
- """切换特定媒体类型的启用状态"""
+ """Toggle the enabled state of a specific media type"""
try:
if media_type not in ['photo', 'document', 'video', 'audio', 'voice']:
- return False, f"无效的媒体类型: {media_type}"
-
+ return False, f"Invalid media type: {media_type}"
+
success, msg, media_types = await self.get_media_types(session, rule_id)
if not success:
return False, msg
-
- # 切换状态
+
+ # Toggle state
current_value = getattr(media_types, media_type)
setattr(media_types, media_type, not current_value)
-
+
session.commit()
- return True, f"媒体类型 {media_type} 切换为 {not current_value}"
+ return True, f"Media type {media_type} toggled to {not current_value}"
except Exception as e:
- logger.error(f"切换媒体类型时出错: {str(e)}")
+ logger.error(f"Error toggling media type: {str(e)}")
session.rollback()
- return False, f"切换媒体类型时出错: {str(e)}"
+ return False, f"Error toggling media type: {str(e)}"
async def add_media_extensions(self, session, rule_id, extensions):
- """添加媒体扩展名
-
+ """Add media extensions
+
Args:
- session: 数据库会话
- rule_id: 规则ID
- extensions: 扩展名列表,比如 ['jpg', 'png', 'pdf']
-
+ session: Database session
+ rule_id: Rule ID
+ extensions: Extension list, e.g. ['jpg', 'png', 'pdf']
+
Returns:
- (bool, str): 成功状态和消息
+ (bool, str): Success status and message
"""
try:
added_count = 0
for ext in extensions:
- # 确保扩展名不带点,去除可能存在的点
+ # Ensure extension has no dot, strip any existing dot
ext = ext.lstrip('.')
-
- # 检查是否已存在相同的扩展名
+
+ # Check if the same extension already exists
existing = session.execute(
text("SELECT id FROM media_extensions WHERE rule_id = :rule_id AND extension = :extension"),
{"rule_id": rule_id, "extension": ext}
)
-
+
if existing.first() is None:
- # 添加新的扩展名
+ # Add new extension
new_extension = MediaExtensions(rule_id=rule_id, extension=ext)
session.add(new_extension)
added_count += 1
-
+
if added_count > 0:
session.commit()
- return True, f"成功添加 {added_count} 个媒体扩展名"
+ return True, f"Successfully added {added_count} media extensions"
else:
- return False, "所有扩展名已存在,未添加任何新扩展名"
-
+ return False, "All extensions already exist, no new extensions added"
+
except Exception as e:
session.rollback()
- logger.error(f"添加媒体扩展名失败: {str(e)}")
- return False, f"添加媒体扩展名失败: {str(e)}"
+ logger.error(f"Failed to add media extensions: {str(e)}")
+ return False, f"Failed to add media extensions: {str(e)}"
async def get_media_extensions(self, session, rule_id):
- """获取规则的媒体扩展名列表
-
+ """Get media extension list for a rule
+
Args:
- session: 数据库会话
- rule_id: 规则ID
-
+ session: Database session
+ rule_id: Rule ID
+
Returns:
- list: 媒体扩展名对象列表
+ list: Media extension object list
"""
try:
- # 使用SQLAlchemy文本SQL查询,不需要await
+ # Use SQLAlchemy text SQL query, no need for await
result = session.execute(
text("SELECT id, extension FROM media_extensions WHERE rule_id = :rule_id ORDER BY id"),
{"rule_id": rule_id}
)
-
- # 构建返回结果
+
+ # Build return result
extensions = []
for row in result:
extensions.append({
"id": row[0],
"extension": row[1]
})
-
- # 返回扩展名列表
+
+ # Return extension list
return extensions
-
+
except Exception as e:
- # 记录错误并返回空列表
- logger.error(f"获取媒体扩展名失败: {str(e)}")
+ # Log error and return empty list
+ logger.error(f"Failed to get media extensions: {str(e)}")
return []
async def delete_media_extensions(self, session, rule_id, indices):
- """删除媒体扩展名
-
+ """Delete media extensions
+
Args:
- session: 数据库会话
- rule_id: 规则ID
- indices: 要删除的扩展名ID列表
-
+ session: Database session
+ rule_id: Rule ID
+ indices: List of extension IDs to delete
+
Returns:
- (bool, str): 成功状态和消息
+ (bool, str): Success status and message
"""
try:
if not indices:
- return False, "未指定要删除的扩展名"
-
+ return False, "No extensions specified for deletion"
+
for index in indices:
- # 查找并删除扩展名
+ # Find and delete extension
result = session.execute(
text("SELECT id FROM media_extensions WHERE id = :id AND rule_id = :rule_id"),
{"id": index, "rule_id": rule_id}
)
-
+
extension = result.first()
if extension:
session.execute(
text("DELETE FROM media_extensions WHERE id = :id"),
{"id": extension[0]}
)
-
+
session.commit()
- return True, f"成功删除 {len(indices)} 个媒体扩展名"
+ return True, f"Successfully deleted {len(indices)} media extensions"
except Exception as e:
session.rollback()
- logger.error(f"删除媒体扩展名失败: {str(e)}")
- return False, f"删除媒体扩展名失败: {str(e)}"
+ logger.error(f"Failed to delete media extensions: {str(e)}")
+ return False, f"Failed to delete media extensions: {str(e)}"
- # RSS配置相关操作
+ # RSS configuration related operations
async def get_rss_config(self, session, rule_id):
- """获取指定规则的RSS配置"""
+ """Get RSS configuration for specified rule"""
return session.query(RSSConfig).filter(RSSConfig.rule_id == rule_id).first()
async def create_rss_config(self, session, rule_id, **kwargs):
- """创建RSS配置"""
+ """Create RSS configuration"""
rss_config = RSSConfig(rule_id=rule_id, **kwargs)
session.add(rss_config)
session.commit()
return rss_config
async def update_rss_config(self, session, rule_id, **kwargs):
- """更新RSS配置"""
+ """Update RSS configuration"""
rss_config = await self.get_rss_config(session, rule_id)
if rss_config:
for key, value in kwargs.items():
@@ -818,7 +818,7 @@ async def update_rss_config(self, session, rule_id, **kwargs):
return rss_config
async def delete_rss_config(self, session, rule_id):
- """删除RSS配置"""
+ """Delete RSS configuration"""
rss_config = await self.get_rss_config(session, rule_id)
if rss_config:
session.delete(rss_config)
@@ -826,18 +826,18 @@ async def delete_rss_config(self, session, rule_id):
return True
return False
- # RSS模式相关操作
+ # RSS pattern related operations
async def get_rss_patterns(self, session, rss_config_id):
- """获取指定RSS配置的所有模式"""
+ """Get all patterns for specified RSS configuration"""
return session.query(RSSPattern).filter(RSSPattern.rss_config_id == rss_config_id).order_by(RSSPattern.priority).all()
async def get_rss_pattern(self, session, pattern_id):
- """获取指定的RSS模式"""
+ """Get specified RSS pattern"""
return session.query(RSSPattern).filter(RSSPattern.id == pattern_id).first()
async def create_rss_pattern(self, session, rss_config_id, pattern, pattern_type, priority=0):
- """创建RSS模式"""
- logger.info(f"创建RSS模式:config_id={rss_config_id}, pattern={pattern}, type={pattern_type}, priority={priority}")
+ """Create RSS pattern"""
+ logger.info(f"Creating RSS pattern: config_id={rss_config_id}, pattern={pattern}, type={pattern_type}, priority={priority}")
try:
pattern_obj = RSSPattern(
rss_config_id=rss_config_id,
@@ -847,35 +847,35 @@ async def create_rss_pattern(self, session, rss_config_id, pattern, pattern_type
)
session.add(pattern_obj)
session.commit()
- logger.info(f"RSS模式创建成功:{pattern_obj.id}")
+ logger.info(f"RSS pattern created successfully: {pattern_obj.id}")
return pattern_obj
except Exception as e:
- logger.error(f"创建RSS模式失败:{str(e)}")
+ logger.error(f"Failed to create RSS pattern: {str(e)}")
session.rollback()
raise
async def update_rss_pattern(self, session, pattern_id, **kwargs):
- """更新RSS模式"""
- logger.info(f"更新RSS模式:pattern_id={pattern_id}, kwargs={kwargs}")
+ """Update RSS pattern"""
+ logger.info(f"Updating RSS pattern: pattern_id={pattern_id}, kwargs={kwargs}")
try:
pattern = session.query(RSSPattern).filter(RSSPattern.id == pattern_id).first()
if not pattern:
- logger.error(f"RSS模式不存在:pattern_id={pattern_id}")
- raise ValueError("RSS模式不存在")
-
+ logger.error(f"RSS pattern does not exist: pattern_id={pattern_id}")
+ raise ValueError("RSS pattern does not exist")
+
for key, value in kwargs.items():
setattr(pattern, key, value)
-
+
session.commit()
- logger.info(f"RSS模式更新成功:{pattern.id}")
+ logger.info(f"RSS pattern updated successfully: {pattern.id}")
return pattern
except Exception as e:
- logger.error(f"更新RSS模式失败:{str(e)}")
+ logger.error(f"Failed to update RSS pattern: {str(e)}")
session.rollback()
raise
async def delete_rss_pattern(self, session, pattern_id):
- """删除RSS模式"""
+ """Delete RSS pattern"""
rss_pattern = await self.get_rss_pattern(session, pattern_id)
if rss_pattern:
session.delete(rss_pattern)
@@ -884,27 +884,27 @@ async def delete_rss_pattern(self, session, pattern_id):
return False
async def reorder_rss_patterns(self, session, rss_config_id, pattern_ids):
- """重新排序RSS模式"""
+ """Reorder RSS patterns"""
patterns = await self.get_rss_patterns(session, rss_config_id)
pattern_dict = {p.id: p for p in patterns}
-
+
for index, pattern_id in enumerate(pattern_ids):
if pattern_id in pattern_dict:
pattern_dict[pattern_id].priority = index
-
+
session.commit()
- # 用户相关操作
+ # User related operations
async def get_user(self, session, username):
- """通过用户名获取用户"""
+ """Get user by username"""
return session.query(User).filter(User.username == username).first()
async def get_user_by_id(self, session, user_id):
- """通过ID获取用户"""
+ """Get user by ID"""
return session.query(User).filter(User.id == user_id).first()
async def create_user(self, session, username, password):
- """创建用户"""
+ """Create user"""
user = User(
username=username,
@@ -915,7 +915,7 @@ async def create_user(self, session, username, password):
return user
async def update_user_password(self, session, username, new_password):
- """更新用户密码"""
+ """Update user password"""
user = await self.get_user(session, username)
if user:
@@ -924,259 +924,259 @@ async def update_user_password(self, session, username, new_password):
return user
async def verify_user(self, session, username, password):
- """验证用户密码"""
-
+ """Verify user password"""
+
user = await self.get_user(session, username)
if user and check_password_hash(user.password, password):
return user
return None
- # 批量操作
+ # Batch operations
async def get_all_enabled_rss_configs(self, session):
- """获取所有启用的RSS配置"""
+ """Get all enabled RSS configurations"""
return session.query(RSSConfig).filter(RSSConfig.enable_rss == True).all()
async def get_rss_config_with_patterns(self, session, rule_id):
- """获取RSS配置及其所有模式"""
+ """Get RSS configuration with all its patterns"""
return session.query(RSSConfig).options(
joinedload(RSSConfig.patterns)
- ).filter(RSSConfig.rule_id == rule_id).first()
+ ).filter(RSSConfig.rule_id == rule_id).first()
- # 规则同步相关操作
+ # Rule sync related operations
async def add_rule_sync(self, session, rule_id, sync_rule_id):
- """添加规则同步关系
-
+ """Add rule sync relationship
+
Args:
- session: 数据库会话
- rule_id: 源规则ID
- sync_rule_id: 目标规则ID(要同步到的规则)
-
+ session: Database session
+ rule_id: Source rule ID
+ sync_rule_id: Target rule ID (rule to sync to)
+
Returns:
- tuple: (bool, str) - (成功状态, 消息)
+ tuple: (bool, str) - (success status, message)
"""
try:
- # 检查源规则是否存在
+ # Check if source rule exists
source_rule = session.query(ForwardRule).get(rule_id)
if not source_rule:
- return False, f"源规则ID {rule_id} 不存在"
-
- # 检查目标规则是否存在
+ return False, f"Source rule ID {rule_id} does not exist"
+
+ # Check if target rule exists
target_rule = session.query(ForwardRule).get(sync_rule_id)
if not target_rule:
- return False, f"目标规则ID {sync_rule_id} 不存在"
-
- # 检查是否已存在相同的同步关系
+ return False, f"Target rule ID {sync_rule_id} does not exist"
+
+ # Check if the same sync relationship already exists
existing_sync = session.query(RuleSync).filter(
RuleSync.rule_id == rule_id,
RuleSync.sync_rule_id == sync_rule_id
).first()
-
+
if existing_sync:
- return False, f"同步关系已存在"
-
- # 创建新的同步关系
+ return False, f"Sync relationship already exists"
+
+ # Create new sync relationship
new_sync = RuleSync(
rule_id=rule_id,
sync_rule_id=sync_rule_id
)
-
- # 启用规则的同步功能
+
+ # Enable sync feature for the rule
source_rule.enable_sync = True
-
+
session.add(new_sync)
session.commit()
-
- logger.info(f"已添加规则同步: 从规则 {rule_id} 到规则 {sync_rule_id}")
- return True, f"成功添加同步关系"
-
+
+ logger.info(f"Added rule sync: from rule {rule_id} to rule {sync_rule_id}")
+ return True, f"Successfully added sync relationship"
+
except Exception as e:
session.rollback()
- logger.error(f"添加规则同步关系时出错: {str(e)}")
- return False, f"添加同步关系失败: {str(e)}"
-
+ logger.error(f"Error adding rule sync relationship: {str(e)}")
+ return False, f"Failed to add sync relationship: {str(e)}"
+
async def get_rule_syncs(self, session, rule_id):
- """获取指定规则的同步关系列表
-
+ """Get sync relationship list for specified rule
+
Args:
- session: 数据库会话
- rule_id: 规则ID
-
+ session: Database session
+ rule_id: Rule ID
+
Returns:
- list: 同步关系列表
+ list: Sync relationship list
"""
try:
- # 获取该规则的所有同步目标
+ # Get all sync targets for this rule
syncs = session.query(RuleSync).filter(
RuleSync.rule_id == rule_id
).all()
-
+
return syncs
-
+
except Exception as e:
- logger.error(f"获取规则同步关系时出错: {str(e)}")
+ logger.error(f"Error getting rule sync relationships: {str(e)}")
return []
-
+
async def delete_rule_sync(self, session, rule_id, sync_rule_id):
- """删除规则同步关系
-
+ """Delete rule sync relationship
+
Args:
- session: 数据库会话
- rule_id: 源规则ID
- sync_rule_id: 目标规则ID
-
+ session: Database session
+ rule_id: Source rule ID
+ sync_rule_id: Target rule ID
+
Returns:
- tuple: (bool, str) - (成功状态, 消息)
+ tuple: (bool, str) - (success status, message)
"""
try:
- # 查找同步关系
+ # Find sync relationship
sync = session.query(RuleSync).filter(
RuleSync.rule_id == rule_id,
RuleSync.sync_rule_id == sync_rule_id
).first()
-
+
if not sync:
- return False, "指定的同步关系不存在"
-
- # 删除同步关系
+ return False, "Specified sync relationship does not exist"
+
+ # Delete sync relationship
session.delete(sync)
-
- # 检查规则是否还有其他同步关系
+
+ # Check if the rule has other sync relationships
remaining_syncs = session.query(RuleSync).filter(
RuleSync.rule_id == rule_id
).count()
-
- # 如果没有其他同步关系,关闭规则的同步功能
+
+ # If no other sync relationships, disable sync feature for the rule
if remaining_syncs == 0:
rule = session.query(ForwardRule).get(rule_id)
if rule:
rule.enable_sync = False
-
+
session.commit()
-
- logger.info(f"已删除规则同步: 从规则 {rule_id} 到规则 {sync_rule_id}")
- return True, "成功删除同步关系"
-
+
+ logger.info(f"Deleted rule sync: from rule {rule_id} to rule {sync_rule_id}")
+ return True, "Successfully deleted sync relationship"
+
except Exception as e:
session.rollback()
- logger.error(f"删除规则同步关系时出错: {str(e)}")
- return False, f"删除同步关系失败: {str(e)}"
+ logger.error(f"Error deleting rule sync relationship: {str(e)}")
+ return False, f"Failed to delete sync relationship: {str(e)}"
async def get_push_configs(self, session, rule_id):
- """获取指定规则的所有推送配置
-
+ """Get all push configurations for specified rule
+
Args:
- session: 数据库会话
- rule_id: 规则ID
-
+ session: Database session
+ rule_id: Rule ID
+
Returns:
- list: 推送配置列表
+ list: Push configuration list
"""
try:
return session.query(PushConfig).filter(
PushConfig.rule_id == rule_id
).all()
except Exception as e:
- logger.error(f"获取推送配置时出错: {str(e)}")
+ logger.error(f"Error getting push configurations: {str(e)}")
return []
-
+
async def add_push_config(self, session, rule_id, push_channel, enable_push_channel=True):
- """添加推送配置
-
+ """Add push configuration
+
Args:
- session: 数据库会话
- rule_id: 规则ID
- push_channel: 推送频道
- enable_push_channel: 是否启用推送频道
-
+ session: Database session
+ rule_id: Rule ID
+ push_channel: Push channel
+ enable_push_channel: Whether to enable push channel
+
Returns:
- tuple: (bool, str, obj) - (成功状态, 消息, 创建的对象)
+ tuple: (bool, str, obj) - (success status, message, created object)
"""
try:
- # 检查规则是否存在
+ # Check if rule exists
rule = session.query(ForwardRule).get(rule_id)
if not rule:
- return False, f"规则ID {rule_id} 不存在", None
-
- # 创建新的推送配置
+ return False, f"Rule ID {rule_id} does not exist", None
+
+ # Create new push configuration
push_config = PushConfig(
rule_id=rule_id,
push_channel=push_channel,
enable_push_channel=enable_push_channel
)
-
+
session.add(push_config)
session.commit()
-
- # 启用规则的推送功能
+
+ # Enable push feature for the rule
rule.enable_push = True
session.commit()
-
- return True, "成功添加推送配置", push_config
+
+ return True, "Successfully added push configuration", push_config
except Exception as e:
session.rollback()
- logger.error(f"添加推送配置时出错: {str(e)}")
- return False, f"添加推送配置失败: {str(e)}", None
-
+ logger.error(f"Error adding push configuration: {str(e)}")
+ return False, f"Failed to add push configuration: {str(e)}", None
+
async def toggle_push_config(self, session, config_id):
- """切换推送配置的启用状态
-
+ """Toggle the enabled state of a push configuration
+
Args:
- session: 数据库会话
- config_id: 配置ID
-
+ session: Database session
+ config_id: Configuration ID
+
Returns:
- tuple: (bool, str) - (成功状态, 消息)
+ tuple: (bool, str) - (success status, message)
"""
try:
push_config = session.query(PushConfig).get(config_id)
if not push_config:
- return False, "推送配置不存在"
-
- # 切换启用状态
+ return False, "Push configuration does not exist"
+
+ # Toggle enabled state
push_config.enable_push_channel = not push_config.enable_push_channel
session.commit()
-
- return True, f"推送配置已{'启用' if push_config.enable_push_channel else '禁用'}"
+
+ return True, f"Push configuration has been {'enabled' if push_config.enable_push_channel else 'disabled'}"
except Exception as e:
session.rollback()
- logger.error(f"切换推送配置状态时出错: {str(e)}")
- return False, f"切换推送配置状态失败: {str(e)}"
-
+ logger.error(f"Error toggling push configuration state: {str(e)}")
+ return False, f"Failed to toggle push configuration state: {str(e)}"
+
async def delete_push_config(self, session, config_id):
- """删除推送配置
-
+ """Delete push configuration
+
Args:
- session: 数据库会话
- config_id: 配置ID
-
+ session: Database session
+ config_id: Configuration ID
+
Returns:
- tuple: (bool, str) - (成功状态, 消息)
+ tuple: (bool, str) - (success status, message)
"""
try:
push_config = session.query(PushConfig).get(config_id)
if not push_config:
- return False, "推送配置不存在"
-
+ return False, "Push configuration does not exist"
+
rule_id = push_config.rule_id
-
- # 删除配置
+
+ # Delete configuration
session.delete(push_config)
-
- # 检查是否还有其他推送配置
+
+ # Check if there are other push configurations
remaining_configs = session.query(PushConfig).filter(
PushConfig.rule_id == rule_id
).count()
-
- # 如果没有其他推送配置,关闭规则的推送功能
+
+ # If no other push configurations, disable push feature for the rule
if remaining_configs == 0:
rule = session.query(ForwardRule).get(rule_id)
if rule:
rule.enable_push = False
-
+
session.commit()
-
- return True, "成功删除推送配置"
+
+ return True, "Successfully deleted push configuration"
except Exception as e:
session.rollback()
- logger.error(f"删除推送配置时出错: {str(e)}")
- return False, f"删除推送配置失败: {str(e)}"
+ logger.error(f"Error deleting push configuration: {str(e)}")
+ return False, f"Failed to delete push configuration: {str(e)}"
From b5641a31ef4f9c02c7eeeb2bf60626dbddf1eb5e Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:15:48 +0000
Subject: [PATCH 34/76] Translate Chinese to English in rss/app/routes/rss.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
rss/app/routes/rss.py | 302 +++++++++++++++++++++---------------------
1 file changed, 151 insertions(+), 151 deletions(-)
diff --git a/rss/app/routes/rss.py b/rss/app/routes/rss.py
index 3d5287d..de9c331 100644
--- a/rss/app/routes/rss.py
+++ b/rss/app/routes/rss.py
@@ -16,7 +16,7 @@
import aiohttp
from utils.constants import RSS_HOST, RSS_PORT, RSS_BASE_URL
-# 配置日志
+# Configure logging
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/rss")
@@ -33,29 +33,29 @@ async def init_db_ops():
async def rss_dashboard(request: Request, user = Depends(get_current_user)):
if not user:
return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
-
+
db_session = get_session()
try:
- # 初始化数据库操作对象
+ # Initialize database operations object
await init_db_ops()
-
- # 获取所有RSS配置
+
+ # Get all RSS configurations
rss_configs = db_session.query(RSSConfig).options(
joinedload(RSSConfig.rule)
).all()
-
- # 将 RSSConfig 对象转换为字典列表
+
+ # Convert RSSConfig objects to a list of dictionaries
configs_list = []
for config in rss_configs:
- # 处理AI提取提示词,使用Base64编码避免JSON解析问题
+ # Process AI extraction prompt, use Base64 encoding to avoid JSON parsing issues
ai_prompt = config.ai_extract_prompt
ai_prompt_encoded = None
if ai_prompt:
- # 使用Base64编码处理提示词
+ # Use Base64 encoding for the prompt
ai_prompt_encoded = base64.b64encode(ai_prompt.encode('utf-8')).decode('utf-8')
- # 添加标记,表示这是Base64编码的内容
+ # Add marker indicating this is Base64 encoded content
ai_prompt_encoded = "BASE64:" + ai_prompt_encoded
-
+
configs_list.append({
"id": config.id,
"rule_id": config.rule_id,
@@ -72,14 +72,14 @@ async def rss_dashboard(request: Request, user = Depends(get_current_user)):
"enable_custom_title_pattern": config.enable_custom_title_pattern,
"enable_custom_content_pattern": config.enable_custom_content_pattern
})
-
- # 获取所有转发规则(用于创建新的RSS配置)
+
+ # Get all forward rules (for creating new RSS configurations)
rules = db_session.query(ForwardRule).options(
joinedload(ForwardRule.source_chat),
joinedload(ForwardRule.target_chat)
).all()
-
- # 将 ForwardRule 对象转换为字典列表
+
+ # Convert ForwardRule objects to a list of dictionaries
rules_list = []
for rule in rules:
rules_list.append({
@@ -93,9 +93,9 @@ async def rss_dashboard(request: Request, user = Depends(get_current_user)):
"name": rule.target_chat.name
} if rule.target_chat else None
})
-
+
return templates.TemplateResponse(
- "rss_dashboard.html",
+ "rss_dashboard.html",
{
"request": request,
"user": user,
@@ -127,26 +127,26 @@ async def rss_config_save(
enable_custom_content_pattern: bool = Form(False)
):
if not user:
- return JSONResponse(content={"success": False, "message": "未登录"})
-
- # 记录接收到的AI提取提示词内容,帮助调试
- logger.info(f"接收到的AI提取提示词字符数: {len(ai_extract_prompt)}")
-
- # 初始化数据库操作
+ return JSONResponse(content={"success": False, "message": "Not logged in"})
+
+ # Log the received AI extraction prompt content for debugging
+ logger.info(f"Received AI extraction prompt character count: {len(ai_extract_prompt)}")
+
+ # Initialize database operations
await init_db_ops()
-
+
db_session = get_session()
try:
- # 创建或更新RSS配置
- # 如果有config_id,表示更新
+ # Create or update RSS configuration
+ # If config_id exists, it means update
if config_id and config_id.strip():
config_id = int(config_id)
- # 检查配置是否存在
+ # Check if configuration exists
rss_config = db_session.query(RSSConfig).filter(RSSConfig.id == config_id).first()
if not rss_config:
- return JSONResponse(content={"success": False, "message": "配置不存在"})
-
- # 更新配置
+ return JSONResponse(content={"success": False, "message": "Configuration does not exist"})
+
+ # Update configuration
rss_config.rule_id = rule_id
rss_config.enable_rss = enable_rss
rss_config.rule_title = rule_title
@@ -161,12 +161,12 @@ async def rss_config_save(
rss_config.enable_custom_title_pattern = enable_custom_title_pattern
rss_config.enable_custom_content_pattern = enable_custom_content_pattern
else:
- # 检查是否已经存在该规则的配置
+ # Check if a configuration already exists for this rule
existing_config = db_session.query(RSSConfig).filter(RSSConfig.rule_id == rule_id).first()
if existing_config:
- return JSONResponse(content={"success": False, "message": "该规则已经存在RSS配置"})
-
- # 创建新配置
+ return JSONResponse(content={"success": False, "message": "An RSS configuration already exists for this rule"})
+
+ # Create new configuration
rss_config = RSSConfig(
rule_id=rule_id,
enable_rss=enable_rss,
@@ -182,19 +182,19 @@ async def rss_config_save(
enable_custom_title_pattern=enable_custom_title_pattern,
enable_custom_content_pattern=enable_custom_content_pattern
)
-
- # 保存配置
+
+ # Save configuration
db_session.add(rss_config)
db_session.commit()
-
+
return JSONResponse({
- "success": True,
- "message": "RSS 配置已保存",
+ "success": True,
+ "message": "RSS configuration saved",
"config_id": rss_config.id,
"rule_id": rss_config.rule_id
})
except Exception as e:
- return JSONResponse({"success": False, "message": f"保存配置失败: {str(e)}"})
+ return JSONResponse({"success": False, "message": f"Failed to save configuration: {str(e)}"})
finally:
db_session.close()
@@ -202,29 +202,29 @@ async def rss_config_save(
async def toggle_rss(rule_id: int, user = Depends(get_current_user)):
if not user:
return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
-
+
db_session = get_session()
try:
- # 初始化数据库操作对象
+ # Initialize database operations object
db_ops_instance = await init_db_ops()
-
- # 获取配置
+
+ # Get configuration
config = await db_ops_instance.get_rss_config(db_session, rule_id)
if not config:
return RedirectResponse(
- url="/rss/dashboard?error=配置不存在",
+ url="/rss/dashboard?error=Configuration does not exist",
status_code=status.HTTP_302_FOUND
)
-
- # 切换启用/禁用状态
+
+ # Toggle enable/disable state
await db_ops_instance.update_rss_config(
db_session,
rule_id,
enable_rss=not config.enable_rss
)
-
+
return RedirectResponse(
- url="/rss/dashboard?success=RSS状态已切换",
+ url="/rss/dashboard?success=RSS status toggled",
status_code=status.HTTP_302_FOUND
)
finally:
@@ -234,36 +234,36 @@ async def toggle_rss(rule_id: int, user = Depends(get_current_user)):
async def delete_rss(rule_id: int, user = Depends(get_current_user)):
if not user:
return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
-
+
db_session = get_session()
try:
- # 初始化数据库操作对象
+ # Initialize database operations object
db_ops_instance = await init_db_ops()
-
- # 删除配置
+
+ # Delete configuration
config_deleted = await db_ops_instance.delete_rss_config(db_session, rule_id)
-
+
if config_deleted:
- # 删除关联的媒体和数据文件
+ # Delete associated media and data files
try:
- logger.info(f"开始删除规则 {rule_id} 的媒体和数据文件")
- # 构建删除API的URL
+ logger.info(f"Starting to delete media and data files for rule {rule_id}")
+ # Build the delete API URL
rss_url = f"http://{RSS_HOST}:{RSS_PORT}/api/rule/{rule_id}"
-
- # 调用删除API
+
+ # Call the delete API
async with aiohttp.ClientSession() as client_session:
async with client_session.delete(rss_url) as response:
if response.status == 200:
- logger.info(f"成功删除规则 {rule_id} 的媒体和数据文件")
+ logger.info(f"Successfully deleted media and data files for rule {rule_id}")
else:
response_text = await response.text()
- logger.warning(f"删除规则 {rule_id} 的媒体和数据文件失败, 状态码: {response.status}, 响应: {response_text}")
+ logger.warning(f"Failed to delete media and data files for rule {rule_id}, status code: {response.status}, response: {response_text}")
except Exception as e:
- logger.error(f"调用删除媒体文件API时出错: {str(e)}")
- # 不影响主流程,继续执行
-
+ logger.error(f"Error calling delete media files API: {str(e)}")
+ # Does not affect the main flow, continue execution
+
return RedirectResponse(
- url="/rss/dashboard?success=RSS配置已删除",
+ url="/rss/dashboard?success=RSS configuration deleted",
status_code=status.HTTP_302_FOUND
)
finally:
@@ -271,21 +271,21 @@ async def delete_rss(rule_id: int, user = Depends(get_current_user)):
@router.get("/patterns/{config_id}")
async def get_patterns(config_id: int, user = Depends(get_current_user)):
- """获取指定RSS配置的所有模式"""
+ """Get all patterns for a specified RSS configuration"""
if not user:
- return JSONResponse({"success": False, "message": "未登录"}, status_code=status.HTTP_401_UNAUTHORIZED)
-
+ return JSONResponse({"success": False, "message": "Not logged in"}, status_code=status.HTTP_401_UNAUTHORIZED)
+
db_session = get_session()
try:
- # 初始化数据库操作对象
+ # Initialize database operations object
db_ops_instance = await init_db_ops()
-
- # 获取所有正则表达式数据
+
+ # Get all regex data
config = await db_ops_instance.get_rss_config_with_patterns(db_session, config_id)
if not config:
- return JSONResponse({"success": False, "message": "配置不存在"}, status_code=status.HTTP_404_NOT_FOUND)
-
- # 将模式转换为JSON格式
+ return JSONResponse({"success": False, "message": "Configuration does not exist"}, status_code=status.HTTP_404_NOT_FOUND)
+
+ # Convert patterns to JSON format
patterns = []
for pattern in config.patterns:
patterns.append({
@@ -294,7 +294,7 @@ async def get_patterns(config_id: int, user = Depends(get_current_user)):
"pattern_type": pattern.pattern_type,
"priority": pattern.priority
})
-
+
return JSONResponse({"success": True, "patterns": patterns})
finally:
db_session.close()
@@ -309,29 +309,29 @@ async def save_pattern(
pattern_type: str = Form(...),
priority: int = Form(0)
):
- """保存模式"""
- logger.info(f"开始保存模式,参数:config_id={rss_config_id}, pattern={pattern}, type={pattern_type}, priority={priority}")
-
+ """Save pattern"""
+ logger.info(f"Starting to save pattern, parameters: config_id={rss_config_id}, pattern={pattern}, type={pattern_type}, priority={priority}")
+
if not user:
- logger.warning("未登录的访问尝试")
- return JSONResponse({"success": False, "message": "未登录"}, status_code=status.HTTP_401_UNAUTHORIZED)
-
+ logger.warning("Unauthenticated access attempt")
+ return JSONResponse({"success": False, "message": "Not logged in"}, status_code=status.HTTP_401_UNAUTHORIZED)
+
db_session = get_session()
try:
- # 初始化数据库操作对象
+ # Initialize database operations object
db_ops_instance = await init_db_ops()
-
- # 检查RSS配置是否存在
+
+ # Check if RSS configuration exists
config = await db_ops_instance.get_rss_config(db_session, rss_config_id)
if not config:
- logger.error(f"RSS配置不存在:config_id={rss_config_id}")
- return JSONResponse({"success": False, "message": "RSS配置不存在"})
-
- logger.debug(f"找到RSS配置:{config}")
-
-
- logger.info("创建新模式")
- # 创建新模式
+ logger.error(f"RSS configuration does not exist: config_id={rss_config_id}")
+ return JSONResponse({"success": False, "message": "RSS configuration does not exist"})
+
+ logger.debug(f"Found RSS configuration: {config}")
+
+
+ logger.info("Creating new pattern")
+ # Create new pattern
try:
pattern_obj = await db_ops_instance.create_rss_pattern(
db_session,
@@ -340,125 +340,125 @@ async def save_pattern(
pattern_type=pattern_type,
priority=priority
)
- logger.info(f"新模式创建成功:{pattern_obj}")
- return JSONResponse({"success": True, "message": "模式已创建", "pattern_id": pattern_obj.id})
+ logger.info(f"New pattern created successfully: {pattern_obj}")
+ return JSONResponse({"success": True, "message": "Pattern created", "pattern_id": pattern_obj.id})
except Exception as e:
- logger.error(f"创建模式失败:{str(e)}")
+ logger.error(f"Failed to create pattern: {str(e)}")
raise
except Exception as e:
- logger.error(f"保存模式时发生错误:{str(e)}", exc_info=True)
- return JSONResponse({"success": False, "message": f"保存模式失败: {str(e)}"})
+ logger.error(f"Error occurred while saving pattern: {str(e)}", exc_info=True)
+ return JSONResponse({"success": False, "message": f"Failed to save pattern: {str(e)}"})
finally:
db_session.close()
@router.delete("/pattern/{pattern_id}")
async def delete_pattern(pattern_id: int, user = Depends(get_current_user)):
- """删除模式"""
+ """Delete pattern"""
if not user:
- return JSONResponse({"success": False, "message": "未登录"}, status_code=status.HTTP_401_UNAUTHORIZED)
-
+ return JSONResponse({"success": False, "message": "Not logged in"}, status_code=status.HTTP_401_UNAUTHORIZED)
+
db_session = get_session()
try:
- # 初始化数据库操作对象
+ # Initialize database operations object
await init_db_ops()
-
- # 查询模式
+
+ # Query pattern
pattern = db_session.query(RSSPattern).filter(RSSPattern.id == pattern_id).first()
if not pattern:
- return JSONResponse({"success": False, "message": "找不到该模式"})
-
- # 删除模式
+ return JSONResponse({"success": False, "message": "Pattern not found"})
+
+ # Delete pattern
db_session.delete(pattern)
db_session.commit()
-
- return JSONResponse({"success": True, "message": "模式删除成功"})
+
+ return JSONResponse({"success": True, "message": "Pattern deleted successfully"})
except Exception as e:
db_session.rollback()
- logger.error(f"删除模式时出错: {str(e)}")
- return JSONResponse({"success": False, "message": f"删除模式失败: {str(e)}"})
+ logger.error(f"Error deleting pattern: {str(e)}")
+ return JSONResponse({"success": False, "message": f"Failed to delete pattern: {str(e)}"})
finally:
db_session.close()
@router.delete("/patterns/{config_id}")
async def delete_all_patterns(config_id: int, user = Depends(get_current_user)):
- """删除配置的所有模式,通常在更新前调用以便重建模式列表"""
+ """Delete all patterns for a configuration, typically called before updating to rebuild the pattern list"""
if not user:
- return JSONResponse({"success": False, "message": "未登录"}, status_code=status.HTTP_401_UNAUTHORIZED)
-
+ return JSONResponse({"success": False, "message": "Not logged in"}, status_code=status.HTTP_401_UNAUTHORIZED)
+
db_session = get_session()
try:
- # 初始化数据库操作对象
+ # Initialize database operations object
await init_db_ops()
-
- # 查询并删除指定配置的所有模式
+
+ # Query and delete all patterns for the specified configuration
patterns = db_session.query(RSSPattern).filter(RSSPattern.rss_config_id == config_id).all()
count = len(patterns)
for pattern in patterns:
db_session.delete(pattern)
-
+
db_session.commit()
- logger.info(f"已删除配置 {config_id} 的所有模式,共 {count} 个")
-
- return JSONResponse({"success": True, "message": f"已删除 {count} 个模式"})
+ logger.info(f"Deleted all patterns for configuration {config_id}, total: {count}")
+
+ return JSONResponse({"success": True, "message": f"Deleted {count} patterns"})
except Exception as e:
db_session.rollback()
- logger.error(f"删除配置 {config_id} 的所有模式时出错: {str(e)}")
- return JSONResponse({"success": False, "message": f"删除所有模式失败: {str(e)}"})
+ logger.error(f"Error deleting all patterns for configuration {config_id}: {str(e)}")
+ return JSONResponse({"success": False, "message": f"Failed to delete all patterns: {str(e)}"})
finally:
db_session.close()
@router.post("/test-regex")
-async def test_regex(user = Depends(get_current_user),
- pattern: str = Form(...),
- test_text: str = Form(...),
+async def test_regex(user = Depends(get_current_user),
+ pattern: str = Form(...),
+ test_text: str = Form(...),
pattern_type: str = Form(...)):
- """测试正则表达式匹配结果"""
+ """Test regex matching results"""
if not user:
- return JSONResponse({"success": False, "message": "未登录"}, status_code=status.HTTP_401_UNAUTHORIZED)
-
+ return JSONResponse({"success": False, "message": "Not logged in"}, status_code=status.HTTP_401_UNAUTHORIZED)
+
try:
-
-
- # 记录测试信息
- logger.info(f"测试正则表达式: {pattern}")
- logger.info(f"测试类型: {pattern_type}")
- logger.info(f"测试文本长度: {len(test_text)} 字符")
-
- # 执行正则匹配
+
+
+ # Log test information
+ logger.info(f"Testing regex: {pattern}")
+ logger.info(f"Test type: {pattern_type}")
+ logger.info(f"Test text length: {len(test_text)} characters")
+
+ # Execute regex matching
match = re.search(pattern, test_text)
-
- # 检查是否有匹配
+
+ # Check if there is a match
if not match:
return JSONResponse({
"success": True,
"matched": False,
- "message": "未找到匹配"
+ "message": "No match found"
})
-
- # 检查捕获组
+
+ # Check capture groups
if not match.groups():
return JSONResponse({
"success": True,
"matched": True,
"has_groups": False,
- "message": "匹配成功,但没有捕获组。请使用括号 () 来创建捕获组。"
+ "message": "Match successful, but no capture groups. Please use parentheses () to create capture groups."
})
-
- # 成功匹配且有捕获组
+
+ # Successfully matched with capture groups
extracted_content = match.group(1)
-
- # 返回匹配结果
+
+ # Return match results
return JSONResponse({
"success": True,
"matched": True,
"has_groups": True,
"extracted": extracted_content,
- "message": "匹配成功!"
+ "message": "Match successful!"
})
-
+
except Exception as e:
- logger.error(f"测试正则表达式时出错: {str(e)}")
+ logger.error(f"Error testing regex: {str(e)}")
return JSONResponse({
"success": False,
- "message": f"测试失败: {str(e)}"
- })
\ No newline at end of file
+ "message": f"Test failed: {str(e)}"
+ })
From d5fd2c761f466d7143c46fc7482b80a58e3eb782 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:16:04 +0000
Subject: [PATCH 35/76] Translate Chinese to English in filters/reply_filter.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
filters/reply_filter.py | 69 +++++++++++++++++++++--------------------
1 file changed, 35 insertions(+), 34 deletions(-)
diff --git a/filters/reply_filter.py b/filters/reply_filter.py
index 34270e7..15f24bd 100644
--- a/filters/reply_filter.py
+++ b/filters/reply_filter.py
@@ -8,65 +8,66 @@
class ReplyFilter(BaseFilter):
"""
- 回复过滤器,用于处理媒体组消息的评论区按钮
- 由于媒体组消息无法直接添加按钮,此过滤器会使用bot回复已转发的消息,并添加评论区按钮
+ Reply filter, used to handle comment section buttons for media group messages.
+ Since media group messages cannot have buttons added directly, this filter uses a bot to reply
+ to forwarded messages and add comment section buttons.
"""
-
+
async def _process(self, context):
"""
- 处理媒体组消息的评论区按钮
-
+ Handle comment section buttons for media group messages
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
try:
- # 如果规则不存在或未启用评论按钮功能,直接跳过
+ # If the rule doesn't exist or comment button feature is not enabled, skip directly
if not context.rule or not context.rule.enable_comment_button:
return True
-
- # 只处理媒体组消息
+
+ # Only process media group messages
if not context.is_media_group:
return True
-
- # 检查是否有评论区链接和已转发的消息
+
+ # Check if there is a comment section link and forwarded messages
if not context.comment_link or not context.forwarded_messages:
- logger.info("没有评论区链接或已转发消息,无法添加评论区按钮回复")
+ logger.info("No comment section link or forwarded messages, cannot add comment section button reply")
return True
-
- # 使用bot客户端(context.client)
+
+ # Use bot client (context.client)
client = context.client
-
- # 获取目标聊天信息
+
+ # Get target chat information
rule = context.rule
target_chat = rule.target_chat
target_chat_id = int(target_chat.telegram_chat_id)
-
- # 获取已转发的第一条消息ID
+
+ # Get the first forwarded message ID
first_forwarded_msg = context.forwarded_messages[0]
-
- # 创建评论区按钮
- comment_button = Button.url("💬 查看评论区", context.comment_link)
+
+ # Create comment section button
+ comment_button = Button.url("💬 View comments", context.comment_link)
buttons = [[comment_button]]
-
- # 回复已转发的媒体组消息
- logger.info(f"正在使用Bot给已转发的媒体组消息 {first_forwarded_msg.id} 发送评论区按钮回复")
-
- # 发送回复消息,附带评论区按钮
+
+ # Reply to the forwarded media group message
+ logger.info(f"Using Bot to send comment section button reply to forwarded media group message {first_forwarded_msg.id}")
+
+ # Send reply message with comment section button
await client.send_message(
entity=target_chat_id,
- message="💬 评论区",
+ message="💬 Comments",
buttons=buttons,
reply_to=first_forwarded_msg.id,
)
- logger.info("成功发送评论区按钮回复")
-
+ logger.info("Successfully sent comment section button reply")
+
return True
-
+
except Exception as e:
- logger.error(f"ReplyFilter处理消息时出错: {str(e)}")
+ logger.error(f"Error in ReplyFilter processing message: {str(e)}")
logger.error(traceback.format_exc())
- return True
\ No newline at end of file
+ return True
From 45a85f1695d7e1a82bf9be57837323986c947a0b Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:16:23 +0000
Subject: [PATCH 36/76] Translate Chinese to English in
handlers/user_handler.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
handlers/user_handler.py | 40 ++++++++++++++++++++--------------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/handlers/user_handler.py b/handlers/user_handler.py
index 5de5e2f..d90a408 100644
--- a/handlers/user_handler.py
+++ b/handlers/user_handler.py
@@ -8,32 +8,32 @@
logger = logging.getLogger(__name__)
async def process_forward_rule(client, event, chat_id, rule):
- """处理转发规则(用户模式)"""
+ """Process forwarding rule (user mode)"""
if not rule.enable_rule:
- logger.info(f'规则 ID: {rule.id} 已禁用,跳过处理')
+ logger.info(f'Rule ID: {rule.id} is disabled, skipping processing')
return
message_text = event.message.text or ''
check_message_text = message_text
- # 添加日志
- logger.info(f'处理规则 ID: {rule.id}')
- logger.info(f'消息内容: {message_text}')
- logger.info(f'规则模式: {rule.forward_mode.value}')
+ # Add logs
+ logger.info(f'Processing rule ID: {rule.id}')
+ logger.info(f'Message content: {message_text}')
+ logger.info(f'Rule mode: {rule.forward_mode.value}')
if rule.is_filter_user_info:
- sender_info = await get_sender_info(event, rule.id) # 调用新的函数获取 sender_info
+ sender_info = await get_sender_info(event, rule.id) # Call the new function to get sender_info
if sender_info:
check_message_text = f"{sender_info}:\n{message_text}"
- logger.info(f'附带用户信息后的消息: {message_text}')
+ logger.info(f'Message with user info attached: {message_text}')
else:
- logger.warning(f"规则 ID: {rule.id} - 无法获取发送者信息")
+ logger.warning(f"Rule ID: {rule.id} - Unable to get sender info")
should_forward = await check_keywords(rule,check_message_text)
- logger.info(f'最终决定: {"转发" if should_forward else "不转发"}')
+ logger.info(f'Final decision: {"forward" if should_forward else "do not forward"}')
if should_forward:
target_chat = rule.target_chat
@@ -43,42 +43,42 @@ async def process_forward_rule(client, event, chat_id, rule):
if event.message.grouped_id:
- # 等待一段时间以确保收到所有媒体组消息
+ # Wait a moment to ensure all media group messages are received
await asyncio.sleep(1)
- # 收集媒体组的所有消息
+ # Collect all messages in the media group
messages = []
async for message in client.iter_messages(
event.chat_id,
- limit=20, # 限制搜索范围
+ limit=20, # Limit search range
min_id=event.message.id - 10,
max_id=event.message.id + 10
):
if message.grouped_id == event.message.grouped_id:
messages.append(message.id)
- logger.info(f'找到媒体组消息: ID={message.id}')
+ logger.info(f'Found media group message: ID={message.id}')
- # 按照ID排序,确保转发顺序正确
+ # Sort by ID to ensure correct forwarding order
messages.sort()
- # 一次性转发所有消息
+ # Forward all messages at once
await client.forward_messages(
target_chat_id,
messages,
event.chat_id
)
- logger.info(f'[用户] 已转发 {len(messages)} 条媒体组消息到: {target_chat.name} ({target_chat_id})')
+ logger.info(f'[User] Forwarded {len(messages)} media group messages to: {target_chat.name} ({target_chat_id})')
else:
- # 处理单条消息
+ # Process single message
await client.forward_messages(
target_chat_id,
event.message.id,
event.chat_id
)
- logger.info(f'[用户] 消息已转发到: {target_chat.name} ({target_chat_id})')
+ logger.info(f'[User] Message forwarded to: {target_chat.name} ({target_chat_id})')
except Exception as e:
- logger.error(f'转发消息时出错: {str(e)}')
+ logger.error(f'Error forwarding message: {str(e)}')
logger.exception(e)
\ No newline at end of file
From 172e4fece19b8a4e197d3e79c05cc6aaffb343da Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:17:04 +0000
Subject: [PATCH 37/76] Translate Chinese to English in filters/media_filter.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
filters/media_filter.py | 305 ++++++++++++++++++++--------------------
1 file changed, 152 insertions(+), 153 deletions(-)
diff --git a/filters/media_filter.py b/filters/media_filter.py
index 2dae0ba..9a65e63 100644
--- a/filters/media_filter.py
+++ b/filters/media_filter.py
@@ -15,48 +15,48 @@
class MediaFilter(BaseFilter):
"""
- 媒体过滤器,处理消息中的媒体内容
+ Media filter, processes media content in messages
"""
-
+
async def _process(self, context):
"""
- 处理媒体内容
-
+ Process media content
+
Args:
- context: 消息上下文
-
+ context: Message context
+
Returns:
- bool: 是否继续处理
+ bool: Whether to continue processing
"""
- # 确保临时目录存在
+ # Ensure temporary directory exists
os.makedirs(TEMP_DIR, exist_ok=True)
-
+
rule = context.rule
event = context.event
client = context.client
-
- # 如果是媒体组消息
+
+ # If it's a media group message
if event.message.grouped_id:
await self._process_media_group(context)
else:
await self._process_single_media(context)
-
+
return True
-
+
async def _process_media_group(self, context):
- """处理媒体组消息"""
+ """Process media group messages"""
event = context.event
rule = context.rule
client = context.client
-
- logger.info(f'处理媒体组消息 组ID: {event.message.grouped_id}')
-
- # 等待更长时间让所有媒体消息到达
+
+ logger.info(f'Processing media group message, group ID: {event.message.grouped_id}')
+
+ # Wait longer for all media messages to arrive
await asyncio.sleep(1)
-
- # 获取媒体类型设置
+
+ # Get media type settings
media_types = None
if rule.enable_media_type_filter:
session = get_session()
@@ -64,10 +64,10 @@ async def _process_media_group(self, context):
media_types = session.query(MediaTypes).filter_by(rule_id=rule.id).first()
finally:
session.close()
-
- # 收集媒体组的所有消息
- total_media_count = 0 # 总媒体数量
- blocked_media_count = 0 # 被屏蔽的媒体数量
+
+ # Collect all messages in the media group
+ total_media_count = 0 # Total media count
+ blocked_media_count = 0 # Blocked media count
try:
async for message in event.client.iter_messages(
event.chat_id,
@@ -78,29 +78,29 @@ async def _process_media_group(self, context):
if message.grouped_id == event.message.grouped_id:
if message.media:
total_media_count += 1
- # 检查媒体类型
+ # Check media type
if rule.enable_media_type_filter and media_types and message.media:
if await self._is_media_type_blocked(message.media, media_types):
- logger.info(f'媒体类型被屏蔽,跳过消息 ID={message.id}')
+ logger.info(f'Media type is blocked, skipping message ID={message.id}')
blocked_media_count += 1
continue
-
- # 检查媒体扩展名
+
+ # Check media extension
if rule.enable_extension_filter and message.media:
if not await self._is_media_extension_allowed(rule, message.media):
- logger.info(f'媒体扩展名被屏蔽,跳过消息 ID={message.id}')
+ logger.info(f'Media extension is blocked, skipping message ID={message.id}')
blocked_media_count += 1
continue
-
- # 检查媒体大小
+
+ # Check media size
if message.media:
file_size = await get_media_size(message.media)
- file_size = round(file_size/1024/1024, 2) # 转换为MB
- logger.info(f'媒体文件大小: {file_size}MB')
- logger.info(f'规则最大媒体大小: {rule.max_media_size}MB')
- logger.info(f'是否启用媒体大小过滤: {rule.enable_media_size_filter}')
- logger.info(f'是否发送媒体大小超限提醒: {rule.is_send_over_media_size_message}')
-
+ file_size = round(file_size/1024/1024, 2) # Convert to MB
+ logger.info(f'Media file size: {file_size}MB')
+ logger.info(f'Rule max media size: {rule.max_media_size}MB')
+ logger.info(f'Media size filter enabled: {rule.enable_media_size_filter}')
+ logger.info(f'Send oversized media notification: {rule.is_send_over_media_size_message}')
+
if rule.max_media_size and (file_size > rule.max_media_size) and rule.enable_media_size_filter:
file_name = ''
if hasattr(message.media, 'document') and message.media.document:
@@ -108,45 +108,45 @@ async def _process_media_group(self, context):
if hasattr(attr, 'file_name'):
file_name = attr.file_name
break
- logger.info(f'媒体文件 {file_name} 超过大小限制 ({rule.max_media_size}MB)')
+ logger.info(f'Media file {file_name} exceeds size limit ({rule.max_media_size}MB)')
context.skipped_media.append((message, file_size, file_name))
continue
-
+
context.media_group_messages.append(message)
- logger.info(f'找到媒体组消息: ID={message.id}, 类型={type(message.media).__name__ if message.media else "无媒体"}')
+ logger.info(f'Found media group message: ID={message.id}, type={type(message.media).__name__ if message.media else "no media"}')
except Exception as e:
- logger.error(f'收集媒体组消息时出错: {str(e)}')
- context.errors.append(f"收集媒体组消息错误: {str(e)}")
-
- logger.info(f'共找到 {len(context.media_group_messages)} 条媒体组消息,{len(context.skipped_media)} 条超限')
-
- # 如果所有媒体都被屏蔽,设置不转发
+ logger.error(f'Error collecting media group messages: {str(e)}')
+ context.errors.append(f"Error collecting media group messages: {str(e)}")
+
+ logger.info(f'Found {len(context.media_group_messages)} media group messages, {len(context.skipped_media)} exceeded limit')
+
+ # If all media are blocked, set not to forward
if total_media_count > 0 and total_media_count == blocked_media_count:
- logger.info('媒体组中所有媒体都被屏蔽,设置不转发')
- # 检查是否允许文本通过
+ logger.info('All media in the media group are blocked, setting not to forward')
+ # Check if text is allowed to pass through
if rule.media_allow_text:
- logger.info('媒体被屏蔽但允许文本通过')
- context.media_blocked = True # 标记媒体被屏蔽
+ logger.info('Media blocked but text allowed to pass through')
+ context.media_blocked = True # Mark media as blocked
else:
context.should_forward = False
return True
-
- # 如果所有媒体都超限且不发送超限提醒,则设置不转发
+
+ # If all media exceeded limit and oversized notification is not enabled, set not to forward
if len(context.skipped_media) > 0 and len(context.media_group_messages) == 0 and not rule.is_send_over_media_size_message:
- # 检查是否允许文本通过
+ # Check if text is allowed to pass through
if rule.media_allow_text:
- logger.info('媒体超限但允许文本通过')
- context.media_blocked = True # 标记媒体被屏蔽
+ logger.info('Media exceeded limit but text allowed to pass through')
+ context.media_blocked = True # Mark media as blocked
else:
context.should_forward = False
- logger.info('所有媒体都超限且不发送超限提醒,设置不转发')
-
+ logger.info('All media exceeded limit and oversized notification not enabled, setting not to forward')
+
async def _process_single_media(self, context):
- """处理单条媒体消息"""
+ """Process single media message"""
event = context.event
rule = context.rule
- # logger.info(f'context属性: {context.rule.__dict__}')
- # 检查是否是纯链接预览消息
+ # logger.info(f'context attributes: {context.rule.__dict__}')
+ # Check if it's a pure link preview message
is_pure_link_preview = (
event.message.media and
hasattr(event.message.media, 'webpage') and
@@ -158,8 +158,8 @@ async def _process_single_media(self, context):
getattr(event.message.media, 'voice', None)
])
)
-
- # 检查是否有实际媒体
+
+ # Check if there is actual media
has_media = (
event.message.media and
any([
@@ -171,193 +171,192 @@ async def _process_single_media(self, context):
])
)
- # 处理实际媒体
+ # Process actual media
if has_media:
- # 检查媒体类型是否被屏蔽
+ # Check if media type is blocked
if rule.enable_media_type_filter:
session = get_session()
try:
media_types = session.query(MediaTypes).filter_by(rule_id=rule.id).first()
if media_types and await self._is_media_type_blocked(event.message.media, media_types):
- logger.info(f'媒体类型被屏蔽,跳过消息 ID={event.message.id}')
- # 检查是否允许文本通过
+ logger.info(f'Media type is blocked, skipping message ID={event.message.id}')
+ # Check if text is allowed to pass through
if rule.media_allow_text:
- logger.info('媒体被屏蔽但允许文本通过')
- context.media_blocked = True # 标记媒体被屏蔽
+ logger.info('Media blocked but text allowed to pass through')
+ context.media_blocked = True # Mark media as blocked
else:
context.should_forward = False
return True
finally:
session.close()
-
- # 检查媒体扩展名
+
+ # Check media extension
if rule.enable_extension_filter and event.message.media:
if not await self._is_media_extension_allowed(rule, event.message.media):
- logger.info(f'媒体扩展名被屏蔽,跳过消息 ID={event.message.id}')
- # 检查是否允许文本通过
+ logger.info(f'Media extension is blocked, skipping message ID={event.message.id}')
+ # Check if text is allowed to pass through
if rule.media_allow_text:
- logger.info('媒体被屏蔽但允许文本通过')
- context.media_blocked = True # 标记媒体被屏蔽
+ logger.info('Media blocked but text allowed to pass through')
+ context.media_blocked = True # Mark media as blocked
else:
context.should_forward = False
return True
-
- # 检查媒体大小
+
+ # Check media size
file_size = await get_media_size(event.message.media)
file_size = round(file_size/1024/1024, 2)
logger.info(f'event.message.document: {event.message.document}')
-
- logger.info(f'媒体文件大小: {file_size}MB')
- logger.info(f'规则最大媒体大小: {rule.max_media_size}MB')
-
- logger.info(f'是否启用媒体大小过滤: {rule.enable_media_size_filter}')
+
+ logger.info(f'Media file size: {file_size}MB')
+ logger.info(f'Rule max media size: {rule.max_media_size}MB')
+
+ logger.info(f'Media size filter enabled: {rule.enable_media_size_filter}')
if rule.max_media_size and (file_size > rule.max_media_size) and rule.enable_media_size_filter:
file_name = ''
if event.message.document:
- # 正确地从文档属性中获取文件名
+ # Correctly get filename from document attributes
for attr in event.message.document.attributes:
if hasattr(attr, 'file_name'):
file_name = attr.file_name
break
-
- logger.info(f'媒体文件超过大小限制 ({rule.max_media_size}MB)')
+
+ logger.info(f'Media file exceeds size limit ({rule.max_media_size}MB)')
if rule.is_send_over_media_size_message:
- logger.info(f'是否发送媒体大小超限提醒: {rule.is_send_over_media_size_message}')
+ logger.info(f'Send oversized media notification: {rule.is_send_over_media_size_message}')
context.should_forward = True
else:
- # 检查是否允许文本通过
+ # Check if text is allowed to pass through
if rule.media_allow_text:
- logger.info('媒体超限但允许文本通过')
- context.media_blocked = True # 标记媒体被屏蔽
+ logger.info('Media exceeded limit but text allowed to pass through')
+ context.media_blocked = True # Mark media as blocked
context.skipped_media.append((event.message, file_size, file_name))
- return True # 跳过后续的媒体下载
+ return True # Skip subsequent media download
else:
context.should_forward = False
context.skipped_media.append((event.message, file_size, file_name))
- return True # 不论如何都跳过后续的媒体下载
+ return True # Skip subsequent media download regardless
else:
- # 如果只转发到RSS,则跳过下载媒体文件,交给RSS处理下载
+ # If only forwarding to RSS, skip downloading media files, let RSS handle the download
if rule.only_rss:
return True
try:
- # 下载媒体文件
+ # Download media file
file_path = await event.message.download_media(TEMP_DIR)
if file_path:
context.media_files.append(file_path)
- logger.info(f'媒体文件已下载到: {file_path}')
+ logger.info(f'Media file downloaded to: {file_path}')
except Exception as e:
- logger.error(f'下载媒体文件时出错: {str(e)}')
- context.errors.append(f"下载媒体文件错误: {str(e)}")
+ logger.error(f'Error downloading media file: {str(e)}')
+ context.errors.append(f"Error downloading media file: {str(e)}")
elif is_pure_link_preview:
- # 记录这是纯链接预览消息
+ # Record that this is a pure link preview message
context.is_pure_link_preview = True
- logger.info('这是一条纯链接预览消息')
-
+ logger.info('This is a pure link preview message')
+
async def _is_media_type_blocked(self, media, media_types):
"""
- 检查媒体类型是否被屏蔽
-
+ Check if media type is blocked
+
Args:
- media: 媒体对象
- media_types: MediaTypes对象
-
+ media: Media object
+ media_types: MediaTypes object
+
Returns:
- bool: 如果媒体类型被屏蔽返回True,否则返回False
+ bool: Returns True if media type is blocked, False otherwise
"""
- # 检查各种媒体类型
+ # Check various media types
if getattr(media, 'photo', None) and media_types.photo:
- logger.info('媒体类型为图片,已被屏蔽')
+ logger.info('Media type is photo, blocked')
return True
-
+
if getattr(media, 'document', None) and media_types.document:
- logger.info('媒体类型为文档,已被屏蔽')
+ logger.info('Media type is document, blocked')
return True
-
+
if getattr(media, 'video', None) and media_types.video:
- logger.info('媒体类型为视频,已被屏蔽')
+ logger.info('Media type is video, blocked')
return True
-
+
if getattr(media, 'audio', None) and media_types.audio:
- logger.info('媒体类型为音频,已被屏蔽')
+ logger.info('Media type is audio, blocked')
return True
-
+
if getattr(media, 'voice', None) and media_types.voice:
- logger.info('媒体类型为语音,已被屏蔽')
+ logger.info('Media type is voice, blocked')
return True
-
- return False
-
+
+ return False
+
async def _is_media_extension_allowed(self, rule, media):
"""
- 检查媒体扩展名是否被允许
-
+ Check if media extension is allowed
+
Args:
- rule: 转发规则
- media: 媒体对象
-
+ rule: Forwarding rule
+ media: Media object
+
Returns:
- bool: 如果扩展名被允许返回True,否则返回False
+ bool: Returns True if extension is allowed, False otherwise
"""
- # 如果没有启用扩展名过滤,默认允许
+ # If extension filter is not enabled, allow by default
if not rule.enable_extension_filter:
return True
-
- # 获取文件名
+
+ # Get filename
file_name = None
-
+
for attr in media.document.attributes:
if hasattr(attr, 'file_name'):
file_name = attr.file_name
break
-
- # 如果没有文件名,则无法判断扩展名,默认允许
+
+ # If filename cannot be obtained, extension cannot be determined, allow by default
if not file_name:
- logger.info("无法获取文件名,无法判断扩展名")
+ logger.info("Cannot get filename, unable to determine extension")
return True
-
- # 提取扩展名
+
+ # Extract extension
_, extension = os.path.splitext(file_name)
- extension = extension.lstrip('.').lower() # 移除点号并转为小写
-
- # 特殊处理:如果文件没有扩展名,将extension设为特殊值"无扩展名"
+ extension = extension.lstrip('.').lower() # Remove dot and convert to lowercase
+
+ # Special handling: if the file has no extension, set extension to a special value "no_extension"
if not extension:
- logger.info(f"文件 {file_name} 没有扩展名")
- extension = "无扩展名"
+ logger.info(f"File {file_name} has no extension")
+ extension = "no_extension"
else:
- logger.info(f"文件 {file_name} 的扩展名: {extension}")
-
- # 获取规则中保存的扩展名列表
+ logger.info(f"File {file_name} extension: {extension}")
+
+ # Get the extension list saved in the rule
db_ops = await get_db_ops()
session = get_session()
allowed = True
try:
- # 使用db_operations中的函数获取扩展名列表
+ # Use the function from db_operations to get the extension list
extensions = await db_ops.get_media_extensions(session, rule.id)
extension_list = [ext["extension"].lower() for ext in extensions]
-
- # 判断是否允许该扩展名
+
+ # Determine if the extension is allowed
if rule.extension_filter_mode == AddMode.BLACKLIST:
- # 黑名单模式:如果扩展名在列表中,则不允许
+ # Blacklist mode: if extension is in the list, not allowed
if extension in extension_list:
- logger.info(f"扩展名 {extension} 在黑名单中,不允许")
+ logger.info(f"Extension {extension} is in the blacklist, not allowed")
allowed = False
else:
- logger.info(f"扩展名 {extension} 不在黑名单中,允许")
+ logger.info(f"Extension {extension} is not in the blacklist, allowed")
allowed = True
else:
- # 白名单模式:如果扩展名不在列表中,则不允许
+ # Whitelist mode: if extension is not in the list, not allowed
if extension in extension_list:
- logger.info(f"扩展名 {extension} 在白名单中,允许")
+ logger.info(f"Extension {extension} is in the whitelist, allowed")
allowed = True
else:
- logger.info(f"扩展名 {extension} 不在白名单中,不允许")
+ logger.info(f"Extension {extension} is not in the whitelist, not allowed")
allowed = False
except Exception as e:
- logger.error(f"检查媒体扩展名时出错: {str(e)}")
- allowed = True # 出错时默认允许
+ logger.error(f"Error checking media extension: {str(e)}")
+ allowed = True # Allow by default on error
finally:
session.close()
-
- return allowed
+ return allowed
From baa3a8d347162f0e6eec133b1f73b546d46537e8 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:17:23 +0000
Subject: [PATCH 38/76] Translate Chinese to English in models/models.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
models/models.py | 301 +++++++++++++++++++++++------------------------
1 file changed, 150 insertions(+), 151 deletions(-)
diff --git a/models/models.py b/models/models.py
index eeb1260..b53825a 100644
--- a/models/models.py
+++ b/models/models.py
@@ -17,7 +17,7 @@ class Chat(Base):
name = Column(String, nullable=True)
current_add_id = Column(String, nullable=True)
- # 关系
+ # Relationships
source_rules = relationship('ForwardRule', foreign_keys='ForwardRule.source_chat_id', back_populates='source_chat')
target_rules = relationship('ForwardRule', foreign_keys='ForwardRule.target_chat_id', back_populates='target_chat')
@@ -31,58 +31,58 @@ class ForwardRule(Base):
use_bot = Column(Boolean, default=True)
message_mode = Column(Enum(MessageMode), nullable=False, default=MessageMode.MARKDOWN)
is_replace = Column(Boolean, default=False)
- is_preview = Column(Enum(PreviewMode), nullable=False, default=PreviewMode.FOLLOW) # 三个值,开,关,按照原消息
- is_original_link = Column(Boolean, default=False) # 是否附带原消息链接
+ is_preview = Column(Enum(PreviewMode), nullable=False, default=PreviewMode.FOLLOW) # Three values: on, off, follow original message
+ is_original_link = Column(Boolean, default=False) # Whether to include original message link
is_ufb = Column(Boolean, default=False)
ufb_domain = Column(String, nullable=True)
ufb_item = Column(String, nullable=True,default='main')
- is_delete_original = Column(Boolean, default=False) # 是否删除原始消息
- is_original_sender = Column(Boolean, default=False) # 是否附带原始消息发送人名称
- userinfo_template = Column(String, default='**{name}**', nullable=True) # 用户信息模板
- time_template = Column(String, default='{time}', nullable=True) # 时间模板
- original_link_template = Column(String, default='原始连接:{original_link}', nullable=True) # 原始链接模板
- is_original_time = Column(Boolean, default=False) # 是否附带原始消息发送时间
- add_mode = Column(Enum(AddMode), nullable=False, default=AddMode.BLACKLIST) # 添加模式,默认黑名单
- enable_rule = Column(Boolean, default=True) # 是否启用规则
- is_filter_user_info = Column(Boolean, default=False) # 是否过滤用户信息
- handle_mode = Column(Enum(HandleMode), nullable=False, default=HandleMode.FORWARD) # 处理模式,编辑模式和转发模式,默认转发
- enable_comment_button = Column(Boolean, default=False) # 是否添加对应消息的评论区直达按钮
- enable_media_type_filter = Column(Boolean, default=False) # 是否启用媒体类型过滤
- enable_media_size_filter = Column(Boolean, default=False) # 是否启用媒体大小过滤
- max_media_size = Column(Integer, default=os.getenv('DEFAULT_MAX_MEDIA_SIZE', 10)) # 媒体大小限制,单位MB
- is_send_over_media_size_message = Column(Boolean, default=True) # 超过限制的媒体是否发送提示消息
- enable_extension_filter = Column(Boolean, default=False) # 是否启用媒体扩展名过滤
- extension_filter_mode = Column(Enum(AddMode), nullable=False, default=AddMode.BLACKLIST) # 媒体扩展名过滤模式,默认黑名单
- enable_reverse_blacklist = Column(Boolean, default=False) # 是否反转黑名单
- enable_reverse_whitelist = Column(Boolean, default=False) # 是否反转白名单
- media_allow_text = Column(Boolean, default=False) # 是否放行文本
- # 推送相关字段
- enable_push = Column(Boolean, default=False) # 是否启用推送
- enable_only_push = Column(Boolean, default=False) # 是否只转发到推送配置
-
- # AI相关字段
- is_ai = Column(Boolean, default=False) # 是否启用AI处理
- ai_model = Column(String, nullable=True) # 使用的AI模型
- ai_prompt = Column(String, nullable=True) # AI处理的prompt
- enable_ai_upload_image = Column(Boolean, default=False) # 是否启用AI图片上传功能
- is_summary = Column(Boolean, default=False) # 是否启用AI总结
+ is_delete_original = Column(Boolean, default=False) # Whether to delete original message
+ is_original_sender = Column(Boolean, default=False) # Whether to include original message sender name
+ userinfo_template = Column(String, default='**{name}**', nullable=True) # User info template
+ time_template = Column(String, default='{time}', nullable=True) # Time template
+ original_link_template = Column(String, default='Original link: {original_link}', nullable=True) # Original link template
+ is_original_time = Column(Boolean, default=False) # Whether to include original message send time
+ add_mode = Column(Enum(AddMode), nullable=False, default=AddMode.BLACKLIST) # Add mode, default blacklist
+ enable_rule = Column(Boolean, default=True) # Whether to enable rule
+ is_filter_user_info = Column(Boolean, default=False) # Whether to filter user info
+ handle_mode = Column(Enum(HandleMode), nullable=False, default=HandleMode.FORWARD) # Handle mode, edit mode and forward mode, default forward
+ enable_comment_button = Column(Boolean, default=False) # Whether to add comment section shortcut button for corresponding message
+ enable_media_type_filter = Column(Boolean, default=False) # Whether to enable media type filter
+ enable_media_size_filter = Column(Boolean, default=False) # Whether to enable media size filter
+ max_media_size = Column(Integer, default=os.getenv('DEFAULT_MAX_MEDIA_SIZE', 10)) # Media size limit, in MB
+ is_send_over_media_size_message = Column(Boolean, default=True) # Whether to send notification for media exceeding size limit
+ enable_extension_filter = Column(Boolean, default=False) # Whether to enable media extension filter
+ extension_filter_mode = Column(Enum(AddMode), nullable=False, default=AddMode.BLACKLIST) # Media extension filter mode, default blacklist
+ enable_reverse_blacklist = Column(Boolean, default=False) # Whether to reverse blacklist
+ enable_reverse_whitelist = Column(Boolean, default=False) # Whether to reverse whitelist
+ media_allow_text = Column(Boolean, default=False) # Whether to allow text through
+ # Push related fields
+ enable_push = Column(Boolean, default=False) # Whether to enable push
+ enable_only_push = Column(Boolean, default=False) # Whether to only forward to push configuration
+
+ # AI related fields
+ is_ai = Column(Boolean, default=False) # Whether to enable AI processing
+ ai_model = Column(String, nullable=True) # AI model in use
+ ai_prompt = Column(String, nullable=True) # AI processing prompt
+ enable_ai_upload_image = Column(Boolean, default=False) # Whether to enable AI image upload feature
+ is_summary = Column(Boolean, default=False) # Whether to enable AI summary
summary_time = Column(String(5), default=os.getenv('DEFAULT_SUMMARY_TIME', '07:00'))
- summary_prompt = Column(String, nullable=True) # AI总结的prompt
- is_keyword_after_ai = Column(Boolean, default=False) # AI处理后是否再次执行关键字过滤
- is_top_summary = Column(Boolean, default=True) # 是否顶置总结消息
- enable_delay = Column(Boolean, default=False) # 是否启用延迟处理
- delay_seconds = Column(Integer, default=5) # 延迟处理秒数
- # RSS相关字段
- only_rss = Column(Boolean, default=False) # 是否只转发RSS
- # 同步功能相关
- enable_sync = Column(Boolean, default=False) # 是否启用规则同步功能
-
- # 添加唯一约束
+ summary_prompt = Column(String, nullable=True) # AI summary prompt
+ is_keyword_after_ai = Column(Boolean, default=False) # Whether to execute keyword filtering again after AI processing
+ is_top_summary = Column(Boolean, default=True) # Whether to pin summary message
+ enable_delay = Column(Boolean, default=False) # Whether to enable delayed processing
+ delay_seconds = Column(Integer, default=5) # Delay processing seconds
+ # RSS related fields
+ only_rss = Column(Boolean, default=False) # Whether to only forward RSS
+ # Sync feature related
+ enable_sync = Column(Boolean, default=False) # Whether to enable rule sync feature
+
+ # Add unique constraint
__table_args__ = (
UniqueConstraint('source_chat_id', 'target_chat_id', name='unique_source_target'),
)
- # 关系
+ # Relationships
source_chat = relationship('Chat', foreign_keys=[source_chat_id], back_populates='source_rules')
target_chat = relationship('Chat', foreign_keys=[target_chat_id], back_populates='target_rules')
keywords = relationship('Keyword', back_populates='rule')
@@ -102,10 +102,10 @@ class Keyword(Base):
is_regex = Column(Boolean, default=False)
is_blacklist = Column(Boolean, default=True)
- # 关系
+ # Relationships
rule = relationship('ForwardRule', back_populates='keywords')
- # 添加唯一约束
+ # Add unique constraint
__table_args__ = (
UniqueConstraint('rule_id', 'keyword','is_regex','is_blacklist', name='unique_rule_keyword_is_regex_is_blacklist'),
)
@@ -115,13 +115,13 @@ class ReplaceRule(Base):
id = Column(Integer, primary_key=True)
rule_id = Column(Integer, ForeignKey('forward_rules.id'), nullable=False)
- pattern = Column(String, nullable=False) # 替换模式
- content = Column(String, nullable=True) # 替换内容
+ pattern = Column(String, nullable=False) # Replace pattern
+ content = Column(String, nullable=True) # Replace content
- # 关系
+ # Relationships
rule = relationship('ForwardRule', back_populates='replace_rules')
- # 添加唯一约束
+ # Add unique constraint
__table_args__ = (
UniqueConstraint('rule_id', 'pattern', 'content', name='unique_rule_pattern_content'),
)
@@ -137,7 +137,7 @@ class MediaTypes(Base):
audio = Column(Boolean, default=False)
voice = Column(Boolean, default=False)
- # 关系
+ # Relationships
rule = relationship('ForwardRule', back_populates='media_types')
@@ -146,12 +146,12 @@ class MediaExtensions(Base):
id = Column(Integer, primary_key=True)
rule_id = Column(Integer, ForeignKey('forward_rules.id'), nullable=False)
- extension = Column(String, nullable=False) # 存储不带点的扩展名,如 "jpg", "pdf"
+ extension = Column(String, nullable=False) # Store extension without dot, e.g. "jpg", "pdf"
- # 关系
+ # Relationships
rule = relationship('ForwardRule', back_populates='media_extensions')
- # 添加唯一约束
+ # Add unique constraint
__table_args__ = (
UniqueConstraint('rule_id', 'extension', name='unique_rule_extension'),
)
@@ -163,7 +163,7 @@ class RuleSync(Base):
rule_id = Column(Integer, ForeignKey('forward_rules.id'), nullable=False)
sync_rule_id = Column(Integer, nullable=False)
- # 关系
+ # Relationships
rule = relationship('ForwardRule', back_populates='rule_syncs')
class PushConfig(Base):
@@ -173,10 +173,10 @@ class PushConfig(Base):
rule_id = Column(Integer, ForeignKey('forward_rules.id'), nullable=False)
enable_push_channel = Column(Boolean, default=False)
push_channel = Column(String, nullable=False)
- #媒体发送方式,一次一张Single还是多张Multiple
+ # Media send mode, one at a time Single or multiple Multiple
media_send_mode = Column(String, nullable=False, default='Single')
- # 关系
+ # Relationships
rule = relationship('ForwardRule', back_populates='push_config')
class RSSConfig(Base):
@@ -184,24 +184,24 @@ class RSSConfig(Base):
id = Column(Integer, primary_key=True)
rule_id = Column(Integer, ForeignKey('forward_rules.id'), nullable=False, unique=True)
- enable_rss = Column(Boolean, default=False) # 是否启用RSS
- rule_title = Column(String, nullable=True) # RSS feed 标题
- rule_description = Column(String, nullable=True) # RSS feed 描述
- language = Column(String, default='zh-CN') # RSS feed 语言
- max_items = Column(Integer, default=50) # RSS feed 最大条目数
- # 是否启用自动提取标题和内容
+ enable_rss = Column(Boolean, default=False) # Whether to enable RSS
+ rule_title = Column(String, nullable=True) # RSS feed title
+ rule_description = Column(String, nullable=True) # RSS feed description
+ language = Column(String, default='zh-CN') # RSS feed language
+ max_items = Column(Integer, default=50) # RSS feed maximum items
+ # Whether to enable auto title and content extraction
is_auto_title = Column(Boolean, default=False)
is_auto_content = Column(Boolean, default=False)
- # 是否启用ai提取标题和内容
+ # Whether to enable AI title and content extraction
is_ai_extract = Column(Boolean, default=False)
- # ai提取标题和内容的prompt
+ # AI title and content extraction prompt
ai_extract_prompt = Column(String, nullable=True)
is_auto_markdown_to_html = Column(Boolean, default=False)
- # 是否启用自定义提取标题和内容的正则表达式
+ # Whether to enable custom regex for title and content extraction
enable_custom_title_pattern = Column(Boolean, default=False)
enable_custom_content_pattern = Column(Boolean, default=False)
- # 关系
+ # Relationships
rule = relationship('ForwardRule', back_populates='rss_config')
patterns = relationship('RSSPattern', back_populates='rss_config', cascade="all, delete-orphan")
@@ -212,15 +212,15 @@ class RSSPattern(Base):
id = Column(Integer, primary_key=True)
rss_config_id = Column(Integer, ForeignKey('rss_configs.id'), nullable=False)
- pattern = Column(String, nullable=False) # 正则表达式模式
- pattern_type = Column(String, nullable=False) # 模式类型: 'title' 或 'content'
- priority = Column(Integer, default=0) # 执行优先级,数字越小优先级越高
+ pattern = Column(String, nullable=False) # Regex pattern
+ pattern_type = Column(String, nullable=False) # Pattern type: 'title' or 'content'
+ priority = Column(Integer, default=0) # Execution priority, lower number means higher priority
- # 关系
+ # Relationships
rss_config = relationship('RSSConfig', back_populates='patterns')
- # 添加联合唯一约束
+ # Add composite unique constraint
__table_args__ = (
UniqueConstraint('rss_config_id', 'pattern', 'pattern_type', name='unique_rss_pattern'),
)
@@ -229,66 +229,66 @@ class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
- username = Column(String, nullable=False)
- password = Column(String, nullable=False)
+ username = Column(String, nullable=False)
+ password = Column(String, nullable=False)
def migrate_db(engine):
- """数据库迁移函数,确保新字段的添加"""
+ """Database migration function, ensures new fields are added"""
inspector = inspect(engine)
-
- # 获取当前数据库中所有表
+
+ # Get all tables in current database
existing_tables = inspector.get_table_names()
-
- # 连接数据库
+
+ # Connect to database
connection = engine.connect()
-
+
try:
with engine.connect() as connection:
- # 如果rule_syncs表不存在,创建表
+ # If rule_syncs table does not exist, create it
if 'rule_syncs' not in existing_tables:
- logging.info("创建rule_syncs表...")
+ logging.info("Creating rule_syncs table...")
RuleSync.__table__.create(engine)
- # 如果users表不存在,创建表
+ # If users table does not exist, create it
if 'users' not in existing_tables:
- logging.info("创建users表...")
+ logging.info("Creating users table...")
User.__table__.create(engine)
- # 如果rss_configs表不存在,创建表
+ # If rss_configs table does not exist, create it
if 'rss_configs' not in existing_tables:
- logging.info("创建rss_configs表...")
+ logging.info("Creating rss_configs table...")
RSSConfig.__table__.create(engine)
-
- # 如果rss_patterns表不存在,创建表
+
+ # If rss_patterns table does not exist, create it
if 'rss_patterns' not in existing_tables:
- logging.info("创建rss_patterns表...")
+ logging.info("Creating rss_patterns table...")
RSSPattern.__table__.create(engine)
- # 如果push_configs表不存在,创建表
+ # If push_configs table does not exist, create it
if 'push_configs' not in existing_tables:
- logging.info("创建push_configs表...")
+ logging.info("Creating push_configs table...")
PushConfig.__table__.create(engine)
-
-
- # 如果media_types表不存在,创建表
+
+
+ # If media_types table does not exist, create it
if 'media_types' not in existing_tables:
- logging.info("创建media_types表...")
+ logging.info("Creating media_types table...")
MediaTypes.__table__.create(engine)
-
- # 如果forward_rules表中有selected_media_types列,迁移数据到新表
+
+ # If forward_rules table has selected_media_types column, migrate data to new table
if 'selected_media_types' in forward_rules_columns:
- logging.info("迁移媒体类型数据到新表...")
- # 查询所有规则
+ logging.info("Migrating media type data to new table...")
+ # Query all rules
rules = connection.execute(text("SELECT id, selected_media_types FROM forward_rules WHERE selected_media_types IS NOT NULL"))
-
+
for rule in rules:
rule_id = rule[0]
selected_types = rule[1]
if selected_types:
- # 创建媒体类型记录
+ # Create media type record
media_types_data = {
'photo': 'photo' in selected_types,
'document': 'document' in selected_types,
@@ -296,8 +296,8 @@ def migrate_db(engine):
'audio': 'audio' in selected_types,
'voice': 'voice' in selected_types
}
-
- # 插入数据
+
+ # Insert data
connection.execute(
text("""
INSERT INTO media_types (rule_id, photo, document, video, audio, voice)
@@ -313,22 +313,21 @@ def migrate_db(engine):
}
)
if 'media_extensions' not in existing_tables:
- logging.info("创建media_extensions表...")
+ logging.info("Creating media_extensions table...")
MediaExtensions.__table__.create(engine)
-
+
except Exception as e:
- logging.error(f'迁移媒体类型数据时出错: {str(e)}')
-
-
+ logging.error(f'Error migrating media type data: {str(e)}')
- # 检查forward_rules表的现有列
+
+ # Check existing columns of forward_rules table
forward_rules_columns = {column['name'] for column in inspector.get_columns('forward_rules')}
- # 检查Keyword表的现有列
+ # Check existing columns of Keyword table
keyword_columns = {column['name'] for column in inspector.get_columns('keywords')}
- # 需要添加的新列及其默认值
+ # New columns to add and their default values
forward_rules_new_columns = {
'is_ai': 'ALTER TABLE forward_rules ADD COLUMN is_ai BOOLEAN DEFAULT FALSE',
'ai_model': 'ALTER TABLE forward_rules ADD COLUMN ai_model VARCHAR DEFAULT NULL',
@@ -360,7 +359,7 @@ def migrate_db(engine):
'enable_sync': 'ALTER TABLE forward_rules ADD COLUMN enable_sync BOOLEAN DEFAULT FALSE',
'userinfo_template': 'ALTER TABLE forward_rules ADD COLUMN userinfo_template VARCHAR DEFAULT "**{name}**"',
'time_template': 'ALTER TABLE forward_rules ADD COLUMN time_template VARCHAR DEFAULT "{time}"',
- 'original_link_template': 'ALTER TABLE forward_rules ADD COLUMN original_link_template VARCHAR DEFAULT "原始连接:{original_link}"',
+ 'original_link_template': 'ALTER TABLE forward_rules ADD COLUMN original_link_template VARCHAR DEFAULT "Original link: {original_link}"',
'enable_push': 'ALTER TABLE forward_rules ADD COLUMN enable_push BOOLEAN DEFAULT FALSE',
'enable_only_push': 'ALTER TABLE forward_rules ADD COLUMN enable_only_push BOOLEAN DEFAULT FALSE',
'media_allow_text': 'ALTER TABLE forward_rules ADD COLUMN media_allow_text BOOLEAN DEFAULT FALSE',
@@ -371,48 +370,48 @@ def migrate_db(engine):
'is_blacklist': 'ALTER TABLE keywords ADD COLUMN is_blacklist BOOLEAN DEFAULT TRUE',
}
- # 添加缺失的列
+ # Add missing columns
with engine.connect() as connection:
- # 添加forward_rules表的列
+ # Add columns to forward_rules table
for column, sql in forward_rules_new_columns.items():
if column not in forward_rules_columns:
try:
connection.execute(text(sql))
- logging.info(f'已添加列: {column}')
+ logging.info(f'Column added: {column}')
except Exception as e:
- logging.error(f'添加列 {column} 时出错: {str(e)}')
-
+ logging.error(f'Error adding column {column}: {str(e)}')
+
- # 添加keywords表的列
+ # Add columns to keywords table
for column, sql in keywords_new_columns.items():
if column not in keyword_columns:
try:
connection.execute(text(sql))
- logging.info(f'已添加列: {column}')
+ logging.info(f'Column added: {column}')
except Exception as e:
- logging.error(f'添加列 {column} 时出错: {str(e)}')
+ logging.error(f'Error adding column {column}: {str(e)}')
- #先检查forward_rules表的列的forward_mode是否存在
+ # First check if forward_rules table has forward_mode column
if 'forward_mode' not in forward_rules_columns:
- # 修改forward_rules表的列mode为forward_mode
+ # Rename mode column to forward_mode in forward_rules table
connection.execute(text("ALTER TABLE forward_rules RENAME COLUMN mode TO forward_mode"))
- logging.info('修改forward_rules表的列mode为forward_mode成功')
+ logging.info('Successfully renamed mode column to forward_mode in forward_rules table')
- # 修改keywords表的唯一约束
+ # Update unique constraint for keywords table
try:
with engine.connect() as connection:
- # 检查索引是否存在
+ # Check if index exists
result = connection.execute(text("""
- SELECT name FROM sqlite_master
+ SELECT name FROM sqlite_master
WHERE type='index' AND name='unique_rule_keyword_is_regex_is_blacklist'
"""))
index_exists = result.fetchone() is not None
if not index_exists:
- logging.info('开始更新 keywords 表的唯一约束...')
+ logging.info('Starting to update keywords table unique constraint...')
try:
-
+
with engine.begin() as connection:
- # 创建临时表
+ # Create temporary table
connection.execute(text("""
CREATE TABLE keywords_temp (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -420,59 +419,59 @@ def migrate_db(engine):
keyword TEXT,
is_regex BOOLEAN,
is_blacklist BOOLEAN
- -- 如果 keywords 表还有其他字段,请在这里一并定义
+ -- If keywords table has other fields, define them here as well
)
"""))
- logging.info('创建 keywords_temp 表结构成功')
+ logging.info('Successfully created keywords_temp table structure')
- # 将原表数据复制到临时表,让数据库自动生成 id
+ # Copy original table data to temporary table, let database auto-generate id
result = connection.execute(text("""
INSERT INTO keywords_temp (rule_id, keyword, is_regex, is_blacklist)
SELECT rule_id, keyword, is_regex, is_blacklist FROM keywords
"""))
- logging.info(f'复制数据到 keywords_temp 成功,影响行数: {result.rowcount}')
+ logging.info(f'Successfully copied data to keywords_temp, rows affected: {result.rowcount}')
- # 删除原表 keywords
+ # Delete original keywords table
connection.execute(text("DROP TABLE keywords"))
- logging.info('删除原表 keywords 成功')
+ logging.info('Successfully deleted original keywords table')
- # 4将临时表重命名为 keywords
+ # Rename temporary table to keywords
connection.execute(text("ALTER TABLE keywords_temp RENAME TO keywords"))
- logging.info('重命名 keywords_temp 为 keywords 成功')
+ logging.info('Successfully renamed keywords_temp to keywords')
- # 添加唯一约束
+ # Add unique constraint
connection.execute(text("""
- CREATE UNIQUE INDEX unique_rule_keyword_is_regex_is_blacklist
+ CREATE UNIQUE INDEX unique_rule_keyword_is_regex_is_blacklist
ON keywords (rule_id, keyword, is_regex, is_blacklist)
"""))
- logging.info('添加唯一约束 unique_rule_keyword_is_regex_is_blacklist 成功')
+ logging.info('Successfully added unique constraint unique_rule_keyword_is_regex_is_blacklist')
- logging.info('成功更新 keywords 表结构和唯一约束')
+ logging.info('Successfully updated keywords table structure and unique constraint')
except Exception as e:
- logging.error(f'更新 keywords 表结构时出错: {str(e)}')
+ logging.error(f'Error updating keywords table structure: {str(e)}')
else:
- logging.info('唯一约束已存在,跳过创建')
+ logging.info('Unique constraint already exists, skipping creation')
except Exception as e:
- logging.error(f'更新唯一约束时出错: {str(e)}')
+ logging.error(f'Error updating unique constraint: {str(e)}')
def init_db():
- """初始化数据库"""
- # 创建数据库文件夹
+ """Initialize database"""
+ # Create database folder
os.makedirs('./db', exist_ok=True)
engine = create_engine('sqlite:///./db/forward.db')
- # 首先创建所有表
+ # First create all tables
Base.metadata.create_all(engine)
- # 然后进行必要的迁移
+ # Then perform necessary migrations
migrate_db(engine)
return engine
def get_session():
- """创建会话工厂"""
+ """Create session factory"""
engine = create_engine('sqlite:///./db/forward.db')
Session = sessionmaker(bind=engine)
return Session()
@@ -481,4 +480,4 @@ def get_session():
logging.basicConfig(level=logging.INFO)
engine = init_db()
session = get_session()
- logging.info("数据库初始化和迁移完成。")
\ No newline at end of file
+ logging.info("Database initialization and migration completed.")
\ No newline at end of file
From 8f0484abff920b14764d391ff254bf28b66e9210 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:17:26 +0000
Subject: [PATCH 39/76] Translate Chinese to English in filters/process.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
filters/process.py | 74 +++++++++++++++++++++++-----------------------
1 file changed, 37 insertions(+), 37 deletions(-)
diff --git a/filters/process.py b/filters/process.py
index 15a749f..1875f71 100644
--- a/filters/process.py
+++ b/filters/process.py
@@ -18,65 +18,65 @@
async def process_forward_rule(client, event, chat_id, rule):
"""
- 处理转发规则
-
+ Process forwarding rule
+
Args:
- client: 机器人客户端
- event: 消息事件
- chat_id: 聊天ID
- rule: 转发规则
-
+ client: Bot client
+ event: Message event
+ chat_id: Chat ID
+ rule: Forwarding rule
+
Returns:
- bool: 处理是否成功
+ bool: Whether processing was successful
"""
- logger.info(f'使用过滤器链处理规则 ID: {rule.id}')
-
- # 创建过滤器链
+ logger.info(f'Processing rule with filter chain, ID: {rule.id}')
+
+ # Create filter chain
filter_chain = FilterChain()
- # 添加初始化过滤器
+ # Add initialization filter
filter_chain.add_filter(InitFilter())
- # 延迟处理过滤器(如果启用了延迟处理)
+ # Delay processing filter (if delay processing is enabled)
filter_chain.add_filter(DelayFilter())
-
- # 添加关键字过滤器(如果消息不匹配关键字,会中断处理链)
+
+ # Add keyword filter (if the message doesn't match keywords, it will interrupt the processing chain)
filter_chain.add_filter(KeywordFilter())
-
- # 添加替换过滤器
+
+ # Add replace filter
filter_chain.add_filter(ReplaceFilter())
- # 添加媒体过滤器(处理媒体内容)
+ # Add media filter (process media content)
filter_chain.add_filter(MediaFilter())
-
- # 添加AI处理过滤器(如果启用了AI处理后的关键字检查,可能会中断处理链)
+
+ # Add AI processing filter (if keyword check after AI processing is enabled, it may interrupt the processing chain)
filter_chain.add_filter(AIFilter())
-
- # 添加信息过滤器(处理原始链接和发送者信息)
+
+ # Add info filter (process original link and sender information)
filter_chain.add_filter(InfoFilter())
-
- # 添加评论区按钮过滤器
+
+ # Add comment section button filter
filter_chain.add_filter(CommentButtonFilter())
- # 添加RSS过滤器
+ # Add RSS filter
filter_chain.add_filter(RSSFilter())
-
- # 添加编辑过滤器(编辑原始消息)
+
+ # Add edit filter (edit original message)
filter_chain.add_filter(EditFilter())
- # 添加发送过滤器(发送消息)
+ # Add sender filter (send message)
filter_chain.add_filter(SenderFilter())
-
- # 添加回复过滤器(处理媒体组消息的评论区按钮)
+
+ # Add reply filter (handle comment section buttons for media group messages)
filter_chain.add_filter(ReplyFilter())
- # 添加推送过滤器
+ # Add push filter
filter_chain.add_filter(PushFilter())
-
- # 添加删除原始消息过滤器(最后执行)
+
+ # Add delete original message filter (executed last)
filter_chain.add_filter(DeleteOriginalFilter())
-
- # 执行过滤器链
+
+ # Execute filter chain
result = await filter_chain.process(client, event, chat_id, rule)
-
- return result
+
+ return result
From da9953537f6d7f5eed298e8b41635c00a1b026e2 Mon Sep 17 00:00:00 2001
From: Claude
Date: Tue, 10 Feb 2026 20:17:37 +0000
Subject: [PATCH 40/76] Translate Chinese to English in
rss/app/services/feed_generator.py
https://claude.ai/code/session_0129i7p4V7j2cnkQ5MYXxrqX
---
rss/app/services/feed_generator.py | 470 ++++++++++++++---------------
1 file changed, 235 insertions(+), 235 deletions(-)
diff --git a/rss/app/services/feed_generator.py b/rss/app/services/feed_generator.py
index c5e3e17..6081664 100644
--- a/rss/app/services/feed_generator.py
+++ b/rss/app/services/feed_generator.py
@@ -11,171 +11,171 @@
import json
from models.models import get_session, RSSConfig
from utils.constants import DEFAULT_TIMEZONE
-import pytz
+import pytz
logger = logging.getLogger(__name__)
class FeedService:
-
-
+
+
@staticmethod
def extract_telegram_title_and_content(content: str) -> tuple[str, str]:
- """从Telegram消息中提取标题和内容
-
+ """Extract title and content from a Telegram message
+
Args:
- content: 原始消息内容
-
+ content: Original message content
+
Returns:
- tuple: (标题, 剩余内容)
+ tuple: (title, remaining content)
"""
if not content:
- logger.info("输入内容为空,返回空标题和内容")
+ logger.info("Input content is empty, returning empty title and content")
return "", ""
-
+
try:
- # 读取标题模板配置
+ # Read title template configuration
config_path = Path(__file__).parent.parent / 'configs' / 'title_template.json'
- logger.info(f"正在读取标题模板配置文件: {config_path}")
+ logger.info(f"Reading title template configuration file: {config_path}")
with open(config_path, 'r', encoding='utf-8') as f:
title_config = json.load(f)
-
- # 遍历每个模式
+
+ # Iterate through each pattern
for pattern_info in title_config['patterns']:
pattern_str = pattern_info['pattern']
pattern_desc = pattern_info['description']
- logger.debug(f"尝试匹配模式: {pattern_desc} ({pattern_str})")
-
- # 编译正则表达式
+ logger.debug(f"Trying to match pattern: {pattern_desc} ({pattern_str})")
+
+ # Compile regex
pattern = re.compile(pattern_str, re.MULTILINE)
-
- # 尝试匹配
+
+ # Try to match
match = pattern.match(content)
if match:
title = FeedService.clean_title(match.group(1))
- # 获取匹配部分的起始和结束位置
+ # Get the start and end positions of the matched portion
start, end = match.span(0)
- # 提取剩余内容,去除开头的空白字符
+ # Extract remaining content, strip leading whitespace
remaining_content = content[end:].lstrip()
- logger.info(f"成功匹配到标题模式: {pattern_desc}")
- logger.info(f"原始内容: {content[:100]}...") # 只显示前100个字符
- logger.info(f"匹配模式: {pattern_str}")
- logger.info(f"提取的标题: {title}")
- logger.info(f"剩余内容长度: {len(remaining_content)} 字符")
+ logger.info(f"Successfully matched title pattern: {pattern_desc}")
+ logger.info(f"Original content: {content[:100]}...") # Only show first 100 characters
+ logger.info(f"Matched pattern: {pattern_str}")
+ logger.info(f"Extracted title: {title}")
+ logger.info(f"Remaining content length: {len(remaining_content)} characters")
return title, remaining_content
-
- # 如果没有匹配到任何模式,使用前20个字符作为标题
- logger.info("未匹配到任何标题模式,使用前20个字符作为标题")
- # 去除内容中的换行符,并限制标题长度为20个字符
+
+ # If no pattern matched, use the first 20 characters as the title
+ logger.info("No title pattern matched, using first 20 characters as title")
+ # Remove line breaks from content and limit title length to 20 characters
clean_content = FeedService.clean_content(content)
clean_content = clean_content.replace('\n', ' ').strip()
title = clean_content[:20]
if len(clean_content) > 20:
title += "..."
- logger.debug(f"生成的默认标题: {title}")
+ logger.debug(f"Generated default title: {title}")
return title, content
-
+
except Exception as e:
- logger.error(f"提取标题和内容时出错: {str(e)}")
+ logger.error(f"Error extracting title and content: {str(e)}")
return "", content
-
+
@staticmethod
def clean_title(title: str) -> str:
- """清理标题中的特殊字符和格式标记
-
+ """Clean special characters and formatting marks from the title
+
Args:
- title: 原始标题文本
-
+ title: Original title text
+
Returns:
- str: 清理后的标题
+ str: Cleaned title
"""
if not title:
return ""
-
- # 移除所有 * 号
+
+ # Remove all * characters
title = title.replace('*', '')
-
- # 处理链接格式 [text](url),保留text部分
+
+ # Handle link format [text](url), keep the text part
title = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', title)
-
- # 移除换行和首尾空白
+
+ # Remove line breaks and leading/trailing whitespace
title = title.replace('\n', ' ').strip()
-
+
return title
-
+
@staticmethod
def clean_content(content: str) -> str:
- """清理内容中的特殊字符和格式标记
-
+ """Clean special characters and formatting marks from the content
+
Args:
- content: 原始内容文本
-
+ content: Original content text
+
Returns:
- str: 清理后的内容
+ str: Cleaned content
"""
if not content:
return ""
-
- # 去除开头可能的1-2个星号
+
+ # Remove possible 1-2 asterisks at the beginning
content = re.sub(r'^\*{1,2}\s*', '', content)
-
- # 去除开头的空行
+
+ # Remove empty lines at the beginning
content = re.sub(r'^\s*\n+', '', content)
-
+
return content
-
+
@staticmethod
async def generate_feed_from_entries(rule_id: int, entries: List[Entry], base_url: str = None) -> FeedGenerator:
- """根据真实条目生成Feed"""
+ """Generate Feed from real entries"""
fg = FeedGenerator()
- # 设置编码
+ # Set encoding
fg.load_extension('base', atom=True)
rss_config = None
-
- # 如果没有提供base_url,使用配置中的默认值
+
+ # If no base_url is provided, use the default from configuration
if base_url is None:
base_url = f"http://{settings.HOST}:{settings.PORT}"
-
- logger.info(f"生成Feed - 规则ID: {rule_id}, 条目数量: {len(entries)}, 基础URL: {base_url}")
-
+
+ logger.info(f"Generating Feed - Rule ID: {rule_id}, Entry count: {len(entries)}, Base URL: {base_url}")
+
session = get_session()
try:
rss_config = session.query(RSSConfig).filter(RSSConfig.rule_id == rule_id).first()
- logger.info(f"获取RSS配置: {rss_config.__dict__}")
- # 获取 Feed 标题和描述
+ logger.info(f"Retrieved RSS config: {rss_config.__dict__}")
+ # Get Feed title and description
if rss_config and rss_config.enable_rss:
if rss_config.rule_title:
fg.title(rss_config.rule_title)
else:
fg.title(f'TG Forwarder RSS - Rule {rule_id}')
-
+
if rss_config.rule_description:
fg.description(rss_config.rule_description)
else:
- fg.description(f'TG Forwarder RSS - 规则 {rule_id} 的消息')
-
- # 设置语言
+ fg.description(f'TG Forwarder RSS - Messages for rule {rule_id}')
+
+ # Set language
fg.language(rss_config.language or 'zh-CN')
else:
- # 默认标题和描述
+ # Default title and description
fg.title(f'TG Forwarder RSS - Rule {rule_id}')
- fg.description(f'TG Forwarder RSS - 规则 {rule_id} 的消息')
+ fg.description(f'TG Forwarder RSS - Messages for rule {rule_id}')
fg.language('zh-CN')
finally:
- # 确保会话被关闭
+ # Ensure session is closed
session.close()
-
- # 设置Feed链接
+
+ # Set Feed link
fg.link(href=f'{base_url}/rss/feed/{rule_id}')
-
- # 添加条目
+
+ # Add entries
for entry in entries:
try:
fe = fg.add_entry()
fe.id(entry.id or entry.message_id)
- # 初始化content变量
+ # Initialize content variable
content = None
fe.title(entry.title)
@@ -187,7 +187,7 @@ async def generate_feed_from_entries(rule_id: int, entries: List[Entry], base_ur
fe.title(entry.title)
if rss_config.enable_custom_content_pattern:
content = entry.content
- # 自动提取标题和内容
+ # Auto-extract title and content
if rss_config.is_auto_title or rss_config.is_auto_content:
extracted_title, extracted_content = FeedService.extract_telegram_title_and_content(entry.content or "")
if rss_config.is_auto_title:
@@ -195,120 +195,120 @@ async def generate_feed_from_entries(rule_id: int, entries: List[Entry], base_ur
if rss_config.is_auto_content:
content = FeedService.convert_markdown_to_html(extracted_content)
else:
- # 如果不自动提取内容,使用原始内容
+ # If not auto-extracting content, use original content
content = FeedService.convert_markdown_to_html(entry.content or "")
else:
- # 如果不是自动提取,直接使用原始内容
+ # If not auto-extracting, use original content directly
content = FeedService.convert_markdown_to_html(entry.content or "")
- # 添加图片 - 针对各种RSS阅读器的优化处理
- all_media_urls = [] # 存储所有媒体URL用于后续检查
-
+ # Add images - optimized handling for various RSS readers
+ all_media_urls = [] # Store all media URLs for subsequent checks
+
if entry.media:
- logger.info(f"处理条目 {entry.id} 的媒体文件,数量: {len(entry.media)}")
- # 处理每个媒体文件
+ logger.info(f"Processing media files for entry {entry.id}, count: {len(entry.media)}")
+ # Process each media file
for idx, media in enumerate(entry.media):
- # 记录原始媒体URL
- original_url = media.url if hasattr(media, 'url') else "未知"
- logger.info(f"媒体 {idx+1}/{len(entry.media)} - 原始URL: {original_url}")
-
- # 构建规范化的媒体URL - 恢复为包含规则ID的格式
+ # Log original media URL
+ original_url = media.url if hasattr(media, 'url') else "unknown"
+ logger.info(f"Media {idx+1}/{len(entry.media)} - Original URL: {original_url}")
+
+ # Build normalized media URL - restored to format including rule ID
media_filename = os.path.basename(media.url.split('/')[-1])
media_url = f"/media/{entry.rule_id}/{media_filename}"
full_media_url = f"{base_url}{media_url}"
all_media_urls.append(full_media_url)
-
- logger.info(f"媒体 {idx+1}/{len(entry.media)} - 新URL: {full_media_url}")
-
- # 处理图片类型
+
+ logger.info(f"Media {idx+1}/{len(entry.media)} - New URL: {full_media_url}")
+
+ # Handle image types
if media.type.startswith('image/'):
try:
- # 构建媒体文件路径
+ # Build media file path
rule_media_path = settings.get_rule_media_path(entry.rule_id)
media_path = os.path.join(rule_media_path, media_filename)
-
- # 添加图片标签到内容中 - 使用包含规则ID的URL格式
+
+ # Add image tag to content - using URL format with rule ID
img_tag = f'
'
content += img_tag
-
- logger.info(f"已添加图片标签到内容中: {media_filename}")
+
+ logger.info(f"Added image tag to content: {media_filename}")
except Exception as e:
- logger.error(f"添加图片标签时出错: {str(e)}")
+ logger.error(f"Error adding image tag: {str(e)}")
elif media.type.startswith('video/'):
- # 为视频添加特殊处理
+ # Special handling for videos
display_name = ""
if hasattr(media, "original_name") and media.original_name:
display_name = media.original_name
else:
display_name = media.filename
-
- # 添加HTML5视频播放器 - 使用内联样式
+
+ # Add HTML5 video player - using inline styles
video_player = f'''
'''
content += file_tag
-
- # 确保content不为空,至少包含一些默认文本
+
+ # Ensure content is not empty, at least include some default text
if not content:
- content = "
该消息没有文本内容。
"
+ content = "
This message has no text content.
"
if entry.media and len(entry.media) > 0:
- content += f"
包含 {len(entry.media)} 个媒体文件。
"
-
- # 确保content是有效的HTML
+ content += f"
Contains {len(entry.media)} media files.
"
+
+ # Ensure content is valid HTML
if not content.startswith("<"):
- # 预处理文本中的换行符,确保段落结构
+ # Preprocess line breaks in text to ensure paragraph structure
processed_content = ""
paragraphs = content.split("\n\n")
for p in paragraphs:
@@ -320,89 +320,89 @@ async def generate_feed_from_entries(rule_id: int, entries: List[Entry], base_ur
processed_content += f" {line}"
processed_content += "
"
content = processed_content if processed_content else f"
{content}
"
-
- # 删除多余的HTML标签和空格,但保留有意义的段落结构
+
+ # Remove redundant HTML tags and spaces, but preserve meaningful paragraph structure
content = re.sub(r' \s* ', ' ', content)
content = re.sub(r'
\s*
', '', content)
content = re.sub(r'
', '', content)
-
- # 检查内容中是否包含硬编码的本地地址
+
+ # Check if content contains hardcoded local addresses
if "127.0.0.1" in content or "localhost" in content:
- logger.warning(f"内容中包含硬编码的本地地址,将替换为: {base_url}")
+ logger.warning(f"Content contains hardcoded local addresses, will replace with: {base_url}")
content = content.replace(f"http://127.0.0.1:{settings.PORT}", base_url)
content = content.replace(f"http://localhost:{settings.PORT}", base_url)
content = content.replace(f"http://{settings.HOST}:{settings.PORT}", base_url)
-
- # 添加媒体附件,并确保内容中包含所有媒体
+
+ # Add media attachments and ensure content includes all media
if entry.media:
for media in entry.media:
try:
- # 使用包含规则ID的媒体URL格式
+ # Use media URL format with rule ID
media_filename = os.path.basename(media.url.split('/')[-1])
full_media_url = f"{base_url}/media/{entry.rule_id}/{media_filename}"
-
- # 确保图片等内容已经添加
+
+ # Ensure images and other content have been added
if media.type.startswith('image/') and full_media_url not in content:
- # 如果内容中没有该图片,添加
+ # If the image is not in the content, add it
img_tag = f'
'
content += img_tag
- logger.info(f"添加缺失的图片标签: {media_filename}")
-
- # 记录添加的媒体附件
- logger.info(f"添加媒体附件: {full_media_url}, 类型: {media.type}, 大小: {media.size}")
-
- # 添加enclosure
+ logger.info(f"Added missing image tag: {media_filename}")
+
+ # Log added media attachment
+ logger.info(f"Added media attachment: {full_media_url}, type: {media.type}, size: {media.size}")
+
+ # Add enclosure
fe.enclosure(
url=full_media_url,
length=str(media.size) if hasattr(media, 'size') else "0",
type=media.type if hasattr(media, 'type') else "application/octet-stream"
)
except Exception as e:
- logger.error(f"添加媒体附件时出错: {str(e)}")
-
- # 设置内容字段
+ logger.error(f"Error adding media attachment: {str(e)}")
+
+ # Set content field
fe.content(content, type='html')
-
- # 设置描述字段 - 使用相同的内容
+
+ # Set description field - using the same content
fe.description(content)
-
- # 解析ISO格式时间字符串,设置发布时间
+
+ # Parse ISO format time string, set publish time
try:
published_dt = datetime.fromisoformat(entry.published)
fe.published(published_dt)
except ValueError:
- # 如果时间格式无效,使用当前时间
+ # If time format is invalid, use current time
try:
tz = pytz.timezone(DEFAULT_TIMEZONE)
fe.published(datetime.now(tz))
except Exception as tz_error:
- logger.warning(f"时区设置错误: {str(tz_error)},使用UTC时区")
+ logger.warning(f"Timezone setting error: {str(tz_error)}, using UTC timezone")
fe.published(datetime.now(pytz.UTC))
-
- # 设置作者和链接
+
+ # Set author and link
if entry.author:
fe.author(name=entry.author)
-
+
if entry.link:
fe.link(href=entry.link)
except Exception as e:
- logger.error(f"添加条目到Feed时出错: {str(e)}")
+ logger.error(f"Error adding entry to Feed: {str(e)}")
continue
-
+
return fg
-
+
@staticmethod
def _extract_chat_name(link: str) -> str:
- """从Telegram链接中提取频道/群组名称"""
+ """Extract channel/group name from Telegram link"""
if not link or 't.me/' not in link:
return ""
-
+
try:
- # 例如从 https://t.me/channel_name/1234 提取 channel_name
+ # e.g. extract channel_name from https://t.me/channel_name/1234
parts = link.split('t.me/')
if len(parts) < 2:
return ""
-
+
channel_part = parts[1].split('/')[0]
return channel_part
except Exception:
@@ -412,138 +412,138 @@ def _extract_chat_name(link: str) -> str:
@staticmethod
def convert_markdown_to_html(text):
- """将Markdown格式转换为HTML,使用标准markdown库,并保留换行结构"""
+ """Convert Markdown format to HTML using the standard markdown library, preserving line break structure"""
if not text:
return ""
-
- # 使用标准markdown库转换
+
+ # Use the standard markdown library for conversion
try:
- # 预处理文本,确保连续的换行符被正确转换成段落
- # 先将连续的多个换行替换为特殊标记
+ # Preprocess text to ensure consecutive line breaks are correctly converted to paragraphs
+ # First replace multiple consecutive line breaks with a special marker
text = re.sub(r'\n{2,}', '\n\n\n\n', text)
-
- # 转义以#开头的标签,防止被识别为标题
+
+ # Escape tags starting with # to prevent them from being recognized as headings
lines = text.split('\n')
processed_lines = []
for line in lines:
if line.startswith('#'):
line = '\\' + line
- processed_lines.append(line + ' ') # 添加两个空格确保换行
+ processed_lines.append(line + ' ') # Add two spaces to ensure line break
text = '\n'.join(processed_lines)
-
- # 使用markdown模块转换
+
+ # Use the markdown module for conversion
html = markdown.markdown(text, extensions=['extra'])
-
- # 处理特殊标记,确保段落分隔
+
+ # Process special markers to ensure paragraph separation
html = html.replace('', '
')
-
+
return html
except Exception as e:
- # 如果出现异常,退回到基本处理
- logger.error(f"Markdown转换异常: {str(e)}")
-
- # 改进的换行处理:将连续的两个或更多换行符转换为段落分隔
+ # If an exception occurs, fall back to basic processing
+ logger.error(f"Markdown conversion exception: {str(e)}")
+
+ # Improved line break handling: convert two or more consecutive line breaks to paragraph separators
text = re.sub(r'\n{2,}', '
', text)
-
- # 将单个换行符转换为
+
+ # Convert single line breaks to
text = text.replace('\n', ' ')
-
+
return f"
{text}
"
-
+
@staticmethod
def generate_test_feed(rule_id: int, base_url: str = None) -> FeedGenerator:
- """生成测试Feed,当没有真实条目数据时使用
-
+ """Generate a test Feed, used when there are no real entry data
+
Args:
- rule_id: 规则ID
- base_url: 请求的基础URL,用于生成链接
-
+ rule_id: Rule ID
+ base_url: Base URL of the request, used for generating links
+
Returns:
- FeedGenerator: 配置好的测试Feed生成器
+ FeedGenerator: Configured test Feed generator
"""
fg = FeedGenerator()
- # 设置编码
+ # Set encoding
fg.load_extension('base', atom=True)
rss_config = None
-
- # 如果没有提供base_url,使用配置中的默认值
+
+ # If no base_url is provided, use the default from configuration
if base_url is None:
base_url = f"http://{settings.HOST}:{settings.PORT}"
-
- logger.info(f"生成测试Feed - 规则ID: {rule_id}, 基础URL: {base_url}")
-
- # 从数据库获取RSS配置
+
+ logger.info(f"Generating test Feed - Rule ID: {rule_id}, Base URL: {base_url}")
+
+ # Get RSS configuration from database
session = get_session()
try:
rss_config = session.query(RSSConfig).filter(RSSConfig.rule_id == rule_id).first()
- logger.info(f"获取RSS配置: {rss_config}")
-
- # 设置Feed基本信息
+ logger.info(f"Retrieved RSS config: {rss_config}")
+
+ # Set Feed basic information
if rss_config and rss_config.enable_rss:
if rss_config.rule_title:
fg.title(rss_config.rule_title)
else:
fg.title(f'')
-
+
if rss_config.rule_description:
fg.description(rss_config.rule_description)
else:
fg.description(f' ')
-
- # 设置语言
+
+ # Set language
fg.language(rss_config.language or 'zh-CN')
finally:
- # 确保会话被关闭
+ # Ensure session is closed
session.close()
-
- # 设置Feed链接
+
+ # Set Feed link
feed_url = f'{base_url}/rss/feed/{rule_id}'
- logger.info(f"设置Feed链接: {feed_url}")
+ logger.info(f"Setting Feed link: {feed_url}")
fg.link(href=feed_url)
-
- # 处理时区
+
+ # Handle timezone
try:
tz = pytz.timezone(DEFAULT_TIMEZONE)
except Exception as tz_error:
- logger.warning(f"时区设置错误: {str(tz_error)},使用UTC时区")
+ logger.warning(f"Timezone setting error: {str(tz_error)}, using UTC timezone")
tz = pytz.UTC
-
- # # 只添加一条测试条目
+
+ # # Only add one test entry
# try:
# fe = fg.add_entry()
-
- # # 设置测试条目ID和标题
+
+ # # Set test entry ID and title
# entry_id = f"test-{rule_id}-1"
# fe.id(entry_id)
- # fe.title(f"测试条目 - 规则 {rule_id}")
-
- # # 生成内容,包括测试说明
+ # fe.title(f"Test entry - Rule {rule_id}")
+
+ # # Generate content, including test description
# current_time = datetime.now(tz)
# content = f'''
- #
这是一个测试条目,由系统自动生成,因为规则 {rule_id} 当前没有任何消息数据。
- #
当有消息被转发时,真实的条目将会在这里显示。
+ #
This is a test entry, automatically generated by the system, because rule {rule_id} currently has no message data.
+ #
When messages are forwarded, real entries will be displayed here.