- 引入AgentTeamOrchestrator支持多agent协同任务执行 - 增加第三方swarms库依赖并配置git协议替换以改善包管理 - 扩展DelegationManager支持团队任务调度和进度跟踪 - 实现中文bigram分词算法提升中文任务检索准确性 - 调整A2AClient和DelegationManager超时时间从30秒增至600秒 - 优化AgentRunResult状态判断逻辑增加有意义摘要检测 - 修改Dockerfile配置npm仓库镜像地址和git协议映射 - 更新CLI命令行接口支持网关端口配置传递 - 调整提供者超时配置机制增强请求稳定性 - 移除过时的support_group字段简化agent描述符结构 - 增强错误处理和进度事件报告机制改进用户体验
290 lines
9.6 KiB
TypeScript
290 lines
9.6 KiB
TypeScript
'use client';
|
|
|
|
import React from 'react';
|
|
import { Bot, Loader2, Paperclip, User } from 'lucide-react';
|
|
|
|
import type { ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun } from '@/types';
|
|
import { getAccessToken, getFileUrl } from '@/lib/api';
|
|
import { AgentTeamBlock } from '@/components/chat-workbench/AgentTeamBlock';
|
|
import { MarkdownContent } from '@/components/chat-workbench/MarkdownContent';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { pickAppText } from '@/lib/i18n/core';
|
|
import { useAppI18n } from '@/lib/i18n/provider';
|
|
|
|
function AuthImage({ src, alt, className }: { src: string; alt: string; className?: string }) {
|
|
const [blobUrl, setBlobUrl] = React.useState<string | null>(null);
|
|
|
|
React.useEffect(() => {
|
|
const token = getAccessToken();
|
|
const headers: Record<string, string> = {};
|
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
|
|
let revoke: string | null = null;
|
|
fetch(src, { headers })
|
|
.then((res) => res.blob())
|
|
.then((blob) => {
|
|
revoke = URL.createObjectURL(blob);
|
|
setBlobUrl(revoke);
|
|
})
|
|
.catch(() => {});
|
|
|
|
return () => {
|
|
if (revoke) URL.revokeObjectURL(revoke);
|
|
};
|
|
}, [src]);
|
|
|
|
if (!blobUrl) return <div className="w-32 h-32 bg-muted animate-pulse rounded" />;
|
|
return <img src={blobUrl} alt={alt} className={className} loading="lazy" decoding="async" />;
|
|
}
|
|
|
|
function MessageBubble({ message }: { message: ChatMessage }) {
|
|
const isUser = message.role === 'user';
|
|
const textContent = typeof message.content === 'string' ? message.content : String(message.content || '');
|
|
|
|
return (
|
|
<div className={`flex gap-3 ${isUser ? 'justify-end' : ''}`}>
|
|
{!isUser && (
|
|
<div className="w-7 h-7 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0 mt-0.5">
|
|
<Bot className="w-4 h-4 text-primary" />
|
|
</div>
|
|
)}
|
|
<div
|
|
className={`rounded-xl px-4 py-3 max-w-[88%] shadow-sm ${
|
|
isUser
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'bg-card border border-border/80'
|
|
}`}
|
|
>
|
|
{message.attachments && message.attachments.length > 0 && (
|
|
<div className="mb-2 space-y-2">
|
|
{message.attachments.map((att) => {
|
|
const fileUrl = getFileUrl(att.file_id);
|
|
if (att.content_type.startsWith('image/')) {
|
|
return (
|
|
<a key={att.file_id} href={fileUrl} target="_blank" rel="noopener noreferrer">
|
|
<AuthImage
|
|
src={fileUrl}
|
|
alt={att.name}
|
|
className="max-w-xs max-h-60 rounded border border-border/50 cursor-pointer hover:opacity-90"
|
|
/>
|
|
</a>
|
|
);
|
|
}
|
|
return (
|
|
<a
|
|
key={att.file_id}
|
|
href={fileUrl}
|
|
download={att.name}
|
|
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm ${
|
|
isUser
|
|
? 'bg-primary-foreground/10 hover:bg-primary-foreground/20'
|
|
: 'bg-muted hover:bg-muted/80'
|
|
}`}
|
|
>
|
|
<Paperclip className="w-3.5 h-3.5 flex-shrink-0" />
|
|
<span className="truncate">{att.name}</span>
|
|
{att.size && (
|
|
<span className="text-xs opacity-70 flex-shrink-0">
|
|
{att.size > 1024 * 1024
|
|
? `${(att.size / 1024 / 1024).toFixed(1)}MB`
|
|
: `${(att.size / 1024).toFixed(0)}KB`}
|
|
</span>
|
|
)}
|
|
</a>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{isUser ? (
|
|
<p className="text-sm whitespace-pre-wrap">{textContent}</p>
|
|
) : (
|
|
<MarkdownContent content={textContent} />
|
|
)}
|
|
</div>
|
|
{isUser && (
|
|
<div className="w-7 h-7 rounded-full bg-secondary flex items-center justify-center flex-shrink-0 mt-0.5">
|
|
<User className="w-4 h-4" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type AgentTeamGroup = {
|
|
rootRun: ProcessRun;
|
|
memberRuns: ProcessRun[];
|
|
startedAt: string;
|
|
};
|
|
|
|
const TERMINAL_RUN_STATUSES = new Set<ProcessRun['status']>(['done', 'error', 'cancelled']);
|
|
|
|
function shouldHideSystemAgentMessage(message: ChatMessage): boolean {
|
|
if (message.role !== 'assistant' || typeof message.content !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
const content = message.content.trim();
|
|
return (
|
|
/^\[(Agent team|Subagent)\s+['"][^'"]+['"]\s+(completed|failed|cancelled|finished)\]/i.test(content)
|
|
|| (content.startsWith('[Agent team ') && content.includes('\nTask:'))
|
|
);
|
|
}
|
|
|
|
function parseTimelineTime(value?: string | null): number | null {
|
|
if (!value) return null;
|
|
const parsed = new Date(value).getTime();
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
|
|
function buildAgentTeamGroups(processRuns: ProcessRun[]): AgentTeamGroup[] {
|
|
const runMap = new Map(processRuns.map((run) => [run.run_id, run]));
|
|
const groups = new Map<string, AgentTeamGroup>();
|
|
|
|
for (const run of processRuns) {
|
|
if (run.actor_type !== 'agent') {
|
|
continue;
|
|
}
|
|
|
|
let root = run;
|
|
const seen = new Set<string>([run.run_id]);
|
|
let parentId = run.parent_run_id ?? null;
|
|
while (parentId) {
|
|
const parent = runMap.get(parentId);
|
|
if (!parent || seen.has(parent.run_id)) {
|
|
break;
|
|
}
|
|
root = parent;
|
|
seen.add(parent.run_id);
|
|
parentId = parent.parent_run_id ?? null;
|
|
}
|
|
|
|
const existing = groups.get(root.run_id);
|
|
if (existing) {
|
|
existing.memberRuns.push(run);
|
|
continue;
|
|
}
|
|
groups.set(root.run_id, {
|
|
rootRun: root,
|
|
memberRuns: [run],
|
|
startedAt: root.started_at || run.started_at,
|
|
});
|
|
}
|
|
|
|
return Array.from(groups.values())
|
|
.map((group) => ({
|
|
...group,
|
|
memberRuns: [...group.memberRuns].sort((a: ProcessRun, b: ProcessRun) => {
|
|
const at = parseTimelineTime(a.started_at) ?? 0;
|
|
const bt = parseTimelineTime(b.started_at) ?? 0;
|
|
return at - bt;
|
|
}),
|
|
}))
|
|
.sort((a, b) => {
|
|
const at = parseTimelineTime(a.startedAt) ?? 0;
|
|
const bt = parseTimelineTime(b.startedAt) ?? 0;
|
|
return at - bt;
|
|
});
|
|
}
|
|
|
|
export function MessageList({
|
|
messages,
|
|
isThinking,
|
|
messagesEndRef,
|
|
viewportRef,
|
|
processRuns,
|
|
processEvents,
|
|
processArtifacts,
|
|
selectedRunId,
|
|
onSelectRun,
|
|
onCancelRun,
|
|
}: {
|
|
messages: ChatMessage[];
|
|
isThinking: boolean;
|
|
messagesEndRef: React.RefObject<HTMLDivElement>;
|
|
viewportRef: React.RefObject<HTMLDivElement>;
|
|
processRuns: ProcessRun[];
|
|
processEvents: ProcessEvent[];
|
|
processArtifacts: ProcessArtifact[];
|
|
selectedRunId: string | null;
|
|
onSelectRun: (runId: string) => void;
|
|
onCancelRun: (runId: string) => void;
|
|
}) {
|
|
const { locale } = useAppI18n();
|
|
const visibleMessages = React.useMemo(
|
|
() => messages.filter((message) => !shouldHideSystemAgentMessage(message)),
|
|
[messages]
|
|
);
|
|
const teamGroups = React.useMemo(
|
|
() =>
|
|
buildAgentTeamGroups(processRuns).filter((group) =>
|
|
group.memberRuns.some((run) => !TERMINAL_RUN_STATUSES.has(run.status))
|
|
),
|
|
[processRuns]
|
|
);
|
|
const timelineItems = React.useMemo(() => {
|
|
const messageItems = visibleMessages.map((message, index) => ({
|
|
kind: 'message' as const,
|
|
key: `${message.role}:${message.timestamp || index}:${index}`,
|
|
sortTime: parseTimelineTime(message.timestamp) ?? Number.MAX_SAFE_INTEGER / 2 + index,
|
|
order: index,
|
|
message,
|
|
}));
|
|
const teamItems = teamGroups.map((group, index) => ({
|
|
kind: 'team' as const,
|
|
key: `team:${group.rootRun.run_id}`,
|
|
sortTime: parseTimelineTime(group.startedAt) ?? Number.MAX_SAFE_INTEGER / 2 + visibleMessages.length + index,
|
|
order: visibleMessages.length + index,
|
|
group,
|
|
}));
|
|
|
|
return [...messageItems, ...teamItems].sort((a, b) => {
|
|
if (a.sortTime !== b.sortTime) {
|
|
return a.sortTime - b.sortTime;
|
|
}
|
|
return a.order - b.order;
|
|
});
|
|
}, [teamGroups, visibleMessages]);
|
|
|
|
return (
|
|
<ScrollArea className="h-full px-4" viewportRef={viewportRef}>
|
|
<div className="max-w-6xl mx-auto py-4 space-y-4">
|
|
{visibleMessages.length === 0 && teamGroups.length === 0 && !isThinking && (
|
|
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground">
|
|
<Bot className="w-12 h-12 mb-4 opacity-50" />
|
|
<p className="text-lg font-medium">Boardware Agent Sandbox</p>
|
|
<p className="text-sm">{pickAppText(locale, '发送消息开始对话', 'Send a message to start the conversation')}</p>
|
|
</div>
|
|
)}
|
|
|
|
{timelineItems.map((item) =>
|
|
item.kind === 'message' ? (
|
|
<MessageBubble key={item.key} message={item.message} />
|
|
) : (
|
|
<AgentTeamBlock
|
|
key={item.key}
|
|
rootRun={item.group.rootRun}
|
|
memberRuns={item.group.memberRuns}
|
|
events={processEvents}
|
|
artifacts={processArtifacts}
|
|
selectedRunId={selectedRunId}
|
|
onSelectRun={onSelectRun}
|
|
onCancelRun={onCancelRun}
|
|
/>
|
|
)
|
|
)}
|
|
|
|
{isThinking && (
|
|
<div className="flex items-center gap-2 text-muted-foreground px-1">
|
|
<Bot className="w-5 h-5" />
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
<span className="text-sm">{pickAppText(locale, '思考中...', 'Thinking...')}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
</ScrollArea>
|
|
);
|
|
}
|