diff --git a/src/admin/pages/AssetGatewayPage.tsx b/src/admin/pages/AssetGatewayPage.tsx index f252447..474be31 100644 --- a/src/admin/pages/AssetGatewayPage.tsx +++ b/src/admin/pages/AssetGatewayPage.tsx @@ -111,7 +111,7 @@ export function AssetGatewayPage() { {config.enabled ? '已启用' : '已关闭'} -
+
{config.plugins.map((plugin) => { const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin); const selectedKey = activeKeys.find((key) => key.id === draft.llmProviderKeyId); diff --git a/src/admin/pages/MemoryV2Page.tsx b/src/admin/pages/MemoryV2Page.tsx index 4109315..e805505 100644 --- a/src/admin/pages/MemoryV2Page.tsx +++ b/src/admin/pages/MemoryV2Page.tsx @@ -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 = { + 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(null); const [message, setMessage] = useState(null); + const [expandedBackends, setExpandedBackends] = useState>({}); const load = useCallback(async () => { setLoading(true); @@ -288,6 +300,9 @@ export function MemoryV2Page() { }; const updateBackendFlag = (backend: BackendKey) => (event: ChangeEvent) => { + 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 ( -
+

Memory V2 配置

这里是 Memory V2 的统一后台控制面。`memind_adm` 负责改配置,主站会从共享数据库读取并自动切换。

@@ -409,20 +428,47 @@ export function MemoryV2Page() { {error &&

{error}

} {message &&

{message}

} -
-
-
+
+
+
MEMORY V2
+

统一记忆控制面

+

+ 全局 {draft.global.enabled ? '已启用' : '未启用'} + {' · '} + 默认 backend {String(draft.global.backend ?? 'legacy')} + {' · '} + 已启用 backend {enabledBackendCount}/{BACKENDS.length} + {' · '} + 更新时间 {updatedAtText} +

+
+
+ +
+
+ +

全局与路由

+
+
+
+
-

全局开关

-

更新时间:{payload?.updatedAt ? new Date(payload.updatedAt).toLocaleString('zh-CN', { hour12: false }) : '未保存过数据库配置'}

-
-
- +

全局开关

+

控制 Memory V2 总开关、Profile / Event Log / Vector 与 fail-open 行为。

+ + {draft.global.enabled ? '已启用' : '未启用'} +
-
+ +
+
+ +
+ {!loading && ( -

+

当前数据库配置默认 backend:{String(current.global.backend ?? 'legacy')}

)} + +
+ + {draft.global.enabled ? 'Memory V2 运行中' : 'Memory V2 关闭'} + + +
-
-
+
+
+
-

前置 LLM 意图路由

-

关闭时主站完全退回原有 Direct Chat / Agent 判定;打开后才会调用轻量 LLM,并可读取 Memory V2 的轻量上下文。

-
-
- - +

前置 LLM 意图路由

+

关闭时主站完全退回原有 Direct Chat / Agent 判定;打开后才会调用轻量 LLM。

+
-
+ +
- - - -
-

+ +

+ 高级参数 +
+ + + + + +
+
+ +

当前数据库配置:{currentRouterSection.enabled ? '已启用' : '未启用'} {' · '} 生效模型链路:{describeEffectiveModelSource(routerSection, providerKeys, globalModelSettings)}

{!providerKeys.length && ( -

+

统一模型中心当前没有可用的激活 key;若启用路由,请先配置 Provider key 与模型列表。

)} -
- {BACKENDS.map((backend) => { +
+ + {routerSection.enabled ? '路由已启用' : '路由已关闭'} + + +
+
+
+ +

Backend 配置

+

+ 单独启用后,只有当全局默认 backend 指向它时才会成为主读写目标。未启用的 backend 默认折叠,点击标题可展开。 +

+
+ {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 ( -
-
-
-

{backend.label}

-

单独启用后,只有当全局默认 backend 指向它时才会成为主读写目标。

+
{ + const nextOpen = event.currentTarget.open; + setExpandedBackends((current) => ({ + ...current, + [backend.key]: nextOpen, + })); + }} + > + + +
+

{backend.label}

+ + {section.enabled ? '已启用' : '未启用 · 点击展开'} +
-
- + +
+ +
+
+ {backend.fields.map((field) => { + const configured = Boolean(currentSection?.[`${field.key}Configured`]); + const masked = String(currentSection?.[`${field.key}Masked`] ?? ''); + return ( + + ); + })} +
+ + {isModelBackend(backend.key) && ( + <> +
+ + + +
+

+ 生效模型:{describeEffectiveModelSource(section, providerKeys, globalModelSettings)} +

+ {!providerKeys.length && ( +

+ 统一模型中心当前没有可用的激活 key,请先到“统一模型中心”配置 provider key 与模型列表。 +

+ )} + + )} + +
+ + {section.enabled ? `${backend.label} 已启用` : `${backend.label} 未启用`} +
-
- {backend.fields.map((field) => { - const configured = Boolean(currentSection?.[`${field.key}Configured`]); - const masked = String(currentSection?.[`${field.key}Masked`] ?? ''); - return ( - - ); - })} -
- - {isModelBackend(backend.key) && ( - <> -
- - - -
-

- 当前生效模型链路:{describeEffectiveModelSource(section, providerKeys, globalModelSettings)} -

- {!providerKeys.length && ( -

- 统一模型中心当前没有可用的激活 key,请先到“统一模型中心”配置 provider key 与模型列表。 -

- )} - - )} -
+ ); })} - -
- -
); diff --git a/src/admin/pages/ProvidersPage.tsx b/src/admin/pages/ProvidersPage.tsx index 8af8919..ab92fbd 100644 --- a/src/admin/pages/ProvidersPage.tsx +++ b/src/admin/pages/ProvidersPage.tsx @@ -35,6 +35,14 @@ import type { const CUSTOM_PROVIDER_ID = '__custom__'; +const EXECUTOR_VISUALS: Record = { + 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 ( -
-
+
+
+

{binding.executorLabel}

-

{binding.executorDescription}

+

{binding.executorDescription}

-
-
- - - +
+ +
-
+
{statusText(launchStatus)} {runtime?.ok ? {runtime.providerName} · {runtime.model} : {runtime?.message ?? '运行配置未就绪'}} @@ -348,10 +357,24 @@ function ExecutorCard({
)} + + {launchStatus?.logFile ? {launchStatus.logFile} : null}
- {launchStatus?.logFile ? {launchStatus.logFile} : null} -
+
+ + {enabled ? '绑定启用中' : '绑定已关闭'} + + +
+
); } @@ -387,45 +410,61 @@ function VisionSection({ }, [model, selectedKey]); return ( -
-
-
-

图片任务模型

-

发送含图片的消息时自动切换到此模型(需支持视觉能力,如 Qwen VL)

+
+
+
+ +
+

图片任务模型

+

发送含图片的消息时自动切换到此模型(需支持视觉能力,如 Qwen VL)

+
+ + {settings?.keyId ? `${settings.keyName} · ${settings.visionModel}` : '未配置'} +
- {settings?.keyId ? ( - + +
+ + +
+ + {!settings?.keyId ? ( +

未配置时,图片消息将由默认模型处理。

) : null} -
- {settings?.keyId ? ( -

- 当前:{settings.keyName} · {settings.visionModel} -

- ) : ( -

未配置,图片消息将由默认模型处理

- )} - -
- - - -
-
+
+ + {settings?.keyId ? '已绑定视觉模型' : '等待配置'} + +
+ {settings?.keyId ? ( + + ) : null} + +
+
+ +
); } @@ -723,123 +762,139 @@ export function ProvidersPage() { } }; + const activeProviderCount = keys.filter((item) => item.status === 'active').length; + return ( -
-
-
-

统一模型中心

-

管理 Provider、执行器绑定和默认模型

-
-
-
- 当前默认 - {globalSettings?.globalModel ?? '未设置'} -
- -
+
+
+

统一模型中心

+

管理 Provider、执行器绑定和默认模型

{message &&

{message}

} {error &&

{error}

} -
-
-
-

执行器绑定

-

把 Provider 和模型绑定到 Goose、Aider、OpenHands

-
- +
+
+
MODEL CENTER
+

默认模型与 Provider 资源池

+

+ 当前默认模型 {globalSettings?.globalModel ?? '未设置'} + {' · '} + Provider 资源池 {activeProviderCount}/{keys.length} 可用 +

- {loading ? ( -

加载中...

- ) : ( -
- {bindings.map((binding) => ( - 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} - /> - ))} -
- )} -
- -
-
-
-

Provider 资源池

-

按名称纵向查看,更接近 list

-
+
+
+
-
- {keys.map((row) => ( -
-
-
-
- {row.name} - {row.isSelected ? 默认 : null} -
- - {row.status === 'active' ? '可用' : '禁用'} - +
+

执行器绑定

+ +
+ {loading ? ( +

加载中...

+ ) : ( +
+ {bindings.map((binding) => ( + 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} + /> + ))} +
+ )} + +

Provider 资源池

+
+ {keys.map((row, index) => { + const accent = PROVIDER_ACCENTS[index % PROVIDER_ACCENTS.length]; + return ( +
+
+ +
+

{row.name}

+

{row.providerKind === 'custom' ? row.apiUrl : row.providerLabel}

-
- {row.providerKind === 'custom' ? row.apiUrl : row.providerLabel} + + {row.status === 'active' ? '可用' : '禁用'} + +
+ +
+ {row.isSelected ? 默认 : null} + {row.isVisionSelected ? 图片任务 : null} +
+ +
+
+ 默认模型 {row.defaultModel} +
+
+ API Key {row.apiKeyMasked}
-
- - - {!row.isSelected && row.status === 'active' ? ( - - ) : null} - - + + {!row.isSelected && row.status === 'active' ? ( + + ) : null} + + +
-
- ))} -
-
+ + ); + })} +
+ +

图片任务模型

.asset-gateway-hero, +.memory-v2-page > .asset-plugin-grid, +.memory-v2-page > .model-center-section-title, +.memory-v2-page > .model-center-card-note, +.memory-v2-page > .banner { + width: 100%; + max-width: 100%; +} + +.memory-v2-page .memory-v2-fields input, +.memory-v2-page .memory-v2-fields select, +.memory-v2-page .asset-plugin-fields select { + padding: 10px 12px; + border-radius: var(--radius-md); + border: 1px solid var(--color-border-input); + background: var(--color-bg-base); + color: var(--color-text-primary); +} + +.memory-v2-top-grid { + grid-template-columns: minmax(0, 1fr); +} + +.memory-v2-backend-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.memory-v2-page .asset-plugin-grid { + gap: 12px; + margin-bottom: 12px; +} + +.memory-v2-page .asset-plugin-card { + gap: 10px; + padding: 12px 14px; + box-shadow: 0 6px 18px rgba(0, 0, 0, .08); +} + +.memory-v2-card { + min-height: 0; +} + +.memory-v2-backend-intro { + margin: -4px 0 10px; +} + +.memory-v2-backend-details { + display: flex; + flex-direction: column; +} + +.memory-v2-backend-summary { + display: flex; + gap: 10px; + align-items: center; + list-style: none; + cursor: pointer; +} + +.memory-v2-backend-summary::-webkit-details-marker { + display: none; +} + +.memory-v2-backend-summary-main { + display: grid; + gap: 2px; + flex: 1; + min-width: 0; +} + +.memory-v2-backend-summary-main h3 { + margin: 0; + font-size: 15px; +} + +.memory-v2-backend-body { + display: grid; + gap: 10px; + padding-top: 10px; + border-top: 1px solid var(--color-border); +} + +.memory-v2-advanced-details { + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: rgba(255, 255, 255, .02); +} + +.memory-v2-advanced-details > summary { + padding: 8px 10px; + color: var(--color-text-muted); + font-size: 12px; + cursor: pointer; + list-style: none; +} + +.memory-v2-advanced-details > summary::-webkit-details-marker { + display: none; +} + +.memory-v2-advanced-details .memory-v2-fields { + padding: 0 10px 10px; +} + +.memory-v2-toggle-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 8px 12px; +} + +.memory-v2-fields { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 10px 12px; +} + +.memory-v2-fields label { + display: grid; + gap: 5px; +} + +.memory-v2-fields label > span { + color: var(--color-text-muted); + font-size: 11px; +} + +.memory-v2-card .inline-check { + align-items: center; + color: var(--color-text-muted); + font-size: 12px; +} + +.memory-v2-page .model-center-card-note { + margin: 0; +} + +@media (min-width: 1280px) { + .memory-v2-backend-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 900px) { + .memory-v2-backend-grid { + grid-template-columns: minmax(0, 1fr); + } +} + +@media (max-width: 620px) { + .asset-gateway-hero, + .model-center-section-head { + align-items: flex-start; + flex-direction: column; + } + + .asset-plugin-grid { + grid-template-columns: 1fr; + } + + .asset-plugin-footer { + align-items: flex-start; + flex-direction: column; + } +} + /* ── Capability settings ─────────────────────────────── */ .capability-intro {