From 01012ff30071ef7488b3699747bc09bcd70045a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Mon, 15 Jun 2026 11:39:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(settings):=20=E8=BD=BB=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E5=8F=82=E6=95=B0=E4=B8=8E=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E7=AE=A1=E7=90=86=E5=91=98=E9=BB=98=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 登录时从路由参数 yUserId/access_token 获取并存入 userInfo - 轻应用管理页路由参数新增 userName/gateDepartName/typeName 选项 - 添加系统默认界面模式设置(管理员可指定全量/简洁) - useLayoutMode hook 支持优先级:用户自选 > 管理员默认 > 会话级 Co-Authored-By: Claude Haiku 4.5 --- .../settings/appearance-settings-page.tsx | 61 +++++++++++++++++-- frontend-web/src/core/auth/yuser-login.ts | 43 ++++++++++--- frontend-web/src/core/system-settings/api.ts | 3 + .../src/core/system-settings/hooks.ts | 22 +++++++ frontend-web/src/pages/ChatPage.tsx | 7 ++- frontend-web/src/pages/LoginPage.tsx | 7 +++ frontend-web/src/pages/PageRoutes.tsx | 11 ++-- .../src/strategy-components/lib/light-app.ts | 8 ++- frontend-web/src/styles/globals.css | 2 +- .../app/gateway/routers/system_settings.py | 5 +- .../deerflow/config/system_settings.py | 4 ++ 11 files changed, 150 insertions(+), 23 deletions(-) diff --git a/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx b/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx index 83c984c5..ae309232 100644 --- a/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx @@ -26,6 +26,7 @@ import { useLocalSettings } from "@/core/settings"; import { useAppearanceSettings, useCitationDisplaySettings, + useLayoutMode, useUpdateAppearanceSettings, useUpdateCitationDisplaySettings, } from "@/core/system-settings/hooks"; @@ -57,6 +58,9 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { const globalDefault = appearance?.default_theme ?? "light"; const globalSkillView = appearance?.default_skill_view ?? "list"; const globalAgentView = appearance?.default_agent_view ?? "card"; + const globalLayoutMode = appearance?.default_layout_mode ?? "full"; + // 有效界面模式:用户自己选过则以其为准,否则跟随管理员系统默认(再否则会话级回退)。 + const [effectiveLayoutMode, setLayoutMode] = useLayoutMode(); const cleanupWords = citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? []; @@ -74,6 +78,18 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { } }; + const handleSetDefaultLayoutMode = async (value: "full" | "compact") => { + if (value === globalLayoutMode) return; + try { + await updateAppearance.mutateAsync({ default_layout_mode: value }); + toast.success( + `已将系统默认界面模式设置为${value === "full" ? "全量模式" : "简洁模式"}`, + ); + } catch (e) { + toast.error(e instanceof Error ? e.message : "保存失败"); + } + }; + const handleSetDefaultView = async ( kind: "skill" | "agent", value: "card" | "list", @@ -156,10 +172,10 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) {
+ {isAdmin && ( + <> + + +
+ + +

+ 当前系统默认: + {globalLayoutMode === "full" ? "全量模式" : "简洁模式"} +

+
+
+ + )} + {isAdmin && ( <> diff --git a/frontend-web/src/core/auth/yuser-login.ts b/frontend-web/src/core/auth/yuser-login.ts index 27e2419c..f118d245 100644 --- a/frontend-web/src/core/auth/yuser-login.ts +++ b/frontend-web/src/core/auth/yuser-login.ts @@ -31,15 +31,44 @@ export async function loginByYUserId( { yuserId: yUserId }, )) as UserDetailResponse; - const result = res.data; - if (!result?.id) { - throw new Error("无法根据 yUserId 获取用户信息。"); - } - - localStorage.setItem(EXTERNAL_USER_ID_STORAGE_KEY, result.id); - localStorage.setItem(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(result)); + // consumer 返回 { accessToken, gateDepartName, typeName, userName }。 + // 一并存入路由参数 yUserId,使「应用管理」里 source=yUserId 的路由参数能取到(与 + // source=userName/gateDepartName/typeName 直接取接口值统一走 userInfo[source])。 + // 注意:接口也会返回 accessToken,但 XK/PG 及 source=accessToken 要用路由透传的 + // access_token,故登录流程随后会用 persistAccessToken 覆盖 userInfo.accessToken。 + const result = res.data ?? {}; + localStorage.setItem( + EXTERNAL_USER_INFO_STORAGE_KEY, + JSON.stringify({ ...result, yUserId }), + ); } export function persistToolsToken(token: string): void { localStorage.setItem(TOOLS_TOKEN_STORAGE_KEY, token); } + +/** + * 把内网登录路由参数 access_token 合并写入 userInfo.accessToken(覆盖接口返回的同名值)。 + * 据此拼参的有:XK/PG 中任务 iframe(XK 用 accessToken、PG 用 access_token,二者均读 + * external-links.getAccessToken() = userInfo.accessToken);以及「应用管理」里 + * source=accessToken 的轻应用路由参数(resolveRouteParamValue 取 userInfo.accessToken)。 + * 用合并而非覆盖整对象,避免抹掉 loginByYUserId 从 consumer 拿到的用户信息。 + */ +export function persistAccessToken(token: string): void { + const trimmed = token?.trim(); + if (!trimmed) return; + let info: Record = {}; + try { + const raw = localStorage.getItem(EXTERNAL_USER_INFO_STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === "object") { + info = parsed as Record; + } + } + } catch { + info = {}; + } + info.accessToken = trimmed; + localStorage.setItem(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(info)); +} diff --git a/frontend-web/src/core/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts index 864801f1..20fd57f1 100644 --- a/frontend-web/src/core/system-settings/api.ts +++ b/frontend-web/src/core/system-settings/api.ts @@ -137,17 +137,20 @@ export async function updatePublishSquares( export type ThemeMode = "light" | "dark" | "lightblue"; export type ViewMode = "card" | "list"; +export type LayoutMode = "full" | "compact"; export interface AppearanceSettings { default_theme: ThemeMode; default_skill_view: ViewMode; default_agent_view: ViewMode; + default_layout_mode: LayoutMode; } export interface AppearanceUpdate { default_theme?: ThemeMode; default_skill_view?: ViewMode; default_agent_view?: ViewMode; + default_layout_mode?: LayoutMode; } export async function loadAppearanceSettings(): Promise { diff --git a/frontend-web/src/core/system-settings/hooks.ts b/frontend-web/src/core/system-settings/hooks.ts index 5ab38951..05bf91a0 100644 --- a/frontend-web/src/core/system-settings/hooks.ts +++ b/frontend-web/src/core/system-settings/hooks.ts @@ -21,6 +21,7 @@ import { type PrivateSquareUpdate, type PromptPrefixUpdate, type PublishSquaresUpdate, + type LayoutMode, type ViewMode, } from "./api"; @@ -143,6 +144,27 @@ export function useViewMode( return [mode, setMode]; } +/** + * Effective interface layout mode (full chrome vs compact). A user's own choice + * (persisted in local settings) wins; otherwise the admin-configured system + * default applies; otherwise `undefined` (callers fall back to the session-level + * `sidebarEnabled` flag set by portal / yUserId login). `setMode` persists the + * user's override locally. + */ +export function useLayoutMode(): [ + LayoutMode | undefined, + (mode: LayoutMode) => void, +] { + const [settings, setSettings] = useLocalSettings(); + const { data: appearance } = useAppearanceSettings(); + const userOverride = settings.display.layoutMode; + const adminDefault = appearance?.default_layout_mode; + const mode = userOverride ?? adminDefault; + const setMode = (next: LayoutMode) => + setSettings("display", { layoutMode: next }); + return [mode, setMode]; +} + export function useCitationDisplaySettings() { return useQuery({ queryKey: CITATION_DISPLAY_KEY, diff --git a/frontend-web/src/pages/ChatPage.tsx b/frontend-web/src/pages/ChatPage.tsx index e39c5f58..c755455b 100644 --- a/frontend-web/src/pages/ChatPage.tsx +++ b/frontend-web/src/pages/ChatPage.tsx @@ -42,10 +42,10 @@ import { getLastAgentId, saveLastAgentId, useKnowledgeRagEnabled, - useLocalSettings, useThreadSettings, } from "@/core/settings"; import { useThreadStream } from "@/core/threads/hooks"; +import { useLayoutMode } from "@/core/system-settings/hooks"; import { useRouteBase } from "@/core/route-base"; import { textOfMessage } from "@/core/threads/utils"; import { env } from "@/env"; @@ -96,7 +96,8 @@ export default function ChatPage() { const { open: historySidebarOpen, toggleSidebar: toggleHistorySidebar } = useSidebar(); const { t } = useI18n(); - const [localSettings] = useLocalSettings(); + // 有效界面模式:用户自选 > 管理员系统默认 > (下方再回退会话级 sidebarEnabled) + const [effectiveLayoutMode] = useLayoutMode(); const [showFollowups, setShowFollowups] = useState(false); const [hasStartedConversation, setHasStartedConversation] = useState(false); const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } = @@ -418,7 +419,7 @@ export default function ChatPage() { // 优先看外观设置的「全量/简洁」,未设置过才回退到会话级 sidebarEnabled。否则用户显式 // 选「全量」时 PageRoutes 已挂出顶栏+侧栏,这里却还按旧 sidebarEnabled 判定,导致深蓝 // 主题底图(workspace-chat-bg)不生效。非紧凑(嵌入/standalone)保持原样。 - const layoutMode = localSettings.display.layoutMode; + const layoutMode = effectiveLayoutMode; const chromeByPreference = layoutMode === "full" ? true diff --git a/frontend-web/src/pages/LoginPage.tsx b/frontend-web/src/pages/LoginPage.tsx index 1d19c45b..8f33b1da 100644 --- a/frontend-web/src/pages/LoginPage.tsx +++ b/frontend-web/src/pages/LoginPage.tsx @@ -16,6 +16,7 @@ import { isWujieEnv, useWujieProps } from '@/core/embed/wujie'; import { GENERAL_QA_ROUTE, loginByYUserId, + persistAccessToken, persistToolsToken, } from '@/core/auth/yuser-login'; import { @@ -53,6 +54,8 @@ export default function LoginPage() { // 外部跳转参数:theme 直接调 setTheme;ticket/userId/env 持久化到 localStorage const yUserId = searchParams.get('yUserId')?.trim() ?? ''; const toolToken = searchParams.get('token')?.trim() ?? ''; + // 内网登录透传的访问令牌:存入 userInfo.accessToken,供 XK/PG 中任务 iframe 拼参 + const accessToken = searchParams.get('access_token')?.trim() ?? ''; // 用户名直登口令门:开启后端开关时,口令从 ?password= 透传 const gatePassword = searchParams.get('password') ?? ''; // token 换登录:?authToken= 传上游 token(与 tools_token 的 ?token= 区分开) @@ -137,6 +140,9 @@ export default function LoginPage() { setStoredAuth(auth); // 标准登录把用户名写入 userInfo,供顶栏用户区显示(已有则不覆盖) setStoredUserName(loginUserName); + // 内网透传的 access_token 合并进 userInfo(在 loginByYUserId 覆盖之后), + // XK/PG 中任务 iframe 据此取 accessToken + persistAccessToken(accessToken); persistSidebarEnabled(sidebarEnabled); setStatus('success'); router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath); @@ -161,6 +167,7 @@ export default function LoginPage() { sidebarEnabled, yUserId, toolToken, + accessToken, onlineTicket, authToken, gatePassword, diff --git a/frontend-web/src/pages/PageRoutes.tsx b/frontend-web/src/pages/PageRoutes.tsx index 6ae9372d..178c1544 100644 --- a/frontend-web/src/pages/PageRoutes.tsx +++ b/frontend-web/src/pages/PageRoutes.tsx @@ -7,7 +7,7 @@ import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-rout import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled"; import { useTheme } from "@/contexts/ThemeContext"; import { useLocation } from "react-router-dom"; -import { useLocalSettings } from "@/core/settings"; +import { useLayoutMode } from "@/core/system-settings/hooks"; import WorkspaceRoutes from "./WorkspaceRoutes"; import StrategyRoutes from "./StrategyRoutes"; @@ -22,11 +22,10 @@ function PageLayout() { const location = useLocation(); const { theme } = useTheme(); const isDark = theme === "dark"; - const [settings] = useLocalSettings(); - // 界面模式开关(外观设置):用户显式选「全量/简洁」时以其为准;未设置过则回退到 - // 会话级 sidebarEnabled(外部门户/yUserId 登录时置位)这一旧默认。这样无论是否走门户 - // 登录,「全量↔简洁」切换都能真正控制顶栏+侧边栏的显示。 - const layoutMode = settings.display.layoutMode; + // 界面模式(外观设置):用户显式选「全量/简洁」时以其为准;否则跟随管理员系统默认 + // (default_layout_mode);两者都没有才回退到会话级 sidebarEnabled(外部门户/yUserId + // 登录时置位)这一旧默认。这样无论是否走门户登录,「全量↔简洁」都能真正控制顶栏+侧边栏。 + const [layoutMode] = useLayoutMode(); const chromeByPreference = layoutMode === "full" ? true : layoutMode === "compact" ? false : sidebarEnabled; const pageSidebarAllowed = diff --git a/frontend-web/src/strategy-components/lib/light-app.ts b/frontend-web/src/strategy-components/lib/light-app.ts index 481b838b..ab796bf6 100644 --- a/frontend-web/src/strategy-components/lib/light-app.ts +++ b/frontend-web/src/strategy-components/lib/light-app.ts @@ -19,11 +19,17 @@ export function mountLabel(value: string | null | undefined): string { return MOUNT_OPTIONS.find((o) => o.value === value)?.label ?? value; } -/** 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 */ +/** + * 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 + * accessToken / yUserId 取登录路由透传值(写入 userInfo 的同名键); + * userName / gateDepartName / typeName 取 consumer 接口 userDetailByYuserId 的返回值。 + */ export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [ { label: "accessToken", value: "accessToken" }, { label: "yUserId", value: "yUserId" }, { label: "userName", value: "userName" }, + { label: "gateDepartName", value: "gateDepartName" }, + { label: "typeName", value: "typeName" }, { label: "其他(自定义值)", value: "other" }, ]; diff --git a/frontend-web/src/styles/globals.css b/frontend-web/src/styles/globals.css index 7ebd17de..3494b65c 100644 --- a/frontend-web/src/styles/globals.css +++ b/frontend-web/src/styles/globals.css @@ -932,7 +932,7 @@ body { .strategy-app-header { background-color: transparent; background-image: url("../assets/img/red-header.png"); - background-size: 100% 100%; + background-size: cover; background-repeat: no-repeat; background-position: center; } diff --git a/offline-backend-20260512/backend/app/gateway/routers/system_settings.py b/offline-backend-20260512/backend/app/gateway/routers/system_settings.py index 10beff43..42e095ef 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/system_settings.py +++ b/offline-backend-20260512/backend/app/gateway/routers/system_settings.py @@ -311,12 +311,14 @@ class AppearanceResponse(BaseModel): default_theme: Literal["light", "dark", "lightblue"] = Field(..., description="System default theme — light, dark or lightblue") default_skill_view: Literal["card", "list"] = Field(..., description="System default skill list display mode — card or list") default_agent_view: Literal["card", "list"] = Field(..., description="System default agent list display mode — card or list") + default_layout_mode: Literal["full", "compact"] = Field(..., description="System default interface layout — full (sidebar + header) or compact (hidden)") class AppearanceUpdateRequest(BaseModel): default_theme: Literal["light", "dark", "lightblue"] | None = Field(default=None) default_skill_view: Literal["card", "list"] | None = Field(default=None) default_agent_view: Literal["card", "list"] | None = Field(default=None) + default_layout_mode: Literal["full", "compact"] | None = Field(default=None) def _appearance_response(appearance) -> AppearanceResponse: @@ -324,6 +326,7 @@ def _appearance_response(appearance) -> AppearanceResponse: default_theme=appearance.default_theme, default_skill_view=appearance.default_skill_view, default_agent_view=appearance.default_agent_view, + default_layout_mode=appearance.default_layout_mode, ) @@ -343,7 +346,7 @@ async def update_appearance_settings( current = load_system_settings() fields = body.model_dump(exclude_unset=True) update: dict[str, object] = {} - for key in ("default_theme", "default_skill_view", "default_agent_view"): + for key in ("default_theme", "default_skill_view", "default_agent_view", "default_layout_mode"): if key in fields and fields[key] is not None: update[key] = fields[key] if update: diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py b/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py index cee35a4e..21e46372 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/config/system_settings.py @@ -59,6 +59,10 @@ class AppearanceSettings(BaseModel): default="card", description="Default agent list display mode for users who haven't picked their own (card / list)", ) + default_layout_mode: Literal["full", "compact"] = Field( + default="full", + description="Default interface layout for users who haven't picked their own — full (sidebar + header) / compact (hidden)", + ) model_config = ConfigDict(extra="ignore")