套用wujie修改

This commit is contained in:
董宏凯 committed 2026-06-11 17:36:20 +08:00
1 parent c48317672c
commit 31525e79a8
5 files changed
+171 -110

No files matched your search

+85 -75
View File
@@ -4,28 +4,38 @@ import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog"; import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import { isWujieEnv } from "@/core/embed/wujie";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
function Dialog({ function Dialog({
modal,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) { }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />; // 无界嵌入时 Radix modal 会给 body 注入异常 margin-right,导致右侧空白。
// 默认降为 non-modal;显式传 modal={true} 仍可强制 modal。
return (
<DialogPrimitive.Root
data-slot="dialog"
modal={modal ?? !isWujieEnv()}
{...props}
/>
);
} }
const DialogTrigger = React.forwardRef< const DialogTrigger = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Trigger>, React.ElementRef<typeof DialogPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
return ( return (
<DialogPrimitive.Trigger <DialogPrimitive.Trigger
ref={ref} ref={ref}
data-slot="dialog-trigger" data-slot="dialog-trigger"
{...props} {...props}
/> />
); );
}); });
DialogTrigger.displayName = DialogPrimitive.Trigger.displayName; DialogTrigger.displayName = DialogPrimitive.Trigger.displayName;
function DialogPortal({ function DialogPortal({
...props ...props
@@ -33,68 +43,68 @@ function DialogPortal({
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
} }
const DialogClose = React.forwardRef< const DialogClose = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Close>, React.ElementRef<typeof DialogPrimitive.Close>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
>(({ ...props }, ref) => { >(({ ...props }, ref) => {
return <DialogPrimitive.Close ref={ref} data-slot="dialog-close" {...props} />; return <DialogPrimitive.Close ref={ref} data-slot="dialog-close" {...props} />;
}); });
DialogClose.displayName = DialogPrimitive.Close.displayName;
const DialogOverlay = React.forwardRef< DialogClose.displayName = DialogPrimitive.Close.displayName;
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => {
return (
<DialogPrimitive.Overlay
ref={ref}
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className,
)}
{...props}
/>
);
});
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
showCloseButton?: boolean; >(({ className, ...props }, ref) => {
} return (
>(({ className, children, showCloseButton = true, ...props }, ref) => { <DialogPrimitive.Overlay
return ( ref={ref}
<DialogPortal data-slot="dialog-portal"> data-slot="dialog-overlay"
<DialogOverlay /> className={cn(
<DialogPrimitive.Content "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
ref={ref} className,
data-slot="dialog-content" )}
className={cn( {...props}
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg", />
className, );
)} });
{...props}
> DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
{children}
{showCloseButton && ( const DialogContent = React.forwardRef<
<DialogPrimitive.Close React.ElementRef<typeof DialogPrimitive.Content>,
data-slot="dialog-close" React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" showCloseButton?: boolean;
> }
<XIcon /> >(({ className, children, showCloseButton = true, ...props }, ref) => {
<span className="sr-only">Close</span> return (
</DialogPrimitive.Close> <DialogPortal data-slot="dialog-portal">
)} <DialogOverlay />
</DialogPrimitive.Content> <DialogPrimitive.Content
</DialogPortal> ref={ref}
); data-slot="dialog-content"
}); className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
DialogContent.displayName = DialogPrimitive.Content.displayName; className,
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
@@ -6,7 +6,7 @@ import { useSearchParams } from "next/navigation";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { loginByUsername } from "@/core/auth/api"; import { loginByUsername } from "@/core/auth/api";
import { setStoredAuth } from "@/core/auth"; import { setStoredAuth } from "@/core/auth";
import { readWujieProps } from "@/core/embed/wujie"; import { readWujieProps, isWujieEnv } from "@/core/embed/wujie";
import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage"; import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage";
import { ChatRuntime } from "./WorkspaceRoutes"; import { ChatRuntime } from "./WorkspaceRoutes";
import "@/roundtable-planning/styles/roundtable-planning.css"; import "@/roundtable-planning/styles/roundtable-planning.css";
@@ -66,6 +66,12 @@ export default function EmbedRoundtablePage() {
setTheme(resolveEmbedRoundtableTheme(themeParam)); setTheme(resolveEmbedRoundtableTheme(themeParam));
}, [themeParam, setTheme]); }, [themeParam, setTheme]);
useEffect(() => {
if (!isWujieEnv()) return;
document.documentElement.classList.add("wujie-embed");
return () => document.documentElement.classList.remove("wujie-embed");
}, []);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
async function runLogin() { async function runLogin() {
@@ -27,7 +27,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { isWujieEnv } from "@/core/embed/wujie";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { listChains, setChainPublic, type ChainScope } from "../api/chains"; import { listChains, setChainPublic, type ChainScope } from "../api/chains";
@@ -188,15 +187,8 @@ export function BusinessChainListDialog({
onOpenChange, onOpenChange,
onConfigure, onConfigure,
}: BusinessChainListDialogProps) { }: BusinessChainListDialogProps) {
const inWujie = isWujieEnv();
return ( return (
<Dialog <Dialog open={open} onOpenChange={onOpenChange}>
open={open}
onOpenChange={onOpenChange}
// Wujie 内部容器场景下,Radix modal 的 body-scroll-lock 可能计算出异常
// margin-right,导致右侧出现大片空白。这里降为 non-modal 规避布局抖动。
modal={!inWujie}
>
<DialogContent className="flex max-h-[85vh] flex-col gap-0 sm:max-w-2xl"> <DialogContent className="flex max-h-[85vh] flex-col gap-0 sm:max-w-2xl">
<DialogHeader className="shrink-0"> <DialogHeader className="shrink-0">
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
@@ -157,6 +157,8 @@ export default function RoundtablePlanningPage() {
const isDark = theme === "dark"; const isDark = theme === "dark";
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
/** 无界(wujie)嵌入抽屉:隐藏业务链条入口,设置按钮改作手动回传交付。 */
const inWujieEmbed = isWujieEnv();
// ─── 会商自动化串联(模块 C):任务页带进来的预填 ───────────────── // ─── 会商自动化串联(模块 C):任务页带进来的预填 ─────────────────
// 只在首次渲染读一次 navigation state;随后清掉 history state,避免刷新/后退重放。 // 只在首次渲染读一次 navigation state;随后清掉 history state,避免刷新/后退重放。
@@ -167,6 +169,12 @@ export default function RoundtablePlanningPage() {
); );
// 由 prefill.businessCode 解析出的「自动选用业务链」结果;有值时「进入研讨」跳过选链弹窗直接进 Step 2。 // 由 prefill.businessCode 解析出的「自动选用业务链」结果;有值时「进入研讨」跳过选链弹窗直接进 Step 2。
const [autoChainResult, setAutoChainResult] = useState<SeatSelectionResult | null>(null); const [autoChainResult, setAutoChainResult] = useState<SeatSelectionResult | null>(null);
/** 无界直传 chainId 时,链条解析完成前禁止弹选链窗(避免 modal 布局抖动)。 */
const [autoChainLoading, setAutoChainLoading] = useState(
() =>
inWujieEmbed &&
!!(prefillRef.current?.chainId || prefillRef.current?.businessCode),
);
const autoStartedRef = useRef(false); const autoStartedRef = useRef(false);
const prefillClearedRef = useRef(false); const prefillClearedRef = useRef(false);
// 共识达成后只向父页面(无界)回传一次交付结果。 // 共识达成后只向父页面(无界)回传一次交付结果。
@@ -774,6 +782,7 @@ export default function RoundtablePlanningPage() {
useEffect(() => { useEffect(() => {
const prefill = prefillRef.current; const prefill = prefillRef.current;
if (!prefill?.chainId && !prefill?.businessCode) return; if (!prefill?.chainId && !prefill?.businessCode) return;
setAutoChainLoading(true);
let cancelled = false; let cancelled = false;
void (async () => { void (async () => {
try { try {
@@ -794,6 +803,8 @@ export default function RoundtablePlanningPage() {
if (!cancelled && result) setAutoChainResult(result); if (!cancelled && result) setAutoChainResult(result);
} catch { } catch {
/* 解析失败 → 保持 null,回退弹窗手选 */ /* 解析失败 → 保持 null,回退弹窗手选 */
} finally {
if (!cancelled) setAutoChainLoading(false);
} }
})(); })();
return () => { return () => {
@@ -801,13 +812,9 @@ export default function RoundtablePlanningPage() {
}; };
}, []); }, []);
// C4(无界回传):第二步达成共识后,把总控结论 + 各智能体交付一次性广播给父页面。 /** 组装无界回传载荷:总控共识 + 各智能体最终交付(剔除总控席位)。 */
// 用 ref 防重(共识态会多次触发渲染);非无界环境静默跳过。 const buildWujieConsensusPayload = useCallback(
useEffect(() => { () => ({
if (!step2.hasConsensus || consensusEmittedRef.current) return;
if (!isWujieEnv()) return;
consensusEmittedRef.current = true;
emitWujieEvent("roundtable:consensus", {
taskTitle: prefillRef.current?.taskTitle ?? null, taskTitle: prefillRef.current?.taskTitle ?? null,
chainId: prefillRef.current?.chainId ?? null, chainId: prefillRef.current?.chainId ?? null,
chainTitle: chainMeta?.title ?? null, chainTitle: chainMeta?.title ?? null,
@@ -817,14 +824,43 @@ export default function RoundtablePlanningPage() {
step2.step2RoundtableDialogues, step2.step2RoundtableDialogues,
step2.coordinatorName, step2.coordinatorName,
), ),
}); }),
}, [ [
step2.hasConsensus, chainMeta,
step2.coordinatorName, step2.coordinatorName,
step2.lastLeaderContent, step2.lastLeaderContent,
step2.step2RoundtableDialogues, step2.step2RoundtableDialogues,
chainMeta, ],
]); );
const emitConsensusToParent = useCallback(() => {
if (!inWujieEmbed) return false;
return emitWujieEvent("roundtable:consensus", buildWujieConsensusPayload());
}, [buildWujieConsensusPayload, inWujieEmbed]);
const handleSettingsClick = useCallback(() => {
if (inWujieEmbed) {
const deliverables = buildWujieConsensusPayload().deliverables;
if (deliverables.length === 0) {
triggerToast("暂无可回传的智能体交付结果");
return;
}
if (emitConsensusToParent()) {
triggerToast(`已回传 ${deliverables.length} 个智能体交付结果`);
}
return;
}
triggerToast("⚙️ 系统管理面板拉取中(暂不开放外部审计)");
}, [buildWujieConsensusPayload, emitConsensusToParent, inWujieEmbed, triggerToast]);
// C4(无界回传):第二步达成共识后,把总控结论 + 各智能体交付一次性广播给父页面。
// 用 ref 防重(共识态会多次触发渲染);非无界环境静默跳过。
useEffect(() => {
if (!step2.hasConsensus || consensusEmittedRef.current) return;
if (!inWujieEmbed) return;
consensusEmittedRef.current = true;
emitConsensusToParent();
}, [step2.hasConsensus, inWujieEmbed, emitConsensusToParent]);
// 把页面重置回「第一步全新任务」——新建任务 + 后台挂起后都复用。 // 把页面重置回「第一步全新任务」——新建任务 + 后台挂起后都复用。
// 注意:只清前端状态,不删后端已建的草稿 / 已挂起的后台作业(它们继续在后台跑, // 注意:只清前端状态,不删后端已建的草稿 / 已挂起的后台作业(它们继续在后台跑,
@@ -1016,15 +1052,17 @@ export default function RoundtablePlanningPage() {
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button {!inWujieEmbed && (
id="business-chain-btn" <button
onClick={() => setIsChainListOpen(true)} id="business-chain-btn"
title="查看 / 配置可复用的业务链条(有序智能体编排)" onClick={() => setIsChainListOpen(true)}
className="bg-white hover:bg-slate-50 border border-[#e2e8f0] text-[#334155] text-xs font-bold px-3 py-2 rounded-xl flex items-center gap-1.5 transition dark:border-border dark:bg-secondary dark:text-foreground dark:hover:bg-accent" title="查看 / 配置可复用的业务链条(有序智能体编排)"
> className="bg-white hover:bg-slate-50 border border-[#e2e8f0] text-[#334155] text-xs font-bold px-3 py-2 rounded-xl flex items-center gap-1.5 transition dark:border-border dark:bg-secondary dark:text-foreground dark:hover:bg-accent"
<ListOrdered className="w-3.5 h-3.5 text-primary" /> >
业务链条 <ListOrdered className="w-3.5 h-3.5 text-primary" />
</button> 业务链条
</button>
)}
<button <button
id="new-task-btn" id="new-task-btn"
@@ -1083,7 +1121,8 @@ export default function RoundtablePlanningPage() {
<button <button
id="settings-btn" id="settings-btn"
onClick={() => triggerToast("⚙️ 系统管理面板拉取中(暂不开放外部审计)")} onClick={handleSettingsClick}
title={inWujieEmbed ? "回传各智能体交付结果给宿主" : "系统设置"}
className="p-1.5 text-slate-400 hover:text-slate-600 rounded-lg hover:bg-slate-100 transition-colors dark:text-muted-foreground dark:hover:bg-accent dark:hover:text-foreground" className="p-1.5 text-slate-400 hover:text-slate-600 rounded-lg hover:bg-slate-100 transition-colors dark:text-muted-foreground dark:hover:bg-accent dark:hover:text-foreground"
> >
<Settings className="w-4 h-4" /> <Settings className="w-4 h-4" />
@@ -1142,6 +1181,14 @@ export default function RoundtablePlanningPage() {
void drafts.ensureDraft(1).finally(() => handleConfirmAgents(result)); void drafts.ensureDraft(1).finally(() => handleConfirmAgents(result));
return; return;
} }
if (
inWujieEmbed &&
autoChainLoading &&
(prefillRef.current?.chainId || prefillRef.current?.businessCode)
) {
triggerToast("正在加载业务链条,请稍候…");
return;
}
// 先把草稿落地拿到 draft_id,推荐历史才有归属;失败也照常打开弹窗 // 先把草稿落地拿到 draft_id,推荐历史才有归属;失败也照常打开弹窗
// (弹窗在无 draftId 时退化为「直接推荐、不存历史」)。 // (弹窗在无 draftId 时退化为「直接推荐、不存历史」)。
void drafts.ensureDraft(1).finally(() => setIsRecommendDialogOpen(true)); void drafts.ensureDraft(1).finally(() => setIsRecommendDialogOpen(true));
+6
View File
@@ -337,6 +337,12 @@
height: 100%; height: 100%;
} }
/* 无界嵌入:Radix scroll-lock 可能误算 margin-right,留下右侧空白条 */
html.wujie-embed body {
margin-right: 0 !important;
padding-right: 0 !important;
}
[data-sidebar="menu-button"][data-active="true"] { [data-sidebar="menu-button"][data-active="true"] {
box-shadow: inset 3px 0 0 var(--primary); box-shadow: inset 3px 0 0 var(--primary);
font-weight: 600; font-weight: 600;