refactor(plugin): add support for plugin feature detection and routing

This commit is contained in:
LTbinglingfeng
2026-06-13 04:31:20 +08:00
Unverified
parent 6513903ff5
commit 227a5679ad
6 changed files with 106 additions and 50 deletions
+47 -42
View File
@@ -300,6 +300,7 @@ export function MainLayout() {
const logout = useAuthStore((state) => state.logout);
const connectionStatus = useAuthStore((state) => state.connectionStatus);
const apiBase = useAuthStore((state) => state.apiBase);
const supportsPlugin = useAuthStore((state) => state.supportsPlugin);
const fetchConfig = useConfigStore((state) => state.fetchConfig);
const clearCache = useConfigStore((state) => state.clearCache);
@@ -430,7 +431,7 @@ export function MainLayout() {
}, [fetchConfig]);
const loadPluginResources = useCallback(async () => {
if (connectionStatus !== 'connected') {
if (connectionStatus !== 'connected' || !supportsPlugin) {
setPluginResources([]);
return;
}
@@ -441,7 +442,7 @@ export function MainLayout() {
} catch {
setPluginResources([]);
}
}, [connectionStatus]);
}, [connectionStatus, supportsPlugin]);
useEffect(() => {
const timer = window.setTimeout(() => {
@@ -468,39 +469,39 @@ export function MainLayout() {
return groups;
}, []);
const pluginPageNavItems: SidebarNavItem[] = pluginResourceGroups.flatMap(
(group): SidebarNavItem[] => {
if (group.entries.length === 1) {
const resource = group.entries[0];
const pluginLogo = resolvePluginAssetURL(resource.pluginLogo, apiBase);
const pluginPageNavItems: SidebarNavItem[] = supportsPlugin
? pluginResourceGroups.flatMap((group): SidebarNavItem[] => {
if (group.entries.length === 1) {
const resource = group.entries[0];
const pluginLogo = resolvePluginAssetURL(resource.pluginLogo, apiBase);
return [
{
path: resource.route,
label: resource.label,
meta: resource.description,
icon: <PluginSidebarIcon src={pluginLogo} />,
},
];
}
const pluginLogo = resolvePluginAssetURL(group.entries[0]?.pluginLogo ?? '', apiBase);
return [
{
path: resource.route,
label: resource.label,
meta: resource.description,
kind: 'drawer',
id: `plugin-pages-${group.pluginID}`,
label: group.pluginTitle,
meta: t('plugin_resource.page_count', { count: group.entries.length }),
icon: <PluginSidebarIcon src={pluginLogo} />,
children: group.entries.map((resource) => ({
path: resource.route,
label: resource.label,
meta: resource.description,
icon: <span className="nav-sub-dot" aria-hidden="true" />,
})),
},
];
}
const pluginLogo = resolvePluginAssetURL(group.entries[0]?.pluginLogo ?? '', apiBase);
return [
{
kind: 'drawer',
id: `plugin-pages-${group.pluginID}`,
label: group.pluginTitle,
meta: t('plugin_resource.page_count', { count: group.entries.length }),
icon: <PluginSidebarIcon src={pluginLogo} />,
children: group.entries.map((resource) => ({
path: resource.route,
label: resource.label,
meta: resource.description,
icon: <span className="nav-sub-dot" aria-hidden="true" />,
})),
},
];
}
);
})
: [];
const navGroups: SidebarNavGroup[] = [
{
@@ -567,18 +568,22 @@ export function MainLayout() {
metaKey: 'nav_meta.config_management',
icon: sidebarIcons.config,
},
{
path: '/plugins',
labelKey: 'nav.plugins',
metaKey: 'nav_meta.plugins',
icon: sidebarIcons.plugins,
},
{
path: '/plugin-store',
labelKey: 'nav.plugin_store',
metaKey: 'nav_meta.plugin_store',
icon: sidebarIcons.pluginStore,
},
...(supportsPlugin
? [
{
path: '/plugins',
labelKey: 'nav.plugins',
metaKey: 'nav_meta.plugins',
icon: sidebarIcons.plugins,
},
{
path: '/plugin-store',
labelKey: 'nav.plugin_store',
metaKey: 'nav_meta.plugin_store',
icon: sidebarIcons.pluginStore,
},
]
: []),
{
path: '/system',
labelKey: 'nav.system_info',
+16 -6
View File
@@ -12,8 +12,9 @@ import { PluginStorePage } from '@/features/plugins/PluginStorePage';
import { ConfigPage } from '@/pages/ConfigPage';
import { LogsPage } from '@/pages/LogsPage';
import { SystemPage } from '@/pages/SystemPage';
import { useAuthStore } from '@/stores';
const mainRoutes = [
const createMainRoutes = (supportsPlugin: boolean) => [
{ path: '/', element: <DashboardPage /> },
{ path: '/dashboard', element: <DashboardPage /> },
{ path: '/settings', element: <Navigate to="/config" replace /> },
@@ -25,10 +26,18 @@ const mainRoutes = [
{ path: '/auth-files/oauth-model-alias', element: <AuthFilesOAuthModelAliasEditPage /> },
{ path: '/oauth', element: <OAuthPage /> },
{ path: '/quota', element: <QuotaPage /> },
{ path: '/plugin-pages/:pluginId/:menuIndex', element: <PluginResourcePage /> },
{ path: '/plugins', element: <PluginsPage /> },
{ path: '/plugin-store', element: <PluginStorePage /> },
{ path: '/plugins/*', element: <Navigate to="/plugins" replace /> },
...(supportsPlugin
? [
{ path: '/plugin-pages/:pluginId/:menuIndex', element: <PluginResourcePage /> },
{ path: '/plugins', element: <PluginsPage /> },
{ path: '/plugin-store', element: <PluginStorePage /> },
{ path: '/plugins/*', element: <Navigate to="/plugins" replace /> },
]
: [
{ path: '/plugin-pages/*', element: <Navigate to="/" replace /> },
{ path: '/plugins/*', element: <Navigate to="/" replace /> },
{ path: '/plugin-store', element: <Navigate to="/" replace /> },
]),
{ path: '/config', element: <ConfigPage /> },
{ path: '/logs', element: <LogsPage /> },
{ path: '/system', element: <SystemPage /> },
@@ -36,5 +45,6 @@ const mainRoutes = [
];
export function MainRoutes({ location }: { location?: Location }) {
return useRoutes(mainRoutes, location);
const supportsPlugin = useAuthStore((state) => state.supportsPlugin);
return useRoutes(createMainRoutes(supportsPlugin), location);
}
+22
View File
@@ -8,6 +8,7 @@ import type { ApiClientConfig, ApiError } from '@/types';
import {
BUILD_DATE_HEADER_KEYS,
CPA_BUILD_DATE_HEADER_KEYS,
CPA_SUPPORT_PLUGIN_HEADER_KEYS,
CPA_VERSION_HEADER_KEYS,
HOME_BUILD_DATE_HEADER_KEYS,
HOME_VERSION_HEADER_KEYS,
@@ -87,6 +88,19 @@ class ApiClient {
return null;
}
private readBooleanHeader(
headers: Record<string, unknown> | undefined,
keys: string[]
): boolean | null {
const value = this.readHeader(headers, keys);
if (value === null) return null;
const normalized = value.trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return null;
}
/**
* 设置请求/响应拦截器
*/
@@ -122,6 +136,7 @@ class ApiClient {
const version = homeVersion || cpaVersion || this.readHeader(headers, VERSION_HEADER_KEYS);
const buildDate =
homeBuildDate || cpaBuildDate || this.readHeader(headers, BUILD_DATE_HEADER_KEYS);
const supportsPlugin = this.readBooleanHeader(headers, CPA_SUPPORT_PLUGIN_HEADER_KEYS);
const runtimeKind: ServerRuntimeKind | null =
homeVersion || homeBuildDate ? 'home' : cpaVersion || cpaBuildDate ? 'cpa' : null;
@@ -133,6 +148,13 @@ class ApiClient {
})
);
}
if (supportsPlugin !== null) {
window.dispatchEvent(
new CustomEvent('server-plugin-support-update', {
detail: { supportsPlugin }
})
);
}
return response;
},
+19 -2
View File
@@ -29,6 +29,7 @@ interface AuthStoreState extends AuthState {
runtimeKind?: ServerRuntimeKind | null
) => void;
updateServerRuntimeKind: (runtimeKind: ServerRuntimeKind) => void;
updateServerPluginSupport: (supportsPlugin: boolean) => void;
updateConnectionStatus: (status: ConnectionStatus, error?: string | null) => void;
}
@@ -54,6 +55,7 @@ export const useAuthStore = create<AuthStoreState>()(
serverVersion: null,
serverBuildDate: null,
serverRuntimeKind: 'unknown',
supportsPlugin: false,
connectionStatus: 'disconnected',
connectionError: null,
@@ -113,7 +115,8 @@ export const useAuthStore = create<AuthStoreState>()(
connectionStatus: 'connecting',
serverVersion: null,
serverBuildDate: null,
serverRuntimeKind: 'unknown'
serverRuntimeKind: 'unknown',
supportsPlugin: false
});
useModelsStore.getState().clearCache();
@@ -169,6 +172,7 @@ export const useAuthStore = create<AuthStoreState>()(
serverVersion: null,
serverBuildDate: null,
serverRuntimeKind: 'unknown',
supportsPlugin: false,
connectionStatus: 'disconnected',
connectionError: null
});
@@ -186,6 +190,7 @@ export const useAuthStore = create<AuthStoreState>()(
try {
// 重新配置客户端
apiClient.setConfig({ apiBase, managementKey });
set({ supportsPlugin: false });
// 验证连接
await useConfigStore.getState().fetchConfig();
@@ -201,7 +206,8 @@ export const useAuthStore = create<AuthStoreState>()(
} catch {
set({
isAuthenticated: false,
connectionStatus: 'error'
connectionStatus: 'error',
supportsPlugin: false
});
return false;
}
@@ -220,6 +226,10 @@ export const useAuthStore = create<AuthStoreState>()(
set({ serverRuntimeKind: runtimeKind });
},
updateServerPluginSupport: (supportsPlugin) => {
set({ supportsPlugin });
},
// 更新连接状态
updateConnectionStatus: (status, error = null) => {
set({
@@ -273,4 +283,11 @@ if (typeof window !== 'undefined') {
.updateServerVersion(detail.version || null, detail.buildDate || null, runtimeKind);
}) as EventListener
);
window.addEventListener(
'server-plugin-support-update',
((e: CustomEvent) => {
useAuthStore.getState().updateServerPluginSupport(e.detail?.supportsPlugin === true);
}) as EventListener
);
}
+1
View File
@@ -19,6 +19,7 @@ export interface AuthState {
serverVersion: string | null;
serverBuildDate: string | null;
serverRuntimeKind: ServerRuntimeKind;
supportsPlugin: boolean;
}
// 连接状态
+1
View File
@@ -18,6 +18,7 @@ export const MANAGEMENT_API_PREFIX = '/v0/management';
export const REQUEST_TIMEOUT_MS = 30 * 1000;
export const CPA_VERSION_HEADER_KEYS = ['x-cpa-version'];
export const CPA_BUILD_DATE_HEADER_KEYS = ['x-cpa-build-date'];
export const CPA_SUPPORT_PLUGIN_HEADER_KEYS = ['x-cpa-support-plugin'];
export const HOME_VERSION_HEADER_KEYS = ['x-cpa-home-version'];
export const HOME_BUILD_DATE_HEADER_KEYS = ['x-cpa-home-build-date'];
export const VERSION_HEADER_KEYS = [