Files
beaver_project/app-instance/frontend/app/(app)/status/page.tsx
steven_li 8a12c30141 feat(beaver): 完成Task Team功能v1实现,重构后端架构支持统一内核
新增内部Task系统,包括验证、反馈门控机制,实现自动质量验证
(通过率>=0.75)和用户反馈闭环(satisfied/revise/abandon)。

实现Agent Team v1协调器,支持sequence/parallel/dag执行策略,
sub-agent复用主AgentLoop,每个run使用独立memory snapshot。

建立Skill学习pipeline,包含draft/审核/发布/回滚完整生命周期,
通过Task验证通过且用户满意才生成学习候选。

重构目录结构,移除third_party依赖,建立统一engine内核,
所有agent共享运行时基础组件。

更新ContextBuilder清理provider消息字段,增强SkillContext版本管理,
集成TaskExecutionPlanner和TaskSkillResolver实现技能解析机制。
2026-05-08 17:14:14 +08:00

490 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useEffect, useState } from 'react';
import {
CheckCircle2,
XCircle,
AlertCircle,
RefreshCw,
Server,
Cpu,
Radio,
Key,
Loader2,
Settings2,
} from 'lucide-react';
import { getStatus, restartSystem, updateProviderConfig } 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 {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import type { ProviderStatus, SystemStatus } from '@/types';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
type ProviderFormState = {
enabled: boolean;
model: string;
apiKey: string;
apiBase: string;
requestTimeoutSeconds: string;
};
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 [selectedProvider, setSelectedProvider] = useState<ProviderStatus | null>(null);
const [providerForm, setProviderForm] = useState<ProviderFormState>(() => ({
enabled: false,
model: '',
apiKey: '',
apiBase: '',
requestTimeoutSeconds: '',
}));
const [savingProvider, setSavingProvider] = useState(false);
const [providerError, setProviderError] = 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'));
}
};
const openProviderDialog = (provider: ProviderStatus) => {
setSelectedProvider(provider);
setProviderError(null);
setProviderForm({
enabled: Boolean(provider.enabled || provider.has_key),
model: status?.model || '',
apiKey: '',
apiBase: provider.api_base || provider.default_api_base || provider.detail || '',
requestTimeoutSeconds: '',
});
};
const handleSaveProvider = async () => {
if (!selectedProvider) return;
const providerId = selectedProvider.id || selectedProvider.name;
setSavingProvider(true);
setProviderError(null);
try {
const timeout = providerForm.requestTimeoutSeconds.trim()
? Number(providerForm.requestTimeoutSeconds.trim())
: undefined;
if (timeout !== undefined && (!Number.isFinite(timeout) || timeout <= 0)) {
throw new Error(pickAppText(locale, '请求超时必须是正数', 'Request timeout must be a positive number'));
}
await updateProviderConfig(providerId, {
enabled: providerForm.enabled,
model: providerForm.model.trim() || undefined,
api_key: providerForm.apiKey.trim() || undefined,
api_base: providerForm.apiBase.trim() || undefined,
request_timeout_seconds: timeout,
});
await loadStatus();
setSelectedProvider(null);
} catch (err: any) {
setProviderError(err.message || pickAppText(locale, '保存提供商配置失败', 'Failed to save provider settings'));
} finally {
setSavingProvider(false);
}
};
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-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
{status.providers.map((p) => (
<button
key={p.id || p.name}
type="button"
onClick={() => openProviderDialog(p)}
className={[
'group flex min-h-[76px] w-full items-start justify-between rounded-lg border p-3 text-left transition',
p.active
? 'border-primary bg-primary/5 shadow-sm'
: 'border-border bg-background hover:border-primary/50 hover:bg-muted/40',
].join(' ')}
>
<span className="min-w-0 space-y-1">
<span className="flex items-center gap-2 text-sm font-medium">
{p.has_key ? (
<CheckCircle2 className="h-4 w-4 shrink-0 text-green-500" />
) : (
<XCircle className="h-4 w-4 shrink-0 text-muted-foreground/40" />
)}
<span className={p.has_key ? 'truncate' : 'truncate text-muted-foreground'}>
{providerLabel(p)}
</span>
</span>
<span className="block truncate text-xs text-muted-foreground">
{p.active
? pickAppText(locale, '当前默认', 'Current default')
: p.enabled
? pickAppText(locale, '已启用', 'Enabled')
: pickAppText(locale, '点击配置', 'Click to configure')}
</span>
{(p.detail || p.api_key_masked) && (
<span className="block truncate text-xs text-muted-foreground">
{p.api_key_masked || p.detail}
</span>
)}
</span>
<Settings2 className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground opacity-60 group-hover:text-primary" />
</button>
))}
</div>
</CardContent>
</Card>
<Dialog open={Boolean(selectedProvider)} onOpenChange={(open) => !open && setSelectedProvider(null)}>
<DialogContent className="sm:max-w-[520px]">
<DialogHeader>
<DialogTitle>
{pickAppText(locale, '配置提供商', 'Configure provider')}
{selectedProvider ? ` · ${providerLabel(selectedProvider)}` : ''}
</DialogTitle>
<DialogDescription>
{pickAppText(locale, '启用后会把它设为当前实例默认提供商。API Key 留空会保留已保存的值。', 'When enabled, this becomes the default provider for this instance. Leave API key empty to keep the saved value.')}
</DialogDescription>
</DialogHeader>
<div className="space-y-5 py-2">
<div className="flex items-center justify-between rounded-lg border px-3 py-2">
<div>
<Label className="text-sm">{pickAppText(locale, '启用提供商', 'Enable provider')}</Label>
<p className="text-xs text-muted-foreground">
{pickAppText(locale, '关闭会从配置中移除这个提供商', 'Turning this off removes this provider from config')}
</p>
</div>
<Switch
checked={providerForm.enabled}
onCheckedChange={(checked) => setProviderForm((prev) => ({ ...prev, enabled: checked }))}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="provider-model">{pickAppText(locale, '默认模型', 'Default model')}</Label>
<Input
id="provider-model"
value={providerForm.model}
onChange={(event) => setProviderForm((prev) => ({ ...prev, model: event.target.value }))}
placeholder="qwen-plus"
disabled={!providerForm.enabled}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="provider-api-key">API Key</Label>
<Input
id="provider-api-key"
type="password"
value={providerForm.apiKey}
onChange={(event) => setProviderForm((prev) => ({ ...prev, apiKey: event.target.value }))}
placeholder={selectedProvider?.api_key_masked || pickAppText(locale, '留空保持不变', 'Leave blank to keep existing')}
disabled={!providerForm.enabled || Boolean(selectedProvider?.is_oauth)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="provider-api-base">API Base</Label>
<Input
id="provider-api-base"
value={providerForm.apiBase}
onChange={(event) => setProviderForm((prev) => ({ ...prev, apiBase: event.target.value }))}
placeholder={selectedProvider?.default_api_base || 'https://api.example.com/v1'}
disabled={!providerForm.enabled || Boolean(selectedProvider?.is_oauth)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="provider-timeout">{pickAppText(locale, '请求超时(秒)', 'Request timeout (seconds)')}</Label>
<Input
id="provider-timeout"
inputMode="decimal"
value={providerForm.requestTimeoutSeconds}
onChange={(event) => setProviderForm((prev) => ({ ...prev, requestTimeoutSeconds: event.target.value }))}
placeholder={pickAppText(locale, '默认', 'Default')}
disabled={!providerForm.enabled}
/>
</div>
{providerError ? (
<p className="text-sm text-destructive">{providerError}</p>
) : null}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setSelectedProvider(null)} disabled={savingProvider}>
{pickAppText(locale, '取消', 'Cancel')}
</Button>
<Button onClick={handleSaveProvider} disabled={savingProvider}>
{savingProvider ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
{pickAppText(locale, '保存', 'Save')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 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>
);
}
function providerLabel(provider: ProviderStatus): string {
return provider.label || provider.name;
}