Merge branch 'dhk-1' into 'master'

Dhk 1

See merge request knowledge-base-bj/deerflow-code!4
This commit is contained in:
董宏凯 committed 2026-07-16 14:45:42 +00:00
commit 2c331aa744
16 files changed
+242 -38

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>CM 助手</title>
<script src="/runtime-config.js"></script>
<script src="/runtime-config.js" data-runtime-config></script>
</head>
<body>
<div id="root"></div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+19
View File
@@ -7,6 +7,25 @@
// 4. 这里的默认值来自 .env.network;如某个环境不用某项,可以留空字符串。
// 5. 布尔开关也用字符串形式填写:"true" / "false"。
window.__CM_RUNTIME_CONFIG__ = {
// 品牌图片相对路径会以 runtime-config.js 所在目录为基准解析。
// Nginx 以子路径部署时不需要修改前端图片路径。
// 打包后可直接替换 dist/brand-assets/ 内的同名 PNG。
// 也可以把下方图片地址改为其他 PNG 地址,无需重新打包。
// 是否显示整块欢迎页图标区域:"true" 显示,"false" 隐藏,默认隐藏。
VITE_WELCOME_LOGO_VISIBLE: "false",
// 红白主题的顶栏系统 Logo 图片地址。
VITE_BRAND_LOGO_LIGHT_URL: "brand-assets/brand-logo-light.png",
// 浅蓝主题的顶栏系统 Logo 图片地址。
VITE_BRAND_LOGO_LIGHTBLUE_URL: "brand-assets/brand-logo-lightblue.png",
// 深色主题的顶栏系统 Logo 图片地址。
VITE_BRAND_LOGO_DARK_URL: "brand-assets/brand-logo-dark.png",
// 红白主题的欢迎页 Logo 图片地址:为空时显示原有 CM 文字和渐变背景。
VITE_BRAND_WELCOME_LIGHT_URL: "",
// 浅蓝主题的欢迎页 Logo 图片地址:为空时显示原有 CM 文字和渐变背景。
VITE_BRAND_WELCOME_LIGHTBLUE_URL: "",
// 深色主题的欢迎页 Logo 图片地址:为空时显示原有 CM 文字和渐变背景。
VITE_BRAND_WELCOME_DARK_URL: "",
// DeerFlow Gateway 地址:前端自有后端接口,例如 /api/agents、/api/auth、/api/threads 等。
VITE_BACKEND_BASE_URL: "http://10.19.200.5:2026",
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react";
import { useNavigate, useLocation } from "react-router-dom";
import { PanelLeftOpen, PanelLeftClose } from "lucide-react";
import { getStoredAuth } from "@/core/auth";
import { isWujieEnv } from "@/core/embed/wujie";
@@ -12,7 +13,13 @@ import {
import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu";
import './page-sdebar.css'
export function PageSidebarDark() {
type PageSidebarDarkProps = {
/** 是否收起(true 时只渲染左下角浮动入口,sidebar 主体隐藏) */
collapsed: boolean;
onToggleCollapsed: () => void;
};
export function PageSidebarDark({ collapsed, onToggleCollapsed }: PageSidebarDarkProps) {
const isAdmin = getStoredAuth()?.system_role === "admin";
const visibleMenuItems = useSidebarMenuItems(!!isAdmin);
const inWujieEnv = isWujieEnv();
@@ -189,6 +196,22 @@ export function PageSidebarDark() {
</div>
);
// 收起时主侧栏不占宽度,保留左下角入口以便随时恢复。
if (collapsed) {
return (
<button
type="button"
title="展开侧边栏"
aria-label="展开侧边栏"
className="sidebar-collapse-btn sidebar-collapse-btn--floating"
onClick={onToggleCollapsed}
>
<PanelLeftOpen className="sidebar-collapse-btn__icon" aria-hidden="true" />
<span className="sidebar-collapse-btn__label">展开面板</span>
</button>
);
}
return (
<aside className={`sidebar-contnet ${inWujieEnv ? "" : "sidebar-contnet--top-submenu"}`}>
<div className="sidebar-contents">
@@ -220,6 +243,17 @@ export function PageSidebarDark() {
</div>
</div>
))}
<button
type="button"
title="收起侧边栏"
aria-label="收起侧边栏"
className="sidebar-collapse-btn sidebar-collapse-btn--inline"
onClick={onToggleCollapsed}
>
<PanelLeftClose className="sidebar-collapse-btn__icon" aria-hidden="true" />
<span className="sidebar-collapse-btn__label">收起面板</span>
</button>
</div>
</aside>
);
@@ -1,5 +1,10 @@
"use client";
import { useState } from "react";
import { useTheme } from "@/contexts/ThemeContext";
import { getWelcomeBrandAsset } from "@/core/branding/brand-assets";
import { readRuntimeFlag } from "@/env";
import { cn } from "@/lib/utils";
import { useDisplayDesensitizer } from "@/core/sensitive-words/use-display-desensitizer";
@@ -10,6 +15,13 @@ import { useDisplayDesensitizer } from "@/core/sensitive-words/use-display-desen
*/
export function CompactWelcome({ className }: { className?: string }) {
const desensitizeText = useDisplayDesensitizer();
const { theme } = useTheme();
const brandTheme = theme;
const welcomeLogo = getWelcomeBrandAsset(brandTheme);
const [failedWelcomeLogo, setFailedWelcomeLogo] = useState<string | null>(null);
const shouldShowWelcomeLogo =
Boolean(welcomeLogo) && failedWelcomeLogo !== welcomeLogo;
const isWelcomeLogoVisible = readRuntimeFlag("VITE_WELCOME_LOGO_VISIBLE");
return (
<div
@@ -19,6 +31,8 @@ export function CompactWelcome({ className }: { className?: string }) {
)}
>
{/* CM logo:浅底圆环 + 主色渐变圆角方块 */}
{/* Controlled by VITE_WELCOME_LOGO_VISIBLE in runtime-config.js. */}
{isWelcomeLogoVisible && (
<div
className="mb-3.5 inline-flex items-center justify-center rounded-full p-3"
style={{
@@ -28,18 +42,29 @@ export function CompactWelcome({ className }: { className?: string }) {
"1px solid rgb(var(--legacy-workspace-brand-rgb) / 12%)",
}}
>
<div
className="flex size-14 items-center justify-center rounded-2xl shadow-md"
style={{
backgroundImage:
"linear-gradient(to top right, var(--workspace-brand), var(--brand-200))",
}}
>
<span className="text-2xl font-black tracking-wide text-white italic">
{desensitizeText("CM")}
</span>
</div>
{shouldShowWelcomeLogo && welcomeLogo ? (
<img
src={welcomeLogo}
alt=""
aria-hidden="true"
className="size-16 object-contain"
onError={() => setFailedWelcomeLogo(welcomeLogo)}
/>
) : (
<div
className="flex size-14 items-center justify-center rounded-2xl shadow-md"
style={{
backgroundImage:
"linear-gradient(to top right, var(--workspace-brand), var(--brand-200))",
}}
>
<span className="text-2xl font-black tracking-wide text-white italic">
{desensitizeText("CM")}
</span>
</div>
)}
</div>
)}
<h1 className="flex items-center justify-center text-2xl font-bold tracking-tight text-foreground/90">
{desensitizeText("欢迎使用CM助手")}
@@ -0,0 +1,69 @@
import fallbackLogoDark from "@/assets/img/brand-logo-dark.png";
import fallbackLogoLight from "@/assets/img/brand-logo-light.png";
import fallbackLogoLightblue from "@/assets/img/brand-logo-lightblue.png";
import { readRuntimeEnv } from "@/env";
export type BrandTheme = "light" | "lightblue" | "dark";
const headerFallbackAssets: Record<BrandTheme, string> = {
light: fallbackLogoLight,
lightblue: fallbackLogoLightblue,
dark: fallbackLogoDark,
};
const headerRuntimeKeys: Record<BrandTheme, string> = {
light: "VITE_BRAND_LOGO_LIGHT_URL",
lightblue: "VITE_BRAND_LOGO_LIGHTBLUE_URL",
dark: "VITE_BRAND_LOGO_DARK_URL",
};
const welcomeRuntimeKeys: Record<BrandTheme, string> = {
light: "VITE_BRAND_WELCOME_LIGHT_URL",
lightblue: "VITE_BRAND_WELCOME_LIGHTBLUE_URL",
dark: "VITE_BRAND_WELCOME_DARK_URL",
};
function runtimeConfigBaseUrl(): string | undefined {
if (typeof document === "undefined") {
return undefined;
}
return document.querySelector<HTMLScriptElement>("script[data-runtime-config]")?.src;
}
/**
* Resolves an optional post-build brand override from runtime-config.js.
* Relative paths are anchored to runtime-config.js, which keeps the URL valid
* when the whole SPA is mounted below an Nginx location prefix.
*/
function readRuntimeBrandAsset(key: string): string | undefined {
const configuredUrl = readRuntimeEnv(key);
if (!configuredUrl) {
return undefined;
}
try {
const baseUrl =
runtimeConfigBaseUrl() ??
(typeof window !== "undefined" ? window.location.href : undefined);
return baseUrl ? new URL(configuredUrl, baseUrl).href : configuredUrl;
} catch {
return configuredUrl;
}
}
export function getHeaderBrandAsset(theme: BrandTheme): string {
return readRuntimeBrandAsset(headerRuntimeKeys[theme]) ?? headerFallbackAssets[theme];
}
export function getHeaderBrandFallback(theme: BrandTheme): string {
return headerFallbackAssets[theme];
}
/**
* Welcome content deliberately has no bundled image fallback. An empty
* runtime URL keeps the original CM text + gradient treatment.
*/
export function getWelcomeBrandAsset(theme: BrandTheme): string | undefined {
return readRuntimeBrandAsset(welcomeRuntimeKeys[theme]);
}
+9 -8
View File
@@ -31,8 +31,6 @@ function PageLayout() {
const location = useLocation();
const { theme } = useTheme();
const isDark = theme === "dark";
// 深色导航侧栏不提供收起入口,始终保持展开,避免切换主题后沿用浅色模式的收起状态。
const sidebarCollapsed = isDark ? false : collapsed;
// 浅色非嵌入:侧边栏为新版「二级导航图标卡片」窄栏(96px);wujie 嵌入仍走旧版手风琴(64/256px)。
const inWujie = useMemo(() => isWujieEnv(), []);
const lightNarrowSidebar = !isDark && !inWujie;
@@ -76,14 +74,17 @@ function PageLayout() {
return (
<RoundtableJobNotifier>
<div className="h-full flex flex-col bg-muted/20">
{pageSidebarAllowed && <Header collapsed={sidebarCollapsed} />}
{pageSidebarAllowed && <Header collapsed={collapsed} />}
<div className="flex flex-1 overflow-hidden">
{pageSidebarAllowed && (
isDark ? (
<PageSidebarDark />
<PageSidebarDark
collapsed={collapsed}
onToggleCollapsed={() => setCollapsed((prev) => !prev)}
/>
) : (
<PageSidebar
collapsed={sidebarCollapsed}
collapsed={collapsed}
onToggleCollapsed={() => setCollapsed((prev) => !prev)}
/>
)
@@ -93,8 +94,8 @@ function PageLayout() {
!pageSidebarAllowed
? ""
: isDark
// 深色 sidebar 固定展开 118px(10 + 98 + 10 gap)。
? "pl-[118px]"
// 深色 sidebar:展开 118px(10 + 98 + 10 gap),收起时主内容不留左内边距。
? (collapsed ? "pl-0" : "pl-[118px]")
: lightNarrowSidebar
// 浅色新版窄栏:展开三级列时 96→236px,主内容左内边距随之过渡
? `transition-[padding] duration-300 ${lightExpanded ? "pl-[236px]" : "pl-[96px]"}`
@@ -108,7 +109,7 @@ function PageLayout() {
path="workspace/*"
element={
<WorkspaceRoutes
pageSidebarCollapsed={sidebarCollapsed}
pageSidebarCollapsed={collapsed}
pageSidebarHidden={!pageSidebarAllowed}
headerHidden={!pageSidebarAllowed}
compactWorkspaceSidebar={pageSidebarAllowed}
@@ -4240,7 +4240,7 @@ function RightSidebar({
{timelineEntries.map((entry) => (
<div
key={entry.id}
className="relative rounded-lg px-1.5 py-1 transition-colors hover:bg-slate-50/80"
className="roundtable-clarification-entry relative rounded-lg px-1.5 py-1 transition-colors hover:bg-slate-50/80"
>
<div className="absolute -left-[20.5px] top-2 h-3 w-3 rounded-full bg-primary border-2 border-white ring-2 ring-[var(--brand-200)]" />
<div className="grid grid-cols-[68px_minmax(0,1fr)] gap-x-2">
@@ -10,10 +10,10 @@
--rt-primary-active: var(--brand-950, #a8071a);
--rt-primary-soft: var(--brand-200, #feeded);
--rt-page-bg: #fef6f6;
/* 圆桌全页最小可读字号(含 Step 3 方案展示 / 结果绘制) */
--roundtable-chat-font-size: 16px;
--roundtable-chat-meta-font-size: 16px;
font-size: 16px;
/* 圆桌全页最小可读字号(含 Step 3 方案展示 / 结果绘制),跟随「设置 → 字号」。 */
--roundtable-chat-font-size: calc(16px * var(--ui-font-scale, 1));
--roundtable-chat-meta-font-size: calc(16px * var(--ui-font-scale, 1));
font-size: calc(16px * var(--ui-font-scale, 1));
}
/* 会商对话区:气泡正文、步骤卡、Markdown 块 */
@@ -29,10 +29,11 @@
/*
* 抬升页内显式写死的小字号类(text-xs / text-sm / text-[10–14px] / calc),
* 避免 Step 3、侧栏、按钮条等遗漏逐文件改 class。text-base 及以上标题保持不变。
* 并让它们跟随设置页字号倍率,避免 Step 3、左右记录区和对话列表遗漏。
* text-base 及以上标题由全局 --text-* token 处理。
*/
.roundtable-planning :where(.text-xs, .text-sm) {
font-size: 16px;
font-size: calc(16px * var(--ui-font-scale, 1));
}
.roundtable-planning :where(
@@ -46,7 +47,7 @@
[class*="text-[14px"],
[class*="text-[calc"]
):not(.w-5) {
font-size: 16px;
font-size: calc(16px * var(--ui-font-scale, 1));
}
.roundtable-planning ::-webkit-scrollbar-thumb {
@@ -343,6 +344,17 @@
color: var(--muted-foreground) !important;
}
/* ---- Step1 右侧「澄清记录」:覆盖 hover:bg-slate-50/80,避免深色下泛灰白 ---- */
:is(.dark .roundtable-planning, .roundtable-planning--dark)
.roundtable-clarification-entry:hover {
background-color: rgb(22 119 255 / 18%) !important;
}
:is(.dark .roundtable-planning, .roundtable-planning--dark)
.roundtable-clarification-entry:hover h4 {
color: #e8edf5 !important;
}
/* ---- focus ring:brand-200 → 主色淡环 ---- */
:is(.dark .roundtable-planning, .roundtable-planning--dark)
:is(
@@ -1,4 +1,4 @@
import { Bell, ChevronDown, FileText, User } from "lucide-react";
import { Bell, ChevronDown, User } from "lucide-react";
import { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
@@ -12,6 +12,7 @@ import {
import { NotificationDialog } from "@/components/workspace/notification-dialog";
import { getStoredAuth } from "@/core/auth";
import { useLiveAccount } from "@/core/auth/use-live-account";
import { getHeaderBrandAsset, getHeaderBrandFallback } from "@/core/branding/brand-assets";
import { isWujieEnv } from "@/core/embed/wujie";
import { useUnreadCount } from "@/core/notifications";
import { useMyPosition } from "@/core/positions";
@@ -46,6 +47,17 @@ type HeaderProps = {
const Header = ({ collapsed }: HeaderProps) => {
const { theme } = useTheme();
const isDark = theme === "dark";
const brandTheme = theme;
const configuredBrandLogo = getHeaderBrandAsset(brandTheme);
const fallbackBrandLogo = getHeaderBrandFallback(brandTheme);
const [failedBrandLogo, setFailedBrandLogo] = useState<string | null>(null);
const brandLogo =
failedBrandLogo === configuredBrandLogo ? fallbackBrandLogo : configuredBrandLogo;
const handleBrandLogoError = () => {
if (brandLogo !== fallbackBrandLogo) {
setFailedBrandLogo(brandLogo);
}
};
const [notificationOpen, setNotificationOpen] = useState(false);
const { data: unreadCount = 0 } = useUnreadCount();
const navigate = useNavigate();
@@ -59,7 +71,8 @@ const Header = ({ collapsed }: HeaderProps) => {
const positionName = myPosition?.name?.trim() || "";
const displayName = liveAccount.displayName;
const showTopNav = isDark && !collapsed && !isWujieEnv();
// 深色侧栏收起只影响左侧导航,不应让顶栏回退到浅色的图标布局。
const showTopNav = isDark && !isWujieEnv();
// 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。
// 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏,
// 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。
@@ -226,7 +239,19 @@ const Header = ({ collapsed }: HeaderProps) => {
{showTopNav ? (
<>
{/* 左分区(flex-1):菜单靠右贴近中间基座 */}
<div className="flex h-full min-w-0 flex-1 items-center justify-end">
<div className="flex h-full min-w-0 flex-1 items-center justify-between">
<div
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-cyan-300/25 bg-blue-950/55"
title="系统 Logo"
>
<img
src={brandLogo}
alt=""
aria-hidden="true"
className="h-[35px] w-[35px] object-contain"
onError={handleBrandLogoError}
/>
</div>
<nav
className="top-submenu-bar top-submenu-bar--group"
aria-label="左侧菜单"
@@ -237,8 +262,12 @@ const Header = ({ collapsed }: HeaderProps) => {
</nav>
</div>
{/* 中:CM助手(落在背景图发光基座上,纯文字无图标) */}
<div className="header-center-title">{desensitizeText("CM业务中心")}</div>
{/* 中:系统 logo + 名称,落在背景图发光基座上。 */}
<div className="header-center-title">
<span className="header-center-title__label">
{desensitizeText("CM业务中心")}
</span>
</div>
{/* 右分区(flex-1):菜单靠左贴近中间基座,操作区靠最右 */}
<div className="flex h-full min-w-0 flex-1 items-center justify-between">
@@ -260,10 +289,16 @@ const Header = ({ collapsed }: HeaderProps) => {
{/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */}
<div className="flex h-full min-w-0 flex-1 items-center">
<div className="flex flex-shrink-0 items-center gap-2 pr-1">
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-white/20">
<FileText className="h-4 w-4 text-white" />
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-white/20">
<img
src={brandLogo}
alt=""
aria-hidden="true"
className="h-[35px] w-[35px] object-contain"
onError={handleBrandLogoError}
/>
</div>
<span className="whitespace-nowrap text-base font-semibold text-white">
<span className="whitespace-nowrap text-lg font-semibold text-white">
{desensitizeText("CM业务中心")}
</span>
</div>
@@ -287,7 +322,13 @@ const Header = ({ collapsed }: HeaderProps) => {
: "bg-white/20",
)}
>
<FileText className="h-4 w-4 text-white" />
<img
src={brandLogo}
alt=""
aria-hidden="true"
className="h-7 w-7 object-contain"
onError={handleBrandLogoError}
/>
</div>
{!collapsed && (
<span className="text-sm font-semibold text-white">
+3
View File
@@ -1179,6 +1179,9 @@ html.dark .strategy-app-header {
white-space: nowrap;
pointer-events: none;
user-select: none;
}
.header-center-title__label {
/* 白→浅蓝→白 金属质感渐变文字 */
background: linear-gradient(
180deg,