feat: 分离本地插件和插件市场,缓存插件市场数据,插件市场搜索同时支持对描述进行搜索

This commit is contained in:
Soulter
2025-03-03 11:13:08 +08:00
parent 036c78750f
commit 175bb3ee01
6 changed files with 454 additions and 286 deletions
@@ -175,7 +175,7 @@ commonStore.getStartTime();
</script>
<template>
<v-app-bar elevation="0" height="70">
<v-app-bar elevation="0" height="55">
<v-btn style="margin-left: 22px;" class="hidden-md-and-down text-secondary" color="lightsecondary" icon rounded="sm"
variant="flat" @click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)" size="small">
@@ -36,10 +36,15 @@ const sidebarItem: menu[] = [
to: '/config',
},
{
title: '插件',
title: '插件管理',
icon: 'mdi-puzzle',
to: '/extension'
},
{
title: '插件市场',
icon: 'mdi-storefront',
to: '/extension-marketplace'
},
{
title: '聊天',
icon: 'mdi-chat',
+5
View File
@@ -16,6 +16,11 @@ const MainRoutes = {
path: '/extension',
component: () => import('@/views/ExtensionPage.vue')
},
{
name: 'ExtensionMarketplace',
path: '/extension-marketplace',
component: () => import('@/views/ExtensionMarketplace.vue')
},
{
name: 'Platforms',
path: '/platforms',
+32 -2
View File
@@ -18,7 +18,9 @@ export const useCommonStore = defineStore({
"gewechat": "https://astrbot.app/deploy/platform/gewechat.html",
"lark": "https://astrbot.app/deploy/platform/lark.html",
"telegram": "https://astrbot.app/deploy/platform/telegram.html",
}
},
pluginMarketData: []
}),
actions: {
@@ -52,6 +54,34 @@ export const useCommonStore = defineStore({
},
getTutorialLink(platform) {
return this.tutorial_map[platform]
}
},
async getPluginCollections(force = false) {
// 获取插件市场数据
if (!force && this.pluginMarketData.length > 0) {
return Promise.resolve(this.pluginMarketData);
}
return axios.get('/api/plugin/market_list')
.then((res) => {
let data = []
for (let key in res.data.data) {
data.push({
"name": key,
"desc": res.data.data[key].desc,
"author": res.data.data[key].author,
"repo": res.data.data[key].repo,
"installed": false,
"version": res.data.data[key]?.version ? res.data.data[key].version : "未知",
"social_link": res.data.data[key]?.social_link,
"tags": res.data.data[key]?.tags ? res.data.data[key].tags : []
})
}
this.pluginMarketData = data;
return data;
})
.catch((err) => {
this.toast("获取插件市场数据失败: " + err, "error");
return Promise.reject(err);
});
},
}
});
@@ -0,0 +1,396 @@
<script setup>
import ExtensionCard from '@/components/shared/ExtensionCard.vue';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import axios from 'axios';
import { useCommonStore } from '@/stores/common';
</script>
<template>
<v-row>
<v-col cols="12" md="12" v-if="announcement">
<v-banner color="success" lines="one" :text="announcement" :stacked="false">
</v-banner>
</v-col>
<v-col cols="12" md="12">
<v-card>
<v-card-title class="d-flex align-center pe-2">
🧩 插件市场
<v-btn icon size="small" style="margin-left: 8px" variant="plain" @click="jumpToPluginMarket()">
<v-icon size="small">mdi-help</v-icon>
<v-tooltip activator="parent" location="start">
<span>
如无法显示请单击此按钮跳转至插件市场复制想安装插件对应的
`repo`
链接然后点击右下角 + 号安装或打开链接下载压缩包安装
如果因为网络问题安装失败点击设置页选择 GitHub 加速地址或前往仓库下载压缩包然后本地上传
</span>
</v-tooltip>
</v-btn>
<v-btn icon @click="isListView = !isListView" size="small" style="margin-left: auto;"
variant="plain">
<v-icon>{{ isListView ? 'mdi-view-grid' : 'mdi-view-list' }}</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-text-field v-model="marketSearch" density="compact" label="Search"
prepend-inner-icon="mdi-magnify" variant="solo-filled" flat hide-details
single-line></v-text-field>
</v-card-title>
<v-divider></v-divider>
<template v-if="isListView">
<v-col cols="12" md="12">
<v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name"
v-model:search="marketSearch" :filter-keys="['name', 'desc']">
<template v-slot:item.name="{ item }">
<span v-if="item?.repo"><a :href="item?.repo"
style="color: #000; text-decoration:none">{{
item.name }}</a></span>
<span v-else>{{ item.name }}</span>
</template>
<template v-slot:item.author="{ item }">
<span v-if="item?.social_link"><a :href="item?.social_link">{{ item.author }}</a></span>
<span v-else>{{ item.author }}</span>
</template>
<template v-slot:item.tags="{ item }">
<span v-if="item.tags.length === 0"></span>
<v-chip v-for="tag in item.tags" :key="tag" color="primary" size="small">{{ tag
}}</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-btn v-if="!item.installed" class="text-none mr-2" size="small" text="Read"
variant="flat" border @click="extension_url = item.repo; newExtension()">安装</v-btn>
<v-btn v-else class="text-none mr-2" size="small" text="Read" variant="flat" border
disabled>已安装</v-btn>
</template>
</v-data-table>
</v-col>
</template>
<template v-else>
<v-row style="margin: 8px;">
<v-col cols="12" md="6" lg="3" v-for="plugin in filteredPluginMarketData">
<ExtensionCard :key="plugin.name" :title="plugin.name" :link="plugin.repo"
style="margin-bottom: 4px;">
<div style="min-height: 130px; max-height: 130px; overflow: hidden;">
<p style="font-weight: bold;">By @{{ plugin.author }}</p>
{{ plugin.desc }}
</div>
<div class="d-flex align-center gap-2">
<v-btn v-if="!plugin.installed" class="text-none mr-2" size="small" text="Read"
variant="flat" border
@click="extension_url = plugin.repo; newExtension()">安装</v-btn>
<v-btn v-else class="text-none mr-2" size="small" text="Read" variant="flat" border
disabled>已安装</v-btn>
</div>
</ExtensionCard>
</v-col>
</v-row>
</template>
</v-card>
</v-col>
<v-col style="margin-bottom: 16px;" cols="12" md="12">
<small><a href="https://astrbot.app/dev/plugin.html">插件开发文档</a></small> |
<small> <a href="https://github.com/Soulter/AstrBot_Plugins_Collection">提交插件仓库</a></small>
</v-col>
</v-row>
<v-dialog v-model="dialog" width="700">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus" size="x-large" style="position: fixed; right: 52px; bottom: 52px;"
color="darkprimary">
</v-btn>
</template>
<v-card>
<v-card-title>
<span class="text-h5">安装插件</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<h3> GitHub 上在线下载</h3>
<v-col cols="12">
<small>请输入合法的 GitHub 仓库链接当前仅支持
GitHubhttps://github.com/Soulter/astrbot_plugin_aiocqhttp</small>
<v-text-field label="仓库链接" v-model="extension_url" variant="outlined"
required></v-text-field>
</v-col>
</v-row>
<v-row>
<h3>从本机上传 .zip 压缩包</h3>
<v-col cols="12">
<small>请保证插件文件存在压缩包根目录中的第一个文件夹中即类似于从 GitHub 仓库页上下载的 Zip 压缩包的格式</small>
<v-file-input label="选择文件" v-model="upload_file" accept=".zip" outlined
required></v-file-input>
</v-col>
</v-row>
</v-container>
<br>
<small>{{ status }}</small>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="dialog = false">
关闭
</v-btn>
<v-btn color="blue-darken-1" variant="text" :loading="loading_" @click="newExtension()">
安装
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar :timeout="2000" elevation="24" :color="snack_success" v-model="snack_show">
{{ snack_message }}
</v-snackbar>
<WaitingForRestart ref="wfr"></WaitingForRestart>
</template>
<script>
export default {
name: 'ExtensionPage',
components: {
ExtensionCard,
WaitingForRestart,
ConsoleDisplayer,
AstrBotConfig
},
data() {
return {
extension_data: {
"data": [],
"message": ""
},
extension_url: "",
status: "",
dialog: false,
snack_message: "",
snack_show: false,
snack_success: "success",
loading_: false,
upload_file: null,
pluginMarketData: [],
loadingDialog: {
show: false,
title: "加载中...",
statusCode: 0, // 0: loading, 1: success, 2: error,
result: ""
},
announcement: "",
isListView: true,
pluginMarketHeaders: [
{ title: '名称', key: 'name', maxWidth: '150px' },
{ title: '描述', key: 'desc', maxWidth: '250px' },
{ title: '作者', key: 'author', maxWidth: '60px' },
{ title: '标签', key: 'tags', maxWidth: '60px' },
{ title: '操作', key: 'actions', sortable: false }
],
marketSearch: "",
commonStore: useCommonStore()
}
},
computed: {
filteredPluginMarketData() {
if (!this.marketSearch) {
return this.pluginMarketData;
}
const search = this.marketSearch.toLowerCase();
return this.pluginMarketData.filter(plugin =>
plugin.name.toLowerCase().includes(search)
);
}
},
mounted() {
// 获取本地插件数据
this.getExtensions();
// 获取插件市场数据
this.commonStore.getPluginCollections().then((data) => {
this.pluginMarketData = data;
this.checkAlreadyInstalled();
this.checkUpdate();
}).catch((err) => {
console.error("获取插件市场数据失败:", err);
});
axios.get('https://api.soulter.top/astrbot-announcement-plugin-market').then((res) => {
let data = res.data.data;
this.announcement = data.text;
});
},
methods: {
jumpToPluginMarket() {
window.open('https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json', '_blank');
},
toast(message, success) {
this.snack_message = message;
this.snack_show = true;
this.snack_success = success;
},
resetLoadingDialog() {
this.loadingDialog = {
show: false,
title: "加载中...",
statusCode: 0,
result: ""
}
},
onLoadingDialogResult(statusCode, result, timeToClose = 2000) {
this.loadingDialog.statusCode = statusCode;
this.loadingDialog.result = result;
if (timeToClose === -1) {
return
}
setTimeout(() => {
this.resetLoadingDialog()
}, timeToClose);
},
getExtensions() {
axios.get('/api/plugin/get').then((res) => {
this.extension_data = res.data;
this.checkAlreadyInstalled();
this.checkUpdate()
});
},
checkUpdate() {
// 创建在线插件的map
const onlinePluginsMap = new Map();
const onlinePluginsNameMap = new Map();
// 将在线插件信息存储到map中
this.pluginMarketData.forEach(plugin => {
if (plugin.repo) {
onlinePluginsMap.set(plugin.repo.toLowerCase(), plugin);
}
onlinePluginsNameMap.set(plugin.name, plugin);
});
// 遍历本地插件列表
this.extension_data.data.forEach(extension => {
// 通过repo或name查找在线版本
const repoKey = extension.repo?.toLowerCase();
const onlinePlugin = repoKey ? onlinePluginsMap.get(repoKey) : null;
const onlinePluginByName = onlinePluginsNameMap.get(extension.name);
const matchedPlugin = onlinePlugin || onlinePluginByName;
if (matchedPlugin) {
extension.online_version = matchedPlugin.version;
extension.has_update = extension.version !== matchedPlugin.version &&
matchedPlugin.version !== "未知";
} else {
extension.has_update = false;
}
});
},
newExtension() {
if (this.extension_url === "" && this.upload_file === null) {
this.toast("请填写插件链接或上传插件文件", "error");
return;
}
if (this.extension_url !== "" && this.upload_file !== null) {
this.toast("请不要同时填写插件链接和上传插件文件", "error");
return;
}
this.loading_ = true;
this.loadingDialog.show = true;
if (this.upload_file !== null) {
this.toast("正在从文件安装插件", "primary");
const formData = new FormData();
formData.append('file', this.upload_file);
axios.post('/api/plugin/install-upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then((res) => {
this.loading_ = false;
if (res.data.status === "error") {
this.onLoadingDialogResult(2, res.data.message, -1);
return;
}
this.extension_data = res.data;
this.upload_file = "";
this.onLoadingDialogResult(1, res.data.message);
this.dialog = false;
this.$refs.wfr.check();
}).catch((err) => {
this.loading_ = false;
this.onLoadingDialogResult(2, err, -1);
});
return;
} else {
this.toast("正在从链接 " + this.extension_url + " 安装插件...", "primary");
axios.post('/api/plugin/install',
{
url: this.extension_url,
proxy: localStorage.getItem('selectedGitHubProxy') || ""
}).then((res) => {
this.loading_ = false;
if (res.data.status === "error") {
this.onLoadingDialogResult(2, res.data.message, -1);
return;
}
this.extension_data = res.data;
this.extension_url = "";
this.onLoadingDialogResult(1, res.data.message);
this.dialog = false;
this.$refs.wfr.check();
}).catch((err) => {
this.loading_ = false;
this.onLoadingDialogResult(2, err, -1);
});
}
},
checkAlreadyInstalled() {
// 创建已安装插件的仓库和名称集合 统一格式
const installedRepos = new Set(this.extension_data.data.map(ext => ext.repo?.toLowerCase()));
const installedNames = new Set(this.extension_data.data.map(ext => ext.name));
// 遍历检查安装状态
for (let i = 0; i < this.pluginMarketData.length; i++) {
const plugin = this.pluginMarketData[i];
plugin.installed = installedRepos.has(plugin.repo?.toLowerCase()) || installedNames.has(plugin.name);
}
// 将已安装的插件移动到最后面
let installed = [];
let notInstalled = [];
for (let i = 0; i < this.pluginMarketData.length; i++) {
if (this.pluginMarketData[i].installed) {
installed.push(this.pluginMarketData[i]);
} else {
notInstalled.push(this.pluginMarketData[i]);
}
}
this.pluginMarketData = notInstalled.concat(installed);
}
},
}
</script>
+14 -282
View File
@@ -4,14 +4,12 @@ import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import ConsoleDisplayer from '@/components/shared/ConsoleDisplayer.vue';
import axios from 'axios';
import { useCommonStore } from '@/stores/common';
</script>
<template>
<v-row>
<v-alert style="margin: 16px" text="1. 如果因为网络问题安装失败,点击设置页选择 GitHub 加速地址。或前往仓库下载压缩包然后本地上传。" title="💡提示" type="info"
color="primary" variant="tonal">
</v-alert>
<v-col cols="12" md="12">
<div style="background-color: white; width: 100%; padding: 16px; border-radius: 10px;">
<div style="display: flex; align-items: center;">
@@ -82,96 +80,6 @@ import axios from 'axios';
</ExtensionCard>
</v-col>
<v-col cols="12" md="12" v-if="announcement">
<v-banner color="success" lines="one" :text="announcement" :stacked="false">
</v-banner>
</v-col>
<v-col cols="12" md="12">
<v-card>
<v-card-title class="d-flex align-center pe-2">
🧩 插件市场
<v-btn icon size="small" style="margin-left: 8px" variant="plain" @click="jumpToPluginMarket()">
<v-icon size="small">mdi-help</v-icon>
<v-tooltip activator="parent" location="start">
<span>
如无法显示请单击此按钮跳转至插件市场复制想安装插件对应的
`repo`
链接然后点击右下角 + 号安装或打开链接下载压缩包安装
</span>
</v-tooltip>
</v-btn>
<v-btn icon @click="isListView = !isListView" size="small" style="margin-left: auto;" variant="plain">
<v-icon>{{ isListView ? 'mdi-view-grid' : 'mdi-view-list' }}</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-text-field v-model="marketSearch" density="compact" label="Search" prepend-inner-icon="mdi-magnify"
variant="solo-filled" flat hide-details single-line></v-text-field>
</v-card-title>
<v-divider></v-divider>
<template v-if="isListView">
<v-col cols="12" md="12">
<v-data-table :headers="pluginMarketHeaders" :items="pluginMarketData" item-key="name"
v-model:search="marketSearch" :filter-keys="['name']">
<template v-slot:item.name="{ item }">
<span v-if="item?.repo"><a :href="item?.repo" style="color: #000; text-decoration:none">{{ item.name }}</a></span>
<span v-else>{{ item.name}}</span>
</template>
<template v-slot:item.author="{ item }">
<span v-if="item?.social_link"><a :href="item?.social_link">{{ item.author}}</a></span>
<span v-else>{{ item.author}}</span>
</template>
<template v-slot:item.tags="{ item }">
<span v-if="item.tags.length === 0"></span>
<v-chip v-for="tag in item.tags" :key="tag" color="primary" size="small">{{ tag }}</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-btn v-if="!item.installed" class="text-none mr-2" size="small" text="Read" variant="flat" border
@click="extension_url = item.repo; newExtension()">安装</v-btn>
<v-btn v-else class="text-none mr-2" size="small" text="Read" variant="flat" border disabled>已安装</v-btn>
</template>
</v-data-table>
</v-col>
</template>
<template v-else>
<v-row style="margin: 8px;">
<v-col cols="12" md="6" lg="3" v-for="plugin in filteredPluginMarketData">
<ExtensionCard :key="plugin.name" :title="plugin.name" :link="plugin.repo" style="margin-bottom: 4px;">
<div style="min-height: 130px; max-height: 130px; overflow: hidden;">
<p style="font-weight: bold;">By @{{ plugin.author }}</p>
{{ plugin.desc }}
</div>
<div class="d-flex align-center gap-2">
<v-btn v-if="!plugin.installed" class="text-none mr-2" size="small" text="Read" variant="flat"
border @click="extension_url = plugin.repo; newExtension()">安装</v-btn>
<v-btn v-else class="text-none mr-2" size="small" text="Read" variant="flat" border
disabled>已安装</v-btn>
</div>
</ExtensionCard>
</v-col>
</v-row>
</template>
</v-card>
</v-col>
<v-col style="margin-bottom: 16px;" cols="12" md="12">
<small><a href="https://astrbot.app/dev/plugin.html">插件开发文档</a></small> |
<small> <a href="https://github.com/Soulter/AstrBot_Plugins_Collection">提交插件仓库</a></small>
</v-col>
</v-row>
<v-dialog v-model="configDialog" width="1000">
@@ -200,49 +108,6 @@ import axios from 'axios';
</v-card>
</v-dialog>
<v-dialog v-model="dialog" width="700">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus" size="x-large" style="position: fixed; right: 52px; bottom: 52px;"
color="darkprimary">
</v-btn>
</template>
<v-card>
<v-card-title>
<span class="text-h5">安装插件</span>
</v-card-title>
<v-card-text>
<v-container>
<v-row>
<h3> GitHub 上在线下载</h3>
<v-col cols="12">
<small>请输入合法的 GitHub 仓库链接当前仅支持 GitHubhttps://github.com/Soulter/astrbot_plugin_aiocqhttp</small>
<v-text-field label="仓库链接" v-model="extension_url" variant="outlined" required></v-text-field>
</v-col>
</v-row>
<v-row>
<h3>从本机上传 .zip 压缩包</h3>
<v-col cols="12">
<small>请保证插件文件存在压缩包根目录中的第一个文件夹中即类似于从 GitHub 仓库页上下载的 Zip 压缩包的格式</small>
<v-file-input label="选择文件" v-model="upload_file" accept=".zip" outlined required></v-file-input>
</v-col>
</v-row>
</v-container>
<br>
<small>{{ status }}</small>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="blue-darken-1" variant="text" @click="dialog = false">
关闭
</v-btn>
<v-btn color="blue-darken-1" variant="text" :loading="loading_" @click="newExtension()">
安装
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="loadingDialog.show" width="700" persistent>
<v-card>
<v-card-title>
@@ -340,19 +205,16 @@ export default {
"data": [],
"message": ""
},
extension_url: "",
status: "",
dialog: false,
snack_message: "",
snack_show: false,
snack_success: "success",
loading_: false,
configDialog: false,
extension_config: {
"metadata": {},
"config": {}
},
upload_file: null,
pluginMarketData: [],
loadingDialog: {
show: false,
@@ -361,7 +223,6 @@ export default {
result: ""
},
announcement: "",
showPluginInfoDialog: false,
selectedPlugin: {},
plugin_handler_info_headers: [
@@ -370,42 +231,21 @@ export default {
{ title: '具体类型', key: 'type' },
{ title: '触发方式', key: 'cmd' },
],
isListView: true,
pluginMarketHeaders: [
{ title: '名称', key: 'name', maxWidth: '150px' },
{ title: '描述', key: 'desc', maxWidth: '250px' },
{ title: '作者', key: 'author', maxWidth: '60px' },
{ title: '标签', key: 'tags', maxWidth: '60px' },
{ title: '操作', key: 'actions', sortable: false }
],
marketSearch: "",
alreadyCheckUpdate: false
}
},
computed: {
filteredPluginMarketData() {
if (!this.marketSearch) {
return this.pluginMarketData;
}
const search = this.marketSearch.toLowerCase();
return this.pluginMarketData.filter(plugin =>
plugin.name.toLowerCase().includes(search)
);
commonStore: useCommonStore()
}
},
mounted() {
this.getExtensions();
this.fetchPluginCollection();
axios.get('https://api.soulter.top/astrbot-announcement-plugin-market').then((res) => {
let data = res.data.data;
this.announcement = data.text;
// 获取插件市场数据
this.commonStore.getPluginCollections().then((data) => {
this.pluginMarketData = data;
this.checkUpdate();
}).catch((err) => {
console.error("获取插件市场数据失败:", err);
});
},
methods: {
jumpToPluginMarket() {
window.open('https://soulter.github.io/AstrBot_Plugins_Collection/plugins.json', '_blank');
},
toast(message, success) {
this.snack_message = message;
this.snack_show = true;
@@ -432,16 +272,14 @@ export default {
getExtensions() {
axios.get('/api/plugin/get').then((res) => {
this.extension_data = res.data;
this.checkAlreadyInstalled();
this.checkUpdate()
});
},
checkUpdate() {
// 创建在线插件的map
const onlinePluginsMap = new Map();
const onlinePluginsNameMap = new Map();
// 将在线插件信息存储到map中
this.pluginMarketData.forEach(plugin => {
if (plugin.repo) {
@@ -449,7 +287,7 @@ export default {
}
onlinePluginsNameMap.set(plugin.name, plugin);
});
// 遍历本地插件列表
this.extension_data.data.forEach(extension => {
// 通过repo或name查找在线版本
@@ -457,77 +295,17 @@ export default {
const onlinePlugin = repoKey ? onlinePluginsMap.get(repoKey) : null;
const onlinePluginByName = onlinePluginsNameMap.get(extension.name);
const matchedPlugin = onlinePlugin || onlinePluginByName;
if (matchedPlugin) {
extension.online_version = matchedPlugin.version;
extension.has_update = extension.version !== matchedPlugin.version &&
matchedPlugin.version !== "未知";
extension.has_update = extension.version !== matchedPlugin.version &&
matchedPlugin.version !== "未知";
} else {
extension.has_update = false;
}
});
},
newExtension() {
if (this.extension_url === "" && this.upload_file === null) {
this.toast("请填写插件链接或上传插件文件", "error");
return;
}
if (this.extension_url !== "" && this.upload_file !== null) {
this.toast("请不要同时填写插件链接和上传插件文件", "error");
return;
}
this.loading_ = true;
this.loadingDialog.show = true;
if (this.upload_file !== null) {
this.toast("正在从文件安装插件", "primary");
const formData = new FormData();
formData.append('file', this.upload_file);
axios.post('/api/plugin/install-upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then((res) => {
this.loading_ = false;
if (res.data.status === "error") {
this.onLoadingDialogResult(2, res.data.message, -1);
return;
}
this.extension_data = res.data;
this.upload_file = "";
this.onLoadingDialogResult(1, res.data.message);
this.dialog = false;
this.$refs.wfr.check();
}).catch((err) => {
this.loading_ = false;
this.onLoadingDialogResult(2, err, -1);
});
return;
} else {
this.toast("正在从链接 " + this.extension_url + " 安装插件...", "primary");
axios.post('/api/plugin/install',
{
url: this.extension_url,
proxy: localStorage.getItem('selectedGitHubProxy') || ""
}).then((res) => {
this.loading_ = false;
if (res.data.status === "error") {
this.onLoadingDialogResult(2, res.data.message, -1);
return;
}
this.extension_data = res.data;
this.extension_url = "";
this.onLoadingDialogResult(1, res.data.message);
this.dialog = false;
this.$refs.wfr.check();
}).catch((err) => {
this.loading_ = false;
this.onLoadingDialogResult(2, err, -1);
});
}
},
uninstallExtension(extension_name) {
this.toast("正在卸载" + extension_name, "primary");
axios.post('/api/plugin/uninstall',
@@ -618,52 +396,6 @@ export default {
this.toast(err, "error");
});
},
fetchPluginCollection() {
axios.get('/api/plugin/market_list').then((res) => {
let data = []
this.pluginMarketDataOrigin = res.data.data;
for (let key in res.data.data) {
data.push({
"name": key,
"desc": res.data.data[key].desc,
"author": res.data.data[key].author,
"repo": res.data.data[key].repo,
"installed": false,
"version": res.data.data[key]?.version ? res.data.data[key].version : "未知",
"social_link": res.data.data[key]?.social_link,
"tags": res.data.data[key]?.tags ? res.data.data[key].tags : []
})
}
this.pluginMarketData = data;
this.checkAlreadyInstalled();
this.checkUpdate();
}).catch((err) => {
this.toast("获取插件市场数据失败: " + err, "error");
});
},
checkAlreadyInstalled() {
// 创建已安装插件的仓库和名称集合 统一格式
const installedRepos = new Set(this.extension_data.data.map(ext => ext.repo?.toLowerCase()));
const installedNames = new Set(this.extension_data.data.map(ext => ext.name));
// 遍历检查安装状态
for (let i = 0; i < this.pluginMarketData.length; i++) {
const plugin = this.pluginMarketData[i];
plugin.installed = installedRepos.has(plugin.repo?.toLowerCase()) || installedNames.has(plugin.name);
}
// 将已安装的插件移动到最后面
let installed = [];
let notInstalled = [];
for (let i = 0; i < this.pluginMarketData.length; i++) {
if (this.pluginMarketData[i].installed) {
installed.push(this.pluginMarketData[i]);
} else {
notInstalled.push(this.pluginMarketData[i]);
}
}
this.pluginMarketData = notInstalled.concat(installed);
},
showPluginInfo(plugin) {
this.selectedPlugin = plugin;
this.showPluginInfoDialog = true;