refactor(chain): 处理链功能域 mixin 化,清理未使用导入并根治兼容层循环导入

- ChainBase 拆分为 RecognitionMixin/MessageProcessingMixin/NotificationMixin
- TransferChain 拆分为 7 个功能 mixin(_mixins.py),SubscribeChain 音乐订阅域拆出 _music.py
- 斜杠命令交互四件套收敛为 InteractionChainMixin 委托,会话管理器移至 application 层,chain 层不再 re-export
- 模块基础类收敛到 app/modules/_base(notification/mediaserver 语义重命名)
- 清理 app/chain/__init__.py 24 个未使用导入,修正 49 处测试 patch 目标到实际命名空间
- 兼容层 legacy 符号不再并入 __all__,根治 schemas 初始化反向拉起 application.transfer 的循环导入
- 修复 bangumi 集数为字符串时 set_bangumi_info 抛 TypeError
- 新增重复代码等架构门禁测试;capability 清单校验排除下划线内部目录
This commit is contained in:
jxxghp
2026-08-16 16:30:16 +08:00
parent 24671f8f18
commit 7e851dbfa7
102 changed files with 6041 additions and 5888 deletions
+2 -17
View File
@@ -6,7 +6,6 @@ import traceback
import uuid
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Callable, Dict, List, Optional
from fastapi.concurrency import run_in_threadpool
@@ -68,7 +67,7 @@ from app.agent.tools.impl.mcp import (
select_legacy_mcp_tools,
)
from app.agent.tools.impl.query_system_settings import QuerySystemSettingsTool
from app.chain import ChainBase
from app.chain.agent import AgentChain
from app.runtime.config import settings
from app.runtime.events import eventmanager
from app.runtime.extensions.plugin_manager import PluginManager
@@ -77,17 +76,12 @@ from app.db.oper.agenttask import AgentTaskOper
from app.db.oper.user import UserOper
from app.runtime.log import logger
from app.schemas import AgentLLMProviderEventData, AgentTokensUsageEventData, Notification, NotificationType
from app.schemas.agent import ReplyMode
from app.schemas.message import ChannelCapabilityManager, ChannelCapability
from app.schemas.types import ChainEventType, EventType, MessageChannel
from app.foundation.identity import SYSTEM_INTERNAL_USER_ID
class AgentChain(ChainBase):
"""Agent 业务处理链。"""
pass
def _finish_processing_status(status: Optional[dict], user_id: Optional[str] = None) -> None:
"""结束入站消息的渠道处理状态。"""
if not status:
@@ -321,15 +315,6 @@ class _ThinkTagStripper:
self.buffer = ""
class ReplyMode(str, Enum):
"""
Agent 最终回复处理模式。
"""
DISPATCH = "dispatch"
CAPTURE_ONLY = "capture_only"
HEARTBEAT_SESSION_PREFIX = "__agent_heartbeat_"
UNSUPPORTED_IMAGE_INPUT_MESSAGE = "当前模型不支持图片输入,请更换支持图片输入的模型,或在系统设置中关闭图片输入支持后重试。"
AGENT_EXECUTION_ERROR_PREFIX = "智能助手执行失败"
+12 -9
View File
@@ -70,14 +70,14 @@ def reload_plugin_runtime(plugin_id: str) -> None:
重载插件并重新注册其命令、定时任务和 API。
"""
# 这些依赖只在真正执行重载时才导入,避免普通查询工具引入不必要的初始化开销。
from app.api.endpoints.plugin import register_plugin_api
from app.command import Command
from app.scheduler import Scheduler
from app.application.plugins import register_plugin_api
from app.application.commands import init_commands
from app.application.scheduling import update_plugin_job
plugin_manager = PluginManager()
plugin_manager.reload_plugin(plugin_id)
Scheduler().update_plugin_job(plugin_id)
Command().init_commands(plugin_id)
update_plugin_job(plugin_id)
init_commands(plugin_id)
register_plugin_api(plugin_id)
@@ -333,8 +333,11 @@ async def uninstall_plugin_runtime(plugin_id: str) -> dict[str, Any]:
"""
按现有卸载逻辑移除插件,并清理运行态注册与分组信息。
"""
from app.api.endpoints.plugin import _remove_plugin_from_folders, remove_plugin_api
from app.scheduler import Scheduler
from app.application.plugins import (
remove_plugin_api,
remove_plugin_from_folders,
)
from app.application.scheduling import remove_plugin_job
config_oper = SystemConfigOper()
install_plugins = config_oper.get(SystemConfigKey.UserInstalledPlugins) or []
@@ -343,7 +346,7 @@ async def uninstall_plugin_runtime(plugin_id: str) -> dict[str, Any]:
await config_oper.async_set(SystemConfigKey.UserInstalledPlugins, install_plugins)
remove_plugin_api(plugin_id)
Scheduler().remove_plugin_job(plugin_id)
remove_plugin_job(plugin_id)
plugin_manager = PluginManager()
plugin_class = plugin_manager.plugins.get(plugin_id)
@@ -362,7 +365,7 @@ async def uninstall_plugin_runtime(plugin_id: str) -> dict[str, Any]:
except Exception:
clone_files_removed = False
_remove_plugin_from_folders(plugin_id)
remove_plugin_from_folders(plugin_id)
plugin_manager.remove_plugin(plugin_id)
return {
+2 -3
View File
@@ -99,7 +99,7 @@ class CreateAgentTaskTool(MoviePilotTool):
def _create_task(self, payload: CreateAgentTaskInput) -> dict:
"""持久化任务并立即注册到运行时调度器。"""
from app.scheduler import Scheduler
from app.application.scheduling import update_agent_task_job
trigger_value = payload.trigger
if payload.trigger_type == "date" and payload.delay_minutes is not None:
@@ -130,8 +130,7 @@ class CreateAgentTaskTool(MoviePilotTool):
source=self._source or (chat.source if chat else None),
original_chat_id=chat.original_chat_id if chat else None,
)
scheduler = Scheduler()
next_run_at = scheduler.update_agent_task_job(task.id)
next_run_at = update_agent_task_job(task.id)
return AgentTaskOper.to_dict(
task,
next_run_at=next_run_at,
+2 -2
View File
@@ -31,14 +31,14 @@ class DeleteAgentTaskTool(MoviePilotTool):
def _delete_task(self, task_id: int) -> bool:
"""删除当前用户的任务并移除运行时调度。"""
from app.scheduler import Scheduler
from app.application.scheduling import remove_agent_task_job
deleted = AgentTaskOper().delete(
task_id=task_id,
user_id=str(self._user_id),
)
if deleted:
Scheduler().remove_agent_task_job(task_id)
remove_agent_task_job(task_id)
return deleted
async def run(self, task_id: int, **kwargs: object) -> str:
+2 -4
View File
@@ -14,7 +14,6 @@ class ListSlashCommandsInput(BaseModel):
"""查询所有可用斜杠命令工具的输入参数模型"""
class ListSlashCommandsTool(MoviePilotTool):
name: str = "list_slash_commands"
tags: list[str] = [
@@ -41,10 +40,9 @@ class ListSlashCommandsTool(MoviePilotTool):
logger.info(f"执行工具: {self.name}")
try:
from app.command import Command
from app.application.commands import get_commands
command_obj = Command()
all_commands = command_obj.get_commands()
all_commands = get_commands()
if not all_commands:
return "当前没有可用的命令"
+2 -3
View File
@@ -48,7 +48,7 @@ class QueryAgentTasksTool(MoviePilotTool):
enabled: Optional[bool],
) -> list[dict]:
"""读取当前用户的任务及运行时下一次触发时间。"""
from app.scheduler import Scheduler
from app.application.scheduling import get_agent_task_next_run
oper = AgentTaskOper()
if task_id:
@@ -56,12 +56,11 @@ class QueryAgentTasksTool(MoviePilotTool):
tasks = [task] if task else []
else:
tasks = oper.list(user_id=str(self._user_id), enabled=enabled)
scheduler = Scheduler()
result = []
for task in tasks:
data = oper.to_dict(
task,
next_run_at=scheduler.get_agent_task_next_run(task.id),
next_run_at=get_agent_task_next_run(task.id),
timezone=settings.TZ,
)
if task_id:
+5 -3
View File
@@ -39,13 +39,15 @@ class QuerySchedulersTool(MoviePilotTool):
"""查询非 Agent 自主任务的运行时定时服务。"""
logger.info(f"执行工具: {self.name}")
try:
from app.scheduler import AGENT_TASK_JOB_PREFIX, Scheduler
from app.application.scheduling import (
AGENT_TASK_JOB_PREFIX,
list_scheduler_jobs,
)
scheduler = Scheduler()
agent_task_prefix = f"{AGENT_TASK_JOB_PREFIX}-"
schedulers = [
scheduler_item
for scheduler_item in scheduler.list()
for scheduler_item in list_scheduler_jobs()
if not str(scheduler_item.id or "").startswith(agent_task_prefix)
]
if schedulers:
+2 -2
View File
@@ -56,7 +56,7 @@ class RunAgentTaskTool(MoviePilotTool):
async def run(self, task_id: int, **kwargs: object) -> str:
"""立即执行当前用户拥有且已启用的 Agent 自主定时任务。"""
from app.scheduler import Scheduler
from app.application.scheduling import start_agent_task
payload = RunAgentTaskInput(task_id=task_id)
status, task_name = await self.run_blocking(
@@ -70,7 +70,7 @@ class RunAgentTaskTool(MoviePilotTool):
return f"Agent 定时任务 {task_id} 已暂停,请先恢复后再执行"
if status == "running":
return f"Agent 定时任务 {task_id} 正在执行,请勿重复触发"
if not Scheduler().start_agent_task(payload.task_id):
if not start_agent_task(payload.task_id):
return f"Agent 定时任务 {task_id} 尚未注册到运行时调度器,无法立即执行"
return (
f"Agent 定时任务 {task_id} 已提交立即执行:{task_name}"
+7 -5
View File
@@ -46,12 +46,14 @@ class RunSchedulerTool(MoviePilotTool):
@staticmethod
def _run_scheduler_sync(job_id: str) -> tuple[bool, str]:
"""同步触发定时服务,避免调度器扫描阻塞事件循环。"""
from app.scheduler import Scheduler
from app.application.scheduling import (
list_scheduler_jobs,
start_scheduler_job,
)
scheduler = Scheduler()
for scheduler_item in scheduler.list():
for scheduler_item in list_scheduler_jobs():
if scheduler_item.id == job_id:
scheduler.start(job_id)
start_scheduler_job(job_id)
return True, scheduler_item.name
return False, ""
@@ -60,7 +62,7 @@ class RunSchedulerTool(MoviePilotTool):
logger.info(f"执行工具: {self.name}, 参数: job_id={job_id}")
try:
from app.scheduler import AGENT_TASK_JOB_PREFIX
from app.application.scheduling import AGENT_TASK_JOB_PREFIX
if job_id.startswith(f"{AGENT_TASK_JOB_PREFIX}-"):
return (
+4 -5
View File
@@ -57,16 +57,15 @@ class RunSlashCommandTool(MoviePilotTool):
if not command.startswith("/"):
command = f"/{command}"
# 从全局 Command 单例中验证命令是否存在(包含系统预设命令 + 插件命令 + 其他命令)
from app.command import Command
# 从命令注册表中验证命令是否存在(包含系统预设命令 + 插件命令 + 其他命令)
from app.application.commands import get_command, get_commands
cmd_name = command.split()[0]
command_obj = Command()
matched_command = command_obj.get(cmd_name)
matched_command = get_command(cmd_name)
if not matched_command:
# 列出所有可用命令帮助用户
all_commands = command_obj.get_commands()
all_commands = get_commands()
available_cmds = [
f"{cmd} - {info.get('description', '无描述')}"
for cmd, info in all_commands.items()
+2 -3
View File
@@ -100,7 +100,7 @@ class UpdateAgentTaskTool(MoviePilotTool):
def _update_task(self, payload: UpdateAgentTaskInput) -> Optional[dict]:
"""更新当前用户的任务并刷新运行时调度。"""
from app.scheduler import Scheduler
from app.application.scheduling import update_agent_task_job
oper = AgentTaskOper()
task = oper.get(task_id=payload.task_id, user_id=str(self._user_id))
@@ -174,8 +174,7 @@ class UpdateAgentTaskTool(MoviePilotTool):
if current and current.last_status == "running":
return {"error": f"Agent 定时任务 {payload.task_id} 正在执行,请稍后再修改"}
return None
scheduler = Scheduler()
next_run_at = scheduler.update_agent_task_job(payload.task_id)
next_run_at = update_agent_task_job(payload.task_id)
updated_task = oper.get(task_id=payload.task_id, user_id=str(self._user_id))
return oper.to_dict(
updated_task,