接口修改

This commit is contained in:
fengxiangdebaozi committed 2026-05-23 10:49:16 +08:00
1 parent edb3238f3e
commit a85f1571a0
17 files changed
+906 -10648

No files matched your search

@@ -0,0 +1,162 @@
import { apiFetch } from "@/core/api/fetch-client";
import { getBackendBaseURL } from "@/core/config";
export type ThreadIdMap = Record<string, string>;
export interface InitMultiAgentRequest {
agentNames: string[];
mainAgentName: string;
model?: string;
}
export interface InitMultiAgentResponse {
status: "success";
main_agent_name: string;
thread_ids: ThreadIdMap;
}
export async function initMultiAgent(
payload: InitMultiAgentRequest,
): Promise<InitMultiAgentResponse> {
const res = await apiFetch(`${getBackendBaseURL()}/api/multi-agent/init`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agent_names: payload.agentNames,
main_agent_name: payload.mainAgentName,
model: payload.model,
}),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`init multi-agent failed (${res.status}): ${text}`);
}
return (await res.json()) as InitMultiAgentResponse;
}
export type StreamAgentType = "leader" | "special";
export type LeaderDispatch = [string, string];
export interface StreamMultiAgentRequest {
agentType: StreamAgentType;
dAgentThreadId: ThreadIdMap;
agentName: string;
newMessage: string;
model?: string;
skillStopNames?: string[];
signal?: AbortSignal;
/** Fires for every non-status data frame the upstream emits. */
onUpstreamFrame?: (frame: unknown) => void;
}
export interface StreamMultiAgentResult {
/** For leader runs: dispatched [agentName, task] tuples. Empty = consensus. */
dispatched: LeaderDispatch[];
/** For special runs: the completion status string. */
message: string | null;
/** Final assistant text emitted by the underlying agent. */
content: string;
/** Echoed agent_name of the agent that produced this run. */
agentName: string | null;
}
/**
* Identify the final status frame our backend appends after the LangGraph
* stream. Required keys are `status` (array for leader, string for special)
* and `content` (final assistant text); `agent_name` is also expected.
*/
function isFinalStatusFrame(value: unknown): value is {
status: LeaderDispatch[] | string;
content?: string;
agent_name?: string;
} {
if (!value || typeof value !== "object") return false;
if (!("status" in value) || !("content" in value)) return false;
const status = (value as { status: unknown }).status;
if (typeof status === "string") return true;
if (Array.isArray(status)) {
return status.every(
(item) =>
Array.isArray(item) && item.length === 2 && typeof item[0] === "string" && typeof item[1] === "string",
);
}
return false;
}
/**
* Open an SSE connection to /api/multi-agent/run/stream and resolve once the
* stream completes. The final frame (`{status: ...}`) is interpreted; every
* other `data:` frame is forwarded to `onUpstreamFrame` so the caller can do
* its own rendering (we don't try to mimic LangGraph SDK semantics here).
*/
export async function streamMultiAgent(
req: StreamMultiAgentRequest,
): Promise<StreamMultiAgentResult> {
const res = await apiFetch(`${getBackendBaseURL()}/api/multi-agent/run/stream`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
agent_type: req.agentType,
d_agent_thread_id: req.dAgentThreadId,
agent_name: req.agentName,
new_message: req.newMessage,
model: req.model,
skill_stop_names: req.skillStopNames ?? [],
}),
signal: req.signal,
});
if (!res.ok || !res.body) {
const text = await res.text().catch(() => "");
throw new Error(`run/stream failed (${res.status}): ${text}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
const result: StreamMultiAgentResult = { dispatched: [], message: null, content: "", agentName: null };
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const rawLine of lines) {
const line = rawLine.trimEnd();
if (!line.startsWith("data:")) continue;
const payload = line.slice(5).trim();
if (!payload) continue;
let parsed: unknown;
try {
parsed = JSON.parse(payload);
} catch {
continue;
}
if (isFinalStatusFrame(parsed)) {
if (Array.isArray(parsed.status)) {
result.dispatched = parsed.status;
} else {
result.message = parsed.status;
}
if (typeof parsed.content === "string") {
result.content = parsed.content;
}
if (typeof parsed.agent_name === "string") {
result.agentName = parsed.agent_name;
}
continue;
}
req.onUpstreamFrame?.(parsed);
}
}
return result;
}
@@ -1,7 +1,14 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, useCallback } from "react";
import { HighFidelityReport } from "@/roundtable-planning/components/HighFidelityReport";
import { useTheme } from "@/contexts/ThemeContext";
import { cn } from "@/lib/utils";
import { useModels } from "@/core/models/hooks";
import {
initMultiAgent,
streamMultiAgent,
type LeaderDispatch,
type ThreadIdMap,
} from "@/roundtable-planning/api/multi-agent";
import {
Shield,
MessageSquare,
@@ -115,56 +122,69 @@ function getAvatar(type: string, size: string = "h-8 w-8") {
}
}
// --- Extended Multi-Agent dialogue simulation rounds pool ---
const simulatedRoundPool = [
{
sender: "情报收集",
avatarType: "purple",
senderRole: "intelligence",
confidence: "高可信",
content: "📢 [情报追加] 检索到华东通信中枢类似物理断线案例,当时曾因极限负载冷切短接导致恢复延期 15%。已向执行规划端推送自恢复重组逻辑。"
},
{
sender: "环境评估",
avatarType: "emerald",
senderRole: "environment",
confidence: "动态沙盒 98%",
content: "🌡️ [热失稳警报] 预测在 T+36h 至 T+40h(高负载测试节点),物理降温柜将执行自清洁维护。我已通知方案设计端调低该时段的瞬时极限功率。"
},
{
sender: "方案设计",
avatarType: "human",
senderRole: "design",
confidence: "模型调整完成",
content: "📐 [方案调优] 收到环境和情报侧指标!已成功将主极速方案的测试镜像烘焙时间错开该高温窗口,挪前至 T+24h。完美规避第 48 小时的高温警戒区!"
},
{
sender: "风险审查",
avatarType: "amber",
senderRole: "risk",
confidence: "合规检测绿码",
content: "🛡️ [安全审查通过] 沙盒深度测析:方案 A 全栈静态脚本和隔离拓扑完全符合特级安全审计底线。安全系数上升 8.3%,物理漏损判定绿码!"
},
{
sender: "执行规划",
avatarType: "pink",
senderRole: "execution",
confidence: "自习惯里程碑",
content: "📅 [甘特图重排就绪] 72 小时全交付链路已锁定:T+12h 架构冻结,T+24h 镜像热部署规避,T+36h 集成通过,T+72h 整体可落地投产。全程算力承载平滑。"
},
{
sender: "总结陈述",
avatarType: "blue",
senderRole: "coordinator",
confidence: "全案收敛盖印",
content: "🏆 [全案决策固化] 研讨论证完美收敛!主极速构建案(310 万)携手备隔离冷库案(152.5 万),共耗资 462.5 万元,各决策节点已正式盖印交割!"
}
// --- Multi-agent backend wiring ---
const DEFAULT_MODEL = "deepseek-chat";
type RoleEntry = {
id: number;
name: string;
agentId?: string;
type: "human" | "agent";
status: string;
};
const ROUNDTABLE_ROLE_TEMPLATES: ReadonlyArray<Omit<RoleEntry, "status">> = [
{ id: 1, name: "指挥员 / 主持人", type: "human" },
{ id: 2, name: "总结陈述", agentId: "roundtable-summary", type: "agent" },
{ id: 3, name: "情报收集", agentId: "roundtable-intelligence", type: "agent" },
{ id: 4, name: "环境评估", agentId: "roundtable-environment", type: "agent" },
{ id: 5, name: "方案设计", agentId: "roundtable-solution-design", type: "agent" },
{ id: 6, name: "风险审查", agentId: "roundtable-risk-review", type: "agent" },
{ id: 7, name: "执行规划", agentId: "roundtable-execution-plan", type: "agent" },
{ id: 8, name: "总结陈述", agentId: "roundtable-summary", type: "agent" },
];
function makeRoles(defaultStatus: string, overrides: Record<number, string> = {}): RoleEntry[] {
return ROUNDTABLE_ROLE_TEMPLATES.map((t) => ({
...t,
status: overrides[t.id] ?? defaultStatus,
}));
}
// Subset of role templates that map to real backend agents (excludes the
// human commander seat) — used as the `agent_names` payload on init.
const ROUNDTABLE_AGENT_IDS = Array.from(
new Set(
ROUNDTABLE_ROLE_TEMPLATES.filter((r) => r.agentId).map((r) => r.agentId as string),
),
);
// agent_name (lowercase) → bubble meta. Used when streaming output, so we can
// light up the right speaker bubble and pick an avatar for the dialogue card.
const AGENT_NAME_TO_ROLE: Record<string, { speakerId: number; avatarType: string; displayName: string }> = {
"roundtable-summary": { speakerId: 2, avatarType: "blue", displayName: "总结陈述" },
"roundtable-intelligence": { speakerId: 3, avatarType: "purple", displayName: "情报收集" },
"roundtable-environment": { speakerId: 4, avatarType: "emerald", displayName: "环境评估" },
"roundtable-solution-design": { speakerId: 5, avatarType: "human", displayName: "方案设计" },
"roundtable-risk-review": { speakerId: 6, avatarType: "amber", displayName: "风险审查" },
"roundtable-execution-plan": { speakerId: 7, avatarType: "pink", displayName: "执行规划" },
};
function nowTime() {
return new Date().toLocaleTimeString("zh-CN", { hour12: false });
}
function newCoordinatorName() {
return `roundtable-coordinator-${Date.now().toString(36)}`;
}
export default function RoundtablePlanningPage() {
const { theme } = useTheme();
const isDark = theme === "dark";
// Backend-supplied model list (used by the per-seat persona drawer).
const { models: availableModels, isLoading: modelsLoading } = useModels();
// Navigation active tab
const [currentStep, setCurrentStep] = useState<number>(1);
const [inputText, setInputText] = useState<string>("");
@@ -180,17 +200,15 @@ export default function RoundtablePlanningPage() {
const [consensusPercentage, setConsensusPercentage] = useState<number>(89.4);
const [selectedAgentId, setSelectedAgentId] = useState<number | null>(null);
const [activeSpeakerId, setActiveSpeakerId] = useState<number | null>(null);
const [agentThinkingStatus, setAgentThinkingStatus] = useState<string | null>(null);
const [simRoundIndex, setSimRoundIndex] = useState<number>(0);
// Custom presets for agent model, weight, temperature and prompt editors
const [agentConfigs, setAgentConfigs] = useState<{ [key: number]: { model: string; weight: number; prompt: string; temp: number } }>({
2: { model: "Gemini 2.5 Flash", weight: 90, temp: 0.2, prompt: "你负责解析核心任务、资源极限和用户目标偏好,监督全局研商收敛。" },
3: { model: "Gemini 2.5 Flash", weight: 85, temp: 0.3, prompt: "你负责搜集电力、通信系统、历史极端部署条件情报,支持备灾架构体系。" },
4: { model: "Gemini 2.5 Pro", weight: 88, temp: 0.1, prompt: "你评估服务器机房热载荷、算力并发承载极限,预报运行瓶颈与风险时钟。" },
5: { model: "Claude 3.5 Sonnet", weight: 92, temp: 0.4, prompt: "你负责双全栈主备建模设计,高内聚低耦合,规避不可预知停机崩溃。" },
6: { model: "Gemini 2.5 Pro", weight: 95, temp: 0.1, prompt: "你作为安全主管,严厉把控预算开销与合规通过率防线,识别计划漏洞。" },
7: { model: "Gemini 2.5 Flash", weight: 80, temp: 0.4, prompt: "你负责对72小时里程碑执行路径规划、重排和资源调配演练序列。" },
2: { model: DEFAULT_MODEL, weight: 90, temp: 0.2, prompt: "你负责解析核心任务、资源极限和用户目标偏好,监督全局研商收敛。" },
3: { model: DEFAULT_MODEL, weight: 85, temp: 0.3, prompt: "你负责搜集电力、通信系统、历史极端部署条件情报,支持备灾架构体系。" },
4: { model: DEFAULT_MODEL, weight: 88, temp: 0.1, prompt: "你评估服务器机房热载荷、算力并发承载极限,预报运行瓶颈与风险时钟。" },
5: { model: DEFAULT_MODEL, weight: 92, temp: 0.4, prompt: "你负责双全栈主备建模设计,高内聚低耦合,规避不可预知停机崩溃。" },
6: { model: DEFAULT_MODEL, weight: 95, temp: 0.1, prompt: "你作为安全主管,严厉把控预算开销与合规通过率防线,识别计划漏洞。" },
7: { model: DEFAULT_MODEL, weight: 80, temp: 0.4, prompt: "你负责对72小时里程碑执行路径规划、重排和资源调配演练序列。" },
});
// Message lists state representation to mimic image items & enable actual conversations
@@ -254,74 +272,35 @@ export default function RoundtablePlanningPage() {
]);
// Roles in roundtable state (updates as we switch steps!)
const [roles, setRoles] = useState([
const [roles, setRoles] = useState<RoleEntry[]>([
{ id: 1, name: "指挥员 / 主持人", type: "human", status: "等待激活" },
{ id: 2, name: "总结陈述", type: "agent", status: "等待激活" },
{ id: 3, name: "情报收集", type: "agent", status: "等待激活" },
{ id: 4, name: "环境评估", type: "agent", status: "等待激活" },
{ id: 5, name: "方案设计", type: "human", status: "等待激活" },
{ id: 6, name: "风险审查", type: "agent", status: "等待激活" },
{ id: 7, name: "执行规划", type: "agent", status: "等待激活" },
{ id: 8, name: "总结陈述", type: "agent", status: "等待陈述" },
{ id: 2, name: "总结陈述", agentId: "roundtable-summary", type: "agent", status: "等待激活" },
{ id: 3, name: "情报收集", agentId: "roundtable-intelligence", type: "agent", status: "等待激活" },
{ id: 4, name: "环境评估", agentId: "roundtable-environment", type: "agent", status: "等待激活" },
{ id: 5, name: "方案设计", agentId: "roundtable-solution-design", type: "agent", status: "等待激活" },
{ id: 6, name: "风险审查", agentId: "roundtable-risk-review", type: "agent", status: "等待激活" },
{ id: 7, name: "执行规划", agentId: "roundtable-execution-plan", type: "agent", status: "等待激活" },
{ id: 8, name: "总结陈述", agentId: "roundtable-summary", type: "agent", status: "等待陈述" },
]);
// Step 2 Simulated Discussion Log
const [step2RoundtableDialogues, setStep2RoundtableDialogues] = useState<any[]>([
{
id: 1,
sender: "情报收集",
avatarType: "purple",
content: "📢 经全网高价值情报与历史同类备案检索,类似“72小时极速部署”在电力与通信中枢系统常面临软硬件适配断层风险,建议采用已验证的云原生容灾中继器架构模型。",
time: "11:02:18",
confidence: "高可信"
},
{
id: 2,
sender: "环境评估",
avatarType: "emerald",
content: "🌍 主服务器托管物理环境测试就绪。在极端并发与资源受限下,机柜最高算力分配可承受 1.8x 的弹性波动。72小时周期内由于降温机组轮休,需规避第48-52小时的高温警戒区。",
time: "11:03:05",
confidence: "精确度96%"
},
{
id: 3,
sender: "指挥员 / 主持人",
avatarType: "man",
content: "🎤 研讨团队注意,用户要求500万预算硬约束,且首要目标是提升执行效率和保障100%合规。方案设计端应当对核心敏捷路径进行显式标注。",
time: "11:04:12",
confidence: "主席裁决"
},
{
id: 4,
sender: "方案设计",
avatarType: "man",
content: "📐 已完成主备双核心方案建模。主极速构建方案基于K3s轻量容器,可在36小时内提前部署,并由情报端微模型预烘焙;备用方案配置纯物理备份,确保全栈零公网隔离。",
time: "11:05:40",
confidence: "主责输出"
},
{
id: 5,
sender: "风险审查",
avatarType: "amber",
content: "⚠️ 指出主极速方案漏洞:由于预算硬性限制500万,自动化测试外包采购与安全审计费用需压缩至60万之内。否则会触碰预算预警线,建议在第三轮调度中裁剪旁路遥测组件。",
time: "11:06:55",
confidence: "黄色警戒"
},
{
id: 6,
sender: "执行规划",
avatarType: "pink",
content: "📅 72小时里程碑已自适应重排:T+12h 架构冻结;T+36h 核心集成验证;T+48h 风险切片模拟与极速演练;T+60h 预算与合规静态查验;T+72h 整体安全投产。",
time: "11:08:10",
confidence: "自动化规划器"
}
]);
// Step 2 dialogue list — populated by the real orchestration loop once the
// user enters the roundtable. Starts empty so we never flash mock content.
const [step2RoundtableDialogues, setStep2RoundtableDialogues] = useState<any[]>([]);
// Inputs for Step 2 conversation simulation controls
const [roundTableSpeed, setRoundTableSpeed] = useState<string>("正常");
const [isRoundTableRunning, setIsRoundTableRunning] = useState<boolean>(true);
const [customAgentInput, setCustomAgentInput] = useState<string>("");
// Multi-agent orchestration state (lives only for one step-2 visit).
const [threadIds, setThreadIds] = useState<ThreadIdMap | null>(null);
const [coordinatorName, setCoordinatorName] = useState<string | null>(null);
const [isInitializing, setIsInitializing] = useState<boolean>(false);
const [initError, setInitError] = useState<string | null>(null);
const [isOrchestrating, setIsOrchestrating] = useState<boolean>(false);
const orchestrationCancelRef = useRef<boolean>(false);
const step2InitRef = useRef<boolean>(false);
// Toast auto-dismiss logic
useEffect(() => {
if (showToast) {
@@ -350,84 +329,221 @@ export default function RoundtablePlanningPage() {
}
}, [step2RoundtableDialogues]);
// Roundtable Dynamic Simulation Timer Hook
useEffect(() => {
if (currentStep !== 2 || !isRoundTableRunning) return;
// Push one dialogue card; uses Date.now()+rand to avoid React key collisions.
const appendDialogue = useCallback((card: {
sender: string;
avatarType: string;
content: string;
confidence?: string;
}) => {
setStep2RoundtableDialogues(prev => [
...prev,
{
id: Date.now() + Math.random(),
sender: card.sender,
avatarType: card.avatarType,
content: card.content,
time: nowTime(),
confidence: card.confidence ?? "实时输出",
},
]);
}, []);
let intervalTime = 5000;
if (roundTableSpeed === "慢速") intervalTime = 8000;
else if (roundTableSpeed === "极速") intervalTime = 2500;
const interval = setInterval(() => {
if (simRoundIndex < simulatedRoundPool.length) {
const nextMessageInfo = simulatedRoundPool[simRoundIndex];
// Determine correct Agent ID to highlight and activate
let speakerId = 3; // default: Purple / 情报收集
if (nextMessageInfo.senderRole === "environment") speakerId = 4;
else if (nextMessageInfo.senderRole === "design") speakerId = 5;
else if (nextMessageInfo.senderRole === "risk") speakerId = 6;
else if (nextMessageInfo.senderRole === "execution") speakerId = 7;
else if (nextMessageInfo.senderRole === "coordinator") speakerId = 2;
// Visual highlights speaker on roundtable diagram
setActiveSpeakerId(speakerId);
// Update active role and highlight typing indicator
setRoles(prevRoles => prevRoles.map(item => {
if (item.id === speakerId) {
return { ...item, status: "研讨中" };
} else if (item.status === "研讨中") {
return { ...item, status: "在线" };
}
return item;
}));
setAgentThinkingStatus(`🤖 【${nextMessageInfo.sender}】正在结合最新人设约束深度推算,生成论证要点...`);
const delayTimeout = setTimeout(() => {
const dialogueTime = new Date().toLocaleTimeString("zh-CN", { hour12: false });
const newDialogue = {
id: Date.now(),
sender: nextMessageInfo.sender,
avatarType: nextMessageInfo.avatarType,
content: nextMessageInfo.content,
time: dialogueTime,
confidence: nextMessageInfo.confidence
};
setStep2RoundtableDialogues(prev => [...prev, newDialogue]);
setSimRoundIndex(prevIndex => prevIndex + 1);
setAgentThinkingStatus(null);
// Smoothly increment consensus percentage up to 100%
setConsensusPercentage(prev => {
const stepInc = (100 - 89.4) / simulatedRoundPool.length;
const updated = parseFloat((prev + stepInc).toFixed(1));
return updated >= 100 ? 100 : updated;
});
}, roundTableSpeed === "极速" ? 800 : 1800);
return () => clearTimeout(delayTimeout);
} else {
// Dialogue fully completed & stabilized
setActiveSpeakerId(null);
setRoles(prevRoles => prevRoles.map(item => {
if (item.type === "agent" && item.status !== "已完成") {
return { ...item, status: "已完成" };
}
return item;
}));
if (consensusPercentage < 100) {
setConsensusPercentage(100);
triggerToast("🎉 圆桌研讨论证会圆满落幕!已取得 100% 专家组共识,提请渲染成果方案");
// Highlight a speaker bubble + flip the role's status label.
const highlightSpeaker = useCallback((speakerId: number | null) => {
setActiveSpeakerId(speakerId);
setRoles(prev =>
prev.map(item => {
if (speakerId !== null && item.id === speakerId) {
return { ...item, status: "研讨中" };
}
}
}, intervalTime);
if (item.status === "研讨中") {
return { ...item, status: "在线" };
}
return item;
}),
);
}, []);
return () => clearInterval(interval);
}, [currentStep, isRoundTableRunning, roundTableSpeed, simRoundIndex, consensusPercentage]);
// Pull the consolidated user intent out of step-1 history. Falls back to a
// generic prompt if the user never spoke.
const buildInitialIntent = useCallback(() => {
const lastUserMessage = [...interactiveMessages].reverse().find(m => m.sender === "user");
const userIntent = lastUserMessage?.content || "用户尚未给出明确意图";
const constraints = `预算硬约束:${budgetLimit} 万元`;
return `${userIntent}\n\n约束条件:${constraints}\n\n请基于上述目标与约束,在圆桌内调度合适的子智能体完成研讨。`;
}, [interactiveMessages, budgetLimit]);
// Run one full orchestration step: 1) leader call → 2) dispatch specials in
// parallel → 3) recurse with a synthesis prompt until leader stops dispatching.
const runOrchestration = useCallback(async (
seedMessage: string,
ids: ThreadIdMap,
coord: string,
) => {
if (isOrchestrating) return;
setIsOrchestrating(true);
orchestrationCancelRef.current = false;
let nextLeaderTask = seedMessage;
let safetyCounter = 0;
const MAX_CYCLES = 8;
try {
while (!orchestrationCancelRef.current && safetyCounter < MAX_CYCLES) {
safetyCounter += 1;
highlightSpeaker(2);
appendDialogue({
sender: "总控协调",
avatarType: "blue",
content: `🧭 [总控派活] 正在分析当前进展并决定下一步:\n${nextLeaderTask.slice(0, 140)}${nextLeaderTask.length > 140 ? "..." : ""}`,
confidence: "调度中",
});
const leaderRes = await streamMultiAgent({
agentType: "leader",
dAgentThreadId: ids,
agentName: coord,
newMessage: nextLeaderTask,
model: DEFAULT_MODEL,
});
if (orchestrationCancelRef.current) break;
if (leaderRes.content) {
appendDialogue({
sender: "总结陈述",
avatarType: "blue",
content: leaderRes.content,
confidence: leaderRes.dispatched.length === 0 ? "全案盖印" : "派活说明",
});
}
if (leaderRes.dispatched.length === 0) {
highlightSpeaker(null);
setConsensusPercentage(100);
triggerToast("🎉 圆桌研讨论证会圆满落幕!已取得 100% 专家组共识");
break;
}
// Dispatch all sub-agents in parallel; per-seat model preference
// overrides the default if the user picked one in the drawer.
await Promise.allSettled(
leaderRes.dispatched.map(async ([subName, task]) => {
if (orchestrationCancelRef.current) return;
const meta = AGENT_NAME_TO_ROLE[subName.toLowerCase()];
if (meta) highlightSpeaker(meta.speakerId);
const perSeatModel = meta ? agentConfigs[meta.speakerId]?.model : undefined;
const specRes = await streamMultiAgent({
agentType: "special",
dAgentThreadId: ids,
agentName: subName,
newMessage: task,
model: perSeatModel || DEFAULT_MODEL,
});
if (orchestrationCancelRef.current) return;
appendDialogue({
sender: meta?.displayName ?? subName,
avatarType: meta?.avatarType ?? "blue",
content: specRes.content || `(无文本输出)`,
confidence: "子智能体交付",
});
}),
);
if (orchestrationCancelRef.current) break;
// Nudge consensus up after each completed round.
setConsensusPercentage(prev => Math.min(95, parseFloat((prev + 8).toFixed(1))));
nextLeaderTask = "各子智能体已就上一轮派活完成交付,请你结合最新结论判断是否仍需继续派活;若信息已足够,请直接输出最终方案并停止派活。";
}
if (safetyCounter >= MAX_CYCLES) {
triggerToast("⏱️ 已达到最大研讨轮次,自动停止以避免循环");
}
} catch (err) {
console.error("orchestration error:", err);
triggerToast(`❌ 研讨异常: ${(err as Error).message}`);
} finally {
highlightSpeaker(null);
setIsOrchestrating(false);
}
}, [appendDialogue, highlightSpeaker, isOrchestrating, agentConfigs]);
// On first entry to step 2: init the coordinator + sub-agent threads, then
// fire the first leader cycle. Leaving step 2 resets so re-entering builds
// a fresh coordinator (per spec).
useEffect(() => {
if (currentStep !== 2) {
// tear down: cancel any in-flight orchestration; reset bookkeeping.
orchestrationCancelRef.current = true;
step2InitRef.current = false;
setIsOrchestrating(false);
return;
}
if (step2InitRef.current) return;
step2InitRef.current = true;
let cancelled = false;
(async () => {
setIsInitializing(true);
setInitError(null);
setStep2RoundtableDialogues([]);
setConsensusPercentage(0);
const coord = newCoordinatorName();
try {
const initRes = await initMultiAgent({
agentNames: ROUNDTABLE_AGENT_IDS,
mainAgentName: coord,
model: DEFAULT_MODEL,
});
if (cancelled) return;
setThreadIds(initRes.thread_ids);
setCoordinatorName(initRes.main_agent_name);
appendDialogue({
sender: "系统",
avatarType: "blue",
content: `✅ 圆桌已就绪:协调智能体 ${initRes.main_agent_name} 已上线,${Object.keys(initRes.thread_ids).length - 1} 个席位已分配独立线程。`,
confidence: "系统就绪",
});
triggerToast("✅ 多智能体圆桌初始化完成");
await runOrchestration(buildInitialIntent(), initRes.thread_ids, initRes.main_agent_name);
} catch (err) {
console.error("init multi-agent failed:", err);
if (cancelled) return;
setInitError((err as Error).message);
triggerToast(`❌ 初始化失败: ${(err as Error).message}`);
} finally {
if (!cancelled) setIsInitializing(false);
}
})();
return () => {
cancelled = true;
orchestrationCancelRef.current = true;
};
// We intentionally re-init only on `currentStep` transition.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentStep]);
// Pause/resume button — pausing cancels in-flight stream, resuming kicks off
// another leader cycle to continue the conversation.
useEffect(() => {
if (currentStep !== 2) return;
if (!isRoundTableRunning) {
orchestrationCancelRef.current = true;
return;
}
if (isOrchestrating || isInitializing || !threadIds || !coordinatorName) return;
// resumed and idle: nudge with a continuation prompt
runOrchestration("用户已恢复研讨,请继续上一轮的协调工作。", threadIds, coordinatorName);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRoundTableRunning, currentStep]);
// Adjust budget constraint dynamically
const handleBudgetChange = (newVal: number) => {
@@ -469,39 +585,32 @@ export default function RoundtablePlanningPage() {
return () => clearTimeout(tid);
};
// Save Agent config overrides and trigger recalculation
// Save per-seat agent config overrides. We persist locally only — the next
// time the orchestration loop dispatches to this seat it will read the
// chosen model out of `agentConfigs` and pass it on the streaming call.
const handleSaveAgentAndRecalculate = (id: number) => {
const roleName = roles.find(r => r.id === id)?.name || "专家智能体";
triggerToast(`⚙️ 已部署“${roleName}”个性化参数偏好!正在同步大模型提示词防线...`);
const role = roles.find(r => r.id === id);
const roleName = role?.name || "专家智能体";
const cfg = agentConfigs[id];
const targetModel = cfg?.model || DEFAULT_MODEL;
setTimeout(() => {
const respTime = new Date().toLocaleTimeString("zh-CN", { hour12: false });
const currentConfig = agentConfigs[id];
const modelShort = currentConfig?.model || "Gemini 2.5 Flash";
const promptSnippet = currentConfig?.prompt.length > 22
? currentConfig.prompt.substring(0, 22) + "..."
: currentConfig?.prompt || "";
const avatarColor =
id === 2 ? "blue"
: id === 3 ? "purple"
: id === 4 ? "emerald"
: id === 5 ? "human"
: id === 6 ? "amber"
: id === 7 ? "pink"
: "blue";
let avatarColor = "blue";
if (id === 2) avatarColor = "blue";
else if (id === 3) avatarColor = "purple";
else if (id === 4) avatarColor = "emerald";
else if (id === 5) avatarColor = "human";
else if (id === 6) avatarColor = "amber";
else if (id === 7) avatarColor = "pink";
const recomputedResponse = {
id: Date.now() + 20,
sender: `${roleName} (${modelShort})`,
avatarType: avatarColor,
confidence: `个性化权重 ${currentConfig?.weight || 90}%`,
content: `⚡ [提示词覆写激活] 独立节点已部署为温度 ${currentConfig?.temp} 指示。结合重新加载的 Persona 指令“${promptSnippet}”,我立即优化了博弈运算函数,当前可用预算重分配函数已广播给其他专家。`,
time: respTime
};
setStep2RoundtableDialogues(prev => [...prev, recomputedResponse]);
setSelectedAgentId(null); // Close drawer
}, 1200);
appendDialogue({
sender: `${roleName} (${targetModel})`,
avatarType: avatarColor,
content: `⚡ [模型偏好已保存] 本席位在下一轮研讨中将使用 ${targetModel} 进行推理。系统提示词与温度(${cfg?.temp ?? 0.2})当前仅在前端记录,等后端开放覆写后再同步生效。`,
confidence: "席位偏好",
});
setSelectedAgentId(null);
triggerToast(`⚙️ "${roleName}"已切换至 ${targetModel},下轮生效`);
};
// Text area counter update
@@ -590,73 +699,38 @@ export default function RoundtablePlanningPage() {
// Step 2 role statuses transition when entering roundtable!
useEffect(() => {
if (currentStep === 1) {
setRoles([
{ id: 1, name: "指挥员 / 主持人", type: "human", status: "等待激活" },
{ id: 2, name: "总结陈述", type: "agent", status: "在线" },
{ id: 3, name: "情报收集", type: "agent", status: "等待激活" },
{ id: 4, name: "环境评估", type: "agent", status: "等待激活" },
{ id: 5, name: "方案设计", type: "human", status: "等待激活" },
{ id: 6, name: "风险审查", type: "agent", status: "等待激活" },
{ id: 7, name: "执行规划", type: "agent", status: "等待激活" },
{ id: 8, name: "总结陈述", type: "agent", status: "等待陈述" },
]);
setRoles(makeRoles("等待激活", { 2: "在线" }));
} else if (currentStep === 2) {
setRoles([
{ id: 1, name: "指挥员 / 主持人", type: "human", status: "在线" },
{ id: 2, name: "总结陈述", type: "agent", status: "在线" },
{ id: 3, name: "情报收集", type: "agent", status: "在线" },
{ id: 4, name: "环境评估", type: "agent", status: "在线" },
{ id: 5, name: "方案设计", type: "human", status: "在线" },
{ id: 6, name: "风险审查", type: "agent", status: "在线" },
{ id: 7, name: "执行规划", type: "agent", status: "在线" },
{ id: 8, name: "总结陈述", type: "agent", status: "等待陈述" },
]);
setRoles(makeRoles("在线", { 8: "等待陈述" }));
} else {
setRoles([
{ id: 1, name: "指挥员 / 主持人", type: "human", status: "已完成" },
{ id: 2, name: "总结陈述", type: "agent", status: "已完成" },
{ id: 3, name: "情报收集", type: "agent", status: "已完成" },
{ id: 4, name: "环境评估", type: "agent", status: "已完成" },
{ id: 5, name: "方案设计", type: "human", status: "已完成" },
{ id: 6, name: "风险审查", type: "agent", status: "已完成" },
{ id: 7, name: "执行规划", type: "agent", status: "已完成" },
{ id: 8, name: "总结陈述", type: "agent", status: "已完成" },
]);
setRoles(makeRoles("已完成"));
}
}, [currentStep]);
// Inject user input to the roundtable room
// Inject user input to the roundtable room — sends it as a new leader call.
const handleInjectToRoundtable = () => {
if (!customAgentInput.trim()) return;
if (!threadIds || !coordinatorName) {
triggerToast("⚠️ 圆桌尚未就绪,请稍候");
return;
}
if (isOrchestrating) {
triggerToast("⏳ 当前轮次仍在进行中,请等待或暂停后再发送");
return;
}
const responseTime = new Date().toLocaleTimeString("zh-CN", { hour12: false });
const userInjectedMessage = {
id: Date.now(),
const userText = customAgentInput.trim();
setCustomAgentInput("");
appendDialogue({
sender: "用户 (插话干预)",
avatarType: "human",
content: `💡 (人工指令介入) ${customAgentInput}`,
time: responseTime,
confidence: "最高指令"
};
content: `💡 (人工指令介入) ${userText}`,
confidence: "最高指令",
});
triggerToast("您的人工纠偏指令已同步至圆桌节点!");
setStep2RoundtableDialogues(prev => [...prev, userInjectedMessage]);
setCustomAgentInput("");
triggerToast("您的人工纠偏指令已同步至圆桌节点!");
// Automatically trigger a response from the designated Agent
setTimeout(() => {
const respTime = new Date().toLocaleTimeString("zh-CN", { hour12: false });
const agentReplyLine = {
id: Date.now() + 5,
sender: "总结陈述智能体 (Gemini 2.5 Pro)",
avatarType: "blue",
content: `🤝 收到统筹人工指令!我已将您的“${userInjectedMessage.content.substring(13, 28)}...”微指令分解并广播映射至情报组、设计组和安全审查端,开始实时博弈重收敛计算。`,
time: respTime,
confidence: "全局统筹"
};
setStep2RoundtableDialogues(prev => [...prev, agentReplyLine]);
triggerToast("研筹团队成功接收指令!");
}, 1250);
void runOrchestration(`用户人工干预新指令:${userText}\n\n请结合当前讨论状态,决定是否继续派活或直接给出更新后的结论。`, threadIds, coordinatorName);
};
return (
@@ -1650,8 +1724,8 @@ export default function RoundtablePlanningPage() {
<div className="space-y-3">
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-500 uppercase tracking-wider block">推理本源模型</label>
<select
value={agentConfigs[selectedAgentId]?.model || "Gemini 2.5 Flash"}
<select
value={agentConfigs[selectedAgentId]?.model || DEFAULT_MODEL}
onChange={(e) => {
const val = e.target.value;
setAgentConfigs(prev => ({
@@ -1663,9 +1737,18 @@ export default function RoundtablePlanningPage() {
}));
}}
className="w-full bg-white border border-slate-200 rounded-xl px-2.5 py-1.5 text-xs text-slate-700 font-bold focus:outline-none"
disabled={modelsLoading}
>
<option value="Gemini 2.5 Pro">Gemini 2.5 Pro (深度分析型)</option>
<option value="Gemini 2.5 Flash">Gemini 2.5 Flash (疾速自适应型)</option>
{availableModels.length === 0 && (
<option value={DEFAULT_MODEL}>{DEFAULT_MODEL}</option>
)}
{availableModels.map((m) => (
<option key={m.name} value={m.name}>
{m.display_name && m.display_name !== m.name
? `${m.display_name} (${m.name})`
: m.name}
</option>
))}
</select>
</div>