From a8a130c08fc77abd5eec50f6eb9019c86f5fcfb0 Mon Sep 17 00:00:00 2001 From: john Date: Sun, 12 Jul 2026 09:51:20 +0800 Subject: [PATCH] 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 = {