feat(model-provider): add plugin update indicators and migrate to oRPC contracts

Problem: Model provider settings page (/plugins?action=showSettings&tab=provider)
was missing plugin update indicators (red dot badge, Update button) that the
/plugins page correctly displayed, because it only fetched installation data
without querying for latest marketplace versions.

Decision: Extract a shared usePluginsWithLatestVersion hook and migrate plugin
API endpoints to oRPC contracts, ensuring both pages use identical data flows.

Model: Both pages now follow the same pattern — fetch installed plugins via
consoleQuery.plugins.checkInstalled, enrich with latest version metadata via
usePluginsWithLatestVersion, then pass complete PluginDetail objects downstream
where useDetailHeaderState computes hasNewVersion for UI indicators.

Impact:
- Update badge red dot and Update button now appear on provider settings page
- Shared hook eliminates 15 lines of duplicate enrichment logic in plugins-panel
- oRPC contracts replace legacy post() calls for plugin endpoints
- Operation dropdown uses auto-width to prevent "View on Marketplace" text wrapping
- Version badge aligned to use Badge component consistently across both pages
- Update button tooltip added with bilingual i18n support
- Deprecated Tooltip migrated to Base UI Tooltip in detail-header
This commit is contained in:
yyh
2026-03-10 23:28:09 +08:00
parent 3a957cc28b
commit 45c96dc254
13 changed files with 178 additions and 105 deletions

View File

@ -1,11 +1,14 @@
import type { CategoryKey, TagKey } from './constants'
import type { PluginDetail } from './types'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { consoleQuery } from '@/service/client'
import {
categoryKeys,
tagKeys,
} from './constants'
import { PluginCategoryEnum } from './types'
import { PluginCategoryEnum, PluginSource } from './types'
export type Tag = {
name: TagKey
@ -95,3 +98,39 @@ export const usePluginPageTabs = () => {
]
return tabs
}
const EMPTY_PLUGINS: PluginDetail[] = []
export function usePluginsWithLatestVersion(plugins: PluginDetail[] = EMPTY_PLUGINS): PluginDetail[] {
const marketplacePluginIds = useMemo(
() => plugins
.filter(p => p.source === PluginSource.marketplace)
.map(p => p.plugin_id),
[plugins],
)
const { data: latestVersionData } = useQuery(consoleQuery.plugins.latestVersions.queryOptions({
input: { body: { plugin_ids: marketplacePluginIds } },
enabled: !!marketplacePluginIds.length,
}))
return useMemo(() => {
const versions = latestVersionData?.versions
if (!versions)
return plugins
return plugins.map((plugin) => {
const info = versions[plugin.plugin_id]
if (!info)
return plugin
return {
...plugin,
latest_version: info.version,
latest_unique_identifier: info.unique_identifier,
status: info.status,
deprecated_reason: info.deprecated_reason,
alternative_plugin_id: info.alternative_plugin_id,
}
})
}, [plugins, latestVersionData])
}