模型下拉框

This commit is contained in:
董宏凯 committed 2026-05-25 20:23:33 +08:00
1 parent 6addf507cb
commit b91a45639f
5 files changed
+121 -32

No files matched your search

@@ -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<Set<number>>(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<string>("");
// 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() {
<RecommendAgentsDialog
open={isRecommendDialogOpen}
intent={intentReady}
model={DEFAULT_MODEL}
model={selectedModel || undefined}
onCancel={() => setIsRecommendDialogOpen(false)}
onConfirm={handleConfirmAgents}
/>
@@ -1919,6 +1955,41 @@ export default function RoundtablePlanningPage() {
{/* Right Actions */}
<div className="flex items-center gap-2">
{/* 全局模型选择器
─────────────────────────────────────────────
影响 Step 1 / 推荐 / Step 2 init / 总控 leader / 子席位 special
所有 API 调用。Persona 抽屉里 per-seat 设置的 model 优先级更高。
选项来自 /api/models(由 config.yaml 配置),内网部署只需配自家模型。 */}
<label className="flex items-center gap-1.5 bg-white border border-[#e2e8f0] rounded-xl px-2.5 py-1.5 text-xs font-bold text-[#334155] dark:border-border dark:bg-secondary dark:text-foreground">
<Cpu className="w-3.5 h-3.5 text-primary" />
<span className="text-[10px] text-slate-400 font-mono">模型</span>
<select
id="global-model-select"
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
disabled={modelsLoading || availableModels.length === 0}
title={
modelsLoading
? "正在加载模型列表…"
: availableModels.length === 0
? "未在 config.yaml 中配置任何模型"
: "切换全局推理模型;不影响 Persona 抽屉里已设置的 per-seat 覆盖"
}
className="bg-transparent border-none outline-none text-xs font-bold text-slate-700 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed dark:text-foreground"
>
{availableModels.length === 0 && (
<option value="">{modelsLoading ? "加载中…" : "未配置模型"}</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>
</label>
<button
id="new-task-btn"
onClick={handleStartNewTask}
@@ -3091,7 +3162,8 @@ export default function RoundtablePlanningPage() {
<div className="space-y-1">
<label className="text-[9px] font-black text-slate-500 uppercase tracking-wider block">推理本源模型</label>
<select
value={agentConfigs[selectedAgentId]?.model || DEFAULT_MODEL}
// 抽屉里显示的「当前模型」:Persona 覆盖优先,否则跟随全局 selectedModel
value={agentConfigs[selectedAgentId]?.model || selectedModel || ""}
onChange={(e) => {
const val = e.target.value;
setAgentConfigs(prev => ({
@@ -3106,7 +3178,7 @@ export default function RoundtablePlanningPage() {
disabled={modelsLoading}
>
{availableModels.length === 0 && (
<option value={DEFAULT_MODEL}>{DEFAULT_MODEL}</option>
<option value="">{modelsLoading ? "加载中…" : "未配置模型"}</option>
)}
{availableModels.map((m) => (
<option key={m.name} value={m.name}>