接口修改
This commit is contained in:
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>
|
||||
|
||||
|
||||
Reference in new issue
Block a user