From 5d6627af50a3cfecbc840a7c6aff7cdd11e2a511 Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 9 Sep 2026 20:49:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E4=BC=98=E9=80=89=E9=A1=B9=E7=9B=AE=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-web/src/core/system-settings/api.ts | 38 + .../src/core/system-settings/hooks.ts | 25 + .../src/strategy-components/Header.tsx | 11 +- .../strategy-selection/AgentConfigDialog.tsx | 734 ++++-- .../StrategyProjectsPage.tsx | 202 +- .../StrategySelectionPage.tsx | 2101 ++++++++++++++--- .../src/strategy-selection/lib.test.ts | 239 +- frontend-web/src/strategy-selection/lib.ts | 300 ++- .../strategy-selection/project-routes.test.ts | 29 + .../src/strategy-selection/project-routes.ts | 28 + .../strategy-selection/project-wal.test.ts | 147 ++ .../src/strategy-selection/project-wal.ts | 362 +++ .../src/strategy-selection/projects.ts | 179 +- .../strategy-agent-mapping.ts | 112 + .../strategy-agents.test.ts | 66 + .../src/strategy-selection/strategy-agents.ts | 128 + frontend-web/src/strategy-selection/types.ts | 47 + offline-backend-20260512/backend/CLAUDE.md | 5 +- offline-backend-20260512/backend/README.md | 5 +- .../backend/app/gateway/app.py | 11 + .../backend/app/gateway/deps.py | 15 + .../routers/_strategy_selection_seed.py | 105 +- .../strategy-selection-intent/SOUL.md | 15 +- .../backend/app/gateway/routers/agents.py | 216 +- .../gateway/routers/parallel_agent_tasks.py | 20 +- .../app/gateway/routers/parallel_agents.py | 281 ++- .../app/gateway/routers/strategy_agents.py | 802 +++++++ .../app/gateway/routers/system_settings.py | 38 + offline-backend-20260512/backend/config.yaml | 4 +- .../harness/deerflow/config/app_config.py | 31 + .../deerflow/config/system_settings.py | 11 + .../deerflow/persistence/models/__init__.py | 2 + .../persistence/parallel_agent_tasks/sql.py | 53 +- .../strategy_agent_roles/__init__.py | 24 + .../persistence/strategy_agent_roles/base.py | 39 + .../strategy_agent_roles/memory.py | 114 + .../persistence/strategy_agent_roles/model.py | 36 + .../persistence/strategy_agent_roles/sql.py | 141 ++ .../harness/deerflow/utils/stream_text.py | 39 +- .../tests/test_roundtable_model_fallback.py | 354 ++- .../tests/test_sandbox_mount_config.py | 46 + .../tests/test_strategy_agent_management.py | 667 ++++++ .../tests/test_strategy_project_cas.py | 81 + .../tests/test_strategy_project_settings.py | 72 + .../tests/test_strategy_selection_seed.py | 28 + 45 files changed, 7235 insertions(+), 768 deletions(-) create mode 100644 frontend-web/src/strategy-selection/project-routes.test.ts create mode 100644 frontend-web/src/strategy-selection/project-routes.ts create mode 100644 frontend-web/src/strategy-selection/project-wal.test.ts create mode 100644 frontend-web/src/strategy-selection/project-wal.ts create mode 100644 frontend-web/src/strategy-selection/strategy-agent-mapping.ts create mode 100644 frontend-web/src/strategy-selection/strategy-agents.test.ts create mode 100644 frontend-web/src/strategy-selection/strategy-agents.ts create mode 100644 offline-backend-20260512/backend/app/gateway/routers/strategy_agents.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/strategy_agent_roles/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/strategy_agent_roles/base.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/strategy_agent_roles/memory.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/strategy_agent_roles/model.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/strategy_agent_roles/sql.py create mode 100644 offline-backend-20260512/backend/tests/test_sandbox_mount_config.py create mode 100644 offline-backend-20260512/backend/tests/test_strategy_agent_management.py create mode 100644 offline-backend-20260512/backend/tests/test_strategy_project_cas.py create mode 100644 offline-backend-20260512/backend/tests/test_strategy_project_settings.py diff --git a/frontend-web/src/core/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts index e6f18342..d9d26e36 100644 --- a/frontend-web/src/core/system-settings/api.ts +++ b/frontend-web/src/core/system-settings/api.ts @@ -13,6 +13,44 @@ export type KnowledgeBaseProductVersion = "v1" | "v2"; export type OrdinaryKnowledgeLanding = "wiki" | "weknora"; +export interface StrategyProjectSettings { + taiwan_strait_demo_enabled: boolean; +} + +export interface StrategyProjectSettingsUpdate { + taiwan_strait_demo_enabled?: boolean; +} + +export async function loadStrategyProjectSettings(): Promise { + const response = await apiFetch( + `${getBackendBaseURL()}/api/system-settings/strategy-projects`, + ); + if (!response.ok) { + throw new Error(`无法读取策略项目设置(HTTP ${response.status})`); + } + return (await response.json()) as StrategyProjectSettings; +} + +export async function updateStrategyProjectSettings( + body: StrategyProjectSettingsUpdate, +): Promise { + const response = await apiFetch( + `${getBackendBaseURL()}/api/system-settings/strategy-projects`, + { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }, + ); + if (!response.ok) { + const detail = await response + .json() + .catch(() => ({ detail: "保存策略项目设置失败" })); + throw new Error(detail.detail ?? "保存策略项目设置失败"); + } + return (await response.json()) as StrategyProjectSettings; +} + export async function loadOrdinaryKnowledgeLanding(): Promise { const response = await apiFetch(`${getBackendBaseURL()}/api/system-settings/ordinary-knowledge-landing`); if (!response.ok) throw new Error("无法读取普通知识库默认页面设置"); diff --git a/frontend-web/src/core/system-settings/hooks.ts b/frontend-web/src/core/system-settings/hooks.ts index 8e51b828..ac138de4 100644 --- a/frontend-web/src/core/system-settings/hooks.ts +++ b/frontend-web/src/core/system-settings/hooks.ts @@ -13,6 +13,7 @@ import { loadPromptPrefixSettings, loadPublishSquares, loadSkillCompressionSettings, + loadStrategyProjectSettings, updateAgentModuleConfig, updateAppearanceSettings, updateCitationDisplaySettings, @@ -23,6 +24,7 @@ import { updatePromptPrefixSettings, updatePublishSquares, updateSkillCompressionSettings, + updateStrategyProjectSettings, type AppearanceUpdate, type CitationDisplayUpdate, type LeaderboardSettingsUpdate, @@ -32,6 +34,7 @@ import { type PromptPrefixUpdate, type PublishSquaresUpdate, type SkillCompressionUpdate, + type StrategyProjectSettingsUpdate, type LayoutMode, type ViewMode, } from "./api"; @@ -45,6 +48,28 @@ const LEADERBOARD_KEY = ["systemSettings", "leaderboard"] as const; const SKILL_COMPRESSION_KEY = ["systemSettings", "skillCompression"] as const; const MODEL_CONCURRENCY_KEY = ["systemSettings", "modelConcurrency"] as const; const PLUGIN_MANAGEMENT_KEY = ["systemSettings", "pluginManagement"] as const; +const STRATEGY_PROJECTS_KEY = ["systemSettings", "strategyProjects"] as const; + +export function useStrategyProjectSettings() { + return useQuery({ + queryKey: STRATEGY_PROJECTS_KEY, + queryFn: () => loadStrategyProjectSettings(), + staleTime: 60_000, + refetchOnWindowFocus: false, + retry: false, + }); +} + +export function useUpdateStrategyProjectSettings() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (body: StrategyProjectSettingsUpdate) => + updateStrategyProjectSettings(body), + onSuccess: (data) => { + queryClient.setQueryData(STRATEGY_PROJECTS_KEY, data); + }, + }); +} export function usePromptPrefixSettings() { return useQuery({ diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index baf95239..ef3d182f 100644 --- a/frontend-web/src/strategy-components/Header.tsx +++ b/frontend-web/src/strategy-components/Header.tsx @@ -41,10 +41,6 @@ const iconButtonClass = (isDark: boolean, isPaleLightTheme: boolean, isCustomThe : "text-white hover:bg-[var(--brand-950)] active:bg-[#8a0618]", ); -// 深色顶栏菜单左右分组(设计稿:左 问答中心/任务管理/应用中心,右 综合管理/资源管理) -const LEFT_MENU_IDS = ["qa-center", "task-management", "light-app-center"]; -const RIGHT_MENU_IDS = ["comprehensive-management", "resource-management"]; - type HeaderProps = { collapsed?: boolean; }; @@ -122,8 +118,11 @@ const Header = ({ collapsed }: HeaderProps) => { const thirdSubNavItems = getActiveThirdLevelItems(visibleMenuItems, pathname); - const leftMenuItems = visibleMenuItems.filter((i) => LEFT_MENU_IDS.includes(i.id)); - const rightMenuItems = visibleMenuItems.filter((i) => RIGHT_MENU_IDS.includes(i.id)); + // 深色顶栏围绕中间标题分成左右两组。直接按菜单管理后的一级顺序均分,确保被升为 + // 一级或重新排序的节点仍会显示;默认 5 项时仍保持左 3 / 右 2 的原布局。 + const topMenuSplitIndex = Math.ceil(visibleMenuItems.length / 2); + const leftMenuItems = visibleMenuItems.slice(0, topMenuSplitIndex); + const rightMenuItems = visibleMenuItems.slice(topMenuSplitIndex); const handleNavItemClick = (item: MenuItem) => { const path = item.path ?? findFirstRoutePath(item.children); diff --git a/frontend-web/src/strategy-selection/AgentConfigDialog.tsx b/frontend-web/src/strategy-selection/AgentConfigDialog.tsx index 57e6f204..3785a07d 100644 --- a/frontend-web/src/strategy-selection/AgentConfigDialog.tsx +++ b/frontend-web/src/strategy-selection/AgentConfigDialog.tsx @@ -1,5 +1,18 @@ import { useEffect, useMemo, useState } from "react"; -import { Bot, Check, LoaderCircle, Save, Sparkles } from "lucide-react"; +import { useQueryClient } from "@tanstack/react-query"; +import { + Bot, + Check, + CirclePlus, + Layers3, + LoaderCircle, + LockKeyhole, + Power, + RotateCcw, + Save, + Sparkles, + Trash2, +} from "lucide-react"; import { toast } from "sonner"; import { SkillTransferSelector } from "@/components/workspace/agents/skill-transfer-selector"; @@ -15,13 +28,21 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; -import { getAgent, updateAgent } from "@/core/agents/api"; -import type { Agent } from "@/core/agents/types"; import { useAllSkillsAdmin } from "@/core/skills/hooks"; import { cn } from "@/lib/utils"; -import { STRATEGY_AGENT_ROLES } from "./constants"; +import { + createStrategyAngle, + disableStrategyAngle, + STRATEGY_AGENTS_QUERY_KEY, + updateStrategyAgent, + useStrategyAgents, + type StrategyAgent, + type StrategyAgentCategory, +} from "./strategy-agents"; interface AgentConfigDialogProps { open: boolean; @@ -29,13 +50,43 @@ interface AgentConfigDialogProps { } interface AgentConfigDraft { + name: string; + description: string; soul: string; model: string | null; skills: string[]; + enabled: boolean; + sortOrder: number; } +const NEW_ANGLE_ID = "__new_strategy_angle__"; + +const CATEGORY_COPY: Record< + StrategyAgentCategory, + { title: string; description: string } +> = { + intent: { + title: "意图理解", + description: "把用户对话整理为结构化任务,固定流程入口", + }, + angle: { + title: "分析角度", + description: "并行生成独立策略,可按业务需要扩展", + }, + judge: { + title: "智能评判", + description: "统一比较各角度策略并给出推荐结论", + }, + solution: { + title: "方案整理", + description: "将用户选中的策略消冲突并汇总成方案", + }, +}; + function normalizeSkills(skills: string[]) { - return [...new Set(skills)].sort((left, right) => left.localeCompare(right)); + return [...new Set(skills)].sort((left, right) => + left.localeCompare(right), + ); } function sameSkills(left: string[], right: string[]) { @@ -47,27 +98,84 @@ function sameSkills(left: string[], right: string[]) { ); } -export function AgentConfigDialog({ open, onOpenChange }: AgentConfigDialogProps) { - const [selectedId, setSelectedId] = useState(STRATEGY_AGENT_ROLES[0].id); - const [agent, setAgent] = useState(null); - const [soul, setSoul] = useState(""); - const [model, setModel] = useState(null); - const [selectedSkills, setSelectedSkills] = useState([]); +function draftFromAgent(agent: StrategyAgent): AgentConfigDraft { + return { + name: agent.name, + description: agent.description, + soul: agent.soul ?? "", + model: agent.model, + skills: agent.skills ?? [], + enabled: agent.enabled, + sortOrder: agent.sort_order, + }; +} + +function emptyAngleDraft(sortOrder: number): AgentConfigDraft { + return { + name: "", + description: "", + soul: "", + model: null, + skills: [], + enabled: true, + sortOrder, + }; +} + +function sameDraft(left: AgentConfigDraft, right: AgentConfigDraft) { + return ( + left.name === right.name && + left.description === right.description && + left.soul === right.soul && + left.model === right.model && + left.enabled === right.enabled && + left.sortOrder === right.sortOrder && + sameSkills(left.skills, right.skills) + ); +} + +export function AgentConfigDialog({ + open, + onOpenChange, +}: AgentConfigDialogProps) { + const [selectedId, setSelectedId] = useState(""); + const [draft, setDraft] = useState(() => + emptyAngleDraft(100), + ); const [savedDraft, setSavedDraft] = useState(null); const [detailSkillName, setDetailSkillName] = useState(null); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); - const [error, setError] = useState(""); - - const selectedRole = useMemo( - () => STRATEGY_AGENT_ROLES.find((role) => role.id === selectedId)!, - [selectedId], - ); + const [busy, setBusy] = useState(false); + const queryClient = useQueryClient(); + const { + data: agents = [], + isLoading, + error, + refetch, + } = useStrategyAgents({ + enabled: open, + includeDisabled: true, + includeConfig: true, + }); const { skills, isLoading: isSkillsLoading, error: skillsError, } = useAllSkillsAdmin(open); + + const fixedAgents = useMemo( + () => agents.filter((agent) => agent.fixed), + [agents], + ); + const angleAgents = useMemo( + () => agents.filter((agent) => agent.category === "angle"), + [agents], + ); + const selectedAgent = useMemo( + () => agents.find((agent) => agent.id === selectedId) ?? null, + [agents, selectedId], + ); + const creating = selectedId === NEW_ANGLE_ID; + const dirty = Boolean(savedDraft && !sameDraft(draft, savedDraft)); const selectableSkills = useMemo( () => skills.filter((skill) => { @@ -81,223 +189,539 @@ export function AgentConfigDialog({ open, onOpenChange }: AgentConfigDialogProps }), [skills], ); - const dirty = Boolean( - agent && - savedDraft && - (soul !== savedDraft.soul || - model !== savedDraft.model || - !sameSkills(selectedSkills, savedDraft.skills)), - ); useEffect(() => { - if (!open || !selectedId) return; - let active = true; - setLoading(true); - setError(""); - setAgent(null); + if (!open || isLoading || creating || agents.length === 0) return; + if (selectedAgent) return; + setSelectedId(agents[0].id); + }, [agents, creating, isLoading, open, selectedAgent]); + + useEffect(() => { + if (!open || creating || !selectedAgent) return; + const next = draftFromAgent(selectedAgent); + setDraft(next); + setSavedDraft(next); + }, [creating, open, selectedAgent?.id, selectedAgent?.updated_at]); + + const confirmDiscardChanges = () => + !dirty || window.confirm("当前配置尚未保存,确定放弃这些修改吗?"); + + const selectAgent = (agentId: string) => { + if (agentId === selectedId || busy || !confirmDiscardChanges()) return; + setSelectedId(agentId); setSavedDraft(null); - void getAgent(selectedId) - .then((data) => { - if (!active) return; - setAgent(data); - const nextDraft = { - soul: data.soul ?? "", - model: data.model ?? null, - skills: data.skills ?? [], - }; - setSoul(nextDraft.soul); - setModel(nextDraft.model); - setSelectedSkills(nextDraft.skills); - setSavedDraft(nextDraft); - }) - .catch((reason: unknown) => { - if (!active) return; - setError( - reason instanceof Error - ? reason.message - : "智能体配置加载失败,请确认后端已完成默认配置初始化。", - ); - }) - .finally(() => { - if (active) setLoading(false); - }); - return () => { - active = false; - }; - }, [open, selectedId]); + }; + + const startCreate = () => { + if (busy || !confirmDiscardChanges()) return; + const maxOrder = angleAgents.reduce( + (current, agent) => Math.max(current, agent.sort_order), + 99, + ); + const next = emptyAngleDraft(maxOrder + 1); + setSelectedId(NEW_ANGLE_ID); + setDraft(next); + setSavedDraft(next); + }; const save = async () => { - if (!agent) return; - setSaving(true); + const name = draft.name.trim(); + if (!name) { + toast.error("请填写智能体名称"); + return; + } + setBusy(true); try { - const updated = await updateAgent(agent.id, { - soul: soul.trim(), - model, - skills: selectedSkills, - }); - setAgent(updated); - setSoul(updated.soul ?? ""); - setModel(updated.model ?? null); - setSelectedSkills(updated.skills ?? []); - setSavedDraft({ - soul: updated.soul ?? "", - model: updated.model ?? null, - skills: updated.skills ?? [], - }); - toast.success(`${updated.name} 配置已保存`); + if (creating) { + const created = await createStrategyAngle({ + name, + description: draft.description.trim(), + soul: draft.soul, + model: draft.model, + skills: draft.skills, + sort_order: draft.sortOrder, + }); + setSelectedId(created.id); + const next = draftFromAgent(created); + setDraft(next); + setSavedDraft(next); + await queryClient.invalidateQueries({ + queryKey: STRATEGY_AGENTS_QUERY_KEY, + }); + toast.success(`${created.name} 已加入分析角度库`); + } else if (selectedAgent) { + const updated = await updateStrategyAgent(selectedAgent.id, { + name, + description: draft.description.trim(), + soul: draft.soul, + model: draft.model, + skills: draft.skills, + enabled: selectedAgent.fixed ? undefined : draft.enabled, + sort_order: selectedAgent.fixed ? undefined : draft.sortOrder, + }); + const next = draftFromAgent(updated); + setDraft(next); + setSavedDraft(next); + await queryClient.invalidateQueries({ + queryKey: STRATEGY_AGENTS_QUERY_KEY, + }); + toast.success(`${updated.name} 配置已保存`); + } } catch (reason) { toast.error(reason instanceof Error ? reason.message : "保存失败"); } finally { - setSaving(false); + setBusy(false); } }; - const confirmDiscardChanges = () => - !dirty || window.confirm("当前智能体配置尚未保存,确定放弃这些修改吗?"); - - const selectRole = (roleId: string) => { - if (roleId === selectedId || saving || !confirmDiscardChanges()) return; - setSelectedId(roleId); + const softDelete = async () => { + if (!selectedAgent || selectedAgent.fixed || busy) return; + const confirmed = window.confirm( + `确定停用“${selectedAgent.name}”吗?\n\n它不会再出现在新项目的角度选择中,但历史项目、SOUL 和技能配置都会保留,可随时恢复。`, + ); + if (!confirmed) return; + setBusy(true); + try { + const updated = await disableStrategyAngle(selectedAgent.id); + const next = draftFromAgent(updated); + setDraft(next); + setSavedDraft(next); + await queryClient.invalidateQueries({ + queryKey: STRATEGY_AGENTS_QUERY_KEY, + }); + toast.success(`${updated.name} 已停用,历史项目不受影响`); + } catch (reason) { + toast.error(reason instanceof Error ? reason.message : "停用失败"); + } finally { + setBusy(false); + } }; const handleOpenChange = (nextOpen: boolean) => { - if (!nextOpen && (saving || !confirmDiscardChanges())) return; + if (!nextOpen && (busy || !confirmDiscardChanges())) return; onOpenChange(nextOpen); }; + const category = creating ? "angle" : selectedAgent?.category; + const categoryCopy = category ? CATEGORY_COPY[category] : null; + return ( - + - 策略智能体配置 + 统一策略智能体 - 这是全局统一配置,不属于某一个项目。管理员保存后,所有策略项目下一次运行都会使用这里的模型、SOUL 和技能。 + 配置对所有项目全局生效。流程角色保持唯一;分析角度可扩展、排序和停用,历史项目仍保留原引用。 -
-
-
- 流程角色 · {STRATEGY_AGENT_ROLES.length} +
+
-
-
+
-

{selectedRole.name}

-

{selectedRole.role}

+

+ 分析角度库 +

+

+ 新项目只使用当前启用的角度 +

- - {selectedRole.id} - +
+
+ {angleAgents.map((agent) => { + const selected = agent.id === selectedId; + return ( + + ); + })} + {!isLoading && angleAgents.length === 0 ? ( +
+ 暂无分析角度,点击“新增”建立第一个角度。 +
+ ) : null} +
+ - {loading ? ( -
+
+ {isLoading ? ( +
- 正在读取智能体配置… + 正在读取全局配置…
) : error ? ( -
-

暂时无法加载该智能体

-

{error}

+
+

策略智能体配置加载失败

+

+ {error instanceof Error ? error.message : "请稍后重试"} +

+
- ) : agent ? ( + ) : categoryCopy ? (
-
+
- 当前名称 -
{agent.name}
+
+

+ {creating ? "新增分析角度" : categoryCopy.title} +

+ {selectedAgent?.fixed ? ( + + 固定角色 + + ) : null} + {!creating && selectedAgent && !selectedAgent.fixed ? ( + + {draft.enabled ? "已启用" : "已停用"} + + ) : null} +
+

+ {creating + ? "创建后会成为所有策略项目可选的全局分析角度。" + : categoryCopy.description} +

- + {!creating && selectedAgent ? ( + + {selectedAgent.id} + + ) : null}
+
+
+ + +