Merge remote-tracking branch 'origin/v3' into v3

# Conflicts:
#	app/api/endpoints/agent.py
#	app/api/endpoints/anthropic.py
#	app/api/endpoints/openai.py
#	app/chain/__init__.py
#	app/chain/message.py
#	app/chain/site.py
#	app/chain/subscribe.py
#	app/chain/transfer.py
#	app/modules/discord/__init__.py
#	app/modules/qqbot/__init__.py
#	app/modules/slack/__init__.py
#	app/modules/telegram/__init__.py
#	app/modules/wechat/__init__.py
#	app/runtime/extensions/module_manager.py
#	app/runtime/extensions/service_registry.py
#	tests/test_agent_interaction.py
#	tests/test_slash_command_interactions.py
#	tests/test_web_agent_stream.py
This commit is contained in:
jxxghp
2026-08-16 19:44:43 +08:00
232 changed files with 21375 additions and 6683 deletions
+148 -19
View File
@@ -1,6 +1,81 @@
from app.agent.orchestrator import agent_manager
from typing import Any
from app.agent.runtime_loader import (
activate_agent_service,
begin_agent_shutdown,
get_agent_manager as get_runtime_agent_manager,
get_running_agent_manager as get_runtime_running_agent_manager,
is_tool_factory_materialized,
reconcile_agent_service,
)
from app.application.agent import register_agent_service_providers
from app.runtime.config import settings
from app.runtime.events import Event, eventmanager
from app.runtime.log import logger
from app.schemas.types import EventType
# 嵌入式启动器可显式注入 manager;常规进程使用 Capability Runtime。
agent_manager: Any = None
def _event_changed_keys(event: Event | None) -> set[str]:
"""兼容对象和 dict 两种配置事件载荷。"""
if event is None:
return set()
event_data = event.event_data
if isinstance(event_data, dict):
keys = event_data.get("key", set())
else:
keys = getattr(event_data, "key", set())
if isinstance(keys, str):
return {keys}
return {str(key) for key in (keys or set())}
def _get_agent_manager() -> Any:
"""兼容显式注入对象,否则按需解析 canonical manager。"""
return agent_manager if agent_manager is not None else get_runtime_agent_manager()
def _get_running_agent_manager() -> Any | None:
"""只返回已运行实例,状态探测不得触发 Agent 物化。"""
if agent_initializer._compat_injected:
return agent_initializer._manager
return get_runtime_running_agent_manager()
def _get_prompt_manager() -> Any:
"""首个提示词调用才导入模板管理器。"""
from app.agent.prompt import prompt_manager
return prompt_manager
def _get_capability_manager() -> Any:
"""首个多模态调用才导入 Agent 能力管理器。"""
from app.agent.llm import AgentCapabilityManager
return AgentCapabilityManager
def _get_llm_helper() -> Any:
"""首个模型能力查询才导入 LLM helper。"""
from app.agent.llm import LLMHelper
return LLMHelper
def _get_manual_redo_prompt_builder() -> Any:
"""首个整理接管请求才导入对应提示词构建器。"""
from app.agent.prompt.transfer_redo import build_manual_redo_prompt
return build_manual_redo_prompt
async def _handle_agent_config_changed(event: Event) -> None:
"""把配置事件交给当前全局 initializer,避免监听器持有过期实例。"""
await agent_initializer.handle_config_changed(event)
class AgentInitializer:
@@ -10,17 +85,33 @@ class AgentInitializer:
def __init__(self):
self._initialized = False
self._manager: Any = None
self._compat_injected = False
self._shutdown_complete = False
eventmanager.add_event_listener(
EventType.ConfigChanged,
_handle_agent_config_changed,
)
async def initialize(self) -> bool:
"""
初始化AI智能体管理器
"""
try:
if not settings.AI_AGENT_ENABLE:
logger.info("AI智能体功能未启用")
return True
await agent_manager.initialize()
self._shutdown_complete = False
if agent_manager is not None:
if not settings.AI_AGENT_ENABLE:
logger.info("AI智能体功能未启用")
return True
self._manager = agent_manager
self._compat_injected = True
await agent_manager.initialize()
else:
self._manager = await activate_agent_service()
self._compat_injected = False
if self._manager is None:
logger.info("AI智能体功能未启用")
return True
self._initialized = True
logger.info("AI智能体管理器初始化成功")
return True
@@ -29,16 +120,38 @@ class AgentInitializer:
logger.error(f"AI智能体管理器初始化失败: {e}")
return False
async def cleanup(self) -> None:
"""
清理AI智能体管理器
"""
async def handle_config_changed(self, event: Event) -> None:
"""仅在 manifest watch 命中时协调 service,关闭态保持 fail closed。"""
changed_keys = _event_changed_keys(event)
if not changed_keys or self._compat_injected or self._shutdown_complete:
return
try:
if not self._initialized:
return
await agent_manager.close()
self._manager = await reconcile_agent_service(
reason="agent_service_config_changed",
changed_keys=changed_keys,
retry=True,
)
self._initialized = self._manager is not None
except Exception as error:
self._manager = None
self._initialized = False
logger.info("AI智能体管理器已关闭")
logger.debug(f"配置变更协调AI智能体失败: {error}")
async def cleanup(self) -> None:
"""清理 initializer 引用;显式注入对象同时在此关闭。"""
try:
manager = self._manager
compat_injected = self._compat_injected
if manager is None:
return
try:
if compat_injected:
await manager.close()
logger.info("AI智能体管理器已关闭")
finally:
self._initialized = False
self._manager = None
self._compat_injected = False
except Exception as e:
logger.debug(f"关闭AI智能体管理器时发生错误: {e}")
@@ -47,16 +160,22 @@ class AgentInitializer:
# 全局AI智能体初始化器实例
agent_initializer = AgentInitializer()
# application 门面仅保存 provider;下列注册不会导入 Agent 实现。
register_agent_service_providers(
agent_manager_provider=_get_agent_manager,
running_agent_manager_provider=_get_running_agent_manager,
prompt_manager_provider=_get_prompt_manager,
capability_manager_provider=_get_capability_manager,
llm_helper_provider=_get_llm_helper,
manual_redo_prompt_builder_provider=_get_manual_redo_prompt_builder,
)
async def init_agent() -> bool:
"""
在应用事件循环中初始化AI智能体。
"""
try:
if not settings.AI_AGENT_ENABLE:
logger.info("AI智能体功能未启用")
return True
return await agent_initializer.initialize()
except Exception as e:
@@ -69,6 +188,16 @@ async def stop_agent():
停止AI智能体(异步版本,用于在应用关闭时调用)
"""
try:
await agent_initializer.cleanup()
if not agent_initializer._shutdown_complete:
if agent_initializer._compat_injected:
await agent_initializer.cleanup()
else:
await begin_agent_shutdown()
await agent_initializer.cleanup()
agent_initializer._shutdown_complete = True
if is_tool_factory_materialized():
from app.agent.tools.base import shutdown_blocking_executors
shutdown_blocking_executors(cancel_futures=True)
except Exception as e:
logger.error(f"停止AI智能体时发生错误: {e}")
+4
View File
@@ -1,5 +1,9 @@
from app.application.commands import register_command_class
from app.command import Command
# 导入期即向 application 门面注册命令类,保证工具调用时不依赖静态边。
register_command_class(Command)
def init_command():
"""
@@ -0,0 +1,47 @@
"""Managed Resource 的启动组合与进程关闭入口。"""
from __future__ import annotations
import threading
from typing import Optional
from app.runtime.capabilities.runtime import CapabilityRuntime
from app.runtime.extensions.managed_resource_adapter import (
AsyncManagedResourceAdapter,
SyncManagedResourceAdapter,
build_managed_resource_registry,
)
from app.runtime.managed_resources import (
MANAGED_RESOURCE_ASYNC_KIND,
MANAGED_RESOURCE_SYNC_KIND,
configure_managed_resource_runtime,
)
_runtime_lock = threading.RLock()
_managed_resource_runtime: Optional[CapabilityRuntime] = None
def init_managed_resources() -> CapabilityRuntime:
"""构建并注入资源 Runtime;只发现声明,不物化或启动任何资源。"""
global _managed_resource_runtime
with _runtime_lock:
if _managed_resource_runtime is None:
_managed_resource_runtime = CapabilityRuntime(
build_managed_resource_registry(),
adapters={
MANAGED_RESOURCE_SYNC_KIND: SyncManagedResourceAdapter(),
MANAGED_RESOURCE_ASYNC_KIND: AsyncManagedResourceAdapter(),
},
)
configure_managed_resource_runtime(_managed_resource_runtime)
return _managed_resource_runtime
async def stop_managed_resources() -> None:
"""关闭已经初始化的资源 Runtime;未初始化时不执行发现或激活。"""
with _runtime_lock:
runtime = _managed_resource_runtime
if runtime is None:
return
await runtime.shutdown_async(reason="application_shutdown")
+16 -5
View File
@@ -22,7 +22,6 @@ from app.runtime.extensions.module_manager import ModuleManager
from app.runtime.events import EventManager
from app.runtime.state import SystemHelper
from app.runtime.thread import ThreadHelper
from app.adapters.system.display import DisplayHelper
from app.adapters.network.doh import DohHelper
from app.adapters.system.resource import (
ResourceHelper,
@@ -36,6 +35,10 @@ from app.command import CommandChain
from app.schemas import Message, MessageType
from app.schemas.types import SystemConfigKey
from app.startup.agent_initializer import init_agent, stop_agent
from app.startup.managed_resources_initializer import (
init_managed_resources,
stop_managed_resources,
)
from app.application.security.access import set_superuser_token_payload_provider
from app.application.security.auth import build_superuser_token_payload
from app.application.image import configure_wallpaper_providers
@@ -170,6 +173,13 @@ def update_resources() -> None:
logger.error(f"资源更新完成但自动重启失败:{message}")
def close_browser_sessions() -> None:
"""在托管资源关闭前释放所有浏览器上下文及其工作线程。"""
from app.adapters.network.browser import BrowserSessionHelper
BrowserSessionHelper.close_all_sessions()
async def stop_modules():
"""
服务关闭
@@ -184,9 +194,10 @@ async def stop_modules():
logger.error(f"关闭{name}失败:{err}")
await run_step("AI智能体", stop_agent)
await run_step("模块", lambda: ModuleManager().stop())
await run_step("模块", lambda: ModuleManager().shutdown())
await run_step("事件消费", lambda: EventManager().stop())
await run_step("虚拟显示", lambda: DisplayHelper().stop())
await run_step("浏览器会话", close_browser_sessions)
await run_step("托管资源", stop_managed_resources)
await run_step("DoH服务", lambda: DohHelper().shutdown())
await run_step("线程池", lambda: ThreadHelper().shutdown())
await run_step("消息服务", stop_message)
@@ -201,12 +212,12 @@ async def init_modules():
"""
启动模块
"""
# 托管资源只在这里装配声明与 adapter,具体资源仍由首个消费者显式激活。
init_managed_resources()
# 应用服务不反向依赖 Chain,由启动组合层注入壁纸来源。
configure_wallpaper_services()
# 认证访问层不反向依赖数据库实现,由启动组合层注入载荷提供器。
set_superuser_token_payload_provider(build_superuser_token_payload)
# 虚拟显示
DisplayHelper()
# DoH
DohHelper()
# 站点管理
+15
View File
@@ -1,25 +1,40 @@
from pathlib import Path
from app.runtime.compat.diagnostics import (
configure_legacy_import_diagnostics,
scan_plugin_legacy_imports,
)
from app.runtime.compat.resource_imports import scan_plugin_resource_imports
from app.runtime.config import global_vars
from app.runtime.extensions.plugin_manager import (
PluginManager,
configure_plugin_install_reporter,
configure_plugin_legacy_import_services,
configure_plugin_resource_import_preparer,
configure_site_auth_level_provider,
)
from app.runtime.managed_resources import acquire_managed_resource
from app.application.site.sites import SitesHelper # pylint: disable=no-name-in-module
from app.adapters.external.server import MoviePilotServerHelper
from app.runtime.log import logger
def _prepare_legacy_plugin_import(*, plugin_id: str, plugin_dir: Path) -> None:
"""在执行旧插件顶层代码前准备其静态导入所需的宿主资源。"""
for capability_id in scan_plugin_resource_imports(plugin_id, plugin_dir):
acquire_managed_resource(
capability_id,
reason="legacy_plugin_import",
)
def _configure_plugin_services() -> None:
"""把兼容诊断、远程上报和站点认证等级装配到插件管理器。"""
configure_plugin_legacy_import_services(
diagnostics_configurator=configure_legacy_import_diagnostics,
import_scanner=scan_plugin_legacy_imports,
)
configure_plugin_resource_import_preparer(_prepare_legacy_plugin_import)
configure_plugin_install_reporter(MoviePilotServerHelper.install_plugin_reg)
configure_site_auth_level_provider(lambda: SitesHelper().auth_level)
+4
View File
@@ -1,5 +1,9 @@
from app.application.scheduling import register_scheduler_class
from app.scheduler import Scheduler
# 导入期即向 application 门面注册调度器类,保证工具调用时不依赖静态边。
register_scheduler_class(Scheduler)
def init_scheduler():
"""