From a8a130c08fc77abd5eec50f6eb9019c86f5fcfb0 Mon Sep 17 00:00:00 2001 From: john Date: Sun, 12 Jul 2026 09:51:20 +0800 Subject: [PATCH 1/4] feat: add asset gateway admin controls --- src/App.tsx | 3 + src/admin/AdminNav.tsx | 1 + src/admin/pages/AssetGatewayPage.tsx | 129 +++++++++++++++++++++++++++ src/admin/pages/DashboardPage.tsx | 1 + src/api/client.ts | 29 ++++++ src/types.ts | 21 +++++ 6 files changed, 184 insertions(+) create mode 100644 src/admin/pages/AssetGatewayPage.tsx diff --git a/src/App.tsx b/src/App.tsx index 83d2ed9..e298a12 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,6 +14,7 @@ import { SystemTestsPage } from './admin/pages/SystemTestsPage'; import { UserDetailPage } from './admin/pages/UserDetailPage'; import { UsersPage } from './admin/pages/UsersPage'; import { WechatPage } from './admin/pages/WechatPage'; +import { AssetGatewayPage } from './admin/pages/AssetGatewayPage'; import { BlockedWordsPage } from './admin/pages/BlockedWordsPage'; import { defaultHomePath } from './lib/routes'; import { OpsLayout } from './ops/components/OpsLayout'; @@ -122,6 +123,7 @@ function AdminApp({ user, onLogout }: { user: PortalUser; onLogout: () => void } } /> } /> } /> + } /> } /> } /> @@ -168,6 +170,7 @@ function loginRedirectPath(pathname: string, role: string | undefined) { || pathname.startsWith('/memory-v2') || pathname.startsWith('/providers') || pathname.startsWith('/wechat') + || pathname.startsWith('/asset-gateway') ) { return role === 'admin' || role === undefined ? pathname : '/ops'; } diff --git a/src/admin/AdminNav.tsx b/src/admin/AdminNav.tsx index f35b0b8..42d14e9 100644 --- a/src/admin/AdminNav.tsx +++ b/src/admin/AdminNav.tsx @@ -34,6 +34,7 @@ const NAV_SECTIONS: NavSection[] = [ { to: '/skills', label: '技能' }, { to: '/policies', label: '策略' }, { to: '/providers', label: '统一模型中心' }, + { to: '/asset-gateway', label: '资产能力' }, { to: '/blocked-words', label: '违禁词管理' }, ], }, diff --git a/src/admin/pages/AssetGatewayPage.tsx b/src/admin/pages/AssetGatewayPage.tsx new file mode 100644 index 0000000..8cb7369 --- /dev/null +++ b/src/admin/pages/AssetGatewayPage.tsx @@ -0,0 +1,129 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + getAssetGatewayConfig, + listLlmProviderKeys, + updateAssetGatewayConfig, + updateAssetPluginConfig, +} from '../../api/client'; +import type { AssetGatewayConfig, AssetPluginConfig, LlmProviderKeyRow } from '../../types'; + +type PluginDraft = { + enabled: boolean; + provider: string; + llmProviderKeyId: string; + llmModel: string; +}; + +function draftFromPlugin(plugin: AssetPluginConfig): PluginDraft { + return { + enabled: plugin.enabled, + provider: plugin.provider ?? '', + llmProviderKeyId: plugin.llmProviderKeyId ?? '', + llmModel: plugin.llmModel ?? '', + }; +} + +export function AssetGatewayPage() { + const [config, setConfig] = useState(null); + const [keys, setKeys] = useState([]); + const [drafts, setDrafts] = useState>({}); + const [loading, setLoading] = useState(true); + const [busy, setBusy] = useState(null); + const [message, setMessage] = useState(null); + const [error, setError] = useState(null); + const activeKeys = useMemo(() => keys.filter((key) => key.status === 'active'), [keys]); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const [nextConfig, nextKeys] = await Promise.all([getAssetGatewayConfig(), listLlmProviderKeys()]); + setConfig(nextConfig); + setKeys(nextKeys); + setDrafts(Object.fromEntries(nextConfig.plugins.map((plugin) => [plugin.pluginId, draftFromPlugin(plugin)]))); + } catch (err) { + setError(err instanceof Error ? err.message : '加载资产能力配置失败'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { void load(); }, [load]); + + const updateDraft = (pluginId: string, patch: Partial) => { + setDrafts((current) => ({ ...current, [pluginId]: { ...current[pluginId], ...patch } })); + }; + + const saveGlobal = async (enabled: boolean) => { + setBusy('global'); setMessage(null); setError(null); + try { + const next = await updateAssetGatewayConfig({ enabled }); + setConfig(next); + setMessage(`资产能力总开关已${enabled ? '开启' : '关闭'}。`); + } catch (err) { setError(err instanceof Error ? err.message : '保存总开关失败'); } + finally { setBusy(null); } + }; + + const savePlugin = async (plugin: AssetPluginConfig) => { + const draft = drafts[plugin.pluginId]; + if (!draft) return; + setBusy(plugin.pluginId); setMessage(null); setError(null); + try { + const result = await updateAssetPluginConfig(plugin.pluginId, { + enabled: draft.enabled, + provider: draft.provider || null, + llmProviderKeyId: draft.llmProviderKeyId || null, + llmModel: draft.llmModel || null, + }); + setConfig(result.config); + setDrafts(Object.fromEntries(result.config.plugins.map((item) => [item.pluginId, draftFromPlugin(item)]))); + setMessage(`${plugin.label}配置已保存。`); + } catch (err) { setError(err instanceof Error ? err.message : '保存插件配置失败'); } + finally { setBusy(null); } + }; + + if (loading) return

加载资产能力配置…

; + if (!config) return

{error ?? '无法加载配置'}

; + + return ( +
+
+

资产能力

+

可插拔图片、素材与处理能力。默认关闭,不会影响现有聊天或页面生成流程。

+
+ {error &&

{error}

} + {message &&

{message}

} +
+

全局总开关

+ +

关闭时,所有插件安全降级为原有流程;插件配置将被保留。

+
+ {config.plugins.map((plugin) => { + const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin); + const selectedKey = activeKeys.find((key) => key.id === draft.llmProviderKeyId); + return
+

{plugin.label}

+

{plugin.description}

+
+ + + {plugin.supportsLlm ? <> + + + :

此插件不调用 LLM。

} + +
+
; + })} +
+ ); +} diff --git a/src/admin/pages/DashboardPage.tsx b/src/admin/pages/DashboardPage.tsx index 8ead9c9..d1220e9 100644 --- a/src/admin/pages/DashboardPage.tsx +++ b/src/admin/pages/DashboardPage.tsx @@ -9,6 +9,7 @@ const QUICK_LINKS = [ { to: '/users', label: '用户管理', desc: '创建账号、启用禁用' }, { to: '/billing/recharge', label: '充值', desc: '为用户账户充值' }, { to: '/providers', label: '统一模型中心', desc: 'Provider、执行器模型与启动控制' }, + { to: '/asset-gateway', label: '资产能力', desc: '可插拔素材插件、Provider 与专属 LLM 选择' }, { to: '/mindspace', label: 'MindSpace 配置', desc: '公开页上限与空间发布参数' }, { to: '/memory-v2', label: 'Memory V2', desc: '长期记忆 backend 与前置路由开关' }, { to: '/system-tests', label: '系统测试验证', desc: '选择测试账号执行联调并汇总问题反馈' }, diff --git a/src/api/client.ts b/src/api/client.ts index abfd32c..e438f17 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,4 +1,5 @@ import type { + AssetGatewayConfig, AdminDashboardSummary, AdminServiceRestartAction, AdminServiceRestartResult, @@ -279,6 +280,34 @@ export async function updateWechatScheduleLlmConfig( }); } +export async function getAssetGatewayConfig(): Promise { + return portalFetch('/admin-api/asset-gateway/config'); +} + +export async function updateAssetGatewayConfig( + payload: Pick, +): Promise { + return portalFetch('/admin-api/asset-gateway/config', { + method: 'PUT', + body: JSON.stringify(payload), + }); +} + +export async function updateAssetPluginConfig( + pluginId: string, + payload: { + enabled: boolean; + provider?: string | null; + llmProviderKeyId?: string | null; + llmModel?: string | null; + }, +): Promise<{ ok: boolean; config: AssetGatewayConfig }> { + return portalFetch(`/admin-api/asset-gateway/plugins/${encodeURIComponent(pluginId)}`, { + method: 'PUT', + body: JSON.stringify(payload), + }); +} + export async function getMindSpaceAdminConfig(): Promise { const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config'); return result.config; diff --git a/src/types.ts b/src/types.ts index 1d35e18..d2a8d3e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -195,6 +195,27 @@ export type LlmExecutorLaunchState = { instruction: string | null; }; +export type AssetPluginConfig = { + pluginId: string; + label: string; + description: string; + providers: string[]; + supportsLlm: boolean; + enabled: boolean; + provider: string | null; + llmProviderKeyId: string | null; + llmProviderName: string | null; + llmProviderId: string | null; + llmModel: string | null; + updatedAt: number | null; +}; + +export type AssetGatewayConfig = { + enabled: boolean; + updatedAt: number | null; + plugins: AssetPluginConfig[]; +}; + export type AdminServiceRestartAction = 'local_restart' | 'pro_restart'; export type AdminServiceRestartResult = { From 90cbcbba7c52018b24f0cbafbdb03f92f1b4b89a Mon Sep 17 00:00:00 2001 From: john Date: Sun, 12 Jul 2026 09:54:47 +0800 Subject: [PATCH 2/4] feat: bridge asset gateway controls to admin api --- server/app.mjs | 28 ++++++++++++++++++++++++++++ server/bootstrap.mjs | 5 +++++ server/index.mjs | 2 ++ 3 files changed, 35 insertions(+) diff --git a/server/app.mjs b/server/app.mjs index 0dcc25c..88c7fe4 100644 --- a/server/app.mjs +++ b/server/app.mjs @@ -91,6 +91,7 @@ export function createAdminApp(services) { const { userAuth, llmProviderService, + assetGatewayConfigService, pool, ready, wechatAdmin, @@ -325,6 +326,33 @@ export function createAdminApp(services) { res.json({ config: result }); }); + adminApi.get('/asset-gateway/config', requireAdmin, async (_req, res) => { + if (!assetGatewayConfigService?.getConfig) { + return res.status(503).json({ message: '资产能力配置服务未启用' }); + } + res.json(await assetGatewayConfigService.getConfig()); + }); + + adminApi.put('/asset-gateway/config', requireAdmin, async (req, res) => { + if (!assetGatewayConfigService?.updateGlobalConfig) { + return res.status(503).json({ message: '资产能力配置服务未启用' }); + } + res.json(await assetGatewayConfigService.updateGlobalConfig(req.body ?? {}, { + updatedBy: req.currentUser.id, + })); + }); + + adminApi.put('/asset-gateway/plugins/:pluginId', requireAdmin, async (req, res) => { + if (!assetGatewayConfigService?.updatePluginConfig) { + return res.status(503).json({ message: '资产能力配置服务未启用' }); + } + const result = await assetGatewayConfigService.updatePluginConfig(req.params.pluginId, req.body ?? {}, { + updatedBy: req.currentUser.id, + }); + if (!result.ok) return res.status(400).json({ message: result.message }); + res.json(result); + }); + adminApi.get('/memory-v2/config', requireAdmin, async (_req, res) => { if (!memoryV2ConfigService) return res.status(503).json({ message: 'Memory V2 配置未启用' }); res.json(await memoryV2ConfigService.getAdminConfig()); diff --git a/server/bootstrap.mjs b/server/bootstrap.mjs index b4f7c30..d6c1104 100644 --- a/server/bootstrap.mjs +++ b/server/bootstrap.mjs @@ -29,6 +29,8 @@ export async function bootstrapAdminServices() { updateMindSpaceConfig, } = await importMemind('mindspace-config.mjs'); const { createMemoryV2AdminConfigService } = await importMemind('memory-v2-admin-config.mjs'); + const { createAssetGatewayConfigService } = await importMemind('asset-gateway.mjs'); + const { ensureAssetGatewaySchema } = await importMemind('db.mjs'); const { createWechatScheduleLlmConfigService } = await importMemind('wechat-schedule-llm-config.mjs'); const { createAdminSystemTestService } = await importMemind('admin-system-tests.mjs'); const { createOpsApi } = await importMemind('admin-routes.mjs'); @@ -86,6 +88,8 @@ export async function bootstrapAdminServices() { apiTarget, apiSecret, }); + await ensureAssetGatewaySchema(pool); + const assetGatewayConfigService = createAssetGatewayConfigService(pool, { llmProviderService }); const memoryV2ConfigService = createMemoryV2AdminConfigService(pool, { env: process.env, }); @@ -133,6 +137,7 @@ export async function bootstrapAdminServices() { pool, userAuth, llmProviderService, + assetGatewayConfigService, plazaOps, createOpsApi, wechatAdmin, diff --git a/server/index.mjs b/server/index.mjs index 5f3d003..4f96cdf 100644 --- a/server/index.mjs +++ b/server/index.mjs @@ -98,6 +98,7 @@ ready pool, userAuth, llmProviderService, + assetGatewayConfigService, plazaOps, createOpsApi, wechatAdmin, @@ -120,6 +121,7 @@ ready pool, userAuth, llmProviderService, + assetGatewayConfigService, plazaOps, createOpsApi, wechatAdmin, From 035848494278ef0a5422547e3953647e682beb1a Mon Sep 17 00:00:00 2001 From: john Date: Sun, 12 Jul 2026 10:00:30 +0800 Subject: [PATCH 3/4] feat: present asset plugins as control cards --- src/admin/pages/AssetGatewayPage.tsx | 55 +++++++++++++++++++--------- src/index.css | 44 ++++++++++++++++++++++ 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/src/admin/pages/AssetGatewayPage.tsx b/src/admin/pages/AssetGatewayPage.tsx index 8cb7369..f252447 100644 --- a/src/admin/pages/AssetGatewayPage.tsx +++ b/src/admin/pages/AssetGatewayPage.tsx @@ -23,6 +23,13 @@ function draftFromPlugin(plugin: AssetPluginConfig): PluginDraft { }; } +const pluginVisuals: Record = { + 'asset-search': { icon: '⌕', accent: 'blue' }, + 'asset-generate': { icon: '✦', accent: 'violet' }, + 'asset-transform': { icon: '✧', accent: 'amber' }, + 'asset-analyze': { icon: '◉', accent: 'green' }, +}; + export function AssetGatewayPage() { const [config, setConfig] = useState(null); const [keys, setKeys] = useState([]); @@ -93,37 +100,51 @@ export function AssetGatewayPage() { {error &&

{error}

} {message &&

{message}

} -
-

