From 3bb4af632f5ff50ec9c7933b7aba5680cf727110 Mon Sep 17 00:00:00 2001
From: RayShaw97 <152388631+RayShaw97@users.noreply.github.com>
Date: Sun, 8 Feb 2026 20:02:07 +0800
Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=E5=9C=A8=E7=9B=AE=E6=A0=87?=
=?UTF-8?q?=E8=81=8A=E5=A4=A9=E5=BC=80=E5=90=AF=E4=BA=86`Topics`=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=E7=9A=84=E6=83=85=E5=86=B5=E4=B8=8B=EF=BC=8C=E4=BD=BF?=
=?UTF-8?q?=E7=94=A8`/bind`=E5=91=BD=E4=BB=A4=E5=B0=86=E8=AE=B0=E5=BD=95?=
=?UTF-8?q?=20Topic=20=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=B9=B6=E4=B8=94?=
=?UTF-8?q?=E5=91=BD=E4=B8=AD=E8=A7=84=E5=88=99=E7=9A=84=E6=B6=88=E6=81=AF?=
=?UTF-8?q?=E4=BC=9A=E8=A2=AB=E6=8A=95=E9=80=92=E5=88=B0=20Topic=20?=
=?UTF-8?q?=E4=B8=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
filters/sender_filter.py | 73 ++++++++++++++++++++--
handlers/button/callback/other_callback.py | 2 +-
handlers/button/settings_manager.py | 6 +-
handlers/command_handlers.py | 30 +++++++--
models/models.py | 4 +-
utils/common.py | 16 +++++
6 files changed, 116 insertions(+), 15 deletions(-)
diff --git a/filters/sender_filter.py b/filters/sender_filter.py
index a22864f..be6c9b6 100644
--- a/filters/sender_filter.py
+++ b/filters/sender_filter.py
@@ -6,11 +6,66 @@
logger = logging.getLogger(__name__)
+_TOPIC_REPLY_ERROR_TOKENS = (
+ "MSG_ID_INVALID",
+ "MESSAGE_ID_INVALID",
+ "REPLY_MESSAGE_ID_INVALID",
+ "TOPIC_DELETED",
+ "TOPIC_CLOSED",
+ "THREAD_ID_INVALID",
+ "TOPIC_ID_INVALID",
+)
+
+def _is_topic_reply_error(exc):
+ text = f"{type(exc).__name__} {exc}".upper()
+ normalized = text.replace("_", "")
+ for token in _TOPIC_REPLY_ERROR_TOKENS:
+ if token in text:
+ return True
+ if token.replace("_", "") in normalized:
+ return True
+ return False
+
+
class SenderFilter(BaseFilter):
"""
消息发送过滤器,用于发送处理后的消息
"""
+ async def _send_message_with_topic_retry(self, client, rule, target_chat_id, *args, **kwargs):
+ topic_id = getattr(rule, 'target_topic_id', None)
+ reply_to = topic_id
+ if reply_to is None:
+ return await client.send_message(target_chat_id, *args, **kwargs)
+
+ try:
+ return await client.send_message(target_chat_id, *args, reply_to=reply_to, **kwargs)
+ except Exception as e:
+ if _is_topic_reply_error(e):
+ logger.warning(
+ f'发送消息到Topic失败,降级到主聊天重试 (rule_id={rule.id}, '
+ f'target_chat_id={target_chat_id}, topic_id={topic_id}, error={str(e)})'
+ )
+ return await client.send_message(target_chat_id, *args, **kwargs)
+ raise
+
+ async def _send_file_with_topic_retry(self, client, rule, target_chat_id, *args, **kwargs):
+ topic_id = getattr(rule, 'target_topic_id', None)
+ reply_to = topic_id
+ if reply_to is None:
+ return await client.send_file(target_chat_id, *args, **kwargs)
+
+ try:
+ return await client.send_file(target_chat_id, *args, reply_to=reply_to, **kwargs)
+ except Exception as e:
+ if _is_topic_reply_error(e):
+ logger.warning(
+ f'发送文件到Topic失败,降级到主聊天重试 (rule_id={rule.id}, '
+ f'target_chat_id={target_chat_id}, topic_id={topic_id}, error={str(e)})'
+ )
+ return await client.send_file(target_chat_id, *args, **kwargs)
+ raise
+
async def _process(self, context):
"""
发送处理后的消息
@@ -159,7 +214,9 @@ async def _send_media_group(self, context, target_chat_id, parse_mode):
caption_text += context.time_info + context.original_link
# 作为一个组发送所有文件
- sent_messages = await client.send_file(
+ sent_messages = await self._send_file_with_topic_retry(
+ client,
+ rule,
target_chat_id,
files,
caption=caption_text,
@@ -214,7 +271,9 @@ async def _send_single_media(self, context, target_chat_id, parse_mode):
text_to_send += original_link
- await client.send_message(
+ await self._send_message_with_topic_retry(
+ client,
+ rule,
target_chat_id,
text_to_send,
parse_mode=parse_mode,
@@ -238,7 +297,9 @@ async def _send_single_media(self, context, target_chat_id, parse_mode):
context.original_link
)
- await client.send_file(
+ await self._send_file_with_topic_retry(
+ client,
+ rule,
target_chat_id,
file_path,
caption=caption,
@@ -284,11 +345,13 @@ async def _send_text_message(self, context, target_chat_id, parse_mode):
# 组合消息文本
message_text = context.sender_info + context.message_text + context.time_info + context.original_link
- await client.send_message(
+ await self._send_message_with_topic_retry(
+ client,
+ rule,
target_chat_id,
str(message_text),
parse_mode=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'{"带预览的" if link_preview else "无预览的"}文本消息已发送')
diff --git a/handlers/button/callback/other_callback.py b/handlers/button/callback/other_callback.py
index 334ecdd..f6239cf 100644
--- a/handlers/button/callback/other_callback.py
+++ b/handlers/button/callback/other_callback.py
@@ -314,7 +314,7 @@ async def callback_perform_copy_rule(event, rule_id_data, session, message, data
for column in inspector.columns:
column_name = column.key
if column_name not in ['id', 'source_chat_id', 'target_chat_id', 'source_chat', 'target_chat',
- 'keywords', 'replace_rules', 'media_types']:
+ 'target_topic_id', 'keywords', 'replace_rules', 'media_types']:
# 获取源规则的值并设置到目标规则
value = getattr(source_rule, column_name)
setattr(target_rule, column_name, value)
diff --git a/handlers/button/settings_manager.py b/handlers/button/settings_manager.py
index 8bf56e4..5825552 100644
--- a/handlers/button/settings_manager.py
+++ b/handlers/button/settings_manager.py
@@ -445,10 +445,13 @@
async def create_settings_text(rule):
"""创建设置信息文本"""
+ topic_id = getattr(rule, 'target_topic_id', None)
+ topic_info = f" | Topic: `{topic_id}`\n" if topic_id is not None else ""
+
text = (
"📋 管理转发规则\n\n"
f"规则ID: `{rule.id}`\n"
- f"{rule.source_chat.name} --> {rule.target_chat.name}"
+ f"{rule.source_chat.name} --> {rule.target_chat.name}{topic_info}"
)
return text
@@ -652,4 +655,3 @@ async def create_buttons(rule):
return buttons
-
diff --git a/handlers/command_handlers.py b/handlers/command_handlers.py
index 0d68115..388c489 100644
--- a/handlers/command_handlers.py
+++ b/handlers/command_handlers.py
@@ -140,10 +140,20 @@ async def handle_bind_command(event, client, parts):
if not target_chat_db.current_add_id:
target_chat_db.current_add_id = str(source_chat_entity.id)
+ target_topic_id = None
+ # 只有在目标聊天是当前聊天时,才尝试提取 Topic ID
+ if not target_chat_input or str(current_chat.id) == str(target_chat_entity.id):
+ try:
+ target_topic_id = extract_topic_id(event.message)
+ except Exception as e:
+ logger.warning(f'提取目标Topic ID时出错: {str(e)}')
+ target_topic_id = None
+
# 创建转发规则
rule = ForwardRule(
source_chat_id=source_chat_db.id,
- target_chat_id=target_chat_db.id
+ target_chat_id=target_chat_db.id,
+ target_topic_id=target_topic_id
)
# 如果是绑定自己,则默认使用白名单模式
@@ -155,10 +165,12 @@ async def handle_bind_command(event, client, parts):
session.commit()
await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0)
+
+ topic_info = f" | Topic: `{target_topic_id}`\n" if target_topic_id is not None else ""
await reply_and_delete(event,
f'已设置转发规则:\n'
f'源聊天: {source_chat_db.name} ({source_chat_db.telegram_chat_id})\n'
- f'目标聊天: {target_chat_db.name} ({target_chat_db.telegram_chat_id})\n'
+ f'目标聊天: {target_chat_db.name} ({target_chat_db.telegram_chat_id}){topic_info}\n'
f'请使用 /add 或 /add_regex 添加关键字',
buttons=[Button.inline("⚙️ 打开设置", f"rule_settings:{rule.id}")]
)
@@ -170,7 +182,8 @@ async def handle_bind_command(event, client, parts):
f'已存在相同的转发规则:\n'
f'源聊天: {source_chat_db.name}\n'
f'目标聊天: {target_chat_db.name}\n'
- f'如需修改请使用 /settings 命令'
+ f'如需修改请使用 /settings 命令\n'
+ f'如需绑定到某个 Topic,请先 /delete_rule 删除该规则,然后在目标群对应 Topic 内重新 /bind\n'
)
return
finally:
@@ -835,7 +848,10 @@ async def handle_help_command(event, command):
"• 括号内为命令的简写形式\n"
"• 尖括号 <> 表示必填参数\n"
"• 方括号 [] 表示可选参数\n"
- "• 导入命令需要同时发送文件"
+ "• 导入命令需要同时发送文件\n\n"
+ "🧵 **Topics(线程)提示**\n"
+ "• 想把消息转发到某个 Topic:必须在目标群对应 Topic 内首次执行 /bind 创建规则\n"
+ "• 规则创建后暂不支持修改 Topic:请先删除规则,再在正确的 Topic 内重新 /bind"
)
await async_delete_user_message(event.client, event.message.chat_id, event.message.id, 0)
@@ -1670,7 +1686,7 @@ async def handle_copy_rule_command(event, command):
for column in inspector.columns:
column_name = column.key
if column_name not in ['id', 'source_chat_id', 'target_chat_id', 'source_chat', 'target_chat',
- 'keywords', 'replace_rules', 'media_types']:
+ 'target_topic_id', 'keywords', 'replace_rules', 'media_types']:
# 获取源规则的值并设置到目标规则
value = getattr(source_rule, column_name)
setattr(target_rule, column_name, value)
@@ -2065,12 +2081,14 @@ async def handle_list_rule_command(event, command, parts):
# 获取源聊天和目标聊天的名称
source_chat = rule.source_chat
target_chat = rule.target_chat
+ topic_id = getattr(rule, 'target_topic_id', None)
+ topic_info = f" | Topic: `{topic_id}`\n" if topic_id is not None else ""
# 构建规则描述
rule_desc = (
f'ID: {rule.id}\n'
f'
来源: {source_chat.name} ({source_chat.telegram_chat_id})\n'
- f'目标: {target_chat.name} ({target_chat.telegram_chat_id})\n'
+ f'目标: {target_chat.name} ({target_chat.telegram_chat_id}){topic_info}'
'
'
)
message_parts.append(rule_desc)
diff --git a/models/models.py b/models/models.py
index eeb1260..2efb05e 100644
--- a/models/models.py
+++ b/models/models.py
@@ -27,6 +27,7 @@ class ForwardRule(Base):
id = Column(Integer, primary_key=True)
source_chat_id = Column(Integer, ForeignKey('chats.id'), nullable=False)
target_chat_id = Column(Integer, ForeignKey('chats.id'), nullable=False)
+ target_topic_id = Column(Integer, nullable=True)
forward_mode = Column(Enum(ForwardMode), nullable=False, default=ForwardMode.BLACKLIST)
use_bot = Column(Boolean, default=True)
message_mode = Column(Enum(MessageMode), nullable=False, default=MessageMode.MARKDOWN)
@@ -365,6 +366,7 @@ def migrate_db(engine):
'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',
'enable_ai_upload_image': 'ALTER TABLE forward_rules ADD COLUMN enable_ai_upload_image BOOLEAN DEFAULT FALSE',
+ 'target_topic_id': 'ALTER TABLE forward_rules ADD COLUMN target_topic_id INTEGER DEFAULT NULL',
}
keywords_new_columns = {
@@ -481,4 +483,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("数据库初始化和迁移完成。")
diff --git a/utils/common.py b/utils/common.py
index 71eb2f5..40450b1 100644
--- a/utils/common.py
+++ b/utils/common.py
@@ -15,6 +15,22 @@
logger = logging.getLogger(__name__)
+def extract_topic_id(message):
+ rt = getattr(message, "reply_to", None)
+ if not rt:
+ return None
+
+ if not getattr(rt, "forum_topic", False):
+ return None
+
+ topic_id = getattr(rt, "reply_to_top_id", None) or getattr(rt, "reply_to_msg_id", None)
+
+ # General 话题视作主聊天
+ if topic_id in (None, 1):
+ return None
+
+ return topic_id
+
async def get_main_module():
"""获取 main 模块"""
try:
From 06b7e12ea31d0e38a20e9b8f4fa94c7e26f8c0cf Mon Sep 17 00:00:00 2001
From: RayShaw97 <152388631+RayShaw97@users.noreply.github.com>
Date: Sun, 8 Feb 2026 20:03:40 +0800
Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E7=BD=91=E7=BB=9C=E4=BB=A3=E7=90=86=EF=BC=8C=E4=BB=A5?=
=?UTF-8?q?=E4=BE=BF=E5=9C=A8=E7=BD=91=E7=BB=9C=E5=8F=97=E9=99=90=E7=9A=84?=
=?UTF-8?q?=E6=83=85=E5=86=B5=E4=B8=8B=E4=BD=BF=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env.example | 14 +++++++++++++-
main.py | 15 +++++++++++++--
requirements.txt | Bin 4158 -> 4198 bytes
3 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/.env.example b/.env.example
index fa1fef5..cd9ff7d 100644
--- a/.env.example
+++ b/.env.example
@@ -37,6 +37,18 @@ CHAT_UPDATE_TIME=03:00
# 数据库配置
DATABASE_URL=sqlite:///./db/forward.db
+######### 网络代理配置 #########
+# 代理类型 (http/socks5/socks4/none)
+PROXY_TYPE=none
+# 代理地址
+PROXY_HOST=
+# 代理端口
+PROXY_PORT=
+# 代理用户名
+PROXY_USERNAME=
+# 代理密码
+PROXY_PASSWORD=
+
######### UI 布局配置 #########
AI_MODELS_PER_PAGE=10
KEYWORDS_PER_PAGE=10
@@ -65,7 +77,7 @@ MEDIA_EXTENSIONS_COLS=6
# 每页显示的规则数量
RULES_PER_PAGE=20
- ######### AI设置 #########
+######### AI设置 #########
# 默认AI模型
DEFAULT_AI_MODEL=gemini-2.0-flash
diff --git a/main.py b/main.py
index b3bf10f..0d519e1 100644
--- a/main.py
+++ b/main.py
@@ -59,10 +59,21 @@ def clear_temp_dir():
for file in os.listdir('./temp'):
os.remove(os.path.join('./temp', file))
+proxy = None
+proxy_type = os.getenv('PROXY_TYPE')
+if proxy_type and proxy_type.lower() != 'none':
+ proxy_host = os.getenv('PROXY_HOST')
+ proxy_port = os.getenv('PROXY_PORT')
+ if not proxy_host or not proxy_port:
+ logger.error("代理配置不完整,缺少 PROXY_HOST 或 PROXY_PORT")
+ raise ValueError("代理配置不完整,缺少 PROXY_HOST 或 PROXY_PORT")
+ proxy_username = os.getenv('PROXY_USERNAME', '')
+ proxy_password = os.getenv('PROXY_PASSWORD', '')
+ proxy = (proxy_type, proxy_host, int(proxy_port), True, proxy_username, proxy_password)
# 创建客户端
-user_client = TelegramClient('./sessions/user', api_id, api_hash)
-bot_client = TelegramClient('./sessions/bot', api_id, api_hash)
+user_client = TelegramClient('./sessions/user', api_id, api_hash, proxy=proxy)
+bot_client = TelegramClient('./sessions/bot', api_id, api_hash, proxy=proxy)
# 初始化数据库
engine = init_db()
diff --git a/requirements.txt b/requirements.txt
index a62e1c29aaef7529d2824a649dd2a7aa1eddfb81..466638058074f2c83befe6b4a538dd068c1b7c74 100644
GIT binary patch
delta 40
rcmdm|@JwOD8gA(fhJ1!R23>|?Af3#R4Ww;>(1<~g!D91#?)fYL=2Z#~
delta 12
UcmaE+uuoyb8t%
Date: Mon, 9 Feb 2026 10:38:31 +0800
Subject: [PATCH 03/10] feat: add GitHub Actions workflow for publishing Docker
images
---
.github/workflows/publish-image.yml | 58 +++++++++++++++++++++++++++++
1 file changed, 58 insertions(+)
create mode 100644 .github/workflows/publish-image.yml
diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml
new file mode 100644
index 0000000..d27d389
--- /dev/null
+++ b/.github/workflows/publish-image.yml
@@ -0,0 +1,58 @@
+name: Publish Image
+
+on:
+ push:
+ # branches: [ "main" ]
+ tags: [ "v*.*.*" ]
+
+jobs:
+ docker:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Login to GHCR
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels)
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ # latest 的默认行为是 auto;会在 semver/tag 等场景自动生成 latest
+ flavor: |
+ latest=auto
+
+ # ✅ Git tag = v1.2.3 时,生成 1.2.3 / 1.2 / 1(注意:{{version}} 会去掉前缀 v)
+ # ✅ push 到 main 时也打 latest(推荐写法:用 is_default_branch 判断)
+ # ✅ 每次都生成提交 SHA tag(默认形如 sha-860c190)
+ tags: |
+ type=raw,value=latest,enable={{is_default_branch}}
+
+ type=semver,pattern={{version}} # 1.2.3
+ type=semver,pattern={{major}}.{{minor}} # 1.2
+ type=semver,pattern={{major}} # 1
+
+ type=sha,format=short
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
\ No newline at end of file
From cf19c631b986f0616ec5d9fae8d0798fe376f2f7 Mon Sep 17 00:00:00 2001
From: RayShaw97 <152388631+RayShaw97@users.noreply.github.com>
Date: Mon, 9 Feb 2026 10:44:55 +0800
Subject: [PATCH 04/10] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B5=8B?=
=?UTF-8?q?=E8=AF=95=E5=88=86=E6=94=AF=E4=B8=8D=E8=A7=A6=E5=8F=91action?=
=?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/publish-image.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml
index d27d389..e697ae7 100644
--- a/.github/workflows/publish-image.yml
+++ b/.github/workflows/publish-image.yml
@@ -2,7 +2,7 @@ name: Publish Image
on:
push:
- # branches: [ "main" ]
+ branches: [ "main", "ghcr" ]
tags: [ "v*.*.*" ]
jobs:
From 02c75ae329ec10e72117e2f671bee74926f91ac2 Mon Sep 17 00:00:00 2001
From: RayShaw97 <152388631+RayShaw97@users.noreply.github.com>
Date: Mon, 9 Feb 2026 10:54:57 +0800
Subject: [PATCH 05/10] =?UTF-8?q?fix:=20=E8=A1=A5=E5=85=A8=E7=8E=AF?=
=?UTF-8?q?=E5=A2=83=E5=8F=98=E9=87=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/publish-image.yml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml
index e697ae7..9b1629e 100644
--- a/.github/workflows/publish-image.yml
+++ b/.github/workflows/publish-image.yml
@@ -5,6 +5,10 @@ on:
branches: [ "main", "ghcr" ]
tags: [ "v*.*.*" ]
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
+
jobs:
docker:
runs-on: ubuntu-latest
From 499240ddc39de6483ea86e52144b7a875b073f36 Mon Sep 17 00:00:00 2001
From: RayShaw97 <152388631+RayShaw97@users.noreply.github.com>
Date: Mon, 9 Feb 2026 14:14:38 +0800
Subject: [PATCH 06/10] =?UTF-8?q?feat:=20Github=20Actions=20=E5=8A=A0?=
=?UTF-8?q?=E9=80=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.dockerignore | 1 +
.github/workflows/publish-image.yml | 120 +++++++++++++++++++++++++---
Dockerfile | 33 ++++----
3 files changed, 124 insertions(+), 30 deletions(-)
diff --git a/.dockerignore b/.dockerignore
index 4bc78bf..7f849b4 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -15,6 +15,7 @@ db/forward.db1
**/venv/
**/env/
**/ENV/
+**/.venv/
# 忽略 Telethon 会话文件
*.session
diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml
index 9b1629e..eb2de60 100644
--- a/.github/workflows/publish-image.yml
+++ b/.github/workflows/publish-image.yml
@@ -2,19 +2,30 @@ name: Publish Image
on:
push:
- branches: [ "main", "ghcr" ]
- tags: [ "v*.*.*" ]
+ branches: ["main", "ghcr"]
+ tags: ["v*.*.*"]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
- docker:
- runs-on: ubuntu-latest
+ build:
+ name: Build (${{ matrix.arch }})
+ runs-on: ${{ matrix.runs_on }}
permissions:
contents: read
packages: write
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platform: linux/amd64
+ arch: amd64
+ runs_on: ubuntu-latest
+ - platform: linux/arm64
+ arch: arm64
+ runs_on: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v5
@@ -34,7 +45,7 @@ jobs:
# latest 的默认行为是 auto;会在 semver/tag 等场景自动生成 latest
flavor: |
latest=auto
-
+
# ✅ Git tag = v1.2.3 时,生成 1.2.3 / 1.2 / 1(注意:{{version}} 会去掉前缀 v)
# ✅ push 到 main 时也打 latest(推荐写法:用 is_default_branch 判断)
# ✅ 每次都生成提交 SHA tag(默认形如 sha-860c190)
@@ -47,16 +58,99 @@ jobs:
type=sha,format=short
- - name: Set up QEMU
- uses: docker/setup-qemu-action@v3
-
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- - name: Build and push
+ - name: Build and push by digest
+ id: build
uses: docker/build-push-action@v6
with:
- platforms: linux/amd64,linux/arm64
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
\ No newline at end of file
+ platforms: ${{ matrix.platform }}
+ labels: ${{ steps.meta.outputs.labels }}
+ outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
+ cache-from: type=gha,scope=${{ matrix.arch }}
+ cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
+
+ - name: Export digest
+ run: |
+ mkdir -p digests
+ echo "${{ steps.build.outputs.digest }}" > "digests/${{ matrix.arch }}.digest"
+
+ - name: Upload digest
+ uses: actions/upload-artifact@v4
+ with:
+ name: digests-${{ matrix.arch }}
+ path: digests/${{ matrix.arch }}.digest
+ if-no-files-found: error
+ retention-days: 1
+
+ merge:
+ name: Merge manifest
+ runs-on: ubuntu-latest
+ needs: build
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - name: Download digests
+ uses: actions/download-artifact@v4
+ with:
+ pattern: digests-*
+ path: digests
+ merge-multiple: true
+
+ - name: Login to GHCR
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Extract metadata (tags, labels)
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ # latest 的默认行为是 auto;会在 semver/tag 等场景自动生成 latest
+ flavor: |
+ latest=auto
+
+ # ✅ Git tag = v1.2.3 时,生成 1.2.3 / 1.2 / 1(注意:{{version}} 会去掉前缀 v)
+ # ✅ push 到 main 时也打 latest(推荐写法:用 is_default_branch 判断)
+ # ✅ 每次都生成提交 SHA tag(默认形如 sha-860c190)
+ tags: |
+ type=raw,value=latest,enable={{is_default_branch}}
+
+ type=semver,pattern={{version}} # 1.2.3
+ type=semver,pattern={{major}}.{{minor}} # 1.2
+ type=semver,pattern={{major}} # 1
+
+ type=sha,format=short
+
+ - name: Create multi-arch manifest and push tags
+ env:
+ TAGS: ${{ steps.meta.outputs.tags }}
+ run: |
+ set -euo pipefail
+
+ IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
+
+ refs=()
+ for f in digests/*.digest; do
+ digest="$(cat "$f")"
+ refs+=("${IMAGE}@${digest}")
+ done
+
+ tag_args=()
+ while IFS= read -r tag; do
+ [[ -n "$tag" ]] || continue
+ tag_args+=("-t" "$tag")
+ done <<< "$TAGS"
+
+ docker buildx imagetools create "${tag_args[@]}" "${refs[@]}"
+
+ first_tag="$(printf '%s\n' "$TAGS" | sed -n '1p')"
+ docker buildx imagetools inspect "$first_tag"
diff --git a/Dockerfile b/Dockerfile
index 977292f..a4ecbb8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,34 +1,33 @@
+FROM python:3.11-slim AS builder
+
+WORKDIR /build
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ gcc \
+ python3-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY requirements.txt .
+RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
+
FROM python:3.11-slim
-# 设置工作目录
WORKDIR /app
-# 设置Docker日志配置
-ENV DOCKER_LOG_MAX_SIZE=10m
-ENV DOCKER_LOG_MAX_FILE=3
+ENV DOCKER_LOG_MAX_SIZE=10m DOCKER_LOG_MAX_FILE=3
-# 安装系统依赖
-RUN apt-get update && apt-get install -y \
+RUN apt-get update && apt-get install -y --no-install-recommends \
tzdata \
&& ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& dpkg-reconfigure -f noninteractive tzdata \
- && apt-get install -y \
- gcc \
- python3-dev \
&& rm -rf /var/lib/apt/lists/*
-# 复制依赖文件并安装
-COPY requirements.txt .
-RUN pip install --no-cache-dir -r requirements.txt
+COPY --from=builder /install /usr/local
-# 创建临时文件目录
RUN mkdir -p /app/temp
-# 复制应用代码
COPY . .
-# 设置环境变量
ENV PYTHONUNBUFFERED=1
-# 启动命令
-CMD ["python", "main.py"]
\ No newline at end of file
+CMD ["python", "main.py"]
From ed6fbd663ca05f6b2e0023e6f9e806267640512d Mon Sep 17 00:00:00 2001
From: RayShaw97 <152388631+RayShaw97@users.noreply.github.com>
Date: Mon, 9 Feb 2026 14:45:01 +0800
Subject: [PATCH 07/10] fix: update image name normalization in GitHub Actions
workflow
---
.github/workflows/publish-image.yml | 20 ++++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml
index eb2de60..3d56b28 100644
--- a/.github/workflows/publish-image.yml
+++ b/.github/workflows/publish-image.yml
@@ -30,6 +30,12 @@ jobs:
- name: Checkout
uses: actions/checkout@v5
+ - name: Normalize image name
+ id: vars
+ shell: bash
+ run: |
+ echo "image=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
+
- name: Login to GHCR
uses: docker/login-action@v3
with:
@@ -41,7 +47,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ images: ${{ steps.vars.outputs.image }}
# latest 的默认行为是 auto;会在 semver/tag 等场景自动生成 latest
flavor: |
latest=auto
@@ -67,7 +73,7 @@ jobs:
with:
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
- outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true
+ outputs: type=image,name=${{ steps.vars.outputs.image }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
@@ -99,6 +105,12 @@ jobs:
path: digests
merge-multiple: true
+ - name: Normalize image name
+ id: vars
+ shell: bash
+ run: |
+ echo "image=${REGISTRY}/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
+
- name: Login to GHCR
uses: docker/login-action@v3
with:
@@ -113,7 +125,7 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ images: ${{ steps.vars.outputs.image }}
# latest 的默认行为是 auto;会在 semver/tag 等场景自动生成 latest
flavor: |
latest=auto
@@ -136,7 +148,7 @@ jobs:
run: |
set -euo pipefail
- IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
+ IMAGE="${{ steps.vars.outputs.image }}"
refs=()
for f in digests/*.digest; do
From 69809944d2680956225919b44125b980b137af5c Mon Sep 17 00:00:00 2001
From: RayShaw97 <152388631+RayShaw97@users.noreply.github.com>
Date: Mon, 9 Feb 2026 17:48:51 +0800
Subject: [PATCH 08/10] =?UTF-8?q?fix:=20=E9=81=BF=E5=85=8D=E7=94=9F?=
=?UTF-8?q?=E6=88=90=20unknown/unknown?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/publish-image.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml
index 3d56b28..a16b0ce 100644
--- a/.github/workflows/publish-image.yml
+++ b/.github/workflows/publish-image.yml
@@ -76,6 +76,8 @@ jobs:
outputs: type=image,name=${{ steps.vars.outputs.image }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=gha,scope=${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=${{ matrix.arch }}
+ provenance: false
+ sbom: false
- name: Export digest
run: |
From 68ffc85d2036ad15c031f17e71f83c0f210bf805 Mon Sep 17 00:00:00 2001
From: RayShaw97 <152388631+RayShaw97@users.noreply.github.com>
Date: Sat, 21 Feb 2026 21:23:08 +0800
Subject: [PATCH 09/10] =?UTF-8?q?feat:=20=E5=AA=92=E4=BD=93=E7=B1=BB?=
=?UTF-8?q?=E5=9E=8B=E8=BF=87=E6=BB=A4=E5=8A=9F=E8=83=BD=E5=A2=9E=E5=8A=A0?=
=?UTF-8?q?"=E8=B4=B4=E7=BA=B8"=E7=B1=BB=E5=88=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
filters/media_filter.py | 15 +++++++++++++--
handlers/button/button_helpers.py | 3 ++-
handlers/button/callback/media_callback.py | 2 +-
models/db_operations.py | 7 ++++---
models/models.py | 15 +++++++++++++++
5 files changed, 35 insertions(+), 7 deletions(-)
diff --git a/filters/media_filter.py b/filters/media_filter.py
index 2dae0ba..201ea81 100644
--- a/filters/media_filter.py
+++ b/filters/media_filter.py
@@ -1,6 +1,7 @@
import logging
import os
import asyncio
+from telethon.tl.types import DocumentAttributeSticker
from utils.media import get_media_size
from utils.constants import TEMP_DIR
from filters.base_filter import BaseFilter
@@ -252,7 +253,14 @@ async def _process_single_media(self, context):
# 记录这是纯链接预览消息
context.is_pure_link_preview = True
logger.info('这是一条纯链接预览消息')
-
+
+ def _is_sticker(self, media):
+ doc = getattr(media, 'document', None)
+ attributes = getattr(doc, 'attributes', None) if doc else None
+ if not attributes:
+ return False
+ return any(isinstance(attr, DocumentAttributeSticker) for attr in attributes)
+
async def _is_media_type_blocked(self, media, media_types):
"""
检查媒体类型是否被屏蔽
@@ -268,6 +276,10 @@ async def _is_media_type_blocked(self, media, media_types):
if getattr(media, 'photo', None) and media_types.photo:
logger.info('媒体类型为图片,已被屏蔽')
return True
+
+ if getattr(media, 'document', None) and self._is_sticker(media) and getattr(media_types, 'sticker', False):
+ logger.info('媒体类型为贴纸,已被屏蔽')
+ return True
if getattr(media, 'document', None) and media_types.document:
logger.info('媒体类型为文档,已被屏蔽')
@@ -360,4 +372,3 @@ async def _is_media_extension_allowed(self, rule, media):
session.close()
return allowed
-
diff --git a/handlers/button/button_helpers.py b/handlers/button/button_helpers.py
index 2f89a25..ddb7deb 100644
--- a/handlers/button/button_helpers.py
+++ b/handlers/button/button_helpers.py
@@ -404,7 +404,8 @@ async def create_media_types_buttons(rule_id, media_types):
'document': '📄 文档',
'video': '🎬 视频',
'audio': '🎵 音频',
- 'voice': '🎤 语音'
+ 'voice': '🎤 语音',
+ 'sticker': '🎭 贴纸'
}
for field, display_name in media_type_names.items():
diff --git a/handlers/button/callback/media_callback.py b/handlers/button/callback/media_callback.py
index e3f638d..ab84e4c 100644
--- a/handlers/button/callback/media_callback.py
+++ b/handlers/button/callback/media_callback.py
@@ -141,7 +141,7 @@ async def callback_toggle_media_type(event, rule_id, session, message, data):
rule_id = parts[1]
media_type = parts[2]
# 检查媒体类型是否有效
- if media_type not in ['photo', 'document', 'video', 'audio', 'voice']:
+ if media_type not in ['photo', 'document', 'video', 'audio', 'voice', 'sticker']:
await event.answer(f"无效的媒体类型: {media_type}")
return
diff --git a/models/db_operations.py b/models/db_operations.py
index 5ed62da..25be206 100644
--- a/models/db_operations.py
+++ b/models/db_operations.py
@@ -631,7 +631,8 @@ async def get_media_types(self, session, rule_id):
document=False,
video=False,
audio=False,
- voice=False
+ voice=False,
+ sticker=False
)
session.add(media_types)
session.commit()
@@ -655,7 +656,7 @@ async def update_media_types(self, session, rule_id, media_types_dict):
session.add(media_types)
# 更新媒体类型设置
- for field in ['photo', 'document', 'video', 'audio', 'voice']:
+ for field in ['photo', 'document', 'video', 'audio', 'voice', 'sticker']:
if field in media_types_dict:
setattr(media_types, field, media_types_dict[field])
@@ -669,7 +670,7 @@ async def update_media_types(self, session, rule_id, media_types_dict):
async def toggle_media_type(self, session, rule_id, media_type):
"""切换特定媒体类型的启用状态"""
try:
- if media_type not in ['photo', 'document', 'video', 'audio', 'voice']:
+ if media_type not in ['photo', 'document', 'video', 'audio', 'voice', 'sticker']:
return False, f"无效的媒体类型: {media_type}"
success, msg, media_types = await self.get_media_types(session, rule_id)
diff --git a/models/models.py b/models/models.py
index 2efb05e..3759559 100644
--- a/models/models.py
+++ b/models/models.py
@@ -137,6 +137,7 @@ class MediaTypes(Base):
video = Column(Boolean, default=False)
audio = Column(Boolean, default=False)
voice = Column(Boolean, default=False)
+ sticker = Column(Boolean, default=False)
# 关系
rule = relationship('ForwardRule', back_populates='media_types')
@@ -394,6 +395,20 @@ def migrate_db(engine):
except Exception as e:
logging.error(f'添加列 {column} 时出错: {str(e)}')
+ # 添加media_types表的列(与 forward_rules/keywords 补列同一段)
+ if 'media_types' in existing_tables:
+ media_types_columns = {column['name'] for column in inspector.get_columns('media_types')}
+ media_types_new_columns = {
+ 'sticker': 'ALTER TABLE media_types ADD COLUMN sticker BOOLEAN DEFAULT FALSE',
+ }
+ for column, sql in media_types_new_columns.items():
+ if column not in media_types_columns:
+ try:
+ connection.execute(text(sql))
+ logging.info(f'已添加media_types.{column}列')
+ except Exception as e:
+ logging.error(f'添加media_types.{column}列时出错: {str(e)}')
+
#先检查forward_rules表的列的forward_mode是否存在
if 'forward_mode' not in forward_rules_columns:
# 修改forward_rules表的列mode为forward_mode
From f771bcda63c9a1d7cfb85b8ad08a4e8832a238fe Mon Sep 17 00:00:00 2001
From: RayShaw97 <152388631+RayShaw97@users.noreply.github.com>
Date: Fri, 5 Jun 2026 16:18:53 +0800
Subject: [PATCH 10/10] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=AB=8B?=
=?UTF-8?q?=E5=8D=B3=E6=80=BB=E7=BB=93=E5=9B=9E=E6=BA=AF=E8=8C=83=E5=9B=B4?=
=?UTF-8?q?=E9=80=89=E6=8B=A9=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
handlers/button/button_helpers.py | 23 +++++
handlers/button/callback/ai_callback.py | 85 ++++++++++++++++---
handlers/button/callback/callback_handlers.py | 1 +
scheduler/summary_scheduler.py | 23 ++---
4 files changed, 110 insertions(+), 22 deletions(-)
diff --git a/handlers/button/button_helpers.py b/handlers/button/button_helpers.py
index ddb7deb..25a8f3e 100644
--- a/handlers/button/button_helpers.py
+++ b/handlers/button/button_helpers.py
@@ -12,6 +12,14 @@
DELAY_TIMES = load_delay_times()
MEDIA_SIZE = load_max_media_size()
MEDIA_EXTENSIONS = load_media_extensions()
+SUMMARY_NOW_RANGES = [
+ ("按总结时间", "schedule", None),
+ ("1小时", "1h", 1),
+ ("2小时", "2h", 2),
+ ("4小时", "4h", 4),
+ ("6小时", "6h", 6),
+ ("12小时", "12h", 12),
+]
async def create_ai_settings_buttons(rule=None,rule_id=None):
"""创建 AI 设置按钮"""
buttons = []
@@ -50,6 +58,21 @@ async def create_ai_settings_buttons(rule=None,rule_id=None):
return buttons
+
+async def create_summary_now_range_buttons(rule_id):
+ """创建立即总结回溯范围选择按钮"""
+ buttons = []
+ for label, value, _ in SUMMARY_NOW_RANGES:
+ buttons.append([Button.inline(label, f"summary_now_range:{rule_id}:{value}")])
+
+ buttons.append([
+ Button.inline('👈 返回', f"ai_settings:{rule_id}"),
+ Button.inline('❌ 关闭', "close_settings")
+ ])
+
+ return buttons
+
+
async def create_media_settings_buttons(rule=None,rule_id=None):
"""创建媒体设置按钮"""
buttons = []
diff --git a/handlers/button/callback/ai_callback.py b/handlers/button/callback/ai_callback.py
index 04b73eb..6422498 100644
--- a/handlers/button/callback/ai_callback.py
+++ b/handlers/button/callback/ai_callback.py
@@ -4,7 +4,13 @@
import asyncio
from telethon.tl import types
-from handlers.button.button_helpers import create_ai_settings_buttons, create_model_buttons, create_summary_time_buttons
+from handlers.button.button_helpers import (
+ SUMMARY_NOW_RANGES,
+ create_ai_settings_buttons,
+ create_model_buttons,
+ create_summary_now_range_buttons,
+ create_summary_time_buttons,
+)
from models.models import ForwardRule, RuleSync
from telethon import Button
import logging
@@ -16,6 +22,13 @@
logger = logging.getLogger(__name__)
+def _get_summary_now_range(range_value):
+ for label, value, hours in SUMMARY_NOW_RANGES:
+ if value == range_value:
+ return label, hours
+ return None
+
+
async def callback_ai_settings(event, rule_id, session, message, data):
# 显示 AI 设置页面
try:
@@ -334,38 +347,86 @@ async def callback_cancel_set_summary(event, rule_id, session, message, data):
return
async def callback_summary_now(event, rule_id, session, message, data):
- # 处理立即执行总结的回调
- logger.info(f"处理立即执行总结回调 - rule_id: {rule_id}")
-
+ # 处理立即执行总结的回溯范围选择
+ logger.info(f"处理立即执行总结范围选择回调 - rule_id: {rule_id}")
+
try:
rule = session.query(ForwardRule).get(int(rule_id))
if not rule:
await event.answer("规则不存在")
return
-
+
+ await event.edit(
+ "请选择立即总结的回溯范围:",
+ buttons=await create_summary_now_range_buttons(rule_id)
+ )
+ except Exception as e:
+ logger.error(f"处理立即总结范围选择时出错: {str(e)}")
+ logger.error(traceback.format_exc())
+ await event.answer(f"处理时出错: {str(e)}")
+ finally:
+ session.close()
+
+ return
+
+
+async def callback_summary_now_range(event, rule_id, session, message, data):
+ # 按选择的回溯范围立即执行总结
+ logger.info(f"处理立即执行总结范围回调 - data: {data}")
+
+ try:
+ parts = data.split(':', 2)
+ if len(parts) != 3:
+ await event.answer("回调数据格式错误")
+ return
+
+ _, rule_id_part, range_value = parts
+ range_option = _get_summary_now_range(range_value)
+ if not range_option:
+ await event.answer("未知的总结范围")
+ return
+
+ range_label, lookback_hours = range_option
+ rule = session.query(ForwardRule).get(int(rule_id_part))
+ if not rule:
+ await event.answer("规则不存在")
+ return
+
main = await get_main_module()
user_client = main.user_client
bot_client = main.bot_client
scheduler = SummaryScheduler(user_client, bot_client)
await event.answer("开始执行总结,请稍候...")
-
+
+ range_text = (
+ "按总结时间生成总结"
+ if lookback_hours is None
+ else f"生成最近 {lookback_hours} 小时总结"
+ )
+
await message.edit(
- f"正在为规则 {rule_id}({rule.source_chat.name} -> {rule.target_chat.name})生成总结...\n"
+ f"正在为规则 {rule_id_part}({rule.source_chat.name} -> {rule.target_chat.name}){range_text}...\n"
f"处理需要一定时间,请耐心等待。",
- buttons=[[Button.inline("返回", f"ai_settings:{rule_id}")]]
+ buttons=[[Button.inline("返回", f"ai_settings:{rule_id_part}")]]
)
-
+
try:
# 执行总结任务
- await asyncio.create_task(scheduler._execute_summary(rule.id,is_now=True))
- logger.info(f"已启动规则 {rule_id} 的立即总结任务")
+ await asyncio.create_task(
+ scheduler._execute_summary(
+ rule.id,
+ is_now=True,
+ lookback_hours=lookback_hours
+ )
+ )
+ logger.info(f"已启动规则 {rule_id_part} 的立即总结任务,范围: {range_label}")
except Exception as e:
logger.error(f"执行总结任务失败: {str(e)}")
logger.error(traceback.format_exc())
await message.edit(
f"总结生成失败: {str(e)}",
- buttons=[[Button.inline("返回", f"ai_settings:{rule_id}")]]
+ buttons=[[Button.inline("返回", f"ai_settings:{rule_id_part}")]]
)
except Exception as e:
logger.error(f"处理总结时出错: {str(e)}")
diff --git a/handlers/button/callback/callback_handlers.py b/handlers/button/callback/callback_handlers.py
index 67db1bd..008c159 100644
--- a/handlers/button/callback/callback_handlers.py
+++ b/handlers/button/callback/callback_handlers.py
@@ -654,6 +654,7 @@ async def handle_callback(event):
'cancel_set_prompt': callback_cancel_set_prompt,
'cancel_set_summary': callback_cancel_set_summary,
'summary_now':callback_summary_now,
+ 'summary_now_range': callback_summary_now_range,
# 媒体设置
'select_max_media_size': callback_select_max_media_size,
'set_max_media_size': callback_set_max_media_size,
diff --git a/scheduler/summary_scheduler.py b/scheduler/summary_scheduler.py
index 4b10edf..2c1066d 100644
--- a/scheduler/summary_scheduler.py
+++ b/scheduler/summary_scheduler.py
@@ -123,7 +123,7 @@ def _get_next_run_time(self, now, target_time):
return next_time
- async def _execute_summary(self, rule_id, is_now=False):
+ async def _execute_summary(self, rule_id, is_now=False, lookback_hours=None):
"""执行单个规则的总结任务"""
session = get_session()
try:
@@ -140,18 +140,21 @@ async def _execute_summary(self, rule_id, is_now=False):
# 计算时间范围
now = datetime.now(self.timezone)
- summary_hour, summary_minute = map(int, rule.summary_time.split(':'))
-
# 设置结束时间为当前时间
end_time = now
- # 设置开始时间为前一天的总结时间
- start_time = now.replace(
- hour=summary_hour,
- minute=summary_minute,
- second=0,
- microsecond=0
- ) - timedelta(days=1)
+ if lookback_hours is not None:
+ start_time = now - timedelta(hours=lookback_hours)
+ else:
+ summary_hour, summary_minute = map(int, rule.summary_time.split(':'))
+
+ # 设置开始时间为前一天的总结时间
+ start_time = now.replace(
+ hour=summary_hour,
+ minute=summary_minute,
+ second=0,
+ microsecond=0
+ ) - timedelta(days=1)
logger.info(f'规则 {rule_id} 获取消息时间范围: {start_time} 到 {end_time}')