feat(desktop): T0.2 多智能体进 Studio —— coordinator 节点 + 专家卡片 + 派发可观测

把已通的多智能体后端(MULTI_AGENT.md)接上 UI,用户可在画布拖出协调者图。

- nodeCatalog: 新增 `coordinator`「多智能体协调」节点(indigo) + 新字段类型
  `agentList` + Specialist 接口({name,use,system,tools}),与后端 parseSpecialists 对齐。
- Inspector: AgentListField —— 可增删的子智能体卡片(名/用途/系统提示词/工具逗号分隔)。
- dsl 校验: agentList 需 ≥1 个有名字的专家、名字不重复。
- RunsView: 「工具调用」面板纳入专家派发(kind=agent),改名「工具/专家」,专家项
  用 Users 图标 + indigo「专家」徽标区分(此前只筛 kind=tool,漏掉多智能体派发)。
- 导出: agents 数组经 exportDsl 原样透传进 DSL → 后端 parseSpecialists 直接消费。

tsc + vitest(19) 绿;UI 同款结构 DSL 后端可跑(协调链路 live 已验)。
DEPTH_ROADMAP T0.2 ,T0 激活 2/2 完成。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-06-29 17:14:14 +08:00
parent 158fe094ab
commit cb6eec5614
5 changed files with 116 additions and 31 deletions
@@ -1,8 +1,52 @@
import type { Node } from "@xyflow/react";
import { NODE_KINDS } from "./nodeCatalog";
import { NODE_KINDS, type Specialist } from "./nodeCatalog";
const inputCls =
"mt-1 w-full rounded-md border border-line bg-ink-800 px-2 py-1 text-sm text-slate-200 focus:border-violet-500/60 focus:outline-none";
const cardCls =
"w-full rounded border border-line bg-ink-800 px-2 py-1 text-[12px] text-slate-200 placeholder:text-slate-600 focus:border-indigo-500/60 focus:outline-none";
// AgentListField:多智能体协调节点的「子智能体(专家)」配置 —— 可增删的专家卡片。
// 每张卡片对齐后端 parseSpecialists 的 {name, use, system, tools}。
function AgentListField({ value, onChange }: { value: Specialist[]; onChange: (next: Specialist[]) => void }) {
const list = Array.isArray(value) ? value : [];
const update = (i: number, patch: Partial<Specialist>) => onChange(list.map((s, idx) => (idx === i ? { ...s, ...patch } : s)));
const add = () => onChange([...list, { name: "", use: "", system: "", tools: [] }]);
const remove = (i: number) => onChange(list.filter((_, idx) => idx !== i));
return (
<div className="mt-1 space-y-2">
{list.map((s, i) => (
<div key={i} className="space-y-1.5 rounded-md border border-indigo-500/20 bg-ink-950/40 p-2">
<div className="flex items-center gap-2">
<input className={cardCls} placeholder="专家名(如 legal" value={s.name} onChange={(e) => update(i, { name: e.target.value })} />
<button onClick={() => remove(i)} className="shrink-0 text-[11px] text-rose-400 hover:underline">
</button>
</div>
<input className={cardCls} placeholder="用途/擅长(lead 据此判断何时派给它)" value={s.use} onChange={(e) => update(i, { use: e.target.value })} />
<textarea
className={`${cardCls} h-12 resize-none`}
placeholder="系统提示词(专家人设)"
value={s.system}
onChange={(e) => update(i, { system: e.target.value })}
/>
<input
className={cardCls}
placeholder="工具(逗号分隔,可空;如 wiki_search, kb_search"
value={(s.tools ?? []).join(", ")}
onChange={(e) => update(i, { tools: e.target.value.split(",").map((t) => t.trim()).filter(Boolean) })}
/>
</div>
))}
<button
onClick={add}
className="w-full rounded border border-dashed border-line py-1 text-[11px] text-slate-400 hover:border-indigo-500/50 hover:text-indigo-300"
>
+
</button>
</div>
);
}
// 右检查器:按选中节点的类型渲染配置表单;空选时显示图级提示(深色)。
export function Inspector({
@@ -49,7 +93,9 @@ export function Inspector({
<label key={f.key} className="text-xs text-slate-500">
{f.label}
{f.required && <span className="text-rose-400"> *</span>}
{f.type === "select" ? (
{f.type === "agentList" ? (
<AgentListField value={(v as Specialist[]) ?? []} onChange={(next) => setConfig(f.key, next)} />
) : f.type === "select" ? (
(() => {
const dyn = dynamicOptions?.[f.key];
const opts = dyn && dyn.length ? dyn : f.options ?? [];
@@ -1,7 +1,7 @@
// 节点类型目录 —— Studio 画布的"类型化节点"定义(决定面板、配色、检查器字段)。
// 与后端 DSL / Eino 图节点对齐:输入 / 检索(RAG) / Agent / 工具 / 记忆 / 分支 / 并行 / 汇聚 / 渲染 / 输出。
export type FieldType = "text" | "textarea" | "number" | "select" | "checkbox";
export type FieldType = "text" | "textarea" | "number" | "select" | "checkbox" | "agentList";
export interface Field {
key: string;
@@ -12,6 +12,14 @@ export interface Field {
required?: boolean;
}
// Specialist 是协调者节点里的一个子智能体(专家)配置,与后端 parseSpecialists 对齐。
export interface Specialist {
name: string; // 工具名(lead 据此调用)
use: string; // 用途/擅长,作为工具描述供 lead 判断何时派发
system: string; // 专家人设
tools: string[]; // 该专家可用的 MCP 工具名子集(空=纯对话专家)
}
export interface NodeKind {
kind: string;
label: string;
@@ -62,6 +70,18 @@ export const NODE_KINDS: Record<string, NodeKind> = {
],
defaults: { model: "占位 Pool", system: "", temperature: 0.7, autonomous: false },
},
coordinator: {
kind: "coordinator",
label: "多智能体协调",
accent: "border-l-indigo-500",
badge: "bg-indigo-100 text-indigo-700",
desc: "协调者自主把子任务派给专家(agent-as-tool)再综合",
fields: [
{ key: "system", label: "任务背景(可选)", type: "textarea", placeholder: "给协调者的任务背景…" },
{ key: "agents", label: "子智能体(专家)", type: "agentList", required: true },
],
defaults: { system: "", agents: [{ name: "", use: "", system: "", tools: [] }] },
},
tool: {
kind: "tool",
label: "工具",
@@ -159,6 +179,7 @@ export const NODE_ORDER = [
"input",
"retriever",
"agent",
"coordinator",
"tool",
"memory",
"branch",