- 引入AgentTeamOrchestrator支持多agent协同任务执行 - 增加第三方swarms库依赖并配置git协议替换以改善包管理 - 扩展DelegationManager支持团队任务调度和进度跟踪 - 实现中文bigram分词算法提升中文任务检索准确性 - 调整A2AClient和DelegationManager超时时间从30秒增至600秒 - 优化AgentRunResult状态判断逻辑增加有意义摘要检测 - 修改Dockerfile配置npm仓库镜像地址和git协议映射 - 更新CLI命令行接口支持网关端口配置传递 - 调整提供者超时配置机制增强请求稳定性 - 移除过时的support_group字段简化agent描述符结构 - 增强错误处理和进度事件报告机制改进用户体验
310 lines
11 KiB
TypeScript
310 lines
11 KiB
TypeScript
'use client';
|
||
|
||
import React, { useEffect, useState } from 'react';
|
||
import {
|
||
CheckCircle2,
|
||
XCircle,
|
||
AlertCircle,
|
||
RefreshCw,
|
||
Server,
|
||
Cpu,
|
||
Radio,
|
||
Key,
|
||
Loader2,
|
||
} from 'lucide-react';
|
||
import { getStatus, restartSystem } from '@/lib/api';
|
||
import {
|
||
AlertDialog,
|
||
AlertDialogAction,
|
||
AlertDialogCancel,
|
||
AlertDialogContent,
|
||
AlertDialogDescription,
|
||
AlertDialogFooter,
|
||
AlertDialogHeader,
|
||
AlertDialogTitle,
|
||
} from '@/components/ui/alert-dialog';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import type { SystemStatus } from '@/types';
|
||
import { pickAppText } from '@/lib/i18n/core';
|
||
import { useAppI18n } from '@/lib/i18n/provider';
|
||
|
||
export default function StatusPage() {
|
||
const { locale } = useAppI18n();
|
||
const [status, setStatus] = useState<SystemStatus | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [restartDialogOpen, setRestartDialogOpen] = useState(false);
|
||
const [restarting, setRestarting] = useState(false);
|
||
const [restartError, setRestartError] = useState<string | null>(null);
|
||
|
||
const loadStatus = async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const data = await getStatus();
|
||
setStatus(data);
|
||
} catch (err: any) {
|
||
setError(err.message || pickAppText(locale, '连接后端失败', 'Failed to connect to the backend'));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
loadStatus();
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!restarting) {
|
||
return;
|
||
}
|
||
|
||
const intervalId = window.setInterval(async () => {
|
||
try {
|
||
await getStatus();
|
||
window.location.reload();
|
||
} catch {
|
||
// Ignore failures until the container is back.
|
||
}
|
||
}, 3000);
|
||
|
||
return () => {
|
||
window.clearInterval(intervalId);
|
||
};
|
||
}, [restarting]);
|
||
|
||
const handleRestart = async () => {
|
||
setRestartError(null);
|
||
try {
|
||
await restartSystem();
|
||
setRestartDialogOpen(false);
|
||
setRestarting(true);
|
||
} catch (err: any) {
|
||
setRestartError(err.message || pickAppText(locale, '重启失败', 'Restart failed'));
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="flex items-center justify-center py-20">
|
||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="max-w-4xl mx-auto p-6">
|
||
<Card className="border-destructive">
|
||
<CardContent className="pt-6">
|
||
<div className="flex items-center gap-3 text-destructive">
|
||
<AlertCircle className="w-5 h-5" />
|
||
<div>
|
||
<p className="font-medium">{pickAppText(locale, '无法连接到 Boardware Agent Sandbox 后端', 'Unable to connect to the Boardware Agent Sandbox backend')}</p>
|
||
<p className="text-sm text-muted-foreground mt-1">{error}</p>
|
||
<p className="text-sm text-muted-foreground mt-1">{pickAppText(locale, '请确认后端服务已启动,并且当前页面可以访问它。', 'Please confirm the backend service is running and reachable from this page.')}</p>
|
||
</div>
|
||
</div>
|
||
<Button onClick={loadStatus} variant="outline" size="sm" className="mt-4">
|
||
<RefreshCw className="w-4 h-4 mr-2" />
|
||
{pickAppText(locale, '重试', 'Retry')}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!status) return null;
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
||
<div className="flex items-center justify-between">
|
||
<h1 className="text-2xl font-bold">{pickAppText(locale, '系统状态', 'System status')}</h1>
|
||
<Button onClick={loadStatus} variant="outline" size="sm" disabled={restarting}>
|
||
<RefreshCw className="w-4 h-4 mr-2" />
|
||
{pickAppText(locale, '刷新', 'Refresh')}
|
||
</Button>
|
||
</div>
|
||
|
||
{/* System Info */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<Server className="w-4 h-4" />
|
||
{pickAppText(locale, '系统信息', 'System information')}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||
<div className="space-y-1">
|
||
<p className="text-sm font-medium">{pickAppText(locale, '重启当前实例', 'Restart current instance')}</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{restarting
|
||
? pickAppText(locale, '正在重启当前 docker,服务恢复后页面会自动刷新。', 'Restarting the current Docker container. The page will refresh automatically once the service is back.')
|
||
: pickAppText(locale, '会重启当前 docker 容器。重启完成后需要重新登录。', 'This restarts the current Docker container. You will need to sign in again afterwards.')}
|
||
</p>
|
||
{restartError ? (
|
||
<p className="text-sm text-destructive">{restartError}</p>
|
||
) : null}
|
||
</div>
|
||
<AlertDialog open={restartDialogOpen} onOpenChange={setRestartDialogOpen}>
|
||
<Button
|
||
variant="destructive"
|
||
onClick={() => setRestartDialogOpen(true)}
|
||
disabled={restarting}
|
||
>
|
||
{restarting ? (
|
||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||
) : (
|
||
<RefreshCw className="w-4 h-4 mr-2" />
|
||
)}
|
||
Restart
|
||
</Button>
|
||
<AlertDialogContent>
|
||
<AlertDialogHeader>
|
||
<AlertDialogTitle>{pickAppText(locale, '确认重启当前实例?', 'Restart the current instance?')}</AlertDialogTitle>
|
||
<AlertDialogDescription>
|
||
{pickAppText(locale, '这会重启当前 docker 容器,页面会短暂不可用。由于当前登录态保存在内存里,重启完成后需要重新登录。', 'This restarts the current Docker container and the page will be temporarily unavailable. Because the current sign-in state is stored in memory, you will need to sign in again after the restart.')}
|
||
</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
<AlertDialogFooter>
|
||
<AlertDialogCancel disabled={restarting}>{pickAppText(locale, '取消', 'Cancel')}</AlertDialogCancel>
|
||
<AlertDialogAction onClick={handleRestart} disabled={restarting}>
|
||
{restarting ? pickAppText(locale, '重启中...', 'Restarting...') : pickAppText(locale, '确认重启', 'Confirm restart')}
|
||
</AlertDialogAction>
|
||
</AlertDialogFooter>
|
||
</AlertDialogContent>
|
||
</AlertDialog>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Model Config */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<Cpu className="w-4 h-4" />
|
||
{pickAppText(locale, '智能体配置', 'Agent configuration')}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
<InfoRow label={pickAppText(locale, '模型', 'Model')} value={status.model} />
|
||
<InfoRow label={pickAppText(locale, '最大令牌数', 'Max tokens')} value={String(status.max_tokens)} />
|
||
<InfoRow label={pickAppText(locale, '温度', 'Temperature')} value={String(status.temperature)} />
|
||
<InfoRow
|
||
label={pickAppText(locale, '最大工具迭代次数', 'Max tool iterations')}
|
||
value={String(status.max_tool_iterations)}
|
||
/>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Providers */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<Key className="w-4 h-4" />
|
||
{pickAppText(locale, '提供商', 'Providers')}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||
{status.providers.map((p) => (
|
||
<div
|
||
key={p.name}
|
||
className="flex items-center gap-2 text-sm"
|
||
>
|
||
{p.has_key ? (
|
||
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||
) : (
|
||
<XCircle className="w-4 h-4 text-muted-foreground/40" />
|
||
)}
|
||
<span className={p.has_key ? '' : 'text-muted-foreground'}>
|
||
{p.name}
|
||
</span>
|
||
{p.detail && (
|
||
<span className="text-xs text-muted-foreground truncate">
|
||
{p.detail}
|
||
</span>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Channels */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<Radio className="w-4 h-4" />
|
||
{pickAppText(locale, '通道', 'Channels')}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||
{status.channels.map((ch) => (
|
||
<div key={ch.name} className="flex items-center gap-2 text-sm">
|
||
<Badge
|
||
variant={ch.enabled ? 'default' : 'secondary'}
|
||
className="text-xs"
|
||
>
|
||
{ch.enabled ? pickAppText(locale, '开启', 'On') : pickAppText(locale, '关闭', 'Off')}
|
||
</Badge>
|
||
<span className="capitalize">{ch.name}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Cron Summary */}
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<AlertCircle className="w-4 h-4" />
|
||
{pickAppText(locale, '调度器', 'Scheduler')}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
<InfoRow
|
||
label={pickAppText(locale, '状态', 'Status')}
|
||
value={status.cron.enabled ? pickAppText(locale, '运行中', 'Running') : pickAppText(locale, '已停止', 'Stopped')}
|
||
ok={status.cron.enabled}
|
||
/>
|
||
<InfoRow label={pickAppText(locale, '任务数', 'Jobs')} value={String(status.cron.jobs)} />
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function InfoRow({
|
||
label,
|
||
value,
|
||
ok,
|
||
}: {
|
||
label: string;
|
||
value: string;
|
||
ok?: boolean;
|
||
}) {
|
||
return (
|
||
<div className="flex items-center justify-between text-sm">
|
||
<span className="text-muted-foreground">{label}</span>
|
||
<div className="flex items-center gap-2">
|
||
<code className="bg-muted px-2 py-0.5 rounded text-xs max-w-[400px] truncate">
|
||
{value}
|
||
</code>
|
||
{ok !== undefined &&
|
||
(ok ? (
|
||
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
||
) : (
|
||
<XCircle className="w-4 h-4 text-destructive" />
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|