mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
refactor(messaging): 拆分用户交互模块到 application/messaging 层
- 新增 application/messaging 交互层:router.py 统一会话优先级与回调分发, site/subscribe/skill/media/plugin 各交互状态与视图从 Chain 迁出 - MessageChain 改为通过 InteractionRouter 派发文本会话与按钮回调, 新增结构化 callback_data 通道(兼容 CALLBACK: 文本前缀) - Transfer 失败重试/AI 接管回调归入 TransferChain - MediaInteractionChain 拆出为 app/chain/interaction.py(旧路径保留兼容别名) - WebAgent Endpoint 去重,统一使用 agent.py 回调协议函数 - 删除 app/chain/skills.py(交互逻辑并入 SkillInteractionHandler) - 同步更新架构文档与测试,全量 4476 通过
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+223
-2099
File diff suppressed because it is too large
Load Diff
+16
-505
@@ -17,14 +17,9 @@ from app.adapters.network.browser import PlaywrightHelper
|
||||
from app.adapters.network.cloudflare import under_challenge
|
||||
from app.application.security.cookie import CookieHelper
|
||||
from app.adapters.external.cookiecloud import CookieCloudHelper
|
||||
from app.application.messaging.interaction import (
|
||||
SlashInteractionManager,
|
||||
build_navigation_buttons,
|
||||
format_markdown_table,
|
||||
page_items,
|
||||
supports_interaction_buttons,
|
||||
supports_markdown,
|
||||
update_or_post_message,
|
||||
from app.application.messaging.site import (
|
||||
SiteInteractionHandler,
|
||||
site_interaction_manager,
|
||||
)
|
||||
from app.application.rss import RssHelper
|
||||
from app.runtime.log import logger
|
||||
@@ -37,7 +32,6 @@ from app.foundation import size as size_tools
|
||||
from app.foundation import url as url_tools
|
||||
from app.foundation.dom import DomUtils
|
||||
|
||||
site_interaction_manager = SlashInteractionManager()
|
||||
|
||||
|
||||
class SiteChain(ChainBase):
|
||||
@@ -45,8 +39,6 @@ class SiteChain(ChainBase):
|
||||
站点管理处理链
|
||||
"""
|
||||
|
||||
_button_page_size = 6
|
||||
_text_page_size = 10
|
||||
|
||||
def __init__(self):
|
||||
"""初始化站点管理处理链及特殊站点测试器"""
|
||||
@@ -756,6 +748,10 @@ class SiteChain(ChainBase):
|
||||
return False, f"无法打开网站!"
|
||||
return True, "连接成功"
|
||||
|
||||
def _interaction_handler(self) -> "SiteInteractionHandler":
|
||||
"""构造 /sites 交互处理器,Cookie 更新动作由本链提供。"""
|
||||
return SiteInteractionHandler(messenger=self, cookie_updater=self.update_cookie)
|
||||
|
||||
def remote_list(
|
||||
self,
|
||||
arg_str: str = "",
|
||||
@@ -764,30 +760,10 @@ class SiteChain(ChainBase):
|
||||
source: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
/sites 统一入口。
|
||||
/sites 统一入口,委托交互处理器。
|
||||
"""
|
||||
request = site_interaction_manager.create_or_replace(
|
||||
user_id=userid,
|
||||
command="/sites",
|
||||
channel=channel,
|
||||
source=source,
|
||||
username=None,
|
||||
)
|
||||
normalized_arg = (arg_str or "").strip()
|
||||
if normalized_arg and self.handle_text_interaction(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username="",
|
||||
text=normalized_arg,
|
||||
):
|
||||
return
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username="",
|
||||
return self._interaction_handler().remote_list(
|
||||
arg_str=arg_str, channel=channel, userid=userid, source=source
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -795,12 +771,7 @@ class SiteChain(ChainBase):
|
||||
"""
|
||||
解析 /sites 按钮回调。
|
||||
"""
|
||||
if not callback_data.startswith("sites:"):
|
||||
return None
|
||||
parts = callback_data.split(":")
|
||||
if len(parts) < 3:
|
||||
return None
|
||||
return parts[1], parts[2]
|
||||
return SiteInteractionHandler.parse_callback(callback_data)
|
||||
|
||||
def handle_callback_interaction(
|
||||
self,
|
||||
@@ -812,59 +783,9 @@ class SiteChain(ChainBase):
|
||||
original_message_id: Optional[Union[str, int]] = None,
|
||||
original_chat_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
处理 /sites 按钮交互。
|
||||
"""
|
||||
parsed = self.parse_callback(callback_data)
|
||||
if not parsed:
|
||||
return False
|
||||
|
||||
request_id, action = parsed
|
||||
request = site_interaction_manager.get_by_id(request_id, userid)
|
||||
if not request:
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="站点交互已失效,请重新发送 /sites",
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
request.channel = channel
|
||||
request.source = source
|
||||
request.username = username
|
||||
|
||||
if action == "close":
|
||||
site_interaction_manager.remove(request.request_id)
|
||||
update_or_post_message(
|
||||
chain=self,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="站点管理",
|
||||
text="站点交互已结束",
|
||||
original_message_id=original_message_id,
|
||||
original_chat_id=original_chat_id,
|
||||
)
|
||||
return True
|
||||
|
||||
if action == "page-prev":
|
||||
request.page = max(0, request.page - 1)
|
||||
request.awaiting_input = None
|
||||
elif action == "page-next":
|
||||
request.page += 1
|
||||
request.awaiting_input = None
|
||||
elif action in {"cookie", "enable", "disable"}:
|
||||
request.awaiting_input = action
|
||||
elif action == "refresh":
|
||||
request.awaiting_input = None
|
||||
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
"""委托交互处理器处理按钮回调。"""
|
||||
return self._interaction_handler().handle_callback_interaction(
|
||||
callback_data=callback_data,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
@@ -872,7 +793,6 @@ class SiteChain(ChainBase):
|
||||
original_message_id=original_message_id,
|
||||
original_chat_id=original_chat_id,
|
||||
)
|
||||
return True
|
||||
|
||||
def handle_text_interaction(
|
||||
self,
|
||||
@@ -882,424 +802,15 @@ class SiteChain(ChainBase):
|
||||
username: str,
|
||||
text: str,
|
||||
) -> bool:
|
||||
"""
|
||||
处理 /sites 文本补充输入。
|
||||
"""
|
||||
request = site_interaction_manager.get_by_user(userid)
|
||||
if not request:
|
||||
return False
|
||||
|
||||
request.channel = channel
|
||||
request.source = source
|
||||
request.username = username
|
||||
|
||||
normalized = (text or "").strip()
|
||||
lowered = normalized.lower()
|
||||
|
||||
if lowered in {"退出", "关闭", "q", "quit", "exit"}:
|
||||
site_interaction_manager.remove(request.request_id)
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="站点交互已结束",
|
||||
save_history=False,
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"取消", "cancel", "返回", "back"}:
|
||||
request.awaiting_input = None
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"刷新", "refresh", "列表", "list"}:
|
||||
request.awaiting_input = None
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"p", "prev", "上一页"}:
|
||||
request.awaiting_input = None
|
||||
request.page = max(0, request.page - 1)
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"n", "next", "下一页"}:
|
||||
request.awaiting_input = None
|
||||
request.page += 1
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
cookie_match = re.match(
|
||||
r"^(?:cookie|更新cookie|更新\s*cookie)\s+(.+)$",
|
||||
normalized,
|
||||
re.IGNORECASE,
|
||||
)
|
||||
enable_match = re.match(r"^(?:启用|enable)\s+(.+)$", normalized, re.IGNORECASE)
|
||||
disable_match = re.match(
|
||||
r"^(?:禁用|disable)\s+(.+)$", normalized, re.IGNORECASE
|
||||
)
|
||||
|
||||
if request.awaiting_input == "cookie":
|
||||
success, message = self._update_site_cookie_from_input(normalized)
|
||||
request.awaiting_input = None
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=message,
|
||||
)
|
||||
)
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if request.awaiting_input == "enable":
|
||||
success, message = self._set_sites_enabled(normalized, enabled=True)
|
||||
request.awaiting_input = None
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=message,
|
||||
)
|
||||
)
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if request.awaiting_input == "disable":
|
||||
success, message = self._set_sites_enabled(normalized, enabled=False)
|
||||
request.awaiting_input = None
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=message,
|
||||
)
|
||||
)
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if cookie_match:
|
||||
success, message = self._update_site_cookie_from_input(cookie_match.group(1))
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=message,
|
||||
)
|
||||
)
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if enable_match:
|
||||
success, message = self._set_sites_enabled(enable_match.group(1), enabled=True)
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=message,
|
||||
)
|
||||
)
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if disable_match:
|
||||
success, message = self._set_sites_enabled(
|
||||
disable_match.group(1), enabled=False
|
||||
)
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=message,
|
||||
)
|
||||
)
|
||||
self._render_site_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=self._site_usage_hint(request.awaiting_input),
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
def _render_site_interaction(
|
||||
self,
|
||||
request,
|
||||
channel: MessageChannel,
|
||||
source: Optional[str],
|
||||
userid: Union[str, int],
|
||||
username: Optional[str],
|
||||
original_message_id: Optional[Union[str, int]] = None,
|
||||
original_chat_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
渲染 /sites 当前页面。
|
||||
"""
|
||||
site_list = SiteOper().list()
|
||||
page_size = self._button_page_size if supports_interaction_buttons(channel) else self._text_page_size
|
||||
page_sites, page, total_pages = page_items(site_list, request.page, page_size)
|
||||
request.page = page
|
||||
|
||||
if site_list:
|
||||
body = self._format_site_list(page_sites, channel=channel)
|
||||
footer = [
|
||||
f"第 {page + 1}/{total_pages} 页,共 {len(site_list)} 个站点",
|
||||
self._site_prompt(request.awaiting_input),
|
||||
self._site_usage_hint(request.awaiting_input),
|
||||
]
|
||||
text = "\n\n".join([body, *[line for line in footer if line]])
|
||||
else:
|
||||
text = "当前没有任何站点。\n\n输入 `退出` 结束交互。"
|
||||
|
||||
buttons = None
|
||||
if supports_interaction_buttons(channel):
|
||||
buttons = build_navigation_buttons("sites", request, page, total_pages)
|
||||
buttons.extend(
|
||||
[
|
||||
[
|
||||
{
|
||||
"text": "更新 Cookie",
|
||||
"callback_data": f"sites:{request.request_id}:cookie",
|
||||
},
|
||||
{
|
||||
"text": "禁用站点",
|
||||
"callback_data": f"sites:{request.request_id}:disable",
|
||||
},
|
||||
{
|
||||
"text": "启用站点",
|
||||
"callback_data": f"sites:{request.request_id}:enable",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": "刷新列表",
|
||||
"callback_data": f"sites:{request.request_id}:refresh",
|
||||
},
|
||||
{
|
||||
"text": "关闭",
|
||||
"callback_data": f"sites:{request.request_id}:close",
|
||||
},
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
update_or_post_message(
|
||||
chain=self,
|
||||
"""委托交互处理器处理文本输入。"""
|
||||
return self._interaction_handler().handle_text_interaction(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="站点管理",
|
||||
text=text,
|
||||
buttons=buttons,
|
||||
original_message_id=original_message_id,
|
||||
original_chat_id=original_chat_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _format_site_list(
|
||||
site_list: List[Site], channel: Optional[MessageChannel]
|
||||
) -> str:
|
||||
"""
|
||||
根据渠道能力格式化站点列表。
|
||||
"""
|
||||
if supports_markdown(channel):
|
||||
rows = [
|
||||
[
|
||||
site.id,
|
||||
site.name,
|
||||
"启用" if site.is_active else "禁用",
|
||||
"已配置" if site.cookie else "未配置",
|
||||
"是" if site.render else "否",
|
||||
site.domain or site_rules.extract_domain(site.url or ""),
|
||||
]
|
||||
for site in site_list
|
||||
]
|
||||
return format_markdown_table(
|
||||
headers=["ID", "站点", "状态", "Cookie", "渲染", "域名"],
|
||||
rows=rows,
|
||||
)
|
||||
|
||||
lines = []
|
||||
for site in site_list:
|
||||
lines.append(
|
||||
f"{site.id}. {site.name} | 状态:{'启用' if site.is_active else '禁用'}"
|
||||
f" | Cookie:{'已配置' if site.cookie else '未配置'}"
|
||||
f" | 渲染:{'是' if site.render else '否'}"
|
||||
f" | 域名:{site.domain or site_rules.extract_domain(site.url or '')}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _site_prompt(awaiting_input: Optional[str]) -> str:
|
||||
"""
|
||||
返回当前输入模式提示。
|
||||
"""
|
||||
if awaiting_input == "cookie":
|
||||
return "当前操作:更新站点 Cookie,请输入:<id> <username> <password> [2fa_code/secret]"
|
||||
if awaiting_input == "enable":
|
||||
return "当前操作:启用站点,请输入站点 ID,多个 ID 用空格分隔。"
|
||||
if awaiting_input == "disable":
|
||||
return "当前操作:禁用站点,请输入站点 ID,多个 ID 用空格分隔。"
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _site_usage_hint(awaiting_input: Optional[str]) -> str:
|
||||
"""
|
||||
返回 /sites 的文本操作提示。
|
||||
"""
|
||||
if awaiting_input == "cookie":
|
||||
return "输入站点 ID、用户名、密码和可选 2FA;输入 `取消` 返回列表,输入 `退出` 结束交互。"
|
||||
if awaiting_input in {"enable", "disable"}:
|
||||
return "输入一个或多个站点 ID;输入 `取消` 返回列表,输入 `退出` 结束交互。"
|
||||
return (
|
||||
"可输入:`cookie <id> <username> <password> [2fa]`、`启用 <id...>`、`禁用 <id...>`、"
|
||||
"`n`、`p`、`刷新`、`退出`。"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_site_ids(arg_str: str) -> List[int]:
|
||||
"""
|
||||
从输入中提取站点 ID。
|
||||
"""
|
||||
return [int(item) for item in re.findall(r"\d+", arg_str or "")]
|
||||
|
||||
def _set_sites_enabled(self, arg_str: str, enabled: bool) -> Tuple[bool, str]:
|
||||
"""
|
||||
批量启用或禁用站点。
|
||||
"""
|
||||
site_ids = self._parse_site_ids(arg_str)
|
||||
if not site_ids:
|
||||
return False, "请输入至少一个有效的站点 ID"
|
||||
|
||||
siteoper = SiteOper()
|
||||
changed = []
|
||||
missing = []
|
||||
for site_id in site_ids:
|
||||
site = siteoper.get(site_id)
|
||||
if not site:
|
||||
missing.append(str(site_id))
|
||||
continue
|
||||
siteoper.update(site_id, {"is_active": enabled})
|
||||
changed.append(site.name)
|
||||
|
||||
action = "启用" if enabled else "禁用"
|
||||
if not changed and missing:
|
||||
return False, f"未找到站点:{', '.join(missing)}"
|
||||
|
||||
message = f"已{action} {len(changed)} 个站点"
|
||||
if changed:
|
||||
message += f":{', '.join(changed)}"
|
||||
if missing:
|
||||
message += f";未找到:{', '.join(missing)}"
|
||||
return True, message
|
||||
|
||||
def _update_site_cookie_from_input(self, arg_str: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
根据输入更新单个站点 Cookie。
|
||||
"""
|
||||
args = str(arg_str or "").split()
|
||||
if len(args) not in {3, 4} or not args[0].isdigit():
|
||||
return (
|
||||
False,
|
||||
"格式错误,请输入:cookie <id> <username> <password> [2fa_code/secret]",
|
||||
)
|
||||
|
||||
site_id = int(args[0])
|
||||
site_info = SiteOper().get(site_id)
|
||||
if not site_info:
|
||||
return False, f"站点编号 {site_id} 不存在"
|
||||
|
||||
status, msg = self.update_cookie(
|
||||
site_info=site_info,
|
||||
username=args[1],
|
||||
password=args[2],
|
||||
two_step_code=args[3] if len(args) == 4 else None,
|
||||
)
|
||||
if not status:
|
||||
logger.error(msg)
|
||||
return False, f"【{site_info.name}】Cookie&UA 更新失败:{msg}"
|
||||
return True, f"【{site_info.name}】Cookie&UA 更新成功"
|
||||
|
||||
def remote_disable(self, arg_str: str, channel: MessageChannel,
|
||||
userid: Union[str, int] = None, source: Optional[str] = None):
|
||||
|
||||
-1078
File diff suppressed because it is too large
Load Diff
+16
-626
@@ -32,14 +32,9 @@ from app.db.models.subscribe import Subscribe
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.application.messaging.interaction import (
|
||||
SlashInteractionManager,
|
||||
build_navigation_buttons,
|
||||
format_markdown_table,
|
||||
page_items,
|
||||
supports_interaction_buttons,
|
||||
supports_markdown,
|
||||
update_or_post_message,
|
||||
from app.application.messaging.subscribe import (
|
||||
SubscribeInteractionHandler,
|
||||
subscribe_interaction_manager,
|
||||
)
|
||||
from app.application.mediaserver import MediaServerHelper
|
||||
from app.application.subscribe import add_subscribe, async_add_subscribe
|
||||
@@ -53,7 +48,6 @@ from app.schemas.types import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MediaS
|
||||
from app.domain.media import MUSIC_SUBSCRIBABLE_TYPES
|
||||
from app.schemas.media import build_media_key, normalize_media_source, resolve_media_identity
|
||||
|
||||
subscribe_interaction_manager = SlashInteractionManager()
|
||||
|
||||
|
||||
def _normalize_music_total_tracks(value: Any) -> Optional[int]:
|
||||
@@ -142,8 +136,6 @@ class SubscribeChain(ChainBase):
|
||||
_rlock = threading.RLock()
|
||||
# 避免莫名原因导致长时间持有锁
|
||||
_LOCK_TIMOUT = 3600 * 2
|
||||
_button_page_size = 6
|
||||
_text_page_size = 10
|
||||
|
||||
@staticmethod
|
||||
def __normalize_episode_priority(episode_priority: Optional[dict]) -> Dict[str, int]:
|
||||
@@ -3251,6 +3243,10 @@ class SubscribeChain(ChainBase):
|
||||
"season": subscribe.season,
|
||||
})
|
||||
|
||||
def _interaction_handler(self) -> "SubscribeInteractionHandler":
|
||||
"""构造 /subscribes 交互处理器,业务动作由本链提供。"""
|
||||
return SubscribeInteractionHandler(messenger=self, actions=self)
|
||||
|
||||
def remote_list(
|
||||
self,
|
||||
arg_str: str = "",
|
||||
@@ -3259,30 +3255,10 @@ class SubscribeChain(ChainBase):
|
||||
source: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
/subscribes 统一入口。
|
||||
/subscribes 统一入口,委托交互处理器。
|
||||
"""
|
||||
request = subscribe_interaction_manager.create_or_replace(
|
||||
user_id=userid,
|
||||
command="/subscribes",
|
||||
channel=channel,
|
||||
source=source,
|
||||
username=None,
|
||||
)
|
||||
normalized_arg = (arg_str or "").strip()
|
||||
if normalized_arg and self.handle_text_interaction(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username="",
|
||||
text=normalized_arg,
|
||||
):
|
||||
return
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username="",
|
||||
return self._interaction_handler().remote_list(
|
||||
arg_str=arg_str, channel=channel, userid=userid, source=source
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -3290,12 +3266,7 @@ class SubscribeChain(ChainBase):
|
||||
"""
|
||||
解析 /subscribes 按钮回调。
|
||||
"""
|
||||
if not callback_data.startswith("subscribes:"):
|
||||
return None
|
||||
parts = callback_data.split(":")
|
||||
if len(parts) < 3:
|
||||
return None
|
||||
return parts[1], parts[2]
|
||||
return SubscribeInteractionHandler.parse_callback(callback_data)
|
||||
|
||||
def handle_callback_interaction(
|
||||
self,
|
||||
@@ -3307,65 +3278,9 @@ class SubscribeChain(ChainBase):
|
||||
original_message_id: Optional[Union[str, int]] = None,
|
||||
original_chat_id: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
处理 /subscribes 按钮交互。
|
||||
"""
|
||||
parsed = self.parse_callback(callback_data)
|
||||
if not parsed:
|
||||
return False
|
||||
|
||||
request_id, action = parsed
|
||||
request = subscribe_interaction_manager.get_by_id(request_id, userid)
|
||||
if not request:
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="订阅交互已失效,请重新发送 /subscribes",
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
request.channel = channel
|
||||
request.source = source
|
||||
request.username = username
|
||||
|
||||
if action == "close":
|
||||
subscribe_interaction_manager.remove(request.request_id)
|
||||
update_or_post_message(
|
||||
chain=self,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="订阅管理",
|
||||
text="订阅交互已结束",
|
||||
original_message_id=original_message_id,
|
||||
original_chat_id=original_chat_id,
|
||||
)
|
||||
return True
|
||||
|
||||
if action == "page-prev":
|
||||
request.page = max(0, request.page - 1)
|
||||
request.awaiting_input = None
|
||||
elif action == "page-next":
|
||||
request.page += 1
|
||||
request.awaiting_input = None
|
||||
elif action in {"search", "delete"}:
|
||||
request.awaiting_input = action
|
||||
elif action == "refresh":
|
||||
request.awaiting_input = None
|
||||
self._run_refresh_action(channel, source, userid, username)
|
||||
elif action == "refresh-list":
|
||||
request.awaiting_input = None
|
||||
elif action == "metadata":
|
||||
request.awaiting_input = None
|
||||
self._run_metadata_refresh_action(channel, source, userid, username)
|
||||
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
"""委托交互处理器处理按钮回调。"""
|
||||
return self._interaction_handler().handle_callback_interaction(
|
||||
callback_data=callback_data,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
@@ -3373,7 +3288,6 @@ class SubscribeChain(ChainBase):
|
||||
original_message_id=original_message_id,
|
||||
original_chat_id=original_chat_id,
|
||||
)
|
||||
return True
|
||||
|
||||
def handle_text_interaction(
|
||||
self,
|
||||
@@ -3383,539 +3297,15 @@ class SubscribeChain(ChainBase):
|
||||
username: str,
|
||||
text: str,
|
||||
) -> bool:
|
||||
"""
|
||||
处理 /subscribes 文本补充输入。
|
||||
"""
|
||||
request = subscribe_interaction_manager.get_by_user(userid)
|
||||
if not request:
|
||||
return False
|
||||
|
||||
request.channel = channel
|
||||
request.source = source
|
||||
request.username = username
|
||||
|
||||
normalized = (text or "").strip()
|
||||
lowered = normalized.lower()
|
||||
|
||||
if lowered in {"退出", "关闭", "q", "quit", "exit"}:
|
||||
subscribe_interaction_manager.remove(request.request_id)
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="订阅交互已结束",
|
||||
save_history=False,
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"取消", "cancel", "返回", "back"}:
|
||||
request.awaiting_input = None
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"刷新列表", "列表", "list"}:
|
||||
request.awaiting_input = None
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"刷新", "refresh"}:
|
||||
request.awaiting_input = None
|
||||
self._run_refresh_action(channel, source, userid, username)
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"元数据", "刷新元数据", "metadata"}:
|
||||
request.awaiting_input = None
|
||||
self._run_metadata_refresh_action(channel, source, userid, username)
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"p", "prev", "上一页"}:
|
||||
request.awaiting_input = None
|
||||
request.page = max(0, request.page - 1)
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if lowered in {"n", "next", "下一页"}:
|
||||
request.awaiting_input = None
|
||||
request.page += 1
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
search_match = re.match(r"^(?:搜索|search)\s+(.+)$", normalized, re.IGNORECASE)
|
||||
delete_match = re.match(r"^(?:删除|delete)\s+(.+)$", normalized, re.IGNORECASE)
|
||||
|
||||
if request.awaiting_input == "search":
|
||||
success, message = self._run_search_action(
|
||||
normalized, channel, source, userid, username
|
||||
)
|
||||
request.awaiting_input = None
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=message,
|
||||
)
|
||||
)
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if request.awaiting_input == "delete":
|
||||
success, message = self._delete_subscribes(normalized)
|
||||
request.awaiting_input = None
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=message,
|
||||
)
|
||||
)
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if search_match:
|
||||
success, message = self._run_search_action(
|
||||
search_match.group(1), channel, source, userid, username
|
||||
)
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=message,
|
||||
)
|
||||
)
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
if delete_match:
|
||||
success, message = self._delete_subscribes(delete_match.group(1))
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=message,
|
||||
)
|
||||
)
|
||||
self._render_subscribe_interaction(
|
||||
request=request,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=self._subscribe_usage_hint(request.awaiting_input),
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
def _render_subscribe_interaction(
|
||||
self,
|
||||
request,
|
||||
channel: MessageChannel,
|
||||
source: Optional[str],
|
||||
userid: Union[str, int],
|
||||
username: Optional[str],
|
||||
original_message_id: Optional[Union[str, int]] = None,
|
||||
original_chat_id: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
渲染 /subscribes 当前页面。
|
||||
"""
|
||||
subscribes = SubscribeOper().list()
|
||||
page_size = (
|
||||
self._button_page_size
|
||||
if supports_interaction_buttons(channel)
|
||||
else self._text_page_size
|
||||
)
|
||||
page_subscribes, page, total_pages = page_items(
|
||||
subscribes, request.page, page_size
|
||||
)
|
||||
request.page = page
|
||||
|
||||
if subscribes:
|
||||
body = self._format_subscribe_list(page_subscribes, channel=channel)
|
||||
footer = [
|
||||
f"第 {page + 1}/{total_pages} 页,共 {len(subscribes)} 个订阅",
|
||||
self._subscribe_prompt(request.awaiting_input),
|
||||
self._subscribe_usage_hint(request.awaiting_input),
|
||||
]
|
||||
text = "\n\n".join([body, *[line for line in footer if line]])
|
||||
else:
|
||||
text = "当前没有任何订阅。\n\n输入 `退出` 结束交互。"
|
||||
|
||||
buttons = None
|
||||
if supports_interaction_buttons(channel):
|
||||
buttons = build_navigation_buttons(
|
||||
"subscribes", request, page, total_pages
|
||||
)
|
||||
buttons.extend(
|
||||
[
|
||||
[
|
||||
{
|
||||
"text": "搜索订阅",
|
||||
"callback_data": f"subscribes:{request.request_id}:search",
|
||||
},
|
||||
{
|
||||
"text": "删除订阅",
|
||||
"callback_data": f"subscribes:{request.request_id}:delete",
|
||||
},
|
||||
{
|
||||
"text": "刷新订阅",
|
||||
"callback_data": f"subscribes:{request.request_id}:refresh",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": "刷新元数据",
|
||||
"callback_data": f"subscribes:{request.request_id}:metadata",
|
||||
},
|
||||
{
|
||||
"text": "刷新列表",
|
||||
"callback_data": f"subscribes:{request.request_id}:refresh-list",
|
||||
},
|
||||
{
|
||||
"text": "关闭",
|
||||
"callback_data": f"subscribes:{request.request_id}:close",
|
||||
},
|
||||
],
|
||||
]
|
||||
)
|
||||
|
||||
update_or_post_message(
|
||||
chain=self,
|
||||
"""委托交互处理器处理文本输入。"""
|
||||
return self._interaction_handler().handle_text_interaction(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="订阅管理",
|
||||
text=text,
|
||||
buttons=buttons,
|
||||
original_message_id=original_message_id,
|
||||
original_chat_id=original_chat_id,
|
||||
)
|
||||
|
||||
def _format_subscribe_list(
|
||||
self, subscribes: List[Subscribe], channel: Optional[MessageChannel]
|
||||
) -> str:
|
||||
"""
|
||||
根据渠道能力格式化订阅列表。
|
||||
"""
|
||||
if supports_markdown(channel):
|
||||
rows = [
|
||||
[
|
||||
subscribe.id,
|
||||
subscribe.name,
|
||||
subscribe.type,
|
||||
subscribe.year or "-",
|
||||
self._format_subscribe_progress(subscribe),
|
||||
self._format_subscribe_state(subscribe.state),
|
||||
]
|
||||
for subscribe in subscribes
|
||||
]
|
||||
return format_markdown_table(
|
||||
headers=["ID", "名称", "类型", "年份", "季/进度", "状态"],
|
||||
rows=rows,
|
||||
)
|
||||
|
||||
lines = []
|
||||
for subscribe in subscribes:
|
||||
lines.append(
|
||||
f"{subscribe.id}. {subscribe.name}({subscribe.year or '-'})"
|
||||
f" | {subscribe.type}"
|
||||
f" | {self._format_subscribe_progress(subscribe)}"
|
||||
f" | 状态:{self._format_subscribe_state(subscribe.state)}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _format_subscribe_state(state: Optional[str]) -> str:
|
||||
"""
|
||||
订阅状态显示文本。
|
||||
"""
|
||||
mapping = {
|
||||
"N": "新建",
|
||||
"R": "订阅中",
|
||||
"P": "待定",
|
||||
"S": "暂停",
|
||||
}
|
||||
return mapping.get(state or "", state or "-")
|
||||
|
||||
@staticmethod
|
||||
def _format_subscribe_progress(subscribe: Subscribe) -> str:
|
||||
"""
|
||||
构造订阅的季和进度说明。
|
||||
"""
|
||||
if subscribe.type == MediaType.MOVIE.value:
|
||||
return "电影"
|
||||
season = subscribe.season if subscribe.season is not None else 1
|
||||
if subscribe.total_episode:
|
||||
lack_episode = (
|
||||
subscribe.lack_episode
|
||||
if subscribe.lack_episode is not None
|
||||
else subscribe.total_episode
|
||||
)
|
||||
downloaded = max(subscribe.total_episode - lack_episode, 0)
|
||||
return f"第{season}季 [{downloaded}/{subscribe.total_episode}]"
|
||||
return f"第{season}季"
|
||||
|
||||
@staticmethod
|
||||
def _subscribe_prompt(awaiting_input: Optional[str]) -> str:
|
||||
"""
|
||||
返回当前输入模式提示。
|
||||
"""
|
||||
if awaiting_input == "search":
|
||||
return "当前操作:搜索订阅,请输入订阅 ID,多个 ID 用空格分隔,或输入 all 搜索全部。"
|
||||
if awaiting_input == "delete":
|
||||
return "当前操作:删除订阅,请输入订阅 ID,多个 ID 用空格分隔。"
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _subscribe_usage_hint(awaiting_input: Optional[str]) -> str:
|
||||
"""
|
||||
返回 /subscribes 的文本操作提示。
|
||||
"""
|
||||
if awaiting_input == "search":
|
||||
return "输入订阅 ID 或 all;输入 `取消` 返回列表,输入 `退出` 结束交互。"
|
||||
if awaiting_input == "delete":
|
||||
return "输入一个或多个订阅 ID;输入 `取消` 返回列表,输入 `退出` 结束交互。"
|
||||
return (
|
||||
"可输入:`搜索 <id...|all>`、`删除 <id...>`、`刷新`、`刷新元数据`、`n`、`p`、`退出`。"
|
||||
)
|
||||
|
||||
def _run_refresh_action(
|
||||
self,
|
||||
channel: MessageChannel,
|
||||
source: str,
|
||||
userid: Union[str, int],
|
||||
username: str,
|
||||
) -> None:
|
||||
"""
|
||||
执行订阅刷新。
|
||||
"""
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="开始刷新订阅...",
|
||||
)
|
||||
)
|
||||
self.refresh()
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="订阅刷新执行完成",
|
||||
)
|
||||
)
|
||||
|
||||
def _run_metadata_refresh_action(
|
||||
self,
|
||||
channel: MessageChannel,
|
||||
source: str,
|
||||
userid: Union[str, int],
|
||||
username: str,
|
||||
) -> None:
|
||||
"""
|
||||
执行订阅元数据刷新。
|
||||
"""
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="开始刷新订阅元数据...",
|
||||
)
|
||||
)
|
||||
self.check()
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="订阅元数据刷新完成",
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_subscribe_ids(arg_str: str) -> List[int]:
|
||||
"""
|
||||
从输入中提取订阅 ID。
|
||||
"""
|
||||
return [int(item) for item in re.findall(r"\d+", arg_str or "")]
|
||||
|
||||
def _run_search_action(
|
||||
self,
|
||||
arg_str: str,
|
||||
channel: MessageChannel,
|
||||
source: str,
|
||||
userid: Union[str, int],
|
||||
username: str,
|
||||
) -> Tuple[bool, str]:
|
||||
"""
|
||||
手动执行订阅搜索。
|
||||
"""
|
||||
normalized = (arg_str or "").strip()
|
||||
if not normalized or normalized.lower() in {"all", "全部", "所有"}:
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="开始搜索所有订阅...",
|
||||
)
|
||||
)
|
||||
self.search(state="N,R,P", manual=True)
|
||||
return True, "所有订阅搜索完成"
|
||||
|
||||
subscribe_ids = self._parse_subscribe_ids(normalized)
|
||||
if not subscribe_ids:
|
||||
return False, "请输入订阅 ID,多个 ID 用空格分隔,或输入 all"
|
||||
|
||||
subscribeoper = SubscribeOper()
|
||||
missing = []
|
||||
searched = []
|
||||
for subscribe_id in subscribe_ids:
|
||||
subscribe = subscribeoper.get(subscribe_id)
|
||||
if not subscribe:
|
||||
missing.append(str(subscribe_id))
|
||||
continue
|
||||
self.post_message(
|
||||
schemas.Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=f"开始搜索订阅【{subscribe.name}】...",
|
||||
)
|
||||
)
|
||||
self.search(sid=subscribe_id, manual=True)
|
||||
searched.append(subscribe.name)
|
||||
|
||||
if not searched and missing:
|
||||
return False, f"未找到订阅:{', '.join(missing)}"
|
||||
|
||||
message = f"已完成 {len(searched)} 个订阅搜索"
|
||||
if searched:
|
||||
message += f":{', '.join(searched)}"
|
||||
if missing:
|
||||
message += f";未找到:{', '.join(missing)}"
|
||||
return True, message
|
||||
|
||||
def _delete_subscribes(self, arg_str: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
批量删除订阅。
|
||||
"""
|
||||
subscribe_ids = self._parse_subscribe_ids(arg_str)
|
||||
if not subscribe_ids:
|
||||
return False, "请输入至少一个有效的订阅 ID"
|
||||
|
||||
subscribeoper = SubscribeOper()
|
||||
deleted = []
|
||||
missing = []
|
||||
for subscribe_id in subscribe_ids:
|
||||
subscribe = subscribeoper.get(subscribe_id)
|
||||
if not subscribe:
|
||||
missing.append(str(subscribe_id))
|
||||
continue
|
||||
deleted.append(subscribe.name)
|
||||
subscribeoper.delete(subscribe_id)
|
||||
MoviePilotServerHelper.sub_done_async(
|
||||
{
|
||||
"media_source": subscribe.media_source,
|
||||
"media_id": subscribe.media_id,
|
||||
"season": subscribe.season,
|
||||
}
|
||||
)
|
||||
|
||||
if not deleted and missing:
|
||||
return False, f"未找到订阅:{', '.join(missing)}"
|
||||
|
||||
message = f"已删除 {len(deleted)} 个订阅"
|
||||
if deleted:
|
||||
message += f":{', '.join(deleted)}"
|
||||
if missing:
|
||||
message += f";未找到:{', '.join(missing)}"
|
||||
return True, message
|
||||
|
||||
def remote_delete(self, arg_str: str, channel: MessageChannel,
|
||||
userid: Union[str, int] = None, source: Optional[str] = None):
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import List, Optional, Tuple, Union, Dict, Callable, Any
|
||||
|
||||
from app import schemas
|
||||
from app.agent.orchestrator import ReplyMode, agent_manager, prompt_manager
|
||||
from app.agent.prompt.transfer_redo import build_manual_redo_prompt
|
||||
from app.chain import ChainBase
|
||||
from app.chain.media import MediaChain
|
||||
from app.chain.storage import StorageChain
|
||||
@@ -4480,6 +4481,209 @@ class TransferChain(ChainBase, ConfigReloadMixin, metaclass=Singleton):
|
||||
"""
|
||||
return self.__re_transfer(logid=history_id)
|
||||
|
||||
@staticmethod
|
||||
def parse_failed_transfer_callback(
|
||||
callback_data: str,
|
||||
) -> Optional[tuple[str, int]]:
|
||||
"""
|
||||
解析整理失败通知按钮回调。
|
||||
"""
|
||||
for prefix, action in (
|
||||
("transfer_retry_", "retry"),
|
||||
("transfer_ai_retry_", "ai_retry"),
|
||||
):
|
||||
if callback_data.startswith(prefix):
|
||||
history_id = callback_data.replace(prefix, "", 1)
|
||||
if history_id.isdigit():
|
||||
return action, int(history_id)
|
||||
return None
|
||||
|
||||
def handle_failed_transfer_callback(
|
||||
self,
|
||||
*,
|
||||
callback_data: str,
|
||||
channel: MessageChannel,
|
||||
source: str,
|
||||
userid: Union[str, int],
|
||||
username: str,
|
||||
) -> bool:
|
||||
"""
|
||||
处理整理失败通知中的重试类按钮。
|
||||
"""
|
||||
callback = self.parse_failed_transfer_callback(callback_data)
|
||||
if not callback:
|
||||
return False
|
||||
|
||||
action, history_id = callback
|
||||
if action == "retry":
|
||||
self._retry_transfer_history(
|
||||
history_id=history_id,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
else:
|
||||
self._take_over_transfer_history_by_ai(
|
||||
history_id=history_id,
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
)
|
||||
return True
|
||||
|
||||
def _retry_transfer_history(
|
||||
self,
|
||||
history_id: int,
|
||||
channel: MessageChannel,
|
||||
source: str,
|
||||
userid: Union[str, int],
|
||||
username: str,
|
||||
) -> None:
|
||||
"""
|
||||
立即重新整理一条失败的整理记录。
|
||||
"""
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=f"开始重新整理记录 #{history_id} ...",
|
||||
save_history=False,
|
||||
)
|
||||
)
|
||||
|
||||
state, errmsg = self.redo_transfer_history(history_id)
|
||||
if state:
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=f"整理记录 #{history_id} 已重新整理",
|
||||
link=settings.MP_DOMAIN("#/history"),
|
||||
save_history=False,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="重新整理失败",
|
||||
text=errmsg,
|
||||
link=settings.MP_DOMAIN("#/history"),
|
||||
save_history=False,
|
||||
)
|
||||
)
|
||||
|
||||
def _take_over_transfer_history_by_ai(
|
||||
self,
|
||||
history_id: int,
|
||||
channel: MessageChannel,
|
||||
source: str,
|
||||
userid: Union[str, int],
|
||||
username: str,
|
||||
) -> None:
|
||||
"""
|
||||
由智能助手接管一条失败的整理记录。
|
||||
"""
|
||||
|
||||
if not settings.AI_AGENT_ENABLE:
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="MoviePilot智能助手未启用,请在系统设置中启用",
|
||||
save_history=False,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
history = TransferHistoryOper().get(history_id)
|
||||
if not history:
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="重新整理失败",
|
||||
text=f"整理记录 #{history_id} 不存在",
|
||||
link=settings.MP_DOMAIN("#/history"),
|
||||
save_history=False,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
redo_prompt = build_manual_redo_prompt(history)
|
||||
|
||||
self.post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title=f"已将整理记录 #{history_id} 交给智能助手处理",
|
||||
text="处理完成后会在这里回复结果。",
|
||||
link=settings.MP_DOMAIN("#/history"),
|
||||
save_history=False,
|
||||
)
|
||||
)
|
||||
|
||||
async def _run_ai_takeover():
|
||||
final_output = ""
|
||||
|
||||
def _capture_output(text_output: str):
|
||||
nonlocal final_output
|
||||
final_output = text_output or ""
|
||||
|
||||
try:
|
||||
await agent_manager.run_background_prompt(
|
||||
message=redo_prompt,
|
||||
session_prefix=f"__agent_manual_redo_{history_id}",
|
||||
output_callback=_capture_output,
|
||||
reply_mode=ReplyMode.CAPTURE_ONLY,
|
||||
allow_message_tools=False,
|
||||
)
|
||||
await self.async_post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="智能助手整理完成",
|
||||
text=final_output.strip()
|
||||
or f"整理记录 #{history_id} 已由智能助手处理完成。",
|
||||
link=settings.MP_DOMAIN("#/history"),
|
||||
save_history=False,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
await self.async_post_message(
|
||||
Notification(
|
||||
channel=channel,
|
||||
source=source,
|
||||
userid=userid,
|
||||
username=username,
|
||||
title="智能助手整理失败",
|
||||
text=str(e),
|
||||
link=settings.MP_DOMAIN("#/history"),
|
||||
save_history=False,
|
||||
)
|
||||
)
|
||||
|
||||
asyncio.run_coroutine_threadsafe(_run_ai_takeover(), global_vars.loop)
|
||||
|
||||
|
||||
def __re_transfer(
|
||||
self,
|
||||
logid: int,
|
||||
|
||||
Reference in New Issue
Block a user