Files
beaver_project/app-instance/frontend/app/(app)/office/page.tsx
steven_li 29dfd14aa6 ```
feat(agent): 添加对持久化子智能体的支持并增强委派管理

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

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

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

BREAKING CHANGE: 委派管理器的构造函数签名已更改,添加了新的控制参数。
```
2026-03-27 10:15:35 +08:00

269 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import Link from 'next/link';
import React from 'react';
import {
Activity,
ArrowRight,
Clock3,
FolderKanban,
Loader2,
Sparkles,
Users,
} from 'lucide-react';
import { OfficeStatusBadge, formatOfficeTime, progressPercent } from '@/components/office/OfficeShared';
import { TaskManagementTabs } from '@/components/task-management/TaskManagementTabs';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { buildOfficeTaskList, isOfficeTaskTerminal } from '@/lib/office';
import { useChatStore } from '@/lib/store';
function TaskCard({
taskId,
title,
sessionLabel,
rootActorName,
status,
updatedAt,
memberCount,
activeRuns,
artifactCount,
errorCount,
currentStageLabel,
progressLabel,
progressValue,
}: {
taskId: string;
title: string;
sessionLabel: string;
rootActorName: string;
status: Parameters<typeof OfficeStatusBadge>[0]['status'];
updatedAt: string;
memberCount: number;
activeRuns: number;
artifactCount: number;
errorCount: number;
currentStageLabel: string | null;
progressLabel: string;
progressValue: number;
}) {
return (
<Card className="border-border/80 transition-colors hover:border-primary/30">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<CardTitle className="truncate text-lg">{title}</CardTitle>
<CardDescription className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
<span>: {sessionLabel}</span>
<span> Agent: {rootActorName}</span>
<span> {formatOfficeTime(updatedAt)}</span>
</CardDescription>
</div>
<OfficeStatusBadge status={status} />
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 sm:grid-cols-4">
<Metric icon={Users} label="成员" value={String(memberCount)} />
<Metric icon={Activity} label="活跃" value={String(activeRuns)} />
<Metric icon={FolderKanban} label="产物" value={String(artifactCount)} />
<Metric icon={Sparkles} label="异常" value={String(errorCount)} />
</div>
<div className="space-y-2">
<div className="flex items-center justify-between gap-3 text-sm">
<span className="truncate text-muted-foreground">{progressLabel}</span>
{currentStageLabel ? <span className="truncate font-medium">{currentStageLabel}</span> : null}
</div>
<div className="h-2.5 overflow-hidden rounded-full bg-secondary">
<div
className="h-full rounded-full bg-primary transition-all"
style={{ width: `${progressValue}%` }}
/>
</div>
</div>
<div className="flex justify-end">
<Button asChild size="sm">
<Link href={`/office/${encodeURIComponent(taskId)}`}>
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>
</CardContent>
</Card>
);
}
function Metric({
icon: Icon,
label,
value,
}: {
icon: React.ComponentType<{ className?: string }>;
label: string;
value: string;
}) {
return (
<div className="rounded-xl border border-border/60 bg-muted/30 px-3 py-3">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Icon className="h-3.5 w-3.5" />
<span>{label}</span>
</div>
<div className="mt-2 text-lg font-semibold">{value}</div>
</div>
);
}
export default function OfficeListPage() {
const sessionId = useChatStore((state) => state.sessionId);
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 wsStatus = useChatStore((state) => state.wsStatus);
const tasks = React.useMemo(
() => buildOfficeTaskList({
sessionId,
sessions,
processRuns,
processEvents,
processArtifacts,
}),
[processArtifacts, processEvents, processRuns, sessionId, sessions]
);
const activeTasks = tasks.filter((task) => !isOfficeTaskTerminal(task.status));
const recentTasks = tasks.filter((task) => isOfficeTaskTerminal(task.status));
return (
<div className="mx-auto max-w-7xl space-y-6 p-6">
<TaskManagementTabs />
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<h1 className="text-3xl font-semibold tracking-tight">Office</h1>
<p className="mt-2 max-w-3xl text-sm text-muted-foreground">
Agent Agent
</p>
</div>
<Card className="min-w-[280px] border-border/70">
<CardContent className="flex items-center justify-between gap-4 p-4">
<div>
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 font-medium">{sessionId}</div>
</div>
<div className="text-right">
<div className="text-xs text-muted-foreground"></div>
<div className="mt-1 font-medium">{wsStatus === 'connected' ? '已连接' : wsStatus}</div>
</div>
</CardContent>
</Card>
</div>
{wsStatus === 'connecting' && tasks.length === 0 ? (
<div className="flex items-center gap-3 rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
...
</div>
) : null}
{tasks.length === 0 ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16 text-center">
<Clock3 className="h-10 w-10 text-muted-foreground/50" />
<h2 className="mt-4 text-xl font-semibold"></h2>
<p className="mt-2 max-w-xl text-sm text-muted-foreground">
Agent office
</p>
<Button asChild className="mt-6">
<Link href="/"></Link>
</Button>
</CardContent>
</Card>
) : (
<>
<section className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold"> Office</h2>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="text-sm text-muted-foreground">{activeTasks.length} </div>
</div>
{activeTasks.length === 0 ? (
<Card className="border-dashed">
<CardContent className="py-10 text-center text-sm text-muted-foreground">
</CardContent>
</Card>
) : (
<div className="grid gap-4 lg:grid-cols-2">
{activeTasks.map((task) => (
<TaskCard
key={task.taskId}
taskId={task.taskId}
title={task.title}
sessionLabel={task.sessionLabel}
rootActorName={task.rootActorName}
status={task.status}
updatedAt={task.updatedAt}
memberCount={task.memberCount}
activeRuns={task.activeRuns}
artifactCount={task.artifactCount}
errorCount={task.errorCount}
currentStageLabel={task.currentStageLabel}
progressLabel={task.progress.label}
progressValue={progressPercent(task.progress.value, task.progress.max)}
/>
))}
</div>
)}
</section>
<section className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="text-sm text-muted-foreground">{recentTasks.length} </div>
</div>
{recentTasks.length === 0 ? (
<Card className="border-dashed">
<CardContent className="py-10 text-center text-sm text-muted-foreground">
</CardContent>
</Card>
) : (
<div className="grid gap-4 lg:grid-cols-2">
{recentTasks.map((task) => (
<TaskCard
key={task.taskId}
taskId={task.taskId}
title={task.title}
sessionLabel={task.sessionLabel}
rootActorName={task.rootActorName}
status={task.status}
updatedAt={task.updatedAt}
memberCount={task.memberCount}
activeRuns={task.activeRuns}
artifactCount={task.artifactCount}
errorCount={task.errorCount}
currentStageLabel={task.currentStageLabel}
progressLabel={task.progress.label}
progressValue={progressPercent(task.progress.value, task.progress.max)}
/>
))}
</div>
)}
</section>
</>
)}
</div>
);
}