@@ -18,8 +18,10 @@ import {
1818 IconSidebarDashboard ,
1919 IconSidebarLogs ,
2020 IconSidebarOauth ,
21+ IconSidebarPlugins ,
2122 IconSidebarProviders ,
2223 IconSidebarQuota ,
24+ IconSidebarStore ,
2325 IconSidebarSystem ,
2426 IconSidebarUsage ,
2527} from '@/components/ui/icons' ;
@@ -34,6 +36,14 @@ import {
3436import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh' ;
3537import { LANGUAGE_LABEL_KEYS , LANGUAGE_ORDER } from '@/utils/constants' ;
3638import { isSupportedLanguage } from '@/utils/language' ;
39+ // v7:插件资源条目(用于在侧边栏展开插件提供的菜单)与支持检测
40+ import { pluginsApi } from '@/services/api' ;
41+ import {
42+ collectPluginResourceEntries ,
43+ PLUGIN_RESOURCES_REFRESH_EVENT ,
44+ resolvePluginAssetURL ,
45+ type PluginResourceEntry ,
46+ } from '@/features/plugins/pluginResources' ;
3747import type { Theme } from '@/types' ;
3848
3949const sidebarIcons : Record < string , ReactNode > = {
@@ -43,11 +53,37 @@ const sidebarIcons: Record<string, ReactNode> = {
4353 oauth : < IconSidebarOauth size = { 18 } /> ,
4454 quota : < IconSidebarQuota size = { 18 } /> ,
4555 usage : < IconSidebarUsage size = { 18 } /> ,
56+ // v7:插件管理 + 插件商店入口图标
57+ plugins : < IconSidebarPlugins size = { 18 } /> ,
58+ pluginStore : < IconSidebarStore size = { 18 } /> ,
4659 config : < IconSidebarConfig size = { 18 } /> ,
4760 logs : < IconSidebarLogs size = { 18 } /> ,
4861 system : < IconSidebarSystem size = { 18 } /> ,
4962} ;
5063
64+ // v7:插件资源条目的侧边栏图标——logo 加载失败时回退到通用插头图标
65+ function PluginSidebarIcon ( { src } : { src : string } ) {
66+ const [ failed , setFailed ] = useState ( false ) ;
67+ const showImage = Boolean ( src ) && ! failed ;
68+
69+ return showImage ? (
70+ < img
71+ src = { src }
72+ alt = ""
73+ onError = { ( ) => setFailed ( true ) }
74+ style = { { width : 18 , height : 18 , objectFit : 'cover' , borderRadius : 4 } }
75+ />
76+ ) : (
77+ < IconSidebarPlugins size = { 18 } />
78+ ) ;
79+ }
80+
81+ // 把插件 logo 转成完整 URL 后塞给 PluginSidebarIcon
82+ function pluginResourceLogo ( entry : PluginResourceEntry , apiBase : string ) {
83+ const src = resolvePluginAssetURL ( entry . pluginLogo , apiBase ) ;
84+ return < PluginSidebarIcon src = { src } /> ;
85+ }
86+
5187// Header action icons - smaller size for header buttons
5288const headerIconProps : SVGProps < SVGSVGElement > = {
5389 width : 16 ,
@@ -209,6 +245,8 @@ export function MainLayout() {
209245
210246 const apiBase = useAuthStore ( ( state ) => state . apiBase ) ;
211247 const connectionStatus = useAuthStore ( ( state ) => state . connectionStatus ) ;
248+ // v7:是否显示插件入口;连接成功后由下面的 effect 探测 /plugins 接口决定
249+ const supportsPlugin = useAuthStore ( ( state ) => state . supportsPlugin ) ;
212250 const logout = useAuthStore ( ( state ) => state . logout ) ;
213251
214252 const config = useConfigStore ( ( state ) => state . config ) ;
@@ -225,6 +263,9 @@ export function MainLayout() {
225263 const [ languageMenuOpen , setLanguageMenuOpen ] = useState ( false ) ;
226264 const [ themeMenuOpen , setThemeMenuOpen ] = useState ( false ) ;
227265 const [ brandExpanded , setBrandExpanded ] = useState ( true ) ;
266+ // v7:插件资源条目(来自 enabled 插件的 menus),由 PluginsPage 在
267+ // 启用/禁用/安装/删除后通过 PLUGIN_RESOURCES_REFRESH_EVENT 派发刷新
268+ const [ pluginResourceEntries , setPluginResourceEntries ] = useState < PluginResourceEntry [ ] > ( [ ] ) ;
228269 const contentRef = useRef < HTMLDivElement | null > ( null ) ;
229270 const languageMenuRef = useRef < HTMLDivElement | null > ( null ) ;
230271 const themeMenuRef = useRef < HTMLDivElement | null > ( null ) ;
@@ -409,6 +450,67 @@ export function MainLayout() {
409450 } ) ;
410451 } , [ fetchConfig ] ) ;
411452
453+ // v7:连接成功后探测一次 /plugins 接口,决定是否启用插件入口;
454+ // 后端不支持时返回 404/405,我们视为 supportsPlugin=false,避免空页面
455+ useEffect ( ( ) => {
456+ if ( connectionStatus !== 'connected' ) return ;
457+ let cancelled = false ;
458+
459+ const detect = async ( ) => {
460+ try {
461+ await pluginsApi . list ( ) ;
462+ if ( ! cancelled ) {
463+ window . dispatchEvent (
464+ new CustomEvent ( 'server-plugin-support-update' , { detail : { supportsPlugin : true } } )
465+ ) ;
466+ }
467+ } catch ( error : unknown ) {
468+ if ( cancelled ) return ;
469+ const status =
470+ typeof error === 'object' && error !== null && 'status' in error
471+ ? ( error as { status ?: unknown } ) . status
472+ : undefined ;
473+ // 仅 404/405 视为"后端无此接口";其它错误(401/500)保持当前状态由后续重试决定
474+ const supported = status !== 404 && status !== 405 ;
475+ window . dispatchEvent (
476+ new CustomEvent ( 'server-plugin-support-update' , { detail : { supportsPlugin : supported } } )
477+ ) ;
478+ }
479+ } ;
480+
481+ void detect ( ) ;
482+ return ( ) => {
483+ cancelled = true ;
484+ } ;
485+ } , [ connectionStatus ] ) ;
486+
487+ // v7:拉取/刷新插件资源条目;PluginsPage 任何变更后都会派发事件触发重建
488+ useEffect ( ( ) => {
489+ if ( ! supportsPlugin ) {
490+ setPluginResourceEntries ( [ ] ) ;
491+ return ;
492+ }
493+
494+ let cancelled = false ;
495+ const load = async ( ) => {
496+ try {
497+ const resp = await pluginsApi . list ( ) ;
498+ if ( cancelled ) return ;
499+ setPluginResourceEntries ( collectPluginResourceEntries ( resp . plugins ) ) ;
500+ } catch {
501+ if ( cancelled ) return ;
502+ setPluginResourceEntries ( [ ] ) ;
503+ }
504+ } ;
505+
506+ void load ( ) ;
507+ window . addEventListener ( PLUGIN_RESOURCES_REFRESH_EVENT , load ) ;
508+ return ( ) => {
509+ cancelled = true ;
510+ window . removeEventListener ( PLUGIN_RESOURCES_REFRESH_EVENT , load ) ;
511+ } ;
512+ } , [ supportsPlugin ] ) ;
513+
412514 const statusClass =
413515 connectionStatus === 'connected'
414516 ? 'success'
@@ -426,6 +528,19 @@ export function MainLayout() {
426528 { path : '/oauth' , label : t ( 'nav.oauth' , { defaultValue : 'OAuth' } ) , icon : sidebarIcons . oauth } ,
427529 { path : '/quota' , label : t ( 'nav.quota_management' ) , icon : sidebarIcons . quota } ,
428530 { path : '/usage' , label : t ( 'nav.usage_stats' ) , icon : sidebarIcons . usage } ,
531+ // v7:仅当后端支持插件 API 时才显示入口;顺序保持在 logs 之前
532+ ...( supportsPlugin
533+ ? [
534+ { path : '/plugins' , label : t ( 'nav.plugins' ) , icon : sidebarIcons . plugins } ,
535+ { path : '/plugin-store' , label : t ( 'nav.plugin_store' ) , icon : sidebarIcons . pluginStore } ,
536+ ]
537+ : [ ] ) ,
538+ // v7:展开的插件资源(每个 effective 插件的每个 menu),点击直达 iframe 页面
539+ ...pluginResourceEntries . map ( ( entry ) => ( {
540+ path : entry . route ,
541+ label : entry . label ,
542+ icon : pluginResourceLogo ( entry , apiBase ) ,
543+ } ) ) ,
429544 ...( config ?. loggingToFile
430545 ? [ { path : '/logs' , label : t ( 'nav.logs' ) , icon : sidebarIcons . logs } ]
431546 : [ ] ) ,
0 commit comments