mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 12:56:34 +08:00
fix(telegram): preserve notification hierarchy and render detail links
This commit is contained in:
@@ -26,9 +26,21 @@ def render_notification(title: Optional[str], blocks: List[Dict[str, Optional[st
|
||||
paragraphs = []
|
||||
if title and text_format == "rich_markdown":
|
||||
paragraphs.append(f"## {_escape_notification_text(title, text_format)}")
|
||||
in_section = False
|
||||
for block in blocks:
|
||||
lines = []
|
||||
block_title, block_text = block.get("title"), block.get("text")
|
||||
if text_format == "rich_markdown" and block_title:
|
||||
heading = _escape_notification_text(block_title, text_format)
|
||||
if not block_text:
|
||||
paragraphs.append(f"### {heading}")
|
||||
in_section = True
|
||||
continue
|
||||
if in_section:
|
||||
# 分节之后的标题/正文对是列表项,硬换行让续行仍缩进在当前条目内。
|
||||
body = _escape_notification_text(block_text, text_format).replace("\n", " \n ")
|
||||
paragraphs.append(f"- **{heading}** \n {body}")
|
||||
continue
|
||||
if block_title:
|
||||
lines.append(f"{bold}{_escape_notification_text(block_title, text_format)}{bold}")
|
||||
if block_text:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import html
|
||||
import json
|
||||
import re
|
||||
from typing import Any, List, Optional, Tuple, Union
|
||||
from urllib.parse import quote
|
||||
|
||||
from app.application.messaging.channel.admin import (
|
||||
matches_channel_admin,
|
||||
@@ -463,8 +463,9 @@ class TelegramModule(_MessageChannelModuleBase[Telegram]):
|
||||
if not rich_text:
|
||||
return message.rich_message
|
||||
if message.link:
|
||||
link = html.escape(message.link, quote=True)
|
||||
rich_text = f'{rich_text}\n\n<a href="{link}">查看详情</a>'
|
||||
# 输入仍是 Markdown,HTML 标签会被 richify 当作普通文本转义。
|
||||
link = quote(message.link, safe=":/?#&=%")
|
||||
rich_text = f'{rich_text}\n\n[查看详情](<{link}>)'
|
||||
return rich_text
|
||||
|
||||
def post_message(self, message: Message, **kwargs) -> None:
|
||||
|
||||
@@ -229,7 +229,7 @@ class IncomingMessage(BaseModel):
|
||||
|
||||
@_dataclass
|
||||
class NotificationBlock:
|
||||
"""一段与渠道无关的通知内容,标题和正文均是未插入格式标记的普通文本。"""
|
||||
"""通知段落,内容均为普通文本;单独标题开启分节,节中的标题/正文对表示条目。"""
|
||||
|
||||
title: Optional[str] = None
|
||||
text: Optional[str] = None
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
`text` 保留完整纯文本结果;`rich_content` 使用 `NotificationContent`,只包含标题和有序
|
||||
`NotificationBlock` 列表,每段的 `title`、`text` 都是普通文本。插件不枚举渠道、不插入
|
||||
Markdown 或 HTML 标签,也不判断渠道权限。
|
||||
独立标题表示分节;分节之后的标题和正文对表示条目。Telegram 转为原生分节标题与缩进列表,
|
||||
同一条目内的续行保留缩进。详情入口使用 Markdown 链接交给真实转换器,避免 HTML 标签被原样展示。
|
||||
|
||||
宿主通过 `ChannelCapabilityManager.get_notification_text_format()` 查询普通通知的投递能力,
|
||||
按 `ChannelCapabilities.notification_text_format` 选择语法和转义方式。未声明该能力的渠道
|
||||
@@ -31,10 +33,11 @@ WebAgent 能渲染 Markdown,但通知中心仍记录普通文本;ClawBot 当
|
||||
Telegram 原有的 `rich_message` 用于完整 Rich Markdown 正文,未提供 `rich_content` 时
|
||||
沿用原来的行为。渠道 API 错误不因一次临时网络错误自动重发另一份通知。
|
||||
|
||||
站点自动签到只负责统计、结果分组和重试标记,结构化内容包括本次全部站点。
|
||||
V3 插件版本为 3.0.0,显式声明 `system_version >=3.1.6`,提供默认开启的
|
||||
站点自动签到只负责统计、结果分组和重试标记;同一状态内按完整原因归并,原因只展示一次,
|
||||
对应名单保留全部站点。不同原因在缩短显示之前区分,避免截断后相同的前缀被误合并。
|
||||
V3 插件显式声明 `system_version >=3.1.6`,提供默认开启的
|
||||
“优化消息通知样式”开关;关闭后不传结构化内容,所有渠道恢复原有通知样式。
|
||||
原 V2 版本 2.9.13 保持源码及版本不变,在市场索引声明 `v3: true` 和 `system_version <=3.1.5`,
|
||||
V2 兼容实现保持独立版本,在市场索引声明 `v3: true` 和 `system_version <=3.1.5`,
|
||||
保留低于 3.1.6 的 V3 主程序选择 V2 兼容实现的安装路径。
|
||||
具体说明见插件仓 `plugins.v3/autosignin/README.md`。
|
||||
|
||||
|
||||
@@ -290,16 +290,55 @@ def test_channel_notification_format_obeys_capabilities_and_instance_mode(monkey
|
||||
|
||||
|
||||
def test_telegram_channel_summary_includes_detail_link_without_changing_legacy_rich_message():
|
||||
"""专用摘要能访问完整结果,原有 Rich Message 不被添加内容。"""
|
||||
"""详情链接经过真实 Markdown 转换仍是链接,原有 Rich Message 不被添加内容。"""
|
||||
from app.modules.telegram.telegram import Telegram
|
||||
|
||||
message = _message()
|
||||
message.link = 'https://example.com/?a=1&b="value"'
|
||||
rich = TelegramModule._notification_rich_message(message)
|
||||
assert '<a href="https://example.com/?a=1&b="value"">查看详情</a>' in rich
|
||||
converted = "".join(chunk["html"] for chunk in Telegram._build_rich_message_chunks(rich))
|
||||
assert '<a href="https://example.com/?a=1&b=%22value%22">查看详情</a>' in converted
|
||||
assert "<a href" not in converted
|
||||
message.rich_content = None
|
||||
message.rich_message = "原有完整 Rich Message"
|
||||
assert TelegramModule._notification_rich_message(message) == message.rich_message
|
||||
|
||||
|
||||
def test_telegram_notification_sections_have_native_list_indentation():
|
||||
"""真实转换产物应包含分节标题和缩进列表,而非全是同级普通段落。"""
|
||||
from app.modules.telegram.telegram import Telegram
|
||||
|
||||
message = Message(rich_content=NotificationContent(title="签到结果", blocks=[
|
||||
NotificationBlock(title="统计", text="本次 47"),
|
||||
NotificationBlock(title="失败 · 9"),
|
||||
NotificationBlock(title="Cookie已失效", text="站点一、站点二"),
|
||||
NotificationBlock(title="网络失败", text="站点三"),
|
||||
NotificationBlock(title="成功 · 28"),
|
||||
NotificationBlock(title="签到成功", text="站点四、站点五"),
|
||||
]))
|
||||
body = "".join(chunk["html"] for chunk in Telegram._build_rich_message_chunks(
|
||||
TelegramModule._notification_rich_message(message)))
|
||||
assert "<h3>失败 · 9</h3>" in body
|
||||
assert "<h3>成功 · 28</h3>" in body
|
||||
assert body.count("<li>") == 3
|
||||
assert "<b>Cookie已失效</b><br/>站点一、站点二" in body
|
||||
assert "<b>签到成功</b><br/>站点四、站点五" in body
|
||||
|
||||
|
||||
def test_telegram_long_section_list_splits_without_losing_items():
|
||||
"""原因很多时由真实转换器拆分长列表,各组和对应名单仍完整保留。"""
|
||||
from app.modules.telegram.telegram import Telegram
|
||||
|
||||
blocks = [NotificationBlock(title="访问失败 · 150")]
|
||||
blocks.extend(NotificationBlock(title=f"失败原因{index}:" + "网络连接失败" * 20,
|
||||
text=f"完整站点{index:03d}") for index in range(150))
|
||||
message = Message(rich_content=NotificationContent(title="签到结果", blocks=blocks))
|
||||
chunks = Telegram._build_rich_message_chunks(TelegramModule._notification_rich_message(message))
|
||||
assert len(chunks) > 1
|
||||
body = "".join(chunk["html"] for chunk in chunks)
|
||||
assert all(body.count(f"完整站点{index:03d}") == 1 for index in range(150))
|
||||
|
||||
|
||||
def test_host_renders_raw_data_safely_for_every_declared_format():
|
||||
"""同一份结构化数据按能力生成语法,站点文本不能注入格式、链接或提醒。"""
|
||||
from app.schemas.notification import NotificationTextFormat
|
||||
|
||||
Reference in New Issue
Block a user