fix(feishu): 补充资源选择卡片的站点、大小和做种数 (#7085)

* fix(feishu): show site size and seeders in torrent cards

* chore(architecture): record Feishu size formatting dependency
This commit is contained in:
jxxghp
2026-10-19 18:47:30 +08:00
committed by GitHub
parent 458329b3f7
commit cfec9497f5
4 changed files with 113 additions and 4 deletions

View File

@@ -19,6 +19,7 @@ from app.application.messaging.channel.admin import matches_channel_admin
from app.application.messaging.ingress import submit_message_to_host
from app.application.security.user import get_configured_user_channel_lookup
from app.domain.context import Context, MediaInfo
from app.foundation import size as size_tools
from app.modules.feishu.openapi import FEISHU_DOMAIN, FeishuOpenApi, FeishuResponse, JsonDict
from app.runtime.log import logger
from app.runtime.settings import get_runtime_setting
@@ -2071,6 +2072,8 @@ class Feishu:
"""
将前 10 个种子候选项渲染为通知卡片。
每项显示站点、标题、大小和做种数;缺失或为零的详情不展示,保留输入顺序与选择序号。
:param message: 原通知标题、按钮、链接和目标
:param torrents: 含种子信息的候选上下文
:param userid: 显式用户标识;未提供用户和会话时回退到默认配置或已互动会话
@@ -2082,7 +2085,16 @@ class Feishu:
for index, torrent in enumerate(torrents[:10], start=1):
torrent_info = getattr(torrent, "torrent_info", None)
title = getattr(torrent_info, "title", None) or getattr(torrent_info, "site_name", None) or "未知种子"
lines.append(f"{index}. {title}")
site_name = getattr(torrent_info, "site_name", None)
size = getattr(torrent_info, "size", None)
seeders = getattr(torrent_info, "seeders", None)
site_prefix = f"【{site_name}】" if site_name else " "
details = [f"{index}.{site_prefix}{title}"]
if size:
details.append(size_tools.format_compact_size(size))
if seeders:
details.append(f"{seeders}↑")
lines.append(" ".join(details))
proxy_message = Message(
title=message.title,
text="\n".join(lines),

View File

@@ -817,7 +817,7 @@ flowchart LR
| 指标 | 当前值 |
|---|---:|
| Python 模块 | 1127 |
| 内部导入边 | 9,640 |
| 内部导入边 | 9,642 |
| 非平凡 SCC | 1(29 个模块的 TMDB vendor 精确 containment) |
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
| Direct egress | 51(精确 policy 准入,Application / Chain 均为 0) |

View File

@@ -975,8 +975,8 @@
"runtime_only": true
}
},
"edge_count": 9640,
"edge_sha256": "e282d6307ea8c6f0cd2843f5c2faae04995bc2630c945e408d1dd490e337fcfe",
"edge_count": 9642,
"edge_sha256": "bace4b8791f142f8d9fdc9e12f532c562d7eec5bed42adb2d07bdb9c8ec8251a",
"edges": [
"app -> app.foundation",
"app -> app.foundation.environment",
@@ -7323,6 +7323,8 @@
"app.modules.feishu.feishu -> app.application.security.user",
"app.modules.feishu.feishu -> app.domain",
"app.modules.feishu.feishu -> app.domain.context",
"app.modules.feishu.feishu -> app.foundation",
"app.modules.feishu.feishu -> app.foundation.size",
"app.modules.feishu.feishu -> app.modules",
"app.modules.feishu.feishu -> app.modules.feishu",
"app.modules.feishu.feishu -> app.modules.feishu.openapi",

View File

@@ -0,0 +1,95 @@
"""
验证飞书资源选择卡片的种子详情、缺失字段和选择按钮。
"""
import json
from unittest.mock import MagicMock, patch
import pytest
from app.domain.context import Context
from app.modules.feishu.feishu import Feishu
from app.modules.feishu.openapi import FeishuOpenApi, FeishuResponse
from app.schemas.context import TorrentInfo
from app.schemas.message import Message
def _build_client() -> tuple[Feishu, MagicMock]:
"""
构造隔离长连接与开放平台请求的客户端,保留真实卡片渲染路径。
"""
api = MagicMock(spec=FeishuOpenApi)
api.create_message.return_value = FeishuResponse(
code=0, msg="success", data={"message_id": "om_test", "chat_id": "oc_test"},
)
with (
patch.object(Feishu, "_build_api_client", return_value=api),
patch.object(Feishu, "_start_ws_client"),
):
client = Feishu(FEISHU_APP_ID="test_app", FEISHU_APP_SECRET="test_secret")
return client, api
@pytest.mark.parametrize(
("fields", "expected"),
[
({"site_name": "站点", "size": 2.5 * 1024 ** 3, "seeders": 35}, "1.【站点】影片 2.5G 35↑"),
({"site_name": "站点", "size": 2.5 * 1024 ** 3}, "1.【站点】影片 2.5G"),
({"site_name": "站点", "seeders": 35}, "1.【站点】影片 35↑"),
({"size": 2.5 * 1024 ** 3, "seeders": 35}, "1. 影片 2.5G 35↑"),
({"site_name": "站点"}, "1.【站点】影片"),
({"size": 2.5 * 1024 ** 3}, "1. 影片 2.5G"),
({"seeders": 35}, "1. 影片 35↑"),
({}, "1. 影片"),
({"site_name": None, "size": None, "seeders": None}, "1. 影片"),
({"site_name": "", "size": 0, "seeders": 0}, "1. 影片"),
({"title": None, "site_name": "站点"}, "1.【站点】站点"),
({"title": None}, "1. 未知种子"),
],
)
def test_torrent_card_displays_available_details(fields: dict, expected: str) -> None:
"""
实际发送的卡片显示可用站点、大小和做种数,省略空值且不附加促销文案。
"""
client, api = _build_client()
torrent = TorrentInfo(**{"title": "影片", "volume_factor": "免费", **fields})
result = client.send_torrents_message(
Message(title="请选择下载"), [Context(torrent_info=torrent)], userid="ou_user",
)
assert result["success"]
card = json.loads(api.create_message.call_args.kwargs["content"])
assert card["body"]["elements"][1]["content"] == expected
def test_torrent_card_keeps_selection_order_and_target() -> None:
"""
列表仅展示前十项,缺失种子信息仍保留序号,接收会话、链接和下载按钮保持可用。
"""
client, api = _build_client()
torrents = [Context()] + [
Context(torrent_info=TorrentInfo(title=f"影片{index}", seeders=index))
for index in range(2, 12)
]
result = client.send_torrents_message(
Message(
title="请选择下载", link="https://example.com/search",
buttons=[[{"text": "下载第2项", "callback_data": "2"}]],
),
torrents, chat_id="oc_target", receive_id_type="chat_id",
)
assert result["success"]
request = api.create_message.call_args.kwargs
assert request["receive_id"] == "oc_target"
assert request["receive_id_type"] == "chat_id"
card = json.loads(request["content"])
content = card["body"]["elements"][1]["content"]
assert content.splitlines()[:10] == ["1. 未知种子"] + [
f"{index}. 影片{index} {index}↑" for index in range(2, 11)
]
assert "影片11" not in content
assert "https://example.com/search" in content
button = card["body"]["elements"][-1]["columns"][0]["elements"][0]
assert button["behaviors"] == [{"type": "callback", "value": {"callback_data": "2"}}]