mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-06 07:09:09 +08:00
fix(types): preserve Feishu notification identity boundaries
This commit is contained in:
@@ -59,8 +59,8 @@ class Feishu:
|
||||
FEISHU_VERIFICATION_TOKEN: Optional[str] = None,
|
||||
FEISHU_ENCRYPT_KEY: Optional[str] = None,
|
||||
name: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""
|
||||
读取应用配置并启动后台长连接接收线程。
|
||||
|
||||
@@ -189,7 +189,7 @@ class Feishu:
|
||||
finally:
|
||||
self._ready.clear()
|
||||
|
||||
def _forward_to_message_chain(self, payload: dict) -> bool:
|
||||
def _forward_to_message_chain(self, payload: JsonDict) -> bool:
|
||||
"""
|
||||
将飞书入站消息转发到统一消息入口,复用现有交互主链。
|
||||
"""
|
||||
@@ -254,7 +254,7 @@ class Feishu:
|
||||
return text, images, audio_refs, files
|
||||
|
||||
@staticmethod
|
||||
def _resolve_post_message_body(content: dict) -> Optional[dict]:
|
||||
def _resolve_post_message_body(content: JsonDict) -> Optional[JsonDict]:
|
||||
"""
|
||||
解析飞书富文本消息在事件和 webhook 结构中的正文节点。
|
||||
"""
|
||||
@@ -275,7 +275,7 @@ class Feishu:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _parse_post_element_text(element: dict) -> str:
|
||||
def _parse_post_element_text(element: JsonDict) -> str:
|
||||
"""
|
||||
将飞书富文本元素转换为消息链可消费的纯文本片段。
|
||||
"""
|
||||
@@ -303,7 +303,7 @@ class Feishu:
|
||||
|
||||
@staticmethod
|
||||
def _parse_post_message_content(
|
||||
content: dict,
|
||||
content: JsonDict,
|
||||
message_id: Optional[str] = None,
|
||||
) -> Tuple[str, Optional[List[IncomingMessage.MessageImage]]]:
|
||||
"""
|
||||
@@ -455,7 +455,7 @@ class Feishu:
|
||||
name=action.get("name"),
|
||||
)
|
||||
|
||||
payload = {
|
||||
payload: JsonDict = {
|
||||
"type": "cardAction",
|
||||
"source": self._name,
|
||||
"message_id": context.get("open_message_id"),
|
||||
@@ -645,11 +645,11 @@ class Feishu:
|
||||
images = IncomingMessage.MessageImage.normalize_list(message.get("images"))
|
||||
audio_refs = None
|
||||
if isinstance(message.get("audio_refs"), list):
|
||||
audio_refs = [str(item).strip() for item in message.get("audio_refs") if str(item).strip()] or None
|
||||
audio_refs = [str(item).strip() for item in (message.get("audio_refs") or []) if str(item).strip()] or None
|
||||
files = None
|
||||
if isinstance(message.get("files"), list):
|
||||
normalized_files = []
|
||||
for item in message.get("files"):
|
||||
for item in (message.get("files") or []):
|
||||
if isinstance(item, dict) and item.get("ref"):
|
||||
normalized_files.append(IncomingMessage.MessageAttachment(**item))
|
||||
files = normalized_files or None
|
||||
@@ -690,7 +690,7 @@ class Feishu:
|
||||
|
||||
def _resolve_target(
|
||||
self,
|
||||
userid: Optional[str] = None,
|
||||
userid: Optional[Union[str, int]] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
) -> Optional[Tuple[str, str]]:
|
||||
@@ -702,7 +702,11 @@ class Feishu:
|
||||
:param receive_id_type: open_id、user_id 或 chat_id;未指定时依据已记录身份推断
|
||||
:return: 接收标识与类型的二元组;无显式及默认目标返回 None
|
||||
"""
|
||||
resolved_userid = (userid or "").strip() or None
|
||||
user_value = userid or ""
|
||||
if isinstance(user_value, int):
|
||||
# 保留历史非零整数在目标解析处失败的异常类型与外层捕获边界。
|
||||
raise AttributeError("'int' object has no attribute 'strip'")
|
||||
resolved_userid = user_value.strip() or None
|
||||
resolved_chat_id = (chat_id or "").strip() or None
|
||||
normalized_receive_id_type = (receive_id_type or "").strip() or None
|
||||
if not resolved_userid and not resolved_chat_id:
|
||||
@@ -740,11 +744,11 @@ class Feishu:
|
||||
def _send_with_fallback_broadcast(
|
||||
self,
|
||||
msg_type: str,
|
||||
content: dict,
|
||||
userid: Optional[str] = None,
|
||||
content: JsonDict,
|
||||
userid: Optional[Union[str, int]] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
) -> Optional[JsonDict]:
|
||||
"""
|
||||
按解析目标发送消息,无明确目标时发送给已互动会话。
|
||||
|
||||
@@ -918,7 +922,7 @@ class Feishu:
|
||||
text: Optional[str],
|
||||
text_size: str = "normal",
|
||||
margin: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
) -> Optional[JsonDict]:
|
||||
"""
|
||||
构造非空卡片文字区块,按需设置图片布局所需的外边距。
|
||||
"""
|
||||
@@ -1038,16 +1042,16 @@ class Feishu:
|
||||
return str(callback_data).strip() if callback_data else None
|
||||
|
||||
@staticmethod
|
||||
def _card_actions(buttons: Optional[List[List[dict]]], margin: Optional[str] = None) -> List[dict]:
|
||||
def _card_actions(buttons: Optional[List[List[JsonDict]]], margin: Optional[str] = None) -> List[JsonDict]:
|
||||
"""
|
||||
将统一按钮结构转换为飞书卡片按钮配置。
|
||||
"""
|
||||
if not buttons:
|
||||
return []
|
||||
card_rows = []
|
||||
for row in buttons[:8]:
|
||||
for button_row in buttons[:8]:
|
||||
columns = []
|
||||
for button in row[:3]:
|
||||
for button in button_row[:3]:
|
||||
text = (button or {}).get("text")
|
||||
if not text:
|
||||
continue
|
||||
@@ -1094,14 +1098,14 @@ class Feishu:
|
||||
}
|
||||
)
|
||||
if columns:
|
||||
row = {
|
||||
card_row: JsonDict = {
|
||||
"tag": "column_set",
|
||||
"flex_mode": "none",
|
||||
"columns": columns,
|
||||
}
|
||||
if margin:
|
||||
row["margin"] = margin
|
||||
card_rows.append(row)
|
||||
card_row["margin"] = margin
|
||||
card_rows.append(card_row)
|
||||
return card_rows
|
||||
|
||||
def _build_card(
|
||||
@@ -1109,13 +1113,13 @@ class Feishu:
|
||||
title: Optional[str],
|
||||
text: Optional[str],
|
||||
link: Optional[str],
|
||||
buttons: Optional[List[List[dict]]],
|
||||
buttons: Optional[List[List[JsonDict]]],
|
||||
image_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
构建飞书交互卡片结构。
|
||||
"""
|
||||
elements: List[dict] = []
|
||||
elements: List[JsonDict] = []
|
||||
if image_key:
|
||||
# 图文混合消息需要让图片贴近卡片顶部,避免先展示文字再露出海报。
|
||||
elements.append(
|
||||
@@ -1169,7 +1173,7 @@ class Feishu:
|
||||
"""
|
||||
构建支持 CardKit 流式更新的飞书卡片 JSON 2.0。
|
||||
"""
|
||||
elements: List[dict] = []
|
||||
elements: List[JsonDict] = []
|
||||
title_content = self._escape_card_text(title).strip() if title else ""
|
||||
body_content = self._escape_card_text(
|
||||
self._strip_streaming_markdown_images(text)
|
||||
@@ -1235,11 +1239,11 @@ class Feishu:
|
||||
self,
|
||||
title: Optional[str],
|
||||
text: Optional[str],
|
||||
userid: Optional[str] = None,
|
||||
userid: Optional[Union[str, int]] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
original_message_id: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
) -> Optional[JsonDict]:
|
||||
"""
|
||||
发送或回复流式卡片,并记录后续更新需要的卡片标识与序号。
|
||||
"""
|
||||
@@ -1276,7 +1280,7 @@ class Feishu:
|
||||
def _send_agent_streaming_images(
|
||||
self,
|
||||
image_urls: List[str],
|
||||
userid: Optional[str] = None,
|
||||
userid: Optional[Union[str, int]] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
sent_image_urls: Optional[List[str]] = None,
|
||||
@@ -1400,7 +1404,7 @@ class Feishu:
|
||||
)
|
||||
return False
|
||||
|
||||
def _send_message(self, receive_id: str, receive_id_type: str, msg_type: str, content: dict) -> Optional[dict]:
|
||||
def _send_message(self, receive_id: str, receive_id_type: str, msg_type: str, content: JsonDict) -> Optional[JsonDict]:
|
||||
"""
|
||||
调用飞书 IM API 发送消息,并返回统一结果结构。
|
||||
"""
|
||||
@@ -1439,9 +1443,9 @@ class Feishu:
|
||||
self,
|
||||
message_id: str,
|
||||
msg_type: str,
|
||||
content: dict,
|
||||
content: JsonDict,
|
||||
reply_in_thread: bool = False,
|
||||
) -> Optional[dict]:
|
||||
) -> Optional[JsonDict]:
|
||||
"""
|
||||
按原消息回复,保持飞书会话中的引用关系。
|
||||
"""
|
||||
@@ -1576,7 +1580,7 @@ class Feishu:
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
original_message_id: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
) -> Optional[JsonDict]:
|
||||
"""
|
||||
发送纯文本,按原消息回复或按目标投递。
|
||||
|
||||
@@ -1608,8 +1612,11 @@ class Feishu:
|
||||
|
||||
if not result:
|
||||
return {"success": False}
|
||||
result["chat_id"] = result.get("chat_id") or chat_id or self._user_chat_mapping.get(
|
||||
userid or "") or self._default_chat_id
|
||||
result["chat_id"] = (
|
||||
result.get("chat_id") or chat_id
|
||||
or (self._user_chat_mapping.get(userid or "") if isinstance(userid, str) or not userid else None)
|
||||
or self._default_chat_id
|
||||
)
|
||||
return result
|
||||
|
||||
def send_file(
|
||||
@@ -1622,7 +1629,7 @@ class Feishu:
|
||||
file_name: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
original_message_id: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
) -> Optional[JsonDict]:
|
||||
"""
|
||||
发送本地图片卡片或普通文件,必要时补发说明。
|
||||
|
||||
@@ -1703,8 +1710,11 @@ class Feishu:
|
||||
|
||||
if not result:
|
||||
return {"success": False}
|
||||
result["chat_id"] = result.get("chat_id") or chat_id or self._user_chat_mapping.get(
|
||||
userid or "") or self._default_chat_id
|
||||
result["chat_id"] = (
|
||||
result.get("chat_id") or chat_id
|
||||
or (self._user_chat_mapping.get(userid or "") if isinstance(userid, str) or not userid else None)
|
||||
or self._default_chat_id
|
||||
)
|
||||
return result
|
||||
|
||||
def send_voice(
|
||||
@@ -1715,7 +1725,7 @@ class Feishu:
|
||||
caption: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
original_message_id: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
) -> Optional[JsonDict]:
|
||||
"""
|
||||
上传并发送本地语音,可另外发送文字说明。
|
||||
|
||||
@@ -1764,18 +1774,21 @@ class Feishu:
|
||||
|
||||
if not result:
|
||||
return {"success": False}
|
||||
result["chat_id"] = result.get("chat_id") or chat_id or self._user_chat_mapping.get(
|
||||
userid or "") or self._default_chat_id
|
||||
result["chat_id"] = (
|
||||
result.get("chat_id") or chat_id
|
||||
or (self._user_chat_mapping.get(userid or "") if isinstance(userid, str) or not userid else None)
|
||||
or self._default_chat_id
|
||||
)
|
||||
return result
|
||||
|
||||
def send_notification(
|
||||
self,
|
||||
message: Message,
|
||||
userid: Optional[str] = None,
|
||||
userid: Optional[Union[str, int]] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
original_message_id: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
) -> Optional[JsonDict]:
|
||||
"""
|
||||
发送通知卡片,符合条件的 Agent 正文使用流式卡片。
|
||||
|
||||
@@ -1795,8 +1808,8 @@ class Feishu:
|
||||
)
|
||||
if is_streaming_agent_text:
|
||||
try:
|
||||
stream_image_urls = []
|
||||
if self._is_external_image_url(message.image):
|
||||
stream_image_urls: List[str] = []
|
||||
if message.image and self._is_external_image_url(message.image):
|
||||
stream_image_urls.append(message.image)
|
||||
stream_image_urls.extend(self._extract_markdown_image_urls(message.text))
|
||||
result = self._send_streaming_card_message(
|
||||
@@ -1812,8 +1825,11 @@ class Feishu:
|
||||
return {"success": False}
|
||||
if not result:
|
||||
return {"success": False}
|
||||
result["chat_id"] = result.get("chat_id") or chat_id or self._user_chat_mapping.get(
|
||||
userid or "") or self._default_chat_id
|
||||
result["chat_id"] = (
|
||||
result.get("chat_id") or chat_id
|
||||
or (self._user_chat_mapping.get(userid or "") if isinstance(userid, str) or not userid else None)
|
||||
or self._default_chat_id
|
||||
)
|
||||
sent_image_urls = self._send_agent_streaming_images(
|
||||
stream_image_urls,
|
||||
userid=userid,
|
||||
@@ -1851,8 +1867,11 @@ class Feishu:
|
||||
|
||||
if not result:
|
||||
return {"success": False}
|
||||
result["chat_id"] = result.get("chat_id") or chat_id or self._user_chat_mapping.get(
|
||||
userid or "") or self._default_chat_id
|
||||
result["chat_id"] = (
|
||||
result.get("chat_id") or chat_id
|
||||
or (self._user_chat_mapping.get(userid or "") if isinstance(userid, str) or not userid else None)
|
||||
or self._default_chat_id
|
||||
)
|
||||
return result
|
||||
|
||||
def _build_notification_cards(self, message: Message, image_key: Optional[str]) -> List[JsonDict]:
|
||||
@@ -1885,7 +1904,7 @@ class Feishu:
|
||||
) for index, text in enumerate(texts)]
|
||||
|
||||
def edit_message(self, message_id: str, title: Optional[str] = None, text: Optional[str] = None,
|
||||
buttons: Optional[List[List[dict]]] = None, metadata: Optional[dict] = None,
|
||||
buttons: Optional[List[List[JsonDict]]] = None, metadata: Optional[JsonDict] = None,
|
||||
chat_id: Optional[str] = None) -> bool:
|
||||
"""
|
||||
更新普通交互卡片或继续写入流式正文。
|
||||
@@ -2007,7 +2026,7 @@ class Feishu:
|
||||
userid: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
) -> Optional[JsonDict]:
|
||||
"""
|
||||
将前 10 个媒体候选项渲染为通知卡片。
|
||||
|
||||
@@ -2048,7 +2067,7 @@ class Feishu:
|
||||
userid: Optional[str] = None,
|
||||
chat_id: Optional[str] = None,
|
||||
receive_id_type: Optional[str] = None,
|
||||
) -> Optional[dict]:
|
||||
) -> Optional[JsonDict]:
|
||||
"""
|
||||
将前 10 个种子候选项渲染为通知卡片。
|
||||
|
||||
|
||||
@@ -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,220 / 247 文件 | 本分支为 Pydantic 字段补充描述并保留默认值,减少 3 项 assignment 错误;本次 NFO 返回类型与契约对齐,减少 2 项历史类型错误;本轮在消息、站点、诊断、备份、用户配置、订阅、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,187 / 246 文件 | 本分支为 Pydantic 字段补充描述并保留默认值,减少 3 项 assignment 错误;本次 NFO 返回类型与契约对齐,减少 2 项历史类型错误;本轮在消息、站点、诊断、备份、用户配置、订阅、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 其余存储基类契约债务暂留待测试完备后处理;缓存同步/异步继承冲突保留为设计债务,未纳入严格门禁 |
|
||||
| Ruff 历史诊断 | 73 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| 覆盖率逐文件低水位 | 全 app(排除 app/plugins)当前聚合 77.68% | 存量文件比例不降;语句减少且未覆盖行数不增时允许比例变化,新文件至少 80% |
|
||||
|
||||
|
||||
1
mypy.ini
1
mypy.ini
@@ -7,6 +7,7 @@ follow_imports = skip
|
||||
show_error_codes = True
|
||||
pretty = True
|
||||
files =
|
||||
app/modules/feishu/feishu.py,
|
||||
app/modules/emby/emby.py,
|
||||
app/modules/discord/discord.py,
|
||||
app/modules/zspace/zspace.py,
|
||||
|
||||
@@ -828,15 +828,6 @@
|
||||
"type-arg": 9,
|
||||
"var-annotated": 1
|
||||
},
|
||||
"app/modules/feishu/feishu.py": {
|
||||
"arg-type": 5,
|
||||
"assignment": 1,
|
||||
"call-overload": 1,
|
||||
"no-untyped-def": 1,
|
||||
"return-value": 1,
|
||||
"type-arg": 22,
|
||||
"union-attr": 2
|
||||
},
|
||||
"app/modules/feishu/module.py": {
|
||||
"assignment": 6,
|
||||
"attr-defined": 1,
|
||||
|
||||
53
tests/test_feishu_target_typing.py
Normal file
53
tests/test_feishu_target_typing.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
飞书列表通知保留用户标识的历史目标解析与异常边界。
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.modules.feishu.feishu import Feishu
|
||||
from app.schemas.message import Message
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["send_medias_message", "send_torrents_message"])
|
||||
@pytest.mark.parametrize("userid, expected_target", [("ou_user", "ou_user"), (0, "ou_default"), (12, None)])
|
||||
def test_list_notification_preserves_userid_target_boundary(method, userid, expected_target):
|
||||
"""
|
||||
字符串发往用户,零值沿用默认目标,非零整数在通知内部捕获失败而不外抛。
|
||||
"""
|
||||
client = Feishu.__new__(Feishu)
|
||||
client._default_open_id = "ou_default"
|
||||
client._default_chat_id = None
|
||||
client._user_receive_id_type_mapping = {}
|
||||
client._user_chat_mapping = {}
|
||||
client._upload_remote_image = Mock(return_value=None)
|
||||
client._build_notification_cards = Mock(return_value=[{"schema": "2.0"}])
|
||||
client._send_message = Mock(return_value={"success": True, "chat_id": "oc_chat"})
|
||||
|
||||
result = getattr(client, method)(Message(title="测试", userid=userid), [])
|
||||
|
||||
if expected_target is None:
|
||||
assert result == {"success": False}
|
||||
client._send_message.assert_not_called()
|
||||
else:
|
||||
assert result["success"] is True
|
||||
assert client._send_message.call_args.args[:2] == (expected_target, "open_id")
|
||||
|
||||
|
||||
def test_reply_does_not_resolve_integer_userid():
|
||||
"""
|
||||
明确回复原消息时不解析用户标识,非零整数仍允许成功回复。
|
||||
"""
|
||||
client = Feishu.__new__(Feishu)
|
||||
client._default_chat_id = None
|
||||
client._user_chat_mapping = {}
|
||||
client._upload_remote_image = Mock(return_value=None)
|
||||
client._build_notification_cards = Mock(return_value=[{"schema": "2.0"}])
|
||||
client._reply_message = Mock(return_value={"success": True, "chat_id": "oc_reply"})
|
||||
client._resolve_target = Mock(side_effect=AssertionError("回复不可解析目标"))
|
||||
|
||||
result = client.send_notification(Message(title="测试"), userid=12, original_message_id="om_reply")
|
||||
|
||||
assert result == {"success": True, "chat_id": "oc_reply"}
|
||||
client._resolve_target.assert_not_called()
|
||||
Reference in New Issue
Block a user