feat: 添加swarms团队编排功能并优化agent委派系统

- 引入AgentTeamOrchestrator支持多agent协同任务执行
- 增加第三方swarms库依赖并配置git协议替换以改善包管理
- 扩展DelegationManager支持团队任务调度和进度跟踪
- 实现中文bigram分词算法提升中文任务检索准确性
- 调整A2AClient和DelegationManager超时时间从30秒增至600秒
- 优化AgentRunResult状态判断逻辑增加有意义摘要检测
- 修改Dockerfile配置npm仓库镜像地址和git协议映射
- 更新CLI命令行接口支持网关端口配置传递
- 调整提供者超时配置机制增强请求稳定性
- 移除过时的support_group字段简化agent描述符结构
- 增强错误处理和进度事件报告机制改进用户体验
This commit is contained in:
2026-04-14 14:34:23 +08:00
parent fee9007da6
commit cdfc222c9f
85 changed files with 5443 additions and 1392 deletions

View File

@ -34,8 +34,84 @@ import {
} from '@/components/ui/sheet';
import { ScrollArea } from '@/components/ui/scroll-area';
import { buildOfficeView, isOfficeTaskTerminal } from '@/lib/office';
import { appEventKindLabel } from '@/lib/i18n/common';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
import { useChatStore } from '@/lib/store';
function traceMetadataLabels(locale: 'zh-CN' | 'en-US'): Record<string, string> {
return {
stage_label: pickAppText(locale, '阶段', 'Stage'),
source: pickAppText(locale, '来源', 'Source'),
phase: 'Phase',
step: 'Step',
selection_mode: pickAppText(locale, '选人方式', 'Selection mode'),
selected_mode: pickAppText(locale, '选中模式', 'Selected mode'),
execution_mode: pickAppText(locale, '执行模式', 'Execution mode'),
selected_targets: pickAppText(locale, '成员', 'Members'),
selected_count: pickAppText(locale, '成员数', 'Member count'),
requested_targets: pickAppText(locale, '请求成员', 'Requested targets'),
planned_targets: pickAppText(locale, '计划成员', 'Planned targets'),
matched_procedure_id: pickAppText(locale, '命中 Procedure', 'Matched procedure'),
candidate_procedure_id: pickAppText(locale, '候选 Procedure', 'Candidate procedure'),
announcement_path: pickAppText(locale, '回流路径', 'Announcement path'),
announcement_sender_id: pickAppText(locale, '回流 Sender', 'Announcement sender'),
announcement_category: pickAppText(locale, '回流类别', 'Announcement category'),
external_fallback_reason: pickAppText(locale, '外部回退原因', 'External fallback reason'),
failure_type: pickAppText(locale, '失败分类', 'Failure type'),
failure_reason: pickAppText(locale, '失败原因', 'Failure reason'),
error: pickAppText(locale, '错误', 'Error'),
origin_channel: pickAppText(locale, '来源 Channel', 'Origin channel'),
origin_chat_id: pickAppText(locale, '来源 Chat', 'Origin chat'),
};
}
function formatTraceValue(value: unknown): string | null {
if (value === null || value === undefined) return null;
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed || null;
}
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
if (Array.isArray(value)) {
const parts = value
.map((item) => formatTraceValue(item))
.filter((item): item is string => Boolean(item));
return parts.length > 0 ? parts.join(', ') : null;
}
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function traceMetadataEntries(
metadata: Record<string, unknown> | null | undefined,
labels: Record<string, string>
): Array<{ key: string; label: string; value: string }> {
if (!metadata) return [];
const entries: Array<{ key: string; label: string; value: string }> = [];
const used = new Set<string>();
for (const [key, label] of Object.entries(labels)) {
const value = formatTraceValue(metadata[key]);
if (!value) continue;
used.add(key);
entries.push({ key, label, value });
}
for (const [key, rawValue] of Object.entries(metadata)) {
if (used.has(key)) continue;
const value = formatTraceValue(rawValue);
if (!value) continue;
entries.push({ key, label: key, value });
}
return entries;
}
function PixelPanel({
title,
subtitle,
@ -87,6 +163,7 @@ function BoardPanel({
}
export default function OfficeDetailPage() {
const { locale } = useAppI18n();
const params = useParams<{ taskId: string }>();
const taskId = decodeURIComponent(Array.isArray(params?.taskId) ? params.taskId[0] : params?.taskId ?? '');
@ -96,9 +173,10 @@ export default function OfficeDetailPage() {
const processArtifacts = useChatStore((state) => state.processArtifacts);
const office = React.useMemo(
() => buildOfficeView(taskId, { sessions, processRuns, processEvents, processArtifacts }),
[processArtifacts, processEvents, processRuns, sessions, taskId]
() => buildOfficeView(taskId, { sessions, processRuns, processEvents, processArtifacts }, locale),
[locale, processArtifacts, processEvents, processRuns, sessions, taskId]
);
const metadataLabels = React.useMemo(() => traceMetadataLabels(locale), [locale]);
const [selectedRunId, setSelectedRunId] = React.useState<string | null>(null);
const [detailOpen, setDetailOpen] = React.useState(false);
@ -112,12 +190,20 @@ export default function OfficeDetailPage() {
() => office?.tasks.find((task) => task.runId === selectedRunId) ?? office?.tasks[0] ?? null,
[office?.tasks, selectedRunId]
);
const selectedRun = React.useMemo(
() => processRuns.find((run) => run.run_id === selectedTask?.runId) ?? null,
[processRuns, selectedTask?.runId]
);
const selectedRunMetadata = React.useMemo(
() => traceMetadataEntries(selectedRun?.metadata, metadataLabels),
[metadataLabels, selectedRun?.metadata]
);
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),
.slice(0, 16),
[processEvents, selectedTask?.runId]
);
@ -139,14 +225,18 @@ export default function OfficeDetailPage() {
<Button asChild variant="outline" className="w-fit">
<Link href="/office">
<ArrowLeft className="mr-2 h-4 w-4" />
Office
{pickAppText(locale, '返回 Office 列表', 'Back to office list')}
</Link>
</Button>
<Card className="border-dashed">
<CardContent className="py-16 text-center">
<h1 className="text-2xl font-semibold"></h1>
<h1 className="text-2xl font-semibold">{pickAppText(locale, '任务不存在', 'Task not found')}</h1>
<p className="mt-2 text-sm text-muted-foreground">
store task Office
{pickAppText(
locale,
'当前 store 中没有这个 task 的运行数据。先从对话页发起任务,或者回到 Office 列表查看当前可用任务。',
'The current store does not contain runtime data for this task yet. Start it from chat first, or return to the office list to inspect available tasks.'
)}
</p>
</CardContent>
</Card>
@ -164,13 +254,13 @@ export default function OfficeDetailPage() {
<Button asChild variant="outline" size="sm">
<Link href="/office">
<ArrowLeft className="mr-2 h-4 w-4" />
Office
{pickAppText(locale, '返回 Office', 'Back to office')}
</Link>
</Button>
<Button asChild variant="ghost" size="sm">
<Link href="/">
<MessageSquare className="mr-2 h-4 w-4" />
{pickAppText(locale, '回到对话', 'Back to chat')}
</Link>
</Button>
</div>
@ -186,18 +276,18 @@ export default function OfficeDetailPage() {
<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>
<span>{pickAppText(locale, '负责人', 'Lead')}: {office.rootActorName}</span>
<span>{pickAppText(locale, '会话', 'Session')}: {office.sourceSessionLabel}</span>
<span>{pickAppText(locale, '开始', 'Started')}: {formatOfficeTime(office.createdAt, locale)}</span>
<span>{pickAppText(locale, '耗时', 'Duration')}: {formatOfficeDuration(office.durationMs, locale)}</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)} />
<MetricTile label={pickAppText(locale, '运行实例', 'Runs')} value={String(office.stats.totalRuns)} />
<MetricTile label={pickAppText(locale, '参与成员', 'Members')} value={String(office.stats.memberCount)} />
<MetricTile label={pickAppText(locale, '产物数量', 'Artifacts')} value={String(office.stats.artifactCount)} />
<MetricTile label={pickAppText(locale, '告警数量', 'Alerts')} value={String(office.alerts.length)} />
</div>
</div>
</div>
@ -213,12 +303,12 @@ export default function OfficeDetailPage() {
<div className="mx-auto grid max-w-[1280px] gap-5 xl:grid-cols-[390px_minmax(0,1fr)_390px]">
<PixelPanel
title="昨日小记"
subtitle="用任务摘要、告警和最近更新来替代原版 memo 区。"
title={pickAppText(locale, '昨日小记', 'Yesterday notes')}
subtitle={pickAppText(locale, '用任务摘要、告警和最近更新来替代原版 memo 区。', 'Use task summaries, alerts, and recent updates instead of the original memo area.')}
>
<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 看现场。'}
{selectedTask?.summary || pickAppText(locale, '当前选中任务没有摘要,先从右侧任务看板切一个具体 run 看现场。', 'The selected task has no summary yet. Pick a specific run from the board on the right to inspect the floor.')}
</div>
{office.alerts.slice(0, 2).map((alert) => (
<button
@ -236,13 +326,13 @@ export default function OfficeDetailPage() {
</PixelPanel>
<PixelPanel
title="任务控制台"
subtitle="保留原版中间控制栏的位置,但改成适配 task runtime 的真实数据。"
title={pickAppText(locale, '任务控制台', 'Task console')}
subtitle={pickAppText(locale, '保留原版中间控制栏的位置,但改成适配 task runtime 的真实数据。', 'Keep the original center console position, but back it with real task runtime data.')}
>
<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)} />
<MiniMetric label={pickAppText(locale, '当前阶段', 'Current stage')} value={office.progress.stageLabel ?? office.currentStageLabel ?? '-'} />
<MiniMetric label={pickAppText(locale, '活跃实例', 'Active runs')} value={String(office.stats.activeRuns)} />
</div>
<div className="space-y-2">
@ -260,10 +350,10 @@ export default function OfficeDetailPage() {
{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="font-mono text-[11px] uppercase tracking-[0.14em] text-slate-400">{pickAppText(locale, '当前聚焦', 'Current focus')}</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 ?? '无阶段标签'}
{selectedTask.actorName} · {selectedTask.stageLabel ?? pickAppText(locale, '无阶段标签', 'No stage label')}
</div>
</div>
) : null}
@ -273,7 +363,7 @@ export default function OfficeDetailPage() {
onClick={() => setDetailOpen(true)}
className="w-full rounded-none border-2 border-[#2f3b16] bg-[#78a340] text-[#f3ffe6] hover:bg-[#8fbe4a]"
>
{pickAppText(locale, '打开详情', 'Open details')}
<PanelRightOpen className="ml-2 h-4 w-4" />
</Button>
<Button
@ -282,7 +372,7 @@ export default function OfficeDetailPage() {
className="w-full rounded-none border-2 border-[#30364d] bg-[#171b29] text-slate-100 hover:bg-[#21283a]"
>
<Link href="/">
{pickAppText(locale, '回到对话', 'Back to chat')}
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
@ -290,15 +380,15 @@ export default function OfficeDetailPage() {
{isOfficeTaskTerminal(office.status) ? (
<div className="rounded-none border-2 border-[#365443] bg-[#12221d] px-3 py-3 text-sm text-emerald-200">
{pickAppText(locale, '任务已结束,办公室已解散,但现场记录仍可回看。', 'The task has ended and the office has dissolved, but the floor record is still available for review.')}
</div>
) : null}
</div>
</PixelPanel>
<PixelPanel
title="办公人员名单"
subtitle="原版 visitor 区的替代,这里展示当前参与 task 的 agent 成员。"
title={pickAppText(locale, '办公人员名单', 'Roster')}
subtitle={pickAppText(locale, '原版 visitor 区的替代,这里展示当前参与 task 的 agent 成员。', 'Replacement for the original visitor area, showing the agents currently participating in this task.')}
>
<div className="space-y-2">
{office.members.map((member) => (
@ -322,8 +412,8 @@ export default function OfficeDetailPage() {
<div className="mx-auto grid max-w-[1280px] gap-5 xl:grid-cols-[1.08fr_0.92fr]">
<BoardPanel
icon={ListTree}
title="任务看板"
description="当前 task 下所有 run 的结构化列表。"
title={pickAppText(locale, '任务看板', 'Task board')}
description={pickAppText(locale, '当前 task 下所有 run 的结构化列表。', 'Structured list of all runs under this task.')}
>
<div className="space-y-3">
{office.tasks.map((task) => (
@ -349,8 +439,8 @@ export default function OfficeDetailPage() {
</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>
<span>{formatOfficeTime(task.updatedAt, locale)}</span>
<span>{pickAppText(locale, `${task.artifactCount} 个产物`, `${task.artifactCount} artifacts`)}</span>
</div>
</div>
<OfficeStatusBadge status={task.status} />
@ -363,13 +453,13 @@ export default function OfficeDetailPage() {
<div className="space-y-5">
<BoardPanel
icon={Boxes}
title="分工关系"
description="主 Agent 到子 Agent 的委派关系。"
title={pickAppText(locale, '分工关系', 'Assignments')}
description={pickAppText(locale, '主 Agent 到子 Agent 的委派关系。', 'Delegation links from the lead agent to sub-agents.')}
>
<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">
{pickAppText(locale, '当前没有可见的子任务分工。', 'No visible subtask assignments yet.')}
</div>
) : (
office.assignments.map((assignment) => (
@ -389,13 +479,13 @@ export default function OfficeDetailPage() {
<BoardPanel
icon={Siren}
title="现场告警"
description="优先展示失败、阻塞和较高风险的任务信号。"
title={pickAppText(locale, '现场告警', 'Live alerts')}
description={pickAppText(locale, '优先展示失败、阻塞和较高风险的任务信号。', 'Prioritize failed, blocked, and higher-risk task signals.')}
>
<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">
{pickAppText(locale, '当前没有高优先级告警。', 'There are no high-priority alerts right now.')}
</div>
) : (
office.alerts.map((alert) => (
@ -420,17 +510,17 @@ export default function OfficeDetailPage() {
<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>
<SheetTitle>{selectedTask?.title ?? pickAppText(locale, '任务详情', 'Task details')}</SheetTitle>
<SheetDescription>
{selectedTask
? `${selectedTask.actorName} · ${selectedTask.stageLabel ?? '无阶段标签'}`
: '当前没有选中的任务实例。'}
? `${selectedTask.actorName} · ${selectedTask.stageLabel ?? pickAppText(locale, '无阶段标签', 'No stage label')}`
: pickAppText(locale, '当前没有选中的任务实例。', 'No task run is currently selected.')}
</SheetDescription>
</SheetHeader>
{!selectedTask ? (
<div className="mt-6 rounded-xl border border-dashed border-border/60 px-4 py-6 text-sm text-muted-foreground">
{pickAppText(locale, '当前没有可展示的任务详情。', 'There are no task details to display right now.')}
</div>
) : (
<ScrollArea className="mt-6 h-[calc(100vh-8.5rem)] pr-3">
@ -445,18 +535,31 @@ export default function OfficeDetailPage() {
</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>
<span className="text-muted-foreground">{pickAppText(locale, '开始时间', 'Started')}</span>
<span>{formatOfficeTime(selectedTask.startedAt, locale)}</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-muted-foreground"></span>
<span>{formatOfficeTime(selectedTask.updatedAt)}</span>
<span className="text-muted-foreground">{pickAppText(locale, '最近更新', 'Last update')}</span>
<span>{formatOfficeTime(selectedTask.updatedAt, locale)}</span>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-muted-foreground"></span>
<span className="text-muted-foreground">{pickAppText(locale, '阶段', 'Stage')}</span>
<span>{selectedTask.stageLabel ?? '-'}</span>
</div>
</div>
{selectedRunMetadata.length > 0 ? (
<div className="mt-3 rounded-lg border border-border/60 bg-muted/20 px-3 py-3">
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{pickAppText(locale, '链路上下文', 'Trace context')}</div>
<div className="mt-2 space-y-1.5">
{selectedRunMetadata.map((item) => (
<div key={item.key} className="grid gap-1 text-xs sm:grid-cols-[110px_minmax(0,1fr)]">
<span className="text-muted-foreground">{item.label}</span>
<span className="break-words font-mono text-foreground/90">{item.value}</span>
</div>
))}
</div>
</div>
) : null}
{selectedTask.summary ? (
<div className="mt-3 rounded-lg bg-muted/40 px-3 py-3 text-sm text-muted-foreground">
{selectedTask.summary}
@ -466,16 +569,16 @@ export default function OfficeDetailPage() {
<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="border-b border-border/60 px-4 py-3 text-sm font-medium">{pickAppText(locale, '产物', 'Artifacts')}</div>
<div className="space-y-2 p-4">
{selectedArtifacts.length === 0 ? (
<div className="text-sm text-muted-foreground"></div>
<div className="text-sm text-muted-foreground">{pickAppText(locale, '当前没有产物。', 'There are no artifacts for this task.')}</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)}
{artifact.artifact_type} · {formatOfficeTime(artifact.created_at, locale)}
</div>
</div>
))
@ -484,22 +587,40 @@ export default function OfficeDetailPage() {
</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="border-b border-border/60 px-4 py-3 text-sm font-medium">{pickAppText(locale, '最近事件', 'Recent events')}</div>
<div className="space-y-2 p-4">
{selectedEvents.length === 0 ? (
<div className="text-sm text-muted-foreground"></div>
<div className="text-sm text-muted-foreground">{pickAppText(locale, '当前没有事件。', 'There are no events for this task.')}</div>
) : (
selectedEvents.map((event) => (
selectedEvents.map((event) => {
const metadataEntries = traceMetadataEntries(event.metadata, metadataLabels);
return (
<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 className="text-xs uppercase tracking-wide text-muted-foreground">{appEventKindLabel(event.kind, locale)}</div>
<div className="text-xs text-muted-foreground">{formatOfficeTime(event.created_at, locale)}</div>
</div>
{event.status ? (
<div className="mt-2 text-xs text-muted-foreground">{pickAppText(locale, '状态', 'Status')}: {event.status}</div>
) : null}
<div className="mt-2 text-sm text-foreground/90">
{event.text || '结构化更新'}
{event.text || pickAppText(locale, '结构化更新', 'Structured update')}
</div>
{metadataEntries.length > 0 ? (
<div className="mt-3 rounded-md bg-muted/20 px-3 py-2">
<div className="mb-2 text-[11px] uppercase tracking-wide text-muted-foreground">{pickAppText(locale, '事件上下文', 'Event context')}</div>
<div className="space-y-1.5">
{metadataEntries.map((item) => (
<div key={`${event.event_id}:${item.key}`} className="grid gap-1 text-xs sm:grid-cols-[110px_minmax(0,1fr)]">
<span className="text-muted-foreground">{item.label}</span>
<span className="break-words font-mono text-foreground/90">{item.value}</span>
</div>
))}
</div>
</div>
) : null}
</div>
))
)})
)}
</div>
</div>