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 { UserDetailPage } from './admin/pages/UserDetailPage';
|
||||||
import { UsersPage } from './admin/pages/UsersPage';
|
import { UsersPage } from './admin/pages/UsersPage';
|
||||||
import { WechatPage } from './admin/pages/WechatPage';
|
import { WechatPage } from './admin/pages/WechatPage';
|
||||||
|
import { AssetGatewayPage } from './admin/pages/AssetGatewayPage';
|
||||||
import { BlockedWordsPage } from './admin/pages/BlockedWordsPage';
|
import { BlockedWordsPage } from './admin/pages/BlockedWordsPage';
|
||||||
import { defaultHomePath } from './lib/routes';
|
import { defaultHomePath } from './lib/routes';
|
||||||
import { OpsLayout } from './ops/components/OpsLayout';
|
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="memory-v2" element={<MemoryV2Page />} />
|
||||||
<Route path="providers" element={<ProvidersPage />} />
|
<Route path="providers" element={<ProvidersPage />} />
|
||||||
<Route path="wechat" element={<WechatPage />} />
|
<Route path="wechat" element={<WechatPage />} />
|
||||||
|
<Route path="asset-gateway" element={<AssetGatewayPage />} />
|
||||||
<Route path="blocked-words" element={<BlockedWordsPage />} />
|
<Route path="blocked-words" element={<BlockedWordsPage />} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
@@ -168,6 +170,7 @@ function loginRedirectPath(pathname: string, role: string | undefined) {
|
|||||||
|| pathname.startsWith('/memory-v2')
|
|| pathname.startsWith('/memory-v2')
|
||||||
|| pathname.startsWith('/providers')
|
|| pathname.startsWith('/providers')
|
||||||
|| pathname.startsWith('/wechat')
|
|| pathname.startsWith('/wechat')
|
||||||
|
|| pathname.startsWith('/asset-gateway')
|
||||||
) {
|
) {
|
||||||
return role === 'admin' || role === undefined ? pathname : '/ops';
|
return role === 'admin' || role === undefined ? pathname : '/ops';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const NAV_SECTIONS: NavSection[] = [
|
|||||||
{ to: '/skills', label: '技能' },
|
{ to: '/skills', label: '技能' },
|
||||||
{ to: '/policies', label: '策略' },
|
{ to: '/policies', label: '策略' },
|
||||||
{ to: '/providers', label: '统一模型中心' },
|
{ to: '/providers', label: '统一模型中心' },
|
||||||
|
{ to: '/asset-gateway', label: '资产能力' },
|
||||||
{ to: '/blocked-words', 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: '/users', label: '用户管理', desc: '创建账号、启用禁用' },
|
||||||
{ to: '/billing/recharge', label: '充值', desc: '为用户账户充值' },
|
{ to: '/billing/recharge', label: '充值', desc: '为用户账户充值' },
|
||||||
{ to: '/providers', label: '统一模型中心', desc: 'Provider、执行器模型与启动控制' },
|
{ to: '/providers', label: '统一模型中心', desc: 'Provider、执行器模型与启动控制' },
|
||||||
|
{ to: '/asset-gateway', label: '资产能力', desc: '可插拔素材插件、Provider 与专属 LLM 选择' },
|
||||||
{ to: '/mindspace', label: 'MindSpace 配置', desc: '公开页上限与空间发布参数' },
|
{ to: '/mindspace', label: 'MindSpace 配置', desc: '公开页上限与空间发布参数' },
|
||||||
{ to: '/memory-v2', label: 'Memory V2', desc: '长期记忆 backend 与前置路由开关' },
|
{ to: '/memory-v2', label: 'Memory V2', desc: '长期记忆 backend 与前置路由开关' },
|
||||||
{ to: '/system-tests', label: '系统测试验证', desc: '选择测试账号执行联调并汇总问题反馈' },
|
{ to: '/system-tests', label: '系统测试验证', desc: '选择测试账号执行联调并汇总问题反馈' },
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AssetGatewayConfig,
|
||||||
AdminDashboardSummary,
|
AdminDashboardSummary,
|
||||||
AdminServiceRestartAction,
|
AdminServiceRestartAction,
|
||||||
AdminServiceRestartResult,
|
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> {
|
export async function getMindSpaceAdminConfig(): Promise<MindSpaceAdminConfig> {
|
||||||
const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config');
|
const result = await portalFetch<{ config: MindSpaceAdminConfig }>('/admin-api/mindspace/config');
|
||||||
return result.config;
|
return result.config;
|
||||||
|
|||||||
@@ -195,6 +195,27 @@ export type LlmExecutorLaunchState = {
|
|||||||
instruction: string | null;
|
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 AdminServiceRestartAction = 'local_restart' | 'pro_restart';
|
||||||
|
|
||||||
export type AdminServiceRestartResult = {
|
export type AdminServiceRestartResult = {
|
||||||
|
|||||||
Reference in New Issue
Block a user