fix(types): type messaging template and queue boundaries

This commit is contained in:
jxxghp
2026-10-17 18:23:57 +08:00
parent 999906e503
commit c1d7deada8
4 changed files with 51 additions and 86 deletions

View File

@@ -11,7 +11,7 @@ import time
from contextvars import Context, copy_context
from datetime import datetime
from functools import partial
from typing import Any, Callable, Dict, List, Literal, Optional, Protocol, Union
from typing import Any, Callable, Dict, List, Literal, Optional, Protocol, Union, cast
from jinja2 import Template
@@ -123,7 +123,7 @@ class TemplateContextBuilder:
episodes_info: Optional[List[TmdbEpisode]] = None,
include_raw_objects: bool = True,
aggregate_music_album: bool = False,
**kwargs
**kwargs: Any,
) -> Dict[str, Any]:
"""
构建一次性渲染上下文字典。
@@ -343,9 +343,9 @@ class TemplateContextBuilder:
"artist": cls.__convert_invalid_characters(meta.artist),
# 标签专辑名常自带尾部年份,与模板独立追加的年份去重(issue #6355)
"album": cls.__strip_album_trailing_year(
cls.__convert_invalid_characters(meta.album), meta.year
cls.__convert_invalid_characters(meta.album or ""), meta.year
),
"album_artist": cls.__convert_invalid_characters(meta.album_artist),
"album_artist": cls.__convert_invalid_characters(meta.album_artist or ""),
"year": meta.year,
"disc_number": meta.disc_number,
"track_number": meta.track_number,
@@ -367,12 +367,12 @@ class TemplateContextBuilder:
})
return
episode_data = {"episode_title": None, "episode_date": None}
episode_data: Dict[str, Optional[str]] = {"episode_title": None, "episode_date": None}
if meta.begin_episode and episodes:
for episode in episodes:
if episode.episode_number == meta.begin_episode:
episode_data.update({
"episode_title": cls.__convert_invalid_characters(episode.name),
"episode_title": cls.__convert_invalid_characters(episode.name or ""),
"episode_date": episode.air_date if episode.air_date else None
})
break
@@ -442,11 +442,12 @@ class TemplateContextBuilder:
"""
if not torrentinfo:
return
size: Union[str, int] = 0
if torrentinfo.size:
if str(torrentinfo.size).replace(".", "").isdigit():
size = size_tools.format_compact_size(torrentinfo.size)
else:
size = torrentinfo.size
size = str(torrentinfo.size)
else:
size = 0
@@ -489,7 +490,7 @@ class TemplateContextBuilder:
ctx = {
"transfer_type": transferinfo.transfer_type,
"file_count": transferinfo.file_count,
"total_size": size_tools.format_compact_size(transferinfo.total_size),
"total_size": size_tools.format_compact_size(transferinfo.total_size or 0),
"err_msg": transferinfo.message,
}
context.update(ctx)
@@ -536,7 +537,7 @@ class TemplateContextBuilder:
context.update(raw_objects)
@staticmethod
def __convert_invalid_characters(filename: str):
def __convert_invalid_characters(filename: str) -> str:
"""
将不支持的字符转换为全角字符
"""
@@ -554,7 +555,7 @@ class TemplateContextBuilder:
@staticmethod
def __strip_album_trailing_year(
album: Optional[str], year: Optional[int]
album: Optional[str], year: Optional[Union[int, str]]
) -> Optional[str]:
"""
去除专辑名尾部的括号年份标记(如 ``欲望反光 (2018)`` -> ``欲望反光``)。
@@ -573,13 +574,13 @@ class TemplateHelper(metaclass=SingletonClass):
模板格式渲染帮助类
"""
def __init__(self):
def __init__(self) -> None:
"""初始化模板上下文构建器和短期渲染缓存。"""
self.builder = TemplateContextBuilder()
self.cache = TTLCache(region="notification", maxsize=100, ttl=600)
@staticmethod
def _generate_cache_key(cuntent: Union[str, dict]) -> str:
def _generate_cache_key(cuntent: Union[str, Dict[str, Any]]) -> str:
"""
生成缓存键
"""
@@ -589,14 +590,14 @@ class TemplateHelper(metaclass=SingletonClass):
return HashUtils.md5(cuntent)
def get_cache_context(self, cuntent: Union[str, dict]) -> Optional[dict]:
def get_cache_context(self, cuntent: Union[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""
获取缓存上下文
"""
cache_key = self._generate_cache_key(cuntent)
return self.cache.get(cache_key)
return cast(Optional[Dict[str, Any]], self.cache.get(cache_key))
def set_cache_context(self, cuntent: Union[str, dict], context: dict) -> None:
def set_cache_context(self, cuntent: Union[str, Dict[str, Any]], context: Dict[str, Any]) -> None:
"""
设置缓存上下文
"""
@@ -606,7 +607,7 @@ class TemplateHelper(metaclass=SingletonClass):
def render(self,
template_content: str,
template_type: Literal['string', 'dict', 'literal'] = "literal",
**kwargs) -> Optional[Union[str, dict]]:
**kwargs: Any) -> Optional[Union[str, Dict[str, Any]]]:
"""
根据模板格式渲染内容
:param template_content: 模板字符串
@@ -655,7 +656,7 @@ class TemplateHelper(metaclass=SingletonClass):
raise ValueError(f"模板处理失败: {str(e)}") from e
@staticmethod
def render_with_context(template_content: str, context: dict) -> str:
def render_with_context(template_content: str, context: Dict[str, Any]) -> str:
"""
使用指定上下文渲染 Jinja2 模板字符串
template_content: Jinja2 模板字符串
@@ -663,18 +664,18 @@ class TemplateHelper(metaclass=SingletonClass):
"""
# 渲染模板
template = Template(template_content)
return template.render(context)
return cast(str, template.render(context))
@staticmethod
def parse_template_content(template_content: Union[str, dict],
template_type: Literal['string', 'dict', 'literal'] = None) -> Optional[Union[str, dict]]:
def parse_template_content(template_content: Union[str, Dict[str, Any]],
template_type: Optional[Literal['string', 'dict', 'literal']] = None) -> Optional[Union[str, Dict[str, Any]]]:
"""
解析模板字符
:param template_content 模板格式字符
:param template_type 模板字符类型
"""
def parse_literal(_template_content: str) -> Union[dict, str]:
def parse_literal(_template_content: str) -> Union[Dict[str, Any], str]:
"""
解析Python字面量
"""
@@ -694,7 +695,7 @@ class TemplateHelper(metaclass=SingletonClass):
'dict': lambda x: x,
'literal': parse_literal
}
return parse_map[template_type](template_content)
return cast(Optional[Union[str, Dict[str, Any]]], parse_map[template_type](template_content))
# 自动判断模板类型
if isinstance(template_content, dict):
@@ -716,7 +717,7 @@ class TemplateHelper(metaclass=SingletonClass):
return None
@staticmethod
def __process_formatted_string(rendered: str) -> Optional[Union[dict, str]]:
def __process_formatted_string(rendered: str) -> Optional[Union[Dict[str, Any], str]]:
"""
处理格式化字符串
保留转义字符
@@ -751,11 +752,11 @@ class TemplateHelper(metaclass=SingletonClass):
# 尝试解析为JSON
try:
rendered_dict = json.loads(processed)
return restore_chars(rendered_dict)
return cast(Optional[Union[Dict[str, Any], str]], restore_chars(rendered_dict))
except json.JSONDecodeError:
return rendered
def close(self):
def close(self) -> None:
"""
清理资源
"""
@@ -769,7 +770,7 @@ class MessageTemplateHelper:
"""
@staticmethod
def render(message: Message, *args, **kwargs) -> Optional[Message]:
def render(message: Message, *args: Any, **kwargs: Any) -> Optional[Message]:
"""
渲染消息模板
"""
@@ -789,7 +790,7 @@ class MessageTemplateHelper:
return False
@staticmethod
def meets_update_conditions(message: Message, *args, **kwargs) -> bool:
def meets_update_conditions(message: Message, *args: Any, **kwargs: Any) -> bool:
"""
判断是否满足消息实例更新条件
@@ -803,19 +804,17 @@ class MessageTemplateHelper:
return False
@staticmethod
def _apply_template_data(message: Message, *args, **kwargs) -> Optional[Message]:
def _apply_template_data(message: Message, *args: Any, **kwargs: Any) -> Optional[Message]:
"""
更新消息实例
"""
try:
if template := MessageTemplateHelper._get_template(message):
try:
rendered = TemplateHelper().render(
template_content=template, *args, **kwargs
)
rendered = TemplateHelper().render(template, *args, **kwargs)
except ValueError as err:
logger.warning(
f"通知模板 {message.ctype.value} 渲染失败,消息保持原样:{str(err)}"
f"通知模板 {message.ctype.value if message.ctype else 'unknown'} 渲染失败,消息保持原样:{str(err)}"
)
return message
if not isinstance(rendered, dict):
@@ -836,7 +835,7 @@ class MessageTemplateHelper:
try:
template_dict = get_configured_system_config().get(SystemConfigKey.NotificationTemplates) or {}
if isinstance(template_dict, dict):
configured = template_dict.get(message.ctype.value)
configured = template_dict.get(message.ctype.value) if message.ctype else None
if str(configured or "").strip() not in {"", "{}", "{ }"}:
return configured
except Exception as err:
@@ -876,7 +875,7 @@ class MessageQueueManager(metaclass=SingletonClass):
def __init__(
self,
send_callback: Optional[Callable] = None,
send_callback: Optional[Callable[..., Any]] = None,
check_interval: Optional[int] = 10,
auto_start: bool = True,
) -> None:
@@ -934,12 +933,12 @@ class MessageQueueManager(metaclass=SingletonClass):
)
@staticmethod
def _parse_schedule(periods: Union[list, dict]) -> List[tuple[int, int, int, int]]:
def _parse_schedule(periods: Union[List[Dict[str, Any]], Dict[str, Any], None]) -> List[tuple[int, int, int, int]]:
"""
将字符串时间格式转换为分钟数元组
支持格式为 'HH:MM' 或 'HH:MM:SS' 的时间字符串
"""
parsed = []
parsed: List[tuple[int, int, int, int]] = []
if not periods:
return parsed
if not isinstance(periods, list):
@@ -1004,7 +1003,7 @@ class MessageQueueManager(metaclass=SingletonClass):
return True
return False
def send_message(self, *args, **kwargs) -> None:
def send_message(self, *args: Any, **kwargs: Any) -> None:
"""
发送消息(立即发送或加入队列)
"""
@@ -1019,7 +1018,7 @@ class MessageQueueManager(metaclass=SingletonClass):
"""使用显式调用方回调立即发送或排队,避免共享队列绑定首个 Chain。"""
self._dispatch_message(send_callback, True, *args, **kwargs)
async def async_send_message(self, *args, **kwargs) -> None:
async def async_send_message(self, *args: Any, **kwargs: Any) -> None:
"""
异步发送消息:``immediately=True`` 立即发送,否则按调度时段入队。
@@ -1129,7 +1128,7 @@ class MessageQueueManager(metaclass=SingletonClass):
self.queue.put(message)
logger.info(f"消息已加入队列,当前队列长度:{self.queue.qsize()}")
def _send(self, *args, **kwargs) -> None:
def _send(self, *args: Any, **kwargs: Any) -> None:
"""
实际发送消息(可通过回调函数自定义)
"""
@@ -1229,7 +1228,7 @@ class MessageHelper(metaclass=Singleton):
@staticmethod
def _build_system_notification_key(
message: Any, role: str, title: str = None, note: Union[list, dict] = None
message: Any, role: str, title: Optional[str] = None, note: Optional[Union[List[Any], Dict[str, Any]]] = None
) -> str:
"""
构建系统通知短期去重键。
@@ -1247,7 +1246,7 @@ class MessageHelper(metaclass=Singleton):
)
def _is_recent_system_notification(
self, message: Any, role: str, title: str = None, note: Union[list, dict] = None
self, message: Any, role: str, title: Optional[str] = None, note: Optional[Union[List[Any], Dict[str, Any]]] = None
) -> bool:
"""
判断系统通知是否在短时间内重复。
@@ -1262,11 +1261,11 @@ class MessageHelper(metaclass=Singleton):
self,
message: Any,
role: str = "plugin",
title: str = None,
note: Union[list, dict] = None,
title: Optional[str] = None,
note: Optional[Union[List[Any], Dict[str, Any]]] = None,
mtype: Optional[MessageType] = None,
level: Optional[NotificationLevel] = None,
):
) -> None:
"""
存消息
:param message: 消息

View File

@@ -108,7 +108,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement |
| Python 源码量 | 373,625 行 | 本次重新扫描物理行数,排除 `app/plugins/**`;74 个文件超过 1,000 行,12 个超过 2,000 行;源码尺寸仅作观察 |
| 长方法 | 309 个超过 80 行 | 本次 AST 扫描排除 `app/plugins/**`;61 个超过 150 行,17 个超过 250 行;新增函数仍受 C901/嵌套深度门禁约束 |
| 全量 mypy 历史债务 | 4,625 / 255 文件 | 本轮在消息、站点、诊断、备份、用户配置、订阅、AcoustID、TMDB、仪表板和命令生命周期边界补齐类型,历史债务再减少 53 项;TMDB v3 API 显式导出并保留集合 parts 的列表合同,逐文件逐错误码无增长;此前工作流筛选、动态插件路由和 stdio 流补齐边界类型,历史债务减少 3 项;Bitpt 与 CookieCloud 补齐边界类型,历史债务减少 16 项;知行站点解析器补齐数值属性与基类合同,历史债务减少 18 项;通知排版补齐 Slack 消息块类型,历史债务减少 2 项;工作流资源过滤改用领域对象调用过滤链,恢复的节点输出在消费前归一化,并收紧可空资源列表的类型低水位;消息媒体交互类型债务已清零,canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降;微信 ClawBot 入站 item_list 媒体解析改为按子项键名分派后,类型错误减少 4 项;站点解析器分享率改用浮点初值,类型错误减少 20 项;本次下载与文件安全修复再减少 3 项;Transmission 进度回退值归一化再减少 2 项;本批清理 Servarr、认证、存储、订阅策略和搜索/规则/通知 Schema 的 18 项类型错误,并修复系统配置中的 Python 2 异常语法;本批再清理订阅元数据、订阅写入、交互宿主、整理路径、删除统计和工作流动作的 14 项类型错误,逐文件逐错误码无增长;本批补齐 OCR 请求参数、工具 Schema 摘要和宿主模块配置快照的 5 项边界类型,逐文件逐错误码无增长;本批为 Rousi 用户消息分页解析补齐返回类型与数据结构兜底,减少 2 项类型错误;本批优先处理 TMDB v3 电影与电视剧对象模型,补齐 98 个同步/异步方法注解并减少 122 项历史类型错误;随后补齐剧集、季度、人物、搜索和趋势对象的 142 个方法注解,再减少 142 项类型错误;绿联 API 响应边界补齐具体泛型与运行时字典收窄,减少 31 项类型错误;运行时限流器补齐状态方法、装饰器边界和时间队列类型,减少 31 项类型错误;QQ Bot API 响应边界补齐具体泛型与 JSON 形状校验,减少 17 项类型错误;rTorrent 客户端清理 182 项错误,调用端同步明确字典合同,累计再减少 190 项错误;豆瓣 API、中心服务接口及订阅消费端补齐返回边界和参数收窄,结合 Context 方法签名与缓存代理类型清理,累计再减少 455 项错误;共享 HTTP 客户端清理 110 项错误,TMDB API 清理 84 项错误,索引器爬虫清理 87 项错误,并补充 U115、AliPan、OpenList 对可空流响应的防守;U115 其余存储基类契约债务暂留待测试完备后处理;缓存同步/异步继承冲突保留为设计债务,未纳入严格门禁 |
| 全量 mypy 历史债务 | 4,448 / 252 文件 | 本轮在消息、站点、诊断、备份、用户配置、订阅、AcoustID、TMDB、仪表板和命令生命周期边界补齐类型,历史债务再减少 53 项;TMDB v3 API 显式导出并保留集合 parts 的列表合同,逐文件逐错误码无增长;此前工作流筛选、动态插件路由和 stdio 流补齐边界类型,历史债务减少 3 项;Bitpt 与 CookieCloud 补齐边界类型,历史债务减少 16 项;知行站点解析器补齐数值属性与基类合同,历史债务减少 18 项;通知排版补齐 Slack 消息块类型,历史债务减少 2 项;工作流资源过滤改用领域对象调用过滤链,恢复的节点输出在消费前归一化,并收紧可空资源列表的类型低水位;消息媒体交互类型债务已清零,canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降;微信 ClawBot 入站 item_list 媒体解析改为按子项键名分派后,类型错误减少 4 项;站点解析器分享率改用浮点初值,类型错误减少 20 项;本次下载与文件安全修复再减少 3 项;Transmission 进度回退值归一化再减少 2 项;本批清理 Servarr、认证、存储、订阅策略和搜索/规则/通知 Schema 的 18 项类型错误,并修复系统配置中的 Python 2 异常语法;本批再清理订阅元数据、订阅写入、交互宿主、整理路径、删除统计和工作流动作的 14 项类型错误,逐文件逐错误码无增长;本批补齐 OCR 请求参数、工具 Schema 摘要和宿主模块配置快照的 5 项边界类型,逐文件逐错误码无增长;本批为 Rousi 用户消息分页解析补齐返回类型与数据结构兜底,减少 2 项类型错误;本批优先处理 TMDB v3 电影与电视剧对象模型,补齐 98 个同步/异步方法注解并减少 122 项历史类型错误;随后补齐剧集、季度、人物、搜索和趋势对象的 142 个方法注解,再减少 142 项类型错误;绿联 API 响应边界补齐具体泛型与运行时字典收窄,减少 31 项类型错误;运行时限流器补齐状态方法、装饰器边界和时间队列类型,减少 31 项类型错误;QQ Bot API 响应边界补齐具体泛型与 JSON 形状校验,减少 17 项类型错误;rTorrent 客户端清理 182 项错误,调用端同步明确字典合同,累计再减少 190 项错误;豆瓣 API、中心服务接口及订阅消费端补齐返回边界和参数收窄,结合 Context 方法签名与缓存代理类型清理,累计再减少 455 项错误;共享 HTTP 客户端清理 110 项错误,TMDB API 清理 84 项错误,索引器爬虫清理 87 项错误,并补充 U115、AliPan、OpenList 对可空流响应的防守;U115 其余存储基类契约债务暂留待测试完备后处理;缓存同步/异步继承冲突保留为设计债务,未纳入严格门禁;本批并行清理消息模板与队列、TMDB 模块和飞牛媒体客户端,累计减少 177 项类型错误,新增严格门禁文件 3 个 |
| Ruff 历史诊断 | 73 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
| 覆盖率逐文件低水位 | 全 app(排除 app/plugins)当前聚合 77.68% | 存量文件低水位只增不减,新文件至少 80% |

View File

@@ -25,6 +25,7 @@ files =
app/modules/_base/downloader.py,
app/monitor/dispatcher.py,
app/application/messaging/skill.py,
app/application/messaging/message.py,
app/workflow/actions/add_subscribe.py,
app/workflow/actions/scan_file.py,
app/workflow/actions/transfer_file.py,
@@ -44,6 +45,8 @@ files =
app/modules/dingtalk/__init__.py,
app/modules/synologychat/synologychat.py,
app/modules/__init__.py,
app/modules/trimemedia/trimemedia.py,
app/modules/themoviedb/__init__.py,
app/runtime/debounce.py,
app/modules/vocechat/vocechat.py,
app/chain/system.py,

View File

@@ -419,18 +419,6 @@
"no-untyped-def": 1,
"type-arg": 2
},
"app/application/messaging/message.py": {
"arg-type": 5,
"assignment": 9,
"dict-item": 1,
"misc": 1,
"no-any-return": 4,
"no-untyped-call": 2,
"no-untyped-def": 12,
"type-arg": 18,
"union-attr": 2,
"var-annotated": 1
},
"app/application/messaging/plugin.py": {
"arg-type": 5,
"no-untyped-call": 1,
@@ -977,7 +965,6 @@
"assignment": 2,
"attr-defined": 1,
"no-redef": 1,
"no-untyped-call": 1,
"no-untyped-def": 3,
"operator": 6,
"type-arg": 2,
@@ -1222,22 +1209,10 @@
"misc": 2,
"no-untyped-def": 2
},
"app/modules/themoviedb/__init__.py": {
"arg-type": 5,
"assignment": 11,
"empty-body": 1,
"index": 2,
"no-any-return": 2,
"no-untyped-call": 3,
"no-untyped-def": 3,
"return-value": 2,
"type-arg": 19,
"union-attr": 2
},
"app/modules/themoviedb/cache.py": {
"no-any-return": 3,
"no-untyped-def": 4,
"type-arg": 5,
"no-untyped-def": 2,
"type-arg": 4,
"union-attr": 5
},
"app/modules/themoviedb/scraper.py": {
@@ -1261,29 +1236,17 @@
},
"app/modules/trimemedia/api.py": {
"misc": 2,
"no-untyped-def": 11,
"no-untyped-def": 9,
"type-arg": 6
},
"app/modules/trimemedia/module.py": {
"assignment": 1,
"empty-body": 1,
"no-any-return": 3,
"no-untyped-call": 1,
"no-any-return": 1,
"no-untyped-def": 7,
"return": 1,
"type-arg": 2
},
"app/modules/trimemedia/trimemedia.py": {
"arg-type": 5,
"no-any-return": 1,
"no-untyped-call": 8,
"no-untyped-def": 9,
"operator": 1,
"return": 1,
"type-arg": 2,
"union-attr": 32,
"var-annotated": 4
},
"app/modules/ugreen/module.py": {
"arg-type": 3,
"assignment": 1,