From 3ab9ffb7b7b2db9c04ee7cfe8582ad1752f2d469 Mon Sep 17 00:00:00 2001 From: sheetung <30528385+sheetung@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:17:25 +0800 Subject: [PATCH] feat(plugins): add plugin new version detection (#1865) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(plugins): 添加插件更新检测功能 * perf: card style --------- Co-authored-by: Junyan Qin --- .../plugin-installed/PluginCardVO.ts | 3 + .../PluginInstalledComponent.tsx | 68 +++++++++++++++++-- .../plugin-card/PluginCardComponent.tsx | 22 ++++-- web/src/app/utils/versionCompare.ts | 45 ++++++++++++ web/src/i18n/locales/en-US.ts | 1 + web/src/i18n/locales/ja-JP.ts | 1 + web/src/i18n/locales/zh-Hans.ts | 1 + web/src/i18n/locales/zh-Hant.ts | 1 + 8 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 web/src/app/utils/versionCompare.ts diff --git a/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts b/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts index 9712cead..2be807c0 100644 --- a/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts +++ b/web/src/app/home/plugins/components/plugin-installed/PluginCardVO.ts @@ -13,6 +13,7 @@ export interface IPluginCardVO { status: string; components: PluginComponent[]; debug: boolean; + hasUpdate?: boolean; } export class PluginCardVO implements IPluginCardVO { @@ -28,6 +29,7 @@ export class PluginCardVO implements IPluginCardVO { install_info: Record; // eslint-disable-line @typescript-eslint/no-explicit-any status: string; components: PluginComponent[]; + hasUpdate?: boolean; constructor(prop: IPluginCardVO) { this.author = prop.author; @@ -42,5 +44,6 @@ export class PluginCardVO implements IPluginCardVO { this.debug = prop.debug; this.install_source = prop.install_source; this.install_info = prop.install_info; + this.hasUpdate = prop.hasUpdate; } } diff --git a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx index e18948c1..d8d1b3db 100644 --- a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx @@ -7,6 +7,8 @@ import PluginForm from '@/app/home/plugins/components/plugin-installed/plugin-fo import PluginReadme from '@/app/home/plugins/components/plugin-installed/plugin-readme/PluginReadme'; import styles from '@/app/home/plugins/plugins.module.css'; import { httpClient } from '@/app/infra/http/HttpClient'; +import { getCloudServiceClientSync } from '@/app/infra/http'; +import { isNewerVersion } from '@/app/utils/versionCompare'; import { Dialog, DialogContent, @@ -72,10 +74,68 @@ const PluginInstalledComponent = forwardRef( getPluginList(); } - function getPluginList() { - httpClient.getPlugins().then((value) => { + async function getPluginList() { + try { + // 获取已安装插件列表 + const installedPluginsResp = await httpClient.getPlugins(); + const installedPlugins = installedPluginsResp.plugins; + + // 获取市场插件列表 + const client = getCloudServiceClientSync(); + const marketplaceResp = await client.getMarketplacePlugins(1, 100); + const marketplacePlugins = marketplaceResp.plugins; + + // 创建市场插件映射,便于快速查找 + const marketplacePluginMap = new Map(); + marketplacePlugins.forEach((plugin) => { + const key = `${plugin.author}/${plugin.name}`; + marketplacePluginMap.set(key, plugin); + }); + + // 转换并比较版本号 + const pluginCards = installedPlugins.map((plugin) => { + const cardVO = new PluginCardVO({ + author: plugin.manifest.manifest.metadata.author ?? '', + label: extractI18nObject(plugin.manifest.manifest.metadata.label), + description: extractI18nObject( + plugin.manifest.manifest.metadata.description ?? { + en_US: '', + zh_Hans: '', + }, + ), + debug: plugin.debug, + enabled: plugin.enabled, + name: plugin.manifest.manifest.metadata.name, + version: plugin.manifest.manifest.metadata.version ?? '', + status: plugin.status, + components: plugin.components, + priority: plugin.priority, + install_source: plugin.install_source, + install_info: plugin.install_info, + }); + + // 检查是否来自市场且有更新 + if (cardVO.install_source === 'marketplace') { + const marketplaceKey = `${cardVO.author}/${cardVO.name}`; + const marketplacePlugin = marketplacePluginMap.get(marketplaceKey); + if (marketplacePlugin && marketplacePlugin.latest_version) { + cardVO.hasUpdate = isNewerVersion( + marketplacePlugin.latest_version, + cardVO.version, + ); + } + } + + return cardVO; + }); + + setPluginList(pluginCards); + } catch (error) { + console.error('获取插件列表失败:', error); + // 失败时仍显示已安装插件,不影响用户体验 + const installedPluginsResp = await httpClient.getPlugins(); setPluginList( - value.plugins.map((plugin) => { + installedPluginsResp.plugins.map((plugin) => { return new PluginCardVO({ author: plugin.manifest.manifest.metadata.author ?? '', label: extractI18nObject(plugin.manifest.manifest.metadata.label), @@ -97,7 +157,7 @@ const PluginInstalledComponent = forwardRef( }); }), ); - }); + } } useImperativeHandle(ref, () => ({ diff --git a/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx index b5d9c4b1..b29877c8 100644 --- a/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-installed/plugin-card/PluginCardComponent.tsx @@ -159,12 +159,17 @@ export default function PluginCardComponent({ }} > - +
+ + {cardVO.hasUpdate && ( +
+ )} +
{/**upgrade */} @@ -179,6 +184,11 @@ export default function PluginCardComponent({ > {t('plugins.update')} + {cardVO.hasUpdate && ( + + {t('plugins.new')} + + )} )} {/**view source */} diff --git a/web/src/app/utils/versionCompare.ts b/web/src/app/utils/versionCompare.ts new file mode 100644 index 00000000..7b28b7b7 --- /dev/null +++ b/web/src/app/utils/versionCompare.ts @@ -0,0 +1,45 @@ +/** + * Compare two version strings and determine if the first is newer than the second. + * Supports semantic versioning format (e.g., "1.2.3", "1.0.0-beta.1"). + * + * @param version1 - The version to compare (potentially newer) + * @param version2 - The version to compare against (base version) + * @returns true if version1 is newer than version2, false otherwise + */ +export function isNewerVersion(version1: string, version2: string): boolean { + if (!version1 || !version2) { + return false; + } + + // Remove any leading 'v' prefix + const v1 = version1.replace(/^v/, ''); + const v2 = version2.replace(/^v/, ''); + + // Split into main version and pre-release parts + const [main1, pre1] = v1.split('-'); + const [main2, pre2] = v2.split('-'); + + // Split main version into numeric parts + const parts1 = main1.split('.').map((p) => parseInt(p, 10) || 0); + const parts2 = main2.split('.').map((p) => parseInt(p, 10) || 0); + + // Normalize length + const maxLen = Math.max(parts1.length, parts2.length); + while (parts1.length < maxLen) parts1.push(0); + while (parts2.length < maxLen) parts2.push(0); + + // Compare main version parts + for (let i = 0; i < maxLen; i++) { + if (parts1[i] > parts2[i]) return true; + if (parts1[i] < parts2[i]) return false; + } + + // Main versions are equal, compare pre-release + // A version without pre-release is newer than one with pre-release + if (!pre1 && pre2) return true; + if (pre1 && !pre2) return false; + if (!pre1 && !pre2) return false; + + // Both have pre-release, compare lexicographically + return pre1! > pre2!; +} diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index dc4120c5..144744d2 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -288,6 +288,7 @@ const enUS = { noComponents: 'No components', delete: 'Delete Plugin', update: 'Update Plugin', + new: 'New', updateConfirm: 'Update Confirmation', confirmUpdatePlugin: 'Are you sure you want to update the plugin ({{author}}/{{name}})?', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index d96cc0cd..8d3ab932 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -289,6 +289,7 @@ const jaJP = { noComponents: '部品がありません', delete: 'プラグインを削除', update: 'プラグインを更新', + new: '新着', updateConfirm: '更新の確認', confirmUpdatePlugin: 'プラグイン「{{author}}/{{name}}」を更新してもよろしいですか?', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index c1e9815e..b967c4a3 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -275,6 +275,7 @@ const zhHans = { noComponents: '无组件', delete: '删除插件', update: '更新插件', + new: '新', updateConfirm: '更新确认', confirmUpdatePlugin: '你确定要更新插件({{author}}/{{name}})吗?', confirmUpdate: '确认更新', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index d55cc17c..e3fc323c 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -274,6 +274,7 @@ const zhHant = { noComponents: '無組件', delete: '刪除插件', update: '更新插件', + new: '新', updateConfirm: '更新確認', confirmUpdatePlugin: '您確定要更新插件({{author}}/{{name}})嗎?', confirmUpdate: '確認更新',