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);
}
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());
})
@@ -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
@@ -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,
)
@@ -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/")
@@ -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: