feat(engine): 添加技能查看工具并优化异步任务管理 - 添加SkillViewTool到引擎加载器中,增强技能管理功能 - 在AgentLoop中引入_active_direct_task来跟踪活跃任务 - 实现直接任务执行时的同步处理逻辑 - 更新工具实例化方式以支持依赖注入 feat(config): 增加智能体运行时参数配置支持 - 扩展AgentDefaultsConfig添加max_tokens和temperature字段 - 实现配置解析函数_first_config_value处理多个配置源 - 支持通过Web API动态更新智能体运行时参数 - 添加前端页面配置表单和验证逻辑 refactor(provider): 统一最大令牌数参数类型为可选整型 - 将所有LLM提供者的max_tokens参数改为int | None类型 - 为AnthropicProvider实现模型特定的最大令牌数默认值 - 调整参数传递逻辑,优先级:调用参数 > 配置文件 > 模型默认值 - 移除硬编码的默认值,改用条件判断 feat(process): 增强事件投影功能 - 添加工具调用开始/结束事件的映射逻辑 - 实现技能激活事件的识别和展示 - 添加辅助函数处理工具调用名称和参数提取 - 优化运行记录关联逻辑,提升事件匹配准确性 fix(web): 更新网络请求客户端信任环境设置 - 将WebFetchTool和WebSearchTool的trust_env参数设为True - 确保HTTP客户端能够正确使用系统代理配置 - 修复可能的网络连接问题 test: 添加配置加载和事件投影相关测试 - 新增智能体默认参数配置测试用例 - 实现API配置持久化和重载测试 - 添加技能卡片和工具事件的投影测试 ```
254 lines
11 KiB
TypeScript
254 lines
11 KiB
TypeScript
'use client';
|
|
|
|
import { AlertTriangle, Bot, Download, ExternalLink, FileText, Users } from 'lucide-react';
|
|
|
|
import { TaskRuntimeStatusBadge, formatTaskRuntimeTime } from '@/components/task-runtime/TaskRuntimeShared';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { getFileUrl } from '@/lib/api';
|
|
import { pickAppText } from '@/lib/i18n/core';
|
|
import { useAppI18n } from '@/lib/i18n/provider';
|
|
import type { TaskRuntimeStatus } from '@/lib/task-runtime';
|
|
import type { BackendTask, ProcessArtifact, ProcessRun, TaskTimelineCard } from '@/types';
|
|
|
|
type Props = {
|
|
task: BackendTask;
|
|
runs: ProcessRun[];
|
|
artifacts: ProcessArtifact[];
|
|
cards: TaskTimelineCard[];
|
|
};
|
|
|
|
const ACTIVE_RUN_STATUSES = new Set<ProcessRun['status']>(['queued', 'running', 'waiting']);
|
|
const RUNTIME_STATUSES = new Set<string>(['queued', 'running', 'waiting', 'blocked', 'done', 'error', 'cancelled']);
|
|
|
|
function isRuntimeStatus(status: string): status is TaskRuntimeStatus {
|
|
return RUNTIME_STATUSES.has(status);
|
|
}
|
|
|
|
function humanTaskStatus(status: string, locale: 'zh-CN' | 'en-US') {
|
|
const map: Record<string, [string, string]> = {
|
|
open: ['已创建', 'Open'],
|
|
running: ['执行中', 'Running'],
|
|
awaiting_acceptance: ['等待验收', 'Awaiting acceptance'],
|
|
needs_revision: ['需要修改', 'Needs revision'],
|
|
closed: ['已完成', 'Closed'],
|
|
abandoned: ['已放弃', 'Abandoned'],
|
|
accept: ['已接受', 'Accepted'],
|
|
satisfied: ['已接受', 'Accepted'],
|
|
revise: ['已请求修改', 'Revision requested'],
|
|
abandon: ['已放弃', 'Abandoned'],
|
|
};
|
|
const item = map[status];
|
|
return item ? pickAppText(locale, item[0], item[1]) : status;
|
|
}
|
|
|
|
function latestFeedback(task: BackendTask): Record<string, unknown> | null {
|
|
return [...(task.feedback ?? [])].reverse()[0] ?? null;
|
|
}
|
|
|
|
function acceptanceState(task: BackendTask, locale: 'zh-CN' | 'en-US'): string {
|
|
const feedback = latestFeedback(task);
|
|
const kind = String(feedback?.acceptance_type || feedback?.feedback_type || '');
|
|
if (kind) return humanTaskStatus(kind, locale);
|
|
if (task.status === 'awaiting_acceptance') return pickAppText(locale, '等待验收', 'Awaiting acceptance');
|
|
if (task.status === 'needs_revision') return pickAppText(locale, '等待修改', 'Awaiting revision');
|
|
return pickAppText(locale, '未验收', 'No acceptance yet');
|
|
}
|
|
|
|
function toTime(value: string): number {
|
|
const parsed = new Date(value).getTime();
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
|
|
function isWarningOrError(card: TaskTimelineCard): boolean {
|
|
const severity = String(card.details?.severity || card.details?.level || '').toLowerCase();
|
|
return card.type === 'error' || card.status === 'error' || severity === 'warning' || severity === 'error';
|
|
}
|
|
|
|
function artifactHref(artifact: ProcessArtifact): string | null {
|
|
if (artifact.url) return artifact.url;
|
|
if (artifact.file_id) return getFileUrl(artifact.file_id);
|
|
return null;
|
|
}
|
|
|
|
function inlineArtifactPayload(artifact: ProcessArtifact): { content: string; filename: string; mimeType: string } | null {
|
|
const baseName = (artifact.title || artifact.artifact_id || 'artifact').replace(/[\\/:*?"<>|]+/g, '-');
|
|
if (artifact.content !== undefined) {
|
|
const isMarkdown = artifact.artifact_type === 'markdown';
|
|
return {
|
|
content: artifact.content,
|
|
filename: `${baseName}.${isMarkdown ? 'md' : 'txt'}`,
|
|
mimeType: isMarkdown ? 'text/markdown;charset=utf-8' : 'text/plain;charset=utf-8',
|
|
};
|
|
}
|
|
if (artifact.data !== undefined) {
|
|
return {
|
|
content: JSON.stringify(artifact.data, null, 2),
|
|
filename: `${baseName}.json`,
|
|
mimeType: 'application/json;charset=utf-8',
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function downloadInlineArtifact(artifact: ProcessArtifact): void {
|
|
const payload = inlineArtifactPayload(artifact);
|
|
if (!payload) return;
|
|
|
|
const url = URL.createObjectURL(new Blob([payload.content], { type: payload.mimeType }));
|
|
const anchor = document.createElement('a');
|
|
anchor.href = url;
|
|
anchor.download = payload.filename;
|
|
document.body.appendChild(anchor);
|
|
anchor.click();
|
|
anchor.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function RunRow({ run }: { run: ProcessRun }) {
|
|
const { locale } = useAppI18n();
|
|
|
|
return (
|
|
<div className="rounded-md border border-border bg-muted/20 p-3">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="truncate text-sm font-medium">{run.title || run.actor_name}</div>
|
|
<div className="mt-1 truncate text-xs text-muted-foreground">{run.actor_name}</div>
|
|
</div>
|
|
<TaskRuntimeStatusBadge status={run.status} />
|
|
</div>
|
|
<div className="mt-2 text-xs text-muted-foreground">{formatTaskRuntimeTime(run.started_at, locale)}</div>
|
|
{run.summary ? <p className="mt-2 line-clamp-2 text-xs text-muted-foreground">{run.summary}</p> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function TaskSideRail({ task, runs, artifacts, cards }: Props) {
|
|
const { locale } = useAppI18n();
|
|
const activeRuns = runs.filter((run) => ACTIVE_RUN_STATUSES.has(run.status));
|
|
const childRuns = runs.filter((run) => Boolean(run.parent_run_id));
|
|
const latestAlert = cards.filter(isWarningOrError).sort((a, b) => toTime(b.createdAt) - toTime(a.createdAt))[0] ?? null;
|
|
|
|
return (
|
|
<aside className="space-y-4">
|
|
<Card className="rounded-md">
|
|
<CardHeader>
|
|
<CardTitle className="text-base">{pickAppText(locale, '任务状态', 'Task status')}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<div className="flex items-center justify-between gap-3">
|
|
{isRuntimeStatus(task.status) ? (
|
|
<TaskRuntimeStatusBadge status={task.status} />
|
|
) : (
|
|
<Badge variant="outline" className="text-[11px]">
|
|
{humanTaskStatus(task.status, locale)}
|
|
</Badge>
|
|
)}
|
|
<div className="text-sm text-muted-foreground">
|
|
{pickAppText(locale, '活跃运行', 'Active runs')}: <span className="font-medium text-foreground">{activeRuns.length}</span>
|
|
</div>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{pickAppText(locale, '更新', 'Updated')}: {formatTaskRuntimeTime(task.updated_at, locale)}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{pickAppText(locale, '验收', 'Acceptance')}: {acceptanceState(task, locale)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="rounded-md">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Bot className="h-4 w-4 text-muted-foreground" />
|
|
{pickAppText(locale, '运行中', 'Active runs')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{activeRuns.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">{pickAppText(locale, '暂无活跃运行', 'No active runs')}</p>
|
|
) : (
|
|
activeRuns.map((run) => <RunRow key={run.run_id} run={run} />)
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{latestAlert ? (
|
|
<Card className="rounded-md border-destructive/40">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<AlertTriangle className="h-4 w-4 text-destructive" />
|
|
{pickAppText(locale, '最新提醒', 'Latest alert')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
<div className="text-sm font-medium">{latestAlert.title}</div>
|
|
{latestAlert.summary ? <p className="text-sm text-muted-foreground">{latestAlert.summary}</p> : null}
|
|
<div className="text-xs text-muted-foreground">{formatTaskRuntimeTime(latestAlert.createdAt, locale)}</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : null}
|
|
|
|
<Card className="rounded-md">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<Users className="h-4 w-4 text-muted-foreground" />
|
|
{pickAppText(locale, 'Agent Team', 'Agent team')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{childRuns.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">{pickAppText(locale, '暂无子运行', 'No child runs')}</p>
|
|
) : (
|
|
childRuns.map((run) => <RunRow key={run.run_id} run={run} />)
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card className="rounded-md">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
|
{pickAppText(locale, '产物', 'Artifacts')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-2">
|
|
{artifacts.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">{pickAppText(locale, '暂无产物', 'No artifacts yet')}</p>
|
|
) : (
|
|
artifacts.map((artifact) => {
|
|
const href = artifactHref(artifact);
|
|
const inlinePayload = inlineArtifactPayload(artifact);
|
|
return (
|
|
<div key={artifact.artifact_id} className="flex items-center justify-between gap-3 rounded-md border border-border bg-muted/20 p-3">
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-2 text-sm font-medium">
|
|
<FileText className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
<span className="truncate">{artifact.title}</span>
|
|
</div>
|
|
<div className="mt-1 text-xs text-muted-foreground">{artifact.artifact_type}</div>
|
|
</div>
|
|
{href ? (
|
|
<Button asChild size="sm" variant="outline" className="shrink-0">
|
|
<a href={href} target="_blank" rel="noopener noreferrer">
|
|
{artifact.url ? <ExternalLink className="mr-2 h-3.5 w-3.5" /> : <Download className="mr-2 h-3.5 w-3.5" />}
|
|
{artifact.url ? pickAppText(locale, '打开', 'Open') : pickAppText(locale, '下载', 'Download')}
|
|
</a>
|
|
</Button>
|
|
) : inlinePayload ? (
|
|
<Button size="sm" variant="outline" className="shrink-0" onClick={() => downloadInlineArtifact(artifact)}>
|
|
<Download className="mr-2 h-3.5 w-3.5" />
|
|
{pickAppText(locale, '下载', 'Download')}
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</aside>
|
|
);
|
|
}
|