提交代码
This commit is contained in:
1 parent
dddd3c1670
commit
cba165933c
16 files changed
+1045
-61
No files matched your search
@@ -207,6 +207,12 @@ export const sidebarMenuItems: MenuItem[] = [
|
||||
icon: Settings,
|
||||
children: [
|
||||
{ id: "mgmt-agents", label: "智能体管理", icon: Bot, path: "/page/strategy/agents" },
|
||||
{
|
||||
id: "mgmt-business-chain-mapping",
|
||||
label: "业务链条管理",
|
||||
icon: Network,
|
||||
path: "/page/strategy/business-chain-mapping",
|
||||
},
|
||||
{ id: "mgmt-skills", label: "技能管理", icon: Sparkles, path: "/page/strategy/skills" },
|
||||
{ id: "mgmt-memory", label: "记忆管理", icon: Brain, path: "/page/strategy/memory" },
|
||||
{
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 业务链条管理页 —— 把固定业务码(3Q/6BF/7BF/8BF)绑定到「智能体」+「业务链」。
|
||||
*
|
||||
* 表格三列:业务(只读)/ 智能体 / 业务链。编辑模式下后两列为可搜索下拉:
|
||||
* - 智能体来自 `/api/agents`
|
||||
* - 业务链来自 `/api/roundtable-chains?scope=public`(只列公共链,保证全局共享时各账号都能解析)
|
||||
* 保存调 `PUT /api/business-mapping/{code}`。映射全局共享、无用户隔离。
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Loading, Select } from "tdesign-react";
|
||||
import { Link2, Pencil, Save, Users, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
import { listAgents } from "@/core/agents/api";
|
||||
import type { Agent } from "@/core/agents/types";
|
||||
import { listChains } from "@/roundtable-planning/api/chains";
|
||||
import type { BusinessChain } from "@/roundtable-planning/lib/business-chain";
|
||||
import {
|
||||
listBusinessMappings,
|
||||
updateBusinessMapping,
|
||||
type BusinessMapping,
|
||||
} from "@/strategy-components/api/business-mapping";
|
||||
|
||||
interface DraftSelection {
|
||||
agentId: string | null;
|
||||
chainId: string | null;
|
||||
}
|
||||
|
||||
export default function BusinessChainMappingPage() {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
|
||||
const [rows, setRows] = useState<BusinessMapping[]>([]);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [chains, setChains] = useState<BusinessChain[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingCode, setEditingCode] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState<DraftSelection>({ agentId: null, chainId: null });
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [mappingRes, agentRes, chainRes] = await Promise.allSettled([
|
||||
listBusinessMappings(),
|
||||
listAgents(),
|
||||
listChains("public"),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
if (mappingRes.status === "fulfilled") setRows(mappingRes.value);
|
||||
else toast.error(`加载业务映射失败:${mappingRes.reason}`);
|
||||
if (agentRes.status === "fulfilled") setAgents(agentRes.value);
|
||||
if (chainRes.status === "fulfilled") setChains(chainRes.value);
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const agentOptions = useMemo(
|
||||
() => agents.map((a) => ({ label: a.name || a.id, value: a.id })),
|
||||
[agents],
|
||||
);
|
||||
const chainOptions = useMemo(
|
||||
() => chains.map((c) => ({ label: c.title || c.id, value: c.id })),
|
||||
[chains],
|
||||
);
|
||||
|
||||
const beginEdit = (row: BusinessMapping) => {
|
||||
setEditingCode(row.businessCode);
|
||||
setDraft({ agentId: row.agentId, chainId: row.chainId });
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingCode(null);
|
||||
setDraft({ agentId: null, chainId: null });
|
||||
};
|
||||
|
||||
const handleSave = async (row: BusinessMapping) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const agentName = draft.agentId
|
||||
? agents.find((a) => a.id === draft.agentId)?.name ?? row.agentName ?? null
|
||||
: null;
|
||||
const chainTitle = draft.chainId
|
||||
? chains.find((c) => c.id === draft.chainId)?.title ?? row.chainTitle ?? null
|
||||
: null;
|
||||
const updated = await updateBusinessMapping(row.businessCode, {
|
||||
agentId: draft.agentId,
|
||||
agentName,
|
||||
chainId: draft.chainId,
|
||||
chainTitle,
|
||||
});
|
||||
if (!updated) {
|
||||
toast.error(`业务「${row.label}」不存在,保存失败`);
|
||||
return;
|
||||
}
|
||||
setRows((prev) => prev.map((r) => (r.businessCode === row.businessCode ? updated : r)));
|
||||
toast.success(`已保存「${row.label}」的配置`);
|
||||
cancelEdit();
|
||||
} catch (err) {
|
||||
toast.error(`保存失败:${(err as Error).message}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const shellClass = isDark
|
||||
? "h-full overflow-y-auto bg-[linear-gradient(180deg,#081322_0%,#0d1d33_42%,#0a1628_100%)] text-slate-100"
|
||||
: "h-full overflow-y-auto bg-[linear-gradient(180deg,#fffdfd_0%,#fff7f7_42%,#fafafa_100%)] text-[#222222]";
|
||||
const cardClass = isDark
|
||||
? "border border-[rgba(67,89,124,0.46)] bg-[rgba(12,29,54,0.94)] shadow-[0_16px_36px_rgba(3,10,22,0.2)]"
|
||||
: "border border-[#E8E8E8] bg-white shadow-[0_14px_30px_rgba(245,34,45,0.06)]";
|
||||
const headRowClass = isDark ? "text-slate-400" : "text-[#8C8C8C]";
|
||||
const rowBorder = isDark ? "border-[rgba(67,89,124,0.32)]" : "border-[#F0F0F0]";
|
||||
const mutedText = isDark ? "text-slate-500" : "text-[#A6A6A6]";
|
||||
const codeBadge = isDark
|
||||
? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]"
|
||||
: "bg-[#FFF1F0] text-[#CF1322]";
|
||||
|
||||
return (
|
||||
<div className={shellClass}>
|
||||
<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"}`}>
|
||||
为每个业务绑定智能体与业务链。多智能体会商从对应业务自动取用此处配置的业务链。业务链仅可选择「公共」链条。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Loading loading={loading} showOverlay={false}>
|
||||
<div className={`overflow-hidden rounded-3xl ${cardClass}`}>
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr className={`text-left text-xs uppercase tracking-widest ${headRowClass}`}>
|
||||
<th className="px-6 py-4 font-semibold">业务</th>
|
||||
<th className="px-6 py-4 font-semibold">智能体</th>
|
||||
<th className="px-6 py-4 font-semibold">业务链</th>
|
||||
<th className="px-6 py-4 text-right font-semibold">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.length === 0 && !loading ? (
|
||||
<tr>
|
||||
<td colSpan={4} className={`px-6 py-12 text-center ${mutedText}`}>
|
||||
暂无业务数据
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
rows.map((row) => {
|
||||
const editing = editingCode === row.businessCode;
|
||||
return (
|
||||
<tr key={row.businessCode} className={`border-t ${rowBorder}`}>
|
||||
{/* 业务(只读) */}
|
||||
<td className="px-6 py-4 align-middle">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-[15px] font-semibold ${isDark ? "text-white" : "text-[#222222]"}`}>
|
||||
{row.label}
|
||||
</span>
|
||||
<span className={`rounded-full px-2 py-0.5 text-[11px] font-mono ${codeBadge}`}>
|
||||
{row.businessCode}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* 智能体 */}
|
||||
<td className="px-6 py-4 align-middle">
|
||||
{editing ? (
|
||||
<Select
|
||||
className="min-w-[200px]"
|
||||
value={draft.agentId ?? undefined}
|
||||
options={agentOptions}
|
||||
filterable
|
||||
clearable
|
||||
placeholder="搜索并选择智能体"
|
||||
onChange={(v) => setDraft((d) => ({ ...d, agentId: (v as string) ?? null }))}
|
||||
/>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Users className={`h-3.5 w-3.5 ${mutedText}`} />
|
||||
{row.agentName || row.agentId ? (
|
||||
<span>{row.agentName || row.agentId}</span>
|
||||
) : (
|
||||
<span className={mutedText}>未配置</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* 业务链 */}
|
||||
<td className="px-6 py-4 align-middle">
|
||||
{editing ? (
|
||||
<Select
|
||||
className="min-w-[220px]"
|
||||
value={draft.chainId ?? undefined}
|
||||
options={chainOptions}
|
||||
filterable
|
||||
clearable
|
||||
placeholder="搜索并选择业务链(公共)"
|
||||
onChange={(v) => setDraft((d) => ({ ...d, chainId: (v as string) ?? null }))}
|
||||
/>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Link2 className={`h-3.5 w-3.5 ${mutedText}`} />
|
||||
{row.chainTitle || row.chainId ? (
|
||||
<span>{row.chainTitle || row.chainId}</span>
|
||||
) : (
|
||||
<span className={mutedText}>未配置</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* 操作 */}
|
||||
<td className="px-6 py-4 text-right align-middle">
|
||||
{editing ? (
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => handleSave(row)}
|
||||
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium disabled:opacity-50 ${
|
||||
isDark ? "bg-[rgba(34,197,94,0.18)] text-[#86EFAC]" : "bg-[#EEFCF7] text-[#0F9D6C]"
|
||||
}`}
|
||||
>
|
||||
<Save className="h-3.5 w-3.5" />
|
||||
保存
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={cancelEdit}
|
||||
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium disabled:opacity-50 ${
|
||||
isDark ? "bg-[rgba(110,136,176,0.18)] text-slate-300" : "bg-[#F5F5F5] text-[#575757]"
|
||||
}`}
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => beginEdit(row)}
|
||||
className={`inline-flex items-center gap-1 rounded-xl px-3 py-1.5 text-xs font-medium ${
|
||||
isDark ? "bg-[rgba(77,141,255,0.16)] text-[#9CCBFF]" : "bg-[#FFF1F0] text-[#CF1322]"
|
||||
}`}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
编辑
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Loading>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import RoundtablePlanningPage from "@/roundtable-planning/pages/RoundtablePlanni
|
||||
import "@/roundtable-planning/styles/roundtable-planning.css";
|
||||
import AgentChatPage from "./AgentChatPage";
|
||||
import AgentsPage from "./AgentsPage";
|
||||
import BusinessChainMappingPage from "./BusinessChainMappingPage";
|
||||
import ChatPage from "./ChatPage";
|
||||
import ChatsPage from "./ChatsPage";
|
||||
import AdminUserLeaderboardPage from "./AdminUserLeaderboardPage";
|
||||
@@ -172,6 +173,7 @@ export default function StrategyRoutes() {
|
||||
<Route path="skills" element={<SkillsPage />} />
|
||||
<Route path="skills/evolution" element={<SkillEvolutionPage />} />
|
||||
<Route path="memory" element={<MemoryPage />} />
|
||||
<Route path="business-chain-mapping" element={<BusinessChainMappingPage />} />
|
||||
|
||||
<Route
|
||||
path="scheduled"
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
* (`/api/roundtable-chains`,按登录用户隔离)。在 Step 2 圆桌研讨里,总控会**按
|
||||
* 链条顺序逐个派活**(见 `docs/multi-agent-business-chain-dev.md` §6)。
|
||||
*
|
||||
* 这里只放纯类型 / 常量,不含 React 状态或请求逻辑(CRUD 在 `api/chains.ts`)。
|
||||
* 这里放纯类型 / 常量,以及不依赖 React 的纯函数(如 `buildChainSeatSelection`);
|
||||
* 不含状态或请求逻辑(CRUD 在 `api/chains.ts`)。
|
||||
*/
|
||||
|
||||
import { buildPlanFromStages } from "./dag-types";
|
||||
|
||||
/** 链条里的一个有序席位。`seats[0]` 是第一个发言者。 */
|
||||
export interface ChainSeat {
|
||||
agent_id: string;
|
||||
@@ -86,6 +89,55 @@ export interface SeatSelectionResult {
|
||||
export const CHAIN_MIN_SEATS = 2;
|
||||
export const CHAIN_MAX_SEATS = 8;
|
||||
|
||||
/**
|
||||
* 由一条业务链条 + 当前候选智能体,构造 `SeatSelectionResult`。
|
||||
*
|
||||
* 与 `BusinessChainPicker.handleUse` 同源逻辑,抽成纯函数供「自动选链」复用
|
||||
* (会商自动化串联:从任务页带 chainId 进圆桌时跳过选链弹窗,直接据此进 Step 2):
|
||||
* - 仅保留候选池里仍存在的存活席位;存活不足 `CHAIN_MIN_SEATS` 返回 null(调用方回退弹窗);
|
||||
* - 链条带分层、过滤存活后仍含并行 stage / 任一阶段目标 / 总控提示 → 走 `dag`(默认 fast);
|
||||
* - 否则走线性 `chain`,席位顺序 = 链条顺序。
|
||||
*
|
||||
* `candidates` 为 `{ agent_id, name }` 列表(一般来自 `filterRecommendCandidates(listAgents())`)。
|
||||
*/
|
||||
export function buildChainSeatSelection(
|
||||
chain: BusinessChain,
|
||||
candidates: ReadonlyArray<{ agent_id: string; name: string }>,
|
||||
): SeatSelectionResult | null {
|
||||
const existingIds = new Set(candidates.map((c) => c.agent_id));
|
||||
const nameById = new Map(candidates.map((c) => [c.agent_id, c.name]));
|
||||
const seatNameById = new Map(chain.seats.map((s) => [s.agent_id, s.name]));
|
||||
const nameOf = (id: string) => nameById.get(id) ?? seatNameById.get(id) ?? id;
|
||||
|
||||
const survivingSeats = chain.seats.filter((s) => existingIds.has(s.agent_id));
|
||||
if (survivingSeats.length < CHAIN_MIN_SEATS) return null;
|
||||
|
||||
const chainMeta = { id: chain.id, title: chain.title };
|
||||
|
||||
if (chain.stages && chain.stages.length > 0) {
|
||||
const surviveIds = survivingSeats.map((s) => s.agent_id);
|
||||
const plan = buildPlanFromStages(
|
||||
chain.stages,
|
||||
surviveIds,
|
||||
true,
|
||||
chain.stageGoals,
|
||||
chain.coordinatorPrompt,
|
||||
);
|
||||
const hasParallel = plan.stages.some((st) => st.agentIds.length > 1);
|
||||
const hasGoal = plan.stages.some((st) => (st.goal?.trim() ?? "") !== "");
|
||||
const hasCoord = (plan.coordinatorPrompt?.trim() ?? "") !== "";
|
||||
if (hasParallel || hasGoal || hasCoord) {
|
||||
const agents = plan.stages
|
||||
.flatMap((st) => st.agentIds)
|
||||
.map((id) => ({ agent_id: id, name: nameOf(id) }));
|
||||
return { agents, mode: "dag", chain: chainMeta, plan, dagExecMode: "fast" };
|
||||
}
|
||||
}
|
||||
|
||||
const agents = survivingSeats.map((s) => ({ agent_id: s.agent_id, name: nameOf(s.agent_id) }));
|
||||
return { agents, mode: "chain", chain: chainMeta };
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 `getAvatarTypeFor()` 产出的头像配色(blue/purple/emerald/amber/pink/human)
|
||||
* 映射成 tag chip 的 tailwind 类(背景 + 文字 + 边框,含深色变体)。
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
* 6) 模型选择全局默认 `availableModels[0].name`,Persona 覆盖更高优先级
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import {
|
||||
Activity,
|
||||
Check,
|
||||
@@ -84,14 +84,44 @@ import {
|
||||
type RoleEntry,
|
||||
type SelectedAgent,
|
||||
} from "@/roundtable-planning/lib/roundtable-constants";
|
||||
import type { SeatSelectionResult } from "@/roundtable-planning/lib/business-chain";
|
||||
import {
|
||||
buildChainSeatSelection,
|
||||
type SeatSelectionResult,
|
||||
} from "@/roundtable-planning/lib/business-chain";
|
||||
import type { OrchestrationPlan } from "@/roundtable-planning/lib/dag-types";
|
||||
import { getJobByDraft, startJob } from "@/roundtable-planning/api/roundtable-jobs";
|
||||
import { getChain } from "@/roundtable-planning/api/chains";
|
||||
import { filterRecommendCandidates } from "@/roundtable-planning/api/recommend";
|
||||
import { listAgents } from "@/core/agents";
|
||||
import { listBusinessMappings } from "@/strategy-components/api/business-mapping";
|
||||
|
||||
/** 从任务页带进来的预填(经 navigation state 传递)。见会商自动化串联(模块 C)。 */
|
||||
interface RoundtablePrefill {
|
||||
/** 任务名称(注入 Step 1 首条用户消息)。 */
|
||||
taskTitle?: string;
|
||||
/** 任务描述(注入 Step 1 首条用户消息)。 */
|
||||
taskGoal?: string;
|
||||
/** 来源业务码(如 "6BF"):据此查业务映射拿到要自动选用的业务链。 */
|
||||
businessCode?: string;
|
||||
/** 是否进入即自动开跑 Step 1 意图分析。 */
|
||||
autoStart?: boolean;
|
||||
}
|
||||
|
||||
export default function RoundtablePlanningPage() {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// ─── 会商自动化串联(模块 C):任务页带进来的预填 ─────────────────
|
||||
// 只在首次渲染读一次 navigation state;随后清掉 history state,避免刷新/后退重放。
|
||||
const prefillRef = useRef<RoundtablePrefill | null>(
|
||||
((location.state as { roundtablePrefill?: RoundtablePrefill } | null)?.roundtablePrefill) ?? null,
|
||||
);
|
||||
// 由 prefill.businessCode 解析出的「自动选用业务链」结果;有值时「进入研讨」跳过选链弹窗直接进 Step 2。
|
||||
const [autoChainResult, setAutoChainResult] = useState<SeatSelectionResult | null>(null);
|
||||
const autoStartedRef = useRef(false);
|
||||
const prefillClearedRef = useRef(false);
|
||||
|
||||
const { models: availableModels, isLoading: modelsLoading } = useModels();
|
||||
|
||||
@@ -604,6 +634,69 @@ export default function RoundtablePlanningPage() {
|
||||
[buildCurrentStep2Snapshot, mergeActiveRun, step2, step3Report, triggerToast],
|
||||
);
|
||||
|
||||
// ─── 会商自动化串联(模块 C) ──────────────────────────────────
|
||||
// 消费 prefill 后清掉 history state,避免刷新 / 后退重放自动开跑与自动选链。
|
||||
useEffect(() => {
|
||||
if (prefillClearedRef.current || !prefillRef.current) return;
|
||||
prefillClearedRef.current = true;
|
||||
try {
|
||||
window.history.replaceState({ ...window.history.state, usr: null }, "");
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}, []);
|
||||
|
||||
// C2:带 autoStart 时,等 Step 1 意图线程就绪后自动把「任务名称/描述」作为首条用户消息发出,
|
||||
// 触发任务理解智能体自动分析(只发一次)。
|
||||
useEffect(() => {
|
||||
const prefill = prefillRef.current;
|
||||
if (!prefill?.autoStart || autoStartedRef.current) return;
|
||||
if (currentStep !== 1) return;
|
||||
if (!step1.intentThreadId || step1.intentReady) return;
|
||||
const title = prefill.taskTitle?.trim();
|
||||
const goal = prefill.taskGoal?.trim();
|
||||
const text = [title ? `任务名称:${title}` : "", goal ? `任务描述:${goal}` : ""]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
if (!text) return;
|
||||
autoStartedRef.current = true;
|
||||
void step1.sendIntentMessage(text);
|
||||
}, [currentStep, step1.intentThreadId, step1.intentReady, step1.sendIntentMessage]);
|
||||
|
||||
// C3:据 prefill.businessCode 查业务映射 → 取 chainId → 拉公共链 + 候选智能体 → 构造自动选链结果。
|
||||
// 解析成功后「进入研讨」会跳过选链弹窗直接据此进 Step 2;解析失败(未配置 / 链不存在 / 席位不足)
|
||||
// 静默回退到弹窗手选。
|
||||
// 不加「已启动」永久闸门 —— StrictMode 的 mount→unmount→remount 会让首次 run 在 cleanup
|
||||
// 里被 cancelled,闸门又让 remount 跳过重启,导致 autoChainResult 永远设不上(与
|
||||
// useStep1Intent 同款坑)。改为:每次 setup 都重跑,靠 cancelled 丢弃过期的首次结果,
|
||||
// 末次 setup 正常落地(生产无 StrictMode 只跑一次)。
|
||||
useEffect(() => {
|
||||
const prefill = prefillRef.current;
|
||||
if (!prefill?.businessCode) return;
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const mappings = await listBusinessMappings();
|
||||
const row = mappings.find((m) => m.businessCode === prefill.businessCode);
|
||||
const chainId = row?.chainId;
|
||||
if (!chainId) return;
|
||||
const [chain, agents] = await Promise.all([
|
||||
getChain(chainId),
|
||||
listAgents().catch(() => []),
|
||||
]);
|
||||
if (cancelled || !chain) return;
|
||||
const candidates = filterRecommendCandidates(agents);
|
||||
const result = buildChainSeatSelection(chain, candidates);
|
||||
if (!cancelled && result) setAutoChainResult(result);
|
||||
} catch {
|
||||
/* 解析失败 → 保持 null,回退弹窗手选 */
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 把页面重置回「第一步全新任务」——新建任务 + 后台挂起后都复用。
|
||||
// 注意:只清前端状态,不删后端已建的草稿 / 已挂起的后台作业(它们继续在后台跑,
|
||||
// 在历史记录里可见)。
|
||||
@@ -903,6 +996,13 @@ export default function RoundtablePlanningPage() {
|
||||
triggerToast("⚠️ 请先与任务理解智能体完成澄清,生成任务摘要后再进入");
|
||||
return;
|
||||
}
|
||||
// 会商自动化串联(模块 C):若任务页带来的业务链已解析成功,跳过选链弹窗,
|
||||
// 直接据该业务链进入 Step 2 研讨(用户无需再手选)。
|
||||
if (autoChainResult) {
|
||||
const result = autoChainResult;
|
||||
void drafts.ensureDraft(1).finally(() => handleConfirmAgents(result));
|
||||
return;
|
||||
}
|
||||
// 先把草稿落地拿到 draft_id,推荐历史才有归属;失败也照常打开弹窗
|
||||
// (弹窗在无 draftId 时退化为「直接推荐、不存历史」)。
|
||||
void drafts.ensureDraft(1).finally(() => setIsRecommendDialogOpen(true));
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 全局业务映射(业务码 → 智能体 / 业务链)API 客户端。
|
||||
*
|
||||
* 后端:`/api/business-mapping`(见 app/gateway/routers/business_mapping.py)。
|
||||
* 全局共享、无用户隔离;只读列表 + 更新 agent / chain 两列(业务行由后端 seed 固定)。
|
||||
* snake_case ⇄ camelCase 在此边界收敛,调用方保持干净。
|
||||
*/
|
||||
|
||||
import { apiFetch } from "@/core/api/fetch-client";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
|
||||
export interface BusinessMapping {
|
||||
businessCode: string;
|
||||
label: string;
|
||||
agentId: string | null;
|
||||
agentName: string | null;
|
||||
chainId: string | null;
|
||||
chainTitle: string | null;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
/** 部分更新 —— 只发改动的字段;显式传 null 清空该字段。 */
|
||||
export interface BusinessMappingPatch {
|
||||
agentId?: string | null;
|
||||
agentName?: string | null;
|
||||
chainId?: string | null;
|
||||
chainTitle?: string | null;
|
||||
}
|
||||
|
||||
const BASE = () => `${getBackendBaseURL()}/api/business-mapping`;
|
||||
|
||||
interface ServerMapping {
|
||||
business_code: string;
|
||||
label: string;
|
||||
agent_id: string | null;
|
||||
agent_name: string | null;
|
||||
chain_id: string | null;
|
||||
chain_title: string | null;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
function toMapping(s: ServerMapping): BusinessMapping {
|
||||
return {
|
||||
businessCode: s.business_code,
|
||||
label: s.label ?? "",
|
||||
agentId: s.agent_id ?? null,
|
||||
agentName: s.agent_name ?? null,
|
||||
chainId: s.chain_id ?? null,
|
||||
chainTitle: s.chain_title ?? null,
|
||||
sortOrder: s.sort_order ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
async function readJson<T>(res: Response, ctx: string): Promise<T> {
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
throw new Error(`${ctx} failed (${res.status}): ${text}`);
|
||||
}
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
|
||||
/** 列出全部业务映射(按展示顺序)。 */
|
||||
export async function listBusinessMappings(): Promise<BusinessMapping[]> {
|
||||
const res = await apiFetch(BASE(), { method: "GET" });
|
||||
const data = await readJson<{ mappings: ServerMapping[] }>(res, "list business mappings");
|
||||
return (data.mappings ?? []).map(toMapping);
|
||||
}
|
||||
|
||||
/** 更新某业务行的智能体 / 业务链选择。404 返回 null。 */
|
||||
export async function updateBusinessMapping(
|
||||
businessCode: string,
|
||||
patch: BusinessMappingPatch,
|
||||
): Promise<BusinessMapping | null> {
|
||||
const body: Record<string, unknown> = {};
|
||||
if (patch.agentId !== undefined) body.agent_id = patch.agentId;
|
||||
if (patch.agentName !== undefined) body.agent_name = patch.agentName;
|
||||
if (patch.chainId !== undefined) body.chain_id = patch.chainId;
|
||||
if (patch.chainTitle !== undefined) body.chain_title = patch.chainTitle;
|
||||
const res = await apiFetch(`${BASE()}/${encodeURIComponent(businessCode)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (res.status === 404) return null;
|
||||
const data = await readJson<ServerMapping>(res, "update business mapping");
|
||||
return toMapping(data);
|
||||
}
|
||||
+38
-29
@@ -4,10 +4,10 @@ import dayjs from 'dayjs';
|
||||
import { DateRangePicker, DateRangePickerProps, DateRangeValue, Loading } from 'tdesign-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { useChatContext } from '@/contexts/ChatContext';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { KanbanColumn } from './KanbanColumn';
|
||||
import { TaskListItem } from './TaskManagement';
|
||||
import { sentimentAnalyzeMock } from './kanbanMock';
|
||||
|
||||
interface TaskListPageProps {
|
||||
title: string;
|
||||
@@ -28,7 +28,6 @@ const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategory }) => {
|
||||
const { theme } = useTheme();
|
||||
const { setStartConfig } = useChatContext();
|
||||
const navigate = useNavigate();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
@@ -80,6 +79,19 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
|
||||
await taskDataChange();
|
||||
};
|
||||
|
||||
const applyColumnData = (taskType: ColumnType, list: TaskListItem[], total: number) => {
|
||||
if (taskType === 'unanalyzed') {
|
||||
setUnanalyzedTotal(total);
|
||||
setUnanalyzedList(list);
|
||||
} else if (taskType === 'running') {
|
||||
setRunningTotal(total);
|
||||
setRunningList(list);
|
||||
} else {
|
||||
setCompletedTotal(total);
|
||||
setCompletedList(list);
|
||||
}
|
||||
};
|
||||
|
||||
const taskListSearch = async (
|
||||
taskType: ColumnType,
|
||||
pageNum = 1,
|
||||
@@ -107,18 +119,19 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
|
||||
column: taskType,
|
||||
taskLevel: item.taskLevel || 1,
|
||||
}));
|
||||
const total = Number(result.total ?? normalized.length);
|
||||
|
||||
if (taskType === 'unanalyzed') {
|
||||
setUnanalyzedTotal(total);
|
||||
setUnanalyzedList(normalized);
|
||||
} else if (taskType === 'running') {
|
||||
setRunningTotal(total);
|
||||
setRunningList(normalized);
|
||||
} else {
|
||||
setCompletedTotal(total);
|
||||
setCompletedList(normalized);
|
||||
if (normalized.length === 0) {
|
||||
// 接口无数据时回落到本地预览数据,方便联调看效果
|
||||
const mock = sentimentAnalyzeMock[taskType];
|
||||
applyColumnData(taskType, mock, mock.length);
|
||||
return;
|
||||
}
|
||||
|
||||
applyColumnData(taskType, normalized, Number(result.total ?? normalized.length));
|
||||
} catch (err) {
|
||||
console.warn('三情任务查询失败,使用本地预览数据', err);
|
||||
const mock = sentimentAnalyzeMock[taskType];
|
||||
applyColumnData(taskType, mock, mock.length);
|
||||
} finally {
|
||||
if (taskType === 'unanalyzed') setUnanalyzedLoading(false);
|
||||
if (taskType === 'running') setRunningLoading(false);
|
||||
@@ -153,23 +166,19 @@ const SentimentAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCa
|
||||
const handleOpenConsultation = () => {
|
||||
const task = stateRef.current.task;
|
||||
if (!task) return;
|
||||
|
||||
try {
|
||||
const sceneItem = JSON.parse(task.scene || '{}');
|
||||
if (sceneItem.taskJson) {
|
||||
const taskJson = JSON.parse(sceneItem.taskJson);
|
||||
setStartConfig({
|
||||
...taskJson,
|
||||
llm_preset_id: 'BAILING',
|
||||
consistency_mode: 'soft',
|
||||
phase_config: { diverge_rounds: 3 },
|
||||
task_context: task.overview,
|
||||
});
|
||||
navigate('/qa-center/mope-chat');
|
||||
}
|
||||
} finally {
|
||||
setShowAnalysisModal(false);
|
||||
}
|
||||
// 会商自动化串联(模块 C):跳转圆桌规划页,带上任务名称/描述 + 本页业务码(3Q=三情分析)。
|
||||
// 圆桌据此自动开跑 Step 1,并在「进入研讨」时自动选用 3Q 配置的业务链(跳过选链弹窗)。
|
||||
navigate('/page/strategy/qa/multi-agent-roundtable', {
|
||||
state: {
|
||||
roundtablePrefill: {
|
||||
taskTitle: task.taskName,
|
||||
taskGoal: task.overview,
|
||||
businessCode: '3Q',
|
||||
autoStart: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
setShowAnalysisModal(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { DateRangePicker, DateRangePickerProps, DateRangeValue, Loading } from '
|
||||
import dayjs from 'dayjs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTheme } from '@/contexts/ThemeContext';
|
||||
import { useChatContext } from '@/contexts/ChatContext';
|
||||
import { requestCrudService } from '@/service/request-axios-service';
|
||||
import { KanbanColumn } from './KanbanColumn';
|
||||
import { TaskListItem } from './TaskManagement';
|
||||
import { taskAnalyzeMock } from './kanbanMock';
|
||||
|
||||
interface TaskListPageProps {
|
||||
title: string;
|
||||
@@ -28,7 +28,6 @@ const zncmUrl = import.meta.env.VITE_API_BASE_URL || '';
|
||||
|
||||
const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategory }) => {
|
||||
const { theme } = useTheme();
|
||||
const { setStartConfig } = useChatContext();
|
||||
const navigate = useNavigate();
|
||||
const isDark = theme === 'dark';
|
||||
|
||||
@@ -80,6 +79,19 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
|
||||
await taskDataChange();
|
||||
};
|
||||
|
||||
const applyColumnData = (taskType: ColumnType, list: TaskListItem[], total: number) => {
|
||||
if (taskType === 'unanalyzed') {
|
||||
setUnanalyzedTotal(total);
|
||||
setUnanalyzedList(list);
|
||||
} else if (taskType === 'running') {
|
||||
setRunningTotal(total);
|
||||
setRunningList(list);
|
||||
} else {
|
||||
setCompletedTotal(total);
|
||||
setCompletedList(list);
|
||||
}
|
||||
};
|
||||
|
||||
const taskListSearch = async (
|
||||
taskType: ColumnType,
|
||||
pageNum = 1,
|
||||
@@ -107,18 +119,19 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
|
||||
column: taskType,
|
||||
taskLevel: item.taskLevel || 1,
|
||||
}));
|
||||
const total = Number(result.total ?? normalized.length);
|
||||
|
||||
if (taskType === 'unanalyzed') {
|
||||
setUnanalyzedTotal(total);
|
||||
setUnanalyzedList(normalized);
|
||||
} else if (taskType === 'running') {
|
||||
setRunningTotal(total);
|
||||
setRunningList(normalized);
|
||||
} else {
|
||||
setCompletedTotal(total);
|
||||
setCompletedList(normalized);
|
||||
if (normalized.length === 0) {
|
||||
// 接口无数据时回落到本地预览数据,方便联调看效果
|
||||
const mock = taskAnalyzeMock[taskType];
|
||||
applyColumnData(taskType, mock, mock.length);
|
||||
return;
|
||||
}
|
||||
|
||||
applyColumnData(taskType, normalized, Number(result.total ?? normalized.length));
|
||||
} catch (err) {
|
||||
console.warn('任务查询失败,使用本地预览数据', err);
|
||||
const mock = taskAnalyzeMock[taskType];
|
||||
applyColumnData(taskType, mock, mock.length);
|
||||
} finally {
|
||||
if (taskType === 'unanalyzed') setUnanalyzedLoading(false);
|
||||
if (taskType === 'running') setRunningLoading(false);
|
||||
@@ -153,23 +166,19 @@ const TaskAnalyzes: React.FC<TaskListPageProps> = ({ title, category, subCategor
|
||||
const handleOpenConsultation = () => {
|
||||
const task = stateRef.current.task;
|
||||
if (!task) return;
|
||||
|
||||
try {
|
||||
const sceneItem = JSON.parse(task.scene || '{}');
|
||||
if (sceneItem.taskJson) {
|
||||
const taskJson = JSON.parse(sceneItem.taskJson);
|
||||
setStartConfig({
|
||||
...taskJson,
|
||||
llm_preset_id: 'BAILING',
|
||||
consistency_mode: 'soft',
|
||||
phase_config: { diverge_rounds: 3 },
|
||||
task_context: task.overview,
|
||||
});
|
||||
navigate('/qa-center/mope-chat');
|
||||
}
|
||||
} finally {
|
||||
setShowAnalysisModal(false);
|
||||
}
|
||||
// 会商自动化串联(模块 C):跳转圆桌规划页,带上任务名称/描述 + 本页业务码(6BF=任务FX)。
|
||||
// 圆桌据此自动开跑 Step 1,并在「进入研讨」时自动选用 6BF 配置的业务链(跳过选链弹窗)。
|
||||
navigate('/page/strategy/qa/multi-agent-roundtable', {
|
||||
state: {
|
||||
roundtablePrefill: {
|
||||
taskTitle: task.taskName,
|
||||
taskGoal: task.overview,
|
||||
businessCode: '6BF',
|
||||
autoStart: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
setShowAnalysisModal(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 看板(任务分析 / 三情分析)的本地预览数据。
|
||||
*
|
||||
* 仅在接口不可用 / 返回为空时回落使用(见各页 taskListSearch 的空数据 / catch 分支),
|
||||
* 后端正常返回数据时不会用到。纯前端联调 / 演示效果用。
|
||||
*/
|
||||
import { TaskListItem } from "./TaskManagement";
|
||||
|
||||
export type KanbanColumnType = "unanalyzed" | "running" | "completed";
|
||||
|
||||
interface Seed {
|
||||
taskName: string;
|
||||
overview: string;
|
||||
taskDirection: string;
|
||||
taskType?: number; // 1单任务 2场景 3会商 4多智能体 5检索
|
||||
taskLevel?: number; // 1一般 2紧急 3加急
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
function buildTask(seed: Seed, column: KanbanColumnType, id: number): TaskListItem {
|
||||
return {
|
||||
id,
|
||||
column,
|
||||
taskName: seed.taskName,
|
||||
overview: seed.overview,
|
||||
taskDirection: seed.taskDirection,
|
||||
taskType: seed.taskType ?? 1,
|
||||
taskLevel: seed.taskLevel ?? 1,
|
||||
createTime: seed.createTime ?? "2026-06-08 09:30:00",
|
||||
updateTime: seed.updateTime ?? "2026-06-09 10:15:00",
|
||||
scene: "",
|
||||
} as unknown as TaskListItem;
|
||||
}
|
||||
|
||||
function makeMock(
|
||||
baseId: number,
|
||||
seeds: Record<KanbanColumnType, Seed[]>,
|
||||
): Record<KanbanColumnType, TaskListItem[]> {
|
||||
let id = baseId;
|
||||
const build = (col: KanbanColumnType) => seeds[col].map((s) => buildTask(s, col, (id += 1)));
|
||||
return {
|
||||
unanalyzed: build("unanalyzed"),
|
||||
running: build("running"),
|
||||
completed: build("completed"),
|
||||
};
|
||||
}
|
||||
|
||||
/** 任务分析(plan-task-fx)预览数据。 */
|
||||
export const taskAnalyzeMock = makeMock(7100, {
|
||||
unanalyzed: [
|
||||
{
|
||||
taskName: "区域目标人群拆解专项",
|
||||
overview: "对重点区域目标人群进行分层拆解,明确触达优先级与切入口。",
|
||||
taskDirection: "策略策划处",
|
||||
taskType: 4,
|
||||
taskLevel: 2,
|
||||
createTime: "2026-06-09 08:20:00",
|
||||
updateTime: "2026-06-09 08:20:00",
|
||||
},
|
||||
{
|
||||
taskName: "竞品动向监测立项",
|
||||
overview: "梳理同类项目近期动向,输出可对照的策略阻力清单。",
|
||||
taskDirection: "情报研判处",
|
||||
taskType: 5,
|
||||
taskLevel: 1,
|
||||
createTime: "2026-06-08 16:45:00",
|
||||
updateTime: "2026-06-08 17:02:00",
|
||||
},
|
||||
],
|
||||
running: [
|
||||
{
|
||||
taskName: "阶段目标可行性会商",
|
||||
overview: "多智能体围绕阶段目标的可达性与风险点开展讨论并形成初步结论。",
|
||||
taskDirection: "指挥协调中心",
|
||||
taskType: 3,
|
||||
taskLevel: 3,
|
||||
createTime: "2026-06-07 10:10:00",
|
||||
updateTime: "2026-06-09 09:40:00",
|
||||
},
|
||||
],
|
||||
completed: [
|
||||
{
|
||||
taskName: "重点议题策略路径分析",
|
||||
overview: "完成重点议题的策略路径推演,已形成可执行的行动建议。",
|
||||
taskDirection: "策略策划处",
|
||||
taskType: 2,
|
||||
taskLevel: 1,
|
||||
createTime: "2026-06-04 14:00:00",
|
||||
updateTime: "2026-06-06 11:25:00",
|
||||
},
|
||||
{
|
||||
taskName: "受众态度归因复盘",
|
||||
overview: "对上一阶段受众态度变化进行归因复盘,沉淀经验供后续复用。",
|
||||
taskDirection: "综合保障组",
|
||||
taskType: 1,
|
||||
taskLevel: 2,
|
||||
createTime: "2026-06-03 09:15:00",
|
||||
updateTime: "2026-06-05 18:30:00",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/** 三情分析(plan-task-sentiment)预览数据。 */
|
||||
export const sentimentAnalyzeMock = makeMock(7200, {
|
||||
unanalyzed: [
|
||||
{
|
||||
taskName: "重点平台情绪监测专项",
|
||||
overview: "采集重点平台舆论样本,识别情绪倾向与潜在风险信号。",
|
||||
taskDirection: "情报研判处",
|
||||
taskType: 5,
|
||||
taskLevel: 2,
|
||||
createTime: "2026-06-09 07:50:00",
|
||||
updateTime: "2026-06-09 07:50:00",
|
||||
},
|
||||
{
|
||||
taskName: "舆情走势预研立项",
|
||||
overview: "围绕热点事件开展舆情走势预研,输出三情研判初判结论。",
|
||||
taskDirection: "策略策划处",
|
||||
taskType: 1,
|
||||
taskLevel: 1,
|
||||
createTime: "2026-06-08 13:20:00",
|
||||
updateTime: "2026-06-08 13:48:00",
|
||||
},
|
||||
],
|
||||
running: [
|
||||
{
|
||||
taskName: "群体情绪共识会商",
|
||||
overview: "多智能体从社情、舆情、敌情多视角讨论群体情绪态势并形成共识。",
|
||||
taskDirection: "指挥协调中心",
|
||||
taskType: 3,
|
||||
taskLevel: 3,
|
||||
createTime: "2026-06-07 11:30:00",
|
||||
updateTime: "2026-06-09 10:05:00",
|
||||
},
|
||||
],
|
||||
completed: [
|
||||
{
|
||||
taskName: "阶段性情绪研判归档",
|
||||
overview: "完成阶段性三情研判,结论已通过审核并归档同步相关单位。",
|
||||
taskDirection: "综合保障组",
|
||||
taskType: 2,
|
||||
taskLevel: 1,
|
||||
createTime: "2026-06-02 15:10:00",
|
||||
updateTime: "2026-06-05 16:40:00",
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in new issue
Block a user