diff --git a/app/application/messaging/message.py b/app/application/messaging/message.py index a011f3139..a494743ce 100644 --- a/app/application/messaging/message.py +++ b/app/application/messaging/message.py @@ -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: 消息 diff --git a/docs/refactor/optimization-checklist.md b/docs/refactor/optimization-checklist.md index 3fab61d3b..b0fd5b09f 100644 --- a/docs/refactor/optimization-checklist.md +++ b/docs/refactor/optimization-checklist.md @@ -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% | diff --git a/mypy.ini b/mypy.ini index d4cbad609..fd7ba189d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -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, diff --git a/tests/fixtures/architecture/mypy-baseline.json b/tests/fixtures/architecture/mypy-baseline.json index 436e2d648..b05072ca3 100644 --- a/tests/fixtures/architecture/mypy-baseline.json +++ b/tests/fixtures/architecture/mypy-baseline.json @@ -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,