Merge branch 'dhk-1' into 'master'

Dhk 1

See merge request knowledge-base-bj/deerflow-server!68
This commit is contained in:
董宏凯 committed 2026-05-27 12:28:25 +00:00
commit 3033597f02
8 files changed
+164 -200

No files matched your search

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { PenLine } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -10,8 +10,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { getBackendBaseURL } from '@/core/config';
import { apiFetch } from '@/core/api';
import { useModels } from '@/core/models/hooks';
import type { AIWritingRequest } from '../../types/ai-writing';
import { useArticleTypes } from '../../hooks/useArticleTypes';
@@ -23,11 +22,6 @@ interface AIWritingFormProps {
hideAdvancedFields?: boolean;
}
interface ModelOption {
name: string;
displayName: string;
}
const INPUT_CLASS =
'flex h-12 w-full rounded-xl border border-[#e8dedd] bg-white px-4 py-2 text-sm shadow-sm ' +
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 ' +
@@ -52,10 +46,24 @@ export function AIWritingForm({
const [wordCountInput, setWordCountInput] = useState('800');
const [strictMode, setStrictMode] = useState(false);
const [modelName, setModelName] = useState('');
const [models, setModels] = useState<ModelOption[]>([]);
const { models, isLoading: modelsLoading } = useModels();
const { articleTypes, loading: typesLoading, refresh: refreshTypes } = useArticleTypes();
// 首次 /api/models 返回后默认选第一项;配置变更导致当前值不在列表时也回退到首项。
useEffect(() => {
if (models.length === 0) return;
const stillExists = models.some((m) => m.name === modelName);
if (!modelName || !stillExists) {
setModelName(models[0].name);
}
}, [models, modelName]);
const selectedModel = useMemo(
() => models.find((m) => m.name === modelName) ?? models[0],
[models, modelName],
);
// 初始化时加载文章类型
useEffect(() => {
refreshTypes();
@@ -81,25 +89,6 @@ export function AIWritingForm({
}
}
useEffect(() => {
apiFetch(`${getBackendBaseURL()}/api/models`)
.then((r) => r.json())
.then((data) => {
const list: ModelOption[] = (data.models ?? []).map(
(m: { name: string; display_name?: string }) => ({
name: m.name,
displayName: m.display_name || m.name,
}),
);
setModels(list);
if (list.length > 0 && !modelName) {
setModelName(list[0].name);
}
})
.catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function handleWordCountBlur() {
const num = parseInt(wordCountInput, 10);
if (isNaN(num) || num < 100) {
@@ -122,7 +111,7 @@ export function AIWritingForm({
wordCountTarget: wordCount,
keywordCount: DEFAULT_KEYWORD_COUNT,
writingMode: hideAdvancedFields ? 'loose' : strictMode ? 'strict' : 'loose',
modelName: modelName || undefined,
modelName: selectedModel?.name || modelName || undefined,
});
}
@@ -192,17 +181,25 @@ export function AIWritingForm({
<div className="mt-4 space-y-2">
<label className={FIELD_LABEL_CLASS}>写作模型</label>
<Select
value={modelName}
value={selectedModel?.name ?? ''}
onValueChange={setModelName}
disabled={disabled || models.length === 0}
disabled={disabled || modelsLoading || models.length === 0}
>
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
<SelectValue placeholder={models.length > 0 ? '选择模型…' : '模型加载中…'} />
<SelectValue
placeholder={
modelsLoading
? '模型加载中…'
: models.length > 0
? '选择模型…'
: '暂无可用模型'
}
/>
</SelectTrigger>
<SelectContent>
{models.map((m) => (
<SelectItem key={m.name} value={m.name} className="text-sm">
{m.displayName}
{m.display_name || m.name}
</SelectItem>
))}
</SelectContent>
@@ -27,7 +27,7 @@ export interface StreamRecommendResult {
* `done` → picks parsed and validated, ready to populate the dialog as
* default-checked selections.
* `asking` → recommender failed to emit a usable [RECOMMEND_READY] block,
* caller should apply its fallback list (typically the 6 roundtable-* seats).
* caller should pick from the live candidate pool as fallback.
* `error` → upstream / network failure.
*/
status: "done" | "asking" | "error";
@@ -62,20 +62,6 @@ export interface RecommendAgentsDialogProps {
const MIN_SELECTED = 2;
const MAX_SELECTED = 8;
/**
* Default selection when the recommender fails or returns < 2 valid picks.
* Intersected with the live candidate pool at runtime so missing built-ins
* don't break the dialog.
*/
const FALLBACK_AGENT_IDS = [
"roundtable-intelligence",
"roundtable-environment",
"roundtable-solution-design",
"roundtable-risk-review",
"roundtable-execution-plan",
"roundtable-summary",
];
type DialogPhase =
| "idle"
| "loading_agents"
@@ -319,12 +305,17 @@ export function RecommendAgentsDialog({
return;
}
// Fallback: status === "asking" or picks too few. Use the 6 built-in
// roundtable seats, intersected against the live pool.
const fallback = intersectInOrder(FALLBACK_AGENT_IDS, candidates);
const seed = fallback.length >= MIN_SELECTED
? fallback
: candidates.slice(0, MIN_SELECTED).map((c) => c.agent_id);
// Fallback: status === "asking" or picks too few — pick from live pool.
const seed = candidates.slice(0, MIN_SELECTED).map((c) => c.agent_id);
if (seed.length < MIN_SELECTED) {
setState((prev) => ({
...prev,
phase: "error",
streamPhase: null,
errorMessage: `候选智能体不足 ${MIN_SELECTED} 个,无法进入圆桌研讨。请先发布更多智能体。`,
}));
return;
}
setSelectedIds(new Set(seed));
const { rationale } = parseRecommendStreamContent(result.content || "");
setState((prev) => ({
@@ -334,7 +325,7 @@ export function RecommendAgentsDialog({
recommendedIds: seed,
reasons,
streamPhase: null,
warning: "智能推荐未生成有效结果,已为你勾选默认的圆桌席位,可手动调整。",
warning: "智能推荐未生成有效结果,已为你勾选候选池中的前几位智能体,可手动调整。",
}));
} catch (err) {
if (myGen !== generationRef.current) return;
@@ -15,7 +15,7 @@
*
* 状态全部来自 `useStep2Orchestration` —— 本组件不调任何后端、不持久化。
*/
import { useRef } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
Cpu,
FileIcon,
@@ -80,7 +80,6 @@ export interface Step2PanelProps {
/** 主聊天里 `present_files` 会在气泡下方渲染卡片;Step 2 沿用同一交互。 */
openArtifactPreview: (threadId: string, path: string) => void;
// ─── Setters ──────────────────────────────────────────────────
setIsRoundTableRunning: React.Dispatch<React.SetStateAction<boolean>>;
setClarificationDraft: React.Dispatch<React.SetStateAction<string>>;
setCustomAgentInput: React.Dispatch<React.SetStateAction<string>>;
setAgentConfigs: React.Dispatch<
@@ -88,8 +87,10 @@ export interface Step2PanelProps {
>;
setSelectedAgentId: React.Dispatch<React.SetStateAction<number | null>>;
// ─── Handlers ─────────────────────────────────────────────────
onPauseRoundtable: () => void;
onResumeRoundtable: () => void;
onRecordInjectMessage: () => void;
onSubmitClarification: (answer: string) => void;
onInjectToRoundtable: () => void;
onSavePersona: (id: number) => void;
onProceedToStep3: () => void;
onTriggerToast: (msg: string) => void;
@@ -116,13 +117,14 @@ export function Step2Panel(props: Step2PanelProps) {
seatStubMessages,
openArtifactFromStep,
openArtifactPreview,
setIsRoundTableRunning,
setClarificationDraft,
setCustomAgentInput,
setAgentConfigs,
setSelectedAgentId,
onPauseRoundtable,
onResumeRoundtable,
onRecordInjectMessage,
onSubmitClarification,
onInjectToRoundtable,
onSavePersona,
onProceedToStep3,
onTriggerToast,
@@ -132,6 +134,25 @@ export function Step2Panel(props: Step2PanelProps) {
const { atBottom: step2AtBottom, scrollToBottom: scrollStep2ToBottom } =
useAutoScroll(step2ScrollRef, [step2RoundtableDialogues]);
// 干预研审 → 暂停会商:与右上角按钮共用 onPauseRoundtable;乐观 UI 避免等父 state 刷新。
const [pauseFlash, setPauseFlash] = useState(false);
const [optimisticPaused, setOptimisticPaused] = useState(false);
const showPausedUi = !isRoundTableRunning || optimisticPaused;
useEffect(() => {
if (!isRoundTableRunning) {
setOptimisticPaused(false);
}
}, [isRoundTableRunning]);
const handleInject = useCallback(() => {
setOptimisticPaused(true);
onPauseRoundtable();
onRecordInjectMessage();
setPauseFlash(true);
window.setTimeout(() => setPauseFlash(false), 1600);
}, [onPauseRoundtable, onRecordInjectMessage]);
const isSandboxStreaming =
isOrchestrating ||
step2RoundtableDialogues.some((dialogue) => dialogue.streaming === true);
@@ -175,28 +196,28 @@ export function Step2Panel(props: Step2PanelProps) {
</option>
))}
</select>
{isRoundTableRunning ? (
{showPausedUi ? (
<button
onClick={() => {
setIsRoundTableRunning(false);
onTriggerToast("⏸️ 会商已暂停");
}}
type="button"
onClick={onResumeRoundtable}
className={`bg-emerald-100 hover:bg-emerald-200 text-emerald-800 text-[10px] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-emerald-200/40 shadow-sm ${
pauseFlash
? "ring-2 ring-emerald-400 ring-offset-1 animate-pulse"
: ""
}`}
>
<Play className="w-3.5 h-3.5 fill-emerald-800" />
恢复会商
</button>
) : (
<button
type="button"
onClick={onPauseRoundtable}
className="bg-amber-100 hover:bg-amber-200 text-amber-800 text-[10px] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-amber-200/40 shadow-sm"
>
<Pause className="w-3.5 h-3.5" />
暂停会商
</button>
) : (
<button
onClick={() => {
setIsRoundTableRunning(true);
onTriggerToast("▶️ 恢复大模型并行博弈与推理决策");
}}
className="bg-emerald-100 hover:bg-emerald-200 text-emerald-800 text-[10px] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-emerald-200/40 shadow-sm"
>
<Play className="w-3.5 h-3.5 fill-emerald-800" />
恢复会商
</button>
)}
</div>
</div>
@@ -324,14 +345,16 @@ export function Step2Panel(props: Step2PanelProps) {
value={customAgentInput}
onChange={(e) => setCustomAgentInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") onInjectToRoundtable();
if (e.key === "Enter") handleInject();
}}
placeholder="作为指挥官,您可以随时插话输入指令,对智能体博弈朝向实施人工干预..."
className="flex-1 bg-white border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs focus:outline-none focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 font-medium"
/>
<button
onClick={onInjectToRoundtable}
type="button"
onClick={handleInject}
title="暂停会商并记录干预指令;点击右上角「恢复会商」继续执行"
className="bg-primary hover:bg-[var(--brand-900)] text-white text-xs font-bold px-4 py-2.5 rounded-xl transition shrink-0 cursor-pointer active:scale-95 shadow-md shadow-primary/10"
>
干预研审
@@ -26,7 +26,6 @@ import {
type ThreadIdMap,
} from "@/roundtable-planning/api/multi-agent";
import {
DEFAULT_SELECTED_AGENTS,
MAX_CYCLES,
buildAgentNameToRole,
buildRoleTemplates,
@@ -99,18 +98,8 @@ export type Step2Snapshot = {
seatStubMessages: Record<string, StubAiMessage[]>;
};
/**
* 6 个默认席位的初始 Persona 配置。model 留空 → 运行时 `agentConfigs[seat]?.model
* || selectedModel || undefined`,即「席位覆盖优先,否则跟随全局模型」。
*/
const DEFAULT_AGENT_CONFIGS: Record<number, AgentConfig> = {
2: { model: "", weight: 90, temp: 0.2, prompt: "你负责解析核心任务、资源极限和用户目标偏好,监督全局研商收敛。" },
3: { model: "", weight: 85, temp: 0.3, prompt: "你负责搜集电力、通信系统、历史极端部署条件情报,支持备灾架构体系。" },
4: { model: "", weight: 88, temp: 0.1, prompt: "你评估服务器机房热载荷、算力并发承载极限,预报运行瓶颈与风险时钟。" },
5: { model: "", weight: 92, temp: 0.4, prompt: "你负责双全栈主备建模设计,高内聚低耦合,规避不可预知停机崩溃。" },
6: { model: "", weight: 95, temp: 0.1, prompt: "你作为安全主管,严厉把控预算开销与合规通过率防线,识别计划漏洞。" },
7: { model: "", weight: 80, temp: 0.4, prompt: "你负责对72小时里程碑执行路径规划、重排和资源调配演练序列。" },
};
/** 席位 Persona 配置;初始为空,用户在 Persona 抽屉保存后写入。 */
const DEFAULT_AGENT_CONFIGS: Record<number, AgentConfig> = {};
export interface UseStep2OrchestrationOptions {
/** 当前页面 currentStep,===2 触发自动 init。 */
@@ -171,9 +160,15 @@ export interface UseStep2OrchestrationReturn {
// ─── Handlers ───────────────────────────────────────────────────────
/** 中断当前编排循环 + 所有 in-flight SSE。 */
stopOrchestration: () => void;
/** 与 Step2Panel「暂停会商」按钮相同 —— 挂起编排并中止 in-flight SSE。 */
pauseRoundtable: () => void;
/** 与 Step2Panel「恢复会商」按钮相同 —— 恢复自动研讨(含待处理的干预指令)。 */
resumeRoundtable: () => void;
/** 记录干预指令(不暂停);通常由 Step2Panel 在 pauseRoundtable 之后调用。 */
recordInjectMessage: () => void;
/** 中断所有 in-flight SSE(给新建任务 / 加载草稿用)。 */
abortAllInFlight: () => void;
/** 用户「干预研审」按钮 —— 追加一条人工指令气泡并触发一轮编排。 */
/** @deprecated 请用 pauseRoundtable + recordInjectMessage;保留兼容组合调用。 */
handleInjectToRoundtable: () => void;
/** 总控澄清回复 —— 把回答拼到 prompt 里再起一轮编排。 */
submitClarification: (rawAnswer: string) => void;
@@ -247,9 +242,7 @@ export function useStep2Orchestration(
Record<string, StubAiMessage[]>
>({});
const seatStubMessagesRef = useRef<Record<string, StubAiMessage[]>>({});
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>(
DEFAULT_SELECTED_AGENTS,
);
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>([]);
const [threadIds, setThreadIds] = useState<ThreadIdMap | null>(null);
const [coordinatorName, setCoordinatorName] = useState<string | null>(null);
const [isInitializing, setIsInitializing] = useState<boolean>(false);
@@ -296,9 +289,8 @@ export function useStep2Orchestration(
const orchestrationCancelRef = useRef<boolean>(false);
const step2InitRef = useRef<boolean>(false);
const activeAbortersRef = useRef<Set<AbortController>>(new Set());
/** 干预研审:暂停当前轮次后待执行的 leader prompt。 */
/** 干预研审:恢复会商后待执行的 leader prompt。 */
const pendingInjectPromptRef = useRef<string | null>(null);
const [injectFlushToken, setInjectFlushToken] = useState(0);
// selectedModel / agentConfigs / agentNameToRole / threadIds / coordinatorName
// 等等被 runOrchestration 在循环里反复读取。把它们打到 ref 上,可以让
// runOrchestration 的 useCallback deps 大幅收窄,避免每次模型切换、agentConfigs
@@ -1112,17 +1104,11 @@ export function useStep2Orchestration(
// 挂起 / 恢复:与上面的 init effect 独立。`isRoundTableRunning` toggle 时:
// - false → stopOrchestration
// - true 且 idle → 触发一次「请继续」的 leader cycle
// 注意 deps 故意只有 isRoundTableRunning,不再额外加 currentStep —— 回到 Step 2
// 检视讨论时不应该自动 fire 新的 leader 调用。
// - true 且 idle → 触发 leader cycle(若有干预指令则优先执行)
useEffect(() => {
if (currentStep !== 2) return;
if (!isRoundTableRunning) {
// 干预研审会先写入 pending 再置 false;此时 stop 已在 handler 里触发,
// 避免 effect 再次 stop 误杀随后 flush 启动的 inject 编排。
if (!pendingInjectPromptRef.current) {
stopOrchestration();
}
stopOrchestration();
return;
}
if (
@@ -1132,6 +1118,18 @@ export function useStep2Orchestration(
!coordinatorNameRef.current
)
return;
const pending = pendingInjectPromptRef.current;
if (pending) {
pendingInjectPromptRef.current = null;
void runOrchestration(
pending,
threadIdsRef.current,
coordinatorNameRef.current,
);
return;
}
void runOrchestration(
"用户已恢复研讨,请继续上一轮的协调工作。",
threadIdsRef.current,
@@ -1140,53 +1138,41 @@ export function useStep2Orchestration(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isRoundTableRunning]);
// 干预研审:当前轮次停止后 flush 待执行的 inject 编排(编排期间 UI 仍为「恢复会商」)。
useEffect(() => {
if (isOrchestrating) return;
const pending = pendingInjectPromptRef.current;
if (!pending) return;
if (!threadIdsRef.current || !coordinatorNameRef.current) return;
pendingInjectPromptRef.current = null;
triggerToast("您的人工纠偏指令已同步至圆桌节点!");
void runOrchestration(
pending,
threadIdsRef.current,
coordinatorNameRef.current,
);
}, [isOrchestrating, injectFlushToken, runOrchestration, triggerToast]);
const pauseRoundtable = useCallback(() => {
setIsRoundTableRunning(false);
orchestrationCancelRef.current = true;
stopOrchestration();
triggerToast("⏸️ 会商已暂停");
}, [stopOrchestration, triggerToast]);
// ─── Public handlers ─────────────────────────────────────────────
const handleInjectToRoundtable = useCallback(() => {
if (!customAgentInput.trim()) return;
const resumeRoundtable = useCallback(() => {
setIsRoundTableRunning(true);
triggerToast("▶️ 恢复大模型并行博弈与推理决策");
}, [triggerToast]);
const recordInjectMessage = useCallback(() => {
const userText = customAgentInput.trim();
if (!userText) return;
if (!threadIdsRef.current || !coordinatorNameRef.current) {
triggerToast("⚠️ 圆桌尚未就绪,请稍候");
triggerToast("⚠️ 圆桌尚未就绪,无法记录干预指令");
return;
}
const userText = customAgentInput.trim();
setCustomAgentInput("");
const prompt = `用户人工干预新指令:${userText}\n\n请结合当前讨论状态,决定是否继续派活或直接给出更新后的结论。`;
pendingInjectPromptRef.current = prompt;
// 与 Step2Panel 右上角「暂停会商 / 恢复会商」联动:立即进入挂起态。
setIsRoundTableRunning(false);
stopOrchestration();
appendDialogue({
sender: "用户 (插话干预)",
avatarType: "human",
content: `💡 (人工指令介入) ${userText}`,
confidence: "最高指令",
});
pendingInjectPromptRef.current = `用户人工干预新指令:${userText}\n\n请结合当前讨论状态,决定是否继续派活或直接给出更新后的结论。`;
triggerToast("干预指令已记录,点击「恢复会商」后执行");
}, [appendDialogue, customAgentInput, triggerToast]);
if (isOrchestratingRef.current) {
triggerToast("⏸️ 会商已暂停,当前轮次结束后将处理您的干预指令");
} else {
triggerToast("⏸️ 会商已暂停,正在处理您的干预指令…");
}
setInjectFlushToken((n) => n + 1);
}, [appendDialogue, customAgentInput, stopOrchestration, triggerToast]);
const handleInjectToRoundtable = useCallback(() => {
pauseRoundtable();
recordInjectMessage();
}, [pauseRoundtable, recordInjectMessage]);
const submitClarification = useCallback(
(rawAnswer: string) => {
@@ -1378,6 +1364,9 @@ export function useStep2Orchestration(
setAgentConfigs,
resetInitGate,
stopOrchestration,
pauseRoundtable,
resumeRoundtable,
recordInjectMessage,
abortAllInFlight,
handleInjectToRoundtable,
submitClarification,
@@ -24,23 +24,7 @@ export type RoleEntry = {
/** 用户在推荐弹窗选定的一个研讨席位。 */
export type SelectedAgent = { agent_id: string; name: string };
/**
* 6 个内置 roundtable 席位的展示元数据。用户保留默认席位时,
* 头像 / 配色 / 中文显示名都按这里走;非内置(用户自建)走 FALLBACK_AVATAR_TYPES。
*/
export const BUILTIN_AGENT_META: Record<
string,
{ avatarType: string; displayName: string }
> = {
"roundtable-intelligence": { avatarType: "purple", displayName: "情报收集" },
"roundtable-environment": { avatarType: "emerald", displayName: "环境评估" },
"roundtable-solution-design": { avatarType: "human", displayName: "方案设计" },
"roundtable-risk-review": { avatarType: "amber", displayName: "风险审查" },
"roundtable-execution-plan": { avatarType: "pink", displayName: "执行规划" },
"roundtable-summary": { avatarType: "blue", displayName: "总结陈述" },
};
/** 非内置 agent 的头像配色轮换池。 */
/** 研讨席位的头像配色轮换池。 */
export const FALLBACK_AVATAR_TYPES = [
"blue",
"purple",
@@ -49,28 +33,12 @@ export const FALLBACK_AVATAR_TYPES = [
"pink",
] as const;
/**
* 用户未确认推荐弹窗时,Step 2 默认使用的 6 个内置席位。
* 保留默认时页面与原硬编码行为完全一致。
*/
export const DEFAULT_SELECTED_AGENTS: SelectedAgent[] = [
{ agent_id: "roundtable-summary", name: "总结陈述" },
{ agent_id: "roundtable-intelligence", name: "情报收集" },
{ agent_id: "roundtable-environment", name: "环境评估" },
{ agent_id: "roundtable-solution-design", name: "方案设计" },
{ agent_id: "roundtable-risk-review", name: "风险审查" },
{ agent_id: "roundtable-execution-plan", name: "执行规划" },
];
export function getAvatarTypeFor(agentId: string, index: number): string {
return (
BUILTIN_AGENT_META[agentId]?.avatarType ??
FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length]
);
export function getAvatarTypeFor(_agentId: string, index: number): string {
return FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length];
}
export function getDisplayNameFor(agent: SelectedAgent): string {
return BUILTIN_AGENT_META[agent.agent_id]?.displayName ?? agent.name;
return agent.name;
}
/**
@@ -62,7 +62,6 @@ import {
import { useStep2Orchestration } from "@/roundtable-planning/hooks/useStep2Orchestration";
import { useDraftPersistence } from "@/roundtable-planning/hooks/useDraftPersistence";
import {
BUILTIN_AGENT_META,
getAvatarTypeFor,
getDisplayNameFor,
makeRolesFromTemplates,
@@ -260,8 +259,7 @@ export default function RoundtablePlanningPage() {
(agents: Array<{ agent_id: string; name: string }>) => {
const normalized: SelectedAgent[] = agents.map(({ agent_id, name }) => ({
agent_id,
// 内置 agent 走 BUILTIN_AGENT_META;非内置用候选池传过来的 name。
name: BUILTIN_AGENT_META[agent_id]?.displayName ?? name ?? agent_id,
name: name ?? agent_id,
}));
step2.setSelectedAgents(normalized);
step2.resetInitGate();
@@ -496,13 +494,14 @@ export default function RoundtablePlanningPage() {
seatStubMessages={step2.seatStubMessages}
openArtifactFromStep={step2.openArtifactFromStep}
openArtifactPreview={step2.openArtifactPreview}
setIsRoundTableRunning={step2.setIsRoundTableRunning}
onPauseRoundtable={step2.pauseRoundtable}
onResumeRoundtable={step2.resumeRoundtable}
onRecordInjectMessage={step2.recordInjectMessage}
setClarificationDraft={step2.setClarificationDraft}
setCustomAgentInput={step2.setCustomAgentInput}
setAgentConfigs={step2.setAgentConfigs}
setSelectedAgentId={step2.setSelectedAgentId}
onSubmitClarification={step2.submitClarification}
onInjectToRoundtable={step2.handleInjectToRoundtable}
onSavePersona={step2.handleSaveAgentAndRecalculate}
onProceedToStep3={() => {
if (!step2.hasConsensus) {
@@ -894,9 +893,7 @@ function LeftSidebar({
...selectedAgents.map((agent, i) => ({
id: agent.agent_id,
name: getDisplayNameFor(agent),
role: BUILTIN_AGENT_META[agent.agent_id]
? `${BUILTIN_AGENT_META[agent.agent_id].displayName} 席位`
: "圆桌席位",
role: "圆桌席位",
icon: getAvatar(getAvatarTypeFor(agent.agent_id, i), "h-7 w-7"),
})),
].map((contrib, idx) => (
@@ -956,8 +953,8 @@ function LeftSidebar({
<IntentSectionIcon className="h-5 w-5" />
</CollapsedSidebarSection>
<CollapsedSidebarSection title="参与角色" className="flex-1 min-h-[120px]">
<div className="flex flex-col items-center gap-2 overflow-y-auto max-h-[320px] custom-scrollbar">
<CollapsedSidebarSection title="参与角色">
<div className="flex flex-col items-center gap-2">
{roles.map((role) => (
<RoleAvatarBadge
key={role.id}
@@ -1245,7 +1242,7 @@ function CollapsedSidebarSection({
<div
title={title}
className={cn(
"w-full bg-white border border-slate-200/50 rounded-2xl p-2 shadow-[0_2px_8px_rgba(0,0,0,0.01)] flex flex-col items-center justify-center",
"w-full shrink-0 bg-white border border-slate-200/50 rounded-2xl p-2 shadow-[0_2px_8px_rgba(0,0,0,0.01)] flex flex-col items-center justify-center",
className,
)}
>
@@ -406,7 +406,7 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
> 以下两个示例覆盖**不同任务类型**,目的是教你「思路要呼应任务本身」,**不要**把任何一个示例的具体职能链当模板。
### 示例 A:合规改造任务(职能型候选池)
### 示例 A:合规改造任务(领域候选池)
**用户输入**:
```
@@ -415,12 +415,11 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
关键假设:现有系统具备基础数字化能力;允许预定窗口停机
候选智能体池:
- roundtable-intelligence (情报收集):检索同类历史案例、行业最佳实践与外部情报
- roundtable-environment (环境评估):评估硬件容量与系统量化承载力
- roundtable-solution-design (方案设计):产出主备方案与关键路径
- roundtable-risk-review (风险审查):合规红线检查与收敛建议
- roundtable-execution-plan (执行规划):里程碑与资源排布
- roundtable-summary (总结陈述):归纳冲突标记与盖印
- compliance-research (合规调研):检索同类历史案例、行业最佳实践与监管口径
- infra-capacity (基础设施评估):评估硬件容量与系统量化承载力
- solution-architect (方案架构):产出主备方案与关键路径
- risk-auditor (风险审查):合规红线检查与收敛建议
- execution-planner (执行规划):里程碑与资源排布
- my-cost-calc (成本测算):基于预算和资源单价计算总成本
```
@@ -431,10 +430,10 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
```json
{
"picks": [
{"agent_id": "roundtable-intelligence", "reason": "检索同类等保 2.0 改造案例与监管口径,直接支撑本次合规对标"},
{"agent_id": "roundtable-solution-design", "reason": "在 6 个月工期约束下产出主备改造方案与关键路径"},
{"agent_id": "roundtable-risk-review", "reason": "对照等保 2.0 合规红线审查方案,避免后期返工"},
{"agent_id": "roundtable-execution-plan", "reason": "把改造拆成可在 6 个月内交付的里程碑与资源排布"},
{"agent_id": "compliance-research", "reason": "检索同类等保 2.0 改造案例与监管口径,直接支撑本次合规对标"},
{"agent_id": "solution-architect", "reason": "在 6 个月工期约束下产出主备改造方案与关键路径"},
{"agent_id": "risk-auditor", "reason": "对照等保 2.0 合规红线审查方案,避免后期返工"},
{"agent_id": "execution-planner", "reason": "把改造拆成可在 6 个月内交付的里程碑与资源排布"},
{"agent_id": "my-cost-calc", "reason": "校验整体方案落在 500 万元预算硬约束内"}
]
}
@@ -453,7 +452,7 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
- channel-planner (渠道规划师):按预算和受众设计跨平台投放组合
- audience-insight (受众洞察):画像、关键词、情绪点研究
- creative-copy (创意文案):产出落地页与社媒文案变体
- roundtable-risk-review (风险审查):合规红线检查与收敛建议
- ad-compliance (广告合规):审查文案与素材合规风险
```
**你的输出**:
@@ -471,8 +470,8 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
}
```
> **关键观察**:示例 A 选了 `roundtable-intelligence / solution-design / execution-plan` 等"职能型"智能体,示例 B **完全没用任何 `roundtable-*` 内置**,而是从用户自建的领域智能体里挑。
> 这是因为:**候选池里的智能体能不能匹配任务,只看 description 和任务的实际语义,不看 id 是不是 `roundtable-` 开头**。如果候选池里有"广告设计智能体"且任务是广告设计,**你必须挑它**,而不是套用示例 A 的职能链。
> **关键观察**:两个示例都从候选池里按任务语义挑选,**不要**套用固定职能链(如"调研→设计→风险→执行")。
> 候选池里的智能体能不能匹配任务,只看 description 和任务的实际语义。如果候选池里有"广告设计智能体"且任务是广告设计,**你必须挑它**,而不是机械复制示例 A 的链路。
"""