fix(frontend): preserve iframe theme across login
The iframe login used a fixed queryless redirect and called setTheme, which both dropped the iframe theme and persisted it into the normal site's shared theme storage.
This commit is contained in:
1 parent
59f2f56124
commit
75aad4eef6
6 files changed
+186
-29
No files matched your search
@@ -1,3 +1,7 @@
|
|||||||
|
import { resolveEmbedThemeMode } from "./theme-mode.mjs";
|
||||||
|
|
||||||
|
export { resolveEmbedThemeMode } from "./theme-mode.mjs";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 「外部系统 iframe 套用深链」相关的**按标签页**会话开关。承载三个相互独立的能力,多由 URL
|
* 「外部系统 iframe 套用深链」相关的**按标签页**会话开关。承载三个相互独立的能力,多由 URL
|
||||||
* 路由参数开启(独立会话 `isolate` 另可由「外观」设置里的开关在运行时切换,见 `setIsolatedSession`),
|
* 路由参数开启(独立会话 `isolate` 另可由「外观」设置里的开关在运行时切换,见 `setIsolatedSession`),
|
||||||
@@ -85,16 +89,6 @@ export function readIsolateParam(search: string): FlagParam {
|
|||||||
return readFlagParam(search, "isolate");
|
return readFlagParam(search, "isolate");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 把外部传入的主题色映射成离散明/暗。与 `resolveThemeFromQuery` 同源约定(dark-blue → 深色),
|
|
||||||
* 另外宽松接受 `dark` / 中文「深色/深蓝」直传;其余(含空)一律浅色。
|
|
||||||
*/
|
|
||||||
export function resolveEmbedThemeMode(raw: string | null | undefined): EmbedTheme {
|
|
||||||
const v = (raw ?? "").trim().toLowerCase();
|
|
||||||
if (v === "dark" || v === "dark-blue" || v === "深色" || v === "深蓝") return "dark";
|
|
||||||
return "light";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 取 URL query 里的 `theme` 值(embed 主题来源)。 */
|
/** 取 URL query 里的 `theme` 值(embed 主题来源)。 */
|
||||||
export function readEmbedThemeParam(search: string): string {
|
export function readEmbedThemeParam(search: string): string {
|
||||||
return (toSearchParams(search).get("theme") ?? "").trim();
|
return (toSearchParams(search).get("theme") ?? "").trim();
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* @typedef {"light" | "dark"} EmbedThemeMode
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map external theme names to the two themes supported by iframe surfaces.
|
||||||
|
*
|
||||||
|
* @param {string | null | undefined} rawTheme
|
||||||
|
* @returns {EmbedThemeMode}
|
||||||
|
*/
|
||||||
|
export function resolveEmbedThemeMode(rawTheme) {
|
||||||
|
const theme = (rawTheme ?? "").trim().toLowerCase();
|
||||||
|
return theme === "dark" ||
|
||||||
|
theme === "dark-blue" ||
|
||||||
|
theme === "深色" ||
|
||||||
|
theme === "深蓝"
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
}
|
||||||
@@ -2,7 +2,10 @@ import assert from "node:assert/strict";
|
|||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
buildChatsIframeRedirectPath,
|
||||||
|
buildChatsIframeThreadPath,
|
||||||
isChatsIframeWorkspacePath,
|
isChatsIframeWorkspacePath,
|
||||||
|
resolvePostLoginRedirectPath,
|
||||||
resolveForcedThemeForPath,
|
resolveForcedThemeForPath,
|
||||||
} from "./chats-iframe-route.ts";
|
} from "./chats-iframe-route.ts";
|
||||||
|
|
||||||
@@ -48,3 +51,73 @@ test("honors the dark-blue theme query on chats_iframe routes", () => {
|
|||||||
"light",
|
"light",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("forces the iframe theme on the login route without changing normal login", () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveForcedThemeForPath(
|
||||||
|
"/login",
|
||||||
|
"?from=iframe&theme=dark-blue",
|
||||||
|
),
|
||||||
|
"dark",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
resolveForcedThemeForPath(
|
||||||
|
"/login/guest",
|
||||||
|
"?from=iframe&theme=dark-blue",
|
||||||
|
),
|
||||||
|
"dark",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
resolveForcedThemeForPath("/login", "?theme=dark-blue"),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
resolveForcedThemeForPath(
|
||||||
|
"/page/workspace/chats/new",
|
||||||
|
"?from=iframe&theme=dark-blue",
|
||||||
|
),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves the iframe theme in the post-login redirect", () => {
|
||||||
|
assert.equal(
|
||||||
|
buildChatsIframeRedirectPath("?from=iframe&theme=dark-blue"),
|
||||||
|
"/page/workspace/chats_iframe/new?from=iframe&theme=dark-blue",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
buildChatsIframeRedirectPath("?from=iframe"),
|
||||||
|
"/page/workspace/chats_iframe/new?from=iframe",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
buildChatsIframeRedirectPath(
|
||||||
|
"?from=iframe&theme=dark-blue&ticket=secret&authToken=secret",
|
||||||
|
),
|
||||||
|
"/page/workspace/chats_iframe/new?from=iframe&theme=dark-blue",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps the iframe redirect ahead of login-method fallbacks", () => {
|
||||||
|
assert.equal(
|
||||||
|
resolvePostLoginRedirectPath(
|
||||||
|
"?from=iframe&theme=dark-blue&yUserId=user-1",
|
||||||
|
"/page/workspace/chats/new",
|
||||||
|
),
|
||||||
|
"/page/workspace/chats_iframe/new?from=iframe&theme=dark-blue",
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
resolvePostLoginRedirectPath("?yUserId=user-1", "/page/workspace/chats/new"),
|
||||||
|
"/page/workspace/chats/new",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves iframe theme when a new chat becomes a thread", () => {
|
||||||
|
assert.equal(
|
||||||
|
buildChatsIframeThreadPath(
|
||||||
|
"/page/workspace",
|
||||||
|
"thread-123",
|
||||||
|
"?from=iframe&theme=dark-blue&ticket=secret",
|
||||||
|
),
|
||||||
|
"/page/workspace/chats_iframe/thread-123?from=iframe&theme=dark-blue",
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,8 +1,60 @@
|
|||||||
import { resolveEmbedThemeMode } from "@/core/embed/embed-session";
|
import { resolveEmbedThemeMode } from "../embed/theme-mode.mjs";
|
||||||
|
|
||||||
|
const CHATS_IFRAME_PATH = "/page/workspace/chats_iframe/new";
|
||||||
|
const IFRAME_LOGIN_SOURCE = "iframe";
|
||||||
|
|
||||||
|
function toSearchParams(search: string): URLSearchParams {
|
||||||
|
return new URLSearchParams(search.startsWith("?") ? search : `?${search}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isChatsIframeLogin(search: string): boolean {
|
||||||
|
return toSearchParams(search).get("from") === IFRAME_LOGIN_SOURCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preserve only the presentation parameters consumed by the iframe workspace.
|
||||||
|
* Login credentials must never be copied into the post-login address bar.
|
||||||
|
*/
|
||||||
|
export function buildChatsIframeRedirectPath(search: string): string {
|
||||||
|
const source = toSearchParams(search);
|
||||||
|
const target = new URLSearchParams();
|
||||||
|
|
||||||
|
if (isChatsIframeLogin(search)) {
|
||||||
|
target.set("from", IFRAME_LOGIN_SOURCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
const theme = source.get("theme")?.trim();
|
||||||
|
if (theme) {
|
||||||
|
target.set("theme", theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = target.toString();
|
||||||
|
return query ? `${CHATS_IFRAME_PATH}?${query}` : CHATS_IFRAME_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildChatsIframeThreadPath(
|
||||||
|
routeBase: string,
|
||||||
|
threadId: string,
|
||||||
|
search: string,
|
||||||
|
): string {
|
||||||
|
const base = routeBase.endsWith("/") ? routeBase.slice(0, -1) : routeBase;
|
||||||
|
const query = buildChatsIframeRedirectPath(search).split("?")[1];
|
||||||
|
const path = `${base}/chats_iframe/${encodeURIComponent(threadId)}`;
|
||||||
|
return query ? `${path}?${query}` : path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePostLoginRedirectPath(
|
||||||
|
search: string,
|
||||||
|
fallbackPath: string,
|
||||||
|
): string {
|
||||||
|
return isChatsIframeLogin(search)
|
||||||
|
? buildChatsIframeRedirectPath(search)
|
||||||
|
: fallbackPath;
|
||||||
|
}
|
||||||
|
|
||||||
export function isChatsIframeWorkspacePath(pathname: string): boolean {
|
export function isChatsIframeWorkspacePath(pathname: string): boolean {
|
||||||
return (
|
return (
|
||||||
pathname === "/page/workspace/chats_iframe/new" ||
|
pathname === CHATS_IFRAME_PATH ||
|
||||||
pathname.startsWith("/page/workspace/chats_iframe/")
|
pathname.startsWith("/page/workspace/chats_iframe/")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -15,9 +67,12 @@ export function resolveForcedThemeForPath(
|
|||||||
return "light";
|
return "light";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChatsIframeWorkspacePath(pathname)) {
|
const isLoginPath = pathname === "/login" || pathname.startsWith("/login/");
|
||||||
const normalizedSearch = search.startsWith("?") ? search : `?${search}`;
|
if (
|
||||||
const rawTheme = new URLSearchParams(normalizedSearch).get("theme")?.trim();
|
isChatsIframeWorkspacePath(pathname) ||
|
||||||
|
(isLoginPath && isChatsIframeLogin(search))
|
||||||
|
) {
|
||||||
|
const rawTheme = toSearchParams(search).get("theme")?.trim();
|
||||||
return rawTheme ? resolveEmbedThemeMode(rawTheme) : "light";
|
return rawTheme ? resolveEmbedThemeMode(rawTheme) : "light";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ShrinkIcon,
|
ShrinkIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { useArtifacts } from "@/components/workspace/artifacts";
|
import { useArtifacts } from "@/components/workspace/artifacts";
|
||||||
@@ -31,6 +32,7 @@ import { getAccessToken } from "@/core/auth";
|
|||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { useModels } from "@/core/models/hooks";
|
import { useModels } from "@/core/models/hooks";
|
||||||
import { useNotification } from "@/core/notification/hooks";
|
import { useNotification } from "@/core/notification/hooks";
|
||||||
|
import { buildChatsIframeThreadPath } from "@/core/page-layout/chats-iframe-route";
|
||||||
import { useRouteBase } from "@/core/route-base";
|
import { useRouteBase } from "@/core/route-base";
|
||||||
import {
|
import {
|
||||||
isThreadRagModeLocked,
|
isThreadRagModeLocked,
|
||||||
@@ -126,6 +128,7 @@ function postBrainyAuthToken(accessToken: string) {
|
|||||||
|
|
||||||
export default function IframeChatPage() {
|
export default function IframeChatPage() {
|
||||||
const routeBase = useRouteBase();
|
const routeBase = useRouteBase();
|
||||||
|
const location = useLocation();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||||
const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } =
|
const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } =
|
||||||
@@ -230,7 +233,11 @@ export default function IframeChatPage() {
|
|||||||
history.replaceState(
|
history.replaceState(
|
||||||
null,
|
null,
|
||||||
"",
|
"",
|
||||||
`${window.location.pathname}${window.location.search}#${routeBase}/chats_iframe/${createdThreadId}`,
|
`${window.location.pathname}${window.location.search}#${buildChatsIframeThreadPath(
|
||||||
|
routeBase,
|
||||||
|
createdThreadId,
|
||||||
|
location.search,
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pendingRequestIdRef.current) {
|
if (pendingRequestIdRef.current) {
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ import {
|
|||||||
persistAccessToken,
|
persistAccessToken,
|
||||||
persistToolsToken,
|
persistToolsToken,
|
||||||
} from '@/core/auth/yuser-login';
|
} from '@/core/auth/yuser-login';
|
||||||
|
import {
|
||||||
|
isChatsIframeLogin,
|
||||||
|
resolvePostLoginRedirectPath,
|
||||||
|
} from '@/core/page-layout/chats-iframe-route';
|
||||||
import {
|
import {
|
||||||
isSidebarEnabledFromSearch,
|
isSidebarEnabledFromSearch,
|
||||||
persistSidebarEnabled,
|
persistSidebarEnabled,
|
||||||
@@ -66,9 +70,7 @@ function summarizeStoredUserInfo(): Record<string, unknown> | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_USERNAME = 'guest';
|
const DEFAULT_USERNAME = 'guest';
|
||||||
const IFRAME_LOGIN_SOURCE = 'iframe';
|
|
||||||
const DEFAULT_REDIRECT_PATH = '/page/workspace/chats/new';
|
const DEFAULT_REDIRECT_PATH = '/page/workspace/chats/new';
|
||||||
const IFRAME_REDIRECT_PATH = '/page/workspace/chats_iframe/new';
|
|
||||||
|
|
||||||
type LoginStatus = 'loading' | 'success' | 'error';
|
type LoginStatus = 'loading' | 'success' | 'error';
|
||||||
|
|
||||||
@@ -89,19 +91,25 @@ export default function LoginPage() {
|
|||||||
() => readEmbedParam(searchParams.toString()) === 'on',
|
() => readEmbedParam(searchParams.toString()) === 'on',
|
||||||
[searchParams],
|
[searchParams],
|
||||||
);
|
);
|
||||||
const redirectPath = useMemo(
|
const isIframeLogin = useMemo(
|
||||||
() =>
|
() => isChatsIframeLogin(searchParams.toString()),
|
||||||
searchParams.get('from') === IFRAME_LOGIN_SOURCE
|
|
||||||
? IFRAME_REDIRECT_PATH
|
|
||||||
: DEFAULT_REDIRECT_PATH,
|
|
||||||
[searchParams],
|
[searchParams],
|
||||||
);
|
);
|
||||||
const resolvedUsername = useMemo(() => {
|
const resolvedUsername = useMemo(() => {
|
||||||
const raw = username?.trim();
|
const raw = username?.trim();
|
||||||
return raw && raw.length > 0 ? raw : DEFAULT_USERNAME;
|
return raw && raw.length > 0 ? raw : DEFAULT_USERNAME;
|
||||||
}, [username]);
|
}, [username]);
|
||||||
// 外部跳转参数:theme 直接调 setTheme;ticket/userId/env 持久化到 localStorage
|
// 外部跳转参数:普通登录的 theme 调 setTheme;iframe 登录由 forcedTheme 接管。
|
||||||
|
// ticket/userId/env 仍按既有登录存储策略持久化。
|
||||||
const yUserId = searchParams.get('yUserId')?.trim() ?? '';
|
const yUserId = searchParams.get('yUserId')?.trim() ?? '';
|
||||||
|
const redirectPath = useMemo(
|
||||||
|
() =>
|
||||||
|
resolvePostLoginRedirectPath(
|
||||||
|
searchParams.toString(),
|
||||||
|
yUserId ? GENERAL_QA_ROUTE : DEFAULT_REDIRECT_PATH,
|
||||||
|
),
|
||||||
|
[searchParams, yUserId],
|
||||||
|
);
|
||||||
const toolToken = searchParams.get('token')?.trim() ?? '';
|
const toolToken = searchParams.get('token')?.trim() ?? '';
|
||||||
// 内网登录透传的访问令牌:存入 userInfo.accessToken,供 XK/PG 中任务 iframe 拼参
|
// 内网登录透传的访问令牌:存入 userInfo.accessToken,供 XK/PG 中任务 iframe 拼参
|
||||||
const access_token = searchParams.get('access_token')?.trim() ?? '';
|
const access_token = searchParams.get('access_token')?.trim() ?? '';
|
||||||
@@ -176,14 +184,14 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
// 主题切换:无界 props.theme 优先,其次 URL query theme。两者取值约定一致
|
// 主题切换:无界 props.theme 优先,其次 URL query theme。两者取值约定一致
|
||||||
// (dark-blue → 深色;red → 浅色;blue 暂无该主题,先用浅色代替),统一走 resolveThemeFromQuery。
|
// (dark-blue → 深色;red → 浅色;blue 暂无该主题,先用浅色代替),统一走 resolveThemeFromQuery。
|
||||||
// **嵌入模式跳过**:setTheme 会写 localStorage `strategy-theme`,跨标签污染正常系统页;
|
// **嵌入模式和 chats_iframe 登录跳过**:setTheme 会写 localStorage `strategy-theme`,
|
||||||
// 嵌入主题改由 forcedTheme(按标签页、不持久化)接管。
|
// 跨标签污染正常系统页;iframe 主题改由 forcedTheme(按标签页、不持久化)接管。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEmbed) return;
|
if (isEmbed || isIframeLogin) return;
|
||||||
const themeValue = wujieProps.theme || externalParams.theme;
|
const themeValue = wujieProps.theme || externalParams.theme;
|
||||||
if (!themeValue) return;
|
if (!themeValue) return;
|
||||||
setTheme(resolveThemeFromQuery(themeValue));
|
setTheme(resolveThemeFromQuery(themeValue));
|
||||||
}, [isEmbed, wujieProps.theme, externalParams.theme, setTheme]);
|
}, [isEmbed, isIframeLogin, wujieProps.theme, externalParams.theme, setTheme]);
|
||||||
|
|
||||||
// ticket / userId / env 持久化(与 login 请求并行做,互不阻塞)
|
// ticket / userId / env 持久化(与 login 请求并行做,互不阻塞)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -341,7 +349,7 @@ export default function LoginPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath);
|
router.replace(redirectPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
return;
|
||||||
@@ -371,6 +379,7 @@ export default function LoginPage() {
|
|||||||
taskId,
|
taskId,
|
||||||
goPath,
|
goPath,
|
||||||
redirectTo,
|
redirectTo,
|
||||||
|
redirectPath,
|
||||||
isEmbed,
|
isEmbed,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user