diff --git a/app/adapters/web/plugin/routes.py b/app/adapters/web/plugin/routes.py index c87a942f1..7f3ca4d6a 100644 --- a/app/adapters/web/plugin/routes.py +++ b/app/adapters/web/plugin/routes.py @@ -37,7 +37,6 @@ class FastAPIDynamicRouteRegistry: verify_token: Callable[..., Any], verify_apikey: Callable[..., Any], prefix: str, - protected_routes: set[str], log: Any, event_loop: Callable[[], asyncio.AbstractEventLoop | None] | None = None, ) -> None: @@ -48,7 +47,6 @@ class FastAPIDynamicRouteRegistry: self._verify_token = verify_token self._verify_apikey = verify_apikey self._prefix = prefix - self._protected_routes = protected_routes self._logger = log self._event_loop = event_loop @@ -111,11 +109,6 @@ class FastAPIDynamicRouteRegistry: modified = False removed = False - existing_paths = { - path: route - for route in self._app.routes - if (path := self._route_path(route)) is not None - } plugin_ids = [plugin_id] if plugin_id else self._plugin_ids() for current_id in plugin_ids: if self.remove(current_id): @@ -150,9 +143,8 @@ class FastAPIDynamicRouteRegistry: f"Error adding plugin route {api_path}: {str(error)}" ) if modified: - self.clean(existing_paths) + # 文档路由由 app.adapters.web.docs 一次性注册,这里只让已缓存的文档按新路由表重建。 self._app.openapi_schema = None - self._app.setup() if removed: # 旧路由已不在路由表中,此后不会再有请求把旧端点写回缓存 _clear_fastapi_callable_caches() @@ -184,15 +176,3 @@ class FastAPIDynamicRouteRegistry: """返回公开路由路径,跳过 FastAPI 内部的无路径 include 包装器。""" path = getattr(route, "path", None) return path if isinstance(path, str) else None - - def clean(self, existing_paths: dict) -> None: - """清理 FastAPI 重建时可能重复的受保护文档路由。""" - for protected_route in self._protected_routes: - try: - existing_route = existing_paths.get(protected_route) - if existing_route: - self._app.routes.remove(existing_route) - except Exception as error: - self._logger.error( - f"Error removing protected route {protected_route}: {str(error)}" - ) diff --git a/app/factory.py b/app/factory.py index 87961393d..d75fe6ee9 100644 --- a/app/factory.py +++ b/app/factory.py @@ -404,12 +404,6 @@ def create_app() -> FastAPI: verify_token=verify_token, verify_apikey=verify_apikey, prefix=f"{get_runtime_setting('API_V1_STR')}/plugin", - protected_routes={ - openapi_path, - "/docs", - "/docs/oauth2-redirect", - "/redoc", - }, log=logger, event_loop=lambda: main_loop_registry.current, )) diff --git a/tests/test_api_docs_routes.py b/tests/test_api_docs_routes.py index bd0baa48b..8c1fb36ce 100644 --- a/tests/test_api_docs_routes.py +++ b/tests/test_api_docs_routes.py @@ -3,6 +3,7 @@ import pytest from fastapi.testclient import TestClient +from app.application.plugin import routes as plugin_routes from app.application.settings.contract import build_setting_specs from app.factory import create_app from app.runtime.config import settings @@ -44,6 +45,29 @@ def test_api_docs_follow_runtime_toggle_without_restart(monkeypatch: pytest.Monk assert app.openapi_schema is None +def test_api_docs_survive_plugin_route_updates(monkeypatch: pytest.MonkeyPatch) -> None: + """插件路由增删后文档路由仍在,FastAPI.setup() 不再补注册它们。""" + app = create_app() + registry = plugin_routes._get_route_registry() + # 测试进程没有运行中的主 loop,改为当前线程同步更新路由表。 + monkeypatch.setattr(registry, "_event_loop", None) + monkeypatch.setattr(registry, "_plugin_apis", lambda _plugin_id: [{ + "path": "/DemoPlugin/health", + "endpoint": lambda: {"ok": True}, + "methods": ["GET"], + "allow_anonymous": True, + }]) + monkeypatch.setattr(settings, "API_DOCS_ENABLE", True) + client = TestClient(app) + + registry.update("DemoPlugin", "add") + assert client.get("/api/v1/plugin/DemoPlugin/health").json() == {"ok": True} + registry.update("DemoPlugin", "remove") + + for path in DOC_PATHS: + assert client.get(path).status_code == 200, path + + def test_api_docs_pages_keep_reverse_proxy_prefix(monkeypatch: pytest.MonkeyPatch) -> None: """文档页引用的 OpenAPI 地址带上 root_path,与 FastAPI 自带文档一致。""" monkeypatch.setattr(settings, "API_DOCS_ENABLE", True) diff --git a/tests/test_api_response.py b/tests/test_api_response.py index 2b006ab46..148b95f44 100644 --- a/tests/test_api_response.py +++ b/tests/test_api_response.py @@ -1102,7 +1102,6 @@ def test_plugin_routes_only_register_v1(monkeypatch): verify_token=lambda: None, verify_apikey=lambda: None, prefix="/api/v1/plugin", - protected_routes=set(), log=SimpleNamespace(debug=lambda *_args: None, error=lambda *_args: None), )) @@ -1148,7 +1147,6 @@ async def test_plugin_routes_ignore_included_router_wrappers(): verify_token=lambda: None, verify_apikey=lambda: None, prefix="/api/v1/plugin", - protected_routes=set(), log=SimpleNamespace(debug=lambda *_args: None, error=lambda *_args: None), )) @@ -1188,18 +1186,12 @@ async def test_plugin_route_updates_run_on_application_event_loop() -> None: main_thread = threading.get_ident() mutation_threads: list[int] = [] original_add_api_route = app.router.add_api_route - original_setup = app.setup def record_add_api_route(*args, **kwargs): mutation_threads.append(threading.get_ident()) return original_add_api_route(*args, **kwargs) - def record_setup() -> None: - mutation_threads.append(threading.get_ident()) - original_setup() - app.router.add_api_route = record_add_api_route - app.setup = record_setup loop = asyncio.get_running_loop() registry = FastAPIDynamicRouteRegistry( app=app, @@ -1213,7 +1205,6 @@ async def test_plugin_route_updates_run_on_application_event_loop() -> None: verify_token=lambda: None, verify_apikey=lambda: None, prefix="/api/v1/plugin", - protected_routes=set(), log=SimpleNamespace(debug=lambda *_args: None, error=lambda *_args: None), event_loop=lambda: loop, ) @@ -1237,7 +1228,6 @@ def test_plugin_route_update_rejects_stopped_application_loop() -> None: verify_token=lambda: None, verify_apikey=lambda: None, prefix="/api/v1/plugin", - protected_routes=set(), log=SimpleNamespace(debug=lambda *_args: None, error=lambda *_args: None), event_loop=lambda: None, ) @@ -1261,7 +1251,6 @@ async def test_plugin_route_update_abandons_late_application_loop_callback() -> verify_token=lambda: None, verify_apikey=lambda: None, prefix="/api/v1/plugin", - protected_routes=set(), log=SimpleNamespace(debug=lambda *_args: None, error=lambda *_args: None), event_loop=lambda: loop, ) @@ -1306,7 +1295,6 @@ async def test_started_plugin_route_update_waits_for_terminal_result() -> None: verify_token=lambda: None, verify_apikey=lambda: None, prefix="/api/v1/plugin", - protected_routes=set(), log=SimpleNamespace(debug=lambda *_args: None, error=lambda *_args: None), event_loop=lambda: loop, ) @@ -1434,7 +1422,6 @@ def build_plugin_api_app(monkeypatch) -> FastAPI: verify_token=lambda: None, verify_apikey=lambda: None, prefix="/api/v1/plugin", - protected_routes=set(), log=SimpleNamespace(debug=lambda *_args: None, error=lambda *_args: None), )) plugin_routes.register_plugin_api("DemoPlugin") diff --git a/tests/test_plugin_reload_cache_release.py b/tests/test_plugin_reload_cache_release.py index 15a969ee0..2ec1e5dd8 100644 --- a/tests/test_plugin_reload_cache_release.py +++ b/tests/test_plugin_reload_cache_release.py @@ -54,7 +54,6 @@ def test_removed_plugin_route_releases_endpoint_from_fastapi_caches() -> None: verify_token=lambda: None, verify_apikey=lambda: None, prefix="/api/v1/plugin", - protected_routes=set(), log=_SILENT_LOG, ) registry.update("RouteCacheDemo", "add")