mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat(plugins): add plugin new version detection (#1865)
* feat(plugins): 添加插件更新检测功能 * perf: card style --------- Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
@@ -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<string, any>; // 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PluginInstalledComponentRef>(
|
||||
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<PluginInstalledComponentRef>(
|
||||
});
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
|
||||
@@ -159,12 +159,17 @@ export default function PluginCardComponent({
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="bg-white dark:bg-[#1f1f22] hover:bg-gray-100 dark:hover:bg-[#2a2a2d]"
|
||||
>
|
||||
<Ellipsis className="w-4 h-4" />
|
||||
</Button>
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="bg-white dark:bg-[#1f1f22] hover:bg-gray-100 dark:hover:bg-[#2a2a2d]"
|
||||
>
|
||||
<Ellipsis className="w-4 h-4" />
|
||||
</Button>
|
||||
{cardVO.hasUpdate && (
|
||||
<div className="absolute -top-0.5 -right-0.5 w-2.5 h-2.5 bg-red-500 rounded-full border-2 border-white dark:border-[#1f1f22]"></div>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{/**upgrade */}
|
||||
@@ -179,6 +184,11 @@ export default function PluginCardComponent({
|
||||
>
|
||||
<ArrowUp className="w-4 h-4" />
|
||||
<span>{t('plugins.update')}</span>
|
||||
{cardVO.hasUpdate && (
|
||||
<Badge className="ml-auto bg-red-500 hover:bg-red-500 text-white text-[0.6rem] px-1.5 py-0 h-4">
|
||||
{t('plugins.new')}
|
||||
</Badge>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{/**view source */}
|
||||
|
||||
45
web/src/app/utils/versionCompare.ts
Normal file
45
web/src/app/utils/versionCompare.ts
Normal file
@@ -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!;
|
||||
}
|
||||
@@ -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}})?',
|
||||
|
||||
@@ -289,6 +289,7 @@ const jaJP = {
|
||||
noComponents: '部品がありません',
|
||||
delete: 'プラグインを削除',
|
||||
update: 'プラグインを更新',
|
||||
new: '新着',
|
||||
updateConfirm: '更新の確認',
|
||||
confirmUpdatePlugin:
|
||||
'プラグイン「{{author}}/{{name}}」を更新してもよろしいですか?',
|
||||
|
||||
@@ -275,6 +275,7 @@ const zhHans = {
|
||||
noComponents: '无组件',
|
||||
delete: '删除插件',
|
||||
update: '更新插件',
|
||||
new: '新',
|
||||
updateConfirm: '更新确认',
|
||||
confirmUpdatePlugin: '你确定要更新插件({{author}}/{{name}})吗?',
|
||||
confirmUpdate: '确认更新',
|
||||
|
||||
@@ -274,6 +274,7 @@ const zhHant = {
|
||||
noComponents: '無組件',
|
||||
delete: '刪除插件',
|
||||
update: '更新插件',
|
||||
new: '新',
|
||||
updateConfirm: '更新確認',
|
||||
confirmUpdatePlugin: '您確定要更新插件({{author}}/{{name}})嗎?',
|
||||
confirmUpdate: '確認更新',
|
||||
|
||||
Reference in New Issue
Block a user