This commit is contained in:
董宏凯 committed 2026-06-10 17:44:51 +08:00
commit 19aeba5fab
8 files changed
+249 -85

No files matched your search

@@ -21,6 +21,7 @@ import {
Grid3X3,
HelpCircle,
History,
Layers,
Layout,
Library,
List,
@@ -209,10 +210,16 @@ export const sidebarMenuItems: MenuItem[] = [
{ id: "mgmt-agents", label: "智能体管理", icon: Bot, path: "/page/strategy/agents" },
{
id: "mgmt-business-chain-mapping",
label: "业务链条管理",
label: "配置管理",
icon: Network,
path: "/page/strategy/business-chain-mapping",
},
{
id: "mgmt-business-chains",
label: "业务链条配置",
icon: Layers,
path: "/page/strategy/business-chains",
},
{ id: "mgmt-skills", label: "技能管理", icon: Sparkles, path: "/page/strategy/skills" },
{ id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" },
{
@@ -1,5 +1,5 @@
/**
* 业务链条管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。
* 配置管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。
*
* 表格三列:业务(只读)/ 智能体 / 业务链。编辑模式下后两列为可搜索下拉:
* - 智能体来自 `/api/agents`
@@ -129,7 +129,7 @@ export default function BusinessChainMappingPage() {
<div className="mx-auto flex min-h-full max-w-[1280px] flex-col gap-5 px-6 py-6 lg:px-8">
<div className="space-y-1">
<h1 className={`text-[26px] font-semibold tracking-[0.02em] ${isDark ? "text-slate-50" : "text-[#222222]"}`}>
业务链条管理
配置管理
</h1>
<p className={`text-sm ${isDark ? "text-slate-300/80" : "text-slate-500"}`}>
为每个业务绑定智能体与业务链。多智能体会商从对应业务自动取用此处配置的业务链。业务链仅可选择「公共」链条。
@@ -17,6 +17,8 @@ import { ActionAnalyzes } from "@/strategy-components/components/task-components
import KnowledgeBaseResourceManagement from "@/strategy-components/components/resource-management/KnowledgeBaseResourceManagement";
import NetworkActorResourceManagement from "@/strategy-components/components/resource-management/NetworkActorResourceManagement";
import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanningPage";
import BusinessChainListPage from "@/roundtable-planning/pages/BusinessChainListPage";
import BusinessChainEditorPage from "@/roundtable-planning/pages/BusinessChainEditorPage";
import "@/roundtable-planning/styles/roundtable-planning.css";
import AgentChatPage from "./AgentChatPage";
import AgentsPage from "./AgentsPage";
@@ -174,6 +176,8 @@ export default function StrategyRoutes() {
<Route path="skills/evolution" element={<SkillEvolutionPage />} />
<Route path="memory" element={<MemoryPage />} />
<Route path="business-chain-mapping" element={<BusinessChainMappingPage />} />
<Route path="business-chains" element={<BusinessChainListPage />} />
<Route path="roundtable/chains" element={<BusinessChainEditorPage />} />
<Route
path="scheduled"
@@ -42,6 +42,7 @@ import StudioNotebooksPage from './StudioNotebooksPage';
import StudioNotebookDetailPage from './StudioNotebookDetailPage';
import RoundtablePlanningPage from '@/roundtable-planning/pages/RoundtablePlanningPage';
import BusinessChainEditorPage from '@/roundtable-planning/pages/BusinessChainEditorPage';
import BusinessChainListPage from '@/roundtable-planning/pages/BusinessChainListPage';
import DispatchChainPreviewPage from '@/roundtable-planning/pages/DispatchChainPreviewPage';
import '@/roundtable-planning/styles/roundtable-planning.css';
import { cn } from '@/lib/utils';
@@ -326,6 +327,14 @@ export default function WorkspaceRoutes({
</WorkspaceLayout>
}
/>
<Route
path="business-chains"
element={
<WorkspaceLayout {...layoutProps}>
<BusinessChainListPage />
</WorkspaceLayout>
}
/>
{/* Phase 7 临时预览入口:派活链路流程图弹窗(假数据自测,接通后台后可删) */}
<Route
path="roundtable/chain-preview"
@@ -40,11 +40,30 @@ export interface BusinessChainListDialogProps {
onConfigure: (chainId?: string) => void;
}
export function BusinessChainListDialog({
open,
onOpenChange,
export interface BusinessChainListBodyProps {
/**
* 是否激活(控制何时拉取列表):弹窗里传 `open`(打开才拉),
* 独立页面里恒为 `true`。
*/
active: boolean;
/** 进入配置页。带 chainId = 打开并切到该链条;不带 = 空白新建。 */
onConfigure: (chainId?: string) => void;
/** 列表为空时的「去创建」引导文案差异(弹窗说「点下方配置」,页面说「点右上配置」)。 */
emptyHint?: string;
}
/**
* 业务链条列表主体(筛选 tab + 滚动卡片列表)。
*
* 从 `BusinessChainListDialog` 抽出,供弹窗与独立配置页(`BusinessChainListPage`)
* 复用,保证两处的加载 / 发布 / 卡片渲染逻辑完全一致。外层负责标题栏与「配置」入口、
* 以及撑出 `flex flex-col` 高度(本组件用 `flex-1` 占满剩余空间)。
*/
export function BusinessChainListBody({
active,
onConfigure,
}: BusinessChainListDialogProps) {
emptyHint,
}: BusinessChainListBodyProps) {
const [chains, setChains] = useState<BusinessChain[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -65,9 +84,9 @@ export function BusinessChainListDialog({
}
}, []);
// 每次打开 / 切换筛选都重新拉,保证配置页改动后回来看到的是最新列表。
// 每次激活 / 切换筛选都重新拉,保证配置页改动后回来看到的是最新列表。
useEffect(() => {
if (!open) return;
if (!active) return;
let cancelled = false;
void (async () => {
setLoading(true);
@@ -84,7 +103,7 @@ export function BusinessChainListDialog({
return () => {
cancelled = true;
};
}, [open, scope]);
}, [active, scope]);
const handleTogglePublic = useCallback(
async (chain: BusinessChain) => {
@@ -101,6 +120,73 @@ export function BusinessChainListDialog({
[refresh, scope],
);
return (
<>
{/* 筛选:我的 / 公共 */}
<div className="shrink-0 flex items-center gap-1 rounded-lg border border-border bg-muted/40 p-0.5 self-start">
{(["mine", "public"] as const).map((s) => (
<button
key={s}
type="button"
onClick={() => setScope(s)}
className={cn(
"rounded-md px-3 py-1 text-xs font-medium transition",
scope === s
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
{s === "mine" ? "我的" : "公共"}
</button>
))}
</div>
<div className="custom-scrollbar -mr-1 min-h-0 flex-1 overflow-y-auto py-3 pr-1">
{loading ? (
<div className="flex items-center gap-2 px-1 py-10 text-sm text-muted-foreground">
<Loader2Icon className="size-4 animate-spin" />
正在加载业务链条…
</div>
) : error ? (
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
加载业务链条失败:{error}
</div>
) : chains.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-3 px-6 py-12 text-center">
<LayersIcon className="size-10 text-primary/40" />
<div className="text-sm font-semibold">
{scope === "public" ? "还没有公共链条" : "还没有业务链条"}
</div>
<p className="max-w-sm text-xs leading-relaxed text-muted-foreground">
{scope === "public"
? "公共链条是其他账号(或你自己)发布出来、所有人可见的链条。去「我的」里发布一条试试。"
: emptyHint ??
"业务链条是可复用的有序智能体编排。点击下方「配置」去创建第一条链条。"}
</p>
</div>
) : (
<div className="space-y-3.5">
{chains.map((chain) => (
<ChainRow
key={chain.id}
chain={chain}
publishing={publishingId === chain.id}
onSettings={() => onConfigure(chain.id)}
onTogglePublic={() => handleTogglePublic(chain)}
/>
))}
</div>
)}
</div>
</>
);
}
export function BusinessChainListDialog({
open,
onOpenChange,
onConfigure,
}: BusinessChainListDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[85vh] flex-col gap-0 sm:max-w-2xl">
@@ -114,61 +200,7 @@ export function BusinessChainListDialog({
</DialogDescription>
</DialogHeader>
{/* 筛选:我的 / 公共 */}
<div className="shrink-0 flex items-center gap-1 rounded-lg border border-border bg-muted/40 p-0.5 self-start">
{(["mine", "public"] as const).map((s) => (
<button
key={s}
type="button"
onClick={() => setScope(s)}
className={cn(
"rounded-md px-3 py-1 text-xs font-medium transition",
scope === s
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground",
)}
>
{s === "mine" ? "我的" : "公共"}
</button>
))}
</div>
<div className="custom-scrollbar -mr-1 min-h-0 flex-1 overflow-y-auto py-3 pr-1">
{loading ? (
<div className="flex items-center gap-2 px-1 py-10 text-sm text-muted-foreground">
<Loader2Icon className="size-4 animate-spin" />
正在加载业务链条…
</div>
) : error ? (
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
加载业务链条失败:{error}
</div>
) : chains.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-3 px-6 py-12 text-center">
<LayersIcon className="size-10 text-primary/40" />
<div className="text-sm font-semibold">
{scope === "public" ? "还没有公共链条" : "还没有业务链条"}
</div>
<p className="max-w-sm text-xs leading-relaxed text-muted-foreground">
{scope === "public"
? "公共链条是其他账号(或你自己)发布出来、所有人可见的链条。去「我的」里发布一条试试。"
: "业务链条是可复用的有序智能体编排。点击下方「配置」去创建第一条链条。"}
</p>
</div>
) : (
<div className="space-y-3.5">
{chains.map((chain) => (
<ChainRow
key={chain.id}
chain={chain}
publishing={publishingId === chain.id}
onSettings={() => onConfigure(chain.id)}
onTogglePublic={() => handleTogglePublic(chain)}
/>
))}
</div>
)}
</div>
<BusinessChainListBody active={open} onConfigure={onConfigure} />
<DialogFooter className="shrink-0 border-t pt-3 sm:justify-end">
<Button onClick={() => onConfigure()} className="gap-1.5">
@@ -246,22 +278,70 @@ function ChainRow({
<div className="mt-1.5 line-clamp-2 text-sm text-muted-foreground">{chain.description}</div>
)}
{/* 席位名 tag(按顺序,→ 箭头连接发言次序) */}
<div className="mt-3.5 flex flex-wrap items-center gap-1.5">
{chain.seats.map((seat, i) => (
<Fragment key={seat.agent_id}>
{i > 0 && <ArrowRightIcon className="size-3.5 shrink-0 text-muted-foreground/50" />}
<span
className={cn(
"rounded-md border px-2 py-1 text-xs font-medium",
chainTagClasses(getAvatarTypeFor(seat.agent_id, i)),
)}
>
{seat.name || seat.agent_id}
</span>
</Fragment>
))}
</div>
{/* 席位编排:按 stage 分组——并行阶段竖向并列在虚线框里,阶段之间用 → 串联。 */}
<ChainSeatsFlow chain={chain} />
</div>
);
}
// ── 链条席位编排流(体现「同层并行 · 层间串行」)─────────────────────────────
function ChainSeatsFlow({ chain }: { chain: BusinessChain }) {
// 名称回退表:用落库快照(seats)解析 stage 里的 agent_id。
const seatById = new Map(chain.seats.map((s) => [s.agent_id, s] as const));
// 分层来源:带 stages(分层编排)就按 stages 渲染(过滤空阶段);否则每个席位各成
// 一段、顺序串行(线性链,与旧版展示一致)。
const stages =
chain.stages && chain.stages.length > 0
? chain.stages.filter((s) => s.length > 0)
: chain.seats.map((s) => [s.agent_id]);
// 跨阶段累加的全局序号 —— 决定头像配色,与编辑器 / 展平 seats 顺序对齐。
const orderById = new Map<string, number>();
let counter = 0;
for (const stageIds of stages) {
for (const id of stageIds) {
orderById.set(id, counter);
counter += 1;
}
}
const seatTag = (agentId: string) => {
const seat = seatById.get(agentId);
return (
<span
key={agentId}
className={cn(
"rounded-md border px-2 py-1 text-xs font-medium",
chainTagClasses(getAvatarTypeFor(agentId, orderById.get(agentId) ?? 0)),
)}
>
{seat?.name || agentId}
</span>
);
};
return (
<div className="mt-3.5 flex flex-wrap items-center gap-1.5">
{stages.map((stageIds, stageIdx) => (
<Fragment key={stageIdx}>
{stageIdx > 0 && (
<ArrowRightIcon className="size-3.5 shrink-0 text-muted-foreground/50" />
)}
{stageIds.length > 1 ? (
// 并行阶段:席位竖向并列在虚线框里,左上角标「并行 N」。
<div className="relative flex flex-col gap-1 rounded-lg border border-dashed border-primary/40 bg-primary/[0.04] px-1.5 pb-1.5 pt-2.5">
<span className="pointer-events-none absolute -top-2 left-1.5 rounded-full bg-card px-1.5 text-[10px] font-semibold leading-tight text-primary">
并行 {stageIds.length}
</span>
{stageIds.map((id) => seatTag(id))}
</div>
) : (
seatTag(stageIds[0])
)}
</Fragment>
))}
</div>
);
}
@@ -0,0 +1,64 @@
/**
* 业务链条配置页(独立页面版)。
*
* 把原本挂在圆桌页顶栏「业务链条」按钮上的 `BusinessChainListDialog` 弹窗内容,
* 单独画成一个全屏页面:综合管理 → 业务链条配置 入口进来即是本页。
*
* - 列表主体复用 `BusinessChainListBody`(与弹窗同一份加载 / 发布 / 卡片逻辑)。
* - 顶部「配置」按钮 = 进入编辑器空白新建;卡片「设置」= 进入编辑器并切到该链条。
*
* 路由 base 感知:用 `useRouteNavigate` 跳转,strategy / workspace 两套路由树下
* 都能落到当前 base 的编辑器(`roundtable/chains`)。
*/
import { LayersIcon, SettingsIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useRouteNavigate } from "@/core/route-base";
import { BusinessChainListBody } from "@/roundtable-planning/components/BusinessChainListDialog";
export default function BusinessChainListPage() {
const navigate = useRouteNavigate();
// 进入编辑器:带 chainId = 打开并切到该链条;不带 = 空白新建。
// 路径写 workspace base,`useRouteNavigate` 会按当前路由 base 改写。
const goConfigure = (chainId?: string) => {
navigate(
chainId
? `/page/workspace/roundtable/chains?chainId=${encodeURIComponent(chainId)}`
: "/page/workspace/roundtable/chains",
);
};
return (
<div className="flex h-full min-h-0 flex-col bg-muted/40 text-foreground">
{/* 顶栏:标题 + 描述 + 配置入口 */}
<header className="shrink-0 border-b border-border bg-card px-6 py-4">
<div className="mx-auto flex w-full max-w-3xl items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2 text-base font-bold text-foreground">
<LayersIcon className="size-5 text-primary" />
业务链条配置
</div>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
可复用的有序智能体编排。点击卡片「设置」可进入编辑器修改该链条;「发布」后所有账号都能在「公共」里查看 / 选用。
</p>
</div>
<Button onClick={() => goConfigure()} className="shrink-0 gap-1.5">
<SettingsIcon className="size-4" />
配置
</Button>
</div>
</header>
{/* 列表主体(与弹窗共用 BusinessChainListBody) */}
<div className="mx-auto flex w-full min-h-0 max-w-3xl flex-1 flex-col px-6 py-4">
<BusinessChainListBody
active
onConfigure={goConfigure}
emptyHint="业务链条是可复用的有序智能体编排。点击右上角「配置」去创建第一条链条。"
/>
</div>
</div>
);
}
@@ -170,7 +170,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
setShowAnalysisModal(false);
};
// 单智能体分析:用「业务链条管理」里为本业务配置的智能体,跳转到其问答页(新会话),
// 单智能体分析:用「配置管理」里为本业务配置的智能体,跳转到其问答页(新会话),
// 把任务名称/简介作为开场消息暂存,由 AgentChatPage 读取 sessionStorage.pendingAgentMessage
// 自动发出并开始问答。未配置智能体则提示去配置。
const handleSingleAgentAnalysis = async () => {
@@ -184,7 +184,7 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
/* ignore → 下面按未配置处理 */
}
if (!agentId) {
toast.error('该业务尚未在「业务链条管理」中配置单智能体');
toast.error('该业务尚未在「配置管理」中配置单智能体');
return;
}
const text = [
@@ -170,7 +170,7 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
setShowAnalysisModal(false);
};
// 单智能体分析:用「业务链条管理」里为本业务配置的智能体,跳转到其问答页(新会话),
// 单智能体分析:用「配置管理」里为本业务配置的智能体,跳转到其问答页(新会话),
// 把任务名称/简介作为开场消息暂存,由 AgentChatPage 读取 sessionStorage.pendingAgentMessage
// 自动发出并开始问答。未配置智能体则提示去配置。
const handleSingleAgentAnalysis = async () => {
@@ -184,7 +184,7 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
/* ignore → 下面按未配置处理 */
}
if (!agentId) {
toast.error('该业务尚未在「业务链条管理」中配置单智能体');
toast.error('该业务尚未在「配置管理」中配置单智能体');
return;
}
const text = [