feat(embed): iframe 嵌入模式(去 chrome + 主题隔离 / hideJump / 登录用户隔离)

外部系统用 iframe 套用深链单智能体问答/多智能体会商时,三个按标签页(sessionStorage)
开关,由深链 URL 参数开启、跨登录跳转/刷新/页内导航存活,绝不跨标签污染正常系统页:

- embed=1(可带 theme=):去全局导航栏 + 侧边栏;按外部主题色强制明/暗,走
  next-themes forcedTheme(不写 localStorage),嵌入侧不调 setTheme/persistSidebarEnabled
- hideJump=1:隐藏深链底部「快捷跳转」栏 TaskJumpBar(底部「下一步」按钮 + 右侧悬浮菜单)
- 登录隔离:嵌入态鉴权/身份键(deerflow.auth/userInfo/login.*/tools_token 等)改走
  sessionStorage(core/auth/scoped-storage.ts 的 authStorage),iframe 登录用户与正常
  登录用户完全隔离、互不覆盖;token 读取统一过 getStoredAuth,改中心函数即全体生效;
  跨标签账号变更监听 AuthSyncWatcher 嵌入态禁用

嵌入会话在 main.tsx 渲染前同步启用(早于任何鉴权写入)。正常标签页 isXxx() 恒 false,
行为零变化。新增 core/embed/embed-session.ts、core/auth/scoped-storage.ts。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
fengxiangdebaoziandClaude Opus 4.8 committed 2026-06-19 12:02:36 +08:00
1 parent 2b58fe6c8b
commit 31f9838095
16 files changed
+337 -58

No files matched your search

+16 -2
View File
@@ -20,6 +20,7 @@ import {
} from '@/core/auth/task-deeplink';
import { resolveUsernameByOnlineTicket } from '@/core/auth/wujie-ticket-login';
import { isWujieEnv, useWujieProps } from '@/core/embed/wujie';
import { readEmbedParam } from '@/core/embed/embed-session';
import {
GENERAL_QA_ROUTE,
loginByYUserId,
@@ -48,6 +49,13 @@ export default function LoginPage() {
() => isSidebarEnabledFromSearch(searchParams.toString()),
[searchParams],
);
// iframe 嵌入模式(外部系统 `&embed=1`):去 chrome + 按标签页强制主题。嵌入侧**不**走
// setTheme / persistSidebarEnabled(那俩落 localStorage、会跨标签污染正常系统页),改用
// sessionStorage 嵌入会话(见 embed-session.ts)。`embed=0` 显式关闭。
const isEmbed = useMemo(
() => readEmbedParam(searchParams.toString()) === 'on',
[searchParams],
);
const redirectPath = useMemo(
() =>
searchParams.get('from') === IFRAME_LOGIN_SOURCE
@@ -105,11 +113,14 @@ export default function LoginPage() {
// 主题切换:无界 props.theme 优先,其次 URL query theme。两者取值约定一致
// (dark-blue → 深色;red → 浅色;blue 暂无该主题,先用浅色代替),统一走 resolveThemeFromQuery。
// **嵌入模式跳过**:setTheme 会写 localStorage `strategy-theme`,跨标签污染正常系统页;
// 嵌入主题改由 forcedTheme(按标签页、不持久化)接管。
useEffect(() => {
if (isEmbed) return;
const themeValue = wujieProps.theme || externalParams.theme;
if (!themeValue) return;
setTheme(resolveThemeFromQuery(themeValue));
}, [wujieProps.theme, externalParams.theme, setTheme]);
}, [isEmbed, wujieProps.theme, externalParams.theme, setTheme]);
// ticket / userId / env 持久化(与 login 请求并行做,互不阻塞)
useEffect(() => {
@@ -154,7 +165,9 @@ export default function LoginPage() {
// 内网透传的 access_token 合并进 userInfo(在 loginByYUserId 覆盖之后),
// XK/PG 中任务 iframe 据此取 accessToken
persistAccessToken(accessToken);
persistSidebarEnabled(sidebarEnabled);
// 嵌入模式不写 sidebarEnabled(localStorage 的「简洁/全量」偏好会跨标签污染正常页);
// 去 chrome 由按标签页的嵌入会话(isEmbedActive)驱动,见 PageLayout。
if (!isEmbed) persistSidebarEnabled(sidebarEnabled);
setStatus('success');
// 任务深链:带 taskId + goPath 时拉任务详情并直送对应问答/会商页;
@@ -209,6 +222,7 @@ export default function LoginPage() {
gatePassword,
taskId,
goPath,
isEmbed,
]);
return (
+8 -1
View File
@@ -6,6 +6,7 @@ import { PageSidebarDark } from "@/components/page-sidebar-dark";
import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route";
import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled";
import { isWujieEnv } from "@/core/embed/wujie";
import { isEmbedActive, readEmbedParam } from "@/core/embed/embed-session";
import { useTheme } from "@/contexts/ThemeContext";
import { useLocation } from "react-router-dom";
import { useActiveThirdLevel } from "@/core/page-layout/use-sidebar-menu";
@@ -37,8 +38,14 @@ function PageLayout() {
const [layoutMode] = useLayoutMode();
const chromeByPreference =
layoutMode === "full" ? true : layoutMode === "compact" ? false : sidebarEnabled;
// iframe 嵌入模式(外部系统 `&embed=1`,按标签页 sessionStorage):去掉全局导航栏 + 侧边栏,
// 只留问答/会商工作区本体。按 location 重算以跟随页内导航;非嵌入标签页恒为 false,不受影响。
const embedActive = useMemo(
() => isEmbedActive() || readEmbedParam(location.search) === "on",
[location.pathname, location.search],
);
const pageSidebarAllowed =
chromeByPreference && !isChatsIframeWorkspacePath(location.pathname);
chromeByPreference && !isChatsIframeWorkspacePath(location.pathname) && !embedActive;
// h-full(100%)而非 h-screen(100vh):配合 globals.css 的 html/body/#root height:100%
// 高度链,根布局跟随容器高度。全屏下 100% 解析到视口,与 h-screen 等价;无界等嵌套