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实现技能解析机制。
This commit is contained in:
@ -11,8 +11,9 @@ import {
|
||||
Radio,
|
||||
Key,
|
||||
Loader2,
|
||||
Settings2,
|
||||
} from 'lucide-react';
|
||||
import { getStatus, restartSystem } from '@/lib/api';
|
||||
import { getStatus, restartSystem, updateProviderConfig } from '@/lib/api';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@ -26,10 +27,29 @@ import {
|
||||
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 {
|
||||
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);
|
||||
@ -38,6 +58,16 @@ export default function StatusPage() {
|
||||
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);
|
||||
@ -86,6 +116,46 @@ export default function StatusPage() {
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
@ -210,31 +280,137 @@ export default function StatusPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div className="grid grid-cols-1 sm: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"
|
||||
<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(' ')}
|
||||
>
|
||||
{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 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>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
@ -307,3 +483,7 @@ function InfoRow({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function providerLabel(provider: ProviderStatus): string {
|
||||
return provider.label || provider.name;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user