fix: 修复 5 项确定 bug + Provider UX 重做 + 文档统一

Bug fixes:
- fix(dao): AsyncSession.delete 补齐漏掉的 await(provider/user/individual 共 4 处)
- fix(worker): result.data.output → result.output.output(pydantic-ai 1.x API 适配)
- fix(api): 删除 create_worker_from_template 死端点(ORM 字段不匹配必崩)
- fix(api): /provider/test 按 provider_type 分支适配 Anthropic/Gemini/OpenAI 三种协议
- fix(chat): SSE 流式聊天在 distributed 模式 fallback 到非流式,避免 asyncio.Queue 序列化崩溃

Features (previously unstaged):
- feat(provider): Provider 管理页重做(品牌图标、5 种类型、Test Connection、编辑模式)
- feat(provider): 新增 Gemini provider_type 支持
- feat(workflow): Finalize 节点输出 blackboard 摘要 + 失败原因;步骤完成/失败实时推送 SSE
- feat(i18n): regulatory_node 提示词从路由模式改为直接对话模式(中英双语)
- feat(consciousness): dynamic_prompt 支持 locale 国际化
- feat(logs): SystemLogsView 自动刷新 + 暂停按钮

Docs:
- docs: README/README-EN 统一为"开源通用多 Agent 协作平台"口径
- docs: ROADMAP 按 v0.1.x / v0.2.x / v0.3.x 重组
- docs: project.md 重写为结构化项目介绍

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 08:49:38 +00:00
parent c0fcbe2849
commit 9b73ae4db4
27 changed files with 858 additions and 214 deletions
@@ -0,0 +1,82 @@
interface ProviderIconProps {
type: string;
size?: number;
className?: string;
}
export function ProviderIcon({ type, size = 18, className = '' }: ProviderIconProps) {
const props = {
width: size,
height: size,
viewBox: '0 0 24 24',
fill: 'currentColor',
className,
};
switch (type?.toLowerCase()) {
case 'openai':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z"/>
</svg>
);
case 'claude':
case 'anthropic':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M4.709 15.955l4.72-2.647.079-.23-.079-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.328h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312z"/>
</svg>
);
case 'deepseek':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.846-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z"/>
</svg>
);
case 'gemini':
case 'google':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M12 24A14.304 14.304 0 000 12 14.304 14.304 0 0012 0a14.305 14.305 0 0012 12 14.305 14.305 0 00-12 12"/>
</svg>
);
case 'local':
case 'ollama':
return (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="12" rx="2" />
<path d="M7 20h10M9 16v4M15 16v4" />
<circle cx="8" cy="10" r="1" fill="currentColor" />
<circle cx="12" cy="10" r="1" fill="currentColor" />
<circle cx="16" cy="10" r="1" fill="currentColor" />
</svg>
);
default:
return (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="3" />
<path d="M9 9h6v6H9z" />
</svg>
);
}
}
const PROVIDER_BRAND_COLORS: Record<string, string> = {
openai: '#10a37f',
claude: '#d97757',
anthropic: '#d97757',
deepseek: '#4d6bfe',
gemini: '#4285f4',
google: '#4285f4',
local: '#8a8a8a',
ollama: '#8a8a8a',
};
export function getProviderBrandColor(type: string): string {
return PROVIDER_BRAND_COLORS[type?.toLowerCase()] || '#8a8a8a';
}
@@ -1,17 +1,51 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Plus, X, Server, Loader2, Boxes } from 'lucide-react';
import { Plus, X, Loader2, Boxes, Zap, ChevronDown, ChevronRight, Settings as SettingsIcon } from 'lucide-react';
import type { Provider } from '../../types';
import apiClient from '../../api/client';
import { ProviderIcon, getProviderBrandColor } from './ProviderIcon';
interface ProviderTypeOption {
id: string;
iconKey: string;
backendType: 'openai' | 'claude' | 'deepseek' | 'gemini';
defaultUrl: string;
descKey: string;
nameKey: string;
}
const PROVIDER_TYPES: ProviderTypeOption[] = [
{ id: 'openai', iconKey: 'openai', backendType: 'openai', defaultUrl: 'https://api.openai.com/v1', nameKey: 'agent.providerTypeOpenai', descKey: 'agent.providerTypeOpenaiDesc' },
{ id: 'openai_compat', iconKey: 'openai', backendType: 'openai', defaultUrl: '', nameKey: 'agent.providerTypeOpenaiCompat', descKey: 'agent.providerTypeOpenaiCompatDesc' },
{ id: 'anthropic', iconKey: 'claude', backendType: 'claude', defaultUrl: 'https://api.anthropic.com', nameKey: 'agent.providerTypeAnthropic', descKey: 'agent.providerTypeAnthropicDesc' },
{ id: 'gemini', iconKey: 'gemini', backendType: 'gemini', defaultUrl: 'https://generativelanguage.googleapis.com/v1beta', nameKey: 'agent.providerTypeGemini', descKey: 'agent.providerTypeGeminiDesc' },
{ id: 'deepseek', iconKey: 'deepseek', backendType: 'deepseek', defaultUrl: 'https://api.deepseek.com/v1', nameKey: 'agent.providerTypeDeepseek', descKey: 'agent.providerTypeDeepseekDesc' },
];
function detectTypeFromProvider(p: Provider): string {
if (p.provider_type === 'openai') {
return p.provider_url?.includes('api.openai.com') ? 'openai' : 'openai_compat';
}
if (p.provider_type === 'claude') return 'anthropic';
return p.provider_type || 'openai';
}
export function ProvidersSettings() {
const { t } = useTranslation();
const [providers, setProviders] = useState<Provider[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [formData, setFormData] = useState({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' });
const [editingProvider, setEditingProvider] = useState<string | null>(null);
const [selectedTypeId, setSelectedTypeId] = useState<string>('openai');
const [formData, setFormData] = useState({ provider_title: '', provider_url: '', provider_apikey: '', custom_models: '' });
const [showAdvanced, setShowAdvanced] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [testLoading, setTestLoading] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; error?: string; model_count?: number } | null>(null);
const [error, setError] = useState('');
const [expandedProvider, setExpandedProvider] = useState<string | null>(null);
const selectedType = PROVIDER_TYPES.find((p) => p.id === selectedTypeId) || PROVIDER_TYPES[0];
const fetchProviders = async () => {
setLoading(true);
@@ -19,7 +53,7 @@ export function ProvidersSettings() {
const response = await apiClient.get('/api/v1/provider/list');
setProviders(Object.values(response.data.provider_list || {}));
} catch (error) {
console.error("Failed to fetch providers", error);
console.error('Failed to fetch providers', error);
setProviders([]);
} finally {
setLoading(false);
@@ -28,6 +62,72 @@ export function ProvidersSettings() {
useEffect(() => { fetchProviders(); }, []);
const openAddModal = () => {
setEditingProvider(null);
setSelectedTypeId('openai');
setFormData({ provider_title: '', provider_url: PROVIDER_TYPES[0].defaultUrl, provider_apikey: '', custom_models: '' });
setError('');
setTestResult(null);
setShowAdvanced(false);
setIsModalOpen(true);
};
const openEditModal = (provider: Provider) => {
const typeId = detectTypeFromProvider(provider);
setEditingProvider(provider.provider_title);
setSelectedTypeId(typeId);
setFormData({
provider_title: provider.provider_title,
provider_url: provider.provider_url || '',
provider_apikey: '',
custom_models: '',
});
setError('');
setTestResult(null);
setShowAdvanced(false);
setIsModalOpen(true);
};
const handleSelectType = (typeId: string) => {
setSelectedTypeId(typeId);
if (!editingProvider) {
const tp = PROVIDER_TYPES.find((p) => p.id === typeId);
if (tp) setFormData((prev) => ({ ...prev, provider_url: tp.defaultUrl }));
}
setTestResult(null);
};
const buildPayload = () => {
const customModels = formData.custom_models
.split(',').map((s) => s.trim()).filter(Boolean);
const payload: any = {
provider_type: selectedType.backendType,
provider_title: formData.provider_title,
provider_url: formData.provider_url,
provider_apikey: formData.provider_apikey,
};
if (customModels.length > 0) payload.custom_models = customModels;
return payload;
};
const handleTestConnection = async () => {
if (!formData.provider_url || !formData.provider_apikey) {
setError(t('agent.providerFillUrlAndKey'));
return;
}
setTestLoading(true);
setTestResult(null);
setError('');
try {
const resp = await apiClient.post('/api/v1/provider/test', buildPayload());
setTestResult(resp.data);
} catch {
setTestResult({ success: false, error: 'Request failed' });
} finally {
setTestLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.provider_title || !formData.provider_url || !formData.provider_apikey) {
@@ -37,9 +137,13 @@ export function ProvidersSettings() {
setSubmitLoading(true);
setError('');
try {
await apiClient.post('/api/v1/provider', formData);
if (editingProvider) {
await apiClient.delete(`/api/v1/provider/${editingProvider}`);
}
await apiClient.post('/api/v1/provider', buildPayload());
await fetchProviders();
setIsModalOpen(false);
setEditingProvider(null);
} catch (err) {
setError(t('agent.providerAddFailed'));
} finally {
@@ -54,7 +158,7 @@ export function ProvidersSettings() {
<h3 className="text-lg font-bold text-text-primary">{t('agent.providerManagement')}</h3>
<p className="text-sm text-text-muted mt-0.5">{t('agent.providerDesc')}</p>
</div>
<button onClick={() => { setFormData({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' }); setError(''); setIsModalOpen(true); }}
<button onClick={openAddModal}
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
<Plus size={14} /> {t('agent.addProvider')}
</button>
@@ -76,25 +180,43 @@ export function ProvidersSettings() {
<div key={i} className="bg-bg-card border border-border-primary rounded-2xl p-5 card-hover">
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-bg-secondary border border-border-secondary flex items-center justify-center">
<Box size={18} className="text-text-secondary" />
<div className="w-9 h-9 rounded-xl bg-bg-secondary border border-border-secondary flex items-center justify-center" style={{ color: getProviderBrandColor(provider.provider_type) }}>
<ProviderIcon type={provider.provider_type} size={20} />
</div>
<div>
<h4 className="font-semibold text-sm text-text-primary">{provider.provider_title}</h4>
<span className="text-[10px] text-text-muted font-mono uppercase">{provider.provider_type}</span>
</div>
</div>
<span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${provider.status === 'Connected' ? 'bg-success-bg text-success border-success/20' : 'bg-bg-secondary text-text-muted border-border-primary'}`}>
{provider.status === 'Connected' && <span className="w-1 h-1 rounded-full bg-success" />}
{provider.status || t('common.unknown')}
<span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${provider.status === 'Connected' || provider.provider_status === 'up' ? 'bg-success-bg text-success border-success/20' : 'bg-bg-secondary text-text-muted border-border-primary'}`}>
{(provider.status === 'Connected' || provider.provider_status === 'up') && <span className="w-1 h-1 rounded-full bg-success" />}
{provider.provider_status === 'up' ? 'Connected' : provider.status || t('common.unknown')}
</span>
</div>
<div className="bg-bg-secondary rounded-lg px-3 py-2 mb-4">
<div className="bg-bg-secondary rounded-lg px-3 py-2 mb-3">
<p className="text-[10px] text-text-muted mb-0.5">{t('agent.endpoint')}</p>
<p className="text-xs font-mono text-text-secondary truncate">{provider.provider_url || t('common.default')}</p>
</div>
{provider.provider_models && provider.provider_models.length > 0 && (
<div className="mb-3">
<button
onClick={() => setExpandedProvider(expandedProvider === provider.provider_title ? null : provider.provider_title)}
className="flex items-center gap-1.5 text-[11px] text-text-muted hover:text-accent transition-colors"
>
{expandedProvider === provider.provider_title ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
{provider.provider_models.length} {t('agent.providerModels')}
</button>
{expandedProvider === provider.provider_title && (
<div className="mt-2 bg-bg-secondary rounded-lg p-2 max-h-32 overflow-y-auto">
{provider.provider_models.map((model: string) => (
<div key={model} className="text-[11px] font-mono text-text-secondary py-0.5 px-1.5">{model}</div>
))}
</div>
)}
</div>
)}
<div className="flex justify-end gap-2">
<button className="px-3 py-1.5 text-xs font-medium text-text-secondary bg-bg-secondary hover:bg-bg-hover rounded-lg transition-colors border border-border-primary">{t('common.edit')}</button>
<button onClick={() => openEditModal(provider)} className="px-3 py-1.5 text-xs font-medium text-text-secondary bg-bg-secondary hover:bg-bg-hover rounded-lg transition-colors border border-border-primary">{t('common.edit')}</button>
<button onClick={async () => {
if (!confirm(t('agent.deleteProviderConfirm'))) return;
try { await apiClient.delete(`/api/v1/provider/${provider.provider_title}`); fetchProviders(); } catch { alert(t('common.deleteFailed')); }
@@ -105,46 +227,171 @@ export function ProvidersSettings() {
</div>
)}
{/* Modal */}
{/* Modal — 2 column layout */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-border-primary animate-fade-in-scale">
<div className="flex justify-between items-center p-5 border-b border-border-primary">
<div className="flex items-center gap-2">
<Server size={16} className="text-accent" />
<h3 className="text-base font-bold text-text-primary">{t('agent.addNewProvider')}</h3>
</div>
<button onClick={() => setIsModalOpen(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={18} /></button>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-3xl overflow-hidden border border-border-primary animate-fade-in-scale flex flex-col max-h-[90vh]">
<div className="flex justify-between items-center px-5 py-4 border-b border-border-primary shrink-0">
<h3 className="text-base font-bold text-text-primary">
{editingProvider ? t('agent.editProvider') : t('agent.addNewProvider')}
</h3>
<button onClick={() => { setIsModalOpen(false); setEditingProvider(null); }} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors">
<X size={18} />
</button>
</div>
<form onSubmit={handleSubmit} className="p-5 space-y-4">
{error && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>}
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.providerType')}</label>
<select name="provider_type" value={formData.provider_type} onChange={(e) => setFormData({...formData, provider_type: e.target.value})}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary">
<option value="openai">OpenAI</option>
<option value="deepseek">DeepSeek</option>
<option value="claude">Claude</option>
<option value="local">Local</option>
</select>
</div>
{['provider_title', 'provider_url', 'provider_apikey'].map((field) => (
<div key={field}>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{field === 'provider_title' ? t('agent.providerTitle') : field === 'provider_url' ? t('agent.baseUrl') : t('agent.apiKey')}
</label>
<input type={field === 'provider_apikey' ? 'password' : field === 'provider_url' ? 'url' : 'text'}
name={field} value={(formData as any)[field]}
onChange={(e) => setFormData({...formData, [field]: e.target.value})}
placeholder={field === 'provider_title' ? t('agent.providerTitlePlaceholder') : field === 'provider_url' ? t('agent.baseUrlPlaceholder') : t('agent.apiKeyPlaceholder')}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono" />
<form onSubmit={handleSubmit} className="flex flex-1 overflow-hidden">
{/* Left: provider type list */}
<div className="w-56 shrink-0 border-r border-border-primary bg-bg-secondary/40 overflow-y-auto p-3 space-y-1">
<div className="text-[10px] font-bold text-text-muted uppercase tracking-wider px-2 py-1.5">
{t('agent.providerType')}
</div>
{PROVIDER_TYPES.map((type) => {
const active = selectedTypeId === type.id;
return (
<button
key={type.id}
type="button"
onClick={() => handleSelectType(type.id)}
disabled={!!editingProvider}
className={`w-full flex items-center gap-3 px-2.5 py-2.5 rounded-lg text-left transition-all ${
active
? 'bg-bg-card border border-accent/40 shadow-sm'
: 'border border-transparent hover:bg-bg-card/60'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
<div
className="w-8 h-8 rounded-lg bg-bg-card border border-border-primary flex items-center justify-center shrink-0"
style={{ color: getProviderBrandColor(type.iconKey) }}
>
<ProviderIcon type={type.iconKey} size={18} />
</div>
<div className="flex-1 min-w-0">
<div className={`text-xs font-semibold truncate ${active ? 'text-accent' : 'text-text-primary'}`}>
{t(type.nameKey)}
</div>
<div className="text-[10px] text-text-muted truncate">{t(type.descKey)}</div>
</div>
</button>
);
})}
</div>
{/* Right: form */}
<div className="flex-1 overflow-y-auto p-5 space-y-4">
{error && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>}
{testResult && (
<div className={`p-3 text-sm rounded-xl border ${testResult.success ? 'bg-success-bg text-success border-success/20' : 'bg-danger-bg text-danger border-danger/20'}`}>
{testResult.success
? `${t('agent.providerTestSuccess')} · ${testResult.model_count} ${t('agent.providerModels')}`
: `${t('agent.providerTestFailed')}: ${testResult.error}`}
</div>
)}
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.providerTitle')}
</label>
<input
type="text"
value={formData.provider_title}
onChange={(e) => setFormData({ ...formData, provider_title: e.target.value })}
placeholder={t('agent.providerTitlePlaceholder')}
disabled={!!editingProvider}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 disabled:opacity-50"
/>
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.baseUrl')}
</label>
<input
type="text"
value={formData.provider_url}
onChange={(e) => setFormData({ ...formData, provider_url: e.target.value })}
placeholder={selectedType.defaultUrl || t('agent.baseUrlPlaceholder')}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono"
/>
{selectedType.defaultUrl && !editingProvider && (
<p className="text-[10px] text-text-muted mt-1">
{t('agent.baseUrlHint')}: <span className="font-mono">{selectedType.defaultUrl}</span>
</p>
)}
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.apiKey')}
</label>
<input
type="password"
value={formData.provider_apikey}
onChange={(e) => setFormData({ ...formData, provider_apikey: e.target.value })}
placeholder={editingProvider ? t('agent.apiKeyEditPlaceholder') : t('agent.apiKeyPlaceholder')}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono"
/>
</div>
{/* 参数设置 — collapsible */}
<div className="border border-border-primary rounded-xl overflow-hidden">
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="w-full flex items-center justify-between px-3.5 py-2.5 bg-bg-secondary/50 hover:bg-bg-secondary transition-colors"
>
<div className="flex items-center gap-2 text-xs font-semibold text-text-secondary">
<SettingsIcon size={13} />
{t('agent.providerAdvanced')}
</div>
{showAdvanced ? <ChevronDown size={14} className="text-text-muted" /> : <ChevronRight size={14} className="text-text-muted" />}
</button>
{showAdvanced && (
<div className="p-4 space-y-3 bg-bg-card">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.providerCustomModels')}
</label>
<textarea
value={formData.custom_models}
onChange={(e) => setFormData({ ...formData, custom_models: e.target.value })}
placeholder={t('agent.providerCustomModelsPlaceholder')}
rows={3}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono resize-none"
/>
<p className="text-[10px] text-text-muted mt-1">{t('agent.providerCustomModelsHint')}</p>
</div>
</div>
)}
</div>
))}
<div className="pt-2 flex justify-end gap-2">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">{t('common.cancel')}</button>
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">{submitLoading ? t('common.saving') : t('agent.addProvider')}</button>
</div>
</form>
{/* Footer */}
<div className="px-5 py-4 border-t border-border-primary bg-bg-secondary/30 flex items-center justify-between shrink-0">
<button
type="button"
onClick={handleTestConnection}
disabled={testLoading}
className="flex items-center gap-1.5 px-3 py-2 text-xs font-medium text-accent bg-accent-light hover:bg-accent/20 rounded-xl transition-colors disabled:opacity-50"
>
{testLoading ? <Loader2 size={12} className="animate-spin" /> : <Zap size={12} />}
{t('agent.testConnection')}
</button>
<div className="flex gap-2">
<button type="button" onClick={() => { setIsModalOpen(false); setEditingProvider(null); }} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">
{t('common.cancel')}
</button>
<button
type="button"
onClick={handleSubmit}
disabled={submitLoading}
className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50"
>
{submitLoading ? t('common.saving') : editingProvider ? t('common.save') : t('agent.addProvider')}
</button>
</div>
</div>
</div>
</div>
)}
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { RefreshCw, Server, GitBranch, ChevronDown, ChevronRight } from 'lucide-react';
import { RefreshCw, Server, GitBranch, ChevronDown, ChevronRight, Pause, Play } from 'lucide-react';
import apiClient from '../../api/client';
interface EventLog {
@@ -47,6 +47,7 @@ const STATUS_STYLES: Record<string, { bg: string; text: string }> = {
export function SystemLogsView() {
const { t } = useTranslation();
const [tab, setTab] = useState<'system' | 'workflow'>('system');
const [autoRefresh, setAutoRefresh] = useState(true);
// System logs state
const [logs, setLogs] = useState<EventLog[]>([]);
@@ -63,7 +64,7 @@ export function SystemLogsView() {
const [wfLoading, setWfLoading] = useState(false);
const [expandedStep, setExpandedStep] = useState<number | null>(null);
const fetchLogs = async () => {
const fetchLogs = useCallback(async () => {
setLoading(true);
try {
const params = new URLSearchParams();
@@ -78,7 +79,7 @@ export function SystemLogsView() {
} finally {
setLoading(false);
}
};
}, [traceFilter, typeFilter, levelFilter]);
const fetchWorkflows = async () => {
try {
@@ -106,7 +107,13 @@ export function SystemLogsView() {
useEffect(() => {
if (tab === 'system') fetchLogs();
else fetchWorkflows();
}, [tab]);
}, [tab, fetchLogs]);
useEffect(() => {
if (tab !== 'system' || !autoRefresh) return;
const interval = setInterval(fetchLogs, 5000);
return () => clearInterval(interval);
}, [tab, autoRefresh, fetchLogs]);
useEffect(() => {
if (selectedTrace) fetchWorkflowDetail(selectedTrace);
@@ -143,6 +150,15 @@ export function SystemLogsView() {
{t('agent.workflowLogs')}
</button>
<div className="flex-1" />
{tab === 'system' && (
<button
onClick={() => setAutoRefresh(!autoRefresh)}
className={`p-2 rounded-lg transition-all ${autoRefresh ? 'text-accent bg-accent-light' : 'text-text-muted hover:text-accent hover:bg-accent-light'}`}
title={autoRefresh ? t('agent.logPauseRefresh') : t('agent.logResumeRefresh')}
>
{autoRefresh ? <Pause size={14} /> : <Play size={14} />}
</button>
)}
<button
onClick={() => tab === 'system' ? fetchLogs() : fetchWorkflows()}
disabled={loading || wfLoading}
+25 -1
View File
@@ -151,8 +151,15 @@
"addProvider": "Add Provider",
"noProviders": "No providers configured yet",
"providerFillAll": "Please fill in all fields.",
"providerFillUrlAndKey": "Please fill in URL and API Key first.",
"providerAddFailed": "Failed to add provider. Please check your inputs and try again.",
"deleteProviderConfirm": "Delete this provider?",
"testConnection": "Test Connection",
"providerTestSuccess": "Connection successful",
"providerTestFailed": "Connection failed",
"providerModels": "models",
"editProvider": "Edit Provider",
"apiKeyEditPlaceholder": "Enter new API key (leave blank to keep current)",
"providerType": "Type",
"providerTitle": "Title",
"baseUrl": "Base URL",
@@ -184,6 +191,8 @@
"logFilterAllTypes": "All event types",
"logFilterAllLevels": "All levels",
"logSearch": "Search",
"logPauseRefresh": "Pause auto-refresh",
"logResumeRefresh": "Resume auto-refresh",
"logLevel": "Level",
"logType": "Type",
"logNode": "Node",
@@ -224,7 +233,22 @@
"customPrompt": "Custom Prompt",
"customPromptPlaceholder": "Additional content appended after the default system prompt...",
"displayName": "Display Name",
"persona": "Persona"
"persona": "Persona",
"providerTypeOpenai": "OpenAI",
"providerTypeOpenaiDesc": "Official OpenAI API",
"providerTypeOpenaiCompat": "OpenAI-Compatible",
"providerTypeOpenaiCompatDesc": "Self-hosted or third-party compatible",
"providerTypeAnthropic": "Anthropic",
"providerTypeAnthropicDesc": "Claude family models",
"providerTypeGemini": "Gemini",
"providerTypeGeminiDesc": "Google AI models",
"providerTypeDeepseek": "DeepSeek",
"providerTypeDeepseekDesc": "DeepSeek official API",
"baseUrlHint": "Default",
"providerAdvanced": "Parameter Settings",
"providerCustomModels": "Custom Model List",
"providerCustomModelsPlaceholder": "Comma separated, e.g. gpt-4o, gpt-4o-mini",
"providerCustomModelsHint": "Optional. Leave empty to auto-fetch model list from provider."
},
"plugin": {
"toolManagement": "Toolset Center",
+25 -1
View File
@@ -151,8 +151,15 @@
"addProvider": "添加供应商",
"noProviders": "暂无已配置的供应商",
"providerFillAll": "请填写所有字段。",
"providerFillUrlAndKey": "请先填写 URL 和 API 密钥。",
"providerAddFailed": "添加供应商失败,请检查输入后重试。",
"deleteProviderConfirm": "确定要删除此供应商吗?",
"testConnection": "测试连接",
"providerTestSuccess": "连接成功",
"providerTestFailed": "连接失败",
"providerModels": "个模型",
"editProvider": "编辑供应商",
"apiKeyEditPlaceholder": "输入新密钥(留空保持现有密钥)",
"providerType": "类型",
"providerTitle": "名称",
"baseUrl": "基础 URL",
@@ -184,6 +191,8 @@
"logFilterAllTypes": "所有事件类型",
"logFilterAllLevels": "所有级别",
"logSearch": "查询",
"logPauseRefresh": "暂停自动刷新",
"logResumeRefresh": "恢复自动刷新",
"logLevel": "级别",
"logType": "类型",
"logNode": "节点",
@@ -224,7 +233,22 @@
"customPrompt": "附加人设",
"customPromptPlaceholder": "在默认系统提示词之后追加的自定义内容...",
"displayName": "显示名称",
"persona": "人设"
"persona": "人设",
"providerTypeOpenai": "OpenAI",
"providerTypeOpenaiDesc": "官方 OpenAI API",
"providerTypeOpenaiCompat": "OpenAI 兼容",
"providerTypeOpenaiCompatDesc": "自托管或第三方兼容服务",
"providerTypeAnthropic": "Anthropic",
"providerTypeAnthropicDesc": "Claude 系列模型",
"providerTypeGemini": "Gemini",
"providerTypeGeminiDesc": "Google AI 模型",
"providerTypeDeepseek": "DeepSeek",
"providerTypeDeepseekDesc": "DeepSeek 官方 API",
"baseUrlHint": "默认地址",
"providerAdvanced": "参数设置",
"providerCustomModels": "自定义模型列表",
"providerCustomModelsPlaceholder": "用逗号分隔,如:gpt-4o, gpt-4o-mini",
"providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单"
},
"plugin": {
"toolManagement": "工具集中心",
+2
View File
@@ -19,6 +19,7 @@ export interface Provider {
provider_url?: string;
provider_owner?: string;
provider_models?: string[];
provider_status?: string;
status?: string;
model?: string;
}
@@ -84,6 +85,7 @@ export interface WorkflowStep {
action: string;
desc: string;
status: string;
output?: string;
agent_id?: string;
}