mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-20 03:16:14 +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;
|
status: string;
|
||||||
components: PluginComponent[];
|
components: PluginComponent[];
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
|
hasUpdate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PluginCardVO implements IPluginCardVO {
|
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
|
install_info: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
status: string;
|
status: string;
|
||||||
components: PluginComponent[];
|
components: PluginComponent[];
|
||||||
|
hasUpdate?: boolean;
|
||||||
|
|
||||||
constructor(prop: IPluginCardVO) {
|
constructor(prop: IPluginCardVO) {
|
||||||
this.author = prop.author;
|
this.author = prop.author;
|
||||||
@@ -42,5 +44,6 @@ export class PluginCardVO implements IPluginCardVO {
|
|||||||
this.debug = prop.debug;
|
this.debug = prop.debug;
|
||||||
this.install_source = prop.install_source;
|
this.install_source = prop.install_source;
|
||||||
this.install_info = prop.install_info;
|
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 PluginReadme from '@/app/home/plugins/components/plugin-installed/plugin-readme/PluginReadme';
|
||||||
import styles from '@/app/home/plugins/plugins.module.css';
|
import styles from '@/app/home/plugins/plugins.module.css';
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
|
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||||
|
import { isNewerVersion } from '@/app/utils/versionCompare';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -72,10 +74,68 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
getPluginList();
|
getPluginList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPluginList() {
|
async function getPluginList() {
|
||||||
httpClient.getPlugins().then((value) => {
|
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(
|
setPluginList(
|
||||||
value.plugins.map((plugin) => {
|
installedPluginsResp.plugins.map((plugin) => {
|
||||||
return new PluginCardVO({
|
return new PluginCardVO({
|
||||||
author: plugin.manifest.manifest.metadata.author ?? '',
|
author: plugin.manifest.manifest.metadata.author ?? '',
|
||||||
label: extractI18nObject(plugin.manifest.manifest.metadata.label),
|
label: extractI18nObject(plugin.manifest.manifest.metadata.label),
|
||||||
@@ -97,7 +157,7 @@ const PluginInstalledComponent = forwardRef<PluginInstalledComponentRef>(
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
|
|||||||
+16
-6
@@ -159,12 +159,17 @@ export default function PluginCardComponent({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<div className="relative">
|
||||||
variant="ghost"
|
<Button
|
||||||
className="bg-white dark:bg-[#1f1f22] hover:bg-gray-100 dark:hover:bg-[#2a2a2d]"
|
variant="ghost"
|
||||||
>
|
className="bg-white dark:bg-[#1f1f22] hover:bg-gray-100 dark:hover:bg-[#2a2a2d]"
|
||||||
<Ellipsis className="w-4 h-4" />
|
>
|
||||||
</Button>
|
<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>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
{/**upgrade */}
|
{/**upgrade */}
|
||||||
@@ -179,6 +184,11 @@ export default function PluginCardComponent({
|
|||||||
>
|
>
|
||||||
<ArrowUp className="w-4 h-4" />
|
<ArrowUp className="w-4 h-4" />
|
||||||
<span>{t('plugins.update')}</span>
|
<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>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{/**view source */}
|
{/**view source */}
|
||||||
|
|||||||
@@ -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',
|
noComponents: 'No components',
|
||||||
delete: 'Delete Plugin',
|
delete: 'Delete Plugin',
|
||||||
update: 'Update Plugin',
|
update: 'Update Plugin',
|
||||||
|
new: 'New',
|
||||||
updateConfirm: 'Update Confirmation',
|
updateConfirm: 'Update Confirmation',
|
||||||
confirmUpdatePlugin:
|
confirmUpdatePlugin:
|
||||||
'Are you sure you want to update the plugin ({{author}}/{{name}})?',
|
'Are you sure you want to update the plugin ({{author}}/{{name}})?',
|
||||||
|
|||||||
@@ -289,6 +289,7 @@ const jaJP = {
|
|||||||
noComponents: '部品がありません',
|
noComponents: '部品がありません',
|
||||||
delete: 'プラグインを削除',
|
delete: 'プラグインを削除',
|
||||||
update: 'プラグインを更新',
|
update: 'プラグインを更新',
|
||||||
|
new: '新着',
|
||||||
updateConfirm: '更新の確認',
|
updateConfirm: '更新の確認',
|
||||||
confirmUpdatePlugin:
|
confirmUpdatePlugin:
|
||||||
'プラグイン「{{author}}/{{name}}」を更新してもよろしいですか?',
|
'プラグイン「{{author}}/{{name}}」を更新してもよろしいですか?',
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ const zhHans = {
|
|||||||
noComponents: '无组件',
|
noComponents: '无组件',
|
||||||
delete: '删除插件',
|
delete: '删除插件',
|
||||||
update: '更新插件',
|
update: '更新插件',
|
||||||
|
new: '新',
|
||||||
updateConfirm: '更新确认',
|
updateConfirm: '更新确认',
|
||||||
confirmUpdatePlugin: '你确定要更新插件({{author}}/{{name}})吗?',
|
confirmUpdatePlugin: '你确定要更新插件({{author}}/{{name}})吗?',
|
||||||
confirmUpdate: '确认更新',
|
confirmUpdate: '确认更新',
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ const zhHant = {
|
|||||||
noComponents: '無組件',
|
noComponents: '無組件',
|
||||||
delete: '刪除插件',
|
delete: '刪除插件',
|
||||||
update: '更新插件',
|
update: '更新插件',
|
||||||
|
new: '新',
|
||||||
updateConfirm: '更新確認',
|
updateConfirm: '更新確認',
|
||||||
confirmUpdatePlugin: '您確定要更新插件({{author}}/{{name}})嗎?',
|
confirmUpdatePlugin: '您確定要更新插件({{author}}/{{name}})嗎?',
|
||||||
confirmUpdate: '確認更新',
|
confirmUpdate: '確認更新',
|
||||||
|
|||||||
Reference in New Issue
Block a user