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:
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user