feat: add asset gateway admin controls
This commit is contained in:
@@ -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 }
|
||||
<Route path="memory-v2" element={<MemoryV2Page />} />
|
||||
<Route path="providers" element={<ProvidersPage />} />
|
||||
<Route path="wechat" element={<WechatPage />} />
|
||||
<Route path="asset-gateway" element={<AssetGatewayPage />} />
|
||||
<Route path="blocked-words" element={<BlockedWordsPage />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Route>
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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: '违禁词管理' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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<AssetGatewayConfig | null>(null);
|
||||
const [keys, setKeys] = useState<LlmProviderKeyRow[]>([]);
|
||||
const [drafts, setDrafts] = useState<Record<string, PluginDraft>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(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<PluginDraft>) => {
|
||||
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 <div className="admin-page"><p className="muted">加载资产能力配置…</p></div>;
|
||||
if (!config) return <div className="admin-page"><p className="banner banner-error">{error ?? '无法加载配置'}</p></div>;
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-head">
|
||||
<h2>资产能力</h2>
|
||||
<p className="muted">可插拔图片、素材与处理能力。默认关闭,不会影响现有聊天或页面生成流程。</p>
|
||||
</div>
|
||||
{error && <p className="banner banner-error">{error}</p>}
|
||||
{message && <p className="banner banner-success">{message}</p>}
|
||||
<section className="settings-card">
|
||||
<h3>全局总开关</h3>
|
||||
<label><input type="checkbox" checked={config.enabled} disabled={busy === 'global'} onChange={(event) => void saveGlobal(event.target.checked)} /> 启用资产能力 Gateway</label>
|
||||
<p className="muted">关闭时,所有插件安全降级为原有流程;插件配置将被保留。</p>
|
||||
</section>
|
||||
{config.plugins.map((plugin) => {
|
||||
const draft = drafts[plugin.pluginId] ?? draftFromPlugin(plugin);
|
||||
const selectedKey = activeKeys.find((key) => key.id === draft.llmProviderKeyId);
|
||||
return <section className="settings-card" key={plugin.pluginId}>
|
||||
<h3>{plugin.label}</h3>
|
||||
<p className="muted">{plugin.description}</p>
|
||||
<div className="executor-form-grid">
|
||||
<label><input type="checkbox" checked={draft.enabled} onChange={(event) => updateDraft(plugin.pluginId, { enabled: event.target.checked })} /> 启用插件</label>
|
||||
<select value={draft.provider} onChange={(event) => updateDraft(plugin.pluginId, { provider: event.target.value })} disabled={!draft.enabled}>
|
||||
<option value="">选择 Provider</option>
|
||||
{plugin.providers.map((provider) => <option key={provider} value={provider}>{provider}</option>)}
|
||||
</select>
|
||||
{plugin.supportsLlm ? <>
|
||||
<select value={draft.llmProviderKeyId} onChange={(event) => updateDraft(plugin.pluginId, { llmProviderKeyId: event.target.value, llmModel: '' })} disabled={!draft.enabled}>
|
||||
<option value="">不使用 LLM</option>
|
||||
{activeKeys.map((key) => <option key={key.id} value={key.id}>{key.name}</option>)}
|
||||
</select>
|
||||
<select value={draft.llmModel} onChange={(event) => updateDraft(plugin.pluginId, { llmModel: event.target.value })} disabled={!draft.enabled || !selectedKey}>
|
||||
<option value="">选择模型</option>
|
||||
{selectedKey?.models.map((model) => <option key={model} value={model}>{model}</option>)}
|
||||
</select>
|
||||
</> : <p className="muted">此插件不调用 LLM。</p>}
|
||||
<button type="button" className="send-btn" disabled={busy === plugin.pluginId} onClick={() => void savePlugin(plugin)}>{busy === plugin.pluginId ? '保存中…' : '保存插件'}</button>
|
||||
</div>
|
||||
</section>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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: '选择测试账号执行联调并汇总问题反馈' },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AssetGatewayConfig,
|
||||
AdminDashboardSummary,
|
||||
AdminServiceRestartAction,
|
||||
AdminServiceRestartResult,
|
||||
@@ -279,6 +280,34 @@ export async function updateWechatScheduleLlmConfig(
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAssetGatewayConfig(): Promise<AssetGatewayConfig> {
|
||||
return portalFetch('/admin-api/asset-gateway/config');
|
||||
}
|
||||
|
||||
export async function updateAssetGatewayConfig(
|
||||
payload: Pick<AssetGatewayConfig, 'enabled'>,
|
||||
): Promise<AssetGatewayConfig> {
|
||||
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<MindSpaceAdminConfig> {
|
||||
const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config');
|
||||
return result.config;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user