diff --git a/frontend-web/src/roundtable-planning/api/recommend.ts b/frontend-web/src/roundtable-planning/api/recommend.ts index 4ccf96bd..db73b4ed 100644 --- a/frontend-web/src/roundtable-planning/api/recommend.ts +++ b/frontend-web/src/roundtable-planning/api/recommend.ts @@ -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 pick from the live candidate pool as fallback. + * caller should apply its fallback list (typically the 6 roundtable-* seats). * `error` → upstream / network failure. */ status: "done" | "asking" | "error"; diff --git a/frontend-web/src/roundtable-planning/components/MessageBubble.tsx b/frontend-web/src/roundtable-planning/components/MessageBubble.tsx index 92476072..6f83fc62 100644 --- a/frontend-web/src/roundtable-planning/components/MessageBubble.tsx +++ b/frontend-web/src/roundtable-planning/components/MessageBubble.tsx @@ -202,6 +202,8 @@ export interface ReasoningBlockProps { /** 用于显示「思考用时」的可选时长(秒)。流式期间不显示。 */ durationSec?: number; className?: string; + /** 展开内容区额外 class,例如 max-h + overflow-y-auto。 */ + contentClassName?: string; } const REASONING_AUTO_CLOSE_DELAY_MS = 1000; @@ -211,6 +213,7 @@ export function ReasoningBlock({ streaming = false, durationSec, className, + contentClassName, }: ReasoningBlockProps) { // open 初始值跟 streaming 同步:流式中默认展开方便观察思考过程,完成后会自动收起一次。 const [open, setOpen] = useState(streaming); @@ -260,7 +263,12 @@ export function ReasoningBlock({ /> {open && ( -
+
{children}
)} diff --git a/frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx b/frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx index 73ec432a..415b48a2 100644 --- a/frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx +++ b/frontend-web/src/roundtable-planning/components/RecommendAgentsDialog.tsx @@ -62,6 +62,20 @@ 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" @@ -305,17 +319,12 @@ export function RecommendAgentsDialog({ return; } - // 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; - } + // 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); setSelectedIds(new Set(seed)); const { rationale } = parseRecommendStreamContent(result.content || ""); setState((prev) => ({ @@ -325,7 +334,7 @@ export function RecommendAgentsDialog({ recommendedIds: seed, reasons, streamPhase: null, - warning: "智能推荐未生成有效结果,已为你勾选候选池中的前几位智能体,可手动调整。", + warning: "智能推荐未生成有效结果,已为你勾选默认的圆桌席位,可手动调整。", })); } catch (err) { if (myGen !== generationRef.current) return; @@ -421,8 +430,8 @@ export function RecommendAgentsDialog({ return ( - - + + 选择参会智能体 @@ -457,7 +466,7 @@ export function RecommendAgentsDialog({ 推荐参会智能体
-
+
{state.phase === "error" ? (
{state.errorMessage ?? "未知错误"} @@ -527,7 +536,7 @@ export function RecommendAgentsDialog({
- +
+
- {open &&
{children}
} + {open && ( +
+ {children} +
+ )}
); } @@ -736,7 +749,12 @@ function RecommenderOutput({ state }: { state: InternalState }) { )} {hasReasoning && ( - {reasoning!} + + {reasoning!} + )} {!hasRationale && !hasReasoning && isStreaming && ( @@ -744,7 +762,7 @@ function RecommenderOutput({ state }: { state: InternalState }) { )} {hasRationale && ( -
+
{cleanedRationale} {isStreaming && }
diff --git a/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts b/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts index dcc0b717..54c65b56 100644 --- a/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts +++ b/frontend-web/src/roundtable-planning/hooks/useStep2Orchestration.ts @@ -26,6 +26,7 @@ import { type ThreadIdMap, } from "@/roundtable-planning/api/multi-agent"; import { + DEFAULT_SELECTED_AGENTS, MAX_CYCLES, buildAgentNameToRole, buildRoleTemplates, @@ -98,8 +99,18 @@ export type Step2Snapshot = { seatStubMessages: Record; }; -/** 席位 Persona 配置;初始为空,用户在 Persona 抽屉保存后写入。 */ -const DEFAULT_AGENT_CONFIGS: Record = {}; +/** + * 6 个默认席位的初始 Persona 配置。model 留空 → 运行时 `agentConfigs[seat]?.model + * || selectedModel || undefined`,即「席位覆盖优先,否则跟随全局模型」。 + */ +const DEFAULT_AGENT_CONFIGS: Record = { + 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小时里程碑执行路径规划、重排和资源调配演练序列。" }, +}; export interface UseStep2OrchestrationOptions { /** 当前页面 currentStep,===2 触发自动 init。 */ @@ -242,7 +253,9 @@ export function useStep2Orchestration( Record >({}); const seatStubMessagesRef = useRef>({}); - const [selectedAgents, setSelectedAgents] = useState([]); + const [selectedAgents, setSelectedAgents] = useState( + DEFAULT_SELECTED_AGENTS, + ); const [threadIds, setThreadIds] = useState(null); const [coordinatorName, setCoordinatorName] = useState(null); const [isInitializing, setIsInitializing] = useState(false); diff --git a/frontend-web/src/roundtable-planning/lib/roundtable-constants.ts b/frontend-web/src/roundtable-planning/lib/roundtable-constants.ts index 7d860806..37e90e31 100644 --- a/frontend-web/src/roundtable-planning/lib/roundtable-constants.ts +++ b/frontend-web/src/roundtable-planning/lib/roundtable-constants.ts @@ -24,7 +24,23 @@ 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", @@ -33,12 +49,28 @@ export const FALLBACK_AVATAR_TYPES = [ "pink", ] as const; -export function getAvatarTypeFor(_agentId: string, index: number): string { - return FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length]; +/** + * 用户未确认推荐弹窗时,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 getDisplayNameFor(agent: SelectedAgent): string { - return agent.name; + return BUILTIN_AGENT_META[agent.agent_id]?.displayName ?? agent.name; } /** diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index ad72b49b..ea94aee7 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -62,6 +62,7 @@ import { import { useStep2Orchestration } from "@/roundtable-planning/hooks/useStep2Orchestration"; import { useDraftPersistence } from "@/roundtable-planning/hooks/useDraftPersistence"; import { + BUILTIN_AGENT_META, getAvatarTypeFor, getDisplayNameFor, makeRolesFromTemplates, @@ -259,7 +260,8 @@ export default function RoundtablePlanningPage() { (agents: Array<{ agent_id: string; name: string }>) => { const normalized: SelectedAgent[] = agents.map(({ agent_id, name }) => ({ agent_id, - name: name ?? agent_id, + // 内置 agent 走 BUILTIN_AGENT_META;非内置用候选池传过来的 name。 + name: BUILTIN_AGENT_META[agent_id]?.displayName ?? name ?? agent_id, })); step2.setSelectedAgents(normalized); step2.resetInitGate(); @@ -893,7 +895,9 @@ function LeftSidebar({ ...selectedAgents.map((agent, i) => ({ id: agent.agent_id, name: getDisplayNameFor(agent), - role: "圆桌席位", + role: BUILTIN_AGENT_META[agent.agent_id] + ? `${BUILTIN_AGENT_META[agent.agent_id].displayName} 席位` + : "圆桌席位", icon: getAvatar(getAvatarTypeFor(agent.agent_id, i), "h-7 w-7"), })), ].map((contrib, idx) => ( diff --git a/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed.py b/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed.py index 96e81628..f4bd40df 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed.py +++ b/offline-backend-20260512/backend/app/gateway/routers/_roundtable_seed.py @@ -406,7 +406,7 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员 > 以下两个示例覆盖**不同任务类型**,目的是教你「思路要呼应任务本身」,**不要**把任何一个示例的具体职能链当模板。 -### 示例 A:合规改造任务(领域候选池) +### 示例 A:合规改造任务(职能型候选池) **用户输入**: ``` @@ -415,11 +415,12 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员 关键假设:现有系统具备基础数字化能力;允许预定窗口停机 候选智能体池: -- compliance-research (合规调研):检索同类历史案例、行业最佳实践与监管口径 -- infra-capacity (基础设施评估):评估硬件容量与系统量化承载力 -- solution-architect (方案架构):产出主备方案与关键路径 -- risk-auditor (风险审查):合规红线检查与收敛建议 -- execution-planner (执行规划):里程碑与资源排布 +- roundtable-intelligence (情报收集):检索同类历史案例、行业最佳实践与外部情报 +- roundtable-environment (环境评估):评估硬件容量与系统量化承载力 +- roundtable-solution-design (方案设计):产出主备方案与关键路径 +- roundtable-risk-review (风险审查):合规红线检查与收敛建议 +- roundtable-execution-plan (执行规划):里程碑与资源排布 +- roundtable-summary (总结陈述):归纳冲突标记与盖印 - my-cost-calc (成本测算):基于预算和资源单价计算总成本 ``` @@ -430,10 +431,10 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员 ```json { "picks": [ - {"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": "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": "my-cost-calc", "reason": "校验整体方案落在 500 万元预算硬约束内"} ] } @@ -452,7 +453,7 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员 - channel-planner (渠道规划师):按预算和受众设计跨平台投放组合 - audience-insight (受众洞察):画像、关键词、情绪点研究 - creative-copy (创意文案):产出落地页与社媒文案变体 -- ad-compliance (广告合规):审查文案与素材合规风险 +- roundtable-risk-review (风险审查):合规红线检查与收敛建议 ``` **你的输出**: @@ -470,8 +471,8 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员 } ``` -> **关键观察**:两个示例都从候选池里按任务语义挑选,**不要**套用固定职能链(如"调研→设计→风险→执行")。 -> 候选池里的智能体能不能匹配任务,只看 description 和任务的实际语义。如果候选池里有"广告设计智能体"且任务是广告设计,**你必须挑它**,而不是机械复制示例 A 的链路。 +> **关键观察**:示例 A 选了 `roundtable-intelligence / solution-design / execution-plan` 等"职能型"智能体,示例 B **完全没用任何 `roundtable-*` 内置**,而是从用户自建的领域智能体里挑。 +> 这是因为:**候选池里的智能体能不能匹配任务,只看 description 和任务的实际语义,不看 id 是不是 `roundtable-` 开头**。如果候选池里有"广告设计智能体"且任务是广告设计,**你必须挑它**,而不是套用示例 A 的职能链。 """