feat(agent): 添加对持久化子智能体的支持并增强委派管理

添加了持久化子智能体的完整生命周期管理功能,包括创建、更新、删除和查询API接口。
新增了子智能体的JSON-RPC通信协议支持,实现了远程调用和任务管理功能。

同时增强了委派管理器的功能:
- 添加了对本地委派、插件委派和本地回退的开关控制
- 实现了持久化子智能体任务的自动检测和本地执行保护
- 增加了对不同委派类型的权限验证机制

修改了智能体注册表以支持插件智能体的条件性包含,并更新了工具注册逻辑以支持可选工具。

BREAKING CHANGE: 委派管理器的构造函数签名已更改,添加了新的控制参数。
```
This commit is contained in:
2026-03-27 10:15:35 +08:00
parent bad1e16ab4
commit 29dfd14aa6
133 changed files with 11656 additions and 220 deletions

View File

@ -0,0 +1,532 @@
'use client';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import React from 'react';
import {
ArrowLeft,
ArrowRight,
Boxes,
FolderOutput,
ListTree,
MessageSquare,
PanelRightOpen,
Siren,
Users,
} from 'lucide-react';
import {
OfficeStatusBadge,
formatOfficeDuration,
formatOfficeTime,
progressPercent,
} from '@/components/office/OfficeShared';
import { OfficePhaserCanvas } from '@/components/office/OfficePhaserCanvas';
import { TaskManagementTabs } from '@/components/task-management/TaskManagementTabs';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { ScrollArea } from '@/components/ui/scroll-area';
import { buildOfficeView, isOfficeTaskTerminal } from '@/lib/office';
import { useChatStore } from '@/lib/store';
function PixelPanel({
title,
subtitle,
children,
icon: Icon,
}: {
title: string;
subtitle?: string;
children: React.ReactNode;
icon?: React.ComponentType<{ className?: string }>;
}) {
return (
<div className="rounded-none border-4 border-[#0e1119] bg-[#141722] p-4 text-slate-100 shadow-[0_0_0_2px_#1a1b2f_inset]">
<div className="flex items-center gap-2 font-mono text-sm font-bold uppercase tracking-[0.18em] text-[#fef3c7]">
{Icon ? <Icon className="h-4 w-4" /> : null}
{title}
</div>
{subtitle ? (
<div className="mt-2 text-xs text-slate-400">{subtitle}</div>
) : null}
<div className="mt-4">{children}</div>
</div>
);
}
function BoardPanel({
icon: Icon,
title,
description,
children,
}: {
icon: React.ComponentType<{ className?: string }>;
title: string;
description?: string;
children: React.ReactNode;
}) {
return (
<Card className="rounded-none border-4 border-[#0e1119] bg-[#141722] text-slate-100 shadow-[0_0_0_2px_#1a1b2f_inset]">
<CardHeader className="border-b border-[#262a3d] pb-3">
<CardTitle className="flex items-center gap-2 text-base text-[#fef3c7]">
<Icon className="h-4 w-4" />
{title}
</CardTitle>
{description ? <CardDescription className="text-slate-400">{description}</CardDescription> : null}
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
);
}
export default function OfficeDetailPage() {
const params = useParams<{ taskId: string }>();
const taskId = decodeURIComponent(Array.isArray(params?.taskId) ? params.taskId[0] : params?.taskId ?? '');
const sessions = useChatStore((state) => state.sessions);
const processRuns = useChatStore((state) => state.processRuns);
const processEvents = useChatStore((state) => state.processEvents);
const processArtifacts = useChatStore((state) => state.processArtifacts);
const office = React.useMemo(
() => buildOfficeView(taskId, { sessions, processRuns, processEvents, processArtifacts }),
[processArtifacts, processEvents, processRuns, sessions, taskId]
);
const [selectedRunId, setSelectedRunId] = React.useState<string | null>(null);
const [detailOpen, setDetailOpen] = React.useState(false);
React.useEffect(() => {
setSelectedRunId(office?.rootRunId ?? null);
setDetailOpen(false);
}, [office?.rootRunId]);
const selectedTask = React.useMemo(
() => office?.tasks.find((task) => task.runId === selectedRunId) ?? office?.tasks[0] ?? null,
[office?.tasks, selectedRunId]
);
const selectedEvents = React.useMemo(
() => processEvents
.filter((event) => event.run_id === selectedTask?.runId)
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.slice(0, 8),
[processEvents, selectedTask?.runId]
);
const selectedArtifacts = React.useMemo(
() => processArtifacts
.filter((artifact) => artifact.run_id === selectedTask?.runId)
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()),
[processArtifacts, selectedTask?.runId]
);
const openRunDetail = React.useCallback((runId: string) => {
setSelectedRunId(runId);
setDetailOpen(true);
}, []);
if (!office) {
return (
<div className="mx-auto flex max-w-4xl flex-col gap-4 p-6">
<Button asChild variant="outline" className="w-fit">
<Link href="/office">
<ArrowLeft className="mr-2 h-4 w-4" />
Office
</Link>
</Button>
<Card className="border-dashed">
<CardContent className="py-16 text-center">
<h1 className="text-2xl font-semibold"></h1>
<p className="mt-2 text-sm text-muted-foreground">
store task Office
</p>
</CardContent>
</Card>
</div>
);
}
const progressValue = progressPercent(office.progress.value, office.progress.max);
return (
<div className="mx-auto max-w-[1720px] space-y-6 p-6">
<TaskManagementTabs />
<div className="flex flex-wrap items-center gap-3">
<Button asChild variant="outline" size="sm">
<Link href="/office">
<ArrowLeft className="mr-2 h-4 w-4" />
Office
</Link>
</Button>
<Button asChild variant="ghost" size="sm">
<Link href="/">
<MessageSquare className="mr-2 h-4 w-4" />
</Link>
</Button>
</div>
<div className="space-y-5">
<div className="mx-auto max-w-[1280px] rounded-none border-4 border-[#0e1119] bg-[#141522] p-4 shadow-[0_0_0_2px_#241d36_inset]">
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-3">
<h1 className="truncate font-mono text-3xl font-bold uppercase tracking-[0.18em] text-[#fef3c7]">
{office.title}
</h1>
<OfficeStatusBadge status={office.status} className="bg-black/20" />
</div>
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 font-mono text-xs uppercase tracking-[0.14em] text-slate-400">
<span>Lead: {office.rootActorName}</span>
<span>Session: {office.sourceSessionLabel}</span>
<span>Started: {formatOfficeTime(office.createdAt)}</span>
<span>Duration: {formatOfficeDuration(office.durationMs)}</span>
</div>
</div>
<div className="grid min-w-[320px] gap-3 sm:grid-cols-2 lg:w-[430px]">
<MetricTile label="运行实例" value={String(office.stats.totalRuns)} />
<MetricTile label="参与成员" value={String(office.stats.memberCount)} />
<MetricTile label="产物数量" value={String(office.stats.artifactCount)} />
<MetricTile label="告警数量" value={String(office.alerts.length)} />
</div>
</div>
</div>
<div className="mx-auto max-w-[1280px]">
<OfficePhaserCanvas
office={office}
selectedRunId={selectedTask?.runId ?? null}
onRunSelect={openRunDetail}
showMetaBar={false}
/>
</div>
<div className="mx-auto grid max-w-[1280px] gap-5 xl:grid-cols-[390px_minmax(0,1fr)_390px]">
<PixelPanel
title="昨日小记"
subtitle="用任务摘要、告警和最近更新来替代原版 memo 区。"
>
<div className="space-y-3 text-sm leading-6 text-slate-300">
<div className="rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-3 py-3">
{selectedTask?.summary || '当前选中任务没有摘要,先从右侧任务看板切一个具体 run 看现场。'}
</div>
{office.alerts.slice(0, 2).map((alert) => (
<button
key={alert.id}
type="button"
disabled={!alert.runId}
onClick={() => alert.runId && openRunDetail(alert.runId)}
className="block w-full rounded-none border-2 border-[#40202a] bg-[#201118] px-3 py-3 text-left transition-colors enabled:hover:border-[#fb7185] disabled:cursor-default"
>
<div className="font-medium text-rose-200">{alert.title}</div>
{alert.description ? <div className="mt-1 text-xs text-slate-400">{alert.description}</div> : null}
</button>
))}
</div>
</PixelPanel>
<PixelPanel
title="任务控制台"
subtitle="保留原版中间控制栏的位置,但改成适配 task runtime 的真实数据。"
>
<div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2">
<MiniMetric label="当前阶段" value={office.progress.stageLabel ?? office.currentStageLabel ?? '-'} />
<MiniMetric label="活跃实例" value={String(office.stats.activeRuns)} />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between gap-3 font-mono text-[11px] uppercase tracking-[0.14em] text-slate-400">
<span>{office.progress.label}</span>
<span>{progressValue}%</span>
</div>
<div className="h-4 rounded-none border-2 border-[#263144] bg-[#0f1420] p-[2px]">
<div
className="h-full bg-[linear-gradient(90deg,#22d3ee,#fde047,#fb7185)] transition-all"
style={{ width: `${progressValue}%` }}
/>
</div>
</div>
{selectedTask ? (
<div className="rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-3 py-3">
<div className="font-mono text-[11px] uppercase tracking-[0.14em] text-slate-400"></div>
<div className="mt-2 text-sm font-semibold text-slate-100">{selectedTask.title}</div>
<div className="mt-1 text-xs text-slate-400">
{selectedTask.actorName} · {selectedTask.stageLabel ?? '无阶段标签'}
</div>
</div>
) : null}
<div className="grid gap-2 sm:grid-cols-2">
<Button
onClick={() => setDetailOpen(true)}
className="w-full rounded-none border-2 border-[#2f3b16] bg-[#78a340] text-[#f3ffe6] hover:bg-[#8fbe4a]"
>
<PanelRightOpen className="ml-2 h-4 w-4" />
</Button>
<Button
asChild
variant="outline"
className="w-full rounded-none border-2 border-[#30364d] bg-[#171b29] text-slate-100 hover:bg-[#21283a]"
>
<Link href="/">
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
{isOfficeTaskTerminal(office.status) ? (
<div className="rounded-none border-2 border-[#365443] bg-[#12221d] px-3 py-3 text-sm text-emerald-200">
</div>
) : null}
</div>
</PixelPanel>
<PixelPanel
title="办公人员名单"
subtitle="原版 visitor 区的替代,这里展示当前参与 task 的 agent 成员。"
>
<div className="space-y-2">
{office.members.map((member) => (
<button
key={member.memberId}
type="button"
onClick={() => openRunDetail(member.currentRunId)}
className="flex w-full items-center justify-between gap-3 rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-3 py-3 text-left transition-colors hover:border-[#64748b]"
>
<div className="min-w-0">
<div className="truncate font-medium text-slate-100">{member.actorName}</div>
<div className="truncate text-xs text-slate-400">{member.currentTitle}</div>
</div>
<OfficeStatusBadge status={member.status} />
</button>
))}
</div>
</PixelPanel>
</div>
<div className="mx-auto grid max-w-[1280px] gap-5 xl:grid-cols-[1.08fr_0.92fr]">
<BoardPanel
icon={ListTree}
title="任务看板"
description="当前 task 下所有 run 的结构化列表。"
>
<div className="space-y-3">
{office.tasks.map((task) => (
<button
key={task.runId}
type="button"
onClick={() => openRunDetail(task.runId)}
className={`w-full rounded-none border-2 px-4 py-3 text-left transition-colors ${
selectedTask?.runId === task.runId
? 'border-[#facc15] bg-[#201922]'
: 'border-[#2d3348] bg-[#0f1420] hover:border-[#64748b]'
}`}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="truncate font-medium text-slate-100">{task.title}</span>
{task.isRoot ? (
<span className="rounded-none border border-[#4a3c17] bg-[#3b2f12] px-2 py-0.5 font-mono text-[10px] text-[#fef3c7]">
ROOT
</span>
) : null}
</div>
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-slate-400">
<span>{task.actorName}</span>
<span>{formatOfficeTime(task.updatedAt)}</span>
<span>{task.artifactCount} </span>
</div>
</div>
<OfficeStatusBadge status={task.status} />
</div>
</button>
))}
</div>
</BoardPanel>
<div className="space-y-5">
<BoardPanel
icon={Boxes}
title="分工关系"
description="主 Agent 到子 Agent 的委派关系。"
>
<div className="space-y-2">
{office.assignments.length === 0 ? (
<div className="rounded-none border-2 border-dashed border-[#30364d] bg-[#0f1420] px-3 py-4 text-sm text-slate-400">
</div>
) : (
office.assignments.map((assignment) => (
<button
key={assignment.ownerRunId}
type="button"
onClick={() => openRunDetail(assignment.ownerRunId)}
className="w-full rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-3 py-3 text-left text-sm transition-colors hover:border-[#64748b]"
>
<div className="font-medium text-slate-100">{assignment.label}</div>
<div className="mt-1 text-slate-400">{assignment.assigneeActorNames.join(' / ')}</div>
</button>
))
)}
</div>
</BoardPanel>
<BoardPanel
icon={Siren}
title="现场告警"
description="优先展示失败、阻塞和较高风险的任务信号。"
>
<div className="space-y-2">
{office.alerts.length === 0 ? (
<div className="rounded-none border-2 border-dashed border-[#30364d] bg-[#0f1420] px-3 py-4 text-sm text-slate-400">
</div>
) : (
office.alerts.map((alert) => (
<button
key={alert.id}
type="button"
disabled={!alert.runId}
onClick={() => alert.runId && openRunDetail(alert.runId)}
className="w-full rounded-none border-2 border-[#40202a] bg-[#201118] px-3 py-3 text-left transition-colors enabled:hover:border-[#fb7185] disabled:cursor-default"
>
<div className="font-medium text-rose-200">{alert.title}</div>
{alert.description ? <div className="mt-1 text-sm text-slate-400">{alert.description}</div> : null}
</button>
))
)}
</div>
</BoardPanel>
</div>
</div>
</div>
<Sheet open={detailOpen} onOpenChange={setDetailOpen}>
<SheetContent side="right" className="w-full border-l border-border sm:max-w-3xl">
<SheetHeader className="pr-8">
<SheetTitle>{selectedTask?.title ?? '任务详情'}</SheetTitle>
<SheetDescription>
{selectedTask
? `${selectedTask.actorName} · ${selectedTask.stageLabel ?? '无阶段标签'}`
: '当前没有选中的任务实例。'}
</SheetDescription>
</SheetHeader>
{!selectedTask ? (
<div className="mt-6 rounded-xl border border-dashed border-border/60 px-4 py-6 text-sm text-muted-foreground">
</div>
) : (
<ScrollArea className="mt-6 h-[calc(100vh-8.5rem)] pr-3">
<div className="space-y-4 pb-6">
<div className="rounded-xl border border-border/60 px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate font-medium">{selectedTask.title}</div>
<div className="mt-1 text-xs text-muted-foreground">{selectedTask.actorName}</div>
</div>
<OfficeStatusBadge status={selectedTask.status} />
</div>
<div className="mt-3 grid gap-2 text-sm">
<div className="flex items-center justify-between gap-3">
<span className="text-muted-foreground"></span>
<span>{formatOfficeTime(selectedTask.startedAt)}</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-muted-foreground"></span>
<span>{formatOfficeTime(selectedTask.updatedAt)}</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-muted-foreground"></span>
<span>{selectedTask.stageLabel ?? '-'}</span>
</div>
</div>
{selectedTask.summary ? (
<div className="mt-3 rounded-lg bg-muted/40 px-3 py-3 text-sm text-muted-foreground">
{selectedTask.summary}
</div>
) : null}
</div>
<div className="grid gap-4 lg:grid-cols-[0.95fr_1.05fr]">
<div className="rounded-xl border border-border/60">
<div className="border-b border-border/60 px-4 py-3 text-sm font-medium"></div>
<div className="space-y-2 p-4">
{selectedArtifacts.length === 0 ? (
<div className="text-sm text-muted-foreground"></div>
) : (
selectedArtifacts.map((artifact) => (
<div key={artifact.artifact_id} className="rounded-lg border border-border/60 px-3 py-3">
<div className="font-medium">{artifact.title}</div>
<div className="mt-1 text-xs text-muted-foreground">
{artifact.artifact_type} · {formatOfficeTime(artifact.created_at)}
</div>
</div>
))
)}
</div>
</div>
<div className="rounded-xl border border-border/60">
<div className="border-b border-border/60 px-4 py-3 text-sm font-medium"></div>
<div className="space-y-2 p-4">
{selectedEvents.length === 0 ? (
<div className="text-sm text-muted-foreground"></div>
) : (
selectedEvents.map((event) => (
<div key={event.event_id} className="rounded-lg border border-border/60 px-3 py-3">
<div className="flex items-center justify-between gap-3">
<div className="text-xs uppercase tracking-wide text-muted-foreground">{event.kind}</div>
<div className="text-xs text-muted-foreground">{formatOfficeTime(event.created_at)}</div>
</div>
<div className="mt-2 text-sm text-foreground/90">
{event.text || '结构化更新'}
</div>
</div>
))
)}
</div>
</div>
</div>
</div>
</ScrollArea>
)}
</SheetContent>
</Sheet>
</div>
);
}
function MetricTile({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-4 py-4 text-slate-100">
<div className="font-mono text-[11px] uppercase tracking-[0.14em] text-slate-400">{label}</div>
<div className="mt-2 text-xl font-semibold">{value}</div>
</div>
);
}
function MiniMetric({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-none border-2 border-[#2d3348] bg-[#0f1420] px-3 py-3 text-slate-100">
<div className="font-mono text-[11px] uppercase tracking-[0.14em] text-slate-400">{label}</div>
<div className="mt-2 text-sm font-semibold">{value}</div>
</div>
);
}