fix: route WeKnora embed through backend prefix

This commit is contained in:
Administrator committed 2026-08-13 20:22:50 +08:00
1 parent 6dbaf40f99
commit 54fd0e7316
5 files changed
+81 -17

No files matched your search

@@ -55,6 +55,23 @@ function postThemeToFrame(frame: HTMLIFrameElement | null) {
frame.contentWindow.postMessage(collectThemePayload(), targetOrigin); 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() { export function WeKnoraKnowledgeDetailPage() {
const { const {
knowledge_base_id: strategyKnowledgeBaseId, knowledge_base_id: strategyKnowledgeBaseId,
@@ -88,15 +105,7 @@ export function WeKnoraKnowledgeDetailPage() {
.then((session) => { .then((session) => {
if (cancelled) return; if (cancelled) return;
const framePath = session.frame_path || ""; const framePath = session.frame_path || "";
const backendBase = getBackendBaseURL().replace(/\/+$/, ""); const nextUrl = buildFrameUrl(framePath);
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(/^\/+/, "")}`);
nextUrl.searchParams.set("theme", collectThemePayload().theme); nextUrl.searchParams.set("theme", collectThemePayload().theme);
setFrameSrc(nextUrl.toString()); setFrameSrc(nextUrl.toString());
}) })
@@ -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 # /assets, /locales and /api/v1/*. Register that proxy last so it stays a
# true fallback and cannot intercept DeerFlow APIs. # true fallback and cannot intercept DeerFlow APIs.
app.include_router(llmwiki.proxy_router) app.include_router(llmwiki.proxy_router)
app.include_router(llmwiki.proxy_router, prefix="/deerflow")
return app return app
@@ -48,6 +48,7 @@ proxy_router = APIRouter(tags=["llmwiki-proxy"])
_MAX_UPLOAD_BYTES = 100 * 1024 * 1024 _MAX_UPLOAD_BYTES = 100 * 1024 * 1024
_WEKNORA_ADMIN_SESSION_CACHE: dict[str, dict[str, Any]] = {} _WEKNORA_ADMIN_SESSION_CACHE: dict[str, dict[str, Any]] = {}
_WEKNORA_PUBLIC_PREFIX = "/deerflow"
_HOP_BY_HOP_RESPONSE_HEADERS = { _HOP_BY_HOP_RESPONSE_HEADERS = {
"connection", "connection",
"content-encoding", "content-encoding",
@@ -517,12 +518,7 @@ def _normalize_public_prefix(value: str | None) -> str:
def _request_public_prefix(request: Request) -> str: def _request_public_prefix(request: Request) -> str:
headers = request.headers return _WEKNORA_PUBLIC_PREFIX
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 ""))
def _with_public_prefix(prefix: str, path: str) -> str: 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 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: def _rewrite_weknora_html_public_paths(html: str, public_prefix: str) -> str:
if not public_prefix: if not public_prefix:
return html return html
@@ -1538,7 +1542,10 @@ async def proxy_weknora_root_asset(request: Request) -> Response:
return await _proxy_weknora_request( return await _proxy_weknora_request(
request, request,
target_base_url=runtime.web_base_url, 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, ctx=ctx,
is_api=False, is_api=False,
) )
@@ -41,6 +41,7 @@ class WeKnoraEmbedContext:
def is_weknora_embed_proxy_path(path: str) -> bool: def is_weknora_embed_proxy_path(path: str) -> bool:
path = path.removeprefix("/deerflow")
return ( return (
path == "/platform" path == "/platform"
or path.startswith("/platform/") or path.startswith("/platform/")
@@ -173,15 +173,25 @@ def test_weknora_embed_cookie_bypasses_only_proxy_paths() -> None:
cookies = {"deerflow_weknora_embed": raw_cookie} 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("/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("/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("/config.js", cookies=cookies))
assert auth_middleware._is_weknora_embed_proxy_request( assert auth_middleware._is_weknora_embed_proxy_request(
_request("/tdesign-icons/0.4.1/fonts/index.js", cookies=cookies) _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("/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( assert auth_middleware._is_weknora_embed_proxy_request(
_request("/api/llmwiki/weknora-embed/api/v1/auth/me", cookies=cookies) _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 csrf_middleware.is_weknora_embed_proxy_request(_request("/api/v1/knowledge-search", cookies=cookies))
assert not auth_middleware._is_weknora_embed_proxy_request( assert not auth_middleware._is_weknora_embed_proxy_request(
_request("/api/v1/knowledge-search", cookies={"deerflow_weknora_embed": "signed"}) _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) 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"] cookie = response.headers["set-cookie"]
assert "deerflow_weknora_embed=" in cookie assert "deerflow_weknora_embed=" in cookie
assert "Secure" 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" 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 @pytest.mark.asyncio
async def test_weknora_client_keeps_anonymous_mode_without_admin_credentials() -> None: async def test_weknora_client_keeps_anonymous_mode_without_admin_credentials() -> None:
requests: list[httpx.Request] = [] 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}" == "/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", "/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: def test_weknora_iframe_hides_current_outer_sidebar_shell() -> None: