diff --git a/frontend-web/.env.production b/frontend-web/.env.production index c655d856..d7dac2cd 100644 --- a/frontend-web/.env.production +++ b/frontend-web/.env.production @@ -5,6 +5,8 @@ VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api +# CMZS后端接口 +VITE_API_BASE_URL=https://ch1.b.uat.4.cn/zncm # 轻应用「新建笔记」系列接口 VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api # 佳来模型对话接口 diff --git a/frontend-web/src/core/page-layout/sidebar-menu.ts b/frontend-web/src/core/page-layout/sidebar-menu.ts index 674e7e52..d2133107 100644 --- a/frontend-web/src/core/page-layout/sidebar-menu.ts +++ b/frontend-web/src/core/page-layout/sidebar-menu.ts @@ -22,6 +22,7 @@ import { Grid3X3, HelpCircle, History, + Layers, Layout, Library, List, @@ -61,8 +62,13 @@ export const sidebarMenuItems: MenuItem[] = [ path: "/page/strategy/qa/multi-agent-roundtable", }, // { id: "expert-qa", label: "专家问答", icon: Brain, path: "/page/strategy/qa/expert-qa" }, - { id: "qa-zzjc", label: "zzjc支持", icon: Bot }, - { id: "qa-zzxd", label: "zzXD应用", icon: Layout }, + { + id: "qa-zzjc", + label: "zzjc支持", + icon: Bot, + path: "/page/strategy/agents/6bf/chats/new", + }, + { id: "qa-zzxd", label: "zzXD应用", icon: Layout, path: "/page/strategy/agents/10d69686718349688e58a8790cb61024/chats/new", }, { id: "qa-research", label: "课题研究", icon: FileText, path: "/page/canvas/ai-writing" }, ], }, @@ -209,10 +215,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" }, { diff --git a/frontend-web/src/pages/BusinessChainMappingPage.tsx b/frontend-web/src/pages/BusinessChainMappingPage.tsx index a3d05703..ae2db62d 100644 --- a/frontend-web/src/pages/BusinessChainMappingPage.tsx +++ b/frontend-web/src/pages/BusinessChainMappingPage.tsx @@ -1,5 +1,5 @@ /** - * 业务链条管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。 + * 配置管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。 * * 表格三列:业务(只读)/ 智能体 / 业务链。编辑模式下后两列为可搜索下拉: * - 智能体来自 `/api/agents` @@ -129,7 +129,7 @@ export default function BusinessChainMappingPage() {

- 业务链条管理 + 配置管理

为每个业务绑定智能体与业务链。多智能体会商从对应业务自动取用此处配置的业务链。业务链仅可选择「公共」链条。 diff --git a/frontend-web/src/pages/StrategyRoutes.tsx b/frontend-web/src/pages/StrategyRoutes.tsx index 5f0129ed..dd8180b4 100644 --- a/frontend-web/src/pages/StrategyRoutes.tsx +++ b/frontend-web/src/pages/StrategyRoutes.tsx @@ -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() { } /> } /> } /> + } /> + } /> } /> + + + + } + /> {/* Phase 7 临时预览入口:派活链路流程图弹窗(假数据自测,接通后台后可删) */} 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([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(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 ( + <> + {/* 筛选:我的 / 公共 */} +

+ {(["mine", "public"] as const).map((s) => ( + + ))} +
+ +
+ {loading ? ( +
+ + 正在加载业务链条… +
+ ) : error ? ( +
+ 加载业务链条失败:{error} +
+ ) : chains.length === 0 ? ( +
+ +
+ {scope === "public" ? "还没有公共链条" : "还没有业务链条"} +
+

+ {scope === "public" + ? "公共链条是其他账号(或你自己)发布出来、所有人可见的链条。去「我的」里发布一条试试。" + : emptyHint ?? + "业务链条是可复用的有序智能体编排。点击下方「配置」去创建第一条链条。"} +

+
+ ) : ( +
+ {chains.map((chain) => ( + onConfigure(chain.id)} + onTogglePublic={() => handleTogglePublic(chain)} + /> + ))} +
+ )} +
+ + ); +} + +export function BusinessChainListDialog({ + open, + onOpenChange, + onConfigure, +}: BusinessChainListDialogProps) { return ( @@ -114,61 +200,7 @@ export function BusinessChainListDialog({ - {/* 筛选:我的 / 公共 */} -
- {(["mine", "public"] as const).map((s) => ( - - ))} -
- -
- {loading ? ( -
- - 正在加载业务链条… -
- ) : error ? ( -
- 加载业务链条失败:{error} -
- ) : chains.length === 0 ? ( -
- -
- {scope === "public" ? "还没有公共链条" : "还没有业务链条"} -
-

- {scope === "public" - ? "公共链条是其他账号(或你自己)发布出来、所有人可见的链条。去「我的」里发布一条试试。" - : "业务链条是可复用的有序智能体编排。点击下方「配置」去创建第一条链条。"} -

-
- ) : ( -
- {chains.map((chain) => ( - onConfigure(chain.id)} - onTogglePublic={() => handleTogglePublic(chain)} - /> - ))} -
- )} -
+
+ ); +} + +// ── 链条席位编排流(体现「同层并行 · 层间串行」)───────────────────────────── + +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(); + 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 ( + + {seat?.name || agentId} + + ); + }; + + return ( +
+ {stages.map((stageIds, stageIdx) => ( + + {stageIdx > 0 && ( + + )} + {stageIds.length > 1 ? ( + // 并行阶段:席位竖向并列在虚线框里,左上角标「并行 N」。 +
+ + 并行 {stageIds.length} + + {stageIds.map((id) => seatTag(id))} +
+ ) : ( + seatTag(stageIds[0]) + )} +
+ ))}
); } diff --git a/frontend-web/src/roundtable-planning/components/BusinessChainPicker.tsx b/frontend-web/src/roundtable-planning/components/BusinessChainPicker.tsx index 05dda9b6..bb431011 100644 --- a/frontend-web/src/roundtable-planning/components/BusinessChainPicker.tsx +++ b/frontend-web/src/roundtable-planning/components/BusinessChainPicker.tsx @@ -289,6 +289,43 @@ function ChainCard({ const deletedCount = chain.seats.length - survivingCount; const usable = survivingCount >= CHAIN_MIN_SEATS; + // ── 席位编排(体现「同层并行 · 层间串行」)───────────────────────────── + // 名称回退表:实时 name(nameById)→ 落库快照(seat.name)→ 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(); + let seatOrder = 0; + for (const stageIds of stages) { + for (const id of stageIds) { + orderById.set(id, seatOrder); + seatOrder += 1; + } + } + // 单个席位 tag:存活按配色,已删除置灰删除线(保留旧行为)。 + const seatTag = (agentId: string) => { + const alive = isExisting(agentId); + const label = nameById.get(agentId) ?? seatById.get(agentId)?.name ?? agentId; + return ( + + {label} + + ); + }; + return (
)} - {/* 智能体名 tag(按顺序,← 箭头连接) */} + {/* 席位编排:按 stage 分组——并行阶段竖向并列在虚线框里,阶段之间用 → 串联。 + 不用 :dev 期 tagger 会往 Fragment 注入 data-lov-id 触发 React 警告, + 这里用带 key 的扁平数组(箭头 + 阶段),注入落到真实元素上即可。 */}
- {chain.seats.flatMap((seat, i) => { - const alive = isExisting(seat.agent_id); - const label = nameById.get(seat.agent_id) ?? seat.name ?? seat.agent_id; - // 不用 :dev 期 tagger 会往 Fragment 注入 data-lov-id 触发 React 警告, - // 这里改成带 key 的扁平数组(箭头 + tag),注入落到真实元素上即可。 + {stages.flatMap((stageIds, stageIdx) => { const nodes: ReactNode[] = []; - if (i > 0) { + if (stageIdx > 0) { nodes.push( , ); } - nodes.push( - - {label} - , - ); + if (stageIds.length > 1) { + // 并行阶段:席位竖向并列在虚线框里,左上角标「并行 N」。 + nodes.push( +
+ + 并行 {stageIds.length} + + {stageIds.map((id) => seatTag(id))} +
, + ); + } else { + nodes.push(seatTag(stageIds[0])); + } return nodes; })}
diff --git a/frontend-web/src/roundtable-planning/hooks/useStep1Intent.ts b/frontend-web/src/roundtable-planning/hooks/useStep1Intent.ts index 89ec494e..251214f1 100644 --- a/frontend-web/src/roundtable-planning/hooks/useStep1Intent.ts +++ b/frontend-web/src/roundtable-planning/hooks/useStep1Intent.ts @@ -123,6 +123,12 @@ export interface UseStep1IntentReturn { clarifications: ClarificationPayload[], overrideByKey?: Record, ) => void; + /** + * 用户在「当前意图摘要」卡里手动编辑后保存:直接覆盖 `intentReady`。下游(Step 2 seed + * `buildInitialIntent`、左右侧栏摘要卡、step2Locked 判定)都读 `intentReady`,改这里即全局生效。 + * 持久化由调用方(页面)在更新后触发草稿保存。 + */ + updateIntentReady: (next: IntentSummary) => void; /** Abort 当前 intent 流。气泡尾部追加「(已暂停)」并 toast。 */ stopIntentStream: () => void; /** 从草稿加载:批量 setState + 标记 init 已完成,跳过自动 runStep1Init。 */ @@ -568,6 +574,12 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe setInteractiveMessages((prev) => [...prev, msg]); }, []); + const updateIntentReady = useCallback((next: IntentSummary) => { + // 用户手动改完意图 → 覆盖为权威值,并清掉「待澄清」态(已确认意图,无需再答问)。 + setIntentReady(next); + setPendingIntentClarificationMsgId(null); + }, []); + return { intentThreadId, intentReady, @@ -582,6 +594,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe runStep1Init, sendIntentMessage, submitIntentClarifications, + updateIntentReady, stopIntentStream, hydrateFromDraft, appendLocalMessage, diff --git a/frontend-web/src/roundtable-planning/pages/BusinessChainListPage.tsx b/frontend-web/src/roundtable-planning/pages/BusinessChainListPage.tsx new file mode 100644 index 00000000..0929ce46 --- /dev/null +++ b/frontend-web/src/roundtable-planning/pages/BusinessChainListPage.tsx @@ -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 ( +
+ {/* 顶栏:标题 + 描述 + 配置入口 */} +
+
+
+
+ + 业务链条配置 +
+

+ 可复用的有序智能体编排。点击卡片「设置」可进入编辑器修改该链条;「发布」后所有账号都能在「公共」里查看 / 选用。 +

+
+ +
+
+ + {/* 列表主体(与弹窗共用 BusinessChainListBody) */} +
+ +
+
+ ); +} diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index e4d75d7f..4e311ef2 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -41,12 +41,14 @@ import { ListOrdered, Loader2, Pencil, + Plus, Save, Settings, Sparkles, Trash2, Upload, Users, + X, } from "lucide-react"; import { useTheme } from "@/contexts/ThemeContext"; @@ -89,6 +91,7 @@ import { type SeatSelectionResult, } from "@/roundtable-planning/lib/business-chain"; import type { OrchestrationPlan } from "@/roundtable-planning/lib/dag-types"; +import type { IntentSummary } from "@/roundtable-planning/api/intent"; import { getJobByDraft, startJob } from "@/roundtable-planning/api/roundtable-jobs"; import { getChain } from "@/roundtable-planning/api/chains"; import { filterRecommendCandidates } from "@/roundtable-planning/api/recommend"; @@ -499,6 +502,26 @@ export default function RoundtablePlanningPage() { loadedFromDraftRefHolder.current = drafts.loadedFromDraftRef.current; }); + // ─── 用户手动编辑「当前意图摘要」并保存 ─────────────────────── + // 先 updateIntentReady 覆盖意图,再 bump 计数触发一次草稿保存。**必须用计数 + effect** + // 而非直接调 handleManualSave:enqueueSave 闭包捕获的 getStep1Snapshot 读的是本次渲染的 + // step1.intentReady,同步调用会在 setState flush 前存进旧意图(踩坑);放到 effect 里,等 + // 重渲染后再保存,getStep1Snapshot 才读到新值(与 step2/step3 的计数触发器同构)。 + const [intentEditTick, setIntentEditTick] = useState(0); + const handleSaveIntent = useCallback( + (next: IntentSummary) => { + step1.updateIntentReady(next); + setIntentEditTick((t) => t + 1); + }, + [step1], + ); + useEffect(() => { + if (intentEditTick === 0) return; + drafts.handleManualSave(); + // 仅在 tick 递增时保存;handleManualSave 取最新闭包(effect 跑在重渲染之后)。 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [intentEditTick]); + // ─── 从侧边栏「多智能体会商」记录进入 ─────────────────────── // 侧边栏点击某条草稿时:不在本页 → 把 id 存进 sessionStorage 再跳转,挂载时消费; // 已在本页 → 直接派 `roundtable:open-draft` 事件。两条路径都走 handleLoadDraft。 @@ -1106,6 +1129,7 @@ export default function RoundtablePlanningPage() { intentReady={step1.intentReady} timelineEntries={step1.timelineEntries} triggerToast={triggerToast} + onSaveIntent={handleSaveIntent} /> @@ -1763,6 +1787,148 @@ function ModelSelectorCard({ ); } +/** + * 「当前意图摘要」编辑态:目标(单行/多行文本)、约束、关键假设(均为可增删的列表)。 + * 受控本地草稿,保存时过滤空行、要求目标非空,再上抛 {@link IntentSummary}。 + */ +function IntentSummaryEditor({ + initial, + title, + onSave, + onCancel, +}: { + initial: IntentSummary; + title: string; + onSave: (next: IntentSummary) => void; + onCancel: () => void; +}) { + const [objective, setObjective] = useState(initial.objective ?? ""); + const [constraints, setConstraints] = useState( + initial.constraints?.length ? [...initial.constraints] : [""], + ); + const [assumptions, setAssumptions] = useState( + initial.assumptions?.length ? [...initial.assumptions] : [], + ); + + const objectiveInvalid = objective.trim().length === 0; + + const updateRow = ( + setter: React.Dispatch>, + idx: number, + value: string, + ) => setter((prev) => prev.map((v, i) => (i === idx ? value : v))); + const removeRow = (setter: React.Dispatch>, idx: number) => + setter((prev) => prev.filter((_, i) => i !== idx)); + const addRow = (setter: React.Dispatch>) => + setter((prev) => [...prev, ""]); + + const handleSave = () => { + if (objectiveInvalid) return; + onSave({ + objective: objective.trim(), + constraints: constraints.map((c) => c.trim()).filter(Boolean), + assumptions: assumptions.map((a) => a.trim()).filter(Boolean), + }); + }; + + const rowList = ( + label: string, + rows: string[], + setter: React.Dispatch>, + placeholder: string, + ) => ( +
+ {label} +
+ {rows.map((row, i) => ( +
+ updateRow(setter, i, e.target.value)} + placeholder={placeholder} + maxLength={200} + className="min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-[11.5px] text-slate-700 focus:border-primary focus:outline-none focus:ring-1 focus:ring-[var(--brand-200)]/50" + /> + +
+ ))} + +
+
+ ); + + return ( +
+
+
+ +

+ 编辑{title} +

+
+
+ + +
+
+ +
+
+ 核心目标 +