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")