diff --git a/frontend-web/docs/multi-agent-frontend-dev.md b/frontend-web/docs/multi-agent-frontend-dev.md index 298536ec..f4336e7c 100644 --- a/frontend-web/docs/multi-agent-frontend-dev.md +++ b/frontend-web/docs/multi-agent-frontend-dev.md @@ -10,7 +10,7 @@ - **整页只有一个文件**:`RoundtablePlanningPage.tsx`(≈ 3 621 行)。三步 UI 通过 `currentStep ∈ {1,2,3}` 切换,不使用 React Router 子路由。 - **不使用 LangGraph SDK**:所有 API 都是自研的 `apiFetch` + `ReadableStream` SSE 解析(见 `api/intent.ts`、`api/recommend.ts`、`api/multi-agent.ts`)。 -- **唯一默认模型**:`DEFAULT_MODEL = "deepseek-chat"`(页面常量,写在 `RoundtablePlanningPage.tsx`)。`useModels()` 只用于 Persona 抽屉的下拉选项。 +- **不写死模型名**:与 `/page/canvas/ai-writing` 同样的策略 —— 全局选中模型由 `selectedModel` state 维护,首次 `useModels()` 完成后初始化为 `availableModels[0].name`,顶栏「模型」下拉可切换。所有 API 调用统一传 `model: selectedModel || undefined`;后端缺省由 `lead_agent._resolve_model_name()` 回退到 `config.yaml` 的 `models[0]`。**内网部署只需在 `config.yaml` 配自家模型即可,前后端均无需改动**。 - **席位区间硬约束**:推荐弹窗要求 **2 ≤ 选中数 ≤ 8**(`MIN_SELECTED` / `MAX_SELECTED`)。 - **总控编排循环上限**:`MAX_CYCLES = 8`,超出后强制停止并 toast。 - **席位派活策略**:**顺序**(不是并行)。旧实现并行触发多路 SSE 会让浏览器卡顿,现已改为顺序。 @@ -364,13 +364,22 @@ isOrchestrating = false - 抽屉控件: - **System Prompt**:仅前端记录(后端覆写未开放) - **Temp**:仅前端记录 - - **推理模型**:选项来自 `useModels()`,默认 `DEFAULT_MODEL` + - **推理模型**:选项来自 `useModels()`;显示值优先级 `agentConfigs[seat]?.model > selectedModel` - 「参数覆盖并重启动算研讨」按钮 → `handleSaveAgentAndRecalculate`: - 把新的 `(seatId, model)` 写入 `agentConfigs` - 追加一条「⚡ [模型偏好已保存] ...」对话气泡 - - **不**立即重启编排;下一轮该席位被派活时 `runOrchestration` 内会读 `agentConfigs[meta.speakerId]?.model` 作为 special 调用的 `model` 参数 + - **不**立即重启编排;下一轮该席位被派活时 `runOrchestration` 内会读 `agentConfigs[meta.speakerId]?.model || selectedModel || undefined` 作为 special 调用的 `model` 参数 -### 5.6 草稿持久化(`utils/drafts.ts`) +### 5.6 全局模型选择器(顶栏) + +- **位置**:顶栏右侧「新建任务」按钮左边,`` 与 Persona 抽屉保持一致;如需统一样式可改为 `@/components/ui/select`,行为不变 + +### 5.7 草稿持久化(`utils/drafts.ts`) - Key:`roundtable-planning:drafts:v1`(`localStorage`) - 草稿快照含:`step1.{intentThreadId, intentReady, interactiveMessages, timelineEntries}`、`step2.{threadIds, coordinatorName, step2RoundtableDialogues, lastLeaderContent, hasConsensus, consensusPercentage, budgetLimit}`、`furthestStep` @@ -544,7 +553,6 @@ interface StreamMultiAgentResult { | 常量 | 值 | 位置 | |------|----|------| -| `DEFAULT_MODEL` | `"deepseek-chat"` | RoundtablePlanningPage | | `COMMANDER_SEAT_ID` | `1` | RoundtablePlanningPage | | `MAX_CYCLES` | `8` | RoundtablePlanningPage(`runOrchestration` 内) | | `MIN_SELECTED` / `MAX_SELECTED` | `2` / `8` | RecommendAgentsDialog | diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index abecde29..4ae16256 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -26,7 +26,13 @@ * 7) **Persona 抽屉**改的 model 不会立即重启编排,下一轮该席位被派活时由 `runOrchestration` * 读 `agentConfigs[meta.speakerId]?.model` 传入 special。prompt / temp 当前**仅前端记录**。 * - * 8) **常量**:`DEFAULT_MODEL`、`MAX_CYCLES=8`、`COMMANDER_SEAT_ID=1`、`INTENT_READY_RE`。 + * 8) **模型选择**:不再硬编码 "deepseek-chat"。全局选中 model 由 `selectedModel` + * state 维护(首次 `useModels()` 完成后初始化为 `availableModels[0].name`), + * 顶栏「模型」下拉可切换;Persona 抽屉的 per-seat 覆盖更高优先级。所有 API 调用 + * 传 `selectedModel || undefined`,后端缺省由 `lead_agent._resolve_model_name()` + * 回退到 `config.yaml` 的 `models[0]`。内网部署只需配 `config.yaml` 即可。 + * + * 9) **常量**:`MAX_CYCLES=8`、`COMMANDER_SEAT_ID=1`、`INTENT_READY_RE`。 * * 调试开关(默认开启): * window.__MULTI_AGENT_INIT_TIMING__ // /api/multi-agent/init 各阶段耗时 @@ -189,7 +195,13 @@ function getAvatar(type: string, size: string = "h-8 w-8") { } // --- Multi-agent backend wiring --- -const DEFAULT_MODEL = "deepseek-chat"; +// +// 模型选择策略(与 /page/canvas/ai-writing 对齐,不再写死 "deepseek-chat"): +// - 全局选中模型由 selectedModel state 维护(首次 useModels 完成后初始化为 models[0].name) +// - 所有 API 调用传 model 时统一用 selectedModel || undefined;后端缺省由 +// lead_agent._resolve_model_name() 回退到 config.yaml 的 models[0] +// - Persona 抽屉的 per-seat 覆盖优先(agentConfigs[seat]?.model > selectedModel) +// 内网部署只需在 config.yaml 配自家模型,前后端都不需要再硬编码具体模型名。 type RoleEntry = { id: number; @@ -373,16 +385,24 @@ export default function RoundtablePlanningPage() { // for each of them concurrently instead of only the most recently set one. const [activeSpeakerSet, setActiveSpeakerSet] = useState>(new Set()); - // Custom presets for agent model, weight, temperature and prompt editors + // Custom presets for agent model, weight, temperature and prompt editors. + // model 字段留空,运行时由 runOrchestration 解析为 + // agentConfigs[seat]?.model || selectedModel || undefined + // 即「席位覆盖优先,否则跟随全局选中模型,否则让后端回退到 config.yaml models[0]」。 const [agentConfigs, setAgentConfigs] = useState<{ [key: number]: { model: string; weight: number; prompt: string; temp: number } }>({ - 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小时里程碑执行路径规划、重排和资源调配演练序列。" }, + 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小时里程碑执行路径规划、重排和资源调配演练序列。" }, }); + // 全局选中模型(对应顶栏「模型」下拉)。 + // 首次 useModels 加载完成后由下面的 useEffect 初始化为 availableModels[0].name, + // 用户可以通过下拉框切换;Persona 抽屉的 per-seat 覆盖更高优先级。 + const [selectedModel, setSelectedModel] = useState(""); + // Message lists state representation to mimic image items & enable actual conversations const [interactiveMessages, setInteractiveMessages] = useState([ { @@ -561,6 +581,17 @@ export default function RoundtablePlanningPage() { } }, []); + // 首次 /api/models 返回后,把 selectedModel 初始化为列表第一个 —— 与 + // AIWritingForm 的行为对齐。若 selectedModel 已经被用户在下拉框里选过,不再覆盖。 + // 若当前选中的 model 已不在 availableModels 里(后端配置变了),也回退到首项。 + useEffect(() => { + if (availableModels.length === 0) return; + const stillExists = availableModels.some((m) => m.name === selectedModel); + if (!selectedModel || !stillExists) { + setSelectedModel(availableModels[0].name); + } + }, [availableModels, selectedModel]); + // Toast auto-dismiss logic useEffect(() => { if (showToast) { @@ -837,7 +868,7 @@ export default function RoundtablePlanningPage() { dAgentThreadId: ids, agentName: coord, newMessage: nextLeaderTask, - model: DEFAULT_MODEL, + model: selectedModel || undefined, signal: leaderAborter.signal, onTextDelta: (text) => appendToDialogue(leaderBubbleId, text), onPhaseChange: (phase, toolName) => { @@ -918,7 +949,8 @@ export default function RoundtablePlanningPage() { dAgentThreadId: ids, agentName: subName, newMessage: task, - model: perSeatModel || DEFAULT_MODEL, + // 优先级: 席位 Persona 覆盖 → 全局选中模型 → 让后端回退到 config.yaml models[0] + model: perSeatModel || selectedModel || undefined, signal: subAborter.signal, onTextDelta: (text) => appendToDialogue(subBubbleId, text), onPhaseChange: (phase, toolName) => { @@ -1026,7 +1058,7 @@ export default function RoundtablePlanningPage() { const initRes = await initMultiAgent({ agentNames: roundtableAgentIds, mainAgentName: coord, - model: DEFAULT_MODEL, + model: selectedModel || undefined, // As each sub-agent's thread comes online, flip every role that // shares the agentId to "在线". Some seats reuse the same agent // (e.g. seat 8 reuses roundtable-summary) so we match on agentId @@ -1148,7 +1180,9 @@ export default function RoundtablePlanningPage() { const role = roles.find(r => r.id === id); const roleName = role?.name || "专家智能体"; const cfg = agentConfigs[id]; - const targetModel = cfg?.model || DEFAULT_MODEL; + // Persona 抽屉里的「保存」反馈文字: 优先显示用户在抽屉里选的 model, + // 没选时退回全局 selectedModel,最后才显示「默认」二字。 + const targetModel = cfg?.model || selectedModel || "默认模型"; const avatarColor = id === 2 ? "blue" @@ -1206,7 +1240,9 @@ export default function RoundtablePlanningPage() { ]); (async () => { try { - const res = await initIntent(DEFAULT_MODEL); + // initIntent 当前不在 thread 上落盘 model,只为占位与兼容;空时由后端 + // 走 lead_agent._resolve_model_name 回退到 config.yaml models[0] + const res = await initIntent(selectedModel || undefined); if (step1InitGenRef.current !== myGen) return; setIntentThreadId(res.thread_id); setTimelineEntries([ @@ -1540,7 +1576,7 @@ export default function RoundtablePlanningPage() { const res = await streamIntent({ threadId: intentThreadId, message: trimmed, - model: DEFAULT_MODEL, + model: selectedModel || undefined, signal: aborter.signal, onTextDelta: (delta) => { setInteractiveMessages(prev => @@ -1791,7 +1827,7 @@ export default function RoundtablePlanningPage() { setIsRecommendDialogOpen(false)} onConfirm={handleConfirmAgents} /> @@ -1919,6 +1955,41 @@ export default function RoundtablePlanningPage() { {/* Right Actions */}
+ {/* 全局模型选择器 + ───────────────────────────────────────────── + 影响 Step 1 / 推荐 / Step 2 init / 总控 leader / 子席位 special + 所有 API 调用。Persona 抽屉里 per-seat 设置的 model 优先级更高。 + 选项来自 /api/models(由 config.yaml 配置),内网部署只需配自家模型。 */} + +