+
@@ -99,6 +101,16 @@ function CompactWorkspaceTopActions() {
);
}
+function CompactAppearanceActions() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
export function WorkspaceLayout({
children,
pageSidebarCollapsed,
@@ -342,7 +354,12 @@ export default function WorkspaceRoutes({
element={
-
+ : undefined
+ }
+ />
}
diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx
index 0bcb8334..b88ee3c9 100644
--- a/frontend-web/src/strategy-components/Header.tsx
+++ b/frontend-web/src/strategy-components/Header.tsx
@@ -4,7 +4,11 @@ import { useLocation, useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import "@/components/page-sdebar.css";
-import { LayoutModeSwitcher, ThemeSwitcher } from "@/components/workspace/appearance-controls";
+import {
+ CompactShortcutButtons,
+ LayoutModeSwitcher,
+ ThemeSwitcher,
+} from "@/components/workspace/appearance-controls";
import { NotificationDialog } from "@/components/workspace/notification-dialog";
import { getStoredAuth, getAccountDisplayName } from "@/core/auth";
import { isWujieEnv } from "@/core/embed/wujie";
@@ -124,6 +128,7 @@ const Header = ({ collapsed }: HeaderProps) => {
// 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用
const actionCluster = (
<>
+
list[CompactShortcutButton]:
+ normalized: list[CompactShortcutButton] = []
+ seen_ids: set[str] = set()
+ for index, item in enumerate(buttons or []):
+ label = (item.label or "").strip() or "快捷入口"
+ url = (item.url or "").strip()
+ row_id = (item.id or "").strip() or uuid4().hex
+ if row_id in seen_ids:
+ row_id = uuid4().hex
+ seen_ids.add(row_id)
+
+ route_params: list[AppearanceRouteParam] = []
+ for param in item.route_params or []:
+ key = (param.key or "").strip()
+ if not key:
+ continue
+ if param.paramType == "theme":
+ route_params.append(
+ AppearanceRouteParam(
+ key=key,
+ paramType="theme",
+ themeValue=(param.themeValue or "").strip()
+ or "red=red;blue=blue;dark-blue=dark-blue",
+ )
+ )
+ continue
+
+ source = (param.source or "access_token").strip() or "access_token"
+ route_params.append(
+ AppearanceRouteParam(
+ key=key,
+ paramType="login",
+ source=source,
+ customValue=(param.customValue or "").strip()
+ if source == "other"
+ else None,
+ )
+ )
+
+ normalized.append(
+ CompactShortcutButton(
+ id=row_id,
+ label=label,
+ url=url,
+ route_params=route_params,
+ enabled=bool(item.enabled),
+ sort_order=index,
+ )
+ )
+ return normalized
def _appearance_response(appearance) -> AppearanceResponse:
@@ -552,6 +611,10 @@ def _appearance_response(appearance) -> AppearanceResponse:
default_skill_view=appearance.default_skill_view,
default_agent_view=appearance.default_agent_view,
default_layout_mode=appearance.default_layout_mode,
+ compact_shortcut_buttons=sorted(
+ appearance.compact_shortcut_buttons,
+ key=lambda item: item.sort_order,
+ ),
)
@@ -574,6 +637,10 @@ async def update_appearance_settings(
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 "compact_shortcut_buttons" in fields and body.compact_shortcut_buttons is not None:
+ update["compact_shortcut_buttons"] = _normalize_compact_shortcut_buttons(
+ body.compact_shortcut_buttons
+ )
if update:
updated_appearance = current.appearance.model_copy(update=update)
saved = save_system_settings(
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 214f84c2..aa0025be 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
@@ -18,6 +18,7 @@ import logging
import threading
from pathlib import Path
from typing import Literal
+from uuid import uuid4
from pydantic import BaseModel, ConfigDict, Field
@@ -38,6 +39,60 @@ class PromptPrefixSettings(BaseModel):
model_config = ConfigDict(extra="ignore")
+class AppearanceRouteParam(BaseModel):
+ """Runtime query-param descriptor for top-bar shortcut buttons.
+
+ The frontend resolves login params from ``userInfo`` and theme params from
+ the current UI theme, matching the light-app center's link builder.
+ """
+
+ key: str = Field(default="", description="Query parameter name appended to the shortcut URL")
+ paramType: Literal["login", "theme"] = Field(default="login", description="Where the parameter value comes from")
+ source: str | None = Field(default=None, description="login param source field in userInfo, or 'other'")
+ customValue: str | None = Field(default=None, description="Literal value when source is 'other'")
+ themeValue: str | None = Field(default=None, description="Theme-value mapping string, e.g. red=red;dark-blue=dark")
+ model_config = ConfigDict(extra="ignore")
+
+
+def _default_shortcut_route_params() -> list[AppearanceRouteParam]:
+ return [
+ AppearanceRouteParam(
+ key="access_token",
+ paramType="login",
+ source="access_token",
+ customValue="",
+ themeValue="red=red;blue=blue;dark-blue=dark-blue",
+ )
+ ]
+
+
+class CompactShortcutButton(BaseModel):
+ """Configurable shortcut shown in top bars before the theme switcher."""
+
+ id: str = Field(default_factory=lambda: uuid4().hex, description="Stable row id for editing")
+ label: str = Field(default="快捷入口", description="Button text shown next to the icon")
+ url: str = Field(default="", description="Target URL opened in a new tab")
+ route_params: list[AppearanceRouteParam] = Field(
+ default_factory=_default_shortcut_route_params,
+ description="Query parameters resolved at launch time and appended to url",
+ )
+ enabled: bool = Field(default=True, description="Whether this shortcut is rendered")
+ sort_order: int = Field(default=0, description="Display order in top bars")
+ model_config = ConfigDict(extra="ignore")
+
+
+def _default_compact_shortcut_buttons() -> list[CompactShortcutButton]:
+ return [
+ CompactShortcutButton(
+ id="default-compact-shortcut",
+ label="快捷入口",
+ url="",
+ enabled=True,
+ sort_order=0,
+ )
+ ]
+
+
class AppearanceSettings(BaseModel):
"""System-wide appearance defaults that any *new* user inherits.
@@ -63,6 +118,10 @@ class AppearanceSettings(BaseModel):
default="full",
description="Default interface layout for users who haven't picked their own — full (sidebar + header) / compact (hidden)",
)
+ compact_shortcut_buttons: list[CompactShortcutButton] = Field(
+ default_factory=_default_compact_shortcut_buttons,
+ description="Shortcut buttons rendered before the theme switcher in top bars",
+ )
model_config = ConfigDict(extra="ignore")