全局总开关

- -

关闭时,所有插件安全降级为原有流程;插件配置将被保留。

+
+
+
OPTIONAL CAPABILITY
+

资产能力 Gateway

+

关闭时所有调用自动降级为现有流程;配置保留,现有聊天和页面生成不受影响。

+
+
+
{config.plugins.map((plugin) => { const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin); const selectedKey = activeKeys.find((key) => key.id === draft.llmProviderKeyId); - return
-

{plugin.label}

-

{plugin.description}

-
- - updateDraft(plugin.pluginId, { enabled: event.target.checked })} /> + {draft.enabled ? '开启' : '关闭'} + +
+
+ {plugin.supportsLlm ? <> - updateDraft(plugin.pluginId, { llmProviderKeyId: event.target.value, llmModel: '' })} disabled={!draft.enabled}> {activeKeys.map((key) => )} - - + + :
确定性处理 · 不调用 LLM
}
+
{draft.enabled ? '待保存为启用' : '默认安全关闭'}
; })} +
); } diff --git a/src/index.css b/src/index.css index 1377c83..5037d8e 100644 --- a/src/index.css +++ b/src/index.css @@ -835,6 +835,50 @@ body, gap: 8px; } +/* ── Asset gateway ───────────────────────────────────── */ + +.asset-gateway-hero { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 20px; + margin-bottom: 16px; + border: 1px solid color-mix(in srgb, var(--color-border) 72%, #7c5cff); + border-radius: var(--radius-lg); + background: linear-gradient(120deg, rgba(75, 55, 146, .28), rgba(21, 27, 38, .78)); +} + +.asset-kicker { color: #aaa1ff; font-size: 11px; font-weight: 700; letter-spacing: .12em; } +.asset-gateway-hero h3 { margin: 5px 0 6px; font-size: 18px; } +.asset-gateway-hero p { max-width: 620px; margin: 0; color: var(--color-text-muted); font-size: 13px; } +.asset-toggle, .asset-mini-toggle { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; font-size: 13px; } +.asset-toggle { padding: 9px 12px; border-radius: 999px; background: rgba(255,255,255,.07); } + +.asset-plugin-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(310px, 1fr)); gap: 14px; } +.asset-plugin-card { display: flex; flex-direction: column; gap: 16px; min-height: 326px; padding: 16px; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-bg-surface); box-shadow: 0 10px 28px rgba(0,0,0,.12); } +.asset-plugin-card--violet { border-top: 3px solid #9374ff; } +.asset-plugin-card--blue { border-top: 3px solid #54a8ff; } +.asset-plugin-card--amber { border-top: 3px solid #e8af55; } +.asset-plugin-card--green { border-top: 3px solid #55c79b; } +.asset-plugin-card-head { display: flex; gap: 10px; align-items: flex-start; } +.asset-plugin-icon { display: grid; flex: 0 0 38px; width: 38px; height: 38px; place-items: center; border-radius: 12px; background: rgba(132, 110, 255, .17); color: #c4b8ff; font-size: 22px; } +.asset-plugin-card-head h3 { margin: 0 0 4px; font-size: 15px; } +.asset-plugin-card-head p { margin: 0; color: var(--color-text-muted); font-size: 12px; line-height: 1.45; } +.asset-mini-toggle { margin-left: auto; color: var(--color-text-muted); font-size: 12px; } +.asset-plugin-fields { display: grid; gap: 9px; } +.asset-plugin-fields label { display: grid; gap: 5px; color: var(--color-text-muted); font-size: 11px; } +.asset-plugin-fields select { width: 100%; min-width: 0; } +.asset-no-llm { padding: 10px; border-radius: 8px; background: rgba(255,255,255,.04); color: var(--color-text-muted); font-size: 12px; } +.asset-plugin-footer { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: auto; padding-top: 12px; border-top: 1px solid var(--color-border); } +.asset-status { color: var(--color-text-muted); font-size: 11px; } +.asset-status.is-on { color: #68d8a7; } + +@media (max-width: 620px) { + .asset-gateway-hero { align-items: flex-start; flex-direction: column; } + .asset-plugin-grid { grid-template-columns: 1fr; } +} + .executor-note { margin: 0; color: var(--color-text-muted); From 6bd0faac40e758ade6dfc3a125666cbee20b053e Mon Sep 17 00:00:00 2001 From: john Date: Sun, 12 Jul 2026 12:17:47 +0800 Subject: [PATCH 4/4] 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 --- src/admin/pages/AssetGatewayPage.tsx | 2 +- src/admin/pages/MemoryV2Page.tsx | 466 ++++++++++++++++----------- src/admin/pages/ProvidersPage.tsx | 375 ++++++++++++--------- src/index.css | 449 +++++++++++++++++++++++--- 4 files changed, 904 insertions(+), 388 deletions(-) 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 {