Merge branch 'dhk-1' into 'master'

Dhk 1

See merge request knowledge-base-bj/deerflow-server!54
This commit is contained in:
董宏凯 committed 2026-05-25 12:35:09 +00:00
commit f7575e4ddb
6 files changed
+130 -32

No files matched your search

@@ -18,6 +18,7 @@
- **澄清协议统一**:Step 1 与 Step 2 leader 都借 `ask_clarification` + 终态 `clarification` 帧驱动同一套选项卡片组件;`allow_multiple` 默认 false(单选、点击即发送)。
- **leader 共识 = `status: []` 空数组**;澄清 = `status: "clarification"`(字符串);派活 = `status: [[name, task], ...]`。这三者**互斥不混用**。
- **后端 picks clamp 2–6,前端用户可手动加到 8**。
- **不写死任何具体模型名**:`intent / recommend / multi_agent` 三个 router 的 `model_name = str(payload.get("model") or "")`,空串会被下游 `lead_agent._resolve_model_name()` 自动回退到 `config.yaml` 的 `models[0]`(与 `/api/ai-writing` 行为一致)。内网部署只需配 `config.yaml`,前后端均无需改硬编码。
---
@@ -664,6 +665,14 @@ grep "special-timing" .runtime/logs/*.log # payload_built / first_upstream_l
`model` 写入 run 的 `context.model_name`;前端可按席位覆盖 special 调用的模型(见前端文档 §5.5 Persona 抽屉)。
**绝不在路由里硬编码具体模型名**。三个 router 一律 `str(payload.get("model") or "")`:
- 前端不传 / 传空串 → 下游 `lead_agent._resolve_model_name("")` 命中 falsy 分支,返回 `app_config.models[0].name`(`config.yaml` 配置顺序里的第一项)
- 前端传了一个不在 `config.yaml` 里的 model 名 → 同样回退到首项,并 `logger.warning("Model 'x' not found ... fallback to ...")`
- 前端传了合法 model 名 → 直接使用
这是 `/api/ai-writing` 与圆桌三路由的统一行为,让本地 / 内网部署只需要改 `config.yaml`,不需要触前端常量或路由代码。详见 `deerflow/agents/lead_agent/agent.py:_resolve_model_name`。
### 11.3 新增席位
1. `.deer-flow/agents/roundtable-xxx/` 增加 `config.yaml` + `SOUL.md`
+13 -5
View File
@@ -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 全局模型选择器(顶栏)
- **位置**:顶栏右侧「新建任务」按钮左边,`<select>` 包在带 `Cpu` 图标的标签里
- **state**:`const [selectedModel, setSelectedModel] = useState<string>("")`
- **初始化**:`useEffect(() => ...)` 在 `availableModels` 首次返回后把 `selectedModel` 设为 `availableModels[0].name`;后端配置变化导致当前选中 model 不在列表里时也回退到首项
- **传参规则**:所有 5 个 API 调用点(`initIntent` / `streamIntent` / `streamRecommend` / `initMultiAgent` / `streamMultiAgent` leader / `streamMultiAgent` special)都传 `selectedModel || undefined`;special 还会先看 `agentConfigs[seat]?.model` per-seat 覆盖
- **禁用态**:`availableModels.length === 0` 或 `modelsLoading` → `disabled`,placeholder 显示「加载中…」或「未配置模型」
- **可改换为 Radix Select**:当前用原生 `<select>` 与 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 |
@@ -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}>
@@ -94,8 +94,9 @@ _INTENT_READY_PATTERN = re.compile(
async def init_intent(request: Request, payload: dict = Body(...)) -> dict[str, Any]:
"""创建任务理解会话用的空 thread。
请求体:``{ "model": "deepseek-chat" }``(可选,当前 init 未把 model 写入 thread,
实际模型在 ``/stream`` 的每次 run 里指定)。
请求体:``{ "model": "<可选>" }``(可选,当前 init 未把 model 写入 thread,
实际模型在 ``/stream`` 的每次 run 里指定;缺省由 lead_agent 回退到
``config.yaml`` 的 ``models[0]``,前端通常传当前下拉框选中的 model 名)。
响应::
@@ -138,7 +139,10 @@ async def stream_intent(request: Request, payload: dict = Body(...)) -> Streamin
"""
thread_id = str(payload.get("thread_id") or "").strip()
message = str(payload.get("message") or "").strip()
model_name = str(payload.get("model") or "deepseek-chat")
# 不写死任何默认模型名:空串会让 lead_agent._resolve_model_name() 回退到
# config.yaml 的 models[0],与 /api/ai-writing 等其它路由一致,内网部署只配
# 自己的本地模型即可工作。
model_name = str(payload.get("model") or "")
if not thread_id:
raise HTTPException(status_code=400, detail="thread_id is required")
if not message:
@@ -303,7 +303,7 @@ async def init_multi_agent(request: Request, payload: dict = Body(...)) -> Strea
{
"agent_names": ["roundtable-intelligence", ...], // 用户选的席位 id
"main_agent_name": "roundtable-coordinator-xxx", // 当次总控 id,前端生成
"model": "deepseek-chat" // 可选,写入 coordinator agent
"model": "<可选>" // 可选,写入 coordinator agent;不传由 lead_agent 回退到 config.yaml models[0]
}
SSE 事件(每条 ``data: {...}\\n\\n``)::
@@ -843,8 +843,8 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) ->
"d_agent_thread_id": { "<agent_id>": "<thread_id>", ... }, // init_done.thread_ids
"agent_name": "<本次运行的 agent_id>",
"new_message": "<用户消息或派活 task 文本>",
"model": "deepseek-chat",
"skill_stop_names": [] // 可选,leader 一般留空,由网关注入 agent_orchestration
"model": "<可选>", // 不传由 lead_agent 回退到 config.yaml models[0]
"skill_stop_names": [] // 可选,leader 一般留空,由网关注入 agent_orchestration
}
响应:上游 SSE 行透传 + 末尾一条 ``data:`` JSON(leader 为 dict,special 同上)。
@@ -855,7 +855,9 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) ->
agent_name: str = str(payload.get("agent_name") or "").lower()
new_message: str = str(payload.get("new_message") or "")
skill_stop_names: list[str] = payload.get("skill_stop_names") or []
model_name: str = str(payload.get("model") or "deepseek-chat")
# 不写死任何默认模型;空串会让 lead_agent._resolve_model_name() 回退到
# config.yaml 的 models[0],与 /api/ai-writing 等其它路由一致。
model_name: str = str(payload.get("model") or "")
if not agent_name or agent_name not in agent_threads:
raise HTTPException(
@@ -151,7 +151,7 @@ async def stream_recommend(request: Request, payload: dict = Body(...)) -> Strea
{
"intent": {"objective": "...", "constraints": [...], "assumptions": [...]},
"candidates": [{"agent_id": "...", "name": "...", "description": "..."}, ...],
"model": "deepseek-chat" # optional
"model": "<可选>" # optional;不传由 lead_agent 回退到 config.yaml models[0]
}
Stream: transparent upstream LangGraph frames, followed by one status
@@ -169,7 +169,10 @@ async def stream_recommend(request: Request, payload: dict = Body(...)) -> Strea
"""
intent = payload.get("intent") or {}
candidates = payload.get("candidates") or []
model_name = str(payload.get("model") or "deepseek-chat")
# 不写死任何默认模型;空串会让 lead_agent._resolve_model_name() 回退到
# config.yaml 的 models[0]。内网部署可能没有 deepseek-chat,前端传当前选中的
# model 名即可,缺失也不会硬错。
model_name = str(payload.get("model") or "")
if not isinstance(intent, dict) or not str(intent.get("objective") or "").strip():
raise HTTPException(status_code=400, detail="intent.objective is required")