refactor: unify site chain data port access

This commit is contained in:
jxxghp
2026-08-24 04:25:29 +08:00
parent 286b5836a1
commit 836cbfbbb2
6 changed files with 51 additions and 18 deletions
+14 -12
View File
@@ -11,7 +11,7 @@ from app.chain import ChainBase
from app.chain._interaction import InteractionChainMixin from app.chain._interaction import InteractionChainMixin
from app.runtime.config import global_vars from app.runtime.config import global_vars
from app.runtime.events import Event, eventmanager from app.runtime.events import Event, eventmanager
from app.application.chain.data import SitePortProxy as SiteOper from app.application.chain.data import get_chain_site_port
from app.application.configuration import get_configured_system_config from app.application.configuration import get_configured_system_config
from app.adapters.network.browser import PlaywrightHelper from app.adapters.network.browser import PlaywrightHelper
from app.adapters.network.cloudflare import under_challenge from app.adapters.network.cloudflare import under_challenge
@@ -68,9 +68,11 @@ class SiteChain(InteractionChainMixin, ChainBase):
""" """
userdata: SiteUserData = self.run_module("refresh_userdata", site=site) userdata: SiteUserData = self.run_module("refresh_userdata", site=site)
if userdata: if userdata:
SiteOper().update_userdata(domain=site_rules.extract_domain(site.get("domain")), get_chain_site_port().update_userdata(
domain=site_rules.extract_domain(site.get("domain")),
name=site.get("name"), name=site.get("name"),
payload=userdata.model_dump()) payload=userdata.model_dump(),
)
# 发送事件 # 发送事件
eventmanager.send_event(EventType.SiteRefreshed, { eventmanager.send_event(EventType.SiteRefreshed, {
"site_id": site.get("id") "site_id": site.get("id")
@@ -421,7 +423,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
self.messagehelper.put(msg, title="CookieCloud同步失败", role="system") self.messagehelper.put(msg, title="CookieCloud同步失败", role="system")
return False, msg return False, msg
siteshelper = SitesHelper() siteshelper = SitesHelper()
siteoper = SiteOper() siteoper = get_chain_site_port()
rsshelper = RssHelper() rsshelper = RssHelper()
total_num = len(cookies) total_num = len(cookies)
update_count = add_count = fail_count = 0 update_count = add_count = fail_count = 0
@@ -485,7 +487,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
cookie: str, cookie: str,
indexer: Optional[dict], indexer: Optional[dict],
site_info: Any, site_info: Any,
siteoper: SiteOper, siteoper: Any,
rsshelper: RssHelper, rsshelper: RssHelper,
) -> Tuple[int, int, int, bool]: ) -> Tuple[int, int, int, bool]:
"""处理单个域名,并返回计数与是否继续发送更新事件。""" """处理单个域名,并返回计数与是否继续发送更新事件。"""
@@ -581,7 +583,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
if str(domain).startswith("http"): if str(domain).startswith("http"):
domain = site_rules.extract_domain(domain) domain = site_rules.extract_domain(domain)
# 站点信息 # 站点信息
siteoper = SiteOper() siteoper = get_chain_site_port()
siteshelper = SitesHelper() siteshelper = SitesHelper()
siteinfo = siteoper.get_by_domain(domain) siteinfo = siteoper.get_by_domain(domain)
if not siteinfo: if not siteinfo:
@@ -659,7 +661,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
""" """
# 检查域名是否可用 # 检查域名是否可用
domain = site_rules.extract_domain(url) domain = site_rules.extract_domain(url)
siteoper = SiteOper() siteoper = get_chain_site_port()
site_info = siteoper.get_by_domain(domain) site_info = siteoper.get_by_domain(domain)
if not site_info: if not site_info:
return False, f"站点【{url}】不存在" return False, f"站点【{url}】不存在"
@@ -737,7 +739,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
return SiteInteractionHandler( return SiteInteractionHandler(
messenger=self, messenger=self,
cookie_updater=self.update_cookie, cookie_updater=self.update_cookie,
repository=SiteOper(), repository=get_chain_site_port(),
) )
def remote_disable(self, arg_str: str, channel: NotificationChannel, def remote_disable(self, arg_str: str, channel: NotificationChannel,
@@ -751,7 +753,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
if not arg_str.isdigit(): if not arg_str.isdigit():
return return
site_id = int(arg_str) site_id = int(arg_str)
siteoper = SiteOper() siteoper = get_chain_site_port()
site = siteoper.get(site_id) site = siteoper.get(site_id)
if not site: if not site:
self.post_message(Message( self.post_message(Message(
@@ -775,7 +777,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
if not arg_str: if not arg_str:
return return
arg_strs = str(arg_str).split() arg_strs = str(arg_str).split()
siteoper = SiteOper() siteoper = get_chain_site_port()
for arg_str in arg_strs: for arg_str in arg_strs:
arg_str = arg_str.strip() arg_str = arg_str.strip()
if not arg_str.isdigit(): if not arg_str.isdigit():
@@ -819,7 +821,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
cookie, ua, msg = result cookie, ua, msg = result
if not cookie: if not cookie:
return False, msg return False, msg
SiteOper().update(site_info.id, { get_chain_site_port().update(site_info.id, {
"cookie": cookie, "cookie": cookie,
"ua": ua "ua": ua
}) })
@@ -867,7 +869,7 @@ class SiteChain(InteractionChainMixin, ChainBase):
# 站点ID # 站点ID
site_id = int(site_id) site_id = int(site_id)
# 站点信息 # 站点信息
site_info = SiteOper().get(site_id) site_info = get_chain_site_port().get(site_id)
if not site_info: if not site_info:
self.post_message(Message( self.post_message(Message(
channel=channel, channel=channel,
+2 -2
View File
@@ -12,7 +12,7 @@ from app.domain.context import TorrentInfo, Context, MediaInfo
from app.domain.context import MusicInfo from app.domain.context import MusicInfo
from app.domain.meta.metamusic import MetaMusic from app.domain.meta.metamusic import MetaMusic
from app.domain.metainfo import MetaInfo from app.domain.metainfo import MetaInfo
from app.application.chain.data import SitePortProxy as SiteOper from app.application.chain.data import get_chain_site_port
from app.application.configuration import get_configured_system_config from app.application.configuration import get_configured_system_config
from app.application.rss import RssHelper from app.application.rss import RssHelper
from app.application.torrent import TorrentHelper from app.application.torrent import TorrentHelper
@@ -804,7 +804,7 @@ class TorrentsChain(ChainBase):
# 获取过期rss除去passkey部分 # 获取过期rss除去passkey部分
new_rss = re.sub(r'&passkey=([a-zA-Z0-9]+)', f'&passkey={new_passkey}', site.get("rss")) new_rss = re.sub(r'&passkey=([a-zA-Z0-9]+)', f'&passkey={new_passkey}', site.get("rss"))
logger.info(f"更新站点 {domain} RSS地址 ...") logger.info(f"更新站点 {domain} RSS地址 ...")
SiteOper().update_rss(domain=domain, rss=new_rss) get_chain_site_port().update_rss(domain=domain, rss=new_rss)
else: else:
# 发送消息 # 发送消息
self.post_message( self.post_message(
@@ -6,7 +6,7 @@
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本 > 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
> 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文 > 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文
> 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md` > 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md`
> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口。 > 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口
## 当前复核结论(2026-08-24 ## 当前复核结论(2026-08-24
@@ -169,6 +169,15 @@
- 兼容边界不变:音乐订阅公开方法、数据库 `SubscribeOper``SubscribePortProxy`、字段写入与插件调用 - 兼容边界不变:音乐订阅公开方法、数据库 `SubscribeOper``SubscribePortProxy`、字段写入与插件调用
合同均未修改。 合同均未修改。
### 长期整改阶段 15:站点 Chain 数据端口收口(2026-08-24
- `SiteChain``TorrentsChain` 原先把 `SitePortProxy` 别名为 `SiteOper`,覆盖 CookieCloud、用户数据、
站点增删改、认证后刷新和 RSS 地址更新。现在全部统一通过 `get_chain_site_port()` 获取同一组合根端口。
- `/sites` 交互测试不再修改 Proxy 类属性,而是替换 getter 返回的 repository;架构门禁同时覆盖两个
消费者,禁止站点域重新引入 `SitePortProxy`
- 兼容边界不变:数据库 `SiteOper``SitePortProxy``SiteChain/TorrentsChain` 公开方法、站点事件、
CookieCloud/RSS 语义和插件调用方式均未改动。
### 总体判断 ### 总体判断
当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**: 当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**:
+1 -1
View File
@@ -131,7 +131,7 @@ Session. `app/db/adapters/` is the concrete persistence-adapter layer: it may
depend on Application-owned Protocols, UoW/Session and Oper implementations. depend on Application-owned Protocols, UoW/Session and Oper implementations.
This deliberate dependency inversion is the only `DB implementation -> This deliberate dependency inversion is the only `DB implementation ->
Application contract` direction; Application must remain free of DB imports. Application contract` direction; Application must remain free of DB imports.
Migrated workflow, user, interaction, messaging and music Chain consumers use Migrated workflow, user, interaction, messaging, music and site Chain consumers use
the named `get_chain_*_port()` functions from `app/application/chain/data.py`; the named `get_chain_*_port()` functions from `app/application/chain/data.py`;
they must not alias migration-time `*PortProxy` classes back to database Oper they must not alias migration-time `*PortProxy` classes back to database Oper
names. Those proxy classes remain compatibility boundaries while the other names. Those proxy classes remain compatibility boundaries while the other
+19
View File
@@ -467,6 +467,25 @@ def test_music_chain_uses_explicit_subscribe_data_port_getter():
assert violations == [] assert violations == []
def test_site_chains_use_explicit_site_data_port_getter():
"""站点与种子链不得把 SitePortProxy 伪装成 SiteOper。"""
paths = [APP_ROOT / "chain" / "site.py", APP_ROOT / "chain" / "torrents.py"]
violations: list[str] = []
for path in paths:
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
for node in ast.walk(tree):
if not isinstance(node, ast.ImportFrom):
continue
if node.module != "app.application.chain.data":
continue
if any(alias.name == "SitePortProxy" for alias in node.names):
violations.append(
f"{path.relative_to(PROJECT_ROOT).as_posix()}:{node.lineno}"
)
assert violations == []
def test_plugin_components_do_not_reexport_legacy_abi_names(): def test_plugin_components_do_not_reexport_legacy_abi_names():
"""新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。""" """新插件组件只提供 canonical 能力,不得复制旧 Helper、Manager 或 Oper 导出。"""
violations: list[str] = [] violations: list[str] = []
+4 -1
View File
@@ -209,7 +209,10 @@ class TestSlashCommandInteractions(unittest.TestCase):
) )
] ]
with patch("app.chain.site.SiteOper.list", return_value=fake_sites), patch.object( with patch(
"app.chain.site.get_chain_site_port",
return_value=SimpleNamespace(list=lambda: fake_sites),
), patch.object(
chain, "post_message" chain, "post_message"
) as post_message: ) as post_message:
chain.remote_list(channel=NotificationChannel.Web, userid="u1", source="web") chain.remote_list(channel=NotificationChannel.Web, userid="u1", source="web")