refactor(admin): unify model center and Memory V2 as asset plugin cards

Align Providers and Memory V2 pages with asset-gateway card layout; fix
Memory V2 root layout and balanced grid on Asset Gateway.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-12 12:17:47 +08:00
parent 0358484942
commit 6bd0faac40
4 changed files with 904 additions and 388 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ export function AssetGatewayPage() {
<span>{config.enabled ? '已启用' : '已关闭'}</span>
</label>
</section>
<div className="asset-plugin-grid">
<div className="asset-plugin-grid asset-plugin-grid--balanced">
{config.plugins.map((plugin) => {
const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin);
const selectedKey = activeKeys.find((key) => key.id === draft.llmProviderKeyId);
+283 -183
View File
@@ -124,6 +124,17 @@ const BACKENDS: Array<{
];
const MODEL_BACKENDS: ModelBackendKey[] = ['mem0', 'letta', 'langgraph'];
const BACKEND_ACCENTS = ['blue', 'violet', 'green', 'amber'] as const;
const BACKEND_ICONS: Record<BackendKey, string> = {
pgvector: 'P',
qdrant: 'Q',
weaviate: 'W',
mem0: 'M',
letta: 'L',
neo4j: 'N',
redisStreams: 'R',
langgraph: 'G',
};
const MODEL_API_TYPES: Array<{ value: MemoryV2ModelApiType; label: string }> = [
{ value: 'chat', label: 'Chat API' },
{ value: 'response', label: 'Response API' },
@@ -231,6 +242,7 @@ export function MemoryV2Page() {
const [busyScope, setBusyScope] = useState<'global' | 'chatIntentRouter' | BackendKey | 'reload' | null>(null);
const [error, setError] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [expandedBackends, setExpandedBackends] = useState<Record<string, boolean>>({});
const load = useCallback(async () => {
setLoading(true);
@@ -288,6 +300,9 @@ export function MemoryV2Page() {
};
const updateBackendFlag = (backend: BackendKey) => (event: ChangeEvent<HTMLInputElement>) => {
if (!event.target.checked) {
setExpandedBackends((current) => ({ ...current, [backend]: false }));
}
setDraft((current) => ({
...current,
[backend]: { ...current[backend], enabled: event.target.checked },
@@ -398,9 +413,13 @@ export function MemoryV2Page() {
const currentRouterSection = current.chatIntentRouter;
const routerProviderKey = findProviderKey(providerKeys, String(routerSection.modelProviderKeyId ?? ''));
const routerAvailableModels = modelOptionsForProvider(routerProviderKey, globalModelSettings);
const enabledBackendCount = BACKENDS.filter((backend) => Boolean(current[backend.key]?.enabled)).length;
const updatedAtText = payload?.updatedAt
? new Date(payload.updatedAt).toLocaleString('zh-CN', { hour12: false })
: '未保存过数据库配置';
return (
<div className="admin-page">
<div className="admin-page memory-v2-page">
<div className="admin-page-head">
<h2>Memory V2 </h2>
<p className="muted"> Memory V2 `memind_adm` </p>
@@ -409,20 +428,47 @@ export function MemoryV2Page() {
{error && <p className="banner banner-error">{error}</p>}
{message && <p className="banner banner-info">{message}</p>}
<div className="admin-form">
<section className="admin-card">
<div className="admin-card-head">
<section className="asset-gateway-hero">
<div>
<div className="asset-kicker">MEMORY V2</div>
<h3></h3>
<p>
{draft.global.enabled ? '已启用' : '未启用'}
{' · '}
backend <strong className="mono">{String(draft.global.backend ?? 'legacy')}</strong>
{' · '}
backend {enabledBackendCount}/{BACKENDS.length}
{' · '}
{updatedAtText}
</p>
</div>
<div className="model-center-hero-actions">
<button
type="button"
className="ghost-btn"
onClick={() => void load()}
disabled={busyScope !== null && busyScope !== 'reload'}
>
{busyScope === 'reload' ? '加载中…' : '重新加载'}
</button>
</div>
</section>
<h3 className="model-center-section-title"></h3>
<div className="asset-plugin-grid memory-v2-top-grid">
<section className="asset-plugin-card asset-plugin-card--violet model-center-card memory-v2-card">
<div className="asset-plugin-card-head">
<div className="asset-plugin-icon" aria-hidden="true">M</div>
<div>
<h2></h2>
<p className="muted">{payload?.updatedAt ? new Date(payload.updatedAt).toLocaleString('zh-CN', { hour12: false }) : '未保存过数据库配置'}</p>
</div>
<div className="admin-actions">
<button type="button" className="send-btn" disabled={loading || busyScope !== null} onClick={() => void saveGlobalConfig()}>
{busyScope === 'global' ? '保存中...' : '保存全局'}
</button>
<h3></h3>
<p> Memory V2 Profile / Event Log / Vector fail-open </p>
</div>
<span className={`asset-status ${draft.global.enabled ? 'is-on' : ''}`}>
{draft.global.enabled ? '已启用' : '未启用'}
</span>
</div>
<div className="admin-form-grid">
<div className="memory-v2-toggle-grid">
<label className="inline-check">
<input type="checkbox" checked={Boolean(draft.global.enabled)} onChange={updateGlobalFlag('enabled')} />
Memory V2
@@ -443,8 +489,11 @@ export function MemoryV2Page() {
<input type="checkbox" checked={Boolean(draft.global.failOpen)} onChange={updateGlobalFlag('failOpen')} />
Memory fail-open
</label>
</div>
<div className="asset-plugin-fields">
<label>
<span> Backend</span>
Backend
<select value={String(draft.global.backend ?? 'legacy')} onChange={updateGlobalText('backend')}>
<option value="legacy">legacy</option>
{BACKENDS.map((backend) => (
@@ -453,39 +502,41 @@ export function MemoryV2Page() {
</select>
</label>
</div>
{!loading && (
<p className="muted">
<p className="model-center-card-note">
backend<strong>{String(current.global.backend ?? 'legacy')}</strong>
</p>
)}
<div className="asset-plugin-footer">
<span className={`asset-status ${draft.global.enabled ? 'is-on' : ''}`}>
{draft.global.enabled ? 'Memory V2 运行中' : 'Memory V2 关闭'}
</span>
<button type="button" className="send-btn" disabled={loading || busyScope !== null} onClick={() => void saveGlobalConfig()}>
{busyScope === 'global' ? '保存中...' : '保存全局'}
</button>
</div>
</section>
<section className="admin-card">
<div className="admin-card-head">
<section className="asset-plugin-card asset-plugin-card--blue model-center-card memory-v2-card">
<div className="asset-plugin-card-head">
<div className="asset-plugin-icon" aria-hidden="true">R</div>
<div>
<h2> LLM </h2>
<p className="muted">退 Direct Chat / Agent LLM Memory V2 </p>
</div>
<div className="admin-actions">
<label className="inline-check">
<input
type="checkbox"
checked={Boolean(routerSection.enabled)}
onChange={updateRouterFlag('enabled')}
/>
</label>
<button
type="button"
className="send-btn"
disabled={loading || busyScope !== null}
onClick={() => void saveRouterConfig()}
>
{busyScope === 'chatIntentRouter' ? '保存中...' : '保存路由'}
</button>
<h3> LLM </h3>
<p>退 Direct Chat / Agent LLM</p>
</div>
<label className="asset-mini-toggle" aria-label="启用前置 LLM 意图路由">
<input
type="checkbox"
checked={Boolean(routerSection.enabled)}
onChange={updateRouterFlag('enabled')}
/>
<span>{routerSection.enabled ? '启用' : '关闭'}</span>
</label>
</div>
<div className="admin-form-grid">
<div className="admin-form-grid memory-v2-fields">
<label>
<span></span>
<select
@@ -523,69 +574,96 @@ export function MemoryV2Page() {
))}
</select>
</label>
<label>
<span></span>
<input
type="number"
min="0"
max="1"
step="0.05"
value={String(routerSection.minConfidence ?? '0.65')}
onChange={updateRouterValue('minConfidence')}
/>
</label>
<label>
<span></span>
<input
type="number"
min="1"
max="50"
value={String(routerSection.memoryResolveLimit ?? '8')}
onChange={updateRouterValue('memoryResolveLimit')}
/>
</label>
<label>
<span> Ms</span>
<input
type="number"
min="0"
max="30000"
value={String(routerSection.timeoutMs ?? '1500')}
onChange={updateRouterValue('timeoutMs')}
/>
</label>
<label>
<span> / 退</span>
<select
value={String(routerSection.fallbackRoute ?? 'agent_orchestration')}
onChange={updateRouterValue('fallbackRoute')}
>
<option value="agent_orchestration">Agent </option>
<option value="direct_chat">Direct Chat</option>
</select>
</label>
<label className="inline-check">
<input
type="checkbox"
checked={Boolean(routerSection.memoryResolveEnabled)}
onChange={updateRouterFlag('memoryResolveEnabled')}
/>
Memory V2
</label>
</div>
<p className="muted">
<details className="memory-v2-advanced-details">
<summary></summary>
<div className="admin-form-grid memory-v2-fields">
<label>
<span></span>
<input
type="number"
min="0"
max="1"
step="0.05"
value={String(routerSection.minConfidence ?? '0.65')}
onChange={updateRouterValue('minConfidence')}
/>
</label>
<label>
<span></span>
<input
type="number"
min="1"
max="50"
value={String(routerSection.memoryResolveLimit ?? '8')}
onChange={updateRouterValue('memoryResolveLimit')}
/>
</label>
<label>
<span> Ms</span>
<input
type="number"
min="0"
max="30000"
value={String(routerSection.timeoutMs ?? '1500')}
onChange={updateRouterValue('timeoutMs')}
/>
</label>
<label>
<span> / 退</span>
<select
value={String(routerSection.fallbackRoute ?? 'agent_orchestration')}
onChange={updateRouterValue('fallbackRoute')}
>
<option value="agent_orchestration">Agent </option>
<option value="direct_chat">Direct Chat</option>
</select>
</label>
<label className="inline-check">
<input
type="checkbox"
checked={Boolean(routerSection.memoryResolveEnabled)}
onChange={updateRouterFlag('memoryResolveEnabled')}
/>
Memory V2
</label>
</div>
</details>
<p className="model-center-card-note">
<strong>{currentRouterSection.enabled ? '已启用' : '未启用'}</strong>
{' · '}
<strong>{describeEffectiveModelSource(routerSection, providerKeys, globalModelSettings)}</strong>
</p>
{!providerKeys.length && (
<p className="muted">
<p className="model-center-card-note">
key Provider key
</p>
)}
</section>
{BACKENDS.map((backend) => {
<div className="asset-plugin-footer">
<span className={`asset-status ${routerSection.enabled ? 'is-on' : ''}`}>
{routerSection.enabled ? '路由已启用' : '路由已关闭'}
</span>
<button
type="button"
className="send-btn"
disabled={loading || busyScope !== null}
onClick={() => void saveRouterConfig()}
>
{busyScope === 'chatIntentRouter' ? '保存中...' : '保存路由'}
</button>
</div>
</section>
</div>
<h3 className="model-center-section-title">Backend </h3>
<p className="model-center-card-note memory-v2-backend-intro">
backend backend
</p>
<div className="asset-plugin-grid memory-v2-backend-grid">
{BACKENDS.map((backend, index) => {
const section = draft[backend.key];
const currentSection = current[backend.key];
const providerKey = isModelBackend(backend.key)
@@ -594,23 +672,123 @@ export function MemoryV2Page() {
const availableModels = isModelBackend(backend.key)
? modelOptionsForProvider(providerKey, globalModelSettings)
: [];
const accent = BACKEND_ACCENTS[index % BACKEND_ACCENTS.length];
const isBackendOpen = Boolean(section.enabled) || Boolean(expandedBackends[backend.key]);
return (
<section key={backend.key} className="admin-card">
<div className="admin-card-head">
<div>
<h2>{backend.label}</h2>
<p className="muted"> backend </p>
<details
key={backend.key}
className={`asset-plugin-card asset-plugin-card--${accent} model-center-card memory-v2-card memory-v2-backend-details`}
open={isBackendOpen}
onToggle={(event) => {
const nextOpen = event.currentTarget.open;
setExpandedBackends((current) => ({
...current,
[backend.key]: nextOpen,
}));
}}
>
<summary className="memory-v2-backend-summary">
<div className="asset-plugin-icon" aria-hidden="true">{BACKEND_ICONS[backend.key]}</div>
<div className="memory-v2-backend-summary-main">
<h3>{backend.label}</h3>
<span className={`asset-status ${section.enabled ? 'is-on' : ''}`}>
{section.enabled ? '已启用' : '未启用 · 点击展开'}
</span>
</div>
<div className="admin-actions">
<label className="inline-check">
<input
type="checkbox"
checked={Boolean(section.enabled)}
onChange={updateBackendFlag(backend.key)}
/>
</label>
<label
className="asset-mini-toggle"
aria-label={`启用 ${backend.label}`}
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
>
<input
type="checkbox"
checked={Boolean(section.enabled)}
onChange={updateBackendFlag(backend.key)}
/>
<span>{section.enabled ? '启用' : '关闭'}</span>
</label>
</summary>
<div className="memory-v2-backend-body">
<div className="admin-form-grid memory-v2-fields">
{backend.fields.map((field) => {
const configured = Boolean(currentSection?.[`${field.key}Configured`]);
const masked = String(currentSection?.[`${field.key}Masked`] ?? '');
return (
<label key={field.key}>
<span>{field.label}</span>
<input
type={field.secret ? 'password' : 'text'}
value={String(section[field.key] ?? '')}
placeholder={field.secret && configured ? `已配置:${masked || '******'};留空保持不变` : ''}
onChange={updateBackendValue(backend.key, field.key)}
/>
</label>
);
})}
</div>
{isModelBackend(backend.key) && (
<>
<div className="admin-form-grid memory-v2-fields">
<label>
<span></span>
<select
value={String(section.modelProviderKeyId ?? '')}
onChange={updateModelProvider(backend.key)}
>
<option value=""></option>
{providerKeys.map((item) => (
<option key={item.id} value={item.id}>{providerLabel(item)}</option>
))}
</select>
</label>
<label>
<span></span>
<select
value={String(section.model ?? '')}
onChange={updateBackendValue(backend.key, 'model')}
>
<option value="">
{providerKey ? '使用当前 Provider 默认模型' : '跟随统一模型中心默认模型'}
</option>
{availableModels.map((model) => (
<option key={model} value={model}>{model}</option>
))}
</select>
</label>
<label>
<span></span>
<select
value={String(section.modelApiType ?? '')}
onChange={updateBackendValue(backend.key, 'modelApiType')}
>
<option value=""></option>
{supportedApiTypes.map((apiType) => (
<option key={apiType} value={apiType}>
{MODEL_API_TYPES.find((item) => item.value === apiType)?.label ?? apiType}
</option>
))}
</select>
</label>
</div>
<p className="model-center-card-note">
<strong>{describeEffectiveModelSource(section, providerKeys, globalModelSettings)}</strong>
</p>
{!providerKeys.length && (
<p className="model-center-card-note">
key provider key
</p>
)}
</>
)}
<div className="asset-plugin-footer">
<span className={`asset-status ${section.enabled ? 'is-on' : ''}`}>
{section.enabled ? `${backend.label} 已启用` : `${backend.label} 未启用`}
</span>
<button
type="button"
className="send-btn"
@@ -621,87 +799,9 @@ export function MemoryV2Page() {
</button>
</div>
</div>
<div className="admin-form-grid">
{backend.fields.map((field) => {
const configured = Boolean(currentSection?.[`${field.key}Configured`]);
const masked = String(currentSection?.[`${field.key}Masked`] ?? '');
return (
<label key={field.key}>
<span>{field.label}</span>
<input
type={field.secret ? 'password' : 'text'}
value={String(section[field.key] ?? '')}
placeholder={field.secret && configured ? `已配置:${masked || '******'};留空保持不变` : ''}
onChange={updateBackendValue(backend.key, field.key)}
/>
</label>
);
})}
</div>
{isModelBackend(backend.key) && (
<>
<div className="admin-form-grid" style={{ marginTop: 12 }}>
<label>
<span></span>
<select
value={String(section.modelProviderKeyId ?? '')}
onChange={updateModelProvider(backend.key)}
>
<option value=""></option>
{providerKeys.map((item) => (
<option key={item.id} value={item.id}>{providerLabel(item)}</option>
))}
</select>
</label>
<label>
<span></span>
<select
value={String(section.model ?? '')}
onChange={updateBackendValue(backend.key, 'model')}
>
<option value="">
{providerKey ? '使用当前 Provider 默认模型' : '跟随统一模型中心默认模型'}
</option>
{availableModels.map((model) => (
<option key={model} value={model}>{model}</option>
))}
</select>
</label>
<label>
<span></span>
<select
value={String(section.modelApiType ?? '')}
onChange={updateBackendValue(backend.key, 'modelApiType')}
>
<option value=""></option>
{supportedApiTypes.map((apiType) => (
<option key={apiType} value={apiType}>
{MODEL_API_TYPES.find((item) => item.value === apiType)?.label ?? apiType}
</option>
))}
</select>
</label>
</div>
<p className="muted">
<strong>{describeEffectiveModelSource(section, providerKeys, globalModelSettings)}</strong>
</p>
{!providerKeys.length && (
<p className="muted">
key provider key
</p>
)}
</>
)}
</section>
</details>
);
})}
<div className="admin-actions">
<button type="button" className="ghost-btn" onClick={() => void load()} disabled={busyScope !== null && busyScope !== 'reload'}>
</button>
</div>
</div>
</div>
);
+215 -160
View File
@@ -35,6 +35,14 @@ import type {
const CUSTOM_PROVIDER_ID = '__custom__';
const EXECUTOR_VISUALS: Record<string, { icon: string; accent: string }> = {
goose: { icon: 'G', accent: 'blue' },
aider: { icon: 'A', accent: 'amber' },
openhands: { icon: 'O', accent: 'violet' },
};
const PROVIDER_ACCENTS = ['blue', 'violet', 'green', 'amber'] as const;
const defaultProviderForm = {
providerId: CUSTOM_PROVIDER_ID,
name: '',
@@ -270,40 +278,41 @@ function ExecutorCard({
}
}, [model, selectedKey]);
const visual = EXECUTOR_VISUALS[binding.executor] ?? { icon: '✦', accent: 'blue' };
return (
<article className="executor-card">
<div className="admin-card-head">
<section className={`asset-plugin-card asset-plugin-card--${visual.accent} model-center-card`}>
<div className="asset-plugin-card-head">
<div className="asset-plugin-icon" aria-hidden="true">{visual.icon}</div>
<div>
<h3>{binding.executorLabel}</h3>
<p className="muted">{binding.executorDescription}</p>
<p>{binding.executorDescription}</p>
</div>
<label className="inline-check">
<label className="asset-mini-toggle" aria-label={`${binding.executorLabel} 启用状态`}>
<input type="checkbox" checked={enabled} onChange={(event) => setEnabled(event.target.checked)} />
<span>{enabled ? '启用' : '关闭'}</span>
</label>
</div>
<div className="executor-form-grid">
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
<option value=""></option>
{modelOptions.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
<button
type="button"
className="send-btn"
disabled={busy || (enabled && (!keyId || !model))}
onClick={() => void onSave(binding, keyId || null, model, enabled)}
>
</button>
<div className="asset-plugin-fields">
<label>
Provider
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
</label>
<label>
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
<option value=""></option>
{modelOptions.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</label>
</div>
<div className="executor-footer">
<div className="model-center-card-body">
<div className="executor-meta">
<span>{statusText(launchStatus)}</span>
{runtime?.ok ? <span>{runtime.providerName} · {runtime.model}</span> : <span>{runtime?.message ?? '运行配置未就绪'}</span>}
@@ -348,10 +357,24 @@ function ExecutorCard({
</div>
</>
)}
{launchStatus?.logFile ? <span className="mono executor-log">{launchStatus.logFile}</span> : null}
</div>
{launchStatus?.logFile ? <span className="mono executor-log">{launchStatus.logFile}</span> : null}
</article>
<div className="asset-plugin-footer">
<span className={`asset-status ${enabled ? 'is-on' : ''}`}>
{enabled ? '绑定启用中' : '绑定已关闭'}
</span>
<button
type="button"
className="send-btn"
disabled={busy || (enabled && (!keyId || !model))}
onClick={() => void onSave(binding, keyId || null, model, enabled)}
>
</button>
</div>
</section>
);
}
@@ -387,45 +410,61 @@ function VisionSection({
}, [model, selectedKey]);
return (
<section className="admin-card providers-panel">
<div className="admin-card-head providers-panel-head">
<div>
<h3></h3>
<p className="muted"> Qwen VL</p>
<div className="asset-plugin-grid model-center-vision-grid">
<section className="asset-plugin-card asset-plugin-card--green model-center-card">
<div className="asset-plugin-card-head">
<div className="asset-plugin-icon" aria-hidden="true"></div>
<div>
<h3></h3>
<p> Qwen VL</p>
</div>
<span className={`asset-status ${settings?.keyId ? 'is-on' : ''}`}>
{settings?.keyId ? `${settings.keyName} · ${settings.visionModel}` : '未配置'}
</span>
</div>
{settings?.keyId ? (
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void onClear()}>
</button>
<div className="asset-plugin-fields">
<label>
Provider
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
</label>
<label>
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
<option value=""></option>
{modelOptions.map((item) => (
<option key={item} value={item}>{item}</option>
))}
</select>
</label>
</div>
{!settings?.keyId ? (
<p className="model-center-card-note"></p>
) : null}
</div>
{settings?.keyId ? (
<p className="muted" style={{ marginBottom: '0.75rem' }}>
<strong>{settings.keyName}</strong> · <span className="mono">{settings.visionModel}</span>
</p>
) : (
<p className="muted" style={{ marginBottom: '0.75rem' }}></p>
)}
<div className="executor-form-grid">
<ProviderSelect keys={keys} value={keyId} onChange={setKeyId} />
<select value={model} onChange={(event) => setModel(event.target.value)} disabled={!keyId}>
<option value=""></option>
{modelOptions.map((item) => (
<option key={item} value={item}>{item}</option>
))}
</select>
<button
type="button"
className="send-btn"
disabled={busy || !keyId || !model}
onClick={() => void onSave(keyId, model)}
>
</button>
</div>
</section>
<div className="asset-plugin-footer">
<span className={`asset-status ${settings?.keyId ? 'is-on' : ''}`}>
{settings?.keyId ? '已绑定视觉模型' : '等待配置'}
</span>
<div className="model-center-card-actions">
{settings?.keyId ? (
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void onClear()}>
</button>
) : null}
<button
type="button"
className="send-btn"
disabled={busy || !keyId || !model}
onClick={() => void onSave(keyId, model)}
>
</button>
</div>
</div>
</section>
</div>
);
}
@@ -723,123 +762,139 @@ export function ProvidersPage() {
}
};
const activeProviderCount = keys.filter((item) => item.status === 'active').length;
return (
<div className="admin-page providers-page">
<div className="admin-page-head providers-hero">
<div>
<h2></h2>
<p className="muted"> Provider</p>
</div>
<div className="providers-hero-meta">
<div className="providers-hero-stat">
<span className="providers-hero-label"></span>
<strong>{globalSettings?.globalModel ?? '未设置'}</strong>
</div>
<button type="button" className="send-btn" disabled={busy || loading} onClick={openCreateProvider}>
Provider
</button>
</div>
<div className="admin-page">
<div className="admin-page-head">
<h2></h2>
<p className="muted"> Provider</p>
</div>
{message && <p className="banner banner-info">{message}</p>}
{error && <p className="banner banner-error">{error}</p>}
<section className="admin-card providers-panel providers-panel-primary">
<div className="admin-card-head providers-panel-head">
<div>
<h3></h3>
<p className="muted"> Provider GooseAiderOpenHands</p>
</div>
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSync()}>
Goose
</button>
<section className="asset-gateway-hero">
<div>
<div className="asset-kicker">MODEL CENTER</div>
<h3> Provider </h3>
<p>
<strong className="mono">{globalSettings?.globalModel ?? '未设置'}</strong>
{' · '}
Provider {activeProviderCount}/{keys.length}
</p>
</div>
{loading ? (
<p className="muted">...</p>
) : (
<div className="executor-grid">
{bindings.map((binding) => (
<ExecutorCard
key={binding.executor}
binding={binding}
keys={activeKeys}
runtime={runtimes.find((item) => item.executor === binding.executor)}
plan={plans.find((item) => item.executor === binding.executor)}
launchStatus={launchStatuses[binding.executor]}
busy={busy}
onSave={handleSaveBinding}
onLaunch={handleLaunch}
onStop={handleStop}
onRestart={handleRestart}
/>
))}
</div>
)}
</section>
<section className="admin-card providers-panel providers-panel-secondary">
<div className="admin-card-head providers-panel-head">
<div>
<h3>Provider </h3>
<p className="muted"> list</p>
</div>
<div className="model-center-hero-actions">
<button type="button" className="ghost-btn" disabled={busy || loading} onClick={() => void load()}>
</button>
<button type="button" className="send-btn" disabled={busy || loading} onClick={openCreateProvider}>
Provider
</button>
</div>
</section>
<div className="providers-list">
{keys.map((row) => (
<article key={row.id} className="provider-list-item">
<div className="provider-list-main">
<div className="provider-list-top">
<div className="providers-name">
<span>{row.name}</span>
{row.isSelected ? <span className="risk-pill risk-low"></span> : null}
</div>
<span className={`provider-state ${row.status === 'active' ? 'is-active' : 'is-disabled'}`}>
{row.status === 'active' ? '可用' : '禁用'}
</span>
<div className="model-center-section-head">
<h3 className="model-center-section-title"></h3>
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSync()}>
Goose
</button>
</div>
{loading ? (
<p className="muted">...</p>
) : (
<div className="asset-plugin-grid">
{bindings.map((binding) => (
<ExecutorCard
key={binding.executor}
binding={binding}
keys={activeKeys}
runtime={runtimes.find((item) => item.executor === binding.executor)}
plan={plans.find((item) => item.executor === binding.executor)}
launchStatus={launchStatuses[binding.executor]}
busy={busy}
onSave={handleSaveBinding}
onLaunch={handleLaunch}
onStop={handleStop}
onRestart={handleRestart}
/>
))}
</div>
)}
<h3 className="model-center-section-title">Provider </h3>
<div className="asset-plugin-grid">
{keys.map((row, index) => {
const accent = PROVIDER_ACCENTS[index % PROVIDER_ACCENTS.length];
return (
<section key={row.id} className={`asset-plugin-card asset-plugin-card--${accent} model-center-card model-center-provider-card`}>
<div className="asset-plugin-card-head">
<div className="asset-plugin-icon" aria-hidden="true"></div>
<div>
<h3>{row.name}</h3>
<p>{row.providerKind === 'custom' ? row.apiUrl : row.providerLabel}</p>
</div>
<div className="provider-list-meta">
<span>{row.providerKind === 'custom' ? row.apiUrl : row.providerLabel}</span>
<span className={`asset-status ${row.status === 'active' ? 'is-on' : ''}`}>
{row.status === 'active' ? '可用' : '禁用'}
</span>
</div>
<div className="model-center-provider-badges">
{row.isSelected ? <span className="risk-pill risk-low"></span> : null}
{row.isVisionSelected ? <span className="risk-pill risk-medium"></span> : null}
</div>
<div className="asset-plugin-fields model-center-provider-meta">
<div className="model-center-meta-row">
<span></span>
<span className="mono">{row.defaultModel}</span>
</div>
<div className="model-center-meta-row">
<span>API Key</span>
<span className="mono">{row.apiKeyMasked}</span>
</div>
</div>
<div className="admin-actions providers-actions provider-list-actions">
<button
type="button"
className="ghost-btn"
disabled={busy}
onClick={() => setProviderDialog({ mode: 'edit', row, form: providerFormFromRow(row) })}
>
</button>
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleTestProvider(row)}>
</button>
{!row.isSelected && row.status === 'active' ? (
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSelectProvider(row.id)}>
<div className="asset-plugin-footer">
<span className={`asset-status ${row.status === 'active' ? 'is-on' : ''}`}>
{row.isSelected ? '当前默认 Provider' : '备用配置'}
</span>
<div className="model-center-card-actions">
<button
type="button"
className="ghost-btn"
disabled={busy}
onClick={() => setProviderDialog({ mode: 'edit', row, form: providerFormFromRow(row) })}
>
</button>
) : null}
<button
type="button"
className="ghost-btn"
disabled={busy || row.isSelected}
onClick={() => void handleToggleProviderStatus(row)}
>
{row.status === 'active' ? '禁用' : '恢复'}
</button>
<button type="button" className="danger-btn" disabled={busy || row.isSelected} onClick={() => void handleDeleteProvider(row)}>
</button>
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleTestProvider(row)}>
</button>
{!row.isSelected && row.status === 'active' ? (
<button type="button" className="ghost-btn" disabled={busy} onClick={() => void handleSelectProvider(row.id)}>
</button>
) : null}
<button
type="button"
className="ghost-btn"
disabled={busy || row.isSelected}
onClick={() => void handleToggleProviderStatus(row)}
>
{row.status === 'active' ? '禁用' : '恢复'}
</button>
<button type="button" className="danger-btn" disabled={busy || row.isSelected} onClick={() => void handleDeleteProvider(row)}>
</button>
</div>
</div>
</article>
))}
</div>
</section>
</section>
);
})}
</div>
<h3 className="model-center-section-title"></h3>
<VisionSection
keys={activeKeys}
settings={visionSettings}