mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
refactor: reorganize backend module boundaries
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
from queue import Queue
|
||||
from threading import Lock
|
||||
from typing import Callable, Iterable, Optional, Union
|
||||
|
||||
from app.schemas.types import MessageChannel
|
||||
|
||||
|
||||
_WEB_AGENT_EDIT_QUEUES: dict[str, list[Queue[dict]]] = {}
|
||||
_WEB_AGENT_EDIT_LOCK = Lock()
|
||||
_ChannelAdminResolver = Callable[[Optional[dict]], Iterable[Union[str, int]]]
|
||||
_CHANNEL_ADMIN_RESOLVERS: dict[str, _ChannelAdminResolver] = {}
|
||||
|
||||
|
||||
def register_channel_admin_resolver(
|
||||
channel: Union[MessageChannel, str],
|
||||
resolver: _ChannelAdminResolver,
|
||||
) -> None:
|
||||
"""
|
||||
注册消息渠道的管理员主体 ID 解析器。
|
||||
|
||||
:param channel: 消息渠道
|
||||
:param resolver: 由渠道配置解析全部管理员主体 ID 的函数
|
||||
"""
|
||||
channel_value = channel.value if isinstance(channel, MessageChannel) else str(channel)
|
||||
_CHANNEL_ADMIN_RESOLVERS[channel_value] = resolver
|
||||
|
||||
|
||||
def resolve_config_principal_ids(
|
||||
config: Optional[dict],
|
||||
*config_keys: str,
|
||||
) -> set[str]:
|
||||
"""
|
||||
从渠道自行声明的配置键中解析主体 ID。
|
||||
|
||||
:param config: 当前消息渠道配置
|
||||
:param config_keys: 由渠道模块维护的主体 ID 配置键
|
||||
:return: 去空白后的主体 ID 集合
|
||||
"""
|
||||
principal_ids = set()
|
||||
for config_key in config_keys:
|
||||
principal_ids.update(
|
||||
item.strip()
|
||||
for item in str((config or {}).get(config_key) or "").split(",")
|
||||
if item.strip()
|
||||
)
|
||||
return principal_ids
|
||||
|
||||
|
||||
def matches_channel_admin(
|
||||
channel: Union[MessageChannel, str],
|
||||
config: Optional[dict],
|
||||
*principal_ids: Optional[Union[str, int]],
|
||||
) -> bool:
|
||||
"""
|
||||
按渠道配置中的稳定主体 ID 判断管理员身份。
|
||||
|
||||
:param channel: 消息渠道
|
||||
:param config: 当前消息渠道配置
|
||||
:param principal_ids: 消息渠道提供的稳定用户主体 ID
|
||||
:return: 任一用户主体 ID 命中渠道注册的管理员集合时返回 True
|
||||
"""
|
||||
channel_value = channel.value if isinstance(channel, MessageChannel) else str(channel)
|
||||
resolver = _CHANNEL_ADMIN_RESOLVERS.get(channel_value)
|
||||
if not resolver:
|
||||
return False
|
||||
authorized_ids = {
|
||||
str(principal_id).strip()
|
||||
for principal_id in resolver(config)
|
||||
if principal_id is not None and str(principal_id).strip()
|
||||
}
|
||||
if not authorized_ids:
|
||||
return False
|
||||
candidates = {
|
||||
str(principal_id).strip()
|
||||
for principal_id in principal_ids
|
||||
if principal_id is not None and str(principal_id).strip()
|
||||
}
|
||||
return bool(authorized_ids.intersection(candidates))
|
||||
|
||||
|
||||
def normalize_web_agent_button_rows(buttons: Optional[list[list[dict]]]) -> list[list[dict]]:
|
||||
"""
|
||||
将消息按钮转换为 WebAgent 前端可识别的按钮行。
|
||||
|
||||
:param buttons: 传统消息模块返回的按钮二维数组
|
||||
:return: WebAgent 前端选项按钮二维数组
|
||||
"""
|
||||
button_rows: list[list[dict]] = []
|
||||
for row in buttons or []:
|
||||
normalized_row = []
|
||||
for button in row or []:
|
||||
label = str(button.get("text") or button.get("label") or "").strip()
|
||||
callback_data = str(button.get("callback_data") or "").strip()
|
||||
if not label or not callback_data:
|
||||
continue
|
||||
normalized_button = {
|
||||
"label": label,
|
||||
"callback_data": callback_data,
|
||||
}
|
||||
if button.get("description"):
|
||||
normalized_button["description"] = str(button.get("description"))
|
||||
normalized_row.append(normalized_button)
|
||||
if normalized_row:
|
||||
button_rows.append(normalized_row)
|
||||
return button_rows
|
||||
|
||||
|
||||
def _resolve_web_agent_choice_id(
|
||||
message_id: Union[str, int],
|
||||
button_rows: list[list[dict]],
|
||||
) -> str:
|
||||
"""
|
||||
从按钮回调中提取稳定的 WebAgent 选项 ID。
|
||||
|
||||
:param message_id: 前端助手消息 ID
|
||||
:param button_rows: 已规范化的按钮行
|
||||
:return: 选项卡片 ID
|
||||
"""
|
||||
for row in button_rows:
|
||||
for button in row:
|
||||
callback_data = str(button.get("callback_data") or "").strip()
|
||||
if not callback_data:
|
||||
continue
|
||||
parts = callback_data.split(":")
|
||||
if len(parts) >= 2 and parts[1]:
|
||||
return parts[1]
|
||||
return callback_data
|
||||
return str(message_id)
|
||||
|
||||
|
||||
def build_web_agent_message_update_event(
|
||||
*,
|
||||
message_id: Union[str, int],
|
||||
title: Optional[str],
|
||||
text: str,
|
||||
buttons: Optional[list[list[dict]]],
|
||||
) -> dict:
|
||||
"""
|
||||
构造 WebAgent 原消息更新事件。
|
||||
|
||||
:param message_id: 前端助手消息 ID
|
||||
:param title: 更新后的标题
|
||||
:param text: 更新后的正文
|
||||
:param buttons: 更新后的按钮
|
||||
:return: 前端可应用到原消息的 SSE 事件
|
||||
"""
|
||||
button_rows = normalize_web_agent_button_rows(buttons)
|
||||
content_parts = [part for part in (title, text) if part]
|
||||
target_message = {
|
||||
"id": str(message_id),
|
||||
"content": "" if button_rows else "\n\n".join(content_parts),
|
||||
"choices": [],
|
||||
"attachments": [],
|
||||
"tools": [],
|
||||
"status": "done",
|
||||
}
|
||||
if button_rows:
|
||||
target_message["choices"].append({
|
||||
"id": _resolve_web_agent_choice_id(message_id, button_rows),
|
||||
"title": title,
|
||||
"prompt": text or "",
|
||||
"buttons": [button for row in button_rows for button in row],
|
||||
"button_rows": button_rows,
|
||||
"status": "pending",
|
||||
})
|
||||
return {
|
||||
"type": "message_update",
|
||||
"target_message": target_message,
|
||||
}
|
||||
|
||||
|
||||
def attach_web_agent_edit_queue(user_id: str, edit_queue: Queue[dict]) -> None:
|
||||
"""
|
||||
为当前 WebAgent 请求挂载原消息编辑事件队列。
|
||||
|
||||
:param user_id: 当前用户 ID
|
||||
:param edit_queue: 用于接收编辑事件的队列
|
||||
"""
|
||||
with _WEB_AGENT_EDIT_LOCK:
|
||||
_WEB_AGENT_EDIT_QUEUES.setdefault(str(user_id), []).append(edit_queue)
|
||||
|
||||
|
||||
def detach_web_agent_edit_queue(user_id: str, edit_queue: Queue[dict]) -> None:
|
||||
"""
|
||||
移除当前 WebAgent 请求的原消息编辑事件队列。
|
||||
|
||||
:param user_id: 当前用户 ID
|
||||
:param edit_queue: 需要移除的队列
|
||||
"""
|
||||
with _WEB_AGENT_EDIT_LOCK:
|
||||
queues = _WEB_AGENT_EDIT_QUEUES.get(str(user_id))
|
||||
if not queues:
|
||||
return
|
||||
_WEB_AGENT_EDIT_QUEUES[str(user_id)] = [
|
||||
item for item in queues if item is not edit_queue
|
||||
]
|
||||
if not _WEB_AGENT_EDIT_QUEUES[str(user_id)]:
|
||||
_WEB_AGENT_EDIT_QUEUES.pop(str(user_id), None)
|
||||
|
||||
|
||||
def dispatch_web_agent_edit_event(
|
||||
*,
|
||||
user_id: str,
|
||||
event: dict,
|
||||
) -> bool:
|
||||
"""
|
||||
将 WebAgent 原消息编辑事件分发给正在等待的请求队列。
|
||||
|
||||
:param user_id: 当前用户 ID
|
||||
:param event: 前端可应用的 SSE 事件
|
||||
:return: 是否存在接收本次编辑事件的请求队列
|
||||
"""
|
||||
with _WEB_AGENT_EDIT_LOCK:
|
||||
queues = list(_WEB_AGENT_EDIT_QUEUES.get(str(user_id)) or [])
|
||||
for edit_queue in queues:
|
||||
edit_queue.put(event)
|
||||
return bool(queues)
|
||||
|
||||
|
||||
def edit_web_agent_message(
|
||||
*,
|
||||
user_id: str,
|
||||
message_id: Union[str, int],
|
||||
title: Optional[str],
|
||||
text: str,
|
||||
buttons: Optional[list[list[dict]]] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
原地更新 WebAgent 前端消息卡片。
|
||||
|
||||
:param user_id: 当前用户 ID
|
||||
:param message_id: 前端助手消息 ID
|
||||
:param title: 更新后的标题
|
||||
:param text: 更新后的正文
|
||||
:param buttons: 更新后的按钮
|
||||
:return: 是否已投递编辑事件
|
||||
"""
|
||||
if not user_id:
|
||||
return False
|
||||
event = build_web_agent_message_update_event(
|
||||
message_id=message_id,
|
||||
title=title,
|
||||
text=text,
|
||||
buttons=buttons,
|
||||
)
|
||||
return dispatch_web_agent_edit_event(user_id=user_id, event=event)
|
||||
Reference in New Issue
Block a user