diff --git a/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx b/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx index ddbff9ac..2f4610e2 100644 --- a/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx +++ b/frontend-web/src/strategy-components/components/resource-management/llmwiki/WeKnoraKnowledgeDetailPage.tsx @@ -55,6 +55,23 @@ function postThemeToFrame(frame: HTMLIFrameElement | null) { frame.contentWindow.postMessage(collectThemePayload(), targetOrigin); } +const WEKNORA_BACKEND_PREFIX = "/deerflow"; + +function buildFrameUrl(framePath: string) { + const backendBase = getBackendBaseURL().replace(/\/+$/, ""); + const backendOrigin = backendBase + ? new URL(backendBase, window.location.origin).origin + : window.location.origin; + if (/^https?:\/\//i.test(framePath)) return new URL(framePath); + if (framePath.startsWith("/")) { + const path = !framePath.startsWith(`${WEKNORA_BACKEND_PREFIX}/`) + ? `${WEKNORA_BACKEND_PREFIX}${framePath}` + : framePath; + return new URL(path, backendOrigin); + } + return new URL(`${backendBase || backendOrigin}/${framePath.replace(/^\/+/, "")}`); +} + export function WeKnoraKnowledgeDetailPage() { const { knowledge_base_id: strategyKnowledgeBaseId, @@ -88,15 +105,7 @@ export function WeKnoraKnowledgeDetailPage() { .then((session) => { if (cancelled) return; const framePath = session.frame_path || ""; - const backendBase = getBackendBaseURL().replace(/\/+$/, ""); - const backendOrigin = backendBase - ? new URL(backendBase, window.location.origin).origin - : window.location.origin; - const nextUrl = /^https?:\/\//i.test(framePath) - ? new URL(framePath) - : framePath.startsWith("/") - ? new URL(framePath, backendOrigin) - : new URL(`${backendBase || backendOrigin}/${framePath.replace(/^\/+/, "")}`); + const nextUrl = buildFrameUrl(framePath); nextUrl.searchParams.set("theme", collectThemePayload().theme); setFrameSrc(nextUrl.toString()); }) diff --git a/offline-backend-20260512/backend/app/gateway/app.py b/offline-backend-20260512/backend/app/gateway/app.py index 2b486c2b..1d3233cf 100644 --- a/offline-backend-20260512/backend/app/gateway/app.py +++ b/offline-backend-20260512/backend/app/gateway/app.py @@ -1647,6 +1647,7 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an # /assets, /locales and /api/v1/*. Register that proxy last so it stays a # true fallback and cannot intercept DeerFlow APIs. app.include_router(llmwiki.proxy_router) + app.include_router(llmwiki.proxy_router, prefix="/deerflow") return app diff --git a/offline-backend-20260512/backend/app/gateway/routers/llmwiki.py b/offline-backend-20260512/backend/app/gateway/routers/llmwiki.py index 5e0800e1..31703a11 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/llmwiki.py +++ b/offline-backend-20260512/backend/app/gateway/routers/llmwiki.py @@ -48,6 +48,7 @@ proxy_router = APIRouter(tags=["llmwiki-proxy"]) _MAX_UPLOAD_BYTES = 100 * 1024 * 1024 _WEKNORA_ADMIN_SESSION_CACHE: dict[str, dict[str, Any]] = {} +_WEKNORA_PUBLIC_PREFIX = "/deerflow" _HOP_BY_HOP_RESPONSE_HEADERS = { "connection", "content-encoding", @@ -517,12 +518,7 @@ def _normalize_public_prefix(value: str | None) -> str: def _request_public_prefix(request: Request) -> str: - headers = request.headers - for key in ("x-forwarded-prefix", "x-script-name"): - prefix = _normalize_public_prefix(headers.get(key)) - if prefix: - return prefix - return _normalize_public_prefix(str(request.scope.get("root_path") or "")) + return _WEKNORA_PUBLIC_PREFIX def _with_public_prefix(prefix: str, path: str) -> str: @@ -531,6 +527,14 @@ def _with_public_prefix(prefix: str, path: str) -> str: return f"{prefix}{path}" if prefix else path +def _without_public_prefix(prefix: str, path: str) -> str: + if prefix and path.startswith(f"{prefix}/"): + return path[len(prefix):] + if prefix and path == prefix: + return "/" + return path + + def _rewrite_weknora_html_public_paths(html: str, public_prefix: str) -> str: if not public_prefix: return html @@ -1538,7 +1542,10 @@ async def proxy_weknora_root_asset(request: Request) -> Response: return await _proxy_weknora_request( request, target_base_url=runtime.web_base_url, - upstream_path=str(request.scope.get("path") or request.url.path), + upstream_path=_without_public_prefix( + _request_public_prefix(request), + str(request.scope.get("path") or request.url.path), + ), ctx=ctx, is_api=False, ) diff --git a/offline-backend-20260512/backend/app/gateway/weknora_embed.py b/offline-backend-20260512/backend/app/gateway/weknora_embed.py index b77070e9..6de5e448 100644 --- a/offline-backend-20260512/backend/app/gateway/weknora_embed.py +++ b/offline-backend-20260512/backend/app/gateway/weknora_embed.py @@ -41,6 +41,7 @@ class WeKnoraEmbedContext: def is_weknora_embed_proxy_path(path: str) -> bool: + path = path.removeprefix("/deerflow") return ( path == "/platform" or path.startswith("/platform/") diff --git a/offline-backend-20260512/backend/tests/test_llmwiki_weknora.py b/offline-backend-20260512/backend/tests/test_llmwiki_weknora.py index 474c8cb1..a0e3480e 100644 --- a/offline-backend-20260512/backend/tests/test_llmwiki_weknora.py +++ b/offline-backend-20260512/backend/tests/test_llmwiki_weknora.py @@ -173,15 +173,25 @@ def test_weknora_embed_cookie_bypasses_only_proxy_paths() -> None: cookies = {"deerflow_weknora_embed": raw_cookie} assert auth_middleware._is_weknora_embed_proxy_request(_request("/platform/knowledge-bases/kb", cookies=cookies)) + assert auth_middleware._is_weknora_embed_proxy_request( + _request("/deerflow/platform/knowledge-bases/kb", cookies=cookies) + ) assert auth_middleware._is_weknora_embed_proxy_request(_request("/assets/index.js", cookies=cookies)) + assert auth_middleware._is_weknora_embed_proxy_request(_request("/deerflow/assets/index.js", cookies=cookies)) assert auth_middleware._is_weknora_embed_proxy_request(_request("/config.js", cookies=cookies)) assert auth_middleware._is_weknora_embed_proxy_request( _request("/tdesign-icons/0.4.1/fonts/index.js", cookies=cookies) ) assert auth_middleware._is_weknora_embed_proxy_request(_request("/api/v1/knowledge-search", cookies=cookies)) + assert auth_middleware._is_weknora_embed_proxy_request( + _request("/deerflow/api/v1/knowledge-search", cookies=cookies) + ) assert auth_middleware._is_weknora_embed_proxy_request( _request("/api/llmwiki/weknora-embed/api/v1/auth/me", cookies=cookies) ) + assert auth_middleware._is_weknora_embed_proxy_request( + _request("/deerflow/api/llmwiki/weknora-embed/api/v1/auth/me", cookies=cookies) + ) assert csrf_middleware.is_weknora_embed_proxy_request(_request("/api/v1/knowledge-search", cookies=cookies)) assert not auth_middleware._is_weknora_embed_proxy_request( _request("/api/v1/knowledge-search", cookies={"deerflow_weknora_embed": "signed"}) @@ -219,7 +229,7 @@ async def test_weknora_session_endpoint_sets_cookie_and_returns_platform_path(mo ) payload = json.loads(response.body) - assert payload["frame_path"] == "/platform/knowledge-bases/remote-kb?tab=wiki" + assert payload["frame_path"] == "/deerflow/platform/knowledge-bases/remote-kb?tab=wiki" cookie = response.headers["set-cookie"] assert "deerflow_weknora_embed=" in cookie assert "Secure" in cookie @@ -259,6 +269,36 @@ async def test_weknora_session_endpoint_preserves_reverse_proxy_prefix(monkeypat assert payload["frame_path"] == "/deerflow/platform/knowledge-bases/remote-kb?tab=wiki" +@pytest.mark.asyncio +async def test_weknora_session_endpoint_uses_backend_prefix_when_frontend_has_different_prefix( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from app.gateway.routers import llmwiki as llmwiki_router + + async def authorized_mapping(*_: object, **__: object) -> tuple[dict[str, str], str, bool]: + return {"id": "local-kb", "weknora_id": "remote-kb", "name": "KB"}, "user-a", False + + class Store: + async def get_authorized(self, *_: object, **__: object) -> dict[str, str]: + return {"id": "local-kb", "weknora_id": "remote-kb"} + + monkeypatch.setattr(llmwiki_router, "_runtime_for_iframe_proxy", lambda _: object()) + monkeypatch.setattr(llmwiki_router, "_authorized_mapping", authorized_mapping) + monkeypatch.setattr(llmwiki_router, "_store", lambda _: Store()) + + response = await llmwiki_router.create_weknora_detail_session( + _request( + "/api/llmwiki/knowledge-bases/local-kb/weknora/session", + request_headers={"Referer": "https://example.test/magentweb/page/workspace/knowledge/local-kb"}, + ), + "local-kb", + tab="wiki", + ) + + payload = json.loads(response.body) + assert payload["frame_path"] == "/deerflow/platform/knowledge-bases/remote-kb?tab=wiki" + + @pytest.mark.asyncio async def test_weknora_client_keeps_anonymous_mode_without_admin_credentials() -> None: requests: list[httpx.Request] = [] @@ -1002,6 +1042,12 @@ def test_weknora_iframe_proxy_is_fallback_after_deerflow_api_routes() -> None: == "/api/llmwiki/weknora-embed/api/v1/{proxied_path:path}" ) assert _first_gateway_match("GET", "/platform/knowledge-bases/remote-kb") == "/platform/{proxied_path:path}" + assert ( + _first_gateway_match("GET", "/deerflow/platform/knowledge-bases/remote-kb") + == "/deerflow/platform/{proxied_path:path}" + ) + assert _first_gateway_match("GET", "/deerflow/assets/index.js") == "/deerflow/assets/{proxied_path:path}" + assert _first_gateway_match("GET", "/deerflow/api/v1/knowledge-search") == "/deerflow/api/v1/{proxied_path:path}" def test_weknora_iframe_hides_current_outer_sidebar_shell() -> None: